[TOC]

drivers/amba: Linux的AMBA总线与SoC设备驱动核心

drivers/amba 目录是 Linux 内核中用于管理和驱动基于 AMBA (Advanced Microcontroller Bus Architecture) 总线的外设的核心框架。在现代基于 ARM 的片上系统 (SoC) 中,几乎所有的片上外设(如 UART、GPIO、SPI、I2C、定时器等)都挂载在 AMBA 总线上。因此,这个目录是 ARM SoC 平台驱动程序的基石。


一、 历史与背景

这项技术是为了解决什么特定问题而诞生的?

在嵌入式系统中,特别是 SoC 中,外设不是像 PCI 或 USB 设备那样可以在运行时动态发现的(即非“热插拔”或“可枚举”的)。它们是静态地集成在芯片上的,其物理地址和中断号在芯片设计时就已经固定。

早期的 Linux 内核通过在 C 代码(board-*.c 文件)中硬编码这些“平台设备”(platform devices)的信息来支持它们。这种方法导致了巨大的问题:

  1. 内核与硬件紧密耦合: 每支持一款新的开发板,就需要编写一个新的 board-*.c 文件并编译到内核中。
  2. 可维护性差: 内核源码树中充满了描述硬件布局的 C 代码,极其臃肿和混乱。
  3. 缺乏灵活性: 无法实现一个单一的内核镜像(binary image)来支持多款硬件配置相似但略有不同的开发板。

drivers/amba 框架,与设备树 (Device Tree) 机制紧密结合,正是为了解决上述问题而诞生的。它提供了一种标准化的、数据驱动的方式来注册和匹配这些静态的片上外设及其驱动程序。


二、 核心原理与设计

drivers/amba 的核心设计思想是将硬件的描述与硬件的驱动逻辑彻底分离

  • 硬件描述: 由 设备树 (.dts 文件) 提供。设备树以一种结构化的文本格式,描述了 SoC 上有哪些外设、它们的寄存器物理地址、使用的中断号、时钟源等信息。
  • 驱动逻辑: 由 C 语言编写的驱动程序(如 pl011.c)提供。驱动程序包含如何操作硬件寄存器以实现特定功能的代码。

drivers/amba 框架扮演了两者之间的**“红娘”**角色。其工作流程如下:

  1. 设备树解析: 内核启动时,会解析 Bootloader 传递过来的设备树二进制文件 (DTB)。
  2. 总线扫描: 当内核发现设备树中描述了一个 AMBA 总线时,drivers/amba 框架会被激活。
  3. 设备创建: 框架会遍历该 AMBA 总线节点下的所有子节点(每个子节点代表一个外设,如一个 UART)。对于每个子节点,它会在内核中动态地创建一个 struct amba_device 对象。这个对象中包含了从设备树中读取到的所有硬件资源信息(寄存器地址、IRQ 等)。
  4. 驱动匹配: 内核的驱动核心会为这个新创建的 amba_device 寻找一个匹配的 amba_driver。匹配的唯一依据是设备树节点中的 compatible 字符串
  5. 驱动加载 (Probe): 一旦找到 compatible 字符串相匹配的驱动,内核就会调用该驱动的 probe() 函数,并将前面创建的 amba_device 对象作为参数传递给它。
  6. 硬件初始化: 在 probe() 函数中,驱动程序从 amba_device 参数中获取到寄存器地址和中断号等资源,然后进行 ioremap 映射内存、request_irq 注册中断,最终完成硬件的初始化。

主要优势:

  • 硬件和软件解耦: 内核代码不再关心具体的硬件地址。
  • 单一内核镜像: 同一个内核镜像,只需要配合不同的 .dtb 文件,就可以在不同的硬件板子上启动。
  • 标准化: 为所有 AMBA 外设提供了一套统一的设备-驱动模型。

三、 关键数据结构与文件

1. amba.c

这是 AMBA 总线框架的核心实现。它定义了 AMBA 总线类型 (amba_bustype),并实现了设备与驱动的匹配逻辑 (amba_match)

2. pl011.c (以及类似的外设驱动文件)

这是一个具体的AMBA 设备驱动程序范例,用于驱动 ARM PrimeCell PL011 UART(一种非常常见的串口控制器)。

  • struct amba_driver: 这是驱动程序的核心结构,用于向 AMBA 框架“注册”自己。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        static struct amba_driver pl011_driver = {
    .drv = {
    .name = "uart-pl011",
    },
    .probe = pl011_probe,
    .remove = pl011_remove,
    .id_table = pl011_ids,
    };
    ``` * `.probe`: 驱动的入口函数,在设备和驱动匹配成功后被调用。
    * `.remove`: 驱动的卸载函数。
    * `.id_table`: 一个 `amba_id` 结构体数组,**极其关键**。它列出了这个驱动程序能够支持的所有设备的 `compatible` 字符串(或硬件 ID)。

    * **`struct amba_id`**: 定义了匹配规则。
    ```c
    static const struct amba_id pl011_ids[] = {
    {
    .id = 0x00041011,
    .mask = 0x000fffff,
    .data = &vendor_arm,
    },
    // ... 其他ID
    { 0, 0, 0 } /* sentinel */
    };
    • 在现代基于设备树的系统中,匹配更多是依赖 compatible 字符串,而不是硬件 id

3. struct amba_device

这是内核中代表一个物理 AMBA 设备的实例。它由框架根据设备树节点自动创建。

  • 关键字段:
    • struct resource res: 保存了设备的资源,主要是内存映射 I/O (MMIO) 的物理基地址和大小。
    • int irq[NUM_IRQS]: 保存了设备使用的中断号。
    • unsigned int periphid: 从硬件寄存器中读出的设备 ID。
    • struct device dev: 内嵌的通用 device 结构,其中包含了设备树节点指针。

四、 实践:观察与交互

你不能直接“运行”AMBA 框架,但你可以通过设备树和 sysfs 来观察和影响它的行为。

1. 设备树 (.dts) 中描述一个 AMBA 设备

这是一个典型的 UART 设备节点:

1
2
3
4
5
6
7
8
// in soc.dtsi
uart0: serial@4000c000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x4000c000 0x1000>;
interrupts = <0 32 4>; // GIC, SPI Interrupt 32, IRQ_TYPE_LEVEL_HIGH
clocks = <&clkc 15>, <&clkc 18>;
clock-names = "uart_pclk", "apb_pclk";
};
  • compatible = "arm,pl011": 这是最重要的属性drivers/amba/pl011.c 驱动就是因为能匹配这个字符串才被加载的。
  • reg: 定义了该 UART 控制器的寄存器基地址是 0x4000c000,范围是 0x1000 字节。这个信息会被填充到 amba_device->res 中。
  • interrupts: 定义了中断号。这个信息会被填充到 amba_device->irq[0] 中。

