字符设备驱动是Linux内核中用于管理字符设备的驱动类型,是Linux内核中三大类设备(字符设备、块设备和网络设备)的一类。
字符设备是以字节流形式进行数据传输的设备;支持基于字节的数据读取和写入,常见的字符型设备如下所示。
从上面可以看出,日常开发中大部分的设备都属于字符设备。网络设备和块设备虽然也多,但一般都使用系统自带的驱动,很少单独开发。目前主要的网络设备包含以太网、CAN、WIFI、蓝牙等;块设备则通常则是SD卡、Nand、EMMC、固态和机械硬盘等,这些设备类型将在后续进行说明。
本节主要讲解字符设备驱动,将从以下几个方面讲解说明。
本章节快速学习知识点:
关于字符设备涉及函数功能和接口如下所示。
| 字符设备操作 | 具体功能 | 功能接口 | 接口说明 |
|---|---|---|---|
| 驱动注册 | 分配cdev设备号 | register_chrdev_region | 根据范围注册申请一组设备号 |
| alloc_chrdev_region | 动态申请一组设备号值 | ||
| misc_register | 创建杂项设备 | ||
| 初始化cdev配置 | cdev_init | 初始化字符型设备结构体 | |
| 注册cdev设备 | cdev_add | 将申请设备号和字符型设备功能结构在内核中添加并绑定 | |
| 设备操作函数 | 操作函数结构体”file_operations” | open | 打开设备 |
| read | 读取设备 | ||
| write | 写入设备 | ||
| ioctl | 设备控制 | ||
| close | 关闭设备 | ||
| 设备创建 | 创建类文件 | class_create | 用于创建在设备添加系统时需要提交的设备类信息,并在/sys/class下添加对应的类型 |
| 创建设备文件 | device_create | 用于在系统中添加设备的接口,成功后会在/dev/目录下创建对应的设备 | |
| 驱动注销 | 移除创建的设备文件 | device_destroy | 移除设备文件 |
| 移除设备类 | class_destroy | 移除已经创建的设备类结构 | |
| 移除cdev设备 | cdev_del | 移除字符型设备 | |
| 释放设备号 | unregister_chrdev_region | 移除已经申请的设备号 |
详细的开发接口说明如下所示。
/*
申请和移除设备号
*/
// 说明: 根据范围注册申请一组设备号(from为起始的主从设备号),仅申请设备号,设备后续创建
// @from: 起始设备号,是一个32位数据类型,高12位主设备号,低20位为次设备号
// @count: 要申请的设备号数目,设备号按照次设备号每次加1
// @name: 设备的名称,在/proc/device中显示
// 返回: 0表示申请成功,其它返回错误码
int register_chrdev_region(dev_t from, unsigned count, const char *name);
// 说明: 动态申请一组设备号值
// @dev: 指向申请的起始设备号
// @baseminor: 起始次设备号,从此次设备号分配设备号
// @count: 要申请的设备号数目,设备号按照次设备号每次加1
// @name: 设备的名称,在/proc/device中显示相应设备号
// 返回: 0表示申请成功,其它返回错误码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
// 说明: 创建杂项设备
// @misc: 杂项设备结构体指针,用于关联字符设备结构体和字符设备操作接口
// 返回:0表示申请成功,其它返回错误码
int misc_register(struct miscdevice *misc)
// 说明: 移除已经申请的设备号
// @from: 已申请的起始设备号
// @count: 已申请的设备号数目
void unregister_chrdev_region(dev_t from, unsigned count)
/*
向系统添加和删除字符型设备
*/
// 说明: 初始化字符型设备结构体
// @cdev: 字符型设备结构体指针,赋值关联
// @fops: 字符设备支持的操作结构体
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
// 说明: 将申请设备号和字符型设备功能结构在内核中添加并绑定
// @cdev: 关联的字符设备节点指针,并添加到内核
// @dev: 关联的设备号起始地址
// @count: 关联设备号的数目
// 返回: 0表示申请成功,其它返回错误码
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
// 说明: 删除字符型设备
// @cdev: 删除已经关联的字符型设备
void cdev_del(struct cdev *p)
/*
将字符设备和设备类文件,以及设备文件关联,用于应用层访问
*/
// 说明: 用于创建在设备添加系统时需要提交的设备类信息,并在/sys/class下添加对应的类型。
// @owner: 驱动的拥有者,一般为THIS_MODULE
// @name: 所属类的名称,定义在/sys/class下,多个设备组成类管理
// 返回: 设备类结构
#define class_create(owner, name)
// 说明: 销毁已经创建的设备类对象
// @cls: 已经创建的类
void class_destroy(struct class *cls)
// 说明: 用于在系统中添加设备的接口,成功后会在/dev/目录下创建对应的设备
// @cls:设备所属的设备类
// @parent: 所属的父设备
// @devt: 对应处理的设备号
// @drvdata: 设备私有数据
// @fmt: 设备私有名称,支持类似可变变量组成字符串
// @返回: 创建完成的device结构,可在/dev/目录下查看
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
// 说明: 用于移除已经添加的设备
// @class: 设备所属的设备类
// @devt: 设备的设备号
void device_destroy(struct class *class, dev_t devt)
这里以I/O驱动蜂鸣器来演示Linux驱动的设计实现思路。接触过单片机开发的应该了解,控制一个I/O去操作蜂鸣器的流程包含如下。
对于嵌入式Linux系统,其实现逻辑也类似。也可以直接配置寄存器实现功能和引脚控制。
不过寄存器的配置有以下缺点:
对于单片机来说,往往每家都有独立的驱动和SDK包,统一的需求并不大,这个问题并不严重。对于Linux系统来说,需要兼容不同指令集、厂商、芯片的驱动需求,实现统一的管理。统一设备模型被设计出来,其中设备树用于描述硬件差异,驱动则解析设备树实现具体功能。
注意: 统一设备模型通过设备树来区分板级信息差异,不过这并没有彻底解决问题。举个例子,全志芯片的驱动大概率不能直接被NXP、瑞芯微的芯片使用;不过这仍然降低了驱动的碎片化,对于同一家厂商的驱动,一般都会前后兼容,只需要更新相应的设备树即可,而不需要不同的板级信息就需要更新驱动,从这个角度已经是很大的进步了。
这里以控制蜂鸣器输出的I/O来展示字符设备驱动的实现,具体步骤实现。
step-1: 确定硬件连接;如下图,蜂鸣器连接硬件为GPIO5_IO01。


