本系列教程将结合TI推出的CC254x SoC 系列,讲解从环境的搭建到蓝牙4.0协议栈的开发来深入学习蓝牙4.0的开发过程。教程共分为六部分,本文为第三部分:
第三部分知识点:
第十一节 串口通信
第十二节 Flash的读写
第十三节 BLE协议栈简介
第十四节 OSAL工作原理
第十五节 BLE蓝牙4.0协议栈启动分析
第十一节 串口通信
在软件开发过程中调试是一个很关键的过程,而调试用的最多的手段就是打印Log,嵌入式平台很少有显示设备,所以我们需要将信息通过串口打印到PC端。
MT254xboard上已经通过RS232芯片将UART0连接到DB9,我们只需要将DB9连接到电脑即可,UART0 对应的外部设备 IO 引脚关系为:P0_2------RX,P0_3------TX。
我们需要将这两个IO配置为复用功能,CC2540的USART可以配置为SPI模式或者异步UART模式,这里我们需要配置为异步UART模式。
首先配置IO为UART模式:
PERCFG &= ~0x01; // 配置UART为位置 1
P0SEL = 0x3c; // P0_2,P0_3,P0_4,P0_5用作串口功能
P2DIR &= ~0XC0; // P0 优先作为UART0
配置UART0寄存器,将UART0配置为8N1模式,波特率为115200。
U0CSR |= 0x80; // UART 方式
U0GCR |= 11; // U0GCR与U0BAUD配合
U0BAUD |= 216; // 波特率设为115200
UTX0IF = 0; // 清除中断标志
U0CSR |= 0X40; // 允许接收
IEN0 |= 0x84; // 开总中断,接收中断
这里采用中断方式来接收串口数据,并在中断中回调应用层的接收处理函数。
#pragma vector = URX0_VECTOR
__interrupt void UART0_ISR(void)
{
uint8 ch;
URX0IF = 0; // 清中断标志
ch = U0DBUF;
if ( NULL != RecvCb ) // 调用回调函数
{
RecvCb(ch);
}
}
为了测试串口的通讯功能,这里我们通过串口接收命令的方式来控制LED的亮灭和蜂鸣器的响和停止,并且显示当前的状态。根据串口输出提示,发送对应字符可以实现相应功能,并且显示状态。
第十二节 Flash的读写
嵌入式系统中需要存储数据,而片内的Flash资源很匮乏,所以我们经常需要使用SpiFlash来存储数据,MT254xboard中板载了一个 512Kbyte的Flash,下面我们来驱动此Flash。上一小节中我们用SPI的方式驱动了LCD12864,这节我们继续用SPI来驱动板载的 Flash,<GD25Q40.pdf>详细的说明了如何驱动这片Flash,在此不做累述,我们复制LCD12864工程,重命名为SpiFlash,在此工程中添加GD25Q40的两个驱动文件。
下面我们来检测这个Flash,检测的方法为,全部写入0xAA,然后再读出,对比是否为0xAA,如果是,那Flash是没有问题的,否则Flash可能已经有坏块。具体的代码见例程,这个过程所需要的时间取决于我们需要检测的区域大小,如果完全检测,则可能需要几分钟的时间。
int main(void)
{
SysStartXOSC();
LCD12864_Init(); // LCD初始化
GD25Q40_Init(); // Flash初始化
LCD12864_DisStr(0, “Flash Check.。。。”);
sprintf(LCDBuf, “Flash ID :%04X”, GD25Q40_ReadID()); // 读取器件ID
LCD12864_DisStr(1, LCDBuf);
GD25Q40_EraseChip(); // 擦除整片Flash 大约需要10S
LCD12864_DisStr(2, “Erase Chip Complete”);
uint32 iCnt = 0;
// 全部写入0xAA
const uint8 Write = 0xAA;
for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
{
GD25Q40_Write(&Write, iCnt, 1); // 写入0xAA
}
// 读取Flash内部的值,与写入的值对比
uint8 Read;
for(iCnt=0; iCnt < CHECK_ADDR_RANGE; iCnt++)
{
GD25Q40_Read(&Read, iCnt, 1);
if(Read != Write)
{
LCD12864_DisStr(3, “Flash Error”);
break;
}
}
// 写入的值与读出的值完全一样
if(iCnt >= CHECK_ADDR_RANGE)
{
LCD12864_DisStr(3, “Flash Check Success”);
}
GD25Q40_EraseChip(); // 再次擦除
while(1);
return 0;
}
MT254X蓝牙4.0开发板Flash效果:
第十三节 BLE协议栈简介
TI的协议栈分为两部分:控制器和主机。对于4.0以前的蓝牙,这两部分是分开的。所有profile和应用都建构在GAP或GATT之上。根据这张图,我们从底层开始介绍。TI的这款CC2540器件可以单芯片实现BLE蓝牙协议栈结构图的所有组件,包括应用程序。
1.1.1 PHY层
1Mbps自适应跳频GFSK(高斯频移键控),运行在免证的2.4GHz。
1.1.2 LL层
LL层为RF控制器,控制设备处于准备(standby)、广播、监听/扫描(scan)、初始化、连接,这五种状态中一种。五种状态切换描述为:未连接时,设备广播信息,另外一个设备一直监听或按需扫描,两个设备连接初始化,设备连接上了。发起聊天的设备为主设备,接受聊天的设备为从设备,同一次聊天只能有一个意见领袖,即主设备和从设备不能切换。
1.1.3 HCI层
HCI层为接口层,向上为主机提供软件应用程序接口(API),对外为外部硬件控制接口,可以通过串口、SPI、USB来实现设备控制。
1.1.4 L2CAP层
L2CAP层提供数据封装服务,允许逻辑上的点对点通讯。
1.1.5 SM层
SM层提供配对和密匙分发,实现安全连接和数据交换。
1.1.6 ATT层
ATT层负责数据检索,允许设备向另外一个设备展示一块特定的数据称之为属性,在ATT环境中,展示属性的设备称之为服务器,与它配对的设备称之为客户端。链路层的主机从机和这里的服务器、客服端是两种概念,主设备既可以是服务器,也可以是客户端。从设备毅然。
1.1.7 GATT层
GATT层定义了使用 ATT 的服务框架和配置文件(profiles)的结构。BLE 中所有的数据通信都需要经过 GATT。GATT负责处理向上与应用打交道,其关键工作是把为检索工作提供合适的profile结构,而profile由检索关键词(characteristics)组成。
1.1.8 GAP层
GAP直接与应用程序或配置文件(profiles)通信的接口,处理设备发现和连接相关服务。另外还处理安全特性的初始化。对上级,提供应用程序接口,对下级,管理各级职能部门,尤其是指示LL层控制室五种状态切换,指导保卫处做好机要工作。
1.2 TI协议栈源码介绍
在第二章我们讲解了源码的安装,这里我们就来剖析源码的结构。打开协议栈目录我们可以看到下图:
BLE源码:
目录名
内容说明
Accessories一些工具和已经编译好的Hex文件此文件夹中有Btool的安装包、USB-CDC的驱动。
ComponentsHal驱动,OSAL源码、协议栈通用源码此文件夹是OSAL各层组件的实现
Documents帮助文档协议栈说明文档,这是学习BLE最好的资料。
Projects工程文件这里有一些TI的Demo,我们开发一般是在Demo的基础上进行
这里TI给出了很多Demo,这些例程都是经过了SIG评审的,ble 文件夹中有很多工程文件,有些是具体的应用,例如BloodPressure、GlucoseCollector 、GlucoseSensor 、 HeartRate 、HIDEmuKbd 等都为传感器的实际应用,有相应标准的 Profile。
其中有4种角色: SimpleBLEBroadcaster 、 SimpleBLECentral 、SimpleBLEObserver、SimpleBLEPeripheral。
他们都有自己的特点。
1.Broadcaster 广播员 —— 非连接性的信号装置
2.Observer 观察者 —— 扫描得到,但不能链接
3.Peripheral 从机 —— 可链接,在单个链路层链接中作为从机
4.Central 主机 —— 扫描设备并发起链接,在单链路层或多链路层中作为主机。
我们的讲解将围绕这主机和从机进行。因为其它的设备都是基于这两种设备扩展开来的。
第十四节 OSAL工作原理
蓝牙为了实现同多个设备相连,或实现多功能,也实现了功能扩充,这就产生了调度问题。因为,虽然软件和协议栈可扩充,但终究最底层的执行部门只有一个。为了实现多事件和多任务切换,需要把事件和任务对应的应用,并起一个名字OSAL操作系统抽象层。
OSAL管理的实现
如果实现软件和硬件的低耦合,使软件不经改动或很少改动即可应用在另外的硬件上,这样就方便硬件改造、升级、迁移后,软件的移植。HAL硬件抽象层正是用来抽象各种硬件的资源,告知给软件。其作用类似于嵌入式系统设备驱动的定义硬件资源的h头文件。
BLE低功耗蓝牙系统架构:
OSAL作为调度核心,BLE协议栈、profile定义、所有的应用都围绕它来实现。OSAL不是传统大家使用的操作系统,而是一个允许软件建立和执行事件的循环。
软件功能是由任务事件来实现的,创建一个任务事件需要以下工作:
1. 创建task identifier任务ID;
2. 编写任务初始化(task initialization routine)进程,并需要添加到OSAL初始化进程中,这就是说系统启动后不能动态添加功能;
3. 编写任务处理程序;
4. 如有需要提供消息服务。
BLE协议栈的各层都是以OSAL任务方式实现,由于LL控制室的时间要求最为迫切,所以其任务优先级最高。为了实现任务管理,OSAL通过消息处理(messageprocess),存储管理,计时器定时等附加服务实现。
系统启动流程:
为了使用OSAL,在main函数的最后要启动一个名叫osal_start_system的进程,该进程会调用由特定应用决定的启动函数 osalInitTasks(来启动系统)。osalInitTasks逐个调用BLE协议栈各层的启动进程来初始化协议栈。随后,设置一个任务的 8bit任务ID(task ID),跳入循环等待执行任务,系统启动完成。
1. 任务优先级决定于任务ID,任务ID越小,优先级越高
2. BLE协议栈各层的任务优先级比应用程序的高
3. 初始化协议栈后,越早调入的任务,任务ID越高,优先级越低,即系统倾向于处理新到的任务
每个事件任务由对应的16bit事件变量来标示,事件状态由旗号(taskflag)来标示。如果事件处理程序已经完成,但其旗号并没有移除,OSAL会认为事情还没有完成而继续在该程序中不返回。比如,在SimpleBLEPeripheral实例工程中,当事件START_DEVICE_EVT发生,其处理函数SimpleBLEPeripheral_ProcessEvent就运行,结束后返回16bit事件变量,并清除旗语 SBP_START_DEVICE_EVT。
每当OSAL事件检测到了有任务事件,其相应的处理进程将被添加到由处理进程指针构成的事件处理表单中,该表单名叫taskArr(taskarray)。taskArr中各个事件进程的顺序和osalInitTasks初始化函数中任务ID的顺序是对应的。
有两种,最简单的方法是使用osal_set_event函数(函数原型在OSAL.h文件中),在这个函数中,用户可以像定义函数参数一样设置任务ID 和事件旗语。第二种方法是使用osal_start_timerEx函数(函数原型在OSAL_Timers.h文件中),使用方法同 osal_set_event函数,而第三个以毫秒为单位的参数osal_start_timerEx则指示该事件处理必须要在这个限定时间内,通过定时器来为事件处理计时。
类似于Linux嵌入式系统内存分配C函数mem_alloc,OSAL利用osal_mem_alloc提供基本的存储管理,但osal_mem_alloc只有一个用于定义byte数的参数。对应的内存释放函数为osal_mem_free。
不同的子系统通过OSAL的消息机制通信。消息即为数据,数据种类和长度都不限定。消息收发过程描述如下:
接收信息,调用函数osal_msg_allocate创建消息占用内存空间(已经包含了osal_mem_alloc函数功能),需要为该函数指定空间大小,该函数返回内存空间地址指针,利用该指针就可把所需数据拷贝到该空间。
发送数据,调用函数osal_msg_send,需为该函数指定发送目标任务,OSAL通过旗语SYS_EVENT_MSG告知目标任务,目标任务的处理函数调用osal_msg_receive来接收发来的数据。建议每个OSAL任务都有一个消息处理函数,每当任务收到一个消息后,通过消息的种类来确定需要本任务做相应处理。消息接收并处理完成,调用函数osal_msg_deallocate来释放内存(已经包含了osal_mem_free函数功能)。
为了实现更好的移植性,协议栈将硬件层抽象出了一个HAL硬件抽象层,当新的硬件平台做好后,只需修改HAL,而不需修改HAL之上的协议栈的其他组件和应用程序。
第十五节 BLE蓝牙4.0协议栈启动分析
TI的这款CC2540/CC2541器件可以单芯片实现BLE蓝牙协议栈结构图的所有组件,包括应用程序。从这章开始我们来剖析协议栈源码,我们选用 SimpleBLEPeripheral工程开刀,这是一个从机的例程,基本的工作是对外广播,等待主机来连接,读写展示的属性。
首先打开工程文件,打开后可以看到整个工程的结构。
我们按照系统的启动顺序来一步一步走,我们都知道在C代码中,一般启动的首个函数为main,这个函数在 SimpleBLEPeripheral_Main.c中,打开文件,可以看到这个文件只有一个main函数和一个函数的申明,我们暂时不理会那个申明的函数,先看main都做了些什么工作:
Int main(void)
{
/* Initialize hardware */
HAL_BOARD_INIT(); // 硬件初始化
// Initialize board I/O
InitBoard( OB_COLD ); // 板级初始化
/* Initialze the HAL driver */
HalDriverInit(); // Hal驱动初始化
/* Initialize NV system */
osal_snv_init(); // Flash存储SNV初始化
/* Initialize LL */
/* Initialize the operating system */
osal_init_system(); // OSAL初始化
/* Enable interrupts */
HAL_ENABLE_INTERRUPTS(); // 使能总中断
// Final board initialization
InitBoard( OB_READY ); // 板级初始化
#if defined ( POWER_SAVING )
osal_pwrmgr_device( PWRMGR_BATTERY ); // 低功耗管理
#endif
/* Start OSAL */
osal_start_system(); // No Return from here 启动OSAL
return 0;
}
通过代码我们可以看到,系统启动的过程,主要是做了一些初始化,如果开启了低功耗,则还需要开启低功耗管理。我们先不去理会初始化做了什么,但是我们知道在main函数的最后启动了OSAL,那么我们就进去看看OSAL是如何运作的。
在IAR中如果需要跳转到某个函数或变量的定义,可以在此函数名中右击然后选择Go To Definition……就可以调到相应的定义。
void osal_start_system( void )
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;) // Forever Loop
#endif
{
osal_run_system();
}
}
这里看到我们进入了一个死循环,并且一直调用osal_run_system(),那我们再进入此函数。
</blockquote></div><div style=“text-align: left;”><div class=“blockcode”><blockquote>void osal_run_system( void )
{
uint8 idx = 0;
#ifndef HAL_BOARD_CC2538
osalTimeUpdate(); // 定时器更新
#endif
Hal_ProcessPoll(); // Hal层信息处理
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的核心,整个OSAL通过检测每个任务是否有事件发生,如果有则执行相应的任务,处理相应的事件。如果没有事件需要处理并且开启了低功耗模式,则系统就会进入低功耗模式。
这里有一个很关键的地方,OSAL是如何知道哪个事件需要哪个任务来处理呢?
events = (tasksArr[idx])( idx, events ); // 执行事件处理函数
我们看这里有一个很关键的数组tasksArr,很显然,这是一个函数指针数组,我们看看它的定义。
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
};
可以看到在这个数组的定义中,每个成员都是任务的执行函数,按照任务的优先级排序,并且在osalInitTasks中初始化的时候,我们可以看到每个任务都有一个对应的初始化函数,并且传递了一个taskID,此ID从0开始自增,这里有一点非常重要,初始化的顺序和任务数组的定义顺序是一样的,这就保证了我们给任务发生消息或事件时能够准确的传递到相应的任务处理函数。
void osalInitTasks( void )
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
/* LL Task */
LL_Init( taskID++ );
/* Hal Task */
Hal_Init( taskID++ );
/* HCI Task */
HCI_Init( taskID++ );
#if defined ( OSAL_CBTIMER_NUM_TASKS )
/* Callback Timer Tasks */
osal_CbTimerInit( taskID );
taskID += OSAL_CBTIMER_NUM_TASKS;
#endif
/* L2CAP Task */
L2CAP_Init( taskID++ );
/* GAP Task */
GAP_Init( taskID++ );
/* GATT Task */
GATT_Init( taskID++ );
/* SM Task */
SM_Init( taskID++ );
/* Profiles */
GAPRole_Init( taskID++ );
GAPBondMgr_Init( taskID++ );
GATTServApp_Init( taskID++ );
/* Application */
SimpleBLEPeripheral_Init( taskID );
}
应用层的初始化SimpleBLEPeripheral_Init,SimpleBLEPeripheral_Init( uint8task_id )主要对 GAP 和 GATT 进行配置,最后调用osal_set_event(simpleBLEPeripheral_TaskID, SBP_START_DEVICE_EVT )启动设备。
设备启动后应用层就能接收到这个设置的事件并进行处理,可以看到设备启动中主要是启动设备,注册绑定管理,并且启动了一个定时器,这个定时器是一个周期事件的第一次启动。
周期事件中每次都会重启这个定时器,并且处理周期事件。
在初始化的时候我们注册了一个很重要的函数,设备状态改变时的回调函数,这个函数在设备的状态改变时会被底层的协议栈回调,我们可以从这个回调函数中看的设备的状态的改变。
static void peripheralStateNotificationCB( gaprole_States_t newState);
从函数的定义可以看出,设备的状态类型都在数据类型gaprole_States_t中定义了,我们看一下这个数据类型的定义:
typedef enum
{
GAPROLE_INIT = 0, //!< Waiting to be started
GAPROLE_STARTED, //!< Started but not advertising
GAPROLE_ADVERTISING, //!< Currently Advertising
GAPROLE_WAITING, //!< Device is started but not advertising, is in waiting period before advertising again
GAPROLE_WAITING_AFTER_TIMEOUT, //!< Device just timed out from a connection but is not yet advertising, is in waiting period before advertising again
GAPROLE_CONNECTED, //!< In a connection
GAPROLE_CONNECTED_ADV, //!< In a connection + advertising
GAPROLE_ERROR //!< Error occurred - invalid state
} gaprole_States_t;
看到这个定义就很明确了,设备的状态就在这几种状态间切换。