标题:
浅析CC2540的OSAL原理
[打印本页]
作者:
liuyy
时间:
2015-1-12 22:36
标题:
浅析CC2540的OSAL原理
一概述
OSAL (Operating System Abstraction Layer)
,翻译为
“
操作系统抽象层
”
。
OSAL
就是一种支持多任务运行的系统资源分配机制。
OSAL
与标准的操作系统还是有很大的区别的。
简单而言,
OSAL
实现了类似操作系统的某些功能,但并不能称之为真正意义上的操作系统
。
二、OSAL任务运行方式
我们以
TI1.2.1
的
BLE
协议栈中的
SimpleBLEPeripheral
为例,分析一下
OSAL
。其中有一个
simpleBLEPeripheral.c
文件,里面有
2
个比较重要的函数:
SimpleBLEPeripheral_Init
和
SimpleBLEPeripheral_ProcessEvent
。
SimpleBLEPeripheral_Init
是任务的初始化函数,而
SimpleBLEPeripheral_ProcessEvent
则负责处理传递给此任务的事件。
大概浏览一下
SimpleBLEPeripheral_ProcessEvent
这个函数,我们可以发现,此函数的主要功能是
判断由参数传递的事件类型,然后执行相应的事件处理函数
。由此,可以推断出
BLE
协议栈应用程序的运行机制如下图所示:
当有一个事件发生的时候,
OSAL
负责将此事件分配给能够处理此事件的任务,然后此任务判断事件的类型,调用相应的事件处理程序进行处理。
明白了这个问题,新的问题又摆在了我们的面前:
OSAL
是如何传递事件给任务的。
三、OSAL的事件传递机制
在试图弄清楚这个问题之前,我们需要弄清楚另外一个十分基础而重要的问题。那就是
如何向我们的应用程序中添加一个任务。
我们先来看看
simpleBLEPeripheral.c
是如何添加任务的。
我们打开
OSAL_SimpleBLEPeripheral.c
文件。这里我们可以找到一个很重要的数组
tasksArr
和一个同样很重要的函数
osalInitTasks
。
TaskArr
这个数组里
存放了所有任务的事件处理函数的地址
,在这里事件处理函数就代表了任务本身,
也就是说事件处理函数标识了与其对应的任务。
osalInitTasks
是
OSAL
的任务初始化函数,
所有任务的初始化工作都在这里面完成,并且自动给每个任务分配一个
ID
。
要添加新任务,我们需要编写新任务的事件处理函数和初始化函数,然后将事件处理函数的地址加入此数组。然后在
osalInitTasks
中调用此任务的初始化函数。
在此例中,我们此前提到过的
SimpleBLEPeripheral_ProcessEvent
这个函数被添加到了数组的末尾,
SimpleBLEPeripheral_Init
这个函数在
osalInitTasks
中被调用。
值得注意的是,
TaskArr
数组里各任务函数的排列顺序要与
osalInitTasks
函数中调用各任务初始化函数的顺序必须一致,只有这样才能够保证每个任务能够通过初始化函数接收到正确的任务
ID
。
另外,为了保存任务初始化函数所接收的任务
ID
,我们需要给每一个任务
定义一个全局变量来保存这个
ID
。在
SimpleBLEPeripheral
中
SimpleBLEPeripheral.c
中定义了一个全局变量
SimpleBLEPeripheral_TaskID;
并且在
SimpleBLEPeripheral_Init
函数中进行了赋值
{
SimpleBLEPeripheral_TaskID = task_id;
}
这条语句将分配给
SimpleBLEPeripheral
的任务
ID
保存了下来。
到此,我们就给应用程序中完整的添加了一个任务。
我们回到
OSAL
如何将事件分配给任务这个问题上来
在
OSAL_SimpleBLEPeripheral.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 )
是
2
个字节,即一个任务的长度(任务函数同样是
uint16
定义),乘以任务数量
tasksCnt
,即全部内存空间
*/
我们可以看出所有任务的状态都被初始化为
0
。代表了当前任务没有需要响应的事件。
紧接着,我们来到了
main()
函数。此
SimpleBLEPeripheral_Main.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
就将需要响应的事件传递给了对应的任务处理函数进行处理。
附:详解
events = (tasksArr[idx])( idx, events )
;
(tasksArr[idx])( idx, events )
是一个函数指针数组。那么什么是函数指针数组呢?顾名思义,函数指针数组是一个数组,数组中存放的元素类型是函数的指针。表达式举例:
char
(
*p[]
)
(int i) ;
对于这个表达式我们从语法上解释为,
p
是一个数组变量名,数组变量类型是
char
(
*
)(
int i
),存放元素的类型是:
char
(
int i
)函数的指针。
tasksArr[idx]
就是一个函数指针数组,里面存储的就是函数的指针。
const pTaskEventHandlerFn tasksArr[] =
{
LL_ProcessEvent, // task 0
Hal_ProcessEvent, // task 1
HCI_ProcessEvent, // task 2
#if defined ( OSAL_CBTIMER_NUM_TASKS )
OSAL_CBTIMER_PROCESS_EVENT( osal_CbTimerProcessEvent ), // task 3
#endif
L2CAP_ProcessEvent, // task 4
GAP_ProcessEvent, // task 5
GATT_ProcessEvent, // task 6
SM_ProcessEvent, // task 7
GAPRole_ProcessEvent, // task 8
GAPBondMgr_ProcessEvent, // task 9
GATTServApp_ProcessEvent, // task 10
SimpleBLEPeripheral_ProcessEvent // task 11
};
假设
idx=11;
那么
events = (tasksArr[11])( 11, events );
也就是调用了
SimpleBLEPeripheral_ProcessEvent
这个函数,其中传入的参数就是(
11
,
events
)
也就是调用了
events = (tasksArr[11])( 11, events )
其实就是执行了
SimpleBLEPeripheral_ProcessEvent(11,events);
四、事件的捕获
不过接下来就有了更加深入的问题了,事件是如何被捕获的?直观一些来说就是,
tasksEvents
这个数组里的元素是什么时候被设定为非零数,来表示有事件需要处理的?为了详细的说明这个过程,我将以
SimpleBLEPeripheral
这个例程中响应按键的过程来进行说明。其他的事件虽然稍有差别,却是大同小异。
按键在我们的应用里面应该属于硬件资源,所以
OSAL
理应为我们提供使用和管理这些硬件的服务。稍微留意一下我们之前说过的
tasksArr
这样一个数组,它保存了所有任务的事件处理函数。我们从中发现了一个很重要的信息:
Hal_ProcessEvent
。
HAL
(
Hardware Abstraction Layer
)翻译为
“
硬件抽象层
”
。许多人在这里经常把将
BLE
的硬件抽象层与物理层混为一谈。在这里,我们应该将
BLE
的硬件抽象层与物理层区分开来。
硬件抽象层所包含的范围是我们当前硬件电路上面所有对于系统可用的设备资源。而物理层则是针对无线通信而言,它所包含的仅限于支持无线通讯的硬件设备。
通过这个重要的信息,我们可以得出这样一个结论:
OSAL
将硬件的管理也作为一个任务来处理
。
那么我们很自然的去寻找
Hal_ProcessEvent
这个事件处理函数,看看它究竟是如何管理硬件资源的。
在
“HAL\Commn\ 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 )
这个函数。此函数在
“OnBoard .c”
文件中可以找到。在这个函数中,又调用了
void OnBoard_KeyCallback ( uint8 keys, uint8 state )
在这个函数中,按键的状态信息被封装到了一个消息结构体中(对于消息,我们稍后再说)。最后有一个极其重要的函数被调用了。
osal_msg_send( registeredKeysTaskID, (uint8 *)msgPtr );
与前面的
pHalKeyProcessFunction
相同,我先直接告诉大家
registeredKeysTaskID
所指示的任务正是我们需要响应按键的
SimpleBLEPeripheral
这个任务。
那么也就是说,在这里我们向
SimpleBLEPeripheral
发送了一个附带按键信息的消息。在
osal_msg_send
函数中
osal_set_event( destination_task, SYS_EVENT_MSG );
被调用,它在这里的作用是设
置
destination_task
这个任务的事件为
SYS_EVENT_MSG
。
而这个
destination_task
正式由
osal_msg_send
这个函数通过参数传递而来的,它也指示的是
SimpleBLEPeripheral
这个任务。在
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
函数向
SimpleBLEPeripheral
任务发送一个按键消息,并且设置
tasksEvents
中
SimpleBLEPeripheral
所对应的值为非零。如此,当
main
函数里这样一段代码
{
do
{
if (tasksEvents[idx])
{
break;
}
} while (++idx < tasksCnt);
}
执行了以后,
SimpleBLEPeripheral
这个任务就会被挑选出来。然后通过
events = (tasksArr[idx])( idx, events )
;
这个函数调用其事件处理函数,完成事件的响应。
现在,我们回过头来处理我们之前遗留下来的问题。
第一、
pHalKeyProcessFunction
这个函数指针为何指向了
OnBoard_KeyCallback
函数。
在
HAL\Common\ 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
为什么标识了
SimpleBLEPeripheral
这个任务?
由于
OSAL
是一个支持多任务的调度机制,所以在同一时间内将会有多个任务同时运行。但是从逻辑上来讲,一个事件只能由一个任务来处理。按键事件也不例外。
那么如何向
OSAL
声明处理按键事件的任务是
SimpleBLEPeripheral
呢?
在
SimpleBLEPeripheral_Init
(
SimpleBLEPeripheral
的任务初始化函数)中有这么一个语句:
{
RegisterForKeys( SimpleBLEPeripheral_TaskID );
}
RegisterForKeys
函数向
OSAL
声明按键事件将由
SimpleBLEPeripheral
任务来处理。在
RegisterForKeys
函数中:
{
registeredKeysTaskID = task_id;
}
我想我不用再做多余的解释了,聪明的您肯定可以理解。
五、消息队列
首先我需要向大家解释清楚消息与事件的联系。
事件是驱动任务去执行某些操作的条件,当系统产生了一个事件,将这个传递给相应的任务后,任务才能执行一个相应的操作。但是某些事件在它发生的同时,又伴随着一些附加信息的产生。任务的事件处理函数在处理这个事件的时候,还需要参考其附加信息。
最典型的一类便是按键消息,它同时产生了一个哪个按键被按下了附加信息。所以在
OnBoard_SendKeys
这个函数中,不仅向
SimpleBLEPeripheral
发送了事件,还通过调用
osal_msg_send
函数向
SimpleBLEPeripheral
发送了一个消息,这个消息记录了这个事件的附加信息。在
SimpleBLEPeripheral_ProcessEvent
中,通过
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SimpleBLEPeripheral_TaskID );
}
获取了这样一个消息,然后再进一步处理。
OSAL
在后台维护了一个消息队列,每一个消息都会被放到这个消息队列中去,当任务接收到事件以后,从消息队列中获取属于自己的消息,然后进行处理。
以上就是我就将
OSAL
这样一个事件驱动的多任务的资源分配机制做了一个简明扼要的介绍,希望对大家有所帮助。
作者:
micemik
时间:
2016-1-22 11:13
言简意赅,简明扼要
作者:
userming
时间:
2016-4-28 13:08
感谢lz的文章,写的挺好
欢迎光临 (http://www.51hei.com/bbs/)
Powered by Discuz! X3.1