☰
Linux SPI驱动数据收发链路:结构体关系、同步异步与DMA避坑
2026/10/26 6:33:54 网站建设 项目流程

翻内核源码或者接手别人写的SPI驱动,最容易被卡住的往往不是寄存器怎么配,而是spi_device、spi_transfer、spi_message这几个结构体之间到底谁管谁,以及spi_sync、spi_async、spi_write_then_read这些函数分别在什么上下文里能用。我见过不少人写驱动时把spi_transfer数组放在栈上、把spi_message反复复用、在中断里直接调spi_sync,代码编译通过、跑起来偶发丢数据或者直接内核报错。这篇就把Linux下SPI设备驱动数据收发这条链路上涉及的结构体和函数捋一遍,从设备树匹配讲到一次完整片选周期内的数据搬运,再讲异步和DMA场景下的坑。适合刚上手写SPI外设驱动的人,也适合已经写过几个驱动但没系统梳理过这套框架的人。

1. 先把SPI驱动的数据流想清楚:从设备树到片选拉低

写驱动之前我习惯先在纸上画一遍数据通路,搞清楚谁调谁。SPI这套框架其实分得很干净:一头是控制器(SoC里的SPI IP核),一头是挂在总线上的外设,中间由内核的SPI核心层做转发和排队。你写的设备驱动,本质上只是生产spi_message、消费返回值。把这个关系理顺,后面看结构体字段就不会觉得是在背单词。

1.1 一条读寄存器通路上到底有哪几个角色

假设要读某型号传感器的ID寄存器,地址0x0F。这条通路上依次出现四个角色:设备树节点描述“这块外设接在哪个控制器的第几个片选上、用哪种时序模式”;spi_device是内核根据设备树或者板级信息创建出来的设备实例,代表“总线上有一个从设备”;spi_driver是你写的那部分,负责probe、注册中断、向上层暴露字符设备或input设备;spi_controller(老内核叫spi_master)是控制器驱动,真正去操作寄存器拉时钟、发数据。

数据本身的载体是spi_transfer,装“发什么、收什么、多长”;一次片选周期内可能包含多个spi_transfer,它们被串到一个spi_message链表里,整体提交给核心层。核心层再把这个message交给控制器驱动的transfer_one_message回调,由它去决定用CPU搬运还是DMA。

有人会问,为什么不直接提供一个spi_read_reg(dev, reg)这样的函数。因为SPI和I2C不一样,它没有统一的协议层,读寄存器这件事在不同器件上时序差别很大:有的要发地址后立即读,有的要发命令字再单独一个周期读,有的干脆是流式读取。内核只提供搬运能力,协议语义留给设备驱动自己拼。

1.2 为什么内核要把“消息”和“传输”拆成两层

这个设计我一开始也觉得绕,后来写过几个需要“片选保持”的器件才明白为什么。spi_transfer对应一段连续的收发,spi_message对应一次片选断言到释放的完整周期。默认行为是:一个message里的所有transfer之间,片选保持有效,时钟可以停顿但不释放CS;message结束后,片选拉高。

拆成两层的价值在于:你可以在一次片选周期里塞进多个语义不同的transfer,比如先发一个命令字(只发不收),再发一个地址(只发不收),最后一个transfer收数据(只收不发)。如果只有一个“读”函数,这三个阶段就没法分别表达,而很多器件的时序恰恰要求命令和地址之间不能释放片选。

反过来,如果你需要片选在中间释放一次(某些器件靠CS上升沿锁存数据),就把中间那个transfer的cs_change置1。这个字段的语义在不同内核版本上实现略有差异,我个人的做法是:对CS时序敏感的场景,宁可用多个独立的spi_sync调用,每个调用之间CS明确释放,也不去赌cs_change在目标内核上的具体行为。

注意:spi_message内部靠transfer_list链表组织transfer,spi_message_add_tail只是挂链,不涉及任何硬件操作;真正的收发发生在spi_sync/spi_async里。别在挂链之后就去读rx_buf。

