前言
对于嵌入式开发初学者而言,德州仪器(TI)的CC2530芯片是一款非常经典的ZigBee无线通信学习平台。然而,很多同学在初次接触时,往往对其核心的无线组网功能感到无从下手。本文将以应用层的角度,介绍ZStack协议栈的基础知识,并通过实际项目案例,帮助读者快速掌握CC2530的无线组网开发技巧。
一、ZStack协议栈概述
1.1 ZStack架构
ZStack是TI公司基于ZigBee协议开发的协议栈实现,采用四层架构设计:
-
应用层(APL):用户功能实现的主要层级
-
安全层:负责通信加密与安全认证
-
网络层(NWK):处理网络组建与路由
-
MAC/物理层(PHY):底层无线通信控制
TI已经对底层协议进行了完善封装,开发者只需专注于应用层开发即可快速实现功能。
1.2 网络设备类型
| 设备类型 | 功能描述 | 典型应用场景 |
|---|---|---|
| 协调器(Coordinator) | 组建和管理网络,分配网络地址 | 网关设备 |
| 路由器(Router) | 数据中继,扩展网络覆盖范围 | 中继节点 |
| 终端设备(End Device) | 数据采集或执行控制指令 | 传感器/执行器节点 |
另外,我们除了这三种状态,还需要了解短地址这个概念,设备一入网,协调器就会向入网的设备自动分配一个唯一的短地址,一个网络中只能有一个协调器,且它的短地址默认为0x0000。
二、ZStack开发环境搭建
2.1 安装流程
使用ZStack协议栈之前我们需要安装ZStack,下图为ZStack安装时的界面,文章全篇使用
ZStack 2.5.1a版本,安装时只需要一直点击下一步,路径自行选择合适的就可以。

安装完后,会在安装目录生成一个ZStack协议栈的文件夹,里面包含了ZStack的工程

2.2 工程配置
-
打开SampleApp工程(路径:Projects/zstack/SampleApp/CC2530DB)

-
在Workspace中选择设备类型:
-
Coordinator:协调器
-
Router:路由器
-
EndDevice:终端设备
-

注意:不同模式对应不同的编译配置(.cfg文件),可在Tool目录下查看具体差异。

三、ZStack核心机制解析
3.1 OSAL操作系统抽象层
OSAL(Operating System Abstraction Layer)是ZStack的任务调度核心,采用事件驱动机制。关键初始化流程在 ZMain.c中实现,下图均为必要的初始化函数,它们最终的运行结果是启动OSAL系统



3.2 关键开发概念
3.2.1 簇(Cluster)
在ZStack中,簇的作用是定义设备的功能和交互方式,关联到设备的功能集(包括设备的属性、命令、响应等),最终协议栈会将簇的逻辑映射到数据包。
使用簇ID前,要定义簇ID,并且添加到下面的变量中
const cId_t SampleApp_ClusterList[SAMPLE_MAX_CLUSTERS] =
{
};
我们可以添加多个簇ID到集群中,只需要在.h文件中提前定义好即可
注意:簇ID本质是数字,在定义的时候不能让不同名称的簇使用同一个数字
3.2.2 无线发送函数(AF)
AF_DataRequest( &SampleApp_Periodic_DstAddr, &SampleApp_epDesc,
SAMPLEAPP_PERIODIC_CLUSTERID,
1,
(uint8*)&SampleAppPeriodicCounter,
&SampleApp_TransID,
AF_DISCV_ROUTE,
AF_DEFAULT_RADIUS ) == afStatus_SUCCESS
ZStack中的无线发送函数如上,它包括了8个参数,分别是目标地址,端点描述符,簇ID,数据长度,数据缓冲区,任务ID,发现选项和最大跳数,实现应用层的功能,我们只需要关注前5个参数即可。
在使用的时候,我们需要注意目标地址是否正确,如果是终端发送给协调器,那么地址设置为0x0000即可,如果想要协调器发送给终端,那么我们就需要先获取终端的短地址,后面我会讲述如何获取并上报设备自己的短地址
端点描述符已经在SampleApp_Init()这个函数中定义好了,直接用就可以
afRegister( (endPointDesc_t *)&SampleApp_epDesc );
簇ID,关联到响应的设备功能集,我们给什么设备和行为设置了什么簇ID,就写什么
3.2.3 事件处理函数
SampleApp_ProcessEvent( uint8 task_id, uint16 events )
我们来看划线的部分,在这部分中,主要包含了TI为我们封装好的按钮,无线接收和网络状态事件的处理模板



