ARM学习笔记之——MiniOS
1. 概述
最近,我花了大量的时间学习了杨铸老师写的《深入浅出嵌入式底层软件开发》,看完了ARM体系结构与编程这一章。在这章节的最后,作者做了一个用于总结前面所学内容的操作系统MiniOS,并附带了其中的源代码。我认真学习了其中的所有代码,悟到了其中非常巧妙的构思。
本文引用地址:https://www.eepw.com.cn/article/201611/318808.htm读这个MiniOS源代码我遇到了最大的几个问题如下:
(1)系统是怎么启动的?
(2)开启了MMU后,虚拟地址是怎么映射上物理地址上的?
(3)系统是怎么开启MMU的,为什么开启了MMU内存地址重映射之后程序还能正常运行?
(4)main( ) 函数是怎么变成task0的?
(5)任务之间是怎么切换的?
(6)任务中怎么被创建,并运行起来的?
上述这几个问题都是很细微,但又很难搞清楚的核心知识。笔者在此把自己悟到的东西分享出来,供大家参考。
其它,如:系统函数调用、任务调度机制、LED、UART、按键怎么实现,不做过多研究。
2. 详细内容
2.1 系统是怎么启动的?
首先说明,书上提供的MiniOS工程编译后的运行地址为0x33FF0000,不是 0x00000000,这点很重要。
-info totals -ro-base 0x33ff0000 -first start.o
而程序编译完成后,生成了bin文件将被烧录到NorFlash的0x00000000地址上,也就很重要!
ARM复位后,PC从NorFlash的0x00000000地址上取提,也就是”b Reset“,之后跳到Reset标号上继续执行。代码如下:
- AREAStart,CODE,READONLY
- ENTRY;代码段开始
- bReset
- ……
- Reset;Reset异常处理符号
- blclock_init;跳往时钟初始化处理
- blmem_init;跳往内存初始化处理
- ldrsp,=SVC_STACK;设置管理模式栈指针,common_asm.h中定义
- bldisable_watch_dog;关闭看门狗
之后所有的跳转都是用到b或bl,进行相对跳转。再跳转也是以PC为起始,相对位置跳转,不会受运行地址的影响。
初始化了时钟、SDRAM、关闭看门狗、设置sp。有人可能会问:为什么在进行了bl之后再设置栈指针?其实,哪里设置都无所谓,因为bl指令返回地址只保存在LR寄存器中,不放在栈里。SP被设置成了0x33FF0000,向下扩展,将来还会提及。
然后初始化SDRAM(如果不初始化,SDRAM是不能使用的),将程序自己从0x00000000地址复制一份到0x33FF0000地址上。然后再来一个绝对地址跳转,转到0x33FF0000地址域上的xmain地址处继续执行。如下:
- copy_code;代码拷贝开始符号
- movr0,#0x0;R0中为数据开始地址(ROM数据保存在0地址开始处)
- ldrr1,=|ImageBase|;R1中存放RO输出域运行地址,
RO - ldrr2,=|ImageLimit|;R2中存放ZI输出域结束地址,
ZI - subr2,r2,r1;R2=R2-R1,得出待拷贝数据长度
- blCopyCode2Ram;将R0,R1,R2三个参数传递给CopyCode2Ram函数执行拷贝
- ldrr0,=|ImageBase|
ZI - ldrr1,=|ImageLimit|
ZI - blclear_bss_region
- blstack_init;跳往栈初始化代码处
- msrcpsr_c,#0x5f;开启系统中断,进入系统模式
- ldrlr,=halt_loop;设置返回地址
- ldrpc,=xmain;跳往main函数,进入OS启动处理
- halt_loop
- bhalt_loop;死循环
xmain()函数定议在main.c文件中。
- intxmain(void)
- {
- pgtb_init();//建立页表
- mmu_init();//mmu初始化
- uart_init();//串口初始化
- irq_init();//中断初始化
- Timer0_init();//定时器0初始化
- key_init();//按键初始化
- led_init();//led灯初始化
- }
2.2 开启了MMU后,虚拟地址是怎么映射上物理地址上的?
在xmain函数中,pgtb_init() 函数的功能就是构建页表,TTB=0x300F0000。- voidpgtb_init()
- {
- unsignedlongentry_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
- *(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);
- }
完成之后,虚拟地址映射如下:

