3. GPIO与基本外设控制:GPIO配置与中断、LEDC(PWM)控制、ADC采样、定时器(Timer)使用

各位好,欢迎来到第三章。这一章我们聊聊嵌入式开发里最基础、也最绕不开的东西——GPIO和外设控制。

你可能会想,GPIO谁不会啊?不就是拉高拉低嘛。嗯,没错,但ESP-IDF里有些细节,搞不好会让你调一整天。我刚开始用ESP32时,就因为在GPIO中断里做了个延时,结果系统直接死机。后来才明白,中断服务程序(ISR)里不能干重活,这是铁律。

好,我们一个一个来看。

3.1 GPIO配置与中断

ESP32的GPIO功能很丰富,但配置起来其实就几步:选引脚、设方向、配上下拉、使能中断。我个人习惯用结构体一次性配完,清晰又不容易漏。

核心要点:GPIO配置推荐使用gpio_config_t结构体,而不是逐个调用API。这样代码更紧凑,也方便后期维护。

先看一个典型配置:

// 配置一个按键输入,带下降沿中断
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << GPIO_NUM_0),   // 选择GPIO0
    .mode = GPIO_MODE_INPUT,                  // 输入模式
    .pull_up_en = GPIO_PULLUP_ENABLE,         // 内部上拉
    .pull_down_en = GPIO_PULLDOWN_DISABLE,    // 下拉关闭
    .intr_type = GPIO_INTR_NEGEDGE            // 下降沿触发
};
gpio_config(&io_conf);

配置完别忘了安装中断服务:

// 安装GPIO中断服务
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);

// 注册中断回调
gpio_isr_handler_add(GPIO_NUM_0, button_isr_handler, NULL);

我曾经踩过的坑:中断回调函数里千万不要做printf、延时、或者任何阻塞操作。ISR里只做两件事:清标志位、用队列或信号量通知任务。否则轻则看门狗复位,重则系统崩溃。

中断回调的推荐写法:

static void IRAM_ATTR button_isr_handler(void* arg) {
    // 只发信号,不做处理
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

然后在任务里处理实际逻辑。这样既快又安全。

3.2 LEDC(PWM)控制

LEDC是ESP32的硬件PWM控制器,说白了就是专门用来产生PWM波形的模块。它支持多通道、多定时器,还能自动调频率。我常用它来控制LED亮度或电机速度。

配置LEDC分三步:

  1. 配置定时器——决定PWM的频率和分辨率
  2. 配置通道——绑定引脚和定时器
  3. 设置占空比——输出想要的PWM波形

看代码:

// 1. 配置定时器
ledc_timer_config_t timer_conf = {
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .duty_resolution = LEDC_TIMER_13_BIT,   // 13位分辨率,0~8191
    .timer_num = LEDC_TIMER_0,
    .freq_hz = 5000,                         // 5kHz
    .clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);

// 2. 配置通道
ledc_channel_config_t channel_conf = {
    .gpio_num = GPIO_NUM_2,
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .channel = LEDC_CHANNEL_0,
    .timer_sel = LEDC_TIMER_0,
    .duty = 0,
    .hpoint = 0
};
ledc_channel_config(&channel_conf);

// 3. 设置占空比(渐变效果)
ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 4096, 1000);
ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);

小技巧:用ledc_set_fade_with_time实现呼吸灯效果,比手动循环平滑得多。硬件自动完成渐变,CPU几乎零负担。

LEDC还有一个好处:它支持多通道独立输出,互不干扰。你可以在一个定时器上挂多个通道,频率一致但占空比各自独立。这在做RGB灯或舵机控制时特别方便。

3.3 ADC采样

ADC就是把模拟电压转成数字值。ESP32有两个SAR ADC模块,总共支持最多18个通道。但要注意,Wi-Fi开启时ADC2会被占用,所以建议优先用ADC1。

