3. ESP32基础与外设:GPIO、I2C、SPI、UART、ADC、PWM等常用接口,以及传感器数据采集方法

在TinyML工作流中,模型训练只是第一步,真正的挑战在于将量化后的模型部署到资源受限的微控制器上,并让它与真实世界交互。ESP32作为性价比极高的双核MCU,集成了Wi-Fi和蓝牙,且拥有丰富的外设接口,是TinyML原型验证的理想平台。本章将系统梳理ESP32的六大核心外设——GPIO、I2C、SPI、UART、ADC、PWM,重点讲解它们在传感器数据采集链路中的角色、配置方法、时序要点以及常见陷阱,为后续在ESP32上运行TensorFlow Lite Micro(TFLM)推理引擎打下坚实的硬件基础。

ESP32 传感器数据采集硬件链路架构 ESP32 SoC 双核 Xtensa LX6 / 240MHz SRAM 520KB / Flash 4MB GPIO 数字输入/输出 中断/唤醒 I2C SDA / SCL 多传感器总线 SPI MOSI/MISO/SCLK/CS 高速传感器/显示屏 UART TX/RX 串口通信 ADC 12-bit 模拟采样 衰减/校准 PWM LEDC 16通道 电机/加热器控制 传感器阵列 IMU / 温湿度 / 麦克风 气体 / 光学 / 触摸 电源管理 (3.3V LDO / 电池) 去耦电容 / 地平面 / 噪声隔离 数字信号 时钟+数据 高速数据 异步串行 模拟电压 占空比控制 多路传感 TP1: GPIO TP2: 总线 TP3: ADC

上图展示了ESP32在传感器数据采集中的核心位置。所有外设最终都将数据汇聚到SoC内部的总线矩阵,经过DMA或CPU搬运后进入内存,供TFLM推理引擎处理。理解这条链路上的每个环节——从传感器模拟信号到数字特征张量——是部署TinyML模型的前提。

3.1 GPIO:数字世界的“开关”与“眼睛”

核心原理

GPIO(General Purpose Input/Output)是ESP32最基础的外设。每个GPIO引脚可配置为输入、输出、开漏或复用功能。在TinyML场景中,GPIO常用于读取数字传感器(如人体红外PIR、霍尔开关)、控制LED指示灯或触发摄像头快门。ESP32的GPIO支持上升沿/下降沿中断,且可从深度睡眠中唤醒,这对电池供电的TinyML设备至关重要。

配置与操作流程

  1. 引脚分配:查阅ESP32 DevKitC引脚图,避开Strapping引脚(GPIO0、GPIO2、GPIO5、GPIO12、GPIO15),这些引脚在启动时影响下载模式或Flash电压。
  2. 初始化:使用gpio_config_t结构体设置模式、上拉/下拉、中断类型。
  3. 读写操作:调用gpio_set_level()和gpio_get_level()。
  4. 中断服务:注册ISR,注意ISR中只做标记,具体处理放在主循环或任务中。
// GPIO 初始化示例(ESP-IDF v5.x)
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << GPIO_NUM_4),
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .intr_type = GPIO_INTR_POSEDGE
};
gpio_config(&io_conf);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_NUM_4, gpio_isr_handler, NULL);

案例拆解:PIR人体感应触发推理

假设我们要做一个“人靠近时自动进行图像分类”的智能门锁。PIR传感器输出高电平表示有人。我们将PIR OUT接GPIO4,LED接GPIO2(注意GPIO2为Strapping引脚,但上电时为高电平,若LED阳极接GPIO2、阴极接地,则上电瞬间LED会闪一下,可接受)。在ISR中置位g_trigger_flag,主循环检测到标志后执行TFLM推理。实测中,PIR的“锁存时间”约2秒,需在代码中做去抖(如连续读取3次均为高电平才确认)。

风险提示:GPIO2和GPIO12在ESP32-WROOM-32模组上电瞬间有特殊电平要求。GPIO12若为高电平,可能导致Flash电压被拉低而无法启动。若必须使用,需确保外部电路不强制上拉。

3.2 I2C:多传感器共享总线

核心原理

I2C(Inter-Integrated Circuit)使用两根线(SDA数据、SCL时钟)连接最多127个设备,每个设备有7位地址。ESP32的I2C控制器支持主机和从机模式,时钟频率标准100kHz、快速400kHz。在TinyML中,I2C是连接温湿度传感器(SHT30)、气压计(BMP280)、IMU(MPU6050)的首选,因为它占用引脚少,且支持多设备挂载。

关键参数与配置

参数典型值说明
SCL频率100kHz / 400kHz线缆较长时降频
上拉电阻4.7kΩ - 10kΩESP32内部有弱上拉,但建议外部加
超时时间50ms防止总线死锁
地址位宽7位部分传感器支持10位