1.3 同步还是异步:先看调用上下文再决定

spi_sync是同步接口,内部会拿控制器锁、可能睡眠,所以只能在进程上下文用。它的好处是调用返回时数据已经在rx_buf里,逻辑直观。spi_async是异步接口,提交后立刻返回,传输完成时通过spi_message.complete回调通知,可以在中断上下文或者原子上下文调用。

选择依据不是“哪个性能好”,而是“你当前在什么上下文”。如果读传感器是在工作队列或者线程化的中断处理里做的,spi_sync完全够用,代码也简单。只有在硬中断里必须立刻发起一次传输(比如响应某个GPIO触发的采样),才需要spi_async。我见过有人在硬中断里调spi_sync,某些控制器上直接触发“scheduling while atomic”告警,因为spi_sync内部会拿mutex。

还有一种情况是同一个设备被多个上下文访问,比如一个线程在读配置、一个工作队列在采数据。这时spi_sync的串行化是靠控制器锁保证的,但你的“发命令+读数据”这个复合操作并不原子,中间可能被另一个调用插进来。解决办法是自己加一把设备级的mutex,把整段业务逻辑包住,而不是指望核心层。

2. 核心结构体逐个拆:谁管设备、谁管控制器、谁管一次传输

这几个结构体定义在include/linux/spi/spi.h里,字段不算多,但每个字段背后都有对应的使用约定。我按“从静态描述到动态传输”的顺序拆,先讲设备侧的spi_device和spi_driver,再讲控制器侧的spi_controller,最后讲传输侧的spi_transfer和spi_message。

2.1 spi_device 与 spi_driver:谁描述硬件,谁描述行为

spi_device描述一块具体硬件:挂在哪个控制器上、用哪个片选、最大时钟多少、时序模式是0到3中的哪一个、每字多少位。这些字段大部分从设备树或者板级信息里来,在设备注册时由核心层填好。驱动里可以直接读,但不应该随意改;要改时钟或者模式,走spi_setup,让控制器驱动有机会校验和重新配置。

spi_driver描述行为:probe、remove、shutdown三个回调,外加id_table用于非设备树场景的匹配。现代平台基本都是设备树匹配,靠compatible字符串和of_match_table,id_table更多出现在一些老平台或者需要兼容多型号的地方。两个表都填上不会有坏处,但要注意id_table里的名字是modalias,不是compatible,两者混用会导致匹配不上。

生命周期上,spi_device的引用计数由spi_dev_get/spi_dev_put管理。多数情况下驱动的probe拿到指针后一直用到remove,不用额外加引用。但如果你的驱动把spi_device指针存到异步回调或者定时器里,就得在存之前get一次,用完put,否则设备被移除后指针就成了悬空的。

probe里我习惯做三件事:先做一次spi_setup确认控制器接受了当前参数,再读一次器件ID做存在性检查(读不到就返回-ENODEV,避免后面莫名其妙地跑),最后再注册上层接口。这个顺序能省掉很多“设备明明没接上但驱动加载成功”的排查时间。

2.2 spi_controller:被大多数人当成黑盒的那一层

设备驱动一般不需要直接碰spi_controller,但理解它的几个字段对排查问题很有帮助。num_chipselect决定这块控制器最多支持几个片选,min_speed_hz和max_speed_hz限制时钟范围,bits_per_word_mask说明支持哪些字长,mode_bits说明支持哪些SPI模式。你请求的max_speed_hz如果超出范围,spi_setup会返回-EINVAL而不是自动截断,这个细节很多人不知道,导致驱动在某些板子上加载失败却找不到原因。

控制器的核心回调是transfer_one_message。核心层把message排进队列,由工作线程或者控制器自己的中断去消费,最终调这个回调。老版本内核里有transfer回调(要求控制器自己处理片选),新内核统一成transfer_one_message,由核心层统一处理片选和消息队列。如果你的控制器驱动是自己写的,务必用新接口,不然片选时序容易出问题。

