3、ESP32基础外设编程:GPIO、UART、I2C、SPI接口的使用与调试

各位同学,欢迎来到第三章。这一章我们直接上手干活。

ESP32 之所以能成为物联网网关的「心脏」,靠的就是它那一堆丰富的外设接口。GPIO、UART、I2C、SPI,这四个是基本功。你想想看,传感器要接吧?显示屏要接吧?跟其他模块通信要接吧?全得靠它们。

我个人习惯,拿到一个新板子,第一件事不是跑 Hello World,而是先把这几个外设点亮。心里有底了,后面做协议转换才不慌。

3.1 GPIO:最基础的数字「手脚」

GPIO,通用输入输出口。说白了,就是芯片伸出来的一个个引脚,你可以控制它输出高电平(3.3V)或低电平(0V),也可以读取它上面的电平状态。

ESP32 有 34 个 GPIO,但不是所有都能随便用。嗯,这里要注意,有些引脚有特殊功能,比如 GPIO6-11 默认连到了 Flash 芯片上,你拿来当普通 IO 用,程序直接跑飞。我在项目中遇到过这种坑,排查了半天,最后发现是用了保留引脚。

避坑指南: 我曾经在量产项目里,因为 GPIO0 在下载时被拉低导致无法启动。后来强制在电路板上加了上拉电阻才解决。记住:GPIO0、GPIO2、GPIO12 这几个引脚,上电时序有特殊要求,设计电路前一定看 datasheet。

3.1.1 输出模式:点亮一盏灯

代码很简单,但背后逻辑要清楚。

// 配置 GPIO2 为输出模式
gpio_pad_select_gpio(GPIO_NUM_2);
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);

// 输出高电平,点亮 LED
gpio_set_level(GPIO_NUM_2, 1);
vTaskDelay(1000 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_NUM_2, 0);

这里有个细节:gpio_pad_select_gpio() 是把引脚从「其他功能」切换到「GPIO 功能」。很多人忘了这步,结果怎么配置都没反应。我刚开始学的时候也犯过这错。

3.1.2 输入模式:读取按键状态

读取输入时,最怕的是引脚悬空,电平乱跳。所以一定要加上拉或下拉电阻。

gpio_set_direction(GPIO_NUM_4, GPIO_MODE_INPUT);
gpio_set_pull_mode(GPIO_NUM_4, GPIO_PULLUP_ONLY);  // 内部上拉

int level = gpio_get_level(GPIO_NUM_4);
if (level == 0) {
    printf("按键按下\n");
}

你想想看,如果不加上拉,引脚就像一根天线,周围电磁干扰都能让它电平变化。做工业网关时,这种干扰尤其明显。

3.2 UART:最经典的串行通信

UART,通用异步收发器。两根线(TX、RX)就能通信,简单可靠。ESP32 有 3 个 UART 控制器,每个都可以映射到任意 GPIO。

做网关时,UART 主要用来跟老旧设备通信,比如电表、PLC、GPS 模块。这些设备很多还是 RS232 或 RS485 电平,需要外加转换芯片。

3.2.1 基本配置与收发

// 配置 UART2,波特率 115200
uart_config_t uart_config = {
    .baud_rate = 115200,
    .data_bits = UART_DATA_8_BITS,
    .parity = UART_PARITY_DISABLE,
    .stop_bits = UART_STOP_BITS_1,
    .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM_2, &uart_config);
uart_set_pin(UART_NUM_2, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_2, BUF_SIZE * 2, 0, 0, NULL, 0);

// 发送数据
uart_write_bytes(UART_NUM_2, "AT\r\n", 4);

// 接收数据
uint8_t data[128];
int len = uart_read_bytes(UART_NUM_2, data, sizeof(data), 1000 / portTICK_PERIOD_MS);

这里我建议你注意波特率匹配。两边差一点点,偶尔能通,但数据全是乱码。我曾经调试一个 Modbus 设备,波特率设成了 9600,对方是 9600 但校验位不同,折腾了两小时才发现。

