目录
一、字符设备驱动简介
二、chrdevbase 字符设备驱动开发实验
1.创建驱动程序的目录
2.创建vscode工程
3.编写实验程序
4.编译驱动程序和测试APP代码
(1)加载驱动模块
(2)创建设备节点文件
(3)chrdevbase 设备操作测试
(4)卸载驱动模块
一、字符设备驱动简介
字符设备是Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
二、chrdevbase 字符设备驱动开发实验
本节我们就以chrdevbase 这个虚拟设备为例,完整的编写一个字符设备驱动模块。chrdevbase 不是实际存在的一个设备,是为了方便讲解字符设备的开发而引入的一个虚拟设备。chrdevbase 设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为100 字节。在应用程序中可以向chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。chrdevbase 这个虚拟设备的功能很简单,但是它包含了字符设备的最基本功能。
1.创建驱动程序的目录
2.创建vscode工程
将工作区另存为chrdevbase
使用命令code . 进入vscode
因为是编写Linux 驱动,因此会用到Linux 源码中的函数。我们需要在VSCode 中添加Linux
源码中的头文件路径。打开VSCode,按下“Crtl+Shift+P”打开VSCode 的控制台,然后输入
“C/C++: Edit configurations(UI) ”,打开C/C++编辑配置文件,如下图所示:
打开以后会自动在.vscode 目录下生成一个名为c_cpp_properties.json 的文件,此文件修改为如下所示如下所示:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/ssz/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include",
"/home/ssz/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include",
"/home/ssz/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
上面includePath 表示头文件路径,需要将Linux 源码里面的头文件路径添加进来,也就是我们前面移植的Linux 内核源码中的头文件路径。
3.编写实验程序
创建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 readbuf[100]; //读缓冲区
static char writebuf[100]; //写缓冲区
static char kerneldata[] = {"kernel data!"};
//打开设备
static int chardevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!\r\n");
return 0;
}
//向设备读取数据
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off_t)
{
int retvalue = 0;
//向用户空间发送数据
memcpy(readbuf, kerneldata, sizeof(kerneldata));
//内核到用户
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0)
{
printk("kernel sendata ok!\r\n");
}
else
{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\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(writebuf, buf, cnt);
if(retvalue == 0)
{
printk("kernel recevdata:%s\r\n",writebuf);
}
else
{
printk("kernel recedata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chardevbase_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()\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
//注销字符设备驱动
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit()\r\n");
}
/*
驱动模块入口与出口
*/
module_init(chrdevbase_init);//入口
module_exit(chrdevbase_exit);//出口
//LICENSE和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ssz");
创建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"
static char usrdate[] = {"usr data!"};
int main(int argc, char *argv[])
{
int fd,retvalue;
char *filename;
char readbuf[100],writebuf[100];
if(argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
//打开驱动文件
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("Cant't open file %s\r\n",filename);
return -1;
}
if(atoi(argv[2]) == 1)
{
//从驱动文件读取数据
retvalue = read(fd, readbuf, 50);
if(retvalue < 0)
{
printf("read file %s failed!\r\n",filename);
}
else
{
//读取成功,打印出读取成功的数据
// printf("1111\n");
printf("read data: %s \r\n",readbuf);
}
}
if(atoi(argv[2]) == 2)
{
//向设备驱动写数据
memcpy(writebuf, usrdate, sizeof(usrdate));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0)
{
printf("write file %s failed!\r\n",filename);
}
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("Can't close file %s\r\n",filename);
return -1;
}
return 0;
}
编译驱动程序,也就是chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建
Makefile 文件,然后在其中输入如下内容:
KERNELDIR := /home/ssz/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build : kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
4.编译驱动程序和测试APP代码
编译驱动程序,输入make命令,会生成chrdevbase.ko文件:
编译测试APP:
使用file命令,chrdevbaseApp这个可执行文件是32 位LSB 格式,ARM 版本的,因此chrdevbaseApp只能在ARM 芯片下运行。
运行测试:
(1)加载驱动模块
驱动模块chrdevbase.ko 和测试软件chrdevbaseAPP 都已经准备好了,接下来就是运行测试。为了方便测试,Linux 系统选择通过TFTP 从网络启动,并且使用NFS 挂载网络根文件系统,确保uboot 中bootcmd 和bootargs环境变量的值为(确保电脑与开发板通过网线连接):
setenv bootcmd 'tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.66:/home/ssz/linux/nfs/
rootfs ip=192.168.1.66:192.168.1.55:192.168.1.1:255.255.255.0::eth0:off'
saveenv
设置好以后启动Linux 系统,检查开发板根文件系统中有没有“/lib/modules/4.1.15”这个目录,如果没有的话自行创建。注意,“/lib/modules/4.1.15”这个目录用来存放驱动模块,使用modprobe 命令加载驱动模块的时候,驱动模块要存放在此目录下。“/lib/modules”是通用的,不管你用的什么板子、什么内核,这部分是一样的。不一样的是后面的“4.1.15”,这里要根据你所使用的Linux 内核版本来设置,比如ALPHA 开发板现在用的是4.1.15 版本的Linux 内核,因此就是“/lib/modules/4.1.15”。如果你使用的其他版本内核,比如5.14.31,那么就应该创建“/lib/modules/5.14.31”目录,否则modprobe 命令无法加载驱动模块。因为是通过NFS 将Ubuntu 中的rootfs(第三十八章制作好的根文件系统)目录挂载为根文件系统,所以可以很方便的将chrdevbase.ko 和chrdevbaseAPP 复制到rootfs/lib/modules/4.1.15 目录中,命令如下:
拷贝完成以后就会在开发板的/lib/modules/4.1.15 目录下存在chrdevbase.ko 和chrdevbaseAPP 这两个文件,如图所示:
输入如下命令加载chrdevbase.ko 驱动文件:
insmod chrdevbase.ko
或者
modprobe chrdevbase.ko
从上图 可以看出,modprobe 提示无法打开“modules.dep”这个文件,因此驱动挂载失败了。我们不用手动创建modules.dep 这个文件,直接输入depmod 命令即可自动生成modules.dep,有些根文件系统可能没有depmod 这个命令,如果没有这个命令就只能重新配置busybox,使能此命令,然后重新编译busybox。输入“depmod”命令以后会自动生成modules.alias、modules.symbols 和modules.dep 这三个文件,如下图所示:
驱动加载成功:
输入“lsmod”命令即可查看当前系统中存在的模块,结果如下图所示:
从上图可以看出,当前系统只有“chrdevbase”这一个模块。输入如下命令查看当前系统中有没有chrdevbase 这个设备:
从上图可以看出,当前系统存在chrdevbase 这个设备,主设备号为200,跟我们设置
的主设备号一致。
(2)创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看,结果如上图所示:
如果chrdevbaseAPP 想要读写chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是chrdevbase 设备在用户空间中的实现。前面一直说Linux 下一切皆文件,包括设备也是文件,
(3)chrdevbase 设备操作测试
读写操作测试:
(4)卸载驱动模块
如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉chrdevbase 这个设备:
从上图可以看出,此时系统已经没有任何模块了,chrdevbase 这个模块也不存在了,说明模块卸载成功。至此,chrdevbase 这个设备的整个驱动就验证完成了,驱动工作正常。