AVR - storing unused variable at fixed memory location

Hey guys,
I’m using platformio/vscode to create some code for a project that uses an AVR atmega328p (in a standalone circuit, not an arduino).
I want to include some information at the end of the final hex file, before the bootloader section, for my own reference.

In one of the source files I have created this variable, which is an array filled with data from other macros (such as compile date/time):

volatile const uint8_t signature[16] PROGMEM __attribute__((section(".calsig"))) = 
{
    0x99,
    CHECKSUM_1,
    CHECKSUM_2,
    MAJOR_VERSION,
    MINOR_VERSION,
    (GET_COMPILE_YEAR & 0xFF),
    ((GET_COMPILE_YEAR >> 8) & 0xFF),
    GET_COMPILE_MONTH,
    GET_COMPILE_DAY,
    GET_COMPILE_HOUR,
    GET_COMPILE_MINUTE,
    GET_COMPILE_SECOND,
    0,
    0,
    0,
    0
};

and this is my platformio.ini file:

[env:atmega328p]
platform = atmelavr
;framework = arduino
board = uno

board_build.mcu = atmega328p
board_build.f_cpu = 16000000L
build_flags =
    ;0x0000 to 0x7DFF is normal flash. 0x7E00 to 0x7FFF is bootloader
    -Wl,--section-start=.calsig=0x7DF0 ; 1 byte //7df0

I’m hoping to have the last 16 bytes of the hex file (before the bootloader section) filled with this data. However when I compile it, the 16 byte “signature” array isn’t even included in the hex file at all (I’m assuming it optimized out, even though I’ve made it volatile?).
If I reference a few of the values from it in one of my other functions (which I don’t have any need to do), it is then included, however its at the wrong address (I’ve found its placed at 0x0068).

