基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

相关系列文章

基于芯科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

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

ESP32开发环境:Eclipse IDE for C/C++ Developers、ESP-IDF Tools、esp-idf v4.1

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

 

链接静态库

在项目根目录下的CMakeList.txt中增加静态库的引用,如下图所示:

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

# 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文件整体复制一份到项目中。

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

以上工作完成后,就可以进行代码级别上的适配。


因为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文件的存储分区,在项目根目录下,创建分区表文件,如下图所示:

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

针对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中启用自定义分区表;如下图所示

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

主要的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网络启动成功等信息。

基于芯科Host-NCP解决方案的Zigbee 3.0 Gateway技术研究(三)-移植到ESP32平台(2)

总结

  1. ESP32对freeRTOS支持比较好,相对移植工作较小;
  2. 生成的ESP32固件为1.2M左右,所以如果需要考虑支持host、ncp、终端的ota功能,最好flash有8M;
  3. 程序运行时,基本剩余RAM在150k左右,有点紧张;所以如果网关需要支持更多的功能,例如语音、视频等,采用ESP32-WORVER系列也许是个更好的选择;
  4. 芯科的SDK有大量基于定时器的event,而且都是ms级别的,所以对系统计时的准确性要求较高(后面移植stm32过程中,更加体会到这一点);
  5. ESP32在互联网操作方面,有较丰富的组件,如果网关应用是面向互联网的,又有一定的成本限制,那选择ESP32会是一个不错的方案。
上一篇:ESP32环境-每次都有新发现


下一篇:基于esp32的语音红外控制