[TOC]

arch/arm/mm: 解剖Linux的NOMMU内存模型

arch/arm/mm 目录包含了 ARM 架构的内存管理代码。在典型的带 MMU 的系统中,它的主要职责是处理虚拟内存、页表、TLB 管理等。但是,当内核被配置为在没有 MMU 的处理器(如 ARM Cortex-M 系列)上运行时,这个目录的功能会发生根本性的变化。

在 NOMMU 模式下,arch/arm/mm 的核心任务不再是管理复杂的虚拟地址空间,而是直接管理一个扁平的、统一的物理地址空间。


一、 NOMMU 的核心概念

在深入代码之前,必须理解 NOMMU 环境下的几个基本事实:

  1. 单一扁平地址空间: 内核、所有用户进程、I/O 内存都共享同一个物理地址空间。不存在虚拟地址到物理地址的转换。一个指针的值就是它在物理内存中的真实地址。
  2. 无内存保护: 由于没有 MMU,处理器无法在硬件层面阻止一个进程访问另一个进程或内核的内存。任何一个有缺陷的应用程序都可以直接读写内核内存,导致整个系统崩溃。
  3. 受限的进程模型: 传统的 fork() 系统调用(它依赖于写时复制技术,而这又依赖于 MMU)无法实现。进程的创建通常依赖于 vfork(),它会与父进程共享内存空间,直到子进程执行 execve() 或退出。
  4. 静态内存布局: 内核和用户程序的内存布局在编译和链接时就已基本确定,并通过链接器脚本(vmlinux.lds.S)固化下来。

二、 关键文件与代码解析 (NOMMU 视角)

当内核配置了 CONFIG_MMU=n 时,arch/arm/mm 目录下的许多文件会被条件编译排除掉,而一些专门为 NOMMU 设计的代码路径则会被激活。

1. nommu.c

这是 NOMMU 实现的核心文件。它为上层通用内存管理代码提供了 ARM 架构下的底层实现,但所有的实现都基于物理内存操作。

  • 物理页面分配:
    • 函数如 __get_free_pages() 和 free_pages() 依然存在,但它们操作的是物理内存。它们负责从系统内存映射中分配或释放连续的物理页面,供 kmalloc 等上层分配器使用。
  • vmalloc 的退化:
    • vmalloc() 在带 MMU 的系统上用于分配虚拟地址连续但物理地址不一定连续的大块内存。在 NOMMU 环境下,这无法实现。nommu.c 中的 vmalloc 实现通常会退化为简单的、基于 kmalloc 的物理连续内存分配器,因此它能分配的内存大小受到物理连续内存碎片的严重限制。
  • 进程内存管理:
    • nommu.c 包含了处理进程内存结构 mm_struct 的函数。但这些函数都被极大地简化了。例如,dup_mm() (在 fork 时复制内存空间) 基本上什么都不做,因为它假设父子进程共享同一个地址空间。
  • 缺页异常处理:
    • 由于没有虚拟内存,也就没有真正的“缺页”概念。nommu.c 提供了 do_page_fault 的一个存根 (stub) 实现,但它通常只会导致内核错误 (Oops) 或杀死进程,因为它表示发生了一次非法的内存访问(例如,访问了不存在的物理地址)。

2. dma-mapping.c

这个文件处理 DMA (Direct Memory Access) 相关的内存操作。在 NOMMU 环境下,它的逻辑也变得非常简单。

  • 地址转换:
    • 在带 MMU 的系统中,dma_map_* 系列函数需要将虚拟地址转换为可供 DMA 控制器使用的物理地址。
    • 在 NOMMU 环境下,虚拟地址就是物理地址。因此,地址转换函数(如 virt_to_phys)基本上是一个空操作或一个简单的偏移量计算。
  • 缓存一致性:
    • 尽管地址转换简化了,但 dma-mapping.c 的另一个重要职责——处理缓存一致性——依然至关重要。在 DMA 操作之前,它必须确保 CPU Cache 中的脏数据被写回(clean/flush)到主内存;在 DMA 操作之后,它必须使 CPU Cache 中对应区域的数据失效(invalidate),以便 CPU 能从主内存中读到 DMA 写入的新数据。这里的代码会直接调用 ARM 架构的底层缓存操作指令。

3. init.c (或 mmu.c 中的 NOMMU 路径)

这个文件负责内核启动早期的内存初始化。

  • mem_init():
    • 此函数负责初始化物理内存分配器。它会获取 Bootloader 或设备树传递过来的可用物理内存的起始地址和大小。
    • 然后,它将这个内存区域交给内核的页分配器(Buddy System)或更简单的 memblock 分配器进行管理。
  • 固定映射的缺失:
    • 在 NOMMU 中,没有“固定映射”(Fixmaps)或“高端内存”(Highmem)的概念,因为所有内存都是直接映射的。init.c 中相关的初始化代码会被禁用。

4. vmlinux.lds.S (链接器脚本)

虽然不直接在 arch/arm/mm 目录下,但链接器脚本是理解 NOMMU 内存布局的关键。

  • 静态布局: 这个脚本硬编码了内核镜像在物理内存中的布局。它定义了 .text (代码)、.data (已初始化数据)、.bss (未初始化数据) 等段的物理加载地址。
  • 用户空间区域: 它还会预留出一块物理内存区域,专门用于后续加载用户应用程序。内核的内存分配器会被告知不要使用这块区域。当 execve 系统调用执行时,它会将应用程序的二进制代码加载到这个预留的物理内存区域中。

三、 NOMMU 的影响与权衡

arch/arm/mm 目录下的 NOMMU 实现,使得 Linux 能够运行在廉价的微控制器上,但这是有代价的:

  • 优点:

    • 低开销: 没有 MMU 地址转换的开销,内存访问速度快。
    • 简单性: 内存模型简单,易于理解和调试底层问题。
    • 低硬件要求: 使得 Linux 可以在没有 MMU 的廉价 MCU 上运行。
  • 缺点:

    • 无保护: 系统的稳定性和安全性极差。任何一个应用程序的指针错误都可能摧毁整个系统。
    • 进程模型残缺: 无法有效利用 fork,限制了许多标准 Linux/POSIX 软件的直接移植。
    • 内存碎片化: 由于只能分配物理连续的内存,长时间运行后,物理内存碎片问题会比带 MMU 的系统更严重,导致大块内存分配失败。

四、 总结

在 NOMMU 配置下,arch/arm/mm 目录的职责从“复杂的虚拟内存管理者”转变为“一个直接、扁平的物理内存管家”。它通过 nommu.c 等文件,为上层内核提供了一套能在单一地址空间上工作的、经过简化的内存管理接口。这种实现是 Linux 强大适应性的体现,使其能够在从大型服务器到微型嵌入式设备的广阔领域中占据一席之地,尽管在 NOMMU 模式下牺牲了现代操作系统的许多核心特性。

arch/arm/mm/nommu.c

adjust_lowmem_bounds 低内存边界调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void __init adjust_lowmem_bounds_mpu(void)
{
unsigned long pmsa = read_cpuid_ext(CPUID_EXT_MMFR0) & MMFR0_PMSA;

switch (pmsa) {
case MMFR0_PMSAv7:
pmsav7_adjust_lowmem_bounds();
break;
case MMFR0_PMSAv8:
pmsav8_adjust_lowmem_bounds();
break;
default:
break;
}
}

void __init adjust_lowmem_bounds(void)
{
phys_addr_t end;
adjust_lowmem_bounds_mpu(); //ARMV7-M 没有 MMFR0_PMSA
end = memblock_end_of_DRAM(); //返回内存结束地址
high_memory = __va(end - 1) + 1;
memblock_set_current_limit(end); //设置memblock的限制地址
}

arm_mm_memblock_reserve 内存块保留

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __init arm_mm_memblock_reserve(void)
{
#ifndef CONFIG_CPU_V7M
vectors_base = IS_ENABLED(CONFIG_CPU_CP15) ? setup_vectors_base() : 0;
/*
* Register the exception vector page.
* some architectures which the DRAM is the exception vector to trap,
* alloc_page breaks with error, although it is not NULL, but "0."
*/
memblock_reserve(vectors_base, 2 * PAGE_SIZE);
#else /* ifndef CONFIG_CPU_V7M */
/*
* V7-M 上没有专门的矢量页面。所以什么都不需要在此处保留。
*/
#endif
/*
* 在任何情况下,请始终确保永远不会使用地址 0,因为如果 0 作为合法地址返回,很多事情都会非常混乱。
*/
memblock_reserve(0, 1);
}

