跳转至

通信协议

ESP32 拥有 3 个 UART、2 个 I2C、3 个 SPI 控制器,且得益于 GPIO 矩阵,这些接口可以映射到几乎任意引脚。本节介绍 ESP32 上各通信协议的使用方法和注意事项。


一、通信外设总览

外设 数量 默认引脚 特点
UART 3 个 UART0: GPIO 1/3 可映射到任意 GPIO
I2C 2 个 I2C0: GPIO 21/22 可映射到任意 GPIO
SPI 3 个 VSPI: GPIO 23/19/18/5 HSPI + VSPI 可用
I2S 2 个 音频接口
CAN/TWAI 1 个 车载总线
graph LR
    ESP32 --> UART0[UART0<br>USB 调试]
    ESP32 --> UART1[UART1<br>传感器/GPS]
    ESP32 --> UART2[UART2<br>扩展设备]
    ESP32 --> I2C0[I2C0<br>传感器总线]
    ESP32 --> I2C1[I2C1<br>显示屏]
    ESP32 --> HSPI[HSPI<br>SD 卡]
    ESP32 --> VSPI[VSPI<br>TFT 屏]

二、UART 串口

ESP32 有 3 个硬件 UART,比 Arduino UNO(1个)多得多:

UART 分配

串口 默认 TX 默认 RX 用途
UART0 (Serial) GPIO 1 GPIO 3 USB 调试/下载
UART1 (Serial1) GPIO 10 GPIO 9 ⚠️ 默认引脚连接 Flash!
UART2 (Serial2) GPIO 17 GPIO 16 自由使用

UART1 必须重映射引脚

UART1 的默认引脚(GPIO 9/10)连接到内部 Flash,直接使用会导致崩溃。必须在初始化时指定其他引脚。

基本使用

void setup() {
    // UART0 — USB 调试
    Serial.begin(115200);

    // UART2 — 连接 GPS 模块
    Serial2.begin(9600, SERIAL_8N1, 16, 17);  // RX=16, TX=17

    // UART1 — 重映射到安全引脚
    Serial1.begin(115200, SERIAL_8N1, 4, 5);  // RX=4, TX=5
}

void loop() {
    // 读取 GPS 数据
    while (Serial2.available()) {
        char c = Serial2.read();
        Serial.write(c);  // 转发到 USB 调试
    }
}

自定义引脚(GPIO 矩阵)

// ESP32 的 UART 可以映射到几乎任意 GPIO
Serial2.begin(115200, SERIAL_8N1, 25, 26);  // RX=25, TX=26
Serial2.begin(115200, SERIAL_8N1, 32, 33);  // 换到 RX=32, TX=33 也行

GPS 模块(NEO-6M)解析

#include <TinyGPSPlus.h>

TinyGPSPlus gps;

void setup() {
    Serial.begin(115200);
    Serial2.begin(9600, SERIAL_8N1, 16, 17);
}

void loop() {
    while (Serial2.available() > 0) {
        gps.encode(Serial2.read());
    }

    if (gps.location.isUpdated()) {
        Serial.printf("纬度: %.6f, 经度: %.6f\n",
            gps.location.lat(), gps.location.lng());
        Serial.printf("海拔: %.1f m, 速度: %.1f km/h\n",
            gps.altitude.meters(), gps.speed.kmph());
        Serial.printf("卫星数: %d, 时间: %02d:%02d:%02d\n",
            gps.satellites.value(),
            gps.time.hour(), gps.time.minute(), gps.time.second());
    }
}

三、I2C 通信

ESP32 I2C 特点

特性 ESP32 Arduino UNO
I2C 控制器数量 2 个 1 个
引脚映射 任意 GPIO 固定 A4/A5
时钟频率 最高 1 MHz 最高 400 kHz
从机模式 支持 支持

基本使用

#include <Wire.h>

void setup() {
    Serial.begin(115200);

    // I2C0 — 使用默认引脚
    Wire.begin();  // SDA=21, SCL=22

    // I2C0 — 自定义引脚
    // Wire.begin(4, 5);  // SDA=4, SCL=5

    // I2C1 — 第二个 I2C 总线
    // Wire1.begin(25, 26);  // SDA=25, SCL=26
}

双 I2C 总线

#include <Wire.h>

// 总线 0: 传感器
TwoWire I2C_Sensor = TwoWire(0);
// 总线 1: 显示屏
TwoWire I2C_Display = TwoWire(1);

