目录
ESP8266
ESP8266是一款由乐鑫(Espressif)公司开发的芯片。
一般来说,搭载了ESP8266芯片,长得像下面的这样的开发板就叫ESP8266NodeMCU。
可能会有些许不同,但是只要芯片上写着ESP8266的就没问题。因为随便来个人搞个ESP8266的芯片都可以搞个自己的ESP8266NodeMCU。
ESP8266内部集成了WiFi,所以可以作为WiFi模块(我干过,用STM32去采集数据,然后通过串口通信传给ESP8266,再由ESP8266把数据上传到服务器),也可以作为独立的微控制器。
接下来我们就开始用ArduinoIDE来编写程序速通一遍ESP8266NodeMCU。(需要对单片机有点基础,否则一些名词会听的稀里糊涂)
ArduinoIED环境配置
要使用ArduinoIED编写ESP8266NodeMCU的程序,我们需要有ESP8266开发板的资源包,这个我们可以去电灯科技的官网里找离线安装包。
包括ESP32的资源也有。
我们点击下载之后会跳转到Arduino中文网,跟着步骤下载即可。得到一个exe文件,直接执行就好了。
然后在ArduinoIDE中按照下面选择即可(不是我不截图,一用快捷键截图这些选项就不见了,所以只能拍照了)
GPIO
要学习一款芯片,我们先成为电灯大师再说。
我们能用的ESP8266NodeMCU的GPIO口其实挺少的。
GPIO口看着不少,其实右边那一排的GPIO口我们是用不了的(不作为普通的GPIO口使用,其他特殊用途可以),因为它们是用来控制内部存储单元的。就记着A0口在的那一排的其他GPIO用不了。
然后左边一排的GPIO中,GPIO1和GPIO3被用来串口通信,一般也不用作其他用途,所以能用的GPIO口其实少的可怜。
那我们就先点个灯吧。
闪烁LED
配置GPIO口模式
pinMode(uint8_t pin, uint8_t mode);
第一个参数可以直接填入ESP8266NodeMCU开发板上的标注,例如“D0”,也可以填入数字,例如D0实际上是GPIO16,因此填入数字16也是可以的。
第二关参数配置模式,简单来说我们就使用三种,OUTPUT,INPUT,INPUT_PULLUP,分别是输出,输入,上拉输入,其实可配置的模式不止这三种,但是这三种是最常用的。
数字输出
digitalWrite(uint8_t pin, uint8_t val);
第一个参数就是指定GPIO口,跟上面一样。
第二个参数直接写数字即可,1就是高电平,0就是低电平。
点亮LED
void setup() { // put your setup code here, to run once: pinMode(D0,OUTPUT); //等价于pinMode(16,OUTPUT); digitalWrite(D0, 1); //等价于digitalWrite(16, 1) } void loop() { // put your main code here, to run repeatedly: }
以防有小伙伴不清楚这种格式的代码编写(因为51和32都是写main函数的),这边稍微解释一下。我们写在setup函数里的是配置东西的代码,只会执行一次,而写在loop函数里的是会一直循环执行的东西,可以理解为我们51,32代码里的while(1)。
这样我们先配置了GPIO口,再输出高电平,然后再接上LED就可以实现点亮LED的操作了。
延时函数
delay(unsigned long ms);
填入数字,延时对应的毫秒数。
delayMicroseconds(unsigned int us);
这个是延时微秒。
闪烁LED
有了延时函数我们就可以让LED闪烁了。
void setup() { // put your setup code here, to run once: pinMode(D0,OUTPUT); } void loop() { // put your main code here, to run repeatedly: digitalWrite(D0,1); delay(1000); digitalWrite(D0,0); delay(1000); }
只要一下输出高电平一下输出低电平即可。
当然,像上面这么写太臃肿了,我们有更简洁的写法。
数字读取
digitalRead(uint8_t pin);
通过这个函数,我们可以读取到对应GPIO口的输入电平。
我们只需要让GPIO口输出与读入电平不同的电平即可实现闪烁了。
闪烁LED 2.0
void setup() { // put your setup code here, to run once: pinMode(D0,OUTPUT); } void loop() { // put your main code here, to run repeatedly: digitalWrite(D0, !digitalRead(D0)); delay(1000); }
可能大家会有疑惑,GPIO口不是配置为输出模式了吗,怎么还可以读入。
我实测是可以的,当GPIO口为输出模式的时候,读入的实际上是自己的输出。如果是要真正的读取其他模块的输入,那么还是需要配置为输入(INTPUT)模式,否则读入的数据会有误,甚至是影响要接入的模块。
定时函数
首先是要#include<Ticker.h>。
然后是定义一个Ticker类型的对象,名字不能取为time(血的教训)。
调用这个对象的成员函数即可完成定时的操作。
定时执行
attach()
这个成员函数的第一个参数填入一个整型,表示定时的秒数。
第二个参数填入函数名,最多可以有一个int类型的参数。也就是每隔第一个参数填入的秒数就会执行一次这个函数。
第三个参数,如果前面填入的函数没有参数,那么第三个位置可以空着,反之第三个参数就是调用前面函数时传入的参数。
这个函数可以执行多次,但是只以最后一次为准。
以毫秒级定时的话用下面的成员函数。
attach_ms()
取消定时
detach()
这个成员函数没有参数,调用之后取消之前的定时。
计时函数
如果只调用一次而不需要循环定时,那么可以使用计时函数,这个函数的参数和定时一致,不过只会调用一次。
once() once_ms()
要注意的是,每个Ticker对象同一时间只能有一个定时任务。
闪烁LED3.0
#include <Ticker.h> Ticker t0; Ticker t1; void test(int flag){ digitalWrite(D0,!digitalRead(D0)); } void setup() { // put your setup code here, to run once: pinMode(D0,OUTPUT); t0.attach(1,test,1); t1.once(10,[&](){ t0.detach(); }); } void loop() { // put your main code here, to run repeatedly: }
上面的函数定义了两个Ticker对象,一个定时1秒,每秒翻转一次D0的电平。另一个计时,十秒之后关闭定时1秒的任务。因为同一时间每个对象只能有一个任务,因此这里用了两个任务。
PWM
配置GPIO口为输出模式之后,我们调用下面的函数就可以输出PWM了。
analogWrite(uint8_t pin, int val);
第二个参数来指定占空比。我看其他教程都说第二个参数填入0~255,数值越大PWM的占空比就越大,不过我实测之后第二个参数的范围是0~1023。这个还是要以大家手上的板子为准,可能是板子不同的原因。
我用的下面的代码来测试的,LED灯除了灭以外,还有三种不同亮度,因此可以知道大于255的参数也是有效的,参数范围也可以确定是0~1023。
void setup() { // put your setup code here, to run once: pinMode(D0, OUTPUT); } void loop() { // put your main code here, to run repeatedly: analogWrite(D0, 0); delay(1000); analogWrite(D0, 256); delay(1000); analogWrite(D0, 512); delay(1000); analogWrite(D0, 1024); delay(1000); }
如果想要弄呼吸灯的话只需要修改连续增加和减少的占空比即可。
另外找的一些资料说有函数可以修改PWM的输出频率,但是我拿无源蜂鸣器测试之后发现并没有什么用,我也找不到原因。
就是下面这个函数,大家可以拿自己的板子去试一试,万一可以呢。
analogWriteFreq(uint32_t freq);
外部中断
外部中断配置
attachInterrupt(uint8_t pin, void (*)(), int mode);
第一个参数指定GPIO口,这个跟之前不一样,这边填入的GPIO口需要用digitalPinToInterrupt()来包裹着,表示为把GPIO口用于外部中断,并且D0是用不了的。
第二个参数填入一个无参无返的函数,这个函数是当我们触发了外部中断之后会调用的函数。在函数定义的时候,需要在函数的一开头写上ICACHE_RAM_ATTR。
第三个参数指定触发外部中断的条件,有三个,RISING,FALLING,CHANGE。分别是上升沿,下降沿,上升沿和下降沿。
以上具体都可以参考我下面的代码。
关闭中断
detachInterrupt(uint8_t pin);
如果我们需要GPIO口不再触发外部中断,那么使用这个函数。这里的参数直接写,不需要那个函数包裹。
开关控制LED
int count=0; ICACHE_RAM_ATTR void test(){ digitalWrite(D0, !digitalRead(D0)); if(++count>=10) detachInterrupt(D1); } void setup() { // put your setup code here, to run once: pinMode(D0,OUTPUT); pinMode(D1, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(D1),test,RISING); } void loop() { // put your main code here, to run repeatedly: }
这里是设置D0为输出模式,D1为输入模式,并且配置D1外部上升沿中断。每次触发后都会使得D0的电平翻转,十次之后关闭D1的中断。
串口通信
我们用数据线把ESP8266NodeMCU和电脑连接之后就可以直接使用串口和电脑通讯了。
ArduinoIDE中有自带串口助手,就在页面的右上角,点击后页面下方就会出现串口监视器。可以进行简单的收发数据。
串口初始化
Serial.begin(unsigned long baud);
初始化函数就这一个,但是有五种重载版本,我们最常用的就是上面这个,我们指定波特率,然后数据位是默认8位,停止位1位,无校验位。
输出数据
输出数据的函数很多,我这里就挑几个我自己最常用的简单介绍一下。
write
Serial.write()
这个函数也有十二种重载版本,我就不细说了,一般来说,要输出原始数据格式,也就是十六进制的时候,我们使用这个函数。
print&println&printf
Serial.print(); Serial.println(); Serial.printf();
这仨输出的都是文本格式,所以常用来输出调试信息。
print就是普通地输出。
println跟print相比就是自己多输出了一个换行符(“\r\n”)。
printf的话就是我们C语言里那样使用的,可以格式化输出。
读入数据
读入数据的函数也有很多,这里也是只挑几个我个人比较常用的来简单说说。
read
Serial.read()
这个函数有两个重载版本,我们都说一下。
第一个版本是没有参数的,直接就是读出一个字节,然后返回出来。
第二个版本是下面这样的。
Serial.read(char *buffer,size_t size);
可以读出第二个参数指定数目的数据到第一个传入参数中,但是如果第二个参数的大小比可以读取的数据更多,那么就只能读出已有的数据的数量,并且返回实际读出数据的数量。
readString
Serial.readString()
这个函数会读出数据,并且以String的类型返回出来。
判断是否有数据
如果我们要读出数据,那么首先需要有数据是吧,我们该怎么知道什么时候有数据呢。
我们使用下面这个函数。
Serial.available()
它会返回一个int值来表示是否有数据可读,但是我们完全可以把它的返回值当成一个bool类型的值来看待 。
串口回声实验
了解上面的函数之后,我们就可以使用ESP8266NodeMCU来进行串口通信了。
下面的代码是当收到数据之后,我们再原封不动地返回回去,所以叫回声。
可以直接使用ArduinoIDE自带的串口助手来进行实验。
void setup() { // put your setup code here, to run once: Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: if(Serial.available()){ Serial.print("receive a data,is "); Serial.println(Serial.readString()); } }
操作存储器
我们可以使用ESP8266NodeMCU的EEPROM,也就是可读可擦存储器,可以掉电不丢失地帮我们存储一些数据。
需要先#include<EEPROM.h>再进行接下来的操作。
初始化
EEPROM.begin(size_t size);
初始化函数和串口一样,都是begin,我们需要使用多少字节,就需要填入多大的数值,最多是4096。如果我只需要使用一个字节,但是这个字节在最后一位,那么初始化的参数仍然需要填入4096,哪怕最后只用了一个字节。
写入操作
EEPROM.write(const int address, const uint8_t val);
第一个参数填入想要写入的是第几个字节,从0开始。
第二个参数填入写进的数据。
提交
它给我们上了个保险,写完之后我们还需要提交,提交之后才会真正地写入EEPROM中。
EEPROM.commit();
读取数据
EEPROM.read(const int address);
传入需要读取第几个字节(从0开始),然后会将读出的数据返回出来。
上述函数就足够我们对EEPROM进行操作了。
#include <EEPROM.h> void setup() { // put your setup code here, to run once: Serial.begin(9600); EEPROM.begin(10); for(int i=0;i<10;i++){ //EEPROM.write(i,0x11*i); //第一次执行写入操作 //EEPROM.commit(); Serial.println(EEPROM.read(i)); //第二次执行读出操作 } } void loop() { // put your main code here, to run repeatedly: }
经过测试我们可以知道写入的数据确实是掉电不丢失的。
WiFi连接
上面的都实操一遍过后,我相信各位小伙伴应该都能够使用ESP8266NodeMCU了,但是目前为止,我们还没有触碰到ESP8266的真正的强大之处。
ESP8266在我眼里最大的亮点就是它可以联网啊。
那么接下来,我们开始给ESP8266连上网络吧。
首先我们需要#include<ESP8266WiFi.h> 注意这边的WiFi,W和F是大写,两个i都是小写。
设置模式
WiFi.mode(WIFI_STA);
使用这个函数,并且参数填入WIFI_STA,我们的ESP8266就开启了无线终端模式,说人话就是可以连接WIFI了。
当然还有其他参数,WIFI_AP,WIFI_OFF,分别表示开启接入点模式和关闭WIFI。
接入点模式就是自己开一个WIFI,当然是不能联网的,但是别人可以通过连接ESP8266发出的WIFI来和ESP8266进行通信,这个我个人认为用的比较少,就不讲这方面的内容了。
连接WIFI
WiFi.begin()
通过这个函数我们可以连接WIFI了,一共有四种重载版本,有些参数也比较多,但是都有默认参数,不需要我们去填。简单来说,我们只需要传入参数WIFI名和密码即可。例如
WiFi.begin("WIFI name","WIFI password");
消除连接配置
WIFI.disconnect();
如果WIFI断了之后我们重连,可能WIFI会给我们分配不同的通道,但是我们ESP8266会记住上次连接的配置,因此如果是程序中的第一次连接倒是无所谓,但如果是先连接然后断开再次连接,那么第二次连接之前最好是先调用一下这个函数用来清除之前的缓存配置。不清楚的话可以参考下面的代码。
获取连接信息
WiFi.status();
调用这个函数,会给我们返回一个值来表示当前WIFI连接的状态
返回的值有很多,大家在ArduinoIDE中输入一个“WL_”,代码提示中就会显示很多。
但是我们只需要知道如果这个函数返回的是WL_CONNECTED,那么就表示连接成功,只要返回的不是WL_CONNECTED,那么我们就认为WIFI连接失败,至少是现在还没有连接上。
获取连接数据
连接成功之后,我们就可以获取到一些数据。
有个信息不用连接WIFI也可以获取,我们这里先说一下。
macAddress
WiFi.macAddress();
这个函数会返回当前这个ESP8266的mac地址,也就是唯一的物理地址,我们可以用物理地址来做一些比较私密的事,因为不会有其他设备跟你的物理地址一样。
SSID
WiFi.SSID();
这个函数会返回当前连接上的WIFI名称。可能会有小伙伴感到疑惑,WIFI不都是我们自己定的吗,我能不知道WIFI叫什么吗。其他另外有个库可以让我们定多个WIFI,然后它会选择信号最强的WIFI连接,这个时候这个函数就有用了。不过这边就不介绍那个库了,一般情况下这个就够用了。
localIP
WiFi.localIP();
这个函数会返回我们在当前网络中的本地IP。同一个网络中的设备就是靠本地IP来进行通信的。
连接
接下来我展示一下完整的WiFi连接流程,里面的函数大家直接复制就能用。
#include <ESP8266WiFi.h> const char* WIFINAME="zhetu"; //WIFI名称 const char* WIFIPASSWORD="zhetu123"; //WIFI密码 //参数一为WIFI名称,参数二为WIFI密码,参数三为设定的最长等待时间 void connectWifi(const char* wifiName,const char* wifiPassword,uint8_t waitTime){ WiFi.mode(WIFI_STA); //设置无线终端模式 WiFi.disconnect(); //清除配置缓存 WiFi.begin(wifiName,wifiPassword); //开始连接 uint8_t count=0; while(WiFi.status()!=WL_CONNECTED){ //没有连接成功之前等待 delay(1000); Serial.printf("connect WIFI...%ds\r\n",++count); if(count>=waitTime){ //超过设定的等待时候后退出 Serial.println("connect WIFI fail"); return; } } //连接成功,输出打印连接的WIFI名称以及本地IP Serial.printf("connect WIFI %s success,local IP is %s\r\n",WiFi.SSID().c_str(),WiFi.localIP().toString().c_str()); } void setup() { // put your setup code here, to run once: Serial.begin(9600); Serial.println(); connectWifi(WIFINAME,WIFIPASSWORD,10); } void loop() { // put your main code here, to run repeatedly: if(WiFi.status()!=WL_CONNECTED){ //如果WIFI断开,那么尝试重新连接 Serial.println("WIFI is break,try to connect..."); connectWifi(WIFINAME,WIFIPASSWORD,10); } }
效果也是很好的,这是我连接的自己手机开的热点,中断还特地断开了一次,也是可以重新连接上的。
MQTT
现在我们已经让ESP8266NodeMCU开发板连上网了,现在我们离通关ESP8266仅剩一步之遥啦。
单片机+联网=物联网。说到物联网,绕不开的通信协议就是MQTT,当然HTTP/HTTPS也是可以使用的,并且ESP8266也有对应的库供我们去使用,但是这里就不说了,主要还是说说怎么进行MQTT通信。
环境配置
首先我们使用PubSubClient这个库。因此我们需要去下载。
PubSubClient - Arduino ReferenceThe Arduino programming language Reference, organized into Functions, Variable and Constant, and Structure keywords.https://www.arduino.cc/reference/en/libraries/pubsubclient/到这里下载了zip包之后导入到ArduinoIDE就可以了。
然后#include<PubSubClient.h>
并且使用这个库我们还需要#include<ESP8266WiFi.h>
没错,就是我们连WIFI的那个库。
初始化
第一步我们需要建立两个对象。
WiFiClient wc; PubSubClient pc(wc);
第一个WiFiClient类型的对象是需要ESP8266WiFi那个库的。
第二个PubSubClient类型的对象是需要PubSubClient库,并且我们需要给这个对象塞一个WiFiClient类型的对象去给他初始化。
就是这么简单。
设置
第二步我们可以开始设置MQTT的服务器以及端口了。
pc.setServer("xx.xx.xx.xx",1883);
服务器有免费可以直接使用的公用MQTT服务器,大家去网上搜一下都有,端口的话MQTT基本上是1883。
就是这么简单。
连接
第三步就可以开始连接了。
pc.connect(WiFi.macAddress().c_str());
需要传入连接MQTT服务器所使用的ID,不能跟同时连接MQTT服务器的其他设备重复,一般我会使用设备的物理地址,这样肯定是不会跟别人重复。有其他的重载版本,可以设置其他连接MQTT服务器所需的信息,这里就不演示了。
会返回是否连接成功。
就是这么简单。
发布主题消息
连接成功之后我们就可以订阅和发布了。
pc.publish(topic,data);
参数一填入发布的主题名,参数二填入要发布的内容。
就是这么简单。
订阅主题
pc.subscribe(topic);
填入要订阅的主题名,这样就OK了。
就是这么简单。
但是订阅之后还没结束,订阅的消息到了之后我们去哪里取呢。
订阅回调函数
pc.setCallback(getMQTT);
填入回调函数的名字,这样一有订阅的主题发来了消息,都会自动调用一次这个回调函数。
那么这个函数该怎么知道是哪个主题发来了消息,消息有是什么呢。
那是因为这个回调函数有固定的参数格式。
void getMQTT(char* topic,byte* payload,unsigned int length)
这个函数名是我自己随意起的,你们也可以随意。重点是后面的参数类型必须统一。
第一个是发来消息的主题名。
第二个是消息的数据本体。
第三个是消息的长度。
还有一点,我们需要定时向MQTT服务器报告自己还活着,也就是发送一个报文来告诉MQTT我们还活着,也就是心跳信息,我们在连接MQTT服务器之前可以设置心跳间隔时间。
pc.setKeepAlive(n);
如果不设置的话就是默认15秒,如果在15秒内我们没有发送心跳信息的话,MQTT就会认为我们阵亡了,我们也就收不到订阅的信息了,因此我们需要在心跳间隔内频繁地发送心跳信息。
也就是下面这个函数。
pc.loop();
同时我们也有函数可以判断我们是否连接着MQTT服务器,如果连接着我们就可以发送心跳信息,如果断开了,那么我们也可以重新连接。
pc.connected();
至此,我们就已经可以拿捏MQTT了,通过下面这段代码,相信可以帮助大家加深理解,最好是大家自己去动手敲一下。
#include <ESP8266WiFi.h> #include <PubSubClient.h> const char* WIFINAME="xxx"; //WIFI名称 const char* WIFIPASSWORD="xxx"; //WIFI密码 //参数一为WIFI名称,参数二为WIFI密码,参数三为设定的最长等待时间 void connectWifi(const char* wifiName,const char* wifiPassword,uint8_t waitTime){ WiFi.mode(WIFI_STA); //设置无线终端模式 WiFi.disconnect(); //清除配置缓存 WiFi.begin(wifiName,wifiPassword); //开始连接 uint8_t count=0; while(WiFi.status()!=WL_CONNECTED){ //没有连接成功之前等待 delay(1000); Serial.printf("connect WIFI...%ds\r\n",++count); if(count>=waitTime){ //超过设定的等待时候后退出 Serial.println("connect WIFI fail"); return; } } //连接成功,输出打印连接的WIFI名称以及本地IP Serial.printf("connect WIFI %s success,local IP is %s\r\n",WiFi.SSID().c_str(),WiFi.localIP().toString().c_str()); } void getMQTT(char* topic,byte* payload,unsigned int length){ Serial.printf("get data from %s\r\n",topic); //输出调试信息,得知是哪个主题发来的消息 for(unsigned int i=0;i<length;++i){ //读出信息里的每个字节 Serial.print((char)payload[i]); //以文本形式读取就这样,以16进制读取的话就把(char)删掉 } Serial.println(); } WiFiClient wc; PubSubClient pc(wc); uint8_t connectMQTT(){ if(WiFi.status()!=WL_CONNECTED) return -1; //如果网没连上,那么直接返回 pc.setServer("xx.xx.xx.xx",1883); //设置MQTT服务器IP地址以及端口(一般固定是1883) if(!pc.connect(WiFi.macAddress().c_str())){ //以物理地址为ID去连接MQTT服务器 Serial.println("connect MQTT fail"); return -1; } String topic=WiFi.macAddress()+"-receive"; //订阅一个主题,主题名为物理地址+"-receive" pc.subscribe(topic.c_str()); pc.setCallback(getMQTT); //绑定订阅回调函数 Serial.println("connect MQTT success"); return 0; } void setup() { // put your setup code here, to run once: Serial.begin(9600); Serial.println(); connectWifi(WIFINAME,WIFIPASSWORD,10); connectMQTT(); Serial.printf("macAddress is %s",WiFi.macAddress().c_str()); //打印出物理地址,以便在MQTT客户端软件中订阅和发布调试信息 } void loop() { // put your main code here, to run repeatedly: if(WiFi.status()!=WL_CONNECTED){ //如果WIFI断开,那么尝试重新连接 Serial.println("WIFI is break,try to connect..."); connectWifi(WIFINAME,WIFIPASSWORD,10); connectMQTT(); }else{ if(pc.connected()){ //如果还和MQTT服务器保持连接 pc.loop(); //发送心跳信息 String topic=WiFi.macAddress()+"-send"; //给主题名为物理地址+"-send"的主题发送信息. pc.publish(topic.c_str(),"Hello World"); }else{ connectMQTT(); //如果和MQTT服务器断开连接,那么重连 } } delay(3000); }
可以看得出ESP8266NodeMCU是可以正常地收发MQTT数据的。
我这边用的MQTT客户端软件是MQTT.fx 1.7.1版本的(更高版本的开始收费了),大家网上都可以找得到资源。
嫌麻烦的小伙伴也可以关注我的公众号"折途想要敲代码"回复关键词“ESP8266”领取ESP8266NodeMCU开发板的相关资料,有MQTT.fx 1.7.1版本的安装包,PubSubClient的zip包,Arduino的ESP8266开发板资源包,ESP8266NodeMCU引脚图等。