目录
1 ESP NOW
1.1 ESP NOW简介
1.2 ESP NOW帧格式
1.3 ESP NOW安全性
1.4 ESP NOW初始化与反初始化
1.5 添加配对设备
1.6 发送ESP-NOW数据
1.7 接收ESP-NOW数据
1.8 ESP-NOW API参考
2 ESP-MDF对ESP-NOW的应用
3 MDF- ESPNOW
3.1 MDF-ESPNOW数据包类型
3.2 MDF-ESPNOW数据包结构
3.3 MDF-ESPNOW API
3.4 mdf_espnow_init
3.5 mdf_espnow_write
3.6 mdf_espnow_read
3.7 其它接口
乐鑫官方网址:https://esp-idf.readthedocs.io/en/latest/api-reference/wifi/esp_now.html
译文如下:
ESP-NOW是由乐鑫定义的一种无连接WiFi通讯协议。在ESP-NOW协议下,应用数据都是被封装在”vendor-specific action”(VSA)帧且在设备间的传输无需连接。CCMP协议用于保护VSA帧的安全性。ESP-NOW协议广泛应用与智能灯具,远程控制,传感器等。
ESP-NOW使用VSA帧传输数据,VSA帧格式如下:
Category code:类别码,设定为127以标识该帧为VSA帧;
Organization Indentifier:组织认证码,该部分是乐鑫设定的0x18fe34;
Vendor Specific Content:帧数据区,其格式如下:
Element ID:设置为221;
Length:Organization Identifier, Type, Version and Body三部分的长度之和;
Organization Indentifier:组织认证码,该部分是乐鑫设定的0x18fe34;
Type:设置为4,代表ESP-NOW协议;
Version:ESP-NOW协议版本;
Body:ESP-NOW数据区
ESP-NOW的VSA帧的MAC头部分与常规VSA帧有不同:
MAC头的FrameControl部分的FromDS位于ToDS位均被设置为0;
MAC头的第一个地址区设置为目的地址;第一个地址区设置为源地址;第三个地址区为广播地址0xFF: 0xFF: 0xFF: 0xFF: 0xFF: 0xFF
ESP-NOW使用CCMP方法加密VSA帧。CCMP可以参见IEEE Std. 802.11-2012标准。
WiFi设备持有一个PMK(Primary Master Key)与数个LMK(Local Master Key),这些Key的长度均为16位。PWK用于通过AES-128算法加密LMK,调用接口esp_now_set_pmk()设置。若PWM未被用户设置,会采用默认的PWK。LMK被设置后用来加密VSA帧。最多支持6个LMK,且不支持加密组播的VSA帧。
调用接口esp_now_init()初始化ESP-NOW;调用接口esp_now_deinit()反初始化;
建议在启动WiFi后初始化ESP-NOW,在停止WiFi前反初始化ESP-NOW协议栈;一旦
esp_now_deinit被调用,所有相关的配对设备信息将被删除。
在向某设备发送数据之前,首先需要调用接口esp_now_add_peer()将这个设备添加到配对设备列表。配对设备列表最大支持20个设备。若使能安全性传输,LMK必须被设置。ESP-NOW数据可以从STA模式接口或者softAP模式接口发送,所以发送数据前需要保证这些接口已被使能。发送广播数据前,必须添加广播的MAC地址(到配对设备列表?)。配对设备的通信信道范围是0-14,若信道被设置为0,则数据会被在当前信道发送;否则,(收发设备的)通信信道必须设置一致。
调用esp_now_send()发送数据;调用esp_now_register_send_cb注册发送回调函数。若发送的数据在MAC层被成功接收,则发送回调函数会返回ESP_NOW_SEND_SUCCESS;否则会返回ESP_NOW_SEND_FAIL。
发送ESP-NOW数据失败有多种可能,例如:目标端设备不存在;收发设备信道不相同,VSA帧传输中丢失等等。因此无法保证应用层接收到数据。如果需要的话,在接收到ESP-NOW数据时返回ACK数据用以确认;若接收ACK数据超时,则重发本次数据。也可以在ESP-NOW数据中加入序列号用于丢弃重复的数据。
若有大量数据需要发送,则调用esp_now_send()接口每次发送不多于250个字节。需要注意的是,两次发送之间的间隔过短可能会导致发送回调函数的错序。因此,推荐在上一次发送数据的回调函数返回后再进行本次发送操作。发送回调函数被一个高优先级的WiFi任务调用,因此不要在发送回调函数内进行过多的操作占用系统;最好是将必要的数据放入队列中并在低优先级的任务内进行相应的处理。
调用接口esp_now_register_recv_cb注册ESP-NOW数据接收回调函数。与发送回调函数相同,接收回调函数也运行在高优先级的WiFi任务内,因此不要在接收回调函数内做过多操作,并且最好将数据传入队列在低优先级的任务内再进行处理。
ESP-NOW在ESP-IDF(ESP开发架构)下是以库的形式提供的,其头文件路径:
\esp-idf\components\esp32\include\esp_now.h
重要API罗列如下:
<1> esp_now_init/esp_now_deinit:初始化与反初始化ESP-NOW协议栈
<2> esp_now_register_recv_cb/esp_now_unregister_recv_cb:注册与解除接收回调函数
<3> esp_now_register_send_cb/esp_now_unregister_send_cb:注册与解除发送回调函数
<4> esp_now_add_peer/esp_now_del_peer/esp_now_mod_peer/esp_now_get_peer/
esp_now_fetch_peer/esp_now_is_peer_exist/esp_now_get_peer_num:向配对列表添加/删除/设备/调整配对列表/从配对列表获取MAC匹配的设备/从配对列表(遍历)获取一个设备/检测配对设备是否存在/获取配对列表内的设备个数
重要的数据结构:
其含义已经非常清晰了:peer_addr即是设备的MAC地址;wifi_interface_t代表设备使用softAP接口/STA接口/ETH接口收发数据。
ESP-MDF是ESP-Mesh Development Framework的缩写,也就是“ESP Mesh开发架构”,其结构如下:
ESP-MDF下对ESP-NOW的基本功能(本文第1章)进行了扩展应用:
<1> 红框部分是ESP-MDF对ESP-NOW的API进行的封装,其代码路径位于:
\esp-mdf\components\protocol_stacks\mdf_espnow\mdf_espnow.c
\esp-mdf\components\protocol_stacks\mdf_espnow\include\mdf_espnow.h
该部分称为MDF-ESPNOW
<2> 紫框部分是对mdf_espnow的应用,主要用于ESP WiFi Mesh的调试与配网,其代码路径位于:
\esp-mdf\components\functions\mdf_debug\mdf_espnow_debug.c
\esp-mdf\components\functions\mdf_debug\include\mdf_espnow_debug.h
\esp-mdf\components\functions\mdf_network_config\mdf_network_config.c
\esp-mdf\components\functions\mdf_network_config\mdf_network_config.h
针对ESP提供的ESP-NOW简单API(本文1.8节所述),ESP WiFi Mesh的开发架构ESP-MDF下进行了一次封装,以便于MDF下进行配网调试等功能,其代码路径:
\esp-mdf\components\protocol_stacks\mdf_espnow\mdf_espnow.c
\esp-mdf\components\protocol_stacks\mdf_espnow\include\mdf_espnow.h
MDF-ESPNOW对ESP-NOW扩展加入了与ESP WiFi Mesh应用相关的几种数据包类型:
由此可见,ESP-NOW机制在ESP WiFi Mesh内可以用于调试,配网,以及一些控制数据的传输。
另外相关的宏定义:
MDF_ESPNOW_INTERFACE:ESP-NOW数据收发接口,默认为STA模式
CONFIG_MDF_ESPNOW_PMK:PMK,见1.3节,可由用户定义
CONFIG_MDF_ESPNOW_LMK:LMK,见1.3节,可由用户定义
单个数据包最大:ESP_NOW_MAX_DATA_LEN 250
单个数据包数据区最大数据:(ESP_NOW_MAX_DATA_LEN - sizeof(mdf_espnow_pkt_t))
也即是一个ESP-NOW数据包包括MDF-ESPNOW数据头与数据区。
mdf_espnow_init
mdf_espnow_read:接收MDF-ESPNOW数据包
mdf_espnow_write:发送MDF-ESPNOW数据包
mdf_espnow_add_peer_base
mdf_espnow_del_peer
初始化ESP-NOW: esp_now_init
注册接收与发送回调函数:mdf_espnow_recv_cb/ mdf_espnow_send_cb
设置PMK:MDF_ESPNOW_PMK
mdf_espnow_write发送函数实现流程如下:
前文说过,单次发送数据包的最大250个字节,且推荐做法是等待发送回调函数返回再进行下一次发送,这里全部都满足。
值得注意的是等待发送回调函数标志位时会一直阻塞,因此mdf_espnow_write函数可能会阻塞任务。
mdf_espnow_read接收的实现复杂一些:
<1> 对于MDF-ESPNOW支持的数据包类型mdf_espnow_pkt_type_t,每种类型用户可以通过调用mdf_espnow_enable来初始化一个接收队数组g_mdf_espnow_queue[i],队列的下标i是其类型,队列大小定义在g_mdf_espnow_queue_size[i];
<2> 当注册的接收回调函数mdf_espnow_recv_cb被调用,首先判断接收到的数据包类型,然后分配空间拷贝数据包,并将数据包地址发送到对应的接收队列;
<3> 调用mdf_espnow_read接口时,会根据用户传入的数据包类型从相应的队列获取接收的数据包地址,再根据数据包的序列号(序列号代表一个完整的数据包被分割为多少个传输)分配空间并继续从队列依次获取余下的数据包拷入以生成完整的数据包,生成完整的数据包后再拷入用户传入的地址空间内。
所以从ESP-NOW的接收回调触发到用户主动获取完毕,发生了三次内存分配与拷贝:
A:接收回调函数内
B:用户调用mdf_espnow_read接口,分配空间并拷贝每个子数据包以获取完整的
C:当获取完整的数据包完毕,还要拷贝数据到用户传入的空间
这里有个巧妙的地方,mdf_espnow_send将大的数据包拆分发送时的序列号是从大到小倒着发的,这样在接收时,从队列中获取的第一个子数据包的序列号就能得知整个包的大小以分配空间:
发送时:
trans_pkt->seq = (pkt_count - 1) - i;
接收时:
uint8_t *recv_data = mdf_calloc(1, (pkt_count + 1) * MDF_ESPNOW_MAX_DATA_LEN);
其它接口与1.8节所述的ESP-NOW接口内容基本相似。