字符设备驱动-9-中断子系统-中断设备树表述与解析

1 中断在设备树中的表述#

1.1 中断控制器#

如图:GPIO1 连接到 GIC,GPIO2 连接到 GIC,所以 GPIO1 的父亲是 GIC,GPIO2的父亲是 GIC。假设 GPIO1 有 32 个中断源,但是它把其中的 16 个汇聚起来向 GIC 发出一个中断,把另外 16 个汇聚起来向 GIC 发出另一个中断。这就意味着 GPIO1 会用到 GIC 的两个中断,会涉及 GIC 里的 2 个 hwirq
image

1.2 设备树中断相关属性#

设备树中,中断控制器节点中必须有一个属性:

1.2.1 interrupt-controller#

表明它是 “中断控制器”
image

1.2.2 interrupt-cells#

#interrupt-cells=<1>
别的节点要使用这个中断控制器时,只需要一个 cell 来表明使用 “哪一个中断”

1
2
3
4
5
6
vic: intc@10140000 {
compatible = "arm,versatile-vic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0x10140000 0x1000>;
};

#interrupt-cells=<2>
别的节点要使用这个中断控制器时,需要一个 cell 来表明使用 “哪一个中断”;还需要另一个 cell 来描述中断,一般是表明触发类型:
第 2 个 cell 的 bits[3:0] 用来表示中断触发类型(trigger type and level flags):

1
2
3
4
1 = low-to-high edge triggered,上升沿触发
2 = high-to-low edge triggered,下降沿触发
4 = active high level-sensitive,高电平触发
8 = active low level-sensitive,低电平触发
1
2
3
4
5
6
7
8
9
10
gpio2: gpio@020a0000 {
compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio";
reg = <0x020a0000 0x4000>;
interrupts = <0 68 4>,
<0 69 4>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

1.2.3 interrupt-parent#

你要用哪一个中断控制器里的中断?
image

1.2.4 interrupts#

你要用哪一个中断?
Interrupts 里要用几个 cell,由interrupt-parent对应的中断控制器决定。在中断控制器里有“#interrupt-cells”属性,它指明了要用几个 cell来描述中断。比如下图表示用到gpio中断控制器的160号中断,上升沿触发。
image

1.3 dts中获取中断#

1.3.1 对于 platform_device#

1.3.1.1 platform_get_resource#

当平台设备和平台驱动match上后,一个节点能被转换为 platform_device,调用platform_get_resource可获取节点资源信息。

1
extern struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned int);

image
image
如果需要获取中断信息,传入IORESOURCE_IRQ

1.3.2 对于 I2C 设备、SPI 设备#

I2C 总线驱动在处理设备树里的 I2C 子节点时,Linux总线会自动处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下(drivers/i2c/i2c-core.c),probe函数里面of_irq_get函数会根据dts中的i2c节点获取中断资源。
image

GIC_SPI表示中断类型为共享中断。
image
image
SPI总线同理,一个 SPI 设备会被转换为一个spi_device结构体,中断号会保存在 spi_device 的 irq 成员里,代码如下(drivers/spi/spi.c):
image

1.3.2.1 调用 of_irq_get 获得中断号#

如果我们没用platform device架构写字符设备驱动,也可以直接调用of_irq_get获取。
image

1.3.3 对于 GPIO#

1.3.3.1 gpio_to_irq或 gpiod_to_irq#

参考:drivers/input/keyboard/gpio_keys.c, api路径linux_5.10\include\linux\of_gpio.h
可以使用 gpio_to_irqgpiod_to_irq 获得中断号。

1
2
3
4
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);
//再去使用 gpiod_to_irq 获得中断号:
irq = gpiod_to_irq(bdata->gpiod);

2 编写一个按键中断#

2.1 按键dts配置#

linux内核自带的input sub system本身就包含了gpio按键驱动,驱动程序 drivers/input/keyboard/gpio_keys.c 就可以,然后你需要做
的只是修改设备树指定引脚及键值。
为了简化我们直接写一个例子:强化熟悉对gpio中断的使用。

我们确定好用gpio5_1, gpio4_14这2个按键来展开实验:定义好dts节点,这里定义gpio_keys_100ask,内核有函数自动把gpio num转成irq。在其他地方dts中(imx6ull.dts中)会描述好gpio5,gpio4节点信息,里面会有

1
#gpio-cells = <2>;

因此,这里表示引用gpio5的第0个引脚,gpio4的第14个引脚,节点如下,需要把原来的节点gpio-keys disable掉,添加下面的gpio_keys_100ask。进入内核目录 make dtbs

1
2
3
4
5
6
7
gpio_keys_100ask {
compatible = "100ask,gpio_key";
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH
&gpio4 14 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&key1_pinctrl &key2_pinctrl>;
};

image

设备树中并没有对这2个引脚进行pinctrl配置,也就是iomux配置,那为什么这2个引脚还能工作,是因为这个个引脚默认就是gpio状态,不用进行iomux切换。
为了保险起见,按照标准流程还需要对其添加pinctrl信息。

2.1.1 添加2个按键的iomux配置#

imx6ull工具有制作好pinctrl如何配置:
image
image

填入到对应的子节点下面:gpio5的iomux配置放在iomuxc_snvs节点下,gpio4的配置放在iomuxc下。
image
image

2.1.2 定义按键dts描述#

再到定义的gpio_keys_100ask引用这2个pinctrl信息 key1_100askkey2_100ask。这里pinctrl只有一个default状态,pinctrl-0表示该默认状态,因此最终gpio5_1, gpio4_14就被iomux成了gpio状态。
image

2.2 驱动代码#

驱动代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
};
static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;
static int g_key = 0;
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) {
int err;
wait_event_interruptible(gpio_key_wait, g_key);
err = copy_to_user(buf, &g_key, 4);
g_key = 0;
return 4;
}
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
g_key = (gpio_key->gpio << 8) | val;
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count)
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++)
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0) {
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}

for (i = 0; i < count; i++) {
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
, "100ask_gpio_key", &gpio_keys_100ask[i]);
}
major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv); /* /dev/100ask_gpio_key */
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;
}
static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;

device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
return platform_driver_register(&gpio_keys_driver);
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

2.2.1 驱动代码分析#

image

定义gpio_key
image
通过.compatible = "100ask,gpio_key"匹配plateform_deviceplatform_driver, 当insmod ko时probe函数被调用。
struct device_node *node = pdev->dev.of_node;//可以从platform_device获取到device_node

image
of_gpio_count可以根据设备树节点获取到gpio的数量。

image
of_get_gpio_flags可以根据设备树节点获取到gpio编号和gpio flags

image
获取gpio描述子和gpio中断号:

image
注册中断服务程序gpio_key_isr,当按键按下会触发gpio中断,执行gpio_key_isr
image
中断服务程序就简单的打印按键的电平状态:

1
2
3
4
5
6
7
static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED;
}

字符设备驱动-9-中断子系统-中断结构体

0 引入SPARSE_IRQ#

如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用 基数树(radix tree) 来代替 irq_desc 数组。
SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛?当中断比较“稀疏”时可以用基数树来代替数组。
image

1 irq_desc 数组#

位于include/linux/irqdesc.h

image

在这里插入图片描述

内核中记录一个irq_desc的数组,数组的每一项对应一个中断或者一组中断(使用同一中断号)。irq_desc几乎记录所有中断相关的东西,这个结构是中断的核心。每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表

irq_desc 数组结构链路如下图:

image

1.1 中断处理函数handle_irq#

1.1.1 共享中断概念引入#


image

  1. 上图一个gpio按键连接gpio模块第一个引脚1,可以设置该引脚,当电平发生变化时,让该引脚产生中断,那么gpio模块会上报中断到gic模块, gic模块继续中断cpu。
  2. 同理当一个外部设备网卡和该gpio按键可以共享一个中断,也接到gpio模块第一个引脚1,gpio模块会上报中断到gic模块, gic模块继续中断cpu。这里就用到了共享中断的概念。

可以看到中断的触发时从左到右的过程,那么cpu进行响应中断请求时就是从右到左的过程。

  1. cpu读取GIC控制器,判段中断号,如果是A号中断说明是来源于gpio模块,如果是A'中断,说明来源于其他模块

  2. A号中断的来源有很多种,有gpio0,gpio1...., 又会从gpio控制寄存器来辨别倒是是哪一个gpio产生的中断,比如是B号中断

  3. B号中断的来源有很多种,有按键,网卡...

1.1.2 中断的处理函数来源#

中断处理函数来源有三:

  1. GIC 的处理函数:
    GIC 中断 CPU 时,CPU 读取 GIC 状态得到中断 A。假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家),这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是B),再去调用 irq_desc[B]. handle_irq

CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的.调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供。

  1. 模块的中断处理函数:
    对于 GPIO 模块向 GIC 发出的中断 B , 它 的 处 理 函 数 是irq_desc[B].handle_irq
    导致 GPIO 中断 B 发生的原因很多,可能是外部设备 1,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以 irq_desc[B].handle_irq会调用链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。

  2. 外部设备提供的处理函数:(也就是action里面的函数)
    这里说的“外部设备”可能是芯片,也可能是简单的按键。它们的处理函数由自己驱动程序提供。对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表, 这个链表就是 action 链表。一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。

1.2 irqaction#

irqaction 结构体在include/linux/interrupt.h

image

image

irq_desc[A]这里对应的action一般为NULL, 而irq_desc[B]handle_irq会调用链表里的函数,这些函数就是对应不同的irqaction

当调用request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体。在里面保存name、dev_id等,最重要的是 handler、thread_fn、thread
函数原型为:

1.2.1 request_threaded_irq#

1.2.2 request_irq#

1.2.3 devm_request_irq#

image
image
image
这里irq编号使用的虚拟中断号,虚拟中断号怎么来?详见后面1.4 irq_domain

handler :是中断处理的上半部函数,用来处理紧急的事情。
thread_fn :对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
  1. 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。
  2. 可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。
  3. 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

在 reqeust_irq 时可以传入 dev_id,为何需要 dev_id?作用有 2:

  1. 中断处理函数执行时,可以使用 dev_id
  2. 卸载中断时要传入 dev_id,这样才能在action链表中根据 dev_id 找到对应项(所以在共享中断中必须提供 dev_id,非共享中断可以不提供)

1.3 irq_data#

定义再include/linux/irq.h

image
irq_data就是个中转站,里面有 irq_chip 指针 irq_domain 指针,irq 是软件中断号,hwirq 是硬件中断号。
比如GPIO 中断 B 就是软件中断号,可以找到 irq_desc[B]这个数组项;GPIO 里的第 x 号中断,这就是 hwirq

irq、hwirq 之间的联系呢?由 irq_domain 来建立。下面介绍irq_domain

image

1.4 irq_domain#

include/linux/irqdomain.h 中定义该结构。

image

img

设备树中你会看到这样的属性:

1
2
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

表示使用gpio1_5作为中断,hwirq 就是 5。当我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断,irq编号就是虚拟中断,那么虚拟中断号(软件中断号)要怎么得到?
就是gpio1对应的irq_domain 结构体。irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数。

1.4.0 中断控制器注册 irq_domain#

img

通过 __irq_domain_add 初始化irq_domain数据结构,然后把 irq_domain 添加到全局的链表irq_domain_list中。

1.4.1 irq_domain_ops#

1.4.1.1 xlate函数#

image

xlate函数
用来解析设备树的中断属性,提取出 hwirq、type 等信息。

1.4.1.2 map函数#

hwirq 转换为 irq

1.5 irq_chip#

irq_chip 结构体在include/linux/irq.h中定义

image

1
2
3
4
5
6
7
8
9
* @irq_startup:  start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt

在这里插入图片描述

我们在request_irq后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的irq_enable函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”,外设备千变万化,内核里没有对应的清除中断操作。

2 中断不同结构体之间的关系框图#

image-20240810191140464

字符设备驱动-9-中断子系统-GICv2架构解析

1 armv7 32位GICv2介绍#

armv7 32位 gic采用v2版本,参考手册 https://developer.arm.com/documentation/ihi0048/bb/?lang=en

image
image

GIC400 就是v2版本的中断控制器 IP 核,当 GIC 接收到外部中断信号以后就会报给 ARM 内核。框架如下:
image

GIC 架构分为了两个逻辑块:DistributorCPU Interface,也就是分发器端和 CPU 接口端。

1.0 分发器端和 CPU 接口端#

  1. 分发器用来全局中断使能控制,每一个中断使能开关,中断优先级,外部中断触发方式(边沿触发、电平触发)等。外设->分发器会设置成pending(或者active and pending状态),这时分发器传递优先级最高的pending中断给cpu 接收端。

  2. cpu接收端用来接收中断信号汇报给cpu, 如:应答中断,通知中断处理完成,定义抢占策略,设置优先级掩码,当多个中断到来选择最高优先级的中断号。

    例如:I.MX6U给了一个 core_ca7.h定义了GIC的所有寄存器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    /*
    * GIC 寄存器描述结构体,
    * GIC 分为分发器端和 CPU 接口端
    */
    typedef struct {
    /* 分发器端寄存器 */
    int32_t RESERVED0[1024];
    _IOM uint32_t D_CTLR; /* Offset: 0x1000 (R/W) */
    _IM uint32_t D_TYPER; /* Offset: 0x1004 (R/ ) */
    _IM uint32_t D_IIDR; /* Offset: 0x1008 (R/ ) */
    int32_t RESERVED1[29];
    _IOM uint32_t D_IGROUPR[16]; /* Offset: 0x1080 - 0x0BC (R/W) */
    uint32_t RESERVED2[16];
    __IOM uint32_t D_ISENABLER[16];/* Offset: 0x1100 - 0x13C (R/W) */
    uint32_t RESERVED3[16];
    __IOM uint32_t D_ICENABLER[16];/* Offset: 0x1180 - 0x1BC (R/W) */
    uint32_t RESERVED4[16];
    __IOM uint32_t D_ISPENDR[16]; /* Offset: 0x1200 - 0x23C (R/W) */
    uint32_t RESERVED5[16];
    __IOM uint32_t D_ICPENDR[16]; /* Offset: 0x1280 - 0x2BC (R/W) */
    uint32_t RESERVED6[16];
    __IOM uint32_t D_ISACTIVER[16];/* Offset: 0x1300 - 0x33C (R/W) */
    uint32_t RESERVED7[16];
    __IOM uint32_t D_ICACTIVER[16];/* Offset: 0x1380 - 0x3BC (R/W) */
    uint32_t RESERVED8[16];
    __IOM uint8_t D_IPRIORITYR[512];/* Offset: 0x1400 - 0x5FC (R/W) */
    uint32_t RESERVED9[128];
    __IOM uint8_t D_ITARGETSR[512];/* Offset: 0x1800 - 0x9FC (R/W) */
    uint32_t RESERVED10[128];
    __IOM uint32_t D_ICFGR[32]; /* Offset: 0x1C00 - 0xC7C (R/W) */
    uint32_t RESERVED11[32];
    __IM uint32_t D_PPISR; /* Offset: 0x1D00 (R/ ) */
    __IM uint32_t D_SPISR[15]; /* Offset: 0x1D04 - 0xD3C (R/ ) */
    uint32_t RESERVED12[112];
    __OM uint32_t D_SGIR; /* Offset: 0x1F00 ( /W) */
    uint32_t RESERVED13[3];
    __IOM uint8_t D_CPENDSGIR[16];/* Offset: 0x1F10 - 0xF1C (R/W) */
    __IOM uint8_t D_SPENDSGIR[16];/* Offset: 0x1F20 - 0xF2C (R/W) */
    uint32_t RESERVED14[40];
    __IM uint32_t D_PIDR4; /* Offset: 0x1FD0 (R/ ) */
    __IM uint32_t D_PIDR5; /* Offset: 0x1FD4 (R/ ) */
    __IM uint32_t D_PIDR6; /* Offset: 0x1FD8 (R/ ) */
    __IM uint32_t D_PIDR7; /* Offset: 0x1FDC (R/ ) */
    __IM uint32_t D_PIDR0; /* Offset: 0x1FE0 (R/ ) */
    __IM uint32_t D_PIDR1; /* Offset: 0x1FE4 (R/ ) */
    __IM uint32_t D_PIDR2; /* Offset: 0x1FE8 (R/ ) */
    __IM uint32_t D_PIDR3; /* Offset: 0x1FEC (R/ ) */
    __IM uint32_t D_CIDR0; /* Offset: 0x1FF0 (R/ ) */
    __IM uint32_t D_CIDR1; /* Offset: 0x1FF4 (R/ ) */
    __IM uint32_t D_CIDR2; /* Offset: 0x1FF8 (R/ ) */
    __IM uint32_t D_CIDR3; /* Offset: 0x1FFC (R/ ) */

    /* CPU 接口端寄存器 */
    __IOM uint32_t C_CTLR; /* Offset: 0x2000 (R/W) */
    __IOM uint32_t C_PMR; /* Offset: 0x2004 (R/W) */
    __IOM uint32_t C_BPR; /* Offset: 0x2008 (R/W) */
    __IM uint32_t C_IAR; /* Offset: 0x200C (R/ ) */
    __OM uint32_t C_EOIR; /* Offset: 0x2010 ( /W) */
    __IM uint32_t C_RPR; /* Offset: 0x2014 (R/ ) */
    __IM uint32_t C_HPPIR; /* Offset: 0x2018 (R/ ) */
    __IOM uint32_t C_ABPR; /* Offset: 0x201C (R/W) */
    __IM uint32_t C_AIAR; /* Offset: 0x2020 (R/ ) */
    __OM uint32_t C_AEOIR; /* Offset: 0x2024 ( /W) */
    __IM uint32_t C_AHPPIR; /* Offset: 0x2028 (R/ ) */
    uint32_t RESERVED15[41];
    __IOM uint32_t C_APR0; /* Offset: 0x20D0 (R/W) */
    uint32_t RESERVED16[3];
    __IOM uint32_t C_NSAPR0; /* Offset: 0x20E0 (R/W) */
    uint32_t RESERVED17[6];
    __IM uint32_t C_IIDR; /* Offset: 0x20FC (R/ ) */
    uint32_t RESERVED18[960];
    __OM uint32_t C_DIR; /* Offset: 0x3000 ( /W) */
    } GIC_Type;

1.1 GIC 类型#

GIC 将众多的中断源分为分为三类:
①、SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断 。
②、PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。

1.2 中断 ID#

为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,ID0~ID1019 中断使能和禁止。这 1020 个 ID 包含了 PPI、SPI 和 SGI。

ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI。

