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只能输入,不能输出
⚠ 我曾经犯过的错: 第一次用ESP32时,我随手选了GPIO6接LED,结果怎么都点不亮。查了半天手册才发现,GPIO6-11被内部Flash占用了。从那以后,我画原理图前一定会先查引脚功能表。

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可能只有几毫秒。

💡 我的调试习惯: 写延时的时候,我一般先用1000ms验证逻辑对不对,再改成需要的值。这样一眼就能看出程序是不是在跑,不用拿示波器去量。

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秒,效果比较自然。

🎯 实际项目经验: 我在做智能家居面板时,用呼吸灯做待机指示。但发现一个问题:人眼对亮度的感知不是线性的。占空比从0到100变化时,人眼觉得前段变化明显,后段几乎没变化。解决办法是用指数曲线或查表法做gamma校正。这个后面讲到传感器数据可视化时再细聊。

知识结构图

下面这张图把本章的知识点串起来了,你可以看到从GPIO基础到呼吸灯实现的完整路径:

GPIO基础与LED控制知识体系 GPIO基础概念 GPIO模式设置 数字输出控制 引脚功能选择 LED闪烁实验 延时控制与任务调度 PWM调光原理 频率 · 占空比 · 分辨率 呼吸灯实验 渐变算法 · 步长控制 · 循环逻辑 从GPIO基础 → 数字输出 → PWM → 呼吸灯,层层递进 每个环节都是下一个环节的基石

避坑指南

做这几个实验时,有几个坑我替你们先踩过了:

  • 引脚电压问题: ESP32的GPIO输出是3.3V,不是5V。如果你接的是5V的LED灯珠,记得串一个限流电阻,不然可能烧引脚。我一般用220Ω或330Ω。
  • PWM频率选择: 控制LED用1kHz-10kHz就够了。频率太低会看到闪烁,频率太高会增加功耗。我习惯用5kHz,人眼完全看不出闪烁,功耗也合理。
  • 占空比突变: 呼吸灯里如果步长太大,亮度变化会一跳一跳的,不自然。步长设成总分辨率的0.1%左右比较合适。
  • FreeRTOS延时: vTaskDelay会让出CPU给其他任务,这是好事。但如果你在延时期间有紧急事情要做,就得用vTaskDelayUntil保证周期性。
🔧 调试小技巧: 拿不准PWM频率和占空比对不对?用逻辑分析仪或者示波器看波形最直接。没有仪器的话,用手机摄像头对着LED——如果看到滚动条纹,说明PWM频率和手机快门频率在打架,换个频率试试。