paging_init 分页初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* paging_init() 设置页表,初始化区域内存映射,并设置零页、坏页和坏页表。
*/
void __init paging_init(const struct machine_desc *mdesc)
{
void *zero_page;
/*
* 在 V7-M 上,无需将矢量表复制到专用内存区域。该地址是可配置的,因此可以使用内核映像中的表。
*/
early_trap_init((void *)vectors_base);
mpu_setup(); //V7-M无MPU

/* 分配 Zero 页。 */
zero_page = (void *)memblock_alloc_or_panic(PAGE_SIZE, PAGE_SIZE);

bootmem_init();

empty_zero_page = virt_to_page(zero_page);
//V7-M v7m_cache_fns
flush_dcache_page(empty_zero_page); //cpu_cache.flush_kern_dcache_area(page_address(page), PAGE_SIZE);
}

ARMv7-M (无MMU) ioremap 核心实现

此代码片段揭示了在没有MMU(内存管理单元)的ARM架构 (如运行在STM32H750上的uClinux)中, ioremap系列函数的最核心、最底层的实现。其根本原理是身份映射(Identity Mapping), 即虚拟地址等于物理地址。

在这种架构下, ioremap函数的主要作用不再是进行复杂的地址翻译, 而是承担了两个至关重要的角色:

  1. 提供API兼容性: 它为设备驱动程序提供了一个与有MMU的系统完全相同的、标准化的API。这使得为全功能Linux编写的驱动程序可以几乎不加修改地被重新编译并运行在无MMU的系统上, 极大地增强了代码的可移植性。
  2. 强制类型安全和正确的访问方式: 函数返回一个特殊的指针类型 void __iomem *。这个类型对编译器和静态分析工具(如sparse)是一个明确的信号, 表明该指针指向的是IO内存, 而不是普通RAM。任何尝试对__iomem指针进行直接解引用(如 *addr = val;)的行为都会产生编译警告。这强制开发者必须使用专用的、体系结构相关的函数(如 readl/writel)来访问这些地址, 这些专用函数内置了必要的内存屏障(memory barrier), 确保了对硬件寄存器的读写操作能够按照预期的顺序、不被编译器或CPU乱序执行优化所干扰地完成。

对于STM32H750这类带有MPU(内存保护单元)但无MMU的微控制器, 缓存策略(caching policy)的设定不是在ioremap调用时动态完成的, 而是在系统启动早期的底层代码中, 通过配置MPU来静态完成的。例如, 内核启动时就会将SRAM区域配置为可缓存(cacheable), 而将所有外设寄存器所在的地址范围配置为强序的、非缓存的设备内存(strongly-ordered, non-cacheable device memory)。因此, 当ioremap被调用时, 它只是返回一个指向已经配置好内存属性的物理地址的指针, 而mtype参数实际上被底层实现__arm_ioremap_caller忽略了。


__arm_ioremap_caller: 核心身份映射函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* __arm_ioremap_caller: ARM架构下ioremap的核心调用实现.
* @phys_addr: 要映射的物理地址.
* @size: 映射区域的大小.
* @mtype: 请求的映射类型(缓存策略).
* @caller: 调用者的返回地址, 用于调试.
*/
void __iomem *__arm_ioremap_caller(phys_addr_t phys_addr, size_t size,
unsigned int mtype, void *caller)
{
/*
* 这是无MMU系统上ioremap的本质:
* 函数直接返回传入的物理地址, 只是将其强制类型转换为 `void __iomem *`.
* 它完全忽略了 size, mtype, 和 caller 参数.
* 1. 忽略 size: 因为没有页表要建立, 只要有起始地址就可以访问.
* 2. 忽略 mtype: 因为缓存策略被认为已由MPU在启动时静态配置好.
* 3. 忽略 caller: 在这个最小化实现中, 放弃了调试追踪功能.
*/
return (void __iomem *)phys_addr;
}

arch_ioremap_caller: 体系结构钩子

1
2
3
4
5
6
7
/*
* 这是一个函数指针, 作为体系结构特定的ioremap实现的钩子.
* 在内核初始化时, 它可以被设置为指向一个更复杂的、特定于某个ARM板卡的ioremap实现.
* 如果不设置, 则系统会依赖一个默认的实现(在有MMU的系统中通常是__arm_ioremap_caller的另一个版本).
* 在无MMU的uClinux中, 所有的ioremap最终都通过 __arm_ioremap_caller 来执行.
*/
void __iomem * (*arch_ioremap_caller)(phys_addr_t, size_t, unsigned int, void *);

ioremap 系列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
/*
* ioremap: 标准的ioremap函数, 用于映射标准的设备内存.
* @res_cookie: 要映射的物理地址 (类型为 resource_size_t, 但本质上传递的是物理地址).
* @size: 映射的大小.
*/
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
/*
* 调用核心实现函数, 并传递:
* - mtype = MT_DEVICE: 表示映射为标准的设备内存(强序, 非缓冲, 非缓存).
* - __builtin_return_address(0): 一个编译器内置函数, 获取当前函数的返回地址, 用于追踪调用者.
* 虽然最终被忽略, 但这是API的标准用法.
*/
return __arm_ioremap_caller(res_cookie, size, MT_DEVICE,
__builtin_return_address(0));
}
/* 将ioremap导出, 使其成为一个可供所有内核模块使用的标准API. */
EXPORT_SYMBOL(ioremap);