特性 ADC1 ADC2
可用通道数 8 (GPIO32~39) 10 (GPIO0,2,4,12~15,25~27)
Wi-Fi影响 无影响 Wi-Fi开启时不可用
分辨率 12位 (0~4095) 12位 (0~4095)
推荐场景 电池电压、传感器 仅在Wi-Fi关闭时使用

ADC配置也很直接:

// 配置ADC1通道0(GPIO36)
adc1_config_width(ADC_WIDTH_BIT_12);           // 12位分辨率
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);  // 0~3.3V量程

// 读取采样值
int raw = adc1_get_raw(ADC1_CHANNEL_0);
float voltage = raw * 3.3 / 4095.0;  // 换算成电压

注意:ADC的输入电压不能超过3.3V。如果需要测量更高电压,必须用分压电阻。我曾经因为没加分压直接烧了一个引脚,教训深刻。

如果你需要更稳定的采样值,建议做多次采样取平均,或者用硬件连续模式。ESP-IDF也提供了adc_read和adc_read_continuous接口,适合高精度场景。

3.4 定时器(Timer)使用

ESP32有4个64位通用定时器,每个都可以独立配置。定时器说白了就是个硬件计数器,到了设定值就触发中断。我常用它来做周期性任务,比如每10ms采集一次传感器数据。

配置定时器分四步:

  1. 选择定时器——TIMER_0到TIMER_3
  2. 设置分频和初始值——决定计时精度
  3. 设置报警值——触发中断的时间点
  4. 使能中断并启动

看一个10ms定时器的例子:

// 定时器配置结构体
timer_config_t config = {
    .divider = 80,                    // 80分频,APB时钟80MHz → 1MHz
    .counter_dir = TIMER_COUNT_UP,    // 向上计数
    .counter_en = TIMER_PAUSE,        // 初始暂停
    .alarm_en = TIMER_ALARM_EN,       // 使能报警
    .auto_reload = TIMER_AUTORELOAD_EN, // 自动重载
};
timer_init(TIMER_GROUP_0, TIMER_0, &config);

// 设置报警值:10ms = 10000个计数(1MHz下)
timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 10000);

// 注册中断回调
timer_isr_callback_add(TIMER_GROUP_0, TIMER_0, timer_isr_handler, NULL, 0);

// 启动定时器
timer_start(TIMER_GROUP_0, TIMER_0);

中断回调同样要轻量:

static bool IRAM_ATTR timer_isr_handler(void* arg) {
    // 通知任务处理
    BaseType_t high_task_awoken = pdFALSE;
    xSemaphoreGiveFromISR(timer_sem, &high_task_awoken);
    return high_task_awoken == pdTRUE;  // 返回是否唤醒高优先级任务
}

我的经验:定时器中断里尽量用信号量或任务通知,而不是直接处理业务逻辑。这样既保证了实时性,又不会阻塞其他中断。

定时器的精度取决于分频系数。80分频下每个计数是1微秒,适合毫秒级任务。如果需要更高精度,可以降低分频,但要注意中断频率不能太高,否则CPU会被中断淹没。

知识结构总览

下面这张图把本章四个外设的关系和配置流程串起来了,方便你对照理解:

第三章 外设控制知识结构图 ESP32 外设控制 GPIO 配置与中断 LEDC (PWM) 控制 ADC 采样 定时器 (Timer) gpio_config_t 结构体 中断服务注册 ISR → 队列/信号量 定时器配置 通道绑定引脚 占空比渐变 ADC1 优先使用 衰减设置 多次采样取平均 分频与计数方向 报警值与自动重载 ISR → 信号量通知 核心原则:ISR 轻量化 + 配置结构体化

这张图把四个外设的配置流程和关键点都列出来了。你可以把它当成速查表,做项目时对照着来,不容易漏步骤。

好了,第三章就到这里。GPIO、PWM、ADC、定时器,这四个东西是嵌入式开发的基石。你把它玩熟了,后面做低功耗、做配网,心里就有底了。