引言

在嵌入式系统中,SPI Flash 常用于存储配置参数、日志或固件升级数据。当多个线程(如传感器采集线程、网络服务线程、用户命令线程)需要读写 Flash 时,并发访问会导致数据错乱或擦写冲突。RT-Thread 提供了互斥量(mutex)和信号量(semaphore)两种同步机制,但很多开发者混淆其适用场景。本文将从原理到实战,分析如何选择,并给出可复用的代码框架。

互斥量与信号量的本质区别

1. 互斥量(Mutex)

  • 所有权:互斥量具有所有权,即只能由持有它的线程释放。这防止了意外解锁。
  • 优先级继承:RT-Thread 的互斥量支持优先级继承,可解决优先级反转问题(低优先级线程持有锁,高优先级线程等待时,临时提升低优先级线程优先级)。
  • 典型用途:保护共享资源(如全局变量、外设寄存器),确保临界区互斥访问。

2. 信号量(Semaphore)

  • 计数机制:信号量是一个计数器,可表示资源数量(如缓冲区空位)。二值信号量(计数为1)可模拟互斥,但无所有权和优先级继承。
  • 同步用途:常用于任务同步(如生产者-消费者模型)或事件通知(如中断释放信号量)。
  • 风险:若用二值信号量保护资源,可能因线程误释放导致死锁或数据损坏。

3. 关键对比表

| 特性 | 互斥量 | 二值信号量 | |------|--------|------------| | 所有权 | 有 | 无 | | 优先级继承 | 支持 | 不支持 | | 典型场景 | 资源互斥 | 同步/事件 | | 误释放风险 | 低 | 高 |

SPI Flash 访问的特性分析

SPI Flash 操作(如读、写、擦除)具有以下特点:

  • 耗时操作:页写入(通常 256B)需数毫秒,扇区擦除(4KB)可达数百毫秒。
  • 不可中断:擦写过程中若被其他线程打断,可能导致 Flash 状态机混乱。
  • 共享总线:SPI 外设是共享资源,多个设备(如 Flash、SD卡)可能共用同一 SPI 总线。
  • 中断上下文:某些系统可能从中断中触发 Flash 操作(如日志记录),但中断中不能阻塞。

因此,保护 Flash 访问时,必须确保:

  1. 同一时刻只有一个线程执行完整操作(读-改-写序列)。
  2. 等待期间允许高优先级任务抢占,但需避免优先级反转。

选型建议:优先互斥量,除非特殊需求

为什么互斥量更合适?

  • 数据一致性:互斥量确保临界区原子性,且线程不会误释放。
  • 优先级继承:若高优先级线程等待 Flash,低优先级线程持有锁时,其优先级会被提升,减少等待时间,避免中优先级线程抢占导致的高优先级饥饿。
  • 语义清晰:互斥量明确表达“独占访问”,代码可读性高。

何时考虑信号量?

  • 需要计数资源:例如,Flash 有多个独立扇区,可同时写不同扇区(但 SPI 总线仍共享,需额外保护)。
  • 同步场景:如一个线程负责擦除,另一个线程等待擦除完成,可用信号量通知。
  • 中断中释放:若中断中需唤醒等待线程,信号量可在中断中释放(互斥量不允许)。但注意:中断中不能调用阻塞 API,需用 rt_sem_release 等非阻塞函数。

实战代码示例

以下示例展示如何使用互斥量保护 SPI Flash 的读写操作。

1. 初始化互斥量

#include <rtthread.h>

static rt_mutex_t flash_mutex;

void flash_driver_init(void)
{
    flash_mutex = rt_mutex_create("flash_mutex", RT_IPC_FLAG_PRIO);
    RT_ASSERT(flash_mutex != RT_NULL);
}

2. 封装 Flash 操作函数

/* 读取 Flash 数据(带互斥保护) */
int flash_read_protected(uint32_t addr, void *buf, size_t size)
{
    rt_err_t result;
    int ret = -1;

    result = rt_mutex_take(flash_mutex, RT_WAITING_FOREVER);
    if (result == RT_EOK)
    {
        /* 执行实际 SPI 读取操作 */
        ret = spi_flash_read(addr, buf, size);
        rt_mutex_release(flash_mutex);
    }
    return ret;
}

/* 写入 Flash 数据(带互斥保护) */
int flash_write_protected(uint32_t addr, const void *buf, size_t size)
{
    rt_err_t result;
    int ret = -1;

    result = rt_mutex_take(flash_mutex, RT_WAITING_FOREVER);
    if (result == RT_EOK)
    {
        /* 执行实际 SPI 写入操作,可能包含擦除等耗时操作 */
        ret = spi_flash_write(addr, buf, size);
        rt_mutex_release(flash_mutex);
    }
    return ret;
}

3. 多线程调用示例

void thread1_entry(void *param)
{
    uint8_t data[256];
    while (1)
    {
        flash_read_protected(0x10000, data, sizeof(data));
        rt_thread_mdelay(100);
    }
}

void thread2_entry(void *param)
{
    uint8_t data[256] = {0xAA};
    while (1)
    {
        flash_write_protected(0x10000, data, sizeof(data));
        rt_thread_mdelay(200);
    }
}

注意事项与陷阱

  • 避免在中断中获取互斥量:互斥量获取可能阻塞,中断中不能调用。若需中断中触发 Flash 操作,应使用信号量通知线程,由线程处理。
  • 超时设置rt_mutex_take 的超时时间建议设为 RT_WAITING_FOREVER 或合理有限值,避免死锁。若使用有限超时,需处理超时错误。
  • 嵌套访问:若在已持有互斥量的线程中再次调用受保护的函数,会导致死锁。可使用 rt_mutex_take 的递归特性(RT-Thread 互斥量支持递归),但需注意释放次数匹配。
  • 优先级反转:虽然互斥量有优先级继承,但若持有时间过长(如 Flash 擦除),仍可能影响实时性。可考虑将 Flash 操作拆分为多个小步骤,或使用异步模型。
  • 信号量误用:若用二值信号量保护 Flash,务必确保每个 take 都有对应的 release,且不能由其他线程释放。否则会导致资源永久锁定。

总结

在多线程访问 SPI Flash 时,互斥量是首选,因为它提供所有权和优先级继承,能有效保护临界区并避免优先级反转。信号量更适合同步场景或资源计数。实际项目中,可结合两者:用互斥量保护 Flash 操作,用信号量通知操作完成(如擦除完成中断)。理解本质差异,才能写出健壮的嵌入式代码。