蓝牙4.0和zigbee的协议栈BLE && Zstack详解(包括OSAL操作系统流程)

  • 本文对TI公司的两个协议栈进行解析,包括BLE蓝牙4.0协议栈和Zstack Zigbee协议栈。二者均采用osal操作系统。本文也会对该系统基本操作流程进行解答。

协议

协议是一系列的通信标准,通信双方按照共同的标准进行数据的传输。

其实说白了协议就是在给每一层的数据进行封装打包,假设收集的数据是一个数组,那就再数组的前面或者后面或者两者加上一些冗余的数据,包括包头包尾校验码等。(物理层除外)

协议栈

协议栈则是协议的具体实现形式,既协议栈是用户和协议之间的一个接口,协议栈以函数形式实现各层协议间的关系,并提供API接口给用户。

名词解释区

  • 此处是一些名词的解释,简单看看就好

CCM -Counter with CBC-MAC(mode of operation)

HAL -Hardware Abstraction Layer (硬件抽象层)

PAN -Personal Area Network (个人局域网)

RF -Radio Frequency (射频)

RSSI -Received Signal Strength Indicator (接收信号强度指示)

串口透传:将数据通过串口给到下位机再通过无线发送出去。

上层实体:既协议的上一层。越下层的协议是越底层的,上层协议才是代码能操作。

回调函数:通过函数指针调用的函数。可以实现参数类型一致的函数用同一个语句调用。

coordinator:协调器

end device:终端

router:路由

蓝牙协议

架构

先对蓝牙协议的架构做一个了解
在这里插入图片描述

Physical Layer(物理层)
  • 简单了解
  1. 只与通信相关的硬件部分。
  2. BLE的通信频段范围为2.400-2.4835 GHz。
  3. 为了同时支持多个设备,将整个频带分为40份,每份的带宽为2MHz,称作RF Channel(Physical Channel)。
Link Layer (连接层)
  • 简单了解
    Link Layer用于解决Physical Channel的共享问题,分两种场景适配
  1. 对于数据量比较少,发送不频繁,对时延不是很敏感的场景:

    这种数据场景,BLE的Link Layer采取了一种比较懒的处理方式——广播通信:从40个Physical Channel中选取3个,作为广播通道,在广播通道上,任何参与者,爱发就发,爱收就收,随便;所有参与者,共享同一个逻辑传输通道(广播通道)

  2. 数据量较大,发送频率较高,对时延较敏感的场景

    BLE的Link Layer会从剩余的37个Physical Channel中,选取一个,为这种场景里面的通信双方建立单独的通道(data channel)。这就是连接(connection)的过程。同时,为了增加容量,增大抗干扰能力,连接不会长期使用一个固定的Physical Channel,而是在多个(如37个)之间随机但有规律的切换,这就是BLE的跳频(Hopping)技术。

Link layer抽象出了5种状态(state)1
Standby State

初始状态,即不发送数据,也不接收数据。根据上层实体的命令(如位于host中GAP),可由其它任何一种状态进入,也可以切换到除Connection状态外的任意一种状态。

Advertising State

可以通过广播通道发送数据的状态,由Standby状态进入。它广播的数据可以由处于Scanning或者Initiating状态的实体接收。上层实体可通过命令将Advertising状态切换回Standby状态。另外,连接成功后,也可切换为Connection状态.

Scanning State

可以通过广播通道接收数据的状态,由Standby状态进入。根据Advertiser所广播的数据的类型,有些Scanner还可以主动向Advertiser请求一些额外数据。上层实体可通过命令将Scanning状态切换回Standby状态.

Initiating State

和Scanning状态类似,不过是一种特殊的接收状态,由Standby状态进入,只能接收Advertiser广播的connectable的数据,并在接收到数据后,发送连接请求,以便和Advertiser建立连接。当连接成功后,Initiater和对应的Advertiser都会切换到Connection状态。

Connection State

和某个实体建立了单独通道(Logical Channel)的状态,在通道建立之后,由Initiating或者Advertising自动切换而来。通道断开后,会重新回到Standby状态。

通道建立后,处于Connection状态的双方,分别有两种角色Master和Slave:Initiater方称作Mater;Advertiser方称作Slave。

5种状态的关系

L2CAP(Logical Link Control and Adaptation Protocol)
  • 简单了解

    一个介于应用程序(Profile、Application等)和Link Layer之间的协议。

    将Logical Channel划分为一个个的L2CAP Channel,以便提供应用程序级别的通道复用。

ATT(Attribute Protocol)
  • 重点
    为了实现信息的采集,BLE将信息以Attribute(属性)的形式抽象出来,并提供一些方法,供远端设备(remote device)读取,修改这些属性的值(Attribute value)。

