当前位置: 首页 > news >正文

4-Linux驱动开发-字符设备驱动

说明:Linux 是文件型系统,所有硬件都会在对应的目录 (/dev) 下面用相应的文件表示。特点是访问文件的方式访问设备。
设备种类:

设备种类:字符设备: 应用程序按字节/字符来读写数据的设备。流形式,逐一存储。块设备: 支持随机存取和寻址,并使用缓存器。数据的读写只能以块的倍数进行。网络设备: 一种特殊设备,并不存在于/dev 下,主要用于网络数据的收发。

设备文件:

linux 中,使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。
cdev 结构体被内核用来记录设备号,
使用设备时,打开设备节点,通过设备节点的 inode 结构体、file 结构体最终找到 file_operations 结构体,
从 file_operations 结构体中得到操作设备的具体方法。ls -l /dev
设备文件详情
crw-r--r--  1 root root     10, 235 11月  3 23:11 autofs
第 1 列:文件类型和权限第 1 个字符 (文件类型):c:字符设备 (Character Device)。d:目录 (Directory)。l:符号链接 (Symbolic Link) / 快捷方式。b:块设备 (Block Device)。后 9 个字符 (权限):三组:Owner (所有者), Group (所属组), Others (其他人)。
第 2 列:硬链接数 (1):有多少个文件名指向这个inode(文件的物理数据)。1 或 2,可以暂时忽略。
第 3 列:所有者 (Owner)。
第 4 列:所属组 (Group)。
第 5 列:主设备号, 次设备号对于普通文件,这里显示的是文件大小。对于字符设备 (c) 和块设备 (b)主设备号 (Major Number)。内核用它来识别驱动程序。次设备号 (Minor Number)。内核用它来告诉驱动具体是哪个设备。
第 6 列:最后修改时间。
第 7 列:文件名。对于链接 (l)会显示一个“箭头”,cdrom -> sr0 意味着 cdrom 只是一个指向 sr0 的“快捷方式”。

字符设备驱动结构

字符cdev 结构体
内核通过一个散列表 (哈希表) 来记录设备编号。哈希表由数组和链表组成,吸收数组查找快,链
表增删效率高,容易拓展等优点。设备号为 cdev_map 编号,使用哈希函数来计算组数下标。
Linux内核,使用cdev结构体描述一个字符设备。
/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/include/linux/cdev.h

struct cdev {struct kobject kobj; //Kernel Object//(内核内部使用) 用于 sysfs 表示和引用计数。不用管。struct module *owner; //Module Owner//(必须提供) 指向 THIS_MODULE,用于防止模块在使用时被卸载(cdev_init 会自动设置)。const struct file_operations *ops;//File Operations//(必须提供) 指向file_operations结构体(函数菜单:open, read, write...)。struct list_head list;//List Head//(内核内部使用) 用于将 cdev 链接到内核的全局哈希表中。不用管。dev_t dev;//Device Number//(内核填充) 存储“主设备号”和“起始次设备号”。unsigned int count;//(内核填充) 驱动能管理多少个“连续”的次设备号。
};

相关宏

//使用以下宏可获得主设备号以及次设备号 
MAJOR(dev_t dev)
MINOR(dev_t dev)
//使用以下宏可根据主设备号以及次设备号生成dev_t
MKDEV(int major,int minor)

操作dev结构体的函数
/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/fs/char_dev.c

