LV06-04-linux设备模型-07-注册设备到总线
前面已经可以注册自定义总线了,现在来看看怎么在总线上注册一个设备?若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
kernel/git/stable/linux.git - Linux kernel stable tree | linux kernel源码(官网,tag 4.19.71) | |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、设备
其实下面这些大部分在《LV06-04-linux设备模型-01-设备模型简介.md》这一节中都已经大概了解过了,这里简单回顾一下吧。
1. 设备简介
驱动开发的过程中,我们最关心的莫过于设备以及对应的驱动了。我们编写驱动的目的,最终就是为了使设备可以正常工作。在Linux中,一切都是以文件的形式存在, 设备也不例外。/sys/devices目录记录了系统中所有设备,实际上在sys目录下所有设备文件最终都会指向该目录对应的设备文件;此外还有另一个目录/sys/dev记录所有的设备节点, 但实际上都是些链接文件,同样指向了devices目录下的文件。
1.1 struct device
在内核使用device结构体来描述我们的物理设备,这个结构体定义在 device.h - include/linux/device.h - struct device:
1 | struct device { |
- parent :表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;
- init_name :指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;
- bus :表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。
- platform_data :特定设备的私有数据,通常定义在板级文件中;
- driver_data :同上,驱动层可通过dev_set/get_drvdata函数来获取该成员;
- of_node :存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后,将匹配的节点保存到该变量。
- devt :dev_t类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys目录中导出对应的设备。
- class :指向了该设备对应类,开篇我们提到的触摸,鼠标以及键盘等设备,对于计算机而言,他们都具有相同的功能,都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备,如input、leds、pwm等目录;
- group :指向struct attribute_group类型的指针,指定该设备的属性;
- release :回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。
1.2 设备的注册与注销
1.2.1 device_register()
device_register() 定义如下:
1 | int device_register(struct device *dev) |
参数:
- dev : struct device 结构体类型指针。
返回值:
- 成功: 0
- 失败: 负数
1.2.2 device_unregister()
device_unregister() 定义如下:
1 | void device_unregister(struct device *dev) |
参数:
- dev :struct device 结构体类型指针
返回值: 无
1.3 总结
当成功注册总线时,会在/sys/bus目录下创建对应总线的目录,该目录下有两个子目录,分别是drivers和devices, 我们使用device_register注册的设备从属于某个总线时,该总线的devices目录下便会存在该设备文件。
2. 设备属性文件
在开发单片机的时候,如果想要读取某个寄存器的值,我们可能需要加入一些新的代码,并重新编译。但对于Linux内核来讲,每次都需要编译一遍源码, 实在太浪费时间和精力了。为此,Linux提供以下接口,来注册和注销一个设备属性文件。我们可以通过这些接口直接在用户层进行查询/修改,避免了重新编译内核的麻烦。
2.1 DEVICE_ATTR()
DEVICE_ATTR()是一个宏,它定义在device.h - include/linux/device.h - DEVICE_ATTR:
1 |
struct device_attribute定义在 device.h - include/linux/device.h - struct device_attribute
1 | /* interface for exporting device attributes */ |
- DEVICE_ATTR宏:定义用于定义一个device_attribute类型的变量,##表示将##左右两边的标签拼接在一起,因此, 我们得到变量的名称应该是带有dev_attr_前缀的。该宏定义需要传入四个参数_name,_mode,_show,_store,分别代表了文件名, 文件权限,show回调函数,store回调函数。show回调函数以及store回调函数分别对应着用户层的cat和echo命令, 当我们使用cat命令,来获取/sys目录下某个文件时,最终会执行show回调函数;使用echo命令,则会执行store回调函数。 参数_mode的值,可以使用S_IRUSR、S_IWUSR、S_IXUSR等宏定义。
2.2 device_create_file()
device_create_file()函数,声明在device.h - include/linux/device.h - device_create_file
1 | extern int device_create_file(struct device *device, |
- **device_create_file()**函数用于创建文件,它有两个参数成员,第一个参数表示的是设备,前面学习device结构体时,其成员中有个bus_type变量, 用于指定设备挂载在某个总线上,并且会在总线的devices子目录创建一个属于该设备的目录,device参数可以理解为在哪个设备目录下,创建设备文件。 第二个参数则是我们自己定义的device_attribute类型变量。
2.3 device_remove_file()
device_remove_file()函数,声明在 device.h - include/linux/device.h - device_remove_file:
1 | extern void device_remove_file(struct device *dev, |
- device_remove_file() 函数用于删除文件,当我们的驱动注销时,对应目录以及文件都需要被移除。 其参数和device_create_file函数的参数是一样。
2.4 使用实例
在调用device_create_file() 函数之前,需要先定义好属性结构体 struct device_attribute ,并将其相关字段填充好。有两种方式,一种是直接定义:
1 | struct device_attribute sdev_attr_data_var = { |
另一种是通过宏来定义:
1 | // 下面这个宏可以简化属性变量的定义,从定义可以看出,这里定义的属性的变量前缀是 dev_attr_,后面要再拼接name才行 |
show和store函数实现如下:
1 | typedef struct __SDEV_ATTR_VAR_{ |
3. 设备注册到总线demo
这里会涉及到两个内核模块的编译,一个是设备的内核模块,一个是总线的内核模块。
3.1 demo源码
源码可以看这里,这里有两个,一个是只创建设备,另一个还为设备添加了属性文件操作。
05_device_model/13_device_register · 苏木/imx6ull-driver-demo - 码云 - 开源中国
05_device_model/14_device_register_with_attr · 苏木/imx6ull-driver-demo - 码云 - 开源中国
点击查看详情
这里直接已加了属性的demo为例 05_device_model/14_device_register_with_attr/sdevice_demo.c · 苏木/imx6ull-driver-demo - 码云 - 开源中国:
1 |
|
3.2 开发板测试
我们编译完,把对对应的驱动拷贝到开发板中,按以下步骤测试效果。
- (1)一开始的 /sys/bus 目录 和 /sys/devices 目录
- (2)加载模块
加载模块后,会发现,在/sys/bus目录生成了 sbus目录,并且/sys/bus/sbus/devices 目录下也生成了指向设备的软链接,我们注册的设备可以在 /sys/devices 中看到。
- (3)查看/修改sdev_attr_data的值
总线的属性,之前已经修改和验证过了,这里只看一下设备的属性文件,其实操作都是一样的:
- (5)卸载模块
可以看到,卸载模块后,资源释放,sbus目录被删除,相关的属性文件也都删除了,/sys/devices 中的sdev目录也删除了。
二、设备怎么注册的?
那肯定是从 device_register() 函数开始了。
1. device_register()
device_register() 函数定义如下:
1 | int device_register(struct device *dev) |
该函数用于注册设备到内核中。函数接受 一个指向 struct device 类型的设备对象指针作为参数。首先,代码调用 device_initialize() 函数对 设备对象进行初始化。接下来,代码调用 device_add() 函数将设备添加到内核中。
device_add() 函数会将设备添加到设备总线的设备列表中,并执行与设备添加相关的操作,例如分配设备号、 创建设备节点等。最后,函数返回设备添加的结果,通常是一个整数值表示成功或失败的状态码。
2. device_initialize()
device_initialize() 函数定义如下:
1 | void device_initialize(struct device *dev) |
该函数用于对设备对象进行初始化。函数 接收一个指向 struct device 类型的设备对象指针作为参数。
1 | dev->kobj.kset = devices_kset; |
代码将设备对象的 kobj.kset 成员设置为 devices_kset,表示该设备对象所属的 kset 为 devices_kset,即设备对象属于 devices 子系统。
1 | kobject_init(&dev->kobj, &device_ktype); |
代码调用 kobject_init() 函数初始化设备对象的 kobj 成员,使用 device_ktype 作为 ktype。通过这个函数调用,设备对象的 kobject 被正确地初始化和设置。
1 | INIT_LIST_HEAD(&dev->dma_pools); |
代码使用 INIT_LIST_HEAD 宏初始化设备对象的 dma_pools链表头,以确保它为空链表。
1 | mutex_init(&dev->mutex); |
代码接着调用 mutex_init()函数初始化设备对象的 mutex 互斥锁,用于对设备进行互斥操作。
1 | lockdep_set_novalidate_class(&dev->mutex); |
通过 lockdep_set_novalidate_class() 函数,设置 dev->mutex 的验证类别为无效,以避免死锁分析 器对该互斥锁的验证。
1 | spin_lock_init(&dev->devres_lock); |
调用 spin_lock_init() 函数初始化设备对象的 devres_lock 自旋锁,用于对设备 资源进行保护。通
1 | INIT_LIST_HEAD(&dev->devres_head); |
过 INIT_LIST_HEAD 宏初始化设备对象的 devres_head 链表头,以确保它为空链表。
1 | device_pm_init(dev); |
调用 device_pm_init() 函数初始化设备对象的电源管理相关信息。
1 | set_dev_node(dev, -1); |
代码使用 set_dev_node() 函数将设备对象的设备节点设置为 -1,表示没有指定设备节点。
1 |
|
在#ifdef CONFIG_GENERIC_MSI_IRQ 条件编译块内,代码使用 INIT_LIST_HEAD 宏初始化设备对象的 msi_list 链表头,用于管理设备的 MSI(消息信号中断)信息。
1 | INIT_LIST_HEAD(&dev->links.consumers); |
代码使用 INIT_LIST_HEAD 宏初始化设备对象的 consumers、suppliers等链表头,用于管理设备间的连接关系。
1 | dev->links.status = DL_DEV_NO_DRIVER; |
代码将设备对象的 status 成员设置为 DL_DEV_NO_DRIVER ,表示设备当前没有驱动程序。
3. device_add()
device_add() 函数定义如下:
1 | int device_add(struct device *dev) |
1 | struct device *parent; |
初始化了函数中使用的一些局部变量。
- 第 1839 行
1 | dev = get_device(dev); |
调用 get_device()函数以获取设备的引用。
- 第 1843 行
1 | if (!dev->p) { |
如果设备结构体的 p 成员为空,那么调用 device_private_init() 函数进行设备的私有初始化。
- 第 1854 行
1 | /* |
如果设备的 init_name 成员非空,那么使用 dev_set_name() 函数将其作为设备的名称,并将 init_name 设置为空。
1 | /* subsystems can specify simple device enumeration */ |
如果设备的名称为空且设备的总线(bus)和总线的名称(dev_name)非空,那么使用总线名称 和设备 ID 设置设备的名称。接着,检查一下设备名称,如果设备的名称为空,那么设置错误码为-EINVAL,并跳转到 name_error 标签处。
1 | pr_debug("device: '%s': %s\n", dev_name(dev), __func__); |
打印调试信息,包括设备的名称和函数名。获取设备的父设备,并设置设备的父对象。如果获取父对象的过程中发生错误,那么将错误码设为获取父对象的返回值,并跳转到 parent_error 标签处。
- 第 1879 行
1 | /* use parent numa_node */ |
如果设备有父设备且设备的节点号为 NUMA_NO_NODE,那么将设备的节点号设为父设备 的节点号。
- 第 1885 行
1 | /* first, register with generic layer. */ |
使用 kobject_add() 函数将设备的内核对象添加到内核对象层次结构中。如果添加过程中发 生错误,那么获取设备的”粘合目录”(glue_dir)并跳转到 Error 标签处。
- 第 1892 行
1 | /* notify platform of device entry */ |
如果存在平台通知函数(platform_notify),则调用该函数通知平台设备已添加。
1 | error = device_create_file(dev, &dev_attr_uevent); |
创建设备的 uevent 属性文件。添加设备的类符号链接。添加设备的属性。添加设备到总 线。添加设备电源管理相关的 sysfs 接口。启动设备接下来的代码主要是处理设备的设备号 (devt)相关操作,以及通知相关组件设备添加的过程。
后面就不再详细分析了。
4. bus_add_device()
bus_add_device() 函数定义如下:
1 | /** |
通过设备的 bus 字段获取设备所属的总线类型(bus_type)的指针。 这个函数会增加总线的引用计数,确保总线在设备添加过程中不会被释放。
检查总线类型指针是否有效。
打印调试信息,包括 总线名称和设备名称,以便跟踪设备添加的过程。
将设备添加到总线类型的设备组(dev_groups) 中。设备组是一组属性文件,用于在设备的 sysfs 目录中显示和设置设备的属性。
在总线类型 的设备集(devices_kset)的内核对象(kobj)下创建设备的符号链接。这个符号链接将设备的 sysfs 目录链接到总线类型的设备集目录中。
在设备的内核对 象(kobj)下创建指向总线类型子系统(subsystem)的符号链接。这个符号链接将设备的 sysfs 目录链接到总线类型子系统的目录中。
将设备的节点添加到总线类 型的设备列表中。这个步骤用于维护总线类型下的设备列表。