step-2: 根据硬件,实现设备树节点;参考之前设备树语法说明,具体思路如下。
按照这个思路,一个完整的BEEP设备树节点就实现完毕,再调整下顺序,设备树节点就如下所示。
/ {
usr_beep {
compatible = "rmk,usr-beep"; //标签,用于驱动匹配的字符串
pinctrl-names = "default"; //选择引脚的复用定义,设备树解析时执行
pinctrl-0 = <&pinctrl_gpio_beep>;
beep-gpios = <&gpio5 1 GPIO_ACTIVE_LOW>; //用于指明在驱动中访问的引脚
status = "okay";
};
//......
};
/* 设备树节点定义 */
&iomuxc {
...
pinctrl_gpio_beep: beep {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0 //定义引脚的复用和工作性能
>;
};
};
注意:在应用中写入的0/1都是逻辑电平,真实输出电平则还与GPIO_ACTIVE_LOW有关。当为GPIO_ACTIVE_LOW时,I/O会翻转,即写1时输出低电平,0输出高电平。反之为GPIO_ACTIVE_HIGH时,写0时输出低电平,1输出高电平。另外此功能生效需要使用devm_gpiod_get,gpiod_set_value,gpiod_set_value接口,gpio_set_value等则相当于gpiod_set_raw_value,不会处理此标志。 **
step-3:根据设备树,就可以再驱动中读取相应的gpio资源,封装成操作gpio的接口,具体如下所示。
// 配置硬件接口
static int beep_hardware_init(struct beep_data *chip)
{
struct platform_device *pdev = chip->pdev;
// 1.获取beep gpio属性, 设置为输出模式
chip->desc = devm_gpiod_get(&pdev->dev, "beep", GPIOD_OUT_LOW);
if (IS_ERR(chip->desc)) {
dev_err(&pdev->dev, "beep request gpios failed!\n");
return PTR_ERR(chip->desc);
}
dev_info(&pdev->dev, "beep_hardware_init is active:%d\n", gpiod_is_active_low(chip->desc));
return 0;
}
// beep_read接口,应用层read调用时执行,读取内部数据
ssize_t beep_read(struct file *filp, char __user *buf, size_t cnt, loff_t *f_pos)
{
int ret;
u8 beep_state;
struct beep_data *chip = filp->private_data;
struct platform_device *pdev = chip->pdev;
cnt = min_t(size_t, cnt, sizeof(beep_state));
beep_state = gpiod_get_value_cansleep(chip->desc);
// 使用copy_to_user用于将内核数据拷贝到应用层数据
// 直接赋值或者使用memcpy在运行时访问非法地址,触发异常
ret = copy_to_user(buf, &beep_state, cnt);
if (ret) {
dev_err(&pdev->dev, "read failed:%d!\n", ret);
return -EFAULT;
}
return cnt;
}
// beep_write接口,应用层write调用时执行,将应用层数据写入硬件中
ssize_t beep_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *f_pos)
{
int ret;
u8 data;
struct beep_data *chip = filp->private_data;
struct platform_device *pdev = chip->pdev;
cnt = min_t(size_t, cnt, sizeof(data));
// 使用copy_from_user用于将应用层数据拷贝到内核数据
// 直接赋值或者使用memcpy在运行时访问非法地址,触发异常
ret = copy_from_user(&data, buf, cnt);
if (ret) {
dev_err(&pdev->dev, "copy_from_user failed!\n");
return -EFAULT;
}
// 字符'0'转换为0, 兼容应用层echo 0 > /dev/beep写入
if (data >= '0' && data <= '9') {
data = data - '0';
}
ret = beep_hardware_set(chip, data);
if (ret) {
return ret;
}
return cnt;
}
//....
可以看到,软件对于硬件的操作包含如下部分。
这里有两个重要的接口,copy_to_user和copy_from_user, 这两个函数用于内核和用户空间的数据拷贝。
// 用于将内核空间的数据复制到用户空间
// @to:指向用户空间目标地址的指针
// @from:指向内核空间源地址的指针
// @n:要复制的字节数
// @return:成功返回0; 发生错误,返回未复制的字节数
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
// 用于将用户空间的数据复制到内核空间
// @to:指向内核空间目标地址的指针
// @from:指向用户空间源地址的指针
// @n:要复制的字节数
// @return:成功返回0; 发生错误,返回未复制的字节数
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
注意: 内核和用户空间访问地址的空间范围,访问权限不一致,在内核中直接使用用户内存会触发异常访问失败;需要通过copy_to_user和copy_from_user接口进行操作处理。
可以在应用层通过file文件接口open,read,write,ioctl,close完成对字符设备硬件的操作。
Linux内核中使用struct cdev结构体描述一个设备,定义如下。
// cdev字符设备结构
struct cdev {
struct kobject kobj; // 基础结构,提供结构共有特性,参考Documentation/kobject.txt
struct module *owner; // 定义字符设备的所有者,一般为THIS_MODULE
const struct file_operations *ops; // 字符设备的操作接口,用于实现应用层到底层的访问
struct list_head list; // 链表头,用于管理字符设备
dev_t dev; // 字符设备设备号,用于区分不同的设备
unsigned int count; // 设备的引用计数
} __randomize_layout;
对于struct cdev字符设备结构,主要配置的属性如下所示。
// file_operations是对于硬件操作最重要的结构,不过在驱动开发中,
// 主要实现open,read, write, unlocked_ioctl, close接口即可,其它接口大部分情况下不需要实现
struct file_operations {
struct module *owner; // 定义字符设备的所有者
loff_t (*llseek) (struct file *, loff_t, int); // 设置文件指针位置的函数
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 从文件读取数据的函数
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 向文件写入数据的函数
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 执行文件的 ioctl 操作的函数
int (*open) (struct inode *, struct file *); // 打开文件时调用的函数
int (*release) (struct inode *, struct file *); // 关闭文件时调用的函数
// ....
};
对于struct file_operations结构体,主要涉及的操作属性如下所示。
// 文件操作支持访问的接口
static const struct file_operations beep_fops = {
.owner = THIS_MODULE,
.open = beep_open,
.read = beep_read,
.write = beep_write,
.unlocked_ioctl = beep_ioctl,
.release = beep_release,
};
// open接口,一般用于获取硬件资源,后续代码使用
int beep_open(struct inode *inode, struct file *filp)
{
struct beep_data *chip;
// 通过inode的cdev属性获取全局对象
chip = container_of(inode->i_cdev, struct beep_data, cdev);
filp->private_data = chip;
return 0;
}
//...
上面定义了创建struct cdev需要的硬件接口,下面就要实现如何创建设备。
对于字符设备的创建方法,归纳起来如下所示。
可以看到,字符设备的创建步骤如下所示。
这样在文件系统中访问设备文件时,就可以查找关联的字符设备,从而最终能够操作实际硬件。
关于字符设备的申请和创建流程,详细如下所示。
// 创建字符设备
static int beep_device_create(struct beep_data *chip)
{
// step1:申请注册设备号
int result;
int major = 0;
int minor = 0;
struct platform_device *pdev = chip->pdev;
if (major) {
// 通过自定义的主设备号和次设备号从系统中申请设备号
chip->dev_id= MKDEV(major, minor);
result = register_chrdev_region(chip->dev_id, 1, "led");
} else {
// 从系统中申请未使用的设备号
result = alloc_chrdev_region(&chip->dev_id, 0, 1, "led");
major = MAJOR(chip->dev_id);
minor = MINOR(chip->dev_id);
}
// step2:初始化字符设备,并添加到内核中
// 完成后即可通过cat /proc/devices查看指定设备号的设备是否被创建
cdev_init(&chip->cdev, &beep_fops);
chip->cdev.owner = THIS_MODULE;
result = cdev_add(&chip->cdev, chip->dev_id, 1);
if (result != 0) {
dev_err(&pdev->dev, "cdev add failed\n");
goto exit_cdev_add;
}
// step3:在系统中创建设备/dev/led(这一步可以在系统中通过mknod实现)
chip->class = class_create(THIS_MODULE, "led");
if (IS_ERR(chip->class)) {
dev_err(&pdev->dev, "class create failed!\r\n");
result = PTR_ERR(chip->class);
goto exit_class_create;
}
chip->device = device_create(chip->class, NULL, chip->dev_id, NULL, "led");
if (IS_ERR(chip->device)) {
dev_err(&pdev->dev, "device create failed!\r\n");
result = PTR_ERR(chip->device);
goto exit_device_create;
}
return 0;
exit_device_create:
class_destroy(chip->class);
exit_class_create:
cdev_del(&chip->cdev);
exit_cdev_add:
unregister_chrdev_region(chip->dev_id, 1);
exit:
return result;
}
static int beep_remove(struct platform_device *pdev)
{
struct beep_data *chip = platform_get_drvdata(pdev);
// step1: 删除设备
device_destroy(chip->class, chip->dev_id);
class_destroy(chip->class);
// step2:从内核中删除字符设备结构
cdev_del(&chip->cdev);
// step3: 注销申请的设备号
unregister_chrdev_region(chip->dev_id, 1);
dev_info(&pdev->dev, "beep release!\n");
return 0;
}
其中对于创建设备的过程可以通过在Linux系统执行mknod命令创建,具体如下。
# 格式mknod device {b | c} MAJOR MINOR
# device 创建的设备名,包含目录
# {b | c} b表示块设备,c表示字符设备
# MAJOR 主设备号
# MINOR 子设备哈号
mknod /dev/beep c 237 0
# 查看内核已注册的字符设备
cat /proc/devices
# 查看系统已经注册的中断
cat /pro/interrupts
# 主设备号对于的设备类型
1 - 内存设备
4 - TTY设备(包含终端设备,串口设备)
5 - console设备
10 - 杂项设备
13 - 输入设备
这样在系统中使用文件访问时,就可以根据设备号,查找到对应的字符设备。另外,通过将应用层的open/read/write/close和底层的字符设备接口ops关联起来,从而实现应用层对于底层的访问。
上述是标准的字符设备的创建方式,不过如果不需要单独的主设备号,可以直接使用杂项设备创建,这样可以简化操作。
关于字符设备创建的详细代码见: beep设备详细代码实现。
杂项设备(misc)也是字符设备的一种,用于生成设备节点。不过杂项设备是固定主设备号的设备(主设备号固定为10),通过次设备号来区别不同的设备。
使用杂项设备可以简化字符设备的创建过程, 杂项设备的结构定义如下。
struct miscdevice {
int minor; /* 子设备号 */
const char *name; /* 设备名称 */
const struct file_operations *fops; /* 文件操作结构体 */
struct list_head list; /* 链表头 */
struct device *parent; /* 父设备指针 */
struct device *this_device; /* 当前设备指针 */
const struct attribute_group **groups; /* 属性组指针 */
const char *nodename; /* 节点名称 */
umode_t mode; /* 设备模式 */
};
对于这个结构,关键的参数类型如下所示。
关于杂项设备的创建,步骤和实现如下所示。
#define DEVICE_NAME "miscbeep" /* 设备名, 应用将以/dev/miscbeep访问 */
#define MISCBEEP_MINOR 156 /* 子设备号 */
struct beep_data
{
/* gpio info */
int gpio;
int status;
/*device info*/
struct platform_device *pdev;
struct miscdevice misc_dev;
};
// 杂项设备打开接口
int beep_open(struct inode *inode, struct file *filp)
{
struct miscdevice *mdev;
struct beep_data *chip;
mdev = filp->private_data;
chip = container_of(mdev,
struct beep_data,
misc_dev);
filp->private_data = chip;
return 0;
}
// 创建杂项设备接口
static int beep_device_create(struct beep_data *chip)
{
int result;
struct platform_device *pdev = chip->pdev;
// 配置杂项设备的接口
chip->misc_dev.fops = &beep_fops;
// 定义次设备号和名称
chip->misc_dev.minor = MISC_DYNAMIC_MINOR;
chip->misc_dev.name = DEVICE_NAME;
result = misc_register(&(chip->misc_dev));
if(result < 0){
dev_info(&pdev->dev, "misc device register failed!\n");
return -EFAULT;
}
dev_info(&pdev->dev, "misc device register ok, minor:%d!\n", chip->misc_dev.minor);
return 0;
}
在杂项设备创建后,即可在系统中创建对应的设备节点/dev/miscbeep。
# 获取系统的杂项设备
cat /proc/misc
注意:杂项设备中的open函数中,flip->private_data中在调用前会保存为miscdevice结构体,使用此数据结构,即可获取到完整的系统设备参数。
本节代码可参考:杂项设备驱动代码.
可以看到,杂项设备的注册和创建由接口misc_register即可完成,相对来说更简单,且不占用主设备号,节省资源。如果开发的设备不需要独立的主设备号,可使用此接口创建杂项设备。另外,Linux也支持其它方式创建字符设备,如对于输入信号可以使用input子系统管理,这个在后面相应会单独说明,rtc设备通过rtc子系统添加等。至此,关于字符设备的简要说明完成。
无论是简单的I/O控制,如LED,BEEP,还是复杂的I2C, SPI设备,从原理上都由字符设备创建和硬件接口管理这两部分实现,按照这个思路,可以更清晰的理解字符型设备。
sysfs是Linux内核中向用户空间导出对象的虚拟文件系统。它提供了一种机制,用于将内核对象、驱动程序和设备的信息以文件和目录的形式呈现给用户空间。
通过sysfs用户空间程序可以方便地与内核进行交互,获取设备信息、配置设备参数等。sysfs并不实际存储数据在磁盘上,而是在内存中维护数据结构,通过文件和目录的形式将这些数据暴露给用户空间,可以通过”ls /sys”目录查看。
常用sys目录核功能说明如下。
/sys/bus:包含各种总线类型的目录,如pci、usb、i2c等。每个总线目录下包含该总线上连接的设备和驱动程序的信息。/sys/class:按设备类型分类的设备类目录,如block(块设备)、char(字符设备)、net(网络设备)等。每个设备类目录下包含该类型的所有设备。/sys/dev:包含设备节点的符号链接,指向/sys/class或/sys/bus下的具体设备目录。/sys/devices:包含系统中所有设备的目录树,按照设备的拓扑结构组织。/sys/module:包含内核模块的信息,每个模块对应一个目录,目录下包含模块的属性和参数。/sys/firmware:包含与固件相关的信息。sysfs将内核中的各种对象(如设备、驱动程序、总线等)映射为文件和目录,用户可以通过读写这些文件来获取或修改内核对象的属性。在内核中,依赖”struct kobject”结构来显示和管理设备,常见的接口如下。
// 初始化 kobject
// @param kobj: 要初始化的 kobject 结构体指针
// @param ktype: 要初始化的 kobject 类型指针
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
// 将kobject添加到父类中
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);
// 初始化并添加到父类中
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt,...);
// 创建一个kobject对象
struct kobject *kobject_create(void)
// 创建并添加到父类中
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
// 设置对象的名称
int kobject_set_name(struct kobject *kobj, const char *fmt,...);
// 删除kobject对象
void kobject_put(struct kobject *kobj);
// 创建文件属性
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
// 移除kobject的属性
void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);
struct attribute {
const char *name; // 属性名称
umode_t mode; // 属性文件的访问权限
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};
struct kobj_attribute {
struct attribute attr;
// 用于读取属性值的函数指针
ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,
char *buf);
// 用于存储属性值的函数指针
ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count);
};
上述就是kobject的主要内容,如果parent指向NULL,则会在sys目录下创建文件。
// 在/sys/目录下创建led_node文件
struct kobject *led_node = kobject_create_and_add("led_node", NULL);
// 配置文件的属性
struct kobj_attribute led_attr = {
.attr.name = "led",
.attr.mode = 0644,
.show = led_show,
.store = led_store,
}
//具有相同功能的宏定义
struct kobj_attribute led_attr = __ATTR(led, 0644, led_show, led_store);
// 未节点添加读写属性
sysfs_create_file(led_node, &led_attr.attr);
// 移除属性的处理,驱动卸载时添加
// 移除节点属性
sysfs_remove_file(led_node, &led_attr.attr);
// 删除属性值
kobject_put(led_node);
不过对于驱动中一般使用更上层的封装接口,device_create_file和device_remove_file接口,最终也会调用上述sysfs接口,在设备下创建sysfs目录和文件。
关于文件创建接口,具体如下所示。
// 创建文件属性
// @param dev: 设备对象
// @param attr: 属性对象
// @return: 成功返回0,失败返回错误码
int device_create_file(struct device *dev, const struct device_attribute *attr);
// 移除属性文件
// @param dev: 设备对象
// @param attr: 属性对象
void device_remove_file(struct device *dev, const struct device_attribute *attr);
// device_attribute和kobject基本一致,可使用宏DEVICE_ATTR定义
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
字符设备方法和杂项设备使用的接口函数不同,但都是在应用层实现设备文件,然后通过文件接口进行访问。不过还有其它方式进行应用层的访问管理,这里列出其它方式。
这三类是大部分字符型设备的访问方式。不过字符型设备相关的知识不止于此,如SPI系统框架,I2C系统框架,input子系统等都属于字符设备的开发中涉及的技术,这些在后续章节后进一步说明。
前面都是讲解的如何实现将硬件加载到内核的驱动,但最终还是需要被软件访问的,驱动在加载成功后,会在系统中创建对应的文件,此时使用C标准文件接口即可进行对硬件的操作,这些接口如下。
// 打开设备文件,获得文件描述符
// 调用驱动中fops的open函数
int open(const char *pathname, int oflag,...);
// 从文件中读取数据
// 调用驱动中fops的read函数
ssize_t read(int fd, void * buf, size_t count);
// 向文件中写入数据
// 调用驱动中fops的write函数
ssize_t write (int fd, const void * buf, size_t count);
// 向设备文件中写入请求命令
// 调用驱动中fops的ioctl函数
int ioctl(int fd, ind cmd, …);
// 关闭文件描述符
// 调用驱动中fops的close函数
int close(int fd);
以本例中的”/dev/miscbeep”为例,应用层访问代码如下。
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define DEVICE_BEEP "/dev/miscbeep"
int main(int argc, const char *argv[])
{
unsigned char val = 1;
int fd;
// 读取设备文件,获取句柄
fd = open(DEVICE_BEEP, O_RDWR | O_NDELAY);
if (fd == -1) {
printf("open %s error\r\n", DEVICE_BEEP);
return -1;
}
if (argc > 1) {
val = atoi(argv[1]);
}
// 写入数据
write(fd, &val, 1);
// 关闭文件
close(fd);
exit(0);
}
本节代码可参考:应用层访问杂项设备实现.
本节主要讲解了字符设备的基本概念和实现,包括字符设备接口,硬件接口和访问接口,同时也实现了应用层访问的接口。
在应用层中我们关心的是/dev/led这样的设备文件,通过应用层的open、read、write、ioctl、close进行操作;而对于驱动中,操作硬件的函数对应就是struct file_operations中的open、read、write、unlock_ioctl、close指定的函数。
关于这两个接口如何关联,其中/dev/led由主设备号和次设备号通过mknod生成;在内核中,cdev_add会将设备号和struct file_operations关联起来。这样在访问设备文件时,就会在内核中根据设备号找到字符设备结构cdev,则和file_operations进行关联,最终调用驱动中的硬件操作接口。这就是字符驱动在内核中访问的流程。
理解了这些,字符设备驱动的实现就可以总结如下。
此即字符设备实现的常规步骤,无论简单的gpio、pwm内部模块、复杂的触摸屏、rtc、nvmem、2c/spi外部器件,其基础框架都是类似的;只是具体的操作接口和硬件资源不同。
具体模板代码可参考:字符设备代码模板。