接口协议基础:UART、I2C、SPI协议原理与对比
做嵌入式这些年,我打交道最多的就是这三种协议。UART、I2C、SPI,说白了就是传感器和主控芯片之间“说话”的方式。你想想看,一个温度传感器、一个加速度计,它们怎么把数据告诉你?就是靠这些协议。
我个人习惯,拿到一个新传感器,第一件事就是看它的接口类型。是UART?那简单,两根线搞定。是I2C?那得注意地址别冲突。是SPI?速度可以拉很高,但线也多。今天咱们就把这三个协议掰开揉碎了讲清楚。
一、UART:最朴实的串行通信
UART,全称通用异步收发器。异步是什么意思?就是没有时钟线。发送方和接收方各用自己的时钟,靠约定好的波特率来同步。
我在项目中遇到过一个问题:两个设备波特率都设成了115200,但就是收不到数据。查了半天,发现一个用的是16MHz晶振,另一个是12MHz,波特率误差累积超过了3%。嗯,这里要注意,UART对时钟精度有要求,误差最好控制在2%以内。
UART的帧结构
- 起始位:1位,拉低电平,告诉接收方“我要发数据了”
- 数据位:5~9位,常用8位
- 校验位:可选,奇校验或偶校验
- 停止位:1位或2位,拉高电平,表示一帧结束
// 一个典型的UART初始化代码(STM32 HAL库)
UART_HandleTypeDef huart2;
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart2);
二、I2C:总线上的“多设备聊天室”
I2C只有两根线:SCL(时钟线)和SDA(数据线)。但神奇的是,它可以挂几十个设备。为什么?因为每个设备都有唯一的地址。
说白了,I2C就像是一个群。主设备是群主,每次@一个从设备(发地址),然后开始聊天(传数据)。其他设备看到不是自己的地址,就保持沉默。
I2C的通信流程
- 起始条件:SCL高电平时,SDA从高变低
- 发送地址:7位或10位地址,加读写位
- 从设备应答:拉低SDA表示“我在”
- 数据传输:每8位数据后跟一个应答位
- 停止条件:SCL高电平时,SDA从低变高
我记得有一次调试一个气压传感器,I2C死活不通。用示波器一看,SCL波形正常,但SDA一直高。原来是上拉电阻没焊。I2C的SDA和SCL必须接上拉电阻,一般4.7kΩ,这个坑我踩过。
// I2C读取传感器数据的典型流程(伪代码)
i2c_start(); // 发送起始条件
i2c_send_byte(0x68 << 1 | 0); // 发送地址+写位
i2c_send_byte(0x01); // 发送寄存器地址
i2c_start(); // 重复起始条件
i2c_send_byte(0x68 << 1 | 1); // 发送地址+读位
data = i2c_read_byte(); // 读取数据
i2c_stop(); // 发送停止条件
三、SPI:速度至上,全双工通信
SPI跟I2C不一样,它追求的是速度。四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
你想想看,SPI是全双工的,发送和接收可以同时进行。I2C是半双工,同一时间只能一个方向。所以SPI的速度可以做到几十MHz,而I2C通常只有100kHz到400kHz。
SPI的四种模式
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。这个很多人搞混,我教你们一个记忆方法:
- 模式0:CPOL=0, CPHA=0 — 空闲时时钟低,第一个边沿采样
- 模式1:CPOL=0, CPHA=1 — 空闲时时钟低,第二个边沿采样
- 模式2:CPOL=1, CPHA=0 — 空闲时时钟高,第一个边沿采样
- 模式3:CPOL=1, CPHA=1 — 空闲时时钟高,第二个边沿采样
我个人习惯,先看传感器数据手册里的时序图。图上会标出数据是在时钟上升沿还是下降沿采样。照着配就行,别死记硬背。
// SPI发送接收一个字节(软件模拟)
uint8_t spi_transfer(uint8_t data) {
uint8_t receive = 0;
for(int i = 7; i >= 0; i--) {
// 发送位
HAL_GPIO_WritePin(MOSI_PORT, MOSI_PIN, (data >> i) & 0x01);
// 时钟上升沿
HAL_GPIO_WritePin(SCLK_PORT, SCLK_PIN, GPIO_PIN_SET);
// 读取接收位
receive |= (HAL_GPIO_ReadPin(MISO_PORT, MISO_PIN) << i);
// 时钟下降沿
HAL_GPIO_WritePin(SCLK_PORT, SCLK_PIN, GPIO_PIN_RESET);
}
return receive;
}
四、三大协议对比:一张表说清楚
| 特性 | UART | I2C | SPI |
|---|---|---|---|
| 线数 | 2根(TX, RX) | 2根(SCL, SDA) | 4根(SCLK, MOSI, MISO, CS) |
| 通信方式 | 异步 | 同步 | 同步 |
| 传输方向 | 全双工 | 半双工 | 全双工 |
| 最大速度 | ~1Mbps(典型) | 100kHz~3.4MHz | ~80MHz(典型) |
| 多设备支持 | 点对点 | 多从机(地址寻址) | 多从机(片选) |
| 抗干扰能力 | 一般 | 较好(差分信号变体) | 一般(高速时需注意) |
| 典型应用 | GPS模块、蓝牙模块 | 温度传感器、加速度计 | SD卡、显示屏、ADC |
五、知识体系总览
下面这张图,是我梳理的三大协议的核心逻辑。你看一眼,心里就有谱了。
六、实战选型建议
说了这么多,到底怎么选?我给你们几个实际场景:
- 接一个GPS模块:用UART。GPS模块通常就是TX/RX两根线,简单粗暴。
- 接多个温度传感器:用I2C。一根总线上挂四五个传感器,省IO口。
- 接一个高分辨率ADC:用SPI。ADC数据量大,速度要求高,SPI最合适。
- 接一个摄像头:以上三种都不行。摄像头需要并行接口或MIPI,那是另一个话题了。
好了,接口协议基础就讲到这里。这三种协议,你只要动手调过一两个传感器,自然就熟了。别怕,多试几次,波形抓一抓,代码跑一跑,很快就能上手。