10 新字符设备驱动文件

一、新字符设备驱动原理

  因为  register_chrdev unregister_chrdev 两个函数是老版本驱动文件,现在可以用新字符设备驱动 API 函数。

 

1. 分配和和释放设备号

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

1、需要我们事先去确定哪些主设备号没有使用;

2、会将主设备号下的所有次设备号都用掉。比如设置 LED 主设备号为 200,那么 0~1048575 这个区间的次设备号全部都被 LED 一个设备分走了。一个 LED 设备肯定只能有一个主设备号,一个次设备号。 

  解决这两个问题最好的方法就是在使用设备号的时候向 Linux 内核申请,需要几个就申请几个,如果没有指定设备号的话就使用如下函数来申请设备号: 

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

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

/*
 * @param - from : 要申请的起始设备号(自己给定的设备号)
 * @param - count : 申请的数量
 * @param - name : 设备名字
 */
int register_chrdev_region(dev_t from, unsigned count, const char *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);    // 如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0,并且大部分驱动次设备号都选择 0
    register_chrdev_region(devid, 1, "test");
} 
else 
{
    /* 没有定义设备号 */
    alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
    major = MAJOR(devid); /* 获取分配号的主设备号 */
    minor = MINOR(devid); /* 获取分配号的次设备号 */
}

/* 设备号释放 */
unregister_chrdev_region(devid, 1);     /* 注销设备号 */

 

2. 新的字符设备注册方法

① 字符设备结构

  编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备:

struct cdev test_cdev;

 

② 初始化字符设备

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

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

  初始化示例如下:

struct cdev testcdev;

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

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */

 

③ 添加字符设备

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

/*
 * @param - p : 指向要添加的字符设备(cdev结构体变量)
 * @param - dev : 设备所使用的设备号
 * @param - count : 要添加的设备数量
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

  添加 cdev_add 示例如下:

struct cdev testcdev;

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

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops);  /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1);     /* 添加字符设备 */

  Linux 内核中大量的字符设备驱动都是采用这种方法向 Linux 内核添加字符设备。 

 

④ 卸载字符设备

  cdev_del 函数是从 Linux 内核中删除相应的字符设备:

void cdev_del(struct cdev *p);

  删除字符设备示例如下:

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

 

二、自动创建设备节点

  使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。 

1. mdev 

  使用 mdev 来实现设备节点文件的自动创建与删除。buildroot 已经帮我们处理好了 mdev 。

 

2. 创建和删除类

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

/*
 * @description : 创建的类
 * @param - owner : 一般为 THIS_MODULE
 * @param - name : 设备名字
 * @return : 指向结构体 class 的指针,也就是创建的类
 */
struct class *class_create (struct module *owner, const char *name);


/* 类删除函数 */
void class_destroy(struct class *cls);    // cls 就是要删除的类

 

3. 创建设备

  自动创建设备节点还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备:

/*
 * @param - cls : 设备要创建哪个类下面
 * @param - parent : 父设备,一般为 NULL
 * @param - devt : 设备号
 * @param - drvdata : 设备可能会使用的一些数据,一般为 NULL
 * @param - fmt : 设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件
 * @return : 创建好的设备
 */
struct device *device_create(struct class *cls,
                             struct device *parent,
                             dev_t devt,
                             void *drvdata,
                             const char *fmt, ...)
    
/* 删除创建的设备 */
void device_destroy(struct class *cls, dev_t devt);  
// classs 是要删除的设备所处的类
// devt 是要删除的设备号

 

4. 示例

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)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,但对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动 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 函数里面设置好私有数据以后,在 writereadclose 等函数中直接读取 private_data 即可得到设备结构体。 

 

四、驱动程序编写

1. LED 驱动程序编写

  在 3/linux/atk-mpl/Drivers 下创建 3_newchrled 子目录,并且 VScode 工作区名为 newchrled,并且新建 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 PERIPH_BASE     		     	(0x40000000)
#define MPU_AHB4_PERIPH_BASE			(PERIPH_BASE + 0x10000000)
#define RCC_BASE        		    	(MPU_AHB4_PERIPH_BASE + 0x0000)	
#define RCC_MP_AHB4ENSETR				(RCC_BASE + 0XA28)
#define GPIOI_BASE						(MPU_AHB4_PERIPH_BASE + 0xA000)	
#define GPIOI_MODER      			    (GPIOI_BASE + 0x0000)	
#define GPIOI_OTYPER      			    (GPIOI_BASE + 0x0004)	
#define GPIOI_OSPEEDR      			    (GPIOI_BASE + 0x0008)	
#define GPIOI_PUPDR      			    (GPIOI_BASE + 0x000C)	
#define GPIOI_BSRR      			    (GPIOI_BASE + 0x0018)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *MPU_AHB4_PERIPH_RCC_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;

/* 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(GPIOI_BSRR_PI);
		val |= (1 << 16);	
		writel(val, GPIOI_BSRR_PI);
	}
    else if(sta == LEDOFF) 
    {
		val = readl(GPIOI_BSRR_PI);
		val|= (1 << 0);	
		writel(val, GPIOI_BSRR_PI);
	}	
}

/*
 * @description		: 取消映射
 * @return 			: 无
 */
void led_unmap(void)
{
	/* 取消映射 */
	iounmap(MPU_AHB4_PERIPH_RCC_PI);
	iounmap(GPIOI_MODER_PI);
	iounmap(GPIOI_OTYPER_PI);
	iounmap(GPIOI_OSPEEDR_PI);
	iounmap(GPIOI_PUPDR_PI);
	iounmap(GPIOI_BSRR_PI);
}