EXPORT_SYMBOL(cdev_init);
EXPORT_SYMBOL(cdev_alloc);
EXPORT_SYMBOL(cdev_del);
EXPORT_SYMBOL(cdev_add);
//初始化cdev成员,并建立cdev和file_operations之间的连接
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;
}
//动态申请一个cdev内存
struct cdev *cdev_alloc(void)
{struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);if (p) {INIT_LIST_HEAD(&p->list);kobject_init(&p->kobj, &ktype_cdev_dynamic);}return p;
}
//cdev_del,cdev_add用于删除添加cdev,用于字符设备的注册与注销
void cdev_del(struct cdev *p)
{cdev_unmap(p->dev, p->count);kobject_put(&p->kobj);
}
//cdev_add向系统注册字符设备之前要先调用register_chrdev_region或alloc_chrdev_region向系统申请设备号
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{int error;p->dev = dev;p->count = count;error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);if (error)return error;kobject_get(p->kobj.parent);return 0;
}
//用于已知起始设备的设备号
/*from指定字符设备的起始设备号
**count指定要申请的设备号个数
**name:用于指定该设备的名称,可以在/proc/devices 中看到该设备*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);//用于设备号未知,向系统动态申请未被占用的设备号,得到的设备号放入dev,可以自动避开设备冲突
/*dev存放分配到的设备编号的起始值
**baseminor次设备号的起始值,通常情况下,设置为 0
**count、name指定需要分配的设备编号的个数以及设备的名称*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
//删除
void unregister_chrdev_region(dev_t from, unsigned count);

内核数据结构

文件操作方式(file_operations)

file_operation 关联系统调用和驱动程序。这个结构的每一个成员都对应着一个系统调用。
file_operations 结构体定义在/home/kun/kernal_code/ebf_linux_kernel-ebf_4.19_star/include/linux/fs.h中
fs.h文件太长了ctrl+f 查找file_operations结构体 

字符设备驱动用到以下部分:

struct file_operations {struct module *owner;//llseek:用于修改文件的当前读写位置,并返回偏移后的位置。loff_t (*llseek) (struct file *, loff_t, int);//read:用于读取设备中的数据,并返回成功读取的字节数。ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//write:用于向设备写入数据,并返回成功写入的字节数。ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//nlocked_ioctl:提供设备执行相关控制命令的实现方法,它对应于应用程序的 fcntl 函数以
及 ioctl 函数。long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//open:设备驱动第一个被执行的函数,一般用于硬件的初始化。int (*open) (struct inode *, struct file *);//release:当 file 结构体被释放时,将会调用该函数。int (*release) (struct inode *, struct file *);
}

字符设备驱动程序框架

==============================================================================
//定义字符设备只取其一
//变量定义方式
static struct cdev chrdev;
//内核提供的动态分配方式
struct cdev *cdev_alloc(void);//上述分配对应的移除内核中移除某个字符设备
void cdev_del(struct cdev *p);
==============================================================================
/*由于cdev_add向系统注册字符设备之前
要先调用register_chrdev_region或alloc_chrdev_region向系统申请设备号
因此注册流程如下:*/
//申请设备号只取其一
//指定字符设备的起始设备号注册
int register_chrdev_region(dev_t from, unsigned count, const char *name);
//自动分配
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);//上述分配对应的移除
void unregister_chrdev_region(dev_t from, unsigned count)
==============================================================================
//初始化 cdev用于关联字符操作结构体(file_operations)与字符设备结构体(cdev)
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
==============================================================================
/*激活驱动*/
//向内核的 cdev_map 散列表添加一个新的字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
//从系统中删除 cdev
void cdev_del(struct cdev *p)
==============================================================================
/*为了让 udev 自动在 /dev/ 目录下创建文件,必须先创建一个“设备类”(Class),例如 /sys/class/my_driver_class/*/
//创建设备类
struct class *class_create(struct module *owner, const char *name);
/*struct module *owner: 指向“拥有”这个 Class 的内核模块。
**const char *name:要创建的“设备类”的名字*/
//销毁设备类
void class_destroy(struct class *cls);
==============================================================================
/*自动化 mknod内核会通知 udev 服务,udev 会自动在 /dev/my_device_name 创建设备文件,并链接到主次设备号。*/
//创建设备节点
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
/*class:指向这个设备应该注册到的 struct 类的指针;
**parent:指向此新设备的父结构设备(如果有)的指针;
**devt:要添加的 char 设备的开发;
**drvdata:要添加到设备进行回调的数据;
**fmt:输入设备名称。*/
//删除设备节点
void device_destroy(struct class *class, dev_t devt)
创建设备号,绑定设备和操作,内核散列添加,创建节点设备注册(自上到下构建策略)
加载顺序:1. alloc_chrdev_region -> 2. cdev_init -> 3. cdev_add -> 4. class_create -> 5. device_create。删除节点设备注销,内核散列移除,注销设备号(连根拔起顺序)
卸载”顺序是:1. device_destroy -> 2. class_destroy -> 3. cdev_del -> 4. unregister_chrdev_region。

字符设备驱动结构:
在这里插入图片描述

chrdev.c驱动部分代码

