字符设备驱动框架

参考资料:
 

基本概念:

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如SPI、IIC、LCD等等都是字符驱动。Linux应用和Linux驱动是分层的。Linux应用程序员无法直接操作底层寄存器。应用程序运行在用户空间,Linux驱动属于内核的一部分,驱动程序运行在内核空间。当用户空间想要实现对内核的操作,必须使用系统调用的方法来实现从用户空间陷入到内核空间,这样才能实现对底层的操作。open、close、write、read等这些函数都是C库提供的,在Linux系统中,系统调用作为C库的一部分。下面是Linux应用程序对驱动程序的调用流程:
0
 
Linux内核操作函数结构体struct file_operations:
 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 *);
         unsigned int (*poll) (struct file *, struct poll_table_struct *);                    // 轮询,用于查询设备是否可以进行非阻塞的读写
         long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);                // 跟应用程序的ioctl函数对应,32位
         long (*compat_ioctl) (struct file *, unsigned int, unsigned long);                    // 64位
         int (*mmap) (struct file *, struct vm_area_struct *);                                // share memory,将设备的内存映射到用户空间
         int (*mremap)(struct file *, struct vm_area_struct *);
         int (*open) (struct inode *, struct file *);                                        // 打开
         int (*flush) (struct file *, fl_owner_t id);
         int (*release) (struct inode *, struct file *);                                       // 关闭,与应用程序的close相对应
         int (*fsync) (struct file *, loff_t, loff_t, int datasync);                        // 用于刷新待处理的数据,用于将缓冲区的数据刷到磁盘
         int (*aio_fsync) (struct kiocb *, 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
 };
 

驱动模块的加载与卸载:

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发。
模块的加载和卸载注册函数如下:
module_init(xxx_init);     //注册模块加载函数
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);
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和 modprobe。
insmod drv.ko
rmmod drv.ko

modprobe -r drv.ko
insmode和rmmode的区别在于:insmode不能解决模块的依赖关系,而rmmode提供了模块的依赖性分析、错误检查、错误报告等功能。modprobe 命令默认会去/lib/modules/目录中查找模块
 

字符设备注册与注销:

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:要注销的设备对应的设备名。
字符设备注册与注销示例代码:
static struct file_operations test_fops;

/* 驱动入口函数 */
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);
可以通过cat /proc/devices查看已经使用的设备号
 

实现设备的具体操作函数:

实现chrtest的打开/关闭/读写操作
/* 打开设备 */
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);

 

添加LICENSE和作者信息:

MODULE_LICENSE()     //添加模块 LICENSE 信息
MODULE_AUTHOR()     //添加模块作者信息

 

Linux设备号与动态分配设备号:

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面:
typedef unsigned int __u32;

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
dev_t是一个无符号int型,这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))        // 从dev_t中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))        // 从dev_t中获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))                // 将主设备号和次设备号组合成设备号
动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数和释放函数如下:
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 开始,要释放的设备号数量。
 

Linux内核编译:

 KERNELDIR := /mnt/d/project/imx6ull/atom/atom/linux            // linux内核源码目录

 CURRENT_PATH := $(shell pwd)

 obj-m :=chrdevbase.o

 build: kernel_modules

 kernel_modules:
         $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

 clean:
         $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
如果在ubuntu下尝试,可以使用
sudo apt-get update
sudo apt-get install build-essential linux-headers-$(uname -r)
来安装相应的工具包
其中,build-essential是用于构建C程序的基本工具集合,linux-headers-$(uname -r)包含了当前运行的Linux内核的头文件
 LINUX_KERNEL := $(shell uname -r)
 LINUX_KERNELDIR := /usr/src/linux-headers-$(LINUX_KERNEL)            // linux内核源码目录
 CURRENT_PATH := $(shell pwd)

 obj-m :=chrdevbase.o

 all:
         make -C $(LINUX_KERNELDIR) M=$(CURRENT_PATH) modules

 clean:
         make -C $(LINUX_KERNELDIR) M=$(CURRENT_PATH) clean
加载查看如下,目录没有直接创建,自己创建的根文件系统没有这些:
0
 

chrdevbase 字符设备驱动开发与测试:

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" /* 调试信息 */
驱动代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.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;
 }
 
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;
}
 
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;
 }
 
static int chrdevbase_release(struct inode *inode,struct file *filp)
 {
    return 0;
 }
 
 /*
 * 设备操作函数结构体
 */
 
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .release = chrdevbase_release,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
};
 
 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;
}
 
 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("lethe1203");

 

APP测试代码:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data!"};


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;
}

 

 

创建设备节点:

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
mknod /dev/chrdevbase c 200 0
其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“200”是设备的主设备号,“0”是设备的次设备号。
 

chrdevbase测试:

0
 
posted @ 2024-03-23 16:33  lethe1203  阅读(14)  评论(0编辑  收藏  举报