【Linux驱动开发完全指南】从零开始搞定IMX6ULL驱动开发——小白入门 + 完整流程 + 面试攻略
【Linux驱动开发完全指南】从零开始搞定IMX6ULL驱动开发——小白入门 + 完整流程 + 面试攻略前言:这篇文章是我在学习IMX6ULL驱动开发过程中,把01到24节的内容全部走了一遍之后,做的一次系统性的梳理和扩展。内容面向想从零入门Linux驱动的同学,也涵盖了不少面试高频考点。如果你也在啃这块骨头,希望这篇能帮你少走一些弯路。目录环境搭建篇第一个驱动程序字符设备驱动框架GPIO子系统与Pinctrl子系统设备树(Device Tree)中断机制内核同步机制高级驱动机制子系统深入实战案例常见踩坑与注意事项面试高频问题汇总一、环境搭建篇1.1 交叉编译环境安装学驱动开发,环境搭好是第一步。我们在x86的虚拟机上编写代码,交叉编译成ARM架构的二进制,再跑到开发板上。交叉编译工具链安装:# 解压工具链(以arm-linux-gnueabihf为例)tar-xjfarm-linux-gnueabihf-gcc-xxx.tar.bz2-C/usr/local/# 添加到PATH(写入 ~/.bashrc)exportPATH=$PATH:/usr/local/arm-linux-gnueabihf/bin# 验证arm-linux-gnueabihf-gcc-v⚠️注意:工具链版本要和内核版本匹配,否则编译出来的模块可能加载失败,报version magic不匹配的错误。1.2 烧写Linux系统到IMX6ULLIMX6ULL的烧写一般有两种方式:MfgTool(NXP官方烧写工具):通过USB OTG烧写到eMMC/SD卡SD卡直接写入:用dd命令写镜像# 使用dd烧写镜像到SD卡(/dev/sdX 替换为你的设备)sudoddif=imx6ull-14x14-evk.imgof=/dev/sdXbs=4Mstatus=progresssync1.3 网络环境配置(静态IP + NFS挂载)开发阶段强烈推荐用NFS挂载根文件系统 + TFTP加载内核,这样改了文件不用每次重新烧写,省时省力。开发板静态IP设置:# 在开发板串口终端执行ifconfigeth0192.168.1.100 netmask255.255.255.0 routeadddefault gw192.168.1.1# 或者修改 /etc/network/interfaces(永久生效)auto eth0 iface eth0 inet static address192.168.1.100 netmask255.255.255.0 gateway192.168.1.1虚拟机NFS服务端配置:# 安装NFSsudoaptinstallnfs-kernel-server# 编辑 /etc/exports/home/user/nfs *(rw,sync,no_subtree_check,no_root_squash)# 重启NFS服务sudoexportfs-asudoservicenfs-kernel-server restartuboot配置TFTP+NFS启动:# 在uboot命令行设置环境变量setenv serverip192.168.1.200# 服务器IP(虚拟机)setenv ipaddr192.168.1.100# 开发板IPsetenv nfsroot /home/user/nfs# NFS根目录# 设置启动命令setenv bootcmd'tftp 0x80800000 zImage; tftp 0x83000000 imx6ull-14x14-evk.dtb; bootz 0x80800000 - 0x83000000'setenv bootargs'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.200:/home/user/nfs,proto=tcp rw ip=192.168.1.100'saveenv boot虚拟机与开发板互传文件的三种方式:方式命令适用场景SCPscp file user@ip:/path小文件传输SFTPsftp user@ip交互式传输NFS挂载后直接读写开发调试首选二、第一个驱动程序:hello驱动2.1 Linux驱动的本质Linux驱动本质上是一个内核模块(Kernel Module),它以.ko文件形式存在,可以动态加载/卸载,不需要重新编译整个内核。驱动程序 = 一堆回调函数的注册应用层调用open/read/write/ioctl→ VFS → 驱动层对应的file_operations2.2 最简Hello驱动代码#includelinux/module.h#includelinux/kernel.h#includelinux/init.h// 驱动加载时执行(insmod)staticint__inithello_init(void){printk(KERN_INFO"Hello, Linux Driver!\n");return0;}// 驱动卸载时执行(rmmod)staticvoid__exithello_exit(void){printk(KERN_INFO"Goodbye, Linux Driver!\n");}module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("你的名字");MODULE_DESCRIPTION("第一个Linux驱动");2.3 Makefile编写# 内核源码路径(替换为你的路径) KERNEL_DIR := /home/user/linux-4.9.88 # 当前目录 PWD := $(shell pwd) # 编译目标 obj-m := hello.o all: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) cleanMakefile解析:obj-m := hello.o:告诉内核构建系统把hello.c编译成模块-C $(KERNEL_DIR):切换到内核源码目录,使用内核的 Kbuild 系统M=$(PWD):返回当前目录编译模块为什么用内核的构建系统?因为需要内核头文件、编译标志等,手动写gcc命令很麻烦编译 测试:# 编译make# 加载模块insmod hello.ko# 查看内核日志dmesg|tail-20# 卸载模块rmmod hello# 查看已加载的模块lsmod|grephello# 查看模块信息modinfo hello.ko2.4 带设备节点的hello驱动(APP与驱动交互)#includelinux/module.h#includelinux/kernel.h#includelinux/fs.h#includelinux/uaccess.h#includelinux/device.h#defineDEVICE_NAME"hello_dev"#defineCLASS_NAME"hello_class"staticintmajor;staticstructclass*hello_class;staticstructdevice*hello_device;staticcharkernel_buf[64]="Hello from kernel!\n";staticinthello_open(structinode*inode,structfile*file){printk(KERN_INFO"hello_open\n");return0;}staticssize_thello_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos){intlen=strlen(kernel_buf);if(copy_to_user(buf,kernel_buf,len))return-EFAULT;returnlen;}staticssize_thello_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos){if(copy_from_user(kernel_buf,buf,count))return-EFAULT;kernel_buf[count]='\0';printk(KERN_INFO"Received from user: %s\n",kernel_buf);returncount;}staticstructfile_operationshello_fops={.owner=THIS_MODULE,.open=hello_open,.read=hello_read,.write=hello_write,};staticint__inithello_init(void){// 动态分配主设备号major=register_chrdev(0,DEVICE_NAME,hello_fops);if(major0){printk(KERN_ERR"register_chrdev failed\n");returnmajor;}// 自动创建设备节点(需要udev/mdev支持)hello_class=class_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(hello_class)){unregister_chrdev(major,DEVICE_NAME);returnPTR_ERR(hello_class);}hello_device=device_create(hello_class,NULL,MKDEV(major,0),NULL,DEVICE_NAME);if(IS_ERR(hello_device)){class_destroy(hello_class);unregister_chrdev(major,DEVICE_NAME);returnPTR_ERR(hello_device);}printk(KERN_INFO"hello_dev registered, major = %d\n"