测试环境
协议栈版本:BLE-CC254x-1.4.0
开发环境IAR版本:IAR 8.20
硬件设备:CC2540/CC2541开发板
示例测试Demo工程:simpleBLEPeripheral工程
系统入口
CC2540/CC2541系统执行的入口点就是“Projects\ble\SimpleBLEPeripheral\Source”目录下的“SimpleBLEPeripheral_Main.c”文件的“int main(void)”方法。该入口方法的源码如下:
/*************************************************************************************************** @fnmain** @brief Start of application.** @param none** @returnnone***************************************************************************************************/int main(void){/* Initialize hardware */HAL_BOARD_INIT();// Initialize board I/OInitBoard( OB_COLD );/* Initialze the HAL driver */HalDriverInit();/* Initialize NV system */osal_snv_init();/* Initialize LL *//* Initialize the operating system */osal_init_system();/* Enable interrupts */HAL_ENABLE_INTERRUPTS();// Final board initializationInitBoard( OB_READY );#if defined ( POWER_SAVING )osal_pwrmgr_device( PWRMGR_BATTERY );#endif/* Start OSAL */osal_start_system(); // No Return from herereturn 0;}
在该入口函数中调用了很多其他文件中的函数,我们重点来看“osal_start_system();”函数,在此函数之前的那些函数都是对板载硬件以及协议栈进行的初始化。调用“osal_start_system();”函数之后,整个协议栈系统就算是真正运行起来了。这个函数启动的是TI封装的OSAL操作系统抽象层,下面来具体了解一下。
操作系统抽象层简介
TI的蓝牙4.0BLE协议栈包含了蓝牙4.0BLE协议所规定的基本功能,这些功能都是以函数的形式实现的,为了便于管理这些函数集,TI 蓝牙4.0协议栈内加入了一个小的操作系统,称为OSAL(操作系统抽象层)。BLE协议栈、profiles以及所有应用程序都是建立在OSAL基础上的。但是,大家要注意的是OSAL只是TI在CC254x等系列芯片上植入的类似操作系统的系统,并不是真正意义上的操作系统。
接下来,我们先来了解下操作系统有关部分的基础知识,便于我们后面对OSAL的了解:
1.资源(Resource)
任何任务所占用的实体都可以称为资源,如一个变量、数组、结构体等。
2.共享资源(Shared Resource)
至少可以被两个任务使用的资源称为共享资源,为了防止共享被破坏,每个任务在操作共享资源时,必须保证是独占该资源。
3.任务(Task)
一个任务,又称作一个线程,是一个简单的程序的执行过程,在任务执行过程中,可以认为CPU完全属于该任务。在任务设计时,需要将问题尽可能地分多个任务,每个任务独立完成某种功能,同时被赋予一定的优先级,拥有自己的CPU寄存器和堆栈空间。一般将任务设计为一个无限循环。
4.多任务运行(Muti-task Running)
实际上,一个时间点只有一个任务在运行,但是CPU可以使用任务调度策略将多个任务进行调度,每个任务执行特定的时间,时间片到了以后,就进行任务切换,由于每个任务执行时间很短,例如:10ms,因此,任务切换很频繁,这就造成了多任务同时运行的“假象”。
5.内核(Kernel)
在多任务系统中,内核负责管理各个任务,主要包括:为每个任务分配CPU时间;任务调度;负责任务间的通信。内核提供的基础的内核服务是任务切换。使用内核可以大大简化应用系统的程序设计方法,借助内核提供的任务切换功能,可以将应用程序分为不同的任务来实现。
6.互斥(Mutual Exclusion)
多任务间通信最简单、最常用的方法是使用共享数据结构。对于单片机系统,所有任务都在单一的地址空间下,使用共享的数据结构包括全局变量、指针、缓冲区等。虽然共享数据结构的方法简单,但是必须保证对共享数据结构的写操作具有唯一性,以避免晶振和数据不同步。
保护共享资源最常用的方法是:
(1)关中断
(2)使用测试并置位指令(T&S指令)
(3)禁止任务切换
(4)使用信号量
其中,在蓝牙4.0BLE协议栈内嵌操作系统OSAL中,经常使用的方法是关中断。
7.消息队列(Message Queue)
消息队列用于任务间传递消息,通常包含任务间同步的信息。通过内核提供的服务、任务或者中断服务程序将一条消息放入消息队列,然后,其他任务可以使用内核提供的服务从消息队列中获取属于自己的消息。为了降低传递消息的开支,通常传递指向消息的指针。
在蓝牙4.0BLE协议栈中,OSAL主要提供如下功能:
(1)任务注册、初始化和启动
(2)任务间的同步、互斥
(3)中断处理
(4)存储器分配和管理
(5)提供定时器功能
OSAL运行机理
OSAL就是一种支持多任务运行的系统资源分配机制。
OSAL与标准的操作系统还是有一定区别的,OSAL实现了类似操作系统的某些功能,例如:任务切换、提供了内存管理功能等,但OSAL并不能称之为真正意义上的操作系统。
OSAL负责调度各个任务的运行,如果有事件发生,则会调用相应的事件处理函数进行处理,OSAL的工作原理示意图如下:
TI官网OSAL工作原理流程图如下:
下面是我根据simpleBLEPeripheral工程为例简单画的流程图,大家可以大体看一下:
那么,事件和任务的事件处理函数是如何联系起来的呢?
TI的OSAL采用的方法是:建立一个事件表,保存各个任务对应的事件,建立另一个函数表,保存各个任务事件处理函数的地址,然后将这两张表建立某种对应关系,当某一事件发生时则查找函数表找到对应的事件处理函数即可。
现在问题转变为:用什么样的数据结构来实现事件表和函数表呢?如何将事件表和函数表建立对应关系呢?在回答这两个问题之前,我们先来了解一下在TI的OSAL中至关重要的三个变量:
我们还是以“SimpleBLEPeripheral”工程为例,涉及到我们接下来要说到的三个变量在“Projects\ble\SimpleBLEPeripheral\Source”目录下的“OSAL_SimpleBLEPeripheral.c”文件中,相应源码如下:
// 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;
详细介绍如下:
1.tasksCnt:该变量保存了任务的总个数。
该变量的声明为:uint8类型,该类型的定义为:
typedef unsigned char uint8;
2.tasksEvents:这是一个指针,指向了事件表的首地址。
该变量的声明为:uint16 *类型,其中uint16的定义为:
typedef unsigned short uint16;
3.tasksArr:这是一个数组,该数组的每一项都是一个函数指针,指向事件处理函数。
该数组的声明为:pTaskEventHandlerFn类型,该类型的定义为(特别注意):
typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );
上述就是函数指针的定义方式。
因此,tasksArr数组的每一项都是一个函数指针,指向事件处理函数。
事件表和函数表的关系如下图所示:
下面,我们来总结一下OSAL的工作原理:通过taskEvents指针访问事件表的每一项,如果有事件发生,则查找函数表找到事件处理函数进行处理,处理完后,继续访问事件表,查看是否有事件发生,无限循环。
从这种意义上说,OSAL是一种基于事件驱动的轮询式操作系统。事件驱动是指发生事件后采取相应的事件处理方法,轮询指的是不断地查询是否有事件发生。
前文说到了,在“main()”函数中,直接调用了“osal_start_system()”函数时,整个TI的蓝牙4.0BLE协议栈才算是真正地运行起来了,下面我们深入到“osal_start_system()”函数的内部去探究协议栈是如何被调动起来的。
osal_start_system()函数原型如下:
void osal_start_system( void ){#if !defined ( ZBIT ) && !defined ( UBIT )for(;;) // Forever Loop#endif{osal_run_system();}}
从上述源码中,我们看到了用到“for(;;)”,也就是无限循环,无限的执行“osal_run_system()”函数,下面我们继续看看“osal_run_system()”函数的原型如下:
/********************************************************************** @fnosal_run_system** @brief** This function will make one pass through the OSAL taskEvents table* and call the task_event_processor() function for the first task that* is found with at least one event pending. If there are no pending* events (all tasks), this function puts the processor into Sleep.** @param void** @return none*/void osal_run_system( void ){uint8 idx = 0;#ifndef HAL_BOARD_CC2538osalTimeUpdate();#endifHal_ProcessPoll();do {if (tasksEvents[idx]) // Task is highest priority that is ready.{break;}} while (++idx < tasksCnt);if (idx < tasksCnt){uint16 events;halIntState_t intState;HAL_ENTER_CRITICAL_SECTION(intState);events = tasksEvents[idx];tasksEvents[idx] = 0; // Clear the Events for this task.HAL_EXIT_CRITICAL_SECTION(intState);activeTaskID = idx;events = (tasksArr[idx])( idx, events );activeTaskID = TASK_NO_TASK;HAL_ENTER_CRITICAL_SECTION(intState);tasksEvents[idx] |= events; // Add back unprocessed events to the current task.HAL_EXIT_CRITICAL_SECTION(intState);}#if defined( POWER_SAVING )else // Complete pass through all task events with no activity?{osal_pwrmgr_powerconserve(); // Put the processor/system into sleep}#endif/* Yield in case cooperative scheduling is being used. */#if defined (configUSE_PREEMPTION) && (configUSE_PREEMPTION == 0){osal_task_yield();}#endif}
函数前面的注释大家可以看一下,大致内容是:这个函数的功能是查询事件表,如果有事件发生,就查找函数表,找到相应的事件处理函数进行处理;如果没有事件发生,系统将进入休眠模式,降低功耗,当然要进入休眠模式,还需要打开相应的宏开关进而开启低功耗功能,这个后面再详细介绍。
为了清楚的了解“osal_run_system()”函数的工作原理,我们先将条件编译指令去掉,另外,前面说到过,在访问共享变量时需要保证该变量不被其他任务同时访问,因此,这里采用的是关中断的方法,如下代码块就是OSAL中一种典型的使用方法:
HAL_ENTER_CRITICAL_SECTION(intState);//关中断....//共享变量HAL_EXIT_CRITICAL_SECTION(intState);//开中断
使用“HAL_ENTER_CRITICAL_SECTION(intState);”关中断,访问完共享变量后,使用“HAL_EXIT_CRITICAL_SECTION(intState);”开中断。
为了方便分析问题,将编译宏去掉之后经过简化的“osal_run_system()”函数源码如下(简化是为了便于分析,同时工作原理没有变化):
void osal_run_system( void ){uint8 idx = 0;osalTimeUpdate();Hal_ProcessPoll();do {if (tasksEvents[idx]) // Task is highest priority that is ready.{break;}} while (++idx < tasksCnt);if (idx < tasksCnt){uint16 events;events = tasksEvents[idx];tasksEvents[idx] = 0; // Clear the Events for this task.events = (tasksArr[idx])( idx, events );tasksEvents[idx] |= events; // Add back unprocessed events to the current task.}}
这个函数是TI实现的蓝牙4.0BLE协议栈的灵魂,实现的功能就是:不断的查看事件表,如果有事件发生就调用相应的事件处理函数。
该函数被一个for循环调用,无限循环,不断遍历。
第3行,定义了一个变量idx,用来在事件表中索引。
第4行,更新系统时钟。
第5行,查看硬件方面是否有事件发生,比如:串口是否收到数据、是否有按键按下等。
第7~12行,用do-while循环查看事件表是否有事件发生。
这里需要注意一点,如何表示一个事件呢?
蓝牙4.0BLE协议栈使用一个“unsigned short”型的变量,“unsigned short”类型占2个字节,即16个二进制位,因此,可以使用每个二进制位表示一个事件,比如如下定义方式:
在系统初始化时,将所有任务的时间初始化为0,因此,第8行通过 taskEvents[idx] 是否为 0 来判断是否有事件发生,如果有事件发生了,则跳出循环。
第16~17行,读取该事件。
第18行,将事件表中该项事件标志清零,注意有可能几个事件同时发生,这里清零是暂时的。
第19行,调用事件处理函数去处理,为什么是调用事件处理函数呢?还记得 tasksArr[] 数组中每个元素的类型吗?函数指针!这就是函数指针的典型应用。执行完事件处理函数之后,需要将未处理的事件返回,即事件处理函数的返回值保存了未处理的事件,需要将未处理的事件再次存放到事件表中,以便于下次进行处理。
第20行,将未处理的事件重新存放到事件表。
现在的问题是:如何在事件处理函数中返回未处理的事件呢?下面结合“SimpleBLEPeripheral_ProcessEvent”事件处理函数来讲解。该函数原型如下:
uint16 SimpleBLEPeripheral_ProcessEvent( uint8 task_id, uint16 events ){VOID task_id; // OSAL required parameter that isn't used in this functionif ( events & SYS_EVENT_MSG ){uint8 *pMsg;if ( (pMsg = osal_msg_receive( simpleBLEPeripheral_TaskID )) != NULL ){simpleBLEPeripheral_ProcessOSALMsg( (osal_event_hdr_t *)pMsg );// Release the OSAL messageVOID osal_msg_deallocate( pMsg );}// return unprocessed eventsreturn (events ^ SYS_EVENT_MSG);}if ( events & SBP_START_DEVICE_EVT ){// Start the DeviceVOID GAPRole_StartDevice( &simpleBLEPeripheral_PeripheralCBs );// Start Bond ManagerVOID GAPBondMgr_Register( &simpleBLEPeripheral_BondMgrCBs );// Set timer for first periodic eventosal_start_timerEx( simpleBLEPeripheral_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );return ( events ^ SBP_START_DEVICE_EVT );}if ( events & SBP_PERIODIC_EVT ){// Restart timerif ( SBP_PERIODIC_EVT_PERIOD ){osal_start_timerEx(simpleBLEPeripheral_TaskID,SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );}// Perform periodic application taskperformPeriodicTask();return (events ^ SBP_PERIODIC_EVT);}// Discard unknown eventsreturn 0;}
代码解析:
第5行,判断是否有系统消息任务,如果有,则定义一个消息指针pMsg.
第9行,判断是否有消息队列接收到数据。
第11行,处理从消息队列接收到的任务信息。
第14行,释放消息的缓存空间。
第18行,返回未处理的任务标志。
第21行,判断是否有启动设备任务。
第24行,启动设备,括号内为设备状态改变时的回调函数。在回调函数中,可以自定义设备启动时想要显示的信息或要执行的操作。
第27行,启动绑定管理函数。在该函数中处理认证信息并在GATT服务应用程序中注册任务信息。
第29行,设置一个定时时间,当定时时间到后,周期事件的任务ID被置起。
第31行,返回未处理的任务标志。
第34行,判断是否有周期事件。
第37~43行,如果有周期事件任务,设置周期事件的定时时间。当时间到达时,执行周期事件中需要处理的工作,具体的任务工作可以根据需要自定义。最后返回未处理的任务标志。
第49行,将未知的任务事件标志清零并返回。
SimpleBLEPeripheral_ProcessEvent函数的基本实现方法是:使用“osal_msg_receive”函数从消息队列上接收一个消息(该消息中包含了事件以及接收到的数据),然后使用“simpleBLEPeripheral_ProcessOSALMsg”消息处理函数来处理接收到的消息,该函数的源码如下:
/********************************************************************** @fnsimpleBLEPeripheral_ProcessOSALMsg** @brief Process an incoming task message.** @param pMsg - message to process** @return none*/static void simpleBLEPeripheral_ProcessOSALMsg( osal_event_hdr_t *pMsg ){switch ( pMsg->event ){#if defined( CC2540_MINIDK )case KEY_CHANGE:simpleBLEPeripheral_HandleKeys( ((keyChange_t *)pMsg)->state, ((keyChange_t *)pMsg)->keys );break;#endif // #if defined( CC2540_MINIDK )default:// do nothingbreak;}}
使用switch-case语句判断消息事件类型,根据消息事件的类型,调用相应的消息事件处理函数。
注意“return (events ^ SBP_PERIODIC_EVT);”这种返回语句,使用了异或运行,可以通过下面的例子看到异或运行的作用。如下表中的事件:
假设同时发生了启动设备和周期事件这两个事件,则此时的 events=0b00000011,即0x03,假设现在处理完启动设备的事件,则应该将events第0位请零,如何实现呢?只需要使用异或运算即可。即 events^0x01=0x03^0x01=0x02,即0b00000010,可见使用异或运算恰好可以将处理完的事件清除,仅留下未处理的事件。
前文说到了可以使用每个二进制位表示一个事件,协议栈本身给出了几个已经定义好的事件,由协议栈定义的事件成为系统强制事件(Mandatory Events),SYS_EVENT_MSG就是其中的一个事件,SYS_EVENT_MSG的定义如下:
#define SYS_EVENT_MSG0x8000 // A message is waiting event
也就是说除了该系统定义的事件之外,我们还可以定义15个事件,这也就是有的资料中提到的,每个任务最多只能自定义15个事件的原因。
总结OSAL的运行机理:
1.通过不断的查询事件表来判断是否有事件发生,如果有事件发生,则查找函数表找到对应的事件处理函数对事件进行处理。
2.事件表使用数组来实现,数组的每一项对应一个任务的事件,每一位表示一个事件;函数表使用函数指针数组来实现,数组的每一项是一个函数指针,指向了相应的事件处理函数。
如果觉得《CC2540/CC2541/CC254x之OSAL操作系统抽象层》对你有帮助,请点赞、收藏,并留下你的观点哦!