Linux学习笔记 驱动开发篇

ARM Linux 驱动开发篇

本篇我们将会详细讲解 Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。
字符设备最多,从最简单的点灯到 I2C、SPI、音频等都属于字符设备驱动的类型。
块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。
网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴
块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。

一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

设备树将是本篇的重点!

字符设备驱动开发

字符设备驱动简介

字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。
Linux学习笔记 驱动开发篇_第1张图片

Linux 驱动属于内核的一部分,因此驱动运行于内核空间。
当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作
Linux学习笔记 驱动开发篇_第2张图片

每一个系统调用,在驱动中都有与之对应的一个驱动函数,
在 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 等都是需要实现的

字符设备驱动开发步骤

初始化相应的外设寄存器。我们需要按照其规定的框架来编写驱动。

1.驱动模块的加载和卸载
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”命令卸载驱动

2.字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备

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”可以查看当前已经被使用掉的设备号

Linux学习笔记 驱动开发篇_第3张图片
第一列就是主设备号

3.实现设备的具体操作函数
file_operations 结构体就是设备的具体操作函数

static struct file_operations test_fops = {
owner = THIS_MODULE, 
open = chrtest_open,
read = chrtest_read,
write = chrtest_write,
release = chrtest_release,
};

4.添加 LICENSE 和作者信息
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学习笔记 驱动开发篇_第4张图片
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址
比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。
如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能
现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址

1、ioremap 函数
用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在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:要映射的内存空间大小。

2、iounmap 函数
卸载驱动的时候需要使用 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 变量以后就要使用 cdev_init 函数对其进行初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合

cdev_add 函数
首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。

卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

自动创建设备节点

当我们使用 modprobe 加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。

mdev 机制

udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。例如自动创建设备节点

使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev
mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理

由于我没有用busybox构建根目录系统,这里就简单看了遍,没有做笔记了
待更新。。。。。。

设备树

DTS(Device Tree Source),采用树形结构描述开发板上的设备信息。
Linux学习笔记 驱动开发篇_第5张图片
树的主干就是系统总线,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》这两份文档

dtsi 头文件
==在.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 属性
Linux学习笔记 驱动开发篇_第6张图片
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 属性
每个节点都有 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 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名。跟label一样的作用。

18 aliases {
19 can0 = &flexcan1;
20 can1 = &flexcan2;
......};

chosen 子节点
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学习笔记 驱动开发篇_第7张图片

绑定信息文档

设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?
在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings

有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这
个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。

设备树常用 OF 操作函数

Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 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 函数
通过路径来查找指定的节点

查找父/子节点的 OF 函数
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:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。

提取属性值的 OF 函数
结构体 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 属性值

其他常用的 OF 函数
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 的电气特性,比如上/下拉、速度、驱动能力等。

1、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 = <&pinctrl_hog_1>;
314 imx6ul-evk {
315 pinctrl_hog_1: hoggrp-1 {
316 fsl,pins = <
317 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
318 MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
319 MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
320 MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
321 >;
322 };
......
371 pinctrl_flexcan1: flexcan1grp{
372 fsl,pins = <
373 MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020
374 MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020
375 >;
376 };
......
587 pinctrl_wdog: wdoggrp {
588 fsl,pins = <
589 MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY 0x30b0
590 >;
591 };
592 };
593 };

设备树中添加 pinctrl 节点模板
这里我们虚拟一个名为“test”的设备,test 使用了 GPIO1_IO00 这个 PIN 的 GPIO 功能
1、创建对应的节点
在 iomuxc 节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”

1 pinctrl_test: testgrp { 
2 /* 具体的 PIN 信息 */
3 };

2、添加“fsl,pins”属性,添加 PIN 配置信息
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl,pins”

1 pinctrl_test: testgrp { 
2 fsl,pins = < 
3 MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config 
/*config 是具体设置值*/
4 >;
5 };

gpio 子系统

pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性
如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了

初始化 GPIO 并且提供相应的 API 函数
比如设置 GPIO为输入输出,读取 GPIO 的值等。

1、设备树中的 gpio 信息
属性值一共有三个,
“&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组
“19”表示 GPIO1 组的第 19 号 IO
“GPIO_ACTIVE_LOW”表示低电平有效

与 gpio 相关的 OF 函数
1、of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息

2、of_gpio_count 函数
和 of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的 GPIO 数量

