首页 / 资讯中心 / 文章详情

Linux设备驱动模型解析:从kobject到platform总线与字符设备实战

Linux设备驱动模型解析:从kobject到platform总线与字符设备实战 ★ FEATURED ARTICLE
做过Linux驱动的人大概都有这种感觉刚接触时觉得驱动开发就是配置寄存器、填结构体、注册接口等真去读内核源码又会被kobject、kset、bus_type、platform_driver这一串概念搞得晕头转向。网上讲驱动的文章很多但要么直接把代码贴出来让你照着抄要么一上来就甩出几百行的struct xxx定义读者根本不知道这些东西为什么要存在、彼此之间是什么关系。这篇文章是Linux设备驱动模型原理、架构与开发系列的第一篇。我想换个讲法——不按代码顺序讲而是按要解决什么问题→内核设计了什么机制→驱动里怎么用的逻辑把Linux设备模型这套东西拆开揉碎讲清楚。内容不追求让你马上能写出一个驱动而是先建立一个完整的认知框架设备模型为什么存在、底层由什么构成、总线在中间扮演什么角色、一个字符设备驱动的标准骨架是什么。之后几篇再往里填具体的子系统细节和高级玩法。1. 为什么 Linux 需要一套设备模型而不是直接操作寄存器很多从单片机转过来的朋友最初都会有一个疑问我在裸机上点个灯、读个按键直接操作GPIO寄存器就完事了为什么到了Linux里非要搞出设备驱动总线这么多抽象概念1.1 先从裸机开发说起驱动在简单语境下就是操作寄存器裸机开发里确实不存在设备模型这回事。你的代码拿到一份芯片手册找到GPIO控制器的基地址往寄存器里写配置值再写输出值灯就亮了。整个过程非常直接因为硬件是确定的、系统是单任务的、软件是直接操作物理地址的。但Linux不一样。Linux要面对的环境极其复杂同一个内核需要跑在几百种SoC上支持几万个外部设备设备可以在系统运行过程中热插拔比如USB设备和SD卡多个驱动可能通过复杂依赖关系共享同一个硬件资源系统还有电源管理、休眠唤醒、能耗优化这些需求。如果每个驱动都直接写寄存器那同一个驱动在A芯片上能用换个B芯片就得全改整个内核会变成一团乱麻。所以Linux内核的做法是把硬件控制逻辑和硬件本身解耦。驱动不再面向某个具体的寄存器编程而是面向某种类型的能力编程比如我是以太网驱动我是串口驱动我是I2C控制器驱动。至于这个能力具体落在哪块硬件上、地址是多少、中断号是多少由系统通过设备信息来告诉驱动。这样驱动代码只写一遍就可以适应任何带有相同外设接口的硬件。1.2 设备、驱动、总线模型解决的三方匹配问题在这个解耦思路下内核引入了三个核心角色设备device描述系统里有哪些硬件包括它的类型、资源内存地址、中断号、GPIO、电源域归属、厂商兼容信息等驱动driver描述谁会处理这类硬件声明自己支持哪些厂商/型号提供init和exit方法总线bus充当设备和驱动之间的中介负责把合适的设备和合适的驱动配对起来配对成功就调用驱动的探测函数。可以把这三者的关系类比成招聘市场设备是岗位需求写明要什么人、提供什么资源驱动是求职者写明自己擅长什么、能干什么总线就是招聘平台负责把合适的求职者匹配到合适的岗位上。平台不会关心求职者具体写了多少行代码它只负责匹配这件事。1.3 设备模型到底带来了什么实际好处有了这套模型内核获得了几项纯寄存器操作完全做不到的能力第一是热插拔支持。USB设备插上的瞬间总线发现新设备立刻去寻找一个能处理它的驱动找到就触发加载流程。U盘能即插即用正是设备和驱动动态匹配的结果。第二是用户态可见的可视化。设备模型会通过sysfs文件系统把所有设备、驱动、总线的关系展现出来/sys/bus/usb/devices、/sys/class/net、/sys/block这些目录都是模型生成的。用户可以像逛文件系统一样参观硬件拓扑。第三是电源管理。设备模型把设备组织成树状结构挂到同一父设备的子设备可以统一进行电源域管理内核可以按照依赖关系逐层休眠或唤醒这在嵌入式设备的低功耗设计中极其重要。接下来我会一层一层拆开这套架构。从最底层的kobject讲起再到设备、驱动、总线三大结构体最后演示一个完整的字符设备驱动开发过程。2. 设备模型的底层基石kobject / kset / sysfs很多初学者在设备模型这里卡住往往是因为一头撞上了kobject。往前翻几页还在理解device和driver往后一翻发现内核里还有更底层的kobject和kset瞬间懵了既然有了device为什么还要kobject这两个东西是什么关系2.1 kobject 到底是什么设备模型里对象的最小单元简单说kobject是Linux设备模型中所有对象的基类。它就是给被管理的对象提供一套公共能力包括引用计数跟踪对象被引用了几次防止还在使用的对象被提前释放名称管理每个对象有个名字用于在sysfs中创建目录或文件父子关系对象可以组织成层次结构sysfs对接对象可以在sysfs中显示出来或接收用户态传来的数据。你在内核代码里看到的struct device其第一个成员通常是struct kobject kobj;这就是继承。device本质上是一个扩展了kobject能力的对象。没有kobject内核无法统一管理设备的生命周期、无法在sysfs中自动生成节点、无法实现用户态与内核态之间的属性交互。// include/linux/kobject.h 中 kobject 结构体的核心字段简化 struct kobject { const char *name; // 对象名称sysfs中的目录名 struct kobject *parent; // 父对象形成层次结构 struct kset *kset; // 所属的对象集合 struct kobj_type *ktype; // 对象类型定义属性操作和释放函数 struct kref kref; // 引用计数 ... };建议把kobject理解为可以被放入内核对象管理框架的小零件。它本身不代表任何具体的硬件语义只是提供管理能力。真正的语义由device、driver、bus这些外层结构赋予。2.2 kset 与 ktype对对象进行分组和统一定义kset和ktype是kobject的两个重要搭档。kset是一组同类kobject的集合。比如所有块设备所有网络设备分别属于不同的kset。kset的作用是提供一个归属的容器让内核能遍历某类所有对象。ktype定义了这个对象里属性文件的读写方式以及释放函数。属性文件就是你在/sys下看到的那些文件比如/sys/class/net/eth0/address。ktype决定这些文件的show/store回调由谁来实现。可以把kobject比作一个人kset比作这个人所在的班级ktype比作这个人遵循的规章制度。规章制度决定了这个人怎么展示自己、怎么被考核、走的时候怎么办理离职手续。2.3 sysfs 从哪里冒出来的用户态是怎么看到设备模型的sysfs是一个基于内存的虚拟文件系统通常挂载在/sys目录。内核在注册device、driver、bus时会自动在sysfs中生成对应目录项。所以你在shell里执行ls /sys/bus/i2c/devices/ ls /sys/class/gpio/ cat /sys/class/net/eth0/address看到的顶层目录结构反过来就是内核设备模型在用户态的一面镜子。sysfs里有一个非常经典的结构——/sys/bus/xxx/devices、/sys/bus/xxx/drivers。bus目录下产生的符号链接会告诉你哪个设备被哪个驱动绑定driver目录里能看到设备的链接哪个驱动支持哪些设备。系统管理员排查为什么我的设备没有驱动时第一个要看的就是这里。2.4 引用计数与生命周期为什么驱动不能随便 freekobject的引用计数机制是所有设备生命周期管理的底层基础。内核的规则是谁拿到对象的使用权谁就负责增加引用计数用完之后必须减少引用计数。当引用计数归零时对象才真正执行释放逻辑即ktype-release()。这个机制对驱动开发的影响非常直接。比如你写了一个字符设备驱动在驱动的probe函数里创建了一个device对象。如果用户在某个瞬间正打开着/dev/xxx节点读取数据此时驱动程序模块被卸载你总不能在exit函数里匆匆忙忙地kfree掉这个还没被释放的设备结构吧一旦用户态还在操作立刻就会踩到已释放的内存。正确的做法是管理器持有驱动模块的引用计数设备节点打开期间file_operations里的open回调会额外增加引用计数release再减少。只有引用计数归零设备对象才会安全释放。这个机制避免了驱动开发者手写大量运行状态判断代码把生命周期交给内核统一管理。3. 从 device / driver / bus 到 platform 总线一套真正能跑的框架理解了kobject之后我们再上到设备模型这一层。这一层的核心就是三个结构体struct device、struct device_driver和struct bus_type。它们和设备树、各种外设子系统共同构成了Linux设备管理的骨架。3.1 struct device 与 struct device_driver两个主角struct device是设备模型对某个具体硬件设备的描述。它不关心硬件怎么工作只负责记录这个设备是谁、在哪里、有哪些资源、挂在哪个总线上。// include/linux/device.h 中 struct device 的关键字段简化 struct device { struct kobject kobj; // 内嵌kobject用于sysfs管理 struct device *parent; // 父设备硬件拓扑中的上层 struct bus_type *bus; // 挂在哪条总线上 struct device_driver *driver; // 绑定到哪个驱动 void *platform_data; // 平台数据老式板级信息常用 struct device_node *of_node; // 设备树节点 dev_t devt; // 设备号字符设备用得最多 struct class *class; // 属于哪个设备类 ... };struct device_driver则描述驱动的能力和接口。它不仅有name、owner、of_match_table这些标识性字段还提供了probe、remove、suspend、resume等回调函数。驱动真正的干活动作几乎都在probe函数里完成。// include/linux/device.h 中 struct device_driver 的关键字段简化 struct device_driver { const char *name; // 驱动名 struct bus_type *bus; // 面向哪种总线 unsigned int owner; // 模块所有者 int (*probe) (struct device *dev); int (*remove) (struct device *dev); void (*shutdown) (struct device *dev); int (*suspend) (struct device *dev, ...); int (*resume) (struct device *dev); const struct of_device_id *of_match_table; // 设备树匹配表 ... };这里要特别提醒新手理解一个区别device描述硬件driver描述软件能力二者不是包含关系而是配对关系。同一个设备在系统启动时只有一个但可能有多个驱动都能匹配它内核会选择最合适的一个绑定。绑定的过程就发生在总线层。3.2 struct bus_type匹配、探测、热插拔的枢纽struct bus_type是整个匹配机制的中枢。它提供了一组回调和方法其中最核心的是match函数// include/linux/device.h 中 struct bus_type 的核心字段简化 struct bus_type { const char *name; // 总线名 int (*match)(struct device *dev, struct device_driver *drv); int (*probe)(struct device *dev); int (*remove)(struct device *dev); ... };内核在设备注册或驱动注册时会触发总线去执行遍历和匹配把新设备拿去和总线上的所有驱动比较或者把新驱动拿去和所有设备比较。match函数返回真值时说明匹配成功紧接着就调用驱动的probe函数。这个过程与我们后面要讲的platform总线、I2C总线、USB总线、PCI总线都通用。不同的总线只是实现了不同风格的match逻辑总线类型典型的 match 策略典型设备例子platform设备树 compatible 属性 / platform_device 名称SoC内部外设、GPIO控制器I2C设备树 compatible 属性 / I2C 从设备地址温湿度传感器、EEPROMSPI设备树 compatible 属性 / SPI 设备IDFlash芯片、触摸屏控制器USBvid/pid 匹配 / 设备类匹配键盘、U盘、摄像头PCIPCI 厂商ID/设备ID网卡、显卡、NVMe 控制器3.3 platform 总线SoC 内部外设的虚拟总线第一次听到platform总线的朋友往往会疑惑它对应哪个物理总线答案是大多数情况下没有物理对应。platform是内核为片上系统里那些不挂在传统物理总线上、但需要统一管理的设备而设计的一条虚拟总线。SoC里的GPIO控制器、DMA控制器、硬件定时器、以太网MAC、I2C控制器它们的地址空间通常是SoC内部直接映射的不经过PCI、USB这类标准总线。但设备模型要求所有设备都必须挂在某条总线上总不能让它们无家可归。于是内核把这类设备统一挂到platform总线上对应结构是platform_device和platform_driver。在设备树时代平台设备大多由设备树节点自动生成。比如一个设备树里写着serialfe001000 { compatible vendor,my-uart; reg 0xfe001000 0x1000; interrupts 33; };内核启动时就会为这个节点创建一个platform_device总线会用compatible vendor,my-uart去匹配驱动里of_match_table中的条目。匹配成功驱动probe被调用串口驱动开始干活。3.4 设备树与 platform_device 的关系这是最容易让新手混乱的点。很多人在网上看到的示例代码里probe函数会调用of_property_read_u32去读设备树属性于是产生疑问设备树和platform_device到底谁先谁后流程是这样的设备树是静态的硬件描述platform_device是运行时由设备树生成的对象。内核在启动早期解析设备树遍历每个带compatible属性的节点逐个创建platform_device。之后把platform_device注册到platform总线上触发设备和驱动的匹配。也就是说你在设备树里写reg 0xfe001000 0x1000内核就会在注册platform_device时自动把这段地址存进platform_device的resource数组里。驱动probe通过platform_get_resource()就能拿到完全不需要驱动自己从设备树解析地址。有朋友会问那为什么probe里还经常用of_property_read_*系列函数因为设备树里除reg、interrupts这些标准属性会被内核自动解析外大量自定义属性比如某个传感器的触发频率、方向极性配置需要驱动自己读取。这些自定义属性没有统一套路只能由驱动主动去设备树节点里取。4. 手写一个字符设备驱动从零到 /dev 节点原理讲了半天总得动手写点东西。下面我们走一遍最标准的字符设备驱动开发流程。这个例子不针对任何真实硬件纯粹为了演示设备模型的运作方式——我把它叫作模拟项目X。你要实现的功能非常简单用户态open这个设备随后write写入什么read就能读回什么。硬件部分用一个全局数组充当寄存器区仅用于演示。4.1 驱动入口的标准动作module_init 并不是一切每个驱动模块都需要声明出入口。Linux模块的加载入口用module_init和module_exit宏来标记// demo.c - 模拟项目X一个最简字符设备驱动 #include linux/module.h #include linux/init.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEMO_BUF_SIZE 256 static int demo_major; // 主设备号动态分配 static struct cdev demo_cdev; // cdev对象 static struct class *demo_class; // 设备类用于自动创建设备节点 static unsigned char demo_buffer[DEMO_BUF_SIZE]; // 内核态缓冲区模拟硬件寄存器区 static int demo_open(struct inode *inode, struct file *filp) { return 0; } static int demo_release(struct inode *inode, struct file *filp) { return 0; } static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { size_t len count DEMO_BUF_SIZE ? DEMO_BUF_SIZE : count; if (*offset len) return 0; if (copy_to_user(buf, demo_buffer, len)) return -EFAULT; *offset len; return len; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { size_t len count DEMO_BUF_SIZE ? DEMO_BUF_SIZE : count; if (copy_from_user(demo_buffer, buf, len)) return -EFAULT; *offset len; return len; } static const struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .release demo_release, .read demo_read, .write demo_write, }; static int __init demo_init(void) { dev_t devno; int ret; // 1. 动态分配设备号 ret alloc_chrdev_region(devno, 0, 1, demo_dev); if (ret 0) { pr_err(demo: failed to alloc chrdev region\n); return ret; } demo_major MAJOR(devno); // 2. 初始化cdev并添加到内核 cdev_init(demo_cdev, demo_fops); demo_cdev.owner THIS_MODULE; ret cdev_add(demo_cdev, devno, 1); if (ret 0) { unregister_chrdev_region(devno, 1); return ret; } // 3. 创建class和设备生成/dev节点 demo_class class_create(demo_class); if (IS_ERR(demo_class)) { cdev_del(demo_cdev); unregister_chrdev_region(devno, 1); return PTR_ERR(demo_class); } device_create(demo_class, NULL, devno, NULL, demo_device); pr_info(demo: initialized, major%d\n, demo_major); return 0; } static void __exit demo_exit(void) { dev_t devno MKDEV(demo_major, 0); device_destroy(demo_class, devno); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(devno, 1); pr_info(demo: exited\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Some Developer); MODULE_DESCRIPTION(A minimal char device demo for learning device model);这段代码虽然不长但它概括了字符设备驱动开发里的所有标准动作。下面我把每一步单独拆开讲。4.2 字符设备注册alloc_chrdev_region 与 cdev_add字符设备在内核里有设备号的概念。设备号分为主设备号和次设备号主设备号标识驱动类型次设备号标识同一驱动下的不同设备实例。alloc_chrdev_region()动态申请一段设备号避免手动指定主设备号的冲突问题。拿到设备号后第二步是cdev_init()和cdev_add()。cdev可以理解为字符设备在内核中的化身它内嵌了一个kobject注册之后就会出现在sysfs中。cdev_add把cdev和指定的设备号绑定起来此后内核知道这个设备号上的操作由demo_fops处理。为何不直接用register_chrdev()这个老接口因为老接口一注册就占用0到255全部次设备号而cdev的分段注册方式更灵活尤其是你有多个同类型设备时每个设备可以占独立的次设备号区间。4.3 file_operations 与真正的业务逻辑struct file_operations是字符设备驱动面向应用层的接口表。用户在用户态对设备文件执行open、read、write、ioctl等系统调用时内核会把调用转到这里对应的回调函数。注意代码里demo_read和demo_write都用了copy_to_user和copy_from_user这是内核开发中最基础的跨特权级拷贝手段。内核态不能直接访问用户态指针反之亦然。如果直接memcpy轻则数据异常重则导致内核崩溃——因为用户态指针可能指向一个尚未被映射的地址内核没有做访问合法性检查。这在驱动开发中非常容易踩坑也是新人代码最常见的bug来源之一。static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { size_t len count DEMO_BUF_SIZE ? DEMO_BUF_SIZE : count; if (copy_from_user(demo_buffer, buf, len)) return -EFAULT; *offset len; return len; }细心的读者会发现我的demo_read里在copy_to_user之前就判断了*offset len这是为了支持用cat /dev/demo_device这种工具时能正常读到EOF退出。否则read永远返回正数cat就会一直循环读直到卡死或OOM。4.4 probe 里该干什么、不该干什么上面的演示驱动没有probe因为它压根没有挂到总线上。但在真实的平台驱动开发中probe才是主角。驱动匹配成功后的所有初始化工作都放这里。一个合格的probe函数应该做这几件事获取资源调用platform_get_resource()获取设备树中描述的寄存器地址、中断号等申请并映射地址用devm_ioremap_resource()把物理地址映射成内核虚拟地址之后驱动才能用指针操作寄存器建立中断处理如果是中断驱动的设备需要调用request_irq()或devm_request_irq()注册中断回调创建设备节点注册字符设备、创建class、创建设备文件让用户态能够访问初始化硬件状态比如设置GPIO方向、初始化外设工作模式。要注意的是probe里的资源申请尽量使用devm_前缀系列函数。这类带devm_的接口会自动跟踪资源生命周期在驱动卸载或probe失败时自动释放大幅降低内存泄漏和资源泄漏的概率。你手写的每一个kzalloc在之后的所有错误分支里都得多写一个kfree稍不注意就漏。4.5 编译与加载Makefile、insmod、dmesg驱动的编译方式与普通程序不同不能直接用gcc必须借助内核提供的Kbuild系统。一个典型的Makefile长这样# Makefile for demo module obj-m : demo.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean然后在源码目录执行make会生成demo.ko。加载和卸载sudo insmod demo.ko ls -l /dev/demo_device echo hello | sudo tee /dev/demo_device sudo cat /dev/demo_device sudo rmmod demo注意看dmesg的输出demo: initialized, majorx会打印出来卸载时也有demo: exited。驱动调试不像应用调试可以打日志随时看内核的printk/pr_info输出就是你的主要调试手段。5. 开发路上最容易踩的几个坑驱动开发的经验很多时候是从踩坑里学来的。以下这几个场景是我见过最多、自己也中过招的高频问题列出来算是给大家打预防针。5.1 主次设备号冲突与动态分配一些从2.4内核时代走来的老代码喜欢在module_init里硬编码一个主设备号比如register_chrdev(240, demo, fops)。这在你的实验机上也许一直没问题但一旦系统里其他驱动恰好也用了240号注册就会失败或者更糟——设备节点被错误的驱动截胡。正确的做法就是用上面代码里的alloc_chrdev_region()动态分配不必关注主设备号具体是多少由内核统一分配。需要固定设备号的场景比如某些特殊应用要求设备节点主次号不变才建议手动指定并且要先检查/proc/devices里该号是否已被占用。5.2 自动创建设备节点udev/mdev 与 class_create有些初学者写完cdev_add之后发现/dev目录下根本没有对应的设备文件于是手动执行mknod /dev/demo_device c 240 0还把主设备号写死。这其实是走入了误区的操作方式。现代Linux系统里设备节点的创建由udev桌面/服务器或mdev嵌入式busybox负责。内核驱动只需要做两件事创建一个class然后调用device_create()。用户空间管理器监听到内核通过设备模型产生的uevent自动在/dev下建好节点。static struct class *demo_class; demo_class class_create(demo_class); device_create(demo_class, NULL, devno, NULL, demo_device);这两个函数的参数中devno是前面分配到的设备号最后一个参数字符串就是/dev下节点的名字。卸载时记得反向调用device_destroy(demo_class, devno)和class_destroy(demo_class)否则设备节点残留下次加载时device_create可能会报错。5.3 生命周期错乱设备先于驱动出现怎么办设备模型是动态的设备注册和驱动注册的顺序不总是一致。有时候insmod一个驱动时对应的设备还没出现有时候设备出现时系统里还没有装驱动。内核的总线机制已经处理了这两种顺序每当新的device注册到总线总线会遍历驱动列表寻找匹配每当新的driver注册到总线总线会遍历设备列表寻找匹配。所以你在probe里拿到的struct device一定是此刻系统确实存在这个硬件不能假设设备顺序。但驱动开发者仍然可能踩一个反向的坑remove/disconnect回调里用户态还持有设备节点同时驱动却卸载了。此时file_operations引用的函数还在模块代码段里一旦模块被rmmod这些代码被卸载用户态下一次read就直接调用已卸载的内存地址内核必崩。解决办法之一是借助cdev的owner字段和模块引用计数。内核在设备节点被打开期间会尝试为该模块增加引用计数。模块在被使用时rmmod会报Module is in use错误这其实就是设备模型的保护机制在起作用。真正复杂的场景还需要配合kref和cdev的原子销毁逻辑这些到后面写实际硬件驱动时再展开。5.4 调试驱动必备三板斧调试驱动环境受限、手段有限远比调试应用层程序麻烦。我自己的日常调试三板斧一是看dmesg。pr_info、pr_debug、dev_info这些输出全部走内核环形缓冲。调试信息找不到时不妨先用dmesg -n 8把控制台日志级别调高再试一次。二是用好sysfs。设备挂上了没有、驱动绑定了没有、资源地址分配得对不对在/sys目录里一查便知。比如ls -l /sys/bus/platform/devices/能看到所有平台设备cat /sys/bus/platform/devices/设备名/uevent能看到设备的型号和匹配信息。三是学会看/proc和/dev。cat /proc/devices能看到字符设备主设备号分配情况/dev目录能确认设备节点是否生成。内核出错时Oops日志里的调用栈要保留完整里面往往直接指明是哪个函数和哪一行出了问题。还有一个经验写驱动时尽量用dev_dbg/dev_info一类带struct device *参数的日志接口而不是裸的printk。它们会自动打印设备名和驱动名日志里一眼就能看清是哪个设备在报错多设备场景下省下的排查时间不可估量。6. 从现实出发设备模型在真实项目里的样子到这里我们应该已经建立了对设备模型的完整印象。很多朋友觉得自己看懂了kobject、device、bus_type但一打开真实的驱动源码还是会蒙。这很正常。真实驱动的复杂度不在于模型本身而在于它同时交织了硬件操作逻辑、内核子系统接口和设备模型框架三层内容。以一个小型I2C温度传感器驱动为例驱动需要基于I2C总线注册一个i2c_driver通过i2c_adapter的传输接口读写寄存器同时还要注册一个hwmon或iio子系统的接口把温度值暴露给用户态。设备模型在其中只负责找到正确的I2C从设备并通知你剩下的大头全在业务驱动代码里。写驱动时如果你的目标是上手真实项目我的建议是分四步走先跑通一个无硬件依赖的字符设备驱动熟悉设备号分配、cdev注册、class/device创建、文件操作集。这一步相当于练基本功喂招再写一个模拟的platform驱动不接任何真实硬件也能学。可以把几个寄存器映射到一块假内存上用devm_ioremap模拟映射过程用readl/writel模拟寄存器读写重点体会probe和资源获取然后找一个真实的外设芯片手册比如I2C接口的EEPROM或加速度计把设备模型和硬件操作真正结合起来。这个阶段你会开始接触devicetree的匹配、中断申请、内核线程处理异步事件等最后尝试改造一个现有的开源驱动比如从精简版触摸屏驱动或PWM驱动开始修bug、加功能读内核邮件列表里的补丁讨论。看到别人怎么设计接口、怎么处理边界情况比看一百篇教程都有用。如果大家感兴趣这个系列的下一篇我可以从platform驱动的完整实战讲起把设备树匹配、resource获取、寄存器映射、中断处理全部串起来写一个能真正挂在总线上的驱动。最后留一个小技巧写设备驱动时#include linux/of.h和#include linux/of_device.h的引入时机取决于你是否需要解析设备树属性。编译报错时先检查是不是头文件引入不全这个细节能省掉你很多看build log的时间。
阅读完成 · 觉得有帮助?
咨询建站