Chapter 02

GPIO、ADC 与 PWM

掌握 ESP32 的通用 IO 矩阵、模数转换校准与 PWM 调光控制

ESP32 GPIO 矩阵

ESP32 最独特的设计之一是 GPIO 矩阵(GPIO Matrix)。传统微控制器的外设引脚是固定绑定的(如 UART TX 只能在某几个特定引脚),而 ESP32 的 GPIO 矩阵允许几乎任意外设信号路由到任意 GPIO 引脚。这极大简化了 PCB 布线。

GPIO 矩阵示意图 外设信号(UART/SPI/I2C/PWM...) │ ▼ ┌─────────────────────────────────┐ │ GPIO Matrix │ │ 任意输入信号 → 任意 GPIO │ │ 任意 GPIO → 任意输出信号 │ └─────────────────────────────────┘ │ ▼ GPIO 0 ~ GPIO 39 (其中 GPIO 34-39 仅输入) 注意:以下 GPIO 有特殊用途,谨慎使用: GPIO 6-11:连接内部 Flash,禁止占用 GPIO 0:Boot 模式选择(上电时需为高) GPIO 2:影响 Boot,板载 LED GPIO 12:Flash 电压选择,影响启动

GPIO 关键概念词表

GPIO_MODE_INPUT
输入模式:读取外部电平(0/1),常用于按钮、传感器数字输出引脚。
GPIO_MODE_OUTPUT
推挽输出:可驱动高(3.3V)或低(GND),最大输出电流约 40mA(不建议长期超过 12mA)。
GPIO_MODE_OUTPUT_OD
开漏输出(Open-Drain):只能拉低,高电平需外部上拉电阻。I2C 总线使用此模式,允许多设备共享总线。
内部上拉/下拉
ESP32 每个 GPIO 内置约 45kΩ 上拉或下拉电阻,可软件启用,省去外部电阻。按钮通常使用内部上拉,按下后接地。
中断触发类型
支持上升沿、下降沿、双边沿、高电平、低电平五种触发方式。中断服务例程(ISR)必须标记 IRAM_ATTR 以便从 IRAM 执行,避免 Flash Cache 未命中导致崩溃。

GPIO 配置与中断示例

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"

#define BTN_PIN   GPIO_NUM_0   // 通常是 BOOT 按钮
#define LED_PIN   GPIO_NUM_2

static QueueHandle_t gpio_evt_queue;
static const char *TAG = "GPIO";

/* ISR 必须标记 IRAM_ATTR,不得调用非 IRAM 函数 */
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
    uint32_t gpio_num = (uint32_t)arg;
    /* 从 ISR 向队列发送,不阻塞 */
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void gpio_task(void *arg)
{
    uint32_t gpio_num;
    while (1) {
        if (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
            ESP_LOGI(TAG, "GPIO[%" PRIu32 "] 触发,当前电平: %d",
                     gpio_num, gpio_get_level(gpio_num));
            /* 切换 LED */
            static int led_state = 0;
            led_state = !led_state;
            gpio_set_level(LED_PIN, led_state);
        }
    }
}

void app_main(void)
{
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));

    /* 配置 LED 输出 */
    gpio_config_t led_cfg = {
        .pin_bit_mask = (1ULL << LED_PIN),
        .mode         = GPIO_MODE_OUTPUT,
    };
    gpio_config(&led_cfg);

    /* 配置按钮输入,内部上拉,下降沿触发中断 */
    gpio_config_t btn_cfg = {
        .pin_bit_mask = (1ULL << BTN_PIN),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .intr_type    = GPIO_INTR_NEGEDGE,
    };
    gpio_config(&btn_cfg);

    /* 安装 GPIO 中断服务(只调用一次) */
    gpio_install_isr_service(0);
    gpio_isr_handler_add(BTN_PIN, gpio_isr_handler, (void *)BTN_PIN);

    xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);
}

