从零开始理解linux设备驱动

点灯的背后原理

从零开始理解linux设备驱动_第1张图片
首先让大家简单了解一下点灯的原理。当我们使用./test_app文件点灯之后,其实是这个文件使用open函数打开了/dev下的设备节点(后面有介绍),然后通过操作设备节点控制系统调用进入linux内核去驱动底层硬件。下面将详细介绍一些相关的概念和具体实现原理。

关于设备的分类

设备主要分为:字符设备、块设备和网络设备
字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解, 因此我们选择从字符设备开始,从最初的模仿,到慢慢熟悉,最终成长为驱动界的高手。也就是说,学了字符设备相当于一通百通,足以应对日常开发。

设备树

有另外的文章介绍设备树,有不懂设备树的请点击:什么是linux设备树

设备号

在linux中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。举个简单的例子,使用i2c通信的两个模块有oled和mpu6050.他们的设备类别一样,都是i2c通信,但是他们是不同的设备。
在linux,使用一个dev_t数据结构来定义设备号,dev_t是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号。 也就是理论上主设备号取值范围:0-212,次设备号0-220。 实际上在内核源码__register_chrdev_region(…)函数中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一个宏,值是512。所以设备号最大值为512.

内核态与用户态是什么

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。简单来说,我们直接操作的比如打开某个文件,操作某个节点,我们直接进行操作的就是在用户态进行。而驱动程序等,比如说某个设备oled是需要时序初始化之后才能使用的,但是你并没有初始化它你就可以通过节点使用它,这是因为在linux内核里面已经帮你做好了,是在内核态完成这个工作的。
用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。
用户态应用程序中调用了 open 这个函数(系统调用),那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合

/dev下的设备节点

从零开始理解linux设备驱动_第2张图片

驱动加载成功以后会在“/dev”目录下生成一个相应的文件,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID 上。
这里讲讲/dev下设备生成的过程:当我们在linux设备树里面添加设备树节点的时候,会在/sys/firmware/devicetree/base目录下出现相应的节点,但是在/dev下是还没有出现的。在/dev下出现的节点是可以直接进行open等操作控制比如点灯等动作的。在/dev下出现节点的条件是驱动模块加载成功,然后驱动模块匹配到设备树节点的属性并且匹配成功(比如说驱动led的模块发现设备树里面确实有led节点,则匹配成功)。

驱动开发等于“填空题”?

其实我们编写linux驱动的时候,都是按照一个linux驱动框架去编写的。所以说学 Linux 驱动开发重点是学习其驱动框架。Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。我们平时开发的时候会将驱动编译成一个模块加载进内核,这样方便开发。
下面我来列举出一个完整驱动框架需要编写的几个部分:

1. 驱动模块的加载和卸载入口函数

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)
static inline void unregister_chrdev(unsigned int major, const char *name)

这两个函数分别写在入口和出口函数里面,功能是加载驱动时注册设备号以及卸载驱动时注销设备号。

3. 对应用户态系统调用函数(open函数等)的具体操作函数

在我们在命令端使用open函数打开某个节点的时候,就需要传递给内核态,让内核态执行关于打开相应的函数。
file_operations 结构体就是设备的具体操作函数,在这个结构体里面定义了一系列关系打开,关闭,读取,写入等操作的函数:

static struct file_operations test_fops = {
 .owner = THIS_MODULE,
 .open = chrtest_open,
 .read = chrtest_read,
 .write = chrtest_write,
 .release = chrtest_release,
 };

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

编写了四个函数: chrtest_open、 chrtest_read、 chrtest_write
和 chrtest_release。这四个函数就是设备的 open、 read、 write 和 release 操作函数。

4.添加模块驱动的描述信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:

MODULE_LICENSE("GPL") //添加模块 LICENSE 信息,LICENSE 采用 GPL 协议。
MODULE_AUTHOR("my_name") //添加模块作者信息

以上四部完成,便可以在内核源码的环境下编译成.ko文件加载进内核中使用。使用需要配合上可执行文件对节点进行操作或者直接在命令行对节点进行操作了。本篇目的在于帮助读者了解linux驱动开发的过程,不做实战训练。希望可以帮到大家

你可能感兴趣的:(linux驱动开发,linux,内核)