# 引言 在嵌入式系统中,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. 初始化互斥量 ```c #include 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 操作函数 ```c /* 读取 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. 多线程调用示例 ```c 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 操作,用信号量通知操作完成(如擦除完成中断)。理解本质差异,才能写出健壮的嵌入式代码。