嵌入式MMU原理与S3C2410实战:从地址映射到内存保护
1. 从DOS时代到现代嵌入式:为什么我们需要MMU?
很多刚接触ARM9、Cortex-A这类处理器的嵌入式工程师,第一次在启动代码里看到“MMU初始化”的章节时,可能会感到困惑。我们不是在写单片机程序吗?为什么需要像桌面电脑操作系统一样管理内存?这个问题,得从计算机发展的源头说起。
早期的计算机,比如DOS时代,程序是直接跑在物理内存上的。你的程序有多大,物理内存就得有多大,或者你得自己费劲地把程序切成一块块的“覆盖块”,手动管理哪些部分在内存里,哪些在磁盘上。这不仅是程序员的噩梦,也极大地限制了软件的发展。虚拟内存和MMU的出现,就是为了解决这个根本矛盾: 让程序可以使用比实际物理内存大得多的地址空间 。
在嵌入式领域,尤其是运行Linux、Android等复杂操作系统的场景,MMU不再是“可选配件”,而是核心基石。它主要干三件大事: 地址翻译、内存保护和内存共享 。地址翻译,就是把你程序里写的地址(虚拟地址)转换成实际内存芯片上的地址(物理地址);内存保护,防止你的程序一个“野指针”写飞了,把整个系统搞崩溃;内存共享,让不同的程序可以安全地共用同一段代码库,比如C库。没有MMU,现代多任务操作系统几乎无法稳定运行。
S3C2410这款经典的ARM920T芯片,内置的MMU是理解这些概念的绝佳样板。虽然它已是“上古”芯片,但其MMU的工作原理与现代Cortex-A处理器一脉相承。搞懂它,你就能打通嵌入式系统从裸机到OS的关键一环。接下来,我们就钻进S3C2410的MMU内部,看看它到底是怎么工作的。
2. MMU核心原理:分页与地址转换的数学之美
理解MMU,核心是理解“分页”机制。你可以把整个内存空间想象成一本很厚的书。
- 虚拟地址空间 :就是这本书的完整目录,条目非常多(32位CPU有4G条),每个条目(虚拟地址)都指向书中的某个内容。这个目录是给程序员看的,他以为整本书都在手边。
- 物理地址空间 :就是这本书实际印刷出来的、有限的页数。可能只有目录的十分之一(比如256MB)。
- MMU :就是一个超级高效的图书管理员。程序员根据目录索要第N条内容(虚拟地址),管理员(MMU)会查阅一个 转换表 ,找到这条目录实际对应的、有限的物理书页的位置,并把内容取出来。如果目录指向的内容不在当前的物理书页中(缺页),管理员会触发一个异常,让操作系统这个“仓库管理员”从仓库(比如硬盘或Flash)里把对应的书页换进来。
在S3C2410的MMU中,支持多种“图书分类法”,即映射粒度,主要有段(Section,1MB)、粗页(Coarse Page,通常4KB)和细页(Fine Page,通常1KB)。我们以最常用的 段(Section)映射 为例,因为它最简单直观,在Bootloader初期内存映射中非常常见。
2.1 虚拟地址到物理地址的拆解过程
假设CPU执行一条指令: LDR R0, [0x30000012] 。这里 0x30000012 就是一个虚拟地址。MMU的工作就是把它变成物理地址。
- 提取段索引(Descriptor Index) :在1MB段模式下,虚拟地址被分成两部分。高12位(bit[31:20])是段索引。
0x30000012的二进制是0011 0000 0000 0000 0000 0000 0001 0010。取高12位0011 0000 0000,换算成十进制是768。这意味着这个地址落在第768个段里。 - 查找转换表(Translation Table) :MMU内部有一个寄存器(CP15的C2寄存器)保存着转换表在物理内存中的基地址(TTB)。用这个基地址加上段索引*4(因为每个描述符占4字节),就能找到第768个段描述符(Descriptor)的位置。这个过程是硬件自动完成的。
- 获取段基址(Section Base Address) :从描述符中,MMU读出关键的“段基址”字段(12位)。假设我们之前初始化描述符时,把第768个描述符的段基址字段写成了
0x300。 - 合成物理地址 :物理地址 = (段基址 << 20) | (虚拟地址低20位)。这里
0x300 << 20等于0x30000000(这就是1MB对齐的物理段起始地址)。虚拟地址低20位(bit[19:0])是0x00012。两者相加,得到物理地址0x30000012。
你会发现,在这个例子里,虚拟地址和物理地址一模一样。 这不是巧合,而是我们故意这样配置的 。在嵌入式系统启动初期,我们经常需要一段“平坦映射”,即把SDRAM的虚拟地址直接映射到对应的物理地址上,这样在开启MMU的瞬间,程序还能继续正确运行。这种映射关系完全由软件(你写的启动代码)通过设置转换表来决定,灵活性极高。
注意 :这里有一个关键点,转换表(Translation Table)本身是存放在物理内存中的(比如S3C2410的片内SRAM或SDRAM),它的地址(TTB)必须是一个 物理地址 。因为MMU在查找转换表时,其自身尚未开始工作(或者说,查找转换表这个动作本身需要物理地址)。这就好像图书管理员必须先有一张放在固定位置(物理位置)的对照表,才能开始工作。
2.2 转换表描述符详解
S3C2410的段描述符是一个32位的数,其格式定义如下:
31 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
+--------------------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| Section Base Address | 0 | 0 | 1 | AP | 0 | 0 | 1 | Domain | 1 | 0 | C | B | 1 | 0 | 0 | 0 | 0 | 0 |
+--------------------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
(注:此为Section描述符格式示意,具体位域请参考ARM手册)
- Section Base Address [31:20] :最重要的字段,即12位的物理段基址高12位。低20位默认为0,因此它指向一个1MB对齐的物理地址空间起始点。
- AP [11:10] :访问权限位。这是实现内存保护的关键。
00: 任何模式下都不可访问(触发权限错误)。01: 兼容旧模式,具体权限由CP15的C1寄存器控制。10: 只读(用户模式),读/写(特权模式)。11: 读/写(任何模式)。
- Domain [8:5] :4位域索引。用于在访问控制寄存器中选择一个权限控制策略。这相当于给内存区域分了个“组”,统一管理这个组的访问策略。
- C, B [3:2] :缓存和缓冲位。
- C位 :是否启用缓存(Cache)。
- B位 :是否启用写缓冲(Write Buffer)。
C=1,B=0:Write-Through(直写)模式。C=1,B=1:Write-Back(回写)模式。C=0,B=0:非缓存、非缓冲。这是对外设寄存器映射区域的典型配置。
- 其他位(如bit[19:12], bit[4], bit[1:0])为固定值,用于标识这是一个段描述符。
实操心得 :在编写启动代码初始化MMU时,最常见的错误就是描述符格式写错。务必对照芯片手册的位图,一个bit一个bit地核对。特别是Domain和AP位,设置错误会导致程序跑飞或权限错误。一个稳妥的做法是,先为所有内存区域配置一个宽松的权限(如AP=11,Domain=0),让系统先跑起来,再逐步细化权限设置。
3. S3C2410 MMU初始化与映射实战
理论说得再多,不如一行代码。下面我们来看一个具体的S3C2410 MMU初始化例子,将64MB的SDRAM进行平坦映射,并设置好外设区域。
3.1 步骤一:准备转换表内存
转换表需要4096个条目(4GB / 1MB),每个条目4字节,共16KB。这块内存必须位于物理内存中,并且最好 4KB对齐 (为了性能)。我们通常把它放在SDRAM的起始部分,或者片内SRAM中。
#define MMU_TABLE_SIZE (4096 * 4) // 16KB
#define MMU_TABLE_BASE (0x30000000) // 假设放在SDRAM起始地址
// 在汇编或C中,确保MMU_TABLE_BASE地址是4KB对齐的。
static unsigned long *mmu_tlb_base = (unsigned long *)MMU_TABLE_BASE;
3.2 步骤二:构建段描述符宏
为了方便,我们先定义一些描述符构成的宏:
// 域(Domain)索引,这里我们只使用一个域,索引为0
#define DOMAIN_0 0
// 访问权限AP: 11 (所有模式可读可写)
#define AP_RW_ALL (3 << 10)
// 段描述符类型标识
#define DESC_SEC (2 << 0) // Bit[1:0]=0b10 for Section
// 组合一个段描述符
// base: 物理段基地址的高12位
// dom: 域索引
// ap: 访问权限
// cb: 缓存和缓冲位组合 (C<<2 | B<<3),这里简化处理
#define CREATE_SEC_DESC(base, dom, ap, cb) \
(((base) << 20) | (dom << 5) | (ap) | (cb << 2) | DESC_SEC)
// 常用的缓存模式组合
#define CB_WT (1 << 2) // C=1, B=0, Write-Through
#define CB_WB (1 << 2 | 1 << 3) // C=1, B=1, Write-Back
#define CB_NONE (0) // C=0, B=0, Non-cacheable, Non-bufferable
3.3 步骤三:填充转换表
这是最关键的一步,我们定义整个4GB地址空间的映射关系。
void mmu_create_page_table(void)
{
unsigned int i;
unsigned int descriptor;
// 1. 首先,将整个4GB空间初始化为无效(Fault)
for (i = 0; i < 4096; i++) {
mmu_tlb_base[i] = 0; // 全0的描述符通常表示Fault
}
// 2. 映射64MB SDRAM (0x3000_0000 ~ 0x33FF_FFFF) 为可缓存、可缓冲(Write-Back)
// 平坦映射:虚拟地址 = 物理地址
for (i = 0x300; i < 0x340; i++) { // 索引从0x300到0x33F
descriptor = CREATE_SEC_DESC(i, DOMAIN_0, AP_RW_ALL, CB_WB);
mmu_tlb_base[i] = descriptor;
}
// 3. 映射外设寄存器区域 (Bank0~Bank5, 比如GPIO、UART等)
// 这些区域必须设置为非缓存、非缓冲
// 例如:GPIO控制寄存器在0x5600_0000,属于第0x560个段
for (i = 0x560; i < 0x580; i++) { // 映射一部分外设区域
descriptor = CREATE_SEC_DESC(i, DOMAIN_0, AP_RW_ALL, CB_NONE);
mmu_tlb_base[i] = descriptor;
}
// 类似地,映射其他外设Bank,如0x4800_0000, 0x4900_0000, 0x4A00_0000等
for (i = 0x480; i < 0x4C0; i++) {
descriptor = CREATE_SEC_DESC(i, DOMAIN_0, AP_RW_ALL, CB_NONE);
mmu_tlb_base[i] = descriptor;
}
// 4. (可选)重映射异常向量表
// 通常ARM异常向量表在物理地址0x0。开启MMU后,我们希望通过虚拟地址(例如0xFFFF_0000)也能访问它。
// 这里我们将虚拟地址0x0和0xFFFF_0000都映射到物理地址0x0(片内SRAM或SDRAM开头)。
// 假设我们的异常向量表代码在物理地址0x3000_0000(SDRAM开头),我们可以这样映射:
descriptor = CREATE_SEC_DESC(0x300, DOMAIN_0, AP_RW_ALL, CB_WB); // 物理段0x3000_0000
mmu_tlb_base[0] = descriptor; // 虚拟地址 0x0000_0000 -> 物理 0x3000_0000
mmu_tlb_base[0xFFF] = descriptor; // 虚拟地址 0xFFF0_0000 -> 物理 0x3000_0000? 注意:这里索引是0xFFF,对应基址0xFFF<<20=0xFFF0_0000
// 更常见的做法是重映射0xFFFF_0000,这需要计算索引:0xFFFF0000的高12位是0xFFF。
}
注意事项 :外设寄存器区域的 CB_NONE (非缓存非缓冲)设置至关重要。如果错误地开启了缓存,当你写一个GPIO控制寄存器时,这个写操作可能会被缓存起来,没有立即到达实际的硬件寄存器,导致程序行为异常且难以调试。
3.4 步骤四:设置域访问控制与启用MMU
转换表准备好了,还要告诉MMU两件事:1. 表在哪里;2. 域的访问策略是什么。
@ 假设在ARM汇编中,r0寄存器保存了转换表的基地址(物理地址)
mmu_enable:
@ 1. 设置域访问控制寄存器 (CP15 c3)
@ 将16个域全部设置为“管理者”模式(不进行权限检查)
MOV r0, #0xFFFFFFFF
MCR p15, 0, r0, c3, c0, 0 @ Write Domain Access Control Reg
@ 2. 设置转换表基址寄存器 (CP15 c2)
LDR r0, =MMU_TABLE_BASE @ 获取转换表物理基地址
MCR p15, 0, r0, c2, c0, 0 @ Write Translation Table Base Reg
@ 3. 无效化TLB(快表)
@ 在启用MMU前或修改转换表后,最好无效化TLB,确保CPU读取最新的映射关系
MOV r0, #0
MCR p15, 0, r0, c8, c7, 0 @ Invalidate unified TLB
@ 4. 设置控制寄存器 (CP15 c1),启用MMU、缓存等
MRC p15, 0, r0, c1, c0, 0 @ Read Control Register
ORR r0, r0, #0x1 @ Bit 0: 启用MMU
ORR r0, r0, #(0x1 << 12) @ Bit 12: 启用指令缓存(I-Cache)
ORR r0, r0, #(0x1 << 2) @ Bit 2: 启用数据缓存(D-Cache)
BIC r0, r0, #(0x1 << 13) @ Bit 13: 选择异常向量表在0x0000_0000 (0=low vector)
@ 注意:启用缓存前,必须确保对应的内存区域描述符中C/B位已正确设置。
MCR p15, 0, r0, c1, c0, 0 @ Write Control Register
@ 5. 确保所有指令执行同步(流水线清空)
NOP
NOP
NOP
NOP
MOV pc, lr @ 返回
关键点解析 :
- 域访问控制 :这里简单粗暴地设为
0xFFFFFFFF,意味着所有16个域都配置为0b11(管理者模式),即不进行AP位检查。在实际操作系统中,会根据内核空间、用户空间等划分不同的域,并配置不同的访问策略。 - TLB无效化 :TLB是MMU内部的一个缓存,用于加速地址转换。在切换地址空间(如进程切换)或修改转换表后,必须无效化TLB,否则CPU可能使用旧的、错误的映射,导致灾难性后果。
- 流水线清空 :在启用MMU的指令(
MCR写C1寄存器)之后,需要几条NOP或类似指令。因为启用MMU是瞬间生效的,但CPU的取指流水线里可能还有几条在启用MMU前取出的、使用物理地址的指令。这些指令必须被清空,让后续指令在新的虚拟地址空间下重新获取。
4. 内存保护与访问控制实战解析
MMU的另一个核心功能是内存保护。在S3C2410中,这主要通过**域(Domain) 和 访问权限位(AP)**协同工作来实现。我们来看一个更贴近实际OS的场景。
假设我们设计一个简单的系统,内存空间分为:
- 内核域(Domain 0) :完全信任,可访问所有内存。
- 用户域(Domain 1) :运行用户程序,权限受限。
我们希望实现:
- 用户程序不能访问外设寄存器(
0x48000000开始的区域)。 - 用户程序只能读内核的某些只读数据区(
0x31000000开始的一段),不能写。 - 用户程序有自己的可读写数据区(
0x32000000开始)。
4.1 配置域访问控制寄存器
// 域访问控制寄存器配置
// Domain 0: 0b11 (Manager, 不检查AP)
// Domain 1: 0b01 (Client, 需要检查AP位)
// 其他域: 0b00 (No Access, 任何访问都触发域错误)
unsigned int domain_ctrl = (0x3 << (0*2)) | // Domain 0: Manager
(0x1 << (1*2)) ; // Domain 1: Client
// 写入协处理器
__asm__ volatile (
"mcr p15, 0, %0, c3, c0, 0" : : "r" (domain_ctrl)
);
4.2 细化转换表描述符
#define DOMAIN_KERNEL 0
#define DOMAIN_USER 1
#define AP_NO_ACCESS (0 << 10) // 00
#define AP_PRIV_RW (1 << 10) // 01,具体行为由S/R位决定,这里简化
#define AP_USER_RO (2 << 10) // 10,用户只读,特权可读写
#define AP_FULL_ACCESS (3 << 10) // 11,所有模式可读写
// 1. 外设区域 (0x4800_0000 ~ 0x5FFF_FFFF): 仅内核可访问
for (i = 0x480; i < 0x600; i++) {
mmu_tlb_base[i] = CREATE_SEC_DESC(i, DOMAIN_KERNEL, AP_FULL_ACCESS, CB_NONE);
}
// 2. 内核只读数据区 (0x3100_0000 ~ 0x310F_FFFF, 1MB): 用户只读,内核可读写
for (i = 0x310; i < 0x311; i++) {
mmu_tlb_base[i] = CREATE_SEC_DESC(i, DOMAIN_USER, AP_USER_RO, CB_WB);
}
// 3. 用户数据区 (0x3200_0000 ~ 0x320F_FFFF, 1MB): 用户可读写
for (i = 0x320; i < 0x321; i++) {
mmu_tlb_base[i] = CREATE_SEC_DESC(i, DOMAIN_USER, AP_FULL_ACCESS, CB_WB);
}
// 4. 内核代码和数据区 (0x3000_0000 ~ 0x300F_FFFF): 仅内核可访问
for (i = 0x300; i < 0x301; i++) {
mmu_tlb_base[i] = CREATE_SEC_DESC(i, DOMAIN_KERNEL, AP_FULL_ACCESS, CB_WB);
}
4.3 理解权限检查流程
当CPU(处于User模式)试图访问一个虚拟地址时,MMU的检查流程如下:
- 获取描述符 :根据虚拟地址找到对应的段描述符。
- 检查Domain :读取描述符中的Domain字段(例如
DOMAIN_USER=1)。 - 查询域控制 :根据Domain索引(1),去查域访问控制寄存器对应的两位。我们配置的是
01(Client模式)。 - 进行AP检查 :因为Domain是Client模式,所以需要进一步检查描述符中的AP位。
- 如果访问的是
0x31000000(用户只读区,AP=10),且操作为“读”,则允许。 - 如果对
0x31000000进行“写”操作,则MMU会触发一个 Permission Fault 异常。 - 如果访问的是
0x48000000(外设区,Domain=0),域控制寄存器对应位是11(Manager),MMU会 跳过AP检查 ,直接允许访问。但此时CPU处于User模式,而该区域的描述符是DOMAIN_KERNEL,这通常意味着用户程序根本不应该有这个区域的映射,如果错误映射了,Manager模式也会放行。因此, 安全模型的核心在于不要将内核区域映射到用户进程的地址空间 。
- 如果访问的是
- 触发异常 :如果AP检查不通过,CPU会跳转到预定义的异常向量(Data Abort)执行。操作系统内核的异常处理程序会介入,通常的做法是向触发错误的用户进程发送一个信号(如SIGSEGV),终止该进程。
实操心得 :调试内存保护错误(Data Abort)是嵌入式开发中的难点。当发生这种错误时,首先要查看CP15的故障地址寄存器(FAR)和故障状态寄存器(FSR),它们会告诉你出错的虚拟地址和错误类型(是权限错误还是转换错误)。结合你的映射表,就能快速定位是哪个区域的配置出了问题。
5. 高级话题:TLB、Cache与MMU的协同
MMU不是孤立工作的,它与CPU的Cache(缓存)和TLB紧密耦合,理解它们的交互对写出高效、正确的代码至关重要。
5.1 TLB:MMU的加速器
每次地址转换都去读内存里的转换表(16KB),效率太低了。因此,MMU内部有一个叫做TLB的缓存,它保存了最近使用过的虚拟地址到物理地址的转换条目。当CPU给出一个虚拟地址时,MMU首先在TLB中查找(这非常快),如果找到(TLB命中),就直接使用;如果没找到(TLB未命中),才去查内存中的转换表,并将结果装入TLB。
TLB管理操作 :
- 无效化整个TLB :
MCR p15, 0, Rd, c8, c7, 0。在启用MMU前、切换进程地址空间后必须做。 - 无效化单个虚拟地址的TLB条目 :
MCR p15, 0, Rd, c8, c7, 1。在某些情况下,你只修改了某个地址的映射,可以只无效化它,性能更好。 - 锁定TLB条目 :有些关键的内核代码路径(如异常处理函数)对性能要求极高,不能忍受TLB未命中带来的延迟。ARM MMU支持将特定的TLB条目锁定,使其不会被替换出去。
5.2 Cache与写缓冲:一致性问题
Cache和写缓冲(Write Buffer)能极大提升系统性能,但它们引入了“数据一致性”问题。
- Cache :缓存了物理内存的内容。当CPU读数据时,如果Cache里有(命中),就直接从Cache读,不访问内存。
- 写缓冲 :当CPU写数据时,可能先写到一个小缓冲区,然后由缓冲区异步写回内存,这样CPU就不用等待慢速的内存写操作完成。
问题来了 :
- DMA操作 :外设通过DMA直接读写物理内存,它绕过了CPU的Cache。如果CPU Cache里有一份该内存数据的旧副本,而DMA更新了物理内存,那么CPU后续读到的就是过时的数据(Cache不一致)。反之,如果CPU更新了Cache但还没写回内存,DMA读到的是旧数据。
- 自修改代码 :如果程序修改了正在执行的指令(虽然不常见),修改的是Cache或内存中的指令,但CPU取指流水线里可能还有旧的指令副本。
解决方案 :通过MMU描述符中的 C(Cacheable)和B(Bufferable)位 来控制。
- 对于普通SDRAM(代码、数据) :通常设置
C=1, B=1(Write-Back with Write-Allocate)。这是性能最好的模式。但需要软件在DMA操作前后,使用clean(将Cache数据写回内存)和invalidate(使Cache数据失效)操作来维护一致性。ARM提供了CP15相关指令来做这件事。 - 对于外设寄存器 : 必须 设置
C=0, B=0。因为对外设的读写有严格的顺序要求和副作用(比如读一个状态寄存器会清除中断标志),绝对不能缓存或缓冲。 - 帧缓冲区(Frame Buffer) :如果LCD控制器通过DMA从内存取数据显示,这块内存通常设置为
C=0(非缓存),但B位可以视情况而定。或者设置为C=1,但在更新完显存数据后,手动cleanCache对应的区域,确保数据已写入物理内存。
一个典型的DMA数据接收流程 :
// 1. 准备接收缓冲区(虚拟地址virt_buf,物理地址phys_buf)
// 2. 在启动DMA接收前,确保Cache中该缓冲区的数据(如果有)被写回内存,并失效化,防止读到旧数据。
clean_and_invalidate_dcache_range(virt_buf, buf_size);
// 3. 启动DMA从外设接收数据到phys_buf
start_dma_receive(phys_buf);
// 4. 等待DMA完成
wait_for_dma_complete();
// 5. 在CPU读取virt_buf的数据前,失效化Cache中对应的行,确保CPU从物理内存读取DMA刚写入的新数据。
invalidate_dcache_range(virt_buf, buf_size);
// 6. 现在可以安全地使用virt_buf里的数据了
process_data(virt_buf);
5.3 实际调试中的常见问题与排查
-
开启MMU后系统立刻跑飞 :
- 检查TTB :确保写入CP15 C2寄存器的转换表基地址是 物理地址 ,并且该地址所在的内存区域在启用MMU前已经被正确映射(至少它自身所在的1MB区域需要被映射,且权限足够)。
- 检查初始映射 :在启用MMU的瞬间,CPU要取的下一条指令的地址(即PC指针)必须在当前有效的映射内。通常的做法是,在转换表中建立一段“平坦映射”,即当前运行代码的物理地址段,其虚拟地址等于物理地址。这样在切换的瞬间,PC指向的虚拟地址能正确映射回原来的物理地址,保证指令流不中断。
- 检查域访问控制 :最简单的方法是先将域访问控制寄存器设为
0xFFFFFFFF(全部Manager),排除权限问题。
-
数据访问错误(Data Abort) :
- 查看FAR和FSR :这是第一手资料。FSR会告诉你错误类型(对齐错误、转换错误、权限错误等)。FAR告诉你出错的虚拟地址。
- 核对转换表 :根据FAR计算段索引,去你的转换表中查看对应的描述符是否正确(地址、Domain、AP、C/B)。
- 检查当前模式 :是在User模式还是Supervisor模式?当前模式的权限是否满足AP位的要求?
-
使能Cache后程序行为异常 :
- 检查DMA区域 :所有用于DMA缓冲区的内存,其描述符的C/B位配置是否正确?软件是否做了必要的Cache维护操作?
- 检查自修改代码 :如果有动态生成代码的情况,在写入新指令后,需要
clean D-Cache并invalidate I-Cache。 - 检查外设区域 :绝对确保所有外设寄存器的映射区域是
Non-cacheable, Non-bufferable (C=0,B=0)。
-
性能问题 :
- TLB抖动 :如果程序频繁访问的地址范围很大,超过了TLB容量,会导致大量的TLB未命中,性能下降。可以考虑使用更大的页(如64KB的Section在某些ARM变体中支持),或者优化代码和数据布局,使其访问更局部化。
- Cache抖动 :类似TLB,如果数据访问模式非常随机,Cache命中率会很低。这更多是算法和数据结构的优化问题。
理解并掌握S3C2410的MMU,是迈向复杂嵌入式系统开发的必经之路。它不再是把CPU当成一个更快的单片机来用,而是开始以系统的视角来管理资源、隔离错误、提升效率。虽然现代Cortex-A处理器的MMU更复杂(支持多级页表、ASID等),但其核心思想——地址翻译、内存保护、与Cache协同——是完全一致的。把这套原理吃透,再去看Linux内核的内存管理、进程地址空间等概念,就会有一种豁然开朗的感觉。
更多推荐

所有评论(0)