返回列表 发新帖我要提问本帖赏金: 30.00元(功能说明)

[单片机芯片] IAP升级-单片机端

[复制链接]
1972|2
 楼主| kissdb 发表于 2025-1-8 10:26 | 显示全部楼层 |阅读模式
本帖最后由 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.更改宏定义

  1. #ifndef CBC
  2. #define CBC 1
  3. #endif

  4. #ifndef ECB
  5. #define ECB 0
  6. #endif

  7. #ifndef CTR
  8. #define CTR 0
  9. #endif

  10. #define AES256 1



2.设置密码
  1. /* AES256 encryption algorithm option */
  2. #define RT_FOTA_ALGO_AES_IV "0123456789123456"
  3. #define RT_FOTA_ALGO_AES_KEY "01234567891234560123456789012345"
3.初始化AES
  1. struct AES_ctx ctx;
  2.     uint8_t KEY[32];
  3.     uint8_t IV[16];
  4.     memcpy(KEY, RT_FOTA_ALGO_AES_KEY, 32);
  5.     memcpy(IV, RT_FOTA_ALGO_AES_IV, 16);
  6.     AES_init_ctx_iv(&ctx, KEY, IV);
4.使用AES解密函数原型是
  1. /**
  2. * 使用AES_CBC模式解密缓冲区。
  3. *
  4. * @param ctx 密钥上下文,包含解密所需的轮密钥和初始化向量。
  5. * @param buf 待解密的数据缓冲区。
  6. * @param length 要解密的数据长度,必须是AES块长度的倍数。
  7. *
  8. * 注意:这个函数假设输入数据长度是AES块长度的倍数,如果不是,可能会导致未被解密的数据被错误地处理。
  9. */
  10. 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);

三、接收解析文件头