controller->cs_gpiod数组在部分平台上用来支持GPIO片选。这种情况下片选拉低拉高由核心层通过GPIO完成,时序精度受GPIO操作延迟影响。如果器件对CS到第一个时钟沿的间隔有严格要求,用硬件片选比GPIO片选稳得多。这一点在选型阶段就该确认,而不是等到数据读不对了再回头查。

2.3 spi_transfer:一次片选周期内的最小搬运单元

spi_transfer是真正装数据的地方,关键字段就几个:tx_buf、rx_buf、len、speed_hz、bits_per_word、delay、cs_change。tx_buf和rx_buf同时非空是全双工,只有一个是半双工,两个都为空则是纯粹的延时占位(有些器件需要时钟打几个空周期)。

len的单位是字节,不是“字数”。如果bits_per_word是16,len要为偶数,否则核心层会返回-EINVAL。speed_hz为0表示沿用spi_device上的默认值;非0则在这个transfer上临时改频率,适合那些“高速读数据、低速写配置”的器件。改频率的代价是控制器每次都要重新算分频,频繁切换会拉低吞吐。

delay字段用来在transfer结束后插入一段等待。老内核是delay_usecs(微秒),新内核改成了delay(struct spi_delay,带单位),写代码时要看清目标内核版本。我踩过一次坑:照着老文档写delay_usecs = 10,在新内核上编译告警,后来发现字段改名了,用spi_transfer_delay_exec才能正确执行。跨版本移植时这个字段一定要检查。

tx_buf和rx_buf的生命周期要覆盖整个传输。同步调用时函数返回就可以释放,异步调用时必须保证缓冲区在complete回调触发之前一直有效。把栈上的局部数组传给spi_async是典型错误,函数一返回栈帧就失效了,DMA还可能正在往里写。

2.4 spi_message:容器、状态机和锁

spi_message本身不装数据,它装的是“这次传输的上下文”:transfer链表、目标设备指针、完成回调、实际传输长度actual_length、返回状态status。另外它还内嵌了一个list_head queue,用于在控制器队列里排队;以及一个spi_message级别的互斥机制,防止同一个message被并发提交。

actual_length在传输结束后由核心层填写,表示真正搬了多少字节。中断传输或者从设备提前释放片选的情况下,这个值可能小于你请求的总长度,驱动里应该检查它而不是假设一定等于预期值。status为0表示成功,负值是错误码,常见的有-ETIMEDOUT(控制器超时)、-EINVAL(参数不合法)、-EIO(控制器报错)。

is_dma_mapped字段是老接口遗留,置1表示tx_dma/rx_dma已经由驱动自己映射好。新代码不应该再用手动DMA映射,交给核心层的spi_map_msg处理就行。如果非要自己映射,得保证映射和去映射严格配对,漏掉一次就会导致内存泄漏或者DMA访问已释放内存。

complete回调只在spi_async场景下有意义,spi_sync内部会自己挂一个完成量然后等待。同一个spi_message在complete触发之前不能被复用,也不能被释放。我习惯在提交异步传输时把message和它引用的transfer数组一起放进一个自己管理的结构体里,用引用计数控制释放,避免回调和释放竞争。

2.5 结构体字段速查表

结构体关键字段作用常见误用
spi_devicemax_speed_hz、mode、chip_select、bits_per_word描述从设备电气与协议属性驱动里直接改字段而不调spi_setup
spi_driverprobe、remove、id_table、of_match_table描述驱动行为与匹配规则id_table名字写成compatible导致匹配失败
spi_controllernum_chipselect、min_speed_hz、mode_bits、transfer_one_message描述控制器能力与搬运实现请求超出范围的频率,spi_setup返回-EINVAL
spi_transfertx_buf、rx_buf、len、speed_hz、delay、cs_change一段连续收发的数据描述len与bits_per_word不匹配;缓冲区生命周期不足
spi_messagetransfers、complete、context、status、actual_length一次片选周期的容器与状态异步未完成就复用或释放message

