- 本文对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(物理层)
- 简单了解
- 既只与通信相关的硬件部分。
- BLE的通信频段范围为2.400-2.4835 GHz。
- 为了同时支持多个设备,将整个频带分为40份,每份的带宽为2MHz,称作RF Channel(Physical Channel)。
Link Layer (连接层)
- 简单了解
Link Layer用于解决Physical Channel的共享问题,分两种场景适配
-
对于数据量比较少,发送不频繁,对时延不是很敏感的场景:
这种数据场景,BLE的Link Layer采取了一种比较懒的处理方式——广播通信:从40个Physical Channel中选取3个,作为广播通道,在广播通道上,任何参与者,爱发就发,爱收就收,随便;所有参与者,共享同一个逻辑传输通道(广播通道)
-
数据量较大,发送频率较高,对时延较敏感的场景
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。
L2CAP(Logical Link Control and Adaptation Protocol)
-
简单了解
一个介于应用程序(Profile、Application等)和Link Layer之间的协议。
将Logical Channel划分为一个个的L2CAP Channel,以便提供应用程序级别的通道复用。
ATT(Attribute Protocol)
- 重点
为了实现信息的采集,BLE将信息以Attribute(属性)的形式抽象出来,并提供一些方法,供远端设备(remote device)读取,修改这些属性的值(Attribute value)。
其实现的主要思路是
-
基于L2CAP,使用固定的Channel ID(0x004)
-
采用client-server的形式.
提供信息(以后都称作Attribute)的一方称作ATT server(一般是那些传感器节点),访问信息的一方称作ATT client。 -
一个Attribute由Attribute Type、Attribute Handle和Attribute Value组成
-
Attribute Type
用于标识Attribute的类型,类似于我们常说的"温度","湿度"等人类可识别的术语,不过与人类术语不同的是,Attribute Type使用UUID(Universally Unique IDenifier,16-bit,32-bit或者128-bit的数值)区分. -
Attribute Handle
一个16-bit的数值,用作唯一识别Attribute server上的所有Attribute。 -
Attribute Value
代表Attribute的值,可以是任何固定长度或者可变长度的字节数组
-
-
权限(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。
-
Profile
由一个或者多个和某一应用场景有关的Service组成。 -
service
一个Service 包含一个或多个Characteristic(特征),也可以通过include的方式,包含其它Service。 -
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
},
已连接的两台设备之间存在的一层协议层,存在以下两种状态:
-
GATT Client(客户端)
向Server端写入或读取数据的设备。可同时包含多个services。 -
GATT Server(服务器端)
被Client写入或读取数据的设备。
GAP(Generic Access)
- 重点
用于解决无连接的advertising channel,以及连接建立的过程的一层协议
GAP层包含四种角色(role)
- broadcaster
表示设备正在发送advertising events,处于无连接状态的advertiser - Observer
设备正在接收advertising events,搜寻广播,但不能发起连接。 - Peripheral
设备接受Link Layer连接(对应Link Layer的slave角色)。 - 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协议
- 我们还是先从协议讲起,再进协议栈看具体实现。
架构
上面的图可能有点复杂,我们简化一下
相比蓝牙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