DISCLAIMER: The first section of this text are just some thoughts that I had in the beginning, the lower half explains a much better way.
If you have a final firmware and know the exact location where that ID integer is stored, you can modify the .bin
file directly. The tricky thing is knowing where the ID is. Especially with regards to compiler optimization, the value might already be propagated into the function code to produce optimized results.
If you think e.g. of
const int my_id = 123; //this is e.g. changed for every build
int get_id() {
return my_id;
}
On low optimization levels like -O0
the const int my_id
would be a 4-byte variable living in flash (since its constant) at some address, and the get_id()
function would read from that address in flash to return the value. With e.g. the addition of a .map file, it’s easy to find what exactly that address is, since it lists the address of each symbol (address map).
But on higher optimization levels the value might (very likely) be inlined so the the function code of get_id()
is directly optimized to return 123;
in thet instruction code. That is significantly harder to spot (since there might also be lots of other callers where constant propagation has also already happened) and patch, but possible. But one really doesn’t want to go that way – if the firmware and code can be built in such a way that the value is only read from one known or predictable memory address and it feeds all of its callers without a compile-time optimized value propagation.
Sadly GCC doesn’t have a nice way of e.g. placing a variable at a known location, that must be done with a modification to a linker script. But what it is certainly possible is a scheme like:
- check the linker script of the project to know where the start and the end of the firmware is (either the board’s default per e.g. this or explicitly set in the
platformio.ini
) - the sketch is placed through the linker script at known location (e.g. here)
- the space at the end of the allocated space for the sketch is unused (by however many bytes your sketch doesn’t use in regards to the maximum size). So, you can freely use this.
- e.g., per linked example, the sketch starts in flash at
0x40200000
and is 1044464 Bytes big, so it ends at0x402feff0
. Let’s say e.g. we use the last 4 bytes for the ID. Withesptool.py
we can write an arbitrary blob to an arbitrary address. If we write 4 bytes starting at0x402fefec
, we fill the last 4 bytes of the sketch space with our value. Then we can write a very simple C(++) function that reads from this constant, known address and returns the ID. The compiler can then not optimize away anything because it doesn’t know what will be placed at that address and is forced to always read from this value.
int get_id_in_flash() {
int* id_read_addr = (int*) 0x402fefec;
//we can read 4-byte aligned address directly without memcpy
return *id_read_addr;
}
And now that I thought about this for a while, there is also a much much easier way to do it: Use the “EEPROM” (in flash emulated). This is basically like the method above but uses a pre-existing and pre-allocated section of flash.
The Arduino-ESP8266 has a EEPROM library that manages (very primitively) a section in flash, called eeprom
. See e.g.
All the EEPROM library does is directly read the flash contents given the address (indexed from 0, so the first byte of EEPROM is “address=0” mapping to e.g. physical 0x405FB000
per above.
Then we have read/write functions and helpers to get multi-byte values.
So, if one just writes to the known EEPROM start address, per above example linker script 0x405FB000
, some arbitrary bytes, e.g. a 4 byte integer, the code can just do
//init, only called once.
//our EEPROM area is only 4 bytes long.
//triggers reading flash into internal (4-byte) buffer
EEPROM.begin(4);
//read value stored during production at EEPROM index 0
uint32_t node_id = 0;
EEPROM.get(0, node_id);
//use node_id variable..
//optional: we can read the node ID into a RAM variable once and then
//EEPROM.end();
//to release the internal 4-byte buffer.
I haven’t tested it, but it should be really easy…
PS: The EEPROM.get(0, node_id);
is ofc also only a fancy way to read from the internal buffer that has initially been read from flash, similiar to what the above self-made C++ function does with a direct pointer access.