
注:本笔记📚为个人记录学习所用,主要通过再记录方式学习ESP32,参考和搬运转载极客侠 GeeksMan的文章。
著作权归极客侠 GeeksMan所有 基于GPL 3.0协议 原文链接:https://docs.geeksman.com/esp32/Arduino/01.esp32-arduino-intro.html
下面列出了一些常用的 Arduino 函数:
当使用 Arduino 进行编程时,有许多内置函数可用。这些函数可以帮助我们更轻松地编写程序,处理输入和输出,控制逻辑流和实现其他功能。下面是一些常用的 Arduino 函数:
pinMode(pin, mode): 用于配置数字引脚的输入或输出模式。pin 是数字引脚的编号,mode 是要设置的模式(输入或输出)。**ed:pinMode(led_pin, OUTPUT);digitalWrite(pin, value): 用于在数字引脚上写入数字值(HIGH 或 LOW)。pin 是数字引脚的编号,value 是要写入的值。**ed: digitalWrite(led_pin, HIGH);digitalRead(pin): 用于读取数字引脚上的数字值(HIGH 或 LOW)。pin 是数字引脚的编号。**ed: digitalRead(button_pin);analogRead(pin): 用于读取模拟引脚上的模拟值(0-1023)。pin 是模拟引脚的编号。analogWrite(pin, value): 用于在支持 PWM 输出的数字引脚上输出模拟值(0-255)。pin 是数字引脚的编号,value 是要输出的值。delay(ms): 用于在程序中创建暂停(延迟)时间。ms 是要延迟的毫秒数。millis(): 返回自启动以来的毫秒数,可以用于时间跟踪和计时器。Serial.begin(baud): 用于初始化串口通信,其中 baud 是波特率。**ed: Serial.begin(9600);Serial.print(data): 用于将数据打印到串口监视器。data 可以是数字,字符串或其他数据类型。**ed: Serial.print("Hello World!");Serial.available(): 用于检查是否有数据可以从串口读取。这些函数只是 Arduino 可用的众多函数中的一部分。熟悉这些常用函数可以帮助我们更轻松地编写程序,并为实现特定功能提供了有用的工具。

注:宽条垂直连接相通,窄条横向连接相通
ESP32 开发板上有一类引脚叫 GPIO 引脚, 负责输入/输出电压。开发板上 D 开头的引脚都是这种引脚, 比如 D2、D4、D15 等等。
什么是电平?
电路上某点的电压(对公共参考点)或电位是高还是低。比如在逻辑电路中,高于某个数值的电位称其为高电位,或高电平,低于某个数值的,为低电位或低电平。比如 ESP32 中,高电平的数值大于2.5V,低电平的数值小于0.5V,具体的数值最好通过测试研究来确定。
LED(light-emitting diode) 即发光二极管。通过 5mA 左右电流即可发光,电流越大,其亮度越强,但若电流过大,会烧毁二极管,一般我们控制在 3mA-20mA 之间,通常我们会在 LED 管脚上串联一个电阻,目的就是为了限制通过发光二极管的电流不要太大,因此这些电阻又可以称为限流电阻。当发光二极管发光时,测量它两端电压约为 1.7V,这个电压又叫做发光二极管的导通压降。
发光二极管正极又称阳极,负极又称阴极,电流只能从阳极流向阴极。直插式发光二极管长脚为阳极,短脚为阴极。
注:长阳短阴
LED 的正极接开发板的 D12 引脚,并串联一个电阻,负极接 GND,如下图:

注意
一定要接电阻,不然会由于电流过大,烧坏 LED。
想要点亮这颗 LED 的话,只需要先设定相关引脚为输出模式,然后给这个引脚赋值一个高电平即可。
// 设置 LED 引脚为12
int led_pin = 12;
void setup() {
// 设置LED的引脚模式为输出模式
pinMode(led_pin, OUTPUT);
// 点亮 LED,在LED的数字引脚上写入数字值为高电平
digitalWrite(led_pin, HIGH);
}
void loop() {
}
实现 LED 闪烁的原理很简单,就是在 loop 函数中使用延时函数 delay。先设置高电平,延时 X 秒,再设置低电平,延时 X 秒,之后就不断循环该语句即可。
// 设置 LED 引脚为12
int led_pin = 12;
void setup() {
// 设置LED的引脚模式为输出模式
pinMode(led_pin, OUTPUT);
}
void loop() {
// 点亮 LED,在LED的数字引脚上写入数字值为高电平
digitalWrite(led_pin, HIGH);
// 延时1000毫米(1秒)
delay(1000);
// 关闭 LED,在LED的数字引脚上写入数字值为低电平
digitalWrite(led_pin, LOW);
// 延时1000毫米(1秒)
delay(1000);
}
每一个 LED 的正极与开发板一个 GPIO 引脚相连,并串联一个电阻,负极接 GND,如下图:

因为我们要用到多个 GPIO 引脚,所以,我们最好把所有的 GPIO 引脚放在一个数组中,然后遍历这个数组
// 定义 GPIO 引脚数组,创建一个数组并设定五个GPIO 引脚
int pin_list[5] = {13, 12, 14, 27, 26};
// 获取数组长度,sizeof(arr) 获取数组总字节数,sizeof(arr[0]) 获取单个元素字节数,二中相除(10/2)即可得到数组长度
int size = sizeof(pin_list) / sizeof(pin_list[0]);
void setup() {
// 逐个设定 GPIO 引脚为输出模式
for (int i=0; i<size;i++) {
pinMode(pin_list[i], OUTPUT);
}
}
void loop() {
// 正向循环遍历,将所有引脚设置为高电平并设置延时50毫秒
for (int i=0;i<size;i++) {
digitalWrite(pin_list[i], HIGH);
delay(50);
}
// 正向循环遍历,将所有引脚设置为低电平并设置延时50毫秒
for (int i=0;i<size;i++) {
digitalWrite(pin_list[i], LOW);
delay(50);
}
}
我们还可以对该程序进行微调,比如之前是依次改变流水灯的状态,现在,修改为让流水灯往复亮。
// 定义 GPIO 引脚数组,创建一个数组并设定五个GPIO 引脚
int pin_list[5] = {13, 12, 14, 27, 26};
// 获取数组长度,sizeof(arr) 获取数组总字节数,sizeof(arr[0]) 获取单个元素字节数,二中相除(10/2)即可得到数组长度
int size = sizeof(pin_list) / sizeof(pin_list[0]);
void setup() {
// 逐个设定 GPIO 引脚为输出模式
for (int i=0; i<size;i++) {
pinMode(pin_list[i], OUTPUT);
}
}
void loop() {
// 正向循环遍历,将所有引脚设置为高电平并设置延时50毫秒
for (int i=0;i<size;i++) {
digitalWrite(pin_list[i], HIGH);
delay(50);
}
// 反向循环遍历,将所有引脚设置为低电平并设置延时50毫秒
for (int i=size-1;i>=0;i--) {
digitalWrite(pin_list[i], LOW);
delay(50);
}
}
让 LED 实现平移的效果是这样实现的,每次在我点亮这颗 LED 的时候,同时把上一颗 LED 的状态改为低电平,并且当索引值为 0 时,让最后一颗 LED 状态改为低电平代码如下:
// 定义 GPIO 引脚数组,创建一个数组并设定五个GPIO 引脚
int pin_list[5] = {13, 12, 14, 27, 26};
// 获取数组长度,sizeof(arr) 获取数组总字节数,sizeof(arr[0]) 获取单个元素字节数,二中相除(10/2)即可得到数组长度
int size = sizeof(pin_list) / sizeof(pin_list[0]);
void setup() {
// 逐个设定 GPIO 引脚为输出模式
for (int i=0; i<size;i++) {
pinMode(pin_list[i], OUTPUT);
}
}
void loop() {
// 正向循环遍历,将所有引脚设置为高电平
for (int i=0;i<size;i++) {
digitalWrite(pin_list[i], HIGH);
//判断当前LED是否不是第一个LED,若不是则为上一个LED的状态改为低电平
if (i > 0){
digitalWrite(pin_list[i-1], LOW);
//否则即目前LED为第一个LED, 则让上一颗LED也就是最末尾的LED的状态改为低电平
}else {
digitalWrite(pin_list[size-1], LOW);
}
delay(250);
}
}
按发光二极管单元连接方式可分为共阳极数码管和共阴极数码管:
共阳数码管是指将所有发光二极管的阳极接到一起形成公共阳极(COM)的数码管,共阳数码管在应用时应将公共极 COM 接到 +5V ,当某一字段发光二极管的阴极为低电平时,相应字段就点亮,当某一字段的阴极为高电平时,相应字段就不亮。共阴数码管是指将所有发光二极管的阴极接到一起形成公共阴极(COM)的数码管,共阴数码管在应用时应将公共极 COM 接到地线 GND 上,当某一字段发光二极管的阳极为高电平时,相应字段就点亮,当某一字段的阳极为低电平时,相应字段就不亮。原理图如下:

引脚图中间的两个 COM,是公共端,共阴数码管要将其接地,共阳数码管将其接电源。a,b,c,d,e,f,g,dp 被称为段选线。
如果你不清楚你的数码管到底是共阴还是共阳,可以使用下面三种方法测试。

2.用 ESP32 单片机给面包板通电(3.3V 引脚),公共端通过一个限流电阻接电源, 用跳线连通电源和数码管的 LED 引脚,如果亮了说明是共阳型数码管;反之,说明是共阳型数码管。

3.使用万用表的二极管档,红表笔接公共端,黑表笔接任一引脚,亮了说明是共阳型数码管,反之,则说明是共阴型数码管。

提示
建议把所有引脚测试一遍,也可以检查出是否有坏了的 LED。
将材料按照下图相连:

计这个程序时,我们需要使用二维数组。
如果我们想让这个数码管某一引脚亮起来,那么我们需要给对应的引脚设置一个低电平。如果我们想要显示一个数字时,就需要让多个 LED 同时亮,比如数字 1 需要 b、c 引脚给低电平,其余引脚给高电平。程序可以这样写:
// 定义输出引脚并把所有引脚存到数组中
int pin_a = 4;
int pin_b = 5;
int pin_c = 19;
int pin_d = 21;
int pin_e = 22;
int pin_f = 2;
int pin_g = 15;
int pin_dp = 18;
int pin_array[8] = {pin_a, pin_b, pin_c, pin_d, pin_e, pin_f, pin_g, pin_dp};
// 定义数字显示逻辑的二维数组
int number_array[][8] = {
{0, 0, 0, 0, 0, 0, 1, 1}, // 0
{1, 0, 0, 1, 1, 1, 1, 1}, // 1
{0, 0, 1, 0, 0, 1, 0, 1}, // 2
{0, 0, 0, 0, 1, 1, 0, 1}, // 3
{1, 0, 0, 1, 1, 0, 0, 1}, // 4
{0, 1, 0, 0, 1, 0, 0, 1}, // 5
{0, 1, 0, 0, 0, 0, 0, 1}, // 6
{0, 0, 0, 1, 1, 1, 1, 1}, // 7
{0, 0, 0, 0, 0, 0, 0, 1}, // 8
{0, 0, 0, 0, 1, 0, 0, 1}, // 9
};
void setup() {
// 设置所有引脚为输出模式,初始化所有引脚为高电平
for (int i=0;i<8;i++){
pinMode(pin_array[i], OUTPUT);
digitalWrite(pin_array[i], HIGH);
}
}
void loop() {
// 显示数字
int num = 8;
for (int i=0;i<8;i++){
digitalWrite(pin_array[i], number_array[num][i]);
}
}
我们也可以使用函数来把显示数字的逻辑代码封装起来,方便我们在其他地方使用:
// 定义输出引脚并把所有引脚存到数组中
int pin_a = 4;
int pin_b = 5;
int pin_c = 19;
int pin_d = 21;
int pin_e = 22;
int pin_f = 2;
int pin_g = 15;
int pin_dp = 18;
int pin_array[8] = {pin_a, pin_b, pin_c, pin_d, pin_e, pin_f, pin_g, pin_dp};
// 定义数字显示逻辑的二维数组
int number_array[][8] = {
{0, 0, 0, 0, 0, 0, 1, 1}, // 0
{1, 0, 0, 1, 1, 1, 1, 1}, // 1
{0, 0, 1, 0, 0, 1, 0, 1}, // 2
{0, 0, 0, 0, 1, 1, 0, 1}, // 3
{1, 0, 0, 1, 1, 0, 0, 1}, // 4
{0, 1, 0, 0, 1, 0, 0, 1}, // 5
{0, 1, 0, 0, 0, 0, 0, 1}, // 6
{0, 0, 0, 1, 1, 1, 1, 1}, // 7
{0, 0, 0, 0, 0, 0, 0, 1}, // 8
{0, 0, 0, 0, 1, 0, 0, 1}, // 9
};
void display_number(int num){
// 清屏
for (int i=0;i<8;i++){
digitalWrite(pin_array[i], HIGH);
}
// 改变对应引脚的电平;
for (int i=0;i<8;i++){
digitalWrite(pin_array[i], number_array[num][i]);
}
}
void setup() {
// 设置所有引脚为输出模式,初始化所有引脚为高电平
for (int i=0;i<8;i++){
pinMode(pin_array[i], OUTPUT);
digitalWrite(pin_array[i], HIGH);
}
}
void loop() {
// 调用显示数字函数显示数字并延时500毫秒
for (int i=0;i<10;i++){
display_number(i);
delay(500);
}
}
4 位数码管,即 4 个 1 位数码管并列集中在一起形成一体的数码管。

