1. 先搞明白:设备驱动到底在解决什么问题
开始写驱动之前,我建议你先想清楚一个事情:Linux设备驱动开发,本质上就是在做“内核和硬件之间的翻译官”。硬件厂商不会主动告诉你芯片内部怎么工作,Linux内核也不关心你用的是哪家的传感器、哪颗LED、哪块显示屏。驱动要做的,就是把硬件的能力“翻译”成内核能理解的统一接口,让上层的应用程序不用关心底层硬件差异,直接通过open、read、write、ioctl这些标准调用就能操作设备。
很多初学者一上来就抱着《Linux设备驱动程序》第三版啃,结果看了两周还在讲内存管理,代码一行没写过,最后放弃了。我的建议是换个思路:先写一个最简单的字符设备驱动,把整个开发、编译、加载、测试、调试的流程跑通,再回头看那些理论,你会发现书里的每个概念都能对上号。
这篇博文我会从一个实际可运行的字符设备驱动入手,把开发环境搭建、驱动框架、核心数据结构、并发控制、调试方法到实际项目中会遇到的问题全部过一遍。文章主要面向三类人:一是正在学嵌入式Linux、准备做驱动方向的同学,二是从单片机或应用开发转内核开发的工程师,三是在工作中需要维护或移植设备驱动的朋友。看完这篇文章,你应该能独立写出一个可以实际使用的字符设备驱动,并且知道怎么排查驱动开发中最常见的那几类问题。
我默认你已经具备一定的C语言基础,懂一点Linux基本操作(会用vim、gcc、make),如果这些还不熟,建议先把这两块补一补。内核编程对C语言的要求比应用编程高,指针、内存、链表这些至少要达到熟练的程度。
2. 内核态和用户态的边界,是驱动开发的底层逻辑
2.1 为什么驱动必须跑在内核态
在Linux系统里,CPU分了两个特权级别:用户态和内核态。应用程序跑在用户态,权限非常有限,不能直接访问硬件寄存器、不能执行特权指令;内核和驱动程序跑在内核态,拥有完全的控制权。
你可能会问:为什么不能让应用程序直接操作硬件?原因很简单——安全和稳定。如果任何程序都能直接写物理内存、直接控制硬件,一个Bug就能让整个系统崩溃,一个恶意程序就能拿到所有权限。所以操作系统把所有敏感操作收归内核统一管理,应用程序需要操作硬件时,通过系统调用(open、read、write等)陷入内核,由驱动代码去完成真正的硬件交互。
这里要注意:驱动开发中大部分崩溃都是致命的。应用程序段错误顶多进程挂掉,内核里一个空指针解引用就是整个系统Panic,所有数据可能丢失。所以写驱动代码,你的心态必须调整:这是拿着手术刀在做手术,不是拿着菜刀在切菜。
2.2 设备文件是用户态和内核态的桥梁
我们平时在Linux下操作硬件,最常见的方式是通过设备文件。比如操作串口就是操作/dev/ttyS0,操作磁盘就是操作/dev/sda。设备文件本身不包含数据,它只是一个“入口”,当你对它调用open、read、write时,VFS(虚拟文件系统)会根据设备文件的类型和主设备号,找到对应的驱动程序,然后调用驱动里注册好的处理函数。
设备文件分为两类:字符设备和块设备。字符设备以字节流的方式读写数据,比如串口、键盘、传感器;块设备以块为单位读写数据,比如硬盘、SSD、SD卡。驱动开发入门,几乎都是从字符设备开始的,因为字符设备的模型简单直接,不需要处理复杂的页缓存、请求队列这些机制。
3. 开发环境准备:一套能跑的最小内核环境
3.1 环境选型思路
Linux设备驱动开发第一步,准备环境。我不建议在实体机上直接搞,万一驱动写崩了,系统起不来,你连救都救不回来。用虚拟机最稳妥,VMware或者VirtualBox都行。操作系统嘛,Ubuntu 20.04或者22.04 LTS版本用了这么多年,稳定、资料多、遇到问题搜得到答案。
内核版本方面,我建议选一个4.x或5.x的长期支持版本,比如5.4、5.10。为什么要强调内核版本?因为驱动代码和内核版本强相关,不同版本之间API可能微调,你写驱动的时候用到的结构体、函数接口必须和你实际编译运行的内核版本完全一致。网上很多教程是老的,代码在新内核上编译不过去,就是版本差异导致的。
我实际用的环境是VMware跑Ubuntu 20.04,内核5.4.0,这个组合文档最全、踩坑最少。
3.2 内核源码和编译工具的安装
驱动不是普通的应用程序,不能直接gcc编译就完事,它需要和内核源码树配合编译,生成.ko模块文件,然后由内核动态加载。所以必须先把内核源码准备好。
驱动开发其实不需要完整编译整个内核,但你需要安装内核头文件,也就是/lib/modules/$(uname -r)/build这个目录。最简单的方式是直接安装系统自带的内核头文件包:
sudo apt update sudo apt install linux-headers-$(uname -r) sudo apt install build-essential装完之后检查一下:
ls /lib/modules/$(uname -r)/build如果里面能看到Makefile,说明头文件就绪。对于初学者,我建议就用这种方式,不要在环境上花太多时间。如果之后要做内核源码级的调试,再单独下载完整内核源码编译不迟。
我自己调试驱动时通常还会额外装两个东西:一个是minicom,用来和串口设备通信测试;一个是sysfsutils,方便查看设备信息。但这些不是必须的,后面用到再说。
3.3 编译Hello World模块,验证环境可用
环境准备好之后,先用一个最简单的模块验证工具链是否正常。创建hello.c:
#include <linux/init.h> #include <linux/module.h> static int __init hello_init(void) { printk(KERN_INFO "Hello, kernel!\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, kernel!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple hello module");对应的Makefile:
obj-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 sudo dmesg | tail sudo rmmod hello sudo dmesg | tail如果dmesg里看到了Hello和Goodbye字样,说明你的驱动开发环境已经通了。这个流程看似简单,但它是后面所有开发的基础,务必跑通再进行下一步。
注意:printk输出的日志不会直接打到终端,而是写到内核日志缓冲区,要用dmesg查看。生产环境中日志级别控制很重要,调试阶段可以先把级别设低一点,让日志直接输出到控制台。
4. 一个完整的字符设备驱动:从设计到实现
4.1 需求定义:我要做什么
下面我用一个实际的例子,带你把一个字符设备驱动完完整整写出来。需求很简单:实现一个虚拟的字符设备,应用层可以write数据进去,可以read数据出来。这个设备内部用一块内存缓冲区保存数据,最多存4KB。我们给它起个名字叫"memdev"。
别看需求简单,它涵盖了字符设备驱动的全部核心要素:设备号管理、file_operations接口实现、内核内存分配、数据拷贝、并发控制、设备文件自动创建、模块卸载清理。把这一套吃透,写真实的硬件驱动时,你只需要把read和write里的逻辑换成实际操作硬件寄存器的代码就行了。
4.2 设备号:驱动的“身份证”
每个字符设备在内核里都有一个唯一的设备号,由主设备号和次设备号组成。主设备号标识驱动程序,次设备号标识同一个驱动管理的不同设备。
设备号的分配有两种方式:静态分配和动态分配。静态分配就是你指定一个主设备号,调用register_chrdev_region注册,好处是设备号固定,生成的设备文件名固定,适合设备数量明确的产品;动态分配是让内核帮你挑一个没用的主设备号,用alloc_chrdev_region,好处是不会冲突,缺点是设备号不固定。
我建议优先使用动态分配,尤其是你自己学习调试的时候。原因很简单:你不知道你的系统里哪些主设备号已经占用了,硬编码一个可能冲突,一冲突加载就失败。
#define DEVICE_NAME "memdev" #define CLASS_NAME "mem_class" static int major_num; static struct class *mem_class = NULL; static struct device *mem_device = NULL; static int __init memdev_init(void) { dev_t dev_num; /* 动态分配设备号 */ alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); major_num = MAJOR(dev_num); /* 注册设备驱动 */ cdev_init(&mem_cdev, &mem_fops); mem_cdev.owner = THIS_MODULE; cdev_add(&mem_cdev, dev_num, 1); /* 自动创建设备文件 */ mem_class = class_create(THIS_MODULE, CLASS_NAME); mem_device = device_create(mem_class, NULL, dev_num, NULL, DEVICE_NAME); printk(KERN_INFO "memdev: major number %d\n", major_num); return 0; }注意上面的class_create和device_create,这两个函数的作用是让系统在/dev目录下自动生成设备文件,同时生成/sys/class/xxx相关的信息。不用它们也可以,你可以手动mknod /dev/memdev c 主设备号 0来创建设备文件,但自动创建省事得多,强烈推荐。
4.3 file_operations:驱动对用户态暴露的操作入口
file_operations结构体是字符设备驱动的核心,它定义了应用层对设备文件执行open、read、write、release等操作时,内核会调用驱动里的哪些函数。这是一个巨大的结构体,我们只需要把用到的函数指针赋值,剩下的保持NULL即可。
static const struct file_operations mem_fops = { .owner = THIS_MODULE, .open = mem_open, .read = mem_read, .write = mem_write, .release = mem_release, };结构体里的每个回调函数,内核都有统一的调用约定。比如read函数,原型是:
ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);参数的含义:filp是文件指针,对应应用层open返回的文件描述符在内核中的表示;buf是用户态缓冲区指针,注意,这个指针绝对不能在内核态直接解引用,必须用copy_to_user拷贝数据过去;count是应用层请求读取的字节数;f_pos是文件读写位置。
我见过很多初学者直接在mem_read里写*buf = xxx,然后模块一加载就重启虚拟机。原因就是直接访问了用户态指针,内核态没有权限,一访问就触发缺页异常,驱动没做好异常处理,整个内核Panic。
4.4 缓冲区管理和数据拷贝
设备内部需要一个缓冲区。这里我用内核的kzalloc分配一块4KB的内存:
static char *device_buffer; #define BUFFER_SIZE 4096 static int __init memdev_init(void) { device_buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL); if (!device_buffer) { printk(KERN_ERR "memdev: failed to allocate buffer\n"); return -ENOMEM; } return 0; }kzalloc和用户态的malloc类似,但它是内核态的分配函数,并且把分配的内存清零。GFP_KERNEL标志表示这是常规分配,可能会睡眠,所以不能用在中断上下文。
read和write函数的实现是驱动开发中真正的难点,因为涉及用户态和内核态的数据拷贝:
static ssize_t mem_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { size_t len; if (*f_pos >= BUFFER_SIZE) return 0; len = min(count, (size_t)(BUFFER_SIZE - *f_pos)); if (copy_to_user(buf, device_buffer + *f_pos, len)) { return -EFAULT; } *f_pos += len; return len; } static ssize_t mem_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { size_t len; len = min(count, (size_t)(BUFFER_SIZE - *f_pos)); if (len == 0) return -ENOSPC; if (copy_from_user(device_buffer + *f_pos, buf, len)) { return -EFAULT; } *f_pos += len; return len; }copy_to_user和copy_from_user为什么不用memcpy?因为它们内部会检查用户态缓冲区是否合法,并且在拷贝过程中处理缺页,整个过程是安全的。memcpy直接拷贝的话,一旦用户态指针非法,就是内核崩溃。
4.5 并发控制:驱动工程师最容易栽的坑
刚才的代码看着能用,但有个致命的问题:没有并发保护。如果两个进程同时调用mem_write,会出现什么情况?两个进程同一时刻操作*f_pos和device_buffer,数据就会错乱。
驱动为什么要特别关注并发?因为应用层的多线程、多进程是常态,而内核可能在任意时刻被抢占,硬件中断也可能随时打断执行。如果在这些情况下,共享数据没有保护,后果就是脏数据、内核崩溃,而且崩溃的位置可能和问题代码的位置完全不同,排查极其痛苦。
最简单有效的保护方式就是互斥锁。我在这个驱动里加上一个mutex,把read和write的临界区保护起来:
static struct mutex mem_mutex; static ssize_t mem_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { size_t len; mutex_lock(&mem_mutex); len = min(count, (size_t)(BUFFER_SIZE - *f_pos)); if (len == 0) { mutex_unlock(&mem_mutex); return -ENOSPC; } if (copy_from_user(device_buffer + *f_pos, buf, len)) { mutex_unlock(&mem_mutex); return -EFAULT; } *f_pos += len; mutex_unlock(&mem_mutex); return len; }mutex的使用规则很简单:lock之后到unlock之间是临界区,其他尝试lock的线程会在这里睡眠等待。这个驱动只有一个共享缓冲区,一个锁就够了。真实驱动里设备寄存器、中断共享数据、环形缓冲区各有各的同步需求,锁的粒度需要根据实际场景设计,锁得太粗性能上不去,锁得太细容易死锁。
4.6 清理函数和模块卸载
模块卸载时,必须把加载时申请的资源全部释放。顺序和加载时相反:
static void __exit memdev_exit(void) { device_destroy(mem_class, MKDEV(major_num, 0)); class_destroy(mem_class); if (mem_cdev.owner) cdev_del(&mem_cdev); unregister_chrdev_region(MKDEV(major_num, 0), 1); kfree(device_buffer); mutex_destroy(&mem_mutex); printk(KERN_INFO "memdev: module unloaded\n"); }资源清理这块没什么技术含量,但漏了任何一个就是内核内存泄漏。我见过有人忘了cdev_del,结果模块卸载后再加载,insmod报设备号冲突;还有人忘了kfree,加载卸载循环几百次之后系统内存被吃光。
4.7 完整代码和测试方法
整个驱动的完整代码就不分段贴了,核心部分在上面已经全部覆盖。你把上面的代码按顺序拼起来,加上头文件包含,就是一份能用的驱动代码。
编译加载之后,测试方法如下:
sudo insmod memdev.ko ls -l /dev/memdev echo "hello driver" > /dev/memdev cat /dev/memdevecho写入时,open、write、release会被依次调用;cat读取时,open、read、release会被调用。如果一切都正常,cat会输出你写入的内容。再用一个简单的C程序测试多进程并发读写,可以检查锁是否正常工作:
// test_memdev.c #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(void) { int fd = open("/dev/memdev", O_RDWR); if (fd < 0) { perror("open"); return -1; } char buf[1024]; for (int i = 0; i < 100; i++) { memset(buf, 'A' + (i % 26), sizeof(buf)); write(fd, buf, sizeof(buf)); lseek(fd, 0, SEEK_SET); read(fd, buf, sizeof(buf)); printf("iter %d: first byte = %c\n", i, buf[0]); } close(fd); return 0; }编译运行这个程序,如果锁没加对,你会发现buf[0]经常不是预期的值。这个测试虽然简单,但能非常直观地暴露并发问题。
5. 中断、锁和延迟:真实驱动绕不开的硬骨头
5.1 中断处理程序为什么要分为上下半部
字符设备驱动的框架熟悉之后,下一个真正的拦路虎是中断处理。大部分真实硬件(网卡、声卡、GPIO按键)都是通过中断通知CPU“有事发生”的。
中断处理程序有个铁律:一定要快。因为中断处理程序执行期间,当前进程被暂停,其他中断也可能被屏蔽,处理太慢会直接影响系统的实时性和吞吐量。但现实中很多设备的事件处理并不能短时间完成,比如一个网卡收到一包数据,要解析协议、投递到协议栈,这些操作可能耗时较长。
为了解决这个矛盾,Linux把中断处理拆成了两部分:上半部(top half)和下半部(bottom half)。上半部就是中断处理函数,它在中断上下文执行,只做最紧急的事情:读取硬件状态、清中断标志、把需要稍后处理的数据保存到缓冲区,然后立即返回,整个过程要求微秒级完成。下半部负责真正的耗时处理,比如数据解析、协议处理、唤醒等待队列,它在更宽松的环境下执行,可以被打断。
下半部的实现方式有几种,比如tasklet、工作队列、软中断。tasklet的特点是执行在软中断上下文,不能睡眠;工作队列执行在进程上下文,可以睡眠。选择哪种,取决于你要处理的任务是否能睡眠。初学者记住:能在进程上下文做的事,优先工作队列;对实时性要求高、处理内容简单的,用tasklet。
5.2 自旋锁和互斥锁,怎么选
内核里还有一类锁叫自旋锁。它和mutex最大的区别是:拿不到锁的时候不会睡眠,而是在原地“自旋”,也就是忙等待,直到拿到锁。
什么时候用自旋锁?最重要的场景是中,中断处理程序里。因为中断处理程序不能睡眠(它没有进程上下文),不能用mutex,只能用自旋锁保护共享数据。另一个判断依据是临界区的大小,如果临界区只是几个寄存器的读写、一个变量的累加,几十个周期就能完成,用自旋锁比mutex更高效,因为mutex的睡眠唤醒开销在这种场景下反而是浪费。
但是自旋锁有个大坑:临界区绝对不能睡眠,否则系统直接死锁。再一个,自旋锁在单核CPU上其实是个空操作,因为关了内核抢占就等于保护了,但这只是优化细节,编码上还是要按多核的标准来写。
我实际开发中总结的经验是:能用mutex就不用自旋锁,尽量在进程上下文中完成复杂操作,把中断里只留最小的处理逻辑。混用锁的时候要极端小心锁的顺序,ABBA就是最常见的死锁模式。
5.3 内核延迟:忙等还是睡眠等待
驱动开发中经常需要做延迟等待,比如需要等待某个硬件操作完成,或者在两个寄存器操作之间需要保证一定的时序。
内核里延迟大致分两类:忙等待和睡眠等待。忙等待用udelay和mdelay,它们让CPU空转,期间不释放CPU,适合延迟时间很短(微秒级)的场景,比如等待某个寄存器状态翻转;睡眠等待用usleep_range、msleep、schedule_timeout等,它们会让出CPU,适合延迟时间较长(毫秒级以上)、对系统吞吐量有要求的场景。
这里要特别提一个很多人踩的坑:在中断上下文或者持有自旋锁的情况下,绝对不能用sleep类函数。中断上下文不能睡眠,持有自旋锁时睡眠会触发系统直接死锁或者崩溃。我调试过的很多崩溃,最后定位都是这个问题。需要延迟,先用代码注释标清楚当前上下文,再选择对应的延迟函数,这是非常必要的编码习惯。
6. 调试设备驱动的实用技巧
6.1 printk:最朴素但最有效的调试手段
内核调试手段有很多,kprobe、ftrace、kgdb、QEMU调试,但说句实在的,我用得最多的还是printk。为什么?简单直接,不需要额外的硬件和调试工具,随时随地打印日志。
printk的使用和printf类似,但它有日志级别控制。常用的是KERN_INFO、KERN_DEBUG、KERN_ERR这几个。日志级别的作用是:只有级别高于当前控制台日志级别的消息才会打印到终端,但不管是否打印到终端,所有消息都会进入dmesg缓冲区。
调试阶段我建议把printk的级别设为KERN_DEBUG,然后通过调整/proc/sys/kernel/printk来让所有日志直接打到控制台:
echo 8 > /proc/sys/kernel/printk这样你就能在系统日志里实时看到驱动的输出,配合printk在关键路径上打点:进入函数、关键分支、错误处理、离开函数。驱动的执行流程就一目了然了。
但printk也有局限。你不能在中断处理程序里做复杂的打印,因为printk本身可能睡眠;还有,printk输出会拖慢实时性,生产环境一定要去掉或者用动态调试替代。
6.2 使用QEMU调试内核模块
当你遇到一个比较难缠的Bug,反复看代码、加printk都定位不了的时候,我推荐用QEMU + GDB的方式调试内核。QEMU可以模拟一台完整的机器来运行Linux内核,配合GDB可以设置断点、单步执行、查看内核变量。
这个方法适合研究内核源码、定位复杂的驱动问题,缺点是启动配置比较复杂,学习和使用成本高。平时用printk就够了,但建议花点时间搭一套QEMU调试环境,关键时刻能救命。
6.3 常见崩溃信息怎么看
内核崩溃时的信息虽然看着吓人,但其实是定位问题的最好线索。最常见的崩溃是NULL pointer dereference,内核会打印出完整的调用栈、寄存器状态、出错地址。
看崩溃信息,重点看这几个部分:第一,Oops/Panic提示信息中给出的出错的函数名和行号;第二,调用栈(Call Trace),从栈里的函数调用序列可以判断出是谁调了谁;第三,寄存器信息中的指令指针(RIP),对应哪个函数一眼便知。配合objdump或者addr2line,把RIP地址转换成源码行号,问题基本就明确了。
7. 驱动开发中的常见问题速查
下面是我整理的一份常见问题表,覆盖了驱动开发入门阶段能遇到的绝大多数问题。这些问题我全都实际踩过,列出来帮你避坑。
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| insmod提示Operation not permitted | 模块签名校验失败或权限不足 | 关闭Secure Boot,获取root权限 |
| insmod提示Unknown symbol | 模块依赖的符号未导出 | 检查内核配置和依赖模块 |
| rmmod提示Module is in use | 设备文件仍被占用 | 检查应用的fd,执行lsof /dev/xxx |
| 内核Panic提示NULL pointer | 解引用了空指针 | 检查kmalloc/kzalloc的返回值 |
| copy_to_user返回非零 | 用户态缓冲区非法 | 不要传NULL,检查用户态内存是否有效 |
| 并发读写数据错乱 | 缺少锁保护 | 加mutex/spinlock保护共享数据 |
| 中断处理函数中睡眠 | 触发内核崩溃 | 确认上下文,改用到tasklet或工作队列 |
| cat设备文件卡死 | read函数没有正确返回0 | 检查EOF条件 |
| 写设备文件返回No space left | 缓冲区满了但没到EOF | 检查写偏移和缓冲区长度计算 |
| 反复insmod/rmmod后内存耗尽 | 模块退出未释放内存 | 检查exit函数是否完整释放资源 |
| dmesg无任何输出 | printk级别高于控制台级别 | 调整/proc/sys/kernel/printk |
这里重点说一下第一个问题。现在很多发行版默认开了Secure Boot,加载未签名的内核模块会被拒绝。解决方式一般是在BIOS里关闭Secure Boot,或者给模块签名。个人开发调试建议直接关掉,生产环境才需要正规的签名流程。
8. 学习驱动开发,我的几条实际建议
根据我这十几年的项目经验和带新人的心得,最后分享几个方向性的建议。
第一,先做减法再做加法。不要一上来就想写一个网卡驱动或者GPU驱动,那是很多年后的事情。老老实实把字符设备驱动吃透,把read、write、ioctl搞明白,再去碰中断、DMA、块设备、网络设备,每一步都找一个小项目练手。字符设备驱动用熟了,其他类型的驱动无非是换了一套框架和接口,内核的底层逻辑是相通的。
第二,手头常备内核源码。在线看代码虽然方便,但内核源码树里有大量的头文件、宏定义、示例代码,离线查询速度快得多。而且你会发现,很多驱动开发的疑问看代码就能解决,比在网上搜答案更直接准确。
第三,一定要会看内核文档。内核自带Documentation目录下有大量驱动开发相关文档,还有内核源码里的注释都是极好的参考资料。遇到一个函数不知道用法,直接在内核源码里找它的实现,比看二手博客靠谱得多。
第四,养成写驱动时随手记日志的习惯。很多驱动问题在开发阶段是随机出现的,可能在加载第100次才暴露,没有日志几乎没法定位。我在代码里喜欢加一个“进入/退出函数”级别的DEBUG日志,上线前再统一清理或者用dynamic_debug控制开关,这样既保证了开发效率,也兼顾了生产环境的日志干净度。
第五,如果条件允许,买一块真实的开发板(树莓派、IMX6ULL、STM32MP1都行),接上GPIO、传感器、显示屏这些外设,写真正的硬件驱动。虚拟设备驱动只能让你学会框架,真实硬件的体验完全不同——你要面对读寄存器时序不对、中断触发方式不对、数据线接反等各种现实问题,这些才是驱动开发工作的常态,也是这门手艺真正的价值所在。
我这些年带过的不少人,应用层写得很溜,一到驱动就发怵,但真正深入进去之后,他们会发现驱动开发并不比应用开发更难,只是规则更多、更底层,一旦掌握了套路,成就感是完全不一样的。希望这篇文章能帮你迈过这道坎,享受控制硬件底层的那种独特乐趣。