「ZigBee模块」协议栈-Z-Stack协议栈基础和数据传输实验
花了好久写的...感觉还不错的呢...如果看,请细看...Mua~
Z-Stack协议栈基础和数据传输实验
一、实验目的
终端节点将数据无线发送到协调器,协调器通过串口将数据发送到PC端,并在屏幕上显示出来。串口优化把有线串口传输改为无线蓝牙传输。
二、实验平台
硬件:2个zigbee节点,1个编译器,1根方口转USB数据线,一个蓝牙模块
软件:实验基于SampleApp工程进行。
三、实验步骤
- 串口初始化代码
- 发送部分代码
- 接收部分代码
四、协议栈基础
做实验之前先了解一点关于协议栈的基础知识吧~
什么是协议栈?我们知道使用Zigbee一般都要进行组网、传输数据。可想而知其中的代码数量是非常庞大的,如果我们每次使用zigbee都需要自己写所以代码的话,会非常麻烦。因此就有了协议栈。可以说它是一个小型的操作系统,把很多通信、组网之类的代码都封装起来了。我们要做的只是通过调用函数来实现我们的目的。
来看一下协议栈的工作流程图(图1)。然后我会对照流程图对协议栈进行简单的分析。
图1
我们就从流程图的“开始”开始分析吧~
打开工程文件SampleApp,main函数是程序执行的开始,我们要先找到它。Main函数在ZMAin文件夹的ZMain.c下,打开它,找到main函数。
1 int main( void ) 2 3 { 4 5 // Turn off interrupts 6 7 //关闭所有中断 8 9 osal_int_disable( INTS_ALL ); 10 11 12 13 // Initialization for board related stuff such as LEDs 14 15 //初始化系统时钟 16 17 HAL_BOARD_INIT(); 18 19 20 21 // Make sure supply voltage is high enough to run 22 23 //检测芯片电压是否正常 24 25 zmain_vdd_check(); 26 27 28 29 // Initialize board I/O 30 31 //初始化外设 32 33 InitBoard( OB_COLD ); 34 35 36 37 // Initialze HAL drivers 38 39 //初始化芯片各硬件模块 40 41 HalDriverInit(); 42 43 44 45 // Initialize NV System 46 47 //初始化flash存储器 48 49 osal_nv_init( NULL ); 50 51 52 53 // Initialize the MAC 54 55 //初始化MAC层 56 57 ZMacInit(); 58 59 60 61 // Determine the extended address 62 63 //确定IEEE 64位地址 64 65 zmain_ext_addr(); 66 67 68 69 #if defined ZCL_KEY_ESTABLISH 70 71 //Initialize the Certicom certificate information. 72 73 zmain_cert_init(); 74 75 #endif 76 77 78 79 // Initialize basic NV items 80 81 //初始化非易失变量 82 83 zgInit(); 84 85 86 87 #ifndef NONWK 88 89 // Since the AF isn't a task, call it's initialization routine 90 91 afInit(); 92 93 #endif 94 95 96 97 // Initialize the operating system 98 99 //初始化操作系统***********************************初始化重点 100 101 osal_init_system(); 102 103 104 105 // Allow interrupts 106 107 //允许中断使能 108 109 osal_int_enable( INTS_ALL ); 110 111 112 113 // Final board initialization 114 115 //初始化按键 116 117 InitBoard( OB_READY ); 118 119 120 121 // Display information about this device 122 123 //显示设备信息 124 125 zmain_dev_info(); 126 127 128 129 /* Display the device info on the LCD */ 130 131 #ifdef LCD_SUPPORTED 132 133 zmain_lcd_init(); 134 135 #endif 136 137 138 139 #ifdef WDT_IN_PM1 140 141 /* If WDT is used, this is a good place to enable it. */ 142 143 WatchDogEnable( WDTIMX ); 144 145 #endif 146 147 148 149 // No Return from here 150 151 // 执行操作系统,进去后不会返回************************运行重点 152 153 osal_start_system(); 154 155 156 157 return 0; // Shouldn't get here. 158 159 }
浏览一下main函数可以看到一开始都是各种初始化函数,即对应流程图中的“各种初始化函数”。初始化中我们需要注意的是“osal_init_system();”初始化操作系统函数。等一下会对它进行说明。继续看下去,“osal_start_system();”这是执行操作系统函数,对应流程中的“运行操作系统”。注意这个函数进去之后是不会再返回的。总结main函数就是初始化和执行操作系统两个部分。
我们再来分析一下“osal_init_system();”这个函数,它的功能是初始化操作系统。我们go to definition看一下这个函数的代码。
1 uint8 osal_init_system( void ) 2 3 { 4 5 // Initialize the Memory Allocation System 6 7 // 初始化内存分配系统 8 9 osal_mem_init(); 10 11 12 13 // Initialize the message queue 14 15 // 初始化消息队列 16 17 osal_qHead = NULL; 18 19 20 21 // Initialize the timers 22 23 // 初始化定时器 24 25 osalTimerInit(); 26 27 28 29 // Initialize the Power Management System 30 31 // 初始化电源管理系统 32 33 osal_pwrmgr_init(); 34 35 36 37 // Initialize the system tasks. 38 39 // 初始化系统任务**********************************重点 40 41 osalInitTasks(); 42 43 44 45 // Setup efficient search for the first free block of heap. 46 47 osal_mem_kick(); 48 49 50 51 return ( SUCCESS ); 52 53 }
浏览这个函数我们可以看到其中依旧是各种初始化函数。重点观察“osalInitTasks();”这个函数,函数功能是初始化任务系统,继续go to definition,查看该函数。
1 void osalInitTasks( void ) 2 3 { 4 5 uint8 taskID = 0; 6 7 8 9 //分配缓冲区内存,返回指针 10 11 tasksEvents = (uint16 *)osal_mem_alloc( sizeof( uint16 ) * tasksCnt); 12 13 //设置所分配的内存空间单元值为0 14 15 osal_memset( tasksEvents, 0, (sizeof( uint16 ) * tasksCnt)); 16 17 18 19 // 任务优先级由高到低依次排列,高优先级对应taskID小 20 21 macTaskInit( taskID++ ); //0 不用考虑 22 23 nwk_init( taskID++ ); //1 | 24 25 Hal_Init( taskID++ ); //2 | 26 27 #if defined( MT_TASK ) 28 29 MT_TaskInit( taskID++ ); 30 31 #endif 32 33 APS_Init( taskID++ ); //3 | 34 35 #if defined ( ZIGBEE_FRAGMENTATION ) 36 37 APSF_Init( taskID++ ); 38 39 #endif 40 41 ZDApp_Init( taskID++ ); //4 需要考虑 42 43 #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) 44 45 ZDNwkMgr_Init( taskID++ ); 46 47 #endif 48 49 SampleApp_Init( taskID ); //5 需要考虑*************************重点 50 51 //通常在这里初始化自己的东西 52 53 }
通过注释我们可以知道这个函数也是拿来初始化的,可以里面的代码有点难以理解......这里我们需要先知道一点,后面会提到,这里先说明下。额,因为这个是我自己的理解,所以部分描述起来可能不是很专业,能懂这个意思就好了,以后专业起来了再回来修改......协议栈采用任务机制,然后使用轮询的方式处理任务。就是说在空闲的时候它从优先级高的任务开始,一个个检查是否有任务要处理,有则处理这个任务,没有则继续循环检测。
好嘞~就是这样!那么再来看这个函数,它的作用就是按“任务”的优先级给它们发一个ID号,发的同时呢又对这个任务进行初始化。需要注意的是任务优先级越高,它的ID号越小!然后上面那些我们全都不用考虑,需要考虑的是最后两个函数(原来我们能操作的优先级最低呀......)。嗯...感觉go to definition好久了...就不继续看下去啦,之后再详细解读这两个函数吧~
这样子初始化的函数算是解释完了,我们回到main函数,继续看下一个函数“osal_start_system();”执行操作系统函数!来来来,继续go to definition找到它本尊。
1 void osal_start_system( void ) 2 3 { 4 5 #if !defined ( ZBIT ) && !defined ( UBIT ) 6 7 for(;;) // Forever Loop 8 9 #endif 10 11 { 12 13 osal_run_system(); 14 15 } 16 17 }
嗯哼,找到“osal_run_system();”我们继续......
1 void osal_run_system( void ) 2 3 { 4 5 uint8 idx = 0; 6 7 8 9 osalTimeUpdate(); //扫描哪个事件被触发了,置相应标志位 10 11 Hal_ProcessPoll(); 12 13 14 15 //从0(最高优先级)开始检索是否有任务需要处理 16 17 //有则立即跳出循环,没有等到检索完再跳出循环 18 19 do { 20 21 if (tasksEvents[idx]) // Task is highest priority that is ready. 22 23 { 24 25 break; 26 27 } 28 29 } while (++idx < tasksCnt); 30 31 32 33 //分析索引号,对任务进行处理,没有任务就跳过 34 35 if (idx < tasksCnt) 36 37 { 38 39 uint16 events; 40 41 halIntState_t intState; 42 43 44 45 HAL_ENTER_CRITICAL_SECTION(intState); //进入临界区,保护 46 47 events = tasksEvents[idx]; //提取任务 48 49 tasksEvents[idx] = 0; // Clear the Events for this task.清除任务 50 51 HAL_EXIT_CRITICAL_SECTION(intState); //退出临界区 52 53 54 55 activeTaskID = idx; 56 57 events = (tasksArr[idx])( idx, events ); //通过指针调用任务处理函数********重点 58 59 activeTaskID = TASK_NO_TASK; 60 61 62 63 HAL_ENTER_CRITICAL_SECTION(intState); //进入临界区 64 65 tasksEvents[idx] |= events; // Add back unprocessed events to the current task. 66 67 //保存未处理事件 68 69 HAL_EXIT_CRITICAL_SECTION(intState); //退出临界区 70 71 } 72 73 #if defined( POWER_SAVING ) 74 75 else // Complete pass through all task events with no activity? 76 77 { 78 79 osal_pwrmgr_powerconserve(); // Put the processor/system into sleep 80 81 } 82 83 #endif 84 85 86 87 /* Yield in case cooperative scheduling is being used. */ 88 89 #if defined (configUSE_PREEMPTION) && (configUSE_PREEMPTION == 0) 90 91 { 92 93 osal_task_yield(); 94 95 } 96 97 #endif 98 99 }
这里就是我之前说的轮询的地方啦~这里就说下我的理解吧......但是不确定对不对......大致思想应该是对的......
先把工作分成两部分,一部分是任务请求,有任务请求了就把相应标志位置1。另一部分就是我们看到的这个函数。在函数开头读一下任务请求的寄存器(也许不是寄存器,就那个意思),然后从最高优先级依次检索是不是有任务请求。只要有任务请求,就进入处理任务请求部分(就是“if (idx < tasksCnt)”这个if语句里面的内容),没有则继续循环。处理任务请求部分中需要注意两点:1. 它在把高优先级任务处理完之后会继续检测是否还有任务请求,直到把使用任务请求处理完毕。2. 处理完一个任务之后它会清除该任务的标志位。
咳,不知道你们有没有看懂......然后这里面的重点函数呢就是“events = (tasksArr[idx])( idx, events );”这一句。先看一看tasksArr[]这个数组的定义。
1 const pTaskEventHandlerFn tasksArr[] = { 2 3 macEventLoop, 4 5 nwk_event_loop, 6 7 Hal_ProcessEvent, 8 9 #if defined( MT_TASK ) 10 11 MT_ProcessEvent, 12 13 #endif 14 15 APS_event_loop, 16 17 #if defined ( ZIGBEE_FRAGMENTATION ) 18 19 APSF_ProcessEvent, 20 21 #endif 22 23 ZDApp_event_loop, 24 25 #if defined ( ZIGBEE_FREQ_AGILITY ) || defined ( ZIGBEE_PANID_CONFLICT ) 26 27 ZDNwkMgr_event_loop, 28 29 #endif 30 31 SampleApp_ProcessEvent 32 33 };
有没有发现它和函数osalInitTasks();在同一个文件里面!有没有发现它就在osalInitTasks()这个函数的上面!再仔细看一看,有没有发现它定义成员变量名的顺序和下面初始化函数的顺序是一样的!这样说估计还是云里雾里的吧...(因为写到这里我也还是没有完全懂...)我再说明白点......之前不是说每个任务都有一个ID号嘛,优先级从0开始的,而数组里面第一位的索引号也是0,就是说任务ID号和数组索引号相对应,那么利用任务ID号就可以在数组里面找到相应的任务。还有很神奇的一点,至少我是怎么觉得的...之前看这行代码感觉非常难以理解......“events = (tasksArr[idx])( idx, events ); ”。刚刚想明白的,原来那个数组的类型是一个函数!也就是说通过任务ID找到相应进行任务处理的函数!这样你们有没有明白?不懂留言......有人看吗?笑......
终于,协议栈的分析工作完成了......会不会觉得很乱?看看下面的流程图再来回顾整理一下吧~
图2
五、实验过程和说明
1. 串口初始化代码
①在协议栈中,用户自己添加代码的地方基本为App这个文件夹。打开其中的文件SampleApp.c,在INCLUDES部分添加代码
#include "MT_UART.h"
图3
操作说明:协议栈中关于串口封装的文件有两个,一个是HAL->Target->CC2530EB->Drivers->hal_uart.c,另一个是MT->MT_UART.c。这两个文件有什么区别呢?打开文件研究一下。首先,在MT_UART.c中有include “hal_uart.h”,所以写头文件只要写”MT_UART.h”即可。然后,仔细看代码,分析hal_uart.c这个文件只要是对不同串口类型的相应操作进行选择,MT_UART.c这个文件则是对任意串口的操作。就是说MT_UART.c这个文件更底层一点。
②同样SampleApp.c文件中,找到函数void SampleApp_Init( uint8 task_id ),在其中加入串口初始化代码
/************串口初始化******************/ MT_UartInit(); //串口初始化 MT_UartRegisterTaskID(task_id); //登记任务号
图4
操作说明:串口初始化就不说了。登记任务号就是把串口事件通过task_id登记在SampleApp_Init();中。之前我们有提到说SampleApp_Init();函数很重要,就是分配ID号,它还是优先级最低的那个。把这个函数的ID号给串口就是告诉串口我是在这个函数里面初始化的,相应的我的任务优先级是最低的......
③更改串口初始化配置。
在上图所示的MT_UartInit();处go to definition,进入MT_UartInit()函数(如图5)。找到其中的MT_UART_DEFAULT_BAUDRATE,go to definition后将波特率设置为115200(如图6)。
回到图3位置,找到MT_UART_DEFAULT_OVERFLOW,go to definition将参数设为FALSE(如图7)。
图5
图6
图7
操作说明:修改波特率就不解释啦。
#define MT_UART_DEFAULT_OVERFLOW FALSE
这行代码是打开串口流控的意思。因为我们串口通讯是两根线的,必须把它关闭。
2. 发送部分代码
①打开SampleApp.c,找到SampleApp事件处理函数SampleApp_ProcessEvent()。
补充一点,我们可以在SampleApp下添加自己的事件,每个事件有自己的事件号。事件号是16位的,但是每个事件号只允许占16位中的1位,也就是说最多有16个事件。
我们先浏览一下代码,大致功能是分析传递进来的事件号,触发相应事件。感觉这个和任务号处理模式还是挺像的。我们需要关注的是“系统消息事件”被触发之后。即 “if ( events & SYS_EVENT_MSG )”语句之后的部分。先看第一行:
MSGpkt = (afIncomingMSGPacket_t *)osal_msg_receive( SampleApp_TaskID );
这一行代码实现功能是获取系统消息数据。我们可以自己去查看里面的定义。afIncomingMSGPacket是包含整个消息内容的结构体类型。
之后的选择语句则是根据消息中的信息对数据进行相应处理。我们需要关注如下代码(图8)
图8
弄了半天,原来还在初始化......意思是网络状态发送变化时(其实就是打开网络),就对数据发送进行初始化。看下这三个参数,第一个是任务号,不重复啦。第二个是事件号,这个也说过啦,每个事件只占1位哦!第三个是设置时间,就是规定你多久发一次信息!这里我们预设SAMPLEAPP_SEND_PERIODIC_MSG_TIMEOUT = 5000,这个值可以自行修改,数值单位是毫秒,就是说这个程序是5秒发送一次数据。
②设置发送内容,自动周期性发送。在同一个函数下找到如下代码(图9)
图9
如果触发周期性数据发送部分(就是说5秒过去了,要发送信息了),就执行SampleApp_SendPeriodicMessage()这个函数。这个函数是重点哦,里面放我们需要发送的数据。继续go to definition......
找到该函数后对该函数做如下修改(如图10)。
图10
我们来看一下AF_DataRequest()这个函数,通过上下文我们就可以知道这个函数一定就是决定发送数据内容的啦。我们需要关注的是其中第3、4、5个参数,第3个参数的作用是和接收方建立联系,这里定义SAMPLEAPP_PERIODIC_CLUSTERID=1,如果协调器收到一个数据包,获取里面的这个标号,为1则证明这个数据包是以周期性广播方式进来的。第4个参数表示发送数据的长度,第5个参数为需要发送的数据的指针。
进行到这里发送数据部分已经结束啦~~~
3. 接收部分代码
在SampleApp.c下找到函数“void SampleApp_MessageMSGCB( afIncomingMSGPacket_t *pkt )”,在“case SAMPLEAPP_PERIODIC_CLUSTERID:”下面一行添加代码(如图11):
HalUARTWrite(0, "I get data!\n", 12); HalUARTWrite(0, &pkt->cmd.Data[0], pkt->cmd.DataLength); HalUARTWrite(0, "\n", 1);
图11
操作说明:我们先看一下这个条件语句“case SAMPLEAPP_PERIODIC_CLUSTERID:”。这个就是在发送部分设置的表示周期性发送数据的编号。看吧,这里就用上了~
在添加代码的这个地方,我们可以对收到的数据进行处理(不局限于串口发送)。这里的三行代码都是串口发送的,不再多说什么啦。
重点看一下“afIncomingMSGPacket_t *pkt”。所有的数据和信息都在函数传进来的afIncomingMSGPacket里面,查看这个定义
1 typedef struct 2 3 { 4 5 osal_event_hdr_t hdr; /* OSAL Message header */ 6 7 uint16 groupId; /* Message's group ID - 0 if not set */ 8 9 uint16 clusterId; /* Message's cluster ID */ 10 11 afAddrType_t srcAddr; /* Source Address, if endpoint is STUBAPS_INTER_PAN_EP, 12 13 it's an InterPAN message */ 14 15 uint16 macDestAddr; /* MAC header destination short address */ 16 17 uint8 endPoint; /* destination endpoint */ 18 19 uint8 wasBroadcast; /* TRUE if network destination was a broadcast address */ 20 21 uint8 LinkQuality; /* The link quality of the received data frame */ 22 23 uint8 correlation; /* The raw correlation value of the received data frame */ 24 25 int8 rssi; /* The received RF power in units dBm */ 26 27 uint8 SecurityUse; /* deprecated */ 28 29 uint32 timestamp; /* receipt timestamp from MAC */ 30 31 uint8 nwkSeqNum; /* network header frame sequence number */ 32 33 afMSGCommandFormat_t cmd; /* Application Data */ 34 35 } afIncomingMSGPacket_t;
它是一个结构体,里面包含了数据包的所以内容,这里就不说啦,想知道的自己翻译下注释吧~我们重点关注其中的afMSGCommandFormat_t cmd。查看它的定义
typedef struct { uint8 TransSeqNumber; uint16 DataLength; // Number of bytes in TransData uint8 *Data; } afMSGCommandFormat_t;
哦哦,这个里面就有我们传送的数据内容啦!其中的DataLength就是数据长度,Data就是数据内容的指针啦。
我们再看回上去,HalUARTWrite(0, &pkt->cmd.Data[0], pkt->cmd.DataLength);这个代码就是把收到的数据发送给串口啦
4. 程序烧录
这里有个注意事项!下载的时候需要选择模式!如图12,CoordinatorEB模式下载到协调器(连接电脑的那个!),EndDeviceEB模式下载到终端模块!
图12
至此,无线数据传输实验就结束啦~
没有记住?没关系!下面就是总结回顾整个流程啦~
六、流程分析
图13
看着这个流程图有没有觉得更加方便理解呢?
七、串口优化
这个推荐去看我之前写的 「51单片机」蓝牙从机基本使用方法
地址:http://www.cnblogs.com/Donut/p/4054348.html
嘻嘻 给自己打个广告~
操作方式一模一样,只是波特率配置是115200,然后串口么连接zigbee的UART0(仅限这个实验,之后你定义了哪个串口就连哪个串口的呗~)