当多位数码管一体时,它们内部的公共端是独立的,而负责显示什么数字的段线(a-dp)全部是连接在一起的,独立的公共端可以控制多位一体中的哪一位数码管点亮,而连接在一起的段线可以控制这个能点亮数码管亮什么数字,通常我们把公共端叫做 位选线 ,连接在一起的段线叫做 段选线,有了这两个线后,通过单片机及外部驱动电路就可以控制任意的数码管显示任意的数字了。
4 位数码管与 1 位数码管的原理基本一致,除了引脚不同。

相同的地方是,数码管中的 LED 的分段映射相同,如下图:

有区别的地方首先是 1 位数码管有两个相同的公共(COM)端接地或者接电源,而 4 位数码管没有公共端,有四个控制不同位置显示的选通端。

一般一位数码管有 10 个引脚,四位数码管是12 个引脚,关于具体的引脚及段、位标号大家可以查询相关资料,最简单的办法就是用数字万用表测量,若没有数字万用表也可用 5V 直流电源串接1k 电阻后测量,将测量结果记录,通过统计便可绘制出引脚标号。多位数码管有许多是按一定要求设计的,引脚不完全按照一般规则设定,所以需要在使用时查找手册,最直接的办法就是按照数码管上的标示向生产商要
将材料按照下图相连:


我们一步一步的来,先写一个最简单的程序,让任意一位数码管显示任意数字,代码可以这么写:
#include <Arduino.h>
// 定义位选线引脚
int seg_1 = 5;
int seg_2 = 18;
int seg_3 = 19;
int seg_4 = 21;
// 定义位选线数组
int seg_array[4] = {seg_1, seg_2, seg_3, seg_4};
// 定义段选线引脚;
int a = 32;
int b = 25;
int c = 27;
int d = 12;
int e = 13;
int f = 33;
int g = 26;
int dp = 14;
// 定义位选线引脚
int led_array[8] = {a, b, c, d, e, f, g, dp};
// 定义共阴极数码管不同数字对应的逻辑电平的二维数组
int logic_array[10][8] = {
//a, b, c, d, e, f, g, dp
{1, 1, 1, 1, 1, 1, 0, 0}, // 0
{0, 1, 1, 0, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1, 0}, // 2
{1, 1, 1, 1, 0, 0, 1, 0}, // 3
{0, 1, 1, 0, 0, 1, 1, 0}, // 4
{1, 0, 1, 1, 0, 1, 1, 0}, // 5
{1, 0, 1, 1, 1, 1, 1, 0}, // 6
{1, 1, 1, 0, 0, 0, 0, 0}, // 7
{1, 1, 1, 1, 1, 1, 1, 0}, // 8
{1, 1, 1, 1, 0, 1, 1, 0}, // 9
};
// 清屏函数
void clear() {
//将段选线和位选线初始化
for (int i=0;i<4;i++) {
digitalWrite(seg_array[i], HIGH);
}
for (int i=0;i<8;i++) {
digitalWrite(led_array[i], LOW);
}
}
// 显示数字的函数
void display_number(int order, int number) {
// 清屏
clear();
// 把对应位选线的电平拉低
digitalWrite(seg_array[order], LOW);
// 显示数字
for (int i=0;i<8;i++) {
digitalWrite(led_array[i], logic_array[number][i]);
}
}
void setup() {
//因为4为数码管上面是段选线下面是位选线,需要导通得段选线为高电平、位选线为低电平,才会有电势差才能导通,在初始化时不能导通,所以要设置段选线为低电平、位选线为高电平
// 设置所有位选线引脚为输出模式,初始化所有位选线引脚为高电平
for (int i=0;i<4;i++) {
pinMode(seg_array[i], OUTPUT);
digitalWrite(seg_array[i], HIGH);
}
// 设置所有段选线引脚为输出模式,初始化所有段选线引脚为低电平
for (int i=0;i<8;i++) {
pinMode(led_array[i], OUTPUT);
digitalWrite(led_array[i], LOW);
}
}
void loop() {
// 第显示数字1818
// display_number(0, 1);
// display_number(1, 8);
// display_number(2, 1);
// display_number(3, 8);
//按顺序让所有位置显示 0~9
for (int i=0;i<4;i++) {
for (int j=0;j<10;j++) {
display_number(i, j);
delay(200);
}
}
}
我们选择多位数码管,肯定是要在不同位置显示不同数字的,这时候,我们需要用到 动态扫描。
什么是动态扫描?
动态扫描是对位选端扫描,8 个引脚控制每个数码管的段选线,通过刷新位选端和 8 个引脚的状态,来实现显示不同的数字。
我们可以通过运行下面这段代码,更生动形象地理解 动态扫描 的原理:
/*
该程序的作用是演示动态扫描原理。
在线文档:https://docs.geeksman.com/esp32/Arduino/08.esp32-arduino-4-digits-7segment.html
*/
// 定义位选线引脚
int seg_1 = 5;
int seg_2 = 18;
int seg_3 = 19;
int seg_4 = 21;
// 定义位选线数组
int seg_array[4] = {seg_1, seg_2, seg_3, seg_4};
// 定义段选线引脚;
int a = 32;
int b = 25;
int c = 27;
int d = 12;
int e = 13;
int f = 33;
int g = 26;
int dp = 14;
// 定义位选线引脚
int led_array[8] = {a, b, c, d, e, f, g, dp};
// 定义共阴极数码管不同数字对应的逻辑电平的二维数组
int logic_array[10][8] = {
//a, b, c, d, e, f, g, dp
{1, 1, 1, 1, 1, 1, 0, 0}, // 0
{0, 1, 1, 0, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1, 0}, // 2
{1, 1, 1, 1, 0, 0, 1, 0}, // 3
{0, 1, 1, 0, 0, 1, 1, 0}, // 4
{1, 0, 1, 1, 0, 1, 1, 0}, // 5
{1, 0, 1, 1, 1, 1, 1, 0}, // 6
{1, 1, 1, 0, 0, 0, 0, 0}, // 7
{1, 1, 1, 1, 1, 1, 1, 0}, // 8
{1, 1, 1, 1, 0, 1, 1, 0}, // 9
};
// 延时时间
int count = 355;
// 清屏函数
void clear() {
for (int i=0;i<4;i++) {
digitalWrite(seg_array[i], HIGH);
}
for (int i=0;i<8;i++) {
digitalWrite(led_array[i], LOW);
}
}
// 显示数字的函数
void display_number(int order, int number) {
// 清屏
clear();
// 把对应位选线的电平拉低
digitalWrite(seg_array[order], LOW);
// 显示数字
for (int i=0;i<8;i++) {
digitalWrite(led_array[i], logic_array[number][i]);
}
}
void setup() {
// 设置所有位选线引脚为输出模式,初始化所有位选线引脚为高电平
for (int i=0;i<4;i++) {
pinMode(seg_array[i], OUTPUT);
digitalWrite(seg_array[i], HIGH);
}
// 设置所有段选线引脚为输出模式,初始化所有段选线引脚为低电平
for (int i=0;i<8;i++) {
pinMode(led_array[i], OUTPUT);
digitalWrite(led_array[i], LOW);
}
}
void loop() {
display_number(0, 1);
delay(count);
display_number(1, 2);
delay(count);
display_number(2, 3);
delay(count);
display_number(3, 4);
delay(count);
if (count > 10) {
if (count > 110) {
count -= 50;
}else {
count -= 10;
}
}
}
理解了 动态扫描 的原理之后,我们就可以写代码了,先把之前写的这些代码复制过来,然后我们还需要实现通过动态扫描的方法实现 4 位数字显示的功能:
// 4 位数码管显示函数
void display_4_number(int number) {
// 把输入的数字格式化为 4 位数的数组
if (number < 10000) {
// 获取每一位对应的数字
// // 获取个位
// int seg_4_number = number % 10;
// number /= 10;
//
// // 获取十位
// int seg_3_number = number % 10;
// number /= 10;
//
// // 获取百位
// int seg_2_number = number % 10;
// number /= 10;
//
// // 获取千位
// int seg_1_number = number % 10;
// 定义格式化数组
int number_array[4];
// 使用循环获取格式化数组
for (int i=3;i>=0;i--) {
number_array[i] = number % 10;
number /= 10;
}
// 显示数字
for (int i=0;i<4;i++) {
display_number(i, number_array[i]);
delay(5);
}
}
}
按钮有两组引脚(触点)。当按下按钮时,它会连接这两个触点,从而关闭电路。
一般来说 4 脚开关(轻触按键)相距较远的是相通的,离得较近的是一组开关,最好是测量一下,如果懒得测,接对角肯定是可以的。
下图说明了按钮内部的连接:

使用按键的时候,通常情况下需要进行消抖。
什么是按键消抖?
该实验中所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
按键的抖动对于人类来说是感觉不到的,但对单片机来说,则是完全可以感应到的,而且还是一个很漫长的过程,因为单片机处理的速度在微秒级,而按键抖动的时间至少在毫秒级。
一次按键动作的电平波形如下图。存在抖动现象,其前后沿抖动时间一般在 5ms~10ms 之间。由于单片机运行速度非常快,刚按下的时候会检测到低电平判断按键被按下。但是由于按键存在抖动,单片机在此时也会检测到高电平,误以为松开按键,紧接着又检测到低电平,判断到按键被按下。周而复始,在 5-10ms 内可能会出现很多次按下的动作,每一次按键的动作判断的次数都不相同。

这种抖动可能会影响程序误判,造成严重后果,一般我们采用两种方式对按键进行消抖:
硬件方法一般用在对按键操作过程比较严格,且按键数量较少的场合,而按键数量较多时,通常采用软件消抖。值得一提的是,对于复杂且多任务的单片机系统来说,若简单地采用循环指令来实现软件延时,则会浪费CPU宝贵的时间资源,大大降低系统的实时性,所以,更好的做法是利用定时中断服务程序或利用标志位的方法来实现软件消抖。
将材料按照下图相连:
按键一端接 3V3 引脚,一端接 D14,LED 接 D2。

