最近尝试使用Arduino配合esp8266实现远程控制Arduino上外接的某些模块,于是开始一边摸索一遍折腾开始了一周的尝试,期间得到很一些对自己很有用的经验,遂记录。
本次使用的策略是Arduino通过串口与esp8266通信,esp8266通过wifi连接到网络,然后通过mqtt协议订阅模块的操作信息,客户端就可以通过远程向mqtt发送相关主题的操作信息从而控制Arduino做出相应的约定动作。
使用硬件信息
- 国版Ardunio Uno Rev3【MacOS搭建ardunio环境】
- Esp8266-01s
- 动作模块使用舵机(或者灯泡等)
关于ESP8266
ESP8266存在3种工作模式:STA,AP,STA+AP
Sta模式: Station,类似于无线终端,本身不接受无线接入,可连接到AP,也就是可以连接wifi。一般无线网卡即工作在该模式
AP模式:相当于路由器,自己发射WiFi,终端可以连接上它,但是无法像sta模式那样连接其他WiFi。
STA+AP模式:既可以自己发射WiFi供其他终端连接,又可以做终端连接其他WiFi。这也是默认的出厂模式。
ESP8266默认携带AT固件,支持AT指令,通过AT指令可以实现模式切换,修改波特率,Wifi连接,TCP连接等功能。
当然,使用不同的固件实现的功能也将不同。例如这里的支持mqtt的AT固件
Arduino与8266的串口通信
ESP8266与Arduino的接线,软硬串口的通信可以参见这篇文章。
ESP8266AT指令不可用
Arduino接上8266之后,通过串口发送AT指令没有响应。于是尝试重刷esp8266的支持AT指令固件
ESP8266重刷固件
重刷了固件之后AT指令还是没有生效。排除了接线问题,看到这篇文章提示,有可能是供电不足的原因。
重新紧了一下线,重插电源线(3.3V),8266的蓝灯一闪,然后微弱发光,这时候AT指令就生效了。(我使用的是MBP笔记本雷电口+usb转接器)
b站有个大佬提供了一个使用外部电源输入的实现方式。
所以AT指令不生效的情况,可以考虑一下供电的问题
ESP8266连接wifi与MQTT服务器
紧接着是8266上的代码编写,需要实现
- Wifi连接
- MQTT服务通信
- 通过串口发送消息到Arduino
ESP8266代码烧录
ESP8266代码编写
ESP8266的代码可以使用ArduinoIDE编写,同时支持8266板的库函数管理相关。也可以使用VSCode安装PlatformIO扩展,然后在PlatformIO里安装相关应用库。
使用ArduinoIDE,需要先安装esp8266的开发板支持。否则使用如ESP8266WiFi.h
这样的库函数会报错sketch_apr17a:15:10: error: ESP8266WiFi.h: No such file or directory
- 打开Arduino的设置页,在
附加开发版管理网址
中填入http://arduino.esp8266.com/stable/package_esp8266com_index.json
- 打开
工具>开发板>开发板管理器
搜索esp8266,点击安装 - 安装成功以后即可在开发板选项中看到
ESP8266 Board
,选择ESP8266 Module
,就可以使用8266的库函数进行开发。注意esp8266代码编译和上传(烧录)时,需要确认开发板和端口的选择,否则可能会失败
8266开发板支持包下载失败
曾经在学校安装支持包速度蹭蹭,在家却是龟速+反复失败。于是想到了手动下载安装:
- 浏览器打开
http://arduino.esp8266.com/stable/package_esp8266com_index.json
,可以看到其实是一大段记录支持包所需要的依赖包的地址(有点类似npm的package.json) - 最主要的
esp8266-2.7.4.zip
,通过github下载其实速度还是感人。后来使用迅雷,10秒结束战斗!!真是个防止浪费生命的神器。 - 需要放到
/Library/Arduino15/staging/packages
目录下,再点击安装,就会跳过这个包了 - 还有一些零碎的依赖,看起来主要是根据不同的编译环境的一些不同包,上边的博客中提到可以把所有带osx,apple的都下下来。经过实际测试,应该不需要,主要是以下几个包。(测试使用笨方法,即点击安装,下载过程会在上述文件中生成文件,然后经过文件名到上边依赖包目录找到对应文件及地址进行下载,所以可能有一些比较小下载极快的这里就不列出来)
- x86_64-apple-darwin14.xtensa-lx106-elf-b40a506.1563313032.tar.gz
- x86_64-apple-darwin14.mkspiffs-7fefeac.1563313032.tar.gz
- x86_64-apple-darwin14.mklittlefs-fe5bb56.1578453304.tar.gz
- python3-macosx-portable.tar.gz
pyserial or esptool directories not found next to this upload.py tool
编译代码时出现以上报错。翻阅一堆博客论坛,找到了解决方法,并且有可能是MacOSBigSur的问题。
- 下载 https://github.com/espressif/esptool/archive/v3.0.zip
- 下载 https://github.com/pyserial/pyserial/archive/v3.4.zip
- 下载解压出文件夹
esptool/
和pyserial/
放到~/Library/Arduino15/packages/esp8266/hardware/esp8266/2.7.4/tools/
文件夹下,替换原有的文件,具体点进目录中,根据文件名应该就明白了
参考链接
Esp8266代码烧录
esp8266通过Arduino板子连接PC进行代码烧录,始终出现问题。说始终是因为前几个月就因为这个问题导致有个小项目搁置,所以出现这个问题也算是意料之中吧。
主要现象是代码编译通过以后点击上传,始终出现Connecting...
打点,过会就出现Failed to connect to ESP32: Timed out waiting for packet header
的报错,上传失败。使用的接线是
将UTXD接到串口模块的TX上,CH_PD和VCC接3.3V,GND和GPIO0接GND
这是烧录模式,如果要工作的话请将GPIO0脚悬空,即断开,否则设备不会正常工作!
找到一篇帖子,其中说解决方式是在8266EN
和GND
之间外接一个10uF的电容。感觉…不太实际,没有尝试。
后来看这篇博客时,发现它实际上也有说到这个问题。提到需要先断开8266的EN
和IO0
,然后在Connecting..
打点时,IO0
接地,EN
接3.3V,程序可以继续烧录,否则会出现以上报错。帖子中提到该方式的成功率不高,确实我一直没有成功,它说还是使用U转串(下载器)进行烧录比较好。(我…怎么没有想到)
帖子中提到的,需要安装驱动,但是我没有,直接插上,点击上传就烧录了==,很成功,就像重刷固件一样,connecting之后log会显示烧写的地址位。同样,需要注意切换端口。
实际上我看了这篇博客才意识到了(是的 才),实际上烧录程序和输入固件是一样的,固件实际上也是包装好的程序,比如AT固件实际上只是就只通过接受串口的固定格式的数据(AT指令),然后操作并返回结果信息的程序罢了,而所谓的需要注意默认的波特率,是因为程序中Serial.begin
同样的使用了该波特率罢了。
走通了代码的编写和烧录流程,接下来终于可以愉快的写代码了。
ESP8266代码编写
ESP8266串口通信
在8266上发送串口数据和Arduino其实一样1
2Serial.begin(); // 指定串口波特率,注意通信的另一端(Arduino)需要统一波特率
Serial.println(); // 输出数据。此外8266上的Serial对象还支持printf格式化输出方式
ESP8266连接Wifi
使用8266开发板自带库函数头文件ESP8266Wifi.h
,很方便实现Wifi连接。基本代码如下1
2WiFi.begin(char* ssid, char* password); // 传入Wifi名和密码,开始连接
if (Wifi.status() != WL_CONNECTED) // 确认连接状态
具体代码示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const char* ssid = "ESP8266 需要连接的WIFI的SSID";
const char* password = "Wifi密码";
void setup() {
Serial.begin(9600);
delay(10);
Serial.print("Connecting to ");
/* 明确将ESP8266设置为WiFi客户端,否则默认情况下,它将尝试同时充当客户端和接入点,并可能导致WiFi网络上的其他WiFi设备出现网络问题 */
WiFi.mode(WIFI_STA);
// 开始连接
WiFi.begin(ssid, password);
// 状态确认
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 连接成功
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
int whetherConnect = 0;
WiFiClient client;
const uint16_t port = 8082;
const char * host = "你的IP"; // ip or dns
void loop() {
// 确认socket连接状态
if (client.connected() == true)
whetherConnect = 1;
else
whetherConnect = 0;
if (whetherConnect == 0) {
Serial.print("connecting to " + host);
if (!client.connect(host, port)) {
Serial.println("connection failed \n wait 5 sec...");
return;
} else {
whetherConnect = 1;
}
}
if (Serial.available()) {
// 读取硬串口
if (Serial.read() == '#') {
// 获取ssid列表
int n = WiFi.scanNetworks();
Serial.println("scan done");
if (n == 0) {
client.println("no networks found");
} else {
client.println(n + " networks found");
for (int i = 0; i < n; ++i) {
// Print SSID and RSSI for each network found
client.print(WiFi.SSID(i));
client.println(WiFi.RSSI(i));
}
}
// This will send the request to the server
client.println("------------------------------------------");
//read back one line from server
Serial.println(client.readStringUntil('\r'));
Serial.println("closing connection");
client.stop();
}
}
}
ESP8266与MQTT协议
库管理中导入PubSubClient.h
头文件,实现MQTT服务端的连接以及消息的订阅和发布。代码也不复杂1
2
3
4
5
6
7
8
9
10
11
12WiFiClient espClient;
PubSubClient client(espClient);
client.setServer(char* server_ip, int port); // 设置服务器IP和端口(思考,如果IP下还带有子目录该如何实现?)
client.setCallback(callback); // 设置回调,当有消息传入时会执行该回调
client.connected(); // 判断连接状态
client.connect(String clientId); // 开始连接,传入客户端ID
client.publish(char* topic, char* msg); // 消息发布
client.subscribe(char* topic); // 消息订阅
client.loop(); // 处理保持活动信号,以及处理传入消息
void callback(char *topic, byte * payload, unsigned int length) { } // 主题,消息,消息长度
具体代码示例:参考1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const char* ssid = "ESP8266 需要连接的WIFI的SSID";
const char* password = "Wifi密码";
const char* mqtt_server = "MQTT服务器IP";
const int mqtt_port = MQTT服务端口;
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
Serial.begin(9600);
Serial.println("Connecting to ");
connectWiFi(); // wifi连接如上文
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
while (!client.connected()) {
String client_id = String(WiFi.macAddress());
Serial.println("Connecting to public emqx mqtt broker.....");
if (client.connect(client_id.c_str())) {
Serial.println("Public emqx mqtt broker connected");
} else {
Serial.print("failed with state ");
Serial.print(client.state());
delay(2000);
}
}
Serial.println("start to subsc");
char* topic = "hello";
client.publish(topic, "hello emqx");
client.subscribe(topic);
}
void callback(char *topic, byte * payload, unsigned int length) {
Serial.print("Message arrived in topic: ");
Serial.println(topic);
Serial.printf("Message:(%d) ", length);
for (int i = 0; i < length; i++) {
Serial.print((char) payload[i]);
}
Serial.println();
Serial.println("-----------------------");
}
void loop() {
client.loop();
}
还有大佬对PubSubClient库进行进一步封装实现了一个库。可以看看这个
以及大佬编写的,关于客户端状态返回码的注释。如果连接MQTT服务器失败返回了状态码可以参考一下
int - 客户端状态,可以采用以下值 (常量定义在 PubSubClient.h):
-4 : MQTT_ CONNECTION_ TIMEOUT - 服务器在保持活动时间内没有响应。
-3 : MQTT_ CONNECTION_ LOST - 网络连接中断。
-2 : MQTT_ CONNECT_ FAILED - 网络连接失败。
-1 : MQTT_ DISCONNECTED - 客户端干净地断开连接。
0 : MQTT_ CONNECTED - 客户端已连接。
1 : MQTT_ CONNECT_ BAD_ PROTOCOL - 服务器不支持请求的MQTT版本。
2 : MQTT_ CONNECT_ BAD_ CLIENT_ ID - 服务器拒绝了客户端标识符。
3 : MQTT_ CONNECT_ UNAVAILABLE - 服务器无法接受连接。
4 : MQTT_ CONNECT_ BAD_ CREDENTIALS - 用户名/密码被拒绝。
5 : MQTT_ CONNECT_ UNAUTHORIZED - 客户端无权连接。
关于MQTT
对于MQTT,此前一直不了解,这几天查了些资料,根据自己的理解对MQTT进行了简单的介绍,大概是对TCP协议进一步封装的一个应用层协议,主要是消息订阅与发布广播的机制。可以看看我对于MQTT的认识
查阅过程中,看到Arduino社区一位大佬写下的科普贴,觉得对理解MQTT还是很有帮助。
以及如果有node.js的环境,也可以使用node.js搭建MQTT服务器和客户端,实现消息的发送订阅。使用mosca
库3 5行就可以实现。
Arduino接收信息并输出
Arduino接收串口信息
实际上Arduino通过软串口连接8266的示例在上边以及提供了,但我还是提供一下我的实现方式。
Arduino处理数据并控制动作模块
Arduino+舵机
舵机图如下,能通过pin口接收一个角度值然后将旋桨旋转到该角度。舵机细节讲解
接线方式参考
控制代码如下
1 |
|
Arduino处理串口数据
说实话通过串口接受的数据处理也是废了不少脑筋。
经过测试,通过SoftwareSerial.read()读取的数据实际上是byte的字节流,需要强转为char类型,组装为原文字符串;而Serial.print()方法打印的数据,稍有不慎会出现乱序和重复的问题。然而这些问题都还没有结论,我也暂时没有深究。
由于在8266中输出的字符串文本接受时都是零散的字符,因此我输出文本时,将每一段文本都加上#
开头,在Arduino接收时以一个#
以及一个结尾的\n
作为一句文本的标志,以此获得完整的字符串。
然而如果是要接受一段mqtt协议相关的消息,那么一段消息我需要得到的最主要信息就是topic
和message
至少两个部分。因此我决定让8266输出的信息拼装为一个json串,然后Arduino上以json的方式取用数据。
实际上这个地方大可不必如此麻烦,直接让8266发送角度值,Arduino接受后送给舵机发动就好了,没必要整花里胡哨的。但我是考虑了如果Arduino上如果连接了多个外设,那就需要区分topic和消息内容了,索性折腾了一下
代码示例
实现esp8266wifi连接且通过软串口输出到ardunio
1 | // ESP8266代码 |
值得注意的是,一直以来很多论坛说的Ardunio和ESP8266的RX和TX需要反着接,但是尝试之后好像不是这样的。并且成功的最后一步操作是,把两条反接的线再反过来一次,即TX对TX,RX对RX。这里不是很明白,望交流告知。因此如果没有反应,可以尝试把RX和TX反过来试试。
舵机
1 | // ESP8266端代码 |
总结
本次折腾了快一周,算是很有收获,搞清楚了之前一直云里雾里的8266编程和他的AT指令以及串口通信的使用。
ESP8266和Arduino就像是两个的单片机,如果需要做AT指令不好完成的操作,当然需要另外编写代码烧录到8266中。而如果只是连接Wifi还是可以通过AT指令完成的,甚至进行MQTT通信,也可以使用对应的AT固件完成。
实际上在各种查阅时候,确实看到某大佬自己编写的支持8266进行mqtt通信的指令固件,参见他的站点。对于这种大佬也是只有崇拜啦
更新
2021-05-08 舵机抖动
使用Servo.h
库操作舵机,出现问题:舵机在接到指令转动之前,会有极小角度的颤抖,然后再进行偏转;甚至在未接收到指令时,偶现出现自发的小幅转动。
经过一番查阅,发现这篇博客,解决了舵机抖动问题。
由文章分析可知,官方提供的Servo.h
代码中使用了定时器中断,由于串口通信也需要使用定时器,所以如果同时使用了Servo和串口通信功能,那么会出现舵机抖动的问题。
博主给出的解决方式,自己实现舵机的驱动程序,实际上也十分简单,也是往舵机的数字接口发送一定频率的高低电平实现,代码如下:1
2
3
4
5
6
7
8
9
10
11
12void servopulse(int angle)//定义一个脉冲函数
{
//发送50个脉冲
for (int i = 0; i < 50; i++) {
int pulsewidth = (angle * 11) + 500; //将角度转化为500-2480的脉宽值
digitalWrite(servoPin, HIGH); //将舵机接口电平至高
delayMicroseconds(pulsewidth); //延时脉宽值的微秒数
digitalWrite(servoPin, LOW); //将舵机接口电平至低
delayMicroseconds(20000 - pulsewidth);
}
// delay(20);
}