第4章 L1页表配置实战:基地址、域访问控制与共享属性

好,咱们直接进入正题。L1页表,说白了就是MMU的第一道关卡。你配置对了,CPU就能顺畅地访问内存;配错了,系统直接挂掉,连个报错都来不及给你。我在几个项目里都吃过这个亏,所以这一章咱们把L1页表的三个核心参数——基地址、域访问控制、共享属性——彻底讲透。

4.1 L1页表基地址:系统内存的“根”

L1页表的基地址,就是页表本身在物理内存中的起始位置。ARM架构要求这个地址必须16KB对齐。为什么?因为L1页表有4096个条目,每个条目4字节,总共16KB。硬件会直接拿这个基地址去索引,不对齐的话,索引会错位。

关键约束:L1页表基地址必须16KB对齐(低14位为0)。

我在一个项目中遇到过这样的问题:同事把基地址写成了0x80001000,结果系统启动时MMU一使能就跳飞。查了半天,发现是地址没对齐。嗯,从那以后我每次配页表都会用宏来检查对齐。

配置基地址的寄存器是TTBR0(Translation Table Base Register 0)。代码很简单,但要注意顺序:

// 假设L1页表放在0x80000000
#define L1_PAGE_TABLE_BASE  0x80000000

// 确保对齐
static_assert((L1_PAGE_TABLE_BASE & 0x3FFF) == 0, 
              "L1页表基地址必须16KB对齐");

// 写入TTBR0
__asm volatile("MCR p15, 0, %0, c2, c0, 0" 
               : : "r" (L1_PAGE_TABLE_BASE));

你想想看,这一步其实就是在告诉CPU:“你的内存地图在这里,去查吧。” 基地址一旦写死,整个系统的虚拟地址到物理地址的映射就全靠它了。

4.2 域访问控制:内存区域的“门禁系统”

ARM的域(Domain)机制,说白了就是给内存区域贴标签。L1页表的每个条目里有一个3位的域字段(Domain),可以指定0-15共16个域。然后CP15的域访问控制寄存器(DACR)决定每个域怎么被访问。

DACR每2位编码 访问权限 说明
00 无访问 任何访问都产生域异常
01 客户模式 由页表条目中的AP位决定
10 保留 不要使用
11 管理者模式 不检查AP位,完全放行

我个人习惯把关键的系统内存(比如中断向量表、页表本身)放在域0,并设为管理者模式。普通应用程序的内存放在域1,设为客户模式,靠AP位做细粒度控制。

实战建议:域0通常用作“安全域”,所有系统级内存都放这里。DACR中域0配成0b11(管理者),其他域按需配置。这样即使应用程序的AP位配错了,系统关键内存也不会被误写。

配置DACR的代码也很直接:

// 域0: 管理者(11), 域1: 客户(01), 其他域: 无访问(00)
#define DACR_VALUE  (0b11 << 0) | (0b01 << 2)

__asm volatile("MCR p15, 0, %0, c3, c0, 0" 
               : : "r" (DACR_VALUE));

我曾经在一个项目里把DACR配成了全0,结果系统一跑用户程序就崩。你猜怎么着?所有域都是“无访问”,连内核自己都进不了内存。嗯,这个坑我替你们踩过了。

4.3 共享属性:多核系统的“缓存一致性”命脉

共享属性(Shareable)是L1页表条目里的一个位。它告诉硬件:这块内存会不会被多个核或者DMA访问。如果设成非共享(Non-Shareable),CPU可以自由地缓存数据,不用通知别人。如果设成共享(Shareable),硬件必须保证缓存一致性。

为什么这个位重要?我举个例子。你在一个双核系统里,核0往地址0x1000写了一个值,核1去读同一个地址。如果0x1000被设成了Non-Shareable,核0可能只写到了自己的L1缓存里,核1读到的就是旧数据。这种bug极难复现,我调试了整整两周才找到原因。

注意:共享属性只对Normal内存有意义。对于Device内存(比如MMIO寄存器),必须设为Non-Shareable,因为外设寄存器不存在缓存一致性问题。

