前面我们字符设备驱动开发使用的是register_chrdev函数注册字符设备,unregister_chrdev函数注销字符设备,驱动模块加载成功后我们还需要手动使用mknod命令创建设备结点。register_chrdev 和 unregister_chrdev是老版本的的驱动使用的函数,新的字符设备驱动使用Linux内核推荐的新字符设备驱动API函数。
一、新字符设备驱动
1、分配和释放设备号
以前我们使用register_chrdev函数注册新字符设备的时候需要给定一个主设备号就可以了,但是这样带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为
200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪
费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请
几个,由 Linux 内核分配设备可以使用的设备号。在fs.h中,包含了Linux内核新的驱动注册函数。
①、没有指定你要申请的设备的设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:我们前面已经分析过,起始就是unsigned int 类型的设备号。
baseminor:次设备号。
count:要申请的数量。
name:申请设备的名字。
②、指定了你要申请的设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
from:给定的设备号。
count:要申请的数量。
name:申请设备的名字。
模板:
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); /* 获取分配号的次设备号 */
}
MKDEV:用来构建设备号。MAJOR:获取主设备号。MINOR:获取次设备号。
无论你如何创建的设备号,注销设备号的函数都是同一个:
void unregister_chrdev_region(dev_t from, unsigned count)
from:要注销的设备号。
count: 要注销的数量。
我们新建一个文件,将上一次的文件拷贝到本次的实验中,并清理一下工程:
使用vscode打开工程,并将led.c文件改为Newchrdev.c。驱动函数的入口和出口函数都是不变的:
我们创建一个驱动的结构体,里面存放一些这个驱动的信息,比如说设备号,主设备号,次设备号这些信息,并实例化一个设备。
根据我们的模板在入口函数创建一个设备号 :
使用goto语句跳转到错误处理,返回错误值。
在出口函数注销设备号:
2、 新字符设备注册方法
1、字符设备结构体
在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中
的定义:
这里有一些我们不了解的成员变量我们先不管,先看我们会的成员变量:
ops:字符设备文件操作集合。
dev:设备号。
在注册设备驱动前,我们就得先创建一个cdev类型的结构题,证明我们的驱动属于字符设备所以我们在我们前面的新字符驱动结构体中添加上cdev结构体。:
2、字符设备结构初始化
我们定义好字符设备结构体后,要对里面的成员变量进行初始化,使用到cdev_init函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
cdev:要初始化的字符结构体。
fops:字符设备文件操作集合
3、向Linux内核添加字符设备
刚刚我们已经将新字符设备初始化完成,下一步我们使用cdev_add函数将我们初始化完成的新字符设备添加到Linux系统中:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p:要添加设备的指针。
dev:设备的设备号。
count:要添加的设备数量。
4、字符设备的删除
卸载驱动时候,要先将Linux内核中注册的设备给删除掉,如果是字符设备的话,使用cdev_del函数:
void cdev_del(struct cdev *p)
5、验证
编译文件,将文件拷贝到nfs的根文件系统里面,使用serialCRT终端控制,当我们挂载驱动时候,Linux内核会自动给我们分配设备号。我们可以使用命令查看:
cat /proc/devices
可以看出Linux内核自动给我们分配了一个设备号是249。
二、自动创建节点
在前面的 Linux 驱动实验中,当我们使用 modprobe 加载驱动程序以后还需要使用命令
mknod手动创建设备节点。其实,在Linux中还可以自动创建节点,我们需要在驱动中实现自动创建设备节点的功能后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
1、mdev机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检
测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用
modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用
rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。
使用 busybox 构建根文件系统的时候, busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理,在/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
2、创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添
加自动创建设备节点相关代码。
首先要创建一个class的类,这里class其实是一个结构体,定义在include/linux/device.h文件里:
我们在驱动结构体里面,创建一个我们的驱动class类:
使用class_create函数,将我们的类进行创建:
原型:
struct class *class_create (struct module *owner, const char *name)
owner:一般为THIS_MODULE。
name:创建类的名字。
返回值:指向class结构体的指针。
这里使用Linux内核中判断指针是否错误的函数,如果指针返回错误,返回一个错误值,使用PRT_ERR函数获取指针错误的原因:
删除类:
void device_destroy(struct class *class)
class:要删除的类的指针。
3、 创建类的设备
创建好类后,还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用函数device_create在类下创建设备:
cls:要在哪个类下创建设备。
parent: 这个设备的父设备。类比C++的父类,如果没有就为NULL。
devt:设备号。
drvdata:这个设备可能使用到的一些数据。一般为NULL。
fmt:设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
返回值:创建好的设备。
同样,我们在驱动结构体中,添加类的设备。然后再入口函数创建类的设备:
同样,在出口函数中,我们也得将类的设备给注销掉,这个我们看到,类的设备创建时依赖于类的,所以类的设备注销我们需要在类注销之前注销。使用device_destroy函数:
void device_destroy(struct class *cls, dev_t devt);
cls:这个设备的类。
devt:设备号。
这样,我们的新字符设备就创建成功了。
三、新字符设备的测试
我们使用新创建的字符设备来测试前面写的LED驱动代码,所以得将LED驱动使用到的代码复制到我们的本次驱动中。
1、Newchrdev.c文件:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#define NEWCHRDEV_NAME "Newchrdev"
#define NEWCHRDEV_CNT 1
#define LED_ON 1
#define LED_OFF 0
/* LED物理地址 */
#define CCM_CCGR1_BASE 0x020C406c
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE 0x020E0068
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE 0x020E02F4
#define GPIO1_GDIR_BASE 0x0209c004
#define GPIO1_DR_BASE 0X0209C000
/* LED映射后的虚拟地址 */
static void __iomem *CCM_CCGR1 = NULL;
static void __iomem *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = NULL;
static void __iomem *IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = NULL;
static void __iomem *GPIO1_GDIR = NULL;
static void __iomem *GPIO1_DR = NULL;
/* LED等控制函数 */
static int Led_Switch(unsigned char Led_Status)
{
unsigned int val = 0;
if(Led_Status == LED_ON){ //开灯
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val,GPIO1_DR);
}else if(Led_Status == LED_OFF){ //关灯
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
}else{
printk("控制LED信号错误!\r\n");
return -1;
}
return 0;
}
struct Newchrdev_t{
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */
struct cdev dev; /* 字符设备结构体 */
struct class *class; /* 字符设备类 */
struct device *cls_device; /* 字符设备类的设备 */
};
static struct Newchrdev_t Newchrdev;
static int Newchrdev_open (struct inode *Inode, struct file *File)
{
return 0;
}
static int Newchrdev_release (struct inode *Inode, struct file *File)
{
return 0;
}
static ssize_t Newchrdev_write (struct file *File, const char __user *buf,
size_t Count, loff_t *Loff)
{
char writebuf[1];
int retvalue = 0;
unsigned char Led_Status = 0;
retvalue = copy_from_user(writebuf,buf,Count);
if(retvalue < 0){
printk("设备向用户拷贝数据失败!\r\n");
return -1;
}
/* 控制LED开关灯 */
Led_Status = writebuf[0];
Led_Switch(Led_Status);
return 0;
}
/* 字符设备文件操作集合 */
static const struct file_operations Newchrdev_fops = {
.owner = THIS_MODULE,
.open = Newchrdev_open,
.release = Newchrdev_release,
.write = Newchrdev_write,
};
/* 驱动入口 */
static int __init Newchrdev_init(void)
{
int error = 0;
unsigned int val = 0;
/* LED驱动地址映射 */
CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE,4);
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE,4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE,4);
GPIO1_DR = ioremap(GPIO1_DR_BASE,4);
/* LED初始化 */
/* 1、LED时钟初始化 */
val = readl(CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val,CCM_CCGR1);
/* 2、设置LED的引脚为GPIO1_IO03复用功能 */
writel(5,IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
/* 设置LED引脚GPIO1_IO03电气属性 */
writel(0x10b0,IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
/* 3、设置GPIO1_IO03为输出模式 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val,GPIO1_GDIR);
/* 4、设置GPIO1_IO03上电后的电平(0:灯亮) */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
/* 注册设备号 */
if(Newchrdev.major){ /* 指定了设备号 */
Newchrdev.major = MKDEV(Newchrdev.major,0);
error = register_chrdev_region(Newchrdev.devid, NEWCHRDEV_CNT,
NEWCHRDEV_NAME);
}else{ /* 没有指定设备号 */
error = alloc_chrdev_region(&Newchrdev.devid, Newchrdev.minor,
NEWCHRDEV_CNT, NEWCHRDEV_NAME);
Newchrdev.major = MAJOR(Newchrdev.major);
Newchrdev.minor = MINOR(Newchrdev.minor);
}
if(error < 0){ /* 创建设备号失败 */
goto flie_devid;
}
/* 注册字符设备 */
/* 1.初始化字符设备 */
Newchrdev.dev.owner = THIS_MODULE;
cdev_init(&Newchrdev.dev, &Newchrdev_fops);
/* 2.将字符设备添加到Linux系统 */
error = cdev_add(&Newchrdev.dev, Newchrdev.devid, NEWCHRDEV_CNT);
if(error < 0){
goto file_dev;
}
/* 创建设备节点 */
/* 1、创建类 */
Newchrdev.class = class_create(THIS_MODULE,NEWCHRDEV_NAME);
if (IS_ERR(Newchrdev.class)){
error = PTR_ERR(Newchrdev.class);
goto file_class;
}
/* 2、创建类的设备 */
Newchrdev.cls_device = device_create(Newchrdev.class,NULL,
Newchrdev.devid,NULL,
NEWCHRDEV_NAME);
if (IS_ERR(Newchrdev.cls_device)){
error = PTR_ERR(Newchrdev.cls_device);
goto file_cls_dev;
}
return 0;
file_cls_dev:
class_destroy(Newchrdev.class);
file_class:
cdev_del(&Newchrdev.dev);
file_dev:
unregister_chrdev_region(Newchrdev.devid, NEWCHRDEV_CNT);
flie_devid:
return error;
}
/* 驱动出口 */
static void __exit Newchrdev_exit(void)
{
unsigned int val = 0;
/* 关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
/* 删除类的设备 */
device_destroy(Newchrdev.class,Newchrdev.devid);
/* 删除类 */
class_destroy(Newchrdev.class);
/* 删除字符设备 */
cdev_del(&Newchrdev.dev);
/* 注销设备号 */
unregister_chrdev_region(Newchrdev.devid, NEWCHRDEV_CNT);
printk("Newchrdev_exit\r\n");
}
module_init(Newchrdev_init);
module_exit(Newchrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZhangXueGuo");
2、测试应用程序ledAPP.c文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
/*
* main主程序
* argc:argv数字个数,一般指传递给函数的参数数量
* argv:具体的参数内容,一般都是字符串格式
* return:0表示成功
*
*/
int main(int argc, char *argv[])
{
int fd,retvalue;
char *FileName;
char writebuf[1];
/* 判断使用命令参数是否正确 */
if(argc != 3){
printf("命令使用错误!\r\n");
return -1;
}
/* 打开程序 */
FileName = argv[1];
fd = open(FileName,O_RDWR);
if(fd < 0){
printf("应用程序打开设备文件失败!\r\n");
return -1;
}
writebuf[0] = atoi(argv[2]);
/* 向设备写LED控制信号 */
retvalue = write(fd,writebuf,sizeof(writebuf));
if(retvalue < 0){
printf("向设备写LED控制数据失败!\r\n");
return -1;
close(fd);
}
/* 关闭文件 */
close(fd);
return 0;
}
3、编译下载验证
将这两个文件编译,应拷贝到nfs的根文件系统下。使用终端控制,加载驱动模块。
当我们加载驱动模块时,自动创建设备号,并且自动在/dev目录下生成节点。当我们使用命令:./ledAPP /dev/Newchrdev 1时led灯亮,使用命令:./ledAPP /dev/Newchrdev 0时,led灯灭。