编写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。其实就是重组字节顺序。
相关推荐
星空1 小时前
金蝶苍穹build.gradle配置
java·build.gradle
筼筜2 小时前
【系统架构设计师】通信系统架构设计理论与实践
网络
泯泷2 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
小程序设计3 小时前
基于IPv6的校园网设计与过渡技术仿真
网络·安全
步行cgn4 小时前
Spring 基于 XML 的自动装配:byName 详解
java·后端·spring
野槐5 小时前
前端项目优化(有了我,会发生...)
前端
大帅点兵5 小时前
Apache Ant 1.9.9 完整讲解
java
Lynne3095 小时前
工业网络,到底该集中还是自治?
网络·架构·数据
LuTshoes5 小时前
spring ai 实战 手搓 PlaneExecuteAgent
java·人工智能·spring·ai
这料鬼有毒5 小时前
二刷hot100-73.矩阵置零
java