前言

最近我购买了一款内置ESP8266的NodeMCU开发板,这标志着我首次涉足开发板领域。开发板巧妙融合了软件与硬件技术,恰逢我对计算机编程及电子电路设计抱有浓厚兴趣,虽仅初窥门径。因此,此番购板实则是将我的两大爱好融汇贯通的一次尝试。鉴于初获此板时,我对其操作一无所知,我特撰研究笔记一篇,以期记录学习心得。

NodeMCU是一块物联网(IOT)开发板,能连WiFi,用起来跟Arduino差不多。你可以用Arduino的软件来编它的程序。ESP8266是块小芯片,NodeMCU就是围绕这块芯片做的板子。NodeMCU上有两排小针脚,直接连着ESP8266的引脚,做实验很方便。板上还有USB接口和电压转换的小电路,你只用一根USB线就能控制它。

特别说明:此文是本人的研究笔记,并非专业教程。若文中存在学术疏漏,恳请不吝指正,望各位大佬能温和以待,雅正为盼。

一、开发环境搭建

在踏入物联网编程的奇妙旅程之前,首先是要安装好必要的开发环境,这是编写程序代码不可或缺的基石。第一步便是下载Arduino IDE,作为我们编写代码的主战场,同时,为了支持ESP8266这一强大的物联网芯片,我们还需要额外下载ESP8266资源(通常也是以exe文件的形式提供)。完成这些下载与安装后,接下来就可以打开Arduino IDE,并按照以下图示进行配置,为接下来的项目开发做好充分准备。

附:Arduino介绍

Arduino是一款开源电子原型平台,集成了硬件(如Arduino板)和软件(Arduino IDE),以简单易用的方式让初学者和非专业人士能够快速上手进行电子项目的设计和原型制作。它支持C/C++编程,通过一系列引脚连接各种传感器、执行器等外部设备,实现环境感知和反馈控制,广泛应用于教育、项目开发、艺术创作等领域。Arduino以其低成本、灵活性、强大的社区支持和丰富的资源而受到全球创客和爱好者的喜爱。

二、了解开发板结构

如左图,这块板子将CH340串口芯片(图片中的款式)和FLASH闪存芯片(25Q32)与ESP8266集成在了一起,并且预留了MicroUSB(有的厂商会焊接type-c接口)接口,也就是说,我们可以直接利用MicroUSB数据线直接将板子与电脑连接进行数据传输。而这个完整的组合就是NodeMCU。

接口:

如图所示(注:仅供参考):

ESP8266+NodeMCU的各个接口具有多种作用,这些接口主要包括电源接口、GPIO引脚、串行端口(UART)、I2C接口、SPI接口、ADC接口、PWM引脚以及控制引脚等。以下是对这些接口作用的详细解释:

1. 电源接口

VIN引脚:如果有5V稳压源,VIN引脚可用于直接为ESP8266及其外设供电。

3.3V引脚:这些是板载稳压器的输出,可用于为外部组件供电。

GND引脚:为ESP8266 NodeMCU开发板的接地引脚。

注:NodeMCU开发板引脚的输入输出电压的限制是3.3V。如果向引脚施加3.3V以上的电压就有可能对芯片电路造成损伤。最大输出电流是12mA。

2. GPIO引脚

ESP8266 NodeMCU有17个GPIO引脚(GPIO0~GPIO16),这些引脚可以通过编程方式分配给各种功能,如I2C、I2S、UART、PWM、红外遥控、LED灯和按钮等。

需要注意的是,GPIO6~GPIO11引脚通常被用于连接开发板的闪存,因此在实验电路中应避免使用这些引脚进行通用输入输出。

GPIO0-15引脚配有内置上拉电阻,而GPIO16引脚配有内置下拉电阻。

开发板引脚的输入输出电压限制是3.3V,向引脚施加高于3.6V的电压可能会对芯片电路造成损坏。

在NodeMCU上有类似于D1、D2之类的编号,这些引脚编号指的是开发板引脚名,跟ESP8266的引脚并不是一一对应(并不是D1对应GPIO1),比如,对于digitalWrite(4, HIGH);,我们操作的引脚是:GPIO4引脚。不是D4引脚。(该操作也可以写成:digitalWrite(D2, HIGH);)也就是说,当直接使用数字的时候,操作的引脚是GPIO引脚,而ESP8266引脚与NodeMCU引脚对应关系可以利用上面的图片查看。

3. 串行端口(UART)

ESP8266有两个硬件串行端口(UART):UART0和UART1。

UART0使用GPIO1(TX0)和GPIO3(RX0)引脚,负责与电脑进行通信,但也用于其他操作时可能导致程序无法烧录成功。

UART1使用GPIO2(TX1)引脚发送数据,但由于GPIO8被用于连接闪存芯片,所以无法接收数据。