这里我们在使用 pinMode 方法的时候,第二个参数就不能传递 OUTPUT 了,并且我们需要使用 digitalRead 方法来获取输入值。
与输出不同的是,设置输入引脚时,我们需要配置上拉或下拉电阻,目的是确定某个状态电路中的高电平或低电平。
上、下拉电阻的作用是提高电路稳定性,避免引起误动作。按键如果不通过电阻上拉到高电平,那么在上电瞬间可能就发生误动作,因为在上电瞬间单片机的引脚电平是不确定的,上拉电阻的存在保证了其引脚处于高电平状态,而不会发生误动作。
在 Arduino 中,设置引脚的上拉电阻或下拉电阻需要使用 pinMode 函数和 INPUT_PULLUP 或 INPUT_PULLDOWN 常量。
如果要设置引脚为上拉电阻,需要将引脚设置为输入模式,并调用 pinMode(pin, INPUT_PULLUP) 函数,其中 pin 为引脚号。
提示
如果你不认识上拉电阻和下拉电阻,在这个阶段是无所谓的,你只需要了解他们的存在是为了确定初始电平状态。 选择上拉电阻,GPIO 引脚默认位高电平,那我们想要改变信号,就需要传递一个低电平,接地(gnd)。 选择下拉电阻,GPIO 引脚默认为低电平,那我们想要改变信号,就需要传递一个高电平,接电源(3.3v)。
因此,我们的代码需要这么写:
// 定义 LED 与 按键引脚
int led_pin = 2;
int button_pin = 14;
// 定义 LED 逻辑值
int led_logic = 0;
// 判断 LED 的状态是否改变过
bool status = false;
void setup() {
//设置LED为输出模式
pinMode(led_pin, OUTPUT);
//设置按钮为上拉模式
pinMode(button_pin, INPUT_PULLDOWN);
}
void loop() {
// 按键消抖
if (digitalRead(button_pin)) {
// 睡眠 10ms,如果依然为高电平,说明抖动已消失。
delay(10);
if (digitalRead(button_pin) && !status) {
led_logic = !led_logic;
digitalWrite(led_pin, led_logic);
// led 的状态发生了变化,即使我持续按着按键,LED 的状态也不应该改变。
status = !status;
}else if (!digitalRead(button_pin)) {
status = false;
}
}
}
我们也可以不在 setup 前定义变量,而是选择 宏定义 的方法。
在 Arduino 中,宏定义是一种预处理器指令,它被用于创建常量和替代符号。通过使用宏定义,可以让代码更易读,更易于维护和修改,同时也可以避免在代码中出现重复的代码块。
宏定义以 #define 关键字开头,后面跟着宏名称和宏值。宏值可以是数字、字符或者表达式。一旦定义了宏,它将会被整个程序使用。
例如,下面的代码创建了一个宏定义 LED_PIN,将其值设置为 13:
#define LED_PIN 13
然后,在程序中可以使用宏定义来指定要使用的引脚,而不是写出具体的数字,从而使代码更具可读性:
#define LED_PIN 2
#define BUTTON_PIN 14
// 定义 LED 逻辑值
int led_logic = 0;
// 判断 LED 的状态是否改变过
bool status = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLDOWN);
}
void loop() {
// 按键消抖
if (digitalRead(BUTTON_PIN)) {
// 睡眠 10ms,如果依然为高电平,说明抖动已消失。
delay(10);
if (digitalRead(BUTTON_PIN) && !status) {
led_logic = !led_logic;
digitalWrite(LED_PIN, led_logic);
// led 的状态发生了变化,即使我持续按着按键,LED 的状态也不应该改变。
status = !status;
}else if (!digitalRead(BUTTON_PIN)) {
status = false;
}
}
}
宏定义和变量定义都是定义标识符的方式,但二者有以下区别:
总之,宏定义主要是一种代码替换的机制,能够提高代码的可读性和可维护性,而变量定义则是用于在程序中存储和管理数据的标识符。
注:宏定义类似与全局变量
脉冲宽度调制(PWM),是英文 Pulse Width Modulation 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在测量、通信到功率控制与变换的许多领域中。
PWM 通过调节输出不同频率(频率是指 1 秒钟内信号从高电平到低电平再回到高电平的次数(一个周期))、占空比(一个周期内高电平出现时间占总时间比例)的方波。以实现固定频率或平均电压输出。频率固定,改变占空比可改变输出电压,如下所示:

LED 的正极接开发板的 D12 引脚,并串联一个电阻,负极接 GND,如下图:

注意:一定要接电阻,不然会由于电流过大,烧坏 LED。
想要通过 Arduino 输出 PWM 有两种方法,第一种就是使用 Arduino 自带的 analogWrite(pin, value) 函数,其中的两个参数:
pin:要写入的 Arduino 引脚。允许的数据类型:int.value:占空比:介于 0(始终关闭)和 255(始终开启)之间。允许的数据类型:int.,代码如下:
// 宏定义 GPIO 输出引脚
#define LED_PIN 12
void setup() {
// 配置 GPIO 输出引脚
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// 实现渐亮效果
for(int i=0;i<256;i++) {
// 设置亮度模拟值
analogWrite(LED_PIN, i);
// 延时 10ms
delay(10);
}
// 实现渐灭效果
for(int i=255;i>=0;i--) {
// 设置亮度模拟值
analogWrite(LED_PIN, i);
// 延时 10ms
delay(10);
}
}
第二种是使用 ESP32 的 LEDC 外设,在 ESP32 上有一个 LEDC 外设模块专用于输出 PWM 波形。
LED PWM 控制器可以生成 16 路通道(0 ~ 15),波形的周期和占空比可配置。分为高低速两组,高速通道(0 ~ 7)由 80MHz 时钟驱动,低速通道(8 ~ 15)由 1MHz 时钟驱动。另外,每路 LED PWM 支持自动步进式地增加或减少占空比,可以用于 LED RGB 彩色梯度发生器。
作为刚入门的学习者,上面这段概念不理解也不影响我们后续的学习,我们需要了解的是 LEDC 的控制函数以及 PWM 信号的产生流程。
打开 esp32_hal_led.h 文件之后,我们可以看到 LEDC 的所有控制函数:
// 设置 LEDC 通道对应的频率和计数位数(占空比分辨率),返回最终频率
// 分辨率的意思就是把一个周期分成 2 的 resolution_bits 份。
uint32_t ledcSetup(uint8_t channel, uint32_t freq, uint8_t resolution_bits);
// 指定通道输出一定占空比波形
void ledcWrite(uint8_t channel, uint32_t duty);
// 类似于 arduino 的 tone ,当外接无源蜂鸣器的时候可以发出某个声音(根据频率不同而不同)
uint32_t ledcWriteTone(uint8_t channel, uint32_t freq);
// 该方法是上面方法的进一步封装,可以直接输出指定调式和音阶声音的信号
uint32_t ledcWriteNote(uint8_t channel, note_t note, uint8_t octave);
// 返回指定通道占空比的值
uint32_t ledcRead(uint8_t channel);
// 返回指定通道当前频率(如果当前占空比为0 则该方法返回0)
uint32_t ledcReadFreq(uint8_t channel);
// 将 LEDC 通道绑定到指定 IO 口上以实现输出
void ledcAttachPin(uint8_t pin, uint8_t channel);
// 解除 IO 口的 LEDC 功能
void ledcDetachPin(uint8_t pin);
使用 LEDC 外设的时候需要遵循以下步骤:
ledcSetup() 函数建立 LEDC 通道;ledcAttachPin() 将 GPIO 口与 LEDC 通道关联;ledcWrite()、ledcWriteTone()、ledcWriteNote()设置频率、设置蜂鸣器音调等等ledcDetachPin() 解除 GPIO 口与 LEDC 通道的关联所有我们可以通过以下代码,实现呼吸灯效果:
#define FREQ 2000 // 频率
#define CHANNEL 0 // 通道
#define RESOLUTION 8 // 分辨率
#define LED 12 // LED 引脚
void setup()
{
ledcSetup(CHANNEL, FREQ, RESOLUTION); // 设置通道
ledcAttachPin(LED, CHANNEL); // 将通道与对应的引脚连接
}
void loop()
{
// 逐渐变亮
//将分辨率分成255份,每5毫秒增加一份,使LED逐渐变亮
for (int i=0;i<pow(2, RESOLUTION); i++)
{
ledcWrite(CHANNEL, i); // 输出PWM
delay(5);
}
// 逐渐变暗
//将分辨率分成255份,每5毫秒减少一份,使LED逐渐变暗
for (int i=pow(2, RESOLUTION)-1;i>=0;i--)
{
ledcWrite(CHANNEL, i); // 输出PWM
delay(5);
}
}
通过 ADC 将模拟信号转换为数字信号给 ESP32 处理。
在学习 ADC 之前,我们要首先学习什么是模拟信号,什么是数字信号。
模拟信号(Analog Signal):模拟信号是连续变化的量或者信号,生活中接触到的信号基本都是模拟信号,温度变化,天体运动等等,这些都是连续的信息,都是模拟信号。模拟信号,简单的说就是用电信号模拟出其他的信号,比如用电信号模拟出图像,模拟出声音的声波。
数字信号(Digital Signal):数字信号是时间离散、数值离散的信号,数字信号存在采样,还存在量化,只能取到一些不连续的固定值,这也是数字信号和模拟信号之间可以进行相互转换的原因。
数字信号是在模拟信号的基础上依次经过 采样、量化、编码而形成的。具体地说,采样 就是把输入的模拟信号按适当的时间间隔得到各个时刻的样本值;量化 是把经采样测得的各个时刻的值用二进制码来表示;编码 则是把量化生成的二进制数排列在一起形成顺序脉冲序列。

ADC(Analog to Digital Converter)即模数转换器,它可以将模拟信号转换为数字信号。由于单片机只能识别二进制数字,所以外界模拟信号常常会通过 ADC 转换成其可以识别的数字信号。常见的应用就是将变化的电压转成数字信号。
注意
使用默认配置时,ADC 引脚上的输入电压必须介于 0.0V 和 1.0V 之间(任何高于 1.0V 的值都将读为 4095)。如果需要增加测量范围,需要配置衰减器。

注:由图可知DAC引脚为25、26,故ADC可以接在这两个引脚上
电位器相当于一个滑动变阻器,两端引脚阻值是固定的,中间引脚对任何一端的引脚阻值是可变的,他等效于从中间把电位器分成两个串联的电阻,串联总阻值是确定的,一端接输入电源,一端接地

打开 esp32_hal_adc.h 文件之后,我们可以看到 ADC 的所有控制函数:
analogReadResolution(resolution):设置样本位和分辨率。它可以是一个介于 9(0 - 511)和 12 位(0 - 4095)之间的值。默认是 12 位分辨率。analogSetWidth(width):设置样本位和分辨率。它可以是一个介于 9(0 - 511)和 12 位(0 - 4095)之间的值。默认是 12 位分辨率。analogSetCycles(cycles):设置每个样本的循环次数。默认是 8。取值范围:1 ~ 255。analogSetSamples(samples):设置范围内的样本数量。默认为 1 个样本。它有增加灵敏度的作用。analogSetClockDiv(attenuation):设置ADC时钟的分压器。默认值为1。取值范围:1 ~ 255。adcAttachPin(pin):附加一个引脚到 ADC(也清除任何其他模拟模式可能是 on)。返回TRUE或FALSE结果。analogSetAttenuation(attenuation):设置所有 ADC 引脚的输入衰减。默认是 ADC_11db。其他取值:
ADC_0db: 集没有衰减。ADC 可以测量大约 800mv (1V 输入 = ADC 读数 1088)。ADC_2_5db: ADC 的输入电压将被衰减,扩展测量范围至约。1100 mV。(1V 输入 = ADC 读数 3722)。ADC_6db: ADC 的输入电压将被衰减,扩展测量范围至约。1350 mV。(1V 输入= ADC 读数 3033)。ADC_11db: ADC 的输入电压将被衰减,扩展测量范围至约。2600 mV。(1V 输入= ADC 读数 1575)。analogSetPinAttenuation(pin, attenuation):设置指定引脚的输入衰减。默认是 ADC_11db。衰减值与前一个函数相同。因此,代码可以这么写:
#define POT 26
#define LED 13
#define CHANNEL 0
// 初始化模拟输入值
int pot_value;
void setup() {
// 设置 ADC 分辨率,设置样本位和分辨率,默认12
analogReadResolution(12);
// 配置衰减器,设置所有 ADC 引脚的输入衰减,默认ADC_11db
analogSetAttenuation(ADC_11db);
// 建立 LEDC 通道,配置 LEDC 分辨率
ledcSetup(CHANNEL, 1000, 12);//2的12次方=4096,故通道访问为 0 ~ 4095
// 关联 GPIO 口与 LEDC 通道
ledcAttachPin(LED, CHANNEL);
}
void loop() {
// 获取模拟输入值以调节LED亮度
pot_value = analogRead(POT);
// 输出 PWM,使用通道输出从电位器获取到的输入值
ledcWrite(CHANNEL, pot_value);
//延时50毫秒以便观察
delay(50);
}
LCD1602 是很多单片机爱好者较早接触的字符型液晶显示器,所以,在这里花点时间是值得的。
1602 液晶屏的称呼来自于其显示的内容容量,其中的 16 代表每行的字符(数字或英文字符)数,02 代表屏幕一共两行,实际开发中根据需要显示信息的内容多少不但可以选用 1602 屏,还可以选用诸如 2004 屏等。如下图:

1602 液晶显示屏除了电源、地以外,有 3 个控制引脚 RS R/W E 和 8 个数据引脚 DB0-7。

由于 1602 的管脚数过多,如果直接与 ESP32 开发板连接需要占用大量的 GPIO 管脚,不但容易造成资源浪费,连接也非常不方便。
因此实际使用时往往会给 1602 屏增加一块 IIC 驱动版,将 1602 的 16 个管脚连接到由 PCF8574T 作为主要芯片的驱动版上,将接口转换为 IIC 再连接开发板,具体情况如上图所示。
IIC 是一种硬件设备间常用的接口通讯协议,全称是 Inter-Integrated Circuit,也可以写为 I2C。他的设计时的理念是:信号线尽量少并且速率要尽量高。 信号线少,可以减少引脚占用,这对早期的芯片(引脚很少)的很重要。
使用 IIC 接口时一共需要连接四根线,包括:VCC、GND、SDA、SCL,其中 SDA 和 SCL 需要占用 GPIO 管脚,连接到开发板上任何一组 IIC 接口的对应管脚都可以。
标准的 I2C 需要两根信号线:
简单来说,只需要 2 根线,就可以对多台设备传输大量数据,减少单片机上 IO 口的占用。
将材料按照下图相连:

注意:
注意需要使用开发板上的 5V 电压,而不是 3.3V。真实环境下使用 3.3V 会无法显示或者显示很暗。
这里连接的这两个引脚可不是随便连接的,而是对应了 ESP32 芯片原理图,ESP32 的 I2C 引脚的 SDA 对应 D21,SCL 对应 D22,如图:

我们可以了解 LiquidCrystal_I2C 库的使用:
LiquidCrystal_I2C(uint8_t addr, uint8_t cols, uint8_t rows):构造函数,用于构造 LCD I2C 对象,参数:addr 是地址,默认的是 0x27,cols 是 LCD 显示的列数,rows 是 LCD 显示的函数;void init():初始化显示屏;void clear():清除 LCD 屏幕上内容,并将光标置于左上角;void home():将光标在定位在屏幕左上角;void noBacklight() 与 void backlight():是否开启背光;print():显示内容;void leftToRight() 与 void rightToLeft():控制文字显示的方向,默认是从左向右;void noDisplay() 与 void display():关闭显示或恢复显示(内容不会丢失);void setCursor(uint8_t col, uint8_t row):设置光标的位置,列,行,基于 0;void noCursor() 与 void cursor:显示与不显示光标,默认不显示;void noBlink() 与 void blink():光标是否闪烁,默认不闪烁。现在,我们就可以写代码了。
了解了第三方库之后,我们先写一个最简单的程序,比如,在屏幕上显示 Hello, world,代码如下:
#include "LiquidCrystal_I2C.h"
// 设置 LCD1602 的地址,列数,行数
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup()
{
// 初始化 LCD 对象
lcd.init();
// 打印内容
lcd.backlight();
lcd.print("Hello, world!");
}
void loop()
{
}
在这个程序中,我们需要用到串口的另外两个方法 Serial.available() 与 Serial.read():
Serial.available():返回串口缓冲区中当前剩余的字符个数。一般用这个函数来判断串口的缓冲区有无数据,当 Serial.available()>0 时,说明串口接收到了数据,可以读取;Serial.read() 指从串口的缓冲区取出并读取一个 Byte 的数据,比如有设备通过串口向 Arduino 发送数据了,我们就可以用 Serial.read() 来读取发送的数据。//使用`LiquidCrystal_I2C` 库
#include "LiquidCrystal_I2C.h"
// 设置 LCD1602 的地址,列数,行数
LiquidCrystal_I2C lcd(0x27,16,2);
void setup()
{
// 初始化 LCD 对象
lcd.init();
// 开启背光
lcd.backlight();
// 开启串口通信
Serial.begin(9600);
}
void loop()
{
// 检测是否有串口输入
if (Serial.available()) {
// 延时以等待所有数据传输完成
delay(100);
// 清屏
lcd.clear();
// 反复读取串口的数据并在 LCD1602 屏幕上显示,直到数据读完
while (Serial.available() > 0) {
lcd.write(Serial.read());
}
}
}
SPI(Serial Peripheral Interface) 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种同步、全双工、主从式接口,但并不是所有的 SPI 都是全双工。来自主机或从机的数据在时钟上升沿或下降沿同步。主机和从机可以同时传输数据。SPI 接口可以是 1 线、2 线 3 线式或 4 线式,这节课,我们用到的就是 3 线 SPI。
产生时钟信号的器件称为 主机。主机和从机之间传输的数据与主机产生的时钟同步。同 I2C 接口相比,SPI 器件支持更高的时钟频率。用户应查阅产品数据手册以了解 SPI 接口的时钟频率规格。
标准 4 线 SPI 芯片的管脚上只占用四根线。
MOSI: 主器件数据输出,从器件数据输入。MISO:主器件数据输入,从器件数据输出。SCK: 时钟信号,由主设备控制发出。CS(NSS): 从机设备选择信号,由主设备控制。当 CS 为低电平则选中从器件。3 线 SPI 没有 MISO,或者 MISO 与 MOSI 共线。
SPI 接口只能有一个主机,但可以有一个或多个从机。下图显示了主机和从机之间的 SPI 连接。来自主机的片选信号用于选择从机。这通常是一个低电平有效信号,拉高时从机与 SPI 总线断开连接。当使用多个从机时,主机需要为每个从机提供单独的片选信号。MOSI 和 MISO 是数据线。MOSI 将数据从主机发送到从机,MISO将数据从从机发送到主机。

ESP32 集成了 4 个 SPI 外设。
I2C 只需两根信号线,而标准 SPI 至少四根信号,如果有多个从设备,信号需要更多。一些 SPI 变种虽然只使用三根线—— SCK、CS 和双向的 MISO/MOSI,但 CS 线还是要和从设备一对一根。另外,如果 SPI 要实现多主设备结构,总线系统需额外的逻辑和线路。用 I2C 构建系统总线唯一的问题是有限的 7 位地址空间,但这个问题新标准已经解决 --- 使用 10 位地址。
如果应用中必须使用高速数据传输,那么 SPI 是必然的选择。因为 SPI 是全双工,IIC 的不是。SPI 没有定义速度限制,一般的实现通常能达到甚至超过 10Mbps。IIC 最高的速度也就快速+模式(1Mbps)和高速模式(3.4Mbps),后面的模式还需要额外的 I/O 缓冲区,还并不是总是容易实现的。SPI 适合数据流应用,而 IIC 更适合“字节设备”的多主设备应用。
SPI 有一个非常大的缺陷,主要是没有标准的协议,SPI 比较混乱,主要是没有标准的协议,只有moto的事实标准。所以衍生出多个版本,但没有本质的差异。
OLED,即有机发光二极管(Organic Light Emitting Diode)。OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被称为是第三代显示技术。
LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示 OLED 效果要来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。

我们今天用到的屏幕是 0.96 寸的 SSD1306 芯片驱动的 OLED 屏幕。他的分辨率是 128*64,意思就是横向有 128 个像素点,纵向有 64 个
OLED 显示屏模块接口定义:

物料清单(BOM 表):
| 材料名称 | 数量 |
|---|---|
| 0.96 寸 OLED 屏幕 | 1 |
| 按键 | 2 |
| 杜邦线(跳线) | 若干 |
| 面包板 | 1 |

如果想要使用 Arduino 控制 SSD1306 驱动的 OLED 屏幕,有以下两种第三方库可以使用:
Adafruit_SSD1306 库:专门针对 SSD1306 驱动 OLED 屏幕的显示图形库;U8G2 库:目前 Arduino 平台上使用最广泛的 OLED 库。想要使用 Adafruit_SSD1306,还需要安装 Adafruit_GFX 第三方库。Arduino 的 Adafruit_GFX 库为我们所有的 LCD 和 OLED 显示器提供了通用语法和图形功能集,也就是说这是一个通用图形库,并不针对特定的显示器型号。
Adafruit_GFX 定义了一系列的绘画方法(线,矩形,圆等等),属于基础类,并且最重要的一点,drawPixel 方法由子类来实现;Adafruit_SSD1306 定义了一系列跟 SSD1306 有关的方法,并且重写了 drawPixel 方法,属于扩展类。首先,我们就需要先下载这两个第三方库,PlatformIO 已经为我们提供了方便的下载途径,我们可以直接在 PlatformIO 的 PIO HOME 页面中选择 Libraries 中分别搜索 Adafruit GFX Library 与 Adafruit_SSD1306,然后添加到项目中即可。

下载完以上两个第三方库之后,打开 platformio.ini 文件,可以看到 lib_deps 中出现了 SSD1306 与 Adafruit GFX Library 两个依赖,

在学习 Adafruit_SSD1306 之前,你需要明白无论什么 OLED 屏幕,最终都可以抽象为像素点阵,想显示什么内容就把具体位置的像素点亮起来。比如 SSD1306-12864 就是一个 128X64 像素点阵,这个点阵拥有自己的一套坐标系,在坐标系中,左上角是原点,向右是X轴,向下是Y轴。