3、of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号,
此函数在驱动中使用很频繁

程序编写
修改设备树文件
1、添加 pinctrl 节点
在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点

1 pinctrl_led: ledgrp { 
2 fsl,pins = < 
3 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
4 >;
5 };

2、添加 LED 设备节点
在根节点“/”下创建 LED 灯节点,节点名为“gpioled”

1 gpioled { 
2 #address-cells = <1>;
3 #size-cells = <1>;
4 compatible = "atkalpha-gpioled"; 
5 pinctrl-names = "default"; 
6 pinctrl-0 = <&pinctrl_led>;
7 led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
8 status = "okay"; 9 };

3、检查 PIN 是否被其他外设使用
这一点非常重要!!!
检查 PIN 有没有被其他外设使用包括两个方面:
①、检查 pinctrl 设置。
②、如果这个 PIN 配置为 GPIO 的话,检查这个 GPIO 有没有被别的外设使用。

Linux 并发与竞争

Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源
在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问

并发与竞争

Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱,可能会导致系统崩溃

现在的 Linux 系统并发产生的原因很复杂
①、多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
②、抢占式并发访问,从 2.6 版本内核开始,Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
④、SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。

临界区就是共享数据段
临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的
这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分

我们要保护的是多个线程都会访问的共享数据

原子操作

原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作
Linux学习笔记 驱动开发篇_第8张图片
假设现在线程 A要向 a 变量写入 10 这个值,而线程 B 也要向 a 变量写入 20 这个值。可能会出现这样的现象
Linux学习笔记 驱动开发篇_第9张图片

要解决这个问题就要保证三行汇编指令作为一个整体运行,也就是作为一个原子存在

Linux 内核提供了一组原子操作 API 函数来完成此功能,Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的

整型变量
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量

175 typedef struct {
176 int counter;
177 } atomic_t;

atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

可以通过宏 ATOMIC_INIT 向原子变量赋初值。
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux 内核提供了大量的原子操作 API 函数
Linux学习笔记 驱动开发篇_第10张图片
如果使用 64 位的 SOC 的话,就要用到 64 位的原子变量,Linux 内核也定义了 64 位原子结构体。相应的也提供了 64 位原子变量的操作 API 函数

typedef struct {
 long long counter; } atomic64_t;

原子位操作
原子位操作是直接对内存进行操作

自旋锁

举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性

到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁
Linux学习笔记 驱动开发篇_第11张图片

如果自旋锁中此时中断也要插一脚,那就会容易产生死锁
所以在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生
Linux学习笔记 驱动开发篇_第12张图片
Linux学习笔记 驱动开发篇_第13张图片
使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。

后面那些读写自旋锁,顺序锁就不说了

信号量

信号量是同步的一种方式。
使用信号量会提高处理器的使用效率
但是,信号量的开销要比自旋锁大,切换线程就会有开销
①、因为信号量可以使等待资源线程进入休眠状态==,因此适用于那些占用资源比较久的场合。==
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
Linux学习笔记 驱动开发篇_第14张图片

互斥体

虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。
Linux学习笔记 驱动开发篇_第15张图片

Linux 内核定时器

Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。
硬件定时器提供时钟源,时钟源的频率可以设置设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率)

高节拍率和低节拍率的优缺点:
①、高节拍率会提高系统时间精度
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担

在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。

先声明一个结构体,这个结构体是超时时间。然后初始化就可以操作了

Linux 中断实验

Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便

1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断

上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可
以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部
去执行,这样中断处理函数就会快进快出。

参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。

下半部机制。
1.软中断
每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同
软中断必须在编译的时候静态注册!

2、tasklet
tasklet 是利用软中断来实现的另外一种下半部机制建议大家使用 tasklet

3、工作队列
工作队列将要推后的工作交给一个内核线程去执行
Linux 内核使用 work_struct 结构体表示一个工作

struct work_struct {
 atomic_long_t data; 
 struct list_head entry;
 work_func_t func; /* 工作队列处理函数 */
};

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作

