一 、简介
字符设备与杂项设备的区别:
(1)杂项设备的主设备号是固定为10,学习字符类设备就需要自己或者系统来分配
(2)杂项设备可以自动生成设备节点,字符设备需要自己生成设备节点
二、申请设备号
注册字符类设备号的两个方法:
第一种:静态分配一个设备号
第二中:
2.1 静态分配设备号
静态分配一个设备号需要知道哪些设备号被用了,哪些设备号没有被用
register_chrdev_region(dev_t, unsigned, const char *);
头文件:
函数原型:
函数功能:
函数参数:
返回值:
头文件:#incldue<linux/fs.h>函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name)函数功能:静态分配一段连续的字符设备号,供字符设备驱动程序使用
函数参数:
@param1 from:要分配的起始设备号(包含主次设备号)(类型为dev_t 数据类型)
@param2 连续分配的设备号数量
@param3 设备名称(出现在
/proc/devices中)返回值:成功返回0,失败返回错误码
头文件:#incldue<linux/types.h>宏定义:
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
作用:dev_t 是用来保存设备号的,是一个32位数;高12为用来保存主设备号,低20位用来保存次设备号
LInux 提供了几个宏定义来操作设备号头文件:#incldue<linux/kdev_t.h>
cpp#define MINORBITS 20 //次设备号位数,一共20位 #define MINORMASK ((1U << MINORBITS) - 1) //次设备号掩码 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))//在dev_t中获取主设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //在dev_t中获取次设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将获取的主设备号和次设备号组成一个dev_t类型,第一个参数是住设备号,第二个参数是次设备号
2.2 动态分配设备号
头文件:#incldue<linux/fs.h>
函数原型:int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *)
函数功能:动态分配设备号
函数参数:
@param1 dev_t * 保存生成的起始设备号
@param2 unsigned 请求的第一个次设备号,通常为0
@param3 unsigned 连续申请的设备号个数
@param4 const char *
name:设备名称(出现在/proc/devices中)返回值:成功返回0,失败返回错误码
使用动态分配会优先使用255~234之间的设备号
2.3 注销设备号
头文件:#incldue<linux/fs.h>
函数原型:void unregister_chrdev_region(dev_t, unsigned);
函数功能:释放注册过的设备号
函数参数:
@param1 dev_t * 要释放的起始设备号
@param2 unsigned 要释放的连续设备号数量
2.4 代码部分
建议使用动态分配设备号;可通过终端命令cat /proc/devices查看设备号是否存在
cpp
#include <linux/init.h>
#include <linux/module.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#define DEV_NUM 1 //定义分配连续申请的设备号数量
#define MION_DEV_NUM 0 //定义请求的第一个次设备号
#define DEV_SNAME "schrdev" //定义静态分配的设备号名称
#define DEV_ANAME "achrdev" //定义动态分配的设备号名称
static int major_num,minor_num;//定义两个变量用于接受主次设备号
dev_t dev_num;//定义dev_t变量用于接收设备设备号
module_param(major_num,int,0644);
module_param(minor_num,int,0644);
static int chrdev_init(void)
{
int ret;
if(major_num)//静态分配设备号
{
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
dev_num= MKDEV(major_num,minor_num);//合成设备号
ret=register_chrdev_region(dev_num,DEV_NUM,DEV_SNAME);
if(ret!=0)
{
printk("register_chrdev_region failed!\n");
return -1;
}
printk("register_chrdev_region succeed!\n");
}
else
{
ret=alloc_chrdev_region(&dev_num,MION_DEV_NUM,DEV_NUM,DEV_ANAME);
if(ret!=0)
{
printk("alloc_chrdev_region succeed!\n");
return -1;
}
major_num=MAJOR(dev_num);//取出主设备号
minor_num=MINOR(dev_num);//取出此设备号
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
}
return 0;
}
static void chrdev_exit(void)
{
unregister_chrdev_region(dev_num,DEV_NUM);
printk("param exit rmmod succeed!\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
三、注册设备到内核
步骤一:定义一个cdev结构体
cdev结构体
头文件:#include <linux/cdev.h>
cppstruct cdev { struct kobject kobj; // 1. 内核对象基础结构 struct module *owner; // 2. 所属模块 const struct file_operations *ops; // 3. 设备操作函数集 struct list_head list; // 4. 链表节点 dev_t dev; // 5. 设备号 unsigned int count; // 6. 设备数量 };
步骤二:初始化一个字符设备结构体(cdev)
头文件:#include <linux/cdev.h>
函数原型:
void cdev_init(struct cdev *, const struct file_operations *);函数功能:初始化一个字符设备结构体(cdev),将其与指定的文件操作函数集(fops)关联,为后续添加到内核系统做准备
函数参数:
@param1:指向要初始化的字符设备结构体(cdev)的指针
@param2:指向设备对应的文件操作函数集(file_operations)的指针
步骤三:将初始化好的字符设备注册到内核
头文件:#include <linux/cdev.h>
函数原型:
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count)函数功能:将初始化好的字符设备注册到内核,使其生效
函数参数:
@param1
cdev已初始化的字符设备结构体@param2
dev:起始设备号@param3
count:连续设备号的数量返回值:成功返回0,失败返回错误码(负数)
注销字符设备
头文件:#include <linux/cdev.h>
函数原型:
void cdev_del(struct cdev *cdev)函数功能:从内核中移除字符设备
函数参数:
@param1
``cdev:要删除的字符设备结构体注意:该函数在代码中的执行顺序
字符设备注册完以后会自动生成设备节点,但是字符类设备经过设备号申请、注册设备到内核、这来两步是没有办法生成设备节点的,需要通过mknod命令手动添加设备节点格式:
mknod 名称 类型 主设备号 次设备号
eg:mknod /dev/test c 247 0 将主设备号为247,次设备号为0的字符类设备生成/dev/test节点
代码
驱动层:
cpp
#include <linux/init.h>
#include <linux/module.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include <linux/cdev.h>
#define DEV_NUM 1 //定义分配连续申请的设备号数量
#define MION_DEV_NUM 0 //定义请求的第一个次设备号
#define DEV_SNAME "schrdev" //定义静态分配的设备号名称
#define DEV_ANAME "achrdev" //定义动态分配的设备号名称
static int major_num,minor_num;//定义两个变量用于接受主次设备号
struct cdev cdev; //定义一个cdev类型结构体
static int cdev_open(struct inode *inode, struct file *file)
{
printk("open this cdev succeed\n");
return 0;
}
struct file_operations cdev_fops=
{
.owner =THIS_MODULE,
.open=cdev_open
}; //定义文件操作集结构体
module_param(major_num,int,0644);
module_param(minor_num,int,0644);
static int chrdev_init(void)
{
int ret1,ret2;
dev_t dev_num;//定义dev_t变量用于接收设备设备号
/*申请设备号*/
if(major_num)//静态分配设备号
{
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
dev_num= MKDEV(major_num,minor_num);//合成设备号
ret1=register_chrdev_region(dev_num,DEV_NUM,DEV_SNAME);
if(ret1!=0)
{
printk("register_chrdev_region failed!\n");
return -1;
}
printk("register_chrdev_region succeed!\n");
}
else
{
ret1=alloc_chrdev_region(&dev_num,MION_DEV_NUM,DEV_NUM,DEV_ANAME);
if(ret1!=0)
{
printk("alloc_chrdev_region succeed!\n");
return -1;
}
major_num=MAJOR(dev_num);//取出主设备号
minor_num=MINOR(dev_num);//取出此设备号
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
}
/*将设备号注册到内核*/
cdev.owner=THIS_MODULE;
cdev_init(&cdev,&cdev_fops);//初始化一个字符设备结构体(cdev),将其与指定的文件操作函数集(fops)关联
ret2=cdev_add(&cdev,dev_num,DEV_NUM);//注册设备到内核
if(ret2!=0)
{
printk("cdev_add failed\n");
return -2;
}
return 0;
}
static void chrdev_exit(void)
{//注意注销顺序
unregister_chrdev_region(MKDEV(major_num,minor_num),DEV_NUM);//注销设备号
cdev_del(&cdev);//注销设备到内核
printk("param exit rmmod succeed!\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
手动生成设备节点

应用层:
cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
int fd;
fd = open("/dev/my_cdev", O_RDWR); //打开驱动对应的设备文件
if(fd < 0)
{
printf("my_cdev device open failed\n");
return -1;
}
printf("app layer device open success\n");
close(fd);
return 0;
}
四、自动创建设备节点
1.问题:怎么自动创建一个设备节点?
在嵌入式Linux中使用mdev来实现设备节点文件的自动创建和删除
2.什么是mdev?
mdev是udev的简化版,是busybox中所带的程序,最适合用在嵌入式系统
3.什么是udev?
udev是一种工具,它能够根据系统中的银行间设备状态动态更新设备文件,包括文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备。udev一般用在PC上的linux中,相对mdev来说要复杂一些
4.怎么自动创建设备节点?
自动创建设备节点分为两个步骤:
步骤一:使用class_create函数出创建一个类
步骤二:使用device_create函数在创建的类下面创建一个设备
4.1创建和删除类函数
创建类:
头文件:#include<linux/device.h>
cppextern struct class * __must_check __class_create(struct module *owner, const char *name, struct lock_class_key *key); #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ })功能:创建的类
参数:
@param1 owner 一般为THIS_MODULE
@param2 name 创建的类的名字(在/sys/class下生成对应名字的类)
返回值:指向结构体class的指针
头文件:#include<linux/device.h>
cppvoid class_destroy(struct class *cls);函数参数:cls 要删除的类
函数功能:删除创建的类
在创建的类下面创建设备节点
头文件:#include<linux/device.h>
cppstruct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);函数功能:在类下面创建设备节点
函数参数:
@param1 cls 设备要创建哪个类下面
@param2 parent 父设备,一般为NULL,也就是没有父设备
@param3 devt 设备号
@param4 drvdata 是设备可能会使用的一些数据,一般为NULL
@param5 fmt 设备节点名称,若是fmt=xxx,则会生成/dev/xxx这个设备文件
返回值:创建好的设备
头文件:#include<linux/device.h>
cppvoid device_destroy(struct class *cls, dev_t devt);函数功能:删除设备节点
函数参数:
@param1 cls 设备节点所在的类
@param2 devt 设备号
4.2 代码
驱动代码
cpp
#include <linux/init.h>
#include <linux/module.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEV_NUM 1 //定义分配连续申请的设备号数量
#define MION_DEV_NUM 0 //定义请求的第一个次设备号
#define DEV_SNAME "schrdev" //定义静态分配的设备号名称
#define DEV_ANAME "achrdev" //定义动态分配的设备号名称
#define DEV_CLASS_NAME "chrdev_class" //定义创建的类的名字
#define DEV_NODE_NAME "my_cdev"
static int major_num,minor_num;//定义两个变量用于接受主次设备号
struct cdev cdev; //定义一个cdev类型结构体
struct class*class; //定义一个class类型的结构体指针
struct device*device; // 定义一个device类型的结构体指针
static int cdev_open(struct inode *inode, struct file *file)
{
printk("open this cdev succeed\n");
return 0;
}
struct file_operations cdev_fops=
{
.owner=THIS_MODULE,
.open=cdev_open
}; //定义文件操作集结构体
module_param(major_num,int,0644);
module_param(minor_num,int,0644);
static int chrdev_init(void)
{
int ret1,ret2;
dev_t dev_num;//定义dev_t变量用于接收设备设备号
/*申请设备号*/
if(major_num)//静态分配设备号
{
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
dev_num= MKDEV(major_num,minor_num);//合成设备号
ret1=register_chrdev_region(dev_num,DEV_NUM,DEV_SNAME);
if(ret1!=0)
{
printk("register_chrdev_region failed!\n");
return -1;
}
printk("register_chrdev_region succeed!\n");
}
else//动态分配设备号
{
ret1=alloc_chrdev_region(&dev_num,MION_DEV_NUM,DEV_NUM,DEV_ANAME);
if(ret1!=0)
{
printk("alloc_chrdev_region succeed!\n");
return -1;
}
major_num=MAJOR(dev_num);//取出主设备号
minor_num=MINOR(dev_num);//取出次设备号
printk("major_num=%d\n",major_num);
printk("minor_num=%d\n",minor_num);
}
/*将设备号注册到内核*/
cdev.owner=THIS_MODULE;
cdev_init(&cdev,&cdev_fops);//初始化一个字符设备结构体(cdev),将其与指定的文件操作函数集(fops)关联
ret2=cdev_add(&cdev,dev_num,DEV_NUM);//注册设备到内核
if(ret2!=0)
{
printk("cdev_add failed\n");
return -2;
}
/*自动生成设备节点*/
class=class_create(THIS_MODULE,DEV_CLASS_NAME); //创建类
device=device_create(class,NULL,dev_num,NULL,DEV_NODE_NAME);//生成设备节点
return 0;
}
static void chrdev_exit(void)
{
unregister_chrdev_region(MKDEV(major_num,minor_num),DEV_NUM);//注销设备号
cdev_del(&cdev);//注销设备到内核
device_destroy(class,MKDEV(major_num,minor_num));//注销设备节点
class_destroy(class);//注销创建的类
printk("param exit rmmod succeed!\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
应用层
cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
int fd;
fd = open("/dev/my_cdev", O_RDWR); //打开驱动对应的设备文件
if(fd < 0)
{
printf("my_cdev device open failed\n");
return -1;
}
printf("app layer device open success\n");
close(fd);
return 0;
}
4.3 结果
通过 cat /proc/devices查看申请的设备号以设备号名称

通过ls /sys/class查看生成的类

通过ls /dev/命令查看生成的设备节点
