GPIO基础与LED控制:数字输出、GPIO模式设置、LED闪烁、PWM调光、呼吸灯实验
好,咱们正式开始动手了。这一章我带你从最基础的GPIO玩起。你想想看,不管多复杂的物联网设备,最终都要跟物理世界打交道——要么输出信号控制东西,要么读取传感器的数据。而GPIO,就是ESP32跟外界对话的最基本通道。
我个人习惯,每接触一块新开发板,第一件事就是点亮一个LED。这就像程序员学新语言先写"Hello World"一样,能最快验证你的硬件、软件和工具链是不是都跑通了。
GPIO是什么?
GPIO,全称General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以把它设成输出模式,往外送高电平(3.3V)或低电平(0V);也可以设成输入模式,读取外部信号是高还是低。
ESP32有34个GPIO引脚,但不是所有都能随便用。我刚开始做项目时就踩过这个坑——有些引脚内部已经连到了Flash或PSRAM,你用它们做普通IO,系统直接罢工。
| 引脚范围 | 可用性 | 注意事项 |
|---|---|---|
| GPIO0-19 | 大部分可用 | GPIO6-11用于Flash,别碰 |
| GPIO21-23 | 完全可用 | 常用作I2C、SPI |
| GPIO25-27 | 完全可用 | DAC输出也在这些脚上 |
| GPIO32-39 | 可用 | GPIO34-39只能输入,不能输出 |
GPIO模式设置
ESP32的GPIO模式比普通单片机丰富得多。除了基本的输入输出,还有开漏、上拉、下拉、中断等模式。咱们先聚焦最常用的几种:
- GPIO_MODE_OUTPUT — 数字输出模式,驱动LED、继电器等
- GPIO_MODE_INPUT — 数字输入模式,读取按键、传感器信号
- GPIO_MODE_OUTPUT_OD — 开漏输出,常用于I2C总线
- GPIO_MODE_INPUT_OUTPUT — 输入输出双向模式
设置模式用这个函数:
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
嗯,就这么一行。但要注意,ESP-IDF里GPIO编号是从0开始的宏定义,别写成数字2就完事了,要用GPIO_NUM_2这种形式。
LED闪烁实验
来,咱们写个最经典的LED闪烁程序。我习惯用GPIO2,因为很多ESP32开发板把板载LED接在这个脚上。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_GPIO GPIO_NUM_2
void app_main(void)
{
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
while(1) {
gpio_set_level(LED_GPIO, 1); // 点亮
vTaskDelay(500 / portTICK_PERIOD_MS); // 延时500ms
gpio_set_level(LED_GPIO, 0); // 熄灭
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
这里有个细节:vTaskDelay的参数单位是"tick",不是毫秒。用portTICK_PERIOD_MS做换算,才能得到我们熟悉的毫秒延时。我见过不少新手直接写vTaskDelay(500),结果LED闪得飞快——因为500个tick可能只有几毫秒。
PWM调光
数字输出只能让LED要么全亮要么全灭,太单调了。想实现从暗到亮的渐变效果,就得用PWM——脉冲宽度调制。
PWM的原理其实很简单:用固定频率的方波,通过改变高电平占整个周期的比例(占空比),来模拟不同的电压值。占空比0%就是全灭,100%就是全亮,50%就是一半亮度。
ESP32的PWM通过LEDC(LED Control)外设实现。它支持16个通道,每个通道可以独立设置频率和占空比。
#include "driver/ledc.h"
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // 13位分辨率,占空比范围0-8191
#define LEDC_FREQUENCY 5000 // 5kHz PWM频率
void pwm_init(void)
{
ledc_timer_config_t timer_config = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_config);
ledc_channel_config_t channel_config = {
.gpio_num = LED_GPIO,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_config);
}
void set_brightness(uint32_t duty)
{
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
分辨率13位是什么意思?就是占空比可以从0到8191之间取值。分辨率越高,亮度调节越细腻。但要注意,分辨率越高,能支持的最高PWM频率就越低。这是个取舍。
| 分辨率(位) | 占空比范围 | 最大频率(约) | 适用场景 |
|---|---|---|---|
| 8 | 0-255 | 312.5kHz | 普通LED调光 |
| 10 | 0-1023 | 78.1kHz | 电机控制 |
| 13 | 0-8191 | 9.7kHz | 高精度调光 |
| 16 | 0-65535 | 1.2kHz | 音频输出 |
呼吸灯实验
有了PWM,呼吸灯就水到渠成了。呼吸灯的效果就是LED从暗慢慢变亮,再慢慢变暗,循环往复,像人呼吸一样。
实现思路:用一个变量表示当前占空比,在循环里逐渐增加或减少它的值。到达上限就改为递减,到达下限就改为递增。
void breath_led(void)
{
uint32_t duty = 0;
int8_t step = 8; // 每次变化的步长
while(1) {
set_brightness(duty);
vTaskDelay(10 / portTICK_PERIOD_MS);
duty += step;
if(duty >= 8191) {
duty = 8191;
step = -step; // 到达上限,改为递减
} else if(duty <= 0) {
duty = 0;
step = -step; // 到达下限,改为递增
}
}
}
步长设成8,是因为13位分辨率有8192个等级。如果步长是1,一次呼吸要16秒多,太慢了。步长8的话,一次呼吸大约2秒,效果比较自然。
知识结构图
下面这张图把本章的知识点串起来了,你可以看到从GPIO基础到呼吸灯实现的完整路径:
避坑指南
做这几个实验时,有几个坑我替你们先踩过了:
- 引脚电压问题: ESP32的GPIO输出是3.3V,不是5V。如果你接的是5V的LED灯珠,记得串一个限流电阻,不然可能烧引脚。我一般用220Ω或330Ω。
- PWM频率选择: 控制LED用1kHz-10kHz就够了。频率太低会看到闪烁,频率太高会增加功耗。我习惯用5kHz,人眼完全看不出闪烁,功耗也合理。
- 占空比突变: 呼吸灯里如果步长太大,亮度变化会一跳一跳的,不自然。步长设成总分辨率的0.1%左右比较合适。
- FreeRTOS延时:
vTaskDelay会让出CPU给其他任务,这是好事。但如果你在延时期间有紧急事情要做,就得用vTaskDelayUntil保证周期性。