struct workqueue_struct {
 struct list_head pwqs; 
 struct list_head list; 
 struct mutex mutex; 
 int work_color;
 int flush_color; 
 atomic_t nr_pwqs_to_flush;
 struct wq_flusher *first_flusher;
 struct list_head flusher_queue; 
 struct list_head flusher_overflow;
 struct list_head maydays; 
 struct worker *rescuer; 
 int nr_drainers; 
 int saved_max_active;
 struct workqueue_attrs *unbound_attrs;
 struct pool_workqueue *dfl_pwq; 
 char name[WQ_NAME_LEN];
 struct rcu_head rcu;
 unsigned int flags ____cacheline_aligned;
 struct pool_workqueue __percpu *cpu_pwqs;
 struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux 内核使用worker 结构体表示工作者线程。worker 结构体表示工作者线程

struct worker {
 union {
 struct list_head entry; 
 struct hlist_node hentry;
 };
 struct work_struct *current_work; 
 work_func_t current_func; 
 struct pool_workqueue *current_pwq;
 bool desc_valid;
 struct list_head scheduled; 
 struct task_struct *task; 
 struct worker_pool *pool; 
 struct list_head node; 
 unsigned long last_active; 
 unsigned int flags; 
 int id; 
 char desc[WORKER_DESC_LEN];
 struct workqueue_struct *rescue_wq;
};

在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。

如果使用设备树中断的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。

Linux 阻塞和非阻塞 IO

Linux学习笔记 驱动开发篇_第16张图片
Linux学习笔记 驱动开发篇_第17张图片

示例代码 52.1.1.1 应用程序阻塞读取数据
1 int fd; 
2 int data = 0; 
3
4 fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */
示例代码 52.1.1.2 应用程序非阻塞读取数据
1 int fd; 
2 int data = 0; 
3
4 fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */

等待队列

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。

Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作
1.创建并初始化一个等待队列头void init_waitqueue_head(wait_queue_head_t *q)

2.等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
创建并初始化一个等待队列项

3、将队列项添加/移除等待队列头

void add_wait_queue(wait_queue_head_t *q, 
 wait_queue_t *wait)
 
void remove_wait_queue(wait_queue_head_t *q, 
wait_queue_t *wait)

4、等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程

void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)

5、等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程

轮询

poll、epoll 和 select 可以用于处理轮询

poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制
传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。

异步通知

之前都是应用程序主动读取。最好的方式就是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序在从驱动程序中读取或写入数据

信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。

驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了

驱动程序发送信号的步骤
1、首先我们需要在驱动程序中定义一个 fasync_struct 结构体指针变量,一般将 fasync_struct 结构体指针变量定义到驱动程序里的设备结构体中。
2、需要在设备驱动中实现 file_operations 操作集中的 fasync 函数

应用程序对异步通知的处理
1、注册信号处理函数
应用程序使用 signal 函数来设置信号的处理函数。
2、将本应用程序的进程号告诉给内核
使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。
3、开启异步通知

flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

platform 设备驱动实验

前面都是对IO进行最简单的读写操作。像I2C、 SPI、LCD 等这些复杂外设的驱动就不能这么去写了

Linux 系统要考虑到驱动的可重用性,因此提出了驱动的分离与分层这样的软件思路
Linux学习笔记 驱动开发篇_第18张图片
实际的 I2C 驱动设备肯定有很多种,不止 MPU6050 这一个
这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来
在实际的驱动开发中,相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可
Linux学习笔记 驱动开发篇_第19张图片
总线就是驱动和设备信息的月老,负责给两者牵线搭桥

当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。

驱动的分层

分层的目的也是为了在不同的层处理不同的内容。

platform 平台驱动模型简介

但是在 SOC 中有些外设是没有总线这个概念的,但是又要使用总线、驱动和设备模型该怎么办呢?
为了解决此问题,Linux 提出了 platform 这个虚拟总线,相应的就有platform_driver 和 platform_device。

Linux系统内核使用bus_type结构体表示总线,里面有个成员match
总线就是使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。match 函数有两个参数:dev 和 drv,这两个参数分别为 device 和 device_driver 类型,也就是设备和驱动。

platform 总线是 bus_type 的一个具体实例

Linux MISC 驱动实验

misc 的意思是混合、杂项的,因此 MISC 驱动也叫做杂项驱动
MISC 驱动其实就是最简单的字符设备驱动,通常嵌套在 platform 总线驱动中,实现复杂的驱动

所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号
MISC 设备会自动创建 cdev,不需要像我们以前那样手动创建,因此采用 MISC 设备驱动可以简化字符设备驱动的编写。

