Linux SPI驱动数据收发全解析:spi_message、片选与时序排查
发布时间:2026/10/26 6:34:06 锦皓数字建站

调试一颗SPI接口的传感器时最让人抓狂的场景往往不是波形完全没动静而是片选拉低了、时钟也出来了代码里spi_read()返回 0 表示成功缓冲区里却清一色是 0xFF或者单字节读 ID 一切正常一旦连续读几十字节就后半段全是脏数据。前面折腾寄存器手册、上电时序、Mode 配错最后发现问题出在对spi_message和spi_transfer的理解上——消息被拆成了几段cs_change位置放错片选在中间被抬高了一次从机状态机直接复位。这篇内容围绕 Linux 下 SPI 设备驱动的数据收发处理流程把这条链路上真正起作用的结构体和函数逐个拆开讲。包括spi_device、spi_driver、spi_controller这三者怎么凑到一起spi_message和spi_transfer怎么组织一次完整的事务spi_sync与spi_async的调用链差在哪里控制器侧的transfer_one_message与set_cs又是怎么把数据真正推到时序线上的。适合已经能写字符设备、但一遇到 SPI 时序问题就卡住的驱动开发者也适合想把现有驱动从能跑改到稳跑的老手。下面这些内容全部基于内核 SPI 子系统的通用实现逻辑不同版本在字段命名上有差异比如老的spi_master后来并入spi_controller我会在关键处点出来。1. 从设备树节点到 probe 函数spi_device 与 spi_driver 的匹配链路很多人写 SPI 驱动是照着模板抄的of_match_table填了、probe写了、spi_setup()调了设备能起来但问到spi_device里的max_speed_hz到底是谁填的、什么时候填的就说不清了。而恰恰是这几个字段决定了后面每一次传输的时钟频率、字长和片选行为配错了就是上面那种读回全 FF的经典故障。1.1 spi_device 不是驱动 new 出来的是总线枚举出来的这一点必须先掰清楚。你写的驱动里没有kzalloc(sizeof(struct spi_device))spi_device是 SPI 总线在枚举阶段创建的驱动只是认领其中一个。整条链路大致是设备树里有一个挂在 SPI 控制器下面的子节点带compatible、reg、spi-max-frequency等属性控制器驱动在probe里注册控制器devm_spi_register_controller内核遍历该控制器下的子节点每个子节点对应调用一次spi_alloc_device()分配spi_device再由of_spi_parse_dt()或spi_device_parse_dt这类平台相关的解析函数把 DTS 属性灌进去接着spi_add_device()把这个spi_device挂到总线设备链表上注册到设备模型总线开始匹配找到spi_driver调用你的probe。所以probe被调用的时候spi-max_speed_hz、spi-mode、spi-chip_select、spi-bits_per_word这些字段已经是从设备树里解析好的初值。你的probe里再调spi_setup(spi)作用是把驱动想用的配置和控制器能力做一次协商和下发而不是从零开始填。这里有个实际影响如果你在probe里手改spi-mode但忘了重新调spi_setup()控制器寄存器还是旧配置波形不会变。反过来如果 DTS 里根本没有spi-max-frequency解析出来的max_speed_hz可能是 0很多控制器会退化成默认频率常见是几百 kHz 到 1 MHz这时候你会发现明明写了 10M示波器上只有 500k根子不在驱动代码里。1.2 关键字段逐个说清楚它们的生效位置spi_device字段不少但真正影响数据收发的就这几个我把它们和在哪一层生效对应起来字段来源生效位置配错的典型表现max_speed_hzDTSspi-max-frequency驱动可下调spi_transfer.speed_hz为空时用它频率过高导致采样错位读回数据高位错乱modeDTSspi-cpol/spi-cpha或spi-tx/rx-*spi_setup()下发到控制器CPOL/CPHA 反了数据整体偏移一位bits_per_wordDTS 或驱动设置每次 transfer 继承8/16 位混用字节序错乱chip_selectDTSreg控制器set_cs使用片选错位多个从机同时被选中cs_gpiodDTScs-gpios控制器用 GPIO 做片选时GPIO 方向或电平反了从机永远不被选中irqDTSinterrupts驱动request_irq使用中断触发类型不对读数据永远等不到mode这个字段值得单独说一句。它的低两位是 CPOL 和 CPHA高位还有SPI_CS_HIGH、SPI_LSB_FIRST、SPI_3WIRE、SPI_LOOP、SPI_NO_CS、SPI_READY这些标志位。很多人只关心 CPOL/CPHA忽略了SPI_CS_HIGH——如果你的从机片选是高电平有效DTS 里没写spi-cs-high那控制器会一直按低有效处理结果就是从机上电后始终处于选中态你发什么它都在听但片选松开的动作永远不发生从机内部状态机就不会复位。这类问题在单次读 ID 时可能碰巧能过连续读写时必然出错。1.3 spi_driver 的两条匹配路径OF 匹配与 modalias 匹配spi_driver结构体本身很简单核心就三块struct device_driver driver内含of_match_table、name、pm回调、const struct spi_device_id *id_table、probe/remove。匹配走的是 SPI 总线的match函数逻辑顺序是先看of_driver_match_device()也就是of_match_table里的compatible和设备树节点比对如果不匹配再看spi_match_device()用id_table里的名字和spi-modalias比对两条都不中设备就挂着没人管/sys/bus/spi/devices/下能看到它但driver链接是空的。提示modalias一般由compatible字符串转换而来斜杠、逗号之类的字符会被替换所以只写of_match_table通常也能匹配上但在某些用spi_board_info静态注册设备的平台上modalias来自spi_board_info.modalias这时候id_table就是必须的。两种都写是最稳的做法。probe的返回值也有讲究。返回 0 表示接管成功返回-ENODEV或-ENXIO表示这个设备我不认识总线会继续尝试其他驱动在有多驱动竞争同一 compatible 时有用返回其他负值则是硬失败设备进入probe failed状态不会再重试。我见过把-EPROBE_DEFER写成-EAGAIN的结果电源管理芯片还没就绪SPI 设备直接放弃注册后续永远起不来。2. spi_message 与 spi_transfer一次事务到底被拆成了什么如果说spi_device是跟谁说话那spi_message就是这一趟要说几句、每句多长。这是整个 SPI 驱动里最需要动脑子设计的地方也是最容易埋坑的地方。因为 SPI 是全双工但半双工时序的总线一次片选拉低期间可以连续发多段数据、中间夹延时、甚至在中途抬片选这些复杂行为全靠spi_message里挂的spi_transfer链表来描述。2.1 spi_transfer 的字段决定了每一段的物理形态一个spi_transfer描述一段连续传输核心字段tx_buf/rx_buf发送和接收缓冲区指针可以为 NULL。两个都为 NULL 就是纯延时配合delay用。len这一段传输的字节数。speed_hz本段专用频率为 0 则继承spi_device.max_speed_hz。bits_per_word本段专用字长为 0 则继承。delay这一段结束后、下一段开始前的延时结构体spi_delay支持微秒和纳秒两种单位。cs_change这一段结束后是否改变片选状态语义在不同上下文下不一样后面细讲。tx_nbits/rx_nbits单线、双线、四线、八线模式用于 QSPI/OctalSPI。tx_dma/rx_dmaDMA 地址仅当is_dma_mapped为真时有效老式用法新代码基本不用。这里最容易搞错的是delay和cs_change的组合语义。举个真实场景读一个 SPI 接口的 ADC协议要求发命令字 → 等 10 微秒转换 → 读结果。正确做法是三段 transfer第一段tx_buf cmd, len 1发命令第二段tx_buf NULL, rx_buf NULL, len 0, delay 10us纯延时第三段rx_buf buf, len 2读结果。如果图省事把延时塞在第二段的delay上然后直接读你会发现时钟线在延时期间的状态不符合从机要求——因为延时发生在段与段之间片选保持低电平但时钟停在最后一个边沿某些从机会把这当成一次额外的时钟跳变。这种情况在手册里通常写成CS must remain low during conversion但没告诉你 SCLK 要保持什么电平只能靠示波器验证。2.2 栈上分配 message 是常见做法但异步场景下是致命错误标准写法是这样的struct spi_transfer xfer { .tx_buf tx, .rx_buf rx, .len len, }; struct spi_message msg; spi_message_init(msg); spi_message_add_tail(xfer, msg); ret spi_sync(spi, msg);spi_message_init()做的是INIT_LIST_HEAD(msg-transfers)加上若干字段清零spi_message_add_tail()把 transfer 挂到链表尾部并累加frame_length。这两个函数本身没有内存分配所以把 message 和 transfer 放在栈上是完全合法且最常见的做法——前提是用spi_sync。换成spi_async就完全不一样了。spi_async在把消息挂进控制器的队列后就返回了真正的传输是异步完成的。如果你在函数里定义栈上的spi_message函数一返回栈帧被覆盖控制器后面再去访问msg-transfers链表读到的就是垃圾指针。表现出来往往是随机的内核 oops而且现场很难复现因为取决于栈被覆写的时机。正确做法是用spi_async时把spi_message和spi_transfer一起嵌进驱动的私有结构体用kzalloc分配或作为设备结构体成员并在complete回调里统一释放。这个坑我在早期项目里踩过一次现象是高负载下偶发 panic查了两天才定位到。2.3 什么时候必须拆成多段什么时候一段就够判断标准其实只有一条从机的协议是否要求在某段数据之间插入片选变化或延时空档。不需要就合并成一段需要就拆。合并的好处很实际减少 message 内部循环次数、减少控制器与驱动的交互开销、对 DMA 更友好长连续缓冲区比多段短缓冲区更容易映射成大块。有些控制器对单个 transfer 有长度上限controller-max_transfer_size超过需要驱动自己拆但这个是控制器能力限制和协议要求是两回事。需要拆的典型情况命令与数据之间有转换时间要求如某些温湿度传感器读操作要求先写地址、抬片选、再拉低片选读数据部分 EEPROM 的时序需要在同一片选周期内切换频率speed_hz是 per-transfer 的整段 message 共享频率的话就不需要拆全双工通信里发送和接收长度不对称且硬件 FIFO 深度有限。spi_write_then_read()这个辅助函数就是两段合并的封装它内部构造两段 transfer写 读中间不抬片选适合绝大多数寄存器地址 读数据的场景。但要注意它有栈上分配内部 buffer 的行为传输长度受SPI_BUFSIZ限制通常是 32 字节左右超过就得自己拼 message。3. spi_sync 与 spi_async调用链一同步一异步语义差别很大很多驱动里spi_sync和spi_async是混着用的初始化读 ID 用spi_sync中断里读数据用spi_async。但两者的调用链和控制流完全不同混用不当就会出问题——最常见的是在原子上下文里调spi_sync导致睡眠告警或者spi_async的返回值被误当成传输完成。3.1 spi_sync 的执行路径与返回值语义spi_sync(spi, message)内部大致是加总线锁spi_bus_lock保证同一时刻一个控制器上只有一个 message 在处理把 message 挂进controller-queue调用控制器的transfer老接口或transfer_one_message新接口启动传输用completion等待传输结束wait_for_completion唤醒后返回message-status。关键点在于spi_sync是可能睡眠的。它要等 completion要持有可睡眠的锁所以绝对不能在中断上下文、软中断、或者持有自旋锁的区域里调用。真在中断里需要发数据只能用spi_async或者把工作推给 workqueue/线程化中断。返回值语义也要注意spi_sync返回 0 只代表消息走完了流程没有报错不代表从机给出了有效应答。从机不回数据、总线被拉死、MISO 悬空spi_sync照样返回 0只有超时或控制器报错才会返回负值。所以判断通信是否成功必须校验接收缓冲区内容比如读 ID 寄存器比对预期值不能只看返回值。message-actual_length是另一个有用的字段它记录了实际传输的字节数。正常情况下等于frame_length但如果控制器中途出错或被信号打断actual_length会比预期小这时候即使status是 0也要当作部分失败处理。3.2 spi_async 的回调上下文与资源释放时机spi_async(spi, message)的返回值含义和spi_sync完全不同返回 0消息已成功入队传输会在稍后发生返回负值入队失败消息根本没发出去complete回调不会被调用。入队成功后控制器完成传输时会调用message-complete(message-context)。这个回调可能在中断上下文里执行所以里面不能睡眠、不能调spi_sync、不能kfree带锁的资源用kfree本身可以但不能在里面等待。回调里通常只做三件事置完成标志、唤醒等待队列、把数据挂到环形缓冲区。资源释放的时机是另一个高频错误点。很多人写成ret spi_async(spi, msg); if (ret 0) kfree(msg.transfers); /* 错传输还没开始 */正确的做法是让complete回调负责释放或者用一个引用计数把驱动持有和控制器持有分开。更省事的做法是把 message 和 transfer 内嵌在设备私有结构体里用一把自旋锁保护当前是否有传输在进行的状态而不是每次动态分配。注意spi_async不保证消息按调用顺序执行虽然多数控制器内部是 FIFO 队列但驱动层不该依赖这个假设。如果协议有严格顺序要求比如先配置寄存器再读数据要么用spi_sync串起来要么自己在驱动层维护队列等上一个回调完成再发下一个。3.3 transfer_one_message 与 transfer_one控制器侧的两套接口写设备驱动的人一般不用直接碰这两个但理解它们有助于判断问题出在驱动还是控制器。spi_controller里有两个回调transfer_one_message(ctlr, msg)控制器负责整条 message 的处理自己遍历 transfer 链表处理片选、延时、频率切换全部完成后调spi_finalize_current_message(ctlr)。返回 0 表示已接管稍后完成。transfer_one(ctlr, msg, xfer)控制器只处理一段transfer返回 1 表示这段已完成且整条 message 也完成返回 0 表示这段已启动但异步完成返回负值表示出错。由 SPI 核心的spi_transfer_one_message()负责遍历链表、调set_cs、调spi_transfer_delay_exec。现代驱动大多实现transfer_one因为核心帮你处理了链表遍历和延时控制器只需要关心怎么把一段 buffer 变成时序。但这也意味着延时和片选的语义由核心统一解释你在设备驱动里写的delay和cs_change最终是被spi_transfer_delay_exec()和spi_set_cs()消费的。4. 片选与延时被低估的时序细节全在 spi_controller 这一侧数据收发出问题八九成的根因不在数据缓冲区而在片选和延时。这两个东西在设备驱动代码里通常只体现为一个cs_change标志和一个delay字段但它们在控制器侧的执行方式直接决定了波形长什么样。4.1 set_cs 与 cs_gpiod片选到底是硬件还是 GPIOspi_controller有两种片选实现方式硬件片选控制器外设自带 CS 引脚驱动实现set_cs(ctlr, spi, enable)回调直接写寄存器。这种方式时序最准片选动作和时钟边沿的间隔由硬件保证。GPIO 片选DTS 里写cs-gpios核心把 GPIO 描述符填进spi-cs_gpiod由核心的spi_set_cs()统一操作 GPIO。这种方式灵活但时序精度差——GPIO 操作要经过几层函数调用片选到第一个时钟边沿的建立时间可能达到微秒级。对于高速 SPI比如 20 MHz 以上GPIO 片选经常导致第一个字节采样错误因为片选建立时间不够。解决方式有两种一是改用控制器硬件片选二是在 DTS 里配置cs-setup-us对应spi-cs_setup来插入建立延时代价是降低吞吐。还有一个容易忽略的字段cs_inactive。它定义片选无效时的电平默认是高电平对应低有效片选。如果从机是高有效片选除了SPI_CS_HIGH标志还要确认cs_inactive的值和硬件实际电平匹配否则会出现片选看起来在动但逻辑反了的情况。4.2 delay 的两种单位与 spi_transfer_delay_exec 的执行位置spi_transfer.delay是struct spi_delay类型里面有一个value和一个unit单位可以是SPI_DELAY_UNIT_USECS或SPI_DELAY_UNIT_NSECS。早期内核用的是delay_usecs纯微秒新代码建议用delay结构体因为纳秒级延时在高速时序里是刚需。执行位置在spi_transfer_one_message()的循环里一段 transfer 交给控制器完成或启动后核心调用spi_transfer_delay_exec(xfer)执行延时然后根据cs_change决定是否调spi_set_cs()。这里的执行顺序是先延时后处理片选。所以如果你写延时 10 微秒后抬片选写法是delay 10uscs_change 1而不是反过来。理解这一点很多片选提前松开的诡异现象就解释得通了。纳秒级延时的实现方式取决于平台有的用ndelay()有的用控制器自带的延时计数器更准有的干脆退化成udelay()不准。如果协议要求严格的纳秒级间隔比如某些高速 ADC 的转换触发最好确认控制器是否支持硬件延时否则只能降频换取时序余量。4.3 cs_change 的三层语义别只记一种cs_change的含义在不同位置完全不同这是 SPI 子系统里最容易误用的字段场景cs_change 语义一段 transfer 之后后面还有别的 transfer抬片选、再拉低制造一个片选脉冲最后一段 transfer 之后传输结束后保持片选有效不抬只有一段 transfer 的 message同上传输结束后保持片选也就是说cs_change 1在中间位置是切换在末尾位置是保持。很多人以为它统一是切换片选结果在单段 message 上设了cs_change 1期望传输完抬片选实际却是一直保持低电平从机永远不释放总线下一次通信直接冲突。这个设计的历史原因是为了支持多次 message 共享一个片选周期的场景。要让片选在末尾抬起正确做法是cs_change 0默认值。4.4 频率切换的实际代价speed_hz是 per-transfer 的看起来可以随意在 message 内部切频率。但实际执行时控制器每次切换频率都要重算分频寄存器某些控制器还会有切换后需要等待若干个时钟周期稳定的隐性要求。如果 message 里塞了七八段不同频率的 transfer实际吞吐可能比单一频率还低。我的经验是除非协议强制要求比如配置阶段用低频、数据传输用高频否则整条 message 用统一频率靠spi_device.max_speed_hz一次性设定。真要分段改频率把不同频率的部分拆成独立 message中间用spi_sync串起来代码更清晰调试也更容易。5. 排查链路实录从读回全 FF到定位到具体字段理论讲完说点实际的。下面这条排查链路是我在一个 SPI 传感器项目里真实走过的顺序基本可以复用到大多数通信不通的场景。5.1 第一步永远是确认物理层而不是改代码拿到读回全 FF的现象先别急着怀疑驱动。全 FF 意味着 MISO 一直保持高电平可能的原因有从机没上电、从机没被片选选中、MISO 线没接、从机处于复位状态、从机根本没收到有效命令。用逻辑分析仪抓四根线CS、SCLK、MOSI、MISO看三件事片选有没有真的被拉低如果一直是高问题在片选配置SCLK 有没有波形如果没有检查控制器是否真的启动了传输或者时钟引脚复用没配对MOSI 上发的字节和代码里写的是否一致如果 MOSI 全 0说明发送缓冲区根本没被控制器读到。这一步能排掉一半的问题。我遇到过一次spi_sync返回 0、波形全无最后发现是控制器的时钟门控没开probe里漏了clk_prepare_enable()。代码层面完全看不出来只有波形能告诉你真相。5.2 波形正常但数据错按 mode、字长、字节序的顺序排波形有了数据不对按这个顺序排ModeCPOL/CPHA 四种组合抓波形对照从机手册的时序图。CPHA 错位表现为数据整体偏移半个到一个时钟读出来的值看着像但不对。字长bits_per_word是 8 还是 16如果从机是 16 位寄存器、你按 8 位发地址会被拆成两个字节从机收到的是错误地址。字节序SPI_LSB_FIRST标志。绝大多数 SPI 器件是 MSB first但有些传感器是 LSB first配反了读出来的值位序是镜像的。位宽模式单线、双线、四线。tx_nbits配成 2 但硬件只接了 MOSI 一根线数据会丢一半。这四项里Mode 和字长占问题总量的八成以上。我现在的习惯是新器件上手先用最低频率1 MHz 以下、8 位字长、Mode 0 跑通读 ID再逐步往上加频率和改 Mode每改一项验证一次出问题立刻知道是哪一项。5.3 短传输正常、长传输出错看长度上限和 DMA 对齐这是另一类高频问题。单字节读 ID 正常连续读 64 字节就后半段全是脏数据。原因通常有三个控制器单次传输长度上限。controller-max_transfer_size和max_message_size是控制器能力声明如果超了有的控制器会截断有的直接报错。查/sys/class/spi_master/spiX/下有没有相关属性或者在驱动里读ctlr-max_transfer_size判断。DMA 缓冲区对齐。走 DMA 的控制器通常要求缓冲区按 cache line 对齐32 或 64 字节。用kmalloc分配的缓冲区不保证对齐栈上的缓冲区更是随机。表现为前半段对、后半段错因为跨 cache line 的部分被 DMA 跳过或重复搬运。解决办法是用dma_alloc_coherent()或者在spi_message上依赖核心的spi_map_msg()做 bounce buffer。从机 FIFO 深度限制。有些从机的接收 FIFO 只有 8 字节你连续发 64 字节超出部分被丢弃。这种情况必须在 message 里拆段每段之间加延时等从机取走数据。5.4 用 spi_statistics 和动态调试定位瓶颈内核给每个spi_device和spi_controller都挂了统计计数器在/sys/class/spi_master/spiX/statistics/和/sys/class/spi_master/spiX/spiX.Y/statistics/下可以看到bytes_tx/bytes_rx累计收发字节数和你代码里的预期对不上就说明有传输被丢弃messages/transfers消息数和段数用来验证 message 拆分是否符合预期errors错误计数配合timedout判断是不是超时spi_sync/spi_async两种调用的次数。排查时先cat一遍这些计数器跑一轮测试再cat一遍差值能告诉你很多信息。比如messages增加了 1 但transfers增加了 5说明你的 message 被拆成了 5 段如果预期是 2 段那拆分逻辑就有问题。动态调试也很实用echo file spi.c p /sys/kernel/debug/dynamic_debug/control echo file spi-mem.c p /sys/kernel/debug/dynamic_debug/control dmesg -w打开后能看到每次spi_sync的 message 长度、每段 transfer 的参数、控制器的启动和完成时间点。配合 ftrace 的irqsoff或preemptofftracer还能看出传输过程中是否被中断打断。6. 一份能直接改着用的驱动骨架最后给一份完整骨架覆盖注册、probe、读写封装、卸载。省略了具体寄存器操作重点在结构体初始化和 message 组织方式上。6.1 私有结构体与驱动的注册struct myspi_dev { struct spi_device *spi; struct mutex io_lock; /* 串行化并发访问 */ u8 *tx_buf; u8 *rx_buf; struct spi_transfer xfer; struct spi_message msg; }; static const struct of_device_id myspi_of_match[] { { .compatible vendor,myspi-sensor }, { } }; MODULE_DEVICE_TABLE(of, myspi_of_match); static const struct spi_device_id myspi_id[] { { myspi-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(spi, myspi_id); static struct spi_driver myspi_driver { .driver { .name myspi-sensor, .of_match_table myspi_of_match, .pm myspi_pm_ops, }, .id_table myspi_id, .probe myspi_probe, .remove myspi_remove, }; module_spi_driver(myspi_driver);module_spi_driver()展开后就是spi_register_driver()加模块退出时的spi_unregister_driver()。id_table和of_match_table都写覆盖 DTS 枚举和spi_board_info静态注册两条路径。6.2 probe 里必须做的事static int myspi_probe(struct spi_device *spi) { struct myspi_dev *dev; int ret; dev devm_kzalloc(spi-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev-spi spi; mutex_init(dev-io_lock); spi_set_drvdata(spi, dev); /* 频率不超过从机上限字长固定 8 位Mode 从 DTS 继承 */ spi-max_speed_hz min(spi-max_speed_hz, 10000000u); spi-bits_per_word 8; spi-mode ~SPI_MODE_X_MASK; /* 如需强制 Mode 0可在此处改 */ ret spi_setup(spi); if (ret) { dev_err(spi-dev, spi_setup failed: %d\n, ret); return ret; } dev-tx_buf devm_kmalloc(spi-dev, MYSPI_MAX_XFER, GFP_KERNEL | __GFP_DMA); dev-rx_buf devm_kmalloc(spi-dev, MYSPI_MAX_XFER, GFP_KERNEL | __GFP_DMA); if (!dev-tx_buf || !dev-rx_buf) return -ENOMEM; spi_message_init(dev-msg); dev-xfer.tx_buf dev-tx_buf; dev-xfer.rx_buf dev-rx_buf; spi_message_add_tail(dev-xfer, dev-msg); return myspi_hw_init(dev); }几个细节缓冲区用__GFP_DMA分配是为了提高 DMA 可用概率但真正的 cache line 对齐还要靠dma_alloc_coherentspi_setup()必须在改完mode/bits_per_word/max_speed_hz之后调用否则改动不生效spi_message_init和spi_message_add_tail放在 probe 里做一次后续每次传输只改xfer.len和缓冲区内容减少重复开销。6.3 读写封装一段式 message 的复用static int myspi_xfer(struct myspi_dev *dev, const u8 *tx, u8 *rx, size_t len) { int ret; if (len 0 || len MYSPI_MAX_XFER) return -EINVAL; mutex_lock(dev-io_lock); if (tx) memcpy(dev-tx_buf, tx, len); else memset(dev-tx_buf, 0, len); dev-xfer.len len; dev-xfer.speed_hz 0; /* 继承 spi_device 设置 */ dev-xfer.bits_per_word 0; dev-xfer.delay.value 0; dev-xfer.cs_change 0; ret spi_sync(dev-spi, dev-msg); if (ret 0 rx) memcpy(rx, dev-rx_buf, len); mutex_unlock(dev-io_lock); return ret; }用mutex而不是自旋锁因为spi_sync会睡眠。xfer和msg在结构体里复用避免每次动态分配。传输完成后memcpy出来是因为下一次传输会覆盖rx_buf不拷走的话数据就丢了。单段 message 里cs_change保持 0传输结束后片选自动抬起这是绝大多数从机期望的行为。如果从机要求多段之间保持片选才需要构造多段 message 并单独处理cs_change。6.4 remove 与异常路径static void myspi_remove(struct spi_device *spi) { struct myspi_dev *dev spi_get_drvdata(spi); myspi_hw_suspend(dev); /* mutex 不需要显式销毁devm 分配的内存自动释放 */ }因为全部用devm_*分配remove 里基本不用手动释放。唯一要注意的是如果驱动里用了spi_asyncremove 之前必须确保没有正在进行的传输否则回调触发时私有结构体已经被释放。做法是在 remove 里先置一个shutdown标志然后wait_for_completion_timeout()等当前传输结束。7. 几个用血换来的经验第一永远先用spi_sync把功能跑通再考虑优化成spi_async。异步带来的复杂度和并发问题只有在确实需要中断上下文、高吞吐队列时才值得。很多驱动写异步是因为看起来性能更好实际 SPI 总线速率受限于从机异步省下的那点调度开销根本体现不出来。第二spi_setup()的返回值一定要检查。它内部要和控制器协商频率、字长、模式如果控制器不支持你要求的组合会返回-EINVAL。忽略这个返回值后面所有传输都会用上一次生效的配置跑现象莫名其妙。第三调试时把max_speed_hz降到 1 MHz 以下。绝大多数时序问题是频率太高导致的建立/保持时间不够降频后问题消失就说明是时序余量问题而不是逻辑错误。这个技巧能帮你在一分钟内把问题范围缩小一半。第四给每个关键传输点加dev_dbg打印actual_length和status。status为 0 但actual_length小于预期的情况说明传输被部分完成这种隐性失败最容易被漏掉。第五片选是 SPI 里最不像数据线的数据线。它不传数据但决定了数据的边界。所有数据看起来错位读出来多一个字节偶尔成功偶尔失败的问题先怀疑片选时序比怀疑数据缓冲区有效得多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。