在物联网智能家居和工业自动化领域,Zigbee 协议凭借其低功耗、自组网和高可靠性的特点,成为了最主流的无线通信标准之一。意法半导体的 STM32WB 系列微控制器集成了双核架构(Cortex-M4 应用处理器 + Cortex-M0 + 无线协处理器),为 Zigbee 应用提供了强大的硬件平台。虽然 Exegin ZSDK(Zigbee 软件设计套件)已经提供了绝大多数标准 ZCL(Zigbee 集群库)集群的模板,但在实际项目开发中,我们经常会遇到需要实现特定功能的场景,这时就需要开发自定义的 ZCL 集群。本文将基于意法半导体 AN5491 应用笔记,详细讲解如何在 STM32WB 系列上创建和使用自定义 Zigbee 集群。
1. 为什么需要自定义 Zigbee 集群?
Zigbee 联盟定义了一套完整的标准集群库,涵盖了照明、温控、安防、能源管理等常见应用场景。这些标准集群确保了不同厂商设备之间的互操作性,是 Zigbee 生态系统的基础。
然而,在实际开发中,我们经常会遇到以下情况:
- 产品需要实现标准集群中没有的特殊功能
- 希望简化标准集群,只保留必要的属性和指令
- 需要添加厂商特定的扩展功能
- 对设备的功耗或性能有特殊要求
在这些情况下,开发自定义集群就成为了必要的选择。AN5491 应用笔记正是为了解决这个问题而编写的,它详细介绍了如何按照与 Exegin ZSDK 标准集群相同的方式,构建和集成自定义 ZCL 集群。
2. ZCL 集群架构基础
在开始开发自定义集群之前,我们需要先了解 ZCL 集群的基本架构和工作原理。
2.1 集群的基本概念
Zigbee 集群库(ZCL)定义了应用节点之间通过网络进行交互的机制。它将特定的功能整理成 "集群",每个集群包含一组相关的属性和指令。例如,"开 / 关" 集群定义了控制设备开关状态的功能,包含一个表示当前状态的属性和几个控制开关的指令。
每个集群都有客户端和服务器两种角色:
- 服务器端:存储属性值并执行客户端发送的指令
- 客户端:向服务器发送指令并读取或写入服务器的属性
以常见的照明系统为例,开关是客户端,灯泡是服务器。开关通过发送 "开 / 关" 指令来控制灯泡的状态,同时也可以读取灯泡的当前状态属性。
2.2 ZCL 在协议栈中的位置
ZCL 基于 APS(应用支持子层)消息构建,而 APS 消息又使用 NWK(网络层)的功能。Exegin ZSDK 提供了通用的 ZCL 基础服务,而集群模板则在这些通用服务之上实现了特定的功能。
在应用开发中,我们的工作就是在集群模板中填充设备特定的细节。例如,在服务器端实现如何控制物理灯泡的开关,在客户端实现如何读取物理开关的状态。
3. 属性操作:从定义到端到端通信
属性是 ZCL 集群的核心组成部分,它代表了设备的状态或配置信息。理解属性的工作原理是开发自定义集群的基础。
3.1 属性的定义与添加
每个属性都由一个唯一的标识符、数据类型和访问权限组成。在 ZSDK 中,我们使用struct ZbZclAttrT结构来定义属性。例如,下面是 "开 / 关" 集群中 "开启时间" 属性的定义:
static const struct ZbZclAttrT attr_list[] = {
{
ZCL_ONOFF_ATTR_ON_TIME,
ZCL_DATATYPE_UNSIGNED_16BIT,
ZCL_ATTR_FLAG_NONE,
0,
NULL,
{0, 0},
{0, 0}
},
};
定义好属性列表后,我们使用ZbZclAttrAppendList()函数将属性添加到集群中:
ZbZclAttrAppendList(cluster, attr_list, ZCL_ATTR_LIST_LEN(attr_list));
3.2 属性的两种管理方式
ZSDK 提供了两种属性管理方式,开发者可以根据应用需求选择:
方式一:协议栈自动管理
这是最简单的方式,如上面的例子所示,将属性回调函数设置为 NULL。在这种情况下,ZCL 集群基库会自动管理属性的值。当收到读取请求时,基库会直接返回当前存储的值;当收到写入请求时,基库会更新存储的值。
应用程序可以通过ZbZclAttrIntegerWrite()等函数来更新属性的值。这种方式的优点是简单易用,不需要编写额外的回调代码。
方式二:应用回调管理
如果需要在属性被读取或写入时执行特定的操作(例如直接读写硬件寄存器),可以为属性提供一个回调函数。例如:
enum ZclStatusCodeT on_time_cb(struct ZbZclClusterT *clusterPtr, struct ZbZclAttrCbInfoT *info)
{
uint8_t *data = info->attr_data;
switch(info->type) {
case ZCL_ATTR_CB_TYPE_READ:
/* 从硬件寄存器读取值 */
data[0] = on_time[0];
data[1] = on_time[1];
break;
case ZCL_ATTR_CB_TYPE_WRITE:
/* 写入值到硬件寄存器 */
on_time[0] = data[0];
on_time[1] = data[1];
break;
case ZCL_ATTR_CB_TYPE_NOTIFY:
/* 处理属性通知 */
on_time[0] = data[0];
on_time[1] = data[1];
break;
}
return ZCL_STATUS_SUCCESS;
}
在使用回调方式时,需要在属性定义中设置相应的标志位:
ZCL_ATTR_FLAG_CB_READ:读取属性时调用回调
ZCL_ATTR_FLAG_CB_WRITE:写入属性时调用回调
AN5491 建议开发者要么同时设置这两个标志并提供完整的回调函数,要么不设置任何标志并将回调设为 NULL。不建议只处理读取或只处理写入请求,因为这可能导致读取和写入的值不一致。
3.3 属性读取的端到端流程
下面以客户端读取服务器的 "开启时间" 属性为例,说明属性操作的完整流程:
- 客户端应用调用
ZbZclReadReq()函数,指定要读取的属性和回调函数
- ZCL 集群基库构建 ZCL 读取属性请求消息,并通过无线发送到服务器
- 服务器的 ZCL 集群基库收到请求后,检查属性是否存在
- 如果属性存在,根据属性的管理方式获取当前值(直接读取存储的值或调用回调函数)
- 服务器构建 ZCL 读取属性响应消息,并发送回客户端
- 客户端的 ZCL 集群基库收到响应后,调用应用提供的回调函数,将结果传递给应用
4. 指令处理:客户端请求与服务器响应
除了属性操作外,指令是 ZCL 集群的另一个重要组成部分。指令用于触发服务器执行特定的操作,例如打开灯光、设置温度等。
4.1 指令的基本结构
每个 ZCL 指令都有一个唯一的指令 ID 和特定的有效负载格式。例如,智能能源标准中的 "GetCalendar" 指令包含以下字段:
- 最早开始时间(UTC 时间)
- 最小颁发者事件 ID(32 位无符号整数)
- 日历数量(8 位无符号整数)
- 日历类型(8 位枚举)
- 提供者 ID(32 位无符号整数)
对应的响应指令 "PublishCalendar" 则包含更复杂的结构,包括提供者 ID、颁发者事件 ID、日历 ID、开始时间、日历类型、日历名称、季节数量等多个字段。
4.2 客户端指令生成与发送
在客户端,每个指令都有一个对应的请求函数,用于构建和发送指令。例如,"GetCalendar" 指令的请求函数是ZbZclCalClientCommandGetCalReq()。
客户端应用使用这个函数的步骤如下:
static void get_cal_cb(struct ZbZclCommandRspT *rsp, void *arg)
{
struct application *app = (struct application *)arg;
/* 处理响应 */
if(rsp->status == ZCL_STATUS_SUCCESS) {
/* 检查响应类型 */
if((rsp->hdr.frameCtrl.frameType & ZCL_FRAMECTRL_TYPE) == ZCL_FRAMETYPE_CLUSTER &&
rsp->hdr.cmdId == ZCL_CAL_SVR_PUBLISH_CALENDAR) {
/* 解析PublishCalendar响应 */
struct ZbZclCalServerPublishCalendarT cal;
ZbZclCalClientParsePublishCalendar(rsp->payload, rsp->length, &cal);
/* 处理日历数据 */
} else if(rsp->hdr.cmdId == ZCL_COMMAND_DEFAULT_RESPONSE) {
/* 解析默认响应 */
struct ZbZclDefaultResponseT def_rsp;
ZbZclParseDefaultResponse(rsp->payload, rsp->length, &def_rsp);
/* 处理错误状态 */
}
} else {
/* 处理传输错误 */
if(rsp->aps_status == ZB_APS_STATUS_NO_ACK) {
/* 服务器未确认 */
} else if(rsp->aps_status == ZB_WPAN_STATUS_NO_ACK) {
/* 网络连接问题 */
}
}
}
/* 发送GetCalendar请求 */
struct ZbZclCalClientGetCalendarT req;
memset(&req, 0, sizeof(req));
req.earliestStartTime = app->calendar_start;
req.minIssuerEventId = ZCL_INVALID_UNSIGNED_32BIT;
req.numCalendars = 1;
req.calendarType = 0x02;
req.providerId = app->provider_id;
status = ZbZclCalClientCommandGetCalReq(cluster, ZbApsAddrBinding, &req, get_cal_cb, app);
4.3 服务器端指令接收与处理
在服务器端,每个指令都有一个对应的回调函数。当服务器收到客户端的指令时,ZCL 集群基库会解析指令,并调用相应的回调函数。
服务器端处理 "GetCalendar" 指令的代码如下:
static enum ZclStatusCodeT get_calendar(struct ZbZclClusterT *clusterPtr, void *arg,
struct ZbZclCalClientGetCalendarT *req,
struct ZbZclAddrInfoT *srcInfo)
{
struct ZbZclCalServerPublishCalendarT rsp;
memset(&rsp, 0, sizeof(rsp));
/* 根据请求条件查找匹配的日历 */
/* 填充响应数据 */
rsp.providerId = req->providerId;
rsp.issuerEventId = 12345;
rsp.issuerCalendarId = 67890;
rsp.startTime = req->earliestStartTime;
rsp.calendarType = req->calendarType;
/* ... 填充其他字段 ... */
/* 发送PublishCalendar响应 */
ZbZclCalServerSendPublishCalendar(clusterPtr, srcInfo, &rsp);
/* 返回成功且不发送默认响应,因为我们已经发送了特定响应 */
return ZCL_STATUS_SUCCESS_NO_DEFAULT_RESPONSE;
}
/* 注册回调函数 */
struct ZbZclCalServerCallbacksT callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.get_calendar = get_calendar;
/* 创建服务器集群 */
cluster = ZbZclCalServerAlloc(zb, endpoint, &callbacks, arg);
4.4 响应的三种情况
客户端在发送指令后,可能会收到以下三种情况的响应:
- 成功收到特定响应:服务器成功处理了指令,并返回了指令特定的响应(如 PublishCalendar)。此时
rsp->status为ZCL_STATUS_SUCCESS,帧类型为ZCL_FRAMETYPE_CLUSTER,指令 ID 为预期的响应指令 ID。
- 收到默认响应:如果指令没有定义特定的响应,或者服务器处理指令时发生错误,服务器会返回默认响应。此时
rsp->status仍为ZCL_STATUS_SUCCESS,但指令 ID 为ZCL_COMMAND_DEFAULT_RESPONSE。默认响应中包含一个状态代码,指示指令处理的结果。
- 传输错误:如果请求无法发送到服务器,或者服务器没有响应,
rsp->status将为ZCL_STATUS_FAILURE。此时需要检查rsp->aps_status字段,了解具体的错误原因。常见的错误包括:
ZB_APS_STATUS_NO_ACK:服务器收到了请求但没有确认
ZB_WPAN_STATUS_NO_ACK:无法到达网络中的下一跳,通常表示网络连接问题
通过本文的解读,我们了解了在 STM32WB 系列上开发自定义 Zigbee 集群的基本原理和方法。AN5491 应用笔记为我们提供了一个清晰的指导,让我们能够按照与标准集群相同的方式构建自定义集群。
在开发自定义集群时,有以下几点建议:
- 优先使用标准集群:如果标准集群能够满足需求,尽量使用标准集群,这样可以确保设备的互操作性。只有在标准集群无法满足需求时,才考虑开发自定义集群。
- 遵循 ZCL 规范:在设计自定义集群时,尽量遵循 ZCL 规范的设计原则和命名约定,这样可以使代码更易于理解和维护。
- 合理选择属性管理方式:对于简单的状态属性,可以使用协议栈自动管理方式;对于需要直接与硬件交互的属性,使用回调管理方式。
- 处理所有可能的响应情况:在客户端代码中,一定要处理所有三种可能的响应情况,包括成功响应、默认响应和传输错误。
- 测试互操作性:开发完成后,务必进行充分的测试,特别是与其他 Zigbee 设备的互操作性测试。
STM32WB 系列结合 Exegin ZSDK 为 Zigbee 应用开发提供了强大的平台。通过掌握自定义集群的开发技术,我们可以开发出功能更丰富、更具竞争力的物联网产品。