深入浅出 Z-Stack 2006 OSAL 多任务资源分配机制
一 概述
OSAL (Operating System Abstraction Layer),翻译为“操作系统抽象层”。如何理解这个复
杂的名词呢?表面上看它是作为操作系统存在的,可是为什么又加上“抽象层”呢?它的本质
是什么?在 Z-Stack 协议栈中,它又扮演了什么角色呢?要解答这些问题,我们必须先从宏
观入手,渐渐深入探究,最后答案自然会浮出水面。
下图是 ZigBee 协议的结构图:
从这幅图中,我们可以很清楚地从宏观上了解 ZigBee 协议的结构。可是,经过粗略的浏览,
我们并没有发现任何 OSAL 的踪迹。当然,我们都知道,Z-Stack 与 ZigBee 之间并不能完全
划等号。Z-Stack 是 ZigBee 的具体实现,所以存在于 Z-Stack 中的 OSAL 并不一定出现在
ZigBee 中。但是,我们可以在 ZigBee 中找到些许 OSAL 的踪影。
在 ZigBee 协议中,协议本身已经定义了大部分内容。在基于 ZigBee 协议的应用开发中,
用户只需要实现应用程序框架即可。从上图可以看出应用程序框架中包含了最多 240 个应用
程序对象。如果我们把一个应用程序对象看做为一个任务的话,那么应用程序框架将包含一
个支持多任务的资源分配机制。于是 OSAL 便有了存在的必要性,它正是 Z-Stack 为了实现
这样一个机制而存在的。
OSAL 就是以实现多任务为核心的系统资源管理机制。所以 OSAL 与标准的操作系统还
是有很大的区别的。简单而言,OSAL 实现了类似操作系统的某些功能,但并不能称之为真
正意义上的操作系统。
二、OSAL 任务运行方式
弄明白了 OSAL 是何方神圣,接下来我们将深入 Z-Stack,进一步研究 OSAL。
为了方便,我们使用 Z-Stack 所提供的 GenericApp 这个例程为例来进行分析。此例程的
默认路径为
C:\Texas Instruments\ZStack-1.4.3-1.2.1\Projects\zstack\Samples\GenericApp。
首先我们去繁就简,先来了解应用程序的运行方式。
在右侧工作空间窗口打开 App 文件夹,我们可以看到三个文件,分别是“GenericApp.c”、
“GenericApp.h”、“OSAL_GenericApp.c”。我们整个程序所实现的功能都在这三个文件当中。
首 先 打 开 GenericApp.c 这 个 文 件 。 我 们 首 先 看 到 的 是 比 较 重 要 的 两 个 函 数 :
GenericApp_Init 和 GenericApp_ProcessEvent。从函数名称上我们很容易得到的信息便是,
GenericApp_Init 是任务的初始化函数,而 GenericApp_ProcessEvent 则负责处理传递给此任
务的事件。
大概浏览一下 GenericApp_ProcessEvent 这个函数,我们可以发现,此函数的主要功能
是判断由参数传递的事件类型,然后执行相应的事件处理函数。我们可以由此推断 Z-Stack
应用程序的运行机制如下图所示:
当有一个事件发生的时候,OSAL 负责将此事件分配给能够处理此事件的任务,然后此任
务判断事件的类型,调用相应的事件处理程序进行处理。
明白了这个问题,新的问题又摆在了我们的面前:OSAL 是如何传递事件给任务的。
三、OSAL 的事件传递机制
在试图弄清楚这个问题之前,我们需要弄清楚另外一个十分基础而重要的问题。那就是
如何向我们的应用程序中添加一个任务。
我们先来看看 GenericApp 是如何添加任务的。
我们打开 OSAL_GenericApp.c 文件。这里我们可以找到一个很重要的数组 tasksArr 和
一个同样很重要的函数 osalInitTasks。
TaskArr 这个数组里存放了所有任务的事件处理函数的地址,在这里事件处理函数就代
表了任务本身,也就是说事件处理函数标识了与其对应的任务。
osalInitTasks 是 OSAL 的任务初始化函数,所有任务的初始化工作都在这里面完成,并
且自动给每个任务分配一个 ID。
要添加新任务,我们需要编写新任务的事件处理函数和初始化函数,然后将事件处理函
数的地址加入此数组。然后在 osalInitTasks 中调用此任务的初始化函数。在此例中,我们此
前提到过的 GenericApp_ProcessEvent 这个函数被添加到了数组的末尾, GenericApp_Init 这
个函数在 osalInitTasks 中被调用。
值得注意的是,TaskArr 数组里各任务函数的排列顺序要与 osalInitTasks 函数中调用各
任务初始化函数的顺序必须一直,只有这样才能够保证每个任务能够通过初始化函数接收到
正确的任务 ID。
另外,为了保存任务初始化函数所接收的任务 ID,我们需要给每一个任务定义一个全
局 变 量 来 保 存 这 个 ID 。 在 GenericApp 中 GenericApp.c 中 定 义 了 一 个 全 局 变 量
GenericApp_TaskID;并且在 GenericApp_Init 函数中进行了赋值
{
GenericApp_TaskID = task_id;
}
这条语句将分配给 GenericApp 的任务 ID 保存了下来。
到此,我们就给应用程序中完整的添加了一个任务。
我们回到 OSAL 如何将事件分配给任务这个问题上来
在 OSAL_GenericApp.c 这个文件中,在定义 TaskArr 这个数组之后,又定义了两个全局
变量。
tasksCnt 这个变量保存了当前的任务个数。
tasksEvents 是一个指向数组的指针,此数组保存了当前任务的状态。在任务初始化函数
中做了如下操作
{
}
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
/*osal_mem_alloc()为当前 OSAL 中的各任务分配存储空间(实际上是一个任务数组),
函数返回指向任务缓冲区的指针,因此 tasksEvents 指向该任务数组(任务队列).注意
tasksEvents 和后面谈到的 tasksArr[]里的顺序是一一对应的, tasksArr[ ]中的第 i 个
事件处理函数对应于 tasksEvents 中的第 i 个任务的事件.*/
/*osal_memset()把开辟的内存全部设置为 0;sizeof( uint16 )是 4 个字节,即一个任
务的长度(同样是 uint16 定义),乘以任务数量 tasksCnt,即全部内存空间*/
我们可以看出所有任务的状态都被初始化为 0。代表了当前任务没有需要响应的事件。
紧接着,我们来到了 main()函数。此函数在 ZMain 文件夹的 ZMain.c 文件中。略过许多
对 当 前 来 说 并 非 重 要 的 语 句 , 我 们 先 来 看 osal_init_system() 这 个 函 数 。 在 此 函 数 中 ,
osalInitTasks()被调用,从而 tasksEvents 中的所有内容被初始化为 0。
之后,在 main()函数中,我们进入了 osal_start_system()函数,此函数为一个死循环,在
这个循环中,完成了所有的事件分配。
首先我们来看这样一段代码:
{
do
{
if (tasksEvents[idx])
{
break;
}
} while (++idx < tasksCnt);
}
当 tasksEvents 这个数组中的某个元素不为 0,即代表此任务有事件需要相应,事件类型
取决于这个元素的值。这个 do-while 循环会选出当前优先级最高的需要响应的任务,
{
events = (tasksArr[idx])( idx, events );
}
此语句调用 tasksArr 数组里面相应的事件处理函数来响应事件。如果我们新添加的任务
有了需要响应的事件,那么此任务的事件处理程序将会被调用。
就这样,OSAL 就将需要响应的事件传递给了对应的任务处理函数进行处理。
四、事件的捕获
不过接下来就有了更加深入的问题了,事件是如何被捕获的?直观一些来说就是,
tasksEvents 这个数组里的元素是什么时候被设定为非零数,来表示有事件需要处理的?为了
详细的说明这个过程,我将以 GenericApp 这个例程中响应按键的过程来进行说明。其他的
事件虽然稍有差别,却是大同小异。
按键在我们的应用里面应该属于硬件资源,所以 OSAL 理应为我们提供使用和管理这
些硬件的服务。稍微留意一下我们之前说过的 tasksArr 这样一个数组,它保存了所有任务的
事件处理函数。我们从中发现了一个很重要的信息:Hal_ProcessEvent。HAL(Hardware
Abstraction Layer)翻译为“硬件抽象层”。许多人在这里经常把将 Z-Stack 的硬件抽象层与
ZigBee 的物理层混为一谈。在这里,我们应该将 Z-Stack 的硬件抽象层与 ZigBee 的物理层
区分开来。硬件抽象层所包含的范围是我们当前硬件电路上面所有对于系统可用的设备资
源。而 ZigBee 中的物理层则是针对无线通信而言,它所包含的仅限于支持无线通讯的硬件
设备。
通过这个重要的信息,我们可以得出这样一个结论:OSAL 将硬件的管理也作为一个任
务来处理。那么我们很自然的去寻找 Hal_ProcessEvent 这个事件处理函数,看看它究竟是如
何管理硬件资源的。
在“HAL\Commen\ hal_drivers.c”这个文件中,我们找到了这个函数。我们直接分析与按
键有关的一部分。
{
if (events & HAL_KEY_EVENT)
{
#if (defined HAL_KEY) && (HAL_KEY == TRUE)
/* Check for keys */
HalKeyPoll();
/* if interrupt disabled, do next polling */
if (!Hal_KeyIntEnable)
{
osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);
}
#endif // HAL_KEY
return events ^ HAL_KEY_EVENT;
}
}
在事件处理函数接收到 HAL_KEY_EVENT 这样一个事件后,首先执行 HalKeyPoll()函
数 。 由 于 这 个 例 程 的 按 键 采 用 查 询 的 方 法 获 取 , 所 以 是 禁 止 中 断 的 , 于 是 表 达 式
(!Hal_KeyIntEnable)的值为真。那么 osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT,
100)得以执行。osal_start_timerEx 这是一个很常用的函数,它在这里的功能是经过 100 毫秒
后,向 Hal_TaskID 这个 ID 所标示的任务(也就是其本身)发送一个 HAL_KEY_EVENT 事
件。这样以来,每经过 100 毫秒,Hal_ProcessEvent 这个事件处理函数都会至少执行一次来
处理 HAL_KEY_EVENT 事件。也就是说每隔 100 毫秒都会执行 HalKeyPoll()函数。
那么我们来看看 HalKeyPoll 函数到底在搞什么鬼!
代码中给的注释为:
/* Check for keys */
HalKeyPoll();
于是我们推断这个函数的作用是检查当前的按键情况。进入函数一看,果不其然。虽然
这个函数很长很复杂,不过凭借着非凡的聪明才智,我们还是十分清楚的明白了,经过一系
列的 if 语句和赋值语句,在接近函数末尾的地方, keys 变量(在函数起始位置定义的)获
得了当前按键的状态。最后,有一个十分重要的函数调用。
(pHalKeyProcessFunction) (keys, HAL_KEY_STATE_NORMAL);
pHalKeyProcessFunction 这个函数指针指向了哪个函数我们现在依然不清楚,但是为了
我们有个清晰而不间断的思路,我在这里先告诉大家。在这里调用的是
void OnBoard_KeyCallback ( uint8 keys, uint8 state )
这个函数。此函数在“ZMain\OnBoard .c”文件中可以找到。在这个函数中,又调用了
void OnBoard_KeyCallback ( uint8 keys, uint8 state )
在这个函数中,按键的状态信息被封装到了一个消息结构体中(对于消息,我们稍后再
说)。最后有一个极其重要的函数被调用了。
osal_msg_send( registeredKeysTaskID, (uint8 *)msgPtr );
与前面的 pHalKeyProcessFunction 相同,我先直接告诉大家 registeredKeysTaskID 所指
示的任务正是我们需要响应按键的 GenericApp 这个任务。
那 么 也 就 是 说 , 在 这 里 我 们 向 GenericApp 发 送 了 一 个 附 带 按 键 信 息 的 消 息 。 在
osal_msg_send 函数中
osal_set_event( destination_task, SYS_EVENT_MSG );
被调用,它在这里的作用是设置 destination_task 这个任务的事件为 SYS_EVENT_MSG。
而这个 destination_task 正式由 osal_msg_send 这个函数通过参数传递而来的,它也指示的是
GenericApp 这个任务。在 osal_set_event 这个函数中,有这样一个语句:
{
tasksEvents[task_id] |= event_flag;
}
至此,刚才所提到的问题得到了解决。我们再将这个过程整理一遍。
首先,OSAL 专门建立了一个任务来对硬件资源进行管理,这个任务的事件处理函数是
Hal_ProcessEvent 。 在 这 个 函 数 中 通 过 调 用 osal_start_timerEx( Hal_TaskID,
HAL_KEY_EVENT, 100); 这个 函数 使得每 隔 100 毫秒 就会 执行一 次 HalKeyPoll()函数 。
HalKeyPoll()获取当前按键的状态,并且通过调用 OnBoard_KeyCallback 函数向 GenericApp
任务发送一个按键消息,并且设置 tasksEvents 中 GenericApp 所对应的值为非零。如此,当
main 函数里这样一段代码
{
do
{
if (tasksEvents[idx])
{
break;
}
} while (++idx < tasksCnt);
}
执行了以后,GenericApp 这个任务就会被挑选出来。然后通过
events = (tasksArr[idx])( idx, events );
这个函数调用其事件处理函数,完成事件的响应。
现在,我们回过头来处理我们之前遗留下来的问题。
第一、pHalKeyProcessFunction 这个函数指针为何指向了 OnBoard_KeyCallback 函数。
在 HAL\Commen\ hal_drivers.c 这个文件中,我们找到了 HalDriverInit 这个函数,在这
个函数中,按键的初始化函数 HalKeyInit 被调用。在 HalKeyInit 中有这样的语句:
{
pHalKeyProcessFunction = NULL;
}
这 说 明 在 初 始 化 以 后 pHalKeyProcessFunction 并 没 有 指 向 任 何 一 个 函 数 。 那
pHalKeyProcessFunction 是什么时候被赋值的呢?
就在 HalKeyInit 的下方有一个这样的函数 HalKeyConfig。其中有这样一条语句:
pHalKeyProcessFunction = cback;
cback 是 HalKeyConfig 所传进来的参数,所以,想要知道它所指向的函数,必须找到其
调用的地方。经过简单的搜索我们不难找出答案。在 main 函数中有这样一个函数调用:
InitBoard( OB_READY );此函数中做了如下调用:
{
HalKeyConfig( OnboardKeyIntEnable, OnBoard_KeyCallback);
}
第二、registeredKeysTaskID 为什么标识了 GenericApp 这个任务?
由于 OSAL 是一个支持多任务的调度机制,所以在同一时间内将会有多个任务同时运行。
但是从逻辑上来讲,一个事件只能由一个任务来处理。按键事件也不例外。
那么如何向 OSAL 声明处理按键事件的任务是 GenericApp 呢?
在 GenericApp_Init(GenericApp 的任务初始化函数)中有这么一个语句:
{
RegisterForKeys( GenericApp_TaskID );
}
RegisterForKeys 函 数 向 OSAL 声 明 按 键 事 件 将 由 GenericApp 任 务 来 处 理 。 在
RegisterForKeys 函数中:
{
registeredKeysTaskID = task_id;
}
我想我不用再做多余的解释了,聪明的您肯定可以理解。
五、消息队列
首先我需要向大家解释清楚消息与事件的联系。事件是驱动任务去执行某些操作的条件,
当系统产生了一个事件,将这个传递给相应的任务后,任务才能执行一个相应的操作。但是
某些事件在它发生的同时,又伴随着一些附加信息的产生。任务的事件处理函数在处理这个
事件的时候,还需要参考其附加信息。最典型的一类便是按键消息,它同时产生了一个哪个
按键被按下了附加信息。所以在 OnBoard_SendKeys 这个函数中,不仅向 GenericApp 发送了
事件,还通过调用 osal_msg_send 函数向 GenericApp 发送了一个消息,这个消息记录了这
个事件的附加信息。在 GenericApp_ProcessEvent 中,通过
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( GenericApp_TaskID );
}
获取了这样一个消息,然后再进一步处理。
OSAL 在后台维护了一个消息队列,每一个消息都会被放到这个消息队列中去,当任务接
收到事件以后,从消息队列中获取属于自己的消息,然后进行处理。
以上就是我就将 OSAL 这样一个事件驱动的多任务的资源分配机制做了一个简明扼要的
介绍,希望对大家有所帮助。