1. 字符设备驱动简介
字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI、LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux应用程序对驱动程序的调用架构如下图:
在 Linux中一切皆为文件,驱动加载成功以后会在“ “/dev”目录下生成一个相应的文件,应用程序通过对这个名为“ “/dev/xxx””(xxx是具体的驱动文件名字 )的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做 /dev/led的驱动文件,此文件是 led灯的驱动文件。应用程序使用 open函数来打开文件 /dev/led,使用完成以后使用 close函数关闭 /dev/led这个文件。 open和 close就是打开和关闭 led驱动的函数,如果要点亮或关闭 led,那么就使用 write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led的控制参数。如果要获取led灯的状态,就用 read函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open函数打开 /dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷 入”到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write和 read等这些函数是由 C库提供的,在 Linux系统中,系统调用作为 C库的一部分。
当我们调用 open函数的时候流程如图 5.1.2所示:
每一个 系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux内核文件include/linux/fs.h中有个叫做 file_operations的结构体,此结构体就是 Linux内核驱动操作函数集合。 结构体内容如下
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_KABI_RESERVE(3);
ANDROID_KABI_RESERVE(4);
} __randomize_layout;
指针和方法解析:
指针或方法 | 解析 |
---|---|
owner | 拥有该模块的指针,一半设置为THIS_MODULE |
llsek | 修改文件当前的读写位置 |
read | 读取设备文件 |
write | 向设备文件写入数据 |
poll | 轮询,用于查询设备是否可以进行非阻塞读写 |
unlocked_ioctl | 提供对设备的控制功能,与应用程序的ioctl函数对应 |
compat_ioctl | 与unlocked_ioctl功能一样,在64位系统上的32位程序调用此函数,32位系统上运行32位应用程序调用unlocked_ioctl |
mmap | 将设备的内存映射到用户空间,帧缓冲设备会使用此函数,缓冲设备会使用此函数,比如 LCD驱动的显存,将帧缓冲 (LCD显存 )映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。 |
open | 打开设备文件 |
release | 释放设备,与应用程序的close函数对应 |
fasync | 刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中 |
字符设备驱动开发过程就是实现以上的函数
2. 字符设备驱动开发步骤
2.1 linux驱动模块的加载和卸载
Linux驱动有两种运行方式:
- 将驱动编译到linux内核中,当linux内核启动的会自动运行驱动程序。【一般用在调试通过后】
- 将驱动编译成模块,【.ko】文件。linux内核启动后,使用
modprobe
或者insmod
命令加载驱动模块,使用rmmod
卸载模块。【一般用在调试过程中】
insmod不能解决模块之间的依赖关系
modprobe能分析模块间的以来关系 。modprobe命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe命令来加载驱动。 modprobe命令默认会去/lib/modules/目录中查找模块,比如本书使用的
模块有加载和卸载两种操作,在开发驱动时要注册这两种操作函数,模块的加载和卸载注册函数如下:
/**
* module_init函数用来向 Linux内核注册一个模块加载函数,参数 xxx_init就是需要注册的具体函数,当使用"modprobe"命令加载驱动的时候 xxx_init这个函数就会被调用。
*/
module_init(xxx_init); //注册模块加载函数
/**
* 这个函数就会被调用。 module_exit函数用来向 Linux内核注册一个模块卸载函数,参数 xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit函数就会被调用。字符设备驱动模块加载和卸
*/
module_exit(xxx_exit); //注册模块卸载函数
字符设备驱动模块加载和卸载模板如下:
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */ 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)
register_chrdev
函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major
主设备号, Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部;
name
:设备名字,指向一串字符串;
fops
结构体 file_operations类型指针,指向设备的操作函数集合变量。
unregister_chrdev
函数用户注销字符设备,此函数有两个参数,这两个 参数含义如下:
major
要注销的设备对应的主设备号。
name
要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init中进行,字符设备的注销在驱动模块的出口函数 xxx_exit中进行。
示例
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "test_fops", &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200, "test_fops");
printk("chrdevbase exit!\r\n");
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
代码分块解析:
- 行,定义了一个 file_operations结构体变量 test_fops test_fops就是设备的操作函数集合,只是此时还没有初始化 test_fops中的 open、 release等这些成员变量,所以这个操作函数集合还是空的。
- 调用函数
register_chrdev
注册字符设备,主设备号为200
,设备名字为test_fops
设备操作函数集合就是第 1行定义的 test_fops。要注意的一点就是,选择没有被使用的主设备
注意:一定要选择没有被使用的设备号,可以使用
cat /proc/devicesc
查看已经被使用的设备号。
示例:
guo@DrGuo-Yoga:~$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
21 sg
108 ppp
128 ptm
136 pts
166 ttyACM
180 usb
188 ttyUSB
189 usb_device
226 drm
229 hvc
242 vhost-vdpa
243 vfio
244 uio
245 macvtap
246 ipvtap
247 virtio-portsdev
248 bsg
249 ptp
250 pps
251 rtc
252 dax
253 dimmctl
254 ndctl
Block devices:
1 ramdisk
7 loop
8 sd
9 md
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 virtblk
254 mdp
259 blkext
如上所示,linux设备分为字符设备和块设备。
2.3 实现字符设备的具体操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。
对chrtest进行读写操作:
假设 chrtest这个设备控制着一段缓冲区 (内存 ),应用程序需要通过 read和 write这两个函数对 chrtest的缓冲区进行读写操作。所以需要实现 file_operations中的 read和 write这两个函数。
具体如下:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : 正点原子
版本 : V1.0
描述 : chrdevbase驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/01 正点原子创建
***************************************************************/
#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 : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
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 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 recevdata failed!\r\n");
}
//printk("chrdevbase write!\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;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 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;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
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和作者信息
* LICENSE是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
3. Linux设备号
3.1 Linux设备号的组成
为了方便管理, Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux提供了一个名为 dev_t的数据类型表示设备号, dev_t定义在文件 include/linux/types.h里面,定义如下:
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
dev_t是 u32类型的 ,也就是 unsigned int 。所以dev_t其实就是 unsigned int类型,是一个 32位的数据类型。
这 32位的数据构成了主设备号和次设备号两部分,其中高 12位为主设备号,低 20位为次设备号。。因此 Linux系统中主设备号范围为 0~4095。
设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h中提供了几个关于设备号的操作函数 (本质是宏 ),如下所示
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
解析如下:
- 宏 MINORBITS表示次设备号位数,一共是 20位。
- 宏 MINORMASK表示次设备号掩码。
- 宏 MAJOR用于从 dev_t中获取主设备号,将 dev_t右移 20位即可。
- 宏 MINOR用于从 dev_t中获取次设备号,取 dev_t的低 20位的值即可。
- 宏 MKDEV用于将给定的主设备号和次设备号的值组合成 dev_t类型的设备号。
3.2 设备号的分配
1. 静态分匹配的设备号
注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的一个设备号,比如 200这个主设备号。
有一些常用的设备号已经被 Linux内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。
并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“ cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。
2. 动态分配设备号
Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev
:保存申请到的设备号。
baseminor
次设备号起始地址, alloc_chrdev_region可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor为起始地址地址开始递增。一般 baseminor为 0,也就是说次设备号从 0开始。
count
要申请的设备号数量。
name
:设备名字。
同样,注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from
:要释放的设备号。
count
表示从 from
开始,要释放的设备号数量。
4. 字符设备驱动开发实战
1. 创建VS Code工程
创建文件夹
mkdir ~/driver_code
cd ~/driver_code
mkdir chrdev_test
添加chrdev_test.c文件
vi chrdev_test.c
2. 添加头文件路径
通过win11下的Vs Code 打开WSL中的chrdev_test文件夹。
编写 Linux驱动,因此会用到 Linux源码中的函数。需要在 VSCode中添加 Linux源码中的头文件路径。打开 VSCode,按下 Crtl+Shift+P”打开 VSCode的控制台,然后输入C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,如图
打开VS CODE的命令面板,选择C++ 编辑配置文件
,如图所示
可以生成如下内容:
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.22621.0",
"compilerPath": "cl.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x64"
}
],
"version": 4
}
修改为
{
"configurations": [
{
"name": "WSL",
"includePath": [
"${workspaceFolder}/**",
"~/rk3568_linux_sdk/kernel/arch/arm64/include",
"~/rk3568_linux_sdk/kernel/include",
"~/rk3568_linux_sdk/kernel/arch/arm64/include/generated"
],
"defines": [
],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "gnu++14",
"intelliSenseMode": "wsl gcc"
}
],
"version": 4
}
}
generated文件夹必须先编译内核成功才会生成,并且需要确认自己的SDK路径
将chrdev_test.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>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : 正点原子
版本 : V1.0
描述 : chrdevbase驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/01 正点原子创建
***************************************************************/
#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 : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
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 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 recevdata failed!\r\n");
}
//printk("chrdevbase write!\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;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 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;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
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("ALIENTEK");
MODULE_INFO(intree, "Y");
chrdevbase_open函数,当应用程序调用 open函数的时候此函数就会调用 。本例程中我们没有做任何工作,只是输出一串字符,用于调试。这里使用了 printk来输出信息。
Linux内核中没有 printf这个函数。 printk相当于 printf的孪生兄妹, printf运行在用户态, printk运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk函数。
printk可以根据日志级别对消息进行分类,一共有 8个消息级别,这 8个消息级别定义在文件 include/linux/kern_levels.h中,如下:
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
如果不显式设置消息级别,则采用MESSAGE_LOGLEVEL_DEFAULT
,此宏在include/linux/printk.h
中定义:
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
MESSAGE_LOGLEVEL_DEFAULT
和 CONFIG_CONSOLE_LOGLEVEL_DEFAULT
是通过内核图形化界面配置的,配置路径如下:
-> Kernel hacking
-> printk and dmesg options
-> (7) Default console loglevel (1-15) //设置默认终端消息级别
-> (4) Default message log level (1-7) //设置默认消 息级别
如图所示:
5. 编写测试APP
5.1 C文件操作基本函数
1. open函数
函数原型;
int open(const char *pathname, int flags)
参数解析:
pathname
::要打开的设备或者文件名。
flags
:文件打开模式,以下三种模式必选其一:
O_RDONLY
只读模式O_WRONLY
只写模式O_RDWR
读写模式
除了上述三种模式以外还有其他的可选模式,通过逻辑或来选择多种模式:
O_APPEND
每次写操作都写入文件的末尾
O_CREAT
如果指定文件不存在,则创建这个文件
O_EXCL
如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC
如果文件存在,并且以只写 /读写方式打开,则清空文件全部内容
O_NOCTTY
如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK
如果路径名指向 FIFO/块文件 /字符文件,则把文件的打开和后继I/O设置为非阻塞
O_DSYNC
等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC
read 等待所有写入同一区域的写操作完成后再进行。
O_SYNC
等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
返回值
:如果文件打开成功的话返回文件的文件描述符。
2. read函数
函数原型:
ssize_t read(int fd, void *buf, size_t count)
参数解析:
fd
:要读取的文件描述符,读取文件之前要先用 open函数打开文件, open函数打开文件成功以后会得到文件描述符。
buf
:数据读取到此 buf中。
count
: 要读取的数据长度,也就是字节数。
返回值
: 读取成功的话返回读取到的字节数;如果返回 0表示读取到了文件末尾;如果返回负值,表示读取失败。在 Ubuntu中输入man 2 read
命令即可查看 read函数的详细内容。
3. write函数
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数解析:
fd
:要进行写操作的文件描述符,写文件之前要先用 open函数打开文件, open函数打开文件成功以后会得到文件描述符。
buf
要写入的数据。
count
要写入的数据长度,也就是字节数。
返回值
: 写入成功的话返回写入的字节数;如果返回 0表示没有写入任何数据;如果返回负值,表示写入失败。在 Ubuntu中输入man 2 write
命令即可查看 write函数的详细内容。
4. close 函数
函数原型:
int close(int fd);
参数解析:
fd
:要关闭的文件描述符。
返回值
: 0表示关闭成功,负值表示关闭失败。在 Ubuntu中输入man 2 close
命令即可查看 close函数的详细内容。
5.2 编写测试APP
驱动编写好以后是需要测试的,一般编写一个简单的测试 APP,测试 APP运行在用户空间。测试 APP很简单通过输入相应的指令来对 chrdevbase设备执行读或者写操作。
在chrdev_test
目录中新建testApp.c
文件,文件内容如下:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbaseApp.c
作者 : 正点原子
版本 : V1.0
描述 : chrdevbase驱测试APP。
其他 : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
argv[2] 1:读文件
argv[2] 2:写文件
论坛 : www.openedv.com
日志 : 初版V1.0 2022/12/01 正点原子团队创建
***************************************************************/
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 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("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, 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 file %s\r\n", filename);
return -1;
}
return 0;
}
5.3 编写Makefile文件
在文件夹下新建Makefile
文件,输入以下内容
KERNELDIR := ~/rk3568_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
obj-m := chrdev_test.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
代码解析:
第 1行, KERNELDIR表示开发板所使用的 Linux内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。
第 2行, CURRENT_PATH表示当前路径,直接通过运行“ pwd”命令来获取当前所处路
径。
第 3行, obj-m表示将 chrdev_test.c这个文件编译为 chrdev_test.ko模块。
第 8行,具体的编译命令,后面的 modules表示编译模块, ,-C表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR目录。 M表示模块源码目录,“ make modules”命令中加入 M=dir以后程序会自动到指定的 dir目录中读取模块的源码并将其编译为 .ko文件。
5.4 编译模块
运行指令
make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译过程如下:
guo@DrGuo-Yoga:~/driver_code/chrdev_test$ make ARCH=arm64
make -C ~/rk3568_linux_sdk/kernel M=/home/guo/driver_code/chrdev_test modules
make[1]: Entering directory '/home/guo/rk3568_linux_sdk/kernel'
CC [M] /home/guo/driver_code/chrdev_test/chrdev_test.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/guo/driver_code/chrdev_test/chrdev_test.mod.o
LD [M] /home/guo/driver_code/chrdev_test/chrdev_test.ko
make[1]: Leaving directory '/home/guo/rk3568_linux_sdk/kernel'
可以看到编译成功,生成了chrdev_test.ko
模块。
5.5 编译测试app
编译指令:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc test_app.c -o test_app
编译成功,可以看到生成了test_app
6. 测试驱动
6.1 部署
使用ADB
工具将chrdev_test.ko
和test_app
文件发送到开发板的/lib/modules/4.19.232
文件夹下。
指令如下:
adb push chrdev_test.ko test_app /lib/modules/4.19.232
6.2 加载模块
运行指令:
modprobe chrdev_test.ko
会遇到如下问题:
modprobe命令会在“ “/lib/modules/4.19.232”目录下解析 modules.dep文件, modules.dep文件里面保存了要加载的 .ko模块,我们不用手动创建modules.dep这个文件,直接输入 depmod命令即可自动生成 modules.dep
有些根文件系统可能没有 depmod这个命令,如果没有这个命令就只能重新配置 busybox,使能此命令,然后重新编译 busybox。
输入“ depmod”命令以后会自动生成 modules.alias、 modules.symbols和 modules.dep等等一些 modprobe所需的文件。
然后,运行指令cat /proc/devices
指令可以看到系统中是否有chrdev_test这个设备。
6.3 创建设备节点文件
驱动加载成功需要在 /dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建 /dev/chrdev_test:
mknod /dev/chrdevbase c 200 0
mknod
是创建节点命令 ,
/dev/chrdevbase
是要创建的节点文件
c
表示这是个字符设备,
200
是设备的主设备号
0
是设备的次设备号。
创建完成以后就会存在/dev/chrdev_test
这个文件,可以使用 ls /dev/chrdev_test -l
命令查看
如果 test_app想要读写 chrdev_test设备,直接对 /dev/chrdev_test进行读写操作即可。相当于 /dev/chrdevbase这个文件是 chrdevbase设备在用户空间中的实现。
6.4 设备操作测试
运行指令
./test_app /dev/chrdev_test 1
./test_app /dev/chrdev_test 2
6.5 卸载模块
运行指令:
rmmod chrdev_test