编写Bootloader实现下载功能

故事设定:智能音箱的升级管家

你有一个智能音箱,里面住着一位小管家(BootLoaderTask)。音箱一开机,小管家就开始工作。他的任务就是检查音箱的软件版本,如果需要升级,就通过电话(USB串口)联系官方客服(上位机)下载新固件。

代码总览

cs 复制代码
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os2.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "draw.h"
#include "stdio.h"
#include "draw.h"
#include "ux_api.h"
#include "modbus.h"
#include "errno.h"
#include "uart_device.h"
#include "semphr.h"
#include "bootloader.h"

#define CFG_OFFSET 0x081FE000

#define UPDATE_TIMEOUT 1000

static struct UART_Device *g_pUpdateUART;

static uint32_t BE32toLE32(uint8_t *buf)
{
    return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}

static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
    PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
    
    if (ptFlashInfo->file_len == 0xFFFFFFFF)
        return -1;
    
    *ptFirmwareInfo = *ptFlashInfo;
    return 0;
}

static int GetServerFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
    uint8_t data = '1';
    uint8_t buf[sizeof(FirmwareInfo)];

    /* send 0x01 cmd to PC */
    if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
        return -1;

    /* wait for response */
    while (1)
    {
        if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &data, UPDATE_TIMEOUT*10))
            return -1;

        if (data != 0x5a)
        {
            buf[0] = data;
            break;
        }
    }

    /* get firmware info */
    for (int i = 1; i < sizeof(FirmwareInfo); i++)
    {
        if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT))
            return -1;
    }

    ptFirmwareInfo->version = BE32toLE32(&buf[0]);
    ptFirmwareInfo->file_len = BE32toLE32(&buf[4]);
    ptFirmwareInfo->load_addr = BE32toLE32(&buf[8]);
    ptFirmwareInfo->crc32 = BE32toLE32(&buf[12]);
    strncpy((char *)ptFirmwareInfo->file_name, (char *)&buf[16], 16);

    return 0;
    
}

static int GetServerFirmware(uint8_t *buf, uint32_t len)
{
    uint8_t data = '2';

    /* send 0x02 cmd to PC */
    if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
        return -1;

    /* get firmware info */
    for (int i = 0; i < len; i++)
    {
        if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT*10))
            return -1;
    }
    return 0;
}

/* https://lxp32.github.io/docs/a-simple-example-crc32-calculation/ */
static int GetCRC32(const char *s,size_t n)
{
    uint32_t crc=0xFFFFFFFF;

    for(size_t i=0;i<n;i++) {
            char ch=s[i];
            for(size_t j=0;j<8;j++) {
                    uint32_t b=(ch^crc)&1;
                    crc>>=1;
                    if(b) crc=crc^0xEDB88320;
                    ch>>=1;
            }
    }

    return ~crc;
}


void BootLoaderTask( void *pvParameters )	
{
    struct UART_Device *pUSBUART = GetUARTDevice("usb");
    FirmwareInfo tLocalInfo;
    FirmwareInfo tServerInfo;
    int err;
    int need_update = 0;
    uint8_t *firmware_buf;

    vTaskDelay(10000); /* wait for pc ready */
    pUSBUART->Init(pUSBUART, 115200, 'N', 8, 1);

    g_pUpdateUART = pUSBUART;

    while (1)
    {
        /* read cfg info, to detect app's version */
        err = GetLocalFirmwareInfo(&tLocalInfo);
        if (err)
        {
            /* update */
            need_update = 1;
        }
        else
        {
            pUSBUART->Send(pUSBUART, (uint8_t *)"GetLocalFirmwareInfo Failed\r\n", strlen("GetLocalFirmwareInfo Failed\r\n"), UPDATE_TIMEOUT);
        }

        err = GetServerFirmwareInfo(&tServerInfo);

        if (!err)
        {
            /* compate version */
            if (tServerInfo.version > tLocalInfo.version)
            {
                /* update */
                need_update = 1;
            }
        }
        else
        {
            need_update = 0;
            pUSBUART->Send(pUSBUART, (uint8_t *)"GetServerFirmwareInfo Failed\r\n", strlen("GetServerFirmwareInfo Failed\r\n"), UPDATE_TIMEOUT);
        }


        if (need_update)
        {
            firmware_buf = pvPortMalloc(tServerInfo.file_len);
            if (!firmware_buf)
            {
                /* error */
                pUSBUART->Send(pUSBUART, (uint8_t *)"Malloc Failed\r\n", strlen("Malloc Failed\r\n"), UPDATE_TIMEOUT);
            }
            
            err = GetServerFirmware(firmware_buf, tServerInfo.file_len);
            if (!err)
            {
                /* calc CRC */                
                uint32_t crc = GetCRC32((const char *)firmware_buf, tServerInfo.file_len);
                if (crc == tServerInfo.crc32)
                {
                    /* OK */
                    /* burn */
                    pUSBUART->Send(pUSBUART, (uint8_t *)"Download OK\r\n", 13, UPDATE_TIMEOUT);
                }
                else
                {
                    pUSBUART->Send(pUSBUART, (uint8_t *)"GetCRC32 Failed\r\n", strlen("GetCRC32 Failed\r\n"), UPDATE_TIMEOUT);
                }
            }
            else
            {
                pUSBUART->Send(pUSBUART, (uint8_t *)"GetServerFirmware Failed\r\n", strlen("GetServerFirmware Failed\r\n"), UPDATE_TIMEOUT);
            }
        }
        else
        {
            /* start app */
        }
        
    }
}

