Skip to content

Instantly share code, notes, and snippets.

@sprintersb
Last active January 28, 2024 16:51
Show Gist options
  • Save sprintersb/7e538928f5b481961d31458d2e5a402d to your computer and use it in GitHub Desktop.
Save sprintersb/7e538928f5b481961d31458d2e5a402d to your computer and use it in GitHub Desktop.
avr-gcc: Locate .rodata in Flash for AVR Devices like AVR64 and AVR128

avr-gcc: Locate .rodata in Flash for AVR Devices like AVR64 and AVR128

Abstract: Modern AVR microcontrollers from families AVR64 and AVR128, like AVR128DA32, map a 32 KiB portion of their flash memory into the RAM address space. This makes it possible to locate read-only data in flash memory, and not in RAM like it is currently the case with GNU Tools for AVR.

Locating .rodata in flash can be done without touching or extending the tools, and by means of the following steps:

  1. Specify the start address of the .rodata segment.
  2. Tell the AVR to use that segment for .rodata.
  3. Locate .rodata in the specified segment.
  4. Adjust the build process.
  5. Check that it all works as expected.

Resources

Part 3 requires a custom linker description file like

A C file that accomplishes parts 1 and 2: Specify the start address for .rodata and initialize the AVR to use it

  • rodata-test.c   Compile and link with, e.g.
    > avr-gcc rodata-test.c -o rodata-test.elf -Os -mmcu=avr128da32 -T avrxmega4-rodata.ld
    

1. Specifying the Flash Segment for .rodata

In order to specify the start address of the 32 KiB rodata segment, we define a symbol __RODATA_FLASH_START__. Symbols are entities used and defined by the linker. The value of the symbol must be a multiple of 32 KiB. An easy way is to define it on the command line by linking with:

> avr-gcc ... -Wl,--defsym,__RODATA_FLASH_START__=96K

We can also define it in a C/C++ module by means of the AVR specific address attribute. Notice that address does not work as expected with -fdata-sections and -fno-common (which is the default since v10) due to PR112952.

#include <avr/io.h> // FLASHEND

// Define symbol SYM to value VAL.
#define DEFINE_SYMBOL(SYM, VAL) \
    __attribute__((__address__((VAL))) char SYM

// The byte address of the beginning of the rodata segment.
// We use the last 32 KiB segment at the end of flash memory.
DEFINE_SYMBOL (__RODATA_FLASH_START__, 1ul + FLASHEND - 32 * 1024ul);

It can also be defined in an assembly module:

#include <avr/io.h> // FLASHEND

.global __RODATA_FLASH_START__
__RODATA_FLASH_START__ = FLASHEND+1 - 32*1024

Use extension .sx or .S, or assemble with, say avr-gcc -x assembler-with-cpp file.asm ...

2. Telling the AVR to use the specified .rodata Segment

Which 32 KiB flash segment is visble in the RAM address space can be set in bitfield FLMAP in special function register NVMCTRL_CTRLB. The function below will be implicitly executed by the startup code, and it must not be call by hand:

__attribute__((__naked__, __section__(".init3")))
__attribute__((__used__, __unused__, __no_instrument_function__))
__attribute__((__error__("Don't call this function by hand!")))
static void init_FLMAP (void)
{
#if (defined NVMCTRL_CTRLB                      \
     && defined NVMCTRL_FLMAP_0_bp              \
     && defined NVMCTRL_FLMAP_1_bp)
    uint8_t tmp, ctrlb;
    __asm volatile ("lds %[ctrlb], %[ctrlb_addr]"              "\n\t"
                    // Bits 4 and 5 of __rodata_flash_start_lsr11 hold FLMAP.
#if NVMCTRL_FLMAP_0_bp == 4 && NVMCTRL_FLMAP_1_bp == 5
                    "cbr %[ctrlb], 0x30"                       "\n\t"
                    "ori %[ctrlb], __rodata_flash_start_lsr11" "\n\t"
#else
                    "ldi %0, __rodata_flash_start_lsr11"       "\n\t"
                    "bst %0, 4"                                "\n\t"
                    "bld %[ctrlb], %[bpos0]"                   "\n\t"
                    "bst %0, 5"                                "\n\t"
                    "bld %[ctrlb], %[bpos1]"                   "\n\t"
#endif
                    "sts %[ctrlb_addr], %[ctrlb]"
                    : "=d" (tmp), [ctrlb] "=d" (ctrlb)
                    : [bpos0] "n" (NVMCTRL_FLMAP_0_bp),
                      [bpos1] "n" (NVMCTRL_FLMAP_1_bp),
                      [ctrlb_addr] "n" (& NVMCTRL_CTRLB));
#endif
}