4. I2C接口

ESP8266只有软件模拟的I2C端口,没有硬件的I2C端口。因此,可以使用任意的两个GPIO引脚通过软件模拟I2C通讯。I2C引脚用于连接项目中的各种I2C传感器和外设,支持I2C Master和I2C Slave。

5. SPI接口

ESP8266在从模式和主模式下具有两个SPI接口(SPI和HSPI)。SPI接口包括GPIO14(CLK)、GPIO12(MISO)、GPIO13(MOSI)和GPIO15(CS/SS)等引脚。

6. ADC接口

ESP8266只有一个模拟输入引脚,该引脚通过模拟-数字转换将引脚上的模拟电压数值转化为数字量。

此引脚可以读取的模拟电压值为0–1.0V,但请注意,将ESP8266芯片模拟输入引脚连接在1.0V以上电压可能会损坏ESP8266芯片。

幸运的是,NodeMCU开发板配有降压电路,因此可以用NodeMCU开发板的模拟输入引脚读取0-3.3V的模拟电压信号。

7. PWM引脚

ESP8266 NodeMCU开发板具有4个脉冲宽度调制(PWM)通道,PWM输出可以通过编程方式实现,并用于驱动数字电机和LED等。

8. 控制引脚

控制引脚包括芯片使能引脚(EN)、复位引脚(RST)和WAKE引脚等。

EN引脚:当EN引脚拉高时,ESP8266芯片被启用;当拉低时,芯片以最小功率工作。

RST引脚:RST引脚用于复位ESP8266芯片。

按钮:

1.Reset(RST)键

Reset键,通常也被称为复位键,其主要功能是将ESP8266模块重置到其初始状态。具体来说,Reset键的功能包括:

重启设备:按下Reset键会使ESP8266模块重新启动,这有助于在程序运行出错或需要重新开始运行时快速恢复设备到初始状态。

深度睡眠唤醒:在特定设置中,Reset键还可以作为ESP8266深度睡眠模式的唤醒源。通过将Reset键连接到具有唤醒功能的GPIO引脚(如GPIO 16),在深度睡眠模式结束时,GPIO 16会发送低电平信号到Reset键,从而唤醒ESP8266模块。

2.Flash键

Flash键通常与ESP8266的固件更新或编程相关,其主要功能包括:

进入下载模式:在ESP8266上电前或复位时,按住Flash键可以使设备进入下载模式(也称为烧录模式)。在这个模式下,可以通过编程工具(如Arduino IDE配合ESP8266插件)将新的固件或程序烧录到ESP8266的Flash存储器中。这对于更新固件、修复错误或安装新功能非常有用。

固件更新:配合烧录软件,Flash键使得用户可以轻松更新ESP8266的固件。这对于保持设备的安全性、兼容性和功能更新至关重要。

三、项目开发

【Arduino】

第一个项目:点亮一个LED

利用Arduino IDE创建一个新的工程项目,如果想偷懒可以看看IDE自带示例内的项目,这里选用一个名为Blink的项目进行演示和测试:

文件打开后,进行开发板的匹配(前文已说),可以看到有以下代码(注释已替换):

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
// 定义setup()函数,该函数在Arduino板通电或重置后自动运行一次  
void setup() {
// 使用pinMode()函数将内置LED引脚设置为输出模式
// LED_BUILTIN是一个预定义的宏,它代表了开发板上内置LED的引脚编号
// OUTPUT是一个常量,表示该引脚将用作输出引脚
pinMode(LED_BUILTIN, OUTPUT);
}
// 定义loop()函数,该函数在setup()函数执行完毕后不断循环执行
void loop() {
// 使用digitalWrite()函数将内置LED引脚设置为低电平(LOW)
// 这通常意味着LED会熄灭(具体取决于LED的连接方式,但在这个上下文中,它通常是熄灭)
digitalWrite(LED_BUILTIN, LOW);

// 使用delay()函数暂停程序执行1000毫秒(即1秒)
// 这意味着LED将保持熄灭状态1秒钟
delay(1000);

// 再次使用digitalWrite()函数,但这次是将内置LED引脚设置为高电平(HIGH)
// 这通常意味着LED会点亮(同样,具体取决于LED的连接方式)
digitalWrite(LED_BUILTIN, HIGH);

// 再次使用delay()函数暂停程序执行2000毫秒(即2秒)
// 这意味着LED将保持点亮状态2秒钟
delay(2000);
}

这段代码实现了一个非常简单的LED闪烁效果。在setup()函数中,它首先通过pinMode()函数将内置LED引脚配置为输出模式,以便能够控制LED的亮灭。然后,在loop()函数中,它通过一个无限循环不断地将LED引脚设置为低电平(熄灭LED),等待1秒,然后将其设置为高电平(点亮LED),再等待2秒。这个过程会不断重复,导致LED以1秒熄灭、2秒点亮的模式闪烁。