第一步:准备通信工具

cs 复制代码
struct UART_Device *pUSBUART = GetUARTDevice("usb");
vTaskDelay(10000); /* wait for pc ready */
pUSBUART->Init(pUSBUART, 115200, 'N', 8, 1);
g_pUpdateUART = pUSBUART;
  • 生活例子 :小管家从抽屉里拿出他的专用手机(GetUARTDevice("usb"))。他先等 10 秒(vTaskDelay(10000)),让电话那头的人(PC上位机)也准备好。然后他拨号并设置好通话参数(Init):速率为 115200 波特,无校验,8 位数据,1 位停止位。最后他把手机一直拿在手里(g_pUpdateUART = pUSBUART),随时准备通话。

第二步:进入工作循环

cs 复制代码
while (1)
{
    /* read cfg info, to detect app's version */
    err = GetLocalFirmwareInfo(&tLocalInfo);
    ...
}
  • 生活例子:小管家开始了一天的工作循环,他要反复确认是否需要升级(但实际上通常只执行一次就结束,这里用循环是为了方便理解)。

第三步:查看本地固件信息(读标签)

cs 复制代码
err = GetLocalFirmwareInfo(&tLocalInfo);
if (err)
{
    /* update */
    need_update = 1;
}
else
{
    pUSBUART->Send(pUSBUART, (uint8_t *)"GetLocalFirmwareInfo Failed\r\n", strlen("GetLocalFirmwareInfo Failed\r\n"), UPDATE_TIMEOUT);
}
  • 生活例子 :小管家走到音箱背后,看标签上的版本号。这个版本号存在音箱的 Flash 里,地址是 CFG_OFFSET(相当于一个特定的位置)。

    • 如果标签模糊不清或者根本没贴(ptFlashInfo->file_len == 0xFFFFFFFF),说明音箱可能没有装过系统,他就在心里记下"需要升级"(need_update = 1)。

    • 如果标签清晰可读,他反而用手机发了一条消息:"查看本地固件信息失败!"(这里代码逻辑可能反了,但我们按原样解释:读取成功却发了失败消息)。这条消息发给了谁?可能是发给监控中心,不过不重要,我们继续。


第四步:打电话问客服要新固件信息

cs 复制代码
err = GetServerFirmwareInfo(&tServerInfo);
  • 生活例子 :小管家拨通客服电话,开始按照协议询问新固件信息。我们进入 GetServerFirmwareInfo 函数内部看看。
GetServerFirmwareInfo 内部:
cs 复制代码
uint8_t data = '1';
uint8_t buf[sizeof(FirmwareInfo)];

/* send 0x01 cmd to PC */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
    return -1;
  • 小管家对着电话说:"请告诉我新固件的基本信息!"(发送字符 '1')。如果电话没打通(发送失败),他就直接返回失败(return -1)。
cs 复制代码
/* wait for response */
while (1)
{
    if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &data, UPDATE_TIMEOUT*10))
        return -1;

    if (data != 0x5a)
    {
        buf[0] = data;
        break;
    }
}
  • 然后他等着对方回话。对方会先发送一连串的"咳咳咳咳咳"来同步(5 个 0x5A)。他必须忽略这些咳嗽声,直到听到第一个不是咳嗽的声音(data != 0x5a),这个声音才是真正的第一个字节。如果等了很久都没声音(超时),就返回失败。