ADC — 模数转换

ESP32 有两组 ADC:ADC1(8 个通道,GPIO 32-39)和 ADC2(10 个通道,GPIO 0/2/4/12-15/25-27)。ADC 将 0~3.3V 的模拟电压转换为 0~4095 的 12bit 数字值。

ADC2 与 Wi-Fi 不兼容

ESP32 的 ADC2 由 Wi-Fi 硬件共享使用。当 Wi-Fi 启用时,ADC2 完全不可用,调用会返回错误。如果你的项目需要 Wi-Fi,请将所有模拟传感器连接到 ADC1(GPIO 32-39)

ADC 通道与 GPIO 对应关系

ADC1 通道(Wi-Fi 启用时仍可用): ADC1_CH0 → GPIO 36 (VP) ADC1_CH1 → GPIO 37 ADC1_CH2 → GPIO 38 ADC1_CH3 → GPIO 39 (VN) ADC1_CH4 → GPIO 32 ADC1_CH5 → GPIO 33 ADC1_CH6 → GPIO 34 ADC1_CH7 → GPIO 35 ADC2 通道(Wi-Fi 启用时不可用): ADC2_CH0 → GPIO 4 ADC2_CH5 → GPIO 12 ADC2_CH1 → GPIO 0 ADC2_CH6 → GPIO 14 ADC2_CH2 → GPIO 2 ADC2_CH7 → GPIO 27 ADC2_CH3 → GPIO 15 ADC2_CH8 → GPIO 25 ADC2_CH4 → GPIO 13 ADC2_CH9 → GPIO 26

ADC 校准与读取

#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_log.h"

static const char *TAG = "ADC";

