RTOS环境下多任务编程要点
一. 分析阶段
1. 需求分析,予以文档描述;
2. 一些初始化问题,探究需求分析中的关键点;
3. 解决时序问题,系统中算法的分析;
4. 决定使用RTOS,依赖于时间响应和任务数量;
5. 划分任务,确定系统所需的任务和模块;
6. 系统间通信,消息机制是最优的方法之一;
7. 共享数据处理,创建独立的模块;
8. 结论,绘制系统设计图。
二. 编码实现
1. 系统组成的基本单元是模块,即单个的C源文件,它封装了数据与程序;
2. 模块之间交互通过调用函数来实现,没有使用任何全局变量;
3. 防止竞态发生的手段是信号量;
4. 非立即获取的数据由回调函数予以处理;
5. 向任务发送消息的方法封装在“外壳”函数之中;
6. 所有模块调用的函数原型全部组织在一个头文件之中;
7. 模块中都有初始化函数,负责任务创建、初始化信号量和队列等,并由main()主函数统一调用。
三. 防止竞态(以下任务包括ISR)
1. 竞态只可能发生在全局的资源上(硬件/全局变量/静态变量),而像任务的私有资源(仅被自己访问)和堆栈数据是不可能发生竞态的;
2. 一个函数被哪个任务调用,它就代表该任务的访问行为,与该函数定义位置无关(回调函数是最让人迷惑的,它定义在A任务中,但它被任务B调用,所以它代表任务B的访问行为);
3. 凡是被多个任务访问(包括读/写)的资源,必定发生竞态;
4. 需要保护的共享资源必须确保在使用范围之内都是处于保护状态;
5. 保护的手段:禁止/启用IRQ、禁止/启用任务调度、信号量。
四. 线程通信
方式一 实时操作系统中任务通信的经典方式是:任务阻塞在等待消息上,直到中断服务程序或者其他任务给自己发送消息。如:
void TaskA(void)
{
while (FOREVER)
{
OSQPend(p_QData, 0, &err); /* waiting for events */
/**** deal with received events ****/
}
}
当其他任务向自己发送消息时,把该消息封装在“外壳函数”中,这样可以避免使用全局变量,同时增加了安全性:
void taska_SendPrompt(Int IMsg)
{
assert(IMsg >= MIN_VAL && Imsg <= MAX_VAL);
OSQPost(p_QData, (void *)IMsg);
}
方式二结合邮箱和信号量可以让任务进行更高级的通信方式,如下代码所示,任务A先创建一信号量,绑定信号量和它希望任务B所做的事情到消息中,再投递该消息到邮箱,然后在信号量上进行等待,当结束等待时说明任务B已经干完该事情,则删除信号量。
void TaskA(void)
{
msg.sem = CreateSemaphore(0);
msg.func = TaskAFunc;
PostMBox(mbox, &msg); /* tell TaskB do something */
WaitSemaphore(msg.sem); /* waiting until TaskB done */
FreeSemaphore(msg.sem);
}
任务B首先在邮箱上等待,当接收到消息后调用函数,完成任务A希望它干的事情,再释放信号量通知任务A工作完毕。
void TaskB(void)
{
msg = PendMBox(mbox);
msg.func(&msg); /* do something that TaskA desired */
SignalSemaphore(msg.sem); /* tell TaskA thing is done */
}
五. 线程共享
1)简单共享,如图1所示,任务A写数据块DataStructure后给任务B发送消息,任务B提取消息并从任务A复制数据块DataStructure到自己的数据区TaskBData中。
优点:简单,容易实现,适合于慢速通信。
缺点:①任务B如果响应速度不够快则有可能任务A又改写了数据块而发生错误(Write两次,Read一次);②DataStructure同时被任务A和任务B访问,为防止“竞态”错误需要进行保护。
图1 简单共享
2)抽象成数据模块,加信号量予以保护,如图2所示。
优点:简单,安全,模块化,适应于被多个任务共享的数据。
缺点:①任务可能会在该信号上阻塞很长时间;②任务为了方便操作数据一般会建立自己的副本,这样一来占用更多的数据存储区。
图2 带信号量的数据模块
3)回调函数,如图3所示,任务A把自己的回调函数传递给任务B的消息队列中,任务B从队列中提取并调用该函数,此时回调函数即可以访问任务B的数据,又可以访问任务A的数据,给编程带来极大的自由度。
优点:自由灵活,如任务A和任务C对任务B中的数据进行不同的运算,只需要修改任务对应的回调函数即可,特别适应于任务中数据被多个其他任务使用且运算方式不同的场合。
缺点:①较为复杂,需要清晰了解回调函数的数据流才能正确使用;②回调函数定义在任务A但被任务B使用,它就代表任务B的行为,这样一来TaskAData就被任务A和任务B(通过TaskACallback)共享,为避免“竞态”带来错误,需要对TaskAData进行保护(参见上述三),同理,TaskCData也需要进行保护。
图3 回调函数
4)生产者与消费者队列,如图4所示,任务A把“生产”的数据存入RingBuffer中,再将该数据的指针传递给任务B的消息队列中,任务B提取该数据的指针后从RingBuffer复制数据到TaskBData中。
优点:简单可靠,适应于尺寸相同且“生产”速率恒定的场合。
缺点:①对数据尺寸有限制,必须是相同大小的数据才能建立RingBuffer(循环缓冲区);②“生产“速率必须恒定且RingBuffer的大小设置合理,才能确保不会发生“溢出”错误。
图4 生产者与消费者队列
5)动态生成消息,如图5所示,任务A动态生成(常见为malloc())消息msg,将该消息指针传递到任务B的消息队列中,任务B提取并处理该消息后,释放(常见为free())该消息所占用的内存。
优点:适用于多任务之间进行自由通信,消息类型和大小都不受限制。
缺点:①较复杂,消息类型可能会非常多,需要较好她组织;②因为动态分配和释放内存,需要细心设计,小心内存泄露;③动态分配内存时间不可控,对于实时性要求特别高的任务需要考虑这个限制。
图5 动态生成消息