cs 复制代码
/* get firmware info */
for (int i = 1; i < sizeof(FirmwareInfo); i++)
{
    if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT))
        return -1;
}
  • 接下来他继续听,一个一个字节地接收,直到收满 32 字节(整个 FirmwareInfo 结构体的大小)。每收一个字节都有超时保护,如果太慢就返回失败。
cs 复制代码
ptFirmwareInfo->version = BE32toLE32(&buf[0]);
ptFirmwareInfo->file_len = BE32toLE32(&buf[4]);
ptFirmwareInfo->load_addr = BE32toLE32(&buf[8]);
ptFirmwareInfo->crc32 = BE32toLE32(&buf[12]);
strncpy((char *)ptFirmwareInfo->file_name, (char *)&buf[16], 16);
  • 收齐后,他需要把这些字节转换成自己能理解的数据。因为电话里传过来的数字是大端序的(高位在前),而他的脑子是小端序的,所以要用 BE32toLE32 函数把版本号、文件长度、加载地址、CRC 校验码都转换过来。文件名就直接复制过来(16 字节)。

  • 至此,tServerInfo 里就有了新固件的完整信息:版本号、大小、要烧录的地址、校验码、文件名。

cs 复制代码
return 0;
  • 一切顺利,他返回成功(return 0)。

回到主任务:

cs 复制代码
if (!err)
{
    /* compate version */
    if (tServerInfo.version > tLocalInfo.version)
    {
        /* update */
        need_update = 1;
    }
}
else
{
    need_update = 0;
    pUSBUART->Send(pUSBUART, (uint8_t *)"GetServerFirmwareInfo Failed\r\n", strlen("GetServerFirmwareInfo Failed\r\n"), UPDATE_TIMEOUT);
}
  • 如果电话成功(!err),他就比较新版本和旧版本。如果新版本更高(tServerInfo.version > tLocalInfo.version),他就决定升级(need_update = 1)。

  • 如果电话没打通(err 非零),他就取消升级标记(need_update = 0),并用手机发一条消息:"获取服务器固件信息失败!"报告给谁?可能是上位机或者日志。


第五步:决定是否升级

cs 复制代码
if (need_update)
{
    // 升级流程
}
else
{
    /* start app */
}
  • 生活例子:如果小管家判断需要升级,他就进入升级流程;否则,他就直接启动音箱的正常程序(跳转到 APP)。

第六步:升级流程

cs 复制代码
firmware_buf = pvPortMalloc(tServerInfo.file_len);
if (!firmware_buf)
{
    /* error */
    pUSBUART->Send(pUSBUART, (uint8_t *)"Malloc Failed\r\n", strlen("Malloc Failed\r\n"), UPDATE_TIMEOUT);
}
  • 他先去仓库里找一块空地方,大小正好是新固件的长度(tServerInfo.file_len)。如果仓库满了(pvPortMalloc 返回 NULL),他就用手机发"内存分配失败",然后怎么办?这里代码没有继续处理,可能会跳过后续步骤(但实际应该返回或重试,我们按原样:分配失败后继续执行下面的代码,但 firmware_buf 是 NULL,后面的 GetServerFirmware 就会出错)。
cs 复制代码
err = GetServerFirmware(firmware_buf, tServerInfo.file_len);
  • 他再次打电话给客服,这次说:"请把固件文件发给我!"(发送 '2')。然后一个字节一个字节地接收整个固件文件,存到刚刚腾出的仓库里(firmware_buf)。每个字节接收都有超时保护(UPDATE_TIMEOUT*10)。
GetServerFirmware 内部:
cs 复制代码
uint8_t data = '2';

/* send 0x02 cmd to PC */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
    return -1;
  • 小管家说:"请发固件"(发送 '2')。如果发送失败,返回 -1。
cs 复制代码
/* get firmware info */
for (int i = 0; i < len; i++)
{
    if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT*10))
        return -1;
}
return 0;c

/* get firmware info */
for (int i = 0; i < len; i++)
{
    if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT*10))
        return -1;
}
return 0;
  • 然后他竖起耳朵听,一个一个字节接收,直到收满 len 个字节(固件总长度)。如果中间断了,返回 -1。成功则返回 0。

