☰
树莓派SPI调试全指南:从原理到实战避坑详解
2026/10/15 17:47:04 网站建设 项目流程

先说结论:树莓派上做 SPI 调试,大多数人不是死在接线和代码上,而是死在“系统里根本就没把 SPI 开出来”和“时序对不对得上”这两件事上。这篇文章把我自己从零开始调通 SPI 外设的完整过程、踩过的坑、用过的工具和方法都摊开讲一遍。如果你正准备在树莓派上接 SPI 屏幕、ADC、传感器或者 DAC,照着这条路走,能省下不少搜教程的时间。

因为 SPI 看着规整(就四根线),实际上比 I2C 和 UART 更容易让人栽跟头。I2C 好歹有地址和设备树可以用来快速判断总线是否工作,SPI 则需要自己定义片选、极性、相位,还要匹配主从设备,一旦配置不一致,波形是出来了,数据却是乱的。我这次调的是某 SPI 接口的 ADC 芯片,目标是把单端电压信号采集进来,通过 SPI 以 1 MHz 左右的时钟读取转换结果,再在终端里打印出来。整个项目本身不复杂,但调试过程中几乎把 SPI 相关的坑都踩了一遍,所以写下来很啰嗦,希望对你有用。

1. 先把要调什么、怎么调这件事想清楚

1.1 SPI 调试为什么容易翻车

SPI 全称是 Serial Peripheral Interface,同步串行接口。它有一条时钟线 SCLK、两条数据线 MOSI 和 MISO,外加一条片选线 CS/CE。发送和接收是同时进行的,不存在像 I2C 那样的 ACK 应答机制,所以只要主从设备的极性、相位、位宽、速率对不上,信号波形照样很“饱满”,但数据读出来全是 0xFF 或者 0x00,最气人的是你还看不出毛病在哪。

调试 SPI 的第一个核心认知是:SPI 调的是“约定”,不是调通不通。电线上只要有 GPIO 翻转和时钟信号,物理层面就算通了;但想让从设备正确理解主设备发出的每一位数据,必须确保四个参数完全对齐:

  • CPOL(时钟极性):空闲时时钟是高还是低。
  • CPHA(时钟相位):数据在第一个边沿采样还是第二个边沿采样。
  • 位宽:通常 8 位,但很多 ADC/DAC 用 16 位甚至 24 位。
  • 速率:从设备支持的极限频率。树莓派默认是 125 MHz 的 SPI 核心时钟往下分。

这四个参数中任何一个不对,你的调试就会变成“盲调”。所以我的做法不是拿到芯片先写代码,而是先去看数据手册里的时序图和寄存器配置命令,把 CPOL/CPHA 查清楚,再决定代码里怎么设。这也是我在这次调试里最推荐别人的第一步。

1.2 树莓派上到底有哪些 SPI 可以用

树莓派这代板子和前几代一样,CPU 内部带两个 SPI 控制器。默认只启用 SPI0,它最多可以引出两个片选信号,对应/dev/spidev0.0和/dev/spidev0.1。SPI1 是给官方 GPIO 扩展座预留的,普通引脚上通常不引出来,我们一般先用 SPI0。

树莓派引脚的 SPI0 分布是这样的(BCM 编号):

功能BCM 引脚编号物理排针位置
MOSIGPIO10Pin 19
MISOGPIO9Pin 21
SCLKGPIO11Pin 23
CE0GPIO8Pin 24
CE1GPIO7Pin 26

用gpioinfo或者pinout命令可以确认。我习惯在接线前先把这几个引脚的编号牢牢记在心里,因为 SPI 真的只是一根线接错,时钟和数据乱了,读出来的值就会非常离谱,而且不会报错。

需要注意的一点是硬件片选由内核 GPIO 控制,而我们用软件把 CE 当作普通 GPIO 拉低时也能工作,但是不推荐。你手动拉低片选的同时,内核驱动也可能会去操作 CE,两个都对同一个引脚输出,会造成竞争。我用/dev/spidev0.0时基本不碰这个引脚,直接用系统自带片选逻辑。

