Linux驱动——字符设备(上)

了解字符设备的雏形和一些函数的用法。

一、字符设备

1、字符设备驱动简介

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI、LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的,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 这个驱动,因为用 户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。openclosewrite read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的 时候流程如下图:

        其中关于 C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下图所示:

        在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、release、write、read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。

简单介绍一下 file_operation 结构体中比较重要的、常用的函数:

owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE
llseek 函数用于修改文件当前的读写位置。
read 函数用于读取设备文件。
write 函数用于向设备文件写入 ( 发送 ) 数据。
poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的unlocked_ioctl。
mmap 函数用于将设备的内存映射到进程空间中 ( 也就是用户空间 ) ,一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲 (LCD 显存 ) 映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
open 函数用于打开设备文件。
release 函数用于释放 ( 关闭 ) 设备文件,与应用程序中的 close 函数对应。
fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。

2、字符设备驱动开发步骤

(1)、驱动模块的加载和卸载

        Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启
动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块 (Linux 下模块扩展名为 .ko) ,在
Linux 内核启动以后使用“ insmod ”命令加载驱动模块。
         模块有加载和卸载两种操作, 我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
        module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod ”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使“rmmod ”命令卸载具体驱动的时候 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)字符设备注册与注销
        对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示 :
static inline int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)
major 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name :设备名字,指向一串字符串。
fops 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
static inline void unregister_chrdev(unsigned int major,const char *name)
major 要注销的设备对应的主设备号。
name 要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
(3)实现设备的具体操作函数
        file_operations 结构体就是设备的具体操作函数,假设定义了 file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、 release、read 和 write 等具体的设备操作函数。对 file_operations 结构体进行操作就是对其进行初始化。这里定义该驱动文件名叫: chrtest
        ①能够对 chrtest 进行打开和关闭操作
        设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open release 这两个函数。
        ②对 chrtest 进行读写操作
        chrtest 这个设备控制着一段缓冲区 ( 内存 ) ,应用程序需要通过 read write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read write 这两个函数。

        模板如下:

    /* 打开设备 */
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);

MODULE_LICENSE("GPL") //添加模块 LICENSE 信息
MODULE_AUTHOR("#Fifndef _strive_h") //添加模块作者信息

我这里没有添加头文件,这些函数或者 结构体都是要头文件的。

(4)添加 LICENSE 和作者信息
        最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否
则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用
如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
LICENSE 采用 GPL 协议。 上面的代码就是一个大概驱动编写模板。

二、设备号

1、设备号的组成

        为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
dev_t __u32 类型的。
typedef unsigned int __u32;
dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。 几个关于设备号的操作函数 ( 本质是宏 ) ,如下所示:
#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 类型的设备号。
2、设备号的分配
①静态分配设备号

静待分配设备号就是:开发者注册字符设备的时候需要给设备指定一个设备号,这个设备号是驱动开发者静态的指定一个设备号,比如选择 100 这个主设备号。

②动态分配设备号

 静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用 的。而且静态分配设备号很容易带来冲突问题,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 开始,要释放的设备号数量。

三、测试APP的编写的函数详解

1 C 库文件操作基本函数
        编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read write close 这四个函数。
①、 open 函数
int open(const char *pathname, int flags)
pathname :要打开的设备或者文件名。
flags 文件打开模式,以下三种模式必选其一:
O_RDONLY         只读模式
O_WRONLY         只写模式
O_RDWR        读写模式
还有其他的这里就不多说了。
返回值 :如果文件打开成功的话返回文件的 文件描述符
②、 read 函数
ssize_t read(int fd, void *buf, size_t count)
fd :要读取的 文件描述符 ,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf 数据读取到此 buf 中。
count 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
③、 write 函数
ssize_t write(int fd, const void *buf, size_t count);
fd :要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf 要写入的数据。
count 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
④、 close 函数
int close(int fd);
fd :要关闭的文件描述符。
返回值: 0 表示关闭成功,负值表示关闭失败。
测试APP函数的详解是为了后面编写完Linux驱动之后对驱动进行测试。

 
  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值