Zigbee入门指导(三)——一个接近实用的WSN系统

http://blog.csdn.net/logiclimit/article/details/5691447

  在之前的两篇指导当中,大家已经了解到了基于Ti CC2430的Zigbee开发的一些基本的概念和知识,也成功的运行了Ti自带的例程,然而这些与实用还相差甚远。作为入门指导的最后一篇,将对如何建立一个实用的WSN系统进行探讨。

      Zigbee的出现是为了满足WSN(Wireless Sensor Network)的要求,一般而言WSN有以下几个特征:

1.采集点众多,分布面积广

2.网络节点间的位置关系不定,节点动态加入或脱离网络

3.采集点无法和市电网络相连,依赖于电池供电,要求有很好的节电及电源管理

为了实现节能的特性,还跟CC2430模块以外的采集模块有关,本文主要关注的是CC2430自身的管理使用,故对外界数据的采集简化为从AD中采集数据。目标系统将具备以下功能:

1.协调器建立网络,终端节点加入网络

2.节点能采集多种数据

      从例程中选用一个合适的范例作为模板可以大大缩短开发时间,节约成本。选用SimpleApp作为模板。SimpleApp中有两个例程,一个是控制器-开关,一个是收集器-传感器,将使用收集器-传感器例程。收集器-传感器例程中以传感器终端的温度及电源电压为数据源,传感器定时采集这两个数据,送往收集器,收集器收到数据后通过串口传给PC机。可以说SimpleApp本身就是一个接近实用的WSN例程,本文的目标在于学习SimpleApp的使用,并加上一个通过AD采集数据的功能。此外,由于SimpleApp的传感器终端启动后就一直采集发送数据,无法由收集器控制其采集的开启/关停,将增添由PC发送指令到收集器,再由收集器发送指令控制某终端的某项采集功能的开启/关闭。

一、建立新工程

      为了和例程SimpleApp区分开,新建工程命名为WSNApp,其中只包含Collector和Sensor,且只为EB板设置,本节内容不会对程序的运行有实质影响,修改工程名只是为了管理上的方便。也可不修改工程名,只做功能上的修改,直接跳到下一节的内容。先原地复制SimpleApp工程文件夹,并修改目录名为WSNApp。WSNApp中有两个目录,为、,中的为应用程序,删除SimpleController.c和SimpleSwitch.c,将SimpleApp.h更名为WSNApp.h。中存放的具体的Project的设置,如果还没打开过相关工程的话,里面应该只有三个文件SimpleApp.ewd、SimpleApp.ewp和SimpleApp.eww,将这三个文件均改名为WSNApp,扩展名保持不变。这三个文件都是XML格式的文本文件,可以用记事本等工具打开修改,我使用的Notepad++,其支持XML语言的高亮显示及折叠功能。

      修改WSNApp.eww,先将所有的SimpleApp替换为WSNApp。里面的代码结构如下

<member>
  <project>WSNApp</< span>project>
  <configuration>SimpleCollectorDB</< span>configuration>
</< span>member>
<member>
  <project>WSNApp</< span>project>
  <configuration>SimpleCollectorEB</< span>configuration>
</< span>member>
<member>
  <project>WSNApp</< span>project>
  <configuration>SimpleControllerDB</< span>configuration>
</< span>member>

每个member下是相应的project及相关的设置,删除所有的DB及Controller、Switch。最后只保留以下内容

<member>
  <project>WSNApp</< span>project>
  <configuration>SimpleCollectorEB</< span>configuration>
</< span>member>
<member>
  <project>WSNApp</< span>project>
  <configuration>SimpleSensorEB</< span>configuration>
</< span>member>

      修改WSNApp.ewp,WSNApp.ewp有8902行,如果所用的文本编辑器不支持XML语言会改得比较痛苦。先将所有的SimpleApp替换为WSNApp。WSNApp.ewp存放的是工程的目录结构,WSNApp.ewp中的configuration段定义的是各个project 的设置,还有group段定义的是各个project中各文件的关联关系。删除有关DB及Controller、Switch的configuration段。修改App的group段的中的DB及Controller、Switch段落。

      修改WSNApp.ewd,先删除有关DB及Controller、Switch的configuration段即可。