3. 关键函数与实操:从probe到一次完整收发

结构体看懂了,接下来是函数怎么用。这一章按调用顺序走:先是匹配和probe,然后是最常用的几个收发封装,再讲参数怎么算,最后是异步和DMA场景的特殊处理。

3.1 从设备树到probe:匹配是怎么发生的

设备树里一个SPI从设备节点的典型写法包含compatible、reg(片选号)、spi-max-frequency、spi-cpol、spi-cpha几个属性。reg就是chip_select,注意它是控制器的片选索引,不是GPIO编号。有些平台支持cs-gpios,这时从设备的片选由GPIO充当,reg仍然要写,核心层靠它去索引cs_gpios数组。

内核解析这些属性生成spi_device,然后把compatible转成modalias,去和spi_driver的of_match_table比对。触发probe。如果probe没进,排查顺序是:节点status是否为okay、compatible是否和驱动里一字不差(包括厂商前缀)、控制器节点是否已经probe成功(控制器没起来,从设备不会实例化)、reg是否与已有设备冲突。

module_spi_driver宏会自动处理注册和注销,等价于在init里调spi_register_driver、在exit里调spi_unregister_driver。用这个宏比手写省事,但要注意它生成的符号名依赖驱动结构体变量名,重名会链接失败。多文件驱动里我一般还是手写注册函数,命名可控。

spi_setup一般在probe里调一次,把设备树里的spi-max-frequency和模式写进控制器。如果驱动运行中要改频率,改完spi_device->max_speed_hz之后必须再调一次spi_setup,否则控制器侧的分频寄存器不会更新,实际时钟还是老值。这个坑我踩过:以为改了字段就生效,示波器一量还是原来频率。

3.2 寄存器读写函数的三种写法与取舍

最简单的是spi_write_then_read,一次调用完成“先发后收”,两个阶段之间片选保持。它内部用了本地bounce buffer,长度上限是32字节(宏SPI_BUFSIZ),超过就返回-EINVAL。优点是代码短、不用自己管理message,缺点是长度受限、内部有一把全局mutex做串行化,高频小数据量读写会在这把锁上打架。

第二种是spi_write和spi_read,分别对应只发和只收。它们内部也是构造一个transfer然后同步提交。适合命令和数据不需要在同一个片选周期内完成的器件。注意spi_read在没有前置命令的器件上才有意义,多数SPI从设备读之前都要先发地址,这时候单独用spi_read读到的是无效数据。

第三种是自己构造transfer数组和message,用spi_sync提交。这是最灵活的方式,能表达任意组合:多段发送、发送后插入延时、中途改频率、最后一段接收。代码长一些,但时序完全可控。我的习惯是:简单寄存器读写用spi_write_then_read,超过32字节或者时序特殊的用自己构造message。

static int sensor_read_reg(struct spi_device *spi, u8 reg, u8 *val) { u8 tx[2] = { reg | 0x80, 0x00 }; /* bit7 置1表示读 */ u8 rx[2] = { 0 }; int ret; ret = spi_write_then_read(spi, tx, 2, rx, 1); if (ret) return ret; *val = rx[0]; return 0; }

上面这段是典型的“地址+哑字节”读法。有些器件只需要发1字节地址,那就把tx长度改成1、rx长度改成1。注意spi_write_then_read的rxbuf是从第一个时钟沿开始收的,如果你的器件在发地址的那个字节里也会吐出数据,那收到的第一个字节就是地址阶段的数据,需要相应调整偏移。

3.3 mode、max_speed_hz、bits_per_word 的参数计算

SPI模式由spi-cpol和spi-cpha两个布尔属性组合成0到3四种。模式选错的表现是“能收到数据但全是0xFF或者0x00”,因为采样沿不对,移位寄存器锁到的都是无效电平。选型时直接看器件手册的时序图:时钟空闲时是高还是低决定CPOL,数据在第一个沿还是第二个沿采样决定CPHA。

