引言
本文记录了笔者基于Z-Stack 3.0.2协议栈,通过学习Zigbee通信协议,实现一个简单的开关控制指令和温湿度数据上报过程。通过这个过程,笔者对Zigbee网络的形成、设备间的通信有了更深入的理解。
在文章末尾,笔者有几个困惑点,希望各位读者能分享一下自己的经验和思路,让笔者更加深入探索Zigbee技术
工程代码链接:通过百度网盘分享的文件:Zigbee开关指令与温湿度数据上报.zip
链接:https://pan.baidu.com/s/1CfruCL2oOfzR2cFF4TbZkg?pwd=a97l
提取码:a97l
这里推荐一个学习zigbee 3.0的网址:
其中笔者也是在这个地方学习的zigbee相关知识
一、前期准备
1.1 下载与安装
首先,从官方渠道下载Z-Stack 3.0.2协议栈,并进行安装。Z-Stack是TI公司提供的Zigbee协议栈,它包含了构建Zigbee网络所需的所有基础组件和示例代码。
1.2 打开示例工程
在Z-Stack 3.0.2中,提供了多个示例工程,本文选择GenericApp
作为起点。这个示例工程展示了如何使用Zigbee协议进行基本的设备通信。
1.3移植外设驱动
这里需要移植一下 善学坊 的oled显示驱动函数,可以参考一下下面的链接:

