Sync Comms
目录
为什么需要任务间的同步与通信
在嵌入式系统中,多个任务同时运行时,它们之间往往不是完全独立的,而是需要相互协作。这种协作关系主要体现在:
-
共享资源保护:当多个任务需要访问同一个硬件资源(如串口、显示屏)或软件资源(如全局变量)时,需要确保同一时间只有一个任务能够访问,避免数据损坏或行为异常。
-
执行顺序协调:某些任务需要等待其他任务完成特定操作后才能开始执行,比如任务B必须等待任务A准备好数据后才能进行处理。
-
数据传递:任务之间经常需要传递数据,比如传感器采集任务需要将数据传递给数据处理任务。
如果没有合适的同步与通信机制,就会出现数据竞争、死锁、优先级反转等问题,导致系统不稳定。
同步与互斥的基本概念
同步
同步指的是任务之间按照某种预定的顺序执行,一个任务执行到某个点时需要等待另一个任务发出信号后才能继续。就像两个人合作完成一项工作,需要互相配合、步调一致。
互斥
互斥指的是保护共享资源,确保在同一时刻只有一个任务能够访问该资源。当某个任务正在使用共享资源时,其他任务必须等待,直到该资源被释放。
临界区
临界区是指访问共享资源的代码段,这段代码在执行时不能被中断或其他任务打断。对临界区的保护是实现互斥的关键。
数据传输的方法
| 数据容量 | 互斥措施 | 阻塞-唤醒 | |
|---|---|---|---|
| 全局变量 | 1个 | 无 | 无 |
| 环形缓冲区 | 多个 | 无 | 无 |
| 队列 | 多个 | 有 | 有 |
环形缓冲区
顾名思义,环形的缓存,比如
int buf[SIZE];
int read_pos,write_pos;
//空状态
read_pos == write_pos;
//写入(未满才能写,定义数据量为SIZE-1时满,即此时write_pos+1==read_pos)
if((write_pos+1)%SIZE != read_pos){
buf[write_pos++]=data;
write_pos%=SIZE;
}
//读取(不空)
if(read_pos!=write_pos){
val = buf[read_pos++];
read_pos%=SIZE;
}
思考:为什么不适用变量num储存数据个数来判断是否满和空?
有缺陷的环形buffer
num为全局变量,在写入和读出时都会被修改。
num++,num--都可能会被任务切换打断,导致更新错乱:
//正常流程 num=10
num -> R0
R0++
R0->num //num=11
//打断流程
num -> R0
<-被打断,切换到B执行了num--
<-恢复
R0++
R0->num //num=11
然而正常的num应该为10
问题所在
让多个任务修改同一个变量。
适用
可见,在两个任务间的通信,如果不要求效率(即阻塞-唤醒机制),使用正确的环形缓冲区就可以避免冲突问题。
队列的本质
就是环形缓冲区,增加了保护措施(所以可以有数据个数;读位置,写位置);如果只是改变计数值,那就变成了信号量(semaphore);信号量再限制计数值上限为1,就变成了互斥量(mutex);
FreeRTOS的队列
FreeRTOS的队列集
FreeRTOS的信号量
FreeRTOS的互斥量
FreeRTOS的事件组
任务通知
实际应用中的选择建议
各种通信机制的比较
| 机制 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
| 队列 | 任务间数据传输 | 线程安全,支持阻塞 | 内存开销较大 |
| 信号量 | 同步和资源管理 | 简单高效 | 不能传递数据 |
| 互斥量 | 共享资源保护 | 优先级继承,防止优先级反转 | 只能用于互斥 |
| 事件组 | 多事件等待 | 可以等待多个事件 | 不能传递数据 |
| 任务通知 | 轻量级通信 | 速度最快,内存开销最小 | 只能一对一通信 |
选择指南
- 需要传递数据 → 使用队列
- 简单的任务同步 → 使用二进制信号量或任务通知
- 管理多个相同资源 → 使用计数信号量
- 保护共享资源 → 使用互斥量(特别是可能发生优先级反转时)
- 等待多个事件 → 使用事件组或任务通知(设置位方式)
- 追求最高性能 → 优先考虑任务通知
最佳实践
- 避免在中断服务程序中执行耗时操作,使用FromISR版本函数
- 合理设置阻塞超时时间,避免任务永久阻塞
- 注意优先级安排,防止优先级反转和饥饿现象
- 使用互斥量保护共享资源时,保持临界区代码尽可能短
- 定期检查API函数的返回值,处理异常情况
通过合理选择和使用这些同步与通信机制,可以构建出稳定、高效的FreeRTOS应用程序。