本篇关键词:CP15 、MCR、MRC、ASID、MMU
硬件架构相关篇为:
本篇很重要,对CP15
协处理所有16
个寄存器一一介绍,可能是全网介绍CP15
最全面的一篇,鸿蒙内核的汇编部分(尤其开机启动)中会使用,熟练掌握后看汇编代码将如虎添翼。
协处理器 (co-processor) 顾名思义是协助主处理器完成工作,例如浮点、图像、音频处理这一类外围工作。角色相当于老板的助理/秘书,咱皇上身边的人,专干些咱皇上又不好出面的脏活累活,您可别小看了这个角色,权利不大但能力大,是能通天的人,而且老板越大,身边这样的人还不止一个。
在 arm
的协处理器设计中,最多可以支持 16
个协处理器,通常被命名为 cp0
~cp15
,本篇主要说第16
号协处理器 cp15
关于 cp15
详细介绍见于 << ARM体系架构参考手册(ARMv7-A/R).pdf >> 的 B3.17。
cp15
一共有 16
个32
位的寄存器,其编号为C0 ~ C15
,用来控制cache
、TCM
和存储器管理。cp15
寄存器都是复合功能寄存器,不同功能对应不同的内存实体,全由访问指令的参数来决定,对于 armv7
架构而言,A
系列和 R
系列是统一设计的,A
系列带有 MMU
相关的控制,而 R
系列带有 MPU
相关控制,针对不同的功能需要做区分,同时又因为协处理器 cp15
只支持 16
个寄存器,而需要支持的功能较多,所以通过同一寄存器不同功能的方式来满足需求。
armv7 中对于协处理器的访问,CP15
的寄存器只能被MRC
和MCR
(Move to Coprocessor from ARM Register )指令访问。MCR
表示将 arm
核心寄存器中的值的写到 cp15
寄存器中,MRC
从 cp15
寄存器中读到 arm
核心寄存器中,大部分指令都需要在 PL1
以及更高的特权级下才能正常执行,这是因为 cp15
协处理器大多都涉及到系统和内存的设置,user
模式没有操作权限,user
模式仅能访问 cp15
中有限的几个寄存器比如:ISB、DSB、DMB、TPIDRURW、TPIDRURO 寄存器。
从 `cp**` 寄存器中读到 `arm` 核心寄存器中 MRC<cond> <coproc>, <opc1>, <Rt>, <CRn>, <CRm>{, <opc2>}
啥玩意,太抽象没看懂,后面直接上内核代码就懂了,先看16个寄存器的功能介绍表
c0 寄存器提供处理器和特征识别 ,内核宏定义为,可参考图理解
/*! * Identification registers (c0) | c0 - 身份寄存器 */ #define MIDR CP15_REG(c0, 0, c0, 0) /*! Main ID Register | 主ID寄存器 */ #define MPIDR CP15_REG(c0, 0, c0, 5) /*! Multiprocessor Affinity Register | 多处理器关联寄存器给每个CPU制定一个逻辑地址*/ #define CCSIDR CP15_REG(c0, 1, c0, 0) /*! Cache Size ID Registers | 缓存大小ID寄存器*/ #define CLIDR CP15_REG(c0, 1, c0, 1) /*! Cache Level ID Register | 缓存登记ID寄存器*/ #define VPIDR CP15_REG(c0, 4, c0, 0) /*! Virtualization Processor ID Register | 虚拟化处理器ID寄存器*/ #define VMPIDR CP15_REG(c0, 4, c0, 5) /*! Virtualization Multiprocessor ID Register | 虚拟化多处理器ID寄存器*/
c1 为系统控制寄存器
/*! * System control registers (c1) | c1 - 系统控制寄存器 各种控制位(可读写) */ #define SCTLR CP15_REG(c1, 0, c0, 0) /*! System Control Register | 系统控制寄存器*/ #define ACTLR CP15_REG(c1, 0, c0, 1) /*! Auxiliary Control Register | 辅助控制寄存器*/ #define CPACR CP15_REG(c1, 0, c0, 2) /*! Coprocessor Access Control Register | 协处理器访问控制寄存器*/
/// 读取CP15的系统控制寄存器到 R0寄存器 STATIC INLINE UINT32 OsArmReadSctlr(VOID) { UINT32 val; __asm__ volatile("mrc p15, 0, %0, c1,c0,0" : "=r"(val)); return val; } /// R0寄存器写入CP15的系统控制寄存器 STATIC INLINE VOID OsArmWriteSctlr(UINT32 val) { __asm__ volatile("mcr p15, 0, %0, c1,c0,0" ::"r"(val)); __asm__ volatile("isb" ::: "memory"); }
解读
c1-0-c0-0
行,后边的备注是 SCTLR, System Control Register 系统控制寄存器,其操作模式是支持 Read/Write%0
表示 r0 寄存器,注意这个寄存器是CPU
的寄存器,: "=r"(val) 意思向编译器声明,会修改R0
寄存器的值,改之前提前打好招呼,都是绅士文明人。其实编译器的功能是非常强大的,不仅仅是大家普遍认为的只是编译代码的工具而已。OsArmReadSctlr
的含义就是读取CP15的系统控制寄存器到R0寄存器。volatile
的意思还告诉编译器,不要去优化这段代码,原封不动的生成目标指令。Cache
,并访问实际的内容,而不是Cache!
c7-0-c10-4
c7-0-c10-5
都表示不同的功能含义/*! * Memory protection and control registers (c2 & c3) | c2 - 传说中的TTB寄存器,主要是给MMU使用 c3 - 域访问控制位 */ #define TTBR0 CP15_REG(c2, 0, c0, 0) /*! Translation Table Base Register 0 | 转换表基地址寄存器0*/ #define TTBR1 CP15_REG(c2, 0, c0, 1) /*! Translation Table Base Register 1 | 转换表基地址寄存器1*/ #define TTBCR CP15_REG(c2, 0, c0, 2) /*! Translation Table Base Control Register | 转换表基地址控制寄存器*/ #define DACR CP15_REG(c3, 0, c0, 0) /*! Domain Access Control Register | 域访问控制寄存器*/
看段代码
STATIC INLINE UINT32 OsArmReadTtbr0(VOID) { UINT32 val; __asm__ volatile("mrc p15, 0, %0, c2,c0,0" : "=r"(val)); return val; } STATIC INLINE VOID OsArmWriteTtbr0(UINT32 val) { __asm__ volatile("mcr p15, 0, %0, c2,c0,0" ::"r"(val)); __asm__ volatile("isb" ::: "memory"); } STATIC INLINE UINT32 OsArmReadTtbr1(VOID) { UINT32 val; __asm__ volatile("mrc p15, 0, %0, c2,c0,1" : "=r"(val)); return val; } STATIC INLINE VOID OsArmWriteTtbr1(UINT32 val) { __asm__ volatile("mcr p15, 0, %0, c2,c0,1" ::"r"(val)); __asm__ volatile("isb" ::: "memory"); }
c2
寄存器负责存页表的基地址,即一级映射描述符表的基地址。还记得吗?每个进程的页表都是独立的!c2
值一变,当前使用的页表就发生了变化,页表变化意味着虚拟地址和物理地址的映射关系发生了变化。那么什么情况下会修改里面的值呢?很容易想到只有在进程切换时发生的mmu
上下文切换,直接看代码吧!
/// mmu 上下文切换 VOID LOS_ArchMmuContextSwitch(LosArchMmu *archMmu) { UINT32 ttbr; UINT32 ttbcr = OsArmReadTtbcr();//读取TTB寄存器的状态值 if (archMmu) { ttbr = MMU_TTBRx_FLAGS | (archMmu->physTtb);//进程TTB物理地址值 /* enable TTBR0 */ ttbcr &= ~MMU_DESCRIPTOR_TTBCR_PD0;//使能TTBR0 } else { ttbr = 0; /* disable TTBR0 */ ttbcr |= MMU_DESCRIPTOR_TTBCR_PD0; } #ifdef LOSCFG_KERNEL_VM /* from armv7a arm B3.10.4, we should do synchronization changes of ASID and TTBR. */ OsArmWriteContextidr(LOS_GetKVmSpace()->archMmu.asid);//这里先把asid切到内核空间的ID ISB; //指令必须同步 ,清楚流水线中未执行指令 #endif OsArmWriteTtbr0(ttbr);//通过r0寄存器将进程页面基址写入TTB ISB; //指令必须同步 OsArmWriteTtbcr(ttbcr);//写入TTB状态位 ISB; //指令必须同步 #ifdef LOSCFG_KERNEL_VM if (archMmu) { OsArmWriteContextidr(archMmu->asid);//通过R0寄存器写入进程标识符至C13寄存器 ISB; } #endif }
至于具体内核哪些地方会触发到 mmu
的切换,可前往翻看 (进程切换篇)
c4 没有用于任何 ARMv7 实现,这么不待见4,难道原因跟中国人一样觉得数字不吉利 ,但老师教的老外是不喜欢 13 啊 , 但c13确很重要
c5和c6寄存器提供内存系统故障报告。此外,c6还提供了MPU区域寄存器。这一类寄存器在软件排错时可以提供非常大的帮助,比如通过 DFSR(数据状态寄存器)、IFSR(指令状态寄存器) 的 status bits 可以查到系统 abort 类型,内核中的缺页异常就是通过该寄存器传递异常地址,从而分配页面的。
/*! * Memory system fault registers (c5 & c6) | c5 - 内存失效状态 c6 - 内存失效地址 */ #define DFSR CP15_REG(c5, 0, c0, 0) /*! Data Fault Status Register | 数据故障状态寄存器 */ #define IFSR CP15_REG(c5, 0, c0, 1) /*! Instruction Fault Status Register | 指令故障状态寄存器*/ #define DFAR CP15_REG(c6, 0, c0, 0) /*! Data Fault Address Register | 数据故障地址寄存器*/ #define IFAR CP15_REG(c6, 0, c0, 2) /*! Instruction Fault Address Register | 指令错误地址寄存器*/
c7寄存器提供高速缓存维护操作和内存屏障操作。
c8 寄存器提供 TLB 维护功能
TLB
是硬件上的一个cache
,因为页表一般都很大,并且存放在内存中,所以处理器引入MMU
后,读取指令、数据需要访问两次内存:首先通过查询页表得到物理地址,然后访问该物理地址读取指令、数据。为了减少因为MMU导致的处理器性能下降,引入了TLB
,可翻译为“地址转换后援缓冲器”,也可简称为“快表”。简单地说,TLB
就是页表的Cache
,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB
无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降。详细看
照着图说吧,步骤是这样的。
page table
的基地址就是上面TTB
寄存器值,整个page table
非常大,有多大接下来会讲,所以只能存在内存里,TTB
中只是存一个开始位置而已。CPU
的地址,必须经过MMU
的转换后变成物理内存才能取到真正的指令和数据。TLB
是page table
的迷你版,MMU
先从TLB
里找物理页,找不到了再从page table
中找,从page table
中找到后会放入TLB
中,注意这一步非常非常的关键。因为page table
是属于进程的会有很多个,而TLB
只有一个,不放入就会出现多个进程的page table
都映射到了同一个物理页框而不自知。一个物理页同时只能被一个page table
所映射。但除了TLB
的唯一性外,要做到不错乱还需要了一个东西,就是进程在映射层面的唯一标识符 - asid
,具体可前往翻看 (进程切换篇) 有详细说明。c9 寄存器主要为 cache、分之预测 和 tcm 保留功能,这些保留功能由处理的实现决定
c10 寄存器主要提供内存重映射和 TLB 控制功能
c11 寄存器主要提供 TCM 和 DMA 的保留功能,这些保留功能由处理的实现决定
c12 安全扩展寄存器
c13 寄存器提供进程、上下文以及线程ID处理功能
/*! * Process, context and thread ID registers (c13) | c13 - 进程标识符 */ #define FCSEIDR CP15_REG(c13, 0, c0, 0) /*! FCSE Process ID Register | FCSE(Fast Context Switch Extension,快速上下文切换)进程ID寄存器 位于CPU和MMU之间*/ #define CONTEXTIDR CP15_REG(c13, 0, c0, 1) /*! Context ID Register | 上下文ID寄存器*/ #define TPIDRURW CP15_REG(c13, 0, c0, 2) /*! User Read/Write Thread ID Register | 用户读/写线程ID寄存器*/ #define TPIDRURO CP15_REG(c13, 0, c0, 3) /*! User Read-Only Thread ID Register | 用户只读写线程ID寄存器*/ #define TPIDRPRW CP15_REG(c13, 0, c0, 4) /*! PL1 only Thread ID Register | 仅PL1线程ID寄存器*/
c14 寄存器提供通用定时器扩展的保留功能
ARMv7 保留 c15 用于实现定义的目的,并且不对 c15 编码的使用施加任何限制。
意思就是可以将他当通用寄存器来使用 语法: c15 0-7 c0-c15 0-7
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。按功能模块:
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
< gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,鸿蒙研究站 | weharmonyos 中回复 百万 可方便阅读。
据说喜欢点赞分享的,后来都成了大神。😃