打印
[蓝牙]

转】TI蓝牙BLE 协议栈代码学习

[复制链接]
2352|22
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
geraldbetty|  楼主 | 2020-7-16 20:54 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

  BLE就是低功率蓝牙。要着重了解两种设备:

  dual-mode双模设备:简单说就是向下兼容。
  single-mode单模设备:仅仅支持BLE。

  关于开发主要讲的是单模设备,它可以只靠纽扣电池即可持续工作。

  TI的蓝牙4.0BLE协议栈为BLE-CC254x-1.4.0,即现在的版本是1.4版本的。可以从TI官方下载或从附件中下载安装,默认是安装在C盘中。因为上一篇博文提到进行空中固件升级,当时没有安装在C盘下,死活生成不了bin文件,改在C盘下生成了。所以,我个人建议,还是默认安装吧,也占不了多大空间。

  TI蓝牙4.0BLE协议栈的结构如下图所示:

  由控制器和主机两部分构成,分层的思想很明晰。

  主机包括物理层PHY、数据链路层LL和主机控制器接口HCI构成。物理层PHY是1Mbps自适应跳频的GFSK射频,工作于免许可证的2.4G频段。数据链路层LL用于控制设备的射频状态,使设备工作于Standby(准备)、Advertising(广播)、Scanning(扫描)、Initiating(初始化)、Connected(连接)五种状态中的一种。主机控制器接口HCI为主机和控制器之前提供标准的通信接口。

  主机包括逻辑链路控制及自适应协议层L2CAP、安全管理层SM、属性协议层ATT、通用访问配置文件层GAP、通用属性配置文件层GATT构成。逻辑链路控制及自适应协议层L2CAP为上层提供数据封装服务,允许逻辑上的点对点通信。安全管理层SM配对和密钥分发服务,实现安全连接和数据交换。属性协议层ATT允许设备向其他设备展示一块特定的数据,这块数据称之为“属性”。通用属性配置文件层GATT定义了使用ATT的服务框架和配置文件(profile),BLE中所有数据的通信都要通过GATT层。通用访问配置文件层GAP提供设备通信的接口,负责处理访问模式和程序,包括设备发现、建立连接、终止连接、初始化安全和设备配置等。对于我们来说,直接接触的是GAP和GATT两个层。

  最早接触这个项目的时候,听说CC2540/2541是51内核的SOC,当时我心想,毛毛雨啦,51的东东还不简单。等真接手了才发现,头大了,TI的工程师把协议栈封装和规划得都很好,不能不佩服。

  先分析协议栈的流程吧,这里以TI的KeyFobDemo为例,该工程位于C:\Texas Instruments\BLE-CC254x-1.4.0\Projects\ble\KeyFob中。先看下工程的架构。对于我们开发来说,App和Profile两个文件夹中的内容是最主要的。

  先从main()函数入手,打开App文件夹下的KeyFob_Main.c,找到main()函数:

int main(void){  /* Initialize hardware */  HAL_BOARD_INIT();//初始化硬件  // Initialize board I/O  InitBoard( OB_COLD );//初始化板卡IO  /* Initialze the HAL driver */  HalDriverInit();//初始化HAL层驱动  /* Initialize NV system */  osal_snv_init();//初始化Flash  /* 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;}

上述代码,我加入了简单的中文注释,会发现有个很重要的东西——OSAL,Operation System Abstraction Layer,操作系统抽象层。OSAL还不是操作系统,但是实现了OS的很多功能。从前面的代码中我们可以看到,跟OSAL相关的有两个函数osal_init_system()和osal_start_system()(osal_pwrmgr_device()暂时先不去理会)。我们依次来看看。

在IAR环境中,可以在代码中osal_init_system()上单击鼠标右键,打开“Go to definition of osal_init_system”,

osal_init_system()在OSAL.c中,下面就是该函数的代码:

uint8 osal_init_system( void ){  // Initialize the Memory Allocation System  osal_mem_init();//初始化内存分配系统
// Initialize the message queue  osal_qHead = NULL;//初始化消息队列  // Initialize the timers  osalTimerInit();//初始化定时器  // Initialize the Power Management System  osal_pwrmgr_init();//初始化电源管理系统
// Initialize the system tasks.  osalInitTasks();//初始化系统任务  // Setup efficient search for the first free block of heap.  osal_mem_kick();  return ( SUCCESS );}

该函数是完成一系列的初始化,跟操作系统有关的,仿佛是osalInitTasks(),我们进到这个函数里面。osalInitTasks()在OSAL_KeyfobDemo.c中。

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 */  KeyFobApp_Init( taskID );}

从每行代码,可以看到整个事件初始化的过程也是分层的。从链路层任务初始化(LL_Init)到硬件抽象层(Hal_Init)、主机控制器接口(HCI_Init)、逻辑链路控制及自适应协议层(L2CAP_Init)、GAP层(GAP_Init)、GATT层(GATT_Init)、安全管理层(SM_Init)。然后完成GAP层的配制(GAPRole_Init)、蓝牙绑定的管理初始化(GAPBondMgr_Init)及GATT层服务的初始化(GATTServApp_Init)。最后完成的是应用层的初始化(KeyFobApp_Init)。程序是一行一行地执行,各层的任务也是依次的完成初始化。  

接下来我们再看main()函数中另一个跟OSAL相关的函数——osal_start_system(),也位于OSAL.c中。

void osal_start_system( void ){#if !defined ( ZBIT ) && !defined ( UBIT )  for(;;)  // Forever Loop#endif  {    osal_run_system();  }}

一看这是个死循环,相当于单片机程序最后一行while(1);。这个函数最主要的部分还是osal_run_system(),找到它,也在OSAL.c中。

void osal_run_system( void ){  uint8 idx = 0;#ifndef HAL_BOARD_CC2538  osalTimeUpdate();#endif  Hal_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}

  去掉条件编译部分,最核心的是一个do-while循环,一个if判断。

  do-while循环:

do-while循环: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);}

  这部分代码应该是OSAL最核心最精髓的部分了。前面的循环中已经确定有事件发生了。HAL_ENTER_CRITICAL_SECTION(intState);和HAL_EXIT_CRITICAL_SECTION(intState);分别是关中断和使能中断,以防止在执行代码时被中断打断。将事件表tasksEvents[]中的事件赋给events,然后该事件清零。接下来events = (tasksArr[idx])( idx, events );就是去处理事件了,这里的tasksArr[]数组非常重要。下面的tasksEvents[idx] |= events;就是把没有响应的事件再放回到tasksEvents[]中。

  我们来看看这个非常重要的数组taskArr[],它被定义在OSAL_KeyFobDemo.c中。

// The order in this table must be identical to the task initialization calls below in osalInitTask.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  KeyFobApp_ProcessEvent                                      // task 11};

  数组内的成员看起来很面熟。最上面一行的注释也写得很清楚,表中的顺序要和osalInitTask()中定义的一致。再来看这个数组的类型,是pTaskEventHandlerFn,这是个什么东西?pTaskEventHandlerFn不是东西,是

typedef unsigned short (*pTaskEventHandlerFn)( unsigned char task_id, unsigned short event );

  这个定义是一个函数指针,看起着很头疼,很蛋疼。如果换一下:

typedef pTaskEventHandlerFn unsigned short (*)( unsigned char task_id, unsigned short event );

  这样或许理解起来要好一些。拿KeyFobApp_ProcessEvent的声明来看,uint16 KeyFobApp_ProcessEvent( uint8 task_id, uint16 events ),这是符合pTaskEventHandlerFn的格式的,函数名就是指针,函数的地址。

  tasksArr[]是一个函数指针数组,里面保存了所有事件处理函数的地址。当有事件发生时,就执行events = (tasksArr[idx])( idx, events );一句,就是对应的tasksArr[]里相应的那个事件的处理函数。

  再看另一个数组,tasksEvents[]。tasksEvents[]声明为全局变量,是在osalInitTasks()中定义和初始化的:

tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));

  这个数组的大小跟事件的数量是一致的,而且被osal_memset()初始化为0.

  这样OSAL的运行机理基本清晰了,就是在do-while()循环中判断tasksEvents[]中哪个事件非0,即事件被触发了;然后在if中把该事件清0,执行tasksArr[]中定义好的事件处理函数,然后把没有执行的事件再放回到tasksEvents[]中。这也是为什么在osalInitTask()中进行初始化的时候,初始化的顺序要和tasksArr[]一致。

  以上是我对OSAL的理解,因为C语言的基本不够瓷实,说得也很大白话。之所以敢这么大胆贴出来,也是请大家多批评指正,让我能得到提高。


使用特权

评论回复

相关帖子

沙发
21ic小喇叭| | 2020-7-30 16:25 | 只看该作者
了解,C语言还是蛮难学的

使用特权

评论回复
板凳
waveforms| | 2020-7-30 19:53 | 只看该作者
感谢分享!学习一下

使用特权

评论回复
地板
10299823| | 2020-8-5 16:17 | 只看该作者
那款TI公司的内嵌蓝牙的单片机是什么型号?

使用特权

评论回复
5
jimmhu| | 2020-8-5 16:17 | 只看该作者
蓝牙4.0和BLE有什么差别吗?

使用特权

评论回复
6
lihuami| | 2020-8-5 16:17 | 只看该作者
              

使用特权

评论回复
7
xiaoyaozt| | 2020-8-5 16:17 | 只看该作者
如何使用BLE蓝牙模块  

使用特权

评论回复
8
uptown| | 2020-8-5 16:18 | 只看该作者
蓝牙协议栈也是根据osi实现的吗

使用特权

评论回复
9
cehuafan| | 2020-8-5 16:18 | 只看该作者
有没有人在uCOS上移植过蓝牙协议栈  

使用特权

评论回复
10
usysm| | 2020-8-5 16:18 | 只看该作者
ti cc2540 蓝牙协议栈开源吗

使用特权

评论回复
11
jkl21| | 2020-8-5 16:18 | 只看该作者
掌握 蓝牙ble 协议开发要多久  

使用特权

评论回复
12
wwppd| | 2020-8-5 16:19 | 只看该作者
如何将蓝牙协议栈移植到嵌入式Linux中

使用特权

评论回复
13
maqianqu| | 2020-8-5 16:19 | 只看该作者
如何测试设置是否支持蓝牙ble

使用特权

评论回复
14
lihuami| | 2020-8-5 16:19 | 只看该作者
谢谢楼主分享的资料了。         

使用特权

评论回复
15
jimmhu| | 2020-8-5 16:19 | 只看该作者
BLE和传统蓝牙有什么区别  

使用特权

评论回复
16
10299823| | 2020-8-5 16:19 | 只看该作者
有哪些好的蓝牙芯片?  

使用特权

评论回复
17
usysm| | 2020-8-5 16:19 | 只看该作者
怎么区分蓝牙模式和ibeacon模式  

使用特权

评论回复
18
cehuafan| | 2020-8-5 16:19 | 只看该作者
nordic蓝牙协议栈是hex文件吗

使用特权

评论回复
19
jkl21| | 2020-8-5 16:19 | 只看该作者
蓝牙BLE如何连接连接多台设备

使用特权

评论回复
20
uptown| | 2020-8-5 16:19 | 只看该作者
蓝牙协议中 Descriptor是必要的吗?  

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

20

主题

1329

帖子

0

粉丝