接下来,我们就可以深入学习该库了。
SSD1306 包括 IIC 和 SPI 总线版本,所以针对不同版本又有对应的构造器方法,因为我们的 OLED 是 SPI 版本的,因此,我们只讲 SPI 总线的构造方法。以下代码是
Adafruit_SSD1306(uint8_t w, uint8_t h, int8_t mosi_pin, int8_t sclk_pin,int8_t dc_pin, int8_t rst_pin, int8_t cs_pin);
/*!
@brief Constructor for SPI SSD1306 displays, using software (bitbang)
SPI.(软件SPI总线)
@param w
Display width in pixels
@param h
Display height in pixels
@param mosi_pin
MOSI (master out, slave in) pin (using Arduino pin numbering).
This transfers serial data from microcontroller to display.
@param sclk_pin
SCLK (serial clock) pin (using Arduino pin numbering).
This clocks each bit from MOSI.
@param dc_pin
Data/command pin (using Arduino pin numbering), selects whether
display is receiving commands (low) or data (high).
@param rst_pin
Reset pin (using Arduino pin numbering), or -1 if not used
(some displays might be wired to share the microcontroller's
reset pin).
@param cs_pin
Chip-select pin (using Arduino pin numbering) for sharing the
bus with other devices. Active low.
@return Adafruit_SSD1306 object.
@note Call the object's begin() function before use -- buffer
allocation is performed there!
*/
Adafruit_SSD1306(uint8_t w, uint8_t h, int8_t mosi_pin, int8_t sclk_pin,
int8_t dc_pin, int8_t rst_pin, int8_t cs_pin);
/*!
@brief Constructor for SPI SSD1306 displays, using native hardware SPI.(硬件SPI总线)
@param w
Display width in pixels
@param h
Display height in pixels
@param spi
Pointer to an existing SPIClass instance (e.g. &SPI, the
microcontroller's primary SPI bus).
@param dc_pin
Data/command pin (using Arduino pin numbering), selects whether
display is receiving commands (low) or data (high).
@param rst_pin
Reset pin (using Arduino pin numbering), or -1 if not used
(some displays might be wired to share the microcontroller's
reset pin).
@param cs_pin
Chip-select pin (using Arduino pin numbering) for sharing the
bus with other devices. Active low.
@param bitrate
SPI clock rate for transfers to this display. Default if
unspecified is 8000000UL (8 MHz).
@return Adafruit_SSD1306 object.
@note Call the object's begin() function before use -- buffer
allocation is performed there!
*/
Adafruit_SSD1306(uint8_t w, uint8_t h, SPIClass *spi,
int8_t dc_pin, int8_t rst_pin, int8_t cs_pin, uint32_t bitrate=8000000UL);
软件 SPI 总线用法,代码如下:
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// 软件SPI总线
// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
使用硬件 SPI 总线,代码如下:
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
&SPI, OLED_DC, OLED_RESET, OLED_CS);
接下来的方法(函数)无论是 I2C 还是 SPI 总线构建的,用法都是一致的:
clearDisplay:清除显示,该方法仅清除 Arduino 缓存,不会立即显示在屏幕上,可以通过调用 display 来立即清除;display:显示内容,这个方法才是真正把绘制内容画在 OLED 屏幕上(非常重要);drawCircle:绘制空心圆;fillCircle:绘制实心圆;drawTriangle:绘制空心三角形;fillTriangle:绘制实心三角形;drawRoundRect:绘制空心圆角方形;fillRoundRect:绘制实心圆角方形;drawBitmap:绘制 Bitmap 图形;drawXBitmap:绘制 XBitmap 图形;drawChar:绘制单个字符;getTextBounds:计算字符串在当前字体大小下的像素大小,返回左上角坐标以及宽度高度像素值;setTextSize:设置字体大小;setFont:设置字体;setCursor:设置光标位置;setTextColor:设置字体颜色;setTextWrap:设置是否自动换行;drawPixel:绘制像素点;drawFastHLine:绘制水平线;drawFastVLine:绘制垂直线;startscrollright:滚动到右边;startscrollleft:滚动到左边;startscrolldiagright:沿着对角线滚动到右边;startscrolldiagleft:沿着对角线滚动到左边;stopscroll:停止滚动:使用 Adafruit_SSD1306 库分为三个步骤:
了解完基本原理之后,我们就可以写一个简单的程序了,比如我们可以在屏幕上显示一些图形和字符,代码如下:
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度
// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
void setup()
{
oled.begin();//初始化方法
oled.clearDisplay(); // 清除显示
oled.drawFastHLine(32, 5, 48, SSD1306_WHITE); // 绘制水平线
oled.drawLine(32, 5, 48, 30, SSD1306_WHITE); // 绘制线
oled.drawRect(5, 5, 10, 25, SSD1306_WHITE); // 绘制矩形
oled.fillRect(75, 5, 10, 30, SSD1306_WHITE); // 绘制实心矩形
oled.setCursor(5, 50); // 设置光标位置
oled.setTextSize(2); // 设置字体大小
oled.setTextColor(WHITE); // 设置文本颜色
oled.println("Hello, world!"); // 显示文字
oled.display(); // 显示内容
}
void loop()
{
}
我们也可以在 OLED 屏幕中实现一个进度条加载的动画效果,代码如下:
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度
// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
// 初始化进度条变量
int progress = 0;
void setup()
{
oled.begin();
oled.setTextSize(2); // 设置字体大小
oled.setTextColor(SSD1306_WHITE); // 设置文本颜色
oled.display(); // 显示内容
}
void loop()
{
// 清空屏幕
oled.clearDisplay();
// 设置光标位置,位置x轴20,y轴40
oled.setCursor(25, 40);
// 显示文字
oled.println("Process");
// 显示进度条边框,方框位置x轴0,y轴10,宽度128,高度20,弧度5,颜色白色
oled.drawRoundRect(0, 10, 128, 20, 5, SSD1306_WHITE);
// 显示进度,填充内容位置x轴5,y轴15,宽度progress的大小,高度10,弧度2,颜色白色
oled.fillRoundRect(5, 15, progress, 10, 2, SSD1306_WHITE);
// 进度递增,progress小于118(128减去左右宽度5的边距)时,progress递增,否则清零
if (progress < 118)
{
progress++;
}
else
{
progress = 0;
}
// 刷新屏幕, 显示内容
oled.display();
delay(50); // 延迟一段时间后更新显示
}
学会使用 Adafruit_SSD1306 库之后,我们再学习另一个并且是 Arduino 平台上使用最广泛的 OLED 库 - U8G2 库open in new window。U8g2 是嵌入式设备的单色图形库,一句话简单明了。主要应用于嵌入式设备,包括我们常见的单片机。
安装方法与 Adafruit_SSD1306 一致,只需要在 PlatformIO 中的 libraries 中搜索对应的库,添加到项目中即可。

为什么要运用 U8g2 库?也就是说 U8g2 库能带给我们什么样的开发便利,主要考虑几个方面:
因为 U8G2 库兼容很多版本的驱动以及不同尺寸的 OLED,所以 U8G2 构造方法有很多,但是我们需要根据我们自己的 OLED 的型号,选择适合我们的构造方法。打开 U8g2lib.h 文件,找到构造器的位置:

我们可以看到这些构造方法的名字有一定的规律:U8G2_驱动芯片_屏幕尺寸_缓存大小_总线,而我们的 OLED 尺寸是 128x64,SPI 总线,SSD1306 驱动,因此,我们可以搜索 U8G2_SSD1306_128X64,

HW 表示硬件(hardware),SW 表示软件(software),4W 表示 4 线,因此,我们就找到了最适合我们的构造器
U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI
这里的 1、2、F 表示不同的缓存大小:
1;只有一页的缓冲区,需要使用 firstPage/nextPage 方法来循环更新屏幕,使用 128 字节的内存;2:保持两页的缓冲区,使用 256 字节的内存;F:保存有完整的显示的缓存,可以使用所有的函数,但是 ram 消耗大,一般用在 ram 空间比较大的开发板;所有的软件模拟总线构造函数的第一个参数都是 rotation, 这个参数表示显示内容是否旋转,U8G2 提供了以下几个选项:
U8G2_R0:不旋转;U8G2_R1:顺时针转 90°;U8G2_R2:顺时针转 180°;U8G2_R3:顺时针转 270°;U8G2_MIRROR:镜像翻转;构造完对象之后,我们就可以学习 U8G2 的方法了,方法可以分为四大类(这里我们只列举了部分,详细内容可以查阅 u8g2 库):
begin():初始化方法;initDisplay():初始化显示控制器,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,会在里面针对具体的 OLED 进行配置;;clearDisplay():清除屏幕内容,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,并且不要在 firstPage 和 nextPage 函数之间调用该方法;clear():清除操作;clearBuffer():清除缓冲区;enableUTF8Print():开启 Arduino 平台下支持输出 UTF8 字符集,我们的中文字符就是UTF8;home():重置显示光标的位置,回到原点(0,0);drawPixel():绘制像素点;drawHLine():绘制水平线;drawLine():两点之间绘制线drawBox():画实心方形;drawFrame():画空心方形drawCircle():画空心圆;drawDisc():画实心圆;drawStr():绘制字符串,需要先设置字体,调用 setFont 方法;drawXBM()/drawXBMP():绘制图像;firstPage()/nextPage():绘制命令,firstPage 方法会把当前页码位置变成 0,修改内容处于 firstPage 和 nextPage 之间,每次都是重新渲染所有内容;print():绘制内容;getDisplayHeight():获取显示器的高度;getDisplayWidth():获取显示器的宽度;setCursor():设置绘制光标位置;setDisplayRotation():设置显示器的旋转角度;setFont():设置字体集(字体集用于字符串绘制方法或者glyph绘制方法);getBufferPtr():获取缓存空间的地址;getBufferTileHeight():获取缓冲区的Tile高度,一个tile等于8个像素点;getBufferTileWidth():获取缓冲区的Tile宽度;getBufferCurrTileRow():获取缓冲区的当前Tile row;clearBuffer():清除内部缓存区;sendBuffer():发送缓冲区的内容到显示器。U8g2 支持以下两种绘制模式:
Full screen buffer mode,全屏缓存模式;Page mode,分页模式;全屏缓存模式使用步骤:
F,因此,需要使用 U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI;了解完构造方法与使用方法之后,我们就可以来在程序中使用 U8G2 库了,代码如下:
#include <Arduino.h>
#include <U8g2lib.h>
// 构造对象
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
void setup(void)
{
// 初始化 oled 对象
u8g2.begin();
// 开启中文字符集支持
u8g2.enableUTF8Print();
}
void loop(void)
{
// 设置字体
u8g2.setFont(u8g2_font_unifont_t_chinese2);
// 设置字体方向
u8g2.setFontDirection(0);
//
u8g2.clearBuffer();
u8g2.setCursor(0, 15);
u8g2.print("Hello GeeksMan!");
u8g2.setCursor(0, 40);
u8g2.print("你好, ESP32!");
u8g2.sendBuffer();
delay(1000);
}
分页模式的使用步骤:
注意
请注意,firstPage() 和 nextPage() 必须配合使用,并在循环中正确调用。另外,确保在每次循环开始时使用 u8g2.clearBuffer() 清除缓冲区,以防止前一页的内容残留在当前页上。
#include <Arduino.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
int progress = 0;
void setup()
{
// 初始化 OLED 对象
u8g2.begin();
}
void loop()
{
// 进入第一页
u8g2.firstPage();
do
{
// 显示进度条边框
u8g2.drawFrame(0, 10, 128, 20);
// 显示进度
u8g2.drawBox(5, 15, progress, 10);
} while (u8g2.nextPage()); // 进入下一页,如果还有下一页则返回true
// 进度递增
if (progress < 118)
{
progress++;
}
else
{
progress = 0;
}
}
在搞清楚 U8G2 库的使用方法之后,我们就可以设计一个按键控制菜单了,UI 大概就是下面这个样子