As the function is not called explicitly

  • it is marked as used so that the compiler won't throw it away, and
  • it is marked as unused so that the compiler won't diagnose that the function is never called and seems to be unused.

Symbol __rodata_flash_start_lsr11 is defined in the linker description file as explained in the next section. It's defined to __RODATA_FLASH_START__ >> 11.

3. Locating .rodata in the specified Segment

This is the tricky part. It requires a custom linker description file. For brevity, we just explain which changes are needed. To that end, start by copying from install avr/lib/ldscripts/. The default linker description file for AVR64* is avrxmega2.x, and the one for AVR128* is avrxmega4.x.

Then provide the adjusted file to the linker by means of, say

> avr-gcc ... -T avrxmega4-rodata.ld

The following adjustments have to be made:

  1. Introduce a new MEMORY region rodata.
  2. Remove the old placement of .rodata input sections in data.
  3. Locate .rodata at the specified flash address.

The result will look something like avrxmega2-rodata.ld or avrxmega4-rodata.ld.

3.1 Introducing a new MEMORY Region for .rodata

Right at the top of the linker description, add a new MEMORY region rodata and some symbols to avoid magic numbers.

__RODATA_VMA__ = 0xa00000;
__RODATA_LDS_OFFSET__ = 0x8000;
__RODATA_ORIGIN__ = __RODATA_VMA__ +__RODATA_LDS_OFFSET__;
__RODATA_LENGTH__ = 32K;
MEMORY
{
  ...
  rodata (r!x) : ORIGIN = __RODATA_ORIGIN__, LENGTH = __RODATA_LENGTH__
}

While introducing a new memory region is not strictly required, it yields better diagnostics in the case when something goes wrong, for example when there is so much program code that .rodata does not fit into (the remaining part of) the chosen flash segment.

We use a Virtual Memory Address (VMA, the address the code will be using) of 0xa00000 which is required in order to linearize memory. This works similar to other regions like data with a VMA of somewhere above 0x800000.

3.2 Removing the old Placement of .rodata

The rule for output section .data reads something like:

.data :
  {
    ...
    *(.rodata)  /* We need to include .rodata here if gcc is used */
    *(.rodata*) /* with -fdata-sections.  */
    *(.gnu.linkonce.r*)
    ...
  }  > data AT> text

Remove the three lines for the three rodata input sections .rodata, .rodata* and *.gnu.linkonce.r*.

3.3 Locating .rodata at the specified Flash Address

Somewhere after handling of the RAM sections, for example prior to the .eeprom output section, insert new symbol definitions and rules for output section .rodata:

/* User can specify position of .rodata in flash (LMA) by supplying __RODATA_FLASH_START__.
   We'd prefer .rodata at the very flashend per default, but theres's no
   reliable way to get the flash size here, hence default start at 32K.  */
__RODATA_FLASH_START__ = DEFINED(__RODATA_FLASH_START__) ? __RODATA_FLASH_START__ : 32K;
ASSERT (__RODATA_FLASH_START__ % 32K == 0,
        "__RODATA_FLASH_START__ must be a multiple of 32KiB")

/* Actual .rodata LMA.  */
__rodata_flash_start = MAX (__data_load_end, __RODATA_FLASH_START__);

/* This might be convenient to initialize NVMCTRL_CTRLB.FLMAP.  */
__rodata_flash_start_lsr11 = __RODATA_FLASH_START__ >> 11;