注意不同板子的引脚不同,需要在对应的.h文件中更改引脚位号
二、实现过程
2.1 开启和加入网络
协调器是Zigbee网络的中心节点,负责创建和管理网络。在zclGenericApp_Init
函数中,通过宏定义ZDO_COORDINATOR
来区分协调器和其他设备(路由器或终端)。协调器的初始化过程包括注册ZDO消息、启动网络形成过程以及允许设备加入网络。
#if defined(ZDO_COORDINATOR) //协调器
#elif defined(RTR_NWK) //路由器
#else //终端节点
#endif
1.在APP组 -> zcl_genericapp.c -> void zclGenericApp_Init( byte task_id )的末尾添加下图代码:
对于协调器:开启网络
对于终端节点:加入网络并在网络中广播自己本体
还需要添加定时事件1,用来控制led闪烁展示设备连接网络的状态
对于终端节点:
还需要添加一个定时事件2,用来读取温湿度数据并且根据网络连接情况来上报数据
#if defined(ZDO_COORDINATOR) //协调器
ZDO_RegisterForZDOMsg ( zclGenericApp_TaskID, Device_annce );
bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_FORMATION |
BDB_COMMISSIONING_MODE_FINDING_BINDING );
NLME_PermitJoiningRequest(255);
#elif defined(RTR_NWK) //路由器
#else //终端节点
bdb_StartCommissioning( BDB_COMMISSIONING_MODE_NWK_STEERING |
BDB_COMMISSIONING_MODE_FINDING_BINDING );
zclGenericApp_DeviceAnnce();
osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_2, 2000 );
#endif
osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 0 );
2.2 设备发现
2.2.1 终端节点设备广播
路由器或终端在加入网络后,会通过ZDP_DeviceAnnce
函数广播自己的设备信息,包括网络地址、物理地址等。协调器接收到这些信息后,会保存设备的网络地址,以便后续通信。
static void zclGenericapp_DeviceAnnce( void )
{
ZDP_DeviceAnnce(
NLME_GetShortAddr(),//获取本设备的网络地址(短地址)
NLME_GetExtAddr(),//获取本设备的物理地址(通常就是MAC地址)
ZDO_Config_Node_Descriptor.CapabilityFlags,//暂不展开简介,可忽略
0//暂不展开讲解,可忽略
);
}
2.2.2 协调器处理设备广播
协调器在接收到设备广播后,会自动调用zclGenericapp_processZDOMgs
函数处理这些信息,并保存设备的网络地址。
这里的zclGenericapp_processZDOMgs
函数需要在zclGenericApp_event_loop函数中调用:
#ifdef ZDO_COORDINATOR
case ZDO_CB_MSG:
zclGenericapp_processZDOMgs( (zdoIncomingMsg_t *)MSGpkt );
break;
#endif
static void zclGenericApp_ProcessZDOMsg(zdoIncomingMsg_t *pMsg)
{
switch ( pMsg->clusterID )
{
case Device_annce:
{
char str[20] = {0};
LedDuty = 3;
zcGenericApp_OnOffAddr[EndDeviceIndex++] = pMsg->srcAddr.addr.shortAddr;
sprintf(str, "Node:%X %X", zcGenericApp_OnOffAddr[0], zcGenericApp_OnOffAddr[1]);
HalLcdWriteString(str, 3);
EndDeviceIndex %= 2;
}
break;
default:
break;
}
}
2.3 连接网络状态处理
在路由器或终端中,需要处理网络加入的状态。如果加入成功,则加快LED闪烁频率;如果加入失败,则减慢LED闪烁频率,并尝试重新加入网络。
这里利用hal库中led驱动函数:
这里定义一个占空比变量:uint8 LedDuty = 50;
HalLedBlink( HAL_LED_2, 1, LedDuty, 1000);
这里的意义是:以周期1000ms占空比50%闪烁一次LED2
在这里初始化了一个任务:在void zclGenericApp_Init( byte task_id )中
osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 0 );
在uint16 zclGenericApp_event_loop( uint8 task_id, uint16 events ) 中
if ( events & GENERICAPP_EVT_1 )
{
// toggle LED 2 state, start another timer for 500ms
HalLedBlink( HAL_LED_2, 1, LedDuty, 1000);
osal_start_timerEx( zclGenericApp_TaskID, GENERICAPP_EVT_1, 1000 );
return ( events ^ GENERICAPP_EVT_1 );
}
在下方函数中处理关于网络状态改变时的操作:
static void zclGenericApp_ProcessCommissioningStatus(bdbCommissioningModeMsg_t *bdbCommissioningModeMsg)函数中:
里面有几个主要的分支:
case BDB_COMMISSIONING_PARENT_LOST:代表已经连接的网络 丢失或者恢复
case BDB_COMMISSIONING_NWK_STEERING:代表网络成功连接,或者网络连接失败
在这些分支中,我们可以执行对应的操作,例如重新连接网络,改变led状态
2.3.1 网络丢失或者恢复
2.3.2 网络连接失败或者成功
2.4 开关指令控制LED
2.4.1 绑定命令回调
路由器或终端在初始化时,会绑定一系列ZCL命令的回调函数。当接收到相应的命令时,会调用对应的回调函数进行处理。例如,接收到开关命令时,会调用zclGenericapp_OnOffCB
函数控制LED的开关。
需要开启一个宏定义:ZCL_ON_OFF
zclGenericApp_Init初始化时会注册一个回调事件:
zclGeneral_RegisterCmdCallbacks( GENERICAPP_ENDPOINT, &zclGenericApp_CmdCallbacks );
static zclGeneral_AppCallbacks_t zclGenericApp_CmdCallbacks =
{
//省略
#if defined(ZDO_COORDINATOR) //协调器
zclGenericApp_OnOffCB,
#elif defined(RTR_NWK) //路由器
#else //终端节点
zclGenericApp_OnOffCB, // On/Off cluster commands
#endif
//省略
}
在zclGenericApp_Init函数初始化:
这里需要绑定函数,接收到开关命令时,会调用zclGenericapp_OnOffCB
函数控制led。
这个的意思是,如果设备收到了这个控制命令,会自动触发这个函数:zclGenericApp_OnOffCB
参数是固定的,这里参数为什么是 uint8 cmd ,笔者认为是因为这里原本就是预设好的回调函数位置,功能是预设的
static void zclGenericApp_OnOffCB( uint8 cmd )
{
if(cmd == COMMAND_ON) // 命令为ON时
{
HalLedSet(HAL_LED_1, HAL_LED_MODE_ON); // 开启所有LED
}
else if(cmd == COMMAND_OFF) // 命令为OFF时
{
HalLedSet(HAL_LED_1, HAL_LED_MODE_OFF); // 关闭所有LED
}
else if(cmd == COMMAND_TOGGLE) // 命令为OFF时
{
HalLedSet(HAL_LED_1, HAL_LED_MODE_TOGGLE); // 关闭所有LED
}
}
2.4.2 开关指令发送
有了接收那肯定有发送,这个发送的函数是协议栈中自带的,拿一个关命令举例:
zclGeneral_SendOnOff_CmdOn(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
然后自己封装一个函数:
传入设备在网络中的短地址和命令,通过点对点通信实现对某一个设备的命令控制
static void zclGenericApp_OnOffCommand( uint16 shortAddr, uint8 cmd )
{
afAddrType_t destAddr;
static uint8 txID = 0;
destAddr.endPoint = GENERICAPP_ENDPOINT;
destAddr.addrMode = afAddr16Bit;
destAddr.addr.shortAddr = shortAddr;
switch( cmd )
{
case COMMAND_ON:
zclGeneral_SendOnOff_CmdOn(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
break;
case COMMAND_OFF:
zclGeneral_SendOnOff_CmdOff(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
break;
case COMMAND_TOGGLE:
zclGeneral_SendOnOff_CmdToggle(GENERICAPP_ENDPOINT, &destAddr, TRUE, txID++);
break;
default:
break;
}
}
2.4.3 开关指令发送
这里面包含后续的数据上报函数
根据预编译设置不同的网络短地址,来实现发送不同设备的功能
在按键处理函数中实现:
static void zclGenericApp_HandleKeys( byte shift, byte keys )
{
if ( keys & HAL_KEY_SW_6 )
{
uint16 shortAddr;
#if defined(ZDO_COORDINATOR) //协调器
char *str = "Coordinator";
shortAddr = zcGenericApp_OnOffAddr[0];
#elif defined(RTR_NWK) //路由器
#else //终端节点
char *str = "EndDevice";
shortAddr = 0x0000;
#endif
zclGenericApp_OnOffCommand( shortAddr, COMMAND_TOGGLE );
zclGenericApp_ReportCommand( shortAddr, osal_strlen(str), (uint8 *)str);
}
}
2.5 数据上报
2.5.1 数据上报函数
这里自己定义一个函数,通过传入设备的短地址和数据长度以及数据指针,实现上报数据到指定的设备中
reportCmd->attrList[0]里面的每一项数据,需要按照自己实际需求来设置,开辟空间,最后需要释放空间
函数中涉及的属性ID和cluster ID最好是使用协议栈中规定的内容,学习过程中,使用私有定义也没问题
函数里调用一个协议栈自带的函数:zcl_SendReportCmd,这个就是发送的功能函数,里面还会一层一层的往下调用,学习中只需要知道这个函数的使用即可
/*
* 数据上报事件的处理函数,用于上报数据
*/
static void zclGenericApp_ReportCommand(uint16 shortAddr, uint16 len, uint8 *cmd)
{
static uint8 seqNum = 0;
zclReportCmd_t *reportCmd;
//目标设备的地址信息
afAddrType_t destAddr;
destAddr.addrMode = afAddr16Bit;
destAddr.endPoint = GENERICAPP_ENDPOINT;
destAddr.addr.shortAddr = shortAddr;
reportCmd = (zclReportCmd_t *)osal_mem_alloc(sizeof(zclReportCmd_t)+sizeof(zclReport_t));//申请内存空间
if(reportCmd == NULL)//判断内存空间是否申请成功
return;
reportCmd->numAttr = 1;//属性数量为1
reportCmd->attrList[0].attrID = ATTRID_CLUSTER_REVISION;//属性ID
reportCmd->attrList[0].dataType = ZCL_DATATYPE_CHAR_STR;//数据类型
(reportCmd->attrList[0].attrData) = cmd;//属性值
//上报数据
zcl_SendReportCmd(GENERICAPP_ENDPOINT,//源端点号
&destAddr,//地址信息
ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT,//Cluster ID
reportCmd,
ZCL_FRAME_CLIENT_SERVER_DIR,//通信方向为从客户端到服务端
TRUE,//关闭默认响应(目标设备的响应)
seqNum++ );//数据包标号,每上报一次数据seqNum的值就会增加1
osal_mem_free(reportCmd);
}
2.5.2 数据上报接收处理函
设备接受数据上报的信息:
uint16 zclGenericApp_event_loop( uint8 task_id, uint16 events ) 的 下方函数
在这个函数中:
static void zclGenericApp_ProcessIncomingMsg( zclIncomingMsg_t *pInMsg )
{
switch ( pInMsg->zclHdr.commandID )
{
//省略
case ZCL_CMD_REPORT:
bdb_ProcessIncomingReportingMsg( pInMsg );
break;
//省略
default:
break;
}
if ( pInMsg->attrCmd )
osal_mem_free( pInMsg->attrCmd );
}
如果接受到数据上报会触发一个分支:case ZCL_CMD_REPORT:
这里放自己的数据处理函数,例如下方的接受节点发送的字符串数据
static uint8 bdb_ProcessIncomingReportingMsg( zclIncomingMsg_t *pInMsg )
{
zclReportCmd_t *reportCmd;
uint8 i;
reportCmd = (zclReportCmd_t *)pInMsg->attrCmd;
for ( i = 0; i < reportCmd->numAttr; i++ )//reportCmd->numAttr为属性数量
{
if( pInMsg -> clusterId == ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT//Cluster ID
&& reportCmd->attrList[i].attrID == ATTRID_CLUSTER_REVISION)//属性ID
{
char str[20];
uint8* attrDat = (reportCmd->attrList[i].attrData);//读取属性值
sprintf(str, "Rx:%s", attrDat);
HalLcdWriteString( str, 4 );//显示属性值
}
}
return ( TRUE );
}
其中这里的:
if( pInMsg -> clusterId == ZCL_CLUSTER_ID_MS_TEMPERATURE_MEASUREMENT//Cluster ID
&& reportCmd->attrList[i].attrID == ATTRID_CLUSTER_REVISION)//属性ID
就是用来判断哪个设备发送来的数据,实际使用中可能需要更多的判断
三、问题与思考,请求各位读者解答
3.1 多个终端设备如何实现控制:
例如我有三个设备,一个协调器,一个温湿度终端设备,一个风扇终端设备,想通过温度去控制风扇
未确认思路:是需要把温湿度数据上报给协调器,然后协调器再根据代码逻辑去控制风扇吗
3.2 当终端节点重新烧录固件后,需要复位节点和协调器,该终端节点才可以重新加入网络中或者重新烧录固件后,需要重新烧录所有的节点代码
未确认思路:是不是里面会有一个掉电不丢失存储着上次连接的状态,每次烧录是擦除了所以才可以重新连接到网络中,那么要怎么解决这样的问题呢
3.3 看网络上一些zigbee设备可以实现多设备或者不同厂家的设备互联,这是如何实现的
未确认的思路:是不是因为协议栈开发规范,需要准者每个设备的实际用途,需要按照相关规定编写代码
例如cluster,属性id,设备id,产品类型等等,网关协调器的代码也是提前写好大量的功能处理函数吗(预判设备),例如我接入一个温湿度节点设备,网关就可以获取我的温湿度
但是不清楚一个具体的实现过程
3.4 低功耗设计
Zigbee设备的一个重要特点是低功耗。目前,本实现还没有进行低功耗设计。未来可以通过优化代码、使用低功耗硬件、设置休眠模式等方式来降低设备功耗。
3.5 工程裁剪与优化
Z-Stack 3.0.1协议栈包含了大量的功能和示例代码,对于特定的应用来说,可能有很多不需要的部分。未来可以根据实际需求对工程进行裁剪和优化,去除不需要的功能和代码,减少资源占用和功耗。
四、总结与展望
通过本次学习实践,笔者成功实现了基于Z-Stack 3.0.2的Zigbee成功实现一个简单的开关控制指令和温湿度数据上报过程。这个过程不仅加深了对Zigbee协议的理解,还提高了编程实践和问题解决的能力。未来,笔者将继续深入探索Zigbee技术的更多应用场景和优化方法,为物联网技术的发展贡献自己的力量。
同时也希望读者阅读本文章末尾的问题后,能提供一些解决思路,让笔者更加深入探索Zigbee技术