我们需要向 Linux 注册一个 miscdevice 设备。设置 minor、name 和 fops 这三个成员变量。
minor 表示子设备号,MISC 设备的主设备号为 10,这个是固定的,需要用户指定子设备号,Linux 系统已经预定义了一些 MISC 设备的子设备号

示例代码 57.1.2 预定义的 MISC 设备子设备号
13 #define PSMOUSE_MINOR 1
14 #define MS_BUSMOUSE_MINOR 2 /* unused */
15 #define ATIXL_BUSMOUSE_MINOR 3 /* unused */
16 /*#define AMIGAMOUSE_MINOR 4 FIXME OBSOLETE */
17 #define ATARIMOUSE_MINOR 5 /* unused */
18 #define SUN_MOUSE_MINOR 6 /* unused */
......
52 #define MISC_DYNAMIC_MINOR 255

接下来我们就使用 platform 加 MISC 驱动框架

Linux INPUT 子系统实验

按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux 内核为此专门做了一个叫做 input子系统的框架来处理输入事件。
输入设备本质上还是字符设备,只是在此基础上套上了 input 框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input 核心层负责处理这些事件。
Linux学习笔记 驱动开发篇_第20张图片
input 核心层会向 Linux 内核注册一个字符设备
input 子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了

1、注册 input_dev
在使用 input 子系统的时候我们只需要注册一个 input 设备即可

2、上报输入事件
首先是 input_event 函数,此函数用于上报指定的事件以及对应的值

LCD驱动

裸机 LCD 驱动编写流程如下:
①、初始化 I.MX6U 的 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、hspw、hbp、hfp、vspw、vbp 和 vfp 等信息。
②、初始化 LCD 像素时钟。
③、设置 RGBLCD 显存。
④、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。

但是在 Linux 系统中内存的管理很严格,显存是需要申请的,不是你想用就能用的。而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存要是同一片物理内存。

在以后的 Linux 学习中见到“Framebuffer”或者“fb”的话第一反应应该想到 RGBLCD或者显示设备。

fb 是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一个 fb 设备。通过framebuffer机制将底层的LCD抽象为/dev/fbX,X=0、1、2…,应用程序可以通过操作/dev/fbX来操作屏幕。

日了,昨晚写的笔记没保存,直接跳到块设备算了,以后回来再填坑

Linux 块设备驱动

块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统

什么是块设备?
块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这样就减少了对块设备的擦除次数,提高了块设备寿命

块设备驱动框架

block_device 结构体
linux 内核使用 block_device 表示块设备
内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk

1、注册块设备
int register_blkdev(unsigned int major, const char *name)

2、注销块设备
void unregister_blkdev(unsigned int major, const char *name)

gendisk 结构体
第 5 行,major 为磁盘设备的主设备号。
第 6 行,first_minor 为磁盘的第一个次设备号。
第 7 行,minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一
样,次设备号不同。
第 21 行,part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型,disk_part_tbl 的核心
是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。
第 24 行,fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作
集 file_operations 一样,是块设备驱动中的重点!
第 25 行,queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱
动程序需要处理此队列中的所有请求。

1、申请 gendisk
struct gendisk *alloc_disk(int minors)
2、删除 gendisk
void del_gendisk(struct gendisk *gp)
3、将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk
void add_disk(struct gendisk *disk)
4、设置 gendisk 容量
void set_capacity(struct gendisk *disk, sector_t size)

5、调整 gendisk 引用计数
truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数

block_device_operations 结构体
块设备也有操作集

块设备 I/O 请求过程
大家如果仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?

1、请求队列 request_queue
内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。
每个磁盘(gendisk)都要分配一个 request_queue。

①、初始化请求队列
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
②、删除请求队列
void blk_cleanup_queue(struct request_queue *q)
③、分配请求队列并绑定制造请求函数
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

2、请求 request
真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

①、获取请求
request *blk_peek_request(struct request_queue *q)
②、开启请求
void blk_start_request(struct request *req)
③、一步到位处理请求
我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启
Linux学习笔记 驱动开发篇_第21张图片

请求队列,请求和bio的关系
Linux学习笔记 驱动开发篇_第22张图片

我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。
数据传输三个要求:数据源、数据长度以及数据目的地
那么 bio 就有必要描述清楚这些信息
其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,例如操作的扇区地址
bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度
Linux学习笔记 驱动开发篇_第23张图片
①、遍历请求中的 bio
__rq_for_each_bio