/* .rodata VMA.  This is what LDS etc. will use.   */
__rodata_start = __RODATA_ORIGIN__ + __rodata_flash_start - __RODATA_FLASH_START__;

.rodata ABSOLUTE(__rodata_start) : AT (ABSOLUTE(__rodata_flash_start))
{
    *(.rodata)
    *(.rodata*)
    *(.gnu.linkonce.r*)
    __rodata_end = ABSOLUTE(.);
} > rodata

__rodata_flash_end = __rodata_flash_start + __rodata_end - __rodata_start;

4. Adjusting the Build Process

  • Provide your customized linker-description file to the linker by means of
    > avr-gcc ... -T <your-ld-script>
    
  • If you chose to specify the flash segment on the command line, link with
    > avr-gcc ... -Wl,--defsym,__RODATA_FLASH_START__=<byte-address>
    
  • Even though modern flash tools like avrdude support flashing ELF files, some people still prefer to use intermediate dumb formats like IntelHEX. When you create such intermediate formats for the flash memory with tools like avr-objcopy, make sure that the .rodata output section is part of the intermediate format flash file by means of avr-objcopy -j .rodata ... or similar.

5. Checking that it all works

Some static checks can be performed. In the following example, the program code and initializers for data extend to 0x1929b and into the rodata segment from 0x18000 to 0x1ffff. Thus, rodata starts at 0x1929c which is all fine because there are only 2 bytes of rodata. Notice how the VMA aligns with the LMA.

> avr-objdump -h ... main.elf
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         0000001c  00804000  00019280  00019334  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00019280  00000000  00000000  000000b4  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  3 .rodata       00000002  00a0929c  0001929c  00019350  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

Symbol values are also shown in the map file, like with

avr-gcc ... -Wl,-Map,main.map

This works even in many cases where the linker terminates with an error, and when no disiassembly resp. ELF file is available.

The disassembly of the startup code will show the setup of NVMCTRL_CTRLB.FLMAP:

0000012e <init_FLMAP>:
     12e:	90 91 01 10 	lds    r25, 0x1001
     132:	9f 7c       	andi   r25, 0xCF
     134:	90 63       	ori    r25, 0x30
     136:	90 93 01 10 	sts    0x1001, r25

The values of the symbols can be displayed with

> avr-nm main.elf | grep -i rodata
00018000 A __RODATA_FLASH_START__
00008000 A __RODATA_LDS_OFFSET__
00008000 A __RODATA_LENGTH__
00a08000 A __RODATA_ORIGIN__
...

FAQ

What about Devices like AVR16* and AVR32*?

For devices from the avrxmega3 and avrtiny families, .rodata is located in flash. This works without any preparations or command line options. Features like __flash, pgm_read_xxx or PROGMEM are not needed.

These devices see the complete program memory in the RAM address space. They have a combined size of program memory and RAM of no more than 64 KiB:

  • AVR16DD14, AVR16DD20, AVR16DD28, AVR16DD32, AVR16EA28, AVR16EA32, AVR16EA48, AVR16EB14, AVR16EB20, AVR16EB28, AVR16EB32
  • AVR32DA28, AVR32DA32, AVR32DA48, AVR32DB28, AVR32DB32, AVR32DB48, AVR32DD14, AVR32DD20, AVR32DD28, AVR32DD32, AVR32EA28, AVR32EA32, AVR32EA48
  • 0-series: ATtiny202, ATtiny204, ATtiny402, ATtiny404, ATtiny406, ATtiny804, ATtiny806, ATtiny807, ATtiny1604, ATtiny1606, ATtiny1607, ATmega808, ATmega809, ATmega1608, ATmega1609, ATmega3208, ATmega3209, ATmega4808, ATmega4809
  • 1-series: ATtiny212, ATtiny214, ATtiny412, ATtiny414, ATtiny416, ATtiny417, ATtiny814, ATtiny816, ATtiny817, ATtiny1614, ATtiny1616, ATtiny1617, ATtiny3214, ATtiny3216, ATtiny3217
  • 2-series: ATtiny424, ATtiny426, ATtiny427, ATtiny824, ATtiny826, ATtiny827, ATtiny1624, ATtiny1626, ATtiny1627, ATtiny3224, ATtiny3226, ATtiny3227
  • Reduced Tiny: ATtiny4, ATtiny5, ATtiny9, ATtiny10, ATtiny102, ATtiny104, ATtiny20, ATtiny40

