当前位置: 首页 > news >正文

ARM9学习笔记之——MiniOS

1. 概述

       最近,我花了大量的时间学习了杨铸老师写的《深入浅出嵌入式底层软件开发》,看完了ARM体系结构与编程这一章。在这章节的最后,作者做了一个用于总结 前面所学内容的操作系统MiniOS,并附带了其中的源代码。我认真学习了其中的所有代码,悟到了其中非常巧妙的构思。

       读这个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标号上继续执行。代码如下:

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为起始,相对位置跳转,不会受运行地址的影响。
       初始化了时钟、SDRAM、关闭看门狗、设置sp。有人可能会问:为什么在进行了bl之后再设置栈指针?其实,哪里设置都无所谓,因为bl指令返回地址只保存在LR寄存器中,不放在栈里。SP被设置成了0x33FF0000,向下扩展,将来还会提及。
       然后初始化SDRAM(如果不初始化,SDRAM是不能使用的),将程序自己从0x00000000地址复制一份到0x33FF0000地址上。然后再来一个绝对地址跳转,转到0x33FF0000地址域上的xmain地址处继续执行。如下: 

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灯初始化  
}

2.2 开启了MMU后,虚拟地址是怎么映射上物理地址上的?

       在xmain函数中,pgtb_init() 函数的功能就是构建页表,TTB=0x300F0000。

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);
}

     完成之后,虚拟地址映射如下: 
       访问0x33FF0000~0x33FFFFFF 与 0xFFF00000~0xFFFFFFFF地址是同一块物理内存空间。
       0xA0000000~0xA01FFFFF地址指向0x00000000~0x001FFFFF,NorFlash物理空间。

2.3 系统是怎么开启MMU的,为什么开启了MMU内存地址重映射之后程序还能正常运行?

       在开启MMU之前,数据访问是直接访问物理地址。但是开启了MMU后,所有的地址访问都需要通过一次虚拟地址转换。同样一个地址并不一定提向的同一个数据内间。
       那在mmu_init()函数开启MMU之后出现什么样的反应呢? 

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开启后这个地址(虚拟地址)对应的还是原来的物理地址吗?除非一种情况: 虚拟地址与物理地址一致。
       上述代码为初始化MMU的函数,当在执行完” mcr p15, 0, reg0, c1, c0, 0“ 指令之后,MMU被开启了。所有的地址访问都要经过MMU转换成物理地址才能访问。而mmu_init()此时运行在SDRAM中0x33FF0000地 址域上。由2.2节图中所示,0x30000000~0x33FFFFFF地址空间上的虚拟地址与物理地址是对应的。也就是说,虚拟地址==物理地址。
       所以,程序能够正常执行。

2.4 main( ) 函数是怎么变成task0的?

       OSCreateProcess()函数所创建任务的ID号从1开始计数。至于任务0,就是xmain()函数自己。
       xmain()自己怎么跑到task0的位置上去坐着的呢?看main.c代码: 

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。
       在执行完 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号进程


   

相关文章:

  • 线程同步之条件变量
  • 混合牛奶 | 贪心算法 (USACO练习题)
  • Solarized Scheme
  • Leetcode题目:Symmetric Tree
  • spring测试junit事务管理及spring面向接口注入和实现类单独注入(无实现接口),实现类实现接口而实现类单独注入否则会报错。...
  • CodeForces 660C Hard Process
  • 流媒体选择Nginx是福还是祸?
  • 解读Secondary NameNode的功能
  • linux下Bash函数功能之编写脚本(十二)
  • PHP~foreach遍历名单数组~有必要多次观看练习
  • 玩聚的博客墙 V
  • 第九周周记
  • Linux系统下proc目录详解
  • 每天一个linux命令:du
  • 【Swift学习】Swift编程之旅---类和结构体(十三)
  • 【5+】跨webview多页面 触发事件(二)
  • 002-读书笔记-JavaScript高级程序设计 在HTML中使用JavaScript
  • android高仿小视频、应用锁、3种存储库、QQ小红点动画、仿支付宝图表等源码...
  • CSS选择器——伪元素选择器之处理父元素高度及外边距溢出
  • Java Agent 学习笔记
  • jquery cookie
  • laravel5.5 视图共享数据
  • leetcode388. Longest Absolute File Path
  • Mybatis初体验
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • vue:响应原理
  • 记一次用 NodeJs 实现模拟登录的思路
  • 扑朔迷离的属性和特性【彻底弄清】
  • 区块链技术特点之去中心化特性
  • 设计模式(12)迭代器模式(讲解+应用)
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 学习HTTP相关知识笔记
  • 用mpvue开发微信小程序
  • 原生JS动态加载JS、CSS文件及代码脚本
  • 自制字幕遮挡器
  • No resource identifier found for attribute,RxJava之zip操作符
  • [Shell 脚本] 备份网站文件至OSS服务(纯shell脚本无sdk) ...
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • ​如何防止网络攻击?
  • (2021|NIPS,扩散,无条件分数估计,条件分数估计)无分类器引导扩散
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (黑马C++)L06 重载与继承
  • (四)模仿学习-完成后台管理页面查询
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (转) RFS+AutoItLibrary测试web对话框
  • (转)http-server应用
  • **PHP二维数组遍历时同时赋值
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET 中创建支持集合初始化器的类型
  • .net解析传过来的xml_DOM4J解析XML文件
  • .Net开发笔记(二十)创建一个需要授权的第三方组件