本帖最后由 kissdb 于 2025-1-8 10:25 编辑
#申请原创# @21小跑堂
一、整体规划
上篇文章把发送的上位机实现了,现在来实现一下单片机端的接收,解密,写入程序。
当设备需要增加功能或者修复BUG时,就要对程序进行更新,有多种烧录方式,使用专用烧录器,拆开机器进行重新烧录,或者使用常用的串口,485,网口等进行IAP升级。重新烧录需要把完整固件发给客户,这样容易造成泄露固件,甚至被修改,被仿制抄板等。采用IAP升级的方式来解决这个问题,保证固件的安全。
使用IAP升级需要单片机的程序分为IAP程序和APP程序,先运行IAP程序,然后在IAP程序中跳转到APP程序运行。在出厂时烧写IAP程序和APP程序,需要升级时只需发送APP程序,APP程序可以加密,烧写到单片机后使用IAP程序来解密,防止被复制抄板等。加密时在APP文件中添加水印信息,防止文件被修改,保证程序的安全。
1.设备分区
单片机一般分为3个区域,IAP区,APP区,备份区。
单片机上电运行IAP程序,IAP程序中首先判断备份区有没有程序,如有则开始把程序解密后写入APP区,如没有就再根据标志判断是更新还是直接进入APP,如需要更新,则进入更新程序,更新完成后,清除标志,重启,再次从IAP中跳转到APP运行。
2.升级流程
首先接收特定字符进入开始接收升级,等待接收96字节的文件头,判断固件是否是本机需要的,判断数据是否完整,并读取文件大小,然后开始接收程序文件。接收完文件后开始使用AES解密,再次判断程序型号是否正确,如正确则重启,把程序解密后写入APP区。
二、AES解密
AES是一种广泛使用的对称密钥加密算法。对称加密算法就是加密和解密用到的密钥是相同的,这种加密方式加密和解密的速度都非常快,占用的空间也很小,非常适合在单片机中使用。本次使用的是开源的tiny-AES-c代码,密钥使用安全性很高的AES256密钥,保护一般的单片机程序也是足够了。
1.更改宏定义
#ifndef CBC
#define CBC 1
#endif
#ifndef ECB
#define ECB 0
#endif
#ifndef CTR
#define CTR 0
#endif
#define AES256 1
2.设置密码/* AES256 encryption algorithm option */
#define RT_FOTA_ALGO_AES_IV "0123456789123456"
#define RT_FOTA_ALGO_AES_KEY "01234567891234560123456789012345"
3.初始化AES
struct AES_ctx ctx;
uint8_t KEY[32];
uint8_t IV[16];
memcpy(KEY, RT_FOTA_ALGO_AES_KEY, 32);
memcpy(IV, RT_FOTA_ALGO_AES_IV, 16);
AES_init_ctx_iv(&ctx, KEY, IV);
4.使用AES解密函数原型是/**
* 使用AES_CBC模式解密缓冲区。
*
* @param ctx 密钥上下文,包含解密所需的轮密钥和初始化向量。
* @param buf 待解密的数据缓冲区。
* @param length 要解密的数据长度,必须是AES块长度的倍数。
*
* 注意:这个函数假设输入数据长度是AES块长度的倍数,如果不是,可能会导致未被解密的数据被错误地处理。
*/
void AES_CBC_decrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length);
直接调用就可以了,AES_CBC_decrypt_buffer(&ctx, (u8 *)pBuf, 512);
每调用一次后,解密的IV都会变,不能从任意位置解密,必须从头开始,解密才能正确。
如需再次解密需要重新初始化 AES_init_ctx_iv(&ctx, KEY, IV);
三、接收解析文件头
定义一个共同体
union RBL{
struct
{
char type[4]; /* RBL 字符头 */
uint16_t fota_algo; /* 算法配置: 表示是否加密或者使用了压缩算法 */
uint8_t fm_time[6]; /* 原始 bin 文件的时间戳, 6 位时间戳, 使用了 4 字节, 包含年月日信息 */
char app_part_name[16]; /* app 执行分区名 ,实际是作为固件型号使用*/
char download_version[24]; /* 固件代码版本号 */
char current_version[24]; /* 这个域在 rbl 文件生成时都是一样的,我用于表示 app
分区当前运行固件的版本号,判断是否固件需要升级 */
uint32_t code_crc; /* 代码的 CRC32 校验值, 它是的打包后的校验值, 即 rbl 文件 96 字节后的数据 */
uint32_t hash_val; /* hash校验值 */
原始的RBL文件头的char app_part_name是作为烧写的分区用的,我是用来当作固件型号来使用的。
初始化一个共同体RBL OTA_part,接收使用的是串口空闲中断或者接收超时模式,判断接收的是不是96字节,如果是的,赋值给共同体的数组,memcpy(&OTA_part.data[0], Recve_buffer, 96);获取到文件的大小,CRC等数据信息,先判断固件型号是否和本机一致,如一致则开始接收程序。
四、接收文件并写入备份区
当文件头判断都正确后就开始接收加密后的APP了,由于上位机是按照256字节发送的,直接按页写入就可以了,并累加写入的大小,并计算crc,直到写入的字节等于头文件中的文件大小,body_crc = rt_fota_step_crc(body_crc, Recve_buffer, len); // 计算CRC
// 快速解锁闪存以准备写入
FLASH_Unlock_Fast();
// 计算写入地址
uint32_t address = (uint32_t)BACKUP_IMAGE_START_ADD + (file_count * FLASH_PAGE_SIZE);
if (len >= 256)
{
// 写入一页数据
FLASH_ProgramPage_Fast(address, Recve_buffer); // 写入一页数据
// 写入完成后锁定闪存
FLASH_Lock_Fast(); // 快速编程模式上锁
file_count++; // 文件写入计数
filesize += len;
UART_IAP_SendData(0X03); // 发送确认
}
else
{
// 写入剩余的数据
// 循环写入剩余的数据
union flash_data {
uint8_t buf[256]; /* data */
uint32_t page[64];
} flash;
for (uint32_t i = 0; i < len; i++)
{
flash.buf[i] = Recve_buffer[i];
}
FLASH_ProgramPage_Fast(address, flash.page); // 写入数据
// 写入完成后锁定闪存
FLASH_Lock_Fast(); // 快速编程模式上锁
filesize += len;
if ((filesize) != OTA_part.fota_part_head.com_size)
{
printf("文件大小不匹配\r\n");
Flag_UART_IAP = 0;
UART_IAP_SendData(0X05); // 发送错误确认
return;
}
}
判断CRC,固件版本,文件Hash等校验,检验无误后,重启写入APP区// 从用户闪存区读取96字节数据到程序文件结构体,再次判断
IAP_EEPROM_READ(FILE_FLAG_ADD, &OTA_part.data[0], 96);
printf("文件头 = %s\r\n", OTA_part.data);
printf("版本号 = %s\r\n", OTA_part.fota_part_head.download_version);
printf("名称 = %s\r\n", OTA_part.fota_part_head.app_part_name);
printf("代码大小 = %d\r\n", OTA_part.fota_part_head.raw_size);
printf("hash_val = %x\r\n", OTA_part.fota_part_head.hash_val);
if (strcmp(OTA_part.fota_part_head.app_part_name, Model_name) == 0)
{
// 设置升级标志
u32 updateFlag = IMAGE_FLAG_UPDATE;
// 擦除存储升级标志的EEPROM区域
IAP_EEPROM_ERASE(UPDATA_FLAG_STORAGE_ADD, FLASH_PAGE_SIZE);
// 写入升级标志到EEPROM
IAP_EEPROM_WRITE(UPDATA_FLAG_STORAGE_ADD, (u8 *)&updateFlag, 4);
// 复位系统
NVIC_SystemReset();
}
五、把备份区文件写入APP区
重启后首先判断升级标志,如果有升级标志,就开始把备份区的数据写入到APP区,首先读取文件头里的大小,根据大小开始循环读取写入,
// 循环复制数据,这样可以确保即使 com_size 不能被 READ_DATA_LEN 整除,循环次数也会多一次,从而覆盖所有数据。
for (i = 0; i < ((OTA_part.fota_part_head.com_size + READ_DATA_LEN - 1) / READ_DATA_LEN);
i++) // i < (BACKUP_IMAGE_MAX_SIZE / READ_DATA_LEN)
{
// 计算当前循环对应的闪存地址
flashAddr = USER_IMAGE_START_ADD + i * READ_DATA_LEN;
// 从用户闪存区读取数据到缓冲区
IAP_EEPROM_READ((flashAddr + USER_IMAGE_MAX_SIZE), (u8 *)pBuf, READ_DATA_LEN);
AES_CBC_decrypt_buffer(&ctx, (u8 *)pBuf, READ_DATA_LEN);
// 将缓冲区的数据写入备份闪存区
IAP_EEPROM_WRITE(flashAddr, (u8 *)pBuf, READ_DATA_LEN);
// 检查写入的数据是否正确
for (j = 0; j < (READ_DATA_LEN / 4); j++)
{
// 如果数据不匹配,返回操作失败状态
if (pBuf[j] != *(u32 *)(flashAddr + 4 * j))
return NoREADY;
}
}
写入完成后可以再次校验CRC32和HASH,判断是否一致,
// 初始化flash地址
flashAddr = USER_IMAGE_START_ADD;
hash = FNV1A_32_INIT;
// 循环计算数据,直到达到代码大小
for (uint32_t i = 0; i < raw_size; i++)
{
hash ^= *(u8 *)(flashAddr + i);
hash *= FNV_PRIME_32;
}
printf("hash_val = %x\r\n", hash);
if (hash != OTA_part.fota_part_head.hash_val)
{
printf("hash is wrong!\r\n");
return NoREADY;
}
校验一致后就可以清除升级标志并跳转到APP运行了。
printf("update success!\r\n");
printf("Run APP1!\r\n");
EEprom_Write_Byte(UPDATA_FLAG_STORAGE_ADD, 0); // 写入完成
Delay_Ms(100);
NVIC_EnableIRQ(Software_IRQn);
NVIC_SetPendingIRQ(Software_IRQn);
|
书接上回,如何使用IAP在单片机上实现程序更新,完成上位机到下位机的整体开发。