RTOS环境下多任务编程要点

来源:互联网 发布:零基础学javascript 编辑:程序博客网 时间:2024/05/18 13:26

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 动态生成消息

0 0
原创粉丝点击