1. 项目概述
在嵌入式系统开发中,Bootloader是一个至关重要的组件,它负责在系统启动时加载和更新应用程序固件。本文将深入探讨如何编写一个具备固件烧录功能的Bootloader,重点解析STM32系列单片机中Flash存储器的编程原理与实现方法。
Flash存储器作为嵌入式系统中存储程序代码的主要介质,具有非易失性、高密度和低成本等优势。但与RAM不同,Flash的写入操作需要遵循特定的"先擦除后编程"原则,这使得固件更新过程比简单的内存拷贝要复杂得多。
2. Flash存储器基础
2.1 Flash存储器的物理特性
STM32单片机的Flash存储器具有以下几个关键特性:
-
按扇区擦除:Flash的最小擦除单位是扇区(Sector),不能像RAM那样直接修改单个字节。这意味着即使只需要修改一个字节,也必须先擦除整个包含该字节的扇区。
-
写入限制:Flash的每个存储单元都有有限的擦写寿命,通常在10万次左右。超过这个次数后,存储单元可能会失效。
-
写入机制:Flash只能将位从1改为0,不能将0改为1。因此,在写入新数据前,必须先将目标区域擦除为全1状态(0xFF)。
-
分Bank管理:大容量STM32芯片通常将Flash分为两个Bank(Bank1和Bank2),每个Bank可以独立操作,在某些情况下甚至可以同时进行读写。
2.2 STM32 Flash地址空间
典型的STM32F4系列单片机Flash地址空间布局如下:
- Bank1: 0x08000000 - 0x080FFFFF (1MB)
- Bank2: 0x08100000 - 0x081FFFFF (1MB)
每个Bank又被划分为多个扇区,扇区大小根据芯片型号不同而有所差异。例如:
- 小容量芯片:扇区大小通常为1KB或2KB
- 大容量芯片:前几个扇区较小(16KB或32KB),后面的扇区较大(128KB)
3. HAL库Flash操作函数
3.1 HAL_FLASH_Unlock()和HAL_FLASH_Lock()
Flash存储器默认处于写保护状态,在进行任何修改操作前必须先解锁:
c复制HAL_StatusTypeDef HAL_FLASH_Unlock(void);
操作完成后,应该重新锁定Flash以防止意外修改:
c复制HAL_StatusTypeDef HAL_FLASH_Lock(void);
3.2 扇区擦除函数HAL_FLASHEx_Erase()
c复制HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError);
该函数需要一个FLASH_EraseInitTypeDef结构体参数,其定义如下:
c复制typedef struct {
uint32_t TypeErase; // 擦除类型:FLASH_TYPEERASE_SECTORS或FLASH_TYPEERASE_MASSERASE
uint32_t Banks; // 指定操作的Bank:FLASH_BANK_1或FLASH_BANK_2
uint32_t Sector; // 起始扇区号
uint32_t NbSectors; // 要擦除的扇区数量
} FLASH_EraseInitTypeDef;
3.3 编程函数HAL_FLASH_Program()
c复制HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t FlashAddress, uint32_t DataAddress);
参数说明:
-
TypeProgram:编程方式,可以是:
- FLASH_TYPEPROGRAM_BYTE:字节写入
- FLASH_TYPEPROGRAM_HALFWORD:半字(2字节)
- FLASH_TYPEPROGRAM_WORD:字(4字节)
- FLASH_TYPEPROGRAM_DOUBLEWORD:双字(8字节)
- FLASH_TYPEPROGRAM_QUADWORD:四字(16字节)
-
FlashAddress:目标Flash地址,必须与编程单位对齐
-
DataAddress:源数据地址(RAM中)
4. 固件烧录实现
4.1 WriteFirmware函数解析
WriteFirmware函数负责将内存中的固件数据写入Flash的指定位置:
c复制static int WriteFirmware(uint8_t *firmware_buf, uint32_t len, uint32_t flash_addr)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t sectors = (len + (SECTOR_SIZE - 1)) / SECTOR_SIZE; // 计算需要擦除的扇区数
uint32_t flash_offset = flash_addr - 0x08000000; // 计算相对于Flash基址的偏移
uint32_t bank_sectors; // 当前Bank剩余的扇区数
uint32_t erased_sectors = 0; // 已擦除的扇区数
// 解锁Flash
HAL_FLASH_Unlock();
// 擦除Bank1部分
if (flash_offset < 0x100000) {
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_1;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
erased_sectors = (sectors <= bank_sectors) ? sectors : bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError)) {
HAL_FLASH_Lock();
return -1;
}
flash_offset += erased_sectors * SECTOR_SIZE;
}
// 准备处理Bank2
sectors -= erased_sectors;
flash_offset -= 0x100000;
// 擦除Bank2部分
if (sectors) {
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
erased_sectors = (sectors <= bank_sectors) ? sectors : bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError)) {
HAL_FLASH_Lock();
return -1;
}
}
// 编程Flash
len = (len + 15) & ~15; // 长度对齐到16字节
for (int i = 0; i < len; i += 16) {
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,
flash_addr,
(uint32_t)firmware_buf)) {
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
firmware_buf += 16;
}
// 锁定Flash
HAL_FLASH_Lock();
return 0;
}
4.2 WriteFirmwareInfo函数解析
WriteFirmwareInfo函数用于写入固件信息结构体到Flash的特定位置:
c复制static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t flash_addr = CFG_OFFSET;
uint8_t *src_buf = (uint8_t *)ptFirmwareInfo;
// 解锁Flash
HAL_FLASH_Unlock();
// 擦除配置扇区
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (flash_addr - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError)) {
HAL_FLASH_Lock();
return -1;
}
// 编程写入
for (int i = 0; i < sizeof(FirmwareInfo); i += 16) {
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,
flash_addr,
(uint32_t)src_buf)) {
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
src_buf += 16;
}
// 锁定Flash
HAL_FLASH_Lock();
return 0;
}
5. 关键技术与注意事项
5.1 地址对齐处理
Flash编程对地址对齐有严格要求:
- 编程地址必须与编程单位对齐(如使用QUADWORD编程时,地址必须是16字节对齐)
- 擦除操作必须从扇区边界开始
在实际应用中,我们通常:
- 确保固件烧录地址是扇区对齐的
- 在编程前检查地址对齐情况
- 必要时进行地址调整或填充
5.2 跨Bank处理策略
当固件较大可能跨越多个Bank时,需要:
- 计算每个Bank可用的扇区数
- 分段进行擦除和编程操作
- 注意Bank边界处的地址转换
5.3 长度对齐与缓冲区管理
代码中的长度对齐操作len = (len + 15) & ~15可能导致读取超出实际数据范围的隐患。更安全的做法是:
- 确保固件长度本身就是对齐的
- 或者单独处理最后一个不足16字节的数据块
- 在缓冲区末尾预留足够的填充空间
5.4 错误处理与恢复
完善的Bootloader应该具备:
- 详细的错误状态报告机制
- 操作失败后的恢复策略
- 断电保护机制(如操作标记)
6. 实际应用建议
6.1 优化烧录速度
- 使用更大的编程单位(如QUADWORD而非BYTE)
- 合理规划扇区布局,减少不必要的擦除操作
- 考虑使用双Bank特性实现后台更新
6.2 增强可靠性
- 添加CRC校验确保数据完整性
- 实现固件回滚机制
- 使用看门狗防止操作过程中死机
6.3 调试技巧
- 通过串口输出详细的操作日志
- 添加调试断点检查关键变量
- 使用Flash内容读取功能验证写入结果
7. 扩展思考
7.1 差分更新实现
对于大容量固件,可以考虑实现差分更新:
- 只更新发生变化的部分
- 使用压缩算法减少传输数据量
- 实现补丁生成和应用机制
7.2 安全考虑
- 添加固件签名验证
- 实现加密传输和存储
- 防止未授权更新
7.3 多固件管理
- 支持多个应用程序映像
- 实现固件选择菜单
- 添加版本管理功能
通过本文的详细解析,相信读者已经掌握了STM32 Bootloader中Flash编程的核心技术。在实际项目中,还需要根据具体需求进行调整和优化,但基本原理和实现方法是相通的。