3. 使用内部Flash存储固件
任务目标
本案例实现Bootloader的核心存储功能:将接收到的固件数据写入STM32内部Flash的指定应用区域,并完成扇区擦除与编程操作。通过串口接收固定格式的固件数据帧,写入Flash后回读校验,验证存储正确性。
硬件与依赖
- 硬件:STM32F103系列(本文以STM32F103C8T6为例,Flash 64KB,页大小1KB)
- 依赖:STM32标准外设库(或HAL库,本文使用标准库寄存器操作以保持最小依赖)
- 串口:USART1,波特率115200,用于接收固件数据
Flash分区规划
- Bootloader区:0x08000000 ~ 0x08003FFF(16KB,存放本Bootloader代码)
- 应用区起始地址:
APP_START_ADDR = 0x08004000(用户固件写入此区域) - 应用区最大容量:48KB(0x08004000 ~ 0x0800BFFF)
关键完整代码
#include "stm32f10x.h"
#include <string.h>
#define APP_START_ADDR 0x08004000 // 应用区起始地址
#define APP_MAX_SIZE (48 * 1024) // 应用区最大48KB
#define FLASH_PAGE_SIZE 1024 // STM32F103页大小1KB
// 串口接收缓冲区(简单示例:一次接收一帧完整数据)
#define FRAME_MAX_SIZE 2048
uint8_t rx_buffer[FRAME_MAX_SIZE];
uint32_t rx_len = 0;
// 擦除应用区指定地址的页(addr必须页对齐)
void Flash_ErasePage(uint32_t addr) {
FLASH_Unlock();
while (FLASH_ErasePage(addr) != FLASH_COMPLETE);
FLASH_Lock();
}
// 编程:将data写入addr,len为字节数(addr建议半字对齐)
void Flash_ProgramData(uint32_t addr, uint8_t *data, uint32_t len) {
FLASH_Unlock();
for (uint32_t i = 0; i < len; i += 2) {
uint16_t half_word = data[i] | (data[i+1] << 8);
while (FLASH_ProgramHalfWord(addr + i, half_word) != FLASH_COMPLETE);
}
FLASH_Lock();
}
// 从Flash读取数据到缓冲区
void Flash_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
buf[i] = *(volatile uint8_t *)(addr + i);
}
}
// 将接收到的固件写入Flash(核心函数)
// 参数:firmware_data - 固件数据指针;len - 固件长度
// 返回:0成功,1长度超限,2擦除失败,3校验失败
int StoreFirmware(uint8_t *firmware_data, uint32_t len) {
if (len == 0 || len > APP_MAX_SIZE) return 1;
// 1. 擦除应用区所有页(从APP_START_ADDR开始,覆盖len所需页数)
uint32_t pages_needed = (len + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;
for (uint32_t i = 0; i < pages_needed; i++) {
Flash_ErasePage(APP_START_ADDR + i * FLASH_PAGE_SIZE);
}
// 2. 编程写入
Flash_ProgramData(APP_START_ADDR, firmware_data, len);
// 3. 回读校验
uint8_t verify_buf[FRAME_MAX_SIZE];
Flash_ReadData(APP_START_ADDR, verify_buf, len);
if (memcmp(firmware_data, verify_buf, len) != 0) return 3;
return 0;
}
// 串口接收中断(示意:接收一帧数据后调用StoreFirmware)
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE)) {
uint8_t ch = USART_ReceiveData(USART1);
if (rx_len < FRAME_MAX_SIZE) {
rx_buffer[rx_len++] = ch;
}
}
}
// 主函数示例:接收完一帧后调用存储
int main(void) {
// 系统初始化(时钟、GPIO、USART1等,此处省略具体配置)
// USART1_Init(115200);
while (1) {
// 假设通过串口接收完一帧完整固件,rx_len为实际长度
// 实际项目中需定义帧协议(如帧头+长度+数据+校验)
if (rx_len > 0) {
int result = StoreFirmware(rx_buffer, rx_len);
if (result == 0) {
// 存储成功,可跳转到APP_START_ADDR执行(本案例不涉及跳转)
// 发送成功应答
} else {
// 发送错误码
}
rx_len = 0; // 清空接收计数
}
}
}
编译与运行
- 使用Keil MDK或IAR创建工程,芯片选择STM32F103C8。
- 将上述代码加入工程,注意将Bootloader链接地址设置为
0x08000000(默认即可)。 - 编译生成HEX文件,通过ST-Link烧录到开发板。
验证方法
- 功能验证:在
main函数中构造测试数据(如uint8_t test_data[] = {0x01, 0x02, 0x03, 0x04, ...}),直接调用StoreFirmware(test_data, sizeof(test_data)),观察返回值是否为0。 - 数据完整性验证:调用
Flash_ReadData(APP_START_ADDR, read_buf, len),对比read_buf与原始数据是否一致。若一致,说明擦除和编程操作正确。 - 边界测试:尝试写入超过
APP_MAX_SIZE的数据,应返回错误码1。 - 通过条件:
StoreFirmware返回0,且回读数据与写入数据完全一致,即判定Flash存储功能正常。
注意事项
- 擦除操作前必须调用
FLASH_Unlock(),操作完成后调用FLASH_Lock()。 - Flash编程最小单位为半字(16位),因此数据长度必须为偶数;若长度为奇数,需在帧协议中做填充处理。
- 擦除操作会清除整页数据,因此应用区起始地址必须页对齐(1KB边界)。
- 本案例未包含串口帧协议解析和Boot跳转功能,仅聚焦Flash擦写核心操作;实际Bootloader需在此基础上增加帧头/长度/校验字段。