2. 在 sysfs 中观察结果

当内核启动后,你可以在 /sys/ 文件系统中看到 AMBA 框架工作的成果:

1
2
3
4
5
6
7
8
9
10
11
# 查看所有注册在 AMBA 总线上的设备
ls /sys/bus/amba/devices/

# 输出可能包含类似
# 4000c000.serial 4001d000.spi ...

# 查看某个具体设备的信息
ls /sys/bus/amba/devices/4000c000.serial/

# 输出可能包含
# driver/ modalias of_node/ power/ resource subsystem/ uevent
  • 4000c000.serial: 设备名通常由其基地址和节点名构成。
  • driver -> ../../../bus/amba/drivers/uart-pl011: 这是一个符号链接,清晰地表明这个设备已经被 uart-pl011 驱动所绑定。
  • of_node: 指向设备树节点的符号链接。
  • resource: 一个可读文件,显示了该设备占用的内存资源,内容就是 reg 属性中定义的值。

五、 总结

drivers/amba 目录提供了一个优雅而强大的框架,用于管理 ARM SoC 上的静态片上外设。它通过将硬件描述(设备树)与驱动逻辑(C代码)分离,并利用 compatible 字符串作为匹配的桥梁,极大地提高了 Linux 内核在 ARM 平台上的可移植性和可维护性。

对于嵌入式 Linux 开发者而言,理解 drivers/amba 的工作原理,就是理解现代 ARM SoC 如何被驱动的根本。它是编写任何与片上外设交互的驱动程序的基础。

drivers/reset/core.c

reset_control_deassert: 将一个外设移出复位状态

此函数是Linux内核复位控制子系统(Reset Control Subsystem)的核心API之一。它的作用是解除对一个硬件外设的复位信号, 允许该外设开始正常工作。这是设备驱动程序初始化过程中的一个关键步骤, 通常在开启外设时钟之后调用。

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
/**
* reset_control_deassert - 解除对复位线的断言 (即让外设脱离复位状态)
* @rstc: 复位控制器句柄
*
* 调用此函数后, 可以保证复位线处于非断言状态(即复位被解除).
* 当使用了 reset_control_(de)assert 后, 消费者(驱动)不能在共享复位线上
* 再使用 reset_control_reset.
*
* 如果 rstc 为 NULL, 这表示这是一个可选的复位, 函数将直接返回0.
*/
int reset_control_deassert(struct reset_control *rstc)
{
/*
* 检查传入的句柄 rstc 是否为 NULL.
* 这种情况发生在驱动请求一个 "可选的(optional)" 复位, 但设备树中并未提供.
* 在这种情况下, 这被认为是合法的, 直接返回成功 (0).
*/
if (!rstc)
return 0;

/*
* 检查 rstc 是否为一个错误指针 (通过 IS_ERR() 宏判断).
* 如果是, 说明获取句柄时就已发生错误. WARN_ON 会打印一条内核警告信息以帮助调试.
* 返回 -EINVAL (无效参数) 错误.
*/
if (WARN_ON(IS_ERR(rstc)))
return -EINVAL;

/*
* 检查这个句柄是否代表一个复位控制器数组 (一组复位线).
* 如果是, 则调用专门处理数组的辅助函数.
*/
if (reset_control_is_array(rstc))
return reset_control_array_deassert(rstc_to_array(rstc));

/*
* 判断复位线是共享的还是专用的.
*/
if (rstc->shared) {
/*
* 对于共享复位线:
* 检查 triggered_count 计数器是否不为0. 如果是, 说明有其他用户调用了 reset_control_reset(),
* 这是API的错误用法, 打印警告并返回错误.
*/
if (WARN_ON(atomic_read(&rstc->triggered_count) != 0))
return -EINVAL;

/*
* 使用原子操作增加 deassert_count 引用计数, 并返回增加后的值.
* 只有当返回值是 1 时 (即我们是第一个调用 deassert 的用户), 才继续执行硬件操作.
* 如果返回值不为 1, 说明已经有其他用户解除了复位, 我们无需重复操作, 直接返回成功.
* 这里的原子操作在单核抢占式内核中是必要的, 防止任务切换导致竞态条件.
*/
if (atomic_inc_return(&rstc->deassert_count) != 1)
return 0;
} else {
/*
* 对于专用复位线:
* 检查 acquired 标志. 专用复位线在使用前必须先被 "获取" (acquire).
* 如果没有被获取, 说明驱动程序有逻辑错误. 打印警告并返回 -EPERM (操作不允许).
*/
if (!rstc->acquired) {
WARN(1, "reset %s (ID: %u) is not acquired\n",
rcdev_name(rstc->rcdev), rstc->id);
return -EPERM;
}
}

/*
* 如果底层的复位控制器驱动 (rstc->rcdev->ops) 没有实现 .deassert() 回调函数,
* 框架会假定这是一个 "自解除" 的复位线 (即通过 .reset() 脉冲复位后会自动解除).
* 在这种情况下, 默认复位线已经处于解除状态, 无需任何操作, 直接返回成功.
* 如果底层驱动的行为不是这样, 它应该实现 .deassert() 并返回 -ENOTSUPP.
*/
if (!rstc->rcdev->ops->deassert)
return 0;

/*
* 调用底层复位控制器驱动 (例如STM32的RCC驱动) 提供的 .deassert() 函数指针.
* 这是实际操作硬件寄存器的步骤.
* @ rstc->rcdev: 指向底层复位控制器设备 (例如RCC).
* @ rstc->id: 要操作的具体的复位线的ID号 (例如在RCC寄存器中的位偏移).
* 函数的返回值就是底层硬件操作的结果.
*/
return rstc->rcdev->ops->deassert(rstc->rcdev, rstc->id);
}
/*
* EXPORT_SYMBOL_GPL 将 reset_control_deassert 函数的符号导出.
* 这使得其他遵循GPL许可证的内核模块 (主要是设备驱动) 可以调用此函数.
*/
EXPORT_SYMBOL_GPL(reset_control_deassert);

drivers/amba/bus.c

AMBA Device Sysfs Attributes: Exposing Hardware Info and Control

此代码片段的作用是为内核中每一个AMBA设备, 在其对应的sysfs目录(例如 /sys/bus/amba/devices/40011000.usart/)下创建一组标准的属性文件。这些文件向用户空间暴露了该设备的关键硬件信息 (如外设ID和内存资源), 并提供了一个强大的控制接口 (driver_override) 来覆盖默认的驱动程序绑定行为。

  • 在单核无MMU的STM32H750平台上的原理与作用