2. 让树莓派系统把 SPI“放出来”

2.1 界面与文件两种开启方式

不管你是跑官方 Raspberry Pi OS 还是精简的 Lite 系统,第一步都是让内核加载 SPI 驱动。最简单的方式是:

sudo raspi-config

进入Interface Options→SPI→ 启用。重启之后,系统里如果能看到/dev/spidev0.0和/dev/spidev0.1,就算成功了一半。

但我更习惯直接改配置文件的方式,方便批量部署或者排查为什么界面里开了却不起效。对应配置文件是/boot/config.txt,在树莓派上它是引导加载环境里的一段配置,内核和用户态都能读到。启用 SPI 需要在底部加入:

dtparam=spi=on

如果你是更老的内核或者想明确指定两个控制器的状态,也可以写:

dtparam=spi0=on dtparam=spi1=off

注意:这段配置本身不叫设备树覆盖,它只是设置了一个布尔参数。如果你要改动 SPI 的引脚映射、片选数量、DMA 通道等,才需要动到类似dtoverlay=spi0-1cs这种覆盖。默认情况你只接一个 SPI 从设备,dtparam=spi=on就够了。

修改完必须重启,因为这里涉及的是内核和固件层面的接口枚举,不像多数用户态程序可以热加载。重启之后先不要急着写代码,先跑一条测试命令:

ls -l /dev/spidev*

如果看不到spidev0.*这样的节点,原因大概率是/boot/config.txt格式写错了、或者你运行的内核缺少spidev驱动模块。再查一下:

cat /proc/device-tree/aliases/ spi0

这段如果不空,说明系统承认 SPI0 的存在。另外也可以用dmesg | grep -i spi看内核启动时有没有报错。老实说,这一步排查完,很多人的 SPI 问题已经从“调不通”变成“该调代码了”。

2.2 确认用户态有权限操作设备节点

设备节点存在不代表应用层就能直接写。/dev/spidev*默认属于spi组,组权限通常是 6 或者 0。我遇到过重启后权限正确、但跑一段程序却报open failed: Permission denied的情况,那次是程序用systemd服务方式自动启动,没有继承用户组身份。

处理办法无非两种:

sudo usermod -aG spi 你的用户名

或者临时给设备节点放开权限:

sudo chmod 666 /dev/spidev*

但我不建议长期放开权限。如果是写一个开机自启动的采集脚本,最稳的做法是在systemd service里加上User=指定到加入了spi组的用户,或者直接在服务参数里用ExecStartPre=/bin/chmod 666 /dev/spidev0.0。这种权限问题在调试阶段不算大事,但等你在柜子里放了几个月再拿出来,突然发现脚本跑不了,就非常耽误事了。

3. 最小可用的读写代码:先让数据从引脚上走起来

3.1 用 spidev_test 先打一发“实弹”

树莓派内核源码里自带一个小工具spidev_test.c,它可以直接从命令行发起 SPI 读写,非常适合先确认主设备和从设备之间最基本的通信链路是否正常。我习惯在调试初期先编译它:

wget https://raw.githubusercontent.com/raspberrypi/linux/rpi-6.6.y/tools/spi/spidev_test.c gcc -o spidev_test spidev_test.c

如果没有现成的测试源码,自己手写一个最小读写程序也不难,核心就是ioctl设置模式、位宽、速率,然后read/write。我这次调 ADC 就是先用十六进制数据直接喂给设备,然后再看读回来是不是预期值。

注意看这几个参数:

uint8_t mode = SPI_MODE_0; uint8_t bits = 8; uint32_t speed = 1000000; uint16_t delay = 0;