/*
* ioremap_cache: 用于映射可缓存的IO内存 (例如帧缓冲区).
* @res_cookie: 要映射的物理地址.
* @size: 映射的大小.
*/
void __iomem *ioremap_cache(resource_size_t res_cookie, size_t size)
{
/*
* 调用核心实现函数, 并传递 mtype = MT_DEVICE_CACHED.
* 在无MMU系统上, 这依赖于MPU已经将此段内存配置为可缓存.
*/
return __arm_ioremap_caller(res_cookie, size, MT_DEVICE_CACHED,
__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap_cache);

/*
* ioremap_wc: 用于映射"写合并"(Write-Combining)的IO内存.
* @res_cookie: 要映射的物理地址.
* @size: 映射的大小.
*/
void __iomem *ioremap_wc(resource_size_t res_cookie, size_t size)
{
/*
* 调用核心实现函数, 并传递 mtype = MT_DEVICE_WC.
* 在无MMU系统上, 这依赖于MPU已经将此段内存配置为支持写合并的设备内存.
*/
return __arm_ioremap_caller(res_cookie, size, MT_DEVICE_WC,
__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap_wc);

arch/arm/mm/init.c

arm_memblock_init 内存块初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
/* 在 memblock 中注册内核文本、内核数据和 initrd. */
memblock_reserve(__pa(KERNEL_START), KERNEL_END - KERNEL_START);

reserve_initrd_mem(); //预留initramfs内存

arm_mm_memblock_reserve();

/* 保留任何特定于平台的 MemBlock 区域*/
//STM32 ARMV7M无该函数
if (mdesc->reserve)
mdesc->reserve();

early_init_fdt_scan_reserved_mem();

/* reserve memory for DMA contiguous allocations */
dma_contiguous_reserve(arm_dma_limit);

arm_memblock_steal_permitted = false;
memblock_dump_all();
}

zone_sizes_init 区域大小初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void __init zone_sizes_init(unsigned long min, unsigned long max_low,
unsigned long max_high)
{
unsigned long max_zone_pfn[MAX_NR_ZONES] = { 0 };

#ifdef CONFIG_ZONE_DMA
max_zone_pfn[ZONE_DMA] = min(arm_dma_pfn_limit, max_low);
#endif
max_zone_pfn[ZONE_NORMAL] = max_low;
#ifdef CONFIG_HIGHMEM
max_zone_pfn[ZONE_HIGHMEM] = max_high;
#endif
free_area_init(max_zone_pfn);
}

bootmem_init 引导内存初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __init bootmem_init(void)
{
memblock_allow_resize(); //Memblock 允许调整大小
/*
* *max_low = PFN_DOWN(memblock_get_current_limit());
* *min = PFN_UP(memblock_start_of_DRAM());
* *max_high = PFN_DOWN(memblock_end_of_DRAM());
*/
find_limits(&min_low_pfn, &max_low_pfn, &max_pfn);
//进行内存测试
early_memtest((phys_addr_t)min_low_pfn << PAGE_SHIFT,
(phys_addr_t)max_low_pfn << PAGE_SHIFT);

/*
*sparse_init() 尝试从 memblock 分配内存,因此必须在固定预留之后完成
* CONFIG_NUMA
*/
sparse_init();

/*
*现在释放内存 - free_area_init需要由 sparse_init() 初始化的稀疏 mem_map 数组 memmap_init_zone(),否则所有 PFN 都无效。
*/
zone_sizes_init(min_low_pfn, max_low_pfn, max_pfn);
}

arch/arm/mm/proc-macros.S

dcache_line_size 获取数据缓存行大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* dcache_line_size - 从 ARMv7 上的 CTR 寄存器获取最小 D-cache 行大小。
*/
.macro dcache_line_size, reg, tmp
#ifdef CONFIG_CPU_V7M
movw \tmp, #:lower16:BASEADDR_V7M_SCB + V7M_SCB_CTR
movt \tmp, #:upper16:BASEADDR_V7M_SCB + V7M_SCB_CTR
ldr \tmp, [\tmp]
#else
mrc p15, 0, \tmp, c0, c0, 1 @ read ctr
#endif
lsr \tmp, \tmp, #16 //将 CTR 寄存器的值右移 16 位,提取缓存行大小的编码字段
and \tmp, \tmp, #0xf @ cache line size encoding
mov \reg, #4 @ bytes per word
mov \reg, \reg, lsl \tmp @ actual cache line size
.endm

define_processor_functions: 创建处理器核心功能分发表

这是一个汇编宏 (macro), 其核心作用是作为一个模板, 用于生成一个名为 _processor_functions 的C语言结构体 (在汇编中表现为一张函数指针表)。这个表包含了特定类型CPU所需要的所有核心操作函数的地址, 例如异常处理函数、初始化函数、缓存管理函数和电源管理函数。内核在启动时会根据检测到的CPU类型, 找到对应的功能表, 并使用表中的函数指针来执行硬件相关的操作。这是一种关键的硬件抽象机制。

在单核无MMU的STM32H750平台上的应用

对于STM32H750 (Cortex-M7) 这样的单核无MMU系统, 这个宏通常会被这样调用 (如您之前提供的代码所示):
define_processor_functions v7m, dabort=nommu_early_abort, pabort=legacy_pabort, nommu=1

这里, nommu=1 是一个至关重要的参数。它会在此宏的内部触发条件汇编, 确保生成的函数指针表中:

  • 与MMU相关的函数指针 (set_pte_ext) 被设置为0 (NULL), 因为硬件上不存在MMU。
  • 异常处理函数指针被设置为专门为无MMU系统编写的版本 (nommu_early_abort)。

这样, 该宏就为STM32H750量身定做了一套正确的底层操作函数集合, 使得上层内核代码无需关心底层是否有MMU, 就可以正常运行。


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
/*
* .macro: 定义一个名为 define_processor_functions 的汇编宏.
* @name: 必需参数, CPU的名称 (例如 v7m), 用于生成函数和结构体的名字.
* @dabort: 必需参数, 指向数据访问异常处理函数的指针.
* @pabort: 必需参数, 指向指令预取异常处理函数的指针.
* @nommu=0: 可选参数, 默认值为0. 如果设置为1, 表示为无MMU的系统进行配置.
* @suspend=0: 可选参数, 默认值为0. 如果设置为1, 表示该CPU支持挂起/恢复功能.
* @bugs=0: 可选参数, 默认值为0, 指向一个用于处理CPU特有BUG的函数.
*/
.macro define_processor_functions name:req, dabort:req, pabort:req, nommu=0, suspend=0, bugs=0
/*
* 如果我们正在为big.LITTLE架构编译, 并且启用了分支预测器加固,
* 我们需要处理器功能表在启动后仍然可用(不能被放入init段回收掉).
* 对于单核STM32, 这个条件不成立.
*/
#if defined(CONFIG_BIG_LITTLE) && defined(CONFIG_HARDEN_BRANCH_PREDICTOR)
/* .section ".rodata": 切换到只读数据段. */
.section ".rodata"
#endif
/* .type: 告诉链接器, 接下来定义的符号是一个数据对象(#object), 而不是一个函数. */
.type \name\()_processor_functions, #object
/* .align 2: 将下面的数据对齐到 2^2 = 4 字节的边界上. 这对于32位指针数组是必需的. */
.align 2
/* ENTRY: 定义一个全局可见的入口点(符号). \name\() 会被替换, 例如 "v7m_processor_functions". */
ENTRY(\name\()_processor_functions)
/* .word: 在当前位置插入一个32位的字. 下面一系列指令构成了函数指针表. */
.word \dabort // 第1项: 数据访问异常处理函数 (在STM32上是 nommu_early_abort).
.word \pabort // 第2项: 指令预取异常处理函数 (在STM32上是 legacy_pabort).
.word cpu_\name\()_proc_init // 第3项: CPU相关的初始化函数 (例如 cpu_v7m_proc_init).
.word \bugs // 第4项: CPU勘误(bug)处理函数 (在STM32上是 0).
.word cpu_\name\()_proc_fin // 第5项: CPU相关的结束/清理函数.
.word cpu_\name\()_reset // 第6项: CPU软复位函数.
.word cpu_\name\()_do_idle // 第7项: CPU进入空闲状态的函数 (例如执行 WFI 指令).
.word cpu_\name\()_dcache_clean_area // 第8项: 清理(写回)数据缓存中特定区域的函数.
.word cpu_\name\()_switch_mm // 第9项: 切换内存上下文的函数 (在无MMU系统上通常是空操作).

/* .if \nommu: 这是一个条件汇编指令. 如果宏参数 nommu 的值非0 (为真)... */
.if \nommu
.word 0 // ...则在此处插入一个0. 这是因为 set_pte_ext 函数用于设置页表项, 只有在有MMU的系统上才有意义.
.else
/* 否则 (如果 nommu 为0)... */
.word cpu_\name\()_set_pte_ext // ...则插入一个指向 set_pte_ext 函数的指针.
.endif

/* .if \suspend: 如果宏参数 suspend 的值非0... */
.if \suspend
.word cpu_\name\()_suspend_size // ...插入一个指向返回挂起状态所需大小的函数的指针.
/*
* #ifdef CONFIG_ARM_CPU_SUSPEND: 如果内核配置了ARM CPU挂起功能...
*/
#ifdef CONFIG_ARM_CPU_SUSPEND
.word cpu_\name\()_do_suspend // ...插入指向执行挂起操作的函数的指针.
.word cpu_\name\()_do_resume // ...插入指向执行恢复操作的函数的指针.
#else
/* 否则, 插入空指针. */
.word 0
.word 0
#endif
.else
/* 否则 (如果 suspend 为0, 对于STM32即是此种情况), 插入三个空指针. */
.word 0
.word 0
.word 0
.endif

/* .size: 这是一个汇编器指令, 用于计算符号的大小, 并将其存入符号表. */
.size \name\()_processor_functions, . - \name\()_processor_functions // 大小 = 当前地址(.) - 起始地址.

/* 对应开头的 #if. */
#if defined(CONFIG_BIG_LITTLE) && defined(CONFIG_HARDEN_BRANCH_PREDICTOR)
/* .previous: 切换回之前的代码段. */
.previous
#endif
/* .endm: 宏定义结束. */
.endm

arch/arm/mm/cache-v7m.S

主题:全面了解 ARMv7-M 架构

ARMv7-M 架构是 ARM 公司专门为微控制器 (Microcontroller, MCU) 市场设计的指令集架构。它不是指某一个具体的芯片,而是一套规范和标准,定义了处理器的行为、指令集、内存模型、异常处理等。基于 ARMv7-M 架构,ARM 设计出了著名的 Cortex-M3, Cortex-M4, 和 Cortex-M7 等处理器核心。


一、 历史与背景

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

在 2000 年代初期,32 位微控制器市场高度碎片化,充满了各种厂商的私有架构(如 8051、AVR、PIC、MSP430 等 8/16 位 MCU 的 32 位升级版)。这种局面导致了几个核心问题:

  1. 生态系统割裂: 为 A 厂商写的代码和工具链很难移植到 B 厂商的芯片上。
  2. 开发效率低下: 开发者需要为不同的架构学习不同的工具和编程模型。
  3. 性能与功耗难以兼顾: 许多架构在提供 32 位性能的同时,难以保持 8/16 位 MCU 的低功耗、低成本和实时确定性。

ARMv7-M 的诞生就是为了统一和标准化 32 位微控制器市场。它旨在提供一个兼具高性能、低功耗、优异的实时性和确定性响应的通用平台,同时建立一个庞大、开放的软硬件生态系统。

它的发展经历了哪些重要的里程碑?

  • ARMv6-M (2006年): 作为探路者,引入了 Cortex-M0 和 Cortex-M1 核心。它基于一个非常精简的 16 位 Thumb 指令集子集,主打超低功耗和低成本领域。
  • ARMv7-M (2006年): 这是真正改变游戏规则的一代。
    • Cortex-M3: 首个基于 ARMv7-M 架构的核心,完整引入了 Thumb-2 指令集,完美地结合了 16 位指令的代码密度和 32 位指令的性能。同时引入了革命性的嵌套向量中断控制器 (NVIC)。
    • Cortex-M4: 在 Cortex-M3 的基础上,增加了 DSP(数字信号处理)指令和可选的单精度浮点单元 (FPU),使其非常适合需要进行信号处理和数学运算的场合(如音频处理、传感器融合)。
    • Cortex-M7: 作为性能最高的 M 系列核心,拥有超标量流水线、更紧密耦合的内存 (TCM) 和完整的缓存支持,面向需要极高性能的实时应用(如高端电机控制、汽车电子)。

目前该技术的社区活跃度和主流应用情况如何?

ARMv7-M 架构取得了空前的成功,已成为 32 位微控制器市场事实上的工业标准。

  • 社区活跃度: 拥有全球最大、最活跃的嵌入式开发者社区。从官方论坛、Stack Overflow 到各大电子爱好者社区,都有海量的资源和讨论。
  • 主流应用: 你几乎可以在任何现代电子设备中找到它的身影:
    • 消费电子: 智能手环、无人机、智能家居设备。
    • 工业控制: PLC、电机驱动器、机器人控制器。
    • 物联网 (IoT): 各种传感器节点、无线模块 (WiFi, Bluetooth)。
    • 汽车电子: 车身控制模块、仪表盘、信息娱乐系统辅助处理器。
    • 医疗设备: 便携式监护仪、血糖仪。

二、 核心原理与设计

它的核心工作原理是什么?

ARMv7-M 的设计哲学是为深度嵌入式应用优化,其核心体现在以下几个方面:

  1. Thumb-2 指令集: 作为唯一的指令集,它混合了 16 位和 32 位指令,在不牺牲性能的前提下实现了极高的代码密度,节省了宝贵的 Flash 空间。
  2. 嵌套向量中断控制器 (NVIC): 这是 ARMv7-M 的“杀手级特性”。它提供了硬件自动处理的中断嵌套和优先级管理。当中断发生时,处理器硬件会自动保存关键寄存器(入栈),当中断返回时再自动恢复(出栈),极大地降低了中断延迟,并简化了中断服务程序 (ISR) 的编写。
  3. 内存保护单元 (MPU): 一个可选的硬件单元,允许将内存划分为多个区域,并为每个区域设置访问权限(如只读、不可执行)。这对于需要运行多任务或高安全性应用的系统至关重要,可以防止一个任务破坏另一个任务或内核的内存。
  4. 确定性的行为: 架构设计保证了指令执行时间和中断响应时间是高度可预测的,这对于硬实时 (Hard Real-Time) 系统至关重要。

它的主要优势体现在哪些方面?

  • 庞大的生态系统: 无数芯片厂商(ST, NXP, TI, Microchip等)生产基于 Cortex-M 的芯片,提供了海量的开发板、软件库和工具链(Keil MDK, IAR, GCC)。
  • 卓越的能效比: 在提供强大 32 位性能的同时,拥有多种低功耗模式(Sleep, Deep Sleep, Standby),非常适合电池供电的应用。
  • 出色的中断处理: NVIC 使得中断处理非常快速和高效,这是传统 MCU 难以比拟的。
  • 易于上手和开发: 标准化的内核和外设接口(得益于 CMSIS 标准),使得开发者可以更容易地在不同厂商的芯片之间迁移。

它存在哪些已知的劣势或局限性?

  • 没有内存管理单元 (MMU): 这是与应用处理器(Cortex-A 系列)最根本的区别。没有 MMU 意味着不支持虚拟内存。因此,它无法运行需要虚拟内存的完整操作系统,如标准 Linux、Windows 或 Android。
  • 有限的性能: 尽管 Cortex-M7 性能强大,但与 Cortex-A 系列相比,其计算能力、内存带宽和整体吞吐量仍然有限,不适合进行大规模的复杂计算。
  • 无硬件内存保护(除非使用 MPU): 如果不配置 MPU,所有任务和代码都运行在同一个地址空间,一个指针错误就可能导致整个系统崩溃。

三、 使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

  • 实时控制类: 需要对外部事件做出快速、确定性响应的场景。例如:控制一个四轴无人机的飞行姿态,驱动一个工业机器人的伺服电机。
  • 混合信号处理: 需要采集模拟信号并进行数字处理的场景。例如:从心率传感器采集数据并计算出心率值的智能手环,处理麦克风输入的音频信号。
  • 低功耗物联网节点: 需要长时间使用电池供电,并进行数据采集和无线通信的设备。
  • 安全关键应用: 在配置了 MPU 的情况下,用于需要隔离关键任务和普通任务的系统,如汽车安全控制器。

是否有不推荐使用该技术的场景?为什么?

  • 运行桌面或移动操作系统: 任何需要虚拟内存和多进程保护的复杂操作系统都无法运行。应选择 Cortex-A 系列处理器。
  • 高性能计算: 需要进行大量数据运算、服务器应用、图形渲染等。应选择 Cortex-A 或其他服务器级 CPU。
  • 高可靠性安全系统: 虽然 MPU 提供了保护,但对于需要硬件冗余和故障锁定等功能的极高可靠性场景(如航空发动机控制),Cortex-R (Real-time) 系列是更专业的选择。

四、 对比分析

特性 ARMv7-M (Cortex-M) ARMv7-A (Cortex-A) 8/16位传统 MCU (AVR/PIC)
定位 高性能实时嵌入式控制 应用处理,运行富操作系统 简单控制,极低成本
内存管理 MPU (可选) MMU (标配) + MPU 无
地址空间 物理地址空间 虚拟地址空间 物理地址空间
操作系统 RTOS (FreeRTOS, Zephyr), 裸机 标准 Linux, Android, Windows 简单调度器, 裸机
中断处理 极低延迟 (NVIC) 延迟较高 延迟可变
性能 中到高 高到极高 低
功耗 极低 中到高 极低
典型应用 物联网、工业控制、消费电子 智能手机、服务器、路由器 家电、玩具、简单传感器

五、 入门实践 (Hands-on Practice)

可以提供一个简单的入门教程或关键命令列表吗?

入门 ARMv7-M 开发最简单的方式是使用一块主流的开发板和官方的集成开发环境 (IDE)。

  1. 硬件准备: 购买一块 ST Nucleo 或 NXP Freedom 开发板。例如,Nucleo-F446RE (基于 Cortex-M4) 是一款非常受欢迎的入门板。
  2. 软件安装: 下载并安装 STM32CubeIDE (ST 官方免费 IDE) 或使用 VS Code + PlatformIO。
  3. 创建第一个项目 (Blinky):
    • 在 IDE 中新建一个项目,选择你的开发板型号。
    • IDE 会自动生成初始化代码。你会看到一个图形化界面(CubeMX)用于配置引脚和外设。
    • 找到板载 LED 连接的 GPIO 引脚(例如 PA5),将其配置为 GPIO_Output。
    • 在 main.c 的主循环 while(1) 中添加以下代码:
      1
      2
      3
      4
      5
      6
      // main.c
      while (1)
      {
      HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转 PA5 引脚电平
      HAL_Delay(500); // 延时 500 毫秒
      }
  4. 编译和烧录:
    • 点击 IDE 中的“编译”按钮。
    • 通过 USB 线连接开发板和电脑。
    • 点击“调试”或“烧录”按钮,代码就会被下载到芯片中并开始运行。你会看到板载 LED 每秒闪烁一次。

在初次使用时,有哪些常见的‘坑’或需要注意的配置细节?

  • 时钟配置: MCU 需要正确的时钟源(内部 RC 振荡器或外部晶振)和分频设置才能正常工作。IDE 的图形化工具能极大地简化这个过程。
  • 链接器脚本 (.ld 文件): 它定义了代码和数据在 Flash 和 RAM 中的存放位置。不当的修改可能导致程序无法启动。
  • 堆栈大小: RTOS 或复杂的函数调用可能需要较大的栈空间。如果栈溢出,系统会崩溃且难以调试(HardFault)。
  • 中断优先级: 如果使用多个中断,必须仔细设置它们的优先级,以避免优先级反转等问题。

六、 生态系统、性能、安全与未来

  • 安全考量: 主要风险是物理攻击和固件提取。使用 MPU 来隔离关键代码和数据是重要的安全实践。关闭调试接口(JTAG/SWD)或设置读保护(RDP)可以防止固件被轻易读出。
  • 生态系统: CMSIS (Cortex Microcontroller Software Interface Standard) 是一个重要的软件标准,它提供了统一的 API 来访问内核和外设,增强了代码的可移植性。
  • 性能与监控: 使用调试探针(如 J-Link, ST-LINK)和 OpenOCD/GDB 可以进行单步调试、设置断点和观察内存。Cortex-M4/M7 的 DWT (Data Watchpoint and Trace) 单元可以用于精确的性能分析。
  • 未来趋势: ARMv7-M 的直接后继者是 ARMv8-M 架构(Cortex-M23, M33, M55)。其最重要的增强是引入了 TrustZone-M 安全技术,从硬件层面将系统划分为安全世界和非安全世界,为物联网安全提供了强大的支持。同时,开源的 RISC-V 架构正在成为 ARM 在嵌入式领域的主要竞争者。

七、 总结

ARMv7-M 架构是嵌入式领域的一个里程碑。它通过提供一个高性能、低功耗、高实时性的标准化平台,并围绕它建立了一个无与伦比的生态系统,成功地统一了 32 位微控制器市场。

关键特性总结:

  • Thumb-2 指令集: 性能与代码密度的完美结合。
  • NVIC: 高效、低延迟的硬件中断处理。
  • MPU: 提供内存保护,增强系统稳定性和安全性。
  • 低功耗: 多种睡眠模式,适合电池供电应用。

学习该技术的要点建议:

  1. 从一块主流开发板开始: 选择 ST Nucleo 或 NXP Freedom 系列,它们拥有最好的社区支持和文档。
  2. 掌握 HAL/LL 库: 学习使用厂商提供的硬件抽象层 (HAL) 库来控制外设,这是最快的开发方式。
  3. 学习一个 RTOS: FreeRTOS 是事实上的工业标准,学习它能让你构建更复杂的、多任务的嵌入式系统。
  4. 深入理解核心: 不要只停留在调用 API。花时间去理解中断、DMA、时钟树等核心概念,这将使你成为一个更优秀的嵌入式工程师。

v7m_cacheop V7-M 缓存操作

1
2
3
4
5
.macro v7m_cacheop, rt, tmp, op, c = al
movw\c \tmp, #:lower16:BASEADDR_V7M_SCB + \op
movt\c \tmp, #:upper16:BASEADDR_V7M_SCB + \op
str\c \rt, [\tmp]
.endm

dccimvac 按 MVA 到 PoC 清理数据缓存行并使之失效(例如系统DMA)

  • POC的缓存维护操作可用于在Cortex®-M7数据缓存与外部代理(例如系统DMA)之间同步数据
1
2
3
4
5
6
7
8
/*
* dccimvac:按 MVA 到 PoC 清理数据缓存行并使之失效。
*/
.irp c,,eq,ne,cs,cc,mi,pl,vs,vc,hi,ls,ge,lt,gt,le,hs,lo
.macro dccimvac\c, rt, tmp
v7m_cacheop \rt, \tmp, V7M_SCB_DCCIMVAC, \c
.endm
.endr

v7m_flush_kern_dcache_area 内核数据缓存区域刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* v7m_flush_kern_dcache_area(void *addr, size_t size)
*
* 确保将页面 kaddr 中保存的数据写回相关页面。
*
* - addr - 内核地址
* - size - 区域大小
*/
SYM_TYPED_FUNC_START(v7m_flush_kern_dcache_area)
dcache_line_size r2, r3 //获取数据缓存行大小
add r1, r0, r1
sub r3, r2, #1
bic r0, r0, r3 //将结束地址对齐缓存行大小
1:
dccimvac r0, r3 @ clean & invalidate D line / unified line
add r0, r0, r2 //将地址 r0 增加一个缓存行大小,移动到下一个缓存行
cmp r0, r1 //比较当前地址是否已达到结束地址。
blo 1b //如果当前地址小于结束地址,跳回标签 1,继续操作下一行
dsb st //数据同步屏障,确保所有缓存操作在返回前完成。这是一个内存屏障,保证缓存清理和失效的效果对后续操作可见。
ret lr
SYM_FUNC_END(v7m_flush_kern_dcache_area)

通用 ARMv7-M 处理器函数 (cpu_v7m_*)

这些是适用于所有ARMv7-M处理器的默认函数。

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
/* SYM_TYPED_FUNC_START 是一个宏, 用于定义一个全局可见的函数入口点 */
SYM_TYPED_FUNC_START(cpu_v7m_proc_init)
/* ret lr: 这是一个ARM汇编伪指令, 实际等价于 "bx lr" (Branch and exchange to Link Register). */
/* lr 寄存器通常保存着函数返回地址. 这条指令的含义是: "立即返回". */
/* 这个函数是空的, 表明对于通用的ARMv7-M, 没有特殊的处理器初始化步骤. */
ret lr
SYM_FUNC_END(cpu_v7m_proc_init) /* SYM_FUNC_END 宏用于标记函数结束, 以便计算函数大小. */

SYM_TYPED_FUNC_START(cpu_v7m_proc_fin)
/* 同样, 这个函数也是空的, 表示没有通用的处理器收尾步骤. */
ret lr
SYM_FUNC_END(cpu_v7m_proc_fin)

/*
* cpu_v7m_reset(loc)
*
* 执行一次系统的软复位. 将CPU置于与硬件复位后相同的状态,
* 并跳转到将成为复位向量的地方.
*
* - loc: 用于软复位跳转的目标地址, 该地址在调用此函数时被放入 r0 寄存器.
*/
.align 5 /* .align 5 表示将下面的代码对齐到 2^5 = 32 字节的边界. */
SYM_TYPED_FUNC_START(cpu_v7m_reset)
/* ret r0: 等价于 "bx r0". 这条指令会无条件跳转到 r0 寄存器中包含的地址. */
/* 这是执行软复位的核心: 直接跳转到内核启动代码的入口地址, 实现重新启动. */
ret r0
SYM_FUNC_END(cpu_v7m_reset)

/*
* cpu_v7m_do_idle()
*
* 使处理器空闲(例如, 等待中断).
*
* (调用此函数时)IRQ中断已经被禁用了.
*/
SYM_TYPED_FUNC_START(cpu_v7m_do_idle)
/* wfi: Wait For Interrupt 指令. */
/* 执行这条指令后, CPU会立即停止执行后续指令, 进入低功耗的睡眠状态, 直到一个中断(或调试事件)发生才会被唤醒. */
/* 这是操作系统调度器在没有任务需要运行时, 让CPU节能的核心指令. */
wfi
/* 当被中断唤醒后, 从这里继续执行, 立即返回. */
ret lr
SYM_FUNC_END(cpu_v7m_do_idle)

SYM_TYPED_FUNC_START(cpu_v7m_dcache_clean_area)
/* 通用的ARMv7-M实现是空操作. 这是因为并非所有v7-M核心都有数据缓存(D-cache). */
/* 这个函数作为一个占位符, 等待像Cortex-M7这样有缓存的CPU来提供具体的实现. */
ret lr
SYM_FUNC_END(cpu_v7m_dcache_clean_area)

/*
* 由于没有MMU(内存管理单元), 这里无事可做.
*/
SYM_TYPED_FUNC_START(cpu_v7m_switch_mm)
/* 在有MMU的系统中, 这个函数会切换页表, 实现进程地址空间的隔离. */
/* 在无MMU的STM32H750上, 所有代码共享同一个物理地址空间, 所以切换是无意义的, 函数为空. */
ret lr
SYM_FUNC_END(cpu_v7m_switch_mm)

.globl cpu_v7m_suspend_size /* 声明一个全局可见的符号 */
.equ cpu_v7m_suspend_size, 0 /* .equ: 将一个符号赋值为一个常量. 这里表示挂起状态所需空间为0. */

#ifdef CONFIG_ARM_CPU_SUSPEND /* 如果内核配置了CPU挂起功能 */
SYM_TYPED_FUNC_START(cpu_v7m_do_suspend)
/* 默认的挂起/恢复函数也是空操作, 需要特定平台提供实现. */
ret lr
SYM_FUNC_END(cpu_v7m_do_suspend)

SYM_TYPED_FUNC_START(cpu_v7m_do_resume)
ret lr
SYM_FUNC_END(cpu_v7m_do_resume)
#endif

Cortex-M7 专用处理器函数 (cpu_cm7_*)

这些是专门为Cortex-M7核心提供的、重写了通用v7-M函数的具体实现。

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
/*
* 这是 Cortex-M7 专用的 dcache_clean_area 实现, 它取代了上面那个空的 v7m 版本.
* 功能: 将指定内存区域([r0, r0+r1-1])的数据从数据缓存中"清理"(写回)到主内存中.
* 这对于保证DMA操作前内存数据的一致性至关重要.
*/
SYM_TYPED_FUNC_START(cpu_cm7_dcache_clean_area)
/* dcache_line_size 是一个宏, 用于获取数据缓存行的大小(例如32字节), 并存入r2. r3被用作临时寄存器. */
dcache_line_size r2, r3
/* movw/movt: 这两条指令组合在一起, 将一个32位的立即数(地址)加载到 r3 寄存器中. */
/* 加载的是 V7M_SCB_DCCMVAC 寄存器的地址. SCB是系统控制块(System Control Block)的缩写. */
/* DCCMVAC (Data Cache Clean by MVA to PoC) 是一个只写的寄存器, 向它写入一个地址, 就会清理该地址所在的缓存行. */
movw r3, #:lower16:BASEADDR_V7M_SCB + V7M_SCB_DCCMVAC
movt r3, #:upper16:BASEADDR_V7M_SCB + V7M_SCB_DCCMVAC

/* 1: 是一个循环标签 */
1:
/* str r0, [r3]: 将 r0 中的地址写入到 DCCMVAC 寄存器. 这一步执行了对单个缓存行的清理操作. */
str r0, [r3]
/* add r0, r0, r2: 将地址 r0 增加一个缓存行的大小, 指向下一个需要清理的缓存行. */
add r0, r0, r2
/* subs r1, r1, r2: 将剩余要清理的字节数 r1 减去一个缓存行的大小. 's'后缀表示更新条件标志位. */
subs r1, r1, r2
/* bhi 1b: Branch if Higher. 如果 r1 仍然大于0, 则跳转回标签1(1b中的'b'表示向后跳转). */
bhi 1b
/* dsb: Data Synchronization Barrier. 数据同步屏障. */
/* 这是一条关键指令, 它确保所有之前的缓存清理操作都已完成, 才能执行后续的指令. */
dsb
/* 返回. */
ret lr
SYM_FUNC_END(cpu_cm7_dcache_clean_area)

/*
* 这是 Cortex-M7 专用的 proc_fin 实现.
* 功能: 在系统关闭或进程结束的某个阶段, 禁用数据缓存和指令缓存.
*/
SYM_TYPED_FUNC_START(cpu_cm7_proc_fin)
/* movw/movt: 将 V7M_SCB_CCR (Cache Control Register, 缓存控制寄存器) 的地址加载到 r2. */
movw r2, #:lower16:(BASEADDR_V7M_SCB + V7M_SCB_CCR)
movt r2, #:upper16:(BASEADDR_V7M_SCB + V7M_SCB_CCR)
/* ldr r0, [r2]: 从CCR寄存器中读取当前的控制字到 r0. */
ldr r0, [r2]
/* bic r0, r0, #(V7M_SCB_CCR_DC | V7M_SCB_CCR_IC): bic 是 Bit Clear 指令. */
/* 它将 r0 中对应 V7M_SCB_CCR_DC (数据缓存使能) 和 V7M_SCB_CCR_IC (指令缓存使能) 的位清零. */
bic r0, r0, #(V7M_SCB_CCR_DC | V7M_SCB_CCR_IC)
/* str r0, [r2]: 将修改后的值写回CCR寄存器, 从而禁用了两个缓存. */
str r0, [r2]
/* 返回. */
ret lr
SYM_FUNC_END(cpu_cm7_proc_fin)

arch/arm/mm/cache.c

v7m_cache_fns V7-M缓存函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void v7m_flush_icache_all(void);
void v7m_flush_kern_cache_all(void);
void v7m_flush_user_cache_all(void);
void v7m_flush_user_cache_range(unsigned long, unsigned long, unsigned int);
void v7m_coherent_kern_range(unsigned long, unsigned long);
int v7m_coherent_user_range(unsigned long, unsigned long);
void v7m_flush_kern_dcache_area(void *, size_t);
void v7m_dma_map_area(const void *, size_t, int);
void v7m_dma_unmap_area(const void *, size_t, int);
void v7m_dma_flush_range(const void *, const void *);

struct cpu_cache_fns v7m_cache_fns __initconst = {
.flush_icache_all = v7m_flush_icache_all,
.flush_kern_all = v7m_flush_kern_cache_all,
.flush_kern_louis = v7m_flush_kern_cache_all,
.flush_user_all = v7m_flush_user_cache_all,
.flush_user_range = v7m_flush_user_cache_range,
.coherent_kern_range = v7m_coherent_kern_range,
.coherent_user_range = v7m_coherent_user_range,
.flush_kern_dcache_area = v7m_flush_kern_dcache_area,
.dma_map_area = v7m_dma_map_area,
.dma_unmap_area = v7m_dma_unmap_area,
.dma_flush_range = v7m_dma_flush_range,
};

arch/arm/mm/fault.c

FSR/IFSR Info: ARM内存访问异常分发表

这两个数组(fsr_info 和 ifsr_info)是Linux内核在处理ARM架构CPU的内存访问异常时使用的静态“分发表”或“查找表”。当CPU因为一次错误的内存访问(例如, 访问一个不存在的地址或向只读区域写入)而触发硬件异常时, 它会在一个特殊的寄存器——故障状态寄存器 (FSR) 中设置一个代码来表明错误的原因。内核的异常处理程序会读取这个代码, 并用它作为索引在此数组中查找对应的处理方式, 包括应该调用哪个内核函数、应该向引发问题的用户进程发送什么信号, 以及应该在内核日志中打印什么描述信息。

关于STM32H750 (ARMv7-M架构) 的适用性说明

这是一个非常关键的区别: 您提供的这段代码源自为**“经典”ARM架构 (如ARMv4, ARMv5, ARMv7-A)** 编写的Linux内核。这些架构使用名为FSR (Fault Status Register) 和IFSR (Instruction Fault Status Register) 的寄存器来报告内存错误。

而您指定的STM32H750微控制器使用的是ARMv7-M架构 (Cortex-M7内核)。ARMv7-M拥有一个更现代、更精细的故障处理机制, 它不使用FSR/IFSR, 而是使用以下三个状态寄存器的组合, 它们共同位于CFSR (Configurable Fault Status Register) 中:

  • MMFSR (MemManage Fault Status Register): 报告由MPU (内存保护单元) 检测到的访问冲突, 例如执行位于“从不执行”(XN)区域的代码, 或写入一个只读区域。
  • BFSR (BusFault Status Register): 报告在总线访问期间发生的错误, 例如访问一个不存在的内存地址。
  • UFSR (UsageFault Status Register): 报告用法错误, 例如执行一条未定义的指令或进行一次非对齐的内存访问。

因此, 虽然您提供的这段代码展示了Linux内核处理内存异常的通用设计原则(即使用查找表来分发异常), 但其内容(具体的故障码和表项)与ARMv7-M架构不直接对应。在为STM32H750编译的Linux内核中, 会有功能类似但内容不同的表, 这些表将由MMFSR, BFSR, UFSR中的故障码来索引。

下面将按您提供的代码本身进行逐行解析, 以解释其设计思想。


fsr_info 数组: 数据访问故障 (Data Abort) 处理表

此表用于处理由数据加载或存储指令(如 LDR, STR)引起的内存访问错误。

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
/* 定义一个fsr_info结构体数组, 用于存储数据访问故障(Data Abort)的处理信息. */
static struct fsr_info fsr_info[] = {
/*
* 以下是标准的ARMv3和ARMv4中止。ARMv5将它们定义为“精确”中止。
* "精确"中止意味着CPU报告的故障地址就是导致中止的指令地址。
* 每一行代表一种故障类型, 由FSR寄存器的值(作为数组索引)来确定。
* 结构体的成员依次是: {内核处理函数, 发送给用户进程的信号, 信号的附加码, 内核日志描述字符串}
*/
/* 索引 0: "vector exception" - 向量异常 */
{ do_bad, SIGSEGV, 0, "vector exception" },
/* 索引 1: "alignment exception" - 对齐异常. 当LDR/STR指令试图访问一个未对齐的地址时发生。*/
{ do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
/* 索引 2: "terminal exception" - 终端异常 */
{ do_bad, SIGKILL, 0, "terminal exception" },
/* 索引 3: "alignment exception" - 对齐异常 (用于后备) */
{ do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
/* 索引 4: "external abort on linefetch" - 取指令行时发生外部中止 */
{ do_bad, SIGBUS, 0, "external abort on linefetch" },
/* 索引 5: "section translation fault" - 段转换故障. 在有MMU的系统中, 访问一个没有页表项映射的内存段。*/
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },
/* 索引 6: "external abort on linefetch" - 取指令行时发生外部中止 (用于后备) */
{ do_bad, SIGBUS, 0, "external abort on linefetch" },
/* 索引 7: "page translation fault" - 页转换故障. 在有MMU的系统中, 访问一个没有页表项映射的内存页。*/
{ do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" },
/* 索引 8: "external abort on non-linefetch" - 非取指令行时发生外部中止 */
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
/* 索引 9: "section domain fault" - 段域故障. 在有MMU的系统中, 访问了一个当前任务无权访问的内存域。*/
{ do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" },
/* 索引 10: "external abort on non-linefetch" - 非取指令行时发生外部中止 (用于后备) */
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
/* 索引 11: "page domain fault" - 页域故障. 在有MMU的系统中, 访问了一个当前任务无权访问的内存域。*/
{ do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },
/* 索引 12: "external abort on translation" - 转换期间发生外部中止 */
{ do_bad, SIGBUS, 0, "external abort on translation" },
/* 索引 13: "section permission fault" - 段权限故障. 在有MMU的系统中, 试图向一个只读的内存段写入数据。*/
{ do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" },
/* 索引 14: "external abort on translation" - 转换期间发生外部中止 (用于后备) */
{ do_bad, SIGBUS, 0, "external abort on translation" },
/* 索引 15: "page permission fault" - 页权限故障. 在有MMU的系统中, 试图向一个只读的内存页写入数据。这在无MMU但有MPU的STM32上等价于MPU权限错误。*/
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" },
/*
* 以下是“非精确”中止, 由FSR的第10位指示, 可能无法恢复。
* 仅当CPU中止处理程序支持第10位时才支持这些。
* "非精确"意味着报告的故障地址与导致中止的指令可能不直接对应, 这使得错误恢复变得困难或不可能。
*/
{ do_bad, SIGBUS, 0, "unknown 16" }, /* 未知故障码 16 */
{ do_bad, SIGBUS, 0, "unknown 17" }, /* ... */
{ do_bad, SIGBUS, 0, "unknown 18" },
{ do_bad, SIGBUS, 0, "unknown 19" },
{ do_bad, SIGBUS, 0, "lock abort" }, /* xscale 特有 */
{ do_bad, SIGBUS, 0, "unknown 21" },
{ do_bad, SIGBUS, BUS_OBJERR, "imprecise external abort" }, /* xscale 特有, 非精确外部中止 */
{ do_bad, SIGBUS, 0, "unknown 23" },
{ do_bad, SIGBUS, 0, "dcache parity error" }, /* xscale 特有, 数据缓存奇偶校验错误 */
{ do_bad, SIGBUS, 0, "unknown 25" },
{ do_bad, SIGBUS, 0, "unknown 26" },
{ do_bad, SIGBUS, 0, "unknown 27" },
{ do_bad, SIGBUS, 0, "unknown 28" },
{ do_bad, SIGBUS, 0, "unknown 29" },
{ do_bad, SIGBUS, 0, "unknown 30" },
{ do_bad, SIGBUS, 0, "unknown 31" },
};

ifsr_info 数组: 指令预取故障 (Prefetch Abort) 处理表

此表用于处理因CPU尝试从内存中预取指令时发生的错误。

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
/* 定义一个ifsr_info结构体数组, 用于存储指令预取故障(Prefetch Abort)的处理信息. */
static struct fsr_info ifsr_info[] = {
{ do_bad, SIGBUS, 0, "unknown 0" },
{ do_bad, SIGBUS, 0, "unknown 1" },
/* 索引 2: "debug event" - 调试事件. */
{ do_bad, SIGBUS, 0, "debug event" },
/* 索引 3: "section access flag fault" - 段访问标志故障. */
{ do_bad, SIGSEGV, SEGV_ACCERR, "section access flag fault" },
{ do_bad, SIGBUS, 0, "unknown 4" },
/* 索引 5: "section translation fault" - 段转换故障. 试图从一个未映射的内存段取指令. */
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },
/* 索引 6: "page access flag fault" - 页访问标志故障. */
{ do_bad, SIGSEGV, SEGV_ACCERR, "page access flag fault" },
/* 索引 7: "page translation fault" - 页转换故障. 试图从一个未映射的内存页取指令. */
{ do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" },
/* 索引 8: "external abort on non-linefetch" - 非取指令行时发生外部中止. */
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
/* 索引 9: "section domain fault" - 段域故障. */
{ do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" },
{ do_bad, SIGBUS, 0, "unknown 10" },
/* 索引 11: "page domain fault" - 页域故障. */
{ do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },
/* 索引 12: "external abort on translation" - 转换期间发生外部中止. */
{ do_bad, SIGBUS, 0, "external abort on translation" },
/* 索引 13: "section permission fault" - 段权限故障. 试图从一个被标记为"从不执行"(XN)的内存段取指令. */
{ do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" },
/* 索引 14: "external abort on translation" - 转换期间发生外部中止 (用于后备). */
{ do_bad, SIGBUS, 0, "external abort on translation" },
/* 索引 15: "page permission fault" - 页权限故障. 类似索引13, 但针对页. 在STM32上, 这等价于MPU将某区域配置为XN后, CPU仍试图从中取指. */
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" },
/* 后续为未知或未定义的故障码. */
{ do_bad, SIGBUS, 0, "unknown 16" },
{ do_bad, SIGBUS, 0, "unknown 17" },
{ do_bad, SIGBUS, 0, "unknown 18" },
{ do_bad, SIGBUS, 0, "unknown 19" },
{ do_bad, SIGBUS, 0, "unknown 20" },
{ do_bad, SIGBUS, 0, "unknown 21" },
{ do_bad, SIGBUS, 0, "unknown 22" },
{ do_bad, SIGBUS, 0, "unknown 23" },
{ do_bad, SIGBUS, 0, "unknown 24" },
{ do_bad, SIGBUS, 0, "unknown 25" },
{ do_bad, SIGBUS, 0, "unknown 26" },
{ do_bad, SIGBUS, 0, "unknown 27" },
{ do_bad, SIGBUS, 0, "unknown 28" },
{ do_bad, SIGBUS, 0, "unknown 29" },
{ do_bad, SIGBUS, 0, "unknown 30" },
{ do_bad, SIGBUS, 0, "unknown 31" },
};

hook_fault_code: ARMv7-A/R 架构的故障状态分派

此代码片段定义了一个名为fsr_info的静态数组和一个名为hook_fault_code的函数。它们共同构成了一个异常分派表机制。其核心作用是, 当一个ARM处理器发生内存访问异常(在ARM术语中称为“abort”)时, 内核的通用异常处理入口程序会读取硬件的故障状态寄存器(FSR), 并使用其中的故障码(fault code)作为索引在此fsr_info表中查找, 以决定应该调用哪个具体的处理函数、以及向用户进程发送什么信号。

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
/*
* hook_fault_code: 在运行时(或初始化时)修改 fsr_info 分派表的函数.
*
* @nr: 要修改的条目的索引, 即故障码.
* @fn: 指向新的处理函数的指针. 类型为 int (*)(unsigned long, unsigned int, struct pt_regs *).
* @sig: 新的信号编号.
* @code: 新的信号附加代码.
* @name: 新的故障描述字符串.
*/
void __init
hook_fault_code(int nr, int (*fn)(unsigned long, unsigned int, struct pt_regs *),
int sig, int code, const char *name)
{
/*
* 检查索引 nr 是否在 fsr_info 数组的有效范围内.
*/
if (nr < 0 || nr >= ARRAY_SIZE(fsr_info))
/*
* 如果索引无效, 调用 BUG() 宏.
* BUG() 会立即让内核崩溃(panic), 并打印出有用的调试信息.
* 这是一种强制性的运行时断言, 用于捕捉严重的编程错误.
*/
BUG();

/*
* 如果索引有效, 就用传入的新参数覆盖掉 fsr_info 数组中第 nr 个元素的所有成员.
*/
fsr_info[nr].fn = fn; // 更新处理函数指针
fsr_info[nr].sig = sig; // 更新信号编号
fsr_info[nr].code = code; // 更新信号附加代码
fsr_info[nr].name = name; // 更新描述字符串
}

exceptions_init: 为特定的ARM架构版本注册异常处理钩子

此函数是在内核启动的早期阶段被调用的一个初始化函数。它的核心作用是为特定类型的硬件异常(在此代码片段中特指ARMv6和ARMv7架构引入的一些内存相关的“faults”)注册相应的处理函数。通过调用hook_fault_code,它将一个特定的故障码与一个内核处理函数(如do_translation_fault或do_bad)以及一个需要发送给用户空间进程的信号(如SIGSEGV)关联起来。

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

STM32H750 使用的是 ARM Cortex-M7 内核, 其架构是 ARMv7-M。这是一个“M” profile(微控制器配置文件)的架构, 它没有MMU, 但通常配备有MPU(内存保护单元)。

  1. 架构匹配: 函数中的cpu_architecture() >= CPU_ARCH_ARMv7这个条件对于STM32H750是成立的。
  2. 异常类型差异: 然而, 代码中提到的“fault code”是针对ARM的“A”和“R” profile(应用和实时配置文件)架构的, 这些架构有MMU, 其内存管理和异常模型与“M” profile有显著不同。
    • “A”/“R” profile的CPU在发生内存访问错误时会生成Data Abort或Prefetch Abort异常, 并提供一个故障状态寄存器(FSR)来指示具体的错误原因, 其编码就是代码中的 3, 4, 6 等。do_translation_fault和section access flag fault这些错误类型都与MMU的页表转换和权限检查紧密相关。
    • ARMv7-M架构则有一套不同的异常模型。它使用UsageFault, BusFault, 和 MemManageFault来处理类似但不同的错误情况(如MPU权限冲突, 非法指令, 无效内存访问等)。这些异常有自己独立的、与A/R profile完全不同的状态寄存器和错误编码。
  3. CONFIG_ARM_LPAE: 这个宏代表“大物理地址扩展”, 是64位寻址的一部分, 与ARMv7-M无关。

结论: exceptions_init这个函数本身是为了带有MMU的ARMv7-A/R架构设计的。在一个为STM32H750 (ARMv7-M) 正确配置的Linux内核中, 会使用一套完全不同的异常处理初始化代码来设置UsageFault, BusFault, MemManageFault的处理程序。因此, 这个特定的exceptions_init函数片段将不会被编译进一个标准的STM32H750内核中, 即使它的#ifndef条件成立。如果由于配置错误它被包含了, 它所注册的钩子也将永远不会被触发, 因为ARMv7-M的硬件根本不会产生这些故障码。


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
static int
do_translation_fault(unsigned long addr, unsigned int fsr,
struct pt_regs *regs)
{
return 0;
}
/*
* This abort handler always returns "fault".
*/
static int
do_bad(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
return 1;
}
/*
* 这个条件编译指令表示, 只有在内核没有配置ARM大物理地址扩展(LPAE)时,
* 下面的代码才会被编译. LPAE是ARMv8/ARMv7的一部分, 用于支持大于32位的物理地址空间.
* ARMv7-M架构 (如STM32H750) 不支持LPAE, 所以这个条件通常为真.
*/
#ifndef CONFIG_ARM_LPAE
/*
* 这是一个静态的初始化函数.
* __init 宏表示此函数仅在内核启动期间执行.
*/
static int __init exceptions_init(void)
{
/*
* 检查CPU的架构版本是否大于或等于ARMv6.
* ARMv7-M (STM32H750) 是 ARMv7 的一个profile, 所以这个条件为真.
*/
if (cpu_architecture() >= CPU_ARCH_ARMv6) {
/*
* hook_fault_code 是一个函数, 用于将一个故障码(fault code)与一个处理程序关联起来.
*
* 参数1, 4:
* 这是ARMv6/v7 A/R profile架构中, 由数据中止(Data Abort)异常产生的故障码,
* 特指 "I-cache maintenance fault" (指令缓存维护故障).
* ARMv7-M 架构不产生此故障码.
*
* 参数2, do_translation_fault:
* 一个指向处理函数的指针, 该函数负责处理与MMU页表转换相关的故障.
*
* 参数3, SIGSEGV:
* 一个信号编号, 表示如果故障发生在用户模式, 应该向该进程发送段错误信号.
*
* 参数4, SEGV_MAPERR:
* 信号的附加代码, 表示这是一个地址未映射的错误.
*
* 参数5, "I-cache maintenance fault":
* 一个描述此故障的字符串, 用于打印调试信息.
*/
hook_fault_code(4, do_translation_fault, SIGSEGV, SEGV_MAPERR,
"I-cache maintenance fault");
}

/*
* 检查CPU的架构版本是否大于或等于ARMv7.
* 对于STM32H750, 这个条件也为真.
*/
if (cpu_architecture() >= CPU_ARCH_ARMv7) {
/*
* TODO注释翻译: ARMv6K中引入了访问标志位故障. 需要在运行时检查'K'扩展.
*/
/*
* 注册故障码 3 和 6 的处理程序.
* 在ARMv7 A/R profile中, 这两个故障码都表示 "section access flag fault"
* (段访问标志位故障), 这是MMU在检查段描述符中的访问权限位时发现的错误.
* ARMv7-M 架构没有段描述符, 也不产生这些故障码.
*
* do_bad 是一个通用的、用于处理未明确分类的严重错误的函数.
*/
hook_fault_code(3, do_bad, SIGSEGV, SEGV_MAPERR,
"section access flag fault");
hook_fault_code(6, do_bad, SIGSEGV, SEGV_MAPERR,
"section access flag fault");
}

/*
* 对于 initcall, 返回0表示初始化成功.
*/
return 0;
}

/*
* 使用 arch_initcall() 宏将 exceptions_init 函数注册为级别为 "3" 的初始化调用.
* 这确保了异常处理的钩子在内核启动的早期、架构相关的设置阶段就已经被建立.
*/
arch_initcall(exceptions_init);
#endif /* !CONFIG_ARM_LPAE */