ESP32 通信总线完全入门:UART、I2C、SPI 选型与实战
为什么需要总线:GPIO/ADC 只能「读电压」,但 OLED 屏、传感器芯片、GPS 模块都有自己的想法——它们通过总线以协议对话。ESP32 有三大总线,选对总线是外设开发的第一决策。
一、三大总线一张图
UART(双线对话): I2C(一主多从总线): SPI(高速专线):
ESP32 ──TX──> RX── 设备 ESP32 ──SDA── 多个设备 ESP32 ──MOSI──> 设备
<──RX<── TX── └──SCL──(共享两根线) <──MISO<──
每个从机有 7 位地址 ──SCK── + 每从机一根 CS
| 维度 | UART | I2C | SPI |
|---|---|---|---|
| 线数 | 2(TX/RX) | 2(SDA/SCL) | 4+(MOSI/MISO/SCK/CS) |
| 速度 | ≤ 115.2kbps 常用 | 100k/400kHz | 可达 80MHz |
| 挂设备数 | 1 对 1 | 同总线多从机(地址区分) | 多从机(每机一根片选) |
| 典型外设 | GPS、蓝牙模块、串口屏 | OLED、温湿度、IMU | TFT 屏、SD 卡、Flash |
| 上拉电阻 | 不需要 | 需要(4.7k~10k) | 不需要 |
选型口诀:慢速小器件选 I2C(省引脚)、屏幕存储选 SPI(要带宽)、模块对讲选 UART(现成协议)。
二、UART:不只是调试串口
Serial 只是 UART 的默认占用。ESP32 有 3 个硬件 UART,Serial1/Serial2 可重映射到任意 GPIO:
// GPS 模块接 Serial2:RX16 ← GPS TX,TX17 → GPS RX
#define GPS_RX 16
#define GPS_TX 17
HardwareSerial GPSSerial(2); // 使用 UART2
void setup() {
Serial.begin(115200); // 调试口(UART0)
GPSSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
}
void loop() {
while (GPSSerial.available()) {
Serial.write(GPSSerial.read()); // 透传 NMEA 语句
}
}
三个易错点:
- 交叉连接:ESP32 的 TX 接对方的 RX(就像电话两端,嘴对耳朵)
- 波特率必须两端一致(GPS 常见 9600,蓝牙 9600/115200)
SERIAL_8N1= 8 数据位、无校验、1 停止位——绝大多数设备默认它
中断篇的「重活放 loop」原则同样适用:UART 数据到达是事件,缓存累积后在 loop 解析。
三、I2C:两根线挂一堆传感器
1. 主从架构与 7 位地址
I2C 是「一主多从」的总线:主机(ESP32)发起一切,从机靠出厂固化的 7 位地址应答。常见地址:OLED SSD1306 = 0x3C,BMP280 = 0x76/0x77,MCP4725 DAC = 0x62。
2. 先跑扫描器(新模块第一步)
#include <Wire.h>
void setup() {
Wire.begin(21, 22); // SDA=21, SCL=22(可重映射)
Serial.begin(115200);
}
void loop() {
Serial.println("扫描 I2C 总线...");
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) { // 0 = 有设备应答
Serial.printf("发现设备: 0x%02X\n", addr);
}
}
delay(5000);
}
扫不到任何设备 = 接线/上拉问题(见避坑表),先解决再谈协议。
3. 读温湿度(Wire 直接对话)
// AHT20(地址 0x38)读温湿度,示意主流程
Wire.beginTransmission(0x38);
Wire.write(0xAC); Wire.write(0x33); Wire.write(0x00); // 触发测量
Wire.endTransmission();
delay(80); // 等待转换
Wire.requestFrom(0x38, 7); // 读 7 字节
uint8_t data[7];
for (int i = 0; i < 7; i++) data[i] = Wire.read();
// data[1..6] 按数据手册公式换算温湿度
实战通常直接用传感器库(
Adafruit_AHTX0等),但看懂这层原始对话,库出问题时你才知道去查什么。
4. 上拉电阻
SDA/SCL 是开漏设计,必须有上拉才能出现高电平:
- 模组板载 10kΩ 上拉 ✅(立创/淘宝模块基本都带)
- 裸芯片裸接:自己加 4.7kΩ ×2 到 3.3V
- 多个模块同总线:上拉并联会变小(两个 10k 并联 = 5k),一般无碍;挂太多才需摘除多余上拉
四、SPI:屏幕与存储的速度之王
TFT 彩屏每帧几百 KB 的数据量,I2C 的 400kHz 远远不够——SPI 用四根线换带宽:
| 信号 | 方向 | 作用 |
|---|---|---|
| MOSI | 主→从 | 数据(Master Out Slave In) |
| MISO | 从→主 | 数据 |
| SCK | 主→从 | 时钟(决定速度) |
| CS | 主→从 | 片选(选中哪个从机) |
#include <SPI.h>
// VSPI 默认引脚:SCK=18, MISO=19, MOSI=23, CS=5
SPIClass SPI(VSPI);
#define TFT_CS 5
void setup() {
SPI.begin(18, 19, 23, TFT_CS);
SPI.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0)); // 40MHz
digitalWrite(TFT_CS, LOW);
SPI.transfer16(0xABCD); // 发送 16 位数据
digitalWrite(TFT_CS, HIGH);
}
CPOL/CPHA:SPI_MODE 的四个模式
时钟极性(CPOL)与相位(CPHA)组合出 4 种模式,必须与从机要求一致:
| 模式 | CPOL | CPHA | 空闲电平 | 采样沿 |
|---|---|---|---|---|
| MODE0 | 0 | 0 | 低 | 上升沿 |
| MODE1 | 0 | 1 | 低 | 下降沿 |
| MODE2 | 1 | 0 | 高 | 下降沿 |
| MODE3 | 1 | 1 | 高 | 上升沿 |
屏幕白屏/花屏、SD 卡初始化失败时,先查模式(数据手册「SPI timing」节)再查接线。绝大多数设备是 MODE0 或 MODE3。
TFT 屏实战(TFT_eSPI 库配置)属 L3 显示驱动主题,本篇掌握底层即可。
五、避坑速查表(收藏级)
| 症状 | 根因与对策 |
|---|---|
| I2C 扫描无设备 | SDA/SCL 接反;无上拉;器件地址 8 位写法(手册常给 8 位,右移 1 位) |
| OLED 不亮但扫描到 0x3C | 地址对、初始化错(库参数 ssd1306_128x64 vs 72) |
| UART 收到乱码 | 波特率不匹配;TX/RX 没交叉 |
| SPI 屏白屏 | MODE 不对;CS 没拉低;速度超器件上限 |
| 两个 I2C 模块冲突 | 地址相同——有的模块可焊改地址(0x76/0x77) |
| I2C 偶发卡死 | 总线被干扰/上拉太弱;库卡住时 9 次时钟脉冲解锁(进阶技巧) |
| SPI 挂两个设备 | 每个从机独立 CS,SCK/MOSI/MISO 共享 |
六、延伸阅读
- 🧭 前作:中断——UART 数据到达的响应方式
- ⏭️ 下一篇:Wi-Fi 联网第一课——无线连接与 HTTP
- 🔍 工具:Pinout 交互式引脚查询器——确认默认总线引脚
版权声明:本文采用 CC BY-SA 4.0 协议授权。转载与二创请注明出处:ESPDIY 创客工坊 - espdiy.com
ESP32 Communication Buses Guide: UART, I2C and SPI
Why buses exist: GPIO/ADC only “read voltages” — but OLED screens, sensor chips and GPS modules have minds of their own. They converse through buses. Picking the right bus is the first decision of any peripheral project.
1. The three buses at a glance
UART (two-wire chat): I2C (one-master bus): SPI (high-speed dedicated):
ESP32 ──TX──> RX── device ESP32 ──SDA── many devices ESP32 ──MOSI──> device
<──RX<── TX── └──SCL── (shared 2 wires) <──MISO<──
each slave has a 7-bit addr ──SCK── + one CS per slave
| Aspect | UART | I2C | SPI |
|---|---|---|---|
| Wires | 2 (TX/RX) | 2 (SDA/SCL) | 4+ (MOSI/MISO/SCK/CS) |
| Speed | commonly ≤115.2kbps | 100k/400kHz | up to 80MHz |
| Devices per bus | 1-to-1 | Many slaves (addressed) | Many slaves (one CS each) |
| Typical peripherals | GPS, BT modules, serial screens | OLED, temp/humidity, IMU | TFT screens, SD cards, flash |
| Pull-ups needed | No | Yes (4.7k–10k) | No |
Selection mnemonic: slow small parts → I2C (saves pins); screens and storage → SPI (bandwidth); module conversations → UART (ready-made protocol).
2. UART: more than the debug port
Serial is just the default UART. The ESP32 has three hardware UARTs; Serial1/Serial2 can be remapped to any GPIO:
// GPS on Serial2: RX16 ← GPS TX, TX17 → GPS RX
#define GPS_RX 16
#define GPS_TX 17
HardwareSerial GPSSerial(2); // using UART2
void setup() {
Serial.begin(115200); // debug (UART0)
GPSSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
}
void loop() {
while (GPSSerial.available()) {
Serial.write(GPSSerial.read()); // pipe NMEA sentences through
}
}
Three classic mistakes:
- Cross wiring: ESP32 TX goes to the peer’s RX (mouth to ear, like a phone call)
- Baud rates must match on both ends (GPS commonly 9600; BT 9600/115200)
SERIAL_8N1= 8 data bits, no parity, 1 stop bit — the default of nearly everything
The interrupts “heavy work in loop” principle applies: UART data arrival is an event — buffer, then parse in loop.
3. I2C: a pile of sensors on two wires
1. Master-slave and 7-bit addresses
I2C is a one-master bus: the ESP32 initiates everything; slaves answer to factory-hardwired 7-bit addresses. Common ones: SSD1306 OLED = 0x3C, BMP280 = 0x76/0x77, MCP4725 DAC = 0x62.
2. Run the scanner first (step one for any new module)
#include <Wire.h>
void setup() {
Wire.begin(21, 22); // SDA=21, SCL=22 (remappable)
Serial.begin(115200);
}
void loop() {
Serial.println("Scanning I2C bus...");
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) { // 0 = a device ACKed
Serial.printf("Device found: 0x%02X\n", addr);
}
}
delay(5000);
}
Nothing found = wiring/pull-up trouble (see the cheat sheet). Fix that before touching the protocol.
3. Reading temperature (raw Wire conversation)
// AHT20 (address 0x38), master flow shown
Wire.beginTransmission(0x38);
Wire.write(0xAC); Wire.write(0x33); Wire.write(0x00); // trigger measurement
Wire.endTransmission();
delay(80); // conversion time
Wire.requestFrom(0x38, 7); // read 7 bytes
uint8_t data[7];
for (int i = 0; i < 7; i++) data[i] = Wire.read();
// convert data[1..6] per the datasheet formulas
In practice you’ll use a sensor library (
Adafruit_AHTX0etc.) — but understanding this raw conversation tells you what to check when the library misbehaves.
4. Pull-up resistors
SDA/SCL are open-drain — high levels only exist with pull-ups:
- Module boards with onboard 10kΩ ✅ (practically all Taobao modules)
- Bare chips: add 4.7kΩ ×2 to 3.3V yourself
- Multiple modules on one bus: pulls parallel to smaller values (two 10k = 5k) — usually fine; only trim when stacking many
4. SPI: the speed king for screens and storage
A color TFT moves hundreds of KB per frame — I2C’s 400kHz chokes. SPI trades four wires for bandwidth:
| Signal | Direction | Role |
|---|---|---|
| MOSI | master→slave | data (Master Out Slave In) |
| MISO | slave→master | data |
| SCK | master→slave | clock (sets the speed) |
| CS | master→slave | chip select (which slave is active) |
#include <SPI.h>
// VSPI defaults: SCK=18, MISO=19, MOSI=23, CS=5
SPIClass SPI(VSPI);
#define TFT_CS 5
void setup() {
SPI.begin(18, 19, 23, TFT_CS);
SPI.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0)); // 40MHz
digitalWrite(TFT_CS, LOW);
SPI.transfer16(0xABCD); // send 16 bits
digitalWrite(TFT_CS, HIGH);
}
CPOL/CPHA: the four SPI modes
Clock polarity (CPOL) and phase (CPHA) combine into 4 modes that must match the slave:
| Mode | CPOL | CPHA | Idle level | Sampling edge |
|---|---|---|---|---|
| MODE0 | 0 | 0 | low | rising |
| MODE1 | 0 | 1 | low | falling |
| MODE2 | 1 | 0 | high | falling |
| MODE3 | 1 | 1 | high | rising |
A white/garbled screen or an SD init failure: check the mode first (datasheet “SPI timing”), then the wiring. Most devices are MODE0 or MODE3.
Full TFT practice (TFT_eSPI configuration) is an L3 display topic — here you only need the underlying layer.
5. Pitfall cheat sheet (bookmark this)
| Symptom | Cause and fix |
|---|---|
| I2C scan finds nothing | SDA/SCL swapped; no pull-ups; datasheet gave an 8-bit address (shift right by 1) |
| OLED dark but scanner sees 0x3C | Right address, wrong init (library: ssd1306_128x64 vs 72) |
| UART garbage | Baud mismatch; TX/RX not crossed |
| SPI screen stays white | Wrong MODE; CS not asserted; speed beyond the device |
| Two I2C modules clash | Same address — some modules are solder-configurable (0x76/0x77) |
| I2C occasionally hangs | Noise/weak pull-ups; advanced: 9-clock-bus unstick trick |
| Two devices on SPI | Independent CS per slave; SCK/MOSI/MISO shared |
6. Further reading
- 🧭 Previous: Interrupts — responding to UART arrivals
- ⏭️ Next: Wi-Fi First Steps — wireless and HTTP
- 🔍 Tool: Pinout Explorer — default bus pin assignments
License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com