在STM32H750平台上, 所有片上外设都挂载在AMBA总线上。当内核根据设备树初始化这些外设时, 会为每一个外设创建一个AMBA设备实例。此时, 本代码片段所定义的amba_dev_groups就会被自动应用, 为每一个外设(UART, SPI, I2C等)创建如下的sysfs文件。这对于嵌入式系统的开发和调试至关重要:

  1. 硬件信息的可视化: 无需连接调试器, 用户可以直接通过shell访问这些文件, 来快速确认:
    • id 文件: 读取该外设的唯一硬件ID (periphid)。这可以用来验证设备树中的配置是否与硬件手册一致。
    • resource 文件: 查看该外设被分配到的寄存器内存区域的起始地址、结束地址和标志位。这对于调试地址冲突或内存映射问题极为有用。
  2. 灵活的驱动测试: driver_override 文件是一个强大的调试工具。它允许开发者在不修改内核代码或设备树、不重新编译的情况下, 强制一个特定的驱动程序与某个AMBA设备进行绑定。这在测试一个新版本的驱动, 或者在有多个驱动都能匹配同一个设备的情况下指定使用某一个时, 非常方便。
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
/*
* driver_override_show: 'driver_override' sysfs文件的读操作处理函数.
*
* @ _dev: 指向标准 device 结构体的指针.
* @ attr: 指向被读取属性的描述符.
* @ buf: 一个指向用户空间提供的缓冲区的指针, 函数的输出需要写入到这个缓冲区中.
* @return: 返回成功写入缓冲区的字节数.
*/
static ssize_t driver_override_show(struct device *_dev,
struct device_attribute *attr, char *buf)
{
/*
* to_amba_device 是一个宏, 用于从内嵌的 device 结构体指针获取其容器 amba_device 结构体的指针.
*/
struct amba_device *dev = to_amba_device(_dev);
ssize_t len;

/*
* 对设备加锁. 这是一个互斥锁, 确保在读取 dev->driver_override 字符串时,
* 没有其他任务(例如, 另一个写操作)正在修改它, 从而保证了读取的原子性.
*/
device_lock(_dev);
/*
* 使用 sprintf 将 dev->driver_override 字符串(如果存在)的内容打印到用户缓冲区 buf 中.
*/
len = sprintf(buf, "%s\n", dev->driver_override);
/*
* 解锁设备.
*/
device_unlock(_dev);
return len;
}

/*
* driver_override_store: 'driver_override' sysfs文件的写操作处理函数.
*
* @ _dev: 指向标准 device 结构体的指针.
* @ attr: 指向被写入属性的描述符.
* @ buf: 指向用户写入内容的缓冲区的指针.
* @ count: 写入内容的字节数.
* @return: 成功时返回已处理的字节数, 失败时返回负值错误码.
*/
static ssize_t driver_override_store(struct device *_dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct amba_device *dev = to_amba_device(_dev);
int ret;

/*
* 调用通用的内核辅助函数 driver_set_override.
* 这个函数会安全地处理内存分配, 将用户写入的字符串 buf 复制到 dev->driver_override 中.
* 它还会触发总线重新为该设备进行驱动匹配.
*/
ret = driver_set_override(_dev, &dev->driver_override, buf, count);
if (ret)
return ret;

return count;
}
/*
* DEVICE_ATTR_RW 是一个辅助宏, 用于快速定义一个可读写(RW)的设备属性.
* 它会自动创建名为 dev_attr_driver_override 的结构体, 并将上面的 show 和 store 函数与之关联.
*/
static DEVICE_ATTR_RW(driver_override);

/*
* 这是一个用于生成简单的只读属性函数的宏.
* @name: 属性的名称 (例如 id).
* @fmt: sprintf 使用的格式化字符串.
* @arg...: 传递给 sprintf 的参数.
*/
#define amba_attr_func(name,fmt,arg...) \
/* 定义一个名为 name##_show 的静态函数 (## 是预处理器的连接操作符). */
static ssize_t name##_show(struct device *_dev, \
struct device_attribute *attr, char *buf) \
{ \
struct amba_device *dev = to_amba_device(_dev); \
/* 将指定的参数按照格式化字符串写入用户缓冲区. */
return sprintf(buf, fmt, arg); \
} \
/* DEVICE_ATTR_RO 宏用于快速定义一个只读(RO)的设备属性. */
static DEVICE_ATTR_RO(name)

/*
* 使用 amba_attr_func 宏来创建 'id' 属性文件.
* 当被读取时, 它会以8位十六进制的格式显示设备的 periphid (外设ID).
*/
amba_attr_func(id, "%08x\n", dev->periphid);
/*
* 使用 amba_attr_func 宏来创建 'resource' 属性文件.
* 当被读取时, 它会显示该设备的主要资源: 内存区域的起始地址、结束地址和标志位.
*/
amba_attr_func(resource, "\t%016llx\t%016llx\t%016lx\n",
(unsigned long long)dev->res.start, (unsigned long long)dev->res.end,
dev->res.flags);

/*
* 定义一个静态的 attribute 指针数组.
* 这个数组收集了所有我们希望为AMBA设备创建的文件的属性定义.
*/
static struct attribute *amba_dev_attrs[] = {
&dev_attr_id.attr,
&dev_attr_resource.attr,
&dev_attr_driver_override.attr,
NULL, /* 数组必须以 NULL 结尾. */
};
/*
* ATTRIBUTE_GROUPS 宏将属性数组包装成一个属性组.
* 当一个AMBA设备被注册时, 内核会使用这个组一次性地创建所有这些文件.
*/
ATTRIBUTE_GROUPS(amba_dev);

amba_get_enable_pclk: 获取并使能AMBA设备的外设时钟

