uboot第一阶段 start.S代码分析

news2024/12/24 21:19:54

u-boot.lds中找到start.S入口
(1)C语言规定整个项目的入口就是main函数。
(2)在uboot中因为有汇编阶段参与,因此不能直接找main.c。整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start)因此定义_start符号
的文件就是整个程序的起始文件,即start.S。

0、宏定义头文件包含

(1)config.h在include目录下,是配置过程中生成的文件,即mkconfig脚本中生成的,,这个文件的内容是包含了一个头文件:#include<configs/x210_sd.h>,这个头文件的内容是一堆宏定义,是跟uboot的配置相关,通过条件编译影响uboot的走向,使得uboot具有可移植性。

(2)version.h中包含了include/version_autogenerated.h,这个头文件里面就一行内容:#define U_BOOT_VERSION “U-Boot 1.3.4”。uboot的版本号信息,这个宏定义的内容是一个字符串,字符串中的版本号信息来自于Makefile中的配置值。这个宏在程序中会被调用,在uboot启动过程中会串口打印出uboot的版本号,那个版本号信息就是从这来的。

(3)#include <asm/proc/domain.h>。asm目录不是uboot中的原生目录,uboot中是没有asm这个目录的。asm目录是mkconfig脚本配置时创建的一个符号链接,实际指向的目录是就是asm-arm,因此实际要包含的头文件是:include/asm-arm/proc-armv/domain.h。

(4)
1.从这里可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到asm/proc/domain.h头文件。
2.之所以uboot不能在windows的共享文件夹下配置编译,是因为windows中没有符号链接,mkconfig配置时在windows环境下生成不了符号链接,编译会报错。
3.之所以不直接包含asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h,是为了可移植性。因为如果直接包含,则start.S文件就和CPU架构(和硬件)有关了,可移植性就差了。譬如当我要把uboot移植到mips架构下,则start.S源代码中所有的头文件包含全部要修改,这里asm-arm目录就要修改成asm-mips目录。我们用了符号链接之后,则start.S中源代码不用改,只需要在具体的硬件移植时配置不同,创建的符号链接指向的不同,则可以具有可移植性。

#include <config.h>
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>

1、留存16B校验头

(1)start.S中在开头位置放了16字节的填充占位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。

#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
    .word 0x2000
    .word 0x0
    .word 0x0
    .word 0x0
#endif

2、异常向量表的定义(未精细处理)

(1)异常向量表是硬件决定的,软件只是参照硬件的设计来实现它。

(2)异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常。因为uboot的核心任务就是启动OS,而并非关注中断异常之类的。

(3)start.S第一行执行的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码。

.globl _start
_start: b    reset
    ldr    pc, _undefined_instruction
    ldr    pc, _software_interrupt
    ldr    pc, _prefetch_abort
    ldr    pc, _data_abort
    ldr    pc, _not_used
    ldr    pc, _irq
    ldr    pc, _fiq

_undefined_instruction:
    .word undefined_instruction
_software_interrupt:
    .word software_interrupt
_prefetch_abort:
    .word prefetch_abort
_data_abort:
    .word data_abort
_not_used:
    .word not_used
_irq:
    .word irq
_fiq:
    .word fiq
_pad:
    .word 0x12345678 /* now 16*4=64 */

3、地址对齐填充 .balignl 16,0xdeadbeef

(1).balignl 16,0xdeadbeef. 这一句指令是让当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用0xdeadbeef来填充。

(2)0xdeadbeef这是一个十六进制的数字,这个数字的十六进制数全是abcdef之中的字母,这8个字母组成了英文dead beef这两个单词,字面意思就是坏牛肉。

(3)为什么要对齐访问?有时候是效率的要求,有时候是硬件的特殊要求。

4、虚拟地址的定义、物理地址的定义,定义成指针形式

(1)TEXT_BASE的定义在Makefile中,它其实就是我们链接时指定的uboot的链接地址,通过-Ttest指定链接地址,值是c3e00000。

(2)源代码和Makefile中很多变量是可以运用的。简单来说有些符号的值可以从Makefile中传递到源代码中。

(3)(CFG_PHY_UBOOT_BASE 33e00000 uboot在DDR中的物理地址

_TEXT_BASE:
    .word    TEXT_BASE

/*
 * Below variable is very important because we use MMU in U-Boot.
 * Without it, we cannot run code correctly before MMU is ON.
 * by scsuh.
 */
_TEXT_PHY_BASE:
    .word    CFG_PHY_UBOOT_BASE

正式工作开始

5、修改程序状态寄存器,修改值为禁止 irq 和 fiq 中断、软件设置为arm状态、软件设置SVC模式

(默认硬件上电就是SVC模式)
(1)msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC
将CPU设置为禁止FIQ IRQ,ARM状态,SVC模式。

6、设置L2、L1cache,关闭MMU(因为还未启用虚拟地址有映射)

cpu_init_crit:
    
    bl    disable_l2cache              //1. 禁止L2 cache

    bl    set_l2cache_auxctrl_cycle    //2. l2 cache相关初始化

    bl    enable_l2cache               //3. 使能l2 cache
    
       /*
        * Invalidate L1 I/D                  //4. 刷新L1 cache的icache和dcache。
        */
        mov    r0, #0                  @ set up for MCR
        mcr    p15, 0, r0, c8, c7, 0   @ invalidate TLBs
        mcr    p15, 0, r0, c7, c5, 0   @ invalidate icache

       /*
        * disable MMU stuff and caches       //5. 关闭MMU
        */
        mrc    p15, 0, r0, c1, c0, 0
        bic    r0, r0, #0x00002000     @ clear bits 13 (--V-)
        bic    r0, r0, #0x00000007     @ clear bits 2:0 (-CAM)
        orr    r0, r0, #0x00000002     @ set bit 1 (--A-) Align
        orr    r0, r0, #0x00000800     @ set bit 12 (Z---) BTB
        mcr     p15, 0, r0, c1, c0, 0

总结:上面这5步都是和CPU的cache和mmu有关的,不用去细看,大概知道即可。

7、根据外部OMPin来判断启动方式—这就是uboot能够兼容多种启动方式的体现

(1)从哪里启动是由SoC的OM5:OM0这6个引脚的高低电平决定的。在210内部有个寄存器(地址是0xE0000004)反映了外部OM引脚的设置,即程序可以通过这个寄存器知道启动介质,然后在程序中做出不同的应对。

(2)start.S的225-227行执行完后,在r2寄存器中存储了一个数字,这个数字等于某个特定值时就表示SD启动,等于另一个特定值时表示从Nand启动····

/* Read booting information */
        ldr    r0, =PRO_ID_BASE
        ldr    r1, [r0,#OMR_OFFSET]
        bic    r2, r1, #0xffffffc1


    /* NAND BOOT */
    cmp    r2, #0x0        @ 512B 4-cycle
    moveq    r3, #BOOT_NAND

    cmp    r2, #0x2        @ 2KB 5-cycle
    moveq    r3, #BOOT_NAND

    cmp    r2, #0x4        @ 4KB 5-cycle    8-bit ECC
    moveq    r3, #BOOT_NAND

    cmp    r2, #0x6        @ 4KB 5-cycle    16-bit ECC
    moveq    r3, #BOOT_NAND

    cmp    r2, #0x8        @ OneNAND Mux
    moveq    r3, #BOOT_ONENAND

    /* SD/MMC BOOT */
    cmp     r2, #0xc
    moveq   r3, #BOOT_MMCSD    

    /* NOR BOOT */
    cmp     r2, #0x14
    moveq   r3, #BOOT_NOR    

#NORflash支持自启动,所以就在NOR中而无需重定位,更何谈把两个BL1和BL2分散加载
    /* Uart BOOTONG failed */
    cmp     r2, #(0x1<<4)
    moveq   r3, #BOOT_SEC_DEV

8、设置栈

(1)284-286行第一次设置栈。这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。

(2)在后续要调用函数前初始化栈,主要原因是在被调用的函数中还有可能再次调用函数,而BL指令执行时只会将返回地址存储到LR寄存器中,但是我们只有一个LR寄存器,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢失了。

/*
     * Go setup Memory and board specific bits prior to relocation.
     */

    ldr    sp, =0xd0036000 /* end of sram dedicated to u-boot */
    sub    sp, sp, #12    /* set stack */
    mov    fp, #0

9、调用lowlevel_init函数

9.1、检查复位状态

(1)复杂CPU允许多种复位情况。譬如直接冷上电(直接上电)、热启动(低功耗休眠唤醒)等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。

(2)判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动状态下的复位则不需要再次初始化DDR。

9.2、IO状态恢复

9.3、关看门狗

/* Disable Watchdog */
    ldr    r0, =ELFIN_WATCHDOG_BASE    /* 0xE2700000 */
    mov    r1, #0
    str    r1, [r0]

9.4、一些SRAM SROM相关GPIO设置

9.5、供电锁存

/* PS_HOLD pin(GPH0_0) set to high */
    ldr    r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)
    ldr    r1, [r0]
    orr    r1, r1, #0x300    
    orr    r1, r1, #0x1    
    str    r1, [r0]

9.6、判断当前代码执行位置是在SRAM还是DDR,如果是在DDR则跳过下面的时钟初始化和DDR初始化。如果在DDR则跳到9.9

(1)lowlevel_init.S的110-115行。

(2)为什么要做这个判定?
原因1:BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,如果是冷上电那么当前代码应该是在SRAM中运行的BL1,如果是热启动状态的复位这时候应该就是在DDR中运行的BL1。
原因2:判定当前运行代码的地址是有用的,可以指导后面代码的运行。譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化。

(3)bic r1, pc, r0 这句代码的意义是:将pc的值中的某些bit位清0,剩下一些特殊的bit位赋值给r1(r0中为1的那些位清零)等价于:r1 = pc & ~(ff000fff)
ldr r2, _TEXT_BASE 加载链接地址到r2,然后将r2的相应位清0剩下特定位。

(4)最后比较r1和r2.
总结:这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化。

/* when we already run in ram, we don't need to relocate U-Boot.
     * and actually, memory controller must be configured before U-Boot
     * is running in ram.
     */
    ldr    r0, =0xff000fff
    bic    r1, pc, r0        /* r0 <- current base addr of code */
    ldr    r2, _TEXT_BASE        /* r1 <- original base addr in ram */
    bic    r2, r2, r0        /* r0 <- current base addr of code */
    cmp     r1, r2                  /* compare r0, r1                  */
    beq     1f            /* r0 == r1 then skip sdram init   */

9.7、system_clock_init 时钟初始化!!!

(1)时钟的初始化在lowlevel_init.S文件的205~385行,可以看到时钟的设置都是宏定义的方式,并非直接赋值一个数字,这些宏定义在x210_sd.h文件中定义。

(2)在x210_sd.h中300行到428行,都是和时钟相关的配置值。这些宏定义就决定了210的时钟配置是多少。也就是说代码在lowlevel_init.S中都写好了,但代码的设置值都被宏定义在x210_sd.h中了。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可。

9.8、mem_ctrl_asm_init DDR初始化----DMC0和DMC1初始化!!!

(1)该函数用来初始化DDR,函数位置在uboot/cpu/s5pc11x/s5pc110/cpu_init.S文件中。

(2)从分析九鼎移植的uboot可以看出:DMC0上允许的地址范围是20000000-3FFFFFFF(一共是512MB),而我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。即内存地址可以设置为200000002FFFFFFF或者300000003FFFFFFF。
总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。

(3)内存配置值在x210_sd.h的438~468行。头文件中考虑了不同时钟配置下的内存配置,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。

9.9、uart_asm_init UART初始化—串口2!!!

(1)这个函数用来初始化串口

(2)初始化完了后通过串口发送了一个’O’

9.10、tzpc初始化

9.11、pop {pc}以返回

(1)返回前通过串口打印’K’
分析:lowlevel_init.S执行完如果没错那么就会串口打印出"OK"字样。这应该是我们uboot中看到的最早的输出信息。
lowlevel_init.S执行完成返回
总结回顾:lowlevel_init.S中总共做了哪些事情:
检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印’O’、tzpc初始化、打印’K’。
其中值得关注的:关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印"OK"

10、再次供电锁存(前面lowlevel_init.S中已经设置过,这里是一段冗余代码)

    /*
     * Go setup Memory and board specific bits prior to relocation.
     */

    ldr    sp, =0xd0036000 /* end of sram dedicated to u-boot */
    sub    sp, sp, #12    /* set stack */
    mov    fp, #0

11、再次设置栈,这次设置的是内存的栈

(1)之前在调用lowlevel_init程序前设置过1次栈(start.S 284-287行),那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈挪移到DDR中,所以要重新设置栈,这是第二次(start.S 297-299行);这里实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。

(2)为什么要再次设置栈?DDR已经初始化了,已经有大片内存可以用了,原先SRAM中内存大小空间有限,栈放在那使用容易溢出。

    /* get ready to call C functions */
    /*    再次设置栈,这次设置的是内存的栈*/
    ldr    sp, _TEXT_PHY_BASE    /* setup temp stack pointer */
    sub    sp, sp, #12
    mov    fp, #0            /* no previous frame, so fp=0 */

12、再次判断运行地址是否等于链接地址,此时运行地址是SRAM,链接地址是内存,因此不等,需要重定位。

(1)再次用相同的代码判断运行地址是在SRAM中还是DDR中,不过本次判断的目的不同(上次判断是为了决定是否要执行后续的初始化时钟和DDR的代码)这次判断是为了决定是否进行uboot的relocate。

(2)冷启动时当前情况是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中正在运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。此时uboot的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。

/* when we already run in ram, we don't need to relocate U-Boot.
     * and actually, memory controller must be configured before U-Boot
     * is running in ram.
     *    再次判断运行地址是否等于链接地址,运动地址是SRAM,链接地址是内存,
     *    因此不等,需要重定位。
     */
    ldr    r0, =0xff000fff
    bic    r1, pc, r0        /* r0 <- current base addr of code */
    ldr    r2, _TEXT_BASE        /* r1 <- original base addr in ram */
    bic    r2, r2, r0        /* r0 <- current base addr of code */
    cmp     r1, r2                  /* compare r0, r1                  */
    beq     after_copy        /* r0 == r1 then skip flash copy   */

13、代码重定位

(1)D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们当前的启动方式是iNand启动还是SD启动来设置为相应的值。当前是iNand启动,则是从SD通道0启动,则设置值为EB000000;当前是SD卡启动,则是从SD通道2启动,则设置值为EB200000;
在这里插入图片描述

(2)我们在start.S的260行确定了从MMC/SD启动,然后又在278行将#BOOT_MMCSD写入了INF_REG3寄存器中存储着。然后又在322行读出来,再和#BOOT_MMCSD去比较,确定是从MMCSD启动。最终跳转到mmcsd_boot函数中去执行重定位动作。

/*    260行    */
/* SD/MMC BOOT */
        cmp     r2, #0xc
    moveq   r3, #BOOT_MMCSD    
/*    278行    */
    ldr    r0, =INF_REG_BASE
    str    r3, [r0, #INF_REG3_OFFSET]    
 
/*    以下是重定位代码   317行开始     */
#if defined(CONFIG_EVT1)
    /* If BL1 was copied from SD/MMC CH2 */
    ldr    r0, =0xD0037488
    ldr    r1, [r0]
    ldr    r2, =0xEB200000
    cmp    r1, r2
    beq     mmcsd_boot
#endif
/*    322行    */
    ldr    r0, =INF_REG_BASE
    ldr    r1, [r0, #INF_REG3_OFFSET]
    cmp    r1, #BOOT_NAND        /* 0x0 => boot device is nand */
    beq    nand_boot
    cmp    r1, #BOOT_ONENAND    /* 0x1 => boot device is onenand */
    beq    onenand_boot
    cmp     r1, #BOOT_MMCSD
    beq     mmcsd_boot
    cmp     r1, #BOOT_NOR
    beq     nor_boot
    cmp     r1, #BOOT_SEC_DEV
    beq     mmcsd_boot
 
 mmcsd_boot:

    bl      movi_bl2_copy
    b       after_copy

(3)真正的重定位是通过调用movi_bl2_copy函数完成的,在uboot/cpu/s5pc11x/movi.c中。是一个C语言的函数,以下是简化版:

void movi_bl2_copy(void)
{
    ulong ch;

    ch = *(volatile u32 *)(0xD0037488);
    copy_sd_mmc_to_mem copy_bl2 =
             (copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));


    u32 ret;
    if (ch == 0xEB000000) {
        ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
            CFG_PHY_UBOOT_BASE, 0);


    }
    else if (ch == 0xEB200000) {
        ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
            CFG_PHY_UBOOT_BASE, 0);
        

    }

}