/*
此代码模拟的128字节的U盘
当向这个文件写入数据时 (echo "hello" > /dev/embed_char_dev)数据会被复制并存储到内核的一个 128 字节的缓冲区 (vbuf) 中。
当从这个文件读取数据时 (cat /dev/embed_char_dev)驱动会将 vbuf 缓冲区中的内容返回。
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME            "EmbedCharDev"
#define DEV_CNT                 (1)
#define BUFF_SIZE               128
//定义字符设备的设备号(主设备号,次设备号)
static dev_t devno;
//定义字符设备结构体chr_dev,用来注册驱动
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
//告诉内核用户相应操作时调用哪个函数,上面预先定义,下面进行函数实现
static struct file_operations  chr_dev_fops = 
{.owner = THIS_MODULE,.open = chr_dev_open,.release = chr_dev_release,.write = chr_dev_write,.read = chr_dev_read,
};static int chr_dev_open(struct inode *inode, struct file *filp)
{printk("\nopen\n");return 0;
}static int chr_dev_release(struct inode *inode, struct file *filp)
{printk("\nrelease\n");return 0;
}static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{//读写位置,偏移量unsigned long p = *ppos;int ret;//写入数量int tmp = count ;//每次写入位置不能超过BUFF_SIZEif(p > BUFF_SIZE)return 0;//写入超过BUFF_SIZE - p的部分进行截断if(tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;//从用户空间 (buf) 获取数据,并存入内核空间 (vbuf) 的方法ret = copy_from_user(vbuf, buf, tmp);*ppos += tmp;//返回实际写入的字节数return tmp;
}static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{unsigned long p = *ppos;int ret;int tmp = count ;//统计 chr_dev_read 这个函数总共被调用了多少次无用static int i = 0;i++;//超出读范围if(p >= BUFF_SIZE)return 0;if(tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;//将内核数据 (vbuf+p) 发送回用户空间 (buf) 的方法ret = copy_to_user(buf, vbuf+p, tmp);*ppos +=tmp;return tmp;
}//注册流程
static int __init chrdev_init(void)
{int ret = 0;printk("chrdev init\n");//第一步//采用动态分配的方式,获取设备编号,次设备号为0,//设备名称为EmbedCharDev,可通过命令cat  /proc/devices查看//DEV_CNT为1,当前只申请一个设备编号ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);if(ret < 0){printk("fail to alloc devno\n");goto alloc_err;}//第二步//关联字符设备结构体cdev与文件操作结构体file_operationscdev_init(&chr_dev, &chr_dev_fops);//第三步//添加设备至cdev_map散列表中ret = cdev_add(&chr_dev, devno, DEV_CNT);if(ret < 0){printk("fail to add cdev\n");goto add_err;}return 0;add_err://添加设备失败时,需要注销设备号unregister_chrdev_region(devno, DEV_CNT);
alloc_err:return ret;
}
module_init(chrdev_init);static void __exit chrdev_exit(void)
{printk("chrdev exit\n");unregister_chrdev_region(devno, DEV_CNT);cdev_del(&chr_dev);
}
module_exit(chrdev_exit);MODULE_LICENSE("GPL");

main.c测试程序部分代码

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{printf("EmbedCharDev test\n");//打开文件int fd = open("/dev/chrdev", O_RDWR);//写入数据write(fd, wbuf, strlen(wbuf));//写入完毕,关闭文件close(fd);//打开文件fd = open("/dev/chrdev", O_RDWR);//读取文件内容read(fd, rbuf, 128);//打印读取的内容printf("The content : %s", rbuf);//读取完毕,关闭文件close(fd);return 0;
}

make后进行cp chrdev.ko然后进行NFS挂载
linux板子上

#插入模块
sudo insmod chrdev.ko#虚拟文件用来实时向用户汇报系统状态。 process
cat /proc/devices
#243 EmbedCharDev 主设备号为 243#创建新设备节点
sudo mknod /dev/chrdev c 243 0#Makefile中会产生内核模块.ko以及一个c代码文件chrdev_test
#方案一:执行查看结果
root@lubancat:/mnt/host_projects# ./chrdev_test
EmbedCharDev test
The content : Hello Worldtest
debian@lubancat:/mnt/host_projects$ echo "EmbedCharDev test" > /dev/chrdev
#方案二:通过 echo 和 cat 命令,来测试设备驱动程序。
root@lubancat:~# cat /dev/chrdev
EmbedCharDev test#不需要内核模块进行卸载并删除相应设备文件
rmmod chrdev.ko
rm /dev/chrdev

说明:由于没有class_create 和device_create,所以需要insmod之后进行mknode手动注册。

一个驱动支持多个设备

区别于单个设备的部分
使用一套驱动代码,管理两个独立的 128 字节“U 盘”(DEV_CNT=2)
#define DEV_CNT                 (2)把一个设备所有相关的东西都打包在一起
struct chr_dev{struct cdev dev;//字符设备char vbuf[BUFF_SIZE];//私有数据缓冲区
};为两个设备分别创建了实例
static struct chr_dev vcdev1; // “设备 0” 的所有数据
static struct chr_dev vcdev2; // “设备 1” 的所有数据注册“设备 0” 注意 count 是 1
cdev_init(&vcdev1.dev, ...) / cdev_add(&vcdev1.dev, devno+0, 1)
注册“设备 1” 注意 count 是 1
cdev_init(&vcdev2.dev, ...) / cdev_add(&vcdev2.dev, devno+1, 1)查到主设备号后(驱动号)要进行两次节点注册
sudo mknod /dev/EmbedCharDev0 c 254 0
sudo mknod /dev/EmbedCharDev1 c 254 1echo和cat时候通过不同的文件名进行操作
/dev/EmbedCharDev0 或 /dev/EmbedCharDev1
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>#define DEV_NAME            "EmbedCharDev"
//向内核申请2 个连续的设备号。
#define DEV_CNT                 (2)
#define BUFF_SIZE               128
//定义字符设备的设备号,(major,minor)
static dev_t devno;//虚拟字符设备
struct chr_dev{struct cdev dev;char vbuf[BUFF_SIZE];
};
//字符设备1
static struct chr_dev vcdev1;
//字符设备2
static struct chr_dev vcdev2;static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);static struct file_operations chr_dev_fops = {.owner = THIS_MODULE,.open = chr_dev_open,.release = chr_dev_release,.write = chr_dev_write,.read = chr_dev_read,
};
static int chr_dev_open(struct inode *inode, struct file *filp)
{printk("open\n");filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);return 0;
}static int chr_dev_release(struct inode *inode, struct file *filp)
{printk("\nrelease\n");return 0;
}static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{unsigned long p = *ppos;int ret;//获取文件的私有数据struct chr_dev *dev = filp->private_data;char *vbuf = dev->vbuf;int tmp = count ;if(p > BUFF_SIZE)return 0;if(tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;ret = copy_from_user(vbuf, buf, tmp);*ppos += tmp;return tmp;
}static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{unsigned long p = *ppos;int ret;int tmp = count ;//获取文件的私有数据struct chr_dev *dev = filp->private_data;char *vbuf = dev->vbuf;if(p >= BUFF_SIZE)return 0;if(tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;ret = copy_to_user(buf, vbuf+p, tmp);*ppos +=tmp;return tmp;
}static int __init chrdev_init(void)
{//ret返回值只用来记录是否成功int ret;printk("chrdev init\n");//申请设备号ret = alloc_chrdev_region(&devno, 0, DEV_CNT,  DEV_NAME);if(ret < 0)goto alloc_err;//关联第一个设备:vdev1cdev_init(&vcdev1.dev, &chr_dev_fops);//1 cdev 对象只管理 1 个次设备号。ret = cdev_add(&vcdev1.dev, devno+0, 1);if(ret < 0){printk("fail to add vcdev1 ");goto add_err1;}//关联第二个设备:vdev2cdev_init(&vcdev2.dev, &chr_dev_fops);ret = cdev_add(&vcdev2.dev, devno+1, 1);if(ret < 0){printk("fail to add vcdev2 ");goto add_err2;}return 0;
add_err2:cdev_del(&(vcdev1.dev));
add_err1:unregister_chrdev_region(devno, DEV_CNT);
alloc_err:return ret;}module_init(chrdev_init);static void __exit chrdev_exit(void)
{printk("chrdev exit\n");unregister_chrdev_region(devno, DEV_CNT);cdev_del(&(vcdev1.dev));cdev_del(&(vcdev2.dev));
}
module_exit(chrdev_exit);MODULE_LICENSE("GPL");
http://www.dtcms.com/a/618511.html

相关文章:

  • 专业网站建设平台郑州房产网
  • 43 全选,单选
  • stm32f103c8t6寄存器点灯法
  • 【Linux】计算机如何管理软硬件
  • 利用DuckDB列表一句SQL输出乘法口诀表
  • 兴宁网站设计凡客的官网
  • 大模型-Vllm 启用多模态数据-3
  • 集团网站建设定制网站建设中企动力是怎么建设网站的
  • Shell脚本猜数字,使用判断提示用户比目标数字是大还是小
  • 【开题答辩全过程】以 基于安卓的校园二手物品为例,包含答辩的问题和答案
  • 内测检测vs第三方软件检测的差异解析
  • 【科研绘图系列】R语言绘制多组条形图图(barplot)
  • Linux-线程
  • 最专业网站建设公备案域名是什么意思
  • SQL 约束
  • 创立一个网站要多少钱上海公司做网站的
  • MoE算法深度解析:从理论架构到行业实践
  • 【2025CVPR 异常检测方向】DFM: Differentiable Feature Matching for Anomaly Detection
  • 北京西站地铁是几号线做网站贵
  • 数据库第六次作业
  • 西宁大型网站建设网站如何做电脑和手机软件
  • 【Linux】Shell脚本
  • qt显示类控件---QProgressBar
  • 复式记账的“借”与“贷”
  • 设备健康管理诊断报告生成:工业智能化的“决策引擎”与效率革命​
  • 淘客网站是怎么做的成都网站排名生客seo怎么样
  • vscode插件开发记录
  • 做淘宝代理哪个网站好淘宝网店页面设计
  • 【Linux系统编程】进程控制
  • day2江协科技-3 GPIO