void setup() {
    I2C_Sensor.begin(21, 22, 400000);   // SDA=21, SCL=22, 400kHz
    I2C_Display.begin(25, 26, 400000);  // SDA=25, SCL=26, 400kHz

    // 传感器使用 I2C_Sensor
    I2C_Sensor.beginTransmission(0x68);
    I2C_Sensor.write(0x6B);
    I2C_Sensor.write(0x00);
    I2C_Sensor.endTransmission();

    // 显示屏使用 I2C_Display
    // Adafruit_SSD1306 display(128, 64, &I2C_Display, -1);
}

使用双 I2C 的场景

  • 两个设备地址冲突且无法修改
  • 需要隔离不同速度的设备(快速传感器 + 慢速 EEPROM)
  • 减少单条总线的电容负载,提高可靠性

I2C 时钟速度

Wire.begin();
Wire.setClock(400000);  // 400 kHz 快速模式
// Wire.setClock(1000000);  // 1 MHz 高速模式(需要设备支持)

MPU6050 读取示例

#include <Wire.h>

const int MPU_ADDR = 0x68;

void setup() {
    Serial.begin(115200);
    Wire.begin(21, 22);
    Wire.setClock(400000);

    // 唤醒 MPU6050
    Wire.beginTransmission(MPU_ADDR);
    Wire.write(0x6B);  // PWR_MGMT_1 寄存器
    Wire.write(0x00);  // 唤醒
    Wire.endTransmission();
}

void loop() {
    Wire.beginTransmission(MPU_ADDR);
    Wire.write(0x3B);  // 加速度数据起始地址
    Wire.endTransmission(false);
    Wire.requestFrom(MPU_ADDR, 14);

    int16_t ax = (Wire.read() << 8) | Wire.read();
    int16_t ay = (Wire.read() << 8) | Wire.read();
    int16_t az = (Wire.read() << 8) | Wire.read();
    int16_t temp = (Wire.read() << 8) | Wire.read();
    int16_t gx = (Wire.read() << 8) | Wire.read();
    int16_t gy = (Wire.read() << 8) | Wire.read();
    int16_t gz = (Wire.read() << 8) | Wire.read();

    Serial.printf("加速度: X=%6d Y=%6d Z=%6d | ", ax, ay, az);
    Serial.printf("陀螺仪: X=%6d Y=%6d Z=%6d | ", gx, gy, gz);
    Serial.printf("温度: %.1f°C\n", temp / 340.0 + 36.53);

    delay(100);
}

四、SPI 通信

ESP32 SPI 控制器

SPI 控制器 用途 默认引脚
SPI0/SPI1 内部 Flash ❌ 不可用
HSPI (SPI2) 用户可用 MOSI=13, MISO=12, SCK=14, CS=15
VSPI (SPI3) 用户可用 MOSI=23, MISO=19, SCK=18, CS=5

VSPI 基本使用

#include <SPI.h>

const int CS_PIN = 5;

void setup() {
    Serial.begin(115200);
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);

    SPI.begin();  // 使用默认 VSPI 引脚
    // SPI.begin(18, 19, 23, 5);  // 或显式指定
}

byte readRegister(byte reg) {
    byte result;
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(CS_PIN, LOW);
    SPI.transfer(reg | 0x80);
    result = SPI.transfer(0x00);
    digitalWrite(CS_PIN, HIGH);
    SPI.endTransaction();
    return result;
}

使用 HSPI

#include <SPI.h>

SPIClass HSPI_Bus(HSPI);  // 创建 HSPI 实例
const int HSPI_CS = 15;

void setup() {
    pinMode(HSPI_CS, OUTPUT);
    HSPI_Bus.begin(14, 12, 13, 15);  // SCK, MISO, MOSI, SS
}

void loop() {
    HSPI_Bus.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
    digitalWrite(HSPI_CS, LOW);
    HSPI_Bus.transfer(0x42);
    digitalWrite(HSPI_CS, HIGH);
    HSPI_Bus.endTransaction();
}

双 SPI 同时使用

#include <SPI.h>

// VSPI — 连接 TFT 显示屏
SPIClass VSPI_Bus(VSPI);
// HSPI — 连接 SD 卡
SPIClass HSPI_Bus(HSPI);

void setup() {
    VSPI_Bus.begin(18, 19, 23, 5);   // 默认 VSPI 引脚
    HSPI_Bus.begin(14, 12, 13, 15);  // 默认 HSPI 引脚

    // 或重映射到其他引脚
    // VSPI_Bus.begin(25, 26, 27, 32);
}