接下来点击IDE内的“上传”,将程序传入进板子内。观察输出,如果出现以下提示则说明写入成功。若发现模块上的LED进行了规律性闪烁说明程序运行成功:

. Variables and constants in RAM (global, static), used 28104 / 80192 bytes (35%)
║ SEGMENT BYTES DESCRIPTION
╠══ DATA 1496 initialized variables
╠══ RODATA 920 constants
╚══ BSS 25688 zeroed variables
. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59667 / 65536 bytes (91%)
║ SEGMENT BYTES DESCRIPTION
╠══ ICACHE 32768 reserved space for flash instruction cache
╚══ IRAM 26899 code in IRAM
. Code in flash (default, ICACHE_FLASH_ATTR), used 232100 / 1048576 bytes (22%)
║ SEGMENT BYTES DESCRIPTION
╚══ IROM 232100 code in flash
esptool.py v3.0
Serial port COM5
Connecting….
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: e0:98:06:07:e0:f6
Uploading stub…
Running stub…
Stub running…
Configuring flash size…
Auto-detected Flash size: 4MB
Compressed 265568 bytes to 195684…
Writing at 0x00000000… (8 %)
Writing at 0x00004000… (16 %)
Writing at 0x00008000… (25 %)
Writing at 0x0000c000… (33 %)
Writing at 0x00010000… (41 %)
Writing at 0x00014000… (50 %)
Writing at 0x00018000… (58 %)
Writing at 0x0001c000… (66 %)
Writing at 0x00020000… (75 %)
Writing at 0x00024000… (83 %)
Writing at 0x00028000… (91 %)
Writing at 0x0002c000… (100 %)
Wrote 265568 bytes (195684 compressed) at 0x00000000 in 17.2 seconds (effective 123.5 kbit/s)…
Hash of data verified.

Leaving…
Hard resetting via RTS pin…

拓:分析过程

1. 内存使用情况

. Variables and constants in RAM (global, static), used 28104 / 80192 bytes (35%)

这行显示了ESP8266的RAM(随机存取存储器)中全局和静态变量及常量的使用情况。总共有80192字节的RAM,其中28104字节被用于这些变量和常量,占用了35%的RAM。

接下来,详细列出了不同类型的内存使用情况:

DATA:1496字节,用于已初始化的变量。

RODATA:920字节,用于只读数据(常量)。

BSS:25688字节,用于未初始化的变量(这些变量在程序开始时会被自动清零)。

2. 指令RAM(IRAM)使用情况

. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59667 / 65536 bytes (91%)

这行显示了ESP8266的指令RAM(IRAM)的使用情况。IRAM用于存储需要快速访问的代码。总共有65536字节的IRAM,其中59667字节被使用,占用了91%的IRAM。

接下来,详细列出了IRAM的使用情况:

ICACHE:32768字节,这是为闪存指令缓存保留的空间,实际上并不直接存储代码,而是用作缓存。

IRAM:26899字节,这是实际存储在IRAM中的代码量。

3. 闪存中的代码(Flash)

. Code in flash (default, ICACHE_FLASH_ATTR), used 232100 / 1048576 bytes (22%)

这行显示了ESP8266的闪存中存储的代码量。总共有1048576字节(即1MB)的闪存空间用于代码存储,其中232100字节被使用,占用了22%的闪存空间。

接下来的IROM段表示了存储在闪存中的代码量,即232100字节。

4. esptool.py 固件烧写过程

esptool.py v3.0
Serial port COM5
Connecting….
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: e0:98:06:07:e0:f6

Writing at 0x0002c000… (100%)
Wrote 265568 bytes (195684 compressed) at 0x00000000 in 17.2 seconds (effective 123.5 kbit/s)…
Hash of data verified.
Leaving…
Hard resetting via RTS pin…

这部分输出来自于使用esptool.py(ESP8266的固件烧写工具)将固件烧写到ESP8266设备的过程。

esptool.py v3.0:表示使用的esptool.py版本是3.0。

Serial port COM5:表示ESP8266设备通过COM5端口与计算机连接。

接下来的几行显示了设备的识别信息,包括芯片型号(ESP8266EX)、功能(WiFi)、晶振频率(26MHz)和MAC地址。烧写过程中,固件被分成多个块写入到设备的闪存中,每写入一个块都会显示当前的进度百分比。烧写完成后,工具会验证数据的哈希值以确保固件正确写入,并通过RTS引脚执行硬复位以重启设备。

第二个项目:连接网络

我们可以充分利用ESP8266的WIFI模块进行网络连接,并利用开发板搭建一个简易的http服务器。示例代码:

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
#include <ESP8266WiFi.h>  
#include <ESP8266WebServer.h>

const char* ssid = "abcd";
const char* password = "12345678";

