当前位置: 首页 > news >正文

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代码起始位置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  // 16KBvoid 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 HandlerappEntry = (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的位置和下一个数据,第一个就是栈顶,在正常范围内,第二个就是复位向量。如果异常的话,就要我们自己检查一下是哪里出问题了

四、串口烧录程序流程

        接下来就是我们的重头戏了,我们将引入串口更新的操作,下面是具体的流程图,大家可以根据自己的需求简化一下,我在app的串口处使用了环形缓存区,会有一点点复杂,可以改为直接接收就好

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

        下面是更新的具体通讯方式:

关于如何导出烧录文件,我们在根目录下的cmakelist中添加这一段

# --- 生成 .bin 文件(修复版)---
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILDCOMMAND ${CMAKE_OBJCOPY}ARGS -O binary $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.binCOMMENT "正在生成 ${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 0xDEADBEEFvoid 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 ACKHAL_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;  // 16KBelse if (offset < 0x8000)sector = FLASH_SECTOR_1;  // 16KBAPP从这里开始else if (offset < 0xC000)sector = FLASH_SECTOR_2;  // 16KBelse if (offset < 0x10000)sector = FLASH_SECTOR_3;  // 16KBelse if (offset < 0x20000)sector = FLASH_SECTOR_4;  // 64KBelse if (offset < 0x40000)sector = FLASH_SECTOR_5;  // 128KBelse if (offset < 0x60000)sector = FLASH_SECTOR_6;  // 128KBelsesector = FLASH_SECTOR_7;  // 128KBreturn 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 HandlerappEntry = (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 0xDEADBEEFUpdateState 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;elseupdate_state = UPDATE_STATE_WAIT_55;break;case UPDATE_STATE_WAIT_55_2:if (data == 0x55)update_state = UPDATE_STATE_WAIT_AA_2;elseupdate_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);#endif

ringbuffer.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);elsereturn (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 Falsepacket = bytes([len(data)]) + dataser.write(packet)ser.flush()return Truedef 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':  # ACKreturn Trueelif response == b'\x1F':  # NACKprint("  Received NACK")return Falsetime.sleep(0.01)return Falsedef 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}")returntime.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()# 等待ACKif not wait_ack(ser):print("No ACK received, device may not be in bootloader mode")ser.close()returnprint("Device entered bootloader mode")# 等待擦除完成确认(设备会先擦除整个区域)print("Waiting for erase completion...")if not wait_ack(ser, timeout=30):  # 擦除可能需要较长时间print("Erase timeout or failed")ser.close()returnprint("Application area erased, starting firmware upload...")if not os.path.exists(APP_BIN):print(f"Error: Firmware file not found: {APP_BIN}")ser.close()returnwith open(APP_BIN, "rb") as f:firmware = f.read()print(f"Firmware size: {len(firmware)} bytes")addr = 0x08004000sent = 0total = len(firmware)# 发送固件数据while sent < total:chunk = firmware[sent:sent + CHUNK_SIZE]addr_bytes = struct.pack(">I", addr)payload = addr_bytes + chunkprint(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) * 100print(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保留接口的方式进行部分程序的烧录(灵活性不够)

http://www.dtcms.com/a/442133.html

相关文章:

  • 360网站怎么做2核4g做网站
  • 从 “手工作坊” 到 “智能工厂”:2025 年 AI 原生应用重构内容创作产业
  • 做网站平台难在哪里网页翻译不见了
  • Flutter技术栈深度解析:从架构设计到性能优化
  • 学做湘菜的视频网站中国建设企业银行登录网站
  • 【Python进阶】网络爬虫核心技能-第三方IP服务
  • CAS密钥管理系统在汽车行业的核心密钥管理实践——构建智能网联汽车的可信安全底座
  • 宝塔面板登录地址和账密都忘了怎么解决
  • 廊坊大城网站建设义乌创源网站建设
  • Spring-AI 接入(本地大模型 deepseek + 阿里云百炼 + 硅基流动)
  • 华为OD机试C卷 - 分苹果 - 二进制 - (Java C++ JavaScript Python)
  • 国内好的seo网站网站建设课程的感受
  • 用 Gradle 配置 Flink 从开发到打包的一条龙实践
  • gRPC从0到1系列【17】
  • 浅谈内存DDR——DDR4性能优化技术
  • 静态网页模板网站电商运营培训班
  • mysqldump导入备份数据到阿里云RDS会报错吗
  • QT肝8天16--加载动态菜单
  • Spring Boot整合缓存——Redis缓存!超详细!
  • 湘潭做网站品牌磐石网络wordpress 柚子皮
  • 前端实战开发(二):React + Canvas 网络拓扑图开发:6 大核心问题与完整解决方案
  • 【C语言数据结构】第2章:线性表(2)--线性表的顺序存储结构
  • 计算机操作系统--进程:共享内存和管道的差异
  • 深圳移动网站建设公司上海建筑工程有限公司
  • 【Linux】入门指南:基础指令详解Part One
  • 使用 Docker 部署 Nginx 教程
  • 重庆做网站微信的公司上海平面网站
  • 整站优化seo公司哪家好千峰网课
  • C语言指针应用的经典案例
  • C++篇(11)继承