例如:I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160,那么irq为0的中断ID即为32。
个。NXP 官方 SDK中的文件 MCIMX6Y2C.h定义了160个中断ID。
image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define NUMBER_OF_INT_VECTORS 160 /* 中断源 160 个,SGI+PPI+SPI*/
typedef enum IRQn {
/* Auxiliary constants */
otAvail_IRQn = -128,
/* Core interrupts */
oftware0_IRQn = 0,
oftware1_IRQn = 1,
Software2_IRQn = 2,
Software3_IRQn = 3,
Software4_IRQn = 4,
Software5_IRQn = 5,
Software6_IRQn = 6,
Software7_IRQn = 7,
Software8_IRQn = 8,
Software9_IRQn = 9,
Software10_IRQn = 10,
Software11_IRQn = 11,
Software12_IRQn = 12,
Software13_IRQn = 13,
Software14_IRQn = 14,
Software15_IRQn = 15,
VirtualMaintenance_IRQn = 25,
HypervisorTimer_IRQn = 26,
VirtualTimer_IRQn = 27,
LegacyFastInt_IRQn = 28,
SecurePhyTimer_IRQn = 29,
NonSecurePhyTimer_IRQn = 30,
LegacyIRQ_IRQn = 31,
/* Device specific interrupts */
IOMUXC_IRQn = 32,
DAP_IRQn = 33,
SDMA_IRQn = 34,
TSC_IRQn = 35,
SNVS_IRQn = 36,
...... ......
} IRQn_Type;

1.3 中断配置(1.7有详细描述)#

1.3.1 IRQ 和 FIQ 总中断使能#

CPSR程序状态寄存器”已经讲过了,寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使
能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。我们还有更简单的指令来完成 IRQ 或者 FIQ 的使能和
禁止:

指令 描述
cpsid i 禁止 IRQ 中断。
cpsie i 使能 IRQ 中断。
cpsid f 禁止 FIQ 中断。
cpsie f 使能 FIQ 中断。

1.3.2 ID0~ID1019 中断使能和禁止#

前面讲到中断ID有 ID0~ID1019, GIC 寄存器 GICD_ISENABLERn GICD_ ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。
一共16组GICD_ISENABLERGICD_ISENABLER,其中GICD_ISENABLER0 bit[15:0]对应ID15~0 的 SGI 中断,GICD_ISENABLER0 bit[31:16]对应ID31~16的 PPI 中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。

1.3.3 中断优先级数量 GICC_PMR#

Cortex-A7 GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高!Cortex-A7 选择了 32 个优先级,GICC_PMR 寄存器,此寄存器用来决定使用几级优先级,GICC_PMR 寄存器只有低 8 位有效,这 8 位最多可以设置 256 个优先级:
image
I.MX6U 为例 Cortex-A7内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000

1.3.4 中断抢占优先级和子优先级位数 GICC_BPR#

寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同:
image
比如 I.MX6U 的优先级位数为 5(32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级。

1.3.5 中断priority D_IPRIORITYR#

Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5,示例代码如下:

1
GICD_IPRIORITYR[40] = 5 << 3;

1.4 中断状态机#

非活动状态(Inactive):这意味着该中断未触发。

挂起(Pending):这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。

活动(Active):描述了一个已被内核接收并正在处理的中断。

活动和挂起(Active and pending):描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。

1.5 GIC初始化硬件流程(软件流程见3.3)#

image

1.6 GIC中断处理#

当CPU核接收到中断时,cpu interface中有Interrupt Acknowledge Register可以读,获取中断ID。并且标记为active状态。

当对应的中断服务程序执行完,会将中断ID写入CPU interface模块中的End of Interrupt register。标记为inactivepending(如果状态为inactive and pending)。

1.7 GIC控制器寄存器介绍#

前面讲了GIC 架构分为了两个逻辑块:Distributor CPU Interface,也就是分发器端和 CPU 接口端。

1.7.1 GIC的内存映射#

image
GIC基地址偏移0x1000是分发器 block, 偏移0x2000是CPU 接口端 block。

1.7.1.1 分发器寄存器#

image

1.7.1.1.1 GICD_CTLR(Distributor Control Register)#

Distributor Control Register,分发器控制寄存器。

image

位域 读写 描述
1 EnableGrp1 R/W 用于将pending Group 1中断从Distributor转发到CPU interfaces 0:group 1中断不转发 1:根据优先级规则转发Group 1中断
0 EnableGrp0 R/W 用于将pending Group 0中断从Distributor转发到CPU interfaces 0:group 0中断不转发 1:根据优先级规则转发Group 0中断
1.7.1.1.2 GICD_TYPER(Controller Type Register)#

image

位域 读写 描述
15:11 LSPI R 如果GIC实现了安全扩展,则此字段的值是已实现的可锁定SPI的最大数量,范围为0(0b00000)到31(0b11111)。 如果此字段为0b00000,则GIC不会实现配置锁定。 如果GIC没有实现安全扩展,则保留该字段。
10 SecurityExtn R 表示GIC是否实施安全扩展: 0未实施安全扩展; 1实施了安全扩展
7:5 CPUNumber R 表示已实现的CPU interfaces的数量。 已实现的CPU interfaces数量比该字段的值大1。 例如,如果此字段为0b011,则有四个CPU interfaces。
4:0 ITLinesNumber R 表示GIC支持的最大中断数。 如果ITLinesNumber = N,则最大中断数为32*(N+1)。 中断ID的范围是0到(ID的数量– 1)。 例如:0b00011最多128条中断线,中断ID 0-127。 中断的最大数量为1020(0b11111)。 无论此字段定义的中断ID的范围如何,都将中断ID 1020-1023保留用于特殊目的
1.7.1.1.3 GICD_IIDR(Implementer Identification Register)#

image

位域 读写 描述
31:24 ProductID R 产品标识ID
23:20 保留
19:16 Variant R 通常是产品的主要版本号
15:12 Revision R 通常此字段用于区分产品的次版本号
11:0 Implementer R 含有实现这个GIC的公司的JEP106代码; [11:8]:JEP106 continuation code,对于ARM实现,此字段为0x4; [7]:始终为0; [6:0]:实现者的JEP106code,对于ARM实现,此字段为0x3B
1.7.1.1.4 GICD_IGROUPRn(Group Registers)#

image

位域 读写 描述
31:0 Group status bits R/W 组状态位,对于每个位: 0:相应的中断为Group 0; 1:相应的中断为Group 1。

对于一个中断,如何设置它的Group ?首先找到对应的GICD_IGROUPRn寄存器,即n是多少?还要确定使用这个寄存器里哪一位。

对于interrtups ID m,如下计算:

1
2
3
4
n = m DIV 32,GICD_IGROUPRn里的n就确定了;
GICD_IGROUPRn在GIC内部的偏移地址是多少?0x080+(4*n)
使用GICD_IPRIORITYRn中哪一位来表示interrtups ID m?
bit = m mod 32
1.7.1.1.5 GICD_ISENABLERn(Set-Enable Registers)#

image

位域 读写 描述
31:0 Set-enable bits R/W 对于SPI和PPI类型的中断,每一位控制对应中断的转发行为:从Distributor转发到CPU interface: 读: 0:表示当前是禁止转发的; 1:表示当前是使能转发的; 写: 0:无效 1:使能转发

对于一个中断,如何找到GICD_ISENABLERn并确定相应的位?

1
2
3
4
5
对于interrtups ID m,如下计算:
n = m DIV 32,GICD_ISENABLERn里的n就确定了;
GICD_ISENABLERn在GIC内部的偏移地址是多少?0x100+(4*n)
使用GICD_ISENABLERn中哪一位来表示interrtups ID m?
bit = m mod 32
1.7.1.1.6 GICD_ICENABLERn(Clear-Enable Registers)#

img

位域 读写 描述
31:0 Clear-enable bits R/W 对于SPI和PPI类型的中断,每一位控制对应中断的转发行为:从Distributor转发到CPU interface: 读: 0:表示当前是禁止转发的; 1:表示当前是使能转发的; 写: 0:无效 1:禁止转发

对于一个中断,如何找到GICD_ICENABLERn并确定相应的位?

1
2
3
4
5
对于interrtups ID m,如下计算:
n = m DIV 32,GICD_ISENABLERn里的n就确定了;
GICD_ISENABLERn在GIC内部的偏移地址是多少?0x100+(4*n)
使用GICD_ISENABLERn中哪一位来表示interrtups ID m?
bit = m mod 32
1.7.1.1.7 GICD_ISACTIVERn(Set-Active Registers)#

img

位域 读写 描述
31:0 Set-active bits R/W 读: 0:表示相应中断不是active状态; 1:表示相应中断是active状态; 写: 0:无效 1:把相应中断设置为active状态,如果中断已处于Active状态,则写入无效

对于一个中断,如何找到GICD_ISACTIVERn并确定相应的位?

1
2
3
4
5
对于interrtups ID m,如下计算:
n = m DIV 32,GICD_ISACTIVERn里的n就确定了;
GICD_ISACTIVERn在GIC内部的偏移地址是多少?0x300+(4*n)
使用GICD_ISACTIVERn 中哪一位来表示interrtups ID m?
bit = m mod 32。
1.7.1.1.8 GICD_ICACTIVERn(Clear-Active Registers)#
位域 读写 描述
31:0 Clear-active bits R/W 读: 0:表示相应中断不是active状态; 1:表示相应中断是active状态; 写: 0:无效 1:把相应中断设置为deactive状态,如果中断已处于dective状态,则写入无效

对于一个中断,如何找到GICD_ICACTIVERn并确定相应的位?

1
2
3
4
5
对于interrtups ID m,如下计算:
n = m DIV 32,GICD_ICACTIVERn里的n就确定了;
GICD_ICACTIVERn 在GIC内部的偏移地址是多少?0x380+(4*n)
使用GICD_ICACTIVERn中哪一位来表示interrtups ID m?
bit = m mod 32。
1.7.1.1.9 GICD_IPRIORITYRn(Priority Registers)#

img

位域 读写 描述
31:24 Priority, byte offset 3 R/W 对于每一个中断,都有对应的8位数据用来描述:它的优先级。 每个优先级字段都对应一个优先级值,值越小,相应中断的优先级越高
23:16 Priority, byte offset 2 R/W
15:8 Priority, byte offset 1 R/W
7:0 Priority, byte offset 0 R/W

如何设置它的优先级(Priority),首先找到对应的GICD_IPRIORITYRn寄存器,即n是多少?还要确定使用这个寄存器里哪一个字节。

1
2
3
4
5
6
7
8
9
对于interrtups ID m,如下计算:
n = m DIV 4,GICD_IPRIORITYRn里的n就确定了;
GICD_IPRIORITYRn在GIC内部的偏移地址是多少?0x400+(4*n)
使用GICD_IPRIORITYRn中4个字节中的哪一个来表示interrtups ID m的优先级?
byte offset = m mod 4
byte offset 0对应寄存器里的[7:0];
byte offset 1对应寄存器里的[15:8];
byte offset 2对应寄存器里的[23:16];
byte offset 3对应寄存器里的[31:24]。
1.7.1.1.10 GICD_ITARGETSRn(Processor Targets Registers)#

img

位域 读写 描述
31:24 CPU targets, byte offset 3 R/W 对于每一个中断,都有对应的8位数据用来描述:这个中断可以发给哪些CPU。 处理器编号从0开始,8位数里每个位均指代相应的处理器。 例如,值0x3表示将中断发送到处理器0和1。 当读取GICD_ITARGETSR0~GICD_ITARGETSR7时,读取里面任意字节,返回的都是执行这个读操作的CPU的编号。
23:16 CPU targets, byte offset 2 R/W
15:8 CPU targets, byte offset 1 R/W
7:0 CPU targets, byte offset 0 R/W

如何设置它的目杯CPU?优先级(Priority),首先找到对应的GICD_ITARGETSRn寄存器,即n是多少?还要确定使用这个寄存器里哪一个字节。

1
2
3
4
5
6
7
8
9
对于interrtups ID m,如下计算:
n = m DIV 4,GICD_ITARGETSRn里的n就确定了;
GICD_ITARGETSRn在GIC内部的偏移地址是多少?0x800+(4*n)
使用GICD_ITARGETSRn中4个字节中的哪一个来表示interrtups ID m的目标CPU?
byte offset = m mod 4。
byte offset 0对应寄存器里的[7:0];
byte offset 1对应寄存器里的[15:8];
byte offset 2对应寄存器里的[23:16];
byte offset 3对应寄存器里的[31:24]。
1.7.1.1.11 GICD_ICFGRn(Configuration Registers)#

img

位域 读写 描述
[2F+1:2F] Int_config, field F R/W 对于每一个中断,都有对应的2位数据用来描述:它的边沿触发,还是电平触发。 对于Int_config [1],即高位[2F + 1],含义为: 0:相应的中断是电平触发; 1:相应的中断是边沿触发。 对于Int_config [0],即低位[2F],是保留位。

如何找到GICD_ICFGRn并确定相应的位域F?

1
2
3
4
对于interrtups ID m,如下计算:
n = m DIV 16,GICD_ICFGRn里的n就确定了;
GICD_ICACTIVERn 在GIC内部的偏移地址是多少?0xC00+(4*n)
F = m mod 16
1.7.1.1.12 ICPIDR2(Identification registers: Peripheral ID2 Register)#

img

位域 读写 描述
[31:0] - R/W 由实现定义
[7:4] ArchRev R 该字段的值取决于GIC架构版本: 0x1:GICv1; 0x2:GICv2。
[3:0] - R/W 由实现定义

1.7.1.2 cpu接口端寄存器#

image

1.7.1.2.1 GICC_CTLR(CPU Interface Control Register)#

img

位域 读写 描述
[31:10] - 保留
[9] EOImodeNS R/W 控制对GICC_EOIR和GICC_DIR寄存器的非安全访问: 0:GICC_EOIR具有降低优先级和deactivate中断的功能; 对GICC_DIR的访问是未定义的。 1:GICC_EOIR仅具有降低优先级功能; GICC_DIR寄存器具有deactivate中断功能。
[8:7] - 保留
[6] IRQBypDisGrp1 R/W 当CPU interface的IRQ信号被禁用时,该位控制是否向处理器发送bypass IRQ信号: 0:将bypass IRQ信号发送给处理器; 1:将bypass IRQ信号不发送到处理器。
[5] FIQBypDisGrp1 R/W 当CPU interface的FIQ信号被禁用时,该位控制是否向处理器发送bypass FIQ信号: 0:将bypass FIQ信号发送给处理器; 1:旁路FIQ信号不发送到处理器
[4:1] - 保留
[0] - R/W 使能CPU interface向连接的处理器发出的组1中断的信号: 0:禁用中断信号 1:使能中断信号
1.7.1.2.2 GICC_PMR(Priority Mask Register)#

img

位域 读写 描述
[31:8] - 保留
[7:0] - R/W 优先级高于这个值的中断,才会发送给CPU

[7:0]共8位,可以表示256个优先级。但是某些芯片里的GIC支持的优先级少于256个,则某些位为RAZ / WI,如下所示:

1
2
3
4
如果有128个级别,则寄存器中bit[0] = 0b0,即使用[7:1]来表示优先级;
如果有64个级别,则寄存器中bit[1:0] = 0b00,即使用[7:2]来表示优先级;
如果有32个级别,则寄存器中bit[2:0] = 0b000,即使用[7:3]来表示优先级;
如果有16个级别,则寄存器中bit[3:0] = 0b0000,即使用[7:4]来表示优先级;
1.7.1.2.3 GICC_BPR(Binary Point Register)#

此寄存器用来把8位的优先级字段拆分为组优先级和子优先级,组优先级用来决定中断抢占。

位域 读写 描述
[31:3] - 保留
[2:0] Binary point R/W 此字段的值控制如何将8bit中断优先级字段拆分为组优先级和子优先级,组优先级用来决定中断抢占。 更多信息还得看看GIC手册。
1.7.1.2.4 GICC_IAR(Acknowledge Register)#

读此寄存器,获得当前中断的interrtup ID

GICC_IAR寄存器描述来自《ARM Generic Interrupt Controller Architecture Specification.pdf》,它用来表示中断ID号。
image
处理完具体的中断处理函数,需要将GICC_IAR寄存器的值写入GICC_EOIR寄存器中。

位域 读写 描述
[31:13] - 保留
[12:10] CPUID R 对于SGI类中断,它表示谁发出了中断。例如,值为3表示该请求是通过对CPU interface 3上的GICD_SGIR的写操作生成的。
[9:0] Interrupt ID R 中断ID
1.7.1.2.5 GICC_EOIR(Interrupt Register)#

写此寄存器,表示某中断已经处理完毕。GICC_IAR的值表示当前在处理的中断,把GICC_IAR的值写入GICC_EOIR就表示中断处理完了。
image

位域 读写 描述
[31:13] - 保留
[12:10] CPUID W 对于SGI类中断,它的值跟GICD_IAR. CPUID的相同。
[9:0] EOIINTID W 中断ID,它的值跟GICD_IAR里的中断ID相同

2. 中断示例start.s分析#

以nxp的IMX6UL为例,SDK中core_ca7.h定了了GIC相关函数:

函数 描述
GIC_Init 初始化 GIC。
GIC_EnableIRQ 使能指定的外设中断。
GIC_DisableIRQ 关闭指定的外设中断。
GIC_AcknowledgeIRQ 返回中断号。
GIC_DeactivateIRQ 无效化指定中断。
GIC_GetRunningPriority 获取当前正在运行的中断优先级。
GIC_SetPriorityGrouping 设置抢占优先级位数。
GIC_GetPriorityGrouping 获取抢占优先级位数。
GIC_SetPriority 设置指定中断的优先级。
GIC_GetPriority 获取指定中断的优先级。
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
.global _start  				/* 全局标号 */
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
/* 复位中断 */
Reset_Handler:
cpsid i /* 关闭全局中断 */
/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */
#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000

dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif
/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endif
b main /* 跳转到main函数 */
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中断 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0

/* IRQ中断!重点!!!!! */
IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */

mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */

mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */

cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */

push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */

pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */

pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */
/* FIQ中断 */
FIQ_Handler:

ldr r0, =FIQ_Handler
bx r0

2.1 start.s启动流程#

  1. 进入_start,初始化异常向量表。进入复位中断,初始化时钟,关闭看门狗,关闭MMUICACHE DCACHE,关闭总中断
  2. 设置各个模式的SP指针
  3. 代码段重定位到DDR上并且清bss段
  4. 开启总中断
  5. 跳转到C语言main函数执行

这里很多流程如代码重定位,清除bss,关闭看门狗等没有列举出来。

3 GIC中断处理流程#

3.1 一级中断控制器流程#

image

  • 假设GIC可以向CPU发出16-1019号中断,这些数字被称为hwirq0-15用于Process之间通信,比较特殊。
  • 假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16
  • GIC domain中会记录(32, 16)
  • 那么注册中断时就是:request_irq(16, ...)
  • 发生UART中断时
    • 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16
    • 调用irq_desc[16]中的handleA函数,它的作用是调用action链表中用户注册的函数.

3.2 多级中断控制器流程#

img

  • 假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断
  • GPIO也可以看作一个中断控制器,对于它的4个中断
  • 对于GPIO模块中0~3这四个hwirq,一般都会一下子分配四个irq_desc
  • 假设这4个irq_desc的序号为100~103,在GPIO domain中记录(0,100) (1,101)(2,102) (3,103)
  • 对于KEY,注册中断时就是:request_irq(102, ...)
  • 按下KEY时:
    • 程序从GIC中读取寄存器知道发生了33号中断,通过GIC irq_domain可以知道virq为16.
    • 调用irq_desc[16]中的handleB函数
      • handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断
      • 通过GPIO irq_domain可以知道virq为102
      • 调用irq_desc[102]中的handleA函数,它的作用是调用action链表中用户注册的函数

3.3 GIC软件初始化过程#

1
2
3
4
5
6
7
start_kernel (init\main.c)
init_IRQ (arch\arm\kernel\irq.c)
irqchip_init (drivers\irqchip\irqchip.c)
of_irq_init (drivers\of\irq.c)//gic子系统
desc->irq_init_cb = match->data;
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);