二、Collector的修改

      Collector程序要添加PC和Collector的间的通讯、通过无线发送指令到Sensor。要为Collector添加一个用户事件,用于发送数据。

1、串口设置

      Ti的协议栈中已经有了有关的串口通信的驱动,可直接调用,使用串口有关的指令,需包含"hal_uart.h"头文件,在SimpleCollector.c和sapi.c中均要包含。要添加预编译指令HAL_UART。使用halUARTCfg_t来配置串口,配置串口时需配置输入/输出缓冲区的大小等信息。由于用户task的初始化在sapi.c中,串口的初始化也放在sapi.c中。先在sapi.h中定义以下常量

#ifdef COLLECTOR
  #define SERIAL_PORT_THRESH 48
  #define SERIAL_PORT_RX_MAX  64
  #define SERIAL_PORT_TX_MAX  64
  #define SERIAL_PORT_IDLE  6
  #define SERIAL_PORT_RX_CNT  80
#endif
由于sapi.h和sapi.c是collector和sensor共用的,故使用的条件编译的方式定义常量。
      在sapi.c中,先定义串口的配置变量。
void SAPI_Init( byte task_id ) 
{ 
  uint8 startOptions; 
#ifdef COLLECTOR 
  halUARTCfg_t uartConfig; 
#endif
在SAPI_Init()的末尾初始化串口
 
#ifdef COLLECTOR
  uartConfig.configured           = TRUE;              // 2430 don't care.
  uartConfig.baudRate             = HAL_UART_BR_38400;  // CC2430 only allow 38.4k or 115.2k
  uartConfig.flowControl          = HAL_UART_FLOW_OFF;  // Turn off flow control to fit most serial ports' setting
  uartConfig.flowControlThreshold = SERIAL_PORT_THRESH;
  uartConfig.rx.maxBufSize        = SERIAL_PORT_RX_MAX;
  uartConfig.tx.maxBufSize        = SERIAL_PORT_TX_MAX;
  uartConfig.idleTimeout          = SERIAL_PORT_IDLE;   // 2430 don't care.
  uartConfig.intEnable            = TRUE;              // 2430 don't care.
  uartConfig.callBackFunc         = rxCB;
  HalUARTOpen (HAL_UART_PORT_0, &uartConfig);
#endif
 
  // Set an event to start the application
  osal_set_event(task_id, ZB_ENTRY_EVENT);
}
此处有两个需要注意的地方,串口波特率只能为38400或115.2k,接收数据的回调函数是rxCB。定义函数rxCB来处理来自串口的数据,收到数据后触发相关的事件。
      在SimpleCollector.c中,向PC机传送数据的指令位于zb_ReceiveDataIndication()中,为
#if defined( MT_TASK )
    debug_str( (uint8 *)buf );
#endif
由于使用自定义的串口操作,需在预编译中去除MT_TASK,故无法使用其debug_str()函数向PC发送数据,将相关代码替换为
HalUARTWrite ( HAL_UART_PORT_0, buf, 32 ); 

2、接收来自PC的数据

收到数据后要通知OS收到了串口数据,在SimpleCollector.c中定义了相关的事件。

#define SERIAL_PORT_MSG_RCV_EVT    0x0008

定义指向数据的指针和数据长度

static uint8 *otaBuf = NULL; 
static uint8 otaLen;

rxCB的定义如下

void rxCB( uint8 port, uint8 event )
{
  uint8 *buf, len;
 
  if (!otaBuf)
  {
    if ( !(buf = osal_mem_alloc( SERIAL_PORT_RX_CNT )) )
    {
      return;
    }
  }
 
  len = HalUARTRead( port, buf, SERIAL_PORT_RX_CNT );
 
  if ( !len )  // Length is not expected to ever be zero.
  {
    osal_mem_free( buf );
    return;
  }
  
  otaBuf = buf;
  otaLen = len;
 
  osal_set_event( sapi_TaskID, SERIAL_PORT_MSG_RCV_EVT );
}

当数据接收完毕后,触发事件SERIAL_PORT_MSG_RCV_EVT。

3、Collector向Sensor发送指令

      从sapi.c中的