(4)copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,CFG_PHY_UBOOT_BASE, 0);
分析参数:2表示通道2;MOVI_BL2_POS是uboot的第二部分在SD卡中的开始扇区,这个扇区数字必须和烧录uboot时烧录的位置相同;MOVI_BL2_BLKCNT是uboot的长度占用的扇区数;CFG_PHY_UBOOT_BASE是重定位时将uboot的第二部分复制到DDR中的起始地址(33E00000).
MOVI_BL2_POS=((1x512)/512 + (8x1024)/512 + (4x4096)/512 )=49扇区
MOVI_BL2_BLKCNT=(512x1024)/512=1024个扇区
CFG_PHY_UBOOT_BASE=0x3000 0000+0x3E0 0000=0x33E00000

14、虚拟地址映射

1)、什么是虚拟地址、物理地址
(1)物理地址就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件地址总线编码的,设计生产时就已确定好,不能更改。
(2)一个事实就是:寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作。坏处就是不够灵活。一个解决方案就是使用虚拟地址。
(3)虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表,也就是页表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行。

2)、MMU单元的作用
(1)MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是操作页表,实现虚拟地址到物理地址的映射。
(2)MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。

3)、地址映射的额外收益1:访问控制
(1)访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)
(2)回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错,访问了不该访问的内存块则就会触发段错误。