How to fix "address of section .rodata is not within region rodata"?

Error messages from the linker like

.../avr/bin/ld: address 0xa1995a of main.elf section `.rodata' is not within region `rodata'

means that the read-only data in input sections like .rodata do not fit (completely) into the rodata memory region.

There is too much program code + read-only data, and the following fixes are possible:

  • Locate the rodata region at a higher address by adjusting the value of symbol __RODATA_FLASH_START__.
  • If that does not help, there might be too much code as to fit into the AVR's flash memory. Consider measures that reduce code size.
  • Orphan sections or other custom sections introduced in the linker description might bump the location counter, so that memory below that section is unused. Consider re-organizing your code / data, e.g. by turning orphan sections into proper sections that allow exact specification of their location.

Why is .rodata located in RAM to begin with?

The address space of AVR microcontrollers is not linear, in partticular

  • in order to access RAM locations, instructions like LD, LDD, LDS and ST, STD, STS must be used, whereas
  • in order to access program memory (flash), instructions like LPM or ELPM have to be used.

For example, address 0x100 might be located in RAM, or it might be located in flash. You have to know which one it is in order to write correct code.

The problem in programs is that when a function gets a const address, like the source operand of memcpy you don't know which is which because it is completely fine to pass a pointer to RAM as, say, const char*. This means that the function cannot know which instruction to use. The following solutions are possible:

  • Put .rodata into RAM, so we know which load instruction to use. This is what avr-gcc implements.
  • Let the user decide how an address is accessed. This is how macros like pgm_read_byte() from AVR-LibC are used: The user must know whether to use that accessor or not.
  • Encode the address space somehow in the address. This is very expensive because the decision which load instuction to use can only be taken at run-time. avr-gcc uses this approach for address space __memx but not for data in the generic address space.

There are devices like ATmega4808 or AVR32DA28 from families avrxmega3 or avrtiny that see the complete flash memory in the RAM address space. On such devices, .rodata is located in flash and it works like on any reasonable hardware: There is no need for address spaces like __flash or special accessors like pgm_read_xxx, and all reads can use load intructions for RAM. avrtiny doesn't even support LPM.

For devices like AVR64DA32 or AVR128DB64 this is more complicated, because

  • RAM + flash occupies more than 64 KiB of addresses, so that not all of the flash memory can be visible in the RAM address space. Only a 32 KiB portion is visible.
  • There is no canonical placement of .rodata in the flash region, and it might rival with other custom sections.

Why is all this not Part of the GNU Toolchain?

Because nobody integrated it.

How to integrate this into the GNU Tools for AVR?

Changes to Binutils

  • As a new linker description file is required, a new emulation like avrxmega8 is needed.

Changes to avr-gcc

  • The configure script has to be extended. The following points depend on whether Binutils support the new emulation(s) or not:
  • In case, provide a new build-in macro so code like AVR-LibC can consume it.
  • The new emulation(s) must be supported as command line options, e.g. -mmcu=avrxmega8.
  • These new options must be multilib options. Map them to their old couterpart(s) in multilib selection so that no new multilib variants are needed (we already have so many of them).
  • The devices in question have to be assigned to the appropriate multilib set.
  • Scripts have to cope with the new flexibility:
  • .rodata should not trigger __do_copy_data: Adjust avr_need_copy_data_p in avr.cc.
  • Adjust the documentation.

Changes to AVR-LibC

  • The configure script has to be extended. The following points depend on whether avr-gcc supports the new option(s) or not:
  • Define a weak symbol __RODATA_FLASH_START__ that defaults to the last 32 KiB segment of flash.
  • The startup code has to set up NVMCTRL_CTRLB.FLMAP according to __RODATA_FLASH_START__.
  • Decide what to do with NVMCTRL_CTRLB.FLMAPLOCK.
  • Adjust the documentation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment