
写 Linux SPI 驱动写到第七篇终于轮到 class_register。很多人在 SPI 驱动里见过 class_create、device_create但很少单独去抠 class_register。其实把这块弄明白之后再回头看 spidev、iio、usb gadget 这些驱动的注册流程都会通透很多。这篇我就从 SPI 驱动的实际需求出发把 class_register 是什么、放在哪里、怎么写、踩过哪些坑一次讲清楚顺便给出一个可以直接参考的自定义 SPI 驱动骨架。1. class_register 到底是干什么的别把它和 SPI 注册混在一起1.1 设备模型里 class 的角色Linux 设备模型里有两套完全不同的“亲人关系”总线关系SPI 设备挂在 SPI 总线上由spi_bus_type管理通过spi_register_driver注册驱动。用户空间访问关系设备要能被用户程序使用需要/dev节点和/sys/class下的目录这依赖 class 机制。class 本质上是内核向用户态提供的一套“分类目录”。注册成功以后你在/sys/class/下就能看到对应的子目录每个子目录代表一类设备。比如/sys/class/spidev/这个目录管理着所有通过/dev/spidevX.Y访问的 SPI 设备。往 class 里添加一个设备实例后会生成/sys/class/类名/设备名/这样的路径里面有dev文件记录主次设备号还有uevent文件触发热插拔通知。用户态的 udev 或 mdev 收到事件后读取dev文件里的设备号在/dev下创建访问节点。这个流程就是“内核注册设备 → 用户态生成节点”的标准链路。class_register 就是这条链路最底层的注册入口。它做的核心事情是把传入的struct class注册到内核设备模型让这个 class 进入/sys/class同时准备好后续device_create所需的框架。1.2 class_register 和 class_create 的差别很多人以为class_register和class_create是两个不相关的东西其实class_create只是class_register的一个便利封装。class_create内部会先kzalloc分配一个临时的struct class填好名字和 owner再调用class_register完成注册最后返回这个分配出来的指针。所以你看内核源码时经常看到demo_class class_create(THIS_MODULE, spi_demo);而class_register更适合静态定义的 class或者把 class 嵌入某个私有结构体里的场景static struct class demo_class { .name spi_demo, .class_release demo_class_release, }; ret class_register(demo_class);两者的清理接口也不一样class_create创建出来的用class_destroy清理class_register注册的静态 class用class_unregister清理。如果混用代码看起来能跑但一旦涉及模块卸载和重复加载很容易出问题。操作返回值使用场景对应清理class_register(cls)int 错误码class 结构体已静态定义class_unregister(cls)class_create(THIS_MODULE, name)struct class *可能是 ERR_PTR动态分配并注册class_destroy(cls)这里有一个老手常犯的错误class_create返回的是错误指针而不是 NULL判断失败时不要写if (!cls)要写if (IS_ERR(cls))。这和cdev_add、spi_register_driver的错误处理不太一样新手很容易抄错。1.3 在 SPI 驱动里class 真正解决的是什么一个标准 SPI 从设备驱动通常只做这两件事通过spi_driver里的probe拿到struct spi_device *。对spi_device做spi_read/spi_write/spi_transfer。这套流程并不需要 class。真正需要 class 的是“把这个 SPI 设备暴露给用户空间”的场景。举个例子你想让用户从/dev/my_spi_dev0读写 SPI 数据或者想通过 sysfs 属性动态调整 SPI 时钟频率。这时候光有 spi_driver 是不够的你还得申请设备号、注册字符设备、注册 class、创建设备节点。device_create负责把一个设备实例挂到某个 class 下但它要求 class 必须已经注册成功。class_register 就是那个“先把房间装修好再往房间里摆物品”的准备工作。所以你可以这样理解SPI 子系统负责让内核找到并控制设备class 机制负责让用户空间找到并访问设备。两者走的是设备模型两条不同分支但最终在struct device上汇合。2. 自己写一个带 class_register 的 SPI 驱动2.1 设备树侧SPI 从设备节点怎么写开始驱动之前先保证 SPI 从设备能被总线枚举出来。用一个最常见的设备树编写方式spi0 { status okay; spi_demo70 { compatible vendor,spi-demo7; reg 0; spi-max-frequency 2000000; }; };说明几个字段reg 0表示使用 SPI 控制器的第 0 个片选也就是 CS0。spi-max-frequency是 SPI 时钟频率最大值驱动里执行spi_setup时会参考这个值。compatible必须和驱动里of_device_id的 compatible 一致。如果你的板子用 GPIO 模拟片选可以加cs-gpios属性。片选有两种实现方式硬件控制器自动拉片选或者用 GPIO 在 begin/end 消息时手动拉低拉高。reg决定选第几个 CScs-gpios决定这个 CS 实际接到哪个 GPIO两者是配合关系不是互斥关系。设备树写好后先确认/sys/bus/spi/devices/下能看到类似spi0.0的目录。没有这个目录说明 SPI 控制器自身没起来或者设备树节点没被解析到后面 probe 根本不会触发。2.2 驱动侧spi_driver 骨架SPI 驱动本身不大。先写一个最精简的骨架#include linux/module.h #include linux/spi/spi.h static struct spi_device_id demo_spi_id[] { { spi-demo7, 0 }, { }, }; MODULE_DEVICE_TABLE(spi, demo_spi_id); static const struct of_device_id demo_spi_of_match[] { { .compatible vendor,spi-demo7 }, { }, }; MODULE_DEVICE_TABLE(of, demo_spi_of_match); static int demo_spi_probe(struct spi_device *spi) { dev_info(spi-dev, probe spi_demo7\n); spi-mode SPI_MODE_0; spi-bits_per_word 8; spi_setup(spi); return 0; } static int demo_spi_remove(struct spi_device *spi) { dev_info(spi-dev, remove spi_demo7\n); return 0; } static struct spi_driver demo_spi_driver { .driver { .name spi_demo7, .of_match_table demo_spi_of_match, }, .id_table demo_spi_id, .probe demo_spi_probe, .remove demo_spi_remove, }; module_spi_driver(demo_spi_driver); MODULE_LICENSE(GPL);这里有几件事要补充说明第一.id_table不是必须的但强烈建议加。设备树匹配走的是.of_match_table而老式板级代码或驱动绑定流程可能会走 SPI 设备 ID 表。加上MODULE_DEVICE_TABLE(spi, demo_spi_id)还能在模块自动加载时生成 spi alias方便 modprobe 识别。第二spi_setup必须在 probe 阶段调用一次。它会重新计算和校验 SPI 控制器的时钟、模式、位宽。忘记调的话后面spi_transfer可能会用默认值导致和从设备握手失败。第三module_spi_driver这个宏会自动生成 init/exit 函数里面只做了spi_register_driver。这看起来很省事但如果你还需要注册 class 和字符设备就不能直接用这个宏了得拆成手动的module_init和module_exit。2.3 类注册与设备节点生成现在把 class 和字符设备一起加进去。先用静态 class 的方式演示class_registerstatic dev_t demo_devno; static struct cdev demo_cdev; static DEFINE_IDA(demo_ida); static void demo_class_release(struct class *cls) { pr_info(spi_demo class released\n); } static struct class demo_class { .name spi_demo, .class_release demo_class_release, };模块入口函数需要做的事比较多static int __init demo_init(void) { int ret; ret alloc_chrdev_region(demo_devno, 0, 8, spi_demo); if (ret 0) return ret; cdev_init(demo_cdev, demo_fops); ret cdev_add(demo_cdev, demo_devno, 8); if (ret 0) goto err_cdev; ret class_register(demo_class); if (ret 0) goto err_class; ret spi_register_driver(demo_spi_driver); if (ret 0) goto err_spi; return 0; err_spi: class_unregister(demo_class); err_class: cdev_del(demo_cdev); err_cdev: unregister_chrdev_region(demo_devno, 8); return ret; }对应的 exit 函数要反序清理static void __exit demo_exit(void) { spi_unregister_driver(demo_spi_driver); class_unregister(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(demo_devno, 8); ida_destroy(demo_ida); } module_init(demo_init); module_exit(demo_exit);这里有一个顺序原则注册时class 必须在device_create之前但可以在字符设备之后注销时必须先把所有device_destroy做完再class_unregister最后cdev_del和unregister_chrdev_region。顺序乱了最常见的问题就是rmmod时 sysfs 里还残留设备报“设备忙”或者内核栈回溯一片红。alloc_chrdev_region申请了 8 个次设备号所以 probe 里要为每个 SPI 设备分配一个次设备号static int demo_spi_probe(struct spi_device *spi) { struct demo_dev *dd; int minor; minor ida_alloc(demo_ida, GFP_KERNEL); if (minor 0 || minor 8) { if (minor 0) ida_free(demo_ida, minor); return -ENODEV; } dd kzalloc(sizeof(*dd), GFP_KERNEL); if (!dd) { ida_free(demo_ida, minor); return -ENOMEM; } dd-spi spi; dd-minor minor; spi_set_drvdata(spi, dd); device_create(demo_class, spi-dev, MKDEV(MAJOR(demo_devno), minor), dd, spi_demo%d, minor); return 0; }需要注意device_create的返回值。如果失败会返回ERR_PTR需要清理之前的ida分配和内存然后把错误码传回去。spi_demo%d是设备节点名最终会体现在两层地方一个是/sys/class/spi_demo/spi_demo0一个是/dev/spi_demo0。这个名字最好和 class 名字有一定对应关系否则排错的时候容易找不到。2.4 给设备加 sysfs 属性class 注册成功、设备节点创建成功之后通常还想加一两个 sysfs 属性。比如让用户看到当前设置的 SPI 时钟或者手动调整某个参数。用DEVICE_ATTR_RO加一个只读属性static ssize_t demo_rate_show(struct device *dev, struct device_attribute *attr, char *buf) { struct demo_dev *dd dev_get_drvdata(dev); if (!dd || !dd-spi) return -EINVAL; return sysfs_emit(buf, %d\n, dd-spi-max_speed_hz); } static DEVICE_ATTR_RO(demo_rate);在device_create之后调用device_create_filestruct device *ddev; ddev device_create(demo_class, spi-dev, MKDEV(MAJOR(demo_devno), minor), dd, spi_demo%d, minor); if (IS_ERR(ddev)) { ida_free(demo_ida, minor); kfree(dd); return PTR_ERR(ddev); } ret device_create_file(ddev, dev_attr_demo_rate); if (ret 0) { device_destroy(demo_class, MKDEV(MAJOR(demo_devno), minor)); ida_free(demo_ida, minor); kfree(dd); return ret; }在 remove 函数里对应调用device_remove_file避免卸载模块之后 sysfs 里还挂着无效属性。3. 加载和验证怎么判断 class 注册是否真的生效3.1 模块加载后应该看到的文件先把模块编译好格式参考obj-m spi_demo7.o得到spi_demo7.ko后加载insmod spi_demo7.ko正常情况dmesg 里应该能看到 probe 日志demo spi_demo7 spi0.0: probe spi_demo7然后检查 sysfsls /sys/class/spi_demo/ spi_demo0再检查设备节点ls -l /dev/spi_demo0如果系统启用了 devtmpfs/dev/spi_demo0通常会在device_create时直接创建。如果用的是 udev节点则依赖规则。但dev文件一定存在cat /sys/class/spi_demo/spi_demo0/dev 247:0这个247:0就是主设备号和次设备号。节点创建失败时可以用它手动建mknod /dev/spi_demo0 c 247 0第三步可以查看 SPI 总线上有没有绑定ls -l /sys/bus/spi/devices/spi0.0/driver如果显示驱动符号链接存在说明 spi_driver 和 spi_device 已经成功匹配。这个匹配关系和 class 是否注册没关系但它决定 probe 是否执行所以必须优先排查。3.2 /dev 节点没生成按顺序排查我遇到过很多次“sysfs 有目录/dev 没有节点”的情况。这不是驱动没注册 class而是用户态那一层出了问题。排查顺序建议固定下来看/sys/class/spi_demo/spi_demo0/dev是否存在。如果存在说明 class 和 device 都注册成功了问题在用户态节点创建。如果/dev用的是 devtmpfs先确认 devtmpfs 挂载路径有权限。如果用的是 udev查看/dev/下设备节点的 owner 和 permission 规则。如果用到的是 busybox mdev需要在/etc/mdev.conf里配置规则并确认/proc/sys/kernel/hotplug指向/sbin/mdev。很多时候设备树里 SPI 从设备没挂上probe 没执行自然没有节点。先去看/sys/bus/spi/devices/下有没有spi0.0。没有的话要返回设备树和 SPI 控制器驱动去查不要死磕 class 注册。3.3 class_register 常见失败原因与退出顺序class_register返回负数时常见原因-EEXIST/sys/class/下已经存在同名 class。模块重复加载或者别的驱动注册了同名类。解决办法是把 class 名字改得更具体比如用厂商前缀。-EINVAL传入的struct class为空或者name字段为空。静态初始化的 class 很容易出现忘记填.name的情况。内存分配失败少见一般出现在系统内存极度紧张时。还有一个比较隐蔽的坑静态 class 注册时不要省略class_release。省掉它内核 kobject 释放时没有回调卸载时可能出现奇怪的告警。哪怕回调函数啥也不干只打一条日志也比没有强。退出顺序方面我再强调一次先spi_unregister_driver确保之后不会再有新的 probe 进来。再逐个device_destroy销毁挂在 class 下的设备。然后class_unregister。最后cdev_del和unregister_chrdev_region。这个顺序能避免“设备还在使用 classclass 先没了”的问题。4. 设备模型视角下的扩展看完这层再看 spidev 就简单了4.1 bus / device / driver / class 四者关系设备模型里四个核心对象经常被搞混bus负责连接 device 和 driverSPI 总线本身由内核 SPI 子系统初始化不需要你写。device物理或逻辑存在SPI 从设备就是struct spi_device。driver具体操作硬件也就是struct spi_driver。class面向用户的分类接口把上面这堆和用户空间对接。可以这么类比SPI 总线是城市道路spi_device是路口的路灯spi_driver是这个路灯的维护工人class 是市政服务窗口。市民不需要知道路灯由哪个工位维护只需要知道去服务窗口办事。这个服务窗口就是/dev和/sys/class。class_register 之所以值得单独讲是因为它把“内核设备”和“用户空间入口”之间的桥搭起来了。排名靠前的设备节点、sysfs 属性、热插拔事件都建立在这座桥上。4.2 模块自动加载时的 id_table 与 of_match_table很多人的 SPI 模块能 insmod但放到开机自动加载时不生效。原因是MODULE_DEVICE_TABLE没有写全。设备树匹配时内核靠of_match_table生成 modalias比如of:Nspi_demo7TNULLCvendor,spi-demo7。如果写了MODULE_DEVICE_TABLE(of, demo_spi_of_match)udev 在发现设备时可以根据 modalias 自动 modprobe。SPI ID 表也类似提供另一种匹配路径。两个都写上兼容性最好static struct spi_device_id demo_spi_id[] { { spi-demo7, 0 }, { }, }; MODULE_DEVICE_TABLE(spi, demo_spi_id);注意设备树 compatible 和spi_device_id的字符串不要写成一样的顺序。of_match_table里用的是vendor,spi-demo7这种带厂商前缀的写法spi_device_id里可以用spi-demo7。它们面向不同匹配机制不是必须完全相同。4.3 调试和代码审查的小建议最后分享几个我实际排查时的习惯第一加载失败不要只看 “insmod 失败” 一句话。先dmesg | tail错误码一定按顺序追。class_register 返回 -17 就是 -EEXIST去/sys/class/找同名目录基本秒解决。第二写 sysfs 属性的时候不要在demo_rate_show里直接访问可能释放的内存。dev_get_drvdata(dev)拿到的 data 是device_create传进去的 dd在device_destroy之前都要保证它的生命周期有效。我见过有人把 dd 换成container_of来取结果 class 和设备模型没有销毁模块卸载时访问悬空指针非常难查。第三probe 失败要记得释放已经分配的资源。probe 函数里可能有这三步分配设备号、注册 cdev、注册 class。如果第 3 步失败前面两步的清理绝对不能省。Linux 驱动最恶心的 bug 不是“注册失败”而是“注册失败后资源还被占着下次重试又失败”。第四SPI 驱动里如果只需要一个延迟发送之类的测试设备不一定要写字符设备和 class。直接注册spi_driver用 debugfs 下发读写也能达到目的。在我个人实际项目里class 的最大价值出现在产品化阶段需要稳定设备节点、需要 udev 规则、需要多实例管理。如果只是调板用 spidev 或者 debugfs 反而更轻。做驱动时间长了就会发现class_register本身不复杂但它牵出来的设备模型、sysfs、热插拔链路才是真正需要花时间啃的部分。把这一层理解透以后看 IIO、看 ALSA、看 USB class driver都会比别人快很多。