尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux SPI主机驱动开发与调试:架构、设备树和spi_sync路径
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 1spi_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 coredrivers/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_controller4.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-gpioscore 优先用了 GPIO 方案。2.3 spi_transfer 与 spi_message运单和集装箱这是传输侧的核心抽象关系是一对多一个 message 包含若干个 transfertransfer 之间可以有不同的时钟、位宽、片选状态。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_resourcedevm_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_bufcore 会在校验时给只有rx_buf的 transfer 自动补一个零缓冲。如果你的控制器是纯半双工或者硬件上不支持空发就要考虑SPI_CONTROLLER_HALF_DUPLEX。这两个 flag 用错典型现象是只读操作返回 EINVAL或者读出来的数据全是 0。bus_num -1让内核动态分配总线号好处是不会和设备树里其他控制器冲突。但如果你要用spidev在用户态按/dev/spidevB.C访问动态编号会让设备名不可预测CI 脚本会挂。这种情况就固定写死一个编号。3.3 设备树怎么写才不会被core拒收控制器节点的模板spi0: spi40010000 { 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; flash0 { 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的接入顺序我的建议是严格按这个顺序推进每一步都验证轮询 PIO8 位单字节收发 → 波形正确轮询 PIO多字节 → 数据正确中断驱动单字节 → 中断次数正确中断 FIFO 水位比如半满触发→ 减少中断数DMA256 字节以上 → 吞吐提升跳过任何一步后面出问题都会退回到原点重来。特别是 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))不同芯片公式不同一定要对着手册。反推DIVDIV 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) 77 - 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返回-EINVALmode_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 是对的0x9Frx 全是 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_delayspi_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.CB 是总线号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包起来留着当保险。真正节省时间的从来不是一次写对而是出问题时能在三分钟内定位到是哪一层。这个习惯我保持了很多年救过我不少次。
RELATED

相关推荐

多人游戏可破坏环境落地:服务器权威、状态机与破坏预算

多人游戏可破坏环境落地:服务器权威、状态机与破坏预算

1. 拆解之前:破坏系统到底在解决什么问题《The Finals》的破坏系统是我这两年反复拆得最多的一套东西。它最容易被误读的地方在于:很多人第一次看到大楼被整层削掉、地板塌下去、掩体被轰成碎渣,第一反应是"这游戏的物理引擎真猛"。…

📅 2026/10/26 21:55:18
MCP协议详解:AI服务间语义握手与标准化协作

MCP协议详解:AI服务间语义握手与标准化协作

1. 这不是又一个“AI Agent框架”科普——MCP到底在解决什么真问题?最近两周,我在三个不同行业的客户现场都遇到了同一个词:MCP。不是某个新出的芯片型号,也不是某家创业公司的缩写,而是一个正在 quietly reshape&…

📅 2026/10/26 21:55:18
LLM面试8问:从Token切分到RAG幻觉与Agent容错全解析

LLM面试8问:从Token切分到RAG幻觉与Agent容错全解析

说实话,我这两年面试过不少人,也在技术社区里帮很多朋友模拟过LLM岗位的面试。有个现象特别典型:简历上写着“精通大模型应用开发”,结果问到大模型的基础原理、Token到底怎么切分、RAG为什么会有幻觉,人就卡住了。还有…

📅 2026/10/26 21:50:14
MORE NEWS

更多资讯

📰

南瑞IEC 101规约报文解析与调试实战:从RTU101.rar到Python解析工具

简介:这份资源面向电力自动化、远动通信方向的开发者与运维人员,聚焦IEC 101规约(IEC 60870-5系列)在RTU与主站通信中的报文解析与实现,尤其涉及南瑞设备的101规约应用。包内共2个文件,以1个cpp源码和1个tx…

📰

图解AI应用架构设计:从分层到Agent、LLM治理与MCP协议实战

1. 从一张架构图说起:AI应用到底该怎么搭很多人第一次接触AI应用开发,脑子里冒出来的第一个念头是"调个API不就完了"。我刚开始也是这么想的,直到真正把一个AI应用从demo推到线上,才发现事情远没有那么简单。一个能跑通…

📰

PHP手机端商城源码搭建与二次开发避坑指南

简介:这份PHP手机端商城源码面向希望搭建H5商城系统的开发者与学习者,整合了抖音商城小店相关功能模块,适合具备PHP基础、想研究移动端电商实现思路的中级开发者参考。压缩包共346个文件,约13.64MB,以52个php业务文件为…

📰

t3code:TypeScript+Tailwind+Turborepo的跨端CLI脚手架范式

1. “t3code”不是工具名,而是开发者圈里一个正在成型的命名惯性最近在几个前端工程群、Electron技术交流频道和iOS开发小圈子刷到“t3code”这个词的频率明显变高——它不像npm包名那样带版本号或作者前缀,也不像GitHub仓库名那样有明确归属&#xff0c…

📰

麻雀搜索算法优化LSTM时间序列预测:MATLAB源码复现与调参避坑指南

简介:这份资源面向时间序列预测方向的研究者与工程师,提供一套在MATLAB环境下将麻雀搜索算法(SSA)与LSTM结合的完整实现,用于股票价格、电力消耗、天气预报等序列数据的预测建模。核心思路是用SSA优化LSTM的权重参数&a…

📰

2025年客户端技术趋势:端侧AI与跨平台革新

1. 2025年客户端技术全景扫描站在2025年的技术节点回望,客户端开发领域正经历着从"跨平台适配"到"智能体验重构"的范式转移。过去一年最显著的变化是端侧AI能力从实验室走向规模化商用,这直接改写了移动应用的交互逻辑与技术栈选型策…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