正点原子I.MX6U-MINI驱动篇3新字符设备驱动实验newchrled,自动创建设备节点

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子I.MX6U-MINI驱动篇3新字符设备驱动实验newchrled,自动创建设备节点相关的知识,希望对你有一定的参考价值。

经过前两章实验的实战操作,我们已经掌握了 Linux 字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点。register_chrdevunregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。

一、设备号的作用

设备号的作用很简单,就是给每一个设备编一个号码,是为了能方便的通过号码去找到设备,而不是通过字符去查找设备。有的设备属于同一种类,如led,fb等,所以就有了主设备号与次设备号之分,主设备号用来表示一个特定的设备驱动,次设备号用来管理下面的设备如现在有两个led设备,led1设备和led2设备,他们俩属于同一种设备,我们就可以只写一个驱动,led设备通过次设备的形式挂在驱动下面,这样我们就可以通过同一个驱动以区分次设备的形式来控制两个led设备

二、主设备号与次设备号

次设备号使用的是同一个驱动,其意思是当应用通过open read write等设备操作函数file_operations来操作驱动时,其实调用的是字符设备在注册时与主设备号绑定的设备操作结构体。也就是使用register_chrdev或者cdev_init/cdev_add注册的字符设备。

2.1 设备号的数据结构

设备号数据类型:dev_t原型是一个32位无符号整形类型的值(unsigned int ),其中高12位表示主设备号,低20位表示次设备号,如下图。

备注:因为主设备号总共占12bit,所以范围为0 ~ 2^12,即:0~4095
在内核源码中主次设备号获取可以使用如下三个宏来获取以及合成:

设备号、主设备号、次设备号操作宏

合成设备号:MKDEV(ma,mi) /* 已经知道主设备号ma和次设备号mi,使用该宏可以合成为完成的设备号 */
主设备号:MAJOR(xxx.devid)
次设备号:MINOR(xxx.devid)

2.2 设备号的查看方法

查看设备的设备号有很多的方法

1、ls

ls /dev -l

2、/proc/devices

cat /proc/devices

这个方法只能看到主设备号

三、分配和释放设备号

使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会
带来两个问题:

  • ①、需要我们事先确定好哪些主设备号没有使用。
  • ②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。

解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。

如果没有指定设备号的话就使用如下函数来申请设备号:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 参数 from 是要申请的起始设备号,也就是给定的设备号;
  • 参数 count 是要申请的数量,一般都是一个;
  • 参数 name 是设备名字。

注销字符设备之后要释放掉设备号,不管是通过alloc_chrdev_region函数还是register_chrdev_region函数申请的设备号,统一使用如下释放函数:

void unregister_chrdev_region(dev_t from, unsigned count)

新字符设备驱动下,设备号分配示例代码如下:

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); /* 获取分配号的次设备号 */

  • 第 1~3 行,定义了主/次设备号变量major和 minor,以及设备号变量devid。
  • 第 5 行,判断主设备号major是否有效,在Linux驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择 0,这算个Linux 驱动开发中约定俗成的一种规定了
  • 第 6 行,如果major有效的话就使用MKDEV来构建设备号,次设备号选择0
  • 第 7 行,使用register_chrdev_region函数来注册设备号。
  • 第 9~11 行,如果major无效,那就表示没有给定设备号。此时就要使用alloc_chrdev_region函数来申请设备号。设备号申请成功以后使用MAJOR和MINOR来提取出主设备号和次设备号,当然了,第 10 和 11 行提取主设备号和次设备号的代码可以不要。如果要注销设备号的话,使用如下代码即可:
unregister_chrdev_region(devid, 1); /* 注销设备号 */

四、新的字符设备注册方法

4.1 字符设备结构体

在Linux中使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下:

在cdev中有两个重要的成员变量:opsdev,这两个就是字符设备文件操作函数集合file_operations以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个
变量就表示一个字符设备,如下所示:

struct cdev test_cdev;

4.2 cdev_init函数

定义好cdev变量以后就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

参数cdev就是要初始化的cdev结构体变量,参数 fops 就是字符设备文件操作函数集合。
使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

struct cdev testcdev;	/* led设备 */

/* 设备操作函数 */
static struct file_operations newchrled_fops = 
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
;

/* 初始化cdev */
testcdev.owner = THIS_MODULE;

cdev_init(&testcdev, &newchrled_fops);

4.3 cdev_add函数

cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
数 count 是要添加的设备数量。内容如下所示:

struct cdev testcdev;	/* led设备 */

/* 设备操作函数 */
static struct file_operations newchrled_fops = 
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
;

/* 初始化cdev */
testcdev.owner = THIS_MODULE;

cdev_init(&testcdev, &newchrled_fops);

/* 添加一个cdev */
cdev_add(&testcdev, devid, 1);

示例代码就是新的注册字符设备代码段,Linux内核中大量的字符设备驱动都是采用这种方法向Linux 内核添加字符设备。如果在加上分配设备号的程序,那么就它们一起实现的就是函数 register_chrdev的功能。

4.4 cdev_del函数

卸载驱动的时候一定要使用cdev_del函数从 Linux 内核中删除相应的字符设备,

cdev_del函数原型如下:

void cdev_del(struct cdev *p)

参数p就是要删除的字符设备。如果要删除字符设备,参考如下代码:
示例代码 42.1.2.3 cdev_del 函数使用示例

