Linux-设备驱动开发-全-
Linux 设备驱动开发(全)
原文:
zh.annas-archive.org/md5/1581478CA24960976F4232EF07514A3E译者:飞龙
前言
Linux 内核是一款复杂、可移植、模块化且广泛使用的软件,约 80%的服务器和超过一半的全球嵌入式系统都在运行该软件。设备驱动程序在 Linux 系统性能方面起着至关重要的作用。随着 Linux 成为最受欢迎的操作系统之一,对于开发个人设备驱动程序的兴趣也在稳步增长。
设备驱动程序是用户空间和设备之间的链接,通过内核。
本书将从两章开始,帮助您了解驱动程序的基础知识,并为您在 Linux 内核中的漫长旅程做好准备。本书还将涵盖基于 Linux 子系统的驱动程序开发,如内存管理、PWM、RTC、IIO、GPIO、中断请求管理。本书还将涵盖直接内存访问和网络设备驱动程序的实际方法。
本书中的源代码已在 x86 PC 和基于 NXP 的 ARM i.MX6 的 SECO UDOO Quad 上进行了测试,具有足够的功能和连接,可以覆盖本书中讨论的所有测试。还提供了一些驱动程序用于测试廉价组件,如 MCP23016 和 24LC512,它们分别是 I2C GPIO 控制器和 EEPROM 存储器。
通过本书的学习,您将能够熟悉设备驱动程序开发的概念,并能够使用最新的内核版本(写作时为 v4.13)从头开始编写任何设备驱动程序。
本书涵盖的内容
第一章,内核开发简介,介绍了 Linux 内核开发过程。本章将讨论下载、配置和编译内核的步骤,适用于 x86 和基于 ARM 的系统。
第二章,设备驱动程序基础,通过内核模块介绍了 Linux 的模块化,并描述了它们的加载/卸载。还描述了驱动程序架构和一些基本概念以及一些内核最佳实践。
第三章,内核设施和辅助函数,介绍了经常使用的内核函数和机制,如工作队列、等待队列、互斥锁、自旋锁,以及其他对于改进驱动程序可靠性有用的设施。
第四章,字符设备驱动程序,侧重于通过字符设备将设备功能导出到用户空间,并使用 IOCTL 接口支持自定义命令。
第五章,平台设备驱动程序,解释了什么是平台设备,并介绍了伪平台总线的概念,以及设备和总线匹配机制。本章以一般方式描述了平台驱动程序架构,以及如何处理平台数据。
第六章,设备树的概念,讨论了向内核提供设备描述的机制。本章解释了设备寻址、资源处理、设备树中支持的每种数据类型及其内核 API。
第七章,I2C 客户端驱动程序,深入探讨了 I2C 设备驱动程序架构、数据结构以及总线上的设备寻址和访问方法。
第八章,SPI 设备驱动程序,描述了基于 SPI 的设备驱动程序架构,以及涉及的数据结构。本章讨论了每个设备的访问方法和具体特性,以及应该避免的陷阱。还讨论了 SPI DT 绑定。
第九章,Regmap API - 寄存器映射抽象,概述了 regmap API 以及它如何抽象底层的 SPI 和 I2C 事务。本章描述了通用 API 以及专用 API。
第十章,IIO 框架,介绍了内核数据采集和测量框架,用于处理数字模拟转换器(DAC)和模拟数字转换器(ADC)。本章介绍了 IIO API,涉及触发缓冲区和连续数据捕获,并介绍了通过 sysfs 接口进行单通道采集。
第十一章,内核内存管理,首先介绍了虚拟内存的概念,以描述整个内核内存布局。本章介绍了内核内存管理子系统,讨论了内存分配和映射,它们的 API 以及涉及这些机制的所有设备,以及内核缓存机制。
第十二章,DMA - 直接内存访问,介绍了 DMA 及其新的内核 API:DMA 引擎 API。本章将讨论不同的 DMA 映射,并描述如何解决缓存一致性问题。此外,本章还总结了基于 NXP 的 i.MX6 SoC 的使用案例中使用的所有概念。
第十三章,Linux 设备模型,概述了 Linux 的核心,描述了内核中对象的表示方式,以及 Linux 是如何设计的,从 kobject 到设备,通过总线、类和设备驱动程序。本章还突出了用户空间中不为人知的一面,即 sysfs 中的内核对象层次结构。
第十四章,引脚控制和 GPIO 子系统,描述了内核引脚控制 API 和 GPIOLIB,这是处理 GPIO 的内核 API。本章还讨论了旧的和已弃用的基于整数的 GPIO 接口,以及基于描述符的接口,这是新的接口,最后讨论了它们如何在设备树中进行配置。
第十五章,GPIO 控制器驱动程序 - gpio_chip,编写此类设备驱动程序所需的必要元素。也就是说,它的主要数据结构是 struct gpio_chip。本章详细解释了这个结构,以及书籍源代码中提供的完整可用的驱动程序。
第十六章,高级中断请求(IRQ)管理,揭开了 Linux IRQ 核心的神秘面纱。本章介绍了 Linux IRQ 管理,从系统中断传播开始,移动到中断控制器驱动程序,因此解释了 IRQ 多路复用的概念,使用 Linux IRQ 域 API。
第十七章,输入设备驱动程序,提供了输入子系统的全局视图,处理基于 IRQ 和轮询的输入设备,并介绍了两种 API。本章解释并展示了用户空间代码如何处理这些设备。
第十八章,RTC 驱动程序,深入讲解了 RTC 子系统及其 API。本章还详细解释了如何在 RTC 驱动程序中处理闹钟。
第十九章,PWM 驱动程序,全面描述了 PWM 框架,讨论了控制器端 API 和消费者端 API。本章最后一节讨论了来自用户空间的 PWM 管理。
第二十章,调节器框架,突出了电源管理的重要性。本章的第一部分涉及电源管理 IC(PMIC),并解释了其驱动程序设计和 API。第二部分侧重于消费者方面,讨论了请求和使用调节器。
第二十一章,帧缓冲驱动程序,解释了帧缓冲的概念及其工作原理。它还展示了如何设计帧缓冲驱动程序,介绍了其 API,并讨论了加速和非加速方法。本章展示了驱动程序如何公开帧缓冲内存,以便用户空间可以在其中写入,而不必担心底层任务。
第二十二章,网络接口卡驱动程序,介绍了 NIC 驱动程序的架构及其数据结构,从而向您展示如何处理设备配置、数据传输和套接字缓冲区。
本书所需的内容
本书假定读者对 Linux 操作系统有中等水平的理解,对 C 编程有基本的知识(至少要能处理指针)。就是这样。如果某一章需要额外的技能,文档中会提供链接,帮助读者快速学习这些技能。
Linux 内核编译是一个相当长而繁重的任务。最低硬件或虚拟要求如下:
-
CPU:4 核
-
内存:4 GB RAM
-
免费磁盘空间:5 GB(足够大)
在本书中,您将需要以下软件清单:
-
Linux 操作系统:最好是基于 Debian 的发行版,例如本书中使用的 Ubuntu 16.04
-
至少需要 gcc 和 gcc-arm-linux 的 5 版本(在书中使用)
其他必要的软件包在书中的专用章节中有描述。需要互联网连接以下载内核源代码。
本书适合对象
为了充分利用本书的内容,需要具备基本的 C 编程和基本的 Linux 命令知识。本书涵盖了广泛使用的嵌入式设备的 Linux 驱动程序开发,使用内核版本 v4.1,并覆盖了撰写本书时的最新版本的更改(v4.13)。本书主要面向嵌入式工程师、Linux 系统管理员、开发人员和内核黑客。无论您是软件开发人员、系统架构师还是愿意深入研究 Linux 驱动程序开发的制造商,本书都适合您。
约定
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“.name字段必须与您在特定文件中注册设备时给出的设备名称相同”。
代码块设置如下:
#include <linux/of.h>
#include <linux/of_device.h>
任何命令行输入或输出都以以下方式编写:
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)
新术语和重要单词以粗体显示。
警告或重要说明显示如下。
提示和技巧显示如下。
读者反馈
我们始终欢迎读者的反馈。让我们知道您对本书的看法-您喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出您真正能充分利用的标题。要向我们发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在主题中提及书名。如果您在某个主题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的自豪所有者,我们有很多东西可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。您可以按照以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的“支持”选项卡上。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名。
-
选择您要下载代码文件的书籍。
-
从下拉菜单中选择您购买本书的地方。
-
单击“代码下载”。
下载文件后,请确保使用最新版本解压文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Linux-Device-Drivers-Development。我们还有其他丰富的图书和视频代码包,可在github.com/PacktPublishing/上找到。去看看吧!
下载本书的彩色图片
我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/LinuxDeviceDriversDevelopment_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书标题的勘误部分下的任何现有勘误列表中。要查看以前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将出现在勘误部分下。
盗版
互联网上盗版受版权保护的材料是一个持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。请通过copyright@packtpub.com与我们联系,并附上涉嫌盗版材料的链接。感谢您帮助我们保护我们的作者和我们为您提供有价值内容的能力。
问题
如果您对本书的任何方面有问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。
第一章:内核开发简介
Linux 是 1991 年芬兰学生 Linus Torvalds 的一个业余项目。该项目逐渐增长,现在仍在增长,全球大约有 1000 名贡献者。如今,Linux 在嵌入式系统和服务器上都是必不可少的。内核是操作系统的核心部分,它的开发并不那么明显。
Linux 相对于其他操作系统有很多优势:
-
免费
-
有着完善的文档和庞大的社区
-
在不同平台上可移植
-
提供对源代码的访问
-
大量免费开源软件
这本书试图尽可能通用。有一个特殊的主题,设备树,它还不是完全的 x86 特性。这个主题将专门用于 ARM 处理器,以及所有完全支持设备树的处理器。为什么选择这些架构?因为它们在台式机和服务器(对于 x86)以及嵌入式系统(ARM)上最常用。
本章主要涉及以下内容:
-
开发环境设置
-
获取、配置和构建内核源代码
-
内核源代码组织
-
内核编码风格简介
环境设置
在开始任何开发之前,你需要设置一个环境。至少在基于 Debian 的系统上,专门用于 Linux 开发的环境是相当简单的:
$ sudo apt-get update
$ sudo apt-get install gawk wget git diffstat unzip texinfo \
gcc-multilib build-essential chrpath socat libsdl1.2-dev \
xterm ncurses-dev lzop
本书中的一些代码部分与 ARM系统芯片(SoC)兼容。你也应该安装gcc-arm:
sudo apt-get install gcc-arm-linux-gnueabihf
我正在一台 ASUS RoG 上运行 Ubuntu 16.04,配备英特尔 i7 处理器(8 个物理核心),16GB 内存,256GB 固态硬盘和 1TB 磁性硬盘。我的最爱编辑器是 Vim,但你可以自由选择你最熟悉的编辑器。
获取源代码
在早期的内核版本(直到 2003 年),使用了奇数-偶数版本样式;奇数版本是稳定的,偶数版本是不稳定的。当 2.6 版本发布时,版本方案切换为 X.Y.Z,其中:
-
X:这是实际内核的版本,也称为主要版本,当有不兼容的 API 更改时会增加。 -
Y:这是次要修订版本,当以向后兼容的方式添加功能时增加。 -
Z:这也被称为 PATCH,表示与错误修复相关的版本
这被称为语义版本控制,一直使用到 2.6.39 版本;当 Linus Torvalds 决定将版本号提升到 3.0 时,这也意味着 2011 年语义版本控制的结束,然后采用了 X.Y 方案。
当到了 3.20 版本时,Linus 认为他不能再增加 Y 了,并决定切换到任意的版本方案,当 Y 变得足够大以至于他数不过来时,就增加 X。这就是为什么版本从 3.20 直接变成了 4.0 的原因。请看:plus.google.com/+LinusTorvalds/posts/jmtzzLiiejc。
现在内核使用任意的 X.Y 版本方案,与语义版本控制无关。
源代码组织
对于本书的需求,你必须使用 Linus Torvald 的 Github 存储库。
git clone https://github.com/torvalds/linux
git checkout v4.1
ls
-
arch/:Linux 内核是一个快速增长的项目,支持越来越多的架构。也就是说,内核希望尽可能地通用。架构特定的代码与其他代码分开,并放在这个目录中。该目录包含处理器特定的子目录,如alpha/,arm/,mips/,blackfin/等。 -
block/:这个目录包含块存储设备的代码,实际上是调度算法。 -
crypto/:这个目录包含加密 API 和加密算法代码。 -
Documentation/:这应该是你最喜欢的目录。它包含了用于不同内核框架和子系统的 API 描述。在向论坛提问之前,你应该先在这里查找。 -
drivers/:这是最重的目录,随着设备驱动程序的合并而不断增长。它包含各种子目录中组织的每个设备驱动程序。 -
fs/:此目录包含内核实际支持的不同文件系统的实现,如 NTFS,FAT,ETX{2,3,4},sysfs,procfs,NFS 等。 -
include/:这包含内核头文件。 -
init/:此目录包含初始化和启动代码。 -
ipc/:这包含进程间通信(IPC)机制的实现,如消息队列,信号量和共享内存。 -
kernel/:此目录包含基本内核的与体系结构无关的部分。 -
lib/:库例程和一些辅助函数位于此处。它们是:通用内核对象(kobject)处理程序和循环冗余码(CRC)计算函数等。 -
mm/:这包含内存管理代码。 -
net/:这包含网络(无论是什么类型的网络)协议代码。 -
scripts/:这包含内核开发期间使用的脚本和工具。这里还有其他有用的工具。 -
security/:此目录包含安全框架代码。 -
sound/:音频子系统代码位于此处。 -
usr/:目前包含 initramfs 实现。
内核必须保持可移植性。任何特定于体系结构的代码应位于arch目录中。当然,与用户空间 API 相关的内核代码不会改变(系统调用,/proc,/sys),因为这会破坏现有的程序。
该书涉及内核 4.1 版本。因此,任何更改直到 v4.11 版本都会被覆盖,至少可以这样说关于框架和子系统。
内核配置
Linux 内核是一个基于 makefile 的项目,具有数千个选项和驱动程序。要配置内核,可以使用make menuconfig进行基于 ncurse 的界面,或者使用make xconfig进行基于 X 的界面。一旦选择,选项将存储在源树的根目录中的.config文件中。
在大多数情况下,不需要从头开始配置。在每个arch目录中都有默认和有用的配置文件,可以用作起点:
ls arch/<you_arch>/configs/
对于基于 ARM 的 CPU,这些配置文件位于arch/arm/configs/中,对于 i.MX6 处理器,默认文件配置为arch/arm/configs/imx_v6_v7_defconfig。同样,对于 x86 处理器,我们在arch/x86/configs/中找到文件,只有两个默认配置文件,i386_defconfig和x86_64_defconfig,分别用于 32 位和 64 位版本。对于 x86 系统来说,这是非常简单的:
make x86_64_defconfig
make zImage -j16
make modules
makeINSTALL_MOD_PATH </where/to/install> modules_install
给定一个基于 i.MX6 的板,可以从ARCH=arm make imx_v6_v7_defconfig开始,然后ARCH=arm make menuconfig。使用前一个命令,您将把默认选项存储在.config文件中,使用后一个命令,您可以根据需要更新添加/删除选项。
在使用xconfig时可能会遇到 Qt4 错误。在这种情况下,应该使用以下命令:
sudo apt-get install qt4-dev-tools qt4-qmake
构建您的内核
构建内核需要您指定为其构建的体系结构,以及编译器。也就是说,对于本地构建并非必需。
ARCH=arm make imx_v6_v7_defconfig
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make zImage -j16
之后,将看到类似以下内容:
[...]
LZO arch/arm/boot/compressed/piggy_data
CC arch/arm/boot/compressed/misc.o
CC arch/arm/boot/compressed/decompress.o
CC arch/arm/boot/compressed/string.o
SHIPPED arch/arm/boot/compressed/hyp-stub.S
SHIPPED arch/arm/boot/compressed/lib1funcs.S
SHIPPED arch/arm/boot/compressed/ashldi3.S
SHIPPED arch/arm/boot/compressed/bswapsdi2.S
AS arch/arm/boot/compressed/hyp-stub.o
AS arch/arm/boot/compressed/lib1funcs.o
AS arch/arm/boot/compressed/ashldi3.o
AS arch/arm/boot/compressed/bswapsdi2.o
AS arch/arm/boot/compressed/piggy.o
LD arch/arm/boot/compressed/vmlinux
OBJCOPY arch/arm/boot/zImage
Kernel: arch/arm/boot/zImage is ready
从内核构建中,结果将是一个单一的二进制映像,位于arch/arm/boot/中。模块使用以下命令构建:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules
您可以使用以下命令安装它们:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules_install
modules_install目标需要一个环境变量INSTALL_MOD_PATH,指定应该在哪里安装模块。如果未设置,模块将安装在/lib/modules/$(KERNELRELEASE)/kernel/中。这在第二章 设备驱动程序基础中讨论过。
i.MX6 处理器支持设备树,这是用来描述硬件的文件(这在第六章中详细讨论),但是,要编译每个ARCH设备树,可以运行以下命令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
但是,并非所有支持设备树的平台都支持dtbs选项。要构建一个独立的 DTB,您应该使用:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6d- sabrelite.dtb
内核习惯
内核代码试图遵循标准规则。在本章中,我们只是介绍它们。它们都在专门的章节中讨论,从第三章开始,内核设施和辅助函数,我们可以更好地了解内核开发过程和技巧,直到第十三章,Linux 设备模型。
编码风格
在深入研究本节之前,您应始终参考内核编码风格手册,位于内核源树中的Documentation/CodingStyle。这种编码风格是一组规则,您至少应该遵守这些规则,如果需要内核开发人员接受其补丁。其中一些规则涉及缩进、程序流程、命名约定等。
最流行的是:
- 始终使用 8 个字符的制表符缩进,并且每行应为 80 列长。如果缩进阻止您编写函数,那是因为该函数的嵌套级别太多。可以使用内核源代码中的
scripts/cleanfile脚本调整制表符大小并验证行大小:
scripts/cleanfile my_module.c
- 您还可以使用
indent工具正确缩进代码:
sudo apt-get install indent
scripts/Lindent my_module.c
-
每个未导出的函数/变量都应声明为静态的。
-
在括号表达式(内部)周围不应添加空格。s = size of (struct file);是可以接受的,而s = size of( struct file );是不可以接受的。
-
禁止使用
typdefs。 -
始终使用
/* this */注释样式,而不是// this -
- 不好:
// 请不要使用这个
- 不好:
-
好的:
/* 内核开发人员喜欢这样 */ -
宏应该大写,但功能宏可以小写。
-
注释不应该替换不可读的代码。最好重写代码,而不是添加注释。
内核结构分配/初始化
内核始终为其数据结构和设施提供两种可能的分配机制。
其中一些结构包括:
-
工作队列
-
列表
-
等待队列
-
Tasklet
-
定时器
-
完成
-
互斥锁
-
自旋锁
动态初始化器都是宏,这意味着它们始终大写:INIT_LIST_HEAD(),DECLARE_WAIT_QUEUE_HEAD(),DECLARE_TASKLET()等等。
说到这一点,所有这些都在第三章中讨论,内核设施和辅助函数。因此,代表框架设备的数据结构始终是动态分配的,每个数据结构都有自己的分配和释放 API。这些框架设备类型包括:
-
网络
-
输入设备
-
字符设备
-
IIO 设备
-
类
-
帧缓冲
-
调节器
-
PWM 设备
-
RTC
静态对象的作用域在整个驱动程序中可见,并且由此驱动程序管理的每个设备都可见。动态分配的对象仅由实际使用给定模块实例的设备可见。
类、对象和 OOP
内核通过设备和类来实现 OOP。内核子系统通过类进行抽象。几乎每个子系统都有一个/sys/class/下的目录。struct kobject结构是这种实现的核心。它甚至带有一个引用计数器,以便内核可以知道实际使用对象的用户数量。每个对象都有一个父对象,并且在sysfs中有一个条目(如果已挂载)。
每个属于特定子系统的设备都有一个指向操作(ops)结构的指针,该结构公开了可以在此设备上执行的操作。
摘要
本章以非常简短和简单的方式解释了如何下载 Linux 源代码并进行第一次构建。它还涉及一些常见概念。也就是说,这一章非常简短,可能不够,但没关系,这只是一个介绍。这就是为什么下一章会更深入地介绍内核构建过程,如何实际编译驱动程序,无论是作为外部模块还是作为内核的一部分,以及在开始内核开发这段漫长旅程之前应该学习的一些基础知识。
第二章:设备驱动程序基础
驱动程序是一种旨在控制和管理特定硬件设备的软件。因此得名设备驱动程序。从操作系统的角度来看,它可以在内核空间(以特权模式运行)或用户空间(权限较低)中。本书只涉及内核空间驱动程序,特别是 Linux 内核驱动程序。我们的定义是设备驱动程序向用户程序公开硬件的功能。
这本书的目的不是教你如何成为 Linux 大师——我自己也不是——但在编写设备驱动程序之前,你应该了解一些概念。C 编程技能是必需的;你至少应该熟悉指针。你还应该熟悉一些操作函数。还需要一些硬件技能。因此,本章主要讨论:
-
模块构建过程,以及它们的加载和卸载
-
驱动程序骨架和调试消息管理
-
驱动程序中的错误处理
用户空间和内核空间
内核空间和用户空间的概念有点抽象。这一切都与内存和访问权限有关。人们可能认为内核是特权的,而用户应用程序是受限制的。这是现代 CPU 的一个特性,允许它在特权或非特权模式下运行。这个概念在第十一章 内核内存管理中会更清楚。

用户空间和内核空间
前面的图介绍了内核空间和用户空间之间的分离,并强调了系统调用代表它们之间的桥梁(我们稍后在本章讨论这一点)。可以描述每个空间如下:
-
内核空间:这是内核托管和运行的一组地址。内核内存(或内核空间)是一段内存范围,由内核拥有,受到访问标志的保护,防止任何用户应用程序无意中干扰内核。另一方面,内核可以访问整个系统内存,因为它以更高的优先级在系统上运行。在内核模式下,CPU 可以访问整个内存(包括内核空间和用户空间)。
-
用户空间:这是正常程序(如 gedit 等)受限制运行的一组地址(位置)。你可以把它看作是一个沙盒或监狱,这样用户程序就不能干扰其他程序拥有的内存或其他资源。在用户模式下,CPU 只能访问带有用户空间访问权限标记的内存。用户应用程序运行的优先级较低。当进程执行系统调用时,会向内核发送软件中断,内核会打开特权模式,以便进程可以在内核空间中运行。当系统调用返回时,内核关闭特权模式,进程再次被限制。
模块的概念
模块对于 Linux 内核来说就像插件(Firefox 就是一个例子)对于用户软件一样。它动态扩展了内核的功能,甚至不需要重新启动计算机。大多数情况下,内核模块都是即插即用的。一旦插入,它们就可以被使用。为了支持模块,内核必须已经使用以下选项构建:
CONFIG_MODULES=y
模块依赖
在 Linux 中,模块可以提供函数或变量,并使用EXPORT_SYMBOL宏导出它们,使它们对其他模块可用。这些被称为符号。模块 B 对模块 A 的依赖是,模块 B 使用了模块 A 导出的符号之一。
depmod 实用程序
depmod 是在内核构建过程中运行的工具,用于生成模块依赖文件。它通过读取/lib/modules/<kernel_release>/中的每个模块来确定它应该导出哪些符号以及它需要哪些符号。该过程的结果被写入文件modules.dep,以及它的二进制版本modules.dep.bin。它是一种模块索引。
模块加载和卸载
要使模块运行,应该将其加载到内核中,可以使用insmod给定模块路径作为参数来实现,这是开发过程中首选的方法,也可以使用modprobe,这是一个聪明的命令,但在生产系统中更受欢迎。
手动加载
手动加载需要用户的干预,用户应该具有 root 访问权限。实现这一点的两种经典方法如下所述:
modprobe 和 insmod
在开发过程中,通常使用insmod来加载模块,并且应该给出要加载的模块的路径:
insmod /path/to/mydrv.ko
这是一种低级形式的模块加载,它构成了其他模块加载方法的基础,也是本书中我们将使用的方法。另一方面,有modprobe,主要由系统管理员或在生产系统中使用。modprobe是一个聪明的命令,它解析文件modules.dep以便先加载依赖项,然后再加载给定的模块。它自动处理模块依赖关系,就像软件包管理器一样:
modprobe mydrv
是否可以使用modprobe取决于depmod是否知道模块安装。
/etc/modules-load.d/.conf
如果您希望某个模块在启动时加载,只需创建文件/etc/modules-load.d/<filename>.conf,并添加应该加载的模块名称,每行一个。<filename>应该对您有意义,人们通常使用模块:/etc/modules-load.d/modules.conf。您可以根据需要创建多个.conf文件:
/etc/modules-load.d/mymodules.conf的一个例子如下:
#this line is a comment
uio
iwlwifi
自动加载
depmod实用程序不仅构建modules.dep和modules.dep.bin文件。它做的不仅仅是这些。当内核开发人员实际编写驱动程序时,他们确切地知道驱动程序将支持哪些硬件。然后他们负责为驱动程序提供所有受支持设备的产品和供应商 ID。depmod还处理模块文件以提取和收集这些信息,并生成一个modules.alias文件,位于/lib/modules/<kernel_release>/modules.alias,它将设备映射到它们的驱动程序:
modules.alias的摘录如下:
alias usb:v0403pFF1Cd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0403pFF18d*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0403pDAFFd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0403pDAFEd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0403pDAFDd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0403pDAFCd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio
alias usb:v0D8Cp0103d*dc*dsc*dp*ic*isc*ip*in* snd_usb_audio
alias usb:v*p*d*dc*dsc*dp*ic01isc03ip*in* snd_usb_audio
alias usb:v200Cp100Bd*dc*dsc*dp*ic*isc*ip*in* snd_usb_au
在这一步,您将需要一个用户空间热插拔代理(或设备管理器),通常是udev(或mdev),它将向内核注册,以便在新设备出现时得到通知。
内核通过发送设备的描述(pid、vid、class、device class、device subclass、interface 以及可能标识设备的所有其他信息)来通知,这些信息发送到热插拔守护程序,它再调用modprobe来处理这些信息。modprobe然后解析modules.alias文件以匹配与设备关联的驱动程序。在加载模块之前,modprobe将在module.dep中查找它的依赖项。如果找到任何依赖项,那么在加载相关模块之前将加载依赖项;否则,模块将直接加载。
模块卸载
卸载模块的常用命令是rmmod。应该优先使用此命令来卸载使用insmod命令加载的模块。应该将模块名称作为参数给出。模块卸载是一个内核功能,可以根据CONFIG_MODULE_UNLOAD配置选项的值来启用或禁用。如果没有此选项,将无法卸载任何模块。让我们启用模块卸载支持:
CONFIG_MODULE_UNLOAD=y
在运行时,内核将阻止卸载可能破坏事物的模块,即使有人要求这样做。这是因为内核保持对模块使用的引用计数,以便它知道模块是否实际上正在使用。如果内核认为移除模块是不安全的,它就不会这样做。显然,人们可以改变这种行为:
MODULE_FORCE_UNLOAD=y
为了强制模块卸载,应该在内核配置中设置前述选项:
rmmod -f mymodule
另一方面,以智能方式卸载模块的更高级命令是modeprobe -r,它会自动卸载未使用的依赖项:
modeprobe -r mymodule
正如你可能已经猜到的,这对开发人员来说是一个非常有帮助的选项。最后,可以使用以下命令检查模块是否已加载:
lsmod
驱动程序骨架
让我们考虑以下helloworld模块。它将成为本章其余部分工作的基础:
helloworld.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) {
pr_info("Hello world!\n");
return 0;
}
static void __exit helloworld_exit(void) {
pr_info("End of the world\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
模块入口和出口点
内核驱动程序都有入口和出口点:前者对应于模块加载时调用的函数(modprobe,insmod),后者是在模块卸载时执行的函数(在rmmod或modprobe -r中)。
我们都记得main()函数,它是每个以 C/C++编写的用户空间程序的入口点,当该函数返回时程序退出。对于内核模块,情况有所不同。入口点可以有任何你想要的名称,而不像用户空间程序在main()返回时退出,出口点是在另一个函数中定义的。你需要做的就是告诉内核哪些函数应该作为入口或出口点执行。实际的函数hellowolrd_init和hellowolrd_exit可以被赋予任何名称。实际上,唯一强制的是将它们标识为相应的加载和卸载函数,并将它们作为参数传递给module_init()和module_exit()宏。
总之,module_init()用于声明在加载模块(使用insmod或modprobe)时应调用的函数。初始化函数中所做的事情将定义模块的行为。module_exit()用于声明在卸载模块(使用rmmod)时应调用的函数。
无论是init函数还是exit函数,在模块加载或卸载后都只运行一次。
__init和__exit属性
__init和__exit实际上是内核宏,在include/linux/init.h中定义,如下所示:
#define __init__section(.init.text)
#define __exit__section(.exit.text)
__init关键字告诉链接器将代码放置在内核对象文件的一个专用部分中。这个部分对内核是预先知道的,并且在模块加载和init函数完成后被释放。这仅适用于内置驱动程序,而不适用于可加载模块。内核将在其引导序列期间首次运行驱动程序的初始化函数。
由于驱动程序无法卸载,其初始化函数直到下次重启之前都不会再次被调用。不再需要保留对其初始化函数的引用。对于__exit关键字也是一样,当模块被静态编译到内核中时,或者未启用模块卸载支持时,其对应的代码将被省略,因为在这两种情况下,exit函数永远不会被调用。__exit对可加载模块没有影响。
让我们花更多时间了解这些属性是如何工作的。这一切都关于名为可执行和可链接格式(ELF)的对象文件。一个 ELF 对象文件由各种命名的部分组成。其中一些是强制性的,并且构成了 ELF 标准的基础,但人们可以创造任何想要的部分,并让特殊程序使用它。这就是内核的做法。可以运行objdump -h module.ko来打印出构成给定module.ko内核模块的不同部分:

helloworld-params.ko 模块的部分列表
在标题中的部分中,只有少数是标准的 ELF 部分:
-
.text,也称为代码,其中包含程序代码 -
.data,其中包含初始化数据,也称为数据段 -
.rodata,用于只读数据 -
.评论 -
未初始化数据段,也称为 由符号开始的块(bss)
其他部分是根据内核目的的需求添加的。对于本章来说,最重要的是 .modeinfo 部分,它存储有关模块的信息,以及 .init.text 部分,它存储以 __init 宏为前缀的代码。
链接器(Linux 系统上的 ld )是 binutils 的一部分,负责将符号(数据、代码等)放置在生成的二进制文件的适当部分,以便在程序执行时由加载器处理。可以通过提供链接器脚本(称为 链接器定义文件(LDF)或 链接器定义脚本(LDS))来自定义这些部分,更改它们的默认位置,甚至添加额外的部分。现在,您只需要通过编译器指令通知链接器符号的放置。GNU C 编译器提供了用于此目的的属性。在 Linux 内核的情况下,提供了一个自定义的 LDS 文件,位于 arch/<arch>/kernel/vmlinux.lds.S 中。然后使用 __init 和 __exit 来标记要放置在内核的 LDS 文件中映射的专用部分中的符号。
总之,__init 和 __exit 是 Linux 指令(实际上是宏),它们包装了用于符号放置的 C 编译器属性。它们指示编译器将它们分别放置在 .init.text 和 .exit.text 部分,即使内核可以访问不同的对象部分。
模块信息
即使不必阅读其代码,人们也应该能够收集有关给定模块的一些信息(例如作者、参数描述、许可证)。内核模块使用其 .modinfo 部分来存储有关模块的信息。任何 MODULE_* 宏都将使用传递的值更新该部分的内容。其中一些宏是 MODULE_DESCRIPTION()、MODULE_AUTHOR() 和 MODULE_LICENSE()。内核提供的真正底层宏用于在模块信息部分中添加条目是 MODULE_INFO(tag, info),它添加了形式为 tag = info 的通用信息。这意味着驱动程序作者可以添加任何他们想要的自由形式信息,例如:
MODULE_INFO(my_field_name, "What eeasy value");
可以使用 objdump -d -j .modinfo 命令在给定模块上转储 .modeinfo 部分的内容:

helloworld-params.ko 模块的 .modeinfo 部分的内容
modinfo 部分可以被视为模块的数据表。实际上以格式化的方式打印信息的用户空间工具是 modinfo:

modinfo 输出
除了自定义信息外,还应提供标准信息,内核为此提供了宏;这些是许可证、模块作者、参数描述、模块版本和模块描述。
许可
许可在给定模块中由 MODULE_LICENSE() 宏定义:
MODULE_LICENSE ("GPL");
许可证将定义您的源代码应如何与其他开发人员共享(或不共享)。MODULE_LICENSE()告诉内核我们的模块使用的许可证。它会影响您的模块行为,因为不兼容 GPL 的许可证将导致您的模块无法看到/使用内核通过EXPORT_SYMBOL_GPL()宏导出的服务/函数,该宏仅向兼容 GPL 的模块显示符号,这与EXPORT_SYMBOL()相反,后者为任何许可证的模块导出函数。加载不兼容 GPL 的模块还将导致内核受到污染;这意味着已加载非开源或不受信任的代码,您可能不会得到社区的支持。请记住,没有MODULE_LICENSE()的模块也不被视为开源,并且也会污染内核。以下是include/linux/module.h的摘录,描述了内核支持的许可证:
/*
* The following license idents are currently accepted as indicating free
* software modules
*
* "GPL" [GNU Public License v2 or later]
* "GPL v2" [GNU Public License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNU Public License v2
* or BSD license choice]
* "Dual MIT/GPL" [GNU Public License v2
* or MIT license choice]
* "Dual MPL/GPL" [GNU Public License v2
* or Mozilla license choice]
*
* The following other idents are available
*
* "Proprietary" [Non free products]
*
* There are dual licensed components, but when running with Linux it is the
* GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL
* is a GPL combined work.
*
* This exists for several reasons
* 1\. So modinfo can show license info for users wanting to vet their setup
* is free
* 2\. So the community can ignore bug reports including proprietary modules
* 3\. So vendors can do likewise based on their own policies
*/
您的模块至少必须与 GPL 兼容,才能享受完整的内核服务。
模块作者
MODULE_AUTHOR()声明模块的作者:
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
可能有多个作者。在这种情况下,每个作者都必须用MODULE_AUTHOR()声明:
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_AUTHOR("Lorem Ipsum <l.ipsum@foobar.com>");
模块描述
MODULE_DESCRIPTION()简要描述模块的功能:
MODULE_DESCRIPTION("Hello, world! Module");
错误和消息打印
错误代码要么由内核解释,要么由用户空间应用程序(通过errno变量)解释。错误处理在软件开发中非常重要,比在内核开发中更重要。幸运的是,内核提供了几个几乎涵盖了你可能遇到的每个错误的错误,并且有时你需要打印它们以帮助你调试。
错误处理
返回给定错误的错误代码将导致内核或用户空间应用程序产生不必要的行为并做出错误的决定。为了保持清晰,内核树中有预定义的错误,几乎涵盖了您可能遇到的每种情况。一些错误(及其含义)在include/uapi/asm-generic/errno-base.h中定义,其余列表可以在include/uapi/asm-generic/errno.h中找到。以下是include/uapi/asm-generic/errno-base.h中错误列表的摘录:
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
大多数时候,返回错误的经典方法是以return -ERROR的形式返回,特别是当涉及到回答系统调用时。例如,对于 I/O 错误,错误代码是EIO,应该return -EIO:
dev = init(&ptr);
if(!dev)
return -EIO
错误有时会跨越内核空间并传播到用户空间。如果返回的错误是对系统调用(open,read,ioctl,mmap)的回答,则该值将自动分配给用户空间的errno全局变量,可以使用strerror(errno)将错误转换为可读字符串:
#include <errno.h> /* to access errno global variable */
#include <string.h>
[...]
if(wite(fd, buf, 1) < 0) {
printf("something gone wrong! %s\n", strerror(errno));
}
[...]
当遇到错误时,必须撤消发生错误之前设置的所有操作。通常的做法是使用goto语句:
ptr = kmalloc(sizeof (device_t));
if(!ptr) {
ret = -ENOMEM
goto err_alloc;
}
dev = init(&ptr);
if(dev) {
ret = -EIO
goto err_init;
}
return 0;
err_init:
free(ptr);
err_alloc:
return ret;
使用goto语句的原因很简单。当涉及到处理错误时,比如在第 5 步,必须清理之前的操作(步骤 4、3、2、1)。而不是进行大量的嵌套检查操作,如下所示:
if (ops1() != ERR) {
if (ops2() != ERR) {
if ( ops3() != ERR) {
if (ops4() != ERR) {
这可能会令人困惑,并可能导致缩进问题。人们更喜欢使用goto以便有一个直接的控制流,如下所示:
if (ops1() == ERR) // |
goto error1; // |
if (ops2() == ERR) // |
goto error2; // |
if (ops3() == ERR) // |
goto error3; // |
if (ops4() == ERR) // V
goto error4;
error5:
[...]
error4:
[...]
error3:
[...]
error2:
[...]
error1:
[...]
这意味着,应该只使用 goto 在函数中向前移动。
处理空指针错误
当涉及到从应该返回指针的函数返回错误时,函数经常返回NULL指针。这是一种有效但相当无意义的方法,因为人们并不确切知道为什么返回了这个空指针。为此,内核提供了三个函数,ERR_PTR,IS_ERR和PTR_ERR:
void *ERR_PTR(long error);
long IS_ERR(const void *ptr);
long PTR_ERR(const void *ptr);
第一个实际上将错误值作为指针返回。假设一个函数在失败的内存分配后可能会return -ENOMEM,我们必须这样做return ERR_PTR(-ENOMEM);。第二个用于检查返回的值是否是指针错误,if (IS_ERR(foo))。最后返回实际的错误代码return PTR_ERR(foo);。以下是一个例子:
如何使用ERR_PTR,IS_ERR和PTR_ERR:
static struct iio_dev *indiodev_setup(){
[...]
struct iio_dev *indio_dev;
indio_dev = devm_iio_device_alloc(&data->client->dev, sizeof(data));
if (!indio_dev)
return ERR_PTR(-ENOMEM);
[...]
return indio_dev;
}
static int foo_probe([...]){
[...]
struct iio_dev *my_indio_dev = indiodev_setup();
if (IS_ERR(my_indio_dev))
return PTR_ERR(data->acc_indio_dev);
[...]
}
这是错误处理的一个优点,也是内核编码风格的一部分,其中说:如果函数的名称是一个动作或一个命令,函数应该返回一个错误代码整数。如果名称是一个谓词,函数应该返回一个succeeded布尔值。例如,add work是一个命令,add_work()函数成功返回0,失败返回-EBUSY。同样,PCI device present是一个谓词,pci_dev_present()函数在成功找到匹配设备时返回1,如果没有找到则返回0。
消息打印 - printk()
printk()对内核来说就像printf()对用户空间一样。由printk()编写的行可以通过dmesg命令显示。根据您需要打印的消息的重要性,您可以在include/linux/kern_levels.h中定义的八个日志级别消息之间进行选择,以及它们的含义:
以下是内核日志级别的列表。这些级别中的每一个都对应于字符串中的一个数字,其优先级与数字的值成反比。例如,0是更高的优先级:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
以下代码显示了如何打印内核消息以及日志级别:
printk(KERN_ERR "This is an error\n");
如果省略调试级别(printk("This is an error\n")),内核将根据CONFIG_DEFAULT_MESSAGE_LOGLEVEL配置选项为函数提供一个调试级别,这是默认的内核日志级别。实际上可以使用以下更有意义的宏之一,它们是对先前定义的宏的包装器:pr_emerg,pr_alert,pr_crit,pr_err,pr_warning,pr_notice,pr_info和pr_debug:
pr_err("This is the same error\n");
对于新驾驶员,建议使用这些包装器。 printk()的现实是,每当调用它时,内核都会将消息日志级别与当前控制台日志级别进行比较;如果前者较高(值较低)则消息将立即打印到控制台。您可以使用以下命令检查日志级别参数:
cat /proc/sys/kernel/printk
4 4 1 7
在此代码中,第一个值是当前日志级别(4),第二个值是默认值,根据CONFIG_DEFAULT_MESSAGE_LOGLEVEL选项。其他值对于本章的目的并不重要,因此让我们忽略这些。
内核日志级别列表如下:
/* integer equivalents of KERN_<LEVEL> */
#define LOGLEVEL_SCHED -2 /* Deferred messages from sched code
* are set to this special level */
#define LOGLEVEL_DEFAULT -1 /* default (or last) loglevel */
#define LOGLEVEL_EMERG 0 /* system is unusable */
#define LOGLEVEL_ALERT 1 /* action must be taken immediately */
#define LOGLEVEL_CRIT 2 /* critical conditions */
#define LOGLEVEL_ERR 3 /* error conditions */
#define LOGLEVEL_WARNING 4 /* warning conditions */
#define LOGLEVEL_NOTICE 5 /* normal but significant condition */
#define LOGLEVEL_INFO 6 /* informational */
#define LOGLEVEL_DEBUG 7 /* debug-level messages */
当前日志级别可以通过以下更改:
# echo <level> > /proc/sys/kernel/printk
printk()永远不会阻塞,并且即使从原子上下文中调用也足够安全。它会尝试锁定控制台并打印消息。如果锁定失败,输出将被写入缓冲区,函数将返回,永远不会阻塞。然后当前控制台持有者将收到有关新消息的通知,并在释放控制台之前打印它们。
内核还支持其他调试方法,可以动态使用#define DEBUG或在文件顶部使用#define DEBUG。对此类调试风格感兴趣的人可以参考内核文档中的Documentation/dynamic-debug-howto.txt文件。
模块参数
与用户程序一样,内核模块可以从命令行接受参数。这允许根据给定的参数动态更改模块的行为,并且可以帮助开发人员在测试/调试会话期间不必无限制地更改/编译模块。为了设置这一点,首先应该声明将保存命令行参数值的变量,并对每个变量使用module_param()宏。该宏在include/linux/moduleparam.h中定义(代码中也应该包括:#include <linux/moduleparam.h>),如下所示:
module_param(name, type, perm);
该宏包含以下元素:
-
name:用作参数的变量的名称 -
type:参数的类型(bool、charp、byte、short、ushort、int、uint、long、ulong),其中charp代表 char 指针 -
perm:这表示/sys/module/<module>/parameters/<param>文件的权限。其中一些是S_IWUSR,S_IRUSR,S_IXUSR,S_IRGRP,S_WGRP和S_IRUGO,其中: -
S_I只是一个前缀 -
R:读取,W:写入,X:执行 -
USR:用户,GRP:组,UGO:用户,组,其他人
最终可以使用|(或操作)来设置多个权限。如果 perm 为0,则sysfs中的文件参数将不会被创建。您应该只使用S_IRUGO只读参数,我强烈建议;通过与其他属性进行|(或)运算,可以获得细粒度的属性。
在使用模块参数时,应该使用MODULE_PARM_DESC来描述每个参数。这个宏将在模块信息部分填充每个参数的描述。以下是一个示例,来自书籍的代码库中提供的helloworld-params.c源文件:
#include <linux/moduleparam.h>
[...]
static char *mystr = "hello";
static int myint = 1;
static int myarr[3] = {0, 1, 2};
module_param(myint, int, S_IRUGO);
module_param(mystr, charp, S_IRUGO);
module_param_array(myarr, int,NULL, S_IWUSR|S_IRUSR); /* */
MODULE_PARM_DESC(myint,"this is my int variable");
MODULE_PARM_DESC(mystr,"this is my char pointer variable");
MODULE_PARM_DESC(myarr,"this is my array of int");
static int foo()
{
pr_info("mystring is a string: %s\n", mystr);
pr_info("Array elements: %d\t%d\t%d", myarr[0], myarr[1], myarr[2]);
return myint;
}
要加载模块并传递我们的参数,我们需要执行以下操作:
# insmod hellomodule-params.ko mystring="packtpub" myint=15 myArray=1,2,3
在加载模块之前,可以使用modinfo来显示模块支持的参数的描述:
$ modinfo ./helloworld-params.ko
filename: /home/jma/work/tutos/sources/helloworld/./helloworld-params.ko
license: GPL
author: John Madieu <john.madieu@gmail.com>
srcversion: BBF43E098EAB5D2E2DD78C0
depends:
vermagic: 4.4.0-93-generic SMP mod_unload modversions
parm: myint:this is my int variable (int)
parm: mystr:this is my char pointer variable (charp)
parm: myarr:this is my array of int (array of int)
构建您的第一个模块
有两个地方可以构建一个模块。这取决于您是否希望人们使用内核配置界面自行启用模块。
模块的 makefile
Makefile 是一个特殊的文件,用于执行一系列操作,其中最重要的是编译程序。有一个专门的工具来解析 makefile,叫做make。在跳转到整个 make 文件的描述之前,让我们介绍obj-<X> kbuild 变量。
在几乎每个内核 makefile 中,都会看到至少一个obj<-X>变量的实例。这实际上对应于obj-<X>模式,其中<X>应该是y,m,留空,或n。这是由内核 makefile 从内核构建系统的头部以一般方式使用的。这些行定义要构建的文件、任何特殊的编译选项以及要递归进入的任何子目录。一个简单的例子是:
obj-y += mymodule.o
这告诉 kbuild 当前目录中有一个名为mymodule.o的对象。mymodule.o将从mymodule.c或mymodule.S构建。mymodule.o将如何构建或链接取决于<X>的值:
-
如果
<X>设置为m,则使用变量obj-m,mymodule.o将作为一个模块构建。 -
如果
<X>设置为y,则使用变量obj-y,mymodule.o将作为内核的一部分构建。然后说 foo 是一个内置模块。 -
如果
<X>设置为n,则使用变量obj-m,mymodule.o将根本不会被构建。
因此,通常使用obj-$(CONFIG_XXX)模式,其中CONFIG_XXX是内核配置选项,在内核配置过程中设置或不设置。一个例子是:
obj-$(CONFIG_MYMODULE) += mymodule.o
$(CONFIG_MYMODULE)根据内核配置过程中的值评估为y或m。如果CONFIG_MYMODULE既不是y也不是m,则文件将不会被编译或链接。y表示内置(在内核配置过程中代表是),m代表模块。$(CONFIG_MYMODULE)从正常配置过程中获取正确的答案。这将在下一节中解释。
最后一个用例是:
obj-<X> += somedir/
这意味着 kbuild 应进入名为somedir的目录;查找其中的任何 makefile 并处理它,以决定应构建哪些对象。
回到 makefile,以下是我们将用于构建书中介绍的每个模块的内容 makefile:
obj-m := helloworld.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
all default: modules
install: modules_install
modules modules_install help clean:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@
-
obj-m := hellowolrd.o:obj-m列出我们要构建的模块。对于每个<filename>.o,构建系统将寻找一个<filename>.c进行构建。obj-m用于构建模块,而obj-y将导致内置对象。 -
KERNELDIR := /lib/modules/$(shell uname -r)/build:KERNELDIR是预构建内核源的位置。正如我们之前所说,我们需要预构建的内核才能构建任何模块。如果您已经从源代码构建了内核,则应将此变量设置为构建源目录的绝对路径。-C指示 make 实用程序在读取 makefile 或执行其他任何操作之前切换到指定的目录。 -
M=$(shell pwd):这与内核构建系统有关。内核 Makefile 使用此变量来定位要构建的外部模块的目录。您的.c文件应放置在这里。 -
all default: modules:此行指示make实用程序执行modules目标,无论是all还是default目标,这些都是在构建用户应用程序时的经典目标。换句话说,make default或make all或简单地make命令将被转换为make modules。 -
modules modules_install help clean::此行表示此 Makefile 中有效的列表目标。 -
$(MAKE) -C $(KERNELDIR ) M=$(shell pwd) $@:这是要为上述每个目标执行的规则。$@将被替换为导致规则运行的目标的名称。换句话说,如果调用 make modules,$@将被替换为 modules,规则将变为:$(MAKE) -C $(KERNELDIR ) M=$(shell pwd) module。
在内核树中
在内核树中构建驱动程序之前,您应首先确定驱动程序应放置在哪个驱动程序目录中的.c文件。给定您的文件名mychardev.c,其中包含您的特殊字符驱动程序的源代码,它应放置在内核源中的drivers/char目录中。驱动程序中的每个子目录都有Makefile和Kconfig文件。
将以下内容添加到该目录的Kconfig中:
config PACKT_MYCDEV
tristate "Our packtpub special Character driver"
default m
help
Say Y here if you want to support the /dev/mycdev device.
The /dev/mycdev device is used to access packtpub.
在同一目录的 makefile 中添加:
obj-$(CONFIG_PACKT_MYCDEV) += mychardev.o
更新Makefile时要小心;.o文件名必须与您的.c文件的确切名称匹配。如果您的源文件是foobar.c,则必须在Makefile中使用foobar.o。为了使您的驱动程序作为模块构建,将以下行添加到arch/arm/configs目录中的板 defconfig 中:
CONFIG_PACKT_MYCDEV=m
您还可以运行make menuconfig从 UI 中选择它,并运行make构建内核,然后运行make modules构建模块(包括您自己的模块)。要使驱动程序内置构建,只需用y替换m:
CONFIG_PACKT_MYCDEV=m
这里描述的一切都是嵌入式板制造商为了提供带有他们的板的BSP(Board Support Package)而做的,其中包含已经包含他们自定义驱动程序的内核:

内核树中的 packt_dev 模块
配置完成后,您可以使用make构建内核,并使用make modules构建模块。
包含在内核源树中的模块将安装在/lib/modules/$(KERNELRELEASE)/kernel/中。在您的 Linux 系统上,它是/lib/modules/$(uname -r)/kernel/。运行以下命令以安装模块:
make modules_install
树外
在构建外部模块之前,您需要拥有完整的预编译内核源代码树。内核源代码树的版本必须与您将加载和使用模块的内核相同。获取预构建内核版本有两种方法:
-
自行构建(之前讨论过)
-
从您的发行版存储库安装
linux-headers-*软件包
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)
这将只安装头文件,而不是整个源代码树。然后,头文件将安装在/usr/src/linux-headers-$(uname -r)中。在我的计算机上,它是/usr/src/linux-headers-4.4.0-79-generic/。将会有一个符号链接,/lib/modules/$(uname -r)/build,指向先前安装的头文件。这是您应该在Makefile中指定为内核目录的路径。这是您为预构建内核所需做的一切。
构建模块
现在,当您完成了您的 makefile,只需切换到您的源目录并运行make命令,或者make modules:
jma@jma:~/work/tutos/sources/helloworld$ make
make -C /lib/modules/4.4.0-79-generic/build \
M=/media/jma/DATA/work/tutos/sources/helloworld modules
make[1]: Entering directory '/usr/src/linux-headers-4.4.0-79-generic'
CC [M] /media/jma/DATA/work/tutos/sources/helloworld/helloworld.o
Building modules, stage 2.
MODPOST 1 modules
CC /media/jma/DATA/work/tutos/sources/helloworld/helloworld.mod.o
LD [M] /media/jma/DATA/work/tutos/sources/helloworld/helloworld.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.4.0-79-generic'
jma@jma:~/work/tutos/sources/helloworld$ ls
helloworld.c helloworld.ko helloworld.mod.c helloworld.mod.o helloworld.o Makefile modules.order Module.symvers
jma@jma:~/work/tutos/sources/helloworld$ sudo insmod helloworld.ko
jma@jma:~/work/tutos/sources/helloworld$ sudo rmmod helloworld
jma@jma:~/work/tutos/sources/helloworld$ dmesg
[...]
[308342.285157] Hello world!
[308372.084288] End of the world
前面的例子只涉及本地构建,在 x86 机器上为 x86 机器进行编译。那么交叉编译呢?这是指在 A 机器上(称为主机)编译旨在在 B 机器上(称为目标机)运行的代码的过程;主机和目标机具有不同的架构。经典用例是在 x86 机器上构建应在 ARM 架构上运行的代码,这恰好是我们的情况。
当涉及交叉编译内核模块时,内核 makefile 需要了解的基本上有两个变量;这些是:ARCH和CROSS_COMPILE,分别代表目标架构和编译器前缀名称。因此,本地编译和交叉编译内核模块之间的变化是make命令。以下是为 ARM 构建的命令行:
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
摘要
本章向您展示了驱动程序开发的基础知识,并解释了模块/内置设备的概念,以及它们的加载和卸载。即使您无法与用户空间交互,您也可以准备编写完整的驱动程序,打印格式化消息,并理解init/exit的概念。下一章将涉及字符设备,您将能够针对增强功能编写代码,编写可从用户空间访问的代码,并对系统产生重大影响。
第三章:内核设施和辅助函数
内核是一个独立的软件,正如您将在本章中看到的,它不使用任何 C 库。它实现了您可能在现代库中遇到的任何机制,甚至更多,例如压缩、字符串函数等。我们将逐步介绍这些功能的最重要方面。
在本章中,我们将涵盖以下主题:
-
引入内核容器数据结构
-
处理内核睡眠机制
-
使用定时器
-
深入了解内核锁定机制(互斥锁、自旋锁)
-
使用内核专用 API 推迟工作
-
使用 IRQs
理解 container_of 宏
当涉及到在代码中管理多个数据结构时,您几乎总是需要将一个结构嵌入到另一个结构中,并在任何时刻检索它们,而不需要询问有关内存偏移或边界的问题。假设您有一个struct person,如此定义:
struct person {
int age;
char *name;
} p;
只需拥有age或name的指针,就可以检索包含该指针的整个结构。正如其名称所示,container_of宏用于查找结构的给定字段的容器。该宏在include/linux/kernel.h中定义,如下所示:
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
不要害怕指针;只需将其视为:
container_of(pointer, container_type, container_field);
以下是前面代码片段的元素:
-
pointer:这是结构中字段的指针 -
container_type:这是包装(包含)指针的结构的类型 -
container_field:这是指针在结构内指向的字段的名称
让我们考虑以下容器:
struct person {
int age;
char *name;
};
现在让我们考虑它的一个实例,以及指向name成员的指针:
struct person somebody;
[...]
char *the_name_ptr = somebody.name;
以及指向name成员的指针(the_name_ptr),您可以使用container_of宏来获取包含此成员的整个结构(容器)的指针,方法如下:
struct person *the_person;
the_person = container_of(the_name_ptr, struct person, name);
container_of考虑了name在结构的开头的偏移量,以获取正确的指针位置。如果您从指针the_name_ptr中减去字段name的偏移量,您将得到正确的位置。这就是宏的最后一行所做的事情:
(type *)( (char *)__mptr - offsetof(type,member) );
将其应用于一个真实的例子,得到以下结果:
struct family {
struct person *father;
struct person *mother;
int number_of_suns;
int salary;
} f;
/*
* pointer to a field of the structure
* (could be any member of any family)
*/
struct *person = family.father;
struct family *fam_ptr;
/* now let us retrieve back its family */
fam_ptr = container_of(person, struct family, father);
这就是您需要了解的关于container_of宏的全部内容,相信我,这已经足够了。在我们将在本书中进一步开发的真实驱动程序中,它看起来像这样:
struct mcp23016 {
struct i2c_client *client;
struct gpio_chip chip;
}
/* retrive the mcp23016 struct given a pointer 'chip' field */
static inline struct mcp23016 *to_mcp23016(struct gpio_chip *gc)
{
return container_of(gc, struct mcp23016, chip);
}
static int mcp23016_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mcp23016 *mcp;
[...]
mcp = devm_kzalloc(&client->dev, sizeof(*mcp), GFP_KERNEL);
if (!mcp)
return -ENOMEM;
[...]
}
controller_of宏主要用于内核中的通用容器。在本书的一些示例中(从第五章开始,平台设备驱动程序),您将遇到container_of宏。
链表
想象一下,您有一个管理多个设备的驱动程序,比如说五个设备。您可能需要在驱动程序中跟踪每个设备。您需要的是一个链表。实际上存在两种类型的链表:
-
简单链表
-
双向链表
因此,内核开发人员只实现循环双向链表,因为这种结构允许您实现 FIFO 和 LIFO,并且内核开发人员会努力维护一组最小的代码。要支持列表,需要在代码中添加的标头是<linux/list.h>。内核中列表实现的核心数据结构是struct list_head结构,定义如下:
struct list_head {
struct list_head *next, *prev;
};
struct list_head在列表的头部和每个节点中都使用。在内核世界中,要将数据结构表示为链表,该结构必须嵌入一个struct list_head字段。例如,让我们创建一个汽车列表:
struct car {
int door_number;
char *color;
char *model;
};
在我们可以为汽车创建一个列表之前,我们必须改变其结构以嵌入一个struct list_head字段。结构变为:
struct car {
int door_number;
char *color;
char *model;
struct list_head list; /* kernel's list structure */
};
首先,我们需要创建一个struct list_head变量,它将始终指向我们列表的头部(第一个元素)。这个list_head的实例不与任何汽车相关联,它是特殊的:
static LIST_HEAD(carlist) ;
现在我们可以创建汽车并将它们添加到我们的列表carlist:
#include <linux/list.h>
struct car *redcar = kmalloc(sizeof(*car), GFP_KERNEL);
struct car *bluecar = kmalloc(sizeof(*car), GFP_KERNEL);
/* Initialize each node's list entry */
INIT_LIST_HEAD(&bluecar->list);
INIT_LIST_HEAD(&redcar->list);
/* allocate memory for color and model field and fill every field */
[...]
list_add(&redcar->list, &carlist) ;
list_add(&bluecar->list, &carlist) ;
就是这么简单。现在,carlist 包含两个元素。让我们深入了解链表 API。
创建和初始化列表
有两种方法可以创建和初始化列表:
动态方法
动态方法包括一个 struct list_head 并使用 INIT_LIST_HEAD 宏进行初始化:
struct list_head mylist;
INIT_LIST_HEAD(&mylist);
以下是 INIT_LIST_HEAD 的展开:
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
静态方法
通过 LIST_HEAD 宏进行静态分配:
LIST_HEAD(mylist)
LIST_HEAD 的定义如下:
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
以下是它的展开:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
这将把 name 字段内的每个指针(prev 和 next)都指向 name 本身(就像 INIT_LIST_HEAD 做的那样)。
创建列表节点
要创建新节点,只需创建我们的数据结构实例,并初始化它们的嵌入式 list_head 字段。使用汽车示例,将得到以下内容:
struct car *blackcar = kzalloc(sizeof(struct car), GFP_KERNEL);
/* non static initialization, since it is the embedded list field*/
INIT_LIST_HEAD(&blackcar->list);
如前所述,使用 INIT_LIST_HEAD,这是一个动态分配的列表,通常是另一个结构的一部分。
添加列表节点
内核提供了 list_add 来将新条目添加到列表中,它是对内部函数 __list_add 的封装:
void list_add(struct list_head *new, struct list_head *head);
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
__list_add 将接受两个已知的条目作为参数,并在它们之间插入您的元素。它在内核中的实现非常简单:
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
以下是我们列表中添加两辆车的示例:
list_add(&redcar->list, &carlist);
list_add(&blue->list, &carlist);
这种模式可以用来实现栈。另一个将条目添加到列表中的函数是:
void list_add_tail(struct list_head *new, struct list_head *head);
这将给定的新条目插入到列表的末尾。根据我们之前的示例,我们可以使用以下内容:
list_add_tail(&redcar->list, &carlist);
list_add_tail(&blue->list, &carlist);
这种模式可以用来实现队列。
从列表中删除节点
在内核代码中处理列表是一项简单的任务。删除节点很简单:
void list_del(struct list_head *entry);
按照前面的示例,让我们删除红色的车:
list_del(&redcar->list);
list_del 断开给定条目的 prev 和 next 指针,导致条目被移除。节点分配的内存尚未被释放;您需要使用 kfree 手动释放。
链表遍历
我们有宏 list_for_each_entry(pos, head, member) 用于列表遍历。
-
head是列表的头节点。 -
member是我们数据结构中struct list_head的列表名称(在我们的例子中是list)。 -
pos用于迭代。它是一个循环游标(就像for(i=0; i<foo; i++)中的i)。head可能是链表的头节点,也可能是任何条目,我们不关心,因为我们处理的是双向链表。
struct car *acar; /* loop counter */
int blue_car_num = 0;
/* 'list' is the name of the list_head struct in our data structure */
list_for_each_entry(acar, carlist, list){
if(acar->color == "blue")
blue_car_num++;
}
为什么我们需要在数据结构中的 list_head 类型字段的名称?看看 list_for_each_entry 的定义:
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
&pos->member != (head); \
pos = list_entry(pos->member.next, typeof(*pos), member))
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
通过这个,我们可以理解这一切都是关于 container_of 的力量。还要记住 list_for_each_entry_safe(pos, n, head, member)。
内核睡眠机制
睡眠是一个进程使处理器放松的机制,有可能处理另一个进程。处理器可以进入睡眠状态的原因可能是为了感知数据的可用性,或者等待资源空闲。
内核调度程序管理要运行的任务列表,称为运行队列。睡眠进程不再被调度,因为它们已从运行队列中移除。除非其状态发生变化(即它被唤醒),否则睡眠进程永远不会被执行。只要有一个进程在等待某些东西(资源或其他任何东西),您就可以放松处理器,并确保某个条件或其他人会唤醒它。也就是说,Linux 内核通过提供一组函数和数据结构来简化睡眠机制的实现。
等待队列
等待队列主要用于处理阻塞的 I/O,等待特定条件成立,并感知数据或资源的可用性。为了理解它的工作原理,让我们来看看 include/linux/wait.h 中的结构:
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
让我们关注task_list字段。如您所见,它是一个列表。您想要让进程进入睡眠状态的每个进程都排队在该列表中(因此称为等待队列),并进入睡眠状态,直到条件成为真。等待队列可以被视为一系列进程和一个锁。
处理等待队列时您将经常遇到的函数是:
- 静态声明:
DECLARE_WAIT_QUEUE_HEAD(name)
- 动态声明:
wait_queue_head_t my_wait_queue;
init_waitqueue_head(&my_wait_queue);
- 阻塞:
/*
* block the current task (process) in the wait queue if
* CONDITION is false
*/
int wait_event_interruptible(wait_queue_head_t q, CONDITION);
- 解除阻塞:
/*
* wake up one process sleeping in the wait queue if
* CONDITION above has become true
*/
void wake_up_interruptible(wait_queue_head_t *q);
wait_event_interruptible不会持续轮询,而只是在调用时评估条件。如果条件为假,则将进程置于TASK_INTERRUPTIBLE状态并从运行队列中移除。然后在等待队列中每次调用wake_up_interruptible时重新检查条件。如果在wake_up_interruptible运行时条件为真,则等待队列中的进程将被唤醒,并且其状态设置为TASK_RUNNING。进程按照它们进入睡眠的顺序被唤醒。要唤醒等待队列中的所有进程,您应该使用wake_up_interruptible_all。
实际上,主要功能是wait_event,wake_up和wake_up_all。它们与队列中的进程一起使用,处于独占(不可中断)等待状态,因为它们不能被信号中断。它们应该仅用于关键任务。可中断函数只是可选的(但建议使用)。由于它们可以被信号中断,您应该检查它们的返回值。非零值意味着您的睡眠已被某种信号中断,驱动程序应返回ERESTARTSYS。
如果有人调用了wake_up或wake_up_interruptible,并且条件仍然为FALSE,那么什么也不会发生。没有wake_up(或wake_up_interuptible),进程将永远不会被唤醒。以下是等待队列的一个示例:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/time.h>
#include <linux/delay.h>
#include<linux/workqueue.h>
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int condition = 0;
/* declare a work queue*/
static struct work_struct wrk;
static void work_handler(struct work_struct *work)
{
printk("Waitqueue module handler %s\n", __FUNCTION__);
msleep(5000);
printk("Wake up the sleeping module\n");
condition = 1;
wake_up_interruptible(&my_wq);
}
static int __init my_init(void)
{
printk("Wait queue example\n");
INIT_WORK(&wrk, work_handler);
schedule_work(&wrk);
printk("Going to sleep %s\n", __FUNCTION__);
wait_event_interruptible(my_wq, condition != 0);
pr_info("woken up by the work job\n");
return 0;
}
void my_exit(void)
{
printk("waitqueue example cleanup\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu <john.madieu@foobar.com>");
MODULE_LICENSE("GPL");
在上面的例子中,当前进程(实际上是insmod)将被放入等待队列中,等待 5 秒钟后由工作处理程序唤醒。dmesg输出如下:
[342081.385491] Wait queue example
[342081.385505] Going to sleep my_init
[342081.385515] Waitqueue module handler work_handler
[342086.387017] Wake up the sleeping module
[342086.387096] woken up by the work job
[342092.912033] waitqueue example cleanup
延迟和定时器管理
时间是最常用的资源之一,仅次于内存。它用于几乎所有事情:延迟工作,睡眠,调度,超时和许多其他任务。
时间有两个类别。内核使用绝对时间来知道现在是什么时间,也就是说,日期和时间,而相对时间则由内核调度程序等使用。对于绝对时间,有一个名为实时时钟(RTC)的硬件芯片。我们将在本书的第十八章中处理这些设备,RTC 驱动程序。另一方面,为了处理相对时间,内核依赖于一个称为定时器的 CPU 特性(外围设备),从内核的角度来看,它被称为内核定时器。内核定时器是我们将在本节中讨论的内容。
内核定时器分为两个不同的部分:
-
标准定时器,或系统定时器
-
高分辨率定时器
标准定时器
标准定时器是以 jiffies 为粒度运行的内核定时器。
Jiffies 和 HZ
jiffy 是在<linux/jiffies.h>中声明的内核时间单位。要理解 jiffies,我们需要介绍一个新的常数 HZ,它是在一秒钟内递增jiffies的次数。每次递增称为tick。换句话说,HZ 表示 jiffy 的大小。HZ 取决于硬件和内核版本,并且还确定时钟中断的频率。这在某些架构上是可配置的,在其他架构上是固定的。
这意味着jiffies每秒递增 HZ 次。如果 HZ = 1,000,则递增 1,000 次(也就是说,每 1/1,000 秒递增一次)。一旦定义了可编程中断定时器(PIT),它是一个硬件组件,就会使用该值来对 PIT 进行编程,以便在 PIT 中断时递增 jiffies。
根据平台的不同,jiffies 可能会导致溢出。在 32 位系统上,HZ = 1,000 将导致大约 50 天的持续时间,而在 64 位系统上,持续时间约为 6 亿年。通过将 jiffies 存储在 64 位变量中,问题得到解决。然后引入了第二个变量,并在<linux/jiffies.h>中定义:
extern u64 jiffies_64;
在 32 位系统上,jiffies将指向低位 32 位,而jiffies_64将指向高位位。在 64 位平台上,jiffies = jiffies_64。
定时器 API
定时器在内核中表示为timer_list的实例:
#include <linux/timer.h>
struct timer_list {
struct list_head entry;
unsigned long expires;
struct tvec_t_base_s *base;
void (*function)(unsigned long);
unsigned long data;
);
expires是 jiffies 中的绝对值。entry是一个双向链表,data是可选的,并传递给回调函数。
定时器设置初始化
以下是初始化定时器的步骤:
- 设置定时器:设置定时器,提供用户定义的回调和数据:
void setup_timer( struct timer_list *timer, \
void (*function)(unsigned long), \
unsigned long data);
也可以使用以下方法:
void init_timer(struct timer_list *timer);
setup_timer是init_timer的包装器。
- 设置到期时间:当初始化定时器时,需要在回调触发之前设置其到期时间:
int mod_timer( struct timer_list *timer, unsigned long expires);
- 释放定时器:当您完成定时器时,需要释放它:
void del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);
del_timer返回void,无论它是否已停用挂起的定时器。其返回值为 0 表示未激活的定时器,1 表示激活的定时器。最后,del_timer_sync等待处理程序完成执行,即使可能发生在另一个 CPU 上的处理程序。您不应持有阻止处理程序完成的锁,否则将导致死锁。您应在模块清理例程中释放定时器。您可以独立检查定时器是否正在运行:
int timer_pending( const struct timer_list *timer);
此函数检查是否有任何已触发的定时器回调待处理。
标准定时器示例
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>
static struct timer_list my_timer;
void my_timer_callback(unsigned long data)
{
printk("%s called (%ld).\n", __FUNCTION__, jiffies);
}
static int __init my_init(void)
{
int retval;
printk("Timer module loaded\n");
setup_timer(&my_timer, my_timer_callback, 0);
printk("Setup timer to fire in 300ms (%ld)\n", jiffies);
retval = mod_timer( &my_timer, jiffies + msecs_to_jiffies(300) );
if (retval)
printk("Timer firing failed\n");
return 0;
}
static void my_exit(void)
{
int retval;
retval = del_timer(&my_timer);
/* Is timer still active (1) or no (0) */
if (retval)
printk("The timer is still in use...\n");
pr_info("Timer module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Standard timer example");
MODULE_LICENSE("GPL");
高分辨率定时器(HRTs)
标准定时器精度较低,不适用于实时应用。高分辨率定时器在内核 v2.6.16 中引入(并通过内核配置中的CONFIG_HIGH_RES_TIMERS选项启用)具有微秒级(取决于平台,可达纳秒级)的分辨率,而标准定时器依赖于 HZ(因为它们依赖于 jiffies),而 HRT 实现基于ktime。
在您的系统上使用 HRT 之前,内核和硬件必须支持 HRT。换句话说,必须实现与体系结构相关的代码以访问您的硬件 HRT。
HRT API
所需的头文件是:
#include <linux/hrtimer.h>
HRT 在内核中表示为hrtimer的实例:
struct hrtimer {
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
u8 state;
u8 is_rel;
};
HRT 设置初始化
- 初始化 hrtimer:在 hrtimer 初始化之前,您需要设置一个代表时间持续的
ktime。我们将在以下示例中看到如何实现:
void hrtimer_init( struct hrtimer *time, clockid_t which_clock,
enum hrtimer_mode mode);
- 启动 hrtimer:hrtimer 可以如以下示例所示启动:
int hrtimer_start( struct hrtimer *timer, ktime_t time,
const enum hrtimer_mode mode);
mode表示到期模式。对于绝对时间值,它应为HRTIMER_MODE_ABS,对于相对于现在的时间值,它应为HRTIMER_MODE_REL。
- hrtimer 取消:您可以取消定时器,或者查看是否可能取消它:
int hrtimer_cancel( struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);
当定时器未激活时,两者返回0,当定时器激活时返回1。这两个函数之间的区别在于,如果定时器处于活动状态或其回调正在运行,hrtimer_try_to_cancel将失败,返回-1,而hrtimer_cancel将等待回调完成。
我们可以独立检查 hrtimer 的回调是否仍在运行,如下所示:
int hrtimer_callback_running(struct hrtimer *timer);
请记住,hrtimer_try_to_cancel内部调用hrtimer_callback_running。
为了防止定时器自动重新启动,hrtimer 回调函数必须返回HRTIMER_NORESTART。
您可以通过以下方式检查系统是否支持 HRT:
-
通过查看内核配置文件,其中应包含类似
CONFIG_HIGH_RES_TIMERS=y的内容:zcat /proc/configs.gz | grep CONFIG_HIGH_RES_TIMERS。 -
通过查看
cat /proc/timer_list或cat /proc/timer_list | grep resolution的结果。.resolution条目必须显示 1 纳秒,事件处理程序必须显示hrtimer_interrupts。 -
通过使用
clock_getres系统调用。 -
从内核代码中,通过使用
#ifdef CONFIG_HIGH_RES_TIMERS。
在系统上启用 HRT 后,睡眠和定时器系统调用的准确性不再取决于 jiffies,但它们仍然与 HRT 一样准确。这就是为什么有些系统不支持nanosleep()的原因,例如。
动态滴答/无滴答内核
使用先前的 HZ 选项,内核每秒被中断 HZ 次以重新安排任务,即使在空闲状态下也是如此。如果 HZ 设置为 1,000,则每秒将有 1,000 次内核中断,防止 CPU 长时间处于空闲状态,从而影响 CPU 功耗。
现在让我们看一个没有固定或预定义滴答声的内核,其中滴答声被禁用,直到需要执行某些任务。我们称这样的内核为无滴答内核。实际上,滴答激活是根据下一个动作安排的。正确的名称应该是动态滴答内核。内核负责任务调度,并在系统中维护可运行任务的列表(运行队列)。当没有任务需要调度时,调度程序切换到空闲线程,通过禁用周期性滴答声来启用动态滴答,直到下一个定时器到期(新任务排队等待处理)。
在底层,内核还维护任务超时的列表(然后知道何时以及需要睡眠多长时间)。在空闲状态下,如果下一个滴答声比任务列表超时中的最低超时时间更长,则内核将使用该超时值对定时器进行编程。当定时器到期时,内核重新启用周期性滴答声并调用调度程序,然后调度与超时相关的任务。这就是无滴答内核在空闲时如何移除周期性滴答声并节省电源的方式。
内核中的延迟和睡眠
不深入细节,根据代码运行的上下文,有两种类型的延迟:原子或非原子。内核中处理延迟的强制头文件是#include <linux/delay>。
原子上下文
在原子上下文中的任务(例如 ISR)无法睡眠,也无法被调度;这就是为什么在原子上下文中用于延迟目的的忙等待循环。内核公开了Xdelay函数系列,这些函数将在忙循环中花费时间,足够长(基于 jiffies)以实现所需的延迟:
-
ndelay(unsigned long nsecs) -
udelay(unsigned long usecs) -
mdelay(unsigned long msecs)
您应该始终使用udelay(),因为ndelay()的精度取决于您的硬件定时器的准确性(在嵌入式 SOC 上并非总是如此)。还不鼓励使用mdelay()。
定时器处理程序(回调)在原子上下文中执行,这意味着根本不允许睡眠。通过睡眠,我的意思是可能导致调用者进入睡眠状态的任何函数,例如分配内存,锁定互斥锁,显式调用sleep()函数等。
非原子上下文
在非原子上下文中,内核提供了sleep[_range]函数系列,使用哪个函数取决于您需要延迟多长时间:
-
udelay(unsigned long usecs):基于忙等待循环。如果您需要睡眠几微秒(<〜10 微秒),则应使用此函数。 -
usleep_range(unsigned long min, unsigned long max):依赖于 hrtimers,并建议让此睡眠几个~微秒或小毫秒(10 微秒-20 毫秒),避免udelay()的忙等待循环。 -
msleep(unsigned long msecs):由 jiffies/legacy_timers 支持。您应该用于更大的毫秒睡眠(10 毫秒以上)。
内核源代码中的Documentation/timers/timers-howto.txt中很好地解释了睡眠和延迟主题。
内核锁定机制
锁定是一种帮助在不同线程或进程之间共享资源的机制。共享资源是可以由至少两个用户同时访问的数据或设备,或者不可以。锁定机制可以防止滥用访问,例如,一个进程在另一个进程读取相同位置时写入数据,或者两个进程访问相同的设备(例如相同的 GPIO)。内核提供了几种锁定机制。最重要的是:
-
互斥锁
-
信号量
-
自旋锁
我们只会学习互斥锁和自旋锁,因为它们在设备驱动程序中被广泛使用。
互斥锁
互斥排他(mutex)是事实上最常用的锁定机制。要了解它的工作原理,让我们看看在include/linux/mutex.h中它的结构是什么样的:
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
[...]
};
正如我们在等待队列部分中所看到的,结构中还有一个list类型的字段:wait_list。睡眠的原理是相同的。
竞争者从调度程序运行队列中移除,并放入等待列表(wait_list)中的睡眠状态。然后内核调度和执行其他任务。当锁被释放时,等待队列中的等待者被唤醒,移出wait_list,并重新调度。
互斥锁 API
使用互斥锁只需要几个基本函数:
声明
- 静态地:
DEFINE_MUTEX(my_mutex);
- 动态地:
struct mutex my_mutex;
mutex_init(&my_mutex);
获取和释放
- 锁:
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
- 解锁:
void mutex_unlock(struct mutex *lock);
有时,您可能只需要检查互斥锁是否被锁定。为此,您可以使用int mutex_is_locked(struct mutex *lock)函数。
int mutex_is_locked(struct mutex *lock);
这个函数的作用只是检查互斥锁的所有者是否为空(NULL)或不为空。还有mutex_trylock,如果互斥锁尚未被锁定,则会获取互斥锁,并返回1;否则返回0:
int mutex_trylock(struct mutex *lock);
与等待队列的可中断系列函数一样,mutex_lock_interruptible()是推荐的,将导致驱动程序能够被任何信号中断,而mutex_lock_killable()只有杀死进程的信号才能中断驱动程序。
在使用mutex_lock()时,应非常小心,并且只有在可以保证无论发生什么都会释放互斥锁时才使用它。在用户上下文中,建议始终使用mutex_lock_interruptible()来获取互斥锁,因为如果收到信号(甚至是 ctrl + c),mutex_lock()将不会返回。
以下是互斥锁实现的示例:
struct mutex my_mutex;
mutex_init(&my_mutex);
/* inside a work or a thread */
mutex_lock(&my_mutex);
access_shared_memory();
mutex_unlock(&my_mutex);
请查看内核源码中的include/linux/mutex.h,以了解您必须遵守的互斥锁的严格规则。以下是其中一些规则:
-
一次只有一个任务可以持有互斥锁;这实际上不是一条规则,而是一个事实
-
不允许多次解锁
-
它们必须通过 API 进行初始化
-
持有互斥锁的任务可能不会退出,因为互斥锁将保持锁定,并且可能的竞争者将永远等待(将永远睡眠)
-
保存锁定的内存区域不得被释放
-
持有的互斥锁不得被重新初始化
-
由于它们涉及重新调度,因此在原子上下文中可能无法使用互斥锁,例如任务和定时器
与wait_queue一样,互斥锁没有轮询机制。每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中是否有等待者。如果有,其中一个(仅一个)会被唤醒并调度;它们被唤醒的顺序与它们入睡的顺序相同。
自旋锁
与互斥锁类似,自旋锁是一种互斥排他机制;它只有两种状态:
-
已锁定(已获取)
-
未锁定(已释放)
需要获取自旋锁的任何线程都将主动循环,直到获取锁为止,然后才会跳出循环。这是互斥锁和自旋锁的区别所在。由于自旋锁在循环时会大量消耗 CPU,因此应该在非常快速获取锁的情况下使用,特别是当持有自旋锁的时间小于重新调度的时间时。自旋锁应该在关键任务完成后尽快释放。
为了避免通过调度可能旋转的线程来浪费 CPU 时间,尝试获取由另一个线程持有的锁,内核在运行持有自旋锁的代码时禁用了抢占。通过禁用抢占,我们防止自旋锁持有者被移出运行队列,这可能导致等待进程长时间旋转并消耗 CPU。
只要持有自旋锁,其他任务可能会在等待它时旋转。通过使用自旋锁,你断言并保证它不会被长时间持有。你可以说在循环中旋转,浪费 CPU 时间,比睡眠线程、上下文切换到另一个线程或进程的成本,然后被唤醒要好。在处理器上旋转意味着没有其他任务可以在该处理器上运行;因此,在单核机器上使用自旋锁是没有意义的。在最好的情况下,你会减慢系统的速度;在最坏的情况下,你会死锁,就像互斥体一样。因此,内核只会在单处理器上对spin_lock(spinlock_t *lock)函数做出响应时禁用抢占。在单处理器(核心)系统上,你应该使用spin_lock_irqsave()和spin_unlock_irqrestore(),分别禁用 CPU 上的中断,防止中断并发。
由于你事先不知道要为哪个系统编写驱动程序,建议你使用spin_lock_irqsave(spinlock_t *lock, unsigned long flags)来获取自旋锁,它会在获取自旋锁之前禁用当前处理器(调用它的处理器)上的中断。spin_lock_irqsave内部调用local_irq_save(flags);,一个依赖于体系结构的函数来保存 IRQ 状态,并调用preempt_disable()来禁用相关 CPU 上的抢占。然后你应该使用spin_unlock_irqrestore()释放锁,它会执行我们之前列举的相反操作。这是一个执行锁获取和释放的代码。这是一个 IRQ 处理程序,但让我们只关注锁方面。我们将在下一节讨论更多关于 IRQ 处理程序的内容。
/* some where */
spinlock_t my_spinlock;
spin_lock_init(my_spinlock);
static irqreturn_t my_irq_handler(int irq, void *data)
{
unsigned long status, flags;
spin_lock_irqsave(&my_spinlock, flags);
status = access_shared_resources();
spin_unlock_irqrestore(&gpio->slock, flags);
return IRQ_HANDLED;
}
自旋锁与互斥体
在内核中用于并发的自旋锁和互斥体各自有各自的目标:
-
互斥体保护进程的关键资源,而自旋锁保护 IRQ 处理程序的关键部分
-
互斥体将竞争者置于睡眠状态,直到获得锁,而自旋锁会无限循环旋转(消耗 CPU),直到获得锁
-
由于前面的观点,你不能长时间持有自旋锁,因为等待者会浪费 CPU 时间等待锁,而互斥体可以持有资源需要受保护的时间,因为竞争者被放置在等待队列中睡眠
在处理自旋锁时,请记住抢占仅对持有自旋锁的线程禁用,而不是对自旋等待者禁用。
工作延迟机制
延迟是一种安排将来执行的工作的方法。这是一种以后报告动作的方式。显然,内核提供了实现这种机制的设施;它允许你推迟函数,无论它们的类型,以便以后调用和执行。内核中有三种:
-
SoftIRQs:在原子上下文中执行
-
Tasklets:在原子上下文中执行
-
Workqueues:在进程上下文中执行
Softirqs 和 ksoftirqd
软件中断(softirq),或软件中断是一种延迟机制,仅用于非常快速的处理,因为它在禁用调度程序的情况下运行(在中断上下文中)。你几乎不会直接处理 softirq。只有网络和块设备子系统使用 softirq。Tasklets 是 softirq 的一个实例,在几乎所有需要使用 softirq 的情况下都足够了。
ksoftirqd
在大多数情况下,softirqs 在硬件中断中调度,这可能会非常快,比它们能够被服务的速度更快。然后它们被内核排队以便稍后处理。Ksoftirqds负责延迟执行(这次是在进程上下文中)。ksoftirqd 是每个 CPU 的内核线程,用于处理未服务的软中断:

在我个人电脑的前面的top示例中,您可以看到ksoftirqd/n条目,其中n是 ksoftirqd 运行的 CPU 编号。消耗 CPU 的 ksoftirqd 可能表明系统负载过重或处于中断风暴下,这是不好的。您可以查看kernel/softirq.c,了解 ksoftirqd 的设计方式。
Tasklets
Tasklet 是建立在 softirqs 之上的一种底半部(稍后我们将看到这意味着什么)机制。它们在内核中表示为tasklet_struct的实例:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
tasklet 本质上不是可重入的。如果代码在执行过程中可以在任何地方被中断,然后可以安全地再次调用,则称为可重入代码。tasklet 被设计成只能在一个 CPU 上同时运行(即使在 SMP 系统上也是如此),这是它被计划的 CPU,但不同的 tasklet 可以在不同的 CPU 上同时运行。tasklet API 非常基本和直观。
声明一个 tasklet
- 动态地:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
- 静态地:
DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data );
DECLARE_TASKLET_DISABLED(name, func, data);
这两个函数之间有一个区别;前者创建一个已经启用并准备好在没有任何其他函数调用的情况下进行调度的 tasklet,通过将count字段设置为0,而后者创建一个已禁用的 tasklet(通过将count设置为1),在这种情况下,必须调用tasklet_enable()才能使 tasklet 可调度:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
全局地,将count字段设置为0意味着 tasklet 被禁用,不能执行,而非零值意味着相反。
启用和禁用 tasklet
有一个函数可以启用 tasklet:
void tasklet_enable(struct tasklet_struct *);
tasklet_enable简单地启用 tasklet。在旧的内核版本中,可能会发现使用void tasklet_hi_enable(struct tasklet_struct *),但这两个函数实际上是一样的。要禁用 tasklet,调用:
void tasklet_disable(struct tasklet_struct *);
您还可以调用:
void tasklet_disable_nosync(struct tasklet_struct *);
tasklet_disable将禁用 tasklet,并且只有在 tasklet 终止执行后才会返回(如果它正在运行),而tasklet_disable_nosync会立即返回,即使终止尚未发生。
Tasklet 调度
有两个用于 tasklet 的调度函数,取决于您的 tasklet 是具有正常优先级还是较高优先级的:
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
内核在两个不同的列表中维护正常优先级和高优先级的 tasklet。tasklet_schedule将 tasklet 添加到正常优先级列表中,并使用TASKLET_SOFTIRQ标志调度相关的 softirq。使用tasklet_hi_schedule,tasklet 将添加到高优先级列表中,并使用HI_SOFTIRQ标志调度相关的 softirq。高优先级 tasklet 用于具有低延迟要求的软中断处理程序。有一些与 tasklet 相关的属性您应该知道:
-
对已经计划的 tasklet 调用
tasklet_schedule,但其执行尚未开始,将不会产生任何效果,导致 tasklet 只执行一次。 -
tasklet_schedule可以在 tasklet 中调用,这意味着 tasklet 可以重新安排自己。 -
高优先级的 tasklet 始终在正常优先级的 tasklet 之前执行。滥用高优先级任务会增加系统的延迟。只能用于非常快速的任务。
您可以使用tasklet_kill函数停止 tasklet,这将阻止 tasklet 再次运行,或者在当前计划运行时等待其完成后再杀死它:
void tasklet_kill(struct tasklet_struct *t);
让我们来看看。看下面的例子:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h> /* for tasklets API */
char tasklet_data[]="We use a string; but it could be pointer to a structure";
/* Tasklet handler, that just print the data */
void tasklet_work(unsigned long data)
{
printk("%s\n", (char *)data);
}
DECLARE_TASKLET(my_tasklet, tasklet_function, (unsigned long) tasklet_data);
static int __init my_init(void)
{
/*
* Schedule the handler.
* Tasklet arealso scheduled from interrupt handler
*/
tasklet_schedule(&my_tasklet);
return 0;
}
void my_exit(void)
{
tasklet_kill(&my_tasklet);
}
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
工作队列
自 Linux 内核 2.6 以来,最常用和简单的推迟机制是工作队列。这是我们将在本章中讨论的最后一个。作为一个推迟机制,它采用了与我们所见其他机制相反的方法,仅在可抢占的上下文中运行。当您需要在底半部分休眠时,它是唯一的选择(我将在下一节中解释什么是底半部分)。通过休眠,我指的是处理 I/O 数据,持有互斥锁,延迟和所有可能导致休眠或将任务移出运行队列的其他任务。
请记住,工作队列是建立在内核线程之上的,这就是为什么我决定根本不谈论内核线程作为推迟机制的原因。但是,在内核中处理工作队列有两种方法。首先,有一个默认的共享工作队列,由一组内核线程处理,每个线程在一个 CPU 上运行。一旦有要安排的工作,您就将该工作排入全局工作队列,该工作将在适当的时刻执行。另一种方法是在专用内核线程中运行工作队列。这意味着每当需要执行工作队列处理程序时,将唤醒您的内核线程来处理它,而不是默认的预定义线程之一。
根据您选择的是共享工作队列还是专用工作队列,要调用的结构和函数是不同的。
内核全局工作队列-共享队列
除非您别无选择,或者需要关键性能,或者需要从工作队列初始化到工作调度的所有控制,并且只偶尔提交任务,否则应该使用内核提供的共享工作队列。由于该队列在整个系统中共享,因此您应该友好,并且不应该长时间垄断队列。
由于在每个 CPU 上对队列中的挂起任务的执行是串行化的,因此您不应该长时间休眠,因为在您醒来之前,队列中的其他任务将不会运行。您甚至不知道与您共享工作队列的是谁,因此如果您的任务需要更长时间才能获得 CPU,也不要感到惊讶。共享工作队列中的工作在由内核创建的每个 CPU 线程中执行。
在这种情况下,工作还必须使用INIT_WORK宏进行初始化。由于我们将使用共享工作队列,因此无需创建工作队列结构。我们只需要作为参数传递的work_struct结构。有三个函数可以在共享工作队列上安排工作:
- 将工作绑定到当前 CPU 的版本:
int schedule_work(struct work_struct *work);
- 相同但带有延迟功能:
static inline bool schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
- 实际在给定 CPU 上安排工作的函数:
int schedule_work_on(int cpu, struct work_struct *work);
- 与之前显示的相同,但带有延迟:
int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay);
所有这些函数都将作为参数安排到系统的共享工作队列system_wq中,该队列在kernel/workqueue.c中定义:
struct workqueue_struct *system_wq __read_mostly;
EXPORT_SYMBOL(system_wq);
已经提交到共享队列的工作可以使用cancel_delayed_work函数取消。您可以使用以下方法刷新共享工作队列:
void flush_scheduled_work(void);
由于队列在整个系统中共享,因此在flush_scheduled_work()返回之前,人们无法真正知道它可能持续多长时间:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h> /* for sleep */
#include <linux/wait.h> /* for wait queue */
#include <linux/time.h>
#include <linux/delay.h>
#include <linux/slab.h> /* for kmalloc() */
#include <linux/workqueue.h>
//static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int sleep = 0;
struct work_data {
struct work_struct my_work;
wait_queue_head_t my_wq;
int the_data;
};
static void work_handler(struct work_struct *work)
{
struct work_data *my_data = container_of(work, \
struct work_data, my_work);
printk("Work queue module handler: %s, data is %d\n", __FUNCTION__, my_data->the_data);
msleep(2000);
wake_up_interruptible(&my_data->my_wq);
kfree(my_data);
}
static int __init my_init(void)
{
struct work_data * my_data;
my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL);
my_data->the_data = 34;
INIT_WORK(&my_data->my_work, work_handler);
init_waitqueue_head(&my_data->my_wq);
schedule_work(&my_data->my_work);
printk("I'm goint to sleep ...\n");
wait_event_interruptible(my_data->my_wq, sleep != 0);
printk("I am Waked up...\n");
return 0;
}
static void __exit my_exit(void)
{
printk("Work queue module exit: %s %d\n", __FUNCTION__, __LINE__);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com> ");
MODULE_DESCRIPTION("Shared workqueue");
为了将数据传递给我的工作队列处理程序,您可能已经注意到在这两个示例中,我将我的work_struct结构嵌入到自定义数据结构中,并使用container_of来检索它。这是将数据传递给工作队列处理程序的常用方法。
专用工作队列
在这里,工作队列表示为struct workqueue_struct的一个实例。要排入工作队列的工作表示为struct work_struct的一个实例。在将您的工作安排到自己的内核线程之前,有四个步骤:
-
声明/初始化一个
struct workqueue_struct。 -
创建您的工作函数。
-
创建一个
struct work_struct,以便将您的工作函数嵌入其中。 -
将您的工作函数嵌入
work_struct。
编程语法
以下函数在include/linux/workqueue.h中定义:
- 声明工作和工作队列:
struct workqueue_struct *myqueue;
struct work_struct thework;
- 定义工作函数(处理程序):
void dowork(void *data) { /* Code goes here */ };
- 初始化我们的工作队列并嵌入我们的工作:
myqueue = create_singlethread_workqueue( "mywork" );
INIT_WORK( &thework, dowork, <data-pointer> );
我们也可以通过一个名为 create_workqueue 的宏创建我们的工作队列。create_workqueue 和 create_singlethread_workqueue 之间的区别在于前者将创建一个工作队列,该工作队列将为每个可用的处理器创建一个单独的内核线程。
- 调度工作:
queue_work(myqueue, &thework);
在给定的延迟时间后排队到给定的工作线程:
queue_dalayed_work(myqueue, &thework, <delay>);
如果工作已经在队列中,则这些函数返回 false,如果不在队列中则返回 true。delay 表示排队前等待的 jiffies 数。您可以使用辅助函数 msecs_to_jiffies 将标准毫秒延迟转换为 jiffies。例如,要在 5 毫秒后排队工作,可以使用 queue_delayed_work(myqueue, &thework, msecs_to_jiffies(5));。
- 等待给定工作队列上的所有待处理工作:
void flush_workqueue(struct workqueue_struct *wq)
flush_workqueue 等待直到所有排队的工作都完成执行。新进入的(排队的)工作不会影响等待。通常可以在驱动程序关闭处理程序中使用这个函数。
- 清理:
使用 cancel_work_sync() 或 cancel_delayed_work_sync 进行同步取消,如果工作尚未运行,将取消工作,或者阻塞直到工作完成。即使工作重新排队,也将被取消。您还必须确保在处理程序返回之前,最后排队的工作队列不会被销毁。这些函数分别用于非延迟或延迟工作:
int cancel_work_sync(struct work_struct *work);
int cancel_delayed_work_sync(struct delayed_work *dwork);
自 Linux 内核 v4.8 起,可以使用 cancel_work 或 cancel_delayed_work,这是取消的异步形式。必须检查函数是否返回 true 或 false,并确保工作不会重新排队。然后必须显式刷新工作队列:
if ( !cancel_delayed_work( &thework) ){
flush_workqueue(myqueue);
destroy_workqueue(myqueue);
}
另一个是相同方法的不同版本,将为所有处理器创建一个线程。如果需要在工作排队之前延迟,请随时使用以下工作初始化宏:
INIT_DELAYED_WORK(_work, _func);
INIT_DELAYED_WORK_DEFERRABLE(_work, _func);
使用上述宏意味着您应该使用以下函数在工作队列中排队或调度工作:
int queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay)
queue_work 将工作绑定到当前 CPU。您可以使用 queue_work_on 函数指定处理程序应在哪个 CPU 上运行:
int queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work);
对于延迟工作,您可以使用:
int queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay);
以下是使用专用工作队列的示例:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h> /* for work queue */
#include <linux/slab.h> /* for kmalloc() */
struct workqueue_struct *wq;
struct work_data {
struct work_struct my_work;
int the_data;
};
static void work_handler(struct work_struct *work)
{
struct work_data * my_data = container_of(work,
struct work_data, my_work);
printk("Work queue module handler: %s, data is %d\n",
__FUNCTION__, my_data->the_data);
kfree(my_data);
}
static int __init my_init(void)
{
struct work_data * my_data;
printk("Work queue module init: %s %d\n",
__FUNCTION__, __LINE__);
wq = create_singlethread_workqueue("my_single_thread");
my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL);
my_data->the_data = 34;
INIT_WORK(&my_data->my_work, work_handler);
queue_work(wq, &my_data->my_work);
return 0;
}
static void __exit my_exit(void)
{
flush_workqueue(wq);
destroy_workqueue(wq);
printk("Work queue module exit: %s %d\n",
__FUNCTION__, __LINE__);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
预定义(共享)工作队列和标准工作队列函数
预定义的工作队列在 kernel/workqueue.c 中定义如下:
struct workqueue_struct *system_wq __read_mostly;
它只是一个标准工作,内核为其提供了一个简单包装标准工作的自定义 API。
内核预定义的工作队列函数与标准工作队列函数的比较如下:
| 预定义工作队列函数 | 等效标准工作队列函数 |
|---|---|
schedule_work(w) |
queue_work(keventd_wq,w) |
schedule_delayed_work(w,d) |
queue_delayed_work(keventd_wq,w,d)(在任何 CPU 上) |
schedule_delayed_work_on(cpu,w,d) |
queue_delayed_work(keventd_wq,w,d)(在给定的 CPU 上) |
flush_scheduled_work() |
flush_workqueue(keventd_wq) |
内核线程
工作队列运行在内核线程之上。当您使用工作队列时,已经在使用内核线程。这就是为什么我决定不谈论内核线程 API 的原因。
内核中断机制
中断是设备停止内核的方式,告诉内核发生了有趣或重要的事情。在 Linux 系统上称为 IRQ。中断提供的主要优势是避免设备轮询。由设备告知其状态是否发生变化;不是由我们轮询它。
为了在中断发生时得到通知,您需要注册到该 IRQ,提供一个称为中断处理程序的函数,每次引发该中断时都会调用它。
注册中断处理程序
您可以注册一个回调函数,在您感兴趣的中断(或中断线)被触发时运行。您可以使用<linux/interrupt.h>中声明的request_irq()函数来实现这一点。
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev)
request_irq()可能会失败,并在成功时返回0。前面代码的其他元素如下所述:
-
flags:这些应该是<linux/interrupt.h>中定义的掩码的位掩码。最常用的是: -
IRQF_TIMER:通知内核,此处理程序由系统定时器中断发起。 -
IRQF_SHARED:用于可以被两个或更多设备共享的中断线。共享同一线的每个设备都必须设置此标志。如果省略,只能为指定的 IRQ 线注册一个处理程序。 -
IRQF_ONESHOT:主要用于线程化的 IRQ。它指示内核在硬中断处理程序完成后不要重新启用中断。它将保持禁用状态,直到线程处理程序运行。 -
在旧的内核版本(直到 v2.6.35),有
IRQF_DISABLED标志,它要求内核在处理程序运行时禁用所有中断。现在不再使用这个标志。 -
name:这由内核用于在/proc/interrupts和/proc/irq中标识您的驱动程序。 -
dev:其主要目标是作为处理程序的参数传递。这应该对每个注册的处理程序都是唯一的,因为它用于标识设备。对于非共享的 IRQ,它可以是NULL,但对于共享的 IRQ 则不行。通常的使用方式是提供一个device结构,因为它既是唯一的,也可能对处理程序有用。也就是说,任何与设备相关的数据结构的指针都是足够的:
struct my_data {
struct input_dev *idev;
struct i2c_client *client;
char name[64];
char phys[32];
};
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *md = dev_id;
unsigned char nextstate = read_state(lp);
/* Check whether my device raised the irq or no */
[...]
return IRQ_HANDLED;
}
/* some where in the code, in the probe function */
int ret;
struct my_data *md;
md = kzalloc(sizeof(*md), GFP_KERNEL);
ret = request_irq(client->irq, my_irq_handler,
IRQF_TRIGGER_LOW | IRQF_ONESHOT,
DRV_NAME, md);
/* far in the release function */
free_irq(client->irq, md);
handler:这是当中断触发时将运行的回调函数。中断处理程序的结构如下:
static irqreturn_t my_irq_handler(int irq, void *dev)
-
这包含以下代码元素:
-
irq:IRQ 的数值(与request_irq中使用的相同)。 -
dev:与request_irq中使用的相同。
这两个参数由内核传递给您的处理程序。处理程序只能返回两个值,取决于您的设备是否引起了 IRQ:
-
IRQ_NONE:您的设备不是该中断的发起者(这在共享的 IRQ 线上经常发生) -
IRQ_HANDLED:您的设备引起了中断
根据处理情况,可以使用IRQ_RETVAL(val)宏,如果值非零,则返回IRQ_HANDLED,否则返回IRQ_NONE。
在编写中断处理程序时,您不必担心重入性,因为内核会在所有处理器上禁用服务的 IRQ 线,以避免递归中断。
释放先前注册的处理程序的相关函数是:
void free_irq(unsigned int irq, void *dev)
如果指定的 IRQ 不是共享的,free_irq不仅会删除处理程序,还会禁用该线路。如果是共享的,只有通过dev(应该与request_irq中使用的相同)标识的处理程序被删除,但中断线路仍然存在,只有在最后一个处理程序被删除时才会被禁用。free_irq将阻塞,直到指定 IRQ 的所有执行中断完成。然后,您必须避免在中断上下文中同时使用request_irq和free_irq。
中断处理程序和锁
不用说,您处于原子上下文中,只能使用自旋锁进行并发。每当全局数据可被用户代码(用户任务;即系统调用)和中断代码访问时,这些共享数据应该在用户代码中由spin_lock_irqsave()保护。让我们看看为什么我们不能只使用spin_lock。中断处理程序将始终优先于用户任务,即使该任务持有自旋锁。简单地禁用 IRQ 是不够的。中断可能发生在另一个 CPU 上。如果用户任务更新数据时被中断处理程序尝试访问相同的数据,那将是一场灾难。使用spin_lock_irqsave()将在本地 CPU 上禁用所有中断,防止系统调用被任何类型的中断中断:
ssize_t my_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
unsigned long flags;
/* some stuff */
[...]
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
data++;
spin_unlock_irqrestore(&my_lock, flags)
[...]
}
static irqreturn_t my_interrupt_handler(int irq, void *p)
{
/*
* preemption is disabled when running interrupt handler
* also, the serviced irq line is disabled until the handler has completed
* no need then to disable all other irq. We just use spin_lock and
* spin_unlock
*/
spin_lock(&my_lock);
/* process data */
[...]
spin_unlock(&my_lock);
return IRQ_HANDLED;
}
在不同的中断处理程序之间共享数据(即,同一驱动程序管理两个或多个设备,每个设备都有自己的 IRQ 线),应该在这些处理程序中使用spin_lock_irqsave()来保护数据,以防止其他 IRQ 被触发并且无用地旋转。
底半部分的概念
底半部分是一种将中断处理程序分成两部分的机制。这引入了另一个术语,即顶半部分。在讨论它们各自之前,让我们谈谈它们的起源以及它们解决了什么问题。
问题-中断处理程序设计的限制
无论中断处理程序是否持有自旋锁,都会在运行该处理程序的 CPU 上禁用抢占。在处理程序中浪费的时间越多,分配给其他任务的 CPU 就越少,这可能会显着增加其他中断的延迟,从而增加整个系统的延迟。挑战在于尽快确认引发中断的设备,以保持系统的响应性。
在 Linux 系统(实际上在所有操作系统上,根据硬件设计),任何中断处理程序都会在所有处理器上禁用其当前中断线,并且有时您可能需要在实际运行处理程序的 CPU 上禁用所有中断,但绝对不想错过中断。为了满足这个需求,引入了halves的概念。
解决方案-底半部分
这个想法是将处理程序分成两部分:
-
第一部分称为顶半部分或硬中断,它是使用
request_irq()注册的函数,最终会掩盖/隐藏中断(在当前 CPU 上,除了正在服务的 CPU,因为内核在运行处理程序之前已经禁用了它),根据需要执行快速操作(基本上是时间敏感的任务,读/写硬件寄存器以及对这些数据的快速处理),安排第二部分和下一个部分,然后确认该线路。所有被禁用的中断必须在退出底半部分之前重新启用。 -
第二部分,称为底半部分,将处理耗时的任务,并在重新启用中断时运行。这样,您就有机会不会错过中断。
底半部分是使用工作推迟机制设计的,我们之前已经看到了。根据您选择的是哪一个,它可能在(软件)中断上下文中运行,或者在进程上下文中运行。底半部分的机制有:
-
软中断
-
任务 let
-
工作队列
-
线程中断
软中断和任务 let 在(软件)中断上下文中执行(意味着抢占被禁用),工作队列和线程中断在进程(或简单任务)上下文中执行,并且可以被抢占,但没有什么可以阻止我们改变它们的实时属性以适应您的需求并改变它们的抢占行为(参见CONFIG_PREEMPT或CONFIG_PREEMPT_VOLUNTARY。这也会影响整个系统)。底半部分并不总是可能的。但当可能时,这绝对是最好的选择。
任务 let 作为底半部分
任务延迟机制在 DMA、网络和块设备驱动程序中最常用。只需在内核源代码中尝试以下命令:
grep -rn tasklet_schedule
现在让我们看看如何在我们的中断处理程序中实现这样的机制:
struct my_data {
int my_int_var;
struct tasklet_struct the_tasklet;
int dma_request;
};
static void my_tasklet_work(unsigned long data)
{
/* Do what ever you want here */
}
struct my_data *md = init_my_data;
/* somewhere in the probe or init function */
[...]
tasklet_init(&md->the_tasklet, my_tasklet_work,
(unsigned long)md);
[...]
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *md = dev_id;
/* Let's schedule our tasklet */
tasklet_schedule(&md.dma_tasklet);
return IRQ_HANDLED;
}
在上面的示例中,我们的 tasklet 将执行函数my_tasklet_work()。
工作队列作为底半部分。
让我们从一个示例开始:
static DECLARE_WAIT_QUEUE_HEAD(my_wq); /* declare and init the wait queue */
static struct work_struct my_work;
/* some where in the probe function */
/*
* work queue initialization. "work_handler" is the call back that will be
* executed when our work is scheduled.
*/
INIT_WORK(my_work, work_handler);
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
uint32_t val;
struct my_data = dev_id;
val = readl(my_data->reg_base + REG_OFFSET);
if (val == 0xFFCD45EE)) {
my_data->done = true;
wake_up_interruptible(&my_wq);
} else {
schedule_work(&my_work);
}
return IRQ_HANDLED;
};
在上面的示例中,我们使用等待队列或工作队列来唤醒可能正在等待我们的进程,或者根据寄存器的值安排工作。我们没有共享的数据或资源,因此不需要禁用所有其他 IRQs(spin_lock_irq_disable)。
Softirqs 作为底半部分
正如本章开头所说,我们不会讨论 softirq。在你感觉需要使用 softirqs 的任何地方,tasklets 都足够了。无论如何,让我们谈谈它们的默认值。
Softirq 在软件中断上下文中运行,禁用了抢占,保持 CPU 直到它们完成。Softirq 应该很快;否则它们可能会减慢系统。当由于任何原因 softirq 阻止内核调度其他任务时,任何新进入的 softirq 将由ksoftirqd线程处理,运行在进程上下文中。
线程化的 IRQs
线程化的 IRQs 的主要目标是将中断禁用的时间减少到最低限度。使用线程化的 IRQs,注册中断处理程序的方式有些简化。你甚至不需要自己安排底半部分。核心会为我们做这件事。然后底半部分将在一个专用的内核线程中执行。我们不再使用request_irq(),而是使用request_threaded_irq():
int request_threaded_irq(unsigned int irq, irq_handler_t handler,\
irq_handler_t thread_fn, \
unsigned long irqflags, \
const char *devname, void *dev_id)
request_threaded_irq()函数在其参数中接受两个函数:
-
@handler 函数:这与使用
request_irq()注册的函数相同。它代表顶半部分函数,运行在原子上下文(或硬中断)中。如果它可以更快地处理中断,以至于你可以完全摆脱底半部分,它应该返回IRQ_HANDLED。但是,如果中断处理需要超过 100 微秒,如前面讨论的那样,你应该使用底半部分。在这种情况下,它应该返回IRQ_WAKE_THREAD,这将导致调度必须已经提供的thread_fn函数。 -
@thread_fn 函数:这代表了底半部分,就像你在顶半部分中安排的那样。当硬中断处理程序(处理函数)返回
IRQ_WAKE_THREAD时,与该底半部分相关联的 kthread 将被调度,在运行 ktread 时调用thread_fn函数。thread_fn函数在完成时必须返回IRQ_HANDLED。执行完毕后,kthread 将不会再次被调度,直到再次触发 IRQ 并且硬中断返回IRQ_WAKE_THREAD。
在任何你会使用工作队列来安排底半部分的地方,都可以使用线程化的 IRQs。必须定义handler和thread_fn以正确使用线程化的 IRQ。如果handler为NULL且thread_fn != NULL(见下文),内核将安装默认的硬中断处理程序,它将简单地返回IRQ_WAKE_THREAD以安排底半部分。handler总是在中断上下文中调用,无论是由你自己提供还是默认情况下由内核提供的。
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
[...]
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
[...]
}
EXPORT_SYMBOL(request_threaded_irq);
使用线程化的 IRQs,处理程序的定义不会改变,但它的注册方式会有一点变化。
request_irq(unsigned int irq, irq_handler_t handler, \
unsigned long flags, const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, \
name, dev);
}
线程化的底半部分
以下简单的摘录演示了如何实现线程化的底半部分机制:
static irqreturn_t pcf8574_kp_irq_handler(int irq, void *dev_id)
{
struct custom_data *lp = dev_id;
unsigned char nextstate = read_state(lp);
if (lp->laststate != nextstate) {
int key_down = nextstate < ARRAY_SIZE(lp->btncode);
unsigned short keycode = key_down ?
p->btncode[nextstate] : lp->btncode[lp->laststate];
input_report_key(lp->idev, keycode, key_down);
input_sync(lp->idev);
lp->laststate = nextstate;
}
return IRQ_HANDLED;
}
static int pcf8574_kp_probe(struct i2c_client *client, \
const struct i2c_device_id *id)
{
struct custom_data *lp = init_custom_data();
[...]
/*
* @handler is NULL and @thread_fn != NULL
* the default primary handler is installed, which will
* return IRQ_WAKE_THREAD, that will schedule the thread
* asociated to the bottom half. the bottom half must then
* return IRQ_HANDLED when finished
*/
ret = request_threaded_irq(client->irq, NULL, \
pcf8574_kp_irq_handler, \
IRQF_TRIGGER_LOW | IRQF_ONESHOT, \
DRV_NAME, lp);
if (ret) {
dev_err(&client->dev, "IRQ %d is not free\n", \
client->irq);
goto fail_free_device;
}
ret = input_register_device(idev);
[...]
}
当中断处理程序被执行时,所有 CPU 上的服务 IRQ 始终被禁用,并在硬件 IRQ(顶半部)完成时重新启用。但是,如果出于任何原因,您需要在顶半部完成后不重新启用 IRQ 线,并且保持禁用直到线程处理程序运行完毕,您应该使用启用了 IRQF_ONESHOT 标志的线程 IRQ(只需像之前显示的那样执行 OR 操作)。然后 IRQ 线将在底半部完成后重新启用。
从内核调用用户空间应用程序
用户空间应用程序大多数情况下是由其他应用程序从用户空间调用的。不深入细节,让我们看一个例子:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h> /* for work queue */
#include <linux/kmod.h>
static struct delayed_work initiate_shutdown_work;
static void delayed_shutdown( void )
{
char *cmd = "/sbin/shutdown";
char *argv[] = {
cmd,
"-h",
"now",
NULL,
};
char *envp[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL,
};
call_usermodehelper(cmd, argv, envp, 0);
}
static int __init my_shutdown_init( void )
{
schedule_delayed_work(&delayed_shutdown, msecs_to_jiffies(200));
return 0;
}
static void __exit my_shutdown_exit( void )
{
return;
}
module_init( my_shutdown_init );
module_exit( my_shutdown_exit );
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu", <john.madieu@gmail.com>);
MODULE_DESCRIPTION("Simple module that trigger a delayed shut down");
在前面的例子中,使用的 API(call_usermodehelper)是 Usermode-helper API 的一部分,所有函数都在 kernel/kmod.c 中定义。它的使用非常简单;只需查看 kmod.c 就能给你一个想法。您可能想知道这个 API 是为什么定义的。例如,内核使用它进行模块(卸载)和 cgroups 管理。
总结
在本章中,我们讨论了开始驱动程序开发的基本元素,介绍了驱动程序中经常使用的每种机制。本章非常重要,因为它涉及到本书其他章节依赖的主题。例如,下一章将处理字符设备,将使用本章讨论的一些元素。
第四章:字符设备驱动程序
字符设备通过字符的方式(一个接一个)向用户应用程序传输数据,就像串行端口一样。字符设备驱动程序通过/dev目录中的特殊文件公开设备的属性和功能,可以用来在设备和用户应用程序之间交换数据,并且还允许你控制真实的物理设备。这是 Linux 的基本概念,即一切都是文件。字符设备驱动程序代表内核源代码中最基本的设备驱动程序。字符设备在内核中表示为include/linux/cdev.h中定义的struct cdev的实例:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
本章将介绍字符设备驱动程序的具体特性,解释它们如何创建、识别和向系统注册设备,还将更好地概述设备文件方法,这些方法是内核向用户空间公开设备功能的方法,可通过使用与文件相关的系统调用(read,write,select,open,close等)访问,描述在struct file_operations结构中,这些你肯定以前听说过。
主要和次要背后的概念
字符设备位于/dev目录中。请注意,它们不是该目录中唯一的文件。字符设备文件可以通过其类型识别,我们可以通过ls -l命令显示。主要和次要标识并将设备与驱动程序绑定。让我们看看它是如何工作的,通过列出*/dev*目录的内容(ls -l /dev):
[...]
drwxr-xr-x 2 root root 160 Mar 21 08:57 input
crw-r----- 1 root kmem 1, 2 Mar 21 08:57 kmem
lrwxrwxrwx 1 root root 28 Mar 21 08:57 log -> /run/systemd/journal/dev-log
crw-rw---- 1 root disk 10, 237 Mar 21 08:57 loop-control
brw-rw---- 1 root disk 7, 0 Mar 21 08:57 loop0
brw-rw---- 1 root disk 7, 1 Mar 21 08:57 loop1
brw-rw---- 1 root disk 7, 2 Mar 21 08:57 loop2
brw-rw---- 1 root disk 7, 3 Mar 21 08:57 loop3
给定上述摘录,第一列的第一个字符标识文件类型。可能的值有:
-
c:这是用于字符设备文件 -
b:这是用于块设备文件 -
l:这是用于符号链接 -
d:这是用于目录 -
s:这是用于套接字 -
p:这是用于命名管道
对于b和c文件类型,在日期之前的第五和第六列遵循<X,Y>模式。X代表主要号,Y是次要号。例如,第三行是<1,2>,最后一行是<7,3>。这是一种从用户空间识别字符设备文件及其主要和次要的经典方法之一。
内核在dev_t类型变量中保存标识设备的数字,它们只是u32(32 位无符号长整型)。主要号仅用 12 位表示,而次要号编码在剩余的 20 位上。
正如可以在include/linux/kdev_t.h中看到的,给定一个dev_t类型的变量,可能需要提取次要或主要。内核为这些目的提供了一个宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
另一方面,你可能有一个次要和一个主要,需要构建一个dev_t。你应该使用的宏是MKDEV(int major, int minor);:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备注册时使用一个标识设备的主要号和一个次要号,可以将次要号用作本地设备列表的数组索引,因为同一驱动程序的一个实例可能处理多个设备,而不同的驱动程序可能处理相同类型的不同设备。
设备号分配和释放
设备号标识系统中的设备文件。这意味着,有两种分配这些设备号(实际上是主要和次要)的方法:
- 静态:使用
register_chrdev_region()函数猜测尚未被其他驱动程序使用的主要号。应尽量避免使用这个。它的原型如下:
int register_chrdev_region(dev_t first, unsigned int count, \
char *name);
该方法在成功时返回0,在失败时返回负错误代码。first由我们需要的主要号和所需范围的第一个次要号组成。应该使用MKDEV(ma,mi)。count是所需的连续设备号的数量,name应该是相关设备或驱动程序的名称。
- 动态地:让内核为我们做这件事,使用
alloc_chrdev_region()函数。这是获取有效设备号的推荐方法。它的原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, \
unsigned int count, char *name);
该方法在成功时返回0,在失败时返回负错误代码。dev是唯一的输出参数。它代表内核分配的第一个号码。firstminor是请求的次要号码范围的第一个,count是所需的次要号码数量,name应该是相关设备或驱动程序的名称。
两者之间的区别在于,对于前者,我们应该预先知道我们需要什么号码。这是注册:告诉内核我们想要什么设备号。这可能用于教学目的,并且只要驱动程序的唯一用户是您,它就可以工作。但是当要在另一台机器上加载驱动程序时,无法保证所选的号码在该机器上是空闲的,这将导致冲突和麻烦。第二种方法更干净、更安全,因为内核负责为我们猜测正确的号码。我们甚至不必关心在将模块加载到另一台机器上时的行为会是什么,因为内核会相应地进行调整。
无论如何,通常不直接从驱动程序中调用前面的函数,而是通过驱动程序依赖的框架(IIO 框架、输入框架、RTC 等)通过专用 API 进行屏蔽。这些框架在本书的后续章节中都有讨论。
设备文件操作简介
可以在文件上执行的操作取决于管理这些文件的驱动程序。这些操作在内核中被定义为struct file_operations的实例。struct file_operations公开了一组回调函数,这些函数将处理文件上的任何用户空间系统调用。例如,如果希望用户能够对表示我们设备的文件执行write操作,就必须实现与write函数对应的回调,并将其添加到与您的设备绑定的struct file_operations中。让我们填写一个文件操作结构:
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 *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
int (*flock) (struct file *, int, struct file_lock *);
[...]
};
前面的摘录只列出了结构的重要方法,特别是对本书需求相关的方法。可以在内核源码的include/linux/fs.h中找到完整的描述。这些回调函数中的每一个都与系统调用相关联,没有一个是强制性的。当用户代码对给定文件调用与文件相关的系统调用时,内核会寻找负责该文件的驱动程序(特别是创建文件的驱动程序),找到其struct file_operations结构,并检查与系统调用匹配的方法是否已定义。如果是,就简单地运行它。如果没有,就返回一个错误代码,这取决于系统调用。例如,未定义的(*mmap)方法将返回-ENODEV给用户,而未定义的(*write)方法将返回-EINVAL。
内核中的文件表示
内核将文件描述为struct inode的实例(而不是struct file),该结构在include/linux/fs.h中定义:
struct inode {
[...]
struct pipe_inode_info *i_pipe; /* Set and used if this is a
*linux kernel pipe */
struct block_device *i_bdev; /* Set and used if this is a
* a block device */
struct cdev *i_cdev; /* Set and used if this is a
* character device */
[...]
}
struct inode是一个文件系统数据结构,保存着关于文件(无论其类型是字符、块、管道等)或目录(是的!从内核的角度来看,目录是一个文件,它指向其他文件)的与操作系统相关的信息。
struct file结构(也在include/linux/fs.h中定义)实际上是内核中表示打开文件的更高级别的文件描述,它依赖于较低级别的struct inode数据结构:
struct file {
[...]
struct path f_path; /* Path to the file */
struct inode *f_inode; /* inode associated to this file */
const struct file_operations *f_op;/* operations that can be
* performed on this file
*/
loff_t f_pos; /* Position of the cursor in
* this file */
/* needed for tty driver, and maybe others */
void *private_data; /* private data that driver can set
* in order to share some data between file
* operations. This can point to any data
* structure.
*/
[...]
}
struct inode和struct file之间的区别在于 inode 不跟踪文件内的当前位置或当前模式。它只包含帮助操作系统找到底层文件结构(管道、目录、常规磁盘文件、块/字符设备文件等)内容的东西。另一方面,struct file被用作通用结构(实际上它持有一个指向struct inode结构的指针),代表并打开文件并提供一组与在底层文件结构上执行的方法相关的函数。这些方法包括:open,write,seek,read,select等。所有这些都强调了 UNIX 系统的哲学,即一切皆为文件。
换句话说,struct inode代表内核中的一个文件,struct file描述了它在实际打开时的情况。可能有不同的文件描述符代表同一个文件被多次打开,但这些将指向相同的 inode。
分配和注册字符设备
在内核中,字符设备被表示为struct cdev的实例。当编写字符设备驱动程序时,您的目标是最终创建并注册与struct file_operations相关联的该结构的实例,暴露一组用户空间可以对设备执行的操作(函数)。为了实现这个目标,我们必须经历一些步骤,如下所示:
-
使用
alloc_chrdev_region()保留一个主设备号和一系列次设备号。 -
使用
class_create()为您的设备创建一个类,在/sys/class/中可见。 -
设置一个
struct file_operation(要提供给cdev_init),并为每个需要创建的设备调用cdev_init()和cdev_add()来注册设备。 -
然后为每个设备创建一个
device_create(),并赋予一个适当的名称。这将导致您的设备在/dev目录中被创建:
#define EEP_NBANK 8
#define EEP_DEVICE_NAME "eep-mem"
#define EEP_CLASS "eep-class"
struct class *eep_class;
struct cdev eep_cdev[EEP_NBANK];
dev_t dev_num;
static int __init my_init(void)
{
int i;
dev_t curr_dev;
/* Request the kernel for EEP_NBANK devices */
alloc_chrdev_region(&dev_num, 0, EEP_NBANK, EEP_DEVICE_NAME);
/* Let's create our device's class, visible in /sys/class */
eep_class = class_create(THIS_MODULE, EEP_CLASS);
/* Each eeprom bank represented as a char device (cdev) */
for (i = 0; i < EEP_NBANK; i++) {
/* Tie file_operations to the cdev */
cdev_init(&my_cdev[i], &eep_fops);
eep_cdev[i].owner = THIS_MODULE;
/* Device number to use to add cdev to the core */
curr_dev = MKDEV(MAJOR(dev_num), MINOR(dev_num) + i);
/* Now make the device live for the users to access */
cdev_add(&eep_cdev[i], curr_dev, 1);
/* create a device node each device /dev/eep-mem0, /dev/eep-mem1,
* With our class used here, devices can also be viewed under
* /sys/class/eep-class.
*/
device_create(eep_class,
NULL, /* no parent device */
curr_dev,
NULL, /* no additional data */
EEP_DEVICE_NAME "%d", i); /* eep-mem[0-7] */
}
return 0;
}
编写文件操作
在引入上述文件操作之后,是时候实现它们以增强驱动程序的功能并将设备的方法暴露给用户空间(通过系统调用)。这些方法各有其特点,我们将在本节中进行重点介绍。
在内核空间和用户空间之间交换数据
本节不描述任何驱动程序文件操作,而是介绍一些内核设施,可以用来编写这些驱动程序方法。驱动程序的write()方法包括从用户空间读取数据到内核空间,然后从内核处理该数据。这样的处理可能是像推送数据到设备一样。另一方面,驱动程序的read()方法包括将数据从内核复制到用户空间。这两种方法都引入了我们需要在跳转到各自步骤之前讨论的新元素。第一个是__user。__user是由稀疏(内核用于查找可能的编码错误的语义检查器)使用的一个标记,用于让开发人员知道他实际上将要不正确地使用一个不受信任的指针(或者在当前虚拟地址映射中可能无效的指针),并且他不应该解引用,而应该使用专用的内核函数来访问该指针指向的内存。
这使我们能够引入不同的内核函数,以便访问这样的内存,无论是读取还是写入。这些分别是copy_from_user()和copy_from_user(),用于将缓冲区从用户空间复制到内核空间,反之亦然,将缓冲区从内核复制到用户空间:
unsigned long copy_from_user(void *to, const void __user *from,
unsigned long n)
unsigned long copy_to_user(void __user *to, const void *from,
unsigned long n)
在这两种情况下,以__user为前缀的指针指向用户空间(不受信任)内存。n代表要复制的字节数。from代表源地址,to是目标地址。这些返回未能复制的字节数。成功时,返回值应为0。
请注意,使用copy_to_user(),如果无法复制某些数据,函数将使用零字节填充已复制的数据以达到请求的大小。
单个值复制
在复制char和int等单个和简单变量时,但不是在复制结构或数组等较大的数据类型时,内核提供了专用宏以快速执行所需的操作。这些宏是put_user(x, ptr)和get_used(x, ptr),解释如下:
-
put_user(x, ptr);:此宏将变量从内核空间复制到用户空间。x表示要复制到用户空间的值,ptr是用户空间中的目标地址。该宏在成功时返回0,在错误时返回-EFAULT。x必须可分配给解引用ptr的结果。换句话说,它们必须具有(或指向)相同的类型。 -
get_user(x, ptr);:此宏将变量从用户空间复制到内核空间,并在成功时返回0,在错误时返回-EFAULT。请注意,错误时x设置为0。x表示要存储结果的内核变量,ptr是用户空间中的源地址。解引用ptr的结果必须可分配给x而不需要转换。猜猜它是什么意思。
打开方法
open是每次有人打开设备文件时调用的方法。如果未定义此方法,则设备打开将始终成功。通常使用此方法来执行设备和数据结构初始化,并在出现问题时返回负错误代码,或0。open方法的原型定义如下:
int (*open)(struct inode *inode, struct file *filp);
每个设备的数据
对于在您的字符设备上执行的每个open,回调函数将以struct inode作为参数,该参数是文件的内核底层表示。该struct inode结构具有一个名为i_cdev的字段,指向我们在init函数中分配的cdev。通过在以下示例中的struct pcf2127中将struct cdev嵌入到我们的设备特定数据中,我们将能够使用container_of宏获取指向该特定数据的指针。以下是一个open方法示例。
以下是我们的数据结构:
struct pcf2127 {
struct cdev cdev;
unsigned char *sram_data;
struct i2c_client *client;
int sram_size;
[...]
};
根据这个数据结构,open方法将如下所示:
static unsigned int sram_major = 0;
static struct class *sram_class = NULL;
static int sram_open(struct inode *inode, struct file *filp)
{
unsigned int maj = imajor(inode);
unsigned int min = iminor(inode);
struct pcf2127 *pcf = NULL;
pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
pcf->sram_size = SRAM_SIZE;
if (maj != sram_major || min < 0 ){
pr_err ("device not found\n");
return -ENODEV; /* No such device */
}
/* prepare the buffer if the device is opened for the first time */
if (pcf->sram_data == NULL) {
pcf->sram_data = kzalloc(pcf->sram_size, GFP_KERNEL);
if (pcf->sram_data == NULL) {
pr_err("Open: memory allocation failed\n");
return -ENOMEM;
}
}
filp->private_data = pcf;
return 0;
}
释放方法
当设备关闭时,将调用release方法,这是open方法的反向操作。然后,您必须撤消在打开任务中所做的一切。您大致要做的是:
-
释放在“open()”步骤中分配的任何私有内存。
-
关闭设备(如果支持),并在最后关闭时丢弃每个缓冲区(如果设备支持多次打开,或者驱动程序可以同时处理多个设备)。
以下是release函数的摘录:
static int sram_release(struct inode *inode, struct file *filp)
{
struct pcf2127 *pcf = NULL;
pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
mutex_lock(&device_list_lock);
filp->private_data = NULL;
/* last close? */
pcf2127->users--;
if (!pcf2127->users) {
kfree(tx_buffer);
kfree(rx_buffer);
tx_buffer = NULL;
rx_buffer = NULL;
[...]
if (any_global_struct)
kfree(any_global_struct);
}
mutex_unlock(&device_list_lock);
return 0;
}
写入方法
“write()”方法用于向设备发送数据;每当用户应用程序在设备文件上调用write函数时,将调用内核实现。其原型如下:
ssize_t(*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos);
-
返回值是写入的字节数(大小)
-
*buf表示来自用户空间的数据缓冲区 -
count是请求传输的大小 -
*pos表示应在文件中写入数据的起始位置
写入步骤
以下步骤不描述任何标准或通用的方法来实现驱动程序的“write()”方法。它们只是概述了在此方法中可以执行的操作类型。
- 检查来自用户空间的错误或无效请求。如果设备公开其内存(eeprom、I/O 内存等),可能存在大小限制,则此步骤才相关:
/* if trying to Write beyond the end of the file, return error.
* "filesize" here corresponds to the size of the device memory (if any)
*/
if ( *pos >= filesize ) return -EINVAL;
- 调整
count以便不超出文件大小的剩余字节。这一步骤不是强制性的,与步骤 1 的条件相同:
/* filesize coerresponds to the size of device memory */
if (*pos + count > filesize)
count = filesize - *pos;
- 找到要开始写入的位置。如果设备具有内存,供“write()”方法写入给定数据,则此步骤才相关。与步骤 2 和 3 一样,此步骤不是强制性的:
/* convert pos into valid address */
void *from = pos_to_address( *pos );
- 从用户空间复制数据并将其写入适当的内核空间:
if (copy_from_user(dev->buffer, buf, count) != 0){
retval = -EFAULT;
goto out;
}
/* now move data from dev->buffer to physical device */
- 写入物理设备并在失败时返回错误:
write_error = device_write(dev->buffer, count);
if ( write_error )
return -EFAULT;
- 根据写入的字节数增加文件中光标的当前位置。最后,返回复制的字节数:
*pos += count;
Return count;
以下是write方法的一个示例。再次强调,这旨在给出一个概述:
ssize_t
eeprom_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct eeprom_dev *eep = filp->private_data;
ssize_t retval = 0;
/* step (1) */
if (*f_pos >= eep->part_size)
/* Writing beyond the end of a partition is not allowed. */
return -EINVAL;
/* step (2) */
if (*pos + count > eep->part_size)
count = eep->part_size - *pos;
/* step (3) */
int part_origin = PART_SIZE * eep->part_index;
int register_address = part_origin + *pos;
/* step(4) */
/* Copy data from user space to kernel space */
if (copy_from_user(eep->data, buf, count) != 0)
return -EFAULT;
/* step (5) */
/* perform the write to the device */
if (write_to_device(register_address, buff, count) < 0){
pr_err("ee24lc512: i2c_transfer failed\n");
return -EFAULT;
}
/* step (6) */
*f_pos += count;
return count;
}
读取方法
read()方法的原型如下:
ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *pos);
返回值是读取的大小。方法的其余元素在这里描述:
-
*buf是我们从用户空间接收的缓冲区 -
count是请求传输的大小(用户缓冲区的大小) -
*pos指示应从文件中读取数据的起始位置
读取步骤
- 防止读取超出文件大小,并返回文件末尾:
if (*pos >= filesize)
return 0; /* 0 means EOF */
- 读取的字节数不能超过文件大小。相应地调整
count:
if (*pos + count > filesize)
count = filesize - (*pos);
- 找到将开始读取的位置:
void *from = pos_to_address (*pos); /* convert pos into valid address */
- 将数据复制到用户空间缓冲区,并在失败时返回错误:
sent = copy_to_user(buf, from, count);
if (sent)
return -EFAULT;
- 根据读取的字节数提前文件的当前位置,并返回复制的字节数:
*pos += count;
Return count;
以下是一个驱动程序read()文件操作的示例,旨在概述可以在那里完成的工作:
ssize_t eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct eeprom_dev *eep = filp->private_data;
if (*f_pos >= EEP_SIZE) /* EOF */
return 0;
if (*f_pos + count > EEP_SIZE)
count = EEP_SIZE - *f_pos;
/* Find location of next data bytes */
int part_origin = PART_SIZE * eep->part_index;
int eep_reg_addr_start = part_origin + *pos;
/* perform the read from the device */
if (read_from_device(eep_reg_addr_start, buff, count) < 0){
pr_err("ee24lc512: i2c_transfer failed\n");
return -EFAULT;
}
/* copy from kernel to user space */
if(copy_to_user(buf, dev->data, count) != 0)
return -EIO;
*f_pos += count;
return count;
}
llseek 方法
当在文件内移动光标位置时,将调用llseek函数。该方法在用户空间的入口点是lseek()。可以参考 man 页面以打印用户空间中任一方法的完整描述:man llseek和man lseek。其原型如下:
loff_t(*llseek) (structfile *filp, loff_t offset, int whence);
-
返回值是文件中的新位置
-
loff_t是相对于当前文件位置的偏移量,定义了它将被改变多少 -
whence定义了从哪里寻找。可能的值有: -
SEEK_SET:这将光标放置在相对于文件开头的位置 -
SEEK_CUR:这将光标放置在相对于当前文件位置的位置 -
SEEK_END:这将光标调整到相对于文件末尾的位置
llseek 步骤
- 使用
switch语句检查每种可能的whence情况,因为它们是有限的,并相应地调整newpos:
switch( whence ){
case SEEK_SET:/* relative from the beginning of file */
newpos = offset; /* offset become the new position */
break;
case SEEK_CUR: /* relative to current file position */
newpos = file->f_pos + offset; /* just add offset to the current position */
break;
case SEEK_END: /* relative to end of file */
newpos = filesize + offset;
break;
default:
return -EINVAL;
}
- 检查
newpos是否有效:
if ( newpos < 0 )
return -EINVAL;
- 使用新位置更新
f_pos:
filp->f_pos = newpos;
- 返回新的文件指针位置:
return newpos;
以下是一个连续读取和搜索文件的用户程序示例。底层驱动程序将执行llseek()文件操作入口:
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <stdio.h>
#define CHAR_DEVICE "toto.txt"
int main(int argc, char **argv)
{
int fd= 0;
char buf[20];
if ((fd = open(CHAR_DEVICE, O_RDONLY)) < -1)
return 1;
/* Read 20 bytes */
if (read(fd, buf, 20) != 20)
return 1;
printf("%s\n", buf);
/* Move the cursor to 10 time, relative to its actual position */
if (lseek(fd, 10, SEEK_CUR) < 0)
return 1;
if (read(fd, buf, 20) != 20)
return 1;
printf("%s\n",buf);
/* Move the cursor ten time, relative from the beginig of the file */
if (lseek(fd, 7, SEEK_SET) < 0)
return 1;
if (read(fd, buf, 20) != 20)
return 1;
printf("%s\n",buf);
close(fd);
return 0;
}
代码产生以下输出:
jma@jma:~/work/tutos/sources$ cat toto.txt
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
jma@jma:~/work/tutos/sources$ ./seek
Lorem ipsum dolor si
nsectetur adipiscing
psum dolor sit amet,
jma@jma:~/work/tutos/sources$
轮询方法
如果需要实现被动等待(在感知字符设备时不浪费 CPU 周期),必须实现poll()函数,每当用户空间程序对与设备关联的文件执行select()或poll()系统调用时都会调用该函数:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
这个方法的核心是poll_wait()内核函数,定义在<linux/poll.h>中,这是驱动程序代码中应该包含的头文件:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address,
poll_table *p)
poll_wait()将与struct file结构(作为第一个参数给出)相关联的设备添加到可以唤醒进程的列表中(这些进程已经在struct wait_queue_head_t结构中休眠,该结构作为第二个参数给出),根据在struct poll_table结构中注册的事件(作为第三个参数给出)。用户进程可以运行poll(),select()或epoll()系统调用,将一组文件添加到等待的列表中,以便了解相关(如果有)设备的准备情况。然后内核将调用与每个设备文件相关联的驱动程序的poll入口。然后,每个驱动程序的poll方法应调用poll_wait()以注册进程需要被内核通知的事件,将该进程置于休眠状态,直到其中一个事件发生,并将驱动程序注册为可以唤醒该进程的驱动程序之一。通常的方法是根据select()(或poll())系统调用支持的事件类型使用一个等待队列(一个用于可读性,另一个用于可写性,如果需要的话,最终还有一个用于异常)。
(*poll)文件操作的返回值必须设置为POLLIN | POLLRDNORM,如果有数据可读(在调用 select 或 poll 时),如果设备可写,则设置为POLLOUT | POLLWRNORM(在这里也是调用 select 或 poll),如果没有新数据且设备尚未可写,则设置为0。在下面的示例中,我们假设设备同时支持阻塞读和写。当然,可以只实现其中一个。如果驱动程序没有定义此方法,则设备将被视为始终可读和可写,因此poll()或select()系统调用会立即返回。
轮询步骤
当实现poll函数时,read或write方法中的任何一个都可能会发生变化:
- 为需要实现被动等待的每种事件类型(读取、写入、异常)声明一个等待队列,当没有数据可读或设备尚不可写时,将任务放入其中:
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static DECLARE_WAIT_QUEUE_HEAD(my_rq);
- 实现
poll函数如下:
#include <linux/poll.h>
static unsigned int eep_poll(struct file *file, poll_table *wait)
{
unsigned int reval_mask = 0;
poll_wait(file, &my_wq, wait);
poll_wait(file, &my_rq, wait);
if (new-data-is-ready)
reval_mask |= (POLLIN | POLLRDNORM);
if (ready_to_be_written)
reval_mask |= (POLLOUT | POLLWRNORM);
return reval_mask;
}
- 当有新数据或设备可写时,通知等待队列:
wake_up_interruptible(&my_rq); /* Ready to read */
wake_up_interruptible(&my_wq); /* Ready to be written to */
可以从驱动程序的write()方法内部或者从 IRQ 处理程序内部通知可读事件,这意味着写入的数据可以被读取,或者从 IRQ 处理程序内部通知可写事件,这意味着设备已完成数据发送操作,并准备好再次接受数据。
在使用阻塞 I/O 时,read或write方法中的任何一个都可能会发生变化。在poll中使用的等待队列也必须在读取时使用。当用户需要读取时,如果有数据,该数据将立即发送到进程,并且必须更新等待队列条件(设置为false);如果没有数据,进程将在等待队列中休眠。
如果write方法应该提供数据,那么在write回调中,您必须填充数据缓冲区并更新等待队列条件(设置为true),并唤醒读取者(参见等待队列部分)。如果是 IRQ,这些操作必须在其处理程序中执行。
以下是对在给定字符设备上进行select()以检测数据可用性的代码的摘录:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#define NUMBER_OF_BYTE 100
#define CHAR_DEVICE "/dev/packt_char"
char data[NUMBER_OF_BYTE];
int main(int argc, char **argv)
{
int fd, retval;
ssize_t read_count;
fd_set readfds;
fd = open(CHAR_DEVICE, O_RDONLY);
if(fd < 0)
/* Print a message and exit*/
[...]
while(1){
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/*
* One needs to be notified of "read" events only, without timeout.
* This call will put the process to sleep until it is notified the
* event for which it registered itself
*/
ret = select(fd + 1, &readfds, NULL, NULL, NULL);
/* From this line, the process has been notified already */
if (ret == -1) {
fprintf(stderr, "select call on %s: an error ocurred", CHAR_DEVICE);
break;
}
/*
* file descriptor is now ready.
* This step assume we are interested in one file only.
*/
if (FD_ISSET(fd, &readfds)) {
read_count = read(fd, data, NUMBER_OF_BYTE);
if (read_count < 0 )
/* An error occured. Handle this */
[...]
if (read_count != NUMBER_OF_BYTE)
/* We have read less than need bytes */
[...] /* handle this */
else
/* Now we can process data we have read */
[...]
}
}
close(fd);
return EXIT_SUCCESS;
}
ioctl 方法
典型的 Linux 系统包含大约 350 个系统调用(syscalls),但只有少数与文件操作相关。有时设备可能需要实现特定的命令,这些命令不是由系统调用提供的,特别是与文件相关的命令,因此是设备文件。在这种情况下,解决方案是使用输入/输出控制(ioctl),这是一种方法,通过它可以扩展与设备相关的系统调用(实际上是命令)的列表。可以使用它向设备发送特殊命令(reset,shutdown,configure等)。如果驱动程序没有定义此方法,内核将对任何ioctl()系统调用返回-ENOTTY错误。
为了有效和安全,一个ioctl命令需要由一个数字标识,这个数字应该对系统是唯一的。在整个系统中 ioctl 号的唯一性将防止它向错误的设备发送正确的命令,或者向正确的命令传递错误的参数(给定重复的 ioctl 号)。Linux 提供了四个辅助宏来创建ioctl标识符,具体取决于是否有数据传输,以及传输的方向。它们的原型分别是:
_IO(MAGIC, SEQ_NO)
_IOW(MAGIC, SEQ_NO, TYPE)
_IOR(MAGIC, SEQ_NO, TYPE)
_IORW(MAGIC, SEQ_NO, TYPE)
它们的描述如下:
-
_IO:ioctl不需要数据传输 -
_IOW:ioctl需要写参数(copy_from_user或get_user) -
_IOR:ioctl需要读参数(copy_to_user或put_user) -
_IOWR:ioctl需要写和读参数
它们的参数意义(按照它们传递的顺序)在这里描述:
-
一个编码为 8 位(0 到 255)的数字,称为魔术数字。
-
一个序列号或命令 ID,也是 8 位。
-
一个数据类型(如果有的话),将通知内核要复制的大小。
在内核源中的Documentation/ioctl/ioctl-decoding.txt中有很好的文档,现有的ioctl在Documentation/ioctl/ioctl-number.txt中列出,这是需要创建ioctl命令时的好起点。
生成 ioctl 号(命令)
应该在专用的头文件中生成自己的 ioctl 号。这不是强制性的,但建议这样做,因为这个头文件也应该在用户空间中可用。换句话说,应该复制 ioctl 头文件,以便内核和用户空间各有一个,用户可以在用户应用程序中包含其中。现在让我们在一个真实的例子中生成 ioctl 号:
eep_ioctl.h:
#ifndef PACKT_IOCTL_H
#define PACKT_IOCTL_H
/*
* We need to choose a magic number for our driver, and sequential numbers
* for each command:
*/
#define EEP_MAGIC 'E'
#define ERASE_SEQ_NO 0x01
#define RENAME_SEQ_NO 0x02
#define ClEAR_BYTE_SEQ_NO 0x03
#define GET_SIZE 0x04
/*
* Partition name must be 32 byte max
*/
#define MAX_PART_NAME 32
/*
* Now let's define our ioctl numbers:
*/
#define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO)
#define EEP_RENAME_PART _IOW(EEP_MAGIC, RENAME_SEQ_NO, unsigned long)
#define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE, int *)
#endif
ioctl的步骤
首先,让我们看一下它的原型。它看起来如下:
long ioctl(struct file *f, unsigned int cmd, unsigned long arg);
只有一步:使用switch ... case语句,并在调用未定义的ioctl命令时返回-ENOTTY错误。可以在man7.org/linux/man-pages/man2/ioctl.2.html找到更多信息:
/*
* User space code also need to include the header file in which ioctls
* defined are defined. This is eep_ioctl.h in our case.
*/
#include "eep_ioctl.h"
static long eep_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
int part;
char *buf = NULL;
int size = 1300;
switch(cmd){
case EEP_ERASE:
erase_eepreom();
break;
case EEP_RENAME_PART:
buf = kmalloc(MAX_PART_NAME, GFP_KERNEL);
copy_from_user(buf, (char *)arg, MAX_PART_NAME);
rename_part(buf);
break;
case EEP_GET_SIZE:
copy_to_user((int*)arg, &size, sizeof(int));
break;
default:
return -ENOTTY;
}
return 0;
}
如果您认为您的ioctl命令需要多个参数,您应该将这些参数收集在一个结构中,并只是将结构中的指针传递给ioctl。
现在,从用户空间,您必须使用与驱动程序代码中相同的ioctl头文件:
my_main.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include "eep_ioctl.h" /* our ioctl header file */
int main()
{
int size = 0;
int fd;
char *new_name = "lorem_ipsum"; /* must not be longer than MAX_PART_NAME */
fd = open("/dev/eep-mem1", O_RDWR);
if (fd == -1){
printf("Error while opening the eeprom\n");
return -1;
}
ioctl(fd, EEP_ERASE); /* ioctl call to erase partition */
ioctl(fd, EEP_GET_SIZE, &size); /* ioctl call to get partition size */
ioctl(fd, EEP_RENAME_PART, new_name); /* ioctl call to rename partition */
close(fd);
return 0;
}
填充 file_operations 结构
在编写内核模块时,最好在静态初始化结构及其参数时使用指定的初始化器。它包括命名需要分配值的成员。形式是.member-name来指定应初始化的成员。这允许以未定义的顺序初始化成员,或者保持不想修改的字段不变,等等。
一旦我们定义了我们的函数,我们只需填充结构如下:
static const struct file_operations eep_fops = {
.owner = THIS_MODULE,
.read = eep_read,
.write = eep_write,
.open = eep_open,
.release = eep_release,
.llseek = eep_llseek,
.poll = eep_poll,
.unlocked_ioctl = eep_ioctl,
};
让我们记住,结构作为参数传递给cdev_init的init方法。
总结
在本章中,我们已经揭开了字符设备的神秘面纱,看到了如何通过设备文件让用户与我们的驱动程序进行交互。我们学会了如何将文件操作暴露给用户空间,并从内核内部控制它们的行为。我们甚至可以实现多设备支持。下一章有点偏向硬件,因为它涉及到将硬件设备的功能暴露给用户空间的平台驱动程序。字符驱动程序与平台驱动程序的结合力量简直令人惊叹。下一章见。
第五章:平台设备驱动程序
我们都知道即插即用设备。它们在插入时立即由内核处理。这些可能是 USB 或 PCI Express,或任何其他自动发现的设备。因此,还存在其他类型的设备,这些设备不是热插拔的,内核需要在管理之前知道它们。有 I2C、UART、SPI 和其他未连接到可枚举总线的设备。
您可能已经知道的真实物理总线:USB、I2S、I2C、UART、SPI、PCI、SATA 等。这些总线是名为控制器的硬件设备。由于它们是 SoC 的一部分,因此无法移除,不可发现,也称为平台设备。
人们经常说平台设备是芯片上的设备(嵌入在 SoC 中)。实际上,这在一定程度上是正确的,因为它们被硬连到芯片中,无法移除。但连接到 I2C 或 SPI 的设备不是芯片上的设备,它们也是平台设备,因为它们不可发现。同样,可能存在芯片上的 PCI 或 USB 设备,但它们不是平台设备,因为它们是可发现的。
从 SoC 的角度来看,这些设备(总线)通过专用总线内部连接,并且大多数时间是专有的,特定于制造商。从内核的角度来看,这些是根设备,与任何东西都没有连接。这就是伪平台总线的作用。伪平台总线,也称为平台总线,是内核虚拟总线,用于内核不知道的物理总线上的设备。在本章中,平台设备指的是依赖于伪平台总线的设备。
处理平台设备基本上需要两个步骤:
-
注册一个管理您的设备的平台驱动程序(使用唯一名称)
-
使用与驱动程序相同的名称注册您的平台设备,以及它们的资源,以便让内核知道您的设备在那里
话虽如此,在本章中,我们将讨论以下内容:
-
平台设备及其驱动程序
-
内核中的设备和驱动程序匹配机制
-
注册平台驱动程序与设备,以及平台数据
平台驱动程序
在继续之前,请注意以下警告。并非所有平台设备都由平台驱动程序处理(或者我应该说伪平台驱动程序)。平台驱动程序专用于不基于常规总线的设备。I2C 设备或 SPI 设备是平台设备,但分别依赖于 I2C 或 SPI 总线,而不是平台总线。一切都需要使用平台驱动程序手动完成。平台驱动程序必须实现一个probe函数,当模块被插入或设备声明它时,内核会调用该函数。在开发平台驱动程序时,必须填写的主要结构是struct platform_driver,并使用以下显示的专用函数将驱动程序注册到平台总线核心:
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "my_platform_driver",
.owner = THIS_MODULE,
},
};
让我们看看组成结构的每个元素的含义,以及它们的用途:
probe():这是在设备在匹配后声明您的驱动程序时调用的函数。稍后,我们将看到核心如何调用probe。其声明如下:
static int my_pdrv_probe(struct platform_device *pdev)
remove():当设备不再需要时,调用此函数来摆脱驱动程序,其声明如下:
static int my_pdrv_remove(struct platform_device *pdev)
struct device_driver:这描述了驱动程序本身,提供名称、所有者和一些字段,我们稍后会看到。
使用platform_driver_register()或platform_driver_probe()在init函数中(加载模块时)注册平台驱动程序与内核一样简单。这些函数之间的区别在于:
-
platform_driver_register()将驱动程序注册并放入内核维护的驱动程序列表中,以便在发生新的匹配时可以按需调用其probe()函数。为了防止您的驱动程序被插入和注册到该列表中,只需使用next函数。 -
使用
platform_driver_probe(),内核立即运行匹配循环,检查是否有与匹配名称相匹配的平台设备,然后调用驱动程序的probe(),如果发生匹配,表示设备存在。如果没有,驱动程序将被忽略。这种方法可以防止延迟探测,因为它不会在系统上注册驱动程序。在这里,probe函数放置在__init部分中,在内核引导完成后释放,从而防止延迟探测并减少驱动程序的内存占用。如果您 100%确定设备存在于系统中,请使用此方法:
ret = platform_driver_probe(&mypdrv, my_pdrv_probe);
以下是一个简单的平台驱动程序,它在内核中注册自己:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
static int my_pdrv_probe (struct platform_device *pdev){
pr_info("Hello! device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev){
pr_info("good bye reader!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
},
};
static int __init my_drv_init(void)
{
pr_info("Hello Guy\n");
/* Registering with Kernel */
platform_driver_register(&mypdrv);
return 0;
}
static void __exit my_pdrv_remove (void)
{
Pr_info("Good bye Guy\n");
/* Unregistering from Kernel */
platform_driver_unregister(&my_driver);
}
module_init(my_drv_init);
module_exit(my_pdrv_remove);
MODULE_LICENSE(
"GPL
");
MODULE_AUTHOR(
"John Madieu
");
MODULE_DESCRIPTION(
"My platform Hello World module
");
我们的模块在init/exit函数中除了在平台总线核心中注册/注销之外什么也不做。大多数驱动程序都是这样。在这种情况下,我们可以摆脱module_init和module_exit,并使用module_platform_driver宏。
module_platform_driver宏如下所示:
/*
* module_platform_driver() - Helper macro for drivers that don't
* do anything special in module init/exit. This eliminates a lot
* of boilerplate. Each module may only use this macro once, and
* calling it replaces module_init() and module_exit()
*/
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, platform_driver_register, \
platform_driver_unregister)
这个宏将负责在平台驱动核心中注册我们的模块。不再需要module_init和module_exit宏,也不再需要init和exit函数。这并不意味着这些函数不再被调用,只是我们可以忘记自己编写它们。
probe函数不能替代init函数。每当给定设备与驱动程序匹配时,都会调用probe函数,而init函数只在模块加载时运行一次。
[...]
static int my_driver_probe (struct platform_device *pdev){
[...]
}
static void my_driver_remove(struct platform_device *pdev){
[...]
}
static struct platform_drivermy_driver = {
[...]
};
module_platform_driver(my_driver);
每个总线都有特定的宏,用于注册驱动程序。以下列表不是详尽无遗的:
-
module_platform_driver(struct platform_driver)用于平台驱动程序,专用于不位于传统物理总线上的设备(我们刚刚在上面使用了它) -
module_spi_driver(struct spi_driver)用于 SPI 驱动程序 -
module_i2c_driver(struct i2c_driver)用于 I2C 驱动程序 -
module_pci_driver(struct pci_driver)用于 PCI 驱动程序 -
module_usb_driver(struct usb_driver)用于 USB 驱动程序 -
module_mdio_driver(struct mdio_driver)用于 mdio -
[...]
如果您不知道驱动程序需要位于哪个总线上,那么它是一个平台驱动程序,您应该使用platform_driver_register或platform_driver_probe来注册驱动程序。
平台设备
实际上,我们应该说伪平台设备,因为这一部分涉及的是位于伪平台总线上的设备。当您完成驱动程序后,您将不得不向内核提供需要该驱动程序的设备。平台设备在内核中表示为struct platform_device的实例,并且如下所示:
struct platform_device {
const char *name;
u32 id;
struct device dev;
u32 num_resources;
struct resource *resource;
};
在涉及平台驱动程序之前,驱动程序和设备匹配之前,struct platform_device和static struct platform_driver.driver.name的name字段必须相同。num_resources和struct resource *resource字段将在下一节中介绍。只需记住,由于resource是一个数组,因此num_resources必须包含该数组的大小。
资源和平台数据
与可热插拔设备相反,内核不知道系统上存在哪些设备,它们的功能是什么,或者为了正常工作需要什么。没有自动协商过程,因此内核提供的任何信息都是受欢迎的。有两种方法可以通知内核设备需要的资源(中断请求,直接内存访问,内存区域,I/O 端口,总线)和数据(任何自定义和私有数据结构,您可能希望传递给驱动程序),如下所述:
设备供应 - 旧的和不推荐的方式
这种方法适用于不支持设备树的内核版本。使用此方法,驱动程序保持通用,设备在与板相关的源文件中注册。
资源
资源代表了从硬件角度来看设备的所有特征元素,以及设备需要的元素,以便进行设置和正常工作。内核中只有六种资源类型,全部列在include/linux/ioport.h中,并用作标志来描述资源的类型:
#define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
#define IORESOURCE_MEM 0x00000200 /* Memory regions */
#define IORESOURCE_REG 0x00000300 /* Register offsets */
#define IORESOURCE_IRQ 0x00000400 /* IRQ line */
#define IORESOURCE_DMA 0x00000800 /* DMA channels */
#define IORESOURCE_BUS 0x00001000 /* Bus */
资源在内核中表示为struct resource的实例:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
};
让我们解释结构中每个元素的含义:
-
start/end:这表示资源的开始/结束位置。对于 I/O 或内存区域,它表示它们的开始/结束位置。对于 IRQ 线、总线或 DMA 通道,开始/结束必须具有相同的值。 -
flags:这是一个掩码,用于描述资源的类型,例如IORESOURCE_BUS。 -
name:这标识或描述资源。
一旦提供了资源,就需要在驱动程序中提取它们以便使用。probe函数是提取它们的好地方。在继续之前,让我们记住平台设备驱动程序的probe函数的声明:
int probe(struct platform_device *pdev);
pdev由内核自动填充,其中包含我们之前注册的数据和资源。让我们看看如何选择它们。
嵌入在struct platform_device中的struct resource可以使用platform_get_resource()函数检索。以下是platform_get_resource的原型:
struct resource *platform_get_resource(structplatform_device *dev,
unsigned int type, unsigned int num);
第一个参数是平台设备本身的实例。第二个参数告诉我们需要什么类型的资源。对于内存,它应该是IORESOURCE_MEM。再次,请查看include/linux/ioport.h以获取更多详细信息。num参数是一个索引,表示所需的资源类型。零表示第一个,依此类推。
如果资源是 IRQ,我们必须使用int platform_get_irq(struct platform_device * pdev, unsigned intnum),其中pdev是平台设备,num是资源中的 IRQ 索引(如果有多个)。我们可以使用以下整个probe函数来提取我们为设备注册的平台数据:
static int my_driver_probe(struct platform_device *pdev)
{
struct my_gpios *my_gpio_pdata =
(struct my_gpios*)dev_get_platdata(&pdev->dev);
int rgpio = my_gpio_pdata->reset_gpio;
int lgpio = my_gpio_pdata->led_gpio;
struct resource *res1, *res2;
void *reg1, *reg2;
int irqnum;
res1 = platform_get_resource(pdev, IORESSOURCE_MEM, 0);
if((!res1)){
pr_err(" First Resource not available");
return -1;
}
res2 = platform_get_resource(pdev, IORESSOURCE_MEM, 1);
if((!res2)){
pr_err(" Second Resource not available");
return -1;
}
/* extract the irq */
irqnum = platform_get_irq(pdev, 0);
Pr_info("\n IRQ number of Device: %d\n", irqnum);
/*
* At this step, we can use gpio_request, on gpio,
* request_irq on irqnum and ioremap() on reg1 and reg2\.
* ioremap() is discussed in chapter 11, Kernel Memory Management
*/
[...]
return 0;
}
平台数据
任何其他数据,其类型不属于前一节中列举的资源类型(例如 GPIO),都属于这里。无论它们的类型是什么,struct platform_device包含一个struct device字段,该字段又包含一个struct platform_data字段。通常,应该将这些数据嵌入到一个结构中,并将其传递给platform_device.device.platform_data字段。例如,假设您声明了一个平台设备,该设备需要两个 GPIO 号作为平台数据,一个中断号和两个内存区域作为资源。以下示例显示了如何注册平台数据以及设备。在这里,我们使用platform_device_register(struct platform_device *pdev)函数,该函数用于向平台核心注册平台设备:
/*
* Other data than irq or memory must be embedded in a structure
* and passed to "platform_device.device.platform_data"
*/
struct my_gpios {
int reset_gpio;
int led_gpio;
};
/*our platform data*/
static struct my_gpiosneeded_gpios = {
.reset_gpio = 47,
.led_gpio = 41,
};
/* Our resource array */
static struct resource needed_resources[] = {
[0] = { /* The first memory region */
.start = JZ4740_UDC_BASE_ADDR,
.end = JZ4740_UDC_BASE_ADDR + 0x10000 - 1,
.flags = IORESOURCE_MEM,
.name = "mem1",
},
[1] = {
.start = JZ4740_UDC_BASE_ADDR2,
.end = JZ4740_UDC_BASE_ADDR2 + 0x10000 -1,
.flags = IORESOURCE_MEM,
.name = "mem2",
},
[2] = {
.start = JZ4740_IRQ_UDC,
.end = JZ4740_IRQ_UDC,
.flags = IORESOURCE_IRQ,
.name = "mc",
},
};
static struct platform_devicemy_device = {
.name = "my-platform-device",
.id = 0,
.dev = {
.platform_data = &needed_gpios,
},
.resource = needed_resources,
.num_resources = ARRY_SIZE(needed_resources),
};
platform_device_register(&my_device);
在前面的示例中,我们使用了IORESOURCE_IRQ和IORESOURCE_MEM来告知内核我们提供了什么类型的资源。要查看所有其他标志类型,请查看内核树中的include/linux/ioport.h。
为了检索我们之前注册的平台数据,我们可以直接使用pdev->dev.platform_data(记住struct platform_device结构),但建议使用内核提供的函数(尽管它做的是同样的事情):
void *dev_get_platdata(const struct device *dev)
struct my_gpios *picked_gpios = dev_get_platdata(&pdev->dev);
在哪里声明平台设备?
设备与其资源和数据一起注册。在这种旧的和不推荐的方法中,它们被声明为一个单独的模块,或者在arch/<arch>/mach-xxx/yyyy.c中的板init文件中声明,这在我们的情况下是arch/arm/mach-imx/mach-imx6q.c,因为我们使用的是基于 NXP i.MX6Q 的 UDOO quad。函数platform_device_register()让您可以这样做:
static struct platform_device my_device = {
.name = "my_drv_name",
.id = 0,
.dev.platform_data = &my_device_pdata,
.resource = jz4740_udc_resources,
.num_resources = ARRY_SIZE(jz4740_udc_resources),
};
platform_device_register(&my_device);
设备的名称非常重要,内核使用它来将驱动程序与相同名称的设备进行匹配。
设备配置-新的推荐方式
在第一种方法中,任何修改都将需要重新构建整个内核。如果内核必须包含任何应用程序/板特定的配置,其大小将会大幅增加。为了保持简单,并将设备声明(因为它们实际上并不是内核的一部分)与内核源代码分开,引入了一个新概念:设备树。DTS 的主要目标是从内核中删除非常特定且从未经过测试的代码。使用设备树,平台数据和资源是同质的。设备树是硬件描述文件,其格式类似于树结构,其中每个设备都表示为一个节点,并且任何数据、资源或配置数据都表示为节点的属性。这样,您只需要在进行一些修改时重新编译设备树。设备树将成为下一章的主题,我们将看到如何将其引入到平台设备中。
设备、驱动程序和总线匹配
在任何匹配发生之前,Linux 都会调用platform_match(struct device *dev, struct device_driver *drv)。平台设备通过字符串与其驱动程序匹配。根据 Linux 设备模型,总线元素是最重要的部分。每个总线都维护着与其注册的驱动程序和设备的列表。总线驱动程序负责设备和驱动程序的匹配。每当连接新设备或向总线添加新驱动程序时,该总线都会启动匹配循环。
现在,假设您使用 I2C 核心提供的函数(在下一章中讨论)注册了一个新的 I2C 设备。内核将通过调用与 I2C 总线驱动程序注册的 I2C 核心匹配函数来触发 I2C 总线匹配循环,以检查是否已经有与您的设备匹配的注册驱动程序。如果没有匹配项,将不会发生任何事情。如果发生匹配,内核将通过一种称为 netlink 套接字的通信机制通知设备管理器(udev/mdev),后者将加载(如果尚未加载)与您的设备匹配的驱动程序。一旦驱动程序加载,其probe()函数将立即执行。不仅 I2C 工作方式如此,而且每个总线都有自己的匹配机制,大致相同。在每个设备或驱动程序注册时都会触发总线匹配循环。
我们可以总结前面部分所说的内容如下图所示:

每个注册的驱动程序和设备都位于总线上。这构成了一棵树。USB 总线可能是 PCI 总线的子级,而 MDIO 总线通常是其他设备的子级,依此类推。因此,我们前面的图将如下所示:

当您使用platform_driver_probe()函数注册驱动程序时,内核会遍历已注册的平台设备表,并寻找匹配项。如果有匹配项,它将使用平台数据调用匹配驱动程序的probe函数。
平台设备和平台驱动程序如何匹配?
到目前为止,我们只讨论了如何填充设备和驱动程序的不同结构。但现在我们将看到它们如何在内核中注册,以及 Linux 如何知道哪些设备由哪个驱动程序处理。答案是MODULE_DEVICE_TABLE。这个宏让驱动程序暴露其 ID 表,描述了它可以支持哪些设备。同时,如果驱动程序可以编译为模块,driver.name字段应该与模块名称匹配。如果不匹配,模块将不会自动加载,除非我们使用MODULE_ALIAS宏为模块添加另一个名称。在编译时,该信息从所有驱动程序中提取出来,以构建设备表。当内核需要为设备找到驱动程序(需要执行匹配时),内核会遍历设备表。如果找到与添加的设备的compatible(对于设备树)、device/vendor id或name(对于设备 ID 表或名称)匹配的条目,那么提供该匹配的模块将被加载(运行模块的init函数),并调用probe函数。MODULE_DEVICE_TABLE宏在linux/module.h中定义:
#define MODULE_DEVICE_TABLE(type, name)
以下是给这个宏的每个参数的描述:
-
type:这可以是i2c、spi、acpi、of、platform、usb、pci或您可能在include/linux/mod_devicetable.h中找到的任何其他总线。它取决于我们的设备所在的总线,或者我们想要使用的匹配机制。 -
name:这是一个指向XXX_device_id数组的指针,用于设备匹配。如果我们谈论的是 I2C 设备,结构将是i2c_device_id。对于 SPI 设备,应该是spi_device_id,依此类推。对于设备树Open Firmware(OF)匹配机制,我们必须使用of_device_id。
对于新的非可发现平台设备驱动程序,建议不再使用平台数据,而是改用设备树功能,使用 OF 匹配机制。请注意,这两种方法并不是互斥的,因此可以混合使用。
让我们深入了解匹配机制的细节,除了我们将在第六章中讨论的 OF 风格匹配之外,设备树的概念。
内核设备和驱动程序匹配函数
内核中负责平台设备和驱动程序匹配功能的函数在/drivers/base/platform.c中定义如下:
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
我们可以列举四种匹配机制。它们都基于字符串比较。如果我们看一下platform_match_id,我们就会了解底层的工作原理:
static const struct platform_device_id *platform_match_id(
const struct platform_device_id *id,
struct platform_device *pdev)
{
while (id->name[0]) {
if (strcmp(pdev->name, id->name) == 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}
现在让我们来看一下我们在第四章中讨论的struct device_driver结构:
struct device_driver {
const char *name;
[...]
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
};
我故意删除了我们不感兴趣的字段。struct device_driver构成了每个设备驱动程序的基础。无论是 I2C、SPI、TTY 还是其他设备驱动程序,它们都嵌入了一个struct device_driver元素。
OF 风格和 ACPI 匹配
OF 风格在第六章中有解释,设备树的概念。第二种机制是基于 ACPI 表的匹配。我们在本书中不会讨论它,但供您参考,它使用acpi_device_id结构。
ID 表匹配
这种匹配风格已经存在很长时间,它基于struct device_id结构。所有设备 id 结构都在include/linux/mod_devicetable.h中定义。要找到正确的结构名称,您需要使用总线名称作为前缀,即您的设备驱动程序所在的总线名称。例如:struct i2c_device_id用于 I2C,struct platform_device_id用于平台设备(不在真实物理总线上),spi_device_id用于 SPI 设备,usb_device_id用于 USB 等。平台设备的device_id 表的典型结构如下:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
无论如何,如果注册了 ID 表,每当内核运行匹配函数以查找未知或新的平台设备的驱动程序时,都会遍历它。如果匹配成功,将调用匹配驱动程序的 probe 函数,并将匹配的 ID 表条目的指针作为参数传递给 struct platform_device ,该指针将指向发起匹配的匹配 ID 表条目。.driver_data 元素是一个 unsigned long ,有时会被强制转换为指针地址,以便指向任何东西,就像在 serial-imx 驱动程序中一样。以下是 drivers/tty/serial/imx.c 中使用 platform_device_id 的示例:
static const struct platform_device_id imx_uart_devtype[] = {
{
.name = "imx1-uart",
.driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX1_UART],
}, {
.name = "imx21-uart",
.driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX21_UART],
}, {
.name = "imx6q-uart",
.driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART],
}, {
/* sentinel */
}
};
.name 字段必须与在特定于板的文件中注册设备时给出的设备名称相同。负责此匹配样式的函数是 platform_match_id 。如果查看 drivers/base/platform.c 中的定义,你会看到:
static const struct platform_device_id *platform_match_id(
const struct platform_device_id *id,
struct platform_device *pdev)
{
while (id->name[0]) {
if (strcmp(pdev->name, id->name) == 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}
在下面的示例中,这是内核源代码中 drivers/tty/serial/imx.c 的摘录,可以看到平台数据是如何通过强制转换转换回原始数据结构的。这就是人们有时将任何数据结构作为平台数据传递的方式:
static void serial_imx_probe_pdata(struct imx_port *sport,
struct platform_device *pdev)
{
struct imxuart_platform_data *pdata = dev_get_platdata(&pdev->dev);
sport->port.line = pdev->id;
sport->devdata = (structimx_uart_data *) pdev->id_entry->driver_data;
if (!pdata)
return;
[...]
}
pdev->id_entry 是一个 struct platform_device_id ,它是一个指向内核提供的匹配 ID 表条目的指针,其 driver_data 元素被强制转换回数据结构的指针。
ID 表匹配的每个特定设备数据
在前一节中,我们已经将 platform_device_id.platform_data 用作指针。你的驱动程序可能需要支持多种设备类型。在这种情况下,你将需要为你支持的每种设备类型使用特定的设备数据。然后,你应该将设备 ID 用作包含每种可能的设备数据的数组的索引,而不再是指针地址。以下是示例中的详细步骤:
- 我们根据驱动程序需要支持的设备类型定义一个枚举:
enum abx80x_chip {
AB0801,
AB0803,
AB0804,
AB0805,
AB1801,
AB1803,
AB1804,
AB1805,
ABX80X
};
- 我们定义特定的数据类型结构:
struct abx80x_cap {
u16 pn;
boolhas_tc;
};
- 我们使用默认值填充数组,并根据
device_id中的索引,我们可以选择正确的数据:
static struct abx80x_cap abx80x_caps[] = {
[AB0801] = {.pn = 0x0801},
[AB0803] = {.pn = 0x0803},
[AB0804] = {.pn = 0x0804, .has_tc = true},
[AB0805] = {.pn = 0x0805, .has_tc = true},
[AB1801] = {.pn = 0x1801},
[AB1803] = {.pn = 0x1803},
[AB1804] = {.pn = 0x1804, .has_tc = true},
[AB1805] = {.pn = 0x1805, .has_tc = true},
[ABX80X] = {.pn = 0}
};
- 我们使用特定索引定义我们的
platform_device_id:
static const struct i2c_device_id abx80x_id[] = {
{ "abx80x", ABX80X },
{ "ab0801", AB0801 },
{ "ab0803", AB0803 },
{ "ab0804", AB0804 },
{ "ab0805", AB0805 },
{ "ab1801", AB1801 },
{ "ab1803", AB1803 },
{ "ab1804", AB1804 },
{ "ab1805", AB1805 },
{ "rv1805", AB1805 },
{ }
};
- 在
probe函数中我们只需要做一些事情:
static int rs5c372_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
[...]
/* We pick the index corresponding to our device */
int index = id->driver_data;
/*
* And then, we can access the per device data
* since it is stored in abx80x_caps[index]
*/
}
名称匹配 - 平台设备名称匹配
现在,大多数平台驱动程序根本不提供任何表;它们只是在驱动程序的名称字段中填写驱动程序本身的名称。但匹配仍然有效,因为如果查看 platform_match 函数,你会发现最终匹配会回退到名称匹配,比较驱动程序的名称和设备的名称。一些旧的驱动程序仍然使用该匹配机制。以下是 sound/soc/fsl/imx-ssi.c 中的名称匹配:
static struct platform_driver imx_ssi_driver = {
.probe = imx_ssi_probe,
.remove = imx_ssi_remove,
/* As you can see here, only the 'name' field is filled */
.driver = {
.name = "imx-ssi",
},
};
module_platform_driver(imx_ssi_driver);
要添加与此驱动程序匹配的设备,必须在特定于板的文件中(通常在 arch/<your_arch>/mach-*/board-*.c 中)调用 platform_device_register 或 platform_add_devices ,并使用相同的名称 imx-ssi 。对于我们的四核 i.MX6-based UDOO,它是 arch/arm/mach-imx/mach-imx6q.c 。
总结
内核伪平台总线对你来说已经没有秘密了。通过总线匹配机制,你能够理解你的驱动程序何时、如何以及为什么被加载,以及它是为哪个设备加载的。我们可以根据我们想要的匹配机制实现任何 probe 函数。由于驱动程序的主要目的是处理设备,我们现在能够在系统中填充设备(旧的和不推荐的方式)。最后,下一章将专门讨论设备树,这是用于在系统上填充设备及其配置的新机制。
第六章:设备树的概念
设备树(DT)是一个易于阅读的硬件描述文件,具有类似 JSON 的格式样式,是一个简单的树结构,其中设备由具有其属性的节点表示。属性可以是空的(只是键,用于描述布尔值),也可以是键值对,其中值可以包含任意字节流。本章是对 DT 的简单介绍。每个内核子系统或框架都有自己的 DT 绑定。当我们处理相关主题时,我们将讨论这些特定的绑定。DT 起源于 OF,这是一个由计算机公司认可的标准,其主要目的是为计算机固件系统定义接口。也就是说,可以在www.devicetree.org/找到更多关于 DT 规范的信息。因此,本章将涵盖 DT 的基础知识,例如:
-
命名约定,以及别名和标签
-
描述数据类型及其 API
-
管理寻址方案和访问设备资源
-
实现 OF 匹配样式并提供特定于应用程序的数据
设备树机制
通过将选项CONFIG_OF设置为Y,可以在内核中启用 DT。为了从驱动程序中调用 DT API,必须添加以下标头:
#include <linux/of.h>
#include <linux/of_device.h>
DT 支持几种数据类型。让我们通过一个示例节点描述来看看它们:
/* This is a comment */
// This is another comment
node_label: nodename@reg{
string-property = "a string";
string-list = "red fish", "blue fish";
one-int-property = <197>; /* One cell in this property */
int-list-property = <0xbeef 123 0xabcd4>; /*each number (cell) is a
*32 bit integer(uint32).
*There are 3 cells in
*/this property
mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45]
byte-array-property = [0x01 0x23 0x45 0x67];
boolean-property;
};
以下是设备树中使用的一些数据类型的定义:
-
文本字符串用双引号表示。可以使用逗号创建字符串列表。
-
单元是由尖括号分隔的 32 位无符号整数。
-
布尔数据只是一个空属性。真或假的值取决于属性是否存在。
命名约定
每个节点必须具有形式为<name>[@<address>]的名称,其中<name>是一个长度最多为 31 个字符的字符串,[@<address>]是可选的,取决于节点是否表示可寻址设备。<address>应该是用于访问设备的主要地址。设备命名的示例如下:
expander@20 {
compatible = "microchip,mcp23017";
reg = <20>;
[...]
};
或
i2c@021a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
[...]
};
另一方面,“标签”是可选的。只有当节点打算从另一个节点的属性引用时,标记节点才有用。可以将标签视为指向节点的指针,如下一节所述。
别名,标签和 phandle
了解这三个元素如何工作非常重要。它们在 DT 中经常被使用。让我们看看以下的 DT 来解释它们是如何工作的:
aliases {
ethernet0 = &fec;
gpio0 = &gpio1;
gpio1 = &gpio2;
mmc0 = &usdhc1;
[...]
};
gpio1: gpio@0209c000 {
compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio";
[...]
};
node_label: nodename@reg {
[...];
gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>;
};
标签只是一种标记节点的方式,以便让节点通过唯一名称进行标识。在现实世界中,DT 编译器将该名称转换为唯一的 32 位值。在前面的示例中,gpio1和node_label都是标签。然后可以使用标签来引用节点,因为标签对节点是唯一的。
指针句柄(phandle)是与节点关联的 32 位值,用于唯一标识该节点,以便可以从另一个节点的属性中引用该节点。标签用于指向节点。通过使用<&mylabel>,您指向其标签为mylabel的节点。
使用&与 C 编程语言中的用法相同;用于获取元素的地址。
在前面的示例中,&gpio1被转换为 phandle,以便它引用gpio1节点。对于以下示例也是如此:
thename@address {
property = <&mylabel>;
};
mylabel: thename@adresss {
[...]
}
为了不必遍历整个树来查找节点,引入了别名的概念。在 DT 中,aliases节点可以看作是一个快速查找表,另一个节点的索引。可以使用函数find_node_by_alias()来查找给定别名的节点。别名不直接在 DT 源中使用,而是由 Linux 内核进行解引用。
DT 编译器
DT 有两种形式:文本形式,表示源也称为DTS,以及二进制 blob 形式,表示已编译的 DT,也称为DTB。源文件的扩展名为.dts。实际上,还有.dtsi文本文件,表示 SoC 级别定义,而.dts文件表示板级别定义。可以将.dtsi视为头文件,应包含在.dts中,这些是源文件,而不是反向的,有点像在源文件(.c)中包含头文件(.h)。另一方面,二进制文件使用.dtb扩展名。
实际上还有第三种形式,即在/proc/device-tree中的 DT 的运行时表示。
正如其名称所示,用于编译设备树的工具称为设备树编译器(dtc)。从根内核源中,可以编译特定体系结构的独立特定 DT 或所有 DT。
让我们为 arm SoC 编译所有 DT(.dts)文件:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
对于独立的 DT:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6dl-sabrelite.dtb
在前面的示例中,源文件的名称是imx6dl-sabrelite.dts。
给定一个已编译的设备树(.dtb)文件,您可以执行反向操作并提取源(.dts)文件:
dtc -I dtb -O dtsarch/arm/boot/dts imx6dl-sabrelite.dtb >path/to/my_devicetree.dts
出于调试目的,将 DT 暴露给用户空间可能很有用。CONFIG_PROC_DEVICETREE配置变量将为您执行此操作。然后,您可以在/proc/device-tree中探索和浏览 DT。
表示和寻址设备
每个设备在 DT 中至少有一个节点。某些属性对许多设备类型都是共同的,特别是对于内核已知的总线上的设备(SPI、I2C、平台、MDIO 等)。这些属性是reg、#address-cells和#size-cells。这些属性的目的是在它们所在的总线上寻址设备。也就是说,主要的寻址属性是reg,这是一个通用属性,其含义取决于设备所在的总线。前缀size-cell和address-cell的#(sharp)可以翻译为length。
每个可寻址设备都有一个reg属性,其形式为reg = <address0size0 [address1size1] [address2size2] ...>的元组列表,其中每个元组表示设备使用的地址范围。#size-cells指示用于表示大小的 32 位单元的数量,如果大小不相关,则可能为 0。另一方面,#address-cells指示用于表示地址的 32 位单元的数量。换句话说,每个元组的地址元素根据#address-cell进行解释;大小元素也是如此,根据#size-cell进行解释。
实际上,可寻址设备继承自其父节点的#size-cell和#address-cell,即代表总线控制器的节点。给定设备中#size-cell和#address-cell的存在不会影响设备本身,而是其子级。换句话说,在解释给定节点的reg属性之前,必须了解父节点的#address-cells和#size-cells值。父节点可以自由定义适合设备子节点(子级)的任何寻址方案。
SPI 和 I2C 寻址
SPI 和 I2C 设备都属于非内存映射设备,因为它们的地址对 CPU 不可访问。相反,父设备的驱动程序(即总线控制器驱动程序)将代表 CPU 执行间接访问。每个 I2C/SPI 设备始终表示为所在的 I2C/SPI 总线节点的子节点。对于非内存映射设备,“#size-cells”属性为 0,寻址元组中的大小元素为空。这意味着这种类型的设备的reg属性始终在单元上:
&i2c3 {
[...]
status = "okay";
temperature-sensor@49 {
compatible = "national,lm73";
reg = <0x49>;
};
pcf8523: rtc@68 {
compatible = "nxp,pcf8523";
reg = <0x68>;
};
};
&ecspi1 {
fsl,spi-num-chipselects = <3>;
cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>;
status = "okay";
[...]
ad7606r8_0: ad7606r8@1 {
compatible = "ad7606-8";
reg = <1>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
convst-gpio = <&gpio6 18 0>;
};
};
如果有人查看arch/arm/boot/dts/imx6qdl.dtsi中的 SoC 级文件,就会注意到#size-cells和#address-cells分别设置为前者为0,后者为1,在i2c和spi节点中,它们分别是 I2C 和 SPI 设备在前面部分列举的父节点。这有助于我们理解它们的reg属性,地址值只有一个单元,大小值没有。
I2C 设备的reg属性用于指定总线上设备的地址。对于 SPI 设备,reg表示分配给设备的芯片选择线的索引,该索引位于控制器节点具有的芯片选择列表中。例如,对于 ad7606r8 ADC,芯片选择索引是1,对应于cs-gpios中的<&gpio5 17 0>,这是控制器节点的芯片选择列表。
你可能会问为什么我使用了 I2C/SPI 节点的 phandle:答案是因为 I2C/SPI 设备应该在板级文件(.dts)中声明,而 I2C/SPI 总线控制器应该在 SoC 级文件(.dtsi)中声明。
平台设备寻址
本节涉及的是内存对 CPU 可访问的简单内存映射设备。在这里,reg属性仍然定义了设备的地址,这是可以访问设备的内存区域列表。每个区域用单元组表示,其中第一个单元是内存区域的基地址,第二个单元是区域的大小。它的形式是reg = <base0 length0 [base1 length1] [address2 length2] ...>。每个元组表示设备使用的地址范围。
在现实世界中,不应该在不知道另外两个属性#size-cells和#address-cells的值的情况下解释reg属性。#size-cells告诉我们每个子reg元组中长度字段有多大。#address-cell也是一样,它告诉我们必须使用多少个单元来指定一个地址。
这种设备应该在一个具有特殊值compatible = "simple-bus"的节点中声明,表示一个没有特定处理或驱动程序的简单内存映射总线:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
aips-bus@02000000 { /* AIPS1 */
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
[...];
spba-bus@02000000 {
compatible = "fsl,spba-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x40000>;
[...]
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
[...]
};
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
[...]
};
};
};
在前面的例子中,具有compatible属性中simple-bus的子节点将被注册为平台设备。人们还可以看到 I2C 和 SPI 总线控制器如何通过设置#size-cells = <0>;来改变其子节点的寻址方案,因为这对它们来说并不重要。查找任何绑定信息的一个著名地方是内核设备树的文档:Documentation/devicetree/bindings/。
处理资源
驱动程序的主要目的是处理和管理设备,并且大部分时间将其功能暴露给用户空间。这里的目标是收集设备的配置参数,特别是资源(内存区域、中断线、DMA 通道、时钟等)。
以下是我们在本节中将使用的设备节点。它是在arch/arm/boot/dts/imx6qdl.dtsi中定义的 i.MX6 UART 设备节点:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_UART_IPG>,
<&clks IMX6QDL_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
命名资源的概念
当驱动程序期望某种类型的资源列表时,没有保证列表按照驱动程序期望的方式排序,因为编写板级设备树的人通常不是编写驱动程序的人。例如,驱动程序可能期望其设备节点具有 2 个 IRQ 线,一个用于索引 0 的 Tx 事件,另一个用于索引 1 的 Rx。如果顺序没有得到尊重会发生什么?驱动程序将产生不需要的行为。为了避免这种不匹配,引入了命名资源(clock,irq,dma,reg)的概念。它包括定义我们的资源列表,并对其进行命名,以便无论它们的索引如何,给定的名称始终与资源匹配。
用于命名资源的相应属性如下:
-
reg-names:这是在reg属性中的内存区域列表 -
clock-names:这是在clocks属性中命名时钟 -
interrupt-names:这为interrupts属性中的每个中断提供了一个名称 -
dma-names:这是dma属性
现在让我们创建一个虚假的设备节点条目来解释一下:
fake_device {
compatible = "packt,fake-device";
reg = <0x4a064000 0x800>, <0x4a064800 0x200>, <0x4a064c00 0x200>;
reg-names = "config", "ohci", "ehci";
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ohci", "ehci";
clocks = <&clks IMX6QDL_CLK_UART_IPG>, <&clks IMX6QDL_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
};
驱动程序中提取每个命名资源的代码如下:
struct resource *res1, *res2;
res1 = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ohci");
res2 = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");
struct dma_chan *dma_chan_rx, *dma_chan_tx;
dma_chan_rx = dma_request_slave_channel(&pdev->dev, "rx");
dma_chan_tx = dma_request_slave_channel(&pdev->dev, "tx");
inttxirq, rxirq;
txirq = platform_get_irq_byname(pdev, "ohci");
rxirq = platform_get_irq_byname(pdev, "ehci");
structclk *clck_per, *clk_ipg;
clk_ipg = devm_clk_get(&pdev->dev, "ipg");
clk_ipg = devm_clk_get(&pdev->dev, "pre");
这样,您可以确保将正确的名称映射到正确的资源,而无需再使用索引。
访问寄存器
在这里,驱动程序将接管内存区域并将其映射到虚拟地址空间中。我们将在第十一章中更多地讨论这个问题,内核内存管理。
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
/*
* Here one can request and map the memory region
* using request_mem_region(res->start, resource_size(res), pdev->name)
* and ioremap(iores->start, resource_size(iores)
*
* These function are discussed in chapter 11, Kernel Memory Management.
*/
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
platform_get_resource()将根据 DT 节点中第一个(索引 0)reg分配中存在的内存区域设置struct res的开始和结束字段。请记住,platform_get_resource()的最后一个参数表示资源索引。在前面的示例中,0索引了该资源类型的第一个值,以防设备在 DT 节点中分配了多个内存区域。在我们的示例中,它是reg = <0x02020000 0x4000>,意味着分配的区域从物理地址0x02020000开始,大小为0x4000字节。然后,platform_get_resource()将设置res.start = 0x02020000和res.end = 0x02023fff。
处理中断
中断接口实际上分为两部分;消费者端和控制器端。在 DT 中用四个属性来描述中断连接:
控制器是向消费者公开 IRQ 线的设备。在控制器端,有以下属性:
-
interrupt-controller:一个空(布尔)属性,应该定义为标记设备为中断控制器 -
#interrupt-cells:这是中断控制器的属性。它说明用于为该中断控制器指定中断的单元格数
消费者是生成 IRQ 的设备。消费者绑定期望以下属性:
-
interrupt-parent:对于生成中断的设备节点,它是一个包含指向设备附加的中断控制器节点的指针phandle的属性。如果省略,设备将从其父节点继承该属性。 -
interrupts:这是中断指定器。
中断绑定和中断指定器与中断控制器设备绑定。用于定义中断输入的单元格数取决于中断控制器,这是唯一决定的,通过其#interrupt-cells属性。在 i.MX6 的情况下,中断控制器是全局中断控制器(GIC)。其绑定在Documentation/devicetree/bindings/arm/gic.txt中有很好的解释。
中断处理程序
这包括从 DT 中获取 IRQ 号,并将其映射到 Linux IRQ,从而为其注册一个函数回调。执行此操作的驱动程序代码非常简单:
int irq = platform_get_irq(pdev, 0);
ret = request_irq(irq, imx_rxint, 0, dev_name(&pdev->dev), sport);
platform_get_irq()调用将返回irq号;这个数字可以被devm_request_irq()使用(irq然后在/proc/interrupts中可见)。第二个参数0表示我们需要设备节点中指定的第一个中断。如果有多个中断,我们可以根据需要更改此索引,或者只使用命名资源。
在我们之前的例子中,设备节点包含一个中断指定器,看起来像这样:
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>;
-
根据 ARM GIC,第一个单元格告诉我们中断类型:
-
0:共享外围中断(SPI),用于在核心之间共享的中断信号,可以由 GIC 路由到任何核心 -
1:私有外围中断(PPI),用于单个核心的私有中断信号
文档可以在以下网址找到:infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0407e/CCHDBEBE.html。
-
第二个单元格保存中断号。这个数字取决于中断线是 PPI 还是 SPI。
-
第三个单元格,在我们的情况下是
IRQ_TYPE_LEVEL_HIGH,表示感应电平。所有可用的感应电平都在include/linux/irq.h中定义。
中断控制器代码
interrupt-controller属性用于声明设备为中断控制器。#interrupt-cells属性定义了必须使用多少个单元格来定义单个中断线。我们将在第十六章中详细讨论这个问题,高级中断管理。
提取应用程序特定数据
应用程序特定数据是超出常见属性(既不是资源也不是 GPIO、调节器等)的数据。这些是可以分配给设备的任意属性和子节点。这些属性名称通常以制造商代码为前缀。这些可以是任何字符串、布尔值或整数值,以及它们在 Linux 源代码中drivers/of/base.c中定义的 API。我们讨论的以下示例并不详尽。现在让我们重用本章前面定义的节点:
node_label: nodename@reg{
string-property = ""a string"";
string-list = ""red fish"", ""blue fish"";
one-int-property = <197>; /* One cell in this property */
int-list-property = <0xbeef 123 0xabcd4>;/* each number (cell) is 32 a * bit integer(uint32). There
* are 3 cells in this property
*/
mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45]
byte-array-property = [0x01 0x23 0x45 0x67];
one-cell-property = <197>;
boolean-property;
};
文本字符串
以下是一个string属性:
string-property = "a string";
回到驱动程序中,应该使用of_property_read_string()来读取字符串值。其原型定义如下:
int of_property_read_string(const struct device_node *np, const
char *propname, const char **out_string)
以下代码显示了如何使用它:
const char *my_string = NULL;
of_property_read_string(pdev->dev.of_node, "string-property", &my_string);
单元格和无符号 32 位整数
以下是我们的int属性:
one-int-property = <197>;
int-list-property = <1350000 0x54dae47 1250000 1200000>;
应该使用of_property_read_u32()来读取单元格值。其原型定义如下:
int of_property_read_u32_index(const struct device_node *np,
const char *propname, u32 index, u32 *out_value)
回到驱动程序中,
unsigned int number;
of_property_read_u32(pdev->dev.of_node, "one-cell-property", &number);
可以使用of_property_read_u32_array来读取单元格列表。其原型如下:
int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values, size_tsz);
在这里,sz是要读取的数组元素的数量。查看drivers/of/base.c以查看如何解释其返回值:
unsigned int cells_array[4];
if (of_property_read_u32_array(pdev->dev.of_node, "int-list-property",
cells_array, 4)) {
dev_err(&pdev->dev, "list of cells not specified\n");
return -EINVAL;
}
布尔值
应该使用of_property_read_bool()来读取函数的第二个参数中给定的布尔属性的名称:
bool my_bool = of_property_read_bool(pdev->dev.of_node, "boolean-property");
If(my_bool){
/* boolean is true */
} else
/* Bolean is false */
}
提取和解析子节点
您可以在设备节点中添加任何子节点。给定表示闪存设备的节点,分区可以表示为子节点。对于处理一组输入和输出 GPIO 的设备,每个集合可以表示为一个子节点。示例节点如下:
eeprom: ee24lc512@55 {
compatible = "microchip,24xx512";
reg = <0x55>;
partition1 {
read-only;
part-name = "private";
offset = <0>;
size = <1024>;
};
partition2 {
part-name = "data";
offset = <1024>;
size = <64512>;
};
};
可以使用for_each_child_of_node()来遍历给定节点的子节点:
struct device_node *np = pdev->dev.of_node;
struct device_node *sub_np;
for_each_child_of_node(np, sub_np) {
/* sub_np will point successively to each sub-node */
[...]
int size;
of_property_read_u32(client->dev.of_node,
"size", &size);
...
}
平台驱动程序和 DT
平台驱动程序也可以使用 DT。也就是说,这是处理平台设备的推荐方式,而且不再需要触及板文件,甚至在设备属性更改时重新编译内核。如果您还记得,在上一章中我们讨论了 OF 匹配样式,这是一种基于 DT 的匹配机制。让我们在下一节中看看它是如何工作的:
OF 匹配样式
OF 匹配样式是平台核心执行的第一个匹配机制,用于将设备与其驱动程序匹配。它使用设备树的compatible属性来匹配of_match_table中的设备条目,这是struct driver子结构的一个字段。每个设备节点都有一个compatible属性,它是一个字符串或字符串列表。任何声明在compatible属性中列出的字符串之一的平台驱动程序都将触发匹配,并将看到其probe函数执行。
DT 匹配条目在内核中被描述为struct of_device_id结构的一个实例,该结构在linux/mod_devicetable.h中定义,如下所示:
// we are only interested in the two last elements of the structure
struct of_device_id {
[...]
char compatible[128];
const void *data;
};
以下是结构的每个元素的含义:
-
char compatible[128]:这是用于匹配设备节点的 DT 兼容属性的字符串。在匹配发生之前,它们必须相同。 -
const void *data:这可以指向任何结构,可以根据设备类型配置数据使用。
由于of_match_table是一个指针,您可以传递struct of_device_id的数组,使您的驱动程序与多个设备兼容:
static const struct of_device_id imx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", },
{ .compatible = "fsl,imx1-uart", },
{ .compatible = "fsl,imx21-uart", },
{ /* sentinel */ }
};
一旦填充了 id 数组,它必须传递给平台驱动程序的of_match_table字段,在驱动程序子结构中:
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = imx_uart_dt_ids,
[...]
},
};
在这一步,只有您的驱动程序知道您的of_device_id数组。为了让内核也知道(以便它可以将您的 ID 存储在平台核心维护的设备列表中),您的数组必须在MODULE_DEVICE_TABLE中注册,如第五章中所述,平台设备驱动程序:
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);
就是这样!我们的驱动程序是 DT 兼容的。回到我们的 DT,在那里声明一个与我们的驱动程序兼容的设备:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
[...]
};
这里提供了两个兼容的字符串。如果第一个与任何驱动程序都不匹配,核心将使用第二个进行匹配。
当发生匹配时,将调用您的驱动程序的probe函数,参数是一个struct platform_device结构,其中包含一个struct device dev字段,在其中有一个struct device_node *of_node字段,对应于我们的设备关联的节点,因此可以使用它来提取设备设置:
static int serial_imx_probe(struct platform_device *pdev)
{
[...]
struct device_node *np;
np = pdev->dev.of_node;
if (of_get_property(np, "fsl,dte-mode", NULL))
sport->dte_mode = 1;
[...]
}
一个可以检查 DT 节点是否设置来知道驱动程序是否已经在of_match的响应中加载,或者是在板子的init文件中实例化。然后应该使用of_match_device函数,以选择发起匹配的struct *of_device_id条目,其中可能包含您传递的特定数据:
static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *match;
match = of_match_device(imx_uart_dt_ids, &pdev->dev);
if (match) {
/* Devicetree, extract the data */
my_data = match->data
} else {
/* Board init file */
my_data = dev_get_platdata(&pdev->dev);
}
[...]
}
处理非设备树平台
在内核中启用了CONFIG_OF选项的情况下启用了 DT 支持。当内核中未启用 DT 支持时,人们可能希望避免使用 DT API。可以通过检查CONFIG_OF是否设置来实现。人们过去通常会做如下操作:
#ifdef CONFIG_OF
static const struct of_device_id imx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", },
{ .compatible = "fsl,imx1-uart", },
{ .compatible = "fsl,imx21-uart", },
{ /* sentinel */ }
};
/* other devicetree dependent code */
[...]
#endif
即使在缺少设备树支持时,of_device_id数据类型总是定义的,但在构建过程中,被包装在#ifdef CONFIG_OF ... #endif中的代码将被省略。这用于条件编译。这不是您唯一的选择;还有of_match_ptr宏,当OF被禁用时,它简单地返回NULL。在您需要将of_match_table作为参数传递的任何地方,它都应该被包装在of_match_ptr宏中,以便在OF被禁用时返回NULL。该宏在include/linux/of.h中定义:
#define of_match_ptr(_ptr) (_ptr) /* When CONFIG_OF is enabled */
#define of_match_ptr(_ptr) NULL /* When it is not */
我们可以这样使用它:
static int my_probe(struct platform_device *pdev)
{
const struct of_device_id *match;
match = of_match_device(of_match_ptr(imx_uart_dt_ids),
&pdev->dev);
[...]
}
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = of_match_ptr(imx_uart_dt_ids),
},
};
这消除了使用#ifdef,在OF被禁用时返回NULL。
支持具有每个特定设备数据的多个硬件
有时,驱动程序可以支持不同的硬件,每个硬件都有其特定的配置数据。这些数据可能是专用的函数表、特定的寄存器值,或者是每个硬件独有的任何内容。下面的示例描述了一种通用的方法:
让我们首先回顾一下include/linux/mod_devicetable.h中struct of_device_id的外观。
/*
* Struct used for matching a device
*/
struct of_device_id {
[...]
char compatible[128];
const void *data;
};
我们感兴趣的字段是const void *data,所以我们可以使用它来为每个特定设备传递任何数据。
假设我们拥有三种不同的设备,每个设备都有特定的私有数据。of_device_id.data将包含指向特定参数的指针。这个示例受到了drivers/tty/serial/imx.c的启发。
首先,我们声明私有结构:
/* i.MX21 type uart runs on all i.mx except i.MX1 and i.MX6q */
enum imx_uart_type {
IMX1_UART,
IMX21_UART,
IMX6Q_UART,
};
/* device type dependent stuff */
struct imx_uart_data {
unsigned uts_reg;
enum imx_uart_type devtype;
};
然后我们用每个特定设备的数据填充一个数组:
static struct imx_uart_data imx_uart_devdata[] = {
[IMX1_UART] = {
.uts_reg = IMX1_UTS,
.devtype = IMX1_UART,
},
[IMX21_UART] = {
.uts_reg = IMX21_UTS,
.devtype = IMX21_UART,
},
[IMX6Q_UART] = {
.uts_reg = IMX21_UTS,
.devtype = IMX6Q_UART,
},
};
每个兼容条目都与特定的数组索引相关联:
static const struct of_device_idimx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
{ .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
{ .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = of_match_ptr(imx_uart_dt_ids),
},
};
现在在probe函数中,无论匹配条目是什么,它都将保存指向特定设备结构的指针:
static int imx_probe_dt(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *of_id =
of_match_device(of_match_ptr(imx_uart_dt_ids), &pdev->dev);
if (!of_id)
/* no device tree device */
return 1;
[...]
sport->devdata = of_id->data; /* Get private data back */
}
在前面的代码中,devdata是原始源代码中结构的一个元素,并且声明为const struct imx_uart_data *devdata;我们可以在数组中存储任何特定的参数。
匹配样式混合
OF 匹配样式可以与任何其他匹配机制结合使用。在下面的示例中,我们混合了 DT 和设备 ID 匹配样式:
我们为设备 ID 匹配样式填充一个数组,每个设备都有自己的数据:
static const struct platform_device_id sdma_devtypes[] = {
{
.name = "imx51-sdma",
.driver_data = (unsigned long)&sdma_imx51,
}, {
.name = "imx53-sdma",
.driver_data = (unsigned long)&sdma_imx53,
}, {
.name = "imx6q-sdma",
.driver_data = (unsigned long)&sdma_imx6q,
}, {
.name = "imx7d-sdma",
.driver_data = (unsigned long)&sdma_imx7d,
}, {
/* sentinel */
}
};
MODULE_DEVICE_TABLE(platform, sdma_devtypes);
我们对 OF 匹配样式也是一样的:
static const struct of_device_idsdma_dt_ids[] = {
{ .compatible = "fsl,imx6q-sdma", .data = &sdma_imx6q, },
{ .compatible = "fsl,imx53-sdma", .data = &sdma_imx53, },
{ .compatible = "fsl,imx51-sdma", .data = &sdma_imx51, },
{ .compatible = "fsl,imx7d-sdma", .data = &sdma_imx7d, },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sdma_dt_ids);
probe函数将如下所示:
static int sdma_probe(structplatform_device *pdev)
{
conststructof_device_id *of_id =
of_match_device(of_match_ptr(sdma_dt_ids), &pdev->dev);
structdevice_node *np = pdev->dev.of_node;
/* If devicetree, */
if (of_id)
drvdata = of_id->data;
/* else, hard-coded */
else if (pdev->id_entry)
drvdata = (void *)pdev->id_entry->driver_data;
if (!drvdata) {
dev_err(&pdev->dev, "unable to find driver data\n");
return -EINVAL;
}
[...]
}
然后我们声明我们的平台驱动程序;将所有在前面的部分中定义的数组都传递进去:
static struct platform_driversdma_driver = {
.driver = {
.name = "imx-sdma",
.of_match_table = of_match_ptr(sdma_dt_ids),
},
.id_table = sdma_devtypes,
.remove = sdma_remove,
.probe = sdma_probe,
};
module_platform_driver(sdma_driver);
平台资源和 DT
平台设备可以在启用设备树的系统中工作,无需任何额外修改。这就是我们在“处理资源”部分中所展示的。通过使用platform_xxx系列函数,核心还会遍历 DT(使用of_xxx系列函数)以找到所需的资源。反之则不成立,因为of_xxx系列函数仅保留给 DT 使用。所有资源数据将以通常的方式提供给驱动程序。现在驱动程序知道这个设备是否是在板文件中以硬编码参数初始化的。让我们以一个 uart 设备节点为例:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
};
以下摘录描述了其驱动程序的probe函数。在probe中,函数“platform_get_resource()”可用于提取任何资源(内存区域、DMA、中断),或特定功能,如“platform_get_irq()”,它提取 DT 中interrupts属性提供的irq:
static int my_probe(struct platform_device *pdev)
{
struct iio_dev *indio_dev;
struct resource *mem, *dma_res;
struct xadc *xadc;
int irq, ret, dmareq;
/* irq */
irq = platform_get_irq(pdev, 0);
if (irq<= 0)
return -ENXIO;
[...]
/* memory region */
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
xadc->base = devm_ioremap_resource(&pdev->dev, mem);
/*
* We could have used
* devm_ioremap(&pdev->dev, mem->start, resource_size(mem));
* too.
*/
if (IS_ERR(xadc->base))
return PTR_ERR(xadc->base);
[...]
/* second dma channel */
dma_res = platform_get_resource(pdev, IORESOURCE_DMA, 1);
dmareq = dma_res->start;
[...]
}
总之,对于诸如dma、irq和mem之类的属性,您在平台驱动程序中无需做任何匹配dtb的工作。如果有人记得,这些数据与作为平台资源传递的数据类型相同。要理解原因,我们只需查看这些函数的内部处理方式;我们将看到它们如何内部处理 DT 函数。以下是platform_get_irq函数的示例:
int platform_get_irq(struct platform_device *dev, unsigned int num)
{
[...]
struct resource *r;
if (IS_ENABLED(CONFIG_OF_IRQ) &&dev->dev.of_node) {
int ret;
ret = of_irq_get(dev->dev.of_node, num);
if (ret > 0 || ret == -EPROBE_DEFER)
return ret;
}
r = platform_get_resource(dev, IORESOURCE_IRQ, num);
if (r && r->flags & IORESOURCE_BITS) {
struct irq_data *irqd;
irqd = irq_get_irq_data(r->start);
if (!irqd)
return -ENXIO;
irqd_set_trigger_type(irqd, r->flags & IORESOURCE_BITS);
}
return r ? r->start : -ENXIO;
}
也许有人会想知道platform_xxx函数如何从 DT 中提取资源。这应该是of_xxx函数族。你是对的,但在系统启动期间,内核会在每个设备节点上调用“of_platform_device_create_pdata()”,这将导致创建一个带有相关资源的平台设备,您可以在其上调用platform_xxx系列函数。其原型如下:
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np, const char *bus_id,
void *platform_data, struct device *parent)
平台数据与 DT
如果您的驱动程序期望平台数据,您应该检查dev.platform_data指针。非空值意味着您的驱动程序已在板配置文件中以旧方式实例化,并且 DT 不涉及其中。对于从 DT 实例化的驱动程序,dev.platform_data将为NULL,并且您的平台设备将获得指向与dev.of_node指针中对应于您设备的 DT 条目(节点)的指针,从中可以提取资源并使用 OF API 来解析和提取应用程序数据。
还有一种混合方法可以用来将在 C 文件中声明的平台数据与 DT 节点关联起来,但这只适用于特殊情况:DMA、IRQ 和内存。这种方法仅在驱动程序仅期望资源而不是特定应用程序数据时使用。
可以将 I2C 控制器的传统声明转换为 DT 兼容节点,如下所示:
#define SIRFSOC_I2C0MOD_PA_BASE 0xcc0e0000
#define SIRFSOC_I2C0MOD_SIZE 0x10000
#define IRQ_I2C0
static struct resource sirfsoc_i2c0_resource[] = {
{
.start = SIRFSOC_I2C0MOD_PA_BASE,
.end = SIRFSOC_I2C0MOD_PA_BASE + SIRFSOC_I2C0MOD_SIZE - 1,
.flags = IORESOURCE_MEM,
},{
.start = IRQ_I2C0,
.end = IRQ_I2C0,
.flags = IORESOURCE_IRQ,
},
};
和 DT 节点:
i2c0: i2c@cc0e0000 {
compatible = "sirf,marco-i2c";
reg = <0xcc0e0000 0x10000>;
interrupt-parent = <&phandle_to_interrupt_controller_node>
interrupts = <0 24 0>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
总结
现在是从硬编码设备配置切换到 DT 的时候了。本章为您提供了处理 DT 所需的一切。现在您已经具备了自定义或添加任何节点和属性到 DT 中,并从驱动程序中提取它们的必要技能。在下一章中,我们将讨论 I2C 驱动程序,并使用 DT API 来枚举和配置我们的 I2C 设备。
第七章:I2C 客户端驱动程序
由飞利浦(现在是 NXP)发明的 I2C 总线是一种双线:串行数据(SDA),串行时钟(SCL)异步串行总线。它是一个多主总线,尽管多主模式并不广泛使用。SDA 和 SCL 都是开漏/开集电器,这意味着它们中的每一个都可以将其输出拉低,但没有一个可以在没有上拉电阻的情况下将其输出拉高。SCL 由主机生成,以同步通过总线传输的数据(由 SDA 携带)。从机和主机都可以发送数据(当然不是同时),从而使 SDA 成为双向线。也就是说,SCL 信号也是双向的,因为从机可以通过保持 SCL 线低来拉伸时钟。总线由主机控制,而在我们的情况下,主机是 SoC 的一部分。这种总线经常用于嵌入式系统,用于连接串行 EEPROM、RTC 芯片、GPIO 扩展器、温度传感器等等:

I2C 总线和设备
时钟速度从 10 KHz 到 100 KHz,400 KHz 到 2 MHz 不等。我们不会在本书中涵盖总线规格或总线驱动程序。然而,总线驱动程序负责管理总线并处理规格。例如,i.MX6 芯片的总线驱动程序的示例可以在内核源代码的drivers/i2C/busses/i2c-imx.c中找到,I2C 规格可以在www.nxp.com/documents/user_manual/UM10204.pdf中找到。
在本章中,我们对客户端驱动程序感兴趣,以处理坐在总线上的从设备。本章将涵盖以下主题:
-
I2C 客户端驱动程序架构
-
访问设备,因此从/向设备读取/写入数据
-
从 DT 中声明客户端
驱动程序架构
当您为其编写驱动程序的设备坐在称为总线控制器的物理总线上时,它必须依赖于称为控制器驱动程序的总线的驱动程序,负责在设备之间共享总线访问。控制器驱动程序在您的设备和总线之间提供了一个抽象层。每当您在 I2C 或 USB 总线上执行事务(读或写)时,例如,I2C/USB 总线控制器会在后台自动处理。每个总线控制器驱动程序都导出一组函数,以便为坐在该总线上的设备开发驱动程序。这适用于每个物理总线(I2C、SPI、USB、PCI、SDIO 等)。
I2C 驱动程序在内核中表示为 struct i2c_driver 的实例。I2C 客户端(代表设备本身)由 struct i2c_client 结构表示。
i2c_driver 结构
在内核中,I2C 驱动程序被声明为struct i2c_driver的实例,其外观如下:
struct i2c_driver {
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
struct i2c_driver 结构包含和表征了通用访问例程,需要处理声称驱动程序的设备,而 struct i2c_client 包含设备特定信息,比如它的地址。struct i2c_client 结构代表和表征了一个 I2C 设备。在本章的后面,我们将看到如何填充这些结构。
probe()函数
probe()函数是struct i2c_driver结构的一部分,一旦实例化了一个 I2C 设备,它就会被执行。它负责以下任务:
-
检查设备是否是您期望的设备
-
使用
i2c_check_functionality函数检查 SoC 的 I2C 总线控制器是否支持设备所需的功能 -
初始化设备
-
设置设备特定数据
-
注册适当的内核框架
probe 函数的原型如下:
static int foo_probe(struct i2c_client *client, const struct
i2c_device_id *id)
正如您所看到的,它的参数是:
struct i2c_client指针:这代表 I2C 设备本身。这个结构继承自设备结构,并由内核提供给您的probe函数。客户端结构在include/linux/i2c.h中定义。它的定义如下:
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct device dev; /* the device structure */
intirq; /* irq issued by device */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
-
所有字段都由内核填充,基于您提供的参数来注册客户端。我们稍后将看到如何向内核注册设备。
-
struct i2c_device_id指针:这指向与正在被探测的设备匹配的 I2C 设备 ID 条目。
每个设备的数据
I2C 核心为您提供了将指针存储到您选择的任何数据结构中的可能性,作为特定于设备的数据。要存储或检索数据,请使用 I2C 核心提供的以下函数:
/* set the data */
void i2c_set_clientdata(struct i2c_client *client, void *data);
/* get the data */
void *i2c_get_clientdata(const struct i2c_client *client);
这些函数内部调用dev_set_drvdata和dev_get_drvdata来更新或获取struct i2c_client结构中struct device子结构的void *driver_data字段的值。
这是一个如何使用额外客户数据的例子;摘自drivers/gpio/gpio-mc9s08dz60.c:
/* This is the device specific data structure */
struct mc9s08dz60 {
struct i2c_client *client;
struct gpio_chip chip;
};
static int mc9s08dz60_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mc9s08dz60 *mc9s;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
mc9s = devm_kzalloc(&client->dev, sizeof(*mc9s), GFP_KERNEL);
if (!mc9s)
return -ENOMEM;
[...]
mc9s->client = client;
i2c_set_clientdata(client, mc9s);
return gpiochip_add(&mc9s->chip);
}
实际上,这些函数并不真正特定于 I2C。它们只是获取/设置struct device的void *driver_data指针,它本身是struct i2c_client的成员。实际上,我们可以直接使用dev_get_drvdata和dev_set_drvdata。可以在linux/include/linux/i2c.h中看到它们的定义。
remove()函数
remove函数的原型如下:
static int foo_remove(struct i2c_client *client)
remove()函数还提供与probe()函数相同的struct i2c_client*,因此您可以检索您的私有数据。例如,您可能需要根据您在probe函数中设置的私有数据进行一些清理或其他操作:
static int mc9s08dz60_remove(struct i2c_client *client)
{
struct mc9s08dz60 *mc9s;
/* We retrieve our private data */
mc9s = i2c_get_clientdata(client);
/* Wich hold gpiochip we want to work on */
return gpiochip_remove(&mc9s->chip);
}
remove函数负责从我们在probe()函数中注册的子系统中注销我们。在上面的例子中,我们只是从内核中移除gpiochip。
驱动程序初始化和注册
当模块加载时,可能需要进行一些初始化。大多数情况下,只需向 I2C 核心注册驱动程序即可。同时,当模块被卸载时,通常只需要从 I2C 核心中移除自己。在第五章,平台设备驱动程序中,我们看到使用 init/exit 函数并不值得,而是使用module_*_driver函数。在这种情况下,要使用的函数是:
module_i2c_driver(foo_driver);
驱动程序和设备供应
正如我们在匹配机制中看到的,我们需要提供一个device_id数组,以便公开我们的驱动程序可以管理的设备。由于我们谈论的是 I2C 设备,结构将是i2c_device_id。该数组将向内核通知我们对驱动程序中感兴趣的设备。
现在回到我们的 I2C 设备驱动程序;在include/linux/mod_devicetable.h中查看,您将看到struct i2c_device_id的定义:
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_tdriver_data; /* Data private to the driver */
};
也就是说,struct i2c_device_id必须嵌入在struct i2c_driver中。为了让 I2C 核心(用于模块自动加载)知道我们需要处理的设备,我们必须使用MODULE_DEVICE_TABLE宏。内核必须知道每当发生匹配时调用哪个probe或remove函数,这就是为什么我们的probe和remove函数也必须嵌入在同一个i2c_driver结构中:
static struct i2c_device_id foo_idtable[] = {
{ "foo", my_id_for_foo },
{ "bar", my_id_for_bar },
{ }
};
MODULE_DEVICE_TABLE(i2c, foo_idtable);
static struct i2c_driver foo_driver = {
.driver = {
.name = "foo",
},
.id_table = foo_idtable,
.probe = foo_probe,
.remove = foo_remove,
}
访问客户端
串行总线事务只是访问寄存器以设置/获取其内容。I2C 遵守这一原则。I2C 核心提供了两种 API,一种用于普通的 I2C 通信,另一种用于与 SMBUS 兼容设备通信,它也适用于 I2C 设备,但反之则不然。
普通 I2C 通信
以下是通常在与 I2C 设备通信时处理的基本函数:
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
几乎所有 I2C 通信函数的第一个参数都是struct i2c_client。第二个参数包含要读取或写入的字节,第三个表示要读取或写入的字节数。与任何读/写函数一样,返回的值是读取/写入的字节数。还可以使用以下函数处理消息传输:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg,
int num);
i2c_transfer发送一组消息,每个消息可以是读取或写入操作,并且可以以任何方式混合。请记住,每个事务之间没有停止位。查看include/uapi/linux/i2c.h,消息结构如下:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags; /* Message flags */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
i2c_msg结构描述和表征了一个 I2C 消息。对于每个消息,它必须包含客户端地址、消息的字节数和消息有效载荷。
msg.len是u16。这意味着您的读/写缓冲区的长度必须始终小于 2¹⁶(64k)。
让我们看一下微芯片 I2C 24LC512eeprom 字符驱动程序的read函数;我们应该了解事物是如何真正工作的。本书的源代码中提供了完整的代码。
ssize_t
eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
[...]
int _reg_addr = dev->current_pointer;
u8 reg_addr[2];
reg_addr[0] = (u8)(_reg_addr>> 8);
reg_addr[1] = (u8)(_reg_addr& 0xFF);
struct i2c_msg msg[2];
msg[0].addr = dev->client->addr;
msg[0].flags = 0; /* Write */
msg[0].len = 2; /* Address is 2bytes coded */
msg[0].buf = reg_addr;
msg[1].addr = dev->client->addr;
msg[1].flags = I2C_M_RD; /* We need to read */
msg[1].len = count;
msg[1].buf = dev->data;
if (i2c_transfer(dev->client->adapter, msg, 2) < 0)
pr_err("ee24lc512: i2c_transfer failed\n");
if (copy_to_user(buf, dev->data, count) != 0) {
retval = -EIO;
goto end_read;
}
[...]
}
msg.flags应为I2C_M_RD表示读取,0表示写入事务。有时,您可能不想创建struct i2c_msg,而只是进行简单的读取和写入。
系统管理总线(SMBus)兼容函数
SMBus 是由英特尔开发的双线总线,与 I2C 非常相似。I2C 设备是 SMBus 兼容的,但反之则不然。因此,如果对于正在为其编写驱动程序的芯片有疑问,最好使用 SMBus 函数。
以下显示了一些 SMBus API:
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_byte_data(struct i2c_client *client,
u8 command, u8 value);
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_data(struct i2c_client *client,
u8 command, u16 value);
s32 i2c_smbus_read_block_data(struct i2c_client *client,
u8 command, u8 *values);
s32 i2c_smbus_write_block_data(struct i2c_client *client,
u8 command, u8 length, const u8 *values);
有关更多解释,请查看内核源代码中的include/linux/i2c.h和drivers/i2c/i2c-core.c。
以下示例显示了在 I2C gpio 扩展器中进行简单的读/写操作:
struct mcp23016 {
struct i2c_client *client;
structgpio_chip chip;
structmutex lock;
};
[...]
/* This function is called when one needs to change a gpio state */
static int mcp23016_set(struct mcp23016 *mcp,
unsigned offset, intval)
{
s32 value;
unsigned bank = offset / 8 ;
u8 reg_gpio = (bank == 0) ? GP0 : GP1;
unsigned bit = offset % 8 ;
value = i2c_smbus_read_byte_data(mcp->client, reg_gpio);
if (value >= 0) {
if (val)
value |= 1 << bit;
else
value &= ~(1 << bit);
return i2c_smbus_write_byte_data(mcp->client,
reg_gpio, value);
} else
return value;
}
[...]
在板配置文件中实例化 I2C 设备(旧的和不推荐的方法)
我们必须告知内核系统上物理存在哪些设备。有两种方法可以实现。在 DT 中,正如我们将在本章后面看到的,或者通过板配置文件(这是旧的和不推荐的方法)。让我们看看如何在板配置文件中实现这一点:
struct i2c_board_info是用于表示我们板上的 I2C 设备的结构。该结构定义如下:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short addr;
void *platform_data;
int irq;
};
再次,我们已经从结构中删除了对我们不相关的元素。
在上述结构中,type应包含与设备驱动程序中的i2c_driver.driver.name字段中定义的相同值。然后,您需要填充一个i2c_board_info数组,并将其作为参数传递给板初始化例程中的i2c_register_board_info函数:
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len)
在这里,busnum是设备所在的总线编号。这是一种旧的和不推荐的方法,因此我不会在本书中进一步介绍。请随时查看内核源代码中的Documentation/i2c/instantiating-devices,以了解如何完成这些操作。
I2C 和设备树
正如我们在前面的章节中所看到的,为了配置 I2C 设备,基本上有两个步骤:
-
定义和注册 I2C 驱动程序
-
定义和注册 I2C 设备
I2C 设备属于 DT 中的非内存映射设备系列,而 I2C 总线是可寻址总线(通过可寻址,我是指您可以在总线上寻址特定设备)。在这种情况下,设备节点中的reg属性表示总线上的设备地址。
I2C 设备节点都是它们所在总线节点的子节点。每个设备只分配一个地址。没有长度或范围的涉及。I2C 设备需要声明的标准属性是reg,表示设备在总线上的地址,以及compatible字符串,用于将设备与驱动程序匹配。有关寻址的更多信息,可以参考第六章,设备树的概念。
&i2c2 { /* Phandle of the bus node */
pcf8523: rtc@68 {
compatible = "nxp,pcf8523";
reg = <0x68>;
};
eeprom: ee24lc512@55 { /* eeprom device */
compatible = "packt,ee24lc512";
reg = <0x55>;
};
};
上述示例声明了 SoC 的 I2C 总线编号 2 上地址为 0x50 的 HDMI EDID 芯片,以及在同一总线上地址为 0x68 的实时时钟(RTC)。
定义和注册 I2C 驱动程序
到目前为止,我们所看到的并没有改变。我们需要额外的是定义一个struct of_device_id。Struct of_device_id定义为匹配.dts文件中相应节点的结构:
/* no extra data for this device */
static const struct of_device_id foobar_of_match[] = {
{ .compatible = "packtpub,foobar-device" },
{}
};
MODULE_DEVICE_TABLE(of, foobar_of_match);
现在我们定义i2c_driver如下:
static struct i2c_driver foo_driver = {
.driver = {
.name = "foo",
.of_match_table = of_match_ptr(foobar_of_match), /* Only this line is added */
},
.probe = foo_probe,
.id_table = foo_id,
};
然后可以通过以下方式改进probe函数:
static int my_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
const struct of_device_id *match;
match = of_match_device(mcp23s08_i2c_of_match, &client->dev);
if (match) {
/* Device tree code goes here */
} else {
/*
* Platform data code comes here.
* One can use
* pdata = dev_get_platdata(&client->dev);
*
* or *id*, which is a pointer on the *i2c_device_id* entry that originated
* the match, in order to use *id->driver_data* to extract the device
* specific data, as described in platform driver chapter.
*/
}
[...]
}
备注
对于早于 4.10 的内核版本,如果查看drivers/i2c/i2c-core.c,在i2c_device_probe()函数中(供参考,这是内核每次向 I2C 核心注册 I2C 设备时调用的函数),将看到类似于以下内容:
if (!driver->probe || !driver->id_table)
return -ENODEV;
这意味着即使一个人不需要使用.id_table,在驱动程序中也是强制性的。实际上,可以只使用 OF 匹配样式,但不能摆脱.id_table。内核开发人员试图消除对.id_table的需求,并专门使用.of_match_table进行设备匹配。补丁可以在此 URL 找到:git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=c80f52847c50109ca248c22efbf71ff10553dca4。
然而,已经发现了回归问题,并且提交已被撤销。有关详细信息,请查看此处:git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=661f6c1cd926c6c973e03c6b5151d161f3a666ed。自内核版本>= 4.10 以来,已经修复了此问题。修复如下:
/*
* An I2C ID table is not mandatory, if and only if, a suitable Device
* Tree match table entry is supplied for the probing device.
*/
if (!driver->id_table &&
!i2c_of_match_device(dev->driver->of_match_table, client))
return -ENODEV;
换句话说,对于 I2C 驱动程序,必须同时定义.id_table和.of_match_table,否则您的设备将无法在内核版本 4.10 或更早版本中进行探测。
在设备树中实例化 I2C 设备-新方法
struct i2c_client是用于描述 I2C 设备的结构。但是,使用 OF 样式,这个结构不再能在板文件中定义。我们需要做的唯一的事情就是在 DT 中提供设备的信息,内核将根据此信息构建一个设备。
以下代码显示了如何在dts文件中声明我们的 I2C foobar设备节点:
&i2c3 {
status = "okay";
foo-bar: foo@55 {
compatible = "packtpub,foobar-device";
reg = <55>;
};
};
将所有内容放在一起
总结编写 I2C 客户端驱动程序所需的步骤:
-
声明驱动程序支持的设备 ID。您可以使用
i2c_device_id来实现。如果支持 DT,也可以使用of_device_id。 -
调用
MODULE_DEVICE_TABLE(i2c, my_id_table)将设备列表注册到 I2C 核心。如果支持设备树,必须调用MODULE_DEVICE_TABLE(of, your_of_match_table)将设备列表注册到 OF 核心。 -
根据各自的原型编写
probe和remove函数。如果需要,还要编写电源管理函数。probe函数必须识别您的设备,配置它,定义每个设备(私有)数据,并向适当的内核框架注册。驱动程序的行为取决于您在probe函数中所做的事情。remove函数必须撤消您在probe函数中所做的一切(释放内存并从任何框架中注销)。 -
声明并填充
struct i2c_driver结构,并使用您创建的 id 数组设置id_table字段。使用上面编写的相应函数的名称设置.probe和.remove字段。在.driver子结构中,将.owner字段设置为THIS_MODULE,设置驱动程序名称,最后,如果支持 DT,则使用of_device_id数组设置.of_match_table字段。 -
使用刚刚填写的
i2c_driver结构调用module_i2c_driver函数:module_i2c_driver(serial_eeprom_i2c_driver),以便将驱动程序注册到内核中。
总结
我们刚刚处理了 I2C 设备驱动程序。现在是时候选择市场上的任何 I2C 设备并编写相应的驱动程序,支持 DT。本章讨论了内核 I2C 核心和相关 API,包括设备树支持,以便为您提供与 I2C 设备通信所需的技能。您应该能够编写高效的probe函数并向内核 I2C 核心注册。在下一章中,我们将使用在这里学到的技能来开发 SPI 设备驱动程序。
第八章:SPI 设备驱动程序
串行外围接口(SPI)是一个(至少)四线总线--主输入从输出(MISO),主输出从输入(MOSI),串行时钟(SCK)和片选(CS),用于连接串行闪存,AD/DA 转换器。主机始终生成时钟。其速度可以达到 80 MHz,即使没有真正的速度限制(比 I2C 快得多)。CS 线也是由主机管理的。
每个信号名称都有一个同义词:
-
每当您看到 SIMO,SDI,DI 或 SDA 时,它们指的是 MOSI。
-
SOMI,SDO,DO,SDA 将指的是 MISO。
-
SCK,CLK,SCL 将指的是 SCK。
-
S̅ S̅是从选择线,也称为 CS。可以使用 CSx(其中 x 是索引,CS0,CS1),也可以使用 EN 和 ENB,表示启用。CS 通常是一个低电平有效的信号:

SPI 拓扑结构(来自维基百科的图片)
本章将介绍 SPI 驱动程序的概念,例如:
-
SPI 总线描述
-
驱动程序架构和数据结构描述
-
半双工和全双工中的数据发送和接收
-
从 DT 声明 SPI 设备
-
从用户空间访问 SPI 设备,既可以进行半双工也可以进行全双工
驱动程序架构
在 Linux 内核中 SPI 的必需头文件是<linux/spi/spi.h>。在讨论驱动程序结构之前,让我们看看内核中如何定义 SPI 设备。在内核中,SPI 设备表示为spi_device的实例。管理它们的驱动程序实例是struct spi_driver结构。
设备结构
struct spi_device结构表示一个 SPI 设备,并在include/linux/spi/spi.h中定义:
struct spi_device {
struct devicedev;
struct spi_master*master;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
int irq;
[...]
int cs_gpio; /* chip select gpio */
};
对我们来说没有意义的一些字段已被删除。也就是说,以下是结构中元素的含义:
-
master:这代表 SPI 控制器(总线),设备连接在其上。 -
max_speed_hz:这是与芯片一起使用的最大时钟速率(在当前板上);此参数可以从驱动程序内部更改。您可以使用每次传输的spi_transfer.speed_hz覆盖该参数。我们将在后面讨论 SPI 传输。 -
chip_select:这允许您启用需要通信的芯片,区分由主控制的芯片。chip_select默认为低电平有效。此行为可以通过在模式中添加SPI_CS_HIGH标志来更改。 -
mode:这定义了数据应该如何进行时钟同步。设备驱动程序可以更改这个。默认情况下,每次传输中的每个字的数据同步是最高有效位(MSB)优先。可以通过指定SPI_LSB_FIRST来覆盖此行为。 -
irq:这代表中断号(在您的板init文件或通过 DT 中注册为设备资源),您应该传递给request_irq()以从此设备接收中断。
关于 SPI 模式的一点说明;它们是使用两个特征构建的:
-
CPOL:这是初始时钟极性: -
0:初始时钟状态为低,并且第一个边沿为上升 -
1:初始时钟状态为高,并且第一个状态为下降 -
CPHA:这是时钟相位,选择在哪个边沿对数据进行采样: -
0:数据在下降沿(高到低转换)锁存,而输出在上升沿改变 -
1:在上升沿(低到高转换)锁存的数据,并在下降沿输出
这允许根据include/linux/spi/spi.h中的以下宏在内核中定义四种 SPI 模式:
#define SPI_CPHA 0x01
#define SPI_CPOL 0x02
然后可以生成以下数组来总结事情:
| 模式 | CPOL | CPHA | 内核宏 |
|---|---|---|---|
| 0 | 0 | 0 | #define SPI_MODE_0 (0|0) |
| 1 | 0 | 1 | #define SPI_MODE_1 (0|SPI_CPHA) |
| 2 | 1 | 0 | #define SPI_MODE_2 (SPI_CPOL|0) |
| 3 | 1 | 1 | #define SPI_MODE_3 (SPI_CPOL|SPI_CPHA) |
以下是每种 SPI 模式的表示,如前述数组中定义的。也就是说,只有 MOSI 线被表示,但对于 MISO 原理是相同的。

常用模式是SPI_MODE_0和SPI_MODE_3。
spi_driver 结构
struct spi_driver代表您开发的用于管理 SPI 设备的驱动程序。其结构如下:
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
probe()功能
它的原型如下:
static int probe(struct spi_device *spi)
您可以参考[第七章](text00189.html),I2C 客户端驱动程序,以了解在“探测”功能中要做什么。相同的步骤也适用于这里。因此,与无法在运行时更改控制器总线参数(CS 状态,每字位,时钟)的 I2C 驱动程序不同,SPI 驱动程序可以。您可以根据设备属性设置总线。
典型的 SPI“探测”功能如下所示:
static int my_probe(struct spi_device *spi)
{
[...] /* declare your variable/structures */
/* bits_per_word cannot be configured in platform data */
spi->mode = SPI_MODE_0; /* SPI mode */
spi->max_speed_hz = 20000000; /* Max clock for the device */
spi->bits_per_word = 16; /* device bit per word */
ret = spi_setup(spi);
ret = spi_setup(spi);
if (ret < 0)
return ret;
[...] /* Make some init */
[...] /* Register with apropriate framework */
return ret;
}
struct spi_device*是一个输入参数,由内核传递给“探测”功能。它代表您正在探测的设备。在您的“探测”功能中,您可以使用spi_get_device_id(在id_table match的情况下)获取触发匹配的spi_device_id并提取驱动程序数据:
const struct spi_device_id *id = spi_get_device_id(spi);
my_private_data = array_chip_info[id->driver_data];
每个设备数据
在“探测”功能中,跟踪私有(每个设备)数据以在模块生命周期中使用是一项常见任务。这已在[第七章](text00189.html),I2C 客户端驱动程序中讨论过。
以下是用于设置/获取每个设备数据的函数的原型:
/* set the data */
void spi_set_drvdata(struct *spi_device, void *data);
/* Get the data back */
void *spi_get_drvdata(const struct *spi_device);
例如:
struct mc33880 {
struct mutex lock;
u8 bar;
struct foo chip;
struct spi_device *spi;
};
static int mc33880_probe(struct spi_device *spi)
{
struct mc33880 *mc;
[...] /* Device set up */
mc = devm_kzalloc(&spi->dev, sizeof(struct mc33880),
GFP_KERNEL);
if (!mc)
return -ENOMEM;
mutex_init(&mc->lock);
spi_set_drvdata(spi, mc);
mc->spi = spi;
mc->chip.label = DRIVER_NAME,
mc->chip.set = mc33880_set;
/* Register with appropriate framework */
[...]
}
remove()功能
remove功能必须释放在“探测”功能中抓取的每个资源。其结构如下:
static int my_remove(struct spi_device *spi);
典型的remove功能可能如下所示:
static int mc33880_remove(struct spi_device *spi)
{
struct mc33880 *mc;
mc = spi_get_drvdata(spi); /* Get our data back */
if (!mc)
return -ENODEV;
/*
* unregister from frameworks with which we registered in the
* probe function
*/
[...]
mutex_destroy(&mc->lock);
return 0;
}
驱动程序初始化和注册
对于设备坐在总线上,无论是物理总线还是伪平台总线,大部分时间都是在“探测”功能中完成的。 init和exit功能只是用来在总线核心中注册/注销驱动程序:
static int __init foo_init(void)
{
[...] /*My init code */
return spi_register_driver(&foo_driver);
}
module_init(foo_init);
static void __exit foo_cleanup(void)
{
[...] /* My clean up code */
spi_unregister_driver(&foo_driver);
}
module_exit(foo_cleanup);
也就是说,如果您除了注册/注销驱动程序之外什么都不做,内核会提供一个宏:
module_spi_driver(foo_driver);
这将在内部调用spi_register_driver和spi_unregister_driver。这与我们在上一章中看到的完全相同。
驱动程序和设备配置
由于我们需要对 I2C 设备使用i2c_device_id,所以我们必须对 SPI 设备使用spi_device_id,以便为我们的设备提供device_id数组进行匹配。它在include/linux/mod_devicetable.h中定义:
struct spi_device_id {
char name[SPI_NAME_SIZE];
kernel_ulong_t driver_data; /* Data private to the driver */
};
我们需要将我们的数组嵌入到struct spi_device_id中,以便通知 SPI 核心我们需要在驱动程序中管理的设备 ID,并在驱动程序结构上调用MODULE_DEVICE_TABLE宏。当然,宏的第一个参数是设备所在的总线的名称。在我们的情况下,它是 SPI:
#define ID_FOR_FOO_DEVICE 0
#define ID_FOR_BAR_DEVICE 1
static struct spi_device_id foo_idtable[] = {
{ "foo", ID_FOR_FOO_DEVICE },
{ "bar", ID_FOR_BAR_DEVICE },
{ }
};
MODULE_DEVICE_TABLE(spi, foo_idtable);
static struct spi_driver foo_driver = {
.driver = {
.name = "KBUILD_MODULE",
},
.id_table = foo_idtable,
.probe = foo_probe,
.remove = foo_remove,
};
module_spi_driver(foo_driver);
在板配置文件中实例化 SPI 设备-旧的和不推荐的方法
只有在系统不支持设备树时,设备才应该在板文件中实例化。由于设备树已经出现,这种实例化方法已被弃用。因此,让我们只记住板文件位于arch/目录中。用于表示 SPI 设备的结构是struct spi_board_info,而不是我们在驱动程序中使用的struct spi_device。只有在您填写并使用spi_register_board_info函数注册了struct spi_board_info后,内核才会构建一个struct spi_device(它将传递给您的驱动程序并在 SPI 核心中注册)。
请随意查看include/linux/spi/spi.h中的struct spi_board_info字段。spi_register_board_info的定义可以在drivers/spi/spi.c中找到。现在让我们来看看板文件中的一些 SPI 设备注册:
/**
* Our platform data
*/
struct my_platform_data {
int foo;
bool bar;
};
static struct my_platform_data mpfd = {
.foo = 15,
.bar = true,
};
static struct spi_board_info
my_board_spi_board_info[] __initdata = {
{
/* the modalias must be same as spi device driver name */
.modalias = "ad7887", /* Name of spi_driver for this device */
.max_speed_hz = 1000000, /* max spi clock (SCK) speed in HZ */
.bus_num = 0, /* Framework bus number */
.irq = GPIO_IRQ(40),
.chip_select = 3, /* Framework chip select */
.platform_data = &mpfd,
.mode = SPI_MODE_3,
},{
.modalias = "spidev",
.chip_select = 0,
.max_speed_hz = 1 * 1000 * 1000,
.bus_num = 1,
.mode = SPI_MODE_3,
},
};
static int __init board_init(void)
{
[...]
spi_register_board_info(my_board_spi_board_info, ARRAY_SIZE(my_board_spi_board_info));
[...]
return 0;
}
[...]
SPI 和设备树
与 I2C 设备一样,SPI 设备属于设备树中的非内存映射设备系列,但也是可寻址的。这里,地址表示控制器(主控)给定的 CS(从 0 开始)列表中的 CS 索引。例如,我们可能在 SPI 总线上有三个不同的 SPI 设备,每个设备都有自己的 CS 线。主控将获得一组 GPIO,每个 GPIO 代表一个 CS 以激活设备。如果设备 X 使用第二个 GPIO 线作为 CS,我们必须将其地址设置为 1(因为我们总是从 0 开始)在reg属性中。
以下是 SPI 设备的真实 DT 列表:
ecspi1 {
fsl,spi-num-chipselects = <3>;
cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>;
pinctrl-0 = <&pinctrl_ecspi1 &pinctrl_ecspi1_cs>;
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "okay";
ad7606r8_0: ad7606r8@0 {
compatible = "ad7606-8";
reg = <0>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
};
label: fake_spi_device@1 {
compatible = "packtpub,foobar-device";
reg = <1>;
a-string-param = "stringvalue";
spi-cs-high;
};
mcp2515can: can@2 {
compatible = "microchip,mcp2515";
reg = <2>;
spi-max-frequency = <1000000>;
clocks = <&clk8m>;
interrupt-parent = <&gpio4>;
interrupts = <29 IRQ_TYPE_LEVEL_LOW>;
};
};
SPI 设备节点中引入了一个新属性:spi-max-frequency。它表示设备的最大 SPI 时钟速度(以赫兹为单位)。每当访问设备时,总线控制器驱动程序将确保时钟不会超过此限制。其他常用的属性包括:
-
spi-cpol:这是一个布尔值(空属性),表示设备需要反向时钟极性模式。它对应于 CPOL。 -
spi-cpha:这是一个空属性,表示设备需要移位时钟相位模式。它对应于 CPHA。 -
spi-cs-high:默认情况下,SPI 设备需要 CS 低才能激活。这是一个布尔属性,表示设备需要 CS 高活动。
也就是说,要获取完整的 SPI 绑定元素列表,您可以参考内核源代码中的Documentation/devicetree/bindings/spi/spi-bus.txt。
在设备树中实例化 SPI 设备-新的方法
通过正确填写设备节点,内核将为我们构建一个struct spi_device,并将其作为参数传递给我们的 SPI 核心函数。以下只是先前定义的 SPI DT 列表的摘录:
&ecspi1 {
status = "okay";
label: fake_spi_device@1 {
compatible = "packtpub,foobar-device";
reg = <1>;
a-string-param = "stringvalue";
spi-cs-high;
};
};
定义和注册 SPI 驱动程序
同样的原则适用于 I2C 驱动程序。我们需要定义一个struct of_device_id来匹配设备,然后调用MODULE_DEVICE_TABLE宏来注册到 OF 核心:
static const struct of_device_id foobar_of_match[] = {
{ .compatible = "packtpub,foobar-device" },
{ .compatible = "packtpub,barfoo-device" },
{}
};
MODULE_DEVICE_TABLE(of, foobar_of_match);
然后定义我们的spi_driver如下:
static struct spi_driver foo_driver = {
.driver = {
.name = "foo",
/* The following line adds Device tree */
.of_match_table = of_match_ptr(foobar_of_match),
},
.probe = my_spi_probe,
.id_table = foo_id,
};
然后可以通过以下方式改进probe函数:
static int my_spi_probe(struct spi_device *spi)
{
const struct of_device_id *match;
match = of_match_device(of_match_ptr(foobar_of_match), &spi->dev);
if (match) {
/* Device tree code goes here */
} else {
/*
* Platform data code comes here.
* One can use
* pdata = dev_get_platdata(&spi->dev);
*
* or *id*, which is a pointer on the *spi_device_id* entry that originated
* the match, in order to use *id->driver_data* to extract the device
* specific data, as described in Chapter 5, Platform Device Drivers.
*/
}
[...]
}
访问和与客户端交流
SPI I/O 模型由一组排队的消息组成。我们提交一个或多个struct spi_message结构,这些结构被同步或异步地处理和完成。单个消息由一个或多个struct spi_transfer对象组成,每个对象代表一个全双工 SPI 传输。这两个主要结构用于在驱动程序和设备之间交换数据。它们都在include/linux/spi/spi.h中定义:

SPI 消息结构
struct spi_transfer代表一个全双工 SPI 传输:
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma;
dma_addr_t rx_dma;
unsigned cs_change:1;
unsigned tx_nbits:3;
unsigned rx_nbits:3;
#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
};
以下是结构元素的含义:
-
tx_buf:这个缓冲区包含要写入的数据。在只读事务的情况下,它应为 NULL 或保持不变。在需要通过直接内存访问(DMA)执行 SPI 事务的情况下,它应该是dma-安全的。 -
rx_buf:这是用于读取数据的缓冲区(具有与tx_buf相同的属性),或在只写事务中为 NULL。 -
tx_dma:这是tx_buf的 DMA 地址,如果spi_message.is_dma_mapped设置为1。DMA 在第十二章中讨论,DMA-直接内存访问。 -
rx_dma:这与tx_dma相同,但用于rx_buf。 -
len:这表示rx和tx缓冲区的字节大小,这意味着如果两者都被使用,它们必须具有相同的大小。 -
speed_hz:这会覆盖默认速度,指定为spi_device.max_speed_hz,但仅适用于当前传输。如果为0,则使用默认值(在struct spi_device结构中提供)。 -
bits_per_word:数据传输涉及一个或多个字。一个字是数据的单位,其大小以位为单位根据需要变化。在这里,bits_per_word表示此 SPI 传输的字位大小。这将覆盖spi_device.bits_per_word中提供的默认值。如果为0,则使用默认值(来自spi_device)。 -
cs_change:这确定此传输完成后chip_select线的状态。 -
delay_usecs:这表示在此传输之后的延迟(以微秒为单位),然后(可选)更改chip_select状态,然后开始下一个传输或完成此spi_message。
在另一侧,struct spi_message被用来原子地包装一个或多个 SPI 传输。驱动程序将独占使用 SPI 总线,直到完成构成消息的每个传输。SPI 消息结构也在include/linux/spi/spi.h中定义:
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
/* completion is reported through a callback */
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
};
-
transfers:这是构成消息的传输列表。稍后我们将看到如何将传输添加到此列表中。 -
is_dma_mapped:这告诉控制器是否使用 DMA(或不使用)执行事务。然后,您的代码负责为每个传输缓冲区提供 DMA 和 CPU 虚拟地址。 -
complete:这是在事务完成时调用的回调,context是要传递给回调的参数。 -
frame_length:这将自动设置为消息中的总字节数。 -
actual_length:这是所有成功段中传输的字节数。 -
status:这报告传输状态。成功为零,否则为-errno。
消息中的spi_transfer元素按 FIFO 顺序处理。在消息完成之前,您必须确保不使用传输缓冲区,以避免数据损坏。您进行完成调用以确保可以。
在消息可以提交到总线之前,必须使用void spi_message_init(struct spi_message *message)对其进行初始化,这将将结构中的每个元素都设置为零,并初始化transfers列表。对于要添加到消息中的每个传输,应该在该传输上调用void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m),这将导致将传输排队到transfers列表中。完成后,您有两种选择来启动事务:
-
同步地,使用
int spi_sync(struct spi_device *spi, struct spi_message *message)函数,这可能会休眠,不应在中断上下文中使用。这里不需要回调的完成。这个函数是第二个函数(spi_async())的包装器。 -
异步地,使用
spi_async()函数,也可以在原子上下文中使用,其原型为int spi_async(struct spi_device *spi, struct spi_message *message)。在这里提供回调是一个好习惯,因为它将在消息完成时执行。
以下是单个传输 SPI 消息事务可能看起来像的内容:
char tx_buf[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x40, 0x00, 0x00, 0x00,
0x00, 0x95, 0xEF, 0xBA, 0xAD,
0xF0, 0x0D,
};
char rx_buf[10] = {0,};
int ret;
struct spi_message single_msg;
struct spi_transfer single_xfer;
single_xfer.tx_buf = tx_buf;
single_xfer.rx_buf = rx_buf;
single_xfer.len = sizeof(tx_buff);
single_xfer.bits_per_word = 8;
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
ret = spi_sync(spi, &msg);
现在让我们写一个多传输消息事务:
struct {
char buffer[10];
char cmd[2]
int foo;
} data;
struct data my_data[3];
initialize_date(my_data, ARRAY_SIZE(my_data));
struct spi_transfer multi_xfer[3];
struct spi_message single_msg;
int ret;
multi_xfer[0].rx_buf = data[0].buffer;
multi_xfer[0].len = 5;
multi_xfer[0].cs_change = 1;
/* command A */
multi_xfer[1].tx_buf = data[1].cmd;
multi_xfer[1].len = 2;
multi_xfer[1].cs_change = 1;
/* command B */
multi_xfer[2].rx_buf = data[2].buffer;
multi_xfer[2].len = 10;
spi_message_init(single_msg);
spi_message_add_tail(&multi_xfer[0], &single_msg);
spi_message_add_tail(&multi_xfer[1], &single_msg);
spi_message_add_tail(&multi_xfer[2], &single_msg);
ret = spi_sync(spi, &single_msg);
还有其他辅助函数,都围绕着spi_sync()构建。其中一些是:
int spi_read(struct spi_device *spi, void *buf, size_t len)
int spi_write(struct spi_device *spi, const void *buf, size_t len)
int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx)
请查看include/linux/spi/spi.h以查看完整列表。这些包装器应该与少量数据一起使用。
把所有东西放在一起
编写 SPI 客户端驱动程序所需的步骤如下:
-
声明驱动程序支持的设备 ID。您可以使用
spi_device_id来做到这一点。如果支持 DT,也使用of_device_id。您可以完全使用 DT。 -
调用
MODULE_DEVICE_TABLE(spi, my_id_table);将设备列表注册到 SPI 核心。如果支持 DT,必须调用MODULE_DEVICE_TABLE(of, your_of_match_table);将设备列表注册到of核心。 -
根据各自的原型编写
probe和remove函数。probe函数必须识别您的设备,配置它,定义每个设备(私有)数据,如果需要配置总线(SPI 模式等),则使用spi_setup函数,并向适当的内核框架注册。在remove函数中,只需撤消probe函数中完成的所有操作。 -
声明并填充
struct spi_driver结构,使用您创建的 ID 数组设置id_table字段。使用您编写的相应函数的名称设置.probe和.remove字段。在.driver子结构中,将.owner字段设置为THIS_MODULE,设置驱动程序名称,最后使用of_device_id数组设置.of_match_table字段,如果支持 DT。 -
在
module_spi_driver(serial_eeprom_spi_driver);之前,使用您刚刚填充的spi_driver结构调用module_spi_driver函数,以便向内核注册您的驱动程序。
SPI 用户模式驱动程序
有两种使用用户模式 SPI 设备驱动程序的方法。为了能够这样做,您需要使用spidev驱动程序启用您的设备。一个示例如下:
spidev@0x00 {
compatible = "spidev";
spi-max-frequency = <800000>; /* It depends on your device */
reg = <0>; /* correspond tochipselect 0 */
};
您可以调用读/写函数或ioctl()。通过调用读/写,您一次只能读取或写入。如果需要全双工读写,您必须使用输入输出控制(ioctl)命令。提供了两种的示例。这是读/写的示例。您可以使用平台的交叉编译器或板上的本地编译器进行编译:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int i,fd;
char wr_buf[]={0xff,0x00,0x1f,0x0f};
char rd_buf[10];
if (argc<2) {
printf("Usage:\n%s [device]\n", argv[0]);
exit(1);
}
fd = open(argv[1], O_RDWR);
if (fd<=0) {
printf("Failed to open SPI device %s\n",argv[1]);
exit(1);
}
if (write(fd, wr_buf, sizeof(wr_buf)) != sizeof(wr_buf))
perror("Write Error");
if (read(fd, rd_buf, sizeof(rd_buf)) != sizeof(rd_buf))
perror("Read Error");
else
for (i = 0; i < sizeof(rd_buf); i++)
printf("0x%02X ", rd_buf[i]);
close(fd);
return 0;
}
使用 IOCTL
使用 IOCTL 的优势在于您可以进行全双工工作。您可以在内核源树中的documentation/spi/spidev_test.c中找到最好的示例。
也就是说,前面使用读/写的示例并没有改变任何 SPI 配置。然而,内核向用户空间公开了一组 IOCTL 命令,您可以使用这些命令来根据需要设置总线,就像在 DT 中所做的那样。以下示例显示了如何更改总线设置:
#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
static int pabort(const char *s)
{
perror(s);
return -1;
}
static int spi_device_setup(int fd)
{
int mode, speed, a, b, i;
int bits = 8;
/*
* spi mode: mode 0
*/
mode = SPI_MODE_0;
a = ioctl(fd, SPI_IOC_WR_MODE, &mode); /* write mode */
b = ioctl(fd, SPI_IOC_RD_MODE, &mode); /* read mode */
if ((a < 0) || (b < 0)) {
return pabort("can't set spi mode");
}
/*
* Clock max speed in Hz
*/
speed = 8000000; /* 8 MHz */
a = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); /* Write speed */
b = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); /* Read speed */
if ((a < 0) || (b < 0)) {
return pabort("fail to set max speed hz");
}
/*
* setting SPI to MSB first.
* Here, 0 means "not to use LSB first".
* In order to use LSB first, argument should be > 0
*/
i = 0;
a = ioctl(dev, SPI_IOC_WR_LSB_FIRST, &i);
b = ioctl(dev, SPI_IOC_RD_LSB_FIRST, &i);
if ((a < 0) || (b < 0)) {
pabort("Fail to set MSB first\n");
}
/*
* setting SPI to 8 bits per word
*/
bits = 8;
a = ioctl(dev, SPI_IOC_WR_BITS_PER_WORD, &bits);
b = ioctl(dev, SPI_IOC_RD_BITS_PER_WORD, &bits);
if ((a < 0) || (b < 0)) {
pabort("Fail to set bits per word\n");
}
return 0;
}
您可以查看Documentation/spi/spidev以获取有关 spidev ioctl 命令的更多信息。在发送数据到总线时,您可以使用SPI_IOC_MESSAGE(N)请求,它提供了全双工访问和复合操作,而无需取消芯片选择,从而提供了多传输支持。这相当于内核的spi_sync()。这里,一个传输被表示为struct spi_ioc_transfer的实例,它相当于内核的struct spi_transfer,其定义可以在include/uapi/linux/spi/spidev.h中找到。以下是一个使用示例:
static void do_transfer(int fd)
{
int ret;
char txbuf[] = {0x0B, 0x02, 0xB5};
char rxbuf[3] = {0, };
char cmd_buff = 0x9f;
struct spi_ioc_transfer tr[2] = {
0 = {
.tx_buf = (unsigned long)&cmd_buff,
.len = 1,
.cs_change = 1; /* We need CS to change */
.delay_usecs = 50, /* wait after this transfer */
.bits_per_word = 8,
},
[1] = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = txbuf(tx),
.bits_per_word = 8,
},
};
ret = ioctl(fd, SPI_IOC_MESSAGE(2), &tr);
if (ret == 1){
perror("can't send spi message");
exit(1);
}
for (ret = 0; ret < sizeof(tx); ret++)
printf("%.2X ", rx[ret]);
printf("\n");
}
int main(int argc, char **argv)
{
char *device = "/dev/spidev0.0";
int fd;
int error;
fd = open(device, O_RDWR);
if (fd < 0)
return pabort("Can't open device ");
error = spi_device_setup(fd);
if (error)
exit (1);
do_transfer(fd);
close(fd);
return 0;
}
总结
我们刚刚处理了 SPI 驱动程序,现在可以利用这个更快的串行(和全双工)总线。我们讨论了 SPI 上的数据传输,这是最重要的部分。也就是说,您可能需要更多的抽象,以便不必理会 SPI 或 I2C 的 API。这就是下一章的内容,介绍了 Regmap API,它提供了更高和统一的抽象级别,使得 SPI(或 I2C)命令对您来说变得透明。
第九章:Regmap API - 寄存器映射抽象
在开发 regmap API 之前,处理 SPI 核心、I2C 核心或两者的设备驱动程序存在冗余代码。原则是相同的;访问寄存器进行读/写操作。以下图显示了在 Regmap 引入内核之前,SPI 或 I2C API 是如何独立存在的:

regmap 之前的 SPI 和 I2C 子系统
regmap API 是在内核的 3.1 版本中引入的,以因式分解和统一内核开发人员访问 SPI/I2C 设备的方式。然后只是如何初始化、配置 regmap,并流畅地处理任何读/写/修改操作,无论是 SPI 还是 I2C:

regmap 之后的 SPI 和 I2C 子系统
本章将通过以下方式介绍 regmap 框架:
-
介绍了 regmap 框架中使用的主要数据结构
-
通过 regmap 配置进行漫游
-
使用 regmap API 访问设备
-
介绍 regmap 缓存系统
-
提供一个总结先前学习的概念的完整驱动程序
使用 regmap API 进行编程
regmap API 非常简单。只有少数结构需要了解。此 API 的两个最重要的结构是struct regmap_config,它表示 regmap 的配置,以及struct regmap,它是 regmap 实例本身。所有 regmap 数据结构都在include/linux/regmap.h中定义。
regmap_config 结构
struct regmap_config在驱动程序的生命周期内存储 regmap 的配置。您在这里设置的内容会影响读/写操作。这是 regmap API 中最重要的结构。源代码如下:
struct regmap_config {
const char *name;
int reg_bits;
int reg_stride;
int pad_bits;
int val_bits;
bool (*writeable_reg)(struct device *dev, unsigned int reg);
bool (*readable_reg)(struct device *dev, unsigned int reg);
bool (*volatile_reg)(struct device *dev, unsigned int reg);
bool (*precious_reg)(struct device *dev, unsigned int reg);
regmap_lock lock;
regmap_unlock unlock;
void *lock_arg;
int (*reg_read)(void *context, unsigned int reg,
unsigned int *val);
int (*reg_write)(void *context, unsigned int reg,
unsigned int val);
bool fast_io;
unsigned int max_register;
const struct regmap_access_table *wr_table;
const struct regmap_access_table *rd_table;
const struct regmap_access_table *volatile_table;
const struct regmap_access_table *precious_table;
const struct reg_default *reg_defaults;
unsigned int num_reg_defaults;
enum regcache_type cache_type;
const void *reg_defaults_raw;
unsigned int num_reg_defaults_raw;
u8 read_flag_mask;
u8 write_flag_mask;
bool use_single_rw;
bool can_multi_write;
enum regmap_endian reg_format_endian;
enum regmap_endian val_format_endian;
const struct regmap_range_cfg *ranges;
unsigned int num_ranges;
}
-
reg_bits:这是寄存器地址中的位数,是强制性字段。 -
val_bits:表示用于存储寄存器值的位数。这是一个强制性字段。 -
writeable_reg:这是一个可选的回调函数。如果提供,当需要写入寄存器时,regmap 子系统将使用它。在写入寄存器之前,将自动调用此函数以检查寄存器是否可以写入:
static bool foo_writeable_register(struct device *dev,
unsigned int reg)
{
switch (reg) {
case 0x30 ... 0x38:
case 0x40 ... 0x45:
case 0x50 ... 0x57:
case 0x60 ... 0x6e:
case 0x70 ... 0x75:
case 0x80 ... 0x85:
case 0x90 ... 0x95:
case 0xa0 ... 0xa5:
case 0xb0 ... 0xb2:
return true;
default:
return false;
}
}
-
readable_reg:与writeable_reg相同,但用于每个寄存器读取操作。 -
volatile_reg:这是一个回调函数,每当需要通过 regmap 缓存读取或写入寄存器时都会调用。如果寄存器是易失性的,则函数应返回 true。然后对寄存器执行直接读/写。如果返回 false,则表示寄存器是可缓存的。在这种情况下,将使用缓存进行读取操作,并在写入操作的情况下写入缓存:
static bool foo_volatile_register(struct device *dev,
unsigned int reg)
{
switch (reg) {
case 0x24 ... 0x29:
case 0xb6 ... 0xb8:
return true;
default:
return false;
}
}
-
wr_table:可以提供writeable_reg回调,也可以提供regmap_access_table,它是一个包含yes_range和no_range字段的结构,都指向struct regmap_range。属于yes_range条目的任何寄存器都被视为可写,并且如果属于no_range,则被视为不可写。 -
rd_table:与wr_table相同,但用于任何读取操作。 -
volatile_table:可以提供volatile_reg,也可以提供volatile_table。原则与wr_table或rd_table相同,但用于缓存机制。 -
max_register:这是可选的,它指定了最大有效寄存器地址,超过该地址将不允许任何操作。 -
reg_read:您的设备可能不支持简单的 I2C/SPI 读取操作。那么您别无选择,只能编写自己定制的读取函数。reg_read应指向该函数。也就是说,大多数设备不需要。 -
reg_write:与reg_read相同,但用于写操作。
我强烈建议您查看include/linux/regmap.h以获取有关每个元素的更多详细信息。
以下是regmap_config的一种初始化方式:
static const struct regmap_config regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = LM3533_REG_MAX,
.readable_reg = lm3533_readable_register,
.volatile_reg = lm3533_volatile_register,
.precious_reg = lm3533_precious_register,
};
regmap 初始化
正如我们之前所说,regmap API 支持 SPI 和 I2C 协议。根据驱动程序中需要支持的协议,您将需要在probe函数中调用regmap_init_i2c()或regmap_init_sp()。要编写通用驱动程序,regmap 是最佳选择。
regmap API 是通用和同质的。只有初始化在总线类型之间变化。其他函数都是一样的。
在probe函数中始终初始化 regmap 是一个良好的实践,必须在初始化 regmap 之前始终填充regmap_config元素。
无论是分配了 I2C 还是 SPI 寄存器映射,都可以使用regmap_exit函数释放它:
void regmap_exit(struct regmap *map)
此函数只是释放先前分配的寄存器映射。
SPI 初始化
Regmap SPI 初始化包括设置 regmap,以便任何设备访问都会在内部转换为 SPI 命令。执行此操作的函数是regmap_init_spi()。
struct regmap * regmap_init_spi(struct spi_device *spi,
const struct regmap_config);
它以一个有效的struct spi_device结构的指针作为参数,这是将要交互的 SPI 设备,以及代表 regmap 配置的struct regmap_config。此函数在成功时返回分配的 struct regmap 的指针,或者在错误时返回ERR_PTR()的值。
一个完整的例子如下:
static int foo_spi_probe(struct spi_device *client)
{
int err;
struct regmap *my_regmap;
struct regmap_config bmp085_regmap_config;
/* fill bmp085_regmap_config somewhere */
[...]
client->bits_per_word = 8;
my_regmap =
regmap_init_spi(client,&bmp085_regmap_config);
if (IS_ERR(my_regmap)) {
err = PTR_ERR(my_regmap);
dev_err(&client->dev, "Failed to init regmap: %d\n", err);
return err;
}
[...]
}
I2C 初始化
另一方面,I2C regmap 初始化包括在 regmap 配置上调用regmap_init_i2c(),这将配置 regmap,以便任何设备访问都在内部转换为 I2C 命令:
struct regmap * regmap_init_i2c(struct i2c_client *i2c,
const struct regmap_config);
该函数以struct i2c_client结构作为参数,这是将用于交互的 I2C 设备,以及代表 regmap 配置的指针struct regmap_config。此函数在成功时返回分配的struct regmap的指针,或者在错误时返回ERR_PTR()的值。
一个完整的例子是:
static int bar_i2c_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
struct my_struct * bar_struct;
struct regmap_config regmap_cfg;
/* fill regmap_cfgsome where */
[...]
bar_struct = kzalloc(&i2c->dev,
sizeof(*my_struct), GFP_KERNEL);
if (!bar_struct)
return -ENOMEM;
i2c_set_clientdata(i2c, bar_struct);
bar_struct->regmap = regmap_init_i2c(i2c,
®map_config);
if (IS_ERR(bar_struct->regmap))
return PTR_ERR(bar_struct->regmap);
bar_struct->dev = &i2c->dev;
bar_struct->irq = i2c->irq;
[...]
}
设备访问函数
该 API 处理数据解析、格式化和传输。在大多数情况下,使用regmap_read、regmap_write和regmap_update_bits执行设备访问。这些是在存储/从设备中获取数据时应该始终记住的三个最重要的函数。它们的原型分别是:
int regmap_read(struct regmap *map, unsigned int reg,
unsigned int *val);
int regmap_write(struct regmap *map, unsigned int reg,
unsigned int val);
int regmap_update_bits(struct regmap *map, unsigned int reg,
unsigned int mask, unsigned int val);
-
regmap_write:向设备写入数据。如果在regmap_config中设置了max_register,则将用它来检查您需要从中读取的寄存器地址是大于还是小于。如果传递的寄存器地址小于或等于max_register,则将执行写入操作;否则,regmap 核心将返回无效的 I/O 错误(-EIO)。紧接着,将调用writeable_reg回调。回调必须在进行下一步之前返回true。如果返回false,则返回-EIO并停止写操作。如果设置了wr_table而不是writeable_reg,则: -
如果寄存器地址位于
no_range中,则返回-EIO。 -
如果寄存器地址位于
yes_range中,则执行下一步。 -
如果寄存器地址既不在
yes_range也不在no_range中,则返回-EIO并终止操作。 -
如果
cache_type != REGCACHE_NONE,则启用缓存。在这种情况下,首先更新缓存条目,然后执行硬件写入;否则,执行无缓存操作。 -
如果提供了
reg_write回调,则将使用它执行写操作;否则,将执行通用的 regmap 写函数。 -
regmap_read:从设备中读取数据。它与regmap_write的工作方式完全相同,具有适当的数据结构(readable_reg和rd_table)。因此,如果提供了reg_read,则将使用它执行读取操作;否则,将执行通用的 remap 读取函数。
regmap_update_bits 函数
regmap_update_bits是一个三合一的函数。其原型如下:
int regmap_update_bits(struct regmap *map, unsigned int reg,
unsigned int mask, unsigned int val)
它在寄存器映射上执行读取/修改/写入循环。它是_regmap_update_bits的包装器,其形式如下:
static int _regmap_update_bits(struct regmap *map,
unsigned int reg, unsigned int mask,
unsigned int val, bool *change)
{
int ret;
unsigned int tmp, orig;
ret = _regmap_read(map, reg, &orig);
if (ret != 0)
return ret;
tmp = orig& ~mask;
tmp |= val & mask;
if (tmp != orig) {
ret = _regmap_write(map, reg, tmp);
*change = true;
} else {
*change = false;
}
return ret;
}
这样,您需要更新的位必须在mask中设置为1,并且相应的位应在val中设置为您需要给予它们的值。
例如,要将第一位和第三位设置为1,掩码应为0b00000101,值应为0bxxxxx1x1。要清除第七位,掩码必须为0b01000000,值应为0bx0xxxxxx,依此类推。
特殊的 regmap_multi_reg_write 函数
remap_multi_reg_write()函数的目的是向设备写入多个寄存器。其原型如下所示:
int regmap_multi_reg_write(struct regmap *map,
const struct reg_sequence *regs, int num_regs)
要了解如何使用该函数,您需要知道struct reg_sequence是什么:
/**
* Register/value pairs for sequences of writes with an optional delay in
* microseconds to be applied after each write.
*
* @reg: Register address.
* @def: Register value.
* @delay_us: Delay to be applied after the register write in microseconds
*/
struct reg_sequence {
unsigned int reg;
unsigned int def;
unsigned int delay_us;
};
这就是它的使用方式:
static const struct reg_sequence foo_default_regs[] = {
{ FOO_REG1, 0xB8 },
{ BAR_REG1, 0x00 },
{ FOO_BAR_REG1, 0x10 },
{ REG_INIT, 0x00 },
{ REG_POWER, 0x00 },
{ REG_BLABLA, 0x00 },
};
staticint probe ( ...)
{
[...]
ret = regmap_multi_reg_write(my_regmap, foo_default_regs,
ARRAY_SIZE(foo_default_regs));
[...]
}
其他设备访问函数
regmap_bulk_read()和regmap_bulk_write()用于从/向设备读取/写入多个寄存器。将它们与大块数据一起使用。
int regmap_bulk_read(struct regmap *map, unsigned int reg, void
*val, size_tval_count);
int regmap_bulk_write(struct regmap *map, unsigned int reg,
const void *val, size_t val_count);
随时查看内核源中的 regmap 头文件,了解您有哪些选择。
regmap 和缓存
显然,regmap 支持缓存。是否使用缓存系统取决于regmap_config中的cache_type字段的值。查看include/linux/regmap.h,接受的值为:
/* Anenum of all the supported cache types */
enum regcache_type {
REGCACHE_NONE,
REGCACHE_RBTREE,
REGCACHE_COMPRESSED,
REGCACHE_FLAT,
};
默认情况下,它设置为REGCACHE_NONE,表示缓存已禁用。其他值只是定义缓存应如何存储。
您的设备可能在某些寄存器中具有预定义的上电复位值。这些值可以存储在一个数组中,以便任何读操作都返回数组中包含的值。但是,任何写操作都会影响设备中的真实寄存器,并更新数组中的内容。这是一种我们可以使用的缓存,以加快对设备的访问速度。该数组是reg_defaults。它在源代码中的结构如下:
/**
* Default value for a register. We use an array of structs rather
* than a simple array as many modern devices have very sparse
* register maps.
*
* @reg: Register address.
* @def: Register default value.
*/
struct reg_default {
unsigned int reg;
unsigned int def;
};
如果将cache_type设置为 none,则将忽略reg_defaults。如果未设置default_reg但仍然启用缓存,则将为您创建相应的缓存结构。
使用起来非常简单。只需声明它并将其作为参数传递给regmap_config结构。让我们看看drivers/regulator/ltc3589.c中的LTC3589调节器驱动程序:
static const struct reg_default ltc3589_reg_defaults[] = {
{ LTC3589_SCR1, 0x00 },
{ LTC3589_OVEN, 0x00 },
{ LTC3589_SCR2, 0x00 },
{ LTC3589_VCCR, 0x00 },
{ LTC3589_B1DTV1, 0x19 },
{ LTC3589_B1DTV2, 0x19 },
{ LTC3589_VRRCR, 0xff },
{ LTC3589_B2DTV1, 0x19 },
{ LTC3589_B2DTV2, 0x19 },
{ LTC3589_B3DTV1, 0x19 },
{ LTC3589_B3DTV2, 0x19 },
{ LTC3589_L2DTV1, 0x19 },
{ LTC3589_L2DTV2, 0x19 },
};
static const struct regmap_config ltc3589_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.writeable_reg = ltc3589_writeable_reg,
.readable_reg = ltc3589_readable_reg,
.volatile_reg = ltc3589_volatile_reg,
.max_register = LTC3589_L2DTV2,
.reg_defaults = ltc3589_reg_defaults,
.num_reg_defaults = ARRAY_SIZE(ltc3589_reg_defaults),
.use_single_rw = true,
.cache_type = REGCACHE_RBTREE,
};
对数组中存在的任何寄存器进行任何读操作都会立即返回数组中的值。但是,写操作将在设备本身上执行,并更新数组中受影响的寄存器。这样,读取LTC3589_VRRCR寄存器将返回0xff;在该寄存器中写入任何值,它将更新数组中的条目,以便任何新的读操作将直接从缓存中返回最后写入的值。
将所有内容放在一起
执行以下步骤设置 regmap 子系统:
-
根据设备的特性设置一个
regmap_config结构。如果需要,设置寄存器范围,默认值,如果需要,cache_type等等。如果需要自定义读/写函数,请将它们传递给reg_read/reg_write字段。 -
在
probe函数中,使用regmap_init_i2c或regmap_init_spi分配一个 regmap,具体取决于总线:I2C 或 SPI。 -
每当您需要从寄存器中读取/写入时,请调用
remap_[read|write]函数。 -
当您完成对 regmap 的操作后,调用
regmap_exit来释放在probe中分配的寄存器映射。
一个 regmap 示例
为了实现我们的目标,让我们首先描述一个假的 SPI 设备,我们可以为其编写驱动程序:
-
8 位寄存器地址
-
8 位寄存器值
-
最大寄存器:0x80
-
写入掩码为 0x80
-
有效地址范围:
-
0x20 到 0x4F
-
0x60 到 0x7F
-
不需要自定义读/写函数。
以下是一个虚拟的骨架:
/* mandatory for regmap */
#include <linux/regmap.h>
/* Depending on your need you should include other files */
static struct private_struct
{
/* Feel free to add whatever you want here */
struct regmap *map;
int foo;
};
static const struct regmap_range wr_rd_range[] =
{
{
.range_min = 0x20,
.range_max = 0x4F,
},{
.range_min = 0x60,
.range_max = 0x7F
},
};
struct regmap_access_table drv_wr_table =
{
.yes_ranges = wr_rd_range,
.n_yes_ranges = ARRAY_SIZE(wr_rd_range),
};
struct regmap_access_table drv_rd_table =
{
.yes_ranges = wr_rd_range,
.n_yes_ranges = ARRAY_SIZE(wr_rd_range),
};
static bool writeable_reg(struct device *dev, unsigned int reg)
{
if (reg>= 0x20 &®<= 0x4F)
return true;
if (reg>= 0x60 &®<= 0x7F)
return true;
return false;
}
static bool readable_reg(struct device *dev, unsigned int reg)
{
if (reg>= 0x20 &®<= 0x4F)
return true;
if (reg>= 0x60 &®<= 0x7F)
return true;
return false;
}
static int my_spi_drv_probe(struct spi_device *dev)
{
struct regmap_config config;
struct custom_drv_private_struct *priv;
unsigned char data;
/* setup the regmap configuration */
memset(&config, 0, sizeof(config));
config.reg_bits = 8;
config.val_bits = 8;
config.write_flag_mask = 0x80;
config.max_register = 0x80;
config.fast_io = true;
config.writeable_reg = drv_writeable_reg;
config.readable_reg = drv_readable_reg;
/*
* If writeable_reg and readable_reg are set,
* there is no need to provide wr_table nor rd_table.
* Uncomment below code only if you do not want to use
* writeable_reg nor readable_reg.
*/
//config.wr_table = drv_wr_table;
//config.rd_table = drv_rd_table;
/* allocate the private data structures */
/* priv = kzalloc */
/* Init the regmap spi configuration */
priv->map = regmap_init_spi(dev, &config);
/* Use regmap_init_i2c in case of i2c bus */
/*
* Let us write into some register
* Keep in mind that, below operation will remain same
* whether you use SPI or I2C. It is and advantage when
* you use regmap.
*/
regmap_read(priv->map, 0x30, &data);
[...] /* Process data */
data = 0x24;
regmap_write(priv->map, 0x23, data); /* write new value */
/* set bit 2 (starting from 0) and 6 of register 0x44 */
regmap_update_bits(priv->map, 0x44, 0b00100010, 0xFF);
[...] /* Lot of stuff */
return 0;
}
总结
本章主要讲述了 regmap API。它有多么简单,让你了解了它有多么有用和广泛使用。本章告诉了你关于 regmap API 的一切你需要知道的东西。现在你应该能够将任何标准的 SPI/I2C 驱动程序转换成 regmap。下一章将涵盖 IIO 设备,这是一个用于模数转换器的框架。这些类型的设备总是位于 SPI/I2C 总线的顶部。在下一章结束时,使用 regmap API 编写 IIO 驱动程序将是一个挑战。
第十章:IIO 框架
工业 I/O(IIO)是一个专门用于模拟到数字转换器(ADC)和数字到模拟转换器(DAC)的内核子系统。随着不断增加的传感器(具有模拟到数字或数字到模拟能力的测量设备)以不同的代码实现分散在内核源代码中,对它们进行收集变得必要。这就是 IIO 框架以一种通用和统一的方式所做的。自 2009 年以来,Jonathan Cameron 和 Linux-IIO 社区一直在开发它。
加速度计、陀螺仪、电流/电压测量芯片、光传感器、压力传感器等都属于 IIO 设备系列。
IIO 模型基于设备和通道架构:
-
设备代表芯片本身。它是层次结构的最高级别。
-
通道表示设备的单个采集线。一个设备可能有一个或多个通道。例如,加速度计是一个具有三个通道的设备,分别用于每个轴(X、Y 和 Z)。
IIO 芯片是物理和硬件传感器/���换器。它以字符设备(当支持触发缓冲时)和一个sysfs目录条目暴露给用户空间,该目录将包含一组文件,其中一些表示通道。单个通道用单个sysfs文件条目表示。
这是从用户空间与 IIO 驱动程序交互的两种方式:
-
/sys/bus/iio/iio:deviceX/:这代表传感器以及其通道 -
/dev/iio:deviceX:这是一个字符设备,用于导出设备的事件和数据缓冲区

IIO 框架的架构和布局
前面的图显示了 IIO 框架在内核和用户空间之间的组织方式。驱动程序管理硬件并将处理报告给 IIO 核心,使用 IIO 核心提供的一组设施和 API。然后,IIO 子系统通过 sysfs 接口和字符设备将整个底层机制抽象到用户空间,用户可以在其上执行系统调用。
IIO API 分布在几个头文件中,列举如下:
#include <linux/iio/iio.h> /* mandatory */
#include <linux/iio/sysfs.h> /* mandatory since sysfs is used */
#include <linux/iio/events.h> /* For advanced users, to manage iio events */
#include <linux/iio/buffer.h> /* mandatory to use triggered buffers */
#include <linux/iio/trigger.h>/* Only if you implement trigger in your driver (rarely used)*/
在本章中,我们将描述和处理 IIO 框架的每个概念,比如
-
遍历其数据结构(设备、通道等)
-
触发缓冲区支持和连续捕获,以及其 sysfs 接口
-
探索现有的 IIO 触发器
-
以单次模式或连续模式捕获数据
-
列出可用的工具,可以帮助开发人员测试他们的设备
IIO 数据结构
IIO 设备在内核中表示为struct iio_dev的实例,并由struct iio_info结构描述。所有重要的 IIO 结构都在include/linux/iio/iio.h中定义。
iio_dev 结构
这个结构表示 IIO 设备,描述设备和驱动程序。它告诉我们关于:
-
设备上有多少个通道可用?
-
设备可以以哪些模式操作:单次、触发缓冲?
-
这个驱动程序有哪些可用的钩子?
struct iio_dev {
[...]
int modes;
int currentmode;
struct device dev;
struct iio_buffer *buffer;
int scan_bytes;
const unsigned long *available_scan_masks;
const unsigned long *active_scan_mask;
bool scan_timestamp;
struct iio_trigger *trig;
struct iio_poll_func *pollfunc;
struct iio_chan_spec const *channels;
int num_channels;
const char *name;
const struct iio_info *info;
const struct iio_buffer_setup_ops *setup_ops;
struct cdev chrdev;
};
完整的结构在 IIO 头文件中定义。这里删除了我们不感兴趣的字段。
-
modes:这代表设备支持的不同模式。支持的模式有: -
INDIO_DIRECT_MODE表示设备提供 sysfs 类型的接口。 -
INDIO_BUFFER_TRIGGERED表示设备支持硬件触发。当您使用iio_triggered_buffer_setup()函数设置触发缓冲区时,此模式会自动添加到您的设备中。 -
INDIO_BUFFER_HARDWARE显示设备具有硬件缓冲区。 -
INDIO_ALL_BUFFER_MODES是上述两者的并集。 -
currentmode:这代表设备实际使用的模式。 -
dev:这代表了 IIO 设备绑定的 struct device(根据 Linux 设备模型)。 -
buffer:这是您的数据缓冲区,在使用触发缓冲区模式时推送到用户空间。当使用iio_triggered_buffer_setup函数启用触发缓冲区支持时,它会自动分配并与您的设备关联。 -
scan_bytes:这是捕获并馈送到buffer的字节数。当从用户空间使用触发缓冲区时,缓冲区应至少为indio->scan_bytes字节大。 -
available_scan_masks:这是允许的位掩码的可选数组。在使用触发缓冲区时,可以启用通道以被捕获并馈送到 IIO 缓冲区中。如果不希望允许某些通道被启用,应该只填充此数组。以下是为加速度计提供扫描掩码的示例(具有 X、Y 和 Z 通道):
/*
* Bitmasks 0x7 (0b111) and 0 (0b000) are allowed.
* It means one can enable none or all of them.
* one can't for example enable only channel X and Y
*/
static const unsigned long my_scan_masks[] = {0x7, 0};
indio_dev->available_scan_masks = my_scan_masks;
-
active_scan_mask:这是启用通道的位掩码。只有这些通道的数据应该被推送到buffer中。例如,对于 8 通道 ADC 转换器,如果只启用第一个(0)、第三个(2)和最后一个(7)通道,位掩码将是 0b10000101(0x85)。active_scan_mask将设置为 0x85。然后驱动程序可以使用for_each_set_bit宏来遍历每个设置的位,根据通道获取数据,并填充缓冲区。 -
scan_timestamp:这告诉我们是否将捕获时间戳推送到缓冲区。如果为 true,则时间戳将作为缓冲区的最后一个元素推送。时间戳为 8 字节(64 位)。 -
trig:这是当前设备的触发器(当支持缓冲模式时)。 -
pollfunc:这是在接收到触发器时运行的函数。 -
channels:这代表通道规范结构表,描述设备具有的每个通道。 -
num_channels:这代表在channels中指定的通道数。 -
name:这代表设备名称。 -
info:来自驱动程序的回调和常量信息。 -
setup_ops:在启用/禁用缓冲区之前和之后调用的回调函数集。此结构在include/linux/iio/iio.h中定义如下:
struct iio_buffer_setup_ops {
int (* preenable) (struct iio_dev *);
int (* postenable) (struct iio_dev *);
int (* predisable) (struct iio_dev *);
int (* postdisable) (struct iio_dev *);
bool (* validate_scan_mask) (struct iio_dev *indio_dev,
const unsigned long *scan_mask);
};
-
setup_ops:如果未指定,IIO 核心将使用在drivers/iio/buffer/industrialio-triggered-buffer.c中定义的默认iio_triggered_buffer_setup_ops。 -
chrdev:这是由 IIO 核心创建的关联字符设备。
用于为 IIO 设备分配内存的函数是iio_device_alloc():
struct iio_dev *devm_iio_device_alloc(struct device *dev,
int sizeof_priv)
dev是为其分配iio_dev的设备,sizeof_priv是用于分配任何私有结构的内存空间。通过这种方式,传递每个设备(私有)数据结构非常简单。如果分配失败,该函数将返回NULL:
struct iio_dev *indio_dev;
struct my_private_data *data;
indio_dev = iio_device_alloc(sizeof(*data));
if (!indio_dev)
return -ENOMEM;
/*data is given the address of reserved momory for private data */
data = iio_priv(indio_dev);
分配了 IIO 设备内存后,下一步是填充不同的字段。完成后,必须使用iio_device_register函数向 IIO 子系统注册设备:
int iio_device_register(struct iio_dev *indio_dev)
此函数执行后,设备将准备好接受来自用户空间的请求。反向操作(通常在释放函数中完成)是iio_device_unregister():
void iio_device_unregister(struct iio_dev *indio_dev)
一旦注销,由iio_device_alloc分配的内存可以使用iio_device_free释放:
void iio_device_free(struct iio_dev *iio_dev)
给定一个 IIO 设备作为参数,可以以以下方式检索私有数据:
struct my_private_data *the_data = iio_priv(indio_dev);
iio_info 结构
struct iio_info结构用于声明 IIO 核心用于读取/写入通道/属性值的钩子:
struct iio_info {
struct module *driver_module;
const struct attribute_group *attrs;
int (*read_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask);
int (*write_raw)(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask);
[...]
};
我们不感兴趣的字段已被移除。
-
driver_module:这是用于确保chrdevs正确拥有权的模块结构,通常设置为THIS_MODULE。 -
attrs:这代表设备的属性。 -
read_raw:这是当用户读取设备sysfs文件属性时运行的回调。mask参数是一个位掩码,允许我们知道请求的是哪种类型的值。channel参数让我们知道所关注的通道。它可以用于采样频率、用于将原始值转换为可用值的比例,或者原始值本身。 -
write_raw:这是用于向设备写入值的回调。例如,可以使用它来设置采样频率。
以下代码显示了如何设置struct iio_info结构:
static const struct iio_info iio_dummy_info = {
.driver_module = THIS_MODULE,
.read_raw = &iio_dummy_read_raw,
.write_raw = &iio_dummy_write_raw,
[...]
/*
* Provide device type specific interface functions and
* constant data.
*/
indio_dev->info = &iio_dummy_info;
IIO 通道
通道表示单个采集线。例如,加速度计将有 3 个通道(X、Y、Z),因为每个轴代表单个采集线。struct iio_chan_spec是在内核中表示和描述单个通道的结构:
struct iio_chan_spec {
enum iio_chan_type type;
int channel;
int channel2;
unsigned long address;
int scan_index;
struct {
charsign;
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type;
long info_mask_separate;
long info_mask_shared_by_type;
long info_mask_shared_by_dir;
long info_mask_shared_by_all;
const struct iio_event_spec *event_spec;
unsigned int num_event_specs;
const struct iio_chan_spec_ext_info *ext_info;
const char *extend_name;
const char *datasheet_name;
unsigned modified:1;
unsigned indexed:1;
unsigned output:1;
unsigned differential:1;
};
以下是结构中每个元素的含义:
-
类型:这指定了通道进行何种类型的测量。在电压测量的情况下,应该是IIO_VOLTAGE。对于光传感器,是IIO_LIGHT。对于加速度计,使用IIO_ACCEL。所有可用类型都在include/uapi/linux/iio/types.h中定义为enum iio_chan_type。要为给定的转换器编写驱动程序,请查看该文件,以查看每个通道所属的类型。 -
通道:当.indexed设置为 1 时,这指定了通道索引。 -
channel2:当.modified设置为 1 时,这指定了通道修饰符�� -
修改:这指定了是否要对该通道属性名称应用修饰符。在这种情况下,修饰符设置为.channel2。(例如,IIO_MOD_X,IIO_MOD_Y,IIO_MOD_Z是关于 xyz 轴的轴向传感器的修饰符)。可用的修饰符列表在内核 IIO 头文件中定义为enum iio_modifier。修饰符只会对sysfs中的通道属性名称进行操作,而不会对值进行操作。 -
indexed:这指定了通道属性名称是否具有索引。如果是,则索引在.channel字段中指定。 -
scan_index和scan_type:这些字段用于在使用缓冲区触发器时识别缓冲区中的元素。scan_index设置了缓冲区中捕获的通道的位置。具有较低scan_index的通道将放置在具有较高索引的通道之前。将.scan_index设置为-1将阻止通道进行缓冲捕获(在scan_elements目录中没有条目)。
向用户空间公开的通道 sysfs 属性以位掩码的形式指定。根据它们的共享信息,属性可以设置为以下掩码之一:
-
info_mask_separate将属性标记为特定于此通道。 -
info_mask_shared_by_type将属性标记为所有相同类型的通道共享的属性。导出的信息由所有相同类型的通道共享。 -
info_mask_shared_by_dir将属性标记为所有相同方向的通道共享的属性。导出的信息由相同方向的所有通道共享。 -
info_mask_shared_by_all将属性标记为所有通道共享的属性,无论它们的类型或方向如何。导出的信息由所有通道共享。这些属性的枚举位掩码都在include/linux/iio/iio.h中定义。
enum iio_chan_info_enum {
IIO_CHAN_INFO_RAW = 0,
IIO_CHAN_INFO_PROCESSED,
IIO_CHAN_INFO_SCALE,
IIO_CHAN_INFO_OFFSET,
IIO_CHAN_INFO_CALIBSCALE,
[...]
IIO_CHAN_INFO_SAMP_FREQ,
IIO_CHAN_INFO_FREQUENCY,
IIO_CHAN_INFO_PHASE,
IIO_CHAN_INFO_HARDWAREGAIN,
IIO_CHAN_INFO_HYSTERESIS,
[...]
};
字节顺序字段应为以下之一:
enum iio_endian {
IIO_CPU,
IIO_BE,
IIO_LE,
};
通道属性命名约定
属性的名称由 IIO 核心自动生成,遵循以下模式:{direction}_{type}_{index}_{modifier}_{info_mask}:
方向对应于属性方向,根据drivers/iio/industrialio-core.c中的struct iio_direction结构:
static const char * const iio_direction[] = {
[0] = "in",
[1] = "out",
};
类型对应于通道类型,根据字符数组const iio_chan_type_name_spec:
static const char * const iio_chan_type_name_spec[] = {
[IIO_VOLTAGE] = "voltage",
[IIO_CURRENT] = "current",
[IIO_POWER] = "power",
[IIO_ACCEL] = "accel",
[...]
[IIO_UVINDEX] = "uvindex",
[IIO_ELECTRICALCONDUCTIVITY] = "electricalconductivity",
[IIO_COUNT] = "count",
[IIO_INDEX] = "index",
[IIO_GRAVITY] = "gravity",
};
-
index模式取决于通道.indexed字段是否设置。如果设置,索引将从.channel字段中取出,以替换{index}模式。 -
modifier模式取决于通道.modified字段是否设置。如果设置,修饰符将从.channel2字段中取出,并且{modifier}模式将根据struct iio_modifier_names结构中的字符数组进行替换:
static const char * const iio_modifier_names[] = {
[IIO_MOD_X] = "x",
[IIO_MOD_Y] = "y",
[IIO_MOD_Z] = "z",
[IIO_MOD_X_AND_Y] = "x&y",
[IIO_MOD_X_AND_Z] = "x&z",
[IIO_MOD_Y_AND_Z] = "y&z",
[...]
[IIO_MOD_CO2] = "co2",
[IIO_MOD_VOC] = "voc",
};
info_mask取决于通道信息掩码,私有或共享,字符数组iio_chan_info_postfix中的索引值:
/* relies on pairs of these shared then separate */
static const char * const iio_chan_info_postfix[] = {
[IIO_CHAN_INFO_RAW] = "raw",
[IIO_CHAN_INFO_PROCESSED] = "input",
[IIO_CHAN_INFO_SCALE] = "scale",
[IIO_CHAN_INFO_CALIBBIAS] = "calibbias",
[...]
[IIO_CHAN_INFO_SAMP_FREQ] = "sampling_frequency",
[IIO_CHAN_INFO_FREQUENCY] = "frequency",
[...]
};
区分通道
当每个通道类型有多个数据通道时,您可能会陷入麻烦。两种解决方案是:索引和修饰符。
使用索引:给定一个具有一个通道线的 ADC 设备,不需要索引。它的通道定义将是:
static const struct iio_chan_spec adc_channels[] = {
{
.type = IIO_VOLTAGE,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}
由前述通道描述产生的属性名称将是in_voltage_raw。
/sys/bus/iio/iio:deviceX/in_voltage_raw
现在假设转换器有 4 个甚至 8 个通道。我们如何识别它们?解决方案是使用索引。将.indexed字段设置为 1 将使用.channel值替换{index}模式来搅乱通道属性名称:
static const struct iio_chan_spec adc_channels[] = {
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 0,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 2,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 3,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
}
结果通道属性是:
/sys/bus/iio/iio:deviceX/in_voltage0_raw
/sys/bus/iio/iio:deviceX/in_voltage1_raw
/sys/bus/iio/iio:deviceX/in_voltage2_raw
/sys/bus/iio/iio:deviceX/in_voltage3_raw
使用修饰符:给定一个具有两个通道的光传感器——一个用于红外光,一个用于红外和可见光,没有索引或修饰符,属性名称将是in_intensity_raw。在这里使用索引可能会出错,因为in_intensity0_ir_raw和in_intensity1_ir_raw是没有意义的。使用修饰符将有助于提供有意义的属性名称。通道的定义可能如下所示:
static const struct iio_chan_spec mylight_channels[] = {
{
.type = IIO_INTENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_IR,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_INTENSITY,
.modified = 1,
.channel2 = IIO_MOD_LIGHT_BOTH,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
{
.type = IIO_LIGHT,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
.info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ),
},
}
结果属性将是:
-
/sys/bus/iio/iio:deviceX/in_intensity_ir_raw用于测量红外强度的通道 -
/sys/bus/iio/iio:deviceX/in_intensity_both_raw用于测量红外和可见光的通道 -
/sys/bus/iio/iio:deviceX/in_illuminance_input用于处理后的数据 -
/sys/bus/iio/iio:deviceX/sampling_frequency用于所有共享的采样频率
这对于加速度计也有效,正如我们将在案例研究中看到的那样。现在,让我们总结一下到目前为止在虚拟 IIO 驱动程序中讨论的内容。
把所有东西放在一起
让我们总结一下到目前为止在一个简单的虚拟驱动程序中看到的内容,它将公开四个电压通道。我们将忽略read()或write()函数:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/events.h>
#include <linux/iio/buffer.h>
#define FAKE_VOLTAGE_CHANNEL(num) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
.channel = (num), \
.address = (num), \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) \
}
struct my_private_data {
int foo;
int bar;
struct mutex lock;
};
static int fake_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *channel, int *val,
int *val2, long mask)
{
return 0;
}
static int fake_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
return 0;
}
static const struct iio_chan_spec fake_channels[] = {
FAKE_VOLTAGE_CHANNEL(0),
FAKE_VOLTAGE_CHANNEL(1),
FAKE_VOLTAGE_CHANNEL(2),
FAKE_VOLTAGE_CHANNEL(3),
};
static const struct of_device_id iio_dummy_ids[] = {
{ .compatible = "packt,iio-dummy-random", },
{ /* sentinel */ }
};
static const struct iio_info fake_iio_info = {
.read_raw = fake_read_raw,
.write_raw = fake_write_raw,
.driver_module = THIS_MODULE,
};
static int my_pdrv_probe (struct platform_device *pdev)
{
struct iio_dev *indio_dev;
struct my_private_data *data;
indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*data));
if (!indio_dev) {
dev_err(&pdev->dev, "iio allocation failed!\n");
return -ENOMEM;
}
data = iio_priv(indio_dev);
mutex_init(&data->lock);
indio_dev->dev.parent = &pdev->dev;
indio_dev->info = &fake_iio_info;
indio_dev->name = KBUILD_MODNAME;
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->channels = fake_channels;
indio_dev->num_channels = ARRAY_SIZE(fake_channels);
indio_dev->available_scan_masks = 0xF;
iio_device_register(indio_dev);
platform_set_drvdata(pdev, indio_dev);
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
struct iio_dev *indio_dev = platform_get_drvdata(pdev);
iio_device_unregister(indio_dev);
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "iio-dummy-random",
.of_match_table = of_match_ptr(iio_dummy_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
加载上述模块后,我们将得到以下输出,显示我们的设备确实对应于我们注册的平台设备:
~# ls -l /sys/bus/iio/devices/
lrwxrwxrwx 1 root root 0 Jul 31 20:26 iio:device0 -> ../../../devices/platform/iio-dummy-random.0/iio:device0
lrwxrwxrwx 1 root root 0 Jul 31 20:23 iio_sysfs_trigger -> ../../../devices/iio_sysfs_trigger
以下清单显示了此设备具有的通道及其名称,这些名称与驱动程序中描述的完全相对应:
~# ls /sys/bus/iio/devices/iio\:device0/
dev in_voltage2_raw name uevent
in_voltage0_raw in_voltage3_raw power
in_voltage1_raw in_voltage_scale subsystem
~# cat /sys/bus/iio/devices/iio:device0/name
iio_dummy_random
触发缓冲区支持
在许多数据分析应用程序中,根据某些外部信号(触发器)捕获数据是有用的。这些触发器可能是:
-
数据准备信号
-
连接到某些外部系统的 IRQ 线(GPIO 或其他)
-
处理器周期性中断
-
用户空间读/写 sysfs 中的特定文件
IIO 设备驱动程序与触发器完全无关。触发器可以初始化一个或多个设备上的数据捕获。这些触发器用于填充缓冲区,向用户空间公开为字符设备。
可以开发自己的触发器驱动程序,但这超出了本书的范围。我们将尝试仅专注于现有的触发器。这些是:
-
iio-trig-interrupt:这提供了使用任何 IRQ 作为 IIO 触发器的支持。在旧的内核版本中,它曾经是iio-trig-gpio。启用此触发模式的内核选项是CONFIG_IIO_INTERRUPT_TRIGGER。如果构建为模块,该模块将被称为iio-trig-interrupt。 -
iio-trig-hrtimer:这提供了使用 HRT 作为中断源的基于频率的 IIO 触发器(自内核 v4.5 以来)。在较旧的内核版本中,它曾经是iio-trig-rtc。负责此触发模式的内核选项是IIO_HRTIMER_TRIGGER。如果作为模块构建,该模块将被称为iio-trig-hrtimer。 -
iio-trig-sysfs:这允许我们使用 sysfs 条目触发数据捕获。CONFIG_IIO_SYSFS_TRIGGER是内核选项,用于添加对此触发模式的支持。 -
iio-trig-bfin-timer:这允许我们将黑脸定时器用作 IIO 触发器(仍在暂存中)。
IIO 公开 API,以便我们可以:
-
声明任意数量的触发器
-
选择哪些通道的数据将被推送到缓冲区
当您的 IIO 设备提供触发缓冲区的支持时,必须设置iio_dev.pollfunc,当触发器触发时执行。此处理程序负责通过indio_dev->active_scan_mask找到已启用的通道,检索其数据,并使用iio_push_to_buffers_with_timestamp函数将其馈送到indio_dev->buffer中。因此,在 IIO 子系统中,缓冲区和触发器是非常相关的。
IIO 核心提供了一组辅助函数,用于设置触发缓冲区,可以在drivers/iio/industrialio-triggered-buffer.c中找到。
以下是支持从驱动程序内部支持触发缓冲区的步骤:
- 如果需要,填写
iio_buffer_setup_ops结构:
const struct iio_buffer_setup_ops sensor_buffer_setup_ops = {
.preenable = my_sensor_buffer_preenable,
.postenable = my_sensor_buffer_postenable,
.postdisable = my_sensor_buffer_postdisable,
.predisable = my_sensor_buffer_predisable,
};
- 编写与触发器相关联的上半部分。在 99%的情况下,只需提供与捕获相关的时间戳:
irqreturn_t sensor_iio_pollfunc(int irq, void *p)
{
pf->timestamp = iio_get_time_ns((struct indio_dev *)p);
return IRQ_WAKE_THREAD;
}
- 编写触发器的下半部分,它将从每个启用的通道中获取数据,并将其馈送到缓冲区中:
irqreturn_t sensor_trigger_handler(int irq, void *p)
{
u16 buf[8];
int bit, i = 0;
struct iio_poll_func *pf = p;
struct iio_dev *indio_dev = pf->indio_dev;
/* one can use lock here to protect the buffer */
/* mutex_lock(&my_mutex); */
/* read data for each active channel */
for_each_set_bit(bit, indio_dev->active_scan_mask,
indio_dev->masklength)
buf[i++] = sensor_get_data(bit)
/*
* If iio_dev.scan_timestamp = true, the capture timestamp
* will be pushed and stored too, as the last element in the
* sample data buffer before pushing it to the device buffers.
*/
iio_push_to_buffers_with_timestamp(indio_dev, buf, timestamp);
/* Please unlock any lock */
/* mutex_unlock(&my_mutex); */
/* Notify trigger */
iio_trigger_notify_done(indio_dev->trig);
return IRQ_HANDLED;
}
- 最后,在
probe函数中,必须在使用iio_device_register()注册设备之前设置缓冲区本身:
iio_triggered_buffer_setup(indio_dev, sensor_iio_polfunc,
sensor_trigger_handler,
sensor_buffer_setup_ops);
这里的魔术函数是iio_triggered_buffer_setup。这也将为您的设备提供INDIO_DIRECT_MODE功能。当从用户空间给您的设备触发器时,您无法知道何时会触发捕获。
在连续缓冲捕获处于活动状态时,应该防止(通过返回错误)驱动程序执行 sysfs 每通道数据捕获(由read_raw()挂钩执行),以避免未确定的行为,因为触发处理程序和read_raw()挂钩将尝试同时访问设备。用于检查是否实际使用了缓冲模式的函数是iio_buffer_enabled()。挂钩将如下所示:
static int my_read_raw(struct iio_dev *indio_dev,
const struct iio_chan_spec *chan,
int *val, int *val2, long mask)
{
[...]
switch (mask) {
case IIO_CHAN_INFO_RAW:
if (iio_buffer_enabled(indio_dev))
return -EBUSY;
[...]
}
iio_buffer_enabled()函数只是测试给定 IIO 设备是否启用了缓冲区。
让我们描述一些在前面部分中使用的重要内容:
-
iio_buffer_setup_ops提供了在缓冲区配置序列的固定步骤(启用/禁用之前/之后)调用的缓冲区设置函数。如果未指定,默认的iio_triggered_buffer_setup_ops将由 IIO 核心提供给您的设备。 -
sensor_iio_pollfunc是触发器的顶半部分。与每个顶半部分一样,它在中断上下文中运行,并且必须尽可能少地进行处理。在 99%的情况下,您只需提供与捕获���关的时间戳。再次,可以使用默认的 IIOiio_pollfunc_store_time函数。 -
sensor_trigger_handler是底半部分,它在内核线程中运行,允许我们进行任何处理,甚至包括获取互斥锁或休眠。重要的处理应该在这里进行。它通常从设备中读取数据,并将其与顶半部分记录的时间戳一起存储在内部缓冲区中,并将其推送到您的 IIO 设备缓冲区中。
触发器对于触发缓冲是强制性的。它告诉驱动程序何时从设备中读取样本并将其放入缓冲区中。触发缓冲对于编写 IIO 设备驱动程序并非强制性。人们也可以通过 sysfs 进行单次捕获,方法是读取通道的原始属性,这将仅执行单次转换(对于正在读取的通道属性)。缓冲模式允许连续转换,因此可以在一次触发中捕获多个通道。
IIO 触发器和 sysfs(用户空间)
sysfs 中与触发器相关的两个位置:
-
/sys/bus/iio/devices/triggerY/一旦 IIO 触发器与 IIO 核心注册并对应于索引Y的触发器,将创建至少一个目录属性: -
name是触发器名称,稍后可以用于与设备关联 -
如果您的设备支持触发缓冲区,则将自动创建
/sys/bus/iio/devices/iio:deviceX/trigger/*目录。可以通过将触发器的名称写入current_trigger文件来将触发器与我们的设备关联。
Sysfs 触发器接口
通过CONFIG_IIO_SYSFS_TRIGGER=y配置选项在内核中启用 sysfs 触发器,将自动创建/sys/bus/iio/devices/iio_sysfs_trigger/文件夹,并可用于 sysfs 触发器管理。目录中将有两个文件,add_trigger和remove_trigger。其驱动程序位于drivers/iio/trigger/iio-trig-sysfs.c中。
add_trigger 文件
用于创建新的 sysfs 触发器。可以通过将正值(将用作触发器 ID)写入该文件来创建新的触发器。它将创建新的 sysfs 触发器,可在/sys/bus/iio/devices/triggerX访问,其中X是触发器编号。
例如:
# echo 2 > add_trigger
这将创建一个新的 sysfs 触发器,可在/sys/bus/iio/devices/trigger2访问。如果系统中已经存在指定 ID 的触发器,则会返回无效参数消息。sysfs 触发器名称模式为sysfstrig{ID}。命令echo 2 > add_trigger将创建触发器/sys/bus/iio/devices/trigger2,其名称为sysfstrig2:
$ cat /sys/bus/iio/devices/trigger2/name
sysfstrig2
每个 sysfs 触发器至少包含一个文件:trigger_now。将1写入该文件将指示所有具有其current_trigger中相应触发器名称的设备开始捕获,并将数据推送到各自的缓冲区中。每个设备缓冲区必须设置其大小,并且必须启用(echo 1 > /sys/bus/iio/devices/iio:deviceX/buffer/enable)。
remove_trigger 文件
要删除触发器,使用以下命令:
# echo 2 > remove_trigger
将设备与触发器绑定
将设备与给定触发器关联包括将触发器的名称写入设备触发器目录下的current_trigger文件。例如,假设我们需要将设备与索引为 2 的触发器绑定:
# set trigger2 as current trigger for device0
# echo sysfstrig2 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
要将触发器与设备分离,应将空字符串写入设备触发器目录的current_trigger文件,如下所示:
# echo "" > iio:device0/trigger/current_trigger
在本章中,我们将进一步看到有关数据捕获的 sysfs 触发器的实际示例。
中断触发器接口
考虑以下示例:
static struct resource iio_irq_trigger_resources[] = {
[0] = {
.start = IRQ_NR_FOR_YOUR_IRQ,
.flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWEDGE,
},
};
static struct platform_device iio_irq_trigger = {
.name = "iio_interrupt_trigger",
.num_resources = ARRAY_SIZE(iio_irq_trigger_resources),
.resource = iio_irq_trigger_resources,
};
platform_device_register(&iio_irq_trigger);
声明我们的 IRQ 触发器,将导致加载 IRQ 触发器独立模块。如果其probe函数成功,将会有一个与触发器对应的目录。IRQ 触发器名称的形式为irqtrigX,其中X对应于您刚刚传递的虚拟 IRQ,在/proc/interrupt中可以看到。
$ cd /sys/bus/iio/devices/trigger0/
$ cat name
irqtrig85:与其他触发器一样,您只需将该触发器分配给您的设备,方法是将其名称写入设备的current_trigger文件中。
# echo "irqtrig85" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
现在,每次触发中断时,设备数据将被捕获。
IRQ 触发器驱动程序尚不支持 DT,这就是为什么我们使用了我们的板init文件的原因。但这并不重要;由于驱动程序需要资源,因此我们可以在不进行任何代码更改的情况下使用 DT。
以下是声明 IRQ 触发接口的设备树节点示例:
mylabel: my_trigger@0{
compatible = "iio_interrupt_trigger";
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
};
该示例假设 IRQ 线是属于 GPIO 控制器节点gpio4的 GPIO#30。这包括使用 GPIO 作为中断源,因此每当 GPIO 变为给定状态时,中断就会被触发,从而触发捕获。
hrtimer 触发接口
hrtimer触发器依赖于 configfs 文件系统(请参阅内核源中的Documentation/iio/iio_configfs.txt),可以通过CONFIG_IIO_CONFIGFS配置选项启用,并挂载到我们的系统上(通常在/config目录下):
# mkdir /config
# mount -t configfs none /config
现在,加载模块iio-trig-hrtimer将创建可在/config/iio下访问的 IIO 组,允许用户在/config/iio/triggers/hrtimer下创建 hrtimer 触发器。
例如:
# create a hrtimer trigger
$ mkdir /config/iio/triggers/hrtimer/my_trigger_name
# remove the trigger
$ rmdir /config/iio/triggers/hrtimer/my_trigger_name
每个 hrtimer 触发器在触发目录中包含一个单独的sampling_frequency属性。在本章的使用 hrtimer 触发进行数据捕获部分中提供了一个完整且可用的示例。
IIO 缓冲区
IIO 缓冲区提供连续数据捕获,可以同时读取多个数据通道。缓冲区可通过/dev/iio:device字符设备节点从用户空间访问。在触发处理程序中,用于填充缓冲区的函数是iio_push_to_buffers_with_timestamp。为设备分配触发缓冲区的函数是iio_triggered_buffer_setup()。
IIO 缓冲区 sysfs 接口
IIO 缓冲区在/sys/bus/iio/iio:deviceX/buffer/*下有一个关联的属性目录。以下是一些现有属性:
-
length:缓冲区可以存储的数据样本(容量)的总数。这是缓冲区包含的扫描数。 -
enable:这将激活缓冲区捕获,启动缓冲区捕获。 -
watermark:此属性自内核版本 v4.2 起可用。它是一个正数,指定阻塞读取应等待多少个扫描元素。例如,如果使用poll,它将阻塞直到达到水印。只有在水印大于请求的读取量时才有意义。它不影响非阻塞读取。可以在带有超时的 poll 上阻塞,并在超时到期后读取可用样本,从而保证最大延迟。
IIO 缓冲区设置
要读取并推送到缓冲区的数据通道称为扫描元素。它们的配置可通过/sys/bus/iio/iio:deviceX/scan_elements/*目录从用户空间访��,包含以下属性:
-
en(实际上是属性名称的后缀)用于启用通道。仅当其属性非零时,触发捕获才会包含此通道的数据样本。例如,in_voltage0_en,in_voltage1_en等。 -
type描述了缓冲区内的扫描元素数据存储方式,因此也描述了从用户空间读取的形式。例如,in_voltage0_type。格式为[be|le]:[s|u]bits/storagebitsXrepeat[>>shift]。 -
be或le指定字节顺序(大端或小端)。 -
s或u指定符号,即有符号(2 的补码)或无符号。 -
bits是有效数据位数。 -
storagebits是该通道在缓冲区中占用的位数。也就是说,一个值可能实际上是用 12 位编码(bits),但在缓冲区中占用 16 位(storagebits)。因此,必须将数据向右移动四次才能获得实际值。此参数取决于设备,应参考其数据表。 -
shift表示在屏蔽未使用的位之前应移动数据值的次数。此参数并非总是需要的。如果有效位数(bits)等于存储位数,则移位将为 0。也可以在设备数据表中找到此参数。 -
repeat指定位/存储位重复的次数。当重复元素为 0 或 1 时,重复值被省略。
解释这一部分的最佳方法是通过内核文档的摘录,可以在这里找到:www.kernel.org/doc/html/latest/driver-api/iio/buffers.html。例如,一个具有 12 位分辨率的 3 轴加速度计的驱动程序,其中数据存储在两个 8 位寄存器中,如下所示:
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|D3 |D2 |D1 |D0 | X | X | X | X | (LOW byte, address 0x06)
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|D11|D10|D9 |D8 |D7 |D6 |D5 |D4 | (HIGH byte, address 0x07)
+---+---+---+---+---+---+---+---+
每个轴将具有以下扫描元素类型:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type
le:s12/16>>4
应该将其解释为小端符号数据,16 位大小,需要在屏蔽掉 12 个有效数据位之前向右移 4 位。
struct iio_chan_spec中负责确定通道值如何存储到缓冲区的元素是scant_type。
struct iio_chan_spec {
[...]
struct {
char sign; /* Should be 'u' or 's' as explained above */
u8 realbits;
u8 storagebits;
u8 shift;
u8 repeat;
enum iio_endian endianness;
} scan_type;
[...]
};
这个结构绝对匹配[be|le]:[s|u]bits/storagebitsXrepeat[>>shift],这是前一节中描述的模式。让我们来看看结构的每个成员:
-
sign表示数据的符号,并匹配模式中的[s|u]。 -
realbits对应于模式中的bits -
storagebits与模式中的相同名称匹配 -
shift对应于模式中的 shift,repeat也是一样的 -
iio_indian表示字节序,与模式中的[be|le]匹配
此时,可以编写与先前解释的类型相对应的 IIO 通道结构:
struct struct iio_chan_spec accel_channels[] = {
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_X,
/* other stuff here */
.scan_index = 0,
.scan_type = {
.sign = 's',
.realbits = 12,
.storagebits = 16,
.shift = 4,
.endianness = IIO_LE,
},
}
/* similar for Y (with channel2 = IIO_MOD_Y, scan_index = 1)
* and Z (with channel2 = IIO_MOD_Z, scan_index = 2) axis
*/
}
把所有东西放在一起
让我们更仔细地看一下 BOSH 的数字三轴加速度传感器 BMA220。这是一个 SPI/I2C 兼容设备,具有 8 位大小的寄存器,以及一个片上运动触发中断控制器,实际上可以感应倾斜、运动和冲击振动��其数据表可在以下网址找到:www.mouser.fr/pdfdocs/BSTBMA220DS00308.PDF,其驱动程序自内核 v4.8 以来已经被引入(CONFIG_BMA200)。让我们来看一下:
首先,我们使用struct iio_chan_spec声明我们的 IIO 通道。一旦触发缓冲区被使用,我们需要填充.scan_index和.scan_type字段:
#define BMA220_DATA_SHIFT 2
#define BMA220_DEVICE_NAME "bma220"
#define BMA220_SCALE_AVAILABLE "0.623 1.248 2.491 4.983"
#define BMA220_ACCEL_CHANNEL(index, reg, axis) { \
.type = IIO_ACCEL, \
.address = reg, \
.modified = 1, \
.channel2 = IIO_MOD_##axis, \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
.scan_index = index, \
.scan_type = { \
.sign = 's', \
.realbits = 6, \
.storagebits = 8, \
.shift = BMA220_DATA_SHIFT, \
.endianness = IIO_CPU, \
}, \
}
static const struct iio_chan_spec bma220_channels[] = {
BMA220_ACCEL_CHANNEL(0, BMA220_REG_ACCEL_X, X),
BMA220_ACCEL_CHANNEL(1, BMA220_REG_ACCEL_Y, Y),
BMA220_ACCEL_CHANNEL(2, BMA220_REG_ACCEL_Z, Z),
};
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)表示每个通道将有一个*_raw sysfs 条目(属性),.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE)表示所有相同类型的通道只有一个*_scale sysfs 条目:
jma@jma:~$ ls -l /sys/bus/iio/devices/iio:device0/
(...)
# without modifier, a channel name would have in_accel_raw (bad)
-rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_scale
-rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_x_raw
-rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_y_raw
-rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_z_raw
(...)
读取in_accel_scale调用read_raw()钩子,将 mask 设置为IIO_CHAN_INFO_SCALE。读取in_accel_x_raw调用read_raw()钩子,将 mask 设置为IIO_CHAN_INFO_RAW。因此,真实值是raw_value * scale。
.scan_type表示每个通道返回的值是 8 位大小(将占用缓冲区中的 8 位),但有用的有效载荷只占用 6 位,并且数据必须在屏蔽未使用的位之前右移 2 次。任何扫描元素类型都将如下所示:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_type
le:s6/8>>2
以下是我们的pollfunc(实际上是底部),它从设备中读取样本并将读取的值推送到缓冲区(iio_push_to_buffers_with_timestamp())。完成后,我们通知核心(iio_trigger_notify_done()):
static irqreturn_t bma220_trigger_handler(int irq, void *p)
{
int ret;
struct iio_poll_func *pf = p;
struct iio_dev *indio_dev = pf->indio_dev;
struct bma220_data *data = iio_priv(indio_dev);
struct spi_device *spi = data->spi_device;
mutex_lock(&data->lock);
data->tx_buf[0] = BMA220_REG_ACCEL_X | BMA220_READ_MASK;
ret = spi_write_then_read(spi, data->tx_buf, 1, data->buffer,
ARRAY_SIZE(bma220_channels) - 1);
if (ret < 0)
goto err;
iio_push_to_buffers_with_timestamp(indio_dev, data->buffer,
pf->timestamp);
err:
mutex_unlock(&data->lock);
iio_trigger_notify_done(indio_dev->trig);
return IRQ_HANDLED;
}
以下是read函数。这是一个钩子,每次读取设备的 sysfs 条目时都会调用它:
static int bma220_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
int ret;
u8 range_idx;
struct bma220_data *data = iio_priv(indio_dev);
switch (mask) {
case IIO_CHAN_INFO_RAW:
/* If buffer mode enabled, do not process single-channel read */
if (iio_buffer_enabled(indio_dev))
return -EBUSY;
/* Else we read the channel */
ret = bma220_read_reg(data->spi_device, chan->address);
if (ret < 0)
return -EINVAL;
*val = sign_extend32(ret >> BMA220_DATA_SHIFT, 5);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
ret = bma220_read_reg(data->spi_device, BMA220_REG_RANGE);
if (ret < 0)
return ret;
range_idx = ret & BMA220_RANGE_MASK;
*val = bma220_scale_table[range_idx][0];
*val2 = bma220_scale_table[range_idx][1];
return IIO_VAL_INT_PLUS_MICRO;
}
return -EINVAL;
}
当读取*raw sysfs 文件时,将调用该钩子,mask参数中给出IIO_CHAN_INFO_RAW,并且*val和val2实际上是输出参数。它们必须设置为原始值(从设备中读取)。对*scale sysfs 文件的任何读取都将调用带有IIO_CHAN_INFO_SCALE的mask参数的钩子,以及每个属性掩码。
这也适用于write函数,用于将值写入设备。你的驱动程序有 80%的可能不需要write函数。这个write钩子允许用户更改设备的比例:
static int bma220_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
int i;
int ret;
int index = -1;
struct bma220_data *data = iio_priv(indio_dev);
switch (mask) {
case IIO_CHAN_INFO_SCALE:
for (i = 0; i < ARRAY_SIZE(bma220_scale_table); i++)
if (val == bma220_scale_table[i][0] &&
val2 == bma220_scale_table[i][1]) {
index = i;
break;
}
if (index < 0)
return -EINVAL;
mutex_lock(&data->lock);
data->tx_buf[0] = BMA220_REG_RANGE;
data->tx_buf[1] = index;
ret = spi_write(data->spi_device, data->tx_buf,
sizeof(data->tx_buf));
if (ret < 0)
dev_err(&data->spi_device->dev,
"failed to set measurement range\n");
mutex_unlock(&data->lock);
return 0;
}
return -EINVAL;
}
每当写入设备时,都会调用此函数。经常更改的参数是比例。例如:echo <desired-scale> > /sys/bus/iio/devices/iio;devices0/in_accel_scale。
现在,要填写一个struct iio_info结构,以提供给我们的iio_device:
static const struct iio_info bma220_info = {
.driver_module = THIS_MODULE,
.read_raw = bma220_read_raw,
.write_raw = bma220_write_raw, /* Only if your driver need it */
};
在probe函数中,我们分配并设置了一个struct iio_dev IIO 设备。私有数据的内存也被保留:
/*
* We provide only two mask possibility, allowing to select none or every
* channels.
*/
static const unsigned long bma220_accel_scan_masks[] = {
BIT(AXIS_X) | BIT(AXIS_Y) | BIT(AXIS_Z),
0
};
static int bma220_probe(struct spi_device *spi)
{
int ret;
struct iio_dev *indio_dev;
struct bma220_data *data;
indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data));
if (!indio_dev) {
dev_err(&spi->dev, "iio allocation failed!\n");
return -ENOMEM;
}
data = iio_priv(indio_dev);
data->spi_device = spi;
spi_set_drvdata(spi, indio_dev);
mutex_init(&data->lock);
indio_dev->dev.parent = &spi->dev;
indio_dev->info = &bma220_info;
indio_dev->name = BMA220_DEVICE_NAME;
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->channels = bma220_channels;
indio_dev->num_channels = ARRAY_SIZE(bma220_channels);
indio_dev->available_scan_masks = bma220_accel_scan_masks;
ret = bma220_init(data->spi_device);
if (ret < 0)
return ret;
/* this call will enable trigger buffer support for the device */
ret = iio_triggered_buffer_setup(indio_dev, iio_pollfunc_store_time,
bma220_trigger_handler, NULL);
if (ret < 0) {
dev_err(&spi->dev, "iio triggered buffer setup failed\n");
goto err_suspend;
}
ret = iio_device_register(indio_dev);
if (ret < 0) {
dev_err(&spi->dev, "iio_device_register failed\n");
iio_triggered_buffer_cleanup(indio_dev);
goto err_suspend;
}
return 0;
err_suspend:
return bma220_deinit(spi);
}
可以通过CONFIG_BMA220内核选项启用此驱动程序。也就是说,这仅在内核 v4.8 及以后版本中可用。在旧版本的内核中,可以使用CONFIG_BMA180选项启用最接近的设备。
IIO 数据访问
您可能已经猜到,使用 IIO 框架访问数据只有两种方式;通过 sysfs 通道进行一次性捕获,或通过 IIO 字符设备进行连续模式(触发缓冲区)。
一次性捕获
一次性数据捕获是通过 sysfs 接口完成的。通过读取与通道对应的 sysfs 条目,您将仅捕获与该通道特定的数据。假设有一个具有两个通道的温度传感器:一个用于环境温度,另一个用于热电偶温度:
# cd /sys/bus/iio/devices/iio:device0
# cat in_voltage3_raw
6646
# cat in_voltage_scale
0.305175781
通过将比例乘以原始值来获得处理后的值。
电压值:6646 * 0.305175781 = 2028.19824053
设备数据表说明处理值以 MV 为单位。在我们的情况下,它对应于 2.02819V。
缓冲区数据访问
要使触发采集工作,触发支持必须已经在您的驱动程序中实现。然后,要从用户空间获取数据,必须:创建触发器,分配它,启用 ADC 通道,设置缓冲区的维度,并启用它)。以下是此代码:
使用 sysfs 触发器进行捕获
使用 sysfs 触发器捕获数据包括发送一组命令到 sysfs 文件。让我们列举一下我们应该做什么来实现这一点:
- 创建触发器:在触发器可以分配给任何设备之前,它应该被创建:
#
echo 0 > /sys/devices/iio_sysfs_trigger/add_trigger
在这里,0对应于我们需要分配给触发器的索引。在此命令之后,触发器目录将在*/sys/bus/iio/devices/*下作为trigger0可用。
- 将触发器分配给设备:触发器通过其名称唯一标识,我们可以使用它来将设备与触发器绑定。由于我们使用 0 作为索引,触发器将被命名为
sysfstrig0:
# echo sysfstrig0 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
我们也可以使用这个命令:cat /sys/bus/iio/devices/trigger0/name > /sys/bus/iio/devices/iio:device0/trigger/current_trigger。也就是说,如果我们写入的值与现有的触发器名称不对应,什么也不会发生。为了确保我们真的定义了一个触发器,我们可以使用cat /sys/bus/iio/devices/iio:device0/trigger/current_trigger。
- 启用一些扫描元素:这一步包括选择哪些通道的数据值应该被推送到缓冲区中。在驱动程序中应该注意
available_scan_masks:
# echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en
- 设置缓冲区大小:在这里,应该设置缓冲区可以容纳的样本集的数量:
#
echo 100 > /sys/bus/iio/devices/iio:device0/buffer/length
- 启用缓冲区:这一步包括将缓冲区标记为准备好接收推送数据:
#
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
要停止捕获,我们必须在同一文件中写入 0。
- 触发:启动采集:
#
echo 1 > /sys/bus/iio/devices/trigger0/trigger_now
现在采集完成了,我们可以:
- 禁用缓冲区:
#
echo 0 > /sys/bus/iio/devices/iio:device0/buffer/enable
- 分离触发器:
#
echo "" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger
- 转储我们的 IIO 字符设备的内容:
#
cat /dev/iio\:device0 | xxd -
使用 hrtimer 触发进行捕获
以下是一组命令,允许使用 hrtimer 触发来捕获数据:
# echo /sys/kernel/config/iio/triggers/hrtimer/trigger0
#
echo 50 > /sys/bus/iio/devices/trigger0/sampling_frequency
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en
#
echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
#
cat /dev/iio:device0 | xxd -
0000000: 0188 1a30 0000 0000 8312 68a8 c24f 5a14 ...0......h..OZ.
0000010: 0188 1a30 0000 0000 192d 98a9 c24f 5a14 ...0.....-...OZ.
[...]
并且,我们查看类型以确定如何处理数据:
$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage_type
be:s14/16>>2
电压处理:0x188 >> 2 = 98 * 250 = 24500 = 24.5 v
IIO 工具
有一些有用的工具可以帮助您简化和加快使用 IIO 设备开发应用程序的过程。它们在内核树中的tools/iio中可用:
-
lsiio.c: 枚举 IIO 触发器、设备和通道 -
iio_event_monitor.c:监视 IIO 设备的 ioctl 接口以获取 IIO 事件 -
generic_buffer.c:从 IIO 设备的缓冲区中检索、处理和打印数据 -
libiio:由模拟设备开发的强大库,用于与 IIO 设备进行接口交互,可在github.com/analogdevicesinc/libiio上获得。
摘要
到本章结束时,您应该已经熟悉了 IIO 框架和词汇。您知道通道、设备和触发器是什么。您甚至可以通过用户空间、sysfs 或字符设备与您的 IIO 设备进行交互。现在是编写自己的 IIO 驱动程序的时候了。有很多现有的驱动程序不支持触发缓冲区。您可以尝试在其中一个驱动程序中添加这样的功能。在下一章中,我们将使用系统上最有用/最常用的资源:内存。要坚强,游戏刚刚开始。
第十一章:内核内存管理
在 Linux 系统上,每个内存地址都是虚拟的。它们不直接指向 RAM 中的任何地址。每当访问内存位置时,都会执行翻译机制以匹配相应的物理内存。
让我们从一个简短的故事开始介绍虚拟内存的概念。给定一个酒店,每个房间都可以有一个私人号码的电话。当然,任何安装的电话都属于酒店。它们都不能直接从酒店外部连接。
如果您需要联系房间的居住者,比如您的朋友,他必须给您酒店的总机号码和他所住的房间号码。一旦您拨打总机并提供您需要交谈的居住者的房间号码,接待员会立即将您的电话转接到房间的真实私人电话。只有接待员和房间的居住者知道私人号码的映射:
(switchboard number + room number) <=> private (real) phone number
每当城市(或世界各地)的某人想要联系房间的居住者时,他必须经过热线。他需要知道酒店的正确热线号码和房间号码。这样,总机号码+房间号码 = 虚拟地址,而私人电话号码对应于物理地址。在 Linux 上也适用一些与酒店相关的规则:
| 酒店 | Linux |
|---|---|
| 您无法联系没有房间里的私人电话的居住者。甚至没有办法尝试这样做。您的电话将突然结束。 | 您无法访问地址空间中不存在的内存。这将导致段错误。 |
| 您无法联系不存在的居住者,或者酒店不知道他们的入住情况,或者总机找不到他们的信息。 | 如果访问未映射的内存,CPU 会引发页面错误,操作系统会处理它。 |
| 您无法联系已经离开的居住者。 | 您无法访问已释放的内存。也许它已经分配给另一个进程。 |
| 许多酒店可能属于同一品牌,但位于不同的地方,每个酒店都有不同的热线号码。如果您犯了热线号码的错误。 | 不同的进程可能在其地址空间中具有相同的虚拟地址,但指向另一个不同的物理地址。 |
| 有一本书(或带有数据库的软件)保存着房间号和私人电话号码之间的映射,并在需要时由接待员查询。 | 虚拟地址通过页表映射到物理内存,页表由操作系统内核维护并由处理器查询。 |
这就是人们如何想象虚拟地址在 Linux 系统上工作的方式。
在本章中,我们将涉及整个 Linux 内存管理系统,涵盖以下主题:
-
内存布局以及地址转换和 MMU
-
内存分配机制(页面分配器、slab 分配器、kmalloc 分配器等)
-
I/O 内存访问
-
将内核内存映射到用户空间并实现
mmap()回调函数 -
介绍 Linux 缓存系统
-
介绍设备管理的资源框架(devres)
系统内存布局-内核空间和用户空间
在本章中,诸如内核空间和用户空间的术语将指的是它们的虚拟地址空间。在 Linux 系统上,每个进程拥有一个虚拟地址空间。这是进程生命周期中的一种内存沙盒。在 32 位系统上,该地址空间大小为 4GB(即使在物理内存小于 4GB 的系统上也是如此)。对于每个进程,这 4GB 地址空间分为两部分:
-
用户空间虚拟地址
-
内核空间虚拟地址
分割的方式取决于一个特殊的内核配置选项CONFIG_PAGE_OFFSET,它定义了内核地址部分在进程地址空间中的起始位置。32 位系统上默认的常见值是0xC0000000,但这可能会改变,就像 NXP 的 i.MX6 系列处理器使用的0x80000000一样。在整个章节中,我们将默认考虑0xC0000000。这被称为 3G/1G 分割,其中用户空间被赋予虚拟地址空间的较低 3GB,内核使用剩余的 1GB。典型的进程虚拟地址空间布局如下:
.------------------------. 0xFFFFFFFF
| | (4 GB)
| Kernel addresses |
| |
| |
.------------------------.CONFIG_PAGE_OFFSET
| |(x86: 0xC0000000, ARM: 0x80000000)
| |
| |
| User space addresses |
| |
| |
| |
| |
'------------------------' 00000000
内核和用户空间中使用的地址都是虚拟地址。不同之处在于访问内核地址需要特权模式。特权模式具有扩展特权。当 CPU 运行用户空间代码时,活动进程被认为是在用户模式下运行;当 CPU 运行内核空间代码时,活动进程被认为是在内核模式下运行。
通过使用上面显示的进程布局,可以区分一个地址(虚拟的当然)是内核空间地址还是用户空间地址。落入 0-3GB 的每个地址都来自用户空间;否则,它来自内核。
内核与每个进程共享地址空间的原因是因为每个单独的进程在某一时刻都会使用系统调用,这将涉及内核。将内核的虚拟内存地址映射到每个进程的虚拟地址空间中,可以避免在每次进入(和退出)内核时切换内存地址空间的成本。这就是为什么内核地址空间永久映射在每个进程的顶部,以加速通过系统调用访问内核的原因。
内存管理单元将内存组织成固定大小的单元,称为页。一个页由 4096 字节(4KB)组成。即使这个大小在其他系统上可能有所不同,在我们感兴趣的 ARM 和 x86 架构上是固定的:
-
内存页、虚拟页或简单地说页是用来指代固定长度的连续虚拟内存块的术语。相同的名称“页”被用作内核数据结构来表示内存页。
-
另一方面,框架(或页框)指的是操作系统映射内存页的物理内存上的固定长度连续块。每个页框都有一个号码,称为页框号(PFN)。给定一个页面,可以很容易地得到它的 PFN,反之亦然,使用
page_to_pfn和pfn_to_page宏,这将在接下来的章节中详细讨论。 -
页表是内核和体系结构数据结构,用于存储虚拟地址和物理地址之间的映射。键对页/页框描述了页表中的单个条目。这代表了一个映射。
由于内存页映射到页框,不言而喻,页和页框的大小相同,在我们的情况下是 4K。页的大小在内核中通过PAGE_SIZE宏定义。
有时需要内存对齐到页。如果内存的地址恰好从一个页面的开头开始,就说内存是对齐的。例如,在一个 4K 页面大小的系统上,4096、20480 和 409600 是对齐的内存地址的实例。换句话说,任何地址是系统页面大小的倍数的内存都被认为是对齐的。
内核地址-低内存和高内存的概念
Linux 内核有自己的虚拟地址空间,就像每个用户模式进程一样。内核的虚拟地址空间(在 3G/1G 分割中大小为 1GB)分为两部分:
-
低内存或 LOWMEM,即前 896MB
-
高内存或 HIGHMEM,表示顶部 128MB
Physical mem
Process address space +------> +------------+
| | 3200 M |
| | |
4 GB +---------------+ <-----+ | HIGH MEM |
| 128 MB | | |
+---------------+ <---------+ | |
+---------------+ <------+ | | |
| 896 MB | | +--> +------------+
3 GB +---------------+ <--+ +-----> +------------+
| | | | 896 MB | LOW MEM
| ///// | +---------> +------------+
| |
0 GB +---------------+
低内存
内核地址空间的前 896MB 构成低内存区域。在引导时,内核会永久映射这 896MB。由此映射得到的地址称为逻辑地址。这些是虚拟地址,但可以通过减去固定偏移量来转换为物理地址,因为映射是永久的并且提前知道。低内存与物理地址的下限相匹配。可以将低内存定义为内核空间中存在逻辑地址的内存。大多数内核内存函数返回低内存。实际上,为了服务不同的目的,内核内存被划分为一个区域。实际上,LOWMEM 的前 16MB 被保留用于 DMA 使用。由于硬件限制,内核无法将所有页面视为相同。因此,我们可以在内核空间中识别三个不同的内存区域:
-
ZONE_DMA:这包含了 16MB 以下的内存页框,保留用于直接内存访问(DMA) -
ZONE_NORMAL:这包含了 16MB 以上和 896MB 以下的内存页框,用于正常使用 -
ZONE_HIGHMEM:这包含了 896MB 及以上的内存页框
这意味着在 512MB 系统上,将没有ZONE_HIGHMEM,16MB 用于ZONE_DMA,496MB 用于ZONE_NORMAL。
逻辑地址的另一个定义:在内核空间中,按物理地址线性映射的地址,可以通过偏移量或应用位掩码转换为物理地址。可以使用__pa(address)宏将物理地址转换为逻辑地址,然后使用__va(address)宏进行恢复。
高内存
内核地址空间的顶部 128MB 称为高内存区域。内核用它来临时映射 1G 以上的物理内存。当需要访问 1GB 以上(或更精确地说,896MB)的物理内存时,内核使用这 128MB 来创建对其虚拟地址空��的临时映射,从而实现能够访问所有物理页面的目标。可以将高内存定义为不存在逻辑地址并且未永久映射到内核地址空间的内存。896MB 以上的物理内存会按需映射到 128MB 的 HIGHMEM 区域。
内核会动态创建和销毁用于访问高内存的映射。这使得高内存访问变慢。也就是说,在 64 位系统上,由于巨大的地址范围(2⁶⁴),3G/1G 分割不再有意义,因此高内存的概念在 64 位系统上不存在。
用户空间地址
在这一部分,我们将通过进程处理用户空间。每个进程在内核中表示为struct task_struct的一个实例(参见*include/linux/sched.h*),它描述和表征一个进程。每个进程都有一个内存映射表,存储在struct mm_struct类型的变量中(参见*include/linux/mm_types.h*)。因此,可以猜到每个task_struct中至少嵌入了一个mm_struct字段。以下一行是我们感兴趣的task_struct结构定义的一部分:
struct task_struct{
[...]
struct mm_struct *mm, *active_mm;
[...]
}
内核全局变量current,指向当前进程。字段*mm,指向其内存映射表。根据定义,current->mm指向当前进程的内存映射表。
现在让我们看一下struct mm_struct是什么样子:
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
unsigned long mmap_base;
unsigned long task_size;
unsigned long highest_vm_end;
pgd_t * pgd;
atomic_t mm_users;
atomic_t mm_count;
atomic_long_t nr_ptes;
#if CONFIG_PGTABLE_LEVELS > 2
atomic_long_t nr_pmds;
#endif
int map_count;
spinlock_t page_table_lock;
struct rw_semaphore mmap_sem;
unsigned long hiwater_rss;
unsigned long hiwater_vm;
unsigned long total_vm;
unsigned long locked_vm;
unsigned long pinned_vm;
unsigned long data_vm;
unsigned long exec_vm;
unsigned long stack_vm;
unsigned long def_flags;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/* Architecture-specific MM context */
mm_context_t context;
unsigned long flags;
struct core_state *core_state;
#ifdef CONFIG_MEMCG
/*
* "owner" points to a task that is regarded as the canonical
* user/owner of this mm. All of the following must be true in
* order for it to be changed:
*
* current == mm->owner
* current->mm != mm
* new_owner->mm == mm
* new_owner->alloc_lock is held
*/
struct task_struct __rcu *owner;
#endif
struct user_namespace *user_ns;
/* store ref to file /proc/<pid>/exe symlink points to */
struct file __rcu *exe_file;
};
我故意删除了一些我们不感兴趣的字段。还有一些字段我们稍后会谈论:例如pgd,它是指向进程基础(第一个条目)级别1表(PGD)的指针,在上下文切换时写入 CPU 的转换表基地址。不管怎样,在继续之前,让我们看一下进程地址空间的表示:

进程内存布局
从进程的角度来看,内存映射实际上可以看作是一组专门用于连续虚拟地址范围的页表条目。这个连续虚拟地址范围称为内存区域,或虚拟内存区域(VMA)。每个内存映射由起始地址和长度、权限(例如程序是否可以从该内存读取、写入或执行)以及关联资源(例如物理页面、交换页面、文件内容等)描述。
mm_struct有两种存储进程区域(VMA)的方式:
-
在红黑树中,根元素由字段
mm_struct->mm_rb指向。 -
在一个链表中,第一个元素由字段
mm_struct->mmap指向。
虚拟内存区域(VMA)
内核使用虚拟内存区域来跟踪进程的内存映射,例如,一个进程对于其代码有一个 VMA,对于每种类型的数据有一个 VMA,对于每个不同的内存映射(如果有的话)有一个 VMA 等等。VMAs 是处理器无关的结构,具有权限和访问控制标志。每个 VMA 都有一个起始地址和长度,它们的大小始终是页面大小(PAGE_SIZE)的倍数。VMA 由多个页面组成,每个页面在页表中都有一个条目。
VMA 描述的内存区域始终是虚拟连续的,而不是物理的。可以通过/proc/<pid>/maps文件或使用pmap命令来检查与进程关联的所有 VMA。

图片来源:http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory/
# cat /proc/1073/maps
00400000-00403000 r-xp 00000000 b3:04 6438 /usr/sbin/net-listener
00602000-00603000 rw-p 00002000 b3:04 6438 /usr/sbin/net-listener
00603000-00624000 rw-p 00000000 00:00 0 [heap]
7f0eebe4d000-7f0eebe54000 r-xp 00000000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eebe54000-7f0eec054000 ---p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eec054000-7f0eec055000 rw-p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eec055000-7f0eec069000 r-xp 00000000 b3:04 21629 /lib/libresolv-2.22.so
7f0eec069000-7f0eec268000 ---p 00014000 b3:04 21629 /lib/libresolv-2.22.so
[...]
7f0eee1e7000-7f0eee1e8000 rw-s 00000000 00:12 12532 /dev/shm/sem.thk-mcp-231016-sema
[...]
前面摘录中的每一行都代表一个 VMA,字段映射以下模式:{address(start-end)} {permissions} {offset} {device(major:minor)} {inode} {pathname(image)}:
-
address:这表示 VMA 的起始和结束地址。 -
permissions:这描述了区域的访问权限:r(读取)、w(写入)和x(执行),包括p(如果映射是私有的)和s(用于共享映射)。 -
Offset:在文件映射(mmap系统调用)的情况下,它是映射发生的文件中的偏移量。否则为0。 -
major:minor:在文件映射的情况下,它们表示文件存储的设备的主要和次要编号(保存文件的设备)。 -
inode:在从文件映射的情况下,表示映射文件的 inode 号。 -
pathname:这是映射文件的名称,否则为空白。还有其他区域名称,如[heap],[stack]或[vdso],表示虚拟动态共享对象,这是内核映射到每个进程地址空间的共享库,以减少系统调用切换到内核模式时的性能损失。
分配给进程的每个页面都属于一个区域;因此,任何不在 VMA 中的页面都不存在,也不能被进程引用。
高内存非常适合用户空间,因为用户空间的虚拟地址必须显式映射。因此,大多数高内存被用户应用程序占用。__GFP_HIGHMEM和GFP_HIGHUSER是请求分配(可能)高内存的标志。没有这些标志,所有内核分配只返回低内存。在 Linux 中,没有办法从用户空间分配连续的物理内存。
可以使用find_vma函数找到与给定虚拟地址对应的 VMA。find_vma在linux/mm.h中声明。
* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
这是一个例子:
struct vm_area_struct *vma = find_vma(task->mm, 0x13000);
if (vma == NULL) /* Not found ? */
return -EFAULT;
if (0x13000 >= vma->vm_end) /* Beyond the end of returned VMA ? */
return -EFAULT;
可以通过读取文件/proc/<PID>/map,/proc/<PID>/smap和/proc/<PID>/pagemap来获取内存映射的整个过程。
地址转换和 MMU
虚拟内存是一个概念,是给进程的一种幻觉,使其认为自己拥有大量几乎无限的内存,有时甚至比系统实际拥有的更多。CPU 负责在每次访问内存位置时将虚拟地址转换为物理地址。这种机制称为地址转换,由 CPU 的一部分内存管理单元(MMU)执行。
MMU 保护内存免受未经授权的访问。对于给定的进程,需要访问的任何页面必须存在于进程的 VMAs 之一,并且因此必须存在于进程页表中(每个进程都有自己的页表)。
内存以固定大小的块命名为页用于虚拟内存,帧用于物理内存,在我们的情况下大小为 4 KB。无论如何,您不需要猜测您为其编写驱动程序的系统的页面大小。它是由PAGE_SIZE宏在内核中定义和访问的。因此,请记住,页面大小是由硬件(CPU)强加的。考虑到 4 KB 页面大小的系统,字节 0 到 4095 位于页面 0 中,字节 4096-8191 位于页面 1 中,依此类推。
引入页表的概念是为了管理页面和帧之间的映射。页面分布在表上,以便每个 PTE 对应于页面和帧之间的映射。然后,每个进程都被分配一组页表来描述其整个内存空间。
为了遍历页,每个页被分配一个索引(类似于数组),称为页号。当涉及到帧时,它是 PFN。这样,虚拟内存地址由两部分组成:页号和偏移量。偏移量表示地址的 12 个最低有效位,而 13 个最低有效位表示它在 8 KB 页面大小系统上的情况:

虚拟地址表示
操作系统或 CPU 如何知道给定虚拟地址对应的物理地址?它们使用页表作为转换表,并知道每个条目的索引是虚拟页号,值是 PFN。为了访问给定虚拟内存的物理内存,操作系统首先提取偏移量、虚拟页号,然后遍历进程的页表,以将虚拟页号与物理页匹配。一旦匹配发生,就可以访问该页框中的数据:

地址转换
偏移量用于指向帧内的正确位置。页表不仅保存物理和虚拟页号之间的映射,还保存访问控制信息(读/写访问权限、特权等)。

虚拟到物理地址转换
用于表示偏移量的位数由内核宏PAGE_SHIFT定义。PAGE_SHIFT是将一个位左移以获得PAGE_SIZE值的位数。它也是右移以将虚拟地址转换为页号和物理地址转换为页框号的位数。以下是内核源代码中/include/asm-generic/page.h中这些宏的定义:
#define PAGE_SHIFT 12
#ifdef __ASSEMBLY__
#define PAGE_SIZE (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#endif
页表是一个部分解决方案。让我们看看为什么。大多数架构需要 32 位(4 字节)来表示 PTE。每个进程都有其私有的 3GB 用户空间地址,我们需要 786,432 个条目来描述和覆盖进程的地址空间。这代表了每个进程花费太多的物理内存,只是为了描述内存映射。事实上,进程通常只使用其虚拟地址空间的一小部分但分散的部分。为了解决这个问题,引入了级别的概念。页表通过级别(页级别)进行层次化。存储多级页表所需的空间仅取决于实际使用的虚拟地址空间,而不是与虚拟地址空间的最大大小成比例。这样,未使用的内存不再表示,页表遍历时间缩短。这样,级别 N 中的每个表项将指向级别 N+1 的表中的一个条目。级别 1 是更高级别。
Linux 使用四级分页模型:
-
页全局目录(PGD):这是第一级(级别 1)页表。内核中每个条目的类型是
pgd_t(通常是unsigned long),并指向第二级表中的一个条目。在内核中,tastk_struct结构表示进程的描述,它又有一个成员(mm),其类型是mm_struct,用于描述和表示进程的内存空间。在mm_struct中,有一个特定于处理器的字段pgd,它是进程级别 1(PGD)页表的第一个条目(条目 0)的指针。每个进程只有一个 PGD,最多可以包含 1024 个条目。 -
页上级目录(PUD):这仅存在于使用四级表的架构中。它代表间接的第二级。
-
页中间目录(PMD):这是第三级间接层,仅存在于使用四级表的架构中。
-
页表(PTE):树的叶子。它是一个
pte_t数组,其中每个条目指向物理页。
并非所有级别都总是被使用。i.MX6 的 MMU 只支持 2 级页表(PGD和PTE),几乎所有 32 位 CPU 都是如此。在这种情况下,PUD和PMD被简单地忽略。

两级表概述
你可能会问 MMU 如何知道进程的页表。很简单,MMU 不存储任何地址。相反,在 CPU 中有一个特殊的寄存器,称为页表基址寄存器(PTBR)或转换表基址寄存器 0(TTBR0),它指向进程的一级(顶级)页表(PGD)的基址(条目 0)。这正是struct mm_struct的pdg字段指向的地方:current->mm.pgd == TTBR0。
在上下文切换(当新进程被调度并获得 CPU 时),内核立即配置 MMU,并使用新进程的pgd更新 PTBR。现在,当虚拟地址被提供给 MMU 时,它使用 PTBR 的内容来定位进程的一级页表(PGD),然后使用从虚拟地址的最高有效位(MSBs)提取的一级索引来找到适当的表项,其中包含指向适当的二级页表的基地址的指针。然后,从该基地址开始,它使用二级索引来找到适当的条目,依此类推,直到达到 PTE。 ARM 架构(我们的情况下是 i.MX6)具有 2 级页表。在这种情况下,二级条目是 PTE,并指向物理页(PFN)。只有在这一步找到物理页。为了访问页面中的确切内存位置,MMU 提取内存偏移量,也是虚拟地址的一部分,并指向物理页面中的相同偏移量。
当进程需要从内存位置读取或写入(当然我们谈论的是虚拟内存)时,MMU 执行该进程的页表中的翻译,以找到正确的条目(PTE)。从虚拟地址中提取虚拟页号,并由处理器用作进程页表的索引,以检索其页表条目。如果该偏移处有有效的页表条目,则处理器从该条目中获取页框号。如果没有,则意味着进程访问了其虚拟内存的未映射区域。然后引发页面错误,操作系统应该处理它。
在现实世界中,地址转换需要进行页表遍历,并且它并不总是一次性操作。至少有与表级别相同数量的内存访问。四级页表将需要四次内存访问。换句话说,每次虚拟访问都会导致五次物理内存访问。如果虚拟内存访问比物理访问慢四倍,那么虚拟内存概念将是无用的。幸运的是,SoC 制造商努力找到了一个聪明的技巧来解决这个性能问题:现代 CPU 使用一个小的关联和非常快速的内存,称为翻译后备缓冲器(TLB),以缓存最近访问的虚拟页面的 PTE。
页面查找和 TLB
在 MMU 进行地址转换之前,还有另一步骤。就像有一个用于最近访问数据的缓存一样,还有一个用于最近转换地址的缓存。数据缓存加快了数据访问过程,TLB 加快了虚拟地址转换(是的,地址转换是一项棘手的任务。它是内容可寻址存储器,简称(CAM),其中键是虚拟地址,值是物理地址。换句话说,TLB 是 MMU 的缓存。在每次内存访问时,MMU 首先检查 TLB 中最近使用的页面,其中包含一些当前分配给物理页面的虚拟地址范围。
TLB 是如何工作的
在虚拟内存访问时,CPU 通过 TLB 尝试查找正在访问的页面的虚拟页号。这一步称为 TLB 查找。当找到 TLB 条目(发生匹配)时,就会发生TLB 命中,CPU 继续运行并使用在 TLB 条目中找到的 PFN 来计算目标物理地址。当发生 TLB 命中时,不会发生页面错误。可以看到,只要在 TLB 中找到翻译,虚拟内存访问就会像物理访问一样快。如果没有找到 TLB 条目(未发生匹配),就会发生TLB 未命中。
在 TLB 未命中事件中,根据处理器类型,TLB 未命中事件可以由软件或硬件通过 MMU 处理:
-
软件处理:CPU 引发 TLB 未命中中断,被操作系统捕获。然后操作系统遍历进程的页表以找到正确的 PTE。如果有匹配和有效的条目,那么 CPU 会在 TLB 中安装新的翻译。否则,将执行页面错误处理程序。
-
硬件处理:由 CPU(实际上是 MMU)在硬件中遍历进程的页表。如果有匹配和有效的条目,CPU 将新的转换添加到 TLB 中。否则,CPU 会引发页面错误中断,由操作系统处理。
在这两种情况下,页面错误处理程序是相同的:执行do_page_fault()函数,这取决于体系结构。对于 ARM,do_page_fault在arch/arm/mm/fault.c中定义:

MMU 和 TLB 遍历过程
页表和页目录条目是与体系结构相关的。操作系统需要确保表的结构与 MMU 识别的结构相对应。在 ARM 处理器上,您必须将转换表的位置写入 CP15(协处理器 15)寄存器 c2,然后通过写入 CP15 寄存器 c1 来启用缓存和 MMU。详细信息请参阅infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/BABHJIBH.htm和infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0433c/CIHFDBEJ.html。
内存分配机制
让我们看一下下图,显示了 Linux 系统上存在的不同内存分配器,并稍后讨论它:
灵感来自:free-electrons.com/doc/training/linux-kernel/linux-kernel-slides.pdf。

内核内存分配器概述
有一种分配机制可以满足任何类型的内存请求。根据您需要内存的用途,您可以选择最接近您目标的分配机制。主要的分配器是页面分配器,它只处理页面(页面是它可以提供的最小内存单位)。然后是建立在页面分配器之上的SLAB 分配器,它从页面中获取页面并返回较小的内存实体(通过 slab 和缓存)。这是kmalloc 分配器依赖的分配器。
页面分配器
页面分配器是 Linux 系统上的低级分配器,其他分配器依赖于它。系统的物理内存由固定大小的块(称为页面帧)组成。页面帧在内核中表示为struct page结构的实例。页面是操作系统在低级别对任何内存请求提供的最小内存单位。
页面分配 API
您将了解到,内核页面分配器使用伙伴算法分配和释放页面块。页面以 2 的幂大小分配在块中(为了从伙伴算法中获得最佳效果)。这意味着它可以分配 1 页、2 页、4 页、8 页、16 页等等:
alloc_pages(mask, order)分配 2^(order)页并返回表示保留块的第一页的struct page实例。要分配一个页面,顺序应为 0。这就是alloc_page(mask)的作用:
struct page *alloc_pages(gfp_t mask, unsigned int order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
__free_pages()用于释放使用alloc_pages()函数分配的内存。它接受指向分配页面的指针作为参数,与分配时使用的顺序相同。
void __free_pages(struct page *page, unsigned int order);
- 还有其他以相同方式工作的函数,但它们返回保留块的地址(虚拟地址)。这些是
__get_free_pages(mask, order)和__get_free_page(mask):
unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);
free_pages()用于释放使用__get_free_pages()分配的页面。它接受表示分配页面起始区域的内核地址,以及应该与分配时使用的相同的顺序:
free_pages(unsigned long addr, unsigned int order);
在任一情况下,mask指定有关请求的详细信息,即内存区域和分配器的行为。可用选择是:
-
GFP_USER,用于用户内存分配。 -
GFP_KERNEL,用于内核分配的常用标志。 -
GFP_HIGHMEM,从 HIGH_MEM 区域请求内存。 -
GFP_ATOMIC,以无法休眠的原子方式分配内存。在需要从中断上下文中分配内存时使用。
有一个关于使用GFP_HIGHMEM的警告,不应与__get_free_pages()(或__get_free_page())一起使用。由于 HIGHMEM 内存不能保证是连续的,因此无法返回从该区域分配的内存的地址。全局只允许在与内存相关的函数中使用GFP_*的子集:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
/*
* __get_free_pages() returns a 32-bit address, which cannot represent
* a highmem page
*/
VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
可以分配的最大页面数为 1024。这意味着在 4 Kb 大小的系统上,最多可以分配 1024*4 Kb = 4 MB。对于kmalloc也是一样的。
转换函数
page_to_virt()函数用于将结构页(例如由alloc_pages()返回)转换为内核地址。virt_to_page()接受内核虚拟地址并返回其关联的结构页实例(就好像是使用alloc_pages()函数分配的)。virt_to_page()和page_to_virt()都在<asm/page.h>中定义:
struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *pg)
宏page_address()可用于返回与结构页实例的开始地址(当然是逻辑地址)对应的虚拟地址:
void *page_address(const struct page *page)
我们可以看到它是如何在get_zeroed_page()函数中使用的:
unsigned long get_zeroed_page(unsigned int gfp_mask)
{
struct page * page;
page = alloc_pages(gfp_mask, 0);
if (page) {
void *address = page_address(page);
clear_page(address);
return (unsigned long) address;
}
return 0;
}
__free_pages()和free_pages()可以混合使用。它们之间的主要区别在于free_page()以虚拟地址作为参数,而__free_page()以struct page结构作为参数。
页桶分配器
页桶分配器是kmalloc()依赖的分配器。它的主要目的是消除由于小内存分配而引起的内存(释放)引起的碎片化,并加速常用对象的内存分配。
伙伴算法
为了分配内存,请求的大小会向上舍入为 2 的幂,并且伙伴分配器会搜索适当的列表。如果请求列表上不存在条目,则从下一个更大的列表(其块大小是前一个列表的两倍)中拆分为两半(称为伙伴)。分配器使用第一半,而另一半添加到下一个列表中。这是一种递归方法,当伙伴分配器成功找到可以拆分的块,或者达到最大块大小且没有可用的空闲块时停止。
以下案例研究受到dysphoria.net/OperatingSystems1/4_allocation_buddy_system.html的启发。例如,如果最小分配大小为 1 KB,内存大小为 1 MB,伙伴分配器将为 1 KB 空洞创建一个空列表,为 2 KB 空洞创建一个空列表,为 4 KB 空洞创建一个列表,8 KB,16 KB,32 KB,64 KB,128 KB,256 KB,512 KB,以及一个 1 MB 空洞的列表。它们最初都是空的,除了 1 MB 列表只有一个空洞。
现在让我们想象一个场景,我们想要分配一个70K的块。伙伴分配器将其向上舍入为128K,然后将 1 MB 拆分为两个512K块,然后256K,最后128K,然后它将分配一个128K块给用户。以下是总结此场景的方案:

使用伙伴算法进行分配
释放与分配一样快。以下图总结了释放算法:

使用伙伴算法进行释放
对页桶分配器的探索
在我们介绍页桶分配器之前,让我们定义一些它使用的术语:
-
页桶:这是由几个页框组成的连续的物理内存块。每个页桶被分成相同大小的块,用于存储特定类型的内核对象,例如索引节点、互斥体等。每个页桶都是对象的数组。
-
缓存:由一个或多个页桶组成的链表,它们在内核中表示为
struct kmem_cache_t结构的实例。缓存只存储相同类型的对象(例如,仅存储索引节点,或仅存储地址空间结构)
SLAB 可能处于以下状态之一:
-
空:这是 SLAB 上的所有对象(块)都标记为空闲
-
部分:SLAB 中存在已使用和空闲对象
-
完整:SLAB 上的所有对象都标记为已使用
由内存分配器构建缓存。最初,每个 SLAB 都标记为空。当为内核对象分配内存时,系统会在该类型对象的缓存中的部分/空闲 SLAB 上寻找该对象的空闲位置。如果找不到,系统将分配一个新的 SLAB 并将其添加到缓存中。新对象从此 SLAB 分配,并且该 SLAB 标记为部分。当代码完成内存(释放内存)时,对象将以其初始化状态简单地返回到 SLAB 缓存中。
这也是内核提供帮助函数以获取零初始化内存的原因,以消除先前的内容。SLAB 保持其对象的使用数量的引用计数,因此当缓存中的所有 SLAB 都已满并且请求另一个对象时,SLAB 分配器负责添加新的 SLAB:

SLAB 缓存概述
这有点像创建每个对象的分配器。系统为每种类型的对象分配一个缓存,只有相同类型的对象才能存储在缓存中(例如,只有task_struct结构)。
内核中有不同类型的 SLAB 分配器,取决于是否需要紧凑性、缓存友好性或原始速度:
-
SLOB,尽可能紧凑
-
SLAB,尽可能缓存友好
-
SLUB,相当简单,需要较少的指令成本计数
kmalloc 家族分配
kmalloc是一个内核内存分配函数,类似于用户空间的malloc()。kmalloc返回的内存在物理内存和虚拟内存中是连续的:

kmalloc 分配器是内核中的通用和高级内存分配器,依赖于 SLAB 分配器。从 kmalloc 返回的内存具有内核逻辑地址,因为它是从LOW_MEM区域分配的,除非指定了HIGH_MEM。它在<linux/slab.h>中声明,这是在驱动程序中使用 kmalloc 时要包含的头文件。原型如下:
void *kmalloc(size_t size, int flags);
size指定要分配的内存的大小(以字节为单位)。flag确定应如何分配内存以及内存应该分配到哪里。可用的标志与页面分配器相同(GFP_KERNEL,GFP_ATOMIC,GFP_DMA等)。
-
GFP_KERNEL:这是标准标志。我们不能在中断处理程序中使用此标志,因为其代码可能会休眠。它总是从LOM_MEM区域返回内存(因此是逻辑地址)。 -
GFP_ATOMIC:这保证了分配的原子性。当我们处于中断上下文时,唯一要使用的标志。请不要滥用这一点,因为它使用了一个紧急内存池。 -
GFP_USER:这将内存分配给用户空间进程。然后,内存与分配给内核的内存是不同的并且分开的。 -
GFP_HIGHUSER:这从HIGH_MEMORY区域分配内存 -
GFP_DMA:这从DMA_ZONE分配内存。
成功分配内存后,kmalloc 返回分配的块的虚拟地址,保证是物理上连续的。在出错时,它返回NULL。
在分配小尺寸内存时,kmalloc 依赖于 SLAB 缓存。在这种情况下,内核将分配的区域大小舍入到它可以适应的最小 SLAB 缓存的大小。始终将其用作默认的内存分配器。在本书中使用的架构(ARM 和 x86)中,每个分配的最大尺寸为 4 MB,总分配量为 128 MB。请查看kaiwantech.wordpress.com/2011/08/17/kmalloc-and-vmalloc-linux-kernel-memory-allocation-api-limits/。
kfree函数用于释放 kmalloc 分配的内存。以下是kfree()的原型;
void kfree(const void *ptr)
让我们看一个例子:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/mm.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu");
void *ptr;
static int
alloc_init(void)
{
size_t size = 1024; /* allocate 1024 bytes */
ptr = kmalloc(size,GFP_KERNEL);
if(!ptr) {
/* handle error */
pr_err("memory allocation failed\n");
return -ENOMEM;
}
else
pr_info("Memory allocated successfully\n");
return 0;
}
static void alloc_exit(void)
{
kfree(ptr);
pr_info("Memory freed\n");
}
module_init(alloc_init);
module_exit(alloc_exit);
其他类似的函数有:
void kzalloc(size_t size, gfp_t flags);
void kzfree(const void *p);
void *kcalloc(size_t n, size_t size, gfp_t flags);
void *krealloc(const void *p, size_t new_size, gfp_t flags);
krealloc()是用户空间realloc()函数的内核等价物。因为kmalloc()返回的内存保留了其先前版本的内容,如果暴露给用户空间,可能存在安全风险。要获取清零的 kmalloc 分配的内存,应该使用kzalloc。kzfree()是kzalloc()的释放函数,而kcalloc()为数组分配内存,其参数n和size分别表示数组中的元素数量和元素的大小。
由于kmalloc()返回的内存区域在内核永久映射中(即物理上连续),可以使用virt_to_phys()将内存地址转换为物理地址,或者使用virt_to_bus()将其转换为 IO 总线地址。这些宏在必要时内部调用__pa()或__va()。物理地址(virt_to_phys(kmalloc'ed address))减去PAGE_SHIFT,将产生分配块所在的第一页的 PFN。
vmalloc 分配器
vmalloc()是本书中将讨论的最后一个内核分配器。它只返回虚拟空间上连续的内存:

返回的内存总是来自HIGH_MEM区域。返回的地址不能转换为物理地址或总线地址,因为无法断定内存是否物理上连续。这意味着vmalloc()返回的内存不能在微处理器外部使用(您不能轻松地用于 DMA 目的)。使用vmalloc()为仅在软件中存在的大型(例如,为网络缓冲区)顺序分配内存是正确的。重要的是要注意,vmalloc()比kmalloc()或页面分配器函数要慢,因为它必须检索内存,构建页表,甚至重新映射到虚拟连续范围,而kmalloc()从不这样做。
在使用此 vmalloc API 之前,您应该在代码中包含此头文件:
#include <linux/vmalloc.h>
以下是 vmalloc 家族的原型:
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);
void vfree( void *addr);
size是您需要分配的内存的大小。成功分配内存后,它返回分配的内存块的第一个字节的地址。失败时返回NULL。vfree函数用于释放vmalloc()分配的内存。
以下是使用vmalloc的示例:
#include<linux/init.h>
#include<linux/module.h>
#include <linux/vmalloc.h>
void *ptr;
static int alloc_init(void)
{
unsigned long size = 8192;
ptr = vmalloc(size);
if(!ptr)
{
/* handle error */
printk("memory allocation failed\n");
return -ENOMEM;
}
else
pr_info("Memory allocated successfully\n");
return 0;
}
static void my_vmalloc_exit(void) /* function called at the time of rmmod */
{
vfree(ptr); //free the allocated memory
printk("Memory freed\n");
}
module_init(my_vmalloc_init);
module_exit(my_vmalloc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("john Madieu, john.madieu@gmail.com");
可以使用/proc/vmallocinfo来显示系统上所有 vmalloc 分配的内存。VMALLOC_START和VMALLOC_END是两个限定 vmalloc 地址范围的符号。它们是与体系结构相关的,并在<asm/pgtable.h>中定义。
底层的进程内存分配
让我们专注于分配页面内存的低级分配器。内核将报告分配帧页(物理页),直到真正必要时(当这些页实际被访问时,通过读取或写入)。这种按需分配称为惰性分配,消除了分配永远不会被使用的页面的风险。
每当请求一个页面时,只更新页表,在大多数情况下,会创建一个新条目,这意味着只分配了虚拟内存。只有当您访问页面时,才会引发称为页面故障的中断。这个中断有一个专门的处理程序,称为页面故障处理程序,并且是由 MMU 响应对虚拟内存的访问尝试而立即成功。
实际上,无论访问类型是读取、写入还是执行,都会引发页面故障中断,指向页表中的条目未设置适当的权限位以允许该类型的访问。对该中断的响应有以下三种方式之一:
-
硬错误:页面不驻留在任何地方(既不在物理内存中,也不在内存映射文件中),这意味着处理程序无法立即解决错误。处理程序将执行 I/O 操作,以准备解决错误所需的物理页面,并且可能会挂起中断的进程并在系统解决问题时切换到另一个进程。
-
软错误:页面在内存中的其他位置(在另一个进程的工作集中)。这意味着错误处理程序可以通过立即将物理内存页面附加到适当的页表项,调整条目并恢复中断的指令来解决错误。
-
错误无法解决:这将导致总线错误或 segv。向有错误的进程发送
SIGSEGV,杀死它(默认行为),除非为SIGSEV安装了信号处理程序以更改默认行为。
内存映射通常开始时不附加任何物理页面,通过定义虚拟地址范围而不附加任何相关的物理内存。实际的物理内存是在以后分配,以响应页面错误异常,当访问内存时,因为内核提供了一��标志来确定尝试访问是否合法,并指定页面错误处理程序的行为。因此,用户空间的brk(), mmap()和类似函数分配(虚拟)空间,但物理内存稍后附加。
在中断上下文中发生的页面错误会导致双重故障中断,通常会导致内核恐慌(调用panic()函数)。这就是为什么在中断上下文中分配的内存来自内存池,不会引发页面错误中断。如果在处理双重故障时发生中断,将生成三重故障异常,导致 CPU 关闭并立即重新启动操作系统。这种行为实际上是与架构相关的。
写时复制(CoW)案例
CoW(与fork()一起广泛使用)是内核功能,不会为两个或多个进程共享的数据多次分配内存,直到进程触及它(写入它)为止;在这种情况下,将为其私有副本分配内存。以下显示了页面错误处理程序如何管理 CoW(单页案例研究):
-
将 PTE 添加到进程页表中,并标记为不可写。
-
映射将导致在进程 VMA 列表中创建一个 VMA。页面将添加到该 VMA,并将该 VMA 标记为可写。
-
在页面访问时(第一次写入),错误处理程序注意到差异,这意味着:这是写时复制。然后将分配一个物理页面,分配给上面添加的 PTE,更新 PTE 标志,刷新 TLB 条目,并执行
do_wp_page()函数,该函数可以将内容从共享地址复制到新位置。
与 I/O 内存一起工作以与硬件通信
除了执行数据 RAM 导向的操作外,还可以执行 I/O 内存事务,以与硬件通信。当涉及访问设备的寄存器时,内核根据系统架构提供两种可能性:
-
通过 I/O 端口:这也称为端口输入输出(PIO)。寄存器可通过专用总线访问,并且需要特定指令(通常是汇编中的
in和out)来访问这些寄存器。这是在 x86 架构上的情况。 -
内存映射输入输出(MMIO):这是最常见和最常用的方法。设备的寄存器��映射到内存。只需读取和写入特定地址即可写入设备的寄存器。这是在 ARM 架构上的情况。
PIO 设备访问
在使用 PIO 的系统上,有两个不同的地址空间,一个用于内存,我们已经讨论过,另一个用于 I/O 端口,称为端口地址空间,仅限于 65,536 个端口。这是一种旧的方式,在当今非常不常见。
内核导出了一些函数(符号)来处理 I/O 端口。在访问任何端口区域之前,我们必须首先通知内核我们正在使用一系列端口,使用request_region()函数,如果出错将返回NULL。完成区域后,必须调用release_region()。这两个都在linux/ioport.h中声明。它们的原型是:
struct resource *request_region(unsigned long start,
unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
这些函数通知内核您打算使用/释放从start开始的len个端口的区域。name参数应设置为您设备的名称。它们的使用不是强制的。这是一种礼貌,可以防止两个或更多的驱动程序引用相同范围的端口。可以通过读取/proc/ioports文件的内容来显示系统上实际使用的端口的信息。
一旦完成区域保留,就可以使用以下函数访问端口:
u8 inb(unsigned long addr)
u16 inw(unsigned long addr)
u32 inl(unsigned long addr)
分别访问(读取)大小为 8、16 或 32 位的端口,并且以下函数:
void outb(u8 b, unsigned long addr)
void outw(u16 b, unsigned long addr)
void outl(u32 b, unsigned long addr)
写入b数据,大小为 8、16 或 32 位,到addr端口。
PIO 使用不同的指令集来访问 I/O 端口或 MMIO 的事实是一个缺点,因为 PIO 需要比正常内存更多的指令来完成相同的任务。例如,1 位测试在 MMIO 中只有一条指令,而 PIO 需要在测试位之前将数据读入寄存器,这不止一条指令。
MMIO 设备访问
内存映射 I/O 与内存驻留在相同的地址空间中。内核使用通常由 RAM(实际上是HIGH_MEM)使用的地址空间的一部分来映射设备寄存器,因此在该地址处不是真正的内存(即 RAM),而是 I/O 设备。因此,与 I/O 设备通信就像读写专门用于该 I/O 设备的内存地址一样。
与 PIO 一样,有 MMIO 函数,用于通知内核我们打算使用内存区域。请记住,这只是纯粹的保留。这些是request_mem_region()和release_mem_region():
struct resource* request_mem_region(unsigned long start,
unsigned long len, char *name)
void release_mem_region(unsigned long start, unsigned long len)
这也是一种礼貌。
可以通过读取/proc/iomem文件的内容来显示系统上实际使用的内存区域。
在访问内存区域之前(并且在成功请求之后),必须通过调用特定于体系结构的函数(利用 MMU 构建页表,因此不能从中断处理程序中调用)将该区域映射到内核地址空间。这些是ioremap()和iounmap(),它们也处理缓存一致性:
void __iomem *ioremap(unsigned long phys_add, unsigned long size)
void iounmap(void __iomem *addr)
ioremap()返回一个__iomem void指针,指向映射区域的起始位置。不要试图对这些指针进行解引用(通过读/写指针来获取/设置值)。内核提供了访问 ioremap'ed 内存的函数。这些是:
unsigned int ioread8(void __iomem *addr);
unsigned int ioread16(void __iomem *addr);
unsigned int ioread32(void __iomem *addr);
void iowrite8(u8 value, void __iomem *addr);
void iowrite16(u16 value, void __iomem *addr);
void iowrite32(u32 value, void __iomem *addr);
ioremap构建新的页表,就像vmalloc一样。但是,它实际上不分配任何内存,而是返回一个特殊的虚拟地址,可以用来访问指定的物理地址范围。
在 32 位系统上,MMIO 窃取物理内存地址空间以为内存映射 I/O 设备创建映射是一个缺点,因为它阻止系统将被窃取的内存用于一般 RAM 目的。
__iomem cookie
__iomem是 Sparse 使用的内核 cookie,Sparse 是内核用于查找可能的编码错误的语义检查器。要利用 Sparse 提供的功能,应该在内核编译时启用它;如果没有,__iomem cookie 将被忽略。
在命令行中,C=1将为您启用 Sparse,但是首先应该在系统上安装 parse:
sudo apt-get install sparse
例如,在构建模块时,请使用:
make -C $KPATH M=$PWD C=1 modules
或者,如果 makefile 编写得很好,只需键入:
make C=1
以下显示了内核中如何定义__iomem:
#define __iomem __attribute__((noderef, address_space(2)))
这可以防止错误的驱动程序执行 I/O 内存访问。为所有 I/O 访问添加__iomem也是一种更严格的方式。因为即使在具有 MMU 的系统上,I/O 访问也是通过虚拟内存进行的,这个标记可以防止我们使用绝对物理地址,并要求我们使用ioremap(),它将返回一个带有__iomem标记的虚拟地址:
void __iomem *ioremap(phys_addr_t offset, unsigned long size);
因此,我们可以使用专用函数,例如ioread32()和iowrite32()。你可能会想为什么不使用readl()/writel()函数。这些已经被弃用,因为它们不进行健全性检查,比ioreadX()/iowriteX()系列函数(只接受__iomem地址)更不安全(不需要__iomem)。
此外,noderef是 Sparse 用来确保程序员不对__iomem指针进行解引用的属性。即使在某些架构上可能有效,也不鼓励这样做。而是使用特殊的ioreadX()/iowriteX()函数。它是可移植的,并且适用于每种架构。现在让我们看看当对__iomem指针进行解引用时,Sparse 将如何警告我们:
#define BASE_ADDR 0x20E01F8
void * _addrTX = ioremap(BASE_ADDR, 8);
首先,Sparse 不高兴是因为错误的类型初始化程序:
warning: incorrect type in initializer (different address spaces)
expected void *_addrTX
got void [noderef] <asn:2>*
或者:
u32 __iomem* _addrTX = ioremap(BASE_ADDR, 8);
*_addrTX = 0xAABBCCDD; /* bad. No dereference */
pr_info("%x\n", *_addrTX); /* bad. No dereference */
Sparse 仍然不高兴:
Warning: dereference of noderef expression
最后一个例子让 Sparse 很高兴:
void __iomem* _addrTX = ioremap(BASE_ADDR, 8);
iowrite32(0xAABBCCDD, _addrTX);
pr_info("%x\n", ioread32(_addrTX));
你必须记住的两条规则是:
-
无论是作为返回类型还是作为参数类型,都要在需要的地方始终使用
__iomem,并使用 Sparse 来确保你这样做了 -
不要对
__iomem指针进行解引用;而是使用专用函数
内存(重新)映射
内核内存有时需要重新映射,无论是从内核到用户空间,还是从内核到内核空间。常见用例是将内核内存重新映射到用户空间,但还有其他情况,例如需要访问高内存。
kmap
Linux 内核将其地址空间的前 896MB 永久映射到物理内存的前 896MB(低内存)。在 4GB 系统上,内核只剩下 128MB 来映射剩下的 3.2GB 物理内存(高内存)。低内存可以直接被内核寻址,因为有永久的一对一映射。当涉及到高内存(896MB 以上的内存)时,内核必须将所请求的高内存区域映射到其地址空间中,之前提到的 128MB 是专门为此保留的。用于执行此技巧的函数kmap()。kmap()用于将给定页面映射到内核地址空间中。
void *kmap(struct page *page);
page是要映射的struct page结构的指针。当分配高内存页面时,它是不可直接寻址的。kmap()是必须调用的函数,用于将高内存临时映射到内核地址空间中。映射将持续到调用kunmap()为止:
void kunmap(struct page *page);
临时地意味着一旦不再需要,映射就应该被取消。记住,128MB 不足以映射 3.2GB。最佳的编程实践是在不再需要时取消高内存映射。这就是为什么在每次访问高内存页面时都必须输入kmap()-kunmap()序列。
这个函数适用于高内存和低内存。也就是说,如果页面结构位于低内存中,那么只返回页面的虚拟地址(因为低内存页面已经有了永久映射)。如果页面属于高内存,内核的页表中将创建永久映射并返回地址:
void *kmap(struct page *page)
{
BUG_ON(in_interrupt());
if (!PageHighMem(page))
return page_address(page);
return kmap_high(page);
}
将内核内存映射到用户空间
映射物理地址是最有用的功能之一,特别是在嵌入式系统中。有时你可能想要与用户空间共享部分内核内存。如前所述,当在用户空间运行时,CPU 以非特权模式运行。为了让一个进程访问内核内存区域,我们需要将该区域重新映射到进程地址空间中。
使用remap_pfn_range
remap_pfn_range()将物理内存(通过内核逻辑地址)映射到用户空间进程。它特别适用于实现mmap()系统调用。
在文件(无论是设备文件还是其他文件)上调用mmap()系统调用后,CPU 将切换到特权模式,并运行相应的file_operations.mmap()内核函数,然后调用remap_pfn_range()。映射区域的内核 PTE 将被派生,并提供给进程,当然,具有不同的保护标志。进程的 VMA 列表将更新为一个新的 VMA 条目(具有适当的属性),它将使用 PTE 来访问相同的内存。
因此,内核只是复制 PTE 而不是浪费内存。但是,内核空间和用户空间的 PTE 具有不同的属性。remap_pfn_range()具有以下原型:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t flags);
成功调用将返回0,失败时返回负错误代码。大多数remap_pfn_range()的参数在调用mmap()方法时提供。
-
vma:这是内核在file_operations.mmap()调用的情况下提供的虚拟内存区域。它对应于用户进程的vma,其中应该进行映射。 -
addr:这是 VMA 应该开始的用户虚拟地址(vma->vm_start),这将导致从addr到addr + size之间的虚拟地址范围的映射。 -
pfn:这代表要映射的内核内存区域的页帧号。它对应于右移PAGE_SHIFT位的物理地址。应考虑vma偏移量(映射必须开始的对象中的偏移量)以产生 PFN。由于 VMA 结构的vm_pgoff字段包含以页数形式的偏移值,这正是您需要的(通过PAGE_SHIFT左移)以提取以字节形式的偏移量:offset = vma->vm_pgoff << PAGE_SHIFT)。最后,pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT。 -
size:这是被重新映射的区域的大小,以字节为单位。 -
prot:这代表新 VMA 请求的保护。驱动程序可以改变默认值,但应使用vma->vm_page_prot中的值作为骨架,使用 OR 运算符,因为用户空间已经设置了一些位。其中一些标志是: -
VM_IO,指定设备的内存映射 I/O -
VM_DONTCOPY,告诉内核在 fork 时不要复制这个vma -
VM_DONTEXPAND,防止vma通过mremap(2)扩展 -
VM_DONTDUMP,防止vma被包含在核心转储中。
如果使用 I/O 内存,可能需要修改此值以禁用缓存(vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);)。
使用 io_remap_pfn_range
讨论的remap_pfn_range()函数在将 I/O 内存映射到用户空间时不再适用。在这种情况下,适当的函数是io_remap_pfn_range(),其参数相同。唯一改变的是 PFN 的来源。它的原型看起来像:
int io_remap_page_range(struct vm_area_struct *vma,
unsigned long virt_addr,
unsigned long phys_addr,
unsigned long size, pgprot_t prot);
在尝试将 I/O 内存映射到用户空间时,无需使用ioremap()。-ioremap()是为内核目的而设计的(将 I/O 内存映射到内核地址空间),而io_remap_pfn_range是为用户空间目的而设计的。
只需将真实的物理 I/O 地址(通过PAGE_SHIFT右移产生 PFN)直接传递给io_remap_pfn_range()。即使有一些体系结构将io_remap_pfn_range()定义为remap_pfn_range(),也有其他体系结构不是这种情况。出于可移植性的考虑,您应该只在 PFN 参数指向 RAM 的情况下使用remap_pfn_range(),并在phys_addr指向 I/O 内存的情况下使用io_remap_pfn_range()。
mmap 文件操作
内核mmap函数是struct file_operations结构的一部分,当用户执行系统调用mmap(2)时执行,用于将物理内存映射到用户虚拟地址。内核将对该映射区域的任何访问通过通常的指针解引用转换为文件操作。甚至可以直接将设备物理内存映射到用户空间(参见/dev/mem)。基本上,写入内存就像写入文件一样。这只是一种更方便的调用write()的方式。
通常,用户空间进程出于安全目的不能直接访问设备内存。因此,用户空间进程使用mmap()系统调用请求内核将设备映射到调用进程的虚拟地址空间中。映射后,用户空间进程可以通过返回的地址直接写入设备内存。
mmap 系统调用声明如下:
mmap (void *addr, size_t len, int prot,
int flags, int fd, ff_t offset);
驱动程序应该已经定义了 mmap 文件操作(file_operations.mmap)以支持mmap(2)。从内核方面来看,驱动程序文件操作结构(struct file_operations结构)中的 mmap 字段具有以下原型:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
其中:
-
filp是指向由 fd 参数翻译而来的驱动程序的打开设备文件的指针。 -
vma由内核分配并作为参数给出。它是指向映射应该放置的用户进程的 vma 的指针。为了理解内核如何创建新的 vma,让我们回顾一下mmap(2)系统调用的原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
该函数的参数在某种程度上影响了 vma 的一些字段:
-
addr:是映射应该开始的用户空间虚拟地址。它对vma>vm_start有影响。如果指定了NULL(最便携的方式),则自动确定正确的地址。 -
length:这指定了映射的长度,并间接影响了vma->vm_end。请记住,vma的大小始终是PAGE_SIZE的倍数。换句话说,PAGE_SIZE始终是vma可以具有的最小大小。内核将始终更改vma的大小,使其成为PAGE_SIZE的倍数。
If length <= PAGE_SIZE
vma->vm_end - vma->vm_start == PAGE_SIZE.
If PAGE_SIZE < length <= (N * PAGE_SIZE)
vma->vm_end - vma->vm_start == (N * PAGE_SIZE)
-
prot:这影响了 VMA 的权限,驱动程序可以在vma->vm_pro中找到。如前所述,驱动程序可以更新这些值,但不能更改它们。 -
flags:这确定了驱动程序可以在vma->vm_flags中找到的映射类型。映射可以是私有的或共享的。 -
offset:这指定了映射区域内的偏移量,从而操纵了vma->vm_pgoff的值。
在内核中实现 mmap
由于用户空间代码无法访问内核内存,mmap()函数的目的是派生一个或多个受保护的内核页表条目(对应于要映射的内存),并复制用户空间页表,删除内核标志保护,并设置允许用户访问与内核相同内存的权限标志,而无需特殊权限。
编写 mmap 文件操作的步骤如下:
- 获取映射偏移并检查它是否超出我们的缓冲区大小:
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
if (offset >= buffer_size)
return -EINVAL;
- 检查映射大小是否大于我们的缓冲区大小:
unsigned long size = vma->vm_end - vma->vm_start;
if (size > (buffer_size - offset))
return -EINVAL;
- 获取与我们缓冲区的
偏移位置所在页面的 PFN 相对应的 PFN:
unsigned long pfn;
/* we can use page_to_pfn on the struct page structure
* returned by virt_to_page
*/
/* pfn = page_to_pfn (virt_to_page (buffer + offset)); */
/* Or make PAGE_SHIFT bits right-shift on the physical
* address returned by virt_to_phys
*/
pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT;
- 设置适当的标志,无论 I/O 内存是否存在:
-
-
- 使用
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot)禁用缓存。
- 使用
-
-
设置
VM_IO标志:vma->vm_flags |= VM_IO。 -
防止 VMA 交换出去:
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP。在 3.7 版本之前的内核中,应该只使用VM_RESERVED标志。
- 使用计算的 PFN、大小和保护标志调用
remap_pfn_range:
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
- 将 mmap 函数传递给
struct file_operations结构:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
[...]
.mmap = my_mmap,
[...]
};
Linux 缓存系统
缓存是频繁访问或新写入数据从或写入到一个小而更快的内存,称为缓存的过程。
脏内存是指数据支持(例如,文件支持)的内存,其内容已被修改(通常在缓存中),但尚未写回磁盘。数据的缓存版本比磁盘上的版本更新,这意味着两个版本不同步。缓存数据被写回磁盘的机制(后备存储)称为写回。我们最终会更新磁盘上的版本,使两者同步。干净内存是文件支持的内存,其中的内容与磁盘同步。
Linux 延迟写操作以加快读取过程,并通过仅在必要时写入数据来减少磁盘磨损。一个典型的例子是dd命令。其完整执行并不意味着数据被写入目标设备;这就是为什么dd在大多数情况下都与sync命令链接在一起的原因。
什么是缓存?
缓存是临时的、小型的、快速的内存,用于保存来自更大且通常非常慢的内存的数据副本,通常放置在数据工作集远远超过其他数据的系统中(例如,硬盘、内存)。
当第一次读取发生时,比如一个进程从大而慢的磁盘请求一些数据,请求的数据被返回给进程,并且访问的数据的副本也被跟踪并缓存。任何后续的读取都将从缓存中获取数据。任何数据修改都将应用于缓存,而不是主磁盘上。然后,内容已被修改并且与磁盘上的版本不同(更新)的缓存区域将被标记为脏。当缓存运行满时,由于缓存的数据被跟踪,新数据开始驱逐那些未被访问并且已经闲置了最长时间的数据,因此如果需要再次访问,它将不得不从大而慢的存储中获取。
CPU 缓存 - 内存缓存
现代 CPU 上有三个缓存内存,按大小和访问速度排序:
-
L1缓存具有最少的内存(通常在 1k 到 64k 之间),可以在单个时钟周期内直接被 CPU 访问,这使得它也是最快的。经常使用的东西在 L1 中,并且保持在 L1 中,直到其他东西的使用比现有的更频繁,并且 L1 中的空间较少。如果是这样,它将被移动到更大的 L2 空间。
-
L2缓存是中间层,具有更多的内存(高达几兆字节),毗邻处理器,可以在少量时钟周期内访问。这适用于从 L2 到 L3 的移动。
-
L3缓存,比 L1 和 L2 更慢,可能比主内存(RAM)快两倍。每个核心可能有自己的 L1 和 L2 缓存;因此,它们都共享 L3 缓存。大小和速度是每个缓存级别之间的主要标准变化:L1 < L2 < L3。例如,原始内存访问可能是 100 纳秒,而 L1 缓存访问可能是 0.5 纳秒。
一个现实生活中的例子是图书馆可能会在展示区放置几本最受欢迎的书籍的副本,以便快速方便地获取,但是还有一个大规模的档案馆,其中有更多的收藏品,但需要等待图书管理员去取。展示柜类似于缓存,档案馆则是大型、慢速的内存。
CPU 缓存解决的主要问题是延迟,这间接增加了吞吐量,因为访问未缓存的内存可能需要一段时间。
Linux 页面缓存 - 磁盘缓存
页面缓存,顾名思义,是 RAM 中页面的缓存,包含最近访问文件的块。RAM 充当了磁盘上页面的缓存。换句话说,它是文件内容的内核缓存。缓存的数据可以是常规文件系统文件、块设备文件或内存映射文件。每当调用read()操作时,内核首先检查数据是否存在于页面缓存中,如果找到则立即返回。否则,数据将从磁盘读取。
如果进程需要写入数据而不涉及任何缓存,必须使用O_SYNC标志,该标志保证write()命令在所有数据传输到磁盘之前不返回,或者O_DIRECT标志,该标志仅保证不会使用缓存进行数据传输。也就是说,O_DIRECT实际上取决于所使用的文件系统,不建议使用。
专用缓存(用户空间缓存)
-
Web 浏览器缓存:将经常访问的网页和图像存储到磁盘上,而不是从网络获取。而在线数据的第一次访问可能需要超过数百毫秒,第二次访问将在仅 10 毫秒内从缓存(在这种情况下是磁盘)中获取数据。
-
libc 或用户应用程序缓存:内存和磁盘缓存实现将尝试猜测您下次需要使用的内容,而浏览器缓存则会保留本地副本以备再次使用。
为什么要延迟将数据写入磁盘?
基本上有两个原因:
-
更好地利用磁盘特性;这是效率
-
允许应用程序在写入后立即继续;这是性能
例如,延迟磁盘访问并仅在达到一定大小时处理数据可能会提高磁盘性能,并减少 eMMC(在嵌入式系统上)的磨损平衡。每个块写入都合并为单个连续的写操作。此外,写入的数据被缓存,允许进程立即返回,以便任何后续读取都将从缓存中获取数据,从而使程序更具响应性。存储设备更喜欢少量大型操作,而不是多个小型操作。
通过稍后在永久存储上报告写操作,我们可以摆脱这些相对较慢的磁盘引入的延迟问题。
写缓存策略
根据缓存策略的不同,可以列举出几个好处:
-
减少数据访问的延迟,从而提高应用程序性能
-
改善存储寿命
-
减少系统工作负载
-
减少数据丢失的风险
缓存算法通常属于以下三种不同的策略之一:
-
写透 缓存,任何写操作都会自动更新内存缓存和永久存储。这种策略适用于不能容忍数据丢失的应用程序,以及写入数据然后频繁重新读取数据的应用程序(因为数据存储在缓存中,读取延迟低)。
-
写绕 缓存,类似于写透,不同之处在于它立即使缓存无效(这对系统也是昂贵的,因为任何写操作都会自动使缓存无效)。主要后果是任何后续读取都将从磁盘获取数据,这很慢,从而增加延迟。它防止缓存被充斥着随后不会被读取的数据。
-
Linux 采用第三种也是最后一种策略,称为写回缓存,它可以在发生更改时每次将数据写入缓存,而不更新主存储器中的相应位置。相反,页面缓存中的相应页面被标记为脏(这项任务由 MMU 使用 TLB 完成),并添加到由内核维护的所谓列表中。只有在指定的时间间隔或在特定条件下,数据才会被写入永久存储的相应位置。当页面中的数据与页面缓存中的数据保持最新时,内核会从列表中删除页面,并且它们不会被标记为脏。
-
在 Linux 系统中,可以从
/proc/meminfo下的Dirty中找到这个信息:
cat /proc/meminfo | grep Dirty
刷新线程
写回缓存将 I/O 数据操作推迟到页面缓存中。一组或内核线程,称为刷新线程,负责此操作。当满足以下任一情况时,脏页写回发生:
-
当空闲内存低于指定阈值时,以重新获得被脏页占用的内存。
-
当脏数据持续到特定时期。最旧的数据被写回磁盘,以确保脏数据不会无限期地保持脏。
-
当用户进程调用
sync()和fsync()系统调用时。这是按需写回。
设备管理的资源 - Devres
Devres 是一个内核设施,通过自动释放驱动程序中分配的资源来帮助开发人员。它简化了在init/probe/open函数中的错误处理。使用 devres,每个资源分配器都有其受管理的版本,它将负责资源释放和释放。
本节严重依赖于内核源树中的Documentation/driver-model/devres.txt文件,该文件处理 devres API 并列出支持的函数以及它们的描述。
使用资源管理函数分配的内存与设备相关联。devres 由与struct device相关联的任意大小的内存区域的链表组成。每个 devres 资源分配器将分配的资源插入列表中。资源保持可用,直到代码手动释放它,设备从系统中分离,或者驱动程序卸载。每个 devres 条目都与一个release函数相关联。释放 devres 有不同的方式。无论如何,在驱动程序分离时,所有 devres 条目都会被释放。在释放时,调用相关的释放函数,然后释放 devres 条目。
以下是驱动程序可用资源的列表:
-
私有数据结构的内存
-
中断(IRQs)
-
内存区域分配(
request_mem_region()) -
内存区域的 I/O 映射(
ioremap()) -
缓冲内存(可能带有 DMA 映射)
-
不同的框架数据结构:时钟、GPIO、PWM、USB 物理层、调节器、DMA 等
本章讨论的几乎每个函数都有其受管理版本。在大多数情况下,函数的受管理版本的名称是通过在原始函数名前加上devm来获得的。例如,devm_kzalloc()是kzalloc()的受管理版本。此外,参数保持不变,但向右移动,因为第一个参数是为其分配资源的设备结构。对于已经在其参数中给出了 struct device 的非受管理版本的函数,有一个例外:
void *kmalloc(size_t size, gfp_t flags)
void * devm_kmalloc(struct device *dev, size_t size, gfp_t gfp)
当设备从系统中分离或设备的驱动程序被卸载时,该内存会自动释放。如果不再需要,可以使用devm_kfree()释放内存。
旧的方法:
ret = request_irq(irq, my_isr, 0, my_name, my_data);
if(ret) {
dev_err(dev, "Failed to register IRQ.\n");
ret = -ENODEV;
goto failed_register_irq; /* Unroll */
}
正确的方法:
ret = devm_request_irq(dev, irq, my_isr, 0, my_name, my_data);
if(ret) {
dev_err(dev, "Failed to register IRQ.\n");
return -ENODEV; /* Automatic unroll */
}
总结
这一章是最重要的章节之一。它揭开了内核中的内存管理和分配(如何以及在哪里)。每个内存方面都有详细讨论,同时也解释了 dvres。缓存机制简要讨论,以便概述 I/O 操作时底层发生了什么。这是一个坚实的基础,引入和理解下一章,即处理 DMA 的章节。
第十二章:DMA - 直接内存访问
DMA 是计算机系统的一种功能,允许设备在没有 CPU 干预的情况下访问主系统内存 RAM,从而使它们能够专注于其他任务。通常用于加速网络流量,但支持任何类型的复制。
DMA 控制器是负责 DMA 管理的外围设备。它主要出现在现代处理器和微控制器中。DMA 是一种用于执行内存读写操作而不占用 CPU 周期的功能。当需要传输一块数据时,处理器将源地址、目的地址和总字节数提供给 DMA 控制器。DMA 控制器然后自动将数据从源传输到目的地,而不占用 CPU 周期。当剩余字节数达到零时,块传输结束。
在本章中,我们将涵盖以下主题:
-
一致和非一致的 DMA 映射,以及一致性问题
-
DMA 引擎 API
-
DMA 和 DT 绑定
设置 DMA 映射
对于任何类型的 DMA 传输,都需要提供源地址、目的地址以及要传输的字数。在外围 DMA 的情况下,外围的 FIFO 作为源或目的地。当外围作为源时,内部或外部的内存位置作为目的地地址。当外围作为目的地时,内部或外部的内存位置作为源地址。
对于外围 DMA,我们根据传输方向指定源或目的地。换句话说,DMA 传输需要合适的内存映射。这是我们将在以下部分讨论的内容。
缓存一致性和 DMA
如第十一章 内核内存管理 中所讨论的,最近访问的内存区域的副本存储在缓存中。DMA 内存也适用于这一点。事实上,两个独立设备之间共享的内存通常是缓存一致性问题的根源。缓存不一致是一个问题,因为其他设备可能不知道写入设备的更新。另一方面,缓存一致性确保每个写操作都似乎是瞬时发生的,因此共享相同内存区域的所有设备看到的是完全相同的变化序列。
在 LDD3 的以下摘录中,对一致性问题进行了详细说明:
让我们想象一个 CPU 配备了缓存和可以被设备直接使用 DMA 访问的外部内存。当 CPU 访问内存中的位置 X 时,当前值将存储在缓存中。对 X 的后续操作将更新 X 的缓存副本,但不会更新 X 的外部内存版本,假设是写回缓存。如果在下一次设备尝试访问 X 之前,缓存没有刷新到内存中,设备将收到 X 的旧值。同样,如果在设备将新值写入内存时,X 的缓存副本没有失效,那么 CPU 将操作 X 的旧值。
实际上有两种解决这个问题的方法:
-
基于硬件的解决方案。这样的系统是一致的系统。
-
一种基于软件的解决方案,其中操作系统负责确保缓存一致性。这样的系统称为非一致的系统。
DMA 映射
任何合适的 DMA 传输都需要合适的内存映射。DMA 映射包括分配 DMA 缓冲区并为其生成总线地址。设备实际上使用总线地址。总线地址是dma_addr_t类型的每个实例。
区分两种类型的映射:一致 DMA 映射和流式 DMA 映射。可以在多个传输中使用前者,它会自动解决缓存一致性问题。因此,它太昂贵了。流式映射有很多约束,并且不会自动解决一致性问题,尽管有解决方案,其中包括每次传输之间的几个函数调用。一致映射通常存在于驱动程序的生命周期中,而流式映射通常在 DMA 传输完成后取消映射。
当可以时应使用流式映射,当必须时应使用一致映射。
回到代码;主要头文件应包括以下内容以处理 DMA 映射:
#include <linux/dma-mapping.h>
一致映射
以下函数设置了一致映射:
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
该函数处理缓冲区的分配和映射,并返回该缓冲区的内核虚拟地址,该地址为size字节宽,并可被 CPU 访问。dev是您的设备结构。第三个参数是一个输出参数,指向相关的总线地址。映射的内存保证是物理连续的,flag确定应如何分配内存,通常是GFP_KERNEL或GFP_ATOMIC(如果我们处于原子上下文中)。
请注意,据说这种映射是:
-
一致(协调),因为它为执行 DMA 分配了设备的非缓存非缓冲内存
-
同步,因为设备或 CPU 的写入可以立即被任一方读取,而不必担心缓存一致性
为了释放映射,可以使用以下函数:
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);
这里的cpu_addr对应于dma_alloc_coherent()返回的内核虚拟地址。这种映射是昂贵的,它最少可以分配一页。实际上,它只分配 2 的幂次方的页数。页面的顺序是用int order = get_order(size)获得的。应该将此映射用于设备生命周期内持续的缓冲区。
流式 DMA 映射
流式映射有更多的约束,并且与一致映射不同,原因如下:
-
映射需要使用已经分配的缓冲区。
-
映射可以接受多个非连续和分散的缓冲区。
-
映射的缓冲区属于设备而不再属于 CPU。在 CPU 可以使用缓冲区之前,应首先取消映射(在
dma_unmap_single()或dma_unmap_sg()之后)。这是为了缓存目的。 -
对于写事务(CPU 到设备),驱动程序应在映射之前将数据放入缓冲区。
-
必须指定数据移入的方向,并且只能根据这个方向使用数据。
人们可能会想知道为什么在取消映射之前不应访问缓冲区。原因很简单:CPU 映射是可缓存的。用于流式映射的dma_map_*()系列函数将首先清除/使缓存无效,然后依赖 CPU 不访问它,直到相应的dma_unmap_*()。然后,CPU 可以访问缓冲区。
实际上有两种形式的流式映射:
-
单个缓冲区映射,只允许单页映射
-
分散/聚集映射,允许传递多个分散在内存中的缓冲区
对于任何映射,都应指定方向,通过include/linux/dma-direction.h中定义的enum dma_data_direction类型的符号:
enum dma_data_direction {
DMA_BIDIRECTIONAL = 0,
DMA_TO_DEVICE = 1,
DMA_FROM_DEVICE = 2,
DMA_NONE = 3,
};
单个缓冲区映射
这是用于偶尔映射的。可以使用以下设置单个缓冲区:
dma_addr_t dma_map_single(struct device *dev, void *ptr,
size_t size, enum dma_data_direction direction);
方向应该是DMA_TO_DEVICE,DMA_FROM_DEVICE或DMA_BIDIRECTIONAL,如前面的代码所述。ptr是缓冲区的内核虚拟地址,dma_addr_t是设备的返回总线地址。确保使用真正符合您需求的方向,而不仅仅是DMA_BIDIRECTIONAL。
应该使用以下方法释放映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction direction);
分散/聚集映射
Scatter/gather 映射是一种特殊类型的流 DMA 映射,其中可以一次传输多个缓冲区区域,而不是分别映射每个缓冲区并逐个传输它们。假设您有几个可能不是物理上连续的缓冲区,所有这些缓冲区都需要同时传输到设备或从设备传输。由于以下情况可能发生:
-
一个 readv 或 writev 系统调用
-
磁盘 I/O 请求
-
或者只是映射内核 I/O 缓冲区中的页面列表
内核将 scatterlist 表示为一个连贯的结构,struct scatterlist:
struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
unsigned int dma_length;
};
为了设置 scatterlist 映射,应该:
-
分配您的分散缓冲区。
-
创建 scatter 列表的数组,并使用
sg_set_buf()填充分配的内存。请注意,scatterlist 条目必须是页面大小(除了末尾)。 -
在 scatterlist 上调用
dma_map_sg()。 -
完成 DMA 后,调用
dma_unmap_sg()取消映射 scatterlist。
虽然可以通过分别映射每个缓冲区来一次性发送多个缓冲区的内容,但是 scatter/gather 可以通过将 scatterlist 的指针和长度一起发送到设备来一次性发送所有缓冲区的内容,长度是列表中的条目数:
u32 *wbuf, *wbuf2, *wbuf3;
wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf3 = kzalloc(SDMA_BUF_SIZE/2, GFP_DMA);
struct scatterlist sg[3];
sg_init_table(sg, 3);
sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE/2);
ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM);
适用于单缓冲区映射部分的相同规则也适用于 scatter/gather。

DMA scatter/gather
dma_map_sg()和dma_unmap_sg()负责缓存一致性。但是,如果需要使用相同的映射来在 DMA 传输之间访问(读/写)数据,则必须以适当的方式在每次传输之间同步缓冲区,通过dma_sync_sg_for_cpu()(如果 CPU 需要访问缓冲区)或dma_sync_sg_for_device()(如果是设备)。单个区域映射的类似函数是dma_sync_single_for_cpu()和dma_sync_single_for_device():
void dma_sync_sg_for_cpu(struct device *dev,
struct scatterlist *sg,
int nents,
enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev,
struct scatterlist *sg, int nents,
enum dma_data_direction direction);
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
size_t size,
enum dma_data_direction dir)
void dma_sync_single_for_device(struct device *dev,
dma_addr_t addr, size_t size,
enum dma_data_direction dir)
在缓冲区被取消映射后,无需再次调用前面的函数。您只需读取内容。
完成的概念
本节将简要描述完成和 DMA 传输使用的 API 的必要部分。有关完整的描述,请随时查看内核文档Documentation/scheduler/completion.txt。内核编程中的一个常见模式涉及启动当前线程之外的某些活动,然后等待该活动完成。
在等待缓冲区被使用时,完成是sleep()的一个很好的替代方案。它适用于感知数据,这正是 DMA 回调所做的。
使用完成需要这个头文件:
<linux/completion.h>
与其他内核设施数据结构一样,可以静态或动态地创建struct completion结构的实例:
- 静态声明和初始化如下:
DECLARE_COMPLETION(my_comp);
- 动态分配如下:
struct completion my_comp;
init_completion(&my_comp);
当驱动程序开始一些必须等待完成的工作(在我们的情况下是 DMA 事务)时,它只需将完成事件传递给wait_for_completion()函数:
void wait_for_completion(struct completion *comp);
当代码的其他部分决定完成已经发生(事务完成)时,它可以唤醒任何等待的人(实际上是需要访问 DMA 缓冲区的代码):
void complete(struct completion *comp);
void complete_all(struct completion *comp);
可以猜到,complete()只会唤醒一个等待的进程,而complete_all()会唤醒每一个等待该事件的进程。即使在调用wait_for_completion()之前调用complete(),完成也是以这样一种方式实现的,即它们将正常工作。
随着下一节中使用的代码示例,您将更好地了解这是如何工作的。
DMA 引擎 API
DMA 引擎是用于开发 DMA 控制器驱动程序的通用内核框架。DMA 的主要目标是在复制内存时卸载 CPU。通过使用通道,可以通过 DMA 引擎委托事务(I/O 数据传输)。DMA 引擎通过其驱动程序/API 公开一组通道,其他设备(从设备)可以使用这些通道。

DMA 引擎布局
在这里,我们将简单地介绍(从设备)API,这仅适用于从设备 DMA 使用。这里的强制性标头如下:
#include <linux/dmaengine.h>
从设备 DMA 的使用非常简单,包括以下步骤:
-
分配 DMA 从设备通道。
-
设置从设备和控制器特定参数。
-
获取事务的描述符。
-
提交事务。
-
发出挂起的请求并等待回调通知。
可以将 DMA 通道视为 I/O 数据传输的高速公路
分配 DMA 从设备通道
使用dma_request_channel()请求通道。其原型如下:
struct dma_chan *dma_request_channel(const dma_cap_mask_t *mask,
dma_filter_fn fn, void *fn_param);
mask是一个位图掩码,表示通道必须满足的功能。主要用于指定驱动程序需要执行的传输类型:
enum dma_transaction_type {
DMA_MEMCPY, /* Memory to memory copy */
DMA_XOR, /* Memory to memory XOR*/
DMA_PQ, /* Memory to memory P+Q computation */
DMA_XOR_VAL, /* Memory buffer parity check using XOR */
DMA_PQ_VAL, /* Memory buffer parity check using P+Q */
DMA_INTERRUPT, /* The device is able to generrate dummy transfer that will generate interrupts */
DMA_SG, /* Memory to memory scatter gather */
DMA_PRIVATE, /* channels are not to be used for global memcpy. Usually used with DMA_SLAVE */
DMA_SLAVE, /* Memory to device transfers */
DMA_CYCLIC, /* Device is ableto handle cyclic tranfers */
DMA_INTERLEAVE, /* Memoty to memory interleaved transfer */
}
dma_cap_zero()和dma_cap_set()函数用于清除掩码并设置我们需要的功能。例如:
dma_cap_mask my_dma_cap_mask;
struct dma_chan *chan;
dma_cap_zero(my_dma_cap_mask);
dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); /* Memory to memory copy */
chan = dma_request_channel(my_dma_cap_mask, NULL, NULL);
在上述摘录中,dma_filter_fn被定义为:
typedef bool (*dma_filter_fn)(struct dma_chan *chan,
void *filter_param);
如果filter_fn参数(可选)为NULL,dma_request_channel()将简单地返回满足能力掩码的第一个通道。否则,当掩码参数不足以指定必要的通道时,可以使用filter_fn例程作为系统中可用通道的过滤器。内核会为系统中的每个空闲通道调用filter_fn例程。在找到合适的通道时,filter_fn应返回DMA_ACK,这将标记给定通道为dma_request_channel()的返回值。
通过此接口分配的通道对调用者是独占的,直到调用dma_release_channel()为止:
void dma_release_channel(struct dma_chan *chan)
设置从设备和控制器特定参数
这一步引入了一个新的数据结构struct dma_slave_config,它表示 DMA 从设备通道的运行时配置。这允许客户端指定设置,例如 DMA 方向、DMA 地址、总线宽度、DMA 突发长度等。
int dmaengine_slave_config(struct dma_chan *chan,
struct dma_slave_config *config)
struct dma_slave_config结构如下:
/*
* Please refer to the complete description in
* include/linux/dmaengine.h
*/
struct dma_slave_config {
enum dma_transfer_direction direction;
phys_addr_t src_addr;
phys_addr_t dst_addr;
enum dma_slave_buswidth src_addr_width;
enum dma_slave_buswidth dst_addr_width;
u32 src_maxburst;
u32 dst_maxburst;
[...]
};
以下是结构中每个元素的含义:
direction:这表示数据当前是否应该在此从设备通道上进出。可能的值为:
/* dma transfer mode and direction indicator */
enum dma_transfer_direction {
DMA_MEM_TO_MEM, /* Async/Memcpy mode */
DMA_MEM_TO_DEV, /* From Memory to Device */
DMA_DEV_TO_MEM, /* From Device to Memory */
DMA_DEV_TO_DEV, /* From Device to Device */
[...]
};
-
src_addr:这是 DMA 从设备数据应该被读取(RX)的缓冲区的物理地址(实际上是总线地址)。如果源是内存,则此元素将被忽略。dst_addr是 DMA 从设备数据应该被写入(TX)的缓冲区的物理地址(实际上是总线地址),如果源是内存,则将被忽略。src_addr_width是 DMA 数据应该被读取的源(RX)寄存器的宽度(以字节为单位)。如果源是内存,根据架构的不同可能会被忽略。合法的值为 1、2、4 或 8。因此,dst_addr_width与src_addr_width相同,但用于目标(TX)。 -
任何总线宽度必顺为以下枚举值之一:
enum dma_slave_buswidth {
DMA_SLAVE_BUSWIDTH_UNDEFINED = 0,
DMA_SLAVE_BUSWIDTH_1_BYTE = 1,
DMA_SLAVE_BUSWIDTH_2_BYTES = 2,
DMA_SLAVE_BUSWIDTH_3_BYTES = 3,
DMA_SLAVE_BUSWIDTH_4_BYTES = 4,
DMA_SLAVE_BUSWIDTH_8_BYTES = 8,
DMA_SLAVE_BUSWIDTH_16_BYTES = 16,
DMA_SLAVE_BUSWIDTH_32_BYTES = 32,
DMA_SLAVE_BUSWIDTH_64_BYTES = 64,
};
src_maxburs:这是可以一次发送到设备的最大字数(在这里,将字视为src_addr_width成员的单位,而不是字节)。通常是 I/O 外围设备上 FIFO 深度的一半,以防止溢出。这可能适用于内存源,也可能不适用。dst_maxburst与src_maxburst相同,但用于目标。
例如:
struct dma_chan *my_dma_chan;
dma_addr_t dma_src, dma_dst;
struct dma_slave_config my_dma_cfg = {0};
/* No filter callback, neither filter param */
my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL);
/* scr_addr and dst_addr are ignored in this structure for mem to mem copy */
my_dma_cfg.direction = DMA_MEM_TO_MEM;
my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES;
dmaengine_slave_config(my_dma_chan, &my_dma_cfg);
char *rx_data, *tx_data;
/* No error check */
rx_data = kzalloc(BUFFER_SIZE, GFP_DMA);
tx_data = kzalloc(BUFFER_SIZE, GFP_DMA);
feed_data(tx_data);
/* get dma addresses */
dma_src_addr = dma_map_single(NULL, tx_data,
BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_dst_addr = dma_map_single(NULL, rx_data,
BUFFER_SIZE, DMA_MEM_TO_MEM);
在上述摘录中,我们调用dma_request_channel()函数以获取 DMA 通道的所有者芯片,然后调用dmaengine_slave_config()来应用其配置。调用dma_map_single()以映射 rx 和 tx 缓冲区,以便可以用于 DMA 的目的。
获取事务的描述符
如果您记得本节的第一步,当请求 DMA 通道时,返回值是struct dma_chan结构的一个实例。如果您查看include/linux/dmaengine.h中的定义,您会注意到它包含一个struct dma_device *device字段,表示提供通道的 DMA 设备(实际上是控制器)。这个控制器的内核驱动程序负责(这是内核 API 对 DMA 控制器驱动程序施加的规则)暴露一组函数来准备 DMA 事务,其中每个函数对应一个 DMA 事务类型(在第 1 步中枚举)。根据事务类型,您别无选择,只能选择专用函数。其中一些函数是:
-
device_prep_dma_memcpy():准备 memcpy 操作 -
device_prep_dma_sg():准备分散/聚集的 memcpy 操作 -
device_prep_dma_xor():进行 xor 操作 -
device_prep_dma_xor_val():准备 xor 验证操作 -
device_prep_dma_pq():准备 pq 操作 -
device_prep_dma_pq_val():准备 pqzero_sum 操作 -
device_prep_dma_memset():准备 memset 操作 -
device_prep_dma_memset_sg():对分散列表进行 memset 操作 -
device_prep_slave_sg():准备从属 DMA 操作 -
device_prep_interleaved_dma():以通用方式传输表达式
让我们看看drivers/dma/imx-sdma.c,这是 i.MX6 DMA 控制器(SDMA)驱动程序。这些函数中的每一个都返回一个指向struct dma_async_tx_descriptor结构的指针,对应于事务描述符。对于内存到内存的复制,可以使用device_prep_dma_memcpy:
struct dma_device *dma_dev = my_dma_chan->device;
struct dma_async_tx_descriptor *tx = NULL;
tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr,
dma_src_addr, BUFFER_SIZE, 0);
if (!tx) {
printk(KERN_ERR "%s: Failed to prepare DMA transfer\n",
__FUNCTION__);
/* dma_unmap_* the buffer */
}
实际上,我们应该使用dmaengine_prep_* DMA 引擎 API。只需注意,这些函数在内部执行了我们之前执行的操作。例如,对于内存到内存,可以使用device_prep_dma_memcpy()函数:
struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(
struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
size_t len, unsigned long flags)
我们的示例变成了:
struct dma_async_tx_descriptor *tx = NULL;
tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr,
dma_src_addr, BUFFER_SIZE, 0);
if (!tx) {
printk(KERN_ERR "%s: Failed to prepare DMA transfer\n",
__FUNCTION__);
/* dma_unmap_* the buffer */
}
请查看include/linux/dmaengine.h,在struct dma_device结构的定义中,看看所有这些挂钩是如何实现的。
提交事务
要将事务放入驱动程序的挂起队列中,可以使用dmaengine_submit()。一旦描述符准备好并添加了回调信息,就应该将其放在 DMA 引擎驱动程序的挂起队列中:
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
这个函数返回一个 cookie,可以用它来通过其他 DMA 引擎检查 DMA 活动的进度。dmaengine_submit()不会启动 DMA 操作,它只是将其添加到挂起队列中。如何启动事务将在下一步中讨论:
struct completion transfer_ok;
init_completion(&transfer_ok);
tx->callback = my_dma_callback;
/* Submit our dma transfer */
dma_cookie_t cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
printk(KERN_ERR "%s: Failed to start DMA transfer\n", __FUNCTION__);
/* Handle that */
[...]
}
发出挂起的 DMA 请求并等待回调通知
启动事务是 DMA 传输设置的最后一步。通过在通道的挂起队列上调用dma_async_issue_pending()来激活通道中的事务。如果通道空闲,则启动队列中的第一个事务,并排队后续事务。在 DMA 操作完成后,启动队列中的下一个事务,并触发一个 tasklet。这个 tasklet 负责调用客户驱动程序完成回调例程进行通知,如果设置:
void dma_async_issue_pending(struct dma_chan *chan);
一个示例可能如下所示:
dma_async_issue_pending(my_dma_chan);
wait_for_completion(&transfer_ok);
dma_unmap_single(my_dma_chan->device->dev, dma_src_addr,
BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_unmap_single(my_dma_chan->device->dev, dma_src_addr,
BUFFER_SIZE, DMA_MEM_TO_MEM);
/* Process buffer through rx_data and tx_data virtualaddresses. */
wait_for_completion()函数将阻塞,直到我们的 DMA 回调被调用,这将更新(完成)我们的完成变量,以便恢复先前被阻塞的代码。这是while (!done) msleep(SOME_TIME);的一个合适的替代方法。
static void my_dma_callback()
{
complete(transfer_ok);
return;
}
实际发出挂起事务的 DMA 引擎 API 函数是dmaengine_issue_pending(struct dma_chan *chan),它是对dma_async_issue_pending()的封装。
将所有内容放在一起 - NXP SDMA(i.MX6)
SDMA 引擎是 i.MX6 中的可编程控制器,每个外设在此控制器中都有自己的复制功能。使用此enum来确定它们的地址:
enum sdma_peripheral_type {
IMX_DMATYPE_SSI, /* MCU domain SSI */
IMX_DMATYPE_SSI_SP, /* Shared SSI */
IMX_DMATYPE_MMC, /* MMC */
IMX_DMATYPE_SDHC, /* SDHC */
IMX_DMATYPE_UART, /* MCU domain UART */
IMX_DMATYPE_UART_SP, /* Shared UART */
IMX_DMATYPE_FIRI, /* FIRI */
IMX_DMATYPE_CSPI, /* MCU domain CSPI */
IMX_DMATYPE_CSPI_SP, /* Shared CSPI */
IMX_DMATYPE_SIM, /* SIM */
IMX_DMATYPE_ATA, /* ATA */
IMX_DMATYPE_CCM, /* CCM */
IMX_DMATYPE_EXT, /* External peripheral */
IMX_DMATYPE_MSHC, /* Memory Stick Host Controller */
IMX_DMATYPE_MSHC_SP, /* Shared Memory Stick Host Controller */
IMX_DMATYPE_DSP, /* DSP */
IMX_DMATYPE_MEMORY, /* Memory */
IMX_DMATYPE_FIFO_MEMORY,/* FIFO type Memory */
IMX_DMATYPE_SPDIF, /* SPDIF */
IMX_DMATYPE_IPU_MEMORY, /* IPU Memory */
IMX_DMATYPE_ASRC, /* ASRC */
IMX_DMATYPE_ESAI, /* ESAI */
IMX_DMATYPE_SSI_DUAL, /* SSI Dual FIFO */
IMX_DMATYPE_ASRC_SP, /* Shared ASRC */
IMX_DMATYPE_SAI, /* SAI */
};
尽管通用 DMA 引擎 API,任何构造函数都可以提供自己的自定义数据结构。这适用于imx_dma_data结构,它是一个私有数据(用于描述需要使用的 DMA 设备类型),将传递给过滤回调中struct dma_chan的.private字段:
struct imx_dma_data {
int dma_request; /* DMA request line */
int dma_request2; /* secondary DMA request line */
enum sdma_peripheral_type peripheral_type;
int priority;
};
enum imx_dma_prio {
DMA_PRIO_HIGH = 0,
DMA_PRIO_MEDIUM = 1,
DMA_PRIO_LOW = 2
};
这些结构和枚举都是特定于 i.MX 的,并在include/linux/platform_data/dma-imx.h中定义。现在,让我们编写我们的内核 DMA 模块。它分配两个缓冲区(源和目标)。用预定义数据填充源,并执行事务以将 src 复制到 dst。可以通过使用来自用户空间的数据(copy_from_user())来改进此模块。此驱动程序受到 imx-test 软件包中提供的驱动程序的启发:
#include <linux/module.h>
#include <linux/slab.h> /* for kmalloc */
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/version.h>
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(3,0,35))
#include <linux/platform_data/dma-imx.h>
#else
#include <mach/dma.h>
#endif
#include <linux/dmaengine.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/delay.h>
static int gMajor; /* major number of device */
static struct class *dma_tm_class;
u32 *wbuf; /* source buffer */
u32 *rbuf; /* destinationn buffer */
struct dma_chan *dma_m2m_chan; /* our dma channel */
struct completion dma_m2m_ok; /* completion variable used in the DMA callback */
#define SDMA_BUF_SIZE 1024
让我们定义过滤函数。当请求 DMA 通道时,控制器驱动程序可能会在通道列表中进行查找。对于细粒度的查找,可以提供一个回调方法,该方法将在找到的每个通道上调用。然后由回调来选择要使用的合适通道:
static bool dma_m2m_filter(struct dma_chan *chan, void *param)
{
if (!imx_dma_is_general_purpose(chan))
return false;
chan->private = param;
return true;
}
imx_dma_is_general_purpose是一个特殊函数,用于检查控制器驱动程序的名称。open函数将分配缓冲区并请求 DMA 通道,给定我们的过滤函数作为回调:
int sdma_open(struct inode * inode, struct file * filp)
{
dma_cap_mask_t dma_m2m_mask;
struct imx_dma_data m2m_dma_data = {0};
init_completion(&dma_m2m_ok);
dma_cap_zero(dma_m2m_mask);
dma_cap_set(DMA_MEMCPY, dma_m2m_mask); /* Set channel capacities */
m2m_dma_data.peripheral_type = IMX_DMATYPE_MEMORY; /* choose the dma device type. This is proper to i.MX */
m2m_dma_data.priority = DMA_PRIO_HIGH; /* we need high priority */
dma_m2m_chan = dma_request_channel(dma_m2m_mask, dma_m2m_filter, &m2m_dma_data);
if (!dma_m2m_chan) {
printk("Error opening the SDMA memory to memory channel\n");
return -EINVAL;
}
wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!wbuf) {
printk("error wbuf !!!!!!!!!!!\n");
return -1;
}
rbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!rbuf) {
printk("error rbuf !!!!!!!!!!!\n");
return -1;
}
return 0;
}
release函数只是open函数的反向操作;它释放缓冲区并释放 DMA 通道:
int sdma_release(struct inode * inode, struct file * filp)
{
dma_release_channel(dma_m2m_chan);
dma_m2m_chan = NULL;
kfree(wbuf);
kfree(rbuf);
return 0;
}
在read函数中,我们只是比较源缓冲区和目标缓冲区,并通知用户结果。
ssize_t sdma_read (struct file *filp, char __user * buf,
size_t count, loff_t * offset)
{
int i;
for (i=0; i<SDMA_BUF_SIZE/4; i++) {
if (*(rbuf+i) != *(wbuf+i)) {
printk("Single DMA buffer copy falled!,r=%x,w=%x,%d\n", *(rbuf+i), *(wbuf+i), i);
return 0;
}
}
printk("buffer copy passed!\n");
return 0;
}
我们使用完成来在事务终止时得到通知(唤醒)。此回调在我们的事务完成后调用,并将我们的完成变量设置为完成状态:
static void dma_m2m_callback(void *data)
{
printk("in %s\n",__func__);
complete(&dma_m2m_ok);
return ;
}
在write函数中,我们用数据填充源缓冲区,执行 DMA 映射以获取与我们的源和目标缓冲区对应的物理地址,并调用device_prep_dma_memcpy来获取事务描述符。然后,将该事务描述符提交给 DMA 引擎,使用dmaengine_submit,这时并不执行我们的事务。只有在我们在 DMA 通道上调用dma_async_issue_pending后,我们的待处理事务才会被处理:
ssize_t sdma_write(struct file * filp, const char __user * buf,
size_t count, loff_t * offset)
{
u32 i;
struct dma_slave_config dma_m2m_config = {0};
struct dma_async_tx_descriptor *dma_m2m_desc; /* transaction descriptor */
dma_addr_t dma_src, dma_dst;
/* No copy_from_user, we just fill the source buffer with predefined data */
for (i=0; i<SDMA_BUF_SIZE/4; i++) {
*(wbuf + i) = 0x56565656;
}
dma_m2m_config.direction = DMA_MEM_TO_MEM;
dma_m2m_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
dmaengine_slave_config(dma_m2m_chan, &dma_m2m_config);
dma_src = dma_map_single(NULL, wbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE);
dma_dst = dma_map_single(NULL, rbuf, SDMA_BUF_SIZE, DMA_FROM_DEVICE);
dma_m2m_desc = dma_m2m_chan->device->device_prep_dma_memcpy(dma_m2m_chan, dma_dst, dma_src, SDMA_BUF_SIZE,0);
if (!dma_m2m_desc)
printk("prep error!!\n");
dma_m2m_desc->callback = dma_m2m_callback;
dmaengine_submit(dma_m2m_desc);
dma_async_issue_pending(dma_m2m_chan);
wait_for_completion(&dma_m2m_ok);
dma_unmap_single(NULL, dma_src, SDMA_BUF_SIZE, DMA_TO_DEVICE);
dma_unmap_single(NULL, dma_dst, SDMA_BUF_SIZE, DMA_FROM_DEVICE);
return 0;
}
struct file_operations dma_fops = {
open: sdma_open,
release: sdma_release,
read: sdma_read,
write: sdma_write,
};
完整的代码可在书的存储库中找到:chapter-12/imx-sdma/imx-sdma-single.c。还有一个模块,可以执行相同的任务,但使用分散/聚集映射:chapter-12/imx-sdma/imx-sdma-scatter-gather.c。
DMA DT 绑定
DMA 通道的 DT 绑定取决于 DMA 控制器节点,这是 SoC 相关的,一些参数(如 DMA 单元格)可能因 SoC 而异。此示例仅关注 i.MX SDMA 控制器,可以在内核源码中找到,位于Documentation/devicetree/bindings/dma/fsl-imx-sdma.txt。
使用者绑定
根据 SDMA 事件映射表,以下代码显示了 i.MX 6Dual/6Quad 外围设备的 DMA 请求信号:
uart1: serial@02020000 {
compatible = "fsl,imx6sx-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6SX_CLK_UART_IPG>,
<&clks IMX6SX_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
DMA 属性中的第二个单元格(25和26)对应于 DMA 请求/事件 ID。这些值来自 SoC 手册(在我们的情况下是 i.MX53)。请查看community.nxp.com/servlet/JiveServlet/download/614186-1-373516/iMX6_Firmware_Guide.pdf和 Linux 参考手册community.nxp.com/servlet/JiveServlet/download/614186-1-373515/i.MX_Linux_Reference_Manual.pdf。
第三个单元格指示要使用的优先级。请求指定参数的驱动程序代码在下面定义。可以在内核源树的drivers/tty/serial/imx.c中找到完整的代码:
static int imx_uart_dma_init(struct imx_port *sport)
{
struct dma_slave_config slave_config = {};
struct device *dev = sport->port.dev;
int ret;
/* Prepare for RX : */
sport->dma_chan_rx = dma_request_slave_channel(dev, "rx");
if (!sport->dma_chan_rx) {
[...] /* cannot get the DMA channel. handle error */
}
slave_config.direction = DMA_DEV_TO_MEM;
slave_config.src_addr = sport->port.mapbase + URXD0;
slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
/* one byte less than the watermark level to enable the aging timer */
slave_config.src_maxburst = RXTL_DMA - 1;
ret = dmaengine_slave_config(sport->dma_chan_rx, &slave_config);
if (ret) {
[...] /* handle error */
}
sport->rx_buf = kzalloc(PAGE_SIZE, GFP_KERNEL);
if (!sport->rx_buf) {
[...] /* handle error */
}
/* Prepare for TX : */
sport->dma_chan_tx = dma_request_slave_channel(dev, "tx");
if (!sport->dma_chan_tx) {
[...] /* cannot get the DMA channel. handle error */
}
slave_config.direction = DMA_MEM_TO_DEV;
slave_config.dst_addr = sport->port.mapbase + URTX0;
slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
slave_config.dst_maxburst = TXTL_DMA;
ret = dmaengine_slave_config(sport->dma_chan_tx, &slave_config);
if (ret) {
[...] /* handle error */
}
[...]
}
这里的魔法调用是dma_request_slave_channel()函数,它将使用of_dma_request_slave_channel()解析设备节点(在 DT 中),以根据 DMA 名称(参考第六章中的命名资源,设备树的概念)收集通道设置。
摘要
DMA 是许多现代 CPU 中的一个功能。本章为您提供了使用内核 DMA 映射和 DMA 引擎 API 的必要步骤,以充分利用此设备。在本章之后,我毫无疑问您将能够设置至少一个内存到内存的 DMA 传输。可以在内核源树中的Documentation/dmaengine/中找到更多信息。因此,下一章涉及一个完全不同的主题——Linux 设备模型。
第十三章:Linux 设备模型
直到 2.5 版本,内核没有描述和管理对象的方法,代码的可重用性也不像现在这样增强。换句话说,没有设备拓扑结构,也没有组织。没有关于子系统关系或系统如何组合的信息。然后Linux 设备模型(LDM)出现了,引入了:
-
类的概念,用于将相同类型的设备或公开相同功能的设备(例如,鼠标和键盘都是输入设备)分组。
-
通过名为
sysfs的虚拟文件系统与用户空间通信,以便让用户空间管理和枚举设备及其公开的属性。 -
使用引用计数(在受管理资源中大量使用)管理对象生命周期。
-
电源管理,以处理设备应该关闭的顺序。
-
代码的可重用性。类和框架公开接口,行为类似于任何注册的驱动程序必须遵守的合同。
-
LDM 在内核中引入了类似于面向对象(OO)的编程风格。
在本章中,我们将利用 LDM 并通过sysfs文件系统向用户空间导出一些属性。
在本章中,我们将涵盖以下主题:
-
引入 LDM 数据结构(驱动程序,设备,总线)
-
按类型收集内核对象
-
处理内核
sysfs接口
LDM 数据结构
目标是构建一个完整的设备树,将系统上存在的每个物理设备映射到其中,并介绍它们的层次结构。已经创建了一个通用结构,用于表示可能是设备模型一部分的任何对象。LDM 的上一级依赖于内核中表示为struct bus_type实例的总线;设备驱动程序,表示为struct device_driver结构,以及设备,作为struct device结构的实例表示的最后一个元素。在本节中,我们将设计一个总线驱动程序包 bus,以深入了解 LDM 数据结构和机制。
总线
公共汽车是设备和处理器之间的通道链接。管理总线并向设备导出其协议的硬件实体称为总线控制器。例如,USB 控制器提供 USB 支持。I2C 控制器提供 I2C 总线支持。因此,总线控制器作为一个设备,必须像任何设备一样注册。它将是需要放在总线上的设备的父级。换句话说,每个放在总线上的设备必须将其父字段指向总线设备。总线在内核中由struct bus_type结构表示:
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs; /* use dev_groups instead */
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
struct subsys_private *p;
struct lock_class_key lock_key;
};
以下是结构中元素的含义:
-
match:这是一个回调,每当新设备或驱动程序添加到总线时都会调用。回调必须足够智能,并且在设备和驱动程序之间存在匹配时应返回非零值,这两者作为参数给出。match回调的主要目的是允许总线确定特定设备是否可以由给定驱动程序处理,或者其他逻辑,如果给定驱动程序支持给定设备。大多数情况下,验证是通过简单的字符串比较完成的(设备和驱动程序名称,或表和 DT 兼容属性)。对于枚举设备(PCI,USB),验证是通过比较驱动程序支持的设备 ID 与给定设备的设备 ID 进行的,而不会牺牲总线特定功能。 -
probe:这是在匹配发生后,当新设备或驱动程序添加到总线时调用的回调。此函数负责分配特定的总线设备结构,并调用给定驱动程序的probe函数,该函数应该管理之前分配的设备。 -
remove:当设备从总线中移除时调用此函数。 -
suspend:这是一种在总线上的设备需要进入睡眠模式时调用的方法。 -
resume:当总线上的设备需要被唤醒时调用此函数。 -
pm:这是总线的电源管理操作集,将调用特定设备驱动程序的pm-ops。 -
drv_groups:这是指向struct attribute_group元素列表(数组)的指针,每个元素都指向struct attribute元素列表(数组)。它代表总线上设备驱动程序的默认属性。传递给此字段的属性将赋予总线上注册的每个驱动程序。这些属性可以在/sys/bus/<bus-name>/drivers/<driver-name>中的驱动程序目录中找到。 -
dev_groups:这代表总线上设备的默认属性。通过传递给此字段的struct attribute_group元素的列表/数组,将赋予总线上注册的每个设备这些属性。这些属性可以在/sys/bus/<bus-name>/devices/<device-name>中的设备目录中找到。 -
bus_group:这保存总线注册到核心时自动添加的默认属性集(组)。
除了定义bus_type之外,总线控制器驱动程序还必须定义一个特定于总线的驱动程序结构,该结构扩展了通用的struct device_driver,以及一个特定于总线的设备结构,该结构扩展了通用的struct device结构,都是设备模型核心的一部分。总线驱动程序还必须为探测到的每个物理设备分配一个特定于总线的设备结构,并负责初始化设备的bus和parent字段,并将设备注册到 LDM 核心。这些字段必须指向总线设备和总线驱动程序中定义的bus_type结构。LDM 核心使用这些来构建设备层次结构并初始化其他字段。
在我们的示例中,以下是两个辅助宏,用于获取 packt 设备和 packt 驱动程序,给定通用的struct device和struct driver:
#define to_packt_driver(d) container_of(d, struct packt_driver, driver)
#define to_packt_device(d) container_of(d, struct packt_device, dev)
然后是用于识别 packt 设备的结构:
struct packt_device_id {
char name[PACKT_NAME_SIZE];
kernel_ulong_t driver_data; /* Data private to the driver */
};
以下是 packt 特定的设备和驱动程序结构:
/*
* Bus specific device structure
* This is what a packt device structure looks like
*/
struct packt_device {
struct module *owner;
unsigned char name[30];
unsigned long price;
struct device dev;
};
/*
* Bus specific driver structure
* This is what a packt driver structure looks like
* You should provide your device's probe and remove function.
* may be release too
*/
struct packt_driver {
int (*probe)(struct packt_device *packt);
int (*remove)(struct packt_device *packt);
void (*shutdown)(struct packt_device *packt);
};
每个总线内部管理两个重要列表;添加到总线上的设备列表和注册到总线上的驱动程序列表。每当添加/注册或移除/注销设备/驱动程序到/从总线时,相应的列表都会更新为新条目。总线驱动程序必须提供辅助函数来注册/注销可以处理该总线上设备的设备驱动程序,以及注册/注销坐在总线上的设备的辅助函数。这些辅助函数始终包装 LDM 核心提供的通用函数,即driver_register(),device_register(),driver_unregister和device_unregister()。
/*
* Now let us write and export symbols that people writing
* drivers for packt devices must use.
*/
int packt_register_driver(struct packt_driver *driver)
{
driver->driver.bus = &packt_bus_type;
return driver_register(&driver->driver);
}
EXPORT_SYMBOL(packt_register_driver);
void packt_unregister_driver(struct packt_driver *driver)
{
driver_unregister(&driver->driver);
}
EXPORT_SYMBOL(packt_unregister_driver);
int packt_device_register(struct packt_device *packt)
{
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
void packt_unregister_device(struct packt_device *packt)
{
device_unregister(&packt->dev);
}
EXPORT_SYMBOL(packt_device_unregister);
用于分配 packt 设备的函数如下。必须使用此函数来创建总线上任何物理设备的实例:
/*
* This function allocate a bus specific device structure
* One must call packt_device_register to register
* the device with the bus
*/
struct packt_device * packt_device_alloc(const char *name, int id)
{
struct packt_device *packt_dev;
int status;
packt_dev = kzalloc(sizeof *packt_dev, GFP_KERNEL);
if (!packt_dev)
return NULL;
/* new devices on the bus are son of the bus device */
strcpy(packt_dev->name, name);
packt_dev->dev.id = id;
dev_dbg(&packt_dev->dev,
"device [%s] registered with packt bus\n", packt_dev->name);
return packt_dev;
out_err:
dev_err(&adap->dev, "Failed to register packt client %s\n", packt_dev->name);
kfree(packt_dev);
return NULL;
}
EXPORT_SYMBOL_GPL(packt_device_alloc);
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
总线注册
总线控制器本身也是一个设备,在 99%的情况下总线是平台设备(即使提供枚举的总线也是如此)。例如,PCI 控制器是一个平台设备,它的相应驱动程序也是如此。必须使用bus_register(struct *bus_type)函数来注册总线到内核。packt 总线结构如下:
/*
* This is our bus structure
*/
struct bus_type packt_bus_type = {
.name = "packt",
.match = packt_device_match,
.probe = packt_device_probe,
.remove = packt_device_remove,
.shutdown = packt_device_shutdown,
};
总线控制器本身也是一个设备,它必须在内核中注册,并且将用作总线上设备的父设备。这是在总线控制器的probe或init函数中完成的。在 packt 总线的情况下,代码如下:
/*
* Bus device, the master.
*
*/
struct device packt_bus = {
.release = packt_bus_release,
.parent = NULL, /* Root device, no parent needed */
};
static int __init packt_init(void)
{
int status;
status = bus_register(&packt_bus_type);
if (status < 0)
goto err0;
status = class_register(&packt_master_class);
if (status < 0)
goto err1;
/*
* After this call, the new bus device will appear
* under /sys/devices in sysfs. Any devices added to this
* bus will shows up under /sys/devices/packt-0/.
*/
device_register(&packt_bus);
return 0;
err1:
bus_unregister(&packt_bus_type);
err0:
return status;
}
当总线控制器驱动程序注册设备时,设备的父成员必须指向总线控制器设备,其总线属性必须指向总线类型以构建物理 DT。要注册 packt 设备,必须调用packt_device_register,并将其分配为packt_device_alloc的参数:
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
设备驱动程序
全局设备层次结构允许以通用方式表示系统中的每个设备。这使得核心可以轻松地遍历 DT 以创建诸如适当排序的电源管理转换之类的东西:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
};
struct device_driver 定义了一组简单的操作,供核心对每个设备执行这些操作:
-
* name表示驱动程序的名称。它可以通过与设备名称进行比较来进行匹配。 -
* bus表示驱动程序所在的总线。总线驱动程序必须填写此字段。 -
module表示拥有驱动程序的模块。在 99% 的情况下,应将此字段设置为THIS_MODULE。 -
of_match_table是指向struct of_device_id数组的指针。struct of_device_id结构用于通过称为 DT 的特殊文件执行 OF 匹配,该文件在引导过程中传递给内核:
struct of_device_id {
char compatible[128];
const void *data;
};
-
suspend和resume回调提供电源管理功能。当设备从系统中物理移除或其引用计数达到0时,将调用remove回调。在系统重新启动期间也会调用remove回调。 -
probe是在尝试将驱动程序绑定到设备时运行的探测回调函数。总线驱动程序负责调用设备驱动程序的probe函数。 -
group是指向struct attribute_group列表(数组)的指针,用作驱动程序的默认属性。使用此方法而不是单独创建属性。
设备驱动程序注册
driver_register() 是用于在总线上注册设备驱动程序的低级函数。它将驱动程序添加到总线的驱动程序列表中。当设备驱动程序与总线注册时,核心会遍历总线的设备列表,并对每个没有与之关联驱动程序的设备调用总线的匹配回调,以找出驱动程序可以处理的设备。
当发生匹配时,设备和设备驱动程序被绑定在一起。将设备与设备驱动程序关联的过程称为绑定。
现在回到使用我们的 packt 总线注册驱动程序,必须使用 packt_register_driver(struct packt_driver *driver),这是对 driver_register() 的包装。在注册 packt 驱动程序之前,必须填写 *driver 参数。LDM 核心提供了用于遍历已注册到总线的驱动程序列表的辅助函数:
int bus_for_each_drv(struct bus_type * bus,
struct device_driver * start,
void * data, int (*fn)(struct device_driver *,
void *));
此助手遍历总线的驱动程序列表,并对列表中的每个驱动程序调用 fn 回调。
设备
结构体设备是用于描述和表征系统上每个设备的通用数据结构,无论其是否是物理设备。它包含有关设备的物理属性的详细信息,并提供适当的链接信息以构建合适的设备树和引用计数:
struct device {
struct device *parent;
struct kobject kobj;
const struct device_type *type;
struct bus_type *bus;
struct device_driver *driver;
void *platform_data;
void *driver_data;
struct device_node *of_node;
struct class *class;
const struct attribute_group **groups;
void (*release)(struct device *dev);
};
-
* parent表示设备的父级,用于构建设备树层次结构。当与总线注册时,总线驱动程序负责使用总线设备设置此字段。 -
* bus表示设备所在的总线。总线驱动程序必须填写此字段。 -
* type标识设备的类型。 -
kobj是处理引用计数和设备模型支持的 kobject。 -
* of_node是指向与设备关联的 OF(DT)节点的指针。由总线驱动程序设置此字段。 -
platform_data是指向特定于设备的平台数据的指针。通常在设备供应期间在特定于板的文件中声明。 -
driver_data是驱动程序的私有数据的指针。 -
class是指向设备所属类的指针。 -
* group是指向struct attribute_group列表(数组)的指针,用作设备的默认属性。使用此方法而不是单独创建属性。 -
release是在设备引用计数达到零时调用的回调。总线有责任设置此字段。packt 总线驱动程序向您展示了如何做到这一点。
设备注册
device_register是 LDM 核心提供的用于在总线上注册设备的函数。调用此函数后,将遍历驱动程序的总线列表以找到支持此设备的驱动程序,然后将此设备添加到总线的设备列表中。device_register()在内部调用device_add():
int device_add(struct device *dev)
{
[...]
bus_probe_device(dev);
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);
[...]
}
内核提供的用于遍历总线设备列表的辅助函数是bus_for_each_dev:
int bus_for_each_dev(struct bus_type * bus,
struct device * start, void * data,
int (*fn)(struct device *, void *));
每当添加设备时,核心都会调用总线驱动程序的匹配方法(bus_type->match)。如果匹配函数表示有驱动程序支持此设备,核心将调用总线驱动程序的probe函数(bus_type->probe),给定设备和驱动程序作为参数。然后由总线驱动程序调用设备的驱动程序的probe方法(driver->probe)。对于我们的 packt 总线驱动程序,用于注册设备的函数是packt_device_register(struct packt_device *packt),它在内部调用device_register,参数是使用packt_device_alloc分配的 packt 设备。
深入 LDM
LDM 在内部依赖于三个重要的结构,即 kobject、kobj_type 和 kset。让我们看看这些结构中的每一个如何参与设备模型。
kobject 结构
kobject 是设备模型的核心,运行在后台。它为内核带来了类似 OO 的编程风格,主要用于引用计数和公开设备层次结构和它们之间的关系。kobject 引入了封装常见对象属性的概念,如使用引用计数:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
/* Fields out of our interest have been removed */
};
-
name指向此 kobject 的名称。可以使用kobject_set_name(struct kobject *kobj, const char *name)函数来更改这个名称。 -
parent是指向此 kobject 父级的指针。它用于构建描述对象之间关系的层次结构。 -
sd指向一个struct sysfs_dirent结构,表示 sysfs 中此 kobject 的 inode 内部的结构。 -
kref提供了对 kobject 的引用计数。 -
ktype描述了对象,kset告诉我们这个对象属于哪个集合(组)。
每个嵌入 kobject 的结构都会嵌入并接收 kobject 提供的标准化函数。嵌入的 kobject 将使结构成为对象层次结构的一部分。
container_of宏用于获取 kobject 所属对象的指针。每个内核设备直接或间接地嵌入一个 kobject 属性。在添加到系统之前,必须使用kobject_create()函数分配 kobject,该函数将返回一个空的 kobject,必须使用kobj_init()进行初始化,给定分配和未初始化的 kobject 指针以及其kobj_type指针:
struct kobject *kobject_create(void)
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
kobject_add()函数用于将 kobject 添加和链接到系统,同时根据其层次结构创建其目录,以及其默认属性。反向函数是kobject_del():
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
kobject_create和kobject_add的反向函数是kobject_put。在书中提供的源代码中,将 kobject 绑定到系统的摘录是:
/* Somewhere */
static struct kobject *mykobj;
mykobj = kobject_create();
if (mykobj) {
kobject_init(mykobj, &mytype);
if (kobject_add(mykobj, NULL, "%s", "hello")) {
err = -1;
printk("ldm: kobject_add() failed\n");
kobject_put(mykobj);
mykobj = NULL;
}
err = 0;
}
可以使用kobject_create_and_add,它在内部调用kobject_create和kobject_add。drivers/base/core.c中的以下摘录显示了如何使用它:
static struct kobject * class_kobj = NULL;
static struct kobject * devices_kobj = NULL;
/* Create /sys/class */
class_kobj = kobject_create_and_add("class", NULL);
if (!class_kobj) {
return -ENOMEM;
}
/* Create /sys/devices */
devices_kobj = kobject_create_and_add("devices", NULL);
if (!devices_kobj) {
return -ENOMEM;
}
如果 kobject 有一个NULL父级,那么kobject_add会将父级设置为 kset。如果两者都是NULL,对象将成为顶级 sys 目录的子成员
kobj_type
struct kobj_type结构描述了 kobjects 的行为。kobj_type结构通过ktype字段描述了嵌入 kobject 的对象的类型。每个嵌入 kobject 的结构都需要一个相应的kobj_type,它将控制在创建和销毁 kobject 以及读取或写入属性时发生的情况。每个 kobject 都有一个struct kobj_type类型的字段,代表内核对象类型:
struct kobj_type {
void (*release)(struct kobject *);
const struct sysfs_ops sysfs_ops;
struct attribute **default_attrs;
};
struct kobj_type结构允许内核对象共享公共操作(sysfs_ops),无论这些对象是否在功能上相关。该结构的字段是有意义的。release是由kobject_put()函数调用的回调,每当需要释放对象时。您必须在这里释放对象持有的内存。可以使用container_of宏来获取对象的指针。sysfs_ops字段指向 sysfs 操作,而default_attrs定义了与此 kobject 关联的默认属性。sysfs_ops是一组在访问 sysfs 属性时调用的回调(sysfs 操作)。default_attrs是指向struct attribute元素列表的指针,将用作此类型的每个对象的默认属性:
struct sysfs_ops {
ssize_t (*show)(struct kobject *kobj,
struct attribute *attr, char *buf);
ssize_t (*store)(struct kobject *kobj,
struct attribute *attr,const char *buf,
size_t size);
};
show是当读取具有此kobj_type的任何 kobject 的属性时调用的回调。缓冲区大小始终为PAGE_SIZE,即使要显示的值是一个简单的char。应该设置buf的值(使用scnprintf),并在成功时返回实际写入缓冲区的数据的大小(以字节为单位),或者在失败时返回负错误。store用于写入目的。它的buf参数最多为PAGE_SIZE,但可以更小。它在成功时返回实际从缓冲区读取的数据的大小(以字节为单位),或者在失败时返回负错误(或者如果它收到一个不需要的值)。可以使用get_ktype来获取给定 kobject 的kobj_type:
struct kobj_type *get_ktype(struct kobject *kobj);
在书中的示例中,我们的k_type变量表示我们 kobject 的类型:
static struct sysfs_ops s_ops = {
.show = show,
.store = store,
};
static struct kobj_type k_type = {
.sysfs_ops = &s_ops,
.default_attrs = d_attrs,
};
这里,show和store回调定义如下:
static ssize_t show(struct kobject *kobj, struct attribute *attr, char *buf)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
printk( "LDM show: called for (%s) attr\n", da->attr.name );
return scnprintf(buf, PAGE_SIZE,
"%s: %d\n", da->attr.name, da->value);
}
static ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
sscanf(buf, "%d", &da->value);
printk("LDM store: %s = %d\n", da->attr.name, da->value);
return sizeof(int);
}
ksets
内核对象集(ksets)主要将相关的内核对象分组在一起。ksets 是 kobjects 的集合。换句话说,kset 将相关的 kobjects 聚集到一个地方,例如,所有块设备:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
};
-
list是 kset 中所有 kobject 的链表 -
list_lock是用于保护链表访问的自旋锁 -
kobj表示集合的基类
每个注册(添加到系统中)的 kset 对应一个 sysfs 目录。可以使用kset_create_and_add()函数创建和添加 kset,并使用kset_unregister()函数删除:
struct kset * kset_create_and_add(const char *name,
const struct kset_uevent_ops *u,
struct kobject *parent_kobj);
void kset_unregister (struct kset * k);
将 kobject 添加到集合中就像将其 kset 字段指定为正确的 kset 一样简单:
static struct kobject foo_kobj, bar_kobj;
example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj);
/*
* since we have a kset for this kobject,
* we need to set it before calling the kobject core.
*/
foo_kobj.kset = example_kset;
bar_kobj.kset = example_kset;
retval = kobject_init_and_add(&foo_kobj, &foo_ktype,
NULL, "foo_name");
retval = kobject_init_and_add(&bar_kobj, &bar_ktype,
NULL, "bar_name");
现在在模块的exit函数中,kobject 及其属性已被删除:
kset_unregister(example_kset);
属性
属性是由 kobjects 向用户空间导出的 sysfs 文件。属性表示可以从用户空间可读、可写或两者的对象属性。也就是说,每个嵌入struct kobject的数据结构可以公开由 kobject 本身提供的默认属性(如果有的话),也可以公开自定义属性。换句话说,属性将内核数据映射到 sysfs 中的文件。
属性定义如下:
struct attribute {
char * name;
struct module *owner;
umode_t mode;
};
用于从文件系统中添加/删除属性的内核函数是:
int sysfs_create_file(struct kobject * kobj,
const struct attribute * attr);
void sysfs_remove_file(struct kobject * kobj,
const struct attribute * attr);
让我们尝试定义两个我们将导出的属性,每个属性由一个属性表示:
struct d_attr {
struct attribute attr;
int value;
};
static struct d_attr foo = {
.attr.name="foo",
.attr.mode = 0644,
.value = 0,
};
static struct d_attr bar = {
.attr.name="bar",
.attr.mode = 0644,
.value = 0,
};
要单独创建每个枚举属性,我们必须调用以下内容:
sysfs_create_file(mykobj, &foo.attr);
sysfs_create_file(mykobj, &bar.attr);
属性的一个很好的起点是内核源码中的samples/kobject/kobject-example.c。
属性组
到目前为止,我们已经看到了如何单独添加属性,并在每个属性上调用(直接或间接通过包装函数,如device_create_file(),class_create_file()等)sysfs_create_file()。如果我们可以一次完成,为什么要自己处理多个调用呢?这就是属性组的作用。它依赖于struct attribute_group结构:
struct attribute_group {
struct attribute **attrs;
};
当然,我们已经删除了不感兴趣的字段。attr字段是指向属性列表/数组的指针。每个属性组必须给定一个指向struct attribute元素的列表/数组的指针。该组只是一个帮助包装器,使得更容易管理多个属性。
用于向文件系统添加/删除组属性的内核函数是:
int sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp)
void sysfs_remove_group(struct kobject * kobj,
const struct attribute_group * grp)
前面定义的两个属性可以嵌入到struct attribute_group中,只需一次调用即可将它们都添加到系统中:
static struct d_attr foo = {
.attr.name="foo",
.attr.mode = 0644,
.value = 0,
};
static struct d_attr bar = {
.attr.name="bar",
.attr.mode = 0644,
.value = 0,
};
/* attrs is a pointer to a list (array) of attributes */
static struct attribute * attrs [] =
{
&foo.attr,
&bar.attr,
NULL,
};
static struct attribute_group my_attr_group = {
.attrs = attrs,
};
在这里唯一需要调用的函数是:
sysfs_create_group(mykobj, &my_attr_group);
这比为每个属性都调用一次要好得多。
设备模型和 sysfs
Sysfs是一个非持久的虚拟文件系统,它提供了系统的全局视图,并通过它们的 kobjects 公开了内核对象的层次结构(拓扑)。每个 kobjects 显示为一个目录,目录中的文件表示由相关 kobject 导出的内核变量。这些文件称为属性,可以被读取或写入。
如果任何注册的 kobject 在 sysfs 中创建一个目录,那么目录的创建取决于 kobject 的父对象(也是一个 kobject)。自然而然地,目录被创建为 kobject 的父目录的子目录。这将内部对象层次结构突显到用户空间。sysfs 中的顶级目录表示对象层次结构的共同祖先,也就是对象所属的子系统。
顶级 sysfs 目录可以在/sys/目录下找到:
/sys$ tree -L 1
├── block
├── bus
├── class
├── dev
├── devices
├── firmware
├── fs
├── hypervisor
├── kernel
├── module
└── power
block包含系统上每个块设备的目录,每个目录包含设备上分区的子目录。bus包含系统上注册的总线。dev以原始方式包含注册的设备节点(无层次结构),每个都是指向/sys/devices目录中真实设备的符号链接。devices显示系统中设备的拓扑视图。firmware显示系统特定的低级子系统树,例如:ACPI、EFI、OF(DT)。fs列出系统上实际使用的文件系统。kernel保存内核配置选项和状态信息。Modules是已加载模块的列表。
这些目录中的每一个都对应一个 kobject,其中一些作为内核符号导出。这些是:
-
kernel_kobj对应于/sys/kernel -
power_kobj对应于/sys/power -
firmware_kobj对应于/sys/firmware,在drivers/base/firmware.c源文件中导出 -
hypervisor_kobj对应于/sys/hypervisor,在drivers/base/hypervisor.c中导出 -
fs_kobj对应于/sys/fs,在fs/namespace.c文件中导出
然而,class/、dev/、devices/是在内核源代码中的drivers/base/core.c中由devices_init函数在启动时创建的,block/是在block/genhd.c中创建的,bus/是在drivers/base/bus.c中作为 kset 创建的。
当将 kobject 目录添加到 sysfs(使用kobject_add)时,它被添加的位置取决于 kobject 的父位置。如果其父指针已设置,则将其添加为父目录中的子目录。如果父指针为空,则将其添加为kset->kobj中的子目录。如果父字段和 kset 字段都未设置,则映射到 sysfs 中的根级目录(/sys)。
可以使用sysfs_{create|remove}_link函数在现有对象(目录)上创建/删除符号链接:
int sysfs_create_link(struct kobject * kobj,
struct kobject * target, char * name);
void sysfs_remove_link(struct kobject * kobj, char * name);
这将允许一个对象存在于多个位置。创建函数将创建一个名为name的符号链接,指向target kobject sysfs 条目。一个众所周知的例子是设备同时出现在/sys/bus和/sys/devices中。创建的符号链接将在target被移除后仍然存在。您必须知道target何时被移除,然后删除相应的符号链接。
Sysfs 文件和属性
现在我们知道,默认的文件集是通过 kobjects 和 ksets 中的 ktype 字段提供的,通过kobj_type的default_attrs字段。默认属性在大多数情况下都足够了。但有时,ktype 的一个实例可能需要自己的属性来提供不被更一般的 ktype 共享的数据或功能。
只是一个提醒,用于在默认集合之上添加/删除新属性(或属性组)的低级函数是:
int sysfs_create_file(struct kobject *kobj,
const struct attribute *attr);
void sysfs_remove_file(struct kobject *kobj,
const struct attribute *attr);
int sysfs_create_group(struct kobject *kobj,
const struct attribute_group *grp);
void sysfs_remove_group(struct kobject * kobj,
const struct attribute_group * grp);
当前接口
目前在 sysfs 中存在接口层。除了创建自己的 ktype 或 kobject 以添加属性外,还可以使用当前存在的属性:设备、驱动程序、总线和类属性。它们的描述如下:
设备属性
除了设备结构中嵌入的默认属性之外,您还可以创建自定义属性。用于此目的的结构是struct device_attribute,它只是标准struct attribute的包装,并且一组回调函数来显示/存储属性的值:
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev,
struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count);
};
它们的声明是通过DEVICE_ATTR宏完成的:
DEVICE_ATTR(_name, _mode, _show, _store);
每当使用DEVICE_ATTR声明设备属性时,属性名称前缀dev_attr_将添加到属性名称中。例如,如果使用_name参数设置为 foo 来声明属性,则可以通过dev_attr_foo变量名称访问该属性。
要理解为什么,让我们看看DEVICE_ATTR宏在include/linux/device.h中是如何定义的:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
最后,您可以使用device_create_file和device_remove_file函数添加/删除这些:
int device_create_file(struct device *dev,
const struct device_attribute * attr);
void device_remove_file(struct device *dev,
const struct device_attribute * attr);
以下示例演示了如何将所有内容放在一起:
static ssize_t foo_show(struct device *child,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", foo_value);
}
static ssize_t bar_show(struct device *child,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", bar_value);
}
以下是属性的静态声明:
static DEVICE_ATTR(foo, 0644, foo_show, NULL);
static DEVICE_ATTR(bar, 0644, bar_show, NULL);
以下代码显示了如何在系统上实际创建文件:
if ( device_create_file(dev, &dev_attr_foo) != 0 )
/* handle error */
if ( device_create_file(dev, &dev_attr_bar) != 0 )
/* handle error*/
对于清理,属性的移除是在移除函数中完成的:
device_remove_file(wm->dev, &dev_attr_foo);
device_remove_file(wm->dev, &dev_attr_bar);
您可能会想知道我们是如何以前定义相同的存储/显示回调来处理相同 kobject/ktype 的所有属性,现在我们为每个属性使用自定义的回调。第一个原因是,设备子系统定义了自己的属性结构,它包装了标准属性结构,其次,它不是显示/存储属性的值,而是使用container_of宏来提取struct device_attribute,从而给出一个通用的struct attribute,然后根据用户操作执行 show/store 回调。以下是来自drivers/base/core.c的摘录,显示了设备 kobject 的sysfs_ops:
static ssize_t dev_attr_show(struct kobject *kobj,
struct attribute *attr,
char *buf)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;
if (dev_attr->show)
ret = dev_attr->show(dev, dev_attr, buf);
if (ret >= (ssize_t)PAGE_SIZE) {
print_symbol("dev_attr_show: %s returned bad count\n",
(unsigned long)dev_attr->show);
}
return ret;
}
static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count)
{
struct device_attribute *dev_attr = to_dev_attr(attr);
struct device *dev = kobj_to_dev(kobj);
ssize_t ret = -EIO;
if (dev_attr->store)
ret = dev_attr->store(dev, dev_attr, buf, count);
return ret;
}
static const struct sysfs_ops dev_sysfs_ops = {
.show = dev_attr_show,
.store = dev_attr_store,
};
原则对于总线(在drivers/base/bus.c中)、驱动程序(在drivers/base/bus.c中)和类(在drivers/base/class.c中)属性是相同的。它们使用container_of宏来提取其特定属性结构,然后调用其中嵌入的 show/store 回调。
总线属性
它依赖于struct bus_attribute结构:
struct bus_attribute {
struct attribute attr;
ssize_t (*show)(struct bus_type *, char * buf);
ssize_t (*store)(struct bus_type *, const char * buf, size_t count);
};
使用BUS_ATTR宏声明总线属性:
BUS_ATTR(_name, _mode, _show, _store)
使用BUS_ATTR声明的任何总线属性都将在属性变量名称中添加前缀bus_attr_:
#define BUS_ATTR(_name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store)
它们是使用bus_{create|remove}_file函数创建/删除的:
int bus_create_file(struct bus_type *, struct bus_attribute *);
void bus_remove_file(struct bus_type *, struct bus_attribute *);
设备驱动程序属性
所使用的结构是struct driver_attribute:
struct driver_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *, char * buf);
ssize_t (*store)(struct device_driver *, const char * buf,
size_t count);
};
声明依赖于DRIVER_ATTR宏,该宏将在属性变量名称中添加前缀driver_attr_:
DRIVER_ATTR(_name, _mode, _show, _store)
宏定义如下:
#define DRIVER_ATTR(_name, _mode, _show, _store) \
struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store)
创建/删除依赖于driver_{create|remove}_file函数:
int driver_create_file(struct device_driver *,
const struct driver_attribute *);
void driver_remove_file(struct device_driver *,
const struct driver_attribute *);
类属性
struct class_attribute是基本结构:
struct class_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *, char * buf);
ssize_t (*store)(struct device_driver *, const char * buf,
size_t count);
};
类属性的声明依赖于CLASS_ATTR:
CLASS_ATTR(_name, _mode, _show, _store)
正如宏的定义所示,使用CLASS_ATTR声明的任何类属性都将在属性变量名称中添加前缀class_attr_:
#define CLASS_ATTR(_name, _mode, _show, _store) \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)
最后,文件的创建和删除是使用class_{create|remove}_file函数完成的:
int class_create_file(struct class *class,
const struct class_attribute *attr);
void class_remove_file(struct class *class,
const struct class_attribute *attr);
请注意,device_create_file(),bus_create_file(),driver_create_file()和class_create_file()都会内部调用sysfs_create_file()。由于它们都是内核对象,它们的结构中嵌入了kobject。然后将该kobject作为参数传递给sysfs_create_file,如下所示:
int device_create_file(struct device *dev,
const struct device_attribute *attr)
{
[...]
error = sysfs_create_file(&dev->kobj, &attr->attr);
[...]
}
int class_create_file(struct class *cls,
const struct class_attribute *attr)
{
[...]
error =
sysfs_create_file(&cls->p->class_subsys.kobj,
&attr->attr);
return error;
}
int bus_create_file(struct bus_type *bus,
struct bus_attribute *attr)
{
[...]
error =
sysfs_create_file(&bus->p->subsys.kobj,
&attr->attr);
[...]
}
允许 sysfs 属性文件进行轮询
在这里,我们将看到如何避免进行 CPU 浪费的轮询以检测 sysfs 属性数据的可用性。想法是使用poll或select系统调用等待属性内容的更改。使 sysfs 属性可轮询的补丁是由Neil Brown和Greg Kroah-Hartman创建的。kobject 管理器(具有对 kobject 的访问权限的驱动程序)必须支持通知,以允许poll或select在内容更改时返回(被释放)。执行这一技巧的神奇函数来自内核侧,即sysfs_notify():
void sysfs_notify(struct kobject *kobj, const char *dir,
const char *attr)
如果dir参数非空,则用于查找包含属性的子目录(可能是由sysfs_create_group创建的)。每个属性的成本为一个int,每个 kobject 的wait_queuehead,每个打开文件一个 int。
poll将返回POLLERR|POLLPRI,而select将返回 fd,无论它是等待读取、写入还是异常。阻塞的 poll 来自用户端。只有在调整内核属性值后才应调用sysfs_notify()。
将poll()(或select())代码视为对感兴趣属性的更改通知的订阅者,并将sysfs_notify()视为发布者,通知订阅者任何更改。
以下是书中提供的代码摘录,这是属性的存储函数:
static ssize_t store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t len)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
sscanf(buf, "%d", &da->value);
printk("sysfs_foo store %s = %d\n", a->attr.name, a->value);
if (strcmp(a->attr.name, "foo") == 0){
foo.value = a->value;
sysfs_notify(mykobj, NULL, "foo");
}
else if(strcmp(a->attr.name, "bar") == 0){
bar.value = a->value;
sysfs_notify(mykobj, NULL, "bar");
}
return sizeof(int);
}
用户空间的代码必须像这样才能感知数据的更改:
-
打开文件属性。
-
对所有内容进行虚拟读取。
-
调用
poll请求POLLERR|POLLPRI(select/exceptfds 也可以)。 -
当
poll(或select)返回(表示值已更改)时,读取数据已更改的文件内容。 -
关闭文件并返回循环的顶部。
如果对 sysfs 属性是否可轮询存在疑问,请设置合适的超时值。书中提供了用户空间示例。
摘要
现在您已经熟悉了 LDM 概念及其数据结构(总线、类、设备驱动程序和设备),包括低级数据结构,即kobject、kset、kobj_types和属性(或这些属性的组合),内核中如何表示对象(因此 sysfs 和设备拓扑结构)不再是秘密。您将能够创建一个通过 sysfs 公开您的设备或驱动程序功能的属性(或组)。如果前面的话题对您来说很清楚,我们将转到下一个第十四章,引脚控制和 GPIO 子系统,该章节大量使用了sysfs的功能。
第十四章:引脚控制和 GPIO 子系统
大多数嵌入式 Linux 驱动程序和内核工程师都使用 GPIO 或玩转引脚复用。在这里,引脚指的是组件的输出线。SoC 会复用引脚,这意味着一个引脚可能有多个功能,例如,在arch/arm/boot/dts/imx6dl-pinfunc.h中的MX6QDL_PAD_SD3_DAT1可以是 SD3 数据线 1、UART1 的 cts/rts、Flexcan2 的 Rx 或普通的 GPIO。
选择引脚应该工作的模式的机制称为引脚复用。负责此功能的系统称为引脚控制器。在本章的第二部分中,我们将讨论通用输入输出(GPIO),这是引脚可以操作的特殊功能(模式)。
在本章中,我们将:
-
了解引脚控制子系统,并看看如何在 DT 中声明它们的节点
-
探索传统的基于整数的 GPIO 接口,以及新的基于描述符的接口 API
-
处理映射到 IRQ 的 GPIO
-
处理专用于 GPIO 的 sysfs 接口
引脚控制子系统
引脚控制(pinctrl)子系统允许管理引脚复用。在 DT 中,需要以某种方式复用引脚的设备必须声明它们需要的引脚控制配置。
引脚控制子系统提供:
-
引脚复用,允许重用同一引脚用于不同的目的,比如一个引脚可以是 UART TX 引脚、GPIO 线或 HSI 数据线。复用可以影响引脚组或单个引脚。
-
引脚配置,应用引脚的电子属性,如上拉、下拉、驱动器强度、去抖时间等。
本书的目的仅限于使用引脚控制器驱动程序导出的函数,并不涉及如何编写引脚控制器驱动程序。
引脚控制和设备树
引脚控制只是一种收集引脚(不仅仅是 GPIO)并将它们传递给驱动程序的方法。引脚控制器驱动程序负责解析 DT 中的引脚描述并在芯片中应用它们的配置。驱动程序通常需要一组两个嵌套节点来描述引脚配置的组。第一个节点描述组的功能(组将用于什么目的),第二个节点保存引脚配置。
引脚组在设备树中的分配严重依赖于平台,因此也依赖于引脚控制器驱动程序。每个引脚控制状态都被赋予一个从 0 开始的连续整数 ID。可以使用一个名称属性,它将映射到 ID 上,以便相同的名称始终指向相同的 ID。
每个客户设备自己的绑定确定了必须在其 DT 节点中定义的状态集,以及是否定义必须提供的状态 ID 集,或者是否定义必须提供的状态名称集。在任何情况下,可以通过两个属性将引脚配置节点分配给设备:
-
pinctrl-<ID>:这允许为设备的某个状态提供所需的 pinctrl 配置列表。这是一个 phandle 列表,每个 phandle 指向一个引脚配置节点。这些引用的引脚配置节点必须是它们配置的引脚控制器的子节点。此列表中可能存在多个条目,以便可以配置多个引脚控制器,或者可以从单个引脚控制器的多个节点构建状态,每个节点都为整体配置的一部分做出贡献。 -
pinctrl-name:这允许为列表中的每个状态提供一个名称。列表条目 0 定义整数状态 ID 0 的名称,列表条目 1 定义状态 ID 1 的名称,依此类推。状态 ID 0 通常被赋予名称default。标准化状态列表可以在include/linux/pinctrl/pinctrl-state.h中找到。 -
以下是 DT 的摘录,显示了一些设备节点以及它们的引脚控制节点:
usdhc@0219c000 { /* uSDHC4 */
non-removable;
vmmc-supply = <®_3p3v>;
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_usdhc4_1>;
};
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_io_foo &pinctrl_io_bar>;
};
iomuxc@020e0000 {
compatible = "fsl,imx6q-iomuxc";
reg = <0x020e0000 0x4000>;
/* shared pinctrl settings */
usdhc4 { /* first node describing the function */
pinctrl_usdhc4_1: usdhc4grp-1 { /* second node */
fsl,pins = <
MX6QDL_PAD_SD4_CMD__SD4_CMD 0x17059
MX6QDL_PAD_SD4_CLK__SD4_CLK 0x10059
MX6QDL_PAD_SD4_DAT0__SD4_DATA0 0x17059
MX6QDL_PAD_SD4_DAT1__SD4_DATA1 0x17059
MX6QDL_PAD_SD4_DAT2__SD4_DATA2 0x17059
MX6QDL_PAD_SD4_DAT3__SD4_DATA3 0x17059
MX6QDL_PAD_SD4_DAT4__SD4_DATA4 0x17059
MX6QDL_PAD_SD4_DAT5__SD4_DATA5 0x17059
MX6QDL_PAD_SD4_DAT6__SD4_DATA6 0x17059
MX6QDL_PAD_SD4_DAT7__SD4_DATA7 0x17059
>;
};
};
[...]
uart3 {
pinctrl_uart3_1: uart3grp-1 {
fsl,pins = <
MX6QDL_PAD_EIM_D24__UART3_TX_DATA 0x1b0b1
MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1
>;
};
};
// GPIOs (Inputs)
gpios {
pinctrl_io_foo: pinctrl_io_foo {
fsl,pins = <
MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x1f059
MX6QDL_PAD_DISP0_DAT13__GPIO5_IO07 0x1f059
>;
};
pinctrl_io_bar: pinctrl_io_bar {
fsl,pins = <
MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x1f059
MX6QDL_PAD_DISP0_DAT9__GPIO4_IO30 0x1f059
MX6QDL_PAD_DISP0_DAT7__GPIO4_IO28 0x1f059
MX6QDL_PAD_DISP0_DAT5__GPIO4_IO26 0x1f059
>;
};
};
};
在上面的示例中,引脚配置以<PIN_FUNCTION> <PIN_SETTING>的形式给出。例如:
MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x80000000
MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09表示引脚功能,在这种情况下是 GPIO,0x80000000表示引脚设置。
对于这一行,
MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1
MX6QDL_PAD_EIM_D25__UART3_RX_DATA表示引脚功能,即 UART3 的 RX 线,0x1b0b1表示设置。
引脚功能是一个宏,其值仅对引脚控制器驱动程序有意义。这些通常在位于arch/<arch>/boot/dts/中的头文件中定义。例如,如果使用的是 UDOO quad,它具有 i.MX6 四核(ARM),则引脚功能头文件将是arch/arm/boot/dts/imx6q-pinfunc.h。以下是与 GPIO5 控制器的第五行对应的宏:
#define MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x19c 0x4b0 0x000 0x5 0x0
<PIN_SETTING>可用于设置上拉电阻、下拉电阻、保持器、驱动强度等。如何指定它取决于引脚控制器绑定,其值的含义取决于 SoC 数据表,通常在 IOMUX 部分。在 i.MX6 IOMUXC 上,仅使用低于 17 位来实现此目的。
这些前置节点是从相应的驱动程序特定节点调用的。此外,这些引脚在相应的驱动程序初始化期间进行配置。在选择引脚组状态之前,必须首先使用pinctrl_get()函数获取引脚控制,调用pinctrl_lookup_state()来检查请求的状态是否存在,最后使用pinctrl_select_state()来应用状态。
以下是一个示例,显示如何获取 pincontrol 并应用其默认配置:
struct pinctrl *p;
struct pinctrl_state *s;
int ret;
p = pinctrl_get(dev);
if (IS_ERR(p))
return p;
s = pinctrl_lookup_state(p, name);
if (IS_ERR(s)) {
devm_pinctrl_put(p);
return ERR_PTR(PTR_ERR(s));
}
ret = pinctrl_select_state(p, s);
if (ret < 0) {
devm_pinctrl_put(p);
return ERR_PTR(ret);
}
通常在驱动程序初始化期间执行这些步骤。此代码的适当位置可以在probe()函数内。
pinctrl_select_state()在内部调用pinmux_enable_setting(),后者又在引脚控制节点中的每个引脚上调用pin_request()。
可以使用pinctrl_put()函数释放引脚控制。可以使用 API 的资源管理版本。也就是说,可以使用pinctrl_get_select(),给定要选择的状态的名称,以配置引脚控制。该函数在include/linux/pinctrl/consumer.h中定义如下:
static struct pinctrl *pinctrl_get_select(struct device *dev,
const char *name)
其中*name是pinctrl-name属性中写的状态名称。如果状态的名称是default,可以直接调用pinctr_get_select_default()函数,这是pinctl_get_select()的包装器:
static struct pinctrl * pinctrl_get_select_default(
struct device *dev)
{
return pinctrl_get_select(dev, PINCTRL_STATE_DEFAULT);
}
让我们看一个真实的例子,位于特定于板的 dts 文件(am335x-evm.dts)中:
dcan1: d_can@481d0000 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&d_can1_pins>;
};
以及相应的驱动程序:
pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl))
dev_warn(&pdev->dev,"pins are not configured from the driver\n");
当设备被探测时,引脚控制核心将自动为我们声明default pinctrl 状态。如果定义了init状态,引脚控制核心将在probe()函数之前自动将 pinctrl 设置为此状态,然后在probe()之后切换到default状态(除非驱动程序已经显式更改了状态)。
GPIO 子系统
从硬件角度来看,GPIO 是一种功能,是引脚可以操作的模式。从软件角度来看,GPIO 只是一个数字线,可以作为输入或输出,并且只能有两个值:(1表示高,0表示低)。内核 GPIO 子系统提供了您可以想象的每个功能,以便从驱动程序内部设置和处理 GPIO 线:
-
在驱动程序中使用 GPIO 之前,应该向内核声明它。这是一种获取 GPIO 所有权的方法,可以防止其他驱动程序访问相同的 GPIO。获取 GPIO 所有权后,可以进行以下操作:
-
设置方向
-
切换其输出状态(将驱动线设置为高电平或低电平)如果用作输出
-
如果用作输入,则设置去抖动间隔并读取状态。对于映射到中断请求的 GPIO 线,可以定义触发中断的边缘/电平,并注册一个处理程序,每当中断发生时就会运行。
实际上,内核中处理 GPIO 有两种不同的方式,如下所示:
-
使用整数表示 GPIO 的传统和已弃用的接口
-
新的和推荐的基于描述符的接口,其中 GPIO 由不透明结构表示和描述,具有专用 API
基于整数的 GPIO 接口:传统
基于整数的接口是最为人熟知的。GPIO 由一个整数标识,该标识用于对 GPIO 执行的每个操作。以下是包含传统 GPIO 访问函数的标头:
#include <linux/gpio.h>
内核中有众所周知的函数来处理 GPIO。
声明和配置 GPIO
可以使用gpio_request()函数分配和拥有 GPIO:
static int gpio_request(unsigned gpio, const char *label)
gpio表示我们感兴趣的 GPIO 编号,label是内核在 sysfs 中用于 GPIO 的标签,如我们在/sys/kernel/debug/gpio中所见。必须检查返回的值,其中0表示成功,错误时为负错误代码。完成 GPIO 后,应使用gpio_free()函数释放它:
void gpio_free(unsigned int gpio)
如果有疑问,可以使用gpio_is_valid()函数在分配之前检查系统上的 GPIO 编号是否有效:
static bool gpio_is_valid(int number)
一旦我们拥有了 GPIO,就可以根据需要改变它的方向,无论是输入还是输出,都可以使用gpio_direction_input()或gpio_direction_output()函数:
static int gpio_direction_input(unsigned gpio)
static int gpio_direction_output(unsigned gpio, int value)
gpio是我们需要设置方向的 GPIO 编号。在配置 GPIO 为输出时有第二个参数:value,这是一旦输出方向生效后 GPIO 应处于的状态。同样,返回值为零或负错误号。这些函数在内部映射到我们使用的 GPIO 控制器驱动程序公开的较低级别回调函数之上。在下一章第十五章,GPIO 控制器驱动程序-gpio_chip中,处理 GPIO 控制器驱动程序,我们将看到 GPIO 控制器必须通过其struct gpio_chip结构公开一组通用的回调函数来使用其 GPIO。
一些 GPIO 控制器提供更改 GPIO 去抖动间隔的可能性(仅当 GPIO 线配置为输入时才有用)。这个功能是平台相关的。可以使用int gpio_set_debounce()来实现这一点:
static int gpio_set_debounce(unsigned gpio, unsigned debounce)
其中debounce是以毫秒为单位的去抖时间。
所有前述函数应在可能休眠的上下文中调用。从驱动程序的probe函数中声明和配置 GPIO 是一个良好的实践。
访问 GPIO-获取/设置值
在访问 GPIO 时应注意。在原子上下文中,特别是在中断处理程序中,必须确保 GPIO 控制器回调函数不会休眠。设计良好的控制器驱动程序应该能够通知其他驱动程序(实际上是客户端)其方法是否可能休眠。可以使用gpio_cansleep()函数进行检查。
用于访问 GPIO 的函数都不返回错误代码。这就是为什么在 GPIO 分配和配置期间应注意并检查返回值的原因。
在原子上下文中
有一些 GPIO 控制器可以通过简单的内存读/写操作进行访问和管理。这些通常嵌入在 SoC 中,不需要休眠。对于这些控制器,gpio_cansleep()将始终返回false。对于这样的 GPIO,可以在 IRQ 处理程序中使用众所周知的gpio_get_value()或gpio_set_value()获取/设置它们的值,具体取决于 GPIO 线被配置为输入还是输出:
static int gpio_get_value(unsigned gpio)
void gpio_set_value(unsigned int gpio, int value);
当 GPIO 配置为输入(使用gpio_direction_input())时,应使用gpio_get_value(),并返回 GPIO 的实际值(状态)。另一方面,gpio_set_value()将影响 GPIO 的值,应该已经使用gpio_direction_output()配置为输出。对于这两个函数,value可以被视为布尔值,其中零表示低,非零值表示高。
在可能休眠的非原子上下文中
另一方面,还有 GPIO 控制器连接在 SPI 和 I2C 等总线上。由于访问这些总线的函数可能导致休眠,因此gpio_cansleep()函数应始终返回true(由 GPIO 控制器负责返回 true)。在这种情况下,您不应该在 IRQ 处理中访问这些 GPIO,至少不是在顶半部分(硬 IRQ)。此外,您必须使用作为通用访问的访问器应该以_cansleep结尾。
static int gpio_get_value_cansleep(unsigned gpio);
void gpio_set_value_cansleep(unsigned gpio, int value);
它们的行为与没有_cansleep()名称后缀的访问器完全相同,唯一的区别是它们在访问 GPIO 时阻止内核打印警告。
映射到 IRQ 的 GPIO
输入 GPIO 通常可以用作 IRQ 信号。这些 IRQ 可以是边沿触发或电平触发的。配置取决于您的需求。GPIO 控制器负责提供 GPIO 和其 IRQ 之间的映射。可以使用goio_to_irq()将给定的 GPIO 号码映射到其 IRQ 号码:
int gpio_to_irq(unsigned gpio);
返回值是 IRQ 号码,可以调用request_irq()(或线程化版本request_threaded_irq())来为此 IRQ 注册处理程序:
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
[...]
return IRQ_HANDLED;
}
[...]
int gpio_int = of_get_gpio(np, 0);
int irq_num = gpio_to_irq(gpio_int);
int error = devm_request_threaded_irq(&client->dev, irq_num,
NULL, my_interrupt_handler,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
input_dev->name, my_data_struct);
if (error) {
dev_err(&client->dev, "irq %d requested failed, %d\n",
client->irq, error);
return error;
}
将所有内容放在一起
以下代码是将所有讨论的关于基于整数的接口的概念付诸实践的摘要。该驱动程序管理四个 GPIO:两个按钮(btn1 和 btn2)和两个 LED(绿色和红色)。Btn1 映射到 IRQ,每当其状态变为 LOW 时,btn2 的状态将应用于 LED。例如,如果 btn1 的状态变为 LOW,而 btn2 的状态为高,则GREEN和RED led 将被驱动到 HIGH:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h> /* For Legacy integer based GPIO */
#include <linux/interrupt.h> /* For IRQ */
static unsigned int GPIO_LED_RED = 49;
static unsigned int GPIO_BTN1 = 115;
static unsigned int GPIO_BTN2 = 116;
static unsigned int GPIO_LED_GREEN = 120;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq,
void *dev_id, struct pt_regs *regs)
{
int state;
/* read BTN2 value and change the led state */
state = gpio_get_value(GPIO_BTN2);
gpio_set_value(GPIO_LED_RED, state);
gpio_set_value(GPIO_LED_GREEN, state);
pr_info("GPIO_BTN1 interrupt: Interrupt! GPIO_BTN2 state is %d)\n", state);
return IRQ_HANDLED;
}
static int __init helloworld_init(void)
{
int retval;
/*
* One could have checked whether the GPIO is valid on the controller or not,
* using gpio_is_valid() function.
* Ex:
* if (!gpio_is_valid(GPIO_LED_RED)) {
* pr_infor("Invalid Red LED\n");
* return -ENODEV;
* }
*/
gpio_request(GPIO_LED_GREEN, "green-led");
gpio_request(GPIO_LED_RED, "red-led");
gpio_request(GPIO_BTN1, "button-1");
gpio_request(GPIO_BTN2, "button-2");
/*
* Configure Button GPIOs as input
*
* After this, one can call gpio_set_debounce()
* only if the controller has the feature
*
* For example, to debounce a button with a delay of 200ms
* gpio_set_debounce(GPIO_BTN1, 200);
*/
gpio_direction_input(GPIO_BTN1);
gpio_direction_input(GPIO_BTN2);
/*
* Set LED GPIOs as output, with their initial values set to 0
*/
gpio_direction_output(GPIO_LED_RED, 0);
gpio_direction_output(GPIO_LED_GREEN, 0);
irq = gpio_to_irq(GPIO_BTN1);
retval = request_threaded_irq(irq, NULL,\
btn1_pushed_irq_handler, \
IRQF_TRIGGER_LOW | IRQF_ONESHOT, \
"device-name", NULL);
pr_info("Hello world!\n");
return 0;
}
static void __exit hellowolrd_exit(void)
{
free_irq(irq, NULL);
gpio_free(GPIO_LED_RED);
gpio_free(GPIO_LED_GREEN);
gpio_free(GPIO_BTN1);
gpio_free(GPIO_BTN2);
pr_info("End of the world\n");
}
module_init(hellowolrd_init);
module_exit(hellowolrd_exit);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
基于描述符的 GPIO 接口:新的推荐方式
使用新的基于描述符的 GPIO 接口,GPIO 由一个连贯的struct gpio_desc结构来描述:
struct gpio_desc {
struct gpio_chip *chip;
unsigned long flags;
const char *label;
};
应该使用以下标头才能使用新接口:
#include <linux/gpio/consumer.h>
使用基于描述符的接口,在分配和拥有 GPIO 之前,这些 GPIO 必须已经映射到某个地方。通过映射,我的意思是它们应该分配给您的设备,而使用传统的基于整数的接口,您只需在任何地方获取一个数字并将其请求为 GPIO。实际上,内核中有三种映射:
-
平台数据映射:映射在板文件中完成。
-
设备树:映射以 DT 样式完成,与前面的部分讨论的相同。这是我们将在本书中讨论的映射。
-
高级配置和电源接口映射(ACPI):映射以 ACPI 样式完成。通常用于基于 x86 的系统。
GPIO 描述符映射-设备树
GPIO 描述符映射在使用者设备的节点中定义。包含 GPIO 描述符映射的属性必须命名为<name>-gpios或<name>-gpio,其中<name>足够有意义,以描述这些 GPIO 将用于的功能。
应该始终在属性名称后缀加上-gpio或-gpios,因为每个基于描述符的接口函数都依赖于gpio_suffixes[]变量,在drivers/gpio/gpiolib.h中定义如下:
/* gpio suffixes used for ACPI and device tree lookup */
static const char * const gpio_suffixes[] = { "gpios", "gpio" };
让我们看看在 DT 中查找设备中 GPIO 描述符映射的函数:
static struct gpio_desc *of_find_gpio(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpio_lookup_flags *flags)
{
char prop_name[32]; /* 32 is max size of property name */
enum of_gpio_flags of_flags;
struct gpio_desc *desc;
unsigned int i;
for (i = 0; i < ARRAY_SIZE(gpio_suffixes); i++) {
if (con_id)
snprintf(prop_name, sizeof(prop_name), "%s-%s",
con_id,
gpio_suffixes[i]);
else
snprintf(prop_name, sizeof(prop_name), "%s",
gpio_suffixes[i]);
desc = of_get_named_gpiod_flags(dev->of_node,
prop_name, idx,
&of_flags);
if (!IS_ERR(desc) || (PTR_ERR(desc) == -EPROBE_DEFER))
break;
}
if (IS_ERR(desc))
return desc;
if (of_flags & OF_GPIO_ACTIVE_LOW)
*flags |= GPIO_ACTIVE_LOW;
return desc;
}
现在,让我们考虑以下节点,这是Documentation/gpio/board.txt的摘录:
foo_device {
compatible = "acme,foo";
[...]
led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */
<&gpio 16 GPIO_ACTIVE_HIGH>, /* green */
<&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */
power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};
这就是映射应该看起来像的,具有有意义的名称。
分配和使用 GPIO
可以使用gpiog_get()或gpiod_get_index()来分配 GPIO 描述符:
struct gpio_desc *gpiod_get_index(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags)
struct gpio_desc *gpiod_get(struct device *dev,
const char *con_id,
enum gpiod_flags flags)
在错误的情况下,如果没有分配具有给定功能的 GPIO,则这些函数将返回-ENOENT,或者可以使用IS_ERR()宏的其他错误。第一个函数返回与给定索引处的 GPIO 对应的 GPIO 描述符结构,而第二个函数返回索引为 0 的 GPIO(对于单个 GPIO 映射很有用)。dev是 GPIO 描述符将属于的设备。这是你的设备。con_id是 GPIO 使用者内的功能。它对应于 DT 中属性名称的<name>前缀。idx是需要描述符的 GPIO 的索引(从 0 开始)。flags是一个可选参数,用于确定 GPIO 初始化标志,以配置方向和/或输出值。它是include/linux/gpio/consumer.h中定义的enum gpiod_flags的一个实例:
enum gpiod_flags {
GPIOD_ASIS = 0,
GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET,
GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET |
GPIOD_FLAGS_BIT_DIR_OUT,
GPIOD_OUT_HIGH = GPIOD_FLAGS_BIT_DIR_SET |
GPIOD_FLAGS_BIT_DIR_OUT |
GPIOD_FLAGS_BIT_DIR_VAL,
};
现在让我们为在前面的 DT 中定义的映射分配 GPIO 描述符:
struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
LED GPIO 将是主动高电平,而电源 GPIO 将是主动低电平(即gpiod_is_active_low(power)将为 true)。分配的反向操作使用gpiod_put()函数完成:
gpiod_put(struct gpio_desc *desc);
让我们看看如何释放red和blue GPIO LED:
gpiod_put(blue);
gpiod_put(red);
在我们继续之前,请记住,除了gpiod_get()/gpiod_get_index()和gpio_put()函数与gpio_request()和gpio_free()完全不同之外,可以通过将gpio_前缀更改为gpiod_来执行从基于整数的接口到基于描述符的接口的 API 转换。
也就是说,要更改方向,应该使用gpiod_direction_input()和gpiod_direction_output()函数:
int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);
value是在将方向设置为输出后应用于 GPIO 的状态。如果 GPIO 控制器具有此功能,则可以使用其描述符设置给定 GPIO 的去抖动超时:
int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce);
为了访问给定描述符的 GPIO,必须像基于整数的接口一样注意。换句话说,应该注意自己是处于原子(无法休眠)还是非原子上下文中,然后使用适当的函数:
int gpiod_cansleep(const struct gpio_desc *desc);
/* Value get/set from sleeping context */
int gpiod_get_value_cansleep(const struct gpio_desc *desc);
void gpiod_set_value_cansleep(struct gpio_desc *desc, int value);
/* Value get/set from non-sleeping context */
int gpiod_get_value(const struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);
对于映射到 IRQ 的 GPIO 描述符,可以使用gpiod_to_irq()来获取与给定 GPIO 描述符对应的 IRQ 编号,然后可以与request_irq()函数一起使用:
int gpiod_to_irq(const struct gpio_desc *desc);
在代码中的任何时候,可以使用desc_to_gpio()或gpio_to_desc()函数从基于描述符的接口切换到传统的基于整数的接口,反之亦然:
/* Convert between the old gpio_ and new gpiod_ interfaces */
struct gpio_desc *gpio_to_desc(unsigned gpio);
int desc_to_gpio(const struct gpio_desc *desc);
把所有东西放在一起
以下是驱动程序总结了描述符接口中介绍的概念。原则是相同的,GPIO 也是一样的:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h> /* For platform devices */
#include <linux/gpio/consumer.h> /* For GPIO Descriptor */
#include <linux/interrupt.h> /* For IRQ */
#include <linux/of.h> /* For DT*/
/*
* Let us consider the below mapping in device tree:
*
* foo_device {
* compatible = "packt,gpio-descriptor-sample";
* led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, // red
* <&gpio2 16 GPIO_ACTIVE_HIGH>, // green
*
* btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>;
* btn2-gpios = <&gpio2 31 GPIO_ACTIVE_LOW>;
* };
*/
static struct gpio_desc *red, *green, *btn1, *btn2;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq,
void *dev_id, struct pt_regs *regs)
{
int state;
/* read the button value and change the led state */
state = gpiod_get_value(btn2);
gpiod_set_value(red, state);
gpiod_set_value(green, state);
pr_info("btn1 interrupt: Interrupt! btn2 state is %d)\n",
state);
return IRQ_HANDLED;
}
static const struct of_device_id gpiod_dt_ids[] = {
{ .compatible = "packt,gpio-descriptor-sample", },
{ /* sentinel */ }
};
static int my_pdrv_probe (struct platform_device *pdev)
{
int retval;
struct device *dev = &pdev->dev;
/*
* We use gpiod_get/gpiod_get_index() along with the flags
* in order to configure the GPIO direction and an initial
* value in a single function call.
*
* One could have used:
* red = gpiod_get_index(dev, "led", 0);
* gpiod_direction_output(red, 0);
*/
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW);
/*
* Configure GPIO Buttons as input
*
* After this, one can call gpiod_set_debounce()
* only if the controller has the feature
* For example, to debounce a button with a delay of 200ms
* gpiod_set_debounce(btn1, 200);
*/
btn1 = gpiod_get(dev, "led", 0, GPIOD_IN);
btn2 = gpiod_get(dev, "led", 1, GPIOD_IN);
irq = gpiod_to_irq(btn1);
retval = request_threaded_irq(irq, NULL,\
btn1_pushed_irq_handler, \
IRQF_TRIGGER_LOW | IRQF_ONESHOT, \
"gpio-descriptor-sample", NULL);
pr_info("Hello! device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
free_irq(irq, NULL);
gpiod_put(red);
gpiod_put(green);
gpiod_put(btn1);
gpiod_put(btn2);
pr_info("good bye reader!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "gpio_descriptor_sample",
.of_match_table = of_match_ptr(gpiod_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
GPIO 接口和设备树
无论需要为什么接口使用 GPIO,如何指定 GPIO 取决于提供它们的控制器,特别是关于其#gpio-cells属性,该属性确定用于 GPIO 指定器的单元格数量。 GPIO 指定器至少包含控制器 phandle 和一个或多个参数,其中参数的数量取决于提供 GPIO 的控制器的#gpio-cells属性。第一个单元通常是控制器上的 GPIO 偏移量编号,第二个表示 GPIO 标志。
GPIO 属性应命名为[<name>-]gpios],其中<name>是设备的 GPIO 用途。请记住,对于基于描述符的接口,这个规则是必须的,并且变成了<name>-gpios(注意没有方括号,这意味着<name>前缀是必需的):
gpio1: gpio1 {
gpio-controller;
#gpio-cells = <2>;
};
gpio2: gpio2 {
gpio-controller;
#gpio-cells = <1>;
};
[...]
cs-gpios = <&gpio1 17 0>,
<&gpio2 2>;
<0>, /* holes are permitted, means no GPIO 2 */
<&gpio1 17 0>;
reset-gpios = <&gpio1 30 0>;
cd-gpios = <&gpio2 10>;
在前面的示例中,CS GPIO 包含控制器 1 和控制器 2 的 GPIO。如果不需要在列表中指定给定索引处的 GPIO,则可以使用<0>。复位 GPIO 有两个单元格(控制器 phandle 之后的两个参数),而 CD GPIO 只有一个单元格。您可以看到我给我的 GPIO 指定器起的名字是多么有意义。
传统的基于整数的接口和设备树
该接口依赖于以下标头:
#include <linux/of_gpio.h>
当您需要使用传统的基于整数的接口支持 DT 时,您应该记住两个函数:of_get_named_gpio()和of_get_named_gpio_count():
int of_get_named_gpio(struct device_node *np,
const char *propname, int index)
int of_get_named_gpio_count(struct device_node *np,
const char* propname)
给定设备节点,前者返回*propname属性在index位置的 GPIO 编号。第二个只返回属性中指定的 GPIO 数量:
int n_gpios = of_get_named_gpio_count(dev.of_node,
"cs-gpios"); /* return 4 */
int second_gpio = of_get_named_gpio(dev.of_node, "cs-gpio", 1);
int rst_gpio = of_get_named_gpio("reset-gpio", 0);
gpio_request(second_gpio, "my-gpio);
仍然支持旧的说明符的驱动程序,其中 GPIO 属性命名为[<name>-gpio]或gpios。在这种情况下,应使用未命名的 API 版本,通过of_get_gpio()和of_gpio_count():
int of_gpio_count(struct device_node *np)
int of_get_gpio(struct device_node *np, int index)
DT 节点如下所示:
my_node@addr {
compatible = "[...]";
gpios = <&gpio1 2 0>, /* INT */
<&gpio1 5 0>; /* RST */
[...]
};
驱动程序中的代码如下所示:
struct device_node *np = dev->of_node;
if (!np)
return ERR_PTR(-ENOENT);
int n_gpios = of_gpio_count(); /* Will return 2 */
int gpio_int = of_get_gpio(np, 0);
if (!gpio_is_valid(gpio_int)) {
dev_err(dev, "failed to get interrupt gpio\n");
return ERR_PTR(-EINVAL);
}
gpio_rst = of_get_gpio(np, 1);
if (!gpio_is_valid(pdata->gpio_rst)) {
dev_err(dev, "failed to get reset gpio\n");
return ERR_PTR(-EINVAL);
}
可以通过重写第一个驱动程序(基于整数接口的驱动程序)来总结这一点,以符合平台驱动程序结构,并使用 DT API:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h> /* For platform devices */
#include <linux/interrupt.h> /* For IRQ */
#include <linux/gpio.h> /* For Legacy integer based GPIO */
#include <linux/of_gpio.h> /* For of_gpio* functions */
#include <linux/of.h> /* For DT*/
/*
* Let us consider the following node
*
* foo_device {
* compatible = "packt,gpio-legacy-sample";
* led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, // red
* <&gpio2 16 GPIO_ACTIVE_HIGH>, // green
*
* btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>;
* btn2-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>;
* };
*/
static unsigned int gpio_red, gpio_green, gpio_btn1, gpio_btn2;
static unsigned int irq;
static irq_handler_t btn1_pushed_irq_handler(unsigned int irq, void *dev_id,
struct pt_regs *regs)
{
/* The content of this function remains unchanged */
[...]
}
static const struct of_device_id gpio_dt_ids[] = {
{ .compatible = "packt,gpio-legacy-sample", },
{ /* sentinel */ }
};
static int my_pdrv_probe (struct platform_device *pdev)
{
int retval;
struct device_node *np = &pdev->dev.of_node;
if (!np)
return ERR_PTR(-ENOENT);
gpio_red = of_get_named_gpio(np, "led", 0);
gpio_green = of_get_named_gpio(np, "led", 1);
gpio_btn1 = of_get_named_gpio(np, "btn1", 0);
gpio_btn2 = of_get_named_gpio(np, "btn2", 0);
gpio_request(gpio_green, "green-led");
gpio_request(gpio_red, "red-led");
gpio_request(gpio_btn1, "button-1");
gpio_request(gpio_btn2, "button-2");
/* Code to configure GPIO and request IRQ remains unchanged */
[...]
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
/* The content of this function remains unchanged */
[...]
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "gpio_legacy_sample",
.of_match_table = of_match_ptr(gpio_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
设备树中的 GPIO 映射到 IRQ
可以轻松地在设备树中将 GPIO 映射到 IRQ。使用两个属性来指定中断:
-
interrupt-parent:这是 GPIO 的 GPIO 控制器 -
interrupts:这是中断说明符列表
这适用于传统和基于描述符的接口。 IRQ 说明符取决于提供此 GPIO 的 GPIO 控制器的#interrupt-cell属性。 #interrupt-cell确定在指定中断时使用的单元数。通常,第一个单元表示要映射到 IRQ 的 GPIO 编号,第二个单元表示应触发中断的电平/边缘。无论如何,中断说明符始终取决于其父级(具有设置中断控制器的父级),因此请参考内核源中的绑定文档:
gpio4: gpio4 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
my_label: node@0 {
reg = <0>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio4>;
interrupts = <29 IRQ_TYPE_LEVEL_LOW>;
};
获取相应的 IRQ 有两种解决方案:
-
您的设备位于已知总线(I2C 或 SPI)上:IRQ 映射将由您完成,并通过
struct i2c_client或struct spi_device结构通过probe()函数(通过i2c_client.irq或spi_device.irq)提供。 -
您的设备位于伪平台总线上:
probe()函数将获得一个struct platform_device,您可以在其中调用platform_get_irq():
int platform_get_irq(struct platform_device *dev, unsigned int num);
随意查看第六章,设备树的概念。
GPIO 和 sysfs
sysfs GPIO 接口允许人们通过集或文件管理和控制 GPIO。它位于/sys/class/gpio下。设备模型在这里被广泛使用,有三种类型的条目可用:
-
/sys/class/gpio/:这是一切的开始。此目录包含两个特殊文件,export和unexport: -
export:这允许我们要求内核将给定 GPIO 的控制权导出到用户空间,方法是将其编号写入此文件。例如:echo 21 > export将为 GPIO#21 创建一个 GPIO21 节点,如果内核代码没有请求。 -
unexport:这将撤消向用户空间的导出效果。例如:echo 21 > unexport将删除使用导出文件导出的任何 GPIO21 节点。 -
/sys/class/gpio/gpioN/:此目录对应于 GPIO 编号 N(其中 N 是系统全局的,而不是相对于芯片),可以使用export文件导出,也可以在内核内部导出。例如:/sys/class/gpio/gpio42/(对于 GPIO#42)具有以下读/写属性: -
direction文件用于获取/设置 GPIO 方向。允许的值是in或out字符串。通常可以写入此值。写入out会将值初始化为低。为了确保无故障操作,可以写入低值和高值以将 GPIO 配置为具有该初始值的输出。如果内核代码已导出此 GPIO,则不会存在此属性,从而禁用方向(请参阅gpiod_export()或gpio_export()函数)。 -
value属性允许我们根据方向(输入或输出)获取/设置 GPIO 线的状态。如果 GPIO 配置为输出,写入任何非零值将被视为高电平状态。如果配置为输出,写入0将使输出低电平,而1将使输出高电平。如果引脚可以配置为产生中断的线,并且已配置为生成中断,则可以在该文件上调用poll(2)系统调用,poll(2)将在中断触发时返回。使用poll(2)将需要设置事件POLLPRI和POLLERR。如果使用select(2),则应在exceptfds中设置文件描述符。poll(2)返回后,要么lseek(2)到 sysfs 文件的开头并读取新值,要么关闭文件并重新打开以读取值。这与我们讨论的可轮询 sysfs 属性的原理相同。 -
edge确定了将让poll()或select()函数返回的信号边缘。允许的值为none,rising,falling或both。此文件可读/可写,仅在引脚可以配置为产生中断的输入引脚时存在。 -
active_low读取为 0(假)或 1(真)。写入任何非零值将反转value属性的读取和写入。现有和随后的poll(2)支持通过边缘属性进行配置,用于上升和下降边缘,将遵循此设置。内核中设置此值的相关函数是gpio_sysf_set_active_low()。
从内核代码中导出 GPIO
除了使用/sys/class/gpio/export文件将 GPIO 导出到用户空间外,还可以使用内核代码中的gpio_export(用于传统接口)或gpioD_export(新接口)等函数来显式管理已经使用gpio_request()或gpiod_get()请求的 GPIO 的导出:
int gpio_export(unsigned gpio, bool direction_may_change);
int gpiod_export(struct gpio_desc *desc, bool direction_may_change);
direction_may_change参数决定是否可以从输入更改信号方向为输出,反之亦然。内核的反向操作是gpio_unexport()或gpiod_unexport():
void gpio_unexport(unsigned gpio); /* Integer-based interface */
void gpiod_unexport(struct gpio_desc *desc) /* Descriptor-based */
一旦导出,可以使用gpio_export_link()(或gpiod_export_link()用于基于描述符的接口)来创建符号链接,从 sysfs 的其他位置指向 GPIO sysfs 节点。驱动程序可以使用此功能在 sysfs 中的自己设备下提供接口,并提供描述性名称:
int gpio_export_link(struct device *dev, const char *name,
unsigned gpio)
int gpiod_export_link(struct device *dev, const char *name,
struct gpio_desc *desc)
可以在基于描述符的接口的probe()函数中使用如下:
static struct gpio_desc *red, *green, *btn1, *btn2;
static int my_pdrv_probe (struct platform_device *pdev)
{
[...]
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW);
gpiod_export(&pdev->dev, "Green_LED", green);
gpiod_export(&pdev->dev, "Red_LED", red);
[...]
return 0;
}
对于基于整数的接口,代码如下:
static int my_pdrv_probe (struct platform_device *pdev)
{
[...]
gpio_red = of_get_named_gpio(np, "led", 0);
gpio_green = of_get_named_gpio(np, "led", 1);
[...]
int gpio_export_link(&pdev->dev, "Green_LED", gpio_green)
int gpio_export_link(&pdev->dev, "Red_LED", gpio_red)
return 0;
}
摘要
在本章中,我们展示了在内核中处理 GPIO 是一项简单的任务。讨论了传统接口和新接口,为您提供了选择适合您需求的接口的可能性,以编写增强的 GPIO 驱动程序。您将能够处理映射到 GPIO 的中断请求。下一章将处理提供和公开 GPIO 线的芯片,称为 GPIO 控制器。
第十五章:GPIO 控制器驱动程序 - gpio_chip
在上一章中,我们处理了 GPIO 线路。这些线路通过一个名为 GPIO 控制器的特殊设备向系统公开。本章将逐步解释如何为这类设备编写驱动程序,从而涵盖以下主题:
-
GPIO 控制器驱动程序架构和数据结构
-
GPIO 控制器的 Sysfs 接口
-
DT 中 GPIO 控制器的表示
驱动程序架构和数据结构
这类设备的驱动程序应该提供:
-
建立 GPIO 方向(输入和输出)的方法。
-
用于访问 GPIO 值的方法(获取和设置)。
-
将给定的 GPIO 映射到 IRQ 并返回相关的编号的方法。
-
标志,表示其方法是否可以休眠,这非常重要。
-
可选的
debugfs dump方法(显示额外状态,如上拉配置)。 -
可选的基数号码,从哪里开始对 GPIO 进行编号。如果省略,将自动分配。
在内核中,GPIO 控制器表示为 struct gpio_chip 的实例,定义在 linux/gpio/driver.h 中:
struct gpio_chip {
const char *label;
struct device *dev;
struct module *owner;
int (*request)(struct gpio_chip *chip, unsigned offset);
void (*free)(struct gpio_chip *chip, unsigned offset);
int (*get_direction)(struct gpio_chip *chip, unsigned offset);
int (*direction_input)(struct gpio_chip *chip, unsigned offset);
int (*direction_output)(struct gpio_chip *chip, unsigned offset,
int value);
int (*get)(struct gpio_chip *chip,unsigned offset);
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
void (*set_multiple)(struct gpio_chip *chip, unsigned long *mask,
unsigned long *bits);
int (*set_debounce)(struct gpio_chip *chip, unsigned offset,
unsigned debounce);
int (*to_irq)(struct gpio_chip *chip, unsigned offset);
int base;
u16 ngpio;
const char *const *names;
bool can_sleep;
bool irq_not_threaded;
bool exported;
#ifdef CONFIG_GPIOLIB_IRQCHIP
/*
* With CONFIG_GPIOLIB_IRQCHIP we get an irqchip
* inside the gpiolib to handle IRQs for most practical cases.
*/
struct irq_chip *irqchip;
struct irq_domain *irqdomain;
unsigned int irq_base;
irq_flow_handler_t irq_handler;
unsigned int irq_default_type;
#endif
#if defined(CONFIG_OF_GPIO)
/*
* If CONFIG_OF is enabled, then all GPIO controllers described in the
* device tree automatically may have an OF translation
*/
struct device_node *of_node;
int of_gpio_n_cells;
int (*of_xlate)(struct gpio_chip *gc,
const struct of_phandle_args *gpiospec, u32 *flags);
}
以下是结构中每个元素的含义:
-
request是一个可选的钩子,用于特定于芯片的激活。如果提供,它将在分配 GPIO 之前执行,每当调用gpio_request()或gpiod_get()时。 -
free是一个可选的钩子,用于特定于芯片的停用。如果提供,它将在每次调用gpiod_put()或gpio_free()时,在释放 GPIO 之前执行。 -
get_direction每当需要知道 GPIOoffset的方向时执行。返回值应为 0 表示输出,1 表示输入(与GPIOF_DIR_XXX相同),或者负错误。 -
direction_input配置信号offset为输入,或返回错误。 -
get返回 GPIOoffset的值;对于输出信号,这将返回实际感应到的值,或者零。 -
set将输出值分配给 GPIOoffset。 -
set_multiple当需要为mask定义的多个信号分配输出值时调用。如果未提供,内核将安装一个通用的钩子,将遍历mask位并在每个设置的位上执行chip->set(i)。
请参阅以下内容,显示了如何实现此功能:
static void gpio_chip_set_multiple(struct gpio_chip *chip,
unsigned long *mask, unsigned long *bits)
{
if (chip->set_multiple) {
chip->set_multiple(chip, mask, bits);
} else {
unsigned int i;
/* set outputs if the corresponding mask bit is set */
for_each_set_bit(i, mask, chip->ngpio)
chip->set(chip, i, test_bit(i, bits));
}
}
-
set_debounce如果控制器支持,这个钩子是一个可选的回调,用于设置指定 GPIO 的去抖时间。 -
to_irq是一个可选的钩子,用于提供 GPIO 到 IRQ 的映射。每当需要执行gpio_to_irq()或gpiod_to_irq()函数时,就会调用这个函数。这个实现可能不会休眠。 -
base标识了该芯片处理的第一个 GPIO 编号;或者,在注册期间为负时,内核将自动(动态)分配一个。 -
ngpio是该控制器提供的 GPIO 数量,从base开始,到(base + ngpio - 1)结束。 -
names,如果设置,必须是一个字符串数组,用作该芯片中 GPIO 的替代名称。数组的大小必须为ngpio,任何不需要别名的 GPIO 可以在数组中的条目中设置为NULL。 -
can_sleep是一个布尔标志,如果get()/set()方法可能会休眠,则设置。对于 GPIO 控制器(也称为扩展器)位于总线上,如 I2C 或 SPI,其访问可能会导致休眠。这意味着如果芯片支持 IRQ,这些 IRQ 需要被线程化,因为芯片访问可能会休眠,例如,读取 IRQ 状态寄存器时。对于映射到内存(SoC 的一部分)的 GPIO 控制器,可以将其设置为 false。 -
irq_not_threaded是一个布尔标志,如果设置了can_sleep,则必须设置该标志,但 IRQs 不需要被线程化。
每个芯片公开了一些信号,通过方法调用中的偏移值(在范围 0(ngpio - 1)内)进行标识。当这些信号通过 gpio_get_value(gpio) 等调用引用时,偏移量是通过从 GPIO 编号中减去基数来计算的。
在定义了每个回调和其他字段之后,应在配置的 struct gpio_chip 结构上调用 gpiochip_add(),以便向内核注册控制器。在注销时,使用 gpiochip_remove()。就是这样。您可以看到编写自己的 GPIO 控制器驱动程序有多么容易。在本书源代码库中,您将找到一个可用的 MCP23016 I2C I/O 扩展器的 GPIO 控制器驱动程序,其数据表可在 ww1.microchip.com/downloads/en/DeviceDoc/20090C.pdf 上找到。
要编写这样的驱动程序,您应该包括:
#include <linux/gpio.h>
以下是我们为控制器编写的驱动程序的摘录,只是为了向您展示编写 GPIO 控制器驱动程序有多么容易:
#define GPIO_NUM 16
struct mcp23016 {
struct i2c_client *client;
struct gpio_chip chip;
};
static int mcp23016_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mcp23016 *mcp;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
mcp = devm_kzalloc(&client->dev, sizeof(*mcp), GFP_KERNEL);
if (!mcp)
return -ENOMEM;
mcp->chip.label = client->name;
mcp->chip.base = -1;
mcp->chip.dev = &client->dev;
mcp->chip.owner = THIS_MODULE;
mcp->chip.ngpio = GPIO_NUM; /* 16 */
mcp->chip.can_sleep = 1; /* may not be accessed from actomic context */
mcp->chip.get = mcp23016_get_value;
mcp->chip.set = mcp23016_set_value;
mcp->chip.direction_output = mcp23016_direction_output;
mcp->chip.direction_input = mcp23016_direction_input;
mcp->client = client;
i2c_set_clientdata(client, mcp);
return gpiochip_add(&mcp->chip);
}
要从控制器驱动程序内部请求自有 GPIO,不应使用 gpio_request()。GPIO 驱动程序可以使用以下函数来请求和释放描述符,而不会永远固定在内核中:
struct gpio_desc *gpiochip_request_own_desc(struct gpio_desc *desc, const char *label)
void gpiochip_free_own_desc(struct gpio_desc *desc)
使用 gpiochip_request_own_desc() 请求的描述符必须使用 gpiochip_free_own_desc() 释放。
引脚控制器指南
根据您为其编写驱动程序的控制器,您可能需要实现一些引脚控制操作,以处理引脚复用、配置等:
-
对于只能执行简单 GPIO 的引脚控制器,简单的
struct gpio_chip就足以处理它。无需设置struct pinctrl_desc结构,只需编写 GPIO 控制器驱动程序即可。 -
如果控制器可以在 GPIO 功能之上生成中断,则必须设置并向 IRQ 子系统注册
struct irq_chip。 -
对于具有引脚复用、高级引脚驱动强度、复杂偏置的控制器,您应该设置以下三个接口:
-
struct gpio_chip,在本章前面讨论过 -
struct irq_chip,在下一章(第十六章,高级中断管理)中讨论。 -
struct pinctrl_desc,本书未讨论,但在内核文档 Documentation/pinctrl.txt 中有很好的解释
GPIO 控制器的 Sysfs 接口
成功调用 gpiochip_add() 后,将创建一个目录条目,路径类似于 /sys/class/gpio/gpiochipX/,其中 X 是 GPIO 控制器基地址(提供从 #X 开始的 GPIO 的控制器),具有以下属性:
-
base,其值与X相同,对应于gpio_chip.base(如果静态分配),并且是由此芯片管理的第一个 GPIO。 -
label,用于诊断(不一定是唯一的)。 -
ngpio,告诉这个控制器提供多少个 GPIO(N到N + ngpio - 1)。这与gpio_chip.ngpios中定义的相同。
所有前述属性都是只读的。
GPIO 控制器和 DT
在 DT 中声明的每个 GPIO 控制器都必须设置布尔属性 gpio-controller。一些控制器提供与 GPIO 映射的中断。在这种情况下,还应该设置属性 interrupt-cells,通常使用 2,但这取决于需要。第一个单元格是引脚编号,第二个表示中断标志。
gpio-cells 应设置为标识用于描述 GPIO 指定器的单元格数量。通常使用 <2>,第一个单元格用于标识 GPIO 编号,第二个用于标志。实际上,大多数非内存映射 GPIO 控制器不使用标志:
expander_1: mcp23016@27 {
compatible = "microchip,mcp23016";
interrupt-controller;
gpio-controller;
#gpio-cells = <2>;
interrupt-parent = <&gpio6>;
interrupts = <31 IRQ_TYPE_LEVEL_LOW>;
reg = <0x27>;
#interrupt-cells=<2>;
};
前述示例是我们的 GPIO 控制器设备节点,完整的设备驱动程序随本书的源代码一起提供。
摘要
本章远不止是为您可能遇到的 GPIO 控制器编写驱动程序的基础。它解释了描述这些设备的主要结构。下一章将涉及高级中断管理,我们将看到如何管理中断控制器,并在微芯片的 MCP23016 扩展器驱动程序中添加此功能。
第十六章:高级 IRQ 管理
Linux 是一个系统,设备通过 IRQ 通知内核特定事件。CPU 暴露 IRQ 线,由连接的设备使用,因此当设备需要 CPU 时,它会向 CPU 发送请求。当 CPU 收到此请求时,它会停止其实际工作并保存其上下文,以便为设备发出的请求提供服务。在为设备提供服务之后,其状态将恢复到中断发生时停止的确切位置。有这么多的 IRQ 线,另一个设备负责它们给 CPU。该设备是中断控制器:

中断控制器和 IRQ 线
设备不仅可以引发中断,某些处理器操作也可以引发中断。有两种不同类型的中断:
-
同步中断称为异常,由 CPU 在处理指令时产生。这些是不可屏蔽中断(NMI),是由于硬件故障等严重故障而产生的。它们始终由 CPU 处理。
-
异步中断称为中断,由其他硬件设备发出。这些是正常的可屏蔽中断。这是我们将在本章的后续部分讨论的内容。因此,让我们深入了解异常:
异常是由内核处理的编程错误的后果,内核向程序发送信号并尝试从错误中恢复。这些被分类为以下两类:
-
处理器检测到的异常:CPU 对异常情况生成的异常,分为三组:
-
故障,通常可以纠正(虚假指令)。
-
陷阱,发生在用户进程中(无效的内存访问,除以零),也是响应系统调用切换到内核模式的机制。如果内核代码确实引起陷阱,它会立即发生恐慌。
-
中止,严重错误。
-
程序化异常:这些是由程序员请求的,像陷阱一样处理。
以下数组列出了不可屏蔽中断(有关更多详细信息,请参阅wiki.osdev.org/Exceptions):
| 中断号 | 描述 |
|---|---|
| 0 | 除零错误 |
| 1 | 调试异常 |
| 2 | NMI 中断 |
| 3 | 断点 |
| 4 | 检测到溢出 |
| 5 | BOUND 范围超出 |
| 6 | 无效的操作码 |
| 7 | 协处理器(设备)不可用 |
| 8 | 双重故障 |
| 9 | 协处理器段溢出 |
| 10 | 无效的任务状态段 |
| 11 | 段不存在 |
| 12 | 栈故障 |
| 13 | 通用保护错误 |
| 14 | 页错误 |
| 15 | 保留 |
| 16 | 协处理器错误 |
| 17 - 31 | 保留 |
| 32 - 255 | 可屏蔽中断 |
NMI 足以覆盖整个异常列表。回到可屏蔽中断,它们的数量取决于连接的设备数量,以及它们实际如何共享这些 IRQ 线。有时它们是不够的,其中一些需要多路复用。常用的方法是通过 GPIO 控制器,它也充当中断控制器。在本章中,我们将讨论内核提供的管理 IRQ 的 API 以及多路复用的方式,并深入研究中断控制器驱动程序编写。
也就是说,在本章中将涵盖以下主题:
-
中断控制器和中断多路复用
-
高级外围 IRQ 管理
-
中断请求和传播(串联或嵌套)
-
GPIOLIB irqchip API
-
从 DT 处理中断控制器
多路复用中断和中断控制器
通常,仅有来自 CPU 的单个中断是不够的。大多数系统有数十甚至数百个中断。现在是中断控制器的时候,它允许它们进行多路复用。非常常见的架构或平台特定提供特定的设施,例如:
-
屏蔽/取消屏蔽单个中断
-
设置优先级
-
SMP 亲和力
-
像唤醒中断这样的奇特事物
IRQ 管理和中断控制器驱动程序都依赖于 IRQ 域,其依次建立在以下结构之上:
-
struct irq_chip:这个结构实现了一组描述如何驱动中断控制器的方法,并且这些方法直接被核心 IRQ 代码调用。 -
struct irqdomain结构,提供: -
给定中断控制器的固件节点的指针(fwnode)
-
将固件描述的 IRQ 转换为本地于此中断控制器的 ID 的方法(hwirq)
-
从 hwirq 中检索 IRQ 的 Linux 视图的方法
-
struct irq_desc:这个结构是 Linux 对中断的视图,包含所有核心内容,并且与 Linux 中断号一一对应 -
struct irq_action:这个结构 Linux 用于描述 IRQ 处理程序 -
struct irq_data:这个结构嵌入在struct irq_desc结构中,包含: -
与管理此中断的
irq_chip相关的数据 -
Linux IRQ 号和 hwirq
-
指向
irq_chip的指针
几乎每个 irq_chip 调用都会给定一个 irq_data 作为参数,从中可以获取相应的 irq_desc。
所有前述结构都是 IRQ 域 API 的一部分。中断控制器在内核中由 struct irq_chip 结构的实例表示,该结构描述了实际的硬件设备,以及 IRQ 核心使用的一些方法:
struct irq_chip {
struct device *parent_device;
const char *name;
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int(*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
unsigned long flags;
};
以下是结构中元素的含义:
-
parent_device:这是指向此 irqchip 的父级的指针。 -
name:这是/proc/interrupts文件的名称。 -
irq_enable:这个钩子函数用于启用中断,默认值是chip->unmask如果为NULL。 -
irq_disable:这个函数用于禁用中断。 -
*****
irq_ack:这是一个新中断的开始。一些控制器不需要这个。Linux 在中断被触发后立即调用此函数,远在中断被服务之前。一些实现将此函数映射到chip->disable(),以便在当前中断请求被服务之后,该线路上的另一个中断请求不会再次引发中断。 -
irq_mask:这个钩子函数用于在硬件中屏蔽中断源,使其无法再次触发。 -
irq_unmask:这个钩子函数用于取消屏蔽中断源。 -
irq_eoi:eoi 代表中断结束。Linux 在 IRQ 服务完成后立即调用此钩子。使用此函数根据需要重新配置控制器,以便在该线路上接收另一个中断请求。一些实现将此函数映射到chip->enable(),以撤消chip->ack()中的操作。 -
irq_set_affinity:这个函数仅在 SMP 机器上设置 CPU 亲和性。在 SMP 环境中,此函数设置将服务中断的 CPU。在单处理器机器中不使用此函数。 -
irq_retrigger:这个函数重新触发硬件中断,将中断重新发送到 CPU。 -
irq_set_type:这个函数设置中断的流类型(IRQ_TYPE_LEVEL/等)。 -
irq_set_wake:这个函数用于启用/禁用中断的电源管理唤醒功能。 -
irq_bus_lock:这个函数用于锁定对慢总线(I2C)芯片的访问。在这里锁定互斥锁就足够了。 -
irq_bus_sync_unlock:这个函数用于同步和解锁慢总线(I2C)芯片。解锁之前锁定的互斥锁。 -
irq_get_irqchip_state和irq_set_irqchip_state:分别返回或设置中断的内部状态。
每个中断控制器都有一个域,对于控制器来说,这就像进程的地址空间一样(参见第十一章,内核内存管理)。中断控制器域在内核中被描述为 struct irq_domain 结构的实例。它管理硬件 IRQ 和 Linux IRQ(即虚拟 IRQ)之间的映射。它是硬件中断编号转换对象:
struct irq_domain {
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
/* Optional data */
struct fwnode_handle *fwnode;
[...]
};
-
name是中断域的名称。 -
ops是指向 irq_domain 方法的指针。 -
host_data是所有者使用的私有数据指针。不会被 irqdomain 核心代码触及。 -
flags是每个irq_domain标志的主机。 -
fwnode是可选的。它是与irq_domain关联的 DT 节点的指针。在解码 DT 中断规范时使用。
中断控制器驱动程序通过调用irq_domain_add_<mapping_method>()函数之一创建并注册irq_domain,其中<mapping_method>是 hwirq 应该映射到 Linux IRQ 的方法。这些是:
irq_domain_add_linear():这使用一个由 hwirq 号索引的固定大小表。当映射 hwirq 时,为 hwirq 分配一个irq_desc,并将 IRQ 号存储在表中。这种线性映射适用于固定和小数量的 hwirq(~ <256)。这种映射的不便之处在于表的大小,它与最大可能的 hwirq 号一样大。因此,IRQ 号查找时间是固定的,irq_desc仅为正在使用的 IRQ 分配。大多数驱动程序应该使用线性映射。此函数具有以下原型:
struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
irq_domain_add_tree():这是irq_domain在 radix 树中维护 Linux IRQ 和 hwirq 号之间的映射。当映射 hwirq 时,将分配一个irq_desc,并且 hwirq 将用作 radix 树的查找键。如果 hwirq 号可能非常大,则树映射是一个不错的选择,因为它不需要分配一个与最大 hwirq 号一样大的表。缺点是 hwirq 到 IRQ 号的查找取决于表中有多少条目。很少有驱动程序应该需要这种映射。它具有以下原型:
struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
irq_domain_add_nomap():您可能永远不会使用此方法。尽管如此,它的整个描述在Documentation/IRQ-domain.txt中可以找到,位于内核源树中。它的原型是:
struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
unsigned int max_irq,
const struct irq_domain_ops *ops,
void *host_data)
of_node 是指向中断控制器的 DT 节点的指针。size 表示域中中断的数量。ops 表示映射/取消映射域回调,host_data 是控制器的私有数据指针。
由于 IRQ 域在创建时为空(没有映射),因此应该使用irq_create_mapping()函数来创建映射并将其分配给域。在下一节中,我们将决定在代码中创建映射的正确位置:
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
-
domain:这是此硬件中断所属的域,或者对于默认域为NULL。 -
Hwirq:这是该域空间中的硬件 IRQ 号
当编写同时作为中断控制器的 GPIO 控制器的驱动程序时,irq_create_mapping()是从gpio_chip.to_irq()回调函数内部调用的,如下所示:
return irq_create_mapping(gpiochip->irq_domain, offset);
其他人更喜欢在probe函数内提前为每个 hwirq 创建映射,如下所示:
for (j = 0; j < gpiochip->chip.ngpio; j++) {
irq = irq_create_mapping(
gpiochip ->irq_domain, j);
}
hwirq 是从 gpiochip 的 GPIO 偏移量。
如果 hwirq 的映射尚不存在,该函数将分配一个新的 Linux irq_desc结构,将其与 hwirq 关联,并调用irq_domain_ops.map()(通过irq_domain_associate()函数)回调,以便驱动程序可以执行任何必需的硬件设置:
struct irq_domain_ops {
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
};
.map():这在虚拟 irq(virq)号和 hwirq 号之间创建或更新映射。对于给定的映射,只调用一次。它通常使用irq_set_chip_and_handler*将 virq 与给定处理程序进行映射,以便调用generic_handle_irq()或handle_nested_irq将触发正确的处理程序。这里的魔法被称为irq_set_chip_and_handler()函数:
void irq_set_chip_and_handler(unsigned int irq,
struct irq_chip *chip, irq_flow_handler_t handle)
其中:
-
irq:这是作为map()函数参数给出的 Linux IRQ。 -
chip:这是您的irq_chip。一些控制器非常愚蠢,几乎不需要在其irq_chip结构中做任何事情。在这种情况下,您应该传递dummy_irq_chip,它在kernel/irq/dummychip.c中定义,这是为这种控制器定义的内核irq_chip结构。 -
handle:这确定将调用使用request_irq()注册的真正处理程序的包装函数。其值取决于 IRQ 是边沿触发还是电平触发。在任何一种情况下,handle应设置为handle_edge_irq或handle_level_irq。这两个都是内核辅助函数,在调用真正的 IRQ 处理程序之前和之后执行一些技巧。示例如下:
static int pcf857x_irq_domain_map(struct irq_domain *domain,
unsigned int irq, irq_hw_number_t hw)
{
struct pcf857x *gpio = domain->host_data;
irq_set_chip_and_handler(irq, &dummy_irq_chip,handle_level_irq);
#ifdef CONFIG_ARM
set_irq_flags(irq, IRQF_VALID);
#else
irq_set_noprobe(irq);
#endif
gpio->irq_mapped |= (1 << hw);
return 0;
}
-
xlate:给定 DT 节点和中断说明符,此钩子解码硬件 IRQ 号码和 Linux IRQ 类型值。根据您的 DT 控制器节点中指定的#interrupt-cells,内核提供了一个通用的翻译函数: -
irq_domain_xlate_twocell():用于直接双元绑定的通用翻译函数。适用于两个单元绑定的 DT IRQ 说明符,其中单元值直接映射到 hwirq 号码和 Linux irq 标志。 -
irq_domain_xlate_onecell():直接单元绑定的通用 xlate。 -
irq_domain_xlate_onetwocell():用于一个或两个单元绑定的通用 xlate。
给出了域操作的示例如下:
static struct irq_domain_ops mcp23016_irq_domain_ops = {
.map = mcp23016_irq_domain_map,
.xlate = irq_domain_xlate_twocell,
};
当收到中断时,应使用 irq_find_mapping() 函数从 hwirq 号码中找到 Linux IRQ 号码。当然,在返回之前必须存在映射。Linux IRQ 号码始终与 struct irq_desc 结构相关联,这是 Linux 用来描述 IRQ 的结构:
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action;
unsigned int irqs_unhandled;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
int parent_irq;
struct module *owner;
const char *name;
};
这里未描述的一些字段是内部字段,由 IRQ 核心使用:
-
irq_common_data是传递给芯片函数的每个 IRQ 和芯片数据 -
kstat_irqs是自启动以来每个 CPU 的 IRQ 统计信息 -
handle_irq是高级别 IRQ 事件处理程序 -
action表示此描述符的 IRQ 动作列表 -
irqs_unhandled是虚假未处理中断的统计字段 -
lock表示 SMP 的锁定 -
threads_active是当前正在运行此描述符的 IRQ 动作线程的数量 -
wait_for_threads表示sync_irq等待线程处理程序的等待队列 -
nr_actions是此描述符上安装的动作数量 -
no_suspend_depth和force_resume_depth表示具有IRQF_NO_SUSPEND或IRQF_FORCE_RESUME标志设置的 IRQ 描述符上的irqactions数量 -
dir表示/proc/irq/ procfs条目 -
name命名了流处理程序,在/proc/interrupts输出中可见
irq_desc.action 字段是 irqaction 结构的列表,每个结构记录了与关联中断源的中断处理程序的地址。每次调用内核的 request_irq() 函数(或线程版本 o)都会在列表的末尾创建一个 struct irqaction 结构。例如,对于共享中断,该字段将包含与注册的处理程序数量相同的 IRQ 动作;
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
};
-
handler是非线程(硬件)中断处理程序函数 -
name是设备的名称 -
dev_id是用于标识设备的 cookie -
percpu_dev_id是用于标识设备的 cookie -
next是共享中断的下一个 IRQ 动作的指针 -
irq是 Linux 中断号 -
flags表示 IRQ 的标志(参见IRQF_*) -
thread_fn是线程中断处理程序函数,用于线程中断 -
thread是线程中断的线程结构的指针 -
thread_flags表示与线程相关的标志 -
thread_mask是用于跟踪线程活动的位掩码 -
dir指向/proc/irq/NN/<name>/条目
irqaction.handler 字段引用的中断处理程序只是与处理来自特定外部设备的中断相关的函数,它们对于将这些中断请求传递给主机微处理器的方式几乎没有(如果有的话)了解。它们不是微处理器级别的中断服务例程,因此不会通过 RTE 或类似的与中断相关的操作码退出。这使得基于中断驱动的设备驱动程序在不同的微处理器架构之间具有很大的可移植性
以下是struct irq_data结构的重要字段的定义,该结构是传递给芯片函数的每个 IRQ 芯片数据:
struct irq_data {
[...]
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
void *chip_data;
};
-
irq是中断号(Linux IRQ) -
hwirq是硬件中断号,局限于irq_data.domain中断域 -
common指向所有 irqchips 共享的数据 -
chip表示底层中断控制器硬件访问 -
domain表示中断转换域,负责在 hwirq 号和 Linux irq 号之间进行映射 -
chip_data是每个芯片方法的特定于平台的芯片私有数据,以允许共享芯片实现
高级外围 IRQ 管理
在第三章 内核设施和辅助函数中,我们介绍了外围 IRQ,使用request_irq()和request_threaded_irq()。使用request_irq(),可以注册一个在原子上下文中执行的处理程序(顶半部),从中可以使用在同一章节中讨论的不同机制之一调度底半部。另一方面,使用request_thread_irq(),可以为函数提供顶部和底部,以便前者将作为 hardirq 处理程序运行,可以决定引发第二个线程处理程序,后者将在内核线程中运行。
这些方法的问题在于,有时,请求 IRQ 的驱动程序不知道提供此 IRQ 线的中断的性质,特别是当中断控制器是一个离散芯片(通常是通过 SPI 或 I2C 总线连接的 GPIO 扩展器)时。现在有了request_any_context_irq(),请求 IRQ 的驱动程序知道处理程序是否在线程上下文中运行,并相应地调用request_threaded_irq()或request_irq()。这意味着无论我们的设备关联的 IRQ 来自可能不休眠的中断控制器(内存映射)还是来自可以休眠的中断控制器(在 I2C/SPI 总线后面),都不需要更改代码。它的原型如下:
int request_any_context_irq ( unsigned int irq, irq_handler_t handler,
unsigned long flags, const char * name, void * dev_id);
以下是函数中每个参数的含义:
-
irq表示要分配的中断线。 -
handler是在发生 IRQ 时要调用的函数。根据上下文,此函数可能作为 hardirq 运行,也可能作为线程运行。 -
flags表示中断类型标志。与request_irq()中的标志相同。 -
name将用于调试目的,在/proc/interrupts中命名中断。 -
dev_id是传递回处理程序函数的 cookie。
request_any_context_irq()表示可以获得 hardirq 或 treaded。它的工作方式类似于通常的request_irq(),只是它检查 IRQ 级别是否配置为嵌套,并调用正确的后端。换句话说,它根据上下文选择硬件中断或线程处理方法。此函数在失败时返回负值。成功时,它返回IRQC_IS_HARDIRQ或IRQC_IS_NESTED。以下是一个用例:
static irqreturn_t packt_btn_interrupt(int irq, void *dev_id)
{
struct btn_data *priv = dev_id;
input_report_key(priv->i_dev, BTN_0,
gpiod_get_value(priv->btn_gpiod) & 1);
input_sync(priv->i_dev);
return IRQ_HANDLED;
}
static int btn_probe(struct platform_device *pdev)
{
struct gpio_desc *gpiod;
int ret, irq;
[...]
gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(gpiod))
return -ENODEV;
priv->irq = gpiod_to_irq(priv->btn_gpiod);
priv->btn_gpiod = gpiod;
[...]
ret = request_any_context_irq(priv->irq,
packt_btn_interrupt,
(IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING),
"packt-input-button", priv);
if (ret < 0) {
dev_err(&pdev->dev,
"Unable to acquire interrupt for GPIO line\n");
goto err_btn;
}
return ret;
}
上述代码是输入设备驱动程序的驱动程序示例的摘录。实际上,它是下一章中使用的代码。使用request_any_context_irq()的优势在于,不需要关心在 IRQ 处理程序中可以做什么,因为处理程序将运行的上下文取决于提供 IRQ 线的中断控制器。在我们的示例中,如果 GPIO 属于坐落在 I2C 或 SPI 总线上的控制器,处理程序将是线程化的。否则,处理程序将在 hardirq 中运行。
中断请求和传播
让我们考虑以下图,它表示链接的 IRQ 流

中断请求始终在 Linux IRQ 上执行(而不是 hwirq)。在 Linux 上请求 IRQ 的一般函数是request_threaded_irq()或request_irq(),它在内部调用前者:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
当调用该函数时,该函数使用irq_to_desc()宏提取与 IRQ 关联的struct irq_desc,然后分配一个新的struct irqaction结构并设置它,填充处理程序、标志等参数。
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
该函数最终通过调用__setup_irq()(通过setup_irq())函数将描述符插入/注册到适当的 IRQ 列表中,该函数在kernel/irq/manage.c中定义。
现在,当发生中断时,内核会执行一些汇编代码以保存当前状态,并跳转到特定于体系结构的处理程序handle_arch_irq,该处理程序在arch/arm/kernel/setup.c的setup_arch()函数中的我们平台的struct machine_desc的handle_irq字段中设置:
handle_arch_irq = mdesc->handle_irq
对于使用 ARM GIC 的 SoC,handle_irq回调使用gic_handle_irq设置,可以在drivers/irqchip/irq-gic.c或drivers/irqchip/irq-gic-v3.c中找到:
set_handle_irq(gic_handle_irq);
gic_handle_irq()调用handle_domain_irq(),执行generic_handle_irq(),然后调用generic_handle_irq_desc(),最终调用desc->handle_irq()。查看include/linux/irqdesc.h以获取最后一次调用,查看arch/arm/kernel/irq.c以获取其他函数调用。handle_irq是实际的流处理程序调用,我们将其注册为mcp23016_irq_handler。
gic_hande_irq()是一个 GIC 中断处理程序。generic_handle_irq()将执行 SoC 的 GPIO4 IRQ 的处理程序,该处理程序将寻找负责中断的 GPIO 引脚,并调用generic_handle_irq_desc(),依此类推。现在您已经熟悉了中断传播,让我们通过编写自己的中断控制器来切换到一个实际的例子。
链接 IRQ
本节描述了父级中断处理程序如何调用子级中断处理程序,进而调用它们的子级中断处理程序,依此类推。内核提供了两种方法来在父级(中断控制器)设备的 IRQ 处理程序中调用子设备的中断处理程序,这些方法是链接和嵌套方法:
链接中断
这种方法用于 SoC 的内部 GPIO 控制器,它们是内存映射的,其访问不会休眠。链接意味着这些中断只是函数调用链(例如,SoC 的 GPIO 模块中断处理程序是从 GIC 中断处理程序调用的,就像函数调用一样)。generic_handle_irq()用于链接子 IRQ 处理程序,并在父 hwirq 处理程序内调用。即使在子中断处理程序内部,我们仍然处于原子上下文(硬件中断)。不能调用可能休眠的函数。
嵌套中断
这种方法用于坐在慢总线上的控制器,比如 I2C(例如,GPIO 扩展器),其访问可能会休眠(I2C 函数可能会休眠)。嵌套意味着这些中断处理程序不在硬件上下文中运行(它们实际上不是 hwirq,它们不在原子上下文中),而是线程化的,可以被抢占(或被另一个中断中断)。handle_nested_irq()用于创建嵌套中断子 IRQ。处理程序在handle_nested_irq()函数创建的新线程内部被调用;我们需要它们在进程上下文中运行,以便我们可以调用可能会休眠的总线函数(比如可能会休眠的 I2C 函数)。
案例研究- GPIO 和 IRQ 芯片
让我们考虑下面的图,它将一个中断控制器设备与另一个设备连接起来,我们将用它来描述中断复用:

mcp23016 IRQ 流程
假设您已将io_1和io_2配置为中断。即使中断发生在io_1或io_2上,相同的中断线也会触发中断控制器。现在 GPIO 驱动程序必须找出读取 GPIO 的中断状态寄存器,以找出哪个中断(io_1或io_2)实际上已触发。因此,在这种情况下,单个中断线是 16 个 GPIO 中断的复用。
现在让我们修改原始的 mcp23016 驱动程序,该驱动程序在第十五章中编写,GPIO 控制器驱动程序 - gpio_chip,以支持首先作为中断控制器的 IRQ 域 API。第二部分将介绍新的和推荐的 gpiolib irqchip API。这将被用作逐步指南来编写中断控制器驱动程序,至少对于 GPIO 控制器。
传统 GPIO 和 IRQ 芯片
- 第一步,为我们的 gpiochip 分配一个
struct irq_domain,它将存储 hwirq 和 virq 之间的映射。线性映射对我们来说是合适的。我们在probe函数中这样做。该域将保存我们的驱动程序希望提供的 IRQ 数量。例如,对于 I/O 扩展器,IRQ 的数量可以是扩展器提供的 GPIO 数量:
my_gpiochip->irq_domain = irq_domain_add_linear( client->dev.of_node,
my_gpiochip->chip.ngpio, &mcp23016_irq_domain_ops, NULL);
host_data参数是NULL。因此,您可以传递任何您需要的数据结构。在分配域之前,我们的域 ops 结构应该被定义:
static struct irq_domain_ops mcp23016_irq_domain_ops = {
.map = mcp23016_irq_domain_map,
.xlate = irq_domain_xlate_twocell,
};
在填充我们的 IRQ 域 ops 结构之前,我们必须至少定义.map()回调:
static int mcp23016_irq_domain_map(
struct irq_domain *domain,
unsigned int virq, irq_hw_number_t hw)
{
irq_set_chip_and_handler(virq,
&dummy_irq_chip, /* Dumb irqchip */
handle_level_irq); /* Level trigerred irq */
return 0;
}
我们的控制器不够智能。因此,没有必要设置irq_chip。我们将使用内核为这种芯片提供的一个:dummy_irq_chip。有些控制器足够智能,需要设置irq_chip。在drivers/gpio/gpio-mcp23s08.c中查看。
下一个 ops 回调是.xlate。在这里,我们再次使用内核提供的帮助程序。irq_domain_xlate_twocell是一个能够解析具有两个单元的中断指定符的帮助程序。我们可以在我们的控制器 DT 节点中添加interrupt-cells = <2>;。
- 下一步是使用
irq_create_mapping()函数填充域与 IRQ 映射。在我们的驱动程序中,我们将在gpiochip.to_irq回调中执行此操作,这样每当有人在 GPIO 上调用gpio{d}_to_irq()时,如果映射存在,它将被返回,如果不存在,它将被创建:
static int mcp23016_to_irq(struct gpio_chip *chip,
unsigned offset)
{
return irq_create_mapping(chip->irq_domain, offset);
}
我们可以在probe函数中为每个 GPIO 都这样做,并在.to_irq函数中只调用irq_find_mapping()。
- 现在仍然在
probe函数中,我们需要注册我们控制器的 IRQ 处理程序,这个处理程序负责调用引发其引脚中断的正确处理程序:
devm_request_threaded_irq(client->irq, NULL,
mcp23016_irq, irqflags,
dev_name(chip->parent), mcp);
在注册 IRQ 之前,函数mcp23016应该已经被定义:
static irqreturn_t mcp23016_irq(int irq, void *data)
{
struct mcp23016 *mcp = data;
unsigned int child_irq, i;
/* Do some stuff */
[...]
for (i = 0; i < mcp->chip.ngpio; i++) {
if (gpio_value_changed_and_raised_irq(i)) {
child_irq =
irq_find_mapping(mcp->chip.irqdomain, i);
handle_nested_irq(child_irq);
}
}
return IRQ_HANDLED;
}
handle_nested_irq()已经在前面的部分中描述,将为每个注册的处理程序创建一个专用线程。
新的 gpiolib irqchip API
几乎每个 GPIO 控制器驱动程序都在使用 IRQ 域来实现相同的目的。内核开发人员决定将这些代码移动到 gpiolib 框架中,通过GPIOLIB_IRQCHIP Kconfig 符号,以便协调开发并避免冗余代码。
该代码部分有助于处理 GPIO irqchips 和相关的irq_domain和资源分配回调,以及它们的设置,使用减少的帮助函数集。这些是gpiochip_irqchip_add()和gpiochip_set_chained_irqchip()。
gpiochip_irqchip_add(): 这将一个 irqchip 添加到一个 gpiochip 中。这个函数的作用是:
-
将
gpiochip.to_irq字段设置为gpiochip_to_irq,这是一个 IRQ 回调,只返回irq_find_mapping(chip->irqdomain, offset); -
使用
irq_domain_add_simple()函数为 gpiochip 分配一个 irq_domain,传递一个内核 IRQ 核心irq_domain_ops,称为gpiochip_domain_ops,并在drivers/gpio/gpiolib.c中定义。 -
使用
irq_create_mapping()函数从 0 到gpiochip.ngpio创建映射
它的原型如下:
int gpiochip_irqchip_add(struct gpio_chip *gpiochip,
struct irq_chip *irqchip,
unsigned int first_irq,
irq_flow_handler_t handler,
unsigned int type)
gpiochip 是我们的 GPIO 芯片,要添加 irqchip 到其中,irqchip 是要添加到 gpiochip 的 irqchip。如果没有动态分配,first_irq 是要从中分配 gpiochip IRQ 的基础(第一个)IRQ。handler 是要使用的 IRQ 处理程序(通常是预定义的 IRQ 核心函数),type 是该 irqchip 上 IRQ 的默认类型,传递 IRQ_TYPE_NONE 以使核心避免在硬件中设置任何默认类型。
此函数将处理两个单元格的简单 IRQ(因为它将 irq_domain_ops.xlate 设置为 irq_domain_xlate_twocell),并假定 gpiochip 上的所有引脚都可以生成唯一的 IRQ。
static const struct irq_domain_ops gpiochip_domain_ops = {
.map = gpiochip_irq_map,
.unmap = gpiochip_irq_unmap,
/* Virtually all GPIO irqchips are twocell:ed */
.xlate = irq_domain_xlate_twocell,
};
gpiochip_set_chained_irqchip():此函数将链式 irqchip 设置为从父 IRQ 到 gpio_chip,并将 struct gpio_chip 的指针传递为处理程序数据:
void gpiochip_set_chained_irqchip(struct gpio_chip *gpiochip,
struct irq_chip *irqchip, int parent_irq,
irq_flow_handler_t parent_handler)
parent_irq 是此芯片连接到的 IRQ 号。在我们的 mcp23016 中,如 Case study-GPIO and IRQ chip 部分中的图所示,它对应于 gpio4_29 线的 IRQ。换句话说,它是此链式 irqchip 的父 IRQ 号。parent_handler 是累积的从 gpiochip 出来的 IRQ 的父中断处理程序。如果中断是嵌套而不是级联的,可以在此处理程序参数中传递 NULL。
有了这个新的 API,我们的 probe 函数中需要添加的唯一代码是:
/* Do we have an interrupt line? Enable the irqchip */
if (client->irq) {
status = gpiochip_irqchip_add(&gpio->chip, &dummy_irq_chip,
0, handle_level_irq, IRQ_TYPE_NONE);
if (status) {
dev_err(&client->dev, "cannot add irqchip\n");
goto fail_irq;
}
status = devm_request_threaded_irq(&client->dev, client->irq,
NULL, mcp23016_irq, IRQF_ONESHOT |
IRQF_TRIGGER_FALLING | IRQF_SHARED,
dev_name(&client->dev), gpio);
if (status)
goto fail_irq;
gpiochip_set_chained_irqchip(&gpio->chip,
&dummy_irq_chip, client->irq, NULL);
}
IRQ 核心为我们做了一切。甚至不需要定义 gpiochip.to_irq 函数,因为 API 已经设置了它。我们的示例使用了 IRQ 核心 dummy_irq_chip,但也可以自己定义。自内核 v4.10 版本以来,还添加了另外两个函数:gpiochip_irqchip_add_nested() 和 gpiochip_set_nested_irqchip()。请查看 Documentation/gpio/driver.txt 了解更多细节。在同一内核版本中使用此 API 的驱动程序是 drivers/gpio/gpio-mcp23s08.c。
中断控制器和 DT
现在我们将在 DT 中声明我们的控制器。如果你还记得第六章:设备树的概念,每个中断控制器必须具有设置为布尔属性 interrupt-controller 的属性。第二个强制性的布尔属性是 gpio-controller,因为它也是 GPIO 控制器。我们需要定义我们的设备的中断描述符需要多少个单元格。由于我们已将 irq_domain_ops.xlate 字段设置为 irq_domain_xlate_twocell,#interrupt-cells 应该是 2:
expander: mcp23016@20 {
compatible = "microchip,mcp23016";
reg = <0x20>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-controller;
#gpio-cells = <2>;
interrupt-parent = <&gpio4>;
interrupts = <29 IRQ_TYPE_EDGE_FALLING>;
};
interrupt-parent 和 interrupts 属性描述了中断线连接。
最后,让我们说一下,我们有一个 mcp23016 的驱动程序,以及两个其他设备的驱动程序:foo_device 和 bar_device,当然都在 CPU 上运行。在 foo_device 驱动程序中,我们希望在 mcp23016 的 io_2 引脚发生变化时请求中断。bar_device 驱动程序需要分别用于复位和电源 GPIO 的 io_8 和 io_12。让我们在 DT 中声明这一点:
foo_device: foo_device@1c {
reg = <0x1c>;
interrupt-parent = <&expander>;
interrupts = <2 IRQ_TYPE_EDGE_RISING>;
};
bar_device {
reset-gpios = <&expander 8 GPIO_ACTIVE_HIGH>;
power-gpios = <&expander 12 GPIO_ACTIVE_HIGH>;
/* Other properties do here */
};
总结
现在 IRQ 多路复用对你来说已经没有秘密了。我们讨论了 Linux 系统下 IRQ 管理的最重要的元素,即 IRQ 域 API。你已经掌握了开发中断控制器驱动程序的基础,以及从 DT 中管理它们的绑定。我们讨论了 IRQ 传播,以便了解从请求到处理的过程。这一章将帮助你理解下一章中的中断驱动部分,该部分涉及输入设备驱动程序。
第十七章:输入设备驱动程序
输入设备是可以与系统交互的设备。这些设备是按钮、键盘、触摸屏、鼠标等。它们通过发送事件来工作,由输入核心捕获并广播到系统中。本章将解释输入核心用于处理输入设备的每个结构。也就是说,我们将看到如何从用户空间管理事件。
在本章中,我们将涵盖以下主题:
-
输入核心数据结构
-
分配和注册输入设备,以及轮询设备系列
-
生成并向输入核心报告事件
-
用户空间的输入设备
-
编写驱动程序示例
输入设备结构
首先,要与输入子系统进行接口的主文件是 linux/input.h:
#include <linux/input.h>
无论输入设备的类型是什么,它发送的事件的类型是什么,输入设备在内核中都表示为 struct input_dev 的实例:
struct input_dev {
const char *name;
const char *phys;
unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
unsigned int repeat_key;
int rep[REP_CNT];
struct input_absinfo *absinfo;
unsigned long key[BITS_TO_LONGS(KEY_CNT)];
int (*open)(struct input_dev *dev);
void (*close)(struct input_dev *dev);
unsigned int users;
struct device dev;
unsigned int num_vals;
unsigned int max_vals;
struct input_value *vals;
bool devres_managed;
};
字段的含义如下:
-
name表示设备的名称。 -
phys是设备在系统层次结构中的物理路径。 -
evbit是设备支持的事件类型的位图。一些类型的区域如下: -
EV_KEY用于支持发送键事件(键盘、按钮等)的设备。 -
EV_REL用于支持发送相对位置的设备(鼠标、数字化器等) -
EV_ABS用于支持发送绝对位置(游戏手柄)的设备
事件列表在内核源代码中的 include/linux/input-event-codes.h 文件中可用。我们使用 set_bit() 宏来根据我们的输入设备功能设置适当的位。当然,设备可以支持多种类型的事件。例如,鼠标将同时设置 EV_KEY 和 EV_REL。
set_bit(EV_KEY, my_input_dev->evbit);
set_bit(EV_REL, my_input_dev->evbit);
-
keybit用于启用EV_KEY类型的设备,是该设备公开的键/按钮的位图。例如,BTN_0,KEY_A,KEY_B等。键/按钮的完整列表在include/linux/input-event-codes.h文件中。 -
relbit用于启用EV_REL类型的设备,是设备的相对轴的位图。例如,REL_X,REL_Y,REL_Z,REL_RX等。请查看include/linux/input-event-codes.h获取完整列表。 -
absbit用于启用EV_ABS类型的设备,是设备的绝对轴的位图。例如,ABS_Y,ABS_X等。请查看相同的先前文件以获取完整列表。 -
mscbit用于启用EV_MSC类型的设备,是设备支持的各种杂项事件的位图。 -
repeat_key存储最后按下的键的键码;用于实现软件自动重复。 -
rep,自动重复参数(延迟、速率)的当前值。 -
absinfo是一个&struct input_absinfo元素的数组,其中包含有关绝对轴的信息(当前值、最小值、最大值、平坦值、模糊值、分辨率)。您应该使用input_set_abs_params()函数来设置这些值。
void input_set_abs_params(struct input_dev *dev, unsigned int axis,
int min, int max, int fuzz, int flat)
min和max指定了较低和较高的边界值。fuzz表示指定输入设备的指定通道上的预期噪音。以下是一个示例,我们仅设置每个通道的边界:
#define ABSMAX_ACC_VAL 0x01FF
#define ABSMIN_ACC_VAL -(ABSMAX_ACC_VAL)
[...]
set_bit(EV_ABS, idev->evbit);
input_set_abs_params(idev, ABS_X, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
input_set_abs_params(idev, ABS_Y, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
input_set_abs_params(idev, ABS_Z, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
-
key反映了设备键/按钮的当前状态。 -
open是在第一个用户调用input_open_device()时调用的方法。使用此方法来准备设备,例如中断请求、轮询线程启动等。 -
close在最后一个用户调用input_close_device()时被调用。在这里,您可以停止轮询(这会消耗大量资源)。 -
users存储了打开此设备的用户(输入处理程序)的数量。它被input_open_device()和input_close_device()使用,以确保只有在第一个用户打开设备时才调用dev->open(),并且在最后一个用户关闭设备时调用dev->close()。 -
dev是与此设备关联的设备结构(用于设备模型)。 -
num_vals是当前帧中排队的值的数量。 -
max_vals是在一个帧中排队的值的最大数量。 -
Vals是当前帧中排队的值的数组。 -
devres_managed表示设备由devres框架管理,不需要显式取消注册或释放。
分配和注册输入设备
在注册并向输入设备发送事件之前,应使用 input_allocate_device() 函数为其分配内存。为了释放先前为未注册的输入设备分配的内存,应使用 input_free_device() 函数。如果设备已经注册,应改用 input_unregister_device()。像每个需要内存分配的函数一样,我们可以使用函数的资源管理版本:
struct input_dev *input_allocate_device(void)
struct input_dev *devm_input_allocate_device(struct device *dev)
void input_free_device(struct input_dev *dev)
static void devm_input_device_unregister(struct device *dev,
void *res)
int input_register_device(struct input_dev *dev)
void input_unregister_device(struct input_dev *dev)
设备分配可能会休眠,因此不能在原子上下文中调用,也不能在持有自旋锁时调用。
以下是一个位于 I2C 总线上的输入设备的 probe 函数的摘录:
struct input_dev *idev;
int error;
idev = input_allocate_device();
if (!idev)
return -ENOMEM;
idev->name = BMA150_DRIVER;
idev->phys = BMA150_DRIVER "/input0";
idev->id.bustype = BUS_I2C;
idev->dev.parent = &client->dev;
set_bit(EV_ABS, idev->evbit);
input_set_abs_params(idev, ABS_X, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
input_set_abs_params(idev, ABS_Y, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
input_set_abs_params(idev, ABS_Z, ABSMIN_ACC_VAL,
ABSMAX_ACC_VAL, 0, 0);
error = input_register_device(idev);
if (error) {
input_free_device(idev);
return error;
}
error = request_threaded_irq(client->irq,
NULL, my_irq_thread,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
BMA150_DRIVER, NULL);
if (error) {
dev_err(&client->dev, "irq request failed %d, error %d\n",
client->irq, error);
input_unregister_device(bma150->input);
goto err_free_mem;
}
轮询输入设备子类
轮询输入设备是一种特殊类型的输入设备,它依赖轮询来感知设备状态的变化,而通用输入设备类型依赖于 IRQ 来感知变化并将事件发送到输入核心。
内核中描述了一个轮询输入设备,它是 struct input_polled_dev 结构的一个实例,它是通用 struct input_dev 结构的一个包装器:
struct input_polled_dev {
void *private;
void (*open)(struct input_polled_dev *dev);
void (*close)(struct input_polled_dev *dev);
void (*poll)(struct input_polled_dev *dev);
unsigned int poll_interval; /* msec */
unsigned int poll_interval_max; /* msec */
unsigned int poll_interval_min; /* msec */
struct input_dev *input;
bool devres_managed;
};
这个结构中元素的含义如下:
-
private是驱动程序的私有数据。 -
open是一个可选的方法,用于准备设备进行轮询(启用设备,可能刷新设备状态)。 -
close是一个可选的方法,当设备不再被轮询时调用。它用于将设备置于低功耗模式。 -
poll是一个强制性的方法,每当需要轮询设备时都会调用。它以poll_interval的频率调用。 -
poll_interval是应调用poll()方法的频率。默认为 500 毫秒,除非在注册设备时被覆盖。 -
poll_interval_max指定了轮询间隔的上限。默认为poll_interval的初始值。 -
poll_interval_min指定了轮询间隔的下限。默认为 0。 -
input是轮询设备构建的输入设备。它必须由驱动程序正确初始化(ID、名称、位)。轮询输入设备只提供了一个接口,用于使用轮询而不是 IRQ 来感知设备状态变化。
使用 input_allocate_polled_device() 和 input_free_polled_device() 来分配/释放 struct input_polled_dev 结构。您应该注意初始化其中嵌入的 struct input_dev 的强制性字段。轮询间隔也应该设置,否则默认为 500 毫秒。也可以使用资源管理版本。两个原型如下:
struct input_polled_dev *devm_input_allocate_polled_device(struct device *dev)
struct input_polled_dev *input_allocate_polled_device(void)
void input_free_polled_device(struct input_polled_dev *dev)
对于资源管理的设备,输入核心将设置字段 input_dev->devres_managed 为 true。
在分配和正确初始化字段之后,可以使用 input_register_polled_device() 注册轮询输入设备,成功时返回 0。反向操作(取消注册)使用 input_unregister_polled_device() 函数完成:
int input_register_polled_device(struct input_polled_dev *dev)
void input_unregister_polled_device(struct input_polled_dev *dev)
这样的设备的 probe() 函数的典型示例如下:
static int button_probe(struct platform_device *pdev)
{
struct my_struct *ms;
struct input_dev *input_dev;
int retval;
ms = devm_kzalloc(&pdev->dev, sizeof(*ms), GFP_KERNEL);
if (!ms)
return -ENOMEM;
ms->poll_dev = input_allocate_polled_device();
if (!ms->poll_dev){
kfree(ms);
return -ENOMEM;
}
/* This gpio is not mapped to IRQ */
ms->reset_btn_desc = gpiod_get(dev, "reset", GPIOD_IN);
ms->poll_dev->private = ms ;
ms->poll_dev->poll = my_btn_poll;
ms->poll_dev->poll_interval = 200; /* Poll every 200ms */
ms->poll_dev->open = my_btn_open; /* consist */
input_dev = ms->poll_dev->input;
input_dev->name = "System Reset Btn";
/* The gpio belong to an expander sitting on I2C */
input_dev->id.bustype = BUS_I2C;
input_dev->dev.parent = &pdev->dev;
/* Declare the events generated by this driver */
set_bit(EV_KEY, input_dev->evbit);
set_bit(BTN_0, input_dev->keybit); /* buttons */
retval = input_register_polled_device(mcp->poll_dev);
if (retval) {
dev_err(&pdev->dev, "Failed to register input device\n");
input_free_polled_device(ms->poll_dev);
kfree(ms);
}
return retval;
}
以下是我们的 struct my_struct 结构的样子:
struct my_struct {
struct gpio_desc *reset_btn_desc;
struct input_polled_dev *poll_dev;
}
以下是 open 函数的样子:
static void my_btn_open(struct input_polled_dev *poll_dev)
{
struct my_strut *ms = poll_dev->private;
dev_dbg(&ms->poll_dev->input->dev, "reset open()\n");
}
open 方法用于准备设备所需的资源。对于这个例子,我们实际上不需要这个方法。
生成和报告输入事件
设备分配和注册是必不可少的,但它们不是输入设备驱动程序的主要目标,输入设备驱动程序旨在向输入核心报告。根据设备支持的事件类型,内核提供了适当的 API 来将它们报告给核心。
给定一个支持 EV_XXX 的设备,相应的报告函数将是 input_report_xxx() 。以下表格显示了最重要的事件类型及其报告函数之间的映射关系:
| 事件类型 | 报告函数 | 代码示例 |
|---|---|---|
EV_KEY |
input_report_key() |
input_report_key(poll_dev->input, BTN_0, gpiod_get_value(ms-> reset_btn_desc) & 1) ; |
EV_REL |
input_report_rel() |
input_report_rel(nunchuk->input, REL_X, (nunchuk->report.joy_x - 128)/10) ; |
EV_ABS |
input_report_abs() |
input_report_abs(bma150->input, ABS_X, x_value) ;input_report_abs(bma150->input, ABS_Y, y_value) ;input_report_abs(bma150->input, ABS_Z, z_value) ; |
它们的原型如下:
void input_report_abs(struct input_dev *dev,
unsigned int code, int value)
void input_report_key(struct input_dev *dev,
unsigned int code, int value)
void input_report_rel(struct input_dev *dev,
unsigned int code, int value)
可用报告函数的列表可以在内核源文件 include/linux/input.h 中找到。它们都具有相同的框架:
-
dev是负责事件的输入设备。 -
code表示事件代码,例如REL_X或KEY_BACKSPACE。完整的列表在include/linux/input-event-codes.h中。 -
value是事件携带的值。对于EV_REL事件类型,它携带相对变化。对于EV_ABS(如摇杆等)事件类型,它包含绝对的新值。对于EV_KEY事件类型,应设置为0表示按键释放,1表示按键按下,2表示自动重复。
在报告了所有更改之后,驱动程序应调用 input_sync() 来指示输入设备此事件已完成。输入子系统将这些事件收集到一个数据包中,并通过 /dev/input/event<X> 发送,这是表示系统上的 struct input_dev 的字符设备,其中 <X> 是输入核心分配给驱动程序的接口号:
void input_sync(struct input_dev *dev)
让我们看一个示例,这是 drivers/input/misc/bma150.c 中 bma150 数字加速传感器驱动程序的摘录:
static void threaded_report_xyz(struct bma150_data *bma150)
{
u8 data[BMA150_XYZ_DATA_SIZE];
s16 x, y, z;
s32 ret;
ret = i2c_smbus_read_i2c_block_data(bma150->client,
BMA150_ACC_X_LSB_REG, BMA150_XYZ_DATA_SIZE, data);
if (ret != BMA150_XYZ_DATA_SIZE)
return;
x = ((0xc0 & data[0]) >> 6) | (data[1] << 2);
y = ((0xc0 & data[2]) >> 6) | (data[3] << 2);
z = ((0xc0 & data[4]) >> 6) | (data[5] << 2);
/* sign extension */
x = (s16) (x << 6) >> 6;
y = (s16) (y << 6) >> 6;
z = (s16) (z << 6) >> 6;
input_report_abs(bma150->input, ABS_X, x);
input_report_abs(bma150->input, ABS_Y, y);
input_report_abs(bma150->input, ABS_Z, z);
/* Indicate this event is complete */
input_sync(bma150->input);
}
在前面的示例中,input_sync() 告诉核心将这三个报告视为同一事件。这是有道理的,因为位置有三个轴(X、Y、Z),我们不希望 X、Y 或 Z 分别报告。
报告事件的最佳位置是在轮询设备的 poll 函数中,或者在启用了 IRQ 的设备的 IRQ 例程(线程部分或非线程部分)中。如果执行了可能休眠的操作,应在 IRQ 处理的线程部分内处理报告:
static void my_btn_poll(struct input_polled_dev *poll_dev)
{
struct my_struct *ms = poll_dev->private;
struct i2c_client *client = mcp->client;
input_report_key(poll_dev->input, BTN_0,
gpiod_get_value(ms->reset_btn_desc) & 1);
input_sync(poll_dev->input);
}
用户空间接口
每个注册的输入设备都由 /dev/input/event<X> 字符设备表示,我们可以从用户空间读取该设备的事件。读取此文件的应用程序将以 struct input_event 格式接收事件数据包:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
}
让我们看看结构中每个元素的含义:
-
time是时间戳,它返回事件发生的时间。 -
type是事件类型。例如,EV_KEY表示按键按下或释放,EV_REL表示相对移动,EV_ABS表示绝对移动。更多类型在include/linux/input-event-codes.h中定义。 -
code是事件代码,例如:REL_X或KEY_BACKSPACE,完整的列表在include/linux/input-event-codes.h中。 -
value是事件携带的值。对于EV_REL事件类型,它携带相对变化。对于EV_ABS(如摇杆等)事件类型,它包含绝对的新值。对于EV_KEY事件类型,应设置为0表示按键释放,1表示按键按下,2表示自动重复。
用户空间应用程序可以使用阻塞和非阻塞读取,还可以使用 poll() 或 select() 系统调用来在打开此设备后接收事件通知。以下是一个使用 select() 系统调用的示例,完整的源代码在书籍源代码库中提供:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/input.h>
#include <sys/select.h>
#define INPUT_DEVICE "/dev/input/event1"
int main(int argc, char **argv)
{
int fd;
struct input_event event;
ssize_t bytesRead;
int ret;
fd_set readfds;
fd = open(INPUT_DEVICE, O_RDONLY);
/* Let's open our input device */
if(fd < 0){
fprintf(stderr, "Error opening %s for reading", INPUT_DEVICE);
exit(EXIT_FAILURE);
}
while(1){
/* Wait on fd for input */
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
ret = select(fd + 1, &readfds, NULL, NULL, NULL);
if (ret == -1) {
fprintf(stderr, "select call on %s: an error ocurred",
INPUT_DEVICE);
break;
}
else if (!ret) { /* If we have decided to use timeout */
fprintf(stderr, "select on %s: TIMEOUT", INPUT_DEVICE);
break;
}
/* File descriptor is now ready */
if (FD_ISSET(fd, &readfds)) {
bytesRead = read(fd, &event,
sizeof(struct input_event));
if(bytesRead == -1)
/* Process read input error*/
if(bytesRead != sizeof(struct input_event))
/* Read value is not an input even */
/*
* We could have done a switch/case if we had
* many codes to look for
*/
if(event.code == BTN_0) {
/* it concerns our button */
if(event.value == 0){
/* Process Release */
[...]
}
else if(event.value == 1){
/* Process KeyPress */
[...]
}
}
}
}
close(fd);
return EXIT_SUCCESS;
}
将所有内容整合在一起
到目前为止,我们已经描述了在编写输入设备驱动程序时使用的结构,以及它们如何可以从用户空间进行管理。
-
根据其类型,轮询或非轮询,使用
input_allocate_polled_device()或input_allocate_device()分配新的输入设备。 -
填写强制字段或不填写(如果有必要):
-
- 通过在
input_dev.evbit字段上使用set_bit()辅助宏指定设备支持的事件类型
- 通过在
-
根据事件类型,
EV_REL、EV_ABS、EV_KEY或其他,指定此设备可以报告的代码,使用input_dev.relbit、input_dev.absbit、input_dev.keybit或其他。 -
指定
input_dev.dev以设置正确的设备树 -
如有必要,填写
abs_信息 -
对于轮询设备,请指定应调用
poll()函数的间隔:
-
如果有必要,请编写您的
open()函数,在其中应准备和设置设备使用的资源。此函数仅调用一次。在此函数中,设置 GPIO,如有需要请求中断,初始化设备。 -
编写您的
close()函数,在其中释放和释放open()函数中完成的内容。例如,释放 GPIO,IRQ,将设备置于省电模式。 -
将您的
open()或close()函数(或两者)传递给input_dev.open和input_dev.close字段。 -
如果是轮询的,请使用
input_register_polled_device()注册您的设备,如果不是,请使用input_register_device()。 -
在您的 IRQ 函数(线程化或非线程化)或
poll()函数中,根据事件类型收集和报告事件,使用input_report_key()、input_report_rel()、input_report_abs()或其他,并在输入设备上调用input_sync()以指示帧结束(报告完成)。
通常的方法是,如果没有提供 IRQ,则使用经典输入设备,否则回退到轮询设备:
if(client->irq > 0){
/* Use generic input device */
} else {
/* Use polled device */
}
查看如何从用户空间管理这些设备,请参考书籍源代码中提供的示例。
驱动程序示例
可以总结以下两个驱动程序。第一个是基于未映射到 IRQ 的 GPIO 的轮询输入设备。轮询输入核心将轮询 GPIO 以检测任何变化。此驱动程序配置为发送 0 键代码。每个 GPIO 状态对应于按键按下或释放:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/of.h> /* For DT*/
#include <linux/platform_device.h> /* For platform devices */
#include <linux/gpio/consumer.h> /* For GPIO Descriptor interface */
#include <linux/input.h>
#include <linux/input-polldev.h>
struct poll_btn_data {
struct gpio_desc *btn_gpiod;
struct input_polled_dev *poll_dev;
};
static void polled_btn_open(struct input_polled_dev *poll_dev)
{
/* struct poll_btn_data *priv = poll_dev->private; */
pr_info("polled device opened()\n");
}
static void polled_btn_close(struct input_polled_dev *poll_dev)
{
/* struct poll_btn_data *priv = poll_dev->private; */
pr_info("polled device closed()\n");
}
static void polled_btn_poll(struct input_polled_dev *poll_dev)
{
struct poll_btn_data *priv = poll_dev->private;
input_report_key(poll_dev->input, BTN_0, gpiod_get_value(priv->btn_gpiod) & 1);
input_sync(poll_dev->input);
}
static const struct of_device_id btn_dt_ids[] = {
{ .compatible = "packt,input-polled-button", },
{ /* sentinel */ }
};
static int polled_btn_probe(struct platform_device *pdev)
{
struct poll_btn_data *priv;
struct input_polled_dev *poll_dev;
struct input_dev *input_dev;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
poll_dev = input_allocate_polled_device();
if (!poll_dev){
devm_kfree(&pdev->dev, priv);
return -ENOMEM;
}
/* We assume this GPIO is active high */
priv->btn_gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN);
poll_dev->private = priv;
poll_dev->poll_interval = 200; /* Poll every 200ms */
poll_dev->poll = polled_btn_poll;
poll_dev->open = polled_btn_open;
poll_dev->close = polled_btn_close;
priv->poll_dev = poll_dev;
input_dev = poll_dev->input;
input_dev->name = "Packt input polled Btn";
input_dev->dev.parent = &pdev->dev;
/* Declare the events generated by this driver */
set_bit(EV_KEY, input_dev->evbit);
set_bit(BTN_0, input_dev->keybit); /* buttons */
ret = input_register_polled_device(priv->poll_dev);
if (ret) {
pr_err("Failed to register input polled device\n");
input_free_polled_device(poll_dev);
devm_kfree(&pdev->dev, priv);
return ret;
}
platform_set_drvdata(pdev, priv);
return 0;
}
static int polled_btn_remove(struct platform_device *pdev)
{
struct poll_btn_data *priv = platform_get_drvdata(pdev);
input_unregister_polled_device(priv->poll_dev);
input_free_polled_device(priv->poll_dev);
gpiod_put(priv->btn_gpiod);
return 0;
}
static struct platform_driver mypdrv = {
.probe = polled_btn_probe,
.remove = polled_btn_remove,
.driver = {
.name = "input-polled-button",
.of_match_table = of_match_ptr(btn_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Polled input device");
这第二个驱动程序根据按钮的 GPIO 映射到的 IRQ 向输入核心发送事件。当使用 IRQ 来检测按键按下或释放时,最好在边缘变化时触发中断:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/of.h> /* For DT*/
#include <linux/platform_device.h> /* For platform devices */
#include <linux/gpio/consumer.h> /* For GPIO Descriptor interface */
#include <linux/input.h>
#include <linux/interrupt.h>
struct btn_data {
struct gpio_desc *btn_gpiod;
struct input_dev *i_dev;
struct platform_device *pdev;
int irq;
};
static int btn_open(struct input_dev *i_dev)
{
pr_info("input device opened()\n");
return 0;
}
static void btn_close(struct input_dev *i_dev)
{
pr_info("input device closed()\n");
}
static irqreturn_t packt_btn_interrupt(int irq, void *dev_id)
{
struct btn_data *priv = dev_id;
input_report_key(priv->i_dev, BTN_0, gpiod_get_value(priv->btn_gpiod) & 1);
input_sync(priv->i_dev);
return IRQ_HANDLED;
}
static const struct of_device_id btn_dt_ids[] = {
{ .compatible = "packt,input-button", },
{ /* sentinel */ }
};
static int btn_probe(struct platform_device *pdev)
{
struct btn_data *priv;
struct gpio_desc *gpiod;
struct input_dev *i_dev;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
i_dev = input_allocate_device();
if (!i_dev)
return -ENOMEM;
i_dev->open = btn_open;
i_dev->close = btn_close;
i_dev->name = "Packt Btn";
i_dev->dev.parent = &pdev->dev;
priv->i_dev = i_dev;
priv->pdev = pdev;
/* Declare the events generated by this driver */
set_bit(EV_KEY, i_dev->evbit);
set_bit(BTN_0, i_dev->keybit); /* buttons */
/* We assume this GPIO is active high */
gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(gpiod))
return -ENODEV;
priv->irq = gpiod_to_irq(priv->btn_gpiod);
priv->btn_gpiod = gpiod;
ret = input_register_device(priv->i_dev);
if (ret) {
pr_err("Failed to register input device\n");
goto err_input;
}
ret = request_any_context_irq(priv->irq,
packt_btn_interrupt,
(IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING),
"packt-input-button", priv);
if (ret < 0) {
dev_err(&pdev->dev,
"Unable to acquire interrupt for GPIO line\n");
goto err_btn;
}
platform_set_drvdata(pdev, priv);
return 0;
err_btn:
gpiod_put(priv->btn_gpiod);
err_input:
printk("will call input_free_device\n");
input_free_device(i_dev);
printk("will call devm_kfree\n");
return ret;
}
static int btn_remove(struct platform_device *pdev)
{
struct btn_data *priv;
priv = platform_get_drvdata(pdev);
input_unregister_device(priv->i_dev);
input_free_device(priv->i_dev);
free_irq(priv->irq, priv);
gpiod_put(priv->btn_gpiod);
return 0;
}
static struct platform_driver mypdrv = {
.probe = btn_probe,
.remove = btn_remove,
.driver = {
.name = "input-button",
.of_match_table = of_match_ptr(btn_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Input device (IRQ based)");
对于这两个示例,当设备与模块匹配时,将在/dev/input目录中创建一个节点。该节点对应于我们示例中的event0。可以使用udevadm工具来显示有关设备的信息:
# udevadm info /dev/input/event0
P: /devices/platform/input-button.0/input/input0/event0
N: input/event0
S: input/by-path/platform-input-button.0-event
E: DEVLINKS=/dev/input/by-path/platform-input-button.0-event
E: DEVNAME=/dev/input/event0
E: DEVPATH=/devices/platform/input-button.0/input/input0/event0
E: ID_INPUT=1
E: ID_PATH=platform-input-button.0
E: ID_PATH_TAG=platform-input-button_0
E: MAJOR=13
E: MINOR=64
E: SUBSYSTEM=input
E: USEC_INITIALIZED=74842430
实际允许我们将事件键打印到屏幕的工具是evtest,给定输入设备的路径:
# evtest /dev/input/event0
input device opened()
Input driver version is 1.0.1
Input device ID: bus 0x0 vendor 0x0 product 0x0 version 0x0
Input device name: "Packt Btn"
Supported events:
Event type 0 (EV_SYN)
Event type 1 (EV_KEY)
Event code 256 (BTN_0)
由于第二个模块是基于 IRQ 的,可以轻松检查 IRQ 请求是否成功,并且它已被触发了多少次:
$ cat /proc/interrupts | grep packt
160: 0 0 0 0 gpio-mxc 0 packt-input-button
最后,可以连续按下/释放按钮,并检查 GPIO 的状态是否发生了变化:
$ cat /sys/kernel/debug/gpio | grep button
gpio-193 (button-gpio ) in hi
$ cat /sys/kernel/debug/gpio | grep button
gpio-193 (button-gpio ) in lo
总结
本章描述了整个输入框架,并突出了轮询和中断驱动输入设备之间的区别。在本章结束时,您将具备为任何输入驱动程序编写驱动程序的必要知识,无论其类型和支持的输入事件如何。还讨论了用户空间接口,并提供了示例。下一章将讨论另一个重要的框架,即 RTC,它是 PC 和嵌入式设备中时间管理的关键元素。
第十八章:RTC 驱动程序
实时时钟(RTC)是用于在非易失性存储器中跟踪绝对时间的设备,可以是内部到处理器,也可以是通过 I2C 或 SPI 总线外部连接的。
可以使用 RTC 执行以下操作:
-
读取和设置绝对时钟,并在时钟更新期间生成中断
-
生成周期性中断
-
设置闹钟
RTC 和系统时钟有不同的目的。前者是硬件时钟,以非易失性方式维护绝对时间和日期,而后者是由内核维护的软件时钟,用于实现gettimeofday(2)和time(2)系统调用,以及在文件上设置时间戳等。系统时钟报告从起始点开始的秒和微秒,定义为 POSIX 纪元:1970-01-01 00:00:00 +0000 (UTC)。
在本章中,我们将涵盖以下主题:
-
介绍 RTC 框架 API
-
描述此类驱动程序的架构,以及一个虚拟驱动程序示例
-
处理闹钟
-
从用户空间管理 RTC 设备,可以通过 sysfs 接口或使用 hwclock 工具
RTC 框架数据结构
在 Linux 系统上,RTC 框架使用三种主要数据结构。它们是strcut rtc_time,struct rtc_device和struct rtc_class_ops结构。前者是表示给定日期和时间的不透明结构;第二个结构表示物理 RTC 设备;最后一个表示驱动程序公开的一组操作,并由 RTC 核心用于读取/更新设备的日期/时间/闹钟。
从驱动程序中提取 RTC 函数所需的唯一标头是:
#include <linux/rtc.h>
同一个文件包含了前一节中列举的三个结构:
struct rtc_time {
int tm_sec; /* seconds after the minute */
int tm_min; /* minutes after the hour - [0, 59] */
int tm_hour; /* hours since midnight - [0, 23] */
int tm_mday; /* day of the month - [1, 31] */
int tm_mon; /* months since January - [0, 11] */
int tm_year; /* years since 1900 */
int tm_wday; /* days since Sunday - [0, 6] */
int tm_yday; /* days since January 1 - [0, 365] */
int tm_isdst; /* Daylight saving time flag */
};
此结构类似于<time.h>中的struct tm,用于传递时间。下一个结构是struct rtc_device,它代表内核中的芯片:
struct rtc_device {
struct device dev;
struct module *owner;
int id;
char name[RTC_DEVICE_NAME_SIZE];
const struct rtc_class_ops *ops;
struct mutex ops_lock;
struct cdev char_dev;
unsigned long flags;
unsigned long irq_data;
spinlock_t irq_lock;
wait_queue_head_t irq_queue;
struct rtc_task *irq_task;
spinlock_t irq_task_lock;
int irq_freq;
int max_user_freq;
struct work_struct irqwork;
};
以下是结构的元素的含义:
-
dev:这是设备结构。 -
owner:这是拥有此 RTC 设备的模块。使用THIS_MODULE就足够了。 -
id:这是内核为 RTC 设备分配的全局索引/dev/rtc<id>。 -
name:这是给 RTC 设备的名称。 -
ops:这是由 RTC 设备公开的一组操作(如读取/设置时间/闹钟),由核心或用户空间管理。 -
ops_lock:这是内核内部使用的互斥锁,用于保护 ops 函数调用。 -
cdev:这是与此 RTC 相关联的字符设备,/dev/rtc<id>。
下一个重要的结构是struct rtc_class_ops,它是一组用作回调的函数,用于在 RTC 设备上执行标准和有限的操作。它是顶层和底层 RTC 驱动程序之间的通信接口:
struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};
在前面的代码中,所有的钩子都以struct device结构作为参数,这与嵌入在struct rtc_device结构中的结构相同。这意味着从这些钩子中,可以随时访问 RTC 设备,使用to_rtc_device()宏,该宏建立在container_of()宏之上。
#define to_rtc_device(d) container_of(d, struct rtc_device, dev)
当用户空间对设备调用open(),release()和read_callback()函数时,内核会内部调用这些钩子。
read_time()是一个从设备读取时间并填充struct rtc_time输出参数的驱动程序函数。此函数应在成功时返回0,否则返回负错误代码。
set_time()是一个驱动程序函数,根据输入参数给定的struct rtc_time结构更新设备的时间。返回参数的备注与read_time函数相同。
如果您的设备支持闹钟功能,驱动程序应提供read_alarm()和set_alarm()来读取/设置设备上的闹钟。struct rtc_wkalrm将在后面的章节中描述。还应提供alarm_irq_enable()来启用闹钟。
RTC API
RTC 设备在内核中表示为struct rtc_device结构的实例。与其他内核框架设备注册不同(其中设备作为参数提供给注册函数),RTC 设备由核心构建并首先注册,然后rtc_device结构返回给驱动程序。使用rtc_device_register()函数将设备与内核构建和注册:
struct rtc_device *rtc_device_register(const char *name,
struct device *dev,
const struct rtc_class_ops *ops,
struct module *owner)
可以看到每个函数的每个参数的含义如下:
-
name:这是您的 RTC 设备名称。它可以是芯片的名称,例如:ds1343。 -
dev:这是父设备,用于设备模型的目的。例如,对于位于 I2C 或 SPI 总线上的芯片,dev可以使用spi_device.dev或i2c_client.dev进行设置。 -
ops:这是您的 RTC 操作,根据 RTC 具有的功能或驱动程序可以支持的功能进行填充。 -
owner:这是此 RTC 设备所属的模块。在大多数情况下,THIS_MODULE就足够了。
注册应该在probe函数中执行,显然,可以使用此函数的资源管理版本:
struct rtc_device *devm_rtc_device_register(struct device *dev,
const char *name,
const struct rtc_class_ops *ops,
struct module *owner)
这两个函数在成功时返回由内核构建的struct rtc_device结构的指针,或者返回一个指针错误,您应该使用IS_ERR和PTR_ERR宏。
相关的反向操作是rtc_device_unregister()和devm_ rtc_device_unregister():
void rtc_device_unregister(struct rtc_device *rtc)
void devm_rtc_device_unregister(struct device *dev,
struct rtc_device *rtc)
读取和设置时间
驱动程序负责提供用于读取和设置设备时间的函数。这是 RTC 驱动程序可以提供的最少功能。在读取方面,读取回调函数被给予一个已分配/清零的struct rtc_time结构的指针,驱动程序必须填充该结构。因此,RTC 几乎总是以二进制编码十进制(BCD)存储/恢复时间,其中每个四位数(4 位的一系列)代表 0 到 9 之间的数字(而不是 0 到 15 之间的数字)。内核提供了两个宏,bcd2bin()和bin2bcd(),分别用于将 BCD 编码转换为十进制,或将十进制转换为 BCD。接下来您应该注意的是一些rtc_time字段,它们具有一些边界要求,并且需要进行一些转换。数据以 BCD 形式从设备中读取,应使用bcd2bin()进行转换。
由于struct rtc_time结构比较复杂,内核提供了rtc_valid_tm()辅助函数,以验证给定的rtc_time结构,并在成功时返回0,表示该结构表示一个有效的日期/时间:
int rtc_valid_tm(struct rtc_time *tm);
以下示例描述了 RTC 读取操作的回调:
static int foo_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
struct foo_regs regs;
int error;
error = foo_device_read(dev, ®s, 0, sizeof(regs));
if (error)
return error;
tm->tm_sec = bcd2bin(regs.seconds);
tm->tm_min = bcd2bin(regs.minutes);
tm->tm_hour = bcd2bin(regs.cent_hours);
tm->tm_mday = bcd2bin(regs.date);
/*
* This device returns weekdays from 1 to 7
* But rtc_time.wday expect days from 0 to 6\.
* So we need to substract 1 to the value returned by the chip
*/
tm->tm_wday = bcd2bin(regs.day) - 1;
/*
* This device returns months from 1 to 12
* But rtc_time.tm_month expect a months 0 to 11\.
* So we need to substract 1 to the value returned by the chip
*/
tm->tm_mon = bcd2bin(regs.month) - 1;
/*
* This device's Epoch is 2000\.
* But rtc_time.tm_year expect years from Epoch 1900\.
* So we need to add 100 to the value returned by the chip
*/
tm->tm_year = bcd2bin(regs.years) + 100;
return rtc_valid_tm(tm);
}
在使用 BCD 转换函数之前,需要以下标头:
#include <linux/bcd.h>
在set_time函数中,输入参数是指向struct rtc_time的指针。该参数已经填充了要存储在 RTC 芯片中的值。不幸的是,这些值是十进制编码的,应在发送到芯片之前转换为 BCD。bin2bcd进行转换。对struct rtc_time结构的一些字段也应该引起注意。以下是描述通用set_time函数的伪代码:
static int foo_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
regs.seconds = bin2bcd(tm->tm_sec);
regs.minutes = bin2bcd(tm->tm_min);
regs.cent_hours = bin2bcd(tm->tm_hour);
/*
* This device expects week days from 1 to 7
* But rtc_time.wday contains week days from 0 to 6\.
* So we need to add 1 to the value given by rtc_time.wday
*/
regs.day = bin2bcd(tm->tm_wday + 1);
regs.date = bin2bcd(tm->tm_mday);
/*
* This device expects months from 1 to 12
* But rtc_time.tm_mon contains months from 0 to 11\.
* So we need to add 1 to the value given by rtc_time.tm_mon
*/
regs.month = bin2bcd(tm->tm_mon + 1);
/*
* This device expects year since Epoch 2000
* But rtc_time.tm_year contains year since Epoch 1900\.
* We can just extract the year of the century with the
* rest of the division by 100\.
*/
regs.cent_hours |= BQ32K_CENT;
regs.years = bin2bcd(tm->tm_year % 100);
return write_into_device(dev, ®s, 0, sizeof(regs));
}
RTC 的纪元与 POSIX 纪元不同,后者仅用于系统时钟。如果根据 RTC 的纪元和年寄存器的年份小于 1970 年,则假定它比实际时间晚 100 年,即在 2000 年至 2069 年之间。
驱动程序示例
可以用一个简单的虚拟驱动程序总结前面的概念,该驱动程序只是在系统上注册一个 RTC 设备:
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/time.h>
#include <linux/err.h>
#include <linux/rtc.h>
#include <linux/of.h>
static int fake_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
/*
* One can update "tm" with fake values and then call
*/
return rtc_valid_tm(tm);
}
static int fake_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
return 0;
}
static const struct rtc_class_ops fake_rtc_ops = {
.read_time = fake_rtc_read_time,
.set_time = fake_rtc_set_time
};
static const struct of_device_id rtc_dt_ids[] = {
{ .compatible = "packt,rtc-fake", },
{ /* sentinel */ }
};
static int fake_rtc_probe(struct platform_device *pdev)
{
struct rtc_device *rtc;
rtc = rtc_device_register(pdev->name, &pdev->dev,
&fake_rtc_ops, THIS_MODULE);
if (IS_ERR(rtc))
return PTR_ERR(rtc);
platform_set_drvdata(pdev, rtc);
pr_info("Fake RTC module loaded\n");
return 0;
}
static int fake_rtc_remove(struct platform_device *pdev)
{
rtc_device_unregister(platform_get_drvdata(pdev));
return 0;
}
static struct platform_driver fake_rtc_drv = {
.probe = fake_rtc_probe,
.remove = fake_rtc_remove,
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(rtc_dt_ids),
},
};
module_platform_driver(fake_rtc_drv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake RTC driver description");
操作闹钟
RTC 闹钟是设备在特定时间触发的可编程事件。RTC 闹钟表示为struct rtc_wkalarm结构的实例:
struct rtc_wkalrm {
unsigned char enabled; /* 0 = alarm disabled, 1 = enabled */
unsigned char pending; /* 0 = alarm not pending, 1 = pending */
struct rtc_time time; /* time the alarm is set to */
};
驱动程序应提供set_alarm()和read_alarm()操作,以设置和读取警报应发生的时间,以及alarm_irq_enable(),这是一个用于启用/禁用警报的函数。当调用set_alarm()函数时,它将作为输入参数给出一个指向struct rtc_wkalrm的指针,其中的.time字段包含必须设置警报的时间。由驱动程序以正确的方式提取每个值(如有必要,使用bin2dcb()),并将其写入适当的寄存器中。rtc_wkalrm.enabled告诉警报设置后是否应启用警报。如果为 true,则驱动程序必须在芯片中启用警报。对于read_alarm()也是如此,它给出了一个指向struct rtc_wkalrm的指针,但这次作为输出参数。驱动程序必须使用从设备中读取的数据填充结构。
{read | set}_alarm()和{read | set}_time()函数的行为方式相同,只是每对函数从/存储数据到设备的不同寄存器集。
在向系统报告警报事件之前,必须将 RTC 芯片连接到 SoC 的 IRQ 线上。它依赖于 RTC 的 INT 线在警报发生时被拉低。根据制造商,该线保持低电平,直到读取状态寄存器或清除特殊位为止:

此时,我们可以使用通用的 IRQ API,例如request_threaded_irq(),以注册警报 IRQ 的处理程序。在 IRQ 处理程序内部,重要的是使用rtc_update_irq()函数通知内核有关 RTC IRQ 事件:
void rtc_update_irq(struct rtc_device *rtc,
unsigned long num, unsigned long events)
-
rtc:这是引发 IRQ 的 RTC 设备 -
num:显示正在报告的 IRQ 数量(通常为一个) -
events:这是带有一个或多个RTC_PF,RTC_AF,RTC_UF的RTC_IRQF掩码
/* RTC interrupt flags */
#define RTC_IRQF 0x80 /* Any of the following is active */
#define RTC_PF 0x40 /* Periodic interrupt */
#define RTC_AF 0x20 /* Alarm interrupt */
#define RTC_UF 0x10 /* Update interrupt for 1Hz RTC */
该函数可以从任何上下文中调用,无论是原子的还是非原子的。IRQ 处理程序可能如下所示:
static irqreturn_t foo_rtc_alarm_irq(int irq, void *data)
{
struct foo_rtc_struct * foo_device = data;
dev_info(foo_device ->dev, "%s:irq(%d)\n", __func__, irq);
rtc_update_irq(foo_device ->rtc_dev, 1, RTC_IRQF | RTC_AF);
return IRQ_HANDLED;
}
请记住,具有警报功能的 RTC 设备可以用作唤醒源。也就是说,每当警报触发时,系统都可以从挂起模式唤醒。此功能依赖于 RTC 设备引发的中断。使用device_init_wakeup()函数声明设备为唤醒源。实际唤醒系统的 IRQ 也必须使用电源管理核心注册,使用dev_pm_set_wake_irq()函数:
int device_init_wakeup(struct device *dev, bool enable)
int dev_pm_set_wake_irq(struct device *dev, int irq)
我们不会在本书中详细讨论电源管理。想法只是为了让您了解 RTC 设备如何改进您的系统。驱动程序drivers/rtc/rtc-ds1343.c可能有助于实现这些功能。让我们通过为 SPI foo RTC 设备编写一个虚假的probe函数来将所有内容放在一起:
static const struct rtc_class_ops foo_rtc_ops = {
.read_time = foo_rtc_read_time,
.set_time = foo_rtc_set_time,
.read_alarm = foo_rtc_read_alarm,
.set_alarm = foo_rtc_set_alarm,
.alarm_irq_enable = foo_rtc_alarm_irq_enable,
.ioctl = foo_rtc_ioctl,
};
static int foo_spi_probe(struct spi_device *spi)
{
int ret;
/* initialise and configure the RTC chip */
[...]
foo_rtc->rtc_dev =
devm_rtc_device_register(&spi->dev, "foo-rtc",
&foo_rtc_ops, THIS_MODULE);
if (IS_ERR(foo_rtc->rtc_dev)) {
dev_err(&spi->dev, "unable to register foo rtc\n");
return PTR_ERR(priv->rtc);
}
foo_rtc->irq = spi->irq;
if (foo_rtc->irq >= 0) {
ret = devm_request_threaded_irq(&spi->dev, spi->irq,
NULL, foo_rtc_alarm_irq,
IRQF_ONESHOT, "foo-rtc", priv);
if (ret) {
foo_rtc->irq = -1;
dev_err(&spi->dev,
"unable to request irq for rtc foo-rtc\n");
} else {
device_init_wakeup(&spi->dev, true);
dev_pm_set_wake_irq(&spi->dev, spi->irq);
}
}
return 0;
}
RTC 和用户空间
在 Linux 系统中,为了正确地从用户空间管理 RTC,有两个内核选项需要关注。这些选项是CONFIG_RTC_HCTOSYS和CONFIG_RTC_HCTOSYS_DEVICE。
CONFIG_RTC_HCTOSYS在内核构建过程中包括代码文件drivers/rtc/hctosys.c,该文件在启动和恢复时从 RTC 设置系统时间。启用此选项后,系统时间将使用从指定 RTC 设备读取的值进行设置。RTC 设备应在CONFIG_RTC_HCTOSYS_DEVICE中指定:
CONFIG_RTC_HCTOSYS=y
CONFIG_RTC_HCTOSYS_DEVICE="rtc0"
在前面的示例中,我们告诉内核从 RTC 设置系统时间,并指定要使用的 RTC 为rtc0。
sysfs 接口
负责在 sysfs 中实例化 RTC 属性的内核代码在内核源树中的drivers/rtc/rtc-sysfs.c中定义。一旦注册,RTC 设备将在/sys/class/rtc目录下创建一个rtc<id>目录。该目录包含一组只读属性,其中最重要的是:
date:此文件打印 RTC 接口的当前日期:
$ cat /sys/class/rtc/rtc0/date
2017-08-28
time:打印此 RTC 的当前时间:
$ cat /sys/class/rtc/rtc0/time
14:54:20
hctosys:此属性指示 RTC 设备是否是CONFIG_RTC_HCTOSYS_DEVICE中指定的设备,这意味着此 RTC 用于在启动和恢复时设置系统时间。将1读为 true,将0读为 false:
$ cat /sys/class/rtc/rtc0/hctosys
1
dev:此属性显示设备的主要和次要。读作 major:minor:
$ cat /sys/class/rtc/rtc0/dev
251:0
since_epoch:此属性将打印自 UNIX 纪元(1970 年 1 月 1 日)以来经过的秒数:
$ cat /sys/class/rtc/rtc0/since_epoch
1503931738
hwclock 实用程序
硬件时钟(hwclock)是用于访问 RTC 设备的工具。man hwclock命令可能比本节讨论的所有内容更有意义。也就是说,让我们写一些命令,从系统时钟设置 hwclock RTC:
$ sudo ntpd -q # make sure system clock is set from network time
$ sudo hwclock --systohc # set rtc from the system clock
$ sudo hwclock --show # check rtc was set
Sat May 17 17:36:50 2017 -0.671045 seconds
上面的例子假设主机有一个可以访问 NTP 服务器的网络连接。也可以手动设置系统时间:
$ sudo date -s '2017-08-28 17:14:00' '+%s' #set system clock manually
$ sudo hwclock --systohc #synchronize rtc chip on system time
如果没有作为参数给出,hwclock假定 RTC 设备文件是/dev/rtc,实际上这是一个指向真实 RTC 设备的符号链接:
$ ls -l /dev/rtc
lrwxrwxrwx 1 root root 4 août 27 17:50 /dev/rtc -> rtc0
摘要
本章向您介绍了 RTC 框架及其 API。其减少的功能和数据结构使其成为最轻量级的框架,并且易于掌握。使用本章描述的技能,您将能够为大多数现有的 RTC 芯片开发驱动程序,甚至可以进一步处理这些设备,轻松设置日期和时间以及闹钟。下一章,PWM 驱动程序,与本章没有任何共同之处,但对于嵌入式工程师来说是必须了解的。
第十九章:PWM 驱动程序
脉冲宽度调制(PWM)类似于不断循环开关。它是一种用于控制舵机、电压调节等的硬件特性。PWM 最著名的应用包括:
-
电机速度控制
-
调光
-
电压调节
现在,让我们通过以下简单的图介绍 PWM:

前面的图描述了完整的 PWM 周期,介绍了我们需要在深入研究内核 PWM 框架之前澄清的一些术语:
-
Ton:这是信号高电平的持续时间。 -
Toff:这是信号低电平的持续时间。 -
周期:这是完整 PWM 周期的持续时间。它代表 PWM 信号的Ton和Toff的总和。 -
占空比:它表示信号在 PWM 信号周期内保持开启的时间的百分比。
不同的公式详细说明如下:
-
PWM 周期:
![]()
-
占空比:
![]()
您可以在en.wikipedia.org/wiki/Pulse-width_modulation找到有关 PWM 的详细信息。
Linux PWM 框架有两个接口:
-
控制器接口:公开 PWM 线的接口。它是 PWM 芯片,即生产者。
-
消费者接口:由控制器公开的使用 PWM 线的设备。此类设备的驱动程序使用控制器导出的辅助函数,通过通用 PWM 框架。
消费者或生产者接口取决于以下头文件:
#include <linux/pwm.h>
在本章中,我们将处理:
-
PWM 驱动程序架构和数据结构,用于控制器和消费者,以及一个虚拟驱动程序
-
在设备树中实例化 PWM 设备和控制器
-
请求和使用 PWM 设备
-
通过 sysfs 接口从用户空间使用 PWM
PWM 控制器驱动程序
在编写 GPIO 控制器驱动程序时需要struct gpio_chip,在编写 IRQ 控制器驱动程序时需要struct irq_chip,PWM 控制器在内核中表示为struct pwm_chip结构的实例。

PWM 控制器和设备
struct pwm_chip {
struct device *dev;
const struct pwm_ops *ops;
int base;
unsigned int npwm;
struct pwm_device *pwms;
struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
bool can_sleep;
};
以下是结构中每个元素的含义:
-
dev:这代表与此芯片关联的设备。 -
Ops:这是一个数据结构,提供此芯片向消费者驱动程序公开的回调函数。 -
Base:这是由此芯片控制的第一个 PWM 的编号。如果chip->base < 0,则内核将动态分配一个基数。 -
can_sleep:如果 ops 字段的.config()、.enable()或.disable()操作可能休眠,则由芯片驱动程序设置为true。 -
npwm:这是此芯片提供的 PWM 通道(设备)的数量。 -
pwms:这是由框架分配给此芯片的 PWM 设备数组,供消费者驱动程序使用。 -
of_xlate:这是一个可选的回调,用于根据 DT PWM 指定器请求 PWM 设备。如果未定义,PWM 核心将将其设置为of_pwm_simple_xlate,同时将of_pwm_n_cells强制设置为2。 -
of_pwm_n_cells:这是 DT 中 PWM 指定器预期的单元数。
PWM 控制器/芯片的添加和移除依赖于两个基本函数,pwmchip_add()和pwmchip_remove()。每个函数都应该以填充的struct pwm_chip结构作为参数。它们各自的原型如下:
int pwmchip_add(struct pwm_chip *chip)
int pwmchip_remove(struct pwm_chip *chip)
与其他框架移除函数不返回值不同,pwmchip_remove()具有返回值。它在成功时返回0,如果芯片仍在使用(仍在请求),则返回-EBUSY。
每个 PWM 驱动程序必须通过struct pwm_ops字段实现一些钩子,该字段由 PWM 核心或消费者接口使用,以配置和充分利用其 PWM 通道。其中一些是可选的。
struct pwm_ops {
int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
int (*config)(struct pwm_chip *chip, struct pwm_device *pwm,
int duty_ns, int period_ns);
int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,
enum pwm_polarity polarity);
int (*enable)(struct pwm_chip *chip,struct pwm_device *pwm);
void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state); /* since kernel v4.7 */
struct module *owner;
};
让我们看看结构中的每个元素的含义:
-
request:这是一个可选的钩子,如果提供,将在请求 PWM 通道时执行。 -
free:这与请求相同,在 PWM 释放时运行。 -
config:这是 PMW 配置钩子。它配置了这个 PWM 的占空比和周期长度。 -
set_polarity:这个钩子配置了 PWM 的极性。 -
Enable:这启用 PWM 线,开始输出切换。 -
Disable:这禁用 PWM 线,停止输出切换。 -
Apply:这个原子地应用一个新的 PWM 配置。状态参数应该根据实际的硬件配置进行调整。 -
get_state:这返回当前 PWM 状态。当 PWM 芯片注册时,每个 PWM 设备只调用一次这个函数。 -
Owner:这是拥有这个芯片的模块,通常是THIS_MODULE。
在 PWM 控制器驱动的probe函数中,最好的做法是检索 DT 资源,初始化硬件,填充struct pwm_chip和它的struct pwm_ops,然后使用pwmchip_add函数添加 PWM 芯片。
驱动示例
现在让我们通过编写一个虚拟 PWM 控制器的虚拟驱动来总结一下事情,它有三个通道:
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
struct fake_chip {
struct pwm_chip chip;
int foo;
int bar;
/* put the client structure here (SPI/I2C) */
};
static inline struct fake_chip *to_fake_chip(struct pwm_chip *chip)
{
return container_of(chip, struct fake_chip, chip);
}
static int fake_pwm_request(struct pwm_chip *chip,
struct pwm_device *pwm)
{
/*
* One may need to do some initialization when a PWM channel
* of the controller is requested. This should be done here.
*
* One may do something like
* prepare_pwm_device(struct pwm_chip *chip, pwm->hwpwm);
*/
return 0;
}
static int fake_pwm_config(struct pwm_chip *chip,
struct pwm_device *pwm,
int duty_ns, int period_ns)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return send_command_to_set_config(priv,
* duty_ns, period_ns);
*/
return 0;
}
static int fake_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return foo_chip_set_pwm_enable(priv, pwm->hwpwm, true);
*/
pr_info("Somebody enabled PWM device number %d of this chip",
pwm->hwpwm);
return 0;
}
static void fake_pwm_disable(struct pwm_chip *chip,
struct pwm_device *pwm)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return foo_chip_set_pwm_enable(priv, pwm->hwpwm, false);
*/
pr_info("Somebody disabled PWM device number %d of this chip",
pwm->hwpwm);
}
static const struct pwm_ops fake_pwm_ops = {
.request = fake_pwm_request,
.config = fake_pwm_config,
.enable = fake_pwm_enable,
.disable = fake_pwm_disable,
.owner = THIS_MODULE,
};
static int fake_pwm_probe(struct platform_device *pdev)
{
struct fake_chip *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->chip.ops = &fake_pwm_ops;
priv->chip.dev = &pdev->dev;
priv->chip.base = -1; /* Dynamic base */
priv->chip.npwm = 3; /* 3 channel controller */
platform_set_drvdata(pdev, priv);
return pwmchip_add(&priv->chip);
}
static int fake_pwm_remove(struct platform_device *pdev)
{
struct fake_chip *priv = platform_get_drvdata(pdev);
return pwmchip_remove(&priv->chip);
}
static const struct of_device_id fake_pwm_dt_ids[] = {
{ .compatible = "packt,fake-pwm", },
{ }
};
MODULE_DEVICE_TABLE(of, fake_pwm_dt_ids);
static struct platform_driver fake_pwm_driver = {
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(fake_pwm_dt_ids),
},
.probe = fake_pwm_probe,
.remove = fake_pwm_remove,
};
module_platform_driver(fake_pwm_driver);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake pwm driver");
MODULE_LICENSE("GPL");
PWM 控制器绑定
在 DT 中绑定 PWM 控制器时,最重要的属性是#pwm-cells。它表示用于表示该控制器的 PWM 设备的单元格数。如果记得,在struct pwm_chip结构中,of_xlate钩子用于翻译给定的 PWM 说明符。如果没有设置这个钩子,这里的pwm-cells必须设置为 2,否则,它应该与of_pwm_n_cells的值相同。以下是 i.MX6 SoC 设备树中 PWM 控制器节点的示例。
pwm3: pwm@02088000 {
#pwm-cells = <2>;
compatible = "fsl,imx6q-pwm", "fsl,imx27-pwm";
reg = <0x02088000 0x4000>;
interrupts = <0 85 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_IPG>,
<&clks IMX6QDL_CLK_PWM3>;
clock-names = "ipg", "per";
status = "disabled";
};
另一方面,对应我们的虚拟 PWM 驱动的节点如下:
fake_pwm: pwm@0 {
#pwm-cells = <2>;
compatible = "packt,fake-pwm";
/*
* Our driver does not use resource
* neither mem, IRQ, nor Clock)
*/
};
PWM 消费者接口
消费者是实际使用 PWM 通道的设备。在内核中,PWM 通道表示为struct pwm_device结构的实例:
struct pwm_device {
const char *label;
unsigned long flags;
unsigned int hwpwm;
unsigned int pwm;
struct pwm_chip *chip;
void *chip_data;
unsigned int period; /* in nanoseconds */
unsigned int duty_cycle; /* in nanoseconds */
enum pwm_polarity polarity;
};
-
Label:这是这个 PWM 设备的名称 -
Flags:这代表与 PWM 设备相关的标志 -
hwpw:这是 PWM 设备的相对索引,局部于芯片 -
pwm:这是 PWM 设备的系统全局索引 -
chip:这是一个 PWM 芯片,提供这个 PWM 设备的控制器 -
chip_data:这是与这个 PWM 设备关联的芯片私有数据
自内核 v4.7 以来,结构已更改为:
struct pwm_device {
const char *label;
unsigned long flags;
unsigned int hwpwm;
unsigned int pwm;
struct pwm_chip *chip;
void *chip_data;
struct pwm_args args;
struct pwm_state state;
};
-
args:这代表与这个 PWM 设备相关的依赖于板的 PWM 参数,通常从 PWM 查找表或设备树中检索。PWM 参数代表用户想要在这个 PWM 设备上使用的初始配置,而不是当前的 PWM 硬件状态。 -
状态:这代表了当前 PWM 通道的状态。
struct pwm_args {
unsigned int period; /* Device's nitial period */
enum pwm_polarity polarity;
};
struct pwm_state {
unsigned int period; /* PWM period (in nanoseconds) */
unsigned int duty_cycle; /* PWM duty cycle (in nanoseconds) */
enum pwm_polarity polarity; /* PWM polarity */
bool enabled; /* PWM enabled status */
}
随着 Linux 的发展,PWM 框架面临了几次变化。这些变化涉及到从消费者端请求 PWM 设备的方式。我们可以将消费者接口分为两部分,或者更准确地说是两个版本。
传统版本,在这个版本中使用pwm_request()和pwm_free()来请求一个 PWM 设备,并在使用后释放它。
新的和推荐的 API,使用pwm_get()和pwm_put()函数。前者给定了消费者设备和通道名称作为参数来请求 PWM 设备,后者给定了要释放的 PWM 设备作为参数。这些函数的托管变体devm_pwm_get()和devm_pwm_put()也存在。
struct pwm_device *pwm_get(struct device *dev, const char *con_id)
void pwm_put(struct pwm_device *pwm)
pwm_request()/pwm_get()和pwm_free()/pwm_put()不能在原子上下文中调用,因为 PWM 核心使用互斥锁,可能会休眠。
在请求后,必须使用以下方式配置 PWM:
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
要开始/停止切换 PWM 输出,使用pwm_enable()/pwm_disable()。这两个函数都以struct pwm_device的指针作为参数,并且都是通过pwm_chip.pwm_ops字段公开的钩子的包装器。
int pwm_enable(struct pwm_device *pwm)
void pwm_disable(struct pwm_device *pwm)
pwm_enable()在成功时返回0,在失败时返回负错误代码。一个很好的 PWM 消费者驱动程序的例子是内核源树中的drivers/leds/leds-pwm.c。以下是一个 PWM led 的消费者代码示例:
static void pwm_led_drive(struct pwm_device *pwm,
struct private_data *priv)
{
/* Configure the PWM, applying a period and duty cycle */
pwm_config(pwm, priv->duty, priv->pwm_period);
/* Start toggling */
pwm_enable(pchip->pwmd);
[...] /* Do some work */
/* And then stop toggling*/
pwm_disable(pchip->pwmd);
}
PWM 客户端绑定
PWM 设备可以从以下分配给消费者:
-
设备树
-
ACPI
-
静态查找表,在板
init文件中。
本书将仅处理 DT 绑定,因为这是推荐的方法。当将 PWM 消费者(客户端)绑定到其驱动程序时,您需要提供其链接的控制器的 phandle。
建议您将 PWM 属性命名为pwms;由于 PWM 设备是命名资源,您可以提供一个可选的属性pwm-names,其中包含一个字符串列表,用于为pwms属性中列出的每个 PWM 设备命名。如果没有给出pwm-names属性,则将使用用户节点的名称作为回退。
使用多个 PWM 设备的设备的驱动程序可以使用pwm-names属性将pwm_get()调用请求的 PWM 设备的名称映射到pwms属性给出的列表中的索引。
以下示例描述了基于 PWM 的背光设备,这是 PWM 设备绑定的内核文档的摘录(请参阅Documentation/devicetree/bindings/pwm/pwm.txt):
pwm: pwm {
#pwm-cells = <2>;
};
[...]
bl: backlight {
pwms = <&pwm 0 5000000>;
pwm-names = "backlight";
};
PWM 规范通常编码芯片相对 PWM 编号和以纳秒为单位的 PWM 周期。使用以下行:
pwms = <&pwm 0 5000000>;
0对应于相对于控制器的 PWM 索引,5000000表示以纳秒为单位的周期。请注意,在前面的示例中,指定pwm-names是多余的,因为名称backlight无论如何都将用作回退。因此,驱动程序必须调用:
static int my_consummer_probe(struct platform_device *pdev)
{
struct pwm_device *pwm;
pwm = pwm_get(&pdev->dev, "backlight");
if (IS_ERR(pwm)) {
pr_info("unable to request PWM, trying legacy API\n");
/*
* Some drivers use the legacy API as fallback, in order
* to request a PWM ID, global to the system
* pwm = pwm_request(global_pwm_id, "pwm beeper");
*/
}
[...]
return 0;
}
PWM 规范通常编码芯片相对 PWM 编号和以纳秒为单位的 PWM 周期。
使用 sysfs 接口的 PWM
PWM 核心sysfs根路径为/sys/class/pwm/。这是管理 PWM 设备的用户空间方式。系统中添加的每个 PWM 控制器/芯片都会在sysfs根路径下创建一个pwmchipN目录条目,其中N是 PWM 芯片的基础。该目录包含以下文件:
-
npwm:这是一个只读文件,打印此芯片支持的 PWM 通道数 -
导出:这是一个只写文件,允许将 PWM 通道导出供sysfs使用(此功能等效于 GPIO sysfs 接口) -
取消导出:从sysfs中取消导出 PWM 通道(只写)
PWM 通道使用从 0 到pwm<n-1>的索引编号。这些数字是相对于芯片的。每个 PWM 通道导出都会在pwmchipN中创建一个pwmX目录,该目录与使用的export文件相同。X是导出的通道号。每个通道目录包含以下文件:
-
周期:这是一个可读/可写文件,用于获取/设置 PWM 信号的总周期。值以纳秒为单位。 -
duty_cycle:这是一个可读/可写文件,用于获取/设置 PWM 信号的占空比。它表示 PWM 信号的活动时间。值以纳秒为单位,必须始终小于周期。 -
极性:这是一个可读/可写文件,仅在此 PWM 设备的芯片支持极性反转时使用。最好只在此 PWM 未启用时更改极性。接受的值为字符串normal或inversed。 -
启用:这是一个可读/可写文件,用于启用(开始切换)/禁用(停止切换)PWM 信号。接受的值为: -
0:已禁用
-
1:已启用
以下是通过sysfs接口从用户空间使用 PWM 的示例:
- 启用 PWM:
# echo 1 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable
- 设置 PWM 周期:
# echo **<value in nanoseconds> >**
/sys/class/pwm/pwmchip**<pwmchipnr>**
/pwm**<pwmnr>**
/period
- 设置 PWM 占空比:占空比的值必须小于 PWM 周期的值:
# echo **<value in nanoseconds>**
> /sys/class/pwm/pwmchip**<pwmchipnr>**
/pwm**<pwmnr>**
/duty_cycle
- 禁用 PWM:
# echo 0 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable
完整的 PWM 框架 API 和 sysfs 描述可在内核源树中的Documentation/pwm.txt文件中找到。
摘要
到本章结束时,您将具备处理任何 PWM 控制器的能力,无论它是内存映射的还是外部连接在总线上的。本章描述的 API 将足以编写和增强控制器驱动程序作为消费者设备驱动程序。如果您对 PWM 内核端还不熟悉,可以完全使用用户空间 sysfs 接口。话虽如此,在下一章中,我们将讨论有时由 PWM 驱动的调节器。所以,请稍等,我们快要完成了。
第二十章:调节器框架
调节器是一种为其他设备提供电源的电子设备。由调节器供电的设备称为消费者。有人说他们消耗调节器提供的电源。大多数调节器可以启用和禁用其输出,有些还可以控制其输出电压或电流。驱动程序应通过特定的函数和数据结构向消费者公开这些功能,我们将在本章讨论。
物理提供调节器的芯片称为电源管理集成电路(PMIC):

Linux 调节器框架已经被设计用于接口和控制电压和电流调节器。它分为四个独立的接口,如下所示:
-
调节器驱动程序接口用于调节器 PMIC 驱动程序。此接口的结构可以在
include/linux/regulator/driver.h中找到。 -
设备驱动程序的消费者接口。
-
用于板配置的机器接口。
-
用户空间的 sysfs 接口。
在本章中,我们将涵盖以下主题:
-
介绍 PMIC/生产者驱动程序接口、驱动程序方法和数据结构
-
ISL6271A MIC 驱动程序的案例研究,以及用于测试目的的虚拟调节器
-
调节器消费者接口及其 API
-
DT 中的调节器(生产者/消费者)绑定
PMIC/生产者驱动程序接口
生产者是产生调节电压或电流的设备。这种设备的名称是 PMIC,它可以用于电源排序、电池管理、DC-DC 转换或简单的电源开关(开/关)。它通过软件控制调节输入电源的输出功率。
它涉及调节器驱动程序,特别是生产者 PMIC 方面,需要一些头文件:
#include <linux/platform_device.h>
#include <linux/regulator/driver.h>
#include <linux/regulator/of_regulator.h>
驱动程序数据结构
我们将从调节器框架使用的数据结构的简短介绍开始。本节仅描述了生产者接口。
描述结构
内核通过struct regulator_desc结构描述了 PMIC 提供的每个调节器,该结构表征了调节器。通过调节器,我指的是任何独立的调节输出。例如,来自 Intersil 的 ISL6271A 是一个具有三个独立调节输出的 PMIC。然后,其驱动程序中应该有三个regulator_desc的实例。这个结构包含调节器的固定属性,看起来像下面这样:
struct regulator_desc {
const char *name;
const char *of_match;
int id;
unsigned n_voltages;
const struct regulator_ops *ops;
int irq;
enum regulator_type type;
struct module *owner;
unsigned int min_uV;
unsigned int uV_step;
};
出于简单起见,我们将省略一些字段。完整的结构定义可以在include/linux/regulator/driver.h中找到:
-
name保存调节器的名称。 -
of_match保存了在 DT 中用于识别调节器的名称。 -
id是调节器的数字标识符。 -
owner表示提供调节器的模块。将此字段设置为THIS_MODULE。 -
type指示调节器是电压调节器还是电流调节器。它可以是REGULATOR_VOLTAGE或REGULATOR_CURRENT。任何其他值都将导致调节器注册失败。 -
n_voltages表示此调节器可用的选择器数量。它代表调节器可以输出的数值。对于固定输出电压,n_voltages应设置为 1。 -
min_uV表示此调节器可以提供的最小电压值。这是由最低选择器给出的电压。 -
uV_step表示每个选择器的电压增加。 -
ops表示调节器操作表。它是一个指向调节器可以支持的一组操作回调的结构。此字段稍后会讨论。 -
irq是调节器的中断号。
约束结构
当 PMIC 向消费者公开调节器时,它必须借助struct regulation_constraints结构为此调节器强加一些名义上的限制。这是一个收集调节器的安全限制并定义消费者不能越过的边界的结构。这是调节器驱动程序和消费者驱动程序之间的一种合同:
struct regulation_constraints {
const char *name;
/* voltage output range (inclusive) - for voltage control */
int min_uV;
int max_uV;
int uV_offset;
/* current output range (inclusive) - for current control */
int min_uA;
int max_uA;
/* valid regulator operating modes for this machine */
unsigned int valid_modes_mask;
/* valid operations for regulator on this machine */
unsigned int valid_ops_mask;
struct regulator_state state_disk;
struct regulator_state state_mem;
struct regulator_state state_standby;
suspend_state_t initial_state; /* suspend state to set at init */
/* mode to set on startup */
unsigned int initial_mode;
/* constraint flags */
unsigned always_on:1; /* regulator never off when system is on */
unsigned boot_on:1; /* bootloader/firmware enabled regulator */
unsigned apply_uV:1; /* apply uV constraint if min == max */
};
让我们描述结构中的每个元素:
-
min_uV,min_uA,max_uA和max_uV是消费者可以设置的最小电压/电流值。 -
uV_offset是应用于消费者电压的补偿电压偏移量。 -
valid_modes_mask和valid_ops_mask分别是可以由消费者配置/执行的模式/操作的掩码。 -
如果寄存器永远不应该被禁用,则应设置
always_on。 -
如果寄存器在系统初始启动时已启用,则应设置
boot_on。如果寄存器不是由硬件或引导加载程序启用的,则在应用约束时将启用它。 -
name是用于显示目的的约束的描述性名称。 -
apply_uV在初始化时应用电压约束。 -
input_uV表示由另一个寄存器供电时该寄存器的输入电压。 -
state_disk,state_mem和state_standby定义了系统在磁盘模式、内存模式或待机模式下挂起时的寄存器状态。 -
initial_state表示默认设置为挂起状态。 -
initial_mode是启动时设置的模式。
初始化数据结构
有两种方法可以将regulator_init_data传递给驱动程序;这可以通过板初始化文件中的平台数据完成,也可以通过设备树中的节点使用of_get_regulator_init_data函数完成:
struct regulator_init_data {
struct regulation_constraints constraints;
/* optional regulator machine specific init */
int (*regulator_init)(void *driver_data);
void *driver_data; /* core does not touch this */
};
以下是结构中各元素的含义:
-
constraints表示寄存器约束 -
regulator_init是在核心注册寄存器时调用的可选回调 -
driver_data表示传递给regulator_init的数据
正如大家所看到的,struct constraints结构是init data的一部分。这是因为在初始化寄存器时,其约束直接应用于它,远在任何消费者使用之前。
将初始化数据输入到板文件中
该方法包括填充约束数组,可以从驱动程序内部或板文件中进行,并将其用作平台数据的一部分。以下是基于案例研究中的设备 ISL6271A from Intersil 的示例:
static struct regulator_init_data isl_init_data[] = {
[0] = {
.constraints = {
.name = "Core Buck",
.min_uV = 850000,
.max_uV = 1600000,
.valid_modes_mask = REGULATOR_MODE_NORMAL
| REGULATOR_MODE_STANDBY,
.valid_ops_mask = REGULATOR_CHANGE_MODE
| REGULATOR_CHANGE_STATUS,
},
},
[1] = {
.constraints = {
.name = "LDO1",
.min_uV = 1100000,
.max_uV = 1100000,
.always_on = true,
.valid_modes_mask = REGULATOR_MODE_NORMAL
| REGULATOR_MODE_STANDBY,
.valid_ops_mask = REGULATOR_CHANGE_MODE
| REGULATOR_CHANGE_STATUS,
},
},
[2] = {
.constraints = {
.name = "LDO2",
.min_uV = 1300000,
.max_uV = 1300000,
.always_on = true,
.valid_modes_mask = REGULATOR_MODE_NORMAL
| REGULATOR_MODE_STANDBY,
.valid_ops_mask = REGULATOR_CHANGE_MODE
| REGULATOR_CHANGE_STATUS,
},
},
};
尽管此方法现在已被弃用,但这里仍介绍了它供您参考。新的推荐方法是 DT,将在下一节中介绍。
将初始化数据输入 DT
为了从 DT 中提取传递的初始化数据,我们需要引入一个新的数据类型struct of_regulator_match,它看起来像这样:
struct of_regulator_match {
const char *name;
void *driver_data;
struct regulator_init_data *init_data;
struct device_node *of_node;
const struct regulator_desc *desc;
};
在使用此数据结构之前,我们需要弄清楚如何实现 DT 文件的寄存器绑定。
DT 中的每个 PMIC 节点都应该有一个名为regulators的子节点,在其中我们必须声明此 PMIC 提供的每个寄存器作为专用子节点。换句话说,每个 PMIC 的寄存器都被定义为regulators节点的子节点,而regulators节点又是 DT 中 PMIC 节点的子节点。
在寄存器节点中,您可以定义标准化的属性:
-
regulator-name:这是用作寄存器输出的描述性名称的字符串 -
regulator-min-microvolt:这是消费者可以设置的最小电压 -
regulator-max-microvolt:这是消费者可以设置的最大电压 -
regulator-microvolt-offset:这是应用于电压以补偿电压下降的偏移量 -
regulator-min-microamp:这是消费者可以设置的最小电流 -
regulator-max-microamp:这是消费者可以设置的最大电流 -
regulator-always-on:这是一个布尔值,指示寄存器是否永远不应该被禁用 -
regulator-boot-on:这是一个由引导加载程序/固件启用的寄存器 -
<name>-supply:这是父供电/寄存器节点的 phandle -
regulator-ramp-delay:这是寄存器的斜坡延迟(以 uV/uS 为单位)
这些属性看起来真的像是struct regulator_init_data中的字段。回到ISL6271A驱动程序,其 DT 条目可能如下所示:
isl6271a@3c {
compatible = "isl6271a";
reg = <0x3c>;
interrupts = <0 86 0x4>;
/* supposing our regulator is powered by another regulator */
in-v1-supply = <&some_reg>;
[...]
regulators {
reg1: core_buck {
regulator-name = "Core Buck";
regulator-min-microvolt = <850000>;
regulator-max-microvolt = <1600000>;
};
reg2: ldo1 {
regulator-name = "LDO1";
regulator-min-microvolt = <1100000>;
regulator-max-microvolt = <1100000>;
regulator-always-on;
};
reg3: ldo2 {
regulator-name = "LDO2";
regulator-min-microvolt = <1300000>;
regulator-max-microvolt = <1300000>;
regulator-always-on;
};
};
};
使用内核辅助函数of_regulator_match(),给定regulators子节点作为参数,该函数将遍历每个调节器设备节点,并为每个构建一个struct init_data结构。在驱动程序方法部分讨论的probe()函数中有一个示例。
配置结构
调节器设备通过struct regulator_config结构进行配置,该结构保存调节器描述的可变元素。在向核心注册调节器时,将传递此结构:
struct regulator_config {
struct device *dev;
const struct regulator_init_data *init_data;
void *driver_data;
struct device_node *of_node;
};
-
dev代表调节器所属的设备结构。 -
init_data是结构的最重要字段,因为它包含一个包含调节器约束(机器特定结构)的元素。 -
driver_data保存调节器的私有数据。 -
of_node用于支持 DT 的驱动程序。这是要解析 DT 绑定的节点。开发人员负责设置此字段。它也可以是NULL。
设备操作结构
struct regulator_ops结构是一个回调列表,表示调节器可以执行的所有操作。这些回调是辅助函数,并由通用内核函数包装:
struct regulator_ops {
/* enumerate supported voltages */
int (*list_voltage) (struct regulator_dev *,
unsigned selector);
/* get/set regulator voltage */
int (*set_voltage) (struct regulator_dev *,
int min_uV, int max_uV,
unsigned *selector);
int (*map_voltage)(struct regulator_dev *,
int min_uV, int max_uV);
int (*set_voltage_sel) (struct regulator_dev *,
unsigned selector);
int (*get_voltage) (struct regulator_dev *);
int (*get_voltage_sel) (struct regulator_dev *);
/* get/set regulator current */
int (*set_current_limit) (struct regulator_dev *,
int min_uA, int max_uA);
int (*get_current_limit) (struct regulator_dev *);
int (*set_input_current_limit) (struct regulator_dev *,
int lim_uA);
int (*set_over_current_protection) (struct regulator_dev *);
int (*set_active_discharge) (struct regulator_dev *,
bool enable);
/* enable/disable regulator */
int (*enable) (struct regulator_dev *);
int (*disable) (struct regulator_dev *);
int (*is_enabled) (struct regulator_dev *);
/* get/set regulator operating mode (defined in consumer.h) */
int (*set_mode) (struct regulator_dev *, unsigned int mode);
unsigned int (*get_mode) (struct regulator_dev *);
};
回调名称很好地解释了它们的作用。这里没有列出的其他回调,您必须在消费者使用它们之前在调节器的约束中启用适当的掩码valid_ops_mask或valid_modes_mask。可用的操作掩码标志在include/linux/regulator/machine.h中定义。
因此,给定一个struct regulator_dev结构,可以通过调用rdev_get_id()函数获取相应调节器的 ID:
int rdev_get_id(struct regulator_dev *rdev)
驱动程序方法
驱动程序方法包括probe()和remove()函数。如果此部分对您不清楚,请参考前面的数据结构。
探测功能
PMIC 驱动程序的probe功能可以分为几个步骤,列举如下:
-
为此 PMIC 提供的所有调节器定义一个
struct regulator_desc对象数组。在此步骤中,您应该已经定义了一个有效的struct regulator_ops,以链接到适当的regulator_desc。假设它们都支持相同的操作,可以对所有调节器使用相同的regulator_ops。 -
现在在
probe函数中,对于每个调节器:
-
- 从平台数据中获取适当的
struct regulator_init_data,该数据必须已包含有效的struct regulation_constraints,或者从 DT 构建一个struct regulation_constraints,以构建一个新的struct regulator_init_data对象。
- 从平台数据中获取适当的
-
使用先前的
struct regulator_init_data来设置struct regulator_config结构。如果驱动程序支持 DT,可以使regulator_config.of_node指向用于提取调节器属性的节点。 -
调用
regulator_register()(或托管版本的devm_regulator_register())来使用先前的regulator_desc和regulator_config作为参数向核心注册调节器。
使用regulator_register()函数或devm_regulator_register(),将调节器注册到内核中:
struct regulator_dev * regulator_register(const struct regulator_desc *regulator_desc, const struct regulator_config *cfg)
此函数返回一个我们到目前为止尚未讨论的数据类型:struct regulator_dev对象,定义在include/linux/regulator/driver.h中。该结构表示来自生产方的调节器设备的实例(在消费方方面不同)。struct regulator_dev结构的实例不应直接被任何东西使用,除了调节器核心和通知注入(应该获取互斥锁,而不是其他直接访问)。也就是说,为了跟踪驱动程序内部注册的调节器,应该为注册函数返回的每个regulator_dev对象保留引用。
删除功能
remove()函数是在probe期间执行的每个操作的地方。因此,你应该牢记的关键函数是regulator_unregister(),当需要从系统中移除调节器时:
void regulator_unregister(struct regulator_dev *rdev)
这个函数接受一个struct regulator_dev结构的指针作为参数。这也是为每个注册的调节器保留引用的另一个原因。以下是 ISL6271A 驱动程序的remove函数:
static int __devexit isl6271a_remove(struct i2c_client *i2c)
{
struct isl_pmic *pmic = i2c_get_clientdata(i2c);
int i;
for (i = 0; i < 3; i++)
regulator_unregister(pmic->rdev[i]);
kfree(pmic);
return 0;
}
案例研究:Intersil ISL6271A 电压调节器
回顾一下,这个 PMIC 提供了三个调节器设备,其中只有一个可以改变其输出值。另外两个提供固定电压:
struct isl_pmic {
struct i2c_client *client;
struct regulator_dev *rdev[3];
struct mutex mtx;
};
首先我们定义 ops 回调函数,来设置struct regulator_desc:
- 处理
get_voltage_sel操作的回调函数:
static int isl6271a_get_voltage_sel(struct regulator_dev *rdev)
{
struct isl_pmic *pmic = rdev_get_drvdata(dev);
int idx = rdev_get_id(rdev);
idx = i2c_smbus_read_byte(pmic->client);
if (idx < 0)
[...] /* handle this error */
return idx;
}
以下是处理set_voltage_sel操作的回调函数:
static int isl6271a_set_voltage_sel(
struct regulator_dev *dev, unsigned selector)
{
struct isl_pmic *pmic = rdev_get_drvdata(dev);
int err;
err = i2c_smbus_write_byte(pmic->client, selector);
if (err < 0)
[...] /* handle this error */
return err;
}
- 既然我们已经完成了回调函数的定义,我们可以构建一个
struct regulator_ops:
static struct regulator_ops isl_core_ops = {
.get_voltage_sel = isl6271a_get_voltage_sel,
.set_voltage_sel = isl6271a_set_voltage_sel,
.list_voltage = regulator_list_voltage_linear,
.map_voltage = regulator_map_voltage_linear,
};
static struct regulator_ops isl_fixed_ops = {
.list_voltage = regulator_list_voltage_linear,
};
你可能会问regulator_list_voltage_linear和regulator_list_voltage_linear函数是从哪里来的。和许多其他调节器辅助函数一样,它们也在drivers/regulator/helpers.c中定义。内核为线性输出调节器提供了辅助函数,就像 ISL6271A 一样。
现在是时候为所有调节器构建一个struct regulator_desc数组了:
static const struct regulator_desc isl_rd[] = {
{
.name = "Core Buck",
.id = 0,
.n_voltages = 16,
.ops = &isl_core_ops,
.type = REGULATOR_VOLTAGE,
.owner = THIS_MODULE,
.min_uV = ISL6271A_VOLTAGE_MIN,
.uV_step = ISL6271A_VOLTAGE_STEP,
}, {
.name = "LDO1",
.id = 1,
.n_voltages = 1,
.ops = &isl_fixed_ops,
.type = REGULATOR_VOLTAGE,
.owner = THIS_MODULE,
.min_uV = 1100000,
}, {
.name = "LDO2",
.id = 2,
.n_voltages = 1,
.ops = &isl_fixed_ops,
.type = REGULATOR_VOLTAGE,
.owner = THIS_MODULE,
.min_uV = 1300000,
},
};
LDO1和LDO2具有固定的输出电压。这就是为什么它们的n_voltages属性被设置为 1,它们的 ops 只提供regulator_list_voltage_linear映射。
- 现在我们在
probe函数中,这是我们需要构建struct init_data结构的地方。如果你记得,我们将使用之前介绍的struct of_regulator_match。我们应该声明一个该类型的数组,在其中我们应该设置每个需要获取init_data的调节器的.name属性:
static struct of_regulator_match isl6271a_matches[] = {
{ .name = "core_buck", },
{ .name = "ldo1", },
{ .name = "ldo2", },
};
仔细看,你会注意到.name属性的设置与设备树中调节器的标签完全相同。这是你应该关心和尊重的规则。
现在让我们看一下probe函数。ISL6271A 提供三个调节器输出,这意味着应该调用regulator_register()函数三次:
static int isl6271a_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
struct regulator_config config = { };
struct regulator_init_data *init_data =
dev_get_platdata(&i2c->dev);
struct isl_pmic *pmic;
int i, ret;
struct device *dev = &i2c->dev;
struct device_node *np, *parent;
if (!i2c_check_functionality(i2c->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
pmic = devm_kzalloc(&i2c->dev,
sizeof(struct isl_pmic), GFP_KERNEL);
if (!pmic)
return -ENOMEM;
/* Get the device (PMIC) node */
np = of_node_get(dev->of_node);
if (!np)
return -EINVAL;
/* Get 'regulators' subnode */
parent = of_get_child_by_name(np, "regulators");
if (!parent) {
dev_err(dev, "regulators node not found\n");
return -EINVAL;
}
/* fill isl6271a_matches array */
ret = of_regulator_match(dev, parent, isl6271a_matches,
ARRAY_SIZE(isl6271a_matches));
of_node_put(parent);
if (ret < 0) {
dev_err(dev, "Error parsing regulator init data: %d\n",
ret);
return ret;
}
pmic->client = i2c;
mutex_init(&pmic->mtx);
for (i = 0; i < 3; i++) {
struct regulator_init_data *init_data;
struct regulator_desc *desc;
int val;
if (pdata)
/* Given as platform data */
config.init_data = pdata->init_data[i];
else
/* Fetched from device tree */
config.init_data = isl6271a_matches[i].init_data;
config.dev = &i2c->dev;
config.of_node = isl6271a_matches[i].of_node;
config.ena_gpio = -EINVAL;
/*
* config is passed by reference because the kernel
* internally duplicate it to create its own copy
* so that it can override some fields
*/
pmic->rdev[i] = devm_regulator_register(&i2c->dev,
&isl_rd[i], &config);
if (IS_ERR(pmic->rdev[i])) {
dev_err(&i2c->dev, "failed to register %s\n",
id->name);
return PTR_ERR(pmic->rdev[i]);
}
}
i2c_set_clientdata(i2c, pmic);
return 0;
}
对于固定调节器,init_data可以是NULL。这意味着对于 ISL6271A,只有可能改变电压输出的调节器可以被分配一个init_data。
/* Only the first regulator actually need it */
if (i == 0)
if(pdata)
config.init_data = init_data; /* pdata */
else
isl6271a_matches[i].init_data; /* DT */
else
config.init_data = NULL;
前面的驱动程序并没有填充struct regulator_desc的每个字段。这在很大程度上取决于我们为其编写驱动程序的设备类型。一些驱动程序将整个工作交给了调节器核心,只提供了调节器核心需要处理的芯片寄存器地址。这样的驱动程序使用regmap API,这是一个通用的 I2C 和 SPI 寄存器映射库。drivers/regulator/max8649.c就是一个例子。
驱动程序示例
让我们总结一下之前讨论的内容,对于一个带有两个调节器的虚拟 PMIC 的真实驱动程序,其中第一个调节器的电压范围为 850000 µV 到 1600000 µV,步进为 50000 µV,而第二个调节器的电压固定为 1300000 µV:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h> /* For platform devices */
#include <linux/interrupt.h> /* For IRQ */
#include <linux/of.h> /* For DT*/
#include <linux/err.h>
#include <linux/regulator/driver.h>
#include <linux/regulator/machine.h>
#define DUMMY_VOLTAGE_MIN 850000
#define DUMMY_VOLTAGE_MAX 1600000
#define DUMMY_VOLTAGE_STEP 50000
struct my_private_data {
int foo;
int bar;
struct mutex lock;
};
static const struct of_device_id regulator_dummy_ids[] = {
{ .compatible = "packt,regulator-dummy", },
{ /* sentinel */ }
};
static struct regulator_init_data dummy_initdata[] = {
[0] = {
.constraints = {
.always_on = 0,
.min_uV = DUMMY_VOLTAGE_MIN,
.max_uV = DUMMY_VOLTAGE_MAX,
},
},
[1] = {
.constraints = {
.always_on = 1,
},
},
};
static int isl6271a_get_voltage_sel(struct regulator_dev *dev)
{
return 0;
}
static int isl6271a_set_voltage_sel(struct regulator_dev *dev,
unsigned selector)
{
return 0;
}
static struct regulator_ops dummy_fixed_ops = {
.list_voltage = regulator_list_voltage_linear,
};
static struct regulator_ops dummy_core_ops = {
.get_voltage_sel = isl6271a_get_voltage_sel,
.set_voltage_sel = isl6271a_set_voltage_sel,
.list_voltage = regulator_list_voltage_linear,
.map_voltage = regulator_map_voltage_linear,
};
static const struct regulator_desc dummy_desc[] = {
{
.name = "Dummy Core",
.id = 0,
.n_voltages = 16,
.ops = &dummy_core_ops,
.type = REGULATOR_VOLTAGE,
.owner = THIS_MODULE,
.min_uV = DUMMY_VOLTAGE_MIN,
.uV_step = DUMMY_VOLTAGE_STEP,
}, {
.name = "Dummy Fixed",
.id = 1,
.n_voltages = 1,
.ops = &dummy_fixed_ops,
.type = REGULATOR_VOLTAGE,
.owner = THIS_MODULE,
.min_uV = 1300000,
},
};
static int my_pdrv_probe (struct platform_device *pdev)
{
struct regulator_config config = { };
config.dev = &pdev->dev;
struct regulator_dev *dummy_regulator_rdev[2];
int ret, i;
for (i = 0; i < 2; i++){
config.init_data = &dummy_initdata[i];
dummy_regulator_rdev[i] = \
regulator_register(&dummy_desc[i], &config);
if (IS_ERR(dummy_regulator_rdev)) {
ret = PTR_ERR(dummy_regulator_rdev);
pr_err("Failed to register regulator: %d\n", ret);
return ret;
}
}
platform_set_drvdata(pdev, dummy_regulator_rdev);
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev)
{
int i;
struct regulator_dev *dummy_regulator_rdev = \
platform_get_drvdata(pdev);
for (i = 0; i < 2; i++)
regulator_unregister(&dummy_regulator_rdev[i]);
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = "regulator-dummy",
.of_match_table = of_match_ptr(regulator_dummy_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
一旦模块加载并且设备匹配,内核将打印类似于这样的内容:
Dummy Core: at 850 mV
Dummy Fixed: 1300 mV
然后可以检查底层发生了什么:
# ls /sys/class/regulator/
regulator.0 regulator.11 regulator.14 regulator.4 regulator.7
regulator.1 regulator.12 regulator.2 regulator.5 regulator.8
regulator.10 regulator.13 regulator.3 regulator.6 regulator.9
regulator.13和regulator.14已经被我们的驱动程序添加。现在让我们检查它们的属性:
# cd /sys/class/regulator
# cat regulator.13/name
Dummy Core
# cat regulator.14/name
Dummy Fixed
# cat regulator.14/type
voltage
# cat regulator.14/microvolts
1300000
# cat regulator.13/microvolts
850000
调节器消费者接口
消费者接口只需要驱动程序包含一个头文件:
#include <linux/regulator/consumer.h>
消费者可以是静态的或动态的。静态消费者只需要一个固定的供应,而动态消费者需要在运行时主动管理调节器。从消费者的角度来看,调节器设备在内核中表示为drivers/regulator/internal.h中定义的struct regulator结构的实例,如下所示:
/*
* struct regulator
*
* One for each consumer device.
*/
struct regulator {
struct device *dev;
struct list_head list;
unsigned int always_on:1;
unsigned int bypass:1;
int uA_load;
int min_uV;
int max_uV;
char *supply_name;
struct device_attribute dev_attr;
struct regulator_dev *rdev;
struct dentry *debugfs;
};
这个结构已经足够有意义,不需要我们添加任何注释。为了看到消费者如何轻松地使用调节器,这里有一个消费者获取调节器的小例子:
[...]
int ret;
struct regulator *reg;
const char *supply = "vdd1";
int min_uV, max_uV;
reg = regulator_get(dev, supply);
[...]
调节器设备请求
在获得对调节器的访问之前,消费者必须通过regulator_get()函数向内核请求。也可以使用托管版本,即devm_regulator_get()函数:
struct regulator *regulator_get(struct device *dev,
const char *id)
使用此函数的一个例子是:
reg = regulator_get(dev, "Vcc");
消费者传递它的struct device指针和电源供应 ID。内核将尝试通过查阅 DT 或特定于机器的查找表来找到正确的调节器。如果我们只关注设备树,*id应该与设备树中调节器供应的<name>模式匹配。如果查找成功,那么此调用将返回一个指向为此消费者提供电源的struct regulator的指针。
要释放调节器,消费者驱动程序应调用:
void regulator_put(struct regulator *regulator)
在调用此函数之前,驱动程序应确保对此调节器源进行的所有regulator_enable()调用都由regulator_disable()调用平衡。
一个消费者可以由多个调节器供应,例如,带有模拟和数字供应的编解码器消费者:
digital = regulator_get(dev, "Vcc"); /* digital core */
analog = regulator_get(dev, "Avdd"); /* analog */
消费者的probe()和remove()函数是抓取和释放调节器的适当位置。
控制调节器设备
调节器控制包括启用、禁用和设置调节器的输出值。
调节器输出启用和禁用
消费者可以通过调用以下方式启用其电源:
int regulator_enable(regulator);
此函数成功返回 0。相反的操作是通过调用此函数来禁用电源:
int regulator_disable(regulator);
要检查调节器是否已启用,消费者应调用此函数:
int regulator_is_enabled(regulator);
如果调节器已启用,则此函数返回大于 0 的值。由于调节器可能会被引导加载程序提前启用或与其他消费者共享,因此可以使用regulator_is_enabled()函数来检查调节器状态。
这里有一个例子,
printk (KERN_INFO "Regulator Enabled = %d\n",
regulator_is_enabled(reg));
对于共享的调节器,regulator_disable()只有在启用的引用计数为零时才会真正禁用调节器。也就是说,在紧急情况下,例如通过调用regulator_force_disable()可以强制禁用:
int regulator_force_disable(regulator);
我们将在接下来的章节中讨论的每个函数实际上都是围绕着regulator_ops操作的一个包装器。例如,regulator_set_voltage()在检查相应的掩码允许此操作设置后,内部调用regulator_ops.set_voltage,依此类推。
电压控制和状态
对于需要根据其操作模式调整其电源的消费者,内核提供了这个:
int regulator_set_voltage(regulator, min_uV, max_uV);
min_uV和max_uV是微伏的最小和最大可接受电压。
如果在调节器禁用时调用此函数,它将更改电压配置,以便在下次启用调节器时物理设置电压。也就是说,消费者可以通过调用regulator_get_voltage()来获取调节器配置的电压输出,无论调节器是否启用:
int regulator_get_voltage(regulator);
这里有一个例子,
printk (KERN_INFO "Regulator Voltage = %d\n",
regulator_get_voltage(reg));
电流限制控制和状态
我们在电压部分讨论的内容在这里也适用。例如,USB 驱动程序可能希望在供电时将限制设置为 500 毫安。
消费者可以通过调用以下方式控制其供应电流限制:
int regulator_set_current_limit(regulator, min_uA, max_uA);
min_uA和max_uA是微安的最小和最大可接受电流限制。
同样,消费者可以通过调用regulator_get_current_limit()来获取调节器配置的电流限制,无论调节器是否启用:
int regulator_get_current_limit(regulator);
工作模式控制和状态
为了有效的电源管理,一些消费者可能会在他们的操作状态改变时改变他们供应的工作模式。消费者驱动程序可以通过调用以下方式请求改变他们的供应调节器工作模式:
int regulator_set_optimum_mode(struct regulator *regulator,
int load_uA);
int regulator_set_mode(struct regulator *regulator,
unsigned int mode);
unsigned int regulator_get_mode(struct regulator *regulator);
消费者应仅在了解调节器并且不与其他消费者共享调节器时,才能在调节器上使用regulator_set_mode()。这被称为直接模式。regulator_set_uptimum_mode()会导致核心进行一些后台工作,以确定请求电流的最佳操作模式。这被称为间接模式。
调节器绑定
本节仅涉及消费者接口绑定。因为 PMIC 绑定包括为该 PMIC 提供的调节器提供init data,所以您应该参考将 init data 输入 DT部分以了解生产者绑定。
消费者节点可以使用以下绑定引用其一个或多个供应/调节器:
<name>-supply: phandle to the regulator node
这与 PWM 消费者绑定的原理相同。 <name> 应该有足够的意义,以便驱动程序在请求调节器时可以轻松地引用它。也就是说,<name> 必须与regulator_get()函数的*id参数匹配:
twl_reg1: regulator@0 {
[...]
};
twl_reg2: regulator@1 {
[...]
};
mmc: mmc@0x0 {
[...]
vmmc-supply = <&twl_reg1>;
vmmcaux-supply = <&twl_reg2>;
};
消费者代码(即 MMC 驱动程序)实际请求其供应可能如下所示:
struct regulator *main_regulator;
struct regulator *aux_regulator;
int ret;
main_regulator = devm_regulator_get(dev, "vmmc");
/*
* It is a good practive to apply the config before
* enabling the regulator
*/
if (!IS_ERR(io_regulator)) {
regulator_set_voltage(main_regulator,
MMC_VOLTAGE_DIGITAL,
MMC_VOLTAGE_DIGITAL);
ret = regulator_enable(io_regulator);
}
[...]
aux_regulator = devm_regulator_get(dev, "vmmcaux");
[...]
摘要
由于需要智能和平稳供电的各种设备,可以依靠本章来处理它们的电源管理。 PMIC 设备通常位于 SPI 或 I2C 总线上。在之前的章节中已经处理过这些总线,因此您应该能够编写任何 PMIC 驱动程序。现在让我们跳到下一章,该章涉及帧缓冲驱动程序,这是一个完全不同但同样有趣的主题。
第二十一章:帧缓冲驱动程序
视频卡始终具有一定数量的 RAM。这个 RAM 是图像数据的位图在显示时缓冲的地方。从软件的角度来看,帧缓冲是一个字符设备,提供对这个 RAM 的访问。
也就是说,帧缓冲驱动程序提供了一个接口:
-
显示模式设置
-
访问视频缓冲区的内存
-
基本的 2D 加速操作(例如滚动)
为了提供这个接口,帧缓冲驱动程序通常直接与硬件通信。有一些众所周知的帧缓冲驱动程序,比如:
-
intelfb,这是各种英特尔 8xx/9xx 兼容图形设备的帧缓冲
-
vesafb,这是一个使用 VESA 标准接口与视频硬件通信的帧缓冲驱动程序
-
mxcfb,i.MX6 芯片系列的帧缓冲驱动程序
帧缓冲驱动程序是 Linux 下最简单的图形驱动程序形式,不要将它们与实现高级功能(如 3D 加速等)的 X.org 驱动程序混淆,也不要将它们与内核模式设置(KMS)驱动程序混淆,后者公开了帧缓冲和 GPU 功能(与 X.org 驱动程序一样)。
i.MX6 X.org 驱动程序是一个闭源的,称为vivante。
回到我们的帧缓冲驱动程序,它们是非常简单的 API 驱动程序,通过字符设备公开了视频卡功能,可以通过/dev/fbX条目从用户空间访问。有关 Linux 图形堆栈的更多信息,可以参考 Martin Fiedler 的全面讲座Linux Graphics Demystified:keyj.emphy.de/files/linuxgraphics_en.pdf。
在本章中,我们涵盖以下主题:
-
帧缓冲驱动程序数据结构和方法,从而涵盖了整个驱动程序架构
-
帧缓冲设备操作,加速和非加速
-
从用户空间访问帧缓冲
驱动程序数据结构
帧缓冲驱动程序严重依赖于四个数据结构,所有这些都在include/linux/fb.h中定义,这也是您应该在代码中包含的头文件,以便处理帧缓冲驱动程序:
#include <linux/fb.h>
这些结构是fb_var_screeninfo,fb_fix_screeninfo,fb_cmap和fb_info。前三个可以在用户空间代码中使用。现在让我们描述每个结构的目的,它们的含义以及它们的用途。
- 内核使用
struct struct fb_var_screeninfo的实例来保存视频卡的可变属性。这些值是用户定义的,比如分辨率深度:
struct fb_var_screeninfo {
__u32 xres; /* visible resolution */
__u32 yres;
__u32 xres_virtual; /* virtual resolution */
__u32 yres_virtual;
__u32 xoffset; /* offset from virtual to visible resolution */
__u32 yoffset;
__u32 bits_per_pixel; /* # of bits needed to hold a pixel */
[...]
/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 rotate; /* angle we rotate counter clockwise */
};
这可以总结为以下所示的图:

- 视频卡的属性是固定的,要么由制造商固定,要么在设置模式时应用,并且否则不能更改。这通常是硬件信息。一个很好的例子是帧缓冲内存的开始,即使用户程序也不能更改。内核将这样的信息保存在
struct fb_fix_screeninfo结构的实例中:
struct fb_fix_screeninfo {
char id[16]; /* identification string eg "TT Builtin" */
unsigned long smem_start; /* Start of frame buffer mem */
/* (physical address) */
__u32 smem_len;/* Length of frame buffer mem */
__u32 type; /* see FB_TYPE_* */
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* see FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */
unsigned long mmio_start; /* Start of Memory Mapped I/O
*(physical address)
*/
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which */
/* specific chip/card we have */
__u16 capabilities; /* see FB_CAP_* */
};
struct fb_cmap结构指定了颜色映射,用于以内核可以理解的方式存储用户对颜色的定义,以便将其发送到底层视频硬件。可以使用这个结构来定义您对不同颜色所需的 RGB 比例:
struct fb_cmap {
__u32 start; /* First entry */
__u32 len; /* Number of entries */
__u16 *red; /* Red values */
__u16 *green; /* Green values */
__u16 *blue; /* Blue values */
__u16 *transp; /* Transparency. Discussed later on */
};
- 代表帧缓冲本身的
struct fb_info结构是帧缓冲驱动程序的主要数据结构。与前面讨论的其他结构不同,fb_info仅存在于内核中,不是用户空间帧缓冲 API 的一部分:
struct fb_info {
[...]
struct fb_var_screeninfo var; /* Variable screen information.
Discussed earlier. */
struct fb_fix_screeninfo fix; /* Fixed screen information. */
struct fb_cmap cmap; /* Color map. */
struct fb_ops *fbops; /* Driver operations.*/
char __iomem *screen_base; /* Frame buffer's
virtual address */
unsigned long screen_size; /* Frame buffer's size */
[...]
struct device *device; /* This is the parent */
struct device *dev; /* This is this fb device */
#ifdef CONFIG_FB_BACKLIGHT
/* assigned backlight device */
/* set before framebuffer registration,
remove after unregister */
struct backlight_device *bl_dev;
/* Backlight level curve */
struct mutex bl_curve_mutex;
u8 bl_curve[FB_BACKLIGHT_LEVELS];
#endif
[...]
void *par; /* Pointer to private memory */
};
struct fb_info结构应始终动态分配,使用framebuffer_alloc(),这是一个内核(帧缓冲核心)辅助函数,用于为帧缓冲设备的实例分配内存,以及它们的私有数据内存:
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)
在这个原型中,size表示私有区域的大小作为参数,并将其附加到分配的fb_info的末尾。可以使用fb_info结构中的.par指针引用此私有区域。framebuffer_release()执行相反的操作:
void framebuffer_release(struct fb_info *info)
设置完成后,应使用register_framebuffer()向内核注册帧缓冲,如果出现错误,则返回负的errno,或者成功返回零:
int register_framebuffer(struct fb_info *fb_info)
注册后,可以使用unregister_framebuffer()函数取消注册帧缓冲,如果出现错误,则返回负的errno,或者成功返回零:
int unregister_framebuffer(struct fb_info *fb_info)
分配和注册应在设备探测期间完成,而取消注册和释放应在驱动程序的remove()函数内完成。
设备方法
在struct fb_info结构中,有一个.fbops字段,它是struct fb_ops结构的一个实例。该结构包含一组需要在帧缓冲设备上执行一些操作的函数。这些是fbdev和fbcon工具的入口点。该结构中的一些方法是强制性的,是使帧缓冲正常工作所需的最低要求,而其他方法是可选的,取决于驱动程序需要公开的功能,假设设备本身支持这些功能。
以下是struct fb_ops结构的定义:
struct fb_ops {
/* open/release and usage marking */
struct module *owner;
int (*fb_open)(struct fb_info *info, int user);
int (*fb_release)(struct fb_info *info, int user);
/* For framebuffers with strange nonlinear layouts or that do not
* work with normal memory mapped access
*/
ssize_t (*fb_read)(struct fb_info *info, char __user *buf,
size_t count, loff_t *ppos);
ssize_t (*fb_write)(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos);
/* checks var and eventually tweaks it to something supported,
* DO NOT MODIFY PAR */
int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info);
/* set the video mode according to info->var */
int (*fb_set_par)(struct fb_info *info);
/* set color register */
int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green,
unsigned blue, unsigned transp, struct fb_info *info);
/* set color registers in batch */
int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info);
/* blank display */
int (*fb_blank)(int blank_mode, struct fb_info *info);
/* pan display */
int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info);
/* Draws a rectangle */
void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);
/* Copy data from area to another */
void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);
/* Draws a image to the display */
void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);
/* Draws cursor */
int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);
/* wait for blit idle, optional */
int (*fb_sync)(struct fb_info *info);
/* perform fb specific ioctl (optional) */
int (*fb_ioctl)(struct fb_info *info, unsigned int cmd,
unsigned long arg);
/* Handle 32bit compat ioctl (optional) */
int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd,
unsigned long arg);
/* perform fb specific mmap */
int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma);
/* get capability given var */
void (*fb_get_caps)(struct fb_info *info, struct fb_blit_caps *caps,
struct fb_var_screeninfo *var);
/* teardown any resources to do with this framebuffer */
void (*fb_destroy)(struct fb_info *info);
[...]
};
可以根据希望实现的功能设置不同的回调。
在第四章,字符设备驱动程序中,我们了解到字符设备可以通过struct file_operations结构导出一组文件操作,这些操作是与文件相关的系统调用的入口点,例如open(),close(),read(),write(),mmap(),ioctl()等。
也就是说,不要混淆fb_ops和file_operations结构。fb_ops提供了低级操作的抽象,而file_operations用于上层系统调用接口。内核在drivers/video/fbdev/core/fbmem.c中实现了帧缓冲文件操作,其中内部调用了我们在fb_ops中定义的方法。通过这种方式,可以根据系统调用接口的需要实现低级硬件操作,即file_operations结构。例如,当用户open()设备时,核心的打开文件操作方法将执行一些核心操作,并在设置时执行fb_ops.fb_open()方法,release,mmap等。
帧缓冲设备支持在include/uapi/linux/fb.h中定义的一些 ioctl 命令,用户程序可以使用这些命令来操作硬件。所有这些命令都由核心的fops.ioctl方法处理。对于其中的一些命令,核心的 ioctl 方法可能在内部执行fb_ops结构中定义的方法。
有人可能会想知道fb_ops.ffb_ioctl用于什么。当给定的 ioctl 命令内核不认识时,帧缓冲核心执行fb_ops.fb_ioctl。换句话说,fb_ops.fb_ioctl在帧缓冲核心的fops.ioctl方法的默认语句中执行。
驱动程序方法
驱动程序方法包括probe()和remove()函数。在进一步描述这些方法之前,让我们设置我们的fb_ops结构:
static struct fb_ops myfb_ops = {
.owner = THIS_MODULE,
.fb_check_var = myfb_check_var,
.fb_set_par = myfb_set_par,
.fb_setcolreg = myfb_setcolreg,
.fb_fillrect = cfb_fillrect, /* Those three hooks are */
.fb_copyarea = cfb_copyarea, /* non accelerated and */
.fb_imageblit = cfb_imageblit, /* are provided by kernel */
.fb_blank = myfb_blank,
};
Probe:驱动程序probe函数负责初始化硬件,使用framebuffer_alloc()函数创建struct fb_info结构,并在其上调用register_framebuffer()。以下示例假定设备是内存映射的。因此,可能存在非内存映射的情况,例如位于 SPI 总线上的屏幕。在这种情况下,应使用特定于总线的例程:
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
struct resource *res;
[...]
dev_info(&pdev->dev, "My framebuffer driver\n");
/*
* Query resource, like DMA channels, I/O memory,
* regulators, and so on.
*/
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
/* use request_mem_region(), ioremap() and so on */
[...]
pwr = regulator_get(&pdev->dev, "lcd");
info = framebuffer_alloc(sizeof(
struct my_private_struct), &pdev->dev);
if (!info)
return -ENOMEM;
/* Device init and default info value*/
[...]
info->fbops = &myfb_ops;
/* Clock setup, using devm_clk_get() and so on */
[...]
/* DMA setup using dma_alloc_coherent() and so on*/
[...]
/* Register with the kernel */
ret = register_framebuffer(info);
hardware_enable_controller(my_private_struct);
return 0;
}
Remove:remove()函数应释放在probe()中获取的任何内容,并调用:
static int myfb_remove(struct platform_device *pdev)
{
/* iounmap() memory and release_mem_region() */
[...]
/* Reverse DMA, dma_free_*();*/
[...]
hardware_disable_controller(fbi);
/* first unregister, */
unregister_framebuffer(info);
/* and then free the memory */
framebuffer_release(info);
return 0;
}
- 假设您使用了资源分配的管理器版本,您只需要使用
unregister_framebuffer()和framebuffer_release()。其他所有操作都将由内核完成。
详细的 fb_ops
让我们描述一些在fb_ops结构中声明的钩子。也就是说,要了解编写帧缓冲区驱动程序的想法,可以查看内核中的drivers/video/fbdev/vfb.c,这是一个简单的虚拟帧缓冲区驱动程序。还可以查看其他特定的帧缓冲区驱动程序,比如 i.MX6,位于drivers/video/fbdev/imxfb.c,或者查看内核关于帧缓冲区驱动程序 API 的文档,位于Documentation/fb/api.txt。
检查信息
钩子fb_ops->fb_check_var负责检查帧缓冲区参数。其原型如下:
int (*fb_check_var)(struct fb_var_screeninfo *var,
struct fb_info *info);
此函数应检查帧缓冲区变量参数并调整为有效值。var表示应检查和调整的帧缓冲区变量参数:
static int myfb_check_var(struct fb_var_screeninfo *var,
struct fb_info *info)
{
if (var->xres_virtual < var->xres)
var->xres_virtual = var->xres;
if (var->yres_virtual < var->yres)
var->yres_virtual = var->yres;
if ((var->bits_per_pixel != 32) &&
(var->bits_per_pixel != 24) &&
(var->bits_per_pixel != 16) &&
(var->bits_per_pixel != 12) &&
(var->bits_per_pixel != 8))
var->bits_per_pixel = 16;
switch (var->bits_per_pixel) {
case 8:
/* Adjust red*/
var->red.length = 3;
var->red.offset = 5;
var->red.msb_right = 0;
/*adjust green*/
var->green.length = 3;
var->green.offset = 2;
var->green.msb_right = 0;
/* adjust blue */
var->blue.length = 2;
var->blue.offset = 0;
var->blue.msb_right = 0;
/* Adjust transparency */
var->transp.length = 0;
var->transp.offset = 0;
var->transp.msb_right = 0;
break;
case 16:
[...]
break;
case 24:
[...]
break;
case 32:
var->red.length = 8;
var->red.offset = 16;
var->red.msb_right = 0;
var->green.length = 8;
var->green.offset = 8;
var->green.msb_right = 0;
var->blue.length = 8;
var->blue.offset = 0;
var->blue.msb_right = 0;
var->transp.length = 8;
var->transp.offset = 24;
var->transp.msb_right = 0;
break;
}
/*
* Any other field in *var* can be adjusted
* like var->xres, var->yres, var->bits_per_pixel,
* var->pixclock and so on.
*/
return 0;
}
前面的代码根据用户选择的配置调整可变帧缓冲区属性。
设置控制器的参数
钩子fp_ops->fb_set_par是另一个硬件特定的钩子,负责向硬件发送参数。它根据用户设置(info->var)来对硬件进行编程:
static int myfb_set_par(struct fb_info *info)
{
struct fb_var_screeninfo *var = &info->var;
/* Make some compute or other sanity check */
[...]
/*
* This function writes value to the hardware,
* in the appropriate registers
*/
set_controller_vars(var, info);
return 0;
}
屏幕空白
钩子fb_ops->fb_blank是一个硬件特定的钩子,负责屏幕空白。其原型如下:
int (*fb_blank)(int blank_mode, struct fb_info *info)
blank_mode参数始终是以下值之一:
enum {
/* screen: unblanked, hsync: on, vsync: on */
FB_BLANK_UNBLANK = VESA_NO_BLANKING,
/* screen: blanked, hsync: on, vsync: on */
FB_BLANK_NORMAL = VESA_NO_BLANKING + 1,
/* screen: blanked, hsync: on, vsync: off */
FB_BLANK_VSYNC_SUSPEND = VESA_VSYNC_SUSPEND + 1,
/* screen: blanked, hsync: off, vsync: on */
FB_BLANK_HSYNC_SUSPEND = VESA_HSYNC_SUSPEND + 1,
/* screen: blanked, hsync: off, vsync: off */
FB_BLANK_POWERDOWN = VESA_POWERDOWN + 1
};
空白显示的通常方法是对blank_mode参数进行switch case操作,如下所示:
static int myfb_blank(int blank_mode, struct fb_info *info)
{
pr_debug("fb_blank: blank=%d\n", blank);
switch (blank) {
case FB_BLANK_POWERDOWN:
case FB_BLANK_VSYNC_SUSPEND:
case FB_BLANK_HSYNC_SUSPEND:
case FB_BLANK_NORMAL:
myfb_disable_controller(fbi);
break;
case FB_BLANK_UNBLANK:
myfb_enable_controller(fbi);
break;
}
return 0;
}
空白操作应该禁用控制器,停止其时钟并将其断电。取消空白应执行相反的操作。
加速方法
用户视频操作,如混合、拉伸、移动位图或动态渐变生成都是繁重的任务。它们需要图形加速才能获得可接受的性能。可以使用struct fp_ops结构的以下字段来实现帧缓冲区加速方法:
-
.fb_imageblit(): 此方法在显示器上绘制图像,非常有用 -
.fb_copyarea(): 此方法将矩形区域从一个屏幕区域复制到另一个屏幕区域 -
.fb_fillrect():此方法以优化的方式填充一个带有像素行的矩形
因此,内核开发人员考虑到没有硬件加速的控制器,并提供了一种经过软件优化的方法。这使得加速实现是可选的,因为存在软件回退。也就是说,如果帧缓冲区控制器没有提供任何加速机制,必须使用内核通用例程填充这些方法。
这些分别是:
-
cfb_imageblit(): 这是用于 imageblit 的内核提供的回退。内核在启动过程中用它将标志输出到屏幕。 -
cfb_copyarea(): 用于区域复制操作。 -
cfb_fillrect(): 这是帧缓冲核心非加速方法,用于实现相同名称的操作。
把所有东西放在一起
在本节中,让我们总结前一节讨论的内容。为了编写帧缓冲区驱动程序,必须:
-
填充
struct fb_var_screeninfo结构,以提供有关帧缓冲区可变属性的信息。这些属性可以由用户空间更改。 -
填充
struct fb_fix_screeninfo结构,以提供固定参数。 -
设置
struct fb_ops结构,提供必要的回调函数,帧缓冲区子系统将使用这些函数响应用户操作。 -
在
struct fb_ops结构中,如果设备支持,必须提供加速函数回调。 -
设置
struct fb_info结构,用之前步骤中填充的结构填充它,并在其上调用register_framebuffer(),以便在内核中注册它。
要了解编写简单帧缓冲区驱动程序的想法,可以查看内核中的drivers/video/fbdev/vfb.c,这是一个虚拟帧缓冲区驱动程序。可以通过CONGIF_FB_VIRTUAL选项在内核中启用它。
用户空间的帧缓冲区
通常通过mmap()命令访问帧缓冲内存,以便将帧缓冲内存映射到系统 RAM 的某个部分,从而在屏幕上绘制像素变得简单,影响内存值。屏幕参数(可变和固定)是通过 ioctl 命令提取的,特别是FBIOGET_VSCREENINFO和FBIOGET_FSCREENINFO。完整列表可在内核源代码的include/uapi/linux/fb.h中找到。
以下是在帧缓冲上绘制 300*300 正方形的示例代码:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#define FBCTL(_fd, _cmd, _arg) \
if(ioctl(_fd, _cmd, _arg) == -1) { \
ERROR("ioctl failed"); \
exit(1); }
int main()
{
int fd;
int x, y, pos;
int r, g, b;
unsigned short color;
void *fbmem;
struct fb_var_screeninfo var_info;
struct fb_fix_screeninfo fix_info;
fd = open(FBVIDEO, O_RDWR);
if (tfd == -1 || vfd == -1) {
exit(-1);
}
/* Gather variable screen info (virtual and visible) */
FBCTL(fd, FBIOGET_VSCREENINFO, &var_info);
/* Gather fixed screen info */
FBCTL(fd, FBIOGET_FSCREENINFO, &fix_info);
printf("****** Frame Buffer Info ******\n");
printf("Visible: %d,%d \nvirtual: %d,%d \n line_len %d\n",
var_info.xres, this->var_info.yres,
var_info.xres_virtual, var_info.yres_virtual,
fix_info.line_length);
printf("dim %d,%d\n\n", var_info.width, var_info.height);
/* Let's mmap frame buffer memory */
fbmem = mmap(0, v_var.yres_virtual * v_fix.line_length, \
PROT_WRITE | PROT_READ, \
MAP_SHARED, fd, 0);
if (fbmem == MAP_FAILED) {
perror("Video or Text frame bufer mmap failed");
exit(1);
}
/* upper left corner (100,100). The square is 300px width */
for (y = 100; y < 400; y++) {
for (x = 100; x < 400; x++) {
pos = (x + vinfo.xoffset) * (vinfo.bits_per_pixel / 8)
+ (y + vinfo.yoffset) * finfo.line_length;
/* if 32 bits per pixel */
if (vinfo.bits_per_pixel == 32) {
/* We prepare some blue color */
*(fbmem + pos) = 100;
/* adding a little green */
*(fbmem + pos + 1) = 15+(x-100)/2;
/* With lot of read */
*(fbmem + pos + 2) = 200-(y-100)/5;
/* And no transparency */
*(fbmem + pos + 3) = 0;
} else { /* This assume 16bpp */
r = 31-(y-100)/16;
g = (x-100)/6;
b = 10;
/* Compute color */
color = r << 11 | g << 5 | b;
*((unsigned short int*)(fbmem + pos)) = color;
}
}
}
munmap(fbp, screensize);
close(fbfd);
return 0;
}
还可以使用cat或dd命令将帧缓冲内存转储为原始图像:
# cat /dev/fb0 > my_image
使用以下命令将其写回:
# cat my_image > /dev/fb0
可以通过特殊的/sys/class/graphics/fb<N>/blank sysfs文件来使屏幕变暗/恢复亮度,其中<N>是帧缓冲索引。写入 1 将使屏幕变暗,而 0 将使其恢复亮度:
# echo 0 > /sys/class/graphics/fb0/blank
# echo 1 > /sys/class/graphics/fb0/blank
总结
帧缓冲驱动程序是 Linux 图形驱动程序的最简单形式,需要很少的实现工作。它们对硬件进行了很大的抽象。在这个阶段,您应该能够增强现有的驱动程序(例如具有图形加速功能),或者从头开始编写一个全新的驱动程序。但是,建议依赖于一个现有的驱动程序,其硬件与您需要编写驱动程序的硬件共享尽可能多的特征。让我们跳到下一个也是最后一个章节,处理网络设备。
第二十二章:网络接口卡驱动程序
我们都知道网络是 Linux 内核固有的一部分。几年前,Linux 仅用于其网络性能,但现在情况已经改变;Linux 不仅仅是一个服务器,而且在数十亿嵌入式设备上运行。多年来,Linux 赢得了成为最佳网络操作系统的声誉。尽管如此,Linux 并不能做到一切。鉴于存在大量以太网控制器的多样性,Linux 别无选择,只能向需要为其网络设备编写驱动程序的开发人员或以一般方式进行内核网络开发的开发人员公开 API。该 API 提供了足够的抽象层,可以保证开发的代码的通用性,并且可以在其他架构上进行移植。本章将简要介绍处理网络接口卡(NIC)驱动程序开发的 API 的部分,并讨论其数据结构和方法。
在本章中,我们将涵盖以下主题:
-
NIC 驱动程序数据结构及其主要套接字缓冲区结构的详细介绍
-
NIC 驱动程序架构和方法描述,以及数据包的传输和接收
-
为测试目的开发一个虚拟 NIC 驱动程序
驱动程序数据结构
当你处理 NIC 设备时,有两个数据结构需要处理:
struct sk_buff结构,在include/linux/skbuff.h中定义,这是 Linux 网络代码中的基本数据结构,你的代码中也应该包含它:
#include <linux/skbuff.h>
-
每个发送或接收的数据包都使用这个数据结构处理。
-
struct net_device结构;这是内核中表示任何 NIC 设备的结构。这是数据传输发生的接口。它在include/linux/netdevice.h中定义,你的代码中也应该包含它:
#include <linux/netdevice.h>
其他应该在代码中包含的文件是include/linux/etherdevice.h,用于 MAC 和以太网相关函数(如alloc_etherdev()),以及include/linux/ethtool.h,用于 ethtools 支持:
#include <linux/ethtool.h>
#include <linux/etherdevice.h>
套接字缓冲区结构
这个结构包装了通过 NIC 传输的任何数据包:
struct sk_buff {
struct sk_buff * next;
struct sk_buff * prev;
ktime_t tstamp;
struct rb_node rbnode; /* used in netem & tcp stack */
struct sock * sk;
struct net_device * dev;
unsigned int len;
unsigned int data_len;
__u16 mac_len;
__u16 hdr_len;
unsigned int len;
unsigned int data_len;
__u16 mac_len;
__u16 hdr_len;
__u32 priority;
dma_cookie_t dma_cookie;
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char * head;
unsigned char * data;
unsigned int truesize;
atomic_t users;
};
以下是结构中元素的含义:
-
next和prev:这表示列表中的下一个和上一个缓冲区。 -
sk:这是与此数据包关联的套接字。 -
tstamp:这是数据包到达/离开的时间。 -
rbnode:这是红黑树中next/prev的替代。 -
dev:表示数据包到达或离开的设备。该字段与此处未列出的其他两个字段相关联。它们是input_dev和real_dev。它们跟踪与数据包相关的设备。因此,input_dev始终指的是数据包接收自的设备。 -
len:这是数据包中的总字节数。套接字缓冲区(SKB)由线性数据缓冲区和一个或多个称为rooms的区域组成。如果存在这样的区域,data_len将保存数据区域的总字节数。 -
mac_len:这保存了 MAC 头的长度。 -
csum:这包含数据包的校验和。 -
Priority:这表示 QoS 中的数据包优先级。 -
truesize:这个字段跟踪数据包占用的系统内存字节数,包括struct sk_buff结构本身占用的内存。 -
users:这用于 SKB 对象的引用计数。 -
Head:Head、data、tail 是套接字缓冲区中不同区域(rooms)的指针。 -
end:这指向套接字缓冲区的末尾。
这个结构的只讨论了一些字段。完整的描述可以在include/linux/skbuff.h中找到。这是你应该包含以处理套接字缓冲区的头文件。
套接字缓冲区分配
套接字缓冲区的分配有点棘手,因为它至少需要三个不同的函数:
-
首先,整个内存分配应该使用
netdev_alloc_skb()函数完成 -
使用
skb_reserve()函数增加和对齐头部空间 -
使用
skb_put()函数扩展缓冲区的已使用数据区域(将包含数据包)。
让我们看一下下面的图:

套接字缓冲区分配过程
- 我们通过
netdev_alloc_skb()函数分配足够大的缓冲区来包含一个数据包以及以太网头部:
struct sk_buff *netdev_alloc_skb(struct net_device *dev,
unsigned int length)
该函数在失败时返回NULL。因此,即使它分配了内存,netdev_alloc_skb()也可以从原子上下文中调用。
由于以太网头部长度为 14 字节,需要进行一些对齐,以便 CPU 在访问缓冲区的这部分时不会遇到性能问题。header_len参数的适当名称应该是header_alignment,因为该参数用于对齐。通常值为 2,这就是内核为此目的在include/linux/skbuff.h中定义了专用宏NET_IP_ALIGN的原因:
#define NET_IP_ALIGN 2
- 第二步通过减少尾部空间为头部保留对齐内存。执行此操作的函数是
skb_reserve():
void skb_reserve(struct sk_buff *skb, int len)
- 最后一步是通过
skb_put()函数扩展缓冲区的已使用数据区域,使其大小与数据包大小一样。该函数返回数据区域的第一个字节的指针:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
分配的套接字缓冲区应该转发到内核网络层。这是套接字缓冲区生命周期的最后一步。应该使用netif_rx_ni()函数来实现:
int netif_rx_ni(struct sk_buff *skb)
我们将在本章节的部分中讨论如何使用前面的步骤来处理数据包接收。
网络接口结构
网络接口在内核中表示为struct net_device结构的实例,定义在include/linux/netdevice.h中:
struct net_device {
char name[IFNAMSIZ];
char *ifalias;
unsigned long mem_end;
unsigned long mem_start;
unsigned long base_addr;
int irq;
netdev_features_t features;
netdev_features_t hw_features;
netdev_features_t wanted_features;
int ifindex;
struct net_device_stats stats;
atomic_long_t rx_dropped;
atomic_long_t tx_dropped;
const struct net_device_ops *netdev_ops;
const struct ethtool_ops *ethtool_ops;
unsigned int flags;
unsigned int priv_flags;
unsigned char link_mode;
unsigned char if_port;
unsigned char dma;
unsigned int mtu;
unsigned short type;
/* Interface address info. */
unsigned char perm_addr[MAX_ADDR_LEN];
unsigned char addr_assign_type;
unsigned char addr_len;
unsigned short neigh_priv_len;
unsigned short dev_id;
unsigned short dev_port;
unsigned long last_rx;
/* Interface address info used in eth_type_trans() */
unsigned char *dev_addr;
struct device dev;
struct phy_device *phydev;
};
struct net_device结构属于需要动态分配的内核数据结构,具有自己的分配函数。NIC 是通过alloc_etherdev()函数在内核中分配的。
struct net_device *alloc_etherdev(int sizeof_priv);
该函数在失败时返回NULL。sizeof_priv参数表示要为附加到此 NIC 的私有数据结构分配的内存大小,并且可以使用netdev_priv()函数提取:
void *netdev_priv(const struct net_device *dev)
给定struct priv_struct,这是我们的私有结构,以下是如何分配网络设备以及私有数据结构的实现:
struct net_device *net_dev;
struct priv_struct *priv_net_struct;
net_dev = alloc_etherdev(sizeof(struct priv_struct));
my_priv_struct = netdev_priv(dev);
未使用的网络设备应该使用free_netdev()函数释放,该函数还会释放为私有数据分配的内存。只有在设备从内核中注销后才应该调用此方法:
void free_netdev(struct net_device *dev)
在net_device结构完成并填充后,应该在其上调用register_netdev()。该函数在本章节的Driver Methods部分中有解释。请记住,该函数会将我们的网络设备注册到内核中,以便可以使用。也就是说,在调用此函数之前,您应该确保设备确实可以处理网络操作。
int register_netdev(struct net_device *dev)
设备方法
网络设备属于不出现在/dev目录中的设备类别(不像块设备、输入设备或字符设备)。因此,像所有这些类型的设备一样,NIC 驱动程序会暴露一组设施以执行。内核通过struct net_device_ops结构公开可以在网络接口上执行的操作,该结构是struct net_device结构的一个字段,表示网络设备(dev->netdev_ops)。struct net_device_ops字段描述如下:
struct net_device_ops {
int (*ndo_init)(struct net_device *dev);
void (*ndo_uninit)(struct net_device *dev);
int (*ndo_open)(struct net_device *dev);
int (*ndo_stop)(struct net_device *dev);
netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb,
struct net_device *dev);
void (*ndo_change_rx_flags)(struct net_device *dev, int flags);
void (*ndo_set_rx_mode)(struct net_device *dev);
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
int (*ndo_validate_addr)(struct net_device *dev);
int (*ndo_do_ioctl)(struct net_device *dev,
struct ifreq *ifr, int cmd);
int (*ndo_set_config)(struct net_device *dev, struct ifmap *map);
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
void (*ndo_tx_timeout) (struct net_device *dev);
struct net_device_stats* (*ndo_get_stats)(
struct net_device *dev);
};
让我们看看结构中每个元素的含义:
-
int (*ndo_init)(struct net_device *dev)和void(*ndo_uninit)(struct net_device *dev);它们是额外的初始化/反初始化函数,分别在驱动程序调用register_netdev()/unregister_netdev()以向内核注册/注销网络设备时执行。大多数驱动程序不提供这些函数,因为真正的工作是由ndo_open()和ndo_stop()函数完成的。 -
int (*ndo_open)(struct net_device *dev);准备并打开接口。每当ip或ifconfig实用程序激活它时,接口就会打开。在此方法中,驱动程序应请求/映射/注册所需的任何系统资源(I/O 端口,中断请求,DMA 等),打开硬件,并执行设备需要的任何其他设置。 -
int (*ndo_stop)(struct net_device *dev):当接口被关闭时(例如,ifconfig <name> down等),内核执行此函数。此函数应执行ndo_open()中所做的相反操作。 -
int (*ndo_start_xmit) (struct sk_buff *skb, struct net_device *dev):每当内核想要通过此接口发送数据包时,就会调用此方法。 -
void (*ndo_set_rx_mode)(struct net_device *dev):调用此方法以更改接口地址列表过滤模式,多播或混杂模式。建议提供此功能。 -
void (*ndo_tx_timeout)(struct net_device *dev):当数据包传输在合理的时间内未能完成时,通常为dev->watchdog滴答声,内核会调用此方法。驱动程序应检查发生了什么问题,处理问题,并恢复数据包传输。 -
struct net_device_stats *(*get_stats)(struct net_device *dev):此方法返回设备统计信息。这是当运行netstat -i或ifconfig时可以看到的内容。
前面的描述遗漏了很多字段。完整的结构描述可在include/linux/netdevice.h文件中找到。实际上,只有ndo_start_xmit是强制性的,但最好提供尽可能多的辅助钩子,以适应设备的功能。
打开和关闭
ndo_open()函数是由内核在授权用户(例如管理员)配置此网络接口时调用的,这些用户使用诸如ifconfig或ip之类的用户空间实用程序。
与其他网络设备操作一样,ndo_open()函数接收一个struct net_device对象作为其参数,驱动程序应该从中获取在分配net_device对象时存储在priv字段中的特定于设备的对象。
网络控制器通常在接收或完成数据包传输时引发中断。驱动程序需要注册一个中断处理程序,每当控制器引发中断时就会调用该处理程序。驱动程序可以在init()/probe()例程中或在open函数中注册中断处理程序。有些设备需要通过在硬件中的特殊寄存器中设置来启用中断。在这种情况下,可以在probe函数中请求中断,并在打开/关闭方法中设置/清除启用位。
让我们总结一下open函数应该做什么:
-
更新接口的 MAC 地址(如果用户更改了它,并且您的设备允许这样做)。
-
必要时重置硬件,并将其从低功耗模式中取出。
-
请求任何资源(I/O 内存,DMA 通道,中断请求)。
-
映射中断请求和注册中断处理程序。
-
检查接口链接状态。
-
调用
net_if_start_queue()来让内核知道您的设备已准备好传输数据包。
open函数的示例如下:
/*
* This routine should set everything up new at each open, even
* registers that should only need to be set once at boot, so that
* there is non-reboot way to recover if something goes wrong.
*/
static int enc28j60_net_open(struct net_device *dev)
{
struct priv_net_struct *priv = netdev_priv(dev);
if (!is_valid_ether_addr(dev->dev_addr)) {
[...] /* Maybe print a debug message ? */
return -EADDRNOTAVAIL;
}
/*
* Reset the hardware here and take it out of low
* power mode
*/
my_netdev_lowpower(priv, false);
if (!my_netdev_hw_init(priv)) {
[...] /* handle hardware reset failure */
return -EINVAL;
}
/* Update the MAC address (in case user has changed it)
* The new address is stored in netdev->dev_addr field
*/
set_hw_macaddr_registers(netdev, MAC_REGADDR_START,
netdev->addr_len, netdev->dev_addr);
/* Enable interrupts */
my_netdev_hw_enable(priv);
/* We are now ready to accept transmit requests from
* the queueing layer of the networking.
*/
netif_start_queue(dev);
return 0;
}
netif_start_queue()简单地允许上层调用设备的ndo_start_xmit例程。换句话说,它通知内核设备已准备好处理传输请求。
另一方面,关闭方法只需执行在打开设备时所做操作的相反操作:
/* The inverse routine to net_open(). */
static int enc28j60_net_close(struct net_device *dev)
{
struct priv_net_struct *priv = netdev_priv(dev);
my_netdev_hw_disable(priv);
my_netdev_lowpower(priv, true);
/**
* netif_stop_queue - stop transmitted packets
*
* Stop upper layers calling the device ndo_start_xmit routine.
* Used for flow control when transmit resources are unavailable.
*/
netif_stop_queue(dev);
return 0;
}
netif_stop_queue()只是netif_start_queue()的反向操作,告诉内核停止调用设备的ndo_start_xmit例程。我们不能再处理传输请求。
数据包处理
数据包处理包括数据包的传输和接收。这是任何网络接口驱动程序的主要任务。传输仅指发送传出帧,而接收指的是传入帧。
驱动网络数据交换有两种方式:轮询或中断。轮询是一种基于定时器的中断,由内核在给定的时间间隔内不断检查设备是否有任何变化。另一方面,中断模式是内核什么都不做,监听 IRQ 线,并等待设备通过 IRQ 通知变化。在高流量时,中断驱动的数据交换会增加系统开销。这就是为什么一些驱动程序混合了这两种方法。允许混合这两种方法的内核部分称为新 API(NAPI),在高流量时使用轮询,在流量变得正常时使用中断 IRQ 驱动管理。新驱动程序应该在硬件支持的情况下使用 NAPI。然而,本章不讨论 NAPI,而是专注于中断驱动的方法。
数据包接收
当数据包到达网络接口卡时,驱动程序必须围绕它构建一个新的套接字缓冲区,并将数据包复制到sk_ff->data字段中。复制的方式并不重要,也可以使用 DMA。驱动程序通常通过中断意识到新数据的到达。当网卡接收到一个数据包时,它会引发一个中断,由驱动程序处理,必须检查设备的中断状态寄存器,并检查引发中断的真正原因(可能是 RX ok,RX error 等)。对应于引发中断的事件的位将在状态寄存器中设置。
处理这个棘手的部分将在分配和构建套接字缓冲区。但幸运的是,我们已经在本章的第一部分讨论过这个问题。所以让我们不要浪费时间,直接跳到一个样本 RX 处理程序。驱动程序必须执行与其接收的数据包数量相同的sk_buff分配:
/*
* RX handler
* This function is called in the work responsible of packet
* reception (bottom half) handler. We use work because access to
* our device (which sit on a SPI bus) may sleep
*/
static int my_rx_interrupt(struct net_device *ndev)
{
struct priv_net_struct *priv = netdev_priv(ndev);
int pk_counter, ret;
/* Let's get the number of packet our device received */
pk_counter = my_device_reg_read(priv, REG_PKT_CNT);
if (pk_counter > priv->max_pk_counter) {
/* update statistics */
priv->max_pk_counter = pk_counter;
}
ret = pk_counter;
/* set receive buffer start */
priv->next_pk_ptr = KNOWN_START_REGISTER;
while (pk_counter-- > 0)
/*
* By calling this internal helper function in a "while"
* loop, packets get extracted one by one from the device
* and forwarder to the network layer.
*/
my_hw_rx(ndev);
return ret;
}
以下的辅助程序负责从设备获取一个数据包,转发到内核网络,并减少数据包计数:
/*
* Hardware receive function.
* Read the buffer memory, update the FIFO pointer to
* free the buffer.
* This function decrements the packet counter.
*/
static void my_hw_rx(struct net_device *ndev)
{
struct priv_net_struct *priv = netdev_priv(ndev);
struct sk_buff *skb = NULL;
u16 erxrdpt, next_packet, rxstat;
u8 rsv[RSV_SIZE];
int packet_len;
packet_len = my_device_read_current_packet_size();
/* Can't cross boundaries */
if ((priv->next_pk_ptr > RXEND_INIT)) {
/* packet address corrupted: reset RX logic */
[...]
/* Update RX errors stats */
ndev->stats.rx_errors++;
return;
}
/* Read next packet pointer and rx status vector
* This is device-specific
*/
my_device_reg_read(priv, priv->next_pk_ptr, sizeof(rsv), rsv);
/* Check for errors in the device RX status reg,
* and update error stats accordingly
*/
if(an_error_is_detected_in_device_status_registers())
/* Depending on the error,
* stats.rx_errors++;
* ndev->stats.rx_crc_errors++;
* ndev->stats.rx_frame_errors++;
* ndev->stats.rx_over_errors++;
*/
} else {
skb = netdev_alloc_skb(ndev, len + NET_IP_ALIGN);
if (!skb) {
ndev->stats.rx_dropped++;
} else {
skb_reserve(skb, NET_IP_ALIGN);
/*
* copy the packet from the device' receive buffer
* to the socket buffer data memory.
* Remember skb_put() return a pointer to the
* beginning of data region.
*/
my_netdev_mem_read(priv,
rx_packet_start(priv->next_pk_ptr),
len, skb_put(skb, len));
/* Set the packet's protocol ID */
skb->protocol = eth_type_trans(skb, ndev);
/* update RX statistics */
ndev->stats.rx_packets++;
ndev->stats.rx_bytes += len;
/* Submit socket buffer to the network layer */
netif_rx_ni(skb);
}
}
/* Move the RX read pointer to the start of the next
* received packet.
*/
priv->next_pk_ptr = my_netdev_update_reg_next_pkt();
}
当然,我们之所以从延迟工作中调用 RX 处理程序,是因为我们在 SPI 总线上。在 MMIO 设备的情况下,所有前面的操作都可以在 hwriq 中执行。看看 NXP FEC 驱动程序,在drivers/net/ethernet/freescale/fec.c中看看是如何实现的。
数据包传输
当内核需要从接口发送数据包时,它调用驱动程序的ndo_start_xmit方法,成功时应返回NETDEV_TX_OK,失败时应返回NETDEV_TX_BUSY,在这种情况下,当错误返回时,你不能对套接字缓冲区做任何事情,因为它仍然由网络排队层拥有。这意味着你不能修改任何 SKB 字段,或者释放 SKB 等等。这个函数受自旋锁的保护,防止并发调用。
在大多数情况下,数据包传输是异步进行的。传输的sk_buff由上层填充。它的data字段包含要发送的数据包。驱动程序应从sk_buff->data中提取数据包并将其写入设备硬件 FIFO,或将其放入临时 TX 缓冲区(如果设备在发送之前需要一定大小的数据)然后将其写入设备硬件 FIFO。只有当 FIFO 达到阈值(通常由驱动程序定义,或在设备数据表中提供)或驱动程序有意开始传输时,数据才会真正发送,通过在设备的特殊寄存器中设置一个位(一种触发器)。也就是说,驱动程序需要通知内核在硬件准备好接受新数据之前不要启动任何传输。通过netif_st op_queue()函数来完成此通知。
void netif_stop_queue(struct net_device *dev)
发送数据包后,网络接口卡将引发中断。中断处理程序应检查中断发生的原因。在传输中断的情况下,应更新其统计数据(net_device->stats.tx_errors和net_device->stats.tx_packets),并通知内核设备可以发送新数据包。通过netif_wake_queue()来完成此通知:
void netif_wake_queue(struct net_device *dev)
总结一下,数据包传输分为两部分:
-
ndo_start_xmit操作,通知内核设备忙,设置一切,并开始传输。 -
TX 中断处理程序,更新 TX 统计数据并通知内核设备再次可用。
ndo_start_xmit函数大致包含以下步骤:
-
调用
netif_stop_queue()来通知内核设备将忙于数据传输。 -
将
sk_buff->data的内容写入设备 FIFO。 -
触发传输(指示设备开始传输)。
操作(2)和(3)可能导致设备在慢总线上休眠(例如 SPI),可能需要延迟到工作结构。这是我们示例的情况。
一旦数据包传输完成,TX 中断处理程序应执行以下步骤:
-
根据设备是内存映射还是位于可能休眠的总线上,以下操作应直接在 hwirq 处理程序中执行,或者在工作(或线程中断)中调度执行:
-
检查中断是否是传输中断。
-
读取传输描述符状态寄存器并查看数据包的状态。
-
如果传输中出现任何问题,增加错误统计数据。
-
增加成功传输数据包的统计数据。
-
启动传输队列,允许内核通过
netif_wake_queue()函数再次调用驱动程序的ndo_start_xmit方法。
让我们在一个简短的示例代码中总结:
/* Somewhere in the code */
INIT_WORK(&priv->tx_work, my_netdev_hw_tx);
static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct priv_net_struct *priv = netdev_priv(dev);
/* Notify the kernel our device will be busy */
netif_stop_queue(dev);
/* Remember the skb for deferred processing */
priv->tx_skb = skb;
/* This work will copy data from sk_buffer->data to
* the hardware's FIFO and start transmission
*/
schedule_work(&priv->tx_work);
/* Everything is OK */
return NETDEV_TX_OK;
}
The work is described below:
/*
* Hardware transmit function.
* Fill the buffer memory and send the contents of the
* transmit buffer onto the network
*/
static void my_netdev_hw_tx(struct priv_net_struct *priv)
{
/* Write packet to hardware device TX buffer memory */
my_netdev_packet_write(priv, priv->tx_skb->len,
priv->tx_skb->data);
/*
* does this network device support write-verify?
* Perform it
*/
[...];
/* set TX request flag,
* so that the hardware can perform transmission.
* This is device-specific
*/
my_netdev_reg_bitset(priv, ECON1, ECON1_TXRTS);
}
TX 中断管理将在下一节中讨论。
驱动程序示例
我们可以在以下虚拟以太网驱动程序中总结上述讨论的概念:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/of.h> /* For DT*/
#include <linux/platform_device.h> /* For platform devices */
struct eth_struct {
int bar;
int foo;
struct net_device *dummy_ndev;
};
static int fake_eth_open(struct net_device *dev) {
printk("fake_eth_open called\n");
/* We are now ready to accept transmit requests from
* the queueing layer of the networking.
*/
netif_start_queue(dev);
return 0;
}
static int fake_eth_release(struct net_device *dev) {
pr_info("fake_eth_release called\n");
netif_stop_queue(dev);
return 0;
}
static int fake_eth_xmit(struct sk_buff *skb, struct net_device *ndev) {
pr_info("dummy xmit called...\n");
ndev->stats.tx_bytes += skb->len;
ndev->stats.tx_packets++;
skb_tx_timestamp(skb);
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
static int fake_eth_init(struct net_device *dev)
{
pr_info("fake eth device initialized\n");
return 0;
};
static const struct net_device_ops my_netdev_ops = {
.ndo_init = fake_eth_init,
.ndo_open = fake_eth_open,
.ndo_stop = fake_eth_release,
.ndo_start_xmit = fake_eth_xmit,
.ndo_validate_addr = eth_validate_addr,
.ndo_validate_addr = eth_validate_addr,
};
static const struct of_device_id fake_eth_dt_ids[] = {
{ .compatible = "packt,fake-eth", },
{ /* sentinel */ }
};
static int fake_eth_probe(struct platform_device *pdev)
{
int ret;
struct eth_struct *priv;
struct net_device *dummy_ndev;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
dummy_ndev = alloc_etherdev(sizeof(struct eth_struct));
dummy_ndev->if_port = IF_PORT_10BASET;
dummy_ndev->netdev_ops = &my_netdev_ops;
/* If needed, dev->ethtool_ops = &fake_ethtool_ops; */
ret = register_netdev(dummy_ndev);
if(ret) {
pr_info("dummy net dev: Error %d initalizing card ...", ret);
return ret;
}
priv->dummy_ndev = dummy_ndev;
platform_set_drvdata(pdev, priv);
return 0;
}
static int fake_eth_remove(struct platform_device *pdev)
{
struct eth_struct *priv;
priv = platform_get_drvdata(pdev);
pr_info("Cleaning Up the Module\n");
unregister_netdev(priv->dummy_ndev);
free_netdev(priv->dummy_ndev);
return 0;
}
static struct platform_driver mypdrv = {
.probe = fake_eth_probe,
.remove = fake_eth_remove,
.driver = {
.name = "fake-eth",
.of_match_table = of_match_ptr(fake_eth_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake Ethernet driver");
一旦模块加载并匹配设备,系统上将创建一个以太网接口。首先,让我们看看dmesg命令给我们显示了什么:
# dmesg
[...]
[146698.060074] fake eth device initialized
[146698.087297] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
如果运行ifconfig -a命令,接口将显示在屏幕上:
# ifconfig -a
[...]
eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
最后可以配置接口,分配 IP 地址,以便使用ifconfig显示:
# ifconfig eth0 192.168.1.45
# ifconfig
[...]
eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
inet addr:192.168.1.45 Bcast:192.168.1.255 Mask:255.255.255.0
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
状态和控制
设备控制是指内核需要自行改变接口属性的情况,或者响应用户操作。它可以使用struct net_device_ops结构公开的操作,如前所述,也可以使用另一个控制工具ethtool,它需要驱动程序引入一组新的钩子,我们将在下一节中讨论。相反,状态包括报告接口的状态。
中断处理程序
到目前为止,我们只处理了两种不同的中断:当新数据包到达或传出数据包的传输完成时;但现在硬件接口变得更加智能,它们能够报告其状态,无论是出于健全性目的还是数据传输目的。这样,网络接口也可以生成中断以信号错误、链路状态更改等。所有这些都应该在中断处理程序中处理。
这是我们的 hwrirq 处理程序的样子:
static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
struct priv_net_struct *priv = dev_id;
/*
* Can't do anything in interrupt context because we need to
* block (spi_sync() is blocking) so fire of the interrupt
* handling workqueue.
* Remember, we access our netdev registers through SPI bus
* via spi_sync() call.
*/
schedule_work(&priv->irq_work);
return IRQ_HANDLED;
}
因为我们的设备位于 SPI 总线上,所以一切都被延迟到work_struct中,其定义如下:
static void my_netdev_irq_work_handler(struct work_struct *work)
{
struct priv_net_struct *priv =
container_of(work, struct priv_net_struct, irq_work);
struct net_device *ndev = priv->netdev;
int intflags, loop;
/* disable further interrupts */
my_netdev_reg_bitclear(priv, EIE, EIE_INTIE);
do {
loop = 0;
intflags = my_netdev_regb_read(priv, EIR);
/* DMA interrupt handler (not currently used) */
if ((intflags & EIR_DMAIF) != 0) {
loop++;
handle_dma_complete();
clear_dma_interrupt_flag();
}
/* LINK changed handler */
if ((intflags & EIR_LINKIF) != 0) {
loop++;
my_netdev_check_link_status(ndev);
clear_link_interrupt_flag();
}
/* TX complete handler */
if ((intflags & EIR_TXIF) != 0) {
bool err = false;
loop++;
priv->tx_retry_count = 0;
if (locked_regb_read(priv, ESTAT) & ESTAT_TXABRT)
clear_tx_interrupt_flag();
/* TX Error handler */
if ((intflags & EIR_TXERIF) != 0) {
loop++;
/*
* Reset TX logic by setting/clearing appropriate
* bit in the right register
*/
[...]
/* Transmit Late collision check for retransmit */
if (my_netdev_cpllision_bit_set())
/* Handlecollision */
[...]
}
/* RX Error handler */
if ((intflags & EIR_RXERIF) != 0) {
loop++;
/* Check free FIFO space to flag RX overrun */
[...]
}
/* RX handler */
if (my_rx_interrupt(ndev))
loop++;
} while (loop);
/* re-enable interrupts */
my_netdev_reg_bitset(priv, EIE, EIE_INTIE);
}
Ethtool 支持
Ethtool 是一个用于检查和调整基于以太网的网络接口设置的小型实用程序。使用 ethtool,可以控制各种参数,如:
-
速度
-
媒体类型
-
双工操作
-
获取/设置 eeprom 寄存器内容
-
硬件校验和
-
唤醒 LAN 等。
需要 ethtool 支持的驱动程序应包括<linux/ethtool.h>。它依赖于struct ethtool_ops结构,这是该功能的核心,并包含一组用于 ethtool 操作支持的方法。其中大多数方法相对直接;有关详细信息,请参阅include/linux/ethtool.h。
为了使 ethtool 支持完全成为驱动程序的一部分,驱动程序应填充一个ethtool_ops结构并将其分配给struct net_device结构的.ethtool_ops字段。
my_netdev->ethtool_ops = &my_ethtool_ops;
宏SET_ETHTOOL_OPS也可以用于此目的。请注意,即使接口处于关闭状态,也可以调用您的 ethtool 方法。
例如,以下驱动程序实现了 ethtool 支持:
-
drivers/net/ethernet/microchip/enc28j60.c -
drivers/net/ethernet/freescale/fec.c -
drivers/net/usb/rtl8150.c
驱动程序方法
驱动程序的方法是probe()和remove()函数。它们负责(取消)在内核中注册网络设备。驱动程序必须通过struct net_device结构的设备方法向内核提供其功能。这些是可以在网络接口上执行的操作:
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_netdev_open,
.ndo_stop = my_netdev_close,
.ndo_start_xmit = my_netdev_start_xmit,
.ndo_set_rx_mode = my_netdev_set_multicast_list,
.ndo_set_mac_address = my_netdev_set_mac_address,
.ndo_tx_timeout = my_netdev_tx_timeout,
.ndo_change_mtu = eth_change_mtu,
.ndo_validate_addr = eth_validate_addr,
};
上述是大多数驱动程序实现的操作。
探测函数
probe函数非常基本,只需要执行设备的早期init,然后在内核中注册我们的网络设备。
换句话说,probe函数必须:
-
使用
alloc_etherdev()函数(通过netdev_priv()辅助)分配网络设备及其私有数据。 -
初始化私有数据字段(互斥锁,自旋锁,工作队列等)。如果设备位于可能休眠的总线上(例如 SPI),应使用工作队列(和互斥锁)。在这种情况下,hwirq 只需确认内核代码,并安排将在设备上执行操作的工作。另一种解决方案是使用线程化的中断。如果设备是 MMIO,可以使用自旋锁来保护关键部分并摆脱工作队列。
-
初始化总线特定的参数和功能(SPI,USB,PCI 等)。
-
请求和映射资源(I/O 内存,DMA 通道,中断请求)。
-
如有必要,生成一个随机 MAC 地址并分配给设备。
-
填充必需的(或有用的)netdev 属性:
if_port,irq,netdev_ops,ethtool_ops等。 -
将设备置于低功耗状态(
open()函数将其从此模式中移除)。 -
最后,在设备上调用
register_netdev()。
对于 SPI 网络设备,probe函数可能如下所示:
static int my_netdev_probe(struct spi_device *spi)
{
struct net_device *dev;
struct priv_net_struct *priv;
int ret = 0;
/* Allocate network interface */
dev = alloc_etherdev(sizeof(struct priv_net_struct));
if (!dev)
[...] /* handle -ENOMEM error */
/* Private data */
priv = netdev_priv(dev);
/* set private data and bus-specific parameter */
[...]
/* Initialize some works */
INIT_WORK(&priv->tx_work, data_tx_work_handler);
[...]
/* Devicerealy init, only few things */
if (!my_netdev_chipset_init(dev))
[...] /* handle -EIO error */
/* Generate and assign random MAC address to the device */
eth_hw_addr_random(dev);
my_netdev_set_hw_macaddr(dev);
/* Board setup must set the relevant edge trigger type;
* level triggers won't currently work.
*/
ret = request_irq(spi->irq, my_netdev_irq, 0, DRV_NAME, priv);
if (ret < 0)
[...]; /* Handle irq request failure */
/* Fill some netdev mandatory or useful properties */
dev->if_port = IF_PORT_10BASET;
dev->irq = spi->irq;
dev->netdev_ops = &my_netdev_ops;
dev->ethtool_ops = &my_ethtool_ops;
/* Put device into sleep mode */
My_netdev_lowpower(priv, true);
/* Register our device with the kernel */
if (register_netdev(dev))
[...]; /* Handle registration failure error */
dev_info(&dev->dev, DRV_NAME " driver registered\n");
return 0;
}
整个章节受到了 Microchip 的 enc28j60 的启发。您可以在drivers/net/ethernet/microchip/enc28j60.c中查看其代码。
register_netdev()函数接受一个完成的struct net_device对象并将其添加到内核接口;成功时返回 0,失败时返回负错误代码。struct net_device对象应存储在总线设备结构中,以便以后可以访问它。也就是说,如果您的网络设备是全局私有结构的一部分,那么您应该注册该结构。
请注意,重复的设备名称可能导致注册失败。
模块卸载
这是清理函数,它依赖于两个函数。我们的驱动程序释放函数可能如下所示:
static int my_netdev_remove(struct spi_device *spi)
{
struct priv_net_struct *priv = spi_get_drvdata(spi);
unregister_netdev(priv->netdev);
free_irq(spi->irq, priv);
free_netdev(priv->netdev);
return 0;
}
unregister_netdev()函数从系统中移除接口,内核将不再调用其方法;free_netdev()释放了struct net_device结构本身使用的内存,以及为私有数据分配的内存,以及与网络设备相关的任何内部分配的内存。请注意,您不应该自行释放netdev->priv。
总结
本章已经解释了编写 NIC 设备驱动程序所需的一切。即使本章依赖于位于 SPI 总线上的网络接口,但对于 USB 或 PCI 网络接口,原理是相同的。人们也可以使用提供用于测试目的的虚拟驱动程序。在本章之后,显然 NIC 驱动程序将不再是神秘的。




浙公网安备 33010602011771号