其实现的主要思路是

  1. 基于L2CAP,使用固定的Channel ID(0x004)

  2. 采用client-server的形式.
    提供信息(以后都称作Attribute)的一方称作ATT server(一般是那些传感器节点),访问信息的一方称作ATT client。

  3. 一个Attribute由Attribute Type、Attribute Handle和Attribute Value组成

    1. Attribute Type
      用于标识Attribute的类型,类似于我们常说的"温度","湿度"等人类可识别的术语,不过与人类术语不同的是,Attribute Type使用UUID(Universally Unique IDenifier,16-bit,32-bit或者128-bit的数值)区分.

    2. Attribute Handle
      一个16-bit的数值,用作唯一识别Attribute server上的所有Attribute。

    3. Attribute Value
      代表Attribute的值,可以是任何固定长度或者可变长度的字节数组

  4. 权限(Permissions)
    Attribute可以定义一些权限(Permissions),以便server控制client的访问行为,包括:

访问有关的权限(access permissions):Readable,Writeable以及Readable and writable。

加密有关的权限(encryption permissions):Encryption required和 No encryption required

认证有关的权限(authentication permissions):Authentication Required和No Authentication Required

授权有关的权限(authorization permissions):Authorization Required和No Authorization Required

GATT(Generic Attribute Profile)
  • 重点
    ATT之所以称作“protocol”,是因为它还比较抽象,仅仅定义了一套机制,允许client和server通过Attribute的形式共享信息。而具体共享哪些信息,ATT并不关心,这是GATT(Generic Attribute Profile)的主场。

作为一个Profile Framework, GATT profile提出了如下的层次结构
在这里插入图片描述
层次结构依次是:profile->service->characteristic。

  1. Profile
    由一个或者多个和某一应用场景有关的Service组成。

  2. service
    一个Service 包含一个或多个Characteristic(特征),也可以通过include的方式,包含其它Service。

  3. Characteristic
    GATT profile中最基本的数据单位,由一个Properties,一个Value,一个或者多个Descriptor组成。
    1. Properties
    定义了charactertistic的value如何被使用,以及characteristic的Descriptor如何被访问。
    2. Value
    特征的实际值。
    3. Descriptor
    保存了一些和Characteristic Value相关的信息.

以上除"Profile"外的每一个定义,Service,Characteristic,Characteristic Properties,Characteristic Value、Characteristic Descriptor等等,都是作为一个Attribute存在的,都具有Attribute的所有特征:Attribute Handle、Attribute Types、Attribute Value 和Attribute Permissions。

示例:

  //profile service
    [USER_PROFILE_SERVICE_IDX] =         {
                                           {ATT_BT_UUID_SIZE, primaryServiceUUID},         //type
                                           GATT_PERMIT_READ,                               //permissions
                                           0,                                              //handle
                                           (uint8*)&UserProfileService                     //pVaule
                                         },

  //char 1 Properties
    [USER_PROFILE_CHAR1_DECLA_IDX] =     {
                                           {ATT_BT_UUID_SIZE, characterUUID},              //type
                                           GATT_PERMIT_READ,                               //READ permissions
                                           0,                                              //handle
                                           &UserProfileChar1Prop                           //pVaule
                                         },


  //char 1 Vaule
    [USER_PROFILE_CHAR1_VALUE_IDX] =     {
                                           {ATT_BT_UUID_SIZE, User_Profile_char1UUID},     //type
                                           GATT_PERMIT_READ,                               //READ permissions
                                           0,                                              //handle
                                           UserProfileChar1                                //pVaule
                                         },

  //char 1 Description
    [USER_PROFILE_CHAR1_DESCRIP_IDX] =   {
                                           {ATT_BT_UUID_SIZE, charUserDescUUID},           //type
                                           GATT_PERMIT_READ,                               //permissions
                                           0,                                              //handle
                                           UserProfileChar1Desp                            //pVaule
                                         },

已连接的两台设备之间存在的一层协议层,存在以下两种状态:

  1. GATT Client(客户端)
    向Server端写入或读取数据的设备。可同时包含多个services。

  2. GATT Server(服务器端)
    被Client写入或读取数据的设备。

GAP(Generic Access)
  • 重点
    用于解决无连接的advertising channel,以及连接建立的过程的一层协议

GAP层包含四种角色(role)

  1. broadcaster
    表示设备正在发送advertising events,处于无连接状态的advertiser
  2. Observer
    设备正在接收advertising events,搜寻广播,但不能发起连接。
  3. Peripheral
    设备接受Link Layer连接(对应Link Layer的slave角色)。
  4. Central
    设备发起Link Layer连接(对应Link Layer的master角色)。
设备连接:

当Peripheral设备广播特殊序列数据,告知Central自己是可连接时,扫描到该信息的Central会向Peripheral发起连接请求,连接请求包括以下数据:

连接间隔(connection interval)

两次"connection event"间的间隔量,以1.25ms为单位。最小值为6(7.5ms),最大值为3200(4s)。

从机延迟(slave latency)

从机不响应主机"connection event"的次数。最大值为499,最小值为0。但最大值不能让有效连接间隔大于16s。

监管超时(supervision timeout)

两次有效"connection event"间的间隔量,单位为10ms。超时将会将设备连接状态改为无连接状态。最小值为10(100ms),最大值为3200(32s)超时时间必须大于Effective connection interval(有效连接间隔)。

