目录
一、简单介绍
Bootloader 是嵌入式设备中负责引导和更新的关键程序。汽车的 ECU 在进行 OTA 升级时,Bootloader 负责下载、校验和写入新固件,确保出现异常时能回退到旧版本保障行车安全;手机系统更新依赖 Bootloader来刷机、验证签名并启动系统内核;电脑中的 BIOS 或 UEFI 同样是一类 Bootloader,用于初始化硬件并加载操作系统。通过统一的下载接口和安全机制,Bootloader 让设备可以远程维护、在线更新并防止恶意篡改,从而在降低生产和维护成本的同时,保证系统在整个生命周期中的稳定与安全。

归根结底,bootloader也是一个工程,和application性质相同
笔者实现的简易bootloader的作用主要有
- 擦除现存app
- 接收新的app固件
- 烧写到flash中
- 跳转到新app
二、开发思路
笔者是基于STM32F103CBT6开发,和常见的STM32F103C8T6相比,flash变成128kb

STM32单片机的flash启动模式下,启动地址总是0x8000000,这也是自己的bootloader的起始地址

bootloader大小为12616字节,需要12个page多一点点(0.32 page)
因此把判断标志放在page 12的开头,即地址0x08003400
App的起始地址为0x08003800,(page13的开头)
内存分布为

boot检测flag信号为true表明需要升级,擦除App并烧写新的固件;否则就表明不需要升级,直接跳转到App即可
三、实战开发
App
cubemx开启时钟,UART1和UART2,LED

App工程需要设置起始地址,使用keil的话如下设置

system_stm32f1xx.c文件中设置向量表偏移地址

App闪烁LED并在串口2发送App running作为演示

App的main函数代码如下
cpp
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
req.TypeErase = FLASH_TYPEERASE_PAGES;
req.Banks = FLASH_BANK_1;
req.PageAddress = APP_REQUEST_ADDRESS;
req.NbPages = 1;
/* go to wait state for getting next data from PC*/
updateReq = *(uint8_t*)(APP_REQUEST_ADDRESS);
HAL_UART_Receive_IT(&huart1, rx, 6);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_UART_Transmit(&huart2, tx, 14, 10);
HAL_Delay(100);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
Boot
cubemx开启时钟,串口

boot工程起始地址就是0x8000000,就不需要偏移了
设置地址宏和升级请求标志
cpp
#define APP_START_ADDRESS 0x08003800
#define APP_REQUEST_ADDRESS 0x08003400
updateReq = *(uint8_t*)(APP_REQUEST_ADDRESS);
判断是否需要升级
cpp
if (updateReq == 1 || updateReq == 0xFF)
{
state = STATE_INIT;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&req, &error);
}
else
{
state = STATE_JUMP;
}
在主循环内进行状态的切换
笔者制作了一个配套的上位机

上位机按下boot则发送由帧头0xAA+0x22222222+CRC组成的升级请求,App检测到后就设置标志位并复位

上位机按下flash则进行新固件的传输,在boot中进行接收和flash的写入
cpp
case STATE_FLASH:
{
/**
* flash code
*/
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, APP_START_ADDRESS + flashPackCnt * 4, *(uint32_t*) (prx + 1));
flashPackCnt++;
/* flash finish, notify PC by sending 0x77 */
tx[0] = 0x77;
HAL_UART_Transmit_IT(&huart1, tx, 1);
/* go to wait state for getting next data from PC*/
HAL_UART_Receive_IT(&huart1, rx, 6);
state = STATE_WAIT;
if ((flashPackCnt) * 4 == dataSize)
{
state = STATE_END;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, APP_REQUEST_ADDRESS, 0);
}
break;
}
上位机发送0x20+四字节的固件包+CRC,单片机接收后发送0x77作为应答信号,这里笔者作为演示,就直接明文传输了,实际应用一般会有加密措施和握手等步骤

烧写完毕后跳转到新的App中
cpp
case STATE_JUMP:
{
deinitEverything();
uint32_t stacktop = *((__IO uint32_t *)APP_START_ADDRESS);
__set_MSP(stacktop);
app_func_t app_func = (app_func_t)(*((__IO uint32_t *)(APP_START_ADDRESS + 4)));
app_func();
}
boot的main函数代码如下
cpp
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
updateReq = *(uint8_t*)(APP_REQUEST_ADDRESS);
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.Banks = FLASH_BANK_1;
erase.PageAddress = APP_START_ADDRESS;
erase.NbPages = 40;
req.TypeErase = FLASH_TYPEERASE_PAGES;
req.Banks = FLASH_BANK_1;
req.PageAddress = APP_REQUEST_ADDRESS;
req.NbPages = 1;
uint32_t error;
if (updateReq == 1 || updateReq == 0xFF)
{
state = STATE_INIT;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&req, &error);
}
else
{
state = STATE_JUMP;
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
switch (state)
{
case STATE_INIT:
{
/* prepare for size info by interrupt */
HAL_UART_Receive_IT(&huart1, rx, 6);
/* tell host the chip is in boot now */
tx[0] = 0x77;
HAL_UART_Transmit_IT(&huart1, tx, 1);
/* go to idle */
state = STATE_IDLE;
break;
}
case STATE_IDLE:
{
break;
}
case STATE_SIZE:
{
break;
}
case STATE_ERASE_BEGIN:
{
break;
}
case STATE_WAIT:
{
break;
}
case STATE_FLASH:
{
/**
* flash code
*/
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, APP_START_ADDRESS + flashPackCnt * 4, *(uint32_t*) (prx + 1));
flashPackCnt++;
/* flash finish, notify PC by sending 0xAA */
tx[0] = 0x77;
HAL_UART_Transmit_IT(&huart1, tx, 1);
/* go to wait state for getting next data from PC*/
HAL_UART_Receive_IT(&huart1, rx, 6);
state = STATE_WAIT;
if ((flashPackCnt) * 4 == dataSize)
{
state = STATE_END;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, APP_REQUEST_ADDRESS, 0);
}
break;
}
case STATE_END:
{
break;
}
case STATE_JUMP:
{
deinitEverything();
uint32_t stacktop = *((__IO uint32_t *)APP_START_ADDRESS);
__set_MSP(stacktop);
app_func_t app_func = (app_func_t)(*((__IO uint32_t *)(APP_START_ADDRESS + 4)));
app_func();
}
default:
break;
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
调试读取内存,0x8003800处的内容和编译的App的hex内容一致
由于升级请求被擦除,下一次复位就直接跳转至App运行了