SPI_MODE_0意味着 CPOL=0,CPHA=0,即时钟空闲是低电平,数据在第一个上升沿采样。很多温度传感器、加速度计和 ADC 都支持这个模式,但绝不是所有设备都支持。我调某 ADC 芯片时,它的数据手册说明支持 SPI Mode 1(CPOL=0,CPHA=1),如果我没看手册直接用 Mode 0,读回来的数据就是最低位错位,结果完全不对。

ioctl的调用方式如下:

int fd = open("/dev/spidev0.0", O_RDWR); ioctl(fd, SPI_IOC_WR_MODE, &mode); ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits); ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);

然后发送一个字节0x9C给 ADC,让它启动转换,同时读回一个字节:

uint8_t tx[] = {0x9C}; uint8_t rx[] = {0x00}; struct spi_ioc_transfer tr = { .tx_buf = (unsigned long)tx, .rx_buf = (unsigned long)rx, .len = 1, .speed_hz = speed, .delay_usecs = delay, .bits_per_word = bits, }; ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

这一步能过,说明物理连接和内核节点都是通的。如果ioctl返回 -1,问题基本出在引脚冲突或者内核驱动拒绝模式下。如果返回正常但数据一直固定某个值,下一步就要上逻辑分析仪。

3.2 写 Python 脚本快速验证多种模式

C 程序适合最终部署,但调试阶段我更爱用 Python,因为改模式、改速率只需要改一个函数参数,跑一下就能看到结果。用spidev库即可:

sudo apt install python3-spidev python3-pip

最小脚本是:

import spidev import time spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 1000000 spi.mode = 0b00 spi.bits_per_word = 8 resp = spi.xfer2([0x9C, 0x00, 0x00]) print([hex(x) for x in resp]) spi.close()

xfer2在本次事务中保持片选拉低,适合连续读取;xfer则每个字节之间会有片选高电平的变化。对某些 ADC 来说,片选重新拉高就代表一次转换结束,重新拉低重新开始,所以你会看到xfer2和xfer的结果完全不同。我强烈建议你调试时把两个都试一遍,不要死磕一个函数。

Python 的好处是能快速遍历 Mode 0 到 Mode 3:

for mode in range(4): spi.mode = mode resp = spi.xfer2([0x00, 0x00]) print(mode, [hex(x) for x in resp])

如果某个模式下读回来全是0xFF或者全是0x00,别急着下结论说接线坏了。SPI 设备在高阻态时,MISO 可能被上拉或下拉,读回全0xFF通常意味着从设备根本没有驱动 MISO,要么是片选没拉对,要么是命令没被识别。这种情况要优先查 CS 和时钟,而不是怀疑芯片烧了。

4. 上逻辑分析仪:把时钟和数据“看在眼里”

4.1 抓取波形的基本接法与采样策略

你可以在没有逻辑分析仪的情况下靠返回值反复试,但效率很低。我这次调试到最后,是靠一个不到百元的 8 通道逻辑分析仪把故障定位到根因的。这种分析仪配合开源软件使用,最高采样率 24 MHz,抓 1 MHz 的 SPI 时钟足够。

接线方法不复杂:

  • 逻辑分析仪通道 0 接树莓派的 SCLK。
  • 通道 1 接 MOSI。
  • 通道 2 接 MISO。
  • 通道 3 接 CE0。
  • 所有通道的地线必须和树莓派的地共地。

采样率设置建议是 SPI 时钟的 4 到 8 倍以上。1 MHz 的 SPI,采样率至少设 4 MHz,我习惯设 8 MHz 或 12 MHz。采样率太低了,捕获到的波形会失真,特别是边沿位置不明显,直接影响你判断 CPOL/CPHA 是否正确。

触发方式设为下降沿触发,选择通道 3(片选)。因为 SPI 事务开始的时候,片选会从高拉低,然后时钟才开始跳。这样每次捕获到的数据都是从一次完整事务的起点开始,分析软件里的解码器才不容易错位。

4.2 从波形能读出哪些信息

