驱动程序开发(字符设备驱动框架实验)

注册字符设备需要首先向 Linux 内核申请设备号,接着使用 cdev_init()函数和 cdev_add()函数初始化并注册字符设备。字符设备注册成功之后,可以使用 class_create()函数device_create()函数实现自动创建设备节点。在此基础上,本文章继续对字符设备框架进行完善,主要内容为完善文件操作集中的成员。

一、文件操作集简介

struct file_operations 结构体又叫文件操作集,struct file_operations 结构体中的每一个成员都对应着一个操作函数,file_operations 结构体定义在内核源码/include/linux/fs.h 文件中,关键成员代码如下所示。

cpp 复制代码
struct file_operations {
    ... 
    //read 函数指针用来从设备读取数据,读取成功返回读取的字节数。
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    struct module *owner;
    //write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    //unlocked_ioctl 函数指针主要提供对于设备的控制功能。也可以实现少量的数据传输
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    //open 函数指针用于打开设备。
    int (*open) (struct inode *, struct file *);
    //执行与 open 函数相反的操作
    int (*release) (struct inode *, struct file *);
    ... 
}

下面对关键成员进行介绍。
1.struct module *owner
owner 是一个指向拥有该结构的模块的指针,避免正在操作时被卸载,一般为初始化为 THIS_MODULES (在 <linux/module.h> 中定义的宏)
2.ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
read 函数指针用来从设备中同步读取数据,读取成功返回读取的字节数。当应用程序中使用read()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 read()函数。
3.ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。当应用程序中使用write()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 write()函数。
4.long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
unlocked_ioctl 函数指针提供对于设备的控制功能,当应用程序中使用 ioctl()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 ioctl()函数。
5.int (*open) (struct inode *, struct file *)
open 函数指针用于打开设备,与应用程序中的 open 函数对应。当应用程序中使用 open()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 open()函数。
6.int (*release) (struct inode *, struct file *)
release 函数指针在 file 结构体释放时被调用。当应用程序中使用 close()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 release()函数。
下面代码填充了 file_operations 结构体中的常用成员:

cpp 复制代码
static struct file_operations cdev_fops_test = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = chrdev_open,//将 open 字段指向 chrdev_open(...)函数
    .read = chrdev_read,//将 open 字段指向 chrdev_read(...)函数
    .write = chrdev_write,//将 open 字段指向 chrdev_write(...)函数
    .release = chrdev_release,//将 open 字段指向 chrdev_release(...)函数
};//定义 file_operations 结构体类型的变量 cdev_test_ops

接下来在上一章节实验的基础上加入 file_operations 结构体,并通过应用程序对字符设备
驱动进行文件操作实验。

二、实验程序的编写

1、驱动程序编写

采用自动申请设备号的方式进行设备号的申请,然后对获取的主设备号与次设备号进行打印,之后对字符设备进行注册,并填充 file_openration 结构体中的成员,最后自动创建设备节点。实验代码 chrdev_fops.c 代码如下所示:

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

static int chrdev_open(struct inode *inode, struct file *file)
{
    printk("This is chrdev_open \n");
    return 0;
}

static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{
    printk("This is chrdev_read \n");
    return 0;
}

static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{
    printk("This is chrdev_write \n");
    return 0;
}

static int chrdev_release(struct inode *inode, struct file *file)
{
    return 0;
}

static dev_t dev_num;//定义 dev_t 类型变量 dev_num 来表示设备号
static struct cdev cdev_test;//定义 struct cdev 类型结构体变量 cdev_test,表示要注册的字符设备
static struct file_operations cdev_fops_test = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = chrdev_open,//将 open 字段指向 chrdev_open(...)函数
    .read = chrdev_read,//将 open 字段指向 chrdev_read(...)函数
    .write = chrdev_write,//将 open 字段指向 chrdev_write(...)函数
    .release = chrdev_release,//将 open 字段指向 chrdev_release(...)函数
};//定义 file_operations 结构体类型的变量 cdev_test_ops

static struct class *class_test;//定于 struct class *类型结构体变量 class_test,表示要创建的类

static int __init chrdev_fops_init(void)//驱动入口函数
{
    int ret;//定义 int 类型的变量 ret,用来对函数返回值进行判断
    int major,minor;//定义 int 类型的主设备号 major 和次设备号 minor
    ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名 chrdev_name
    if (ret < 0){
        printk("alloc_chrdev_region is error \n");
    }
    printk("alloc_chrdev_region is ok \n");
    major = MAJOR(dev_num);//使用 MAJOR()函数获取主设备号
    minor = MINOR(dev_num);//使用 MINOR()函数获取次设备号
    printk("major is %d\n minor is %d \n",major,minor);
    cdev_init(&cdev_test,&cdev_fops_test);//使用 cdev_init()函数初始化 cdev_test 结构体,并链接到cdev_test_ops 结构体
    cdev_test.owner = THIS_MODULE;//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    ret = cdev_add(&cdev_test,dev_num,1); //使用 cdev_add()函数进行字符设备的添加
    if (ret < 0){
        printk("cdev_add is error \n");
    }
    printk("cdev_add is ok \n");
    class_test = class_create(THIS_MODULE,"class_test");//使用 class_create 进行类的创建,类名称为class_test
    device_create(class_test,NULL,dev_num,NULL,"device_test");//使用 device_create 进行设备的创建,设备名称为 device_test
    return 0;
}

