字符设备驱动是Linux内核中用于管理字符设备的驱动类型,是Linux内核中三大类设备(字符设备、块设备和网络设备)的一类。
字符设备是以字节流形式进行数据传输的设备;支持基于字节的数据读取和写入,常见的字符型设备如下所示。
从上面可以看出,我们日常开发中大部分的设备都属于字符设备。网络设备和块设备虽然也多,但一般都使用系统自带的驱动,很少单独开发。目前主要的网络设备包含以太网、CAN、WIFI、蓝牙等;块设备则通常则是SD卡,Nand FLASH,EMMC,固态和机械硬盘等,这些设备类型将在后续进行说明;本节主要讲解字符设备驱动。
关于字符设备驱动的开发,具体目录如下所示。
关于字符设备涉及函数功能和接口如下所示。
| 字符设备操作 | 具体功能 | 功能接口 | 接口说明 |
|---|---|---|---|
| 驱动注册 | 分配cdev设备号 | register_chrdev_region | 根据范围注册申请一组设备号 |
| alloc_chrdev_region | 动态申请一组设备号值 | ||
| 初始化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)
// 说明: 移除已经申请的设备号
// @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驱动LED来演示Linux驱动的设计实现思路。接触过单片机开发的应该了解,控制一个I/O去操作LED的流程包含如下。
对于嵌入式Linux系统,其实现逻辑也类似。也可以直接配置寄存器实现功能和引脚控制,不过寄存器的配置有以下缺点:
对于单片机来说,往往每家都有独立的驱动和SDK包,统一的需求并不大,因此问题并没有那么严重。对于Linux系统来说,需要兼容不同指令集、厂商、芯片的驱动需求,实现统一的管理,统一设备模型被设计出来,其中设备树用于描述硬件差异,驱动则解析设备树实现具体功能。
注意: 统一设备模型通过优化板级信息差异,不过并没有彻底解决。举个例子,全志芯片的驱动大概率不能被NXP、瑞芯微的芯片使用。不过这仍然降低了驱动的碎片化,对于同一家厂商的驱动,往往可以前后兼容,而不需要不同的板级信息就需要相应的驱动,从这个角度已经是很大的进步了。
这里以控制LED输出的I/O来展示字符设备驱动的实现,具体步骤实现。
step-1: 确定硬件连接;如下图,LED连接硬件为GPIO1_IO03.

step-2: 根据硬件,实现设备树节点;参考之前设备树语法说明,具体思路如下。
按照这个思路,一个完整的LED设备树节点就实现完毕,再调整下顺序,设备树节点就如下所示。
/ {
usr_led {
compatible = "rmk,usr-led"; // 标签,用于驱动匹配
pinctrl-names = "default", "improve"; // 选择引脚的复用定义,对应后续的pinctrl-n标签,default表示默认选择
pinctrl-0 = <&pinctrl_gpio_led>;
pinctrl-1 = <&pinctrl_led_improve>;
led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; // 选择引脚的线号和逻辑电平定义
reg = <0x020c406c 0x04>,
<0x020e0068 0x04>,
<0x020e02f4 0x04>,
<0x0209c000 0x04>,
<0x0209c004 0x04>;
status = "okay";
};
}
pinctrl_gpio_led: gpio-leds {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x17059
>;
};
pinctrl_led_improve: led-improve {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x40017059
>;
};
对于上述设备树,扩展支持了多pinctrl切换、寄存器管理功能,这也是设备树开发中的常用属性。
注意:在应用中写入的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 led_hardware_init(struct led_data *chip)
{
u8 index;
int ret, status;
u32 regval;
u32 is_active;
struct platform_device *pdev = chip->pdev;
struct device_node *led_nd = pdev->dev.of_node;
// 1. 获取"led-gpios"指定的gpio属性
chip->led_desc = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(chip->led_desc)) {
dev_info(&pdev->dev, "devm_gpiod_get error!\n");
return PTR_ERR(chip->led_desc);
}
// 2. 获取"reg"指定的寄存器属性
for (index=0; index<5; index++) {
chip->io_reg[index] = of_iomap(led_nd, index);
if (!chip->io_reg[index]) {
ret = -ENOMEM;
goto err_iomap;
}
}
regval = readl(chip->io_reg[2]);
is_active = gpiod_is_active_low(chip->led_desc);
dev_info(&pdev->dev, "reg value:0x%x, active_low:%d\n", regval, is_active);
// 3. 获取"pinctrl-x"属性
chip->led_pinctrl = devm_pinctrl_get(&pdev->dev);
if (IS_ERR(chip->led_pinctrl)) {
dev_info(&pdev->dev, "[devm_pinctrl_get]failed\n");
ret = PTR_ERR(chip->led_pinctrl);
goto err_iomap;
}
chip->pinctrl_state[0] = pinctrl_lookup_state(chip->led_pinctrl, "default");
if (IS_ERR(chip->pinctrl_state[0])) {
dev_info(&pdev->dev, "[pinctrl_lookup_state]read pinctrl_state 0 failed!\n");
ret = PTR_ERR(chip->pinctrl_state[0]);
goto err_iomap;
}
chip->pinctrl_state[1] = pinctrl_lookup_state(chip->led_pinctrl, "improve");
if (IS_ERR(chip->pinctrl_state[1])) {
dev_info(&pdev->dev, "[pinctrl_lookup_state]read pinctrl_state 1 failed!\n");
ret = PTR_ERR(chip->pinctrl_state[1]);
goto err_iomap;
}
// 4. 获取platform match表中的私有数据
chip->init_data = of_device_get_match_data(&pdev->dev);
if (!chip->init_data) {
ret = -ENOMEM;
dev_info(&pdev->dev, "[of_device_get_match_data]read full, null!\n");
goto err_iomap;
}
status = *(chip->init_data);
gpiod_set_value(chip->led_desc, status);
dev_info(&pdev->dev, "[led_hardware_init]init success, status:%d\n", status);
return 0;
err_iomap:
for (index=0; index<5; index++) {
if (chip->io_reg[index])
iounmap(chip->io_reg[index]);
}
return ret;
}
// 读取LED状态
ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *f_pos)
{
int ret;
struct led_data *chip;
struct platform_device *pdev;
u8 status;
cnt = min_t(size_t, cnt, 1);
chip = (struct led_data *)filp->private_data;
pdev = chip->pdev;
// 从硬件中读取LED状态
status = gpiod_get_value_cansleep(chip->led_desc);
ret = copy_to_user(buf, &status, cnt);
if (ret) {
dev_err(&pdev->dev, "read failed!\n");
return -EFAULT;
}
return cnt;
}
// 向硬件中写入设置的LED状态
ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *f_pos)
{
int ret;
u8 data;
struct led_data *chip;
struct platform_device *pdev;
chip = (struct led_data *)filp->private_data;
pdev = chip->pdev;
if (cnt < 1) {
return -EINVAL;
}
ret = copy_from_user(&data, buf, 1);
if (ret) {
dev_err(&pdev->dev, "write failed!\n");
return -EFAULT;
}
// 字符'0'转换为0
if (data >= '0') {
data = data - '0';
}
// 更新LED硬件状态
led_hardware_set(chip, data);
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 *); // 向文件写入数据的函数
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); // 读取数据的迭代函数
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); // 写入数据的迭代函数
int (*iterate) (struct file *, struct dir_context *); // 遍历目录的函数
unsigned int (*poll) (struct file *, struct poll_table_struct *); // 用于轮询文件状态的函数
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 执行文件的 ioctl 操作的函数
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 执行兼容的 ioctl 操作的函数
int (*mmap) (struct file *, struct vm_area_struct *); // 用于内存映射文件的函数
int (*open) (struct inode *, struct file *); // 打开文件时调用的函数
int (*flush) (struct file *, fl_owner_t id); // 刷新文件数据的函数
int (*release) (struct inode *, struct file *); // 关闭文件时调用的函数
int (*fsync) (struct file *, loff_t, loff_t, int datasync); // 同步文件数据的函数
int (*aio_fsync) (struct kiocb *, int datasync); // 异步同步文件数据的函数
int (*fasync) (int, struct file *, int); // 异步通知的函数
int (*lock) (struct file *, int, struct file_lock *); // 锁定文件的函数
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); // 发送页面数据的函数
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); // 获取未映射区域的函数
int (*check_flags)(int); // 检查文件标志的函数
int (*flock) (struct file *, int, struct file_lock *); // 文件锁操作的函数
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); // 写入数据到管道的函数
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); // 从管道读取数据的函数
int (*setlease)(struct file *, long, struct file_lock **); // 设置文件租约的函数
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); // 预分配文件空间的函数
void (*show_fdinfo)(struct seq_file *m, struct file *f); // 显示文件描述符信息的函数
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *); // 获取内存映射能力的函数(仅在没有 MMU 的情况下使用)
#endif
};
对于struct file_operations结构体,主要涉及的操作属性如下所示。
下面以beep的驱动实现进一步说明。
// 文件操作支持访问的接口
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.unlocked_ioctl = led_ioctl,
.release = led_release,
};
// open接口,一般用于获取硬件资源,后续代码使用
int led_open(struct inode *inode, struct file *filp)
{
struct led_data *chip;
// container_of函数,可通过结构体中结构变量地址获取所在对象地址
chip = container_of(inode->i_cdev, struct led_data, cdev);
filp->private_data = chip;
return 0;
}
//...
上面定义了创建struct cdev需要的硬件接口,下面就要实现如何创建设备。
// 创建字符设备
static int led_device_create(struct led_data *chip)
{
int ret;
int major, minor;
struct platform_device *pdev;
major = DEFAULT_MAJOR;
minor = DEFAULT_MINOR;
pdev = chip->pdev;
// 1.申请设备号
if (major) {
chip->dev_id = MKDEV(major, minor);
ret = register_chrdev_region(chip->dev_id, 1, DEVICE_NAME);
} else {
ret = alloc_chrdev_region(&chip->dev_id, 0, 1, DEVICE_NAME);
}
if (ret < 0) {
dev_err(&pdev->dev, "id alloc failed!\n");
goto exit;
}
// 2.创建字符设备,关联设备号,并添加到内核
cdev_init(&chip->cdev, &led_fops);
chip->cdev.owner = THIS_MODULE;
ret = cdev_add(&chip->cdev, chip->dev_id, 1);
if (ret) {
dev_err(&pdev->dev, "cdev add failed:%d!\n", ret);
goto exit_cdev_add;
}
// 3.创建设备类和设备文件,关联设备号,用于应用层访问
chip->class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(chip->class)) {
dev_err(&pdev->dev, "class create failed!\n");
ret = PTR_ERR(chip->class);
goto exit_class_create;
}
chip->device = device_create(chip->class, NULL, chip->dev_id, NULL, DEVICE_NAME);
if (IS_ERR(chip->device)) {
dev_err(&pdev->dev, "device create failed!\n");
ret = PTR_ERR(chip->device);
goto exit_device_create;
}
// 创建设备管理文件/sys/devices/platform/20c406c.usr_led/pinctrl_config
// cat info (status=1)
// echo 0 > /sys/devices/platform/20c406c.usr_led/pinctrl_config # 选择配置0
// echo 1 > /sys/devices/platform/20c406c.usr_led/pinctrl_config # 选择配置1
chip->led_attr.attr.name = "pinctrl_config";
chip->led_attr.attr.mode = 0644;
chip->led_attr.show = led_show;
chip->led_attr.store = led_store;
ret = device_create_file(&pdev->dev, &chip->led_attr);
if (ret != 0) {
dev_info(&pdev->dev, "device create file failed!\n");
goto exit_device_file_create;
}
// 创建类管理文件,创建/sys/class/leds/led0目录,其下brigntness文件可以控制gpio状态
//echo 0 > brightness #关闭LEDcd
//echo 1 > brightness #开启LED
chip->led.name = "led0";
chip->led.brightness_set = gpio_led_set;
chip->led.flags = LED_CORE_SUSPENDRESUME;
chip->led.max_brightness = 1;
chip->led.default_trigger = "default-on";
ret = devm_led_classdev_register(&pdev->dev, &chip->led);
if (ret != 0) {
dev_info(&pdev->dev, "device create led class failed!\n");
goto exit_classdev_register;
}
dev_info(&pdev->dev, "device create success!\n");
return 0;
exit_classdev_register:
device_remove_file(&pdev->dev, &chip->led_attr);
exit_device_file_create:
device_destroy(chip->class, chip->dev_id);
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 ret;
}
static int led_remove(struct platform_device *pdev)
{
struct led_data *chip = platform_get_drvdata(pdev);
// 删除管理文件
device_remove_file(&pdev->dev, &chip->led_attr);
// 删除设备
device_destroy(chip->class, chip->dev_id);
class_destroy(chip->class);
// 从内核中删除字符设备结构
cdev_del(&chip->cdev);
// 注销申请的设备号
unregister_chrdev_region(chip->dev_id, 1);
led_hardware_release(chip);
dev_info(&pdev->dev, "driver 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关联起来,从而实现应用层对于底层的访问。
上述是标准的字符设备的创建方式,不过如果不需要单独的主设备号,可以直接使用杂项设备创建,这样可以简化操作。
关于字符设备LED的详细代码见:src/file/ch03-03/beep/kernel_led.c。
sysfs是Linux内核中的一个虚拟文件系统;它提供了一种机制,用于将内核对象、驱动程序和设备的信息以文件和目录的形式呈现给用户空间。通过sysfs用户空间程序可以方便地与内核进行交互,获取设备信息、配置设备参数等。sysfs并不实际存储数据在磁盘上,而是在内存中维护数据结构,通过文件和目录的形式将这些数据暴露给用户空间,可以通过”ls /sys”目录查看,常用目录核功能说明如下。
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);
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 = 0666,
.show = led_show,
.store = led_store,
}
//具有相同功能的宏定义
struct kobj_attribute led_attr = __ATTR(led, 0666, 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)
杂项设备是属于字符设备,可以自动生成设备节点,主设备号相同,统一为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; /* 设备模式 */
};
对于这个结构,关键的参数类型如下所示。
关于杂项设备的创建,步骤和实现如下所示。
struct beep_data
{
/* gpio info */
int gpio;
int status;
/*device info*/
struct platform_device *pdev;
struct miscdevice misc_dev;
struct file_operations misc_fops;
};
//创建杂项设备接口
static int beep_device_create(struct beep_data *chip)
{
int result;
struct platform_device *pdev = chip->pdev;
chip->misc_fops.owner = THIS_MODULE;
chip->misc_fops.open = beep_open;
chip->misc_fops.read = beep_read;
chip->misc_fops.write = beep_write,
chip->misc_fops.unlocked_ioctl = beep_ioctl,
chip->misc_fops.release = beep_release,
chip->misc_dev.minor = MISCBEEP_MINOR;
chip->misc_dev.name = DEVICE_NAME;
chip->misc_dev.fops = &(chip->misc_fops);
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;
}
关于misc_fops中对应接口和LED上面操作基本一致,不过有个特殊说明,对于misc设备,在open执行时,filp->private_data默认会被设置为miscdevice指针;可通过此指针通过container_of找到全局变量地址。
代码中实现示例如下所示。
int beep_open(struct inode *inode, struct file *filp)
{
struct miscdevice *mdev;
struct beep_data *chip;
// 获取miscdevice指针
mdev = filp->private_data;
// 根据miscdevice指针,查找在全局设备中的地址
chip = container_of(mdev,
struct beep_data,
misc_dev);
filp->private_data = chip;
return 0;
}
在杂项设备创建后,即可在系统中创建对应的设备节点/dev/miscbeep,另外可通过/proc/misc获取系统当前支持的杂项设备。
# 获取系统的杂项设备
cat /proc/misc
杂项设备驱动代码可参考: src/file/ch03-03/miscbeep/kernel_miscbeep.c.
可以看到,杂项设备的注册和创建由接口misc_register即可完成,相对来说更简单,且不占用主设备号,节省资源。如果开发的设备不需要独立的主设备号,可使用此接口创建杂项设备。
另外,Linux也支持其它方式创建字符设备,如对于输入信号可以使用input子系统管理,这个在后面相应章节会说明。
至此,关于字符设备的框架讲述完成。无论是简单的I/O控制,如LED、BEEP,还是复杂的I2C、SPI设备,从原理上都由字符设备创建和硬件接口管理这两部分实现,按照这个思路,可以更清晰的理解字符型设备。
字符设备方法和杂项设备使用的接口函数不同,但都是在应用层实现设备文件,然后通过文件接口进行访问。不过还有其它方式进行应用层的访问管理,这里列出其它方式。
/dev目录下。大部分字符设备、杂项设备、input子系统、rtc设备创建接口都是使用该方式实现/sys/devices/platform/[节点]/目录下, 通过修改属性文件内容操作底层硬件(device_create_file)/sys/bus/iio/devices/iio:device[0/...]/[file]下,通过文件可以访问硬件这三类是大部分字符型设备的访问方式。不过字符型设备相关的知识不止于此,如SPI系统框架、I2C系统框架、input子系统等都属于字符设备的开发中涉及的技术,这些在后续章节后进一步说明。
前面都是讲解的如何实现将硬件加载到内核的驱动,但最终还是需要被软件访问的。驱动在加载成功后,会在系统中创建对应的文件,此时使用C标准文件接口即可进行对硬件的操作,具体接口如下所示。
// 打开设备文件,获得文件描述符
// open调用驱动中fops的open函数
int open(const char *pathname, int oflag,...);
// 从文件中读取数据
// open调用驱动中fops的read函数
ssize_t read(int fd, void * buf, size_t count);
// 向文件中写入数据
// open调用驱动中fops的write函数
ssize_t write (int fd, const void * buf, size_t count);
// 向设备文件中写入请求命令
// ioctl调用驱动中fops的ioctl函数
int ioctl(int fd, ind cmd, …);
// 关闭文件描述符
// open调用驱动中fops的close函数
int close(int fd);
以本例中的/dev/led为例,应用层访问代码如下。
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define DEVICE_LED "/dev/led"
int main(int argc, const char *argv[])
{
unsigned char val = 1;
int fd;
// 读取设备文件,获取句柄
fd = open(DEVICE_LED, O_RDWR | O_NDELAY);
if (fd == -1) {
printf("open %s error\r\n", DEVICE_LED);
return -1;
}
if (argc > 1) {
val = atoi(argv[1]);
}
// 写入数据
write(fd, &val, 1);
// 关闭文件
close(fd);
exit(0);
}
应用层访问字符设备实现可参考如下:src/file/ch03-03/led/test/led_test.c.
本节主要讲解了字符设备的基本概念和实现,包括字符设备接口,硬件接口和访问接口,同时也实现了应用层访问的接口。
在应用层中我们关心的是/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外部器件,其基础框架都是类似的;只是具体的操作接口和硬件资源不同。