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