max_speed_hz不是你想要多少就是多少,控制器会用父时钟做整数分频,实际频率是父时钟除以分频系数后向下取整。假设控制器父时钟100MHz,你请求8MHz,分频系数取13时得到约7.69MHz,取12得到8.33MHz超过请求值,所以最终会落在7.69MHz。这个偏差对多数器件无所谓,但对时序卡得很死的器件要留余量,请求值比手册上限低10%到20%比较稳。

bits_per_word为0时用控制器默认值,通常是8。设成16或者32要注意字节序:SPI本身是MSB先出,但一个16位字内部的字节顺序取决于控制器实现,有的平台需要软件做swab16。调试时如果发现高低字节颠倒,先查这个,别急着怀疑器件。

delay的计算要结合器件手册的“CS释放到下一次CS断言的最小时间”(tCSH)和“最后一个时钟沿到CS释放的时间”(tCSD)。如果控制器不支持在transfer结束后插入精确延时,就用udelay在上层补,虽然占CPU但时序确定。

3.4 异步与DMA场景下的注意事项

spi_async提交后立即返回,数据搬运在控制器的工作线程或者DMA完成中断里进行。适合的场景是:中断上下文里触发一次采样、大块数据流式读取不想阻塞调用线程。使用要点有三条:message和transfer必须在回调触发前保持有效;回调里不能做耗时操作,最好只是complete()一个完成量或者把工作丢给工作队列;同一个设备的异步传输要考虑顺序,核心层对同一个控制器的message是排队处理的,但对同一个设备连续提交多个message时,完成顺序不一定和提交顺序一致,需要自己加序号或者串行化。

DMA相关的坑集中在缓冲区属性上。控制器如果支持DMA,核心层会对tx_buf/rx_buf做DMA映射。栈上的缓冲区、vmalloc分配的缓冲区在某些平台上不能直接做DMA,核心层会用bounce buffer中转,代价是多一次内存拷贝。用kmalloc分配(物理连续)是最稳的。如果缓冲区来自用户空间,必须先copy_from_user到内核缓冲区,不能直接把用户指针传给SPI核心层。

大块传输时用spi_message加多个transfer,或者用sg_table做散列表映射,能减少一次连续大内存分配的压力。不过对多数传感器类应用,单次传输几十字节,用简单方式就够,上DMA反而增加复杂度。

struct sensor_xfer { struct spi_message msg; struct spi_transfer xfer; u8 tx[8]; u8 rx[8]; struct completion done; }; static void sensor_xfer_complete(void *ctx) { struct sensor_xfer *x = ctx; complete(&x->done); } static int sensor_read_block_async(struct spi_device *spi, struct sensor_xfer *x, u8 cmd, u8 len) { memset(&x->msg, 0, sizeof(x->msg)); memset(&x->xfer, 0, sizeof(x->xfer)); x->tx[0] = cmd; x->xfer.tx_buf = x->tx; x->xfer.rx_buf = x->rx; x->xfer.len = len + 1; spi_message_init(&x->msg); spi_message_add_tail(&x->xfer, &x->msg); x->msg.complete = sensor_xfer_complete; x->msg.context = x; return spi_async(spi, &x->msg); }

上面这个结构体的关键点是:msg、xfer、缓冲区、完成量打包在一起,提交前清零,回调里只做complete。调用方在拿到spi_async返回值后,如果是0就等wait_for_completion(进程上下文)或者在回调里继续后续流程(中断上下文)。注意memset不能省,spi_message里的链表头必须初始化,否则挂链时会踩到野指针。

3.5 一份可复现的驱动骨架

把上面的内容拼起来,一个最小可用的SPI设备驱动大概是这个形态:probe里做spi_setup、读ID校验、注册字符设备或者input设备;读写函数封装成read_reg/write_reg;remove里注销并关闭电源。

