4、ESP32通信协议:UART串口通信、I2C总线、SPI总线详解与传感器驱动
各位同学,咱们今天聊聊ESP32的三大通信协议。说实话,搞嵌入式这些年,我见过太多人在协议选择上栽跟头。选对了,项目顺风顺水;选错了,调试到怀疑人生。今天我就把UART、I2C、SPI这三兄弟掰开揉碎了讲清楚。
4.1 UART串口通信:最基础的“对话”方式
UART,全称是通用异步收发传输器。说白了,就是两根线(TX和RX)来回传数据。一根发,一根收,就这么简单。
为什么叫“异步”?因为没有时钟线。收发双方得提前约定好速度——也就是波特率。我刚开始学的时候,总搞不懂为什么有时候数据乱码。后来才发现,原来是波特率没对上。嗯,这是个经典坑。
UART的关键参数:
- 波特率:常见的有9600、115200。速度越快,传输距离越短。
- 数据位:通常是8位,也有7位、9位。
- 停止位:1位或2位,用来标识一帧数据的结束。
- 校验位:可选奇校验或偶校验,用于简单的错误检测。
在ESP32上,UART用起来特别方便。我给大家看个驱动温湿度传感器DHT11的例子。注意,DHT11虽然是单总线协议,但很多开发板都通过UART转接,所以这里用UART演示。
#include <HardwareSerial.h>
// 定义UART对象,使用UART2
HardwareSerial mySerial(2);
void setup() {
Serial.begin(115200); // 调试串口
mySerial.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17
}
void loop() {
if (mySerial.available()) {
uint8_t data = mySerial.read();
Serial.print("收到数据: 0x");
Serial.println(data, HEX);
}
delay(100);
}
我的经验:调试UART时,先用逻辑分析仪看波形。我曾经花了两天时间查一个通信故障,最后发现是TX和RX接反了。你说冤不冤?
4.2 I2C总线:多设备“共享”一条线
I2C总线,只用两根线:SDA(数据线)和SCL(时钟线)。但它能挂载多个设备,每个设备有唯一的地址。你想想看,这比UART省线多了吧?
I2C的工作流程:
- 主机发送起始信号
- 主机发送设备地址+读写位
- 从机应答(ACK)
- 数据传输(每字节后跟ACK)
- 主机发送停止信号
我记得有一次项目,需要同时读取温度、湿度和气压三个传感器。如果用UART,得三个串口,引脚根本不够用。换成I2C,一根总线全搞定。这就是I2C的魅力。
下面是用ESP32驱动I2C温度传感器LM75的代码:
#include <Wire.h>
#define LM75_ADDR 0x48 // LM75的I2C地址
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA=21, SCL=22
}
void loop() {
Wire.beginTransmission(LM75_ADDR);
Wire.write(0x00); // 温度寄存器地址
Wire.endTransmission();
Wire.requestFrom(LM75_ADDR, 2);
if (Wire.available() == 2) {
int16_t raw = (Wire.read() << 8) | Wire.read();
float temp = raw * 0.125; // 分辨率0.125°C
Serial.print("温度: ");
Serial.print(temp);
Serial.println(" °C");
}
delay(1000);
}
避坑指南:我曾经遇到过I2C总线锁死的问题。原因是某个从机没有正确释放SDA线。解决办法:在初始化时强制复位总线,或者加一个上拉电阻(通常4.7kΩ)。
4.3 SPI总线:速度之王
SPI,全称串行外设接口。它比I2C快得多,但需要更多线:MOSI(主机输出从机输入)、MISO(主机输入从机输出)、SCLK(时钟)、CS(片选)。
SPI的特点:
- 全双工:可以同时收发数据
- 速度快:轻松跑到几十MHz
- 主从模式:一个主机,多个从机(通过片选区分)
- 无地址:靠片选线选择设备
我个人的习惯是,需要高速数据采集时首选SPI。比如驱动OLED显示屏、SD卡、或者高速ADC。I2C虽然方便,但速度上限摆在那里,跑不了太快。
下面是用SPI驱动MAX6675热电偶模块的代码:
#include <SPI.h>
#define CS_PIN 5
void setup() {
Serial.begin(115200);
SPI.begin();
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
}
float readThermocouple() {
digitalWrite(CS_PIN, LOW);
delay(1);
uint16_t raw = SPI.transfer16(0x0000);
digitalWrite(CS_PIN, HIGH);
if (raw & 0x0004) { // 检查热电偶是否断开
return NAN;
}
raw >>= 3; // 去掉低3位
return raw * 0.25; // 分辨率0.25°C
}
void loop() {
float temp = readThermocouple();
if (isnan(temp)) {
Serial.println("热电偶断开!");
} else {
Serial.print("温度: ");
Serial.print(temp);
Serial.println(" °C");
}
delay(1000);
}
我的经验:SPI的时钟极性(CPOL)和时钟相位(CPHA)一定要和从机匹配。我曾经因为模式设置错误,读回来的数据全是0xFF。查了半天手册才发现,原来从机要求的是模式1(CPOL=0, CPHA=1)。
4.4 三种协议对比与选型建议
说了这么多,到底什么时候用哪个?我给大家整理了一个表格:
| 特性 | UART | I2C | SPI |
|---|---|---|---|
| 线数 | 2(TX/RX) | 2(SDA/SCL) | 4+(MOSI/MISO/SCLK/CS) |
| 速度 | 慢(通常<1Mbps) | 中(标准100kHz,快速400kHz) | 快(可达几十MHz) |
| 通信方式 | 异步 | 同步 | 同步 |
| 多设备 | 点对点 | 总线(地址区分) | 总线(片选区分) |
| 全双工 | 是 | 否 | 是 |
| 典型应用 | GPS模块、蓝牙模块 | 温湿度传感器、OLED屏 | SD卡、高速ADC、显示屏 |
我的选型建议:
- 如果只需要和单个设备通信,UART最简单
- 如果需要挂多个低速设备,I2C最省引脚
- 如果追求速度,或者设备本身支持SPI,那就用SPI
重要提醒:ESP32的引脚功能可以复用。比如UART的TX/RX,I2C的SDA/SCL,SPI的MOSI/MISO等,都可以通过代码重新映射到任意GPIO。但要注意,有些引脚有特殊功能(比如ADC、DAC),别冲突了。
4.5 实战:用I2C驱动多个传感器
最后,我给大家展示一个综合例子。用I2C同时驱动温度传感器LM75和气压传感器BMP180。这两个设备地址不同,可以挂在同一条I2C总线上。
#include <Wire.h>
#define LM75_ADDR 0x48
#define BMP180_ADDR 0x77
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
}
float readLM75() {
Wire.beginTransmission(LM75_ADDR);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(LM75_ADDR, 2);
if (Wire.available() == 2) {
int16_t raw = (Wire.read() << 8) | Wire.read();
return raw * 0.125;
}
return NAN;
}
float readBMP180() {
// 简化版:读取BMP180的温度
Wire.beginTransmission(BMP180_ADDR);
Wire.write(0xF4); // 控制寄存器
Wire.write(0x2E); // 启动温度测量
Wire.endTransmission();
delay(5); // 等待测量完成
Wire.beginTransmission(BMP180_ADDR);
Wire.write(0xF6); // 数据寄存器
Wire.endTransmission();
Wire.requestFrom(BMP180_ADDR, 2);
if (Wire.available() == 2) {
uint16_t raw = (Wire.read() << 8) | Wire.read();
return raw / 10.0; // 简化计算
}
return NAN;
}
void loop() {
float temp1 = readLM75();
float temp2 = readBMP180();
Serial.print("LM75: ");
Serial.print(temp1);
Serial.print(" °C, BMP180: ");
Serial.print(temp2);
Serial.println(" °C");
delay(2000);
}
你看,代码并不复杂。关键是理解每种协议的特性,然后根据实际需求选择。我刚开始做嵌入式时,总觉得协议越多越难。后来发现,其实每种协议都有它的适用场景。选对了,事半功倍;选错了,事倍功半。
好了,这一章就到这里。下一章我们聊聊ESP32的WiFi和蓝牙通信,那又是另一番天地了。