一.应用程序、内核、驱动的关系
硬件设备收驱动程序(中断服务函数)控制。
当用户调用一些API函数(库函数)会产生系统调用与内核产生关联,每一个API函数都会产生一个或多个系统调用,每个系统调用都会执行相应的SW指令,执行该指令相当于产生中断,中断进入中断服务函数,即驱动程序。如此:应用层就与硬件产生关联。
简言之:系统调用时应用层与内核层接口,驱动程序是内核层与硬件层接口
二.设备驱动分类
1.字符设备
LED、KEY、UART、SPI、IIC、RTC、LCD
是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。他不具备缓冲区,所以对这种设备的读写是实时的。
2.块设备
FLASH、SD卡、emmc
是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,将缓冲区数据一次性写入设备或者从设备一次性读取。
3.网络设备
网卡、WIFI
网络设备是一类特殊的设备,它不像字符设备或块设备那样通过对应的设备文件节点访问。网络设备是通过套接字来进行访问的。
注:字符设备是驱动开发中重点研究的对象。块设备和网络设备驱动程序虽然更复杂,但是这些驱动程序已经标准化,芯片厂商都已经做好了,我们直接使用即可。对于字符设备而言,虽然简单,但是比较杂,无法形成统一标准,需要用户自行开发。
三.设备文件与设备号
用户是通过设备文件(/dev/xxx)来访问对应硬件设备。每个设备文件都有其文件属性:c代表字符设备,b代表块设备。每个设备文件都有两个设备号,主设备号和次设备号。
主设备号:用于标识驱动程序。LED
次设备号:用于标识使用同一个设备驱动程序的不同硬件设备。LED1、LED2、LED3
内核通过设备号(32bit,主设备号(12bit)+次设备号(20bit))来唯一的标识一个设备。在/dev目录下使用ls -l命令可以查看各个设备的设备类型、主从设备号等。cat /proc/devices可以查看系统中所有设备对应的主设备号。
注:设备文件的主设备号必须与设备驱动程序在申请时主设备号保持一致,这样就能保证应用程序通过访问设备文件调用指定的设备驱动程序,进而控制硬件操作。
四.字符设备开发三种方式
1.经典字符设备开发
1)申请设备号
头文件 | #include <linux/fs.h> | |
函数原型 | int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) | |
函数功能 | 字符设备注册函数,用于申请设备号,同时将驱动接口函数加载到内核。 | |
函数参数 | major | 申请的主设备号,指定为0代表自动分配,返回值就是分配的设备号。 |
name | 设备名,用来描述设备。/proc/devices列举出所有已经注册的设备。 | |
fops | 文件操作对象(结构体),提供驱动接口函数 | |
函数返回值 | 成功返回系统分配的主设备号,出错返回负数。 | |
缺陷:主设备号需要手动指定,且指定后默认次设备0-255全被注册,占用内核资源
|
2)释放设备号
头文件 | #include <linux/fs.h> | |
函数原型 | void unregister_chrdev(unsigned int major,const char *name); | |
函数功能 | 字符设备注销函数,用于释放设备号,同时将驱动接口函数从内核中卸载。 | |
函数参数 | major | 要注销驱动的主设备号 |
name | 要注销驱动的名字 | |
函数返回值 | 无 |
3)用户空间和内核空间数据交换
系统运行分两种状态:用户态、内核态 。驱动程序工作在内核态,应用程序工作在用户态。这两种状态下数据是互不可见的。
解决通信: copy_from_user 、copy_to_use
头文件 | #include <asm/uaccess.h> | |
函数原型 | long copy_from_user(void *to,const void __user *from, unsigned long n) | |
函数功能 | 将数据从用户空间拷贝到内核空间 | |
函数参数 | to | 数据会存储到该指针指向的空间 |
from | 从应用层传输来的数据 | |
n | 传输数据的字节个数 | |
函数返回值 | 成功返回值为0,失败返回值大于0,表示还剩下多少个没有拷贝成功 |
头文件 | #include <asm/uaccess.h> | |
函数原型 | long copy_to_user(void __user *to,const void *from, unsigned long n) | |
函数功能 | 将数据从内核空间拷贝到用户 | |
函数参数 | to | 传输到该指针指向的空间 |
from | 向应用层传输的数据 | |
n | 传输数据的字节个数 | |
函数返回值 | 成功返回值为0,失败返回值大于0,表示还剩下多少个没有拷贝成功 |
4)示例
内核层:
#include<linux/module.h>//include path
#include<linux/kernel.h>//include path
#include<linux/io.h>//include path
#include<linux/fs.h>//include path
#include <asm/uaccess.h>
#include <linux/device.h>
static char buff[32] = "hello"; //内核向应用层发送数据
static int major; //存储主设备号
int xxx_open(struct inode *inode, struct file *file)
{
printk("xxx_open is run\n");
return 0;
}
ssize_t xxx_read(struct file *file, char __user *app_buff, size_t size, loff_t *loff)
{
int ret;
printk("xxx_read is run\n");
ret = copy_to_user(app_buff,buff,size); //内核将数据传递到应用层
if(ret != 0)
{
return -1;
}
return size;
}
ssize_t xxx_write(struct file *file, const char __user *app_buff, size_t size, loff_t *loff)
{
char temp_buff[32] = {0};
int ret;
printk("xxx_write is run\n");
ret = copy_from_user(temp_buff, app_buff, size);//应用层传递数据给内核层
if(ret != 0)
{
return -1;
}
printk("temp_buff = %s\n",temp_buff);
return size;
}
int xxx_close(struct inode *inode, struct file *file)
{
printk("xxx_close is run\n");
return 0;
}
static struct file_operations fops = {
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_close
};
static int __init xyd_module_init(void)
{
printk("xyd_module_init is run\n");
if((major = register_chrdev(0,"xyd_module",&fops)) < 0 )
{
printk("get major error");
return -1;
}
printk("major = %d\n",major);
return 0;
}
static void __exit xyd_module_exit(void)
{
unregister_chrdev(major,"xyd_module");
printk("xyd_module_exit is run\n");
}
module_init(xyd_module_init);
module_exit(xyd_module_exit);
MODULE_LICENSE("GPL");
应用层:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argv,char *argc[])
{
char buff[32] = "nihao";
char buff1[32] = {0};
int fd = open("/dev/xyd_module",O_RDWR);
if(fd < 0)
{
perror("open");
}
int write_num = write(fd,buff,5);
printf("write_num = %d\n",write_num);
read(fd,buff1,5);
printf("buff1 = %s\n",buff1);
close(fd);
return 0;
}
2.杂项设备驱动开发
杂项设备属于字符设备的一种,使用杂项设备模型主设备号为10。
优点:使用简单,相当于对新版设备驱动进行了封装,节约主设备号(驱动不管静态还是动态分配,都会消耗主设备号,杂项同一使用10,用子设备号区分)
缺点:应用层可能调用不了设备驱动。可能是封装的不太好。
miscdevice结构体在include/linux/miscdevice.h文件中的定义如下:
struct miscdevice {
int minor; //次设备号(0-255) 写255代表自动分配次设备号
const char *name; //设备文件名,会根据设备名自动创建设备节点
const struct file_operations *fops; //字符设备文件操作函数集合
struct list_head list; //以下是内核使用,用户不需要关注
struct device *parent;
struct device *this_device;
const char *nodename;
umode_t mode;
};
1)杂项设备注册函数
头文件 | #include <linux/miscdevice.h> |
函数原型 | int misc_register(struct miscdevice *misc); |
函数功能 | 向内核注册一个杂项设备 |
函数参数 | 杂项设备对象 |
函数返回值 | 成功返回0,失败返回负数 |
2)杂项设备注销函数
头文件 | #include <linux/miscdevice.h> |
函数原型 | int misc_deregister(struct miscdevice *misc); |
函数功能 | 从内核删除一个杂项设备 |
函数参数 | 杂项设备对象 |
函数返回值 | 成功返回0,失败返回负数 |
3)示例
内核层:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
int miscDev_open(struct inode *inode, struct file *file)
{
printk("*****%s*****\n",__FUNCTION__);
return 0;
}
int miscDev_close(struct inode *inode, struct file *file)
{
printk("*****%s*****\n",__FUNCTION__);
return 0;
}
ssize_t miscDev_read(struct file *file, char __user *buff, size_t count, loff_t *loff)
{
printk("*****%s*****\n",__FUNCTION__);
return 0;
}
ssize_t miscDev_write(struct file *file, const char __user *buff, size_t count, loff_t *loff)
{
printk("*****%s*****\n",__FUNCTION__);
return 0;
}
struct file_operations fops = {
.open = miscDev_open,
.release = miscDev_close,
.read = miscDev_read,
.write = miscDev_write,
};
struct miscdevice misc = {
.minor = 255,
.name = "miscDev",
.fops = &fops,
};
static int __init miscDev_init(void)
{
printk("*****%s*****\n",__FUNCTION__);
/* 杂项设备注册 */
if(misc_register(&misc)<0){
printk("misc_register error");
return -1;
}
return 0;
}
static void __exit miscDev_exit(void)
{
printk("*****%s*****\n",__FUNCTION__);
/* 杂项设备注销 */
misc_deregister(&misc);
}
module_init(miscDev_init);
module_exit(miscDev_exit);
MODULE_LICENSE("GPL");
应用层:同上
3.新版字符设备开发
旧版:申请主设备号需要指定,在申请一个主设备号的同时会将该主设备号下的所有次设备号都使用掉。主设备号一致的情况下可以使用任何次设备号
新版:申请主设备号系统分配。可以连续申请设备号(主设备一致,次设备相邻连续)
1)申请设备号
如果给定了设备的主设备号和次设备号,可以使用静态申请设备号。如果没有指定设备号,就可以使用动态申请设备号。
静态申请
头文件 | #include <linux/fs.h> | |
函数原型 | int register_chrdev_region(dev_t from, unsigned count, const char *name) | |
函数功能 | 静态申请一个指定的设备号范围 | |
函数参数 | from | 指定起始设备号(主+次) |
count | 指定次设备号数量 | |
name | 设备驱动名字,可通过/proc/devices文件查看 | |
函数返回值 | 成功返回0,失败返回负数 |
动态申请
头文件 | #include <linux/fs.h> | ||
函数原型 | int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) | ||
函数功能 | 动态申请一个随机的设备号范围
| ||
函数参数 | dev | 存放申请到的设备号 凡是看到dev_t就表示设备号 | |
baseminor | 指定申请的起始次设备号,一般写0 | ||
count | 指定申请次设备号的数量 | ||
name | 设备驱动名字,可通过/proc/devices文件查看 | ||
函数返回值 | 返回值:成功返回0,失败返回负数 |
|
2)释放设备号
头文件 | #include <linux/fs.h> | |
函数原型 | void unregister_chrdev_region(dev_t from, unsigned count) | |
函数功能 | 释放一个设备号范围 | |
函数参数 | from | 起始设备号(包含主次设备号) |
count | 连续的此设备号数量 | |
函数返回值 | 无 |
3)设备注册
使用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分配空间有两种方法,第一种方法是直接定义cdev结构体变量;另外一种方法是调用cdev_alloc()函数来开辟一个cdev结构体空间。
struct cdev k; //或者
struct cdev *p = cdev_alloc();
初始化cdev(相当于申请了QQ,服务器需要开辟空间存储)
头文件 | #include <linux/cdev.h> | |
函数原型 | void cdev_init(struct cdev *cdev, const struct file_operations *fops); | |
函数功能 | 初始化cedv结构体 | |
函数参数 | cdev | 要初始化的cdev结构体变量 |
fops | 字符设备文件操作函数集合 | |
函数返回值 | 无 |
将cdev添加到内核(相当于QQ登录)
头文件 | #include <linux/cdev.h> | |
函数原型 | int cdev_add(struct cdev *p, dev_t dev, unsigned int count); | |
函数功能 | 向内核添加字符设备(cdev结构体变量) | |
函数参数 | p | 指向要添加的字符设备(cdev结构体变量) |
dev | 设备申请的设备号 | |
count | 连续次设备号数量 | |
函数返回值 | 返回值:成功返回0,失败返回负数 |
设备注销(相当于注销QQ)
头文件 | #include <linux/cdev.h> |
函数原型 | void cdev_del(struct cdev *p); |
函数功能 | 删除字符设备 |
函数参数 | 要删除的字符设备 |
函数返回值 | 无 |
释放cdev空间(相当于服务器删除用户信息)
4)创建设备文件
头文件 | #include <linux/device.h> | |
函数原型 | struct class * class_create(struct module *owner, const char *name) | |
函数功能 | 创建一个类 | |
函数参数 | owner | 直接写THIS_MODULE |
name | 类名,自定义 | |
函数返回值 | 成功返回指向类的指针,失败返回NULL |
头文件 | #include <linux/device.h> | |
函数原型 | struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...) | |
函数功能 | 创建一个设备文件 | |
函数参数 | class | class结构体,class_create调用之后得到的返回值。 |
parent | 表示父亲,一般直接填NULL。 | |
devt | 完整设备号(32位),只要是类型为dev_t的,该变量就是存放完整设备号的。 | |
drvdata | 私有数据,一般直接填NULL。 | |
fmt、... | 表示可变参数,字符串,表示设备节点名字。 | |
函数返回值 | 返回值:成功返回设备指针,失败返回NULL |
5)删除设备文件
头文件 | #include <linux/device.h> | |
函数原型 | void class_destroy(struct class *class) | |
函数功能 | 摧毁类 | |
函数参数 | class | 类指针 |
函数返回值 | 无 |
头文件 | #include <linux/device.h> | |
函数原型 | void device_destroy(struct class *class, dev_t devt) | |
函数功能 | 摧毁设备文件 | |
函数参数 | class | 类指针 |
devt | 完整设备号 | |
函数返回值 | 无 |
6)整体思路(先用后删)
INIT
- 动态注册设备号
- 创建cdev对象
- 初始化cdev对象
- 向内核添加cdev对象
- 创建类
- 创建设备节点 xyd_module会自动在/dev下创建
EXIT
- 删除设备文件
- 删除类
- 从内核注销cdev对象
- 释放cdev空间,销毁cdev对象
- 释放设备号
7)示例
内核层:
/*************************************************************************
# File Name: xyd_module.c
# Author: 冷瑾瑜
# mail: 1650337649@qq.com
# Created Time: 2021年04月20日 星期二 10时53分43秒
************************************************************************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
static struct class *xxx_class;
static struct device *xxx_device;//存放device_create的返回值
static struct cdev *_cdev;
static char buff[32] = "hello";
static dev_t _dev; //存放设备号
static int major; //存放主设备号
int xxx_open(struct inode *inode, struct file *file)
{
printk("xxx_open is run\n");
return 0;
}
ssize_t xxx_read(struct file *file, char __user *app_buff, size_t size, loff_t *loff)
{
int ret;
printk("xxx_read is run\n");
ret = copy_to_user(app_buff,buff,size);//内核发送数据到应用层
if(ret != 0)
{
return -1;
}
return size;
}
ssize_t xxx_write(struct file *file, const char __user *app_buff, size_t size, loff_t *loff)
{
char temp_buff[32] = {0};
int ret;
printk("xxx_write is run\n");
ret = copy_from_user(temp_buff, app_buff, size);//内核接收来自应用层的数据
if(ret != 0)
{
return -1;
}
printk("temp_buff = %s\n",temp_buff);
return size;
}
int xxx_close(struct inode *inode, struct file *file)
{
printk("xxx_close is run\n");
return 0;
}
static struct file_operations fops = {
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_close
}; //内核函数与应用层函数关联
static int __init xyd_module_init(void)
{
int ret;
printk("xyd_module_init is run\n");
//动态注册设备号
ret = alloc_chrdev_region(&_dev, 0, 5,"xyd_module");
if(ret < 0){
printk("alloc_chrdev_region error\n");
goto ERROR;
}
//创建cdev对象
_cdev = cdev_alloc();
if(_cdev== NULL){
printk("cdev_alloc error\n");
goto Unregister_chrdev;
// return -1;
}
//初始化cdev对象
cdev_init(_cdev,&fops);
//向内核添加cdev对象
ret = cdev_add(_cdev,_dev,5);
if(ret < 0){
printk("cdev_add error\n");
goto Free_Cdev;
}
//打印设备号
printk("major = %d\n",major);
//printk("major=%d,minor=%d\n",_dev >> 20,_dev & 0x000FFFFF);
printk("major=%d,minor=%d\n",MAJOR(_dev),MINOR(_dev));
//创建类
xxx_class = class_create(THIS_MODULE, "my_class");
if(xxx_class == NULL){
printk("class_create error");
goto Class_destor;
}
//创建设备节点 xyd_module会自动在/dev下创建
xxx_device = device_create(xxx_class, NULL,_dev, NULL, "xyd_module");
if(xxx_device == NULL){
printk("device_create error");
goto Device_destor;
}
printk("CREATE SUCCESS\n");
return 0;
Device_destor:
class_destroy(xxx_class);
Class_destor:
cdev_del(_cdev);
Free_Cdev:
kfree(_cdev);
Unregister_chrdev:
unregister_chrdev_region(_dev,5);
ERROR:
return -1;
}
static void __exit xyd_module_exit(void)
{
unregister_chrdev(major,"xyd_module");
printk("xyd_module_exit is run\n");
//删除设备文件
device_destroy(xxx_class, _dev);
//删除类
class_destroy(xxx_class);
//从内核注销cdev对象
cdev_del(_cdev);
//释放cdev空间,销毁cdev对象
kfree(_cdev);
//释放设备号
unregister_chrdev_region(_dev,5);
}
module_init(xyd_module_init);
module_exit(xyd_module_exit);
MODULE_LICENSE("GPL");
应用层:同上
结果: