一、什么是、为什么要写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)
简单写就是这样:
cpp
int main()
{
系统初始化();
if (收到升级信号)
{
进入升级模式();
跳转到主程序();
}
else
{
跳转到主程序();
}
}
三、如何跳转程序
我们可能会认为"跳转到主程序();"只需要把app下main函数的函数地址找出来执行就好了,但其实没有那么简单。假设我们已经写好了程序,那么程序在Flash中的分布大概会是这样子的,两个代码:bootloader代码起始位置0x08000000,app程序的起始位置就是0x08004000

图中上面的三个数据(中断向量表、栈顶、复位向量)是代码刚刚启动进入bootloader程序设置的值,那么现在摆在我们面前的有两种情况:
1.接收不到更新信号,需要跳转程序到app
2.接收到更新信号,要进入升级程序,再跳转程序到app
为了方便验证和学习,我们先从跳转程序说起,首先我们得写一个仅仅有跳转功能的bootloader程序(仅需要初始化时钟,和一个输出的GPIO口就好),外加上一个LED闪烁的APP程序
下面是bootloader跳转代码的撰写:
cpp
/* 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闪烁,没什么特别的
cpp
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的位置和下一个数据,第一个就是栈顶,在正常范围内,第二个就是复位向量。如果异常的话,就要我们自己检查一下是哪里出问题了
四、串口烧录程序流程
接下来就是我们的重头戏了,我们将引入串口更新的操作,下面是具体的流程图,大家可以根据自己的需求简化一下,我在app的串口处使用了环形缓存区,会有一点点复杂,可以改为直接接收就好

看上图的流程会有一点复杂,大家可以自行修改,只要能识别到更新信号就可以(也可以是按钮之类的,方便操作)
下面是更新的具体通讯方式:

关于如何导出烧录文件,我们在根目录下的cmakelist中添加这一段
python
# --- 生成 .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))
cpp
/* 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
cpp
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
cpp
#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
cpp
#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);
#endif
ringbuffer.c(正点原子的)
cpp
#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
cpp
#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新增环形缓冲区
cpp
// 串口环形缓冲区定义
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烧录程序代码
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保留接口的方式进行部分程序的烧录(灵活性不够)