ESP8266WebServer server(80); // 创建Web服务器对象,监听80端口

void handleRoot() {
server.send(200, "text/plain", "This is an http server developed based on ESP8266+NodeMCU. More information:https://blog.goldenapplepie.xyz/");
}

void setup() {
//server主体
Serial.begin(115200);
WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}

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

server.on("/", handleRoot); // 设置根路径的处理函数
server.begin(); // 启动服务器

Serial.println("HTTP server started");
}

void loop() {
//server
server.handleClient(); // 处理客户端请求
}

设备运行该程序时,会不断闪烁LED(同时输出窗口提示“….”),表示正在联网中,当成功联网后LED保持常亮(同时输出窗口出现WiFi connected等提示)。接下来就可以利用IP(在输出窗口可以查询)在浏览器访问这个网页了。需要注意的是,我们只能利用局域网连接这个板子,所以得保证两端必须在同一网络下。

在网页上能看到“This is an http server developed based on ESP8266+NodeMCU. More information:https://blog.goldenapplepie.xyz/”一行字则说明一切正常。

我们可以继续修改html部分给网页升级(这将涉及html+css+js的编写):

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
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

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

ESP8266WebServer server(80);

// 假设LED_BUILTIN是内置的LED引脚编号,通常NodeMCU上是D4(GPIO2)
#define LED_BUILTIN 2

bool isWiFiConnected = false;

void handleRoot()
{
String html =
"<html>"
"<head>"
"<title>ESP8266 HTTP Server</title>"
"<style>"
"body { font-family: Arial, sans-serif; }"
"h2 { color: blue; }"
"a {color: #fad17e;text-decoration: none;}"
"</style>"
"</head>"
"<body>"
"<h2>Welcome to my ESP8266 HTTP Server</h1>"
"<p>This is an http server developed based on ESP8266+NodeMCU.</p>"
"<p><a href='https://blog.goldenapplepie.xyz/'>GoldenApplePie's blog</a></p>"
"<script>"
"function showAlert() {"
" alert('yee');"
"}"
"</script>"
"<button onclick='showAlert()'>Click Me</button>"
"</body>"
"</html>";

server.send(200, "text/html", html);
}

void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);

// 开始连接WiFi,并在连接过程中让LED闪烁
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
// 切换LED状态以指示正在连接
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// WiFi连接成功后,设置LED为常亮
digitalWrite(LED_BUILTIN, LOW);
isWiFiConnected = true;

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

server.on("/", handleRoot);
server.begin();
Serial.println("HTTP server started");
}

void loop()
{

if (!isWiFiConnected)
{
}

// 处理客户端请求
server.handleClient();
}

在handleRoot()部分,我们可以利用前端代码进一步升级网页内容。由于没有加装本地文件读取的库,所以只能利用将html代码逐行转换为字符串,让其作为上传内容使用。设备运行后你可以在网页上看到:

第三个项目:使用MQTT

随着物联网技术的迅猛发展,一个由海量设备互联互通的庞大生态系统应运而生,这一过程中,通信协议作为核心要素,其重要性不言而喻。面对设备接入量的激增及其对网络带宽、时延、协议兼容性及平台服务架构的严苛要求,物联网协议亟需解决一系列关键通信难题。在此背景下,MQTT(Message Queuing Telemetry Transport)协议应运而生,它以其轻量级、高效能、高可靠性、支持海量连接及提供安全双向通信等显著优势,经过时间的验证与技术的迭代,已成为物联网领域的标准通信协议,广泛应用于车联网、工业物联网、智能家居、智慧交通等多个前沿领域,推动着物联网生态的持续繁荣与发展。

MQTT分为客户端、服务端。每个客户端可以接收数据(订阅)、发送数据(发布),每个消息都由主题+消息内容组成。假设我们有4个客户端(a、b、c、d)和1个服务端,这4个客户端都已经通过网络连接上了服务端。假如客户端a向服务器订阅了一个主题为abc的信息,那么当服务器接收到来自任何一个客户端(b/c/d)的消息时,只要这个消息的主题是abc,那么服务器就会将这个消息发送给客户端a同理,如果除了客户端a之外还有其它的客户端(b/c)也订阅了abc这个主题,那么服务器也会同时将这个消息送给客户端b客户端c

在本小节,我会尝试创建一个MQTT服务器,并且利用ESP8266连接,实现PC、ESP8266消息互通。首先,我们要创建一个MQTT服务器,你可以选择在自己的电脑本地创建,也可以利用云资源进行部署。在这里我用云资源演示。

我推荐EMQX,打开网站注册/登录账号后,选择一个合适的产品进行云资源部署,这里以专有版云服务为例,按照指引部署即可,实在不行可以查看他们站内的文档,非常详细。当你创建完一个MQTT云服务器后,请切换到该服务器的控制台界面,找到“连接信息”,这一栏包含了连接地址和各种端口。为了测试,你需要在控制台内添加几个客户端认证用户。

