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一主多从的典型连接方式:

SPI一主多从拓扑结构图 主机 (MCU) 从机1 Flash 从机2 ADC 从机3 传感器 SCK SCK SCK MOSI MOSI MOSI MISO MISO MISO CS1 CS2 CS3 信号线说明 SCK: 时钟线(黑色实线) MOSI: 主机→从机(红色虚线) MISO: 从机→主机(紫色点线) CS: 片选线(橙色实线)

从图中可以看到,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通信出问题,通常逃不出这几个原因:

  1. 时钟模式不匹配:主从设备的CPOL/CPHA不一致。用逻辑分析仪抓一下SCK和MOSI的时序,一眼就能看出来。
  2. CS时序错误:CS拉低后没有等待足够时间就发数据,或者传输结束后没有拉高CS。有些芯片对CS的建立时间有要求。
  3. 速率过高:从机跟不上主机的时钟频率。降速试试,如果降速后正常了,那就是速率问题。
  4. 信号完整性问题:线太长、地线不好、没有匹配电阻。示波器看波形,如果有过冲或振铃,加电阻或降低速率。
  5. MISO总线冲突:多个从机同时驱动MISO。检查CS逻辑,确保同一时间只有一个从机被选中。

我曾经在一个项目里,SPI通信时好时坏。查了三天,最后发现是CS引脚虚焊,接触不良。用手一按芯片,通信就正常了。嗯,硬件问题往往比软件问题更难排查。

调试建议:手边常备逻辑分析仪。SPI调试,看波形比看代码更直观。我个人的习惯是:先抓波形确认物理层没问题,再去看驱动代码。

SPI协议本身不复杂,但细节决定成败。时钟极性、片选时序、信号完整性——每一个环节都可能成为绊脚石。理解了这些原理,你就能在调试时快速定位问题,而不是盲目地改代码碰运气。