static void __exit chrdev_fops_exit(void)//驱动出口函数
{
    device_destroy(class_test,dev_num);//删除创建的设备
    class_destroy(class_test);//删除创建的类
    cdev_del(&cdev_test);//删除添加的字符设备 cdev_test
    unregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号
    printk("module exit \n");
}

module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意 GPL 开源协议
MODULE_AUTHOR("moss");//作者信息

2、编写测试 APP

测试应用程序 app.c 内容代码如下所示:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc,char *argv[])
{
    int fd;//定义 int 类型的文件描述符
    char buf[32];//定义读取缓冲区 buf
    fd=open(argv[1],O_RDWR,0666);//调用 open 函数,打开输入的第一个参数文件,权限为可读可写
    if(fd<0){
        printf("open is error\n");
        return -1;
    }
    printf("open is ok\n");
    /*如果第二个参数为 read,条件成立,调用 read 函数,对文件进行读取*/
    if(!strcmp(argv[2], "read")){
        read(fd,buf,32);
    }
    /*如果第二个参数为 write,条件成立,调用 write 函数,对文件进行写入*/
    else if(!strcmp(argv[2], "write")){
        write(fd,"hello\n",6);
    }
    close(fd);//调用 close 函数,对取消文件描述符到文件的映射
    return 0;
}

代码中第一个参数为要进行读写操作的设备节点,第二个参数为 read 时,对设备节点进行读操作,第二个参数为 write 时,对设备节点进行写操作。

三、运行测试

1、编译驱动程序

在 hrdev_fops.c 代码同一目录下创建 Makefile 文件,Makefile 文件如下:

cpp 复制代码
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += chrdev_fops.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules #make 操作
clean:
    make -C $(KDIR) M=$(PWD) clean #make clean 操作

回到到存放 chrdev_fops.c 和 Makefile 文件目录下,使用命令 make 编译驱动程序,编译完生成 chrdev_fops.ko 目标文件。

bash 复制代码
make

2、编译应用程序

使用 aarch64-linux-gnu-gcc -o app app.c -static 命令交叉编译应用程序

bash 复制代码
aarch64-linux-gnu-gcc -o app app.c -static

3、运行测试

开发板启动之后,使用 insmod chrdev_fops.ko 命令加载驱动程序,加载成功如图所示:

使用 ls /dev/device_test 代码检查设备节点 device_test 是否自动创建成功,创建成功如图所示:

使用./app /dev/device_test 命令运行应用程序对设备进行打开测试,如图所示:

在上图中可以看到"This is chrdev_open"和"open is ok"打印信息,说明应用程序运行成功,且调用了驱动程序中的 open()函数,而"Segmentation fault"错误打印是因为没有对第二个参数进传入,这里忽略即可,随后使用./app /dev/device_test read 命令对设备进行读测试,如图所示:

在上图中可以看到"This is chrdev_read"打印信息,证明驱动程序中的 read()函数被调 用了,然后使用./app /dev/device_test write 命令对设备进行写测试,如图所示: 在上图中可以看到"This is chrdev_write"打印信息,证明驱动程序中的 write()函数被调用了。 最后可以使用 rmmod chrdev_fops.ko 命令卸载驱动程序,如图所示: 通过字符设备驱动框架,应用程序通过设备节点可以与字符设备驱动进行交互,从而实现对硬件的操作。

相关推荐
ashcn20012 小时前
linux 制作一个自解压文件
linux·运维·服务器
天码-行空2 小时前
Linux 系统 MySQL 8.0 详细安装教程
linux·运维·mysql
何妨呀~2 小时前
Keepalived+Haproxy高可用集群实验
linux·服务器·网络
悠哉悠哉愿意2 小时前
【嵌入式学习笔记】OLED 显示驱动 (SSD1306)
笔记·单片机·嵌入式硬件·学习
林鸿风采2 小时前
在Alpine Linux上部署docker和Portainer管理工具
linux·运维·docker·portainer
float_六七2 小时前
设备分配核心数据结构全解析
linux·服务器·数据结构
萧技电创EIIA3 小时前
如何使用嘉立创EDA绘制元件
嵌入式硬件·学习·硬件工程·pcb工艺
梁洪飞3 小时前
使用uboot学习I2C
嵌入式硬件·arm
_She0013 小时前
滤波器 变压器 功分器 的笔记
嵌入式硬件