②、遍历 bio 中的所有段
bio_for_each_segment

③、通知 bio 处理结束
bio_endio

Linux 网络驱动

嵌入式网络硬件分为两部分:MAC 和 PHY
1、SOC 内部没有网络 MAC 外设
既然没有内部 MAC,那么可以找个外置的 MAC 芯片啊
有些外置的网络芯片更强大,内部甚至集成了硬件 TCP/IP 协议栈,对外提供一个 SPI 接口。因此单片机就不需要移植负责的软件协议栈,直接通过 SPI来操作
缺点:网速不高,成本贵
Linux学习笔记 驱动开发篇_第24张图片

2、SOC 内部集成网络 MAC 外设
①、内部 MAC 外设会有专用的加速模块,比如专用的 DMA,加速网速数据的处理。
②、网速快,可以支持 10/100/1000M 网速。
③、外接 PHY 可选择性多,成本低。
Linux学习笔记 驱动开发篇_第25张图片
MII/RMII 接口
内部 MAC 通过 MII/RMII 接口来与外部的 PHY 芯片连接,完成网络数据传输

1、MII 接口
MII 全称是 Media Independent Interface,直译过来就是介质独立接口,它是 IEEE-802.3 定
义的以太网标准接口,MII 接口用于以太网 MAC 连接 PHY 芯片
Linux学习笔记 驱动开发篇_第26张图片2、RMII 接口
RMII 全称是 Reduced Media Independent Interface,翻译过来就是精简的介质独立接口,也
就是 MII 接口的精简版本。RMII 接口只需要 7 根数据线,相比 MII 直接减少了 9 根,极大的
方便了板子布线
Linux学习笔记 驱动开发篇_第27张图片

MDIO 接口
MDIO 全称是 Management Data Input/Output,直译过来就是管理数据输入输出接口,是一个简单的两线串行接口,一根 MDIO 数据线,一根 MDC 时钟线。
动程序可以通过 MDIO 和MDC 这两根线访问 PHY 芯片的任意一个寄存器。MDIO 接口支持多达 32 个 PHY。

RJ45 接口
网络设备是通过网线连接起来的,插入网线的叫做 RJ45 座
RJ45 座要与 PHY 芯片连接在一起,但是中间需要一个网络变压器,网络变压器用于隔离以及滤波等,网络变压器也是一个芯片
但是现在很多 RJ45 座子内部已经集成了网络变压器
Linux学习笔记 驱动开发篇_第28张图片ENET 接口简介
ENET 外设其实就是一个网络 MAC,支持 10/100M。实现了三层网络加速,用于加速那些通用的网络协议,比如 IP、TCP、UDP 和 ICMP 等,为客户端应用程序提供加速服务。

PHY 芯片详解

SOC 可以对 PHY 进行配置或者读取PHY 相关状态,这个就需要 PHY 内部寄存器去实现了。
PHY 芯片寄存器地址空间为 5 位,地址 0-31 共 32 个寄存器,IEEE 定义了 0-15 这 16 个寄存器的功能,仅靠这 16 个寄存器是完全可以驱动起 PHY 芯片的 16-31 这 16 个寄存器由厂商自行实现。
事实上在实际开发中可能会遇到一些其他的问题导致 Linux 内核的通用 PHY 驱动工作不正常,这个时候就需要驱动开发人员去调试了。
很多厂商采用分页技术来扩展寄存器地址空间,以求定义更多的寄存器。实现技术

LAN8720A 详解
AN8720A 是低功耗的 10/100M 单以太网 PHY 层芯片
可应用于机顶盒、网络打印机、嵌入式通信设备、IP 电话等领域。
LAN8720A 的主要特点如下:
· 高性能的 10/100M 以太网传输模块
· 支持 RMII 接口以减少引脚数
· 支持全双工和半双工模式
· 两个状态 LED 输出
· 可以使用 25M 晶振以降低成本
· 支持自协商模式
· 支持 HP Auto-MDIX 自动翻转功能
· 支持 SMI 串行管理接口
· 支持 MAC 接口

Linux 内核网络驱动框架

net_device 结构体
Linux 内核使用 net_device 结构体表示一个具体的网络设备,net_device 是整个网络驱动的灵魂
网络驱动的核心就是初始化 net_device 结构体中的各个成员变量,然后将初始化完成以后的 net_device 注册到 Linux 内核中。

