Linux驱动02-驱动工程搭建 一、linux 系统中驱动的分类字符设备驱动包括 GPIO、I2C、SPI、ADC、UART、LCD、摄像头等设备。这类驱动以字节为单位进行操作对数据顺序有严格要求且数据吞吐量较小。在设备驱动中占比最高超过 95%。块设备驱动主要用于存储设备如 eMMC、NAND 闪存、SD 卡和 U 盘等。支持随机字节访问按固定大小的数据块进行读写操作具有较大的数据吞吐量。网络设备驱动支持网络协议栈的设备驱动包括 WiFi、蓝牙、ZigBee 和 CAN 等通信模块。二、驱动的编写2.1 静态加载内核源码内部写的驱动编写步骤驱动在zImage 中① 编写驱动.c 在linux内核下的/drivers/char/文件下创建mydrv.c文件/home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char//mydrv.c #include linux/init.h #include linux/module.h static int __init mydrv_init(void) { printk(mydrv_init ok\n); //内核中类似printf函数 return 0; } static void __exit mydrv_exit(void) { pr_info(mydrv_exit); //带输出等级的输出,一般信息 return; } module_init(mydrv_init); //驱动模块加载入口 module_exit(mydrv_exit); //驱动模块卸载入口 MODULE_LICENSE(GPL);② 修改Kconfig在char目录下找到 kconfig 文件添加以下内容控制菜单选择功能config MYDRV //这个是驱动控制变量名 在Makefile中要用 bool this my first dev test //bool 是 Y N default n //选择的默认状态 help //说明信息 This is my first dev test, hello world③ 修改Makefile在char目录下找到 Makefile 文件添加以下内容obj-$(CONFIG_MYDRV) mydrv.o //#CONFIG_MYDRV 这个变量有menuconfig 中的选项决定④ 执行 make menuconfig 在Device Drivers - Character devices 菜单下勾选所需的驱动程序 [*]三态 * 、 Y 包含、 N 排除、 M 功能模块化bool [ * ]内置、[ ]排除Enter 选择子菜单 、按空格进行选中、save保存配置make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig⑤ 再进行全部编译不是自己写的.c文件导致报错的执行make clean 清理下整个工程然后再执行编译make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- all -j16⑥ 移动zImage 到tftpboot下cp linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/boot/zImage ~/tftpboot/⑦ 开发板上电打开MOBAX连接开发板验证驱动是否加载成功dsemg |grep mydrv⑧ 显示如下即可加载成功2.2 动态加载(源码目录外编写驱动)编写步骤① 目录的结构在家目录下创建目录并创建以下文件② 先编译源码生成zImage文件先决条件③ 编写drv.c 和 Makefile文件//drv.c #include linux/init.h #include linux/module.h static int __init mydrv_init(void) { pr_info(init_hello\n); return 0; } static void __exit mydrv_exit(void) { pr_info(exit_hello\n); pr_err(err_hello\n); return; } module_init(mydrv_init); module_exit(mydrv_exit); MODULE_LICENSE(GPL);//drv下的Makefile //内核源码目录 KER_DIR :/home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek //当前目录 CUR_DIR :$(shell pwd) obj-m drv.o all: $(MAKE) -C $(KER_DIR) M$(CUR_DIR) modules cp *.ko ~/nfs/rootfs clean: $(MAKE) -C $(KER_DIR) M$(CUR_DIR) clean rm ~/nfs/rootfs/*.ko④ 编写app下main.c和Makefile//main.c #include stdio.h int main() { return 0; }//Makefile SRC: main.c CC : arm-linux-gnueabihf-gcc all:$(SRC) $(CC) $(SRC) -o app cp ./app ~/nfs/rootfs clean: rm ~/nfs/rootfs/app⑤ 编写总的MakefileAPP:./app DRV./drv all: $(MAKE) -C $(APP) all $(MAKE) -C $(DRV) all clean: $(MAKE) -C $(APP) clean $(MAKE) -C $(DRV) clean⑥ 执行编译生成.ko文件⑦ 连接开发板进入系统手动加载驱动insmod drv.ko //手动加载驱动 mknod /dev/myled c 248 0 //手动创建设备节点 ls /dev //查看dev下是否生成myled文件 ./app //执行驱动2.3 重要的宏与命令① 内核常用函数及宏功能表函数/宏名功能描述module_init指定驱动模块加载时的入口函数内核加载模块时自动调用module_exit指定驱动模块卸载时的入口函数内核卸载模块时自动调用MODULE_LICENSE(_license)MODULE_LICENSE(GPL)声明模块的许可证如GPL避免内核污染警告printk(char *s, ...)内核态打印函数支持格式化输出避免浮点数输出等级需通过配置文件控制pr_info(fmt, ...)封装了printk的宏默认输出等级为KERN_INFO用于打印一般信息pr_err(fmt, ...)封装了printk的宏默认输出等级为KERN_ERR用于打印错误信息注意输出等级printk的输出行为受/proc/sys/kernel/printk配置控制优先级高于配置值的消息会被过滤。浮点数限制内核态不支持浮点运算printk及相关宏禁止使用%f等浮点格式化符。宏扩展pr_info和pr_err等宏实际调用printk并自动添加优先级前缀如KERN_INFO。② 重要的命令make distclean 源码的彻底清除大清除内核恢复默认状态 make clean 小清除 只会删除 .o 文件。配置文件是保留(.config) make zImage 只编译内核文件 make dtbs 只编译设备树 make modules 编译所有的模块 make all 编译所以 zimge,dtbs,ko insmod drv.ko //动态加载驱动 lsmod //显示已经加载的模块 rmmod drv.ko //卸载模块 mknod /dev/myled c 248 0 //手动创建设备节点 tar -cJvf 01drv_temp.tar.xz ./01drv_temp/ //tar压缩命令 tar -xvf 01drv_temp.tar.xz //tar解压缩三、字符设备驱动的编写3.1 字符设备驱动编写步骤字符设备注册标准四步走的核心环节完整流程是提前通过alloc_chrdev_region()或者register_chrdev_region()向内核申请好设备号定义一个全局的struct cdev my_cdev;设备实例调用cdev_init(my_cdev, fops)完成基础初始化把你之前写好的fops传入调用cdev_add(my_cdev, dev_no, 1)把这个初始化完成的cdev正式加入内核的设备管理系统此时这个字符设备就正式活了用户态可以通过设备文件访问它① 申请设备号设备号由主设备号和次设备号构成主设备号用于标识特定驱动程序次设备号则用于区分使用该驱动的不同设备。在Linux系统中设备号采用32位编码方式其中高12位为主设备号取值范围0~4095低20位为次设备号。静态申请int register_chrdev_region(dev_t from, unsigned count, const char *name)功能从你指定的起始设备号from开始连续申请count个字符设备号不会由内核自动避让已占用的号码。参数含义from你预先指定的起始设备号一般通过MKDEV(你选定的主设备号, 起始次设备号)宏合成得到count需要连续申请的设备号总数量name驱动/设备的名称会显示在/proc/devices文件中返回值分配成功返回0失败返回负的错误码动态申请int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);功能特点不需要你手动指定主设备号内核会自动从系统中挑选一段完全空闲、没有被其他驱动占用的连续设备号分配给你从根源上避免了静态分配容易出现的设备号冲突问题是实际驱动开发中最推荐的方式。参数含义dev输出参数申请成功后内核会把生成的完整设备号写入这个指针指向的dev_t变量baseminor指定起始次设备号绝大多数场景下直接填0即可count你需要连续申请的设备号总数量name设备的名称会直接显示在/proc/devices文件中返回值申请成功返回0失败返回负的错误码。注意模块卸载时要卸载设备号void unregister_chrdev_region(dev_t from, unsigned count);from你之前申请到的起始设备号count你当初连续申请的设备号总数量如果驱动里还使用了cdev注册字符设备卸载时必须先删除字符设备结构体再释放设备号。static dev_t dev_no; // 12主 20次 static int __init mydrv_init(void) { int ret 0; // 自动申请设备号 ret alloc_chrdev_region(dev_no, 0, 10, myled); if (ret 0) { pr_err(alloc_chrdev_region error, errno :%d\n, ret); return ret; } }② 准备 file_operation是驱动程序和用户态系统调用之间的对接接口准备file_operation文件操作结构体注册应用层对应的open、close、read和write函数接口并为相应的函数指针赋值。ssize_t myread(struct file *f, char __user *userdata, size_t size, loff_t *off); ssize_t mywrite(struct file *f, const char __user *userdata, size_t size, loff_t *off); int myopen(struct inode *inode, struct file *f); int myrelease(struct inode *inode, struct file *f); struct file_operations fops { .owner THIS_MODULE, .open myopen, .release myrelease, .read myread, .write mywrite, }; ssize_t myread(struct file *f, char __user *userdata, size_t size, loff_t *off) { pr_info(read caller\n); return 0; } ssize_t mywrite(struct file *f, const char __user *userdata, size_t size, loff_t *off) { pr_info(write caller\n); return 0; } int myopen(struct inode *inode, struct file *f) { pr_info(open caller\n); return 0; } int myrelease(struct inode *inode, struct file *f) { pr_info(close caller\n); return 0; }③ 初始化字符设备结构体初始化字符设备结构体并关联文件操作fops需调用cdev_init函数。函数原型void cdev_init(struct cdev *cdev, const struct file_operations *fops)cdev_init的本质是做struct cdev结构体的初始化工作它会帮你自动初始化cdev内部的链表、引用计数等内核底层成员直接把你传入的file_operations结构体指针绑定到cdev的对应字段里完成设备和你自己写的操作函数的关联省去你手动给struct cdev一个个成员赋值的繁琐步骤是Linux驱动开发的标准初始化流程。参数含义struct cdev *cdev指向你自己定义的字符设备实例结构体的指针这个结构体是内核用来管理单个字符设备的核心对象每个独立的字符设备都要有一个自己的cdev变量。const struct file_operations *fops自己定义的那个文件操作函数表的指针把你实现的open/read/write等函数关联到这个字符设备上。通过cdev_add函数将字符设备添加到内核链表中。函数原型int cdev_add(struct cdev *p, dev_t dev, unsigned count)cdev_add的本质是完成struct cdev对象的最终注册动作把你之前通过cdev_init初始化好的字符设备对象挂载到内核的字符设备全局哈希链表中将你传入的设备号和这个cdev对象做绑定内核后续收到用户态对该设备号的open/read等系统调用时就能直接找到对应的驱动函数执行自动初始化cdev剩余的底层成员包括引用计数、内核对象关联让设备从驱动里的一个结构体变量变成内核认可的可用字符设备。参数含义struct cdev *p指向你之前用cdev_init初始化完成的字符设备对象指针不能传未初始化、或者已经调用过cdev_del销毁的野对象。dev_t dev提前向内核申请好的字符设备号是主设备号次设备号组成的32位组合值必须是通过alloc_chrdev_region或register_chrdev_region申请到的合法设备号不能自己随便写数值。unsigned count这个设备要关联的连续次设备号数量最常用的取值是1代表这一个cdev对象只对应1个字符设备如果要一次性注册多个同属性的次设备可以填对应数量比如填3就会占用dev、dev1、dev2三个连续次设备号。返回值说明调用成功返回0失败返回负的错误码-EEXIST。static struct cdev mycdev; //全局的 //初始化字符设备结构体并关联文件操作结构体 cdev_init(mycdev, fops); //注册字符设备到内核 ret cdev_add(mycdev, dev_no, 10); if (ret 0) { pr_err(cdev_add error, errno :%d\n, ret); unregister_chrdev_region(dev_no, 10); return ret; } pr_info(major:%d , minor:%d\n, MAJOR(dev_no), MINOR(dev_no));注意事项需手动创建设备节点例如设备号为 248 的 0 号节点。创建命令如下cat /proc/devices //查看设备类出现设备类后没有具体的设备 //动创建节点创建具体的设备主设备号 248 次设备号0 mknod /dev/led0 c 248 0目的确保设备号与驱动匹配。只有当设备号与驱动的设备号一致时二者才能正确关联。3.2 相关函数接口① cdev操作函数接口函数名功能描述cdev_init用于初始化已分配的字符设备结构体将其与文件操作函数集关联。cdev_alloc动态分配并返回一个新的字符设备结构体需配合cdev_init使用。cdev_add将初始化完成的字符设备注册到内核指定设备号范围。cdev_del从内核移除已注册的字符设备并释放相关资源。② 字符设备操作接口函数名功能描述适用场景内核版本建议register_chrdev_region静态注册已知设备号范围需提前确定主/次设备号设备号已明确规划时使用2.6及以上推荐alloc_chrdev_region动态分配设备号并注册内核自动分配主设备号设备号无需固定时使用2.6及以上推荐unregister_chrdev_region释放已注册的设备号范围静态/动态注册均适用模块卸载时调用通用