接下来为了测试连接情况,我们需要创建一系列的客户端(至少2个),分别在ESP8266和电脑上运行。电脑上你可以选择利用EMQX内的客户端资源,EMQX站内提供了各种编程语言的客户端开发教程,你也可以选择使用现成的MQTT客户端工具:MQTTX。MQTTX提供了一个可视化的操作界面,让MQTT调试变得更简单便捷。

下载并安装好MQTTX后,打开程序在出现的窗口找到“General”一栏开始信息填写。Name只是一个区分名,对MQTT的连接影响不是很大;Client ID是你的自定义名称,它用于在MQTT消息中区分不同用户,类似聊天昵称;Host部分则需要在后边一栏填入刚刚在MQTT服务器控制台记下的连接地址;Port代表的是连接端口,默认为1883。对于其它内容和细节,你可以查阅相关文档

填入完信息后,点击右上角的Connect,当提示Connected时说明连接成功。这时候你需要在左边找到New Subscription并点击它以订阅一个话题。这个话题你可以类比成一个聊天群的群名(或群号),我们可以通过这个聊天群名进行消息发送和接收。订阅就相当于加群,接下来你可以在不同的客户端上用这个“群”来聊天了。

让我们把视线换到ESP8266上,接下来我们利用代码在ESP8266上创建一个MQTT客户端。代码示例(请安装PubSubClient库):

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

const int ledPin = 2;
// WiFi 设置
const char *ssid = ""; // WiFi账
const char *password = ""; // WiFi密

// MQTT 设置
const char *mqtt_broker = ""; // MQTT服务器连接地址
const char *mqtt_topic = ""; //话题
const char *mqtt_username = ""; // 客户端用户名
const char *mqtt_password = ""; // 客户端用户密码
const int mqtt_port = 1883; // MQTT 端口 (TCP)

WiFiClient espClient;
PubSubClient mqtt_client(espClient);

void connectToWiFi();

void connectToMQTTBroker();

void mqttCallback(char *topic, byte *payload, unsigned int length);
void blinkLED(); // LED亮or不亮?

void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(115200);
connectToWiFi();
mqtt_client.setServer(mqtt_broker, mqtt_port);
mqtt_client.setCallback(mqttCallback);
connectToMQTTBroker();
}
// 网络连接
void connectToWiFi() {
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
blinkLED();
Serial.print(".");
}
digitalWrite(ledPin, HIGH);
Serial.println("\nConnected to the WiFi network");
}
// LED闪烁
void blinkLED() {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}

