简介:面向嵌入式驱动开发学习者的SDIO驱动资料包,聚焦SDIO协议基础、驱动分层、设备探测与初始化、数据传输、中断处理及Linux MMC框架下的注册流程,适合需要接入Wi-Fi、蓝牙、GPS等SDIO设备的中高级开发者,也可供课程设计与项目开发参考。压缩包共23个文件,约1.3MB,以协议规范PDF、项目总结文档、C/H源码、Keil/IAR工程文件以及原理图和PCB设计资料为主,兼顾理论查阅与工程实践。已有669人学习下载,具备一定参考热度。资料围绕FSEL功能选择、命令响应、DMA与中断传输、错误恢复、低功耗和并发同步等环节展开,并指向Linux下示例驱动的学习路径,便于读者对照规范梳理驱动结构、搭建调试环境、排查总线通信与中断问题,形成可复用的开发思路。同时保留规范文档与工程输出,方便按模块核对配置参数与编译结果。
1. 板子上了电,dmesg 里死活不出现 new SDIO card
前段时间在调一块焊死的 SDIO 模块,硬件说供电正常、复位也拉了,结果上电后/dev底下什么都没有,dmesg 里mmc1翻了半天只报一句初始化超时。我把这份 sdio 驱动程序的源码拆开对照,才发现问题根本不在驱动代码,而是在 host 侧的 pwrseq 时序和 CIS 里的 vendor/device id 上——驱动本身写得没问题,是我把SDIO_DEVICE()的参数填成了芯片型号。这套代码值得读的地方,是它把 SDIO 从协议层到驱动层串成了一条完整的链路:CMD52 单字节寄存器访问、CMD53 块传输、sdio_driver注册与 probe 时序、中断线程化拆分,再到 sysfs 调试接口,一层都没跳过。适合已经写过字符设备、想往 mmc 子系统和嵌入式 BSP 方向深入的开发者;如果你只是想要个能跑的 demo,也能照着第 3 章直接编译加载。
2. 协议与框架:一笔数据从 CMD52 到 CMD53 走了哪几层
SDIO 的坑有一半出在概念混淆上:很多人以为 SDIO 就是“SD 卡加了个中断脚”,于是拿 SD 存储卡的经验去套,结果在功能号、块大小、CIS 这几处反复翻车。这一章先把协议侧和内核侧的结构对齐,后面写代码时你才知道每一行调用落在了哪一层。
2.1 CMD52 与 CMD53:慢速寄存器口和高速数据口
SDIO 在 SD 总线的命令集上扩展了两条命令。CMD52 是单字节读写,用来访问每个功能的寄存器空间,特点是慢但简单,一次搬运一个字节,还要带地址;CMD53 是批量读写,支持字节模式和块模式,FIFO 数据、固件下载、大包收发都走它。
一张 SDIO 卡最多有 7 个功能(function 1~7),另外还有一个所有卡都有的 function 0,叫 CIA(Common I/O Area),里面放的是公共控制寄存器。功能自己的寄存器区叫 FBR(Function Basic Register),每个功能占 0x100 字节。常用的几个位置几乎是所有 SDIO 驱动都会碰的:
| 寄存器 | 地址 | 作用 |
|---|---|---|
| SDIO_CCCR_IOEx | 0x02 | 功能使能位,第 n 位置 1 表示使能 function n |
| SDIO_CCCR_IENx | 0x04 | 中断使能,配合 IENM 使用 |
| SDIO_CCCR_IF | 0x07 | 总线宽度、块模式/字节模式选择 |
| SDIO_CCCR_BLKSIZE | 0x10 | 功能 0 的块大小(低字节) |
| FBR + 0x00 | 0x100×n | 功能 n 的使能状态与块大小 |
这里有个反直觉的点:块大小在 CCCR 里也有一份,FBR 里也有一份。很多人只调了sdio_set_block_size()就以为完事,实际固件那边读的是 FBR 的值,两边不一致时会出现“发得出去、收不回来”的玄学现象。常见做法是设完块大小后立刻回读一遍 FBR 校验。
2.2 内核里的四层:host 控制器、core、sdio bus、功能驱动
Linux 的 mmc 子系统把 SDIO 拆得比想象中干净。最底下是 host 控制器驱动,各家 SoC 一套,负责发命令、搬 DMA;往上是mmc_core,处理卡的初始化、枚举、电源管理;再往上是sdio_bus,把每个功能抽象成一个struct sdio_func设备挂到总线上;最上面才是你我要写的功能驱动,也就是struct sdio_driver。
这个分层带来的直接好处是:功能驱动里看不到任何寄存器地址,也不需要知道 host 是哪个 SoC。你能用的接口就那几个——sdio_readb、sdio_writeb、sdio_readsb、sdio_writesb、sdio_memcpy_fromio、sdio_memcpy_toio、sdio_claim_irq。驱动结构和 platform 驱动几乎一模一样,只是probe的参数换成了struct sdio_func *。
/* 功能驱动要填的东西:id 表 + probe/remove 回调 */ static const struct sdio_device_id demo_ids[] = { { SDIO_DEVICE(0x02df, 0x9135) }, /* vendor, device 来自 CIS */ { /* 必须以空项结尾 */ } }; MODULE_DEVICE_TABLE(sdio, demo_ids); static struct sdio_driver demo_driver = { .name = "demo_sdio", .id_table = demo_ids, .probe = demo_probe, .remove = demo_remove, }; module_sdio_driver(demo_driver);SDIO_DEVICE(vendor, device)这两个参数必须是 CIS 里读出来的 16 位 MANFID 和 PRODID,不是数据手册封面上的芯片型号。module_sdio_driver()这个宏展开后就是module_init/module_exit加注册注销,和module_platform_driver()一个套路。设备端不用自己创建总线,sdio_bus是内核启动时就注册好的。
2.3 枚举是谁做的:从设备树到 sdio_func
很多人以为 SDIO 设备要在设备树里写一个节点,驱动才会被匹配。实际上枚举是 mmc core 自己做的:host 控制器探测到卡之后,mmc_attach_sdio()会读 CIS、算出这张卡有几个功能、每个功能的 vendor/device id 是多少,然后对每个功能调sdio_add_func()注册成设备。设备树节点的作用只有一个——给这个已经创建出来的功能设备绑上of_node,好让驱动能用of_match_table或者从节点里取 GPIO、时钟这些板级资源。
/* 板级 dts:给焊死的 SDIO 设备准备 host 控制器 */ &mmc1 { status = "okay"; bus-width = <4>; non-removable; /* 焊在板上,没有插拔检测 */ keep-power-in-suspend; cap-sdio-irq; /* 允许 SDIO 中断走 DAT1 */ mmc-pwrseq = <&sdio_pwrseq>; vmmc-supply = <&vcc_3v3>; /* 子节点的 reg 就是功能号,不是片选 */ demo_card: demo@1 { reg = <1>; compatible = "demo,demo1000"; }; }; &sdio_pwrseq { reset-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>; post-power-on-delay-ms = <50>; /* 复位释放到第一条命令的间隔 */ };non-removable是焊死模块的标配,缺了它 core 会不停做卡检测,日志里刷一堆 rescan。cap-sdio-irq决定 DAT1 中断能不能用,这个不写,后面sdio_claim_irq()直接返回-EINVAL。子节点的reg = <1>对应 function 1,#address-cells/#size-cells在某些内核版本上要求父节点必须声明,否则子节点解析会失败——这也是个容易被忽略的小坑。
3. 最小可跑骨架:一个 sdio_driver 从注册到 probe 的完整代码
协议理清之后,落地其实很快。这一章按“文件放哪 → id 表怎么填 → probe 里按什么顺序做事 → host 侧还要准备什么”的顺序走一遍,代码可以直接抄进你的工程改。
3.1 目录与构建:把驱动塞进内核树的哪个位置
我的习惯是先做成树外模块,调通了再决定要不要进内核树。树外模块的好处是改一行make一下,不用整编内核。目录结构就三个文件:
demo_sdio/ ├── Makefile ├── Kconfig # 进内核树时才需要 └── demo_sdio.c# 树外编译:make -C /lib/modules/$(uname -r)/build M=$PWD modules obj-m += demo_sdio.o KDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) cleanKDIR指向当前运行内核的构建目录,交叉编译时换成目标板的 kernel build 路径即可,同时要带上ARCH和CROSS_COMPILE。如果之后要进内核树,Kconfig里加一个tristate "Demo SDIO device driver",depends on MMC,再把它加进drivers/misc/Kconfig和对应的Makefile。这一步不着急,先把树外跑通。
提示:交叉编译时
KDIR一定要指向目标内核的 build 目录,而不是宿主机的。指错了会出现“模块版本魔数不匹配”,insmod直接拒绝加载。
3.2 id_table 与注册:先让 probe 被调起来
写驱动最怕的就是“代码全对,probe 不进来”。所以第一步不是写初始化,而是先把 id 表填对、把注册打出来,看内核有没有认这个设备。
#include <linux/module.h> #include <linux/mmc/sdio_func.h> #include <linux/mmc/sdio_ids.h> struct demo_card { struct sdio_func *func; struct work_struct rx_work; u8 *rx_buf; u8 irq_status; }; /* 这两个值必须和 CIS 里的一致,改成芯片型号是匹配不上的 */ #define DEMO_VENDOR_ID 0x02df #define DEMO_DEVICE_ID 0x9135 static const struct sdio_device_id demo_ids[] = { { SDIO_DEVICE(DEMO_VENDOR_ID, DEMO_DEVICE_ID) }, { } }; MODULE_DEVICE_TABLE(sdio, demo_ids);模块加载后,/sys/bus/sdio/devices/下会列出形如mmc1:0001:1的目录,名字里的0001是 vendor,最后的1是功能号。进去cat vendor和cat device,把这两个十六进制值抄回来填进SDIO_DEVICE(),匹配基本就稳了。
# 板子上执行,确认功能设备已经枚举出来 ls /sys/bus/sdio/devices/ # mmc1:0001:1 mmc1:0001:2 cat /sys/bus/sdio/devices/mmc1:0001:1/vendor cat /sys/bus/sdio/devices/mmc1:0001:1/device如果/sys/bus/sdio/devices/是空的,别急着改驱动——那是枚举阶段就没成功,问题在 host 侧供电或时序,回头去看第 5 章第一条。
3.3 probe 的正确顺序:使能功能、占用 host、设块大小、申请中断
probe里最容易出错的是顺序和错误路径。少了sdio_enable_func()就去读寄存器,读到的是全 0xff;sdio_claim_host()和sdio_release_host()没配对,host 被锁住,整张卡后面的访问全部卡死。
static int demo_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct demo_card *card; int ret; /* 先打印 CIS 里的真实 id,方便核对 */ dev_info(&func->dev, "demo probe: vendor=%04x device=%04x func=%u\n", func->vendor, func->device, func->num); card = devm_kzalloc(&func->dev, sizeof(*card), GFP_KERNEL); if (!card) return -ENOMEM; card->func = func; card->rx_buf = devm_kmalloc(&func->dev, 4096, GFP_KERNEL); if (!card->rx_buf) return -ENOMEM; sdio_set_drvdata(func, card); /* 一串需要原子完成的时序,整段占住 host */ sdio_claim_host(func); ret = sdio_enable_func(func); /* 置位 CCCR IOEx */ if (ret) { dev_err(&func->dev, "enable func failed: %d\n", ret); goto err_release_host; } ret = sdio_set_block_size(func, 512); /* 块模式的块大小 */ if (ret) { dev_err(&func->dev, "set block size failed: %d\n", ret); goto err_disable_func; } /* 厂商私有的初始化序列放这里:写配置、下载固件、开中断掩码 */ ret = demo_hw_init(func); if (ret) goto err_disable_func; sdio_release_host(func); INIT_WORK(&card->rx_work, demo_rx_work); /* 申请中断:内部自己会占一次 host,别放在 claimed 区间里 */ ret = sdio_claim_irq(func, demo_irq); if (ret) { dev_err(&func->dev, "claim irq failed: %d\n", ret); goto err_release_host_after; } return 0; err_disable_func: sdio_disable_func(func); err_release_host: sdio_release_host(func); err_release_host_after: return ret; }这段代码有三个细节值得说。sdio_enable_func()失败通常意味着这个功能在卡上就不存在,或者 CIS 里的功能号写错了,先回ls /sys/bus/sdio/devices/核对功能号。sdio_set_block_size(func, 512)的第二个参数必须是 2 的幂且不超过 host 支持的上限,func->cur_blksize里能看到最终生效的值,传 0 的话内核会用一个默认值。错误路径用了三段 goto,分别对应“已 enable”“已 claim”“都已做”三种状态,这种分层写法看着啰嗦,但漏掉任何一次sdio_release_host(),debug 的时候你会想砸键盘。
sdio_claim_irq()之所以放到release_host之后,是因为它内部会自己占一次 host,放在已占用的区间里就变成自己等自己。这是典型的黑匣子式死锁,现象是 insmod 之后系统整个卡住,连串口都没输出。
3.4 host 侧还要准备什么:上电时序和 32k 时钟
驱动代码没问题但 probe 不进来,九成是这一节的事。焊死的 SDIO 模块通常需要三样东西同时到位:3.3V 主供电、复位脚释放、32.768kHz 低速时钟(部分模块用于睡眠唤醒)。
主供电和复位用mmc-pwrseq-simple描述,代码见 2.3 的 dts。这里的关键是post-power-on-delay-ms,很多模块手册上写的是“复位释放后 10ms 可用”,实测往往要 30~50ms 才稳,给少了就是随机枚举失败,换个板子又能跑,非常迷惑。
32k 时钟一般是 host 控制器的一个输出脚,需要在 pinctrl 里配置成低功耗模式,并且确认mmc1节点上开了对应属性。有些 SoC 把它叫mmc-clkgate-delay,有些叫keep-power-in-suspend配套的低速时钟使能,具体看 host 驱动。这部分是纯粹的平台差异,我的经验是直接找 SoC 厂商提供的同型号参考 dts 对照,比自己啃手册快得多。
4. 数据面实现:寄存器封装、块传输和中断拆分
probe 通了之后就到了真正干活的部分。SDIO 驱动的数据面其实就三件事:怎么安全地读写寄存器、怎么高效地搬大块数据、中断来了之后在哪里做耗时处理。
4.1 寄存器读写封装:别在业务代码里裸调 sdio_readb
裸调sdio_readb()的问题是错误码没地方放。它的返回值是读到的字节,读写失败时返回 0xff,错误码只能通过最后一个指针参数带回。业务代码里到处写int ret显然不现实,所以我的习惯是包一层:
/* 返回负数表示失败,成功时把值写进 *val */ static int demo_reg_read(struct demo_card *card, unsigned int addr, u8 *val) { int ret = 0; *val = sdio_readb(card->func, addr, &ret); if (ret) dev_dbg(&card->func->dev, "read %#x failed: %d\n", addr, ret); return ret; } static int demo_reg_write(struct demo_card *card, unsigned int addr, u8 val) { int ret = 0; sdio_writeb(card->func, val, addr, &ret); return ret; }封装之后,if (demo_reg_read(card, REG_CHIP_ID, &id))这种写法就清爽了,错误码也统一进dev_dbg。addr是功能内的偏移,不用自己加SDIO_FBR_BASE(),内核在sdio_readb里已经按func->num处理过了。要读 function 0 的 CCCR,需要用sdio_func[0]那个特殊的 func,但一般不建议驱动直接碰,交给 core 管。
顺手加一个 sysfs 属性,调试时不用改代码重编模块:
static ssize_t chip_id_show(struct device *dev, struct device_attribute *attr, char *buf) { struct sdio_func *func = dev_to_sdio_func(dev); u8 id; int ret; id = sdio_readb(func, DEMO_REG_CHIP_ID, &ret); if (ret) return ret; return sysfs_emit(buf, "0x%02x\n", id); } static DEVICE_ATTR_RO(chip_id);dev_to_sdio_func()把设备指针还原成sdio_func,sysfs_emit()是新内核里替代sprintf的写法,会自己处理缓冲区边界。属性在 probe 里用device_create_file(&func->dev, &dev_attr_chip_id)创建,remove 里对应删掉。之后cat /sys/bus/sdio/devices/mmc1:0001:1/chip_id就能读到芯片 ID,验证通信是否正常最直接的办法就是这个。
4.2 块传输:块大小、块数上限和缓冲区对齐
大块数据走sdio_readsb/sdio_writesb,它们内部用的是 CMD53。这里有两个硬约束:一次 CMD53 最多 511 个块,字节模式单次最多 512 字节;缓冲区如果给的是栈上地址或者 vmalloc 出来的非连续内存,走 DMA 的 host 会直接报错或者搬出错乱数据。
#define DEMO_FIFO_REG 0x20 #define DEMO_MAX_BLOCKS 64 /* 自己限一个安全值,别贴着 511 */ static int demo_read_fifo(struct demo_card *card, u8 *buf, int len) { struct sdio_func *func = card->func; int blocks, ret; if (len <= 0 || len % func->cur_blksize) return -EINVAL; blocks = len / func->cur_blksize; if (blocks > DEMO_MAX_BLOCKS) return -EINVAL; /* buf 必须是 kmalloc + DMA 可访问的内存 */ ret = sdio_readsb(func, buf, DEMO_FIFO_REG, len); if (ret) dev_err(&func->dev, "fifo read %d bytes failed: %d\n", len, ret); return ret; }func->cur_blksize是上一步sdio_set_block_size()生效后的值,直接拿来算块数最稳妥。sdio_readsb的第四个参数是字节数,是块大小整数倍时内核自动走块模式,不是整数倍就退回字节模式——后者效率差很多,所以缓冲长度最好按块对齐。缓冲区用devm_kmalloc()分配的普通内存就够,物理连续,DMA 能直接用;如果非要用自己管理的环形缓冲,记得用dma_map_single()映射一遍再传进来。
对于字节数不是块大小整数倍的零碎读取,常见做法是先算sdio_align_size(func, size)拿到对齐后的长度再传,避免内核在内部做一次 bounce buffer 拷贝——那层拷贝不会报错,但会悄悄吃掉性能。
4.3 中断拆分:回调里只做三件事
SDIO 中断有个和普通外设很不一样的地方:它不是硬中断上下文。mmc core 为每张卡起了一个sdio_irq_thread内核线程,你的回调是在这个线程里被调用的,所以可以睡眠、可以读寄存器,不用担心原子上下文那一套限制。
但有个前提必须先说清楚:回调进来的时候 host 已经被这个线程占住了,你在里面再调一次sdio_claim_host()就是自己等自己,系统直接挂死。这是我在这个项目里踩得最深的一脚。
static void demo_irq(struct sdio_func *func) { struct demo_card *card = sdio_get_drvdata(func); u8 status; int ret; /* 进来时 host 已被 mmc 的 irq 线程持有,这里不能再 claim */ status = sdio_readb(func, DEMO_REG_IRQ_STATUS, &ret); if (ret || !status) return; /* 写 1 清中断,位定义看模块手册,别照抄别的芯片 */ sdio_writeb(func, status, DEMO_REG_IRQ_STATUS, &ret); card->irq_status |= status; if (status & DEMO_IRQ_RX_READY) schedule_work(&card->rx_work); /* 搬运丢给工作队列 */ }回调里做的事情严格控制在“读状态、清中断、唤醒工作队列”三件。真正的 FIFO 搬运放到demo_rx_work里做,因为一次读 4KB 可能要几百微秒到几毫秒,占着 irq 线程会挡住后续中断的处理,表现出来就是高负载下丢包。工作队列里第一件事是sdio_claim_host(),搬完sdio_release_host(),中间不要有msleep()之类的长延时。
sdio_claim_irq(func, demo_irq)是在 probe 里申请的,remove 里必须sdio_release_irq(func)再sdio_disable_func(func),顺序反了会出现“中断还在来、功能已经关了”的竞态。很多人在这里翻车,因为 rmmod 之后系统不一定立刻崩,等你再 insmod 一次就炸了。
5. 避坑与排查:枚举不到、probe 不进、传输超时的现场处理
这几条都是我在实际板子上遇到的,按出现的阶段分了组。每条都写清现象、原因和解决办法,你遇到类似症状可以直接对号入座。
5.1 枚举阶段:设备根本没出现在 sysfs 里
现象:dmesg 里反复刷mmc1: error -110 whilst initialising SDIO card,/sys/bus/sdio/devices/是空的,insmod之后 probe 一次都没进。
原因:-110是 ETIMEDOUT,说明 CMD5 发出去没有响应。最常见的是复位脚释放太早或者供电还没稳,其次是 32k 低速时钟没给。还有一种情况是bus-width配成 4 位但硬件只接了 1 位,命令阶段能过、数据阶段超时。
解决:先把post-power-on-delay-ms从 50 加到 200 试一次,能过说明就是时序问题,再慢慢往下调回最优值。然后抓一眼/sys/kernel/debug/mmc1/ios,确认clock和bus width是否符合预期。如果 ios 里时钟是 400kHz 一直没上去,那是 host 侧识别失败,跟驱动无关,回去查 pinctrl 和供电。
5.2 probe 阶段:设备在,但驱动匹配不上
现象:/sys/bus/sdio/devices/mmc1:0001:1存在,但dmesg里没有任何demo probe的打印,lsmod显示模块已加载。
原因:SDIO_DEVICE()里的 vendor/device 填错了。这个值来自 CIS,和芯片型号、和厂商名字的缩写都没有必然关系,凭印象填几乎必错。
解决:先cat /sys/bus/sdio/devices/mmc1:0001:1/vendor和cat .../device,把真实值抄回去。如果设备树里写了compatible = "demo,demo1000",也可以在驱动里加一份of_match_table,sdio_bus_match会依次尝试 id 表、设备树、ACPI 三种匹配方式。查匹配过程可以开echo 8 > /proc/sys/kernel/printk看驱动核心的调试输出。
5.3 probe 阶段:insmod 之后整个系统卡住
现象:insmod demo_sdio.ko敲下去,串口没输出,shell 也不响应,只能断电重启。
原因:sdio_claim_host()和sdio_release_host()没有配对。典型写法是中途return -EIO直接返回,把 host 永久锁住了;或者在已经 claim 的区间里调了sdio_claim_irq(),内部又去 claim 一次。
解决:probe 里所有错误路径统一走 goto 分层释放,不要图省事直接 return。sdio_claim_irq()挪到sdio_release_host()之后调用。调试期可以打开内核的CONFIG_PROVE_LOCKING,虽然查不出 mmc host 这种自定义锁,但能顺手发现别的问题。
注意:host 被锁死之后重启模块是没用的,因为锁在
mmc_host结构体里,必须重新加载 host 驱动或者直接重启。所以这类 bug 一定要在第一次写的时候就避免。
5.4 传输阶段:CMD53 报 -84 或 -110
现象:寄存器读写都正常,一旦开始搬 FIFO 就报-84(EILSEQ)或者-110,偶尔能成功一次。
原因:三个可能。块大小和 FBR 里报的不一致;单次请求的块数超过 511;传入的缓冲区不是 DMA 可访问的连续内存。-84通常是数据线 CRC 错,多半是总线宽度配错或者信号完整性问题;-110是没响应,多半是参数越界。
解决:先用demo_read_fifo只读一个块(512 字节)验证通路,通了再往上加。缓冲区改用devm_kmalloc()分配。如果单块也失败,把bus-width临时改成 1 位试试,1 位能过 4 位不过,就是硬件布线或者上拉电阻的问题。
5.5 中断阶段:中断风暴或者一个中断都收不到
现象:sdio_claim_irq()返回 0,但回调一次都没进;或者进了之后疯狂刷屏,CPU 占满。
原因:收不到中断一般是 CCCR 里的IENM/IENx没打开,或者设备树的cap-sdio-irq没写。中断风暴则是回调里没清干净状态位——只清了主状态没清子状态,中断线一直拉着。
解决:cap-sdio-irq补上,sdio_claim_irq()之后回读SDIO_CCCR_IENx确认。风暴的话,把状态寄存器每一位都打出来,看看是不是有个位一直置着,然后按手册逐位清。有些模块要求先清子状态再清主状态,顺序反了也会反复触发。
6. 二次开发:把这个骨架改成你自己的 SDIO 设备驱动
拿到这份代码之后,真正要改的地方其实就四处,用一张表说清楚比逐行讲更快:
| 位置 | 改什么 | 怎么验证 |
|---|---|---|
demo_ids[] | vendor/device 换成你设备 CIS 里的值 | cat /sys/bus/sdio/devices/*/vendor |
demo_hw_init() | 换成你的上电初始化序列 | probe 日志里加返回值打印 |
DEMO_FIFO_REG等宏 | 换成你模块的寄存器偏移 | sysfs 属性读一遍对照手册 |
demo_irq() | 换成你的中断状态位和清除方式 | 用echo 1 > /sys/.../irq_test手动触发 |
改完之后,最省事的验证手段不是接调试器,而是把内核的 mmc 跟踪点打开,看每条命令的 opcode、参数和返回值:
# 看 mmc 子系统的命令级 trace cd /sys/kernel/debug/tracing echo 1 > events/mmc/enable echo 1 > tracing_on cat trace_pipe | grep -E "CMD52 <p> <a href="https://download.csdn.net/download/qcmc123/2698232" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>