if ( events & ( ZB_USER_EVENTS ) )
{
  // User events are passed to the application
  zb_HandleOsalEvent( events );
 
  // Do not return here, return 0 later
}

可知,在zb_HandleOsalEvent()中处理用户自定义的事件,当收到PC传来的数据后触发了SERIAL_PORT_MSG_RCV_EVT,将无线发送指令的程序作为此事件的响应,原有的SimpleCollector.c中的void zb_HandleOsalEvent( uint16 event )是空的,为其添加以下内容

void zb_HandleOsalEvent( uint16 event )
{
  if ( event & SERIAL_PORT_MSG_RCV_EVT )
  {
    Collector_SendData( otaBuf, otaLen );
  }
}

其中Collector_SendData()的定义如下

void Collector_SendData( uint8 *buf, uint8 len )
{
  volatile uint16 tempDstAddr;
  tempDstAddr = buf[0];
  tempDstAddr = tempDstAddr << 8;
  tempDstAddr += buf[1];
  zb_SendDataRequest( tempDstAddr, COLLECTOR_CMD_ID, 1, &buf[2], 0, 0, AF_DEFAULT_RADIUS );
  osal_mem_free( otaBuf );
} 

其中的zb_SendDataRequest()即为通过无线发送指令的函数,其中的参数COLLECTOR_CMD_ID是Endpoint中的发送cluster。原有的Collector程序只能接受无线数据,不能发送无线数据,是通过设置发送cluster为实现的,增添发送cluster

#define NUM_OUT_CMD_COLLECTOR                1
 
const cId_t zb_OutCmdList[NUM_OUT_CMD_COLLECTOR] =
{
  COLLECTOR_CMD_ID
};
 
const SimpleDescriptionFormat_t zb_SimpleDesc =
{
  MY_ENDPOINT_ID,             //  Endpoint
  MY_PROFILE_ID,              //  Profile ID
  DEV_ID_COLLECTOR,          //  Device ID
  DEVICE_VERSION_COLLECTOR,  //  Device Version
  0,                          //  Reserved
  NUM_IN_CMD_COLLECTOR,      //  Number of Input Commands
  (cId_t *) zb_InCmdList,     //  Input Command List
  NUM_OUT_CMD_COLLECTOR,     //  Number of Output Commands
  (cId_t *) zb_OutCmdList   //  Output Command List
};
COLLECTOR_CMD_ID定义在WSNApp.h中
#define COLLECTOR_CMD_ID                  1

三、Sensor的修改

      Sensor要增添的内容有:1.在endpoint中增加接收cluster;2.添加指令处理内容;3.增添外部AD采集内容。

1、修改Endpoint描述

      在SimpleSensor.c中添加及修改以下内容

#define NUM_IN_CMD_SENSOR                 1
 
const cId_t zb_InCmdList[NUM_IN_CMD_SENSOR] =
{
  COLLECTOR_CMD_ID
};
 
const SimpleDescriptionFormat_t zb_SimpleDesc =
{
  MY_ENDPOINT_ID,             //  Endpoint
  MY_PROFILE_ID,              //  Profile ID
  DEV_ID_SENSOR,              //  Device ID
  DEVICE_VERSION_SENSOR,      //  Device Version
  0,                          //  Reserved
  NUM_IN_CMD_SENSOR,          //  Number of Input Commands
  (cId_t *) zb_InCmdList,             //  Input Command List
  NUM_OUT_CMD_SENSOR,         //  Number of Output Commands
  (cId_t *) zb_OutCmdList     //  Output Command List
};

2、指令处理

      指令为一个byte,其各个bit定义为各采集数据的开关,在SimpleSensor.c中定义了以下位选常量

