一 简介
Linux设备驱动是指驱动Linux内核与硬件设备进行通信的软件模块。设备驱动通常分为两类:字符设备驱动和块设备驱动。
设备驱动的主要功能包括:
- 设备初始化:在系统启动时,设备驱动需要初始化相应的硬件设备,设置设备的寄存器和接口等参数,以确保设备能够正常工作。
- 设备控制:设备驱动需要提供一些接口,用于控制设备的各种操作,如打开设备、读取数据、写入数据、关闭设备等。
- 中断处理:设备驱动需要处理硬件设备的中断请求,在中断发生时执行相应的中断处理程序,以便及时响应设备的各种事件和请求。
- 数据传输:设备驱动需要实现数据的传输功能,包括从硬件设备读取数据和向硬件设备写入数据等操作。
- 错误处理:设备驱动需要处理设备发生的错误和异常情况,例如设备读写错误、中断丢失等。
1.1 设备驱动分类
1.1.1 块设备驱动
块设备驱动是指以块为单位与设备进行通信的驱动程序,例如硬盘、固态硬盘、USB闪存驱动器等存储设备驱动
块设备驱动程序通常采用分层结构,包括以下几个层次:
- 设备驱动程序层:这一层直接与硬件设备进行通信,并控制设备的各种操作。
- 存储卷管理器层:这一层负责管理存储卷(如硬盘分区、逻辑卷等),并提供对卷的访问接口。
- 文件系统层:这一层提供对卷的的文件系统接口,使得用户可以按照文件系统的目录结构访问和管理文件。
1.1.2 字符设备驱动
字符设备是
Linux
驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、
IIC
、
SPI
,
LCD
等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
I2C/SPI 设备可以是字符设备、块设备或网络设备,具体取决于其功能和应用场景。
然而,对于某些I2C设备来说,它们可能被归类为块设备或网络设备等其他类型的设备。例如,某些I2C存储器(如EEPROM)可以被视为块设备的例子,因为它们以数据块为单位进行数据传输。而其他一些I2C设备,如温度传感器或光传感器等,则可以被视为网络设备,因为它们以数据包为单位进行数据传输。
因此,并不是所有的I2C/SPI设备都是字符设备,而是根据其功能和应用场景可能被归类为不同类型的设备。不同类型的设备需要不同的驱动程序来实现对它们的访问和控制。
Linux 应用程序对驱动程序的调用如图 40.1.1 所示
在
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 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。
二 设备驱动原理
2.1 驱动模块的加载和卸载
2.1.1 加载
Linux
驱动有两种运行方式,第一种就是将驱动编译进
Linux
内核中,这样当
Linux
内核启
动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块
(Linux
下模块扩展名为
.ko)
,在
Linux
内核启动以后使用“
insmod
”命令加载驱动模块。在调试驱动的时候一般都选择将其编译
为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个
Linux
代码。
而且在调试的时候只需要加载或者卸载驱动模块即可
module_init(xxx_init); //注册模块加载函数module_exit(xxx_exit); //注册模块卸载函数
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和modprobe
insmod
命令不能解决模块的依赖关系
比如
drv.ko
依赖
first.ko
这个模块,就必须先使用 insmod 命令加载
first.ko
这个模块,然后再加载
drv.ko
这个模块。
modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,
insmod drv.komodprobe drv.ko
2.1.2 卸载
驱动模块的卸载使用命令“
rmmod
”即可,比如要卸载
drv.ko
,
rmmod drv.komodprobe -r drv.ko //使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是 推荐使用 rmmod 命令
2.2 地址映射 MMU
MMU
全称叫做
Memory Manage Unit,也就是内存管理单元。在老版本的
Linux
中要求处理器必须有
MMU
,但是现在
Linux
内核已经支持无
MMU
的处理器了。
MMU
主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
对于
32
位 的处理器来说,虚拟地址范围是 2^32=4GB
Linux 内核启动的时候会初始化 MMU ,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。
比如I.MX6ULL 的 GPIO1_IO03 引脚的复用寄存器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为
0X020E0068
。
如果没有开启
MMU
的话 直接向 0X020E0068
这个寄存器地址写入数据就可以配置
GPIO1_IO03
的复用功能。
现在开启 了 MMU
,并且设置了内存映射,因此就不能直接向
0X020E0068
这个地址写入数据了。我们必 须得到 0X020E0068
这个物理地址在
Linux
系统里面对应的虚拟地址,这里就涉及到了物理内 存和虚拟内存之间的转换,需要用到两个函数:ioremap
和
iounmap
。
2.2.1 ioremap 函数
ioremap
函数用于获取指定物理地址空间对应的虚拟地址空间,定义在 arch/arm/include/asm/io.h 文件中,定义如下:
示例代码 41.1.1.1 ioremap 函数
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),
MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
}
此函 数有三个参数和一个返回值,这些参数和返回值的含义如下:
phys_addr
:要映射的物理起始地址。
size
:要映射的内存空间大小。
mtype
:
ioremap
的类型,可以选择
MT_DEVICE
、
MT_DEVICE_NONSHARED
、
MT_DEVICE_CACHED
和
MT_DEVICE_WC
,
ioremap
函数选择
MT_DEVICE
。
返回值:
__iomem
类型的指针,指向映射后的虚拟空间首地址。
2.2.2 iounmap 函数
卸载驱动的时候需要使用
iounmap
函数释放掉
ioremap
函数所做的映射,
iounmap
函数原
型如下
void iounmap (volatile void __iomem *addr)
iounmap
只有一个参数
addr
,此参数就是要取消映射的虚拟地址空间首地址。
2.2.3 I/O 内存访问函数
linux通过操作虚拟地址来间接操作物理地址
1
、读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
2 写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
2.3 设备号
Linux
中每个设备都有一个设备号,设备号由主设备号和次设备号两部分 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备
2.3.1组成
Linux
提供了 一个名为 dev_t
的数据类型表示设备号,
dev_t
定义在文件
include/linux/types.h 里面,
dev_t __u32 类型的其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095 ,所以大家在选择主设备号的时候一定不要超过这个范围。
2.3.2 设备号分配与注销
静态分配
有些常用的设备号已经被
Linux
内核开发者给分配掉 了,具体分配的内容可以查看文档 Documentation/devices.txt
。并不是说内核开发者已经分配掉 的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个 主设备号,使用“cat /proc/devices
”命令即可查看当前系统中所有已经使用了的设备号。
动态分配
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用
的。而且静态分配设备号很容易带来冲突问题,
Linux
社区推荐使用动态分配设备号,在注册字
符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。
卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
1
申请函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
baseminor
:
次设备号起始地址,
alloc_chrdev_region
可以申请一段连续的多个设备号,这
些设备号的主设备号一样,但是次设备号不同,次设备号以
baseminor
为起始地址地址开始递
增。
一般 baseminor 为 0,也就是说次设备号从 0 开始。
count
:
要申请的设备号数量。
2
注册函数
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
3 注销函数
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from
:要释放的设备号。
count
:
表示从
from
开始,要释放的设备号数量。
2.4 新的字符设备注册方法
2.4.1、字符设备结构
在
Linux
中使用
cdev
结构体表示一个字符设备,
cdev
结构体在
include/linux/cdev.h
文件中
的定义如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
在
cdev
中有两个重要的成员变量:
ops
和
dev
,这两个就是字符设备文件操作函数集合
file_operations
以及设备号
dev_t
。
2.4.2、cdev_init 函数
定义好
cdev
变量以后就要使用
cdev_init
函数对其进行初始化,
cdev_init
函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数
cdev
就是要初始化的
cdev
结构体变量,参数
fops
就是字符设备文件操作函数集合。
使用
cdev_init
函数初始化
cdev
变量的示例代码如下:
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/* 2、初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
2.4.3、cdev_add 函数
cdev_add
函数用于向
Linux
系统添加字符设备
(cdev
结构体变量
)
,
首先使用
cdev_init
函数
完成对 cdev
结构体变量的初始化,然后使用
cdev_add
函数向
Linux
系统添加这个字符设备。
cdev_add
函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数
p
指向要添加的字符设备
(cdev
结构体变量
)
,参数
dev
就是设备所使用的设备号,参
数
count
是要添加的设备数量。
2.4.4、cdev_del 函数
卸载驱动的时候一定要使用
cdev_del
函数从
Linux
内核中删除相应的字符设备,
cdev_del
函数原型如下:
void cdev_del(struct cdev *p)
参数
p
就是要删除的字符设备。
2.5 自动创建设备节点
在前面的
Linux
驱动实验中,当我们使用
modprobe
加载驱动程序以后还需要使用命令
“
mknod
”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现
自动创建设备节点的功能以后,使用
modprobe
加载驱动模块成功的话就会自动在
/dev
目录下
创建对应的设备文件。
2.5.1 mdev 机制
udev
是一个用户程序,在
Linux
下通过
udev
来实现设备文件的创建与删除,
udev
可以检
测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用
modprobe
命令成功加载驱动模块以后就自动在
/dev
目录下创建对应的设备节点文件
,
使用
rmmod
命令卸载驱动模块以后就删除掉
/dev
目录下的设备节点文件。
使用 busybox 构建根文件
系统的时候,busybox 会创建一个 udev 的简化版本—mdev
,
所以在嵌入式 Linux 中我们使用
mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理,在
/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
如何通过
mdev
来实现设备文件节点的自动创建与删除?
2.5.1.1 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在
cdev_add
函数后面添
加自动创建设备节点相关代码。首先要创建一个
class
类,
class
是个结构体,定义在文件
include/linux/device.h
里面。
class_create
是类创建函数,
class_create
是个宏定义,
struct class *class_create (struct module *owner, const char *name)
卸载驱动程序的时候需要删除掉类,类删除函数为
class_destroy
,函数原型如下:
void class_destroy(struct class *cls);
2.5.1.2 创建设备
使用
device_create
函数在类下面创建设备
struct device *device_create(struct class*class,struct device *parent,dev_tdevt,void*drvdata,const char*fmt, ...)
device_create
是个可变参数函数,参数
class
就是设备要创建哪个类下面;参数
parent
是父
设备,一般为
NULL
,也就是没有父设备;参数
devt
是设备号;参数
drvdata
是设备可能会使用
的一些数据,一般为
NULL
;参数
fmt
是设备名字,如果设置
fmt=xxx
的话,就会生成
/dev/xxx
这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为
device_destroy
,函数原
型如下:
void device_destroy(struct class *class, dev_t devt)
2.6 设置文件私有数据
每个硬件设备都有一些属性,比如主设备号
(dev_t)
,类
(class)
、设备
(device)
、开关状态
(state)
等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,如下
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
2.7 通用设备驱动创建模版 参考示例
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
...
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (newchrled.major) { /* 定义了设备号 */
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申请设备号 */
newchrled.major = MAJOR(newchrled.devid); /* 获取分配号的主设备号 */
newchrled.minor = MINOR(newchrled.devid); /* 获取分配号的次设备号 */
}
printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);
/* 2、初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/* 3、添加一个cdev */
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
/* 4、创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
return PTR_ERR(newchrled.class);
}
/* 5、创建设备 */
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
return PTR_ERR(newchrled.device);
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
....
/* 注销字符设备驱动 */
cdev_del(&newchrled.cdev);/* 删除cdev */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */
device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
// MODULE_VERSION("4.1.15-g3dc0a4b SMP preempt mod_unload modversions ARMv7 p2v8")