定义一个共同体

  1. union   RBL{
  2.     struct
  3.     {
  4.         char type[4];              /* RBL 字符头 */
  5.         uint16_t fota_algo;        /* 算法配置: 表示是否加密或者使用了压缩算法 */
  6.         uint8_t fm_time[6];        /* 原始 bin 文件的时间戳, 6 位时间戳, 使用了 4 字节, 包含年月日信息 */
  7.         char app_part_name[16];    /* app 执行分区名 ,实际是作为固件型号使用*/
  8.         char download_version[24]; /* 固件代码版本号 */
  9.         char current_version[24];  /* 这个域在 rbl 文件生成时都是一样的,我用于表示 app
  10.                                       分区当前运行固件的版本号,判断是否固件需要升级 */
  11.         uint32_t code_crc;         /* 代码的 CRC32 校验值, 它是的打包后的校验值, 即 rbl 文件 96 字节后的数据 */
  12.         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,直到写入的字节等于头文件中的文件大小,
  1. body_crc = rt_fota_step_crc(body_crc, Recve_buffer, len); // 计算CRC
  2.         // 快速解锁闪存以准备写入
  3.         FLASH_Unlock_Fast();
  4.         // 计算写入地址
  5.         uint32_t address = (uint32_t)BACKUP_IMAGE_START_ADD + (file_count * FLASH_PAGE_SIZE);
  6.         if (len >= 256)
  7.         {
  8.             // 写入一页数据
  9.             FLASH_ProgramPage_Fast(address, Recve_buffer); // 写入一页数据
  10.             // 写入完成后锁定闪存
  11.             FLASH_Lock_Fast(); // 快速编程模式上锁
  12.             file_count++;      // 文件写入计数
  13.             filesize += len;
  14.             UART_IAP_SendData(0X03); // 发送确认
  15.         }
  16.         else
  17.         {
  18.             // 写入剩余的数据
  19.             // 循环写入剩余的数据
  20.             union flash_data {
  21.                 uint8_t buf[256]; /* data */
  22.                 uint32_t page[64];
  23.             } flash;
  24.             for (uint32_t i = 0; i < len; i++)
  25.             {
  26.                 flash.buf[i] = Recve_buffer[i];
  27.             }
  28.             FLASH_ProgramPage_Fast(address, flash.page); // 写入数据
  29.             // 写入完成后锁定闪存
  30.             FLASH_Lock_Fast(); // 快速编程模式上锁
  31.             filesize += len;
  32.             if ((filesize) != OTA_part.fota_part_head.com_size)
  33.             {
  34.                 printf("文件大小不匹配\r\n");
  35.                 Flag_UART_IAP = 0;
  36.                 UART_IAP_SendData(0X05); // 发送错误确认
  37.                 return;
  38.             }
  39.         }
判断CRC,固件版本,文件Hash等校验,检验无误后,重启写入APP区
  1. // 从用户闪存区读取96字节数据到程序文件结构体,再次判断
  2.                 IAP_EEPROM_READ(FILE_FLAG_ADD, &OTA_part.data[0], 96);
  3. printf("文件头 = %s\r\n", OTA_part.data);
  4. printf("版本号 = %s\r\n", OTA_part.fota_part_head.download_version);
  5. printf("名称 = %s\r\n", OTA_part.fota_part_head.app_part_name);
  6. printf("代码大小 = %d\r\n", OTA_part.fota_part_head.raw_size);
  7. 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区,首先读取文件头里的大小,根据大小开始循环读取写入,
  1. // 循环复制数据,这样可以确保即使 com_size 不能被 READ_DATA_LEN 整除,循环次数也会多一次,从而覆盖所有数据。
  2.     for (i = 0; i < ((OTA_part.fota_part_head.com_size + READ_DATA_LEN - 1) / READ_DATA_LEN);
  3.          i++) // i < (BACKUP_IMAGE_MAX_SIZE / READ_DATA_LEN)
  4.     {
  5.         // 计算当前循环对应的闪存地址
  6.         flashAddr = USER_IMAGE_START_ADD + i * READ_DATA_LEN;
  7.         // 从用户闪存区读取数据到缓冲区
  8.         IAP_EEPROM_READ((flashAddr + USER_IMAGE_MAX_SIZE), (u8 *)pBuf, READ_DATA_LEN);

  9.         AES_CBC_decrypt_buffer(&ctx, (u8 *)pBuf, READ_DATA_LEN);

  10.         // 将缓冲区的数据写入备份闪存区
  11.         IAP_EEPROM_WRITE(flashAddr, (u8 *)pBuf, READ_DATA_LEN);

  12.         // 检查写入的数据是否正确
  13.         for (j = 0; j < (READ_DATA_LEN / 4); j++)
  14.         {
  15.             // 如果数据不匹配,返回操作失败状态
  16.             if (pBuf[j] != *(u32 *)(flashAddr + 4 * j))
  17.                 return NoREADY;
  18.         }
  19.     }

写入完成后可以再次校验CRC32和HASH,判断是否一致,
  1. // 初始化flash地址
  2.     flashAddr = USER_IMAGE_START_ADD;
  3.     hash = FNV1A_32_INIT;
  4.     // 循环计算数据,直到达到代码大小
  5.     for (uint32_t i = 0; i < raw_size; i++)
  6.     {
  7.         hash ^= *(u8 *)(flashAddr + i);
  8.         hash *= FNV_PRIME_32;
  9.     }
  10.     printf("hash_val = %x\r\n", hash);
  11.     if (hash != OTA_part.fota_part_head.hash_val)
  12.     {
  13.         printf("hash is wrong!\r\n");
  14.         return NoREADY;
  15.     }
校验一致后就可以清除升级标志并跳转到APP运行了。
  1. printf("update success!\r\n");
  2.             printf("Run APP1!\r\n");
  3.             EEprom_Write_Byte(UPDATA_FLAG_STORAGE_ADD, 0); // 写入完成
  4.             Delay_Ms(100);
  5.             NVIC_EnableIRQ(Software_IRQn);
  6.             NVIC_SetPendingIRQ(Software_IRQn);





AES.zip

25.19 KB, 下载次数: 9

AES解密

打赏榜单

21小跑堂 打赏了 30.00 元 2025-01-16
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

书接上回,如何使用IAP在单片机上实现程序更新,完成上位机到下位机的整体开发。  发表于 2025-1-16 17:25
您需要登录后才可以回帖 登录 | 注册

本版积分规则

13

主题

295

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部