void app_main(void)
{
    /* 1. 初始化 ADC 单次模式句柄 */
    adc_oneshot_unit_handle_t adc1_handle;
    adc_oneshot_unit_init_cfg_t init_config = {
        .unit_id = ADC_UNIT_1,
    };
    adc_oneshot_new_unit(&init_config, &adc1_handle);

    /* 2. 配置通道(12bit,11dB 衰减 → 量程 0~3.3V)*/
    adc_oneshot_chan_cfg_t chan_cfg = {
        .bitwidth = ADC_BITWIDTH_DEFAULT,   // 12bit
        .atten    = ADC_ATTEN_DB_11,        // 0~3.3V 量程
    };
    adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_6, &chan_cfg); // GPIO34

    /* 3. 创建校准方案(Curve Fitting 或 Line Fitting)*/
    adc_cali_handle_t cali_handle = NULL;
    adc_cali_curve_fitting_config_t cali_cfg = {
        .unit_id  = ADC_UNIT_1,
        .chan     = ADC_CHANNEL_6,
        .atten    = ADC_ATTEN_DB_11,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle);

    while (1) {
        int raw = 0, voltage_mv = 0;
        adc_oneshot_read(adc1_handle, ADC_CHANNEL_6, &raw);
        adc_cali_raw_to_voltage(cali_handle, raw, &voltage_mv);
        ESP_LOGI(TAG, "ADC 原始值: %d  电压: %d mV", raw, voltage_mv);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
ADC 衰减(Attenuation)选择

ADC_ATTEN_DB_0:0~1.1V(最精准,适合低压传感器)
ADC_ATTEN_DB_2_5:0~1.5V
ADC_ATTEN_DB_6:0~2.2V
ADC_ATTEN_DB_11:0~3.3V(最常用,满量程)

ledc — LED 控制器 PWM

PWM(脉宽调制)通过快速切换 GPIO 高低电平,利用占空比(duty cycle)模拟模拟信号。ESP32 的 ledc(LED Control)模块提供最多 16 路 PWM,分为高速(HS,8路)和低速(LS,8路)两组,频率和精度可独立配置。

PWM 信号原理: 占空比 = 高电平时间 / 总周期时间 × 100% 占空比 25%: ┌──┐ ┌──┐ │ │ │ │ ┘ └───────────┘ └── |--| |--| 高 ←低(75%)→ 高 占空比 75%: ┌────────┐ ┌──── │ │ │ ┘ └─────┘ |--------| | 高 ←低→ 高 → 占空比越高,LED 越亮,电机转速越快

ledc 架构

Timer(定时器)
配置 PWM 频率和分辨率(位数)。4 个高速定时器 + 4 个低速定时器,多个 Channel 可共享一个 Timer。
Channel(通道)
绑定到具体 GPIO 引脚,控制占空比。8 个高速通道(由硬件驱动,切换更快)+ 8 个低速通道。
Fade(渐变)
硬件级平滑渐变占空比,不占用 CPU。用 ledc_fade_func_install() 启用后,可实现平滑呼吸灯效果。
分辨率(位数)
13bit = 0~8191 档位,精度更高但受最高频率限制。频率 × 2^位数 不能超过 APB 时钟(80MHz)。

呼吸灯完整示例

#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

#define LED_PIN        GPIO_NUM_2
#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

static const char *TAG = "BREATH";

static void ledc_init(void)
{
    /* 配置定时器 */
    ledc_timer_config_t timer_cfg = {
        .speed_mode      = LEDC_MODE,
        .duty_resolution = LEDC_DUTY_RES,
        .timer_num       = LEDC_TIMER,
        .freq_hz         = LEDC_FREQUENCY,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&timer_cfg);

    /* 配置通道 */
    ledc_channel_config_t channel_cfg = {
        .speed_mode = LEDC_MODE,
        .channel    = LEDC_CHANNEL,
        .timer_sel  = LEDC_TIMER,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = LED_PIN,
        .duty       = 0,
        .hpoint     = 0,
    };
    ledc_channel_config(&channel_cfg);

    /* 启用硬件 Fade 功能 */
    ledc_fade_func_install(0);
}

void app_main(void)
{
    ledc_init();
    ESP_LOGI(TAG, "呼吸灯启动");

    while (1) {
        /* 渐亮:0 → 8191,耗时 1000ms,等待完成 */
        ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 8191, 1000);
        ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_WAIT_DONE);

        /* 渐暗:8191 → 0,耗时 1000ms */
        ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 0, 1000);
        ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_WAIT_DONE);
    }
}

触摸传感器(Touch Sensor)

ESP32 内置电容式触摸传感器,支持 10 个触摸通道(Touch0~Touch9,对应 GPIO 4/0/2/15/13/12/14/27/33/32)。通过检测 GPIO 上电容变化来感应触摸,无需外部器件。

#include "driver/touch_pad.h"
#include "esp_log.h"

#define TOUCH_CHANNEL TOUCH_PAD_NUM7  // GPIO27
#define TOUCH_THRESH  800             // 触摸阈值,需根据实际校准
static const char *TAG = "TOUCH";

void app_main(void)
{
    touch_pad_init();
    touch_pad_set_voltage(TOUCH_HVOLT_2V7, TOUCH_LVOLT_0V5, TOUCH_HVOLT_ATTEN_1V);
    touch_pad_config(TOUCH_CHANNEL, TOUCH_THRESH);
    touch_pad_filter_start(10);  // 10ms 滤波周期

    while (1) {
        uint16_t touch_val = 0;
        touch_pad_read_filtered(TOUCH_CHANNEL, &touch_val);
        ESP_LOGI(TAG, "Touch7 = %d %s",
                 touch_val, touch_val < TOUCH_THRESH ? "[TOUCHED]" : "");
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}
触摸阈值需要现场校准

触摸传感器的基线值受环境湿度、PCB 布线、探针长度等因素影响,代码中的阈值 800 仅为示例。正确做法是先运行程序读取无触摸时的基线值,再将阈值设置为基线值的约 70%(即触摸时读数下降 30% 触发)。