此函数是一个专用的辅助函数, 它的职责非常明确: 为给定的AMBA设备获取并使能其外设时钟 (pclk)。在像STM32H750这样的现代MCU中, 为了节省功耗, 绝大多数外设的硬件时钟在系统启动时都是默认关闭的。在访问一个外设的寄存器或使其工作之前, 必须先为其提供时钟信号。此函数封装了与内核通用时钟框架 (Common Clock Framework) 交互的标准流程, 以完成这一关键的准备步骤。

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
/*
* amba_get_enable_pclk: 获取并使能AMBA设备的外设时钟(pclk).
*
* @pcdev: 指向需要操作的 amba_device 结构体的指针.
* @return: 成功时返回0, 失败时返回一个负值的错误码.
*/
static int amba_get_enable_pclk(struct amba_device *pcdev)
{
/*
* 定义一个整型变量 ret, 用于存储函数调用的返回值.
*/
int ret;

/*
sdmmc1: mmc@52007000 {
compatible = "arm,pl18x", "arm,primecell";
arm,primecell-periphid = <0x10153180>;
reg = <0x52007000 0x1000>;
interrupts = <49>;
clocks = <&rcc SDMMC1_CK>;
clock-names = "apb_pclk";
resets = <&rcc STM32H7_AHB3_RESET(SDMMC1)>;
cap-sd-highspeed;
cap-mmc-highspeed;
max-frequency = <120000000>;
};

sdmmc2: mmc@48022400 {
compatible = "arm,pl18x", "arm,primecell";
arm,primecell-periphid = <0x10153180>;
reg = <0x48022400 0x400>;
interrupts = <124>;
clocks = <&rcc SDMMC2_CK>;
clock-names = "apb_pclk";
resets = <&rcc STM32H7_AHB2_RESET(SDMMC2)>;
cap-sd-highspeed;
cap-mmc-highspeed;
max-frequency = <120000000>;
status = "disabled";
};
*/

/*
* 调用 clk_get 函数, 从内核的通用时钟框架中获取一个时钟句柄.
* @ &pcdev->dev: 指向与该时钟关联的设备. 框架会根据这个设备和时钟名称来查找正确的时钟源.
* @ "apb_pclk": 要获取的时钟的名称. 这个名称必须与设备树中为该设备定义的 'clock-names' 属性中的一个相匹配.
* "apb_pclk" 是一个常见的AMBA外设总线时钟的命名约定.
* 函数返回一个指向 'struct clk' 的指针(时钟句柄), 或一个内嵌了错误码的错误指针.
* 返回的句柄被存入设备结构体的 pclk 成员中.
*/
pcdev->pclk = clk_get(&pcdev->dev, "apb_pclk");
/*
* 使用 IS_ERR 宏检查 clk_get 的返回值是否为一个错误指针.
* 这通常发生在设备树中没有正确定义时钟, 或者时钟驱动尚未准备好时.
*/
if (IS_ERR(pcdev->pclk))
/*
* 如果是错误指针, 使用 PTR_ERR 宏从中提取出真正的负值错误码并返回.
*/
return PTR_ERR(pcdev->pclk);

/*
* 调用 clk_prepare_enable 函数, 这是一个复合操作. 它会:
* 1. 'prepare': 确保该时钟的所有上游父时钟也都被使能.
* 2. 'enable': 实际打开控制该时钟的硬件门控, 让时钟信号传递给外设.
* 将操作结果(成功为0, 失败为错误码)存入 ret.
*/
ret = clk_prepare_enable(pcdev->pclk);
/*
* 检查 clk_prepare_enable 是否失败 (ret不为0).
*/
if (ret)
/*
* 如果使能失败, 必须调用 clk_put 来释放之前通过 clk_get 获取的时钟句柄,
* 以避免资源泄漏. clk_put 是 clk_get 的反向操作.
*/
clk_put(pcdev->pclk);

/*
* 返回最终的执行结果. 如果成功, ret为0; 如果失败, ret为某个负的错误码.
*/
return ret;
}

amba_read_periphid: 从硬件读取AMBA外设的ID