#define GLOBAL_SWITCH               0x80
#define TEMP_SWITCH                 0x40
#define BATT_SWITCH                 0x20
#define ADC_SWITCH                  0x10
当Sensor接到数据后会调用函数zb_ReceiveDataIndication(),为其添加以下内容
void zb_ReceiveDataIndication( uint16 source, uint16 command, uint16 len, uint8 *pData  )
{
  if (command == COLLECTOR_CMD_ID)
  {
    HalUARTWrite ( HAL_UART_PORT_0, pData, 1 );
    if (GLOBAL_SWITCH & pData[0])
    {
      if (TEMP_SWITCH & pData[0])
      {
        osal_start_timerEx( sapi_TaskID, MY_REPORT_TEMP_EVT, myTempReportPeriod );
      }
      else
      {
        osal_stop_timerEx( sapi_TaskID, MY_REPORT_TEMP_EVT );
      }
      if (BATT_SWITCH & pData[0])
      {
        osal_start_timerEx( sapi_TaskID, MY_REPORT_BATT_EVT, myBatteryCheckPeriod );
      }
      else
      {
        osal_stop_timerEx( sapi_TaskID, MY_REPORT_BATT_EVT );
      }
    }
    else
    {
      myApp_StopReporting();
    }
  }
}

根据不同位的值来做出是否关停所有采集或特定的采集功能。

      至此,就可以通过PC机发送指令到Collector,再由Collector发送到Sensor,控制某项采集功能的开启/关闭。如网络中有短地址为0x796F的节点,要关闭其所有的采集功能,PC向Collector发送0x79、0x6F、0x80即可。

四、增添采集外部数据功能

      要添加采集外部数据的功能,Collector及Sensor都要添加响应的内容。

1、Sensor

      通过对Sensor部分的分析,需要定义采集时间,添加事件及相关的处理。定义采集时间

static uint16 myADCCheckPeriod = 1000;   // milliseconds

每隔1s采集一次数据。定义ADC取值的函数

      添加新事件MY_REPORT_ADC_EVT

#define MY_REPORT_ADC_EVT           0x0010
 

      在zb_HandleOsalEvent()中添加有关的内容

if ( event & MY_REPORT_ADC_EVT )
{
  // Read ADC7 value
  pData[0] = ADC_REPORT;
  pData[1] =  HalAdcRead(0x07, HAL_ADC_RESOLUTION_8);
  zb_SendDataRequest( 0xFFFE, SENSOR_REPORT_CMD_ID, 2, pData, 0, AF_ACK_REQUEST, 0 );
  osal_start_timerEx( sapi_TaskID, MY_REPORT_ADC_EVT, myADCCheckPeriod );
}

其中的ADC_REPORT定义为

#define ADC_REPORT      0x03

HalAdcRead(0x07, HAL_ADC_RESOLUTION_8)是Ti的协议栈自带的ADC驱动,以8bit分辨率读取AD7的值。

      在zb_ReceiveDataIndication()中添加有关的内容

if (ADC_SWITCH & pData[0])
{
  osal_start_timerEx( sapi_TaskID, MY_REPORT_ADC_EVT, myADCCheckPeriod );
}
else
{
  osal_stop_timerEx( sapi_TaskID, MY_REPORT_ADC_EVT );
}  

      接下来是myApp_StartReporting()和myApp_StopReporting()

void myApp_StartReporting( void )
{
  osal_start_timerEx( sapi_TaskID, MY_REPORT_TEMP_EVT, myTempReportPeriod );
  osal_start_timerEx( sapi_TaskID, MY_REPORT_BATT_EVT, myBatteryCheckPeriod );
  osal_start_timerEx( sapi_TaskID, MY_REPORT_ADC_EVT, myADCCheckPeriod );
  HalLedSet( HAL_LED_1, HAL_LED_MODE_ON );
}
 
void myApp_StopReporting( void )
{
  osal_stop_timerEx( sapi_TaskID, MY_REPORT_TEMP_EVT );
  osal_stop_timerEx( sapi_TaskID, MY_REPORT_BATT_EVT );
  osal_stop_timerEx( sapi_TaskID, MY_REPORT_ADC_EVT );
  HalLedSet( HAL_LED_1, HAL_LED_MODE_OFF );
}
 

2、Collector

      Collector中有关的修改不多,集中在zb_ReceiveDataIndication(),先要定义常量ADC_REPORT

#define ADC_REPORT      0x03

再将zb_ReceiveDataIndication()中的

if ( pData[0] == BATTERY_REPORT )
{
...
}
else
{
...
}

修改为switch-case结构