按键控制菜单的原理其实很简单 -,当我检测到按键按下的时候,就切换屏幕状态,因为只有部分区域发生了改变,让你产生立箭头移动的错觉,代码如下:
/*
* OLED菜单显示程序
* 使用U8g2库控制SSD1306 OLED显示屏
* 通过两个按钮在菜单项之间导航
*/
#include <Arduino.h> // 包含Arduino核心功能库
#include <U8g2lib.h> // 包含U8g2图形库,用于控制OLED显示屏
// 函数声明 - 在PlatformIO中,如果函数定义在调用之后,需要提前声明
void display_menu(unsigned int index);
/*
* 初始化U8g2库实例
* 参数说明:
* U8G2_R0 - 显示旋转方向(0度旋转)
* 18 - 时钟引脚(SCK)
* 13 - 数据引脚(MOSI)
* 4 - 片选引脚(CS)
* 2 - 数据/命令选择引脚(DC)
* 15 - 复位引脚(RESET)
* 使用软件SPI方式驱动OLED显示屏
*/
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
// 定义菜单项数量和内容
#define MENU_SIZE 4 // 菜单项总数
char *menu[MENU_SIZE] = {"Item 1", "Item 2", "Item 3", "Item 4"}; // 菜单项文本
// 定义按钮引脚
#define BUTTON_UP 12 // 向上导航按钮连接的引脚
#define BUTTON_DOWN 14 // 向下导航按钮连接的引脚
// 定义当前选中的菜单项索引
unsigned int order = 0; // 初始选中第一个菜单项(索引0)
/*
* 初始化设置函数
* Arduino启动时自动调用一次,用于初始化硬件和变量
*/
void setup()
{
// 初始化OLED显示屏
u8g2.begin(); // 启动U8g2库
u8g2.setFont(u8g2_font_6x12_tr); // 设置显示字体为6x12像素的字体
// 配置按钮引脚为输入模式,并启用内部上拉电阻
pinMode(BUTTON_UP, INPUT_PULLUP); // 上按钮,默认高电平,按下时变为低电平
pinMode(BUTTON_DOWN, INPUT_PULLUP); // 下按钮,默认高电平,按下时变为低电平
}
/*
* 主循环函数
* 在setup()执行完成后重复执行,实现程序的主要功能
*/
void loop()
{
// 检测上按钮是否按下(低电平表示按下)
if(!digitalRead(BUTTON_UP))
{
delay(10); // 简单延时消抖(实际应用中可能需要更完善的消抖处理)
// 减少当前选项索引,实现向上导航
// 使用模运算确保索引在0到3之间循环
order = (order - 1) % 4;
}
// 检测下按钮是否按下(低电平表示按下)
else if (!digitalRead(BUTTON_DOWN))
{
// 增加当前选项索引,实现向下导航
// 使用模运算确保索引在0到3之间循环
order = (order + 1) % 4;
}
// 显示菜单,传入当前选中的菜单项索引
display_menu(order);
// 延时100毫秒,降低循环执行频率,减少CPU占用
delay(100);
}
/*
* 显示菜单函数
* 在OLED屏幕上绘制菜单界面
* 参数:index - 当前选中的菜单项索引
*/
void display_menu(unsigned int index)
{
// 开始绘制第一页内容
u8g2.firstPage();
do
{
// 绘制菜单标题
u8g2.drawStr(0, 12, "Menu"); // 在坐标(0,12)处绘制"Menu"文本
// 绘制标题下方的水平分隔线
u8g2.drawHLine(0, 14, 128); // 从(0,14)到(128,14)绘制水平线
// 遍历所有菜单项
for (int i = 0; i < MENU_SIZE; i++)
{
// 检查当前项是否为选中的项
if (i == index)
{
// 绘制选中指示符(箭头)和菜单文本
u8g2.drawStr(5, (i + 2) * 12 + 2, ">"); // 在左侧绘制箭头
u8g2.drawStr(20, (i + 2) * 12 + 2, menu[i]); // 绘制菜单文本
}
else
{
// 绘制普通菜单文本(无箭头)
u8g2.drawStr(5, (i + 2) * 12 + 2, menu[i]); // 绘制菜单文本
}
}
} while (u8g2.nextPage()); // 继续处理下一页(如果有),直到所有内容绘制完成
}
在写这个程序的时候,我们用到了以下几个新的知识点,
unsigned int 可以声明小于 0 的整数;do ... while 是先执行一次循环体,再判断的循环;之前我们学习了 ESP32 的按键控制,当时通过查询 GPIO 输入电平来判断按键状态,这种方法占用 CPU 资源,效率不高。本节课我们学习外部中断,通过外部中断实现按键控制 LED。
在单片机中,中断是指当 CPU 在正常处理主程序时,突然发生了另一件事件 A(中断发生)需要 CPU 去处理,这时 CPU 就会暂停处理主程序(中断响应),转而去处理事件 A(中断服务)。当事件 A 处理完以后,再回到主程序原来中断的地方继续执行主程序(中断返回)。这一整个过程称为中断。
如,当你正在洗衣时,突然手机响了(中断发生),你暂时中断洗衣的工作,转去接电话(中断响应和中断服务),待你接完后,再回来继续洗衣(中断返回),这一过程就是中断。

当中断过程 A 中,发生了另一个中断级别更高的中断事件 B,则 CPU 又会中断当前的 A 转而去处理 B,完毕后再回到 A 的断点继续处理。这称为中断的嵌套。

中断的嵌套涉及到中断的优先级问题,优先级高的中断就可以在打断优先级低的中断执行。
中断可以根据中断源分为 硬件中断 和 软件中断:
硬件中断:也被称为外部中断,硬件中断响应外部硬件事件而发生。例如,当检测到触摸时会发生触摸中断,而当 GPIO 引脚的状态发生变化时会发生 GPIO 中断。GPIO 中断和触摸中断属于这一类;软件中断:当触发软件事件(例如定时器溢出)时,会发生这种类型的中断。定时器中断是软件中断的一个例子。前面我们在做按键控制实验时,虽然能实现 IO 口输入功能,但代码是一直在检测 IO 输入口的变化,因此效率不高,特别是在一些特定的场合,比如某个按键,可能 1 天才按下一次去执行相关功能,这样我们就浪费大量时间来实时检测按键的情况。
为了解决这样的问题,我们引入外部中断概念,顾名思义,就是当按键被按下(产生中断)时,才去执行相关功能。这大大节省了 CPU 的资源,因此中断在实际项目中应用非常普遍。
ESP32 的外部中断有上升沿、下降沿、低电平、高电平触发模式。上升沿和下降沿触发如下:

若将按键对应 IO 配置为下降沿触发,当按键按下后即触发中断,然后在中断回调函数内执行对应的功能。
将材料按照下图相连:

Arduino 中的外部中断配置函数 attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) 包括 3 个参数:
pin:GPIO 端口号;
ISR:中断服务程序,没有参数与返回值的函数;
mode
:中断触发的方式,支持以下触发方式:
LOW 低电平触发HIGH 高电平触发RISING 上升沿触发FALLING 下降沿触发CHANGE 电平变化触发在 Arduino 中使用中断需要注意一下几点:
delay()),使用非阻塞的延迟方法来处理需要延迟的操作(micros() 函数),以保证中断的正常执行和系统的稳定性。这是因为 delay() 函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应并避免中断积压;Serial 对象的打印函数。当在中断处理函数中使用 Serial 打印函数时,会导致以下问题:
Serial 打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。Serial 对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用 Serial 打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。为了避免这些问题,建议在中断处理函数中尽量避免使用 Serial 打印函数。如果需要在中断处理函数中输出调试信息,可以使用其他方式,如设置标志位,在主循环中检查标志位并进行打印,代码如下:
#define BUTTON 14
// 定义可以在外部中断函数中使用的变量
volatile bool flag = false;
// 定义外部中断函数
void handle_interrupt() {
flag = true;
number += 10000;
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLDOWN);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, FALLING);
}
void loop() {
if (flag) {
Serial.println("外部中断触发");
flag = false;
}
}
因此,我们使用外部中断点灯的代码可以这么写:
#define BUTTON 14
#define LED 2
//定义标记为false
volatile bool flag = false;
//定义标记函数
void ISR() {
flag = true;
}
void setup() {
//因为按钮接高电平,使用设置按钮输出模式为pulldown
pinMode(BUTTON, INPUT_PULLDOWN);
pinMode(LED, OUTPUT);
// 配置中断引脚
attachInterrupt(digitalPinToInterrupt(BUTTON), ISR, FALLING);
}
void loop() {
if (flag) {
digitalWrite(LED, HIGH);
delay(2000);
digitalWrite(LED, LOW);
// 重置中断标志位
flag = false;
}
}
定时器,顾名思义就是用来计时的,我们常常会设置计时或闹钟,然后时间到了就告诉我们要做什么。ESP32 也是这样,通过定时器可以完成各种预设好的任务。ESP32 定时器到达指定时间后也会产生中断,然后在回调函数内执行所需功能,这个和外部中断类似。
在 Arduino 中操控 ESP32 时,有 硬件定时器 和 软件定时器 两种类型的定时器可供选择。它们具有不同的工作原理和用途。
硬件定时器 是 ESP32 芯片上的内置计时器,它们是专门设计用于定时和计时任务的硬件模块。硬件定时器可以通过设置特定的寄存器来配置和控制,通常具有更高的精确度和稳定性。它们不受软件的影响,可以在后台独立运行,不会受到其他代码的干扰。硬件定时器适用于需要高精度和实时性的定时任务,例如 PWM 输出、捕获输入脉冲等。
软件定时器 是通过编写代码在 Arduino 中模拟实现的定时器。它们不依赖于硬件模块,而是使用计数器变量来实现定时功能。软件定时器是基于延时循环的原理,在特定的时间间隔内执行特定的任务。但是,使用软件定时器时需要注意,它们可能会受到其他代码的影响而产生误差,特别是当涉及到需要精确时间控制的应用时,如通信协议处理、高速数据采集等。
硬件定时器和软件定时器各有优劣,具体选择取决于你的应用需求。如果需要高精度、实时性和稳定性,建议使用硬件定时器。如果时间精度要求不高,或者只需要基本的定时功能,可以使用软件定时器来简化代码编写。
需要注意的是,ESP32 具有 4 个硬件定时器,具体使用哪个定时器取决于你的需求和硬件资源的可用性。请参考 ESP32 的官方文档和相关库的文档以获取更详细的信息和使用示例。
LED 的正极接开发板的 D2、D4 引脚,并串联一个电阻,负极接 GND,如下图:

注意
一定要接电阻,不然会由于电流过大,烧坏 LED。
在 ESP32 Arduino 开发环境中,可以使用以下几个库函数来配置和操作硬件定时器(Timer):
void timerBegin(timer_num_t timer_num, uint32_t divider, bool count_up):初始化硬件定时器,参数说明:timer_num:定时器编号,可选值为 0-3 等。divider:定时器的分频系数,用于设置定时器的时钟频率。较大的分频系数将降低定时器的时钟频率。可以根据需要选择合适的值,一般设置为 80 即可;count_up:指定定时器是否为向上计数模式。设置为 true 表示向上计数,设置为 false 表示向下计数。timerAttachInterrupt(hw_timer_t *timer, void (*isr)(void *), void *arg, int intr_type):用于将中断处理函数与特定的定时器关联起来,参数含义如下:timer;定时器指针;isr: 中断处理函数。arg: 传递给中断处理函数的参数。intr_type: 中断类型,可选值为 ture(边沿触发)或 false(电平触发)。 2.timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload):用于设置定时器的计数值,即定时器触发的时间间隔,参数含义如下:
timer:定时器指针;alarm_value: 定时器的计数值,即触发时间间隔;autoreload: 是否自动重载计数值,可选值为 true(自动重载)或 false(单次触发)。timerAlarmEnable(hw_timer_t *timer):用于启动定时器,使其开始计数;timerAlarmDisable(hw_timer_t *timer):用于禁用定时器,停止计数;timerGetAutoReload(hw_timer_t *timer):获取定时器是否自动重新加载;timerAlarmRead(hw_timer_t *timer):获取定时器计数器报警值;timerStart(hw_timer_t *timer):计数器开始计数;timerStop(hw_timer_t *timer):计数器停止计数;timerRestart(hw_timer_t *timer):计数器重新开始计数,从 0 开始;timerStarted(hw_timer_t *timer):计数器是否开始计数。以上是一些常用的硬件定时器相关的库函数,你可以根据自己的需求和定时器的特性,调用适当的函数来配置和操作硬件定时器。请参考 ESP32 的官方文档和相关库的文档,以获取更详细的信息。
使用硬件定时器的基本步骤如下:
timerBegin() 函数初始化所需的硬件定时器;timerAttachInterrupt() 函数将中断处理函数与定时器关联起来;timerAlarmWrite(),设置触发一次,还是周期性触发;timerAlarmEnable() 函数启动定时器,使其开始计数。因此,我们的代码可以这么写:
#define LED 2
#define LED_ONCE 4
hw_timer_t *timer = NULL;
hw_timer_t *timer_once=NULL;
// 定时器中断处理函数
void timer_interrupt(){
digitalWrite(LED, !digitalRead(LED));
}
void timer_once_interrupt() {
digitalWrite(LED_ONCE, !digitalRead(LED_ONCE));
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(LED_ONCE, OUTPUT);
// 初始化定时器
timer = timerBegin(0,80,true);
timer_once = timerBegin(1, 80, true);
// 配置定时器
timerAttachInterrupt(timer,timer_interrupt,true);
timerAttachInterrupt(timer_once, timer_once_interrupt, true);
// 定时模式,单位us,只触发一次
timerAlarmWrite(timer,1000000,true);
timerAlarmWrite(timer_once, 3000000, false);
// 启动定时器
timerAlarmEnable(timer);
timerAlarmEnable(timer_once);
}
void loop() {
}
使用软件计时器的时候,我们需要用到 ESP32 内置的库 Ticker,Ticker 是 ESP32 Arduino 内置的一个定时器库,这个库用于规定时间后调用函数。
接着我们来看看 Ticker 库的一些方法
detach():停止 Ticker;active():Ticker 是否激活状态,True 表示启用;once(n, callback,arg):n 秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);once_ms(n, callback,arg):n 毫秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);attach(n, callback, arg):每隔 n 秒周期性执行;attach_ms(n, callback, arg):每隔 n 毫秒周期性执行;注意
不建议使用 Ticker 回调函数来阻塞 IO 操作(网络、串口、文件);可以在 Ticker 回调函数中设置一个标记,在 loop 函数中检测这个标记;
#include <Ticker.h>
#define LED 4
#define LED_ONCE 2
// 定义定时器对象
Ticker timer;
Ticker timer_once;
// 定义定时器中断回调函数
void toggle(int pin) {
digitalWrite(pin, !digitalRead(pin));
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(LED_ONCE, OUTPUT);
// 配置周期性定时器
timer.attach(0.5, toggle, LED);
// 配置一次性定时器
timer_once.once(3, toggle, LED_ONCE);
}
void loop() {
}
舵机在电子产品中非常常见,比如四足机器人、固定翼航模等都有应用本节课学习使用 MicroPython 的 PWM 对 SG90 舵机旋转角度控制。
舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。舵机只是一种通俗的叫法,其本质是一个伺服电机。