// 连接MQTT服务器
void connectToMQTTBroker() {
while (!mqtt_client.connected()) {
String client_id = "MQTT/ESP8266";
Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str());
if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("Connected to MQTT broker");
mqtt_client.subscribe(mqtt_topic);
// Publish message upon successful connection
mqtt_client.publish(mqtt_topic, "ESP8266:Hello,I'm ESP8266!"); // 发送的消息
} else {
Serial.print("Failed to connect to MQTT broker, rc=");
Serial.print(mqtt_client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
// 响应MQTT
void mqttCallback(char *topic, byte *payload, unsigned int length) {
Serial.print("Message received on topic: ");
Serial.println(topic);
Serial.print("Message:");
for (unsigned int i = 0; i < length; i++) {
Serial.print((char) payload[i]);
}
Serial.println();
Serial.println("-----------------------");
}

void loop() {
if (!mqtt_client.connected()) {
connectToMQTTBroker();
}
mqtt_client.loop();
}

将代码上传至ESP8266并成功运行后,你可以在MQTTX和Adruino控制台上同时看到消息内容(ESP8266:Hello,I’m ESP8266!)。

接下来我们给这个程序继续加点花样。我们可以将ESP8266当成一个聊天机器人的客户端,当聊天机器人识别到其他用户发出消息的关键词后会做出回应。比如当用户发送“你好”时,ESP8266会发送“你好呀~”。代码示例:

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

const int ledPin = 2;
// WiFi settings
const char *ssid = "";
const char *password = "";

// MQTT Broker settings
const char *mqtt_broker = "";
const char *mqtt_topic = "";
const char *mqtt_username = "";
const char *mqtt_password = "";
const int mqtt_port = 1883; // MQTT port (TCP)
// 关键词和回复
const char* KEYWORD_HELLO = "你好";
const char* REPLY_HELLO = "你好呀~";
// 用于跟踪是否已经回复过的标志
bool hasReplied = false;

WiFiClient espClient;
PubSubClient mqtt_client(espClient);

void connectToWiFi();

void connectToMQTTBroker();

void mqttCallback(char *topic, byte *payload, unsigned int length);
void blinkLED(); // 新增函数,用于LED闪烁

void setup() {
pinMode(ledPin, OUTPUT); // 设置LED引脚为输出模式
Serial.begin(115200);
connectToWiFi();
mqtt_client.setServer(mqtt_broker, mqtt_port);
mqtt_client.setCallback(mqttCallback);
connectToMQTTBroker();
}

void connectToWiFi() {
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
blinkLED(); // 在连接WiFi过程中,LED闪烁
Serial.print(".");
}
digitalWrite(ledPin, HIGH);
Serial.println("\nConnected to the WiFi network");
}
void blinkLED() { // 新增函数,实现LED闪烁效果
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}

void connectToMQTTBroker() {
while (!mqtt_client.connected()) {
String client_id = "MQTT/ESP8266";
Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str());
if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("Connected to MQTT broker");
mqtt_client.subscribe(mqtt_topic);
// Publish message upon successful connection
mqtt_client.publish(mqtt_topic, "ESP8266:聊天机器人已上线~");
} else {
Serial.print("Failed to connect to MQTT broker, rc=");
Serial.print(mqtt_client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
hasReplied = false;
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
// 将收到的消息转换为字符串
char message[length + 1];
strncpy(message, (char*)payload, length);
message[length] = '\0'; // 确保字符串以null结尾

// 检查关键词并回复
if (strstr(message, KEYWORD_HELLO) != NULL && !hasReplied) {
// 发送回复消息,只发送一次
mqtt_client.publish(mqtt_topic, REPLY_HELLO);
// 设置标志,表示已经回复过
hasReplied = true;
}

// 打印收到的消息(可选,用于调试)
Serial.print("Received message: ");
Serial.println(message);
}

void loop() {
if (!mqtt_client.connected()) {
connectToMQTTBroker(); // 尝试重新连接MQTT代理
}
mqtt_client.loop(); // 处理MQTT消息

// 每10秒自动重置hasReplied标志,以确保ESP8266能够再次对"你好"消息作出回应
static unsigned long lastResetTime = 0;
unsigned long currentTime = millis();
if (currentTime - lastResetTime > 2000) {
hasReplied = false;
lastResetTime = currentTime; // 更新上次重置时间
}
}

在上面的代码上继续修改,当我发送“打开LED”时,开发板上的LED亮;当我发送“关闭LED”时,开发板上的LED灭;当我发送“LED闪烁”时,开发板上的LED开启闪烁模式,代码示例如下:

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

const int ledPin = 2;
// WiFi settings
const char *ssid = "";
const char *password = "";

// MQTT Broker settings
const char *mqtt_broker = "";
const char *mqtt_topic = "";
const char *mqtt_username = "";
const char *mqtt_password = "";
const int mqtt_port = 1883; // MQTT port (TCP)
// 关键词和回复
const char* KEYWORD_HELLO = "你好";
const char* REPLY_HELLO = "你好呀~";
const char* KEYWORD_OPEN_LED = "打开LED";
const char* KEYWORD_CLOSE_LED = "关闭LED";
const char* KEYWORD_BLINK_LED = "LED闪烁";
// 用于跟踪是否已经回复过的标志
bool hasReplied = false;
bool ledState = false; // false表示LED关闭,true表示LED打开
bool isBlinking = false; // 表示LED是否处于闪烁状态

WiFiClient espClient;
PubSubClient mqtt_client(espClient);

void connectToWiFi();

void connectToMQTTBroker();

void mqttCallback(char *topic, byte *payload, unsigned int length);
void blinkLED(); // 新增函数,用于LED闪烁

void setup() {
pinMode(ledPin, OUTPUT); // 设置LED引脚为输出模式
Serial.begin(115200);
connectToWiFi();
mqtt_client.setServer(mqtt_broker, mqtt_port);
mqtt_client.setCallback(mqttCallback);
connectToMQTTBroker();
}

void connectToWiFi() {
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
blinkLED(); // 在连接WiFi过程中,LED闪烁
Serial.print(".");
}
digitalWrite(ledPin, HIGH);
Serial.println("\nConnected to the WiFi network");
}
void blinkLED() { // 新增函数,实现LED闪烁效果
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}
void blinkLED1() {
if (isBlinking) {
digitalWrite(ledPin, !digitalRead(ledPin)); // 切换LED状态
}
}

void connectToMQTTBroker() {
while (!mqtt_client.connected()) {
String client_id = "MQTT/ESP8266";
Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str());
if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("Connected to MQTT broker");
mqtt_client.subscribe(mqtt_topic);
// Publish message upon successful connection
mqtt_client.publish(mqtt_topic, "ESP8266:聊天机器人已上线~");
} else {
Serial.print("Failed to connect to MQTT broker, rc=");
Serial.print(mqtt_client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
hasReplied = false;
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
// 将收到的消息转换为字符串
char message[length + 1];
strncpy(message, (char*)payload, length);
message[length] = '\0'; // 确保字符串以null结尾

// 检查关键词并回复
if (strstr(message, KEYWORD_HELLO) != NULL && !hasReplied) {
// 发送回复消息,只发送一次
mqtt_client.publish(mqtt_topic, REPLY_HELLO);
// 设置标志,表示已经回复过
hasReplied = true;
}
// 检查并处理LED控制关键词
if (strstr(message, KEYWORD_OPEN_LED) != NULL) {
digitalWrite(ledPin, LOW); // 打开LED
ledState = true;
isBlinking = false; // 停止闪烁
} else if (strstr(message, KEYWORD_CLOSE_LED) != NULL) {
digitalWrite(ledPin, HIGH); // 关闭LED
ledState = false;
isBlinking = false; // 停止闪烁
} else if (strstr(message, KEYWORD_BLINK_LED) != NULL) {
isBlinking = true; // 开始闪烁
}

// 打印收到的消息(可选,用于调试)
Serial.print("Received message: ");
Serial.println(message);
}

void loop() {
if (!mqtt_client.connected()) {
connectToMQTTBroker(); // 尝试重新连接MQTT代理
}
mqtt_client.loop(); // 处理MQTT消息

// 每10秒自动重置hasReplied标志,以确保ESP8266能够再次对"你好"消息作出回应
static unsigned long lastResetTime = 0;
unsigned long currentTime = millis();
if (currentTime - lastResetTime > 2000) {
hasReplied = false;
lastResetTime = currentTime; // 更新上次重置时间
}
// 如果LED处于闪烁状态,则每隔500ms切换一次状态
if (isBlinking) {
static unsigned long lastBlinkTime = 0;
unsigned long currentTime = millis();
if (currentTime - lastBlinkTime > 500) {
blinkLED1();
lastBlinkTime = currentTime; // 更新上次闪烁时间
}
}
}

你可能发现了上面的代码中,关键词触发都涉及到一个状态切换的过程,这是为了防止当你发送关键词消息后,开发板会持续循环的发送消息。

未完待续…….

【MircoPython】

在上面的例子中,你会发现我们都是用Arduino结合C/C++语言编写程序的,其实ESP8266还能支持python(MicroPython)。

首先同样要安装IDE:推荐使用Thonny IDE,安装正常步骤安装后打开,并连接你的板子。

然后进行配置。先下载一个MicroPython的烧录驱动:在MicroPython官网可以找到的,点击download,找到ESP8266的驱动,找到下面的Releases选择合适的版本下载后,将其保存至一个目录内。

接下来在IDE在顶栏找:运行—配置解释器,点开后出现一个窗口。接下来先点击“Install or update MicroPython”,Port选择你板子连接电脑的端口(可以通过设备管理器查看),下面的Firmware则需填入刚刚下载驱动程序的所在路径。最后点击安装,等待安装结束。

然后回到第一个窗口,将端口或WebREPL也换成你板子连接电脑的端口,点击好的。然后刷新一下,当下方控制台出现“>>>”时(python标准的窗口),说明成功烧录。


到这里,你已经将烧录驱动给安装完毕了,整个IDE初始化的样子是这样的:

boot.py是开发板启动时自动运行的主文件。开发板一通电,就会运行boot.py里的程序。为了避免出错,比如不小心写了让程序无限循环的代码,我们最好不在boot.py里直接编程。因为如果程序卡住了,连重启(按reset键)也没用,因为重启后还是会运行同样的程序。所以,我们要在别的文件里写程序,并给程序加一个“停止按钮”,比如通过按某个外置按钮(已经通过引脚连接)来让程序安全停止,这样就不怕程序出错了。

点灯程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 导入必要的库
from machine import Pin
import time
# 定义引脚
led = Pin(2,Pin.OUT)
# 设置循环
while True:
led.value(0) # 设置电平
print("亮") # 控制台输出
time.sleep(1) # 间隔时间

led.value(1)
print("灭")
time.sleep(1)

上面的代码是一个点灯实例。点击保存后它会出现提示窗口,根据需要选择操作(当然选择MicroPython设备)。然后写入后(你可以选择覆盖boot文件,也可以重新命名新文件保存)在点击运行即可。

如果你能看见板子上LED闪烁,则说明运行成功。同时,你利用面包板、杜邦线等材料,将另一个LED接入D4引脚(负极)、正极连接3v,你会发现这个LED也会闪烁,因为这个LED和板子上的LED是同一个引脚上的。将正负极对调(记得更换接线至GND引脚),你可以发现当板子led亮时,外接的LED灭,反之,板子上led灭时,外接的led亮。

呼吸灯

呼吸灯是利用了开发板的PWM功能,PWM(Pulse Width Modulation)简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在测量、通信、工控等方面。我们可以利用PWM控制LED渐变闪烁,而这也被称为呼吸灯,下面是一个简单的示例代码,当你看到LED渐变闪烁时说明运行成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from machine import Pin, PWM  
import utime

# 初始化GPIO引脚
led_pin = Pin(2, Pin.OUT)
# 初始化PWM对象,频率设置为1000Hz,占空比初始化为0(即LED关闭)
pwm = PWM(led_pin, freq=500)

# 呼吸灯效果函数
def breath_led():
while True:
# 模拟吸气(亮度增加)
for duty in range(0, 1024, 32): # 逐渐增加占空比
pwm.duty(duty)
utime.sleep_ms(30)

# 模拟呼气(亮度减少)
for duty in range(1023, -1, -32): # 逐渐减少占空比
pwm.duty(duty)
utime.sleep_ms(30)

# 调用函数开始呼吸灯效果
breath_led()

http服务器

和Arduino一样,我们也可以利用micropython让开发板联网。接下来我们来编写基于python的http服务器程序,直接放上代码:

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
import network  
import socket

# 配置网络参数
ssid = ''
password = ''

# 连接到WiFi网络
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect(ssid, password)

while not sta_if.isconnected():
pass

print('网络已连接,IP地址:', sta_if.ifconfig()[0])

# 创建socket
addr = ('', 80) # 监听所有可用的网络接口上的80端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(addr)
s.listen(1)

def http_response(client_socket, status_line, headers, content=""):
client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n')
for header in headers:
client_socket.sendall(header.encode() + b'\r\n')
client_socket.sendall(b'\r\n')
client_socket.sendall(content.encode())

try:
while True:
print('等待连接...')
client_socket, addr = s.accept()
print('连接自:', addr)
request = client_socket.recv(1024)
print('收到请求:', request.decode())

# 简单的请求处理(仅支持GET /)
if request.startswith(b'GET / HTTP/1.1'):
status_line = '200 OK'
headers = [
'Content-Type: text/html',
'Connection: close'
]
content = '''<!DOCTYPE html>
<html>
<head>
<title>ESP8266 HTTP Server</title>
</head>
<body>
<h1>Hello from ESP8266!</h1>
</body>
</html>
'''
http_response(client_socket, status_line, headers, content)

client_socket.close()

except KeyboardInterrupt:
s.close()
print('服务器已关闭')

当看到控制台输出了IP后复制打开,当你看到网页有“Hello from ESP8266!”则说明运行成功:

结合上面的LED程序,我们可以写一个在网页上控制LED开关的程序,结构大差不差,我们主要对前端代码和后续的http请求进行了功能扩充:

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
import network  
import socket
import machine

# 配置LED引脚
led = machine.Pin(2, machine.Pin.OUT)

# 配置网络参数
ssid = 'GAP'
password = 'nahidaaa'

# 连接到WiFi网络
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect(ssid, password)

# 等待网络连接
while not sta_if.isconnected():
pass

print('网络已连接,IP地址:', sta_if.ifconfig()[0])

# HTML页面内容
HTML_CONTENT = """
<!DOCTYPE html>
<html>
<head>
<title>LED Control</title>
<script>
function toggleLED(state) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/' + state, true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
document.getElementById('ledStatus').innerText = xhr.responseText;
}
};
xhr.send();
}
</script>
</head>
<body>
<h1>LED Control</h1>
<button onclick="toggleLED('on')">Turn LED On</button>
<button onclick="toggleLED('off')">Turn LED Off</button>
<p id="ledStatus">LED is OFF</p>
</body>
</html>
"""

# HTTP服务器处理函数
def http_response(client_socket, request):
request = request.decode()
if request.startswith('GET /on HTTP/1.1'):
led.value(0) # 打开LED
response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is ON'
elif request.startswith('GET /off HTTP/1.1'):
led.value(1) # 关闭LED
response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is OFF'
elif request.startswith('GET / HTTP/1.1'):
response = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n' + HTML_CONTENT
else:
response = 'HTTP/1.1 404 Not Found\r\n\r\nNot Found'

client_socket.sendall(response.encode())
client_socket.close()

# 创建socket并监听
addr = ('', 80) # 监听所有传入连接,端口80
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(addr)
s.listen(1)

print('\nHTTP服务器启动,等待连接...')

try:
while True:
client_socket, addr = s.accept()
print('\n连接自:', addr)
request = client_socket.recv(4096) # 增加缓冲区大小以容纳整个HTTP请求
if request:
http_response(client_socket, request)

except KeyboardInterrupt:
s.close()
print('\n\n\n\n服务器已关闭')

温馨提示:若你在运行第二个程序时发现板子还是在运行前一个程序,记得点击RES刷新,然后再开启运行

参考资料

附:参考文件(内涵代码文件和安装程序等)