ARM Linux 驱动开发篇
本文将详细解释 Linux 中的三类驱动:字符设备驱动、块设备驱动和网络设备驱动。 字符设备最多,从最简单的点灯到 I2C、SPI、音频属于字符设备驱动的类型。 块设备驱动是存储设备的驱动,如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是基于存储块,所以它们被称为块设备。 网络驱动,无论是有线还是无线,都属于网络设备驱动的范畴 块设备和网络设备的驱动比字符设备更复杂,因为它们很复杂,所以半导体制造商通常为我们准备,在大多数情况下可以直接使用。
一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 界面,所以属于字符设备,但又可以上网,所以也属于网络设备驱动。
设备树将是本文的重点!
字符设备驱动开发
字符设备驱动简介
字符设备是按字节流读写的设备,读写数据按顺序进行。
Linux 驱动属于核心的一部分,因此驱动在核心空间中运行。 当我们想在用户空间实现内核的操作时,比如使用 open 函数打开/dev/led 这种驱动,因为用户空间不能直接操作核心,所以必须使用一种叫做系统呼叫的方法来实现从用户空间陷入到核心空间,从而实现底部驱动的操作
每个系统调用,在驱动中都有相应的驱动函数, 在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 这个结构体就是结构体 Linux 核驱动操作函数集合 简单介绍一下 file_operation 结构中更重要、更常用的函数: 第 1589 行,owner 该结构模块的指针一般设置为 THIS_MODULE。 第 1590 行,llseek 函数用于修改文件当前的读写位置。 第 1591 行,read 读取设备文件时使用函数。 第 1592 行,write 函数用于将(发送)数据写入设备文件。 第 1596 行,poll 这是一个轮询函数,用于查询设备是否可以非阻塞读写。 第 1597 行,unlocked_ioctl 在应用程序中,函数为设备提供控制功能 ioctl 函数对应。 第 1598 行,compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上 第 1599 行,mmap 函数用于将设备的内存映射到过程空间(即用户空间) 第 1601 行,open 用于打开设备文件的函数。 第 1603 行,release 在应用程序中,函数用于释放(关闭)设备文件 close 函数对应。 第 1604 行,fasync 该函数用于刷新待处理的数据,并将缓冲区的数据刷新到磁盘中。 第 1605 行,aio_fsync 函数与 fasync 函数功能相似。 aio_fsync 是异步刷新
在字符设备驱动开发中最常用的就是上面这些函数。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、release、write、read 等等都需要实现
字符设备驱动开发步骤
相应的外设寄存器初始化。驱动器需要按照规定的框架编写。
Linux 驱动有两种操作方式 第一种是将驱动程序编译成 Linux 内核,这样做 Linux 当内核启动时,驱动程序将自动运行 第二种是将驱动程序编译成模块(Linux 以下模块扩展为名.ko),在Linux 内核启动后使用insmod命令加载驱动模块。 这样,我们只需要在修改驱动程序后编译驱动代码,而不需要编译整个代码 Linux 代码。
模块有两种操作:加载和卸载。我们在编写驱动程序时需要注册这两个操作函数
module_init(xxx_init); //注册模块加载函数 module_exit(xxx_exit); ///注册模块卸载函数
当使用“insmod命令加载驱动时,xxx_init 调用此函数 “rmmod命令卸载具体驱动时 xxx_exit 调用函数。
modprobe 命令主要提供模块依赖分析、错误检查、错误报告等功能 modprobe 默认命令去/lib/modules/目录中查找模块。需要自己手动创建 “modprobe -r命令卸载驱动
对于字符设备驱动,当驱动模块加载成功时,需要注册字符设备
tatic inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops) static inline void unregister_chrdev(unsigned int major, const char *name)
major:主设备号,Linux 每台设备都有一个设备号,设备号分为主设备号和次设备号两部分,以后会详细说明设备号。 name:设备名称,指向一串字符串。 fops:结构体 file_operations 类型指针,指向设备的操作函数集变量
需要注意的是,选择未使用的主设备号,输入命令cat /proc/devices可查看已使用的设备号
第一列是主设备号
file_operations 结构体是设备的具体操作函数
static struct fileoperations test_fops = {
owner = THIS_MODULE,
open = chrtest_open,
read = chrtest_read,
write = chrtest_write,
release = chrtest_release,
};
MODULE_LICENSE() //添加模块 LICENSE 信息 MODULE_AUTHOR() //添加模块作者信息
设备号
设备号分为主设备号和次设备号 主设备号标识某一个具体的驱动,次设备号表示使用这个驱动的各个设备 Linux提供了dev_t的数据类型表示设备号,是32位的数据类型。分成了主次两部分 其中高12位为主设备,低20位为次设备
设备号的分配
1.静态分配设备号
前面讲过,注册字符设备的时候需要指定一个设备号
。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号
2.动态分配设备号 Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突 用于申请设备号的函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:保存申请到的设备号。 baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。 count:要申请的设备号数量。 name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
地址映射
MMU是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在Linux 内核已经支持无 MMU 的处理器了 ①、完成虚拟空间到物理空间的映射。 ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。 对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB, 我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间 Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。 如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能 现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址
用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
phys_addr:要映射给的物理起始地址。 size:要映射的内存空间大小。
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射
iounmap(SW_MUX_GPIO1_IO03);
I/O 内存访问函数
两个概念:I/O 端口和 I/O 内存 当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。 当外部寄存器或内存映射到内存空间时,称为 I/O 内存。 对于 ARM 来说没有 I/O 空间,所以ARM体系下只有I/O内存
推荐使用一组操作函数来对映射后的内存进行读写操作。
1 u8 readb(const volatile void __iomem *addr)
2 u16 readw(const volatile void __iomem *addr)
3 u32 readl(const volatile void __iomem *addr)
1 void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)
b、w 和 l 这三个函数分别对应 8bit、16bit 和 32bit 读操作
新字符设备驱动实验
驱动模块加载成功以后还需要手动使用 mknod 命令创建设备节点。register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。
其实新字符设备驱动也有点老了
两个问题: ①、需要我们事先确定好哪些主设备号没有使用。 ②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
新字符设备驱动下,示例代码如下:
1 int major; /* 主设备号 */
2 int minor; /* 次设备号 */
3 dev_t devid; /* 设备号 */
4
5 if (major) {
/* 定义了主设备号 */
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
7 register_chrdev_region(devid, 1, "test");
8 } else {
/* 没有定义设备号 */
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
10 major = MAJOR(devid); /* 获取分配号的主设备号 */
11 minor = MINOR(devid); /* 获取分配号的次设备号 */
12 }
1、字符设备结构 在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中
1 struct cdev {
2 struct kobject kobj;
3 struct module *owner;
4 const struct file_operations *ops;
5 struct list_head list;
6 dev_t dev;
7 unsigned int count; 8 };
定义好 cdev 变量以后就要使用 函数对其进行初始化 void cdev_init(struct cdev *cdev, const struct file_operations *fops) 参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。 int cdev_add(struct cdev *p, dev_t dev, unsigned count) 参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
卸载驱动的时候一定要使用 函数从 Linux 内核中删除相应的字符设备
自动创建设备节点
当我们使用 modprobe 加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。
mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。例如自动创建设备节点
使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理
由于我没有用busybox构建根目录系统,这里就简单看了遍,没有做笔记了 待更新。。。。。。
设备树
DTS(Device Tree Source),采用树形结构描述开发板上的设备信息。 树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支
在没有设备树的时候,Linux 内核源码中大量的 arch/arm/mach-xxx 和 arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就是对应平台下的板级信息。
随着智能手机的发展,每年新出的 ARM 架构芯片少说都在数十、数百款,Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c 或.h 文件,都会被硬编码进 Linux 内核中,导致 Linux 内核“虚胖”
当 Linux 之父 linus 看到 ARM 社区向 Linux 内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”
从此以后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内核中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts
DTS、DTB 和 DTC
DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。需要用到 DTC 工具 基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢? arch/arm/boot/dts/Makefile,
400 dtb-$(CONFIG_SOC_IMX6ULL) += \
401 imx6ull-14x14-ddr3-arm2.dtb \
402 imx6ull-14x14-ddr3-arm2-adc.dtb \
403 imx6ull-14x14-ddr3-arm2-cs42888.dtb \
404 imx6ull-14x14-ddr3-arm2-ecspi.dtb \
405 imx6ull-14x14-ddr3-arm2-emmc.dtb \
406 imx6ull-14x14-ddr3-arm2-epdc.dtb \
......
如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。
DTS 语法
我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改 关于设备树详 细 的 语 法 规 则 请 参 考 《 Devicetree SpecificationV0.2.pdf 》 和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档
==在.dts 设备树文件中,可以通过“#include”来引用.h、.dtsi 和.dts 文件。==只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点 每个节点都通过一些属性信息来描述节点信息,属性就是键—值对
1 / {
2 aliases {
3 can0 = &flexcan1; 4 };
5
6 cpus {
7 #address-cells = <1>;
8 #size-cells = <0>;
9
10 cpu0: cpu@0 {
11 compatible = "arm,cortex-a7";
12 device_type = "cpu";
13 reg = <0>;
14 };
15 };
16
17 intc: interrupt-controller@00a01000 {
18 compatible = "arm,cortex-a7-gic";
19 #interrupt-cells = <3>;
20 interrupt-controller;
21 reg = <0x00a01000 0x1000>,
22 <0x00a02000 0x100>;
23 };
24 }
第 1 行,“/”是根节点,每个设备树文件只有一个根节点 imx6ull.dtsi和 imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,会合并在一起。
第 2、6 和 17 行,aliases、cpus 和 intc 是三个子节点
cpu0:cpu@0
前面的是节点标签(label),“:”后面的才是节点名字 引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点
在设备树中节点命名格式
node-name@unit-address
node-name”是节点名字。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”
Linux 下的很多外设驱动都会使用这些标准属性 1、compatible 属性 compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性! 用于将设备和驱动绑定起来 字符串列表用于选择设备所要使用的驱动程序
compatible 属性的值格式如下所示:
"manufacturer,model"
其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字 一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值, 如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
2、model 属性 一般 model 属性描述设备模块信息,比如名字什么的
3、status 属性 4、#address-cells 和#size-cells 属性 用于描述子节点的地址信息 #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。 #address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值
reg = <address1 length1 address2 length2 address3 length3……>
5、reg 属性
1
spi4 {
2 compatible = "spi-gpio";
3 #address-cells = <1>;
4 #size-cells = <0>;
5
6
gpio_spi: gpio_spi@0 {
7 compatible = "fairchild,74hc595";
8 reg = <0>;
9
};
10 };
11
12 aips3: aips-bus@02200000 {
13 compatible = "fsl,aips-bus", "simple-bus";
14 #address-cells = <1>;
15 #size-cells = <1>;
16
17
dcp: dcp@02280000 {
18 compatible = "fsl,imx6sl-dcp";
19 reg = <0x02280000 0x4000>;
20
};
21 };
6、ranges 属性 ranges 是一个地址映射/转换表 ranges 属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵
**child-bus-address:**子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。 **parent-bus-address:**父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。 **length:**子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。 如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
7、name 属性 name 属性值为字符串,name 属性用于记录节点名字,name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性。
8、device_type 属性 用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。 此属性只能用于 cpu 节点或者 memory 节点。
每个节点都有 compatible 属性,根节点“/”也不例外
这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊! 这里就要引入另外一个内容,那就是如何向节点追加数据 I.MX6U-ALPHA 开发 板使 用 的设 备 树文 件 为 imx6ull-alientek-emmc.dts , 因 此 我们 需 要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容
1 &i2c1 {
2
/* 要追加或修改的内容 */
3 };
第 1 行,&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的“i2c1:i2c@021a0000”。 第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。
创建小型模板设备树
本节我们就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。 在实际产品开发中,一般都是使用 SOC 厂商提供好的.dts 文件,我们只需要在上面根据自己的实际情况做相应的修改即可。
首先,搭建一个仅含有根节点“/”的基础的框架。 根节点里面只有一个 compatible 属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
一般开头要加 /dev-1/ 和头文件.h,.dtsi
1、添加 cpus 节点 只有一个 CPU,因此只有一个cpu0 节点 2、添加 soc 节点 像 uart,iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点来管理这些 SOC 内部外设的子节点 3、添加 ocram 节点 ocram 是 I.MX6ULL 内部 RAM,因此 ocram 节点应该是 soc 节点的子节点。 …
设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹 根节点属性属性表现为一个个的文件。文件内容就是他们的值 各个文件夹(图中粗字体文件夹)就是根节点“/”的各个子节点
特殊节点
在根节点“/”中有两个特殊的子节点:aliases 和 chosen 单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名。跟label一样的作用。
18 aliases {
19 can0 = &flexcan1;
20 can1 = &flexcan2;
......};
chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少
18 chosen {
19
stdout-path = &uart1;
20 };
chosen 节点仅仅设置了属性“stdout-path”。但当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性,他就是uboot创建的,内容就是bootargs
Linux 内核解析 DTB 文件
绑定信息文档
设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢? 在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings
有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这 个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。
设备树常用 OF 操作函数
Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。
获取这个设备的其他属性信息,必须先获取到这个设备的节点。
Linux 内核使用 device_node 结构体来描述一个节点
49 struct device_node {
50 const char *name; /* 节点名字 */
51 const char *type; /* 设备类型 */
52 phandle phandle;
53 const char *full_name; /* 节点全名 */
54 struct fwnode_handle fwnode;
55
56 struct property *properties; /* 属性 */
57 struct property *deadprops; /* removed 属性 */
58 struct device_node *parent; /* 父节点 */
59 struct device_node *child; /* 子节点 */
60 struct device_node *sibling;
61 struct kobject kobj;
62 unsigned long _flags;
63 void *data;
64 #if defined(CONFIG_SPARC)
65 const char *path_component_name;
66 unsigned int unique_id;
67 struct of_irq_controller *irq_trans;
68 #endif
69 };
1、of_find_node_by_name 函数 节点名字查找指定的节点
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 name:要查找的节点名字。
2、of_find_node_by_type 函数 通过 device_type 属性查找指定的节点
3、of_find_compatible_node 函数 数根据 device_type 和 compatible 这两个属性查找指定的节点,
4、of_find_matching_node_and_match 函数 通过 of_device_id 匹配表来查找指定的节点
struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)
函数参数和返回值含义如下: from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。 match:找到的匹配的 of_device_id。 返回值:找到的节点,如果为 NULL 表示查找失败
5、of_find_node_by_path 函数 通过路径来查找指定的节点
1、of_get_parent 函数 of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话)
2、of_get_next_child 函数 of_get_next_child 函数用迭代的方式查找子节点
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
node:父节点。 prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。
结构体 property 表示属性
35 struct property {
36 char *name; /* 属性名字 */
37 int length; /* 属性长度 */
38 void *value; /* 属性值 */
39 struct property *next; /* 下一个属性 */
40 unsigned long _flags;
41 unsigned int unique_id;
42 struct bin_attribute attr;
43 };
1、of_find_property 函数 of_find_property 函数用于查找指定的属性
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
np:设备节点。 name: 属性名字。 lenp:属性值的字节数
2、of_property_count_elems_of_size 函数 of_property_count_elems_of_size 函数用于获取属性中元素的数量,例如数组的大小
3、of_property_read_u32_index 函数 从属性中获取指定标号的 u32 类型数据值(无符号 32 位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值
4、 of_property_read_u8_array 函数 of_property_read_u16_array 函数 of_property_read_u32_array 函数 of_property_read_u64_array 函数 这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据, 比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据
5、of_property_read_u8 函数 of_property_read_u16 函数 of_property_read_u32 函数 of_property_read_u64 函数 有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性
6、of_property_read_string 函数 of_property_read_string 函数用于读取属性中字符串值
7、of_n_addr_cells 函数 of_n_addr_cells 函数用于获取#address-cells 属性值
8、of_n_size_cells 函数 of_size_cells 函数用于获取#size-cells 属性值
1、of_device_is_compatible 函数 of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字 符串,也就是检查设备节点的兼容性
2、of_get_address 函数 of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性
3、of_translate_address 函数 of_translate_address 函数负责将从设备树读取到的地址转换为物理地址
4、of_address_to_resource 函数 本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型
IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用 resource 结构体来描述一段内存空间 用 resource结构体描述的都是设备资源信息
18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 };
对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型
5、of_iomap 函数 of_iomap 函数用于直接内存映射 采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址
pinctrl 和 gpio 子系统实验
我们编写了基于设备树的 LED 驱动,但是驱动的本质还是没变,都是配置 LED 灯所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别 像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班
Linux 内核提供了 pinctrl 和 gpio 子系统用于GPIO 驱动
pinctrl 子系统
pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物
我们先来回顾一下上一章是怎么初始化 LED 灯所使用的 GPIO,步骤如下: ①、修改设备树,添加相应的节点,节点里面重点是设置 reg 属性,reg 属性包括了 GPIO 相关寄存器。 ② 、 获 取 reg 属 性 中 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这 两个寄存器用于设置 GPIO1_IO03 这个 PIN 的复用功能、上下拉、速度等。 ③、在②里面将 GPIO1_IO03 这个 PIN 复用为了 GPIO 功能,因此需要设置 GPIO1_IO03 这个 GPIO 相关的寄存器,也就是 GPIO1_DR 和 GPIO1_GDIR 这两个寄存器。
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置 方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题
pinctrl 子系统主要工作内容如下: ①、获取设备树中 pin 信息。 ②、根据获取到的 pin 信息来设置 pin 的复用功能 ③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
一般会在设备树里面创建一个节点来描述 PIN 的配置信息。打开 imx6ull.dtsi 文件,找到一个叫做 iomuxc 的节点 imx6ull.dtsi文件
756 iomuxc: iomuxc@020e0000 {
757 compatible = "fsl,imx6ul-iomuxc";
758 reg = <0x020e0000 0x4000>;
}
imx6ull-alientek-emmc.dts文件
311 &iomuxc {
312 pinctrl-names = "default";
313 pinctrl-0 = <&