通信协议¶
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 的通信代码有什么区别?
大部分代码完全兼容,主要区别:
- 引脚可以自定义:
Wire.begin(4, 5)指定引脚 - 多控制器:Wire/Wire1、SPI(VSPI)/SPI(HSPI)、Serial/Serial1/Serial2
- 无 SoftwareSerial:ESP32 有 3 个硬件 UART,不需要软件串口
- 中断回调加 IRAM_ATTR
为什么 Serial1 使用默认引脚会崩溃?
UART1 默认引脚 GPIO 9/10 连接到内部 Flash(QSPI 接口)。使用 Serial1 前==必须重映射引脚==:
多个 SPI 设备应该用 VSPI 还是 HSPI?
- 如果设备都不需要高速 SPI:用一个控制器 + 多个 CS 引脚即可
- 如果需要同时高速通信(如 TFT 刷屏 + SD 卡读写):VSPI 和 HSPI 各接一个
- TFT 显示屏建议用 VSPI(默认引脚更适合高速通信)