当Bootloader准备跳转到应用程序时,它必须确认这个APP是"健康"的,而不是一个损坏的、不完整的或错误的数据。如果不加校验直接跳转,可能导致:
-
"变砖"风险:APP固件不完整或损坏,单片机可能卡死在未知状态,无法再通过正常方式进入Bootloader进行恢复。
-
不可预测的行为:程序跑飞,外设错误操作,甚至引发硬件安全问题。
-
升级失败无感知:通过OTA或串口升级后,虽然烧录过程没有报错,但固件在传输或存储过程中可能已出错,系统却依然尝试启动损坏的APP。
校验的
常见的校验方法
主要有三种,按实现复杂度和安全性递增:
1. 栈指针(SP)校验
这是最基本、必须做的校验。
-
原理:在Cortex-M等ARM芯片中,向量表的第一个字(地址
APP_BASE + 0)存放的是主堆栈指针的初始值。这个值必须指向一个有效的、可读写的RAM地址。 -
校验方法:
#define APP_ADDRESS 0x08010000 // APP起始地址
// 获取APP向量表中的初始SP值
uint32_t app_sp = *(volatile uint32_t *)APP_ADDRESS;// 校验SP值是否在合理的RAM范围内
if ((app_sp < RAM_START) || (app_sp > (RAM_END - 1024))) { // 留一些栈空间余量
// SP无效,不跳转
return ERROR_INVALID_SP;
} else {
// SP看起来有效,可以进行下一步校验或跳转
}
目的就是在跳转前,最后一次确认APP固件的完整性和正确性。
-
优点:简单、快速,能过滤掉大部分完全无效的固件(如全0xFF或全0x00)。
-
缺点:无法检测固件代码本身的错误或数据损坏
-
2. 校验和
这是一种轻量级的完整性校验。
-
原理:对APP固件的整个或部分二进制数据,进行累加计算,得到一个校验和值。将这个值与预先计算好并存储在固定位置的值进行比较。
-
实现步骤:
-
构建阶段:在PC上生成APP二进制文件后,计算其校验和,并 appended到文件的末尾(或写入一个特定的Flash地址,如APP区的最后4个字节)。
-
Bootloader中:读取APP区的数据(不包括存储校验和本身的那部分),同样计算校验和,再与存储的校验和值进行比较。
-
-
代码示例(简单求和校验):
-
uint32_t Calculate_Checksum(uint32_t *start_addr, uint32_t size_bytes) {
uint32_t checksum = 0;
uint32_t num_words = size_bytes / 4;
for (uint32_t i = 0; i < num_words; i++) {
checksum += start_addr[i];
}
return checksum;
}bool Validate_APP_Checksum(void) {
// 假设APP从 0x08010000 开始,大小为 0x30000
// 校验和存储在APP区的最后4个字节
uint32_t app_start = 0x08010000;
uint32_t app_size = 0x30000 - 4; // 减去存储校验和本身的4字节
uint32_t stored_checksum_addr = 0x08010000 + 0x30000 - 4;uint32_t calculated_checksum = Calculate_Checksum((uint32_t*)app_start, app_size);
uint32_t stored_checksum = *(volatile uint32_t*)stored_checksum_addr;return (calculated_checksum == stored_checksum);
} -
优点:实现简单,计算速度快。
-
缺点:检错能力较弱。例如,两个数据字节交换位置,校验和可能不变。
-
CRC循环冗余校验
这是工业界最常用、最推荐的方法。
-
原理:CRC是一种更强的散列函数,能够检测出多位错误、突发性错误等。它比简单的校验和可靠得多。
-
实现步骤:
-
构建阶段:使用CRC算法(如CRC16、CRC32)计算APP固件的CRC值,并将其存储在固件末尾的固定位置。
-
Bootloader中:使用相同的CRC算法和多项式,对APP区数据重新计算CRC,并与存储的CRC值比对。
-
-
代码示例(使用HAL库的CRC外设,如果芯片支持):
c
bool Validate_APP_CRC32(void) { // 启用CRC外设时钟 __HAL_RCC_CRC_CLK_ENABLE(); CRC_HandleTypeDef hcrc; hcrc.Instance = CRC; HAL_CRC_Init(&hcrc); // 计算APP区域的CRC(不包括存储CRC自身的4字节) uint32_t app_start = 0x08010000; uint32_t app_size_words = (0x30000 - 4) / 4; // 转换为32位字的数量 uint32_t stored_crc_addr = 0x08010000 + 0x30000 - 4; // HAL_CRC_Calculate 会重置CRC计算单元并开始新计算 uint32_t calculated_crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)app_start, app_size_words); uint32_t stored_crc = *(volatile uint32_t*)stored_crc_addr; __HAL_RCC_CRC_CLK_DISABLE(); // 用完关闭时钟 return (calculated_crc == stored_crc); }如果芯片没有硬件CRC,可以使用软件算法,网上有大量开源实现(如
crclib)。 -
优点:检错能力极强,可靠性高。
-
缺点:计算量比校验和稍大(但有硬件CRC支持的话速度很快)。
-
完整的Bootloader跳转流程(包含校验)
一个健壮的Bootloader跳转逻辑如下:
c
void Bootloader_JumpToApp(void) { uint32_t app_address = 0x08010000; uint32_t *app_vector_table = (uint32_t *)app_address; // 1. 检查APP区是否已被擦除(全0xFF)。如果是,说明没有APP。 if (*app_vector_table == 0xFFFFFFFF) { printf("No APP found.\r\n"); return; } // 2. 栈指针(SP)校验 uint32_t app_sp = app_vector_table[0]; if ((app_sp < 0x20000000) || (app_sp > 0x20010000)) { // 根据实际RAM修改 printf("Invalid Stack Pointer.\r\n"); return; } // 3. 程序计数器(PC)校验(可选,但推荐) uint32_t app_pc = app_vector_table[1]; if ((app_pc < app_address) || (app_pc > (app_address + 0x30000))) { // 根据APP大小修改 printf("Invalid Program Counter.\r\n"); return; } // 4. CRC校验(或校验和) if (!Validate_APP_CRC32()) { printf("APP CRC Error! Firmware is corrupted.\r\n"); return; } // 5. 所有校验通过,准备跳转 printf("All checks passed. Jumping to APP...\r\n"); // 禁用所有中断 __disable_irq(); // 设置向量表偏移(对于Cortex-M,如果APP设置了VTOR,这里也需要设置) SCB->VTOR = app_address; // 设置主堆栈指针 __set_MSP(app_sp); // 获取复位处理函数地址并跳转 uint32_t app_reset_handler = app_vector_table[1]; void (*app_start)(void) = (void (*)(void))app_reset_handler; // 跳转! app_start(); }

1442

被折叠的 条评论
为什么被折叠?



