0%

Arduino+Esp8266实现远程的模块控制

最近尝试使用Arduino配合esp8266实现远程控制Arduino上外接的某些模块,于是开始一边摸索一遍折腾开始了一周的尝试,期间得到很一些对自己很有用的经验,遂记录。
本次使用的策略是Arduino通过串口与esp8266通信,esp8266通过wifi连接到网络,然后通过mqtt协议订阅模块的操作信息,客户端就可以通过远程向mqtt发送相关主题的操作信息从而控制Arduino做出相应的约定动作。

使用硬件信息

关于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重刷固件

  • 在乐鑫官网获取下载工具【需要在windows下使用】
  • 同样在乐鑫官网获取所需的AT固件
  • 下载工具和固件使用方式可以参考这篇文章。烧录使用的连接模块,我使用的是学弟借的ESP专用的U转串模块,插上即用(下图)。也有人使用pl2303。
    IOTMCU CH340C ESP8266 ESP-01 Prog WIFI下载器

重刷了固件之后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的问题。

  1. 下载 https://github.com/espressif/esptool/archive/v3.0.zip
  2. 下载 https://github.com/pyserial/pyserial/archive/v3.4.zip
  3. 下载解压出文件夹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脚悬空,即断开,否则设备不会正常工作!

找到一篇帖子,其中说解决方式是在8266ENGND之间外接一个10uF的电容。感觉…不太实际,没有尝试。

后来看这篇博客时,发现它实际上也有说到这个问题。提到需要先断开8266的ENIO0,然后在Connecting..打点时,IO0接地,EN接3.3V,程序可以继续烧录,否则会出现以上报错。帖子中提到该方式的成功率不高,确实我一直没有成功,它说还是使用U转串(下载器)进行烧录比较好。(我…怎么没有想到)

帖子中提到的,需要安装驱动,但是我没有,直接插上,点击上传就烧录了==,很成功,就像重刷固件一样,connecting之后log会显示烧写的地址位。同样,需要注意切换端口。

实际上我看了这篇博客才意识到了(是的 才),实际上烧录程序和输入固件是一样的,固件实际上也是包装好的程序,比如AT固件实际上只是就只通过接受串口的固定格式的数据(AT指令),然后操作并返回结果信息的程序罢了,而所谓的需要注意默认的波特率,是因为程序中Serial.begin同样的使用了该波特率罢了。

走通了代码的编写和烧录流程,接下来终于可以愉快的写代码了。

ESP8266代码编写

ESP8266串口通信

在8266上发送串口数据和Arduino其实一样

1
2
Serial.begin();		// 指定串口波特率,注意通信的另一端(Arduino)需要统一波特率
Serial.println(); // 输出数据。此外8266上的Serial对象还支持printf格式化输出方式

ESP8266连接Wifi

使用8266开发板自带库函数头文件ESP8266Wifi.h,很方便实现Wifi连接。基本代码如下

1
2
WiFi.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
#include <ESP8266WiFi.h>

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
12
WiFiClient 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
#include<ESP8266WiFi.h>
#include<PubSubClient.h>

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
2
3
4
#include<Servo.h>		// 头文件
Servo myServo;
pinMode(int pin, myServo); // 和控制led灯类似,需传入对应的pin口号
myServo.write(int angle); // 写入角度值,进行旋转

Arduino处理串口数据

说实话通过串口接受的数据处理也是废了不少脑筋。
经过测试,通过SoftwareSerial.read()读取的数据实际上是byte的字节流,需要强转为char类型,组装为原文字符串;而Serial.print()方法打印的数据,稍有不慎会出现乱序和重复的问题。然而这些问题都还没有结论,我也暂时没有深究。

由于在8266中输出的字符串文本接受时都是零散的字符,因此我输出文本时,将每一段文本都加上#开头,在Arduino接收时以一个#以及一个结尾的\n作为一句文本的标志,以此获得完整的字符串。
然而如果是要接受一段mqtt协议相关的消息,那么一段消息我需要得到的最主要信息就是topicmessage至少两个部分。因此我决定让8266输出的信息拼装为一个json串,然后Arduino上以json的方式取用数据。

最后实现代码

实际上这个地方大可不必如此麻烦,直接让8266发送角度值,Arduino接受后送给舵机发动就好了,没必要整花里胡哨的。但我是考虑了如果Arduino上如果连接了多个外设,那就需要区分topic和消息内容了,索性折腾了一下

