文章目录
- 目的
- 基础说明
- 驱动测试应用程序
- 基础开发与使用
- 驱动模块入口与出口
- 驱动模块安装与卸载
- 字符设备注册与注销
- 设备开关与读写
- 自动创建与销毁设备节点
- 使用 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在文件中写入下面代码:
#include<string.h>#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>/* 该程序用于读写测试本文要编写的驱动程序对应生成的设备 */intmain(intargc,char**argv){intfd;charbuf[4096];intlen;/* 判断参数 */if((argc<3)||(argc>4)){printf("Usage: char_dev_test <devpath> -w <string>\n");// 写数据printf(" char_dev_test <devpath> -r\n");// 读数据return-1;}/* 打开文件 */fd=open(argv[1],O_RDWR);if(fd<0){printf("applog: can not open file %s\n",argv[1]);return-1;}/* 写数据或读数据 */if((0==strcmp(argv[2],"-w"))&&(argc==4)){len=strlen(argv[3])+1;write(fd,argv[3],len);}elseif((0==strcmp(argv[2],"-r"))&&(argc==3)){len=read(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 root@192.168.31.142:/root/基础开发与使用
驱动模块入口与出口
# cd ~/nuc980-sdk/drivers/char_dev/gedit char_dev.c在文件中写入下面代码:
#include<linux/module.h>staticint__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-m+=char_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 root@192.168.31.142:/root/驱动模块安装与卸载
在开发板上使用insmod来安装驱动模块,使用rmmod卸载驱动模块。使用lsmod查看已安装的驱动模块:
需要注意的是上面模块安装和卸载时出现的内核日志只有在串口终端中才会出现,如果时通过SSH的终端,那么需要使用dmesg命令来查看内核日志:
上面出现了多条日志信息,前面一些是之前测试时产生的。
字符设备注册与注销
下面的字符设备驱动的编写方式是Linux 2.6 内核之前的旧方式,现在字符设备主要使用cdrv方式。
完成了最基本的模块和使用,接下来开始准备字符设备驱动,将驱动代码改成如下:
#include<linux/module.h>#include<linux/fs.h>staticintmajor=0;constchar*dev_name="char_dev";staticconststructfile_operationschar_dev_fops={.owner=THIS_MODULE,};staticint__initchar_dev_init(void){printk("modlog: func %s, line %d.\n",__FUNCTION__,__LINE__);major=register_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中看到字符类型设备驱动了:
设备开关与读写
接下来继续完善驱动代码,添加相应操作功能:
#include<linux/module.h>#include<linux/fs.h>#include<linux/uaccess.h>staticintmajor=0;staticconstchar*dev_name="char_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;ret=copy_to_user(buf,dev_buf,MIN(size,4096));// 从内核空间拷贝数据到用户空间returnret;}staticssize_tchar_dev_write(structfile*file,constchar__user*buf,size_tsize,loff_t*offset){intret;ret=copy_from_user(dev_buf,buf,MIN(size,4096));// 从用户空间拷贝数据到内核空间returnret;}staticconststructfile_operationschar_dev_fops={.owner=THIS_MODULE,.open=char_dev_open,.release=char_dev_close,.read=char_dev_read,.write=char_dev_write,};staticint__initchar_dev_init(void){printk("modlog: func %s, line %d.\n",__FUNCTION__,__LINE__);major=register_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进行删除。
自动创建与销毁设备节点
上面测试中安装模块后还需要手动创建设备节点,其实也可以在安装时自动生成设备节点,在卸载时自动删除设备节点:
#include<linux/module.h>#include<linux/fs.h>#include<linux/uaccess.h>#include<linux/device.h>staticintmajor=0;staticconstchar*char_dev_name="char_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;ret=copy_to_user(buf,dev_buf,MIN(size,4096));// 从内核空间拷贝数据到用户空间returnret;}staticssize_tchar_dev_write(structfile*file,constchar__user*buf,size_tsize,loff_t*offset){intret;ret=copy_from_user(dev_buf,buf,MIN(size,4096));// 从用户空间拷贝数据到内核空间returnret;}staticconststructfile_operationschar_dev_fops={.owner=THIS_MODULE,.open=char_dev_open,.release=char_dev_close,.read=char_dev_read,.write=char_dev_write,};staticint__initchar_dev_init(void){printk("modlog: func %s, line %d.\n",__FUNCTION__,__LINE__);major=register_chrdev(0,char_dev_name,&char_dev_fops);// 注册字符设备,第一个参数0表示让内核自动分配主设备号char_dev_class=class_create(THIS_MODULE,"char_dev_class");//if(IS_ERR(char_dev_class)){unregister_chrdev(major,char_dev_name);return-1;}char_dev_device=device_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 Code:
sudosnapinstallcode--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":"gnu++98"}],"version":4}总结
本文内容只是嵌入式Linux中最基础的驱动开发过程,实际去参考别的驱动的时候可能比上面要复杂些,一方面因为实际功能的实现,另一方面考虑兼容性和可移植性等从而对驱动和设备分层解耦等。
虽然本文内容比较基础,但是如果只是自己简单用用的话也是可以的,毕竟简单。