第10章:通信协议入门——I2C、SPI、UART详解
各位同学,今天我们来聊聊嵌入式开发中最基础也最重要的三个通信协议。说实话,我刚开始做驱动开发那会儿,被这几个协议搞得晕头转向。尤其是I2C的时序图,看着就像天书。但后来我发现,只要抓住几个关键点,这些协议其实没那么复杂。
咱们一个一个来。先讲I2C,再讲SPI,最后聊UART。这三个协议,你搞懂了,市面上90%的传感器驱动你都能拿得下。
一、I2C协议详解
I2C,全称Inter-Integrated Circuit,是飞利浦公司搞出来的。它最大的特点就是——只用两根线!一根时钟线SCL,一根数据线SDA。你想想看,一根线上能挂几十个设备,多省IO口啊。
时序要点
I2C的时序,说白了就是几个状态:起始、停止、应答、非应答。我画个简单的流程:
- 起始条件:SCL高电平时,SDA从高变低
- 停止条件:SCL高电平时,SDA从低变高
- 数据采样:SCL低电平时改变SDA,高电平时采样
- 应答位:每发送8位数据后,接收方拉低SDA表示应答
避坑指南:我曾经在一个项目里,I2C死活通信不上。查了两天,最后发现是起始条件没满足时序要求。SCL高电平的保持时间不够,从机根本不认。所以啊,时序参数一定要看数据手册,别想当然。
地址与读写操作
I2C的地址是7位的,但实际发送时是8位——左移一位,最低位表示读写方向。0表示写,1表示读。比如地址0x50,写操作就是0xA0,读操作就是0xA1。
读写流程是这样的:
- 主机发送起始条件
- 主机发送7位地址+读写位
- 从机应答(拉低SDA)
- 主机发送寄存器地址(如果是写操作)
- 从机应答
- 数据传输(每字节后跟应答)
- 主机发送停止条件
// I2C写操作示例(伪代码)
void i2c_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) {
i2c_start(); // 起始条件
i2c_send_byte(dev_addr << 1); // 地址+写位
i2c_wait_ack(); // 等待应答
i2c_send_byte(reg_addr); // 寄存器地址
i2c_wait_ack();
i2c_send_byte(data); // 发送数据
i2c_wait_ack();
i2c_stop(); // 停止条件
}
个人经验:我习惯在I2C通信前后加个延时,尤其是高速模式。有些从机反应慢,连续操作容易丢数据。另外,记得加上超时处理,别让程序死等应答。
二、SPI协议详解
SPI,Serial Peripheral Interface,摩托罗拉发明的。它比I2C快,但用的线也多——四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
四种模式
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 采样边沿 |
|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 |
| 模式1 | 0 | 1 | 下降沿采样 |
| 模式2 | 1 | 0 | 下降沿采样 |
| 模式3 | 1 | 1 | 上升沿采样 |
说白了,CPOL决定空闲时时钟是高还是低,CPHA决定数据在哪个边沿采样。我遇到过不少新手,SPI通信失败,十有八九是模式没配对。
注意:我曾经在一个项目中,传感器手册写的是模式0,但实际测试发现模式3才能正常工作。后来发现是手册版本错了。所以啊,别完全信手册,实际测试才是王道。
片选与全双工
SPI的片选信号是低电平有效。你要跟哪个设备通信,就把对应的CS拉低。注意,片选切换时最好留点时间,别太快。
全双工是SPI的一大优势。什么意思?就是发送和接收可以同时进行。你发一个字节的同时,也能收到一个字节。这在I2C里是做不到的。
// SPI读写示例(伪代码)
uint8_t spi_transfer(uint8_t data) {
// 发送一个字节,同时接收一个字节
for (int i = 7; i >= 0; i--) {
// 发送位
if (data & (1 << i))
MOSI_HIGH();
else
MOSI_LOW();
// 时钟上升沿
SCK_HIGH();
delay();
// 接收位
if (MISO_READ())
rx_data |= (1 << i);
// 时钟下降沿
SCK_LOW();
delay();
}
return rx_data;
}
小技巧:我习惯用DMA做SPI传输,尤其是大数据量的时候。CPU不用一直等着,效率高很多。但要注意DMA的配置,别搞错了传输方向。
三、UART协议详解
UART,Universal Asynchronous Receiver/Transmitter。它是最简单的串行通信协议,就两根线——TX和RX。没有时钟线,全靠约定的波特率来同步。
数据帧格式
UART的一帧数据包括:起始位(1位)、数据位(5-8位)、校验位(可选)、停止位(1-2位)。最常见的是8N1——8位数据、无校验、1位停止位。
通信过程是这样的:
- 空闲时,TX线为高电平
- 起始位:TX拉低一个位时间
- 数据位:从低位到高位依次发送
- 校验位:奇校验或偶校验(可选)
- 停止位:TX拉高一个或多个位时间
// UART发送一个字节(伪代码)
void uart_send_byte(uint8_t data) {
// 起始位
TX_LOW();
delay_bit_time();
// 发送8位数据(低位在前)
for (int i = 0; i < 8; i++) {
if (data & (1 << i))
TX_HIGH();
else
TX_LOW();
delay_bit_time();
}
// 停止位
TX_HIGH();
delay_bit_time();
}
避坑指南:我曾经在一个项目里,UART通信偶尔会丢字节。查了半天,发现是波特率误差太大。晶振频率和波特率不匹配,导致采样点偏移。所以啊,选波特率时一定要算一下误差,最好在2%以内。
波特率与同步
UART没有时钟线,收发双方必须用相同的波特率。常见的波特率有9600、115200等。波特率越高,传输越快,但对时钟精度要求也越高。
接收端怎么知道什么时候采样?它会在检测到起始位(TX从高变低)后,等待半个位时间,然后开始采样。之后每个位时间采样一次。这就是所谓的「异步」——没有时钟线,全靠约定好的速率。
个人习惯:我一般用115200作为调试串口的波特率。9600太慢,再高的话容易受干扰。另外,记得加个环形缓冲区,防止数据丢失。尤其是中断接收的时候,缓冲区能帮你缓冲一下处理不过来的数据。
小结
好了,三个协议都讲完了。我总结一下:
- I2C:两根线,多设备,速度慢,适合传感器配置和少量数据传输
- SPI:四根线,速度快,全双工,适合高速数据采集
- UART:两根线,异步,简单,适合调试和低速通信
你想想看,这三个协议各有各的适用场景。做能耗监测传感器,我一般用I2C配置传感器参数,用SPI读取高速数据,用UART输出调试信息。选对协议,事半功倍。
下一章,咱们会结合具体的传感器芯片,讲讲怎么把这些协议用在实际的驱动开发中。到时候我会拿一个真实的项目案例来演示,保证你们能学到干货。