一、什么是、为什么要写bootloader?
首先是一个场景:比如我们做了一款智能洗碗机,这款洗碗机带有无线功能,能连接到app,用app进行控制,后来这款产品量产卖出去了,但是用户买到手之后反馈说这洗碗机太废水了。我们根据现有的洗碗机硬件去改良算法,最后算法搞出来了,我们要怎么把新的程序装到用户的洗碗机上呢?
注明:本项目以STM32F411CEU6为例,使用VSCODECube插件+CMake环境进行开发
按照以往开发的经验,我们是这样实现程序烧录的:
但是用户那边就是买个洗碗机来洗碗的,又不是开发人员,没有这么多工具啊!要怎么实现烧录的功能呢?
Bootloader登场,我们先来看看具体对比:
我们开发的时候使用Link进行烧录,是通过单片机内部的预设程序(这一部分无法更改,是芯片厂商提前烧录的,只能接收固定几个协议的数据,且需要专业开发软件工具等)进行烧录(具体就是把Flash擦除,然后把电脑发来的数据覆盖进去)
那么如果我们提前设计了bootloader,我们给用户手机发升级包,用户点一下app,自动把新程序发到单片机里面(具体是我们写的预留程序bootloader,能接收用户发来的数据,然后我们的预留程序擦除一部分的Flash,把新代码搬进去,然后只执行即可)这样子用户只要会用手机app就能实现单片机的程序升级了。这样子看的话,其实我们就是写一段bootloader代码把厂商预设的程序给顶替掉了
bootloader其实就是我们自己写的一段引导代码,通过检测有没有升级信号,然后把接收到的数据搬到指定的Flash里面,然后执行。这就给了我们很多的灵活性,我们只要能发送数据就能够升级程序,不局限于开发工具的通讯协议,我们可以使用UART、IIC、SPI、CAN、USB、WIFI、BLE等等,只要能够发送数据就可以。那么我们的单片机其实里面包含两段独立但是有关联的代码,一个是bootloader,一个是app程序,我们日常的时候主要是执行app的代码,只有程序升级的时候会跳转到bootloader里面,那么我们最关键的其实就是协调两段代码之间的关系
至于为什么一定要分成两段独立的代码,我可以做一下简单的说明,我们的bootloader是接收外面的更新代码程序,然后把程序放到Flash里面。如果我们把bootloader和app乱放,放在一起(我们要清楚Flash只能按扇区块擦除)。你这样一看,两个代码怼在一起,如果要把APP擦除的话,只能把两个块都擦除掉,如果bootloader被擦除,那么就没有了"数据的搬运工"了,这玩意到了用户手中就成了砖头。如果只擦除部分app的话,那还剩下一点点app,大概率会影响程序的运行。除了这些还有一些深层次的原因,需要往下继续看
我们根据Flash的分块大小,分出一小块给bootloader,其余的都划分给app程序,我们查一下STM32F411数据手册,找到主存储器扇区划分(我们烧录程序的地方),我们把起始扇区0划分为bootloader区域,为什么选择扇区0呢?因为他是程序的起始位置0x08000000,进来的时候就会进行初始化等,大小也足够,不会占用太多的位置。那么app程序的起始位置就是0x08004000(也可以往下几个扇区延后,但是这样子就会缩小app空间)
二、单片机启动简化步骤
要想自己写Bootloader,我们需要一点前置知识。在以往的开发中,我们默认单片机启动后就会进入main函数,其实不然,下面我来简单说一说具体的流程(一部分):
1、初始化堆栈指针 SP = _initial_sp (告诉单片机栈要从哪里写进入,也就是函数中的变量要储存在哪里,重要)
2、初始化程序计数器指针PC = Reset_Handler(告诉单片机复位后要从哪里运行,会指向单片机的初始化函数_main的地址,我们不用动他,但是重要)
3、设置堆和栈的大小(告诉单片机堆栈的大小,我们不用动他)
4、初始化中断向量表(告诉单片机触发中断后要在哪里找中断函数,重要)
5、调用 C库中的 _main 函数初始化用户堆栈,最终调用 main 函数
那他一共执行了这么多个步骤,这么复杂,和bootloader有什么鸟关系呢?首先我们知道我们需要有两个程序bootloader和app,假设我们的bootloader的main函数里面有这些东西:初始化时钟、初始化串口、检测更新标志、更新程序(擦除Flash,串口搬数据,写Flash)
简单写就是这样:
int main() { 系统初始化(); if (收到升级信号) { 进入升级模式(); 跳转到主程序(); } else { 跳转到主程序(); } }三、如何跳转程序
我们可能会认为“跳转到主程序();”只需要把app下main函数的函数地址找出来执行就好了,但其实没有那么简单。假设我们已经写好了程序,那么程序在Flash中的分布大概会是这样子的,两个代码:bootloader代码起始位置0x08000000,app程序的起始位置就是0x08004000
图中上面的三个数据(中断向量表、栈顶、复位向量)是代码刚刚启动进入bootloader程序设置的值,那么现在摆在我们面前的有两种情况:
1.接收不到更新信号,需要跳转程序到app
2.接收到更新信号,要进入升级程序,再跳转程序到app
为了方便验证和学习,我们先从跳转程序说起,首先我们得写一个仅仅有跳转功能的bootloader程序(仅需要初始化时钟,和一个输出的GPIO口就好),外加上一个LED闪烁的APP程序
下面是bootloader跳转代码的撰写:
/* Bootloader for STM32F411 */ #include <stdint.h> #include "bootloader.h" #include "stm32f4xx_hal_gpio.h" #define APP_START_ADDR 0x08004000 #define BOOTLOADER_SIZE 0x4000 // 16KB void JumpToApplication(uint32_t app_addr); int bootloader(void) { // 检查 App 栈顶是否有效(合理范围:0x20000000 ~ 0x2001FFFF for 128KB SRAM) uint32_t stack_top = *(__IO uint32_t*)APP_START_ADDR; if ((stack_top & 0x2FF00000) == 0x20000000) { JumpToApplication(APP_START_ADDR); } // 无效 App,停留在 Bootloader(可加 LED 闪烁提示) while (1) { HAL_Delay(100); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(100); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); } } void JumpToApplication(uint32_t app_addr) { typedef void (*pFunc)(void); pFunc appEntry; /* 关闭全局中断 */ __set_PRIMASK(1); /* 关闭滴答定时器,复位到默认值 */ SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; /* 设置所有时钟到默认状态 */ HAL_RCC_DeInit(); /* 关闭所有中断,清除所有中断挂起标志 */ for (uint8_t i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } /* 使能全局中断 */ __set_PRIMASK(0); /* 设置为特级模式,使用MSP指针 */ __set_CONTROL(0); __set_MSP(*(__IO uint32_t*)app_addr); // 修改中断向量表位置 SCB->VTOR = app_addr; // 获取并跳转到 App Reset Handler appEntry = (pFunc)(*(__IO uint32_t*)(app_addr + 4)); appEntry(); }可以看见呢,我们函数一上来就是检查APP的栈起始地址是否安全(0x20000000-0x2001FF00一共是128K,不能超出这个地址的范围),如果地址安全,下一步就是关闭全局中断,以免有中断会打断跳转程序,再是复位滴答定时器,在开启中断(不然跳到程序那边没中断可以用),设置app的新栈顶,修改中断向量表到app程序(以免中断后去bootloader找中断函数,上面启动流程上有写),最后跳转到app的复位向量位置(没有重新设置复位向量,单片机复位后依旧是到bootloader),进行初始化,可以通过下图看一下变化
那么写下来的这个代码在烧录的时候要注意,在根目录下找到.ld文件,要设置Flash的起始位置为0X08000000,最大范围为16K,如果超出的话会报错
接下来是appLED闪烁程序的撰写:
就是简单的LED闪烁,没什么特别的
HAL_Delay(4000); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(4000); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);接下来在烧录的时候也要注意,偏移地址的位置
我们将两个程序烧录至单片机里面,如果LED是缓慢的闪烁,就表示我们我们成功了,如果失败的话,我们需要检查一下STM32里面的Flash具体情况,打开STM32Program
我们可以看到0x08000000和0x08004000的位置和下一个数据,第一个就是栈顶,在正常范围内,第二个就是复位向量。如果异常的话,就要我们自己检查一下是哪里出问题了
面试必背:
1. APP 跳转到 Bootloader(软件复位/升级触发)
场景:APP 收到升级指令,需要重启并进入 Bootloader 进行固件下载。
通信方式:APP 在内存特定地址(如后备寄存器或RAM保留区)写入“升级标志”(Magic Number)。
触发动作:调用
NVIC_SystemReset()(Cortex-M内核复位)。严禁使用普通函数指针直接跳转,因为此时外设(串口、DMA、USB)都处于开启状态,直接跳转会导致 Bootloader 中断向量表错乱或硬错误。复位后的判断:MCU 复位后,Bootloader 最先执行的汇编代码中,立即读取该升级标志。如果标志匹配,则进入升级模式(擦写Flash、接收固件);如果不匹配,则直接跳转 APP。
2. Bootloader 跳转到 APP(正常启动/升级完成)
这是最精细、最容易翻车的步骤,必须严格按照“五步走”:
合法性校验(防变砖):检查 APP 首地址(向量表起始)的栈顶指针(
MSP)是否在合法 RAM 范围内(如0x20000000到0x20008000)。如果栈顶地址非法,说明 APP 固件损坏,拒绝跳转,停留在 Bootloader 等待救援。关闭所有外设中断(极重要):跳转前必须调用
HAL_DeInit()或手动关闭SysTick、所有使能的 NVIC 中断、DMA。否则跳转后,APP 的中断向量表未初始化,外设中断一触发,程序立马飞进 HardFault。重设中断向量表偏移(VTOR):APP 的起始地址通常是
0x0800xxxx(如0x08008000)。执行SCB->VTOR = APP_START_ADDR,告诉 CPU 新的中断入口在哪里。设置主堆栈指针(MSP):从 APP 起始地址取出前 4 字节(栈顶值),赋值给
__set_MSP(*(uint32_t*)APP_START_ADDR)。跳转(PC指针):从 APP 起始地址 + 4 处取出复位向量,通过函数指针强制跳转:
((void (*)(void))(*(uint32_t*)(APP_START_ADDR + 4)))();。
3. 看门狗(Watchdog)扮演的“终极守门人”角色
如果升级过程死机导致设备变砖,后果严重。看门狗在 Bootloader 中有两大生死攸关的作用,面试时务必分两点答:
作用一:防止升级中断“永远卡死”。
在擦除/写入 Flash 的大数据量过程中(如写外部 QSPI Flash 或内部扇区),必须在循环内喂狗(IWDG->KR = 0xAAAA)。如果写入失败或通讯超时,不要死等,等待看门狗超时复位 MCU。复位后,Bootloader 检测到升级未完成,自动重试或切回备份分区。作用二:利用“复位原因寄存器”实现救砖(杀手锏)。
上电后 Bootloader 第一步必须读取RCC->CSR(复位状态寄存器)。如果检测到是IWDG(独立看门狗)复位,而不是上电复位或软件复位,说明上一次 APP 启动失败或升级过程掉电了。
此时 Bootloader 的策略应改为:“无视升级标志,强制切回备份分区(A/B 系统中的 Slot B),并恢复出厂默认参数”。这叫“看门狗引导的自动回滚”,是工业级产品防变砖的最后一道防线。
四、串口烧录程序流程
接下来就是我们的重头戏了,我们将引入串口更新的操作,下面是具体的流程图,大家可以根据自己的需求简化一下,我在app的串口处使用了环形缓存区,会有一点点复杂,可以改为直接接收就好
看上图的流程会有一点复杂,大家可以自行修改,只要能识别到更新信号就可以(也可以是按钮之类的,方便操作)
下面是更新的具体通讯方式:
关于如何导出烧录文件,我们在根目录下的cmakelist中添加这一段
# --- 生成 .bin 文件(修复版)--- add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_OBJCOPY} ARGS -O binary $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin COMMENT "正在生成 ${CMAKE_PROJECT_NAME}.bin" )之后我们可以在“\build\Debug\”中找到.bin,我们可以执行烧录的文件
五、具体实现代码
1.bootloader代码(只需在main中初始化时钟、串口、GPIO后调用int bootloader(void))
/* main.c - Bootloader for STM32F411 */ #include <stdint.h> #include "bootloader.h" #include "stm32f4xx_hal_gpio.h" #define APP_START_ADDR 0x08004000 #define BOOTLOADER_SIZE 0x4000 // 16KB #define BOOTLOADER_MAGIC_ADDR 0x20018000 #define BOOTLOADER_MAGIC_VALUE 0xDEADBEEF void JumpToApplication(uint32_t app_addr); void HandleFirmwareUpdate(void); uint8_t ReceivePacket(uint8_t* data, uint16_t* len, uint32_t timeout); uint32_t ReadWordFromBuffer(uint8_t* buf); void EraseAppSector(uint32_t addr); uint32_t GetSector(uint32_t addr); uint8_t EraseEntireApplicationArea(void); uint8_t ShouldEnterBootloader(void); int bootloader(void) { uint32_t start = HAL_GetTick(); // 获取是否有标志更新信号 if (ShouldEnterBootloader()) { uint8_t ack = 0x79; // STM32 bootloader ACK HAL_UART_Transmit(&huart1, &ack, 1, 100); while (HAL_GetTick() - start < 10000) { // 双重保险,等待回应 if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) { uint8_t cmd; if (HAL_UART_Receive(&huart1, &cmd, 1, 100) == HAL_OK) { if (cmd == 0x7F) // 使用简单命令字节触发更新 { HAL_UART_Transmit(&huart1, &ack, 1, 100); HandleFirmwareUpdate(); } } } HAL_Delay(10); } } // 检查 App 栈顶是否有效(合理范围:0x20000000 ~ 0x2001FFFF for 128KB SRAM) uint32_t stack_top = *(__IO uint32_t*)APP_START_ADDR; if ((stack_top & 0x2FF00000) == 0x20000000) { JumpToApplication(APP_START_ADDR); } // 无效 App,停留在 Bootloader(可加 LED 闪烁提示) while (1) { HAL_Delay(100); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(100); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); } } // 检查是否需要进入Bootloader模式 uint8_t ShouldEnterBootloader(void) { uint32_t magic = *(__IO uint32_t*)BOOTLOADER_MAGIC_ADDR; if (magic == BOOTLOADER_MAGIC_VALUE) { // 清除标志 *(__IO uint32_t*)BOOTLOADER_MAGIC_ADDR = 0; return 1; } return 0; } void HandleFirmwareUpdate(void) { uint8_t packet[512]; uint16_t len; // 进入更新模式指示 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 1. 预先擦除整个应用程序区域 if (!EraseEntireApplicationArea()) { uint8_t nack = 0x1F; HAL_UART_Transmit(&huart1, &nack, 1, 100); return; } // 2. 发送擦除完成确认 uint8_t ack = 0x79; HAL_UART_Transmit(&huart1, &ack, 1, 100); // 3. 开始接收并写入数据 while (1) { if (ReceivePacket(packet, &len, 10000)) // 10秒超时 { if (len == 4) { // 跳转命令 uint32_t cmd = ReadWordFromBuffer(packet); if (cmd == 0xAA55AA55) { uint8_t ack = 0x79; HAL_UART_Transmit(&huart1, &ack, 1, 100); HAL_Delay(100); // 重置系统,让bootloader重新检查并跳转 NVIC_SystemReset(); } } else if (len >= 5) { uint32_t addr = ReadWordFromBuffer(packet); uint8_t* data = &packet[4]; uint16_t data_len = len - 4; // 安全检查:必须在 App 区域且4字节对齐 if (addr < APP_START_ADDR || addr >= 0x08080000 || (addr & 0x3)) { uint8_t nack = 0x1F; HAL_UART_Transmit(&huart1, &nack, 1, 100); continue; } // 直接写入Flash(扇区已经预先擦除) HAL_FLASH_Unlock(); HAL_StatusTypeDef flash_status = HAL_OK; for (uint16_t i = 0; i < data_len; i += 4) { uint32_t word = 0xFFFFFFFF; uint16_t bytes_remaining = data_len - i; uint16_t bytes_to_copy = (bytes_remaining < 4) ? bytes_remaining : 4; memcpy(&word, &data[i], bytes_to_copy); flash_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word); if (flash_status != HAL_OK) { break; } } HAL_FLASH_Lock(); if (flash_status == HAL_OK) { uint8_t ack = 0x79; HAL_UART_Transmit(&huart1, &ack, 1, 100); } else { uint8_t nack = 0x1F; HAL_UART_Transmit(&huart1, &nack, 1, 100); } } } else { // 接收超时,退出更新模式 break; } } } // 擦除整个应用程序区域 uint8_t EraseEntireApplicationArea(void) { FLASH_EraseInitTypeDef erase; uint32_t sector_error; // 计算需要擦除的扇区范围 // APP_START_ADDR = 0x08004000 从 Sector 1 开始 uint32_t first_sector = GetSector(APP_START_ADDR); uint32_t last_sector = GetSector(0x08080000 - 1); // 最后一个扇区 erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; erase.Sector = first_sector; erase.NbSectors = last_sector - first_sector + 1; HAL_FLASH_Unlock(); // 执行擦除 if (HAL_FLASHEx_Erase(&erase, §or_error) != HAL_OK) { HAL_FLASH_Lock(); return 0; } HAL_FLASH_Lock(); return 1; } // 根据地址获取扇区号(STM32F411) uint32_t GetSector(uint32_t addr) { uint32_t sector = 0; uint32_t offset = addr - 0x08000000; if (offset < 0x4000) sector = FLASH_SECTOR_0; // 16KB else if (offset < 0x8000) sector = FLASH_SECTOR_1; // 16KBAPP从这里开始 else if (offset < 0xC000) sector = FLASH_SECTOR_2; // 16KB else if (offset < 0x10000) sector = FLASH_SECTOR_3; // 16KB else if (offset < 0x20000) sector = FLASH_SECTOR_4; // 64KB else if (offset < 0x40000) sector = FLASH_SECTOR_5; // 128KB else if (offset < 0x60000) sector = FLASH_SECTOR_6; // 128KB else sector = FLASH_SECTOR_7; // 128KB return sector; } uint8_t ReceivePacket(uint8_t* data, uint16_t* len, uint32_t timeout) { uint8_t length_byte; // 读取长度字节 if (HAL_UART_Receive(&huart1, &length_byte, 1, timeout) != HAL_OK) return 0; *len = length_byte; if (*len == 0 || *len > 250) return 0; // 读取数据 if (HAL_UART_Receive(&huart1, data, *len, timeout) != HAL_OK) return 0; return 1; } uint32_t ReadWordFromBuffer(uint8_t* buf) { return (uint32_t)buf[0] << 24 | (uint32_t)buf[1] << 16 | (uint32_t)buf[2] << 8 | buf[3]; } void JumpToApplication(uint32_t app_addr) { typedef void (*pFunc)(void); pFunc appEntry; /* 关闭全局中断 */ __set_PRIMASK(1); /* 关闭滴答定时器,复位到默认值 */ SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; /* 设置所有时钟到默认状态 */ HAL_RCC_DeInit(); /* 关闭所有中断,清除所有中断挂起标志 */ for (uint8_t i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } /* 使能全局中断 */ __set_PRIMASK(0); /* 设置为特级模式,使用MSP指针 */ __set_CONTROL(0); __set_MSP(*(__IO uint32_t*)app_addr); SCB->VTOR = app_addr; // 获取并跳转到 App Reset Handler appEntry = (pFunc)(*(__IO uint32_t*)(app_addr + 4)); appEntry(); }2.app代码
main.c
int main(void) { /* USER CODE BEGIN 1 */ SCB->VTOR = 0x08004000; /* USER CODE END 1 */ /* MCU * * Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the * Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ HAL_RCC_DeInit(); /* 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 */ HAL_UART_Receive_IT(&huart1, &rx_byte, 1); printf("Enter APP! Hello World!\r\n"); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ // uint8_t rx; HAL_Delay(4000); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(4000); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); if (CheckUpdateCommandWithTimeout()) { printf("GetUpdateCommand!\r\n"); JumpToBootloader(); } } /* USER CODE END 3 */ }updataapp.c
#include "UpdataAPP.h" static uint32_t last_cmd_time = 0; #define CMD_TIMEOUT_MS 1000 // 1秒超时 #include "main.h" #include "stdint.h" #define BOOTLOADER_MAGIC_ADDR 0x20018000 #define BOOTLOADER_MAGIC_VALUE 0xDEADBEEF UpdateState update_state = UPDATE_STATE_WAIT_55; uint8_t CheckUpdateCommandWithTimeout(void) { uint8_t data; uint16_t read_len; // 检查超时 if (HAL_GetTick() - last_cmd_time > CMD_TIMEOUT_MS) { update_state = UPDATE_STATE_WAIT_55; // 超时重置状态机 } while ((read_len = USART1_getRxData(&data, 1)) > 0) { last_cmd_time = HAL_GetTick(); // 更新最后接收时间 switch (update_state) { case UPDATE_STATE_WAIT_55: if (data == 0x55) update_state = UPDATE_STATE_WAIT_AA; break; case UPDATE_STATE_WAIT_AA: if (data == 0xAA) update_state = UPDATE_STATE_WAIT_55_2; else update_state = UPDATE_STATE_WAIT_55; break; case UPDATE_STATE_WAIT_55_2: if (data == 0x55) update_state = UPDATE_STATE_WAIT_AA_2; else update_state = UPDATE_STATE_WAIT_55; break; case UPDATE_STATE_WAIT_AA_2: if (data == 0xAA) { update_state = UPDATE_STATE_WAIT_55; return 1; } else { update_state = UPDATE_STATE_WAIT_55; } break; } } return 0; } // 跳转到Bootloader void JumpToBootloader(void) { printf("Preparing to jump to bootloader...\n"); // 1. 设置魔法值 *(__IO uint32_t*)BOOTLOADER_MAGIC_ADDR = BOOTLOADER_MAGIC_VALUE; // 2. 清理外设 HAL_RCC_DeInit(); HAL_DeInit(); // 3. 禁用所有中断 __disable_irq(); // 4. 清除所有中断挂起位 for (int i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } // 5. 系统复位 NVIC_SystemReset(); }updataapp.h
#ifndef __UPDATAAPP_H__ #define __UPDATAAPP_H__ #include "main.h" #include "usart.h" #include "ringbuffer.h" typedef enum { UPDATE_STATE_WAIT_55, UPDATE_STATE_WAIT_AA, UPDATE_STATE_WAIT_55_2, UPDATE_STATE_WAIT_AA_2 } UpdateState; uint8_t CheckUpdateCommandWithTimeout(void); void JumpToBootloader(void); #endifringbuffer.c(正点原子的)
#include "ringbuffer.h" /** * @brief fifo初始化 * @param fifo: 实例 * @param buffer: fifo的缓冲区 * @param size: 缓冲区大小 * @retval None */ void ringbuffer_init(ringbuffer_t* fifo, uint8_t* buffer, uint16_t size) { fifo->buffer = buffer; fifo->size = size; fifo->in = 0; fifo->out = 0; } /** * @brief 获取已经使用的空间 * @param fifo: 实例 * @retval uint16_t: 已使用个数 */ uint16_t ringbuffer_getUsedSize(ringbuffer_t* fifo) { if (fifo->in >= fifo->out) return (fifo->in - fifo->out); else return (fifo->size - fifo->out + fifo->in); } /** * @brief 获取未使用空间 * @param fifo: 实例 * @retval uint16_t: 剩余个数 */ uint16_t ringbuffer_getRemainSize(ringbuffer_t* fifo) { return (fifo->size - ringbuffer_getUsedSize(fifo) - 1); } /** * @brief FIFO是否为空 * @param fifo: 实例 * @retval uint8_t: 1 为空 0 不为空(有数据) */ uint8_t ringbuffer_isEmpty(ringbuffer_t* fifo) { return (fifo->in == fifo->out); } /** * @brief 发送数据到环形缓冲区(不检测剩余空间) * @param fifo: 实例 * @param data: &#& * @param len: &#& * @retval none */ void ringbuffer_in(ringbuffer_t* fifo, uint8_t* data, uint16_t len) { for (int i = 0; i < len; i++) { fifo->buffer[fifo->in] = data[i]; fifo->in = (fifo->in + 1) % fifo->size; } } /** * @brief 发送数据到环形缓冲区(带剩余空间检测,空间不足发送失败) * @param fifo: 实例 * @param data: &#& * @param len: &#& * @retval uint8_t: 0 成功 1失败(空间不足) */ uint8_t ringbuffer_in_check(ringbuffer_t* fifo, uint8_t* data, uint16_t len) { uint16_t remainsize = ringbuffer_getRemainSize(fifo); if (remainsize < len) // 空间不足 return 1; ringbuffer_in(fifo, data, len); return 0; } /** * @brief 从环形缓冲区读取数据 * @param fifo: 实例 * @param buf: 存放数组 * @param len: 存放数组长度 * @retval uint16_t: 实际读取个数 */ uint16_t ringbuffer_out(ringbuffer_t* fifo, uint8_t* buf, uint16_t len) { uint16_t remainToread = ringbuffer_getUsedSize(fifo); if (remainToread > len) { remainToread = len; } for (int i = 0; i < remainToread; i++) { buf[i] = fifo->buffer[fifo->out]; fifo->out = (fifo->out + 1) % fifo->size; } return remainToread; } /*******************************END OF FILE************************************/ringbuffer.h
#ifndef _RINGBUFFER_H_ #define _RINGBUFFER_H_ #include "main.h" /*环形缓冲区数据结构*/ typedef struct { uint8_t *buffer; uint16_t size; uint16_t in; uint16_t out; } ringbuffer_t; void ringbuffer_init(ringbuffer_t *fifo, uint8_t *buffer, uint16_t size); uint16_t ringbuffer_getUsedSize(ringbuffer_t *fifo); uint16_t ringbuffer_getRemainSize(ringbuffer_t *fifo); uint8_t ringbuffer_isEmpty(ringbuffer_t *fifo); void ringbuffer_in(ringbuffer_t *fifo, uint8_t *data, uint16_t len); uint8_t ringbuffer_in_check(ringbuffer_t *fifo, uint8_t *data, uint16_t len); uint16_t ringbuffer_out(ringbuffer_t *fifo, uint8_t *buf, uint16_t len); #endif /* _RINGBUFFER_H_ */ /*******************************END OF FILE************************************/usart.c新增环形缓冲区
// 串口环形缓冲区定义 ringbuffer_t uart_rx_buffer; uint8_t uart_rx_data[512]; /* USER CODE BEGIN 1 */ void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ // 检查是否是接收中断 if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) { uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF); // 将数据存入环形缓冲区 ringbuffer_in_check(&uart_rx_buffer, &data, 1); // 清除中断标志(HAL库会自动处理) } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ } uint16_t USART1_getRxData(uint8_t* buf, uint16_t len) { return ringbuffer_out(&uart_rx_buffer, buf, len); }3.python烧录程序代码
import sys import serial import struct import time import os # === 配置 === APP_BIN = r"E:\Project\ProjectSTM32\STM32project\BT\APPTEST\build\Debug\APPTEST.bin" SERIAL_PORT = "COM5" BAUDRATE = 115200 CHUNK_SIZE = 128 # 每个数据包的数据长度 def send_packet(ser, data): """发送数据包: [长度][数据]""" if not data: return False packet = bytes([len(data)]) + data ser.write(packet) ser.flush() return True def wait_ack(ser, timeout=3): """等待ACK响应""" start_time = time.time() while time.time() - start_time < timeout: if ser.in_waiting > 0: response = ser.read(1) if response == b'\x79': # ACK return True elif response == b'\x1F': # NACK print(" Received NACK") return False time.sleep(0.01) return False def main(): print(f"Opening serial port {SERIAL_PORT} at {BAUDRATE} baud...") try: ser = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=2) except Exception as e: print(f"Failed to open serial port: {e}") return time.sleep(2) # 等待设备启动 # 发送进入bootloader命令 print("Sending bootloader entry command...") jump_cmd = struct.pack(">I", 0x55AA55AA) if send_packet(ser, jump_cmd): #单片机进入reset,我们等待进入bootloader信号 if wait_ack(ser,timeout=10): print("Jump command acknowledged, device resetting...") else: print("No ACK for jump command") return # 发送进入确认bootloader命令,双重保险 print("Sending bootloader entry command...") ser.write(b'\x7F') # 简单触发命令 ser.flush() # 等待ACK if not wait_ack(ser): print("No ACK received, device may not be in bootloader mode") ser.close() return print("Device entered bootloader mode") # 等待擦除完成确认(设备会先擦除整个区域) print("Waiting for erase completion...") if not wait_ack(ser, timeout=30): # 擦除可能需要较长时间 print("Erase timeout or failed") ser.close() return print("Application area erased, starting firmware upload...") if not os.path.exists(APP_BIN): print(f"Error: Firmware file not found: {APP_BIN}") ser.close() return with open(APP_BIN, "rb") as f: firmware = f.read() print(f"Firmware size: {len(firmware)} bytes") addr = 0x08004000 sent = 0 total = len(firmware) # 发送固件数据 while sent < total: chunk = firmware[sent:sent + CHUNK_SIZE] addr_bytes = struct.pack(">I", addr) payload = addr_bytes + chunk print(f"Writing to 0x{addr:08X} ({len(chunk)} bytes)...") if send_packet(ser, payload): if wait_ack(ser, 3): print(" ACK received") addr += len(chunk) sent += len(chunk) else: print(" No ACK received, retrying...") # 重试当前数据包 time.sleep(0.1) else: print(" Failed to send packet") break # 进度显示 progress = (sent / total) * 100 print(f"Progress: {progress:.1f}%") # 发送跳转命令 if sent == total: print("Firmware upload completed, sending jump command...") jump_cmd = struct.pack(">I", 0xAA55AA55) if send_packet(ser, jump_cmd): if wait_ack(ser): print("Jump command acknowledged, device resetting...") else: print("No ACK for jump command") ser.close() print("Done.") if __name__ == "__main__": main()六、现有缺陷
其实这个代码还是有很多问题,大家还可以再改一下
1.没有在bootloader留更新后路,只能从app中跳转到更新程序,如果app中没有留更新程序的相关代码,bootloader就废了
2.烧录速度有点慢
3.整块区域擦除,而不是通过app保留接口的方式进行部分程序的烧录(灵活性不够)