#include <linux/module.h> #include <linux/spi/spi.h> #define SENSOR_REG_ID 0x0F #define SENSOR_REG_CTRL 0x20 #define SENSOR_ID_VAL 0x3A struct sensor_dev { struct spi_device *spi; struct mutex lock; }; static int sensor_write_reg(struct spi_device *spi, u8 reg, u8 val) { u8 buf[2] = { reg & 0x7F, val }; return spi_write(spi, buf, sizeof(buf)); } static int sensor_read_reg(struct spi_device *spi, u8 reg, u8 *val) { u8 tx = reg | 0x80; return spi_write_then_read(spi, &tx, 1, val, 1); } static int sensor_probe(struct spi_device *spi) { struct sensor_dev *dev; u8 id; int ret; ret = spi_setup(spi); if (ret) return ret; ret = sensor_read_reg(spi, SENSOR_REG_ID, &id); if (ret) return ret; if (id != SENSOR_ID_VAL) { dev_err(&spi->dev, "unexpected id 0x%02x\n", id); return -ENODEV; } dev = devm_kzalloc(&spi->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev->spi = spi; mutex_init(&dev->lock); spi_set_drvdata(spi, dev); mutex_lock(&dev->lock); ret = sensor_write_reg(spi, SENSOR_REG_CTRL, 0x01); mutex_unlock(&dev->lock); return ret; } static void sensor_remove(struct spi_device *spi) { /* 关闭器件、注销上层接口 */ } static const struct of_device_id sensor_of_match[] = { { .compatible = "vendor,sensor-x" }, { } }; MODULE_DEVICE_TABLE(of, sensor_of_match); static struct spi_driver sensor_driver = { .driver = { .name = "sensor-x", .of_match_table = sensor_of_match, }, .probe = sensor_probe, .remove = sensor_remove, }; module_spi_driver(sensor_driver);

这份骨架里有几个地方值得说:spi_setup放在最前面,参数不合法就直接退出;ID校验放在初始化写寄存器之前,避免对不存在的器件发配置;mutex_init和spi_set_drvdata在分配之后立刻做,后面所有需要设备上下文的操作都能拿到;配置写用spi_write而不是spi_write_then_read,因为不需要读回,少一个transfer就少一次锁竞争。

设备树对应节点:

&spi1 { status = "okay"; sensor@0 { compatible = "vendor,sensor-x"; reg = <0>; spi-max-frequency = <8000000>; spi-cpol; spi-cpha; }; };

spi-cpol和spi-cpha同时存在对应模式3。如果器件手册写的是模式0,把这两个属性都删掉。reg = <0>表示用控制器的第0号片选,硬件上要确认CS0确实接到了这颗器件。

4. 常见问题与排查技巧实录

SPI驱动的调试有个特点:软件层面的错误和硬件层面的错误表现很像,都是“读出来不对”。所以排查要分层:先确认驱动流程有没有跑通,再确认时序参数对不对,最后才是硬件连线。下面这些是我在实际项目里整理出来的。

4.1 现象到原因的速查表

现象优先怀疑排查手段
probe完全没进compatible不匹配、控制器未就绪、reg冲突dmesg看控制器probe日志;确认节点status
驱动加载报-EINVALmax_speed_hz超出控制器范围、bits_per_word不支持查控制器min_speed_hz/max_speed_hz与bits_per_word_mask
读回全0x00MISO未接、器件未供电、片选未生效、模式错误示波器看MISO与CS;换模式0重试
读回全0xFFMISO被上拉且无器件响应、片选接错、时钟过快降低频率到1MHz重试;确认CS引脚
数据偶尔错位多上下文并发访问未加锁、缓冲区生命周期不足加设备级mutex;检查异步缓冲区
spi_sync返回-ETIMEDOUT控制器超时、DMA未完成、中断未响应查控制器驱动日志;改polling模式试
传输成功但actual_length偏小器件提前释放、控制器分段传输检查status;打印actual_length

这张表里最值得展开的是“读回全0xFF”和“数据偶尔错位”。前者九成是硬件问题,但很多人第一反应是改驱动。我的经验是:先拿示波器或者逻辑分析仪抓CS、SCLK、MOSI、MISO四根线,看有没有时钟出来、CS有没有拉低、MISO在采样沿上是不是稳定电平。时钟完全没有,那是控制器配置或者引脚复用问题;时钟有但MISO一直高,那是从设备没响应。