此函数是AMBA总线驱动与硬件交互的第一个关键步骤。它的核心任务是访问一个AMBA设备(外设)的物理寄存器, 读取并解析出能够唯一标识该设备类型和厂商的ID号。这个ID号包括外设ID(periphid)和单元ID(cid)。为了能成功地访问寄存器, 此函数必须执行一系列前置的准备工作, 包括打开设备的电源域、使能其时钟, 以及解除其复位状态。这是一个典型的、完整的硬件访问流程。

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
/*
* amba_read_periphid: 从硬件寄存器中读取AMBA设备的外设ID(periphid)和单元ID(cid).
*
* @dev: 指向需要被读取ID的 amba_device 结构体的指针.
* @return: 成功时返回0, 失败时返回一个负值的错误码.
*/
static int amba_read_periphid(struct amba_device *dev)
{
/*
* rstc: 一个指向 reset_control 结构体的指针, 用于控制设备的复位信号.
*/
struct reset_control *rstc;
/*
* size: 用于存储设备寄存器区域的大小.
* pid: 用于存储最终读取到的外设ID(Peripheral ID).
* cid: 用于存储最终读取到的单元ID(Cell ID).
*/
u32 size, pid, cid;
/*
* tmp: 一个 __iomem 类型的指针. __iomem 是一个特殊的标记, 告诉内核和开发者
* 这个指针指向的是I/O内存(硬件寄存器), 不能被直接解引用, 必须使用特殊的函数(如readl/writel)来访问.
*/
void __iomem *tmp;
/*
* i: 循环变量.
* ret: 用于存储函数执行过程中的返回值.
*/
int i, ret;

/*
* 调用 dev_pm_domain_attach 将设备附加到其电源域, 并通过 PD_FLAG_ATTACH_POWER_ON 标志确保该电源域被上电.
* 这是一个先决条件, 只有上电后, 设备的寄存器才可能被访问.
*/
ret = dev_pm_domain_attach(&dev->dev, PD_FLAG_ATTACH_POWER_ON);
if (ret) {
/*
* 如果失败, 使用 dev_dbg 打印一条设备相关的调试日志.
*/
dev_dbg(&dev->dev, "can't get PM domain: %d\n", ret);
/*
* 跳转到 err_out 标签, 直接退出函数.
*/
goto err_out;
}

/*
* 调用 amba_get_enable_pclk, 获取并使能该外设的时钟(pclk).
* 时钟是让外设内部逻辑工作的动力, 也是访问其寄存器的先决条件.
*/
ret = amba_get_enable_pclk(dev);
if (ret) {
/*
* 如果失败, 打印调试日志.
*/
dev_dbg(&dev->dev, "can't get pclk: %d\n", ret);
/*
* 跳转到 err_pm 标签, 在退出前先执行电源域的分离操作.
*/
goto err_pm;
}

/*
* 获取AMBA总线上的复位控制器, 并解除其复位状态.
*/
rstc = of_reset_control_array_get_optional_shared(dev->dev.of_node);
/*
* IS_ERR 宏用于检查返回的指针是否为一个内嵌了错误码的错误指针.
*/
if (IS_ERR(rstc)) {
/*
* PTR_ERR 宏用于从错误指针中提取出真正的负值错误码.
*/
ret = PTR_ERR(rstc);
/*
* 特别地, 如果错误不是 -EPROBE_DEFER, 则打印一条错误日志.
* -EPROBE_DEFER 是暂时性错误, 无需报错.
*/
if (ret != -EPROBE_DEFER)
dev_err(&dev->dev, "can't get reset: %d\n", ret);
/*
* 跳转到 err_clk 标签, 在退出前先执行时钟的禁用操作.
*/
goto err_clk;
}
/*
* 调用 reset_control_deassert, 将复位信号线置为非活动状态, 即让设备脱离复位状态, 开始工作.
*/
reset_control_deassert(rstc);
/*
* 调用 reset_control_put, 释放对复位控制器的引用.
*/
reset_control_put(rstc);

/*
* 调用 resource_size 获取设备寄存器区域的总大小. 这个信息来自 dev->res 结构体.
*/
size = resource_size(&dev->res);
/*
* 调用 ioremap, 将设备的物理寄存器地址 (dev->res.start) 映射为内核可以直接访问的虚拟地址.
* 返回的地址被存入 __iomem 指针 tmp.
*/
tmp = ioremap(dev->res.start, size);
if (!tmp) {
/*
* 如果映射失败(通常因为内存不足), 设置错误码为 -ENOMEM.
*/
ret = -ENOMEM;
goto err_clk;
}

/*
* 根据资源区域的大小读取 pid 和 cid. 它们位于该区域的末尾.
* AMBA标准规定了ID寄存器相对于区域末尾的固定偏移量.
*/
/*
* 循环4次, 每次从ID寄存器地址读取一个字节, 并通过位运算将它们拼接成一个32位的 pid.
* tmp + size - 0x20 是 Peripheral ID0 寄存器的地址. readl 用于读取32位寄存器.
*/
for (pid = 0, i = 0; i < 4; i++)
pid |= (readl(tmp + size - 0x20 + 4 * i) & 255) << (i * 8);
/*
* 以同样的方式, 循环4次, 读取并拼接成一个32位的 cid.
* tmp + size - 0x10 是 PrimeCell ID0 寄存器的地址.
*/
for (cid = 0, i = 0; i < 4; i++)
cid |= (readl(tmp + size - 0x10 + 4 * i) & 255) << (i * 8);

/*
* 检查 cid 是否为 CORESIGHT_CID, CoreSight是ARM的调试和追踪架构.
*/
if (cid == CORESIGHT_CID) {
/*
* 如果是CoreSight设备, 读取额外的设备架构(devarch)和设备类型(devtype)信息.
* 这些信息位于相对于区域末尾4KB对齐的基地址处.
*/
void __iomem *csbase = tmp + size - 4096;

dev->uci.devarch = readl(csbase + UCI_REG_DEVARCH_OFFSET);
dev->uci.devtype = readl(csbase + UCI_REG_DEVTYPE_OFFSET) & 0xff;
}

/*
* 检查 cid 是否为有效的AMBA CID或CoreSight CID.
*/
if (cid == AMBA_CID || cid == CORESIGHT_CID) {
/*
* 如果是, 将读取到的 pid 和 cid 保存到设备结构体中.
*/
dev->periphid = pid;
dev->cid = cid;
}

/*
* 如果在所有操作完成后, dev->periphid 仍然是0, 说明ID读取失败或ID无效.
*/
if (!dev->periphid)
/*
* 设置错误码为 -ENODEV (No such device).
*/
ret = -ENODEV;

/*
* 调用 iounmap, 解除之前由 ioremap 创建的地址映射, 释放相关资源.
*/
iounmap(tmp);

err_clk:
/*
* 错误处理标签: 调用 amba_put_disable_pclk, 禁用并释放外设时钟.
*/
amba_put_disable_pclk(dev);
err_pm:
/*
* 错误处理标签: 调用 dev_pm_domain_detach, 从电源域分离.
*/
dev_pm_domain_detach(&dev->dev, true);
err_out:
/*
* 错误处理标签: 返回最终的执行结果(成功为0, 失败为错误码).
*/
return ret;
}

amba_match: 匹配AMBA设备与驱动

此函数是AMBA总线驱动模型的核心匹配接口。当一个新的AMBA设备被系统发现(通常通过解析设备树), 或者一个新的AMBA驱动程序被加载时, 内核的驱动核心会调用此函数, 以判断该驱动(drv)是否声称自己能够支持该设备(dev)。此函数的返回值决定了是否要继续进行下一步的probe(探测)过程。它的逻辑包含了惰性硬件ID读取、用户空间强制覆盖以及标准的ID表匹配, 是一个健壮且灵活的匹配机制。

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
/*
* amba_match: 设备与驱动的匹配函数.
*
* @dev: 一个指向 'struct device' 的指针, 代表需要为其寻找驱动程序的物理设备.
* @drv: 一个指向 'const struct device_driver' 的指针, 代表一个潜在的、待匹配的驱动程序.
* @return: 如果匹配成功, 返回1(布尔真); 如果不匹配, 返回0(布尔假); 如果出现可重试的错误, 返回负值错误码.
*/
static int amba_match(struct device *dev, const struct device_driver *drv)
{
/*
* to_amba_device 是一个宏, 它将通用的 'struct device' 指针安全地转换为AMBA总线特定的
* 'struct amba_device' 指针, 以便访问像 'periphid' 这样的AMBA特有成员.
*/
struct amba_device *pcdev = to_amba_device(dev);
/*
* to_amba_driver 宏与上面类似, 将通用的驱动指针转换为AMBA特定的驱动指针,
* 以便访问像 'id_table' 这样的AMBA特有成员.
*/
const struct amba_driver *pcdrv = to_amba_driver(drv);

/*
* 对设备私有的 periphid_lock 互斥锁进行加锁.
* 这个锁的作用是保护 pcdev->periphid 这个成员变量, 防止在读取或初始化它时发生并发访问.
* 即使在单核抢占式内核中, 这也是必要的, 以防止一个任务在执行此代码块时被另一个任务抢占而导致数据竞争.
*/
mutex_lock(&pcdev->periphid_lock);
/*
* 检查设备的外设ID(periphid)成员是否为0. 这是一个"惰性初始化"(lazy initialization)的实现.
* ID不是在设备创建时就读取, 而是在第一次需要用它来进行匹配时才从硬件读取, 这是一种优化.
*/
if (!pcdev->periphid) {
/*
* 调用 amba_read_periphid 函数, 通过总线访问硬件寄存器来读取设备真实的ID(包括外设ID和单元ID).
* 将返回值(成功为0, 失败为错误码)存入局部变量 ret.
*/
int ret = amba_read_periphid(pcdev);

/*
* 注释解释: 从总线匹配函数返回除 -EPROBE_DEFER 之外的任何错误都可能导致驱动注册失败.
* 因此, 如果读取ID时发生永久性故障, 我们也简单地将其映射为 -EPROBE_DEFER.
* 这样做可以给系统一个机会, 也许之后某些条件变化了, 驱动仍有机会加载.
*/

/*
* 检查 amba_read_periphid 的返回值, 如果它不为0(即读取失败).
*/
if (ret) {
/*
* 立刻解锁互斥锁, 释放对 periphid_lock 的持有.
*/
mutex_unlock(&pcdev->periphid_lock);
/*
* 返回 -EPROBE_DEFER. 这是一个非常特殊的错误码, 它告诉内核驱动核心:
* "现在匹配失败了, 但这只是一个暂时性问题(例如, 该设备的时钟可能还未就绪),
* 请不要将此驱动彻底拉黑, 等稍后依赖项就绪了再来重试."
* 这是Linux内核健壮地处理设备启动依赖关系的关键机制.
*/
return -EPROBE_DEFER;
}
/*
* 如果成功读取了ID, 就调用 dev_set_uevent_suppress(dev, false) 来解除uevent事件的抑制.
* 在ID被读取之前, 设备信息不完整, 内核会抑制其uevent事件的生成, 以免用户空间收到不完整的信息.
*/
dev_set_uevent_suppress(dev, false);
/*
* 手动触发一个 KOBJ_ADD 类型的uevent事件.
* 这会通知用户空间(如udev), 一个信息完整的、可识别的设备现在已经出现在总线上了.
*/
kobject_uevent(&dev->kobj, KOBJ_ADD);
}
/*
* 解锁互斥锁, 结束临界区.
*/
mutex_unlock(&pcdev->periphid_lock);

/*
* 检查用户是否通过sysfs的'driver_override'文件为该设备指定了一个强制绑定的驱动.
*/
if (pcdev->driver_override)
/*
* 如果指定了, 则忽略所有其他的匹配规则, 直接比较被覆盖的驱动名和当前驱动名是否相同.
* strcmp 在字符串相同时返回0, 所以使用 ! 操作符将其转换为布尔真(1).
*/
return !strcmp(pcdev->driver_override, drv->name);

/*
* 如果没有driver_override, 则执行标准的匹配逻辑.
* 调用 amba_lookup 函数, 在驱动程序的 id_table (这是一个驱动声称支持的ID列表) 中,
* 查找是否存在与当前设备的硬件ID(pcdev)相匹配的条目.
* 如果找到了(返回值不为NULL), 则 amba_lookup 返回一个指向匹配条目的指针, 整个表达式的结果为布尔真(1), 表示匹配成功.
* 如果没找到(返回值为NULL), 则表达式结果为布尔假(0), 表示不匹配.
*/
return amba_lookup(pcdrv->id_table, pcdev) != NULL;
}

amba_uevent: 为AMBA设备生成uevent环境变量

此函数是一个回调函数, 在内核为AMBA (Advanced Microcontroller Bus Architecture) 总线上的设备生成一个 uevent (内核事件) 时被调用。它的核心作用是向这个uevent中添加两个关键的环境变量: AMBA_IDMODALIAS。这些变量随后被发送到用户空间, 以便 udevmdev 等守护进程能够识别该设备并为其自动加载正确的驱动程序。

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
/*
* 这是一个静态函数, 是AMBA总线类型的 .uevent 回调实现.
* 当一个AMBA设备的状态发生变化(例如被添加或移除)需要通知用户空间时, 此函数被内核调用.
*
* @dev: 指向一个 const struct device 的指针. 这是触发事件的通用设备对象的只读指针.
* @env: 指向 struct kobj_uevent_env 的指针. 这是一个环境块, 此函数需要向其中添加变量.
* @return: 0 表示成功, 负值错误码表示失败.
*/
static int amba_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
/*
* 定义一个指向 const struct amba_device 的指针 pcdev.
* to_amba_device() 是一个宏 (通常基于 container_of 实现), 它将通用的 struct device 指针转换为
* 包含它的、更具体的 struct amba_device 结构体指针. 这样做是为了访问AMBA总线特有的成员, 如 periphid.
*/
const struct amba_device *pcdev = to_amba_device(dev);
/*
* 定义一个整型变量 retval, 用于存储函数调用的返回值, 初始化为0(成功).
*/
int retval = 0;

/*
* 调用 add_uevent_var() 函数, 向 uevent 环境块中添加第一个变量.
* @ env: 目标环境块.
* @ "AMBA_ID=%08x": 格式化字符串. 这会创建一个名为 "AMBA_ID" 的变量.
* %08x 表示将 pcdev->periphid 的值格式化为一个8位的、用0补齐的十六进制数.
* @ pcdev->periphid: AMBA设备的外设ID. 这是一个由硬件定义的唯一标识符, 用于识别外设的类型.
* 这一行的作用是添加一个类似 "AMBA_ID=40011000" 的变量, 供用户空间的脚本使用.
*/
retval = add_uevent_var(env, "AMBA_ID=%08x", pcdev->periphid);
/*
* 检查 add_uevent_var() 是否返回了错误 (非零值).
* 这通常发生在环境块的缓冲区已满的情况下.
*/
if (retval)
/*
* 如果出错, 立即返回错误码, 不再继续添加变量.
*/
return retval;

/*
* 再次调用 add_uevent_var() 来添加 MODALIAS 变量. 这是自动加载模块最关键的一步.
* @ "MODALIAS=amba:d%08X": 格式化字符串, 创建一个名为 "MODALIAS" 的变量.
* "amba:" 指定了总线类型.
* "d" 表示后面的十六进制数是设备ID.
* "%08X" 将 periphid 格式化为一个8位的、大写的十六进制数.
* @ pcdev->periphid: 同样是外设ID.
* 这一行的作用是生成一个标准格式的模块别名, 例如 "MODALIAS=amba:d40011000".
* 用户空间的udev会根据这个别名去查找并加载能够处理该设备的内核驱动模块.
*/
retval = add_uevent_var(env, "MODALIAS=amba:d%08X", pcdev->periphid);
/*
* 返回最后一次 add_uevent_var() 调用的结果.
*/
return retval;
}

amba_probe: AMBA总线设备探测和初始化

此函数是AMBA总线驱动模型的核心, 它扮演着“探测”或“初始化”一个具体AMBA设备的角色。当内核发现一个AMBA设备(例如, STM32H750上的一个USART外设)与一个驱动程序(例如, STM32的USART驱动)成功匹配后, 内核就会调用该amba_probe函数。它的职责是按照严格的顺序, 为该设备准备好所有必要的系统资源(如中断、时钟、电源), 然后调用具体设备驱动自己的probe函数来完成最终的初始化。如果其中任何一步失败, 它必须能按相反的顺序“回滚”所有已完成的操作, 以确保系统处于干净的状态。

