一、OSAL基础概念
蓝牙4.0 BLE协议栈的功能依靠函数实现,为更有效地管理这些函数库,引入了操作系统抽象层(OSAL)。BLE协议栈、profiles以及所有应用程序都构建在OSAL之上。对于非计算机专业的读者而言,尽管他们对操作系统知识的了解可能有限,但蓝牙4.0 BLE协议内嵌入的OSAL较为简单,通过几个小实验就能快速掌握其工作原理。
(一)OSAL常用术语
- 资源相关概念
- 资源:任何被任务占用的实体,像变量、数组、结构体等,都可称为资源。
- 共享资源:能被至少两个任务使用的资源即共享资源。为防止其被破坏,任务操作时需确保独占。
- 任务与多任务运行
- 任务:任务类似于线程,是程序的执行过程。在任务执行期间,可认为CPU完全归其所用。设计任务时,应将复杂问题拆解为多个独立任务,赋予它们优先级,并为每个任务配备独立的CPU寄存器和堆栈空间,通常将任务设计成无限循环。
- 多任务运行:实际上,同一时间点只有一个任务在运行。但CPU通过任务调度策略,让多个任务轮流执行,每个任务执行特定时长(如10ms),由于切换频繁,造成了多任务同时运行的假象。
- 内核与互斥机制
- 内核:在多任务系统里,内核负责管理任务,涵盖为任务分配CPU时间、进行任务调度以及处理任务间通信等工作,其基本服务是任务切换。借助内核的任务切换功能,能简化应用系统的程序设计。
- 互斥:多任务访问通信资源常用共享数据结构,如单片机系统中的全局变量、指针、缓冲区等。但要保证对共享数据结构的写操作唯一,避免数据不同步。保护共享资源常用方法有关中断、使用测试并置位指令(TAS指令)、禁止任务切换和使用信号量,在蓝牙4.0 BLE协议栈内嵌操作系统中,常采用关中断的方式。
- 消息队列:消息队列用于任务间传递消息,通常包含任务同步信息。为降低通信开销,一般传递消息指针。任务或中断服务程序将消息放入队列,其他任务可从中获取属于自己的消息。
(二)OSAL的功能
OSAL主要具备以下功能:任务注册、初始化和启动;实现任务间的同步与互斥;处理中断;进行存储器分配和管理;提供定时器功能 。
二、OSAL运行机理
(一)工作原理核心
在蓝牙4.0 BLE协议栈应用程序开发中,开发者只需专注于应用层程序开发。因一个应用程序可视为一个任务,所以需要OSAL来实现任务的切换、同步与互斥。OSAL是一种支持多任务运行的系统资源分配机制,但它与标准操作系统存在差异,虽实现了部分类似功能,却并非真正意义上的操作系统。
OSAL通过建立事件表和函数表来调度任务。事件表记录各任务对应的事件,函数表存储各任务事件处理函数的地址。当有事件发生时,OSAL通过tasksEvents
指针访问事件表,找到对应事件后,调用tasksArr
数组中相应的函数指针,执行事件处理函数。处理完后,继续查询事件表,查看是否有新事件,这是一种基于事件驱动的轮询式操作系统。
(二)关键变量解析
taskID
:用于保存任务的总个数,定义为uint8 taskID;
,其中uint8
是typedef unsigned char uint8
。tasksEvents
:是一个指针,指向事件表的首地址,定义为uint16 *tasksEvents;
,uint16
即typedef unsigned short uint16
。tasksArr
:这是一个数组,数组中的每一项都是函数指针,类型为pTaskEventHandlerFn
,定义为typedef unsigned short (*pTaskEventHandlerFn) ( unsigned char task_id, unsigned short events )
。每个函数指针都指向任务的事件处理函数。
(三)核心函数剖析
osal_start_system()
函数是蓝牙4.0 BLE协议栈的关键:
void osal_start_system( void )
{
#if!defined ( ZBIT ) &&!defined ( UNIT_TEST )
for(;;)
#endif
{
uint8 idx = 0;
osalTimeUpdate();
Hal_ProcessPoll();
do {
if (tasksEvents[idx])
break;
} while (++idx < tasksCnt);
if (idx < tasksCnt)
{
uint16 events;
halIntState_t intState;
HAL_ENTER_CRITICAL_SECTION(intState);
events = tasksEvents[idx];
tasksEvents[idx] = 0;
HAL_EXIT_CRITICAL_SECTION(intState);
events = (tasksArr[idx])( idx, events );
HAL_ENTER_CRITICAL_SECTION(intState);
tasksEvents[idx] |= events;
HAL_EXIT_CRITICAL_SECTION(intState);
}
#if defined( POWER_SAVING )
else
{
osal_pwrmgr_powerconserve();
}
#endif
}
}
为便于分析,去除条件编译指令和相关宏后,简化函数如下:
void osal_start_system( void )
{
for(;;)
{
uint8 idx = 0;
osalTimeUpdate();
Hal_ProcessPoll();
do {
if (tasksEvents[idx])
break;
} while (++idx < tasksCnt);
if (idx < tasksCnt)
{
uint16 events;
events = tasksEvents[idx];
events -= (tasksArr[idx])( idx, events );
tasksEvents[idx] = events;
}
}
}
该函数不断循环执行:
- 定义变量
idx
,用于在事件表中索引。 osalTimeUpdate()
更新系统时间,Hal_ProcessPoll()
检查硬件是否有事件发生,如串口数据接收、按键按下等。- 通过
do - while
循环检查事件表,若tasksEvents[idx]
不为0,则跳出循环,表明有事件发生。 - 若有事件,读取该事件,调用对应的事件处理函数处理,处理后将未处理的事件重新放回事件表。
蓝牙4.0 BLE协议栈用unsigned short
型变量表示事件,其16个二进制位,每一位代表一个事件。系统初始化时,所有任务的事件都被初始化为0,通过检查tasksEvents[idx]
是否为0来判断有无事件发生。
以SimpleBLEPeripheral_ProcessEvent
函数为例:
uint16 SimpleBLEPeripheral_ProcessEvent( uint8 task_id, uint16 events )
{
if ( events & SYS_EVENT_MSG )
{
uint8 *pMsg;
if ( (pMsg = osal_msg_receive( simpleBLEPeripheral_TaskID )) != NULL )
{
SimpleBLEPeripheral_ProcessOSALMsg( (osal_event_hdr_t *)pMsg );
VOID osal_msg_deallocate( pMsg );
}
return (events ^ SYS_EVENT_MSG);
}
if ( events & SBP_START_DEVICE_EVT )
{
VOID GAPRoleStartDevice( &simpleBLEPeripheral_PeripheralCBs );
osal_start_timerEx( simpleBLEPeripheral_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );
return (events ^ SBP_START_DEVICE_EVT);
}
if ( events & SBP_PERIODIC_EVT )
{
if ( SBP_PERIODIC_EVT_PERIOD )
{
osal_start_timerEx( simpleBLEPeripheral_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );
performPeriodicTask();
}
return (events ^ SBP_PERIODIC_EVT);
}
#if defined ( PLUS_BROADCASTER )
if ( events & SBP_ADV_IN_CONNECTION_EVT )
{
uint8 turnAdv = TRUE;
GAPRole_SetParameter( GAPROLE_ADVERT_ENABLED, sizeof(uint8), &turnAdv );
return (events ^ SBP_ADV_IN_CONNECTION_EVT);
}
#endif
return 0;
}
该函数首先判断事件类型:
- 若为
SYS_EVENT_MSG
,从消息队列接收消息,处理后释放消息内存空间,并通过异或运算返回未处理的事件标志。 - 若为
SBP_START_DEVICE_EVT
,启动设备并设置定时器,同样通过异或运算返回未处理事件标志。 - 若为
SBP_PERIODIC_EVT
,根据条件设置定时器并执行周期性任务,再返回未处理事件标志。 - 若有连接状态广播事件(
SBP_ADV_IN_CONNECTION_EVT
,需PLUS_BROADCASTER
宏定义支持),设置设备广播参数后返回未处理事件标志。若都不满足,则返回0。
OSAL运行机理可总结为:持续查看事件表判断是否有事件发生,若有则查找函数表,调用对应事件处理函数;事件表以数组实现,数组每项对应一个任务的事件,每一位表示一个事件;函数表由函数指针数组构成,数组每项指向事件处理函数。
三、OSAL消息队列
(一)消息与事件的区别
事件是驱动任务执行操作的条件,系统产生事件后,OSAL将其传递给相应任务,任务调用事件处理函数进行处理。某些事件发生时会伴随附加信息,如天线接收数据,这些事件和数据会封装成消息放入消息队列。在事件处理函数中,可使用osal_msg_receive
从队列获取消息并处理数据。
pMsg = osal_msg_receive( simpleBLEPeripheral_TaskID );
(二)消息队列结构
OSAL维护着一个消息队列,每个消息都包含一个消息头osal_msg_hdr_t
和自定义消息。osal_msg_hdr_t
结构体定义如下:
typedef struct
{
void *next;
uint16 len;
uint8 dest_id;
} osal_msg_hdr_t;
任务接收到事件后,从消息队列获取属于自己的消息,调用消息处理函数进行处理。
四、OSAL添加新任务
在蓝牙4.0 BLE协议栈应用程序开发中,以SimpleBLEPeripheral
工程为例添加新任务:打开OSAL
中的SimpleBLEPeripheral.c
文件,可找到tasksArr[]
数组和osalInitTasks()
函数。tasksArr[]
存放所有任务的事件处理函数地址,osalInitTasks()
负责所有任务的初始化并分配ID号。
添加新任务需编写两个函数:新任务的事件处理函数和初始化函数。将新任务的初始化函数添加到osalInitTasks()
函数最后,同时将新任务的事件处理函数地址添加到tasksArr[]
数组中。
const pTaskEventHandlerFn tasksArr[] =
{
LL_ProcessEvent, // task 0
Hal_ProcessEvent, // task 1
HCI_ProcessEvent, // task 2
#if defined ( OSAL_CBTIMER_NUM_TASKS )
OsalCBTimer_ProcessEvent, // task 3
#endif
L2CAP_ProcessEvent, // task 4
GATT_ProcessEvent, // task 5
SM_ProcessEvent, // task 6
GAPRole_ProcessEvent, // task 7
GAPBondMgr_ProcessEvent, // task 8
GATTServApp_ProcessEvent,// task 9
GATTAttribute_ProcessEvent, // task 10
SimpleBLEPeripheral_ProcessEvent // task 11
};
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
Hal_Init( taskID++ );
HCI_Init( taskID++ );
#if defined ( OSAL_CBTIMER_NUM_TASKS )
osal_cbtimerInit( taskID++ );
taskCBs = OSAL_CBTIMER_TASK_CBS;
#endif
L2CAP_Init( taskID++ );
GATT_Init( taskID++ );
SM_Init( taskID++ );
GAPRole_Init( taskID++ );
GAPBondMgr_Init( taskID++ );
GATTServApp_Init( taskID++ );
SimpleBLEPeripheral_Init( taskID );
// 将SimpleBLEPeripheral_Init()函数添加到数组的末尾,将SimpleBLEPeripheral_ProcessEvent函数添加到osalInitTasks()函数初始化函数中。
}
添加新任务时要注意:tasksArr[]
数组中事件处理函数的排列顺序应与osalInitTasks()
函数中任务初始化函数的调用顺序一致,确保任务的事件处理函数能接收正确的任务ID;为保存osalInitTasks()
函数分配的任务ID,需为每个任务定义全局变量,如SimpleBLEPeripheral.c
中的SimpleBLEPeripheral_TaskID
,并在SimpleBLEPeripheral_Init
函数中赋值。
五、OSAL应用编程接口
蓝牙4.0 BLE协议栈内嵌的操作系统支持多任务运行,任务间同步、互斥等操作需要相应API支持。OSAL提供的API可使协议栈独立于特定操作系统、内核或任务环境进行编写。
OSAL提供了8个方面的API,包括消息管理、任务同步、时间管理、中断管理、任务管理、内存管理、电源管理和非易失性内存管理。这里选取部分典型API介绍:
(一)消息管理API
用于处理任务间消息交换:
osal_msg_allocate( uint16 len )
:函数原型为uint8 *osal_msg_allocate( uint16 len )
,功能是为消息分配缓存空间。osal_msg_deallocate( uint8 *msg_ptr )
:原型是void osal_msg_deallocate( uint8 *msg_ptr )
,用于释放消息的缓存空间。osal_msg_send( uint8 destination_task, uint8 *msg_ptr )
:函数原型uint8 osal_msg_send(uint8 destination_task, uint8 *msg_ptr )
,可将一个任务的消息发送到消息队列。osal_msg_receive( uint8 task_id )
:原型为uint8 *osal_msg_receive(uint8 task_id )
,用于一个任务从消息队列接收属于自己的消息。
(二)任务同步API
osal_set_event( )
:函数原型uint8 osal_set_event(uint8 task_id, uint16 event_flag )
,用于运行一个任务设置某一事件,实现任务间的同步。
(三)时间管理API
用于开启和关闭定时器:
osal_start_timerEx( )
:函数原型uint8 osal_start_timerEx( uint8 task_id, uint16 event_id, uint16 timeout_value )
,设置定时时间,时间到后相应事件被设置。osal_stop_timerEx( )
:原型uint8 osal_stop_timerEx( uint8 task_id, uint16 event_id )
,用于停止已经启动的定时器。
(四)中断管理API
主要用于控制中断的开启与关闭,一般较少使用,如需使用可参考相关文档。
(五)任务管理API
对OSAL进行初始化和启动:
osal_init_system( )
:函数原型uint8 osal_init_system( void )
,是第一个被调用的OSAL函数,用于初始化OSAL。osal_start_system( )
:原型void osal_start_system( void )
,启动所有任务事件,有事件发生时调用相应处理函数,处理后继续检测事件。若开启节能模式,无事件时使处理器进入休眠模式以降低功耗。
(六)内存管理API
用于在堆上分配和释放缓冲区:
osal_mem_alloc( uint16 size )
:函数原型void *osal_mem_alloc( uint16 size )
,在堆上分配指定大小的缓冲区。osal_mem_free( void *ptr )
:原型void osal_mem_free( void *ptr )
,释放osal_mem_alloc()
分配的缓冲区,这两个函数需成对使用,防止内存泄露。
(七)电源管理API
主要用于电池供电的蓝牙4.0 BLE设备节点,这里暂不讨论。