1、申请 net_device
编写网络驱动的时候首先要申请 net_device,使用 alloc_netdev 函数来申请 net_device
2、删除 net_device
当 我 们注 销 网络 驱动 的时 候 需要 释 放掉 前面 已经 申 请到 的 net_device ,释 放 函数 为free_netdev
3、注册 net_device
net_device 申请并初始化完成以后就需要向内核注册 net_device,要用到函数 register_netdev
3、注销 net_device
既然有注册,那么必然有注销,注销 net_device 使用函数 unregister_netdev

网络是分层的,对于应用层而言不用关系具体的底层是如何工作的,只需要按照协议将要发送或接收的数据打包好即可。打包好以后都通过 static inline int dev_queue_xmit(struct sk_buff *skb) 函数将数据发送出去,接收数据的话使用 int netif_rx(struct sk_buff *skb) 函数即可

1、分配 sk_buff

static inline struct sk_buff *alloc_skb(unsigned int size,
																				gfp_t priority)

2、释放 sk_buff
当 使 用 完 成 以 后 就 要 释 放 掉 sk_buff , 释 放 函 数 可 以 使 用 kfree_skb
对于网络设备而言最好使用如下所示释放函数:
void dev_kfree_skb (struct sk_buff *skb)

3、skb_put、skb_push、sbk_pull 和 skb_reserve
skb_put 函数,此函数用于在尾部扩展 skb_buff的数据区,也就将 skb_buff 的 tail 后移 n 个字节
Linux学习笔记 驱动开发篇_第29张图片
skb_push 函数用于在头部扩展 skb_buff 的数据区
Linux学习笔记 驱动开发篇_第30张图片
sbk_pull 函数用于从 sk_buff 的数据区起始位置删除数据
Linux学习笔记 驱动开发篇_第31张图片
sbk_reserve 函数用于调整缓冲区的头部大小
skb_buff 的 data 和 tail 同时后移 n 个字节即可

网络 NAPI 处理机制
Linux 里面的网络数据接收也轮询和中断两种
中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的 CPU 处理时间在中断自身处理上
轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的 CPU 处理时间。

NAPI 是一种高效的网络处理技术。
NAPI 的核心思想就是不全部采用中断来读取网络数据,而是==采用中断来唤醒数据接收服务程
序,在接收服务程序中采用 POLL 的方法来轮询处理数据。==这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。

1、初始化 NAPI
首先要初始化一个 napi_struct 实例,使用 netif_napi_add 函数
2、删除 NAPI
如果要删除 NAPI,使用 netif_napi_del 函数即可
3、使能 NAPI
初始化完 NAPI 以后,必须使能才能使用,使用函数 napi_enable
4、关闭 NAPI
关闭 NAPI 使用 napi_disable 函数即可
5、检查 NAPI 是否可以进行调度
使用 napi_schedule_prep 函数检查 NAPI 是否可以进行调度
6、NAPI 调度
如果可以调度的话就进行调度,使用__napi_schedule 函数完成 NAPI 调度
7、NAPI 处理完成
NAPI 处理完成以后需要调用 napi_complete 函数来标记 NAPI 处理完成
Linux学习笔记 驱动开发篇_第32张图片Linux学习笔记 驱动开发篇_第33张图片

Linux 内核 PHY 子系统与 MDIO 总线简析
和 platform 总线一样, PHY 子系统也是一个设备、总线和驱动模型

Linux WIFI 驱动

正点原子 I.MX6U-ALPHA 开发板支持 USB 和 SDIO 这两种接口的 WIFI
因为 realtek 公司提供了 WIFI 驱动源码,因此我们只需要将 WIFI 驱动源码添加到 Linux 内核中,然后通过图形化界面配置,选择将其编译成模块即可。

linux内核自带的驱动不稳定!因此不建议大家使用。最好使用自己板子上的的rtl8192CU 驱动。
在编译之前要先将内核自带的驱动屏蔽掉

wireless tools 工具移植与测试

wireless tools 是操作 WIFI 的工具集合,包括一下工具:
①、iwconfig:设置无线网络相关参数。
②、iwlist:扫描当前无线网络信息,获取 WIFI 热点。
③、iwspy:获取每个节点链接的质量。
④、iwpriv:操作 WirelessExtensions 特定驱动。
⑤、ifrename:基于各种静态标准命名接口