访问0x33FF0000~0x33FFFFFF 与 0xFFF00000~0xFFFFFFFF地址是同一块物理内存空间。
0xA0000000~0xA01FFFFF地址指向0x00000000~0x001FFFFF,NorFlash物理空间。
2.3系统是怎么开启MMU的,为什么开启了MMU内存地址重映射之后程序还能正常运行?
在开启MMU之前,数据访问是直接访问物理地址。但是开启了MMU后,所有的地址访问都需要通过一次虚拟地址转换。同样一个地址并不一定提向的同一个数据内间。
那在mmu_init()函数开启MMU之后出现什么样的反应呢?
- voidmmu_init()
- {
- unsignedlongttb=MMU_TABLE_BASE;
- /*reg1待清除位*/
- intreg0,reg1=(VECTOR|ICACHE|R_S_BIT|ENDIAN|DCACHE|ALIGN|MMU_ON);
- /*CP15,C1设置位:异常向量表设置在高地址,使用ICACHE,系统采用小端模式,
- 使用DCACHE,使用地址对齐检查,开启MMU*/
- intCP15_C1_set=(VECTOR|ICACHE|DCACHE|ALIGN|MMU_ON);
- __asm{
- movreg0,#0
- /*使ICaches和DCaches无效*/
- mcrp15,0,reg0,c7,c7,0
- /*使能写入缓冲器*/
- mcrp15,0,reg0,c7,c10,4
- /*使指令,数据TLB无效无效*/
- mcrp15,0,reg0,c8,c7,0
- /*页表基址写入C2*/
- mcrp15,0,ttb,c2,c0,0
- /*将0x2取反变成0xFFFFFFFD,Domain0=0b01为用户模式,其它域为0b11管理模式*/
- mvnreg0,#0x2
- /*写入域控制信息*/
- mcrp15,0,reg0,c3,c0,0
- /*取出C1寄存器中值给reg0*/
- mrcp15,0,reg0,c1,c0,0
- /*先清除不需要的功能,现开启*/
- bicreg0,reg0,reg1
- /*设置相关位并开启MMU*/
- orrreg0,reg0,CP15_C1_set
- mcrp15,0,reg0,c1,c0,0
- }
- //DPRINTK(KERNEL_DEBUG,"MmuinitOK");
- }
刚开始,我在看上面代码的时候,我在想。这个一开启MMU之后,这个函数还能正常返回吗?原来MMU在启时前保存的返回地址(物理地址),在MMU开启后这个地址(虚拟地址)对应的还是原来的物理地址吗?除非一种情况: 虚拟地址与物理地址一致。
上述代码为初始化MMU的函数,当在执行完”mcr p15, 0, reg0, c1, c0, 0“ 指令之后,MMU被开启了。所有的地址访问都要经过MMU转换成物理地址才能访问。而mmu_init()此时运行在SDRAM中0x33FF0000地址域上。由2.2节图中所示,0x30000000~0x33FFFFFF地址空间上的虚拟地址与物理地址是对应的。也就是说,虚拟地址==物理地址。
所以,程序能够正常执行。
2.4main( ) 函数是怎么变成task0的?
OSCreateProcess()函数所创建任务的ID号从1开始计数。至于任务0,就是xmain()函数自己。
xmain()自己怎么跑到task0的位置上去坐着的呢?看main.c代码:
- intxmain(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:process0");
- printk("process0,idle");
- wait(1000000);
- }
- return0;
- }
在执行完 mmu_init 函数之后,所有的数据访问均是通过虚拟地址访问的。包括接下来的UART、IRQ、TIMER0、LED、KEY的初始化,通是访问的虚拟地址。详见uart_init 函数中,读写的寄存器地址。
sched_init() 函数的功能是初始化所有的PCB。在最后,初始化PCB[0]。把 current=&task[0] 。
- /*初始化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号进程
评论