Modifying AVR PROGMEM at runtime
As part of a project this year, I needed about 11.5KiB of data which would be maintained on power off, could be modified on the fly, could be read blazingly fast, and didn’t use the Arduino bootloader. Storing stuff in flash using PROGMEM sounds great, but you can’t modify that, right? Wrong!
First up: Don’t do this unless you have a very good reason to. The flash memory on most AVR microcontrollers has an endurance of only 10,000 writes. Make sure that you’re not going to hit this limit in your application, or bad things could happen! The data is also lost on every program flash (unlike EEPROM, which can be preserved), so keep that in mind if you rapidly iterate.
Here’s some example code to make it all work:
#include <avr/io.h>
#include <avr/boot.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
void rewrite_data(void);
BOOTLOADER_SECTION void boot_program_page(uint32_t page, uint8_t *buf);
const uint8_t flash_data[11520] __attribute__((aligned(SPM_PAGESIZE))) PROGMEM = {};
void rewrite_data(void) {
uint8_t data[SPM_PAGESIZE];
// Break data into pages
for(int i = 0; i < 11520; i+= SPM_PAGESIZE) {
// Load data
for(int j = 0; j < SPM_PAGESIZE; j++) {
// For my project this came from serial, don't just use 42!
data[i] = 42;
}
boot_program_page((uint32_t)flash_data + i, data);
}
}
// avr/boot.h documentation example
BOOTLOADER_SECTION void boot_program_page(uint32_t page, uint8_t *buf)
{
uint16_t i;
uint8_t sreg;
// Disable interrupts.
sreg = SREG;
cli();
eeprom_busy_wait ();
boot_page_erase (page);
boot_spm_busy_wait (); // Wait until the memory is erased.
for (i=0; i<SPM_PAGESIZE; i+=2)
{
// Set up little-endian word.
uint16_t w = *buf++;
w += (*buf++) << 8;
boot_page_fill (page + i, w);
}
boot_page_write (page); // Store buffer in flash page.
boot_spm_busy_wait(); // Wait until the memory is written.
// Reenable RWW-section again. We need this if we want to jump back
// to the application after bootloading.
boot_rww_enable ();
sei();
// Re-enable interrupts (if they were ever enabled).
SREG = sreg;
}
To explain some tricky sections:
const uint8_t flash_data[11520] __attribute__((aligned(SPM_PAGESIZE))) PROGMEM = {};
This defines your data - in my case, a 11520 byte array of unsigned 8 bit integers. PROGMEM
is the AVR directive to put this variable inside flash memory instead of SRAM. The ___attribute__((aligned(SPM_PAGESIZE))
is so that the data gets put into its own page. AVR flash memory is per page, and there is no way around that. You could read in the previous page data before yours, add your data, then write it back, but this is unnecessary effort. Aligning the data to a page boundary means you can write data directly to that page without worrying about other things in that memory space.
BOOTLOADER_SECTION void boot_program_page(uint32_t page, uint8_t *buf)
This puts this function (only this function) inside the bootloader section of the flash memory. If you try to execute flash memory modifications outside this area, nothing happens.
Now, if you were to compile this, you’d get a linker error. Whilst you’ve said that your special programmer function should exist in the bootloader, it doesn’t actually know where that is. How do we find out where it should go? The datasheet! I was using the Atmega328P, so I’ll use this for an example.
The bootloader section can be several different sizes depending on which fuses you’ve set. For the 328 we go to section 26.8.16, page 291.
In this case, the page programming code happily fits inside 256 words, so we set our BOOTSZ[1:0] bits to 3. We can also see that the bootloader will then start at 0x3F00. So how do we tell the linker this?
In Atmel Studio you can set this up under the project settings:
Using commandline tools, just add --section-start=.bootloader=0x3F00
to your linker flags. Double check your address is actually in bootloader space! Otherwise the code will run happily but not modify anything.
That’s it! Enjoy your newfound “kind of EEPROM but not really” space!
Comments