3、硬件接口基础:I2C协议详解(时序、地址、读写操作)、SPI协议详解(模式、片选、全双工)、GPIO模拟时序
各位同学,大家好。今天我们聊聊嵌入式开发里最基础、也最绕不开的两个协议——I2C和SPI。再加上GPIO模拟时序,这几乎是每个驱动工程师的必修课。
我个人习惯,讲协议之前先问一个问题:为什么需要这些协议?说白了,芯片之间要说话,就得有共同的“语言”。I2C和SPI就是两种最流行的“方言”。
3.1 I2C协议详解
I2C,全称Inter-Integrated Circuit,飞利浦公司发明的。它最大的特点就是省引脚——只用两根线:SCL(时钟线)和SDA(数据线)。
我在项目中遇到过不少新手,上来就问:“两根线怎么传数据?”嗯,这就是I2C的精妙之处。
3.1.1 时序基础
I2C的时序,说白了就是SCL和SDA两根线上的电平变化规则。我把它拆成几个关键动作:
- 起始条件(Start Condition):SCL高电平时,SDA从高变低。这是通信开始的信号。
- 停止条件(Stop Condition):SCL高电平时,SDA从低变高。通信结束。
- 数据采样:数据在SCL低电平时变化,高电平时被采样。记住这个节奏。
关键点:I2C是同步协议,所有动作都跟着SCL走。没有时钟,就没有数据。
3.1.2 地址与读写操作
I2C总线上可以挂多个设备,每个设备有唯一的7位或10位地址。我常用的7位地址,最多挂127个设备。
读写操作流程是这样的:
- 主机发送起始条件
- 主机发送7位从机地址 + 1位读写位(0写,1读)
- 从机应答(ACK)
- 数据传输(每字节后跟一个ACK)
- 主机发送停止条件
避坑指南:我曾经在调试一个温度传感器时,死活读不到数据。折腾了半天,发现是地址搞错了——芯片手册上写的地址是7位,但我忘了左移一位。记住:I2C地址在发送时是左移一位,最低位是读写位。
举个例子,读一个寄存器:
// 伪代码:读I2C设备寄存器
i2c_start(); // 起始条件
i2c_send_byte(0x50 << 1 | 0); // 地址+写位
i2c_wait_ack(); // 等待应答
i2c_send_byte(reg_addr); // 发送寄存器地址
i2c_wait_ack();
i2c_start(); // 重复起始条件
i2c_send_byte(0x50 << 1 | 1); // 地址+读位
i2c_wait_ack();
data = i2c_read_byte(); // 读取数据
i2c_send_nack(); // 发送非应答
i2c_stop(); // 停止条件
3.2 SPI协议详解
SPI,全称Serial Peripheral Interface。和I2C不同,SPI是全双工的,而且速度更快。你想想看,I2C最高也就几MHz,SPI轻松上几十MHz。
3.2.1 四根线
SPI用四根线:
- SCLK:时钟线,由主机控制
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出
- CS/SS:片选线,低电平有效
我刚开始学SPI时,总觉得MOSI和MISO容易搞混。后来记住一句话:“主出从入,主入从出”,再也没错过。
3.2.2 四种模式
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 数据采样边沿 |
|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 |
| 模式1 | 0 | 1 | 下降沿采样 |
| 模式2 | 1 | 0 | 下降沿采样 |
| 模式3 | 1 | 1 | 上升沿采样 |
注意:我曾经在移植一个SPI Flash驱动时,发现读写数据全是乱的。查了半天,原来是主控和从设备的SPI模式不匹配。主控默认模式0,Flash要求模式3。记住:通信双方模式必须一致。
3.2.3 片选与全双工
片选(CS)是SPI的一大特色。每个从设备独占一根CS线,主机通过拉低对应CS来选择通信对象。这就避免了I2C那种地址冲突的问题。
全双工是什么意思?就是主机发数据的同时,也能收数据。SPI的移位寄存器是同时工作的——你发一个字节,同时就收到一个字节。我习惯用“交换数据”来理解它。
// 伪代码:SPI全双工读写
uint8_t spi_transfer(uint8_t tx_data) {
// 发送一个字节,同时接收一个字节
SPI_DR = tx_data; // 写入发送寄存器
while(!(SPI_SR & TX_EMPTY)); // 等待发送完成
while(!(SPI_SR & RX_FULL)); // 等待接收完成
return SPI_DR; // 读取接收数据
}
3.3 GPIO模拟时序
有时候,芯片的硬件I2C或SPI外设不够用,或者要接一些非标设备,就得用GPIO来“模拟”协议时序。说白了,就是用软件控制GPIO的高低电平,模拟出协议规定的波形。
我做过一个项目,主控只有两个SPI外设,但接了三个SPI设备。没办法,第三个设备只能用GPIO模拟。嗯,这里要注意:GPIO模拟的时序精度取决于CPU的主频和中断响应时间。
3.3.1 GPIO模拟I2C
模拟I2C的关键是精确控制SCL和SDA的时序。我常用的方法是:
- 用定时器或延时函数控制时钟周期
- 严格按照I2C规范,保证建立时间和保持时间
- 注意开漏输出和上拉电阻
// 模拟I2C起始条件
void i2c_sim_start(void) {
SDA_OUT(); // SDA设为输出
SDA_HIGH(); // SDA高电平
SCL_HIGH(); // SCL高电平
delay_us(5); // 保持时间
SDA_LOW(); // SDA拉低
delay_us(5); // 保持时间
SCL_LOW(); // SCL拉低
}
避坑指南:我曾经用GPIO模拟I2C时,发现通信偶尔会失败。后来用示波器一看,原来是延时函数不够准——系统中断导致时序抖动。解决办法:关中断,或者用硬件定时器做精确延时。
3.3.2 GPIO模拟SPI
模拟SPI相对简单,因为SPI的时序要求没那么严格。关键是:
- 控制好时钟频率,别太快
- 注意数据在时钟边沿的建立时间
- 片选信号要稳定
// 模拟SPI模式0发送一个字节
void spi_sim_send_byte(uint8_t data) {
for(int i = 7; i >= 0; i--) {
SCLK_LOW(); // 时钟拉低
if(data & (1 << i)) {
MOSI_HIGH(); // 数据位1
} else {
MOSI_LOW(); // 数据位0
}
delay_us(1); // 建立时间
SCLK_HIGH(); // 时钟拉高,采样数据
delay_us(1); // 保持时间
}
SCLK_LOW(); // 结束
}
3.3.3 什么时候用GPIO模拟?
我个人总结了几种场景:
- 硬件外设不够用:比如上面说的,多个设备争抢外设
- 非标协议:有些传感器时序和标准协议略有不同,用GPIO模拟更灵活
- 调试阶段:先模拟验证,再切到硬件外设
- 成本敏感:低端MCU没有硬件I2C/SPI,只能模拟
注意:GPIO模拟不适合高速通信。我一般只在100kHz以下的I2C和1MHz以下的SPI用模拟。再高的话,CPU就忙不过来了。
好了,这一章的内容就到这里。I2C和SPI是嵌入式开发的基石,GPIO模拟则是灵活应变的手段。下一章我们聊聊中断处理,这可是驱动开发的灵魂。