3.3.1 gic驱动注册#

内核支持多种GIC, 在内核为每一类GIC定义一个结构体of_device_id,并放在一个段里:

1
2
3
4
5
6
7
8
9
10
// drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);//imx6ull对应gic类型
IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init);
IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);

IRQCHIP_DECLARE宏进行展开:

1
2
3
4
5
6
7
8
9
// include\linux\irqchip.h
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__irqchip_of_table) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }

例如:IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);展开后:

1
2
3
4
static const struct of_device_id __of_table_cortex_a7_gic		\
__used __section(__irqchip_of_table) \
= { .compatible = "arm,cortex-a7-gic", \
.data = gic_of_init }

3.3.1.1 dts匹配#

在这里插入图片描述

根据dts匹配调用IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);, 对irq chip driver 的声明。

定义 IRQCHIP_DECLARE 之后,相应的内容会保存到 __irqchip_of_table 里。__irqchip_of_table 在链接脚本 vmlinux.lds 里,被放到了__irqchip_begin__irqchip_of_end 之间,该段用于存放中断控制器信息。

3.3.1.2 gic_of_init(GIC驱动初始化入口)#

gic_of_init内容太多,大致就是对中断控制器初始化:

  1. 初始化GICD(分发器寄存器)

  2. 初始化GICC(cpu接口端寄存器)

  3. 调用gic_init_bases 流程

    1. 调用set_handle_irq注册gic_handle_irq,异常处理的入口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      void __init set_handle_irq(void (*handle_irq)(struct pt_regs *)){
      if (handle_arch_irq)
      return;
      handle_arch_irq = handle_irq;
      }
      static int __init gic_init_bases(void __iomem *dist_base,
      struct redist_region *rdist_regs,
      u32 nr_redist_regions,
      u64 redist_stride,
      struct fwnode_handle *handle) {
      set_handle_irq(gic_handle_irq);
      }

3.3.1.3 申请GIC中断#

3.3.1.3.1 在设备树里指定中断#

image-20240809003246767

3.3.1.3.2 对设备树中断的处理#

在这里插入图片描述

3.4 GIC中断处过程#

  1. 进入中断栈irq_stack_entry
  2. 执行中断控制器的中断入口handle_arch_irq
  3. 退出中断栈irq_stack_exit

中断栈用来保存中断的上下文,中断发生和退出的时候调用 irq_stack_entry irq_stack_exit 来进入和退出中断栈。

3.4.1 handle_arch_irq入口#

image-20240810150555648

3.4.1.1 gic_handle_irq#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs){
u32 irqnr;

do {
irqnr = gic_read_iar(); ------(1)

if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) { ------(2)
int err;

if (static_key_true(&supports_deactivate))
gic_write_eoir(irqnr);
else
isb();

err = handle_domain_irq(gic_data.domain, irqnr, regs); ------(3)
if (err) {
WARN_ONCE(true, "Unexpected interrupt received!\n");
if (static_key_true(&supports_deactivate)) {
if (irqnr < 8192)
gic_write_dir(irqnr);
} else {
gic_write_eoir(irqnr);
}
}
continue;
}
if (irqnr < 16) { ------(4)
gic_write_eoir(irqnr);
if (static_key_true(&supports_deactivate))
gic_write_dir(irqnr);
#ifdef CONFIG_SMP
/*
* Unlike GICv2, we don't need an smp_rmb() here.
* The control dependency from gic_read_iar to
* the ISB in gic_write_eoir is enough to ensure
* that any shared data read by handle_IPI will
* be read after the ACK.
*/
handle_IPI(irqnr, regs); ------(5)
#else
WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
continue;
}
} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}
  1. 读取中断控制器的寄存器GICC_IAR,并获取 hwirq
  2. 外设触发的中断。硬件中断号0-15表示 SGI (软件中断)类型的中断,15-1020 表示外设中断(SPI或PPI共享中断类型),8192-MAX 表示 LPI 类型的中断
  3. 中断控制器中断处理的主体
  4. 软件触发的中断
  5. 核间交互触发的中断
3.4.1.1.1 handle_domain_irq#

image-20240810151815454

  1. 进入中断上下文
  2. 根据 hwirq 去查找 linux 中断号
  3. 通过中断号找到全局中断描述符数组irq_desc[NR_IRQS]中的一项,然后调用 generic_handle_irq_desc,执行该 irq 号注册的 action
  4. 退出中断上下文
3.4.1.1.1.1 generic_handle_irq#

generic_handle_irq展开:

image-20240810151917713

image-20240810152019303

调用 desc->handle_irq 指向的回调函数。

image-20240810152332559

irq_domain_set_info 根据硬件中断号的范围设置 irq_desc->handle_irq 的指针,共享中断入口为 handle_fasteoi_irq,私有中断入口为 handle_percpu_devid_irq

  • handle_percpu_devid_irq:处理私有中断处理,在这个过程中会分别调用中断控制器的处理函数进行硬件操作,该函数调用 action->handler() 来进行中断处理
  • handle_fasteoi_irq:处理共享中断,并且遍历 irqaction 链表,逐个调用 action->handler() 函数,这个函数正是设备驱动程序调用 request_irq/request_threaded_irq 接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用__irq_wake_thread唤醒内核线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags) {
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;

for_each_action_of_desc(desc, action) {
irqreturn_t res;
res = action->handler(irq, action->dev_id);//requst_irq注册的函数
switch (res) {
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
case IRQ_HANDLED:
*flags |= action->flags;
break;
}
}
...
}
3.4.1.1.2 总结request_irq的函数如何被执行#

image-20240810152452686

4 中断控制器GIC的设备树描述#

中断控制器而言 ,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。以nxp的imx6ull.dtsi为例:

1
2
3
4
5
6
7
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
  1. compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到 GIC 中断控制器驱动文件, GIC 中断控制器驱是架构通用的,在drivers/irqchip/irq-gic.c
  2. interrupt-cells #address-cells、#size-cells 一样。
    2.1 每个 cells 都是 32 位整形值,对于 ARM 处理的GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:
1
2
3
4
5
6
第一个 cells:中断类型,0 表示 SPI(共享) 中断,1 表示 PPI(私有) 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于PPI
中断来说中断号的范围为 0~15
第三个 cells:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,
2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低
电平触发。bit[15:8]为 PPI 中断的 CPU 掩码。
  1. interrupt-controller表示该节点中断控制器

对于gpio来说也可以作为中断控制器,如imx6ull的gpio5
image
对于 gpio5 来说一共有两条信息,中断类型都是 SPI,
触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节:
image
GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应GPIO5_IO00~GPIO5_IO15这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。

使用者:

1
2
3
4
5
6
7
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;
interrupts = <0 8>;
};

fxls8471 有一个中断引脚链接到了 I.MX6ULL 的SNVS_TAMPER0因脚上,这个引脚可以复用为GPIO5_IO00 interrupts设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。

4.1 获取中断号函数#

1
2
3
4
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
int gpio_to_irq(unsigned int gpio)
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq);

irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。

4.2 gic使用示例-按键gpio中断#

4.2.1 dts描述#

我们驱动一个按键,采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来:

1
2
3
4
5
6
7
8
9
10
key {
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
status = "okay";
};

可以看到key使用key-gpio使用GPIO1_IO18, 可以看到也用到了gpio中断源interrupts,IRQ_TYPE_EDGE_BOTH定义在include/linux/irq.h:

1
2
3
4
5
6
7
8
9
10
11
enum {
IRQ_TYPE_NONE = 0x00000000,
IRQ_TYPE_EDGE_RISING = 0x00000001,
IRQ_TYPE_EDGE_FALLING = 0x00000002,
IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING |
IRQ_TYPE_EDGE_RISING),
IRQ_TYPE_LEVEL_HIGH = 0x00000004,
IRQ_TYPE_LEVEL_LOW = 0x00000008,
IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW |
IRQ_TYPE_LEVEL_HIGH),
};

4.2.2 驱动代码与分析#

驱动代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define IMX6UIRQ_CNT 1 /* 设备号个数 */
#define IMX6UIRQ_NAME "imx6uirq" /* 名字 */
#define KEY0VALUE 0X01 /* KEY0按键值 */
#define INVAKEY 0XFF /* 无效的按键值 */
#define KEY_NUM 1 /* 按键数量 */

/* 中断IO描述结构体 */
struct irq_keydesc {
int gpio; /* gpio */
int irqnum; /* 中断号 */
unsigned char value; /* 按键对应的键值 */
char name[10]; /* 名字 */
irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
atomic_t keyvalue; /* 有效的按键键值 */
atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */
struct timer_list timer;/* 定义一个定时器*/
struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */
unsigned char curkeynum; /* 当前的按键号 */
};

struct imx6uirq_dev imx6uirq;

static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */
return IRQ_RETVAL(IRQ_HANDLED);
}
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

num = dev->curkeynum;
keydesc = &dev->irqkeydesc[num];

value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
if(value == 0){ /* 按下按键 */
atomic_set(&dev->keyvalue, keydesc->value);
}
else{ /* 按键松开 */
atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */
}
}

static int keyio_init(void)
{
unsigned char i = 0;
int ret = 0;

imx6uirq.nd = of_find_node_by_path("/key");
if (imx6uirq.nd== NULL){
printk("key node not find!\r\n");
return -EINVAL;
}
for (i = 0; i < KEY_NUM; i++) {
imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
if (imx6uirq.irqkeydesc[i].gpio < 0) {
printk("can't get key%d\r\n", i);
}
}
for (i = 0; i < KEY_NUM; i++) {
memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name)); /* 缓冲区清零 */
sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */
gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);
imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif
printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio,
imx6uirq.irqkeydesc[i].irqnum);
}
imx6uirq.irqkeydesc[0].handler = key0_handler;
imx6uirq.irqkeydesc[0].value = KEY0VALUE;

for (i = 0; i < KEY_NUM; i++) {
ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
if(ret < 0){
printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
return -EFAULT;
}
}
init_timer(&imx6uirq.timer);
imx6uirq.timer.function = timer_function;
return 0;
}
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
filp->private_data = &imx6uirq; /* 设置私有数据 */
return 0;
}
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keyvalue = 0;
unsigned char releasekey = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

keyvalue = atomic_read(&dev->keyvalue);
releasekey = atomic_read(&dev->releasekey);
if (releasekey) { /* 有按键按下 */
if (keyvalue & 0x80) {
keyvalue &= ~0x80;
ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
} else {
goto data_error;
}
atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
} else {
goto data_error;
}
return 0;
data_error:
return -EINVAL;
}
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
};
static int __init imx6uirq_init(void)
{
if (imx6uirq.major) {
imx6uirq.devid = MKDEV(imx6uirq.major, 0);
register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
} else {
alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
imx6uirq.major = MAJOR(imx6uirq.devid);
imx6uirq.minor = MINOR(imx6uirq.devid);
}
cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.class)) {
return PTR_ERR(imx6uirq.class);
}
imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
if (IS_ERR(imx6uirq.device)) {
return PTR_ERR(imx6uirq.device);
}

atomic_set(&imx6uirq.keyvalue, INVAKEY);
atomic_set(&imx6uirq.releasekey, 0);
keyio_init();
return 0;
}
static void __exit imx6uirq_exit(void)
{
unsigned int i = 0;
del_timer_sync(&imx6uirq.timer);
for (i = 0; i < KEY_NUM; i++) {
free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
gpio_free(imx6uirq.irqkeydesc[i].gpio);
}
cdev_del(&imx6uirq.cdev);
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
device_destroy(imx6uirq.class, imx6uirq.devid);
class_destroy(imx6uirq.class);
}
分析: ![image](字符设备驱动-9-中断子系统-GICv2架构解析/37.png) 从key节点取出key-gpio,得到gpio编号 调用gpio请求配置函数,配成input模式 根据key节点信息解析出中断号,或者gpio编号转成中断号.(这里用到一个函数`irq_of_parse_and_map`) 注册中断 创建定时器用来消抖

中断响应过程:
image
image
按键按下或松开,中断产生调用key0_handler,修改定时器超时10ms, 如果是抖动那么,定时器中断那么不会触发(原理请参考[字符设备驱动-9.内核定时器 - fuzidage - 博客园 (cnblogs.com)]

字符设备驱动-8-内核定时器 | Hexo (fuzidage.github.io),只有当不是抖动真正按下或松开,定时器中断触发进行读取按键,原子操作设置键值。releasekey置1表示一次完整的按下松开。
image
最后用户调用read, 返回键值。可见releasekey很好的控制着按键按下和read的次数,比如当连续read 2次但是只按了一次,则读取失败。
image

硬中断和虚拟中断号的映射关系可以用 /proc/interrupts 查看:

img

通过 ps 命令可以查看系统中的中断线程,注意这些线程是实时线程 SCHED_FIFO:

1
2
3
4
5
6
7
8
9
10
# ps -A | grep "irq/"
root 1749 2 0 0 irq_thread 0 S [irq/433-imx_drm]
root 1750 2 0 0 irq_thread 0 S [irq/439-imx_drm]
root 1751 2 0 0 irq_thread 0 S [irq/445-imx_drm]
root 1752 2 0 0 irq_thread 0 S [irq/451-imx_drm]
root 2044 2 0 0 irq_thread 0 S [irq/279-isl2902]
root 2192 2 0 0 irq_thread 0 S [irq/114-mmc0]
root 2199 2 0 0 irq_thread 0 S [irq/115-mmc1]
root 2203 2 0 0 irq_thread 0 S [irq/322-5b02000]
root 2361 2 0 0 irq_thread 0 S [irq/294-4-0051]

字符设备驱动-9-中断子系统-中断引入

1 中断与异常种类#

image
CPU 在运行的过程中,也会被各种“异常”打断。这些“异常”有:

  1. 指令未定义
  2. Reset复位
  3. 指令、数据访问有问题
  4. SWI(软中断)
  5. 快中断
  6. IRQ中断

IRQ中断只是一种(一类)异常而已。导致中断发生的情况有很多,比如:

  1. 按键
  2. 定时器
  3. ADC 转换完成
  4. UART 发送完数据、收到数据

这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知 CPU。如上图所示:

2 中断的处理流程#

arm 对异常(中断)处理过程:

  1. 初始化:
    a) 设置中断源,让它可以产生中断
    b) 设置中断控制器(可以屏蔽某个中断,优先级)
    c) 设置 CPU 总开关(使能中断)
  2. 执行其他程序:正常程序
  3. 产生中断:比如按下按键(中断源发出中断请求)—>中断控制器—>CPU
  4. CPU 每执行完一条指令都会检查有无中断/异常产生
  5. CPU 发现有中断/异常产生,开始处理。

对于不同的异常,跳去不同的地址执行程序。这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。③④⑤都是硬件做的。③是中断源来做,④⑤是cpu来做

综上5个过程,软件要做的事情:
a) 保存现场(各种寄存器)
b) 处理异常(中断): 从异常向量表跳到不同的异常向量去执行,分辨中断源,再调用不同的处理函数
c) 恢复现场

2.1 异常向量表#

可以参考我之前写的s3c2440裸机-异常中断(一. 异常、中断的原理与流程) 介绍了异常向量表。
uboot中就有大量类似这种的异常向量表,不同系列芯片每个异常的偏移地址会有所不同。下图以s3c2440芯片为例:
image
image

向量地址 中断类型 中断模式
0X00 复位中断(Rest) 特权模式(SVC)
0X04 未定义指令中断(Undefined Instruction) 未定义指令中止模式(Undef)
0X08 软中断(Software Interrupt,SWI) 特权模式(SVC)
0X0C 指令预取中止中断(Prefetch Abort) 中止模式
0X10 数据访问中止中断(Data Abort) 中止模式
0X14 未使用(Not Used) 未使用
0X18 IRQ 中断(IRQ Interrupt)
0X1C FIQ 中断(FIQ Interrupt)

这就是异常向量表,每一条指令对应一种异常。
发生复位时,CPU 就去 执行第 1 条指令:b reset
发生中断时,CPU 就去执行“ldr pc, _irq”这条指令。这些指令存放的位置是固定的,比如对于ARM9芯片中断向量的地址是0x18。当发生中断时,CPU 就强制跳去执行 0x18 处的代码。

2.1.1 中断向量表偏移(vector base)#

在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU 就会执行向量表中的跳转指令,去调用更复杂的函数。当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如imx6ull芯片设置 vector base 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

2.2 GIC概述#

对于 ARM 的中断控制器,述语上称之为 GIC (Generic Interrupt Controller),到目前已经更新到 v4 版本了。在STM32叫做NVIC(内嵌向量中断控制器 Nested Vectored Interrupt Controller)
简单地说,GIC v3/v4 用于 ARMv8 架构,即 64 位 ARM 芯片。
GIC v2 用于 ARMv7 和其他更低的32位架构。v2架构下一节:设备驱动-10.中断子系统-5 armv7 GIC架构解析 会展开细说。

v8架构是在32位ARM架构上进行开发的,将被首先用于对扩展虚拟地址和64位数据处理技术有更高要求的产品领域,如企业应用、高档消费电子产品。ARMv8架构包含两个执行状态:AArch64AArch32AArch64执行状态针对64位处理技术,引入了一个全新指令集A64;而AArch32执行状态将支持现有的ARM指令集。

2.3 保护现场,恢复现场的核心:栈#

中断当前正在运行的进程、线程。进程、线程是什么?内核如何切换进程、线程、中断?要理解这些概念,必须理解栈的作用。

进程是资源分配的基本单位,线程是调度的基本单位。
比如全局变量a, 对不同线程它是共享的,但是这个资源a是属于该进程独立的资源,对其他进程是不可见的。
一个进程可以包含多个线程,线程有自己的栈空间,也就是局部变量。

2.3.1 ARM 处理器程序运行的过程#

ARM 芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

