Craig's Arduino PROGMEM Page

SRAM is a scare resource when working with most ATmegas (the Atmega328P only have 2K bytes worth) and statically initialized variables and string constants quickly eat it up. It turns out you can store static data in "program space" (aka flash memory) which is usually available in abundance. The catch is you have to copy data out to SRAM before you can work with it.

For example here's a statically allocated array of values:

static const int8_t ledstate[] PROGMEM = { 1, 2, 1, 36 };

You can use pgm_read_byte() to read a byte from program space:

int16_t val;

val = ((int8_t)pgm_read_byte(ledstate[2]);

The PSTR() macro stores a string in program space. The F() macro uses PSTR() to store a string in program space and also flags this to library routines such as Serial.print().

printf_P() uses a F() format string from program space. In addition all members of the printf() family interpret %S arguments as program space strings.

Stringizing is a method to convert a macro argument into a string constant. It actually takes two macros to accomplish this:

#define STR(s) _STR(s)
#define _STR(s) #s

Here's an example of turning a number into a string:

#define PIN_LED 3

Serial.print(F("LED is pin " STR(PIN_LED) "\n"));

which prints "LED is pin 3\n".

Here's a macro that can be used to subtract micros() values (which are unsigned long) and handles overflow correctly:

/* Subtract t2 from t1; handles overflow correctly */
#define MICROS_SUB(t1, t2) \
    (((t1) >= (t2)) ? (t1) - (t2) : (t1) + (UINT32_MAX - (t2)))

It's common to print something on the serial port from setup():

void
setup()
{
	Serial.println(F("howdy"));
}

However when serial port is USB the message is never seen because the USB serial port disconnects on reset. At least with parts such as the ATmega32U4 "Serial" does not exist until it is connected. This makes it possible to delay the message until then by moving it into loop():

void
loop()
{
	static boolean init = 0;

	if (!init && Serial) {
		Serial.println(F("howdy"));
		init = 1;
	}
}

Although larger parts such as the ATmega1284 have 128K flash your code must fit in the first 64K of address space due to 16 bit pointers. However it's possible to store data in the upper 64K in a manner similar to how PROGMEM works. However anything in the upper 64K has "far" addresses which are 24 bits wide.

First you need to define a new section, here's a compiler flag that passes a linker flag:

-Wl,--section-start=.progmem64k=0x10000

Next, define a macro that can be used to place data into the new section:

#include <avr/pgmspace.h>
#define PROGMEM64K __attribute__((section(".progmem64k")))

Since the cross compiler is configured to know that pointers are 16 bits wide you need to use the pgm_get_far_address() macro to get the "far" address of data in the upper 64K. This returns a uint32_t (32 bits) although only the lower 24 bits are used.

Here's some example code:

const char msg_howdy[] PROGMEM64K = "howdy\r\n";

void
setup()
{
	uint_farptr_t x;
	char ch;

	x = pgm_get_far_address(msg_howdy);
	Serial.print(F("msg_howdy far addr is "));
	Serial.println(x, HEX);

	Serial.print(F("msg_howdy: "));
	while ((ch = pgm_read_byte_far(x++)) != '\0')
		Serial.write(ch);
}

When this sketch is run it outputs:

msg_howdy far addr is 0x10000
msg_howdy: howdy

Note that PROGMEM data is stored in the beginning of the .text segment and will have low addresses. This means text strings stored in PROGMEM are pretty much guaranteed to reside in the first 64K of address space. If you are running out of .text space for instructions, moving some data to the upper 64K can help free up some lower 64K space.

Unfortunately it isn't possible to make a 64K version of the F() macro; this is because the compiler doesn't allow section attributes on inline data (.progmem is a special case exception). But you can work around this by using static variables, for example:

const char fmt_test[] PROGMEM64K = "The value of n is %d";

void
setup()
{
	int n;
	char fmt[sizeof(fmt_test)];
	char buf[128];

	n = 123;
	strlcpy_PF(fmt, pgm_get_far_address(fmt_test), sizeof(fmt));
	snprintf(buf, sizeof(buf), fmt, n);
	Serial.println(buf);
}

Note the use of strlcpy_PF() which accepts a "far" address (uint_farptr_t) for the source as opposed to strlcpy_P() which a "near" (PROGMEM) address. avr-libc provides _PF versions for many of the _P string routines.


Copyright © 2023, 2024
Craig Leres