Loop stops executing if I omit Serial.println

Status
Not open for further replies.

rbwilcoxon11

New member
I have a hello blink program I wrote in assembler (trying to learn it) and found that the code will stop executing the main loop if I omit a serial println statement

Code:
int led = 13;
void setup() {
    Serial.begin(9600);
    pinMode(led, OUTPUT);
}

void loop() {
    // Serial.println("Why do I need this?");
    asm volatile(
    "start:\n"
    "bl led_on\n"
    "bl delay\n"
    "bl led_off\n"
    "bl delay\n"
    "b  done\n"
    "led_on:\n"
    "ldr r6, = 0x400FF080\n"
    "ldr r0, = 0x20\n"
    "str r0, [r6]\n"
    "mov pc, LR\n"
    "led_off:\n"
    "ldr r6, = 0x400FF080\n"
    "ldr r0, = 0x0\n"
    "str r0, [r6]\n"
    "mov pc, LR\n"
    "delay:\n"
    "ldr r1, = 0x1FFFFFF\n"
    "delay_loop:\n"
    "sub r1, r1, #1\n"
    "cmp r1, #0\n"
    "bne delay_loop\n"
    "mov pc, LR\n"
    "done:\n"
    );
}

Any ideas on what the reason might be? Arduino version 1.8.8, Teenyduino version 1.8.8.
 
Perhaps looking at the .ASM (sketch.lst in temp build directory) the With and the Without the .println() version output from the compiler will show - maybe something is missing in the loop() entry?

Write the equivalent loop() code in "C" first and see that work - then perhaps again visit the .lst file to see what is different.
 
Didn't know that I could access the .ASM, nice!

So I don't know assembler, and don't really know whats going on... Intuition tells me that without the Serial.println that the compiler optimizes something out of loop that lets the program return to the Arduino main loop? Looking at the .ASM though, I don't think thats the case. Here is what I think is the interesting bit

.ASM with WITHOUT the Serial.println
Code:
0000046c <setup>:

int led = 13;
void setup() {
    Serial.begin(9600);
    pinMode(led, OUTPUT);
     46c:	4b02      	ldr	r3, [pc, #8]	; (478 <setup+0xc>)
     46e:	2101      	movs	r1, #1
     470:	7818      	ldrb	r0, [r3, #0]
     472:	f000 b875 	b.w	560 <pinMode>
     476:	bf00      	nop
     478:	1fff8720 	.word	0x1fff8720

0000047c <loop>:
    "sub r1, r1, #1\n"
    "cmp r1, #0\n"
    "bne delay_loop\n"
    "mov pc, LR\n"
    "done:\n"
    );
     47c:	f000 f807 	bl	48e <led_on>
     480:	f000 f80d 	bl	49e <delay>
     484:	f000 f807 	bl	496 <led_off>
     488:	f000 f809 	bl	49e <delay>
     48c:	e00e      	b.n	4ac <done>

0000048e <led_on>:
     48e:	4e08      	ldr	r6, [pc, #32]	; (4b0 <done+0x4>)
     490:	2020      	movs	r0, #32
     492:	6030      	str	r0, [r6, #0]
     494:	46f7      	mov	pc, lr

00000496 <led_off>:
     496:	4e06      	ldr	r6, [pc, #24]	; (4b0 <done+0x4>)
     498:	2000      	movs	r0, #0
     49a:	6030      	str	r0, [r6, #0]
     49c:	46f7      	mov	pc, lr

0000049e <delay>:
     49e:	f06f 417e 	mvn.w	r1, #4261412864	; 0xfe000000

000004a2 <delay_loop>:
     4a2:	f1a1 0101 	sub.w	r1, r1, #1
     4a6:	2900      	cmp	r1, #0
     4a8:	d1fb      	bne.n	4a2 <delay_loop>
     4aa:	46f7      	mov	pc, lr

000004ac <done>:
     4ac:	4770      	bx	lr
     4ae:	0000      	.short	0x0000
     4b0:	400ff080 	.word	0x400ff080

000004b4 <main>:
 */

#include <Arduino.h>

extern "C" int main(void)
{
     4b4:	b508      	push	{r3, lr}
	}


#else
	// Arduino's main() function just calls setup() and loop()....
	setup();
     4b6:	f7ff ffd9 	bl	46c <setup>
	while (1) {
		loop();
     4ba:	f7ff ffdf 	bl	47c <loop>
		yield();
     4be:	f001 f805 	bl	14cc <yield>
     4c2:	e7fa      	b.n	4ba <main+0x6>

.ASM with WITH the Serial.println
Code:
0000046c <setup>:

int led = 13;
void setup() {
    Serial.begin(9600);
    pinMode(led, OUTPUT);
     46c: 4b02        ldr r3, [pc, #8]  ; (478 <setup+0xc>)
     46e: 2101        movs  r1, #1
     470: 7818        ldrb  r0, [r3, #0]
     472: f000 b8fd   b.w 670 <pinMode>
     476: bf00        nop
     478: 1fff8720  .word 0x1fff8720

0000047c <loop>:
}

void loop() {
     47c: b508        push  {r3, lr}
        virtual int read() { return usb_serial_getchar(); }
        virtual int peek() { return usb_serial_peekchar(); }
        virtual void flush() { usb_serial_flush_output(); }  // TODO: actually wait for data to leave USB...
        virtual void clear(void) { usb_serial_flush_input(); }
        virtual size_t write(uint8_t c) { return usb_serial_putchar(c); }
        virtual size_t write(const uint8_t *buffer, size_t size) { return usb_serial_write(buffer, size); }
     47e: 2113        movs  r1, #19
     480: 480f        ldr r0, [pc, #60] ; (4c0 <done+0x4>)
     482: f001 f929   bl  16d8 <usb_serial_write>
  size_t print(double n, int digits = 2)    { return printFloat(n, digits); }
  size_t print(const Printable &obj)    { return obj.printTo(*this); }
  size_t println(void);
  size_t println(const String &s)     { return print(s) + println(); }
  size_t println(char c)        { return print(c) + println(); }
  size_t println(const char s[])      { return print(s) + println(); }
     486: 480f        ldr r0, [pc, #60] ; (4c4 <done+0x8>)
     488: f000 f820   bl  4cc <Print::println()>

0000048c <start>:
    "sub r1, r1, #1\n"
    "cmp r1, #0\n"
    "bne delay_loop\n"
    "mov pc, LR\n"
    "done:\n"
    );
     48c: f000 f807   bl  49e <led_on>
     490: f000 f80d   bl  4ae <delay>
     494: f000 f807   bl  4a6 <led_off>
     498: f000 f809   bl  4ae <delay>
     49c: e00e        b.n 4bc <done>

0000049e <led_on>:
     49e: 4e0a        ldr r6, [pc, #40] ; (4c8 <done+0xc>)
     4a0: 2020        movs  r0, #32
     4a2: 6030        str r0, [r6, #0]
     4a4: 46f7        mov pc, lr

000004a6 <led_off>:
     4a6: 4e08        ldr r6, [pc, #32] ; (4c8 <done+0xc>)
     4a8: 2000        movs  r0, #0
     4aa: 6030        str r0, [r6, #0]
     4ac: 46f7        mov pc, lr

000004ae <delay>:
     4ae: f06f 417e   mvn.w r1, #4261412864 ; 0xfe000000

000004b2 <delay_loop>:
     4b2: f1a1 0101   sub.w r1, r1, #1
     4b6: 2900        cmp r1, #0
     4b8: d1fb        bne.n 4b2 <delay_loop>
     4ba: 46f7        mov pc, lr

000004bc <done>:
     4bc: bd08        pop {r3, pc}
     4be: bf00        nop
     4c0: 0000298c  .word 0x0000298c
     4c4: 1fff8728  .word 0x1fff8728
     4c8: 400ff080  .word 0x400ff080

000004cc <Print::println()>:
  return printNumber(n, 10, sign);
}


size_t Print::println(void)
{
     4cc: b500        push  {lr}
  uint8_t buf[2]={'\r', '\n'};
     4ce: 4a06        ldr r2, [pc, #24] ; (4e8 <Print::println()+0x1c>)
  return write(buf, 2);
     4d0: 6803        ldr r3, [r0, #0]
}


size_t Print::println(void)
{
  uint8_t buf[2]={'\r', '\n'};
     4d2: 8812        ldrh  r2, [r2, #0]
  return write(buf, 2);
     4d4: 685b        ldr r3, [r3, #4]
  return printNumber(n, 10, sign);
}


size_t Print::println(void)
{
     4d6: b083        sub sp, #12
  uint8_t buf[2]={'\r', '\n'};
  return write(buf, 2);
     4d8: a901        add r1, sp, #4
}


size_t Print::println(void)
{
  uint8_t buf[2]={'\r', '\n'};
     4da: f8ad 2004   strh.w  r2, [sp, #4]
  return write(buf, 2);
     4de: 2202        movs  r2, #2
     4e0: 4798        blx r3
}
     4e2: b003        add sp, #12
     4e4: f85d fb04   ldr.w pc, [sp], #4
     4e8: 000029a0  .word 0x000029a0

000004ec <main>:
 */

#include <Arduino.h>

extern "C" int main(void)
{
     4ec: b508        push  {r3, lr}
  }


#else
  // Arduino's main() function just calls setup() and loop()....
  setup();
     4ee: f7ff ffbd   bl  46c <setup>
  while (1) {
    loop();
     4f2: f7ff ffc3   bl  47c <loop>
    yield();
     4f6: f001 f9f3   bl  18e0 <yield>
     4fa: e7fa        b.n 4f2 <main+0x6>

I can include more if needed!
 
My starting point would be :: Write the equivalent loop() code in "C" first and see that work - then perhaps again visit the .lst file to see what is different.

That is write in C only what you want the ASM code to do and see that work:
led_on\n"
delay\n"
led_off\n"
delay\n"
return

Use Goto or function()'s as appropriate - see what that ASM looks like and go from there
 
It fails because you're clobbering registers, but you're not telling the compiler. If the surrounding code depends on those registers, as is the case when you omit the Serial.print call, then you'll almost certainly crash or cause the compiled code to do wrong things when it uses those registers you overwrote.

Here's a fixed copy of your code.

Code:
int led = 13;
void setup() {
    Serial.begin(9600);
    pinMode(led, OUTPUT);
}

void loop() {
    // nothing needed here
    asm volatile(
    "start:\n"
    "bl led_on\n"
    "bl delay\n"
    "bl led_off\n"
    "bl delay\n"
    "b  done\n"
    "led_on:\n"
    "ldr r6, = 0x400FF080\n"
    "ldr r0, = 0x20\n"
    "str r0, [r6]\n"
    "mov pc, LR\n"
    "led_off:\n"
    "ldr r6, = 0x400FF080\n"
    "ldr r0, = 0x0\n"
    "str r0, [r6]\n"
    "mov pc, LR\n"
    "delay:\n"
    "ldr r1, = 0x1FFFFFF\n"
    "delay_loop:\n"
    "sub r1, r1, #1\n"
    "cmp r1, #0\n"
    "bne delay_loop\n"
    "mov pc, LR\n"
    "done:\n"
    : : : "r0","r1,"r6","lr"
    );
}

Notice the line near the end. This is where you tell the compiler which registers you have clobbered.

In this case, the LR register is the key. When loop() is called, LR has the return address. If you call other functions like Serial.print(), the compiler will save LR somehow, then while Serial.print() runs LR is the return address to get back to loop. Your code was able to work only because the compiler happened to be saving LR, so it didn't matter when you clobbered it. When you don't call any other functions, usually the compiler will leave LR alone, then use "BX LR" to return. That's what it was doing, but your code was changed LR and the compiler had no way of knowing. You got 1 blink, but then when loop() tried to return to main(), it couldn't because LR had the wrong address. The compiler assumed LR still had the address where loop should return.

In this case, R0, R1 & R6 didn't matter. But if you put this ASM into any larger program, they almost certainly will. You can expect all sorts of strange results if the compiler happened to be storing something important in R0 or R1 or R6.

Here's an excellent guide about writing inline asm.

http://www.ethernut.de/en/documents/arm-inline-asm.html

When your code does pretty much anything with any registers, you need to use the complex syntax with three ":" colons, so the compiler is aware of which registers you've used. It's very good about working around whatever you do, but only if you specify things properly. If you omit those 3 specs, the compiler assumes you didn't touch any registers or memory or conditional status flags. That's pretty much only useful for adding asm("nop") instructions. For almost anything beyond NOP, you need to let the compiler know what registers you use.
 
The other thing you'll see in that guide is register macros. For ASM code that integrates well with surrounding C & C++, the idea is to let the compiler choose which registers your ASM code will actually use. If you hard-code for R0, R1 & R6, and properly specify them, the compiler will make those registers available for your code to use. But what if R6 isn't the best choice? What it R1 has something important and your fixed use of R1 forces the compiler to do a play musical chairs with the registers. The macro approach lets the compiler's optimizer choose which registers are most optimal, balancing your code's needs with the register usage of the surrounding code. It's pretty awesome, but only happens if you use the special (complex) macro syntax for the registers.
 
Good info Paul. My starting point would have failed given C tracks the registers in use without the explicit ASM clobber list showing in the .lst output file.
 
Status
Not open for further replies.
Back
Top