对内存只有读、写指令
对于数据的运算是在 CPU 内部实现
使用 RISC 指令的 CPU 复杂度小一点,易于设计

比如对于a=a+b这样的算式,需要经过下面 4 个步骤才可以实现:
image
我们先忽略各种 CPU 模式(系统模式、用户模式等等)。详细过程如下:
image

1
2
3
4
5
6
7
8
9
LDR R0, [a]
LDR R1, [b]
ADD R0, R0, R1
STR R0, [a]
/*翻译如下:
把内存 a 的值读入 CPU 寄存器 R0
把内存 b 的值读入 CPU 寄存器 R1
把 R0、R1 累加,存入 R0
把 R0 的值写入内存 a*/

2.3.2 入栈保护现场/出栈恢复现场#

当进行函数调用跳转到下一个函数,又或者中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。保存的寄存器那块内存就称为栈空间。
当跳转的函数执行完成,就需要从栈中恢复那些 CPU 内部寄存器的值,这一出栈的过程也被叫做“恢复现场”

①函数调用:

1.在函数 A 里调用函数 B,实际就是中断函数 A 的执行。
2.那么需要把函数 A 调用 B 之前瞬间的 CPU 寄存器的值,保存到栈里

②中断处理:

a) 进程 A 正在执行,这时候发生了中断。
b) CPU 强制跳到中断异常向量地址去执行,
c) 这时就需要保存进程 A 被中断瞬间的 CPU 寄存器值,
d) 可以保存在进程 A 的内核态栈,也可以保存在进程 A 的内核结构体中。
e) 中断处理完毕,要继续运行进程 A 之前,恢复这些值

③进程切换:
进程 A 的时间用完了,就切换到进程 B。怎么切换?切换过程是发生在内核态里的,跟中断的处理类似。
a) 进程 A 被切换瞬间的 CPU 寄存器值保存在某个地方;
b) 恢复进程 B 之前保存的 CPU 寄存器值,这样就可以运行进程 B 了。
image
总结有3种场景会要用到栈去保存和恢复现场:

①函数调用,②进程切换,③中断过程。进程调度核心就是靠定时器中断来实现

2.4 硬件中断、软件中断#

2.4.1 硬中断#

硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断,定时器中断的处理函数肯定不一样。
为方便理解,可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:一个中断号对应一个中断服务函数
image

2.4.2 软中断#

相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:
image

  1. 软件中断何时生产?
    由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断。
  2. 软件中断何时处理?
    软件中断嘛,并不是那么十万火急,有空再处理它好了,因此一般软件中断是硬件中断处理完后,顺便来处理软件中断。
  3. 有哪些软件中断?

2.4.2.1 软中断的类型#

查内核源码 include/linux/interrupt.h
image
怎么设置使用软中断,比如tasklet (后面会讲中断上半部分, 和中断下半部分)就是使用软件中断实现的。还有字符设备驱动-8-内核定时器 字符设备驱动-9.内核定时器也是利用软中断实现的。

总结使用软中断的类型有:

1
2
3
4
5
6
7
8
9
优先级为0,HI_SOFTIRQ,高优先级的tasklet
优先级为1,定时器软中断
发送网络数据包的软中断
接受网络数据包的软中断
块设备的软中断
优先级为0,tasklet,低优先级的tasklet
进程调度和负载均衡
高进度定时器
RCU

2.4.2.2 软件中断的API使用#

2.4.2.2.1 注册软中断-open_softirq#

image

1
2
3
void open_softirq(int nr, void (*action)(struct softirq_action *)){
softirq_vec[nr].action = action;
}

例如网络发包对应类型为NET_TX_SOFTIRQ的处理函数net_tx_action

1
2
3
// net/core/dev.c
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
2.4.2.2.2 使能软中断-raise_softirq#

image
最核心的函数是 raise_softirq,简单地理解就是设置 softirq_veq[nr]的标记位,设置后表示使能该软中断号。

2.4.2.2.3 执行软中断-do_softirq#

每个 CPU 上会初始化一个 ksoftirqd 内核线程,负责处理各种类型的 softirq 中断事件:

image-20240810221847189

当注册软中断使能后,ksoftirqd 线程,执行pending的软中断。ksoftirqd 里面会进一步调用到 __do_softirq

image-20240810223529233

可以看到软中断执行,硬件中断是使能的。遍历使能的softlrq,执行对应的action函数。

2.4.3 硬件中断和软中断区别#

image

  1. 硬件中断包含gpio,网卡,外围电路IP等等,tick(产生一次tick系统滴答中断,jiffies加1
  2. 软件中断包含TIMER 表示定时中断、RCU 表示 RCU 锁中断、SCHED 表示内核调度中断

image

区别:
上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行
硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行

cat /proc/softirqs可以看软件中断信息

1
2
3
4
5
6
7
8
9
10
11
12
$ cat /proc/softirqs
CPU0 CPU1 ... CPU46 CPU47
HI: 2 0 ... 0 1
TIMER: 443727 467971 ... 313696 270110
NET_TX: 57919 65998 ... 42287 54840
NET_RX: 28728 5262341 ... 81106 55244
BLOCK: 261 1564 ... 268986 463918
IRQ_POLL: 0 0 ... 0 0
TASKLET: 98 207 ... 129 122
SCHED: 1854427 1124268 ... 5154804 5332269
HRTIMER: 12224 68926 ... 25497 24272
RCU: 1469356 972856 ... 5961737 5917455

cat /proc/interrupts可以看硬件中断

2.5 中断处理原则#

2.5.1 原则 1:不能嵌套#

中断 A 正在处理的过程中,假设又发生了中断 B,那么在栈里要保存 A 的现场,然后处理 B。在处理 B 的过程中又发生了中断 C,那么在栈里要保存 B 的现场,然后处理C。
如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。
为了防止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上规定中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。

1
local_irq_disable();

2.5.2 原则 2:越快越好#

在单核心芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。

2.5.3 原则 3:耗时久的中断操作切分为中断上半部、下半部#

当处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在irq_handler 中等待吗?对于计算机来说,这可是一个段很长的时间。又比如图像处理中,当一个硬件IP处理完成一张图像的操作,那么对这张图像的后处理操作难道要放在中断服务中来操作嘛,显然这个耗时是非常久的。
那么中断操作切分为中断上半部、下半部。上半部分关中断,清中断执行关键紧急的事情,下半部分去处理耗时久的事情,如下图:
image
中断下半部的实现有很多种方法: ①tasklet(小任务)、②work queue(工作队列), ③threaded irq等。

2.5.4 原则 4:上半部和下半部均不能休眠#

中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

2.6 中断下半部处理方法#

2.6.1 小任务tasklet#

tasklet 是使用软中断来实现的:
image
中断上半部和下半部的处理流程:
image
image

1
2
3
4
5
6
7
8
9
10
1. 中断源产生中断,执行irq_enter(), 最开始preempt_count=0,preempt_count++后为1, 
generic_handle_irq中会找到该中断源对应的中断服务程序
2. 执行irq函数,执行中断上半部,(注意执行上半部分时是无法被中断的,调用了local_irq_disable())
3. irq_exit(),preempt_count-- 后为0
4. 判断preempt_count是否等于0,此时等于0,也就是执行下半部分,也叫做软中断流程
5. 下半部过程中会对preempt_count++,开始软件中断
6. 由于是软件中断,开总中断,允许其他的硬件中断响应local_irq_enable();
7. 根据软中断号找到服务函数,执行所谓的中断下半部分(可以进行耗时的一些操作,因为有使能中断)
8. 下半部分执行完后,local_irq_disable()
9. preempt_count--,preempt_count又回到0

上半部中断(硬件中断)有local_irq_disable(),中断是不允许被另一个中断打断的。而下半部(软件中断)时中断是开的,它可以被其他中断打断local_irq_enable()

那么软中断(下半部)A还没有执行到⑨preempt_count--,当被其他中断B打断时,又执行①preempt_count++,等于2,又进入了下一个硬件中断B流程。当下一个硬件中断B流程执行完后,preempt_count--,等于1,此时不会进入软总断流程直接结束,然后恢复A中断的下半部,继续执行完A中断下半部分的代码。
image
那这样B的下半部怎么执行呢?难道不要了吗?注意:步骤7中的中断下半部处理过程中,它处理的是所有中断的下半部分,处理完A的下半部后会继续处理B中断的下半部。所以,多个中断的下半部,是汇集在一起处理的。
总结:

1
2
3
4
5
1.中断的处理可以分为上半部,下半部
2.中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
3.中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的.
中断下半部执行时,有可能会被其他硬件中断打断
4.中断上半部、下半部的执行过程中,不能休眠

2.6.2 工作队列workqueue#

如果下半部要做的事情太多,那么tasklet就有点不太符合需求了,我们希望建立一个线程来专门执行中断后处理,用内核线程来做:在中断上半部唤醒内核线程。
在linux操作系统中,有一个内核线程**kworker** 线程,是系统帮我们创建的。内核中有很多这样的线程:
image
kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。

1.创建 work
image

2.要执行这个函数时,把 work 提交给 work queue 就可以了
image
上述函数会把 work 提供给**系统默认的 work queue:system_wq**,它是一个队列。schedule_work 函数不仅仅是把 work 放入队列,还会把kworker 线程唤醒。

3.什么时候把 work 提交给 work queue
在中断场景中,可以在中断上半部调用 schedule_work 函数。
因此耗时久的中断下半部分,应该利用线程化处理方式,比如使用工作队列workqueue,上半部调用schedule_work 函数,触发 work 的处理。

2.6.3 threaded irq#

image
threaded_irq:下半部也是利用线程化处理。前面的workqueue处理使用方法太麻烦,需要在上半部进行work定义,schedule_work操作。

1
2
参数handler:上半部分可以为空
参数:thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数

以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个CPU 上?
新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。

字符设备驱动-8-内核定时器

1 引入定时器#

前面的阻塞非阻塞IO, 休眠唤醒,poll查询,异步通知小结内容都是针对按键驱动为例进行的演示。

字符设备驱动-8.休眠唤醒机制

字符设备驱动-6-pre-休眠唤醒机制 | Hexo (fuzidage.github.io)引入了中断,当按键按下会记录按键信息,理想状况是按下一次按键记录一组数据,但实际上按下机械振动导致电平反复跳动最后才稳定,按下一次gpio irq会触发多次,这个被叫做“抖动”,那么可以利用定时器进行“去抖”
image

1.0 定时器timer_list结构#

image

linux_5.10版本struct timer_list变成了
image

1
2
3
unsigned long expires; //设定的超时的值
void(*function)(unsigned long); //超时处理函数
unsigned long data; //超时处理函数参数

1.1 timer内核函数#

在内核中使用定时器很简单,涉及这些函数(参考内核源码include\linux\timer.h)

  1. setup_timer(timer, fn, data)
    image

linux_5.10版本变成了timer_setup
image

设置定时器,主要是初始化 timer_list 结构体,设置其中的函数、参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//还有一种用宏DEFINE_TIMER去定义和初始化一个timer_list
#define DEFINE_TIMER(_name, _function, _expires, _data) \
struct timer_list _name = \
TIMER_INITIALIZER(_function, _expires, _data)

#define __TIMER_INITIALIZER(_function, _expires, _data, _flags) { \
.entry = { .next = TIMER_ENTRY_STATIC }, \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.flags = (_flags), \
__TIMER_LOCKDEP_MAP_INITIALIZER( \
__FILE__ ":" __stringify(__LINE__)) \
}
  1. void add_timer(struct timer_list *timer)
    image
    向内核添加定时器。timer->expires表示超时时间。
    当超时时间到达 , 就会调用这个函数 :timer->function(timer->data)

  2. int mod_timer(struct timer_list *timer, unsigned long expires)
    image

    修改定时器的超时时间:
    它等同于:

    1
    2
    3
    del_timer(timer);
    timer->expires = expires;
    add_timer(timer);

    但是更加高效。

  3. int del_timer(struct timer_list *timer)
    删除定时器。

1.2 定时器时间单位#

可以在内核源码根目录下用“ ls -a”看到一个隐藏文件.config, 可以看到如下这项:会被内核转换成include/generated/autoconf.hHZ定义头文件uapi/asm-generic/param.hinclude/asm-generic/param.h
image

CONFIG_HZ=100
image
image

这表示内核每秒中会发生 100 次系统滴答中断(tick),这就像人类的心跳一样,这是 Linux 系统的心跳。每发生一次 tick 中断,全局变量 jiffies 就会累加 1
CONFIG_HZ=100 表示每个滴答是 10ms。
定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这 2 种方法:

  1. add_timer 之前,直接修改:
    imer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
    imer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ, 2*HZ HZ是100个10ms, 就相当于 2 秒
  2. add_timer 之后,使用 mod_timer 修改:
    mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
    mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ, 2*HZ 就相当于 2 秒

1.2.1 jiffies与秒的转换#

1
2
将 jiffies转换为秒,可采用公式:(jiffies/HZ) 计算。
将秒转换为jiffies,可采用公式:(seconds*HZ) 计算。

1.2.2 内核时间获取#

1
2
3
4
ktime_t curTime = 0;
curTime = ktime_get();//不包含了设备进入休眠的时间
printk("ktime_get:%lld ns", curTime);
//结果:ktime_get:492257307974640 ns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ktime_t curTime = 0;
curTime = ktime_get_boottime();//包含了设备进入休眠的时间
printk("ktime_get_boottime:%lld ns", curTime);
//结果: ktime_get_boottime:581660801601637 ns

void ktime_get_ts64(struct timespec64 *ts); //这也是一种

ktime_t start, end, elapsed;
start = ktime_get_boottime();
...
end = ktime_get_boottime();
elapsed = ktime_sub(end, start);
elapsed_msecs = ktime_to_ms(elapsed);
printk("%d.%03d seconds", elapsed_msecs / 1000,elapsed_msecs % 1000);

1.2.2.1 秒-毫秒-微秒-纳秒和jiffies兑换#

1
2
3
4
5
6
7
8
9
10
int jiffies_to_msecs(const unsigned long j);
int jiffies_to_usecs(const unsigned long j);
u64 jiffies_to_nsecs(const unsigned long j);
long msecs_to_jiffies(const unsigned int m);
long usecs_to_jiffies(const unsigned int u);//jiffies.h
unsigned long nsecs_to_jiffies(u64 n);

static inline s64 ktime_to_us(const ktime_t kt);//ktime.h
static inline s64 ktime_to_ms(const ktime_t kt);
static inline s64 ktime_to_ns(const ktime_t kt);

1.2.2.2 usleep_range#

1
void __sched usleep_range(unsigned long min, unsigned long max)

1.2.3 CLOCK_MONOTONIC与CLOCK_REALTIME#

CLOCK_MONOTONIC(即monotonic time)
CLOCK_MONOTONIC:以绝对时间为准,获取的时间为系统重启到现在的时间,更改系统时间对它没有影响。
字面意义:单调时间,表示系统启动后流逝的时间,由变量jiffies来记录的。
系统每次启动时,jiffies初始化为0。每来一个timer interruptjiffies加1,即它代表系统启动后流逝的tick数。
jiffies一定是单调递增的,因为时间不可逆。

CLOCK_REALTIME(即wall time)
CLOCK_REALTIME:相对时间,从1970.1.1到目前的时间。更改系统时间会更改获取的值。它以系统时间为坐标。
字面意思: wall time挂钟时间,表示现实的时间,由变量xtime来记录的。
一些题外话
一些应用软件可能就是用到了这个wall time。比如以前用vmware workstation,一启动提示试用期已过,但是只要把系统时间调整一下提前一年,再启动就不会有提示了。这很可能就是因为它启动时,用gettimeofday去读wall time,然后判断是否过期,只要将wall time改一下,就可以欺骗过去了。

2 内核定时器实例#

就前面讲到的gpio按键中断来举例,每次发生gpio 中断,irq中我们都去调用一次mod_timer函数判断是不是抖动带来的垃圾数据。如果是抖动,那么irq会一直触发,一直调用mod_timer,一直推迟定时器中断函数的触发。
当按下按键键值稳定下来后机械振动没了,电平数据趋于稳定了,那么gpio irq就不会一直响应了,也就是mod_timer函数不会一直调用了,那么等到timer超时,上报数据,整个流程如下图所示:
image

image
按键为下降沿触发,因此会在t1、t2 和 t3 这三个时刻会触发按键中断,每次进入中断处理函数都会重新开器定时器中断,但是 t1~t2 t2~t3这两个时间段是小于我们设置的定时器中断周期(也就是消抖时间,比如 10ms),所以虽然 t1 开启了定时器,但是定时器定时时间还没到呢 t2 时刻就重置了定时器,最终只有 t3 时刻开启的定时器能完整的完成整个定时周期并触发中断,我们就可以在中断处理函数里面做按键处理了,这就是定时器实现按键防抖的原理。

驱动代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

struct fasync_struct *button_fasync;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void){
return (r == w);
}

static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}

static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static void key_timer_expire(unsigned long data){
struct gpio_key *gpio_key = data;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);

printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf
, size_t size, loff_t *offset){
int err;
int key;

if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;

wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static int gpio_key_drv_fasync(int fd, struct file *file, int on){
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
mod_timer(&gpio_key->key_timer, jiffies + HZ/5);
return IRQ_HANDLED;
}

static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n"
, __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n"
, __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);

setup_timer(&gpio_keys_100ask[i].key_timer
, key_timer_expire
, &gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0;
add_timer(&gpio_keys_100ask[i].key_timer);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr
, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
, "100ask_gpio_key"
, &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key"
, &gpio_key_drv); /* /dev/gpio_key */

gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class
, NULL
, MKDEV(major, 0)
, NULL
, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;

}

static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
del_timer(&gpio_keys_100ask[i].key_timer);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};

static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
return platform_driver_register(&gpio_keys_driver);
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

2.2 实例源码分析#

image

linux_5.10内核版本用法
image

probe函数进行初始化定时器,这里add_timer前修改expires,为了防止定时器中断提前触发(按键还没按下就触发了定时器中断),因此修改expires为一个最大值。
image
当按下按键gpio_irq按键中断服务程序响应后,调用mod_timer进行修改定时器超时时间,用来去除 “抖动”。jiffies + HZ/5表示延时20个tick时钟滴答,也就是20*10ms,如果这个时间内有多次电平跳动,那么不会去上报数据,而是继续修改定时器超时时间,直到按键数据稳定后,也就是200ms已经到了但是没有人去修改定时器超时时间,那么将会触发定时器中断,上报按键数据。
image
linux_5.10内核版本用法:
image

3 深入理解定时器中断#

3.1 提前引入硬件中断和软件中断#

《后面中断子系统会专门介绍》设备驱动-10.中断子系统-1异常中断引入

