1. 先搞清楚主机驱动在SPI体系里坐哪个位置
写了这么多年板级驱动,我发现一个规律:凡是 SPI 出问题,最后十有八九不是"协议写错了",而是"位置搞混了"——有人把本该写在从设备驱动里的时序塞进主机驱动,也有人反过来指望主机驱动去管某个传感器的寄存器。所以我讲 SPI 主机驱动,第一件事不是贴代码,而是先把边界画出来。
Linux 下的 SPI 主机驱动,本质是一块 SPI 控制器的"翻译器":把内核上层通用的"发一个 spi_message"这种语义,翻译成"往某个基地址的寄存器里写分频值、置位传输使能、拉低片选、等中断、搬 FIFO"这一串硬件动作。它不关心对面挂的是 Flash、屏幕还是编码器,也不关心寄存器地址是 0x10 还是 0x2A,这些属于从设备驱动(协议驱动)的活儿。
适合谁看:手里有一块新板子、SPI 控制器在内核里还没有驱动(或者有但不完整)的底层工程师;需要判断"是我的驱动问题还是硬件问题"的调试者;以及想弄明白spi_sync到底干了什么、为什么偶发超时的应用层开发者。后面的内容默认你已经会用设备树、能看懂内核代码、知道 MMIO 是什么,但即便你只是刚接触这块,也能顺着读下来。
1.1 一根SCLK背后:SPI的物理约束决定了驱动怎么写
SPI 只有四根线:SCLK、MOSI、MISO、CS。简单得让人轻视,但正是这份简单,把所有的复杂度都推给了软件。
先说极性(CPOL)和相位(CPHA)。CPOL 决定空闲时 SCLK 是高还是低,CPHA 决定数据在第一个还是第二个时钟沿采样,组合出 Mode 0~3 四种模式。这里有个关键认知:模式不是软件"约定"出来的,而是由控制器的硬件能力决定的。如果你的控制器硬件只支持 Mode 0 和 Mode 3,那么当设备树里某个从设备声明了spi-cpha要求 Mode 1,spi_setup()就会直接返回-EINVAL,连传输都进不去。所以主机驱动里那个mode_bits字段不是摆设,它是你向 core 声明的"我能干什么",写错了会连累所有从设备。
再说全双工。SPI 的移位寄存器本质是环形结构,你发一个字节,必然同时收一个字节。这意味着即使你只想读,也得发点"垃圾数据"把时钟打出来——这就是为什么很多从设备驱动的读操作里会看到一个全 0xFF 的 tx buffer。有些控制器为了省事做了半双工模式,只接三根线,这时候驱动要设置SPI_CONTROLLER_HALF_DUPLEX,并且要注意tx_buf和rx_buf不能同时有效。
最后是片选。CS 是唯一一个"可以有多个"的信号,一个控制器挂 4~8 个从设备是常态。片选怎么拉、什么时候拉低、传输之间要不要释放,这些细节直接决定了多设备场景下会不会串数据。我见过最典型的事故:两个从设备共用总线,驱动在 message 之间没正确释放片选,导致第二个设备的时钟把第一个设备的状态机打乱,现象是"单独测都正常,一起跑就随机出错"。
1.2 三层结构:core、controller、protocol 各管什么
内核的 SPI 子系统是标准的三层,理解这三层各自负责什么,比背 API 有用得多。
| 层次 | 代码位置 | 职责 | 你要不要改 |
|---|---|---|---|
| SPI core | drivers/spi/spi.c | 总线注册、设备与驱动匹配、消息队列调度、参数校验、sysfs/debugfs 节点 | 基本不改,理解它即可 |
| Controller 驱动 | drivers/spi/spi-*.c | 寄存器操作、时钟分频、FIFO/DMA 搬运、中断处理、片选控制 | 这就是本文的主角 |
| Protocol 驱动 | 各子系统目录下 | 面向具体器件的寄存器读写、时序语义 | 多数情况下你不用碰 |
core 层最值得关注的是消息队列。当你在从设备驱动里调用spi_sync(),core 并不会立刻操作硬件,而是把spi_message挂到控制器的队列上,然后唤醒一个内核线程(老版本叫kworker的 pump 线程,新版本用kthread或者直接走spi_async的快速路径)。这个设计的意义在于:把"谁先发"的仲裁权和"怎么发"的实现权分开了。core 负责排队和互斥,控制器驱动只负责执行队首那一条。
这个分层带来一个非常实用的推论:如果你在调试时发现消息根本没进到控制器的transfer_one,那问题一定在 core 层的匹配、校验或排队环节,压根不用去看寄存器。反过来,如果进了transfer_one但波形不对,那就是纯硬件层面的问题。用这个二分法,能省掉大量瞎猜的时间。
1.3 主机驱动的最小职责清单
一个能跑起来的主机驱动,必须完成下面这几件事,缺一个都会在某个环节挂掉:
- 声明能力:填好
mode_bits、bits_per_word_mask、min_speed_hz/max_speed_hz、num_chipselect,让 core 知道你的边界。 - 提供 setup:在
setup()回调里根据spi_device的mode、max_speed_hz、bits_per_word配置硬件,这是唯一能改硬件参数的时机。 - 实现传输:
transfer_one()或transfer_one_message(),二选一,前者简单后者灵活。 - 控制片选:要么用
set_cs()回调,要么声明use_gpio_descriptors让 core 用 GPIO 帮你拉。 - 管理时钟:
prepare_message/unprepare_message里开关时钟,别让总线在空闲时白烧电。
我个人的经验是:先写一个只支持 PIO 轮询、只支持 8bit、只支持 Mode 0 的最小版本,把它跑通,波形用逻辑分析仪确认无误,再往上加中断、DMA、多模式支持。上来就写全套的,出问题时你根本分不清是哪一层错的。
2. 核心数据结构:五张"身份证"和一张"运单"
内核 SPI 子系统的数据结构不多,但每个字段背后都有故事。把这几张结构体吃透,看任何一家厂商的主机驱动都能快速定位关键逻辑。
2.1 spi_controller:控制器的全部家当
struct spi_controller(4.13 之前叫spi_master,老代码里还能见到这个名字,读到别懵)是整个驱动的核心。字段很多,但真正决定行为的是这么几组:
struct spi_controller { struct device *dev; /* 控制器设备,用于日志和资源管理 */ u16 num_chipselect; /* 硬件支持多少个片选 */ u16 bus_num; /* 总线编号,-1 表示动态分配 */ u32 min_speed_hz, max_speed_hz; /* 时钟能力边界 */ u16 mode_bits; /* 支持的 CPOL/CPHA/3WIRE/LOOP 等组合 */ u32 bits_per_word_mask; /* 支持的位宽,用 SPI_BPW_MASK(n) 组合 */ /* 这是三个不同档次的传输回调,只需实现一个 */ int (*transfer)(struct spi_device *, struct spi_message *); /* 最老,已废弃 */ int (*transfer_one_message)(struct spi_controller *, struct spi_message *); /* 中等,整条消息 */ int (*transfer_one)(struct spi_controller *, struct spi_device *, struct spi_transfer *); /* 最新,单个 transfer */ void (*set_cs)(struct spi_device *, bool enable); int (*setup)(struct spi_device *); int (*prepare_message)(struct spi_controller *, struct spi_message *); int (*unprepare_message)(struct spi_controller *, struct spi_message *); ... };transfer_one是现在推荐的实现方式,因为 core 里有个默认的spi_transfer_one_message()会帮你遍历 message 里的所有 transfer、处理cs_change、更新actual_length、算延迟,你只需要管"一个 transfer 怎么搬到硬件上"。这个分工非常合理:通用的循环逻辑不用每个驱动重复实现,厂商只写自己特有的部分。
bits_per_word_mask这个字段值得单独说一下。它不是一个数字,而是位掩码,SPI_BPW_MASK(8) | SPI_BPW_MASK(16)表示支持 8 位和 16 位。如果你的控制器其实只支持 8 位,但你在 mask 里顺手加了 16,那么当某个从设备驱动传bits_per_word = 16时,core 会放行,然后你的寄存器配置就会出诡异的结果——通常是数据右移 8 位或者干脆全错。这类 bug 极难查,因为软件层面所有返回值都是 0。
2.2 spi_device 与 spi_driver:从设备侧的两个视角
spi_device描述"总线上挂了一个什么东西",由 core 在解析设备树或板级信息时创建;spi_driver描述"我这份代码能驱动什么东西",由协议驱动注册。两者靠modalias或设备树的compatible字符串匹配。
主机驱动基本不直接操作这两个结构,但有两个字段跟你有关:
spi_device->max_speed_hz:从设备声明的最高频率,最终会被min(它, controller->max_speed_hz)截断。spi_device->mode:从设备声明的模式,会在spi_setup()里被校验,不合法就直接失败。
有一类情况主机驱动必须参与:如果你用的是 GPIO 片选(cs-gpios),那spi_device->cs_gpiod才有值,spi_set_cs()会走 GPIO 路径而不是调你的set_cs()。这点很容易踩坑——有人写了set_cs()却发现根本没被调用,就是因为设备树里配了cs-gpios,core 优先用了 GPIO 方案。
2.3 spi_transfer 与 spi_message:运单和集装箱
这是传输侧的核心抽象,关系是一对多:一个 message 包含若干个 transfer,transfer 之间可以有不同的时钟、位宽、片选状态。
struct spi_transfer { const void *tx_buf; void *rx_buf; unsigned len; /* 字节数 */ u32 speed_hz; /* 0 表示沿用 spi_device 的设置 */ u8 bits_per_word; /* 0 表示沿用 spi_device 的设置 */ struct spi_delay delay; /* 老内核是 u16 delay_usecs */ bool cs_change; /* 传输后是否要翻转片选 */ u8 tx_nbits, rx_nbits; /* 1/2/4,用于 Dual/Quad SPI */ ... };len是字节数,不是传输次数,也不是字(word)数。这个看似废话,但我见过有人按"传输了 16 个 16 位字"填了len = 16,结果只发出一半数据。正确写法是len = 16 * 2 = 32。
cs_change的语义要看位置:如果它是 message 里的最后一个transfer,那表示"传输完不释放片选"(保持片选有效,等下一次操作);如果不是最后一个,表示"这个 transfer 结束后释放片选,下一个 transfer 再重新拉低"。这个语义被很多人误解,包括一些厂商驱动。设备树里对应的是spi-cs-change属性,但更常见的是协议驱动在代码里手动设。
delay字段的存在是因为很多器件需要"发完命令等一会儿再读数据",比如某些 ADC 在转换完成后才输出。这个延迟由 core 负责执行,主机驱动不用管,但你要保证transfer_one返回后不阻塞——如果你在里面自己忙等,两处延迟会叠加。
3. 从零写出一个能跑的主机驱动
理论说完了,现在进入实操。以下流程基于常见的 SoC 内置 SPI 控制器,寄存器名做了通用化处理,实际项目里对着手册替换即可。
3.1 硬件侧准备工作:时钟、复位、引脚、寄存器
动手写代码之前,先把这四件事在设备树里描述清楚,否则后面全是-EPROBE_DEFER。
时钟:SPI 控制器的输入时钟通常来自 PLL 分频,频率决定了你能生成的最低和最高 SCLK。要拿到clk_get_rate()才能算分频系数,所以驱动里第一件事就是devm_clk_get()+clk_prepare_enable()。这里有个细节:时钟频率会变,如果系统做了调频(比如为了省电降频),你缓存的clk_rate就过期了,分频算出来会偏。稳妥的做法是每次setup()时重新读一次,或者注册clk_notifier(成本高,一般用前者)。
复位:devm_reset_control_get_optional(),有就用,没有就跳过。复位要放在时钟使能之后、寄存器访问之前,否则可能读到全 0 或者总线挂死。
引脚:pinctrl状态机,默认态和空闲态。如果硬件支持,可以把 SCLK/MOSI/CS 在空闲时切成 GPIO 输出低电平,防止从设备被浮空时钟误触发。这个技巧在多从设备、低功耗场景里很有用。
寄存器:devm_platform_ioremap_resource()一把梭,比老的platform_get_resource+devm_ioremap简洁得多。
3.2 分配与注册:几个必须踩准的钩子
注册流程本身不复杂,但顺序和资源管理要小心:
static int my_spi_probe(struct platform_device *pdev) { struct spi_controller *ctlr; struct my_spi *priv; int ret; /* 用 devm 版本,省掉手动释放,5.15 之后推荐 */ ctlr = devm_spi_alloc_master(&pdev->dev, sizeof(*priv)); if (!ctlr) return -ENOMEM; priv = spi_controller_get_devdata(ctlr); priv->dev = &pdev->dev; /* 硬件资源 */ priv->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); priv->clk = devm_clk_get(&pdev->dev, "spi"); if (IS_ERR(priv->clk)) return PTR_ERR(priv->clk); ret = clk_prepare_enable(priv->clk); if (ret) return ret; /* 能力声明:这一段写错,后面全白搭 */ ctlr->bus_num = -1; /* 动态分配编号 */ ctlr->num_chipselect = 4; ctlr->mode_bits = SPI_MODE_0 | SPI_MODE_3 | SPI_CS_HIGH; ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16); ctlr->min_speed_hz = 1000; ctlr->max_speed_hz = 50000000; ctlr->flags = SPI_CONTROLLER_MUST_TX; /* 只读时也要给 tx_buf */ /* 回调 */ ctlr->setup = my_spi_setup; ctlr->transfer_one = my_spi_transfer_one; ctlr->set_cs = my_spi_set_cs; ctlr->prepare_message = my_spi_prepare_message; ctlr->unprepare_message = my_spi_unprepare_message; ctlr->dev.of_node = pdev->dev.of_node; ret = devm_spi_register_controller(&pdev->dev, ctlr); if (ret) { clk_disable_unprepare(priv->clk); return ret; } return 0; }SPI_CONTROLLER_MUST_TX这个 flag 特别值得说。它的意思是"即使只做接收,也必须有 tx_buf",core 会在校验时给只有rx_buf的 transfer 自动补一个零缓冲。如果你的控制器是纯半双工或者硬件上不支持空发,就要考虑SPI_CONTROLLER_HALF_DUPLEX。这两个 flag 用错,典型现象是"只读操作返回 EINVAL"或者"读出来的数据全是 0"。
bus_num = -1让内核动态分配总线号,好处是不会和设备树里其他控制器冲突。但如果你要用spidev在用户态按/dev/spidevB.C访问,动态编号会让设备名不可预测,CI 脚本会挂。这种情况就固定写死一个编号。
3.3 设备树怎么写才不会被core拒收
控制器节点的模板:
spi0: spi@40010000 { compatible = "vendor,spi-ctrl"; reg = <0x40010000 0x1000>; interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clk_spi>; clock-names = "spi"; resets = <&rst 12>; dmas = <&dma 5>, <&dma 6>; dma-names = "tx", "rx"; #address-cells = <1>; #size-cells = <0>; num-cs = <4>; cs-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>, <0>, <0>, <0>; status = "okay"; flash@0 { compatible = "vendor,nor-flash"; reg = <0>; spi-max-frequency = <40000000>; spi-cpol; spi-cpha; }; };几个容易翻车的点:
#address-cells = <1>和#size-cells = <0>是硬性要求。SPI 子节点的reg表示片选编号,只有一个 cell,所以 size 必须是 0。写成<0>以外的东西,子节点会解析失败,现象是"设备树里明明写了,/sys/bus/spi/devices下却没有"。
cs-gpios里用<0>占位表示"这一路用硬件片选",这比全用 GPIO 更高效,因为硬件片选可以和传输自动同步,不需要额外的 GPIO 操作延迟。
spi-max-frequency如果超过ctlr->max_speed_hz,新内核会在spi_setup()里打印警告并截断到上限;老内核可能直接返回错误。所以声明max_speed_hz时宁可保守,别虚标。
num-cs和cs-gpios长度不一致时,core 会取小的那个,剩下的片选不可用。这个行为没有明显报错,只能靠dmesg里的spi_master spi0: too many chipselects之类的信息发现。
3.4 传输回调的三种实现档次
档次一:纯轮询 PIO。适合调试初期和低速场景。核心就三步:填 FIFO、等标志位、搬 FIFO。
static int my_spi_transfer_one(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer) { struct my_spi *priv = spi_controller_get_devdata(ctlr); int i, ret; ret = my_spi_config(priv, spi, xfer); /* 配时钟、位宽、模式 */ if (ret) return ret; my_spi_set_cs(spi, true); for (i = 0; i < xfer->len; i++) { u8 tx = xfer->tx_buf ? ((u8 *)xfer->tx_buf)[i] : 0; ret = my_spi_wait_txe(priv, 1000); if (ret) goto out; writel(tx, priv->base + SPI_DATA); ret = my_spi_wait_rxne(priv, 1000); if (ret) goto out; if (xfer->rx_buf) ((u8 *)xfer->rx_buf)[i] = readl(priv->base + SPI_DATA); } out: my_spi_set_cs(spi, false); return ret; }这个版本看起来笨,但它是你验证硬件通不通的唯一可靠手段。先让它跑通,再用逻辑分析仪抓波形对齐,确认时钟频率、模式、片选时序都符合预期,然后才去优化。
档次二:中断驱动。把transfer_one改造成"启动传输 + 返回 1",由 core 等待完成信号:
static int my_spi_transfer_one(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer) { ... reinit_completion(&priv->xfer_done); priv->tx = xfer->tx_buf; priv->rx = xfer->rx_buf; priv->len = xfer->len; priv->pos = 0; my_spi_set_cs(spi, true); my_spi_enable_irq(priv); my_spi_start(priv); return 1; /* 告诉 core:我不是同步完成的,别帮我收尾 */ }返回值的语义要记牢:返回 0 表示这个 transfer 已经同步完成了,core 会继续处理下一个,并且由 core 负责在合适的时候释放片选;返回 1 表示异步,core 会等你在完成时调用spi_finalize_current_transfer(ctlr)。这个返回值写错,现象是"传输看起来成功但片选一直不释放"或者"程序卡死在 spi_sync"。
档次三:DMA。只在大块数据(超过几百字节)时才有意义。DMA 的接入点通常是can_dma()回调,core 会问你"这个 transfer 你想用 DMA 吗",你根据长度、对齐、方向判断。dmaengine的配置在probe里做一次,传输时只提交 descriptor。这块坑最多的是缓存一致性——接收缓冲在 DMA 写完之后要invalidate,否则 CPU 读到的是旧数据。用dma_alloc_coherent分配的缓冲没这个问题,但普通kmalloc的必须处理。
3.5 中断、FIFO水位与DMA的接入顺序
我的建议是严格按这个顺序推进,每一步都验证:
- 轮询 PIO,8 位,单字节收发 → 波形正确
- 轮询 PIO,多字节 → 数据正确
- 中断驱动,单字节 → 中断次数正确
- 中断 + FIFO 水位(比如半满触发)→ 减少中断数
- DMA,256 字节以上 → 吞吐提升
跳过任何一步,后面出问题都会退回到原点重来。特别是 FIFO 水位:水位设得太高,短传输永远触发不了中断;设得太低,中断风暴把 CPU 打满。常见做法是按 transfer 长度动态设水位——短传输用水位 1(每字节一次),长传输用半满。
4. 一次spi_sync到底跑了多远
很多问题追到根上,是因为不知道一次调用在各个层之间发生了什么。我把这条路径完整走一遍。
4.1 从调用到片选拉低的完整路径
spi_sync()的第一步是mutex_lock(&ctlr->bus_lock_mutex),然后调用__spi_pump_messages()。这里就开始出分支了:
- 如果控制器设置了
SPI_CONTROLLER_QUEUED(有硬件队列,比如某些高端控制器),消息会被直接塞进硬件队列,立即返回。 - 否则消息进入
ctlr->queue链表尾部,唤醒 pump 线程(新内核用kthread_queue_work)。
pump 线程醒来后,从队首取一条 message,依次调用prepare_message→ 遍历 transfer → 每个 transfer 先调spi_setup()(如果需要改参数)→set_cs(true)→transfer_one()→ 根据返回值决定是等完成还是继续 →set_cs(false)(如果 cs_change 要求)→ 全部完成后调用message->complete()。
spi_sync在把消息挂上去之后,会wait_for_completion(&done),直到 pump 线程回调 complete 才返回。所以从调用者的角度看是同步的,但实际执行是在内核线程上下文。
这个设计带来一个重要的推论:spi_sync()不能在原子上下文(中断、spinlock 内)调用,因为它会睡眠等完成。需要用spi_async(),它不阻塞,但要求你自己处理完成回调,而且同一时刻控制器只能有一条 async 消息在飞,第二次调用会返回-EBUSY。
4.2 分频参数怎么算:给出具体数字
这是主机驱动里最容易算错的地方。假设控制器输入时钟clk_rate = 100 MHz,从设备要求max_speed_hz = 8 MHz,寄存器分频字段DIV的公式是SCLK = clk_rate / (2 * (DIV + 1))(不同芯片公式不同,一定要对着手册)。
反推DIV:DIV = clk_rate / (2 * 8000000) - 1 = 100e6 / 16e6 - 1 = 6.25 - 1 = 5.25。
这里有个取舍原则:永远向下取整,让实际频率不超过请求值。取DIV = 5,实际SCLK = 100e6 / (2 * 6) = 8.33 MHz,超过 8 MHz,不行。取DIV = 6,实际= 100e6 / 14 = 7.14 MHz,低于请求值,安全。
代码里通常这么写:
static u32 my_spi_calc_div(u32 clk_rate, u32 target_hz) { u32 div; if (target_hz == 0) target_hz = clk_rate / 2; /* DIV_ROUND_UP 保证结果不会让频率超过 target */ div = DIV_ROUND_UP(clk_rate, 2 * target_hz); if (div == 0) div = 1; if (div > 256) div = 256; /* 分频器上限 */ return div - 1; /* 写进寄存器的值 */ }注意DIV_ROUND_UP和div - 1的配合:DIV_ROUND_UP(100e6, 16e6) = 7,7 - 1 = 6,实际频率 7.14 MHz,正确。如果我用DIV_ROUND_CLOSEST,得到 6,减 1 后是 5,实际 8.33 MHz 超标。超标不会立刻出错,但会让从设备的时序余量变差,高温或者电压波动时随机丢数据——这是最讨厌的一类 bug。
有些平台的做法不一样:不直接算分频,而是调clk_set_rate(clk, target_hz)让时钟树自己找最近的频点,然后读回实际值。这种方式更灵活,但要检查clk_round_rate()的返回值,因为它可能给你一个差很多的值。
4.3 片选时序与延迟参数
现代控制器的spi_controller有几个新字段:
struct spi_controller { ... struct spi_delay cs_setup; struct spi_delay cs_hold; struct spi_delay cs_inactive; ... };cs_setup:片选拉低到第一个时钟沿之间的延迟。有些器件(尤其是 Flash)要求 CS 稳定之后才能给时钟,否则第一条命令会被误判。
cs_hold:最后一个时钟沿到片选释放之间的延迟。某些器件需要时钟停止后再保持一小段时间才认命令结束。
cs_inactive:片选释放后到下一次拉低之间的最小间隔。这是给那些"释放后需要时间复位内部状态"的器件准备的。
这些参数通常在设备树里通过spi-cs-setup-ns、spi-cs-hold-ns之类的属性配置,由 core 解析后传给控制器。如果你的控制器硬件不支持这些延迟,就直接忽略,但要在dmesg里提示一下,免得用户以为是驱动没生效。
我遇到过一个真实案例:某传感器要求 CS 拉低后至少 100ns 才能给时钟,但控制器默认是 0 延迟,导致读出来的第一个字节总是错的。加了cs_setup之后问题消失。这种问题波形上看不出来(100ns 在示波器上就是一小段),只能靠手册对照。
4.4 传输长度上限与分片
spi_controller有两个字段限制单次传输:
max_transfer_size:单个 transfer 的最大字节数max_message_size:整条 message 的最大字节数
超过限制时,core 会返回-EMSGSIZE。但有些驱动偷懒不填,结果 DMA 描述符写溢出,表现为"传输最后几个字节丢失"或者"内存被踩"。建议一定要填,哪怕填一个保守值(比如 4096),让 core 帮你兜底。
对于需要更长传输的场景(比如烧写 Flash 几 MB),正确做法是在协议驱动里手动分段,每段一条 message,而不是指望主机驱动一次搞定。分段还能带来额外好处:段之间可以插入进度回调、检查点、错误重试。
5. 常见问题速查与避坑实录
5.1 症状对照表
| 症状 | 可能原因 | 排查入口 |
|---|---|---|
probe 返回-EPROBE_DEFER | 时钟/复位/pinctrl 还没准备好 | 看dmesg里的依赖名,确认 provider 的 probe 顺序 |
spi_setup返回-EINVAL | mode_bits没声明对应模式,或max_speed_hz超限 | 打印spi->mode和ctlr->mode_bits对比 |
| 传输超时 | 片选没拉、时钟没开、FIFO 标志等错位 | 先抓波形,确认 SCLK 有没有输出 |
| MISO 全是 0xFF | 从设备没驱动、片选错路、MISO 引脚复用了别的功能 | 万用表量 MISO 空闲电平,硬件片选量 CS 电平 |
| 偶发数据错位 | 分频算错导致频率超标,或模式(CPOL/CPHA)不匹配 | 逻辑分析仪看采样沿,对照器件手册时序图 |
| 第一次传输成功,后续全失败 | 片选没释放,或者cs_change语义用反 | 抓 CS 波形,看两次传输之间是否拉高 |
| DMA 传输尾部数据丢失 | 缓存没 invalidate,或者max_transfer_size没填 | 用 coherent 缓冲做对比测试 |
| 中断风暴,CPU 100% | FIFO 水位设太低,或者中断标志没清干净 | 统计单位时间中断次数,检查状态寄存器回读 |
5.2 三个排障过程
案例一:SPI NOR Flash 读 ID 全 FF。某开发板新加了 SPI Flash,驱动 probe 成功,但读 JEDEC ID 返回0xFFFFFF。第一步先在transfer_one里打印 tx/rx 的前几个字节,发现 tx 是对的(0x9F),rx 全是 FF。抓波形:SCLK 有,MOSI 有,CS 有,MISO 一直高。换到另一块板子,同样现象。最后量硬件,发现 MISO 的引脚复用寄存器没配对,被配成了别的功能,实际上 MISO 根本没接到控制器。这个案例的教训是:软件层面所有返回值都正常时,问题大概率在 pinctrl 或硬件连接,别在内核代码里死磕。
案例二:屏幕花屏,偶发但频繁。SPI 接口的 LCD,跑静态画面正常,一刷动态内容就出现随机色块。先怀疑 DMA,改成 PIO 后问题依旧,排除。再怀疑频率,把spi-max-frequency从 48 MHz 降到 24 MHz,问题消失。回头算分频:输入时钟 200 MHz,写的分频值让实际频率变成了 50 MHz,超过声明的 48 MHz。虽然只超了 4%,但屏幕的时序余量不够,长时间跑就出错。改成向下取整后,实际 47.6 MHz,稳定。这个坑告诉我:频率永远向下取,不要用四舍五入。
案例三:双设备共总线,单独测都正常,一起跑就错。一个控制器挂了两路 SPI 设备(一个传感器、一个屏幕),分别测试都 OK,同时初始化就随机失败。加日志发现spi_sync偶尔返回-EBUSY。查代码,发现两个设备的驱动都在自己的工作队列里并发调spi_sync,而 core 的bus_lock是 per-controller 的互斥锁,本应串行化,但其中一个驱动自己用了spi_async并且没等完成就返回,导致总线状态混乱。改成所有传输都走spi_sync,问题消失。
5.3 我个人的避坑清单
- 先抓波形再改代码。我见过太多人对着代码改三天,最后发现是硬件飞线接错了。逻辑分析仪(或者便宜的 USB 逻辑分析仪)是这块最值钱的工具。
mode_bits只声明真正支持的。多写一位,就是给未来埋一个随机故障。- 在
probe里就把max_speed_hz和min_speed_hz填对,别指望运行时改。 - 分段测试:先轮询、再中断、再 DMA,每步都用固定测试向量验证。
- 给
transfer_one加限时等待,别用死循环。总线挂了的时候,while (!(readl(...) & FLAG))会让整个系统卡死,连dmesg都看不到。 - 注意内核版本差异:
delay_usecs在 5.x 之后换成了struct spi_delay,spi_alloc_master在 5.15 之后推荐用devm_spi_alloc_master。抄老代码之前先看版本。 spidev只用于调试,量产产品里别开,它会把总线的控制权直接交给用户态,任何一个进程都能把设备打乱。
6. 性能与进阶:把总线用满又不翻车
到这一步,驱动已经能稳定工作了。剩下的是怎么跑得更快、更稳。
6.1 减少消息数量的思路
SPI 的瓶颈往往不在时钟频率,而在消息之间的间隙。每次spi_sync都涉及一次加锁、一次队列操作、一次线程唤醒、一次片选翻转,这些开销加起来可能有几十微秒。如果你的应用需要高频小包传输(比如每 100 微秒读一次传感器),这些开销会吃掉大部分带宽。
优化思路有三条:
一是合并 transfer。把"发命令 + 读数据"两条 message 合并成一条 message 里的两个 transfer,利用cs_change = 0保持片选不释放(如果是最后一个 transfer 则设cs_change = 1表示保持)。这样中间没有锁开销,片选也不用翻转。
二是用spi_async+ 完成回调,在回调里立刻提交下一条,形成流水线。但要注意:spi_async在消息未完成时返回-EBUSY,所以你得自己判断时机,或者用spi_controller的 queued 模式。
三是批量化。如果协议允许,把多次小读取攒成一次大读取。这需要协议驱动配合,但效果最明显——从 100 次 32 字节变成 1 次 3200 字节,开销降低两个数量级。
6.2 用户态调试通道的正确用法
spidev是内核提供的通用用户态 SPI 接口,设备节点是/dev/spidevB.C(B 是总线号,C 是片选号)。用spidev_test工具可以在不写内核驱动的情况下验证硬件:
# 自发自收测试(需要硬件把 MOSI 和 MISO 短接) spidev_test -D /dev/spidev0.0 -s 1000000 -m 0 -v # 发送特定数据 spidev_test -D /dev/spidev0.0 -p "9F000000" -v它的价值在于把软件变量降到最低:没有你的驱动、没有协议层,直接对着硬件发数据。如果spidev_test都不通,那就不是驱动的问题。
但要注意,spidev会接管片选和总线锁,不能和内核里的协议驱动同时用同一个片选。设备树里要专门留一个"测试用"的从节点,compatible = "rohm,dh2228fv"或者用spidev的通用 compatible,并且只在调试版本里编译进去。
6.3 多从设备并发与锁的粒度
前面提到 core 的bus_lock是 per-controller 的,一条总线同一时刻只能有一条消息在飞。这个设计在多数场景下够用,但有两种情况会让它变成瓶颈:
一是不同片选的设备互相等待。设备 A 在传大块数据,设备 B 急着读一个状态寄存器,B 必须等 A 传完。这在实时性要求高的系统里会造成抖动。
二是中断上下文里的紧急传输。如果某个设备的中断处理里需要立刻读状态(否则会丢数据),而总线正被占用,那就只能等,等久了就丢。
应对办法:把紧急设备的传输拆成小块,穿插在长传输之间;或者用硬件支持的多通道控制器(有些控制器有独立的 TX/RX 通道和独立片选,可以真并行)。软件层面能做的有限,因为 SPI 物理上就是一条共享时钟线,真要并行只能加控制器。
一个实用技巧是给不同从设备设置不同的优先级:core 本身没有优先级概念,但你可以让高优先级设备用spi_async抢占(在低优先级消息的 transfer 之间插入检查点),代价是实现复杂度上升。多数项目里,把长传输拆小、把总线占用时间控制在毫秒级以内,就足够应付了。
最后分享一点实际体会。SPI 主机驱动这活儿,代码量不大(一个完整的控制器驱动通常 800~1500 行),但调试周期往往很长,因为问题是"偶发"的、是"波形上看着对但数据错"的。我的习惯是:每写一个新控制器,先在transfer_one里加一段print_hex_dump,把每一次收发的原始数据打出来,配合逻辑分析仪对照。等驱动稳定了,这段代码用pr_debug包起来,留着当保险。真正节省时间的从来不是"一次写对",而是"出问题时能在三分钟内定位到是哪一层"。这个习惯我保持了很多年,救过我不少次。