Navigation
External Links
Donate
Show your appreciation for OpenBLT
and support future development by
donating.
External Links
Donate
Show your appreciation for OpenBLT
and support future development by
donating.
Wouldn't it be nice if the bootloader can check if the firmware file, selected for the firmware update, is actually the correct one for the hardware? Why? Well, here's a potential scenario:
Imagine if you have multiple products on which you run the OpenBLT bootloader. In this case it can happen that the person initiating the firmware update, selected the wrong firmware file. As a result, the firmware for product A got programmed onto product B. This could have disastrous consequences. For example, if GPIOD-pin1 on product A is connected to an LED and on product B connected to a hydrogen purge valve. Instead of toggling an LED, hydrogen is purged. A bit of an extreme example, yet not impossible.
Thanks to the Firmware Info Check feature of the OpenBLT bootloader, you can implement a check in your bootloader application, and verify that the to-be-programmed firmware is actually meant for the hardware.
When making use of the Firmware Info Check feature, the idea is that you add a table with firmware info details to your firmware, such that this table is always located at the same fixed memory address in non-volatile memory:
The actual contents of the firmware info table are completely up to you. In the illustration above, the firmware info table consists of the following elements:
0x9A4B8107
.1234
is an air pump.10429
is version 1.4.29.If you would like to make use of the firmware info check feature, this section contains step-by-step instructions to help you get started. As a reference, you can look at the Olimexino-STM32F3 demo programs, as these demo programs are configured to support the firmware info check feature.
Since the firmware info check feature hinges on the presence of an info table in your firmware, the first step is adding such an info table to your firmware. Note that you are free to choose whatever information you would like to include in the table. Just make sure to declare the table as const
such that the linker places it in non-volatile memory:
struct firmwareInfoTable { unsigned long tableId; /**< fixed value for identification as an info table. */ unsigned long productId; /**< product identification. E.g. 1234 = Airpump. */ unsigned long firmwareVersion; /**< firmware version. E.g. 10429 = v1.4.29 */ }; const struct firmwareInfoTable firmwareInfoTable = { .tableId = 0x9A4B8107UL, .productId = 1234UL, .firmwareVersion = 10429UL };
After adding the info table, we now need to make sure it is always present at the same fixed memory address. My personal preference is to place it after the vector table. Another option would be to place it at the end of flash memory.
How to actually place the info table at a specific location, is compiler dependent. The following example assumes that you are using the GNU ARM Embedded toolchain. This is the one used by STM32CubeIDE. Note that you can find IAR and Keil examples in the Olimexino-STM32F3 OpenBLT demo programs.
Start by adding the section attribute to the info table constant declaration. Change:
const struct firmwareInfoTable firmwareInfoTable =
To:
const struct firmwareInfoTable firmwareInfoTable __attribute__ ((section (".infoTable"))) =
Next, open up your firmware project's linker script. This is the file that typically has the .ld
file extension. By default the start address of the firmware was moved forward already by 40kb (0xA000
) to make space for the bootloader.
I added a VECTAB
memory definition of 512 bytes for the vector table. Next, I added a INFO_TABLE
memory definition of 12 bytes for the actual info table. Then the start of the FLASH
memory definition was moved forward to make space for (a) the bootloader, (b) the vector table and © the info table:
/* Memories definition */ MEMORY { CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 8K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 40K VECTAB (rx) : ORIGIN = 0x0800A000, LENGTH = 512 INFO_TABLE (rx) : ORIGIN = 0x0800A200, LENGTH = 12 FLASH (rx) : ORIGIN = 0x0800A20C, LENGTH = 256K - 40K - 512 - 12 }
With the memory definitions updated, the next step is to update the sections such that the vector table (.isr_vector
) and info table (.infoTable
) are linked to the correct memory regions. In a nutshell, the .isr_vector
and .infoTable
sections were newly added:
/* Sections */ SECTIONS { /* The vector table goes into "VECTAB". */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Vector table */ . = ALIGN(4); } >VECTAB /* The firmware info table goes into INFO_TABLE. */ .infoTable : { . = ALIGN(4); KEEP(*(.infoTable)) /* Info table */ . = ALIGN(4); } >INFO_TABLE /* Remainder of the sections was left unchanged... */ }
If you now rebuild your firmware and inspect the MAP-file afterwards, you'll see that the linker placed the 12-byte long info table starting at address 0x0800A200
:
With the firmware info table added to a fixed location into your firmware, it's now time to switch to your bootloader project. As a first step, enable and configure the firmware info check feature. Open up your “blt_conf.h” bootloader configuration header-file and add the following three macros:
BOOT_INFO_TABLE_ENABLE
⇒ Set to 1 to enable the firmware info check feature.BOOT_INFO_TABLE_LEN
⇒ Set to the length in bytes of the info table you added to your firmware.BOOT_INFO_TABLE_ADDR
⇒ Set to the base address where the info table is located in your firmware.Example:
After adding the info table to a fixed location in your firmware and enabling the info table check feature in the bootloader, the last step is to implement the hook-function that actually checks the info table. Add the function InfoTableCheckHook()
somewhere in your bootloader. I typically place all hook-functions in the “hooks.c” source-file.
This function is automatically called during the firmware updates, when it is time to check the info table. The function receives two memory addresses as parameters:
newInfoTable
⇒ Address of the opaque pointer to the info table that was extracted from the firmware file that was selected for the firmware update.currentInfoTable
⇒ Address of the opaque pointer to the info table of the currently programmed firmware. With the return value, you can inform the bootloader if the info table check passed or not. If not passed, then the firmware update will be aborted before any changes were made to the non-volatile memory.
Example implementation of the InfoTableCheckHook()
function:
/************************************************************************************//** ** \brief Callback that gets called at the start of the firmware update, before ** performing erase and program operations on non-volatile flash. It enables ** you to implement info table comparison logic to determine if the firmware ** update is allowed to proceed. Could for example be used to make sure a ** firmware update only goes through if the selected firmware file contains ** firmware for the correct product type. ** \param newInfoTable Address of the opaque pointer to the info table that was ** extracted from the firmware file that was selected for the firmware update. ** \param oldInfoTable Address of the opaque pointer to the info table of the ** currently programmed firmware. ** \return BLT_TRUE if the info table check passed and the firmware update is allowed ** to proceed. BLT_FALSE if the firmware update is not allowed to proceed. ** ****************************************************************************************/ blt_bool InfoTableCheckHook(blt_addr newInfoTable, blt_addr currentInfoTable) { blt_bool result = BLT_FALSE; /* Important: This structure must have the same layout as the actual info table in the * firmware itself. */ struct firmwareInfoTable { blt_int32u tableId; /**< fixed value for identification as an info table. */ blt_int32u productId; /**< product identification. E.g. 1234 = Airpump. */ blt_int32u firmwareVersion; /**< firmware version. E.g. 10429 = v1.4.29 */ }; /* Cast addresses of opaque pointers to info table pointers. */ struct firmwareInfoTable const * newInfoTablePtr = (void const *)newInfoTable; struct firmwareInfoTable const * currentInfoTablePtr = (void const *)currentInfoTable; /* Sanity check on the configured length of the info table at compile time. */ ASSERT_CT(BOOT_INFO_TABLE_LEN == sizeof(struct firmwareInfoTable)); /* Do table IDs match? The table ID identifies the tables as firmware info tables. */ if (currentInfoTablePtr->tableId == newInfoTablePtr->tableId) { /* Only allow the firmware update to proceed if it's firmware for the same * product type. */ if (currentInfoTablePtr->productId == newInfoTablePtr->productId) { /* Allow the firmware update to proceed. */ result = BLT_TRUE; } } /* Give the result back to the caller. */ return result; }
To provide a better understanding of how the info table check feature works during a firmware update, refer to the following flowchart:
At the start of the firmware update, a check is performed to see if the firmware info table check feature is actually enabled. For firmware updates from a firmware file that is stored on a locally attached file system (e.g. SD-card), macro BOOT_INFO_TABLE_ENABLE
is evaluated for this. For remote firmware updates, a custom XCP command is sent to request the info table's start address and length. A positive response to this XCP command indicates that the firmware info table check feature is enabled, a command unknown negative response indicates that it is disabled and/or not supported.
With the firmware info table check feature enabled, the next step is to determine the info table start address and length. For remote firmware updates, the previously mentioned custom XCP command already obtained this information. For firmware updates from a locally attached file system, this information is obtained from the BOOT_INFO_TABLE_ADDR
and BOOT_INFO_TABLE_LEN
macros.
Once it's known where the info table is located and how large it is, the info table contents are extracted from the firmware file that was selected for the firmware update. For remote firmware updates, the info table contents are sent to the bootloader with the help of another custom XCP command. Upon reception, the bootloader stores it in a dedicated RAM buffer. For firmware updates from a locally attached file system, the bootloader directly copies the info table contents into this RAM buffer, while extracting it from the firmware file.
With the info table contents present in the dedicated RAM buffer, the bootloader calls the InfoTableCheckHook()
that you implemented in your bootloader application. It passes the pointer to the dedicated RAM buffer, which holds the info table of the to-be-programmed firmware, and a pointer to the info table of the currently programmed firmware. This allows you to compare the two info tables and decide if it's okay for the firmware update to continue, based on the return value you give from this hook-function.
Note that the bootloader only calls InfoTableCheckHook()
, if firmware is currently actually programmed. It uses the internal checksum mechanism to make this determination. If no firmware is currently programmed, then the bootloader always allows the firmware update to proceed.
As mentioned in the previous section, custom XCP commands were designed and implemented to support the firmware info table check feature, during remote firmware updates. The following three commands were added:
0xF1
is for USER_CMD
0x17
0x04
0x06
0x08
Obtains information about the memory location and length of the info table in the user program firmware.
Position | Type | Description |
---|---|---|
0 | BYTE | Command Code = 0xF1 |
1 | BYTE | Sub Command Code = 0x17 |
2 | BYTE | Info Table Command ID = 0x04 |
Positive Response
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFF |
1 | BYTE | Info Table Command ID = 0x04 |
2,3 | WORD | Info Table Length [BYTE] |
4..7 | DWORD | Info Table Base Address |
After successful reception of this command, the bootloader clear the contents of the info table RAM buffer and sets its write pointer to the start of the RAM buffer.
Note that this command does not make use of the address extension, which is typically added whenever addresses are exchanged. The address extension is usually not used and if at all used, it is to select the memory device that the address belong to. The bootloader knows in which memory the info table is stored and therefore no address extension is needed.
Once the memory location and length of the info table are known, it can be extracted from the firmware file that was selected for the firmware update. With this info table command ID the extracted table is downloaded to a RAM buffer in the bootloader.
Position | Type | Description |
---|---|---|
0 | BYTE | Command Code = 0xF1 |
1 | BYTE | Sub Command Code = 0x17 |
2 | BYTE | Info Table Command ID = 0x06 |
3 | BYTE | Length of info table bytes to download |
4..MAX_CTO-1 | BYTE | Info table content |
If the total length of the info table is bigger than MAX_CTO-4, the master has to send the info table bytes with consecutive IT_CID_DOWNLOAD command(s). Similar to how the XCP DOWNLOAD command works.
After successful reception of this command, the bootloader attempts to store the new data at the current write pointer of the internal RAM buffer and automatically increments the write pointer. If this data does not fit in the info table RAM buffer anymore, the ERR_OUT_OF_RANGE
negative response is issued.
Positive Response
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFF |
1 | BYTE | Info Table Command ID = 0x06 |
After downloading the info table of the firmware that is selected for the firmware update, the bootloader can access it in the RAM buffer. The bootloader can also access the info table of the currently programmed firmware in non-volatile memory. When the bootloader receives this info table command ID, it invokes a hook-function for comparing the two info tables, allowing to perform a final check to decide if it allows this firmware update to proceed.
Position | Type | Description |
---|---|---|
0 | BYTE | Command Code = 0xF1 |
1 | BYTE | Sub Command Code = 0x17 |
2 | BYTE | Info Table Command ID = 0x08 |
Positive Response
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFF |
1 | BYTE | Info Table Command ID = 0x08 |
2 | BYTE | Okay (1) or Abort (any other value) |
After successful reception of this command, the bootloader first checks if the info table in RAM was fully written to. If not the ERR_SEQUENCE
negative response is issued.
All commands use the T1 timeout.
In case the info table feature is disabled or an invalid info table command ID is received:
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFE |
1 | BYTE | ERR_CMD_UNKNOWN = 0x20 |
In case more info table data than expected is downloaded:
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFE |
1 | BYTE | ERR_OUT_OF_RANGE = 0x22 |
In case the check command is received but not yet the full contents of the info table was received:
Position | Type | Description |
---|---|---|
0 | BYTE | Packet ID = 0xFE |
1 | BYTE | ERR_SEQUENCE = 0x29 |
The firmware info table check feature is available starting with OpenBLT stable release 1.20. If you read this before the release of version 1.20, it is already present in the development trunk. Refer to the download page for details on where to download stable releases and how to access the development trunk.
Note that the firmware info table check feature is not a security feature. It's a convenience feature. In fact, it is relatively easy to bypass the firmware info table check feature during a remote firmware update. The already existing seed/key security feature can be used to better control who is authorized to perform firmware updates.
Some users perform a firmware update in two steps with two firmware files. One firmware file with the actual firmware code and another firmware file with calibration and/or configuration parameters. This now doesn't work anymore, because one of these files probably doesn't have the info table included, resulting in the bootloader aborting the firmware update for the firmware file that doesn't have the info table.
A fairly simple workaround is to make sure the info table is always included in each firmware file. For example, by placing the firmware info table in the same area as the calibration / configuration parameters. The first firmware file that contains the actual firmware, should then also include the firmware info table and default values for the calibration / configuration parameters. When programming the second firmware file, the one with the up-to-date calibration / configuration parameters, this one now also includes the info table, meaning that all works as expected.