抓到的波形首先要看两个事情:片选拉低期间,SCLK 是否稳定翻转;MOSI 上有没有正常的电平变化。如果没有时钟,检查有没有把 SPI0 的时钟引脚接错到别的 GPIO 上。如果时钟有、MOSI 有,接下来就要对照从设备数据手册的时序图,确认数据是否在正确的边沿被采样。

以 Mode 0 为例:SCLK 空闲是低,第一个上升沿采样 MOSI 上的数据。所以你在逻辑分析仪上应该看到的是:片选拉低后,SCLK 从低跳高,此时 MOSI 上的第一位数据已经稳定,然后在 SCLK 下降沿之后,MOSI 再切到第二位。如果你看到的数据位变化跟边沿关系错位了半个周期,说明模式设置和实际芯片期望不一致。

再一个容易被忽略的问题是时钟毛刺。树莓派 GPIO 翻转速度很快,如果杜邦线过长或者接线松散,SCLK 上会出现回波和振铃,逻辑分析仪上看到同一沿有好几次抖动。这在实际使用中会导致从设备多采样或少采样一位。我把 SPI 速率降到 500 kHz 后,问题明显缓解,这就是速率跳跃带来的影响。

还有一个小技巧:把逻辑分析仪解码器的 bit order 选对。SPI 通常高位在前,但不少 ADC 芯片的命令寄存器是低位在前。如果软件解码出来二进制看着反了,别怀疑硬件,先去查芯片手册的 MSB/LSB 说明。

5. 真实调试中撞见的典型问题

5.1 数据读回为 0xFF 或者 0x00,各有各的命

我在这次调试里,一开始读回来的永远是0xFF。逻辑分析仪显示 MISO 在整个片选期间都是高电平。查了芯片手册才发现,这颗 ADC 的 MISO 在无数据时是弱上拉,只有片选正确且命令被解析成功后,它才会在对应位驱动 MISO 电平。所以0xFF并不代表连接短路,恰恰说明从设备根本没被“唤醒”。

排查顺序是:

  1. 查片选有没有正常拉到低电平,且持续整个事务时间。
  2. 查 MOSI 上发送的命令字节和芯片手册里的命令格式是否一致。
  3. 查模式是不是芯片要求的 Mode。
  4. 查供电电压是否达标。

如果读回来是0x00,MISO 电平被拉低,大概率是因为协议对了、但从设备返回的数据正好全是 0,或者 MISO 引脚悬空被板载下拉电阻拉低。这时候可以尝试连续多读几个字节,有些 ADC 需要先发一个命令,紧接着才能读出一组结果。只发一字节只读一字节,拿不到有效数据。

5.2 访问/dev/spidev0.1却毫无反应

另一个我见过很多次的场景:用户把设备接在 CE1 引脚上,程序里也正确打开/dev/spidev0.1,但就是读不回来。这往往是因为 SPI0 默认就开了两路片选,但你的设备树配置里只声明了 1 路片选,导致spidev0.1节点并不存在。

处理方式很简单,在/boot/config.txt中指定两路片选:

dtoverlay=spi0-2cs

或者直接不写这个 overlay,默认就是 2cs。如果你只用到一路,让它保持默认也无所谓。不过要注意:CE1 和音频输出的 GPIO 有可能冲突,早期树莓派型号上如果同时启用了音频输出,CE1 会被占用。

5.3 时钟速率不是你想设就能设

树莓派的 SPI 时钟来源于核心时钟的整数分频。你给ioctl传入1000000表示 1 MHz,但实际分频结果可能不是精确的 1 MHz。这个影响不大,大多数从设备对速率都有容忍范围。但如果你把自己限定的速率设成 3.5 MHz 这种非整数分频值,实际得到的可能是 3.125 MHz 或者 4 MHz。高于从设备额定最高速率,通信就会随机出错。

