Linker script (ldscript) decides the address range of each ELF section. It can either put them at fixed locations (like it does for e.g. the interrupt table), or concatenate them. It can even concatenate code in multiple sections to form one consecutive chunk of machine code; this is exactly how AVR startup (prior to main()) is generated in avr-libc. It can also compute simple arithmetic expressions (most typical example being section sizes), and emit those into the final binary.
Do not confuse ELF sections with ELF memory segments or memory regions. An ELF memory segment is just a continuous address range with a set of logical attributes (like read-only, or executable) the linker tries to honor. The segments are defined in the
MEMORY command of the linker script. In general, linker scripts are easier to use than many initially think. While GCC and LLVM/Clang toolchains use different linkers, they use the same linker scripts. I recommend looking at
Linker Scripts at the OSDev Wiki.
A very common use case is to provide the start address of something (like the start and end addresses of a section) to the compiled code using the and end addresses of a section address, simply by defining them in the linker script.
In your C and C++ code (using gcc, g++, or clang), you can use the
__attribute__((section ("name"))) to put variables, constants, and functions in a specific section. The compiler and linker will manage their addresses.
ATmega1608 is of the megaAVR0 family, which has an unified 16-bit address space accessible with the standard instructions; see Figure 7-1. Memory map in the
datasheet on page 38. I/O registers are at addresses 0x0000-0x003F, extended I/O at 0x0040-0x0FFF, NVM I/O registers and data at 0x1000-0x13FF, EEPROM starting at address 0x1400, Internal SRAM starting at 0x2800, and Flash starting at address 0x4000.
Your linker script should contain line
eeprom (rw!x) : ORIGIN = 0x1400, LENGTH = 0x100 or equivalent in the
MEMORY command, and
.eeprom: { __eeprom_start = .; KEEP(*(.eeprom*)) __eeprom_end = . ; } > eepromor equivalents in the
SECTION command, so everything in sections whose names start with
.eeprom is put into one output section called
.eeprom (thus combining content from multiple sections into a single section), and placed in the eeprom memory segment. Note that here,
.eeprom refers to the section –– note the full stop at the beginning! ––, and
eeprom –– without a full stop! –– refers to the memory segment. The two symbols,
__eeprom_start and
__eeprom_end, will have addresses corresponding to the start of the EEPROM region and the first unused (by any global variables or constants) address in EEPROM. Your C or C++ code can access these via
&__eeprom_start and
&__eeprom_end, as long as you declare them in your code via
extern TYPE __eeprom_start, __eeprom_end;noting that TYPE is entirely up to you; if you don't have any need for the address-of to yield a specific type of a pointer, use
unsigned char.
Basically, the EEPROM on the ATmega1608 consists of eight objects (pages) of 32 bytes each. You can pack 32 bytes, 16 two-byte objects, 10 three-byte objects, 8 four-byte objects, 6 five-byte objects, 5 six-byte objects, 4 seven-byte objects, 4 eight-byte objects, 3 nine-byte objects, 3 ten-byte objects, 2 objects between 11 and 16 bytes, or a single object between 16 and 32 bytes in size. Crossing a page boundary within an object means an update has to update two EEPROM pages, so try to avoid that if at all possible.
Unfortunately, the C/C++ toolchain does not have a way to express "pack these objects in this region, but do it so that none crosses an 2
k-byte boundary".
What I do, is add an intermediate step in my build toolchain, just after compiling, but before linking, that examines the ELF object files. The object files describe all objects to be placed in the .eeprom section, and their sizes. I then have a script (Python or Awk for me, but whatever floats your boat) do the ol' Optimum Packing Problem work (for 8 containers of 32 bytes each); this is small enough Brute Forcing it will be essentially instantaneous, so no complicated work here. The script then simply generates derivatives of the source ELF object files, with only the section names for these objects changed from
.eeprom to
.eeprom0 through
.eeprom7, reflecting the respective EEPROM page. (If the script is not utterly stupid, it also means that as a programmer, the section name you use determines whether that object will always be in that EEPROM page, or if the script has to pick the page to put it in. Given additional information like "estimated write rate" per object, and enough EEPROM to do so, the script can also make better decisions, like putting most often modified objects in their own pages, or combine them with the most rarely used ones, so that on average, all EEPROM pages have approximately the same write rate.)
Anyway, for each chunk of bytes within a single EEPROM page, you start by making sure the Page Buffer refers to the correct EEPROM page.
You do that by writing 255 to any byte within that page. Writes to the Page Buffer do not
set the page buffer contents; they do a binary AND with the page buffer contents and the written value. In other words, when you write a value, only its zero bits are copied over to that byte in the Page Buffer. There is only 32 bytes of Page Buffer, and the NVMCTRL keeps track which page it refers to. So, writing 255 to anywhere in the EEPROM does not change any data or contents of the Page Buffer, it only makes sure the Page Buffer refers to that page.
Next, you use the NVMCTRL to issue the Page Buffer Clear command (9.3.2.4.4, takes seven cycles). After this, the Page Buffer contains all ones.
Next, you write the new data to the target EEPROM address normally, essentially a memcpy() (but don't use one, just in case it is "optimized": use a loop, and make sure the destination is
volatile unsigned char to stop the compiler from doing stupid assumptions). Because the Page Buffer values are all ones, this sets them to the desired new values, even though the operation is a binary AND and not a set. We must take care to not cross a page boundary, howeve!
Next, we copy the existing data for the rest of the page.
If the new data did not start at the 32-byte boundary, then there is one or more bytes before the new data. Similarly, if the new data did not end at the 32-byte boundary, there is one or more bytes after the new data. We do this by simply reading and writing the EEPROM data in place, i.e. given pointer
p to such a byte, we do
*(volatile unsigned char *)p = *(const volatile unsigned char *)p;.
Compilers are idiots and [... rant voluntarily pre-emptively deleted ...] so one may have to resort to assembly, though. Not a big hassle, both GCC and LLVM/Clang support extended inline assembly, so writing a suitable low-level function for AVR assembly is actually easy. It boils down to writing to each of those 32 bytes in the EEPROM page, either using the current or the new data. It is easiest to do in three pieces, where the prefix and suffix parts are optional, and the middle part is the new data.
When you have done the above, you have tricked the Page Buffer into containing the data you want the EEPROM page to have.
Next, you use the NVMCTRL to issue a Erase and Write Page command (9.3.2.4.3). The CPU will be halted for the duration of the operation, so this update will be atomic.
The only thing that can mess it up is something interrupting the above copy stuff, by trying to *write* to anywhere in the EEPROM. Any interrupt or such *reading* from the EEPROM does absolutely no harm.
If you want example code, describe to me what you want to store in the EEPROM, and I can chuck together a help function. Making a generic one that just writes data to the EEPROM is not what you should be interested in, because that is the wrong way to use EEPROM anyway. If you want to treat it as a log for a data object, and spread it all over the EEPROM instead of using a fixed page for it, that takes a different approach altogether, because turning EEPROM bits to 1 is costly, but to 0 cheap: it boils down to encoding the data so that an all ones bit pattern is invalid, and to prefer sequences where updates that only turn ones to zeros but not zeros to ones. Since there are only 8 pages, we only need to reserve four bits per page (out of the 32 bytes, or 256 bits, per page) to tell which one is the latest one. If we initialize all EEPROM pages to all ones, and use the latest one for the new value (overwriting the first entry having all ones in that page), we can do NVMCTRL Write Page commands without wearing them down with Erases. We only need to do an Erase whenever we run out of room in the newest page, and don't have any pages left that are still all ones; and then we erase the oldest one of them (that's where that four-bit counter comes in) to all ones. However, if we do that Erase at least one step before we run out of entries, we won't need to reserve the four bits per page: we know the latest page is the one with unused entries, or if all pages are either full or empty, the page before the first empty page is the latest one. Using an extra bit in the counter lets us use the pages in a random order (of consecutive, incremented counter values, excluding the all-bits-ones counter value that indicates an unused page), since the last one in the consecutive sequence is the latest one, and the oldest one is the first one in the consecutive sequence. (For a robust implementation that detects an EEPROM page is no longer working like it should, this approach allows ignoring such a page with just a shorter log. I'd reserve extra 8 bits per EEPROM page for a Page Broken map, though, with a zero bit indicating a broken page, in this case.)
So many options, so many good patterns, and so long a description you don't have the time to read about them. Oh well.