一、前期规划(关键!决定后续实现难度)
1. 明确需求与功能边界
核心功能:是否需要串口/I2C/SPI/OTA升级?是否需要固件校验(CRC/MD5)?是否需要升级失败回滚?
触发方式:按键触发、串口指令触发、定时器触发(如定时检查升级标志)?
硬件限制:Flash大小(决定Bootloader和应用分区大小)、RAM大小(接收缓冲区大小)。
2. 内存分区规划(以STM32F103C8T6为例,64KB Flash + 20KB RAM)
原则:
Bootloader大小需预留冗余(基础功能16KB足够,复杂功能需32KB)。
应用程序起始地址必须是Flash扇区对齐的(如F103每页1KB,0x08004000是第4页起始)。
二、搭建Bootloader工程(CubeMX + MDK)
1. 新建CubeMX工程
选择芯片型号(如STM32F103C8T6)。
配置最小系统资源:
时钟:启用HSE,配置PLL(如72MHz,保证串口等外设稳定)。
升级通信外设:如串口(USART1,115200波特率,无校验)、GPIO(按键引脚,下拉输入)。
调试接口:保留SWD(方便调试Bootloader)。
2. 生成工程代码
选择MDK-ARM工具链,生成代码时勾选“Generate peripheral initialization as .c/.h files”。
打开生成的工程,确认时钟初始化(SystemClock_Config)、外设初始化(如MX_USART1_UART_Init)正确。
三、实现Bootloader核心功能
按“初始化→检测升级条件→升级流程→跳转应用”的逻辑编写代码。
1. 系统初始化(最小资源初始化)
仅初始化Bootloader必需的外设(避免占用过多资源影响应用跳转):
void Bootloader_Init(void) {
HAL_Init(); // HAL库初始化( systick 等)
SystemClock_Config(); // 时钟初始化(CubeMX生成)
MX_USART1_UART_Init(); // 升级用串口初始化
MX_GPIO_Init(); // 按键GPIO初始化(若用按键触发)
}
2. 升级触发条件检测
判断是否进入升级模式(示例:按键按下或串口接收“UPGRADE”指令):
uint8_t Check_Upgrade_Trigger(void) {
// 条件1:按键按下(PA0为高电平,消抖处理)
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
return 1; // 进入升级模式
}
}
// 条件2:串口接收“UPGRADE”指令(500ms内检测)
uint8_t rx_cmd[8] = {0};
if (HAL_UART_Receive(&huart1, rx_cmd, 7, 500) == HAL_OK) { // 超时500ms
if (memcmp(rx_cmd, "UPGRADE", 7) == 0) {
return 1; // 进入升级模式
}
}
return 0; // 直接跳转应用
}
3. Flash操作函数(擦除与写入)
STM32 Flash操作需先解锁,按页擦除,按字写入(4字节对齐):
#include "stm32f1xx_hal_flash_ex.h"
// Flash解锁
void Flash_Unlock(void) {
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
}
// Flash锁定
void Flash_Lock(void) {
HAL_FLASH_Lock();
}
// 擦除应用程序区域(从0x08004000开始,共48页)
uint8_t Flash_Erase_AppArea(void) {
FLASH_EraseInitTypeDef erase_init;
uint32_t page_error = 0;
erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
erase_init.PageAddress = 0x08004000; // 应用起始地址
erase_init.NbPages = 48; // 48页=48KB(F103C8T6共64KB,Bootloader占16KB)
if (HAL_FLASHEx_Erase(&erase_init, &page_error) != HAL_OK) {
return 0; // 擦除失败
}
return 1; // 擦除成功
}
// 写入数据到Flash(addr:起始地址,data:数据,len:4字节单位的长度)
uint8_t Flash_Write(uint32_t addr, uint32_t *data, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i*4, data) != HAL_OK) {
return 0; // 写入失败
}
}
return 1; // 写入成功
}
4. 接收固件并写入Flash(以串口为例)
需定义简单通信协议(示例:先发送4字节固件大小,再发送固件数据,每包1024字节):
#define APP_ADDR 0x08004000 // 应用程序起始地址
uint8_t rx_buf[1024]; // 接收缓冲区(1024字节=256个32位字)
uint8_t Receive_and_Write_Firmware(void) {
uint32_t firmware_size = 0; // 固件总大小(字节)
uint32_t received = 0; // 已接收字节数
// 1. 接收固件大小(4字节,小端模式)
if (HAL_UART_Receive(&huart1, (uint8_t*)&firmware_size, 4, 10000) != HAL_OK) {
HAL_UART_Transmit(&huart1, (uint8_t*)"SizeErr", 7, 100);
return 0;
}
// 2. 擦除应用区域(先解锁)
Flash_Unlock();
if (!Flash_Erase_AppArea()) {
HAL_UART_Transmit(&huart1, (uint8_t*)"EraseErr", 8, 100);
Flash_Lock();
return 0;
}
// 3. 循环接收固件数据并写入
while (received < firmware_size) {
// 计算当前包长度(不超过缓冲区和剩余大小)
uint16_t pkg_len = (firmware_size - received) > 1024 ? 1024 : (firmware_size - received);
// 接收一包数据(超时10秒)
if (HAL_UART_Receive(&huart1, rx_buf, pkg_len, 10000) != HAL_OK) {
HAL_UART_Transmit(&huart1, (uint8_t*)"RxErr", 5, 100);
Flash_Lock();
return 0;
}
// 转换为32位数据(4字节对齐,不足补0)
uint32_t write_data[256] = {0}; // 1024字节=256个32位
memcpy(write_data, rx_buf, pkg_len);
// 写入Flash(地址=应用起始+已接收字节数,长度=pkg_len/4向上取整)
uint32_t write_len = pkg_len / 4 + (pkg_len % 4 ? 1 : 0);
if (!Flash_Write(APP_ADDR + received, write_data, write_len)) {
HAL_UART_Transmit(&huart1, (uint8_t*)"WriteErr", 8, 100);
Flash_Lock();
return 0;
}
received += pkg_len;
HAL_UART_Transmit(&huart1, (uint8_t*)"OK", 2, 100); // 发送确认
}
Flash_Lock(); // 写完锁定Flash
return 1;
}
5. 跳转至应用程序(核心!必须保证跳转正确)
应用程序的首地址是栈顶,次地址是复位函数入口,需按如下步骤跳转:
typedef void (*pFunction)(void); // 函数指针类型
void Jump_To_Application(void) {
pFunction app_reset_handler; // 应用程序复位函数
uint32_t app_stack_top; // 应用程序栈顶地址
// 1. 检查应用程序是否存在(栈顶地址是否在RAM范围内)
app_stack_top = *(volatile uint32_t*)APP_ADDR; // 应用首地址是栈顶
if (app_stack_top < 0x20000000 || app_stack_top > 0x20005000) { // F103C8T6 RAM:0x20000000~0x20005000(20KB)
HAL_UART_Transmit(&huart1, (uint8_t*)"NoApp", 5, 100);
return; // 无有效应用,不跳转
}
// 2. 关闭中断,避免Bootloader中断干扰应用
__disable_irq();
// 3. 复位外设(可选,根据应用需求,如关闭Bootloader开启的时钟)
HAL_RCC_DeInit(); // 复位RCC,恢复默认时钟(应用需重新初始化时钟)
// 4. 设置中断向量表偏移(应用的中断向量表在自己的起始地址)
SCB->VTOR = APP_ADDR; // 向量表偏移到应用起始地址
// 5. 设置栈顶指针,跳转至应用复位函数
__set_MSP(app_stack_top); // 更新主栈指针为应用栈顶
app_reset_handler = (pFunction)*(volatile uint32_t*)(APP_ADDR + 4); // 应用次地址是复位函数
app_reset_handler(); // 跳转执行应用程序
}
6. 主函数逻辑整合
int main(void) {
Bootloader_Init(); // 初始化最小系统
HAL_UART_Transmit(&huart1, (uint8_t*)"Bootloader Ready\r\n", 19, 100);
// 检测升级触发条件
if (Check_Upgrade_Trigger()) {
HAL_UART_Transmit(&huart1, (uint8_t*)"Enter Upgrade Mode\r\n", 19, 100);
// 接收并写入固件
if (Receive_and_Write_Firmware()) {
HAL_UART_Transmit(&huart1, (uint8_t*)"Upgrade Success\r\n", 17, 100);
} else {
HAL_UART_Transmit(&huart1, (uint8_t*)"Upgrade Failed\r\n", 16, 100);
}
}
// 跳转至应用程序
HAL_UART_Transmit(&huart1, (uint8_t*)"Jump to App...\r\n", 16, 100);
Jump_To_Application();
// 若跳转失败,进入死循环
while (1) {
HAL_Delay(1000);
HAL_UART_Transmit(&huart1, (uint8_t*)"Jump Failed\r\n", 13, 100);
}
}
四、应用程序适配(必须修改!否则应用无法运行)
应用程序需修改起始地址和中断向量表偏移,才能被Bootloader正确跳转:
1. 修改应用程序的链接地址(MDK)
打开应用工程→Options for Target→Linker→取消勾选“Use Memory Layout from Target Dialog”→启用“Scatter File”,编写分散加载文件(如app.sct):
LR_IROM1 0x08004000 0x0000C000 { ; 起始地址0x08004000,大小48KB(0xC000字节)
ER_IROM1 0x08004000 0x0000C000 { *.o } ; 代码段
RW_IRAM1 0x20000000 0x00005000 { *.o } ; 数据段(RAM)
}
2. 设置应用程序的中断向量表偏移
在应用程序main函数开头添加:
int main(void) {
// 设置中断向量表偏移到应用程序起始地址(0x08004000)
SCB->VTOR = 0x08004000;
// 后续初始化(HAL_Init、时钟等)
HAL_Init();
SystemClock_Config();
// ... 其他代码
}
3. 生成应用程序固件(.bin文件)
MDK中通过fromelf工具生成:在工程Options for Target→User→After Build/Rebuild添加命令:
fromelf --bin -o ./Output/app.bin ./Output/app.axf
(需替换app.axf为实际输出文件名)。
五、测试与验证
1. 烧录Bootloader
通过ST-Link将Bootloader程序烧录到STM32的0x08000000地址(默认启动地址)。
2. 测试升级流程
上电后,Bootloader启动,若触发升级(如按下按键),通过串口工具发送固件:
① 先发送4字节固件大小(小端模式,如0x12345678表示固件大小为0x78563412字节);
② 再发送应用程序的.bin文件数据;
③ 接收“Upgrade Success”后,重启开发板,Bootloader会自动跳转至新应用。
3. 验证跳转功能
未触发升级时,Bootloader应直接跳转至应用程序,应用能正常运行(如LED闪烁、串口打印)。
六、关键注意事项
Flash对齐:写入地址和数据必须4字节对齐(STM32 Flash硬件要求)。
中断关闭:跳转前必须用__disable_irq()关闭中断,避免Bootloader的中断服务程序干扰应用。
栈顶检查:应用程序的栈顶地址必须在RAM范围内(否则视为无效应用)。
固件校验:实际产品中需添加CRC32校验(发送固件时附加校验值,Bootloader接收后验证),防止固件损坏。
兼容性:不同STM32系列(如F1/F4/H7)的Flash操作函数略有差异(如擦除类型、页大小),需根据手册调整。
————————————————
版权声明:本文为CSDN博主「Baizhou12138」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Baizhou12138/article/details/153644863
|
|