SPI 速度

  • 通过 GPIO 矩阵映射的 SPI 最高 40 MHz
  • 使用默认引脚(直连外设)时可达 80 MHz
  • TFT 显示屏建议使用默认引脚以获得最大刷新率

五、I2S 音频接口

ESP32 有 2 个 I2S 控制器,可以连接数字麦克风和 DAC 芯片:

#include <driver/i2s.h>

// I2S 配置 — 连接 INMP441 数字麦克风
void setupI2S() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
        .sample_rate = 16000,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 4,
        .dma_buf_len = 1024,
    };

    i2s_pin_config_t pin_config = {
        .bck_io_num = 26,    // 位时钟
        .ws_io_num = 25,     // 字选择(LRCK)
        .data_out_num = -1,  // 不使用
        .data_in_num = 22,   // 数据输入
    };

    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
}

void loop() {
    int16_t samples[1024];
    size_t bytesRead;
    i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, portMAX_DELAY);
    // 处理音频数据...
}

六、CAN/TWAI 总线

ESP32 内置 TWAI(兼容 CAN 2.0B)控制器,适用于汽车和工业场景:

#include <driver/twai.h>

void setup() {
    Serial.begin(115200);

    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
        GPIO_NUM_21, GPIO_NUM_22, TWAI_MODE_NORMAL);
    twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

    twai_driver_install(&g_config, &t_config, &f_config);
    twai_start();

    Serial.println("TWAI/CAN 已启动");
}

void loop() {
    // 发送
    twai_message_t tx_msg = {
        .identifier = 0x123,
        .data_length_code = 4,
        .data = {0x01, 0x02, 0x03, 0x04}
    };
    twai_transmit(&tx_msg, pdMS_TO_TICKS(100));

    // 接收
    twai_message_t rx_msg;
    if (twai_receive(&rx_msg, pdMS_TO_TICKS(100)) == ESP_OK) {
        Serial.printf("收到 CAN ID: 0x%03X, 数据: ", rx_msg.identifier);
        for (int i = 0; i < rx_msg.data_length_code; i++) {
            Serial.printf("%02X ", rx_msg.data[i]);
        }
        Serial.println();
    }
}

TWAI/CAN 需要外部收发器

ESP32 的 TWAI 控制器只输出 TX/RX 逻辑信号,需要外接 CAN 收发器芯片(如 SN65HVD230 或 TJA1050)来驱动 CAN 总线。


七、常用函数速查

UART

函数 功能
Serial2.begin(baud, config, rx, tx) 初始化并指定引脚
Serial2.available() 可读字节数
Serial2.read() / readString() 读取数据
Serial2.write() / print() 发送数据
Serial2.updateBaudRate(baud) 运行时修改波特率

I2C

函数 功能
Wire.begin(sda, scl) 初始化并指定引脚
Wire.setClock(freq) 设置时钟频率
Wire1.begin(sda, scl) 使用第二个 I2C 控制器

SPI

函数 功能
SPI.begin(sck, miso, mosi, ss) 初始化并指定引脚
SPIClass HSPI_Bus(HSPI) 创建 HSPI 实例

八、常见问题

ESP32 和 Arduino 的通信代码有什么区别?

大部分代码完全兼容,主要区别:

  1. 引脚可以自定义Wire.begin(4, 5) 指定引脚
  2. 多控制器:Wire/Wire1、SPI(VSPI)/SPI(HSPI)、Serial/Serial1/Serial2
  3. 无 SoftwareSerial:ESP32 有 3 个硬件 UART,不需要软件串口
  4. 中断回调加 IRAM_ATTR

为什么 Serial1 使用默认引脚会崩溃?

UART1 默认引脚 GPIO 9/10 连接到内部 Flash(QSPI 接口)。使用 Serial1 前==必须重映射引脚==:

Serial1.begin(115200, SERIAL_8N1, 4, 5);  // 重映射到 GPIO 4/5

多个 SPI 设备应该用 VSPI 还是 HSPI?

  • 如果设备都不需要高速 SPI:用一个控制器 + 多个 CS 引脚即可
  • 如果需要同时高速通信(如 TFT 刷屏 + SD 卡读写):VSPI 和 HSPI 各接一个
  • TFT 显示屏建议用 VSPI(默认引脚更适合高速通信)