E f f e c t i v e c o n n e c t i o n i n t e r v a l = ( C o n n e c t i o n i n t e r v a l ) ∗ ( 1 + ( s l a v e l a t e n c y ) Effective connection interval = (Connection interval)*(1+(slave latency) Effectiveconnectioninterval=(Connectioninterval)(1+(slavelatency)
( s u p e r v i s i o n t i m e o u t ) ∗ 10 > E f f e c t i v e c o n n e c t i o n i n t e r v a l (supervision timeout)*10 > Effective connection interval (supervisiontimeout)10>Effectiveconnectioninterval

安全特征(security features)

连接建立之后,主从设备可进行配对,以此来交换彼此的密钥,达到加密数据的目的。

BLE协议栈

  • 讲了这么多协议上的事,那么具体的。我们要怎么去实现对该协议的操作呢。其实很简单,就是函数调用而已。协议栈本身就是提供API接口给用户而存在的。具体来看看需要什么函数才能实现BLE协议栈的操作吧。(这里先不讲OSAL操作系统,先看实现蓝牙所需的函数)2
  • BLE协议栈下载链接(看自己芯片型号下载适合的协议栈,用IAR打开编译下载):http://www.ti.com.cn/tool/cn/ble-stack?keyMatch=BLE协议栈&tisearch=Search-CN-Everything
  • 看协议栈的时候最好跟着例程代码跳转。IAR里右键-go to destination可跳转到定义,右键-find all reference可找到所有引用

下面这些函数将其作为手册进行查找,暂时不用细深究,浏览一遍有印象就行。等理解了osal的操作系统,再回来看。

// Setup the GAP
bStatus_t GAP_SetParamValue( gapParamIDs_t paramID, uint16 paramValue );

param:

TGAP_CONN_PAUSE_PERIPHERAL: 连接状态更新前最小连接确认时间
TGAP_LIM_DISC_ADV_INT_MIN:高速探索模式下的最小广播间隔时间(n*0.625 mSec)
TGAP_LIM_DISC_ADV_INT_MAX:高速探索模式下的最大广播间隔时间(n*0.625 mSec)
TGAP_GEN_DISC_ADV_INT_MIN:普通探索模式下的最小广播间隔时间(n*0.625 mSec)
TGAP_GEN_DISC_ADV_INT_MAX:普通探索模式下的最大广播间隔时间(n*0.625 mSec)

示例:
VOID GAP_SetParamValue( TGAP_CONN_PAUSE_PERIPHERAL, DEFAULT_CONN_PAUSE_PERIPHERAL );
  // Set advertising interval
  {
    uint16 advInt = DEFAULT_ADVERTISING_INTERVAL;

    GAP_SetParamValue( TGAP_LIM_DISC_ADV_INT_MIN, advInt );
    GAP_SetParamValue( TGAP_LIM_DISC_ADV_INT_MAX, advInt );
    GAP_SetParamValue( TGAP_GEN_DISC_ADV_INT_MIN, advInt );
    GAP_SetParamValue( TGAP_GEN_DISC_ADV_INT_MAX, advInt );
  }
\\GAP Role设置函数
GAPRole_SetParameter( uint16 param, uint8 len, void *pValue )

param :

GAPROLE_ADVERT_ENABLED:开启或关闭广播。paramValue: enables/disables
GAPROLE_ADVERT_DATA:广播数据。
GAPROLE_SCAN_RSP_DATA:扫描回应时会包含该串数据,通常为设备名称。
GAPROLE_ADVERT_OFF_TIME:该值表示当前设备过多长时间可再次被扫描到,当为0时只有在GAPROLE_ADVERT_ENABLED重新被设置为TRUE时才能再次被扫描到。
APROLE_PARAM_UPDATE_ENABLE:自动更新连接参数请求。
GAPROLE_MIN_CONN_INTERVAL:设置最小的connection interval value
GAPROLE_MAX_CONN_INTERVAL:设置最大的connection interval value
GAPROLE_SLAVE_LATENCY:设置slave latency
GAPROLE_TIMEOUT_MULTIPLIER:设置connection supervision timeout


示例:
	uint8 initial_advertising_enable = TRUE;
    uint16 gapRole_AdvertOffTime = 0;

    uint8 enable_update_request = DEFAULT_ENABLE_UPDATE_REQUEST;
    uint16 desired_min_interval = DEFAULT_DESIRED_MIN_CONN_INTERVAL;    //连接事件间隔最小值
    uint16 desired_max_interval = DEFAULT_DESIRED_MAX_CONN_INTERVAL;    //连接事件间隔最大值
    uint16 desired_slave_latency = DEFAULT_DESIRED_SLAVE_LATENCY;       //从机可忽略的连接事件数量
    uint16 desired_conn_timeout = DEFAULT_DESIRED_CONN_TIMEOUT;

    // Set the GAP Role Parameters
    GAPRole_SetParameter( GAPROLE_ADVERT_ENABLED, sizeof( uint8 ), &initial_advertising_enable );
    GAPRole_SetParameter( GAPROLE_ADVERT_OFF_TIME, sizeof( uint16 ), &gapRole_AdvertOffTime );

    GAPRole_SetParameter( GAPROLE_SCAN_RSP_DATA, sizeof ( scanRspData ), scanRspData );
    GAPRole_SetParameter( GAPROLE_ADVERT_DATA, sizeof( advertData ), advertData );

    GAPRole_SetParameter( GAPROLE_PARAM_UPDATE_ENABLE, sizeof( uint8 ), &enable_update_request );
    GAPRole_SetParameter( GAPROLE_MIN_CONN_INTERVAL, sizeof( uint16 ), &desired_min_interval );
    GAPRole_SetParameter( GAPROLE_MAX_CONN_INTERVAL, sizeof( uint16 ), &desired_max_interval );
    GAPRole_SetParameter( GAPROLE_SLAVE_LATENCY, sizeof( uint16 ), &desired_slave_latency );
    GAPRole_SetParameter( GAPROLE_TIMEOUT_MULTIPLIER, sizeof( uint16 ), &desired_conn_timeout );
//设置GAP GATT Server 的参数值
bStatus_t GGS_SetParameter( uint8 param, uint8 len, void *value );

param:

GGS_DEVICE_NAME_ATT:扫描时候设备显示的名字。

示例:
  // Set the GAP Characteristics
  GGS_SetParameter( GGS_DEVICE_NAME_ATT, GAP_DEVICE_NAME_LEN, attDeviceName );
\\设置GAP Bond Manger 参数值
bStatus_t GAPBondMgr_SetParameter( uint16 param, uint8 len, void *pValue )

param:

GAPBOND_PAIRING_MODE:配对模式设置。
GAPBOND_MITM_PROTECTION:配对时是否需要认证主从机的连接。
GAPBOND_IO_CAPABILITIES:表明设备是否存在passkey输入输出设备。
GAPBOND_BONDING_ENABLED:是否开启配对。

示例:
  // Setup the GAP Bond Manager
  {
    uint32 passkey = 0; // passkey "000000"
    uint8 pairMode = GAPBOND_PAIRING_MODE_WAIT_FOR_REQ;
    uint8 mitm = TRUE;
    uint8 ioCap = GAPBOND_IO_CAP_DISPLAY_ONLY;
    uint8 bonding = TRUE;
    GAPBondMgr_SetParameter( GAPBOND_DEFAULT_PASSCODE, sizeof ( uint32 ), &passkey );
    GAPBondMgr_SetParameter( GAPBOND_PAIRING_MODE, sizeof ( uint8 ), &pairMode );
    GAPBondMgr_SetParameter( GAPBOND_MITM_PROTECTION, sizeof ( uint8 ), &mitm );
    GAPBondMgr_SetParameter( GAPBOND_IO_CAPABILITIES, sizeof ( uint8 ), &ioCap );
    GAPBondMgr_SetParameter( GAPBOND_BONDING_ENABLED, sizeof ( uint8 ), &bonding );
  }
  // Initialize GATT attributes这里几个函数就属于没提供底层代码的直接写进去就行
  GGS_AddService( GATT_ALL_SERVICES );            // GAP
  GATTServApp_AddService( GATT_ALL_SERVICES );    // GATT attributes
  DevInfo_AddService();                           // Device Information Service
\\这个函数是重点,它的参数无所谓,不是0就行。
bStatus_t SimpleProfile_AddService( uint32 services )
{
···
if ( services & SIMPLEPROFILE_SERVICE )
  {
    // Register GATT attribute list and CBs with GATT Server App
    //这里对Attribute表和函数的读写回调进行了注册,
    //Attribute表需要经过注册才能使用。回调函数的注册会在之后讲解。
    status = GATTServApp_RegisterService( simpleProfileAttrTbl, 
                                          GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                          GATT_MAX_ENCRYPT_KEY_SIZE,
                                          &simpleProfileCBs );
  }
···
}
\\设置characteristic的值。
bStatus_t SimpleProfile_SetParameter( uint8 param, uint8 len, void *value )

示例:
  // Setup the SimpleProfile Characteristic Values
  {
    uint8 charValue1 = 1;
    uint8 charValue2 = 2;
    uint8 charValue3 = 3;
    uint8 charValue4 = 4;
    uint8 charValue5[SIMPLEPROFILE_CHAR5_LEN] = { 1, 2, 3, 4, 5 };
    SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR1, sizeof ( uint8 ), &charValue1 );
    SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR2, sizeof ( uint8 ), &charValue2 );
    SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR3, sizeof ( uint8 ), &charValue3 );
    SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR4, sizeof ( uint8 ), &charValue4 );
    SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR5, SIMPLEPROFILE_CHAR5_LEN, charValue5 );
  }
  // Register callback with SimpleGATTprofile
  //回调函数的注册,之后会说明。
  //现在只需知道characteristic值被改变时会调用回调函数。
  VOID SimpleProfile_RegisterAppCBs( &simpleBLEPeripheral_SimpleProfileCBs );