struct cdev testcdev;	/* led设备 */

/* 设备操作函数 */
static struct file_operations newchrled_fops = 
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
;

/* 初始化cdev */
testcdev.owner = THIS_MODULE;

cdev_init(&testcdev, &newchrled_fops);

/* 添加一个cdev */
cdev_add(&testcdev, devid, 1);

/* 删除 cdev */
cdev_del(&testcdev);  

cdev_delunregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。

4.5 自动创建设备节点

在第一章和第二章的驱动实验中,当我们使用modprobe加载驱动程序以后还需要使用命令mknod手动创建设备节点。如何实现自动创建设备节点呢?在驱动中实现自动创建设备节点的功能以后,使用 modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。

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

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类,class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下:

根据上述代码,将宏 class_create 展开以后内容如下:

struct class *class_create (struct module *owner, const char *name)
  • 参数owner一般为THIS_MODULE。
  • 参数name是类名字。
  • 返回值是个指向结构体 class 的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为class_destroy,函数原型如下:

void class_destroy(struct class *cls);

参数cls就是要删除的类。

创建设备

创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用device_create函数在类下面创建设备,device_create函数原型如下:

struct device *device_create(struct class *class,
							struct device *parent,
							dev_t devt,
							void *drvdata,
							const char *fmt, ...)
  • device_create是个可变参数函数,
  • 参数class就是设备要创建哪个类下面;
  • 参数parent是父设备,一般为NULL,也就是没有父设备;
  • 参数devt是设备号;
  • 参数drvdata是设备可能会使用的一些数据,一般为NULL;
  • 参数fmt是设备名字,如果设置fmt=xxx的话,就会生成/dev/xxx这个设备文件。
  • 返回值就是创建好的设备。

同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

void device_destroy(struct class *class, dev_t devt)

同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为device_destroy,函数原型如下:

void device_destroy(struct class *class, dev_t devt)
  • 参数 classs 是要删除的设备所处的类,
  • 参数 devt 是要删除的设备号。

参考示例

在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:


struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */

/* 驱动入口函数 */
static int __init xxx_init(void)

	/* 创建类 */
	class = class_create(THIS_MODULE, "xxx");
	/* 创建设备 */
	device = device_create(class, NULL, devid, NULL, "xxx");
	return 0;
 

 /* 驱动出口函数 */
 static void __exit led_exit(void)
 
	/* 删除设备 */
	device_destroy(newchrled.class, newchrled.devid);
	/* 删除类 */
	class_destroy(newchrled.class);
 

 module_init(led_init);
 module_exit(led_exit);

设置文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:

dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */

这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示:

/* 设备结构体 */
struct test_dev
	dev_t devid; /* 设备号 */
	struct cdev cdev; /* cdev */
	struct class *class; /* 类 */
	struct device *device; /* 设备 */
	int major; /* 主设备号 */
	int minor; /* 次设备号 */
;

struct test_dev testdev;

/* open 函数 */
static int test_open(struct inode *inode, struct file *filp)

	filp->private_data = &testdev; /* 设置私有数据 */
	return 0;

open函数里面设置好私有数据以后,在write、read、close等函数中直接读取private_data即可得到设备结构体。

五、硬件原理图分析

驱动开发第二章已经分析过了

六、实验程序编写

6.1 LED 灯驱动程序编写

newchrled.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>


#define NEWCHRLED_CNT			1		  	/* 设备号个数 */
#define NEWCHRLED_NAME			"newchrled"	/* 名字 */
#define LEDOFF 					0			/* 关灯 */
#define LEDON 					1			/* 开灯 */
 
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* newchrled设备结构体 */
struct newchrled_dev
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
;

struct newchrled_dev newchrled;	/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)

	u32 val = 0;
	if(sta == LEDON) 
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	else if(sta == LEDOFF) 
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
		


/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)

	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;


/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)

	return 0;


/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)

	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) 
		printk("kernel write failed!\\r\\n");
		return -EFAULT;
	

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) 	
		led_switch(LEDON);		/* 打开LED灯 */
	 else if(ledstat == LEDOFF) 
		led_switch(LEDOFF);	/* 关闭LED灯 */
	
	return 0;


/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)

	return 0;


/* 设备操作函数 */
static struct file_operations newchrled_fops = 
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
;

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)

	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) 		/*  定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	 else 						/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */
		newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */
	
	printk("newcheled major=%d,minor=%d\\r\\n",newchrled.major, newchrled.minor);	
	
	/* 2、初始化cdev */
	newchrled.cdev.owner =以上是关于正点原子I.MX6U-MINI驱动篇3新字符设备驱动实验newchrled,自动创建设备节点的主要内容,如果未能解决你的问题,请参考以下文章

正点原子I.MX6U-MINI驱动篇1字符设备驱动开发-Hello驱动(不涉及硬件操作)

正点原子I.MX6U-MINI驱动篇5 LED驱动程序框架:从分层到分离(非常重要)

正点原子I.MX6U-MINI驱动篇2嵌入式 Linux驱动开发之点灯大法

正点原子I.MX6U-MINI应用篇7输入设备(鼠标键盘触摸屏按钮)的应用编程和tslib库

正点原子I.MX6U-MINI移植篇rootfs移植过程详解

正点原子I.MX6U-MINI移植篇rootfs移植过程详解