# 基于 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 为例,提供了完整的配置步骤和代码示例,并指出了常见陷阱。在实际项目中,应根据具体场景调整锁粒度和超时策略,确保系统稳定性和实时性。掌握信号量的正确使用,是嵌入式多任务开发的核心技能之一。