image

  1. 硬件中断包含gpio,网卡,外围电路IP等等,tick(产生一次tick系统滴答中断,jiffies加1
  2. 软件中断包含TIMER 表示定时中断、RCU 表示 RCU 锁中断、SCHED 表示内核调度中断

image

区别:
上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行
硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行

cat /proc/softirqs可以看软件中断信息

cat /proc/interrupts可以看硬件中断

3.2 定时器底层原理#

定时器就是通过软件中断来实现的,它属于 TIMER_SOFTIRQ 软中断
对于 TIMER_SOFTIRQ 软中断,内核启动时会调用start_kernel初始化init_timers(); 代码如下:

1
2
3
4
5
void __init init_timers(void) {
init_timer_cpus();
init_timer_stats();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。
对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:

1
2
3
4
5
6
7
8
9
run_timer_softirq
__run_timers(base);
while (time_after_eq(jiffies, base->clk)) {
……
expire_timers(base, heads + levels);
fn = timer->function;
data = timer->data;
call_timer_fn(timer, fn, data);
fn(data);

image-20240804152644572

简单地说, add_timer 函数会把 timer 放入内核里某个链表;在 TIMER_SOFTIRQ 的处理函数run_timer_softirq中,会从链表中把这些超时的 timer 取出来,
执行其中的函数。怎么判断是否超时? jiffies 大于或等于 timer->expires 时, timer 就超时,执行超时处理函数。

3.3 找到自己芯片的时钟滴答数-jiffies#

在开发板执行以下命令,可以看到 CPU0 下有一个数值变化特别快,它就是滴答中断tickjiffies也叫做系统节拍数。
image

100ASK_IMX6ULL 为做,滴答中断名字就是“ i.MX Timer Tick”。在 Linux内核源码目录下执行以下命令:

1
2
grep "i.MX Timer Tick" * -nr
drivers/clocksource/timer-imx-gpt.c:319: act->name = "i.MX Timer Tick";

⚫ 打开 timer-imx-gpt.c 319 行左右,可得如下源码:

1
2
3
4
5
act->name = "i.MX Timer Tick";
act->flags = IRQF_TIMER | IRQF_IRQPOLL;
act->handler = mxc_timer_interrupt;
act->dev_id = ced;
return setup_irq(imxtm->irq, act);

mxc_timer_interrupt 应该就是滴答中断的处理函数,代码如下:

1
2
3
4
5
6
7
8
9
10
static irqreturn_t mxc_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *ced = dev_id;
struct imx_timer *imxtm = to_imx_timer(ced);
uint32_t tstat;
tstat = readl_relaxed(imxtm->base + imxtm->gpt->reg_tstat);472 / 573
imxtm->gpt->gpt_irq_acknowledge(imxtm);
ced->event_handler(ced);
return IRQ_HANDLED;
}

在看ced->event_handler(ced);调用:

ced->event_handler(ced)是哪一个函数?不太好找,使用 QEMU 来调试内核,在 mxc_timer_interrupt 中打断点跟踪代码发现它对应 tick_handle_periodic
tick_handle_periodic 位于 kernel\time\tick-common.c 中,它里面的调用关系如下:

1
2
3
4
tick_handle_periodic
tick_periodic(cpu);
do_timer(1);
jiffies_64 += ticks; // jiffies 就是 jiffies_64

为何说jiffies就是 jiffies_64?在 arch\arm\kernel\vmlinux.lds.S 有如下代码:

1
2
3
4
5
#ifndef __ARMEB__
jiffies = jiffies_64;
#else
jiffies = jiffies_64 + 4;
#endif

上述代码说明了,对于大字节序的 CPU, jiffies 指向 jiffies_64 的高 4字节;对于小字节序的 CPU, jiffies 指向 jiffies_64 的低 4 字节。对 jiffies_64 的累加操作,就是对 jiffies 的累加操作。
image

1
2
extern u64 __cacheline_aligned_in_smp jiffies_64; //include/linux/jiffies.h
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;

字符设备驱动-7-异步通知

1.异步通知概述#

前面讲到APP 读取按键方式里面包含4种方式:1.查询方式,2.休眠唤醒,3,poll机制的休眠唤醒,4.异步通知
字符设备驱动-3-GPIO驱动KEY示例 | Hexo (fuzidage.github.io)
什么是异步通知?
你去买奶茶:
◼ 你在旁边等着,眼睛盯着店员,生怕别人插队,他一做好你就知道:你是主动等待他做好,这叫 “同步”。
◼ 你付钱后就去玩手机了,店员做好后他会打电话告诉你:你是被动获得结果,这叫“异步”。
同理,还是以gpio_key的例子,app想要判断按键是否有按下,无需去查询或者休眠,只需要注册SIGIO信号给driver, 当按键按下,driver中会自己主动通知app,给app发送SIGIO信号,app上层收到信号SIGIO会执行事先注册的信号处理函数

异步通知使用信号来实现。在 Linux 内核源文件 include\uapi\asmgeneric\signal.h 中:
image

1.1 sigaction函数#

1
2
3
4
5
第一个参数为信号的值, 可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号。
第二个参数是指向结构体sigaction的一个实例的指针, 在结构体sigaction的实例中,
指定了对特定信号的处理函数, 若为空, 则进程会以缺省方式对信号处理;
第三个参数oldact指向的对象用来保存原来对相应信号的处理函数, 可指定oldact为NULL
如果把第二、 第三个参数都设为NULL, 那么该函数可用于检查信号的有效性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
/*
sa_flags:用来设置信号处理的其他相关操作,下列的数值可用:
1.SA RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值;
2.SIG DFLSA RESTART:如果信号中断了进程的某个系统调用,
则系统自动启动该系统调用;
3.SA NODEFER:当信号处理函数运行时,内核将阻塞该给定信号。
但是如果设置了 SA NODEFER标记, 那么在该信号处理函数运行*/
1
2
3
4
5
6
7
8
9
10
11
12
13
void _SAMPLE_PLAT_SYS_HandleSig(int nSignal, siginfo_t *si, void *arg) {
_SAMPLE_PLAT_ERR_Exit();
exit(1);
}

struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = _SAMPLE_PLAT_SYS_HandleSig;
sa.sa_flags = SA_SIGINFO|SA_RESETHAND; // Reset signal handler to
//system default after signal triggered
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);

1.2 signal函数#

1.2.1 示例1#

按下“Ctrl+C”将向其发出SIGINT信号, 正在运行kill的进程将向其发出SIGTERM信号, 以下代码的进程可捕获这两个信号并输出信号值:

点击查看代码
1
2
3
4
5
6
7
8
9
10
void sigterm_handler(int signo){
printf("Have caught sig N.O. %d\n",signo);
exit(0);
}
int main(void){
signal(SIGINT,sigterm_handler);
signal(SIGTERM,sigterm_handler);
while(1);
return0;
}

可以看到对应SIGINT
image
输入kill [pid]
image
可以看到对应SIGTERM信号
image

1.2.2 示例2#

通过signal(SIGIO, input_handler); 对标准输入文件描述符STDIN_FILENO启动信号机制.用户输入后, 应用程序将接收到SIGIO信号, 其处理函数input_handler()将被调用.

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void input_handler(int num)
{
char data[MAX_LEN];
int len;
/* 读取并输出STDIN_FILENO上的输入 */
len = read(STDIN_FILENO, &data, MAX_LEN);
data[len] = 0;
printf("input available:%s\n", data);
}
int main(void)
{
int oflags;
/* 启动信号驱动机制 */
/*为SIGIO信号安装input_handler()作为处理函数*/
signal(SIGIO, input_handler);
/*设置本进程为STDIN_FILENO文件的拥有者,
没有这一步,内核不会知道应该将信号发给哪个进程*/
fcntl(STDIN_FILENO, F_SETOWN, getpid());
/*而为了启用异步通知机制, 还需对设备设置FASYNC标志,下面两行行代码可实现此目的。 */
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
while(1);
return 0;
}

image

1.1.3 示例3#

手动对app进程发送SIGIO信号

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void my_sig_func(int signo)
{
printf("get a signal : %d\n", signo);
}
int main(int argc, char **argv)
{
int i = 0;
signal(SIGIO, my_sig_func);
while (1)
{
printf("Hello, world %d!\n", i++);
sleep(2);
}
return 0;
}

发送SIGIO信号给进程,也就是kill -29 [pid]
image
image

2 异步通知的实现原理流程#

驱动程序怎么通知 APP:发信号,这只有 3 个字,却可以引发很多问题:

1
2
3
4
5
6
7
1. 谁发:驱动程序发
2. 发什么:信号
3. 发什么信号:SIGIO
4. 怎么发:内核里提供有函数
5. 发给谁:APP,APP 要把自己告诉驱动
6. APP 收到后做什么:执行信号处理函数
7. 信号处理函数和信号,之间怎么挂钩:APP 注册信号处理函数

Linux系统中也有很多信号,内核源文件include\uapi\asmgeneric\signal.h中,有很多信号的宏定义:
image
就APP而言,你想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟SIGIO 挂钩。这可以通过一个 signal 函数来 “给某个信号注册处理函数”,用法如下:
image
APP 还要做什么事?想想这几个问题:

1
2
3
a) 内核里有那么多驱动,你想让哪一个驱动给你发 SIGIO 信号?APP 要打开驱动程序的设备节点。
b) 驱动程序怎么知道要发信号给你而不是别人?APP 要把自己的进程 ID 告诉驱动程序。
c) APP 有时候想收到信号,有时候又不想收到信号应该可以把 APP 的意愿告诉驱动。

驱动程序要做什么?发信号。

1
2
3
4
5
6
7
a) APP 设置进程 ID 时,驱动程序要记录下进程 ID;
b) APP 还要使能驱动程序的异步通知功能,驱动中有对应的函数:APP 打开驱动程序时,
内核会创建对应的 file 结构体,file 中有 f_flags;
c) FASYNC 位:
flags 中有一个 FASYNC 位,它被设置为 1 时表示使能异步通知功能。
当 f_flags 中的 FASYNC 位发生变化时,驱动程序的 fasync 函数被调用;
d) 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。这个辅助函数名为 kill_fasync.

2.1. 异步通知的信号流程#

image
① APP open,在drv中调用drv_open,进行注册按键中断服务函数gpio_key_irq;
② APP 给SIGIO信号注册信号处理函数func, 以后 APP 收到 SIGIO信号时,这个函数会被自动调用;
③ APP 调用fcntl, 把PID(进程 ID)告诉驱动程序,这个调用不涉及驱动程序,只是在内核的文件系统层次sys_cntl,记录 PID在filp中(sys_call会建立filp结构);
④ APP 调用fcntl, 读取驱动程序文件 Flag;
⑤ 设置 Flag 里面的FASYNC位为 1:当 FASYNC 位发生变化时,会导致驱动程序的fasync被调用;
⑥⑦ 调 用 faync_helper , 它 会 根 据FAYSNC的值决定是否设置button_async->fa_file = 驱动文件filp
驱动文件filp结构体里面含有之前设置的 PID。
⑧ APP 可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用kill_fasync 发信号;
⑪⑫⑬ APP 收到信号后,它的信号处理函数被自动调用,可以在里面调用read 函数读取按键。

3.驱动代码编写#

先修改Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts建立dts节点,“100ask,gpio_key”platform drvier中保持一致。这里将goio-keys disabled掉是为了drv去匹配新添加的这个节点。
image

使用异步通知时,驱动程序的核心有2个:
① 提供对应的drv_fasync函数;
② 并在合适的时机发信号。

3.1 开启async#

drv_fasync 函数很简单,调用 fasync_helper 函数就可以,如下:
image
image
fasync_helper 函 数 会 分 配 、 构 造 一 个 fasync_struct 结构体button_async
⚫ 驱动文件的 flag 被设置为 FAYNC 时
button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID
⚫ 驱动文件被设置为非FASYNC 时:
button_async->fa_file = NULL;
以后想发送信号时,使用 button_async 作为参数就可以,它里面 “可能” 含有 PID。

3.2 发SIGIO信号#

什么时候发信号呢?在本例中,在 GPIO 中断服务程序中发信号。
怎么发信号呢?代码如下:

1
kill_fasync(&button_async, SIGIO, POLL_IN);

image
第 1 个参数:button_async->fa_file 非空时,可以从中得到 PID,表示发给哪一个 APP;
第 2 个参数表示发什么信号:SIGIO
第 3 个参数表示为什么发信号:POLL_IN,有数据可以读了。(APP 用不到这个参数)

点击查看完整驱动代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

struct fasync_struct *button_fasync;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void){
return (r == w);
}

static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}

static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf,
size_t size, loff_t *offset){
int err;
int key;

wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static int gpio_key_drv_fasync(int fd, struct file *file, int on){
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);

printk("key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
return IRQ_HANDLED;
}

static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n"
, __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n"
, __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr
, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
,"100ask_gpio_key"
,&gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);

gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL
, MKDEV(major, 0)
, NULL
, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;

}
static int gpio_key_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
return platform_driver_register(&gpio_keys_driver);
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

4.应用编写#

应用程序要做的事情有这几件:

  1. 编写信号处理函数:

    1
    2
    3
    4
    5
    static void sig_func(int sig) {
    int val;
    read(fd, &val, 4);
    printf("get button : 0x%x\n", val);
    }
  2. 注册信号处理函数:

    1
    signal(SIGIO, sig_func);
  3. 打开驱动:

    1
    fd = open(argv[1], O_RDWR);//./aout /dev/100ask_gpio_key
  4. 把进程 ID 告诉驱动:

    1
    fcntl(fd, F_SETOWN, getpid());

    使能驱动的 FASYNC 功能:

    1
    2
    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC);
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
static int fd;
static void sig_func(int sig){
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv){
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
int flags;
if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
signal(SIGIO, sig_func);
fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
fcntl(fd, F_SETOWN, getpid());
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
while (1){
printf("www.100ask.net \n");
sleep(2);
}

close(fd);
return 0;
}

字符设备驱动-6-poll底层驱动机制

1 前言引入#

前面字符设备驱动-3-GPIO驱动KEY示例 | Hexo (fuzidage.github.io)

字符设备驱动-3.gpio驱动(按键) - fuzidage - 博客园 (cnblogs.com)

就引入了poll机制,那么底层驱动的poll机制实现原理到底是什么呢?

1.1 阻塞与非阻塞IO#

APP 调用 open 函数时,不要传入“ O_NONBLOCK”。APP 调用 read 函数读取数据时,为阻塞io
APP 调用 open 函数时,传入“ O_NONBLOCK”表示“非阻塞”。APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read函数会返回数据,否则也会立刻返回错误。这种需要APP反复主动去”轮询”设备,否则无法及时响应。
注意:对于普通文件、块设备文件,O_NONBLOCK不起作用。
注意:对于字符设备文件,O_NONBLOCK 起作用的前提是驱动程序针对O_NONBLOCK做了处理
只能在 open 时表明 O_NONBLOCK 吗?
在 open 之后,也可以通过 fcntl 修改为阻塞或非阻塞:
⚫ open 时设置:

1
2
int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */

⚫ open 之后设置:

1
2
3
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */

驱动O_NONBLOCK flag的话,如果没有数据read函数立即返回。
image

1.2 带超时的阻塞IO(poll/select)#

POLL 机制、SELECT机制是完全一样的,只是 APP 接口函数不一样。简单地说,它们就是 “定个闹钟” :在调用 poll、 select 函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。
使用 poll 时,如果传入的超时时间不为 0,这种访问方法也是阻塞的。
使用 poll 时,可以设置超时时间为 0,这样即使没有数据它也会立刻返回,这就是非阻塞方式。

1.2.1 select#

select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。

那么select会有2个结果:

1
2
3
4
1, 查询到资源,返回查询到的fd总数。
2,没查到,则睡眠
①带timeout参数,timeout后,唤醒退出,此时fd总数为0
②不带timeout, 阻塞且睡眠中,直到有资源可用才唤醒

fd_set结构体就是一个可用资源文件描述符的集合。

1.2.1.1 select使用示例#

1
2
3
4
FD_SET(int fd, fd_set *fdset);       //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd

image

该示例假如传入200us的timeout,表示200us内有被驱动唤醒就可以检测到fd在set集合中,从而调用read读取数据。假如不传入timeout,那么select查询会立即返回。

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FD_ZERO(&wfds);
FD_SET(fd, &wfds);
tv.tv_sec = 0;
tv.tv_usec = 100 * 1000;

struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);

ret = select(fd + 1, NULL, &wfds, NULL, &tv)) == -1);
if (ret == -1) {
printf("select error(%s)\n", strerror(errno));
return ret;
}

if (ret == 0) {
printf("select timeout\n");
ret = -1;
return ret;
}

if (FD_ISSET(fd, &wfds)) {
//start write data to drv
}

该示例传入100ms的timeout,表示100ms内有被驱动唤醒就可以检测到fd在set集合中,从而start write data to drv, 否则select timeout.

1.2.2 poll#

使用休眠唤醒机制,实现简单。比如前面所讲的一个按键字符设备驱动中,read函数中进行等待队列wait_event, 然后当按键按下,中断服务程序进行唤醒等待队列wake_up,read函数将会从休眠中唤醒返回数据给用户。

但是这种有一个缺点,如果要等很久,那么这种方式明显不好。 如果按键一直不去按下,read函数将会一直休眠,应用程序用户线程被一直阻塞。

1. 那么poll机制就是给它加一个超时机制,防止一直休眠和用户线程被阻塞。
2. APP 不知道驱动程序中是否有数据,可以先调用 poll 函数查询一下, poll 函数可以传入超时时间;
3. APP 进入内核态,调用到驱动程序的 poll 函数
    3.1 如果发现没有数据时就休眠一段时间;当超时时间到了之后,内核也会唤醒 APP;
    3.2 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
4. APP 根据 poll 函数的返回值就可以知道是否有数据,如果有数据就调用read 得到数据

1.2.2.1 polls使用示例#

poll/select 监测的事件
image

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct pollfd fds[1];
nfds_t nfds = 1;
while (1) {
fds[0].fd = fd;
fds[0].events = POLLIN;
fds[0].revents = 0;
ret = poll(fds, nfds, 5000);
if (ret > 0) {
if (fds[0].revents == POLLIN) {
while (read(fd, &event, sizeof(event)) == sizeof(event)) {
printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);
}
}
} else if (ret == 0) {
printf("time out\n");
} else {
printf("poll err\n");
}
}
  1. 打开设备文件。
  2. 设置 pollfd 结构体:
    想查询哪个文件(fd)?
    想查询什么事件(POLLIN)?
    先清除 “返回的事件” (revents)。
    使用 poll 函数查询事件,指定超时时间为 5000(ms)。

1.3 休眠唤醒#

这里由于poll底层机制提前用到了一个休眠唤醒机制,也就是等待队列wait_queue。先提前引入概念,

字符设备驱动-6-pre-休眠唤醒机制 | Hexo (fuzidage.github.io)

