简介:面向嵌入式开发者的SDIO驱动学习资料包,适合正在接触Linux或裸机SDIO、Wi-Fi与蓝牙模块驱动,或需要理解SDIO协议、探测初始化、数据传输与中断处理流程的初中级工程师参考。压缩包为rar格式,共23个文件、约1.3MB,包含多份SDIO与SD卡规范PDF、项目总结doc、原理图与PCB源文件,以及SDIO.c、SDIO.H和Keil工程配置等源码与工程文件,便于把协议说明、硬件设计与代码实现对应起来。已有668人学习下载,说明其在SDIO入门与项目复盘场景中具有一定参考价值。读者可从中获得协议要点、驱动分层与工作流程、设备配置和中断处理思路,也能借助现有工程文件了解SDIO项目的目录组织与调试线索,适合作为课程设计、模块移植和驱动开发的辅助材料。
1. SDIO 驱动不是"写个读写函数":先认清总线上跑的是什么
把一块 SDIO 接口的无线模组焊到自研板子上,上电,串口里只冒出一句mmc1: new high speed SDIO card at address 0001,然后就再没有任何动静——总线认到了卡,驱动没绑上,功能一个都跑不出来。这种"设备在、驱动不在"的状态,是很多人第一次写 SDIO 驱动的起点。它跟 USB 驱动、I2C 驱动最大的区别在于:SDIO 是 SD 总线的一个扩展分支,设备不是插拔式枚举,而是挂在 MMC 主机控制器下面,靠功能号(function number)区分多个逻辑设备,靠 CMD52/CMD53 两条命令完成全部寄存器访问和数据搬运。这份资料围绕的是一个从零写 SDIO 功能驱动的完整工程,覆盖驱动注册、CMD52 单字节访问、CMD53 块传输封装、主机控制器侧时钟与中断配置,以及接自研板子时的调试手段。适合已经会写字符设备、但被 SDIO 枚举和块传输卡住的中级开发者,也适合要接手厂商驱动做二次开发的人。
2. 驱动骨架:从 sdio_register_driver 到 probe 的完整注册链路
SDIO 驱动在 Linux 里属于 mmc 子系统的一条总线分支,它不像 platform 驱动那样到处可见,很多人第一次找sdio_register_driver的调用链都要翻半天。整条链路大致是:主机控制器驱动(sdhci 或厂商自研 host)注册一个mmc_host,MMC 核心在mmc_rescan里发 CMD5 探测 SDIO 设备,成功后调用mmc_attach_sdio,逐个功能号创建struct sdio_func,最后sdio_add_func把 func 挂到 sdio 总线上,总线匹配器拿 func 的 vendor/device 号去和各个sdio_driver的 id_table 比对,命中就调 probe。
理解这条链路的价值在于:当 probe 没被调用时,你能快速判断问题出在哪一段。是 CMD5 没响应(硬件与电源),还是 func 创建了但没有驱动匹配(id_table 写错),还是匹配上了但 probe 里卡死(驱动自身问题)。三种情况的排查方向完全不同,混在一起查就是纯玄学。
2.1 struct sdio_driver 里真正起作用的三个字段
一个最小的 SDIO 驱动骨架长这样,字段不多,但每一个都有明确用途:
#include <linux/module.h> #include <linux/mmc/sdio_func.h> #include <linux/mmc/sdio_ids.h> /* 虚构的厂商号与设备号,真实值从卡的 CIS 里读出来 */ static const struct sdio_device_id demo_sdio_ids[] = { { SDIO_DEVICE(0x0412, 0x0001) }, { /* 结束标记,漏了会越界匹配 */ } }; MODULE_DEVICE_TABLE(sdio, demo_sdio_ids); static int demo_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id) { /* 这里做资源申请、寄存器初始化、注册字符设备等 */ return 0; } static void demo_sdio_remove(struct sdio_func *func) { /* 与 probe 严格对称地释放 */ } static struct sdio_driver demo_sdio_driver = { .name = "demo_sdio", .id_table = demo_sdio_ids, .probe = demo_sdio_probe, .remove = demo_sdio_remove, }; module_sdio_driver(demo_sdio_driver);三个字段里,id_table是最容易翻车的一个。SDIO_DEVICE(vendor, device)里的两个数字不是随便填的,它们来自卡片的 CIS(Card Information Structure)区域,也就是厂商在出厂时烧进卡里的身份信息。常见的错误是照抄别家模组的 ID,结果总线上明明有卡,/sys/bus/sdio/devices/下也能看到条目,就是 probe 不进来。正确的做法是先把卡枚举起来,然后读:
# 列出总线上已枚举的 SDIO 功能,格式为 厂商号:设备号:功能号 ls /sys/bus/sdio/devices/ # 典型输出:0x0412:0x0001:1看到这行输出,说明硬件链路是通的,剩下的就是驱动匹配问题。name字段只用于 sysfs 展示和日志,不影响匹配;remove必须与 probe 对称,少释放一样东西,模块卸载时就会留下悬挂的中断或时钟引用。
module_sdio_driver()这个宏展开后是module_driver(__sdio_driver_register, __sdio_driver_unregister, ...),也就是说注册和注销都在模块初始化/退出时完成,不需要你手写 init/exit。
2.2 probe 里第一件事为什么是 claim_host
很多新手在 probe 里直接调sdio_readb()读版本寄存器,编译能过,运行偶尔也能过,但压测时随机挂死。原因在于 SDIO 总线是独占的:一条总线上所有功能共享同一个主机控制器的命令通道,sdio_readb内部会发 CMD52,如果此时另一个线程(比如中断下半部)也在发命令,两条命令就可能交错,主机控制器直接报命令超时或者返回错乱数据。
正确的姿势是成对使用sdio_claim_host()/sdio_release_host()把一段总线操作包起来:
static int demo_hw_init(struct sdio_func *func) { int ret; u8 ver; sdio_claim_host(func); /* 使能该功能号,内部会写 CCCR 的 IOEx 寄存器 */ ret = sdio_enable_func(func); if (ret) { dev_err(&func->dev, "enable func failed: %d\n", ret); goto out; } /* 设置块大小,后续 CMD53 块传输按这个粒度走 */ ret = sdio_set_block_size(func, 512); if (ret) goto err_disable; /* 读芯片版本号,验证总线真的通了 */ ver = sdio_readb(func, 0x00, &ret); if (ret) goto err_disable; dev_info(&func->dev, "chip version: 0x%02x\n", ver); sdio_release_host(func); return 0; err_disable: sdio_disable_func(func); out: sdio_release_host(func); return ret; }这段代码有几个必须说清的点。sdio_enable_func()是必须的,未使能的功能号上发 CMD53 会被卡直接拒绝;sdio_set_block_size()的第二个参数要和卡支持的最大块大小对齐,SDIO 规范上限是 512 字节,写大了会在传输时返回错误标志。sdio_readb()的返回值是数据本身,错误码通过第三个指针参数带出——这个签名和大多数内核 API 相反,第一次写几乎都会搞混。
还要注意goto的错误处理顺序:先 claim 最后 release,先 enable 最后 disable,任何一步失败都要按逆序回滚。这套模板看着啰嗦,但它是后面所有复杂逻辑能稳住的基础。
2.3 设备树与 ID 表怎么把设备和驱动对上
在带设备树的平台上,SDIO 设备节点通常挂在对应的 mmc 控制器下面,用reg表示功能号:
&mmc1 { status = "okay"; bus-width = <4>; non-removable; /* SDIO 设备焊死,不需要插拔检测 */ keep-power-in-suspend; /* 休眠时保持供电,否则唤醒后枚举失败 */ cap-sdio-irq; /* 允许使用 DAT1 上的带内中断 */ max-frequency = <50000000>; vmmc-supply = <&vcc_wifi>; wifi: wifi@1 { reg = <1>; /* 功能号 1,通常功能 0 是 CIS/CCCR */ interrupt-parent = <&gpio>; interrupts = <12 IRQ_TYPE_LEVEL_HIGH>; }; };non-removable这一条漏写的后果非常典型:内核会按可插拔卡处理,rescan 时先把电源断一下再重新上电,某些模组掉电后需要几百毫秒才能重新响应 CMD5,于是枚举时好时坏。reg = <1>也不是随便写的,它必须和卡上实际使用的功能号一致,大多数无线模组的功能 0 是公共控制区,数据通路走功能 1。
至于设备树节点和sdio_driver的关系,需要说清楚:SDIO 总线不走 of_match_table,设备树节点里的compatible对匹配不起作用,真正决定绑定的是id_table里的厂商号和设备号。这一点和 platform 驱动差别很大,很多人写了compatible却发现毫无效果,就是踩在这里。
3. 传输层拆解:CMD52 与 CMD53 的差异决定了你的读写封装
SDIO 的全部数据交互最终都落到两条命令上。CMD52 是单字节读写,带 17 位地址和一个字节数据,适合访问配置寄存器、中断状态、控制位;CMD53 是多字节读写,支持字节模式和块模式,适合搬固件、传网络包。两条命令的开销差距很大:一次 CMD52 只搬 1 字节,但命令本身有完整的命令/响应周期;一次 CMD53 在块模式下能搬 512 字节,命令开销被摊薄。所以"该用哪条"的判断标准很简单——单字节控制用 CMD52,批量数据用 CMD53,用反了性能会差出几十倍。
Linux 把它们封装成了四个层次的 API:sdio_readb/sdio_writeb对应 CMD52,sdio_readsb/sdio_writesb对应 CMD53 的固定地址模式(适合 FIFO),sdio_memcpy_fromio/sdio_memcpy_toio对应 CMD53 的自增地址模式(适合内存映射区)。选错这一层封装,代码能跑但会很慢,这是移植厂商驱动时最常见的性能问题来源。
3.1 单字节寄存器访问:sdio_readb 背后的 CMD52
先看最基础的一层。写一个读寄存器的辅助函数,看起来只有三行,但每一行都有讲究:
/* 读单个 8 位寄存器:value 通过返回值带出,错误码走 ret */ static int demo_reg_read(struct demo_dev *dev, u32 addr, u8 *value) { struct sdio_func *func = dev->func; int ret; if (addr > 0x1FFFF) /* CMD52 地址只有 17 位 */ return -EINVAL; sdio_claim_host(func); *value = sdio_readb(func, addr, &ret); sdio_release_host(func); return ret; } /* 写单个 8 位寄存器,写失败时 ret 为负 */ static int demo_reg_write(struct demo_dev *dev, u32 addr, u8 value) { struct sdio_func *func = dev->func; int ret; if (addr > 0x1FFFF) return -EINVAL; sdio_claim_host(func); sdio_writeb(func, value, addr, &ret); sdio_release_host(func); return ret; }地址范围检查不是形式主义。CMD52 的参数里地址字段只有 17 位,超过这个范围会被截断,写操作会静默地写到别的寄存器上,这种 bug 极难定位——寄存器读回来值不对,但没有任何错误码。规范里功能 0 的地址空间是 CCCR(0x00–0xFF)加 CIS(0x1000–0x17FFF),功能 1 之后的地址由厂商定义,写驱动前一定要拿到寄存器手册。
另一个细节是sdio_readb的"读后写"行为。它内部会做一次读-改-写的保护(read-after-write 标志),用来避免对同一个寄存器做位操作时覆盖其他位,代价是多一次总线事务。对于纯读取场景,这个开销是浪费的,但在标准封装里无法关掉,所以寄存器读不要放在高频路径上。
3.2 块传输:sdio_memcpy_fromio 与块大小设置
批量读写走 CMD53,封装上要注意块大小、长度对齐和方向参数:
/* 从设备自增地址读一段数据到内存,走 CMD53 自增模式 */ static int demo_bulk_read(struct demo_dev *dev, u32 addr, void *buf, int len) { struct sdio_func *func = dev->func; int ret; if (len <= 0 || len > 512 * 64) /* 单次传输上限,按卡能力调整 */ return -EINVAL; sdio_claim_host(func); ret = sdio_memcpy_fromio(func, buf, addr, len); sdio_release_host(func); if (ret) dev_err(&func->dev, "bulk read failed: %d, addr=0x%x len=%d\n", ret, addr, len); return ret; }sdio_memcpy_fromio内部会根据长度自动选择字节模式还是块模式:长度是块大小整数倍时走块模式,否则退化到字节模式。字节模式下每一个字节都要一个命令周期,性能差一个量级,所以缓冲区长度尽量按块大小对齐。块大小在 2.2 节里通过sdio_set_block_size(func, 512)设置过,如果设成 64 却按 512 的倍数传数据,就会频繁在两种模式间切换。
还有一个容易忽略的点:buf必须是 DMA 安全的。SDIO 主机控制器大多用 DMA 搬运数据,如果传入的是栈上的缓冲区或者没做 cache 维护的 kmalloc 内存,在带 cache 的 ARM 平台上会出现"写进去的数据读回来是旧的"这类问题。常见做法是用dma_alloc_coherent()或者kmalloc加dma_map_single(),传输前刷 cache。
3.3 自己封一层读写接口(含对齐与超时处理)
厂商驱动里往往有一套自己的寄存器抽象层,因为同一个 IP 可能同时有 SDIO、PCIe、USB 三种接口版本,业务代码不应该关心底层是 CMD52 还是 MMIO。仿照这个思路封一层:
struct demo_bus_ops { int (*read8)(struct demo_dev *dev, u32 addr, u8 *val); int (*write8)(struct demo_dev *dev, u32 addr, u8 val); int (*read_bulk)(struct demo_dev *dev, u32 addr, void *buf, int len); }; /* SDIO 版本的实现 */ static const struct demo_bus_ops demo_sdio_ops = { .read8 = demo_reg_read, .write8 = demo_reg_write, .read_bulk = demo_bulk_read, };这么做的好处很直接:换接口形态时只改 ops 表,上层协议栈一行不动。代价是多一层间接调用,对寄存器密集访问的场景有微小开销,但相比总线事务本身可以忽略。
超时处理要放在这一层统一做。SDIO 命令的超时由主机控制器负责,驱动侧能做的补偿是:对关键寄存器加重试,对批量传输做分片。比如一次 32KB 的固件下载,拆成 64 次 512 字节的块传输,每次失败单独重试,比整块失败重来要可靠得多。重试次数不要设太大,超过三次基本就是硬件问题了,继续重试只会让日志更难读。
4. 主机控制器侧:SDHCI/DW 时钟、电源与中断配置
驱动能跑起来,一半功劳在 host 侧。SDIO 功能驱动看到的是struct sdio_func,它下面压着mmc_host、mmc_card、以及具体的主机控制器驱动。绝大多数 SoC 用的是 SDHCI 兼容控制器,寄存器布局遵循同一套标准,所以很多调试手段是通用的:时钟控制、电源控制、中断状态都在固定偏移上。搞懂这一层,遇到"驱动逻辑没问题但就是不工作"的情况才有下手的地方。
这一层的关键参数不多,但每一个都能单独把项目卡住几天:总线时钟上限、初始化的低速时钟、1.8V 电压切换时序、带内中断的使能方式。下面按这三个方向拆。
4.1 设备树里几个必须写对的属性
第 2 章给过一份基础的 dts 片段,这里把每个属性的实际影响讲透,因为改错一个就是几小时的排查:
| 属性 | 作用 | 写错的后果 |
|---|---|---|
bus-width | 数据线宽度,1/4/8 | 写成 1 会让吞吐掉到 1/4 |
non-removable | 声明设备常驻 | 漏写导致 rescan 反复断电重枚举 |
keep-power-in-suspend | 休眠保持供电 | 漏写导致唤醒后 CMD5 超时 |
cap-sdio-irq | 启用 DAT1 带内中断 | 漏写只能用轮询或外接 GPIO 中断 |
max-frequency | 总线时钟上限 | 写超硬件能力会导致 CMD53 CRC 错 |
vmmc-supply | 供电 regulator | 漏写导致上电顺序不受控 |
bus-width要和硬件实际布线一致,四线模式用了但只焊了两根数据线,枚举能过、传输会随机错。max-frequency是最容易虚高的一个,很多模组标称支持 50MHz,但板级走线、电源质量、主机控制器分频精度都可能让实际可用频率低一档。稳妥做法是先写 25MHz 跑通,再用dd或 iperf 压测逐步往上调。
4.2 时钟与 1.8V 电压切换的时序
SDIO 枚举分两个阶段:初始化和高速传输。初始化阶段必须用 400kHz 的低速时钟,因为此时卡还不知道主机的时序能力,高速时钟下 CMD5 的响应窗口对不上,表现出来就是"命令超时"。等 CMD5 响应拿到 OCR 之后,主机才能切到工作频率。
1.8V 切换是个更隐蔽的坑。SDIO 规范里,CMD5 的参数是一个 OCR 值,其中 S18A 位(bit 24)表示卡支持 1.8V 信号电平。主机如果也支持,就会在参数里把这个位置 1,卡回应后双方进入 1.8V 模式。问题在于:主机侧的 IO 电源必须真的切过去,如果只在命令层面协商了 1.8V 而 regulator 还是 3.3V,通信会在大负载下随机出错,小数据量测试完全正常,一跑吞吐就崩。排查这类问题要看 regulator 的实际输出电压:
# 查看 regulator 当前电压与约束 cat /sys/kernel/debug/regulator/regulator_summary时序上还有一条硬性要求:断电到重新上电之间要有足够的保持时间,多数模组要求 1ms 以上,电源设计差的板子需要更长。如果vmmc-supply的 regulator 支持regulator-ramp-delay,把这个值配对,能让内核在切换电压时等够时间。
4.3 SDIO 中断:DAT1 与 cap-sdio-irq
SDIO 有两种中断路径。带内中断走 DAT1 信号线,卡拉低该线表示有事件,主机控制器检测到后触发中断;带外中断走一根独立 GPIO,由卡直接连到 SoC 的中断控制器。前者省引脚,后者响应更确定。
用带内中断需要在 dts 里写cap-sdio-irq,然后驱动里注册回调:
static irqreturn_t demo_sdio_irq(struct sdio_func *func) { struct demo_dev *dev = sdio_get_drvdata(func); u8 status; int ret; /* 中断上下文里已持有 host 锁,不能调 sdio_claim_host */ status = sdio_readb(func, DEMO_REG_IRQ_STATUS, &ret); if (ret) return IRQ_NONE; if (status & DEMO_IRQ_RX_READY) demo_handle_rx(dev); if (status & DEMO_IRQ_TX_DONE) demo_handle_tx_done(dev); /* 写回清除中断标志,漏了会持续触发 */ sdio_writeb(func, status, DEMO_REG_IRQ_STATUS, &ret); return IRQ_HANDLED; } static int demo_irq_init(struct demo_dev *dev) { return sdio_claim_irq(dev->func, demo_sdio_irq); }这里有一条要命的规则:sdio_claim_irq注册的回调运行在已经持有 host 锁的上下文里,回调内部绝不能再调sdio_claim_host(),否则直接死锁,表现是系统卡死且没有任何 oops。上面代码里直接调sdio_readb是安全的,因为它在这个上下文里不需要再加锁。
另外,中断状态寄存器必须写清除。漏掉这一步的症状是中断风暴,CPU 占用 100%,但功能看起来还在跑,很容易被误判成性能问题。
5. 避坑与排查:从 CMD52 超时到中断不来的五条血泪记录
SDIO 的问题有个共同特征:现象都很像,原因差很远。下面五条是按"现象 → 原因 → 解决"整理的实测记录,基本覆盖了从枚举到传输再到中断的常见翻车点。
5.1 枚举阶段 CMD5 无响应
现象:上电后 dmesg 只有mmc1: card never left busy state或者干脆没有任何 SDIO 相关日志,/sys/bus/sdio/devices/是空的。
原因:三种可能。一是电源没起来或上电时序不对,模组的复位引脚还拉着;二是non-removable没写,内核按可插拔卡处理,探测时断电导致模组还没启动就被判定失败;三是时钟频率配置有问题,初始化阶段用了高速时钟。
解决:先用万用表确认模组供电和复位引脚电平,再看 dts 里non-removable、keep-power-in-suspend、vmmc-supply三个属性是否齐全。如果供电和配置都对,把max-frequency临时降到 400000 试一次,能枚举就说明是时序问题,再逐步往上调。枚举阶段的日志级别可以临时提高:
echo 8 > /proc/sys/kernel/printk dmesg -w | grep -i mmc5.2 probe 里读寄存器返回 -ETIMEDOUT
现象:驱动能绑定,probe 打印出进入日志,但第一个sdio_readb就返回-110(ETIMEDOUT),后面全部失败。
原因:最常见的是没调sdio_enable_func()。功能号未使能时,CMD52 对功能 1 之后地址空间的访问会被卡拒绝或直接无响应。其次是 CMD52 地址超出了该功能号实际实现的地址范围,卡不响应。
解决:检查 probe 里sdio_enable_func的调用和返回值,确认它在任何读写之前。然后对照寄存器手册核对地址,特别注意某些模组的寄存器基址是从 0x1000 开始的,直接写 0x00 访问的是 CCCR 而不是厂商寄存器。
5.3 中断回调里死锁
现象:功能跑起来后,第一次收到中断系统就卡死,看门狗复位,没有任何 oops 输出。
原因:中断回调里调用了sdio_claim_host()。中断上下文已经持有 host 锁,再次申请就是自死锁,而且因为是自旋锁,连 panic 都来不及打。
解决:回调里只使用不加锁的访问函数,把耗时处理推到工作队列。如果确实需要在回调里做批量传输,用schedule_work()丢给线程上下文,在线程里正常 claim/release。
5.4 大块传输 CRC 错误、数据错位
现象:小数据量读写完全正常,一旦单次传输超过几 KB 就开始报 CRC 错误,或者读回来的数据前半段对、后半段是垃圾。
原因:缓冲区不是 DMA 安全的,或者长度没按块大小对齐导致模式切换时边界错位。带 cache 的平台上,CPU 写进 kmalloc 缓冲区的数据可能还在 cache 里没落到内存,DMA 读到的是旧内容。
解决:批量缓冲区统一用dma_alloc_coherent()分配,或者在传输前用dma_map_single()做映射和 cache 维护。长度统一按块大小整数倍切分,不要图省事一次传不规则长度。
5.5 吞吐只有标称值的零头
现象:功能全对,但实测吞吐只有几 Mbps,远低于模组标称。
原因:三个高频原因。一是数据通路用了sdio_readsb/sdio_writesb(固定地址 FIFO 模式)去读内存映射区,所有数据都打在同一地址上;二是块大小设成了默认的 64 字节,没调sdio_set_block_size;三是数据线只用了 1 线模式,dts 里bus-width写的 1。
解决:按顺序排查——先确认bus-width是 4 或 8,再确认块大小设到 512,最后确认批量读写用的是sdio_memcpy_fromio/to。三样都对,吞吐通常能上到总线速率的六七成。
6. 二次开发:把驱动接到自研板子上的改造清单
拿到一份能跑的 SDIO 驱动,真正的工作量在适配自己的板子。按经验,改造点集中在四个位置,按依赖顺序排列如下:
| 改造点 | 位置 | 关键动作 |
|---|---|---|
| 身份匹配 | id_table | 从/sys/bus/sdio/devices/读出实际厂商号/设备号 |
| 总线参数 | 设备树 mmc 节点 | bus-width、max-frequency、non-removable对齐硬件 |
| 电源与复位 | regulator / GPIO | 确认上电时序与复位释放顺序,必要时加延时 |
| 中断方式 | dts +sdio_claim_irq | 带内中断加cap-sdio-irq,带外中断改 GPIO 号 |
改完之后不要急着跑业务,先做一次最小验证:只做enable_func+ 读版本寄存器 + 一次 512 字节的块读写回环,全部通过再往下走。这个最小验证能覆盖电源、时钟、块大小、DMA 对齐四个最容易出问题的环节,任何一环不通都在这里暴露,而不是等到网络栈跑起来才发现数据错乱。
验证代码可以直接写在 probe 末尾,用模块参数控制开关,避免污染正式流程:
static bool selftest; module_param(selftest, bool, 0644); static int demo_selftest(struct demo_dev *dev) { u8 *tx, *rx; int ret; tx = dma_alloc_coherent(dev->dev, 512, &dev->tx_dma, GFP_KERNEL); rx = dma_alloc_coherent(dev->dev, 512, &dev->rx_dma, GFP_KERNEL); if (!tx || !rx) return -ENOMEM; memset(tx, 0xA5, 512); ret = demo_bulk_write(dev, DEMO_REG_DATA, tx, 512); if (ret) goto out; ret = demo_bulk_read(dev, DEMO_REG_DATA, rx, 512); if (ret) goto out; /* 回环比对,任一个字节不同都说明通路有问题 */ if (memcmp(tx, rx, 512)) ret = -EIO; out: dma_free_coherent(dev->dev, 512, tx, dev->tx_dma); dma_free_coherent(dev->dev, 512, rx, dev->rx_dma); return ret; }把这套自检跑通之后,再往上接协议栈或字符设备接口就只是常规工作了。
最后说一个我自己的习惯。早年调一块模组,枚举、probe、寄存器读写全绿,唯独吞吐上不去,翻了两天代码才发现 dts 里bus-width是上一版硬件留下的 1。从那以后我每次接新板子,第一件事不是改驱动,而是先把 dts 里六个总线属性逐条对着原理图核一遍,核完再让自检跑三遍——冷启动一遍、热复位一遍、休眠唤醒后一遍。这三遍都能过,后面才有资格谈性能。希望帮到你。
本文还有配套的精品资源,点击获取