最基础的字符设备驱动开始,重点学习 Linux 下字符设备驱动开发框架。
驱动框架
Linux 应用程序对驱动程序的调用:
在 Linux 中一切皆为文件,驱动加载成功以后会在"/dev"目录下生成一个相应的文件,应用程序通过对这个名为"/dev/xxx"(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
- open和 close 就是打开和关闭 led 驱动的函数
- write 函数来操作,向驱动写入数据,read 函数从驱动中读取相应的状态
因为驱动是运行在内核空间中,用户空间想要对驱动进行操作,必须要通过系统调用进行。
应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。
字符驱动开发步骤
学 Linux 驱动开发重点是学习其驱动框架。
驱动加载和卸载
Linux驱动有两种运行方式:
- 将驱动编译到内核中,当Linux内核启动自动运行驱动程序
- 将驱动编译成模块(Linux下模块扩展名为.ko),在内核启动之后通过
**modprobe**
或**insmod**
命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,将驱动编译为模块最大的好处就是方便开发,这里我们统一用**<font style="color:#DF2A3F;">modprobe</font>**
进行模块加载。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init);
module_exit(xxx_exit);
c
/***************************************************************
* chrdevbase_init - 函数名
* description : 驱动入口函数
* @param : 无
* 返回值 : 0 成功
***************************************************************/
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;
}
/***************************************************************
* chrdevbase_exit - 函数名
* description : 驱动出口函数
* @param : 无
* 返回值 : 0 成功
***************************************************************/
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit() \r\n");
}
- 当使用"modprobe"命令加载驱动的时候,xxx_init 这个函数就会被调用
- 当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用
注:加载模块命令insmod 和modprobe的区别:
- insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块,insmod 命令不能解决模块的依赖关系。
- modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动
字符设备注册和注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备
c
/* 注册字符设备 */
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
/* 注销字符设备 */
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。****如上节所示
- test_fops 就是设备的操作函数集合
- 要注意的一点就是,选择没有被使用的主设备号,输入命令"cat /proc/devices"可以查看当前已经被使用掉的设备号
- 查看已使用设备号
cat /proc/devices
实现设备具体操作
file_operations 结构体就是设备的具体操作函数。完成变量 test_fops 的初始化,设置好针对 chrtest 设备的操作函数。
c
/*
* 设备操作函数结构体
* 将驱动函数映射为系统调用
*/
static struct file_operations chrdevbase_fops =
{
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
c
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/***************************************************************
* chrdevbase_open - 函数名
* description : 打开设备
* @param-inode : 传递给驱动的 inode
* @param-filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
*
* 返回值 : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open! \r\n");
return 0;
}
/***************************************************************
* chrdevbase_read - 函数名
* @description : 从设备读取数据
* @param-filp : 要打开的文件设备
* @param-buf : 返回给用户空间的数据缓冲区
* @param-cnt : 读取的数据长度
* @param-off_t : 相对于文件首地址的偏移
* @return :读取的字节数
***************************************************************/
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 senddata success! \r\n");
}
else
{
printk("kernel senddata failed! \r\n ");
}
return 0;
}
/***************************************************************
* chrdevbase_write - 函数名
* @description : 向设备写入数据
* @param-filp : 设备文件,要打开的文件设备
* @param-buf : 要写入设备的数据
* @param-cnt : 写入的数据长度
* @param-off_t : 相对于文件首地址的偏移
* @return :写入的文件数
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{
int retvalue = 0;
/* 接收用户空间的数据并打印 */
retvalue = copy_from_user(writebuf, buf, cnt);
if (retvalue==0)
{
printk("kernel recevdata:%s \r\n", writebuf);
}
else
{
printk("kernel recevdata failed! \r\n ");
}
return 0;
}
/***************************************************************
* chrdevbase_release - 函数名
* description : 关闭释放设备
* @param-inode : 传递给驱动的 inode
* @param-filp : 设备文件,要关闭的文件设备描述符
*
* 返回值 : 0 成功
***************************************************************/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
printk("chrdevbase release! \r\n");
return 0;
}
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>
/***************************************************************
* 文件名 : chrdevbase.c
* 功能 : 这是一个简单的虚拟设备驱动程序,用于演示内核模块的初始化和清理
* 作者 : zxk
* 创建日期: 2024年11月18日
***************************************************************/
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/***************************************************************
* chrdevbase_open - 函数名
* description : 打开设备
* @param-inode : 传递给驱动的 inode
* @param-filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
*
* 返回值 : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open! \r\n");
return 0;
}
/***************************************************************
* chrdevbase_read - 函数名
* @description : 从设备读取数据
* @param-filp : 要打开的文件设备
* @param-buf : 返回给用户空间的数据缓冲区
* @param-cnt : 读取的数据长度
* @param-off_t : 相对于文件首地址的偏移
* @return :读取的字节数
***************************************************************/
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 senddata success! \r\n");
}
else
{
printk("kernel senddata failed! \r\n ");
}
return 0;
}
/***************************************************************
* chrdevbase_write - 函数名
* @description : 向设备写入数据
* @param-filp : 设备文件,要打开的文件设备
* @param-buf : 要写入设备的数据
* @param-cnt : 写入的数据长度
* @param-off_t : 相对于文件首地址的偏移
* @return :写入的文件数
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{
int retvalue = 0;
/* 接收用户空间的数据并打印 */
retvalue = copy_from_user(writebuf, buf, cnt);
if (retvalue==0)
{
printk("kernel recevdata:%s \r\n", writebuf);
}
else
{
printk("kernel recevdata failed! \r\n ");
}
return 0;
}
/***************************************************************
* chrdevbase_release - 函数名
* description : 关闭释放设备
* @param-inode : 传递给驱动的 inode
* @param-filp : 设备文件,要关闭的文件设备描述符
*
* 返回值 : 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 = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/***************************************************************
* chrdevbase_init - 函数名
* description : 驱动入口函数
* @param : 无
* 返回值 : 0 成功
***************************************************************/
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;
}
/***************************************************************
* chrdevbase_exit - 函数名
* description : 驱动出口函数
* @param : 无
* 返回值 : 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);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
Linux设备号机制
设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备
设备号组成:
- 主设备号表示某一个具体的驱动
- 次设备号表示使用这个驱动的各个设备
设备号分配
- 静态分配:使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号,分配未使用的。
- 动态分配
Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可
设备号的申请函数:
c
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)
驱动实验流程
应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。
添加头文件路径
c
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include",
"/home/zxk/work/rk3568_linux_sdk/kernel/include",
"/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include/generated"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c17",
"cppStandard": "gnu++14",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
实现过程
- chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用。
- chrdevbase_read 函数,应用程序调用 read 函数从设备中读取数据的时候此函数会执行,因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
- chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数就会执行。
- chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会执行
- module_init 和 module_exit 这两个函数来指定驱动的入口和出口函数。
- 为了欺骗内核,给本驱动添加 intree 标记,如果不加就会有"loading out-of-treemodule taints kernel."这个警告
驱动测试APP-应用层
c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
* 文件名 : chrdevbaseAPP.c
* 功能 : 这是一个chrdevbase 驱测试 APP
* 作者 : zxk
* 创建日期: 2024年11月18日
* 其他 :使用方法 ./chrdevbaseAPP /dev/chrdevbase <1>|<2>
* argv[2] 1:读文件
* argv[2] 2:写文件
***************************************************************/
static char usrdata[] = {"usr data!"};
/***************************************************************
* description : main主函数
* @param-argc : argv 数组元素个数
* @param-argv : 具体参数
*
* @return : 0 成功
***************************************************************/
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(" Can't open the file %s \r\n", argv[1]);
return -1;
}
if (atoi(argv[2]) == 1) /* 从驱动读取数据 */
{
retvalue = read(fd, readbuf, 50);
if (retvalue < 0)
{
printf(" read the file %s failed!\r\n", filename);
}
else
{
printf("read data is %s \r\n", readbuf);
}
}
if (atoi(argv[2]) == 2) /* 写数据到驱动文件 */
{
memcpy(usrdata, writebuf, sizeof(usrdata));
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 the file %s!\r\n", filename);
return -1;
}
return 0;
}
驱动和测试函数编译
驱动编译成模块
编写Makefile文件
makefile
# ARCH := arm64
KERNELDIR := /home/zxk/work/rk3568_linux_sdk/kernel
CURRENT_PATH := /home/zxk/linux_driver/01_chrdevbase
obj-m := chrdevbase.o
bulid: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译模块
make ARCH=arm64 //<font style="color:#DF2A3F;">ARCH=arm64 必须指定,否则编译会失败</font>
编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。
编译APP测试文件-交叉编译
bash
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseApp
开发板测试驱动
上传编译后的驱动和测试文件
bash
scp ./01_chrdevbase/chrdevbase.ko root@192.168.137.65:/lib/modules/4.19.232/
scp ./01_chrdevbase/chrdevbaseAPP root@192.168.137.65:/lib/modules/4.19.232/
加载驱动模块
- 输入"depmod"命令以后会自动生成 modules.alias、modules.symbols 和 modules.dep 等等一些 modprobe 所需的文件
bash
depmod
- modprobe命令加载 chrdevbase.ko 驱动文件
bash
modprobe chrdevbase.ko
- 查看是否加载成功
bash
1.看到"chrdevbase init!"这一行
2.cat /proc/devices # 查看当前系统中有没有 chrdevbase 这个设备
- 创建设备节点
bash
mknod /dev/chrdevbase c 200 0
# 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用"ls /dev/chrdevbase -l"命令查看
- chrdevbase 设备操作测试
bash
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
- 卸载驱动模块
bash
rmmod chrdevbase