4.2 几个踩过的坑

第一个坑是spi_write_then_read的长度上限。有个项目要一次读40字节的FIFO数据,代码里直接用spi_write_then_read传了41字节,返回-EINVAL,但错误码没打印,导致现象是“读回来的数组全是初始值”。查了半天以为是DMA问题,最后把返回值打出来才发现是长度超限。后来改成自己构造message,问题消失。这个函数的上限是32字节,超过了必须自己拼。

第二个坑是异步传输的缓冲区生命周期。早期写过一个驱动,在中断里用spi_async提交,transfer的tx_buf指向一个函数内的局部数组。测试时大部分情况正常,压力测试下偶发数据错乱。原因是函数返回后栈帧被覆盖,而DMA还在读那块内存。改成在设备结构体里预分配缓冲区,问题再没出现。异步场景下所有被引用的内存都必须是长生命周期的,这一条没有例外。

第三个坑是并发访问。设备同时被一个sysfs属性读和一个定时器写,两边都用spi_sync,看起来核心层有锁应该安全。但实际上“发命令+读数据”是两步,中间可能被另一个调用插进去,导致读到的数据和命令对不上。加了一把设备级mutex把整个复合操作包住之后才稳定。核心层的锁保护的是单次message的提交,不保护你的业务逻辑。

第四个坑是模式配置。某型号ADC手册写的是“数据在SCLK下降沿采样”,我按模式0配的,读出来数值一直在跳。后来仔细看时序图,空闲电平是高,对应CPOL=1,采样在第二个沿,也就是模式3。改成模式3后数据立刻稳定。SPI模式的四个组合里,模式0和模式3最常见,但千万别靠猜,一定对着时序图确认。

提示:调试阶段把max_speed_hz降到1MHz甚至500kHz,能排除掉大部分时序余量不足导致的问题。确认功能正常后再逐步往上加,找到稳定工作的上限。

4.3 调试手段:从printk到逻辑分析仪

软件侧最有效的手段是打印。在probe里打印spi->mode、spi->max_speed_hz、spi->chip_select、spi->bits_per_word,确认设备树解析结果符合预期;在读写函数里打印命令字、返回码、actual_length,确认每次传输的实际结果。dev_dbg配合动态调试开关比直接printk好,生产版本不会刷屏。

内核里还有spi相关的调试节点,部分平台提供/sys/kernel/debug/spi下的统计信息,能看到每个控制器的传输次数和错误计数。控制器的错误计数在排查“偶发失败”时特别有用:如果错误计数在涨,说明是硬件或者时序问题;如果计数不涨但数据不对,说明是协议层拼错了。

硬件侧,逻辑分析仪比示波器好用,因为SPI是同步串行协议,需要按沿解码。抓的时候至少接CS、SCLK、MOSI、MISO四根线,采样率至少是SCLK频率的10倍。解码器设成对应的模式和位宽,直接看解出来的字节,比人眼看波形快得多。我一般会先在1MHz下抓一次,确认命令、地址、数据的字节序都对,再往上提频率。

还有一个容易忽略的点:电源和地。SPI频率上到十几兆之后,地线回流不好会导致波形振铃,采样出错。飞线调试时地线要短、要粗,最好和信号线绞在一起。这个问题在实验室里经常被当成“驱动bug”查很久,实际上是接线问题。

最后说一个我个人的习惯:新器件的驱动,先写一个只做“读ID”的最小模块,不注册任何上层接口,加载后看dmesg输出。ID读对了,说明片选、时钟、模式、位宽、字节序全对,后面所有功能都是在这条正确通路上扩展。ID读不对,就别往下写,先把这条通路打通。这个做法帮我省掉了大量“功能写完发现底层不通、回头逐层排查”的时间。

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

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

立即咨询