3. Rust嵌入式开发环境搭建
SPI,全称Serial Peripheral Interface,是嵌入式世界里最常用的同步串行通信协议之一。说实话,我调试过的板子里面,十块有八块都挂着SPI设备——从Flash存储器到传感器,从LCD屏幕到SD卡槽,到处都是它的身影。
为什么这么流行?因为它简单、快速、全双工。但简单不代表不会出问题。我见过太多工程师被SPI折腾得焦头烂额,最后发现只是时钟极性配错了,或者CS引脚没拉对。今天我们就把它彻底讲透。
SPI四线制:每根线的职责
标准的SPI通信需要四根线。你想想看,这四根线各司其职,缺一不可。
| 信号线 | 全称 | 方向 | 作用 |
|---|---|---|---|
| SCK | Serial Clock | 主机→从机 | 时钟信号,由主机产生 |
| MOSI | Master Out Slave In | 主机→从机 | 主机发送数据给从机 |
| MISO | Master In Slave Out | 从机→主机 | 从机发送数据给主机 |
| CS | Chip Select | 主机→从机 | 片选信号,低电平有效 |
这里有个关键点:SPI是全双工的。什么意思?就是主机在发送数据的同时,也在接收数据。每次时钟跳变,数据同时在两个方向上移动一位。我在项目中遇到过有人以为SPI是半双工的,结果写驱动时只发了数据没读回来,折腾了半天才发现问题。
核心要点:SPI的每次传输都是同时发送和接收。如果你只关心发送,接收到的数据也必须处理,否则会干扰后续通信。
时钟极性与相位:最容易踩的坑
SPI的时钟配置有两个参数:CPOL(Clock Polarity,时钟极性)和CPHA(Clock Phase,时钟相位)。这两个参数决定了数据在时钟的哪个边沿被采样。
- CPOL = 0:空闲时SCK为低电平
- CPOL = 1:空闲时SCK为高电平
- CPHA = 0:在第一个时钟边沿采样数据
- CPHA = 1:在第二个时钟边沿采样数据
组合起来就是四种模式:
| 模式 | CPOL | CPHA | 采样边沿 | 常见设备 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿 | 大多数Flash、SD卡 |
| 模式1 | 0 | 1 | 下降沿 | 某些ADC |
| 模式2 | 1 | 0 | 下降沿 | 某些传感器 |
| 模式3 | 1 | 1 | 上升沿 | 某些RF模块 |
我曾经在一个项目里,把Flash芯片的CPOL配反了。结果呢?数据读出来全是0xFF。用逻辑分析仪一看,时钟空闲电平不对,数据根本没被正确采样。嗯,从那以后我养成了一个习惯:拿到新芯片,第一件事就是看数据手册里的时序图。
警告:主从设备的CPOL和CPHA必须完全一致,否则通信会直接失败。不要想当然地认为默认模式0就能用,一定要查手册确认。
多从机拓扑:怎么接多个设备
SPI支持一主多从的结构。有两种常见的接法:
1. 独立片选方式
每个从机独占一根CS线。主机通过拉低对应从机的CS来选中它,其他从机的CS保持高电平。
- 优点:从机之间完全独立,互不干扰
- 缺点:占用GPIO多,N个从机需要N+3根线
- 适用场景:从机数量少(一般不超过4个)
2. 菊花链方式
所有从机共享SCK、MOSI、MISO,但数据从第一个从机传到第二个,再传到第三个……
- 优点:节省GPIO,只需要4根线
- 缺点:延迟累加,时序要求高
- 适用场景:从机数量多,且支持菊花链模式
我个人习惯用独立片选方式。为什么?调试方便。菊花链一旦出问题,你根本不知道是哪个节点坏了。我在一个项目里试过菊花链接三个ADC,结果中间那个芯片虚焊,后面的数据全乱了。排查了整整一个下午。
经验之谈:多从机系统中,MISO线需要做三态控制。未被选中的从机必须把MISO置为高阻态,否则多个从机同时驱动MISO会导致总线冲突。很多初学者会忽略这一点。
电气特性:信号完整性基础
SPI的速率可以很高——从几百kHz到几十MHz。速率越高,信号完整性问题越突出。
几个关键参数:
- 上升/下降时间:信号从低到高或高到低的时间,一般控制在时钟周期的10%以内
- 建立时间:数据在时钟边沿到来前必须稳定的时间
- 保持时间:数据在时钟边沿后必须继续稳定的时间
- 最大时钟频率:由最慢的从机决定,不是主机想多快就多快
我记得有一次调试一块高速ADC,时钟跑到20MHz时数据就乱码了。用示波器一看,MISO线上的信号振铃严重。加了个33Ω的串联电阻,问题就解决了。说白了,高速SPI的布线要讲究:线要短,地要近,阻抗要匹配。
SPI通信结构图
下面这张图展示了SPI一主多从的典型连接方式:
从图中可以看到,SCK、MOSI、MISO是共享总线,所有从机都接在一起。而CS是独立的,每个从机一根。这就是独立片选方式的精髓。
Rust中的SPI配置示例
在嵌入式Rust中,配置SPI通常使用embedded-hal的SPI trait。下面是一个典型的初始化代码:
use embedded_hal::spi::{Mode, Phase, Polarity};
// 定义SPI模式:模式0
const MODE_0: Mode = Mode {
polarity: Polarity::IdleLow,
phase: Phase::CaptureOnFirstTransition,
};
// 初始化SPI外设
let spi = Spi::new(
spi_peripheral,
pins.sck,
pins.mosi,
pins.miso,
8_000_000u32.Hz(), // 8MHz时钟
MODE_0,
);
// 片选引脚管理
let mut cs = Output::new(pins.cs, Level::High);
// 发送数据
cs.set_low();
spi.transfer(&mut buffer).unwrap();
cs.set_high();
这里要注意:片选引脚必须由GPIO手动控制。embedded-hal的SPI trait不负责CS管理,你需要自己拉低CS、发起传输、再拉高CS。我见过有人忘了拉高CS,结果下一个设备通信时发现总线一直被占用。
Rust小技巧:可以用一个结构体封装CS引脚和SPI外设,实现Drop trait来自动拉高CS。这样即使发生panic,CS也能被正确释放,避免总线锁死。
常见故障与排查思路
SPI通信出问题,通常逃不出这几个原因:
- 时钟模式不匹配:主从设备的CPOL/CPHA不一致。用逻辑分析仪抓一下SCK和MOSI的时序,一眼就能看出来。
- CS时序错误:CS拉低后没有等待足够时间就发数据,或者传输结束后没有拉高CS。有些芯片对CS的建立时间有要求。
- 速率过高:从机跟不上主机的时钟频率。降速试试,如果降速后正常了,那就是速率问题。
- 信号完整性问题:线太长、地线不好、没有匹配电阻。示波器看波形,如果有过冲或振铃,加电阻或降低速率。
- MISO总线冲突:多个从机同时驱动MISO。检查CS逻辑,确保同一时间只有一个从机被选中。
我曾经在一个项目里,SPI通信时好时坏。查了三天,最后发现是CS引脚虚焊,接触不良。用手一按芯片,通信就正常了。嗯,硬件问题往往比软件问题更难排查。
调试建议:手边常备逻辑分析仪。SPI调试,看波形比看代码更直观。我个人的习惯是:先抓波形确认物理层没问题,再去看驱动代码。
SPI协议本身不复杂,但细节决定成败。时钟极性、片选时序、信号完整性——每一个环节都可能成为绊脚石。理解了这些原理,你就能在调试时快速定位问题,而不是盲目地改代码碰运气。