1、字符设备驱动简介
字符设备是 Linux
驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、
IIC
、
SPI
,
LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux 应用程序对驱动程序的调用如图 40.1.1 所示:
Linux 应用程序对驱动程序的调用如图 40.1.1 所示:
2 字符设备驱动开发步骤
2.1 驱动模块的加载和卸载
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
2.2 字符设备注册与注销
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
2.3 实现设备的具体操作函数
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
/* 用户实现具体功能 */
return 0;
}
/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
/* 用户实现具体功能 */
return 0;
}
/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200, "chrtest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
2.4 添加 LICENSE 和作者信息
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
3 Linux 设备号
3.1
设备号的组成
为了方便管理,
Linux
中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux
提供了 一个名为 dev_t
的数据类型表示设备号,
dev_t
定义在文件
include/linux/types.h 里面,dev_t
其实就是
unsigned int
类型,是一个
32
位的数据类型。这
32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。
3.2 设备号的分配
1、静态分配设备号
3.2 设备号的分配
1、静态分配设备号
使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。
2、动态分配设备号
Linux
社区推荐使用动态分配设备号,在注册字 符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。 卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
4 chrdevbase 字符设备驱动开发实验
4.1 实验程序编写
1、创建 VSCode 工程
2、添加头文件路径
因为是编写
Linux
驱动,因此会用到
Linux
源码中的函数。我们需要在
VSCode
中添加
Linux
源码中的头文件路径。打开
VSCode
,按下“
Crtl+Shift+P
”打开
VSCode
的控制台,然后输入
“
C/C++: Edit configurations(JSON)
”,打开
C/C++
编辑配置文件:
打开以后会自动在 .vscode 目录下生成一个名为 c_cpp_properties.json 的文件,需要将 Linux 源码里面的头文件路径添加进来,添加头文件路径以后的 c_cpp_properties.json的文件内容如下所示:
打开以后会自动在 .vscode 目录下生成一个名为 c_cpp_properties.json 的文件,需要将 Linux 源码里面的头文件路径添加进来,添加头文件路径以后的 c_cpp_properties.json的文件内容如下所示:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
"/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
"/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
3、编写实验程序
工程建立好以后就可以开始编写驱动程序了,新建 chrdevbase.c,然后在里面输入如下内容:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /*读缓冲区*/
static char writebuf[100]; /*写缓冲区*/
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
// printk("chrdevbase_open\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
// printk("chrdevbase_release\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
int ret = 0;
// printk("chrdevbase_read\r\n");
memcpy(readbuf, kerneldata, sizeof(kerneldata));
ret = copy_to_user(buf, readbuf, count);
if (ret == 0)
{
}
else
{
}
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret = 0;
// printk("chrdevbase_write\r\n");
ret = copy_from_user(writebuf, buf, count);
if (ret == 0)
{
printk("kernel recevdata:%s\r\n", writebuf);
}
else
{
}
return 0;
}
/*
* 字符设备,操作集合
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int ret = 0;
printk("chrdevbase_init\r\n");
/*注册字符设备*/
ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
&chrdevbase_fops);
if (ret < 0)
{
printk("chrdevbase init failed!\r\n");
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
/*注销字符设备*/
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init); /*入口*/
module_exit(chrdevbase_exit); /*出口*/
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("supersmart");
4.2 编写测试 APP
1 、 C 库文件操作基本函数
①、open 函数
int open(const char *pathname, int flags)
②、 read 函数
ssize_t read(int fd, void *buf, size_t count)
③、 write 函数
ssize t write(int fd, const void *buf, sizet count);
④、 close 函数
int close(int fd);
2 、编写测试 APP 程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
*argc:应用程序参数个数
* argv[]:具体的参数内容,字符串形式
* ./chrdevbaseAPP <filename> <1:2> 1 表示读,2表示写
* ./chrdevbaseAPP /dev/chrdevbase 1 表示从驱动中读数据
* ./chrdevbaseAPP /dev/chrdevbase 2 表示向驱动中写数据
* */
int main(int argc, char *argv[])
{
int ret = 0;
int fd = 0;
char *filename;
char readbuf[100], writebuf[100];
static char usrdata[] = {"usr data"};
if (argc != 3)
{
printf("Error usage!\r\n");
return -1;
}
filename = argv[1];
/*打开*/
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("Can't open file %s \r\n", filename);
return -1;
}
/*读*/
if (atoi(argv[2])== 1)
{
/*read*/
ret = read(fd, readbuf, 50);
if (ret < 0)
{
printf("read file %s failed!\r\n", filename);
}
else
{
printf("APP read data:%s\r\n", readbuf);
}
}
/*写*/
if (atoi(argv[2]) == 2)
{
/*write*/
memcpy(writebuf, usrdata, sizeof(usrdata));
ret = write(fd, writebuf, 50);
if (ret < 0)
{
printf("write file %s failed!\r\n", filename);
}
else
{
}
}
/*close*/
ret = close(fd);
if (ret < 0)
{
printf("close file %s failed!\r\n", filename);
}
else
{
}
return 0;
}
4.3
编译驱动程序和测试 APP
1 、编译驱动程序
首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建
Makefile 文件,然后在其中输入如下内容:
1 、编译驱动程序
首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建
Makefile 文件,然后在其中输入如下内容:
KERNELDIR := /home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PAHT := $(shell pwd)
obj-m := chrdevbase.o
build :kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PAHT) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PAHT) clean
Makefile
编写好以后输入“
make
”命令编译驱动模块,编译过程如图
所示:
编译成功以后就会生成一个叫做
chrdevbaes.ko
的文件,此文件就是
chrdevbase
设备的驱动
模块。至此,
chrdevbase
设备的驱动就编译成功。
2、编译测试 APP
因为测试
APP
是要在
ARM
开发板上运行的,所以需要使用
arm-linux-gnueabihf-gcc 交叉编译器
来编译,
输入如下命令:
arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP
编译完成以后会生成一个叫做
chrdevbaseAPP
的可执行程序
4.4 运行测试
1、加载驱动模块
将 chrdevbase.ko 和 chrdevbaseAPP 复制到 rootfs/lib/modules/4.1.15 目录中,命令如下:
sudo cp chrdevbase.ko chrdevbaseAPP /home/znn/linux/nfs/rootfs/lib/modules/4.1.15/ -f
拷 贝 完 成 以 后 就 会 在 开 发 板 的
/lib/modules/4.1.15
目 录 下 存 在
chrdevbase.ko
和
chrdevbaseAPP
这两个文件。
使用 modprobe 加载 chrdevbase.ko。
查看当前系统中有没有 chrdevbase 这个设备
2、创建设备节点文件
驱动加载成功需要在
/dev
目录下创建一个与之对应的设备节点文件,应用程序就是通过操
作这个设备节点文件来完成对具体设备的操作。
输入如下命令创建
/dev/chrdevbase
这个设备节点文件:
mknod /dev/chrdevbase c 200 0
创建完成以后就会存在 /dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看
如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对 /dev/chrdevbase 进行读写操作即可。
3 、 chrdevbase 设备操作测试
如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对 /dev/chrdevbase 进行读写操作即可。
3 、 chrdevbase 设备操作测试
使用
chrdevbaseApp 对
chrdevbase
这 个设备进行读操作:
chrdevbaseAPP
使用
read
函数从
chrdevbase
设备读取数 据,因此 chrdevbase_read
函数就会执行。
chrdevbase_read
函数向
chrdevbaseAPP
发送“
kernel data!”数据,
chrdevbaseAPP
接收到以后就打印出来,“
read data:kernel data!
”就是
chrdevbaseAPP 打印出来的接收到的数据。
接下来测试对
chrdevbase
设备的写操作:
chrdevbaseAPP
使用
write
函数向
chrdevbase
设备写入数据“
usr data!
”。
chrdevbase_write
函数接 收到以后将其打印出来。
4、卸载驱动模块
使用 rmmod卸载驱动模块
卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在
总结:通过以上虚拟的 chrdevbase 设备为例,学习了字符设备驱动的开发步骤,掌握了字符设备驱动的开发框架以及测试方法,