舵机有很多规格,但所有的舵机都有外接三根线,分别用棕、红、橙三种颜色进行区分,由于舵机品牌不同,颜色也会有所差异,棕色为接地线,红色为电源正极线,橙色为信号线。只要通过信号线给予规定的控制信号即可实现舵机码盘的转动。

SG90 的主要电气参数:
舵机的工作原理是由接收机或者单片机发出信号给舵机,其内部有一个基准电路,将获得的直流偏置电压与电位器的电压比较,获得电压差输出。经由电路板上的 IC 判断转动方向,再驱动无核心马达开始转动,透过减速齿轮将动力传至摆臂,同时由位置检测器送回信号,判断是否已经到达定位。当电机转速一定时,通过级联减速齿轮带动电位器旋转,使得电压差为0,电机停止转动。一般舵机旋转的角度范围是 0 度到 180 度,当然也有 0 度到 360 度。
我们没有必要了解舵机的内部结构,只需要知道如何通过 PWM 控制其转动即可。舵机的控制就是通过一个固定的频率,给其不同的占空比的,来控制舵机不同的转角。
舵机的转动的角度是通过调节 PWM(脉冲宽度调制)信号的占空比来实现的,标准 PWM(脉冲宽度调制)信号的周期固定为 20ms(50Hz),理论上脉宽分布应在 1ms 到 2ms 之间,但是,事实上脉宽可由 0.5ms 到 2.5ms 之间,脉宽和舵机的转角 0°~180° 相对应。有一点值得注意的地方,由于舵机牌子不同,对于同一信号,不同牌子的舵机旋转的角度也会有所不同。

0.5-2.5ms 的 PWM 高电平部分对应控制 180 度舵机的 0-180 度,因此,对应的控制关系是这样的:

| 高电平占整个周期(20ms)的时间 | 舵机旋转的角度 | 对应的占空比 |
|---|---|---|
| 0.5ms | 0° | 0.5 // 20 |
| 1ms | 45° | 1 // 20 |
| 1.5ms | 90° | 1.5 // 20 |
| 2ms | 135° | 2 // 20 |
| 2.5ms | 180° | 2.5 // 20 |

注意
注意接线顺序
首先,我们使用 LEDC 输出 PWM 信号,根据之前的实验原理,我们可以确定频率、最大脉宽 与 最小脉宽,代码如下:
// 1/20 秒,50Hz 的频率,20ms 的周期,这个变量用来存储时钟基准。
#define FREQ 50
// 通道(高速通道(0 ~ 7)由 80MHz 时钟驱动,低速通道(8 ~ 15)由 1MHz 时钟驱动。)
#define CHANNEL 0
// 分辨率设置为 8,就是 2 的 8 次方,用 256 的数值来映射角度。
#define RESOLUTION 8
// 定义舵机 PWM 控制引脚。
#define SERVO 13
//定义函数用于输出 PWM 的占空比
int calculatePWM(int degree)
{
//20ms 周期内,高电平持续时长 0.5-2.5 ms,对应 0-180 度舵机角度。
//对应 0.5ms(0.5ms/(20ms/256))
float min_width = 0.6 / 20 * pow(2, RESOLUTION);
//对应 2.5ms(2.5ms/(20ms/256))
float max_width = 2.5 / 20 * pow(2, RESOLUTION);
if (degree < 0)
degree = 0;
if (degree > 180)
degree = 180;
//返回度数对应的高电平的数值
return (int)(((max_width - min_width) / 180) * degree + min_width);
}
void setup()
{
// 用于设置 LEDC 通道的频率和分辨率
ledcSetup(CHANNEL, FREQ, RESOLUTION);
// 将通道与对应的引脚连接
ledcAttachPin(SERVO, CHANNEL);
}
void loop()
{
for (int i = 0; i <= 180; i += 10)
{
// 输出PWM,设置 LEDC 通道的占空比。
ledcWrite(CHANNEL, calculatePWM(i));
delay(1000);
}
}
我们可以在 VSCode 的 PlatformIO 中,根据案例了解 ESP32Servo 库的使用方法

代码如下:
#include <ESP32Servo.h>
#define SERVO_PIN 13
#define MAX_WIDTH 2500
#define MIN_WIDTH 500
// 定义 servo 对象
Servo my_servo;
void setup() {
// 分配硬件定时器
ESP32PWM::allocateTimer(0);
// 设置频率
my_servo.setPeriodHertz(50);
// 关联 servo 对象与 GPIO 引脚,设置脉宽范围
my_servo.attach(SERVO_PIN, MIN_WIDTH, MAX_WIDTH);
}
void loop() {
my_servo.write(180);
delay(1000);
my_servo.write(0);
delay(1000);
}
连接无线路由器,将 ESP32 的 IP 地址等信息通过 Shell 控制台输出显示。
由于 ESP32 内置 WIFI 功能,所以直接在开发板上使用即可,无需额外连接。
Arduino 已经集成了 Wi-Fi 模块,因此我们可以直接使用该模块。
模块包含热点 AP 模式和客户端 STA 模式,热点 AP 是指电脑或手机端直接连接 ESP32 发出的热点实现连接,如果电脑连接模块 AP 热点,这样电脑就不能上网,因此在使用电脑端和模块进行网络通信时,一般情况下都是使用 STA 模式。也就是电脑和设备同时连接到相同网段的路由器上。
下面是一些 ESP32 Arduino 库中常用的 Wi-Fi 相关函数的介绍:
WiFi.begin(ssid, password):该函数用于连接到 Wi-Fi 网络。需要提供要连接的网络的 SSID 和密码作为参数。
WiFi.disconnect():该函数用于断开当前的 Wi-Fi 连接。
WiFi.status()
:该函数返回当前 Wi-Fi 连接的状态。返回值可能是以下之一:
WL_CONNECTED:已连接到 Wi-Fi 网络。WL_DISCONNECTED:未连接到 Wi-Fi 网络。WL_IDLE_STATUS:Wi-Fi 处于空闲状态。WL_NO_SSID_AVAIL:未找到指定的 Wi-Fi 网络。WiFi.localIP():该函数返回 ESP32 设备在 Wi-Fi 网络中分配的本地 IP 地址。
WiFi.macAddress():该函数返回 ESP32 设备的 MAC 地址。
WiFi.scanNetworks():该函数用于扫描周围可用的 Wi-Fi 网络。它返回一个整数,表示扫描到的网络数量。可以使用其他函数(如 WiFi.SSID() 和 WiFi.RSSI())来获取每个网络的详细信息。
WiFi.SSID(networkIndex):该函数返回指定索引的扫描到的 Wi-Fi 网络的 SSID。
WiFi.RSSI(networkIndex):该函数返回指定索引的扫描到的 Wi-Fi 网络的信号强度(RSSI)。
#include <WiFi.h>
#define LED 2
// 定义 Wi-Fi 名与密码
const char * ssid = "WiFi名";
const char * password = "WiFi密码";
void setup() {
Serial.begin(9600);
// 断开之前的连接
WiFi.disconnect(true);
// 连接 Wi-Fi
WiFi.begin(ssid, password);
Serial.print("正在连接 Wi-Fi");
// 检测是否链接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("连接成功");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// 使用板载 LED 反馈连接成功
pinMode(LED, OUTPUT);
digitalWrite(LED, HIGH);
delay(100);
digitalWrite(LED, LOW);
delay(100);
digitalWrite(LED, HIGH);
delay(100);
digitalWrite(LED, LOW);
delay(100);
digitalWrite(LED, HIGH);
delay(1500);
digitalWrite(LED, LOW);
}
void loop() {
}
在 C 和 C++ 编程语言中,const char * 是一个常见的类型声明,通常用于表示指向字符常量的指针。让我们逐个解释这个声明的各个部分:
const:这是一个关键字,表示指针所指向的数据是常量,即不可修改。使用 const 修饰指针可以确保在使用指针时不会意外地修改所指向的数据;char:这是字符类型的关键字,表示该指针指向的数据是字符类型的数据。char 类型通常用于表示单个字符或字符数组;*:这是指针声明符号,用于表示将声明一个指向特定类型的指针。在这种情况下,它表示将声明一个指向 char 类型数据的指针。const char *:将上述部分组合在一起,表示一个指向字符常量的指针。这意味着该指针指向的字符数据是不可修改的。
可以使用 const char *声明来指向字符串常量,例如:
const char *str = "Hello, world!";
在上面的示例中,str 是一个指向字符常量的指针,它指向存储在内存中的字符串 "Hello, world!"。通过使用 const 关键字,我们确保不会通过 str 指针修改该字符串的内容。
刚才我们已经可以正常连接 WiFi 了,接下来,我们就可以来创建热点了。
#include <WiFi.h>
// 设置要创建的热点名与密码
const char * ssid = "ESP32_AP";
const char * password = "12345678";
void setup() {
Serial.begin(9600);
// 创建热点
WiFi.softAP(ssid, password);
// 打印热点 IP
Serial.print("Wi-Fi 接入的 IP:");
Serial.println(WiFi.softAPIP());
}
void loop() {
}
ESP32支持2.4G网络,通过发送HTTP请求获取API数据,例如获取实时天气数据。一般来说,天气数据是由一些公共 API 接口提供的,这些接口需要向它们发送 HTTP 请求以获取数据。
当我们在浏览器中输入网址或者使用应用程序时,我们实际上是向服务器发出请求。HTTP 请求是客户端(如浏览器)与服务器之间通信的方式,用于获取或发送 Web 资源。这些资源可以是文本文件、图像、脚本等,客户端通过 HTTP 协议发起请求,服务器返回相应的响应。
HTTP 请求通常由以下几个部分组成:
请求行:包含请求方法、请求 URL 和 HTTP 协议版本,例如GET https://www.baidu.com/content-search.xml HTTP/1.1
GET 是请求方法,https://www.baidu com/ 是 URL 地址,HTTP/1.1 指定了协议版本。
HTTP 协议版本一般都是 HTTP/1.1,URL 是你要访问的地址,而请求方法除了 GET 还有 POST、PUT、DELETE 经常使用的 4 个请求方式,以及一些其他的请求方法。
请求头:包含与请求相关的信息,例如浏览器类型、请求时间等。请求体:包含请求所需的数据。我们虽然可以对任意网址发送网络请求,但是这样毫无意义,比如,我想要获取某个地区的天气状况,就需要调用相对应的接口,也就是 API。
API(Application Programming Interface)是指应用程序编程接口,它定义了应用程序之间进行通信的方式和规范。API 允许不同的应用程序之间进行数据交换,使得应用程序可以共享资源和信息,从而提高应用程序的效率和可用性。
API 通常使用 HTTP 请求来提供服务,客户端通过发送 HTTP 请求访问 API,服务器则通过 HTTP 响应返回所需的数据。API 可以提供许多不同的服务,例如访问数据库、获取实时数据、处理图像等。
当我们使用别人提供的 API 的时候就需要遵守别人制定的规则,使用对应的链接、请求方法等等,我们需要查看 API 文档来获取这些信息。比如,我们今天使用聚合数据的 API 接口。