操作流程与排障

  1. 扫描总线:使用i2c_scanner例程确认设备地址。
  2. 初始化驱动:i2c_master_init()设置端口、SDA/SCL引脚、时钟。
  3. 读写时序:注意寄存器地址的字节序(大端居多)。
  4. 错误处理:检查ESP_FAIL返回值,常见错误为NACK(设备未应答)或总线繁忙。
实战经验:当I2C总线上同时挂载多个传感器时,务必在每次通信前检查总线状态。若发现SDA一直为低,说明有设备拉死总线,可尝试逐个断开设备排查。另外,MPU6050的地址引脚AD0若接高电平,地址变为0x69,否则为0x68,这是最常见的地址冲突点。

3.3 SPI:高速数据搬运通道

核心原理

SPI(Serial Peripheral Interface)采用主从模式,四线制:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。ESP32有4个SPI控制器(SPI0/SPI1用于Flash,SPI2/SPI3可自由使用)。SPI的传输速率可达40MHz以上,适合高吞吐量的传感器,如摄像头(OV2640)、SD卡、LCD显示屏。在TinyML中,SPI常用于将预处理后的图像数据快速搬运到内存。

模式选择与DMA

SPI有4种模式(CPOL时钟极性、CPHA时钟相位)。大多数传感器使用模式0(CPOL=0, CPHA=0)或模式3。ESP32的SPI驱动支持DMA传输,可大幅降低CPU占用。配置时需指定DMA通道(如SPI_DMA_CH_AUTO)。

// SPI 总线初始化(主机模式)
spi_bus_config_t buscfg = {
    .mosi_io_num = 23,
    .miso_io_num = 19,
    .sclk_io_num = 18,
    .quadwp_io_num = -1,
    .quadhd_io_num = -1,
    .max_transfer_sz = 4096
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);

案例拆解:读取MAX31865热电偶数据

在工业温度监控场景中,MAX31865通过SPI输出24位数据。我们配置SPI时钟2MHz(线缆较长时降低),CS接GPIO5。读取流程:拉低CS→发送寄存器地址(0x00)→读取4字节(含故障状态)→拉高CS。实测中,若MISO数据全为1,说明传感器未接或线路断开;若数据跳变异常,检查SCLK是否受干扰,建议在SCLK和MISO上加22pF电容滤波。

注意:SPI从设备对时序敏感,CS拉低后必须等待至少半个时钟周期再发送数据。ESP32的SPI驱动已处理此细节,但若使用GPIO模拟SPI,需手动添加ets_delay_us(1)。

3.4 UART:异步串行通信与调试利器

核心原理

UART(Universal Asynchronous Receiver/Transmitter)只需TX和RX两根线,无需时钟线,通过波特率约定采样时刻。ESP32有3个UART控制器,支持最高5Mbps。UART在TinyML中扮演两个角色:一是与PC通信输出推理结果(日志),二是连接串口型传感器(如部分GPS模块、激光雷达)。

配置要点

  • 波特率:115200是调试默认值,但高波特率(如921600)可加快固件下载。
  • 数据位/停止位/校验:通常8N1(8数据位、无校验、1停止位)。
  • 流控:若传感器数据量大,可启用硬件流控(RTS/CTS)。
  • 缓冲区:设置环形缓冲区大小,避免数据丢失。
// UART 配置示例
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_1, &uart_config);
uart_driver_install(UART_NUM_1, 1024 * 2, 0, 0, NULL, 0);

常见误区

很多开发者直接用printf()输出调试信息,但在TinyML推理循环中,printf()会阻塞CPU且耗时较长。建议使用ESP_LOGI()(异步日志)或先格式化到缓冲区再一次性输出。另外,UART的RX引脚默认有上拉,若外部设备为开漏输出,需注意电平匹配。

3.5 ADC:模拟世界的数字化入口

核心原理

ESP32的ADC(Analog-to-Digital Converter)为12位逐次逼近型,支持衰减(0dB、2.5dB、6dB、11dB),对应输入范围约0-1.1V、0-1.5V、0-2.2V、0-3.3V。ADC2与Wi-Fi共用,当Wi-Fi开启时ADC2不可用,因此传感器应优先接ADC1(GPIO32-GPIO39)。

校准与非线性

ESP32的ADC存在较大的非线性(尤其在低压端),官方提供esp_adc_cal库进行两点校准或查表校准。在TinyML中,若传感器输出为模拟信号(如麦克风MAX4466、光敏电阻),必须做校准,否则模型输入分布偏移会导致推理精度下降。

衰减档位量程(mV)推荐场景
0dB0-1100高精度小信号
2.5dB0-1500电池电压监测
6dB0-2200通用传感器
11dB0-33003.3V供电传感器

操作流程

  1. 选择通道:确认ADC1的GPIO编号。
  2. 配置衰减:根据传感器输出范围选择。
  3. 多次采样取平均:建议连续采样16次取平均,降低噪声。
  4. 应用校准:调用esp_adc_cal_get_voltage()转换为实际电压。
