1. 认识STM32F103C8T6的Flash存储特性

STM32F103C8T6这款芯片虽然价格亲民,但资源确实有限——20KB的RAM和64KB的Flash。很多初学者第一次看到这个配置都会有点懵,这么小的空间能做什么?其实只要规划得当,它能做的事情远超你的想象。

Flash存储和RAM最大的区别在于:RAM是易失性存储器,断电后数据就没了;而Flash是非易失性的,断电后数据还能保持。但Flash的读写操作比RAM复杂得多,需要特殊的操作流程。STM32F103C8T6的Flash被划分为64页,每页1KB,从0x08000000开始地址映射。

我在实际项目中经常遇到这样的情况:需要存储设备配置参数、运行日志或者校准数据。如果直接用变量存储,复位后数据就丢失了。这时候Flash存储就派上用场了。但Flash写入前必须先擦除,而且擦除是以页为单位的,这个特性让很多新手踩坑。

Flash的写入速度也比RAM慢很多,实测下来,写入一个32位数据大约需要40us,而擦除一页(1KB)则需要约40ms。这个速度差异在设计存储方案时必须要考虑进去。

2. 环境搭建与基础配置

2.1 开发工具选择

我习惯使用STM32CubeMX + Keil MDK的组合,这个搭配对初学者特别友好。CubeMX能帮你生成基础配置代码,省去很多底层寄存器配置的麻烦。安装完这两个工具后,记得还要安装STM32F1系列的HAL库,这是操作Flash的关键。

如果你喜欢开源工具,也可以选择VSCode + PlatformIO的方案,不过对于初学者,我还是推荐先用官方工具链,等熟悉了再尝试其他方案。

2.2 工程基础配置

打开CubeMX,选择STM32F103C8T6芯片后,首先配置时钟树。这款芯片最高能跑到72MHz,但Flash读写操作时要注意,当系统时钟超过24MHz时,需要增加Flash的等待周期,否则会出现读写错误。

在RCC配置中,我建议选择外部晶振作为时钟源,这样时钟更稳定。SYS配置里把Debug设为Serial Wire,这样后面才能用ST-Link进行调试。GPIO配置根据你的实际需求来,如果只是做Flash读写实验,可以暂时不配置。

时钟配置完成后,在Project Manager里设置工程名称和路径,Toolchain选择MDK-ARM。生成代码时,建议选择"Copy only the necessary library files",这样可以减少工程体积。

3. Flash读写操作实战

3.1 Flash操作的基本原理

STM32的Flash操作有一套严格的流程,不是直接往地址写数据那么简单。每次写入前,目标扇区必须是已擦除状态(全为0xFF)。擦除操作是以页为单位的,而写入则是以半字(16位)或字(32位)为单位。

HAL库提供了完整的Flash操作函数,但在使用前需要先解锁Flash。STM32的Flash默认是锁定的,这是为了防止误操作导致程序被擦除。解锁需要按特定顺序向KEYR寄存器写入两个密钥值。

// Flash解锁示例
HAL_FLASH_Unlock();

// Flash擦除一页
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t PageError = 0;

EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = 0x0800C000; // 要擦除的页地址
EraseInitStruct.NbPages = 1; // 擦除的页数

HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);

// Flash锁定
HAL_FLASH_Lock();

3.2 实现高效的数据写入策略

由于Flash擦除比较耗时,我在实际项目中总结出一个经验:尽量减少擦除次数。比如要存储经常变化的数据,可以采用"双页轮换"的策略——用两页Flash交替存储,每次只擦除即将写入的那一页。

写入数据时要注意对齐问题。STM32的Flash写入要求地址必须是2的倍数(半字写入)或4的倍数(字写入)。不对齐的写入会导致硬件错误,整个系统都会挂掉。

// 数据写入示例
#define FLASH_TARGET_ADDRESS 0x0800C000

uint32_t data[2048]; // 8KB数据,2048个32位字

// 写入数据
HAL_FLASH_Unlock();
for(int i = 0; i < 2048; i++) {
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, 
                     FLASH_TARGET_ADDRESS + i*4, 
                     data[i]);
}
HAL_FLASH_Lock();

实测下来,写入8KB数据大约需要320ms,其中大部分时间花在了实际写入操作上。这个速度对于大多数应用场景是可以接受的,但如果需要频繁写入大量数据,就需要考虑优化策略了。

4. 数据校验机制实现

4.1 为什么需要数据校验

Flash存储并不总是可靠的。我在项目中就遇到过这样的情况:设备突然断电时正在写入Flash,导致部分数据损坏。或者由于Flash寿命问题(STM32F103的Flash典型擦写寿命是1万次),某些位可能出现错误。

常见的数据校验方法有校验和、CRC校验等。校验和计算简单,但检错能力有限;CRC校验更可靠,但计算量稍大。对于STM32F103这种资源有限的芯片,需要权衡校验强度和计算开销。

4.2 CRC校验的实现

STM32F103内置了CRC计算单元,可以硬件计算CRC32,大大减轻CPU负担。使用前需要先使能CRC时钟,然后就可以直接使用HAL_CRC_Calculate函数计算CRC值。