字符设备驱动-8.休眠唤醒机制 - fuzidage - 博客园 (cnblogs.com) 有展开细讲。

1.3.1 等待队列#

wait_event_interruptible_timeout:
image

image

1
2
3
4
5
6
7
void init_waitqueue_head(wait_queue_head_t *q);
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
wait_event(wq, condition);
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition);
wait_event_interruptible_timeout(wq, condition, timeout);

1.3.2 等待队列项#

利用等待队列项来实现read函数的阻塞式访问,底层驱动去进行状态切换。下图把wait_event的方式换成等待队列项。
image

3 poll机制驱动底层原理#

3.1 我们期望的poll流程#

我们期望的大致流程如下:
image

1. app进行open, drv进行drv_open,注册好中断服务
2. app进行poll , drv进行drv_poll
3. 第一次如果没有数据到来,那么会执行else进行休眠,加入等待队列。
    要么被中断服务程序唤醒,进入for循环此时有数据到来返回;
    要么超时,也会从等待队列唤醒回来,进入for循环此时返回超时

可以看到会查询判断2次,但实际上内核做的更好,我们drv_poll中只需要(这些流程内核帮我们已经做好了):

1.把线程放入wq等待队列,并不会调用休眠
2.返回event状态

3.2 Linux内核实际的poll机制#

实际内核中poll函数流程如下:内核把poll抽出去了,调用sys_poll
image

1. app进行open, drv进行drv_open,注册好中断服务
2. app进行poll , 内核文件系统进行sys_poll
3. 调用驱动开发者实现的drv_poll
    调用poll_wait,把线程加入wq,但是不会进入休眠
    而是直接返回event状态
4. drv_poll返回后,sys_poll中进行数据判断(如果第一次进入没有数据到来,执行else, 将线程休眠(可以看到休眠是drv_poll上层sys_poll已经帮我们做好了),如果有数据则直接返回,那么就只会进入一次drv_poll)
   sys_poll函数执行else休眠的过程中,会被event唤醒or被超时唤醒
   第2进入for循环执行drv_poll,如果被event唤醒了,则返回数据,否则说明是超时唤醒,返回超时
5.最终内核文件系统sys_poll返回,唤醒userspace线程

可以看到当用户调用poll函数,在底层drv下可能会调用2次drv_poll。用户线程不会被一直阻塞休眠,要么有数据时中断的event唤醒,要么超时唤醒。我们只要实现drv_poll的部分,也就是紫色绿色的提示部分。

4 poll驱动编程实例(gpio key为例)#

使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。在drv_poll 函数中要做 2 件事:

1
2
3
4
5
6
7
8
9
1. 把当前线程挂入队列 wq: poll_wait
a) APP 调用一次 poll,可能会 drv_poll 被调用 2 次,但是我们并不需要把当前线程挂入队列 2 次。
b) 可以使用内核的函数 poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
2. 返回设备状态:
APP 调用 poll 函数时,有可能是查询“有没有数据可以读”: POLLIN
也有可能是查询“你有没有空间给我写数据”: POLLOUT。
所以 drv_poll 要返回自己的当前状态: (POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
a) POLLRDNORM 等同于 POLLIN,为了兼容某些 APP 把它们一起返回。
b) POLLWRNORM 等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。

APP 调用 poll 后,很有可能会休眠。对应的,在中断服务程序中,也要有唤醒操作。
完整驱动代码如下:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void){
return (r == w);
}

static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}

static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()) {
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
int err;
int key;

if (file->f_flags & O_NONBLOCK) { /* 非阻塞访问 */
if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下 */
return -EAGAIN;
} else {
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
}
return 4;
}

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);

printk("key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");/* /sys/class/100ask_gpio_key_class */
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;
}

static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;

device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");

count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};

static int __init gpio_key_init(void){
int err;
err = platform_driver_register(&gpio_keys_driver);
return err;
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

4.1 probe函数分析#

image

定义gpio_key:
image
先确保dts中含有gpio_key设备树节点,才能通过.compatible = "100ask,gpio_key"匹配plateform_deviceplatform_driver, 当insmod ko时probe函数被调用。

1
struct device_node *node = pdev->dev.of_node;//可以从platform_device获取到device_node

image
of_gpio_count可以根据设备树节点获取到gpio的数量。

image
of_get_gpio_flags可以根据设备树节点获取到gpio编号和gpio flags

image
获取gpio描述子和gpio中断号

image
注册中断服务程序gpio_key_isr,当按键按下会触发gpio中断,执行gpio_key_isr.

4.2 drv_poll函数分析#

1
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

这里先定义并初始化一个wait_queue gpio_key_wait,也就是等待队列。(等待队列使用详见kernel下include\linux\wait.h)
image

  1. drv_poll函数中call poll_wait将线程加入等待队列,并返回event

    1. 可以看到如果有数据,返回POLLIN | POLLRDNORM(那么drv_poll只会被调用1次); 回到sys_poll
    2. 如果没有数据则返回0, 也回到sys_poll
  2. 回到sys_poll后发现:

  3. 如果有数据,直接返回

  4. 如果没有数据则进入休眠,当超时或者被event唤醒后,sys_poll又会再次进入drv_poll,此时判断is_key_buf_empty,如果是按键按下触发中断响应,那么就有数据,返回POLLIN | POLLRDNORM,否则无数据表示是被超时唤醒,event为0。这种就是sys_poll调用2次。

3.`sys_poll`最终返回,回到`userspace`线程

4.3 drv_read函数分析#

image
当userspace得知驱动有数据时,调用read函数,进入drv_read中调用wait_event_interruptible
image
这里是利用等待队列,等待队列gpio_key_waitevent为true后,wait_event_interruptible该函数会返回。那么在对应的中断处理函数中,需要call wake_up函数来唤醒等待队列,并且把event设置成true,也就是把wait_event_interruptiblecondition设置成true.

4.4 按键中断服务函数分析#

image
put_key是将event设置成true,这样表示有数据了,is_key_buf_empty非空了。调用wake_up_interrptible函数唤醒等待队列gpio_key_wait,因此drv_read函数就能立马返回数据。

4.5 测试#

测试demo用户态程序如下:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
/*
* ./button_test /dev/100ask_gpio_key
*
*/
int main(int argc, char **argv){
int fd;
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;

if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}

fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
fds[0].fd = fd;
fds[0].events = POLLIN;

while (1){
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN)){
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}else{
printf("timeout\n");
}
}
close(fd);
return 0;
}

字符设备驱动-6-pre-休眠唤醒机制

1 休眠与唤醒#

image

1.APP 调用 read 等函数试图读取数据,比如读取按键;
2.APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
3.如果 APP 在内核态,也就是在驱动程序中发现没有数据,则 APP 休眠;
4.当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
5.APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回

drv_read函数当没有数据时需要休眠等待有数据唤醒。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒 APP的read函数。

1.0 引入上下文概念#

app执行整个过程如下图,可以把它看作被拆分成2段:被叫做“上下文”
image

1.0.1 进程上下文#

或者这样说:红实线所涉及的代码,都是APP调用的,被叫做进程上下文,drv_read中有数据直接返回数据,没有数据的话让出CPU休眠等待。

1.0.1 中断上下文#

但是按键的中断服务程序,不属于APP的 “上下文”,这是突如其来的,当中断发生时,APP1 正在休眠呢。在 APP 的“上下文”,也就是在 APP的执行过程中,它是可以休眠的。
在中断的处理过程中,也就是gpio_key_irq的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他 APP 啊?
所以:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数, 被叫做中断上下文

1.1 休眠函数#

内核源码:include\linux\wait.h
image
参数1:wq: waitqueue等待队列
用来等待条件值,condition不为0则立即返回
参数2:condition
这可以是一个变量,也可以是任何表达式。表示“一直等待,直到 condition为真

1.2 唤醒函数#

image

1.3 使用#

image

1.read函数中进行wait_event_interruptible,因此需要初始化一个wq, 用init_waitqueue_head函数进行初始化等待队列。

1
2
wait_queue_head_t job_done_wq;
init_waitqueue_head(&job->job_done_wq);

image
image
或者用DECLARE_WAIT_QUEUE_HEAD声明定义初始化一起。
image

1
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

read函数判断是否有key,有key则直接返回,否则wait_event_interruptible休眠。
image

wait_event_interruptible(gpio_key_wait, g_key);

2.如果按键按下了,中断isr进行响应,调用wake_up_interruptible,并且设置condition为true.驱动的read函数将会被唤醒。
image

驱动代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;
static int g_key = 0;
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;

wait_event_interruptible(gpio_key_wait, g_key);
err = copy_to_user(buf, &g_key, 4);
g_key = 0;
return 4;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;

val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
g_key = (gpio_key->gpio << 8) | val;
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv); /* /dev/100ask_gpio_key */
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;
}
static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
int err;
err = platform_driver_register(&gpio_keys_driver);
return err;
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
驱动代码2(使用环形buf存放键值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;
static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void){
return (r == w);
}
static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}
static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}
static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
int key;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
};

static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);

printk("key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}
for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv); /* /dev/gpio_key */
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;
}
static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
int err;
err = platform_driver_register(&gpio_keys_driver);
return err;
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

app代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv){
int fd;
int val;

if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}

fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
while (1){
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
close(fd);
return 0;
}

字符设备驱动-5-设备树函数

1.设备树相关的头文件#

1.处理 DTB

1
2
of_fdt.h // dtb 文件的相关操作函数, 我们一般用不到,
// 因为 dtb 文件在内核中已经被转换为 device_node 树(它更易于使用)

2.处理 device_node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
of.h // 提供设备树的一般处理函数,
// 比如 of_property_read_u32(读取某个属性的 u32 值),
// of_get_child_count(获取某个 device_node 的子节点数)
of_address.h // 地址相关的函数,
// 比如 of_get_address(获得 reg 属性中的 addr, size 值)
// of_match_device (从 matches 数组中取出与当前设备最匹配的一项)
of_dma.h // 设备树中 DMA 相关属性的函数
of_gpio.h // GPIO 相关的函数
of_graph.h // GPU 相关驱动中用到的函数, 从设备树中获得 GPU 信息
of_iommu.h // 很少用到
of_irq.h // 中断相关的函数
of_mdio.h // MDIO (Ethernet PHY) API
of_net.h // OF helpers for network devices.
of_pci.h // PCI 相关函数
of_pdt.h // 很少用到
of_reserved_mem.h // reserved_mem 的相关函数

3.处理 platform_device

1
2
3
4
5
of_platform.h // 把 device_node 转换为 platform_device 时用到的函数,
// 比如 of_device_alloc(根据 device_node 分配设置 platform_device),
// of_find_device_by_node (根据 device_node 查找到 platform_device),
// of_platform_bus_probe (处理 device_node 及它的子节点)
of_device.h // 设备相关的函数, 比如 of_match_device

2.设备树相关的函数#

2.1 找res属性和platform device#

of_find_device_by_node
函数原型为:

1
extern struct platform_device *of_find_device_by_node(struct device_node *np);

设备树中的每一个节点,在内核里都有一个 device_node;你可以使用device_node 去找到对应的 platform_device

platform_get_resource
这 个 函 数 跟 设 备 树 没 什 么 关 系 , 但 是 设 备 树 中 的 节 点 被 转 换 为 platform_device 后,设备树中的 reg 属性、interrupts 属性也会被转换为“resource”。 这时,你可以使用这个函数取出这些资源。
函数原型为:

1
2
3
4
5
6
7
8
/** 
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type // 取哪类资源?IORESOURCE_MEM、IORESOURCE_REG
* // IORESOURCE_IRQ 等
* @num: resource index // 这类资源中的哪一个?
*/
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源; 对于设备树节点中的 interrupts 属性,它对应 IORESOURCE_IRQ 类型的资源。

2.2 找节点#

image

of_find_node_by_path
根据路径找到节点,比如“/”就对应根节点,“/memory”对应 memory 节点。
函数原型:

1
static inline struct device_node *of_find_node_by_path(const char *path);c

of_find_node_by_name
根据名字找到节点,节点如果定义了 name 属性,那我们可以根据名字找到它。
函数原型:

1
extern struct device_node *of_find_node_by_name(struct device_node *from,const char *name);

参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。 但是在设备树的官方规范中不建议使用“name”属性,所以这函数也不建议 使用。
of_find_node_by_type
根据类型找到节点,节点如果定义了 device_type 属性,那我们可以根据类型找到它。
函数原型:

1
extern struct device_node *of_find_node_by_type(struct device_node *from, const char *type);

参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。 但是在设备树的官方规范中不建议使用“device_type”属性,所以这函数也不建议使用。
of_find_compatible_node
根据 device_typecompatible 找到节点,节点如果定义了 compatible 属性,那我们可以根据 compatible 属性找到它。
函数原型:

1
extern struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
  • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。
  • 参数 compat 是一个字符串,用来指定 compatible 属性的值;
  • 参数type 是一个字符串,用来指定 device_type 属性的值,可以传入 NULL。

of_find_node_by_phandle
根据 phandle 找到节点。dts 文件被编译为 dtb 文件时,每一个节点都有一个数字 ID,这些数字 ID 彼此不同。可以使用数字 ID 来找到 device_node。 这些数字 ID 就是 phandle
函数原型:

1
extern struct device_node *of_find_node_by_phandle(phandle handle);

of_find_matching_node_and_match
通过 of_device_id 匹配表来查找指定的节点.

1
2
3
4
5
6
7
//from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
//matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
//match:找到的匹配的 of_device_id。
//返回值:找到的节点,如果为 NULL 表示查找失败
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)

of_get_parent
找到 device_node 的父节点。
函数原型:

1
extern struct device_node *of_get_parent(const struct device_node *node);
  • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。

of_get_next_parent
这个函数名比较奇怪,怎么可能有“next parent”
它实际上也是找到 device_node 的父节点,跟 of_get_parent 的返回结果是一样的。差别在于它多调用下列函数,把 node 节点的引用计数减少了 1。这意味着 调用 of_get_next_parent 之后,你不再需要调用 of_node_put 释放 node 节点。
of_node_put(node);
函数原型:

1
extern struct device_node *of_get_next_parent(struct device_node *node);
  • 参数 from 表示从哪一个节点开始寻找,传入 NULL 表示从根节点开始寻找。

of_get_next_child
取出下一个子节点。
函数原型:

1
extern struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
  • 参数 node 表示父节点;
  • prev 表示上一个子节点,设为 NULL 时表示想找到第 1 个子节点。

不断调用 of_get_next_child 时,不断更新pre参数,就可以得到所有的子节点。
of_get_next_available_child
取出下一个 “可用” 的子节点,有些节点的 status 是“disabled”,那就会跳过这些节点。
函数原型:

1
struct device_node *of_get_next_available_child( const struct device_node *node, struct device_node *prev);
  • 参数 node 表示父节点;
  • prev 表示上一个子节点,设为 NULL 时表示想找到第 1 个子节点。

of_get_child_by_name
根据名字取出子节点。
函数原型:

1
extern struct device_node *of_get_child_by_name(const struct device_node *node, const char *name);
  • 参数 node 表示父节点;
  • name 表示子节点的名字。

2.3 找到属性#

image

of_find_property
内核源码 incldue/linux/of.h 中声明了 device_node 的操作函数,当然也包括属性的操作函数:
函数原型:

1
2
extern struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
//eg: proper = of_find_property(dtsled.nd, "compatible", NULL);
  • 参数np表示节点,我们要在这个节点中找到名为 name 的属性。
  • lenp 用来保存这个属性的长度,即它的值的长度。

2.3.1 找到属性所指向的节点#

1
2
3
ion_heap0: heap_carveout@0 {
memory-region = <&ion_for_npu>;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline struct device_node *of_parse_phandle(const struct device_node *np,
const char *phandle_name, int index)
//例如:
// get reserved memory-region
res_node = of_parse_phandle(np, "memory-region", 0);
if (!res_node) {
dev_err(&pdev->dev, "failed to get memory region node\n");
return -ENODEV;
}
ret = of_address_to_resource(res_node, 0, res);
if (ret) {
dev_err(&pdev->dev, "failed to get reserved region address\n");
return -ENODEV;
}

2.3.2 设备节点找到资源信息#

1
2
static inline int of_address_to_resource(struct device_node *dev, int index,
struct resource *r)

2.4 获取属性的值#

of_get_property
根据名字找到节点的属性,并且返回它的值。
函数原型:

1
2
3
4
5
6
/*
* Find a property with a given name for a given node
* and return the value.
*/
const void *of_get_property(const struct device_node *np, const char *name, int *lenp);
//eg: of_find_property(dtsled.nd, "compatible", NULL);
  • 参数 np 表示节点,我们要在这个节点中找到名为 name 的属性,然后返回它的值。
  • lenp 用来保存这个属性的长度,即它的值的长度。

of_property_count_elems_of_size
根据名字找到节点的属性,确定它的值有多少个元素(elem)。
函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
* of_property_count_elems_of_size - Count the number of elements in a property
*
* @np:
* device node from which the property value is to be read.
* @propname: name of the property to be searched.
* @elem_size: size of the individual element
*
* Search for a property in a device node and count the number of elements of
* size elem_size in it. Returns number of elements on sucess, -EINVAL if the
* property does not exist or its length does not match a multiple of elem_size
* and -ENODATA if the property does not have a value.
*/
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
  • 参数 np 表示节点,我们要在这个节点中找到名为 propname 的属性,然后返回下列结果:
    return prop->length / elem_size;
    在设备树中,节点大概是这样:

    1
    2
    3
    xxx_node {
    xxx_pp_name = <0x50000000 1024> <0x60000000 2048>;
    };
  • 调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 8)时,返回值是 2;

  • 调用 of_property_count_elems_of_size(np, “xxx_pp_name”, 4)时,返回值是 4。

2.5 读整数 u32/u64#

of_property_read_u32
of_property_read_u64

1
2
static inline int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
extern int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);

在设备树中,节点大概是这样:

1
2
3
4
xxx_node {
name1 = <0x50000000>;
name2 = <0x50000000 0x60000000>;
};
  • 调用 of_property_read_u32 (np, “name1”, &val)时,val 将得到值 0x50000000;
  • 调用 of_property_read_u64 (np, “name2”, &val)时,val 将得到值 0x6000000050000000。

读某个整数 u32/u64

1
extern int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value);

在设备树中,节点大概是这样:

1
2
3
xxx_node {
name2 = <0x50000000 0x60000000>;
};
  • 调用 of_property_read_u32 (np, “name2”, 1, &val)时,val 将得到值 0x60000000。

2.6 读数组#

1
2
3
4
int of_property_read_variable_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz_min, size_t sz_max);
int of_property_read_variable_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz_min, size_t sz_max);
int of_property_read_variable_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz_min, size_t sz_max);
int of_property_read_variable_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz_min, size_t sz_max);

在设备树中,节点大概是这样:

1
2
3
xxx_node {
name2 = <0x50000012 0x60000034>;
};

上述例子中属性 name2 的值,长度为 8。

  • 调用 of_property_read_variable_u8_array (np, “name2”, out_values, 1, 10)时, out_values 中将会保存这 8 个字节: 0x12,0x00,0x00,0x50,0x34,0x00,0x00,0x60
  • 调用 of_property_read_variable_u16_array (np, “name2”, out_values, 1, 10)时, out_values 中将会保存这 4 个 16 位数值: 0x0012, 0x5000,0x0034,0x6000
    总之,这些函数要么能取到全部的数值,要么一个数值都取不到;
  • 如果值的长度在 sz_minsz_max 之间,就返回全部的数值;
  • 否则一个数值都不返回。

2.7 读字符串#

1
2
int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
// eg:of_property_read_string(dtsled.nd, "status", &str);
  • 返回节点 np 的属性(名为 propname)的值;
  • (*out_string)指向这个值,把它当作字符串

2.8 其他of函数#

of_device_is_compatible

1
2
int of_device_is_compatible(const struct
device_node *device, const char *compat);

检查设备节点的兼容性, 用于查看节点的 compatible属性是否有包含 compat指定的字符。

of_translate_address
函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:

1
u64 of_translate_address(struct device_node *np, const __be32 *addr)

of_address_to_resource
根据设备节点转成资源信息。

1
2
3
4
5
int of_address_to_resource(struct device_node *dev, int index, struct resource *r);
//dev:设备节点。
//index:地址资源标号。
//r:得到的 resource 类型的资源值。
//返回值:0,成功;负值,失败。

image
IIC、 SPI、 GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存 空间, Linux内核使用 resource结构体来描述一段内存空间。
对于 32位的 SOC来说, resource_size_t是 u32类型的。其中 start表示开始地址, end表示结束地址, name是这个资源的名字, flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件 include/linux/ioport.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define IORESOURCE_MEM          0x00000200
#define IORESOURCE_REG 0x00000300 /* Register offsets */
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000 /* No side effects */
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000 /* size indicates alignment */
#define IORESOURCE_STARTALIGN 0x00080000 /* start field is alignment */
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000 /* forwarded by bridge */
#define IORESOURCE_MUXED 0x00400000 /* Resource is software muxed */
#define IORESOURCE_EXT_TYPE_BITS 0x01000000 /* Resource extended types */
#define IORESOURCE_SYSRAM 0x01000000 /* System RAM (modifier) */
/* IORESOURCE_SYSRAM specific bits. */
#define IORESOURCE_SYSRAM_DRIVER_MANAGED 0x02000000 /* Always detected via a driver. */
#define IORESOURCE_SYSRAM_MERGEABLE 0x04000000 /* Resource can be merged. */
#define IORESOURCE_EXCLUSIVE 0x08000000 /* Userland may not map this resource */
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000 /* No address assigned yet */
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000 /* Driver has marked this resource busy */

常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和IORESOURCE_IRQ

of_iomap
以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数。
of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段:

1
void __iomem *of_iomap(struct device_node *np, int index);

3 使用设备树示例#

3.1 led灯驱动设备树方式实现#

led灯dts定义在根节点下,作为根节点子节点:

1
2
3
4
5
6
7
8
9
10
11
alphaled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};

3.2 驱动程序#

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define DTSLED_CNT 1 /* 设备号个数 */
#define DTSLED_NAME "dtsled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

struct dtsled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
};

struct dtsled_dev dtsled; /* led设备 */

void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}

static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &dtsled; /* 设置私有数据 */
return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}


static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;

retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}

ledstat = databuf[0]; /* 获取状态值 */

if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations dtsled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};

static int __init led_init(void)
{
u32 val = 0;
int ret;
u32 regdata[14];
const char *str;
struct property *proper;

/* 获取设备树中的属性数据 */
/* 1、获取设备节点:alphaled */
dtsled.nd = of_find_node_by_path("/alphaled");
if(dtsled.nd == NULL) {
printk("alphaled node nost find!\r\n");
return -EINVAL;
} else {
printk("alphaled node find!\r\n");
}

/* 2、获取compatible属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL) {
printk("compatible property find failed\r\n");
} else {
printk("compatible = %s\r\n", (char*)proper->value);
}

/* 3、获取status属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
} else {
printk("status = %s\r\n",str);
}

/* 4、获取reg属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
if(ret < 0) {
printk("reg property read failed!\r\n");
} else {
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 10; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}

/* 初始化LED */
#if 0
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
GPIO1_DR = ioremap(regdata[6], regdata[7]);
GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif

/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);

/* 3、设置GPIO1_IO03的复用功能,将其复用为
* GPIO1_IO03,最后设置IO属性。
*/
writel(5, SW_MUX_GPIO1_IO03);

/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);

/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);

/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);

/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (dtsled.major) { /* 定义了设备号 */
dtsled.devid = MKDEV(dtsled.major, 0);
register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */
dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
}
printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);

/* 2、初始化cdev */
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);

/* 3、添加一个cdev */
cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

/* 4、创建类 */
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
return PTR_ERR(dtsled.class);
}

/* 5、创建设备 */
dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
if (IS_ERR(dtsled.device)) {
return PTR_ERR(dtsled.device);
}

return 0;
}

static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

/* 注销字符设备驱动 */
cdev_del(&dtsled.cdev);/* 删除cdev */
unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */

device_destroy(dtsled.class, dtsled.devid);
class_destroy(dtsled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

4 根设备树扫描流程#

4.1 解析root dts总览#

1
2
3
4
5
6
7
8
9
10
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
mdesc = setup_machine_fdt(__atags_pointer);
...
arm_memblock_init(mdesc);
...
unflatten_device_tree();
...
}

setup_machine_fdt() 根据传入的设备树dtb的首地址完成一些初始化操作。
arm_memblock_init() 主要是内存相关,为设备树保留相应的内存空间,保证设备树dtb本身存在于内存中而不被覆盖。
unflatten_device_tree()对设备树具体的解析,将设备树各节点转换成相应的struct device_node结构体.

image-20240728203202947

4.1.1 setup_machine_fdt#

参数__atags_pointer就是 r2 的寄存器值,是设备树在内存中的起始地址。

1
2
3
4
5
6
7
8
9
10
11
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys))) ——————part 1
return NULL;

mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); ——————part 2

early_init_dt_scan_nodes(); ——————part 3
...
}
  1. 第一部分先将设备树在内存中的物理地址转换为虚拟地址,uboot 传递给内核的设备树地址为物理地址,因为设备树被放置在内存的线性映射区,因此可以简单地通过偏移计算得出其对应的虚拟地址,然后再early_init_dt_verify检查该地址上是否有设备树的魔数(magic)。检查设备树是否匹配成功。最后将设备树地址赋值给全局变量 initial_boot_params

  2. of_flat_dt_match_machine(mdesc_best, arch_get_next_mach),逐一读取dts根目录下的 compatible 属性, 返回machine_desc结构体。

  3. 第三部分就是扫描设备树中的各节点:

    1
    2
    3
    4
    5
    6
    7
    void __init early_init_dt_scan_nodes(void) {
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    //boot_command_line是一个静态数组,存放着启动参数,
    //而of_scan_flat_dt()函数的作用就是扫描设备树中的节点,然后对各节点分别调用传入的回调函数。
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
    }

    这3个函数分别是处理chosen节点、root节点中除子节点外的属性信息、memory节点。

    1
    2
    3
    4
    5
    6
    7
    int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data){
    ...
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
    strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
    ...
    }

获取 bootargs,然后将bootargs放入boot_command_line中,作为启动参数,而并非处理整个chosen节点。

再看第二个函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{
dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
if (prop)
dt_root_size_cells = be32_to_cpup(prop);
prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
if (prop)
dt_root_addr_cells = be32_to_cpup(prop);
...
}

将 root 节点中的#size-cells#address-cells属性提取出来,并非获取root节点中所有的属性,放到全局变量dt_root_size_cellsdt_root_addr_cells中。

接下来看第三个函数调用:

1
2
3
4
5
6
7
8
9
10
11
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data){
...
if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
return 0;
reg = of_get_flat_dt_prop(node, "reg", &l);
endp = reg + (l / sizeof(__be32));
while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
base = dt_mem_next_cell(dt_root_addr_cells, &reg);
size = dt_mem_next_cell(dt_root_size_cells, &reg);
early_init_dt_add_memory_arch(base, size);
}

函数先判断节点的unamememory@0,如果不是,则返回。然后将所有memory相关的reg属性取出来,根据address-cellsize-cell的值进行解析,然后调用early_init_dt_add_memory_arch()来申请相应的内存空间。

到这里,setup_machine_fdt()函数对于设备树的第一次扫描解析就完成了,主要是获取了一些设备树提供的总览信息。

4.1.2 arm_memblock_init#

1
2
3
4
5
6
void __init arm_memblock_init(const struct machine_desc *mdesc) {
...
early_init_fdt_reserve_self();
early_init_fdt_scan_reserved_mem();
...
}

扫描设备树节点中的"reserved-memory"节点,为其分配保留空间。

4.1.3 unflatten_device_tree#

image

第一步是__unflatten_device_tree函数:

image

unflatten_dt_nodes被调用两次,第一次是扫描得出设备树转换成device node需要的空间,然后系统申请内存空间,第二次就进行真正的解析工作。

4.1.3.1 unflatten_dt_nodes遍历子节点#

image

从根节点开始,对子节点依次调用populate_node(),从函数命名上来看,这个函数就是填充节点,为节点分配内存。

4.1.3.1.1 populate_node#

image

为当前节点申请内存空间,使用of_node_init() 函数对node进行初始化,populate_properties设置node属性。populate_properties设置节点的属性。

4.1.3.2 of_alias_scan#

/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */

这句就能看出它是用来处理aliases,chosen等特殊节点的。处理带有别名的节点,函数实现细节如下:

image

1.先处理chosen节点中的"stdout-path"或者"stdout"属性(两者最多存在其一),然后将stdout指定的path赋值给全局变量of_stdout_options,并将返回的全局struct device_node类型数据赋值给of_stdout,指定系统启动时的log输出。

2.接下来为aliases节点申请内存空间,如果一个节点中同时没有 name/phandle/linux,phandle,则被定义为特殊节点,对于这些特殊节点将不会申请内存空间。

3.of_alias_add添加到alias链表。

5 device_node转换成platform_device#

5.0 转换过程总览#

1
2
3
4
5
6
7
8
9
10
11
12
13
of_platform_default_populate_init()
|
of_platform_default_populate();
|
of_platform_populate();
|
of_platform_bus_create()
_____________________|_________________
| |
of_platform_device_create_pdata() of_platform_bus_create()
_________________|____________________
| |
of_device_alloc() of_device_add()

5.1 device_node转换到platform_device的条件#

  • 一般情况下,只对设备树中根的一级子节点进行转换,也就是子节点的子节点并不处理。但是存在一种特殊情况,就是当某个根子节点的compatible属性为"simple-bus"、"simple-mfd"、"isa"、"arm,amba-bus"时,当前节点中的子节点将会被转换成platform_device节点。
  • 节点中必须有compatible属性。

设备树节点的reginterrupts 资源将会被转换成 platform_device 内的 struct resources 资源。

5.2 转换过程解析#

展开of_platform_device_create_pdata:

image

调用of_device_alloc,可以看到为设备树节点分配了一个dev(struct platform_device),展开of_device_alloc函数:

image

可以看到把设备树节点的属性转成platform_device的io,irq等资源信息。同时将device_node *np指针记录到dev->dev.of_node。这样就建立了设备树节点到platform_device的转换关系。

然后调用of_device_add注册到系统device中去。

最后调用of_platform_bus_create在用户空间创建相应的访问节点。

字符设备驱动-4-设备树

1 引用设备树#

在内核中,使用同一个芯片的板子,它们所用的外设资源不一样,比如 A 板用 GPIO A,B 板用 GPIO B, 如果用plateform_device定义资源信息,那么每次单板硬件资源变动后,都要改驱动程序源码,重新编译驱动,重新加载驱动,非常麻烦。
随着 ARM 芯片的流行,内核中针对这些 ARM 板保存有大量的、没有技术含量的文件。 Linus 大发雷霆:"this whole ARM thing is a f*cking pain in the ass"。于是,Linux 内核开始引入设备树。 设备树并不是重新发明出来的,在 Linux 内核中其他平台如 PowerPC,早就使用设备树来描述硬件了。
设备树只是用来给内核里的驱动程序,指定硬件的信息。

1.1 根文件系统中查看设备树#

image

烧录的dtb文件,显然两者是完全相同的。
image

除了原始的dtb文件,根文件系统还以目录结构的方式呈现dtb文件,在devicetree目录下,则有一个base目录,这个base目录,就对应着根节点。
image

base目录下,每一个节点对应一个目录, 每一个属性对应一个文件. 这些属性的值如果是字符串,可以使用 cat 命令把它打印出来。对于数值,可以用 hexdump 把它打印出来。
image-20240728192831389

进入led目录,里面一共有三个文件,除name外,分别对应着led节点的两个属性,cat属性如下:
image

pin属性的值为0x00 05 00 05,也就是GPF5.
如果节点没有设置name属性,那么转换为device_node时,会将节点自己的名称作为name属性值。 所以这里nameled.
根文件系统下也可以查看platform_device。系统中所有的platform_device,都可以在/sys/devices/platform/路径下查看。
image

另外,系统中所有的platform_device,有来自设备树的,也有来自.c文件中注册的。那么,我们怎么知道哪些platform_device是来自设备树,哪些是来自.c文件中注册的?

答:可以查看该platform_device的相关目录下,是否有of_node,如果有of_node,那么这个platform_device就来自于设备树;否则,来自.c文件。

led为例,进入led目录,可以看到有of_node,说明这个platform_device来自设备树。同时,可以看到这个of_node是一个链接文件,指向的是/sys/firmware/devicetree/base/led。也就是说,可以进入 /sys/devices/platform/<设备名>/of_node 查看它的设备树属性。
image

在/proc下有一个链接文件device-tree,它指向的是/sys/firmware/devicetree/base
image

2 设备树的语法#

设备树文件(dts: device tree source),它需要编译为 dtb(device tree blob)文件,内核使用的是 dtb 文件。

2.1 DTS 文件#

2.1.1 DTS文件的总体布局#

设备树源文件通常以dts为后缀,其总体布局如下:

1
2
3
4
5
6
/dts-v1/;
[memory reservations]
/{
[property definitions]
[child nodes]
}

以上各项的含义分别为:

名称 含义
/dts-v1/ 设备树文件的版本
memory reservations 指定保留内存,内核不会使用保留内存
/ 根节点(使用花括号括出属于根节点的内容)
property definitions 根节点的属性,用来描述硬件
child nodes 孩子节点(使用花括号括出属于孩子节点的内容)

2.1.2 memory reservations的格式#

该项是可选项,如果想要保留一段内存不让内核使用,可用如下格式指定:

1
2
3
4
5
/*
address 指定要保留的内存的起始地址
length 指定要保留的内存的长度
*/
/memreserve/<address><length>;

