基于 Arduino 玩转 UINIO-MCU-ESP32 全攻略
Arduino-ESP32
是由乐鑫科技在 GitHub
开源社区推出的一款基于 Arduino IDE
的板级支持包(BSP,Board Support
Package),除了兼容大部分通用的 Arduino
API 之外,还能够支持 ESP32
系列芯片一些独有的特性化
API。由于几年以前已经撰写过一篇基于标准 Arduino API
的《玩转 Arduino
Uno、Mega、ESP
开源硬件》,所以本篇文章不再赘述相关内容,而是结合 UINIO-Monitor、UINIO-Keyboard
等开源项目,以及
U8G2
、AsyncTimer
、RBD_BUTTON
、servo
、TFT_eSPI
、LiquidCrystal_I2C
等常用第三方库,通过例举典型的示例代码,重点介绍各类片上外设资源的实例化运用。
ESP32-C3 和 ESP32-S3 是当前市场上比较典型的两款主控方案,它们分别基于开源的 RISC-V 内核,以及商业化的 Xtensa 内核,并且同时支持 WiFi 与 Bluetooth 无线连接。由于日常工作当中经常使用到这两款微控制器,所以特意设计了 UINIO-MCU-ESP32C3 和 UINIO-MCU-ESP32S3 两款核心板,关于它们硬件电路设计方面的内容,可以参考本文的姊妹篇《UINIO-MCU-ESP32 核心板电路设计》。
Arduino IDE 2 开发环境
Arduino IDE 2 相较于之前的 1.8.19
版本,提供了更加友好的用户界面,新增了自动补全
、内置调试器
、Arduino Cloud 同步
等功能,拥有一个改进的侧边栏,使得常用的功能更加易于访问,详细用法可以查阅
Arduino 官方提供的《Arduino IDE 2
Tutorials》:
注意:Arduino IDE 创建的以
.ino
作为后缀名的源代码文件,被称为草图(Sketche)文件。
Arduino-ESP32 库概览
乐鑫科技在 GitHub 开源社区推出的 Arduino-ESP32
板级支持包,目前已经更新到 2.0.9
版本,通过向
Arduino IDE
的【开发板管理器】添加如下的开发板管理器地址
,就可以完成
Arduino-ESP32 板级支持包的安装:
- 稳定版本链接:
https://espressif.github.io/arduino-esp32/package_esp32_index.json
- 开发版本链接:
https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json
Arduino-ESP32 提供了对于 ESP32、ESP32-S2、ESP32-C3、ESP32-S3 系列芯片的支持,各个片上外设的具体兼容情况可以参见下表:
注意:所有 ESP32 系列芯片都支持 SPI 以太网,其中 RMII 只有 ESP32 能够支持。
《ESP32 Arduino 核心文档》 当中提供了如下这些 API 的使用说明,具体内容可以点击下面表格当中的链接逐一查阅:
安装完成 CH343P 的 USB 转串口驱动程序之后,就可以将 UINIO-MCU-ESP32 核心板连接至电脑,再打开 Arduino IDE 选择【ESP32C3 Dev Module】或者【ESP32S3 Dev Module】开发板,以及相应的 USB 端口,就可以完成全部的开发连接准备:
接下来,编写如下的代码,以 115200
波特率向
Arduino IDE 的【串口监视器】打印字符串
Welcome to UinIO.com
:
1 | /* 该函数只调用一次 */ |
如果 Arduino IDE 的【串口监视器】当中正确打印出了如下结果,就表明当前的开发环境已经搭建成功了:
1 | Welcome to UinIO.com |
LED 闪烁实验
发光二极管(LED,Light Emitting
Diode)在正向导通之后就会发光,对于直插式发光二极管(长脚为正,短脚为负),其红色和黄色的正向压降为
2.0V ~ 2.2V
,而绿色、白色、蓝色产生的正向压降为
3.0V ~ 3.2V
,额定工作电流介于 5mA ~ 20mA
范围之间。接下来以红色发光二极管为例,介绍其限流电阻的计算方法。
首先,红色 LED 正常工作时产生的压降约为 2.0V
,而 ESP32
引脚输出的高电平为 3.3V
,此时限流电阻上流过的电压等于
3.3 - 2.0 = 1.3V
,而红色发光二极管的额定电流约为
10mA
,所以这个限流电阻的取值应当为 \(\frac{1.3V}{0.01A} =
130Ω\),这里近似的取电阻标称值为
120Ω
,并且将其连接到 Arduino-MCU-ESP32 的
GPIO0 引脚,具体的电路连接关系如下图所示:
注意:ESP32 系列芯片高电平信号的最低电压值为
3.3V × 0.8 = 2.64V
,而低电平信号的最高电压值为3.3V × 0.1 = 0.33V
。
pinMode(pin, mode)
:配置引脚工作模式,其中mode
参数可选的值有INPU
、OUTPUT
、INPUT_PULLUP
、INPUT_PULLDOWN
;digitalWrite(pin, value)
:设置数字输出引脚的电平状态,其中value
参数可选的值是HIGH
或者LOW
;delay(ms)
:延时函数,其参数ms
的单位为毫秒;
1 | int LED_Pin = 0; |
由于使用 delay()
延时函数会阻塞后续任务的执行,所以这里改用如下两个
API,通过循环计算时间差值的方式来实现 LED 灯的闪烁:
millis()
:程序当前运行的毫秒数;micros()
:程序当前运行的微秒数;
下面的示例代码通过 UINIO-MCU-ESP32 的
GPIO0
引脚控制一个 LED 灯,每间隔 1
秒循环不断的进行闪烁:
1 | int LED_Pin = 0; |
如果需要控制多个 LED
的闪烁,则需要将电路连接关系修改为下面的样子,此时控制引脚需要变更为
UINIO-MCU-ESP32 的 GPIO1
和
GPIO2
:
注意需要同步修改代码当中控制引脚变量 LED_Pin_x
的值,其它的功能代码只需要进行相应的复制粘贴即可:
1 | int LED_Pin_1 = 1; // 将 LED 1 的控制引脚设置为 GPIO1 |
按键控制 与 RBD_BUTTON 库
本示例需要将 UINIO-MCU-ESP32 的 GPIO3
和 GPIO4
分别连接至 LED
和按键:
由于按键的控制引脚被配置为输入上拉
INPUT_PULLUP
,所以当按键被按下时低电平有效,读取引脚的电平状态需要使用到如下的
API:
digitalRead(pin)
:读取指定输入引脚pin
的电平状态,返回值是HIGH
或者LOW
;
1 | int LED_Pin = 3; // LED 控制引脚 |
观察上述代码的运行结果,可以发现按键对于 LED
亮灭状态的控制并不准确,这是由于按键在按下时,触点的接触不够稳定所导致。在这里我们可以方便的借助
RBD_BUTTON
这款第三方库来消除这种抖动。接下来在 Arduino IDE
当中安装 RBD_Button 以及关联的
RBD_Timer 依赖库,由于该库所提供的
Button
类位于 C++ 的RBD
命名空间当中,所以其构造函数的调用形式应当书写为:
1 | RBD::Button constructor(pin,[input, input_pullup, input_pulldown]) |
Button
类当中提供了如下一系列可以用于消除按键抖动的方法:
button.isPressed()
:当按键被按下或开启时返回true
,否则返回false
;button.isReleased()
:当按键弹起或者释放时返回true
,否则返回false
;button.onPressed()
:当按钮被按下(已经去除抖动)一次以后返回true
,接下来必须释放按钮,并且再次按下才能够返回true
;button.onReleased()
:当按钮被释放(已经去除抖动)一次以后返回true
,接下来必须按下按钮,并且再次释放才能够返回true
;button.setDebounceTimeout(value)
:设置消除抖动的时间,参数的单位为毫秒;
修改前面的示例代码,加入按键消抖的处理逻辑,可以看到在消除抖动错误的同时,代码的书写也得到了极大简化:
1 |
|
LEDC 与 PWM
LED 发光二极管的正常工作电压介于
1.8V ~ 2.0V
之间,由于该电压变化区间的取值范围较小,难以通过电压大小来控制 LED
的亮度。而脉冲宽度调制(PWM,Pulse
Width
Modulation)则另辟蹊径,通过改变输出方波的占空比来控制
LED 的亮灭频率,从而达到调整亮度的目的。
ESP32-C3 和 ESP32-S3 分别拥有 6 和 8 个通道,用于产生独立的波形信号,最大精度为 14 位。Arduino-ESP32 提供了专门的 LED 控制 API(LEDC,LED Control),可以方便的以 PWM 方式来控制 LED 的亮度,具体的 API 方法可以参考下面的列表:
API | 功能描述 |
---|---|
uint32_t ledcSetup(uint8_t channel, uint32_t freq, uint8_t resolution_bits); |
用于设置 LEDC 通道的频率和分辨率; |
void ledcWrite(uint8_t chan, uint32_t duty); |
设置指定 LEDC 通道的占空比; |
uint32_t ledcRead(uint8_t chan); |
获取指定 LEDC 通道的占空比; |
uint32_t ledcReadFreq(uint8_t chan); |
获取指定 LEDC 通道的频率; |
uint32_t ledcWriteTone(uint8_t chan, uint32_t freq); |
用于在指定频率上将 LEDC 通道设置为
50% 占空比的 PWM 音调; |
uint32_t ledcWriteNote(uint8_t chan, note_t note, uint8_t octave); |
用于将 LEDC 通道设置为指定的音符; |
void ledcAttachPin(uint8_t pin, uint8_t chan); |
用于将指定的 GPIO 引脚绑定至 LEDC 通道; |
void ledcDetachPin(uint8_t pin); |
用于取消指定的 GPIO 引脚与 LEDC 通道的绑定; |
uint32_t ledcChangeFrequency(uint8_t chan, uint32_t freq, uint8_t bit_num); |
用于动态改变 LEDC 通道的频率; |
void analogWrite(uint8_t pin, int value); |
用于在指定 GPIO
引脚上写入模拟值(PWM 波形信号),该接口兼容 Arduino
官方的 analogWrite() 函数; |
void analogWriteResolution(uint8_t bits); |
用于设置所有 analogWrite()
通道的分辨率; |
void analogWriteFrequency(uint32_t freq); |
用于设置所有 analogWrite()
通道的频率; |
下面的示例代码将 LEDC 配置为 0
通道,工作频率为 5000
赫兹,精度为 12
位(即将一个周期划分为 \(2^{12}\) 等分)。如果需要将其占空比调整为
50%
,那么高电平就需要占据 \(2^{12} \div 2 = 2^{12 - 1} = 2^{11}\)
等分:
1 | void setup() { |
接下来再利用 LEDC 和 PWM
实现一个呼吸灯效果,具体策略为每秒钟调整占空比 50
次,假设
T
为呼吸周期,那么 LED 从熄灭到最高亮度需要经过的时间为
\(\frac{T}{2}\)(即半个呼吸周期)。这样每半个周期就需要进行
\(50 \times \frac{T}{2}\)
次占空比调整,而 count
表示占空比为 100%
时候的等分数量,step
就是每次占空比调整所需要增加的步进值
\(step = \frac{count}{50 \times \frac{T}{2}} =
2 \times \frac{count}{50 \times T}\),当占空比超过
Count
时,就需要逐步将 Step
步进值递减至
0
:
1 | int GPIO4 = 4; // 指定 GPIO 引脚 4 |
上面代码当中的 delay()
函数会阻塞
UINIO-MCU-ESP32 的后续代码运行,下面通过
prevTime
和 curTime
两个变量来循环计算时间差值,实现一个非阻塞式的呼吸灯:
1 | int GPIO4 = 4; // 指定 GPIO 引脚 4 |
软件定时器 与 AsyncTimer 库
ESP32-C3 和 ESP32-S3 分别拥有
2
个和 4
个硬件定时器,虽然它们的精度较高,但是数量着实有限。在一些对于精度要求不高的场合,可以考虑使用诸如
AsyncTimer
这样的第三方库来作为软件定时器使用,它适用于一些对于精度要求不高的场合(精度为毫秒级别),具体的使用步骤如下面所示:
- 首先,在 Arduino IDE
的【库管理器】当中安装
AsyncTimer
库; - 然后,在工程代码当中包含头文件
#include <AsyncTimer.h>
; - 接下来,声明定时器变量
AsyncTimer timer
; - 最后,在
void loop()
函数当中调用t.handle()
;
下面的示例代码,会通过 AsyncTimer 提供的
setTimeout()
函数,分别延时 3
秒和
5
秒向串口打印提示信息:
1 |
|
同样的,可以通过类似的方式调用 AsyncTimer 的
setInterval()
函数,周期性的不断重复向串口打印提示信息:
1 |
|
注意:注意每次调用
setTimeout()
和setInterval()
之后返回的 ID 值都并不相同。
接下来,结合前面介绍的 RBD_Button 和 AsyncTimer 两个第三方库,让一个 LED 在刚开始启动的时候,每间隔 1 秒钟进行闪烁,而在按下按键之后,再切换至间隔 3 秒进行闪烁,再次按下按键则切换回间隔 1 秒进行闪烁,这里依然沿用之前的按键与 LED 实验电路:
1 |
|
ADC 模数转换
模数转换器(ADC,Analog to Digital Converter)是一种常见外设,用于将模拟信号转换为便于 ESP32 微控制器,读取与处理的数字信号。
- ESP32-C3 集成有两个 12 位的逐次逼近寄存器型(SAR, Successive Approximation Register)ADC,一共支持 6 个模拟通道输入,其中 ADC1 支持 5 个模拟通道输入(已工厂校准),而ADC2 支持 1 个模拟通道输入(未工厂校准);
- ESP32-S3 同样集成有两个 12 位逐次逼近寄存器型 ADC,一共拥有 20 个模拟输入通道,乐鑫官方推荐优先使用 ADC1;
Arduino-ESP32 当中针对 ADC 外设,提供了如下一系列通用的 API 函数:
Arduino 通用的 ADC API | 功能描述 |
---|---|
uint16_t analogRead(uint8_t pin); |
获取指定引脚或者 ADC 通道的原始值。 |
uint32_t analogReadMilliVolts(uint8_t pin); |
获取指定引脚或者 ADC 通道的原始值(以毫伏为单位)。 |
void analogReadResolution(uint8_t bits); |
设置 analogRead()
返回值的分辨率,ESP32S3 默认为 13
位(从 0 到 8191 ),其它型号默认为
12 位(从 0 到 4095 )。 |
void analogSetClockDiv(uint8_t clockDiv); |
设置 ADC 时钟的分频器,范围为
0 ~ 255 ,默认值为 1 。 |
void analogSetAttenuation(adc_attenuation_t attenuation); |
设置全部通道的衰减系数,共拥有
ADC_ATTEN_DB_0 、ADC_ATTEN_DB_2_5 、ADC_ATTEN_DB_6 、ADC_ATTEN_DB_11
四个选项。 |
void analogSetPinAttenuation(uint8_t pin, adc_attenuation_t attenuation); |
设置指定引脚或者 ADC 通道的衰减系数。 |
bool adcAttachPin(uint8_t pin); |
将 GPIO 引脚关联至 ADC,关联成功返回
true ,否则返回 false 。 |
ADC 衰减系数 | ESP32-C3 可测量输入电压范围 | ESP32-S3 可测量输入电压范围 |
---|---|---|
ADC_ATTEN_DB_0 |
0 mV ~ 750 mV | 0 mV ~ 950 mV |
ADC_ATTEN_DB_2_5 |
0 mV ~ 1050 mV | 0 mV ~ 1250 mV |
ADC_ATTEN_DB_6 |
0 mV ~ 1300 mV | 0 mV ~ 1750 mV |
ADC_ATTEN_DB_11 |
0 mV ~ 2500 mV | 0 mV ~ 3100 mV |
注意:ESP32S3 的最高采样分辨率为 13 位,由于计数范围从
0
开始进行计数,所以其最大计数值为 \(2^{13} - 1 = 8191\),同理 ESP32S3 的最大计数值等于 \(2^{12} - 1 = 4095\)。
ESP32 专用的 ADC API | 功能描述 |
---|---|
void analogSetWidth(uint8_t bits); |
设置硬件采样分辨率,取值范围为
9 ~ 12 ,默认值是 12 ; |
void analogSetVRefPin(uint8_t pin); |
设置需要进行 ADC 校准的引脚。 |
int hallRead(); |
读取连接至 36 (SVP)和
39 (SVN)引脚的霍尔传感器 ADC 值。 |
接下来通过 ADC 完成一个实验,使用电位器调整 UINIO-MCU-ESP32 的 ADC 引脚所读取到的输入电压,然后根据这个输入电压的大小,调节 GPIO 引脚输出信号的占空比,从而达到调整 LED 亮度的目的,UINIO-MCU-ESP32 的电路连接关系如下图所示:
可以看到,这里把 UINIO-MCU-ESP32 的
GPIO2
引脚连接至电位器,而 GPIO1
作为 LED
发光二极管的控制引脚,接着编写并且上传如下的控制逻辑代码:
1 |
|
I²C 总线通信
内部集成电路总线(I²C,Inter-Integrated
Circuit)是一种低速串行通信协议(标准模式
100 Kbit/s
,快速模式 400 Kbit/s
),采用
SDA
(串行数据线)和
SCL
(串行时钟线)两线制结构(需要使用上拉电阻),分别可以连接多个设备,每个设备都拥有唯一的
7 位地址(最多 128
个设备)。Arduino-ESP32 的 I²C 库实现了 Arduino
Wire 官方库当中的如下一系列 API 函数:
I²C 通用 API | 功能描述 |
---|---|
bool begin(); |
基于默认参数配置 I²C
外设,正确初始化之后返回 true 。 |
bool setPins(int sdaPin, int sclPin); |
用于定义 SDA 和
SCL 引脚,两个参数的默认值分别为 GPIO21 和
GPIO22 。 |
bool setClock(uint32_t frequency); |
设置 I²C
总线的时钟频率,默认为 100KHz 。 |
uint32_t getClock(); |
获取 I²C 总线的时钟频率。 |
void setTimeOut(uint16_t timeOutMillis); |
设置 I²C 总线超时时间(毫秒)。 |
void setTimeOut(uint16_t timeOutMillis); |
获取 I²C 总线超时时间(毫秒)。 |
size_t write(const uint8_t *, size_t); |
将数据写入到总线缓冲区,返回值为写入数据的大小。 |
bool end(); |
完成 I²C 通信并且释放之前所有被分配的外设资源。 |
Arduino-ESP32 当中的 I²C 总线可以分别运行于主设备(I²C Master Mode)和从设备(I²C Slave Mode)两种不同的工作模式:
I²C 主设备模式:该模式用于向从设备发起通信,由主设备发出时钟信号,并且负责发起与从设备的通信。
I²C 从设备模式:时钟信号依然由主设备产生,如果 I²C 地址与从设备匹配,那么这个从设备就会响应主设备。
I²C 主设备模式
下面的表格展示了 I²C 总线工作在主设备模式下时所使用到的 API:
I²C 主设备模式 API | 功能描述 |
---|---|
bool begin(int sdaPin, int sclPin, uint32_t frequency) |
指定 I²C 总线的 SDA 和
SCL 引脚,以及通信频率。 |
void beginTransmission(uint16_t address) |
开始启动与指定 I²C 地址从设备的通信。 |
uint8_t endTransmission(bool sendStop); |
将数据写入至缓冲区以后,使用该函数把数据发送给从设备,参数
sendStop 用于使能 I²C 总线停止信号。 |
uint8_t requestFrom(uint16_t address, uint8_t size, bool sendStop) |
要求从设备向主设备发送响应数据。 |
上述 API 函数的基本使用步骤如下面的列表所示:
#include "Wire.h"
,包含Wire.h
头文件;Wire.begin()
,开始配置 I²C 总线;Wire.beginTransmission(I2C_DEV_ADDR)
,指定 I²C 从设备地址,开始进行数据传输;Wire.write(x)
,把数据写入到缓冲区;Wire.endTransmission(true)
,将缓冲区的全部数据写入至从设备;Wire.requestFrom(I2C_DEV_ADDR, SIZE)
,请求读取指定从设备的数据;Wire.readBytes(temp, error)
,开始读取从设备响应的数据;
下面是一个如何在主设备模式下使用 I²C 总线的示例代码:
1 |
|
I²C 从设备模式
下面的表格展示了 I²C 总线工作在从设备模式下时所使用到的 API:
I²C 从设备模式 API | 功能描述 |
---|---|
bool Wire.begin(uint8_t addr, int sdaPin, int sclPin, uint32_t frequency) |
在从设备模式下,必须通过传递从设备的地址来调用
begin() 函数。 |
void onReceive( void (*)(int) ) |
定义从设备接收主设备数据的回调函数。 |
void onRequest( void (*)(void) ) |
定义从设备请求主设备数据的回调函数。 |
size_t slaveWrite(const uint8_t *, size_t) |
接收到响应数据之前,该函数用于向从设备的缓冲区写入数据。 |
上述 API 函数的基本使用步骤如下面的列表所示:
#include "Wire.h"
,包含Wire.h
头文件;Wire.onReceive(onReceive)
和Wire.onRequest(onRequest)
,创建两个回调函数来接收或者请求主设备的数据;Wire.begin((uint8_t)I2C_DEV_ADDR);
,使用指定的地址配置 I²C 总线;Wire.slaveWrite((uint8_t *)message, strlen(message));
,预先向从设备的缓冲区写入数据;
下面是一个如何在从设备工模式下使用 I²C 总线的示例代码:
1 |
|
主从设备通信实例
接下来,以 UINIO-MCU-ESP32S3 作为主设备,而
UINIO-MCU-ESP32C3 作为从设备(I²C 地址为
55
),两者的 SDA
和 SCK
都分别指定为为 GPIO5
和 GPIO6
,并且在从设备的
GPIO8
上面连接一枚 LED:
UINIO-MCU-ESP32S3 作为主设备,每间隔 2
秒就会向从设备 UINIO-MCU-ESP32C3
发送一个递增的数值,从设备接收到主设备的数据之后
LED 就会闪烁 0.5 秒,并且在收到的数值后面添加 已经被接收
字样,然后返回给主设备打印至串口,具体的示例代码如下面所示:
1 | /* UINIO-MCU-ESP32S3 主设备程序 */ |
如果主设备 requestFrom()
所指定的
quantity
参数的数据量,大于从设备发送过来的数据量,那么多出的空间将会由
0xff
进行填充。
1 | /* UINIO-MCU-ESP32C3 从设备程序 */ |
基于 Arduino 玩转 UINIO-MCU-ESP32 全攻略