Search code examples
gccglobal-variablesavravr-gcc

Unexpected global variable read result in C++ using avr-gcc for (local variable access is as expected)


I am getting unexpected global variable read results when compiling the following code in avr-gcc 4.6.2 for ATmega328:

#include <avr/io.h>
#include <util/delay.h>
    
#define LED_PORT            PORTD
#define LED_BIT             7
#define LED_DDR             DDRD
    
uint8_t latchingFlag;
    
int main() {
    LED_DDR = 0xFF;
    
    for (;;) {
        latchingFlag=1;
        
        if (latchingFlag==0) {
            LED_PORT ^= 1<<LED_BIT; // Toggle the LED
            _delay_ms(100);         // Delay
            latchingFlag = 1;
        }
    }
}

This is the entire code. I would expect the LED toggling to never execute, seeing as latchingFlag is set to 1, however the LED blinks continuously. If latchingFlag is declared local to main() the program executes as expected: the LED never blinks.

The disassembled code doesn't reveal any gotchas that I can see, here's the disassembly of the main loop of the version using the global variable (with the delay routine call commented out; same behavior)

59                  .L4:
27:main.cpp      ****   for (;;) {
60                      .loc 1 27 0
61 0026 0000            nop
62                  .L3:
28:main.cpp      ****   latchingFlag=1;
63                      .loc 1 28 0
64 0028 81E0            ldi r24,lo8(1)
65 002a 8093 0000       sts latchingFlag,r24
29:main.cpp      ****   if (latchingFlag==0) {
66                      .loc 1 29 0
67 002e 8091 0000       lds r24,latchingFlag
68 0032 8823            tst r24
69 0034 01F4            brne .L4
30:main.cpp      ****   LED_PORT ^= 1<<LED_BIT; // Toggle the LED
70                      .loc 1 30 0
71 0036 8BE2            ldi r24,lo8(43)
72 0038 90E0            ldi r25,hi8(43)
73 003a 2BE2            ldi r18,lo8(43)
74 003c 30E0            ldi r19,hi8(43)
75 003e F901            movw r30,r18
76 0040 3081            ld r19,Z
77 0042 20E8            ldi r18,lo8(-128)
78 0044 2327            eor r18,r19
79 0046 FC01            movw r30,r24
80 0048 2083            st Z,r18
31:main.cpp      ****   latchingFlag = 1;
81                      .loc 1 31 0
82 004a 81E0            ldi r24,lo8(1)
83 004c 8093 0000       sts latchingFlag,r24
27:main.cpp      ****   for (;;) {
84                      .loc 1 27 0
85 0050 00C0            rjmp .L4

The lines 71-80 are responsible for port access: according to the datasheet, PORTD is at address 0x2B, which is decimal 43 (cf. lines 71-74).

The only difference between local/global declaration of the latchingFlag variable is how latchingFlag is accessed: the global variable version uses sts (store direct to data space) and lds (load direct from data space) to access latchingFlag, whereas the local variable version uses ldd (Load Indirect from Data Space to Register) and std (Store Indirect From Register to Data Space) using register Y as the address register (which can be used as a stack pointer, by avr-gcc AFAIK). Here are the relevant lines from the disassembly:

      63 002c 8983              std Y+1,r24
    
      65 002e 8981              ldd r24,Y+1
    
      81 004a 8983              std Y+1,r24

The global version also has latchingFlag in the .bss section. I am really not what to attribute the different global vs. local variable behavior to. Here's the avr-gcc command-line (notice -O0):

/usr/local/avr/bin/avr-gcc \
    -I. -g -mmcu=atmega328p -O0 \
    -fpack-struct \                                                 
    -fshort-enums \                                         
    -funsigned-bitfields \                                        
    -funsigned-char \                                                 
    -D CLOCK_SRC=8000000UL \
    -D CLOCK_PRESCALE=8UL \
    -D F_CPU="(CLOCK_SRC/CLOCK_PRESCALE)" \
    -Wall \
    -ffunction-sections \
    -fdata-sections \
    -fno-exceptions \
    -Wa,-ahlms=obj/main.lst \
    -Wno-uninitialized \
    -c main.cpp -o obj/main.o

With -Os compiler flags the loop is gone from the disassembly, but can be forced to be there again if latchingFlag is declared volatile, in which case the unexpected persists for me.


Solution

  • Egor Skriptunoff suggestion is almost exactly right: the SRAM variable is mapped to the wrong memory address. The latchingFlag variable is not at 0x0100 address, which is the first valid SRAM address, but is mapped to 0x060, overlapping the WDTCSR register. This can be seen in the disassembly lines like the following one:

    lds r24, 0x0060
    

    THis line is supposed to load the value of latchingFlag from SRAM, and we can see that location 0x060 is used instead of 0x100.

    The problem has to with a bug in the binutils which two conditions are met:

    • The linker is invoked with --gc-sections flag (compiler options: -Wl,--gc-sections) to save code space
    • None of your SRAM variables are initialized (i.e. initialized to non-zero values)

    When both of these conditions are met, the .data section gets removed. When the .data section is missing, the SRAM variables start at address 0x060 instead of 0x100.

    One solution is to reinstall binutils: the current versions have this bug fixed. Another solution is to edit your linker scripts: on Ubuntu this is probably in /usr/lib/ldscripts. For ATmega168/328 the script that needs to be edited is avr5.x, but you should really edit all them, otherwise you could run into this bug on other AVR platforms. The change that needs to be made is the following one:

       .data   : AT (ADDR (.text) + SIZEOF (.text))
       {
          PROVIDE (__data_start = .) ;
    -     *(.data)
    +     KEEP(*(.data))
    

    So replace the line *(.data) with KEEP(*(.data)). This ensures that the .data section is not discarded, and consequently the SRAM variable addresses start at 0x0100