字符设备驱动开发
配置开发环境
新建一个VScode文档,先配置开发环境
Ctrl+Shift+P//打开控制台
C/C++:Edit configuration(JSON)
在生成的c_cpp_properties.json中添加Linux开发需要包含的头文件
{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "/home/lux/Linux/linux/linux-lux/include", "/home/lux/Linux/linux/linux-lux/arch/arm/include", "/home/lux/Linux/linux/linux-lux/arch/arm/include/generated" ], "defines": [], "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "clang-x64" } ], "version": 4 }
编写程序
编写驱动程序chrdevbase.c
#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #define CHRDEVBASE_MAJOR 200 //主设备号 #define CHRDEVBASE_NAME "chrdevbase" //设备名 static char readbuff[100];//读缓冲区 static char writebuff[100];//写缓冲区 static char kerneldata[]={"kernel data!"}; static int chrdevbase_open(struct inode *inode,struct file *filp) { return 0; } static ssize_t chrdevbase_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int retvalue = 0; memcpy(readbuff,kerneldata,sizeof(kerneldata)); retvalue = copy_to_user(buf,readbuff,cnt); if (retvalue == 0) { printk("kernel senddata ok!\r\n"); } else { printk("Kernek senddata failed!\r\n"); } return 0; } static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt,loff_t *offt) { int retvalue = 0; retvalue = copy_from_user(writebuff,buf,cnt); if (retvalue == 0) { printk("kernel recevdata:%s\r\n",writebuff); } else { printk("kernel recevdata failed!\r\n"); } return 0; } static int chrdevbase_release(struct inode *inode,struct file *filp) { return 0; } static struct file_operations chrdevbase_fops = { .owner = THIS_MODULE, .open = chrdevbase_open, .read = chrdevbase_read, .write = chrdevbase_write, .release = chrdevbase_release, }; static int __init chrdevbase_init(void) { int retvalue = 0; retvalue = register_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME,&chrdevbase_fops); if (retvalue < 0) { printk("chrdevbase driver register failed\r\n"); } printk("chrdevbase init done!\r\n"); return 0; } static void __exit chrdevbase_exit(void) { unregister_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME); printk("chrdevbase_exit done!\r\n"); } module_init(chrdevbase_init); module_exit(chrdevbase_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Lux");
字符设备注册函数
static int __init chrdevbase_init(void) { } module_init(chrdevbase_init);
字符设备注销函数
static void __exit chrdevbase_exit(void) { } module_exit(chrdevbase_exit);
添加LICENSE和作者信息
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Lux");
动态分配设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) /* 函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数: dev:保存申请到的设备号。 baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。 count: 要申请的设备号数量。 name:设备名字 */
释放设备号
void unregister_chrdev_region(dev_t from, unsigned count) /* 此函数有两个参数: from:要释放的设备号。 count: 表示从 from 开始,要释放的设备号数量。 */
编写应用程序chrdevbaseApp.c
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" //数组 usrdata 是测试 APP 要向 chrdevbase 设备写入的数据 static char usrdata[] = {"user data!"}; int main(int argc, char const *argv[]) { int fd,retvalue; char *filename; char readbuf[100],writebuf[100]; /*判断运行测试APP的时候输入的参数是不是为3个,main函数的argc参数表示参数数量,argv[]保存着具体的参数,如果参数不为 3 个的话就表示测试APP用法错误 第一个参数表示运行chrdevbaseAPP这个软件, 第二个参数表示测试APP要打开/dev/chrdevbase这个设备。 第三个参数就是要执行的操作,1表示从chrdevbase中读取数据,2表示向chrdevbase 写数据。*/ if(argc != 3) { printf("Error Usage!\r\n"); return -1; } //获取要打开的设备文件名字, argv[1]保存着设备名字 filename = argv[1]; //调用 C 库中的 open 函数打开设备文件: /dev/chrdevbase。 fd = open(filename,O_RDWR); if (fd < 0) { printf("Can not open file %s\r\n",filename); return -1; } /*判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字 当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印 出读取到的数据。*/ if (atoi(argv[2])==1) { retvalue = read(fd,readbuf,50); if (retvalue < 0) { printf("read file %s failed\r\n",filename); } else { printf("read data %s\r\n",readbuf); } } /*当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据。*/ if (atoi(argv[2])==2) { memcpy(writebuf,usrdata,sizeof(usrdata)); retvalue = write(fd,writebuf,50); if (retvalue < 0) { printf("wtite file %s\r\n",filename); } } //对 chrdevbase 设备操作完成以后就关闭设备。 retvalue = close(fd); if (retvalue < 0) { printf("can not open file %s \r\n",filename); return -1; } return 0; }
编译测试程序
编译驱动程序
将.c文件编译为.ko的模块文件
创建Makefile文件
#KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径, KERNELDIR := /home/lux/Linux/linux/linux-lux #CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取 CURRENT_PATH := $(shell pwd) #obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块 obj-m := chrdevbase.o build: kernel_modules #具体的编译命令,后面的 modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 #M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件 kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译应用程序
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
查看文件类型
file chrdevbaseApp
测试程序
为了方便测试, Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统,确保 uboot 中 bootcmd 环境变量的值为:
tftp 80800000 zImage;tftp 83000000 imx6ull-lux-emmc.dtb;bootz 80800000 - 83000000
bootargs 环境变量的值为:
console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.31.224:/home/lux/Linux/nfs/lux_rootfs ip=192.168.31.55:192.168.31.224:192.168.31.1:255.255.255.0::eth0:off
设置好以后启动 Linux 系统,检查开发板根文件系统中有没有“/lib/modules/4.1.15”这个目录,如果没有的话自行创建。 注意,“/lib/modules/4.1.15”这个目录用来存放驱动模块,使用modprobe 命令加载驱动模块的时候,驱动模块要存放在此目录下。“/lib/modules”是通用的,不管你用的什么板子、什么内核,这部分是一样的。不一样的是后面的“4.1.15”,这里要根据你所使用的 Linux 内核版本来设置否则 modprobe 命令无法加载驱动模块
将 chrdevbase.ko 和 chrdevbaseAPP 复制到 rootfs/lib/modules/4.1.15 目录中,命令如下:
sudo cp chrdevbase.ko chrdevbaseApp /home/lux/Linux/nfs/lux_rootfs/lib/modules/4.1.15/ -f
在开发板终端安装模块
使用insmod安装
使用rmmod卸载
使用modprobe安装,如果报错"can`t open ‘modules.dep’:no such file or directory",执行
depmod//即可
安装完成
查看当前系统安装的模块
lsmod
查看当前系统中有没有chrdevbase这个设备
cat /proc/devices
创建设备结点文件(应用程序就是通过操作这个设备节点文件来完成对具体设备的操作 )
mknod /dev/chrdevbase c 200 0
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看
应用程序测试
读操作
./chrdevbaseApp /dev/chrdevbase 1
写操作
./chrdevbaseApp /dev/chrdevbase 2
卸载模块
rmmod chrdevbase.ko