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

嵌入式Linux驱动开发 01:基础开发与使用

文章目录目的基础说明驱动测试应用程序基础开发与使用驱动模块入口与出口驱动模块安装与卸载字符设备注册与注销设备开关与读写自动创建与销毁设备节点使用 VS Code 进行开发总结目的驱动开发是嵌入式Linux中工作比重比较大的一部分。这篇文章将记录下最基本的驱动开发过程。这篇文章中内容均在下面的开发板上进行测试《新唐NUC980使用记录自制开发板基于NUC980DK61YC》这篇文章主要是在下面文章基础上进行的《新唐NUC980使用记录访问以太网LAN8720A 启用SSH》基础说明对于驱动程序而言从不同角度来看对它的认知是不同的。从用户应用程序来看——Linux中一切皆文件系统中各种设备也都是文件通常/dev/目录下的就是各种设备文件。对于文件来说用户操作时无非就是open / close / read / write等方法。那么这些设备的驱动程序无非就是要实现这些方法提供给用户使用。驱动开发主要就是实现上面提到的各种文件操作的方法及其具体对硬件的操作。除了实现这些方法还需要用某种机制将这些方法和用户应用程序中各种文件操作方法关联起来通常就是注册下。Linux中驱动程序主要可以分为字符设备驱动、块设备驱动、网络设备驱动、复合设备驱动等这里主要以字符设备驱动展开说明。Linux中驱动程序可以直接编译到内核中也可以编译成*.ko形式的文件然后在使用时再安装。为了方便测试这里主要先以这种形式展开说明。驱动测试应用程序编写完驱动程序后最终需要进行测试这里先准备个测试程序。根据上一章节的内容这里的测试程序主要就是对根据驱动程序生成的设备文件进行 open / close / read / write 等操作。cd~/nuc980-sdk/mkdir-pdrivers/char_devcddrivers/char_dev/ gedit char_dev_test.c在文件中写入下面代码#includestring.h#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.h/* 该程序用于读写测试本文要编写的驱动程序对应生成的设备 */intmain(intargc,char**argv){intfd;charbuf[4096];intlen;/* 判断参数 */if((argc3)||(argc4)){printf(Usage: char_dev_test devpath -w string\n);// 写数据printf( char_dev_test devpath -r\n);// 读数据return-1;}/* 打开文件 */fdopen(argv[1],O_RDWR);if(fd0){printf(applog: can not open file %s\n,argv[1]);return-1;}/* 写数据或读数据 */if((0strcmp(argv[2],-w))(argc4)){lenstrlen(argv[3])1;write(fd,argv[3],len);}elseif((0strcmp(argv[2],-r))(argc3)){lenread(fd,buf,4096);buf[4095]\0;printf(applog: read - %s\n,buf);}else{close(fd);return-1;}close(fd);return0;}编译生成可执行文件# 配置编译工具链exportPATH$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/bin# 编译生成开发板的可执行文件arm-linux-gcc-ochar_dev_test char_dev_test.c# 开发板启用了SSH的话可以使用SCP命令将程序通过网络拷贝到开发板中scpchar_dev_test root192.168.31.142:/root/基础开发与使用驱动模块入口与出口# cd ~/nuc980-sdk/drivers/char_dev/gedit char_dev.c在文件中写入下面代码#includelinux/module.hstaticint__initchar_dev_init(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);return0;}staticvoid__exitchar_dev_exit(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);}module_init(char_dev_init);// 模块入口module_exit(char_dev_exit);// 模块出口MODULE_LICENSE(GPL);// 模块许可上面代码中printk用在内核中用于打印内核日志。MODULE_LICENSE(GPL);只是个声明但不加的话驱动安装时会有个提示。创建Makefile文件gedit Makefile在文件中写入下面代码编译驱动模块需要用到内核源码下面需要指定正确的路径# 配置内核源码路径KERNEL_DIR :/home/nx/nuc980-sdk/NUC980-linux-4.4.y MODULE_DIR :$(shellpwd)obj-mchar_dev.o# 以模块方式编译驱动all:$(MAKE)-C$(KERNEL_DIR)M$(MODULE_DIR)modules clean:$(MAKE)-C$(KERNEL_DIR)M$(MODULE_DIR)modules clean编译生成驱动模块# export PATH$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/binmake# 编译后生成的char_dev.ko就是驱动模块拷贝到开发板上scpchar_dev.ko root192.168.31.142:/root/驱动模块安装与卸载在开发板上使用insmod来安装驱动模块使用rmmod卸载驱动模块。使用lsmod查看已安装的驱动模块需要注意的是上面模块安装和卸载时出现的内核日志只有在串口终端中才会出现如果时通过SSH的终端那么需要使用dmesg命令来查看内核日志上面出现了多条日志信息前面一些是之前测试时产生的。字符设备注册与注销下面的字符设备驱动的编写方式是Linux 2.6 内核之前的旧方式现在字符设备主要使用cdrv方式。完成了最基本的模块和使用接下来开始准备字符设备驱动将驱动代码改成如下#includelinux/module.h#includelinux/fs.hstaticintmajor0;constchar*dev_namechar_dev;staticconststructfile_operationschar_dev_fops{.ownerTHIS_MODULE,};staticint__initchar_dev_init(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);majorregister_chrdev(0,dev_name,char_dev_fops);// 注册字符设备第一个参数0表示让内核自动分配主设备号return0;}staticvoid__exitchar_dev_exit(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);unregister_chrdev(major,dev_name);// 注销字符设备}module_init(char_dev_init);// 模块入口module_exit(char_dev_exit);// 模块出口MODULE_LICENSE(GPL);// 模块许可重新编译后在开发板上安装模块就可以在/proc/devices中看到字符类型设备驱动了设备开关与读写接下来继续完善驱动代码添加相应操作功能#includelinux/module.h#includelinux/fs.h#includelinux/uaccess.hstaticintmajor0;staticconstchar*dev_namechar_dev;staticchardev_buf[4096];#defineMIN(a,b)((a)(b)?(a):(b))staticintchar_dev_open(structinode*node,structfile*file){// printk(modlog: func %s, line %d.\n, __FUNCTION__, __LINE__);return0;}staticintchar_dev_close(structinode*node,structfile*file){// printk(modlog: func %s, line %d.\n, __FUNCTION__, __LINE__);return0;}staticssize_tchar_dev_read(structfile*file,char__user*buf,size_tsize,loff_t*offset){intret;retcopy_to_user(buf,dev_buf,MIN(size,4096));// 从内核空间拷贝数据到用户空间returnret;}staticssize_tchar_dev_write(structfile*file,constchar__user*buf,size_tsize,loff_t*offset){intret;retcopy_from_user(dev_buf,buf,MIN(size,4096));// 从用户空间拷贝数据到内核空间returnret;}staticconststructfile_operationschar_dev_fops{.ownerTHIS_MODULE,.openchar_dev_open,.releasechar_dev_close,.readchar_dev_read,.writechar_dev_write,};staticint__initchar_dev_init(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);majorregister_chrdev(0,dev_name,char_dev_fops);// 注册字符设备第一个参数0表示让内核自动分配主设备号return0;}staticvoid__exitchar_dev_exit(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);unregister_chrdev(major,dev_name);// 注销字符设备}module_init(char_dev_init);// 模块入口module_exit(char_dev_exit);// 模块出口MODULE_LICENSE(GPL);// 模块许可重新编译后在开发板上安装模块接着使用mknod来创建设备节点文件然后就可以使用前面的应用程序进行读写测试了卸载驱动后使用mknod创建的设备节点可以使用rm进行删除。自动创建与销毁设备节点上面测试中安装模块后还需要手动创建设备节点其实也可以在安装时自动生成设备节点在卸载时自动删除设备节点#includelinux/module.h#includelinux/fs.h#includelinux/uaccess.h#includelinux/device.hstaticintmajor0;staticconstchar*char_dev_namechar_dev;staticstructclass*char_dev_class;staticstructdevice*char_dev_device;staticchardev_buf[4096];#defineMIN(a,b)((a)(b)?(a):(b))staticintchar_dev_open(structinode*node,structfile*file){// printk(modlog: func %s, line %d.\n, __FUNCTION__, __LINE__);return0;}staticintchar_dev_close(structinode*node,structfile*file){// printk(modlog: func %s, line %d.\n, __FUNCTION__, __LINE__);return0;}staticssize_tchar_dev_read(structfile*file,char__user*buf,size_tsize,loff_t*offset){intret;retcopy_to_user(buf,dev_buf,MIN(size,4096));// 从内核空间拷贝数据到用户空间returnret;}staticssize_tchar_dev_write(structfile*file,constchar__user*buf,size_tsize,loff_t*offset){intret;retcopy_from_user(dev_buf,buf,MIN(size,4096));// 从用户空间拷贝数据到内核空间returnret;}staticconststructfile_operationschar_dev_fops{.ownerTHIS_MODULE,.openchar_dev_open,.releasechar_dev_close,.readchar_dev_read,.writechar_dev_write,};staticint__initchar_dev_init(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);majorregister_chrdev(0,char_dev_name,char_dev_fops);// 注册字符设备第一个参数0表示让内核自动分配主设备号char_dev_classclass_create(THIS_MODULE,char_dev_class);//if(IS_ERR(char_dev_class)){unregister_chrdev(major,char_dev_name);return-1;}char_dev_devicedevice_create(char_dev_class,NULL,MKDEV(major,0),NULL,char_dev_name);// 创建设备节点创建设备节点成功后就会出现/dev/char_dev_name的设备文件if(IS_ERR(char_dev_device)){device_destroy(char_dev_class,MKDEV(major,0));unregister_chrdev(major,char_dev_name);return-1;}return0;}staticvoid__exitchar_dev_exit(void){printk(modlog: func %s, line %d.\n,__FUNCTION__,__LINE__);device_destroy(char_dev_class,MKDEV(major,0));// 销毁设备节点销毁后/dev/下设备节点文件就会删除class_destroy(char_dev_class);unregister_chrdev(major,char_dev_name);// 注销字符设备}module_init(char_dev_init);// 模块入口module_exit(char_dev_exit);// 模块出口MODULE_LICENSE(GPL);// 模块许可重新编译后在开发板上进行测试使用 VS Code 进行开发驱动开发和内核中很多源码有交互使用 VS Code 等工具进行开发可以方便编写代码方便查阅源码一定程度上可以提升开发效率。Ubuntu中可以使用下面命令来安装VS Codesudosnapinstallcode--classic通常VS Code中安装了C/C支持后配置下内核源码位置即可.vscode下c_cpp_properties.json文件中内容如下其中重要的就是内核源码路径{configurations:[{name:Linux,includePath:[../../NUC980-linux-4.4.y/**],intelliSenseMode:linux-gcc-arm,compilerPath:/home/nx/nuc980-sdk/arm_linux_4.8/bin/arm-linux-gcc,cStandard:gnu89,cppStandard:gnu98}],version:4}总结本文内容只是嵌入式Linux中最基础的驱动开发过程实际去参考别的驱动的时候可能比上面要复杂些一方面因为实际功能的实现另一方面考虑兼容性和可移植性等从而对驱动和设备分层解耦等。虽然本文内容比较基础但是如果只是自己简单用用的话也是可以的毕竟简单。
分享:

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

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