查看: 59706|回复: 69

我心目中的Zstack OSAL & Message

[复制链接]
ssls18years 发表于 2010-8-20 11:32:09 | 显示全部楼层 |阅读模式
奥版和Cyril3分别发了关于OSAL专题讲座
我也把自己的心得笔记发出来给大家互相印证参考。

一. Zstack OSAL


        void osal_start_system( void )

所有应用程序,无论是自己写的最简单的测试程序还是复杂的OSAL操作系统,都必须从main( )来入口。所谓的OS操作系统,我们不妨这样想像:自己写一个最简单的main( ),里面就一句打印“Hello, World”.如果需要加入Key, LED这样的输入输出功能,那么就需要扩充main( ),加入Key, LED的驱动,如果要实现多线程调度,就要加入Timer驱动等等。其实操作系统就是这么来的,简单吧:)。
一般在OS中都会有个死循环,在这个循环中去处理各种各样的事件。在Zstack OSAL中,这个死循环就存在于osal_start_system()这个函数中。下面来详细分析在这个死循环中到底在做哪些事情。

        OSAL的“心跳”

在OSAL的死循环中,各个事件只是在某些特定的情况下发生,如果OSAL一刻不停去轮询去处理这些应用程序,迟早会累死(热量,功耗,寿命…),这样做是完全没有必要的。所以这里就引入了心跳的概念,也就是OS的时钟节奏。在Zstack OSAL中这个节奏定义为1ms, 由8 bits HW_TIMER4来控制,当然这些都可以由程序员来修改,后面就以系统的默认值来讲述。在void InitBoard( byte level )这个函数中有下面这段代码就是在定义系统的心跳Timer。
         HalTimerConfig (OSAL_TIMER,
HAL_TIMER_MODE_CTC,                  HAL_TIMER_CHANNEL_SINGLE,
  HAL_TIMER_CH_MODE_OUTPUT_COMPARE,
                  OnboardTimerIntEnable,
                  Onboard_TimerCallBack);

在OSAL的Timer定义好了以后,就要启动Timer, 至于如何启动Timer, 请自行查阅2430 Spec, 我这里想说的是,在一步步跟踪源码到死循环开始,都没有发现启动OSAL Timer的代码,最后通过观察Timer相关的控制寄存器,发现,在网络层初始化函数nwk_init( taskID++ )执行完毕后Timer启动了,也就是说在网络层初始化函数中有启动Timer的语句,因为网络层初始化是不开源的,无从去看源代码验证,总之,Timer启动了就好。

每当1ms心跳来临时,Timer4的中断标志置位,这样在OSAL的死循环中检测到这个标志置位后,就去轮询处理各事件。没有检测到这个标志位则继续死循环。在死循环的开始有调用Hal_ProcessPoll()这条语句,实际上就是在查询中断标志并作相应的处理。

        OSAL的“心跳”来临后的处理

上节提到Hal_ProcessPoll()这条语句,实际上就是在查询中断标志并作相应的处理。那么当1ms心跳来临时,我们跟踪进这个函数看看它到底干了些什么。
当判断到中断标志,表明1ms心跳来临了,就去调用Timer4相应的回调函数。这个回调函数由HalTimerConfig()的最后一个参数来定义,请回看上节,OSAL Timer的回调函数就是Onboard_TimerCallBack(),一步步跟进,最终调用osalTimerUpdate()这个函数。在这个函数中会去轮询Timer事件链表。
Timer事件链表是下面这样一个结构,next指向下一个Timer事件,timeout值表明本Timer事件还需要timeout个心跳才需要被处理,因为此处心跳是1ms,所以也就是说还需要timeout个ms才处理。所谓的处理也就是检测timeout是否小于1ms,如果小于1ms, 则发出event_flag这个消息到消息队列,这个消息隶属于task_id这个任务。如果大于1ms,说明该Timer事件还不到处理的时候,则Timeout = Timeout-1,然后继续耐心等待下一次心跳。注:Timer事件链表的维护是通过osal_start_timerEx()这个函数来实现的。

typedef struct
{
  void *next;
  UINT16 timeout;
  UINT16 event_flag;
  byte task_id;
} osalTimerRec_t;


        消息发出后的处理

上节讲到在心跳中发出任务的事件消息到消息队列。那么这个消息由谁来处理?回头再看osal_start_system( )中的死循环,有检测消息队列的语句,当发现有消息时,判断该消息隶属于哪个任务就去调用对应于该任务的消息处理函数。各任务的消息处理函数是在tasksArr[]这个常量数组中定义。这个数组中定义的消息处理函数和任务初始化函数中的任务必须一一对应。

        节电模式

细心的同学会发现在死循环体的后面有调用osal_pwrmgr_powerconserve()这样一条语句。从名字及注释来看,属于节电模式的调用。此处不详细列举代码,只讲其工作原理。
上面章节讲到1ms心跳来临时去轮询各事件Timer是否需要处理。这里心跳很快(1ms),各事件的Timeout很慢(往往成百上千)。譬如Key检测的Timer事件的Timeout是100,意思是说100ms才去检测一次是否有Key按下。假如说Key检测的Timout在各Timer事件中Timeout最小,那么也就是说有99次心跳都不会有事件需要处理,但是死循环依然在跑,在做无用功,为了解决这个问题,就加入了节电模式。
在osal_pwrmgr_powerconserve()这个函数中会检测Timer时间链表中Timeout最小的值,假设为next, 然后设定CPU进入休眠模式next个毫秒。休眠时间到了苏醒过来立即就会有Timer事件需要处理,这样就可以达到省电的目的。

        小结

