2. ESP32 基础外设控制:GPIO 数字输入输出、PWM 调光、ADC 模拟量采集、中断机制与回调函数
好,咱们直接进入正题。ESP32 的外设控制,是后续所有交互和通信的基础。你想想看,无论是读取传感器、控制电机,还是驱动 LED 灯带,最终都要落到这些基础操作上。
我个人习惯把这一章叫做「硬件的敲门砖」。搞懂了 GPIO、PWM、ADC 和中断,你基本就能和物理世界对话了。咱们一个一个来。
2.1 GPIO 数字输入输出:最基础的「0」和「1」
GPIO 就是通用输入输出引脚。说白了,就是让芯片的某个引脚输出高电平(3.3V)或低电平(0V),或者读取外部信号是高还是低。
在 ESP32 上,大部分引脚都可以作为 GPIO 使用。但要注意,有些引脚有特殊功能(比如接 Flash 的引脚),不能乱用。
2.1.1 输出模式:点亮一个 LED
先来个最简单的,点亮板载 LED。ESP32 开发板上通常有一个 GPIO2 连接的 LED。
// 配置 GPIO2 为输出模式
pinMode(2, OUTPUT);
// 点亮 LED
digitalWrite(2, HIGH);
// 熄灭 LED
digitalWrite(2, LOW);
就这么简单。但实际项目中,我建议你养成一个好习惯:用宏定义给引脚起个名字。
#define LED_BUILTIN 2
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
2.1.2 输入模式:读取按键状态
读取按键输入,需要配置为 INPUT 模式。但这里有个坑:浮空输入。
如果引脚什么都没接,读取到的电平是随机的。所以,我们需要上拉或下拉电阻。
ESP32 内部有上拉电阻,可以直接启用:
#define BUTTON_PIN 0
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // 启用内部上拉
Serial.begin(115200);
}
void loop() {
int val = digitalRead(BUTTON_PIN);
Serial.println(val);
delay(100);
}
注意,启用内部上拉后,按键没按下时读到的是 HIGH,按下后是 LOW。逻辑是反的,写代码时心里要有数。
2.2 PWM 调光:用数字信号模拟模拟量
PWM,脉冲宽度调制。说白了,就是快速开关引脚,通过调整高电平占整个周期的比例(占空比),来模拟不同的电压。
ESP32 的 PWM 比 Arduino 强多了。它有独立的 LEDC 控制器,可以设置频率、分辨率,还能绑定到任意 GPIO。
2.2.1 配置 PWM 通道
ESP32 的 PWM 分三步:配置通道、绑定引脚、设置占空比。
#define LED_PIN 2
#define PWM_CH 0
#define PWM_FREQ 5000
#define PWM_RES 8
void setup() {
ledcSetup(PWM_CH, PWM_FREQ, PWM_RES);
ledcAttachPin(LED_PIN, PWM_CH);
}
void loop() {
// 从暗到亮
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(PWM_CH, duty);
delay(10);
}
}
这里的分辨率 8 位,意味着占空比范围是 0~255。频率 5000Hz,对人眼来说已经看不到闪烁了。
| 参数 | 说明 | 常用值 |
|---|---|---|
| 频率 | PWM 信号的开关速度 | LED 调光用 1kHz~5kHz |
| 分辨率 | 占空比的精细程度 | 8 位(0~255)或 10 位(0~1023) |
| 通道数 | ESP32 最多 16 个独立通道 | 够用 |
2.3 ADC 模拟量采集:读取真实世界的电压
ADC 把模拟电压转换成数字值。ESP32 有两个 ADC 模块,ADC1 和 ADC2。ADC2 在 Wi-Fi 开启时会被占用,所以一般都用 ADC1。
2.3.1 基本读取
#define ADC_PIN 34
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 设置分辨率 12 位
}
void loop() {
int val = analogRead(ADC_PIN);
float voltage = val * (3.3 / 4095.0);
Serial.print("ADC Value: ");
Serial.print(val);
Serial.print(" Voltage: ");
Serial.println(voltage);
delay(100);
}
12 位分辨率,范围 0~4095。对应电压 0~3.3V。
2.3.2 衰减模式
如果你要测超过 3.3V 的电压,可以用衰减模式。ESP32 支持 0dB、2.5dB、6dB、11dB 四档衰减。
analogSetAttenuation(ADC_11db); // 最大可测约 3.6V
但说实话,衰减模式会降低精度。我建议还是用分压电阻把电压降到 3.3V 以内。
2.4 中断机制与回调函数:让系统响应更及时
轮询(Polling)是傻等,中断(Interrupt)是通知。你想想看,如果 CPU 一直在循环里检查按键是否按下,那它什么都干不了。
中断就是:让 CPU 该干嘛干嘛,一旦有事件发生,立刻跳转去处理。
2.4.1 GPIO 中断
ESP32 支持所有 GPIO 的中断。可以配置上升沿、下降沿、或者电平触发。
volatile int interruptCounter = 0;
void IRAM_ATTR handleInterrupt() {
interruptCounter++;
}
void setup() {
pinMode(0, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(0), handleInterrupt, FALLING);
Serial.begin(115200);
}
void loop() {
if (interruptCounter > 0) {
noInterrupts();
int count = interruptCounter;
interruptCounter = 0;
interrupts();
Serial.print("Interrupt count: ");
Serial.println(count);
}
}
这里有几个关键点:
- IRAM_ATTR:把中断函数放到 IRAM 中,保证执行速度。
- volatile:告诉编译器这个变量可能被中断修改,不要优化掉。
- noInterrupts()/interrupts():在读取共享变量时关中断,防止数据竞争。
2.4.2 定时器中断
除了 GPIO 中断,定时器中断也很常用。比如你要精确控制采样频率。
hw_timer_t *timer = NULL;
volatile bool timerFlag = false;
void IRAM_ATTR onTimer() {
timerFlag = true;
}
void setup() {
timer = timerBegin(0, 80, true); // 定时器0,预分频80,向上计数
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000000, true); // 1秒中断一次
timerAlarmEnable(timer);
}
void loop() {
if (timerFlag) {
timerFlag = false;
Serial.println("Timer fired!");
}
}
预分频 80,是因为 ESP32 的主频是 80MHz。80 分频后,计数器每 1 微秒加 1。设置报警值 1000000,就是 1 秒。
2.5 本章知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。
2.6 综合实战:呼吸灯 + 按键控制
最后,咱们把今天学的知识串起来。做一个呼吸灯,用按键切换呼吸速度。
#define LED_PIN 2
#define BTN_PIN 0
#define PWM_CH 0
#define PWM_FREQ 5000
#define PWM_RES 8
volatile bool speedToggle = false;
void IRAM_ATTR btnISR() {
speedToggle = !speedToggle;
}
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BTN_PIN), btnISR, FALLING);
ledcSetup(PWM_CH, PWM_FREQ, PWM_RES);
ledcAttachPin(LED_PIN, PWM_CH);
}
void loop() {
int delayTime = speedToggle ? 5 : 20;
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(PWM_CH, duty);
delay(delayTime);
}
for (int duty = 255; duty >= 0; duty--) {
ledcWrite(PWM_CH, duty);
delay(delayTime);
}
}
这个例子把 GPIO 中断、PWM 调光、以及主循环逻辑都结合起来了。你按下按键,呼吸速度就会变化。