XIAO ESP32C6 Zigbee 快速入门指南 (Arduino)
概述
本教程指导您在 Seeed Studio XIAO ESP32C6 开发板上实现 Zigbee 应用。该开发板由 ESP32-C6 芯片驱动,结合了 Wi-Fi、蓝牙低功耗 (BLE) 和 Zigbee 连接功能,使其非常适合 物联网应用。本指南中的示例使用 esp-arduino Zigbee SDK 来实现 Zigbee 功能。

您将学到什么
如果您还没有准备好 Arduino IDE,请参考 入门指南。确保 esp-arduino 板版本 为 v3.0.6 或更高版本,该版本支持 Zigbee 功能。
本指南专注于在 XIAO ESP32C6 上使用 Zigbee 的基本要点,确保清楚理解其实际应用:
- Zigbee 概述:了解 Zigbee 协议及其网络结构。
- Zigbee Arduino 示例:在 ESP32-C6 上实现 Zigbee 示例,如灯泡和开关。
Zigbee 概述
Zigbee 是一种基于 IEEE 802.15.4 标准的 低功耗、低带宽 无线通信协议。它专为 家庭自动化、智慧城市 和 工业控制 等物联网场景量身定制,提供强大的网状网络功能,在动态环境中实现可靠通信。
Zigbee 数据模型
Zigbee 通信依赖于 Zigbee 集群库 (ZCL),它定义了设备如何组织其功能并进行交互。关键组件包括:
-
设备类型 Zigbee 设备(例如开关、传感器、灯具)预定义了特定行为,按功能分组到 集群 中。
-
集群 集群是以下内容的逻辑分组:
- 属性:表示设备状态,如亮度或温度。
- 命令:触发动作,如打开灯或将亮度设置为 50%。
示例:
- 开/关集群:控制二进制状态如电源。
- 电平控制集群:调整强度或亮度。
- 温度测量集群:发送温度读数。
- 场景集群:保存和调用预设配置。
-
属性和命令 属性存储设备数据(例如状态、配置),而命令启动动作。