我的经验是尽量选择一个比额定值低 20% 左右的速率,比如设备最高支持 1 MHz,就设 800 kHz,给自己留一点裕量,同时也降低长线带来的信号完整性问题。如果你发现高速率下偶发数据错误,先把速率降低四分之一试试。很多时候“代码没逻辑问题”的 SPI 故障,就是速率太高导致信号质量撑不住。

6. 把一次调试变成一套可复用的方法

6.1 建立“从硬件到内核再到应用”的排查顺序

我这次最深的体会是:SPI 调试的难点不在于某个特定芯片,而在于排错顺序。很多人一上来就写应用层大段的采集代码,结果跑不通后又不知道从哪查起。我自己总结了一套固定顺序,遇到问题就按这个查,基本不走弯路:

  1. 先查硬件连接。对照 BCM 编号,用万用表或者逻辑分析仪确认引脚有没有接反。
  2. 再查内核节点。/dev/spidev*存在、权限可写。
  3. 然后用spidev_test做最小读写,发送一个已知字节,看返回是否可预期。
  4. 上逻辑分析仪,抓片选、时钟、数据,对照芯片手册时序图。
  5. 等波形完全符合预期了,再上真正业务代码,比如连续采样、DMA 传输。

这套顺序能把问题域从硬件到软件逐步缩小。尤其是“用时序图校准波形”这步,看起来最麻烦,其实是最快的一步。你在波形上看到的每一位移动,比在 Python 里反复打印几千个错误的十六进制数要直观得多。

6.2 脚本化你的测试流程,顺手记录日志

调试不能靠“试一次,看一眼”,那样效率太低了。我这次直接写了一个小的 Python 测试脚本,把所有模式和速率组合跑一遍,并把每次结果写进 CSV:

import spidev import time import csv results = [] for mode in range(4): for speed in [100000, 250000, 500000, 1000000]: spi = spidev.SpiDev() try: spi.open(0, 0) spi.mode = mode spi.max_speed_hz = speed resp = spi.xfer2([0x00, 0x00]) results.append((mode, speed, [hex(x) for x in resp])) except Exception as e: results.append((mode, speed, str(e))) finally: spi.close() time.sleep(0.1) with open("spi_test_results.csv", "w") as f: writer = csv.writer(f) writer.writerow(["mode", "speed", "response"]) writer.writerows(results)

这个 csv 文件放到工程目录里,之后就算隔很久再回过头来,也能一眼看出当时是哪个组合能通、哪个组合通不了。实际调试中,我常常发现某个模式下前 3 个字节是对的,第 4 个字节开始错,这种规律用脚本扫出来以后再回去查协议,比纯手动一次一次试要高效得多。

6.3 虚拟示波器与逻辑分析仪的局限性

最后提醒一句,逻辑分析仪能看时序关系,但看不了模拟电压质量。如果你怀疑 MISO 电平不够、上升沿太缓或者振铃幅度过大,逻辑分析仪会呈现出一个看似正确的波形,因为它只判断超过阈值就算逻辑 1。这种情况下就得用示波器看实际电压,看看是不是有严重的过冲欠冲。

我在调试中遇到过 MISO 线上毛刺频繁,逻辑分析仪判定完全正常,但采集数据偶发错误的情况。最后用示波器一量,发现 MISO 线太长,又与 MOSI 线靠太近,串扰导致信号在采样点附近不断跳变。解决办法就是缩短线缆、把 MISO 和 MOSI 分开走,必要时在从设备侧加一个小滤波电容。这个教训说明:SPI 调试的工具不止软件,硬件层面的信号完整性意识更重要。

我个人现在做树莓派 SPI 项目的固定工具箱是:一块支持 8 路 24 MHz 采样的逻辑分析仪、一根尽量短的双排杜邦线缆、一个可调直流电源。软件上则是spidev_test+ Python 脚本 + CSV 记录,三者配合,从“现象反常”到“定位根因”通常不超过半天。希望这篇分享能让你少走一点我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询