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程序要添加PC和Collector的间的通讯、通过无线发送指令到Sensor。要为Collector添加一个用户事件,用于发送数据。
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
void SAPI_Init( byte task_id )
{
uint8 startOptions;
#ifdef COLLECTOR
halUARTCfg_t uartConfig;
#endif
#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);
}
#if defined( MT_TASK )
debug_str( (uint8 *)buf );
#endif
HalUARTWrite ( HAL_UART_PORT_0, buf, 32 );
收到数据后要通知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。
从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
};
#define COLLECTOR_CMD_ID 1
Sensor要增添的内容有:1.在endpoint中增加接收cluster;2.添加指令处理内容;3.增添外部AD采集内容。
在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
};
指令为一个byte,其各个bit定义为各采集数据的开关,在SimpleSensor.c中定义了以下位选常量
#define GLOBAL_SWITCH 0x80
#define TEMP_SWITCH 0x40
#define BATT_SWITCH 0x20
#define ADC_SWITCH 0x10
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都要添加响应的内容。
通过对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 );
}
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采集值。数据值显示在上方的文本框,数据绘制成波形显示在下侧。
图1
图2
此系统只是接近实用,还没达到实用的目的,最欠缺的是电源管理,当采集间隔较长时可将节点转入休眠状态。可作以下扩展:
1.加入休眠功能;
2.上位机可发送指令调节Sensor的采集间隔。