Linux驱动开发笔记4:新字符设备驱动

字符设备驱动开发重点是使用 register_chrdev 函数注册字符设备,当不再使用设备的时候就使用
unregister_chrdev 函数注销字符设备,驱动模块加载成功以后还需要手动使用 mknod 命令创建设备节点。
新字符设备驱动:动态分配和释放设备号、自动创建设备节点、设置文件的私有数据。

动态分配和释放设备号

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

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)

注销设备号

unregister_chrdev_region(devid, 1);

新字符设备注册方法

字符设备结构

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

struct cdev {
    struct kobject kobj;               // 设备对象,用于 sysfs 目录
    struct module *owner;              // 所属模块
    const struct file_operations *ops; // 操作函数集合(open/read/write等)
    struct list_head list;             // 链表结构(内部使用)
    dev_t dev;                         // 设备号(主+次)
    unsigned int count;                // 管理的设备个数
};

cdev_init 函数

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

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

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 是要添加的设备数量。

cdev_del 函数

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

void cdev_del(struct cdev *p)

自动创建设备节点

在前面的 Linux 驱动实验中,当我们使用 modprobe 加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。

mdev 机制

udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理,在/etc/init.d/rcS 文件中如下语句置热插拔事件由 mdev 来管理。

echo /sbin/mdev > /proc/sys/kernel/hotplug

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。类创建函数class_create 内容如下

struct class *class_create (struct module *owner, const char *name)

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

void class_destroy(struct class *cls);

创建和删除设备

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

struct device *device_create(	
	struct class *class, 
	struct device *parent,
	dev_t devt, 
	void *drvdata, 
	const char *fmt, ...
)

参数 class: 设备要创建在哪个类下面
参数 parent: 父设备,一般为 NULL,也就是没有父设备
参数 devt : 设备号
参数 drvdata : 设备可能会使用的一些数据,一般为 NULL
参数 fmt : 设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
返回值: 创建好的设备。
卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

void device_destroy(struct class *class, dev_t devt)

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

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

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

static int __init newled_init(void)
{
    class   =   class_create(THIS_MODULE, "xxx");               // 创建类
    device  =   device_create(class, NULL, devid, NULL, "xxx"); // 创建设备
    return 0;
}
static void __exit newled_exit(void)
{
    class_destroy(newled.class, newled.devid);
    device_destroy(newled.class);
}
module_init(newled_init);
module_exit(newled_exit);

设置文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,编写驱动 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;

static int test_open(struct inode *inode, struct file *file)
{
	file->private_data = &testdev;	// 设置私有数据
	return 0;
}

NEW LED驱动程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NEWLED_CNT  1			// 设备个数
#define NEWLED_NAME "newled"	// 设备名称
#define LED_ON  1
#define LED_OFF 0

// 寄存器物理地址
#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;

/*
struct cdev {
    struct kobject kobj;               // 设备对象,用于 sysfs 目录
    struct module *owner;              // 所属模块
    const struct file_operations *ops; // 操作函数集合(open/read/write等)
    struct list_head list;             // 链表结构(内部使用)
    dev_t dev;                         // 设备号(主+次)
    unsigned int count;                // 管理的设备个数
};
*/
/* newled 设备驱动结构体
*	cdev 	:	字符设备对象,用于将file_operations 与 设备号绑定, 实现核心驱动功能
*	class	:	类对象, 表示一个设备类,用于自动创建设备节点,归类设备
*	device	:	表示一个具体的设备实例,用于注册到内核并创建 /dev/xxx 节点
*	总结	:   cdev 实现字符设备功能,class 定义设备分类,device 创建 /dev/xxx 节点,
*				它们共同完成驱动与用户空间的连接
*/
struct newled_dev{
	dev_t devid;
	struct cdev cdev;
	struct class *class;
	struct device *device;
	int major;
	int minor;
};
struct newled_dev newled;

void led_switch(u8 state)
{
	u32 val = 0;
	if(state == LED_ON){
		val = readl(GPIO1_DR);
		val = val & ~(1 << 3);
		writel(val, GPIO1_DR);
	}else{
		val = readl(GPIO1_DR);
		val = val & ~(1 << 3);
		val = val |  (1 << 3);
		writel(val, GPIO1_DR);
	}
}
static int newled_open(struct inode *inode, struct file *file)
{	
	file->private_data = &newled; //设置私有数据
	return 0;
}
static int newled_release(struct inode *inode, struct file *file)
{
	return 0;
}

static ssize_t newled_read(struct file *file, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}
static ssize_t newled_write(struct file *file, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	unsigned char databuf[1];
	unsigned char state = 0;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0){
		printk("kernel write data failed!\r\n");
		return -EFAULT;
	}
	state = databuf[0];
	if(state == LED_ON){
		led_switch(LED_ON);
	}else if(state == LED_OFF){
		led_switch(LED_OFF);
	}
	return 0;
}