Zigbee 网络架构
Zigbee 网络由三种主要节点类型组成:
-
Zigbee 协调器 (ZC)
- 作为网络的中央枢纽。
- 处理网络创建、设备认证和地址分配。
- 负责初始化和管理网络。
- 每个 Zigbee 网络只能有 一个协调器。
-
Zigbee 路由器 (ZR)
- 通过在设备之间中继消息来扩展网络范围。
- 支持更多设备加入网络。
- 通常使用市电供电以确保持续运行和可靠的消息中继。
- 电池供电的路由器是可能的,但由于更高的能耗需求而不太常见。
-
Zigbee 终端设备 (ZED)
- 轻量级和节能设备,与父节点(协调器或路由器)通信。
- 不向其他设备路由消息。
- 针对电池操作进行优化,通常进入睡眠模式以节省能源。
-
寻址和路由:
- Zigbee 使用 16 位寻址方案。设备通过直接和间接寻址的混合方式进行通信。
- 路由决策由路由器使用 AODV(按需距离矢量)等算法做出。
-
电源管理:
- Zigbee 终端设备针对低功耗进行了优化。它们通常在睡眠模式下运行,只在需要时唤醒。
- 路由器和协调器通常使用市电供电以确保持续可用性。
网络拓扑
Zigbee 支持三种主要网络拓扑,具体取决于应用需求和环境:
1. 网状拓扑
-
单个协调器和多个路由器形成自愈、强健的网络。
-
如果通信路径中断,设备可以动态重新路由消息,确保高可靠性。
-
适用于需要广泛覆盖和冗余的大规模网络。
-
关键特性:
- 动态重新路由确保高可靠性。
- 支持具有可扩展覆盖范围的大型网络。
- 自愈机制增加容错能力。
2. 树形拓扑
-
协调器充当分层结构的根节点,路由器形成分支。
-
每个分支可以有多个终端设备或额外的路由器,创建树状结构。
-
通信依赖于分层路径,这引入了潜在的单点故障。
-
主要特点:
- 在结构化环境中运行良好。
- 比网状网络更容易设置和管理。
- 容易受到分支故障的影响,这可能会断开整个子网络。
3. 星型拓扑
-
所有设备直接与协调器通信。
-
部署简单,但协调器是单点故障。
-
最适合设备位于协调器附近的小型网络。
-
主要特点:
- 易于设置和管理。
- 由于范围和设备容量限制,可扩展性有限。
- 依赖协调器进行所有通信,降低了容错性。
在快速了解这些概念后,让我们开始在XIAO ESP32C6上进行Zigbee开发。
Arduino 示例
示例 1:灯泡和灯开关
首先,准备两个XIAO ESP32C6,一个作为Zigbee 灯泡,另一个作为Zigbee 灯开关。
使用 Zigbee_On_Off_Light
和 Zigbee_On_Off_Switch
示例来了解支持Zigbee的设备如何在实际场景中交互。准备好开始了吗?让我们深入开发!
Zigbee 灯泡
确保您为Zigbee模式选择了 Zigbee ED(end device)
。
一些常量:
#define LED_PIN LED_BUILTIN
#define BUTTON_PIN 9 // ESP32-C6/H2 Boot button
#define ZIGBEE_LIGHT_ENDPOINT 10
LED_PIN
用于控制内置LED。BUTTON_PIN
用于出厂重置按钮。ZIGBEE_LIGHT_ENDPOINT
代表灯泡的Zigbee端点,它在网络中充当服务标识符的作用。
定义Zigbee灯设备
ZigbeeLight zbLight = ZigbeeLight(ZIGBEE_LIGHT_ENDPOINT);
这行代码定义了一个带有端点ID的ZigbeeLight
对象。端点用于表示Zigbee设备内的不同功能。
设备状态控制函数
setLED()
函数控制LED状态:
void setLED(bool value) {
digitalWrite(LED_PIN, value);
}
setLED()
函数接受一个布尔值,并根据输入值相应地设置 LED 状态,基于输入值打开或关闭 LED。
setup()
函数
setup()
函数初始化设备,包括 LED、按钮和 Zigbee 设置。
void setup() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
首先,我们将LED引脚配置为输出并初始关闭。
pinMode(BUTTON_PIN, INPUT_PULLUP);
按钮引脚配置为带有内部上拉电阻的输入。
zbLight.setManufacturerAndModel("Espressif", "ZBLightBulb");
这设置了设备的制造商和型号名称,有助于在 Zigbee 网络上识别它。
zbLight.onLightChange(setLED);
这将 setLED()
注册为回调函数,每当灯光状态发生变化时就会调用该函数。
Zigbee.addEndpoint(&zbLight);
我们将 zbLight
作为端点添加到 Zigbee 核心。这允许其他 Zigbee 设备与此端点交互。
Zigbee.begin();
最后,我们调用 Zigbee.begin()
来初始化 Zigbee 协议栈并启动设备作为网络中的终端设备。
loop()
函数
主循环处理按钮按下以执行恢复出厂设置:
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
delay(100); // Key debounce handling
int startTime = millis();
while (digitalRead(BUTTON_PIN) == LOW) {
delay(50);
if ((millis() - startTime) > 3000) {
Serial.printf("Resetting Zigbee to factory settings, reboot.\n");
Zigbee.factoryReset();
}
}
}
delay(100);
}
此代码检查按钮是否被按下:
- 如果按下,它会等待 100 毫秒(用于防抖处理)。
- 如果按钮保持按下状态超过 3 秒,它会通过调用
Zigbee.factoryReset()
触发恢复出厂设置。
当用户因网络或配对问题需要重新配置设备时,此功能非常有用。
官方例程仍在持续更新中,我们的文档可能无法第一时间同步最新程序,如有差异,请以 Espressif 的程序示例 为准。
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif
#include "ZigbeeCore.h"
#include "ep/ZigbeeLight.h"
#define LED_PIN LED_BUILTIN
#define BUTTON_PIN 9 // ESP32-C6/H2 Boot button
#define ZIGBEE_LIGHT_ENDPOINT 10
ZigbeeLight zbLight = ZigbeeLight(ZIGBEE_LIGHT_ENDPOINT);
/********************* RGB LED functions **************************/
void setLED(bool value) {
digitalWrite(LED_PIN, value);
}
/********************* Arduino functions **************************/
void setup() {
// Init LED and turn it OFF (if LED_PIN == RGB_BUILTIN, the rgbLedWrite() will be used under the hood)
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Init button for factory reset
pinMode(BUTTON_PIN, INPUT_PULLUP);
//Optional: set Zigbee device name and model
zbLight.setManufacturerAndModel("Espressif", "ZBLightBulb");
// Set callback function for light change
zbLight.onLightChange(setLED);
//Add endpoint to Zigbee Core
log_d("Adding ZigbeeLight endpoint to Zigbee Core");
Zigbee.addEndpoint(&zbLight);
// When all EPs are registered, start Zigbee. By default acts as ZIGBEE_END_DEVICE
log_d("Calling Zigbee.begin()");
Zigbee.begin();
}
void loop() {
// Checking button for factory reset
if (digitalRead(BUTTON_PIN) == LOW) { // Push button pressed
// Key debounce handling
delay(100);
int startTime = millis();
while (digitalRead(BUTTON_PIN) == LOW) {
delay(50);
if ((millis() - startTime) > 3000) {
// If key pressed for more than 3secs, factory reset Zigbee and reboot
Serial.printf("Resetting Zigbee to factory settings, reboot.\n");
Zigbee.factoryReset();
}
}
}
delay(100);
}
Zigbee 灯光开关
在这里,XIAO ESP32C6 作为 Zigbee 协调器,负责控制其他 Zigbee 设备。这里,Zigbee 开关 代表控制器,它绑定到 Zigbee 灯光设备并通过命令控制它,例如切换灯光的开关状态。
包含文件和定义
#include "ZigbeeCore.h"
#include "ep/ZigbeeLight.h"
#define SWITCH_ENDPOINT_NUMBER 5
#define GPIO_INPUT_IO_TOGGLE_SWITCH 9
#define PAIR_SIZE(TYPE_STR_PAIR) (sizeof(TYPE_STR_PAIR) / sizeof(TYPE_STR_PAIR[0]))
SWITCH_ENDPOINT_NUMBER
定义为5
。它表示开关的端点。就像在灯泡示例中一样,端点号用于定义 Zigbee 设备内的特定功能。GPIO_INPUT_IO_TOGGLE_SWITCH
指向 GPIO 引脚9
,它作为开关按钮。PAIR_SIZE()
是一个用于计算给定数组大小的宏,这里用于处理按钮配置。
开关配置类型和函数
代码定义了几个与开关功能相关的枚举和数据结构:
typedef enum {
SWITCH_ON_CONTROL,
SWITCH_OFF_CONTROL,
SWITCH_ONOFF_TOGGLE_CONTROL,
SWITCH_LEVEL_UP_CONTROL,
SWITCH_LEVEL_DOWN_CONTROL,
SWITCH_LEVEL_CYCLE_CONTROL,
SWITCH_COLOR_CONTROL,
} SwitchFunction;
typedef struct {
uint8_t pin;
SwitchFunction func;
} SwitchData;
typedef enum {
SWITCH_IDLE,
SWITCH_PRESS_ARMED,
SWITCH_PRESS_DETECTED,
SWITCH_PRESSED,
SWITCH_RELEASE_DETECTED,
} SwitchState;
SwitchFunction
枚举了开关可以执行的不同功能,例如打开灯光、关闭、切换、调节亮度等。SwitchData
是一个将GPIO引脚与特定功能配对的结构体,这允许在添加具有不同功能的多个按钮时更好地组织。SwitchState
表示用户交互期间开关的不同状态(例如,空闲、按下、释放)。
实例化Zigbee开关
static SwitchData buttonFunctionPair[] = {{GPIO_INPUT_IO_TOGGLE_SWITCH, SWITCH_ONOFF_TOGGLE_CONTROL}};
ZigbeeSwitch zbSwitch = ZigbeeSwitch(SWITCH_ENDPOINT_NUMBER);
buttonFunctionPair
是一个定义按钮功能的数组。这里,连接到GPIO 9
的按钮将用于切换灯的开关状态。zbSwitch
创建一个ZigbeeSwitch
实例,端点号为5
。
Zigbee 功能和 GPIO 中断处理
static void onZbButton(SwitchData *button_func_pair) {
if (button_func_pair->func == SWITCH_ONOFF_TOGGLE_CONTROL) {
zbSwitch.lightToggle(); // 向灯发送切换命令。
}
}
onZbButton()
在按下按钮时被调用。在这种情况下,它发送一个 Zigbee 命令来切换灯的状态。
处理 GPIO 事件
static void IRAM_ATTR onGpioInterrupt(void *arg) {
xQueueSendFromISR(gpio_evt_queue, (SwitchData *)arg, NULL);
}
onGpioInterrupt()
是处理 GPIO 引脚中断的中断服务例程 (ISR)。每当按钮被按下时,它会将一个事件放入队列中。
static void enableGpioInterrupt(bool enabled) {
for (int i = 0; i < PAIR_SIZE(buttonFunctionPair); ++i) {
if (enabled) {
enableInterrupt((buttonFunctionPair[i]).pin);
} else {
disableInterrupt((buttonFunctionPair[i]).pin);
}
}
}
enableGpioInterrupt()
根据参数 enabled
是 true
还是 false
来启用或禁用 GPIO 中断。
设置函数
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);
}
zbSwitch.setManufacturerAndModel("Espressif", "ZigbeeSwitch");
zbSwitch.allowMultipleBinding(true);
Zigbee.addEndpoint(&zbSwitch);
Zigbee.setRebootOpenNetwork(180);
for (int i = 0; i < PAIR_SIZE(buttonFunctionPair); i++) {
pinMode(buttonFunctionPair[i].pin, INPUT_PULLUP);
gpio_evt_queue = xQueueCreate(10, sizeof(SwitchData));
if (gpio_evt_queue == 0) {
log_e("Queue was not created and must not be used");
while (1);
}
attachInterruptArg(buttonFunctionPair[i].pin, onGpioInterrupt, (void *)(buttonFunctionPair + i), FALLING);
}
Zigbee.begin(ZIGBEE_COORDINATOR);
Serial.println("Waiting for Light to bound to the switch");
while (!zbSwitch.isBound()) {
Serial.printf(".");
delay(500);
}
std::list<zb_device_params_t *> boundLights = zbSwitch.getBoundDevices();
for (const auto &device : boundLights) {
Serial.printf("Device on endpoint %d, short address: 0x%x\n", device->endpoint, device->short_addr);
Serial.printf(
"IEEE Address: %02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X\n", device->ieee_addr[0], device->ieee_addr[1], device->ieee_addr[2], device->ieee_addr[3],
device->ieee_addr[4], device->ieee_addr[5], device->ieee_addr[6], device->ieee_addr[7]
);
Serial.printf("Light manufacturer: %s", zbSwitch.readManufacturer(device->endpoint, device->short_addr));
Serial.printf("Light model: %s", zbSwitch.readModel(device->endpoint, device->short_addr));
}
Serial.println();
}
- 串行通信初始化:初始化串行通信用于调试。
- 设备信息:设置制造商和型号,允许多个设备绑定,并向Zigbee核心添加端点。
- 网络初始化:重启后打开Zigbee网络
180
秒,允许设备加入。 - 按钮初始化:为按钮设置GPIO引脚,创建队列来处理GPIO中断,并为按钮附加中断。
- 等待绑定:协调器等待直到绑定到灯设备后才继续。绑定后,它会打印绑定的设备信息。
循环函数
void loop() {
uint8_t pin = 0;
SwitchData buttonSwitch;
static SwitchState buttonState = SWITCH_IDLE;
bool eventFlag = false;
if (xQueueReceive(gpio_evt_queue, &buttonSwitch, portMAX_DELAY)) {
pin = buttonSwitch.pin;
enableGpioInterrupt(false);
eventFlag = true;
}
while (eventFlag) {
bool value = digitalRead(pin);
switch (buttonState) {
case SWITCH_IDLE: buttonState = (value == LOW) ? SWITCH_PRESS_DETECTED : SWITCH_IDLE; break;
case SWITCH_PRESS_DETECTED: buttonState = (value == LOW) ? SWITCH_PRESS_DETECTED : SWITCH_RELEASE_DETECTED; break;
case SWITCH_RELEASE_DETECTED:
buttonState = SWITCH_IDLE;
(*onZbButton)(&buttonSwitch);
break;
default: break;
}
if (buttonState == SWITCH_IDLE) {
enableGpioInterrupt(true);
eventFlag = false;
break;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 10000) {
lastPrint = millis();
zbSwitch.printBoundDevices();
}
}
- loop 函数通过从中断队列(
gpio_evt_queue
)读取来管理按钮按压,并相应地更新buttonState
。 - 当按钮被按下并释放时(
SWITCH_RELEASE_DETECTED
),调用onZbButton()
回调函数来切换灯的状态。 - 每 10 秒,打印绑定的灯以进行监控。
官方例程仍在持续更新中,我们的文档可能无法第一时间同步最新程序,如有差异,请以 Espressif 的程序示例 为准。
#ifndef ZIGBEE_MODE_ZCZR
#error "Zigbee 协调器模式未在 Tools->Zigbee mode 中选择"
#endif
#include "ZigbeeCore.h"
#include "ep/ZigbeeLight.h"
#define SWITCH_ENDPOINT_NUMBER 5
/* 开关配置 */
#define GPIO_INPUT_IO_TOGGLE_SWITCH 9
#define PAIR_SIZE(TYPE_STR_PAIR) (sizeof(TYPE_STR_PAIR) / sizeof(TYPE_STR_PAIR[0]))
typedef enum {
SWITCH_ON_CONTROL,
SWITCH_OFF_CONTROL,
SWITCH_ONOFF_TOGGLE_CONTROL,
SWITCH_LEVEL_UP_CONTROL,
SWITCH_LEVEL_DOWN_CONTROL,
SWITCH_LEVEL_CYCLE_CONTROL,
SWITCH_COLOR_CONTROL,
} SwitchFunction;
typedef struct {
uint8_t pin;
SwitchFunction func;
} SwitchData;
typedef enum {
SWITCH_IDLE,
SWITCH_PRESS_ARMED,
SWITCH_PRESS_DETECTED,
SWITCH_PRESSED,
SWITCH_RELEASE_DETECTED,
} SwitchState;
static SwitchData buttonFunctionPair[] = {{GPIO_INPUT_IO_TOGGLE_SWITCH, SWITCH_ONOFF_TOGGLE_CONTROL}};
ZigbeeSwitch zbSwitch = ZigbeeSwitch(SWITCH_ENDPOINT_NUMBER);
/********************* Zigbee 函数 **************************/
static void onZbButton(SwitchData *button_func_pair) {
if (button_func_pair->func == SWITCH_ONOFF_TOGGLE_CONTROL) {
// 向灯发送切换命令
zbSwitch.lightToggle();
}
}
/********************* GPIO 函数 **************************/
static QueueHandle_t gpio_evt_queue = NULL;
static void IRAM_ATTR onGpioInterrupt(void *arg) {
xQueueSendFromISR(gpio_evt_queue, (SwitchData *)arg, NULL);
}
static void enableGpioInterrupt(bool enabled) {
for (int i = 0; i < PAIR_SIZE(buttonFunctionPair); ++i) {
if (enabled) {
enableInterrupt((buttonFunctionPair[i]).pin);
} else {
disableInterrupt((buttonFunctionPair[i]).pin);
}
}
}
/********************* Arduino 函数 **************************/
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);
}
//可选:设置 Zigbee 设备名称和型号
zbSwitch.setManufacturerAndModel("Espressif", "ZigbeeSwitch");
//可选:允许多个灯绑定到开关
zbSwitch.allowMultipleBinding(true);
//将端点添加到 Zigbee 核心
log_d("将 ZigbeeSwitch 端点添加到 Zigbee 核心");
Zigbee.addEndpoint(&zbSwitch);
//启动后开放网络 180 秒
Zigbee.setRebootOpenNetwork(180);
// 初始化按钮开关
for (int i = 0; i < PAIR_SIZE(buttonFunctionPair); i++) {
pinMode(buttonFunctionPair[i].pin, INPUT_PULLUP);
/* 创建队列来处理来自 isr 的 gpio 事件 */
gpio_evt_queue = xQueueCreate(10, sizeof(SwitchData));
if (gpio_evt_queue == 0) {
log_e("队列未创建,不能使用");
while (1);
}
attachInterruptArg(buttonFunctionPair[i].pin, onGpioInterrupt, (void *)(buttonFunctionPair + i), FALLING);
}
// 当所有 EP 注册完成后,以 ZIGBEE_COORDINATOR 模式启动 Zigbee
log_d("调用 Zigbee.begin()");
Zigbee.begin(ZIGBEE_COORDINATOR);
Serial.println("等待灯绑定到开关");
//等待开关绑定到灯:
while (!zbSwitch.isBound()) {
Serial.printf(".");
delay(500);
}
// 可选:从绑定的灯读取制造商和型号名称
std::list<zb_device_params_t *> boundLights = zbSwitch.getBoundDevices();
//列出所有绑定的灯
for (const auto &device : boundLights) {
Serial.printf("端点 %d 上的设备,短地址:0x%x\n", device->endpoint, device->short_addr);
Serial.printf(
"IEEE 地址:%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X\n", device->ieee_addr[0], device->ieee_addr[1], device->ieee_addr[2], device->ieee_addr[3],
device->ieee_addr[4], device->ieee_addr[5], device->ieee_addr[6], device->ieee_addr[7]
);
Serial.printf("灯制造商:%s", zbSwitch.readManufacturer(device->endpoint, device->short_addr));
Serial.printf("灯型号:%s", zbSwitch.readModel(device->endpoint, device->short_addr));
}
Serial.println();
}
void loop() {
// 在 loop() 中处理按钮开关
uint8_t pin = 0;
SwitchData buttonSwitch;
static SwitchState buttonState = SWITCH_IDLE;
bool eventFlag = false;
/* 检查是否有队列接收,如果有则读出 buttonSwitch */
if (xQueueReceive(gpio_evt_queue, &buttonSwitch, portMAX_DELAY)) {
pin = buttonSwitch.pin;
enableGpioInterrupt(false);
eventFlag = true;
}
while (eventFlag) {
bool value = digitalRead(pin);
switch (buttonState) {
case SWITCH_IDLE: buttonState = (value == LOW) ? SWITCH_PRESS_DETECTED : SWITCH_IDLE; break;
case SWITCH_PRESS_DETECTED: buttonState = (value == LOW) ? SWITCH_PRESS_DETECTED : SWITCH_RELEASE_DETECTED; break;
case SWITCH_RELEASE_DETECTED:
buttonState = SWITCH_IDLE;
/* 回调到 button_handler */
(*onZbButton)(&buttonSwitch);
break;
default: break;
}
if (buttonState == SWITCH_IDLE) {
enableGpioInterrupt(true);
eventFlag = false;
break;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
// 每 10 秒打印绑定的灯
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 10000) {
lastPrint = millis();
zbSwitch.printBoundDevices();
}
}
演示
恭喜您成功完成了 Zigbee 控制照明项目!还有许多令人兴奋的 Zigbee 应用等待您去探索。继续保持出色的工作!
参考资料
技术支持与产品讨论
感谢您选择我们的产品!我们在这里为您提供不同的支持,以确保您使用我们产品的体验尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。