需要注意的是,address和`length都是64位。

2.1.3 属性的格式#

1
2
• 属性有值 [label:] property-name = value;
• 属性没有值 [label:] property-name;

其中各项的含义:

名称 含义
label 标签
property-name 属性名
value 属性值

2.1.3.1 有关属性名#

属性名的长度为1~31个字符,可以自己取,只要能够提供可以解读该属性名的驱动即可。也有一些属性名有着特定的含义,比如compatible用于表示哪个或哪些驱动支持该设备。对于自己命名的属性,并非所有字符均可组成属性名,它需要由以下字符组成:
image

2.1.3.2 有关属性值#

属性值有以下四种:

  1. array of cells 一个cell就是一个u32类型的数据,一个或多个cell用尖括号括起来,并以空格隔开就可以作为一种合法的属性值,如

    1
    example = <0x1 0x2 0x3>;。
  2. 含有结束符的字符串 如:

    1
    example = "example value";
  3. 字节序列 用方括号括起一个或多个字节,字节之间可用也可不用空格隔开,且字节以两位16进制数表示,如:

    1
    example = [12 34 56 78];
  4. 以上三种值的混合(以逗号隔开) 如:

    1
    compatible = "fsl,mpc8641", "ns16550";

    ◆ 文本字符串(包含’\0’结束符)用双引号表示:

    1
    string-property="a string";

    ◆ Cells(32位无符号整数)用尖括号表示:

    1
    cell-property = <0xbeef 123 0xabcd1234>;

    ◆ 64bit 数据使用 2 个 cell 来表示:

    1
    clock-frequency = <0x00000001 0x00000000>; 

    ◆ 二进制数据用方括号表示:

    1
    binary-property=[0x01 0x23 0x45 0x67];

    ◆ 类型不同的数据的组合也是可以的,但需要用逗号隔开:

    1
    mixed-property="a string", [0x01 0x23 0x45 0x67], <0x12345678>;

    ◆ 逗号同样用于创建字符串列表:

    1
    string-list = "red fish", "blue fish";

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x23 0x34 0x56];
child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
};
};
};

2.1.4 一些特定的属性#

设备树文件中有一些特定的属性,他们拥有约定俗成的名称和含义,在devicetree-specification中,这些属性被称为Standard Properties,我们在使用这些属性时,应当遵守相应的约定。这些属性有很多,我将在下文中介绍它们中的一部分。

2.1.4.1 #address-cells#

该属性的值表示在该节点的子节点的reg属性中,使用使用多少个cell,也即使用多少个u32整数来表示地址(对于32位系统,一个u32整数就够了;而对于64位系统,需要两个u32整数)。

2.1.4.2 #size-cells#

该属性的值表示在该节点的子节点的reg属性中,使用多少个cell,也即使用多少个u32整数来表示大小(一段地址空间的长度)。
image

2.1.4.3 compatible#

其值为一个或多个字符串,用来描述支持该设备的驱动程序。比如,该属性位于根节点时,用于指定内核中哪个machine_desc可以支持本设备,即当前设备与哪些平台兼容。其值的格式一般是"manufacturer, model",其中manufacturer表示厂家,model表示型号(厂家的哪型产品)。
当该属性的值有多个字符串时,从左往右,从最特殊到最一般。举例来说

1
compatible = "samsung,smdk2416"

"samsung, s3c2416" 作为根节点的属性时,第一个字符串指示了一个具体的开发板型号,而第二个字符串要更一般,只指示了SoC的型号。在linux初始化时,会优先找支持"samsung,smdk2416"machine_desc用以初始化硬件,找不到时才退而求其次找到"samsung, s3c2416"

1
2
3
led { 
compatible = “A”, “B”, “C”;
};

“compatible”表示“兼容”,对于某个 LED,内核中可能有 A、B、C 三个驱 动都支持它。

2.1.4.4 model#

其值为一个字符串,用来描述当前设备的型号(单板的名字)。当多个设备的compatible相同时,可以通过model来进一步区分多个设备。

1
2
3
4
{ 
compatible = "samsung,smdk2440", "samsung,mini2440";
model = "jz2440_v3";
};

它表示jz2440_v3这个单板,可以兼容内核中的“smdk2440”,也兼容“mini2440”.

2.1.4.5 phandle#

该属性可以为节点指定一个全局唯一的数字标识符。这个标识符可以被需要引用该节点的另一个节点使用。举例来说,现有一个中断控制器:

1
2
3
4
pic@10000000{
phandle =<1>;
interrupt-controller;
};

还有一个可以产生中断的设备,且这个设备的中断信号线连接到了上述中断控制器,为了描述清楚这种关系,该设备的设备节点就需要引用中断控制器的节点:

1
2
3
another-device-node {
interrupt-parent =<1>;/* 数字1就唯一标识了节点pic@10000000 */
};

2.1.4.6 interrupt-controller#

这是一个没有值的属性,用在中断控制器的设备节点中,以表明这个节点描述的是一个中断控制器。

2.1.4.7 interrupt-parent#

该属性用于可以产生中断,且中断信号连接到某中断控制器的设备的设备节点,用于表示该设备的中断信号连接到了哪个中断控制器。该属性的值通常是中断控制器设备节点的数字标识(phandle),具体示例在上文已经出现过了。

2.1.4.8 reg#

reg属性描述了设备资源在其父总线定义的地址空间内的地址。通俗的说,该属性使用一对或多对(地址,长度)来描述设备所占的地址空间。至于地址和长度使用多少个cell来表示呢?这取决于上文介绍的#address-cells、#size-cells属性的值。
举个例子,当:

1
2
3
#address-cells =<1>;
#size-cells =<1>;
reg = <0x3000 0x20 0xFE00 0x100>;

那么reg = <0x3000 0x20 0xFE00 0x100>,表示该属性所属的设备占据了两块内存空间,第一块是以0x3000为起始的32字节内存块;第二块是以0xFE00为起始的256字节内存块。

2.1.4.8 status#

image

1
2
3
&uart1 {
status = "disabled";
};

2.1.4.9 ranges#

ranges属性值可以为空或者按照 (child-bus-address,parent-bus-address,length)格式编写的数字组成:

1
2
3
4
5
6
7
8
9
10
11
child-bus-address//子总线地址空间的物理地址,由父节点的 #address-cells确定此物理地址所占用的字长。
parent-bus-address//父总线地址空间的物理地址,同样由父节点的 #address-cells确定此物理地址所占用的字长。
length//子地址空间的长度,由父节点的 #size-cells确定此地址长度所占用的字长。

soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
}

ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。

1
2
3
4
5
6
7
8
9
10
11
soc { 
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
};
};

soc定义的 ranges属性,值为 <0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000

serial是串口设备节点, reg属性定义了 serial设备寄存器的起始地址为 0x4600寄存器长度为 0x100。经过地址转换, serial设备可以从 0xe0004600开始进行读写操作,0xe0004600=0x4600+0xe0000000

2.1.4.10 device_type#

此属性也被抛弃了。此属性只能用于 cpu节点或者 memory节点。

1
2
3
4
5
6
7
8
9
10
11
cpus {
#size-cells = <0x0>;
#address-cells = <0x1>;
A53_0: cpu@0 {
reg = <0x0>;
enable-method = "psci";
compatible = "arm,cortex-a53";
device_type = "cpu";
next-level-cache = <&CA53_L2>;
};
}

2.1.5 节点的格式#

节点的格式如下:

1
2
3
4
[label:]node-name[@unit-address]{
[properties definitions]
[child nodes]
};

以uart节点为例:

1
2
3
4
5
6
/ { 
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};

节点名(node-name)长为1~31个字符,这些字符可以是:
image

每个设备节点代表一个设备,因此节点名的命名通常要和设备相应,比如以太网控制器,我们可以给其取名ethernet。考虑到一个SoC中可能有多个以太网控制器,为了做区分,我们通常在其节点名后面加上设备的地址,也就是上文中出现的可选部分@unit-address。仍以以太网控制器为例,假如两个以太网控制器的寄存器组的首地址分别为0xfe0020000xfe003000,那么相应的节点名可以取为ethernet@fe002000ethernet@fe003000
不难看出,设备节点允许嵌套,假设节点b嵌套于节点a中,那么节点a是节点b的父节点。根节点的名字比较特殊,就是一个斜杠/,其他的设备节点都是根节点的孩子,或者孩子的孩子…因此,所有的设备节点呈现出一个树状的层次结构(设备树因此得名),下图就是一个例子:
image

2.1.5.1 推荐的节点名#

关于节点的命名,官方有一些推荐的命名,具体可见devicetree-specification-v0.3的2.2.2节。

2.1.5.2 节点的路径名#

在文件系统中有个术语叫文件的路径名(pathname),在按照树状结构组织的众多文件中,用以唯一标识某个文件。类似的,节点也有路径名的概念。将根节点类比为根目录,以上图为例,其中cpu0节点的路径名为/cpus/cpu@0。我们在给节点命名时,必须保证每个节点拥有唯一的路径名(注意区别于每个节点拥有唯一的节点名)。

2.1.6 一些特殊的节点#

有一些特殊的节点不代表任何设备,而是有着特定的用途,本节就将介绍一些这样的节点。

2.1.6.1 /aliases节点#

/aliases节点应当作为根节点的孩子节点,用于定义一个或多个别名属性,每条别名属性会为一个设备节点的路径名设置一个别名,如下面这个例子所示:

1
2
3
4
aliases {
serial0 ="/simple-bus@fe000000/serial@llc500";
ethernet0 ="/simple-bus@fe000000/ethernet@31c000";
};

别名只能由1~31个下面的字符组成:
image
image

别名通常以数字结尾,比如别名为i2c1,设备树的初始化程序在解析别名属性时,会将数字1记录在struct alias_prop结构的id成员中,使用of_alias_get_id可以获得这个数字。因为本文主要介绍设备树文件的格式,因此这里不再深究这部分内容。

2.1.6.2 /chosen节点#

/chosen节点应当用作根节点的孩子节点,有以下可选属性:
bootargs
stdout-path
stdin-path
顾名思义,该节点可以指定启动参数、标准输出和标准输入,一个例子如下:
image

1
2
3
4
5
6
7
/{
......
chosen {
bootargs ="root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
......
};

上图中chosen节点仅仅设置了属性“ stdout-path”,表示标准输出使用 uart0。但是当我们进入到 /proc/device-tree/chosen目录里面,会发现多了 bootargs属性,原因如下:
do_bootm_linux 函数会通过一系列复杂的调用,最终通过fdt_chosen 函数在chosen 节点中加入
bootargs 属性。
image

do_bootm_linux细节见uboot-5_bootm/bootz启动内核过程 - fuzidage - 博客园 (cnblogs.com)

uboot-bootm和bootz启动内核 | Hexo (fuzidage.github.io)

2.1.6.3 /根节点#

1
2
3
4
5
6
/ { 
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
};

根节点中必须有这些属性:

1
2
3
4
#address-cells // 在它的子节点的 reg 属性中, 使用多少个 u32 整数来描述地址(address) 
#size-cells // 在它的子节点的 reg 属性中, 使用多少个 u32 整数来描述大小(size)
compatible // 定义一系列的字符串, 用来指定内核中哪个 machine_desc 可以支持本设备 (即这个板子兼容哪些平台)
model // 咱这个板子是什么

image
Linux内核通过根节点 compatible属性找到对应的设备的函数调用过程如下:
image

2.1.7 必要的节点和必要的属性#

一个完整的设备树文件(DTS文件),有一些节点是必须要有的,这些必要的节点有:

节点名 节点的必要属性
/ #address-cells、#size-cells、model、compatible
/memory device_type、reg
/cpus #address-cells、#size-cells
/cpus/cpu* device_type、reg、clock-frequency、timebase-frequency
/cpus/cpu*/l?-cache compatible、cache-level

2.1.8 label(标签)的使用#

在上文就提到过标签,只是没有细说,这里就介绍一下标签的使用。首先,标签名可由1~31个字符组成,这些字符可以是:
image

接下来仍以中断控制器的例子来说:

1
2
3
4
5
6
7
8
pic@10000000{
phandle =<1>;
interrupt-controller;
};
......
another-device-node {
interrupt-parent =<1>;/* 数字1就唯一标识了节点pic@10000000 */
};

如果我们使用phandle来标识设备,当设备多了,数字标识符是比较难记忆的,可读性也差,此时可以使用标签:

1
2
3
4
5
6
7
PIC:pic@10000000{
interrupt-controller;
};
......
another-device-node {
interrupt-parent =<&PIC>;/* 用标签来引用设备节点 */
};

还有一种常见的标签的用法,当我们需要修改某设备节点的属性,但又不想直接在原地修改(保持原来的内容不被破坏),此时可以在设备树文件的末尾重写该设备节点的相应属性,从而覆盖之前的内容:

1
2
3
4
5
6
7
8
9
10
11
12
/{
device-node {
p ="xxx";
};
};

/* 重写device-node的属性p */
/{
device-node {/* 因此这样写比较麻烦(特别是在路径比较深的时候) */
p ="yyy";
};
};

那么使用标签的写法就简单很多:标签名DN.

1
2
3
4
5
6
7
8
9
10
/{
DN:device-node {
p ="xxx";
};
}

/* 重写device-node的属性p */
&DN{
p ="yyy";
};

修改节点,节点追加内容:

1
2
3
4
5
6
7
8
9
10
i2c0: i2c@29000000 {
compatible = "snps,designware-i2c";
clocks = <&clk ATHENA2_CLK_I2C0>;
reg = <0x0 0x29000000 0x0 0x1000>;
clock-frequency = <400000>;
#size-cells = <0x0>;
#address-cells = <0x1>;
resets = <&rst RST_I2C0>;
reset-names = "i2c0";
};

再另一个dts中使用i2c0:

1
2
3
4
5
6
7
8
9
10
&i2c0 {
clock-frequency = <100000>;
status = "okay";

mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
};

2.1.9 编写设备树文件#

2.1.9.1 在DTS文件中包含其他文件#

编写设备树文件时,我们通常会把多型设备的共性抽出来,写在DTSI文件(后缀为.dtsi)中,其语法与DTS文件一样。比如,多款使用了am335x的板子,因为使用了同一款SoC,描述设备时肯定会有一些相同的部分,可以把这部分抽出来,写到am335x.dtsi中,然后在具体的某型板子的设备树中包含相应的DTSI文件,包含的方式有:

1
2
/include/ “xxx.dtsi”
#include “xxx.dtsi”

设备树编译器还支持c语言的头文件,因此,如果有需要可以定义一些宏并在设备树文件中使用。

2.1.9.2 如何在设备树文件中描述设备#

2.1.9.2.1 Documentation/devicetree/bindings#

设备树写出来是给驱动程序看的,也就是说驱动程序怎么写的,相应的设备树就该怎么写;或者反过来,先约定好设备树怎么写,在相应的设计驱动。驱动和设备树有着对应的关系,这种对应关系也被称为bindings。具体的:

1
2
对于上游芯片厂商,应当按照devicetree-specification推荐的设备树写法,遵守各种约定,确定好如何规范的描述设备,并提供相应的驱动程序。devicetree-specification-v0.3的第四章给出了一些推荐的做法。
对于下游产品厂商,当使用芯片厂商的芯片做产品时,芯片厂商通常会提供驱动程序和设备树文件编写的参考文档,这些文档位于linux内核源码树的Documentation/devicetree/bindings目录下。如果芯片厂商没提供相应文档的话,就要读驱动的源码,知道驱动怎么写的,自然也就知道如何写设备树了。

2.2 DTB文件#

DTS文件只是文本文件,需要使用设备树编译器(DTC)将其编译为DTB文件(二进制文件),然后才能传递给内核,内核解析的是DTB文件。dtb文件由四个部分组成:
image

2.2.1 struct ftd_header#

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct fdt_header {
fdt32_t magic; /* magic word FDT_MAGIC */
fdt32_t totalsize; /* total size of DT block */
fdt32_t off_dt_struct; /* offset to structure */
fdt32_t off_dt_strings; /* offset to strings */
fdt32_t off_mem_rsvmap; /* offset to memory reserve map */
fdt32_t version; /* format version */
fdt32_t last_comp_version; /* last compatible version */
/* version 2 fields below */
fdt32_t boot_cpuid_phys; /* Which physical CPU id we're
booting on */
/* version 3 fields below */
fdt32_t size_dt_strings; /* size of the strings block */
/* version 17 fields below */
fdt32_t size_dt_struct; /* size of the structure block */
};

totalsize:
这个设备树的size,也可以理解为所占用的实际内存空间。
off_dt_struct:
offset to dt_struct,表示整个dtb中structure部分所在内存相对头部的偏移地址
off_dt_strings:
offset to dt_string,表示整个dtb中string部分所在内存相对头部的偏移地址
off_mem_rsvmap:
offset to memory reserve map,dtb中memory reserve map所在内存相对头部的偏移地址

2.2.2 memory reservation block#

该部分由memory reservations编译得来,由一个或多个表项组成,每一项都描述了一块要保留的内存区域,每项由两个64位数值(起始地址、长度)组成:
image

reserved memory作用:设备驱动-15.内核kmalloc/vmalloc及CMA内存介绍 - fuzidage - 博客园 (cnblogs.com)

Linux内核-kmalloc与vmalloc及CMA内存 | Hexo (fuzidage.github.io)

2.2.3 structure block#

image

2.2.4 strings block#

该部分类似于ELF文件中的字符串表,存储了所有属性名(字符串),考虑到很多节点拥有一些同名的属性,集中存放属性名可以有效的节约DTB文件的空间,存放有属性的structure block部分只需要保存一个32位的偏移值——属性名的起始位置在strings block中的偏移。

2.3 DTB文件的编译#

2.3.1 在内核中直接 make#

进入内核源码的目录,执行如下命令即可编译 dtb 文件。make all命令是编译 Linux源码中的所有东西,包括 zImage,dtb,.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“ make dtbs”命令。

make dtbs V=1

2.3.2 手工编译#

内核目录下 scripts/dtc/dtc 是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include”,而必须使用“/incldue”

编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件:

1
2
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译 dts 为 dtb 
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb // 反编译 dtb 为 dts

DTC工具源码在 Linux内核的 scripts/dtc目录下,scripts/dtc/Makefile文件内容如下:
image

2.3.3 反编译dtb#

cd 板子所用的内核源码目录

1
./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp.dts

2.4 内核对设备树的处理过程#

image

① dts 在 PC 机上被编译为 dtb 文件;
② u-boot 把 dtb 文件传给内核;
③ 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
④ 对于某些 device_node 结构体,会被platform_device 结构体获取资源信息
dts解析过程:
image
最终实际dts解析的函数为unflatten_dt_node

2.4.1 dtb 中每一个节点都被转换为 device_node 结构体#

image

根节点被保存在全局变量 of_root 中,从 of_root 开始可以访问到任意节点。

2.4.2 哪些设备树节点会被转换为 platform_device#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/{ 
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};

i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};

①根节点下含有 compatile 属性的子节点
②含有特定 compatile 属性的节点的子节点 :

如果一个节点的 compatile 属性,它的值是这 4 者之一:"simple-bus","simple-mfd","isa","arm,amba-bus", 那 么 它 的 子结点 ( 需 含compatile 属性)也可以转换为 platform_device。 

③总线 I2C、SPI 节点下的子节点:不转换为platform_device
某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device
比如以下的节点中:

⚫ /mytest 会被转换为 platform_device, 因为它兼容"simple-bus"; 它的子节点/mytest/mytest@0 也会被转换为 platform_device 
⚫ /i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核 中有对应的 platform_driver; 
⚫ /i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 i2c_client。 
⚫ 类似的也有/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为 platform_device, 在内核中有对应的 platform_driver; 
⚫ /spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由 父节点的 platform_driver 决定, 一般是被创建为一个 spi_device。 

2.4.3 节点怎么转换为 platform_device#

⚫ platform_device 中含有 resource 数组, 它来自 device_node 的 reg, interrupts 属性; 
⚫ platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性

3 完整dts示例#

编写设备树之前要先定义一个设备,我们就以 I.MX6ULL这个 SOC为例,我们需要在设备树里面描述的内容如下:

1
2
3
4
5
①、 I.MX6ULL这个 Cortex-A7架构的 32位 CPU。
②、 I.MX6ULL内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)。
③、 I.MX6ULL内部 aips1域下的 ecspi1外设控制器,寄存器起始地址为 0x02008000,大小为 0x4000
④、 I.MX6ULL内部 aips2域下的 usbotg1外设控制器,寄存器起始地址为 0x02184000,大小为 0x4000
⑤、 I.MX6ULL内部 aips3域下的 rngb外设控制器,寄存器起始地址为 0x02284000,大小为 0x4000

3.1 添加 cpus节点#

cpu节点, I.MX6ULL采用 Cortex-A7架构,而且只有一个 CPU,因此只有一个cpu0节点,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/ { 
compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";
cpus {
#address-cells = <1>;
#size-cells = <0>;
//CPU0节点
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
};

3.2 添加 soc节点#

uart iic控制器等等这些都属于 SOC内部外设,因此一般会创建一个叫做 soc的父节点来管理这些 SOC内部外设的子节点:

1
2
3
4
5
6
7
8
9
/ { 
//soc节点
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
};
};

soc节点设置 #address-cells = <1>#size-cells = <1>,这样 soc子节点的 reg属性中起始地占用一个字长,地址空间长度也占用一个字长。ranges属性为空,说明子空间和父空间地址范围相同。

3.2.1 添加 ocram节点#

根据第②点的要求,添加 ocram节点, ocram I.MX6ULL内部 RAM,因此 ocram节点应该是 soc节点的子节点。 ocram起始地址为 0x00900000,大小为128KB(0x20000):

1
2
3
4
5
6
7
8
9
10
11
12
13
//soc节点 
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;

//ocram节点
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>;
};
};

3.2.2 添加 aips1、 aips2和 aips3节点#

IMX6ULL外设控制分为三个域: aips1~3,这三个域分管不同的外设控制器,aips1~3这三个域都属于 soc节点的子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//aips1节点
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
};
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
};
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
};

3.2.2.1 添加 ecspi1、 usbotg1和 rngb节点#

每个域节点下都添加一个外设节点。ecspi1属于 aips1的子节点, usbotg1属于 aips2的子节点, rngb属于 aips3的子节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ecspi1节点 
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "disabled";
};

//usbotg1节点
usbotg1: usb@02184000 {
compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};

//rngb节点
rngb: rngb@02284000 {
compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imx-rng";
reg = <0x02284000 0x4000>;
};