3、ESP32基础外设驱动:GPIO、ADC、I2C、SPI的初始化与使用,点亮第一盏LED。

好,咱们正式开始动手了。

前面聊了那么多架构和协议,我知道你肯定手痒了。今天这一章,咱们就真刀真枪地干——把ESP32的四个最基础的外设驱动讲透:GPIO、ADC、I2C、SPI。最后,我会带你点亮第一盏LED。

别小看这个LED。我做了这么多年嵌入式,每次拿到一块新板子,第一件事就是点灯。灯亮了,说明你的开发环境、编译链、下载工具、芯片最小系统,全通了。这是硬件工程师的“Hello World”。

3.1 GPIO:最基础,也最容易翻车

GPIO,通用输入输出口。说白了,就是芯片的“手脚”。你可以让它输出高电平或低电平,也可以读取外部信号是0还是1。

初始化流程

在ESP-IDF里,GPIO的初始化分三步:

  1. 配置引脚功能(输入/输出/上拉/下拉)
  2. 设置中断模式(如果需要)
  3. 使能并开始使用

代码长这样:

#include "driver/gpio.h"

#define LED_GPIO  GPIO_NUM_2

void app_main(void)
{
    // 1. 配置GPIO为推挽输出
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    // 2. 循环闪烁
    while (1) {
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
我的小习惯: 我习惯把GPIO号用宏定义写清楚,比如 #define LED_GPIO GPIO_NUM_2。别问为什么,问就是吃过亏——项目大了,引脚号满天飞,改起来想哭。

避坑指南

  • ESP32的GPIO2默认是板载LED,但有些模组把它用作其他功能。拿到板子先看原理图。
  • GPIO6-11是Flash引脚,别乱动。我曾经手贱配置了GPIO6,结果程序直接跑飞。
  • 输出模式记得关掉上下拉,否则电平可能不对。

3.2 ADC:把模拟世界翻译给数字芯片听

ADC,模数转换器。传感器输出的电压是连续的模拟量,芯片只认0和1。ADC就是那个翻译官。

ESP32有两个ADC模块,每个模块支持多个通道。我一般用ADC1,因为它更稳定。

初始化与读取

#include "driver/adc.h"
#include "esp_adc_cal.h"

#define ADC_CHANNEL ADC1_CHANNEL_0  // GPIO36

void app_main(void)
{
    // 1. 配置ADC
    adc1_config_width(ADC_WIDTH_BIT_12);          // 12位精度,0-4095
    adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN_DB_11); // 0-3.6V范围

    // 2. 校准(推荐)
    esp_adc_cal_characteristics_t adc_chars;
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars);

    while (1) {
        uint32_t raw = adc1_get_raw(ADC_CHANNEL);
        uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
        printf("Raw: %d, Voltage: %d mV\n", raw, voltage);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
注意: ADC输入电压绝对不能超过3.3V!ESP32的ADC引脚耐压有限。我见过有人直接接5V传感器,结果芯片冒烟了。如果你要测更高电压,必须用分压电阻。

另外,ESP32的ADC线性度其实一般。如果你需要高精度测量,建议用外部ADC芯片,比如ADS1115。这个咱们后面I2C章节会用到。

3.3 I2C:两根线串起一箩筐传感器

I2C总线,两根线:SCL(时钟)和SDA(数据)。一根线上可以挂多个设备,每个设备有唯一地址。

我个人特别喜欢I2C,因为它接线少、调试方便。在物联网项目里,温湿度传感器、气压计、OLED屏幕,基本都是I2C接口。

初始化与读写

#include "driver/i2c.h"

#define I2C_MASTER_SCL_IO    22
#define I2C_MASTER_SDA_IO    21
#define I2C_MASTER_FREQ_HZ   100000

void i2c_master_init(void)
{
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };
    i2c_param_config(I2C_NUM_0, &conf);
    i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);
}

// 写一个字节到从机
esp_err_t i2c_write_byte(uint8_t dev_addr, uint8_t reg, uint8_t data)
{
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd, reg, true);
    i2c_master_write_byte(cmd, data, true);
    i2c_master_stop(cmd);
    esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd);
    return ret;
}
经验之谈: I2C的速率别贪快。100kHz最稳,400kHz容易受干扰。我在一个项目里用过400kHz,结果线长了半米,数据就乱套了。后来老老实实降到100kHz,稳如老狗。

3.4 SPI:要速度?用它就对了

SPI,全双工、高速、四线制(MOSI、MISO、SCLK、CS)。如果你要驱动LCD屏幕、SD卡、高速ADC,SPI是首选。

ESP32的SPI控制器很强大,支持DMA传输。但咱们先从基础讲起。

初始化与发送

#include "driver/spi_master.h"

#define SPI_HOST    SPI2_HOST
#define PIN_MOSI    23
#define PIN_MISO    19
#define PIN_SCLK    18
#define PIN_CS      5

void spi_init(void)
{
    spi_bus_config_t buscfg = {
        .mosi_io_num = PIN_MOSI,
        .miso_io_num = PIN_MISO,
        .sclk_io_num = PIN_SCLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4096,
    };
    spi_bus_initialize(SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);

    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 10 * 1000 * 1000,  // 10MHz
        .mode = 0,
        .spics_io_num = PIN_CS,
        .queue_size = 7,
    };
    spi_device_handle_t spi;
    spi_bus_add_device(SPI_HOST, &devcfg, &spi);
}

void spi_write_byte(spi_device_handle_t spi, uint8_t data)
{
    spi_transaction_t t = {
        .length = 8,
        .tx_buffer = &data,
    };
    spi_device_transmit(spi, &t);
}
注意: SPI的时钟极性(CPOL)和相位(CPHA)必须和从机匹配。不匹配的话,数据全是错的。我调试LCD屏时,就因为这个折腾了一下午。后来用逻辑分析仪一看,才发现是模式搞反了。

3.5 点亮第一盏LED:把知识串起来

好了,理论讲完了。咱们来点实际的。

我选GPIO2作为LED控制引脚。为什么?因为大多数ESP32开发板,GPIO2都连着一个LED。你不需要额外接线。

完整代码

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED_PIN    GPIO_NUM_2

void app_main(void)
{
    // 配置GPIO
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

    printf("ESP32 LED Blink Started!\n");

    while (1) {
        gpio_set_level(LED_PIN, 1);   // 亮
        vTaskDelay(500 / portTICK_PERIOD_MS);
        gpio_set_level(LED_PIN, 0);   // 灭
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}

编译、下载、上电。如果LED以0.5秒间隔闪烁,恭喜你——你的ESP32活了。

为什么点灯这么重要? 它验证了你的整个工具链:编译器能编译、链接器能链接、下载器能烧录、芯片能运行、GPIO能输出。这一步通了,后面的所有外设驱动才有意义。

3.6 本章小结

咱们今天干了四件事:

  • GPIO:学会了推挽输出和输入读取,点亮了LED。
  • ADC:知道了怎么把模拟电压转成数字值,也记住了别超3.3V。
  • I2C:两根线搞定多设备通信,速率别贪快。
  • SPI:高速全双工,适合屏幕和SD卡,时钟模式要对。

这些外设,是物联网节点和传感器交互的基础。下一章,咱们会把这些外设真正用起来——接一个温湿度传感器,把数据通过MQTT发到云端。

嗯,到时候你就能远程看到家里的温度了。想想是不是有点小激动?

课后练习: 试着把LED接到GPIO4,用按键(GPIO0)控制它的亮灭。提示:GPIO0是ESP32的BOOT按键,按下时为低电平。试试看,你行的。