文章目录
1.前言
开发板:正点原子阿尔法]
本文不涉及具体代码编写,代码编写可以看下一节:2.led驱动-字符设备的驱动实现
2.Linux下设备的分类
Linux下外设驱动可以分为三类:
-
字符设备驱动:最基础的一类驱动,字符设备指的是可以一个一个字节,按照字节读写操作的设备,比如文件,可以进行读写操作的设备,其中点灯,按键,IIC,SPI都属于字符设备
-
块设备驱动:以存储块为基础的设备驱动,比如EMMC,NAND Flash,SD卡等
-
网络设备驱动:具有块设备和字符设备的特点,具有输入和输出是有结构块的。但是包,帧,他的块大小又不是固定的
3.Linux下应用程序调用驱动流程
在Linux下,一切皆文件,驱动加载成功之后会在/dev目录下生成对应的驱动文件,应用可以直接调用相关函数对该驱动文件进行读写操作,比如打开,读写这个设备文件,但应用程序运行在用户空间,内核驱动运行在内核空间,应用程序如何调用驱动函数可以看这篇文章https://blog.csdn.net/weixin_42523774/article/details/103341058
简单总结为:以打开一个设备文件举例子
-
应用程序:应用程序使用open函数来打开/dev/xxx
-
open函数通过glibc库将传入的参数执行‘’swi"指令,引起CPU异常,进入内核
-
内核的处理异常的函数会找到对应的驱动程序,从而返回文件句柄给应用层,从而应用层可以操作这个驱动程序
4.Linux内核的驱动操作函数
应用程序使用到的函数在具体的驱动程序中都有与之对应的驱动函数,举个例子,应用程序有open函数,驱动程序也有open函数,在Linux内核文件include/linux/fs.h中有个file_operations结构体,就是Linux内核驱动操作函数集合:
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 *, constchar __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 *, unsignedint, unsignedlong);
long (*compat_ioctl) (struct file *, unsignedint, unsignedlong);
int (*mmap) (struct file *, struct vm_area_struct *);
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 *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
.......
};
简单介绍一下常用的函数:
-
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 是异步刷新待处理的
5. Linux设备号
为了方便管理每一个设备,Linux驱动将每一个设备分配一个设备号,设备号分为主设备号,次设备号,主设备号表示某一个具体设备,次设备号表示这个驱动的各个设备,Linux 提供了名为dev_t的数据类型表示设备号,其本质是32位的unsigned int数据类型,其中高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))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
- MINORBITS:表示次设备号占的位数
- MINORMASK :表示次设备号掩码
- MAJOR(dev):用于获取主设备号,将dev_t右移20位即可
- MINOR(dev):从dev_t中获取次设备号,取dev_t的低20位即可
- 宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号
5.1 设备号的分配
设备号的分配主要指主设备号的分配,分为静态分配和动态分配
- 静态分配:需要手动指定设备号,不可与系统中已经使用的设备号重复,使用“cat /proc/devices”命令可查看当前系统中所有已经使用了的设备号。
- 动态分配:在字符设备注册的时候申请一个设备号,系统会分配一个没有被使用的设备号,但是在系统卸载的时候需要释放掉这个设备号
设备号申请函数:次设备号会申请注册设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
-
dev:保存申请到的设备号
-
baseminor:次设备号的起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同 可以通过第三个参数指定申请的次设备号的个数,一般baseminor从0开始,表示次设备号从0开始
-
count:申请的设备号数量
-
name:设备名字
释放设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
-
from:要释放的设备号
-
count:表示从from开始,释放的设备号数量
6.Linux驱动的开发步骤
6.1 驱动的加载,卸载
Linux驱动有两种运行方式,一种是直接将驱动编译进内核里面,另一种将驱动编译成ko文件,在Linux系统起来之后动态加载这个驱动,在调试阶段一般将内核编译成ko,直接push进开发板或者通过tftp传到开发板,方便调试,
6.1.1 驱动的加载
驱动加载命令:insmod,modprobe.insmod不能解决不同内核驱动间的依赖关系,而modprobe会分析不同模块之间的依赖关系,在Linux驱动中,modprobe会自动到开发板/lib/modules/下寻找模块驱动,该目录一般需要自己手动创建
加载模块:
insmod drv.ko
使用modprobe加载驱动时需要创建module.dep文件,使用depmod命令可以自动创建这个文件,所以modprobe加载驱动的命令为:
depmod
modprobe drv.ko
卸载:使用以下任意命令都可以
rmmod drv.ko
modprobe -r drv.ko
使用modprobe命令可以卸载掉驱动模块所依赖的模块,前提是这些依赖模块没有被其他模块所引用,否则就不能使用modprobe来卸载,只能使用rmmod来卸载
6.1.2 模块加载卸载调用函数
在编写模块驱动时,需要编写模块的加载与卸载函数,分别为:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
其中,当我们使用insmod加载驱动时,xxx_init就会被调用,当我们使用rmmod卸载驱动时xxx_exit函数就会被调用
7.字符设备驱动模板
7.1 动态分配设备号
int major; /*主设备号*/
int minor; /*次设备号*/
dev_t devid; /*设备号*/
/*定义了主设备号*/
if (major)
{
devid = MKDEV(major, 0); /*大部分驱动次设备号都选择0*/
register_chrdev_region(devid, 1, "test");
}
/*没有定义设备号*/
else
{
alloc_chrdev_region(&devid, 0, 1, "test"); /*申请设备号*/
major = MAJOR(devid); /*获取分配号的主设备号*/
minor = MINOR(devid); /*获取分配号的次设备号*/
}
unregister_chrdev_region(devid, 1); /* 注销设备号 */
-
定义主/次设备号变量 major 和 minor,以及设备号变量 devid。
-
判断主设备号 major 是否有效,在 Linux 驱动中一般给出主设备号的话就表示这
个设备的设备号已经确定了,因为次设备号基本上都选择 0,这算个 Linux 驱动开发中约定俗成的一种规定。 -
如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0
-
使用 register_chrdev_region 函数来注册设备号。
-
如果 major 无效,那就表示没有给定设备号。此时就要使用 alloc_chrdev_region
函数来申请注册设备号。设备号申请成功以后使用 MAJOR 和 MINOR 来提取出主设备号和次设备号,当然提取主设备号和次设备号的代码可以不要
注销设备号都用这个函数:unregister_chrdev_region(devid, 1)
7.2 添加设备
在Linux下使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h中定义:
struct cdev {
struct kobject kobj;
struct module *owner;
conststruct file_operations *ops;/*文件操作函数集合*/
struct list_head list;
dev_t dev; /*设备号*/
unsignedint count;
};
定义一个cdev变量
struct cdev test_cdev
cdev_init函数
对定义的cdev进行初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
fops:test_fops为驱动的操作函数集
cdev_add
向Linux内核中添加字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
-
p:cdev结构体
-
dev:用到的设备号
-
count:添加的设备数量
cdev_del
卸载驱动时使用使用cdev_del卸载掉字符设备
void cdev_del(struct cdev *p)
p:要删除的字符设备
使用示例:
/*要初始化的cdev结构体*/
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
/* 初始化cdev*/
cdev_init(&testcdev, &test_fops);
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
dev_del(&testcdev); /* 删除 cdev */
7.3 自动创建设备结点
在嵌入式Linux,使用mdev来实现设备文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理在/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
下面通过mdev来实现设备文件节点的自动创建与删除
7.4 创建类和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成,一般在cdev_add函数后面添加自动创建设备节点相关代码,首先要创建一个class结构体,定义在include/linux/device.h 里面, class_create 是类创建函数, class_create 是个宏定义,内容如下
#define class_create(owner, name) \
({
\
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
struct class *__class_create(struct module *owner,
const char *name,
struct lock_class_key *key)
owner:一般为THIS_MODULE
name:类的名字
卸载驱动时需要删除类
/*
* cls:要删除的类
*/
void class_destroy(struct class *cls);
7.5 创建设备节点
创建class之后还不可以创建设备节点,我们还需要在这个类下创建一个设备,使用device_create在类下创建设备,函数原型为:
/***********************************************
* class:设备要创建哪个类下面
* parent:父设备, 一般为 NULL
* devt:设备号
* drvdata:设备可能会使用的一些数据,一般为 NULL
* fmt:设备名字
**********************************************/
struct device *device_create(struct clas *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
删除设备:
/*************************************************
* class:要删除设备所在的类
* devt:要删除的设备号
*************************************************/
void device_destroy(struct class *class, dev_t devt)
7.6 参考代码:
init代码:
static int __init led_init(void)
{
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (led.major) /* 定义了设备号 */
{
led.devid = MKDEV(led.major, 0);
register_chrdev_region(led.devid, LED_CNT, LED_NAME);
}
else/* 没有定义设备号 */
{
alloc_chrdev_region(&led.devid, 0, LED_CNT, LED_NAME); /* 申请设备号 */
led.major = MAJOR(led.devid); /* 获取分配号的主设备号 */
led.minor = MINOR(led.devid); /* 获取分配号的次设备号 */
}
printk("led major=%d,minor=%d\r\n",led.major, led.minor);
/* 2、初始化cdev */
led.cdev.owner = THIS_MODULE;
cdev_init(&led.cdev, &led_fops);
/* 3、添加一个cdev */
cdev_add(&led.cdev, led.devid, LED_CNT);
/* 4、创建类 */
led.class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(led.class))
{
return PTR_ERR(led.class);
}
/* 5、创建设备 */
led.device = device_create(led.class, NULL, led.devid, NULL, LED_NAME);
if (IS_ERR(led.device))
{
return PTR_ERR(led.device);
}
printk("led init done!\r\n");
return0;
卸载函数:
static void __exit led_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&led.cdev);/* 删除cdev */
unregister_chrdev_region(led.devid, LED_CNT); /* 注销设备号 */
device_destroy(led.class, led.devid);
class_destroy(led.class);
printk("led exit done!\r\n");
}
7.7 设备操作函数
这里以open,close,read.write为例:
/*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
{
/*用户实现具体功能*/
return0;
}
/*从设备读取*/
static ssize_tled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
/*用户实现具体功能*/
return0;
}
/*向设备写数据*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
/*用户实现具体功能*/
return0;
}
/*关闭释放设备*/
static int led_release(struct inode *inode, struct file *filp)
{
/*用户实现具体功能*/
return0;
}
/*文件操作结构体*/
staticstruct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
7.8 添加LICENSE和作者信息
LICENSE是必须添加的,否则编译时会报错,作者信息可加可不加
MODULE_LICENSE("GPL");//GPL:Linux开源协议
MODULE_AUTHOR("xxx"); //xxx:作者名字
8.代码模板
下面代码不是具体代码,只是一个框架
#define LED_NAME "led"
#define LED_CNT 1
struct led_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct led_dev led;
/*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
{
/*用户实现具体功能*/
filp->private_data = &chrdevbase; /* 设置私有数据 */
return0;
}
/*从设备读取*/
static ssize_tled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
/*用户实现具体功能*/
return0;
}
/*向设备写数据*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
/*用户实现具体功能*/
return0;
}
/*关闭释放设备*/
static int led_release(struct inode *inode, struct file *filp)
{
/*用户实现具体功能*/
return0;
}
/*文件操作结构体*/
staticstruct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (led.major) /* 定义了设备号 */
{
led.devid = MKDEV(led.major, 0);
register_chrdev_region(led.devid, LED_CNT, LED_NAME);
}
else/* 没有定义设备号 */
{
alloc_chrdev_region(&led.devid, 0, LED_CNT, LED_NAME); /* 申请设备号 */
led.major = MAJOR(led.devid); /* 获取分配号的主设备号 */
led.minor = MINOR(led.devid); /* 获取分配号的次设备号 */
}
printk("led major=%d,minor=%d\r\n",led.major, led.minor);
/* 2、初始化cdev */
led.cdev.owner = THIS_MODULE;
cdev_init(&led.cdev, &led_fops);
/* 3、添加一个cdev */
cdev_add(&led.cdev, led.devid, LED_CNT);
/* 4、创建类 */
led.class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(led.class))
{
return PTR_ERR(led.class);
}
/* 5、创建设备 */
led.device = device_create(led.class, NULL, led.devid, NULL, LED_NAME);
if (IS_ERR(led.device))
{
return PTR_ERR(led.device);
}
printk("led init done!\r\n");
return0;
}
static void __exit led_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&led.cdev);/* 删除cdev */
unregister_chrdev_region(led.devid, LED_CNT); /* 注销设备号 */
device_destroy(led.class, led.devid);
class_destroy(led.class);
printk("led exit done!\r\n");
}
MODULE_LICENSE("GPL");//GPL:Linux开源协议
MODULE_AUTHOR("xxx"); //xxx:作者名字