Hardware Driver Interface Reference¶
All six driver modules live in hw/<TARGET>/Core/Bl_drv_interface/. Each module has a .h header that declares the interface and a .c implementation file. The bootloader core (src/main_app.c) depends only on the headers — never on the implementation files.
Start from hw/STM32_TEMPLATE/Core/Bl_drv_interface/ which contains skeleton implementations with TODO comments for every function.
bl_usart_drv — UART Communication¶
Header: bl_usart_drv.h
typedef void (*data_rx_cb_t)(uint8_t data);
void init_bl_usart(void);
void register_data_rx_cb(data_rx_cb_t callback);
void deinit_bl_usart_periph(void);
void deinit_bl_usart_clk(void);
void send_byte_table(const uint8_t *d, size_t l);
void send_byte(uint8_t b);
bool byte_transmission_complete(void);
Requirements¶
| Function | Contract |
|---|---|
init_bl_usart() |
Configure the UART for 115200 baud, 8N1. Enable the RX-complete interrupt. Enable the NVIC for the UART IRQ. |
register_data_rx_cb(cb) |
Store cb. Call it from the RX ISR for every received byte, passing the byte value. Must be safe to call before init_bl_usart(). |
deinit_bl_usart_periph() |
Disable the UART peripheral and its NVIC IRQ. Restore GPIO pins to their reset state. |
deinit_bl_usart_clk() |
Gate off the UART clock in RCC. Call after deinit_bl_usart_periph(). |
send_byte(b) |
Wait until the transmit data register is empty, then write b. Must include a timeout guard to prevent infinite blocking if the UART is in a fault state. |
send_byte_table(d, l) |
Call send_byte() for each of the l bytes at d. |
byte_transmission_complete() |
Return true when the TC (transmission complete) flag is set — i.e., the last byte has fully shifted out on the wire. Used by do_reset() before calling system_reset(). |
ISR pattern¶
void USARTx_IRQHandler(void)
{
uint32_t isr = USARTx->ISR;
/* Clear any error flags first */
if (isr & (USART_ISR_ORE | USART_ISR_FE | USART_ISR_NE | USART_ISR_PE)) {
USARTx->ICR = ...;
if (isr & USART_ISR_RXNE_RXFNE) (void)USARTx->RDR;
return;
}
/* Forward received byte to the protocol FSM */
if (isr & USART_ISR_RXNE_RXFNE) {
uint8_t c = (uint8_t)USARTx->RDR;
if (byte_rx_cb != NULL) byte_rx_cb(c);
}
}
bl_flash_drv — Internal Flash¶
Header: bl_flash_drv.h
void FLASH_waitBusy(void);
void FLASH_unlock(void);
void FLASH_lock(void);
void FLASH_erasePage(uint32_t flash_page_no);
void FLASH_write(uint32_t addr, uint32_t const *data, size_t dataLen);
Requirements¶
| Function | Contract |
|---|---|
FLASH_waitBusy() |
Poll the flash busy flag (BSY in FLASH->SR) until clear. |
FLASH_unlock() |
Write FLASH_KEY1 then FLASH_KEY2 to FLASH->KEYR. Call FLASH_waitBusy() before and after. |
FLASH_lock() |
Set the LOCK bit in FLASH->CR. Call FLASH_waitBusy() before. |
FLASH_erasePage(page_no) |
Erase the page identified by page_no (0-based page index). Clear error flags first. Set PER/PNB, set STRT, wait busy, clear PER. |
FLASH_write(addr, data, dataLen) |
Write dataLen double-words (2 × uint32_t = 8 bytes each) starting at addr. Each double-word requires two consecutive 32-bit writes followed by a busy wait. |
FLASH_write data layout¶
dataLen counts 8-byte units (one double-word each). The core always calls this function with dataLen = sizeof(page_buf) / (2 * sizeof(uint32_t)) minus any adjustment for the deferred first 8 bytes.
/* STM32G0/G4 typical write loop */
for (size_t i = 0; i < dataLen; i++) {
*(volatile uint32_t *) addr = *data++; // low word
*(volatile uint32_t *)(addr + 4U) = *data++; // high word
FLASH_waitBusy();
FLASH->SR &= ~FLASH_SR_EOP;
addr += 8U;
}
Flash keys in .c only
Do not define FLASH_KEY1 / FLASH_KEY2 in bl_hw_config.h. Define them as file-scope constants in bl_flash_drv.c where they are used. Keeping unlock keys out of the public header prevents accidental exposure.
bl_key_drv — Push-Button Input¶
Header: bl_key_drv.h
void init_bl_key(void);
void deinit_bl_key(void);
void deinit_bl_key_clk(void);
bool bl_key_pressed(void);
Requirements¶
| Function | Contract |
|---|---|
init_bl_key() |
Enable the GPIO clock. Configure the button pin as a digital input. Enable internal pull-up if the button is active-low (typical). |
deinit_bl_key() |
Reset the pin to its power-on state (analog/input, no pull). |
deinit_bl_key_clk() |
Gate off the GPIO port clock in RCC. Call after all pins on the port have been deinitialized. |
bl_key_pressed() |
Return true when the button is in the pressed state. Must be side-effect-free; the core calls it from bl_key_check() exactly once per main-loop iteration. |
Active-level convention¶
The convention (active-low vs. active-high) is fully encapsulated inside bl_key_drv.c. The bootloader core only sees the logical bool result.
/* Example: active-low button on PC13 (NUCLEO USER button) */
bool bl_key_pressed(void)
{
bool pressed = true;
if ((GPIOC->IDR & GPIO_IDR_ID13_Msk) == GPIO_IDR_ID13_Msk)
pressed = false; /* pin high → button released */
return pressed;
}
bl_status_led_drv — Status LED Output¶
Header: bl_status_led_drv.h
void init_bl_status_led(void);
void deinit_bl_status_led_port(void);
void deinit_bl_status_led_clk(void);
void toggle_bl_led(void);
Requirements¶
| Function | Contract |
|---|---|
init_bl_status_led() |
Enable the GPIO clock. Configure the LED pin as push-pull output. Drive it low (LED off) initially. |
deinit_bl_status_led_port() |
Reset the pin to its power-on state (input, no pull, LED off). |
deinit_bl_status_led_clk() |
Gate off the GPIO port clock. Call after deinit_bl_status_led_port(). |
toggle_bl_led() |
Toggle the LED output. Use ODR ^= pin or BSRR-based atomic toggle. Called from update_bl_status_led() every 500 ms. |
bl_crc_drv — Hardware CRC (optional)¶
Header: bl_crc_drv.h (includes crc_api.h)
Only compiled when CRC_MODULE=HW. The file implements the same crc_api.h interface as the software implementation.
/* crc_api.h — shared interface for both SW and HW implementations */
void CRC_hw_init(void);
uint32_t CRC_init(void);
uint32_t CRC_add_byte(uint32_t crc, uint8_t b);
uint32_t CRC_add_byte_tab(uint32_t crc, uint8_t const *data, size_t dataLen);
uint32_t CRC_result(uint32_t crc);
STM32 HW CRC requirements¶
The STM32 CRC peripheral must be configured for CRC-32/IEEE 802.3:
| Register field | Setting |
|---|---|
CRC->INIT |
0xFFFFFFFF |
CRC_CR_REV_IN |
01b (byte reversal) |
CRC_CR_REV_OUT |
1 (output reversal) |
CRC_CR_RESET |
Write 1 to load INIT |
| Final XOR | ^ 0xFFFFFFFF applied in CRC_result() |
uint32_t CRC_init(void)
{
CRC->INIT = 0xFFFFFFFFUL;
CRC->CR = CRC_CR_REV_IN_0 | CRC_CR_REV_OUT | CRC_CR_RESET;
return CRC->DR; /* flush pipeline */
}
uint32_t CRC_add_byte_tab(uint32_t crc, uint8_t const *data, size_t dataLen)
{
(void)crc;
while (dataLen--)
*((volatile uint8_t *)&CRC->DR) = *data++;
return CRC->DR;
}
uint32_t CRC_result(uint32_t crc)
{
(void)crc;
return CRC->DR ^ 0xFFFFFFFFUL;
}
No rev_u32 export
rev_u32 is an internal implementation detail of crc.c. It is declared static and must not appear in bl_crc_drv.h or crc_api.h.
core_drv — Core / SysTick / App Jump¶
Header: core_drv.h
typedef void (*SysTick_cb_t)(void);
void init_sys_tick(void);
void deinit_sys_tick(void);
void register_sysTick_cb(SysTick_cb_t callback);
void jump_to_app(void);
void system_reset(void);
bool CORE_is_valid_app(void);
Requirements¶
| Function | Contract |
|---|---|
init_sys_tick() |
Configure SysTick for a 100 ms interrupt. Enable the SysTick ISR. The ISR must call the registered callback. |
deinit_sys_tick() |
Stop SysTick (clear CTRL register). Clear LOAD and VAL. |
register_sysTick_cb(cb) |
Store cb. Call it from SysTick_Handler(). |
jump_to_app() |
Transfer execution to the application. See sequence below. |
system_reset() |
Trigger an immediate system reset. On Cortex-M: NVIC_SystemReset(). On AVR: watchdog with shortest timeout. |
CORE_is_valid_app() |
Return true only if the application vector table at APP_START contains a plausible SP and reset vector. See Security Model. |
jump_to_app() sequence (Cortex-M)¶
void jump_to_app(void)
{
volatile uint32_t *app_begin = (volatile uint32_t *)APP_START;
uint32_t stack = *app_begin;
uint32_t start = *(app_begin + 1);
__disable_irq();
/* Disable and clear all pending IRQs */
NVIC->ICER[0] = 0xFFFFFFFFUL; /* Cortex-M0/M0+: one register */
NVIC->ICPR[0] = 0xFFFFFFFFUL;
/* For Cortex-M4 (G474): iterate all ICER/ICPR registers */
SCB->VTOR = APP_START; /* relocate vector table */
__DSB();
__ISB();
__set_MSP(stack);
__DSB();
__ISB();
((void (*)(void))start)(); /* branch to application reset handler */
}
SysTick period formula
SysTick->LOAD = CPU_F / 10 - 1 gives exactly 100 ms at CPU_F Hz using the processor clock source.
Driver Initialisation Order¶
init_hardware() in main_app.c calls drivers in this sequence:
1. init_bl_key() GPIO — safe to call first; no interrupts
2. init_bl_status_led() GPIO — same
3. register_data_rx_cb(byte_rx_cb) register callback before enabling ISR
4. init_bl_usart() enables RX interrupt → ISR now live
5. register_sysTick_cb(...) register callback before enabling SysTick
6. init_sys_tick() starts the 100 ms tick
7. CRC_hw_init() enable CRC peripheral clock (HW mode only)
deinit_hardware() reverses in a safe order:
1. deinit_bl_usart_periph() disable ISR before touching GPIO
2. deinit_sys_tick() stop tick
3. deinit_bl_status_led_port() GPIO
4. deinit_bl_key() GPIO
5. deinit_bl_usart_clk() clock gate after peripheral disabled
6. deinit_bl_status_led_clk() clock gate
7. deinit_bl_key_clk() clock gate last (shared port possible)