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的队列

Queue

FreeRTOS的队列集

Queue_Set

FreeRTOS的信号量

Semaphore

FreeRTOS的互斥量

Mutex

FreeRTOS的事件组

Event_Group

任务通知

TaskNote

实际应用中的选择建议

各种通信机制的比较

机制 使用场景 优点 缺点
队列 任务间数据传输 线程安全,支持阻塞 内存开销较大
信号量 同步和资源管理 简单高效 不能传递数据
互斥量 共享资源保护 优先级继承,防止优先级反转 只能用于互斥
事件组 多事件等待 可以等待多个事件 不能传递数据
任务通知 轻量级通信 速度最快,内存开销最小 只能一对一通信

选择指南

  1. 需要传递数据 → 使用队列
  2. 简单的任务同步 → 使用二进制信号量或任务通知
  3. 管理多个相同资源 → 使用计数信号量
  4. 保护共享资源 → 使用互斥量(特别是可能发生优先级反转时)
  5. 等待多个事件 → 使用事件组或任务通知(设置位方式)
  6. 追求最高性能 → 优先考虑任务通知

最佳实践

  • 避免在中断服务程序中执行耗时操作,使用FromISR版本函数
  • 合理设置阻塞超时时间,避免任务永久阻塞
  • 注意优先级安排,防止优先级反转和饥饿现象
  • 使用互斥量保护共享资源时,保持临界区代码尽可能短
  • 定期检查API函数的返回值,处理异常情况

通过合理选择和使用这些同步与通信机制,可以构建出稳定、高效的FreeRTOS应用程序。