一 简介
Linux设备驱动是指驱动Linux内核与硬件设备进行通信的软件模块。设备驱动通常分为两类:字符设备驱动和块设备驱动。
设备驱动的主要功能包括:
- 设备初始化:在系统启动时,设备驱动需要初始化相应的硬件设备,设置设备的寄存器和接口等参数,以确保设备能够正常工作。
- 设备控制:设备驱动需要提供一些接口,用于控制设备的各种操作,如打开设备、读取数据、写入数据、关闭设备等。
- 中断处理:设备驱动需要处理硬件设备的中断请求,在中断发生时执行相应的中断处理程序,以便及时响应设备的各种事件和请求。
- 数据传输:设备驱动需要实现数据的传输功能,包括从硬件设备读取数据和向硬件设备写入数据等操作。
- 错误处理:设备驱动需要处理设备发生的错误和异常情况,例如设备读写错误、中断丢失等。
1.1 设备驱动分类
1.1.1 块设备驱动
块设备驱动是指以块为单位与设备进行通信的驱动程序,例如硬盘、固态硬盘、USB闪存驱动器等存储设备驱动
块设备驱动程序通常采用分层结构,包括以下几个层次:
- 设备驱动程序层:这一层直接与硬件设备进行通信,并控制设备的各种操作。
- 存储卷管理器层:这一层负责管理存储卷(如硬盘分区、逻辑卷等),并提供对卷的访问接口。
- 文件系统层:这一层提供对卷的的文件系统接口,使得用户可以按照文件系统的目录结构访问和管理文件。
1.1.2 字符设备驱动
I2C/SPI 设备可以是字符设备、块设备或网络设备,具体取决于其功能和应用场景。
然而,对于某些I2C设备来说,它们可能被归类为块设备或网络设备等其他类型的设备。例如,某些I2C存储器(如EEPROM)可以被视为块设备的例子,因为它们以数据块为单位进行数据传输。而其他一些I2C设备,如温度传感器或光传感器等,则可以被视为网络设备,因为它们以数据包为单位进行数据传输。
因此,并不是所有的I2C/SPI设备都是字符设备,而是根据其功能和应用场景可能被归类为不同类型的设备。不同类型的设备需要不同的驱动程序来实现对它们的访问和控制。
Linux 应用程序对驱动程序的调用如图 40.1.1 所示
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开 /dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。
二 设备驱动原理
2.1 驱动模块的加载和卸载
2.1.1 加载
module_init(xxx_init); //注册模块加载函数module_exit(xxx_exit); //注册模块卸载函数
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和modprobe
insmod drv.komodprobe drv.ko
2.1.2 卸载
rmmod drv.komodprobe -r drv.ko //使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是 推荐使用 rmmod 命令
2.2 地址映射 MMU
Linux 内核启动的时候会初始化 MMU ,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。
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));
}
2.2.2 iounmap 函数
void iounmap (volatile void __iomem *addr)
2.2.3 I/O 内存访问函数
linux通过操作虚拟地址来间接操作物理地址
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 设备号
2.3.1组成
dev_t __u32 类型的其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095 ,所以大家在选择主设备号的时候一定不要超过这个范围。
2.3.2 设备号分配与注销
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)
2.4 新的字符设备注册方法
2.4.1、字符设备结构
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
2.4.2、cdev_init 函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
/* 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 函数
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
2.4.4、cdev_del 函数
void cdev_del(struct cdev *p)
2.5 自动创建设备节点
2.5.1 mdev 机制
echo /sbin/mdev > /proc/sys/kernel/hotplug
2.5.1.1 创建和删除类
struct class *class_create (struct module *owner, const char *name)
void class_destroy(struct class *cls);
2.5.1.2 创建设备
struct device *device_create(struct class*class,struct device *parent,dev_tdevt,void*drvdata,const char*fmt, ...)
void device_destroy(struct class *class, dev_t devt)
2.6 设置文件私有数据
/* 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")