最近,我花了大量的时间学习了杨铸老师写的《深入浅出嵌入式底层软件开发》,看完了ARM体系结构与编程这一章。在这章节的最后,作者做了一个用于总结 前面所学内容的操作系统MiniOS,并附带了其中的源代码。我认真学习了其中的所有代码,悟到了其中非常巧妙的构思。
读这个MiniOS源代码我遇到了最大的几个问题如下:
(1)系统是怎么启动的?
(2)开启了MMU后,虚拟地址是怎么映射上物理地址上的?
(3)系统是怎么开启MMU的,为什么开启了MMU内存地址重映射之后程序还能正常运行?
(4)main( ) 函数是怎么变成task0的?
(5)任务之间是怎么切换的?
(6)任务中怎么被创建,并运行起来的?
上述这几个问题都是很细微,但又很难搞清楚的核心知识。笔者在此把自己悟到的东西分享出来,供大家参考。
其它,如:系统函数调用、任务调度机制、LED、UART、按键怎么实现,不做过多研究。
AREA Start, CODE, READONLY ENTRY ; 代码段开始 b Reset …… Reset ; Reset异常处理符号 bl clock_init ; 跳往时钟初始化处理 bl mem_init ; 跳往内存初始化处理 ldr sp, =SVC_STACK ; 设置管理模式栈指针,common_asm.h中定义 bl disable_watch_dog ; 关闭看门狗之后所有的跳转都是用到b或bl,进行相对跳转。再跳转也是以PC为起始,相对位置跳转,不会受运行地址的影响。
copy_code ; 代码拷贝开始符号 mov r0, #0x0 ; R0中为数据开始地址 (ROM数据保存在0地址开始处) ldr r1, =|Image$$RO$$Base| ; R1中存放RO输出域运行地址, ldr r2, =|Image$$ZI$$Limit| ; R2中存放ZI输出域结束地址, sub r2, r2, r1 ; R2 = R2 - R1,得出待拷贝数据长度 bl CopyCode2Ram ; 将R0,R1,R2三个参数传递给CopyCode2Ram函数执行拷贝 ldr r0, =|Image$$ZI$$Base| ldr r1, =|Image$$ZI$$Limit| bl clear_bss_region bl stack_init ; 跳往栈初始化代码处 msr cpsr_c, #0x5f ; 开启系统中断,进入系统模式 ldr lr, =halt_loop ; 设置返回地址 ldr pc, =xmain ; 跳往main函数,进入OS启动处理 halt_loop b halt_loop ; 死循环
在执行了”ldr pc, =xmain“这条指令之后,PC就指向了SDRAM的0x33FF0000地址区域上了,不再是NorFlash上了,从此达到了运行地址与加载地址的统一。谨记!
xmain()函数定议在main.c文件中。
int xmain(void) { pgtb_init(); // 建立页表 mmu_init(); // mmu初始化 uart_init(); // 串口初始化 irq_init(); // 中断初始化 Timer0_init(); // 定时器0初始化 key_init(); // 按键初始化 led_init(); // led灯初始化 }
void pgtb_init() { unsigned long entry_index, SFR_base; /* 建立到Norflash的2MB的地址空间的映射 */ /* 0xA0000000 映射到0开始的1MB地址空间 */ *( mmu_tlb_base + (0xA0000000 >> 20) ) = 0x0 | SEC_DESC; /* 0xA0100000 映射到0x100000~0x1FFFFF的1MB地址空间 */ *( mmu_tlb_base + (0xA0100000 >> 20) ) = 0x100000 | SEC_DESC; /* 令0x30000000~0x34000000的64MB虚拟地址等于物理地址空间,方便miniOS内部进程管理 */ for(entry_index = 0x30000000; entry_index < 0x34000000; entry_index += 0x100000) { *( mmu_tlb_base + (entry_index >> 20) ) = entry_index | SEC_DESC; } /* 特殊功能寄存器0x48000000~0x60000000地址空间映射到0xC8000000~0xE0000000虚拟地址空间 */ for(entry_index = 0x48000000 + 0x80000000, SFR_base = 0x48000000; SFR_base < 0x60000000 ; entry_index += 0x100000, SFR_base += 0x100000 ){ *(mmu_tlb_base+(entry_index>>20)) = SFR_base | SEC_DESC; } /* * 进程1-23号进程地址空间,每个进程32MB,miniOS允许进程使用32MB虚拟地址空间,但是只分配其1MB的实际物理空间 * 进程1:物理地址空间 0x30100000-0x301fffff,对应MVA(修正虚拟地址,进程PID<<25形成) * MVA地址空间:0x02000000-0x021fffff * 进程2:物理地址空间 0x30200000-0x302fffff * MVA地址空间:0x04000000-0x041fffff * ... ... ... * 进程23:物理地址空间 0x31700000-0x317fffff * MVA地址空间:0x2E000000-0x2E1fffff * 对应进程24由于MVA地址空间是0x30000000是物理内存起始空间,该空间用来放置页表,并且前面已经用该 * 地址空间做了映射,因此它不能被映射成,24号进程的物理地址空间,跳过该进程号24,同样道理, * 跳过进程号25 * 进程24:物理地址空间 0x31800000-0x318fffff * MVA地址空间:0x30000000-0x31ffffff * 进程25:物理地址空间 0x31900000-0x319fffff * MVA地址空间:0x32000000-0x33ffffff */ for(entry_index = 1; entry_index < 24; entry_index++){ *(mmu_tlb_base+((entry_index*0x02000000)>>20)) = (entry_index*0x00100000+SDRAM_BASE) | SEC_DESC; } /* * 进程26:物理地址空间 0x31A00000-0x31Afffff * MVA地址空间:0x34000000-0x35ffffff * ... ... ... * 进程62:物理地址空间 0x33E00000-0x33Efffff * MVA地址空间:0xC4000000-0xC5ffffff */ for(entry_index = 26; entry_index < TASK_SZ; entry_index++){ *(mmu_tlb_base+((entry_index*0x02000000)>>20)) = (entry_index*0x00100000+SDRAM_BASE) | SEC_DESC; } /* * 异常向量表 * 0xFFFF0000为高地址异常向量表,可以通常设置CP15,C1寄存器V位,当异常产生时,由硬件自动去0xFFFF0000 * 地址处执行异常跳转执行,而不是之前的0地址处异常向量表跳转,我们将该虚拟地址映射到0x33F00000这1MB地址 * 空间,同样,将全部miniOS代码拷贝到这1MB地址空间来。 */ *(mmu_tlb_base + (0xffff0000>>20)) = ((VECTORS_PHY_BASE) | SEC_DESC); }
void mmu_init() { unsigned long ttb = MMU_TABLE_BASE; /* reg1待清除位 */ int reg0, reg1 = (VECTOR | ICACHE | R_S_BIT | ENDIAN | DCACHE | ALIGN | MMU_ON); /* CP15,C1设置位:异常向量表设置在高地址,使用ICACHE,系统采用小端模式, 使用DCACHE, 使用地址对齐检查,开启MMU */ int CP15_C1_set = (VECTOR | ICACHE | DCACHE | ALIGN | MMU_ON); __asm{ mov reg0, #0 /* 使ICaches和DCaches无效 */ mcr p15, 0, reg0, c7, c7, 0 /* 使能写入缓冲器 */ mcr p15, 0, reg0, c7, c10, 4 /* 使指令,数据TLB无效无效 */ mcr p15, 0, reg0, c8, c7, 0 /* 页表基址写入C2 */ mcr p15, 0, ttb, c2, c0, 0 /* 将0x2取反变成0xFFFFFFFD,Domain0 = 0b01为用户模式,其它域为0b11管理模式 */ mvn reg0, #0x2 /* 写入域控制信息 */ mcr p15, 0, reg0, c3, c0, 0 /* 取出C1寄存器中值给reg0 */ mrc p15, 0, reg0, c1, c0, 0 /* 先清除不需要的功能,现开启 */ bic reg0, reg0, reg1 /* 设置相关位并开启MMU */ orr reg0, reg0, CP15_C1_set mcr p15, 0, reg0, c1, c0, 0 } //DPRINTK(KERNEL_DEBUG, "Mmu init OK"); }刚开始,我在看上面代码的时候,我在想。这个一开启MMU之后,这个函数还能正常返回吗?原来MMU在启时前保存的返回地址(物理地址),在MMU开启后这个地址(虚拟地址)对应的还是原来的物理地址吗?除非一种情况: 虚拟地址与物理地址一致。
int xmain(void) { // PC=0x33FF???? , SP=0x33FF0000 , MMU=关 pgtb_init(); // 建立页表 mmu_init(); // mmu初始化 // PC=0x33FF???? , SP=0x33FF0000 , MMU=开 // 对UART、IRQ、TIMER0、LED、KEY进行初始化 OS_ENTER_CRITICAL(); // 关闭中断,准备进入进程初始化函数 sched_init(); // 进程调度初始化 OS_EXIT_CRITICAL(); // 开启中断 ENTER_USR_MODE(); // 进入用户模式 // 进程0执行内容 while(1){ DPRINTK(KERNEL_DEBUG,"kernel:process 0"); printk("process 0, idle"); wait(1000000); } return 0; }执行到 xmain 函数时,PC地址是在 SDRAM 的 0x33FF???? 上的,而且SP栈指针在 start.s 中已指定向了 0x33FF0000。
/* 初始化0号进程 */ p = &task[0]; // p指向0号进程PCB p->pid = 0; // 设置0号进程pid p->state = TASK_RUNNING; // 设置其运行状态为就绪态 p->count = 5; // 设置其时间片为5 p->priority = 5; // 设置优先级为5 p->content[0] = 0x5f; // 保存状态寄存器cpsr值,表示为系统模式,开启中断 p->content[1] = SYS_MODE_STACK_BASE; // 设置当前进程栈指针 p->content[2] = 0; p->content[16]= 0; // 设置PC寄存器的值为0,该进程起始地址被MMU映射为0地址 current = &task[0]; // 当前运行进程为0号进程