CC2530 芯片由 TI 出品,在硬件上和支持 ZigBee 协议;同时 TI 提供的 ZStack 协议栈则是一套符合 ZigBee 协议的协议栈,本章所讲的CC2530 协议栈即为 TI ZStack 协议栈。
2007 年1月,TI 公司宣布推出 ZigBee 协议栈(Z-Stack),并于 2007年4月提供免费下载版本 V1.4.1。Z-Stack 达到 ZigBee 测试机构德国莱茵集团(TUV Rheinland)评定的 ZigBee 联盟参考平台(Golden Unit)水平,目前已为全球众多 ZigBee 开发商所广泛采用。Z-Stack 符合 ZigBee 2006 规范,支持多种平台,其中包括面向 IEEE 802.15.4/ZigBee 的 CC2430 片上系统解决方案、基于 CC2420 收发器的新平台以及 TI 公司的 MSP430 超低功耗微控制器(MCU)。
除了全面符合 ZigBee 2006 规范以外,Z-Stack 还支持丰富的新特性,如无线下载,可通过ZigBee 网状网络(Mesh Network)无线下载节点更新。Z-Stack 还支持具备定位感知(Location Awareness)特性的 CC2431。上述特性使用户能够设计出可根据节点当前位置改变行为的新型ZigBee 应用。
Z-Stack 与低功耗 RF 开发商网络,是 TI 公司为工程师提供的广泛性基础支持的一部分,其他支持还包括培训和研讨会、设计工具与实用程序、技术文档、评估板、在线知识库、产品信息热线以及全面周到的样片供应服务。
2007年7月,Z-Stack 升级为 V1.4.2,之后对其进行了多次更新,并于 2008 年 1 月升级为V1.4.3。2008 年 4 月,针对 MSP430F4618+CC2420 组合把 Z-Stack 升级为 V2.0.0;2008 年 7月,Z-Stack 升级为 V2.1.0,全面支持 ZigBee 与 ZigBee PRO 特性集(即 ZigBee2007/Pro)并符合最新智能能源规范,非常适用于高级电表架构(AMI)。因其出色的 ZigBee 与 ZigBee Pro 特性集,Z-Stack 被ZigBee 测试机构国家技术服务公司(NTS)评为 ZigBee 联盟最高业内水平。2009 年 4 月,Z-Stack 支持符合 2.4GHz IEEE 802.15.4 标准的第二代片上系统 CC2530;2009 年 9 月,Z-Stack 升级为 V2.2.2,之后,于 2009 年 12 月升级为 V2.3.0 ;2010 年 5 月,Z-Stack 升级为 V2.3.1。
Z-Stack 协议栈由 TI 公司出品,符合最新的 ZigBee2007 规范。它支持多平台,其中就包括CC2530 芯片。ZStack 的安装包为 ZStack-CC2530-2.5.1a.exe,双击之后直接安装(傻瓜式安装) ,安装完后生成 C:\Texas Instruments\ZStack-CC2530-2.5.1a 文件夹,文件夹内包括协议栈中各层部分源程序(有一些源程序被以库的形式封装起来了),Documents 文件夹内包含一些与协议栈相关的帮助和学习文档, Projects 包含与工程相关的库文件、配置文件等, 其中基于 ZStack 的工程应放在 C:\Texas Instruments\ZStack-CC2530-2.5.1a\Projects\zstack\Samples 文件夹下。
打开 ZStack 协议栈提供的示例工程,可以看到如图所示的层次结构图
从层次的名字就能知道代表的含义,比如 NWK 层就是网络层。一般应用中较多关注的是HAL 层(硬件抽象层)和 App 层(用户应用),前者要针对具体的硬件进行修改,后者要添加具体的应用程序。而 OSAL 层是 ZStack 特有的系统层,相当于一个简单的操作系统,便于对各层次任务的管理,理解它的工作原理对开发是很重要的,下面对各层进行简要介绍:
在 ZStack 协议栈中各层次具有一定的关系,如图所示是 ZStack 协议栈的体系结构图
TI Z-Stack 协议栈是一个基于轮转查询式的操作系统,它的 main 函数在 ZMain 目录下的ZMain.c 中,该协议栈总体上来说,一共做了两件工作,一个是系统初始化,即由启动代码来初始化硬件系统和软件构架需要的各个模块,另外一个就是开始启动操作系统实体,如图所示
系统启动代码需要完成初始化硬件平台和软件架构所需的要的各个模块,微操作系统的运行做好准备工作,主要分为初始化系统时钟,检测芯片工作电压,初始化堆栈,初始化各个硬件模块, 初始化 FLASH 存储,形成芯片 MAC 地址,初始化非易失变量,初始化 MAC 层协议,初始化应用帧层协议,初始化操作系统等十余部分,其具体流程图和对应的函数如图所示
系统初始化为操作系统的运行做好准备工作以后,就开始执行操作系统入口程序,并由此彻底将控制权交给操作系统,其实,启动操作系统实体只有一行代码
osal_start_system();
该函数没有返回结果,通过将该函数一层层展开之后就知道该函数其实就是一个死循环。这个函数就是轮转查询式操作系统的主体部分,他所做的就是不断地查询每个任务是否有事件发生,如果发生,执行相应的函数,如果没有发生,就查询下一个任务。
zigbee 无线通信中一般含有 3 种节点类型,分别是协调器、路由节点和终端节点。本实验指导书打开 ZStack 协议栈官方提供的实验例子工程可以在IAR 开发环境下的workspace 下拉列表中选择设备类型,可以选择设备类型为协调器,路由器或终端节点(根据开发板对应的类型选择)
对于一个特定的工程,编译选项存在于两个地方,一些很少需要改动的编译选项存在于连接控制文件中,每一种设备类型对应一种连接控制文件,当选择了相应的设备类型后,会自动选择相应的配置文件,如选择了设备类型为终端节点后f8wEndev.cfg 和 f8w2530.xcl、f8wConfig.cfg 配置文件被自动选择,如图 所示;选择了设备类型为协调器,则工程会自动选择 f8wCoord.cfg 和f8w2530.xcl、f8wConfig.cfg 配置文件;选择了设备类型为路由器后 f8wRouter.cfg 和 f8w2530.xcl、f8wConfig.cfg 配置文件被自动选择
其实这些文件中定义的就是一些工程中常用到的宏定义,由于这些文件用户基本不需要改动, 所以在此不作介绍,用户可参考 ZStack 的帮助文档。
在 ZStack 协议栈的例程开发时,有时候需要自定义添加一些宏定义来使能/禁用某些功能,这些宏定义的选项在 IAR 的工程文件中,下面进行简要介绍。
在 IAR 工程中选择 Project/Options/C/C++ Complier 中的 Processor 标签,如图所示所示
上图 中“Defined symbols”输入框中就是宏定义的编译选项。若想在这个配置中增加一个编译选项,只需将相应的编译选项添加到列表框中,若禁用一个编译选项,只需在相应编译选项的前面增加一个 x ,如上图所示 POWER_SAVING 选项被禁用,这一编译选项表示支持省电模式。很多编译选项都作为开关量使用,来选择源程序中的特定程序段,也可定义数字量,如可添加DEFAULT_CHANLIST 即相应数值来覆盖默认设置(DEFAULT_CHANLIST 在 Tools 目录下的f8wConfig.cfg 文件中配置,默认选择信道 11)。ZStack 协议栈支持大量的编译选项,读者可参考 ZStack 的帮助文档Z-Stack Compile Options.pdf
ZStack 中定义了两种地址,64 位的扩展地址(IEEE 地址)和 16 位网络短地址。扩展地址是全球唯一的,就像网卡地址,可由厂家设置或者用户烧写进芯片(本实验指导书配套的节点用 RF Flash Programmer 就可以完成)。网络短地址是加入 ZigBee 网时,由协调器分配,在特定的网络中是唯一的但是不一定每次都是一样,只是和其他同网设备相区别,作为标识符。
ZStack 符合 ZigBee 的分布式寻址方案来分配网络地址。这个方案保证在整个网络中所有分配的地址是唯一的。这一点是必须的,因为这样才能保证一个特定的数据包能够发给它指定的设备, 而不出现混乱。同时,这个寻址算法本身的分布特性保证设备只能与他的父辈设备通讯来接受一个网络地址。不需要整个网络范围内通讯的地址分配,这有助于网络的可测量性 Z-Stack 的网络地址分配由三个参数决定 MAX_DEPTH,MAX_ROUTERS 和 MAX_CHILDREN,这也是 profile 的一部分。MAX_DEPTH 代表网络最大深度,协调器为 0 级深度,它决定了物理上网络的―长度; MAX_CHILDREN 决定了一个协调器或路由器能拥有几个子节点;MAX_ROUTERS 决定了一个协调器或路由器能拥有几个路由功能的节点,它是 MAX_CHILDREN 的子集。虽然不同的 profile 有规定的参数值,但用户针对自己的应用可以修改这些参数,但要保证这些参数新的赋值要合法。即, 整个地址空间不能超过 216,这就限制了参数能够设置的最大值。当选择了合法的数据后,开发人员还要保证不再使用标准的栈配置,取而代之的是网络自定义栈配置( 例如:在 nwk_globals.h 文件中将 STACK_PROFILE_ID 改为 NETWORK_SPECIFIC) 。然后 nwk_globals.h 文件中的MAX_DEPTH 参数将被设置为合适的值。此外,还必须设置 nwk_globals.c 文件中的 Cskipchldrn 数组和 CskipRtrs 数组。这些数组的值由MAX_CHILDREN 和 MAX_ROUTER 构成。
为了在 ZigBee 网络中发送数据,应用层主要调用 AF_DataRequest()函数。目的设备由类型afAddrType_t 决定,定义如下
typedef struct
{
union
{
uint16 shortAddr;
ZLongAddr_t extAddr;
} addr;
afAddrMode_t addrMode;
uint8 endPoint;
uint16 panId; // used for the INTER_PAN feature
} afAddrType_t;
其中寻址模式有几种不同的方式,具体定义是:
typedef enum
{
afAddrNotPresent = AddrNotPresent,
afAddr16Bit = Addr16Bit,
afAddr64Bit = Addr64Bit,
afAddrGroup = AddrGroup,
afAddrBroadcast = AddrBroadcast
} afAddrMode_t;
下面是针对寻址的模式进行简要介绍:
路由对应用层是透明的,应用层只需要知道地址而不在乎路由的过程。Z-Stack 的路由实现了ZigBee 网络的自愈机制,一条路由损坏了,可以自动寻找新的路由。
无线自组织网络(Ad-hoc )中有很多著名的路由技术,其中 AODV 是很常用的一种,AODV 是按需路由协议。Z-Stack 简化了 AODV,使之适应于无线传感器网络的特点,能在有移动节点、链路失效和丢包的环境下工作。当路由器从应用层或其他设备收到单播的包时,网络层根据下列步骤转发:如果目的地是自己的邻居,就直接传送过去。否则,该路由器检查路由表寻找目的地,如果找到了就发给下一跳,没找到就开始启动路由发现过程,确定了路由之后才发过去。路由发现基本按照 AODV 的算法进行,请求地址的源设备向邻居广播路由请求包(RREQ),收到 RREQ 的节点更新链路花费域,继续广播路由请求。这样,直到目的节点收到 RREQ,此时的链路花费域可能有几个值,对应不同的路由,选择一条最好的作为路由。 然后目的设备发送路由应答包(RREP ), 反向到源设备,路径上其他设备由此更新自己的路由表。这样一条新的路由就建成了。
为了方便任务管理,Z-Stack 协议栈定义了 OSAL 层(Operation System Abstraction Layer, 操作系统抽象层)。OSAL 完全构建在应用层上,主要是采用了轮询的概念,并且引入了优先级。它的主要作用是隔离 Z-Stack 协议栈和特定硬件系统,用户无须过多了解具体平台的底层,就可以利用操作系统抽象层提供的丰富工具实现各种功能,包括任务注册、初始化和启动,同步任务,多任务间的消息传递,中断处理,定时器控制,内存定位等。
OSAL 中判断事件发生是通过 tasksArr[idx]任务事件数组来进行的。在 OSAL 初始化的时候, tasksArr[] 数组被初始化为零。不同的任务有不同的 taskID ,这样任务事件数组tasksArr中就表示了系统中哪些任务存在没有处理的事件。然后就会调用各任务处理对应的事件,任务是 OSAL 中很重要的概念。任务通过函数指针来调用,参数有两个:任务标识符(taskID ) 和对应的事件(event)。Z-Stack 中有9种默认的任务,它们存储在 taskArr 这个函数指针数组中。定义如下
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
GenericApp_ProcessEvent
};
从 9 个事件的名字就可以看出,每个默认的任务对应着的是协议的层次。而且根据 ZStack 协议栈的特点,这些任务从上到下的顺序反映出了任务的优先级,如 MAC 事件处理 macEventLoop 的优先级高于网路层事件处理 nwk_event_loop 。
要深入理解ZStack 协议栈中 OSAL 的调度管理关键是要理解任务的初始化 osalInitTasks()、任务标识符 taskID、任务事件数组 tasksEvents、任务事件处理函数 tasksArr 数组之间的关系。
图中是系统任务、任务标识符和任务事件处理函数之间的关系。其中 tasksArr 数组中存储了任务的处理函数,tasksEvents 数组中则存储了各任务对应的事件,由此便可得知任务与事件之间是多对多的关系,即多个任务对应着多个事件。系统调用 osalInitTasks()函数进行任务初始化时首先将 taskEvents 数组的各任务对应的事件置 0,也就是各任务没有事件。当调用了各层的任务初始化函数之后,系统就会调用 osal_set_event(taskID,event)函数将各层任务的事件存储到 taskEvent 数组中。系统任务初始化结束之后就会轮询调用 osal_run_system()函数开始运行系统中所有的任务, 运行过程中任务标识符值越低的任务优先运行。执行任务的过程中,系统就会判断各任务对应的事 件是否发生,若发生了则执行相应的事件处理函数。关于 OSAL 系统的任务之间调度的源码分析可以查看《Zstack协议栈工程解析实验》文档内容。
根据上述的解析过程可知系统是按照死循环形式工作的,模拟了通常的多任务操作系统,把CPU 分成N 个时间片,在高速的频率下感觉就是同时运行多个任务了。
串口通信的目的是协调器把整个网络的信息发给上位机进行可视化和数据存储等处理。 同时在开发阶段非常需要有串口功能的支持,以了解调试信息。Z-Stack 已经把串口部分的配置简单化了, 设置的位置是mt_uart.c 的 MT_UartInit ()函数。配置方法是给 uartConfig 这一结构体赋值,它包括了波特率、缓冲区大小,回调函数等参数。需要注意的有几个参数:
波特率,赋值为宏MT_UART_DEFAULT_BAUDRATE,进一步跟踪查询可知就是38400 Baud;这决定了和上位机通信的速率。流控,默认是打开的,本项目没有使用,改为关闭。回调函数,在主动控制模块中会用到
每一个设备都必须有一个 DEFAULT_CHANLIST 来控制信道集合。对于一个 ZigBee 协调器来说,这个表格用来扫描噪音最小的信道。对于终端节点和路由器几点来说,这个列表用来扫描并加入一个存在的网络。
这个可选配置项用来控制 ZigBee 路由器和终端节点要加入哪个网络。文件f8wConfg.cfg 中的ZDO_CONFIG_PAN_ID 参数可以设置为一个 0~0x3FFF 之间的一个值。协调器使用这个值,作为它要启动的网络的 PANID。而对于路由器节点和终端节点来说只要加入一个已 经用这个参数配置了 PAN ID 的网络。如果要关闭这个功能,只要将这个参数设置为 0xFFFF。要更进一步控制加入过程,需要修改 ZDApp.c 文件中的 ZDO_NetworkDiscoveryConfirmCB 函数。
对于一个应用程序最大有效载荷的大小基于几个因素。MAC 层提供了一个有效载荷长度常数102。NWK 层需要一个固定头大小,一个有安全的大小和一个没有安全的大小。APS 层必须有一个可变的基于变量设置的头大小,包括 ZigBee 协议版本,KVP 的使用和 APS 帧控制设置等等。最后,用户不必根据前面的要素来计算最大有效载荷大小。AF 模块提供一个 API,允许用户查询栈的最大有效载荷或者最大传送单元(MTU) 。用户调用函数 afDataReqMTU(见 af.h 文件),该函数将返回MTU 或者最大有效载荷大小
typedef struct
{
uint8 kvp;
APSDE_DataReqMTU_t aps;
} afDataReqMTU_t;
uint8 afDataReqMTU( afDataReqMTU_t* fields )
通常 afDataReqMTU_t 结构只需要设置 kvp 的值,这个值表明 KVP 是否被使用,而 aps 保留。
ZigBee 设备有许多状态信息需要被存储到非易失性存储空间中,这样能够让设备在意外复位或者断电的情况下复原。否则它将无法重新加入网络或者起到有效作用。 为了启用这个功能,需要包含 NV_RESTORE 编译选项。注意,在一个真正的 ZigBee 网络中,这个选项必须始终启用。关闭这个选项的功能也仅仅是在开发阶段使用。ZDO 层负责保存和恢复网络层最重要的信息,包括最基本的网络信息(Network Information Base NIB,管理网络所需要的最基本属性);儿子节点和父亲节点的列表;包含应用程序绑定表。此外,如果使用了安全功能,还要保存类似于帧个数这样信息。当一个设备复位后重新启动,这类信息恢复到设备当中。如果设备重新启动,这些信息可以使设备重新恢复到网络当中。
在 ZDAPP_Init 中,函数 NLME_RestoreFromNV() 的调用指示网络层通过保存在 NV 中的数据重新恢复网络。如果网络所需的 NV 空间没有建立,这个函数的调用将同时初始化这部分 NV 空间。NV 同样可以用来保存应用程序的特定信息,用户描述符就是一个很好的例子。NV 中用户描述符 ID 项是 ZDO_NV_USERDESC( 在 ZComDef.h 中定义)。ZDApp_Init() 函数中, 调用函数osal_nv_item_init()来初始化用户描述符所需要的NV 空间。如果这个针对这个 NV 项,这个函数是第 一次调用 , 这个 初始化函 数将为用 户描述 符保留空 间, 并 且将它设 置为默认 值ZDO_DefaultUserDescriptor 。 当需要使用保存在 NV 中的用户描述符时,就像ZDO_ProcessUserDescReq()(在 ZDObject.c 中)函数一样,调用 osal_nv_read()函数从 NV 中获取用户描述符。如果要更新 NV 中的用户描述符,就像ZDO_ProcessUserDescSet()(在 ZDObject.c 中)函数一样,调用 osal_nv_write()函数更新 NV 中的用户描述符。记住:NV 中的项都是独一无二的。如果用户应用程序要创建自己的 NV 项,那么必须从应用值范围 0x0201~0x0FFF 中选择 ID