Lets you embed "Binary Info" in RP2040 Firmware, as readable by picotool
When you compile firmware for the Raspberry Silicon RP2040 using the official pico-sdk, your compiled binaries (in both ELF and UF2 format), contain some extra metadata. This metadata is known as "Binary Info", and it can be used to inform the holder of the ELF (or UF2) file various properties. These include, but are not limited to:
- The program version
- The git revision
- The program name
The format and structure of the Binary Info is defined by https://github.com/raspberrypi/pico-sdk/tree/master/src/common/pico_binary_info.
When you run picotool on your ELF or UF2 file, it scans the first 256 bytes of your program (it skips the first 256 bytes because that contains boot2, and it looks at the next 256 bytes) for a Magic Header. . The Magic Header five 32-bit values:
marker_start
- The value0x7188ebf2
entries_start
- The address of the start of the entry tableentries_end
- The address of the end of the entry tablemapping_table
- The start address of the mapping tablemarker_end
- the value0xe71aa390
The Entry Table is a list of 32-bit values. Each value is the address of an Entry. The address may point to either RAM or Flash, although RAM addresses are converted back to Flash addresses using the Mapping Table discussed later.
There is no particular order to the Entry Table. The idea is to set up a Linker section for this table. The way the pico-sdk works is that when you use the macros to create an Entry as a global variable, the macro also creates a global variable holding the address of the Entry. That second global variable is tagged in such as a way that it ends up in the special linker section. The start address and end address for the section can then be calculated by the linker, and exported back into the C program as symbols.
We use a similar trick in Rust.
Each Entry starts with two 32-bit values:
data_type
- describes the kind of information (or 'payload') this Entry holdstag
- describes the 'ID authority' responsible for the ID that follows (this prevents collisions if you make your own IDs up)
The values for data_type
(or at least the ones the pico-sdk current uses) are:
IdAndInt
(5) - the payload is a 32-bit ID and a 32-bit integer valueIdAndString
(6) - the payload is a 32-bit ID and a 32-bit pointer to a null-terminated stringPinsWithFunction
(8) - the payload is a single 32-bit value which describes one or more Pins on the RP2040 (e.g. GP0), and the mode those Pins are in (e.g. UART TX)PinsWithName
(9) - likePinsWithFunction
, but followed by a 32-bit pointer to a null-terminated stringNamedGroup
(10) - starts a new group of Entries, with the payload including the 'parent' group ID, and a 'tag', 'ID' and 'label' for the group.
Raspberry Pi have reserved the tag 0x5250 (which corresponds to the ASCII
letters R
and P
). When this tag is used, the following IDs are valid:
ID | Name | Expected Type | Example |
---|---|---|---|
0x02031c86 | Program Name | IdAndString |
"My Program" |
0x11a9bc3a | Program Version String | IdAndString |
"v1.2.3" |
0x9da22254 | Program Build Date String | IdAndString |
"Oct 11 2021" |
0x68f465de | Binary End Address | IdAndInt |
0x20001234 |
0x1856239a | Program Url | IdAndString |
"https://github.com/my_org/my_code.git" |
0xb6a07c19 | Program Description | IdAndString |
"This is my amazing program" |
0xa1f4b453 | Program Feature | IdAndString |
"UART stdin / stdout" |
0x4275f0d3 | Program Build Attribute | IdAndString |
"Debug" |
0x5360b3ab | Sdk Version | IdAndString |
"1.2.3" |
0xb63cffbb | Pico Board | IdAndString |
"adafruit_qtpy_rp2040" |
0x7f8882e1 | Boot2 Name | IdAndString |
"boot2_w25q080" |
Note that entries of type PinsWithFunction
and PinsWithName
don't have an ID
- the data-type is sufficient to describe the entry.
We (and you) are free to select a unique tag and then create your own range of
IDs - however, picotool can only decode the RP
tagged IDs listed above.
When picotool looks at your binary, it is effectively looking at a copy of the Flash memory. However, some of the values referred to in the various Entries may reside in a global variable in RAM at run-time. Normally your program's start-up code will copy any default values for these global variables from Flash to RAM at start-up. Any global variables which have an all-zeroes default value, simply get initialised with a call to 'memset' instead.
The Mapping Table tells picotool which parts of the Flash image get mapped to which regions of RAM. If it then finds any RAM addresses when looking at the Entries, it can work backwards to find the equivalent Flash address and load the appropriate data.
Of course, if you were planning on constructing or modifying your Entry at run-time, you're out of luck - picotool will only ever be able to find the power-up default value located in Flash.
Each entry in the Mapping Table contains three 32-bit values:
source_addr_start
- the start address in Flashdest_addr_start
- the start address in RAMdest_addr_end
- the end address in RAM
You may have noticed that the Magic Header only notes the start of this Mapping Table, and not the end. The reason for this is unclear, but picotool can find the end of the table easily enough by looking for a 'null' entry - one with zeroes for all the addresses.
You will need to add two extra pieces to your memory.x
file (as used by
cortex-m-rt).
SECTIONS {
/* ### Picotool 'Binary Info' Header Block
*
* Picotool only searches the second 256 bytes of Flash for this block, but
* that's where our vector table is. We squeeze in this block after the
* vector table, but before .text.
*/
.bi_header : ALIGN(4)
{
KEEP(*(.bi_header));
/* Keep this block a nice round size */
. = ALIGN(4);
} > FLASH
} INSERT BEFORE .text;
/* Move _stext, to make room for our new section */
_stext = ADDR(.bi_header) + SIZEOF(.bi_header);
SECTIONS {
/* ### Picotool 'Binary Info' Entries
*
* Picotool looks through this block (as we have pointers to it in our header) to find interesting information.
*/
.bi_entries : ALIGN(4)
{
/* We put this in the header */
__bi_entries_start = .;
/* Here are the entries */
KEEP(*(.bi_entries));
/* Keep this block a nice round size */
. = ALIGN(4);
/* We put this in the header */
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
You can then include Binary Info entries in your application's main.rs
file:
extern "C" {
static __bi_entries_start: rp_binary_info::entry::Addr;
static __bi_entries_end: rp_binary_info::entry::Addr;
static __sdata: u32;
static __edata: u32;
static __sidata: u32;
}
/// Picotool can find this block in our ELF file and report interesting metadata.
#[link_section = ".bi_header"]
#[used]
pub static PICOTOOL_META: rp_binary_info::Header =
unsafe { rp_binary_info::Header::new(&__bi_entries_start, &__bi_entries_end, &MAPPING_TABLE) };
/// This tells picotool how to convert RAM addresses back into Flash addresses
static MAPPING_TABLE: [rp_binary_info::MappingTableEntry; 2] = [
// This is the entry for .data
rp_binary_info::MappingTableEntry {
source_addr_start: unsafe { &__sidata },
dest_addr_start: unsafe { &__sdata },
dest_addr_end: unsafe { &__edata },
},
// This is the terminating marker
rp_binary_info::MappingTableEntry {
source_addr_start: core::ptr::null(),
dest_addr_start: core::ptr::null(),
dest_addr_end: core::ptr::null(),
},
];
/// This is a list of references to our table entries
#[link_section = ".bi_entries"]
#[used]
pub static PICOTOOL_ENTRIES: [rp_binary_info::entry::Addr; 3] = [
PROGRAM_NAME.addr(),
PROGRAM_VERSION.addr(),
NUMBER_OF_KITTENS.addr(),
];
/// This is the name of our program
static PROGRAM_NAME: rp_binary_info::entry::IdAndString =
rp_binary_info::program_name(concat!("my stupid tool 2", "\0"));
/// This is the version of our program
static PROGRAM_VERSION: rp_binary_info::entry::IdAndString =
rp_binary_info::version(concat!(env!("GIT_VERSION"), "\0"));
/// This is just some application-specific random information to test integer support
static NUMBER_OF_KITTENS: rp_binary_info::entry::IdAndInt =
rp_binary_info::custom_integer(rp_binary_info::make_tag(b'J', b'P'), 0x0000_0001, 0x12345678);
Until this crate reaches version 1.0, the API is liable to change. In particular, we'd quite like a macro which can both create the Entry and stuff the address of the Entry into the Entry Table.
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
The steps are:
- Fork the Project by clicking the 'Fork' button at the top of the page.
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Make some changes to the code or documentation.
- Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Feature Branch (
git push origin feature/AmazingFeature
) - Create a New Pull Request
- An admin will review the Pull Request and discuss any changes that may be required.
- Once everyone is happy, the Pull Request can be merged by an admin, and your work is part of our project!
Contribution to this crate is organized under the terms of the Rust Code of Conduct, and the maintainer of this crate, the rp-rs team, promises to intervene to uphold that code of conduct.
The contents of this repository are dual-licensed under the MIT OR Apache
2.0 License. That means you can chose either the MIT licence or the
Apache-2.0 licence when you re-use this code. See MIT
or APACHE2.0
for more
information on each specific licence.
Any submissions to this project (e.g. as Pull Requests) must be made available under these terms.