调试技巧: 用逻辑分析仪抓 UART 波形,一眼就能看出波特率对不对。比用示波器方便多了,而且便宜。

3.3 I2C:多设备共享两根线

I2C 只有 SDA(数据线)和 SCL(时钟线)两根线,却能挂载上百个设备。每个设备有唯一地址,通过地址寻址。

做物联网网关时,I2C 常用来接温湿度传感器(如 SHT30)、OLED 显示屏、EEPROM 存储芯片。我个人的经验是,I2C 适合短距离、低速率的板内通信,超过 20cm 就容易受干扰。

3.3.1 主机模式读写传感器

// 配置 I2C 主机
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 = 100000  // 100KHz 标准模式
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);

// 向从机地址 0x3C 写数据
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (0x3C << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0x00, true);  // 控制字节
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);

为什么地址要左移一位?因为 I2C 协议里,7 位地址加上读写位,组成一个字节。左移一位是为了给读写位腾位置。很多新手在这里搞混,地址写错了,设备怎么都不应答。

核心要点: I2C 通信失败,90% 的原因是地址错误或上拉电阻没接。ESP32 内部有弱上拉,但建议外部加 4.7KΩ 上拉电阻,更稳定。

3.4 SPI:高速数据通道

SPI 是四线制(MOSI、MISO、SCLK、CS),全双工通信,速度可以跑到几十 MHz。适合接显示屏、SD 卡、射频模块等需要高速传输的设备。

ESP32 有 4 个 SPI 控制器,其中 SPI2 和 SPI3 可以映射到任意 GPIO。做边缘计算时,SPI 常用来接摄像头或高速 ADC,采集数据后直接在本地处理。

3.4.1 主机模式读写

// 配置 SPI 主机
spi_bus_config_t buscfg = {
    .miso_io_num = PIN_MISO,
    .mosi_io_num = PIN_MOSI,
    .sclk_io_num = PIN_SCLK,
    .quadwp_io_num = -1,
    .quadhd_io_num = -1,
    .max_transfer_sz = 4096
};
spi_bus_initialize(SPI2_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(SPI2_HOST, &devcfg, &spi);

// 发送数据
spi_transaction_t t = {
    .length = 8 * sizeof(tx_data),
    .tx_buffer = tx_data,
    .rx_buffer = rx_data
};
spi_device_transmit(spi, &t);

SPI 的模式(Mode 0-3)取决于时钟极性(CPOL)和相位(CPHA)。不匹配的话,数据全是错的。我建议你查一下从机设备的 datasheet,确认它工作在哪个模式。Mode 0 是最常见的,但不是绝对的。

注意: SPI 的 CS 片选信号,一定要由主机控制。我曾经见过有人把 CS 接地,想省一个引脚,结果多个设备同时响应,总线冲突,数据全乱套了。

3.5 调试技巧总结

做外设编程,调试比写代码更花时间。我总结几个实用技巧:

  • 逻辑分析仪是神器:几十块钱的 USB 逻辑分析仪,能抓取 GPIO、UART、I2C、SPI 的波形。比靠猜靠谱一万倍。
  • 打印调试信息:在关键节点加 printf,看数据对不对。但注意,UART 打印本身会占用时间,影响时序。
  • 分步验证:不要一口气写完所有代码。先验证 GPIO 能亮灯,再验证 UART 能收发,最后才组合起来。这样出问题了,定位范围小。
  • 看 errno:ESP-IDF 的函数很多返回 esp_err_t,用 ESP_ERROR_CHECK() 捕获错误。很多问题其实函数已经告诉你原因了,只是你没看。
我的习惯: 每个外设初始化成功后,先让一个 LED 闪烁几下,作为「初始化成功」的视觉反馈。这样不用接串口线,看一眼就知道板子跑起来了。

好了,这一章的内容就到这里。GPIO、UART、I2C、SPI 这四个接口,是 ESP32 物联网网关的「四肢」。你把这些练熟了,后面做协议转换、边缘计算,才能游刃有余。

下一章,我们开始玩点高级的——WiFi 和蓝牙通信。到时候见。