拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux设备驱动开发详解:从字符设备到设备树与总线驱动

直接把一个驱动写崩内核几乎是每个Linux驱动开发者的成年礼。我最早接触这个领域时面对满屏的struct file_operations、platform_driver和各种probe回调一度以为驱动开发就是往内核里塞一堆结构体和回调函数后来踩了无数坑才明白Linux设备驱动本质上是在做一件事用软件把一个硬件设备的“脾气”完整地描述给内核再通过一套标准接口把它交给用户态程序使用。这篇内容我打算把Linux设备驱动开发中最核心的几个环节拆开揉碎讲一遍包括驱动在内核里的运行逻辑、字符设备驱动框架、设备树匹配机制、PCI和I2C这两种典型总线驱动的写法以及驱动调试和性能优化中真正有用的手段。如果你是刚开始接触嵌入式Linux驱动开发或者正被设备树、file_operations、probe这些概念搞得头大这篇文章值得你耐心看完。1. 一个Linux驱动在内核里是怎么“活着”的1.1 驱动开发的本质用软件描述硬件的“脾气”很多人容易把驱动想得很玄其实驱动干的事和普通业务代码没有本质区别无非是读取硬件寄存器、配置工作模式、搬运数据、响应中断。真正的难点在于硬件有自己固有时序、地址空间、中断请求方式而这些细节只有通过硬件手册才能完全掌握。驱动要做的就是用软件精确模拟这些时序和规则把硬件的“怪脾气”包装成内核和用户态都能理解的标准接口。举个例子一块I2C接口的温度传感器芯片它的数据手册会规定要启动一次温度转换需要往寄存器0x01写入0x20转换完成后的温度值从寄存器0x02和0x03读出。驱动做的事情就是按这个时序去操作I2C控制器再把读到的原始值换算成摄氏温度通过read接口交给应用层。听起来是不是没那么难对原理简单但过程中牵涉到总线竞争、时钟频率、寄存器位宽、字节序这些细节任何一个不对硬件就是不搭理你。1.2 从insmod到rmmod模块的完整生命周期Linux驱动可以编译进内核镜像也可以编译成独立的内核模块动态加载。实践中我强烈建议驱动开发阶段全部用模块方式——改一行代码只需要重新编译模块然后insmod替换不用反复烧写整个内核镜像开发效率天差地别。一个标准驱动模块的骨架长这样#include linux/module.h #include linux/init.h #include linux/fs.h static int __init my_driver_init(void) { /* 注册设备号、创建字符设备、注册驱动等操作 */ return 0; } static void __exit my_driver_exit(void) { /* 反向注销所有资源 */ } module_init(my_driver_init); module_exit(my_driver_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple demo driver);module_init指定的函数在insmod时执行module_exit指定的函数在rmmod时执行。__init标记的作用是这个函数在内核启动或模块加载完成后所占用的内存会被释放掉。如果驱动的init函数非常庞大你会发现insmod之后内核占用内存反而变小了就是这个机制在起作用。开发过程中最烦的问题之一是模块卸载不干净。比如你注册了字符设备但我没删设备文件或者probe阶段申请了中断但错误处理路径里忘了释放rmmod的时候要么直接卡死要么提示Device or resource busy。我的习惯是每个资源申请都立刻对应写好释放逻辑宁可多写也不等出问题再补。1.3 驱动代码到底跑在哪一层用户态、内核态与硬件的三角关系理解这个问题需要先建立一个基本认知驱动代码运行在内核态有完全的权限操作硬件但也意味着代码一旦出错整个系统直接崩溃而不是像应用层那样崩个进程就完事。用户态程序通过系统调用进入内核态再通过设备文件操作走到驱动接口。打个比方把内核比作酒店管理层驱动是酒店的服务员硬件是客人。用户态程序是访客访客需要什么服务通过客房电话系统调用告诉前台前台指派对应的服务员驱动去服务对应房间硬件设备。如果服务员业务不熟把客人的房间钥匙弄丢了轻则这个房间没法服务重则整个酒店管理系统崩溃重启。所以驱动开发最根本的心态转变是你不是在写一个程序你是在给内核写一段可信的代码。任何可能出错的分支都要考虑任何用户态传进来的参数都要校验任何并发访问都要有锁保护。这也是为什么很多应用层转内核开发的人一开始特别不适应因为在应用层你只需要对自己负责在内核层你要对整台机器负责。2. 字符设备驱动框架设备号、file_operations与生命周期管理2.1 设备号分配和设备节点用户态访问硬件的两把钥匙字符设备是Linux驱动最基础也最常见的类型按键、串口、GPIO控制器、传感器、帧缓冲等等都属于字符设备。要访问一个字符设备用户态程序需要两条信息设备号和设备文件路径。设备号由主设备号和次设备号组成。主设备号标识设备对应的驱动程序次设备号标识同一个驱动管理下的不同设备实例。比如同一个驱动管理了两个串口主设备号相同次设备号分别是0和1。设备号的申请有两种方式/* 静态申请指定主设备号适合你确切知道这个主设备号没被占用 */ int major 250; register_chrdev_region(MKDEV(major, 0), 1, my_device); /* 动态申请让内核分配一个可用的主设备号推荐 */ dev_t dev_num; alloc_chrdev_region(dev_num, 0, 1, my_device); major MAJOR(dev_num);我基本只用动态分配因为静态指定的主设备号很可能和别的模块冲突。驱动在/proc/devices里可以看到系统当前所有已注册的主设备号开发时不确定就用动态分配最稳妥。拿到设备号之后还要创建设备节点文件也就是/dev下面的文件。设备节点可以用mknod命令手动创建但正规做法是驱动里通过class_create和device_create自动创建设备节点让udev在模块加载时自动在/dev下生成对应文件static struct class *my_class; my_class class_create(my_class); device_create(my_class, NULL, dev_num, NULL, my_device); /* 卸载时别忘了销毁 */ device_destroy(my_class, dev_num); class_destroy(my_class);这样insmod之后/dev/my_device就会自动出现用户态程序直接open这个文件就能操作硬件了。2.2 file_operations回调逐一拆解驱动和用户态聊天的方式字符设备驱动最核心的部分是struct file_operations它其实就是一张“回调函数表”内核把用户态对设备文件的各种操作open、read、write、ioctl、mmap、release转换成对这张表里对应函数指针的调用。一个典型的字符设备file_operations定义static struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .release my_release, .read my_read, .write my_write, .unlocked_ioctl my_ioctl, .mmap my_mmap, };这里有个重要细节ioctl现在一般用unlocked_ioctl而不是传统的ioctl。因为内核在2.6.36之后移除了全局大锁BKL旧的ioctl指针已经废弃。实际面试和开发中经常被问到这个问题很多初学者还在照着老书抄.ioctl结果编译直接报错。open回调里通常做设备初始化、权限检查、递增使用计数。release做反向的释放操作。read和write是数据搬运的核心用户态的read(fd, buf, len)会走到驱动的my_read但有一个关键点内核态拿到的buf指针是用户态地址不能直接访问必须用copy_to_user和copy_from_user来安全地读写数据static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { char kernel_buf[128]; /* 从硬件寄存器或内核缓冲区读取数据到kernel_buf */ if (copy_to_user(buf, kernel_buf, count)) return -EFAULT; return count; } static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { char kernel_buf[128]; if (len sizeof(kernel_buf)) return -EINVAL; if (copy_from_user(kernel_buf, buf, len)) return -EFAULT; /* 把kernel_buf中的数据写入硬件 */ return len; }为什么直接访问用户态指针是危险的因为用户态指针可能指向一个非法的地址区域内核态直接解引用会引起页错误进而panic。copy_to_user内部会做地址合法性检查并且确保在拷贝过程中不会被调度走。这是驱动开发里最基础也最容易犯的错误之一。2.3 并发与锁驱动崩溃的头号来源驱动开发里最隐蔽的坑不是语法错误而是并发问题。同一个设备可能被多个进程同时open两个线程同时read中断处理和进程上下文同时访问共享数据——这些场景如果不加锁轻则数据错乱重则内核直接死锁或崩溃。新手最常见的错误是“我觉得我的设备不会被并发访问”这个想法非常危险。即使你的硬件只有一个进程在用中断处理函数和其他内核路径也可能同时访问驱动里的同一个变量。我在做设备读取接口时吃过一次大亏一个全局缓冲区正常流程是应用线程写、中断回调读我图省事没做同步结果系统运行一段时间后数据随机错乱抓了整整两天的包才定位到是中断和应用层的竞争访问。字符设备驱动常用的同步手段信号量/互斥锁适合临界区执行时间较长的场景可以睡眠等待自旋锁适合临界区很短且不能睡眠的场景比如中断上下文原子变量适合简单的计数操作完成量completion适合等待某个硬件事件的场景一个简单经验这个共享区的操作可能会被中断处理函数访问就用spinlock如果只会在进程上下文中访问互斥量就够了需要等待硬件完成某个操作再继续考虑completion。2.4 一个字符设备驱动的完整注册流程把这些串起来一个标准字符设备驱动的初始化部分大概长这样#include linux/cdev.h static dev_t dev_num; static struct cdev my_cdev; static struct class *my_class; static int __init my_driver_init(void) { int ret; /* 1. 动态分配设备号 */ ret alloc_chrdev_region(dev_num, 0, 1, my_device); if (ret 0) return ret; /* 2. 初始化并添加cdev到内核 */ cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, dev_num, 1); if (ret 0) goto err_cdev_add; /* 3. 创建设备类让udev自动生成设备节点 */ my_class class_create(my_device_class); if (IS_ERR(my_class)) { ret PTR_ERR(my_class); goto err_class_create; } device_create(my_class, NULL, dev_num, NULL, my_device); return 0; err_class_create: cdev_del(my_cdev); err_cdev_add: unregister_chrdev_region(dev_num, 1); return ret; }注意错误处理路径必须把之前申请过的资源逐级释放顺序和申请顺序正好相反。这个习惯能让你少宕机十次。3. 设备树把硬件接线图写进内核的机制解析3.1 为什么要有设备树从“代码硬编码”到“数据驱动配置”老一代的开发方式里设备的地址、中断号、寄存器偏移等信息是直接硬编码在驱动代码里的。换一个板子硬件引脚不一样就得改驱动重新编译。这种模式在ARM平台越来越行不通因为ARM平台不像x86那样支持即插即用可枚举的总线硬件变化非常频繁。设备树Device Tree的引入解决的就是这个问题把硬件配置信息从驱动代码里拆出来变成一个独立的数据文件.dts驱动通过匹配设备树节点来获取硬件资源。同一个驱动只要设备树节点里的配置信息正确不需要重新编译驱动就能适配不同的板卡。这是一个典型的将策略从机制中分离的思路。3.2 compatible匹配机制驱动怎么找到自己的“接线图”设备树节点到驱动的匹配核心是compatible字符串。设备树里写一个节点my_device: my-device1c00000 { compatible myvendor,mydevice; reg 0x1c00000 0x1000; interrupts 0 23 4; clock-frequency 1000000; };驱动这边声明static const struct of_device_id my_of_match[] { { .compatible myvendor,mydevice, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name my_device, .of_match_table my_of_match, }, };当内核发现设备树里某个节点的compatible和驱动的of_match_table中的某一项匹配时就会调用这个驱动的probe函数。所以probe不是驱动主动调用的而是内核匹配后回调的。理解这个“回调”思维很重要很多初学者以为probe是驱动自己该干的事其实probe的本质是“你的硬件被内核发现了来领活儿吧”。3.3 在probe里获取硬件资源platform_get_resource与devm_ API匹配成功进入probe之后驱动需要从设备树节点中提取地址、中断号等信息。常用APIstatic int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; /* 获取reg属性中的第一个地址区域 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); /* 获取中断号 */ int irq platform_get_irq(pdev, 0); if (irq 0) return irq; /* 获取自定义属性 */ u32 freq; of_property_read_u32(pdev-dev.of_node, clock-frequency, freq); return 0; }注意devm_前缀的API——这是设备资源管理框架probe里用devm_申请的资源在设备解绑或驱动卸载时会自动释放。这个机制能极大简化错误处理路径的编写。我个人的经验是内核里凡是能用devm_开头的接口就不要用原始接口比如devm_kzalloc、devm_ioremap_resource、devm_request_irq这套API几乎是现代驱动开发的事实标准。3.4 设备树排查看到“failed to get resource”怎么办设备树配置写入后如果没生效最常见的是三类问题节点路径或名字错误内核日志里搜索驱动名字看有没有probe被调用如果没有大概率是compatible字符串对不上reg属性格式错误reg 0x1c00000 0x1000第一个是起始地址第二个是长度很多人把这两个写反pinctrl配置缺失一些外设引脚需要通过pinctrl子系统配置复用模式设备树里忘了加pinctrl-names和pinctrl-0导致probe即使成功硬件也不工作调试设备树最有效的方式是检查内核启动日志设备树解析阶段如果有错误会直接打印OF: fdt: Error之类的信息。还可以在设备树里临时加一个status disabled再改成okay确认节点本身是否被屏蔽。4. PCI与I2C总线驱动从注册函数到数据流转的完整链路4.1 PCI设备驱动从device ID表到BAR空间映射PCI设备驱动是总线型驱动的典型代表。PCI总线的特点是硬件支持枚举系统启动时固件或内核可以自动扫描总线上的设备读取设备的Vendor ID、Device ID等信息然后通过匹配表找到对应驱动。PCI驱动注册的标准写法static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x10EE, 0x9038) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids); static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { /* 使能设备 */ pci_enable_device(pdev); /* 请求设备的BAR地址资源以BAR0为例 */ pci_request_region(pdev, 0, my_pci_device); /* 映射BAR0的物理地址到内核虚拟地址 */ void __iomem *bar0 pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!bar0) goto err_iomap; /* 设置总线主控能力如果设备需要发起DMA */ pci_set_master(pdev); /* 申请中断 */ if (pdev-irq) { ret request_irq(pdev-irq, my_isr, IRQF_SHARED, my_pci_device, dev); } return 0; err_iomap: pci_release_region(pdev, 0); pci_disable_device(pdev); return -ENOMEM; } static void my_pci_remove(struct pci_dev *pdev) { /* 释放中断、iounmap、release_region、disable_device */ }PCI驱动最核心的能力是访问设备的配置空间和BAR空间。BARBase Address Register是设备暴露给系统的一组内存或I/O地址区域通过pci_iomap映射到内核虚拟地址后直接对返回值进行读写就能和设备的寄存器交互。很多PCIe设备驱动会用到DMA来搬运大块数据。DMA的经典流程是驱动分配一块内核缓冲区用dma_alloc_coherent获得物理连续且支持DMA的地址把地址告诉设备的DMA引擎设备直接把数据写进缓冲区写完发中断通知驱动。这个过程比CPU逐字节读写效率高几个量级。4.2 I2C设备驱动注册函数、adapter与读写时序I2C总线的驱动模型稍微特殊一些它分两层I2C控制器驱动adapter和I2C设备驱动client。控制器驱动负责管理底层的I2C总线处理时钟和数据的时序设备驱动面向挂在总线上的具体外设只关心设备地址、寄存器读写。I2C设备驱动注册流程static int my_i2c_probe(struct i2c_client *client) { /* client-addr 就是从设备树里读到的I2C从机地址 */ /* 在这里初始化硬件比如写配置寄存器 */ return 0; } static void my_i2c_remove(struct i2c_client *client) { /* 释放资源 */ } static const struct i2c_device_id my_i2c_id[] { { my_i2c_sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, my_i2c_id); static const struct of_device_id my_i2c_of_match[] { { .compatible myvendor,myi2csensor }, { } }; MODULE_DEVICE_TABLE(of, my_i2c_of_match); static struct i2c_driver my_i2c_driver { .driver { .name my_i2c_sensor, .of_match_table my_i2c_of_match, }, .probe my_i2c_probe, .remove my_i2c_remove, .id_table my_i2c_id, }; module_i2c_driver(my_i2c_driver);设备树里声明I2C设备i2c1 { my_i2c_sensor48 { compatible myvendor,myi2csensor; reg 0x48; }; };这里的reg 0x48就是该设备在I2C总线上的7位从机地址。compatible用于匹配驱动程序很多I2C传感器芯片数据手册会推荐compatible字符串的写法。I2C驱动中读写数据的核心是struct i2c_transfer机制static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; struct i2c_msg msg { .addr client-addr, .flags 0, .len sizeof(buf), .buf buf, }; return i2c_transfer(client-adapter, msg, 1); } static int i2c_read_reg(struct i2c_client *client, u8 reg) { struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, .len 1, .buf val, }, }; if (i2c_transfer(client-adapter, msgs, 2) ! 2) return -EIO; return val; }第一条消息写寄存器地址第二条消息读数据这是I2C最常见的“写地址再读数据”组合操作。注意i2c_transfer的返回值是成功传输的消息数不是0/1的布尔值很多初学者在这里判断错误导致误判通信失败。4.3 为什么说总线驱动模型是理解所有驱动的万能钥匙当你掌握了platform、PCI、I2C三种驱动模型之后会发现它们的套路惊人相似都有一个匹配机制都有一个probe回调probe里获取硬件资源并初始化设备然后注册对应的设备接口。事实上Linux的设备模型device/driver/bus正是建立在这套机制之上SPI、USB、MDIO、CAN等所有总线驱动都遵循同样的逻辑。理解了这套模型读任何外设驱动的源码都会变得容易很多。拿到一个陌生驱动的第一步永远不是看read/write函数而是看它的probe函数——里面藏着这个驱动的硬件资源、初始化流程和设计思路。5. 驱动调试与性能优化动态加载、读写拦截和瓶颈定位5.1 动态加载内核模块的核心操作与常见报错驱动开发中用到的命令不多但每个都值得牢记# 编译当前目录下的模块 make -C /lib/modules/$(uname -r)/build M$(pwd) modules # 加载模块 sudo insmod my_driver.ko # 查看模块是否加载成功 lsmod | grep my_driver # 查看内核日志驱动printk输出到这里 dmesg | tail -20 # 卸载模块 sudo rmmod my_driver常见报错里insmod: ERROR: could not insert module: Invalid module format通常是内核版本或配置不匹配模块编译时用的内核头文件和当前运行内核不是同一个版本Operation not permitted可能是Secure Boot拦了没有签名的模块Device or resource busy则通常是设备被占用或设备节点没删干净。模块加载后如果probe没有执行第一件事是dmesg里搜my_driver看有没有OF: fdt或i2c/pci子系统报的匹配错误。如果dmesg里没有任何输出检查MODULE_DEVICE_TABLE和设备树compatible是否对得上。printk是驱动调试最原始也最可靠的武器。它的级别参数很关键printk(KERN_INFO xxx\n)能在默认日志级别下显示出来printk(KERN_DEBUG xxx\n)则需要调高内核日志级别才能看到。临时快速改日志级别的命令echo 8 /proc/sys/kernel/printk5.2 性能瓶颈定位从read/write到mmap字符设备驱动最常被诟病的性能问题就是read/write每调用一次就陷入一次内核态拷贝一次数据。对于高速数据采集场景比如摄像头、高速ADC、网卡逐次read/write的方式完全扛不住。优化方向通常按以下顺序考虑增大内核缓冲区的粒度把单次read/write能处理的数据量做大减少系统调用次数用ioctl走控制通路用mmap走数据通路配置类的低频操作走ioctl大块数据通过mmap让应用层直接映射内核缓冲区绕过copy_to_user的额外拷贝用DMA替代PIO让硬件直接把数据写到内存不经过CPU逐字搬运这是PCIe、网卡、USB3.0等高速外设的必选项mmap实现的一个最小例子static int my_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long size vma-vm_end - vma-vm_start; unsigned long pfn virt_to_phys(kernel_buf) PAGE_SHIFT; /* 禁止把映射区域换出到swap */ vma-vm_flags | VM_IO | VM_DONTEXPAND | VM_DONTDUMP; return remap_pfn_range(vma, vma-vm_start, pfn, size, vma-vm_page_prot); }之后应用层就可以通过mmap拿到一个用户态指针直接读写内核缓冲区性能接近内存拷贝的极限。5.3 拦截read/write文件系统层与驱动层的区别我注意到很多人搜索“拦截read/write”大部分场景是想做文件透明加密、审计或沙箱。这里有个概念必须分清在设备驱动里拦截read/write只能拦截针对该设备节点文件的系统调用不是拦截整个文件系统所有文件的操作。想拦截所有文件的读写要去hook VFS层的函数比如do_sys_open、vfs_read或者使用内核的LSM框架或者用eBPF的tracepoint机制。如果确实需要在驱动层拦截某个设备文件的读写就是在file_operations的read/write回调里加入自己的逻辑static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { /* 先对用户态数据进行自己的处理比如加密 */ char kernel_buf[4096]; if (copy_from_user(kernel_buf, buf, len)) return -EFAULT; /* 对kernel_buf做变换处理 */ my_transform(kernel_buf, len); /* 再传给硬件 */ my_hardware_send(kernel_buf, len); return len; }这里有个隐藏的坑copy_from_user之后的数据已经在内核空间里了但如果你截断了write长度用户态认为数据已写完而实际只写入一部分会造成静默数据丢失。要么严格返回实际处理长度要么对不支持的情况直接返回错误码。5.4 驱动性能调优中值得关注的几个经验调优驱动性能不能只盯着驱动本身的代码要从整个数据链路看。我自己常用的检查顺序一是确认数据在每一层的拷贝次数。用户态到内核态第一次拷贝内核态到硬件第二次拷贝。如果驱动里有临时缓冲区中转就是三次。拷贝次数越少越好。二是确认中断处理开销。中断处理函数里只做最紧急的事比如读取硬件状态、唤醒等待队列把耗时的数据搬运挪到tasklet、工作队列或内核线程里。三是确认锁的粒度和持有时间。持锁时间超过几十微秒就要考虑改用无锁数据结构或RCU。四是用好perf和tracepoint直接看驱动函数的CPU占比、中断延迟分布。驱动性能调优和普通用户态调优最大的区别在于驱动调优一旦出错往往不是性能下降而是系统崩溃。所以我会强烈建议调优过程中每个改动都单独验证并且保持模块可以随时卸载的状态出现异常时第一时间rmmod回滚。6. 几个绕不开的实战提醒6.1 用modpost和内核日志做第一道防线模块编译完成后先执行modinfo my_driver.ko看看模块的依赖、描述、license信息是否正确。加载前用modprobe --dry-run模拟加载可以提前发现依赖模块缺失的问题。加载后必看dmesg我的习惯是每次dmesg都带时间戳输出到文件方便出问题时回溯。如果驱动涉及多个源文件Makefile里要写清楚obj-m的依赖关系obj-m : my_driver.o my_driver-objs : core.o hardware.o sysfs.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean6.2 字符设备还是杂项设备年轻开发者常纠结的问题很多刚学驱动的人会发现有些驱动注册的是miscdevice杂项设备而不是完整的字符设备。杂项设备是字符设备的一种简化封装使用系统预留的主设备号10不需要自己申请设备号。static struct miscdevice my_misc_device { .minor MISC_DYNAMIC_MINOR, .name my_device, .fops my_fops, };什么时候选择miscdevice我个人的经验是只有一个设备实例、功能简单的场景比如单路GPIO按键、小型传感器、简单的LED控制器用miscdevice开发效率更高。需要多实例、需要明确主设备号隔离、需要完整设备模型的场景还是老老实实用cdev platform_driver的组合。6.3 学习驱动的三个层次最后聊聊怎么学驱动这条路。第一层是会用能照着框架写出能跑的模块修改设备树适配自己的板子这是多数培训课能达到的水平。第二层是能调出了问题知道从内核日志、设备树、硬件时序三个维度去排查能看懂内核源码里相关子系统的实现。第三层是能设计面对一个全新的硬件设备能结合数据手册和应用场景设计驱动架构选择合适的接口模型字符设备、net_device、input_device还是framebuffer合理规划中断/DMA/内存映射策略让驱动在复杂场景下稳定高效地运行。驱动开发的辛苦在于必须同时面对硬件的不确定性和内核的严格约束但回报也直接——你写的代码直接控制着硬件这份掌控感是应用层开发很难体会到的。如果你准备入坑我的核心建议是先在小项目里把字符设备框架和platform驱动模型吃透再去碰设备树、DMA、中断和复杂总线千万不要一上来就铺太开内核的调试手段和用户态完全不同体系感是慢慢垒出来的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门