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的工作流程:

  1. 主机发送起始信号
  2. 主机发送设备地址+读写位
  3. 从机应答(ACK)
  4. 数据传输(每字节后跟ACK)
  5. 主机发送停止信号

我记得有一次项目,需要同时读取温度、湿度和气压三个传感器。如果用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和蓝牙通信,那又是另一番天地了。