Accessing constants beyond 64K is tricky as you've already found out, due to the use of 16 bit pointers. If you want to access constants in program memory beyond 64K, you have to calculate the address yourself using 32 bit arithmetic (eg with uint32_t
types) and use pgm_read_word_far()
.
Since you have to have special compiler instructions to access constants located in program space anyway (eg use of PROGMEM
), I suggest you make all accesses to these constants go through a function designed to access your data and return the results in the native types that the compiler can more easily work with (ie non PROGMEM
types). The abstraction to get the constant's and return it doesn't have to be peppered through your code, but only exist in one place. Use static inline
functions and compiler optimization to make this efficient.
You have to link the binaries destined for upper/lower flash differently, so there is almost no extra work to also recompile each binary with a different TEXT_OFFSET
#define
which is used during compilation to specify the location at which the binary will be linked. You can always use the ELPM
instructions independent of compilation for upper/lower flash.
For example:
static inline uint16_t getdata(uint8_t index)
{
return pgm_read_word_far((uint32_t)TEXT_OFFSET + (uint16_t)port_to_mode_PGM + index);
}
where TEXT_OFFSET
is passed in at compile time and matches the value passed to your linker.
If you look at the assembly listing produced for source like this you'll notice a lot of instructions to do the 32 bit arithmetic. If efficiency is important, you can roll your own inline assembly function to do the access in the required manner. The following code shows a custom inline assembly snippet, hacked from samples in <avr/pgmspace.h>
. (This snippet only covers the single case of the ELPM_word_enhanced__
macro which suits your micro).
#define __custom_ELPM_word_enhanced__(offset, addr) \
(__extension__({ \
uint32_t __offset = (uint32_t)(offset);\
uint16_t __addr16 = (uint16_t)(addr);\
uint16_t __result; \
__asm__ \
( \
"out %3, %C2" "\n\t" \
"movw r30, %1" "\n\t" \
"elpm %A0, Z+" "\n\t" \
"elpm %B0, Z" "\n\t" \
: "=r" (__result) \
: "r" (__addr16), \
"r" (__offset), \
"I" (_SFR_IO_ADDR(RAMPZ)) \
: "r30", "r31" \
); \
__result; \
}))
#define custom_pgm_read_word_far(offset, address_long) __custom_ELPM_word_enhanced__((uint32_t)(offset), (uint16_t)(address_long))
It would be used similarly as before, but this time the offset is passed in as a separate parameter:
static inline uint16_t getdata(uint8_t index)
{
return custom_pgm_read_word_far(TEXT_OFFSET, (uint16_t)port_to_mode_PGM + index);
}
This will produce more efficient code as no 32 bit arithmetic is required. It can be made even more efficient if required by requiring that the offset passed in in not a 32 bit value but just the upper word or byte of TEXT_OFFSET
. Since all of your constant data accesses can go through a function like this, the information about TEXT_OFFSET
is constrained to a single place in your code. Have your data access go through a function such as this rather than using the variable directly also has the advantage that you can mock your getdata()
method for testing.
NOTE: These code sample haven't been completely tested.