基于 RTOS 信号量实现多任务对 SPI Flash 的原子读写保护
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式多任务系统中,多个任务并发访问 SPI Flash 等共享外设时,容易产生数据竞争和总线冲突,导致读写错误甚至系统崩溃。本文深入剖析 RTOS 信号量的原理,并以 FreeRTOS 为例,详细讲解如何利用互斥信号量(Mutex)实现对 SPI Flash 的原子读写保护,提供完整代码示例和关键注意事项,帮助开发者构建稳定可靠的多任务嵌入式系统。
# 基于 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. 创建互斥信号量
在系统初始化时创建:
```c
SemaphoreHandle_t spi_flash_mutex;
void spi_flash_init(void) {
spi_flash_mutex = xSemaphoreCreateMutex();
if (spi_flash_mutex == NULL) {
// 处理创建失败
}
// 其他初始化代码
}
```
### 2. 封装原子操作函数
将 SPI Flash 的底层操作封装为带锁的函数,确保每次调用都是原子的。
```c
// 原子读操作
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. 任务中调用
各任务直接调用封装后的原子函数,无需关心锁细节。
```c
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 读写。
```c
#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 为例,提供了完整的配置步骤和代码示例,并指出了常见陷阱。在实际项目中,应根据具体场景调整锁粒度和超时策略,确保系统稳定性和实时性。掌握信号量的正确使用,是嵌入式多任务开发的核心技能之一。