到此为止,OSAL神秘面纱已完全揭开,为了巩固知识,下面以Key为例讲述从Key按下到Key消息被处理的整个过程。
首先在Key的初始化过程中会调用下面这条语句:osal_start_timerEx (Hal_TaskID, HAL_KEY_EVENT, HAL_KEY_POLLING_VALUE),这条语句的功能就是将检测Key的这个事件放入Timer事件链表。这个事件隶属于Hal_Task, Timeout是HAL_KEY_POLLING_VALUE。
当1ms心跳来临时,判断timeout是否小于1,如果不小于,则timeout=timeout-1并等待下一次心跳。如果小于1,则发出HAL_KEY_EVENT这个消息到消息队列,然后调用Hal_Task的事件处理函数Hal_ProcessEvent()处理HAL_KEY_EVENT消息,在处理这个消息的过程中调用函数HalKeyPoll()。这个函数检测当前有无按键,如果有按键并且和上次按键值不同则认为有新的按键按下并发出相应的按键消息。
在上面这个过程完成后,必须通过osal_start_timerEx()这个函数将Key检测事件继续放入Timer事件链表,以便后面心跳时能检测到该事件,也就是说每100ms都会扫描看有无按键按下。

二.  消息详解

        消息结构:消息头结构+消息数据结构(Key)



        消息空间分配函数中秘密

以Key消息为例,在调用byte * osal_msg_allocate( uint16 len )这个函数给Key消息分配空间的时候,并不是分配了len大小的空间,而是分配了len+消息头大小的空间,也就是说预留出了消息头空间以便后面对消息头进行操作。但是请注意消息空间指针没有指向消息头,而是指向消息数据。这样的做法可以说是编程上的技巧,为后面“-1”操作埋下伏笔。

        “-1”的奥秘

在给消息头赋值时有这样一条语句:
OSAL_MSG_ID( msg_ptr ) = destination_task;
                        OSAL_MSG_ID是个宏,定义如下:
#define OSAL_MSG_ID(msg_ptr)
      ((osal_msg_hdr_t *) (msg_ptr) - 1)->dest_id

很多人对这个宏定义中的-1感到迷惑,我们来分析:上节提到消息空间指针指向消息数据,也就是这里的msg_ptr. 而(osal_msg_hdr_t *) (msg_ptr)这条语句的含义是将该指针强制转换为消息头结构的指针,这样的话,-1后指针将向上移动消息头结构大小的偏移量,也就是恰好指向消息头结构处,而且因为是强制为消息头结构指针,自然就找到消息头中的dest_id成员,并将destination_task赋值给它。这样做的好处是效率高,坏处是不好理解,当然理解了也就没有坏处了:)。

        消息队列:当一个新消息封装好了以后,就要加入消息队列。由以下语句来完成。
osal_msg_enqueue( &osal_qHead, msg_ptr );
此处的osal_qHead就是消息队列的头指针。




消息队列是由头指针和消息构成,消息又分为两部分:消息头(蓝色)和消息数据(黄色)构成。所有的消息,消息头结构是完全相同的,但是消息数据部分,其大小可按任务需要进行定义。


        事件数组:消息操作完成后,还需要向系统发个通知,也就是说通知系统某某任务有消息来了,你去处理一下。事件数组和任务是一一对应的,就是说每个任务都对应事件数组中的一个单元。对于Key来说,就是往事件数组中对应与Hal任务的单元中放入SYS_EVENT_MSG。

        可以这样理解:一个小区100户,每户一个邮箱,那么小区就有100个任务,100个邮箱构成事件数组。消息是包裹,事件就是包裹通知单,包裹通知单送达某户的邮箱,这户人家查看邮箱,发现有包裹通知单,就去领出包裹,领出包裹后拆开其实就是解析消息后再决定是吃掉它,用掉它,回覆它还是丢掉它等等。

        消息的解析:正如上面包裹的例子一样,收到包裹后你一定要打开包裹看看是什么东西再决定下步的行动,反应到消息上,就是消息的解析,实际上是封装的反过程。
还是以Key为例:Hal任务的事件处理函数是Hal_ProcessEvent(),在这个函数中收到SYS_EVENT_MSG后会调用osal_msg_receive(Hal_TaskID);这条语句来进行解析,以下不在叙述。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
outman 发表于 2010-8-20 11:42:37 | 显示全部楼层
哈哈,今天热闹了。鼓掌欢迎~~
真的非常感谢大家的热情,让人很感动
大家的支持,让我们有了前进下去的动力!谢谢!
cyril3 发表于 2010-8-20 11:45:36 | 显示全部楼层
先支持,后研究
outman 发表于 2010-8-20 11:55:05 | 显示全部楼层
楼主写得很生动,有很多内容也对我们前面我们写的进行了补充,节电模式,消息的分配机制等写得更细致。比喻也恰到好处。楼主辛苦了~

已加精!
xingqing 发表于 2010-8-20 18:54:46 | 显示全部楼层
哈哈  楼主强人 你们太超前了  我这学习了
品味开心茶 发表于 2010-8-22 09:12:35 | 显示全部楼层
哇,大牛!学习了!
xingqing 发表于 2010-9-4 22:17:45 | 显示全部楼层
又重新温故了下楼主的大作  受益匪浅
icebear 发表于 2010-9-7 17:05:49 | 显示全部楼层
非常不错,受教了
aa_bb_cc 发表于 2010-9-20 09:50:32 | 显示全部楼层
感谢楼主!!!
CE_Aloysha 发表于 2010-9-20 16:39:55 | 显示全部楼层
谢谢楼主,您的帖子对初学的我很有帮助~
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表