ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux设备驱动开发从入门到实战:字符设备、设备树与调试全指南

Linux设备驱动开发从入门到实战:字符设备、设备树与调试全指南 1. 先把驱动开发的整体思路理清楚1.1 驱动到底在干什么我做了这几年Linux驱动开发最怕别人一上来就问“驱动怎么做”因为这个问题能拆出三层意思第一层是“内核模块怎么写”第二层是“怎么和设备硬件打交道”第三层是“怎么让应用层开开心心地把数据拿到手”。实际上驱动开发的核心就一句话在内核里管理硬件并向应用层提供干净、统一的接口。如果你是刚转到这行的嵌入式工程师或者在学校学过单片机、想往Linux方向走你首先得接受一个观念Linux驱动不是像单片机那样直接操作寄存器就完事而是要有“框架感”。内核给你搭好了平台——总线、设备、驱动三个角色你只是往这个平台里填自己的硬件事务。打个比方内核是个大型物业公司设备模型是物业管理条例驱动是负责某一套房子的管家设备是这套房子本身。管家不直接盖房子但负责把水电气全部接通让住客应用层能正常生活。很多初学者一上来就搜“Linux设备驱动开发详解 pdf”买一堆书结果被里面的内存管理、并发控制、阻塞IO、中断下半部劝退。其实不必害怕驱动开发里百分之八十的日常工作是写注册函数、填file_operations结构体、处理中断、配置设备树、调通DMA或者I2C/SPI通道。真正让你掉头发的往往是总线协议和硬件时序不是Linux机制本身。1.2 设备模型总线、设备、驱动三者怎么配合Linux设备模型用两个核心数据结构来描述硬件struct device设备和struct device_driver驱动总线负责配对。开发时你只需要做两件事在设备树或者ACPI表里声明硬件“我在这里我是什么”写一个驱动声明“我能管这个类型的设备”总线机制会在系统启动时进行“配对”。配对成功后驱动里的probe函数被调用你的硬件初始化代码就放在这个函数里。这个机制的好处是设备与驱动解耦你可以先写好驱动但硬件还没上也可以先有硬件但驱动还在开发甚至同一款芯片在不同板子上跑不同的驱动由设备树切换而不需要重新编译内核。static const struct of_device_id mydriver_of_match[] { { .compatible vendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydriver_of_match); static struct platform_driver mydriver_driver { .probe mydriver_probe, .remove mydriver_remove, .driver { .name mydevice, .of_match_table mydriver_of_match, }, }; module_platform_driver(mydriver_driver);看到没有整个驱动骨架就这些。probe函数才是一切的开始。很多面试官喜欢问“驱动probe怎么被调到的”其实就是在考察你对总线匹配机制的理解。1.3 为什么先写字符设备驱动Linux驱动按设备类型分三种字符设备、块设备、网络设备。新手入门几乎都是从字符设备开始的原因很简单字符设备接口最直观操作文件的方式就是open/read/write/ioctl而文件操作是每个程序员都熟悉的。块设备要考虑页缓存、I/O调度、块大小对齐网络设备要走netlink、NAPI、sk_buff如果你一上来就啃这两个基本就是给自己挖坑。相反一个简单的字符设备驱动几十行代码就能亮出第一个“Hello驱动”这种即时反馈对建立信心特别重要。我见过太多人买了开发板第一周就开始抄网上的LCD驱动、网卡驱动抄完跑起来也不知道为什么。我的建议始终是第一周老老实实把字符设备驱动吃透自己写、自己改、自己制造BUG再自己修这个基本功比什么都值钱。2. 开发环境搭建从内核源码到交叉编译工具链2.1 内核源码选择与配置做驱动开发的前提是你得有一套和你目标平台匹配的内核源码。这里有一个新手最容易踩的坑用发行版自带内核的头文件去编模块。在Ubuntu上装个linux-headers-xxx然后照着网上教程敲个Makefilemake确实能过但insmod的时候大概率报版本不一致错误。我的习惯做法是如果做X86平台验证直接用发行版对应的内核源码包也很方便如果是嵌入式ARM、RISC-V、ARM64一定要拿到芯片厂商提供的BSP内核或者在官方内核仓库里选对分支和defconfig然后用交叉编译器单独编译整个内核。为什么非要自己编内核因为设备驱动模块本质是内核的一部分它内部依赖一堆内核符号。模块加载时内核会检查模块的vermagic和struct module版本信息和你当前正在运行的内核不一致直接拒绝加载。这就是“版本魔法”校验。提示uname -r查看当前内核版本编译模块时Makefile里的KDIR必须指向该版本的内核源码目录并且该内核源码必须已经执行过make modules_prepare。这步不做后面什么都白搭。2.2 编译环境的坑WSL 和虚拟机都有很多PC用户喜欢用WSL或者VMware里装的Linux来搞驱动开发。这里需要提醒你WSL1 没有完整设备模型WSL2 虽然有虚拟化内核但它和宿主机的硬件交互很受限直接加载你自己的模块虽然可以但无法访问真实的设备寄存器所以WSL适合纯软件验证——比如只写一个不访问硬件的字符设备驱动测试文件操作逻辑。如果你要操控USB、PCIe、GPIO老老实实用VMware或者物理机装Ubuntu再配一个真实开发板才好干活。如果你用的是Win11的WSL2还有可能遇到“Your version of Windows Subsystem for Linux (WSL) is too old. Run the command ...”解决方式很粗暴管理员权限打开PowerShellwsl --update wsl --shutdown然后重启WSL。这问题多半是WSL内核和系统版本不匹配升级完就好。虚拟机里装Linux如果蓝屏十有八九是CPU虚拟化没开全或者Hyper-V和VMware打架。建议进BIOS打开VT-x/AMD-V然后在Windows功能里关掉Hyper-V两个虚拟化方案别同时用。2.3 最小可运行模块的编译验证环境搭好后我建议你先编译一个最小模块确认工具链没问题。下面是经典的hello模块放在~/driver-dev/hello/目录下// hello.c #include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO hello_init: module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello_exit: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name);对应的Makefileobj-m : hello.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean然后依次执行make sudo insmod hello.ko dmesg | tail -5 sudo rmmod hello dmesg | tail -5如果你能在dmesg里看到两条日志恭喜你的驱动开发链路是通的。这一步很小但意义重大——它隔离了环境问题后面再遇到的问题基本上就是代码和硬件的问题了。3. 第一次完整跑通一个字符设备驱动的全流程实操3.1 驱动代码整体结构光会打印日志不算完新手真正有成就感的是写出一个能对应用层提供服务的驱动。下面我就把平时最常用的一套字符设备驱动核心代码拆开讲。首先是核心数据结构——file_operations。它定义了应用层调用open/read/write/release时内核会跳转到你驱动的哪些函数#include linux/fs.h #include linux/uaccess.h #include linux/cdev.h #include linux/device.h #include linux/slab.h #define DEVICE_NAME mychardev #define CLASS_NAME mychar #define BUF_SIZE 128 static int major_number; static struct class* mychar_class NULL; static struct device* mychar_device NULL; static char device_buffer[BUF_SIZE]; static int buffer_len 0;我看到很多教程直接把缓冲区定义成全局变量平时写着玩没问题但你要明白真正的驱动里数据是每设备独立的应该放到struct mychar_dev里再用container_of取出来。这里为了演示先用全局变量简化但不代表这是最佳实践。再写open和releasestatic int mychar_open(struct inode *inode, struct file *filep) { printk(KERN_INFO mychar: device opened\n); return 0; } static int mychar_release(struct inode *inode, struct file *filep) { printk(KERN_INFO mychar: device closed\n); return 0; }open和release是驱动里面最简单也最容易忽略的函数。很多初学者不理解为什么驱动里还要写这两个空函数其实它们的作用是给驱动一个机会去做资源准备和清理比如打开时申请内存、启动定时器、请求GPIO关闭时反向操作。如果自己不处理也要占住接口位置防止内核调用空指针。然后是read和writestatic ssize_t mychar_read(struct file *filep, char __user *buffer, size_t len, loff_t *offset) { int bytes_to_read; if (*offset buffer_len) { return 0; } bytes_to_read min(len, (size_t)(buffer_len - *offset)); if (copy_to_user(buffer, device_buffer *offset, bytes_to_read)) { return -EFAULT; } *offset bytes_to_read; return bytes_to_read; } static ssize_t mychar_write(struct file *filep, const char __user *buffer, size_t len, loff_t *offset) { if (len BUF_SIZE) { return -ENOMEM; } if (copy_from_user(device_buffer, buffer, len)) { return -EFAULT; } buffer_len len; *offset 0; return len; }这里有个关键点copy_to_user和copy_from_user是驱动和用户态数据通信的专用接口绝不能直接用memcpy。原因是用户态指针可能指向不合法地址直接复制会导致内核崩溃同时涉及SMAP/域保护只有这个接口能安全穿越用户态和内核态的边界。这是面试高频考点也是实际开发中必须遵守的规矩。再比如offset的处理read和write都带loff_t *offset多字节操作时会不断累加你要自己判断是否到文件末尾。很多人第一次写驱动就栽在这上面read时没有判断*offset buffer_len导致应用层每次读到的都是0字节而后面的dd、cat这类工具就会一直读直到出错。然后定义file_operations并注册static struct file_operations fops { .owner THIS_MODULE, .open mychar_open, .read mychar_read, .write mychar_write, .release mychar_release, };最后是模块初始化和退出static int __init mychar_init(void) { major_number register_chrdev(0, DEVICE_NAME, fops); if (major_number 0) { printk(KERN_ALERT mychar: failed to register major number\n); return major_number; } mychar_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(mychar_class)) { unregister_chrdev(major_number, DEVICE_NAME); return PTR_ERR(mychar_class); } mychar_device device_create(mychar_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME); if (IS_ERR(mychar_device)) { class_destroy(mychar_class); unregister_chrdev(major_number, DEVICE_NAME); return PTR_ERR(mychar_device); } printk(KERN_INFO mychar: registered successfully with major %d\n, major_number); return 0; } static void __exit mychar_exit(void) { device_destroy(mychar_class, MKDEV(major_number, 0)); class_destroy(mychar_class); unregister_chrdev(major_number, DEVICE_NAME); printk(KERN_INFO mychar: unregistered\n); }3.2 Makefile 与编译验证Makefile 和前面 hello 模块几乎一样只是把obj-m改成了mychar.o。编译、加载、确认设备节点一套流程make sudo insmod mychar.ko ls -l /dev/mychardev这里面有个知识register_chrdev(0, ...)是动态分配主设备号手动指定主设备号容易和系统现有设备冲突所以能用动态就用动态。而device_create会在/dev下自动生成设备节点这依赖udev只要设备号和class信息正确就能自动建好节点。如果没有出现节点多半是class_create/device_create没配对好或者udev规则有问题。3.3 应用层测试与交互测试代码可以很简单用一行脚本读再用一个C程序写。下面是个简单的测试程序#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include string.h int main(void) { int fd open(/dev/mychardev, O_RDWR); char buf[64] {0}; char *msg hello from userspace; if (fd 0) { perror(open); return 1; } write(fd, msg, strlen(msg) 1); read(fd, buf, sizeof(buf)); printf(read from kernel: %s\n, buf); close(fd); return 0; }编译gcc test.c -o test ./test运行结果正常输出“hello from userspace”就代表整个用户态到内核态的链路已经通了。这一步为什么值得高兴因为它证明文件操作接口、权限管理、数据拷贝、模块生命周期管理全都正常。注意设备节点的权限默认是root拥有。如果不想每次sudo运行测试程序可以sudo chmod 666 /dev/mychardev或者写udev规则别图省事把开发机权限全放开安全第一。3.4 并发控制驱动开发避不开的主题写字符设备驱动早晚会遇到“两个应用同时读同一个设备”的场景。如果你没有加锁数据会莫名其妙乱掉。Linux驱动常用的并发控制手段有mutex、spinlock、atomic_t、completion。以mutex为例在所有对共享缓冲区操作的函数里加锁static DEFINE_MUTEX(dev_mutex); static ssize_t mychar_write(struct file *filep, const char __user *buffer, size_t len, loff_t *offset) { int ret; if (len BUF_SIZE) { return -ENOMEM; } mutex_lock(dev_mutex); if (copy_from_user(device_buffer, buffer, len)) { ret -EFAULT; goto out_unlock; } buffer_len len; *offset 0; ret len; out_unlock: mutex_unlock(dev_mutex); return ret; }很多教程把锁当成可选项你自己写写玩玩可以不加但凡是可能被多进程访问的驱动必须考虑并发。面试也爱问“自旋锁和互斥锁的区别”核心就是互斥锁在临界区会睡眠可以打个盹再继续自旋锁在临界区只会原地打转等待期间不睡眠。所以在中断上下文里绝对不能用互斥锁只能用自旋锁或者更轻量的atomic操作。4. 设备树配置现代ARM平台驱动开发的必备技能4.1 设备树的作用与基本语法如果你在X86平台练完字符设备下一步肯定要上ARM开发板。这时就会遇到设备树Device Tree。设备树其实就是一张描述板级硬件信息的数据结构表它解决了过去内核里“arch/arm/plat-xxx”下面堆满板级代码的混乱局面内核源码不再针对每块开发板写死硬件资源而是由设备树文件.dts来描述“我这个板子上有哪些设备、它们挂在哪里、资源是多少”内核启动后通过device tree blob.dtb解析并创建设备。举个例子你想在开发板上加一个I2C触摸屏只需要在设备树里声明i2c1 { status okay; clock-frequency 400000; touchscreen38 { compatible vendor,tsc2007; reg 0x38; interrupt-parent gpio1; interrupts 3 IRQ_TYPE_EDGE_FALLING; }; };这段描述的意思是I2C1控制器使能速度400K下面挂了地址0x38的触摸屏中断在gpio1组的第3号引脚下降沿触发。驱动侧只要在of_match_table里声明compatible vendor,tsc2007就会被自动匹配上然后在probe里用device_property_read_u32之类的API读属性。4.2 从设备树节点到 platform 驱动设备树中的每个“compatible”节点最终会变成一个platform_device与platform_driver匹配。匹配成功后驱动里要使用硬件资源一般分两类寄存器地址和中断号用platform_get_resourcedevm_ioremap_resourceGPIO/时钟/pinctrl用devm_gpiod_get、clk_get等通用API看一个常见的probe函数写法static int my_platform_probe(struct platform_device *pdev) { struct resource *res; struct mydevice_priv *priv; int irq; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) { return -ENOMEM; } res platform_get_resource(pdev, IORESOURCE_MEM, 0); priv-reg_base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(priv-reg_base)) { return PTR_ERR(priv-reg_base); } irq platform_get_irq(pdev, 0); if (irq 0) { return irq; } platform_set_drvdata(pdev, priv); return 0; }注意devm_开头的函数都带资源管理功能也就是说驱动移除时内核会自动帮你释放映射、gpio和irq。这能避免非常多在卸载模块时忘记释放资源导致的“内存泄漏式”问题。新写的驱动要尽量用这批接口。4.3 设备树调试三板斧设备树出问题多数情况下是节点地址写错、status没有设为okay、中断号冲突、时钟配置不对。我的排查顺序是这样先确认dtb有没有正确加载看启动日志里的/proc/device-tree是否存在或者ls /proc/device-tree查节点。确认驱动有没有匹配上先看/sys/bus/platform/devices/下有没有生成对应设备再看/sys/bus/platform/drivers/下驱动是否bind。加上of_node打印在probe开头打印pdev-dev.of_node-full_name确认设备树节点确实被解析到。我见过的一种很隐蔽的问题是设备树写了一个interrupts gpio1 3 IRQ_TYPE_EDGE_FALLING但GPIO控制器没使能或者中断控制器层级不对导致驱动probe成功但中断永远不触发。排查这种问题需要看/proc/interrupts确认request_irq之后中断号有没有注册进去然后检查GPIO复用配置尤其是涉及pinctrl的时候。5. 常见问题与排查技巧实录5.1 编译类问题速查表驱动写多了编译错误来来回回就那么几类。我把最常见的整理成一张表对照着查能节约不少时间错误现象可能原因解决办法version magic mismatch模块和运行内核版本不一致Makefile的KDIR必须指向当前内核源码目录重新编译内核和模块error: implicit declaration of function ...缺头文件或内核版本API变了按当前内核版本查include/linux下有没有对应接口适配新APIerror: unknown field ... specified in initializer结构体成员在新内核改了名查当前内核源码里对应结构体定义比如file_operations某些版本调整过字段名/bin/sh: 1: gcc: not found缺编译工具链安装build-essential或交叉编译工具链检查PATHmake[1]: *** No rule to make target modulesKDIR指向错误或modules_prepare没执行确认/lib/modules/$(uname -r)/build存在并链接正确或先编译一次内核5.2 运行时问题排查实录模块加载失败是最常见的运行时问题。insmod报错信息很少关键要在dmesg里看内核给你留的遗言。我举几个真实遇到的场景第一个Unknown symbol。这是因为你的模块依赖了未导出的内核函数或者依赖的内核符号不是GPL兼容的。解决办法加EXPORT_SYMBOL导出你需要的函数或加上MODULE_LICENSE(GPL)。第二个insmod: ERROR: could not insert module: Invalid parameters。通常是module_param传入参数非法或者驱动的init_module函数返回了负错误码。注意返回值是负数时才被视为错误返回0反而是成功这个细节很容易弄反。第三个设备节点找不到。device_create没执行、udev没触发、或者class目录冲突。可以先dmesg | grep mychar确认驱动是否成功注册然后cat /proc/devices | grep mychar看主设备号是否注册。5.3 中断和并发问题排查中断处理是所有驱动里最难调的。我自己踩过最深的坑是“中断太频繁导致系统卡死”。因为中断处理函数里如果加了耗时操作CPU会疲于奔命地处理中断用户态根本抢不到时间片。解决办法是中断处理函数只做“唤醒工作队列”这种短线操作耗时任务放到workqueue或tasklet里处理。英文叫做“top half”和“bottom half”前者处理紧急的硬件应答后者做善后工作。另外并发问题有个直观的排查方向如果数据偶发性错误、概率不高优先怀疑锁使用不当——要么锁没锁全、要么睡眠函数用在了自旋锁保护区域内。此时可以临时把驱动改成互斥锁保守一点再观察能不能复现。我还遇到过一种情况应用层调用read时死等永不返回。这种多半是驱动阻塞在等待一个永远不会来的中断上而中断为什么不来可能是硬件没使能、中断号错、GPIO没配置成中断模式。排查时先看对应驱动有没有把物理中断成功注册到内核然后看/proc/interrupts里中断计数有没有增加再用示波器或者LED点灯法确认硬件信号是否真的有一层层往下逼。5.4 调试工具与思路驱动开发不是靠猜手里得有趁手的工具。我只做这些最实用的dmesg一切的起点内核日志输出主力。/proc和/sys看设备树、看驱动绑定状态、看中断注册信息。ftrace追踪内核函数调用适合分析函数执行流程和耗时。kgdb内核断点调试适合疑难杂症但环境准备麻烦新手不用勉强。devmem//dev/mem直接读硬件寄存器在调试寄存器映射时很有用但一定要小心乱写寄存器可能导致系统死机。这里重点说说ftrace。当你怀疑驱动里的某个函数没有被调用或者调用顺序不对时ftrace比printk好用。它可以让你在不停机的情况下跟踪特定的内核函数cd /sys/kernel/debug/tracing echo function current_tracer echo mydriver_probe set_ftrace_filter echo 1 tracing_on # 然后去触发你的驱动程序运行 cat trace一次完整的ftrace记录会帮你看到从用户态进入内核后函数调用的完整路径。很多看似玄学的“驱动不工作”问题用ftrace可以很清晰地看出是哪个环节断了。6. 面试与日常开发经验总结6.1 高频面试题背后的功底要求Linux驱动开发的岗位面试问来问去就是那些核心机制但它们背后是对体系结构、内核机制、硬件协议的全面理解。我给读者把必背核心考点整理在一张表里面试题考察点回答要点字符设备、块设备、网络设备的区别熟悉三类设备的本质差异数据访问方式不同、内核接口不同、注册方式不同file_operations里常用函数有哪些熟悉核心操作open/release/read/write/unlocked_ioctl/mmap/pollcopy_to_user失败返回什么体会内核安全返回EFAULT并且数据不能传完内核空间和用户空间通信方式有哪些熟悉多种通信机制系统调用、procfs、sysfs、netlink、mmap、ioctl中断上下文能不能睡眠考察并发控制不能中断上下文不参与调度自旋锁保护操作要短信号量和互斥锁区别考察并发原理信号量允许N个互斥锁允许1个互斥锁优先Linux设备模型三大组件考察总线模型总线、设备、驱动靠match配对设备树的作用考察ARM平台开发经验描述硬件资源解耦设备与驱动面试不是背题能把这些机制结合自己做过的项目讲出“为什么这样设计”就比只背概念强很多。比如问“为什么中断上下文不能睡眠”你能说出“内核调度器依赖进程上下文中断没有task_struct入睡后没有唤醒路径”这句话一出口面试官就知道你学过Linux内核调度。6.2 招聘中的高频关键词这篇博客后面挂着一堆热搜词里面有好几个其实代表了市场方向——linux嵌入式驱动开发、linux i2c emio、linux kvm网络详解、系统裁剪优化。做驱动开发久了你会发现市场上的需求通常分两个方向一是底层BSP工程师要会看原理图、看芯片手册、配置设备树、调DRAM/DDR频率、裁剪内核和根文件系统。这个方向对硬件的理解要求很高得能看懂时序图。二是业务类驱动工程师主要写外设驱动触摸、Camera、Sensor、显示、Audio、WiFi/BT这中间I2C/SPI/UART是高频总线很多人说“Linux i2c emio”其实是说用ARM的GPIO模拟I2C的时序来调试问题——这个技巧很有用遇到控制器I2C被占死或者时序不匹配时软件模拟I2C能帮你快速验证从设备是否存在。系统裁剪优化这个词也经常出现在招BSP工程师的JD里。内核裁剪不只是删config选项而是要保证功能的前提下化繁为简去掉不需要的驱动、关闭冗余的debugfs、配置合适的内核HZ值、调整DMA缓存策略、裁剪根文件系统的库文件。这块需要积累大量实践数据比如裁剪前后启动时间、内存占用对比。6.3 推荐的学习路线与资源规划如果你想好了要入行Linux驱动开发我给一个靠谱的路线第一阶段1~2周把C语言和Linux基础补扎实——常用命令、Shell脚本、进程/内存概念。这个阶段的目标不是成为运维而是保证自己能在命令行环境里顺手干活。第二阶段3~4周啃字符设备驱动。把本文的示例自己敲一遍改成不同的行为比如多设备注册、ioctl命令扩展、poll等待队列。这个阶段要搞懂file_operations和并发控制。第三阶段4~8周接触真实硬件。买个带ARM核心的开发板从点灯开始再到按键中断、I2C读取传感器、SPI驱动LCD屏。设备树从抄到改到写遇到不懂的总线协议去啃对应芯片手册。第四阶段长期往深了走就看《Linux设备驱动程序》第三版的内核机制部分配着https://elixir.bootlin.com/linux/latest/source按版本查源码。不用整本啃完当成字典查就好。我看到太多人困在“资料收藏”这一步今天看到一篇好文章就收藏明天搜到一个好视频就放着结果一个月过去了代码一行没写。驱动开发没有任何捷径唯一的路就是把开发板上的每个外设调到能工作、稳定工作再回去理解内核的每个机制。7. 一点真心话最后说点我自己这些年踩出来的一条体会写驱动和写应用代码有一个很大的不同就是你永远不能假设硬件会按你想象的方式工作。应用代码错了能调试、能崩溃、还能重启驱动出错了轻则模块崩溃、重则整个内核宕机连日志都来不及留。所以做这一行我一直坚持三个习惯一是每次动寄存器之前先翻手册确认时序二是每次改代码之前先想在崩溃前怎么留下足够多的调试线索三是每调通一个功能就老老实实在文档里记下当时的设备树配置、内核版本和测试命令。这三个习惯帮我在无数个“为什么昨天还能跑今天就不行”的清晨里最快定位到问题。希望这篇分享能让你在Linux设备驱动这条路上少走一些弯路。如果只能记住一句话那我希望你记住驱动开发不是玄学它是“内核机制芯片手册硬件时序”三个维度交织出来的工程实践多动手、多读源码、多善用日志你的水平一定会稳步上去。
返回列表