总之,HTTP 请求是客户端与服务器之间通信的方式,API 则是应用程序之间通信的方式。通过 HTTP 请求访问 API,我们可以实现不同应用程序之间的数据交换和共享。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于 Web 应用程序之间的数据传输。它是一种文本格式,易于阅读和编写,并且可以被各种编程语言支持。
JSON 数据由键值对组成,其中键是字符串,值可以是字符串、数字、布尔值、数组、对象等数据类型。一个基本的 JSON 对象看起来像这样:
{
"name": "十八加十八",
"age": 22,
"isStudent": true,
"hobbies": ["打羽毛球", "骑自行车"],
"address": {
"city": "揭阳",
"state": "广东"
}
}
其中,name、age、isStudent、hobbies 和 address 都是键,而对应的值分别是字符串 "十八加十八"、数字 22、布尔值 true、字符串数组 ["打羽毛球", "骑自行车"] 和一个嵌套的 JSON 对象 { "city": "揭阳", "state": "广东" }。
JSON 数据通常用于 Web 应用程序中,例如从后端服务器获取数据或向后端服务器发送数据。在前端 JavaScript 中,可以使用内置的 JSON 对象将 JSON 字符串转换为 JavaScript 对象,或将 JavaScript 对象转换为 JSON 字符串。
想要发送 HTTP 请求,我们就需要用到 HTTPClient 库。
HTTPClient 库是一个用于 Arduino 的 HTTP 客户端库,它提供了一组函数来轻松地发送 HTTP 请求并处理服务器响应。HTTPClient 库基于 ESP-IDF 的 HTTP 客户端实现,并在 Arduino 框架下进行了封装,使其易于使用。
以下是 HTTPClient 库的一些常用功能和函数:
HTTPClient http;:创建 HTTPClient 对象。http.begin(url):指定要发送请求的 URL。http.addHeader(name, value):添加 HTTP 头部。http.setAuthorization(username, password):设置 HTTP 基本身份验证的用户名和密码。http.setTimeout(timeout):设置请求超时时间(以毫秒为单位)。http.GET():发送 GET 请求,并返回一个 HTTP 状态码。http.POST(payload):发送 POST 请求,并将 payload 作为请求正文。http.responseStatusCode():获取响应的状态码。http.responseHeaders():获取响应的头部。http.responseBody():获取响应的正文。http.getString():获取响应正文作为字符串。http.getStream():获取响应正文作为流对象。http.end():关闭连接并释放资源。我们从 Web 服务获取的是 JSON 数据,要想解析 JSON 数据,可以使用 Arduino 的 ArduinoJSON 库。ArduinoJSON 库使您能够解析和生成 JSON 数据,以及在 Arduino 上处理 JSON 格式的数据。
下面是使用 ArduinoJSON 库(v7 及以上版本)解析 JSON 数据的基本步骤:
引入 ArduinoJson.h 头文件;
#include <ArduinoJson.h>
创建一个 JsonDocument 对象来存储和处理 JSON 数据。
在较新版本的 ArduinoJson 库中,DynamicJsonDocument 和 StaticJsonDocument 已被统一为 JsonDocument。您不再需要预先指定大小,库会在解析时自动为您管理内存,这简化了使用流程。
JsonDocument doc; // 无需指定大小
使用 deserializeJson() 函数将 JSON 数据解析到 JsonDocument 对象中,并检查是否出错。
这是一个非常关键的步骤。deserializeJson() 会返回一个 DeserializationError 对象,通过检查这个对象可以判断解析是否成功。
// json 是包含 JSON 数据的字符串或字符数组
DeserializationError error = deserializeJson(doc, json);
// 检查解析是否成功
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return; // 如果解析失败,应停止后续操作
}
doc:JsonDocument 对象,用于存储解析后的 JSON 数据。json:包含 JSON 数据的字符串或字符数组。通过键名 (key) 从 JsonDocument 对象中获取值。
对于基本类型(如 int, float, bool),通常可以直接赋值。对于字符串,推荐使用 const char* 以获得更高效率。如果需要显式类型转换,也可以继续使用 .as<type>() 方法。
// 示例1:直接获取整型值
int temp = doc["result"]["realtime"]["temperature"];
// 示例2:获取字符串,使用 const char* 更高效
const char* info = doc["result"]["realtime"]["info"];
// 示例3:如果需要,也可以使用 .as<String>() 来创建一个String对象
String infoString = doc["result"]["realtime"]["info"].as<String>();
"key":JSON 对象的键。const char* 会直接指向 JsonDocument 内存中的数据,避免了不必要的内存复制,特别适合内存有限的微控制器。因此,我们使用聚合数据的获取实时天气网络数据的代码可以这么写:
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
//WiFi设置
const char* ssid = "Eighteen-WiFi";
const char* password = "zxcvbnm1818";
//定义天气网络数据
String url = "http://apis.juhe.cn/simpleWeather/query";
String city = "佛山";
String key = "51053fd81c1b2ebdb77b9ff9f9d3d6d7";
void setup() {
//设置波特率
Serial.begin(9600);
//断开旧WiFi
WiFi.disconnect(true);
//连接 WiFi
WiFi.begin(ssid,password);
Serial.print("正在连接:WiFi");
// 检测是否连接成功
while (WiFi.status()!=WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println(""); // 换行,让输出更美观
Serial.print("连接成功!");
Serial.print("IP address: ");
//打印WiFi的IP地址
Serial.println(WiFi.localIP());
//创建HTTPClient对象
HTTPClient http;
//发送GET请求
http.begin(url+"?city="+city+"&key="+key);
int http_code =http.GET();
Serial.printf("
Http 状态码:%d
",http_code);
if (http_code > 0) { // 检查请求是否成功
// 获取响应数据
String response =http.getString();
Serial.println("响应数据:");
Serial.println(response);
http.end();
// 创建 JsonDocument 对象 <-- 修改在这里
JsonDocument doc;
// 解析 JSON 数据,并增加错误检查
DeserializationError error = deserializeJson(doc, response);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
// 如果解析失败,则不继续执行
return;
}
// 从解析后的 JSON 文档中获取值
unsigned int temp = doc["result"]["realtime"]["temperature"];
// 使用 const char* 可以更高效获取数据
const char* info = doc["result"]["realtime"]["info"];
int aqi = doc["result"]["realtime"]["aqi"];
Serial.printf("温度:%u
", temp);
Serial.printf("天气:%s
", info);
Serial.printf("空气指数:%d
", aqi);
} else {
Serial.printf("HTTP GET请求失败, 错误: %s
", http.errorToString(http_code).c_str());
http.end();
}
}
void loop() {
}
我们目前使用的这些 API 接口,都是其他公司提供的服务,而如果我们想要搭建自己的 API 接口,该怎么办呢?
首先,你需要明白,想要搭建自己的 API 接口,其实就是搭建你自己的网站,而搭建网站分为前端和后端,前端和后端是指构成Web应用程序的两个主要部分:
后端的功能往往是由编程语言(如 Python、Java 等)和相关框架实现的,而前端通常由 HTML、CSS 和 JavaScript 等技术实现。前端和后端需要通过网络协议(如 HTTP)进行通信,将前端用户输入的数据传递到后端进行处理,再将处理结果返回到前端。
前端和后端的交互使得 Web 应用程序具有强大的功能和可扩展性。
如果你是非科班出身,想要转行做程序员,前端是你最好的选择,WEB 前端是最容易入门的编程岗位,初级前端技术很容易掌握,高级前端需要一步步学习和工作经验的积累。
后端的分类就很多了,国内主流的后端开发使用的是 Java、Go、Python、Node.js、C++、Rust、PHP 等。所以你该怎么选呢?以下仅代表我个人建议:
单片机中最常用的通讯协议有 UART、I2C、SPI。我们已经学习了 I2C 和 SPI。这节课,我们来学习 UART,也就是串口通讯。
串口基本上是所有单片机中都具备的资源外设,使用它可实现程序下载,串口通信等。由于串口通信的简单方便,现如今越来越多的设备和模块支持串口通信功能,让开发工作变得越来越简单且高效。这节课我们来学习如何使用 MicroPython 控制 ESP32 的串口实现数据收发。
要了解串口通信就要先了解串行通信和并行通信:
并行通信 就是说我们的数据字节用多条数据线同时开始发送,这种传输方式只适合短距离传输,这种传输方式使用较少,而且长距离传输成本高,所以只需要简单了解即可;串行通信 是将数据字节一位一位的形式在一条传输线上逐个的传输,只需要一条数据线就可以了。发送时,要把并行数据变成串行数据发送到线路上,接收时,再把串行数据变为并行数据。而关于串行数据传输也分为了两种方式,异步串行通信和同步串行通信,一般同步串行方式使用较少,一般不会使用,不了解也没关系,而一定要了解的是异步串行通信方式。
异步通信 是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程,为使双方收发协调,要求发送和接收的设备的时钟尽可能一致。
异步通信是以字符(构成的帧)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的,当每个字符的各位是以固定的时间传送的,即字符之间不一定有 位间隔 的整数倍关系,但同一字符内的各位之间的距离均为 位间隔 的整数倍。异步通信的一帧字符信息由 4 部分组成,如下图所示:

起始位,数据位,校验位还有就是停止位,由上图所示,一般我们也不需要使用校验位。但是串行通信偶尔也会使用校验位,校验位由名字就可以知道,就是说看你这帧数据有没有错误,在我们的串行通信中一般使用奇偶校验,数据位尾随的 1 位为奇偶校验位。奇校验时,数据中 1 的个数与校验位的和是奇数就为奇校验,反之就是偶校验,接收字符时,我们通过对 1 的个数的校验,若发现 1 的个数不一致,那么就说明数据传输过程中出现了错误。
UART 全称为通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),其工作原理是约定好通讯的波特率,然后将数据一位位地进行传输。

ESP32 有三个硬件 UART:UART0、UART1 和 UART2。它们每个都分配有默认的 GPIO,如下表:
| UART0 | UART1 | UART2 | |
|---|---|---|---|
| TX | 1 | 10 | 17 |
| RX | 3 | 9 | 16 |
UART0 用于下载和 REPL(交互式解释器) 调试,UART1 用于模块内部连接 FLASH,通常也不使用,因此可以使用 UART2 与外部串口设备进行通信。
物料清单(BOM 表):
| 材料名称 | 数量 |
|---|---|
| 串口模块 | 1 |
| 杜邦线(跳线) | 若干 |
ESP32 的 RX2 引脚连串口模块的 TX,TX2 连 RX,让 ESP32 和 串口模块都连接电脑。
将材料按照下图相连:

我们在之前一直都在使用 Serial 对象,在 Arduino IDE 的串口监视器中显示信息,其实这个 Serial,就是在 HardwareSerial.h 头文件中定义好的,它提供了与硬件串口通信相关的类和函数。该头文件定义了 HardwareSerial 类,允许你使用硬件串口进行通信。
HardwareSerial 类是用于访问和控制硬件串口的主要类。你可以使用预定义好的 HardwareSerial 对象(Serial、Serial1、Serial2 分别对应了 UART0、UART1 和 UART2。)与特定的硬件串口进行通信。
下面是 HardwareSerial 类的常用函数:
begin():初始化硬件串口的通信。你需要在使用硬件串口之前调用该函数,并指定所需的波特率。available():检查是否有可用的串口数据可供读取。如果串口接收缓冲区中有数据,该函数将返回一个大于0的值。read():从串口接收缓冲区中读取一个字节的数据,并将其作为无符号字节返回。write():向串口发送数据。你可以使用该函数发送单个字节、字符数组或字符串。print() 和 println():这些函数可用于将数据以文本形式发送到串口。你可以使用这些函数来发送数字、字符串、字符和其他数据类型的内容。所以,我们就可以使用 Serial 与 Serial2 实现两个串口之间的信息交互,代码如下:
void setup() {
// 初始化串口通信波特率
Serial.begin(9600);
Serial2.begin(9600);
}
void loop() {
// 从串口监视器读取输入数据
if (Serial.available()) {
char data = Serial.read();
// 将数据发送到 UART2
Serial2.write(data);
}
// 从UART2读取输入数据
if (Serial2.available()) {
char data = Serial2.read();
// 将数据发送到 UART0
Serial.write(data);
}
}
Article Navigation System © 2025
欢迎来到我的留言板,留下你的足迹,与我分享你的想法和感受。
点击文本框会有惊喜哦`(。•̀ᴗ-)✧