1. Arduino程序结构
Arduino编程的代码结构:/** * ESP8266 Arduino程序结构 * @author SXXZY * @date 2020/11/22 */void setup() { // 这里开始写初始化代码,只会执行一次 }void loop() { //这里写运行代码,重复执行}
对于习惯c语言编程的读者,以上代码又可以抽象成以下伪代码结构:
/** * ESP8266 Arduino程序伪代码结构 * @author SXXZY * @date 2020/11/22 */void main(){ watchdogEnable();//启动看门狗 setup();//初始化函数 while(1){ loop();//业务代码函数 }}
代码解析1.在ESP8266 Arduino编程中,默认会开启看门狗功能,也就是对应伪代码的watchdogEnable(),意味着我们需要适当喂狗,不然会触发看门狗复位;2.setup()方法:初始化函数,只会运行一次,所以一般情况下,我们都会在这里配置好初始化参数,比如IO口模式、串口波特率设置等等;3.loop()方法:不断重复执行,这里编写我们的业务代码,同时要注意执行喂狗操作。
2. 计时和延时(Timing and delays)
时间控制,基本上可以说存在于每一个项目代码中。目前在Arduino中跟时间控制有关的方法包括以下几个:
delay(ms)
暂停一个给定的毫秒数的时间间隔。
delayMicroseconds(us)
暂停一个给定的微秒数的时间间隔。
millis()
返回重启(reset)后所经过的毫秒数。
micros()
返回重启(reset)后所经过的微秒数
温馨提示
通常,我们控制LED灯闪烁都会加上一个delay延时来达到切换亮灭时间长度。但是delay有个缺点就是:在给定的时间间隔内是不能做其他操作,这样对于一些需要响应按键操作的场景就不适用了。那么有没有什么办法既能延时又能不影响其他操作呢?当然,这就是millis()的妙用,通过获取两个时间点的毫秒数,然后计算它们的差值,差值时间间隔内是可以执行其他操作的。代码片段如下:
long debouncdDelay = 60;//延时间隔long lastDebounceTime = 0; //最近记录的一次时间// 判断时间间隔是否大于设定的时间间隔。if(millis()-lastDebounceTime>debouncdDelay){ lastDebounceTime = millis();}
3. NodeMcu 端口映射
接下来,先了解一下NoodeMcu的实物图,如下图: 同时,读者也需要知道ESP8266-12F与NodeMcu的端口映射关系,如下图:可以看出:
1.中间的DEVKIT部分,就是NodeMcu提供给外界的端口,对应实物图上标注的端口名称;
2.除开中间部分,其他部分基本上对应ESP8266引脚,以不同颜色块来区分不同功能;
温馨提示
NodeMcu上的CLK、SD0、CMD、SD1、SD2引脚,是用于连接外接flash芯片,不应该用于连接其他模块,悬空即可,以防程序奔溃。
或许笔者会觉得看图有点复杂,所以笔者总结了下面的GPIO引脚映射表,以供参考:
从上面表格可以看出,我们大约11个GPIO引脚可用。而11个中的2个引脚通常被保留用于RX和TX,以便进行串口通信。因此最后,只剩下8个通用I / O引脚,即D0到D8(除开D3特殊用途)。温馨提示
请注意,D0 / GPIO16引脚只能用作GPIO读/写,不支持特殊功能。
4. 数字IO(Digital IO)
上面说到,ESP8266-12F(也可以大胆说ESP8266-12系列)最终只剩下8个通用的I/O引脚以供我们使用,即是NodeMcu上的D0-D8(除D3之外)。
Arduino中的引脚号直接与ESP8266 GPIO的引脚号对应通信。pinMode/digitalRead/digitalWrite函数不变,所以要读取GPIO2,可调用digitalRead(2)。除了D0可以设置为INPUT(输入)、OUTPUT(输出)或者INPUT_PULLDOWN(输入,默认下拉,也就是低电平),剩余的数字IO引脚可以设置为INPUT(输入)、OUTPUT(输出)或者INPUT_PULLUP(输入,默认上拉,也就是高电平)。
下面,将在NodeMcu的D4引脚上写一个LED Blink的Arduino草图:
/** * LED灯闪烁实验 */void setup() { pinMode(D4, OUTPUT); // 初始化D4引脚为输出引脚} void loop() { digitalWrite(D4, LOW); // 亮灯 delay(1000); // 延时1s digitalWrite(D4, HIGH);// 灭灯 delay(1000); // 延时1s}
5.模拟输入(ADC)
学过模拟电路或者数字电路的人都会听过ADC,它又叫做模数转换器,用于将模拟信号转换成可视化的数字形式。ESP8266具有内置的10位ADC,只有一个ADC通道,即只有一个ADC输入引脚可读取来自外部器件的模拟电压。ESP8266上的ADC通道和芯片供电电压复用,也就是说我们可以将其设置为测量系统电压或者外部电压。
5.1 测量外部电压
相关方法
analogRead(A0),用于读取施加在模块的ADC引脚上的外部电压;
输入电压范围
0 - 1.0V之间;
测量精度
由于ADC具有10位分辨率,因此会给出0-1023的值范围;
注意点
为了支持外部电压范围(0-3.3v),NodeMcu做了一个电阻分压器,如图所示:
例程 编写一个读取NodeMcu的ADC引脚上的模拟电压。我们这里使用电位器在ADC引脚上提供0-3.3V的可变电压。如下图连接线:
代码如下:
/** * 功能描述:ESP8266 ADC 读取外部电压 * 在串口调试器查看效果 */void setup() { Serial.begin(115200);//配置波特率} void loop() { Serial.print("ADC Value: "); Serial.println(analogRead(A0));//输出0-1023 对应 外部输入电压 0-1.0v //延时1s delay(1000);}
5.2 测量系统电压
相关方法
ESP.getVcc(),读取NodeMCU模块的VCC电压,单位是mV;
注意点
ADC引脚必须保持悬空;在读取VCC电源电压之前,应更改ADC模式以读取系统电压。
要ADC_MODE(mode)在#include行后面改变ADC模式。
模式是ADC_TOUT(对于外部电压),ADC_VCC(对于系统电压)。默认情况下,它读取外部电压。
例程
编写ESP8266读取系统电压,代码如下:
/** * 功能描述:ESP8266 ADC 读取系统电压 * 在串口调试器查看效果 */ADC_MODE(ADC_VCC);//设置ADC模式为读取系统电压 void setup() { Serial.begin(115200);} void loop() { Serial.print("ESP8266当前系统电压(mV): "); Serial.println(ESP.getVcc()); delay(1000);}
6. 模拟输出(PWM)
PWM(Pulse Width Modulation,脉宽调制),是在保持波的频率不变的同时改变脉宽的技术。当我们需要连续控制电压变化,实现呼吸灯或者电机转速的时候,就要用到PWM,如下图。
首先,我们来理解一下占空比。一个脉冲周期由一个ON周期(VCCC)和一个OFF周期(GND)组成。一段时间内ON周期占据脉冲周期的比例就叫做占空比。DutyCycle(percentage)=Ton/TotalPeriodX100
例如,一个10ms的脉冲保持ON 2ms,那么根据公式,占空比是20%。
注意点
脉冲频率一般都是固定的,跟占空比没有关系。
NodeMcu PWM引脚
如下图,标注PWM引脚。
基本上数字IO都可以作为PWM复用引脚,除了D0。不过需要注意的是,D3尽量不用,它内部连接ESP8266 GPIO0。NodeMcu PWM有关Arduino函数
1.analogWrite()
该功能用于在指定的引脚上启用软件PWM。
函数:analogWrite(pin,val)
参数:
pin:要启用软件PWM的GPIO引脚。
val:数值,一般在0到PWMRANGE范围,默认PWMRANGE是1023。
返回值:无;
注意点:
analogWrite(pin, 0)用于禁用指定引脚上的PWM。
2.analogWriteRange()
该功能用于改变PWMRANGE数值。
函数:analogWriteRange(new_range)
参数:
new_range:新的PWMRANGE数值。
返回值:无;
注意点:
可以理解为PWM精度范围。同样的PWM频率下,默认占空数值0-123。如果你改变PWMRANGE为2047,那么占空数值就变成0-2047。精度高了一倍。
3.analogWriteFreq()
该功能用于改变PWM频率。
函数:analogWriteFreq(new_frequency)
参数:
new_frequency:新PWM频率,默认是1kHZ。
返回值:无;
注意点:
百度上很多资料都说PWM频率范围为1-1KHz。但是通过查看源码,如下:
static uint16_t analogFreq = 1000;extern void __analogWriteFreq(uint32_t freq) { if (freq < 100) { analogFreq = 100; } else if (freq > 40000) { analogFreq = 40000; } else { analogFreq = freq; }}
可以看出,Arduino For ESP8266的PWM频率范围应该是100Hz-40KHz。
PWM例程
呼吸灯,LED灯明暗连续变化。代码如下:/** * 功能描述:ESP8266 PWM演示例程 * @author SXXZY * @date 2020/11/22 */ #define PIN_LED D6 void setup() { // 这里开始写初始化代码,只会执行一次 pinMode(PIN_LED,OUTPUT); analogWrite(PIN_LED,0);}void loop() { //这里写运行代码,重复执行 for(int val=0;val<1024;val++){ //占空比不断增大 亮度渐亮 analogWrite(PIN_LED,val); delay(2); } for(int val=1023;val>=0;val--){ //占空比不断变小 亮度渐暗 analogWrite(PIN_LED,1023); delay(2); }}
7. 串口通信(Serial)
ESP8266的串口通信与传统的Arduino设备完全一样。除了硬件FIFO(128字节用于TX和RX)之外,硬件串口还有额外的256字节的TX和RX缓存。发送和接收全都由中断驱动。当FIFO/缓存满时,write函数会阻塞工程代码的执行,等待空闲空间。当FIFO/缓存空时,read函数也会阻塞工程代码的执行,等待串口数据进来。NodeMcu上有两组串口,Serial和Serial1。
Serial使用UART0,默认对应引脚是GPIO1(TX)和GPIO3(RX)。在Serial.begin执行之后,调用Serial.swap()可以将Serial重新映射到GPIO15(TX)和GPIO13(RX)。再次调用Serial.swap()将Serial重新映射回GPIO1和GPIO3。不过,一般情况下,默认就好。
串口映射例程
/** * 功能描述:ESP8266 Serial映射例程 * @author SXXZY * @date 2020/11/22 */ void setup() { // 这里开始写初始化代码,只会执行一次 Serial.begin(115200); Serial.println("GPIO1(TX),GPIO3(RX)"); //调用映射方法 Serial.swap(); Serial.println("GPIO15(TX),GPIO13(RX)"); //重新映射回来 Serial.swap(); Serial.println("GPIO1(TX),GPIO3(RX)");}void loop() { //这里写运行代码,重复执行}
Serial1使用UART1,默认对应引脚是GPIO2(TX)。Serial1不能用于接收数据,因为它的RX引脚被用于flash芯片连接。要使用Serial1,请调用Serial.begin(baudrate)。代码如下:
/** * 功能描述:ESP8266 串口例程 * @author SXXZY * @date 2020/11/22 */ void setup() { // 这里开始写初始化代码,只会执行一次 Serial.begin(115200); Serial.println("Hello Serial"); Serial1.begin(115200); Serial1.println("Hello Serial1");}void loop() { //这里写运行代码,重复执行}
如果不使用Serial1并且不映射串口,可以将UART0的TX映射到GPIO2,具体操作是:在Serial.begin()之后调用Serial.set_tx(2)或者直接调用Serial.begin(baud,config,mode,2)。
默认情况下,当调用Serial.begin后,将禁用WiFi库的诊断输出。要想再次启动调试输出,请调用Serial.setDebugOutput(true)。要将调试输出映射到Serial1时,需要调用Serial1.setDebugOutput(true)。
调用Serial.setRxBufferSize(size_t size)允许定义接收缓冲区的大小,默认值是256(缓冲区也是使用内存,意味着不能一味地去增大这个值)。
Serial和Serial1对象都支持5,6,7,8个数据位,奇数(O)、偶数(E)和无(N)奇偶校验,以及1或者2个停止位。要设置所需的模式,请调用Serial.begin(baudrate, SERIAL_8N1), Serial.begin(baudrate, SERIAL_6E2)等。
Serial和Serial1都实现了一种新方法用来获取当前的波特率设置。要获取当前的波特率,请调用Serial.baudRate(),Serial1.baudRate()。代码如下:
/** * 功能描述:ESP8266 串口波特率例程 * @author SXXZY * @date 2020/11/22 */ void setup() { // 这里开始写初始化代码,只会执行一次 // 设置当前波特率为57600 Serial.begin(57600); // 获取当前波特率 int br = Serial.baudRate(); // 将打印 "Serial is 57600 bps" Serial.printf("Serial is %d bps", br);}void loop() { //这里写运行代码,重复执行}
Serial和Serial1都属于硬件串口(HardwareSerial)的实例,如果读者需要使用ESP8266 软件串口的功能,请参考以下库:https://github.com/plerup/espsoftwareserial。
为了检测进入Serial的未知波特率的数据,可以调用Serial.detectBaudrate(time_t timeoutMillis)。这个方法尝试在timeoutMillis ms的时间内检测波特率,检测成功返回波特率,检测失败返回0。detectBaudrate()方法在Serial.begin()被调用之前调用(因为它不需要用到接收缓冲区或者串口配置),并且它不能检测数据位位数或者停止位。这个检测过程不会去改变数据的波特率,所以可以在检测成功之后,调用Serial.begin(detectedBaudrate)。
串口用处
一般来说,串口通信用在两个方面:
1.与外围串口设备传输数据,比如蓝牙模块、Arduino等等;
2.开发过程中用来调试代码,通过串口输出Debug信息了解程序运行信息。例程如下:
/** * Demo1: * statin模式下,创建一个连接到可接入点(wifi热点),并且打印IP地址 * @author SXXZY * @date 2020/11/22 */#include #define AP_SSID "xxxxx" //这里改成你的wifi名字#define AP_PSW "xxxxx"//这里改成你的wifi密码//以下三个定义为调试定义#define DebugBegin(baud_rate) Serial.begin(baud_rate)#define DebugPrintln(message) Serial.println(message)#define DebugPrint(message) Serial.print(message) void setup(){ //设置串口波特率,以便打印信息 DebugBegin(115200); //延时2s 为了演示效果 delay(2000); DebugPrintln("Setup start"); //启动STA模式,并连接到wifi网络 WiFi.begin(AP_SSID, AP_PSW); DebugPrint(String("Connecting to ")+AP_SSID); //判断网络状态是否连接上,没连接上就延时500ms,并且打出一个点,模拟连接过程 //笔者扩展:加入网络一直都连不上 是否可以做个判断,由你们自己实现 while (WiFi.status() != WL_CONNECTED){ delay(500); DebugPrint("."); } DebugPrintln(""); DebugPrint("Connected, IP address: "); //输出station IP地址,这里的IP地址由DHCP分配 DebugPrintln(WiFi.localIP()); DebugPrintln("Setup End");} void loop() {}
8. 总结
总体上讲,基础内容比较多,介绍ESP8266在Arduino平台上的一些基础知识点,包括程序结构、NodeMcu端口映射、ESP8266 数字IO、PWM、ADC、串口通信等等。