/*
 * @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;
	int ret;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
    MPU_AHB4_PERIPH_RCC_PI = ioremap(RCC_MP_AHB4ENSETR, 4);
    GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4);
    GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER, 4);
    GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR, 4);
    GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR, 4);
    GPIOI_BSRR_PI = ioremap(GPIOI_BSRR, 4);

    /* 2、使能PI时钟 */
    val = readl(MPU_AHB4_PERIPH_RCC_PI);
    val &= ~(0X1 << 8); /* 清除以前的设置 */
    val |= (0X1 << 8);  /* 设置新值 */
    writel(val, MPU_AHB4_PERIPH_RCC_PI);

    /* 3、设置PI0通用的输出模式。*/
    val = readl(GPIOI_MODER_PI);
    val &= ~(0X3 << 0); /* bit0:1清零 */
    val |= (0X1 << 0);  /* bit0:1设置01 */
    writel(val, GPIOI_MODER_PI);

    /* 3、设置PI0为推挽模式。*/
    val = readl(GPIOI_OTYPER_PI);
    val &= ~(0X1 << 0); 
    writel(val, GPIOI_OTYPER_PI);

    /* 4、设置PI0为高速。*/
    val = readl(GPIOI_OSPEEDR_PI);
    val &= ~(0X3 << 0); /* bit0:1 清零 */
    val |= (0x2 << 0); /* bit0:1 设置为10*/
    writel(val, GPIOI_OSPEEDR_PI);

    /* 5、设置PI0为上拉。*/
    val = readl(GPIOI_PUPDR_PI);
    val &= ~(0X3 << 0); /* bit0:1 清零*/
    val |= (0x1 << 0); /*bit0:1 设置为01*/
    writel(val,GPIOI_PUPDR_PI);

    /* 6、默认关闭LED */
    val = readl(GPIOI_BSRR_PI);
    val |= (0x1 << 0);
    writel(val, GPIOI_BSRR_PI);


	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) 
    {		
        /* 定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
		if(ret < 0) 
        {
			pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);
			goto fail_map;
		}
	} 
    else 
    {						
        /* 没有定义设备号 */
		ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		if(ret < 0) 
        {
			pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRLED_NAME, ret);
			goto fail_map;
		}
		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 = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
	if(ret < 0)
    {
        goto del_unregister;
    }

	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class)) 
    {
		goto del_cdev;
	}

	/* 5、创建设备 */
	newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.device)) 
    {
		goto destroy_class;
	}
	
	return 0;

destroy_class:
	class_destroy(newchrled.class);
del_cdev:
	cdev_del(&newchrled.cdev);
del_unregister:
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
fail_map:
	led_unmap();
	return -EIO;

}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
   led_unmap();
   
	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("LXS");
MODULE_INFO(intree, "Y");

  这里做了一个流程图方便理解:

 

2.  编写测试 APP 

  这个用上次的那个 ledApp.c 来测试。

 

五、运行测试

1.  编译驱动程序

  编写 Makefile:

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linux内核源码路径
CURRENT_PATH := $(shell pwd)		# 获取当前所处路径
obj-m := newchrled.o		

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  输入命令:

make
# 成功后会出现一个 newchrled.ko 驱动模块文件

 

2.  编译测试 APP

 利用交叉编译器编译 APP 测试程序:

arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

 

3.  运行测试

  需要将 ledApp newchrled.ko 复制到 /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ 目录下。

depmod              # 第一次加载驱动的时候需要运行此命令
modprobe newchrled  # 加载驱动

 

  申请主设备号为 241,次设备号为 0。并且驱动会自动在 /dev 目录下创建设备节点文件 /dev/newchrdev:

  再测试一下 LED 红灯是否可以工作:

./ledApp /dev/newchrled 1  # 打开 LED 灯

./ledApp /dev/newchrled 0  # 关闭 LED 灯

# 最后卸载驱动
rmmod newchrled

 

总结:跟上一章相比,多了设备自动分配、新的字符设备注册方法、自动创建设备节点和设置文件私有数据,其他和上一章相差不大。

本文作者:烟儿公主

本文链接:https://www.cnblogs.com/toutiegongzhu/p/17628910.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   烟儿公主  阅读(119)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 夏日大冒险 暴躁的兔子
夏日大冒险 - 暴躁的兔子
00:00 / 00:00
An audio error has occurred.

作词 : 暴躁的兔子

作曲 : 暴躁的兔子

编曲 : IOF

混音:Gfanfan

出品:网易飓风

夏天 不要再浪费时间

实现 你承诺过的改变

别再 找一堆借口拖延

现在就和我一起飞向海边

人生苦短 你应该学会如何作乐

低着头还怎么应对挫折

人应该为自己活着

不用去迎合

要去寻欢作乐

撮合我的浪漫和悲欢

把这荒诞人生都塞满

生活难免磕磕绊绊

对抗生活的平庸就是浪漫

学会取悦自己逆风翻盘

去反抗变态的三观

把条条框框都砸烂

建立新的规则推翻谈判

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

去冒险

我真的不care你是否会喜欢我

不跟风被定义的美 全都是灾祸

我才不讨好大多数绝不与示弱

过好你的生活

你管我应该怎么快活

没有人能有资格审判

别人的生活和牵绊

快闭上你的高谈阔论

乘风破浪吧 理想的风帆

我就是肆意张扬又如何

我就是锋芒毕露又如何

我就是离经叛道又如何

我就是要出格 你管我要如何

我就是与众不同又如何

我就是特立独行又如何

我就是不知好歹又如何

你管我怎样出格 你管我如何

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

不知进退的人呐

和我一起去海边

聊聊曾经的理想 一起想当年

那曾想改变世界的人

是否还满腔热忱

不羁的我们放肆着

反抗那命运的指针

解放灵魂

推广:网易飓风

企划:贾焱祺

监制:徐思灵

出品人:谢奇笛