4)、地址映射的额外收益2:cache
(1)cache是快速缓存,访问速度介于CPU和DDR之间,用于协调CPU和DDR速度不匹配的问题,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的,也就是cache命中;如果cache中没有才会去DDR中寻找,也就是cache没命中。
(2)cache的工作和虚拟地址映射有关系。对程序中使用的虚拟地址进行二进制拆解,得到页号+页内偏移量的地址结构,MMU通过页号得到在页表中对应的内存块号,得到内存块号之后并非是直接通过内存块号访问DDR中对应的物理地址,而是先访问cache,看cache是否命中,命中则直接使用cache中的内容,否则才去DDR中寻找。
参考阅读:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=22891521&id=2109284

14.1、使能域访问(cp15的c3寄存器)

(1)cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。

(2)c3寄存器在mmu中的作用是控制域访问。域访问是和MMU的访问控制有关的。

after_copy:

#if defined(CONFIG_ENABLE_MMU)
enable_mmu:
    /* enable domain access */
    ldr    r5, =0x0000ffff
    mcr    p15, 0, r5, c3, c0, 0        @load domain access register

14.2、设置TTB(cp15的c2寄存器)

(1)TTB就是translation table base,转换表基地址,也就是页表基地址,页表在内存中的位置。

(2)一个程序上处理机运行时会将该进程分成若干的页,假如一个进程是1024KB大小,而一个页的大小是512KB,则这个进程有两个页,分别是第0页和第1页;OS为每个进程创建一张页表,用于实现虚拟地址到物理地址之间的映射,页表分为两部分,页号和内存块号,页号和内存块号都是从0开始的,一个页号和一个内存块号构成了一个页表项,页号对应虚拟地址在进程空间中属于哪一页,内存块号对应虚拟地址在内存物理空间中属于哪一块。
实际上页表中的页号是隐藏的,也就是不存储的,存储的只有物理块号,因为在对内存分块的时候,我们就知道这片内存可以分为多少个块,假如我们的DDR有4MB,而一个块的大小是512KB,则我们能分为8个块,那么我们至少需要3位二进制才能表示8,但是实际使用时并不会用3位2进制来表示一个页表项,往往出于考虑对齐的目的而使用4位2进制来表示一个页表项,由于每个页表项的大小都是相同的,我们可以通过偏移量得知这个内存块号和哪一个页对应。页表的产生使得我们能够随意使用一个不超出进程空间大小的地址,而不关心这个地址真正对应的物理地址。
(映射中规定了进程分的页和内存分的块大小是相同的,这样的分区能充分使用内存,减少内部碎片,至于块有多大,要看MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。页表由若干个页表项构成,每个页表项负责1个内存块号,总体的页表负责整个内存空间(0-4G)的映射。

(3)整个建立虚拟地址映射的主要工作就是建立这张页表

(4)页表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。页表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查页表。

    /* Set the TTB register */
    ldr    r0, _mmu_table_base
    ldr    r1, =CFG_PHY_UBOOT_BASE
    ldr    r2, =0xfff00000
    bic    r0, r0, r2
    orr    r1, r0, r1
    mcr    p15, 0, r1, c2, c0, 0

14.3、使能MMU单元(cp15的c1寄存器)

(1)cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。

mmu_on:
    mrc    p15, 0, r0, c1, c0, 0
    orr    r0, r0, #1
    mcr    p15, 0, r0, c1, c0, 0
    nop
    nop
    nop
    nop
#endif

14.4、找到映射表_mmu_table_base分析

(1)通过符号查找,确定转换表在lowlevel_init.S文件的593行。
宏观上理解转换表:整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。
ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096.实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。

/* form a first-level section entry */
.macro FL_SECTION_ENTRY base,ap,d,c,b
    .word (\base << 20) | (\ap << 10) | \
          (\d << 5) | (1<<4) | (\c << 3) | (\b << 2) | (1<<1)
.endm
.section .mmudata, "a"
    .align 14
    // the following alignment creates the mmu table at address 0x4000.
    .globl mmu_table
mmu_table:
    .set __base,0
    // Access for iRAM
    .rept 0x100
    FL_SECTION_ENTRY __base,3,0,0,0
    .set __base,__base+1
    .endr

    // Not Allowed
    .rept 0x200 - 0x100
    .word 0x00000000
    .endr

    .set __base,0x200
    // should be accessed
    .rept 0x600 - 0x200
    FL_SECTION_ENTRY __base,3,0,1,1
    .set __base,__base+1
    .endr

    .rept 0x800 - 0x600
    .word 0x00000000
    .endr

    .set __base,0x800
    // should be accessed
    .rept 0xb00 - 0x800
    FL_SECTION_ENTRY __base,3,0,0,0
    .set __base,__base+1
    .endr

/*    .rept 0xc00 - 0xb00
    .word 0x00000000
    .endr */

    .set __base,0xB00
    .rept 0xc00 - 0xb00
    FL_SECTION_ENTRY __base,3,0,0,0
    .set __base,__base+1
    .endr

    // 0xC000_0000?灏??x2000_0000
    .set __base,0x300
    //.set __base,0x200
    // 256MB for SDRAM with cacheable
    .rept 0xD00 - 0xC00
    FL_SECTION_ENTRY __base,3,0,1,1
    .set __base,__base+1
    .endr

    // access is not allowed.
    @.rept 0xD00 - 0xC80
    @.word 0x00000000
    @.endr

    .set __base,0xD00
    // 1:1 mapping for debugging with non-cacheable
    .rept 0x1000 - 0xD00
    FL_SECTION_ENTRY __base,3,0,0,0
    .set __base,__base+1
    .endr    

VA PA length
0-10000000 0-10000000 256MB
10000000-20000000 0 256MB
20000000-60000000 20000000-60000000 1GB 512-1.5G
60000000-80000000 0 512MB 1.5G-2G
80000000-b0000000 80000000-b0000000 768MB 2G-2.75G
b0000000-c0000000 b0000000-c0000000 256MB 2.75G-3G
c0000000-d0000000 30000000-40000000 256MB 3G-3.25G
d-完 d-完 768MB 3.25G-4G
DRAM有效范围:
DMC0: 0x30000000-0x3FFFFFFF
DMC1: 0x40000000-0x4FFFFFFF
结论:虚拟地址映射只是把虚拟地址的c0000000开头的256MB映射到了DMC0的30000000开头的256MB物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的。
思考:为什么配置时将链接地址设置为c3e00000?
因为这个地址将来会被映射到33e00000这个物理地址。然而链接地址设置为33e00000也能使用,因为33e00000和c3e00000的虚拟地址都映射到物理地址33e00000处了。

15、第三次设置栈

(1)第三次设置栈。这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。

(2)我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

/* Set up the stack						    */
	stack_setup:
	ldr	sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
	sub	sp, r0, #12		/* leave 3 words for abort-stack    */

16、清理bss

(1)清理bss段代码和裸机中一样。注意表示bss段的开头和结尾地址的符号都是从链接脚本u-boot.lds得来的。

clear_bss:
	ldr	r0, _bss_start		/* find start of bss segment        */
	ldr	r1, _bss_end		/* stop here                        */
	mov 	r2, #0x00000000		/* clear                            */

clbss_l:
	str	r2, [r0]		/* clear loop...                    */
	add	r0, r0, #4
	cmp	r0, r1
	ble	clbss_l
	

17、uboot第一阶段最后一句话 ldr pc, _start_armboot

(1)start_armboot是uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。

(2)远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。

(3)这里这个远跳转就是uboot第一阶段和第二阶段的分界线。

18、总结:uboot的第一阶段做了哪些工作

(1)构建异常向量表
(2)设置CPU为SVC模式
(3)关看门狗
(4)开发板供电置锁
(5)时钟初始化
(6)DDR初始化
(7)串口初始化并打印"OK"
(8)重定位
(9)建立页表并开启MMU
(10)跳转到第二阶段

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/493191.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python报错提示以及logger的一些应用

本篇是拆解这篇【python︱函数、for、if、name、迭代器、防范报错、类定义、装饰器、argparse模块、yield 】 将报错 logger提示拿出来 文章目录 1、防范报错1.1 assert 断言 2 try...except...报错并提示异常信息优雅的异常报错&#xff1a;suppress 3、报错日志记录&#xf…

【Java 并发编程】一文详解 Java 内置锁 synchronized

一文详解 Java 内置锁 synchronized 1. 前言1.1 并发编程中存在线程安全问题1.2 设计同步器的意义1.3 如何解决线程并发安全问题&#xff1f; 2. synchronized 的用法2.1 修饰实例方法&#xff0c;锁是当前实例对象2.2 修饰静态方法&#xff0c;锁是当前类对象2.3 修饰代码块&a…

简单创建SSM项目

1、在idea中创建项目 点击new-project&#xff0c;点击Maven项目&#xff0c;勾选 creat from archetype &#xff0c;找到maven-archetype-webapp 写好相关信息 点击下一步&#xff0c;需要检查maven环境 点击后下载对应的插件&#xff0c;选择项目地址。 开始下载资源&#x…

rust vscode编辑器常用插件与配置

插件&#xff1a;rust-analyzer 会实时编译和分析你的 Rust 代码&#xff0c;提示代码中的错误&#xff0c;并对类型进行标注 插件的完整手册地址&#xff1a;https://rust-analyzer.github.io/manual.html。 插件&#xff1a; rust syntax 为代码提供语法高亮。 …

就挺无语的,这是有脾气的博客

文章目录 前言1. 背景2. 使用3. 公众号体验4. 结束语 前言 ChatGPT已经推出两个多月了&#xff0c;热度已经不减。ChatGPT由人工智能研究实验室OpenAI在2022年11月30日发布的全新聊天机器人模型&#xff0c;一款人工智能技术驱动的自然语言处理工具。它能够通过学习和理解人类的…

大厂都用DevOps!十分钟带你了解自动化在DevOps中的运用

Hi&#xff0c;大家好。DevOps、CI/CD、Docker、Kubernetes……好像全世界都在谈论这些技术&#xff0c;以至于你觉得即将到达NoOps阶段。别担心&#xff0c;在工具和各种最佳实践的浩瀚海洋中感到迷失是正常的&#xff0c;是时候让我们来分析一下DevOps到底是什么了。 一、De…

JAVA类加载机制

在Java的世界里&#xff0c;每一个类或者接口&#xff0c;在经历编译器后&#xff0c;都会生成一个个.class文件。 类加载机制指的是将这些.class文件中的二进制数据读入到内存中&#xff0c;并对数据进行校验&#xff0c;解析和初始化。最终&#xff0c;每一个类都会在方法区保…

ChatGPT - 学习和提高新技能的Prompt

文章目录 Prompt例子 Prompt “我想学习/提高[技能]。我是一个完全的初学者。创建一个30天的学习计划&#xff0c;可以帮助像我这样的初学者学习和提高这项技能。”例子 我想学习/提高Flink。我是一个完全的初学者。 创建一个30天的学习计划&#xff0c;可以帮助像我这样的初…

Xilinx Artix-7【XC7A35T-2CSG324I】【XC7A35T-1CSG324I】成本与收发器优化的FPGA器件

产品介绍&#xff1a; Xilinx Artix -7系列 FPGA 重新定义了成本敏感型解决方案&#xff0c;功耗比上一代产品降低了一半&#xff0c;同时为高带宽应用提供一流的收发器和信号处理能力。这些设备基于 28 纳米 HPL 工艺构建&#xff0c;提供一流的性能功耗比。与 MicroBlaze™ 软…

boot-admin整合Liquibase实现数据库版本管理

Liquibase 和 Flyway 是两款成熟的、优秀的、开源/商业版的数据库版本管理工具&#xff0c;鉴于 Flyway 的社区版本对 Oracle 数据库支持存在限制&#xff0c;所以 boot-admin 选择整合 Liquibase 提供数据库版本管理能力支持。 Liquibase 开源版使用 Apache 2.0 协议。 Liqui…

实现服务器版本---表白墙(Servlet)

目录 一、创建Servlet项目 二、约定前后端交互接口 三、前端代码 四、后端代码 五、效果演示 结合Servlet API &#xff0c;实现一个服务器版本表白墙。实现的这个表白墙&#xff0c;就通过服务器来保存这里的消息数据&#xff0c;进而做到 “持久化” 存储。 一、创建Se…

浮点数中的阶码和尾数

阶码和尾数 阶码尾数浮点数浮点数表示示例 例题分析总结 阶码 在机器中表示一个浮点数时需要给出指数&#xff0c;这个指数用整数形式表示&#xff0c;这个整数叫做阶码。 尾数 常用对数的小数部分&#xff0c;用于科学计数法&#xff0c;其表示方法为:Mantissa x Base^Expo…

k210单片机的串口交互实验

先来看看实验的结果吧&#xff0c;k210的9口为RX&#xff0c;10口为TX。接线&#xff1a; 9口接usb转ttl的TX 10口接usb转ttl的RX 下面介绍一下k210需要使用的模块&#xff1a; K210 一共有 3 个串口&#xff0c;每个串口可以自由映射引脚。 例&#xff1a; # IO10→RX1&#…

JuiceFS__持久化缓存源码走读

JuiceFS__持久化缓存源码走读 JuiceFS 是一款高性能 POSIX 文件系统&#xff0c;针对云原生环境特别优化设计&#xff0c;在 Apache 2.0 开源协议下发布。使用 JuiceFS 存储数据&#xff0c;数据本身会被持久化在对象存储&#xff08;例如 Amazon S3&#xff09;&#xff0c;而…

java小记 2023-05-05

public class Test {/*** 谓类的方法就是指类中用static 修饰的方法&#xff08;非static 为实例方法&#xff09;&#xff0c;比如main 方法&#xff0c;那么可以以main* 方法为例&#xff0c;可直接调用其他类方法&#xff0c;必须通过实例调用实例方法&#xff0c;this 关键…

7.3 有源滤波电路(2)

四、开关电容滤波器 开关电容电路由受时钟脉冲信号控制的模拟开关、电容器和运算放大电路三部分组成。这种电路的特性与电容器的精度无关&#xff0c;而仅与各电容器电容量之比的准确性有关。在集成电路中&#xff0c;可以通过均匀地控制硅片上氧化层的介电常数及其厚度&#…

国产版ChatGPT大盘点

我们看到,最近,国内大厂开始密集发布类ChatGPT产品。 一方面,是因为这是最近10年最大的趋势和机会。 另一方面,国内的AI,不能别国外卡了脖子。 那在类ChatGPT赛道上,哪些中国版的ChatGPT能快速顶上?都各有哪些困境需要突破呢?本文给诸位带来各个玩家的最新进展。 *…

大数据Doris(十二):Unique数据模型

文章目录 Unique数据模型 一、读时合并 二、写时合并 Unique数据模型 在某些多维分析场景下,用户更关注的是如何保证 Key 的唯一性,即如何获得 Primary Key 唯一性约束。因此,我们引入了 Unique 数据模型,该模型可以根据相同的Primary Key 来保留后插入的数据,确保数据…

Day962.如何更好地重构和组织后端代码 -遗留系统现代化实战

如何更好地重构和组织后端代码 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录是关于如何更好地重构和组织后端代码的内容。 如果说在气泡上下文中开发新的需求&#xff0c;类似于老城区旁边建设一个新城区&#xff0c;那么在遗留系统中开发新的需求&#xff0c;就类似于在…

c++的构造函数与析构函数

构造函数是一种特殊的成员函数&#xff0c;用于在对象创建时初始化对象的成员变量。它的名称与类名相同&#xff0c;没有返回类型&#xff0c;可以有参数。当创建对象时&#xff0c;构造函数会自动调用&#xff0c;以初始化对象的成员变量。如果没有定义构造函数&#xff0c;编…