案例:使用光敏电阻(LDR)检测环境光强度。LDR与10kΩ电阻分压,输出接GPIO34(ADC1_CH6)。配置11dB衰减,采样值经校准后得到电压,再通过公式Lux = (Vcc - Vout) / (R * K)估算照度。实测中,发现电压在1.2V附近波动较大,原因是室内50Hz灯光频闪。解决方案:采样周期设为20ms的整数倍(如100ms),并做滑动平均滤波。

3.6 PWM:模拟输出与执行器控制

核心原理

PWM(Pulse Width Modulation)通过改变占空比模拟模拟电压。ESP32的LEDC控制器提供16个通道,可分组绑定到任意GPIO,支持高达40MHz的载波频率。在TinyML应用中,PWM常用于控制电机转速、LED亮度或加热器功率,作为推理结果的执行器。

配置与使用

// LEDC 初始化(50Hz 舵机控制)
ledc_timer_config_t timer_conf = {
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .timer_num = LEDC_TIMER_0,
    .duty_resolution = LEDC_TIMER_13_BIT,
    .freq_hz = 50,
    .clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
    .gpio_num = 18,
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .channel = LEDC_CHANNEL_0,
    .timer_sel = LEDC_TIMER_0,
    .duty = 0,
    .hpoint = 0
};
ledc_channel_config(&channel_conf);

精度与抖动

13位分辨率意味着占空比步进为1/8192,对于舵机控制足够。但若PWM频率过高(如1MHz),分辨率会下降。在电机控制中,建议频率在1kHz-20kHz之间,避免 audible noise。另外,LEDC的占空比更新是异步的,若需要精确相位同步,需使用ledc_update_duty()。

3.7 传感器数据采集综合实战:环境监测节点

我们设计一个完整的TinyML环境监测节点,采集温度(I2C)、光照(ADC)、运动(GPIO中断),并通过UART输出推理结果。

硬件连接:

  • SHT30 → I2C0 (SDA=21, SCL=22)
  • LDR分压 → GPIO34 (ADC1_CH6)
  • PIR → GPIO4 (中断输入)
  • LED → GPIO2 (PWM调光)

软件架构:

  1. FreeRTOS任务1:每2秒读取SHT30和LDR,计算温湿度和光照,存入全局结构体。
  2. FreeRTOS任务2:等待PIR中断标志,触发TFLM模型推理(如“是否有人”分类)。
  3. UART任务:每5秒输出一次所有传感器数据及推理结果。

关键代码片段:

// 读取SHT30(简化版)
uint8_t data[6] = {0};
i2c_master_write_read_device(I2C_NUM_0, 0x44, cmd, 2, data, 6, 100);
uint16_t temp_raw = (data[0] << 8) | data[1];
float temp = -45 + 175 * (temp_raw / 65535.0f);

实测数据与调优:初始测试发现LDR读数波动±5%,原因是ADC参考电压随温度漂移。解决:在PCB上增加一个2.5V基准电压源(如TL431)接到另一个ADC通道,用比值法消除漂移。另外,PIR中断频繁触发(每30秒一次),导致CPU唤醒频繁,功耗增加。优化:在ISR中增加软件去抖(记录时间戳,若小于5秒则忽略)。

复盘总结:传感器数据采集的三大核心原则:1) 电源完整性——所有传感器必须加0.1µF去耦电容,模拟和数字地单点连接;2) 时序确定性——I2C和SPI通信必须设置超时,防止总线死锁导致任务卡死;3) 数据有效性——ADC采样必须做校准和滤波,I2C读取必须校验CRC(如SHT30自带CRC)。这些细节决定了TinyML模型在真实环境中的鲁棒性。

3.8 常见误区与总结

误区后果正确做法
所有GPIO都当普通IO用启动失败或下载异常避开Strapping引脚,查阅数据手册
I2C不加上拉电阻通信不稳定,偶发NACK外部加4.7kΩ上拉至VCC
SPI速率越高越好信号完整性差,数据错位根据线缆长度降频,加端接电阻
UART只用printf调试阻塞CPU,影响推理实时性使用异步日志或DMA传输
ADC不校准直接读电压偏差达10%以上使用esp_adc_cal库进行两点校准
PWM频率随意设置电机啸叫或LED闪烁根据负载选择合适频率

本章从ESP32的六大外设出发,详细阐述了它们在TinyML传感器数据采集链路中的角色和用法。GPIO负责数字事件触发,I2C和SPI承担多传感器数据搬运,UART提供调试和外部通信,ADC将模拟世界数字化,PWM则驱动执行器完成闭环控制。掌握这些接口的时序特性、配置方法和排障技巧,是确保TFLM模型在ESP32上稳定运行的基础。下一阶段,我们将进入TensorFlow Lite Micro的集成与部署,届时这些外设采集的数据将直接作为模型的输入张量。