OSAL运行原理
蓝牙协议栈PROFILE、所有的应用程序、驱动等都是围绕着OSAL组织运行的。OSAL(Operating System Abstraction Layer)操作系统抽象层,它不是一个真正的操作系统(它没有 Context Switch 上下文切换功能),但它巧妙地组织各任务,支持任务优先级,任务之间可以通过事件和消息来通信,为任务提供软定时器和动态内存分配。要避免的陷阱是,应用任务的单个函数运行时间不能太长 (如操作大批量数据的 Flash 写),否则它无法及时调度高优先级的 LL(Link Layer)任务而导致蓝牙通信中断。
OSAL 为每一个任务函数分配了一个 16 位的EVENT 事件,每一位代表一个事件,其中最高位代表的事件为 SYS_EVENT_MSG,这个事件被 OSAL 系统保留,其他的 15 位可以由用户定义,OSAL 在主循环里运行每次都会检查每个任务函数的是否有事件发生(事件置位),如果有事件发生,将通过 taskid 来调用发生事件的任务函数,并将发生的事件传递到该函数中去,由任务函数处理对应的事件。
为什么要将返回值设置到tasksEvents中,就是因为其本质是一个单任务循环,如果某个子任务时间执行过长,会影响更高优先级的任务的响应变慢,影响整体性能。因此如果一个任务执行比较长,宜进行分割,在返回值那里设置继续处理事件,然后返回大循环,看看有没有更高优先级的任务需要执行。
事件和任务对应关系
事件和任务的事件处理函数是如何关联起来的呢?
建立一个事件表,保存各个任务对应的事件。【uint16 *tasksEvents;】。tasksEvents为指向一个内存分配的事件数组的指针。比如:tasksEvents[0]为第0个任务的事件变量(short int)。
建立另一个函数表,保存各个任务事件处理函数的地址。【const pTaskEventHandlerFn tasksArr[] =】将会根据tasksArr[x]去执行对应的回调函数。
然后将这两张表建立某种对应关系,【void osalInitTasks( void ),初始化task_id和tasksArr[]的函数指针对应关系】【uint8 osal_set_event( uint8 task_id, uint16 event_flag ),设置事件发生】,蓝牙协议栈也会调用并设置任务,具体的实现已经被封装起来了。
当某一事件发生时则查找函数表找到对应的事件处理函数即可。【events = (tasksArr[idx])( idx, events );,调用idx变好的函数指针回调函数】
//定义了一个函数指针
/** Event handler function prototype*/
typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );//这是一个数组,该数组的每一项都是一个函数指针,指向了事件处理函数
// The order in this table must be identical to the task initialization calls below in osalInitTask.
const pTaskEventHandlerFn tasksArr[] =
{LL_ProcessEvent, // task 0Hal_ProcessEvent, // task 1HCI_ProcessEvent, // task 2
#if defined ( OSAL_CBTIMER_NUM_TASKS )OSAL_CBTIMER_PROCESS_EVENT( osal_CbTimerProcessEvent ), // task 3
#endifL2CAP_ProcessEvent, // task 4GAP_ProcessEvent, // task 5GATT_ProcessEvent, // task 6SM_ProcessEvent, // task 7GAPRole_ProcessEvent, // task 8GAPBondMgr_ProcessEvent, // task 9GATTServApp_ProcessEvent, // task 10SimpleBLEPeripheral_ProcessEvent // task 11
};//该变量保存了任务的总个数
const uint8 tasksCnt = sizeof( tasksArr ) / sizeof( tasksArr[0] );//这是一个指针,指向了事件表的首地址,事件表实际上是一个内存分配的数组
uint16 *tasksEvents;
OSAL是一种事件驱动的轮询式操作系统。事件驱动是指发生事件后采取相应的事件处理方法,轮询指的是不断地查询是否有事件发生。
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
在系统初始化时,将所有任务的事件初始化为0。通过taskEvents[idx]是否为0来判断是否有事件发生【if (tasksEvents[idx])】。如果有事件发生,则查找函数对应的事件处理函数对事件进行处理【events = (tasksArr[idx])( idx, events );】。
事件表使用数组来实现,数组的每一项对应任务的事件,每一位表示一个事件;函数表使用函数指针数据来实现,数组的每一项是一个函数指针,指向了事件处理函数。
OSAL提供的API
总体而言,大致可以分为10个方面:
1.消息管理
2.任务同步
3.时间管理
4.中断管理
5.任务管理
6.内存管理
7.电源管理
8.非易失性闪存管理
9.时钟管理
10.其他常用
事件
OSAL为每个任务函数分配了一个16位的事件变量,每一位代表一个事件。最高位0x8000保留为系统事件SYS_ENENT_MSG。其余的15位留给用户自定义需要的事件。通常事件由定时器启动,比如2s后我要点亮LED1,这就需要发送一个点亮LED1的事件,然后等待,当2s后接收到点亮LED1事件的时候调用HAL层开关LED1的函数开启LED1。
消息
MAG是比EVENT事件更具体并且可以携带数据的一种通信方式。而且MSG的标记是按数值,而不是按位。比如0x02和0x03是两个不同的消息,但是对于事件0x03则是0x01和0x02事件的组合。MSG收发使用osal_mag_send()和osal_msg_receive();当调用osal_msg_send()发送一个MSG的同时会在EVENT列表中触发一个message ready event。为了降低消息传递的开支,通常传递指向消息的指针。
消息与事件的区别
讲解消息队列之前需要讲解一下消息与事件的区别。
事件是驱动任务去执行某些操作的条件,当系统中产生了一个事件,OSAL 将这个事件传递给相应的任务后,任务才能执行一个相应的操作(调用事件处理函数去处理)。
通常某些事件发生时,又伴随着一些附加信息的产生,例如:主机 GATT 接收到数据后,会产生 GATT_MSG_EVENT 消息,但是任务的事件处理函数在处理这个事件的时候,还需要得到所收到的数据。
因此,这就需要将事件和数据封装成一个消息,将消息发送到消息队列osal_msg_send,然后在事件处理函数中就可以使用 osal_msg_receive 从消息队列中得到该消息。这里需要说明一点,消息一般用于不同任务函数之间的数据传递,因为不同的任务具有各自的堆栈空间。在同一任务中使用全局函数或者用户事件完全能够胜任。当然消息也可以在同一个任务中传递数据。
EVENT用于同一任务函数传递命令,而MSG则用于不同的任务函数传递命令数据。
这里需要说明一点,消息一般用于不同任务函数之间的数据传递,因为不同的任务具有各自的堆栈空间。在同一任务中使用全局函数或者用户事件完全能够胜任。当然消息也可以在同一个任务中传递数据。
事件处理
消息处理
OSAL 维护了一个消息队列,每一个消息都会被放到这个消息队列中去,当任务接收到事件后,可以从消息队列中获取属于自己的消息,然后调用消息处理函数进行相应的处理即可。
Osal_msg_allocate()为消息分配缓存空间,分配之后,可以填充消息,然后通过osal_msg_send( ) 将消息发送出去,然后任务函数中通过 osal_msg_reveive()函数接收属于 自己的消息,并处理,最后调用osal_msg_deallocate() 函数销毁由Osal_msg_allocate()分配的内存空间。
下面是按键的消息处理过程,按键的消息最终将会发送到第一次注册的任务中去。
这里需要说明一点,消息一般用于不同任务函数之间的数据传递,因为不同的任务具有各自的堆栈空间。在同一任务中使用全局函数或者用户事件完全能够胜任。当然消息也可以在同一个任务中传递数据。
事实上,它只是发了一个KEY_CHANGE事件,而键值是以MSG消息的形式发到系统的消息队列,而该消息也会带上目标taskId的标识。
如下代码可以从消息队列中得到一个消息:pMsg = osal_msg_receive( registeredKeysTaskID))
时间管理
时间管理API用于开启和关闭定时器,定时时间一般为毫秒级定时。
osal_start_reload_timer()和osal_start_timerEx()功能一样,但是本接口还多了一个功能:就是定时时间到后相应事件被执行,并重新加载定时器,也就是又重新设置了定时器,继续进行定时工作,除非调“osal_stop_timerEx()”接口,否则一直循环定时操作。