//设备启动函数
// Start the Device
    VOID GAPRole_StartDevice( &simpleBLEPeripheral_PeripheralCBs );

    // Start Bond Manager
    VOID GAPBondMgr_Register( &simpleBLEPeripheral_BondMgrCBs );

OSAL操作系统

  • 重重重重点理解了这个系统的流程,写起协议栈来就很轻松。
  • 首先记住这一句话:函数注册是指将该函数的执行交付给哪一个任务
  • 看这里的时候请务必拿出程序跟着走。

一个程序拿上手,毫无疑问肯定先从main看起。

int main(void)
{
  /* Initialize hardware */
   HAL_BOARD_INIT();

  // Initialize board I/O
  InitBoard( 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 initialization
  InitBoard( OB_READY );

  #if defined ( POWER_SAVING )
    osal_pwrmgr_device( PWRMGR_BATTERY );
  #endif

  /* Start OSAL */
  osal_start_system(); // No Return from here

  return 0;
}

就几行代码,让我想起了知乎一个问题,一行代码可以实现什么。。。
回正题。帮你们排排坑,一堆Init的初始化先不用看,只看osal_init_system()这个初始化就好。右键-go to destination跳进函数看一下。
在这里插入图片描述

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()这一个就好。跳进去。

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 );
}

又TM是一堆初始化。还都看不到底层代码。。。除了最后一个。对,最后一个就是我们应用层初始化的重点。而且我们还可以发现每个初始化函数都传进去一个参数,还是依次递增的一个址。有意思。先不管。进SimpleBLEPeripheral_Init()里看一下。

有点长,就不贴代码了。
假装这里有代码。

可以看到里面就是对上面我们提到的协议栈函数进行了调用,实现了各种赋值。嗯。看来要修改一些初始参数就在这里改了。
函数执行到最后呢。做了什么。

  // Setup a delayed profile startup
  osal_set_event( simpleBLEPeripheral_TaskID, SBP_START_DEVICE_EVT );           //处理函数入口  

set 了个SBP_START_DEVICE_EVT 给simpleBLEPeripheral_TaskID。有啥用?往下看。

函数执行到这里看回main之后执行了什么

  /* Start OSAL */
  osal_start_system(); // No Return from here

注释写着不会从里面跳出来。那可得好好看看了。结果就一个osal_run_system()。。。行吧,我再跳。重点来了。

························重点分割线···················

通过解读我们了解了,osal是一个任务轮询机制,通过在osal_run_system()里的

do 
{ 
if (tasksEvents[idx]) 
{ 
break; 
} 
} while (++idx < tasksCnt); 

将触发了事件的任务挑选出来。然后通过

events = (tasksArr[idx])( idx, events ); 

执行任务与对应的事件。tasksArr[]的定义,里面都是函数指针。

const pTaskEventHandlerFn tasksArr[] = {
  macEventLoop,
  nwk_event_loop,
  Hal_ProcessEvent,
#if defined( MT_TASK )
  MT_ProcessEvent,
#endif
  APS_event_loop,
#if defined ( ZIGBEE_FRAGMENTATION )
  APSF_ProcessEvent,
#endif
  ZDApp_event_loop,
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
  ZDNwkMgr_event_loop,
#endif
  SampleApp_ProcessEvent
};

初始化的时候将函数指针在数组里对应的位置给了tasksEvents[]这个数组,这样通过哪个位置被置1就知道要执行哪个函数了。同时在osalInitTasks()这个函数里每个函数初始化传递进去一个TaskID,还是递增的,其实是与上面的tasksArr[]相对应。

tasksEvents这个数组在osalInitTasks()一开始赋值了全0。那么里面的元素是什么时候被设定为非零数?

OSAL专门建立了一个任务来对硬件资源进行管理,这个任务的事件处理函数是Hal_ProcessEvent(在hal_driver.c中)。以按键事件做说明的话:

  if (events & HAL_KEY_EVENT)
  {

#if (defined HAL_KEY) && (HAL_KEY == TRUE)
    /* Check for keys */
    HalKeyPoll();

    /* if interrupt disabled, do next polling */
    if (!Hal_KeyIntEnable)
    {
      osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);
    }
#endif // HAL_KEY

在这个函数中通过调用:

osal_start_timerEx( Hal_TaskID, HAL_KEY_EVENT, 100);

这个函数使得每隔100毫秒就会执行一次HalKeyPoll()函数。为什么这里会这样每隔100ms执行一次呢?因为100ms以后会将HAL_KEY_EVENT给Hal_TaskID,同时tasksArr[]里Hal_TaskID对应位会被置1,这样在执行do-while时会将该任务事件跳出来执行,又会回到上面的函数,如此循环。

HalKeyPoll()获取当前按键的状态。当检测到按键按下的时候:

if (keys && (pHalKeyProcessFunction))
  {
    (pHalKeyProcessFunction) (keys, HAL_KEY_STATE_NORMAL);
  }

会将按键传递给某个函数 pHalKeyProcessFunction,此处是函数指针,也就是回调函数。在

HalKeyConfig(HAL_KEY_INTERRUPT_DISABLE, OnBoard_KeyCallback);



void HalKeyConfig (bool interruptEnable, halKeyCBack_t cback)
{
    ····
    /* Register the callback fucntion */
  pHalKeyProcessFunction = cback;
  ·····
}

中pHalKeyProcessFunction该函数指针被赋值OnBoard_KeyCallback:

void OnBoard_KeyCallback ( uint8 keys, uint8 state )
{
    ····
    if ( OnBoard_SendKeys( keys, shift ) != ZSuccess )
    ····
}



uint8 OnBoard_SendKeys( uint8 keys, uint8 state )
{
    ·····
     msgPtr = (keyChange_t *)osal_msg_allocate( sizeof(keyChange_t) );
    if ( msgPtr )
    {
      msgPtr->hdr.event = KEY_CHANGE;
      msgPtr->state = state;
      msgPtr->keys = keys;

      osal_msg_send( registeredKeysTaskID, (uint8 *)msgPtr );
    }
    return ( ZSuccess );
    ·····
}

而OnBoard_KeyCallback中将按键状态打包成结构体send到registeredKeysTaskID,在主函数,registeredKeysTaskID已被注册为SampleApp_TaskID。既SampleApp_Task该任务会对key状态进行处理

RegisterForKeys( SampleApp_TaskID );


uint8 RegisterForKeys( uint8 task_id )
{
  // Allow only the first task
  if ( registeredKeysTaskID == NO_TASK_ID )
  {
    registeredKeysTaskID = task_id;
    return ( true );
  }
  else
    return ( false );
}

理一下流程:
在这里插入图片描述
重点是要理解do-while的轮询,通过函数指针的形式将各种参数类型一样的函数通过(tasksArr[idx])( idx, events )这样去调用。

消息和事件的联系:

事件是驱动任务去执行某些操作的条件,当系统产生了一个事件,将这个传递给相应的任务后,任务才能执行一个相应的操作。但是某些事件在它发生的同时,又伴随着一些附加信息的产生。任务的事件处理函数在处理这个事件的时候,还需要参考其附加信息。最典型的一类便是按键消息,它同时产生了一个哪个按键被按下了附加信息。所以在OnBoard_SendKeys这个函数中,不仅向SimpleBLEPeripheral发送了事件,还通过调用osal_msg_send函数向SimpleBLEPeripheral发送了一个消息,这个消息记录了这个事件的附加信息。在SimpleBLEPeripheral_ProcessEvent中,通过

{ 
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SimpleBLEPeripheral_TaskID ); 
}

获取了这样一个消息,然后再进一步处理。

自此osal操作系统与BLE协议栈就结束了。与此很相似的一套Zigbee协议栈Zstack也是在osal操作系统上实现的。见下文。

Zigbee协议

  • 我们还是先从协议讲起,再进协议栈看具体实现。

架构

在这里插入图片描述
上面的图可能有点复杂,我们简化一下

Application Layer
Application Support Sublayer
Network Layer
Medium Access Control Layer
Physical Layer

相比蓝牙4.0,zigbee的协议就相对简单一些,当然也是分为几个协议层。

物理层(PHY)
  • 简单了解

物理层一样是通信硬件部分。
物理层频率范围: 868/915 MHz 和 2.4 GHz。2.4 GHz波段射频可以提供250kbps的数据速率和16个不同的信道。868/915MHz波段中,868MHz支持1个数据速率为20kbps的信道,915MHz支持10个数据速率为40kbps的信道。

MAC子层
  • 简单了解

MAC子层通过CSMA/CA机制控制无线电信道。它的职责还可以包括发送信标帧、同步和提供可靠的传输机制。MAC层负责相邻设备间的单跳数据通信。每个cc2530出厂都有全球唯一一个32位的MAC地址。不可改。

网络层(NWK)
  • 简单了解

网络层提供相应的功能以确保MAC子层的正确操作并为应用层提供合适的服务接口。为了给应用层提供接口,网络层在概念上包括两个提供必要功能的服务实体——NWK层数据实体(NLDE)和NWK层管理实体(NLME)。NLDE-SAP提供数据传输服务,NLME-SAP提供管理服务。NLME利用NLDE来实现其一些管理任务,并且还维护一个称为网络信息库(NIB)的托管对象数据库。

NWK支持星型、树型和网状拓扑。星型网络由单一设备coordinator(协调器)控制。Coordinator的职责主要是初始化和维护设备在网络中。其他装置都称为end device(终端),直接与coordinator通信。树状和网状网络中,coordinator负责创建网络并选择一些关键网络参数。网络可通过router进行扩展。在树状网络中,router(路由)通过分层路由策略传递数据和控制信息。树状网络可以采用信标定向通信,如IEEE 802.15.4规范中所描述。网状网络允许对等实体间的通信,Router当前不发射常规IEEE 802.15.4信标。该规范只描述intra-PAN networks,即通信在同一网络内开始和终止的网络。

APS子层(ZigBee Application Support Sub-Layer)

介于网络层和应用层之间,指定了应用层提供服务规范和接口的部分,该规范定义了允许应用对象传输数据的数据服务,以及提供绑定的管理服务。

应用层(APL)

应用层为体系结构的顶层,包括应用支持子层(APS)、ZigBee 设备对象(ZDO)和制造商所定义的应用对象(在应用框架层)。该层就是我们要操作的对象。

Zstack

  • 下载链接:http://www.ti.com.cn/tool/cn/z-stack
    Zigbee的协议没什么很值得注意的,因为Zstack已经封装得很好了。基本思路和BLE一样,就不多做介绍。
协议栈使用流程
组网

首先要知道的是。在ZigBee网络中的节点主要包含三个:终端节点、路由器节点、PAN协调器节点。

协调器节点(coordinator)

是启动和配置网络的一种设备,是网络的中心节点,一个ZigBee网络只允许有一个ZigBee协调器。

第一个节点一定是该网络的协调器,且一个zigbee网络中有且只有一个网络协调器。一旦网络建立好了,协调器就退化成路由器的角色,甚至是可以去掉协调器的,这一切得益于ZigBee网络的分布式特性。

路由器节点(router)

是一种支持关联的设备,能够将消息转发到其他设备

终端节点(end device)

执行具体功能的设备。

对于设备在网络中作为什么节点存在,主要取决于烧录时如下的设置
在这里插入图片描述

组建一个完整的zigbee网状网络包括两个步骤:网络初始化、节点加入网络。其中节点加入网络又包括两个步骤:通过与协调器连接入网和通过已有父节点入网。

当设备连入网络中的时候,每个设备都能获得由协调器分配的16 位短地址,协调器默认地址(0x0000)

发送
afStatus_t AF_DataRequest( afAddrType_t *dstAddr, endPointDesc_t *srcEP,
                           uint16 cID, uint16 len, uint8 *buf, uint8 *transID,
                           uint8 options, uint8 radius )
                           
param:

*dstAddr:目的设备的全地址(Nwk地址 + End Point)

*srcEP:起始EndPoint描述

cID:按照profile说明的有效群集ID(cluster ID)

len:下个参数指向的数据空间大小

*buf:指向发送数据的指针

*transID:指向一个可改的序列号字节

options:数据类型说明

radius:通常设置为AF_DEFAULT_RADIUS (传输半径???)
接收

当有数据发送过来时,会使得SampleApp_ProcessEvent对应位被置1,并传输一个AF_INCOMING_MSG_CMD的事件,通过处理不同的cluster ID实现对不同值操作。示例:

case AF_INCOMING_MSG_CMD:
          SampleApp_MessageMSGCB( MSGpkt );
          break;


void SampleApp_MessageMSGCB( afIncomingMSGPacket_t *pkt )
{
  uint16 flashTime;

  switch ( pkt->clusterId )
  {
    case SAMPLEAPP_POINT_TO_POINT_CLUSTERID:
      HalUARTWrite(0,"I get data\n",11);//用于提示有数据
      HalUARTWrite(0, &pkt->cmd.Data[0],10); //打印收到数据
      HalUARTWrite(0,"\n",1);  //回车换行,便于观察
      break;

    case SAMPLEAPP_FLASH_CLUSTERID:
      flashTime = BUILD_UINT16(pkt->cmd.Data[1], pkt->cmd.Data[2] );
      HalLedBlink( HAL_LED_4, 4, 50, (flashTime / 4) );
      break;
  }
}

通讯方式

通讯类型的设置,由下面的结构体决定

typedef struct

{
  union
  {
    uint16      shortAddr;          //通信地址,为0x0000时发给协调器
    ZLongAddr_t extAddr;
  } addr;
  afAddrMode_t addrMode;        //通信模式
  uint8 endPoint;
  uint16 panId;  // used for the INTER_PAN feature
} afAddrType_t;

通信模式那里又是一个结构体afAddrMode_t

typedef enum
{
  afAddrNotPresent = AddrNotPresent,
  afAddr16Bit      = Addr16Bit,         点播方式
  afAddr64Bit      = Addr64Bit,         
  afAddrGroup      = AddrGroup,         组播方式
  afAddrBroadcast  = AddrBroadcast      广播方式
} afAddrMode_t;
点播

两个设备间的通信,不容许第三个设备收到信息

参数设置示例如下:

  // Setup for the Point to Point's destination address
  Point_To_Point_DstAddr.addrMode = (afAddrMode_t)Addr16Bit;    //点播模式
  Point_To_Point_DstAddr.endPoint = SAMPLEAPP_ENDPOINT;
  Point_To_Point_DstAddr.addr.shortAddr = 0x0000;   //协调器地址
组播

把网络中的节点分组,每一个组员发出的信息只有相同组号的组员能收到。

分组内容结构体

typedef struct
{
  uint16 ID;                       // Unique to this table
  uint8  name[APS_GROUP_NAME_LEN]; // Human readable name of group
} aps_Group_t;

参数设置示例如下:

  // Setup for the flash command's destination address - Group 1
  SampleApp_Flash_DstAddr.addrMode = (afAddrMode_t)afAddrGroup;     //组播模式
  SampleApp_Flash_DstAddr.endPoint = SAMPLEAPP_ENDPOINT;
  SampleApp_Flash_DstAddr.addr.shortAddr = SAMPLEAPP_FLASH_GROUP;   //组号

Add a group for an endpoint

  // By default, all devices start out in Group 1
  SampleApp_Group.ID = SAMPLEAPP_FLASH_GROUP;   //0x0001
  osal_memcpy( SampleApp_Group.name, "Group 1", 7  );
  aps_AddGroup( SAMPLEAPP_ENDPOINT, &SampleApp_Group );

组播时终端设备不参与,因为终端默认工作在睡眠中断状态,可通过将f8config.cfg里的-RFD_RCVC_ALWAYS_ON=FALSE改为-RFD_RCVC_ALWAYS_ON=TRUE实现终端组播.

广播

一个设备发出的信息其他设备都能接收到。

参数设置示例如下:

  // Setup for the periodic message's destination address       //广播模式
  // Broadcast to everyone
  SampleApp_Periodic_DstAddr.addrMode = (afAddrMode_t)AddrBroadcast;
  SampleApp_Periodic_DstAddr.endPoint = SAMPLEAPP_ENDPOINT;
  SampleApp_Periodic_DstAddr.addr.shortAddr = 0xFFFF;       //广播地址

协议栈的广播地址主要有3种类型,如下:

0xFFFF——数据包将被传送到网络上的所有设备,
包括睡眠中的设备。对于睡眠中的设备,数据包将被保留在其父亲节点直到查询
到它,或者消息超时。
0xFFFD——数据包将被传送到网络上的所有在空闲时
打开接收的设备(RXONWHENIDLE),也就是说,除了睡眠中的所有设备。
0xFFFC——数据包发送给所有的路由器,包括协调器。
设备开启
Start the device in the network.  This function will read
 *   ZCD_NV_STARTUP_OPTION (NV item) to determine whether or not to


@param   startDelay - timeDelay to start device (in milliseconds).
 *                       There is a jitter added to this delay:
 *                       ((NWK_START_DELAY + startDelay)
 *                       + (osal_rand() & EXTENDED_JOINING_RANDOM_MASK))
 *                       When startDelay is set to ZDO_INIT_HOLD_NWK_START
 *                       this function will hold the network init. Application
 *                       can start the device.

NOTE:    If the application would like to force a "new" join, the
 *          application should set the ZCD_STARTOPT_DEFAULT_NETWORK_STATE
 *          bit in the ZCD_NV_STARTUP_OPTION NV item before calling
 *          this function. "new" join means to not restore the network
 *          state of the device. Use zgWriteStartupOptions() to set these
 *          options.

uint8 ZDOInitDevice( uint16 startDelay )

Zstack大部分产生的回调都是在SampleApp_ProcessEvent()中对传进来的事件信息进行判断操作,相对容易一些。
示例代码如下:

  if ( events & SYS_EVENT_MSG )
  {
    MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SampleApp_TaskID );
    while ( MSGpkt )
    {
      switch ( MSGpkt->hdr.event )
      {
        // Received when a key is pressed
        case KEY_CHANGE:
          SampleApp_HandleKeys( ((keyChange_t *)MSGpkt)->state, ((keyChange_t *)MSGpkt)->keys );
          break;

        // Received when a messages is received (OTA) for this endpoint
        case AF_INCOMING_MSG_CMD:
          SampleApp_MessageMSGCB( MSGpkt );
          break;

        // Received whenever the device changes state in the network
        case ZDO_STATE_CHANGE:
          SampleApp_NwkState = (devStates_t)(MSGpkt->hdr.status);
          if ( //(SampleApp_NwkState == DEV_ZB_COORD)|| //协调器不给自己点播
               (SampleApp_NwkState == DEV_ROUTER)
              || (SampleApp_NwkState == DEV_END_DEVICE) )
          {
            // Start sending the periodic message in a regular interval.
            osal_start_timerEx( SampleApp_TaskID,
                              SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
                              SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT );
          }
          else
          {
            // Device is no longer in the network
          }
          break;

        default:
          break;
      }

      // Release the memory
      osal_msg_deallocate( (uint8 *)MSGpkt );

      // Next - if one is available
      MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SampleApp_TaskID );
    }

    // return unprocessed events
    return (events ^ SYS_EVENT_MSG);
  }

文章参考链接:
猩猩女友:https://blog.csdn.net/zhengyachen2018/article/details/81204669

wowo:http://www.wowotech.net/bluetooth/ble_stack_overview.html


  1. 协议的每一个层都可能会有自己的状态state(或是角色role),名字可能会一样,在看协议栈的时候,一定不能搞混。 ↩︎

  2. 有些函数官方封装好了,不提供我们底层的代码,会用就行。 ↩︎

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值