static struct file_operations newled_fops = {
	.owner		=	THIS_MODULE,
	.open		=	newled_open,
	.release	=	newled_release,
	.read		=	newled_read,
	.write		=	newled_write,
};

/* module_init 需要完成的事情
* 1. 地址映射
*    配置GPIO
* 2. 动态申请设备号
* 3. 注册字符设备
* 4. 创建设备节点
*/
static int __init newled_init(void)
{
	u32 val = 0;
	/*----------------物理地址映射为虚拟地址---------------*/
	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);

	/*------------------配置 GPIO1_IO03-----------------*/
	// 1. 使能时钟
	val = readl(IMX6U_CCM_CCGR1);
	val = val & ~(3 << 26); // 把bit27,bit26置0,其它位不变
	val = val |  (3 << 26); // 把bit27,bit26置1,其它位不变
	writel(val, IMX6U_CCM_CCGR1);
	// 2. 配置IO复用, 设置为GPIO功能,也就是ATL5(32'b0101=32'd5)
	writel(5, SW_MUX_GPIO1_IO03);
	// 3. 配置电气属性
	writel(0X10B0, SW_PAD_GPIO1_IO03);
	// 4. 设置GPIO1_IO03为输出功能
	val = readl(GPIO1_GDIR);
	val = val & ~(1 << 3);
	val = val |  (1 << 3);
	writel(val, GPIO1_GDIR);
	// 5.设置GPIO1_IO03输出数据为1(LDE熄灭)
	val = readl(GPIO1_DR);
	val = val & ~(1 << 3);
	val = val |  (1 << 3);
	writel(val, GPIO1_DR);

	/*------------------注册字符设备驱动-----------------*/
	// 1. 创建设备号
	if(newled.major){
		// 定义了设备号
		newled.devid = MKDEV(newled.major, 0);
		register_chrdev_region(newled.devid, NEWLED_CNT, NEWLED_NAME);
	}else{
		// 没有定义设备号
		alloc_chrdev_region(&newled.devid, 0, NEWLED_CNT, NEWLED_NAME); // 申请设备号
		newled.major = MAJOR(newled.devid); // 获取主设备号
		newled.minor = MINOR(newled.devid); // 获取次设备号
	}
	printk("newled major = %d, minor = %d\r\n", newled.major, newled.minor);
	// 2. 初始化 cdev
	newled.cdev.owner = THIS_MODULE;
	cdev_init(&newled.cdev, &newled_fops);
	// 3.添加一个 cdev
	cdev_add(&newled.cdev, newled.devid, NEWLED_CNT);
	// 4.创建类 
	newled.class = class_create(THIS_MODULE, NEWLED_NAME);
	if(IS_ERR(newled.class)){
		return PTR_ERR(newled.class);
	}
	// 5.创建设备
	newled.device = device_create(newled.class, NULL, newled.devid, NULL, NEWLED_NAME);
	if(IS_ERR(newled.device)){
		return PTR_ERR(newled.device);
	}
	printk("newled_init();\r\n");
	return 0;
}
/* module_init 需要完成的事情
* 1. 取消地址映射
* 2. 动态申请设备号
* 3. 注销字符设备
* 4. 创建设备节点
*/
static void __exit newled_exit(void)
{
	// 1. 取消地址映射
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_GDIR);
	iounmap(GPIO1_DR);
	// 2. 删除cdev
	cdev_del(&newled.cdev);
	unregister_chrdev_region(newled.devid, NEWLED_CNT);
	// 3. 删除设备
	device_destroy(newled.class, newled.devid);
	// 4. 删除类
	class_destroy(newled.class);
	printk("newled_exit();\r\n");
}


module_init(newled_init);
module_exit(newled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("FSXC");

NEW LED测试程序

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define LED_ON  1
#define LED_OFF 0

int main(int argc, char *argv[]){
    int fid, retvalue;
    char *filename;
    if(argc != 3){
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fid = open(filename, O_RDWR);
    if(fid < 0){
        printf("Can't open the file %s!\r\n",filename);
        return -1;
    }

    unsigned char databuf[1];
    databuf[0] = atoi(argv[2]);
    retvalue = write(fid, databuf, sizeof(databuf));
    if(retvalue < 0){
        printf("LED control failed!\r\n");
        close(fid);
        return -1;
    }

    retvalue = close(fid);
    if(retvalue < 0){
        printf("Can't close the file %s\r\n!", filename);
        return -1;
    }
    return 0;
}

你可能感兴趣的:(Linux驱动开发笔记4:新字符设备驱动)