ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32简单的串口Bootloader入门

2026/8/19 20:01:54 拓冰建站 浏览量
STM32简单的串口Bootloader入门 一、什么是、为什么要写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代码起始位置0x08000000app程序的起始位置就是0x08004000图中上面的三个数据中断向量表、栈顶、复位向量是代码刚刚启动进入bootloader程序设置的值那么现在摆在我们面前的有两种情况1.接收不到更新信号需要跳转程序到app2.接收到更新信号要进入升级程序再跳转程序到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。重设中断向量表偏移VTORAPP 的起始地址通常是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, sector_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.cint 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 rE:\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, timeout3): 等待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(fOpening serial port {SERIAL_PORT} at {BAUDRATE} baud...) try: ser serial.Serial(SERIAL_PORT, BAUDRATE, timeout2) except Exception as e: print(fFailed 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,timeout10): 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, timeout30): # 擦除可能需要较长时间 print(Erase timeout or failed) ser.close() return print(Application area erased, starting firmware upload...) if not os.path.exists(APP_BIN): print(fError: Firmware file not found: {APP_BIN}) ser.close() return with open(APP_BIN, rb) as f: firmware f.read() print(fFirmware 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(fWriting 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(fProgress: {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保留接口的方式进行部分程序的烧录灵活性不够