Arduino上的Json库:ArduinoJson
同样的库管理中下载该库。由于网上大部分都是v5版本,为此还在其官网徜徉了一番,找到版本变更点,以及v6正确打开方式
十分详尽,此处不过多介绍使用了

代码示例

实现esp8266wifi连接且通过软串口输出到ardunio

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
// ESP8266代码
#include<ESP8266WiFi.h>

const char* ssid = "ssid";
const char* pwd = "password";

void setup() {
Serial.begin(9600);

Serial.println("Connecting to ");
WiFi.begin(ssid, pwd);
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("Wifi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}

// Ardunio代码
#include <SoftwareSerial.h>

SoftwareSerial mySerial(10, 11); // TX,RX

void setup() {
Serial.begin(9600);
while (!Serial) {}
Serial.println("Goodnight moon!");

delay(300);
mySerial.begin(9600);
}

void loop() {
if (mySerial.available()) {
Serial.write(mySerial.read());
}
if (Serial.available()) {
mySerial.write(Serial.read());
}
}

首次成功

值得注意的是,一直以来很多论坛说的Ardunio和ESP8266的RX和TX需要反着接,但是尝试之后好像不是这样的。并且成功的最后一步操作是,把两条反接的线再反过来一次,即TX对TX,RX对RX。这里不是很明白,望交流告知。因此如果没有反应,可以尝试把RX和TX反过来试试。

舵机

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// ESP8266端代码
#include<ESP8266WiFi.h>
#include<PubSubClient.h>

const char* ssid = "ssid";
const char* pwd = "pwd";
const char* mqtt_server = "192.168.1.103";
const int mqtt_port = 1883;

WiFiClient espClient;
PubSubClient client(espClient);

void connectWifi() {
WiFi.begin(ssid, pwd);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("Wifi connected");

Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}

void reConnectMqtt() {
while (!client.connected()) {
String client_id = "esp8266-client-";
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 setup() {
Serial.begin(9600);
Serial.println("Connecting to ");

connectWifi();

client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
reConnectMqtt();
}

void callback(char *topic, byte * payload, unsigned int length) {
String content = "#{\"topic\":\"" + String(topic) + "\", \"msg\":\"";
for (int i = 0; i < length; i++) {
content += (char) payload[i];
}
content += "\", \"other\":213}";
Serial.println(content);
}

void loop() {
if (!client.connected()) {
Serial.println("Reconnenct");
reConnectMqtt();
}
client.loop();
}

// ============================
// Arduino端代码

#include <SoftwareSerial.h>
#include <ArduinoJson.h>
#include <Servo.h>

Servo myServo;
int ServoPin = 8;

SoftwareSerial mySerial(10, 11);

void setup() {
Serial.begin(9600);
pinMode(ServoPin, OUTPUT);
myServo.attach(ServoPin);

while (!Serial) {}
Serial.println("Goodnight moon!");

delay(300);
mySerial.begin(9600);
}

String con = "";
/*
* 0:未记录
* 1:记录日志
*/
int note = 0;
void handler(String);

void loop() {
if (mySerial.available()) {
char a = mySerial.read();
if (a == '\n' && note == 1) {
// 记录中且读到/n:关闭记录且输出,并清空缓存;
note = 0;
handler(con);
con = "";
return ;
} else if (a == '#' && note == 1) {
// 记录中又读到一个#:输出当前,清空缓存
handler(con);
con = "";
return ;
}
if (a == '#') {
note = 1;
} else if (note == 1) {
con += a;
}
}
}

StaticJsonDocument<200> doc;

void handler(String str) {
char* con = str.c_str();
auto error = deserializeJson(doc, con);
Serial.println(str);
if (error) {
Serial.println("parseJson Fail");
return;
}

char* topic = doc["topic"];
char* msg = doc["msg"];
int other = doc["other"];
Serial.println("**" + String(topic) + "**");
Serial.println("**" + String(msg) + "**");
Serial.println("**" + String(other) + "**");

if (String(topic) == "hello") {
int te = atoi(msg);
// Serial.println("Servo Angle " + te); // 若是打开这句输出,串口输出就会乱掉,不解
myServo.write(te);
}
}

实物图

总结

本次折腾了快一周,算是很有收获,搞清楚了之前一直云里雾里的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
12
void 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);
}


完结 撒花 ฅ>ω<*ฅ