基于 RTOS 信号量实现多任务对 SPI Flash 的原子读写保护
一、问题背景与挑战
在嵌入式系统中,SPI Flash 常用于存储配置参数、日志数据或固件升级包。当系统引入 RTOS 后,多个任务(如日志任务、配置管理任务、OTA 任务)可能同时访问 SPI Flash。若不加以保护,会发生以下问题:
- 数据竞争:两个任务同时写同一地址,导致数据错乱。
- 总线冲突:SPI 外设被非原子操作打断,产生无效时序。
- 状态不一致:读操作可能读到半写状态的数据,破坏系统逻辑。
因此,必须确保对 SPI Flash 的每次操作(读、写、擦除)都是原子的,即一个任务在操作期间,其他任务不能介入。
二、RTOS 信号量原理
信号量是 RTOS 中用于任务同步和互斥的机制。常见类型有:
- 二值信号量:只有 0 和 1 两个状态,适合任务同步。
- 计数信号量:计数值可大于 1,用于资源计数。
- 互斥信号量(Mutex):具有优先级继承机制,专门用于互斥访问共享资源。
对于 SPI Flash 保护,推荐使用 互斥信号量,原因如下:
- 防止优先级反转:低优先级任务持有锁时,高优先级任务等待,系统会临时提升低优先级任务优先级,减少等待时间。
- 递归锁支持:同一任务可多次获取,避免死锁。
三、FreeRTOS 互斥信号量 API
FreeRTOS 中互斥信号量相关 API:
-
xSemaphoreCreateMutex():创建互斥信号量,返回句柄。 -
xSemaphoreTake(handle, timeout):获取信号量,可指定阻塞时间。 -
xSemaphoreGive(handle):释放信号量。
四、配置步骤
1. 创建互斥信号量
在系统初始化时创建:
SemaphoreHandle_t spi_flash_mutex;
void spi_flash_init(void) {
spi_flash_mutex = xSemaphoreCreateMutex();
if (spi_flash_mutex == NULL) {
// 处理创建失败
}
// 其他初始化代码
}
2. 封装原子操作函数
将 SPI Flash 的底层操作封装为带锁的函数,确保每次调用都是原子的。
// 原子读操作
int spi_flash_read_atomic(uint32_t addr, uint8_t *buf, uint32_t len) {
if (xSemaphoreTake(spi_flash_mutex, portMAX_DELAY) != pdTRUE) {
return -1; // 获取失败
}
// 执行 SPI Flash 读操作(非抢占)
spi_flash_read(addr, buf, len);
xSemaphoreGive(spi_flash_mutex);
return 0;
}
// 原子写操作(含擦除)
int spi_flash_write_atomic(uint32_t addr, const uint8_t *data, uint32_t len) {
if (xSemaphoreTake(spi_flash_mutex, portMAX_DELAY) != pdTRUE) {
return -1;
}
// 擦除扇区(如果需要)
spi_flash_erase_sector(addr);
// 写入数据
spi_flash_write(addr, data, len);
xSemaphoreGive(spi_flash_mutex);
return 0;
}
3. 任务中调用
各任务直接调用封装后的原子函数,无需关心锁细节。
void log_task(void *arg) {
uint8_t log_data[64];
while (1) {
// 生成日志数据
spi_flash_write_atomic(LOG_ADDR, log_data, sizeof(log_data));
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void config_task(void *arg) {
uint8_t cfg[32];
while (1) {
// 读取配置
spi_flash_read_atomic(CFG_ADDR, cfg, sizeof(cfg));
// 处理配置
vTaskDelay(pdMS_TO_TICKS(500));
}
}
五、完整示例代码
以下是一个基于 STM32 + FreeRTOS 的完整示例,演示如何保护 SPI Flash 读写。
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "spi_flash.h" // 假设已有底层驱动
// 互斥信号量句柄
SemaphoreHandle_t spi_flash_mutex;
// 底层 SPI Flash 操作(假设已实现)
extern void spi_flash_read(uint32_t addr, uint8_t *buf, uint32_t len);
extern void spi_flash_write(uint32_t addr, const uint8_t *buf, uint32_t len);
extern void spi_flash_erase_sector(uint32_t addr);
// 原子读
int spi_flash_read_atomic(uint32_t addr, uint8_t *buf, uint32_t len) {
if (xSemaphoreTake(spi_flash_mutex, portMAX_DELAY) != pdTRUE) {
return -1;
}
spi_flash_read(addr, buf, len);
xSemaphoreGive(spi_flash_mutex);
return 0;
}
// 原子写(含擦除)
int spi_flash_write_atomic(uint32_t addr, const uint8_t *data, uint32_t len) {
if (xSemaphoreTake(spi_flash_mutex, portMAX_DELAY) != pdTRUE) {
return -1;
}
// 假设每次写前擦除扇区(实际需根据 Flash 特性)
spi_flash_erase_sector(addr);
spi_flash_write(addr, data, len);
xSemaphoreGive(spi_flash_mutex);
return 0;
}
// 任务1:日志写入
void vLogTask(void *arg) {
uint8_t log_buf[64];
uint32_t log_addr = 0x10000;
while (1) {
// 生成日志
snprintf((char*)log_buf, sizeof(log_buf), "Log at %lu", (unsigned long)xTaskGetTickCount());
spi_flash_write_atomic(log_addr, log_buf, strlen((char*)log_buf)+1);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务2:配置读取
void vConfigTask(void *arg) {
uint8_t cfg[32];
uint32_t cfg_addr = 0x20000;
while (1) {
if (spi_flash_read_atomic(cfg_addr, cfg, sizeof(cfg)) == 0) {
// 处理配置
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void) {
// 初始化硬件和 RTOS
spi_flash_init();
spi_flash_mutex = xSemaphoreCreateMutex();
if (spi_flash_mutex == NULL) {
// 错误处理
}
xTaskCreate(vLogTask, "LogTask", 256, NULL, 2, NULL);
xTaskCreate(vConfigTask, "CfgTask", 256, NULL, 1, NULL);
vTaskStartScheduler();
// 不应到达这里
}
六、注意事项
- 锁粒度:锁的粒度应尽量小,避免长时间占用信号量导致其他任务阻塞。例如,不要将整个 Flash 操作(如擦除多个扇区)放在一个锁内,除非必要。
-
超时处理:
xSemaphoreTake的等待时间应根据任务实时性要求设置,避免无限等待导致系统卡死。可使用pdMS_TO_TICKS(100)等有限超时。 -
中断上下文:不要在中断服务函数中调用
xSemaphoreTake,因为互斥信号量不支持从中断获取。若需中断与任务同步,应使用二值信号量或队列。 - 优先级反转:虽然互斥信号量有优先级继承,但若多个低优先级任务竞争,仍可能影响高优先级任务。合理设计任务优先级,避免低优先级任务频繁访问 Flash。
- Flash 特性:SPI Flash 写入前需擦除,且擦除操作耗时较长。在锁内执行擦除会阻塞其他任务,可考虑将擦除和写入拆分为两个原子操作,或使用双缓冲策略。
- 调试:在开发阶段,可添加日志记录锁获取和释放,便于分析死锁或超时问题。
七、总结
通过 RTOS 互斥信号量,我们可以简单高效地实现对 SPI Flash 的原子访问保护,避免多任务并发导致的数据错乱。本文以 FreeRTOS 为例,提供了完整的配置步骤和代码示例,并指出了常见陷阱。在实际项目中,应根据具体场景调整锁粒度和超时策略,确保系统稳定性和实时性。掌握信号量的正确使用,是嵌入式多任务开发的核心技能之一。