这里的原理就是系统的一个定时器时间一到,就会依次检查按钮,无线接收,网络状态这三个case是否有事件产生,osal_start_timerEx()就是我们的定时器函数,是必不可少的。
在每一个if 事件分支结束前,假设当前为if ( events & SYS_EVENT_MSG )这个系统事件,我们需要return (events ^ SYS_EVENT_MSG);这一句的含义是清除已经完成的SYS_EVENT_MSG事件,未完成的事件保留,当所有事件完成后,return 的自然就是0了。
除了系统定义的事件外,用户也可以自己定义自己的事件,定义自己的事件需要在SampleApp.h中声明一下这个事件,如下图

声明后,我们就可以在.c中使用它了

当然定时器的时间也要自己提前设置一下,这里例子里给的是5000也就是5s
四、实战项目:基于ZigBee的种子仓库监管系统
接下来,我将分享一个实战项目,基于ZigBee的种子仓库监管系统
项目全开源,链接会放在文章末,有需要自取即可
4.1项目介绍
4.1.1硬件组成
传感器节点:cc2530,DHT11(温湿度模块),SGP30(空气质量检测模块),MRC 522 (RFID模块)
网关节点:cc2530,ESP8266(WIFI模块),OLED显示屏
执行器节点:cc2530 LED*3
4.1.2项目功能
传感器节点通过sensor采集环境数据,以ZStack协议栈的方式,将数据无线发送到网关节点,网关节点接收数据后判断参数是否超过阈值,并下达控制指令给执行器节点,执行器节点接入三块LED灯用来模拟空调的制冷制热以及风扇的通风,同时传感器节点接入了RFID模块,读取种子的出库入库信息,上报给网关节点,网关节点会将接收到的数据以串口的方式发送给ESP8266,ESP8266会建立两套socket,一套用于上传数据给云平台,另一套则是与QT上位机进行连接,进行数据的通信。
4.2功能实现
4.2.1 温湿度等数据的上报以及超过阈值的自动控制
由于cc2530读取DHT11的代码网上有开源,这里只讲述如何发送给协调器以及如何实现后续功能
void SampleApp_Send_P2P_Message( void )
{
uint8 str[5]={0};
uint8 strTemp[32]={0};
int len=0;
unsigned long co2 = 0;
DHT11(); //获取温湿度
str[0] = 1;//增加一个ID,如果是多个终端就增加这个值
str[1] = wendu;//温度
str[2] = shidu;//湿度
SGP30_Write(); //向SGP30发送读取数据命令
co2 = SGP30_Read(); //获取CO2浓度
//I2C_Delay_ms(1000);
str[3] = ( co2 >> 8 ) & 0xFF;
str[4] = co2 & 0xFF;
len=5;
sprintf(strTemp, "T&H&C:%d %d %lu", str[1],str[2],co2);
//HalLcdWriteString(strTemp, HAL_LCD_LINE_3); //LCD显示
HalUARTWrite(0, strTemp, osal_strlen(strTemp)); //串口输出提示信息
HalUARTWrite(0, "\r\n",2);
//无线发送到协调器
if ( AF_DataRequest( &SampleApp_P2P_DstAddr, &SampleApp_epDesc,
SAMPLEAPP_P2P_CLUSTERID,
len,
str,
&SampleApp_MsgID,
AF_DISCV_ROUTE,
AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
{
}
else
{
// Error occurred in request to send.
}
}
值得一提的是,TI为我们配置好了串口,在ZStack中,串口有两种使用方式,一种是DMA的方式,一种是中断的方式,只是用一种的话,哪种都可以,但是要同时用两个,就必须一个设置为中断,一个设置为DMA的方式。
那么接下来就要讲述ID的作用了,首先我们已经通过AF将数据包发送了出去,我们紧接着关注SampleApp_ProcessEvent()这个函数,在系统事件中,如果接收到了数据包,会进入到case AF分支,该分支主要执行一个函数,SampleApp_ProcessMSGCmd()

这个函数的作用就是用于解析接收到的数据并进行下一步操作
这里我们先展示和温湿度有关的部分,我们逐步解析一下
void SampleApp_ProcessMSGCmd( afIncomingMSGPacket_t *pkt )
{
uint8 buff[100]={0};
switch ( pkt->clusterId )
{
// 接收终端上传的温度数据
#ifdef ZDO_COORDINATOR
case SAMPLEAPP_P2P_CLUSTERID:
{
uint8 id = pkt->cmd.Data[0];//终端id
if(id==1)
{
temp1Int = pkt->cmd.Data[1]; //终端温度
hum1Int = pkt->cmd.Data[2]; //终端湿度
co2Int = (pkt->cmd.Data[3] << 8) | pkt->cmd.Data[4]; // CO2(16位)
//保存终端1的温度和湿度
if(temp1Int >= 28)
{
P0_7=1;
SampleApp_Device2Ctl(2,0x01,0);
}
else if(temp1Int <= 20)
{
P0_7 = 1;
SampleApp_Device2Ctl(2,0x02,0);
}
else if(hum1Int >= 35 || co2Int >= 1000)
{
P0_7 = 1;
SampleApp_Device2Ctl(2,0x04,0);
}
else
{
P0_7=0;//不报警
SampleApp_Device2Ctl(2,0x01,1);
SampleApp_Device2Ctl(2,0x02,1);
SampleApp_Device2Ctl(2,0x04,1);
}
}
}
break;
首先在switch语句中,我们看到了pkt->clusterId,pkt->clusterId的作用是区分ZigBee中的不同簇,体现在下面的case 语句中,由于我们前面无线发送使用的是SAMPLEAPP_P2P_CLUSTERID这个簇ID,这里的case 就要保持一致,接着我们这里用到了一个条件编译开关ZDO_COORDINATOR这里使用这个条件编译的原因是只让协调器接收数据,其他的不会接收到传感器节点发送的环境参数,接着我们去读取pkt->cmd.Data[0],在前面无线发送的时候,我们的str[5]中的数据被cmd.Data接收,所以只需要按照我们发送过来的格式解析就可以了,接收到的数据会存放在全局变量中。

前面我们讲述了终端如何无线发送数据给协调器,接下来我们讲述协调器如何发送命令给终端,我们回到SampleApp_ProcessEvent这个函数当中,关注case ZDO_STATE_CHANGE:
在设备联网成功后,除协调器外的所有设备都要执行SampleApp_DeviceConnect(),用于上报自己在网络中的短地址,协调器的短地址默认是0x0000,所以不用上报

函数定义部分

在这个函数中主要做三件事
1.获取设备地址:
nwkAddr = NLME_GetShortAddr() 获取设备自身的16位短地址
2.设置目标地址:
创建afAddrType_t结构体SampleApp_TxAddr
设置地址模式为16位短地址(Addr16Bit)
设置端点号为SAMPLEAPP_ENDPOINT
目标地址设为0x0000(协调器地址)
3. 准备并发送数据
建立5字节缓冲区buff并初始化为0
buff[0]设置为设备ID
buff[1]和buff[2]分别存储设备网络地址的高字节和低字节
AF发送自己的ID+短地址到协调器
函数运行完后,设备的短地址就成功发送给了协调器上,但是一定要注意ID的问题,否则会导致短地址覆盖的问题,举个例子,传感器节点的ID为1,执行器节点的终端为2,在烧录程序的时候一定要去修改这个ID值,否则执行器节点也会发送错误的环境数据给协调器,这个ID是我们人为为了区分不同的设备而添加的校验值
这里再教给大家一个办法来区分开不同设备,以该项目为例,项目有传感器节点和执行器节点,这两个节点都属于终端设备怎么办呢,我们只需要两个宏定义,一个是传感器节点的,一个是执行器节点的
要执行传感器节点的代码的时候就把执行器节点的宏定义注释掉,反之则反,并且在要实现功能的部分前加上,以下为示例
#id defined ( SENSOR_TERMINAL )
/*
自己要实现的功能代码
*/
#endif
就可以了
当协调器也成功接收到了短地址后,会将终端传来的短地址存入到全局变量devAddr[3]中,终端1的短地址存到devAddr[0]中,终端二的短地址传入到devAddr[1]中。
当传感器节点发送的数据超过阈值,协调器就会进入对应情况的分支,去执行SampleApp_Device2Ctl()这个函数
SampleApp_Device2Ctl()的定义如下
void SampleApp_Device2Ctl(uint8 id, uint8 led,uint8 cmd)
{
uint8 str[3]={id,led,cmd};
afAddrType_t SampleApp_TxAddr;
SampleApp_TxAddr.addrMode = (afAddrMode_t)Addr16Bit;//点播
SampleApp_TxAddr.endPoint = SAMPLEAPP_ENDPOINT;
SampleApp_TxAddr.addr.shortAddr = devAddr[1];//终端2短地址
if ( AF_DataRequest( &SampleApp_TxAddr, &SampleApp_epDesc,
SAMPLEAPP_P2P_CLUSTERID,
3,
(uint8 *)str,
&SampleApp_MsgID,
0,
AF_DEFAULT_RADIUS ) == afStatus_SUCCESS )
{
}
else
{
}
}
这个时候,就是我们前面讲述的协调器向终端发送命令的过程,和终端向协调器发送数据的结构是一样的,唯一需要修改的就是终端2的短地址,在该项目中,执行器都是用LED灯模拟变化,所以我们需要发送三个参数,设备ID,LED地址,控制命令
设备ID是我们自己定义的,LED的的地址定义在HAL库下,以独热码的形式存在,还包括LED灯运行的模式,可以自己使用喜欢的模式。
发送控制命令需要两个关键的参数,一是设备地址,二是指令,理解原理后可以自己尝试其他设备的无线控制
来总结一下整个功能实现的步骤
传感器节点读取数据并解析,无线发送给目标设备,发送时要保证目标地址正确,保证协调器接收时的簇ID与AF参数中的簇ID一致,设备上报自己的短地址,协调器接收后判断情况发送控制指令给终端二。
4.2.2 RFID的读取与上报
在种子仓库监管系统中,RFID 模块(MFRC522)用于识别种子的出库入库信息,传感器节点需要将读取到的 RFID 卡片数据通过 ZigBee 网络上报给协调器,实现种子流转的自动化记录。以下从硬件初始化、卡片数据读取、无线上报三个环节详细说明实现过程。
1. RFID 模块初始化(MFRC522)
MFRC522 是一款高频 RFID 读写器芯片,支持 ISO 14443A 标准(如 Mifare 系列卡片)。在 CC2530 上使用时,需先完成硬件引脚配置和模块初始化,确保其能正常与卡片通信。
初始化流程:
#define MF522_RST P0_4 // 复位引脚
#define MF522_NSS P0_5 // 片选引脚
#define MF522_SO P0_6 // SPI数据输入
#define MF522_SI P1_5 // SPI数据输出
#define MF522_SCK P1_0 // SPI时钟
模块复位与配置:在RFID_Init()函数中完成 MFRC522 的复位、天线开启和通信协议配置(ISO 14443A)
void RFID_Init()
{
P0DIR |= 0xF0; //P0_4、P0_5、P0_6、P0_7定义为输出
P1DIR |= 0x21;//p1_0,P15输出
P0 |= 0xF0; //P0_4、P0_5、P0_6、P0_7输出1
P1 |= 0x21; //P1_0,P15输出高电平
P0SEL &= ~0x40; //设置P0.6口为普通IO
P0DIR &= ~0x40; //设置P0.6为输入
CmdValid=0;
PcdReset(); //复位
PcdAntennaOff(); //关闭天线
PcdAntennaOn(); //开启天线
M500PcdConfigISOType( 'A' ); //配置位ISO 14443A协议
}
初始化完成后,MFRC522 进入等待状态,可开始检测并读取 RFID 卡片。
2. RFID 卡片数据读取流程
读取 RFID 卡片需经过寻卡、防冲撞、选卡三个步骤,最终获取卡片的类型和唯一序列号(UID)。这些操作封装在iccardcode()函数中,由传感器节点周期性调用。
关键步骤解析:
寻卡(PcdRequest):发送寻卡命令(如PICC_REQIDL),检测天线范围内是否有未休眠的卡片,返回卡片类型(如 Mifare S50、S70 等):
// 寻卡示例(iccardcode()中case 2分支)
status = PcdRequest(RevBuffer[1], &RevBuffer[2]); // RevBuffer[1]为寻卡模式(0x26或0x52)
if (status != MI_OK) {
// 寻卡失败,重试一次
status = PcdRequest(RevBuffer[1], &RevBuffer[2]);
}
成功后,RevBuffer[2]和RevBuffer[3]存储卡片类型(如 0x0400 表示 Mifare S50)。
防冲撞(PcdAnticoll):当多个卡片同时在天线范围内时,通过防冲撞算法获取唯一卡片的序列号(4 字节 UID),避免数据冲突:
// 防冲撞示例(iccardcode()中case 3分支)
status = PcdAnticoll(&RevBuffer[2]); // 读取UID到RevBuffer[2]~RevBuffer[5]
if (status == MI_OK) {
memcpy(MLastSelectedSnr, &RevBuffer[2], 4); // 保存UID到全局变量
}
选卡(PcdSelect):根据获取的 UID 选中特定卡片,后续操作(如读写数据)仅对该卡片生效:
// 选卡示例(iccardcode()中case 4分支)
status = PcdSelect(MLastSelectedSnr); // MLastSelectedSnr为之前获取的UID
数据封装:读取成功后,卡片信息(类型 + UID)被存入SendBuf数组,格式为:
SendBuf[0]~SendBuf[1]:卡片类型(2 字节)
SendBuf[2]~SendBuf[5]:卡片 UID(4 字节)
3. RFID 数据无线上报
传感器节点将读取到的 RFID 数据通过 ZigBee 单播发送给协调器,使用独立的簇 ID(SAMPLEAPP_P2P_CLUSTERID1)与温湿度数据区分,确保协调器能正确解析。
上报实现:
周期性触发:在SampleApp_SendPeriodicMessage()函数中,传感器节点每 3 秒(可通过定时器调整)调用iccardcode()读取 RFID 数据,并通过AF_DataRequest发送:
void SampleApp_SendPeriodicMessage(void) {
uint8 SendBuf[6] = {0}; // 存储卡片类型(2字节)+ UID(4字节)
uint8 error = 0;
// 读取RFID卡片(过程略,最终结果存入SendBuf)
iccardcode();
if (/* 读取成功 */) {
// 发送数据到协调器
AF_DataRequest(&SampleApp_P2P_DstAddr, // 目标地址:协调器(0x0000)
&SampleApp_epDesc, // 端点描述符
SAMPLEAPP_P2P_CLUSTERID1, // 簇ID(RFID专用)
6, // 数据长度(6字节)
SendBuf, // 数据缓冲区
&SampleApp_MsgID,
0,
AF_DEFAULT_RADIUS);
}
}
协调器解析与显示:协调器通过SampleApp_ProcessMSGCmd()函数接收数据,根据簇 IDSAMPLEAPP_P2P_CLUSTERID1解析卡片类型和 UID,并通过串口打印或 LCD 显示:
// 协调器解析RFID数据(SampleApp_ProcessMSGCmd()中case SAMPLEAPP_P2P_CLUSTERID1分支)
if (pkt->cmd.DataLength == 6) {
// 解析卡片类型
if (pkt->cmd.Data[0] == 0x04 && pkt->cmd.Data[1] == 0x00) {
HalUARTWrite(0, "Mifare_One(S50),", strlen("Mifare_One(S50),"));
}
// 解析UID(转换为十六进制字符串)
char card_buff[20] = {0};
card_buff[0] = 'I'; card_buff[1] = 'D'; card_buff[2] = ':';
for (int i=0; i<4; i++) {
// 将每个字节转换为2位十六进制字符(如0x1A → "1A")
card_buff[3+i*2] = NumberToLetter((pkt->cmd.Data[2+i] >> 4) & 0x0F);
card_buff[3+i*2+1] = NumberToLetter(pkt->cmd.Data[2+i] & 0x0F);
}
HalUARTWrite(0, card_buff, strlen(card_buff)); // 串口输出UID
}
上述流程就是传感器节点实时读取RFID卡片的信息并上报给协调器,实现种子出库入库的识别与记录。
这里再说一点,RFID使用的簇ID是SAMPLEAPP_P2P_CLUSTERID1,也可以使用SAMPLEAPP_P2P_CLUSTERID这个簇ID,我们使用不同的簇ID是为了区分RFID和环境传感器
结束
本片文章写的比较匆忙,如有错误部分,虚心接受各位的指正,希望能对初学者能有些许的帮助
992

被折叠的 条评论
为什么被折叠?



