新字符设备(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 章节