of_amba_device_decode_irq: 从设备树解析AMBA设备的中断信息

这是一个辅助函数, 专门负责从设备树(Device Tree)中读取一个AMBA设备的中断(IRQ)配置信息, 并将这些信息填充到该设备的内核数据结构中。在STM32H750上, 每个外设的中断连接关系都在设备树文件(.dts)中定义, 此函数就是将这些定义转化为内核可以理解和使用的Linux 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/*
* 这是一个内部静态函数, 用于从设备树(OF - Open Firmware)中解析AMBA设备的中断信息.
*
* @dev: 指向需要被解析中断的 amba_device 结构体的指针.
* @return: 0 表示成功, 负值错误码表示失败, 特别地, -EPROBE_DEFER 表示需要延迟探测.
*/
static int of_amba_device_decode_irq(struct amba_device *dev)
{
/*
* 获取与此设备关联的设备树节点(device_node)的指针.
* dev->dev.of_node 是内核设备模型中存储设备树节点的标准位置.
*/
struct device_node *node = dev->dev.of_node;
/*
* i: 循环计数器.
* irq: 用于存储解析出的Linux中断号, 初始化为0(表示无中断).
*/
int i, irq = 0;

/*
* 检查内核是否配置了设备树中断支持(CONFIG_OF_IRQ), 并且该设备确实有一个关联的设备树节点.
* 在STM32平台上, 这两个条件通常都为真.
*/
if (IS_ENABLED(CONFIG_OF_IRQ) && node) {
/*
* 循环遍历设备可能拥有的所有中断源. AMBA_NR_IRQS 是一个宏, 定义了AMBA设备支持的最大中断数量.
*/
for (i = 0; i < AMBA_NR_IRQS; i++) {
/*
* 调用 of_irq_get() 函数, 这是内核提供的标准API, 用于从设备树节点的 "interrupts" 属性中
* 获取指定索引(i)的中断, 并将其转换为一个Linux内核可以使用的虚拟中断号.
*/
irq = of_irq_get(node, i);
/*
* 检查 of_irq_get() 的返回值. 如果小于0, 表示发生了错误或中断不存在.
*/
if (irq < 0) {
/*
* -EPROBE_DEFER 是一个非常特殊的错误码. 它意味着该中断所依赖的中断控制器驱动
* (例如STM32上的NVIC控制器驱动) 尚未初始化完成. 此时我们不能继续,
* 必须向调用者返回这个错误, 以便内核的驱动模型框架可以稍后重新尝试探测此设备.
*/
if (irq == -EPROBE_DEFER)
return irq;
/*
* 对于其他所有错误 (例如, 索引i处没有定义中断), 我们将其视为无中断, 将irq置为0.
*/
irq = 0;
}
/*
* 将获取到的中断号 (或0) 存入 amba_device 结构体的 irq 数组中.
* 这样, 具体的设备驱动后续就可以从这个结构体中方便地获取到它的中断号.
*/
dev->irq[i] = irq;
}
}

/*
* 如果所有中断都成功解析(或不存在), 返回0表示成功.
*/
return 0;
}

函数 amba_probe 详细解析

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
/*
* amba_probe: AMBA总线类型的 .probe 回调函数.
* 当一个AMBA设备和一个AMBA驱动匹配时, 内核调用此函数.
*
* @dev: 指向一个通用的 struct device 的指针, 代表被探测的设备.
* @return: 0 表示成功, 负值错误码表示失败.
*/
static int amba_probe(struct device *dev)
{
/* 将通用的 device 指针转换为具体的 amba_device 指针. */
struct amba_device *pcdev = to_amba_device(dev);
/* 将通用的 driver 指针转换为具体的 amba_driver 指针. */
struct amba_driver *pcdrv = to_amba_driver(dev->driver);
/*
* 在驱动程序的ID表中查找与当前设备匹配的条目.
* 驱动程序会有一个它所支持的设备ID列表(id_table), amba_lookup 会找到匹配的那一项.
* 这个 'id' 结构体可能包含了一些设备特定的数据, 可以传递给驱动使用.
*/
const struct amba_id *id = amba_lookup(pcdrv->id_table, pcdev);
/*
* ret 用于存储每一步操作的返回值.
*/
int ret;

/*
* 这是一个 do-while(0) 循环, 是C语言中一种常用的控制流技巧.
* 它本身只执行一次, 作用是创建一个可以随时使用 'break' 语句跳出的代码块,
* 从而方便地实现统一的错误处理流程.
*/
do {
/* 步骤1: 解析中断. 调用我们上面分析的函数. */
ret = of_amba_device_decode_irq(pcdev);
/* 如果解析中断失败, 使用 'break' 跳转到循环末尾的错误处理部分. */
if (ret)
break;

/* 步骤2: 设置设备的时钟. of_clk_set_defaults 会根据设备树的 "clocks" 属性来准备时钟. */
ret = of_clk_set_defaults(dev->of_node, false);
/* 如果失败, 'break'. */
if (ret < 0)
break;

/* 步骤3: 将设备附加到其电源域(Power Domain), 并确保电源开启. 这是内核电源管理框架的一部分. */
ret = dev_pm_domain_attach(dev, PD_FLAG_ATTACH_POWER_ON);
/* 如果失败, 'break'. */
if (ret)
break;

/* 步骤4: 获取并使能该设备的外设时钟(pclk). 在STM32上, 这是通过操作RCC寄存器来完成的. */
ret = amba_get_enable_pclk(pcdev);
/* 如果失败, 需要先撤销上一步(脱离电源域), 然后再 'break'. */
if (ret) {
dev_pm_domain_detach(dev, true);
break;
}

/* 步骤5: 初始化运行时电源管理(Runtime PM). */
pm_runtime_get_noresume(dev); /* 增加使用计数, 防止设备被自动挂起. */
pm_runtime_set_active(dev); /* 将设备状态设置成"活动". */
pm_runtime_enable(dev); /* 为此设备启用运行时电源管理. */

/* 步骤6: 调用真正的、特定于设备的驱动程序的 probe 函数. 这是初始化的核心. */
ret = pcdrv->probe(pcdev, id);
/* 如果特定驱动的 probe 函数返回0(成功), 那么整个初始化过程就完成了. 'break' 到循环外. */
if (ret == 0)
break;

/*
* 如果特定驱动的 probe 失败了 (ret != 0), 我们必须执行清理操作,
* 按相反的顺序撤销之前的所有步骤.
*/
pm_runtime_disable(dev);
pm_runtime_set_suspended(dev);
pm_runtime_put_noidle(dev);

amba_put_disable_pclk(pcdev);
dev_pm_domain_detach(dev, true);
} while (0);

/*
* 返回最终的结果. 如果成功, ret为0; 如果失败, ret为导致失败的那一步的错误码.
*/
return ret;
}

