相关系列文章
基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(-)-Z3GatewayHost应用搭建
基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(二)-使用gateway-management-ui
基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(1)
基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)
概要
前面一章节已经介绍了如何为ESP32生成静态库的方法,这章记录一下使用Z3GatewayHost静态库的方法和注意事项。
参考文档
感谢相关文章作者的分享,在此借以引用参考,如有涉及版权问题,请及时联系我
软硬件环境
ESP32-EFR32开发板一套
Host:ESP32-WROOM-32D Flash:8MB SRAM :536KB
NCP:EFR32MG21A020F768IM32
ESP32开发环境:Eclipse IDE for C/C++ Developers、ESP-IDF Tools、esp-idf v4.1
链接静态库
在项目根目录下的CMakeList.txt中增加静态库的引用,如下图所示:
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
# (Not part of the boilerplate)
# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# 引入静态库
link_libraries("D:/Users/Administrator/eclipse-workspace/hello-world/lib/Z3GatewayHost_ESP32.a")
project(blink)
备注:目前静态库使用了绝对路径,后面应该调整为相对路径方式。
引入静态库后,根据自己应用的需要引入芯科SDK相应的头文件.h,主要的有af、ash、ezsp、hal、stack这些;都是在芯科SDK中将对应的.h文件整体复制一份到项目中。
以上工作完成后,就可以进行代码级别上的适配。
因为ESP32平台主要是基于freeRTOS的,所以在网络应用、文件应用方面的api都与linux比较接近,也支持多任务的方式;将host应用移植到ESP32平台的工作和难度都相对较小。
在移植到ESP32的时候,主要的工作集中在以下几个方面:
- 串口功能适配
- mqtt功能适配
- 文件功能适配
串口功能适配
esp32中有3个串口资源,分别是UART_NUM_0、UART_NUM_1、UART_NUM_2;这里将串口0留作日志打印、命令输入;将UART_NUM_2用作host-ncp传输。
关键代码如下
//uart2配置
#define ECHO_TEST_TXD (GPIO_NUM_16)
#define ECHO_TEST_RXD (GPIO_NUM_17)
#define BUF_SIZE (1024)
void uart2init()
{
// uart2初始化
uart_config_t uart_config = { .baud_rate = 115200, .data_bits =
UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits =
UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB, };
uart_param_config(UART_NUM_2, &uart_config);
//IO映射
uart_set_pin(UART_NUM_2, ECHO_TEST_TXD, ECHO_TEST_RXD, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_2, BUF_SIZE * 2, 0, 0, NULL, 0);
}
// 串口写入
int esp32_uart_SerialWriteByte(const char* src, size_t size)
{
return uart_write_bytes(UART_NUM_2, src,size);
}
// 串口读取
int16_t esp32_uart_SerialReadByte(uint8_t* buf, uint16_t length)
{
return uart_read_bytes(UART_NUM_2, buf, length,20 / portTICK_RATE_MS);
}
备注:esp32_uart_SerialWriteByte、esp32_uart_SerialReadByte为适配方法(前一篇blog中对应的extern方法)
mqtt功能适配
因为ESP32有自己的mqtt实现方式,所以这里采用ESP32的mqtt模块,具体是重写、实现transport-mqtt.h里面的相关方法。
备注:芯科SDK中的mqtt实现是采用paho.mqtt.c模块,具体在SDK的“util\third_party\paho.mqtt.c”目录下。
在transport-mqtt.h只定义了两个方法:
/** @brief MQTT Subscribe
*
* This function should be called to subscribe to a specific topic. If the
* publish succeeds then true should be returned, if the publish fails or
* there is no connection to a broker false should be returned.
*
* @param topic String contains the topic for a message subscription
*/
bool emberAfPluginTransportMqttSubscribe(const char* topic);
/** @brief MQTT Publish
*
* This function should be called to publish to a specific topic. If the publish
* succeeds then true should be returned, if the publish fails or there is no
* connection to a broker false should be returned.
*
* @param topic String contains the topic for the message to be published
* @param content String contains the payload for the message to be published
*/
bool emberAfPluginTransportMqttPublish(const char* topic, const char* paylaod);
很明显,就是实现mqtt主题的订阅与发布
因此,在ESP32中,实现相关的功能,代码如下:
bool emberAfPluginTransportMqttPublish(const char *topic, const char *paylaod) {
if (mqtt_connected == true) {
int msg_id;
msg_id = esp_mqtt_client_publish(client, topic, paylaod, 0, 1, 0);
} else {
return false;
}
return true;
}
bool emberAfPluginTransportMqttSubscribe(const char *topic) {
if (mqtt_connected == true) {
int msg_id;
// 订阅主题
msg_id = esp_mqtt_client_subscribe(client,topic, 2);
return true;
}else{
return false;
}
}
备注:ESP32的mqtt的事件处理方法中,需要处理mqtt连接成功事件、断开连接事件、数据到达等事件,如下所示:
static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) {
//esp_mqtt_client_handle_t client = event->client;
// your_context_t *context = event->context;
switch (event->event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG_MQTT, "MQTT_EVENT_CONNECTED");
mqtt_connected = true;
// mqtt初始化完成,调用callback
emberAfPluginTransportMqttInitCallback();
// mqtt成功连接后,开启对应的定时器事件
// 启动心跳事件HeartbeatEvent(5000ms一次) @#define DEFAULT_HEARTBEAT_RATE_MS 5000 // milliseconds
// 启动事件轮询事件ProcessCommandEvent(20ms一次) @#define PROCESS_COMMAND_RATE_MS 20 // milliseconds
// 心跳事件定时器
emberEventControlSetActive(
emberAfPluginGatewayRelayMqttHeartbeatEventControl);
// 命令事件定时器
emberEventControlSetActive(
emberAfPluginGatewayRelayMqttProcessCommandEventControl);
// 状态更新事件定时器
emberEventControlSetActive(
emberAfPluginGatewayRelayMqttStateUpdateEventControl);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG_MQTT, "MQTT_EVENT_DISCONNECTED");
mqtt_connected = false;
// mqtt断链后取消心跳事件HeartbeatEvent、事件轮询事件ProcessCommandEvent
emberEventControlSetInactive(
emberAfPluginGatewayRelayMqttHeartbeatEventControl);
emberEventControlSetInactive(
emberAfPluginGatewayRelayMqttProcessCommandEventControl);
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG_MQTT, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG_MQTT, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
break;
case MQTT_EVENT_DATA:
// 打印接收到的数据
//ESP_LOGI(TAG_MQTT, "MQTT_EVENT_DATA");
//printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
//printf("DATA=%.*s\r\n", event->data_len, event->data);
// 调用host的callback方法处理mqtt数据
emberAfPluginTransportMqttMessageArrivedCallback(event->topic,
event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG_MQTT, "MQTT_EVENT_ERROR");
mqtt_connected = false;
break;
default:
ESP_LOGI(TAG_MQTT, "Other event id:%d", event->event_id);
break;
}
return ESP_OK;
}
备注:SDK中关于host的mqtt部分,重要的文件有gateway-relay-mqtt、gateway-relay-mqtt-command、transport-mqtt
host订阅的主题有:(具体调用,可查看gateway-relay-mqtt.c中emberAfPluginTransportMqttStateChangedCallback方法)
gw/+gatewayEui/commands
gw/+gatewayEui/publishstate
gw/+gatewayEui/updatesettings
host中对应的处理方法入口分别为:handleCommandsMessage、handlePublishStateMessage、handleUpdateSettingsMessage
host 发布的主题有:(具体数据格式,可查看gateway-management-ui\nodeserver\src\common\routes\GatewayInterface.js)
gw/+gatewayEui/devicejoined
gw/+gatewayEui/deviceleft
gw/+gatewayEui/devices
gw/+gatewayEui/relays
gw/+gatewayEui/settings
gw/+gatewayEui/zclresponse
gw/+gatewayEui/devicestatechange
gw/+gatewayEui/otaevent
gw/+gatewayEui/heartbeat
gw/+gatewayEui/executed
文件功能适配
调整ESP32的分区文件,增加ota文件的存储分区,在项目根目录下,创建分区表文件,如下图所示:
针对8M flash的分区可以如下
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 2M,
#ota_0, app, spiffs, , 0x180000,
#ota_1, app, spiffs, , 0x180000,
ota_dev, data, spiffs, , 0xF0000,
预留了980KB的存储空间,用于放终端的ota文件。
在sdkconfig中启用自定义分区表;如下图所示
主要的ota spiffs分区使用代码如下:
// 终端ota文件保存分区配置
esp_vfs_spiffs_conf_t spiffs_ota_conf = { .base_path = "/ota-files",
.partition_label = "ota_dev", .max_files = 5, .format_if_mount_failed =
true };
void initOtaMgr(void) {
// 已经加载spiffs分区
if(spiffs_ota_mounted)
{
return;
}
esp_err_t ret = init_spiffs_ota();
// 初始化spiffs
if (ret != ESP_OK) {
spiffs_ota_mounted = false;
ESP_LOGE(TAG, "*************加载ota_dev分区失败**********");
} else {
spiffs_ota_mounted = true;
ESP_LOGI(TAG, "*************加载ota_dev分区成功*********");
}
}
void downloadOtaFile(char *filePath) {
if (!spiffs_ota_mounted) {
ESP_LOGE(TAG,"=====================ota_dev分区未加载,退出下载================");
return;
}
ESP_LOGI(TAG, "=====================终端ota文件下载开始================");
http_get_ota_file_task(filePath);
ESP_LOGI(TAG, "=====================终端ota文件下载结束================");
}
// 调用esp32初始化ota spiffs
static esp_err_t init_spiffs_ota(void) {
ESP_LOGI(TAG, "Initializing SPIFFS");
// Use settings defined above to initialize and mount SPIFFS filesystem.
// Note: esp_vfs_spiffs_register is an all-in-one convenience function.
esp_err_t ret = esp_vfs_spiffs_register(&spiffs_ota_conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)",
esp_err_to_name(ret));
}
return ret;
}
ESP_LOGI(TAG, "initialize SPIFFS info:\r\n");
printOTAPartionInfo();
return ret;
}
备注:http_get_ota_file_task为通过http方式到外网文件服务器下载ota文件的任务进程,由主任务进行调度。
ESP32任务调度
至此,ESP32适配host的应用代码基本完成,接下来就是在自己的应用中调用host的相关api,完成网关的业务功能。
基本可以参考SDK中的“af-main-host.c”中的emberAfMain方法实现
void zgeAfMain()
{
printf("Reset info: %d (%d) \r\n",
halGetResetInfo(),
halGetResetString());
// emberAfCoreFlush();
// This will initialize the stack of networks maintained by the framework,
// including setting the default network.
emAfInitializeNetworkIndexStack();
// We must initialize the endpoint information first so
// that they are correctly added by emAfResetAndInitNCP()
emberAfEndpointConfigure();
// initialize the network co-processor (NCP)
emAfResetAndInitNCP();
// initialize the ZCL framework
emAfInit();
// COMMAND_READER_INIT();
// main loop
while (true) {
//halResetWatchdog(); // Periodically reset the watchdog.
// see if the NCP has anything waiting to send us
ezspTick();
while (ezspCallbackPending()) {
ezspCallback();
}
// check if we have hit an EZSP Error and need to reset and init the NCP
// if (ncpNeedsResetAndInit) {
// ncpNeedsResetAndInit = false;
// // re-initialize the NCP
// emAfResetAndInitNCP();
// }
// Wait until ECC operations are done. Don't allow any of the clusters
// to send messages as the NCP is busy doing ECC
// if (emAfIsCryptoOperationInProgress()) {
// continue;
// }
// let the ZCL Utils run - this should go after ezspTick
emAfTick();
//emberSerialBufferTick();
emberAfRunEvents();
// 执行cmd命令
zgwCliCommand();
// After each interation through the main loop, our network index stack
// should be empty and we should be on the default network index again.
emAfAssertNetworkIndexStackIsEmpty();
}
}
备注:裁剪了原来部分代码;zgwCliCommand为实现接收ESP32串口0输入的命令方法。
在ESP32的主任务中,完成硬件初始化、网络初始化后,分别启动了几个TASK
- zigbee网关TASK
- 串口0(cli命令行)TASK
- 按键检测TASK
- MQTT TASK
其中zigbee网关TASK应该分配较大的堆栈深度资源和较高的优先级。
void app_main(void) {
esp_log_level_set("*", ESP_LOG_VERBOSE);
ESP_LOGE(TAG, "app start: %d ", zgwVersion);
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
// 初始化硬件
halInit();
// oled显示屏初始化
HalLcdInit();
vTaskDelay(200 / portTICK_PERIOD_MS);
vTaskDelay(1000 / portTICK_PERIOD_MS);
HalLcdWriteString("Network Init...", HAL_LCD_LINE_2);
#ifdef SUPPORT_WIFI
// 连接网络
netInit();
#else
wifiInit = true;
#endif
vTaskDelay(2000 / portTICK_PERIOD_MS);
if (wifiInit == true) {
HalLcdWriteString("Net connect succ", HAL_LCD_LINE_2);
ESP_LOGE(TAG,
"====================start zgwgateway========================= ");
// 启动网关
xTaskCreate(zgwWork, "zgwWork", 16 * 1024, NULL, 5, NULL);
xTaskCreate(uart0RevWork, "uart0Work", 4 * 1024, NULL, 8, NULL);
xTaskCreate(scanKey, "scanKey", 1 * 1024, NULL, 10, NULL);
vTaskDelay(2000 / portTICK_PERIOD_MS);
#ifdef SUPPORT_WIFI
// 启动mqtt
zgwMqttInit();
vTaskDelay(2000 / portTICK_PERIOD_MS);
#endif
} else {
ESP_LOGE(TAG,
"====================wifi connect fail,app quit========================= ");
HalLcdWriteString("Net connect fail", HAL_LCD_LINE_2);
HalLcdWriteString("Error:0x01", HAL_LCD_LINE_3);
}
}
然后进行项目的编译,将代码烧录到ESP32-EFR32开发板中,打开串口助手,即可看到相关的日志输出,显示网络链接成功、mqtt链接成功、zigbee网络启动成功等信息。
总结
- ESP32对freeRTOS支持比较好,相对移植工作较小;
- 生成的ESP32固件为1.2M左右,所以如果需要考虑支持host、ncp、终端的ota功能,最好flash有8M;
- 程序运行时,基本剩余RAM在150k左右,有点紧张;所以如果网关需要支持更多的功能,例如语音、视频等,采用ESP32-WORVER系列也许是个更好的选择;
- 芯科的SDK有大量基于定时器的event,而且都是ms级别的,所以对系统计时的准确性要求较高(后面移植stm32过程中,更加体会到这一点);
- ESP32在互联网操作方面,有较丰富的组件,如果网关应用是面向互联网的,又有一定的成本限制,那选择ESP32会是一个不错的方案。