beaglebone black学习-05-新字符设备驱动开发

新字符设备(LED驱动)开发

上节介绍的字符驱动开发有两个弊端,第一无法指定次设备号,第二无法动态创建和删除设备节点,新的字符设备驱动开发可以有效的避免上述问题
参考文章:
Linux 字符设备驱动结构

新字符设备基础知识

1、字符设备结构体

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops; //该结构体描述了字符设备所实现的方法,非常重要
    struct list_head list;
    dev_t dev;                         //字符设备的设备号,由主设备号和次设备号构成
    unsigned int count;                //隶属于同以主设备的次设备号的个数
}

在 Linux 内核中,使用 cdev 结构体来描述一个字符设备,字符设备的加载、卸载以及用户空间对驱动的调用皆是围绕此结构体进行

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

字符设备、驱动、用户空间关系图

如上图所示,在 Linux 内核中:

  • 使用 cdev 结构体来描述字符设备;
  • 通过其成员 dev_t 来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
  • 通过其成员 file_operations 来定义字符设备驱动提供给 VFS 的接口函数,如常见的 open()、read()、write() 等;

在 Linux 字符设备驱动中:

  • 模块加载函数通过 register_chrdev_region( )alloc_chrdev_region( ) 来静态或者动态获取设备号;
  • 通过 cdev_init( ) 建立 cdev 与 file_operations 之间的连接,通过 cdev_add( ) 向系统添加一个 cdev 以完成注册;
  • 模块卸载函数通过 cdev_del( ) 来注销cdev,通过 unregister_chrdev_region( ) 来释放设备号;

用户空间访问该设备的程序:

  • 通过 Linux 系统调用,如 open( )read( )write( ),来调用file_operations 来定义字符设备驱动提供给 VFS 的接口函数;

3、字符设备驱动模型

字符设备驱动模型

字符设备注册注销相关函数接口

1、void cdev_init(struct cdev *, const struct file_operations *)

该函数主要对 struct cdev 结构体做初始化,最重要的就是建立 cdev 和 file_operations 之间的连接

参数介绍:

  • struct cdev *p: 自己申请或者定义的 cdev 结构体变量
  • const struct file_operations *: 文件操作结构体变量

2、struct cdev *cdev_alloc(void)

用于动态申请一个 cdev 内存,编写驱动时也不使用该函数,直接静态申请。

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

该函数向内核注册一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经可以使用了。
注意,添加之前需要先去注册一系列的设备号,cdev 相关接口只负责使用,不负责注册,即你提供给 cdev 的设备号必须是向内核注册过,实际可用的设备号

参数介绍:

  • struct cdev *p:自己申请或者定义的 cdev 变量
  • dev_t dev:设备号,注意设备号本质是一个 32 位无符号数,本身包含主次设备号
  • count:次设备号的个数

4、void cdev_del(struct cdev *p)

该函数向内核注销一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经不可以使用了

注册字符设备需要提供的内容

字符驱动开发本身是为了实现对于特定物理设备的操作,因此需要开发这自己提供以下信息用于字符设备的注册:

  • struct file_operations 结构指针,用于实现对字符设备的读写操作以及访问设备的私有数据
  • dev 设备号(主次设备号合在一起叫设备号),主设备号用来反应驱动类型,子设备号用来区分同类型设备
  • count:次设备号的个数

1、定义 file_operations

根据自己的设备特性和操作方法,自行实现 open() write() read() release()相关函数

2、定义设备号

设备号的本质是一个 4 字节的无符号数,内核为我们提供几个函数来提取或者构建设备号

  • 从设备号中提取主次设备号
MAJOR(dev_t dev);//提取主设备号

MINOR(dev_t dev);//提取次设备号
  • 通过主、次设备号生成设备号

MKDEV(int major,int minor);

注意:这里只是构建了设备号,并没有向系统申请设备号,跟定义了一个普通变量没有任何区别

3、分配设备号

(1)静态申请:int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数介绍:
- from: 第一个设备号
- count: 所需的连续设备号个数
- name: 设备或者驱动的名字

(2) 动态分配:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数介绍:
- dev: 第一个设备号(输出参数)
- baseminor: 请求的第一个次设备号
- name: 设备或者驱动的名字

4、注销设备号

void unregister_chrdev_region(dev_t from, unsigned count)

自动创建设备节点

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

具体步骤:
1、创建类class_create(owner, name)
2、创建设备struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
3、删除设备void device_destroy(struct class *class, dev_t devt)
4、删除类类void class_destroy(struct class *cls)

驱动源码

#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>

#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */

#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* 寄存器物理地址 */
#define GPIO1_OE_ADDR (0X4804C134) //GPIO输出使能寄存器
#define GPIO1_DATAOUT_ADDR (0X4804C13C) //GPIO输出电平控制

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *virtual_gpio1_oe_addr;
static void __iomem *virtual_gpio1_dataout_addr;

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(virtual_gpio1_dataout_addr);
		val &= ~(1 << 21);
		writel(val, virtual_gpio1_dataout_addr);
	} else if (sta == LEDOFF) {
		val = readl(virtual_gpio1_dataout_addr);
		val |= (1 << 21);
		writel(val, virtual_gpio1_dataout_addr);
	}
}

/*
 * @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);//必须从用户空间(buf)复制块数据到内核空间(databuf),因为用户空间的地址可能是虚拟地址,不能长久保存
	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、寄存器地址映射 */
	virtual_gpio1_oe_addr = ioremap(GPIO1_OE_ADDR, 4);
	virtual_gpio1_dataout_addr = ioremap(GPIO1_DATAOUT_ADDR, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(virtual_gpio1_oe_addr);
	val &= ~(1 << 21); // bit21写0,使能GPIO输出
	writel(val, virtual_gpio1_oe_addr);

	/* 3、默认关闭LED */
	val = readl(virtual_gpio1_dataout_addr);
	val &= ~(1 << 21);
	writel(val, virtual_gpio1_dataout_addr);

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) {		/*  定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, 1, "newchrled");
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, 1, "newchrled");	/* 申请设备号 */
		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 */
	cdev_add(&newchrled.cdev, newchrled.devid, 1);

	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE, "newchrled");
	if (IS_ERR(newchrled.class)) {
		return PTR_ERR(newchrled.class);
	}

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

	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(virtual_gpio1_oe_addr);
	iounmap(virtual_gpio1_dataout_addr);

	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, 1); /* 注销设备号 */

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

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxxxxx");

测试 APP

同 04 章节

编译验证

参考 04 章节

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值