2. 硬件接口基础:SPI、I2C、QSPI 总线协议详解
做 NVM 存储模块,说白了就是跟 Flash 芯片打交道。而 Flash 芯片怎么跟主控沟通?靠的就是 SPI、I2C、QSPI 这些总线协议。我见过不少工程师,代码写得飞起,但连时序图都读不利索,结果板子调不通,拿着示波器干瞪眼。
这一章,咱们就把这三个协议掰开揉碎。不讲虚的,直接上时序图和实战波形。
2.1 SPI 总线:最经典的存储接口
SPI(Serial Peripheral Interface)是 NVM 模块用得最多的接口。它简单、高速、全双工,非常适合 Flash 这种需要批量数据传输的场景。
2.1.1 四线制与信号定义
SPI 用四根线:
- SCLK(串行时钟)—— 由主控产生,决定传输速率
- MOSI(主出从入)—— 主控发数据给从设备
- MISO(主入从出)—— 从设备发数据给主控
- CS(片选)—— 低电平有效,选中目标设备
注意,SPI 是全双工的。什么意思?主控发一个字节的同时,从设备也在发一个字节。你想想看,这比 I2C 那种半双工效率高多了。
2.1.2 四种模式(CPOL & CPHA)
SPI 有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。很多新手在这里栽跟头。
| 模式 | CPOL | CPHA | 时钟空闲电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿 |
| 模式 1 | 0 | 1 | 低电平 | 下降沿 |
| 模式 2 | 1 | 0 | 高电平 | 下降沿 |
| 模式 3 | 1 | 1 | 高电平 | 上升沿 |
大多数 SPI Flash 默认用模式 0。但别想当然,一定要看数据手册。我曾经被一颗国产 Flash 坑过,手册上写支持模式 0,实际跑起来必须用模式 3 才能稳定通信。查了两天才发现是芯片内部逻辑有 bug。
2.1.3 时序图实战分析
咱们来看一个典型的 SPI Flash 读数据时序。主控先发指令(0x03),再发 3 字节地址,然后从设备连续返回数据。
// SPI Flash 读数据伪代码
CS = 0; // 拉低片选
spi_write(0x03); // 发送读指令
spi_write(addr >> 16); // 发送地址高字节
spi_write(addr >> 8); // 发送地址中字节
spi_write(addr); // 发送地址低字节
data = spi_read(); // 读取第一个字节
data = spi_read(); // 读取第二个字节
// ...
CS = 1; // 拉高片选
这里有个关键点:地址发送完后,主控需要继续提供时钟,从设备才会在 MISO 上返回数据。很多初学者以为发完地址就完事了,结果读回来的全是 0xFF。
2.2 I2C 总线:两线制的优雅与局限
I2C 只有两根线:SCL(时钟)和 SDA(数据)。它用地址来区分设备,一根总线上可以挂几十个芯片。但代价是什么?速度慢,而且协议复杂。
2.2.1 信号与协议基础
I2C 的几个关键概念:
- 起始条件:SCL 高电平时,SDA 从高变低
- 停止条件:SCL 高电平时,SDA 从低变高
- 应答(ACK):每发送 8 位数据后,接收方拉低 SDA 表示收到
- 非应答(NACK):接收方不拉低 SDA,表示结束或错误
I2C 的地址通常是 7 位,加上读写位组成 8 位。比如一个 EEPROM 的地址是 0x50,读操作就是 0xA1,写操作是 0xA0。
2.2.2 标准速率与模式
| 模式 | 最大速率 | 适用场景 |
|---|---|---|
| 标准模式 | 100 kbps | 低速传感器、小容量 EEPROM |
| 快速模式 | 400 kbps | 大多数 I2C 设备 |
| 快速模式+ | 1 Mbps | 高速 EEPROM、小容量 NVM |
| 高速模式 | 3.4 Mbps | 特殊应用,但兼容性差 |
说实话,I2C 在 NVM 存储领域用得越来越少。为什么?因为速度太慢。一个 256 字节的页写入,SPI 几微秒就搞定了,I2C 要磨蹭几十微秒。但 I2C 有个不可替代的优势:引脚少。如果你做的是小尺寸产品,比如智能戒指、医疗贴片,I2C 可能是唯一选择。
2.2.3 时序图实战分析
来看一个 I2C EEPROM 的页写入时序:
// I2C EEPROM 页写入伪代码
i2c_start(); // 发送起始条件
i2c_write(0xA0); // 发送设备地址 + 写位
i2c_wait_ack(); // 等待应答
i2c_write(addr_high); // 发送地址高字节
i2c_wait_ack();
i2c_write(addr_low); // 发送地址低字节
i2c_wait_ack();
for (i = 0; i < 16; i++) { // 发送 16 字节数据
i2c_write(data[i]);
i2c_wait_ack();
}
i2c_stop(); // 发送停止条件
注意,I2C 的应答机制是强制性的。如果从设备没拉低 SDA 表示 ACK,主控必须认为通信失败。我遇到过一种情况:上拉电阻选太大,SDA 信号上升沿太慢,从设备明明发了 ACK,主控却读不到。后来把 10kΩ 换成 4.7kΩ,问题解决。
2.3 QSPI 总线:四线并行的性能飞跃
QSPI(Quad SPI)是 SPI 的升级版。它把 MOSI 和 MISO 扩展成四根数据线(IO0~IO3),一个时钟周期能传 4 个 bit。说白了,就是 SPI 的带宽翻了 4 倍。
2.3.1 与标准 SPI 的对比
| 特性 | 标准 SPI | QSPI |
|---|---|---|
| 数据线数量 | 2(MOSI + MISO) | 4(IO0~IO3) |
| 每时钟传输 bit | 1 | 4 |
| 典型速率 | 50~100 Mbps | 200~400 Mbps |
| 引脚数 | 4 | 6(加了两根数据线) |
| 协议复杂度 | 低 | 中 |
QSPI 的厉害之处在于,它可以在单线模式和四线模式之间切换。指令阶段用单线,数据阶段切到四线。这样既兼容 SPI,又能在传输大数据时飙速度。
2.3.2 操作模式详解
QSPI 有三种操作模式:
- 标准模式:跟普通 SPI 一样,用 MOSI/MISO
- 双线模式(Dual SPI):用两根数据线,每时钟传 2 bit
- 四线模式(Quad SPI):用四根数据线,每时钟传 4 bit
实际应用中,读 Flash 时最常用四线模式。比如 W25Q64 这个经典芯片,用四线模式读数据,速度能到 80MB/s 以上。你想想看,这比标准 SPI 快了将近 4 倍。
2.3.3 时序图实战分析
QSPI 的时序比 SPI 复杂一些。以四线快速读(指令 0xEB)为例:
// QSPI 四线快速读时序
CS = 0;
// 阶段 1:发送指令(单线模式)
qspi_write_byte(0xEB); // 四线快速读指令
// 阶段 2:发送 3 字节地址(单线模式)
qspi_write_byte(addr_high);
qspi_write_byte(addr_mid);
qspi_write_byte(addr_low);
// 阶段 3:发送 1 字节 dummy(单线模式)
qspi_write_byte(0x00); // 等待内部读取完成
// 阶段 4:切换到四线模式读取数据
qspi_set_quad_mode(); // 将 IO0~IO3 设为双向
for (i = 0; i < 256; i++) {
data[i] = qspi_read_quad(); // 每时钟读 4 bit
}
CS = 1;
注意那个 dummy 字节。为什么需要它?因为 Flash 内部需要时间把数据从存储阵列搬到缓存。这个等待时间因芯片而异,有的需要 4 个时钟,有的需要 8 个。一定要看数据手册。
2.4 三种协议的选型对比
做项目时怎么选?我一般按这个思路来:
| 需求场景 | 推荐协议 | 理由 |
|---|---|---|
| 引脚受限、容量小 | I2C | 只需 2 根线,适合小封装 |
| 通用存储、中等容量 | SPI | 简单可靠,生态成熟 |
| 高速存储、大容量 | QSPI | 带宽高,适合代码执行 |
| 极端高速、XIP 执行 | QSPI + DDR 模式 | 双倍数据速率,性能极致 |
我个人习惯是:能用 QSPI 就不用 SPI,能用 SPI 就不用 I2C。除非引脚实在不够,否则别在 I2C 上浪费性能。
2.5 本章知识体系
下面这张图总结了三种协议的核心关系,帮你快速建立知识框架:
这张图把三种协议的核心特性、选型对比都列出来了。你可以把它当成一个快速参考,做项目时拿出来对照一下,基本不会选错。
2.6 实战波形解读技巧
最后,分享几个看波形的小技巧。这些是我这些年用示波器抓出来的经验:
- 先看时钟:SCLK 或 SCL 的频率对不对?占空比是不是 50%?如果时钟歪了,后面都不用看。
- 再看片选:CS 有没有在正确的时间拉低和拉高?很多通信失败是因为 CS 毛刺或者没拉低。
- 检查建立/保持时间:数据信号在时钟边沿前后有没有稳定?特别是高速 SPI 和 QSPI,这个很容易出问题。
- 确认应答:I2C 一定要看到 ACK 位。如果看到 NACK,要么地址错了,要么设备没准备好。
- 对比数据:发出去的数据和抓到的数据逐 bit 对比。有时候波形看着对,但数据就是错的,可能是位序搞反了。