在本文中,我想写一下我为使用固件加密的STM32创建引导加载程序的经验。 我是个人开发人员,因此以下代码可能不符合任何公司标准
在此过程中,设置了以下任务:
- 通过SD卡为设备用户提供固件更新。
- 确保控制固件的完整性,并排除控制器内存中记录错误的固件。
- 提供固件加密以防止设备克隆。
该代码使用stdperiph,fatFS和tinyAES库在Keil uVision中编写。 实验性的微控制器是STM32F103VET6,但是该代码可以轻松地适用于另一个STM控制器。 完整性控制由CRC32算法提供,校验和位于固件文件的最后4个字节中。
本文没有描述项目的创建,连接库,初始化外围设备和其他琐碎的步骤。
首先,您需要确定什么是引导加载程序。 STM32体系结构意味着当闪存,RAM,外设寄存器以及其他所有部件位于同一地址空间时,可以对存储器进行平面寻址。 引导加载程序是在微控制器启动时启动的程序,检查是否有必要更新固件(如果有必要),执行固件并启动主设备程序。 本文将介绍SD卡的更新机制,但您可以使用任何其他来源。
固件的加密由AES128算法执行,并使用tinyAES库实现。 它仅包含两个文件,一个文件的扩展名为.c,另一个文件的扩展名为.h,因此其连接应该没有问题。
创建项目后,您应该确定加载程序和主程序的大小。 为方便起见,应以微控制器存储器页面大小的倍数选择大小。 在此示例中,引导加载程序将占用64 Kb,而主程序将占用其余的448 Kb。 引导加载程序将位于闪存的开头,主程序位于引导加载程序之后。 这应该在Keil的项目设置中指定。 引导加载程序的起始地址为0x80000000(正是STM32在启动后从他那里开始执行代码),其大小为0x10000,我们在设置中对此进行了说明。

为了方便起见,主程序将以0x08010000开始,以0x08080000结尾,我们将定义所有地址:
#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000
我们还将加密密钥和AES初始化向量添加到程序中。 这些密钥最好随机生成。
static const uint8_t AES_FW_KEY[] = {0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF}; static const uint8_t AES_IV[] = {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA};
在此示例中,整个固件更新过程被构造为状态机。 这样,更新过程就可以在屏幕上显示某些内容,重置看门狗并执行其他任何操作。 为了方便起见,我们将定义自动机的基本状态,以免混淆数字:
#define FW_START 5 #define FW_READ 1000 #define FW_WRITE 2000 #define FW_FINISH 10000 #define FW_ERROR 100000
初始化外围设备后,您需要检查是否需要固件更新。 在第一种状态下,尝试读取SD卡并检查其上是否存在文件。
uint32_t t; uint32_t fw_step; uint32_t fw_buf[512]; uint32_t aes_buf[512]; uint32_t idx; char tbuf[64]; FATFS FS; FIL F; case FW_READ: { if(f_mount(&FS, "" , 0) == FR_OK) { if(f_open(&F, "FIRMWARE.BIN", FA_READ | FA_OPEN_EXISTING) == FR_OK) { f_lseek(&F, 0); CRC_ResetDR(); lcd_putstr(" ", 1, 0); idx = MAIN_PROGRAM_START_ADDRESS; fw_step = FW_READ + 10; } else {fw_step = FW_FINISH;} } else {fw_step = FW_FINISH;} break; }
现在我们需要检查固件的正确性。 这里,首先是校验和验证代码,在文件读取完成时执行,然后是读取本身。 也许您不应该这样写,在评论中写下您的想法。 为了方便使用闪存,读取以2 KB进行,因为 STM32F103VET6具有2 Kb的存储器页面大小。
case FW_READ + 10: { sprintf(tbuf, ": %d", idx - MAIN_PROGRAM_START_ADDRESS); lcd_putstr(tbuf, 2, 1); if (idx > MAIN_PROGRAM_END_ADDRESS) { f_read(&F, &t, sizeof(t), &idx); CRC_CalcCRC(t); if(CRC_GetCRC() == 0) { idx = MAIN_PROGRAM_START_ADDRESS; f_lseek(&F, 0); fw_step = FW_READ + 20; break; } else { lcd_putstr(" ", 3, 2); fw_step = FW_ERROR; break; } } f_read(&F, &fw_buf, sizeof(fw_buf), &t); if(t != sizeof(fw_buf)) { lcd_putstr(" ", 3, 2); fw_step = FW_ERROR; break; } AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); for(t=0;t<NELEMS(aes_buf);t++) { CRC_CalcCRC(aes_buf[t]); } idx+=sizeof(fw_buf); break; }
现在,如果固件没有损坏,则需要再次读取它,但这一次将其写入闪存。
case FW_READ + 20:
现在,为了美观,我们将创建错误处理和成功更新的状态:
case FW_ERROR: { break; } case FW_FINISH: { ExecMainFW(); break; }
启动主程序ExecMainFW()的功能值得更详细地考虑。 这是:
void ExecMainFW() { uint32_t jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); pFunction Jump_To_Application = (pFunction) jumpAddress; RCC->APB1RSTR = 0xFFFFFFFF; RCC->APB1RSTR = 0x0; RCC->APB2RSTR = 0xFFFFFFFF; RCC->APB2RSTR = 0x0; RCC->APB1ENR = 0x0; RCC->APB2ENR = 0x0; RCC->AHBENR = 0x0; RCC_DeInit(); __disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS); Jump_To_Application(); }
启动启动文件后,它立即重新初始化了所有内容,因此主程序应再次将指针指向其地址空间内的中断向量:
__disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); __enable_irq();
在主程序的项目中,需要指定正确的地址:

实际上,这里是整个更新过程。 检查固件的正确性并对其进行加密,所有任务均已完成。 如果在更新过程中断电,则设备当然会积木,但是引导加载程序将保持不变,并且可以重复更新过程。 在特别紧急的情况下,您可以通过Option字节锁定加载程序所在的页面以进行写入。
但是,对于SD卡,您可以在引导加载程序中为自己安排一个很好的便利。 新固件版本的测试和调试完成后,可以出于某些特殊条件(例如,内部的按钮或跳线),强制设备本身对完成的固件进行加密并将其上传到SD卡。 在这种情况下,仅需从设备中取出SD卡,将其插入计算机中,然后将固件放在Internet上即可使用户满意。 我们将以有限状态机的另外两个状态的形式来执行此操作:
case FW_WRITE: { if(f_mount(&FS, "" , 0) == FR_OK) { if(f_open(&F, "FIRMWARE.BIN", FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) { CRC_ResetDR(); idx = MAIN_PROGRAM_START_ADDRESS; fw_step = FW_WRITE + 10; } else {fw_step = FW_ERROR;} } else {fw_step = FW_ERROR;} break; } case FW_WRITE + 10: { if (idx > MAIN_PROGRAM_END_ADDRESS) { t = CRC_GetCRC(); f_write(&F, &t, sizeof(t), &idx); f_close(&F); fw_step = FW_FINISH; } memcpy(&fw_buf, (uint32_t *)idx, sizeof(fw_buf)); for(t=0;t<NELEMS(fw_buf);t++) { CRC_CalcCRC(fw_buf[t]); } AES_CBC_encrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); f_write(&F, &aes_buf, sizeof(aes_buf), &t); idx+=sizeof(fw_buf); break; }
实际上,这就是我想说的。 在本文结尾,我希望您在创建了这样的引导加载程序后不要忘记在Option字节中包含防止读取微控制器内存的保护措施。
参考文献
tinyAES脂肪