简介:CF卡与SD卡驱动源码资源,面向ARM架构Linux开发者与嵌入式驱动学习者,针对存储设备在视频解码等对传输速度和稳定性要求较高的场景中驱动移植与调试需求。压缩包共8个文件,包含5个C源码、2个头文件和1个Makefile,整体仅24KB,代码精简集中,覆盖MMC协议核心、CRC校验及平台插槽实现。源码涉及Linux内核块设备驱动模型、MMC子系统、SPI/IDE接口选择及设备树配置,可结合Makefile快速编译加载,并通过insmod或modprobe挂载测试。通过阅读MMC核心模块、CRC校验模块与磁盘驱动实现,开发者既能理解从命令发送、数据传送到块设备操作的完整链路,也能掌握平台相关驱动的适配方法和常见读写错误、设备无法识别的排查思路。目前已有620人学习下载,适合需要参考成熟驱动实现以优化存储性能的中高级嵌入式开发人员。
1. 从一份 SD.rar 说起:ARM Linux 下 CF/SD 驱动到底在折腾什么
很多做 ARM 嵌入式的人第一次接触 CF 卡和 SD 卡驱动,都是从老平台迁移开始的。手头一个基于 S3C2410 的项目,视频解码出来要落盘,板子上只有 CF 卡槽和 SD 卡槽,内核起来之后/dev下根本看不到mmcblk0。这时候你才发现,所谓“Linux 自带存储驱动”在 ARM 平台上是句空话——SD/MMC 协议栈、块设备层、板级 slot 控制代码,全得自己编进内核。这份 SD.rar 就是干这个用的,里面是 mmc_core、mmc_slot_s3c2410、crc、mmc_disk 这一整套可编译的驱动源码,加上一份 5.6SD 驱动使用实验,专门针对 ARM Linux 下 CF/SD 卡的识别、读写和调试。适合正在做视频存储、数据采集或者维护老 ARM 板卡的系统工程师,也适合刚入门想搞明白块设备驱动怎么落地的嵌入式开发者。
2. 源码结构拆解:MMC 协议栈、CRC 校验和块设备层各管哪一段
2.1 为什么 CF 卡和 SD 卡都挤在 MMC 协议栈里
先说一个很多人容易搞混的点:CF 卡有 IDE 和 SPI 两种模式,SD 卡走 MMC 协议,但这两类卡在内核驱动里经常共用同一套mmc_core框架。原因不复杂——SD 卡规范本身就是从 MMC 规范演化来的,命令集、响应格式、寄存器定义有大量重叠;而 CF 卡在 SPI 模式下,底层传输逻辑也和 MMC 的 SPI 模式兼容。所以你在老内核里会看到drivers/mmc目录下同时管着 MMC、SD 和部分 CF 卡,靠 card 识别阶段发的 ACMD41、CMD1 这类命令去区分卡的类型。
这份源码里的mmc_core.c就是协议栈的中枢。它不直接操作寄存器,而是维护一张卡的状态机:从上电后的初始态,到发送 CMD0 进入空闲态,再通过 CMD2 获取 CID、CMD3 分配 RCA,最后 CMD7 选中卡进入传输态。mmc_core-1.c从命名看应该是早期版本或参考实现,我一般拿它当对照——当新版 core 调试不过去时,翻一翻老版本的状态跳转逻辑,往往能定位到是哪一步命令时序被改动过。
2.2 mmc.h 和 crc.c:协议宏定义与校验是驱动最容易翻车的地方
mmc.h里定义的是整个协议层的命令字面量和数据结构,比如MMC_CMD_READ_SINGLE_BLOCK、MMC_CMD_WRITE_SINGLE_BLOCK、struct mmc_command和struct mmc_data。这块没什么玄学,纯粹是规范翻译,但要注意不同内核版本对struct mmc_command的字段定义有差异,比如flags字段从u32拆成unsigned int的位域,直接决定了你这包代码能不能在目标内核版本上编译过。
crc.c是很多人会忽略但绝对不能省的部分。MMC 协议里,命令和响应都要带 CRC7 校验,数据块在 SD 卡上要用 CRC16。协议规定 CRC7 的生成多项式是0x89(即x^7 + x^3 + 1),CRC16 用的则是标准 CCITT 多项式0x1021。给一个常见的 CRC7 实现片段:
unsigned char mmc_crc7(unsigned char *buf, int len) { unsigned char crc = 0; int i, bit; for (i = 0; i < len; i++) { for (bit = 0; bit < 8; bit++) { crc <<= 1; if ((buf[i] & (0x80 >> bit)) ^ (crc & 0x80)) crc ^= 0x09; /* 生成多项式 0x89 去掉最高位后的值 */ } } return crc | 1; /* 协议要求 CRC7 结果最低位置 1 */ }这段代码的关键点有两个。第一,crc ^= 0x09是用移位后的常数参与异或,等价于对多项式0x89做逐位模 2 除法,很多照着规范写的人容易把0x89整个写进去,算出来的校验值永远是错的。第二,返回值最低位强制写 1,这是 MMC 协议对 CRC7 传输格式的特殊要求,把校验值和结束位拼在一个字节里发出去。SD 卡对命令的 CRC 错误是静默丢弃的,表现在现象上就是发 CMD2 之后总线没有任何响应,而dmesg里只留下一条超时日志。我调试过的一块板子,就是这里没置位最后一位,卡死活不进识别流程,排查了大半天。
2.3 mmc_disk.c:从协议到 /dev/mmcblk0 的最后一公里
协议栈负责和卡对话,但用户程序读写文件最终走的是块设备层。mmc_disk.c做的事情就是注册一个gendisk设备,把协议层的读写请求转成request队列里的struct request,再映射到具体的 MMC 命令上。
这块代码最重要的两个参数是块大小和容量计算。mmc_disk.c里会从 CSD 寄存器读C_SIZE、C_SIZE_MULT、READ_BL_LEN字段,算出卡的扇区数和总容量。初始化时注册 512 字节逻辑块、设置队列逻辑块大小为 512 字节,调用set_capacity()写入设备容量。这里有个老内核常见的坑:READ_BL_LEN大于 9(即物理块大于 512 字节)时,驱动要左移READ_BL_LEN - 9位得到实际扇区数,有些移植代码把移位写反了,容量直接差 2 倍。
mmc_disk.c里还承担了写保护状态的管理。卡侧有WP引脚和状态寄存器两个来源,驱动通过mmc_card_read_only()判断是否只读,这个状态在挂载时直接决定mount -o rw是否报错。如果硬件上 WP 引脚悬空没接上拉,驱动会误判为写保护,表现为卡能识别、分区能看到,但一挂载就返回Read-only file system。
2.4 Makefile:这套源码怎么编成内核模块
压缩包里的Makefile走的是标准内核外部模块编译方式:
obj-m := mmc_core.o mmc_slot_s3c2410.o mmc_disk.o mmc_core-objs := mmc_core-1.c crc.c KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules注意mmc_core-objs这一行——mmc_core.o不是单文件编出来的,而是把mmc_core-1.c和crc.c链接成一个目标文件。这种写法在驱动拆分成多个源文件时很常见,但新手容易漏了-objs变量,直接把两个.c都写成obj-m的成员,结果编译出两个独立模块,mmc_core.ko什么也没编进去。KDIR指向的是目标平台的内核源码树,交叉编译时必须配合ARCH=arm和CROSS_COMPILE一起用,否则跑到 x86 的内核树里编出来一个vmlinux不认的模块,insmod时直接invalid module format。
3. 把驱动跑起来:配置内核、交叉编译、加载与挂载全流程
3.1 内核配置:哪些项不开,驱动编了也白编
把代码拿到手先别急着make,要先确认目标内核的drivers/mmc相关配置。MMC 子系统要能工作,至少得开以下配置项:
| 配置项 | 作用 | 选值 |
|---|---|---|
CONFIG_MMC | MMC/SD 协议栈主开关 | =y 或 =m |
CONFIG_MMC_BLOCK | 块设备层驱动,生成 /dev/mmcblkX | =y 或 =m |
CONFIG_MMC_S3C2410 | S3C2410 平台控制器驱动 | =m |
CONFIG_MMC_DEBUG | 协议层调试日志 | 建议 =y |
CONFIG_BLOCK | 块设备层基础 | =y |
如果CONFIG_MMC_BLOCK没开,协议栈仍然能识别卡,但/dev下永远不会出现mmcblk0,SD 卡对外表现的只有 sysfs 里的一个 mmc 设备节点。很多人在这一步排查半天,以为驱动没加载,其实cat /sys/bus/mmc/devices/里已经能看到设备了,卡在的是块设备注册环节。
CONFIG_MMC_DEBUG建议在开发阶段打开,它会把协议层的命令发送、响应状态、超时重试等信息打到dmesg里。内核的mmc/core/debug.c会通过pr_debug输出日志,运行时还需要echo 8 > /proc/sys/kernel/printk把调试级别拉起来,否则默认级别下pr_debug是不会出现在控制台和日志里的。我第一次调这块驱动时不知道这层关系,dmesg干干净净,以为是驱动没加载成功,白白浪费了两个小时。
3.2 交叉编译与加载:insmod 和 modprobe 的区别要分清
配置项确认之后,回到源码目录直接执行make,产出.ko文件。把这些.ko通过 NFS 或者 TFTP 传到目标板上,然后按顺序加载。加载顺序有讲究:先加载mmc_core.ko,再加载mmc_slot_s3c2410.ko,最后加载mmc_disk.ko。因为mmc_slot_s3c2410在 probe 阶段会调用mmc_alloc_host和mmc_add_host注册一个 host,而mmc_disk要等 card detect 触发后才真正开始干活。反过来先加载mmc_disk也没报错,但它会找不到已注册的 host,卡插入事件来了之后没有块设备响应。
# 目标板终端,按顺序执行 insmod mmc_core.ko insmod mmc_slot_s3c2410.ko insmod mmc_disk.ko # 查看内核是否识别到 MMC/SD 设备 dmesg | tail -n 20 # 如果使用了 modprobe 和 depmod depmod -a modprobe mmc_diskinsmod和modprobe的差别在于依赖处理。modprobe会读取modules.dep自动加载依赖模块,但前提是执行过depmod -a生成了依赖文件。我一般调试阶段全是insmod一条一条来,因为哪个模块加载失败、具体是什么原因,insmod的报错比modprobe的二次封装更直白。modprobe经常出现的情况是依赖解析正确、模块也加载了,但总线无响应,最后发现是依赖文件的路径指向了内核自带的旧版本模块——这种“加载了但加载的不是你的代码”的坑最难查。
3.3 验证识别结果:mmcblk0 出现之后,格式化与挂载才算真跑通
驱动加载成功后,dmesg里应该能看到完整的卡识别链路:
mmc0: new SD card at address 1234 mmcblk0: mmc0:1234 SD32G 29.7 GiB mmcblk0: p1第一行是协议层完成识别,第二行是mmc_disk注册块设备成功,第三行是分区扫描结果。注意mmcblk0后面的p1是分区,不是整卡设备。接下来格式化、挂载:
mkfs.vfat /dev/mmcblk0p1 mount -t vfat /dev/mmcblk0p1 /mnt/sd mount | grep mmcblk0格式化工具的选择这里有个现实问题。Windows 上用的 SD Memory Card Formatter 会把卡格式化成带隐藏保留区的结构,Linux 上mkfs.vfat不会动保留区。所以同一张卡在 Windows 格式化后插到 Linux 上,分区起始位置可能不是 2048 扇区,旧驱动按固定偏移扫描分区表可能识别不正常。如果遇到分区表解析失败,先fdisk -l /dev/mmcblk0看看实际的分区结构,而不是直接怀疑驱动。
挂载成功后用dd做一轮全卡读写验证,同时打开CONFIG_MMC_DEBUG观察命令时序是否异常。这套流程跑通,驱动的基本功能就算落地了——但后面还有更隐蔽的板级问题等着你。
4. S3C2410 平台接入:mmc_slot_s3c2410.c 里的中断、GPIO 和时序细节
4.1 platform_driver 的匹配逻辑:老内核没有设备树怎么办
mmc_slot_s3c2410.c是典型的老式platform_driver写法。在引入设备树之前,ARM 平台描述硬件的方式是arch/arm/mach-s3c2410/mach-smdk2410.c里静态定义的platform_device,包括寄存器地址、中断号、GPIO 配置,全部写死在 C 代码里。驱动侧要做的就是让自己的platform_driver和这个platform_device名字对上:
static struct platform_driver s3c2410_mmc_driver = { .probe = s3c2410_mmc_probe, .remove = s3c2410_mmc_remove, .driver = { .name = "s3c2410-mmc", .owner = THIS_MODULE, }, }; module_platform_driver(s3c2410_mmc_probe);module_platform_driver是一个封装宏,展开后就是module_init和module_exit,内部调用platform_driver_register注册驱动。匹配的关键是.driver.name,也就是"s3c2410-mmc"这个字符串,必须和板级代码里platform_device的name字段完全一致。这个串儿不统一是移植驱动时最常见的低端错误,但报错信息贼隐晦——驱动注册成功,probe 就是不执行,因为总线匹配没找到对应的设备。
设备树引入之后,匹配方式改成了compatible属性。drivers/mmc/host/dw_mmc.c这类新驱动会声明"samsung,s3c2410-mmc"这样的 compatible 列表,设备树节点里写compatible = "samsung,s3c2410-mmc"才能对上。老驱动往新内核上移植时,这一步是绕不过去的。
4.2 probe 函数拿到什么:资源映射和中断申请的必修课
probe 函数被执行后,驱动要完成四件事:映射寄存器地址、申请中断、初始化 GPIO、注册 mmc host。S3C2410 的 MMC 控制器通过 SDINIT/SDCMD/SDDAT 等寄存器组操作总线,寄存器基地址由platform_get_resource从设备资源里拿到:
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); s3c2410_mmc_base = ioremap(res->start, resource_size(res)); irq = platform_get_irq(pdev, 0); ret = request_irq(irq, s3c2410_mmc_irq, IRQF_TRIGGER_RISING, "s3c2410-mmc", host);ioremap拿到的是虚拟地址,之后的寄存器读写全部通过writel/readl在这块虚拟地址上进行。中断号在 S3C2410 上是 IRQ_S3C2410_MMC 这样的平台宏定义,但驱动代码里不应该写死,用platform_get_irq动态获取才是正确做法。request_irq注册的中断处理函数里要操作状态寄存器清除中断标志位,否则中断一直挂起,CPU 被中断风暴打满——表现为系统响应极慢,top里irq占用接近 100%。
GPIO 的初始化容易被忽略。S3C2410 的 MMC 数据线、命令线、时钟线需要复用为特殊功能引脚,板级代码通过s3c2410_gpio_cfgpin设置引脚功能。如果引脚复用配置不对,总线波形根本出不来,逻辑分析仪上看 CLK 始终为低,卡永远不会有响应。我遇到过的一种场景是内核里CONFIG_S3C2410_GPIO没开,引脚配置函数变成空操作,驱动一切正常就是不识别卡,查到最后是内核裁剪惹的祸。
4.3 卡检测机制:轮询还是中断,决定你的 sd 卡是不是“玄学”识别
SD/CF 卡座子上通常有一根卡检测引脚(CD#),插入时被拉低,拔出时恢复高电平。mmc_slot_s3c2410.c对这种 detection 有两种处理方式:轮询和中断。老内核里很多板子用轮询,驱动每 500ms 调一次mmc_detect_change检查 GPIO 状态。这种做法的缺点是插入到识别之间有最大 500ms 的延迟,而且如果 GPIO 状态读反了,会一直报卡移除:
static void s3c2410_mmc_detect(struct work_struct *work) { struct s3c2410_mmc_host *host; int status; status = gpio_get_value(host->pdata->det_pin); if (status == host->pdata->det_pin_invert) mmc_detect_change(host->mmc, 0); }核心参数是det_pin_invert。它表示卡插入时 GPIO 是低电平还是高电平,取决于板级电路设计。拉上拉到高、插入拉低,det_pin_invert就是 0;反过来插入拉高,就要设成 1。这个参数错了,驱动会把卡移除事件当成插入,把插入当成移除,dmesg里反复出现mmc0: card removed。所以拿到别人的驱动源码,第一件事是核对板子原理图上 CD 引脚接的是高有效还是低有效,不要想当然。
中断方式则是在 CD 引脚上注册 GPIO 中断,卡插入瞬间触发mmc_detect_change。代价是占一个系统中断号,但在低功耗场景下比轮询省电得多。新内核里都推荐中断方式,老驱动改造时加一段request_threaded_irq也不是很难,关键点是中断触发沿要匹配det_pin_invert的极性,沿设反了依然是反复插拔的假象。
5. 避坑与常见问题:CF/SD 驱动跑不起来的五类高频翻车现场
5.1 insmod 报“version magic”不匹配
现象:insmod mmc_core.ko直接失败,dmesg里看到version magic '5.4.0 ARM' should be '5.4.0-xyz ARM'。
原因:内核开启了CONFIG_MODVERSIONS和CONFIG_MODULE_FORCE_LOAD未开启时,模块的 vermagic 必须精确匹配当前运行内核。你的源码包是针对某个内核版本写的,而目标板跑的内核带了发行版后缀或者打了补丁,版本字符串不一致。
解决:在目标板的/lib/modules/$(uname -r)/build下重新编这个模块,而不是用宿主机 x86 内核树编。如果手头没有完全一致的内核源码树,用make modules_prepare生成 Module.symvers 后重编。我一般会在源码包根目录留一份build.sh,把KDIR和CROSS_COMPILE参数化,换平台只需要改两个变量。
5.2 卡识别成功但 /dev/mmcblk0 不出现
现象:dmesg里能看到mmc0: new SD card at address ...,但/dev下面没有mmcblk0,挂载无从谈起。
原因:CONFIG_MMC_BLOCK没开,或者mmc_disk.ko没有成功加载。前者是内核配置问题,后者是模块加载顺序问题。协议层和块设备层是两层,识别卡的工作由 core 层完成,生成块设备由mmc_disk完成,缺一环都不行。
解决:lsmod | grep mmc先看mmc_disk在不在。不在就insmod mmc_disk.ko。在的话查内核配置:zcat /proc/config.gz | grep MMC_BLOCK。老内核里也可以用grep MMC_BLOCK /boot/config-$(uname -r)。确认是=y还是=m,然后把对应配置改掉重编内核。这台设备上如果不方便重编内核,就用模块方式加载。
5.3 SD 卡容量显示 2 倍或减半
现象:插入 32GB 卡,fdisk -l显示只有 16GB,或者显示 64GB。
原因:mmc_disk.c解析 CSD 寄存器计算扇区数时移位逻辑出错。CSD 里C_SIZE和C_SIZE_MULT组合出总块数,计算公式是(C_SIZE + 1) * (1 << (C_SIZE_MULT + 2)),再乘以块大小得到字节容量。如果代码里把READ_BL_LEN的理解错位,扇区数和字节数之间换算就出偏差。
解决:打开CONFIG_MMC_DEBUG,通过dmesg查看打印出来的 CSD 原始值和计算过程,逐一核对C_SIZE、C_SIZE_MULT、READ_BL_LEN三个字段。对照 SD 卡 Physical Spec 2.0 的 CSD 寄存器定义表,检查移位位数。我调过的一块板子就是READ_BL_LEN为 11(2048 字节物理块),代码却按 512 字节算扇区数,容量直接变成 4 倍,修改后恢复正常。
5.4 挂载报 Read-only file system
现象:mount -t vfat /dev/mmcblk0p1 /mnt/sd失败,dmesg里没有明显的 I/O 错误,但mount返回只读。
原因:mmc_card_read_only()函数返回了真。两个来源:CSD 寄存器的PERM_WRITE_PROTECT位被置位,或者硬件 WP 引脚电平被误判。前者是卡本身出厂设置,后者是板级电路问题。
解决:拿一张确定没有写保护的卡交叉验证,排除卡的问题。然后检查 WP 引脚 GPIO 配置,用gpio_get_value手动读一次电平,跟原理图对照。如果 WP 引脚悬空且没有上拉/下拉电阻,电平处于不定态,驱动读到的值不稳定,时好时坏——这种硬件问题无解,只能在驱动里把mmc_card_read_only强制改写为 0 绕过,前提是系统设计上不需要硬件写保护。
5.5 数据读写 CRC 错误频繁
现象:大文件拷贝时卡死,dmesg刷mmc0: error -EILSEQ或mmc0: CRC error。
原因:总线时序不满足 SD 卡规格。常见诱因是时钟频率太高,S3C2410 的 MMC 控制器跑 48MHz 主频时分频配置不对,实际 CLK 超过卡的 25MHz 上限。另一个常见原因是数据线布线太长,信号完整性恶化。
解决:把时钟频率降档试,SD 卡标准模式最高 25MHz,高速模式才到 50MHz。控制器寄存器里通过SDIPRE分频寄存器配置 CLK,公式是CLK = PCLK / (SDIPRE + 1)。老内核里的mmc_slot_s3c2410.c初始化代码会写这个寄存器,把分频值调大,时钟砍到 12.5MHz 再看 CRC 错误是否消失。如果消失,基本锁定是时序裕量不足,再考虑调 I/O 驱动强度或者换卡测试,而不是盲目改驱动代码。
6. 用 5.6SD 驱动使用实验做一次靠谱的读写吞吐验证
驱动能识别、能挂载,只是及格线。对视频解码落盘这种场景,还得证明它能在持续读写下不掉链子。源码包里那份“5.6SD驱动使用实验”就是干这个用的,我会拿它当模板,加一段自己的验证脚本跑吞吐和稳定性。
先做一轮全卡顺序写,把脏数据填满,再看读取速度和校验一致性:
#!/bin/bash # 驱动压测脚本:顺序写 -> 回读校验 -> 吞吐统计 DEV=/dev/mmcblk0p1 MNT=/mnt/sdtest mkdir -p $MNT mount -t vfat $DEV $MNT # 1. 顺序写 1GB,block size 1MB,统计平均速度 dd if=/dev/urandom of=$MNT/test.img bs=1M count=1024 conv=fsync 2>&1 | tail -n 1 # 2. 落盘后 sync 一次,确保写缓存全部刷进卡里 sync # 3. 回读并计算 md5,同时统计读取速度 dd if=$MNT/test.img of=/dev/null bs=1M count=1024 2>&1 | tail -n 1 md5sum $MNT/test.img # 4. 随机读 100 次,每次 4KB,模拟视频解码时的索引查询 time (for i in $(seq 1 100); do dd if=$MNT/test.img of=/dev/null bs=4K count=1 skip=$((RANDOM % 262144)) 2>/dev/null done) umount $MNT脚本里的conv=fsync很关键,它强制dd在写完每个块后调fsync,避免数据还滞留在页缓存里就报告完成。视频解码场景你最怕的就是写了一半掉电,全卡文件系统损坏。md5sum那一步是校验数据一致性,顺序写之后必须回读比对。随机读那一段模拟的是解码头索引的跳跃访问——视频流的包头和关键帧位置是随机分布的,dd skip配合$RANDOM能粗略模拟。
我从这轮实验里学到的教训是:驱动编译的内核版本必须和目标板运行版本严格一致,加载顺序必须按 core、slot、disk 来,CRC 校验实现的细节一处都不能想当然。从那以后我每移植一次这套源码,都强制走一遍“版本核对、配置确认、卡检测 GPIO 极性、dmesg 逐行核对”的流程,先把板级问题排除干净,再碰协议层的代码。这套驱动源码的价值不在代码本身多精巧,而是它能让你在一个完整的 MMC 协议栈上摸清 ARM Linux 驱动从 probe 到块设备落地的每一环。希望这轮拆解能帮你的平台少走几步弯路。
本文还有配套的精品资源,点击获取