// CRC校验示例
uint32_t calculate_crc(uint32_t *data, uint32_t length) {
    CRC_HandleTypeDef hcrc;
    hcrc.Instance = CRC;
    HAL_CRC_Init(&hcrc);
    
    uint32_t crc = HAL_CRC_Calculate(&hcrc, data, length);
    HAL_CRC_DeInit(&hcrc);
    return crc;
}

// 写入数据时同时存储CRC
uint32_t data_with_crc[2049]; // 数据+CRC
// ... 填充数据 ...
uint32_t crc_value = calculate_crc(data, 2048);
data_with_crc[2048] = crc_value;

// 读取时验证CRC
uint32_t read_crc = calculate_crc(read_data, 2048);
if(read_crc != stored_crc) {
    // 数据校验失败,需要处理错误
}

我在实际测试中发现,使用硬件CRC比软件计算快10倍以上,这对于性能有限的系统来说是个很大的优势。

5. 性能优化与稳定性提升

5.1 读写速度优化技巧

Flash读写速度的瓶颈主要在擦除操作上。我通过实测发现,擦除一页1KB的Flash需要约40ms,而写入一个32位数据只需要40us。也就是说,擦除时间占了总操作时间的99%以上。

优化策略很明确:尽量减少擦除次数。我常用的方法是尽可能批量写入数据,减少擦除频率。比如要存储多组配置参数,可以先把它们收集起来,一次性写入,而不是每次修改都擦写一次。

另一个优化点是利用STM32的Flash写入缓冲。STM32支持连续写入,只要保持写入地址递增,就可以减少等待时间。实测采用连续写入模式比单次写入能提升约20%的速度。

5.2 提升数据存储可靠性

除了数据校验,还要考虑Flash的寿命问题。STM32F103的Flash典型擦写次数是1万次,如果每天擦写10次,大约只能使用3年。对于需要频繁更新的数据,需要采用均衡磨损策略。

我常用的方法是使用多个页轮换存储,通过一个索引表来记录当前使用的页。每次写入时选择擦写次数最少的页,这样可以显著延长Flash的使用寿命。

// 简易的均衡磨损实现
#define FLASH_PAGE_COUNT 8
uint32_t page_usage[FLASH_PAGE_COUNT] = {0};

uint32_t find_least_used_page() {
    uint32_t min_usage = 0xFFFFFFFF;
    uint32_t selected_page = 0;
    
    for(int i = 0; i < FLASH_PAGE_COUNT; i++) {
        if(page_usage[i] < min_usage) {
            min_usage = page_usage[i];
            selected_page = i;
        }
    }
    
    page_usage[selected_page]++;
    return FLASH_BASE + (selected_page + 12) * FLASH_PAGE_SIZE;
}

6. 实战调试与问题排查

6.1 常见问题及解决方法

我在调试Flash读写时遇到过各种奇怪的问题,这里分享几个典型的案例。有一次程序总是卡死在写入操作,后来发现是忘记解锁Flash。还有一次写入的数据总是错位,原因是地址没有按字对齐。

最头疼的是偶尔出现的写入失败,后来发现是因为在写入过程中发生了中断。Flash操作期间必须禁止中断,否则可能导致操作失败甚至数据损坏。

// 安全的Flash写入流程
__disable_irq(); // 禁止中断
HAL_FLASH_Unlock();
// 执行擦除和写入操作
HAL_FLASH_Lock();
__enable_irq(); // 重新使能中断

6.2 调试技巧与工具使用

Keil的Memory Window是个很有用的调试工具,可以实时查看Flash内容。在View -> Memory Windows -> Memory 1中输入Flash地址(如0x0800C000),就可以看到该地址开始的数据。

Watch Window也很好用,可以监控写入缓冲区和读取缓冲区的数据。通过对比这两个缓冲区的数据,可以快速发现读写错误。

如果遇到HardFault等严重错误,可以查看Call Stack + Locals窗口,这里会显示错误发生时的函数调用栈和局部变量值,对排查问题很有帮助。

7. 实际应用案例分享

7.1 参数存储系统

在我最近做的一个物联网项目中,需要存储设备配置参数和运行日志。由于参数可能随时修改,而日志需要持续记录,Flash存储成了唯一的选择。

我设计了一个简单的文件系统:将Flash划分为参数区和日志区。参数区采用双页备份,每次修改时先写入新页再擦除旧页,确保任何时候都有一份完整可用的参数。日志区采用循环队列方式,写满后从头开始覆盖旧的日志。

这个系统已经稳定运行了半年多,经历了多次意外断电考验,没有出现数据损坏的情况。

7.2 数据采集系统

另一个项目需要采集传感器数据并存储。由于数据量较大,我采用了压缩后再存储的策略。首先对采集到的数据进行差分压缩,然后用CRC校验确保数据完整性,最后分批写入Flash。

为了优化写入速度,我预先分配了多个写入缓冲区。当一个缓冲区写满时,启动后台写入Flash,同时继续向其他缓冲区采集数据。这样避免了写入操作阻塞数据采集。

这个方案成功将64KB的Flash存储空间有效利用率提高了3倍,而且读写速度完全满足实时采集的要求。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