故事设定:智能音箱的升级管家
你有一个智能音箱,里面住着一位小管家(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。其实就是重组字节顺序。