switch( pData[0] )
{
case BATTERY_REPORT:
  tmpLen = (uint8)osal_strlen( (char*)strBattery );
  pBuf = osal_memcpy( pBuf, strBattery, tmpLen );
  
  *pBuf++ = (sensorReading / 10 ) + '0';    // convent msb to ascii
  *pBuf++ = '.';                            // decimal point ( battery reading is in units of 0.1 V
  *pBuf++ = (sensorReading % 10 ) + '0';    // convert lsb to ascii
  *pBuf++ = ' ';
  *pBuf++ = 'V';
  break;
case TEMP_REPORT:
  tmpLen = (uint8)osal_strlen( (char*)strTemp );
  pBuf = osal_memcpy( pBuf, strTemp, tmpLen );
 
  *pBuf++ = (sensorReading / 10 ) + '0';    // convent msb to ascii
  *pBuf++ = (sensorReading % 10 ) + '0';    // convert lsb to ascii
  *pBuf++ = ' ';
  *pBuf++ = 'C';
  break;
case ADC_REPORT:
  tmpLen = (uint8)osal_strlen( (char*)strADC );
  pBuf = osal_memcpy( pBuf, strADC, tmpLen );
 
  *pBuf++ = (sensorReading / 10 ) + '0';    // convent msb to ascii
  *pBuf++ = (sensorReading % 10 ) + '0';    // convert lsb to ascii
  break;
default:
  {}
}

Collector的预编译内容为

CC2430EB
HOLD_AUTO_START
SOFT_START
REFLECTOR
NV_INIT
xNV_RESTORE
HAL_UART
COLLECTOR

 

Sensor的预编译内容为

CC2430EB
NWK_AUTO_POLL
HOLD_AUTO_START
REFLECTOR
POWER_SAVING
NV_INIT

编译,下载后即可运行,使用串口调试工具,可以接受数据显示,并可向collector发送指令,控制sensor的采集行为。

      以下为要注意的操作事项,节点首次下载后,要先按下SW6,停止LED1的闪烁。之后按下SW1即可。

五、可视化数据显示

      在实际应用中,collector要配置相关的存储外设记录数据,或是配合上位机来接收所有数据进行处理及显示。基于Qt编写了一个专用的上位机程序,用来显示收到的数据,程序源码不公开,但可到http://download.csdn.net/source/2482714下载可执行程序。

      再有上位机的情况下,只需传送数据,而不用传送ASCII码,故可将collector的zb_ReceiveDataIndication()修改为

void zb_ReceiveDataIndication( uint16 source, uint16 command, uint16 len, uint8 *pData  )
{
  uint8 buf[32];
  uint8 *pBuf;
  uint8 tmpLen;
  uint8 sensorReading;
 
  if (command == SENSOR_REPORT_CMD_ID)
  {
    // Received report from a sensor
    sensorReading = pData[1];
 
    // If tool available, write to serial port
 
    tmpLen = (uint8)osal_strlen( (char*)strDevice );
    pBuf = osal_memcpy( buf, strDevice, tmpLen );
    _ltoa( source, pBuf, 16 );
    pBuf += 4;
    *pBuf++ = ' ';
    *pBuf++ = pData[0];
    *pBuf++ = sensorReading;
    
    *pBuf++ = '/r';
    *pBuf++ = '/n';
    *pBuf = '/0';
    tmpLen = (uint8)osal_strlen( (char*)buf );
    HalUARTWrite ( HAL_UART_PORT_0, buf, tmpLen );   
  }
}

保留发送字符串strDevice[] = "Device:0x"及"/r/n",用做传送帧的帧头和帧尾。

上位机程序如图1、2所示。<节点>为网络中各节点的短地址,选择一个节点为当前显示节点,可以切换显示其电池电量、温度、ADC采集值。数据值显示在上方的文本框,数据绘制成波形显示在下侧。

Zigbee入门指导(三)——一个接近实用的WSN系统_第1张图片

图1

Zigbee入门指导(三)——一个接近实用的WSN系统_第2张图片

图2

六、扩展

      此系统只是接近实用,还没达到实用的目的,最欠缺的是电源管理,当采集间隔较长时可将节点转入休眠状态。可作以下扩展:

1.加入休眠功能;

2.上位机可发送指令调节Sensor的采集间隔。

你可能感兴趣的:(Zigbee入门指导(三)——一个接近实用的WSN系统)