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)

简单写就是这样:

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, &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.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保留接口的方式进行部分程序的烧录(灵活性不够)

相关推荐
东木君_2 小时前
RK3588:MIPI底层驱动学习——入门第四篇(驱动精华:OV13855驱动加载时究竟发生了什么?)
单片机·嵌入式硬件·学习
东方欲晓w2 小时前
STM32 UART篇
stm32·单片机·嵌入式硬件
Gary Studio3 小时前
第三类笔记
笔记·算法
悠哉悠哉愿意3 小时前
【ROS2学习笔记】分布式通信
笔记·学习·ros2
virtual_k1smet3 小时前
#rsa.md
笔记·python
丰锋ff3 小时前
2022 年真题配套词汇单词笔记(考研真相)
笔记
丰锋ff3 小时前
2010 年真题配套词汇单词笔记(考研真相)
笔记·学习·考研
驱动探索者4 小时前
linux 学习平台 arm+x86 搭建
linux·arm开发·学习
A9better4 小时前
嵌入式开发学习日志33——stm32之PWM舵机简单项目
stm32·单片机·嵌入式硬件·学习