AMBA总线: 注册ARM内核的片上总线类型

此代码片段的核心作用是在Linux内核中定义并注册 AMBA (Advanced Microcontroller Bus Architecture) 总线类型。这为所有基于AMBA总线的片上外设 (on-chip peripherals) 提供了一个标准的驱动模型框架。注册成功后, 内核就拥有了识别、匹配、和管理AMBA设备及其驱动程序的能力, 并在sysfs中创建了相应的目录结构 (/sys/bus/amba/)。

  • 在单核无MMU的STM32H750平台上的原理与作用

STM32H750微控制器是基于ARM Cortex-M7内核的, 其内部所有片上外设 (如GPIO, UART, SPI, I2C, 定时器, DMA控制器等) 都是通过AMBA总线 (具体为AHB和APB总线) 连接到CPU核心的。因此, 这段代码是在STM32H750上运行Linux并驱动其片上外设的基石

  1. 软件与硬件的桥梁: amba_bustype 结构体是物理AMBA总线在内核中的软件抽象。它本身不是硬件, 而是内核用来理解和管理硬件的一套规则和回调函数。
  2. 基于设备树的探测: 当Linux内核在STM32H750上启动时, 它会解析设备树 (.dts文件)。设备树中详细描述了AMBA总线上存在哪些外设、它们的寄存器地址、中断号等信息。
    • 内核会为设备树中描述的每个AMBA外设创建一个struct device实例。
    • 然后, 内核的驱动核心会调用amba_bustype中的.match函数, 尝试为这个新发现的设备寻找一个名称或compatible字符串相匹配的驱动程序。
    • 一旦找到匹配的驱动, 核心就会调用amba_bustype中的.probe函数, 而这个函数接着会调用具体设备驱动自己的probe函数, 从而完成设备和驱动的初始化。
  3. 电源和DMA管理: .pm.dma_configure等回调函数为总线上的所有设备提供了统一的电源管理和DMA配置接口。这使得具体的设备驱动可以以一种标准化的方式来处理休眠、唤醒和DMA传输, 无需关心底层的具体实现。

总之, 这段代码为STM32H750的所有片上外设建立了一个标准的、可扩展的驱动管理框架, 是整个平台驱动能够正常工作的前提。


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
/*
* Primecell是高级微控制器总线架构(AMBA)的一部分,
* 所以我们称这个总线为 "amba".
* 平台总线和AMBA总线的DMA配置是相同的. 所以这里我们复用了平台的DMA配置例程.
*/
/*
* 定义一个常量 bus_type 结构体实例, 名为 amba_bustype.
* 这是AMBA总线类型在内核中的核心软件表示.
*/
const struct bus_type amba_bustype = {
/*
* .name: 总线的名称. 这个名字将用于在sysfs中创建目录, 即 /sys/bus/amba/.
*/
.name = "amba",
/*
* .dev_groups: 为挂载到此总线上的每个设备默认创建的sysfs属性文件组.
*/
.dev_groups = amba_dev_groups,
/*
* .match: 一个函数指针, 指向设备与驱动的匹配函数. 当一个新设备或新驱动被添加到总线时,
* 内核会调用此函数来判断它们是否兼容. 这是驱动模型的核心.
*/
.match = amba_match,
/*
* .uevent: 一个函数指针, 用于生成uevent事件. 当设备被添加或移除时, uevent用于通知用户空间程序(如udev).
*/
.uevent = amba_uevent,
/*
* .probe: 一个函数指针. 在 .match 成功后被调用. 这个函数通常会执行一些总线层面的准备工作,
* 然后调用具体设备驱动自己的 probe 函数.
*/
.probe = amba_probe,
/*
* .remove: 一个函数指针, .probe 的反向操作. 当设备被移除时调用.
*/
.remove = amba_remove,
/*
* .shutdown: 一个函数指针, 在系统关机时为总线上的每个设备调用.
*/
.shutdown = amba_shutdown,
/*
* .dma_configure: 一个函数指针, 用于配置总线上设备的DMA能力.
*/
.dma_configure = amba_dma_configure,
/*
* .dma_cleanup: 一个函数指针, .dma_configure的反向操作.
*/
.dma_cleanup = amba_dma_cleanup,
/*
* .pm: 一个指向 dev_pm_ops 结构体的指针, 包含了总线层面的电源管理回调函数(如 suspend 和 resume).
*/
.pm = &amba_pm,
};
/*
* 使用 EXPORT_SYMBOL_GPL 将 amba_bustype 符号导出.
* 这使得其他遵循GPL许可证的内核模块(例如, 具体的AMBA设备驱动)可以引用这个总线类型.
*/
EXPORT_SYMBOL_GPL(amba_bustype);

/*
* dev_is_amba: 一个辅助函数, 用于检查一个给定的设备是否属于AMBA总线.
*
* @dev: 指向要检查的 device 结构体的常量指针.
* @return: 如果设备的总线是 amba_bustype, 则返回 true, 否则返回 false.
*/
bool dev_is_amba(const struct device *dev)
{
/*
* 这是一个简单的指针比较, 判断设备的 .bus 成员是否指向全局的 amba_bustype 实例.
*/
return dev->bus == &amba_bustype;
}
/*
* 将 dev_is_amba 函数导出, 供其他GPL模块使用.
*/
EXPORT_SYMBOL_GPL(dev_is_amba);

/*
* amba_init: AMBA总线的初始化函数.
* 这是一个静态函数, 标记为 __init, 表示它仅在内核初始化期间执行,
* 其占用的内存之后可能会被回收.
* @return: 返回 bus_register 的执行结果.
*/
static int __init amba_init(void)
{
/*
* 调用 bus_register 函数, 将我们上面定义的 amba_bustype 注册到内核的驱动核心中.
* 只有在调用此函数之后, AMBA总线才在系统中"生效", 内核才能开始处理AMBA设备和驱动.
* 如果注册成功, 返回0. 失败则返回一个负的错误码.
*/
return bus_register(&amba_bustype);
}

/*
* 使用 postcore_initcall 宏来注册 amba_init 函数.
* 这确保了它在内核的核心组件(如内存管理, kobject框架)初始化之后,
* 但在绝大多数设备驱动开始探测之前被调用. 这是一个注册总线类型的恰当时机.
*/
postcore_initcall(amba_init);