项目三 了解ZigBee无线传感器网络协议栈
本章目标
知识目标
- 掌握ZigBee无线传感器网络的协议栈和协议的区别等知识。
- 掌握Z-Stack协议栈的OSAL分配机制。
- 了解Z-Stack协议栈的OSAL运行机制。
- 掌握Z-Stack协议栈的OSAL常用函数。
技能目标
- 掌握Z-Stack协议栈的运行机制。
- 掌握Z-Stack协议栈中OSAL的添加新任务的方法。
在实际ZigBee无线传感器网络工程的开发过程中,首先借助TI提供的协议栈中例程SampleApp,接着根据需要完成的功能,查看支持Z-Stack协议栈的硬件电路图,再查阅数据手册(CC2530的数据手册、Z-Stack协议栈说明、Z-Stack协议栈API函数使用说明等)文件,然后再进行协议栈的修改。最后,还需要烧录器下载到相应的硬件,实现ZigBee无线传感器网络的组建和开发。
3.1 Z-Stack协议栈
3.1.1 协议与协议栈
协议定义的是一系列的通信标准,通信双方需要共同按照这一标准进行正常的数据收发;协议栈是协议的具体实现形式,可通俗地理解为代码实现的函数库,以便于开发人员调用。
ZigBee的协议分为两部分,IEEE 802.15.4定义了物理层和数据链路层技术规范,ZigBee联盟定义了网络层、安全层和应用层技术规范,ZigBee协议栈就是将各层定义的协议都集合在一起,以函数的形式实现,并提供一些应用层API供用户调用,如图3.1所示。
图3.1 ZigBee协议栈示意图
协议栈是指网络中各层协议的总和,一套协议的规范。其形象地反映了一个网络中文件传输的过程:由上层协议到底层协议,再由底层协议到上层协议。
使用最广泛的是因特网协议栈,由上到下的协议分别是:应用层(Http、Telnet、DNS、E-mail等),运输层(TCP、UDP),网络层(IP),链路层(WI-FI、以太网、令牌环、FDDI等)。
ZigBee协议栈开发的基本思路如下。
(1)借助TI提供的协议栈中例程SampleApp进行二次开发,用户不需要深入研究复杂的ZigBee协议栈,这样可以减轻开发者的工作量。
(2)ZigBee无线传感器网络中数据采集,只需要用户在应用层加入传感器的读取函数和添加头文件即可实现。
(3)如果考虑节能,可以根据数据采集周期(ZigBee协议栈例程中已开发了定时程序)进行定时,时间到就唤醒ZigBee终端节点,终端节点被唤醒后,自动采集传感器数据,然后将数据发送给路由器或者直接发给协调器,即监测节点定时汇报监测数据。
(4)协调器(网关)根据下发的控制命令,将控制信息转发到具体的节点即控制节点,等待控制命令下发。
3.1.2 使用Z-Stack协议栈传输
ZigBee协议栈已经实现了ZigBee协议,用户可以使用协议栈提供的API进行应用程序的开发。开发过程中不必关心协议具体的实现,只需要关心应用程序的数据来源和去向即可。
SampleApp.c中定义了发送函数static void SampleApp_SendTheMessage(void)。该函数通过调用AF_DataRequest来发送数据。该函数定义在Profile目录下的AF.c文件中,如图3.2所示。
图3.2 AF_DataRequest函数定义示意图
afStatus_t AF_DataRequest( afAddrType_t *dstAddr, endPointDesc_t *srcEP,
uint16 cID, uint16 len, uint8 *buf, uint8 *transID,
uint8 options, uint8 radius )
用户调用该函数即可实现数据的无线发送。该函数中有8个参数,参数具体含义如下。
*dstAddr:发送目的地址+端点地址(端点号)和传送模式。
*srcEP:源(答复或确认)终端的描述(如操作系统中任务ID等)源EP。
cID:被Profile指定的有效的集群号。
len:发送数据长度。
*buf:指向存放发送数据的缓冲区的指针。
*transID:任务ID号。
options: 有效位掩码的发送选项。
Radius:发送跳数,通常设置为AF_DEFAULT_RADIUS。
其中,最核心的两个参数是uint16 len(发送数据的长度)和uint8 *buf(指向存放发送数据的缓冲区的指针)。使用ZigBee协议栈只需调用相应的数据发送、接收函数即可。
3.2 ZigBee无线传感器网络功能层简介
3.2.1 物理层
物理层(PHY层)定义了无线信道和MAC子层之间的接口,提供物理层数据服务和物理层管理服务,主要是在驱动程序的基础上,实现数据传输和管理。物理层数据服务从无线物理信道上收发数据,管理服务包括信道能量监测(ED)、链接质量指示(LQI)、载波检测(CS)和空闲信道评估(CCA)等,维护一个由物理层相关数据组成的数据库。
物理层是整个协议栈最底层的部分,该层主要完成基带数据处理、物理信号的接收和发送以及无线电规格参数(包括功率谱密度、符号速率、接收机灵敏度、接收机干扰抑制、转换时间和调制误差等)设置等基本功能。
3.2.2 介质访问控制层
介质访问控制层(MAC层)提供点对点通信的数据确认(Per-hop Acknowledgments)以及一些用于网络发现和网络形成的命令,但是介质访问控制层不支持多跳(Multi-hop)、网型网络(Mesh)等概念。
3.2.3 网络层
网络层(NWK层)主要负责设备加入和退出网络、路由管理、在设备之间发现和维护路由、发现邻设备及存储邻设备信息等。例如,在网络范围内发送广播包,为单播数据包选择路由,确保数据包能够可靠地从一个节点发送到另一个节点,此外,网络层还具有安全特性,用户可以自行选择所需的安全策略。
1.地址类型
每一个ZigBee设备有一个64位IEEE地址,即MAC地址,跟网卡MAC一样,是全球唯一的。但在实际网络中,为了方便,通常用16位的短地址来标识自身和识别对方,也称为网络地址。对于协调器来说,短地址为0000H;对于路由器和节点来说,短地址是由它们所在网络中的协调器分配的。
2.网络地址分配
网络地址分配由网络中的协调器来完成,为了让网络中的每一个设备都有唯一的网络地址(短地址),它要按照事先配置的参数,并遵循一定的算法来分配。这些参数是MAX_DEPTH、MAX_ROUTERS和 MAX_CHILDREN。
MAX_DEPTH决定了网络的最大深度。协调器位于深度为0,其子节点位于深度为1,子节点的子节点位于深度为2,以此类推。MAX_DEPTH参数限制了网络在物理上的长度。
MAX_CHILDREN决定了一个路由器或者一个协调器节点可以连接的子节点的最大个数。MAX_ROUTERS决定了一个路由器或者一个协调器可以处理的具有路由功能的子节点的最大个数,它是MAX_CHILDREN的一个子集。
ZigBee 2007协议栈已经规定了这些参数的值:MAX_DEPTH=5,MAX_ROUTERS=6和MAX_CHILDREN=20。
3.Z-Stack寻址
向ZigBee节点发送数据时,通常使用AF_DataRequest()函数。该函数需要一个afAssr-Type_t 类型的目标地址作为参数。
typedef struct
{
union
{
uint16 shortAddr;
}addr;
afAddrMode_t addrMode;
byte endpoint;
}afAddrType_t;
这里,除了网络地址(短地址)和端点外,还要指定地址模式参数。地址模式参数可以设置为以下几个值。
typedef enum
{
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-Stack API文档中的aps_AddGroup()函数)。
4.路由
ZigBee设备主要工作在2.4GHz频段上,这一基本特性限制了ZigBee设备的数据传输距离,那么ZigBee通过什么办法来解决这个问题呢?答案是路由器。
路由器的工作是为经过路由器的每个数据帧寻找一条最佳传输路径,并将该数据有效地传送到目的节点,称为“路由”。选择通畅快捷的近路,能大大提高通信速度、减轻网络系统通信负荷、节约网络系统资源、提高网络系统畅通率,从而让网络系统发挥出更大的效益。而在ZigBee无线网络中,路由器是非常重要的节点设备,它不仅完成路由的功能,更重要的是,它在数据传输过程中起到了“接力棒”的作用,大大拓展了数据传输的距离,是ZigBee网络中的“交通枢纽”。
选择最佳的策略即路由算法是路由器的关键所在。Z-Stack提供了比较完善、高效的路由算法。路由对于应用层来说是完全透明的。应用程序只需将数据下发到协议栈中,协议栈会负责寻找路径,通过多跳的方式将数据传送到目的地址。
ZigBee网络路由故障能够自愈,如果某个无线连接断开了,路由功能又能自动寻找一条新的路径避开那个断开的网络连接。这就极大地提高了网络的可靠性,这也是ZigBee网络的一个关键特性。
(1)路由协议
ZigBee路由协议是基于AODV专用网络路由协议来实现的。ZigBee将AODV路由协议优化,使其能够适应于各种环境,支持移动节点、连接失败和数据包丢失等复杂环境。
当路由器从它自身的应用程序或者别的设备那里收到一个单点发送的数据包后,网络层会遵循以下流程将它继续传递下去:如果目标节点是它的相邻节点或子节点,则数据包会被直接传送给目标设备。否则,路由器将要检索它的路由表中与所要传送的数据包的目标地址相符合的记录。如果存在与目标地址相符合的有效路由记录,数据包将被发送到记录中的下一跳地址中去,如果没有发现任何相关的路由记录,则路由器开始进行路径寻找,将数据包暂时存储在缓冲区中,直到路径寻找结束为止。
ZigBee终端节点不执行任何路由功能。如果终端节点想要向其他设备传送数据包,只需要将数据向上发送给其父节点,由其父节点代表它来执行路由。同样,任何一个设备要给终端节点发送数据,开始进行路径寻找,终端节点的父节点都将代表它作出响应。
在Z-Stack中,在执行路由功能的过程中就实现了路由表记录的优化。通常,每一个目标设备都需要一条路由表记录。通过将父节点的路由表记录和其所有子节点的路由表记录相结合,可以在保证不丧失任何功能的基础上优化路径。
ZigBee路由器(含协调器)将完成路径寻找与选择、路径保持与维护及路径期满处理功能。
① 路径的寻找与选择。路径寻找是网络设备之间相互协作去寻找和建立路径的一个过程。任意一个路由设备都可以发起路径寻找,去寻找某个特定的目标设备。路径寻找机制是指寻找源地址和目标地址之间的所有可能路径,并且选择其中最好的路径。路径选择尽可能选择成本最小的路径。每一个节点通常保持它的所有邻节点的“连接成本(Link Costs)”。连接成本最典型的表示方法是一个关于接收信号强度的函数。沿着路径,求出所有连接的连接成本总和,便可以得到整个路径的“路径成本”。路由算法将寻找到拥有最小路径成本的路径。
路由器通过一系列的请求和回复数据包来寻找路径。源设备向它的所有邻节点广播一个路由请求数据包(RREQ),来请求一个目标地址的路径。在一个节点收到RREQ数据包后,会依次转发RREQ数据包。在转发之前,要加上最新的连接成本,然后更新RREQ数据包中的成本值。这样,RREQ数据包携带着连接成本的总和通过所有的连接最终到达目标设备。由于RREQ经过不同的路径,目标设备将收到许多RREQ副本。目标设备选择最好的RREQ数据包,然后沿着相反的路径将路径答复数据包(RREP)发给源设备。
一旦一条路径被创建,数据包就可以发送了。当一个节点与它的下一级相邻节点失去连接时(即当它发送数据时,没有收到MAC ACK),该节点就会向所有等待接收它的RREQ数据包的节点发送一个RERR数据包,将它的路径设为无效。各个节点根据收到的数据包(RREQ、RREP或RERR)来更新它的路由表。
② 路径保持与维护。无线网状网(Mesh)提供路径维护和网络自愈功能。一个路径上的中间节点一直跟踪着数年传送过程,如果一个连接失败,那么上游节点将对所有使用这条连接的路径启动路径修复功能。当下一闪数据包到达该节点时,节点将重新寻找路径。如果不能够启动路径寻找或者由于某种原因使路径寻找失败,节点会向数据包的源节点发送一个路径错误包(RERR),它将负责启动新的路径寻找。这两种方法都实现了路径的自动重建。
③ 路径期满处理。路由表为已经建立连接路径的节点维护路径记录。如果在一定的时间周期内没有数据通过这条路径发送,则这条路径被表示为期满。期满的路径一直保留到它所占用的空间要被使用为止。在配置文件f8wConfig.cfg中配置自动路径期满时间。设置ROUTE_EXPI_TIME为期满时间,单位为秒。如果设备为0,则表示关闭自动期满功能。
(2)表存储
要实现路由功能,需要路由器建立一些表格去保持和维护路由信息。
① 路由表。每一个路由表包括协调器都包含一个路由表。设备在路由表中保存了数据包参与路由所需的信息。每一条路由表记录都包含目的地址、下一级节点和连接状态等信息。所有数据包都通过相邻的一级节点发送到目的地址。同样,为了回收路由表空间,可以终止路由表中的那些已经无用的路径记录。在文件f8wConfig.cfg 中配置路由表的大小,将MAX_RTG_ENTRIES设置为表的大小(不能小于4)。
② 路径寻找表。路径寻找表用来保存寻找过程中的临时信息。这些记录只是在路径寻找操作期间存在,一旦某个记录到期,它就可以被另一个路径寻找所使用。记录的个数决定了在一个网络中可以同时并发执行的路径寻找的最大个数。这个值MAX-RREQ-ENTRIES可以通过在f8wConfig.cfg文件中配置。
5.安全
为了保证一个ZigBee网络通信的保密性,防止重要数据被窃取,ZigBee协议还可以采用AEC/CCM安全算法,提供可选的安全功能。在一个安全的网络中,协调器可以允许或者不允许节点加入网络,也可以只允许一个设备在很短的时间窗口加入网络。例如,协调器上有一个“push”按键,在这个很短的时间窗口中,它允许任何设备加入网络,否则,所有的加入请求都被拒绝。
3.2.4 应用层
应用层主要包括应用支持子层(APS层)和ZigBee设备对象(ZDO)。其中,APS负责维护和绑定表、在绑定设备之间传送消息;而ZDO定义设备在网络中的角色,发起和响应绑定请求,在网络设备之间建立安全机制。
1.绑定
绑定指的是两个节点在应用层上建立起来的一条逻辑链路。在同一个节点上可以建立多个绑定服务,分别对应不同种类的数据包。此外,绑定也允许有多个目标节点(一对多绑定)。
一旦在源节点上建立了绑定,其应用服务即可向目标节点发送数据,而不需指定目标地址(调用zb_SendDataRequest(),目标地址可用一个无效值0xFFFE代替)。这样,协议栈将会根据数据包的命令标识符,通过自身的绑定表查找到所对应的目标设备地址。
在绑定表的条目中,有时会有多个目标端点,这使得协议栈自动地重复发送数据包到绑定表指定的各个目标地址。同时,如果在编译目标文件时,编译选项NV_RESTORE被打开,协议栈将会把绑定条目保存在非易失性存储器里。因此,当意外重启(或者节点电池耗尽需要更换)等突发情况发生时,节点能自动恢复到掉电前的工作状态,而不需要用户重新设置绑定服务。
2.配置文件
配置文件(Profile)就是应用程序框架,它是由ZigBee技术开发商提供的,应用于特定的应用场合,是用户进行ZigBee技术开发的基础。当然,用户也可以使用专用工具建立自己的Profile。Profile是这样一种规范,它规定不同设备对消息帧的处理行为,使不同的设备之间可以通过发送命令、数据请求来实现互操作。
3.端点
端点(EndPoint)是一种网络通信中的数据通信,它是无线通信节点的一个通信部件,如果选择“绑定”方式实现节点间的通信,那么可以直接面对端点操作,而不需要知道绑定的两个节点的地址信息。每个ZigBee设备支持240个这样的端点。端点的值和IEEE长地址、16位短地址一样,是唯一确定的网络地址,通常结合绑定功能一起使用。它是ZigBee无线通信的一个重要参数。
4.簇
间接通信是指各个节点通过端点的绑定建立通信关系,这种通信方式不需要知道目标节点的地址信息,包括IEEE地址或网络短地址,Z-Stack底层将自动从栈的绑定表中查找目标设备的具体网络地址并将其发送出去。
直接通信不需要节点之间通过绑定建立联系,它使用网络短地址作为参数调用适当的API来实现通信。直接通信部分关键点在于节点网络短地址的获得。在发送信息帧之前,必须知道要发送的目标短地址。由于网络协调器的短地址是固定的0X0000,因此可容易地把消息帧发送到协调器。其他网络节点的网络短地址是它们在加入到网络中时由协调器动态分配的,与网络深度、最大路由数、最大节点数等参数有关,没有一个固定值。所以,要想知道目标节点的网络短地址还需要通过其他手段,可以采用通过目标节点的IEEE地址来查询短地址的方法。通常,ZigBee节点的IEEE地址是固定的,它被写在节点的EEPROM中,这个作为ZigBee节点参数一般会被标示在节点上。所以,有了IEEE地址以后,可以通过部分网络API的调用,得到与之对应的网络短地址。
簇(Cluster)就是人们在着手建立Profile时遇到的这个概念,它是一簇网络变量(Attributes)的集合。在同一个Profile中,ClusterID是唯一的。在直接寻址方式和间接寻址方式中都会用到这个概念。在间接寻址方式中,建立绑定表时需要搞清楚它的含义与属性。对于可以建立绑定关系的两个节点,它们的Cluster的属性必须一个选择“输入”,另一个选择“输出”,而且ClusterID值相等,只有这样,它们才能建立绑定。而在直接寻址方式中,常用ClusterID作为参数来将数据或命令发送到对应地址的Cluster(簇)上。
3.3 OSAL多任务分配机制
操作系统抽象层(Operating System Abstraction Layer,OSAL)表面上看是作为操作系统存在的,可是为什么又加上“抽象层”呢?它的本质是什么?在Z-stack协议栈中,它又扮演了什么角色呢?要解答这些问题必须先从宏观入手,渐渐深入浅出,最后答案自然会浮出水面。
3.3.1 OSAL基础知识
这里,先介绍与OSAL有关的基础知识。
1.资源(Resource)
任何任务所占用的实体都可以称为资源,如一个变量、数组、结构体等。
2.共享资源(Shared Resource)
至少可以被两个任务使用的资源称为共享资源。为了防止共享资源被破坏,每个任务在操作共享资源时,必须保证是独占该资源。
3.任务(Task)
一个任务又称为一个线程,是一个简单程序的执行过程。单个任务中CPU完全是被该任务独占的。在任务设计时,需要将问题尽可能地分为多个任务,每个任务独立完成某种功能,同时被赋予一定的优先级,拥有自己的CPU寄存器和堆栈空间。一般将任务设计为一个无限循环。
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。线程和进程的区别在于子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。
4.多任务运行(Muti-Task Running)
多任务运行就是一个线程组,其实质只有一个任务在运行,但是CPU可以使用任务调度策略将多个任务进行调度,每个任务执行特定的时间,时间片到了以后,就进行任务切换,由于每个任务执行时间都很短,因此,任务切换比较频繁,这就造成了多任务同时运行“假象”。
5.内核(Kernel)
在多任务系统中,内核负责管理各个任务,主要包括为每个任务分配CPU时间,任务调度,负责任务间的通信。内核提供的基本的内核服务就是任务切换。使用内核可以大大简化应用系统的程序设计方法。借助内核提供的任务切换功能,可以将应用程序分为不同的任务来实现。
6.互斥(Mutual Exclusion)
多任务通信最简单、最常用的方法是使用共享数据结构。对于嵌入式系统而言,所有任务都在单一的地址空间下,使用共享的数据结构包括全局变量、指针、缓冲区等。虽然共享数据结构的方法简单,但是必须保证对共享数据结构的写操作具有唯一性,以避免晶振和数据不同步。
保护共享资源最常用的方法具体如下。
① 关中断。
② 使用测试并置位指令(T&S指令)。
③ 禁止任务切换。
④ 使用信号量。
其中,在ZigBee协议栈中,OSAL中经常使用的方法是关中断。
7.消息队列(Message Queue)
消息队列用于任务间传递消息,通常包含任务间同步的信息。通过内核提供的服务、任务或者中断服务程序将一条消息放入消息队列,然后,其他任务可以使用内核提供的服务从消息队列中获取属于自己的消息。为了降低传递消息的开支,通常传递指向消息的指针。
在ZigBee协议栈中,OSAL主要提供如下功能。
① 任务注册、初始化和启动。
② 任务间的同步、互斥。
③ 中断处理。
④ 存储器的分配和管理。
3.3.2 OSAL简介
Z-Stack是TI公司开发的ZigBee协议栈,并经过ZigBee联盟认可而被全球众多开发商所广泛采用。Z-Stack的采用基于一个轮转查询式操作系统,可帮助程序员方便地开发一套ZigBee系统。
TI的Z-Stack协议栈就是基于一个最基本的轮转查询式操作系统,这个操作系统就是操作系统抽象层。在ZigBee协议中,协议本身已经定义了大部分内容。在基于ZigBee协议的应用开发中,用户只需要实现应用程序框架即可。从图3.3中可以看出应用程序框架中包含了最多240个应用程序对象。如果将一个应用程序对象视为一个任务的话,那么应用框架将包含一个支持多任务的资源分配机制。于是OSAL便有了存在的必要性,它正是Z-Stack为了实现这样一个机制而存在的。
图3.3 ZigBee协议的结构图
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”是某一特定层提供的服务与上层之间的接口。大多数层有数据实体接口和管理实体接口两个接口。
数据实体接口的目标是向上层提供所需的常规数据服务;管理实体接口的目标是向上层提供访问内部层的参数、配置和管理数据服务。
物理层和媒体接入控制子层均属于IEEE 802.15.4标准,而IEEE 802.15.4标准与网络/安全层、应用层一起,构成了ZigBee协议栈。
3.3.3 协议栈软件架构
Z-Stack采用事件轮询机制来设计操作系统,当各层初始化之后,系统进入低功耗模式,当事件发生时,唤醒系统,开始进入中断处理事件,处理结束后继续进入低功耗模式。如果同时有几个事件发生,则判断优先级,逐次处理事件。这种软件构架可以极大地降级系统的功耗。
整个Z-Stack的主要工作流程如图3.4所示,大致分为系统启动、驱动初始化、OSAL初始化和启动、进入任务轮询几个阶段。
图3.4 Z-Stack系统运行流程图
1.系统初始化
系统上电后,通过执行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口方向和控制函数定义等。
3.4 OSAL的运行机制
弄明白了OSAL是何方神圣,接下来深入Z-Stack,进一步研究OSAL。为了方便,使用Z-Stack所提供的SampleApp例程来进行分析。在此例程的默认路径C:\Texas Instruments\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.5 OSAL任务调度流程图
由此推断Z-Stack应用程序的运行机制如图3.6所示。
图3.6 OSAL的运行机制
那么,事件和任务的事件处理函数究竟是如何联系的呢?
ZigBee协议栈采用的方法是,建立一个事件表,保存各个任务对应的事件,建立另一个函数表,保存各个任务事件处理函数的地址,然后将这两张表建立某种对应关系,当某一事件发生时则查找函数表即可。
OSAL用什么样的数据结构来实现事件表和函数表呢?如何将事件表和函数表建立对应关系呢?
OSAL通过tasksEvents指针访问事件表的每一项,如果有事件发生,则查找函数表找到事件处理函数进行处理,处理完后,继续访问事件表,查看是否有事件发生,无限循环。
在ZigBee协议栈中,3个关键变量其数据结构具体如下。
① tasksCnt。该变量保存了任务数,其声明为const uint8 tasksCnt,其中uint8的定义为typedef unsigned char uint8。tasksCnt变量的定义在OSAL SampleApp.c文件中。
② tasksEvents。该变量是一个指针,指向了事件表的首地址,其声明为uint16 *tasksEvents,其中uint16的定义为typedef unsigned short uint16。tasksEvents[]是一个指针数组,只是在OSAL_SampleApp.c文件中进行定义。
③ tasksArr。该变量是一个数组,该数组的每一项都是一个函数指针,指向了事件的处理函数,其声明为pTaskEventHandlerFn tasksArr[],其中pTaskEventHandlerFn 的定义为typedef unsigned short(*pTaskEventHandlerFn)(unsigned char task_id,unsigned short event)。变量pTaskEventHandlerFn的定义在OSAL_Tasks.h文件中。
OSAL中最大任务数量为9,最大事件数量为16。
const uint8 tasksCnt = sizeof( tasksArr ) / sizeof( tasksArr[0] ); //最大任务数量为9
uint16 *tasksEvents; //最大事件数量为16
OSAL是一种基于事件驱动的轮询式操作系统,事件驱动是指发生事件后采取相应的事件处理方法,轮询指的是不断地查看是否有事件发生。OSAL调度机制如下。
① 入口程序为Zmain.c。
② 执行main()主程序。
③ 任务调度初始化osal_init_system()。
④ 默认启动了osalInitTasks(),最多9个任务,添加到队列,序号为0~8。
⑤ 最后通过调用 SampleApp_Init()实现用户自定义任务的初始化(用户根据项目需要修改该函数)。
3.4.1 OSAL任务启动和初始化
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.8 ZMain. c函数布局示意图
图3.9 系统流程图及对应函数
其代码如下。
int main( void )
{
// Turn off interrupts关闭中断
osal_int_disable( INTS_ALL );
// Initialization for board related stuff such as LEDs
HAL_BOARD_INIT();
// Make sure supply voltage is high enough to run电压检测,最好保证芯片能正常工作的电压。
zmain_vdd_check();
// Initialize board I/O初始化板载IO
InitBoard( OB_COLD );
// Initialze HAL drivers初始化HAL
HalDriverInit();
// Initialize NV System初始化NV系统
osal_nv_init(NULL);
// Initialize the MAC
ZMacInit();
// Determine the extended address
zmain_ext_addr();
// Initialize basic NV items
zgInit();
#ifndef NONWK
// Since the AF isn't a task, call it's initialization routine
afInit();
#endif
// Initialize the operating system
osal_init_system();
// Allow interrupts
osal_int_enable(INTS_ALL);
// Final board initialization
InitBoard(OB_READY);
// Display information about this device
zmain_dev_info();
/* Display the device info on the LCD */
#ifdef LCD_SUPPORTED
zmain_lcd_init();
#endif
#ifdef WDT_IN_PM1
/* If WDT is used, this is a good place to enable it. */
WatchDogEnable(WDTIMX);
#endif
osal_start_system(); // No Return from here
return 0; // Shouldn't get here
} // main()
总之,任务初始化的主要步骤如下。
① 初始化应用服务变量。const pTaskEventHandlerFn tasksArr[]数组定义系统提供的应用服务和用户服务变量,如MAC层服务macEventLoop、用户服务SampleApp_ProcessEvent等。
② 分配任务ID和分配堆栈内存。void osalInitTasks(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任务均不自行注册系统服务,两者均需要由用户应用程序注册。值得注意的是,有且仅有一个OSAL Task可以注册服务。例如,注册键盘活动响应可调用RegisterForKeys()函数。
⑤ 处理任务事件。处理任务事件通过创建“ApplicationName”_ProcessEvent()函数处理。一个OSAL任务可以响应16个事件,除了协议栈默认的强制事件(Mandatory Events)之外还可以再定义15个事件。
SYS_EVENT_MSG(0x8000)是强制事件。该事件主要用来发送全局的系统信息,包括以下信息。
AF_DATA_CONFIRM_CMD:该信息用来指示通过唤醒AF DataRequest()函数发送的数据请求信息的情况。ZSuccess确认数据请求成功的发送。如果数据请求是通过AF_ACK_ REQUEST置位实现的,那么ZSussess可以确认数据正确地到达目的地。否则,ZSucess仅仅能确认数据成功地传输到了下一个路由。
AF_INCOMING_MSG_CMD:用来指示接收到的AF信息。
KEY_CHANGE:用来确认按键动作。
ZDO_NEW_DSTADDR:用来指示自动匹配请求。
ZDO_STATE_CHANGE:用来指示网络状态的变化。
3.4.2 OSAL任务的执行
启动代码为操作系统的执行做好准备工作后,就开始执行操作系统入口程序,并由此彻底将控制权移交给操作系统,完成新老更替。
其实,操作系统实体只有一行代码。
Osal_start_system();//运行系统[OSAL.c],进入系统调度,无返回
可以看到这句代码的注释,本函数不会返回,也就是说它是一个死循环,永远不可能执行完,即操作系统从启动代码接到程序的控制权之后,就不会将权力释放。这个函数就是轮转查询式操作系统的主体部分,它所做的工作就是不断地查询每个任务中是否有事件发生,如果发生,就执行相应的函数;如果没有发生,就查询下一个任务。
osal_start_system(); //此函数是任务系统的主循环函数,它将轮询所有任务事件然后调用相关的任务处理函数,没有任务时将进入休眠状态。
函数的主体部分代码:
void osal_start_system(void)//此函数是任务系统的主循环函数,它将轮询所有任务事件然后调用相关的任务处理函数,没有任务时将进入休眠状态。
{
#if !defined ( ZBIT ) && !defined ( UBIT )
For(;;) // Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate();
Hal_ProcessPoll(); // This replaces MT_SerialPoll() and osal_check_timer().OSAL调用此函数来推送UART、TIMER
[hal_drivers.c],
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);
events = (tasksArr[idx])( idx, events );
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
}
}
操作系统专门分配了存放所有任务事件的 tasksEvents[]数组,每一单元对应存放着每一个任务的所有事件。在这个函数中,首先通过一个do-while循环来遍历tasksEvents[],找到第一个具有事件的任务(即具有待处理事件的优先级最高的任务,因为序号低的任务优先级高),然后跳出循环,此时,就得到了有事件待处理的最高优先级的任务的序号idx,然后通过events-tasksEvents[idx]语句,将这个当前具有最高优先级的任务的事件取出,接着就调用(taskeArr[idx](idx,events)函数来执行具体的处理函数。taskArr[]是一个函数指针的数组,根据不同的idx就可以执行不同的函数。
事件表和函数表的关系如图3.10所示。
图3.10 事件表和函数表的关系示意图
首先介绍一下tasksArr 、tasksEvents(在OSAL_SampleApp.c文件中)。
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,
};
const uint8 tasksCnt = sizeof( tasksArr) / sizeof( tasksArr[0] );
uint16 *tasksEvents;
TaskArr这个数组里存放了所有任务的事件处理函数的地址,在这里事件处理函数就代表了任务本身,也就是说事件处理函数标识了与其对应的任务。tasksCnt这个变量保存了当前的任务个数,最大任务数量为9。
tasksEvents是一个指向数组的指针,此数组保存了当前任务的状态。OSAL每个任务可以有16个事件,其中SYS_EVENT_MSG定义为0x8000,为系统事件,用户可以定义剩余的15个事件。
SYS_EVENT_MSG是由协议栈定义的系统强制事件(Mandatory Events),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,需要给每一个任务定义一个全局变量。
其中,任务处理函数具体如下。
macEventLoop, //MAC层任务处理函数
nwk_event_loop, //网络层任务处理函数
Hal_ProcessEvent, //硬件抽象层任务处理函数
MT_ProcessEvent, //监控任务处理函数可选(透过编译选项MT_TASK来决定是否编译该任务处理函数,一般情况下该功能通过串行端口通信来交换实现)
APS_event_loop, //应用支持子层任务处理函数,用户不用修改
APSF_ProcessEvent, //应用支持子层消息分割任务处理函数(用户编译选项ZIGBEE_ FRAGMENTATION来决定是否启动ZigBee消息分割功能)
ZDApp_event_loop, //设备应用层任务处理函数,用户可以根据需要修改
ZDNwkMgr_event_loop, //网络管理层任务处理函数(用户可透过编译选项ZIGBEE_ FREQ_AGILITY或ZIGBEE_PANID_CONFIG来实现该功能)
SampleApp_ProcessEvent, //用户应用层任务处理函数,用户自己编写
如果不算调试任务,操作系统一共要处理6项任务,分别为MAC层,网络层、硬件抽象层、应用层、ZigBee设备应用层以及完全由用户处理的应用层,其优先级由高到低。MAC层任务具有最高优先级,用户层具有最低的优先级。Z-Stack已经编写了对从MAC层到ZigBee设备应用层这5层任务的事件处理函数,一般情况下不需要修改这些函数,只需要按照自己的需求编写应用层的任务及事件处理函数即可。
Z-Stack已经编写了对MAC层(macEventLoop)到ZigBee设备应用层(ZDApp_event_loop)这5层任务的事件处理函数,一般情况下无需要修改这些函数,只需要按照自己的需求编写应用层的任务及事件处理函数即可。
Z-Stack的协议栈架构及操作系统实体如图3.11所示。
图3.11 Z-Stack的协议栈架构及操作系统实体
TI的Z-Stack中给出了几个例子来演示Z-Stack协议栈,每个例子对应一个项目。对于不同的项目来说,大部分代码都是相同的,只是在用户应用层,添加不同的任务及事件处理函数。
明白了这个问题,新的问题又摆在了我们面前:OSAL是如何传递事件给任务的呢?
3.4.3 OSAL的事件传递机制
在试图弄清楚这个问题之前,我们需要弄清楚另外一个十分基础而重要的问题。消息、事件、任务之间到底存在什么样的关系呢?如何实现事件传递机制呢?
事件是驱动任务去执行某些操作的条件,当系统中产生了一个事件,OSAL将这个事件传递给相应的任务后,任务才能执行一个相应的操作(调用事件处理函数去处理)。
通常某些事件发生后,又伴随着一些附加信息的产生。例如,从天线接收到数据后,会产生AF_INCOMING_MSG_CMD消息,但是任务的事件处理函数在处理这个事件的时候,还需要得到所接收到的数据。
因此,这就需要将事件和数据封装成一个消息,将消息发送到消息队列,然后在事件处理函数中就可以使用osal_msg_receive,从消息队列中得到该消息,即:
MSGpkt = (afIncomingMSGPacket_t *) osal_msg_receive(SampleicApp_TaskID);
OSAL维护了一个消息队列,每一个消息都会被放到这个消息队列中去,当任务接收到事件后,可以从消息队列中获取属于自己的消息,然后再调用消息处理函数进行相应的处理。
OSAL中的消息队列如图3.12所示。
图3.12 OSAL中的消息队列
每个消息都包含一个消息头osal_msg_hdr_t和用户自定义的消息,osal_msg_hdr_t结构体的定义如下。
typedef struct
{
void *next;
uint16 len;
uint8 dest_id;
}osal_msg_hdr_t;
进入事件轮询后的第一个事件是网络状态变化事件,其处理函数为SampleApp_
ProcessEvent()。网络状态变化事件与节点功能(根据节点功能分为协调器、路由/节点)有一定关联。
(1)协调器
从没有网络到组建起网络,触发网络状态变更事件ZDO_STATE_CHANGE。
(2)路由/节点
从没有接入网络到接入网络,触发网络状态变更事件ZDO_STATE_CHANGE。
其处理方法如下。
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
这个表示默认启动第2个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVT。
osal_start_timerEx( SampleApp_TaskID,
SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT ); //5s 定时事件
}
else
{
// Device is no longer in the network
}
break;
协议栈默认启动了第2个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVT,其处理函数SampleApp_ProcessEvent()。
其处理方法如下。
//定时事件处理功能
if ( events & SAMPLEAPP_SEND_PERIODIC_MSG_EVT ) //匹配成功 SAMPLEAPP_SEND_PERIODIC_MSG_EVT 事件
{
// Send the periodic message
SampleApp_SendPeriodicMessage();//定时事件具体处理函数
// Setup to send message again in normal period (+ a little jitter) 默认启动下一个事件SAMPLEAPP_SEND_PERIODIC_MSG_EVT
osal_start_timerEx(SampleApp_TaskID, SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
(SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & 0x00FF);
// return unprocessed events
return (events ^ SAMPLEAPP_SEND_PERIODIC_MSG_EVT);
}
3.4.4 OSAL添加新任务
在使用ZigBee协议栈进行程序开发时,OSAL如何在应用程序中添加一个新任务呢?
在Z-Stack中,对于每个用户自己新建立的任务通常需要两个相关的处理函数,具体如下。
(1)新任务的初始化函数
例如,SampleApp_Init(), 这个函数是在osalInitTasks()这个OSAL(Z-Stack中自带的小操作系统)中去调用的,其目的就是把一些用户自己写的任务中的一些变量、网络模式、网络终端类型等进行初始化,并且自动给每个任务分配一个ID。
(2)新任务的事件处理函数
例如,SampleApp_ProcessEvent(),这个函数是首先在const TaskEventHandlerFntasksArr[ ] 中进行设置,然后在osalInitTasks()中如果发生事件进行调用绑定的事件处理函数。
下面分3个部分进行分析。
1.用户自己设计的任务代码在Z-Stack中的调用过程
① 首先,执行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()中增加。
void osalInitTasks( void )
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
macTaskInit( taskID++ );
nwk_init( taskID++ );
Hal_Init( taskID++ );
#if defined( MT_TASK )
MT_TaskInit( taskID++ );
#endif
APS_Init( taskID++ );
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_Init( taskID++ );
#endif
ZDApp_Init( taskID++ );
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_Init( taskID++ );
#endif
SampleApp_Init( taskID++ );//用户自己需要增加的任务在SampleApp_Init()添加
NewProcessApp_Init( taskID++ ); //新增加的用户任务1的初始化函数
NewProcess2App_Init( taskID ); //新增加的用户任务2的初始化函数
}
2.任务处理调用的重要数据结构
这里要解释一下,在Z-Stack里,对于同一个任务可能有多种事件发生,那么需要执行不同的事件处理,为了方便,对于每个任务的事件处理函数都统一在一个处理函数中实现,然后根据任务的ID号(task_id)和该任务的具体事件(events)调用某个任务的总事件处理函数,进入了该任务的总事件处理函数之后,再根据events来判别是该任务的哪一种事件发生,进而执行相应的事件处理。pTaskEventHandlerFn 是一个指向函数(事件处理函数)的指针,这里实现的每一个数组元素各对应于一个任务的事件处理函数,比如SampleApp_ProcessEvent对应于用户自行实现的事件处理函数uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events ),所以这里如果我们实现了一个任务,还需要把实现的该任务的事件处理函数在这里添加。
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,
NewProcessApp_ProcessEvent, //新增第1个任务处理函数
NewProcess2App_ProcessEvent, // 新增第2个任务处理函数
};
注意:tasksEvents和tasksArr[]里的顺序是一一对应的,tasksArr[]中的第i个事件处理函数对应于tasksEvents中的第i个任务的事件。
//计算出任务的数量
const uint8 tasksCnt = sizeof( tasksArr ) / sizeof( tasksArr[0] );
uint16 *tasksEvents;
3.对于不同事件发生后的任务处理函数的调用
osal_start_system() 很重要,决定了当某个任务的事件发生后调用对应的事件处理函数。
对应调用第idx个任务的事件处理函数,用events说明是什么事件。
events = (tasksArr[idx])( idx, events );
用户自定义功能在NewProcess App.c文件中利用NewProcessApp_ProcessEvent ()函数实现,其程序代码如下。
uint16 NewProcessApp_ProcessEvent( uint8 task_id, uint16 events )
{
afIncomingMSGPacket_t *MSGpkt;
(void)task_id; // Intentionally unreferenced parameter
if ( events & SYS_EVENT_MSG )
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( NewProcessApp_TaskID );
while ( MSGpkt )
{
switch ( MSGpkt->hdr.event )
{
// Received when a key is pressed
case KEY_CHANGE:
NewProcessApp_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:
NewProcessApp_MessageMSGCB( MSGpkt );
break;
// Received whenever the device changes state in the network
case ZDO_STATE_CHANGE:
NewProcessApp_NwkState = (devStates_t)(MSGpkt->hdr.status);
if ( (NewProcessApp_NwkState == DEV_ZB_COORD)
|| (NewProcessApp_NwkState == DEV_ROUTER)
|| (NewProcessApp_NwkState == DEV_END_DEVICE) )
{
// Start sending the periodic message in a regular interval.
osal_start_timerEx( NewProcessApp_TaskID,
NEWAPP_SEND_PERIODIC_MSG_EVT,
NEWAPP_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( NewProcessApp_TaskID );
}
// return unprocessed events
return (events ^ SYS_EVENT_MSG);
}
// Send a message out - This event is generated by a timer
// (setup in NewProcessApp_Init()).
if ( events & NEWAPP_SEND_PERIODIC_MSG_EVT )
{
// Send the periodic message
NewProcessApp_SendPeriodicMessage();
HalLedSet(HAL_LED_1,HAL_LED_MODE_TOGGLE);
// HalLedBlink( HAL_LED_1, 4, 50, 100 );
// Setup to send message again in normal period (+ a little jitter)
osal_start_timerEx( NewProcessApp_TaskID, NEWAPP_SEND_PERIODIC_MSG_EVT,
(NEWAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & 0x00FF)) );
// return unprocessed events
return (events ^ NEWAPP_SEND_PERIODIC_MSG_EVT);
}
// Discard unknown events
return 0;
}
用户自定义功能在利用NewProcess2App_ProcessEvent ()函数实现程序代码类似。
注意:需要在NewProcess App.h文件中添加新增函数声明:
extern void NewProcessApp_Init( uint8 task_id );
extern UINT16 NewProcessApp_ProcessEvent( uint8 task_id, uint16 events );
注意:需要在OSAL_SampleApp.c文件中添加新增函数声明:
#include "NewProcessApp.h"
#include "NewProcess2App.h"
在NEW Process APP.C文件中void NewApp_Init( uint8 task_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(Hardware Abstraction Layer)翻译为“硬件抽象层”。许多人在这里经常把Z-Stack的硬件抽象层与ZigBee的物理层混为一谈。在这里,我们应该将其区分开来。硬件抽象层所包含的范围是我们当前硬件电路上面所有对于系统可用的设备资源,而ZigBee的物理层则是针对无线通信而言的,它所包含的仅限于无线通信的硬件设备。
通过这个重要的信息,我们可以得到这样一个结论:OSAL将硬件的管理也作为一个任务来处理。那么,我们很自然地去寻找Hal_ProcessEvent这个事件处理函数,看看它究竟是如何管理硬件资源的。
在“HAL\Commen\hal_drivers.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
Return events ^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()完成什么功能?
代码中给出的注释如下。
/*Check for keys*/
HalKeyPoll();
于是,我们推断这个函数的作用是检查当前的按键情况。进入函数一看,果不其然。虽然这个函数很长很复杂,但经过一系列的if语句和赋值语句,在接近函数末尾的地方,keys变量(在函数起始位置定义的)获得了当前按键的状态。最后,有一个十分重要的函数调用。
(pHalKeyProcessFunction)(keys,HAL_KEY_STATE_NORMAL);
虽然不清楚pHalKeyProcessFunction这个函数指针指向哪个函数,但是我们知道这里调用的是void OnBoard _KeyCallback(uinte keys,uint8 state)函数。
此函数在“ZMain\OnBoard.c”文件中可以找到。在此函数中,又调用了void OnBoard_sendKeys(uint8 keys,uint8 state),按键的状态信息被封装到了一个消息结构体中。最后有一个极其重要的函数被调用了。
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()函数获取当前按键的状态,并且通过调用void OnBoard _KeyCallback(uinte keys,uint8 state)函数向SampleApp任务发送一个按键消息,并且设置tasksEvents中GenericApp所对应的值为非零。此时,main函数里有如下一段代码。
{
do
{
If(tasksEvents[ids])
{
break;
}
} while (++idx < tasksCnt);
}
执行了此段代码以后,SampleApp任务就会被挑选出来,然后通过执行以下代码,这个函数调用其事件处理函数,完成事件的响应。
{
events = (tasksArr[idx])( idx, events );
}
3.5 OSAL应用编程接口
OSAL提供了8个方面的应用编程接口(Application Programming Interface,API)解决多任务间同步和互斥,具体包括消息管理、任务同步、时间管理、中断管理、任务管理、内存管理、电源管理和非易失性闪存管理。
1.消息管理API
消息管理API主要用于处理任务间消息的交换,主要包括任务分配消息缓存、释放消息缓存、接收消息和收送消息等API函数。
① osal_msg_allocate()
函数原型:uint8 *osal _msg_allocate(uint16 len)。
功能描述:为消息分配缓存空间。
② osal_msg_deallocate()
函数原型:uint8*osal _msg_allocate(uint8 *msg_ptr)。
功能描述:释放消息的缓存空间。
③ osal_msg_send()
函数原型:uint8 osal_msg_send(uint8 destination_task,uint8 *msg_ptr)。
功能描述:一个任务发送消息到消息队列。
④ osal_msg_receive()
函数原型:uint8 *osal_msg_receive(uint8 task_id)。
功能描述:一个任务从消息队列接收属于自己的消息。
2.任务同步API
任务同步API主要用于任务间的同步,允许一个任务等待某个事件的发生。
osal_set_event()
函数原型:uint8 osal _set_event(uint8 task_id,uint16 event_flag)。
功能描述:运行一个任务时设置某一事件同时发生。
3.时间管理API
时间管理API用于开启和关闭定时器,定时时间一般为毫秒级定时,使用该API,用户不必关心底层定时器是如何初始化的,只需要调用即可,在ZigBee协议栈物理层已经将定时器初始化了。
① osal_start_timerEx()
函数原型:uint8 osal _start_timerEx(uint8 task_id,uint16 event_id,uint16 timeout_value)。
功能描述:设置一个定时器时间,定时时间到后,相应的事件被设置。
② osal_stop_timerEx()
函数原型:uint8 osal _stop_timerEx(uint8 task_id,uint16 event_id)。
功能描述:停止已经启动的定时器。
4.中断管理API
中断管理API主要用于控制中断的开启与关闭,一般很少使用。
5.任务管理API
任务管理API主要是对OSAL进行初始化和启动。
① osal_init_system()
函数原型:uint8 osal _start_system(void)。
功能描述:初始化OSAL,该函数是第一个被调用的OSAL函数。
② osal_start_system()
函数原型:uint8 osal _start_system(void)。
功能描述:该函数包含一个无限函数,它将查询所有的任务事件,如果有事件发生,则调用相应的事件处理函数,处理完该事件后,返回主循环继续检测是否有事件发生,如果开启了节能模式,则没有事件发生时,该函数将使处理器进入休眠模式,以降低系统功耗。
6.内存管理API
内存管理API用于在堆栈上分配缓冲区。注意以下两个API函数必须成对使用,防止产生内存泄漏。
① osal_mem_alloc()
函数原型:uint8 osal _mem_alloc(uint16 size)。
功能描述:在堆栈上分配指定大小的缓冲区。
② osal_mem_free()
函数原型:uint8 osal _mem_free(void *ptr)。
功能描述:释放使用osal_mem_alloc()分配的缓冲区。
7.电源管理API
电源管理API主要用于电池供电的ZigBee网络节点,在此不作讨论。
8.非易失性闪存管理API
非易失性闪存(Non-Volatile Memory,NV)管理API主要添加了对非易失性闪存的管理函数,一般这里的非易失性闪存指的是系统Flash存储器(也可以是E2PROM),每个NV条目分配唯一的ID号。
① osal_nv_item_init()
函数原型:byte osal _nv_item_init(uint16 id,uint16 len,void *buf)。
功能描述:初始化NV条目,该函数检查是否存在NV条目,如果不存在,它将创建并初始化该条目;如果该条目存在,每次调用osal_nv_read()osal_nv_write()。
② osal_nv_read()
函数原型:byte osal _nv_read(uint16 id,uint16 offset,void *buf)。
功能描述:从NV条目中读取数据;可以读取整个条目的数据,也可以读取部分数据。
③ osal_nv_write()
函数原型:uint8 osal_nv_write(uint16 id,uint16 offset,uint16 len,void *buf)。
功能描述:写数据到NV条目。
3.6 OSAL应用编程
1.组网成功测试点灯
利用ZDO_STATE_CHANGE实现组网成功点灯功能,程序代码如下。
int16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
{
afIncomingMSGPacket_t *MSGpkt;
(void)task_id; // Intentionally unreferenced parameter
if ( events & SYS_EVENT_MSG )
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SampleApp_TaskID );
while ( MSGpkt )
{
switch ( MSGpkt->hdr.event )
{
// 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) )
{
//点灯 ?
P1SEL &= ~0x3;
P1DIR |= 0x3; // 定义P10、P11为输出
P1_0 = 1;
// 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;
}
...
return 0;
}
2.定时事件测试
利用SAMPLEAPP_SEND_PERIODIC_MSG_EVT实现LED灯的定时翻转功能,其程序代码如下。
uint16 SampleApp_ProcessEvent( uint8 task_id, uint16 events )
{
afIncomingMSGPacket_t *MSGpkt;
(void)task_id; // Intentionally unreferenced parameter
if ( events & SYS_EVENT_MSG )
{
...
default:
break;
}
if ( events & SAMPLEAPP_SEND_PERIODIC_MSG_EVT )
{
P1_0 ^= 1; //反转灯测试定时事件的到来
// Send the periodic message
SampleApp_SendPeriodicMessage(); //定时事件的具体处理函数→协调器与节点需要区分开
// Setup to send message again in normal period (+ a little jitter)
osal_start_timerEx( SampleApp_TaskID, SAMPLEAPP_SEND_PERIODIC_MSG_EVT,
(SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & 0x00FF)) );
// return unprocessed events
return (events ^ SAMPLEAPP_SEND_PERIODIC_MSG_EVT);
}
return 0;
}
//默认的定时事件的具体处理函数
void SampleApp_SendPeriodicMessage( void )
{
//调用AF_DataRequest实现数据包发送
if ( AF_DataRequest( &SampleApp_Periodic_DstAddr,
&SampleApp_epDesc,
SAMPLEAPP_PERIODIC_CLUSTERID,
1,
(uint8*)&SampleAppPeriodicCounter,
&SampleApp_TransID,
AF_DISCV_ROUTE,
AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
{
}
else
{
// Error occurred in request to send.
}
}
项目小结
(1)协议定义的是一系列的通信标准,通信双方需要共同按照这一标准进行正常的数据收发;协议栈是协议的具体实现形式。
(2)afStatus_t AF_DataRequest( afAddrType_t *dstAddr, endPointDesc_t *srcEP,
uint16 cID, uint16 len, uint8 *buf, uint8 *transID,
uint8 options, uint8 radius )
用户调用该函数即可实现数据的无线发送。
(3)TaskArr数组里存放了所有任务的事件处理函数的地址,在这里事件处理函数就代表了任务本身,也就是说事件处理函数标识了与其对应的任务。变量tasksCnt保存了当前的任务个数,最大任务数量为9。
(4)tasksEvents是一个指向数组的指针,此数组保存了当前任务的状态。OSAL每个任务可以有16个事件,其中SYS_EVENT_MSG定义为0x8000,为系统事件,用户可以定义剩余的15个事件。
(5)在Z-Stack中,对于每个用户自己新建立的任务通常需要两个相关的处理函数,包括新任务的初始化函数和新任务的事件处理函数。
主要概念
协议、协议栈、OSAL运行机制、事件传递机制。
实训项目
任务 在Z-Stack协议栈添加新任务
[任务目标]
(1)熟悉Z-Stack协议栈的源文件架构。
(2)熟悉Z-Stack常见接口函数的调用。
(3)学习Z-Stack下OSAL增加任务的方法。
[内容与要求]
sampleApp任务定时改变蓝灯的状态,NewApp任务定时改变黄灯的状态。
实训考核
任务 在Z-Stack协议栈添加新任务
考核要素 | 评价标准 | 分值 (分) | 评分(分) | ||||
---|---|---|---|---|---|---|---|
自评(10%) | 小组(10%) | 教师(80%) | 专家(0%) | 小计(100%) | |||
进行新任务初始化和事件处理 | ① 进行新任务初始化和事件处理函数定义 | 40 | |||||
新任务函数的调用 | ② 新任务函数的调用 | 30 | |||||
分析总结 | 30 | ||||||
合计 | |||||||
评语(主要是建议) |
实训参考
任务 在Z-Stack协议栈添加新任务
一、实验设备
实 验 设 备 |
数量 |
备 注 |
---|---|---|
ZigBee Debugger仿真器 |
1 |
下载和调试程序 |
CC2530节点 |
1 |
调试程序 |
USB线 |
1 |
连接PC机、网关板、调试器 |
RS232串口连接线 |
1 |
调试程序 |
SmartRF Flash Programmer软件 |
1 |
烧写物理地址软件 |
电源 |
5 |
供电 |
Z-Stack-CC2530-2.3.0-1.4.0 |
1 |
协议栈软件 |
二、实验基础
1.协议栈介绍
一般情况下,只需要额外添加3个文件就可以完成一个项目。这3个文件具体如下。
① 主控文档taskApp.c。该文件存放具体的任务事件处理函数,如GenericApp_ProcessEvent。
② taskApp.h。该文件为主控文件的头文件。
③ 操作系统接口文件OSALtaskApp.c。该文件存放任务组constpTaskEventHandleFn tasksArr[]。任务数组的具体内容为每个任务的相应的处理函数指针;存放任务初始化函数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中断操作
① 允许中断
Byte osal_int_enable(byte interrupt_id)
interrupt_id:中断标识符。
② 禁止中断
byte osal_int_disable(byte interrupt_id)
interrupt_id:中断标识符。
③ 暂停中断
HAL_ENTER_CRITICAL_SECTION(x)
④ 重新启动中断
HAL_EXIT_CRITICAL_SECTION(x)
(2)OSAL内存操作
① 分配内存
void *osal_mem_alloc(uint16 size)
② 释放内存
Void osal_mem_free(void *ptr)
(3)OSAL消息传递
① 分配消息缓冲区
byte *osal_msg_allocate(uint16 len)
② 发送信息
byte osal_msg_send(byte destination_task,byte *msg_ptr)
destination_task:接收信息任务的标示符。
msg_ptr:消息指针。
③ 接收信息
byte *osal_msg_receive(byte task_id)
task_id:接收任务的ID。
④ 释放消息缓冲区
byte osal_msg_deallocate(byte *msg_ptr)
msg_ptr:消息指针。
(4)OSAL任务管理
① 任务初始化
byte osal_init_system(void)
要创建的任务列表。
② 任务开始
Void osal_self(void)
系统任务的主循环函数。
③ 获取活动任务
IDbyte osal_self(void)
中断服务子程序中调用将会发生错误。
(5)OSAL定时器
① 启动定时器
byte osal_start_timerEx(byte taskID,UINT16 event_id,UINT16 timeout_value)
taskID:定时器终止时事件任务的任务ID。
event_id:用户定义的事件,时间终止时通知这个事件。
timeout_value:定时器设置定时参数,单位为毫秒。
② 停止定时器
Byte osal_stop_timerEx(byte task_id,UINT16 event_id)
task_id:事件任务的任务ID。
event_id:用户自定义事件。
③ 读取系统时钟
Uint32 osal_GetSystemClock(void)
用来读取系统时钟(毫秒级)。
三、实现步骤
在Z-Stack中,对于每个用户自己新建立的任务通常需要两个相关的处理函数,具体如下。
① 用于初始化的函数,如NewProcessApp_Init(),将在osalInitTasks()中调用,其目的就是把用户自定义的任务中的一些变量(如网络模式、网络终端类型等)进行初始化。
② 用于该任务新事件发生后所需要执行的事件处理函数,如NewProcessApp_ProcessEvent(),首先在const pTaskEventHandlerFn tasksArr[]中进行设置(绑定),然后在系统运行期间中如果某任务发生新事件,则进行调用绑定的事件处理函数。
其操作步骤如下。
(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文件中找到如下代码。
void osalInitTasks( void )
{
uint8 taskID = 0;
tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt);
osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt));
macTaskInit( taskID++ );
nwk_init( taskID++ );
Hal_Init( taskID++ );
#if defined( MT_TASK )
MT_TaskInit( taskID++ );
#endif
APS_Init( taskID++ );
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_Init( taskID++ );
#endif
ZDApp_Init( taskID++ );
#if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT )
ZDNwkMgr_Init( taskID++ );
#endif
SampleApp_Init( taskID++ );
NewApp_Init( taskID ) //新增加的用户任务初始化函数
}
(2)添加新任务处理调用的事件处理函数
在Z-Stack里,对于同一个任务可能有多种事件发生,那么需要执行不同的事件处理。为了方便,对于每个任务的事件处理函数都统一在一个事件处理函数中实现,然后根据任务的ID号(task_id)和该任务的具体事件(events)调用某个任务的事件处理函数。进入了该任务的事件处理函数之后,再根据events再来判别是该任务的哪一种事件发生,进而执行相应的事件处理。
pTaskEventHandlerFn是一个指向函数(事件处理函数)的指针,这里实现的每一个数组元素各对应于一个任务的事件处理函数。例如,SampleApp_ProcessEvent对应于系统默认的事件处理函数uint16 SampleApp_ProcessEvent( uint8 task_id,uint16 events ),所以如果我们实现了一个任务,还需要在此把实现的该任务的事件处理函数进行添加。
const pTaskEventHandlerFn tasksArr[] = {
macEventLoop, // MAC层任务处理函数
nwk_event_loop, // 网络层任务处理函数
Hal_ProcessEvent, // 硬件抽象层任务处理函数
#if defined( MT_TASK )
MT_ProcessEvent, // 监控测试任务处理函数
#endif
APS_event_loop, // 应用支持子层任务处理函数
#if defined ( ZIGBEE_FRAGMENTATION )
APSF_ProcessEvent, // APSF层任务处理函数
#endif
ZDApp_event_loop, // ZigBee设备应用任务处理函数
#if defined ( 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,参考如下。
uint16 NewApp_ProcessEvent( uint8 task_id, uint16 events )
{
afIncomingMSGPacket_t *MSGpkt;
(void)task_id; // Intentionally unreferenced parameter
if ( events & SYS_EVENT_MSG )
{
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( NewApp_TaskID );
while ( MSGpkt )
{
switch ( MSGpkt->hdr.event )
{
// Received when a key is pressed
case KEY_CHANGE:
NewApp_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:
NewApp_MessageMSGCB( MSGpkt );
break;
// Received whenever the device changes state in the network
case ZDO_STATE_CHANGE:
NewApp_NwkState = (devStates_t)(MSGpkt->hdr.status);
if ( (NewApp_NwkState == DEV_ZB_COORD)
|| (NewApp_NwkState == DEV_ROUTER)
|| (NewApp_NwkState == DEV_END_DEVICE) )
{
// Start sending the periodic message in a regular interval.
osal_start_timerEx( NewApp_TaskID,
NEWAPP_SEND_PERIODIC_MSG_EVT,
NEWAPP_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( NewApp_TaskID );
}
// return unprocessed events
return (events ^ SYS_EVENT_MSG);
}
// Send a message out - This event is generated by a timer
// (setup in NewApp_Init())
if ( events & NEWAPP_SEND_PERIODIC_MSG_EVT )
{
// Send the periodic message
NewApp_SendPeriodicMessage();
HalLedSet(HAL_LED_1,HAL_LED_MODE_TOGGLE);
// HalLedBlink( HAL_LED_1, 4, 50, 100 );
// Setup to send message again in normal period (+ a little jitter)
osal_start_timerEx( NewApp_TaskID, NEWAPP_SEND_PERIODIC_MSG_EVT,
(NEWAPP_SEND_PERIODIC_MSG_TIMEOUT + (osal_rand() & x00FF)) );
// return unprocessed events
return (events ^ NEWAPP_SEND_PERIODIC_MSG_EVT);
}
// Discard unknown events
return 0;
}
快速的实现方法是,可以将App目录中原有的SampleApp.c与SampleApp.h 复制并重命名为NewApp.c和NewApp.h,并将文件中的关键字SampleApp修改为NewApp,也可以根据实际项目需要来编写这两个文件。实例可参考附录实践例程代码。
(3)对于不同事件发生后的任务处理函数osal_start_system的调用分析
void osal_start_system(void)
{
#if !defined ( ZBIT ) && !defined ( UBIT )
for(;;)//Forever Loop
#endif
{
uint8 idx = 0;
osalTimeUpdate();
Hal_ProcessPoll();//This replaces MT_SerialPoll() and //osal_check_timer()
//这里是轮训任务队列,并检查是否有某个任务的事件发生
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];//处理该idx的任务事件,是第idx个任务的事件发生了
tasksEvents[idx] = 0; // Clear the Events for this task
HAL_EXIT_CRITICAL_SECTION(intState);
//对应调用第idx个任务的事件处理函数,用events说明是什么事件
events = (tasksArr[idx])( idx, events );
//当没有处理完,把返回的events继续放到tasksEvents[idx]中
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
}
}
在NEWAPP.C文件中void NewApp_Init( uint8 task_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 );
(4)添加函数声明
在NewApp.c中添加函数声明#include "NewApp.h"头文件,注释掉#include "NewAppHw.h"头文件,如//#include "NewAppHw.h"。
添加代码后如图3.13所示。
图3.13 添加代码后示意图
程序运行后,sampleApp任务定时改变蓝灯的状态,NewApp任务定时改变黄灯的状态。
课后练习
简答题
(1)简述ZigBee无线传感器网络协议和协议栈的关系。
(2)什么是节点?什么是端口?节点和端口之间有什么关系?
(3)IT公司ZigBee协议栈中数据发送函数是哪个?数据接收函数是哪个?