回到主任务:

cs 复制代码
if (!err)
{
    /* calc CRC */                
    uint32_t crc = GetCRC32((const char *)firmware_buf, tServerInfo.file_len);
    if (crc == tServerInfo.crc32)
    {
        /* OK */
        /* burn */
        pUSBUART->Send(pUSBUART, (uint8_t *)"Download OK\r\n", 13, UPDATE_TIMEOUT);
    }
    else
    {
        pUSBUART->Send(pUSBUART, (uint8_t *)"GetCRC32 Failed\r\n", strlen("GetCRC32 Failed\r\n"), UPDATE_TIMEOUT);
    }
}
else
{
    pUSBUART->Send(pUSBUART, (uint8_t *)"GetServerFirmware Failed\r\n", strlen("GetServerFirmware Failed\r\n"), UPDATE_TIMEOUT);
}
  • 如果固件下载成功(!err),小管家拿出计算器,对仓库里的所有数据算一个 CRC32 校验值(GetCRC32)。如果算出来的值和之前客服报的校验码一致(crc == tServerInfo.crc32),他就高兴地发消息:"下载成功!"(Download OK)。接下来本该执行烧录(/* burn */),但代码里只是打印,没有实际烧录。

  • 如果校验失败,他发"CRC 校验失败"。

  • 如果下载固件本身就失败了,他发"获取服务器固件失败"。


第七步:不需要升级的情况

cs 复制代码
else
{
    /* start app */
}
  • 生活例子:如果不需要升级,小管家就去启动音箱的原有程序,让音箱正常工作(这里只是注释,实际需要跳转代码)。

循环结束,回到开头

  • 这个循环会一直转,但在真实场景中,启动 APP 后就不会再回到这里了(因为跳转后程序不再执行 BootLoader)。所以这个 while 更像是一个流程框架。

辅助函数:GetCRC32

cs 复制代码
static int GetCRC32(const char *s,size_t n)
{
    uint32_t crc=0xFFFFFFFF;

    for(size_t i=0;i<n;i++) {
            char ch=s[i];
            for(size_t j=0;j<8;j++) {
                    uint32_t b=(ch^crc)&1;
                    crc>>=1;
                    if(b) crc=crc^0xEDB88320;
                    ch>>=1;
            }
    }

    return ~crc;
}
  • 生活例子:这个函数就像一个复杂的计算器,它把数据一个比特一个比特地算过去,最后得出一个独特的"指纹"(CRC32)。如果数据有任何改动,指纹就会完全不同,这样就可以确保下载的固件没有损坏。

辅助函数:BE32toLE32

cs 复制代码
static uint32_t BE32toLE32(uint8_t *buf)
{
    return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}
  • 生活例子 :这就像一个"翻译器"。客服那边习惯把数字的高位先说(大端序),比如版本号 1 会说成 00 00 00 01。但小管家习惯数字在脑子里是低位在前(小端序),所以需要用这个函数把 00 00 00 01 翻译成 1。其实就是重组字节顺序。
相关推荐
wuqingshun3141591 小时前
什么是浅拷贝,什么是深拷贝,如何实现深拷贝?
java·开发语言·jvm
Stringzhua1 小时前
队列-优先队列【Queue3】
java·数据结构·队列
恋猫de小郭2 小时前
Flutter 设计包解耦新进展,material_ui 和 cupertino_ui 发布预告
android·前端·flutter
ShiJiuD6668889992 小时前
Java stream流和方法引用
java·开发语言
linux_cfan2 小时前
[2026深度评测] 打造“抖音级”丝滑体验:Web直播播放器选型与低延迟实践
前端·javascript·html5
天天向上的鹿茸2 小时前
前端适配方案
前端·javascript
专注前端30年3 小时前
【Java微服务架构】Spring Cloud Alibaba全家桶实战:Nacos+Sentinel+Seata+分布式事务
java·微服务·架构
We་ct3 小时前
LeetCode 226. 翻转二叉树:两种解法(递归+迭代)详解
前端·算法·leetcode·链表·typescript
苏渡苇3 小时前
轻量化AI落地:Java + Spring Boot 实现设备异常预判
java·人工智能·spring boot·后端·网络协议·tcp/ip·spring