Bootloader implementation in C code for general use in embedded systems.
Main features of bootloader:
Bootloader has custom, lightweight and pyhsical layer agnostics communication interface. Detailed specifications of interface can be found in Bootloader_Interface_Specifications.xlsx.
Bootloader and application data exchange takes over special RAM section defined as non-initialized aka. .noinit section.
Following data is being exhange between bootloader and application:
Shared memory space V1 is 32 bytes in size with following data structure:
Setup linker script for common shared memory between bootloader and application by first defining new memory inside RAM called SHARED_MEM region:
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K - 0x20
/* Reserve SHARED_MEM memory region at the end of RAM */
/* Region is used for app<->boot inteface and it is 32 bytes in size */
SHARED_MEM (rw) : ORIGIN = 0x20000000 + 128K - 0x20, LENGTH = 0x20
/** Bootloader flash memory space */
BOOT_FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
/** Application flash memory space */
APP_FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 512K-32K
}
Afterwards a shared_mem section needs to be defined that will fill in symbols to SHARED_MEM space:
/* No init section for app<->boot interface */
.noinit (NOLOAD):
{
/* place all symbols in input sections that start with .shared_mem */
KEEP(*(*.shared_mem*))
} > SHARED_MEM
Change bootloader configuration for shared memory linker directive according to defined section in linker file inside boot_cfg.h:
/**
* Shared memory section directive for linker
*/
#define __BOOT_CFG_SHARED_MEM__ __attribute__((section(".shared_mem")))
More info about no-init memory: https://interrupt.memfault.com/blog/noinit-memory
Bootloader support up to four different validation criteria for new application before update process can initiate:
Bootloader can check if new application will fit into reserved application flash by defining maximum application size and enabling app size check.
Firmware size check configuration in boot_cfg.h:
/**
* Complete (maximum) application size
*
* Unit: byte
*/
#define BOOT_CFG_APP_SIZE (( 512U - 32U ) * 1024U )
/**
* Enable/Disable new firmware size check
*
* @note At prepare command bootloader will check if new firmware app
* can be fitted into "BOOT_CFG_APP_SIZE" space, if that macro
* is enabled!
*/
#define BOOT_CFG_FW_SIZE_CHECK_EN ( 1 )
New application SW version compatibility can be checked if module is configurted to do so:
Firmware compatibility configuration in boot_cfg.h:
/**
* Enable/Disable new firmware version compatibility check
*/
#define BOOT_CFG_FW_VER_CHECK_EN ( 0 )
/**
* New firmware compatibility value
*
* @note New firmware version is compatible up to
* version specified in following defines.
*/
#if ( 1 == BOOT_CFG_FW_VER_CHECK_EN )
#define BOOT_CFG_FW_VER_MAJOR ( 1 )
#define BOOT_CFG_FW_VER_MINOR ( 0 )
#define BOOT_CFG_FW_VER_DEVELOP ( 0 )
#define BOOT_CFG_FW_VER_TEST ( 0 )
#endif
/**
* Enable/Disable firmware downgrade
*
* @note At prepare command bootloader will check if new firmware app
* has higher version than current, if that macro is enabled!
*/
#define BOOT_CFG_FW_DOWNGRADE_EN ( 0 )
Bootloader can check for hardware compatibility with new application image and can prevent uploading of application if not suitable for given HW version of the system. Each application image shall have a HW version embedded into application header and that information is then used to check for HW compatibility.
Hardware compatibility configuration in boot_cfg.h:
/**
* Enable/Disable new firmware version compatibility check
*/
#define BOOT_CFG_HW_VER_CHECK_EN ( 0 )
/**
* New firmware hardware compatibility value
*
* @note New firmware hardware version is compatible up to
* version specified in following defines.
*/
#if ( 1 == BOOT_CFG_HW_VER_CHECK_EN )
#define BOOT_CFG_HW_VER_MAJOR ( 1 )
#define BOOT_CFG_HW_VER_MINOR ( 0 )
#define BOOT_CFG_HW_VER_DEVELOP ( 0 )
#define BOOT_CFG_HW_VER_TEST ( 0 )
#endif
Bootloader can detect & prevent upgrading with older application as it currently running by enabling BOOT_CFG_FW_DOWNGRADE_EN.
Downgrade enable/disable configuration in boot_cfg.h:
/**
* Enable/Disable firmware downgrade
*
* @note At prepare command bootloader will check if new firmware app
* has higher version than current, if that macro is enabled!
*/
#define BOOT_CFG_FW_DOWNGRADE_EN ( 1 )
In order to prevent repetative re-booting of corrupted application, bootloader can be configured to detect such an anomaly. This is done with following logic:
Configuring boot counter in boot_cfg.h:
/**
* Enable/Disable boot counting check
*
* @note Boot count is safety mechanism build into bootloader
* in order to detect malfunctional application!
*/
#define BOOT_CFG_APP_BOOT_CNT_CHECK_EN ( 0 )
/**
* Boot counts limit
*
* @note After boot count reaches that limit it will
* not enter application! Bootloader will declare
* a faulty app and will request new application!
*/
#if ( 1 == BOOT_CFG_APP_BOOT_CNT_CHECK_EN )
#define BOOT_CFG_BOOT_CNT_LIMIT ( 5 )
#endif
Back-door entry to bootloader is implemented as simple waiting for "Connect" command from Bootloader Manager (typical PC application). This safety mechanism enables entering bootloader at power-on or reset event, in case application is corrupted and thus prevents entering bootloader from application.
Configuration of back-door entry to bootloader in boot_cfg.h:
/**
* Bootloader back-door entry timeout
*
* @note Wait specified amount of time before entering application
* code at bootloader startup if application is validated OK.
*
* To disable waiting set to timeout to 0.
*
* Unit: ms
*/
#define BOOT_CFG_WAIT_AT_STARTUP_MS ( 100U )
In case program ends up in bootloader with no requests from Bootloader Manager (PC app) and there is a valid application, it will timeout and start the application.
Configuration of idle timeout in boot_cfg.h:
/**
* Jump to application (if valid) if communication idle
* for more than this value of timeout
*
* Unit: ms
*/
#define BOOT_CFG_JUMP_TO_APP_TIMEOUT_MS ( 5000U )
One of the purposes for firmware image cryption is to prevent:
Firmware upgrade procedure using crypted image is shown below:
In case application image is encrypted, bootloader must first decrypt it and then store to the internal flash. To use encrytpion option enable crypto switch in boot_cfg.h:
/**
* Enable/Disable firmware binary encryption
*/
#define BOOT_CFG_CRYPTION_EN ( 1 )
After that, cryptographic library needs to be initilized and that must be done in boot_if.c. Example of using STM32 CMOX:
/**
* AES CTR context handle
*/
static cmox_ctr_handle_t g_ctr_ctx = {0};
/**
* AES cipher context handle
*/
static cmox_cipher_handle_t * gp_cipher_ctx = NULL;
/**
* AES CTR Encryption Key
*/
static const uint8_t gu8_key[] = { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
/**
* AES CTR Initial Vector (IV) Key
*/
static const uint8_t gu8_iv[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };
////////////////////////////////////////////////////////////////////////////////
/**
* Initialize cryptographic library
*
* @return status - Status of operation
*/
////////////////////////////////////////////////////////////////////////////////
static boot_status_t bool_if_crypto_init(void)
{
boot_status_t status = eBOOT_OK;
// Initialize cryptographic library
if ( CMOX_INIT_SUCCESS != cmox_initialize( NULL ))
{
status = eBOOT_ERROR;
}
// Construct a cipher context
gp_cipher_ctx = cmox_ctr_construct( &g_ctr_ctx, CMOX_AESFAST_CTR_DEC );
// Check for construction creation
if ( NULL == gp_cipher_ctx )
{
status = eBOOT_ERROR;
}
// Initialize the cipher context
if ( CMOX_CIPHER_SUCCESS != cmox_cipher_init( gp_cipher_ctx ))
{
status = eBOOT_ERROR;
}
// Setup decryption key
if ( CMOX_CIPHER_SUCCESS != cmox_cipher_setKey( gp_cipher_ctx, gu8_key, sizeof( gu8_key )))
{
status = eBOOT_ERROR;
}
// Setup Initilization Vector (IV)
if ( CMOX_CIPHER_SUCCESS != cmox_cipher_setIV( gp_cipher_ctx, gu8_iv, sizeof( gu8_iv )))
{
status = eBOOT_ERROR;
}
return status;
}
Next decryption function needs to be implemented inside boot_if.c. Example:
////////////////////////////////////////////////////////////////////////////////
/**
* Decrypt flash data received over communication
*
* @param[in] p_crypt_data - Pointer to crypted flash data
* @param[out] p_decrypt_data - Pointer to decrypted flash data
* @param[in] size - Size of data to decrypt
* @return void
*/
////////////////////////////////////////////////////////////////////////////////
void boot_if_decrypt_data(const uint8_t * const p_crypt_data, uint8_t * const p_decrypt_data, const uint32_t size)
{
// USER CODE BEGIN...
(void) cmox_cipher_append( gp_cipher_ctx, p_crypt_data, size, p_decrypt_data, NULL );
// USER CODE END...
}
And finaly, cryptographic library reset function must be implemented, also in boot_if.c. Example:
////////////////////////////////////////////////////////////////////////////////
/**
* Reset cryto engine
*
* @return void
*/
////////////////////////////////////////////////////////////////////////////////
void boot_if_decrypt_reset(void)
{
// USER CODE BEGIN...
//Cleanup the handle
(void) cmox_cipher_cleanup( gp_cipher_ctx );
// USER CODE END...
}
As bootloader expects crypted binary image, plain binary needs to be crypted using the same cryptography method as for decrypting. Following Python snipped provides compatible encryption for given example:
import binascii
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util import Counter
# ===============================================================================
# @brief Crypt plaing data to AES with key and initial vector
#
# @param[in] plain_data - Inputed non-cryptic data
# @return crypted_data - Outputed cryptic data
# ===============================================================================
def aes_encode(plain_data):
# AES Key and IV
key = b"\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\x15\x88\x09\xcf\x4f\x3c"
iv = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
# Create cipher
ctr = Counter.new(128, initial_value=int(binascii.hexlify(iv), 16))
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
# Encode
return cipher.encrypt( bytearray( plain_data ))
NOTICE: Don't forget to change AES Key and Initial Vector (IV) for your end application!
Bootloader expect predefined application binary code as shown in picture. Size of bootloader and application are subject to change to suit SW requirements and are used only for presentation purposes.
Application code must have a Application Header in order to validate data integritiy of image.
Revision module provides information of application header.
FSM module provides state transitions and timings spend in each state of bootloader.
Current implementation of bootloader supports only ARM Cortex-M processor family, as it expects stack pointer to be first 4 bytes of binary file, follow by reset vector.
Picture taken from ARM® Cortex®-M for Beginners: An overview of the ARM Cortex-M processor family and comparison:
In order to be part of General Embedded C Libraries Ecosystem this module must be placed in following path:
root/middleware/boot/boot/"module_space"
API Functions | Description | Prototype |
---|---|---|
boot_init | Initialization of bootloader module | boot_status_t boot_init(void) |
boot_hndl | Handle bootloader module | boot_status_t boot_hndl(void) |
boot_get_state | Get current bootlaoder state | boot_state_t boot_get_state(void) |
boot_shared_mem_get_version | Get shared memory version | boot_status_t boot_shared_mem_get_version(uint8_t * const p_version) |
boot_shared_mem_set_boot_reason | Set shared memory boot version | boot_status_t boot_shared_mem_set_boot_reason(const boot_reason_t reason) |
boot_shared_mem_get_boot_reason | Get shared memory boot version | boot_status_t boot_shared_mem_get_boot_reason(boot_reason_t * const p_reason) |
boot_shared_mem_set_boot_cnt | Set shared memory boot counter | boot_status_t boot_shared_mem_set_boot_counter(const uint8_t cnt) |
boot_shared_mem_get_boot_cnt | Get shared memory boot counter | boot_status_t boot_shared_mem_get_boot_counter(uint8_t * const p_cnt) |
GENERAL NOTICE: Put all user code between sections: USER CODE BEGIN & USER CODE END!
Configuration | Description |
---|---|
BOOT_CFG_APP_HEAD_ADDR | Application header address in flash |
BOOT_CFG_APP_HEAD_SIZE | Application header size in bytes |
BOOT_CFG_APP_SIZE | Complete (maximum) application size in bytes |
BOOT_CFG_FW_SIZE_CHECK_EN | Enable/Disable new firmware size check |
BOOT_CFG_FW_VER_CHECK_EN | Enable/Disable new firmware version compatibility check |
BOOT_CFG_FW_VER_MAJOR | New firmware compatibility major version |
BOOT_CFG_FW_VER_MINOR | New firmware compatibility minor version |
BOOT_CFG_FW_VER_DEVELOP | New firmware compatibility develop version |
BOOT_CFG_FW_VER_TEST | New firmware compatibility test version |
BOOT_CFG_FW_DOWNGRADE_EN | Enable/Disable firmware downgrade |
BOOT_CFG_HW_VER_CHECK_EN | Enable/Disable new firmware version compatibility check |
BOOT_CFG_HW_VER_MAJOR | New firmware hardware compatibility major version |
BOOT_CFG_HW_VER_MINOR | New firmware hardware compatibility minor version |
BOOT_CFG_HW_VER_DEVELOP | New firmware hardware compatibility develop version |
BOOT_CFG_HW_VER_TEST | New firmware hardware compatibility test version |
BOOT_CFG_APP_BOOT_CNT_CHECK_EN | Enable/Disable boot counting check |
BOOT_CFG_BOOT_CNT_LIMIT | Boot counts limit |
BOOT_CFG_WAIT_AT_STARTUP_MS | Bootloader back-door entry timeout |
BOOT_CFG_PREPARE_IDLE_TIMEOUT_MS | Communication idle timeout time in PREPARE state |
BOOT_CFG_FLASH_IDLE_TIMEOUT_MS | Communication idle timeout time in FLASH DATA state |
BOOT_CFG_EXIT_IDLE_TIMEOUT_MS | Communication idle timeout time in EXIT state |
BOOT_CFG_RX_BUF_SIZE | Reception buffer size in bytes |
BOOT_CFG_DATA_PAYLOAD_SIZE | Maximum size of flash data payload command |
BOOT_CFG_JUMP_TO_APP_TIMEOUT_MS | Jump to app (if valid) timeout time |
BOOT_GET_SYSTICK | System timetick in 32-bit unsigned integer form |
BOOT_CFG_STATIC_ASSERT | Static assert definition |
BOOT_CFG_WEAK | Weak compiler directive |
__BOOT_CFG_SHARED_MEM__ | Shared memory section directive for linker |
BOOT_CFG_CRYPTION_EN | Enable/Disable firmware binary encryption |
BOOT_CFG_DEBUG_EN | Enable/Disable debug mode |
BOOT_CFG_ASSERT_EN | Enable/Disable assertions |
Modify bootloader linker script as described in chapter Bootloader<->Application Communication.
Setup interface files boot_if.c, provide definition for communication interface and flash handling functions.
Initialize bootloader module
// Init boot
if ( eBOOT_OK != boot_init())
{
// Initialization failed...
}
Handle bootloader module
@main loop
{
// Handle boot
(void) boot_hndl();
}
Preparing application to be bootloadable
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K - 0x20
/ Reserve SHARED_MEM memory region at the end of RAM / / Region is used for app<->boot inteface and it is 32 bytes in size / SHARED_MEM (rw) : ORIGIN = 0x20000000 + 128K - 0x20, LENGTH = 0x20
/* Bootloader flash memory space / BOOT_FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
/* Application flash memory space / APP_HEADER (r) : ORIGIN = 0x08008000, LENGTH = 0x100 APP_FLASH (rx) : ORIGIN = 0x08008000 + 0x100, LENGTH = 512K -32K - 0x100 }
In sections add following snippet:
/* No init section for app<->boot interface */
.noinit (NOLOAD):
{
/* place all symbols in input sections that start with .shared_mem */
KEEP(*(*.shared_mem*))
} > SHARED_MEM
/* Application header section */
.app_header :
{
APP_HEADER_START = .;
*(.app_header) /*Application header */
*(.app_header*) /*Application header */
KEEP (*(.app_header*))
APP_HEADER_END = .;
} > APP_HEADER
NOTE: Linker scripts between application and bootloader shall be alligned!
Example of system_stm32g4xx.c, where using preprocessor symbol __BOOTLOADER_SUPPORT__ to enable offseting:
#ifndef __BOOTLOADER_SUPPORT__
#define VECT_TAB_OFFSET 0x0U /*!< Vector Table base offset field. This value must be a multiple of 0x100. */
#else
#define VECT_TAB_OFFSET 0x8100U /**< Offset for bootloader size (32kb=0x8000) and application header (256b=0x100) */
#endif
NOTE: Vector table base offset shall be alligned with linker memory settigs!
Use Application Signature Tool to sign binary file.