I’ve tried adding “-Wl,–undefined=calsig” to platformio.ini build_flags and it made no difference.
I’ve tried “attribute((used,section(”.cal_signature")))" and “attribute((unused,section(”.cal_signature")))" and it also made no difference.

How do I tell the compiler/linker to simply place this data at this location no matter what?

Thanks in advance

While I did find the topic https://www.avrfreaks.net/forum/how-do-i-specify-address-variable-gcc for data variables (in SRAM), it seemed to not work for constant program memory. That post also seemed to rely on an already existing section (.noinit) the start of which is then shifted with the linker script flag.

The only way I could find is using a modified copy if the linker script that AVR-GCC would internally use, create a new section there at a fixed address, and declare the array to be in that section. I also removed volatile and PROGMEM.

platformio.ini

[env:atmega328p]
platform = atmelavr
board = uno
framework = arduino
board_build.mcu = atmega328p
board_build.f_cpu = 16000000L
build_flags = 
    -Wl,-Tcustom_linkerscript.xn

src/main.cpp:

#include <stdint.h>
#include <avr/pgmspace.h>  
const uint8_t signature[16] __attribute__((section(".callsig"), used)) = 
{
    0xc0,
    0xfe,
    0xba,
    0xbe,
    0x12,
    0x34,
    0x56,
    0x78,
    0x9a,
    0xbc,
    0xde,
    0xff,
    0x11,
    0x22,
    0x33,
    0x44
};

#include <Arduino.h>
void setup(){
    Serial.begin(9600);
}
void loop(){
    #define SIG_START_ADDR 0x7DF0
    #define SIG_LEN 16
    uint8_t tempData[SIG_LEN] = {0};
    // copy data from program memory into array
    // could also use pgm_read_byte(SIG_START_ADDR + i); in a loop
    memcpy_P(tempData, (const void*) SIG_START_ADDR, SIG_LEN);
    // convert to hex string
    String hexStr = "";
    for (int i=0; i < SIG_LEN; i++) {
        String temp = String(tempData[i], HEX);
        if(temp.length() == 1)
            temp = "0" + temp;
        hexStr += temp + " ";
    }
    Serial.println("Read custom signature at " + 
        String(SIG_START_ADDR, HEX)+  " as: " + hexStr);

    delay(1000);
}

custom_linkerscript.xn

/* Script for -n: mix text and data on same page */
/* Copyright (C) 2014-2015 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf32-avr","elf32-avr","elf32-avr")
OUTPUT_ARCH(avr:5)
__TEXT_REGION_LENGTH__ = DEFINED(__TEXT_REGION_LENGTH__) ? __TEXT_REGION_LENGTH__ : 128K;
__DATA_REGION_LENGTH__ = DEFINED(__DATA_REGION_LENGTH__) ? __DATA_REGION_LENGTH__ : 0xffa0;
__EEPROM_REGION_LENGTH__ = DEFINED(__EEPROM_REGION_LENGTH__) ? __EEPROM_REGION_LENGTH__ : 64K;
__FUSE_REGION_LENGTH__ = DEFINED(__FUSE_REGION_LENGTH__) ? __FUSE_REGION_LENGTH__ : 1K;
__LOCK_REGION_LENGTH__ = DEFINED(__LOCK_REGION_LENGTH__) ? __LOCK_REGION_LENGTH__ : 1K;
__SIGNATURE_REGION_LENGTH__ = DEFINED(__SIGNATURE_REGION_LENGTH__) ? __SIGNATURE_REGION_LENGTH__ : 1K;
__USER_SIGNATURE_REGION_LENGTH__ = DEFINED(__USER_SIGNATURE_REGION_LENGTH__) ? __USER_SIGNATURE_REGION_LENGTH__ : 1K;
__DATA_REGION_ORIGIN__ = DEFINED(__DATA_REGION_ORIGIN__) ? __DATA_REGION_ORIGIN__ : 0x800060;
MEMORY
{
  text   (rx)   : ORIGIN = 0, LENGTH = __TEXT_REGION_LENGTH__ - 16
  callsig   (r)   : ORIGIN = 0x7DF0, LENGTH = 16
  data   (rw!x) : ORIGIN = __DATA_REGION_ORIGIN__, LENGTH = __DATA_REGION_LENGTH__
  eeprom (rw!x) : ORIGIN = 0x810000, LENGTH = __EEPROM_REGION_LENGTH__
  fuse      (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__
  lock      (rw!x) : ORIGIN = 0x830000, LENGTH = __LOCK_REGION_LENGTH__
  signature (rw!x) : ORIGIN = 0x840000, LENGTH = __SIGNATURE_REGION_LENGTH__
  user_signatures (rw!x) : ORIGIN = 0x850000, LENGTH = __USER_SIGNATURE_REGION_LENGTH__
}
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  .hash          : { *(.hash)		}
  .dynsym        : { *(.dynsym)		}
  .dynstr        : { *(.dynstr)		}
  .gnu.version   : { *(.gnu.version)	}
  .gnu.version_d   : { *(.gnu.version_d)	}
  .gnu.version_r   : { *(.gnu.version_r)	}
  .rel.init      : { *(.rel.init)		}
  .rela.init     : { *(.rela.init)	}
  .rel.text      :
    {
      *(.rel.text)
      *(.rel.text.*)
      *(.rel.gnu.linkonce.t*)
    }
  .rela.text     :
    {
      *(.rela.text)
      *(.rela.text.*)
      *(.rela.gnu.linkonce.t*)
    }
  .rel.fini      : { *(.rel.fini)		}
  .rela.fini     : { *(.rela.fini)	}
  .rel.rodata    :
    {
      *(.rel.rodata)
      *(.rel.rodata.*)
      *(.rel.gnu.linkonce.r*)
    }
  .rela.rodata   :
    {
      *(.rela.rodata)
      *(.rela.rodata.*)
      *(.rela.gnu.linkonce.r*)
    }
  .rel.data      :
    {
      *(.rel.data)
      *(.rel.data.*)
      *(.rel.gnu.linkonce.d*)
    }
  .rela.data     :
    {
      *(.rela.data)
      *(.rela.data.*)
      *(.rela.gnu.linkonce.d*)
    }
  .rel.ctors     : { *(.rel.ctors)	}
  .rela.ctors    : { *(.rela.ctors)	}
  .rel.dtors     : { *(.rel.dtors)	}
  .rela.dtors    : { *(.rela.dtors)	}
  .rel.got       : { *(.rel.got)		}
  .rela.got      : { *(.rela.got)		}
  .rel.bss       : { *(.rel.bss)		}
  .rela.bss      : { *(.rela.bss)		}
  .rel.plt       : { *(.rel.plt)		}
  .rela.plt      : { *(.rela.plt)		}
  /* Internal text space or external memory.  */
  .text   :
  {
    *(.vectors)
    KEEP(*(.vectors))
    /* For data that needs to reside in the lower 64k of progmem.  */
     *(.progmem.gcc*)
    /* PR 13812: Placing the trampolines here gives a better chance
       that they will be in range of the code that uses them.  */
    . = ALIGN(2);
     __trampolines_start = . ;
    /* The jump trampolines for the 16-bit limited relocs will reside here.  */
    *(.trampolines)
     *(.trampolines*)
     __trampolines_end = . ;
    /* avr-libc expects these data to reside in lower 64K. */
     *libprintf_flt.a:*(.progmem.data)
     *libc.a:*(.progmem.data)
     *(.progmem*)
    . = ALIGN(2);
    /* For future tablejump instruction arrays for 3 byte pc devices.
       We don't relax jump/call instructions within these sections.  */
    *(.jumptables)
     *(.jumptables*)
    /* For code that needs to reside in the lower 128k progmem.  */
    *(.lowtext)
     *(.lowtext*)
     __ctors_start = . ;
     *(.ctors)
     __ctors_end = . ;
     __dtors_start = . ;
     *(.dtors)
     __dtors_end = . ;
    KEEP(SORT(*)(.ctors))
    KEEP(SORT(*)(.dtors))
    /* From this point on, we don't bother about wether the insns are
       below or above the 16 bits boundary.  */
    *(.init0)  /* Start here after reset.  */
    KEEP (*(.init0))
    *(.init1)
    KEEP (*(.init1))
    *(.init2)  /* Clear __zero_reg__, set up stack pointer.  */
    KEEP (*(.init2))
    *(.init3)
    KEEP (*(.init3))
    *(.init4)  /* Initialize data and BSS.  */
    KEEP (*(.init4))
    *(.init5)
    KEEP (*(.init5))
    *(.init6)  /* C++ constructors.  */
    KEEP (*(.init6))
    *(.init7)
    KEEP (*(.init7))
    *(.init8)
    KEEP (*(.init8))
    *(.init9)  /* Call main().  */
    KEEP (*(.init9))
    *(.text)
    . = ALIGN(2);
     *(.text.*)
    . = ALIGN(2);
    *(.fini9)  /* _exit() starts here.  */
    KEEP (*(.fini9))
    *(.fini8)
    KEEP (*(.fini8))
    *(.fini7)
    KEEP (*(.fini7))
    *(.fini6)  /* C++ destructors.  */
    KEEP (*(.fini6))
    *(.fini5)
    KEEP (*(.fini5))
    *(.fini4)
    KEEP (*(.fini4))
    *(.fini3)
    KEEP (*(.fini3))
    *(.fini2)
    KEEP (*(.fini2))
    *(.fini1)
    KEEP (*(.fini1))
    *(.fini0)  /* Infinite loop after program termination.  */
    KEEP (*(.fini0))
     _etext = . ;
  }  > text
  /* custom memory section */
  .callsig : 
  {
    KEEP(*(.callsig))
  } > callsig
  .data          :
  {
     PROVIDE (__data_start = .) ;
    *(.data)
     *(.data*)
    *(.gnu.linkonce.d*)
    *(.rodata)  /* We need to include .rodata here if gcc is used */
     *(.rodata*) /* with -fdata-sections.  */
    *(.gnu.linkonce.r*)
    . = ALIGN(2);
     _edata = . ;
     PROVIDE (__data_end = .) ;
  }  > data AT> text
  .bss  ADDR(.data) + SIZEOF (.data)   : AT (ADDR (.bss))
  {
     PROVIDE (__bss_start = .) ;
    *(.bss)
     *(.bss*)
    *(COMMON)
     PROVIDE (__bss_end = .) ;
  }  > data
   __data_load_start = LOADADDR(.data);
   __data_load_end = __data_load_start + SIZEOF(.data);
  /* Global data not cleared after reset.  */
  .noinit  ADDR(.bss) + SIZEOF (.bss)  :  AT (ADDR (.noinit))
  {
     PROVIDE (__noinit_start = .) ;
    *(.noinit*)
     PROVIDE (__noinit_end = .) ;
     _end = . ;
     PROVIDE (__heap_start = .) ;
  }  > data
  .eeprom  :
  {
    /* See .data above...  */
    KEEP(*(.eeprom*))
     __eeprom_end = . ;
  }  > eeprom
  .fuse  :
  {
    KEEP(*(.fuse))
    KEEP(*(.lfuse))
    KEEP(*(.hfuse))
    KEEP(*(.efuse))
  }  > fuse
  .lock  :
  {
    KEEP(*(.lock*))
  }  > lock
  .signature  :
  {
    KEEP(*(.signature*))
  }  > signature
  .user_signatures  :
  {
    KEEP(*(.user_signatures*))
  }  > user_signatures
  /* Stabs debugging sections.  */
  .stab 0 : { *(.stab) }
  .stabstr 0 : { *(.stabstr) }
  .stab.excl 0 : { *(.stab.excl) }
  .stab.exclstr 0 : { *(.stab.exclstr) }
  .stab.index 0 : { *(.stab.index) }
  .stab.indexstr 0 : { *(.stab.indexstr) }
  .comment 0 : { *(.comment) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  /* DWARF debug sections.
     Symbols in the DWARF debugging sections are relative to the beginning
     of the section so we begin them at 0.  */
  /* DWARF 1 */
  .debug          0 : { *(.debug) }
  .line           0 : { *(.line) }
  /* GNU DWARF 1 extensions */
  .debug_srcinfo  0 : { *(.debug_srcinfo) }
  .debug_sfnames  0 : { *(.debug_sfnames) }
  /* DWARF 1.1 and DWARF 2 */
  .debug_aranges  0 : { *(.debug_aranges) }
  .debug_pubnames 0 : { *(.debug_pubnames) }
  /* DWARF 2 */
  .debug_info     0 : { *(.debug_info .gnu.linkonce.wi.*) }
  .debug_abbrev   0 : { *(.debug_abbrev) }
  .debug_line     0 : { *(.debug_line .debug_line.* .debug_line_end ) }
  .debug_frame    0 : { *(.debug_frame) }
  .debug_str      0 : { *(.debug_str) }
  .debug_loc      0 : { *(.debug_loc) }
  .debug_macinfo  0 : { *(.debug_macinfo) }
  /* SGI/MIPS DWARF 2 extensions */
  .debug_weaknames 0 : { *(.debug_weaknames) }
  .debug_funcnames 0 : { *(.debug_funcnames) }
  .debug_typenames 0 : { *(.debug_typenames) }
  .debug_varnames  0 : { *(.debug_varnames) }
  /* DWARF 3 */
  .debug_pubtypes 0 : { *(.debug_pubtypes) }
  .debug_ranges   0 : { *(.debug_ranges) }
  /* DWARF Extension.  */
  .debug_macro    0 : { *(.debug_macro) }
}

Outputs:

Read custom signature at 7df0 as: c0 fe ba be 12 34 56 78 9a bc de ff 11 22 33 44 
Read custom signature at 7df0 as: c0 fe ba be 12 34 56 78 9a bc de ff 11 22 33 44 
Read custom signature at 7df0 as: c0 fe ba be 12 34 56 78 9a bc de ff 11 22 33 44 

That works perfect! Thank you

For anyone reading in the future, I have created a struct to store all the data I want at the end of the file:

static struct {
    uint8_t aValue = 1;
    uint8_t signature[9] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
    uint8_t someOtherValue = 10;
} calibration __attribute__((used,section(".calsection")));

I have modified a copy of the linker script, and added calsection:

MEMORY
{
  text   (rx)   : ORIGIN = 0, LENGTH = __TEXT_REGION_LENGTH__
  calsection   (r)   : ORIGIN = 0x7DF0, LENGTH = 16
  data   (rw!x) : ORIGIN = 0x800060, LENGTH = __DATA_REGION_LENGTH__
  ....
}
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  .....
  /* Internal text space or external memory.  */
  .text   :
  {
    ........
  }  > text
  /* custom memory section */
  .calsection : 
  {
    KEEP(*(.calsection))
  } > calsection
  .data          :
  {
  ..............

I can read/access/reference parts of the data in my code if required using:

pgm_read_byte(&(calibration.signature[i]));

And I can confirm the data is being placed into the correct part of the hex file. The rest of the file remains unchanged no matter what the data is so its not being loaded into ram or anything. Perfect!