在实际ZigBee无线传感器网络工程的开发过程中,首先借助TI提供的协议栈中例程SampleApp,接着根据需要完成的功能,查看支持Z-Stack协议栈的硬件电路图,再查阅数据手册(CC2530的数据手册、Z-Stack协议栈说明、Z-Stack协议栈API函数使用说明等)文件,然后再进行协议栈的修改。最后,还需要烧录器下载到相应的硬件,实现ZigBee无线传感器网络的组建和开发。
协议定义的是一系列的通信标准,通信双方需要共同按照这一标准进行正常的数据收发;协议栈是协议的具体实现形式,可通俗地理解为代码实现的函数库,以便于开发人员调用。
ZigBee的协议分为两部分,IEEE802.15.4定义了物理层和数据链路层技术规范,ZigBee联盟定义了网络层、安全层和应用层技术规范,ZigBee协议栈就是将各层定义的协议都集合在一起,以函数的形式实现,并提供一些应用层API供用户调用,如图3.1所示。
图3.1ZigBee协议栈示意图
协议栈是指网络中各层协议的总和,一套协议的规范。其形象地反映了一个网络中文件传输的过程:由上层协议到底层协议,再由底层协议到上层协议。
使用最广泛的是因特网协议栈,由上到下的协议分别是:应用层(Http、Telnet、DNS、E-mail等),运输层(TCP、UDP),网络层(IP),链路层(WI-FI、以太网、令牌环、FDDI等)。
ZigBee协议栈开发的基本思路如下。
(1)借助TI提供的协议栈中例程SampleApp进行二次开发,用户不需要深入研究复杂的ZigBee协议栈,这样可以减轻开发者的工作量。
(2)ZigBee无线传感器网络中数据采集,只需要用户在应用层加入传感器的读取函数和添加头文件即可实现。
(4)协调器(网关)根据下发的控制命令,将控制信息转发到具体的节点即控制节点,等待控制命令下发。
SampleApp.c中定义了发送函数staticvoidSampleApp_SendTheMessage(void)。该函数通过调用AF_DataRequest来发送数据。该函数定义在Profile目录下的AF.c文件中,如图3.2所示。
图3.2AF_DataRequest函数定义示意图
afStatus_tAF_DataRequest(afAddrType_t*dstAddr,endPointDesc_t*srcEP,uint16cID,uint16len,uint8*buf,uint8*transID,uint8options,uint8radius)用户调用该函数即可实现数据的无线发送。该函数中有8个参数,参数具体含义如下。
*dstAddr:发送目的地址+端点地址(端点号)和传送模式。
*srcEP:源(答复或确认)终端的描述(如操作系统中任务ID等)源EP。
cID:被Profile指定的有效的集群号。
len:发送数据长度。
*buf:指向存放发送数据的缓冲区的指针。
*transID:任务ID号。
options:有效位掩码的发送选项。
Radius:发送跳数,通常设置为AF_DEFAULT_RADIUS。
其中,最核心的两个参数是uint16len(发送数据的长度)和uint8*buf(指向存放发送数据的缓冲区的指针)。使用ZigBee协议栈只需调用相应的数据发送、接收函数即可。
介质访问控制层(MAC层)提供点对点通信的数据确认(Per-hopAcknowledgments)以及一些用于网络发现和网络形成的命令,但是介质访问控制层不支持多跳(Multi-hop)、网型网络(Mesh)等概念。
网络层(NWK层)主要负责设备加入和退出网络、路由管理、在设备之间发现和维护路由、发现邻设备及存储邻设备信息等。例如,在网络范围内发送广播包,为单播数据包选择路由,确保数据包能够可靠地从一个节点发送到另一个节点,此外,网络层还具有安全特性,用户可以自行选择所需的安全策略。
每一个ZigBee设备有一个64位IEEE地址,即MAC地址,跟网卡MAC一样,是全球唯一的。但在实际网络中,为了方便,通常用16位的短地址来标识自身和识别对方,也称为网络地址。对于协调器来说,短地址为0000H;对于路由器和节点来说,短地址是由它们所在网络中的协调器分配的。
网络地址分配由网络中的协调器来完成,为了让网络中的每一个设备都有唯一的网络地址(短地址),它要按照事先配置的参数,并遵循一定的算法来分配。这些参数是MAX_DEPTH、MAX_ROUTERS和MAX_CHILDREN。
MAX_DEPTH决定了网络的最大深度。协调器位于深度为0,其子节点位于深度为1,子节点的子节点位于深度为2,以此类推。MAX_DEPTH参数限制了网络在物理上的长度。
MAX_CHILDREN决定了一个路由器或者一个协调器节点可以连接的子节点的最大个数。MAX_ROUTERS决定了一个路由器或者一个协调器可以处理的具有路由功能的子节点的最大个数,它是MAX_CHILDREN的一个子集。
ZigBee2007协议栈已经规定了这些参数的值:MAX_DEPTH=5,MAX_ROUTERS=6和MAX_CHILDREN=20。
向ZigBee节点发送数据时,通常使用AF_DataRequest()函数。该函数需要一个afAssr-Type_t类型的目标地址作为参数。
typedefstruct{union{uint16shortAddr;}addr;afAddrMode_taddrMode;byteendpoint;}afAddrType_t;这里,除了网络地址(短地址)和端点外,还要指定地址模式参数。地址模式参数可以设置为以下几个值。
typedefenum{afAddrNotPresent=AddrNotPresent;afAddr16Bit=Addr16Bit;afAddrGroup=AddrGroup;afAddrBroadcast=AddrBroadcast}afAddrMode_t;这是因为在ZigBee协议栈中,数据包可以单点传送(unicast)、多点传送(multicast)或者广播传送,所以必须有地址模式参数。一个单点传送数据包只发送给一个设备,多点传送数据包则要传送给一组设备,而广播数据包则要发送给整个网络中的所有节点。
(1)单点传送
单点传送是标准寻址模式,它将数据包发送给一个已经知道网络地址的网络设备。将afAddrMode设置为Addr16Bit,并且在数据包中携带目标设备地址。
(2)多点传送
当应用程序不知道数据包的目标设备在哪里时,将模式设置为AddrNotPresent。Z-Stack底层将自动从栈的绑定表中查找目标设备的具体网络地址,这种特点称为源绑定。如果在绑定表中找到多个设备,则向每个设备都发送一个数据包的复制。
(3)广播传送
当应用程序需要将数据包发送给网络的每一个设备时,将使用广播模式,此时将模式设置为AddrBroadcast。目标shortAddr可以设置为下面广播地址中的一种。
NWK_BROADCAST_SHORTADDR_DEVALL(0xFFFF):数据包将被传送到网络上的所有设备,包括睡眠中的设备。对于睡眠中的设备,数据包将被保留在其父节点,直到苏醒后主动到父节点查询,或者直到消息超时。
NWK_BROADCAST_SHORTADDR_DEVRXON(0xFFFD):数据包将被传送到网络上的所有空闲时打开接收的设备(RXONWHENIDELE),即除了睡眠中的所有设备。
NWK_BROADCAST_SHORTADDR_DEVZCZR(0xFFFC):数据发送给所有的路由器(包括协调器,它是一种特殊的路由器)。
(4)组寻址
当应用程序需要将数据包发送给网络上的一组设备时,使用该模式。地址模式设置为afAddrGroup并且shortAddr设置为组ID。在使用这个功能之前,必须在网络中定义组(详见Z-StackAPI文档中的aps_AddGroup()函数)。
ZigBee设备主要工作在2.4GHz频段上,这一基本特性限制了ZigBee设备的数据传输距离,那么ZigBee通过什么办法来解决这个问题呢?答案是路由器。
路由器的工作是为经过路由器的每个数据帧寻找一条最佳传输路径,并将该数据有效地传送到目的节点,称为“路由”。选择通畅快捷的近路,能大大提高通信速度、减轻网络系统通信负荷、节约网络系统资源、提高网络系统畅通率,从而让网络系统发挥出更大的效益。而在ZigBee无线网络中,路由器是非常重要的节点设备,它不仅完成路由的功能,更重要的是,它在数据传输过程中起到了“接力棒”的作用,大大拓展了数据传输的距离,是ZigBee网络中的“交通枢纽”。
选择最佳的策略即路由算法是路由器的关键所在。Z-Stack提供了比较完善、高效的路由算法。路由对于应用层来说是完全透明的。应用程序只需将数据下发到协议栈中,协议栈会负责寻找路径,通过多跳的方式将数据传送到目的地址。
ZigBee网络路由故障能够自愈,如果某个无线连接断开了,路由功能又能自动寻找一条新的路径避开那个断开的网络连接。这就极大地提高了网络的可靠性,这也是ZigBee网络的一个关键特性。
(1)路由协议
ZigBee路由协议是基于AODV专用网络路由协议来实现的。ZigBee将AODV路由协议优化,使其能够适应于各种环境,支持移动节点、连接失败和数据包丢失等复杂环境。
ZigBee终端节点不执行任何路由功能。如果终端节点想要向其他设备传送数据包,只需要将数据向上发送给其父节点,由其父节点代表它来执行路由。同样,任何一个设备要给终端节点发送数据,开始进行路径寻找,终端节点的父节点都将代表它作出响应。
在Z-Stack中,在执行路由功能的过程中就实现了路由表记录的优化。通常,每一个目标设备都需要一条路由表记录。通过将父节点的路由表记录和其所有子节点的路由表记录相结合,可以在保证不丧失任何功能的基础上优化路径。
ZigBee路由器(含协调器)将完成路径寻找与选择、路径保持与维护及路径期满处理功能。
①路径的寻找与选择。路径寻找是网络设备之间相互协作去寻找和建立路径的一个过程。任意一个路由设备都可以发起路径寻找,去寻找某个特定的目标设备。路径寻找机制是指寻找源地址和目标地址之间的所有可能路径,并且选择其中最好的路径。路径选择尽可能选择成本最小的路径。每一个节点通常保持它的所有邻节点的“连接成本(LinkCosts)”。连接成本最典型的表示方法是一个关于接收信号强度的函数。沿着路径,求出所有连接的连接成本总和,便可以得到整个路径的“路径成本”。路由算法将寻找到拥有最小路径成本的路径。
路由器通过一系列的请求和回复数据包来寻找路径。源设备向它的所有邻节点广播一个路由请求数据包(RREQ),来请求一个目标地址的路径。在一个节点收到RREQ数据包后,会依次转发RREQ数据包。在转发之前,要加上最新的连接成本,然后更新RREQ数据包中的成本值。这样,RREQ数据包携带着连接成本的总和通过所有的连接最终到达目标设备。由于RREQ经过不同的路径,目标设备将收到许多RREQ副本。目标设备选择最好的RREQ数据包,然后沿着相反的路径将路径答复数据包(RREP)发给源设备。
一旦一条路径被创建,数据包就可以发送了。当一个节点与它的下一级相邻节点失去连接时(即当它发送数据时,没有收到MACACK),该节点就会向所有等待接收它的RREQ数据包的节点发送一个RERR数据包,将它的路径设为无效。各个节点根据收到的数据包(RREQ、RREP或RERR)来更新它的路由表。
②路径保持与维护。无线网状网(Mesh)提供路径维护和网络自愈功能。一个路径上的中间节点一直跟踪着数年传送过程,如果一个连接失败,那么上游节点将对所有使用这条连接的路径启动路径修复功能。当下一闪数据包到达该节点时,节点将重新寻找路径。如果不能够启动路径寻找或者由于某种原因使路径寻找失败,节点会向数据包的源节点发送一个路径错误包(RERR),它将负责启动新的路径寻找。这两种方法都实现了路径的自动重建。
(2)表存储
要实现路由功能,需要路由器建立一些表格去保持和维护路由信息。
①路由表。每一个路由表包括协调器都包含一个路由表。设备在路由表中保存了数据包参与路由所需的信息。每一条路由表记录都包含目的地址、下一级节点和连接状态等信息。所有数据包都通过相邻的一级节点发送到目的地址。同样,为了回收路由表空间,可以终止路由表中的那些已经无用的路径记录。在文件f8wConfig.cfg中配置路由表的大小,将MAX_RTG_ENTRIES设置为表的大小(不能小于4)。
②路径寻找表。路径寻找表用来保存寻找过程中的临时信息。这些记录只是在路径寻找操作期间存在,一旦某个记录到期,它就可以被另一个路径寻找所使用。记录的个数决定了在一个网络中可以同时并发执行的路径寻找的最大个数。这个值MAX-RREQ-ENTRIES可以通过在f8wConfig.cfg文件中配置。
应用层主要包括应用支持子层(APS层)和ZigBee设备对象(ZDO)。其中,APS负责维护和绑定表、在绑定设备之间传送消息;而ZDO定义设备在网络中的角色,发起和响应绑定请求,在网络设备之间建立安全机制。
绑定指的是两个节点在应用层上建立起来的一条逻辑链路。在同一个节点上可以建立多个绑定服务,分别对应不同种类的数据包。此外,绑定也允许有多个目标节点(一对多绑定)。
一旦在源节点上建立了绑定,其应用服务即可向目标节点发送数据,而不需指定目标地址(调用zb_SendDataRequest(),目标地址可用一个无效值0xFFFE代替)。这样,协议栈将会根据数据包的命令标识符,通过自身的绑定表查找到所对应的目标设备地址。
在绑定表的条目中,有时会有多个目标端点,这使得协议栈自动地重复发送数据包到绑定表指定的各个目标地址。同时,如果在编译目标文件时,编译选项NV_RESTORE被打开,协议栈将会把绑定条目保存在非易失性存储器里。因此,当意外重启(或者节点电池耗尽需要更换)等突发情况发生时,节点能自动恢复到掉电前的工作状态,而不需要用户重新设置绑定服务。
配置文件(Profile)就是应用程序框架,它是由ZigBee技术开发商提供的,应用于特定的应用场合,是用户进行ZigBee技术开发的基础。当然,用户也可以使用专用工具建立自己的Profile。Profile是这样一种规范,它规定不同设备对消息帧的处理行为,使不同的设备之间可以通过发送命令、数据请求来实现互操作。
端点(EndPoint)是一种网络通信中的数据通信,它是无线通信节点的一个通信部件,如果选择“绑定”方式实现节点间的通信,那么可以直接面对端点操作,而不需要知道绑定的两个节点的地址信息。每个ZigBee设备支持240个这样的端点。端点的值和IEEE长地址、16位短地址一样,是唯一确定的网络地址,通常结合绑定功能一起使用。它是ZigBee无线通信的一个重要参数。
间接通信是指各个节点通过端点的绑定建立通信关系,这种通信方式不需要知道目标节点的地址信息,包括IEEE地址或网络短地址,Z-Stack底层将自动从栈的绑定表中查找目标设备的具体网络地址并将其发送出去。
直接通信不需要节点之间通过绑定建立联系,它使用网络短地址作为参数调用适当的API来实现通信。直接通信部分关键点在于节点网络短地址的获得。在发送信息帧之前,必须知道要发送的目标短地址。由于网络协调器的短地址是固定的0X0000,因此可容易地把消息帧发送到协调器。其他网络节点的网络短地址是它们在加入到网络中时由协调器动态分配的,与网络深度、最大路由数、最大节点数等参数有关,没有一个固定值。所以,要想知道目标节点的网络短地址还需要通过其他手段,可以采用通过目标节点的IEEE地址来查询短地址的方法。通常,ZigBee节点的IEEE地址是固定的,它被写在节点的EEPROM中,这个作为ZigBee节点参数一般会被标示在节点上。所以,有了IEEE地址以后,可以通过部分网络API的调用,得到与之对应的网络短地址。
簇(Cluster)就是人们在着手建立Profile时遇到的这个概念,它是一簇网络变量(Attributes)的集合。在同一个Profile中,ClusterID是唯一的。在直接寻址方式和间接寻址方式中都会用到这个概念。在间接寻址方式中,建立绑定表时需要搞清楚它的含义与属性。对于可以建立绑定关系的两个节点,它们的Cluster的属性必须一个选择“输入”,另一个选择“输出”,而且ClusterID值相等,只有这样,它们才能建立绑定。而在直接寻址方式中,常用ClusterID作为参数来将数据或命令发送到对应地址的Cluster(簇)上。
操作系统抽象层(OperatingSystemAbstractionLayer,OSAL)表面上看是作为操作系统存在的,可是为什么又加上“抽象层”呢?它的本质是什么?在Z-stack协议栈中,它又扮演了什么角色呢?要解答这些问题必须先从宏观入手,渐渐深入浅出,最后答案自然会浮出水面。
这里,先介绍与OSAL有关的基础知识。
任何任务所占用的实体都可以称为资源,如一个变量、数组、结构体等。
至少可以被两个任务使用的资源称为共享资源。为了防止共享资源被破坏,每个任务在操作共享资源时,必须保证是独占该资源。
一个任务又称为一个线程,是一个简单程序的执行过程。单个任务中CPU完全是被该任务独占的。在任务设计时,需要将问题尽可能地分为多个任务,每个任务独立完成某种功能,同时被赋予一定的优先级,拥有自己的CPU寄存器和堆栈空间。一般将任务设计为一个无限循环。
多任务通信最简单、最常用的方法是使用共享数据结构。对于嵌入式系统而言,所有任务都在单一的地址空间下,使用共享的数据结构包括全局变量、指针、缓冲区等。虽然共享数据结构的方法简单,但是必须保证对共享数据结构的写操作具有唯一性,以避免晶振和数据不同步。
保护共享资源最常用的方法具体如下。
①关中断。
②使用测试并置位指令(T&S指令)。
③禁止任务切换。
④使用信号量。
其中,在ZigBee协议栈中,OSAL中经常使用的方法是关中断。
消息队列用于任务间传递消息,通常包含任务间同步的信息。通过内核提供的服务、任务或者中断服务程序将一条消息放入消息队列,然后,其他任务可以使用内核提供的服务从消息队列中获取属于自己的消息。为了降低传递消息的开支,通常传递指向消息的指针。
在ZigBee协议栈中,OSAL主要提供如下功能。
①任务注册、初始化和启动。
②任务间的同步、互斥。
③中断处理。
④存储器的分配和管理。
Z-Stack是TI公司开发的ZigBee协议栈,并经过ZigBee联盟认可而被全球众多开发商所广泛采用。Z-Stack的采用基于一个轮转查询式操作系统,可帮助程序员方便地开发一套ZigBee系统。
TI的Z-Stack协议栈就是基于一个最基本的轮转查询式操作系统,这个操作系统就是操作系统抽象层。在ZigBee协议中,协议本身已经定义了大部分内容。在基于ZigBee协议的应用开发中,用户只需要实现应用程序框架即可。从图3.3中可以看出应用程序框架中包含了最多240个应用程序对象。如果将一个应用程序对象视为一个任务的话,那么应用框架将包含一个支持多任务的资源分配机制。于是OSAL便有了存在的必要性,它正是Z-Stack为了实现这样一个机制而存在的。
图3.3ZigBee协议的结构图
OSAL就是以实现多任务为核心的系统资源管理机制,所以OSAL与标准的操作系统还是有很大的区别的。简单而言,OSAL实现了类似操作系统的某些功能,但不能称之为真正意义上的操作系统。
一般情况下,用户只需额外添加3个文件就可以完成一个项目,一个是主控文件,存放具体的任务事件处理函数(如SampleApp_ProcessEvent或GenericApp_ProcessEvent);一个是这个主控文件的头文件(如SampleApp.h);还有一个是操作系统接口文件(如OSAL_SampleApp.c),主要存放任务数组tasksArr[],任务数组的具体内容为每个任务的相应的处理函数指针。
通过这种方式,Z-Stack就实现了绝大部分代码公用,用户只需要添加这几个文件,编写自己的任务处理函数就可以了,无需改动Z-Stack核心代码,大大增加了项目的通用性和易移植性。
从图3.3中可以看到,应用程序框架中包含了最多240个应用程序对象,每个应用程序对象运行在不同的端口上。因此,端口的作用就是区分不同的应用对象。可以把一个应用程序对象看成一个任务。因此,需要一个机制来实现任务的切换、同步和互斥,这就是OSAL产生的根源。
OSAL实现了类似操作系统的某些功能(如任务切换、内存管理等),但它并不能称为真正意义上的操作系统,其实质就是一种支持多任务运行的系统资源分配机制。
图3.3中的“SAP”是某一特定层提供的服务与上层之间的接口。大多数层有数据实体接口和管理实体接口两个接口。
数据实体接口的目标是向上层提供所需的常规数据服务;管理实体接口的目标是向上层提供访问内部层的参数、配置和管理数据服务。
物理层和媒体接入控制子层均属于IEEE802.15.4标准,而IEEE802.15.4标准与网络/安全层、应用层一起,构成了ZigBee协议栈。
Z-Stack采用事件轮询机制来设计操作系统,当各层初始化之后,系统进入低功耗模式,当事件发生时,唤醒系统,开始进入中断处理事件,处理结束后继续进入低功耗模式。如果同时有几个事件发生,则判断优先级,逐次处理事件。这种软件构架可以极大地降级系统的功耗。
整个Z-Stack的主要工作流程如图3.4所示,大致分为系统启动、驱动初始化、OSAL初始化和启动、进入任务轮询几个阶段。
图3.4Z-Stack系统运行流程图
系统上电后,通过执行ZMain文件夹中ZMain.c的main()函数来实现硬件的初始化。
关总中断osal_int_disable(INTS_ALL);
初始化板上硬件设置HAL_BOARD_INIT();
检查工作电压状态zmain_vdd_check();
初始化I/O口InitBoard(OB_COLD);
初始化HAL层驱动HalDriverInit();
初始化非易失性存储器sal_nv_init(NULL);
初始化MAC层ZMacInit();
分配64位地址zmain_ext_addr();
初始化Zstack的全局变量并初始化必要的NV项目zgInit();
初始化操作系统osal_init_system();
使能全局中断osal_int_enable(INTS_ALL);
初始化后续硬件InitBoard(OB_READY);
显示必要的硬件信息zmain_dev_info();
最后进入操作系统调度osal_start_system()。
硬件初始化需要根据HAL文件夹中的hal_board_cfg.h文件配置寄存器8051的寄存器。TI官方发布Z-Stack的配置针对的是TI官方的开发板CC2530DB等,如采用其他开发板,则需根据原理图设计改变hal_board_cfg.h文件的配置。例如,本方案制作的实验板与TI官方的I/O口配置略有不同,需要重新设置控制引脚口、通用I/O口方向和控制函数定义等。
弄明白了OSAL是何方神圣,接下来深入Z-Stack,进一步研究OSAL。为了方便,使用Z-Stack所提供的SampleApp例程来进行分析。在此例程的默认路径C:\TexasInstruments\ZStack-CC2530-2.3.0-1.4.0\Projects\zstack\Samples\SampleApp\CC2530DB下找到SampleApp.eww。
在右侧工作空间窗口中打开App文件夹,我们可以看到5个文件,分别是“SampleApp.c”“SampleApp.h”“OSAL_SampleApp.c”“SampleAppHw.c”和“SampleAppHw.h”。整个程序所实现的功能都在这5个文件当中。
打开文件SampleApp.c,我们首先看到的是两个比较重要的函数SampleApp_Init和SampleApp_ProcessEvent。从函数名称上我们很容易得到的信息便是SampleApp_Init是任务的初始化函数,而SampleApp_ProcessEvent则负责处理传递给此任务的事件。
浏览函数SampleApp_ProcessEvent,我们可以发现,此函数的主要功能是判断由参数传递的事件类型,然后执行相应的事件处理函数。
当顺利完成上述初始化时,执行osal_start_system()函数开始运行OSAL系统。该任务调度函数按照优先级检测各个任务是否就绪。如果存在就绪的任务则调用tasksArr[]中相对应的任务处理函数去处理该事件,直到执行完所有就绪的任务。如果任务列表中没有就绪的任务,则可以使处理器进入睡眠状态实现低功耗。程序流程如图3.5所示。osal_start_system()一旦执行,则不再返回main()函数。
图3.5OSAL任务调度流程图
由此推断Z-Stack应用程序的运行机制如图3.6所示。
图3.6OSAL的运行机制
那么,事件和任务的事件处理函数究竟是如何联系的呢?
ZigBee协议栈采用的方法是,建立一个事件表,保存各个任务对应的事件,建立另一个函数表,保存各个任务事件处理函数的地址,然后将这两张表建立某种对应关系,当某一事件发生时则查找函数表即可。
OSAL用什么样的数据结构来实现事件表和函数表呢?如何将事件表和函数表建立对应关系呢?
OSAL通过tasksEvents指针访问事件表的每一项,如果有事件发生,则查找函数表找到事件处理函数进行处理,处理完后,继续访问事件表,查看是否有事件发生,无限循环。
在ZigBee协议栈中,3个关键变量其数据结构具体如下。
OSAL中最大任务数量为9,最大事件数量为16。
constuint8tasksCnt=sizeof(tasksArr)/sizeof(tasksArr[0]);//最大任务数量为9
uint16*tasksEvents;//最大事件数量为16
OSAL是一种基于事件驱动的轮询式操作系统,事件驱动是指发生事件后采取相应的事件处理方法,轮询指的是不断地查看是否有事件发生。OSAL调度机制如下。
①入口程序为Zmain.c。
②执行main()主程序。
③任务调度初始化osal_init_system()。
④默认启动了osalInitTasks(),最多9个任务,添加到队列,序号为0~8。
⑤最后通过调用SampleApp_Init()实现用户自定义任务的初始化(用户根据项目需要修改该函数)。
OSAL是协议栈的核心,Z-Stack的任何一个子系统都作为OSAL的一个任务,因此在开发应用层的时候,必须通过创建OSAL任务来运行应用程序。通过osalInitTasks()函数来创建OSAL任务,其中TaskID为每个任务的唯一标识号。任何OSAL任务的工作必须分为两步:一是进行任务初始化;二是处理任务事件。
Z-Stack的main函数在Zmain.c中,总体上来说,它主要完成两项工作,一是系统初始化,即由启动代码来初始化硬件系统和软件架构需要的各个模块,二是开始执行操作系统实体,如图3.7所示。
ZMain.c函数布局如图3.8所示。系统启动代码需要完成初始化硬件平台和软件架构所需要的各个模块,为操作系统的运行做好准备工作,主要分为初始化系统时钟、检测芯片工作电压、初始化堆栈、初始化各个硬件模块、初始化FLASH存储、形成芯片MAC地址、初始化非易失变量、初始化MAC层协议、初始化应用帧协议、初始化操作系统等十多个部分,其具体流程图和对应函数如图3.9所示。
图3.7协议栈主流程
图3.8ZMain.c函数布局示意图
图3.9系统流程图及对应函数
其代码如下。
①初始化应用服务变量。constpTaskEventHandlerFntasksArr[]数组定义系统提供的应用服务和用户服务变量,如MAC层服务macEventLoop、用户服务SampleApp_ProcessEvent等。
②分配任务ID和分配堆栈内存。voidosalInitTasks(void)的主要功能是通过调用osal_mem_alloc()函数给各个任务分配内存空间和给各个已定义任务指定唯一的标识号。
③在AF层注册应用对象。通过填入endPointDesc_t数据格式的EndPoint变量,调用afRegister()在AF层注册EndPoint应用对象。
通过在AF层注册应用对象的信息,告知系统afAddrType_t地址类型数据包的路由端点,例如用于发送周期信息的SampleApp_Periodic_DstAddr和发送LED闪烁指令的SampleApp_Flash_DstAddr。
④注册相应的OSAL或者HAL系统服务。在协议栈中,Z-Stack提供键盘响应和串口活动响应两种系统服务,但是任何Z-Stask任务均不自行注册系统服务,两者均需要由用户应用程序注册。值得注意的是,有且仅有一个OSALTask可以注册服务。例如,注册键盘活动响应可调用RegisterForKeys()函数。
⑤处理任务事件。处理任务事件通过创建“ApplicationName”_ProcessEvent()函数处理。一个OSAL任务可以响应16个事件,除了协议栈默认的强制事件(MandatoryEvents)之外还可以再定义15个事件。
SYS_EVENT_MSG(0x8000)是强制事件。该事件主要用来发送全局的系统信息,包括以下信息。
AF_DATA_CONFIRM_CMD:该信息用来指示通过唤醒AFDataRequest()函数发送的数据请求信息的情况。ZSuccess确认数据请求成功的发送。如果数据请求是通过AF_ACK_REQUEST置位实现的,那么ZSussess可以确认数据正确地到达目的地。否则,ZSucess仅仅能确认数据成功地传输到了下一个路由。
AF_INCOMING_MSG_CMD:用来指示接收到的AF信息。
KEY_CHANGE:用来确认按键动作。
ZDO_NEW_DSTADDR:用来指示自动匹配请求。
ZDO_STATE_CHANGE:用来指示网络状态的变化。
启动代码为操作系统的执行做好准备工作后,就开始执行操作系统入口程序,并由此彻底将控制权移交给操作系统,完成新老更替。
其实,操作系统实体只有一行代码。
Osal_start_system();//运行系统[OSAL.c],进入系统调度,无返回可以看到这句代码的注释,本函数不会返回,也就是说它是一个死循环,永远不可能执行完,即操作系统从启动代码接到程序的控制权之后,就不会将权力释放。这个函数就是轮转查询式操作系统的主体部分,它所做的工作就是不断地查询每个任务中是否有事件发生,如果发生,就执行相应的函数;如果没有发生,就查询下一个任务。
函数的主体部分代码:
事件表和函数表的关系如图3.10所示。
图3.10事件表和函数表的关系示意图
首先介绍一下tasksArr、tasksEvents(在OSAL_SampleApp.c文件中)。
constpTaskEventHandlerFntasksArr[]={macEventLoop,nwk_event_loop,Hal_ProcessEvent,#ifdefined(MT_TASK)MT_ProcessEvent,#endifAPS_event_loop,#ifdefined(ZIGBEE_FRAGMENTATION)APSF_ProcessEvent,#endifZDApp_event_loop,#ifdefined(ZIGBEE_FREQ_AGILITY)||defined(ZIGBEE_PANID_CONFLICT)ZDNwkMgr_event_loop,#endifSampleApp_ProcessEvent,};constuint8tasksCnt=sizeof(tasksArr)/sizeof(tasksArr[0]);uint16*tasksEvents;TaskArr这个数组里存放了所有任务的事件处理函数的地址,在这里事件处理函数就代表了任务本身,也就是说事件处理函数标识了与其对应的任务。tasksCnt这个变量保存了当前的任务个数,最大任务数量为9。
tasksEvents是一个指向数组的指针,此数组保存了当前任务的状态。OSAL每个任务可以有16个事件,其中SYS_EVENT_MSG定义为0x8000,为系统事件,用户可以定义剩余的15个事件。
SYS_EVENT_MSG是由协议栈定义的系统强制事件(MandatoryEvents),SYS_EVENT_MSG是一个事件集合,主要包括以下几个事件(其中前两个事件较为常用)。
①AF_INCOMING_MSG_CMD表示收到了一个新的无线数据。
②ZDO_STATE_CHANGE当网络状态发生变化时,会产生该事件,如协调器建立网络;终端节点加入网络时,就可以通过判断该事件来决定何时向协调器发送数据包;终端节点退出网络等。
③ZDO_CB_MSG表示每一个注册的ZDO响应消息。
④AF_DATA_CONFIRM_CMD调用AF_DATARequest()发送数据时,有时需要确认信息,该事件与此有关。
tasksEvents和tasksArr[]里的顺序是一一对应的,tasksArr[]中的第i个事件处理函数对应于tasksEvents中的第i个任务的事件。只有这样才能保证每个任务的事件处理函数能够接收到正确的任务ID(在osalInitTasks函数中分配)。
为了保存osalInitTasks函数中所分配的任务ID,需要给每一个任务定义一个全局变量。
其中,任务处理函数具体如下。
Z-Stack已经编写了对MAC层(macEventLoop)到ZigBee设备应用层(ZDApp_event_loop)这5层任务的事件处理函数,一般情况下无需要修改这些函数,只需要按照自己的需求编写应用层的任务及事件处理函数即可。
Z-Stack的协议栈架构及操作系统实体如图3.11所示。
图3.11Z-Stack的协议栈架构及操作系统实体
TI的Z-Stack中给出了几个例子来演示Z-Stack协议栈,每个例子对应一个项目。对于不同的项目来说,大部分代码都是相同的,只是在用户应用层,添加不同的任务及事件处理函数。
明白了这个问题,新的问题又摆在了我们面前:OSAL是如何传递事件给任务的呢?
在试图弄清楚这个问题之前,我们需要弄清楚另外一个十分基础而重要的问题。消息、事件、任务之间到底存在什么样的关系呢?如何实现事件传递机制呢?
事件是驱动任务去执行某些操作的条件,当系统中产生了一个事件,OSAL将这个事件传递给相应的任务后,任务才能执行一个相应的操作(调用事件处理函数去处理)。
通常某些事件发生后,又伴随着一些附加信息的产生。例如,从天线接收到数据后,会产生AF_INCOMING_MSG_CMD消息,但是任务的事件处理函数在处理这个事件的时候,还需要得到所接收到的数据。
因此,这就需要将事件和数据封装成一个消息,将消息发送到消息队列,然后在事件处理函数中就可以使用osal_msg_receive,从消息队列中得到该消息,即:
MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(SampleicApp_TaskID);OSAL维护了一个消息队列,每一个消息都会被放到这个消息队列中去,当任务接收到事件后,可以从消息队列中获取属于自己的消息,然后再调用消息处理函数进行相应的处理。
OSAL中的消息队列如图3.12所示。
图3.12OSAL中的消息队列
每个消息都包含一个消息头osal_msg_hdr_t和用户自定义的消息,osal_msg_hdr_t结构体的定义如下。
typedefstruct{void*next;uint16len;uint8dest_id;}osal_msg_hdr_t;进入事件轮询后的第一个事件是网络状态变化事件,其处理函数为SampleApp_
ProcessEvent()。网络状态变化事件与节点功能(根据节点功能分为协调器、路由/节点)有一定关联。
(1)协调器
从没有网络到组建起网络,触发网络状态变更事件ZDO_STATE_CHANGE。
(2)路由/节点
从没有接入网络到接入网络,触发网络状态变更事件ZDO_STATE_CHANGE。
其处理方法如下。
caseZDO_STATE_CHANGE:SampleApp_NwkState=(devStates_t)(MSGpkt->hdr.status);if((SampleApp_NwkState==DEV_ZB_COORD)||(SampleApp_NwkState==DEV_ROUTER)||(SampleApp_NwkState==DEV_END_DEVICE){//Startsendingtheperiodicmessageinaregularinterval这个表示默认启动第2个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVT。
osal_start_timerEx(SampleApp_TaskID,SAMPLEAPP_SEND_PERIODIC_MSG_EVT,SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT);//5s定时事件}else{//Deviceisnolongerinthenetwork}break;协议栈默认启动了第2个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVT,其处理函数SampleApp_ProcessEvent()。
//定时事件处理功能if(events&SAMPLEAPP_SEND_PERIODIC_MSG_EVT)//匹配成功SAMPLEAPP_SEND_PERIODIC_MSG_EVT事件{//SendtheperiodicmessageSampleApp_SendPeriodicMessage();//定时事件具体处理函数//Setuptosendmessageagaininnormalperiod(+alittlejitter)默认启动下一个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVTosal_start_timerEx(SampleApp_TaskID,SAMPLEAPP_SEND_PERIODIC_MSG_EVT,(SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT+(osal_rand()&0x00FF);//returnunprocessedeventsreturn(events^SAMPLEAPP_SEND_PERIODIC_MSG_EVT);}3.4.4OSAL添加新任务在使用ZigBee协议栈进行程序开发时,OSAL如何在应用程序中添加一个新任务呢?
(1)新任务的初始化函数
例如,SampleApp_Init(),这个函数是在osalInitTasks()这个OSAL(Z-Stack中自带的小操作系统)中去调用的,其目的就是把一些用户自己写的任务中的一些变量、网络模式、网络终端类型等进行初始化,并且自动给每个任务分配一个ID。
(2)新任务的事件处理函数
例如,SampleApp_ProcessEvent(),这个函数是首先在constTaskEventHandlerFntasksArr[]中进行设置,然后在osalInitTasks()中如果发生事件进行调用绑定的事件处理函数。
下面分3个部分进行分析。
①首先,执行main()(在ZMain.c文件中)主程序,接着执行osal_init_system()。
②接着,在osal_init_system()中调用osalInitTasks()(在OSAL.c文件中)。
③最后,在osalInitTasks()中调用SampleApp_Init()(在OSAL_SampleApp.c文件中)。
在osalInitTasks()中实现了多个任务初始化的设置,其中macTaskInit(taskID++)到ZDApp_Init(taskID++)的几行代码表示对于几个系统运行初始化任务的调用,而用户自己实现的SampleApp_Init()在最后,这里taskID随着任务的增加也随之递增。所以,用户自己实现的任务初始化操作应该在osalInitTasks()中增加。
constpTaskEventHandlerFntasksArr[]={macEventLoop,nwk_event_loop,Hal_ProcessEvent,#ifdefined(MT_TASK)MT_ProcessEvent,#endifAPS_event_loop,#ifdefined(ZIGBEE_FRAGMENTATION)APSF_ProcessEvent,#endifZDApp_event_loop,#ifdefined(ZIGBEE_FREQ_AGILITY)||defined(ZIGBEE_PANID_CONFLICT)ZDNwkMgr_event_loop,#endifSampleApp_ProcessEvent,NewProcessApp_ProcessEvent,//新增第1个任务处理函数NewProcess2App_ProcessEvent,//新增第2个任务处理函数};注意:tasksEvents和tasksArr[]里的顺序是一一对应的,tasksArr[]中的第i个事件处理函数对应于tasksEvents中的第i个任务的事件。
//计算出任务的数量constuint8tasksCnt=sizeof(tasksArr)/sizeof(tasksArr[0]);uint16*tasksEvents;3.对于不同事件发生后的任务处理函数的调用osal_start_system()很重要,决定了当某个任务的事件发生后调用对应的事件处理函数。
对应调用第idx个任务的事件处理函数,用events说明是什么事件。
events=(tasksArr[idx])(idx,events);用户自定义功能在NewProcessApp.c文件中利用NewProcessApp_ProcessEvent()函数实现,其程序代码如下。
#include"NewProcessApp.h"#include"NewProcess2App.h"在NEWProcessAPP.C文件中voidNewApp_Init(uint8task_id)函数未尾添加以下Led灯初始化代码:
HalLedInit();HalLedSet(HAL_LED_1,HAL_LED_MODE_ON);osal_start_timerEx(NewApp_TaskID,NEWAPP_SEND_PERIODIC_MSG_EVT,NEWAPP_SEND_PERIODIC_MSG_TIMEOUT);3.4.5事件的捕获接下来,就有了更加深入的问题——事件是如何被捕获的?直观地讲,tasksEvents这个数组里的元素是什么时候被设定为非零数来表示有事件需要处理的?为了详细地说明这个过程,我们将以SampleApp这个例程中响应按键的过程来进行说明。其他的事件虽然稍有差别,却大同小异。
按键在我们的应用里应该属于硬件资源,所以OSAL理应为我们提供使用和管理这些硬件的服务。稍微留意一下我们之前说过的taskArr这样一个数组,它保存了所有任务的事件处理函数。我们从中发现了一个很重要的信息:Hal_ProcessEvent。HAL(HardwareAbstractionLayer)翻译为“硬件抽象层”。许多人在这里经常把Z-Stack的硬件抽象层与ZigBee的物理层混为一谈。在这里,我们应该将其区分开来。硬件抽象层所包含的范围是我们当前硬件电路上面所有对于系统可用的设备资源,而ZigBee的物理层则是针对无线通信而言的,它所包含的仅限于无线通信的硬件设备。
通过这个重要的信息,我们可以得到这样一个结论:OSAL将硬件的管理也作为一个任务来处理。那么,我们很自然地去寻找Hal_ProcessEvent这个事件处理函数,看看它究竟是如何管理硬件资源的。
在“HAL\Commen\hal_drivers.c”这个文件中,我们找到了这个函数,直接分析与按键有关的一部分。
{If(events&HAL_KEY_EVENT){#if(definedHAL_KEY)&&(HAL_KEY==TRUE)/*Checkforkeys*/HalKeyPoll();/*ifinterruptdisabled,donextpolling*/If(!Hal_KeyIntEnable){Osal_start_timerEx(Hal_TaskID,HAL_KEY_EVENT,100);}#endif//HAL_KeyReturnevents^HAL_KEY_EVENT;}}在事件处理函数接收到HAL_KEY_EVENT这样一个事件后,首先执行HalKeyPoll()函数。由于这个例程采用查询的方法获取,所以是禁止中断的,于是表达式(!Hal_KeyIntEnable)的值为真。那么Osal_start_timerEx(Hal_TaskID,HAL_KEY_EVENT,100)得以执行。Osal_start_timerEx是一个很常用的函数,它在这里的功能是经过100ms后,向Hal_TaskID这个ID所标示的任务(也就是其本身)发送一个HAL_KEY_EVENT事件。这样一来,每经过100ms,HAL_ProcessEvent这个事件处理函数都会至少执行一次来处理HAL_KEY_EVENT事件,也就是说每隔100ms都会执行HalKeyPoll()函数。
那么,我们来看看HalKeyPoll()完成什么功能?
代码中给出的注释如下。
/*Checkforkeys*/HalKeyPoll();于是,我们推断这个函数的作用是检查当前的按键情况。进入函数一看,果不其然。虽然这个函数很长很复杂,但经过一系列的if语句和赋值语句,在接近函数末尾的地方,keys变量(在函数起始位置定义的)获得了当前按键的状态。最后,有一个十分重要的函数调用。
(pHalKeyProcessFunction)(keys,HAL_KEY_STATE_NORMAL);虽然不清楚pHalKeyProcessFunction这个函数指针指向哪个函数,但是我们知道这里调用的是voidOnBoard_KeyCallback(uintekeys,uint8state)函数。
此函数在“ZMain\OnBoard.c”文件中可以找到。在此函数中,又调用了voidOnBoard_sendKeys(uint8keys,uint8state),按键的状态信息被封装到了一个消息结构体中。最后有一个极其重要的函数被调用了。
Osal_msg_send(registeredKeysTaskID,(uint8*)mstPtr);registeredKeysTaskID所指示的任务正是我们需要响应按键的SampleApp。
也就是说,我们向SampleApp发送了一个附带按键信息的消息,在osal_msg_send函数中,osal_set_event(destination_task,SYS_EVENT_MSG);被调用,它在这里的作用是设置destination_task任务的事件为SYS_EVENT_MSG。而destination_task任务的事件由osal_msg_send函数通过参数传递而来。它也指示的是SampleApp这个任务。在osal_set_event函数中,有这样一个语句:
{tasksEvents[task_id]|=event_flag;}至此,刚才所提到的问题得到了解决。我们再将这个过程整理一下。
首先,OSAL专门建立了一个任务来对硬件资源进行管理,这个任务的事件处理函数是Hal_ProcessEvent。
在这个函数中通过调用Osal_start_timerEx(Hal_TaskID,HAL_KEY_EVENT,100)函数使得每隔100ms就会执行一次HalKeyPoll()函数。HalKeyPoll()函数获取当前按键的状态,并且通过调用voidOnBoard_KeyCallback(uintekeys,uint8state)函数向SampleApp任务发送一个按键消息,并且设置tasksEvents中GenericApp所对应的值为非零。此时,main函数里有如下一段代码。
{do{If(tasksEvents[ids]){break;}}while(++idx
消息管理API主要用于处理任务间消息的交换,主要包括任务分配消息缓存、释放消息缓存、接收消息和收送消息等API函数。
①osal_msg_allocate()
函数原型:uint8*osal_msg_allocate(uint16len)。
功能描述:为消息分配缓存空间。
②osal_msg_deallocate()
函数原型:uint8*osal_msg_allocate(uint8*msg_ptr)。
功能描述:释放消息的缓存空间。
③osal_msg_send()
函数原型:uint8osal_msg_send(uint8destination_task,uint8*msg_ptr)。
功能描述:一个任务发送消息到消息队列。
④osal_msg_receive()
函数原型:uint8*osal_msg_receive(uint8task_id)。
功能描述:一个任务从消息队列接收属于自己的消息。
任务同步API主要用于任务间的同步,允许一个任务等待某个事件的发生。
osal_set_event()函数原型:uint8osal_set_event(uint8task_id,uint16event_flag)。
功能描述:运行一个任务时设置某一事件同时发生。
①osal_start_timerEx()
函数原型:uint8osal_start_timerEx(uint8task_id,uint16event_id,uint16timeout_value)。
②osal_stop_timerEx()
函数原型:uint8osal_stop_timerEx(uint8task_id,uint16event_id)。
功能描述:停止已经启动的定时器。
中断管理API主要用于控制中断的开启与关闭,一般很少使用。
任务管理API主要是对OSAL进行初始化和启动。
①osal_init_system()
函数原型:uint8osal_start_system(void)。
功能描述:初始化OSAL,该函数是第一个被调用的OSAL函数。
②osal_start_system()
功能描述:该函数包含一个无限函数,它将查询所有的任务事件,如果有事件发生,则调用相应的事件处理函数,处理完该事件后,返回主循环继续检测是否有事件发生,如果开启了节能模式,则没有事件发生时,该函数将使处理器进入休眠模式,以降低系统功耗。
内存管理API用于在堆栈上分配缓冲区。注意以下两个API函数必须成对使用,防止产生内存泄漏。
①osal_mem_alloc()
函数原型:uint8osal_mem_alloc(uint16size)。
功能描述:在堆栈上分配指定大小的缓冲区。
②osal_mem_free()
函数原型:uint8osal_mem_free(void*ptr)。
功能描述:释放使用osal_mem_alloc()分配的缓冲区。
电源管理API主要用于电池供电的ZigBee网络节点,在此不作讨论。
非易失性闪存(Non-VolatileMemory,NV)管理API主要添加了对非易失性闪存的管理函数,一般这里的非易失性闪存指的是系统Flash存储器(也可以是E2PROM),每个NV条目分配唯一的ID号。
①osal_nv_item_init()
函数原型:byteosal_nv_item_init(uint16id,uint16len,void*buf)。
功能描述:初始化NV条目,该函数检查是否存在NV条目,如果不存在,它将创建并初始化该条目;如果该条目存在,每次调用osal_nv_read()osal_nv_write()。
②osal_nv_read()
函数原型:byteosal_nv_read(uint16id,uint16offset,void*buf)。
功能描述:从NV条目中读取数据;可以读取整个条目的数据,也可以读取部分数据。
③osal_nv_write()
函数原型:uint8osal_nv_write(uint16id,uint16offset,uint16len,void*buf)。
功能描述:写数据到NV条目。
利用ZDO_STATE_CHANGE实现组网成功点灯功能,程序代码如下。
int16SampleApp_ProcessEvent(uint8task_id,uint16events){afIncomingMSGPacket_t*MSGpkt;(void)task_id;//Intentionallyunreferencedparameterif(events&SYS_EVENT_MSG){MSGpkt=(afIncomingMSGPacket_t*)osal_msg_receive(SampleApp_TaskID);while(MSGpkt){switch(MSGpkt->hdr.event){//Receivedwhenamessagesisreceived(OTA)forthisendpointcaseAF_INCOMING_MSG_CMD:SampleApp_MessageMSGCB(MSGpkt);break;//ReceivedwheneverthedevicechangesstateinthenetworkcaseZDO_STATE_CHANGE:SampleApp_NwkState=(devStates_t)(MSGpkt->hdr.status);if((SampleApp_NwkState==DEV_ZB_COORD)||(SampleApp_NwkState==DEV_ROUTER)||(SampleApp_NwkState==DEV_END_DEVICE)){//点灯?P1SEL&=~0x3;P1DIR|=0x3;//定义P10、P11为输出P1_0=1;//Startsendingtheperiodicmessageinaregularinterval.osal_start_timerEx(SampleApp_TaskID,SAMPLEAPP_SEND_PERIODIC_MSG_EVT,SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT);}else{//Deviceisnolongerinthenetwork}break;default:break;}...return0;}2.定时事件测试利用SAMPLEAPP_SEND_PERIODIC_MSG_EVT实现LED灯的定时翻转功能,其程序代码如下。
(2)afStatus_tAF_DataRequest(afAddrType_t*dstAddr,endPointDesc_t*srcEP,
uint16cID,uint16len,uint8*buf,uint8*transID,uint8options,uint8radius)用户调用该函数即可实现数据的无线发送。
(3)TaskArr数组里存放了所有任务的事件处理函数的地址,在这里事件处理函数就代表了任务本身,也就是说事件处理函数标识了与其对应的任务。变量tasksCnt保存了当前的任务个数,最大任务数量为9。
(4)tasksEvents是一个指向数组的指针,此数组保存了当前任务的状态。OSAL每个任务可以有16个事件,其中SYS_EVENT_MSG定义为0x8000,为系统事件,用户可以定义剩余的15个事件。
协议、协议栈、OSAL运行机制、事件传递机制。
任务在Z-Stack协议栈添加新任务
[任务目标]
(1)熟悉Z-Stack协议栈的源文件架构。
(2)熟悉Z-Stack常见接口函数的调用。
(3)学习Z-Stack下OSAL增加任务的方法。
[内容与要求]
sampleApp任务定时改变蓝灯的状态,NewApp任务定时改变黄灯的状态。
一、实验设备
实验设备
数量
备注
ZigBeeDebugger仿真器
1
下载和调试程序
CC2530节点
调试程序
USB线
连接PC机、网关板、调试器
RS232串口连接线
SmartRFFlashProgrammer软件
烧写物理地址软件
电源
5
供电
Z-Stack-CC2530-2.3.0-1.4.0
协议栈软件
二、实验基础
1.协议栈介绍
一般情况下,只需要额外添加3个文件就可以完成一个项目。这3个文件具体如下。
①主控文档taskApp.c。该文件存放具体的任务事件处理函数,如GenericApp_ProcessEvent。
②taskApp.h。该文件为主控文件的头文件。
③操作系统接口文件OSALtaskApp.c。该文件存放任务组constpTaskEventHandleFntasksArr[]。任务数组的具体内容为每个任务的相应的处理函数指针;存放任务初始化函数initTasks(),其功能为初始化系统中的每一个任务。
一般来说,只要增加这三个文件,就可加入自己的应用,而不必更改其他层的代码。
对于本实验,项目任务处理函数如下。
macEventloop:MAC层任务处理函数。
nwk_event_loop:网络层任务处理函数。
Hal_ProcessEvent:硬件抽象层任务处理函数。
MT_ProcessEvent:监控测试任务处理函数(通过编译选项MT_TASK来决定是否编译该任务处理函数,一般情况下该功能通过串行端口通信来实现)。
APS_event_loop:应用支持子层任务处理函数,用户不要更改。
APSF_ProcessEvent:应用支持子层消息分割任务处理函数(用户可通过编译选项ZigBee_FRAGMENTATION来决定是否启动ZigBee消息分割功能)。
ZDApp_event_loop:ZigBee设备应用任务处理函数。
ZDNwkMgr_event_loop:网络管理层任务处理函数(用户可通过编译选项ZigBee_FREQ_AGILITY或者ZigBee_PANID_CONFLICT来实现该功能。
GenericApp_processEvent:用户应用层任务处理函数,由用户自己编写。
2.OSAL常用API函数简介
(1)OSAL中断操作
①允许中断
Byteosal_int_enable(byteinterrupt_id)interrupt_id:中断标识符。
②禁止中断
byteosal_int_disable(byteinterrupt_id)interrupt_id:中断标识符。
③暂停中断
HAL_ENTER_CRITICAL_SECTION(x)④重新启动中断
HAL_EXIT_CRITICAL_SECTION(x)(2)OSAL内存操作
①分配内存
void*osal_mem_alloc(uint16size)②释放内存
Voidosal_mem_free(void*ptr)(3)OSAL消息传递
①分配消息缓冲区
byte*osal_msg_allocate(uint16len)②发送信息
byteosal_msg_send(bytedestination_task,byte*msg_ptr)destination_task:接收信息任务的标示符。
msg_ptr:消息指针。
③接收信息
byte*osal_msg_receive(bytetask_id)task_id:接收任务的ID。
④释放消息缓冲区
byteosal_msg_deallocate(byte*msg_ptr)msg_ptr:消息指针。
(4)OSAL任务管理
①任务初始化
byteosal_init_system(void)要创建的任务列表。
②任务开始
Voidosal_self(void)系统任务的主循环函数。
③获取活动任务
IDbyteosal_self(void)中断服务子程序中调用将会发生错误。
(5)OSAL定时器
①启动定时器
byteosal_start_timerEx(bytetaskID,UINT16event_id,UINT16timeout_value)taskID:定时器终止时事件任务的任务ID。
timeout_value:定时器设置定时参数,单位为毫秒。
②停止定时器
Byteosal_stop_timerEx(bytetask_id,UINT16event_id)task_id:事件任务的任务ID。
event_id:用户自定义事件。
③读取系统时钟
Uint32osal_GetSystemClock(void)用来读取系统时钟(毫秒级)。
三、实现步骤
①用于初始化的函数,如NewProcessApp_Init(),将在osalInitTasks()中调用,其目的就是把用户自定义的任务中的一些变量(如网络模式、网络终端类型等)进行初始化。
②用于该任务新事件发生后所需要执行的事件处理函数,如NewProcessApp_ProcessEvent(),首先在constpTaskEventHandlerFntasksArr[]中进行设置(绑定),然后在系统运行期间中如果某任务发生新事件,则进行调用绑定的事件处理函数。
其操作步骤如下。
(1)添加用于新任务初始化的函数
用户自定义的任务代码在Zstack中的调用过程具体如下。
①执行main()(在ZMain.c文件中)主程序,接着执行osal_init_system()。
②在osal_init_system()调用osalInitTasks()(在OSAL.c文件中)。
③在osalInitTasks()中执行SampleApp_Init()中的NewProcessApp_Init()语句(在OSAL_SampleApp.c文件中)。
在osalInitTasks()中实现了多个任务初始化的设置,其中macTaskInit(taskID++)到ZDApp_Init(taskID++)的几行代码表示对于几个系统运行初始化任务的调用,而用户自定义的NewProcessApp_Init可以添加在SampleApp_Init()后,这里taskID随着任务的增加也随之递增。所以,用户自己实现的任务的初始化操作应该在osalInitTasks()中增加。例如,在OSAL_SampleApp.c文件中找到如下代码。
voidosalInitTasks(void){uint8taskID=0;tasksEvents=(uint16*)osal_mem_alloc(sizeof(uint16)*tasksCnt);osal_memset(tasksEvents,0,(sizeof(uint16)*tasksCnt));macTaskInit(taskID++);nwk_init(taskID++);Hal_Init(taskID++);#ifdefined(MT_TASK)MT_TaskInit(taskID++);#endifAPS_Init(taskID++);#ifdefined(ZIGBEE_FRAGMENTATION)APSF_Init(taskID++);#endifZDApp_Init(taskID++);#ifdefined(ZIGBEE_FREQ_AGILITY)||defined(ZIGBEE_PANID_CONFLICT)ZDNwkMgr_Init(taskID++);#endifSampleApp_Init(taskID++);NewApp_Init(taskID)//新增加的用户任务初始化函数}(2)添加新任务处理调用的事件处理函数
在Z-Stack里,对于同一个任务可能有多种事件发生,那么需要执行不同的事件处理。为了方便,对于每个任务的事件处理函数都统一在一个事件处理函数中实现,然后根据任务的ID号(task_id)和该任务的具体事件(events)调用某个任务的事件处理函数。进入了该任务的事件处理函数之后,再根据events再来判别是该任务的哪一种事件发生,进而执行相应的事件处理。
pTaskEventHandlerFn是一个指向函数(事件处理函数)的指针,这里实现的每一个数组元素各对应于一个任务的事件处理函数。例如,SampleApp_ProcessEvent对应于系统默认的事件处理函数uint16SampleApp_ProcessEvent(uint8task_id,uint16events),所以如果我们实现了一个任务,还需要在此把实现的该任务的事件处理函数进行添加。
constpTaskEventHandlerFntasksArr[]={macEventLoop,//MAC层任务处理函数nwk_event_loop,//网络层任务处理函数Hal_ProcessEvent,//硬件抽象层任务处理函数#ifdefined(MT_TASK)MT_ProcessEvent,//监控测试任务处理函数#endifAPS_event_loop,//应用支持子层任务处理函数#ifdefined(ZIGBEE_FRAGMENTATION)APSF_ProcessEvent,//APSF层任务处理函数#endifZDApp_event_loop,//ZigBee设备应用任务处理函数#ifdefined(ZIGBEE_FREQ_AGILITY)||defined(ZIGBEE_PANID_CONFLICT)ZDNwkMgr_event_loop,//网络管理层任务处理函数#endif//oadAppEvt,SampleApp_ProcessEvent,//Z-Stack默认应用层任务处理函数NewApp_ProcessEvent//新增加的用户任务处理函数};注意:tasksEvents和tasksArr[]里的顺序是一一对应的,tasksArr[]中的第i个事件处理函数对应于tasksEvents中的第i个任务的事件。
NewApp_ProcessEvent处理函数的设计可以参考APP目录中的系统例程文件SampleApp.c,参考如下。
(3)对于不同事件发生后的任务处理函数osal_start_system的调用分析
添加代码后如图3.13所示。
图3.13添加代码后示意图
程序运行后,sampleApp任务定时改变蓝灯的状态,NewApp任务定时改变黄灯的状态。
(1)简述ZigBee无线传感器网络协议和协议栈的关系。
(2)什么是节点?什么是端口?节点和端口之间有什么关系?
(3)IT公司ZigBee协议栈中数据发送函数是哪个?数据接收函数是哪个?
本文仅用于学习和交流目的,不代表人邮教育社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。
HTML5与CSS3是下一代Web应用技术的基础,使互联网进入了一个崭新的时代。本书从HTML5和CSS3的基...
Web标准的最大特点是采用HTML+CSS+JavaScript将网页内容、外观样式及动态效果彻底分离,从...
本书以MicrosoftOffice2010为环境,通过案例的形式,对Office2010中的Word、...
PHP是一种运行于服务器端并完全跨平台的嵌入式脚本编程语言,是目前Web应用开发的主流语言之一。本书是面向...
本书以实训教学为主线,分为6篇:虚拟机与VMwareWorkstation、Linux系统安装与常用命令、L...
《ASP.NET就业实例教程》是一本Web应用程序开发的中级教材。本书由浅入深,全面系统地介绍了ASP.N...
2007-2024人邮教育社区·人民邮电出版社有限公司·Allrightsreserved