一.ESP8266简介
ESP8266是乐鑫(Espressif)制作的WiFi芯片,可以将其理解为一块带有WiFi功能的MCU,就像是stm32一样,它也有GPIO、UART、SPI、I2C、I2S等硬件资源,它也可以跑RTOS等操作系统。
基于乐鑫提供的ESP8266芯片,许多厂商设计了自己的WiFi模组,比较有名的是安信可(Ai-Thinker)的ESP系列模组,正点原子也制作了它自己的模组:ATK-ESP8266,除此之外还有非常非常多种类的WiFi模组,他们很多都是基于ESP8266芯片开发的。
很多WiFi模组外都自带一个屏蔽罩,用来减少外界的电磁干扰,ATK-ESP8266使用的是板载PCB天线,除此之外还有陶瓷天线和IPEX天线等。
我们可以把WiFi模组理解成MCU的最小系统板,可以直接用串口为它烧录程序(和51单片机比较相似)。一般WiFi模块出厂都自带了固件,它其实就是出厂时厂家给模块烧录的程序,我们可以在这个基础上用AT指令实现一些简单的功能,但是更复杂的功能就需要自己编程实现了。
二.ESP8266开发
开发ESP8266有很多种方式,可以使用乐鑫官方提供的ESP-IDF开发环境,基于官方SDK开发,也可以使用LUA语言开发,也可以使用Arduino方式开发,我使用的是Arduino方式开发。
使用Arduino开发ESP8266的环境配置,网上有非常多的教程,我随便找了一个:【ESP8266之Arduino开发】
一、环境配置
在使用NodeMcu(可以理解为开发板)时,因为其自带了自动下载电路(见下图),所以直接插上USB就可以用Arduino IDE为其烧录程序,并且会自动运行烧录进去的程序。
要注意的是使用正点原子ATK-ESP8266模块时,IO_0引脚拉低才能进入烧录模式,IO_0拉高是运行模式(悬空应该也是运行模式),在改变IO_0的电平之后,一定要给模块复位,才能成功切换模式。
三.ESP-NOW协议
ESP-NOW 是由乐鑫开发的另一款协议,可以使多个设备在没有或不使用 Wi-Fi 的情况下进行通信。这种协议类似常见于无线鼠标中的低功耗 2.4GHz 无线连接——设备在进行通信之前要进行配对。配对之后,设备之间的连接是持续的、点对点的,并且不需要握手协议。
有关ESP-NOW更详细的介绍可以查看官方的手册:ESP-NOW 用户指南,也可以看乐鑫在其SDK中的介绍:ESP-IDF编程指南,这应该是有关ESP-NOW所有的官方资料了。除此之外,该网站上有关使用Arduino开发ESP8266的教程也非常棒。
利用ESP-NOW协议我们可以实现多设备点对点的双向通信。
而ESP-NOW同样有Arduino的库文件espnow.h,我利用这个库文件编写了多节点相互通信的程序。
四.实现多点通信
1.获取WiFi模块的MAC地址
首先我们需要将print_mac_address.ino
文件烧录进WiFi模块,获取其MAC地址,其源代码如下。
#include <ESP8266WiFi.h>
void setup()
{
Serial.begin(115200);//设置串口波特率为115200
WiFi.mode(WIFI_STA);//设置WiFi模式为STA,这里要与之后使用的模式相对应
}
void loop()
{
//每隔一秒钟打印一次MAC地址
Serial.println(WiFi.macAddress());
delay(1000);
}
打开串口调试助手,WiFi模块每秒钟会打印一次自己的MAC地址,将其记录下来。
2.烧录通信程序
我使用三个WiFi模块进行通讯,每个模块都会发送数据到另两个模块,这里就分析模块1的程序,另两个模块的程序非常相似,稍作更改即可。
#include <ESP8266WiFi.h>
#include <espnow.h>
uint8_t esp_1_address[] = {0x80, 0x7D, 0x3A, 0x42, 0xD3, 0xEB};//模块1的mac地址
uint8_t esp_2_address[] = {0x8C, 0xAA, 0xB5, 0x0D, 0x97, 0x2C};//模块2的mac地址
uint8_t esp_3_address[] = {0x8C, 0xAA, 0xB5, 0x0D, 0x80, 0x7C};//模块3的mac地址
//Callback when data is sent
//发送信息时的回调函数,每次发送信息会自动调用该函数
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)
{
//Serial.print("Last Packet Send Status: ");
if (sendStatus == 0)//信息发送成功时
{
//打印接收方的mac地址
Serial.print("deliver data to: ");
for(int i = 0; i < 5; ++i)
{
Serial.printf("%x:", *(mac_addr + i));
}
Serial.printf("%x", *(mac_addr + 5));
Serial.println(" ");
//Serial.println("Delivery success");
}
else{
Serial.println("Delivery fail");
}
}
// Callback when data is received
//接收信息时的回调函数,每次接收信息会自动调用该函数
void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len)
{
//在此处先通过mac地址判断是哪一个设备发送的数据,再进行数据解析(在主控中写代码时)
//或者只是使用memcpy函数存储数据,在主函数中处理(类似于DMA的形式)
//打印发送方的mac地址
Serial.print("receive data from: ");
for(int i = 0; i < 5; ++i)
{
Serial.printf("%x:", *(mac + i));
}
Serial.printf("%x", *(mac + 5));
Serial.println(" ");
/*
Serial.print("Bytes received: ");
Serial.println(len);
*/
//打印接收到的数据
for(int i = 0; i < len; ++i)
{
Serial.printf("%c", *(incomingData + i));
}
Serial.println(" ");
}
void setup() {
Serial.begin(115200);//初始化串口,设置其波特率为115200
WiFi.mode(WIFI_STA);//设置WIFI模式为STA
WiFi.disconnect();//断开WIFI连接
// Init ESP-NOW
if (esp_now_init() != 0)//初始化espnow
{
Serial.println("Error initializing ESP-NOW");
return;
}
else
{
Serial.println("esp_now init success");
}
// Set ESP-NOW Role
//双向通信时需要设定角色为 ESP_NOW_ROLE_COMBO
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
// Once ESPNow is successfully Init, we will register for Send CB to
// get the status of Trasnmitted packet
//执行完这个函数,每次发送数据就会自动调用回调函数了
esp_now_register_send_cb(OnDataSent);
// Register peer
//与模块2和3配对
esp_now_add_peer(esp_2_address, ESP_NOW_ROLE_COMBO, 1, NULL, 0);
esp_now_add_peer(esp_3_address, ESP_NOW_ROLE_COMBO, 2, NULL, 0);
// Register for a callback function that will be called when data is received
//执行完这个函数,每次接收数据就会自动调用回调函数了
esp_now_register_recv_cb(OnDataRecv);
}
uint8_t data_to_send[] = {"test from esp8266_1"};//要发送的数据
void loop()
{
// Send message via ESP-NOW
//向模块2和3发送数据
esp_now_send(esp_2_address, data_to_send, sizeof(data_to_send));
esp_now_send(esp_3_address, data_to_send, sizeof(data_to_send));
delay(500);
}
分别将esp8266_1.ino
、esp8266_2.ino
和esp8266_3.ino
烧录进模块1、2、3即可。
效果如下图所示:
五.espnow库函数注释
Arduino的espnow.h似乎没有官方文档,我把我研究的一小部分内容写成注释加进去,不保证完全正确,还是得以手册为准。
#ifndef __ESPNOW_H__
#define __ESPNOW_H__
#ifdef __cplusplus
extern "C" {
#endif
enum esp_now_role {
ESP_NOW_ROLE_IDLE = 0,//未设置角色,不允许发送数据
ESP_NOW_ROLE_CONTROLLER,//控制方
ESP_NOW_ROLE_SLAVE,//被控制方
ESP_NOW_ROLE_COMBO,//控制方&被控制方双角色,双向通信时就用它
ESP_NOW_ROLE_MAX,//不懂
};
//回调函数
typedef void (*esp_now_recv_cb_t)(u8 *mac_addr, u8 *data, u8 len);
typedef void (*esp_now_send_cb_t)(u8 *mac_addr, u8 status);
int esp_now_init(void);//初始化esp_now
int esp_now_deinit(void);//取消esp_now的初始化
int esp_now_register_send_cb(esp_now_send_cb_t cb);//使用该函数之后,接收到数据会自动调用接收回调函数,回调函数的写法可以参考我上面的代码
int esp_now_unregister_send_cb(void);//与上面的函数作用相反
int esp_now_register_recv_cb(esp_now_recv_cb_t cb);//使用该函数之后,发送数据后会自动调用发送回调函数,回调函数的写法可以参考我上面的代码
int esp_now_unregister_recv_cb(void);//与上面的函数作用相反
int esp_now_send(u8 *da, u8 *data, int len);//发送数据,MAC地址中传入NULL会广播
int esp_now_add_peer(u8 *mac_addr, u8 role, u8 channel, u8 *key, u8 key_len);//与新设备配对
int esp_now_del_peer(u8 *mac_addr);//将已配对的设备删除
int esp_now_set_self_role(u8 role);//设定设备自己的角色
int esp_now_get_self_role(void);//获取设备自己的角色
int esp_now_set_peer_role(u8 *mac_addr, u8 role);//设定某个已配对设备的角色
int esp_now_get_peer_role(u8 *mac_addr);//获取某个已配对设备的角色
int esp_now_set_peer_channel(u8 *mac_addr, u8 channel);//设定某个已配对设备的WiFi通道
int esp_now_get_peer_channel(u8 *mac_addr);//获取某个已配对设备的WiFi通道
int esp_now_set_peer_key(u8 *mac_addr, u8 *key, u8 key_len);//设定某个已配对设备的密钥
int esp_now_get_peer_key(u8 *mac_addr, u8 *key, u8 *key_len);//获取某个已配对设备的密钥
u8 *esp_now_fetch_peer(bool restart);//不懂
int esp_now_is_peer_exist(u8 *mac_addr);//检查已经配对的设备是否在线
int esp_now_get_cnt_info(u8 *all_cnt, u8 *encrypt_cnt);//不懂
int esp_now_set_kok(u8 *key, u8 len);//对通信的key进行加密,不设置时使用默认的PMK
#ifdef __cplusplus
}
#endif
#endif