在L1页表条目中,共享属性位在bit 1(对于段映射)或bit 9(对于页映射)。配置时要注意:

// L1段描述符格式 (32位)
// bit[1]: 共享属性 (0=Non-Shareable, 1=Shareable)
// bit[18:16]: 域编号
// bit[31:20]: 段基地址

// 配置一个共享的段描述符
#define SECTION_SHAREABLE    (1 << 1)
#define DOMAIN0              (0 << 16)
#define SECTION_BASE_ADDR    0x80000000

uint32_t l1_entry = SECTION_BASE_ADDR | 
                    SECTION_SHAREABLE | 
                    DOMAIN0 | 
                    0x400;  // AP=完全访问

// 写入L1页表
L1_PAGE_TABLE_BASE[0] = l1_entry;  // 虚拟地址0-1MB映射到物理0x80000000

4.4 实战:完整配置一个L1页表条目

好了,咱们把三个参数串起来,写一个完整的L1页表配置函数。这个函数我在好几个项目里复用,基本没出过问题。

// L1页表条目配置结构体
typedef struct {
    uint32_t    phys_base;      // 物理基地址 (1MB对齐)
    uint8_t     domain;         // 域编号 (0-15)
    uint8_t     shareable;      // 0=Non-Shareable, 1=Shareable
    uint8_t     ap;             // 访问权限 (0-3)
    uint8_t     tex;            // 内存类型指示
    uint8_t     cacheable;      // 0=Non-Cacheable, 1=Cacheable
    uint8_t     bufferable;     // 0=Non-Bufferable, 1=Bufferable
} l1_section_cfg_t;

uint32_t l1_section_create(l1_section_cfg_t *cfg) {
    uint32_t desc = 0;
    
    // 检查物理地址对齐
    if (cfg->phys_base & 0xFFFFF) {
        return 0;  // 错误:不是1MB对齐
    }
    
    // 构建描述符
    desc = cfg->phys_base;                    // bit[31:20]
    desc |= (cfg->domain & 0xF) << 16;       // bit[18:16]
    desc |= (cfg->shareable & 1) << 1;       // bit[1]
    desc |= (cfg->ap & 3) << 10;             // bit[11:10]
    desc |= (cfg->tex & 7) << 12;            // bit[14:12]
    desc |= (cfg->cacheable & 1) << 3;       // bit[3]
    desc |= (cfg->bufferable & 1) << 2;      // bit[2]
    desc |= 0x400;                            // 段描述符标识
    
    return desc;
}

// 使用示例:映射DDR为共享、可缓存
l1_section_cfg_t ddr_cfg = {
    .phys_base  = 0x80000000,
    .domain     = 0,
    .shareable  = 1,
    .ap         = 3,      // 完全访问
    .tex        = 1,      // Normal内存
    .cacheable  = 1,
    .bufferable = 1
};

uint32_t entry = l1_section_create(&ddr_cfg);
L1_PAGE_TABLE_BASE[0] = entry;  // 虚拟地址0-1MB

核心要点:L1页表配置就是三个参数的组合游戏——基地址决定页表在哪,域决定谁有权访问,共享属性决定缓存一致性策略。三者缺一不可,顺序也不能错。

4.5 知识结构总览

下面这张图把L1页表配置的完整流程和三个核心参数的关系画出来了。你可以把它当作配置时的检查清单。

L1页表配置核心流程 L1页表基地址 (TTBR0) L1页表条目 (4096个,每个4字节) 域访问控制 DACR寄存器 16个域 × 2位 共享属性 Shareable位 缓存一致性策略 其他属性 AP/TEX/C/B 访问权限+类型 MMU使能 → 系统运行 配置顺序:基地址 → 页表条目 → DACR → 共享属性 → MMU使能

这张图里我把三个核心参数放在同一层,它们之间没有先后依赖,但都必须在MMU使能之前配好。我个人习惯先配基地址,再填页表条目,最后配DACR和共享属性。这个顺序能避免中间状态导致异常。

调试技巧:如果MMU使能后系统异常,先检查基地址对齐,再检查DACR是否把关键域设成了“无访问”。这两个问题占了L1页表配置错误的80%以上。