linux 驱动开发指南
Linux Driver Development Guide
基于
Linux 5.7
内核
李山文 1
官方交流 QQ 群:806452875
本书配套的源码获取方式:https://gitee.com/li-shan-asked/linux-develop-guide.git
linux 驱动开发指南 | 李山文
1
本书是以开源的方式发布,书中所有代码均可在指定的仓库中获取,本书不得以任
何商业功利方式转发或者售卖,同时引用本书中的内容请注明出处。若有疑问,请联系
作者
1
(邮箱:1477153217@qq.com
经过长达四个月(大部分时间为周末)的编写,本书第一版终于完成了,由于实际
按仓促,书中难免有诸多不足,恳请读者给出宝贵的意见。
版权所有,违者必究。
1
作者简介:李山文(1995 -,湖北咸宁,现工作上海浦东新区,毕业于南京理工大学,微电子学与固体电子学硕士
linux 驱动开发指南 | 李山文
2
虽然市面上有很多关于驱动开发的书,但无不例外都不太适合初学者。有的书过于理论,使得读
者没有兴趣或者看完之后没有收获;有的书太过庞大,使得读者并不能抓住驱动开发的主干;而有的
书有太过陈旧,已经无法跟上 Linux 更新的速度。基于这些原因,作者便开始了编写此书,旨在能够
让读者进入 Linux 驱动开发的大门。
本书的内容循序渐进,如果读者是初学者,最好能从头开始阅读。后面的内容都是基于前面的内
容讲解,书中尽可能提供完善的代码,同时以实践作为根本,让读者能够体会驱动开发的乐趣。
Linux 驱动开发经历了多次的更新迭代,这使得当前 Linux 驱动源码中充斥了各种不同框架代码。
这也让初学者一头雾水,不知所措。本书不会特别深入的研究内核源码,而是以驱动开发作为主干,
帮助读者领略驱动开发的过程。如果读者想继续深入了解驱动框架内部的实现过程,读者可以阅读
Linux 内核相关的书籍。
本书分为三卷,其中 1 卷主要讲解的 Linux 系统的移植裁剪,包括 uboot 移植、Linux 内核移植
和根文件系统制作。2 卷主要讲解 Linux 的字符设备驱动开发,从最简单的驱动模型到基于设备树再
到平台设备驱动框架和 sysfs 设备文件系统,最后详细讲解了 Linux 内核中常用的各类子系统。3
主要讲解了 Linux 的块设备驱动和网络设备驱动,这类驱动将对于字符设备驱动要复杂得多,因此主
要讲解其基本框架和相关驱动的移植工作。
每个章节都会有一个示例驱动程序和测试代码,这些工作旨在帮助读者不仅能够理解 Linux 驱动
的基本原理和架构,同时还会如何运用将其应用在实际的开发中。
书中的代码尽可能简单,省略与驱动框架无关的代码,其目的是把握驱动框架重点,不会让读者
沉溺于代码细节之中。
本书中所使用的开发板可以 Lite200 系列(笔者自己设计)licheeePI nanolicheePI zero
从第 9 章起将不限于开发板。
限于作者水平有限,书中难免存在不完善甚至错误之处,恳请读者指出。
李山文
2021 10
linux 驱动开发指南 | 李山文
3
........................................................................................................................................................ 2
第一章 绪论.............................................................................................................................................. 8
1.1 Linux 驱动基础 ........................................................................................................................... 9
1.1.1 Linux 文件结构 ................................................................................................................ 9
1.1.2 驱动框架发展................................................................................................................ 11
1.2 Linux 系统移植 ......................................................................................................................... 12
1.2.1 环境搭建........................................................................................................................ 13
1.2.2 u-boot 移植 ..................................................................................................................... 18
1.2.3 kernel 移植...................................................................................................................... 25
1.2.4 rootfs 移植 ...................................................................................................................... 32
1.2.5 系统镜像打包................................................................................................................ 36
1.3 软硬件分离思想....................................................................................................................... 44
1.3.1 对象编程概念................................................................................................................ 44
1.3.2 C 语言对象化编程 ......................................................................................................... 46
1.3.3 软件层次化设计............................................................................................................ 47
1.3.4 设备管理........................................................................................................................ 51
1.4 应用程序编译........................................................................................................................... 52
第二章 字符设备驱动............................................................................................................................ 56
2.1 驱动架构................................................................................................................................... 56
2.2 LED 驱动实例 ........................................................................................................................... 61
2.2.1 知识讲解........................................................................................................................ 61
2.3 驱动编译进内核....................................................................................................................... 77
2.3.1 驱动源码结构................................................................................................................ 77
2.3.2 新增驱动配置................................................................................................................ 79
2.4 简单中断设备驱动................................................................................................................... 81
2.4.1 按键中断示例................................................................................................................ 83
2.4.1 测试程序(由于没有相关硬件,未测试)............................................................... 88
第三章 基于设备树驱动模型................................................................................................................ 90
3.1 设备树(Device Tree ........................................................................................................... 90
3.1.1 设备树模型.................................................................................................................... 90
3.1.2 获取设备树信息............................................................................................................ 95
3.1.3 设备树的编译过程....................................................................................................... 102
3.2 驱动实例.................................................................................................................................. 103
3.2.1 LED 驱动 ...................................................................................................................... 103
第四章 platform 驱动模型 ................................................................................................................... 111
4.1 设备和驱动注册(未引入 dts ........................................................................................... 111
4.1.1 驱动注册....................................................................................................................... 112
4.1.2 设备注册....................................................................................................................... 114
4.2 驱动注册(引入 dts ........................................................................................................... 117
linux 驱动开发指南 | 李山文
4
4.2.1 驱动的注册步骤.......................................................................................................... 120
4.2.2 示例源码...................................................................................................................... 124
4.3 miscdevice 设备驱动............................................................................................................... 128
4.3.1 misc 设备的注册步骤 .................................................................................................. 129
4.3.2 示例源码...................................................................................................................... 132
第五章 中断系统.................................................................................................................................. 137
5.1 中断控制器............................................................................................................................. 137
5.2 GIC 中断控制器 ...................................................................................................................... 138
5.3 中断控制器驱动..................................................................................................................... 141
5.4 设备树中断节点解析............................................................................................................. 142
5.5 中断节点属性......................................................................................................................... 143
5.6 中断 API ................................................................................................................................. 145
5.6.1 中断注册...................................................................................................................... 145
5.6.2 中断释放...................................................................................................................... 147
5.6.3 中断开关...................................................................................................................... 147
5.7 按键示例................................................................................................................................. 148
5.7.1 ADC 按键原理 ............................................................................................................. 148
5.7.2 驱动程序编写.............................................................................................................. 149
5.7.3 驱动测试程序.............................................................................................................. 158
第六章 sysfs 设备模型......................................................................................................................... 160
6.1 sysfs 设备文件系统................................................................................................................. 160
6.1.1 kobject ........................................................................................................................... 162
6.1.2 kset ................................................................................................................................ 163
6.1.3 kobj_type....................................................................................................................... 165
6.1.4 attribute ......................................................................................................................... 165
6.2 创建属性文件 API ................................................................................................................. 168
6.3 LED 驱动示例 ......................................................................................................................... 169
第七章 并发控制.................................................................................................................................. 175
7.1 基本概念................................................................................................................................. 175
7.1.1 操作系统实现原理...................................................................................................... 175
7.1.2 进程和线程.................................................................................................................. 176
7.1.3 资源共享与互斥访问.................................................................................................. 177
7.2 线程同步机制......................................................................................................................... 177
7.2.1 创建进程...................................................................................................................... 178
7.2.2 信号量.......................................................................................................................... 179
7.2.3 自旋锁.......................................................................................................................... 180
7.3 SMP 多核处理器..................................................................................................................... 182
7.4 原子操作................................................................................................................................. 183
7.4.1 整型原子操作.............................................................................................................. 183
7.4.2 位型原子操作.............................................................................................................. 183
7.4.3 原子操作的应用.......................................................................................................... 184
第八章 GPIO 子系统 ........................................................................................................................... 186
linux 驱动开发指南 | 李山文
5
8.1 GPIO 子系统 ........................................................................................................................... 186
8.1.1 GPIO 子系统 API(已弃用) ..................................................................................... 187
8.1.2 GPIO 子系统 API ......................................................................................................... 189
8.2 pinctrl 子系统 .......................................................................................................................... 192
8.3 LED 示例 ................................................................................................................................. 194
第九章 输入子系统.............................................................................................................................. 200
9.1 input_dev 结构体..................................................................................................................... 201
9.2 输入设备的注册与注销......................................................................................................... 203
9.3 输入设备的事件报告............................................................................................................. 203
9.4 按键模拟键盘示例................................................................................................................. 204
9.4.1 驱动程序编写.............................................................................................................. 204
9.4.2 测试程序编写.............................................................................................................. 210
第十章 SPI 子系统 ............................................................................................................................... 214
10.1 SPI 子系统框架 ..................................................................................................................... 215
10.2 SPI 设备结构体 ..................................................................................................................... 216
10.3 SPI 驱动注册与注销 ............................................................................................................. 219
8.3.1 SPI 驱动的注册 ............................................................................................................ 220
8.3.2 SPI 驱动的注销 ............................................................................................................ 220
10.4 SPI 数据传输 ......................................................................................................................... 220
10.4.1 spi_transfer 结构体 ..................................................................................................... 221
10.4.2 spi_message 结构体 ................................................................................................... 222
10.5 SPI 驱动示例 ......................................................................................................................... 224
第十一章 I
2
C 子系统 ........................................................................................................................... 232
11.1 I2C 子系统框架 ..................................................................................................................... 233
11.2 I2C 设备(客户端) ............................................................................................................. 234
11.2.1 设备(客户端)结构体 ............................................................................................ 234
11.2.2 设备树节点 ................................................................................................................ 235
11.3 I2C 驱动注册和注销 ............................................................................................................. 236
11.3.1 probe 函数 ................................................................................................................... 237
11.3.2 remove 函数 ................................................................................................................ 237
11.3.3 id_table 设备 ID ..................................................................................................... 238
11.3.4 driver 驱动匹配 .......................................................................................................... 238
11.3.5 I2C 驱动注册与注销 .................................................................................................. 238
11.4 I2C 数据传输 ......................................................................................................................... 239
11.4.1 传统的 I2C 数据传输 ................................................................................................ 239
11.4.2 系统管理总线数据传输(SMBus ........................................................................ 242
11.5 I2C 驱动示例 ......................................................................................................................... 243
第十二章 Regmap 框架 ....................................................................................................................... 251
12.1 Regmap 接口使用 ................................................................................................................. 251
12.1.1 配置 regmap_config 结构体 ...................................................................................... 251
12.1.2 Regmap 初始化 .......................................................................................................... 253
12.1.3 Regmap 读写操作 ...................................................................................................... 254
linux 驱动开发指南 | 李山文
6
12.1.4 释放 Regmap ............................................................................................................. 255
12.2 OLED 示例 ............................................................................................................................ 255
12.2.1 驱动分析.................................................................................................................... 255
12.2.2 完整源码.................................................................................................................... 257
12.2.3 驱动测试.................................................................................................................... 261
第十三章 Frame Buffer 设备驱动 ....................................................................................................... 263
13.1 显示原理与计算机图形学................................................................................................... 263
13.2 fb 设备相关结构体 ............................................................................................................... 265
13.2.1 fb_ops .......................................................................................................................... 265
13.2.2 fb_var_screeninfo 结构体 .......................................................................................... 267
13.2.3 fb_fix_screeninfo 结构体 ........................................................................................... 268
13.2.4 fb_cmpa 结构体.......................................................................................................... 269
13.2.5 fb_info 结构体 ............................................................................................................ 269
13.3 帧缓冲设备注册与注销....................................................................................................... 271
13.4 显示刷新............................................................................................................................... 271
13.6 console tty uart 关系 .............................................................................................................. 274
13.5 LCD 驱动示例....................................................................................................................... 275
13.5.1 驱动分析及编写........................................................................................................ 275
13.5.2 驱动代码.................................................................................................................... 281
13.5.3 驱动测试.................................................................................................................... 287
13.6 fbtft 驱动使用 ........................................................................................................................ 289
第十四章 应用移植开发示例(待删除).......................................................................................... 294
14.1 串口传输工具移植............................................................................................................... 294
14.2 QT 应用开发 ......................................................................................................................... 296
14.2.1 开发环境搭建............................................................................................................ 297
14.2.2 QT 应用程序开发 ...................................................................................................... 298
14.2.3 QT Creator 开发 ......................................................................................................... 299
附录........................................................................................................................................................ 307
A. Linux 键盘键值对照表(部分) ............................................................................................ 307
B. Linux fdisk 命令分区格式编码 .......................................................................................... 308
linux 驱动开发指南 | 李山文
7
1
linux 驱动开发指南 | 李山文
8
第一章 绪论
Linux 操作系统是一个较为复杂的宏内核操作系统,从官方下载的源码可以看到,其文件大小约
200M 左右,但是内核部分只有很小的一部分,剩下的都是各个不同的平台的设备驱动部分,也就是
Linux 内核绝大部分文件都是驱动文件。当设备变得越来越丰富的时候,此时如果不对设备进行有
效的管理,操作一个设备将变得相当混乱。因此现代操作系统都采用软硬件分离的思想,这体现了低
耦合高内聚代码设计规范。
1-1 各种发行 Linux 操作系
Linux 内核没有图形界面,当前内核源码由操作系统核心组件和驱动构成,因此大多数人接触的 Linux
其实是各个软件开发商或者开源社区进行包装及修改的发行版 Linux这种操作系统一般有图形界面
的和没有图形界面的。有图形界面的一般为桌面操作系统,而没有图形界面的一般是服务器用的。
1-2 ubuntu 桌面操作系统界面
本书中将使用 ubuntu-16.04.6该版本较为稳定,同时也是一个长期维护版本。嵌入式开发需要有
宿主机来实现源代码的编译,之后需要将编译好的目标文件下载到目标机(嵌入式平台)上,这样目
标机就可以执行我们的代码。
linux 驱动开发指南 | 李山文
9
1-3 嵌入式开发流程
整个 Linux 操作系统的驱动部分分为三类,分别是字符设备驱动、块设备驱动和网络设备驱动。
字符设备驱动主要包括鼠标、键盘、显示器、触控面板等,该设备的特点是读写设备的时候必须按照
一个字节的顺序进行;块设备驱动包括硬盘、SD 卡等一些存储设备,该设备可以读取多个字节进行
操作;网络设备驱动包括有限网卡、无线网卡等网络设备。其中字符设备最为简单,而网络设备最为
复杂,整个 Linux 核心与驱动紧密相关,因此驱动开发也变得越来越复杂。特别是在开发驱动的过程
中很有可能导致内核崩溃,这样调试将强制终止,给开发人员带来了巨大的挑战。
1-1 Linux 驱动架构
1.1 Linux 驱动基础
1.1.1 Linux 文件结构
了解 Linux 的文件结构非常重要,在后面的驱动开发中,读者需要知道我们的代码对文件做了哪
些改变。举个简单的例子,当我们添加设备节点时,我们知道,当成功创建节点时就会在/dev 目录下
生成一个节点文件,同时我们在成功注册设备类的时候,会在/sys/class 目录下创建一个设备类文件夹。
Linux 中有一句经典的话,“一切皆文件” Linux 驱动开发可以非常深刻的体会到这一点,所有
的设备驱动都会对 Linux 文件结构产生变化,开发者几乎可以从文件中直接观察。下面我们简单看下
linux 驱动开发指南 | 李山文
10
开发板中的文件结构。启动 Linux 开发板,进入根文件目录,然后执行
cd /
ls -al
如下图所示:
1-2 开发板上 Linux 件结构
可以看到,在根目录下有图中所示文件和文件夹,每个文件或者文件夹最前面都有属性,例如 bin
最前面是 drwxr-xr-xLinux 中将文件或者文件夹分了三组,分别是属主权限、属组权限、其他用户权
限,第一个是文件类型,d 表示文件夹,-表示文件,l 表示链接。属主权限是指 root 用户对该文件进
行操作的权限;属组权限是指在该系统中的其他用户所具有的权限;而其他用户权限是指即不是 root
用户,也不是属组用户的用户所具有的权限。
1-3 Linux 文件属性
├── bin 常用的系统命令
├── boot 放置一些启动用的临时文件
├── dev 设备节点文件
├── etc 系统所有配置文件都放在它下面
├── home 用户目录,对应权限用户只能查看到自己的目录
├── name 用户私有目录
├── lib 用于存取程序的动态库和模块文件
linux 驱动开发指南 | 李山文
11
├── lost+found 用于存放系统异常时丢失的文件
├── media 用于挂在本地磁盘或者其他储存设备
├── mnt 用于挂在其他临时系统文件
├── opt 存放用户应用程序
├── proc 包含进程等信息,不是内存映射,不是真实目录
├── root root 用户的目录
├── run 系统启动后存放的系统相关文件,关机后会删除
├── sbin 里面很多是 root 用户才能执行的命令,系统的更新备份还原和开关机用的
├── sys sysfs 设备文件系统
├── tmp 存放各种临时文件
├── usr 我们主要的操作空间
├── var 变量文件--在运行中内容不断变化的文件。
上面是 Linux 的文件结构以及说明,对于驱动开发者而言,我们经常会接触/dev 目录和/sys 目录
以及/proc 目录。例如当我们创建一个设备节点时,会在/dev 目录下生成一个设备节点文件,应用程
序会通过接口来对这些文件进行读写,内核会通过系统调用来回调执行底层的驱动接口。当我们使用
sysfs 设备文件时,我们将会在/sys 目录下创建相关的设备文件,这些文件可以直接在终端中对其进行
操作,方便驱动开发过程中的调式。/proc 目录下存放了一些系统相关的信息,我们可以通过
/proc/interrupts 文件来查看当前所有的中断号注册情况以及中断触发次数。
对于一些发行版的操作系统可能会加入其他对的一些目录,但基本的目录仍然还是上面那些。
要注意的是,对于一些特别关键的文件和文件夹,用户不可随意删除,同时也不可随意修改,特别是
/etc 中的文件。
Linux 操作系统使用的文件格式为 JFS ReiserFSextext2ext3ISO9660XFSMinxMSDOS
UMSDOSVFATNTFSHPFSNFS SMBSysVPROC而我们在嵌入式 Linux 中一般使用的文件系
统格式为 ext3 或者 ext4 格式,这些格式在 Windows 系统下是不可访问的,会被识别为硬盘损害而报
错,因此我们的开发板的根文件系统一定要在 Linux 操作系统中打开。
1.1.2 动框架发展
随着 Linux 内核的不断完善,其驱动框架经历了很多次的不断修改和发展。从最开始的较为原始
的驱动框架发展成为现在众多子系统的基于设备树的平台设备驱动框架,这提高了驱动开发人员的学
习成本,但同时由于其框架的不断完善,也降低了驱动开发人员的开发难度。
1-4 Linux 驱动框架发展
linux 驱动开发指南 | 李山文
12
可以看到,在 2.4 版本之前,Linux 内核的驱动框架还较为原始,这种框架一般只能由 Linux
核开发维护人员来实现。在 2.4 版本之后,内核开始引入了基本的 devfs 驱动框架,该驱动框架虽然
有了很大的改进,但仍然存在缺陷,原因在于该驱动的一些执行过程仍然在内核态,但大多数人认为
该执行过程应该交给用户态,这也是当年的 udev devfs 之争。随后在 2.6 版本之后有了非常大的突
破,废除了 devfs全面使用 udev2.6 版本可以说是 Linux 内核发展的重要里程碑,在该版本中添加
了重要的驱动框架,比如我们呢熟悉的 udevplatform 平台设备驱动框架以及 sysfs 设备。实际上还
有很多其他的设备驱动框架,例如 profs,这里没有对其进行过多说明,因为本书中内容内有过多涉
及到该驱动框架。虽然在 Linux 2.6 版本之后有了非常大的完善,但任何事物都是朝着发展的方向前
进,随着驱动设备的日益增多,各个 SoC 厂家添加了自己的芯片驱动代码到 Linux 内核中,这也导致
Linux 2.6 之后的版本中充斥了各种 xxx_match 的厂家 BSP 文件。然而 Linus 是一个精益求精且脾
气暴躁的大牛,他公然发邮件斥责 ARM 架构的驱动代码,认为他们在他的内核中不断填充垃圾。
于这件事之后,Linux 借鉴 PowerPC 的驱动框架,开始引入 platform 驱动和设备树 dts,从此设备树
框架由此诞生。然而由于 Linux 的发展太过久远,至今为止 Linux 核中仍然充斥着各种 xxx_match
厂家代码。不过随着时间的推进,Linux 内核驱动代码已经极力推崇基于设备树的驱动框架,因此本
书中的所有代码将会基于设备树来讲解(在前面的篇章中可能够没有基于设备树,直到讲解设备树后
会全面使用设备树驱动框架)
1.2 Linux 系统移植
所谓 Linux 移植就是让 Linux 操作系统能够在嵌入式硬件上面成功运行,并正确执行我们的任
务。实际上在这并不是一件容易的事情,由于硬件的多样化,每个硬件的底层都不一样,特别是不同
的架构,其硬件结构差异相当巨大,这也导致了 Linux 系统移植的难度加大。对于初学者而言,从零
独自移植 Linux 几乎是一件不可能完成的事情,即使是有一定工作年限的开发者而言也不是一件轻松
的事情。因此下面我们的 Linux 移植工作不会从零开始移植,而是基于现有的最接近的 SoC 代码来
移植,当然我们的原则是尽可能采用官方发布分 Linux 源码
2
为基础。
Linux 内核移植主要分为三个部分,分别是 bootloader 移植、kernel 移植、rootfs 根文件系统制作。
这些移植工作都需要相关人员具备基本的 Linux 操作技能,这里默认读者已具备此技能。
bootloader SoC 芯片最开始执行的代码,这个部分的主要工作是进行硬件初始化,例如时钟初
始化,SRAM 初始化等,同时也为后面运行 C 语言建立环境(初始化堆栈)kernel 在存放在 bootloader
之后,对于 SoC 来说,代码都需要在 RAM 中运行,这里与 MCU 不一样的地方就是引入了 MMU(内
存管理单元)。对于 MCU 而言,由于其执行速度低,因此运行代码都在 ROM 中直接运行,而对于
Flash 而言,其读取速度远不及 RAM 的速度,因此对于运行速度非常快的 SoC 而言,所有的代码都
需要在 RAM 中运行。但是这里有一个问题,RAM 掉电数据将会丢失,故代码保存不可能放在 RAM
中,当前所有的嵌入式设备而言,代码保存都是放在 ROM 中,因此在 SoC 中运行代码需要将代码搬
运到 RAM 中然后再执行。对于 bootloader 来说。其代码较少,很多开发者认为再将 bootloader 搬到
RAM 中实际意义不大,除非 bootloader 的体积很大。实际上在 u-boot 中,全志公司提供的代码是直
接在 ROM 中运行的, u-boot 运行完大部分后会将 kernel 搬运到指定的 RAM 位置,之后将启动内
核,这个启动过程后面会详细说明。对于根文件系统(rootfs)而言,由于其执行过程需要对 ROM
行读写操作,因此可以不用搬运到 RAM 中,但是实际过程中内核启动后会产生一个虚拟的文件系统,
该文件系统是挂在根文件系统的关键所在,这里不详细讲解。整体来说,大致的过程为,嵌入式设备
上电后将执行 bootloader,对硬件进行硬件和堆栈初始化,然后搬运内核到 RAM 中并启动内核,紧
接着挂载根文件系统。
1-5 bootloader, parameter, kernel, rootfs 间的位置关系
2
官方发布网站:https://www.kernel.org/
linux 驱动开发指南 | 李山文
13
现在绝大多数 SoC 内部有一段固化的代码,该代码放在 bootloader 前,也就是 IROM故实际
上最开始执行的也并不是 bootloader而是芯片内部的 IROMIROM 的主要目的是方便芯片的操作,
例如通过 USB Flash 芯片进行烧写程序时,芯片执行的是正是 IROM 中的烧录代码,这段代码将
芯片设置为一个 USB 从设备,这样电脑可以将识别到芯片从而进行烧录。
1.2.1 环境搭建
因为我们是在 Linux 下完成所有的工作,因此我们最开始需要搭建开发环境,这包括 Linux 操作
系统的安装、基本组件库安装以及各种必要的工具安装。
1.2.1.1 虚拟机安装
对于大部分人而言,使用的都是 Windows 操作系统,但我们的开发需要在 Linux 下进行。为了能
够在 Window 下一样使 Linux,我们需要安装虚拟机软件,这样我们的开发都在虚拟机上进行。虚
拟机实际上就是一个应用软件,和其他的软件一样,它的主要目的是用软件模拟硬件指令,这样对于
其他平台而言,只需要能够将底层的指令用软件模拟,就能够运行其软件。现在的计算机都有支持硬
件虚拟化,这样可以加速硬件模拟过程。这里我们使用开源的 VisualBox 虚拟机来作为我们的 Linux
开发环境的运行平台,该虚拟机的最大好处是不断在更新中,同时其开源的优点使得我们不需要担心
版权问题。
首先我们在 VisualBox 官网上下载我们需要的虚拟机安装包,官网地址为:
https://www.virtualbox.org/在页面左侧点击 Downloads 按钮,进入下载页面,然后找到 Windows hosts
按钮,点击进入下载,此时会创建下载链接。默认下载的版本为最新版本,即 VirtualBox 6.1.30
1-4 VisualBox 载按钮
1-5 Visual Box Windows 机的下载按钮
linux 驱动开发指南 | 李山文
14
笔者发现最新版本的 Visual Box 存在小问题,因此推荐读者使用 Visual Box 6.0 版本,下载连接:
https://download.virtualbox.org/virtualbox/6.0.24/VirtualBox-6.0.24-139119-Win.exe 下载完毕后,安装
即可,然后接着我们需要下载 ubuntu 镜像,这里推荐使用 Ubuntu16 及以上版本。安装完毕后,打开
Visual Box 如下图所示:
1-6 Visual Box 6.0 虚拟机控制台
我们点击“新建”然后设置需要新建的虚拟机名称以及设置虚拟机安装的位置,如下图所示
我们新建一个 ubuntu16指定位置为 E 盘,然后点击“下一步”设置内存大小,这里我们设置 2G
读者可以根据自己的电脑内存适当增加即可,然后点击“下一步”选择“现在创建虚拟硬盘”最后
点击“新建”
1-7 设置新建虚拟机内存大小
然后我们选择“VDI,接着点击“下一步”,然后选择“动态分配”,再点击“下一步”,然后我
们设置硬盘大小,笔者这里设置 20G读者根据自己的实际硬盘大小适当增加即可,最后点击“创建”
需要注意的是,在选择安装路径的时候,最好选择一个空的盘,这个盘专门存放虚拟机的相关文
件,如果该盘有其他不相干的文件,可能会造成虚拟机出现莫名奇妙的错误。
linux 驱动开发指南 | 李山文
15
1-8 设置虚拟机硬盘大小
现在我们选中Ubuntu16然后点击“设置”因为我们之前没有指定我们的 Ubuntu 系统镜像,
这里我们在设置里面选择系统镜像,如下图所示,先点击左侧的“存储”,然后我们点击右侧的“分
配光驱”后面的选择按钮,然后点击“选择虚拟盘”。这时会弹出一个选择我们镜像文件的窗口,
到我们下载好的镜像,
1-10 选择镜像文件
选择好镜像文件后,我们点击左侧的“显示”,这里我们需要设置下显存大小,笔者这里直接设
置为 128M,然后我们再点击左侧的“网络”,设置连接方式为“桥接网卡”,然后在界面名称中选择
我们可用的网卡名称,笔者这里可用的网卡为 WiFi 网卡,读者根据自己的联网方式选择即可。然后
在混杂模式中选择“全部允许”。最后点击“OK”按钮即可。
1-11 设置显存大小及网卡
linux 驱动开发指南 | 李山文
16
设置完毕后,我们点击最上方的“启动“按钮,开启我们的虚拟机,第一次开启虚拟机我们会进
入虚拟机安装界面,选择“中文(简体),然后点击“安装 Ubuntu现在我们会进入安装界面,如
下图所示:
1-12 开始安 Ubuntu
接着我们选择“安装 Ubuntu 时下载更新”,然后点击“继续”,然后我们选择“清除整个磁盘并
安装”。这就是为何笔者希望读者在选择安装位置的时候最好选择空盘,因此读者需要非常小心,否
则将删除重要文件,资料无价。
1-13 开始安装
然后点击“现在安装”,此时我们需要选择时区,默认即可,然后我们设置用户名核密码,读者
根据个人喜好设置即可。安装完毕后我们重启虚拟机,然后点击回车键即可进入 Ubuntu 操作系统。
linux 驱动开发指南 | 李山文
17
1.2.1.2 交叉工具链安装
所有的嵌入式设备开发都会用到各种工具,由于每个嵌入式 SoC 的芯片架构不同,其开发工具
也不尽相同。当前用的最多的是 ARM本书中使用的 Lite200 主芯片的内核为 ARM9其架构使用的
ARMv5 架构。最主要的工具为交叉工具链,对于 F1C200S使用的交叉工具链必须高于 6.0 版本,
本书编译 u-boot kernel 使 7.2.1 版本,连接:
https://releases.linaro.org/components/toolchain/binaries/7.2-2017.11/arm-linux-gnueabi/gcc-linaro-7.2.1-
2017.11-x86_64_arm-linux-gnueabi.tar.xz温馨提示:下载速度慢可以使用迅雷等下载软件进行下载
下载完成后解压文件:
tar -vxjf gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi.tar.xz
然后在/usr/local 目录下新建 arm-linux-gcc 目录
sudo mkdir /usr/local/arm-linux-gcc
进入解压目录下:
cd gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabi/
将该目录下的所有文件复制到新建的目录下
sudo cp -rd * /usr/local/arm-linux-gcc/
最后需要添加该工具链的环境变量使其可以在任何目录下执行,打开/etc/profile 文件
sudo vim /etc/profile
在文件末尾 添加以下内容
PATH=$PATH:/usr/local/arm-linux-gcc/bin
添加完毕,使路径生效
source /etc/profile
说明arm-linux-gnueabi arm-linux-gnueabihf 的区别在于前者可以编译无浮点运算单元的 SoC
片,而后者只能只能编译带浮点运算单元的芯片,由于全志 F1C200S 内部没有浮点运算单元,因
此这里必须安装前者。对于全志 V3s 或者 H3 芯片,其内部有浮点运算单元,故应该安装后者。
上述也可以修改/etc/environment 文件,在 PATH 值追加 :/usr/local/arm-linux-gcc/bin"
执行 source /etc/profile 仅仅对当前目录操作有用,当换一个目录时,仍会提示找不到交叉工具链,
因此最好的还是关闭系统,重启即可。
下面验证交叉工具链是否成功安装,在任何目录中输入 arm-linux-,然后连按两次 Tab 键,如果
出现补全交叉工具链名称,则说明安装成功,如下图所示:
linux 驱动开发指南 | 李山文
18
1-6 交叉工具链安装成功
如果没有出现,且安装步骤确认没有问题,则进行下面操作:
安装必要的动态链接库
sudo apt-get install lib32ncurses5 lib32z1
上面安装的动态链接库主要是因为 ubuntu 64 位的操作系统,而交叉工具链是 32 位的,因此我们
需要安装 32 位必须的一些动态链接库。为了后续移植 Linux 系统中出现库缺失问题,这里可以一次
性将可能需要的库全部安装,执行如下命令即可。
sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl
zlib1g-dev libc6-dev lib32ncurses5-dev gcc-multilib x11proto-core-dev libx11-dev
lib32z1-dev libgl1-mesa-dev g++-multilib tofrodos python-markdown libxml2-utils libssl-
dev swig python-dev
1.2.2 u-boot 移植
一般来说 u-boot 是启动内核的关键所在,当前大部分使用的嵌入式设备都是 u-boot最新版本的
u-boot 几乎包含当前主流的 SoC 芯片,Lite200 使用的芯片和 licheePI nano 相同,大部分硬件也是兼
容的,为了快速移植该部分,这里采用 licheePI nano u-boot 来进行移植。在终端输入如下命令克隆
u-boot
git clone https://github.com/Lichee-Pi/u-boot.git -b nano-v2018.01
克隆完毕文件会保存在当前目录下,进入该目录
cd u-boot
在该文件夹下有很多分支,我们可以查看所有分支,使用如下命令:
git branch -a
现在我们使用的是 nano 开发板,所以将当前分支切换到 nano 分支,命令如下:
git checkout nano-v2018.01
u-boot 默认的没有指定交叉工具链和架构,因此在编译之前需要指定交叉工具链和芯片架构,u-
boot 的交叉编译器在 u-boot 的根目录下中的 Makefile 文件中定义了。打开 Makefile 文件:
linux 驱动开发指南 | 李山文
19
vim Makefile
找到 CROSS_COMPILE 变量,将其改为如下:
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabi-
如下图所示:
1-7 指定交叉工具链
configs 目录下是板级配置文件,由于每个板子的外设不同,因此编译之前必须要对 u-boot 进行
配置。然而配置是一件比较繁琐的事情,特别是像 u-boot 这种比较复杂的项目而言,初学者几乎无法
完成。幸运的是对于大部分开发板而言,configs 目录下有其配置好的默认配置文件。 进入 configs
录中,然后执行 ls 查看当前所有的配置文件
cd configs/
ls
找到 licheepi_nano_defconfig licheepi_nano_spiflash_defconfig前者表示为 TF 卡启动,后者表
示从 SPI 设备启动,这里使用前者即可,如图下图所示:
1-8 licheePI nano 默认配置文件
现在回到上级目录,然后执行 make licheepi_nano_defconfig
cd ..
make licheepi_nano_defconfig
执行后如下图所示:
linux 驱动开发指南 | 李山文
20
1-9 执行默认配置文件
配置完成后就可以进入图形界面进行配置了,执行 make menuconfig 命令:
make menuconfig
此时出现图形配置选项,如图
1-10 menuconfig 图像配置界面
我们可以通过键盘上的上下键来移动光标,使用“Y”和“N”以及“M”键来选择配置驱动。Y
确认勾选,表示将该模块编译进内核,N”取消勾选,表示将该模块不编译进内核,M”表示编译
该模块,但不编译进内核,即开发者可以自己手动加载这个模块到内核中。图形配置选项中有两个非
常重要的参数配置--传参,下小结重点讲解该部分。
1.2.2.1 启动命令和传参
在移植 uboot 的过程中我们会接触到很多环境变量,其中 bootcmd bootargs 这两个环境变量可
以说是所有环境变量中最重要的变量,读者看完之后就会有体会,这两个在内核启动过程中起着决定
性作用。
启动参数 bootcmd
前面说过,在系统还未启动之前,系统镜像文件都存放在 FlashTF 卡或者 eMMC)中,在 SoC
上,操作系统的代码会全部加载到内存中运行,不会在 Flash 中直接运行。其中一个原因是当前的 Flash
都是 NAND Flash,其不能直接寻址;另一个原因是 Flash 的运行速度很慢,不管是读还是写都远远
小于 RAM因此我们的首要工作是将 Linux 操作系统从 Flash 复制到 RAM 中,这个必须在 Linux
linux 驱动开发指南 | 李山文
21
动之前完成。这个复制的工作也是 uboot 的主要任务之一,也是最重要的任务之一,当然 uboot 还需
要完成硬件的初始化和参数传递,那么 uboot 是如何完成将 Linux 统镜像复制到 RAM 中的呢?下
面我们将详细讲解。
uboot 实现了很多命令来操作 Flash,例如对于 eMMC 而言,我们可以使用 mmc 命令进行读写;
对于 SPI Flash 而言,我们可以使用 sf 命令进行读写。我们先来看下 F1C200 bootcmd 启动参数:
load mmc 0:1 0x80008000 zImage; load mmc 0:1 0x80c08000 suniv-f1c100s-licheepi-nano.dt
b; bootz 0x80008000 - 0x80c08000
上面是 F1C200 的启动命令,可以看到实际上有三个步骤,分别是:
load mmc 0:1 0x80008000 zImage
load mmc 0:1 0x80c08000 suniv-f1c100s-licheepi-nano.dtb
bootz 0x80008000 - 0x80c08000
我们分别对这三个参数进行详细说明。首先,load mmc 命令是将 emmc 中的数据加载到内存中,
例如上面的第一个是将 mmc0 的第一个分区中的 zImage 加载到内存中的 0x80008000 处。这里的 0:1
表示第 0 emmc 第一个分区,首先因为我们的开发板上只有一个 emmcTF 卡),uboot 在挂载
emmc 的时候会将该 emmc 编号为 0如果有两个 emmc那就会有两 emmc 号, emmc0 emmc1
0:1 中的 1 表示该 emmc 中的第一个分区,我们的 zImage 文件存放在 emmcTF 卡)的第一个分区
中。同理,第二个是将第一分区中的 suniv-f1c100s-licheepi-nano.dtb 设备树文件加载到 0x80c08000
存处,至于什么是设备树文件后面章节会深入讲解。第三个使用了 bootz 命令,这个命令是启动内核
的命令,其中带有两个参数,第一个参数为内核的存放位置,第二个参数为设备树的存放位置。对于
早期没有设备树的内核而言,使用的启动命令为 bootmbootm 命令是对没有使用设备树内核的镜像
启动命令,早期版本的内核没有引入设备树,因此对于早期的内核一般使用的是 bootm其命令格式
bootm+内核地址,比如 bootm 0x30008000意思是从 0x30008000 开始启动内核,启动内核的过
程其实是将 pc 指针指向该地址,这样处理器就会从该地址处运行代码。
注意:bootz 命令的格式是:bootz 空格 0x80008000 空格-空格 0x80c08000,注意-左右有空格。bootcmd
环境变量若要执行多条命令,则每个命令之间用“;”隔开。
下面我们来配置 uboot 中的 bootcmd 环境变量,首先我们进入 menuconfig 图形配置界面:
1-11 开启 bootcmd 命令
然后将光标移到 bootcmd value 处,点击 Enter 键,进入编辑模式,我们将 F1C200 bootcmd
命令参数填写其中:
linux 驱动开发指南 | 李山文
22
1-12 进入编 bootcmd,然后编辑该值
编辑完毕后,我们将光标移动到<ok>处,然后按下 Enter 按键,回答上一级,如下图所示:
1-13 bootcmd 命令参数
上述就是 bootcmd 的命令讲解,我们设置好了该环境变量后,uboot 就可以正常将内核搬移到指
定的内存中,但是想要内核正常启动,我们还需要设置 bootargs 环境变量。
传参 bootargs
bootargs 也是 u-boot 境变讲解
bootcmd 来完成,那接下来内核启动完毕后必须挂在根文件系统(rootfs)但是内核并不知道根文件系
统的具体位置,我们必须要告诉根文件的位置后内核才能将其挂载,这时就需要有 bootargs 变量。
变量的作用是告诉内核根文件系统的位置和属性以及必要的配置,下图就中的 parameter 的部分就是
我们这里将要将的 bootargs 环境变量的值,u-boot 会将 bootargs 的值(其实就是字符串)保存到事先
规定好的地址处,当内核启动成功后就会获取该地址处的字符串值,然后对其进行解析并做相关的内
核配置。这个过程看起来比较复杂,这里可以不用去了解细节,只需要知道 bootargs 是告诉内核根文
mmc 而言,一般采用分区来划分内存区域,下面以
F1C200S 常用的 bootargs 来讲解。
console=ttyS0,115200 panic=5 rootwait root=/dev/mmcblk0p2 earlyprintk rw
上述有很多参数,每个参数用空格隔开,所以上面有 6 个参数。其中 console=ttyS0,115200 表示
终端为 ttyS0 即串口 0,波特率为 115200panic=5 字面意思是恐慌, linux 内核恐慌,其实就是 linux
不知道怎么执行了,此时内核就需要做一些相关的处理,这里的 5 表示超时时间, Linux 卡住 5
后仍未成功就会执行 Linux 恐慌异常的一些操作。rootwait 该参数是告诉内核挂在文件系统之前需要
先加载相关驱动,这样做的目的是防止因 mmc 驱动还未加载就开始挂载驱动而导致文件系统挂载失
败,所以一般 bootargs 中都要加上这个参数。root=/dev/mmcblk0p2 表示根文件系统的位置在 mmc
0:2 分区处,/dev 是设备文件夹,内核在加载 mmc 中的时候就会在根文件系统中生成 mmcblk0p2
备文件,该设备文件其实就是 mmc 0:2 分区,这样内核对文件系统的读写操作方式本质上就是读
/dev/mmcblk0p2 该设备文件。earlyprintk 参数是指在内核加载的过程中打印输出信息,这样内核在
加载的时候终端就会输出相应的启动信息。rw 表示文件系统的操作属性,此处 rw 表示可读可写。
linux 驱动开发指南 | 李山文
23
bootargs 参数时将光标移动到 Enable bootarguments 处,点击键盘'Y'按键使其参数开启。
1-14 开启 bootargs 境变量
点击 Enter 键进入编辑模式,然后将我们的 bootargs 设置为上述的环境变量值,然后点击<ok>
如下图所示:
1-15 设置好 bootargs
1.2.2.2 编译
上面我们设置好了 bootcmd bootargs 环境变量后,我们将光标移到<Save>处, Enter 键保存,
然后退出 menuconfig 图形配置界面,然后我们在终端中执行 make -j4 编译 uboot
1-16 编译 uboot
说明:make -j4 后面的-j4 表示 4 个核心进行编译,若电脑的处理器是 2 核心,请使用 make -j2 进行
编译
编译完成后会在当前目录生成 u-boot-sunxi-with-spl.bin 烧录文件。如下图,该文件就是我们最终
要烧录的二进制文件。在当前目录下会有一个隐藏的文件.config该文件是 u-boot 编译后根据各个选
项产生的配置文件,这个配置文件记录了所有配置选项的宏开关,编译的时候是根据最终的.config
件来进行编译的,当然编译前是需要有脚本解析.config 文件然后进行相应的编译。
linux 驱动开发指南 | 李山文
24
关于 u-boot 的配置过程这里简单说明下,其文件结构的和 linux 内核结构大致相同,对于当前而言,
其配置方式有三种,分别是
make menuconfig
make xxx_defconfig
.config
这三种方式都可以对内核进行配置,三者之间的关系比作去饭店吃饭,make menuconfig 以看作菜
单,.config make xxx\_defconfig 是客人点的菜,但是其三个文件之间可能因为存在依赖关系,如果
直接修改.config 可能会导致配置失效。
1-17 uboot 最终编译的二进制烧录文件
只要将 u-boot-sunxi-with-spl.bin 烧录到 tf 卡的 8k 偏移处地址就可以了,使用 dd 命令进行块搬移
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8
if 指的是输入文件,of 指的是输出文件,这里的输出文件为主机电脑的/dev/sdb 文件,也就是 TF
卡,这个可以用 gparted 软件查看,该软件可以直接用命令安装即可:sudo apt-get install gparted,安
装好后打开该软件,如图\ref{fig:gparted}在右上角可以看到两个硬 sda,其中一个是主机的本地硬
盘,另一个是 TF,即 sdb 这里可以看到 TF 卡的卷标名为 sdb,因此这里的 of=/dev/sdb 录到 8k
移地址处是指绝对地址,这个绝对地址指的是 TF 卡的物理地址。这里为何是 8k 处而不是其他地址
是由于 F1C200S 内部的 IROM 中的一小段代码决定的。
烧录完毕后如图:
1-18 使用 dd 命令烧录 uboot TF 卡中
现在这个 TF 卡内的 u-boot 可以启动内核了。 TF 卡插入到开发板上,然后通过 USB 连接到电
脑端,打开串口工具,可以看到按下开发板上的复位按钮,可以看到此时串口终端有信息输出,如下
图由于没有烧录内核,因此加载内核失败,停止在了 u-boot 命令终端处。
此时 uboot 会自动进入命令输入状态,我们可以输入 pri 来打印所有的环境变量,可以看到,此
时的 bootcmd bootargs 的值是我们刚设置的值。
1-19 pri 打印环境变量
linux 驱动开发指南 | 李山文
25
1-19 uboot 启动
至此,uboot 移植结束,下面我们要进行 kernel 的移植。
1.2.3 kernel 移植
内核移植相对与 u-boot 移植复杂些,对于 F1C200S 而言,Linux 官方源码已经对 licheepi nano
行了支持,因此本次移植采用 licheepi nano 的配置文件,下面以 linux5.7 版本内核来讲解 kernel 移植
步骤。进入 linux 内核官网 https://www.kernel.org 点击 https://www.kernel.org/pub/进入下载界面,如图
所示:
1-20 Linux 源码下载地
下载连接为 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.7.1.tar.gz,可以使用下载工
具进行下载,下载后的源码包通过 FTP 传输到虚拟机。上传完毕后进入虚拟机解压源码, u-boot
样,在编译时必须指定交叉编译器。
打开主目录下的 Makefile 文件,然后找到 CROSS_COMPILE 变量,将其修改为读者使用的交叉
工具链前缀,同时指定架构即 ARCH 变量,这里使用 arm-linux-gnueabi-为例,如图所示:
linux 驱动开发指南 | 李山文
26
1-21 指定交叉工具链
u-boot 步骤一样,在编译前必须对源码进行配置。进入该源码中的 arch/arm/configs 目录中,
可以看到有很多开发板的配置文件,其中 sunxi_defconfig 是全志的配置文件,但是该配置文件非常不
使 licheepi_nano
https://github.com/LiShanwenGit/MelonPI-MINI/tree/master/software linux-
licheepi_nano_defconfig 文件,这个文件是针对 licheepi_nano 的配置文件。这个配置文件与 Lite200
全兼容,后面会详细分析该文件以及一些配置,这里为了快速启动内核就不做过多阐述。下载该文件,
然后将其放到 arch/arm/configs/目录下,如下图所示,然后回到源码主目录。
1-22 linux-licheepi_nano_defconfig 件复制到 arch/arm/configs/目录下
在终端中执行如下命令:
make linux-licheepi_nano_defconfig
此时我们的内核配置就完成了,看似很简单,这是由于该文件做了非常多的工作。执行完之后我
们还需要进行图形配置界面,其目的是在源码主目录下生成一个.config 配置文件。执行下面命令:
make menuconfig
进入图形配置界面,如下图所示:
linux 驱动开发指南 | 李山文
27
1-22 内核 menuconfig 界面
可以看到其配置模式和 u-boot 近似相同,也是通过上下键左右来操作和[Y][N]键来选择是否编译
进内核。这里简单的先让 linux 内核跑起来,因此使用默认配置,不做任何修改。选择[Save]然后退
出,在终端下输入:
make -j4
1-23 编译 Linux 内核
zImage 文件和 dtb 文件,zImage arch/arm/boot dtb
arch/arm/boot/dts 目录下。
linux 驱动开发指南 | 李山文
28
1-24 zImage 文件和 dtb 文件
镜像编译完毕后就需要将其烧录到 TF 中,从上面的 u-boot bootcmd 可以看到需要
zImage dtb 文件复制到 TF 卡的 0:1 分区中,这样 u-boot 在执行 bootcmd 中的 load mmc 命令时就
可以找到 zImage dtb 文件。下面来对 TF 卡进行分区。分区工具最常用的是 gparted 软件,该软件
可以直接在终端中安装即可。执行如下命令安装 gparted 软件:
sudo apt-get install gparted
安装完毕之后,打开该软件,然后将 TF 卡插入到脑上由于使用的虚拟机,我们需要
VisualBox 的下方点击 USB 设备,然后勾选 Generic Mass Stotage Device[0100]。这样我们的 Tf 卡就
会挂载到我们的虚拟机上。
1-25 TF 卡挂载到虚拟机上
打开 gparted 该软件,可以看到此时有两个硬盘,一个是 sda 另一个是 sdb其中 sdb 就是 TF 卡。
如下图所示。选中 sdb可以看到有一个未分配的空间,一般对于嵌入式系统而言需要将其分两个区,
一个是存放 zImage dtb 文件,另一个区存放根文件系统。对于第一个分区,一般格式为 fat16 格式,
对于第二个区,一般为 ext4 格式。下面开始分区操作。
linux 驱动开发指南 | 李山文
29
1-26 gparted 分区软件界面
1-27 选择/dev/sdb 盘,即我们的 TF
选中未分配空间并右击鼠标,点击[新建],然后填写相关属性,如下图所示。我们将 sdb 前面
预留 1MB 大小的空间,这个空间留给 uboot 使用。在新建大小中填写 10MB这个分区用来存放 zImage
文件和 dtb 文件,一般来说,10MB 已经够用了,当然读者也可以分配 8MB 也是没问题的。然后在文
件系统中选择 fat16 格式,我们这里必须初始化 fat16 格式,这个是因为 uboot 中的 bootcmd 参数使用
的是 FAT 的分区表格式。在卷标中我们填写该分区的名称,这里笔者填写 boot,这个名称一般随意
填写都可以。然后点击[添加]按钮,此时就会将分区的任务记录下载。
linux 驱动开发指南 | 李山文
30
1-28 TF 创建第一个分区
然后我们以相同的方式创建第二个分区,选中未分配空间并右击鼠标,点击[新建],然后填写相
关属性。如下图所示:
1-29 TF 卡创建第二个分区
linux 驱动开发指南 | 李山文
31
第二个分区为根文件系统分区,我们在“之前的空余空间”填写 0因为这里紧接着第一个分区,
不留空余。然后在文件系统中选择 ext4 格式文件系统,因为 Linux 内核默认支持的是该格式的文件
系统。在卷标中填写 rootfs,然后点击[添加]按钮。如下图所示:
1-30 点击确认分区,进行分区操作
1-31 分区完成后的样子
linux 驱动开发指南 | 李山文
32
最后点击上图中的“红勾”确认分区并进行分区操作,等待一段时间后分区完成。分区完之后,
我们就可以将我们的 zImage 文件和 dtb 文件下载到第一个分区中了,此时复制可以直接通过图形操
作即可,也可以通过命令复制。将复制好内核的 TF 卡插到 Lite200 上接上 USB 和串口即可看到终端
有信息输出,如图所示:
1-32 Linux 内核成功启
可以看到内核已经成功启动,不过最终内核卡住了,其原因在于没有根文件系统,下节开始移植
根文件系统。上面仅仅是简单的启动了 Linux 内核,实际还有很多文件需要分析,但这里作为简单的
了解大致的过程,故不做深入讲解,后面分析驱动的时候将会详细讲解内核的文件结构和设备树相关
的驱动以及如何添加和修改驱动。
1.2.4 rootfs 移植
根文件系统是内核启动后挂载的第一个文件系统,如果没有根文件系统,内核将无法开启 shell
以及其他进程,下面来开始移植根文件系统。
1-33 buildroot 官网
linux 驱动开发指南 | 李山文
33
实际上内核启动后会先挂载一个虚拟的文件系统,这个虚拟文件系统是在内存中运行的,其主要运
行核心进程,虚拟文件系统挂载之后才挂载硬盘(TF 卡或者 emmc)上的根文件系统。
制作根文件系统的工具最有名的莫过于 busyBox该工具体积非常小,非常适合制作根文件系统。
但是笔者尝试过使用 busyBox 制作,然而体积较大,这主要原因在于文件系统需要有动态库和静态
库,而对于 7.2 版本的交叉编译器而言,其动态和静态链接库实在太大,因此本文将使用另一个非常
强悍的工具--Buildroot,该工具集成了非常多的其他应用,制作过程相对简单,不会像 busyBox 那样
出现硬件架构不兼容情况。由于根文件系统制作比较简单,这里就完全从头开始。进入 buildroot 官网
https://buildroot.org/downloads/下面以 buildroot2018.2.11 版本作为移植示例。将下载的源码包上传到
虚拟机上,然后解压进入该源码目录中。进入源码目录后在终端输入:
make clean
在开始编译之前必须执行 make clean 以清楚一些预设配置,即使是第一次编译也是一样。然后执
:
make menuconfig
此时会进入图形配置界面,如下图所示。进入第一个 Target options 选项,配置如图:
1-34 buildroot 形配置界面
第一个选项为架构选择,这里选择 ARM 架构小端模式,第二个为输出的二进制文件格式,这里
选择 EFL 格式,第三个为架构体系,这里选择 arm926t因为 F1C100S 的架构就是这个架构,第四个
为矢量浮点处理器,这里不勾选,因为对于 F1C100S 而言,其内部没有浮点运算单元,只能进行软浮
点运算,也就是模拟浮点预运算。第五个为应用程序二进制接口,这里选择 EABI,原因是该格式支
持软件浮点和硬件实现浮点功能混用。第六个为浮点运算规则,这里使用软件浮点,第七个选择指令
集,这里选择 ARM 指令集,因为 thumb 主要针对 Cortex M 系列而言的,对于运行操作系统的 A
列以及 ARM9 ARM11 而言,使用的都是 32 位的 ARM 指令集。
保存后,回到上一级配置界面,然后进入第二个 Build options 选项,配置如下图所示。这里简单
说明下配置,第一个($(CONFIG_DIR)/defconfig) Location to save buildroot config 指定了使用的默认配
置文件是哪个,实际上 buildroot 使用的是 Busybox我们可以在源码中找到 busybox 的身影。第二个
($(TOPDIR)/dl)/Download dir 指定了下载的目录,buildroot 在编译的时候会下载非常多的组件,其中
包括交叉工具链,这些组件会下载到该目录中,我们可以在这个目录中找到下载的东西。图中紫色框
中指定了编译时使用的库类型,我们这里选择(both static and shared)选项,即同时使用静态库和动态
库。剩下的我们使用默认的即可,将光标移动到<Save>上,然后按 Enter 保存后退出。
linux 驱动开发指南 | 李山文
34
1-35 Build options 选项
保存后,回到上一级配置界面,然后进入第三个 Toolchain 选项,配置如图:
1-36 Toolchain 选项
这里是选择交叉工具链,对于初学者而言,好选择 buildroot 指定的默认交叉工具链,因为这里
涉及到 C 库问题,如果使用自己安装的交叉工具链,编译很可能会报错,因为使用的 C 库不匹配。
linux 驱动开发指南 | 李山文
35
黄色框中的选项尽可能勾选,因为后面移植 QT5 的时候需要用到 C++相关库,如果这里没有勾选,
QT5 选项将无法勾选。
保存后回到上一级配置界面,然后进入第四个 System configuration 选项,配置如图:
1-37 System configuration 选项
第一个红色框中表示启动根文件系统后输出的信息(横幅)这里默认,当然你也可以修改此值,
比如改为 Welcome to Lite200,第二个红色框中表示开启登录密码,可以看到默认密码为空,这里就
默认了,读者也可以根据自己情况修改即可。
保存后回到上一级配置界面,可以看到后面还有部分选项没有配置,由于剩下的选项也可以不用
配置,因此这里为了简便,直接保持推出,然后执行:
make
这里最好不要用多核编译,一般来说,首次编译过程非常慢,因为要下载很多必要的文件和交叉
工具链。
编译完毕后可以在 output/images 目录下有一个 rootfs.tar,该文件就是最终生成的根文件系统镜
像,现在只需要将该镜像解压到 TF 卡的第二分区即可。插入 TF 卡到电脑端,进入 out/images 目录,
然后输入:
sudo tar -xvf rootfs.tar -C /media/lsw/rootfs/
此时可以看到 TF 卡的 rootfs 分区中有文件系统了,现在将 TF 卡弹出,插入到 Lite200 上,连接
好串口,打开串口助手或者其他串口终端软件,可以看到根文件系统成功挂载,同时进入 shell 交互,
如下图所示。用户名默认为 root无密码,在终端中直接输入 root 后直接进入 root 用户空间。可以看
到文件系统中的 linux 的根目录情况,到此根文件系统的移植完成。
linux 驱动开发指南 | 李山文
36
1-38 根文件系统成功挂载并进入 shell 界面
1.2.5 系统镜像打包
首先我们回顾下,上面的过程中,我们对 TF 卡进行了分区,那为何要分区呢?分区到底对 TF
做了什么操作呢?实际上分区的本质是对硬盘进行了文件系统格式化。也就是我们的分区操作本质上
会在硬盘上创建一个文件系统,这样硬盘上就会产生一个分区表,这个分区表记录着分区的物理地址
位置。uboot 在挂载 TF 卡之后就会获取到这个分区表,这样 uboot 在执行 bootcmd 环境变量中的命令
时,此时 load mmc 就可以知道 0:1 的物理地址,从而找到 zImage 文件和 dtb 文件的绝对地址。
用过 FATFS 文件系统的读者应该对分区表比较熟悉,下面是 FAT32 的分区格式:
1-39 FAT32 分区表
linux 驱动开发指南 | 李山文
37
我们的 TF 卡的第一个分区会被初始化为 FAT16 格式,其分区和上图类似。如下图所示,TF
被分区后,在 TF 卡的最开始 0 地址处会有一个分区表,这个分区表是 gparted 工具创建的,这也是
为什么我们的 Uboot 需要放在 8k 偏移处的原因,至于为何定义 8k 而不是 1M 或者其他大小,这个取
决于全志芯片内部的 IROM Boot 代码了(这段很小的代码固化在芯片内部,使用掩膜工艺制作)
1-40 TF 卡分区表
对于产品量产来说,一般会将打包好的镜像文件烧录到 eMMC 中,那么我们如何将上面的 uboot
kernelrootfs 打包成一.img 文件呢?上面我们知道了分区的作用以及大致的原理,实际上.img
件就是一个包含了分区表的组合文件。读者可以做一个实验,将我们可以启动内核的 TF 卡原封不动
的拷贝到另一个卡中或者一个虚拟的文件中,那么这个时候这个虚拟的文件正好就是我们的 TF 卡大
的分区样子。要实现 TF 卡的拷贝不得不提 dd 这个命令,在 Linux 中,dddevice driver)命令为设
备拷贝命令,这个命令异常的强大,可以拷贝一切文件,首先我们先来了解下 dd 命令的使用。
dd 命令有很多参数,下面是常用的参数:
if=文件名:输入文件名,缺省为标准输入。即指定源文件。< if=input file >
of=文件名:输出文件名,缺省为标准输出。即指定目的文件。< of=output file >
ibs=bytes:一次读入 bytes 个字节,即指定一个块大小为 bytes 个字节。
obs=bytes:一次输出 bytes 个字节,即指定一个块大小为 bytes 个字节。
bs=bytes:同时设置读入/输出的块大小为 bytes 个字节。
cbs=bytes:一次转换 bytes 个字节,即指定转换缓冲区大小。
skip=blocks:从输入文件开头跳过 blocks 个块后再开始复制。
seek=blocks:从输出文件开头跳过 blocks 个块后再开始复制。
count=blocks:仅拷贝 blocks 个块,块大小等于 ibs 指定的字节数。
conv=conversion:用指定的参数转换文件。
ascii:转换 ebcdic ascii
ebcdic:转换 ascii ebcdic
ibm:转换 ascii alternate ebcdic
block:把每一行转换为长度为 cbs,不足部分用空格填充
unblock:使每一行的长度都为 cbs,不足部分用空格填充
lcase:把大写字符转换为小写字符
ucase:把小写字符转换为大写字符
swab:交换输入的每对字节
noerror:出错时不停止
notrunc:不截短输出文件
sync:将每个输入块填充到 ibs 个字节,不足部分用空(NUL)字符补齐。
上面的参数看着很多,实际我们可能指用到部分参数,例如我们需要将一个硬盘数据拷贝到另一
linux 驱动开发指南 | 李山文
38
个硬盘,我们可以使用如下命令:
sudo dd if=/dev/sdb of=/dev/sdc
上面这个命令实现了将 sdb 硬盘数据整个直接拷贝到 sdc需要注意的是每个硬盘上都有分区表,
这个也会将分区表一起拷贝过去,从而实现硬盘的备份。
还记得我们之前将 uboot 烧录到 TF 卡中的命令么?这里再回顾下:
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8
上述命令的 if 表示输入文件,显然我们的数据文件就是将要烧录到 TF 卡中的文件,即 uboot
标文件;of 文件表示输出文件,我们的 TF 卡就是 sdbbs 表示块的大小为 1kByte然后 seek 表示需
要偏移的 bs 数量,这里的 8 表述 8 bs,即 8kByte
现在我们需要将 ubootkernelrootfs 这三个打包成一个文件,读者试想一下,我们的输出文件
可以是 TF 卡,那是否可以是一个虚拟的文件,我们将这个文件看作是完整的镜像不就可以了么?对
的,我们将镜像打包的实现就是这个原理,因此我们需要创建一个虚拟文件或者叫空文件,这个文件
我们取名为 Lite200.img。首先我们新建这个文件,新建一个固定大小的文件需要使用 dd 命令,如下
所示:
sudo dd if=/dev/zero of=Lite200.img bs=1M count=256
上面这个命令实现了创建一个 256MByte 的空文件,如下图所示:
1-41 创建一个打包镜像文件 Lite200.img
可以看到我们的空文件已经创建成功,其大小正好是我们想要创建的大小,现在我们就可以将这
个文件当作 TF 卡向里面烧录文件了。
注意:/dev 目录下有两个特殊文件,一个是 null一个是 zero null 可以理解为黑洞,只要将
文件丢到该文件中,就会永久删除,null 文件只能写入。 zero 文件与 null 文件正好相反,这个文件
是只读文件,用来初始化 0
现在我们先将 uboot 烧录到这个空文件中,执行如下命令:
sudo dd if=u-boot-sunxi-with-spl.bin of=Lite200.img bs=1024 seek=8
1-42 uboot 烧录到 Lite200.img 件中
linux 驱动开发指南 | 李山文
39
下面我们接着需要创建 BOOT 分区和 rootfs 分区,其中 BOOT 的文件系统格式为 FAT16,而 rootfs
的文件格式为 ext4 式,为了方便分区,我们分别创建两个文件:Lite200.img1 Lite200.img2。我
们的实现过程如下图所示:
1-43 img 组成部分
我们的 Img 由三部分组成,首先是前面预留的 1M 空间,作为 uboot 和分区表的空间,这里的 8k
偏移地址是全志 SoC IROM boot 规定的地址,然后紧接着是两个分区。我们的 256M 镜像打包过
程大致如下:
1. 制作 256M 的空文件并烧录 ubootuboot 和分区表空间)
2. 制作 BOOT 分区
3. 制作 rootfs 分区
4. 创建分区表并写入
1.2.5.1 制作 BOOT 分区
上面我们已经创建了一个 256M Lite200.img 文件,同时写入了 uboot 8k 地址处,由于还没
有制作分区,我们先不创建分区表,下面我们来创建一个 BOOT 分区。由于我们的是虚拟文件,但这
个文件本质上是 Ubuntu 的扩展分区,而不是主分区,而我们的最终镜像中的 BOOT rootfs 分区都
是主分区,因此我们并不能像 TF 一样直接用 gparted 进行分区,只能单独分区,然后合并到一起。
首先我们创建一个 Lite200.img1 文件,这个文件作为 BOOT 的分区。和前面一样,我们使用 dd
命令创建 Lite200.img1 空文件,大小为 32MiB
3
(后面的所有 M 都是指 MiB
sudo dd if=/dev/zero of=Lite200.img1 bs=1M count=32
1-44 创建 Lite200.img1 空文件
现在我们需要将这个空文件格式化为 vfat 格式,在 Linux 操作系统中有格式化工具 mkfs。执行
下面命令:
sudo mkfs.vfat -n BOOT Lite200.img1
3
MB MiB 的区别:MB 1000Byte=1M,而 MiB 1024Byte=1M
linux 驱动开发指南 | 李山文
40
1-45 格式化 Lite200.img1 vfat 格式
上面的命令中,-n 表示分区的名字,我们这里设置分区卷标名为 BOOT然后我们将这个文件系
统挂载到 mnt 目录下,执行如下命令:
sudo mount -t vfat Lite200.img1 /mnt/
1-46 挂载 Lite200.img1 文件系统到/mnt 目录下
现在我们就可以向这个挂载的文件系统中写入我们的 zImage dtb 文件了。
sudo cp zImage /mnt/
sudo cp sun8i-v3s-licheepi-zero.dtb /mnt/
1-47 zImage 件和 dtb 文件复制到/mnt 目录下(此时/mnt 目录就是 BOOT 分区)
现在我们的 BOOT 分区已经做好了,此时 BOOT 仍然挂载在 ubuntu 上面,我们现在卸载下来,
然后写入到 Lite200.img 中,执行下面命令:
sudo umount /mnt
sudo dd if=Lite200.img1 of=Lite200.img bs=1M seek=1
1-48 BOOT 分区写入到 Lite200.img
1.2.5.2 制作 rootfs 分区
下面我们以同样的方式制作 rootfs 分区,执行如下命令,创建一个 Lite200.img2 空文件:
sudo dd if=/dev/zero of=Lite200.img2 bs=1M count=223
然后我们格式化这个文件为 ext4 格式,如下:
sudo mkfs.ext4 -F -b 4096 -E stride=2,stripe-width=1024 -L rootfs Lite200.img2
现在我们将这个文件挂载在/mnt 目录下面,然后将根文件系统放进去,如下所示:
sudo mount -t ext4 Lite200.img2 /mnt/
sudo tar -xvf rootfs.tar -C /mnt/
linux 驱动开发指南 | 李山文
41
现在这个文件已经写入了根文件系统,此时 rootfs 仍然挂载在/mnt 下,我们现在卸载它,然后写
入到 Lite200.img 中,如下所示:
sudo umount /mnt
sudo dd if=Lite200.img2 of=Lite200.img bs=1M seek=33
1-49 rootfs 分区写入 Lite200.img 件中
1.2.5.3 添加分区表
上面我们的 BOOT 分区和 rootfs 分区已经做好了,剩下的就是添加分区表了,分区表非常重要,
如果没有分区表,操作系统是找不到各个分区的,更谈不上从分区中拷贝文件了。下面我们来添加分
区表,新建分区表工具我们使用 fdisk 命令。
首先执行如下命令:
sudo fdisk Lite200.img
1-50 fdisk 命令创建分区表
此时我们需要输入命令来创建不太类型的分区,现在我们输入 o,回车:
1-51 输入 o 来创建 DOS 分区表
然后我们输入 n 来新建分区,再输入 p 确定分区为主分区,然后输入 1 表示第一个分区,如下:
1-52 创建分 1
linux 驱动开发指南 | 李山文
42
现在我们需要指定分区的物理地址范围了,到这里就比较重要了。首先我们需要知道什么是扇区,
所谓扇区是机械硬盘中的叫法(在固态硬盘中习惯叫 lba即逻辑块地址,也有些人仍然叫扇区)
个扇区是一个最基本的写入单位,即最小的写入数据量。我们的 TF 卡一般默认是 512Byte 作为一个
扇区单位,这样我们 1Mbyte 就是 2048 个扇区。好了,回归到操作上,现在终端要求输入扇区号,
2048~524287 之间,也就是对应着 1M~256M正好对应着我们的 Lite200.img 的大小,那为何只能
1M 开始呢?这是由于文件系统规定,前面 1M 的位置作为分区表,然而大部分的分区表只有一个
扇区大小或者几个扇区大小,所以我们的 uboot 放在 8k 处是没有问题的。
现在我们输入 2048表示从 1M 地址处开始分区,然后输入+32M表示分区大小为 32M如图
所示:
1-53 完成第一分区表
接着我们创建第二个分区的分区表,我们以同样的方式创建第二个分区,在终端中接着输 n表示新建一个分区
然后输入 p,表示主分区,再输 2,表示第二个分区,如下图:
1-53 开始第二个分区表
接着我们输入 67584 33M 作为第二个分区的起始位置,然后输入 524287 作为第二个分区的终止位置:
1-54 第二个分区完成
现在两个分区都是 DOS 格式,这个可不行,我们需要将第二个分区修改为 ext4 格式,现在我们
单独修改第二个分区格式,在终端中输入 t,表示修改分区类型,然后输入 2,表示现在选中分区 2
再输入 83,表示分区格式为 ext4,最后输入 w,将分区表写入到 Lite200.img 文件中:
说明:最开始 o 表示 DOS 分区格式,在老版本的 DOS 中,默认为 FAT16 格式,在新版的 DOS 中,
默认的是 FAT32 格式。这里的 83 表示里 Linux 挂载的根文件系统类型,一般默认为 ext4 格式。Linux
中提供了多种分区格式,详细请查看附录 B
linux 驱动开发指南 | 李山文
43
1-55 修改第二个分区类型并将分区表写入到 Lite200.img 镜像中
此时我们已经完成了分区表的添加工作,现在我们将 Lite200.img 文件烧录到 TF 卡中,下面我们
使用 Win32DiskImager 工具在 Windows 下测试下,感觉一切良好,但插上卡到开发板发现什么输出
也没有,这肯定是 uboot 丢了,是的,通过 winhex 工具查看确实是 uboot 丢了,为何会丢呢?实际上
我们在第一个分区的时候会提示 2048 开始,这个正好是 1M分区表默认保留了 1M当我们新建分
区表完成后写入到 Lite200.img 时,此时文件的前 1M 被完全覆盖了,这也导致 uboot 丢掉了,怎么
uboot 就可以了。但是需要非常注意的是,我们这次写入需要加
conv=notrun 参数,这个参数表示只替换写入的部分,其他保持不变。如下所示:
sudo dd if=u-boot-sunxi-with-spl.bin of=Lite200.img bs=1024 seek=8 conv=notrunc
1-56 重新写 uboot
现在我们使用 Win32DiskImager 工具在 Windows 烧录到 TF 卡中,插到开发板上,此时就可以看
到启动了,并成功进入 shell
1-57 打包后的镜像成功启动并进入 shell
至此,镜像打包部分我们已经完成了,当然也可以写成一个 shell 脚本直接执行,上面我们通过制作
一个 256M 的打包镜像来讲解镜像打包过程,读者也可以根据自己的 TF 卡大小制作属于自己的打包
镜像。
linux 驱动开发指南 | 李山文
44
1.3 硬件分离思
没有软件的硬件只是一块废铁,没有硬件的软件也只是一个空中楼阁。软件和硬件相互配。然而
事情总是没有那么美好,当系统变得非常复杂时,就如同需要建筑一栋高楼时,我们需要精心设计才
行,否则一切将变得不受控制。
为了让软件适应更多的平台,软件的设计时候需要将硬件相关的代码尽可能抽离出来,这也就出
现了软硬件分离思想, Linux 内核中软硬件分离思想尤其重要。下面我们将讲解如何实现软硬件分
离,并且在考虑资源和运行时间的尽可能最优的前提下实现代码的层次化。本章内容看似与 Linux
动开发无关,实则是 Linux 驱动设计的精髓所在。
1.3.1 对象编程概念
C++开发中面向对象编程是一件再正常不过的事情了,但对于 C 语言这种面向过程编程的语
言是否能实现面向对象编程呢?答案是肯定的, Linux 驱动中处处可见面向对象编程,因此在此之
前我们需要了解面向对象编程的原理以及如何实现面向对象编程。
首先我们需要明白什么是对象, C++中,对象是一个类的实例化,但这是一种局限性的对象概
念。拓开思维,在自然界中,对象可以是一件衣服,可以是一只猫咪也可以一个不存在的概念。
归根到底,对象是一个具有一定特征的形态(可以是实体,也可以是虚体)现在我们将一只猫定义
为一个对象,它具有体重、毛色、性格等特征,现在我们需要用 C 语言描述它,那如何描述呢?答案
是结构体,是的,结构体正是 C 语言中面向对象实现的基础。例如下面我们来描述这只猫,它有如下
特征:
体重:10kg
毛色:杂色
性格:狂躁
下面是 C 语言的描述:
typedef enum
{
red=0, //红色
green, //绿色
misc, //杂色
linux 驱动开发指南 | 李山文
45
}hair_clor_e;
typedef enum
{
mild=0, //温和
crazy, //狂躁
}character_e;
typedef struct cat
{
hair_clor_e hl; //毛色
character_e cht; //性格
int weight; //体重
}cat_t;
上面仅仅定义了一个猫的类(结构体),现在我们需要定义一个具体的猫(即对象)。如下所
示:
cat_t herry =
{
.hl = green,
.cht = crazy,
.w = 10,
};
上面我们定义了一只名为 herry 的小猫,没有接触过 Linux 编程的读者可能对这个代码不熟
悉,实际上这个代码是定义了一个 herry 的结构体变量,同时对其进行了初始化。在 Linux 中这
种初始化的方式非常常见,可以说遍地都是。C 语言中结构体的初始化有三种方式,上面便是其中
的一种。现在我们知道了如何用 C 语言定义一个对象了,但对象是需要方法的(操作接口)。在
C++中我们需要实现类的方法,这样实例化出来的对象就具备这下方法,但 C 语言是不允许像 C++
那样定义类的,那我们如何实现这种方法呢?答案是函数指针,是的,在 Linux 中所有的类中的方
法都是通过函数指针来实现的。现在我们看一个简单的例子:
typedef struct cat
{
hair_clor_e hl; //毛色
character_e cht; //性格
int weight; //体重
int (*eat)(struct cat *obj,struct food); //定义吃的方法
int (*sleep)(struct cat *obj,int time);//定义
}cat_t;
我们在上面的结构体中添加了两个函数指针,一个是 eat,一个是 sleep。读者是否会感到奇
怪,这里的函数指针为何添加 struct cat *obj 这个参数,是不是有些多余。初看可能确实是多
余,但是由于 C 语言没有 this 指针,因此我们在引用自身对象时是需要通过参数传进来的。下面
我们来实现其中一个读者自会明白,如下所示:
int eat(struct cat *obj,struct food)
{
if(food is mouse)
{
linux 驱动开发指南 | 李山文
46
obj->weight++;
return 0;
}
else
{
return -1;
}
}
可以看到,在函数中,使用的是 obj->weight 来访问其对象的成员,如果参数没有 struct
cat *obj,我们将无法访问成员。函数指针归根到底还是指针,因此我们在使用之前必须对其初始
化,这也就是构造函数,现在我们来实现一个构造函数:
cat_t *new_cat(void)
{
cat_t *new = (cat_t*)malloc(sizeof(cat_t));
new->eat = eat;
new->sleep = sleep;
return new;
}
是不是很简单,实际上我们使用了动态内存分配的方式,当然如果不想使用动态内存分配的方
式,我们也可以使用静态变量的方式,这样就变为了如下:
cat_t herry =
{
.hl = green,
.cht = crazy,
.w = 10,
};
new_cat(&herry);
这样我们就可以使用 herry 中的方法了,例如这样:
herry.eat(&herry, mouse);
上述过程目的是让读者明白一种面向对象的编程思想,即使是 C 语言这种面向过程的语言,也可
以写成面向对象化,实质上面向对象并不是一种语言特有,而是一种思维方式而已。
1.3.2 C 语言对象化编程
那我们如何在实际中实现面对象呢?例如在操作硬件上,我们能够使用面向对象的思维去实现我
们的驱动程序,而不是仅仅只停留在表面使用最直接粗暴的方式操作硬件。现在我们先来看一个例子:
例子:
现在我们需要操作两个 LED,这两个 LED 分别接在 GPIOA2 GPIOE3 上面,我们需要实现对
这两个 LED 的操作函数,其中包括点亮和熄灭这两个 LED
typedef struct led
{
int pin_num;
int status;
linux 驱动开发指南 | 李山文
47
void (*write)(struct led *obj, int status);
int (*read)(struct led *obj);
}led_t;
static void write(struct led *obj, int status)
{
obj->status = status;
GPIO_Write(obj->pin_num,status);
}
static int read(struct led *obj)
{
return obj->status = status;
}
void new_led(struct led *obj)
{
obj->read = read;
obj->write = write;
}
led_t led1 =
{
.pin_num = 2,
.status = 0,
};
led_t led2 =
{
.pin_num = 3,
.status = 0,
};
new_led(&led1); //new 一个 LED 对象
new_led(&led2); //new 一个 LED 对象
led1.write(&led1,0); //开启 LED1
这样做的目的是使硬件变得更加清晰,这样我们操作硬件就变为了操作这个对象,当硬件非常
复杂时,我们必须采用这种抽象的编程思想去设计,否则系统将变得异常的复杂,以至于几乎无法
控制了。当然上面仅仅只是一种面向对象的方法之一,还有很多其他的面向对象的编程方法,不但
大体上都是殊途同归。
1.3.3 件层次化设
我们知道了如何用 C 语言面向对象编程,但这还不够。当软件与硬件太过紧密时,硬件的更改使
得我们有时候不得不重构整个代码,这将会是一件灾难性的事情,因此优秀的软件一定是层次分明且
移植性良好的。首先,在设计软件框架首先需要清楚软件和硬件的关系,哪些硬件是不可改变的,
哪些硬件是可以改变的,这些都是软件架构设计必须考虑的问题。因此我们在设计软件时首先需要清
晰的划分硬件和软件之间的界限,同时把握硬件与软件之间的联系,这好比太远的距离失去了交流,
linux 驱动开发指南 | 李山文
48
太近的距离又失去了各自的空间,把握一个合适的度是嵌入式软件架构设计的难点之一。下面我们将
讲解如何实现软件的层次化设计。
对于单片机开发者而言,对简单直接的方法莫过于如下图左边的,这种设计的优点是结构简单,
执行速度快,直接操作寄存器,但其也有致命的缺点,那就是移植性几乎没有,当我们需要修改硬件
时,我们的驱动程序需要重构。
1-6 软件直接操作寄存器
但是我们可以添加一层接口层,这样我们的驱动程序将全部基于这个接口层来间接的操作寄存器,
这样即使硬件底层全部改变了,我们的驱动程序将不发生任何改变,其本质就是将驱动程序尽可能与
硬件剥离。如下图所示:
1-7 添加中间接口层
我们的驱动开发将全部基于这个接口层来完成,而与硬件没有关系,唯一与硬件关系紧密的便是
中间接口层。该层实现了对硬件的所有最基本的操作,这些操作由熟悉单片机或者 SoC 寄存器的人
员来开发。很显然这种架构设计可以极大的增加软件的可移植性,同时也使得软硬件分工明确,减少
错误的概率。
为了能够更深刻的理解这种思想,我们用一个简单的示例进行说明。
示例:利用单片机实现一 AT24C02 的驱动程序,单片机种类没有确定,可能带有 I2C 控制器,可能没
有带 I2C 控制器。
上面的难点不在 AT24C02 程序如何编写,而在于其单片机的种类没有确定,如果我们简单的实
现上面的功能,我们需要实现两套代码,一套使用 I2C 控制器的代码,另一套则使用 IO 拟的方式。
那是否有更好的方式,只需要实现一套驱动呢?答案是肯定的。
首先,我们的驱动程序必须脱离硬件,不能直接操作寄存器,而是使用中间接口,这个中间接口
实现最基本的读和写功能。由于中间接口与硬件关系紧密,因此我们需要将 I2C 控制器或者用 IO
拟出来的 I2C 抽象为一个标准的统一框架,根据 I2C 的特点,我们抽象出来的 I2C 控制器如下:
struct i2c_control
{
void (*init)(void);
int32_t (*read_reg)(uint32_t reg);
int8_t (*write_reg)(uint32_t reg, uint32_t value);
}i2c_control_t;
上面的结构规定了最基本的操作,有初始化操作、读寄存器操作和写寄存器操作。这些操作供上
层的驱动层使用,同时这些操作也规范了下层的 BSP 驱动接口。现在我们再来编写底层的 BSP 驱动,
linux 驱动开发指南 | 李山文
49
伪代码如下:
int32_t stm32_read_reg_sim(uint32_t reg)
{
uint32_t value;
...
...
if(wait_ack)
{
return value;
}
return -1;
}
int8_t stm32_write_reg_sim(uint32_t reg, uint32_t value)
{
...
}
int32_t stm32_read_reg_half(uint32_t reg)
{
...
}
int8_t stm32_write_reg_half(uint32_t reg, uint32_t value)
{
...
}
上面我们实现了两种方式的读写,一种是 IO 模拟的方式,另一种是硬件的方式,这两种方式都
是很底层的操作,我们称之为 BSP即板级支持包。中间接口层只提供了一种接口规范,因此我们需
要将中间接口层“重定向”到具体的操作中,例如我们如果使用 IO 口模拟,那么我们可以这样实现:
首先实例化一个中间接口层:
i2c_control my_i2c_sim =
{
.init = stm32__i2c_init_sim,
.read_reg = stm32_read_reg_sim,
.write_reg = stm32_write_reg_sim,
};
i2c_control my_i2c_half =
{
.init = stm32__i2c_init_half,
.read_reg = stm32_read_reg_half,
.write_reg = stm32_write_reg_half,
};
上面我们实例化了两个 i2c 对象,这两个对象中一个是用 IO 模拟方式,另一个是使用硬件控制
器,这样上层就可以直接使用这个实例化的 i2c 对象进行底层的操作。
linux 驱动开发指南 | 李山文
50
相对于之前直接操作寄存器的方式而言有了一点提高,但是这还远远不够,显然这里存在一个非
常严重的问题:当我们使用两个 IO 模拟方式时,此时底层如何实现呢?因为底层并不知道你使用的
引脚号,而这里的硬件控制器也存在问题,如果我们去使用两个控制器呢?那我们也同样不知道如何
初始化底层。因此可以看到,这里我们的结构体 struct i2c_control 远远不够全面,连最基本的都无
法实现,现在我们开始完善这个结构体:
typedef struct i2c_control_descp //i2c 控制器描述结构体
{
int8_t control_index; //指定使用哪个 i2c 控制器,如果为-1,则表示使用 io 模拟
int8_t data_width; //数据位宽,一般 8
uint32_t scl_pin; //如果使用 io 模拟,指定 scl 引脚,引脚 = 32*GPIO +偏移
uint32_t sda_pin; //如果使用 io 模拟,指定 sda 引脚
uint32_t speed_hz; //指定 I2C 时钟速率
}i2c_control_descp_t;
struct i2c_control
{
i2c_control_descp_t descp;
void (*init)(void);
int32_t (*read_reg)(uint32_t reg);
int8_t (*write_reg)(uint32_t reg, uint32_t value);
}i2c_control_t;
上面我们新增了一个控制器描述结构体,该结构体用于对控制器信息的一些描述,这样我们在实
例化 i2c 控制器对象的时候就知道我们的控制器信息,其中包括 i2c 引脚、i2c 是否使用模拟、哪个 i2c
控制器以及时钟速度。这些信息仅仅只是保存到对象中,其具体实现还是需要依赖于 BSP 的实现。
上面的框架虽然实现了可以定义多个对象,但对于用户(驱动开发者)而言还是过于复杂,因为很多
硬件资源其实已经被固定了,我们只需要指定使用哪个资源就可以了。这样整个架构就变为了如下图
所示:
1-8 新的驱动框架
上面的驱动框架将设备信息与驱动分离了,例如,对于 W25Q128 设备而言,它不知道需要使用
哪个 SPI 控制器,同时也不知道 W25Q128 设备的地址等信息。这个信息全部由 W25Q128 的另一个
设备信息来记录,这样实现了驱动与设备的分离,这种架构设计的目的是一个驱动可以兼容多个设备,
而不需要每个设备都写一个驱动。对于控制器而言,其硬件资源同样也有多个,我们是否可以将 BSP
驱动也分为设备信息和驱动呢?答案是肯定的,实际上 Linux 内核就是这样做的。这样我们的框架就
变为了如下所示:
linux 驱动开发指南 | 李山文
51
1-9 设备信息与驱动分
实际上这种结构正是 Linux 内核中早期版本的设计思想,但随着后来的不断发展,Linux 开始引
入了设备树,因此这种设备信息和硬件信息也将全部由设备树替代。
1.3.4 设备管理
随着计算机外部设备的不断增加,操作系统需要对设备进行合理的管理,那如何去管理这些设备
呢?学过数据结构的读者应该对链表比较了解,我们可以将设备都以节点的形式挂载在双向或者单项
链表中,这样我们需要哪个设备就可以直接通过该节点来进行访问,我们称这种节点为设备节点,
Linux 内核中也存在这种设备节点,同时由于 Linux 一切皆文件的思想,在内核中同时存在设备节点
文件,应用层访问设备节点文件就是在访问设备。
1-10 Linux 字符设备驱动管理结
linux 驱动开发指南 | 李山文
52
假设我们现在需要将各种设备驱动进行管理起来,那么我们就需要提供一个统一的设备管理方式,
当我们需要新增加一个驱动时,我们就需要将设备驱动的节点添加到链表中,这个过程我们称之为设
备注册,实际上设备注册远不止如此简单。为了让设备更加方便管理,我们还需要为每个设备编一个
号。在计算机操作系统中,设备作为一种公共资源,每个设备都有唯一的编号,对于同一种设备,
们将其归为一类,用主设备号表示,同一种设备下有不同的具体设备,我们用子设备号表示。例如我
们有三个 LED,这三个 LED 的主设备号为 10,次设备号为 012。在 Linux 内核中,设备号用了
一个 32 位的无符号整型来表示,主设备号占了高 12 位,次设备号占了剩下的 20 位。可以看到,理
论上 Linux 设备中主设备号最多能表示

=4096 种设备,次设备为

=1048576 个。实际上 Linux
核中没有使用这么多,对于字符设备而言,主设备最多只有 255
4
,而同一个主设备的次设备最多
支持 256 个。 Linux 中,字符设备管理使用了一个 cdev_map 的结构体指针来表示,最大空间为 255
Linux 符设备驱动如上图所示,它利用了一个 255 元素的结构体数组来作为设备映射管理的
hash 散列。这种管理方式在操作系统中极为常见,例如在 RTOS 中常常利用 Hash 来管理不同优先级
的任务控制块TCB其好处是可以快速查找到需要的对象。当一个设备删除时,我们需要在 Hash
中删除其映射的对象,同时释放设备所占用的资源,包括硬件资源和内存资源;添加设备也是一样的。
实际过程中,由于不同架构以及不同需求,Linux 的设备管理远不止如此简单,上面仅仅简述了非常
表层的结构,内部的结构还需读者自己阅读和分析源码才能深有体会。
1.4 用程序编译
在驱动开发过程中有时候需要编写简单的应用测试程序,因此这里对如何编写应用程序做一个简
单的讲解。首先应用程序不像驱动程序,应用程序一般是动态编译,因此应用程序大部分需要动态链
接库的支持。而我们在编译根文件系统的时候使用的时 buildroot 的交叉工具链,因此我们在编写应
用程序时一般使用编译根文件系统的交叉工具链。
上面我们制作根文件系统时使用的是 buildroot2021.02.8编译完根文件系统后,我们可以在指定
的目录中找到编译根文件系统的交叉工具链,打开 buildroot 的主目录,进入 output/host/目录,此目录
下就是 buildroot 编译根文件系统中安装的交叉工具链,我们可以将该交叉工具链安装到 usr/local
录下,下面我们安装该交叉工具链。
Buildroot 编译完成后下载的交叉工具链
usr/local 目录下新建一个 arm-gcc-app 目录:
sudo mkdir /usr/local/arm-gcc-app
然后将 output/host/目录下的文件全部拷贝到/usr/local/arm-gcc-app/目录下:
sudo cp -a ./* /usr/local/arm-gcc-app/
接下来我们添加环境变量,打开/etc/profile 文件,在末尾添加路径:
4
详细请阅读源码:drivers\base\map.c kobj_map 的实现。
linux 驱动开发指南 | 李山文
53
1-11 添加环境变量
保存退出后,重新生效该文件,执行:
source /etc/profile
现在我们检测下交叉工具链时候生效,在终端中输入 arm-linux-,然后双击 Tab 按键,此时会出
现如下:
1-12 交叉工具链
可以看到,交叉工具链已经安装成功了,需要注意的是 source 命令只对当前文件夹有效,如果换
一个文件夹就失效了,因此读者最好重启下虚拟机即可。
现在我们编写一个简单的测试程序,打开一个目录,然后新建一个文件夹,创建一个 main.c
件,我们简单写一个 hello linux 程序:
#include "stdio.h"
int main(void)
{
printf("hello linux\n");
return 0;
}
编写完毕后将其保存,在终端中执行如下命令对其进行编译:
arm-linux-gcc main.c -o main.exe
linux 驱动开发指南 | 李山文
54
然后将 main.exe 文件拷贝到开发板中,进入开发板后,找到该文件,运行该程序,如下所示:
# ./main.exe
hello linux
#
可以看到,该程序已经成功运行,我们可以在虚拟机中使用 readelf 工具来查看该文件的一些参
数,如下(笔者使用的是 V3s 芯片)
1-13 readelf 工具
可以看到,编译出来的文件按架构为 v7 架构。还有其他参数可以查看,具体查看 readelf 工具使
用说明。
linux 驱动开发指南 | 李山文
55
linux 驱动开发指南 | 李山文
56
第二章 字符设备驱动
2.1 驱动架构
5
Linux 操作系统将字符设备统一用一个 cdev 结构体进行管理,该结构体对字符设备进行了详细
的描述,cdev 结构体中最重要的成员有两个,分别是 dev_t file_operations其中 dev_t 用来对设备
进行说明,例如设备号,设备名称等设备属性,其目的是确保每个设备的设备 ID 不同。file_operations
用来对设备的读写控制等操作进行管理,可以说该结构体是 cdev 最复杂的成员,也是最重要的成
员,因此在写驱动设备的时候必须对该结构体进行实例化。 整个驱动代码都运行在内核空间,而最
上层的用户应用运行在用户空间,因此应用调用驱动的时候并不能直接运行驱动程序,这个时候驱动
开发者就需要将用户空间映射在内核中间以及反过来映射,这样操作系统在调用驱动的时候会通过系
统调用来对设备进行操作。字符设备的操作过程如图 2-1 所示:
2-1 字符设备驱动调用关系
当然,cdev 结构体除了有 dev_t file_operations 之外,还有其他成员,例如 count该成员记录
owner
THIS_MODULE其中 list 成员为设备链表的前驱和后继,kobj 为字符设备驱动中的一个内核对象。
cdev 结构体在<include/linux/cdev.h>文件中。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
6
file_operations 结构体最为重要,也是最复杂的成员,该结构体中包含了非常多的字符设备操作函
数,当然在初始化的时候并不需要全部初始化,一般用到哪些就初始化哪些。其具体结构如下:
struct file_operations {
5
本书中的驱动框架以 Linux kernel 5.7 为例进行说明
6
结构体摘自 Linux kernel 5.7 版本<include/linux/cdev.h>
linux 驱动开发指南 | 李山文
57
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,
unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,
unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
看到这么大的结构体也不必害怕,大部分时候我们仅仅只需要实现其中的 read()write()ioctl()
open()close()这几个函数。
dev_t 成员实际上是 u32 类型,从 Linux 源码可以看到其定义:
#define MINORBITS 20
linux 驱动开发指南 | 李山文
58
#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))
可以看得到主设备号为 u32 的高 12 ,即 [31:20]次设备号为低 20 位,[19:0]在定义主设备
号和次设备号时可以使用 MAJOR MINOR 这两个宏来得到。
2-2 是字符设备驱动编写的基本流程,首先需要为字符设备分配一个 cdev 设备号,分配方式
有两种,一种是静态分配,另一种是动态分配。分配好 cdev 之后需要对其进行初始化,即初始化 cdev
中的成员。然后调用 cdev_add 函数来将设备驱动注册到内核中。设备的注销比较简单,使用 cdev_del
删除该设备以及使用 unregister_chrdev_region 注销设备号。设备操作函数实现主要是具体编写 open
readwriteclose 等基本的操作函数。
2-2 字符设备驱动步骤
从图 2-1 可以看到,当用户输入 insmod 时,系统将会匹配到相应的驱动模块,同时进入 module_init
宏,该函数会执行设备的注册;当用户输入 remmod 时,进入 module_exit 宏,此时会执行设备注销
函数。下面是字符设备驱动的详细过程:
1. 分配 cdev 结构体内存
该分配方式有两种,一种是静态分配,直接在最外面声明一个全局的 cdev 结构体即可,另一
种是使用 Linux 提供的函数进行分配,即 cdev_alloc() struct cdev
*cdev_alloc(void);
2. 配设备号
分配设备号可以使用两种方式分配,一种是使用 register_chrdev_region 静态分配,另一种是使
alloc_chrdev_region 动态分配。
3. 始化字符设备
使用 cdev_init()函数来初始化设备驱动,将 cdev 设备管理结构体与 file_operatoins 连接起来。
需要注意的是,当使用动态分配 cdev 时,不需要再使用 cdev_init()函数对其进行初始化了,
linux 驱动开发指南 | 李山文
59
因为在分配 cdev 时已经进行了初始化,此时仅仅只需要初始化 file_operations 即可。
4. 字符设备添加到内核
使用 cdev_add()函数将字符设备添加到内核中。
5. 现字符设备的操作函数
操作函数主要有 read()write()ioctl()open()close()等。
6. 备注销
使用 cdev_del()函数来实现对字符设备管理结构体的删除,同时使用 unregister_chrdev_region()
函数来删除字符设备的设备号。
7. 指定字符设备的入口和出口
使用 module_init() module_exit()这两个宏来指定字符设备的出口和入口,这样做的目的是告诉
内核当用户使用 insmod 命令挂载驱动时,此时内核需要执行哪个函数,同理,当用户使用 rmmod
令卸载驱动时,内核需要执行哪个函数。
注:这里对动态申请字符设备管理结构体进行一个说明,首先 cdev
7
是管理字符设备驱动的,这个
需要占用内存,可以是编译器自行编译,也可以使用函数动态获取,这里编译器自动编译分配的空间
有个缺点,存在这种情况,就是当用户虽然挂载了驱动,但设备并未进行注册使用,这样内核还是为
cdev 分配了内存空间,但如果是动态申请,那么只有当用户挂载设备时才为该设备分配空间。
下面是 Linux 内核中 cdev_alloc ()函数实现:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
我们再来看 cdev_init()函数在 Linux 内核中的实现,如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
cdev_alloc () cdev_init()
cdev->ops 这个成员,因此在使用 cdev_alloc()函数分配 cdev 时,需要单独对 ops 进行初始化。
上面我们已经分析了 Linux 中的字符设备驱动的大致框架,由于篇幅有限,这里未对其进行深入
探索,也许读者也会有很多很困惑,特别是刚接触 Linux 驱动开发的读者来说可能更加不好理解,
此,笔者后面将以示例对齐进行更加详细的讲解和说明。
虽然上面我们已经知道了需要实现 cdev 这个结构体并初始化,然而有这个结构体还不够,因为
一个完成的文件操作需要根据设备节点来实现查找。用户在对一个文件操作时其实就是对一个设备节
点进行操作,每个设备都有一个设备节点。设备节点可以在 Linux 内核中的/dev 目录下查看,例如
/dev/fb0 为一个 framebuffer 设备节点,应用程序可以对该设备进行读写操作。如果我们需要自己实现
一个设备,我们也需要创建一个设备节点,哪如何来创建设备节点呢?设备注册的时候并不会创建设
7
cdev 结构体可以看作是 OS 中的 TCB,管理任务的控制块一样。
linux 驱动开发指南 | 李山文
60
备节点,注册仅仅是将设备添加进内核中,内核可以操作这个设备,但对于应用程序而言就无法操作
了,因此我们还需要创建,一般分两种,一种是手动创建设备节点,另一种是自动创建设备节点。
动创建设备节点较为简单,使用 mknod 命令即可,例如我们的主设备号为 1次设备号为 0那么我
们就可以根据主设备号和次设备号来创建。mknod 的使用如下:
mknod [设备目录] [设备类型] [主设备号] [次设备号]
我们以 led 为例:
mknod /dev/led0 c 1 0
说明: /dev/led0 表示在/dev 目录下创建一个 led0 的设备节点(文件)c 表示字符设备,1 表示主
设备号为 10 表示次设备号为 0。这样我们就可以在/dev 目录下看到 led0 设备节点了。
实际驱动开发过程中,一般都会自动创建设备节点,因此在开发时需要调用自动创建节点函数。
具体使用如下:
1. 建一个类
8
static struct class *led_class
led_class = class_create(THIS_MODULE, "led_class");
2. 建一个设备
9
static struct device *led0;
led0 = device_create(led_class,NULL,devno,NULL,"led0");
同样在设备注销的时候会自动删除设备,也会一起删除该设备类,这个过程也是自动完成的。
此在驱动开发过程中需要进行删除相关的设备类:
3. 除设备
device_destroy(led_class,led_dev_num);
4. 除类
class_destroy(led_class);
上面两步就可以自动为字符设备创建一个节点名为 led 的设备,设备默认创建在/dev 目录下。
2-3 设备节点与设备号的关系
8
创建类会在/sys/class 目录下在创建一个该类,为什么要创建设备类会在第 6 章讲到。
9
创建设备会在/dev 目录下创建该设备节点
linux 驱动开发指南 | 李山文
61
上面的注册字符设备中内核提供了一个简单的 API 来实现,内核提供了下面这个函数:
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
该函数有三个参数,第一个参数为主设备号,如果主设备号为 0则内核会动态自动分配一个设
备号;第二个参数为设备名字;第三个为设备文件操作结构体。从上面看好像没有次设备号,是的,
这个函数默认次设备号为 0同时这个函数一次为这个主设备号申请 255 个次设备号,也就是一下子
将这 255 个次设备号全部用完了,因此这种方式注册设备简单粗暴,笔者不建议使用这种方式注册,
仅仅只是为了演示除外。
2.2 LED 驱动实例
2.2.1 知识讲解
在几乎所有的 C 语言教科书中的第一个示例就是hello world这里也从硬件的hello world”,
LED 驱动来开始我们的驱动开发之路。
动态申请
1. 分配 cdev 结构体内存
struct cdev *led_dev = cdev_alloc();
2. 分配设备号
dev_t led_dev_num;
alloc_chrdev_region(&led_dev_num,0,1,"led");
说明:alloc_chrdev_region 函数的第一个参数是 dev_t 变量,该函数会将自动分配的设备
号赋值给 dev_t 变量,也就是 led_dev_num 变量。第二个参数是次设备号的起始号,这里为 0
第三个参数为需要动态分配设备号的数量,这里只需要分配一个设备号,因此填写 1如果需
要多个,则可填写需要的个数即可。第四个参数为设备的名称。
3. 初始化字符设
由于上面使用的是动态分配,因此这里仅仅只需要初始化 file_operations owner
可,如下所示:
led_dev->owner = THIS_MODULE;
led_dev->ops = led_ops;
4. 将字符设备添加到内核
cdev_add(led_dev,led_dev_num,1);
说明:cdev_add 函数的第一个参数是字符设备管理结构体,第二个参数是设备号
10
,第三
个参数是添加多少个设备到内核。
5. 实现字符设备操作函数
在实现之前我们需要分配一个字符设备操作结构体,有两种方式,一种是直接申明一个结构
体,另一种是利用内核函数动态申请一个,这里使用静态直接申明一个:
10
设备号 = 主设备号 + 次设备号
linux 驱动开发指南 | 李山文
62
static struct file_operations led_ops={
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
然后开始实现各个函数,如下:
static int led_open(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
open 这个函数的参数有两个,第一个是 inode,这个参数指向该设备节点,设备节点里面
包含了很多的设备信息,因此我们可以在 open 函数中获取该设备的所有信息,例如设备
号,cdev 节点等,在实际的开发中该参数基本没用到。第二个参数是 file,这个指针指向
该设备的文件结构体,里面包含了该设备的所有内容,其中 ops 只是其中一个,还有打开
方式、标识位、私有数据等,在实际的开发中也很少用到。
可以看到这里 led_open 函数没有做任何事,仅仅在内核中输出了一个信息,这里我们先给
出驱动框架,因此没有具体到内部的具体代码,后面将结合硬件实际编写。
static int led_close(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
close open 一样也是有两个参数,参数也是一样的,这里不再累述。
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *off
p)
{
printk(KERN_DEBUG"read led!!!\n");
return 0;
}
read 函数参数比较多,第一个参数是文件指针,该指针指向正在操作的文件。第二个参数
是用户的内存空间。第三个参数是读的数据大小,对于 32 位为 4 个字节。第四个参数是用
户当前 read 这个操作时的字段,每个设备(文件)在被读的时候都会保存当前读数据的位
置。
static int led_write(struct file *filp, char __user *buff, size_t count, loff_t *of
fp)
{
printk(KERN_DEBUG"write led!!!\n");
return 0;
}
write read 一样,因此这里不再累述。
linux 驱动开发指南 | 李山文
63
6. 设备的注销
在用户输入 rmmod 时,此时设备需要被注销掉,这是内核需要调用 module_exit 宏来执行
相应的函数,我们将其一起放到驱动的入口和出口处编写。
cdev_del(&led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备号
7. 指定驱动的入口和出口
由于内核规定用户可以通过 insmod rmmod 来挂载和卸载驱动,因此我们必须指定驱动的
入口和出口,
module_init(led_init);
module_exit(led_exit);
只需要调用上面两个宏即可,下面时需要具体实现这两个函数,如下:
static struct class *led_class //定义一个 led
static struct device *led0; //定义一个 led0 设备
static int __init led_init(void)
{
/* 分配设备和注册设备号 */
...
led_class = class_create(THIS_MODULE, "led_class"); //创建一个 led
led0 = device_create(led_class,NULL,devno,NULL,"led0"); //创建一个/dev/led0 设备
printk(KERN_DEBUG"led init ok!!!\n");
return 0;
}
static void __exit led_exit(void)
{
/* 删除设备和注销设备号 */
printk(KERN_DEBUG"led exit ok!!!\n");
}
上面的给出了简单的模型,后面将结合具体硬件来实现这个 LED 驱动程序。
静态分配
1. 分配 cdev 结构体内存
/* 定义一个 led 设备号 */
static struct cdev_t led_dev_num;
/* 根据主设备号为 1,次设备号 0 来创建设备号 */
led_dev_num = MKDEV(1,0);
/* 向内核中注册一个设备号为 led_dev_num,注册个数为 1,名称 led 的设备号 */
register_chrdev_region(&led_dev_num,1,"led");
2. 初始化字符设
这里使用 cdev_init()函数对其进行初始化,初始化的目的是将 cdev ops 连接起来。
static struct cdev led_dev;
cdev_init(&led_dev,&led_ops);
linux 驱动开发指南 | 李山文
64
3. 将字符设备添加到内核
cdev_add(led_dev,led_dev_num,1);
说明:cdev_add 函数的第一个参数是字符设备管理结构体,第二个参数是设备号
11
,第三
个参数是添加多少个设备到内核。
后面的过程与动态申请时一样的,这里不再累述。
2.2.1.2 具体实现
这里使用 Lite200 作为验证板,Lite200 板子采用全志公司的 F1C200S 芯片作为主控,其主频默
400M,内核为为 ARM9。其中 LED2 连接原理图如下所示:
2-4 LED2 引脚连接
原理图中的 LED2 连接在 PE10 上,根据 F1C200S 的寄存器手册,可以看到关于 GPIOE2 的配置
寄存器地址。
2-1 GPIO 寄存器基地址
Module Name
Base Address
GPIO
0x01C20800
2-2 GPIOn 寄存器偏移地址
Register Name
Offset
Description
Pn_CFG0
n*0x24+0x00
端口 n 配置寄存器 0 (n=0~5)
Pn_CFG1
n*0x24+0x04
端口 n 配置寄存器 1 (n=0~5)
Pn_CFG2
n*0x24+0x08
端口 n 配置寄存器 2 (n=0~5)
Pn_CFG3
n*0x24+0x0C
端口 n 配置寄存器 3 (n=0~5)
Pn_DATA
n*0x24+0x10
端口 n 数据寄存器 (n=0~5)
Pn_DRV0
n*0x24+0x14
端口 n 多选-驱动寄存器 0 (n=0~5)
Pn_DRV1
n*0x24+0x18
端口 n 多选-驱动寄存器 1 (n=0~5)
Pn_PUL0
n*0x24+0x1C
端口 n /下拉寄存器 0 (n=0~5)
Pn_PUL1
n*0x24+0x20
端口 n /下拉寄存器 1 (n=0~5)
PIO_INT_CFG0
0x200+n*0x20+0x00
端口 n 中断配置寄存器 0 (n=0~2)
PIO_INT_CFG1
0x200+n*0x20+0x04
端口 n 中断配置寄存器 1 (n=0~2)
PIO_INT_CFG2
0x200+n*0x20+0x08
端口 n 中断配置寄存器 2 (n=0~2)
PIO_INT_CFG3
0x200+n*0x20+0x0C
端口 n 中断配置寄存器 3 (n=0~2)
PIO_INT_CTRL
0x200+n*0x20+0x10
端口 n 中断控制寄存器 (n=0~2)
PIO_INT_STA
0x200+n*0x20+0x14
端口 n 中断状态寄存器 (n=0~2)
11
设备号 = 主设备号 + 次设备号
linux 驱动开发指南 | 李山文
65
PIO_INT_DEB
0x200+n*0x20+0x18
端口 n 中断消抖寄存器 (n=0~2)
SDR_PAD_DRV
0x2C0
SDRAM 引脚多选-驱动寄存器
SDR_PAD_PUL
0x2C4
SDRAM 引脚上/拉寄存器
从上面的表中可以看到 GPIOE
12
的相关寄存器地址如下(只列出需要用到的寄存器)
2-3 GPIOE 关寄存器地址
Register Name
Address
Description
GPIOE_CFG0
0x01C20890
配置寄存器 0
GPIOE_CFG1
0x01C20894
配置寄存器 1
GPIOE_DATA
0x1C208A0
数据寄存器
GPIOE_PUL0
0x1C208AC
/下拉输出配置
有了需要配置的寄存器外,还需要知道如何去配置 GPIOE 引脚,因此还需要知道配置寄存器以及数
据寄存器的相关定义,以下是相关寄存器的说明:
2-4 GPIOE_CFG0 寄存器说明
Offset: 0x90
Register Name: GPIOE_CFG0
Bit
R/W
Default/Hex
Description
31
/
/
Reserved
30:28
R/W
0x07
PE7 Select
000: Input
001: Output
010: CSI_D4
011: UART2_TX
100: SPI1_CS
101: Reserved
110: EINTE7
111: Disable
27
/
/
Reserved
26:24
R/W
0x07
PE6 Select
000: Input
001: Output
010: CSI_D3
011: PWM1
100: DA_OUT
101: OWA_OUT
110: EINTE6
111: Disable
23
/
/
Reserved
22:20
R/W
0x07
PE5 Select
000: Input
001: Output
010: CSI_D2
011: LCD_D17
100: DA_IN
101: Reserved
110: EINTE5
111: Disable
19
/
/
Reserved
18:16
R/W
0x07
PE4 Select
000: Input
001: Output
010: CSI_D1
011: LCD_D16
100: DA_LRCK
101: RSB_SDA
110: EINTE4
111: Disable
15
/
/
Reserved
14:12
R/W
0x07
PE3 Select
000: Input
001: Output
010: CSI_D0
011: LCD_D9
12
GPIOA 对应着 n=0GPIOB 对应着 n=1 GPIOE 对应着 n=4
linux 驱动开发指南 | 李山文
66
100: DA_BCLK
101: RSB_SCK
110: EINTE3
111: Disable
11
/
/
Reserved
10:8
R/W
0x07
PE2 Select
000: Input
001: Output
010: CSI_PCLK
011: LCD_D8
100: CLK_OUT
101: Reserved
110: EINTE2
111: Disable
7
/
/
Reserved
6:4
R/W
0x07
PE1 Select
000: Input
001: Output
010: CSI_VSYNC
011: LCD_D1
100: TWI2_SDA
101: UART0_TX
110: EINTE1
111: Disable
3
/
/
Reserved
2:0
R/W
0x07
PE0 Select
000: Input
001: Output
010: CSI_HSYNC
011: LCD_D0
100: TWI2_SCK
101: UART0_RX
110: EINTE0
111: Disable
2-5 GPIOE_CFG1 寄存器说明
Offset: 0x94
Register Name: GPIOE_CFG1
Bit
R/W
Default/Hex
Description
31:19
/
/
Reserved
18:16
R/W
0x07
PE12 Select
000: Input
001: Output
010: DA_MCLK
011: TWI0_SDA
100: PWM0
101: Reserved
110: EINTE12
111: Disable
15
/
/
Reserved
14:12
R/W
0x07
PE11 Select
000: Input
001: Output
010: CLK_OUT
011: TWI0_SCK
100: IR_RX
101: Reserved
110: EINTE11
111: Disable
11
/
/
Reserved
10:8
R/W
0x07
PE10 Select
000: Input
001: Output
010: CSI_D7
011: UART2_CTS
100: SPI1_MISO
101: Reserved
110: EINTE10
111: Disable
7
/
/
Reserved
6:4
R/W
0x07
PE9 Select
000: Input
001: Output
010: CSI_D6
011: UART2_RTS
100: SPI1_CLK
101: Reserved
linux 驱动开发指南 | 李山文
67
110: EINTE9
111: Disable
3
/
/
Reserved
2:0
R/W
0x07
PE8 Select
000: Input
001: Output
010: CSI_D5
011: UART2_RX
100: SPI1_MOSI
101: Reserved
110: EINTE8
111: Disable
2-6 GPIOE_DATA 寄存器说明
Offset: 0xA0
Register Name: GPIOE_DATA
Bit
R/W
Default/Hex
Description
31:13
/
/
Reserved
12:0
R/W
0x00
Input: 此时状态为对应引脚的状态
Output: 设置该位可以设置对应引脚的状态
2-7 GPIOE_PUL0 寄存器说明
Offset: 0xAC
Register Name: GPIOE_PUL0
Bit
R/W
Default/Hex
Description
31:26
/
/
Reserved
2i+1:2i
i=0~12
R/W
0x00
Port n Pull-up/down Select (n=0~12)
00: Pull-up/down Disable
01: Pull-up
10: Pull-down
00: Reserved
知道了寄存器的地址以及每个位的配置说明,我们就可以根据这些信息来设置我们需要配置的引
脚了。首先,我们在代码里面定义这些需要配置的寄存器地址,定义如下:
#define GPIOE_CFG0 (0x01C20890)
#define GPIOE_CFG1 (0x01C20894)
#define GPIOE_DATA (0x01C208A0)
#define GPIOE_PUL0 (0x01C208AC)
需要说明的是上面的地址都是物理地址,在开发驱动的时候一般需要将物理地址映射到虚拟地址
中,然后再在虚拟地址上进行操作,后面我们会详细说明这个。当用户使用 insmod 命令时,此时驱
动将会执行 module_init 宏中指定的函数,这个函数是驱动程序的入口,一般来说,设备的初始化和
注册都会放到这个函数中,这样进入入口时,驱动就可以被顺利注册。同理,用户使用 rmmod 命令
时,此时驱动将会执行 module_exit 宏中指定的函数,这个是驱动程序的出口,一般来说,设备的注
销和删除都会放在该函数中,这样驱动进入出口时,此时设备就可以被顺利的注销和删除掉。
static dev_t led_dev_num; //定义一个设备号
static struct cdev *led_dev; //定义一个设备管理结构体指
static struct class *led_class; //定义一个设备类
static struct device *led0; //定义一个设备
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static int __init led_init(void)
{
int ret;
linux 驱动开发指南 | 李山文
68
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
goto error;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret != 0)
{
goto error;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针为 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核
led_class = class_create(THIS_MODULE, "led_class"); //创建一个类
if(led_class == NULL)
{
goto error;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0")
13
; //创建一个设备
if(IS_ERR(led0))
{
goto error;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
return 0;
error:
printk(KERN_WARNING"led_init failed!\n");
return 1;
}
注:ioremap()函数是将 IO 空间的物理地址映射为虚拟地址,操作寄存器的地址是物理地址,但
由于 Linux 运行在 MMU 中,其存在的地址为虚拟地址,因此我们并不能直接对物理地址进行操
作。Linux 操作系统为此提供了这个函数,目的是将这段地址映射为虚拟地址。
static void __exit led_exit(void)
{
cdev_del(led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备类
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
13
该函数会在/dev 目录下创建一个名为 led0 的设备节点,详细请看 1.1.3.1 小节。
linux 驱动开发指南 | 李山文
69
}
下面实现 led_ops 相关函数,首先,定义一个 led_ops 结构体如下:
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
然后实现 led_openled_readled_writeled_close 函数:
static int led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
*((volatile size_t*)gpioe_data) &= ~(1<<16); //清除数据寄存
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int led_close(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
int ret;
size_t status = ((*((volatile size_t*)gpioe_data))>>12)&0x01;//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int led_write(struct file *filp, const char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间 status
linux 驱动开发指南 | 李山文
70
if(ret < 0)
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
上面实现了所有的操作函数,最后我们指定一下驱动程序的入口和出口即可:
module_init(led_init);
module_exit(led_exit);
由于很多宏和结构体都在 Linux 中定义了,因此在编写的时候必须包含相关的头文件,上面需要
包含的都文件有如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函
#include <linux/cdev.h> //包含 cdev 结构体
由于 Linux 是遵循 GPL 协议,因此我们在编写驱动的过程中需要添加相关的宏,其目的是消除
编译警告。
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_dev"); //简单的描述
注:在 Linux 中,打印分为了很多等级,每个等级不同其输出的优先级将不同。在 C 语言中常用
标准的输入输出函数,而在 Linux 内核中使用 printk 函数来打印信息。其等级有如下:
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
其数值越小,打印优先级越高。需要说明的是级别 0~3 会在控制台中打印,而级别 4~7 将不会在
控制台中打印,如果需要看打印级别为 4~7 的打印信息,需要使用 dmesg 命令查看。
使用 printk 函数的格式为 printk(打印等级+打印的字符串);
下面是 LED 驱动程序的完整代码:
linux 驱动开发指南 | 李山文
71
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函
#include <linux/cdev.h>
#define GPIOE_CFG0 (0x01C20890)
#define GPIOE_CFG1 (0x01C20894)
#define GPIOE_DATA (0x01C208A0)
#define GPIOE_PUL0 (0x01C208AC)
static dev_t led_dev_num; //定义一个设备号
static struct cdev *led_dev; //定义一个设备管理结构体指
static struct class *led_class; //定义一个设备类
static struct device *led0; //定义一个设备
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static int led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int led_close(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
int ret;
size_t status = ((*((volatile size_t*)gpioe_data))>>12)&0x01;//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
linux 驱动开发指南 | 李山文
72
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int led_write(struct file *filp, const char __user *buff, size_t count, loff_t *o
ffp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
static int __init led_init(void)
{
int ret;
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体的 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针为 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个名为 led_class 的类
linux 驱动开发指南 | 李山文
73
if(led_class == NULL)
{
printk(KERN_WARNING"led_class failed!\n");
return -1;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0");//创建一个设备名为 led0
if(IS_ERR(led0))
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
return 0;
}
static void __exit led_exit(void)
{
cdev_del(led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_dev"); //简单的描述
2.2.1.3 编译测试
1) 编译驱动模块
上述我们已经完成了代码的编写,下面需要对其进行测试,由于 Linux 编译比较复杂,这里暂时
先采用动态加载的方式来验证,后面笔者会详细讲解如何在 Linux 内核中添加自己的源码同时添加编
译选项。动态加载驱动是指用户使用 insmod 命令来手动挂载驱动,这样不需要对内核进行重新编译,
其好处是调试较为方便,但缺点就是每次系统关闭时驱动将会自动注销,因此每次上电都需要重新挂
载驱动。
需要上面驱动进行,由个没有现 IDE ,因需要
Makefile 脚本,编译脚本较为简单,需要注意的是编译驱动文件的时候需要有板子当前运行的 Linux
linux 驱动开发指南 | 李山文
74
操作系统的源码,我们将源码放到一个固定的目录,然后指定编译器包含里面的相关头文件,只有这
样才能成功编译。
编译脚本如下:
KERN_DIR = /home/lsw/licheepi/linux
all:
make -C $(KERN_DIR) M=$(shell pwd) modules
clean:
rm -rf *.order *o *.symvers *.mod.c *.mod *.ko
obj-m += led_dev.o
注意:由于文本问题,直接拷贝时需要将 3 6 行最开始变为 Tab 键。
KERN_DIR 变量表示内核的源码目录,这里笔者将内核源码放置在/home/lsw/licheepi/linux
目录下。make -C $(KERN_DIR) M=$(shell pwd) modules 这句表示编译的时候会先进入
KERN_DIR 指定了模块的位置为 M M=$(shell pwd)即当前文件
led_dev Makefile)路径。最后一个 modules 是编译器指定的参数,以模块方式编译。obj-
m += led_dev.o 表示编译的时候以模块的方式编译 led_dev.c 文件。
obj-m Linux 内部一个默认的编译选项,总共有三种, obj-n,obj-y,obj-m其中 obj-n
表示不编译;obj-y 表示编译到内核中去;obj-m 表示编译为模块,需要手动加载驱动。
在终端指定 make,可以看到编译出了一个.ko 文件,如下图所示:
2-4 编译生成 . ko 文件
.ko 文件拷贝到开发板文件系统中,并启动系统,进入终端,然后用 insmod 命令来挂载该驱动:
insmod led_dev.ko
此时终端会输出信息如下:
2-5 手动加载 led_dev.ko 驱动
现在我们查看设备节点和设备文件是否成功创建,进入/dev 目录下,可以看到该目录下已经成
功创建设备节点,其设备名称为“led0”,主设备号为 248,次设备号为 0
linux 驱动开发指南 | 李山文
75
2-6 /dev 目录下 led0 设备节点
然后进入/sys/class 目录下,可以看到有一个 led_class 文件夹被创建:
2-7 led_class 设备类被创建
从上面可以看到我们的驱动初始化时候已经成功创建设备节点和设备类。
说明:上面编译驱动的交叉工具链应该与编译内核的工具链保持一致
2) 用程序测试
测试应用程序较为简单,和 C 语言文件操作一样,具体代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd;
char* filename=NULL;
linux 驱动开发指南 | 李山文
76
int val;
filename = argv[1];
fd = open(filename, O_RDWR);//打开 dev/设备文件
if (fd < 0)//小于 0 说明没有成功
{
printf("error, can't open %s\n", filename);
return 0;
}
if(argc !=3)
{
printf("usage: ./led_dev.exe [device] [on/off]\n"); //打印用法
}
if(!strcmp(argv[2], "on")) //如果输入等于 on,则 LED
val = 0;
else if(!strcmp(argv[2], "off")) //如果输入等于 off,则 LED
val = 1;
else
goto error;
write(fd, &val, 4);//操作 LED
close(fd);
return 0;
error:
printf("usage: ./led_dev.exe [device] [on/off]\n"); //打印用法
close(fd);
return -1;
}
编写好测试程序后,我们需要将其编译为可执行文件,由于测试程序是应用程序,因此我们需要
用编译跟文件系统的交叉工具链编译源码。在终端执行:
arm-linux-gcc led_dev.c -o led_dev.exe
此时在目录下会生成一个 led_dev.exe 可执行文件,将其拷贝到开发板中,然后为其添加执行权
限:
chmod +x led_dev.exe
执行该测试程序:
./led_dev.exe /dev/led0 on
此时可以看到开发板上的 LED 灯亮起,再执行:
./led_dev.exe /dev/led0 off
此时可以看到开发板上的 LED 灯熄灭。
上述我们已经完成了最简单的 LED 驱动程序,基本的驱动框架就是这样,但随着 Linux 驱动程
序的复杂化,Linux 的驱动框架也变得越来越负责,例如在 Linux 3.x 开始引入设备树,随后又引入大
量的子系统,诸如 pinctrl,这也使得 Linux 的学习路线变得越来越长。
linux 驱动开发指南 | 李山文
77
2.3 动编译进内
上面的驱动都是采用动态挂载的方式,这种方式比较适合在代码调试的时候使用,但这种方式挂
载的驱动有个缺点,当开发板关机后重启时,此时驱动就没有了,用户必须重新使用 insmod 命令挂
载该驱动。因此,在调试好驱动后,一般会将驱动静态编译进系统镜像中,系统启动后会自动挂载驱
动,这样用户不需要每次重启手动挂载驱动了,下面我们来讲解下如何将驱动编译到内核中。
2.3.1 驱动码结构
Linux 驱动源码中, drivers 目录下,有非常多的文件夹,每个文件夹下面有很多文件或者文
件夹。这么多文件对应着不同的驱动,可以看到驱动目录下的源码几乎占了内核所有源码的一大半。
进入 drivers 目录,观察发现每个目录下都会有一个 Makefile 和一 Kconfig 文件,这两个文件非常
关键,下面会对其进行说明。由于所有的驱动源码太过庞大,因此我们下面以 LEDS 驱动为例进行详
细说明。
首先下载 Linux-5.7,然后解压到用户目录下,进入 drivers/leds 目录下,可以看到该目录下有很
led-xxx.c 文件,这些文件都是不同板子的驱动现在打开当前目录下的 Kconfig 文件,从名字可以
看到该文件是内核配置文件,打开任意一个 Kconfig 文件,可以看到里面的内容都与 make menuconfig
的选项有关。Kconfig 文件是当前目录下的配置文件,用来选择当前哪些文件编译进内核,Kconfig
件如下图所示,其中 config 是一个系统变量,该变量在每个配置前面都有,其后是一个宏开关,该宏
是控制该模块是否编译进内核中,该宏的值有三种:y, n, m。其中 y 是编译进内核,n 是不编译进内
核,m 是将其编译为模块,即.ko 文件。那这个宏是如何控制是否编译进内核的呢?我们先在源码根
目录下的终端中执行 meke menuconfig 进入图形配置界面。
2-9 Kconfig 文件
linux 驱动开发指南 | 李山文
78
在图形配置界面中找到> Device Drivers > Led Support,可以看到里面有很多配置选项,仔细观
察发现,在图形界面中的选项和上面的 Kconfig 文件中的 tristate 后面的字符串是一样的,所以我们要
添加自己的配置就需要在 Kconfig 文件中添加一个自己的 config 并填写 tristate 字符串。
2-10 menuconfig LED 的配置界面
有读者可能发现在 Kconfig 中有一个 LED Support for Motorola CACAP,但是在 menuconfig 配置
中却没有,实际上这里需要提到 Kconfig 中的 depends on 这个关键字了。该关键字是依赖关系,例如
depends on LEDS_CLASS,这表示只有当 LEDS_CLASS 宏开启后该配置选项才能配置,否则不会显
示在 menuconfig 中。上面的 LED Support for Motorola CACAP 由于依赖 MFD_CACAP 宏,但该宏并
未打开,因此看不到这个配置选项。说了这么多没有说一个很重要的宏就是 config 后面的这个宏,
宏是控制 Makefile 中的编译选项的,我们打开 drivers/leds 目录下的 Makefile 文件,可以看到里面有
非常多的 obj-$(CONFIG_XXX)+=xxx.o。这里的 CONFIG_XXX 其实就是上面说的 Kconfig 文件中的
config y,则 obj-
$(CONFIG_XXX)+=xxx.o 就为 obj-y+=xxx.o这样 obj-y 这个变量就会将 xxx.o 目标文件增加到编译
的目标文件中,从而实现该文件的编译。不知读者是否注意到 obj-m 这个很眼熟,是的,在动态编译
驱动模块的时候我们使用的就是这个 obj-m 来告诉编译器将目标文件编译为.ko 文件。
2-11 Makefile 文件中的内容
linux 驱动开发指南 | 李山文
79
2.3.2 增驱动配置
从上面的分析中可以知道如何添加自己的驱动配置选项了,下面来将 2.2.1 LED 驱动添加到内
核中并添加可配置选项。首先在 drivers/leds 目录下新建一个 leds-myled.c 文件,
2-8 driver/leds 录下新 myled.c 文件
然后将 2.2.1 LED 驱动代码填写进去,同时在当前目录下的 Kconfig 文件最后添加:
config LEDS_MYLED
tristate “led for Lite200 board”
depends on LEDS_CLASS
default y if LEDS_CLASS
help
this driver supports Lite200.
如下图所示,其中 config LEDS_MYLED 表示新增加一个宏配置,tristate “led for Lite200 board”
表示会在图形配置界面显示的字符串;depends on LEDS_CLASS 表示该宏依赖于 LEDS_CLASS 这个
宏;只有当 LEDS_CLASS 宏开启时,此配置才显示;default y if LEDS_CLASS 表示当该配置打开时,
此时默认选择 yhelp 后面描述的是帮助信息。可以看到上面的依赖只有一个,当 LEDS_CLASS
开启后默认编译进内核。 LEDS_CLASS 又是一个啥宏呢?其实这个宏表示在是否在/sys/class 中创
建一个设备类,对于一般的驱动来说,我们都要创建一个设备类,创建类这个操作需要追溯到 devfs
udev 之争,最开始的内核使用的是 devfs但由于其存在很多缺陷,后来逐渐被淘汰, Linux 2.6
内核之后全面使用 udev,该设备框架要求开发者每创建一个设备必须创建一个设备类和一个设备节
点,因此我们需要符合标准的规范。
随着
linux 驱动开发指南 | 李山文
80
2-12 Kconfig 加新的配置选项
Kconfig 文件添加好了,然后需要在 Makefile 文件中添加编译选项,添加如下:
obj-$(CONFIG_LEDS_MYLED) +=myled.o
根据 Makefile 的潜在规则,执行该脚本时,此时编译器会默认编译 myled.c 文件。如下图所示:
2-13 Makefile 加编译选项
然后回到内核源码的主目录中,执行 make menuconfig 命令,可以看到此时图形界面中的> Device
Drivers > Led Support 下面有我们新建的配置,如下图所示。可以看到默认选项时 y,也就是默认将
leds-myled.c 编译进内核镜像中,这样内核启动后会自动挂载驱动程序。当然也可以通过按键 N 来取
消编译该模块,将光标移动到<help>处,按下回车,可以看到此时的帮助信息正好是我们之前添加的
help 下面的字符信息。
2-14 menuconfig 新增配置选项
linux 驱动开发指南 | 李山文
81
点击<Save>选项,然后退出 menuconfig 图形界面,执行 make 命令,最终将编译好的镜像文件下载
TF 卡中,启动板子,进入控制台终端,执行 ls /dev 可以看到我们的驱动程序已经加载进来了。
注意:有时候会出现编译失败的情况,这种情况大多是因为 Kconfig 文件中的 config 后面的宏与
Makefile obj-后面的宏不符合规范造成的,特别强调,Makefile 中的 obj-后面的宏是以
CONFIG_开头的,即 obj-$(CONFIG_XXX),而 Kconfig 里面的宏是 config XXX,没有
CONFIG_
2-15 系统启动后已自动挂载 led0 驱动
2.4 简单断设备驱
中断是所有 CPU 都具备的功能,有时候我们并不需要每时每刻去查询某个引脚或者某个外设的
状态,因为大部分情况下这些外设可能并没有任何变化,而只有在很少的时间内才被触发,因此中断
非常有必要,这减少了 CPU 无效的轮询。例如最简单的按键设备,该设备连接在处理器的一个引脚
上,当电平变化时,此时 CPU 检测到外部事件后会立刻处理中断服务程序。
2-16 中断框架
上图是中断框架图,所有的外设连接到中断控制器,中断的优先级由中断控制器决定,每个外设
都分配了一个硬件中断号,所有的中断信号由中断控制器通知处理器。
在驱动程序中,我们的中断需要向 CPU 申请,内核提供了下面两个接口供驱动开发者调用:
1. static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags,const char *name, void *dev)
linux 驱动开发指南 | 李山文
82
2. const void *free_irq(unsigned int irq, void *dev_id)
第一个函数是向 CPU 申请一个中断,该函数的第一个参数为中断号;第二个参数为中断服务函
数的句柄,也就是中断服务函数地址;第三个参数为中断类型标志,即触发方式;第四个参数为中断
名称;第五个参数为中断设备 ID。例如下面这个例如:
request_irq(IRQ_EINT10, btn_irq, IRQT_FALLING, "btn", (void * )&key_value)
上面的表示中断号为 10中断服务函数为 btn_irq,中断触发方式为下降沿触发,中断名称为 btn
中断设备 ID key_value。下面是 key_value 实现:
static u8 key_value = 2;
下面是 btn_irq 的实现:
static irqreturn_t btn_irq(int irq, void *dev_id)
{
u8 key = *dev_id; //读取设 ID 中的值
wake_up_interruptible(&btn_waitq); /* 唤醒休眠的进程,即调 read 函数的进程 */
ev_press = 1;
return IRQ_HANDLED;
}
上面的 wake_up_interruptible(&btn_waitq);是向内核发送信号,唤醒一个休眠的进程,
底是唤醒哪个进程由 wait_event_interruptible 在哪个函数决定。当我们关闭这个设备时,我们
需要释放中断号,例如下面:
free_irq(IRQ_EINT10 ,(void*)&key_value);//释放中断
上面的代码中有一个 btn_waitq 全局变量,这个全局变量用一个宏来申明:
DECLARE_WAIT_QUEUE_HEAD(btn_waitq);
这个宏向内核申明了一个等待队列,将 btn_waitq 这个变量作为一个队列头部放到队列中。
还有上面的 ev_press 也是一个全局变量:
static unsigned int ev_press; //一个全局变量,记录中断事件状态
现在我们需要指定一个进程来让其睡眠,毫无疑问这个进程必须是读操作文件,即 read 数:
static ssize_t btn_cdev_read(struct file * file, char __user * userbuf, size_t count,
loff_t * off)
{
int ret;
if(count != 1)
{
printk("read size must be 1r\n");
return -1;
}
wait_event_interruptible (button_waitq, ev_press);//将当前进程放入等待队列
ret = copy_to_user(userbuf, &key_val, 1); //将取得的按键值传给上层应用
ev_press = 0;//将中断事件置为 0,表示中断已结束
if(ret)
{
printk("copy error\n");
linux 驱动开发指南 | 李山文
83
return -1;
}
return 1;
`}
上面的函数中使用了一个 wait_event_interruptible 这个函数,该函数是将该进程放到睡眠
队列中,等待唤醒,这里读者应该会有疑问,为何这个就可以将其放到队列中呢?此时该函数还没有
执行啊。是的,确实是这样的,如果该函数没有执行,那么这个 read 进程是永远无法放到队列中的,
但是我们需要提前让其执行,只有一个办法就是应用程序去做 read 操作,因此在应用程序中必须要
不断的做 read 操作,至少做一次,后面的测试代码可以很容易的明白这一点。
其他的实现和字符设备是一样的,首先注册设备号和创建设备节点,然后实现各个操作函数,这
里将不再累述,下面我们通过一个完整的例子来说明。
2.4.1 键中断示例
我们开发板上的按键连接在 LRADC 上,即 KEYADC 上:
2-16 开发板按键连接图
现在我们查看我们的 F1C200 数据手册中的寄存器如下:
2- 8 F1C200 中断号表
Name
Number
Vector
Description
NMI
0
0x0000
NMI interrupt 不可屏蔽中断
UART0
1
0x0004
UART0 interrupt
UART1
2
0x0008
UART1 interrupt
UART2
3
0x000C
UART2 interrupt
/
4
0x0010
/
OWA
5
0x0014
OWA interrupt
CIR
6
0x0018
CIR interrupt
TWI0
7
0x001C
TWI0 interrupt
TWI1
8
0x0020
TWI1 interrupt
TWI2
9
0x0024
TWI2 interrupt
SPI0
10
0x0028
SPI0 interrupt
SPI1
11
0x002C
SPI1 interrupt
/
12
0x0030
/
Timer0
13
0x0034
Timer0 interrupt
linux 驱动开发指南 | 李山文
84
Timer1
14
0x0038
Timer1 interrupt
Timer2
15
0x003C
Timer2 interrupt
Watchdog
16
0x0040
Watchdog interrupt
RSB
17
0x0044
RSB interrupt
DMA
18
0x0048
DMA interrupt
19
0x004C
Touch Panel
20
0x0050
Touch Panel interrupt
Audio Codec
21
0x0054
Audio Codec interrupt
KEYADC
22
0x0058
KEYADC interrupt
SDC0
23
0x005C
SDC0 interrupt
SDC1
24
0x0060
SDC1 interrupt
/
25
0x0064
/
USB-OTG
26
0x0068
USB-OTG interrupt
TVD
27
0x006C
TVD interrupt
TVE
28
0x0070
TVE interrupt
TCON
29
0x0074
LCD interrupt
DE_FE
30
0x0078
DE_FE interrupt
DE_BE
31
0x007C
DE_BE interrupt
CSI
32
0x0080
CSI interrupt
DE-interlacer
33
0x0084
DE-interlacer interrupt
VE
34
0x0088
VE interrupt
DAUDIO
35
0x008C
DAUDIO interrupt
/
36
0x0090
/
37
0x0094
/
PIOD
38
0x0098
GPIOD interrupt
PIOE
39
0x009C
GPIOE interrupt
PIOF
40
0x00A0
GPIOF interrupt
从上面的表中我们可以知道 KEYADC 的中断号为 22上面的是硬件中断号。我们申请中断如下:
ret = request_irq(22, btn_irq, IRQT_FALLING, "btn",(void*)&key_value);
我们释放中断如下:
free_irq(22,(void*)&key_value);//释放中断
但实际上我们还需要配置中断寄存器,这样才能实现中断功能,我们查看 F1C200 数据手册,
KEYADC 相关寄存器,如下:
2-9 KEYADC 寄存器基址
Module Name
Base Address
KEYADC
0x01C23400
2-10 KEYADC 相关寄存器
Register Name
Offset
Description
KEYADC_CTRL_REG
0x00
KEYADC 控制寄存器
KEYADC_INTC_REG
0x04
KEYADC 中断控制寄存器
KEYADC_INTS_REG
0x08
KEYADC 中断状态寄存器
KEYADC_DATA_REG
0x0C
KEYADC 数据寄存器
linux 驱动开发指南 | 李山文
85
2-11 KEYADC 控制寄存
Offset: 0x00
Register Name: KEYADC_CTRL_REG
Bit
R/W
Default/Hex
描述
31:24
R/W
0x1
FIRST_CONCERT_DLY
ADC 第一次转换延时设置,延时时间为 n 个采样时间
14
23:22
R/W
0x0
保留为 0
21:20
/
/
/
19:16
R/W
0x0
CONTINUE_TIME_SELECT
持续模式次数选择,每次的时间为 8*(N+1)个采样时间
15:14
/
/
/
13:12
R/W
0x0
KEY_MODE_SELECT按键模式选择
00:普通模式
01:单次模式
10:持续模式
11:8
R/W
0x1
LEVELA_B_CNT
等级 A 到等级 B 的时间阈值选择,时间为 n+1 个采样时
7
R/W
0x0
KEY_ADC_HOLD_KEY_EN, KEY_ADC 定使能
0Disable 1: Enable
6
R/W
0x1
KEYADC_HOLD_EN
KEYADC 采样率锁定使能
0Disable 1: Enable
5:4
R/W
0x2
3:2
R/W
0x2
KEYADC_SAMPLE_RATE
KEYADC 采样率设置
00250Hz
01125Hz
1062.5Hz
1132.25Hz
1
/
/
/
0
R/W
0x0
KEYADC_EN
KEYADC 使能
0Disable 1Enable
2-12 KEYADC 中断控制寄存器
Offset: 0x04
Register Name: KEYADC_INTC_REG
Bit
R/W
Default/Hex
描述
31:5
/
/
/
4
R/W
0x0
ADC0_KEYUP_IRQ_EN
ADC0 按键增加使能
0Disable 1Enable
3
R/W
0x0
ADC0_ALRDY_HOLD_IRQ_EN
ADC0 按键已准备号锁定中断使能
0Diasble 1Enable
2
R/W
0x0
ADC0_HOLD_IRQ_EN
ADC0 按键锁定中断使能
14
采样率为 250Hz
linux 驱动开发指南 | 李山文
86
0Diasble 1Enable
1
R/W
0x0
ADC0_KEYDOWN_EN
ADC0 按键减小使能
0Disable 1Enable
0
R/W
0x0
ADC0_DATA_IRQ_EN
ADC0 数据中断使能
0Disable 1Enable
2-12 KEYADC 中断状态寄存器
Offset: 0x08
Register Name: KEYADC_INTS_REG
Bit
R/W
Default/Hex
描述
31:5
/
/
/
4
R/W
0x0
ADC0_KEYUP_PENDING
ADC0 按键增加挂起状态位
0No IRQ
1IRQ Pending
1 清除该位
3
R/W
0x0
ADC0_ALRDY_HOLD_PENDING
ADC0 按键已准备号锁定挂起状态
0No IRQ
1IRQ Pending
1 清除该位
2
R/W
0x0
ADC0_HOLDKEY_PENDING
ADC0 按键锁定挂起状态位
0No IRQ
1IRQ Pending
1 清除该位
1
R/W
0x0
ADC0_KEYDOWN_PENDING
ADC0 按键减小挂起状态位
0No IRQ
1IRQ Pending
1 清除该位
0
R/W
0x0
ADC0_DATA_PENDING
ADC0 数据挂起状态位
0No IRQ
1IRQ Pending
1 清除该位
2-12 KEYADC 数据寄存器
Offset: 0x08
Register Name: KEYADC_INTS_REG
Bit
R/W
Default/Hex
描述
31:6
/
/
/
5:0
R
0x0
KEYADC_DATA
KEYADC 数据
我们需要设置 KEYADC 相关的寄存器,主要配置两个寄存器,分别是控制寄存器和中断控制寄
存器。 对于控制寄存器而言,大部分寄存器都不需要配置,默认就可以了,我们只需要开启中断就
可以了,
#include <linux/module.h>
linux 驱动开发指南 | 李山文
87
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 iomap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数
#include <linux/device.h> //含有类相关的处理函数
#include <linux/irq.h> //含有 IRQ_HANDLED IRQ_TYPE_EDGE_RISING
#include <asm-arm/irq.h> //含有中断触发类型
#include <linux/interrupt.h> //含有 request_irqfree_irq 函数
#define KEYADC_BASE (0x01C23400)
#define KEYADC_CTRL_REG (KEYADC_BASE+0x00)
#define KEYADC_INTC_REG (KEYADC_BASE+0x04)
#define KEYADC_INTS_REG (KEYADC_BASE+0x08)
#define KEYADC_DATA_REG (KEYADC_BASE+0x0C)
static unsigned int ev_press; //一个全局变量,记录中断事件状态
DECLARE_WAIT_QUEUE_HEAD(btn_waitq);//注册一个等待队 button_waitq,用宏来申明一个全局变量
static unsigned int key_value = 2; //定义一个变量保存按键值
static irqreturn_t btn_irq(int irq, void *dev_id)
{
u8 key = *dev_id; //读取设 ID 中的值
wake_up_interruptible(&btn_waitq); /* 唤醒休眠的进程,即调 read 函数的进程 */
ev_press = 1;
return IRQ_HANDLED;
}
static int btn_cdev_open (struct inode * inode, struct file * file)
{
int ret;
ret = request_irq(22, btn_irq, IRQT_FALLING, "btn",(void*)&key_value);
if(ret) //返回不为 0,表示申请失败
{
printk("request irq failed!\n");
return -1;
}
return 0;
}
static int btn_cdev_close(struct inode * inode, struct file * file)
{
free_irq(22,(void*)&key_value);//释放中断
return 0;
}
linux 驱动开发指南 | 李山文
88
static ssize_t btn_cdev_read(struct file * file, char __user * userbuf, size_t count,
loff_t * off)
{
int ret;
if(count != 1)
{
printk("read size must be 1r\n");
return -1;
}
//将当前进程放入等待队列 button_waitq ,并且释放 CPU 进入睡眠状态
wait_event_interruptible(button_waitq, ev_press);
ret = copy_to_user(userbuf, &key_val, 1);//将取得的按键值传给上层应用
ev_press = 0;//按键已经处理可以继续睡眠
if(ret)
{
printk("copy error\n");
return -1;
}
return 1;
}
2.4.1 测试程序(由于没有相关硬件,未测试)
由于按键应用比较特殊,因为在按键驱动程序中有睡眠进程,而且在读操作的时候里面会将该读
操作的进程先加入到队列中,因此我们必须先至少执行一次读操作,然后按键才会生效。为了方便我
们操作,我们直接将读操作写在 while(1)死循环中,让应用程序一直去读,当没有中断来的时候,该
进行就会进入睡眠,因此我们不必担心卡死。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
unsigned char keyval;
int fd = 0;
/* 打开驱动文件 */
fd = open(argv[1], O_RDWR);
if (fd<0) printf("can't open %s file\n",argv[1]);
while(1)
{
read(fd, &keyval,sizeof(keyval));
printf("key_value = %d\n",keyval);
linux 驱动开发指南 | 李山文
89
}
return 0;
}
linux 驱动开发指南 | 李山文
90
第三章 基于设备树驱动模型
通过前几章讲述知道了 Linux 的驱动框架,但随着设备类型的不断增加以及处理器架构种类的多
样性,原本的驱动框架已经使得 Linux 驱动源码冗余不堪,例如每新增一个硬件就需要新建加一个驱
动代码,特别是不同的处理器需要不同的代码。Linus 本人曾斥责 Linux 内核中充斥着大量垃圾代码,
之后 Linux 开始全面使用基于设备树的驱动框架,也就是 Device Tree接下来我们将详细讲解基于设
备树的驱动框架。
3.1 设备树(Device Tree
什么是设备树呢?如其名一样,设备树就是“一棵大树”有主干和树枝以及叶子。主干相当于
系统的总线,树枝则相当于不同设备的总线,叶子相当于挂载在总线上面的具体设备。
那为何要引入设备树呢?其实不难发现一个问题,例如 2.2.1 小节的驱动中,假设现在的 LED
量由 1 个变为了 2 个甚至更多,那我们的驱动程序就需要重新编写来兼容这些改动。实际上 LED
驱动很多过程都是相同的,一个 LED 也好,两个 LED 也好,每个 LED 的初始化相同,操作也是相
同的,唯一不同的就是寄存器相关的信息,那是不是可以将这些信息记录下来,当需要初始化的时候
获取其寄存器信息,这样就驱动程序就不是那么死板了。答案是肯定的,当前的 Linux 设备树驱动框
架就是基于这种方式的,在设备树中保存的都是一些设备的寄存器信息或者数量信息,这些信息是给
驱动程序用的,当我们的设备发生小的改动后,我们不需要去修改驱动源码,而仅仅去修改设备树中
的寄存器信息即可。这样很多设备的驱动就可以共用一套驱动,这也就是为何要引入设备树的原因。
为了方便驱动开发者的开发和维护,Linux 给出了设备树的一些规范,开发者必须严格按照此规
范来编写驱动程序,下面我们来讲解一些基础的设备树知识。
3.1.1 设备树模型
在说设备树模型之前我们先来了解下总线的概念,在硬件中,总线就是实实在在的线路也被称为
控制器,比如 AHBAPB 这些总线,每个总线上会挂载一些设备,处理器通过这些总线来访问各个
不同的设备。设备树为了能够详细的描述硬件上的所有信息,也相应的有了寄存器、总线、内存、
址等这些东西。如下图所示,CPU 作为所有设备的核心,系统结构上有不同的设备总线,每个设备都
有各自的控制器和地址。CPU 可以通过这些总线访问左右设备,对于设备树模型也是类似的,驱动想
要获取初始化所有的设备就需要去获取设备树上的设备节点。所有的设备都需要在设备树中描述相关
的信息,这样驱动程序就可以从设备树中获取设备信息然后初始化设备。
设备树的引入使得 Linux 动源码可以减少不必要的重复代码,当然任何是事情有好处也有坏
linux 驱动开发指南 | 李山文
91
处,设备树的引入也使得设备驱动程序变得稍微复杂,特别是设备树的语法相对来说比较晦涩难懂,
对于刚接触 Linux 驱动开发的人更是一个绊脚石。因此下面我们将详细的讲解设备树的相关概念以及
编写方法,然后通过实际的驱动程序来深入体会设备树模型驱动框架。
3-1 SoC 硬件结构
设备树模型如下图所示,图中最上面的为根节点,每个设备树有且只有一个根节点,根节点用/
表示。根节点下面有很多其他的节点,例如时钟源节点、CPU 节点、内存节点等。CPU 下面又有很
多子节点,比如 SPI IIS 节点。这些节点实际上就是一个,驱动可以通过访问这棵树上的节点来访
问设备的信息。读者不用担心如何去访问这些节点, Linux 内核中提供了很多访问这些节点的 API
开发者可以直接利用这些函数来访问相应的设备节点,下面我们来详细讲解下设备树的语法规范。
3-2 设备树模
1. 常见节点
在设备树中定义一个节点如下:
节点名 {
};
例如我们现在需要定义一个 CPU 节点:
cpu {
};
是不是很像 C 语言的一个函数定义,我们也可以定义一个根节点如下:
/ {
linux 驱动开发指南 | 李山文
92
};
在实际的代码中一般有很多常用的节点,下面列举一些来详细说明:
1) /
该节点为根节点,每个设备树有且仅有一个根节点,所有的设备节点必须定义在根节点里面,
节点必须包含
modelcompatible#size-cells#address-cells 这四个属性,其他属性可选。后面我们会一一介绍这些
常用的属性。
2) aliases
该节点为别名节点,每个根节点下最多只能有一个 aliases 节点,且该节点只能在根节点下。例如
下面这个 aliases 节点:
aliases {
serial0 = &uart0;
};
该节点表示将 uart0 节点取一个别名为 serial0,也就是当在驱动程序中对节点 serial0 的操作实际
上就是对 uart0 节点的操作,这两个节点就是同一个节点。
3) chosen
该节点实际上没有意义,但是该节点可以用来传递系统参数,例如最常用的 bootargs 参数,我们
可以在参数中指定控制台终端等信息,如下:
chosen {
bootargs = "mem=64M console=ttyS0,115200 root=/dev/mtdblock5 rw rootfstype=ubifs";
};
2. 节点属性
节点有很多属性,该属性可以认为是这些节点的成员,因为节点存在的目的就是用来描述设备的
信息,这些信息我们称之为属性,下面列举一些常见的属性。
1) name
15
已弃用
该属性表示该节点的名字,例如下面这个设备节点:
cpu {
name = "cpu";
};
我们可以利用 of_find_node_by_name 函数来查找该节点。有些节点可能没有定义 name 属性,
dtc
16
会为该节点自动添加 name 属性,因此任何节点都有该属性,即使没有定义 name 属性,设备
树编译器也会为其分配一个 name 属性。例如下面这个节点:
uart0: serial@1c25000 {
compatible = "snps,dw-apb-uart";
status = "disabled";
};
我们没有定义其 name 属性,那么 dtc 会自动为其定义一个 serial 作为 name 属性的值。
2) compatible
该属性表示该节点能够匹配哪些驱动程序,例如下面这个设备节点:
15
name 属性已经被放弃,一般在设备树中不建议用此属性,但在内核解析设备树后其 name 仍保留。
16
dtc: device tree compiler
linux 驱动开发指南 | 李山文
93
cpu {
compatible = "arm,arm926ej-s";
};
该节点是一个 cpu 节点,这个节点中的 compatible 属性值为"arm,arm926ej-s",其中 arm 表示该
cpu 的架构是 arm 架构arm926ej-s 表示该 cpu 的具体架构是 arm9。一般来收,compatible 的值命令
规范为“芯片制造商,设备型号”。例如下面的一个节点:
watchdog@1c20ca0 {
compatible = "allwinner,suniv-f1c100s-wdt","allwinner,sun4i-a10-wdt";
};
该节点是一个看门狗设备节点
17
,节点的 compatible "allwinner,suniv-f1c100s-
wdt","allwinner,sun4i-a10-wdt"可以看到这个节点的属性值有两个,这也就是说改节点能够匹配两种
驱动程序。
3) model
该属性用来描述该设备的硬件说明,例如该设备的名称等,一般可以在根节点中用该属性描述板
子的名称:
/ {
model = "samsung, s3c2440";
};
该节点表示这个硬件板子为 s3c2440,生产厂家为 samsung
4) status
该属性用来描述该设备的状态,其值一般有如下几种:
1. disable: 表示该设备关闭,不挂载该设备驱动。
2. okay: 表示该设备打开,系统启动时挂载该设备驱动。
3. reserved表示该设备保留,由于该设备比较特殊,一般不能被修改的设备可以使用该属性值,
例如某设备的固件程序。
4. fail: 表示设备不可用,可能原因时遇到了设备故障。
5. fail-sss: 表示该设备遇到 sss 错误导致无法操作。
其中 disable okay 这两种用的最多,例如下面这个节点:
uart0: serial@1c25000 {
compatible = "snps,dw-apb-uart";
status = "disabled";
};
上面的 serial@1c25000 节点的 status 属性的值为 disable,表示该设备在系统启动时不挂载驱动,
我们可以通过修改其值为 okay 来让系统启动时挂载该设备驱动。
说明:serial@1c25000 的节点前面有一个 uart0,这个地方是为该节点添加一个引用标签,当后面
需要修改该节点的属性时,只需要引用该标签就可以了,而不需要重复定义该设备节点,例如后面
我们向将这个设备节点打开,我们可以这样:
&uart0 {
status = "okay";
};
5) #address-cells #size-cells
17
该节点名后面有一个@,这个是为了放置因为有多个相同的名为 watchdog 节点而出现重复,一般为了避免命名重复,节点可
以在节点名后加@然后加上寄存器地址来命名。
linux 驱动开发指南 | 李山文
94
这两个属性较为重要,这两个属性是用来描述子节点 reg 属性的,其值用来描述子节点中 reg
性的地址和大小。值得说明的是这#address-cells #size-cells 这两个属性一般出现在含有子节点的父
节点中。
#address-cellss:该属性表示其子节点中 reg 性的地址用多少个 32 位数来表示,例如子节点的
寄存器地址是 32 位的,那么#address-cellss 的值为 1;如果子节点的寄存器地址是 64 的,那么
#address-cellss 的值为 2
#size-cells:该属性表示其子节点中 reg 属性的地址大小占多少个 32 位,例如现在一个节点中子
节点的寄存器占内存的大小为 512 字节,则我们只需要用一个 32 就可以表示其大小,如果其值超
过了 32 位能表示的大小,则我们可以用两个 32 位来表示其大小,这是#size-cells 的值就为 2 了。
现在我们需要定义一个 32 位的寄存器,总共有 4 个寄存器,那么我们可以这样写:
cpu {
compatible = "arm,arm926ej-s";
#address-cells = <1>;
#size-cells = <1>;
gpio {
reg = < 0x01C20890 0x04
0x01C20894 0x04
0x01C208A0 0x04
0x01C208AC 0x04 >;
};
};
如果我们需表示 2 64 位的寄存器,那我们可以这样写:
cpu {
compatible = "arm,arm926ej-s";
#address-cells = <2>;
#size-cells = <1>;
gpio {
reg = < 0x0 0x01C20890 0x04
0x1 0x01C20894 0x04 > ;
};
上面的节点中 reg 的值中第一个数表示 64 位寄存器的高 32 位,第二个数表示 64 位寄存器的低
32 位,第三个表示 4 个字节的长度。
如果我们只需要表示一个寄存器的值呢?我们只需要将#size-cells 设置为 0 即可,例如现在只要
表示一个寄存器,那么可以这样写:
cpu {
compatible = "arm,arm926ej-s";
#address-cells = <1>;
#size-cells = <0>;
gpio {
reg = < 0x01C20890 > ;
};
6) reg
reg 节点和上面的#address-cells #size-cells 这两个属性紧密相关,由于 reg 属性的值是很多数的
集合,为了方便区分地址和大小信息,我们规定 reg 属性值的第一个数是起始地址,第二个数是什么
取决于#address-cells 的值,如果#address-cells 的值为 1,则第二个数是地址的长度,第三个数是什么
linux 驱动开发指南 | 李山文
95
取决于#size-cells 的值,如果#size-cells 的值为 1,则第三个数就是另一个地址的起始地址了如果
#address-cells 的值为 2,则第二个数仍然是地址,那么第三个数是地址的大小。
我们举一个简单的例子来说明这个属性:
cpu {
#address-cells = <1>;
#size-cells = <1>;
gpio {
reg = < 0x01C20890 0x04
0x01C20894 0x04 > ;
};
};
上面这个节点表示定义了两个寄存器地址,第一个寄存器为 0x01C20890,占了 4 字节的内存空
间,第二个寄存器为 0x01C20894,占了 4 个字节的内存空间。
说明:根节点必须设置 #address-cells #size-cells 这两个属性,因为设备树编译器在编译时如
果没有指定这两个属性,那么根节点下面的子节点中的 reg 就无法区分起始地址和地址长度了
#address-cells #size-cells 这两个节点没有继承属性,也就是说这两个属性仅仅只能表示其子节点
reg 属性,不能表示其子节点的子节点中 reg 属性。
7) device_type
该属性用来描述设备的设备类型,该节点为可选项,可以描述也可以不描述。例如我们用该属性
来描述一个设备节点的设备属性:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
gpio {
device_type = "gpio";
reg = < 0x01C20890 0x04
0x01C20894 0x04 > ;
};
};
除了上面的属性外,还有其他属性很重要的属性,这里暂时不去讲解,后面会放到中断系统中全
面的讲解,这里仅仅只要有简单的认识就可以了
3.1.2 获取设备树信息
上面我们已经知道了如何在设备树中定义设备了,但设备树的作用就是描述设备信息供驱动程序
使用,下面我们将详细介绍如何从设备树中获取设备节点以及设备节点的属性值。
3.1.2.1 查找节点
1) 通过节点名查找节点
Linux 内核提供了通过节点名查找设备节点的函数:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
该函数第一个参数 from 表示从哪一个节点开始查找,如果为 NULL,则认为是从根节点开始查找
linux 驱动开发指南 | 李山文
96
第二个参数是 name 表示查找的节点名;返回值为设备节点地址,如果为 NULL,则表示不存在该节
点。例如我们在设备树中定义了一个设备节点为 led,则我们可以通过该函数来获取该设备节点。
例子 设备树节点如下:
led {
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
获取 led 设备节点:
struct device_node *led_node;
led_node = of_find_node_by_name(NULL, "led");
2) 通过 device_type 查找节点
如果我们知道了节点的设备类型,我们也可以根据 device_type 属性来查找设备节点。Linux 提供
了如下函数供驱动开发者使用:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
该函数第一个参数 from 表示从哪个节点开始查找,如果为 NULL认为是从根节点开始查找;
第二个参数 type 表示根据节点属性 device_type 来查找设备节点;函数的返回值为节点地址,如果返
NULL,则不存在属性 device_type type 的设备节点。
例子 设备树节点如下:
led {
device_type = "led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
获取 led 设备节点:
struct device_node *led_node;
led_node = of_find_node_by_type(NULL, "led");
3) 根据 compatible 属性查找节点
如果设备节点中定义了 compatible 属性,我们可以根据该属性来查找对应的设备节点:
struct device_node *of_find_compatible_node(struct device_node *from, const char
*type, const char *compatible)
该函数第一个参数 from 表示从哪个节点开始查找,如果为 NULL认为是从根节点开始查找;
第二个参数 type 表示根据节点的 device_type 属性来查找设备节点,如果为 NULL,则忽略;第三个
参数 compatible 表示根据节点的 compatible 属性来查找设备节点;函数的返回值为设备节点地址,
果返回 NULL,则表示不存在该设备节点。
例子 设备树节点如下:
led {
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
linux 驱动开发指南 | 李山文
97
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
获取 led 设备节点:
struct device_node *led_node;
led_node = of_find_compatible_node (NULL,NULL, "lite200, led");
4) 根据节点路径查找节点
如果我们知道了设备节点的路径,我们也可以根据路径来查找设备节点:
inline struct device_node *of_find_node_by_path(const char *path)
该函数的参数 path 表示要查找设备节点的路径(查找的路径也可以是设备节点的别名)函数返
回值为设备节点,如果为 NULL,则表示不存在该设备节点。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
gpio {
device_type = "gpio";
reg = < 0x01C20890 0x04
0x01C20894 0x04 >;
};
};
获取 led 设备节点:
struct device_node *led_node;
led_node = of_find_node_by_path("/cpu/gpio");
3.1.2.2 查找父/子节点
有时候我们需要知道设备节点的父节点和子节点,内核也提供了相应的查找函数来供开发者使用,
下面我们逐一对其进行介绍。
1. 查找父节点
struct device_node *of_get_parent(const struct device_node *node)
该函数参数 node 表示要查找的节点,返回值为该节点的父节点地址。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
gpio {
device_type = "gpio";
reg = < 0x01C20890 0x04
0x01C20894 0x04 >;
};
linux 驱动开发指南 | 李山文
98
};
struct device_node *led_node;
led_node = of_find_node_by_path("/cpu/gpio");
struct device_node *led_parent = of_get_parent(led_node); //获取 led_node 的父节
2. 查找子节点
struct device_node *of_get_next_child(const struct device_node *node, struct
device_node *prev)
该函数的第一个参数为要查找的节点;第二个参数为要查找节点的子节点的前一个节点,如果该
参数为 NULL则表示查找第一个子节点;函数返回值为要查找的节点的子节点,如果返回为 NULL
则表示其节点的子节点不存在。
3.1.2.3 获取节点属性
驱动程序读取设备树最终要获得的是设备树中设备节点的属性值,因此获取节点属性非常重要
下面详细说明如何获取节点的属性。
1) 查找指定属性
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
该函数的第一个参数为查找属性所在的节点;第二个参数为要查找的属性名;第三个参数为要查
找的属性值的字节;函数返回值为要查找的属性。property 是设备属性结构体,里面包含了属性的一
些信息:
struct property {
char *name;
int length;
void *value;
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
我们可以用 property->name 来引用属性的名字,也可以用 property->value 来引用属性的值。为何
属性的值是一个指针变量呢
18
?因为在设备节点大部分属性的值是以字符串形式编写的。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
led {
18
设备树被内核解析之后会以树的结构保存起来,这就意味着内核为设备树上的所有设备节点已经分配的内存空间。
linux 驱动开发指南 | 李山文
99
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
};
现在我们需要获取 led 设备节点的 reg 属性值:
struct led_reg
{
u32 gpio_cfg0; u32 cfg0_size;
u32 gpio_cfg1; u32 cfg1_size;
u32 gpio_data; u32 date_size;
u32 gpio_pul0; u32 pul0_size;
}
struct property *led_property;
struct led_reg *led_reg;
struct device_node *led_node;
led_node = of_find_node_by_path("/cpu/led"); //先获取 led 设备节点
int length;
led_property = of_find_property(led_node,"reg",&length); //获取 reg 属性
led_reg = led_property->value; //获取其属性值
这样我们就可以通过 led_reg 来访问 reg 中的值了。
2) 查找属性 u32
可以看到上面我们定义了一个 struct led_reg 结构体来方便访问其值,但对于 reg 属性来说,其值
都是 u32 类型的,为了开发者更加方便访问其属性值,内核提供了读取属性 u32 类型值的函数:
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32
index, u32 *out_value)
该函数第一个参数是要查找的节点;第二个参数是属性的名字;第三个参数是属性的偏移索引位
置;第四个参数是输出的值;函数的返回值 0 表示查找成功,否则失败。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
led {
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
};
我们可以这样获取寄存器的值:
linux 驱动开发指南 | 李山文
100
u32 gpio_cfg0;
int ret;
int length;
struct device_node *led_node;
led_node = of_find_node_by_path("/cpu/led"); //先获取 led 设备节点
ret = of_property_read_u32_index(led_node,"reg",0,&gpio_cfg0);//读取 reg 的第 0 个值
if(ret != 0)
{
return ret;
}
3) 查找单值整型属性的值
有些属性只有一个整型值,我们用 of_property_read_u32_index 这个函数不太方便,内核提供了一
些专门访问只有单个值的属性函数:
int of_property_read_u8(const struct device_node *np, const char *propname,
u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname,
u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname,
u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname,
u64 *out_value)
函数的第一个参数为要查找的节点;第二个参数为属性名字;第三个参数为属性的值。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
led {
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
};
我们可以这样获取寄存器的值:
u32 gpio_cfg0;
int ret;
int length;
struct device_node *led_node;
led_node = of_find_node_by_path("/cpu/led"); //先获取 led 设备节点
ret = of_property_read_u32(led_node,"reg",&gpio_cfg0);//读取 reg 的第 0 个值
if(ret != 0)
{
return ret;
linux 驱动开发指南 | 李山文
101
}
4) 查找字符串型属性值
有些属性的值是字符串,我们就不能用上面的函数获取,内核提供了专门用于字符串类型属性的
查找函数:
int of_property_read_string(struct device_node *np, const char *propname,
const char **out_string)
该函数的第一个参数是要查找的节点;第二个参数为属性名字;第三个参数为属性的字符串值。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
led {
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
};
u32 gpio_cfg0;
int ret;
struct property *led_property;
struct device_node *led_node;
char *led_compatible;
led_node = of_find_node_by_path("/cpu/led"); //先获取 led 设备节点
int length;
ret = of_property_read_string(led_node,"compatible",&led_compatible);
if(ret != 0)
{
return ret;
}
5) 查找#address-cells #size-cells
有时候需要获取#address-cells #size-cells 属性的值,内核也提供了相应的函数:
int of_n_addr_cells(struct device_node *np)
int of_n_size_cells(struct device_node *np)
该函数的参数为要查找的节点。
例子 设备树节点如下:
cpu {
#address-cells = <1>;
#size-cells = <1>;
device_type = "cpu";
led {
compatible = "lite200, led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
linux 驱动开发指南 | 李山文
102
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
};
};
struct device_node *led_node;
char *led_compatible;
int led_address_cells;
int led_size_cells;
led_node = of_find_node_by_path("/cpu/led"); //先获取 led 设备节点
led_address_cells = of_n_addr_cells(led_node);
int led_size_cells = int of_n_size_cells(led_node);
3.1.3 设备树的编译过程
设备树的编译过程类似于 C 语言测编译过程,也是将设备树源文件编译为二进制的文件,只不
过设备树编译得到的二进制文件只能由内核进行解析。编译设备树的编译器称为 DTCDevice Tree
Compiler设备树源文件称为 DTSDevice Tree Source,编译完成生成的二进制文件称为 DTBDevice
Tree Blob,其编译流程如下图所示:
3-3 设备树编译过程
设备树的编译和内核源码的编译是独立的,但很多时候在编译内核源码的时候已经将设备树也一
起编译了,这个是因为在 Linux 源码 arch/arm/boot/dts 目录下的 Makefile 已经指定了其编译的 DTS
文件,我们也可以单独编译设备树文件。DTC 编译器在源码 scripts/dtc/目录下,命令如下:
dtc xxx.dts -o xxx.dtb
内核在启动的时候会最先解析设备树,解析完成之后会取识图挂载相应的设备驱动,需要说明的
是内核解析完设备树之后并不是设备树就不存在了,而是设备树中左右的数据结构被存放在内存中,
也就是内核会将设备树的信息全部以树链表的方式保存,这样我们使用设备树 of
19
函数就可以正常的
访问设备树中的信息。
Linux 支持的处理器繁多,很多板子使用的芯片都是相同的,但不同板子的资源又有差异,因此
在实际的设备树中会分成两个,分别是 dts 文件和 dtsi 文件。其中 dtsi 文件描述了同一种 CPU 的所有
可使用资源, dts 文件则对应着使用该 CPU 的不同板子。这样在写不同板子的 dts 文件时,只需要
C 语言一样#include <xxx.dtsi>即可添加设备节点,而在 dts 文件中则通过修改设备节点标签来开启
相应的设备节点。例如在 dtsi 中定义了如下节点:
xxx.dtsi 文件中 spi@1c05000 节点:
spi0:spi@1c05000 {
compatible = "allwinner,suniv-spi", "allwinner,sun8i-h3-spi";
reg = <0x1c05000 0x1000>;
interrupts = <0xa>;
clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_BUS_SPI0>;
clock-names = "ahb", "mod";
19
of 函数指 3.1.2 小节所讲的这些函数
linux 驱动开发指南 | 李山文
103
resets = <&ccu RST_BUS_SPI0>;
status = "disable";
#address-cells = <1>;
#size-cells = <0>;
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
};
xxx.dts 文件中开启 spi@1c05000 节点:
&spi0 {
status = "okay";
};
上面的例子中的这种方式被称为 phandle 引用,这样我们就可以使用&spi0 来引用该节点中的内
容。
3.2 驱动实例
通过之前的讲解我们已经知道了设备树的驱动框架,设备树的出现使得使用者只需要修改 dts
可适配其他不同的板子,下面我们来实际编写驱动。
3.2.1 LED 驱动
2.2.1 小节的 LED 驱动一样,需要申请设备号,注册设备,然后实现设备驱动的各个操作函
数,下面我们也使用动态分配的方式来分配设备号。之前的代码大部分都是相同的,唯一不同的就是
获取寄存器参数,由于我们使用了设备树,因此这里我们需要通过设备树 of 函数来获取相关的参数。
首先我们先在设备树文件中添加 led@0x01C20800 设备节点,打开 suniv-f1c100s-licheepi-nano.dts
件,在最后的一个节点后面添加如下:
/ {
model = "Lichee Pi Nano";
compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
};
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
led@0x01C20800 {
compatible = "lite200,led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
linux 驱动开发指南 | 李山文
104
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
};
然后在 Linux 内核源码 drivers/leds 目录下新建 leds-myled.c,在 menuconfig 中添加模块配置选项
20
之前的代码可以复用,这里不重复讲解,需要做修改,主要修改部分是__init 初始化函数部分。
因为现在我们是基于设备树框架来实现驱动程序的,因此我们的寄存器信息都是通过 of 函数来得到
的。
static int __init led_init(void)
{
int ret;
u32 GPIOE_CFG0;
u32 GPIOE_CFG1;
u32 GPIOE_DATA;
u32 GPIOE_PUL0;
struct device_node *led_node; //定义一个 led 节点
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针为 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个类
if(led_class == NULL)
{
printk(KERN_WARNING"led_class failed!\n");
return -1;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0"); //创建一个设备
if(IS_ERR(led0))
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
20
menuconfig 添加配置详细过程请看 2.3 小节
linux 驱动开发指南 | 李山文
105
led_node = of_find_node_by_path("/led@0x01C20800"); //按路径查找
if(led_node == NULL)
{
goto no_led_node;
}
if(of_property_read_u32_index(led_node,"reg",0,&GPIOE_CFG0))//led@0x01C20800 节点的
reg 的第 0 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",2,&GPIOE_CFG1))//led@0x01C20800 节点的
reg 的第 2 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",4,&GPIOE_DATA)) //led@0x01C20800 节点
reg 的第 4 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",6,&GPIOE_PUL0)) //led@0x01C20800 节点
reg 的第 6 个值
{
goto of_error;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
return 0;
of_error:
printk(KERN_WARNING"get reg failed!\n");
return -1;
no_led_node:
printk(KERN_WARNING"NO led node!\n");
return -1;
}
led_node = of_find_node_by_path("/led@0x01C20800")通过路径/led@0x01C20800 来查找设备节点,
当然也可以使用其他方式查找节点。
注:如果使用 of_find_node_by_name 函数进行查找节点,则我们的 dts 节点中需要定义 name
性,如下所示:
led@0x01C20800 {
compatible = "lite200,led";
name = " led@0x01C20800";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
linux 驱动开发指南 | 李山文
106
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
如果没有定义 name 属性,则 dtc name 属性值为 led 而不是
led@0x01C20800 ,这样按节点名字查找就查找“led ”了而不是
led@0x01C20800”。
在上面用到的所有设备节点函数需要包含#include<linux/of.h>of 函数都在这个头文件中申明
了,因此我们需要包含这个头文件。
下面是完整的代码:
dts 文件中添加的节点:
/ {
model = "Lichee Pi Nano";
compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
};
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
led@0x01C20800 {
compatible = "lite200,led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
};
leds-myled.c 文件源码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
linux 驱动开发指南 | 李山文
107
#include <linux/of.h> //设备树的 of 函数
static dev_t led_dev_num; //定义一个设备号
static struct cdev *led_dev; //定义一个设备管理结构体指针
static struct class *led_class; //定义一个设备类
static struct device *led0; //定义一个设备
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static int led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int led_close(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
int ret;
size_t status = *((volatile size_t*)gpioe_data);//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int led_write(struct file *filp, const char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
linux 驱动开发指南 | 李山文
108
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
static int __init led_init(void)
{
int ret;
u32 GPIOE_CFG0;
u32 GPIOE_CFG1;
u32 GPIOE_DATA;
u32 GPIOE_PUL0;
struct device_node *led_node; //定义一个 led 节点
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个类
if(led_class == NULL)
{
printk(KERN_WARNING"led_class failed!\n");
return -1;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0"); //创建一个设备
if(IS_ERR(led0))
linux 驱动开发指南 | 李山文
109
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
led_node = of_find_node_by_path("/led@0x01C20800"); //查找节点名为 led@0x01C20800 的节
if(led_node == NULL)
{
goto no_led_node;
}
if(of_property_read_u32_index(led_node,"reg",0,&GPIOE_CFG0)) //查找节点的 reg 属性的第
0 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",2,&GPIOE_CFG1)) //查找节点的 reg 属性的第
2 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",4,&GPIOE_DATA)) //查找节点的 reg 属性的第
4 个值
{
goto of_error;
}
if(of_property_read_u32_index(led_node,"reg",6,&GPIOE_PUL0)) //查找节点的 reg 属性的第
6 个值
{
goto of_error;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
return 0;
of_error:
printk(KERN_WARNING"get reg failed!\n");
return -1;
no_led_node:
printk(KERN_WARNING"NO led node!\n");
return -1;
}
static void __exit led_exit(void)
{
cdev_del(led_dev); //从内核中删除设备管理结构体
linux 驱动开发指南 | 李山文
110
unregister_chrdev_region(led_dev_num,1); //注销设备号
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备类
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_dev"); //简单的描述
将驱动编译进内核,然后进入根目录,执行
./led_dec.exe /dev/led0 on
可以看到此时开发板上的等亮了。然后执行
./led_dec.exe /dev/led0 off
此时开发板上的灯熄灭,这说明驱动程序没有问题。然后现在进入/pro/device-tree/目录下,查
看当前目录的所有节点,可以看到此时有我们添加的 led@0x01C20800 节点。
3-4 新建的设备节点已成功创
进入该设备节点目录下,可以看到有很多文件,使用 hexdump 命令可以看到 reg 属性的值。
3-5 新建的设备节点 reg 属性的值
linux 驱动开发指南 | 李山文
111
第四章 platform 动模型
上一章已经详细讲解了基于设备树的驱动框架,这在之前的基础上有了很大的提高。随着设备的
复杂和多元化,但我们发现很多设备虽然用途不同,但其接口定义都是一样的,例如我们使用的液晶
屏,可能液晶控制器完全不同,五花八门,但其接口无非就是 LVDSMIPI8080SPIRGBI
2
C
等。因此很多时候我们不需要去为每个设备都写一个驱动,很多设备的驱动方式都是一样的,显然驱
动是可以共用一套的。为了尽可能解决设备之前的兼容问题,Linux 之后又引入了基于 platform 驱动
模型,该驱动模型将设备抽象为总线上的节点。platform 模型又称为总线驱动模型,该模型将整个驱
动主要分为三个部分,即总线、驱动、设备。这三个部分是独立的,也就是即使没有相应的驱动,
备也可以被加载,但可能无法正常工作,同理,即使总线不存在,驱动也可能存在。总线相当于胶水,
驱动和设备相当于两个独立的物体,总线会设备和驱动粘合在一起,从而实现驱动和设备绑定。
Platform 驱动模型最大的好处就是可以让一个驱动可以与多个设备绑定,也就是多个设备可以共用一
个驱动。
4-1 总线、设备、驱动关系
为了简化开发者的工作,在总线驱动框架下产生了各种子系统,例如 pinctrl 子系统、SPI 子系统、
I
2
C 子系统、SCSI 子系统、ALSA 子系统、input 子系统、tty 子系统等等。这些子系统的产生使得整
个驱动模型变得规范化和流程化,也很大程度上提高了代码的可靠性以及可移植性。为了更好的区分,
Linux 驱动中将 platform 总线称为 platform Busplatform 设备称为 platform 称为 platform Device
platform 驱动称为 platform DriverLinux 内核驱动中定义了不同种类的 platform Bus,开发者不需要
自己去定义总线,只需要实现 platform Device platform Driver 的定义和加载。系统在启动时候会去
加载设备节点,设备节点加载的过程实际上是该设备节点挂载在一个指定的总线上,随后总线会去查
找相应的驱动,这个过程就是匹配的过程,我们称为 match
之前我们写驱动可以用一个文件来全部编写完所有的代码,现在由于采用了 platform 驱动模型,
我们比如使用驱动和设备分离的方式,因此,我们需要编写两套代码,分别是驱动和设备代码
21
。有
读者可能会问,既然是两个独立的注册过程,那应该会有一个先后关系,要不然如果驱动注册了发现
设备还未来得及注册就有问题了。其实有这种想法是正常的,但实际上这个问题是不存在的,因为不
管是驱动还是设备注册,他们都会去尝试匹配相应的驱动或者设备。例如现在注册设备时,它会去匹
配对应的驱动,但发现此时没有驱动,这时系统仅仅会将设备挂载在总线上,然后驱动开始注册了,
驱动注册时也会去匹配设备,这时它会发现有一个相应的设备与之匹配,从而完成设备和驱动的绑定。
总之,设备和驱动是两个独立的驱动,因此这两套代码都有 module_init module_exit 宏。下面我们
具体说明如何注册驱动和设备。
4.1 设备驱动注册(未引入 dts
由于历史原因,在 dts 未引入之前,所有基于 platform 驱动模型都没有采用设备树,而是在源码
21
如果已经有驱动了,就可以无需再编写驱动,只需要编写设备代码就可以了。
linux 驱动开发指南 | 李山文
112
中直接填写硬件信息,这就导致如今在 Linux 源码中也充斥着相当多的未引入 dts platform 驱动代
码,这也是 Linux 代码杂乱的一个原因。因此笔者认为有必要简单说明下如何在没有引入 dts 的情况
下采用 platform 驱动模型,读者了解了这之后再理解有 dts platform 将更容易理解。当然笔者不希
望读者以后采用这种没有 dts 的驱动框架,因为这种框架仍然存在仅仅只是历史原因,终究会被基于
dts platform 驱动模型替代。
4.1.1 驱动注册
现在我们先明白一个概念,什么是驱动,区分驱动和设备的唯一目的是让软件分层,实现硬件和
软件的独立化,这也是解耦的体现之一。驱动指除硬件相关的代码,举个简单例子,2.2.1 小节的 LED
驱动,很显然 openreadwriteclose 这些操作函数都是驱动,而与硬件相关的就是设备,例如 led
的寄存器、引脚配置这些。为了实现驱动的注册,Linux 内核提供了接口方便开发者使用。
1) platform_driver_register(struct platform_driver *drv)
该函数实现了 platform 驱动的注册,这个函数会将设备挂载在 platform 总线上,该函数的参
数是一个 platform 驱动结构体,我们在源码中可以看到如下(定义位于 include/linux/
platform_device.h 中):
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
第一个是 probe 函数指针,该函数在 platform 模型中非常重要,当驱动注册到总线上时,此时会
执行该函数来挂载该驱动以完成驱动的注册;第二个参数 remove顾名思义就是移除该驱动;第三个
参数 shutdown 是关闭该设备,该参数很少用到;第四个参数 suspend 是挂起驱动,当该设备需要被挂
起的时候会执行,一般也很少用到;第五个参数 resume suspend 对应,当设备需要从挂起转向继续
运行时会执行,也很少用到;第六个参数 driver 是该驱动给总线提供信息的,这个参数很重要,后面
会重点介绍;第七个参数 id_table 是指该驱动能够其他设备的匹配表,后面会详细介绍;第八个参数
prevent_deferred_probe 是指该驱动是否延迟挂载。
细心的读者是否观察到,该结构体的成员中的参数都是与设备相关的,这也就是驱动中需要获取
设备中的相关信息。
2) platform_driver_unregister(struct platform_driver *drv)
这个函数与 platform_driver_register 函数是对应关系,参数是同一个参数,该函数会将驱动从总
线上卸载下来。
上面两个函数可以实现驱动的注册和注销,一般注册一个驱动的步骤如下:
1. 申请一个 platform_driver 结构体
该结构体是我们的驱动注册和注销时候使用的,也就是上面两个函数的参数。这里我们可以
这样定义:
static struct platform_driver led_driver =
{
linux 驱动开发指南 | 李山文
113
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "led",
},
};
上面定义了一个 led_driver 结构体,该结构体初始化了 proberemovedriver其中 driver
目的是用来匹配设备的,因为驱动在注册到总线上时,总线需要获取一些信息用来匹配和哪
个设备进行绑定,这里的.name 就是用来和设备名相同的进行匹配。
2. 注册 platform 驱动
static int __init led_init(void)
{
ret = platform_driver_register(&led_driver);
return ret;
}
上面是驱动的注册函数,该函数会将 led_driver 驱动结构体注册到总线上。
3. 注销 platform 驱动
static void __exit led_exit(void)
{
platform_driver_unregister(&led_driver);
}
上面是驱动的注销函数,该函数会将 led_driver 驱动结构体从总线上面注销。
4. 填充 platform_driver 结构体
led_driver 结构体中初始化了很多成员,这里需要对成员进行定义,probe remove 这两个函
数需要定义的。
首先我们先实现 probe 函数:
static dev_t led_dev_num; //定义一个设备号
static struct cdev *led_dev; //定义一个设备管理结构体指针
static struct class *led_class; //定义一个设备类
static struct device *led0; //定义一个设备
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
static int led_probe(struct platform_device *pdev)
linux 驱动开发指南 | 李山文
114
{
struct resource *res;
res = platform_get_resource
22
(pdev, IORESOURCE_MEM, 0); //获取 device 中的
resource 资源
gpioe_cfg0 = ioremap(res->start,(res->end - res->start)+1);
alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
led_dev->owner = THIS_MODULE; //始化设备管理结构体的 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针为 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个名 led_class 的类
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0"); //创建一个设备名
led0
return 0;
}
然后实现 remov 函数:
static int led_remove(struct platform_device *pdev)
{
cdev_del(led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备号
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备类
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
return 0;
}
5. 指定模块入口和出口
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
上面已经完成了驱动部分的代码,由于很多操作函数在之前的章节中已经详细讲解,这里就不重
复说明了。
4.1.2 设备注册
下面我们需要完成设备的注册,Linux 将驱动和设备分离的主要目的就是希望硬件和软件能够独
立,这样修改硬件的时候就不用修改软件,也就是当设备发生变化时,我们只需要修改设备代码,
无需修改驱动代码。下面讲述其注册步骤。
Linux 内核提供了两个函数来供开发者注册和注销设备,分别如下:
1) int platform_device_register(struct platform_device *pdev);
该函数的参数为 platform 设备结构体,其定义如下:
struct platform_device {
const char *name;
int id;
22
platform_get_resource 函数是从设备中获取资源,后面会详细讲解。
linux 驱动开发指南 | 李山文
115
bool id_auto;
struct device dev;
u64 platform_dma_mask;
struct device_dma_parameters dma_parms;
u32 num_resources;
struct resource * resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
该结构体第一个成员 name 为设备名,这个设备名必须和驱动名相同,因为 platform_bus 会根据
这个名字来匹配相应的驱动程序;第二个 id,第三个参数 id_auto,第四个参数 dev 是,第五个参数
platform_dma_mask六个参数 dma_parms第七个参数 num_resources 表示该设备的资源数目,
面读者自然会明白;第八个参数 resource 示该设备的资源,主要是设备的硬件信息。第九个参
id_entry 为设备 id 入口,一般驱动中不会用到;第十个参数 driver_override 驱动名字用来强制匹配驱
动的;第十一个参数是 mfd_cell MFD cell 的指针;第十二个参数 archdata 是芯片架构特殊的额外
信息。
我们一般只用关心上面的 nameidnum_resourcesresourcedev 这五个成员,其中 name 是用
来指定该设备的设备名,用来和相同驱动名匹配的;id 一般初始化为-1 即可;num_resources 指定该
设备的资源个数;resource 来指定该设备的资源结构体数组;最后 dev 是用来指定设备相关的描述,
一般只需初始化 release 即可。
下面是注册设备的步骤:
1) 定义硬件相关资源
下面定义了 GPIOE 的寄存器地址信息:
static struct resource led_resource[] =
{
[0] = {
.start = 0x01C20890, /* GPIOE_CFG0 */
.end = 0x01C20893,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 0x01C20894, /* GPIOE_CFG1 */
.end = 0x01C20897,
.flags = IORESOURCE_MEM,
},
[2] = {
.start = 0x01C208A0, /* GPIOE_DATA */
.end = 0x01C208A3,
.flags = IORESOURCE_MEM,
},
[3] = {
.start = 0x01C208AC, /* GPIOE_PUL0 */
.end = 0x01C208AF,
linux 驱动开发指南 | 李山文
116
.flags = IORESOURCE_MEM,
},
};
static void led_release(struct device *dev)
{
/*do nothing */
}
2) 定义设备结构
下面定义了一个设备结构体如下:
static struct platform_device led_dev =
{
.name = "led", //定义名字为 led
.id = -1,
.num_resources = ARRAY_SIZE(led_resource), //资源数量
.resource = led_resource, //资源结构体数组
.dev = {
.release = led_release, //设备注销
},
}
3) 注册设备
设备结构体定义好了就需要将其注册到总线上,下面是注册设备代码:
static int led_dev_init(void)
{
platform_device_register(&led_dev);
return 0;
}
4) 注销设备
static void led_dev_exit(void)
{
platform_device_unregister(&led_dev);
}
5) 指定模块入口和出口
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
platform_get_resource (pdev, IORESOURCE_MEM, 0);
led_resource[]资源结构体中获取第一个数据。led_resource[]结构体的目的是将硬件的资源存放到里面
供驱动调用,这样设备改变了驱动程序不需要变,只需要修改设备文件即可。
从上面的代码可以看到该驱动模型将设备和驱动分为两个不同的模块,分别加载到内核中。但这
样有一个非常严重的问题,就是硬件发生了变化仍然避免不了需要修改代码重新编译内核。实际上在
第三章我们已经知道了设备树这个概念,设备树的引入就是避免这一点的。在上面代码中设备注册的
时候使用了一个 resource[]数组来存放硬件信息,其实读者应该也注意到了这些信息完全可以在设备
树中获取,而不用在总线上去匹配设备获取硬件信息。为何存在这个主要是由于在设备树未出现时候,
这种方式是一种比较好的解决方案,但当设备树出现之后,这种方案很显然已经过时了,下面我们将
linux 驱动开发指南 | 李山文
117
会重点介绍基于设备树的 platform 驱动框架。
4.2 动注册(引 dts
这一小节算是这章中最重要的小节, 4.1 节可以看到设备代码已经毫无用武之地,完全可以被
设备树替代掉了,而且设备树能够完成的更好。实际上内核在解析设备树的过程中,已经做了很多工
作,最重要的是将设备树上的节点信息填充到 device_resource[ ]组中,同时对会自动注册相应的设
备。以最简单的 LED 节点为例,LED 设备节点如下:
led@0x01C20800 {
compatible = "lite200,led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 > /* GPIOE_PUL0 */
status = "okay";
};
在系统启动之后,内核会将其进行解析,解析成功后会将其生成 device_node 并加入到设备节点
树上,之后内核又会将 device_node 转换为 platform device同时内核会将这个 platform device 注册到
platform_bus 总线上,这样我们就无需再自己写 platform_device 相关代码了。
但是并不是说所有的设备树节点最终都会转换为 platform_device,而是需要符合一下要求:
1) 该节点必须包含 compatible 属性。
2) 该节点必须包含 reg 属性或者包含 interrupt 类型属性。
3) 该节点节点为根节点的子节点。
一般来说,内核只会将根节点的子节点注册为 platform_device而根节点的子节点的子节点都将不
会被注册为 platform_device,但是仍然会将其生成 device_node。不过有一种情况需要非常注意,
当根节点的子节点其 compatible 属性为"simple-bus""simple-mfd""isa""arm,amba-bus"时,该节
点的一级子节点将会被注册为 platform_device 设备。
我们来看个例子,设备树如下:
/ {
model = "Lichee Pi Nano";
compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
};
reg_vcc3v3: vcc3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
led@0x01C20800 {
compatible = "lite200,led";
linux 驱动开发指南 | 李山文
118
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
blink@0 {
compatible = "led-blink";
};
};
uart0@0x02020000 {
compatible = "simple-bus";
status = "okay";
esp8266@0 {
compatible = "suniv-esp8266";
reg = < 0x040000 0x04>
};
nbiot@1 {
compatible = "suniv-nbiot";
reg = < 0x040000 0x04>
};
};
};
上面的 led@0x01C20800 节点会被内核注册为 platform 设备,而 blink@0 点不会被注册
platform_deviceuart0@0x02020000 节点会被内核注册为 platform 设备,同时 esp8266@0 节点和
nbiot@1 节点也会被注册为 platform 设备;因为 uart0@0x02020000 的节点中的 compatible 属性中包
simple-bus
4-2 设备树转换为 platform_device resource
linux 驱动开发指南 | 李山文
119
设备树节点注册为 platform 设备时,其设备节点的 reg 会填充到 platform_device resource 中的
IORESOURCE_MEM 中,而对于存在中断的设备树节点会被填充到 platform_device resource
IORESOURCE_IRQ。设备节点的名字会被填充到 platform_device name 属性中,如下图所示。图
中给除了一个 led 节点的填充对应图,很显然如果设备树节点中没有 reg 属性和 interrupt 属性,那么
platform 设备的 resource 资源将为空,因此内核也就不会去将该设备树节点注册为 platform_device
但仍然会添加到 device_node 中。
面可到,经无去写 platform_device
platform_driver。由于引入了设备树,因此 platform_driver 的注册也发生了改变。
我们先回顾下在未引入设备树的时候,驱动结构体如下:
static struct platform_driver led_driver =
{
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "gpio",
},
};
其中 probe 是当驱动与设备匹配时需要执行的函数,remove 是当驱动卸载时执行的函数,driver
是一个结构体用来说明该驱动的一些属性,例如名字属性。前面说过,platform_driver
platform_device 的匹配是根据 name 是否相同来匹配的,如果相同,则总线将其绑定。
4-3 driver device 匹配根据 name 属性
注意:有些设备树中的设备节点没有定义 name 属性,设备树被内核解析的时候会默认将其添
name,例如下面的设备树节点:
beep@0x114000a0{
compatible = "s3c2440,beep";
reg = <0x114000a0 0x4 0x139D0000 0x14>;
};
该节点会在内核解析的时候自动添加 name = "beep"
引入了设备树之后,我们需要的就是如何去注册 platform_driver,很显然,我们的
platform_driver name 属性必须和设备节点名字的属性相同才能保证驱动和设备是匹配的。下
面来看我们如何去注册我们的 platform_driver 了,引入设备树之后,struct device_driver
结构体又添加了一个新成员,如下:
const struct of_device_id *of_match_table;
这个成员是用来匹配设备树中节点的 compatible 属性的,当 of_match_table 中的成员与设备树
中的 compatible 属性相同时,此时内核便会将此驱动和设备树生成的 platform_device 设备绑
定,并注册驱动,那么问题来了,既然上面已经说过驱动与设备绑定是通过 name 属性,那为何还
linux 驱动开发指南 | 李山文
120
要引入 compatible 呢?实际上确实是的,内核提供了五种绑定方式,分别是
overrid,compatible 属性、ACPI 属性、ID 表以及 name 属性。这五种属性绑定是有优先级的,
其中 overrid 优先级最高,compatible 优先级其次,然后是 ACPI 样式,之后是 ID 表,最后是
name 属性。因此我们在写驱动的时候一般用 compatible 属性和 name 属性以及 ID 表这三种。
4-1 驱动与设备匹配优先级
匹配方式
优先级
overrid 重写
最高
compatible 属性
ACPI 样式
次高
ID
name 属性
现在我们已经知道了如何去匹配设备了,但是有时候我们需要将一个驱动匹配多个设备,因此内核
struct platform_driver 结构体中引入了一个新成员:
const struct platform_device_id *id_table;
该成员是一个 table,用来告诉驱动此时可以匹配多个设备,这样我们就知道了如何注册驱动了,
下面我们来举个简单的例子说明一下。
4.2.1 动的注册步
1) 添加 dts 设备节点
在编写驱动之前,我们需要为我们的驱动添加 dts 设备节点,当内核解析设备树时,此时设备将
会注册到总线上,下面是 led 的设备节点:
led@0x01C20800 {
compatible = "lite200,led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
2) 定义一个驱动结构体
在源码中定义一个 platform_driver 驱动结构体,该结构体用来注册 platform 驱动。
static struct platform_driver led_driver=
{
.probe = led_probe,
.remove = led_remove,
.driver={
.name = "led",
.of_match_table = led_match_table,
},
.id_table = led_device_ids,
};
linux 驱动开发指南 | 李山文
121
上面我们定义了一个 led_driver 驱动结构体,该结构体初始化了 probe 函数、led_remove 数、
driver 结构体和 id_table其中 probe 函数是用来当设备与驱动匹配后会执行 probe 函数,该函数中一
般用来初始化设备驱动和注册 platform 驱动。led_remove 函数是当驱动卸载后会执行的函数,一般设
备驱动的释放和注销。driver 结构体是驱动函数做匹配的结构体,也就是当驱动是以何种方式来匹配
使 name compatible 使
of_match_table 是一个列表,用来匹配多个 compatible 属性。id_table 是用来匹配设备的名字,有时候
我们希望驱动可以兼容其他的设备驱动,因此这里可以用 id_table 来指定可以兼容的设备名。
3) 定义 probe 函数
probe 函数是当驱动与设备树节点匹配后会执行的函数,这个函数一般是来注册设备设备的,例
如下面的 led 驱动 led_probe 函数:
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
int ret;
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个类
if(led_class == NULL)
{
printk(KERN_WARNING"led_class failed!\n");
return -1;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0"); //创建一个设备
if(IS_ERR(led0))
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 GPIOE_CFG0
gpioe_cfg0 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 GPIOE_CFG1
gpioe_cfg1 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 GPIOE_DATA
gpioe_data = ioremap(res->start,(res->end - res->start)+1);
linux 驱动开发指南 | 李山文
122
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 GPIOE_PUL0
gpioe_pul0 = ioremap(res->start,(res->end - res->start)+1);
return 0;
}
上面的函数需要实现 led 字符设备的申请和注册,同时实现资源的映射。
4) 定义 of_match_table 结构体
该结构体是用来匹配设备的,当内核解析设备后会生成 platform_device 设备,总线会根据这
个表来进行匹配,如果有相同的,则将该设备与驱动进行绑定,同时执行驱动的 probe 函数来试图
挂载驱动。
static struct of_device_id led_match_table[] = {
{.compatible = "lite200,led",},
};
5) 定义 id_table 结构
一般情况下一个驱动可以匹配多个设备,该 id_table 就是用来匹配多个设备用的,如下:
static struct platform_device_id led_device_ids[] = {
{.name = "led",},
};
这里仅仅支持一种设备,设备名为 lite200-led 的设备。
6) 定义 remove 函数
该函数需要实现当驱动移除时候所作的一些工作,一般是需要对设备进行释放,例如下面的
led_remove 函数:
static int led_remove(struct platform_device *pdev)
{
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
cdev_del(led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备号
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备类
return 0;
}
该函数实现了寄存器的释放,同时注销掉设备。
7) 义字符设备结构
我们这里的驱动仍然是为 led 字符设备所写的驱动,因此我们必须要为应用程序提供一个操作结
构,这个就需要我们实现 file_operations 结构体,例如下面的 led_ops 结构体:
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
linux 驱动开发指南 | 李山文
123
.release = led_close,
};
8) 现各个操作函数
我们定义了 file_operations 就需要对其进行实例化,如下:
static int led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int led_close(struct inode *inode, struct file *filp)
{
/* GPIOE 配置 */
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
int ret;
size_t status = *((volatile size_t*)gpioe_data);//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int led_write(struct file *filp, const char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
linux 驱动开发指南 | 李山文
124
return 0;
}
上面的实现较为简单,这里不再累述。
9) 定模块入口和出
static int led_driver_init(void)
{
platform_driver_register(&led_driver);
return 0;
}
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_driver"); //简单的描述
4.2.2 示例源码
设备树中的设备节点如下:
led@0x01C20800 {
compatible = "lite200,led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
下面是驱动源码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
linux 驱动开发指南 | 李山文
125
static dev_t led_dev_num; //定义一个设备号
static struct cdev *led_dev; //定义一个设备管理结构体指针
static struct class *led_class; //定义一个设备类
static struct device *led0; //定义一个设备
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static int led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int led_close(struct inode *inode, struct file *filp)
{
/* GPIOE 配置 */
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int led_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
int ret;
size_t status = *((volatile size_t*)gpioe_data);//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int led_write(struct file *filp, const char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
linux 驱动开发指南 | 李山文
126
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_close,
};
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
int ret;
led_dev = cdev_alloc(); //动态申请一个设备结构
if(led_dev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&led_dev_num,0,1,"led"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
led_dev->owner = THIS_MODULE; //初始化设备管理结构体 owner THIS_MODULE
led_dev->ops = &led_ops; //初始化设备操作函数指针 led_ops 函数
cdev_add(led_dev,led_dev_num,1); //将设备添加到内核中
led_class = class_create(THIS_MODULE, "led_class"); //创建一个类
if(led_class == NULL)
{
printk(KERN_WARNING"led_class failed!\n");
return -1;
}
led0 = device_create(led_class,NULL,led_dev_num,NULL,"led0"); //创建一个设备
if(IS_ERR(led0))
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
linux 驱动开发指南 | 李山文
127
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 GPIOE_CFG0
gpioe_cfg0 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 GPIOE_CFG1
gpioe_cfg1 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 GPIOE_DATA
gpioe_data = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 GPIOE_PUL0
gpioe_pul0 = ioremap(res->start,(res->end - res->start)+1);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
cdev_del(led_dev); //从内核中删除设备管理结构体
unregister_chrdev_region(led_dev_num,1); //注销设备号
device_destroy(led_class,led_dev_num); //删除设备节点
class_destroy(led_class); //删除设备类
return 0;
}
static struct of_device_id led_match_table[] = {
{.compatible = "lite200,led",},
};
static struct platform_device_id led_device_ids[] = {
{.name = "led",},
};
static struct platform_driver led_driver=
{
.probe = led_probe,
.remove = led_remove,
.driver={
.name = "led",
.of_match_table = led_match_table,
},
.id_table = led_device_ids,
};
static int led_driver_init(void)
{
linux 驱动开发指南 | 李山文
128
platform_driver_register(&led_driver);
return 0;
}
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_driver"); //简单的描述
将上面的代码以模块的方式编译为.ko 文件,然后修改设备树重新编译设备树文件,将编译好的 dtb
件拷贝到板子上,然后用 insmod 命令挂载.ko 驱动:
[ 11.685307] led_dev: loading out-of-tree module taints kernel.
然后在/dev 目录下查看已经存在 led0 节点:
4-4 led0 节点
现在我们重点看/sys/devices/platform 目录下的文件夹
1c20890.led serial8250 uevent
power snd-soc-dummy usb_phy_generic.0.auto
reg-dummy snd_aloop.0 vcc3v3
regulatory.0 soc
存在 1c20890.led 目录,可以看到我们的 led 驱动已经注册成功。
4.3 miscdevice 备驱动
有些设备可能并不属于字符设备、块设备、网络设备这三种,而是类似于字符设备。Linux 内核
提供了一个新的设备类型,也就是 miscdevice一般称该类型为杂项设备,但其实该设备广泛上说还
是属于字符设备,只是该设备使用的结构体不是 cdev,而是 miscdevice 结构体。
查看所有的 miscdevice 设备
23
可以在/proc/misc 文件中查看,值得注意的是,所有的 misc 设备的
主设备号都是 10,因此这样我们不需要自己指定主设备号了,misc 设备的注册和注销过程相对简单
23
后面的 miscdevice 设备都简称为 misc 设备,使用该设备 API 需要包含头文件<linux/miscdevice.h>
linux 驱动开发指南 | 李山文
129
很多,下面我们来详细说明 misc 的驱动程序编写。
4.3.1 misc 设备的注册步骤
内核为 misc 设备提供了两个重要的函数,该函数如下:
int misc_register(struct miscdevice *misc)
void misc_deregister(struct miscdevice *misc)
第一函数 misc 备的注册第二数是 misc 备的,我来看数的数,
misc_register 函数中已经对设备进行了动态分配,同时调用了 class_create device_create 实现了设备
节点的自动创建。其 misc_deregister 函数也是一样,其内部已经实现了设备节点的注销。
miscdevice 其结构体如下:
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
可以看到该结构体中包含了次设备号、设备名、设备操作结构体、设备父类等。我们一般只需要关
注前三个成员即可。这里没有主设备号是因为 misc 设备的主设备号默认是 10,因此我们无需指定
其主设备号。次设备号我们一般采用动态分配,将该 minor 赋值为 MISC_DYNAMIC_MINOR 即可。
设备名 name 是该设备注册设备节点(/dev 目录下设备文件)时的名字。然后是 fops,该结构体
相信读者比较熟悉,我们需要实现该设备的常用操作供应用程序使用。
1) 定义 misc 设备结构体
static struct miscdevice misc_led_dev = {
.minor = MISC_DYNAMIC_MINOR, //动态分配次设备号
.name = "miscled", //设备节点名为 miscled
.fops = &miec_led_fops, //文件操作集
};
上面定义了一个 misc_led_dev 结构图,该结构图指定了次设备号、设备节点名以及文件操作结构
体。
2) 定义 miec_led_fop 文件操作结构体及实现
static int misc_led_close(struct inode *inode, struct file *filp)
{
/* GPIOE 配置 */
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
linux 驱动开发指南 | 李山文
130
static int misc_led_read(struct file *filp, char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status = *((volatile size_t*)gpioe_data);//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int misc_led_write(struct file *filp, const char __user *buff, size_t count,
loff_t *offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
static struct file_operations misc_led_ops = {
.owner = THIS_MODULE,
.open = misc_led_open,
.read = misc_led_read,
.write = misc_led_write,
.release = misc_led_close,
};
3) 注册 misc 设备
我们可以使用 misc_register 函数来注册 misc 设备,下面我们以 platform 模型来实现设备的注
册:
static int misc_led_probe(struct platform_device *pdev)
{
struct resource *res;
int ret;
misc_register(&misc_led_dev); //注册 misc 设备
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 GPIOE_CFG0
gpioe_cfg0 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 GPIOE_CFG1
linux 驱动开发指南 | 李山文
131
gpioe_cfg1 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 GPIOE_DATA
gpioe_data = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 GPIOE_PUL0
gpioe_pul0 = ioremap(res->start,(res->end - res->start)+1);
return 0;
}
上面的代码在 misc_led_probe 函数中 misc_register(misc_led_dev)来注册 misc 设备,可
以看到设备的注册已经变得相当简单,只需要调用一个接口就实现了设备的注册和设备节点的创建。
4) 注销 misc 设备
static int misc_led_remove(struct platform_device *pdev)
{
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
misc_deregister(&misc_led_dev); //注销 misc 设备
return 0;
}
5) 实现平台设备结构体
static struct of_device_id misc_led_match_table[] = {
{.compatible = "lite200,misc_led",},
};
static struct platform_device_id misc_led_device_ids[] = {
{.name = "misc_led",},
};
static struct platform_driver misc_led_driver=
{
.probe = misc_led_probe,
.remove = misc_led_remove,
.driver={
.name = "misc_led",
.of_match_table = misc_led_match_table,
},
.id_table = misc_led_device_ids,
};
上面的过程与 4.2 小节一样,这里不再累述。
6) 指定模块入口和出口
static int misc_led_driver_init(void)
{
platform_driver_register(&misc_led_driver);
linux 驱动开发指南 | 李山文
132
return 0;
}
static void misc_led_driver_exit(void)
{
platform_driver_unregister(&misc_led_driver);
}
module_init(misc_led_driver_init);
module_exit(misc_led_driver_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("misc_led_driver"); //简单的描述
通过上面的步骤可以实现 misc 设备的驱动注册,下面我们来结合 led 实际测试。
4.3.2 示例源码
设备树中的设备节点如下:
misc_led@0x01C20800 {
compatible = "lite200,misc_led";
reg = < 0x01C20890 0x04 /* GPIOE_CFG0 */
0x01C20894 0x04 /* GPIOE_CFG1 */
0x01C208A0 0x04 /* GPIOE_DATA */
0x01C208AC 0x04 >; /* GPIOE_PUL0 */
status = "okay";
};
驱动源码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/miscdevice.h> //包含 misc 相关函
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
linux 驱动开发指南 | 李山文
133
static int misc_led_open(struct inode *inode, struct file *file)
{
/* GPIOE 配置 */
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
printk(KERN_DEBUG"open led!!!\n");
return 0;
}
static int misc_led_close(struct inode *inode, struct file *filp)
{
/* GPIOE 配置 */
printk(KERN_DEBUG"close led!!!\n");
return 0;
}
static int misc_led_read(struct file *filp, char __user *buff, size_t count, loff_t
*offp)
{
int ret;
size_t status = *((volatile size_t*)gpioe_data);//获取 GPIOE12 状态
ret = copy_to_user(buff,&status,4); //将内核空间拷贝到用户空间 buff
if(ret < 0)
printk(KERN_DEBUG"read error!!!\n"); //输出信息
else
printk(KERN_DEBUG"read led ok!!!\n"); //输出信息
return 0;
}
static int misc_led_write(struct file *filp, const char __user *buff, size_t count,
loff_t *offp)
{
int ret;
size_t status;
ret = copy_from_user(&status,buff,4); //将用户空间拷贝到内核空间的 status
if(ret < 0)
printk(KERN_DEBUG"write error!!!\n"); //输出信息
else
printk(KERN_DEBUG"write led ok!!!\n"); //输出信息
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
if(status)
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
return 0;
}
linux 驱动开发指南 | 李山文
134
static struct file_operations misc_led_ops = {
.owner = THIS_MODULE,
.open = misc_led_open,
.read = misc_led_read,
.write = misc_led_write,
.release = misc_led_close,
};
static struct miscdevice misc_led_dev = {
.minor = MISC_DYNAMIC_MINOR, //动态分配次设备号
.name = "miscled",
.fops = &misc_led_ops, //文件操作集
};
static int misc_led_probe(struct platform_device *pdev)
{
struct resource *res;
misc_register(&misc_led_dev); //注册 misc 设备
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 GPIOE_CFG0
gpioe_cfg0 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 GPIOE_CFG1
gpioe_cfg1 = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 GPIOE_DATA
gpioe_data = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 GPIOE_PUL0
gpioe_pul0 = ioremap(res->start,(res->end - res->start)+1);
return 0;
}
static int misc_led_remove(struct platform_device *pdev)
{
iounmap(gpioe_cfg0); //取消 GPIOE_CFG0 映射
iounmap(gpioe_cfg1); //取消 GPIOE_CFG1 映射
iounmap(gpioe_data); //取消 GPIOE_DATA 映射
iounmap(gpioe_pul0); //取消 GPIOE_PUL0 映射
misc_deregister(&misc_led_dev); //注销 misc 设备
return 0;
}
static struct of_device_id misc_led_match_table[] = {
{.compatible = "lite200,misc_led",},
};
static struct platform_device_id misc_led_device_ids[] = {
{.name = "misc_led",},
};
linux 驱动开发指南 | 李山文
135
static struct platform_driver misc_led_driver=
{
.probe = misc_led_probe,
.remove = misc_led_remove,
.driver={
.name = "misc_led",
.of_match_table = misc_led_match_table,
},
.id_table = misc_led_device_ids,
};
static int misc_led_driver_init(void)
{
platform_driver_register(&misc_led_driver);
return 0;
}
static void misc_led_driver_exit(void)
{
platform_driver_unregister(&misc_led_driver);
}
module_init(misc_led_driver_init);
module_exit(misc_led_driver_exit);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("misc_led_driver"); //简单的描述
将上面的源码以模块的方式编译成.ko 文件,然后写入内核的根文件系统中,同时编译设备树
文件,将编译好的 dtb 文件替换原来的 dtb 文件,启动开发板,进入根文件系统,然后使用
insmod 命令挂载 misc_led.ko,如下:
[ 20.876898] misc_led_dev: loading out-of-tree module taints kernel
/dev 目录下可以看到有一个 miscled 设备节点创建:
4-5 miscled 设备节点
linux 驱动开发指南 | 李山文
136
led_dev.exe
24
应用测试可以看到驱动正常工作。
查看下 miscled 的设备号:
srw-rw-rw- 1 root root 0 Jan 1 00:00 log
crw------- 1 root root 1, 1 Jan 1 00:00 mem
crw------- 1 root root 10, 62 Jan 1 00:00 miscled
crw------- 1 root root 14, 0 Jan 1 00:00 mixer
crw------- 1 root root 14, 16 Jan 1 00:00 mixer1
可以看到该设备的主设备号为 10,次设备号为 62,现在我们查看/proc/misc 文件内容:
# cat /proc/misc
62 miscled
63 cpu_dma_latency
可以看到开发板中 misc 设备有两个,分别是 miscled cpu_dma_latency
利用 misc 提供的方式注册设备可以很大简化驱动程序。
24
应用程序使用的是 2.2 小节中的测试用例。
linux 驱动开发指南 | 李山文
137
第五章 中断系统
5.1 中断控制器
2.4 小节将到一个按键中断驱动实现,中断使用的是 INTC。如下图所示,这是最原始的中断
控制器连接方式,但随着中断数量的不断增加,简单的中断控制器已经无法满足应用需求,因此中断
控制器开始出现了级联。
5-1 最原始的中断控制器连接方式
如下图所示,每个中断控制器之间可以相互连接,这样当外设有中断触发时,此时最先会触发与
之相连的中断控制器,然后中断控制器再去触发级联的中断控制器,最后触发处理器。这样的级联有
个好处就是可以大大增加中断的数量,同时外部中断可以进行合理的管理。
5-2 中断控制器级联方式
级联的方式无疑为开发者提供的更多的中断入口,但是这也会带来一个问题,我们如何去管理这
些中断呢?为此 Linux 内核中引入了“域”的概念,即“domain。每个中断控制器都被划分为一个
区域,每个区域管理自己的中断,这样我们的中断号就出现了“中断号+域”
5-3 中断域
为了统一中断号,Linux 内核引入的虚拟中断号,即软中断号(sw IRQ vIRQ)而通过数据手册
查到的中断号为硬件中断号(hw IRQ)。后面我默认 IRQ 为软中断号,hw IRQ 为硬件中断号。
但是中断控制器与中断控制器之间如何联系起来呢?就是硬件号和软件中断之间
linux 驱动开发指南 | 李山文
138
映射的?Linux 内核为此引入了中断控制器映射概念
25
,映射方式目前有四种,分别是 Linear map
Radix Tree mapNo map Legacy map
Linear map即线性映射,这种映射实际上是一个简单的线性表,索引是硬件中断号,值为软件
中断号。
5-4 线性映射
Linux 内核提供如下接口:
1. irq_domain_add_linear()
2. irq_domain_create_linear()
Radix Tree map:即基数树,这种映射就是将每个 domain 用基数树连接起来,目前使用这种
映射的平台不多。Linux 内核提供如下接口:
1. irq_domain_add_tree()
2. irq_domain_create_tree()
No map:有些中断控制器比较自带映射能力,也就是可以直接把硬件中断号保存在中断控制器
中,这样就不需要实现软中断和硬中断之间的映射了,Linux 内核提供如下接口:。
1. irq_domain_add_nomap()
Legacy map:即传统映射。当硬件中的 hwirq 号是可编程的时候,就可以采用无映射类型。
这种情况下,最好将 Linux IRQ 号编入硬件本身,这样就不需要映射了,Linux 内核提供如下接
口。
1. irq_domain_add_simple()
2. irq_domain_add_legacy()
3. irq_domain_add_legacy_isa()
4. irq_domain_create_simple()
5. irq_domain_create_legacy()
大部分情况下使用的是 Linear map 映射方式,只有非常特殊的才使用其他方式。、
Linux 内核中中断控制器的概念比价广泛,一个可以复用为外部中断的 GPIO 也可以当作一个中
断控制器,同理一个 UART 中断也可以当作一个中断控制器,这些中断控制器可以通过级联的方式来
实现桥接。
5.2 GIC 中断控制器
为了更好的管理中断,ARM Cortex A7 列的推出之后就开始使 GICGeneric Interrupt
Controller)即通用中断控制器。当前仅仅在比较老的 SoC 中仍存在 INTC 传统的中断控制器。
25
相关文档请查看 Linux 源码:Documentation\translations\zh_CN\core-api\irq\irq-domain.rst
linux 驱动开发指南 | 李山文
139
5-5 GIC 中断控制
GIC 的出现其最大的目的是解决多核处理器之间的中断线路问题,下图是 Cortex A7 中的多核中
断示意图。
5-6 Cortex A7 多核中断系统示意图
可以看到,每个核心都会连接在 SCU 上,中断信号会先经过 GIC 控制器,然后将信号传输给 SCU
信号探测控制单元。GIC 的发展也经历了很多变化,从最开始的 GIC-V1 版本到后面的 GIC-V4 版本,
后面我们以 GIC-V2 版本来讲解,下面我们将来详细介绍 Linux 驱动开发中的中断系统。
即上面所提到的,GIC-V2 是通用中断控制器的第二个版本,这个版本主要在 A7A8A9 处理
linux 驱动开发指南 | 李山文
140
器中使用,不仅针对多核处理器,也可以用于单核处理器。GIC-V2 版本主要有两个部件,分别是中
断分发器和处理器接口,这两个部件是处理器的核心部分,GIC-V2 增加了虚拟化接口,当然这里不
对其进行详细说明,我们主要关心与 Linux 相关的部分进行讲解。
下图是 GIC-V2 的框图,从图中可以看到,这是一个八核处理器,每个核心都有各自的中断号。
分发器中最上面有一个 32~1019 的中断号,该终端号是共享中断号,下面我们来分析下这个框图。
首先对于每个处理器而言都有各自的中断源,从图中可以看到 PPIs FIQ 以及 IRQ,而 FIQ
IRQ 是直接连接到处理器的,没有经过分发器,FIQ IRQ 编号为 0~15,这里我们重点看 PPIs
SPIs
PPIsPrivate Peripheral Interrupt即私有外设中断,该中断连接到外设上,每个处理器核心都
有各自的私有外设中断;中断号为 16~31
SPIsShared Peripheral Interrupt即共享外设中断,该中断是共享中断号的,对每个处理器而
言都是可见的;中断号为 32~1019
从图中可以看到所有的外部中断和内部中断都由 GIC 来管理,每个核都有各自的 FIQ IRQ
PPIs,所有的核共用 SPI 中断。
5-7 GIC 通用中断控制器框架
linux 驱动开发指南 | 李山文
141
GIC 可以很好的解决多核中断问题,但随着处理器的复杂程度增加,一个 GIC 可能不够用了,
出现了多个 GIC。为了更好的管理多个 GICLinux 使用 GIC Domain 来管理这些 GIC 的连接方式,
即级联。
5-8 GIC 级联
GIC 的级联和 5.1 中讲到的是一样的,也是通过映射方式来实现软键中断号和硬件中断号绑定。
5.3 中断控制器驱动
我们先看下 Linux 内核中中断系统的架构,如下所示:
5-9 Linux 中断结构体
从上图中可以看到,Linux 中断系统实现需要依赖于中断控制器驱动程序,也就是说,只有中断
控制器驱动实现了 Linux 中断系统才能正常工作,因此我们在使用中断之前一定要保证中断控制器驱
动能够成功加载。目前 Cortex-A 系列之后全部使用 GIC 中断控制器,因此 Linux 内核为其实现了一
套完整的 GIC 控制器驱动源码,我们只需要在设备树中添加 GIC 中断控制器节点即可。例如我们使
用的是 Cortex A7 内核 SoC根据数据手册可以查找到其中断控制器使用的是 GIC-400
26
因此我们
在设备树中需要添加 GIC-400 的中断控制器设备节点,如下所示:
gic: interrupt-controller@1c81000 {
compatible = "arm,gic-400";
26
更多详细内容请看 Linux 内核源码 Documentation\devicetree\bindings\interrupt-controller\arm,gic.yaml
linux 驱动开发指南 | 李山文
142
reg = <0x01c81000 0x1000>,
<0x01c82000 0x2000>,
<0x01c84000 0x2000>,
<0x01c86000 0x2000>;
interrupt-controller;
#interrupt-cells = <3>;
interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
};
首先 compatible="arm,gic-400"指定其需要匹配的中断控制器驱动,这里我们使用
arm,gic-400 即可,该驱动时 Linux 内核提供的通用驱动;reg 指定了其中断控制器的寄存器,
GIC 控制器的寄存器一般而言其大小都是通用的。一般来说,GIC 控制器有分发配置寄存器
GICD)、CPU 接口寄存器(GICIF)、用于执行访问处理器的虚拟接口控制块寄存器、用于地址
位选择处理器的虚拟接口控制块寄存器。这四个寄存器的大小空间分别是 0x10000x2000
0x20000x2000。这些都是 GIC 控制器规定好的,因此我们在填写 GIC 设备节点的时候需要按照
这种格式来编写,这样 Linux 内核中的 GIC 驱动程序就可以完成初始化。interrupt-controller
表示该 GIC 设备节点是一个中断控制器。interrupts 属性指定了该 GIC 中断控制器的中断号,这
里使用的是 PPI 9 号中断线,这里使用 9 号是由于 Linux 规定了的。
5.4 设备树中断节点解析
上述已经对中断进行了比较详细的讲解,从简单的中断控制器到级联以及后面出现的 GIC 中断
控制器,这些放在一起很容易将读者弄糊涂。这里再对上面做一个简单的说明,首先, ARM-Cortex
A7 之前的处理器没有 GIC 控制器,但仍然有多个中断控制器,因此比较老的 SoC 一般使用的没有带
GIC 的方式,而之后带有 GIC 中断控制器的 SoC 编写驱动程序的时候需要考虑 GIC 中断框架。不管
是没有 GIC 处理器的还是有 GIC 中断控制器的 SoC,其都使用的 IRQ Domain 的概念,因此我们在
注册中断号是需要创建 domain
我们在注册中断时候首先需要将硬件中断号与软件中断号关联起来,GIC 中断控制器由于其中断
非常多,从上面的图可以看到中断多达 1020 个,如果事先全部为这些中断号分配一个虚拟中断号,
这将会占中很多空间。实际上在开发的时候中断比较少,不会全部使用,需要用到的中断可能也就十
几个。因此 Linux 内核在初始化中断映射的时候并不会将虚拟中断和硬件中断的连接关系全部分配,
仅仅只去将设备树dtb文件中定义的中断进行映射。因此我们在使用中断之前必须在设备树中定义
中断节点,某则无法正常注册中断
27
5-9 系统启动后解析设备树并映射 hwIRQ vIRQ
27
对于一些比较老的驱动代码,其硬件中断号与软件中断号已经写死在代码中,因此其关联已经全部完成了。
linux 驱动开发指南 | 李山文
143
上图中的过程前提是在设备树中已经实现了 GIC 中断控制器设备节点,目的是让系统起来之后
能够解析设备树并初始化 GIC 中断控制器。一般而言,GIC 中断控制器的驱动是兼容的,都是 gic-
400
读者也许会问,我们是否可以自己去设置硬件与软件中断号的关联,是的,确实是可以的,只是
这个过程比较复杂,涉及到 domain 的操作接口,这个将会在本章后面详细讲解,此处先来实现一个
简单的按键中断。
5.5 中断节点属性
1) interrupt-controller
该属性表示该设备节点是一个中断控制器,有些 GPIO 也可以是一个中断控制器,因此中断控制
器的改变不仅仅局限于物理上的中断控制器;该属性没有值,只是一个标签。
例子 设备树节点如下:
pinctrl@1c20800 {
compatible = "xxx, xxx-pinctrl";
reg = <0x01c20800 0x400>;
interrupt-controller;
}
上面定义了一个 pinctrl@1c20800 的设备节点,节点中的 interrupt-controller 表示该设
备节点是一个中断控制器,这意味着该接地可以作为其他中断控制器节点的父节点。
2) interrupts
该属性指定了该设备节点的中断属性,例如中断号、中断触发标志(方式)。该节点的值的格式
由父中断节点中的 interrupt-cells 决定。
例子 设备树节点如下:
intc: interrupt-controller@1c20400 {
compatible = "xxx, xxx-inct";
reg = <0x01c20400 0x400>;
interrupt-controller;
#interrupt-cells = <1>;
};
根节点如下:
/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
此处省略无关代码
uart0: serial@1c25000 {
compatible = "snps,dw-apb-uart";
reg = <0x01c25000 0x400>;
interrupts = <1>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu CLK_BUS_UART0>;
resets = <&ccu RST_BUS_UART0>;
status = "disabled";
linux 驱动开发指南 | 李山文
144
};
此处省略无关代码
}
interrupt-controller@1c20400 #interrupt-
cells=<1>;,这样中断控制器的子节点以及孙子节点都是只有一个 cell。在根节点中必须使用
interrupt-parent=<&intc>来指定子设备节点的中断控制器。同时在子节点里面的 interrupts
性只能有一个 cell
例子 设备树节点如下:
gic: interrupt-controller@1c81000 {
compatible = "arm,gic-400";
reg = <0x01c81000 0x1000>,
<0x01c82000 0x1000>,
<0x01c84000 0x2000>,
<0x01c86000 0x2000>;
interrupt-controller;
#interrupt-cells = <3>;
interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
};
根节点如下:
/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&gic>;
此处省略无关代
wdt0: watchdog@1c20ca0 {
compatible = "allwinner,sun6i-a31-wdt";
reg = <0x01c20ca0 0x20>;
interrupts = <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&osc24M>;
};
}
上面定义了一个 interrupt-controller@1c81000 中断控制器节点,其中#interrupt-cells
= <3>;,这说明该子中断节点有 3 cell。在 watchdog@1c20ca0 节点中 interrupts 的值有三
个单元,分别是 GIC_SPI ,该值表示这是一个 SPI 共享中断;25 表示共享中断的 ID 25
IRQ_TYPE_LEVEL_HIGH 表示中断触发标志(方式)为电平触发。
注意:对于 SPI 中断类型,从图 5-7 可以看到 SPI 中断 ID 32~1019这里 25 是相对值,也即是
实际的中断号为 25+32=57,这个中断号是硬件中断号,通过查看数据手册可以发现中断号确实是
57
对于 GIC 中断控制器而言,其驱动一般的兼容的, arm,gic-400因此如果确定 SoC 中的
中断控制器是 GIC,则一般都可以使用该中断控制器节点,只需要修改其寄存器即可。
3) interrupt-parent
该属性指定节点的父中断节点,对于根节点一般都会有这个属性。对于级联的中断而言,需要用
该节点来指定父中断节点。
例子 设备树节点如下:
linux 驱动开发指南 | 李山文
145
/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
此处省略无关代码
}
上面根节点中 interrupt-parent 指定了根节点使用的中断控制器为 intc 标签所指定的节点。
根节点里面的子节点如果没有指定其父中断节点,则默认表示继承根节点的中断控制器。
4) #interrupt-cells
该属性指定了中断控制器的 interrupts 属性的单元数,对于 INTC 中断控制器一般为 1,即只
有一个 cell,而对于 GIC 中断控制器一般为 3,即有三个 cell。详细示例见 interrupts 属性。
5) gpio-controller
现在处理器的 GPIO 都可以作为一个中断控制器,也就是说 GPIO 既可以作为 IO 引脚,也可以
作为一个外部中断控制器。
例子 设备树节点如下:
pio: pinctrl@1c20800 {
compatible = "allwinner,sun8i-v3s-pinctrl";
reg = <0x01c20800 0x400>;
此处省略无关代码
gpio-controller;
#gpio-cells = <3>;
}
上面的 pinctrl@1c20800 节点中定义了该节点是一个 gpio-controller。因此这些 GPIO 都是中断控
制器。
5.6 中断 API
Linux 内核提供了中断接口函数,这些函数包括中断的申请、释放、失能、使能等等,下面我们
逐一对其进行讲解。
5.6.1 中断注册
Linux 内核中所有的资源使用前都需要先申请,中断也不例外,因此我们在使用中断之前必须
申请中断。Linux 内核提供了多种中断申请函数,如下:
1) request_irq(unsigned int irq, irq_handler_t handler, unsigned long
flags,const char *name, void *dev)
该函数实现了用户向内核申请一个中断
irq:该参数是硬件中断号,可以通过数据手册查看此值。
handler:该参数是中断句柄,也就是中断触发后执行的函数。
flag:该参数表示中断触发标志(方式),断触发方式有多种,Linux 中提供了如下触发
方式:
IRQF_TRIGGER_NONE:无触发中断,指采用默认触发或者之前设置的方式
IRQF_TRIGGER_RISING:上升沿触发
linux 驱动开发指南 | 李山文
146
IRQF_TRIGGER_FALLING:下降沿触发
IRQF_TRIGGER_HIGH:高电平触发
IRQF_TRIGGER_LOW:低电平触发
IRQF_TRIGGER_MASK:所有电平都可触发
IRQF_TRIGGER_PROBE:触发式检测中断
IRQF_SHARED:共享中断
还有其他触发方式这里不一一列举。
name:中断名,此值会在/proc/interrupts 中断列表中查看。
dev:设备 ID,只要是唯一的即可。
示例:
request_irq(62, btn_irq, IRQF_TRIGGER_FALLING, "btn",(void*)&key_value);
该例子申请一个硬件中断号为 62 的中断,中断服务函数为 btn_irq,触发方式为下降沿触发,
中断名为 btn,中断设备 ID key_valuestatic unsigned int key_value)。
2) request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t
thread_fn,unsigned long flags, const char *name, void *dev);
该函数提供了一个线程化中断申请 API,其主要目的是减少中断的中执行的时间,该函数将一
个中断服务函数拆分为两个部分,分为上半部和下半部。上半部在中断服务中执行,下半部在
中断服务外执行。
handler:该参数和 request_irq 中的参数一样,表示中断触发后会执行的中断服务函数,
这个函数表示中断的上半部。
thread_fn:该参数是中断的下半部,当中断的上半部执行完毕后会执行中断下半部函数。
其他参数和 request_irq 函数中的参数一样。
3) devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t
handler,unsigned long irqflags, const char *devname, void *dev_id)
该函数和上面的函数一样,唯一不同的是新增加了一个参数 dev,该参数与一个设备进行绑定。
4) devm_request_threaded_irq(struct device *dev, unsigned int irq,irq_handler_t
handler, irq_handler_t thread_fn,unsigned long irqflags, const char
*devname,void *dev_id);
该函数提供了一个线程化中断申请的 API,参数和上面一样,这里不再累述。
上面的所有中断的申请 API 其功能都是一样的,唯一不一样的就是对于线程化的中断而言,其拆
分为了两部分(上半部和下半部)。将执行时间短且重要的任务放到上半部,不是那么紧急的任务放
到下半部。
5-10 线程化中
当中断处理任务很简单时,我们可以使用普通的中断申请函数即可,当中断任务比较复杂时,
们需要使用线程化中断来拆分任务。Linux 内核中还提供了其他大量的中断申请函数,这里不对其一
一说明。
platform 驱动中,中断号需要通过设备树来获取,Linux 内核专门提供了一个函数来获取平台
设备的中断,函数原型如下:
int platform_get_irq(struct platform_device *dev, unsigned int num)
linux 驱动开发指南 | 李山文
147
该函数可以根据 platform 设备来直接获取设备树中的中断号,需要注意的是该函数返回的是
硬件真实中断号,在设备树中标注的设备号实际上是相对的。
dev:该参数为 probe 函数传入的参数,即平台设备。
num:该参数为需要申请的中断号索引,表示中断号 interrupt 列表第 1 个。
这个函数根据设备树中对应设备节点中的 interrupts 属性来获取中断号,详细如何获取可阅
Linux 源码实现过程。
5.6.2 中断释放
中断申请后就需要对中断进行释放,因为中断是处理器中的共有资源。Linux 也提供了响应的操
作函数,如下:
1) extern const void *free_irq(unsigned int, void *);
该函数与 request_irq 函数对应,用来释放 request_irq 函数申请的中断号。第一个参数为中
断号,第二个参数为中断设备 ID
2) extern void devm_free_irq(struct device *dev, unsigned int irq, void
*dev_id);
该函数与 devm_request_irq 函数对应,用来释放 devm_request_irq 函数申请的中断。第一
个参数为设备。其他参数与上一个参数相同。
5.6.3 中断开关
5.6.3.1 关闭单个中断
有时候我们想控制一个中断,在一定时间内关闭中断,然后再开启中断,Linux 内核提供了标注
的开关中断的函数,如下:
1) extern void disable_irq(unsigned int irq);
2) extern void disable_irq_nosync(unsigned int irq);
3) extern void enable_irq(unsigned int irq);
1 2 函数的不同在于 2 是立刻关闭中断,而 1 是等待当前的中断结束后再关闭中断;关闭和开启中
断的参数都是一样的,即申请的中断号。
5.4.3.2 关闭全局中断
Linux 内核还提供了关闭和开启全局中断的函数,如下:
1) #define local_irq_enable() do { raw_local_irq_enable(); } while (0)
2) #define local_irq_disable() do { raw_local_irq_disable(); } while (0)
3) #define local_irq_save(flags) do { raw_local_irq_save(flags); } while (0)
4) #define local_irq_restore(flags) do { raw_local_irq_restore(flags); } while
(0)
1 2 是关闭全局中断,3 4 是开启全局中断。
linux 驱动开发指南 | 李山文
148
5.7 按键示例
上面我们已经对中断进行了详细的讲解,从中断控制器的发展过程到现在 ARM 广泛使用的 GIC
中断控制器,但并没有对其进行具体的编程讲解,因此下面我们来通过一个示例来具体讲解如何使用
中断。
5.7.1 ADC 按键原
说到按键可能读者比较熟悉矩阵按键和独立按键,在单片机开发中通常使用的是矩阵按键,矩阵
按键的原理较为简单,这里不对其进行过多讲解。独立按键也就是通过读取响应的 IO 电平来判断按
键是否按下,这个过程有中断的方式也有轮询的方式。
5-11 独立按键和矩阵按键
不管是独立按键还是矩阵按键,他们的原理都是通过判断 IO 电平来实现判断哪个按键按下。这
种按键在单片机中用的非常广泛。但对于 SoC 而言,其 IO 资源非常紧缺,因为处理器的任务繁重,
其外部挂载的设备通常很多,因此 IO 资源是非常宝贵的。目前 SoC 所使用的按键并不是基于上面这
种方式实现的,而是利用 ADC 实现按键的采集。原理也比较简单,学过数电模电的读者应该对此有
或多或少的了解。ADC 也就是模拟/数字转换器,它可以将模拟信号转换为数字信号。
5-12 ADC 按键原理图
如上图所示,该电路是一个简单的电阻分压电路,Key1~Key5 各自连接到不同的节点上。在没有
任何按键按下时,此时 ADC 检测点处的电压为 VCC 电压;当 Key1 按键按下时,此时电压为 0V
Key2 按键按下时,此时电压为
;当 Key3 按键按下时,此时电压为
。这样我们只需
要一个 ADC 就可以实现多个按键的扫描。例如一个 10 位的 ADC 可以实现 1024 个按键,但这仅仅
是理论上,实际会有很多干扰。
知道了 SoC 中按键的实现原理,我们就可以利用其内部的 ADC 实现自己的按键驱动程序。一般
linux 驱动开发指南 | 李山文
149
来说,所有的按键最好是利用中断方式,因为很多设备在大多数情况下不会被触发,而仅仅在有人交
互的时候才触发,这样的好处是可以减少不必要的处理器资源占用。
5-13 LRADC
28
读取按键流程
5.7.2 驱动序编写
由于 GIC 控制器在解析设备树的时候完成软件中断号与硬件中断号的映射,因此我们必须在设
备树中定义中断号,否则我们需要自己实现软硬件中断号的映射过程。全志 V3s 芯片的中断号表如
下:
5-1 GIC 控制器中断描述
中断号
向量地址
中断源
描述
62
0x00F8
LRADC
LRADC 中断
这样我们的中断号为 62注意这是绝对的中断号,从数据手册上可以看到,此中断为 SPI 类型。
根据 GIC 驱动程序可以发现,所有的 SPI 类型被编号为中断号=绝对中断号-32这样我们的设备树节
点如下:
mykey: mykey@1c22800 {
compatible = "mykey";
reg = <0x01c22800 0x4>, <0x01c22804 0x4>, <0x01c22808 0x4>, <0x01c2280c 0x4>;
interrupts = <GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH>;
status = "okay";
};
首先我们定义了一个 mykey@1c22800 的设备节点,然后 compatible="mykey"作为平台设备匹
配值,寄存器用 reg 属性标注,这里定义了四个寄存器,分别是 LRADC 控制寄存器、中断控制寄存
器、中断状态寄存器、数据寄存器。interrupts 属性指定了该设备节点的中断信息,这里的 GIC_SPI
表示该中断为 SPI 中断类型,30
29
表示 SPI 中断号为 30IRQ_TYPE_LEVEL_HIGH 表示该中断的触发
方式为高电平触发。status 属性指定该设备节点使能,下面我们来实现驱动程序。
这里需要注意一个问题,就是我们在写测试应用程序的时候需要去获取按键值,但是应用程序没
有中断这一概念,因此我们何时去获取中断呢?对于应用程序来收最简单也最直接的办法就是不断的
去读取按键(文件),但这就会有一个问题,如果大部分情况下没有中断呢,那应用程序将会一直
无用功,这样效率很低。那我们如何去做让效率更高呢?我们可以先去读按键,驱动程序会立刻让其
睡眠,等到有中断到来时再将其唤醒。Linux 提供了一个等待队列,这个队列就是用来存放需要睡眠
的任务。当有中断到来时,我们可以在中断句柄中将其唤醒。等待队列如下:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
这个是一个宏,我们可以用这个宏来定义一个我们自己的等待队列。如下:
DECLARE_WAIT_QUEUE_HEAD(btn_waitq);
28
LRADC Low Resolution ADC,低分辨率 ADC
29
这里的 SPI 中断号 = 绝对中断号 32,即 LRADC 中断号 = 62 – 32 = 30
linux 驱动开发指南 | 李山文
150
我们定义了一个 btn_waitq 的等待队列,等待队列实际上是一个链表结构,其结构如下图所示:
5-14 等待队列
我们可以使用这面两个函数来实现等待队列中任务的唤醒和睡眠。
1) wait_event_interruptible(wq_head, condition)
wq_head condition
condition=0 表示休眠,condition=1 表示唤醒。
2) wake_up_interruptible(x)
该函数是将 x 等待队列的所有任务全部唤醒,下面举一个简单的例子来说明这个函数的使用。
DECLARE_WAIT_QUEUE_HEAD(btn_waitq); //定义一个等待队列头
...
static unsigned int cdn_flag = 0;//定义一个变量,记录等待队列标志
...
static irqreturn_t xxx_irq(xxx)
{
...
wake_up_interruptible(&btn_waitq); /* 唤醒休眠的进程,即调用 xxx_read 进程 */
cdn_flag = 1;
...
return IRQ_HANDLED;
}
static ssize_t xxx_read(xxx)
{
...
wait_event_interruptible(btn_waitq, cdn_flag);
...
cdn_flag = 0; //设置为休眠状态
return 0;
}
上面的代码中 cdn_flag 为一个状态标志变量,给 wait_event_interruptible 函数用的。开始
时候该标志位 0,意味着 xxx_read 进行会进入休眠状态,当中断到来时,此时中断函数中的
wake_up_interruptible 函数将会唤醒 btn_waitq 等待队列即 xxx_read 进程。
linux 驱动开发指南 | 李山文
151
5-15 进程与等待队列
下面我们将会使用等待队列来完成按键驱动的唤醒和睡眠应用。驱动框架我们使用平台设备驱动
模型即 platform 框架,使用这个驱动框架的目的是我们不必自己来实现硬件中断号与软件中断号的映
射关系,这个过程由 GIC 驱动来做了(在解析 DTB 文件时,我们只需要在设备树中指定 interrupts
属性即可。
首先在 probe 函数中实现驱动的注册以及中断号的申请:
static int btn_probe(struct platform_device *pdev)
{
此处省略设备注册以及
IO
重映射
ret = devm_request_irq(&pdev->dev, platform_get_irq(pdev, 0), btn_irq, 0, "mykey",
(void*)&key_value);
if(ret)
{
return ret;
}
return 0;
}
下面我们对 devm_request_irq 中的参数进行说明
platform_get_irq(pdev, 0)
IO 使
devm_request_irq 函数,该函数前面及讲解过,这里着重说明下 platform_get_irq 这个函数。
这个函数是获取硬件中断号,第一个参数是平台设备参数;第二个是中断号的 Index如果为 0 表示
获取设备树节点的 interrupts 属性中的中断列表第 1 个。
5-16 获取中断号过程简图
linux 驱动开发指南 | 李山文
152
platform_get_irq 函数返回的是一个硬件中断号,例如我们使用的是 LRADC其硬件中断号为 62
则此时该函数返回值为 62
5-17 系统启动后解析设备树自动填充设备资源信息
上图中 GIC_PPI 的硬件绝对中断号=GIC_PPI 编号+16,为什么是这样读者可以回顾 5.2 小节内
容,CIC SPI 类型中断编号为 32~1019,为共享中断号;GIC PPI 类型中断编号为 16~31,为外
使 使
platform_get_irq(pdev, 0)就是获取 timer_resource [0]的值。
btn_irq
在上面的 probe 函数中 btn_irq 表示中断服务函数,当中断触发时,此时会去执行该函数,函
数的定义如下:
static irqreturn_t btn_irq(int irq, void *dev_id)
{
wake_up_interruptible(&btn_waitq); /* 唤醒休眠的进程,即调 read 函数的进程 */
ev_press = 1;
*keyadc_ints |= ((1<<0)|(1<<1) | (1<<2) |(1<<3)|(1<<4));
return IRQ_HANDLED;
}
该函数的定义较为简单,因此没有使用线程化方式。首先进入中断后立刻唤醒 btn_waitq 等待
队列,此时会将等待队列中的进程从等待队列中移除。ev_press = 1 用来设置此时按键按下,这
个值后面的 read 进程中会使用该值,用来判断此时进程是否进入休眠状态。中断函数中需要对一
些标志位进行清楚,这里为了简单,将多有的标志位全部清 0
"mykey"
该值定义了中断号的名称,我们可以用 cat /proc/interrupts 来查看中断号列表,此时会看
到我们注册的中断名。
(void*)&key_value
该值是一个任何类型的变量,只要是唯一就可以了,实际上这里就是提供一个 32(假设为 32
处理器)的内存空间用来存放中断的 ID
probe 函数已经分析完了,我们再来看 read 函数,该函数实现如下:
linux 驱动开发指南 | 李山文
153
static ssize_t btn_cdev_read(struct file * file, char __user * userbuf, size_t count,
loff_t * off)
{
int ret;
unsigned int adc_value;
adc_value = *(keyadc_data);
//将当前进程放入等待队 button_waitq ,并且释放 CPU 进入睡眠状态
wait_event_interruptible(btn_waitq, ev_press!=0);
ret = copy_to_user(userbuf, &adc_value, 4);//将取得的按键值传给上层应用
printk(KERN_WARNING"key adc = %d\n",(adc_value&(0x3f)));
printk(KERN_WARNING"key statue = 0x%x\n",*(keyadc_ints));
ev_press = 0;//按键已经处理可以继续睡眠
if(ret)
{
printk("copy error\n");
return -1;
}
return 1;
}
实现过程也比较简单,首先定义一个变量来存放 LRADC 的数据,然后使用
wait_event_interruptible 函数来将等待队列中的进程从等待队列中移除并继续执行该进程(注
意:此时的 ev_press=1 满足唤醒条件)。然后将一些数据打印出来并拷贝到用户空间。
open 函数的实现需要将 LRADC 进行初始化,例如初始 ADC 的采样周期、触发模式、首次转换延
时时间等。
static int btn_cdev_open (struct inode * inode, struct file * file)
{
*keyadc_intc |= ((1<<0)|(1<<1) | (1<<2) |(1<<3)|(1<<4));
*keyadc_ctrl |= (1<<0);
return 0;
}
这里我们仅仅开启了中断控制器的使能,其他参数默认即可。目前我们已经基本实现了按键驱
动的重要部分,具体的完整代码如下:
设备树中设备节点如下(根节点):
/ {
model = "Lichee Pi Zero";
compatible = "licheepi,licheepi-zero", "allwinner,sun8i-v3s";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
};
linux 驱动开发指南 | 李山文
154
mykey: mykey@1c22800 {
compatible = "mykey";
reg = <0x01c22800 0x4>, <0x01c22804 0x4>, <0x01c22808 0x4>, <0x01c2280c 0x4>;
interrupts = <GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH>;
status = "okay";
};
};
我们在设备树中的根节点中定义了一个 mykey@1c22800 设备节点,节点中的 reg 使用了另一种写
法,这两种写法是等价的,例如:
reg = <0x01c22800 0x4>, <0x01c22804 0x4>, <0x01c22808 0x4>, <0x01c2280c 0x4>;
等价与:
reg = < 0x01c22800 0x4
0x01c22804 0x4
0x01c22808 0x4
0x01c2280c 0x4>;
需要注意的是在.dtsi 文件中已经定义过一个根节点,其中已经定义了 interrupt-parent =
<&gic>,所有这里的 dts 文件中的根节点就不必重新定义了。
驱动代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/irq.h> //含有 IRQ_HANDLED IRQ_TYPE_EDGE_RISING
#include <linux/interrupt.h> //含有 request_irqfree_irq 函数
static dev_t btn_cdev_num; //定义一个设备号
static struct cdev *btn_cdev; //定义一个设备管理结构体指针
static struct class *btn_class; //定义一个设备类
static struct device *btn; //定义一个设备
ssize_t volatile *keyadc_ctrl;
ssize_t volatile *keyadc_intc;
ssize_t volatile *keyadc_ints;
ssize_t volatile *keyadc_data;
static unsigned int ev_press; //一个全局变量,记录中断事件状态
DECLARE_WAIT_QUEUE_HEAD(btn_waitq);//注册一个等待队 button_waitq,用宏来申明一个全局变量
static unsigned int key_value = 0; //定义一个变量保存按键值
static irqreturn_t btn_irq(int irq, void *dev_id)
{
linux 驱动开发指南 | 李山文
155
wake_up_interruptible(&btn_waitq); /* 唤醒休眠的进程,即调 read 函数的进程 */
ev_press = 1;
*keyadc_ints |= ((1<<0)|(1<<1) | (1<<2) |(1<<3)|(1<<4));
return IRQ_HANDLED;
}
static int btn_cdev_open (struct inode * inode, struct file * file)
{
*keyadc_intc |= ((1<<0)|(1<<1) | (1<<2) |(1<<3)|(1<<4));
*keyadc_ctrl |= (1<<0);
return 0;
}
static int btn_cdev_close(struct inode * inode, struct file * file)
{
return 0;
}
static ssize_t btn_cdev_read(struct file * file, char __user * userbuf, size_t count,
loff_t * off)
{
int ret;
unsigned int adc_value;
adc_value = *(keyadc_data);
//将当前进程放入等待队 button_waitq ,并且释放 CPU 进入睡眠状态
wait_event_interruptible(btn_waitq, ev_press!=0);
ret = copy_to_user(userbuf, &adc_value, 4);//将取得的按键值传给上层应用
printk(KERN_WARNING"key adc = %d\n",(adc_value&(0x3f)));
printk(KERN_WARNING"key statue = 0x%x\n",*(keyadc_ints));
ev_press = 0;//按键已经处理可以继续睡眠
if(ret)
{
printk("copy error\n");
return -1;
}
return 1;
}
static struct file_operations btn_cdev_ops = {
.owner = THIS_MODULE,
.open = btn_cdev_open,
.read = btn_cdev_read,
.release = btn_cdev_close,
};
static int btn_probe(struct platform_device *pdev)
{
linux 驱动开发指南 | 李山文
156
int ret;
struct resource *res;
btn_cdev = cdev_alloc(); //动态申请一个设备结构体
if(btn_cdev == NULL)
{
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}
ret = alloc_chrdev_region(&btn_cdev_num,0,1,"button"); //动态申请一个设备号
if(ret !=0)
{
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}
btn_cdev->owner = THIS_MODULE; //初始化设备管理结构体的 owner THIS_MODULE
btn_cdev->ops = &btn_cdev_ops; //初始化设备操作函数指针为 led_ops 函数
cdev_add(btn_cdev,btn_cdev_num,1); //将设备添加到内核中
btn_class = class_create(THIS_MODULE, "button"); //创建一个名为 led_class 的类
if(btn_class == NULL)
{
printk(KERN_WARNING"btn_class failed!\n");
return -1;
}
btn = device_create(btn_class,NULL,btn_cdev_num,NULL,"button0");//创建一个设备名 led0
if(IS_ERR(btn))
{
printk(KERN_WARNING"device_create failed!\n");
return -1;
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 LRADC_CTRL
keyadc_ctrl = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 LRADC_INTC
keyadc_intc = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 LRADC_INTS
keyadc_ints = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 LRADC_DATA
keyadc_data = ioremap(res->start,(res->end - res->start)+1);
ret = devm_request_irq(&pdev->dev, platform_get_irq(pdev, 0), btn_irq, 0, "mykey",
(void*)&key_value);
if(ret)
{
return ret;
}
return 0;
}
linux 驱动开发指南 | 李山文
157
static int btn_remove(struct platform_device *pdev)
{
iounmap(keyadc_ctrl); //取消 LRADC_CTRL 映射
iounmap(keyadc_intc); //取消 LRADC_INTC 映射
iounmap(keyadc_ints); //取消 LRADC_INTS 映射
iounmap(keyadc_data); //取消 LRADC_DATA 映射
cdev_del(btn_cdev); //从内核中删除设备管理结构体
unregister_chrdev_region(btn_cdev_num,1); //注销设备号
device_destroy(btn_class,btn_cdev_num); //删除设备节点
class_destroy(btn_class); //删除设备类
return 0;
}
static struct of_device_id btn_match_table[] = {
{.compatible = "mykey",},
};
static struct platform_device_id btn_device_ids[] = {
{.name = "btn",},
};
static struct platform_driver btn_driver=
{
.probe = btn_probe,
.remove = btn_remove,
.driver={
.name = "btn",
.of_match_table = btn_match_table,
},
.id_table = btn_device_ids,
};
module_platform_driver(btn_driver);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("btn_driver"); //简单的描述
上面的代码中使用了 module_platform_driver(btn_driver)来设置模块的入口和出口,这里其
实是等价与_init 的,如下:
module_platform_driver(btn_driver)等价与
module_init(btn_driver_init);
module_exit(btn_driver_exit);
linux 驱动开发指南 | 李山文
158
5.7.3 动测试程序
上面已经将驱动程序编写好了,现在我们需要编写应用程序来测试驱动是否能正常工作。
测试程序如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
unsigned int keyval;
int fd = 0;
/* 打开驱动文件 */
fd = open(argv[1], O_RDWR);
if (fd<0) printf("can't open %s file\n",argv[1]);
while(1)
{
read(fd, &keyval,sizeof(keyval));
printf("key_value = %d\n",keyval);
}
return 0;
}
上面的程序中有一个比较特殊的地方就是使用了 while(1)这个死循环,此时的目的是希望我们不断
的去获取按键的值,之前说过,此时当去执行 read 进程时,此时如果没有按键按下,进程会立刻进
入等待队列(休眠),直到中断触发后,进入中断服务函数,然后唤醒 read 进程,此时会将按键相
关的数据传给用户空间。这样看,实际上并没有一直去读,而是没有按键按下就会让出处理器。
我们以模块的方式编译成.ko 文件,然后拷贝到根文件系统中,使用 insmod 命令挂载驱动。
# insmod key_dev_platform.ko
[ 108.384137] key_dev_platform: loading out-of-tree module taints kernel.
此时模块已经挂载成功,然后我们查看下/dev 目录下是否有我们创建的设备节点。
# ls /dev/
bus ptye9 ptyu2 ptyzb ttya3 ttypc ttyv5
button0 ptyea ptyu3 ptyzc ttya4 ttypd ttyv6
console ptyeb ptyu4 ptyzd ttya5 ttype ttyv7
fd ptyec ptyu5 ptyze ttya6 ttypf ttyv8
full ptyed ptyu6 ptyzf ttya7 ttyq0 ttyv9
gpiochip0 ptyee ptyu7 random ttya8 ttyq1 ttyva
kmsg ptyef ptyu8 rtc0 ttya9 ttyq2 ttyvb
可以看到,我们的 button0 设备已经成功注册,这样我们就可以利用测试程序来测试该驱动。
linux 驱动开发指南 | 李山文
159
# ./key_cdev.exe /dev/button0
此时终端像卡住了一样,这个是正常的,因为在读操作的时候没有按键按下,此时读进程进入休
眠,现在我们按下任意一个按键,可以看到此时终端有数据输出:
[ 8669.024862] key adc = 63
[ 8669.027443] key statue = 0x0
key_value = 63
[ 8669.040508] key adc = 24
[ 8669.043056] key statue = 0x0
key_value = 24
[ 8669.056100] key adc = 24
[ 8669.058645] key statue = 0x0
key_value = 24
[ 8669.071744] key adc = 24
[ 8669.074288] key statue = 0x0
key_value = 24
可以看到确实输出了数据,但是有个问题,就是我们明明只按了以下,却输出了这么多次数据,是
的,这个是正常的。首先读者应该会第一时间想到是不是没有做消抖,想到这个是正常的。
5-18 按键机械抖动
ADC 62.5Hz
1/62.5=0.016s=16ms,这个和抖动时间差不多,因此我们可以看到这个问题并不是抖动造成的,这个
是由于触发模式造成的。全志的 LRADC 提供了三种触发模式,分别是正常触发、单次触发和连续触
发。我们默认使用的是正常触发,这种触发方式特点是一旦按键触发到来,就会一直读取 LRADC
值,直到按键释放。
因此我们可以修改为单次触发模式,只要在 open 函数中添加一行即可:
static int btn_cdev_open (struct inode * inode, struct file * file)
{
*keyadc_intc |= ((1<<0)|(1<<1) | (1<<2) |(1<<3)|(1<<4));
*keyadc_ctrl |= (1<<12); //设置单次触发模式
*keyadc_ctrl |= (1<<0);
return 0;
}
这样我们按下或释放的时候就只会触发一次,但是测试发现有一个小问题,按键只能在按下的时候
获取到的 LRADC 是正确的,释放的时候不对。这个并不是程序问题,而是这款芯片的 LRADC 确实
只能在按下的时候读取到的 LRADC 是对的,那我们怎么办?很简单,我们只要记录下按下时候的键
值就可以了,具体实现会在输入子系统中看到。
linux 驱动开发指南 | 李山文
160
第六章 sysfs 设备模型
前面所用到的驱动框架都是基于 udev 设备文件系统框架的(Linux 2.6 版本之前),在 Linux 2.6
版本之后开始引入了 sysfs 设备文件系统,在内核目录中可以看到有/sys 目录,该目录就是 sysfs 文件
系统目录。该文件系统是基于内存的一种虚拟设备文件系统,只有在内核启动后才存在,当机器断电
后其文件系统将全部消失。传统的文件系统时基于硬盘存储的,即使掉电也仍然存在为何要引入
sysfs 呢?其主要原因是因为 sysfs 以硬件资源为模型,非常直观的反应硬件的拓扑结构,因此当前较
为提倡的是在基于 udev 框架的同时也使用基于 sysfs 设备文件系统来编写驱动。
6.1 sysfs 设备文件系统
sysfs 设备文件系统与 udevproc 以及 devpty 是同一类别的文件系统,该文件系统是一个虚拟的
文件系统, sysfs 是一种基于 ramfs 实现的内存文件系统。在开发板中进入/sys 目录下,查看其内容
如下:
block class devices fs module
bus dev firmware kernel power
其中 block 目录包含所有的块设备;class 录包含所有的设备类;devices 目录包含所有存在的
设备,fs 目录包含了当前内核挂载的所有文件系统;module 目录包含了内核挂载了所有模块;bus
录包含了内核中所有的总线设备类型;dev 目录下有两个目录,分别是 block char 目录,该目录包
含了所有的/sys/devices 目录中所有设备的设备号信息;fireware 目录包含对固件对象(firmware object)
和属性进行操作和观察的接口,即这里是系统加载固件机制的对用户空间的接口(关于固件有专用于
固件加载的一套 API)kernel 目录存放当前内核版本所有可以更改的参数;power 目录包含所有电源
管理的文件,用户可以通过对该目录下的文件进行读写来控制系统电源。
6-1 sys 文件结构
为例更好熟悉 sysfs 文件系统,我们来看一个现有的驱动,在/sys/class/gpio 目录下有一个已经实
现的 gpio 实例,我们可以通过文件对其进行操作。
export gpiochip0 unexport
linux 驱动开发指南 | 李山文
161
其中 export unexport 就是 gpio 的用户接口,我们现在来操作一个 GPIOE12
30
。首先利用重定
向对 export 进行写操作:
echo 140 > /sys/class/gpio/export
此时/sys/class/gpio 目录下会生成一个 gpio140 目录。然后对 gpio140 目录中的 direction 写入 out
这样就可以设置了 gpio140 为输出模式:
echo out > /sys/class/gpio/gpio140/direction
可以看到此时 LED 等亮其,因为此时默认 GPIO 输出为低电平。我们现在可以来设置其电平控
GPIO 的输出状态:
echo 1 > /sys/class/gpio/gpio140/value
这样我们就将输出电平设置为高电平,此时 LED 熄灭,同样,我们可以设置其输出电平为低电
平,此时 LED 亮起:
echo 0 > /sys/class/gpio/gpio140/value
上面的一些列操作实际上是 sysfs 文件系统留给用户空间的接口,用户可以对文件进行需要的操
作。可以看到虽然其目录结构和 dev 目录有不一样,但其还是提供用户的接口,这也体现了 Linux
一切皆文件的设计思想。
/sys 下的录每个都 kobject那什 kobject 下面将相 kobject
kobj_type kset这三个数据类型是 Linux 的核心,也是 Linux 驱动的精髓,不过这里不会太深入讲
解其内部的实现,这里只讲解下这三者与 sysfs 的联系。
不用将这三个对象想象的太过复杂,实际上这三个对象是用来描述 sysfs 文件系统的结构关系。
文件系统的结构包括文件夹和文件,对于 sysfs 文件系统而言,kobject 就是用来描述文件夹的的对象,
kobj_type 就是用来描述文件的对象。而文件夹可能存在多个子文件夹,因此又用 kset 来将多个子
文件夹联系一起。例如下图所示:
6-2 ksetkobjectkobj_type 三者与文件系统的对应关系
图中有三个文件夹和四个文件,三个文件夹的层次关系图中已经比较清楚,首先文件夹 A 下面
有两个文件夹 B C对于文件夹 A 而言,其必定有一个 kobject 来表示,但是文件夹 B C 隶属于
文件夹 A,因此文件夹 A 还需要用 kset 表示其隶属关系。同时文件夹 B C 下各自有两个文件,
这两个文件用 kobj_type 来表示。文件夹我们只要是用来存放文件的,但这些文件是用来干嘛的?细
心的读者也许通过上面的 gpio 子系统的操作能够猜到这些文件可能是留给用户的操作接口,没错,
这四个文件确实是留给用户操作的接口,这些文件直接暴露给用的进行内核空间的读写,下面我们来
30
Linux 内核将 gpio 编号,每 32 个为一组编号,GPIOA5 = 0*32+5=5GPIOE12= 4*32+12=140
linux 驱动开发指南 | 李山文
162
详细讲解这三个对象的具体使用。
6.1.1 kobject
Linux 内核中蕴含着大量的面向对象的编程思想,大部分的驱动代码中有非常多的相同部分,
内核开发者将这些相同部分的结构归并在一起成为一个父类,这个父类就是 kobject。所有的驱动源
码都要包含这个父类,只是对于不太的类型设备进行了更上层的封装罢了。下面是 Linux 5.7 内核中
定义的 kobject 结构体:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};
上面的结构体中 name 指定 kobject 对象的名字,即在 sys 目录下创建一个目录的名字,我们可以
用;enrty kobject head_list 链表结构的入口;parent 指向当前 kobject 对象的父类;kset 指定了当
kobject 对象在
常用的 kobject 函数有下面这些:
1) void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
该函数对 struct kobject *kobj 结构体进行初始化,LDD3
31
中明确建议在使用 kobject
前需要对其进行初始化,否则会出现意想不到的错误。
2) int kobject_add(struct kobject *kobj, struct kobject *parent, const char
*fmt, ...)
该函数是将 kobject 结构体添加到其父类中,可以看到这是一个变参函数,由于这些函数我们
很少用到,因此这里不去细究。
3) int kobject_init_and_add(struct kobject *kobj, struct kobj_type
*ktype,struct kobject *parent, const char *fmt, ...)
该函数是将上面两个函数封装在一起了,即初始化 object 对象并添加到其父类中。
4) struct kobject *kobject_create(void)
该函数是创建一个 kobject 对象,函数的返回值为对象地址,该函数是动态创建的,而且该函
数在创建的过程中已经将 kobject 进行了初始化。
31
LDD3Linux Device Driver Linux 设备驱动,这时 Linux 官方发布的设备驱动开发权威手册
linux 驱动开发指南 | 李山文
163
5) struct kobject *kobject_create_and_add(const char *name, struct kobject
*parent)
该函数是创建一个 kobject 对象同时将该对象添加到其父类中,这个函数其实就是上面的多个
函数封装,当然,该函数在创建类的过程中已经对其进行了初始化。
该函数的第一个参数是需要创建 kobject 对象的名字;第二个参数是创建对象后将其添加到哪
kobject 对象上来作为创建对象的父类。
6) int kobject_set_name(struct kobject *kobj, const char *fmt, ...)
该函数用来设置 kobject 对象的名字,这个函数也是一个变参函数。
7) void kobject_put(struct kobject *kobj)
该函数与 kobject_create_and_add 函数相反,将对象删除。
上面我们用的最多的就是 kobject_create_and_add 这个函数,如下
struct kobject *kob = kobject_create_and_add("led",NULL);
这里的第二个参数没有指定其父类,而是 NULL,也就是该 kobject 没有父类,其为最顶层的一
Kobject,也就是在/sys 目录下直接创建。
6-3 创建 kobject 对象过程
6.1.2 kset
kset 是包含了多个 kobject 的集合,也就是说 kset 用来打包 kobject,例如现在我们需要在
/sys 目录下出创建多个目录或者文件,那么就需要使用 kset 来指定这些目录在同一级目录下。下
面是该结构体的具体内容:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
} __randomize_layout;
其中 list 成员将 kset 下所有的 kobject 全部连接起来,组成一个双向链表;list_lock
示其中的自旋锁的 kobject;然后 kobj 为该 kset kobject 对象,也是该 kset 包含的所有
kobject 的父类,这样看来实际上 kset 就是一个 kobject 对象;uevent_ops uevent 的操作
函数集合,该成员是用来指定 uevent 事件的操作函数的,当 kset 中的任何一个 kobject 发生变
linux 驱动开发指南 | 李山文
164
化时,就会执行 uevent_ops 中的函数。
6-4 kset 图示
上图对 kset 进行了说明,从图中可以很清晰的看到 kset 本质上是一个包含了多个 kobject 对象的
kobjectkset 常用的函数如下:
1) static struct kset *kset_create(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj)
该函数创建一个 kset 对象,第一个函数为要创建的 kset 的名字;第二个函数要创建 kset uevent
事件的操作函数;第三个函数为该 kset 的父类。
2) void kset_init(struct kset *k)
该函数用来初始化一个 kset 对象。
3) int kset_register(struct kset *k)
该函数用来注册一个 kset其中参数为 kset这个函数会为 kset 创建 uevent 事件,该事件类型为
KOBJ_ADD即一个添加对象的事件,同时该函数还会为这个对象创建一个文件夹,其文件夹的名称
kset name 成员决定。
4) struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj)
该函数会动态创建一个 kset 对象,第一个参数为该 kset 对象的 name第二个参数为 kset 对象的
uevent 事件的操作函数集合;第三个参数是该 kset 的父类。可以看到这个函数完成了上面几个函数的
功能。
5) struct kobject *kset_find_obj(struct kset *kset, const char *name)
该函数是在一个 kset 查找一个 kobject 对象,查找方式为按名字查找;第一个参数为要在哪个
kset 中查找;第二个参数为要查找的 kobject name
6) static void kobj_kset_join(struct kobject *kobj)
该函数是将一个 kobject 对象加入到一个 kset 中。
7) static void kobj_kset_leave(struct kobject *kobj)
该函数为要将 kset 中的一个 kobject 对象移除出去。
可以看到 6 7 这两个函数都是 static 的函数,也就是外部不能够访问,因此驱动开发者会用的较少,
但这个两个函数很重要。
8) void kset_unregister(struct kset *k)
该函数将注销一个 kset 对象,注销过程包括删除该 kset 对象,同时删除其 uevent 操作集
合,并且删除这个 kset 的文件夹。
我们一般会调用 kset_create_and_add 函数创建一个 kset 对象:
linux 驱动开发指南 | 李山文
165
struct mykset = kset_create_and_add("led",&led_uevent_ops,NULL);
6.1.3 kobj_type
kobj_type 又是一个什么东东呢?我们先来看下这个结构体的具体内容:
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
}
可以看到该结构体成员很陌生,都是没有见到的结构,下面我们来简单介绍下这个结构体成员。
首先第一个 kobject 指定了该 kobj_type 释放时候的 kobject 对象以及所占用的资源;第二个参数
sysfs_ops kobj_type default_attrs
default_attrs,一般包含 show store 这两个方法;后面两个成员基本用不到,这里不对其进行说明。
可以看到 kobj_type 指定了对 sysfs 的最基本的操作,主要由 show store 这两个方法,注意,
两个方法非常重要,后面一定会常常用到的。下面我们再来说名下 default_attrs
6.1.4 attribute
该结构体表示 kobj_type 的属性,为何要描述 kobj_type 的属性呢?因为在 sysfs 是以文件和文件
夹来管理的,那么这些文件和文件夹都会存在属性,例如文件名、文件的读写权限、文件的分组等等。
这些信息都需要对其进行管理,因此内核提供了这个 attribute 属性。
struct attribute {
const char *name;
umode_t mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};
上面的 name 为属性的名字,即要创建的文件名;mode 为属性的模式,也就是权限,这个权限和
Linux 下的文件所属的权限相同。和属性相关的操作函数如下:
1. int sysfs_create_file(struct kobject *kobj, const struct attribute *attr)
该函数会为 kobject 创建一个属性,这个属性就是该 kobject 向用户空间提供的接口,表现为文件
操作。
例如下面这个例子:
static struct kobj_attribute led_attr = __ATTR(led_status,0660,led_show,led_store);
sysfs_create_file(led_kobj,&led_attr_k.attr);
上面创建了一个 led_attr 属性,其属性名字为 led_status,操作权限为 0660show 函数为
led_show,
linux 驱动开发指南 | 李山文
166
store 函数为 led _store,然后将这个属性赋给 led_kobj 这个对象。这样 led_kobj 对象就存在
这个属性了,这个属性会以文件的形式存在。上面的代码会创建一个名为 led_status 的文件。
上面使用了一个__ATTR 的宏,这个宏实际上是各个成员的封装,具体如下:
#define __ATTR(_name, _mode, _show, _store) { \
.attr = {.name = __stringify(_name), \
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
.show = _show, \
.store = _store, \
}
我们也可以这样使用而不使用这个宏:
static struct kobj_attribute led_attr = {
.attr.name = "led_status",
.attr.mode = 0660,
.show = led_status_show,
.store = led_status_store,
};
6-5 属性文件创建过程
上面的效果是一样的,里面有两个函数需要说明下,分别是 show store 这两个函数。首先
show 这个函数:
static ssize_t led_show(struct kobject* kobjs,struct kobj_attribute *attr,char *buf)
{
printk(KERN_INFO "Read led\n");
return sprintf(buf,"The led_status status = %d\n",led_status);
}
这个函数实现了 led 的读操作,当我们使用 cat 命令读取这个 led_status 属性时,此时就会
执行这个函数。同样的道理,对于写操作也是一样的:
static ssize_t led_store(struct kobject *kobj, struct kobj_attribute *attr,const char
*buf, size_t count)
linux 驱动开发指南 | 李山文
167
{
printk(KERN_INFO "led status store\n");
if(0 == memcmp(buf,"on",2))
{
gpio_set_value(LED_PIN,1);
led_status = 1;
}
else if(0 == memcmp(buf,"off",3))
{
gpio_set_value(LED_PIN,0);
led_status = 0;
}
else
{
printk(KERN_INFO "Not support cmd\n");
}
return count;
}
上面这个函数时 led_status store 函数,当使用 echo on > led_status 时,此时会执
行这个函数。
注:kobj_attribute attribute 不一样,前者包含后者,内容如下:
struct kobj_attribute {
struct attribute attr;
ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,char *buf);
ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count);
};
上面使用了一个 memcmp 函数,该函数是一个比较函数,当第一个参数与第二个参数相等时,
此时结果为 0,第三个参数为字节长度,例如 on 这里占了 2 个字节,off 占了 3 个字节。
上面的函数实际上就是来为一个 kobject 对象创建一个属性,这个属性是以文件的形式存在,同
时该属性具有可读写特性,下图展示了该函数的功能:
6-6 属性示意
linux 驱动开发指南 | 李山文
168
2. void sysfs_remove_file(struct kobject *kobj,const struct attribute *attr)
该函数是上面的函数相反操作,这个函数会移除 kobj 的属性,同时会删除这个属性文件。
有时候我们希望能够同时创建多个属性,这样我们就需要调用多个 sysfs_create_files 函数,
如下所示:
static struct kobj_attribute led_attr_A =
__ATTR(led_A_status,0660,led_A_show,led_A_store);
static struct kobj_attribute led_attr_B =
__ATTR(led_B_status,0660,led_B_show,led_B_store);
sysfs_create_files(led_kobj,&led_attr_A.attr);
sysfs_create_files(led_kobj,&led_attr_B.attr);
为了简单操作,内核还提供了一个更方便的函数,也就是下面这个函数:
3. int sysfs_create_group(struct kobject *kobj,const struct attribute_group
*grp)
这个函数实现了对一个 kobject 对象赋值多个属性,使用方法如下:
static struct kobj_attribute led_attr_A =
__ATTR(led_A_status,0660,led_A_show,led_A_store);
static struct kobj_attribute led_attr_B =
__ATTR(led_B_status,0660,led_B_show,led_B_store);
static struct attribute *led_attr[] =
{
&led_attr_A,
&led_attr_B,
NULL,
};
static struct attribute_group my_led_attr_g =
{
.attrs = led_attr,
};
sysfs_create_group(led_kobj,&my_led_attr_g);
注意:上面的*led_attr[]中最后一个必须要写 NULL
6.2 建属性文件 API
虽然上面我们介绍了创建属性文件的函数 sysfs_create_file,但这个函数实在太过裸露和危
险,实际上在 Linux 内核中,提供了一个更安全的创建属性文件的函数,该函数原型如下:
int device_create_file(struct device *dev,const struct device_attribute *attr)
这个函数实现了一个属性文件的创建,但该函数是在/sys/devices/目录下创建文件,具体可以
看该函数的实现过程,其内部仍然是调用了 sysfs_create_file 函数。
同样也有删除属性文件接口:
void device_remove_file(struct device *dev,const struct device_attribute*attr)
linux 驱动开发指南 | 李山文
169
上面这个两个函数的参数和之前的 sysfs_xxx_file 不太一样,上面两个函数的第一个参数是设备
指针,一般指向 device->dev第二个参数是设备文件的属性, Linux 中,提供了一个专门的宏来创
建设备属性:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
我们一般使用这个宏来定义我们的设备属性,其参数说明如下:
_name:要创建属性文件的名字
_mode:属性文件的读写权限
_show:属性文件的读操作函数
_store:属性文件的写操作函数
例如我们创建一个属性文件:
static DEVICE_ATTR(ads7846, 0660, ads_show, ads_store);
static int ads7846_probe(struct spi_device *spi)
{
/*省略无关代码*/
device_create_file(&spi->dev, &dev_attr_ads7846);//这里根据 DEVICE_ATTR 宏可知属性变量为
dev_attr_name
/*省略无关代码*/
}
上面的代码中,第一步先定义属性文件的属性参数,例如文件名、读写权限、读操作函数、写操作函
数。上面我们创建的属性文件的文件名为“ads7846,读写权限为 660
相应的,对于 DEVICE_ATTR show store 函数定义如下:
static ssize_t show_xxx(struct device *dev,struct device_attribute *attr,char *buf)
{
}
static ssize_t store_xxx(struct device *dev,struct device_attribute *attr,const char *buf,
size_t count)
{
return count;
}
Linux 中,不仅仅提供了设备属性,还提供了驱动属性和总线属性,其宏为 BUS_ATTR(_name, _mode,
_show, _store) DRIVER_ATTR(_name, _mode, _show, _store),相应的创建文件的函数有:
int bus_create_file(struct bus_type *, struct bus_attribute *);
void bus_remove_file(struct bus_type *, struct bus_attribute *);
int driver_create_file(struct device_driver *, const struct driver_attribute *);
void driver_remove_file(struct device_driver *, const struct driver_attribute *);
由于在 Linux 内核中用的比较少,因此这里我们不对其进行详细讲解,读者可自行阅 Linux 内核源码。
6.3 LED 驱动示例
下面以一个 LED 的简单例子对 sysfs 统一设备模型进行说明,为了让示例尽可能简单,这里使用
linux 驱动开发指南 | 李山文
170
最直接的操作寄存器方式。
1) 创建 kobject 对象文件夹并为该对象创建一个属性文件
创建对象文件夹和属性文件一般在驱动的初始化过程中完成,这里我们放到__init 函数中,如下:
static int __init sysfs_led_init(void)
{
int ret;
led_kobj = kobject_create_and_add("sys_led",NULL);
if(led_kobj == NULL)
{
printk(KERN_INFO"create led_kobj failed!\n");
return -1;
}
ret = sysfs_create_file(led_kobj, &led_attr.attr);
if(ret != 0)
{
printk(KERN_INFO"create sysfa file failed!\n");
return -1;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
return 0;
}
上面的 kobject_create_and_add("sys_led",NULL)函数会在/sys 目录下创建一个“sys_led
对象文件夹,因为这里没有指定对象的父类,因此文件夹在/sys 目录下。然后
sysfs_create_file(led_kobj,&led_att r.attr)该函数会在“sys_led”对象文件夹下创建一
个名为“led_status”的属性文件,属性的申明过程属下:
//定义一个 led_status 对象属性
static struct kobj_attribute led_attr = __ATTR(led_status,0660,led_show,led_store);
上面定义的属性指定了 show 函数和 store 函数。
2) 实现 show store 函数
这两个函数是给用户读写操作的接口函数,当用户读属性文件时,此时会调用 show 函数,当用
户写属性文件时,此时会调用 store 函数,下面是实现的具体示例:
static ssize_t led_show(struct kobject* kobjs,struct kobj_attribute *attr,char *buf)
{
printk(KERN_INFO "Read led\n");
return sprintf(buf,"The led_status status is = %d\n",led_status);
}
linux 驱动开发指南 | 李山文
171
static ssize_t led_store(struct kobject *kobj, struct kobj_attribute *attr,const char
*buf, size_t count)
{
printk(KERN_INFO "led status store\n");
if(0 == memcmp(buf,"on",2))
{
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
led_status = 1;
}
else if(0 == memcmp(buf,"off",3))
{
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
led_status = 0;
}
else
{
printk(KERN_INFO "Not support cmd\n");
}
return count;
}
上面的 led_store 函数实现了对 GPIO 的设置状态操作,led_show 数实现了对 led 状态的打印,
该函数使用了 sprintf 函数,这个函数会在用户空间打印出来,也就是用户会在应用程序上看到此输出
信息,具体输出在哪上面取决于标准输入输出。
可以看到 sysfs 统一设备模型非常简单,里面并没有涉及到用户空间和内核空间之间的数据拷贝,
但最后达到了相同的功能,这正是内核设计者的设计思想巧妙之处。
下面是 led 的源码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/kobject.h> //包含 sysfs 件系统对象类
#include <linux/sysfs.h> //包含 sysfs 操作文件函数
#include <linux/slab.h>
#include <linux/string.h>
#define GPIOE_CFG0 (0x01C20890)
#define GPIOE_CFG1 (0x01C20894)
#define GPIOE_DATA (0x01C208A0)
#define GPIOE_PUL0 (0x01C208AC)
size_t *gpioe_cfg0; //存储虚拟地址到物理地址映射
size_t *gpioe_cfg1; //存储虚拟地址到物理地址映射
size_t *gpioe_data; //存储虚拟地址到物理地址映射
linux 驱动开发指南 | 李山文
172
size_t *gpioe_pul0; //存储虚拟地址到物理地址映射
static int led_status = 0; //定义一个 led 状态变量
static struct kobject *led_kobj; //定义一个 led_kobj
static ssize_t led_show(struct kobject* kobjs,struct kobj_attribute *attr,char *buf)
{
printk(KERN_INFO "Read led\n");
return sprintf(buf,"The led_status status is = %d\n",led_status);
}
static ssize_t led_store(struct kobject *kobj, struct kobj_attribute *attr,const char
*buf, size_t count)
{
printk(KERN_INFO "led status store\n");
if(0 == memcmp(buf,"on",2))
{
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
led_status = 1;
}
else if(0 == memcmp(buf,"off",3))
{
*((volatile size_t*)gpioe_data) |= (1<<12);//设置 GPIOE12 状态 1
led_status = 0;
}
else
{
printk(KERN_INFO "Not support cmd\n");
}
return count;
}
//定义一个 led_status 对象属性
static struct kobj_attribute led_attr = __ATTR(led_status,0660,led_show,led_store);
static int __init sysfs_led_init(void)
{
int ret;
led_kobj = kobject_create_and_add("sys_led",NULL);
if(led_kobj == NULL)
{
printk(KERN_INFO"create led_kobj failed!\n");
return -1;
}
ret = sysfs_create_file(led_kobj, &led_attr.attr);
if(ret != 0)
{
linux 驱动开发指南 | 李山文
173
printk(KERN_INFO"create sysfa file failed!\n");
return -1;
}
gpioe_cfg0 = ioremap(GPIOE_CFG0,4); // GPIOE_CFG0 物理地址映射为虚拟地址
gpioe_cfg1 = ioremap(GPIOE_CFG1,4); // GPIOE_CFG1 物理地址映射为虚拟地址
gpioe_data = ioremap(GPIOE_DATA,4); // GPIOE_DATA 物理地址映射为虚拟地址
gpioe_pul0 = ioremap(GPIOE_PUL0,4); // GPIOE_PUL0 物理地址映射为虚拟地址
*((volatile size_t*)gpioe_cfg1) &= ~(7<<16); //清除配置寄存器
*((volatile size_t*)gpioe_cfg1) |= (1<<16); //配置 GPIOE12 为输出模式
*((volatile size_t*)gpioe_pul0) &= ~(3<<16); //清除上/下拉寄存器
*((volatile size_t*)gpioe_pul0) |= (1<<12); //配置 GPIOE12 为上拉模式
*((volatile size_t*)gpioe_data) &= ~(1<<12) ;//清除 GPIOE12 状态
return 0;
}
static void __exit sysfs_led_exit(void)
{
sysfs_remove_file(led_kobj,&led_attr.attr); //删除属性
kobject_put(led_kobj); //删除对象
printk(KERN_INFO "exit sysfs led!\n");
}
module_init(sysfs_led_init);
module_exit(sysfs_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_DESCRIPTION("sysfs led");
MODULE_VERSION("0.1");
我们以模块的形式编译成.ko 文件后下载到根文件系统中,然后使用 insmod 命令挂载驱动,如
下:
# insmod sysfs_led_dev.ko
[ 21.693225] sysfs_led_dev: loading out-of-tree module taints kernel.
此时可以看到开发板上的 LED 亮起,因为我们初始化的时候默认电平为低电平。现在我们进入
/sys 目录下
# cd /sys/
# ls
block class devices fs module sys_led
bus dev firmware kernel power
可以看到,此时在/sys 目录下有一个 sys_led 目录,这个目录正是我们创建的 kobject 对象。然后
进入 sys_led 目录,可以看到有一个文件:
# cd sys_led/
# ls
linux 驱动开发指南 | 李山文
174
led_status
这个文件正是我们创建的属性文件,我们查看下 led_status 文件属性:
# ls -l
total 0
-rw-rw---- 1 root root 4096 Jan 1 00:05 led_status
可以看到该文件的 root 用户权限为 rw,即 6,同组用户的权限为 rw,即 6。这正好是我们创建
属性时候指定的权限 0660。现在我们对该文件进行读写操作,看是否能正确操作 LED
首先是写操作,先向该文件写入“on
# echo on > led_status
[ 826.260643] led status store
可以看到此时 LED 灯是亮的状态,现在对该文件写入“off”:
# echo off > led_status
[ 904.615464] led status store
可以看到此时 LED 灯熄灭,这说明我们操作正确,与预期一样。
通过上面的讲解,读者应该已经掌握了 sysfs 统一设备模型了。在实际的开发过程中,我们都会
建立在 platform 平台设备框架上,同时也会实现传统的 devfs 设备模型。为何会在这里将 sysfs 设备
模型主要是因为在后面的子系统和块设备驱动中会大量用到,因为这个模型非常方便的提供了很多操
作接口,可以当作一种调试手段。特别是块设备驱动中一般必须基于 sysfs 设备模型来开发驱动,否
则一旦代码量非常庞大的时候我们将无法对其进行控制。
linux 驱动开发指南 | 李山文
175
第七章 并发控制
操作系统的引入其最主要的目的就是实现程序的并发执行,在嵌入式操作系统中,使用较多的是
RTOSReal-Time Operating System)这类操作系统。这种操作系统与 Linux 的应用场景不同,对于
RTOS 而言,由于其实时性更强,因此大多数应用在工业、汽车、飞机等实时性要求很高的领域。
Linux 由于其更加复杂、组件齐全、应用丰富,大多用于在消费嵌入式产品中。虽然这两种操作系统
的内部有很大的不同,但最终的目的是让应用程序能够并发执行。
所谓并发,对于单核处理器而言,实际上并没有真正的并发。只不过是利用时间片切换任务。
计过操作系统的读者应该熟悉操作系统的实现原理,任务之间的切换需要实现保存和恢复现场,对于
RTOS 这种操作系统而言,一般应用在比较简单的系统中,因此其任务切换一般由操作系统开发者来
实现。而对于 Linux 操作系统而言,其一般运行在 SoC 上面,这类处理器一般实现了硬件的任务切换
或者硬件对其进行了辅助支持。而对于多核操作系统而言,是可以实现真正的并发执行。为了后面不
产生歧义,文后的并发都指宏观上的并发执行,而并行则指微观上的真实的并发执行。
7-1 单核和多核处理器任务并
7.1 基本概念
7.1.1 作系统实现原理
我们先来简单了解下 RTOS 这种操作系统的实现过程,这样有利于理解后面的多线程和多核相关
的概念。学过 51 单片机的读者可能对死循环比较熟悉,在一些简单的单片机控制场合,我们的固件
开发只需要一个 while(true)就可以实现大部分需求了。但随着需求的复杂化,我们的程序中简单的一
个死循环将无法满足应用需求,这就出现了前后台机制,所谓前后台机制是指将一些任务放在中断函
数中执行,这些任务一般都是实时性较高且响应较快的任务,而把一些实时性不高的任务放在 while
中执行。我们把放到中断中的任务统称为前台任务,把放在 while 中的任务统称为后台任务。虽然这
种实现机制可以适应稍微复杂的需求,但面对更加复杂的需求时仍然会面临力不从心的窘境。
7-2 前后台程
linux 驱动开发指南 | 李山文
176
为了应付更加复杂的需求,操作系统便出现了。操作系统的核心思想是将任务分时执行,即使有
些任务还没有执行完毕,该任务也必须让出处理器,直到轮到下一次再次执行。
7-3 操作系统中任务被划分多份分时执行
如上图所示,任务 1 和任务 2 被划分为多个小的任务执行,当然上面仅仅只是一种简单的分时算
法,实际上在成熟的操作系统中有非常多的算法。上面的任务 1 和任务 2 来回执行,这个过程由操作
系统来实现,我们将这个过程称为调度(schedule
(scheduler)。对于 RTOS 而言,其最核心最底层的就是调度器的实现,一般这个调度器的实现必须用
汇编代码来编写,这是因为调度一般都需要进行压栈(保存现场)和出栈操作(恢复现场)那为何
要保护和恢复现场呢?我们的任务执行都是有各自的栈空间,但是处理器的资源(寄存器组:保存变
量,运算单元:用来进行数学运算的硬件)却只有一个,这样当我们的任务 1 还没有执行完毕就要让
出处理器了,那任务 1 的运算结果以及寄存器中保存的数据都将丢失,为了不让其数据丢失,操作系
统必须将这个数据保存起来,这也就是保护现场的目的,恢复现场和这个过程正好相反。
7.1.2 进程和线程
在学习操作系统时我们经常会遇到这两个概念,很多初学者可能对这个两个概念很模糊,实际上
这两个概念是操作系统的发展而来的。
进程Process是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调
度的基本单位,是操作系统结构的基础。这是百度百科上的解释,这种解释实在太过抽象,实际上进
程的概念不得不与计算机中内存联系在一起。在计算机早期,程序比较小,操作系统完全可以将所有
的程序全部加载到内存中(大部分单片机程序在 ROM 中直接运行,对于 SoC 而言,要运行的程序必
须从硬盘(可能是 Flash,也可能是 HD)加载到 RAM 中运行)。但是随着程序的不断变大,内存已
经远远达不到程序的大小,此时计算机工程师就想出来换入换出的机制来“扩大”内存。实际上换入
换出不可能扩大内存,但由于其速度很快,给人的感觉像内存可以加载更大的程序了。
7-4 换入换出机制
因此我们就不难理解了进程是计算机资源调度和分配的最小单位,也就是操作系统将一个程序从
硬盘中加载到内存的最小单位就是进程。
linux 驱动开发指南 | 李山文
177
线程(thread)是操作系统能够进行运算调度的最小单位。这个是百度百科的解释,实际上我们
在使用的 RTOS 中创建的所有任务都属于线程(这也是 RT-Thread 的名字由来)。很显然线程是进程
的更小单元,但线程与线程之间之间是共享内存的,即线程之间的访问可以直接进行,但进程是不行
的,进程之间的访问必须采用特定的通信机制。现在相信读者已经对进程和线程有更深入的了解了,
然而实际上线程内部的实现也是比较复杂的,这里不对其进行过多讲解。知道了进程和线程的关系以
及区别,我们在创建任务的时候就应该有一个大体的认知,而不是随意创建任务。
说了这么多,也许读者仍然不理解进程或者线程表示什么,没关系,我们举一个具体的例子加以
说明。现在我们在 Linux 开发板上编写了一个 main.c 源程序,然后我们将其编译为 main.exe 可执行
文件,此时,main.exe 仅仅只是一个放在硬盘上的文件,不占用任何内存。我们的 main.c 件调用了
一个 LED 驱动模块,模块为 led.ko(该模块已经提前挂载了)现在我们执行./main.exe 操作来运行该
程序,此时 main.exe 就是一个进程,这个.exe 运行过程中需要的所有资源(例如内存、磁盘、IO 等)
都是该进程所占有的资源。led.ko 模块驱动程序中有很多个线程,那个 led 驱动程序中的所有涉及到
的资源都是 main.exe 进程中的所属,而其中开辟的线程也就是该进程中的子线程。因此可以看到,
程是操作系统分配资源的一个最小单元。当然在 Linux 中也存在大量的内核进程用来完成内核中的一
些工作,例如守护进程。
7.1.3 资源共享与互斥访问
对于一个处理器而言,其内部的硬件资源是有限的,很多进程或者线程都有可能出现同时访问一
个硬件。例如现在有两个作业,作业 A 和作业 B这两个作业都要利用打印机打印,但打印机只有一
个,如果不采用任何机制,那打印出来的信息将全部乱套。为了让作业能够得到预期的结果,我们需
要将作业 A 和作 B 进行合理的分配,最简单的办法是先让其中一个打印,打印完毕后再剩下的打
印。但是计算机是不知道的,计算机只知道执行程序,为了实现这个功能,锁的概念就出现了。
7-5 锁机制
上面我们讲到进程与进程之间的访问不能直接进行,需要通过特定的通信机制,这也就出现了很
多信号量机制与消息事件机制,我们可以统称为信号量Semaphore实际上互作锁这些都可以称为
信号量。值得注意的是,不管是进程还是线程,这两个都都可能发生资源竞争问题,因此对于多进程
或者多线程编程时,我们必须考虑资源的互斥访问。
7.2 线程步机制
上面我们讲解了锁这个概念, Linux 内核中,还有大量的同步机制。对于线程而言,一个进程
linux 驱动开发指南 | 李山文
178
内的线程是不需要通信的,可以直接访问,但是这些线程会发生资源竞争问题,特别是一些重要的资
源,一旦分配不合理,任务执行的结果可能完全与我们的预期背道而驰,因此多线程之间的同步非常
重要。 Linux 中,多线程之间的同步最常用的便是自旋锁,其次是互斥锁。实际上互斥锁是一个特
殊的信号量,后面的小节会讲解自旋锁核信号量之间的关系。
7.2.1 创建进程
首先我们需要知道如何在 Linux 驱动开发过程中创建一个线程,Linux 内核提供了如下函数来供
驱动开发者创建线程:
1) #define kthread_run(threadfn, data, namefmt, ...)
该函数实际上是一个宏,可以看到该参数是可变的,使用这个宏可以创建一个线程同时立刻开
始运行该线程。
threadfn:线程函数,需要运行的函数
data:线程中传入的参数
namefmt:线程名称
下面我们举一个例子来说明如何创建一个线程:
static struct task_struct *fb_thread; //定义一个线程刷新屏幕
fb_thread= kthread_run(thread_func_fb, myfb, "test");
我们创建了一个 fb_thread 线程,其线程函数为 thread_func_fb,其传入的参数为 myfb,线
程名为“test。我们的线程函数如下:
int thread_func_fb(void *data)
{
struct fb_info *fbi = (struct fb_info *)data;
while (1)
{
if (kthread_should_stop())
break;
/* 任务函数*/
Task();
}
return 0;
}
上面我们就创建了一个线程,关于上面的参数传递问题这里做一个简单的说明,上面的传参是
传递一个地址,也就是告诉这个线程,你需要的参数首地址在这个地方,你可以从中获取你想要的
参数。这里为何能这样的原因在于线程不同于进程,进程中的所有线程都可以随意访问该进程中的
所有数据,因此我们可以直接传递一个首地址给线程。
2) extern int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
Linux4.x 之后驱动开发者已经不能再使用该函数来创建线程。该函数也是创建一个线程,
其中前面两个参数和上面的函数一样,第三个参数为标志。返回值为 PID 即线程 ID
3) #define kthread_create(threadfn, data, namefmt, arg...)
linux 驱动开发指南 | 李山文
179
这函数也是创建一个线程,但并不运行,只有调用 wake_up_process 函数才开始运行。该函数
的参数和第一个函数一样,这里不再累述。wake_up_process 函数用作让一个线程开始运行,其函
数运原型如下:
int wake_up_process(struct task_struct *p)
该函数的参数 kthread_create 的返回值,该函数的返回值 0 表示运行成功,否则失败。
7.2.2 信号量
信号量的作用就是实现通信的作用(锁的机制实际上也是信号量)在多个线程或进程竞争临界
资源(所谓临界资源就是能够共同访问的资源)时,我们需要防止因多个线程的混乱访问而导致结果
不符合预期。 Linux 驱动程序中,我们经常会面临多个线程或者进程访问临界资源问题,此时,
号量就起到至关重要的作用。
为了让读者明白什么是信号量,这里我们来实现一个非常简单但不完全的信号量以说明原理。
先我们有两个线程,分别是线程 A 和线程 BA B 线程都在进程 P 中,我们在进程 P 中定义一个
全局变量 s,我们将这个 s 初始化为 1,如下代码:
7-6 实现一个简单的互斥信号量
上面的代码实现了一个非常简单的互斥信号量,可以看到,当线程 A 开始运行时,此时 s 的值为
1大于 0线程 A 就会输出打印信息,同时 s 的值被修改为 0 了。现在线程 B 开始循行,发现 s=0
此时线程 B 不会打印信息,当线程 A 打印信息完毕后,此时 s 值变为了 1,这时线程 B 再打印时就
可以打印了,这样就保证了只有一个线程在打印信息。
上面虽然看似实现了一个互斥信号量,但实际上存在问题。因为上面的 s=s-1 这个操作不一定是
原子操作,至于什么是原子操作,后面我们会讲到。需要注意的是,上面的 s 变量不能被其他线程修
改,否则有问题。
可以看到,线程在访问进程中的全局变量的时候一定要非常小心,因为很有可能这个全局变量此
时被其他访问,这就造成了不一致问题。因此在 Linux 内核中,线程访问进程中的全局变量都需要加
锁,因为进程中的全局变量对于该进程中的所有线程来说是临界资源(共享资源)
实际上上面的代码中的 s=s-1 操作对应着信号量的获取,学术界称为 P
32
操作; s=s+1 操作则正
好相反,对应着信号量的释放,学术界称为 V 操作。这两个操作正是信号量中赫赫有名的 PV 操作,
32
PpasserenVvrijgeven,荷兰语,即“通过”和“释放”之意。
linux 驱动开发指南 | 李山文
180
即获取信号量和释放信号量。
7.2.2.1 普通信号量
在使用信号量之前,我们需要定义下,例如下面这个例子:
struct semaphore sem;
上面我们定义了一个 sem 信号量,在操作信号量之前我们需要对其进行初始化,Linux 内核提供
了如下函数初始化:
static inline void sema_init(struct semaphore *sem, int val)
该函数用来初始化信号量,例如下面我们初始化 sem 信号量:
sema_init(sem,1); //初始化值 1,等同于一个互斥信号量(二值信号量)
初始化好了信号了之后我们就可以对其进行 PV 操作了,Linux 内核中提供了如下函数进行信号
量的获取和释放:
extern void down(struct semaphore *sem);
extern void up(struct semaphore *sem);
第一个函数为 P 操作,即获取信号量,第二个函数为 V 操作,即释放信号量。上面的 down 函数
会导致该线程睡眠,也就是当在线程中使用时,如果该线程拿不到信号量,那么这个线程将会进入睡
眠状态。down 是一个不可被打断函数,也就是说在执行这个函数的时候不应被打断。Linux 内核中还
提供了一个可打断的 P 操作,如下:
extern int __must_check down_interruptible(struct semaphore *sem);
Linux 内核中还提供了其他的信号量操作,具体查看 include/linux/semaphore.h 文件内容。
7.2.2.2 互斥锁
Linux 中提供了专门的互斥信号量,这里称为互斥锁,本质还是信号量,我们在使用互斥锁时
和信号量一样的步骤就可以了。首先我们定义一个互斥锁:
struct mutex mx;
然后我们对其进行初始化操作,使用如下函数:
mutex_init(mutex)
现在我们就可以对其进行关锁和开锁操作了,使用如下函数:
extern void mutex_lock(struct mutex *lock);
extern void mutex_unlock(struct mutex *lock);
上面的 mutex_lock 是一个不可打断函数,Linux 内核也提供了 mutex_lock_interruptible
数,这个函数是可打断的。
函数打断的意思是说这个函数在执行开始到执行结束的过程中不应该被打断,我们直到,一个函数有很多个
语句组成,这些语句执行中可能被其他线程或者中断打断了,即使是一个语句也会被打断,因为这个语句可能不
是原子操作。
7.2.3 自旋锁
自旋锁是 Linux 驱动开发中最常用的一种互斥访问机制,何为自旋锁呢?我们先看一个例子:
linux 驱动开发指南 | 李山文
181
上面有两份代码,代码1中, i<0 时,此时程序会卡在死循环中;而在代码2中, i<0
时,此时程序会立刻跳出。两个程序都是在 i>=0 的前提下打印 linux,但两个程序的执行却不相同。
可以看到,在代码(1)中,如果 i<0,那么这个程序会一直占用 CPU 执行空操作。
自旋锁和上面的代码(1)类似,当发现锁是关闭时,此时线程会一直在这里等待,该线程不会
进入睡眠状态,而其他信号量就不同,其他信号量类似于代码2当拿不到信号量时,此时线程或
者进程会进入睡眠状态,这也就是自旋锁和其他信号量的最大区别。那读者会不会觉得一直占用 CPU
是不是太浪费 CPU 了,为何要占着不放呢?确实,自旋锁关闭时(拿不到锁)此时一直占着 CPU
实浪费资源,但是读者需要明白,在操作系统中,一旦进程拿不到信号量,此时进程会进入挂起状态,
这个时候操作系统会将该进程所占用的资源(内存)释放,直到下一次进程再次被唤醒时才重新分配
资源(内存),这个过程是需要时间的。如下图所示:
7-6 自旋锁与信号量
在进程 1 中使用的是信号量,当进程 1 拿不到信号量时,即代表没有资源可供使用,此时操作系
统就会将该进程从内存中换出到硬盘中,同时将进程 1 所占的资源(内存)释放。而进程 2 使用的是
自旋锁,当进程 2 拿不到锁时,此时进程不会被换出,而是一直会在此处等待直到有资源。我们可以
想象下有这么一个场景:
假设进程 1 和进程 2 都去发送数据给进程 3但是发送数据必须是进程 1 发送一个字节,然后
再进程 2 发送,发送速度尽可能快。
这个例子可以看到,如果我们使用的是信号量,那么一旦进程 1 发现进程 2 正在发送,还没有完
成,那么此时进程 1 会传出到硬盘,这个过程需要消耗 100us,当进程 2 发送完毕后唤醒进程 1 时,
此时进程从硬盘中搬移到内存中又要花 100us,这很显然太费时间了,虽然进程 1 减少了 CPU 的占
用,但是其效率太低,而进程 2 使用的是自旋锁,不存在换入换出,明显速度就快了。但如果发送的
速度很慢呢?比如 10min 发送一次呢?那很显然是使用信号量合理,因为自旋锁空等 10min 是不可
取的。
从上面的例子可以看到自旋锁一般用在速度非常快(访问频率快)情况下。但还有一种情况就
是中断服务程序中,如果在中断服务程序中使用信号量,那么一旦拿不到资源,此时进程会强制换出
到硬盘,处于休眠状态,很显然这是非常可怕的,因为在中断是不允许休眠的,如果中断中休眠,
会导致中断异常,严重的会造成系统死锁(具体为何会这样这个又涉及到操作系统的更深层的知识了,
由于篇幅有限,这里不过多解释)
代码(1):
/** 省略无关代码 **/
while(i<0)
{ }
printf("linux\n");
/** 省略无关代码 **/
代码(2):
/** 省略无关代码 **/
if(i<0)
break;
printf("linux\n");
/** 省略无关代码 **/
linux 驱动开发指南 | 李山文
182
Linux 内核中提供了自旋锁的操作函数,首先我们需要定义一个自旋锁,使用如所示:
spinlock_t sl;
然后我们需要对这个自旋锁初始化,如下:
spin_lock_init(sl);
现在我们就可以调用关锁和开锁操作了,如下:
static inline void spin_lock(spinlock_t *lock)
static inline void spin_unlock(spinlock_t *lock)
关锁和开锁操作使用和信号量一样,我们只需要放在需要保护的代码段上下就可以了。
7.3 SMP 多核理器
SMPSymmetrical Multi-Processing)即 对称多个处理器,随着处理器的不断发展,单个处理器已
经无法满足更高的需求了,因此出现了多个处理器,这应验了一个不变的真理:如果一个解决不了,
那就两个。既然用两个处理器来处理任务,那么问题来了,两个虽然可以同时干两件事了,但是很多
事情之间是有联系的,并不是绝对独立的,那咋办?因此这就出现了多个核共享同一块内存,同时大
部分资源全部共享,这样每个核心虽然是独立的,但是绝大部分的硬件资源确实共享的。如下图所示:
7-7 SMP 处理器简化结构
在现代的 SMP 处理器中,一般分为主核和从核,对于主核而言,其要完成绝大部分的任务,而
对于从核来说,任务由主核分配。在操作系统刚开始的时候,从核是关闭的,只有主核在运行,特别
是在起始阶段至关重要。当操作系统加载完毕后,从核会由操作系统打开,同时分配任务。因此在多
核处理器运行的时候,主核至始至终都会运行,不能被关闭,而从核可以关闭也可以打开。需要注意
的是,主核和从核在物理结构上完全相同,因此我们称之为同构处理器。
linux 驱动开发指南 | 李山文
183
7.4 原子操作
何为原子操作呢?原子操作指该操作是不可再分的,也就不可能被打断。为何会存在原子操作呢?
我们回顾上面的 7.2.2 中可以看到,s=s-1 这个操作看似是一个语句,不可能被打断的,是实际上计
算机最终执行的是汇编语句,这样 s=s-1 被编译器编译之后会得到多条汇编语句,例如我们在 AMD
处理器上用 gcc 编译器编译 s=s-1 这个语句,其对应的汇编代码如下:
4015ce: c7 44 24 0c 01 00 00 movl $0x1,0xc(%esp)
4015d5: 00
4015d6: 83 6c 24 0c 01 subl $0x1,0xc(%esp)
可以看到 s=s-1 这一条 C 语句被编译为 3 条指令了,因此这一条语句是可以被打断的。那我们如
何实现一个原子语句呢?即不可以被打断的语句,实际上这是一件很难的事情,如果没有专门的指令
那么这几乎是不可能的(在单片机的 RTOS 一般使用关闭中断来实现原子操作)。现在计算机都会提
供原子操作指令,操作系统会对这些指令进行封装,提供给开发者使用。因此驱动工程师就可以在驱
动程序中实现原子操作了,Linux 提供了基本的原子操作函数。
对于操作系统而言,必须实现原子操作,否则信号量、自旋锁这些都没办法保证。Linux 中有两
种原子操作,一种是整型原子操作,另一种是位型原子操作。
7.4.1 型原子操作
33
作为整型原子操作是指我们对整型变量进行操作,一般只有加减操作。在使用原子操作之前,
们需要定义一个原子变量,同时对其进行初始化。
atomic_t a = ATOMIC_INIT(0); //定义一个原子变量 a,初值为 0
Linux 内核中提供了一些 API 对其进行操作,函数如下:
1. static inline void atomic_set(atomic_t *v, int i)
2. static inline int atomic_read(const atomic_t *v)
3. static inline void atomic_inc(atomic_t *v)
4. static inline int atomic_dec_and_test(atomic_t *v)
第一个函数为设置原子变量的值,第二个函数为获取原子变量的值,第三个函数为对原子变量+1
操作,第四个函数为对原子变量-1 操作,同时检测该值是否为 0如果为 0返回 true否则返回 false
7.4.2 型原子操作
34
所谓位型原子操作就是按位运算的方式,而 Linux 中实现位型原子操作是利用内存方式实现的,
当然,每个操作系统的原子操作实现方式都会不一样,这里不去细说。Linux 中提供了如下函数来实
现位型原子操作:
1. static inline int test_and_clear_bit(int nr, volatile void *addr)
2. static inline int test_and_set_bit(int nr, volatile void *addr)
3. static inline int test_and_change_bit(int nr, volatile void *addr)
4. static inline void clear_bit(int nr, volatile void *addr)
5. static inline void set_bit(int nr, volatile void *addr)
33
相关函数请查阅 asm/atomic.h 文件
34
相关函数请查阅 asm/bitops.h 文件
linux 驱动开发指南 | 李山文
184
6. static inline void change_bit(int nr, volatile void *addr)
上面这六个函数是 Linux 中提供了位型原子操作函数,其参数说明如下:
第一个参数:要操作的 bit
第二个参数:内存地址
带有 test_的函数表示返回值为该位操作之后的值。
例如我们需要操作一个变量的值的一个位,但是我们在操作这个变量的时候不希望被打算,即其
操作是原子的,这个时候我们就可以使用位型原子操作函数了,如下所示:
ssize_t a = 10; //定义一个变量 a
clear_bit(2,&a); //清除第二个位,即设置第二个位 0
可以看到,原子操作非常简单,但是我们并不能随便使用原子操作,因为原子操作会阻塞其他核
心,导致性能下降,因此我们在有必要的情况下使用即可。
7.4.3 子操作的应
原子操作的目的最终还是实现竞争访问的冲突问题,我们可以利用这些原子操作来实现信号量、
互斥锁等。回到的 7.2.2 章节,我们实现了一个有缺陷的信号量,这个信号量的实现非常简单, s=s-
1 s=s+1。前面说过,这个操作的问题所在就是操作不是原子的,因此我们实现的信号量有问题,
现在我们来重新实现这个可用的信号量。
我们将 s=s-1 修改为 atomic_dec_and_test(s),将 s=s+1 修改为 atomic_inc(s)这样我们的信号量实
现如下图所示:
7-8 使用原子操作实现的互斥
linux 驱动开发指南 | 李山文
185
代码行数来衡量开发进度,就像是凭重量来衡量
飞机制造的进度
----比尔盖茨
插叙
上述篇章已经讲解了所
有的 Linux 驱动的基础
知识,上面的设备模型是
经历了历史的不断发展
的结果。然而任何复杂的
工程都是由各个组件井
然有序的设计,而不是杂
乱无章的堆砌。因此在接
下来的篇章中,我们将进
Linux 各个子系统
的学习,这些子系统将各
个接口进行规范化。现在
的驱动程序开发主要分
为两部分,一部分为 BSP
开发,即直接和寄存器打
交道的,BSP 开发者专门
负责各个子系统的接口
实现。另一部分则是本书
要将的驱动开发,驱动开
发者主要负责实际设备
的逻辑过程实现;对于驱
动开发者而言,他们仅仅
只需要调用子系统的接
口就可以实现对硬件的
操作。这也是为何要引入
子系统的原因,下面篇章
将专门讲解各个子系统,
不会再直接操作寄存器。
而子系统的实现过程一
般交给 SoC 厂家来完成。
下面我们将正式进入实际的驱动开发,我们将会接触子系
统这个概念,所谓子系统就是将一类驱动进行规范化,提供统
接口驱动者不实现重复
码,而是将重点放到设备驱动程序本身。
当然子系统也是 Linux 走向规范化和系统化编程的必然趋
势,因此,在实际开发的驱动过程中必须基于子系统之上进行
开发。如果不基于子系统编写驱动,那么这中驱动将不能被其
他应用程序或者驱动调用。
linux 驱动开发指南 | 李山文
186
第八章 GPIO 子系统
上面已经讲解了当前驱动框架的所有知识,包括从最简单的字符设备的简单注册,到后面的基于
设备树驱动;软硬件分离驱动框架上有基于 Platform 的平台设备驱动框架,然后引入了 sysfs 统一设
备驱动模型。这些驱动框架使得原来的驱动变得更加严谨和规范,但随着设备的数量不断增加,很多
设备其实都是由为数不多的驱动组合而来,因此内核为了让驱动开发变得更加简单和规范,开始进入
了基本的子系统。试想一下,如果我们的代码量非常大,那我们的所有底层基本的操作都需要自己重
新实现一遍,而且硬件发生了改变,我们的接口也全部需要修改,很显然这是非常不合理的设计。Linux
内核为了能够实现底层代码的复用以及实现软硬件分离的思想,提出了子系统的概念,所谓子系统就
是将共有的东西抽象为一个统一框架,提供上层统一的接口,而底层由每个厂家自己实现,这样驱动
层去调用这些底层接口的时候就不需要考虑底层的实现,而只需要使用统一的 API 即可。这些底层由
厂家所完成的工作由每个厂家的额 BSP
35
工程师来开发。
8-1 引入子系
本章要讲解的 GPIO 子系统,后面也会继续讲解 SPI 子系统和 IIC 子系统以及 UART 子系统和
Input 子系统。今后的驱动开发都应基于子系统进行开发,这样
别人看到代码的时候很快就能理解驱动的大体框架。
从右边图可以看到,Linux 驱动程序基于 Linux 子系统开
发的, Linux 子系统需要基于 BSP 驱动程序来开发。对于驱
动开发者来说,所开发的程序需要基于 Linux 子系统所提供的
接口,这样即使 BSP 驱动程序进行了改变,我们的驱动代码也
不会受到影响,这就是引入子系统的好处。但对于真正想理解
Linux 子系统的开发者来说,其难度也会变得更大,因此一般
而言,公司的代码会分配至少两部分,一部分负责开发驱动程
序,另一部分负责开发 BSP 驱动程序。BSP 驱动程序与硬件
紧密相关,其主要任务就是能够将寄存器信息和处理器的总线
相关信息描述在一起,顺便需要说明的是设树文件的编写也
是由 BSP 开发人员完成的。
下面我们一起来认识 Linux 中的 GPIO 子系统。由于 Linux 历史原因,GPIO 子系统中有两套接
口,其中一套较为古老,另一套是目前用的较多的一种。我们对古老的接口只做一个简单的概述,
点放在目前使用最多的接口上。
8.1 GPIO 子系统
GPIO 子系统是所有子系统中最基本的子系统之一,该子系统提供了基本的 GPIO 操作接口,包
GPIO 的申请、释放、设置输入输出、设置高低电平等等,我们在开发驱动的时候只需要按照规范
的操作流程就可以轻松的操作 GPIO
35
BSP Board Support Packeg,板级支持包
8-2 驱动开发 BSP 开发的
关系
linux 驱动开发指南 | 李山文
187
8.1.1 GPIO 子系 API(已弃用)
8.1.1.1 申请 GPIO 资源
Linux 系统中,所有的硬件资源都要先申请后使用,使用完毕后必须释放,下面是内核中提供
的基本的申请 GPIO 资源的 API
1) static inline int gpio_request(unsigned gpio, const char *label)
该函数申请一个 GPIO 资源,其参数如下:
gpio申请的 GPIO 编号
36
label申请时为该 GPIO 取的标签名
2) static inline int devm_gpio_request(struct device *dev, unsigned gpio,
const char *label)
该函数和上面的函数一样,都是申请一个 GPIO 资源,但是该函数主要针对采用 platform 架构
的驱动使用的。其中第一个参数就是该平台设备。
dev设备指针
gpio申请的 GPIO 编号
label申请时为该 GPIO 取的标签名
3) static inline int gpio_request_one(unsigned gpio,
unsigned long flags, const char *label)
该函数也是申请一个 GPIO 资源,但该函数相对于 gpio_request 而言多了一个参数 flags 用来
设置电平状态,下面时该函数的参数说明:
gpio申请的 GPIO 编号
flagsGPIO 的输入输出模式
GPIOF_DIR_OUT:输出
GPIOF_DIR_IN:输入
GPIOF_OUT_INIT_LOW:输出且初始低电平
GPIOF_OUT_INIT_HIGH:输出且初始高电平
GPIOF_ACTIVE_LOW:低电平有效
GPIOF_OPEN_DRAIN:开漏输出
GPIOF_OPEN_SOURCE:开源输出
label申请时为该 GPIO 取的标签名
4) int devm_gpio_request_one(struct device *dev, unsigned gpio,
unsigned long flags, const char *label)
该函数和上面的函数一样,devm 的意思是 devices manager 即设备管理,其主要作用就是设备在
没有使用后会进行自动释放。其参数说明如下:
dev设备指针
gpio申请的 GPIO 编号
flagsGPIO 的输入输出模式
label申请时为该 GPIO 取的标签名
5) static inline int gpio_request_array(const struct gpio *array, size_t num)
该函数用于申请多个 GPIO 资源,参数说明如下:
首先看下 struct gpio 这个结构体:
struct gpio {
36
gpio 编号 = GPIO *32 + Index,例如 GPIOB2 的编号 = 1 * 32 + 2 = 34
linux 驱动开发指南 | 李山文
188
unsigned gpio;
unsigned long flags;
const char *label;
};
该结构体包含了 GPIO 编号、电平状态和标签名。
array该指针指向需要申请的 GPIO 结构体数组
num申请 GPIO 资源的个数
8.1.1.2 设置 GPIO 方向
在申请 GPIO 资源之后我们需要设置其方向,Linux 内核提供了如下 API 来实现操作:
1) static inline int gpio_direction_input(unsigned gpio)
该函数用来设置 GPIO 的方向为输入,其参数为 GPIO 编号。
2) static inline int gpio_direction_output(unsigned gpio, int value)
该函数用来设置 GPIO 的反向为输出,其参数为 GPIO 编号以及输出的值。
8.1.1.3 获取和设置 GPIO
1) #define gpio_get_value __gpio_get_value
static inline int __gpio_get_value(unsigned gpio)
gpio_get_value 该函数获取 GPIO 的电平状态,参数为 GPIO 的编号。
2) #define gpio_set_value __gpio_set_value
static inline void __gpio_set_value(unsigned gpio, int value)
gpio_set_value 该函数用来设置 GPIO 的电平状态,参数为 GPIO 的编号和设置的值。
8.1.1.4 释放 GPIO 资源
申请 GPIO 资源使用完毕后需要进行释放,Linux 内核提供了如下 API
static inline void gpio_free(unsigned gpio)
该函数与 gpio_request 函数对应,用来释放一个 GPIO 资源,参数为 GPIO 编号。
static inline void devm_gpio_free(struct device *dev, unsigned int gpio)
该函数与 devm_gpio_request 函数对应,也是一个释放 GPIO 资源接口,该函数多了一个 dev
参数。
static inline void gpio_free_array(const struct gpio *array, size_t num)
该函数与 gpio_request_array 函数对应,释放多个 GPIO 资源。
上面是比较常用的 GPIO 子系统调用接口,但上面的接口过于陈旧,现已废除不再使用或者不建议
使用,而较为常用的就是 GPIOD 接口。
8.1.1.5 从设备树中获取 GPIO 资源
Linux 引入设备树后,所有的设备信息一般都需要先从设备树中获取,例如我们现在定义了一
LED,其引脚为 GPIOB5,那么最好的办法是在设备树中申明 GPIO 节点,然后通过 Linux 提供的
函数来获取 GPIO 编号,Linux 提供了如下函数用来从设备树中获取 GPIO 编号:
static inline int of_get_named_gpio(struct device_node *np,
const char *propname, int index)
该函数用于从设备树中获取 GPIO 编号,其参数如下:
np设备节点
propname设备树中的 gpio 属性名称
indexgpio 属性名称的索引
linux 驱动开发指南 | 李山文
189
例如下面是设备树中的 GPIO 设备节点:
panel@0 {
compatible = "samsung,s6e63j0x03";
reg = <0>;
vdd3-supply = <&ldo16_reg>;
vci-supply = <&ldo20_reg>;
reset-gpios = <&gpe0 1 GPIO_ACTIVE_LOW>;
te-gpios = <&gpx0 6 GPIO_ACTIVE_HIGH>
<&gpx0 7 GPIO_ACTIVE_HIGH>;
};
上面的设备节点中有两个 gpio 资源,分别是 reset-gpios te-gpios,其中 te-gpios 属性有
两个 GPIO 资源,我们可以通过 index 来获取我们想要的哪个 GPIO,我们在驱动程序中获取其编号
可以这样使用:
struct device *panel;
int reset_gpio, te_gpio1, te_gpio2;
...
reset_gpio = of_get_named_gpio(panel->of_node, "reset-gpios", 0);
te_gpio1 = of_get_named_gpio(panel->of_node, "te-gpios", 0);
te_gpio2 = of_get_named_gpio(panel->of_node, "te-gpios", 1);
第一个会获取设备节点中属性为 reset-gpios GPIO 编号其
8-3 获取设备树中的 GPIO 资源
8.1.1.6 GPIO 与中断信号线
Linux 中,GPIO 引脚也可以作为中断信号线,而且对于 GPIO 控制器而言其也可以是一个中
断控制器。对于大部分情况而言,我们可以利用一个 GPIO 当作一个中断信号线,例如一个外部触发
信号,我们可以利用这个信号来实现一个特殊功能。下面是一些常用的 GPIO 转为中断信号线函数:
#define gpio_to_irq __gpio_to_irq
static inline int __gpio_to_irq(unsigned gpio)
gpio_to_irq 该函数是将 GPIO 引脚转换为中断线,例如我们可以这样实现一个按键,我们可以这样
写:
int key_irq = gpio_to_irq(KEY_IRQ_PIN);
8.1.2 GPIO 子系 API
现在我们使用较多的是前缀为 gpiod 的子系统接口,实际上上面已经弃用的接口为了兼容现已经
linux 驱动开发指南 | 李山文
190
替换成了 gpiod_xxx 的包壳函数。那么为何要引入新的 GPIO 相关接口呢?其主要原因是 Linux 开始
引入设备树后,大量的资源申请可以直接在设备树中注明,而在驱动中我们可以使用新的 GPIO 子系
统接口来调用,这样一旦我们的 GPIO 引脚发生了改动,我们的驱动程序不需要变动,仅仅只需要修
改设备树即可。在使用新的 GPIO 子系统时需要配置内核开启 CONFIG_GPIOLIB 该宏。
8.1.2.1 申请 GPIO 资源
在新的 GPIO 子系统中,所有的操作函数都是 gpiod 为前缀,即 gpiod_xxx,其中表示 dts。我们
可以通过如下的 API 来完成从设备树中获取 GPIO 资源:
1) struct gpio_desc *__must_check gpiod_get(struct device *dev,
const char *con_id,
enum gpiod_flags flags);
该函数从设备树中获取 GPIO 的描述信息,参数如下:
dev设备结构体
con_idgpio 属性 id 名称
flagsgpio 输入输出状态标记
下面我们举一个例子来说明该接口如何使用:
设备树
&xgenet {
status = "ok";
rxlos-gpios = <&sbgpio 12 1>;
};
该设备节点中有一个 rxlos-gpios 属性,该属性就是 GPIO 子系统的属性,一般 GPIO 的子系统
属性命名为 xxx-gpios。我们可以使用 gpiod_get 函数来获取该 GPIO 编号:
struct gpio_desc *rxlos_gpio = gpiod_get(dev, "rxlos", GPIOD_IN);
可以看到,这里的 con_id 参数只有 GPIO 属性名称的前缀,即 rxlos 而不是 rxlos-gpios,这
里我们需要与已弃用的方式区分。
2) static inline struct gpio_desc *__must_check gpiod_get_index( struct
device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags)
有时候我们会遇到一个 GPIO 属性有多个 GPIO 资源的情况,这时我们需要使用上面的函数来获
GPIO 资源。其中 inx 就是获取资源的索引。下面我们看一个例子:
&blink {
status = "ok";
rgb-gpios = <&sbgpio 12 1>,
<&sbgpio 13 1>,
<&sbgpio 14 1>,
};
上面的设备节点有三个 GPIO 资源,我们需要使用 gpiod_get_index 函数来一一获取资源:
struct gpio_desc *red_gpio = gpiod_get_index(dev, "rgb", 0,GPIOD_OUT_HIGH);
struct gpio_desc *green_gpio = gpiod_get_index(dev, "rgb", 1,GPIOD_OUT_HIGH);
struct gpio_desc *blue_gpio = gpiod_get_index(dev, "rgb", 2,GPIOD_OUT_HIGH);
上面我们就可以分别获取其节点的三个 GPIO 资源,这种写法很常见,因为在很多场合一个总线
上会有很多信号线,例如 LCD 8080 接口。
linux 驱动开发指南 | 李山文
191
和已弃用的 API 一样也有带设备管理的接口,如下所示:
3) struct gpio_desc *__must_check devm_gpiod_get( struct device *dev,
const char *con_id,
enum gpiod_flags flags)
该函数和 gpiod_get 函数一样,也是用来申请 GPIO 资源,这里不再对参数进行说明。
4) struct gpio_desc *__must_check devm_gpiod_get_index( struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags)
该函数与 gpiod_get_index 函数一样,也是用来申请 GPIO 资源,其参数一样这里不再累述。
上面的两个 API 都是带有设备管理的接口,其中 devm 即是 device manager。上面提到的
GPIO 资源申请函数只是常用的几个,Linux 内核中还提供了大量的申请 GPIO 接口函数,详细请
查看 drivers\gpio\gpiolib-devres.c 文件。
注意:上面的函数中带有__must_check 表示执行该函数后必须进行返回值的检测,否则会出现
意想不到的错误。
8.1.2.2 设置 GPIO 方向
申请 GPIO 资源成功后,我们就可以设置 GPIO 方向了,Linux 内核提供了如下设置 GPIO 方向
的函数:
1) int gpiod_direction_input(struct gpio_desc *desc)
该函数用来设置 GPIO 方向为输入,参数是 GPIO 描述信息结构体,我们申请 GPIO 资源后就可
以调用该函数来设置 GPIO 方向了。
2) int gpiod_direction_output(struct gpio_desc *desc, int value)
该函数和上面的函数对应,用来设置 GPIO 方向为输出,参数是 GPIO 描述信息结构体。
8.1.2.3 获取/设置 GPIO 输出电平
设置好 GPIO 方向之后我们可以设置 GPIO 的电平来控制外设,下面是 API 函数:
1) int gpiod_get_value(const struct gpio_desc *desc)
该函数用来获取某个 GPIO 的电平,参数为 GPIO 资源描述结构体。
2) void gpiod_set_value(struct gpio_desc *desc, int value)
该函数用来设置 GPIO 的输出电平。
8.1.2.4 释放 GPIO 资源
GPIO 使用完毕后需要及时释放,我们可以使用下面函数来释放 GPIO 资源:
1) void gpiod_put(struct gpio_desc *desc)
该函数 gpiod_get 对应,用来释放一个 GPIO 资源,参数为 GPIO 描述结构体。
2) void devm_gpiod_put(struct device *dev, struct gpio_desc *desc)
该函数和 devm_gpiod_get 对应,用来释放一个 GPIO 资源。
8.1.2.5 GPIO 与中断信号线
有些 GPIO 可以也可以是中断引脚,Linux 内核提供了一个接口来将 GPIO 引脚转换为对应的中
断号。这主要是因为在 3.x 版本之后,Linux 的中断号变为了动态申请的,也就是在内核启动的时候
不会分配中断号,只有当驱动程序需要时才进行分配。该接口如下所示:
int gpiod_to_irq(const struct gpio_desc *desc)
linux 驱动开发指南 | 李山文
192
该函数的参数为 GPIO 的引脚描述结构体指针,返回值为中断号。
8.2 pinctrl 子系统
既然已经有了 GPIO 子系统,为何还要 pinctrl 子系统呢?随着 SoC 越来越复杂, GPIO 资源变
得越来越珍贵,不可能所有的资源都为其分配一个 IO因此就出现了 IO 口复用的情况。用过 STM32
的读者应该会比较熟悉 GPIO 复用功能,其实在 SoC 中会变得非常常见,每个 GPIO 几乎否是其他功
能的复用引脚,为了使整个系统变得更加好管理,Linux 出现了 pinmux pinctrl 子系统,由于历史
原因,pinmux 已经弃用了,目前 pinctrl 子系统用的最为广泛。
GPIO 子系统提供给驱动开发者一个方便的操作接口,但 pinctrl 子系统不仅提供 GPIO 子系统的
操作,还需要对 GPIO 的复用功能进行完善的管理。在使用 pinctrl 子系统之前我们需要在设备树中编
pinctrl 子系统的设备树节点。对于大部分 SoC 而言,pinctrl 设备树节点都是已经编写好的,我们
需要用的时候只需要引用该节点就可以了。
8.2.1 pinctrl 设备树节点
在设备树文件中我们经常会看到如下节点:
pinctrl@1c20800 {
compatible = "allwinner,sun8i-v3s-pinctrl";
reg = <0x01c20800 0x400>;
interrupts = <GIC_SPI 15 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_PIO>, <&osc24M>, <&rtc 0>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
#gpio-cells = <3>;
interrupt-controller;
#interrupt-cells = <3>;
/* 此处省略子节点 */
}
上面的一个全志公司的 V3s 的设备树文件中的 pinctrl 设备树节点,该设备树节点用于给 pinctrl
子系统记录 GPIO 的寄存器和 GPIO 引脚的复用功能。从 gpio-controller 属性可以看到 GPIO
时还是一个中断控制器,下面我们来简单分析下该 pinctrl 节点。
compatible 用于匹配 pinctrl 子系统的底层驱动程序,该驱动程序由各个芯片厂家来编写;
reg 指定了该 GPIO 控制器的寄存器映射地址,通过数据手册可以看到其 GPIO 控制器的地址为
0x01c20800大小为 0x400interrupts 指定了该 GPIO 控制器的中断信号线,我们查看 V3s 的数
据手册可以看到 GPIO 的中断号为 47(GPIOB) 49(GPIOG)这两个。从中断表可以看到并非是所有
GPIO 都可以当作中断,每个芯片都不一样,例如 V3s 这个芯片就只有 GPIOB GPIOG 这两组
GPIO 可以映射为中断线。clocks 属性指定了 pinctrl 时钟的主时钟源,clock-names 属性对 clocks
属性进行了一个说明,第一个时钟为 apb 时钟,第二个时钟为晶振时钟,第三个时钟为 RTC 时钟源。
gpio-controller 属性表明该节点是一个 GPIO 控制器。#gpio-cells 属性指定了在引用 GPIO
点时需要有三个单元。#interrupt-cells 属性指定了引用该节点作为中断时需要有三个单元。
8.2.2 映射复用功能
由于 pinctrl 子系统是管理 GPIO 引脚复用的,因此我们在将 GPIO 映射为其他功能时,我们需
要在 pinctrl 设备树节点中添加我们的功能,例如现在我们需要使用串口 uart0 功能,那么 uart
一定是复用了两个 GPIO 引脚,这时我们需要在 pinctrl 设备树节点中这样写:
linux 驱动开发指南 | 李山文
193
pinctrl@1c20800 {
compatible = "allwinner,sun8i-v3s-pinctrl";
reg = <0x01c20800 0x400>;
interrupts = <GIC_SPI 15 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_PIO>, <&osc24M>, <&rtc 0>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
#gpio-cells = <3>;
interrupt-controller;
#interrupt-cells = <3>;
uart0_pins: uart0-pins {
pins = "PB8", "PB9";
function = "uart0";
};
}
上面我们添加了一个 uart0-pins 子节点,该节点中由两个属性,分别是 pins function 属性。pins
属性描述了该节点的中使用的引脚,可以看到这里使用的是 PBx 这种方式,而不是使用编号或者引
pannel 方式,至于哪一个是 TXD,哪一个是 RTD,这个取决于 pinctrl 驱动的实现;function 属性
指定了该引脚作为何种功能,为何写为 uart0 也是取决于 pinctrl 驱动内部的实现。
8.2.3 设备树节点引用复用功能
我们在使用复用功能时,除了在 pinctrl 中添加设备引脚说明外,还需要在复用功能的节点中引用
pinctrl 中申明的节点。例如上面我们在 pinctrl 中添加了一个 uart0-pins 节点,我们在 uart 节点中
需要指定引脚:
uart0: serial@1c28000 {
compatible = "snps,dw-apb-uart";
reg = <0x01c28000 0x400>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
reg-shift = <2>;
reg-io-width = <4>;
clocks = <&ccu CLK_BUS_UART0>;
dmas = <&dma 6>, <&dma 6>;
dma-names = "rx", "tx";
resets = <&ccu RST_BUS_UART0>;
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
status = "disabled";
};
uart0 节点中有 pinctrl-0 = <&uart0_pins> pinctrl-names =
"default"这两个属性,这两个属性正是 pinctrl 的特有属性,在对这两个属性说明之前我们先来
看下 pinctrl 的一些特有属性。
pinctrl-<ID>:这个属性指定了 pin 的状态功能,简单来说就是什么状态时该引脚用作什
么功能,例如现在我们可以这样写一个属性:
pinctrl-0 = <&uart0_pins>;
上面指定了当 uart0 为活跃状态时,此时该引脚用作 uart0 引脚,那么我们想要其在睡眠
linux 驱动开发指南 | 李山文
194
时,该引脚用作普通 GPIO,则我们可以这样:
pinctrl-1 = <&uart0_sleep>;
当然,我们需要在 pinctrl 节点中定义 uart0_sleep 子节点来指定其 GPIO 复用功能。
pinctrl-name:该属性指定了其 ID 的名称,例如我们有两个 pinctrl-ID
pinctrl-0 = <&uart0_pins>;
pinctrl-1 = <&uart0_sleep>;
我们可以对上面起一个名称,如下:
pinctrl-name = "default","sleep";
这样 ID0 对应着默认状态,ID1 对应着睡眠状态。
8.3 LED 示例
下面我们来利用 GPIO 子系统实现两个简单的功能,一个是 LED 控制,另一个是按键中断控制。
首先我们在设备树中添加我们的 LED 设备节点,如下所示:
led_subsys {
compatible = "test,led_subsys";
status = "ok";
led-gpios = <&pio 6 2 GPIO_ACTIVE_HIGH>; /* PG2 */
};
注意上面的 GPIO_ACTIVE_HIGH 表示高电平有效,即逻辑电平为正逻辑,也就是在设置电平高
低时,0 表示低电平,1 表示高电平;GPIO_ACTIVE_LOW 的逻辑则相反
该节点添加在根节点中,然后我们实现 probe 函数,为了能够方便调试,我们使用 sysfs 设备
文件文件系统框架,这样我们就不用再写应用测试程序了,而是直接使用 echo cat 直接操作。
probe 函数如下:
static int led_probe(struct platform_device *pdev)
{
int ret;
ret = device_create_file(&pdev->dev,&dev_attr_led);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create w25q128_dev file failed!\n");
return -1;
}
led_pin = devm_gpiod_get(&pdev->dev,"led",GPIOF_OUT_INIT_LOW);
if(IS_ERR(led_pin))
{
printk(KERN_ERR"Get gpio resource failed!\n");
return -1;
}
gpiod_direction_output(led_pin,0);
return 0;
}
probe 函数较为简单,首先在/sys/devices/目录下新建一个 led_dev 的属性文件,我们设置
该文件的操作权限为 0660,即可读可写。devm_gpiod_get 函数用来申请一个 gpio 资源,默认电
linux 驱动开发指南 | 李山文
195
平为低电平。然后 gpiod_direction_output 设置 gpio 的方向为输出方向,同时设置其电平为低
电平。下面实现 show store 这两个函数:
static ssize_t led_show(struct device *dev,struct device_attribute *attr,char *buf)
{
return sprintf(buf,"led status=%d\n",gpiod_get_value(led_pin));
}
static ssize_t led_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
if(0 == memcmp(buf,"on",2))
{
gpiod_set_value(led_pin,0);
}
else if(0 == memcmp(buf,"off",3))
{
gpiod_set_value(led_pin,1);
}
else
{
printk(KERN_INFO "Not support cmd\n");
}
return count;
}
这两个函数也较为简单,在 led_show 函数中我们直接输出 gpio 的电平状态,这里使用
gpiod_get_value 函数来实现;在 led_store 函数中,根据用户数据的命令来设置 gpio 的电平
状态,从而控制 LED 亮灭的目的,这里用 gpiod_set_value 该函数即可。我们还需要设置文件属
性,如下:
static DEVICE_ATTR(led, 0660, led_show, led_store); //定义文件属性
然后我们实现 remove 函数,如下:
static int led_remove(struct platform_device *pdev)
{
device_remove_file(&pdev->dev,&dev_attr_led);//删除属性文件
devm_gpiod_put(&pdev->dev,led_pin);
printk(KERN_INFO "exit sysfs sys_led!\n");
return 0;
}
remove 函数主要实现了属性文件的删除,同时释放 gpio 资源。
最后我们定义 platform_driver 结构体如下:
static struct platform_driver led_cdev = {
.driver = {
.name = "led_subsys",
.owner = THIS_MODULE,
.of_match_table = led_match_table,
},
.probe = led_probe,
linux 驱动开发指南 | 李山文
196
.remove = led_remove,
.id_table = led_ids,
};
下面是驱动程序的完整代码:
设备树中根节点中添加的设备节点:
led_subsys {
compatible = "test,led_subsys";
status = "ok";
led-gpios = <&pio 6 2 GPIO_ACTIVE_HIGH>; /* PG2 */
};
驱动源码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/kobject.h> //包含 sysfs 件系统对象类
#include <linux/sysfs.h> //包含 sysfs 操作文件函数
#include <linux/gpio/consumer.h> //包含 gpio 子系统接口
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/gpio.h> //包含 gpio 一些宏
struct gpio_desc *led_pin; //gpio 资源
static ssize_t led_show(struct device *dev,struct device_attribute *attr,char *buf)
{
return sprintf(buf,"led status=%d\n",gpiod_get_value(led_pin));
}
static ssize_t led_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
if(0 == memcmp(buf,"on",2))
{
gpiod_set_value(led_pin,0);
}
else if(0 == memcmp(buf,"off",3))
{
gpiod_set_value(led_pin,1);
}
else
linux 驱动开发指南 | 李山文
197
{
printk(KERN_INFO "Not support cmd\n");
}
return count;
}
static DEVICE_ATTR(led, 0660, led_show, led_store); //定义文件属性
static int led_probe(struct platform_device *pdev)
{
int ret;
ret = device_create_file(&pdev->dev,&dev_attr_led);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create w25q128_dev file failed!\n");
return -1;
}
led_pin = devm_gpiod_get(&pdev->dev,"led",GPIOF_OUT_INIT_LOW);
if(IS_ERR(led_pin))
{
printk(KERN_ERR"Get gpio resource failed!\n");
return -1;
}
gpiod_direction_output(led_pin,0);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
device_remove_file(&pdev->dev,&dev_attr_led);//删除属性文件
devm_gpiod_put(&pdev->dev,led_pin);
printk(KERN_INFO "exit sysfs sys_led!\n");
return 0;
}
static struct of_device_id led_match_table[] = {
{.compatible = "test,led_subsys",},
};
static struct platform_device_id led_ids[] = {
{.name = "led_subsys",},
};
static struct platform_driver led_cdev = {
.driver = {
.name = "led_subsys",
.owner = THIS_MODULE,
linux 驱动开发指南 | 李山文
198
.of_match_table = led_match_table,
},
.probe = led_probe,
.remove = led_remove,
.id_table = led_ids,
};
module_platform_driver(led_cdev);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("led_cdev"); //简单的描述
将上面的源码以动态挂载的方式编译为.ko 文件,然后下载到根文件系统中的根目录中,进入
根目录,如下所示:
# ls
bin linuxrc root
dev lost+found run
etc media sbin
gpio_subsys.ko mnt sys
tmp usr lib
opt var lib32
proc
使用 insmod 命令挂载 gpio_subsys.ko 驱动,然后进入/sys/devices/目录下:
# insmod gpio_subsys.ko
[ 666.087141] gpio_subsys: loading out-of-tree module taints kernel.
[ 666.094237] sun8i-v3s-pinctrl 1c20800.pinctrl: supply vcc-pg not found, using dum
my regulator
# cd /sys/
# ls
block class devices fs module
bus dev firmware kernel power
可以看到在/sys/devices/platform/led_subsys 目录下新建了一个 led 文件:
# cd /sys/devices/platform/led_subsys
# ls
led
然后我们对该文件写 on,此时可以看到 LED 亮起:
# echo on > led
我们也可以查看 led gpio 引脚状态:
# cat led
led status=0
linux 驱动开发指南 | 李山文
199
linux 驱动开发指南 | 李山文
200
第九章 输入子系统
输入设备非常繁多,种类也数不胜数,但从计算机角度看,平时我们生活中的输入设备无非就是
键盘、鼠标、触摸屏等等。为了方便管理这些设备,Linux 内核提供了输入子系统,这个子系统将与
硬件无关的操作部分都封装起来,只为驱动开发者提供一个接口,驱动开发人员根据这些 API 来实现
输入设备的注册和注销。
9-1 常见的输入设备
输入子系统将将输入设备分为三种类型,分别是键盘设备、鼠标设备和操作杆设备。我们所接触
的设备几乎都属于上面的设备之一,它们之间最主要的区别在于他们输入的数据类型不同。
键盘设备的功能是实现键盘按键的输入,例如我们打字的时候需要配合键盘才能实现字母的输入;
这些输入的数据都是单个数据,我们的键盘由很多键组成,每个按键都有自己的键位编号,当我们按
下一个按键时,此时键盘设备驱动会将此时的键位码发送给 Linux 内核,这样内核就直到是哪个按键
按下了。驱动程序向内核发送的键码一定是在键盘所有编码中的一个,不会出现随便的一个数字。
9-2 键盘设备数据传输
现代的键盘上会有自己的控制器,当按键按下时,此时键盘上的控制器会自动检测到按下哪个按
键,然后向操作系统发送一个中断通知,同时发送一个键位码。驱动程序收到中断后会接受中断发送
的键码,此时驱动程序会去解析键码的相对应的 Linux 键盘编码,然后会将对应的 Linux 键盘编码发
送给 Linux 内核,此时内核就知道输入的是什么按键了。
对于相对数据输入设备来说,例如鼠标设备就和键盘设备不同了,首先鼠标设备需要发送鼠标移
动的坐标,包括 X 坐标和 Y 坐标。这两个坐标是一个相对值,也就是相对与当前位置的相对移动量,
而且一般来说,鼠标移动的数据不像键盘那样是离散的,鼠标数据是一个连续的数据,可以是坐标的
任意数据。因此,可以看到鼠标相对来说键盘要复杂一些,但实际上其数据传输过程都是相同的,
备将特定的数据发送给 Linux 内核。
linux 驱动开发指南 | 李山文
201
9-3 鼠标设备数据传输
对于绝对数据输入设备而言,其输入的数据是一个绝对大小值;例如游戏手柄,其数据都是绝对
值,方向摇杆的数据都是一个绝对的值,也就是内核接收的数据都是一个绝对量,而鼠标不一样,
标接收的数据都是一个相对量。
9-10 游戏手柄数据传输
不管是鼠标也好,还是游戏手柄也好,这些仅仅只是一个典型的代表,关键在于设备传输的数据
属性是相对的还是绝对的,我们在注册设备的时候只要注册符合要求的类型就可以了。
9.1 input_dev 构体
下面我们先看一个最重要的结构体 input_dev
struct input_dev {
const char *name;
const char *phys;
const char *uniq;
struct input_id id;
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
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 long ledbit[BITS_TO_LONGS(LED_CNT)];
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
unsigned int hint_events_per_packet;
linux 驱动开发指南 | 李山文
202
unsigned int keycodemax;
unsigned int keycodesize;
void *keycode;
int (*setkeycode)(struct input_dev *dev,
const struct input_keymap_entry *ke,
unsigned int *old_keycode);
int (*getkeycode)(struct input_dev *dev,struct input_keymap_entry *ke);
struct ff_device *ff;
struct input_dev_poller *poller;
unsigned int repeat_key;
struct timer_list timer;
int rep[REP_CNT];
struct input_mt *mt;
struct input_absinfo *absinfo;
unsigned long key[BITS_TO_LONGS(KEY_CNT)];
unsigned long led[BITS_TO_LONGS(LED_CNT)];
unsigned long snd[BITS_TO_LONGS(SND_CNT)];
unsigned long sw[BITS_TO_LONGS(SW_CNT)];
int (*open)(struct input_dev *dev);
void (*close)(struct input_dev *dev);
int (*flush)(struct input_dev *dev, struct file *file);
int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int
value);
struct input_handle __rcu *grab;
spinlock_t event_lock;
struct mutex mutex;
unsigned int users;
bool going_away;
struct device dev;
struct list_head h_list;
struct list_head node;
unsigned int num_vals;
unsigned int max_vals;
struct input_value *vals;
bool devres_managed;
ktime_t timestamp[INPUT_CLK_MAX];
bool inhibited;
};
可以看到该结构体非常庞大,我们只关心常用的几个就可以了:
name:代表设备名称。
phys:代表设备的设备文件路径。
evbit:表示设备支持的事件类型,例如:
EV_KEY:表示按键事件,例如键盘事件设备
EV_REL:表示相对输入数据设备,即 relative,相对的;例如鼠标等相对位置的设备
EV_ABS表示绝对输入数据设备, absolute绝对的;例如操作杆等绝对位置的设
EV_MSC:表示杂项设备
linux 驱动开发指南 | 李山文
203
keybit这个对应着 EV_KEY 类型设备,该值用来设置键值,例如 KEY_AKEY_BKEY_C
等等
relbit这个对应着 EV_REL 类型设备,该值表示输入的坐标相对值,例如 REL_XREL_Y
REL_ZREL_RX
adsbit这个对应着 EV_ABS 类型设备,该值表示输入的坐标绝对值,例如 ABS_XABS_Y
等。
mscbit:这个对应着 EV_MSC 类型设备。
repeat_key:存储最后一次设备输入的键值。
rep自动重复参数的当前值。
9.2 入设备的注与注销
最终我们的目的是将设备注册到内核中,Linux 提供了相关的函数供驱动开发者调用,主要有以
下函数:
1) struct input_dev *input_allocate_device(void)
该函数实现动态分配一个 input_dev 设备结构体,这个函数较为简单。
2) int input_register_device(struct input_dev *dev)
该函数将一个 input_dev 注册到内核中。
3) void input_unregister_device(struct input_dev *dev)
该函数实现一个设备的注销一个 input_dev 设备。
4) void input_free_device(struct input_dev *dev)
该函数与 input_allocate_device 函数相对应,释放 input_dev 结构体。
9.3 入设备的事件报告
在事件到来时,例如当前按键按键,会去触发中断,这个时候处理器会去执行中断服务函数。
时中断已经响应,但是 Linux 内核并不知道此时按键是哪个,因此在中断服务函数中一般需要添加事
件报告。内核提供了以下函数来供驱动开发者使用:
1) void input_report_key(struct input_dev *dev, unsigned int code, int
value)
该函数实现向内核上报一个键盘事件,其中第一个参数为当前事件产生的输入设备;第二个参数
为键值编码,例如 KEY_AKEY_B 等等;第三个参数为键值,用 0 1 表示释放和按下。
2) void input_report_rel(struct input_dev *dev, unsigned int code, int
value)
该函数实现向内核上报一个 REL 事件,其中第一个参数为当前事件产生的输入设备;第二个参
数为事件码;第三个参数为事件的值。例如游戏杆为例,此时游戏杆的遥杆是 REL_X,这个 REL_X
为事件码;此时 REL_X 遥杆的值为 200,那么此时第三个参数就是 200
3) input_report_abs(struct input_dev *dev, unsigned int code, int value)
该函数实现向内核上报一个 ABS 事件,其中第一个参数为当前事件产生的输入设备;第二个参
数为事件码;第三个参数为事件的值。
完成了事件的上报还需要调用一个函数来实现内核的同步,该函数就是:
void input_sync(struct input_dev *dev)
这个函数事件将事件同步到内核中,表示事件已经结束。
linux 驱动开发指南 | 李山文
204
9.4 键模拟键盘示例
我们个按驱动现一只有键的,需的是于全 V3s
F1C200S 而言,其按键由 LRADC 实现, LRADC 有个缺点,只能检测哪个按下,按键释放没办法
检测,因此在写驱动的时候需要注意下。
9.4.1 动程序编写
下面我们利用按键来模拟键盘实现一个简单的输入设备。
1) 输入设备注册
首先我们在 probe 函数中对输入设备进行注册和初始化工作。
static int mykeypad_probe(struct platform_device *pdev)
{
int ret;
struct resource *res;
mykeypad_dev=input_allocate_device(); //向内核申请 input_dev 结构体
set_bit(EV_KEY,mykeypad_dev->evbit); //支持键盘事件
set_bit(EV_REP,mykeypad_dev->evbit); //支持键盘重复按事件
set_bit(KEY_A,mykeypad_dev->keybit); //支持按键 A
set_bit(KEY_B,mykeypad_dev->keybit); //支持按键 B
set_bit(KEY_C,mykeypad_dev->keybit); //支持按键 C
set_bit(KEY_D,mykeypad_dev->keybit); //支持按键 D
mykeypad_dev->name = pdev->name; //设备名称
mykeypad_dev->phys = "dev/input"; //设备文件路径
mykeypad_dev->open = NULL; //设备打开操作函数
mykeypad_dev->close = NULL; //设备关闭操作函数
mykeypad_dev->id.bustype = BUS_HOST; //设备总线类型
mykeypad_dev->id.vendor = 0x0001; //设备厂家编号
mykeypad_dev->id.product = 0x0001; //设备产品编号
mykeypad_dev->id.version = 0x0100; //设备版本
ret = input_register_device(mykeypad_dev); //注册 input_dev
if(ret)
{
input_free_device(mykeypad_dev);
printk(KERN_ERR "regoster input device failed!\n");
return ret;
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取 device 中的 LRADC_CTRL
keyadc_ctrl = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); //获取 device 中的 LRADC_INTC
keyadc_intc = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2); //获取 device 中的 LRADC_INTS
keyadc_ints = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3); //获取 device 中的 LRADC_DATA
linux 驱动开发指南 | 李山文
205
keyadc_data = ioremap(res->start,(res->end - res->start)+1);
ret = devm_request_irq(&pdev->dev, platform_get_irq(pdev, 0), mykeypad_irq, 0,
"mykey", (void*)&key_value);
*keyadc_ctrl&=0;
*keyadc_ctrl |= (2<<24) | (1<<8) | (1<<6);
*keyadc_intc |= (1<<4) | (1<<1);
*keyadc_ctrl |= (1<<0);
if(ret)
{
return ret;
}
return 0;
}
上面的是输入设备初始化部分,首先向内核申请一个结构体,然后设置输入设备的一些属性,
后将输入设备注册到内核中,由于我们使用的是中断方式,因此我们还需要申请一个中断号来实现中
断功能。
2) 实现中断服务函数
static irqreturn_t mykeypad_irq (int irq, void *dev_id) //中断服务函数
{
ssize_t key_value;
/*上报事件*/
if(((*keyadc_ints)&(1<<1)) != 0)
{
key_value = ((*keyadc_data)&(0x3f));
if(key_value == 24)
{
input_event(mykeypad_dev, EV_KEY, KEY_A , 1); //上报 EV_KEY 类型,button
,1(按下)
}
else if(key_value == 17)
{
input_event(mykeypad_dev, EV_KEY, KEY_B, 1); //上报 EV_KEY 类型,button
,1(按下)
}
else if(key_value == 11)
{
input_event(mykeypad_dev, EV_KEY, KEY_C, 1); //上报 EV_KEY 类型,button
,1(按下)
}
else
{
input_event(mykeypad_dev, EV_KEY, KEY_D, 1); //上报 EV_KEY 类型,button
,1(按下)
}
}
else if(((*keyadc_ints)&(1<<4)) != 0)
linux 驱动开发指南 | 李山文
206
{
if(key_value == 24)
{
input_event(mykeypad_dev, EV_KEY, KEY_A , 0); //上报 EV_KEY 类型,button
,0(释放)
}
else if(key_value == 17)
{
input_event(mykeypad_dev, EV_KEY, KEY_B, 0); //上报 EV_KEY 类型,button
,0(释放)
}
else if(key_value == 11)
{
input_event(mykeypad_dev, EV_KEY, KEY_C, 0); //上报 EV_KEY 类型,button
,0(释放)
}
else
{
input_event(mykeypad_dev, EV_KEY, KEY_D, 0); //上报 EV_KEY 类型,button
,0(释放)
}
}
input_sync(mykeypad_dev); // 上传同步事件,告诉系统有事件出现
*keyadc_ints |= ((1<<0) | (1<<1) | (1<<2) | (1<<3) | (1<<4));//清楚中断标
return IRQ_HANDLED;
}
上面的中断服务函数中向内核发送一个按键事件,发送完毕之后同步以下事件告知内核事件完成。
input_event 函数中最后一个参数 1 表示 KEY_A 按键按下,如果要表示按键释放,则用 0 表示。需要
注意的而是在中断服务函数中还需要清除中断标志。
3) 输入设备注销
当设备卸载之后还需要对我们注册的输入设备进行注销操作,如下:
static int mykeypad_remove(struct platform_device *pdev)
{
iounmap(keyadc_ctrl); //取消 LRADC_CTRL 映射
iounmap(keyadc_intc); //取消 LRADC_INTC 映射
iounmap(keyadc_ints); //取消 LRADC_INTS 映射
iounmap(keyadc_data); //取消 LRADC_DATA 映射
input_unregister_device(mykeypad_dev); //卸载类下的驱动设备
input_free_device(mykeypad_dev); //释放驱动结构体
return 0;
}
上面实现了输入设备的中断释放、输入设备注销以及输入设备结构体释放。下面是完整的驱动代
码,如下所示:
设备树新增设备节点:
/ {
linux 驱动开发指南 | 李山文
207
model = "Lichee Pi Zero";
compatible = "licheepi,licheepi-zero", "allwinner,sun8i-v3s";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
};
mykeypad: mykeypad@1c22800 {
compatible = "mykeypad";
reg = <0x01c22800 0x4>, <0x01c22804 0x4>, <0x01c22808 0x4>, <0x01c2280c 0x4>;
interrupts = <GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH>;
status = "okay";
};
};
在根节点中我们添加了一个 mykeypad@1c22800 的设备节点,该设备节点寄存器可查看数据手
册。中断号为 GIC_SPI30,即共享中断号的 30 中断号。
下面是驱动源码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/irq.h> //含有 IRQ_HANDLED IRQ_TYPE_EDGE_RISING
#include <linux/interrupt.h> //含有 request_irqfree_irq 函数
#include <linux/input.h>
#include <linux/irq.h>
struct input_dev *mykeypad_dev; //定义一个 input_dev 结构体
ssize_t volatile *keyadc_ctrl;
ssize_t volatile *keyadc_intc;
ssize_t volatile *keyadc_ints;
ssize_t volatile *keyadc_data;
static unsigned int key_value = 0; //定义一个变量保存按键值
static irqreturn_t mykeypad_irq (int irq, void *dev_id) //中断服务函数
{
linux 驱动开发指南 | 李山文
208
/*上报事件*/
if(((*keyadc_ints)&(1<<1)) != 0) //按键按下
{
key_value = ((*keyadc_data)&(0x3f)); //获取按键值
if(key_value == 24)
{
input_event(mykeypad_dev, EV_KEY, KEY_A , 1); //上报 EV_KEY 类型,1(按下)
}
else if(key_value == 17)
{
input_event(mykeypad_dev, EV_KEY, KEY_B, 1); //上报 EV_KEY 类型, 1(按下)
}
else if(key_value == 11)
{
input_event(mykeypad_dev, EV_KEY, KEY_C, 1); //上报 EV_KEY 类型, 1(按下)
}
else
{
input_event(mykeypad_dev, EV_KEY, KEY_D, 1); //上报 EV_KEY 类型, 1(按下)
}
}
else if(((*keyadc_ints)&(1<<4)) != 0) //按键释放
{
if(key_value == 24)
{
input_event(mykeypad_dev, EV_KEY, KEY_A , 0); //上报 EV_KEY 类型, 0(释放)
}
else if(key_value == 17)
{
input_event(mykeypad_dev, EV_KEY, KEY_B, 0); //上报 EV_KEY 类型, 0(释放)
}
else if(key_value == 11)
{
input_event(mykeypad_dev, EV_KEY, KEY_C, 0); //上报 EV_KEY 类型, 0(释放)
}
else
{
input_event(mykeypad_dev, EV_KEY, KEY_D, 0); //上报 EV_KEY 类型, 0(释放)
}
}
input_sync(mykeypad_dev); // 上传同步事件,告诉系统有事件出现
*keyadc_ints |= ((1<<0) | (1<<1) | (1<<2) | (1<<3) | (1<<4));
return IRQ_HANDLED;
}
static int mykeypad_probe(struct platform_device *pdev)
linux 驱动开发指南 | 李山文
209
{
int ret;
struct resource *res;
mykeypad_dev=input_allocate_device(); //向内核申请 input_dev 结构体
set_bit(EV_KEY,mykeypad_dev->evbit); //支持键盘事件
set_bit(EV_REP,mykeypad_dev->evbit); //支持键盘重复按事件
set_bit(KEY_A,mykeypad_dev->keybit); //支持按键 A
set_bit(KEY_B,mykeypad_dev->keybit); //支持按键 B
set_bit(KEY_C,mykeypad_dev->keybit); //支持按键 C
set_bit(KEY_D,mykeypad_dev->keybit); //支持按键 D
mykeypad_dev->name = pdev->name; //设备名称
mykeypad_dev->phys = "mykeypad/input0"; //设备文件路
mykeypad_dev->open = NULL; //设备打开操作函
mykeypad_dev->close = NULL; //设备关闭操作函
mykeypad_dev->id.bustype = BUS_HOST; //设备总线类型
mykeypad_dev->id.vendor = 0x0001; //设备厂家编号
mykeypad_dev->id.product = 0x0001; //设备产品编号
mykeypad_dev->id.version = 0x0100; //设备版本
ret = input_register_device(mykeypad_dev); //注册 input_dev
if(ret)
{
input_free_device(mykeypad_dev);
printk(KERN_ERR "regoster input device failed!\n");
return ret;
}
mykeypad_dev->name="mykeypad";
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);//获取 device 中的 LRADC_CTRL
keyadc_ctrl = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);//获取 device 中的 LRADC_INTC
keyadc_intc = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 2);//获取 device 中的 LRADC_INTS
keyadc_ints = ioremap(res->start,(res->end - res->start)+1);
res = platform_get_resource(pdev, IORESOURCE_MEM, 3);//获取 device 中的 LRADC_DATA
keyadc_data = ioremap(res->start,(res->end - res->start)+1);
ret = devm_request_irq(&pdev->dev, platform_get_irq(pdev, 0), mykeypad_irq, 0,
"mykey", (void*)&key_value);
*keyadc_ctrl&=0;
*keyadc_ctrl |= (1<<12); //单次模式,其他默
*keyadc_intc |= (1<<4) | (1<<1);
*keyadc_ctrl |= (1<<0);
if(ret)
{
return ret;
}
return 0;
linux 驱动开发指南 | 李山文
210
}
static int mykeypad_remove(struct platform_device *pdev)
{
iounmap(keyadc_ctrl); //取消 LRADC_CTRL 映射
iounmap(keyadc_intc); //取消 LRADC_INTC 映射
iounmap(keyadc_ints); //取消 LRADC_INTS 映射
iounmap(keyadc_data); //取消 LRADC_DATA 映射
input_unregister_device(mykeypad_dev); //卸载类下的驱动设备
input_free_device(mykeypad_dev); //释放驱动结构体
return 0;
}
static struct of_device_id mykeypad_match_table[] = {
{.compatible = "mykeypad",},
};
static struct platform_device_id mykeypad_device_ids[] = {
{.name = "mykeypad",},
};
static struct platform_driver mykeypad_driver=
{
.probe = mykeypad_probe,
.remove = mykeypad_remove,
.driver={
.name = "mykeypad",
.of_match_table = mykeypad_match_table,
},
.id_table = mykeypad_device_ids,
};
module_platform_driver(mykeypad_driver);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("mykeypad_driver"); //简单的描述
9.4.2 测试序编写
为了检验上面的驱动程序是否符合预期,我们需要编写一个测试程序来检测模拟键盘是否能正常
工作。
#include <string.h>
linux 驱动开发指南 | 李山文
211
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#define CHECK_POINT
int main(int argc, char** argv)
{
int fd = open(argv[1],O_RDONLY);
#ifdef CHECK_POINT
printf("fd = %d\n",fd);
#endif
struct input_event t;
printf("size of t = %d\n",sizeof(t));
while(1)
{
printf("while -\n");
int len = read(fd, &t, sizeof(t));
if(len == sizeof(t))
{
printf("read over\n");
if(t.type==EV_KEY)
{
printf("key %d %s\n", t.code, (t.value) ? "Pressed" : "Released");
if(t.code == KEY_ESC)
break;
}
}
#ifdef CHECK_POINT
printf("len = %d\n",len);
#endif
}
return 0;
}
上面的测试代码较为简单,可以看到上面的主函数中有一个死循环,这个死循环在不断查询输入
的类型是不是键盘事件,如果是键盘事件,则打印其键盘编码;如果是 ESC 键,则退出主函数,结
束程序。该程序并不会一直无限次打印,而是只有按键按下后才会打印键盘按键编码。
我们将上面的驱动程序编译为.ko 文件,然后下载到根文件系统中, insmod 命令挂载驱动。
驱动测试程序编译为可执行文件,下载到根文件系统中。在开发板上启动系统,进入根文件系统后,
挂载.ko 文件,如下:
# insmod mykeypad.ko
[ 11.331283] mykeypad: loading out-of-tree module taints kernel.
[ 11.338362] input: Unspecified device as /devices/virtual/input/input0
可以看到我们的模拟键盘设备已经挂载成功,同时在/sys/devices/virtual/input 目录下面产生了一
linux 驱动开发指南 | 李山文
212
input0 设备,该设备就是我们的驱动。现在我们用测试程序对其进行测试,在终端输入:
# ./mykeypad.exe /dev/input/event0
fd = 3
size of t = 16
while -
此时终端貌似卡住了,其实并不是,这是在等待按键事件,我们现在按 LRADC 按键四个不同键
值,此时终端会打印按键值,如下:
key 32 Pressed
len = 16
while -
read over
len = 16
while -
read over
key 32 Released
len = 16
while -
read over
len = 16
while -
read over
key 46 Pressed
len = 16
while -
read over
len = 16
while -
read over
key 46 Released
len = 16
while -
read over
len = 16
while -
read over
key 48 Pressed
len = 16
while -
read over
len = 16
while -
read over
key 48 Released
len = 16
while -
linux 驱动开发指南 | 李山文
213
read over
len = 16
while -
read over
key 30 Pressed
len = 16
while -
read over
len = 16
while -
read over
key 30 Pressed
len = 16
while -
read over
len = 16
while -
read over
key 30 Pressed
len = 16
这些键值分别是 32464830我们对照 Linux 键盘键值表
37
(参考附录 A可以看到,这
值对应的正是 DCBA这样我们就实现了一个只有四个按键的键盘。当然我们也可以增加按键
数目来实现更多的键值。
37
请参考 include\uapi\linux\input-event-codes.h
linux 驱动开发指南 | 李山文
214
第十章 SPI 子系统
SPI Serial Peripheral Interface串行外设接口。该总线协议由 Motorola 首先在其 MC68HCXX
系列处理器上定义的,由于其高速的传输以及简单的协议时序,被各大设备厂家接受,随后该协议被
广泛使用。
10-1 常见的 SPI 口设备
SPI 协议有四根线,分别是 CSSCLKMOSIMISO,这四根线的定义如下:
CSChip Select,即片选线
SCLKSerial Clock,即串行时钟线
MOSIMaster Out Slave Input,即主机输出从机输入线
MISOMaster Input Slave Out,即主机输入从机输出线
这四根线可以实现数据的完整传输,可以看到由于有两根数据线(输入和输出数据线独立)
因此该协议是一种全双工协议
38
10-2 SPI 主机从机电路连接图
为了能够为上层提供统一的接口,Linux 内核实现了 SPI 子系统,该子系统将底层的实现过程留
给驱动工程师来实现,而仅仅提供一个标准的接口函数。
38
全双工:在同一时刻即可接收数据也可发送数据;半双工:在同一时刻只能发送或者接收数据。
linux 驱动开发指南 | 李山文
215
SPI 总共有四种模式
39
,这四种模式由 CPOL CPHA 这两个决定是其中的哪一种模式,CPOL
CPHA 的定义如下:
CPOL:时钟极性
0:表示初始时钟状态为低电平时,第一个边沿为上升沿
1:表示初始时钟状态为高电平时,第一个边沿为下降沿
CPHA:时钟相位
0:表示数据在下降沿有效时,输出在上升沿时改变
1:表示数据在上升沿有效时,输出在下降沿时改变
10-2 SPI 四种工作模式(CPHA=0 左,CPHA=1 右)
10.1 SPI 子系统框架
首先我们来看下 SPI 子系统的大体架构,如下图所示:
10-3 SPI 驱动框架图
从上面的结构图可以看到 SPI 子系统驱动中最底层为 SPI 控制器驱动,该控制器驱动需要实现
SPI 控制器的最基本的读写操作,这些操作用来提供给 SPI 子系统。实际上驱动开发分为两个部分,
39
具体协议请阅读:SPI Block Guide 文档(该文档由 Motorola 公司制定)
linux 驱动开发指南 | 李山文
216
一部分由 BSP 开发工程师来完成 SPI 控制器驱动的实现,这部分和 SoC 硬件非常紧密;而那些 SPI
提供的接口则是给驱动开发工程师来完成实现具体的驱动,例如开发一个 SPI 网卡驱动。
可以看到 Linux 内核将设备驱动开发的工作分的更细了,这样的好处是可以让驱动工程师和 BSP
开发工程师并行开发,提高开发效率,同时也使得驱动框架变得更加合理,层次化,这也体现了程序
思想:低耦合,高内聚。
10-4 驱动与 BSP 之间的关系
由于篇幅有限,本书不对 BSP 开发做讲解,只对驱动开发做讲解,因此这里我们将重点说明如
何使用 SPI 子系统来完成驱动程序的开发。由于 SPI 子系统用到了中断线,因此我们在编写驱动之前
需要实现设备树,来指定我们的中断号以及其他信息。
10.2 SPI 设备结构体
和其他子系统一样,Linux 内核也为其定义了统一的设备结构体,们需要根据设备结构体的内
容来完成设备的初始化同时实现其需要的接口。Linux 内核中 SPI 的结构体
40
具体如下:
struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_controller *master; /* compatibility layer */
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
bool rt;
#define SPI_NO_TX BIT(31) /* no transmit wire */
#define SPI_NO_RX BIT(30) /* no receive wire */
/*
* All bits defined above should be covered by SPI_MODE_KERNEL_MASK.
* The SPI_MODE_KERNEL_MASK has the SPI_MODE_USER_MASK counterpart,
* which is defined in 'include/uapi/linux/spi/spi.h'.
* The bits defined here are from bit 31 downwards, while in
* SPI_MODE_USER_MASK are from 0 upwards.
* These bits must not overlap. A static assert check should make sure of that.
* If adding extra bits, make sure to decrease the bit index below as well.
40
该结构体在 Linux 内核源码 include\linux\spi\spi.h 中定义
linux 驱动开发指南 | 李山文
217
*/
#define SPI_MODE_KERNEL_MASK (~(BIT(30) - 1))
u32 mode;
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
const char *driver_override;
int cs_gpio; /* LEGACY: chip select gpio */
struct gpio_desc *cs_gpiod; /* chip select gpio desc */
struct spi_delay word_delay; /* inter-word delay */
/* the statistics */
struct spi_statistics statistics;
/*
* likely need more hooks for more protocol options affecting how
* the controller talks to each chip, like:
* - memory packing (12 bit samples into low bits, others zeroed)
* - priority
* - chipselect delays
* - ...
*/
};
下面对其重要成员进行简单说明:
dev该成员为设备文件,一般将其设置为 platform dev 即可。
max_speed_hz最大传输速率,单位 Hz在驱动中我们可以使用 spi_transfer.speed_hz
来修改传输速率。
chip_select:片选线,默认低电平有效,这个值可以用 SPI_CS_HIGH 来设置。
mode:数据传输模式,这里的模式指 SPI 传输时先传输低位还是高位,默认为先传输高位。
如果想先传输低位,则可以通过指定 SPI_LSB_FIRST 来设置。
irq表示 spi 传输时的中断号,该值内核中通过解析设备树来获取。
上面是 spi 子系统的设备结构体,实际上这些设备结构体的初始化并不需要驱动开发工程师来
完成,因为现在已经引入了设备树,在系统启动后,内核会自动解析设备树,然后会填充相关的设备
信息,这也是为何我们必须填写设备树的原因。
10-5 SPI 子系统设备初始化
下面我们看一个典型的 SPI 设备树节点:
spi0: spi@1c68000 {
compatible = "allwinner,sun8i-h3-spi";
reg = <0x01c68000 0x1000>;
interrupts = <GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_SPI0>;
clock-names = "ahb", "mod";
dmas = <&dma 23>, <&dma 23>;
dma-names = "rx", "tx";
pinctrl-names = "default";
linux 驱动开发指南 | 李山文
218
pinctrl-0 = <&spi0_pins>;
resets = <&ccu RST_BUS_SPI0>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
上面是 V3s SPI 设备树节点,首先 compatible = "allwinner,sun8i-h3-spi"用来匹配
SPI 控制器驱动程序reg = <0x01c68000 0x1000>属性指定了 SPI 控制器的寄存器物理地址;
interrupts = <GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>属性指定了该 SPI 控制器的中断线为 SPI
65 个中断号,触发标志位高电平触发;clocks = <&ccu CLK_BUS_SPI0>, <&ccu CLK_SPI0>
指定了该 SPI 控制器的时钟,需要注意的是时钟也有自己的时钟子系统,时钟子系统和硬件联系相
当紧密,因此这里不对其进行说明;clock-names = "ahb", "mod"是用来方便驱动查找时钟用
的,这个是一个技巧,clock-names clocks 是对应的,用来描述后者列表中的信息,这里表示
CLK_BUS_SPI0 ahb 时钟,CLK_SPI0 mod 时钟,这样我们在设备驱动中只需要利用这两个就
可以获取其相应的值,这一点在其他地方也有用到。pinctrl-names GPIO 子系统中的属性,该
属性一般为 default,即默认启动状态,这些状态还有 sleep idle;然后 pinctrl-0 属性也是
GPIO 子系统中的属性,该属性表示引脚的 default 状态该引脚作为 SPI 引脚使用,其相对的是
pinctrl-1 属性表示空闲时该引脚可作为其他用处,这些属性存在的目的主要是由于 GPIO 可以复
用;resets 属性指定了其复位信号线;status 状态设置了 disable,我们在使用时需要将其设置
okay 状态;#address-cells #size-cells 指定了其子节点的 reg 列表格式,从其值可以看
到其子节点的 reg 没有大小列表,实际上 SPI 子节点的 reg 表示从设备的编号,编号从 0 开始。
现在我们需要添加两个 SPI 设备都挂载在 SPI0 总线上,这两个设备分别是 ili9341 控制器的
TFT 液晶显示屏和 MAX6675 热电偶探测器,这样我们可以这样写:
&spi0 {
cs-gpios = <&pio 6 0 GPIO_ACTIVE_LOW>, <&pio 6 1 GPIO_ACTIVE_LOW>;
status = "okay";
ili9341@0 {
compatible = "ilitek,ili9341";
reg = <0>;
spi-max-frequency = <15000000>;
spi-cpol;
spi-cpha;
rotate = <270>;
bgr;
fps = <10>;
buswidth = <8>;
reset-gpios = <&pio 1 7 GPIO_ACTIVE_LOW>;
dc-gpios = <&pio 1 5 GPIO_ACTIVE_LOW>;
debug = <0>;
};
max6675@1 {
compatible = "max,max6675";
reg = <1>;
spi-max-frequency = <1000000>;
}
};
linux 驱动开发指南 | 李山文
219
上面我们在 spi0 节点中添加了 ili9341@0 max6675@1 节点。我们先来看 cs-gpios 属性,
该属性为 SPI 子系统内部属性,指定了 SPI 主设备的片选线,这里我们指定的是 GPIOF0 作为片选
线,GPIO_ACTIVE_LOW 表示默认都是低电平有效,这样其子节点中的 reg 属性就表示从设备的地
址;如何这里没有指定 cs-gpios 属性,那么系统默认 cs spi0 总线上的 cs,这里我们指定了
cs-gpios,则系统会使用我们这里指定的片选,而不会去使用 spi0 的片选线了,我们再看下两个
从设备节点:
首先不知读者是否观察到一个问题,在 4.2 章节中指出,并非所有包含 compatible 的节点都
会被内核解析为 platform_device,解析为 platform_device 是要有几个条件的,这里再次对其
进行说明下:
一般来说,内核只会将根节点的子节点注册为 platform_device而根节点的子节点的子节点都将不
会被注册为 platform_device,但是仍然会将其生成 device_node。不过有一种情况需要非常注意,
当根节点的子节点其 compatible 属性为"simple-bus""simple-mfd""isa""arm,amba-bus"时,该节
点的一级子节点将会被注册为 platform_device 设备。
很显然,这里的两个子节点并不会被解析为 platform_device 设备,只有 spi0 节点会被注册
为平台设备,这样我们虽然在 spi0 节点中添加了两个子节点,但是并不会被注册为平台设备,这
就意味着内核并不会为这两个子节点生成设备资源,但只要有 compatible 属性的节点都会去尝试
匹配驱动,即去执行 probe 函数。下面我们来看这两个节点:
ili9341@0 节点中 compatible = "ilitek,ili9341"用来匹配设备驱动;reg = <0>用来指
定该节点的地址,即第一个设备,reg = <1>表示挂载的第二个设备;spi-max-frequency 属性指
定了 spi 的工作最大速率,单位为 Hz,这里指定的最大速率为 15MHzspi-cpol spi-cpha
性是 SPI 子系统内部的特有属性,用来指定 SPI 的工作模式:
10-1 SPI 工作模
41
工作模式
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_X_MASK (SPI_CPOL|SPI_CPHA)
对于设备而言,只要在设备树中注明了 spi-cpol 或者 spi-cpha 则表示其各自的值为 1,例如
上面 ili9341 节点 SPI 工作模式为 3,而 max6675 没有注明这两个属性,因此工作在模式 0。剩下
的属性都是 ili9341 驱动特有的属性,这里不对其进行解释。
10.3 SPI 驱动注册与注销
和其他驱动一样,SPI 设备也是需要注册的,在此之前,我们先熟悉下 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;
};
该结构体和 platform_driver 结构体类似:
41
更详细的宏请查看 Linux 源码:include\uapi\linux\spi\spi.h
linux 驱动开发指南 | 李山文
220
id_table:用来做设备匹配的表,其作用和 platform_driver 相同。
(*probe)spi:该函数指针指向 spi 驱动的 probe 函数,即探测函数。
(*remove)spi:该函数指针指向 spi 驱动的 remove 函数,即移除函数。
(*shutdown)spi:该函数指针指向 spi 驱动的 shutdown 函数,即关闭函数。
driver:该成员为设备驱动结构体,该结构体中保存设备的一些信息。
现在我们可以实例化一个 spi 驱动:
static struct spi_driver w25qxx_cdev = {
.driver = {
.name = "w25qxx",
.owner = THIS_MODULE,
},
.probe = w25qxx_probe,
.remove = w25qxx_remove,
.id_table = w25qxx_ids,
};
8.3.1 SPI 驱动的注册
上面我们实例化了一个 w25qxx_cdev 驱动结构体,该结构体指定了一些必要的函数和信息,如
probe 函数、remove 函数等等。和 platform 设备驱动一样,Linux 提供了如下函数供驱动开发者
使用:
#define spi_register_driver(driver) __spi_register_driver(THIS_MODULE,
driver)
其中__spi_register_driver 函数原型如下:
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv)
我们只需要调用 spi_register_driver 函数来注册我们的 spi 驱动即可,例如上面我们定义了一
w25qxx_cdev 的驱动,我们可以这样对其注册:
ret = spi_register_driver(&w25qxx_cdev);
返回值为 0 表示注册成功,否则注册失败。
8.3.2 SPI 驱动的注销
当我们卸载驱动时,我们需要将驱动注销掉,Linux 内核提供了如下 API
static inline void spi_unregister_driver(struct spi_driver *sdrv)
该函数实现了 SPI 驱动的注销,参数为 spi 驱动结构体,例如上面我们定义了一个 w25qxx_cdev
驱动,我们可以这样来注销它:
spi_unregister_driver(&w25qxx_cdev);
函数无返回值。
10.4 SPI 数据传输
上面我们已经知道了如何去注册一个 SPI 驱动,但是 SPI 最终还是传输数据用的,因此我们必须
知道如何去发送数据。Linux SPI 子系统的数据传输抽象为 SPI IO 模型,该模型类似于客户端通
信,我们发送数据被抽象为向客户端发送一条或多条消息的过程。而一条消息又由多个传输包组成,
如下图所示:
linux 驱动开发指南 | 李山文
221
10-6 SPI 数据传输过程
所有的数据最终会由 SPI 控制器驱动发出去,但是我们如何将数据传输给控制器驱动呢?Linux
提供了两种数据结构,分别是 spi_message spi_transfer,这两个数据结构如上图所示。下面我们来
看这两个数据结构的具体内容。
10.4.1 spi_transfer 结构体
该结构体是 SPI 数据传输的最小单元,一个 spi_message 包含一个或多个 spi)transfer 传输包,该
传输结构体如下:
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma;
dma_addr_t rx_dma;
struct sg_table tx_sg;
struct sg_table rx_sg;
unsigned dummy_data:1;
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;
struct spi_delay delay;
struct spi_delay cs_change_delay;
struct spi_delay word_delay;
u32 speed_hz;
u32 effective_speed_hz;
unsigned int ptp_sts_word_pre;
unsigned int ptp_sts_word_post;
struct ptp_system_timestamp *ptp_sts;
bool timestamped;
struct list_head transfer_list;
#define SPI_TRANS_FAIL_NO_START BIT(0)
u16 error;
};
可以看到,该结构体相对来说还有点复杂,我们只看比较重要的部分。
tx_buf:发送数据缓存区,对于只读数据而言,此值应该为 NULL
rx_buf:接收数据缓存区,对于只读数据而言,此值也应该为 NULL
len:发送和接收缓存区的大小,发送和缓存区的大小必须相同。
linux 驱动开发指南 | 李山文
222
tx_dma:采用 DMA 方式发送的地址
rx_dma:采用 DMA 方式接收的地址
cs_change:指定数据传输完毕后 cs 片选线的状态
bits_per_word:单次传输数据时的长度,这里的单位为字,即 32 位(4 个字节)
cs_change_delay:指定传输数据之后延时多久再改变 cs 片选线的状态
speed_hz:传输数据时的速率,设置此时会覆盖设备树中的指定的最大值
一般我们用的最多的是 tx_bufrx_buflenbits_per_wordcs_change 这几个参数。下面我们举
个例子简单说明下,我们现在发送一串数据如下:
char tx_data[16] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,
0x0c,0x0d,0x0e,0x0f};
char rx_data[16] = {0};
struct spi_transfer xfer;
xfer.tx_buf = tx_data;
xfer.rx_buf = rx_data;
xfer.len = sizeof(tx_data);
xfer.bits_per_word = 8;
上面设置了 SPI 的传输事务为以 8 位即一个字节传输,数据缓存区和数据长度都指定了,下面我
们来看消息事务如何使用。
10.4.2 spi_message 结构体
消息事务包含了一个或者多个传输事务,最终数据传输给 SPI 控制器驱动程序的是以消息的方
式,因此我们需要知道如何去操作。spi_message 的结构体如下:
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
struct list_head queue;
void *state;
struct list_head resources;
};
结构体看起来较为简单,其参数说明如下:
transfers传输事务,该成员指向一个消息事务链表,将多个传输事务链接在一起。
is_dma_mapped指定传输是否使用 DMA 方式
complete传输完毕回调函数,当传输完毕后会指定该函数指针所指向的函数
context回调函数传入的参数
frame_length传输的长度,表示在该消息事务中传输的总字节数
actual_length实际传输长度,表示在该消息事务中传输成功的总字节数
status传输报告状态标志,当此值为 0 时表示传输成功,否则传输失败
linux 驱动开发指南 | 李山文
223
实现一个完整的数据传输需要将 spi_transfer 添加到 spi_message 中,首先我们需要先初始化消息
事务,Linux 内核提供了如下 API
static inline void spi_message_init(struct spi_message *m)
该函数对一个消息进行初始化,在将 spi_transfer 传输事务添加到消息事务之前必须使用该函数
来初始化消息事务。初始化完毕之后我们就可以将 spi_transfer 添加到 spi_message 中了,Linux 内核
提供了如下 API 来实现将 spi_transfer 添加到 spi_message 中:
static inline void spi_message_add_tail(struct spi_transfer *t, struct
spi_message *m)
添加完毕之后我们需要使用一个命令来通知 SPI 控制器驱动程序来进行数据传输,接口如下:
extern int spi_sync(struct spi_device *spi, struct spi_message *message);
extern int spi_async(struct spi_device *spi, struct spi_message *message);
这两个函数的第一个参数是 spi 设备,第二个参数是传输消息,进行上述操作后就实现了一个完
整的数据传输。这两个函数有区别,第一个函数是一个同步传输函数,即此时 CPU 将会在此等待传
输结束,因此该函数不能用于中断中。第二个函数是一个异步传输函数,此时 CPU 发送数据之后会
执行其他的,当数据传输完毕后,此时会去调用回调函数。下面我们举个简单的例子来说明之:
char tx_data[16] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,
0x0b,0x0c,0x0d,0x0e,0x0f};
char rx_data[16] = {0};
struct spi_transfer xfer;
struct spi_message msg;
xfer.tx_buf = tx_data;
xfer.rx_buf = rx_data;
xfer.len = sizeof(tx_data);
xfer.bits_per_word = 8;
spi_message_init(&msg); //初始化 msg
spi_message_add_tail(&xfer,&msg); // xfer 添加到 msg
spi_sync(spi,&msg);//通知 SPI 控制器驱动传输数据
上面列举了最常用的使用方式,但 Linux 还提供了大量的其他操作函数
42
,例如 spi_transfer
spi_message 中删除的函数、初始化并添加多个传输事务 spi_message_init_with_transfers
的函数。为了方便驱动开发者使用,Linux 内核提供了一个接口:
static inline int spi_sync_transfer(struct spi_device *spi, struct
spi_transfer *xfers,unsigned int num_xfers)
spispi 设备指针
xfersspi_transfe 传输事务
num_xfers传输事务的个数
该接口可以实现不需要用户去创建 spi_message,而只需要创建 spi_transfer 结构体即可,
实际上该函数的内部实现还是先创建 spi_message,然后将 spi_transfer 添加到 spi_message
中,最后在发送出去,只是这个添加的工作该函数已经帮忙做了。例如我们可以这样就实现了数据
的发送:
char tx_data[3] = {0x01,0x02,0x03};
char rx_data[3] = {0};
42
更多函数请查看 Linux 内核源码:include\linux\spi\spi.h
linux 驱动开发指南 | 李山文
224
struct spi_transfer xfer;
xfer.tx_buf = tx_data;
xfer.rx_buf = rx_data;
xfer.len = sizeof(tx_data);
xfer.bits_per_word = 8;
ret = spi_sync_transfer(spi,&xfer,1); //将数据发送出去,同步方式
10-7 例子中 SPI 据发送时序
这里需要说明的是 SPI 是一个全双工的通信总线,因此在写的过程中也会读,但对于设备而言,
一般主设备发送数据时不会立刻写,而是等到主设备发送数据完毕后从设备再发送数据给主设备。
样就出现了我们在操作的时候大部分都是读的上一次写数据时候的 MISO 上的数据,这些数据一般都
是无效的,因此我们在读设备的时候需要注意这一点。
Linux 内核中还有其他简单的接口,例如 spi_writespi_read
43
,需要注意的是,这些接口都
是同步接口,底层调用的还是 spi_sync_transfer 函数。
10.5 SPI 驱动示
为了让读者更直观的理解 SPI 子系统,我们来实现一个 W25Q128 的缺损驱动程序。
W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128Mbit,即
16M 字节。W25Q128 可以支持 SPI 0 和模式 3,也就是 CPOL=0/CPHA=0
CPOL=1/CPHA=1 这两种模式。我们在写驱动的时候使用模式 3,即 CPOL=1CPHA=1对于 W25Q128
需要注意的是在写数据的时候必须按整页整页的写;而且在一个地址上写入数据后不能再在该地址上
直接写入数据,只能先将该地址上的数据全部擦除后再写入,对于 W25Q128,最小的擦除单位是扇区。
W25Q128 的常用操作命令如下:
10-1 W25Q128 常用命令
指令名称
操作码
说明
Write Enable
06H
写使能
Write Disable
04H
写失能
Read Status Register
35H
读控制和状态寄存器
Write Status Register
01H
写控制和状态寄存器
Read Data
03H
读数据
Fast Read
0BH
快速读数据
Page Program
02H
页编程
Sector Erase(4KB)
20H
扇区擦除
Block Erase(32KB)
52H
32KB 块擦除
Block Erase(64KB)
D8H
64KB 块擦除
Chip Erase
C7H,60H
全片擦除
Read ID
ABH,90H,92H,94H
读取芯片 ID
43
更多接口请阅读源码 include\linux\spi\spi.h
linux 驱动开发指南 | 李山文
225
10-1 W25Q128 操作命令格式
可以看到上面我们读写 W25Q128 芯片时只需要按照命令格式来就可以了,下面我们简单说下读和写
的命令流程。读写命令的流程主要有:
1. 发送命令操作码
2. 发送操作地址
3. 发送/接收相关数据
例如,对于读取地址 0xff0125 的数据,数据长度为 256Byte
SPI 总线先 write0x03 0xff 0x01 0x25 0xff 然后 read 数据。
对于写入地址 0xff0125,数据长度为 256Byte,数据为[0x...]
SPI 总线先 write0x02 0xff 0x01 0x25 0xff 然后 write 数据[0x...]
注意:读写之前我们需要开启写使能。
下面我们来开始实现驱动程序。首先,设备树文件中我们需要编写设备节点,设备树节点添加如下:
&spi0 {
status = "okay";
w25q128@0 {
compatible = "test,w25qxx";
reg = <0>;
linux 驱动开发指南 | 李山文
226
spi-max-frequency = <150000>;
spi-cpol;
spi-cpha;
buswidth = <8>;
};
};
上面的 spi-cpol spi-cpha 指定了 SPI 的传输模式为模式 3,即 CPOL=1CPHA=1buswidth
= <8>指定了 SPI 传输数据宽度为 8 位。设备树的设备节点编写完毕后,我们需要开始编写 W25Q128
的设备驱动了。
为了不用编写应用测试程序,我们的驱动框架采用 sysfs 设备文件系统,这样我们操作驱动可
以直接在根文件系统中操作设备文件。
首先是 probe 函数的实现,如下所示:
static int w25qxx_probe(struct spi_device *spi)
{
int ret;
ret = device_create_file(&spi->dev,&dev_attr_w25q128);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create w25q128_dev file failed!\n");
return -1;
}
w25qxx_spi = spi_dev_get(spi);
w25qxx_init();
return 0;
}
上面的代码比较简单,我们在/sys/devices/目录下创建一个 w25q128 的属性文件。属性定义
如下:
static DEVICE_ATTR(w25q128, 0660, w25qxx_show, w25qxx_store); //定义文件属
可以看到属性文件名为 w25q128读写权限为 0660即可读可写,操作函数句柄为 w25qxx_show
以及 w25qxx_store 这两个函数。为了简单,便于读者理解 SPI 驱动的框架,这里不会最复杂的事
情,只是简单的使用 Read Status Register Sector Erase(4KB)这两个命令来验证驱动。
w25qxx_store 函数如下:
static ssize_t w25qxx_store(struct device *dev,struct device_attribute *attr,const
char *buf, size_t count)
{
int ret;
int erase_addr=0x123456;
tx_data[0]=ERASE_OPCODE;
tx_data[1]=(erase_addr>>16)&0xff;
tx_data[2]=(erase_addr>>8)&0xff;
tx_data[3]=(erase_addr)&0xff;
xfer.len = 4;
ret = spi_sync_transfer(w25qxx_spi,&xfer,1); //擦除操作
if(ret)
{
linux 驱动开发指南 | 李山文
227
printk(KERN_INFO"erase w25q128 failed!\n");
}
tx_data[0]=R_STS_OPCODE;
ret = spi_write_then_read(w25qxx_spi,tx_data,1,rx_data,1);
return count;
}
首先开始初始化要发送的数据,然后设置发送数据的长度,最后使用 spi_sync_transfer 或者
spi_write_then_read 函数来将数据发送出去。这里使用的都是同步数据发送,因为没有涉及到大
量的数据传输,为此简单考虑。
说明:spi_write_then_read 该函数是 Linux 内核提供的一个先写数据后读数据的操作接
口,这样我们在读操作的时候前面会有写命令操作,这正好符合我们的时序,因此该函数用途也很
广泛。
上面的 w25qxx_store 函数仅仅做了两件事,首先做了一个擦除操作,擦除地址为 0x123456
第二件事是做了一个读寄存器操作。下面是 w25qxx_show 的实现:
static ssize_t w25qxx_show(struct device *dev,struct device_attribute *attr,char *buf)
{
return sprintf(buf,"read status register=%d\n",rx_data[0]);
}
可以看到这里仅仅是将读到的数据输出到终端,我们使用 cat 命令就可以查看上面读取寄存器
的值了。剩下的就是一些匹配表的定义了,如下:
static struct of_device_id w25qxx_match_table[] = {
{.compatible = "test,w25qxx",},
};
static struct spi_device_id w25qxx_ids[] = {
{.name = "w25q128",},
};
static struct spi_driver w25qxx_cdev = {
.driver = {
.name = "w25qxx",
.owner = THIS_MODULE,
.of_match_table = w25qxx_match_table,
},
.probe = w25qxx_probe,
.remove = w25qxx_remove,
.id_table = w25qxx_ids,
};
其过程较为简单,完全是模板话的东西,因此这里不再对其进行累述。下面是完整的代码:
首先是设备树节点:
&spi0 {
status = "okay";
w25q128@0 {
compatible = "test,w25qxx";
linux 驱动开发指南 | 李山文
228
reg = <0>;
spi-max-frequency = <150000>;
spi-cpol;
spi-cpha;
buswidth = <8>;
};
};
然后是驱动源码,如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/spi/spi.h>
#include <linux/kobject.h> //包含 sysfs 件系统对象类
#include <linux/sysfs.h> //包含 sysfs 操作文件函数
#define ERASE_OPCODE 0x20 //擦除操作码
#define R_STS_OPCODE 0x35 //读状态操作码
static char tx_data[16] =
{0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f};
static char rx_data[16] = {0};
struct spi_transfer xfer;
struct spi_device *w25qxx_spi; //保存 spi 设备结构体
static ssize_t w25qxx_show(struct device *dev,struct device_attribute *attr,char *buf)
{
return sprintf(buf,"read status register=%d\n",rx_data[0]);
}
static ssize_t w25qxx_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
int ret;
int erase_addr=0x123456;
tx_data[0]=ERASE_OPCODE;
tx_data[1]=(erase_addr>>16)&0xff;
tx_data[2]=(erase_addr>>8)&0xff;
tx_data[3]=(erase_addr)&0xff;
xfer.len = 4;
linux 驱动开发指南 | 李山文
229
ret = spi_sync_transfer(w25qxx_spi,&xfer,1); //擦除操作
if(ret)
{
printk(KERN_INFO"erase w25q128 failed!\n");
}
tx_data[0]=R_STS_OPCODE;
ret = spi_write_then_read(w25qxx_spi,tx_data,1,rx_data,1);
return count;
}
static DEVICE_ATTR(w25q128, 0660, w25qxx_show, w25qxx_store); //定义文件属
static void w25qxx_init(void)
{
xfer.tx_buf = tx_data;
xfer.rx_buf = rx_data;
xfer.len = sizeof(tx_data);
xfer.bits_per_word = 8;
memset(rx_data,0,16);
}
static int w25qxx_probe(struct spi_device *spi)
{
int ret;
ret = device_create_file(&spi->dev,&dev_attr_w25q128);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create w25q128_dev file failed!\n");
return -1;
}
w25qxx_spi = spi_dev_get(spi);
w25qxx_init();
return 0;
}
static int w25qxx_remove(struct spi_device *spi)
{
device_remove_file(&spi->dev,&dev_attr_w25q128);//删除属性文件
printk(KERN_INFO "exit sysfs w25qxx!\n");
return 0;
}
static struct of_device_id w25qxx_match_table[] = {
{.compatible = "test,w25qxx",},
};
static struct spi_device_id w25qxx_ids[] = {
{.name = "w25q128",},
linux 驱动开发指南 | 李山文
230
};
static struct spi_driver w25qxx_cdev = {
.driver = {
.name = "w25qxx",
.owner = THIS_MODULE,
.of_match_table = w25qxx_match_table,
},
.probe = w25qxx_probe,
.remove = w25qxx_remove,
.id_table = w25qxx_ids,
};
module_spi_driver(w25qxx_cdev);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("w25qxx_driver");//简单的描述
上面的源码较为简单,使用的是 sysfs 设备文件系统,并没有使用 udev,因此挂载驱动后并不会
产生设备节点,我们直接操作/sys/devices/platform/soc/1c68000.spi/spi_master/spi0/spi0.0 目录下的属性
文件就可以来调试我们的驱动了。
编写好了源码,以动态方式编译为.ko 文件,同时重新编译内核,将 dtb 文件替换为新的,然
后下载到根目录中,进入根目录后,如下:
bin lost+found run
dev media sbin
etc mnt sys
tmp usr lib
opt var lib32
proc w25q128.ko linuxrc
root
执行 insmod w25q128.ko
# insmod w25q128.ko
[ 8.088379] w25q128: loading out-of-tree module taints kernel.
现在我们进入/sys/devices/目录下,此时会有一个 w25q128 属性文件:
# cd /sys/devices/platform/soc/1c68000.spi/spi_master/spi0/spi0.0
# ls
driver of_node subsystem
driver_override power uevent
modalias statistics w25q128
可以看到,这里创建了一个 w25q128 文件,该文件正是我们创建暴露给用户的接口,我们可以对
该文件进行读写。现在我们先简单执行一个 echo 操作重定向到该文件中:
linux 驱动开发指南 | 李山文
231
# echo > w25q128
我们什么也没写入,仅仅是触发下写操作,在驱动程序中,只要执行写操作,就会执行 SPI 控制
器发送擦除命令和读命令,现在我们读下该文件,使用 cat 命令:
# cat w25q128
read status register=2
可以看到,我们的读状态寄存的值为 2但是这个到底是不是确实是发送正确的呢?为了更直观
的反应数据真实性,我们使用逻辑分析仪来监测数据。连接好 SPI 的各个信号线,注意逻辑分析仪需
要共地,也就是需要将逻辑分析仪的地和开发板的低相连。连接好信号线后,我们设置好触发方式,
这里选择 CS 下降沿触发。下面是我们截获的时序图(注意:逻辑分析仪的 SPI 协议需要设置为模式
3):
10-7 逻辑分析仪分 SPI 波形
可以看到,发送数据完全符合我们的驱动程序逻辑。首先第一个字节 0x20 是一个擦除命令,然
0x12,0x34,0x56 就是擦除的地址,然后过了一段时间后,我们开始读状态寄存器,发送字节 0x35
之后可以看到有返回数据 0x02,这和我们读文件得到的值一样,可以看到,驱动程序符合预期。
linux 驱动开发指南 | 李山文
232
第十一章 I
2
C 子系统
上一章我们讲解了 SPI 子系统,相信读者到这里已经领会了子系统的核心思想,子系统最大的好
处在于它为驱动开发者提供了统一的底层接口,为 BSP 开发者提供了统一的实现接口,这样驱动工
程师和 BSP 开发工程师就彼此默契的结合在一起。同时,当硬件发生变化时,上层的控制逻辑依然
不变,这也体现了 Linux 软硬件分离的思想。既然有 SPI 子系统,那当然也有 I2C 子系统,目前常见
I2C 接口的设备主要是一些传感器芯片居多。学过 51 单片机的读者应该对 AT24C02 不陌生吧,
芯片使用的接口正是 I2C 接口。
11-1 AT24C02 芯片
I2C SPI 虽然都是同步通信的串行总线,但 I2C SPI 不同,这不仅仅是信号线个数的不同,
还体现在协议的不同。I2C 每发一个数据给从机,从机必须都有一个回应,否则传输失败,主机将不
会继续发数据,但 SPI 不同,SPI 协议没有应答,不管从机有没有,其数据发出去就发出去了。因此
从数据安全性上看,I2C 会比 SPI 更可靠一点,但实际上由于当前 SPI 控制器已经非常成熟,发送数
据几乎不会产生失败的情况,同时由于其速度较高,稳定性实际上比 I2C 要好,下面我们来简单认识
I2C 总线的协议。
I2C 总线有两根数据线组成,分别是数据线 SDA 和时钟线 SCL在空闲时候,I2C 总线上的电平
必须处于高电平,为了保证电平处于高电平,在 SDA SCL 线上都连接有上拉电阻。这一点和 SPI
有点差别,SPI 总线的 MOSI MISO 是不需要上拉电阻的,但实际上当 SPI 的速率很快时,SPI
线上的电平跳变质量也会降低,为了保证信号质量,SPI 总线上有时候也会接上拉电阻,下图是 I2C
总线的系统连接图。
11-2 I2C 总线
从图中可以看到,I2C 总线和其他总线一样,都存在很多个设备(从机)每个从机都可以当作一
个地址,这样当访问其中的一个从机时,主机先要发送地址信号,从机会根据这个地址来确认该命令
是否属于自己,当主机发送的地址确实是自己时,此时从机会给出应答信号来确保该设备是正常工作
的,这样就实现了主机和多个从设备通信。实际上每个从机设备都有一个唯一的地址,这个地址一般
是由芯片厂家设定的,用户是没办法更改了,因此我们在编写驱动程序的时候一定要仔细阅读相关的
数据手册。
linux 驱动开发指南 | 李山文
233
I2C 的时序也较为简单,下图是其简化时序图,I2C 的数据传输以两个标志位来作为一个数据的
完整传输,分别是开始标志和结束标志。图中可以看到,开始标志的特点是当 SCL 信号是高电平时,
SDA 电平由高电平变为低电平;结束标志是当 SCL 是高电平时,SDA 由低电平变为高电平。从机会
根据这两个状态来区分主机发送数据的开始和结束。
11-3 I2C 协议开始和结束标志
每个芯片的协议都会存在不同,因此为了能够将硬件不同的部分分离出来,而将更上层的部分抽
象为统一的接口,这就出现了 I2C 子系统,下面我们来重点学习下该子系统的框架和驱动开发。
11.1 I2C 子系统框架
如下图是 I2C 子系统的框架图, SPI 子系统类似,最底层依然是硬件,每个 I2C 设备都自己的
地址,设备会与 SoC 上的 I2C 控制器直接连接。控制器上层是控制器驱动程序,这部分程序由 BSP
开发工程师完成,也就是直接与寄存器打交道。控制器上层就是 I2C 的总线层,每个控制器驱动程序
都需要与 I2C 总线进行适配。I2C 总线之上就是 I2C 的子系统核心层,该层是 I2C 子系统的核心逻辑
部分,同时它也为驱动和设备提供标准的接口。驱动由 Linux 驱动开发工程师完成,设备在由 BSP
程师或者驱动开发工程师在设备树文件中定义。最上层就是应用层,该层会由系统调用来实现对 I2C
子系统核心的操作。
11-4 I2C 子系统结构体图
linux 驱动开发指南 | 李山文
234
从上面的结构体可以看到,I2C 驱动程序实际上已经搭建了非常完善的框架,驱动工程师的主要
工作就是根据实际的设备控制逻辑来实现具体的设备驱动程序。所以架构是驱动的核心,掌握了架构
驱动程序自然就通了。当然,凡事都是需要一个过程,先从简单的开始。
11.2 I2C 设备(客户端)
11.2.1 备(客户端)结构体
SPI 驱动一样,我们先要熟悉 Linux 中的 I2C 设备,虽然现在驱动开发者不需要自己写设备文
件,但仍然需要写设备树,设备树最终会被内核解析充当设备文件,因此我们还是有比较对 I2C 设备
结构体,也叫做客户端结构体进行简单的了解,这样更有利于我们如何写设备树的设备节点。
我们先看 Linux 中的客户端结构体,如下所示:
struct i2c_client {
unsigned short flags; /* div., see below */
#define I2C_CLIENT_PEC 0x04 /* Use Packet Error Checking */
#define I2C_CLIENT_TEN 0x10 /* we have a ten bit chip address */
/* Must equal I2C_M_TEN below */
#define I2C_CLIENT_SLAVE 0x20 /* we are the slave */
#define I2C_CLIENT_HOST_NOTIFY 0x40 /* We want to use I2C host notify */
#define I2C_CLIENT_WAKE 0x80 /* for board_info; true iff can wake */
#define I2C_CLIENT_SCCB 0x9000 /* Use Omnivision SCCB protocol */
/* Must match I2C_M_STOP|IGNORE_NAK */
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 */
int init_irq; /* irq set at initialization */
int irq; /* 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
void *devres_group_id; /* ID of probe devres group */
};
主要成员说明如下:
addr设备地址
name设备名称
adapter适配器
dev设备对象
上面的 addr 比较好理解,I2C 属于总线设备,每个设备必然有自己的设备地址,该地址需要在设
备树中指定。设备名也很好理解,每个设备都会有自己的名称,设备树中的 I2C 子节点中的节点名@
前面的便是 I2C 设备名。 I2C 子系统中进入了适配器的概念,实际上适配器指的正是主机,而从机
也就客户端。当主机需要对从机进行读写操作时,此时主机需要先注册到总线上去,而从机(客户端)
linux 驱动开发指南 | 李山文
235
也是一样的,也需要先注册到总线上,这个正是我们的 platform 设备模型。
可以看到,I2C SPI 一样,其设备结构体都较为简单,这主要是因为 I2C SPI 这种都是一种
协议,已经非常成熟的规范,其内部的时序和逻辑已经相当固定了, I2C SPI 控制器驱动会稍微
复杂,但对于 I2C 或者 SPI 这种设备来说,就没有太多关系了,真正的处理逻辑只用交给驱动开发者
自己去实现就可以了。例如对于 SSD1306 这种 OLED 自带驱动控制器的显示屏来说,I2C 接口仅仅
是一个通信的传输管道,而真正要做的事情是对 OLED 寄存器的读写,这个需要用户在驱动程序中
实现即可。
11.2.2 备树节点
我们都知道,Linux 发展中将设备文件全部废除,全面采用设备树的方式,也就是之前的所有的
设备信息都需要单独写一个文件放到内核中,设备文件和驱动文件是一一对应的,其匹配方式和
platform 平台设备驱动的 match 是一样的,没有改变。但是这个设备文件由设备树替代了,当内核启
动时,内核会解析设备树文件,将符合要求的节点注册为 platform 平台设备。因此对于 I2C 设备而
言,我们必须要在设备树中定义我们的设备节点,这样驱动程序才能进行匹配。下面我们先看一个典
型的 I2C 控制器设备节点:
i2c0: i2c@1c2ac00 {
compatible = "allwinner,sun6i-a31-i2c";
reg = <0x01c2ac00 0x400>;
interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_I2C0>;
resets = <&ccu RST_BUS_I2C0>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
上面是一个全志 V3s I2C 控制器设备节点,该节点中 compatible = "allwinner,sun6i-
a31-i2c"指定了控制器驱动程序的匹配字段。reg = <0x01c2ac00 0x400>指定了 I2C 控制器的寄
存器地址。interrupts 指定了控制器的中断号,该中断号为 SPI 类型的第 6 个中断号,触发方式默
认为高电平触发。clocks 指定了该控制器的时钟源。resets 指定了复位时钟信号。pinctrl-names
指定了该 pinctrl-0 的状态为默认状态即开启状态。Status 表示该节点的使用状态,默认关闭。
#address-cells #size-cells 分别指定了 I2C 控制器子节点中的 reg 的地址宽度和大小宽度。
上面的是 I2C 控制器驱动,一般由芯片芯片厂家来实现(因为芯片厂家最了解自己的芯片寄存
器和使用)而对于驱动开发者而言,我们只需要实现我们自己 I2C 设备的设备节点即可,例如现在
我们要实现一个 SSD1306 OLED 驱动程序,我们可以这样编写设备节点:
i2c0: i2c@1c2ac00 {
compatible = "allwinner,sun6i-a31-i2c";
reg = <0x01c2ac00 0x400>;
interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_I2C0>;
resets = <&ccu RST_BUS_I2C0>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
status = "disabled";
linux 驱动开发指南 | 李山文
236
#address-cells = <1>;
#size-cells = <0>;
display@3c {
compatible = "solomon,ssd1306fb-i2c";
reg = <0x3c>;
solomon,height = <16>;
solomon,width = <96>;
}
};
上面我们添加了一个 display@3c 设备节点,该设备节点中指定了匹配驱动的字段和寄存器信
息。当然在实际过程中我们更倾向于使用 pannel 来添加我们的设备节点:
&i2c0 {
display@3c {
compatible = "solomon,ssd1306fb-i2c";
reg = <0x3c>;
solomon,height = <16>;
solomon,width = <96>;
};
};
这样我们的设备会更加清晰分明。可以看到,该节点中添加了一些额外的属性 solomon,height
solomon,width,着两个属性用来指定屏幕的高度和宽度,这样我们的驱动的适配性就会更好,
当然,我们也可以添加其他的属性,例如是否支持旋转、屏幕颜色、数据位宽等等。
11.3 I2C 驱动注册和注销
所有的设备都需要注册到内核中才能正常工作,因此我们需要将我们的 I2C 设备注册到内核中,
Linux 为我们提供了专用的注册和注销接口,在此之前,我们先来了解下 I2C 驱动结构体,其具体结
构如下:
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
int (*probe_new)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
unsigned int data);
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
int (*detect)(struct i2c_client *client, struct i2c_board_info *info);
const unsigned short *address_list;
struct list_head clients;
};
SPI 驱动结构体一样,都有基本的 probrremoveshutdownid_tabledriver 这些。
剩下的比较少用到,我们重点关注这几个就可以了。
linux 驱动开发指南 | 李山文
237
11.3.1 probe 函数
首先看 probe 函数,该函数指针指向的是 xxX_probe 函数,当驱动和设备匹配时,此时会执行
该函数从而对设备进行初始化和注册等操作。但是这里和 SPI 的又会有不同,可以看到这里的参数
列表有两个参数,分别是 struct i2c_client *client struct i2c_device_id *id。第一
个参数是客户端本身,即 I2C 设备;第二个参数是设备 ID 匹配表;实际上这两个参数可以供
probe 函数内部来获取一些设备信息。我们来看一个简单的例子,如下:
static int si514_probe(struct i2c_client *client,const struct i2c_device_id *id)
{
struct clk_si514 *data;
struct clk_init_data init;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 省略无关代码 */
data->regmap = devm_regmap_init_i2c(client, &si514_regmap_config);
if (IS_ERR(data->regmap)) {
dev_err(&client->dev, "failed to allocate register map\n");
return PTR_ERR(data->regmap);
}
/* 省略无关代码 */
i2c_set_clientdata(client, data);
return 0;
}
我们来分析下上面的代码,首先 devm_kzalloc 向内核申请一块内存存放 data 的数据,然后使
用相关的函数去填充 data 中的 regmap 数据,最后使用了 i2c_set_clientdata 函数来设置 client
的数据。可以看到这里唯一和 I2C 有关系的函数只有 i2c_set_clientdata。从名字可以看到,该
函数仅仅只是设置一些数据,而并没有做其他任何事。我们先来看些 i2c_set_clientdata 这个到
底做了啥,实际上在 Linux 中的 struct device 这个结构体中有一个很奇怪的成员,该成员便是
void *driver_data,该成员是一个空指针类型,此指针可以提供给用户来记录用户的驱动相关的
数据的地址,这样做的好处是我们可以不直接操作驱动相关的数据,而是通过 client 这样的参数
去引用,这里我们放到本章后面去深究这样做的好处。
从上面的代码分析可以看到,这里的 probe 函数基本没有做任何实质性的事情。是的,在
probe 里面确实不用做和 i2c 有关的事情,我们一般仅仅只需要完成一些初始化工作,例如 dev
备节点的创建或者 sysfs 文件的创建以及寄存器的初始化等等。
11.3.2 remove 函数
我们再来看 remove 函数,该函数和 probe 对应,实际上也不用做于 I2C 相关的事情,只需要
完成 dev 设备文件节点的删除或者 sysfs 文件的删除以及寄存器的 iounmap 的操作即可。下面我
们看下这个简单的例子:
static int si514_remove(struct i2c_client *client)
{
of_clk_del_provider(client->dev.of_node);
return 0;
linux 驱动开发指南 | 李山文
238
}
上面的代码中没有做任何事情,仅仅只是删除了设备节点。
11.3.3 id_table 设备 ID
和其他的驱动程序一样,I2C 也有自己的 id_table,如下所示是一个简单的例子,里面的记录
了本驱动兼容设备的信息。
static const struct i2c_device_id xxx_id[] = {
{ " xxx,xxxx ", 0 },
{ }
};
11.3.4 driver 驱动匹配
和其他驱动一样,I2C 驱动结构体中也需要实现 driver 驱动匹配,当设备于驱动中的匹配信息相
同时,此时驱动就会和设备进行绑定。对于其他驱动来说,我们需要实现 of_match_table 中匹配
表的填写就可以了,有时候为了兼容多个驱动还会添加 id_table。对于 I2C 而言,在早期的代码
中只能支持 id_table 匹配,这也导致当前很多厂家仍然使用这种方式作为唯一的匹配。但笔者非
常不建议这样做,因为在 platform 平台框架引入之后,我们首要的还是要遵守这种规范。因此我
们还是应该使用 of_match_table 作为首要的匹配方式,但有些代码历史悠久,有时候仅仅有
of_match_table 表却并不能匹配,必须使用 id_table。不过这种状况已经没有了,当前较新的内
核已经推荐使用 of_match_table 方式。例如下面这个例子:
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx,xxxx"},
{ },
};
MODULE_DEVICE_TABLE(of, xxx_of_match);
该匹配表给出了能够匹配的设备,当设备树中的 compatible 属性值与之相同时,此时就能够
触发 probe 并进行驱动的绑定。上面出现了一个 MODULE_DEVICE_TABLE 宏,该宏的作用是告知内
核有 xxx_of_match 这个匹配表,这样的好处是当有其他设备动态加载的时候,能够取匹配该该驱
动。
11.3.5 I2C 驱动注册与注销
上面都没有说驱动如何注册,实际上 I2C 的驱动注册过程和 SPI 一样,内核提供了专门的接口:
module_i2c_driver(__i2c_driver);
该函数实际上是一个宏,实现 I2C 驱动程序的注册和注销,我们只需要在驱动中使用该函数即可。
读者如果感兴趣可以进入该函数看看,会发现所有的驱动注册和注销最终调用的都是同一个接口。
如下面我们定义一个驱动结构体:
static struct i2c_driver xxx_driver = {
.driver = {
.name = "xxx",
.owner = THIS_MODULE,
linux 驱动开发指南 | 李山文
239
},
.probe = xxx_probe,
.remove = xxx_remove,
.id_table = xxx_id,
};
我们可以这样来注册该驱动:
module_i2c_driver(xxx_driver);
11.4 I2C 数据传输
前面说了很多也只是进行了 I2C 驱动的注册,I2C 的驱动程序最终的目的是完成数据传输功能,
因此我们必须要知道如何通过 I2C 来传输数据即如何调用内核接口来完成底层 I2C 驱动控制器的调
用。
Linux 内核为 I2C 数据传输实现了两套通信方式,一种是简单的 I2C 也叫传统的 I2C 通信方式,
另一种为 SMBusSystem Management Bus)的通信方式,也是较为推荐的一种方式,下面我们对这
两种方式分别进行介绍。
在了解 Linux 内核提供了接口之前,我们需要先了解下 I2C 的具体时序。对于 SPI 而言,其时序
较为简单,用户的使用方式较为灵活;而对于 I2C 而言,我们必须严格按照 I2C 时序的逻辑传输数
据,这也是 I2C 数据传输中的重点注意地方。
如下图所示,该图是 I2C 协议的基本时序图,从图中可以看得到其有两根数据线,分别是 SCL
SDA在数据传输的开始时,此时需要发送一个 7 位的设备地址,因此可以看到 I2C 设备最多只能连
128 个从设备,也就是其从设备的地址只能是 0~127发送 7 位的地址后面还有一位用来标志该命
令是读还是写操作,一般而言,读操作为高电平,写操作为低电平。第一个字节发送完毕后,主机(适
配器)必须等待从机的应答,如果从机没有应答,则主机将不会继续发数据。当从机响应应答后(从
机将 SDA 信号线拉低)主机接着发送 8 (一个字节)数据,随后从机响应应答,接着主机继续发
送数据,从机接着应答。这个过程可以一直持续下去,也可以发送一个就停止。最后数据传输完毕后
总线状态变为空闲。
11-5 I2C 标准时序图
从上面的过程中可以看到,每次传输数据都会发送一个 7bit(设备地址)+ 1bit(读写标志)数据,
随后才发送数据。为了能够让驱动开发者更加方便,Linux 内核将这个过程封装了起来,也就是读数
据也好,也数据也好,传输过程中第一个字节是设备的地址+读写标志。
11.4.1 传统的 I2C 数据传输
Linux 内核提供了两个函数供驱动开发者使用,即 i2c_master_send i2c_master_recv前面一个
函数是数据的发送,后面一个函数是数据的结构,其函数原型如下:
linux 驱动开发指南 | 李山文
240
1) static inline int i2c_master_send(const struct i2c_client *client,const char
*buf, int count)
2) static inline int i2c_master_recv(const struct i2c_client *client, char
*buf, int count)
从其接口的参数列表可以看到,其使用方式基本可以知道如何使用了,例如我们发送几个字节的
数据到客户端中,我们可以这样写:
static int i2c_write_le16(struct i2c_client *client, unsigned word)
{
u8 buf[2] = { word & 0xff, word >> 8, };
int status;
status = i2c_master_send(client, buf, 2);
return (status < 0) ? status : 0;
}
上面使用了 i2c_master_send 函数来实现数据的发送,第一个参数为 I2C 客户端地址,如果有些
函数的形参没有 I2C client,这是我们有很多种方式获取 I2C client,其中一种最简单粗暴的方
式就是定义一个*client 指针,该指针在 probe 函数中初始化,这样我们就可以在其他函数中直接
获取 i2c client 了。当然还有其他方式,例如可以通过 to_i2c_driver()该函数来使用 dev
client,其内部实现使用了 container_of 操作,这个操作本书中会讲到该操作的原理及实现
过程。
需要接收数据时,我们可以使用 i2c_master_recv 函数,例如我们现在需要接收几个字节的数
据,我们可以这样:
static int adxl34x_i2c_read_block(struct device *dev,unsigned char reg, int count,void
*buf)
{
struct i2c_client *client = to_i2c_client(dev);
int ret;
ret = i2c_master_send(client, &reg, 1);
if (ret < 0)
return ret;
ret = i2c_master_recv(client, buf, count);
if (ret < 0)
return ret;
if (ret != count)
return -EIO;
return 0;
}
上面利用了 to_i2c_client 函数来将 dev 转换为 client,这个函数实际上就是
container_of,获取到了 client 后,我们就可以先发送寄存器地址,然后读取数据了。
Linux 内核还提供了一种传输方式,该传输方式即可以发送数据,也可以接收数据:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
adapi2c 适配器(主机),可以通过 client 来得到
msgsi2c 消息
num消息的个数
可以看到,该函数是的参数不再是简单的 buffer 了,而是 i2c_msg 结构体,我们先看
i2c_msg 这个结构体:
linux 驱动开发指南 | 李山文
241
struct i2c_msg {
__u16 addr;
__u16 flags;
#define I2C_M_RD 0x0001 /* guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* use only if I2C_FUNC_10BIT_ADDR */
#define I2C_M_DMA_SAFE 0x0200 /* use only in kernel space */
#define I2C_M_RECV_LEN 0x0400 /* use only if I2C_FUNC_SMBUS_READ_BLOCK_DATA */
#define I2C_M_NO_RD_ACK 0x0800 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* use only if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
__u16 len;
__u8 *buf;
};
上面是 i2c_msg 结构体的具体内容,其中 addr 表示 I2C 客户端地址,即从设备地址;flag
示信息标志,即传输方向,0 表示写数据,1I2C_M_RD)读数据,还有其他标志位请参考上面的
结构体中的宏;len 表示传输数据长度,buf 表示传输数据过程中数据保存的地址。下面我们来看
一个简单的例子:
int i2c_write_and_read_data(i2c_client *client, unsigned int *send_buf, unsigned int
*recv_buf)
{
struct i2c_msg msgs[2];
msgs[0].addr = client->addr; //获取客户端(从机地址)
msgs[0].len = sizeof(send_buf);
msgs[0].buf = &send_buf;
msgs[0].flags = 0; //传输为发送数据
msgs[1].addr = client->addr; //获取客户端(从机地址)
msgs[1].len = 10;
msgs[1].buf = recv_buf;
msgs[1].flags = I2C_M_RD; //传输为接收数据
ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
if (ret < 0)
return ret;
else if (ret != ARRAY_SIZE(msgs))
return -EIO;
return 0;
}
上面的代码实现了数据的发送和接收,需要注意的是,发送数据和接收数据的客户端地址可以
通过 client 获取,但是这里有一个前提,就是我们需要在 probe 函数中设置 client 地址数据,
读者应该还有印象,在上面的 probe 函数中提到过 i2c_set_clientdata 这个函数,该函数可以
将获得的客户端的一些数据地址保存到 client 中的空指针中,这样我们需要数据时就可以直接通
client 来获取了。
linux 驱动开发指南 | 李山文
242
11.4.2 统管理总线数据传输SMBus
上面我们介绍了传统方式的数据传输,下面我们来讲解基于 System Management Bus 方式来实现
数据的传输,这种传输方式较为推荐。SMBus 1995 年由 Intel 出的,该总线与 I2C 总线基本一
致,但在 I2C 总线上做了拓展,用过 AVR 单片机的读者可能会对 TWI 总线不陌生,TWI 总线也是在
I2C 总线的基础上做了拓展,总的来说,SMBus TWI 总线都是兼容 I2C 总线的,反过来则不然。
Linux 内核提供了如下函数来实现数据的发送和接收:
1) s32 i2c_smbus_read_byte(const struct i2c_client *client);
2) s32 i2c_smbus_write_byte(const struct i2c_client *client, u8 value);
3) s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command);
4) s32 i2c_smbus_write_byte_data(const struct i2c_client *client,u8 command, u8
value);
5) s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command);
6) s32 i2c_smbus_write_word_data(const struct i2c_client *client,u8 command,
u16 value);
7) s32 i2c_smbus_read_block_data(const struct i2c_client *client,u8 command, u8
*values);
8) s32 i2c_smbus_write_block_data(const struct i2c_client *client,u8 command,
u8 length, const u8 *values);
9) s32 i2c_smbus_read_i2c_block_data(const struct i2c_client *client,u8
command, u8 length, u8 *values);
10) s32 i2c_smbus_write_i2c_block_data(const struct i2c_client *client,u8
command, u8 length, const u8 *values);
上面的函数从名字上就可以看出其实现的目的,我们简单说明下,首先 i2c_smbus_read_byte
函数用来读取一个字节的数据,返回值为读取的数据。i2c_smbus_read_byte_data 函数也是读取
一个字节的数据,但是该函数多了一个 command 参数,该参数实际上是写入的数据,因为对于大部
分的 I2C 设备而言,读寄存器之前必须先要写入寄存器的地址或者命令字节,因此这里增加了一个
command 参数。i2c_smbus_read_i2c_block_data 函数也是读取数据,但是该函数可以读取多个
数据,数据长度由参数 length 指定。
11-5 TCA9554 读寄存器时序,可以看到,该时序需要一 command 字节的数据
上面的函数中虽然返回值类型都是 s32,但是其意义不同,对于 1~6 而言,读操作都是返回的是
读取到的数据,而写操作表示操作是否成功,成功返回 0;对于 7~10 而言,读操作和写操作返回的
都是操作是否成功,成功返回 0
linux 驱动开发指南 | 李山文
243
也就是当参数中含有 value,则表示写入或者读取的数据(单个读)或者地址(多个读)。上面的
command 参数读者不必想的太复杂,实际上就是简单的写入数据的值,也就是在读寄存器之前都会
写操作。我们看下面这个简单的例子(假设设备 0x50 地址):
例子:
char buf[2] = {0xC5};
ret = i2c_smbus_write_i2c_block_data(client, 0xA3 , 1, buf);
上面的代码相当简单,i2c_smbus_read_i2c_block_data 函数的第一个参数为 i2c 客户端,
第二个参数为寄存器地址,第三个参数为写入数据的长度(单位字节),第四个参数为写入数据地
址。
11-6 例子中写数据的时序图
11.5 I2C 驱动示例
上面我们详细介绍了如何使用 I2C 子系统,下面我们需要通过实际的操作来对其进行熟练,这里
以一个简单的 AT24C02 的缺损驱动来做示例讲解。
AT24C02 Atmel 公司(现已被 MicroChip 公司收购)推出的一款小容量 EEPROM
44
产品,该产
品的容量为 2Kbit 256Byte,接口为标准的 I2C 接口,内部继承小容量的 RAM,来加快读写速度。
11-6 AT24C02 电路原理
上图是 AT24C02 的电路原理图,其中 A0A1A2 是芯片的地址线,因此该芯片最多可接 8
相同的设备;WP 为写保护,这里直接接地,不进行写保护;SCL SDA 分别是 I2C 的时钟线和数
据线,需要注意的是,时钟线和数据线都需要上拉电阻,这样才能保证 I2C 总线在空闲的时候是高电
平。
AT24C02 的写过程非常简单,下图是其写数据时序图,首先需要是总线传输数据开始标志,然后
发送一个字节的设备地址,然后再发送一个字节的需要写入的地址,接着再发送一个字节的数据即可,
这样数据都写入到了 AT24C02 中了。
44
EEPROMElectrically Erasable Programmable read only memory,为 Flash 前代存储器
linux 驱动开发指南 | 李山文
244
11-7 AT24C02 写单个字数据时序图
AT24C02 的读数据过程也同样非常简单,下图是其读数据时序图,首先写入一个字节的设备地址,
然后写入需要读取的地址,然后再写入一个器件地址,最后数据就会返回给主机,这样就实现了数据
的读取过程。
11-8 AT24C02 随机读时序图
上面需要注意的是设备的地址并不 A2A1A0,而是由数据手册规定了,设备地址定义如下:
1
0
1
0
A2
A1
A0
R/W
现在我们开始实现 AT24C02 的缺损驱动程序。
首先我们需要在设备树中定义一个 AT24C02 的设备节点,如下所示:
&i2c0 {
clock-frequency = <100000>; //时钟设置 100KHz
status = "okay";
at24c02: at24c02@50 {
compatible = "test,at24c0x";
reg = <0x50>;
};
};
上面节点中添加了 at24c02@0 节点,该节点为 I2C0 控制器下对的一个设备;compatible
性指定了驱动的匹配字段;reg=<0x50>表示该设备的地址为 0x50(因为我们的 A2,A1,A0 都接地
了); clock-frequency 指定了 I2C 控制器最高的速率。上面需要特别说明的是,clock-frequ
ency BSP 代码中的私有属性,并不是内核的内部属性,因此该属性由 BSP 程序进行解析解析。
不知道读者是否注意到 SPI 的设备节点中,使用了 spi-max-frequency 属性,该属性是 Linux
SPI 子系统中的内部属性,内核加载设备树的时候会自动解析并填充到设备结构体中。
注意:有时候我们会遇到多个 master 的情况,这个时候设备树中还需要添加 slave 的地址,
例如在三星的 SoC 中,可以简单很多这种情况:
linux 驱动开发指南 | 李山文
245
&i2c2 {
samsung,i2c-sda-delay = <100>;
samsung,i2c-max-bus-freq = <400000>;
samsung,i2c-slave-addr = <0x10>;
status = "okay";
touchscreen@4a {
compatible = "atmel,maxtouch";
reg = <0x4a>;
/* 省略无关代码 */
};
};
上面的 samsung,i2c-slave-addr 属性是用来指定 slave 地址的,因为对于该 SoC master
有多个。对于 reg 中的值必须为七位地址,也就是其值必须为 0~0x7F,否则该设备将无法注册
device
设备树中的设备节点添加好了之后,我们就需要实现我们的驱动程序了,为了简单,我们还是
使用 sysfs 设备文件系统,这样,我们就可以直接在用户空间中通过读取/sys 文件夹下的设备文
件来对我们的设备进行读写操作,好处是我们就省去了写应用测试程序的麻烦。下面的驱动程序我
们一律使用 SMBus 协议传输数据,这样可以向下兼容。
首先我们开始编写 probe 函数,该函数和 SPI 子系统中的 probe 类似,只不过参数不一样,如
下:
static int at24c02_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
ret = device_create_file(&client->dev,&dev_attr_at24c02);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create at24c02_dev file failed!\n");
return -1;
}
at24c02_dev = client; //初始化 i2c 设备结构体指针
return 0;
}
上面代码中过程比较简单,仅仅只做了 sysfs 设备文件的创建过程,同时将 i2c_client 客户
端的地址保存到一个静态变量中(at24c02_dev = client),这样我们就可以在其他函数中访问
客户端信息。
然后我们实现 remove 函数,该函数主要任务是删除 sysfs 设备文件,如下:
static int at24c02_remove(struct i2c_client *client)
{
device_remove_file(&client->dev,&dev_attr_at24c02);//删除属性文件
printk(KERN_INFO "exit sysfs at24c02!\n");
return 0;
}
我们最终还是需要和芯片进行数据传输,因此需要实现 show store 函数,这两个函数实现
AT24C02 的读写数据操作,为了能够让读者清晰的明白框架,这里仅仅简单操作,如下所示:
linux 驱动开发指南 | 李山文
246
static ssize_t at24c02_show(struct device *dev,struct device_attribute *attr,char
*buf)
{
int ret;
ret = i2c_smbus_write_byte(at24c02_dev,0x02); //先发写地址操作
if(!ret)
{
printk(KERN_ERR"write addr failed!\n");
}
recv[0] = i2c_smbus_read_byte(at24c02_dev); //然后发读数据操
return sprintf(buf,"read data = 0x%x\n",recv[0]);
}
static ssize_t at24c02_store(struct device *dev,struct device_attribute *attr,const
char *buf, size_t count)
{
int ret;
char data = 0x2f;
ret = i2c_smbus_write_i2c_block_data(at24c02_dev,0x02,1,&data);//写一个字节数据
if(ret < 0)
{
printk(KERN_ERR"write data failed!\n");
}
return count;
}
需要注意的是在 show 函数中,我们先要发送一个地址数据,根据数据手册,可以看到发送的方
式是写操作,因此我们在读取数据之前需要先写一个地址,然后再进行读操作。不知道读者是否观
察到,我们使用的是最简单的写和读操作,i2c_smbus_write_byte i2c_smbus_read_byte
这主要是因为我们仅仅只需要写一个数据,而不用写 command,因此我们不能使用其他的函数。对
store 函数则较为简单,对指定的地址中写入数据即可。下面我们需要实现匹配表,用来和设备
进行匹配,如下所示:
static const struct i2c_device_id at24c02_id[] = {
{ "test,at24c0x", 0 },
{ }
};
static const struct of_device_id at24c02_of_match[] = {
{ .compatible = "test,at24c0x"},
{ },
};
实际上对于 I2C 子系统而言,我们只需要实现 id_table 就可以了,但是为了和前面的一致,
我们还是实现了 of_match_table。现在一般提倡使用 of_match_table 来进行驱动与设备的匹
配,但由于有些代码的年份已久,还是基于老的一套,对于此类驱动,如果仅仅实现
of_match_table,驱动将不会执行 probe 函数,也就不会挂载驱动,这一点需要读者注意。剩下
的事就是驱动结构体的初始化和驱动的注册工作:
static struct i2c_driver at24c02_driver = {
linux 驱动开发指南 | 李山文
247
.driver = {
.name = "test,at24c0x",
.owner = THIS_MODULE,
.of_match_table = at24c02_of_match,
},
.probe = at24c02_probe,
.remove = at24c02_remove,
.id_table = at24c02_id,
};
注册驱动我们可以直接使用 module_i2c_driver 函数(实际上是一个宏)即可:
module_i2c_driver(at24c02_driver);
下面是完整的驱动代码。
设备树添加设备节点:
&i2c0 {
clock-frequency = <100000>; //时钟设置 100KHz
status = "okay";
at24c02: at24c02@50 {
compatible = "test,at24c0x";
reg = <0x50>;
};
};
驱动代码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/i2c.h>
#include <linux/kobject.h> //包含 sysfs 件系统对象类
#include <linux/sysfs.h> //包含 sysfs 操作文件函数
static char recv[16] = {0}; //保存接收数据
struct i2c_client *at24c02_dev; //定义一个 i2c 备结构体指针
static ssize_t at24c02_show(struct device *dev,struct device_attribute *attr,char *buf)
{
int ret;
ret = i2c_smbus_write_byte(at24c02_dev,0x02); //先发写地址操作
if(!ret)
linux 驱动开发指南 | 李山文
248
{
printk(KERN_ERR"write addr failed!\n");
}
recv[0] = i2c_smbus_read_byte(at24c02_dev); //然后发读数据操
return sprintf(buf,"read data = 0x%x\n",recv[0]);
}
static ssize_t at24c02_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
int ret;
char data = 0x2f;
ret = i2c_smbus_write_i2c_block_data(at24c02_dev,0x02,1,&data);//写一个字节数据
if(ret < 0)
{
printk(KERN_ERR"write data failed!\n");
}
return count;
}
static DEVICE_ATTR(at24c02, 0660, at24c02_show, at24c02_store); //定义文件属
static int at24c02_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
ret = device_create_file(&client->dev,&dev_attr_at24c02);//创建属性文件
if(ret != 0)
{
printk(KERN_INFO"create at24c02_dev file failed!\n");
return -1;
}
at24c02_dev = client; //初始化 i2c 设备结构体指针
return 0;
}
static int at24c02_remove(struct i2c_client *client)
{
device_remove_file(&client->dev,&dev_attr_at24c02);//删除属性文件
printk(KERN_INFO "exit sysfs at24c02!\n");
return 0;
}
static const struct i2c_device_id at24c02_id[] = {
{ "test,at24c0x", 0 },
{ }
};
static const struct of_device_id at24c02_of_match[] = {
linux 驱动开发指南 | 李山文
249
{ .compatible = "test,at24c0x"},
{ },
};
MODULE_DEVICE_TABLE(of, at24c02_of_match);
static struct i2c_driver at24c02_driver = {
.driver = {
.name = "test,at24c0x",
.owner = THIS_MODULE,
.of_match_table = at24c02_of_match,
},
.probe = at24c02_probe,
.remove = at24c02_remove,
.id_table = at24c02_id,
};
module_i2c_driver(at24c02_driver);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("at24c02_driver"); //简单的描述
我们以的动态加载驱动的方式将驱动程序编译为.ko 文件,然后下载到开发板的根目录中,启动开
发板,进入根目录后,使用 insmod 命令加载该驱动:
# insmod at24c02.ko
[ 96.578982] at24c02: loading out-of-tree module taints kernel.
#
然后进入/sys/devices/platform/soc/1c2ac00.i2c/i2c-0/0-0050 目录下,可以看到此时
生成了一个 at24c02 属性文件,现在我们对属性文件进行读写:
# cd /sys/devices/platform/soc/1c2ac00.i2c/i2c-0/0-0050
# ls
at24c02 modalias of_node subsystem
driver name power uevent
现在我们触发 store 函数, AT24C02 进行写数据,根据代码中,我们应该写入 0x2F 数据到 0x02
地址处,现在我们执行如下:
# echo > at24c02
#
同时我们抓取波形,可以看到 SoC 发送的数据和我们的预期相同。
linux 驱动开发指南 | 李山文
250
11-9 抓取的写数据时序
现在我们触发 show 函数,此时终端会打印写入的数据,如下:
# cat at24c02
read status register=0x2f
#
可以看到确实是我们写入的值,我们抓下波形也符合预期。
11-10 抓取读数据时序图
linux 驱动开发指南 | 李山文
251
第十二章 Regmap 框架
在上两章节中讲解了 SPI 子系统和 I2C 子系统,可以看到,这两个子系统的具体流程都是一样
的,其操作流程有很大的相似度。为了能够让驱动开发者接口变得更加简单和统一, Linux3.1 版本
之后,内核对其进行了一次重构,引入了 Regmap 接口,即寄存器映射。当然该 Regmap 如果只是为
了统一 SPI I2C 供驱动开发者的接口,那未免太过大动干戈了。实际上 Regmap 的野心没有这么
小,这样做是为了能够让其他的总线也能够加入到 Regmap 中来。当前 Regmap 已经支持 I2CSPI
AC97
45
MMIO
46
SPMI
47
这五种总线了,未来还会有更多的总线加入其中。
12-1 引入 Regmap 之后的总线接口
Regmap 框架不仅进提供了一种统一的接口,而且还支持缓存,当有大量数据传输时,此时有缓
存的机制可以减少内存的开销,因为 Regmap 内部实现了数据压缩机制。
12.1 Regmap 接口使用
Regmap 的接口使用非常简单,我们只需要按照基本的配置流程就可以实现设备的读写操作,其
流程如下:
1. 配置 regmap_config 结构体
2. Regmap 初始化
3. 调用 Regmap API 函数进行读写操作
4. 释放 Regmap
我们只需要按照上面的步骤就可以使用 Regmap 了,下面我们来对其进行具体的讲解。
12.1.1 配置 regmap_config 结构体
第一步我们需要配置该结构体,这个结构体非常复杂,看着比较唬人,但是对于大部分驱动而言,
我们仅仅只需要配置其中的一部分即可,其具体结构如下:
45
AC97Audio Codec1997
46
MMIOMemory-mapped I/O
47
SPMISystem Power Management Interface
linux 驱动开发指南 | 李山文
252
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);
bool (*writeable_noinc_reg)(struct device *dev, unsigned int reg);
bool (*readable_noinc_reg)(struct device *dev, unsigned int reg);
bool disable_locking;
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 regmap_access_table *wr_noinc_table;
const struct regmap_access_table *rd_noinc_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;
unsigned long read_flag_mask;
unsigned long write_flag_mask;
bool zero_flag_mask;
bool use_single_read;
bool use_single_write;
bool use_relaxed_mmio;
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;
bool use_hwlock;
unsigned int hwlock_id;
unsigned int hwlock_mode;
bool can_sleep;
};
这个结构体看着确实太过庞大,我们来看下比较重要的成员,如下:
linux 驱动开发指南 | 李山文
253
reg_bits:寄存器地址的位数,该成员必须初始化一个有效值
val_bits:寄存器值的位数,该成员必须初始化一个有效值
writeable_reg:回调函数,该值为一个可写的寄存器表
readable_reg:和上一个成员相同,针对每个寄存器的读取操作
volatile_reg:回调函数,每次从缓存中写入或者读取寄存器时就会调用
max_register:最大寄存器地址,防止越界访问内存
wr_table:不提供
12.1.2 Regmap 初始化
由于 Regmap 仅仅只是一个统一的接口,其底层调用最终还是走的各个子系统协议,因此我们在
使用 Regmap 之前须对进行初始。例我们在要使用 I2C 统,那么们需要将
Regmap 映射到 I2C 接口上,这个过程不需要我们自己实现,Linux 内核已经实现好了,我们要做的
仅仅是调用下函数就可以了。以 I2C SPI 为例
48
Linux 内核提供了如下函数来初始化 Regmap
1) struct regmap * regmap_init_i2c(struct i2c_client *i2c, const struct *regmap_config)
2) struct regmap * regmap_init_spi(struct spi_device *spi, const struct *regmap_config);
第一个函数是将 regmap 的接口映射到 i2c 子系统中,其第一个参数为 i2c 客户端,第二个参数为
regmap 配置结构体;第二个函数是将 regmap 的接口映射到 spi 子系统中,其中第一个参数为 spi
备结构体,第二个参数为 regmap 配置结构体。
我们举一个例子:
static struct regmap *xxx_regmap;
static const struct regmap_config xxx_config =
{
.reg_bits = 8, //寄存器 8
.val_bits = 8, //数据 8
.max_register = 255, //最大寄存器 255
.cache_type = REGCACHE_NONE, //不使用 cache
.volatile_reg = false,
};
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
...
xxx_regmap = regmap_init_i2c(client,&xxx_config);
...
}
上面的例子中,首先定义 regmap 配置寄存器,然后在 probe 函数中初始化 regmap i2c 接口,
这样我们操作 regmap 的时候实际上就是在操作 i2c 接口。
对于 SPI 也是一样,我们可以将接口映射到 spi 子系统接口上,我们只需要在 probe 函数中调用
regmap_init_spi 函数即可。
48
Linux 内核中还有其他的初始化总线接口,具体请查看 include\linux\regmap.h 文件
linux 驱动开发指南 | 李山文
254
12.1.3 Regmap 读写操作
最终我们的目的是与各个设备进行通信,Linux 核提供了响应的接口函数供驱动开发者使用,
主要用到的接口如下:
1) static inline int regmap_write(struct regmap *map, unsigned int reg,unsigned int val)
向单个寄存器中写入一个值,第一个参数为 regmap 结构体,即我们初始化 regmap 的返回值;
二个参数是寄存器地址,第三个参数是要写入的值;返回值为 0 表示写入成功,否则写入失败。
2) static inline int regmap_raw_write( struct regmap *map, unsigned int reg,
const void *val, size_t val_len)
向单个寄存器写入多个数据,第一个参数为 regmap 结构体;第二个参数为寄存器地址;第三个
参数为要写入的数据的地址;第四个参数为写入数据的长度;返回值为 0 表示成功,否则失败。
3) static inline int regmap_bulk_write(struct regmap *map, unsigned int reg,
const void *val, size_t val_count)
向多个连续的寄存器中写入多个值,第一个参数为 regmap 结构体;第二个参数为寄存器地址;
第三个参数为要写入的数据的地址;第四个参数为数据的数量;每写一个寄存器后,寄存器地址会自
动加一。
4) static inline int regmap_read(struct regmap *map, unsigned int reg,unsigned int *val)
对单个寄存器进行读操作,第一个参数为 regmap 结构体;第二个参数为寄存器地址;第三个参
数为读取的数据存放地址;返回值 0 表示成功,否则失败。
5) static inline int regmap_raw_read(struct regmap *map, unsigned int reg,
void *val, size_t val_len)
对单个寄存器读多个数据,第一个参数为 regmap 结构体;第二个参数为寄存器地址;第三个参
数为读取的数据存放地址;第四个数据为读取的数据长度。
6) static inline int regmap_bulk_read( struct regmap *map, unsigned int reg,
void *val, size_t val_count)
向多个连续的寄存器中读多个值,第一个参数为 regmap 结构体;第二个参数为寄存器地址;第
三个参数为要读取的数据的地址;第四个参数为读取的数据的数量。每次读数据后寄存器地址自动加
一。
7) int regmap_multi_reg_write( struct regmap *map, const struct reg_sequence *regs,
int num_regs);
向多个指定的寄存器序列中写入数据,第一个参数为 regmap 结构体;第二个参数为寄存器序列
结构体;第三个数据为寄存器数量。其中 const struct reg_sequence 定义如下:
struct reg_sequence {
unsigned int reg; //寄存器
unsigned int def; //
unsigned int delay_us; //延时时间
};
例如我们要写三个寄存器的值,我们可以这样使用:
const struct reg_sequence setup[] = {0x8024, 0x76},{0x8023, 0x01},{0x8157, 0x03},};
regmap_multi_reg_write(regmap,setup,ARRAY_SIZE(setup));
linux 驱动开发指南 | 李山文
255
8) int regmap_raw_write_async( struct regmap *map, unsigned int reg,
const void *val, size_t val_len);
多个
regmap第二个参数为要写入的从设备地址,第三个参数为写入的值的地址;第四个参数为写入的数
据长度。
12.1.4 释放 Regmap
当我们使用完毕了,我们需要对 Regmap 进行释放,Linux 内核提供了如下函数供驱动开发者使
用:
void regmap_exit(struct regmap *map)
12.2 OLED 示例
12.2.1 驱动分析
为了让读者更好的理解 regmap 的使用,下面以 I2C 接口的 OLED 的简单驱动来做为一个例子,
和之前一样,我们需要在设备树中定位我们的设备节点,如下所示:
&i2c0 {
clock-frequency = <1000>; //时钟设置为 1KHz
status = "okay";
oled: oled@3c {
compatible = "test,oled";
reg = <0x3c>;
};
};
上面的 OLED 设备地址可以根据数据手册查到,一般为 0x3c 或者为 0x3d笔者这里使用的 OLED
设备地址为 0x3c
现在我们需要了解 OLED 的驱动过程,对于绝大部分的 OLED 而言,其内部的显示控制器最常见
的便是 SSD1306该控制器的数据手册可以在官网上找到,这里默认读者对 OLED 的操作比较熟悉。
和其他驱动一样,首先我们需要实现 probe 函数:
static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
ret = device_create_file(&client->dev,&dev_attr_oled);//建属性文件
if(ret != 0)
{
printk(KERN_INFO"create oled_dev file failed!\n");
return -1;
}
oled_regmap = regmap_init_i2c(client,&oled_config);
oled_init();
return 0;
}
probe 函数中所做的事情比较简单,首先创建一个 sysfs 的文件节点,这样我们就可以直接在
Linux 的根文件系统中直接操作设备驱动,从而省去了编写应用程序测试代码。然后初始化 regmap
linux 驱动开发指南 | 李山文
256
构体指针,最后调用 oled 初始化函数。上面的 oled_config 的定义如下:
static const struct regmap_config oled_config =
{
.reg_bits = 8, //寄存器 8
.val_bits = 8, //数据 8
.max_register = 255, //最大寄存器 255
.cache_type = REGCACHE_NONE, //不使用 cache
.volatile_reg = false,
};
我们的寄存器为 8 位,数据位宽也为 8 位,不适用 cache,直接刷到寄存器中,这样主要是为了
程序简单,笔者希望程序尽可能简单,这样方便读者能够把握驱动程序的整体框架而不会陷入局部中。
然后我们定义了 remove 函数,该函数的主要任务是删除 sysfs 属性文件以及释放内存,如下所示:
static int oled_remove(struct i2c_client *client)
{
device_remove_file(&client->dev,&dev_attr_oled);//删除属性文
regmap_exit(oled_regmap);
printk(KERN_INFO "exit sysfs oled!\n");
return 0;
}
remove 函数中一定要记得使用 regmap_exit,避免内存泄漏。然后我们需要定义驱动结构体,
初始化各个函数指针:
static struct i2c_driver oled_driver = {
.driver = {
.name = "test,oled",
.owner = THIS_MODULE,
.of_match_table = oled_of_match,
},
.probe = oled_probe,
.remove = oled_remove,
.id_table = oled_id,
};
其中匹配表如下所示:
static const struct i2c_device_id oled_id[] = {
{ "test,oled", 0 },
{ }
};
static const struct of_device_id oled_of_match[] = {
{ .compatible = "test,oled"},
{ },
};
笔者建议对于 I2C 驱动而言,最好这两个匹配表都填充,因为在比较老的驱动程序中,只填写
match 表有可能无法正确绑定设备。最后就是我们的 sysfs 设备文件系统定义:
static ssize_t oled_show(struct device *dev,struct device_attribute *attr,char *buf)
linux 驱动开发指南 | 李山文
257
{
oled_off();
return 1;
}
static ssize_t oled_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
oled_fill_screen(*buf);
return count;
}
static DEVICE_ATTR(oled, 0660, oled_show, oled_store); //定义文件属性
这两个函数较为简单,做一个简单的关闭 OLED 和填充 OLED。剩下的 OLED 的一些底层的操作这
里不对其进行累述了,具体可参考完整的源码。
12.2.2 完整源码
设备树节点如下所示:
&i2c0 {
clock-frequency = <1000>; //时钟设置为 1KHz
status = "okay";
oled: oled@3c {
compatible = "test,oled";
reg = <0x3c>;
};
};
驱动程序如下所示:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/i2c.h>
#include <linux/kobject.h> //包含 sysfs 件系统对象类
#include <linux/sysfs.h> //包含 sysfs 操作文件函数
#include <linux/regmap.h>
static struct regmap *oled_regmap;
static uint8_t diaplay_buffer[128][8];
static int oled_write_cmd(uint8_t cmd)
linux 驱动开发指南 | 李山文
258
{
int ret;
ret = regmap_write(oled_regmap,0x00,cmd); //DC=0 command
if(ret)
{
return ret;
}
else
{
return 0;
}
}
static int oled_write_data(uint8_t data)
{
int ret;
ret = regmap_write(oled_regmap,0x40,data); //DC=1 command
if(ret)
{
return ret;
}
else
{
return 0;
}
}
static void oled_on(void)
{
oled_write_cmd(0x8D);
oled_write_cmd(0x14);
oled_write_cmd(0xAF);
}
static void oled_off(void)
{
oled_write_cmd(0x8D);
oled_write_cmd(0x10);
oled_write_cmd(0xAE);
}
void oled_refresh(void)
{
uint8_t i, j;
for (i = 0; i < 8; i ++)
{
oled_write_cmd(0xB0 + i);
oled_write_cmd(0x02);
linux 驱动开发指南 | 李山文
259
oled_write_cmd(0x10);
for (j = 0; j < 128; j ++)
{
oled_write_data(diaplay_buffer[j][i]);
}
}
}
void oled_fill_screen(uint8_t data)
{
memset(diaplay_buffer,data, sizeof(diaplay_buffer));
oled_refresh();
}
static void oled_init(void)
{
oled_write_cmd(0xAE);//--turn off oled panel
oled_write_cmd(0x00);//---set low column address
oled_write_cmd(0x10);//---set high column address
oled_write_cmd(0x40);//--set start line address
oled_write_cmd(0x81);//--set contrast control register
oled_write_cmd(0xCF);// Set SEG Output Current Brightness
oled_write_cmd(0xA1);//--Set SEG/Column Mapping
oled_write_cmd(0xC0);//Set COM/Row Scan Direction
oled_write_cmd(0xA6);//--set normal display
oled_write_cmd(0xA8);//--set multiplex ratio(1 to 64)
oled_write_cmd(0x3f);//--1/64 duty
oled_write_cmd(0xD3);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
oled_write_cmd(0x00);//-not offset
oled_write_cmd(0xd5);//--set display clock divide ratio/oscillator frequency
oled_write_cmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
oled_write_cmd(0xD9);//--set pre-charge period
oled_write_cmd(0xF1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
oled_write_cmd(0xDA);//--set com pins hardware configuration
oled_write_cmd(0x12);
oled_write_cmd(0xDB);//--set vcomh
oled_write_cmd(0x40);//Set VCOM Deselect Level
oled_write_cmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
oled_write_cmd(0x02);//
oled_write_cmd(0x8D);//--set Charge Pump enable/disable
oled_write_cmd(0x14);//--set(0x10) disable
oled_write_cmd(0xA4);// Disable Entire Display On (0xa4/0xa5)
oled_write_cmd(0xA6);// Disable Inverse Display On (0xa6/a7)
oled_write_cmd(0xAF);//--turn on oled panel
oled_on();
oled_fill_screen(0xff);
}
linux 驱动开发指南 | 李山文
260
static const struct regmap_config oled_config =
{
.reg_bits = 8, //寄存器 8
.val_bits = 8, //数据 8
.max_register = 255, //最大寄存器 255
.cache_type = REGCACHE_NONE, //不使用 cache
.volatile_reg = false,
};
static ssize_t oled_show(struct device *dev,struct device_attribute *attr,char *buf)
{
oled_off();
return 1;
}
static ssize_t oled_store(struct device *dev,struct device_attribute *attr,const char
*buf, size_t count)
{
oled_fill_screen(*buf);
return count;
}
static DEVICE_ATTR(oled, 0660, oled_show, oled_store); //定义文件属性
static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
ret = device_create_file(&client->dev,&dev_attr_oled);//建属性文件
if(ret != 0)
{
printk(KERN_INFO"create oled_dev file failed!\n");
return -1;
}
oled_regmap = regmap_init_i2c(client,&oled_config);
oled_init();
return 0;
}
static int oled_remove(struct i2c_client *client)
{
device_remove_file(&client->dev,&dev_attr_oled);//删除属性文
regmap_exit(oled_regmap);
printk(KERN_INFO "exit sysfs oled!\n");
return 0;
}
static const struct i2c_device_id oled_id[] = {
linux 驱动开发指南 | 李山文
261
{ "test,oled", 0 },
{ }
};
static const struct of_device_id oled_of_match[] = {
{ .compatible = "test,oled"},
{ },
};
MODULE_DEVICE_TABLE(of, oled_of_match);
static struct i2c_driver oled_driver = {
.driver = {
.name = "test,oled",
.owner = THIS_MODULE,
.of_match_table = oled_of_match,
},
.probe = oled_probe,
.remove = oled_remove,
.id_table = oled_id,
};
module_i2c_driver(oled_driver);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提醒
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("oled_driver"); //简单的描述
12.2.3 驱动测试
我们将驱动程序以动态加载的方式编写为.ko 文件,然后下载到根文件目录中,进入.ko 文件所在
目录,然后挂载该驱动,如下:
# insmod simple_oled.ko
[ 19.581244] simple_oled: loading out-of-tree module taints kernel.
#
此时,/sys/devices/platform/soc/1c2ac00.i2c/i2c-0/0-0050 目录下会产生一个 oled
属性文件,我们使用 echo 命令对其进行写操作,如下所示:
# cd /sys/devices/platform/soc/1c2ac00.i2c/i2c-0/0-0050
# echo 0 > oled
#
可以看到,此时 OLED 屏幕刷新为了多条线,我们还可以写一些其他数据,OLED 会继续刷新,
现在我们读取下 OLED,可以看到此时屏幕将关闭。
linux 驱动开发指南 | 李山文
262
# cat oled
#
使 Regmap
Regmap 了吧。 Regmap I2C 以及 SPI 子系统对比可以看出,Regmap 的接口必须对寄存器进行操
作,也就是在我们的读写过程中必须要有寄存器,那如果没有呢?如果我们的读写就是没有寄存器,
而是直接写数据,那么我们就不能使用 regmap 接口,规规矩矩使用 I2C 或者 SPI 子系统的接口。
12-2 驱动历程中抓取的部分波形,与代码后相符
linux 驱动开发指南 | 李山文
263
第十三章 Frame Buffer 设备驱动
在早期的输出显示设备中,大部分为 CRT 显示器,随着技术的不断发展,现在大部分使用的是
液晶显示器。这些显示设备在 Linux 中统称为 Frame Buffer 设备,即帧缓冲设备,简称 FB
13-1 常见的帧缓冲输出设备
需要说明的是,并不是只有显示屏这种设备才是帧缓存设备,实际上帧缓冲设备只是一种显示原
理,将显示缓冲区中的数据重定向到输出设备中,也许有些输出设备仅仅只是一个虚拟的设备而言。
那为何叫帧缓冲设备呢?这主要是因为帧缓冲设备的显示原理是在内存中开辟一块特定的暂存区,
块暂存区记录着所有位图(像素)数据,在一定的时间内将这个数据一一对应的写入到输出设备中,
这样输出设备上就是我们需要显示的信息了。
Linux 操作系统提供了标准的帧缓存设备驱动接口,有时候我们也称为帧缓存子系统,在我们的
开发板/dev 目录中的 fb0 就是一个典型的帧缓冲设备节点,我们可以使用输出重定向到该设备中,
ls / > /dev/fb0 这样就可以将根目录下的内容显示到 fb0 设备上。在 Linux 中,帧缓冲子系统提供的
仅仅是一种输出通道,至于该缓冲区的图像是如何输出到显示器的对于 Linux 来说是不关心的。这样
做的好处是 Linux 输出到帧缓冲设备节点上,该设备也许是 RGB 接口的,也许是 LVDS 接口的,也
许是 8080 并口的等等,这个对于帧缓冲子系统而言是没有区别的,具体的显示部分由用户驱动自己
去实现。
13.1 显示原理与计算机图形学
为何要在这里介绍显示原理呢?这主要是因为只有读者清楚的认知到计算机中显示字符图像原
理,才能知道为什么帧缓冲设备的驱动实现要这样做,即知其然且知其所以然。
首先我们来看一个简单的显示过程,玩过单片机的读者也许对 LED 点阵比较熟悉, LED 点阵
屏驱动程序中,我们需要使用扫描的方式来显示一个字符或者一张图片,扫描的原理利用了人眼视觉
暂留的特点,从而看起来是一张完整的图片。如下图所示,这是一个“你”字的字模图,对于行扫描
而言,该字被划分为 16 行,每一行可以将其编码为两个字节,每个字节的一个位对应着一个像素。
这样连续快速的单独显示每一行,由于人眼的视觉暂留就看起来是一副完整的“你”字。这是所有显
示的基础,即使是现在的显示屏其原理这是这样,上述只是简单的显示一个字符,如果我们需要显示
多个字符,或者一大段字符呢?那我们难道需要一个一个去显示么?实际上当我们的图像变得复杂一
些时,我们不能一个一个字符去显示了,而是需要一起显示,例如我们需要显示 100 个汉字,那么这
100 个汉字的字模数据(编码)首先需要放到一个缓存区中,接着有一个单独的任务专门负责将这个
缓存区不断的刷新到显示屏上,这样就实现了多个字符的显示。上面的缓存区我们称之为显存,也就
是显示存储区。现在的显示画面要求极高,已经需要单独用一个设备来显示画面,这就出现了显卡,
显卡中显存就是用来保存显示数据的。当然这些数据不仅仅只是保存显示的像素信息,同时还需要保
存显示过程中计算的数据,特别是需要做渲染工作时,显卡中的显存变得非常重要。
linux 驱动开发指南 | 李山文
264
13-2 “你”字模图
13-3 字符显示原理图
随着计算机的显示越来越复杂,我们的显示信息开始变得越累越繁重。我们不再简单的只是显示
一些字符信息了,我们还需要显示图片信息。例如我们需要显示一个圆形,那如何要实现这个呢?难
道我们需要先将一个“圆”进行编码,然后再进行显示吗?对于固定的圆确实可行,但是一旦我们的
圆的半径变化时,我们的编码就失效了,因此这种方式显然不再可行了,这就出现了计算机图形学。
计算机图形学是一门比较复杂的分支。画一圆实际上在计算机学中有很多经典的算法,例如中点画圆
法、Bresenham 算法、 正负判定画圆法、快速画圆法。不仅仅是画一个圆,还有画一条线也是需要算
法的,本书的重点不再如何实现画圆算法,因此这里不再对其累述.
13-4 Bresenham 画圆算法
随着显示的需求不断提高,陆陆续续出现了很多 GUI 库(Graphical User Interface,即图形用户
linux 驱动开发指南 | 李山文
265
接口。这个 GUI 库提高了很多图形显示功能,包括基本的按钮、窗口界面、图片解码扽等等。比例注
明的 GUI ucGUILVGLMiniGUIAWTK 等。
用过这些 GUI 的读者应该或多或少知道如何移植这个 GUI 到自己的开发板上,这个 GUI 移植工
作最重要的就是实现最基本的画点函数。只要 GIU 库中算法的实现是不依赖于硬件的,唯一依赖于
硬件的就是这个最基本的画点函数,读者可以想到,只要能够在显示器上画一个点,那么一个画面也
就可以画出来。当然实际上我们有时候为了能够加快这个绘图功能,我们必须用硬件实现底层的这些
画点画线画圆等操作。
13.2 fb 设备相关结构体
13.2.1 fb_ops
对于 Linux 内核也是一样的,绘制图形的算法不依赖于硬件,但是用户需要提供最基本的画点操
作,也就是如何在显示器上画一个点,只要提供了这个接口,Linux 内核就能够在显示器上显示图形
了,至于如何显示,用户是不需要关心的。下面是 Linux 内核提供的 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 non linear 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, 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 */
linux 驱动开发指南 | 李山文
266
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);
/* called at KDB enter and leave time to prepare the console */
int (*fb_debug_enter)(struct fb_info *info);
int (*fb_debug_leave)(struct fb_info *info);
};
这个结构体比较复杂,我们只看一些比较重要的成员:
owner:和其他驱动一样,一般初始化为 THIS_MODULE
fb_check_var检查设备信息,该函数负责检测帧缓冲设备的各个参数,同时对参数进行
调整
fb_set_par:设置帧缓冲设备的控制器参数
fb_setcolreg:设置颜色
fb_blank:消隐,实现帧缓冲设备的显示和关闭以及挂起等功能
fb_fillrect:绘制矩形
fb_copyarea:将屏幕中的一个区域复制到另一个区域
fb_imageblit:显示图片
上面的操作函数中有一点需要说明,其中 fb_setcolreg 函数是调色板函数,这个函数必须实现,
fb_fillrectfb_copyareafb_imageblit 这三个函数是支持硬件加速的,即当我们硬件能够
实现这个功能时,我们可以初始化这些函数来加快我们的绘图。下面是一个简单的例子:
struct fb_ops myfb_ops = //定义一个 fb_ops 结构体指针
{
.owner = THIS_MODULE,
.fb_check_var = myfb_check_var,
.fb_set_par = myfb_set_par;
.fb_setcolreg = myfb_setcolreg,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
.fb_blank = myfb_blank,
};
上面的代码中 cfb_fillrectcfb_copyareacfb_imageblit 这三个函数是系统缺损函数,
linux 驱动开发指南 | 李山文
267
即如果我们没有定义硬件加速的操作,我们就需要使用系统默认的画图函数。上面说了这么多,读者
是否注意到一个问题,我们最终还是没有实现最底层最基本的画点函数,如果没有画点函数,那么
Linux 内核是不可能在显示屏上显示信息的,因为上层的画图函数都是基于该画点函数的(假设此处
没有硬件加速支持)。在 Frame Buffer 中,确实没有使用该画点函数,而是只提供了图像的显存,
fb_infoi 结构体中我们需要设置显存的地址和大小,Linux 内核会将要显示的数据存放到该显存
中,而将显存中的数据显示出来就交给用户(驱动开发者)自己实现了。一般我们可以开一个线程专
门将显存中的数据刷新到显示屏上,也可以开一个定时器定时刷新,也可以开启一个工作队列进行刷
新。
13.2.2 fb_var_screeninfo 结构体
每个显示设备的分辨率和参数都不一样,Linux 内核中 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 */
__u32 yoffset; /* resolution */
__u32 bits_per_pixel; /* guess what */
__u32 grayscale; /* 0 = color, 1 = grayscale, */
/* >1 = FOURCC */
struct fb_bitfield red; /* bitfield in fb mem if true color, */
struct fb_bitfield green; /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp; /* transparency */
__u32 nonstd; /* != 0 Non standard pixel format */
__u32 activate; /* see FB_ACTIVATE_* */
__u32 height; /* height of picture in mm */
__u32 width; /* width of picture in mm */
__u32 accel_flags; /* (OBSOLETE) see fb_info.flags */
/* 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 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 rotate; /* angle we rotate counter clockwise */
__u32 colorspace; /* colorspace for FOURCC-based modes */
__u32 reserved[4]; /* Reserved for future compatibility */
};
linux 驱动开发指南 | 李山文
268
该结构体的成员很多,我们看一些常用的成员:
xres:可显示的水平分辨率
yres:可显示的垂直分辨率
xres_virtual:虚拟的水平分辨
yres_virtual:虚拟的垂直分辨
xoffset:从虚拟到可显示的水平偏移量
yoffset:从虚拟到可显示的垂直偏移量
bits_per_pixel:每个像素点所需要的位数
grayscale:灰度显示,默认彩色显示
height:内存中图像高度
width:内存中图像宽度
实际上帧缓冲设备最开始是为 CRT 显示器的 VGA 这种接口设备实现的,因此在代码中有可见
区和虚拟区。但现在大部分的设备都是液晶,upper margin 这种参数已经毫无意义,但有些显示屏仍
然为了兼容 VGA 接口而特意在时序上留出了 margin本章将不使用显示控制器,而是使用 I2C 这种
接口的 OLED 显示屏作为验证,因此这里不对这个参数进行过多说明。
13-5 屏幕参数
13.2.3 fb_fix_screeninfo 结构体
有些 SoC 集成了视频硬件解码等功能,Linux 内核为其预留出了接口用来加速视频。该结构体便
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 */
linux 驱动开发指南 | 李山文
269
__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_* */
__u16 reserved[2]; /* Reserved for future compatibility */
};
当我们需要硬件加速的时候,我们就可以初始化该结构体。
13.2.4 fbcmap 构体
该结构体为 color map,即颜色映射,其结构定义如下:
struct fbcmap {
int index; /* first element (0 origin) */
int count;
unsigned char __user *red;
unsigned char __user *green;
unsigned char __user *blue;
};
该结构体较为简单,其中 index 表示第一个元素;count 表示颜色数量;red 指向红色的值;
green 指向绿色的值;blue 指向蓝色的值。
13.2.5 fb_info 构体
fb_info 结构体是 Frame Buffer 设备的最重要的结构体,里面包含了上面的所有结构体。其定义如
下:
struct fb_info {
atomic_t count;
int node;
int flags;
/*
* -1 by default, set to a FB_ROTATE_* value by the driver, if it knows
* a lcd is not mounted upright and fbcon should rotate to compensate.
*/
int fbcon_rotate_hint;
struct mutex lock; /* Lock for open/release/ioctl funcs */
struct mutex mm_lock; /* Lock for fb_mmap and smem_* fields */
struct fb_var_screeninfo var; /* Current var */
linux 驱动开发指南 | 李山文
270
struct fb_fix_screeninfo fix; /* Current fix */
struct fb_monspecs monspecs; /* Current Monitor specs */
struct work_struct queue; /* Framebuffer event queue */
struct fb_pixmap pixmap; /* Image hardware mapper */
struct fb_pixmap sprite; /* Cursor hardware mapper */
struct fb_cmap cmap; /* Current cmap */
struct list_head modelist; /* mode list */
struct fb_videomode *mode; /* current mode */
#if IS_ENABLED(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
#ifdef CONFIG_FB_DEFERRED_IO
struct delayed_work deferred_work;
struct fb_deferred_io *fbdefio;
#endif
const struct fb_ops *fbops;
struct device *device; /* This is the parent */
struct device *dev; /* This is this fb device */
int class_flag; /* private sysfs flags */
#ifdef CONFIG_FB_TILEBLITTING
struct fb_tile_ops *tileops; /* Tile Blitting */
#endif
union {
char __iomem *screen_base; /* Virtual address */
char *screen_buffer;
};
unsigned long screen_size; /* Amount of ioremapped VRAM or 0 */
void *pseudo_palette; /* Fake palette of 16 colors */
#define FBINFO_STATE_RUNNING 0
#define FBINFO_STATE_SUSPENDED 1
u32 state; /* Hardware state i.e suspend */
void *fbcon_par; /* fbcon use-only private area */
/* From here on everything is device dependent */
void *par;
/* we need the PCI or similar aperture base/size not
smem_start/size as smem_start may just be an object
allocated inside the aperture so may not actually overlap */
struct apertures_struct {
unsigned int count;
struct aperture {
resource_size_t base;
resource_size_t size;
linux 驱动开发指南 | 李山文
271
} ranges[0];
} *apertures;
bool skip_vt_switch; /* no VT switch on suspend/resume required */
};
该结构体成员数量庞大,我们只关心最重要的几个:
fb_var_screeninfo var:屏幕变量信息
fb_fix_screeninfo:具有硬件加速的屏幕信息
fb_cmap cmap:颜色映射
fb_ops *fbops:底层驱动操作
screen_base:帧缓冲区的虚拟地址
screen_size:帧缓冲区大小
device:父设备指针
dev:该 fb 设备
pseudo_palette:假的调色板
可以看到帧缓冲设备中对硬件的支持力度很大,本书的重点不是如何去优化驱动性能,而是让读
者明白 fb 驱动的大体框架。上面的成员中 screen_base screen_size 这两个参数非常重要,
Linux 内核的 frame buffer 所有的显示数据都会放到该内存中,因此我们在初始化时必须指定内
存的地址和大小。
13.3 缓冲设备注册与注
Linux 内核提供了帧缓冲设备的注册和注销接口,其接口如下:
int register_framebuffer(struct fb_info *fb_info)
该函数的第一个参数为 fb 信息结构体,我们只需要调用该接口就可以注册我们的 fb 驱动了,一
般来说,我们需要动态分配一个 fb_info 结构体,Linux 内核提供了如下接口:
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)
该函数的第一个参数为要分配的大小,第二个参数为 fb 的父设备。
当我们卸载 fb 设备时,我们需要将其注销掉,其接口如下:
Void unregister_framebuffer(struct fb_info *fb_info)
在使用该函数之前,我们需要先释放内存,使用的接口如下:
void framebuffer_release(struct fb_info *info)
13.4 显示刷新
14.2.1 中说过,Frame Buffer 框架中的 fb_ops 中,我们只填充了最底层的调色板、消隐、绘制
矩形(硬件加速)等函数,但始终没有提供画点函数,如果没有这个函数,frame buffer 肯定是不知道
如何在显示器上画图像的。 Frame Buffer 框架中,只是将要显示的图像全部放到了显存fb_info
指定)中,如果没有定义硬件加速这些函数,用户就必须自己手动将图像刷到显示屏上。这样就出现
了两种方式。第一种方式就是将刷新的操作交给应用程序,应用程序调用 ioctrl 接口来实现图像的刷
新。这种方式最大的好处就是如果图形没有变化,此时处理器是不需要刷新的,也即是用户知道何时
需要刷新,何时不需要刷新,可以大大减少处理器不必要的操作。但这种方式有个明显的缺陷,这种
方式必须让用户来完成刷新工作,否则图像不会更新,显然增加了应用开发者的负担。因 Frame
Buffer 框架同时还提供了一种延时工作机制,驱动开发者可以利用该机制实现定时刷新。
linux 驱动开发指南 | 李山文
272
#ifdef CONFIG_FB_DEFERRED_IO
struct fb_deferred_io {
/* delay between mkwrite and deferred handler */
unsigned long delay;
struct mutex lock; /* mutex that protects the page list */
struct list_head pagelist; /* list of touched pages */
/* callback */
void (*first_io)(struct fb_info *info);
void (*deferred_io)(struct fb_info *info, struct list_head *pagelist);
};
#endif
上面便是 Frame Buffer 框架预留的一种方式,这种方式类似于开了一个线程来刷新,但是这种方
式比线程的方式更安全,实际上这种方式是创建一个工作队列。我们先来看下这个结构体:
delay:每次调用刷新函数的时间(tick 值)
lock:互斥锁,在刷新图像时会使用
pagelist:页链表,所有的图像会划分为小页
first_io:第一次刷新图像的回调函数
deferred_io:刷新图像的回调函数(延时工作队列)
说明:每个操作系统都会利用一个硬件定时器来作为滴答时钟,即 tick。但是每个处理器使用的
滴答定时器时间不可能一样,这就造成了我们在设置 delay 的时候会在不同的平台上实际的时间不同
为了让这个时间与硬件无关,我们可以利用 HZ 这个宏来初始化 delayHZ Linux 内核中定义的一
Tick该值表示一秒钟的计数次数,例如 HZ = 100 时,表示 0.1ms tick 1在早期的 Linux
核中 HZ 一般为 100每个 Linux 版本的 HZ 不一样。因此我们在初始化 delay 的时候可以使用如下方
式:
delay = HZ/fps
其中 fps 为帧率,这样我们每次刷新的速率就是 fps 了。
上面成员中 pagelist 比较难懂,这里对其进行解释下。我们的图片可以定义为一个非常大的缓存,
例如我们的显示屏是 240*240 16 位,那么我们需要的显存大小为 240*240*2=115200Byte正常来
说只要我们的图像有变化就刷新显示屏,但实际上我们的图像很有可能不会全部变化,大部分情况下
只变化一小部分。这个时候如果我们全部刷新那未免太浪费了,做了很多无用功,因此 Frame Buffer
框架中将图像划分了很多个小页,当我们的图像有变化时,我们只刷新其对应的页,而其他将不刷新,
这样就大大提高了刷新速度,当然其代价就是程序变得稍微复杂了一些。
13-6 图像划分多个页,粉色部分为需要刷新区域
linux 驱动开发指南 | 李山文
273
这种显示方式的定义在 drivers\video\fbdev\core\fb_defio.c 文件中,该文件定义了相关的结构体和函
数,我们在使用这种显示机制时需要开启 CONFIG_FB_DEFERRED_IO 个宏。这种显示刷新的使用方法
非常简单,我们只需要初始化 fb_info struct fb_deferred_io 中的 delay deferred_io
即可,然后初始化该刷新机制。下面我们举个简单你的例子来说明如何使用
49
static struct fb_deferred_io myfb_defio = {
.delay = HZ / 30,
.deferred_io = myfb_deferred_io,
};
static int myfb_probe(struct platform_device *dev)
{
/* 省略无关代码 */
fb_info->fbdefio = &myfb_defio; //初始化 fb_info 中的 fbdefio
fb_deferred_io_init(fb_info); //初始化刷新机
/* 省略无关代码 */
}
static int myfb_remove(struct xenbus_device *dev)
{
/* 省略无关代码 */
fb_deferred_io_cleanup(fb_info); //清除刷新机制
return 0;
}
上面我们还有一个函数没有定义,就是 myfb_deferred_io 函数,这个函数实际上是刷新图像
的回调函数,该函数实现了局部刷新,下面我们来看这个一个典型的例子:
static void myfb_deferred_io(struct fb_info *fb_info,struct list_head *pagelist)
{
struct myfb_info *info = fb_info->par;
struct page *page;
unsigned long beg, end;
int y1, y2, miny, maxy;
miny = INT_MAX;
maxy = 0;
list_for_each_entry(page, pagelist, lru) {
beg = page->index << PAGE_SHIFT;
end = beg + PAGE_SIZE - 1;
y1 = beg / fb_info->fix.line_length;
y2 = end / fb_info->fix.line_length;
if (y2 >= fb_info->var.yres)
y2 = fb_info->var.yres - 1;
if (miny > y1)
miny = y1;
if (maxy < y2)
maxy = y2;
}
49
详细请参考 Documentation\fb\deferred_io.rst 说明文档。
linux 驱动开发指南 | 李山文
274
myfb_refresh(info, 0, miny, fb_info->var.xres, maxy - miny + 1);
}
上面这个函数中主要是 myfb_refresh 这个函数以及 for_each 中的 page 遍历。首先
myfb_refresh 这个函数是刷新一个窗口,其参数列表为*info, int x1, int y1, int w, int
h。下面我们看 for_each 的遍历干了些啥,首先 for_each 是实际上是一个宏,这个宏在 Linux
用的比较多,其目的是用来遍历一个链表。
13.6 console tty uart 关系
console 即控制台的意思,在早期的计算机中,由于没有如今这样的输入设备,而是使用比较古
老的控制面板,上面有各种按钮和控制开关。
13-7 早期计算机的控制台
后来这个词一直沿用至今,可以看到,控制台也是一种输入输出的设备,但是这种不是普通的输
入输出的设备,而是控制任务作业的输入输出设备。因此一个计算机只能有一个控制台, Linux
的控制台即是 console。当我们通过串口登录开发板系统时,我们进入的实际就是控制台。然而随
系统的不断发展,Linux 内核也变得越来越复杂,我们的控制台不能仅仅是一种特定的物理设备,有
可能是其他设备。因此现在我们指的控制台都是虚拟控制台,而控制台最终会落实到固定的硬件上,
最常见的便是串口, Linux 内核中,串口设备统一叫做 ttyS0而我们开发板中的 tty0tty1tty2
种是虚拟终端设备。
13-8 串口登录开发板控制台
为何叫虚拟终端呢?原理也是一样的,如果我们仅仅局限于一种特定的物理设备,那么设备一旦
变化我们的终端也需要变化,这样就不得不对终端进行抽象,即变成了虚拟终端。而实际的终端设备
可以是显示器、串口等等。
linux 驱动开发指南 | 李山文
275
Linux 嵌入式平台中,我们一般使用串口作为开发板的交互终端,这样就可以进入 Linux 操作
系统的 Shell 进行调试和测试。当然串口终端并不是唯一的方式,在安卓平台中更常用的是 adb 登录
方式。
13-9 consolettyttyS0 三者之间关系
如上图所示,console 作为操作系统的唯一控制台,它可以映射到其他上面,例如可以将 console
映射到 tty0 中,也可以映射到 tty1 中,这个取决于在注册 ttyXX 的时候是否将其作为控制台。在我们
注册 ttyS0 时,我们可以将其设置为控制台,当然我们也可以在 bootargs 内核启动参数中设置相关设
备。 tty0tty1tty2 等这些虚拟终端只是一个接口层,具体的设备需要用户去实现,例如我们在注
fb0 驱动之后,启动会检测到此时有一个实际的物理终端,这个时候系统会默认将此终端和 tty0
拟终端进行绑定,这样我们在对 tty0 的写入实际上就是在对 fb0 的写入。当我们的串口 0 已经注册为
控制台后,假如我们还想注册串口 1 驱动,那么我们不能将串口 1 驱动定义为控制台了,只能将其绑
定到其他的虚拟终端上。Linux 内核已经实现了大量的 tty 这种虚拟设备,因此我们在注册串口驱动
的时候就不需要再实现 tty 虚拟终端了。
13.5 LCD 驱动示例
13.5.1 动分析及编写
下面我们以一个 LCD 显示屏的 fb 驱动来详细讲解如何向内核添加 fb 设备驱动。LCD 屏使用的
SPI 接口的屏,分辨率为 240*240,颜色为 16 位。
13-10 LCD 实物和参数
linux 驱动开发指南 | 李山文
276
刚开始我们当然是需要在设备树中定义我们的设备信息,在设备树文件中的 spi 设备节点中添加
一个 fb 的设备节点,如下所示:
&spi0 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
myfb@0 {
compatible = "test,myfb-spi";
reg = <0>;
spi-max-frequency = <30000000>; /* 30MHz */
spi-cpol; //使用模式 3
spi-cpha;
buswidth = <8>; //数据宽度 8
dc-gpios = <&pio 6 0 GPIO_ACTIVE_HIGH>; /* GPIOG0 */
reset-gpios = <&pio 6 1 GPIO_ACTIVE_HIGH>; /* GPIOG1 */
};
};
然后,我们需要实现最基本的 probe 函数,这个函数需要实现 LCD 的初始化以及 FB 驱动的注
册,如下所示:
static int myfb_probe(struct spi_device *spi)
{
int ret;
void *gmem_addr;
u32 gmem_size;
fbspi = spi; //保存 spi 驱动指
printk(KERN_ERR"register myfb_spi_probe!\n");
//申请 GPIO 用作 DC 引脚
dc_pin = devm_gpiod_get(&spi->dev,"dc",GPIOF_OUT_INIT_LOW);
if(IS_ERR(dc_pin))
{
printk(KERN_ERR"fail to request dc-gpios!\n");
return -1;
}
//申请 GPIO 用作 RESET 引脚
reset_pin = devm_gpiod_get(&spi->dev,"reset",GPIOF_OUT_INIT_HIGH);
if(IS_ERR(reset_pin))
{
printk(KERN_ERR"fail to request reset-gpios!\n");
return -1;
}
gpiod_direction_output(dc_pin,0);//设置输出方向
gpiod_direction_output(reset_pin,1);//设置输出方向
printk(KERN_INFO"register myfb_probe dev!\n");
myfb = framebuffer_alloc(sizeof(struct fb_info), &spi->dev);//申请 fb_info 结构
linux 驱动开发指南 | 李山文
277
//初始化底层操作结构体
myfb->fbops = &myfb_ops; //指定底层操作结构体
gmem_size = 240 * 240 * 2; //设置显存大小,16bit 2 字节
gmem_addr = kmalloc(gmem_size,GFP_KERNEL); //分配 Frame Buffer 显存
if(!gmem_addr)
{
printk(KERN_ERR"fail to alloc fb buffer!\n");
}
myfb->pseudo_palette = pseudo_palette;
myfb->var = myfb_var; //设置分辨率参数
myfb->fix = myfb_fix; //设置显示参数
myfb->screen_buffer = gmem_addr;//设置显存地
myfb->screen_size = gmem_size;//设置显存大小
myfb->fix.smem_len = gmem_size;//设置应用层显存大小
myfb->fix.smem_start = (u32)gmem_addr;//设置应用层数据地址
memset((void *)myfb->fix.smem_start, 0, myfb->fix.smem_len); //清楚数据缓存
myfb_init(spi); //初始化显示屏
myfb->fbdefio = &myfb_defio; //设置刷新参数
fb_deferred_io_init(myfb); //初始化刷新机制
ret = register_framebuffer(myfb); //注册 fb 驱动
if(ret)
{
framebuffer_release(myfb);
unregister_framebuffer(myfb);
devm_gpiod_put(&spi->dev, dc_pin);
devm_gpiod_put(&spi->dev, reset_pin);
printk(KERN_ERR"fail to register fb dev!\n");
return -1;
}
//开启一个线程用来刷新显示屏
fb_thread= kthread_run(thread_func_fb, myfb, spi->modalias);
return 0;
}
上面的代码较为简单,主要是对显示屏进行初始化。首先申请 DC RESET 引脚,然后设置
FB 的主要参数,需要注意的是 fb_var_screeninfo fb_fix_screeninfo 参数必须设置,同时
ops 底层的接口也需要设置。接着我们需要开辟一块大的显存用于 FB 的图形存储,这里必须对这
这几个进行初始化:screen_bufferscreen_size fix.smem_startfix.smem_len,前者
为显示图形的数据保存,后面的在 fb_deferred_io 刷新机制中会调用,因此必须初始化。接着我
们需要初始化 fbdefio fb_deferred_io_init,这个刷新机制是将应用层的数据搬移到显存
中,不然即使注册了显存里面的数据也会一直为空。注册完毕了 fb 驱动,我们需要开启一个线程来
刷新显示屏。
remove 函数和 probe 函数正好相反,主要是 fb 驱动的卸载,在卸载时候需要注意资源的释
放。具体代码如下所示:
int myfb_remove(struct spi_device *spi)
{
linux 驱动开发指南 | 李山文
278
fb_deferred_io_cleanup(myfb); //清除刷新机制
unregister_framebuffer(myfb);
devm_gpiod_put(&spi->dev, dc_pin);
devm_gpiod_put(&spi->dev, reset_pin);
return 0;
}
这个函数中对 deferred_io 进行了清除,同时释放 DC RESET 引脚资源。下面我们来看下显
示屏参数的一些设置,首先我们先看写 fb_var_screeninfo 参数设置:
static struct fb_var_screeninfo myfb_var = {
.rotate = 0,
.xres = 240, //屏幕宽度
.yres = 240, //屏幕高度
.xres_virtual = 240, //显示宽度
.yres_virtual = 240, //显示高度
.bits_per_pixel = 16, //像素位
.nonstd = 1, //非标准
/* RGB565 */
.red.offset = 11, //红色偏移
.red.length = 5, //红色所占位宽
.green.offset = 5, //绿色偏移
.green.length = 6, //绿色所占位宽
.blue.offset = 0, //蓝色偏移
.blue.length = 5, //蓝色所占位宽
.transp.offset = 0, //透明度所占偏移
.transp.length = 0, //透明度位宽
.activate = FB_ACTIVATE_NOW, //立即更新
.vmode = FB_VMODE_NONINTERLACED,
};
上面的参数设置比较容易理解,只需要对应屏幕的数据手册进行设置即可。下面我们来看下
fb_fix_screeninfo,如下所示:
static struct fb_fix_screeninfo myfb_fix = {
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.line_length = 240*2, //16 bit = 2byte
.accel = FB_ACCEL_NONE,//没有使用硬件加
.id = "myfb",
};
上面中的 line_length 比较重要,这个参数指定了每一行屏幕像素所占的字节数,例如我们的是
240*240 的分辨率,那么每一行的像素点 240 个,每个像素点 16 位,这样需要用 2 字节表示,从而一行
240*2 个字节数。
下面我们来看 Fb ops 如何设置:
static struct fb_ops myfb_ops = {
.owner = THIS_MODULE,
.fb_write = fb_sys_write,
.fb_setcolreg = myfb_setcolreg, /*设置颜色寄存器*/
linux 驱动开发指南 | 李山文
279
.fb_fillrect = sys_fillrect, /*用像素行填充矩形框,通用库函*/
.fb_copyarea = sys_copyarea, /*将屏幕的一个矩形区域复制到另一个区域,通用库函数*/
.fb_imageblit = sys_imageblit, /*显示一副图像,通用库函数*/
};
上面的设置其实大部分用的都是 Linux 内核的库函数,没有使用硬件加速。fb_sys_write
sys_fillrectsys_copyareasys_imageblit 这四个函数都是 fb 驱动默认的基本绘图函数。
myfb_setcolreg 为调色板函数,该函数的定义如下所示:
static int myfb_setcolreg(u32 regno, u32 red,u32 green, u32 blue,u32 transp, struct
fb_info *info)
{
unsigned int val;
if (regno > 16)
{
return 1;
}
val = chan_to_field(red, &info->var.red);
val |= chan_to_field(green, &info->var.green);
val |= chan_to_field(blue, &info->var.blue);
pseudo_palette[regno] = val;
return 0;
}
该函数基本是一个模板里面出来的,几乎绝大部分的都是使用这个函数,完全不用修改即可使
用。这个函数的核心工作就是设置调色板的值。上面有个函数 chan_to_field,其定义如下:
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xffff;
chan >>= 16 - bf->length;
return chan << bf->offset;
}
这个函数对每个颜色位进行分配,可以看到,我们的液晶屏的颜色值为 16 位,因此我们需要将
每个颜色取出来。我们再来看下在 probe 函数中开的一个线程进行刷新,其线程任务定义如下:
int thread_func_fb(void *data)
{
struct fb_info *fbi = (struct fb_info *)data;
while (1)
{
if (kthread_should_stop())
break;
fb_refresh(fbi,fbspi);
}
return 0;
}
该线程比较简单,里面就只有一个 fb_refresh 函数,这个函数就是将显存中的数据刷新到液
晶屏上,其定义如下:
linux 驱动开发指南 | 李山文
280
void fb_refresh(struct fb_info *fbi, struct spi_device *spi)
{
int x, y;
u16 *p = (u16 *)(fbi->screen_base);
fb_set_win(spi, 0,0,239,239);
for (y = 0; y < fbi->var.yres; y++)
{
for (x = 0; x < fbi->var.xres; x++)
{
fb_write_data(spi,*p);
p++;
}
}
}
可以看到,刷新很简单,就是将数据原封不动的搬移到液晶屏上
50
。最后我们来看下重头戏,
probe 函数中一个 deferred_io 初始化部分,这个部分非常重要,下面我们来详细看看这个如何定
义的,其代码如下:
static void myfb_update(struct fb_info *fbi, struct list_head *pagelist)
{
//比较粗暴的方式,直接全部刷新
fbi->fbops->fb_pan_display(&fbi->var,fbi); //将应用层数据刷 FrameBuffer 缓存中
}
static struct fb_deferred_io myfb_defio = {
.delay = HZ/20,
.deferred_io = &myfb_update,
};
第一个函数是一个更新显存中内容的函数,这个函数实际上使用了一种页的刷新机制,这就是
13.4 小节讲到的。这种刷新机制的好处就是我们不需要全部刷新,而是动态刷新显示屏,特别是对
于一些简单的显示,大部分情况下屏幕只变化了很小的一部分,这样我们就可以局部刷新,提高刷
新速度同时也降低 CPU 的使用率。从第一个 myfb_update 函数中,可以看到这里非常简单,并没
有做页面机制刷新,而是直接使用了 fb_pan_display,这个函数是 fb ops 的一个操作函数,
我们不需要定义,直接调用就可以了。在实际的开发中我们最好使用基于页的刷新方式。这里为了
简单,帮助读者理解 FB 框架,就省略了这个优化的地方。
最后就是我们的驱动结构体的定义了,如下所示:
struct of_device_id myfb_match[] = {
{.compatible = "test,myfb-spi"},
{},
};
struct spi_driver myfb_drv = {
.probe = myfb_probe,
.remove = myfb_remove,
50
不知读者是否注意到,这里有优化的空间,仔细看 spi 驱动章节就会发现,笔者这里主要是为了简单,方便读者能够快速明白
框架,而不要太局限于细节,在实际中一定需要对其进行优化。
linux 驱动开发指南 | 李山文
281
.driver = {
.owner = THIS_MODULE,
.name = "myfb_spi_driver",
.of_match_table = myfb_match,
},
};
需要注意的是上面的.name 必须定义,因为在注册驱动的时候调用了该变量,如果没有定义会
出现指针未初始化造成的段错误。
13.5.2 驱动代码
设备节点如下:
&spi0 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
myfb@0 {
compatible = "test,myfb-spi";
reg = <0>;
spi-max-frequency = <30000000>; /* 10MHz */
spi-cpol;
spi-cpha;
buswidth = <8>;
dc-gpios = <&pio 6 0 GPIO_ACTIVE_HIGH>; /* GPIOG0 */
reset-gpios = <&pio 6 1 GPIO_ACTIVE_HIGH>; /* GPIOG1 */
};
};
驱动源码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有 ioremap 函数 iounmap 函数
#include <asm/uaccess.h> //含有 copy_from_user 函数和含有 copy_to_user 函数
#include <linux/device.h> //含有类相关的设备函数
#include <linux/cdev.h>
#include <linux/platform_device.h> //包含 platform 函数
#include <linux/of.h> //包含设备树相关函数
#include <linux/fb.h> //包含 frame buffer
#include <linux/spi/spi.h>
#include <linux/regmap.h>
#include <linux/gpio.h>
static struct fb_info *myfb; //定义一个 fb_info
static struct task_struct *fb_thread; //定义一个线程刷新屏
static struct spi_device *fbspi; //保存 spi 驱动指针
static struct gpio_desc *dc_pin; //dc 引脚
static struct gpio_desc *reset_pin; //reset 引脚
linux 驱动开发指南 | 李山文
282
static u32 pseudo_palette[16]; //调色板缓存区
static void fb_write_reg(struct spi_device *spi, u8 reg)
{
gpiod_set_value(dc_pin, 0); //低电平,命令
spi_write(spi, &reg, 1);
}
static void fb_write_data(struct spi_device *spi, u16 data)
{
u8 buf[2];
buf[0] = ((u8)(data>>8));
buf[1] = ((u8)(data&0x00ff));
gpiod_set_value(dc_pin, 1); //高电平,数据
spi_write(spi, &buf[0], 1);
spi_write(spi, &buf[1], 1);
}
static void fb_set_win(struct spi_device *spi, u8 xStar, u8 yStar,u8 xEnd,u8 yEnd)
{
fb_write_reg(spi,0x2a);
fb_write_data(spi,xStar);
fb_write_data(spi,xEnd);
fb_write_reg(spi,0x2b);
fb_write_data(spi,yStar);
fb_write_data(spi,yEnd);
fb_write_reg(spi,0x2c);
}
static void myfb_init(struct spi_device *spi)
{
gpiod_set_value(reset_pin, 0); //设低电平
msleep(100);
gpiod_set_value(reset_pin, 1); //设高电平
msleep(50);
/* 写寄存器,初始化 */
fb_write_reg(spi,0x36);
fb_write_data(spi,0x0000);
fb_write_reg(spi,0x3A);
fb_write_data(spi,0x0500);
fb_write_reg(spi,0xB2);
fb_write_data(spi,0x0C0C);
fb_write_data(spi,0x0033);
fb_write_data(spi,0x3300);
fb_write_data(spi,0x0033);
fb_write_data(spi,0x3300);
fb_write_reg(spi,0xB7);
linux 驱动开发指南 | 李山文
283
fb_write_data(spi,0x3500);
fb_write_reg(spi,0xB8);
fb_write_data(spi,0x1900);
fb_write_reg(spi,0xC0);
fb_write_data(spi,0x2C00);
fb_write_reg(spi,0xC2);
fb_write_data(spi,0xC100);
fb_write_reg(spi,0xC3);
fb_write_data(spi,0x1200);
fb_write_reg(spi,0xC4);
fb_write_data(spi,0x2000);
fb_write_reg(spi,0xC6);
fb_write_data(spi,0x0F00);
fb_write_reg(spi,0xD0);
fb_write_data(spi,0xA4A1);
fb_write_reg(spi,0xE0);
fb_write_data(spi,0xD004);
fb_write_data(spi,0x0D11);
fb_write_data(spi,0x132B);
fb_write_data(spi,0x3F54);
fb_write_data(spi,0x4C18);
fb_write_data(spi,0x0D0B);
fb_write_data(spi,0x1F23);
fb_write_reg(spi,0xE1);
fb_write_data(spi,0xD004);
fb_write_data(spi,0x0C11);
fb_write_data(spi,0x132C);
fb_write_data(spi,0x3F44);
fb_write_data(spi,0x512F);
fb_write_data(spi,0x1F1F);
fb_write_data(spi,0x2023);
fb_write_reg(spi,0x21);
fb_write_reg(spi,0x11);
mdelay(50);
fb_write_reg(spi,0x29);
mdelay(200);
}
void fb_refresh(struct fb_info *fbi, struct spi_device *spi)
{
int x, y;
u16 *p = (u16 *)(fbi->screen_base);
fb_set_win(spi, 0,0,239,239);
for (y = 0; y < fbi->var.yres; y++)
{
for (x = 0; x < fbi->var.xres; x++)
{
fb_write_data(spi,*p);
linux 驱动开发指南 | 李山文
284
p++;
}
}
}
int thread_func_fb(void *data)
{
struct fb_info *fbi = (struct fb_info *)data;
while (1)
{
if (kthread_should_stop())
break;
fb_refresh(fbi,fbspi);
}
return 0;
}
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xffff;
chan >>= 16 - bf->length;
return chan << bf->offset;
}
static u32 pseudo_palette[16];
static int myfb_setcolreg(u32 regno, u32 red,u32 green, u32 blue,u32 transp, struct
fb_info *info)
{
unsigned int val;
if (regno > 16)
{
return 1;
}
val = chan_to_field(red, &info->var.red);
val |= chan_to_field(green, &info->var.green);
val |= chan_to_field(blue, &info->var.blue);
pseudo_palette[regno] = val;
return 0;
}
static struct fb_ops myfb_ops = {
.owner = THIS_MODULE,
.fb_write = fb_sys_write,
.fb_setcolreg = myfb_setcolreg, /*设置颜色寄存器*/
.fb_fillrect = sys_fillrect, /*用像素行填充矩形框,通用库函*/
.fb_copyarea = sys_copyarea, /*将屏幕的一个矩形区域复制到另一个区域,通用库函数*/
linux 驱动开发指南 | 李山文
285
.fb_imageblit = sys_imageblit, /*显示一副图像,通用库函*/
};
static void myfb_update(struct fb_info *fbi, struct list_head *pagelist)
{
//比较粗暴的方式,直接全部刷新
fbi->fbops->fb_pan_display(&fbi->var,fbi); //将应用层数据刷 FrameBuffer 缓存中
}
static struct fb_deferred_io myfb_defio = {
.delay = HZ/20,
.deferred_io = &myfb_update,
};
static struct fb_var_screeninfo myfb_var = {
.rotate = 0,
.xres = 240,
.yres = 240,
.xres_virtual = 240,
.yres_virtual = 240,
.bits_per_pixel = 16,
.nonstd = 1,
/* RGB565 */
.red.offset = 11,
.red.length = 5,
.green.offset = 5,
.green.length = 6,
.blue.offset = 0,
.blue.length = 5,
.transp.offset = 0,
.transp.length = 0,
.activate = FB_ACTIVATE_NOW,
.vmode = FB_VMODE_NONINTERLACED,
};
static struct fb_fix_screeninfo myfb_fix = {
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.line_length = 240*2,//16 bit = 2byte
.accel = FB_ACCEL_NONE,//没有使用硬件加
.id = "myfb",
};
static int myfb_probe(struct spi_device *spi)
{
int ret;
void *gmem_addr;
u32 gmem_size;
linux 驱动开发指南 | 李山文
286
fbspi = spi; //保存 spi 驱动指针
printk(KERN_ERR"register myfb_spi_probe!\n");
//申请 GPIO 用作 DC 引脚
dc_pin = devm_gpiod_get(&spi->dev,"dc",GPIOF_OUT_INIT_LOW);
if(IS_ERR(dc_pin))
{
printk(KERN_ERR"fail to request dc-gpios!\n");
return -1;
}
//申请 GPIO 用作 RESET 引脚
reset_pin = devm_gpiod_get(&spi->dev,"reset",GPIOF_OUT_INIT_HIGH);
if(IS_ERR(reset_pin))
{
printk(KERN_ERR"fail to request reset-gpios!\n");
return -1;
}
gpiod_direction_output(dc_pin,0);//设置输出方向
gpiod_direction_output(reset_pin,1);//设置输出方向
printk(KERN_INFO"register myfb_probe dev!\n");
myfb = framebuffer_alloc(sizeof(struct fb_info), &spi->dev);//向内核申请 fb_info 结构体
//初始化底层操作结构体
myfb->fbops = &myfb_ops; //指定底层操作结构体
gmem_size = 240 * 240 * 2; //设置显存大小,16bit 2 字节
gmem_addr = kmalloc(gmem_size,GFP_KERNEL); //分配 Frame Buffer 显存
if(!gmem_addr)
{
printk(KERN_ERR"fail to alloc fb buffer!\n");
}
myfb->pseudo_palette = pseudo_palette;
myfb->var = myfb_var; //设置分辨率参数
myfb->fix = myfb_fix; //设置显示参数
myfb->screen_buffer = gmem_addr;//设置显存地址
myfb->screen_size = gmem_size;//设置显存大小
myfb->fix.smem_len = gmem_size;//设置应用层显存大小
myfb->fix.smem_start = (u32)gmem_addr;//设置应用层数据地址
memset((void *)myfb->fix.smem_start, 0, myfb->fix.smem_len); //清楚数据缓存
myfb_init(spi); //初始化显示屏
myfb->fbdefio = &myfb_defio; //设置刷新参数
fb_deferred_io_init(myfb); //初始化刷新机制
ret = register_framebuffer(myfb); //注册 fb 驱动
if(ret)
{
framebuffer_release(myfb);
unregister_framebuffer(myfb);
linux 驱动开发指南 | 李山文
287
devm_gpiod_put(&spi->dev, dc_pin);
devm_gpiod_put(&spi->dev, reset_pin);
printk(KERN_ERR"fail to register fb dev!\n");
return -1;
}
//开启一个线程用来刷新显示屏
fb_thread= kthread_run(thread_func_fb, myfb, spi->modalias);
return 0;
}
int myfb_remove(struct spi_device *spi)
{
fb_deferred_io_cleanup(myfb); //清除刷新机制
unregister_framebuffer(myfb);
devm_gpiod_put(&spi->dev, dc_pin);
devm_gpiod_put(&spi->dev, reset_pin);
return 0;
}
struct of_device_id myfb_match[] = {
{.compatible = "test,myfb-spi"},
{},
};
struct spi_driver myfb_drv = {
.probe = myfb_probe,
.remove = myfb_remove,
.driver = {
.owner = THIS_MODULE,
.name = "myfb_spi_driver",
.of_match_table = myfb_match,
},
};
module_spi_driver(myfb_drv);
MODULE_LICENSE("GPL"); //不加的话加载会有错误提
MODULE_AUTHOR("1477153217@qq.com"); //作者
MODULE_VERSION("0.1"); //版本
MODULE_DESCRIPTION("myfb_spi_driver"); //简单的描述
13.5.3 驱动测试
51
我们以动态加载的方式测试驱动程序,首先将源码编译为.ko 文件,然后下载到开发板中的根文
件系统中,进入根文件系统,执行如下:
51
注意:SPI CLK MOSI 引脚必须接上拉电阻,因为不接上拉电阻,SPI 的时钟频率上不去
linux 驱动开发指南 | 李山文
288
# insmod mytftfb.ko
[ 46.216918] mytftfb: loading out-of-tree module taints kernel.
[ 46.223565] register myfb_spi_probe!
[ 46.227400] sun8i-v3s-pinctrl 1c20800.pinctrl: supply vcc-pg not found, using dum
my regulator
[ 46.236181] register myfb_probe dev!
[ 46.679297] Console: switching to colour frame buffer device 30x30
此时可以看到屏幕刷新了,然后编程了黑色背景,同时出现了光标,从终端的 log 可以看到,
fb 挂载成功后,控制台驱动检测到 fb 设备后会自动将 fb0 映射到 tty0 上,这样 tty0 实际上就是 fb0
设备。现在我们向屏幕输入一个 hello frame buffer,如下:
# echo hello frame buffer > /dev/tty0
#
可以看到此时液晶屏上显示了该文字,我们也可以将当前目录输出到液晶屏上:
# ls / > /dev/tty0
#
如图所示:
13-11 液晶屏显示效果
上面的驱动虽然可以当作终端设备显示了,但其刷新率实在太低,测试过的读者会发现其刷新了
只有 1Hz 左右,这种刷新率是没办法在实际中使用的,因此我们必须对其进行优化,读者可以参考第
十章内容,主要是修改 fb_refresh 函数,将单个发送 16 位的数据修改为一次发送一个屏幕的数据,
样就可以大大提高刷新速度。
当然通过将多次发送数据修改为一次发送可以提高速度,但并不能减少 CPU 的使用率,如何减
CPU 的使用率呢?在 13.4 小节中讲到过局部刷新的机制,这种机制就是将整个屏幕划分为多个区
域,我们称之为页,我们在刷新之前检测下哪些页被修改了(即有哪些脏页)我们就只刷新这些脏
linux 驱动开发指南 | 李山文
289
页,其他的不进行刷新,这样 CPU 就可以少干点没有意义的事情。
13.6 fbtft 驱动使用
Linux 驱动源码中,已经支持了几乎所有的自带控制器的 SPI 接口的液晶屏驱动,其源码在
drivers/staging/fbtft/中,该驱动的原理实际上正是上面讲的,基于局部刷新机制,这种刷新机制可以使
帧数达到最大,因此如果我们的项目需要使用 SPI 液晶屏驱动时,使用 fbtft 驱动是最佳的选择,下面
来讲解下如何使用 fbtft 驱动。
首先在设备树中的 dts 文件中添加设备节点:
&spi0 {
status = "okay";
st7789v@0 {
status = "okay";
compatible = "sitronix,st7789v";
reg = <0>;
spi-max-frequency = <3000000>;
rotate = <0>;
spi-cpol;
spi-cpha;
rgb;
fps = <30>;
buswidth = <8>;
reset-gpios = <&pio 6 1 GPIO_ACTIVE_LOW>; //GPIOG1
dc-gpios = <&pio 6 0 GPIO_ACTIVE_HIGH>; //GPIOG0
debug = <0>;
};
};
然后在 menuconfig 中将驱动编译到内核中,在终端中执行 make menuconfig 命令,进入图形配置
界面,然后在 Device Drivers>Staging drivers>Support for small TFT LCD display modules 选项
中勾选<*> FB driver for the ST7789V LCD Controller 选项,退出保存。
13-12 ST7789V 驱动编译进内核
说明: fbtft 驱动源码中,reset 引脚使用的是负逻辑,因此我们在设备树节点中设定的 LOW否则我们
的液晶屏将无法复位。
由于自带的驱动中,ST7789V 的初始化程序与我们常用的液晶屏不符合,导致颜色失真,因此我
们需要修改 drivers/staging/fbtft/fb_st7789v.c 文件中的初始化程序。打开该文件,然后找到 init_display
函数,将其替换为如下:
linux 驱动开发指南 | 李山文
290
static int init_display(struct fbtft_par *par)
{
par->fbtftops.reset(par);
mdelay(50);
write_reg(par,0x36,0x00);
write_reg(par,0x3A,0x05);
write_reg(par,0xB2,0x0C,0x0C,0x00,0x33,0x33);
write_reg(par,0xB7,0x35);
write_reg(par,0xBB,0x19);
write_reg(par,0xC0,0x2C);
write_reg(par,0xC2,0x01);
write_reg(par,0xC3,0x12);
write_reg(par,0xC4,0x20);
write_reg(par,0xC6,0x0F);
write_reg(par,0xD0,0xA4,0xA1);
write_reg(par,0xE0,0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F
,0x23);
write_reg(par,0xE1,0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20
,0x23);
write_reg(par,0x21);
write_reg(par,0x11);
mdelay(50);
write_reg(par,0x29);
mdelay(200);
return 0;
}
同时修改屏幕参数,找到 static struct fbtft_display display 结构体,将其中的 width height 修改
240
13-13 修改屏幕分辨率
上面的初始化程序根据液晶屏的数据手册修改,保存之后,编译内核即可。编译完毕后,将新的
zImage dtb 文件放到 BOOT 分区中,然后启动内核,此时可以看到我们的液晶屏亮起,我们中串
口终端中输入如下:
# ls / > /dev/tty0
#
可以地看到,此时我们的液晶屏上就出现了根目录的内容。
linux 驱动开发指南 | 李山文
291
13-13 fbtft 驱动效果
linux 驱动开发指南 | 李山文
292
第十四章 字符设备总结
前面的章节我们已经讲解了字符设备的绝大部分的内容,包括字符设备的基本框架、基于设备树
的驱动框架、基于 platform 的平台设备驱动、基于 sysfs 的统一设备模型以及各个常用的子系统。如
果读者能够认真读到本章,相信读者已经对字符设备有了最基本的掌握,现在我们再对字符设备进行
一个总结。
14-1 字符设备驱动框架
如上图所示,platform 实现了软硬件分离,SoC 的硬件信息有设备树文件来实现,内核加载的时
候会将设备树节点解析为 device,而 driver 由驱动开发者编写,bus 总线会将 driver device 绑定。
对于 udevfs 而言,其目的是实现应用程序所需要的设备节点文件,该文件就是我们接触最多的/dev
录下的文件。对于 sysfs 而言,使用这种框架可以在/sys/devices 目录下生成属性文件,我们可以直接
操作属性文件来进行驱动的读写操作,当然这种操作是有限制的,后面我们会详细说明。
可以看到,上面的驱动框架三者并不是密切相关的,而是相互独立的,也就是一个驱动可以仅仅
使用 udevfs,可以不适用 platform,也可以仅仅使用 platform 框架,不使用其他的。但是他们都最终
都是通过调用 module_init module_exit 来实现驱动的注册(对于 platform 框架而言,其对 module_init
module_exit 进行了封装)
在实际上项目开发中,我们一般这样使用这些框架的:
platform:实现软硬件分离,即使用 dts 来指定设备信息,使用 compatible 匹配驱动并挂载
udevfs:实现应用程序调用的设备节点文件
sysfs:用来调试驱动,调试时可以更改驱动中的一些参数以及底层的参数查看
因此在实际编写驱动的时候,上面的框架都会使用,当然,有时候驱动实在太过简单时我们也可
能仅仅只用其中的一种框架,根据实际的场景来编写驱动。
14.1 几个点说明
14.1.1 show store 限制
不知道读者是否记得,我们在使用 sysfs 时候,show store 函数比较奇怪,这两个函数居然不
需要使用 copy_to_user copy_from_user 数,实际上这个内核实现了,因此我们在执行 show
linux 驱动开发指南 | 李山文
293
store 中的函数时,参数 buf 指向的已经时内核空间了。但是这里还有一个问题,即在 show 函数的参
数中并没有 count 参数,也就是我们的数据大小是否是没有限制?显然不是,在 Linux 内核中,有一
PAGE_SIZE 宏,这个宏就是 buf 的大小,也就是我们的读写并不是无限的空间。在内核调用这两
个函数时,此时内核会分配一个 PAGE_SIZE 的内核空间,我们的 show store 函数的 buf 就是指向
这个内核空间,因此我们在读写的时候应该确保不会超过这个空间的大小。因此我们需要对其进行判
断,当然,如果使用 scnprintf()函数就不需要做判断了,因为这个函数内部已经做了检测了。
14.1.2 多文件驱动模块
我们上面的所有驱动都是只有一个.c 文件,如果我们的驱动代码包含多.c 文件那如何来编写
呢?下面我们举一例例子便会明白:
例子:
现在有三个 C 文件,分别是 at24c02.c at24c02_cmd.c at24c02_back.c,我们希望最终编译的模
块名未 at24cxx.ko,这样我们的 Make 应该写成下面这样:
KERN_DIR = /home/lsw/licheepi/linux
all:
make -C $(KERN_DIR) M=$(shell pwd) modules
clean:
rm -rf *.order *o *.symvers *.mod.c *.mod *.ko
at24cxx-objs += at24c02.o at24c02_cmd.o at24c02_back.o
obj-m += at24cxx.o
上面的最终编译的模块名最好不要和 C 文件的名字相同,否则会出现奇怪的问题,因此我们在编
译多个 C 文件的模块时,我们的 Makefile 遵循的原则是:
模块名-objs += C 源文件.o
obj-m += 模块名.o
linux 驱动开发指南 | 李山文
294
第十五章 应用移植开发示例(待删除)
因为在驱动开发的过程中,我们会做一些简单的应用移植和开发。本章将会举几个典型的应用移
植和开发示例来供读者参考。(本章节不属于驱动开发,后期会将本章节删除)
15.1 口传输工具移植
在驱动开发的过程中,我们的驱动会编译为.ko 文件,该文件需要放到开发板中,但是对于有些
开发板比较简陋,可能没有网络功能,无法实现 FTP 这种文件传输,更谈不上 ssh 远程登录。但几乎
所有的开发板都有串口,因此我们可以利用串口来传输我们的文件,最常用的串口文件传输工具就是
lrzsz,该工具非常方便,只要串口终端支持 Xmodem 或者 Ymodem 或者 Zmodem 其中的任意一种即
可,目前 Xshell SecurtCRT 是支持的,下面我们开始移植这个工具到我们的开发板上。
首先进入 lrzsz 的官网,地址:https://www.ohse.de/uwe/software/lrzsz.html 然后点击 Downloading
下载连接,如下图所示,下载完毕后我通过 SSH 或者 FTP 放到 ubuntu 虚拟机中。
15-1 lrzsz 串口传输工具
然后我们解压源码,进入该源码目录,在目录中执行如下命令:
CFLAGS=-O2 CC=arm-linux-gcc ./configure --cache-file=arm_linux_cache
15-2 指定交叉工具链并执行配置文件
linux 驱动开发指南 | 李山文
295
需要注意的是,上面的 arm-linux-gcc 是使用编译根文件系统的交叉工具链,因此这里读者最好
使用的交叉工具链和编译根文件系统的工具链为同一个。至于编译根文件系统的交叉工具链在哪里
找,可以参考第一章节中的 1.4 小节
结束之后,在执行 make 即可,此时会在 src 目录下生成两个可执行文件,如下图所示,我们使
readelf 工具可以看到该文件的架构是 arm-v7(作者使用的是 V3s 芯片)
15-3 生成的 lrz lsz 可执行文件
将这两个可执行文件下载到开发板的文件中,放在/usr/sbin/目录下,此时我们就可以使用这两个
工具通过串口来传输我们的文件了。
作者这里使用 SecureCRT 作为演示,首先打开此工具,然后新建一个会话,选择 serial 类型。
击“下一页”。
15-4 新建会话
然后选择 COM 端口,设置波特率 115200,点击“下一页”,然后再次点击“下一页”,点击“完成”
15-5 设置串口参数
linux 驱动开发指南 | 李山文
296
选中左侧的串口,鼠标右击,点击最后一个属性,然后点击 X/Y/Zmodem,设置相应的上传和下
载目录,最后点击“OK”。
15-6 设置上传和下载目录
现在我们连接串口,然后进入开发板根目录,然后知名 lrz 命令上传文件,此时会弹出一个对话
框,选择相应的文件,同时点击 Add 按钮,最后点击 OK 即可完成上传。
15-7 通过 lrz 上传文件到开发板
我们还可以通过 lsz 来将开发板的文件下载到主机,操作命令相同。
15.2 QT 应用开发
Linux 中,大部分图形应用开发都会选择 QT 作为首选框架,其主要优势在于 QT 的跨平台。
下面我们通过一个简单的 QT 开发示例来学习下如何在 ARM 开发板上开发 QT 应用。
linux 驱动开发指南 | 李山文
297
15.2.1 发环境搭
首先我们需要搭建 QT 开发环境,前面我们的应用开发使用的是交叉工具链,QT 也是应用开发,
也需要使用相应的交叉工具链,我们可以使用和前面应用程序一样的交叉工具链,但是由于 QT 使用
的是 qmake,因此我们需要构建 qmake 工具,这里我们使用 buildroot 自动生成 qmake
打开根文件系统,执行 make menuconfig,进入图形配置界面。进入 Toolchain --->然后勾选
[*] Enable WCHAR support 选项,勾选这个主要是因为 QT5 必须支持 WCHAR 的支持。然后勾选 C++,
因为 QT 是必须依赖于 C++的,如图所示,作者勾选了更多选项:
15-8 勾选 Enable WCHAR support 选项
然后将光标移到<Exit>处,点击回车,回到上一级目录,现在我们进入 Target packages --->
项,然后进入 Graphic libraries and applications (graphic/text) --->选项,此时我们可以看到有一个
[] Qt5 --->选项,这个就是我们需要勾选的选项,现在我们勾选这个 QT5 选项,然后进入该选项中,
15-10 勾选 QT5 并勾选 gui_module 组件 widgets_module 组件
然后勾选[*]gui module [*]widgets module 选项,现在将光标移到<Save>点击回车保存,然后
退出图形化配置界面,在终端中执行 make 命令编译即可,等待编译完毕。由于是首次安装,编译
程会比较耗时。编译完毕之后,我们需要将开发板的根文件系统重新全部更新下。
注意:笔者使用的是 Buildroot 2018.2.5 版本,其他版本可能会略有差别,但步骤一样。
linux 驱动开发指南 | 李山文
298
15.2.2 QT 应用程序开发
15.2.2.1 Buildroot 自带示例
编译完毕后, output/build/qt5base-5.15.2/bin 目录下可以找到 qmake 文件,这个文件就是 QT
用开发中需要的 make 工具。
15-11 编译之后生成 qmake 工具
Buildroot 编译之后会自带一些简单的示例, output/build/qt5base-5.15.2/examples/widgets/widgets
目录下,如下图所示:
15-12 buildroot 带的示例
现在我们就可以利用这个工具来对我们的应用程序进行构建了
52
进入 analogclock 目录,这个是
一个简单的模拟时钟的示例。可以看到,该目录下有源码,但是没有 Makefile 文件,现在我们需要使
qmake 工具构建出 Makefile 文件。
15-13 analogclock 示例
在当前目录下执行如下命令:
../../../../../qt5base-5.9.6/bin/qmake analogclock.pro
此时可以看到会生成一个 Makefile 文件,该文件就是 qmake 构建出来的工程管理文件:
15-14 qmake 构建后生成的 Makefile 文件
现在我们就可以执行 make 命令来编译工程了:
make
15-5 执行 Make 编译 analogclock 工程
编译完毕后,我们可以看到最终的二进制文件生成了,即 analogclock 文件,现在我们将该文件
下载到开发板的根文件系统中。
15-6 生成了二进制可执行文件
52
QT 应用开发中,源文件需要使用 qmake 工具构建生成 Makefile 文件,然后就可以使用交叉工具链编译成可执行文件了。
linux 驱动开发指南 | 李山文
299
下载到开发板上之后,我们在终端中执行如下命令(前提是 fb 驱动已经挂载且可用)
# ./analogclock -platform linuxfb
此时我们可以看到屏幕已经出现了模拟时钟,如下图所示:
15-5 模拟时钟示例效果
15.2.3 QT Creator 开发
在实际 QT 开发中,我们一般使用 IDE 进行开发,QT 官方的 QT Creator 可以说是最好的 IDE
我们可以借助 IDE 进行图形化的控件开发,这样可以减少工作量,下面我们来搭建开发 QT Creator
境。
首先我们在官方上下载 QT Creator,官网地址:https://download.qt.io/official_releases/,我们下载
QT 5.12.4 版本的。
15-6 下载 QT Creator 5.12.4 版本
下载完毕后,在终端中执行如下命令来为文件添加可执行权限:
sudo chmod +x qt-opensource-linux-x64-5.12.4.run
linux 驱动开发指南 | 李山文
300
然后执行该文件:
./qt-opensource-linux-x64-5.12.4.run
执行后会弹出安装界面,我们点击next”即可,然后会提示需要登录用户账号,输入账号和密
码即可继续进行安装。
15-7 安装 QT Creator IDE
安装完毕后,我们运行 QT Creator IDE,如果没有安装错误,此时的界面如下:
15-8 QT Creator IDE 界面
下面我们需要设置交叉工具链,因为我们是为 ARM 开发板进行应用开发的。点击“工具”然后
选择“选项”,此时会弹出 kit 工具设置。如下图所示。
linux 驱动开发指南 | 李山文
301
15-9 设置 Kit 工具
现在我们设置 C 编译器,点击最右侧的“添加”,然后选择“GCC”然后点击 C,此时会出现编
译选项,如下图所示:
14-10 添加 C 编译器
15-11 设置 C 叉工具链路径
linux 驱动开发指南 | 李山文
302
我们将名称修改为GCC(ARM)然后设置 C 工具链的路径。同理,我们以同样的方式设置 C++
工具链位置,如下图所示:
15-12 设置 C++交叉工具链位置
设置完毕后点击“Apply,然后我们需要设置 QT Versions,实际上是指定 qmake 的位置,现在
我们将 Buildroot 编译生成的 qmake 复制到我们的交叉工具链里面,执行如下:
sudo cp -a qmake /usr/local/arm-gcc-app
14-13 buildroot 中的 output/build/qt5base-5.9.6 目录下的 qmake 文件夹复制到交叉工具链目录
现在,我们点击 QT Versions 栏,然后选择刚才复制的 qmke 即可。
15-14 选择 qmake
此时会出现一个 QT 可用的版本,如下图所示,需要注意的是,我们最好不要直接添加 Buildroot 路径
中的 qmake,这样会使 buildroot 出现一些莫名其妙的问题。
linux 驱动开发指南 | 李山文
303
15-15 添加成功的 QT 版本
接下来我们配置“构建套件(Kit,点击“构建套件(Kit”栏,然后点击“添加”。此时会出
现配置界面,现在我们设置如下图所示:
15-16 配置构建套件
配置完毕后,我们点击“OK”按钮,退出选项设置,现在我们就可以开始开发我们的应用程序
了,首先点击新建项目,然后一路默认,最后选择 kits 时需要选择 ARM,然后点击“完成”
15-17 新建项目时选择 ARM 构建套件
此时我们就可以正常编写我们的应用程序了,然后点击“构建项目”,项目会正常构建完成,但
当我们点击运行的时候会出现如下图所示错误,这种错误是因为我们的可执行文件必须在开发板上运
行,不能在我们的主机上运行。
linux 驱动开发指南 | 李山文
304
15-18 运行是报错是因为主机不能运行 ARM 应用程序
在我们的项目同级目录下会生成一个 build-xxx-ARM-Debug 文件夹,该文件夹存放的就是我
们构建编译后生成的可执行文件。
15-19 生成的可执行文件
我们将该执行文件放到开发板的根目录下,执行如下命令:
./untitled -platform linuxfb
此时可以看到屏幕上出现我们的 QT 应用程序界面,但是会出现一个问题,发现界面程序没有文
字,而且终端提示如下:
15-20 提示没有找到字库报错
可以看到,提示/usr/lib/fonts 没有字库,因此我们需要新建字库,这个实际上很简单,我们将.ttf
字库文件放到该目录下就可以了。但是我们在哪找字库呢?字库太容易了,我们的 windows 操作系统
就有很多字库,主机 Linux 系统中也有大量的字库文件,我们只需要将这个字库文件放到/usr/lib/fonts
目录下就可以了。
实际上在 Buildroot 工具中,也可以直接添加字库, buildroot 配置界面中,找到 Target packages
选项中的 Fonts, cursors, icons, sounds and themes
linux 驱动开发指南 | 李山文
305
15-21 buildroot 勾选字库编译到根文件系统中
保存退出图形配置界面,执行 make 编译根文件系统,编译完毕后我们将替换之前的根文件系统,
但是 buildroot 的字库默认安装到/usr/share/fonts 目录下,因此我们还需要将/usr/share/fonts 下面的所
.ttf 文件搬移到/usr/lib/fonts 目录下(如果/usr/lib/fonts 目录不存在,则手动创建)。此时我们再次执
行我们的 QT 应用程序,现在就不会有错误了,液晶屏上也会显示出应用界面,文字也出来了。
15-22 QT Creator 开发的应用程序效
至此,QT 的应用开发部分讲解完毕。
linux 驱动开发指南 | 李山文
306
该卷将全面讲解块设备驱动,包括块设备的基本概
念、驱动框架,以及目前手机主流的块设备驱动。主要
围绕 SCSI 子系统来讲解,将会以 UFS 驱动作为主要内
容。
linux 驱动开发指南 | 李山文
307
附录
A. Linux 键盘键值对照表(部分)
53
编码
键值
编码
键值
编码
键值
0
保留
41
`
82
P 0
1
ESC
42
Shift(左)
83
P .
2
1
43
\
84
未定义
3
2
44
Z
85
半角/全角
4
3
45
X
86
5
4
46
C
87
F11
6
5
47
V
88
F12
7
6
48
B
89
片假名(日)
8
7
49
N
90
平假名(日)
9
8
50
M
91
10
9
51
,
92
11
0
52
.
93
片假名/平假名
12
-
53
/
94
13
/
54
Shift(右)
95
14
Backspace
55
Alt(左)
96
15
Tab
56
97
Ctrl(右)
16
Q
57
Space
98
P /
17
W
58
CapsLk
99
PrtSc
18
E
59
F1
100
Alt(右)
19
R
60
F2
101
换行
20
T
61
F3
102
Home
21
Y
62
F4
103
22
U
63
F5
104
PageUp
23
I
64
F6
105
24
O
65
F7
106
25
P
66
F8
107
End
26
(
67
F9
108
27
)
68
F10
109
PageDown
28
Enter
69
NumLock
110
Insert
29
Ctrl(左)
70
Scroll Lock
111
Delete
30
A
71
P 7
112
Macro
31
S
72
P 8
113
32
D
73
P 9
114
33
F
74
P -
115
34
G
75
P 4
116
35
H
76
P 5
117
P-
36
J
77
P 6
118
P±
37
K
78
P +
119
53
参考 include\uapi\linux\input-event-codes.h
linux 驱动开发指南 | 李山文
308
38
L
79
P 1
120
39
;
80
P 2
121
P ,
40
81
P 3
122
注:P 1 表示数字键盘 1,类推其他。
B. Linux fdisk 令分区格式编码
0 24 NEC DOS 81 Minix / Linu bf Solaris
1 FAT12 27 Hidden NTFS Win 82 Linux 交换 / So c1 DRDOS/sec (FAT-
2 XENIX root 39 Plan 9 83 Linux c4 DRDOS/sec (FAT-
3 XENIX usr 3c PartitionMagic 84 OS/2 hidden or c6 DRDOS/sec (FAT-
4 FAT16 <32M 40 Venix 80286 85 Linux 扩展 c7 Syrinx
5 扩展 41 PPC PReP Boot 86 NTFS 卷集 da 非文件系统数据
6 FAT16 42 SFS 87 NTFS 卷集 db CP/M / CTOS / .
7 HPFS/NTFS/exFAT 4d QNX4.x 88 Linux 纯文本 de Dell 工具
8 AIX 4e QNX4.x 2 部分 8e Linux LVM df BootIt
9 AIX 可启动 4f QNX4.x 3 部分 93 Amoeba e1 DOS 访问
a OS/2 启动管理器 50 OnTrack DM 94 Amoeba BBT e3 DOS R/O
b W95 FAT32 51 OnTrack DM6 Aux 9f BSD/OS e4 SpeedStor
c W95 FAT32 (LBA) 52 CP/M a0 IBM Thinkpad ea Rufus alignment
e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a5 FreeBSD eb BeOS fs
f W95 扩展 (LBA) 54 OnTrackDM6 a6 OpenBSD ee GPT
10 OPUS 55 EZ-Drive a7 NeXTSTEP ef EFI (FAT-12/16/
11 隐藏的 FAT12 56 Golden Bow a8 Darwin UFS f0 Linux/PA-RISC
12 Compaq 诊断 5c Priam Edisk a9 NetBSD f1 SpeedStor
14 隐藏的 FAT16 <3 61 SpeedStor ab Darwin 启动 f4 SpeedStor
16 隐藏的 FAT16 63 GNU HURD or Sys af HFS / HFS+ f2 DOS 次要
17 隐藏的 HPFS/NTF 64 Novell Netware b7 BSDI fs fb VMware VMFS
18 AST 智能睡眠 65 Novell Netware b8 BSDI swap fc VMware VMKCORE
1b 隐藏的 W95 FAT3 70 DiskSecure 多启 bb Boot Wizard fd Linux raid 自动
1c 隐藏的 W95 FAT3 75 PC/IX bc Acronis FAT32 L fe LANstep
1e 隐藏的 W95 FAT1 80 Minix be Solaris 启动 ff BBT