【Linux】基于驱动框架的程序编写测试
- 字符设备驱动工作原理
- ☆ 驱动程序开发
- 驱动程序开发步骤
- 驱动代码框架
- 驱动框架设计流程
- 编译与测试
- 编译
- 测试
参考博文:
【Linux】基于框架编写驱动代码、驱动代码编译和测试
Linux驱动(驱动程序开发、驱动框架代码编译和测试)
字符设备驱动工作原理
字符设备驱动工作原理在linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?
补充:
- 在Linux文件系统中,每个文件都用一个 struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。
- 在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。
- 在linux操作系统中, 每个驱动程序都有一个设备号。
- 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。
(1)当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
(2) 根据struct inode
结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev
结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。
其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。
字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
-
如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员
dev_t
来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations
来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。 -
在Linux字符设备驱动:
- 模块加载函数通过
register_chrdev_region( )
或alloc_chrdev_region( )
来静态或者动态获取设备号 - 通过
cdev_init( )
建立cdev与file_operations之间的连接 - 通过
cdev_add( )
向系统添加一个cdev以完成注册。 - 模块卸载函数通过
cdev_del( )
来注销cdev - 通过
unregister_chrdev_region( )
来释放设备号。
- 模块加载函数通过
-
用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),用
file_operations
来定义字符设备驱动提供给VFS的接口函数。
☆ 驱动程序开发
驱动程序开发步骤
Linux 内核就是由各种驱动组成的,驱动程序的编写一般都是弄清楚现有驱动程序的框架,并在这个框架中加入硬件
一般来说,编写一个 linux 设备驱动程序的大致流程如下:
- 查看原理图、数据手册,了解设备的操作方法;
- 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
- 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序;
- 设计所要实现的操作,比如 open、close、read、write 等函数;
- 实现中断服务(中断并不是每个设备驱动所必须的);
- 编译该驱动程序到内核中,或者用 insmod 命令加载;
- 测试驱动程序;
下面就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。
驱动代码框架
上层测试代码:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd;
char buf[1] = {'1'};
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open error\n");
perror("reason:");
}
else
printf("open success\n");
write(fd, buf, 1);
}
驱动框架代码
#include <linux/fs.h> // 包含了文件系统相关的数据结构和函数 file_operations声明
#include <linux/init.h> // 包含了模块初始化和清理函数的宏定义 __init __exit 宏定义声明
#include <linux/module.h> // 提供了Linux内核模块的基本函数和宏 module_init module_exit声明
#include <linux/cdev.h> // 定义了字符设备相关的结构和函数 cdev_init 字符设备初始化
#include <linux/device.h> // 包含了设备类和设备的定义 class devise声明
#include <linux/uaccess.h> // 提供了用户空间和内核空间数据传输的函数 copy_from_user 的头文件
#include <linux/types.h> // 提供了用户空间和内核空间数据传输的函数 设备号 dev_t 类型声明
//变量定义
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; // 设备号
static int major = 231; // 主设备号
static int minor = 0; // 次设备号
static char* module_name = "pin4"; // 模块名
// led_open 函数
static int pin4_open(struct inode *inode, struct file *file)
{
printk("pin4 open\n"); //内核的打印函数用 printk
return 0;
}
// led_write 函数
static int pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4 write\n");
return 0;
}
// led_read 函数
static int pin4_read(struct file* file, char __user * buf, size_t count, loff_t *ppos)
{
printk("pin4 read\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read
};
// 真实驱动入口
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major, minor); // 2. 创建设备号
//3. 注册驱动 告诉内核 把这个驱动加入到内核链表中
ret = register_chrdev(major, module_name, &pin4_fops);
// 让代码在dev下自动生成设备
pin4_class = class_create(THIS_MODULE, "myfirstdemo");
// 创建设备文件
pin4_class_dev = device_create(pin4_class, NULL, devno, NULL, module_name);
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口, 内核加载该驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
驱动框架设计流程
- 确定主设备号
- 定义结构体 类型
file_operations
- 实现对应的
drv_open/drv_read/drv_write
等函数,填入file_operations
结构体 - 实现驱动入口:安装驱动程序时,就会去调用这个入口函数,执行工作:
- 把
file_operations
结构体告诉内核:注册驱动程序register_chrdev
. - 创建类
class_create
. - 创建设备
device_create
.
- 把
- 实现出口:卸载驱动程序时,就会去调用这个出口函数,执行工作:
- 把
file_operations
结构体从内核注销:unregister_chrdev
. - 销毁类
class_destroy
. - 销毁设备结点
device_destroy
.
- 把
- 其他完善:GPL协议、入口加载
1. 确定主设备、变量定义
//变量定义
static struct class *pin4_class; // 设备类
static struct device *pin4_class_dev; // 设备文件
static dev_t devno; // 设备号
static int major = 231; // 主设备号
static int minor = 0; // 次设备号
static char* module_name = "pin4"; // 模块名
2. 定义file_operations结构体,加载到内核驱动链表中
这是Linux内核中的file_operations 结构体
根据上层调用函数定义结构体成员:
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read
};
3. 实现结构体成员pin4_read等函数
// led_open 函数
static int pin4_open(struct inode *inode, struct file *file)
{
printk("pin4 open\n"); //内核的打印函数用 printk
return 0;
}
// led_write 函数
static int pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4 write\n");
return 0;
}
// led_read 函数
static int pin4_read(struct file* file, char __user * buf, size_t count, loff_t *ppos)
{
printk("pin4 read\n");
return 0;
}
4. 驱动入口
// 真实驱动入口
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major, minor); // 2. 创建设备号
//3. 注册驱动 告诉内核 把这个驱动加入到内核链表中
ret = register_chrdev(major, module_name, &pin4_fops);
// 让代码在dev下自动生成设备
pin4_class = class_create(THIS_MODULE, "myfirstdemo");
// 创建设备文件
pin4_class_dev = device_create(pin4_class, NULL, devno, NULL, module_name);
return 0;
}
其中
pin4_class=class_create(THIS_MODULE, "myfirstdemo");
由代码在/dev
自动生成设备,除此之外还可以手动生成设备,在dev目录下
sudo mknod + 设备名字 + 设备类型(c表示字符设备驱动) + 主设备号 + 次设备号。
sudo mknod pin0 c 8 1
5. 驱动出口
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
6、GPI协议,入口加载,出口加载
module_init(pin4_drv_init); //入口, 内核加载该驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
编译与测试
模块的编译需要配置Linux平台的交叉编译工具链,配置Linux内核源码,其中关于交叉编译工具链的安装,Linux内核配置及编译请参考博文:
- 1. 交叉编译工具链的安装及带wiringPi库的交叉编译实现
- 2. Linux内核编译并移植至树莓派
注意:这里需要将编写的 pin4driver.c 放到 linux-rpi-4.14.y/drivers/char/ 下才能编译
编译
- 在Makefile中添加编译成模块: 编译、连接后生成的内核模块后缀为.ko
-
回到linux-rpi-4.14.y,编译驱动文件,模块编译指令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
-
编译驱动模块成功会生成以下几个文件:
-
最后将编译生成的驱动模块和编译好的测试文件拷贝至树莓派测试
scp
scp drivers/char/pin4driver.ko pi@192.168.137.64:/home/pi
scp pin4test pi@192.168.137.64:/home/pi
编译过程中,经历了这样的步骤: 先进入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件,而后根据此文件编译出pin4drive.mod.o,之后连接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。
测试
1. 加载内核驱动模块:
sudo insmod pin4driver.ko
加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中) 加载完成后就可以在dev下面看到名字为pin4的设备驱动
2. 查看内核驱动模块: 使用 lsmod
命令查看已加载的内核模块。
3. 添加访问权限:
sudo chmod 666 /dev/pin4
4.执行上层代码
5. 查看内核打印的信息
dmesg |grep pin4