我们最常用的就是 iwlist 和 iwconfig 这两个工具

Linux 4G 通信

很多 4G 模块都是 MiniPCIE 接口的
这些 4G 模块虽然用了 MiniPCIE 接口,但是实际上的通信接口都是 USB,所以 4G 模块的驱动就转换为了 USB 驱动。虽然外形是 MiniPCIE 的,但是内心却是 USB 的。

高新兴 ME3630 4G 模块

ME3630 4G 模组特性如下:
①、一路 USB2.0 接口。
②、一路 UART 接口。
③、SIM 卡接口支持 1.8/3.0V。
④、内置 TCP、UDP、FTP 和 HTTP 等协议。
⑤、支持 RAS/ECM/NDIS。
⑥、支持 AT 指令。

ME3630 4G 模块驱动修改
Linux学习笔记 驱动开发篇_第34张图片在这里插入图片描述Linux学习笔记 驱动开发篇_第35张图片
Linux学习笔记 驱动开发篇_第36张图片Linux学习笔记 驱动开发篇_第37张图片Linux学习笔记 驱动开发篇_第38张图片

RGB 转 HDMI

目前大多数的显示器都提供了 HDMI 接口
但是 I.MX6ULL这颗芯片原生并不支持 HDMI 显示,但是我们可以通过 RGB 转 HDMI 芯片将 RGB 信号转为HDMI 信号,这样就可以连接 HDMI 显示器了

Linux PWM 驱动

设备树下的 PWM 控制器节点
I.MX6ULL 有 8 路 PWM 输出,因此对应 8 个 PWM 控制器
这 8 路 PWM 都属于 I.MX6ULL 的 AIPS-1 域,但是在设备树 imx6ull.dtsi 中分为了两部分,PWM1~PWM4 在一起,PWM5~PWM8 在一起

PWM 子系统
Linux 内核提供了个 PWM 子系统框架,核心是 pwm_chip 结构
Linux学习笔记 驱动开发篇_第39张图片
PWM 子系统驱动的核心初始化 pwm_chip 结构体,然后向内核注册初始化完成以后的pwm_chip

PWM 驱动源码分析
当设备树 PWM 节点的 compatible 属性值为“fsl,imx27-pwm”的话就会匹配此驱动,注意后面的.data 为 imx_pwm_data_v2,这是一个 imx_pwm_data 类型的结构体变量
Linux学习笔记 驱动开发篇_第40张图片
PWM 驱动编写
修改设备树
1、在 iomuxc 节点下添加 GPIO1_IO04 引脚信息
2、在 imx6ull-alientek-emmc.dts文件中向pwm3 节点追加信息
3、屏蔽掉其他复用的 IO

使能 PWM 驱动
Linux学习笔记 驱动开发篇_第41张图片

Regmap API

针对 I2C 和 SPI 设备寄存器的操作都是通过相关的 API 函数进行操作的。这样 Linux 内核中就会充斥着大量的重复、冗余代码
为了方便内核开发人员统一访问 I2C/SPI 设备的时候,为此引入了 Regmap 子系统

什么是 Regmap
Linux学习笔记 驱动开发篇_第42张图片
1、regmap 框架结构
regmap 驱动框架如下图所示:
Linux学习笔记 驱动开发篇_第43张图片
要使用 regmap,肯定要先给驱动分配一个具体的 regmap 结构体实例

Linux IIO 驱动

Linux 内核为了管理这些日益增多的 ADC 类传感器,特地推出了 IIO 子系统
IIO 全称是 Industrial I/O,翻译过来就是工业 I/O,大家不要看到“工业”两个字就觉得 IIO是只用于工业领域的。

当你使用的传感器本质是 ADC 或 DAC 器件的时候,可以优先考虑使用 IIO 驱动框架。

1、iio_dev 结构体
IIO 子 系 统 使 用 结 构 体 iio_dev 来 描 述 一 个 具 体 IIO 设 备
2、iio_dev 申请与释放
3、iio_dev 注册与注销

基础驱动框架建立
Linux学习笔记 驱动开发篇_第44张图片Linux学习笔记 驱动开发篇_第45张图片

IIO 设备的申请、初始化以及注册在 probe 函数中完成,在注销驱动的时候还需要在 remove函数中注销掉 IIO 设备、释放掉申请的一些内存。

你可能感兴趣的:(驱动开发,linux,运维)