uboot源码分析-第一汇编阶段

本文主要分析 uboot 源码的 start.S 文件

本文主要分析 uboot 源码的 start.S 文件

uboot源码分析-启动第一阶段

start.S引入

u-boot.lds中找到start.S入口

  • C语言中整个项目的入口就是main函数(这是C语言规定的),所以譬如说一个有10000个.c文件的项目,第一个要分析的文件就是包含了main函数的那个文件。
  • 在uboot中因为有汇编阶段参与,因此不能直接找 main.c。整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start) 因此 _start 符号所在的文件就是整个程序的起始文件,_start 所在处的代码就是整个程序的起始代码。

start.S解析

不简单的头文件包含

#include <config.h> // 就是 include/configs/x210_sd.h 文件
// version.h 就是 include/version_autogenerated.h
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>

#ifndef CONFIG_ENABLE_MMU
#ifndef CFG_PHY_UBOOT_BASE
#define CFG_PHY_UBOOT_BASE	CFG_UBOOT_BASE
#endif
#endif
  • config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include <configs/x210_sd.h>
  • 经过分析后,发现start.S中包含的第一个头文件就是:include/configs/x210_sd.h,这个文件是整个uboot移植时的配置文件。这里面是好多宏。因此这个头文件包含将 include/configs/x210_sd.h 文件和 start.S 文件关联了起来。因此之后在分析 start.S 文件时,主要要考虑的就是 x210_sd.h 文件。
  • #include <version.h>include/version.h 中包含了 include/version_autogenerated.h,这个头文件就是配置过程中自动生成的。里面就一行内容:#define U_BOOT_VERSION “U-Boot 1.3.4”。这里面定义的宏U_BOOT_VERSION的值是一个字符串,字符串中的版本号信息来自于Makefile中的配置值。这个宏在程序中会被调用,在uboot启动过程中会串口打印出uboot的版本号,那个版本号信息就是从这来的。
  • #include <asm/proc/domain.h>。asm目录不是uboot中的原生目录,uboot中本来是没有这个目录的。asm目录是配置时创建的一个符号链接,实际指向的是就是asm-arm(详解上一章节分析mkconfig脚本时)经过分析后发现,实际文件是:include/asm-arm/proc-armv/domain.h
  • 从这里可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。(所以uboot不能在windows的共享文件夹下配置编译,因为windows中没有符号链接)

启动代码的16字节头部

#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
	.word 0x2000
	.word 0x0
	.word 0x0
	.word 0x0
#endif
  1. 在SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。(mkv210image.c中就是为了计算这个校验头)。我们以前做裸机程序时根本没考虑这16字节校验头,因为:1、如果我们是usb启动直接下载的方式启动的则不需要16字节校验头(irom application note);2、如果是SD卡启动mkv210image.c中会给原镜像前加16字节的校验头。
  2. uboot这里start.S中在开头位置放了16字节的填充占位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充。

异常向量表的构建

.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
// 省略 ....
.global _end_vect
_end_vect:
	.balignl 16,0xdeadbeef // 地址对齐
        
_TEXT_BASE:
	.word	TEXT_BASE
  1. 异常向量表是硬件决定的,软件只是参照硬件的设计来实现它。

  2. 异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常。

  3. 复位异常处的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码,因此reset符号处才是真正的有意义的代码开始的地方。

    reset:
     // set the cpu to SVC32 mode and IRQ & FIQ disable
     msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC
    
    • msr cpsr_c, #0xd3 将CPU设置为禁止 FIQ IRQARM 状态,SVC 模式。
    • 其实ARM CPU在复位时默认就会进入SVC模式,但是这里还是使用软件将其置为SVC模式。整个uboot工作时CPU一直处于SVC模式。
  4. .balignl 16,0xdeadbeef. 这一句指令是让当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用 0xdeadbeef 来填充。

    • 0xdeadbeef 这是一个十六进制的数字,这个数字很有意思,组成这个数字的十六进制数全是 abcdef 之中的字母,而且这8个字母刚好组成了英文的 dead beef 这两个单词,字面意思是坏牛肉。
    • 为什么要对齐访问?有时候是效率的要求,有时候是硬件的特殊要求。
  5. TEXT_BASE 就是我们链接时指定的uboot的链接地址。(值就是c3e00000)

    源代码中和配置Makefile中很多变量是可以互相运送的。简单来说有些符号的值可以从Makefile中传递到源代码中

DDR 的物理地址

/*
 * 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

.globl _armboot_start
_armboot_start:
	.word _start
  1. CFG_PHY_UBOOT_BASE uboot在DDR中的物理地址

    #define CFG_UBOOT_BASE		0x33e00000  /*yan*/
    

识别并暂存启动介质选择

/* Read booting information */
ldr	r0, =PRO_ID_BASE
ldr	r1, [r0,#OMR_OFFSET]
bic	r2, r1, #0xffffffc1
  1. 从哪里启动是由SoC的 OM5:OM0 这6个引脚的高低电平决定的。

  2. 实际上在210内部有一个寄存器(地址是 0xE0000004),这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低),也就是真正的启动介质是谁。

  3. 我们代码中可以通过读取这个寄存器的值然后判断其值来确定当前选中的启动介质是Nand还是SD还是其他的。

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

  5. 260行中给r3中赋值 #BOOT_MMCSD(0x03),这个在SD启动时实际会被执行,因此执行完这一段代码后r3中存储了0x03,以后备用。

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

设置栈(SRAM中的栈)并调用lowlevel_init

// 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

bl	lowlevel_init	/* go setup pll,mux,memory */
  1. 284-286行第一次设置栈。这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址 0xd0036000 是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。

    内存地址分配

  2. 在调用函数前初始化栈,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。

lowlevel_init.S

检查复位状态

push	{lr}
/* check reset status  */
ldr	r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
ldr	r1, [r0]
bic	r1, r1, #0xfff6ffff
cmp	r1, #0x10000 // 第17位
beq	wakeup_reset_pre
cmp	r1, #0x80000 // 第19位
beq	wakeup_reset_from_didl
  1. 复杂CPU允许多种复位情况。譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。

    • ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET = 0xE010_A000

    • s5pv210 数据手册说明

      复位控制器

  2. 判断哪种复位的意义在于:

    • 冷上电时DDR是需要初始化才能用的
    • 热启动或者低功耗状态下的复位则不需要再次初始化DDR。

其他设置

/* IO Retention release */
/* Disable Watchdog */
/* SRAM(2MB) init for SMDKC110 */
/* GPJ4 SROM_ADDR_16to21 */
/* CS0 - 16bit sram, enable nBE, Byte base address */
/* 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]
  • ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET = 0xE010_E000

  • s5pv210 数据手册说明

    MISC Register (OTHERS, R/W, Address = 0xE010_E000)

    OTHERS Bit 说明 初始值
    保留 [31:12] 保留 0x5
    保留 [11:10] 保留 0x0
    DIR [9] 方向(0:输入;1:输出) 1
    DATA [8] 驱动值(0:低;1:高) 0
    保留 [7:1] 保留 0x0
    PS_HOLD_OUT_EN [0] XEINT[0] 引脚由该寄存器值控制,当该字段为’1’时,GPIO 章的 XEINT[0] 控制寄存器的值被忽略,也就是不做GPIO引脚使用。(0:禁用,1:启用) 0

    PS_HOLD(与XEINT[0]复用)引脚的值在任何电源模式下都会保持不变。该寄存器处于存活区域,仅通过XnRESET或断电复位。

判断当前代码执行位置

/* 当我们已经在RAM中运行的时候,我们不需要重新配置U-Boot。
 * 实际上,在U-Boot运行在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   */
  1. lowlevel_init.S 的110-115行。
  2. 判定当前代码执行的位置在SRAM中还是在DDR中。为什么要做这个判定?原因
    • BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,因此如果是冷启动那么当前代码应该是在SRAM中运行的BL1,如果是低功耗状态的复位这时候应该就是在DDR中运行的。
    • 我们判定当前运行代码的地址是有用的,可以指导后面代码的运行。
      • 譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。
      • 如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;
      • 如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化。
  3. bic r1, pc, r0 这句代码的意义是:将pc的值中的某些bit位清0,剩下一些特殊的bit位赋值给r1(r0中为1的那些位清零)相等于:r1 = pc & ~(ff000fff),取出 PC 中的高位
  4. ldr r2, _TEXT_BASE 加载链接地址到r2,然后将r2的相应位清0剩下特定位 bic r2, r2, r0,取出链接地址的高位放到 r2 中
  5. 最后比较r1和r2.

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

内存、时钟、串口初始化

bl system_clock_init
/* Memory initialize */
bl mem_ctrl_asm_init

1:
	/* for UART */
	bl uart_asm_init
	bl tzpc_init
  1. system_clock_init,这个初始化时钟的过程和裸机中初始化的过程一样的,只是更加完整而且是用汇编代码写的。

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

    system_clock_init:
    	ldr	r0, =ELFIN_CLOCK_POWER_BASE	@0xe0100000
    	/* Set Mux to FIN */
    	ldr	r1, =0x0
    	str	r1, [r0, #CLK_SRC0_OFFSET] 
    	ldr	r1,	=APLL_LOCKTIME_VAL // 在x210_sd.h中定义
    	str	r1,	[r0, #APLL_LOCK_OFFSET]
    
  3. mem_ctrl_asm_init,初始化DDR。

    • 该函数和裸机中初始化DDR代码是一样的。实际上裸机中初始化DDR的代码就是从这里抄的。
    • 配置值中有一个和裸机中讲的不一样。DMC0_MEMCONFIG_0,在裸机中配置值为 0x20E01323;在uboot中配置为 0x30F01313.这个配置不同就导致结果不同。
      • 在裸机中DMC0的256MB内存地址范围是0x20000000-0x2FFFFFFF
      • 在uboot中DMC0的256MB内存地址范围为 0x30000000-0x3FFFFFFF
    • 之前在裸机中时配置为2开头的地址,当时并没有说可以配置为3开头。从分析九鼎移植的uboot可以看出:DMC0上允许的地址范围是 20000000-3FFFFFFF(一共是512MB),而我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。
    • 总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。
    • 我们需要的内存配置值在x210_sd.h的438行到468行之间。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。
  4. uart_asm_init,初始化串口,初始化完了后通过串口发送了一个’O’

  5. tzpc_init ,trust zone初始化

  6. pop {pc} 以返回,返回前通过串口打印’K’

lowlevel_init.S 执行完如果没错那么就会串口打印出”OK”字样。这应该是我们uboot中看到的最早的输出信息。

检查复位状态

	ldr	r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
	ldr	r1, [r0]
	bic	r1, r1, #0xfffeffff
	cmp	r1, #0x10000
	beq	wakeup_reset_pre
	/* ABB disable */
	ldr	r0, =0xE010C300
	orr	r1, r1, #(0x1<<23)
	str	r1, [r0]
	/* Print 'K' */
	ldr	r0, =ELFIN_UART_CONSOLE_BASE
	ldr	r1, =0x4b4b4b4b
	str	r1, [r0, #UTXH_OFFSET]

	pop	{pc}
  1. r0 指向复位状态寄存器

    复位状态寄存器

  2. 判断第16位:是否通过SLEEP模式唤醒复位

    • 是:进行一系列初始化
    • 否:继续,并打印 K 结束

到此 lowlevel_init.S 执行完成,下面是对其中初始化函数的详细分析。

system_clock_init 时钟初始化分析(lowlevel_init.S)

设置 A/MPLL 时钟源和锁定时间

system_clock_init:
	ldr	r0, =ELFIN_CLOCK_POWER_BASE	@0xe0100000
	/* Set Mux to FIN */
	ldr	r1, =0x0
	str	r1, [r0, #CLK_SRC0_OFFSET] @0x200
	ldr	r1,	=APLL_LOCKTIME_VAL @0xe10
	str	r1,	[r0, #APLL_LOCK_OFFSET] @0x0
	/********lxg added*********************/
	ldr	r0, =ELFIN_CLOCK_POWER_BASE	@0xe0100000
	ldr	r1,	=MPLL_LOCKTIME_VAL @0xe10
	str	r1,	[r0, #MPLL_LOCK_OFFSET] @ 0x8
	/********end*********************/
  1. ELFIN_CLOCK_POWER_BASE = 0xe0100000

    由 S5PV210 数据手册可以找到寄存器映射表

    寄存器 地址 R/W 说明 初始值
    APLL_LOCK 0xE010_0000 R/W 用于APLL的控制PLL锁定期 0x0000_0FFF
    保留 0xE010_0004 - 保留 -
  2. 选择 FIN

    选择时钟源

    在 $MUX_{APLL}$ 开关处,选择 $FIN_{PLL}$ 而不是 $FOUT_{APLL}$,这里的 $FIN_{APLL} = 24MHz$

    • 时钟源 0 地址 CLK_SRC0_OFFSET = 0x200

      寄存器 地址 R/W 说明 初始值
      CLK_SRC0 0xE010_0200 R/W 选择时钟源0(主时钟源) 0x0000_0000
    • 时钟源0寄存器功能

      时钟源0寄存器

      • APLL_SEL = FINPLL
      • MPLL_SEL = FINPLL
  3. 设定 PLL 稳定时间

    • PLL 需要一段时间锁定,之后才能输出稳定的时钟信号

    • A/MPLL_LOCK_OFFSET = 0xe10

      A/M/VPLL_LOCK Bit 说明 初始值
      保留 [31:16] 保留 0x0000
      PLL_LOCKTIME [15:0] 产生稳定时钟输出所需的周期,该计数基于PLL的源时钟。
      - 用于APLL、MPLL、EPLL的FINPLL
      - 用于VPLL的FINVPLL
      0x0

设置 A/M/EPLL 的时钟输出频率

#define CLK_DIV0_MASK	0x7fffffff
#define CLK_DIV0_OFFSET	0x300
#define set_pll(mdiv, pdiv, sdiv)	(1<<31 | mdiv<<16 | pdiv<<8 | sdiv)
#define APLL_VAL	set_pll(APLL_MDIV,APLL_PDIV,APLL_SDIV)
#define CLK_DIV0_VAL ((0<<APLL_RATIO)|(4<<A2M_RATIO)|(4<<HCLK_MSYS_RATIO)\
        |(1<<PCLK_MSYS_RATIO) | (3<<HCLK_DSYS_RATIO)|(1<<PCLK_DSYS_RATIO)\
        |(4<<HCLK_PSYS_RATIO)|(1<<PCLK_PSYS_RATIO))

# 关闭 PLL
ldr	r1, =0x0
str	r1, [r0, #APLL_CON0_OFFSET]
ldr	r1, =0x0
str	r1, [r0, #MPLL_CON_OFFSET]
# 设置分频系数
ldr   	r1, [r0, #CLK_DIV0_OFFSET] @0x300
ldr	r2, =CLK_DIV0_MASK  @0x7fffffff
bic	r1, r1, r2

ldr	r2, =CLK_DIV0_VAL
orr	r1, r1, r2
str	r1, [r0, #CLK_DIV0_OFFSET]

ldr	r1, =APLL_VAL
str	r1, [r0, #APLL_CON0_OFFSET]

ldr	r1, =MPLL_VAL
str	r1, [r0, #MPLL_CON_OFFSET]

ldr	r1, =VPLL_VAL
str	r1, [r0, #VPLL_CON_OFFSET]
  1. 关闭 A/MPLL

    • 在设置 PLL 之前要关闭 PLL

    • APLL_CON0 功能

      APLL_CON0功能

  2. 时钟分频 CLK_DIV0_VAL

    • 寄存器功能

      时钟分频寄存器

    • 清除 CLK_DIV0_OFFSET 寄存器

    • 设置 CLK_DIV0_OFFSET 寄存器

      分频系数 说明4 设置值 结果
      PCLK_PSYS_RATIO PCLK_PSYS = HCLK_PSYS / (PCLK_PSYS_RATIO + 1) 1 66MHz
      HCLK_PSYS_RATIO HCLK_PSYS = MOUT_PSYS / (HCLK_PSYS_RATIO + 1) 4 133MHz
      PCLK_DSYS_RATIO PCLK_DSYS = HCLK_DSYS / (PCLK_DSYS_RATIO + 1) 1 83MHz
      HCLK_DSYS_RATIO HCLK_DSYS = MOUT_DSYS / (HCLK_DSYS_RATIO + 1) 3 166MHz
      PCLK_MSYS_RATIO PCLK_MSYS = HCLK_MSYS / (PCLK_MSYS_RATIO + 1) 1 100MHz
      HCLK_MSYS_RATIO HCLK_MSYS = ARMCLK / (HCLK_MSYS_RATIO + 1) 4 200MHz
      A2M_RATIO SCLKA2M = SCLKAPLL / (A2M_RATIO + 1) 4 200MHz
      APLL_RATIO ARMCLK = MOUT_MSYS / (APLL_RATIO + 1) 0 1GHz
  3. 设置 APLL 的输出频率

    • APLL_CON0_OFFSET 功能

      APLL配置寄存器0

    • APLL_VAL 的值

      这些值来自三星官方的推荐

      APLL 推荐值

      #define APLL_MDIV       0x7d = 125
      #define APLL_PDIV       0x3
      #define APLL_SDIV       0x1
           
      #define set_pll(mdiv, pdiv, sdiv)	(1<<31 | mdiv<<16 | pdiv<<8 | sdiv)
      #define APLL_VAL	set_pll(APLL_MDIV,APLL_PDIV,APLL_SDIV)
           
      @ APLL_VAL = (1<<31 | 0x7d<<16 | 0x3<<8 | 0x1)
      
    • APLL 输出频率计算公式

  4. 设置 MPLL 的输出频率

    1. MPLL_CON0_OFFSET 功能

      MPLL配置寄存器

    2. MPLL_VAL 的值

      MPLL 推荐

      #define MPLL_MDIV	0x29b  = 667
      #define MPLL_PDIV	0xc    = 12
      #define MPLL_SDIV	0x1
            
      @MPLL_VAL = (1<<31 | 0x29b<<16 | 0xc <<8 | 0x1)
      
    3. MPLL 输出频率

    4. EPLL 的输出频率计算方法与上同

设置 A/M/EPLL 的时钟分频系数

ldr r1, [r0, #CLK_DIV1_OFFSET]
ldr	r2, =CLK_DIV1_MASK
bic	r1, r1, r2

ldr	r2, =CLK_DIV1_VAL
orr	r1, r1, r2
str	r1, [r0, #CLK_DIV1_OFFSET]

ldr	r2, =CLK_DIV2_VAL
orr	r1, r1, r2
str	r1, [r0, #CLK_DIV2_OFFSET]

ldr	r2, =CLK_DIV4_VAL
orr	r1, r1, r2
str	r1, [r0, #CLK_DIV4_OFFSET]

ldr	r2, =CLK_DIV6_VAL
orr	r1, r1, r2
str	r1, [r0, #CLK_DIV6_OFFSET]
  1. 设置 CLK_DIV1

    1. 寄存器功能

      CLK_DIV功能

    2. 设置值 CLK_DIV_VAL

      #define CLK_DIV1_VAL	((1<<16)|(1<<12)|(1<<8)|(1<<4))
      
  2. 设置 DLK_DVI2

    1. 寄存器

      CLK_DIV2功能

    2. 设置值

      #define CLK_DIV2_VAL	(1<<0)
      
  3. 设置 CLK_DIV4

    1. 寄存器

      CLK_DIV4功能

    2. 设置值

      #define CLK_DIV4_VAL	0x99990000
      
  4. 设置 CLK_div6

    1. 寄存器

      CLK_DIV6功能

    2. 设置值

      #define CLK_DIV6_VAL	0x71000
      

打开 APLL 的 AFC 功能

#define AFC_ON		0x00000000

#if defined(CONFIG_EVT1) @ autoconfig.mk 中定义
	ldr	r1, =AFC_ON
	str	r1, [r0, #APLL_CON1_OFFSET]
#endif
寄存器 地址 R/W 说明 初始值
APLL_CON1 0xE010_0104 R/W 控制PLL-AFC(自适应频率校准器) 0x0000_0000
APLL_CON1 Bit 说明 初始值
AFC_ENB [31] 决定是否启用AFC。0 : 启用;1 : 停用
AFC在宽范围、高相位噪声(或抖动)和快速锁定时间的情况下选择VCO的自适应频率曲线。
用户应参考3.3.1关于是否在给定的P/M/S值下使用AFC。
0x0000
AFC [4:0] AFC值。如果用户禁用AFC,该值应手动设置 0x0

检察 MPLL 是否锁定

#if defined(CONFIG_CHECK_MPLL_LOCK)
	/* MPLL software workaround */
	ldr	r1, [r0, #MPLL_CON_OFFSET]
	orr     r1, r1, #(1<<28)
	str	r1, [r0, #MPLL_CON_OFFSET]

	mov	r1, #0x100
1:	subs	r1, r1, #1
	bne	1b

	ldr	r1, [r0, #MPLL_CON_OFFSET]
	and	r1, r1, #(1<<29)
	cmp	r1, #(1<<29)
	bne 	retryloop

	/* H/W lock detect disable */
	ldr	r1, [r0, #MPLL_CON_OFFSET]
	bic     r1, r1, #(1<<28)
	str	r1, [r0, #MPLL_CON_OFFSET]
#endif
  1. 先使能 H/W 锁定探测 MPLL_CON_OFFSET |= (1<<28)

    这一位在 S5PV210 数据手册中是保留位,有可能是三星没有写出来,也有可能是为了兼容其他板子

  2. 延时 0x100,再检查 (1<<29) 是不是锁定

    MPLL配置寄存器

再次选择时钟源

ldr	r1, [r0, #CLK_SRC0_OFFSET]
ldr	r2, =0x00000111
orr	r1, r1, r2
str	r1, [r0, #CLK_SRC0_OFFSET]

// added by terry 2012.12.4 for camera 
ldr r1, [r0, #CLK_SRC1_OFFSET]
bic r1, r1, #(0xf<<12)
orr r1, r1, #(0x1<<12) //0001 XusbXTI
str r1, [r0, #CLK_SRC1_OFFSET]

mov	pc, lr @ 时钟配置完成,函数返回
  1. CLK_SRC0_OFFSET 设置 A/M/EPLL 时钟源

    时钟源0寄存器

    把 A/M/EPLL 的时钟源都选择为 $F_{OUT}$

  2. CLK_SRC1_OFFSET 设置摄像头0时钟源

    CLK_SRC1 Bit 说明 初始值
    CAM0_SEL [15:12] 控制MUXCAM0,它是CAM0的源时钟(0000:XXTI,0001:XusbXTI,0010:SCLK到HDMI27M,0011:SCLK到USBPHY0,0100:SCLK到USBPHY1,010101:SCLK到HDMIPHY,0110:SCLKMPLL,0111:SCLKEPLL,1000:SCLKVPLL,其他:保留) 0x0

mem_ctrl_asm_init 内存初始化分析(lowlevel_init.S)

设置 DMC0/1 驱动强度

/* DMC0 Drive Strength (Setting 2X) */
@ DRAM memory controllers (DMC0 and DMC1)
ldr	r0, =ELFIN_GPIO_BASE
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_0DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_1DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_2DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_3DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_4DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_5DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_6DRV_SR_OFFSET]
ldr	r1, =0x0000AAAA
str	r1, [r0, #MP1_7DRV_SR_OFFSET]
ldr	r1, =0x00002AAA
str	r1, [r0, #MP1_8DRV_SR_OFFSET]
/* DMC1 设置与上同 */
  1. ELFIN_GPIO_BASE = 0xE020_0000

    特殊功能寄存器映射

  2. GPIO 驱动能力设置寄存器

    设置 MP1 各引脚驱动能力为 2x

    MP1_0DRV Bit 说明 初始值
    MP1_0DRV[n] [2n+1:2n]
    n=0~7
    00 = 1x
    10 = 2x
    01 = 3x
    11 = 4x
    0xAAAA
  3. 核心板原理图

    核心板内存

  4. S5PV210 数据手册

    引脚名称 GPIO
    Xm0ADDR[0:15] MP0_4[0:7], MP0_5[0:7]
    Xm0DATA[0:15] MP0_6[0:7], MP0_7[0:7]
    Xm1ADDR[0:15] MP1_0[0:7], MP1_1[0:7]
    Xm1DATA[0:31] MP1_2[0:7], MP1_3[0:7], MP1_4[0:7], MP1_5[0:7]
    Xm1DQS[0:3], Xm1DQSn[0:3] MP1_6[0:7]
    Xm1DQM[0:3] MP1_7[0:3]
    Xm1CKE[0:1] MP1_7[4:5]
    Xm1SCLK, Xm1nSCLK MP1_7[6:7]
    Xm1CSn[0:1] MP1_8[0:1]
    Xm1RASn MP1_8[2]

    Xm2ADDR[0] 与上同

DMC0 初始化

/* DMC0 initialization at single Type*/
ldr	r0, =APB_DMC_0_BASE @ 0xF000_0000

@PhyControl0 DLL parameter setting, manual 0x00101000
ldr	r1, =0x00101000 @ (0x10<<16 | 0x10<<8)
str	r1, [r0, #DMC_PHYCONTROL0]
@PhyControl1 DLL parameter setting, LPDDR/LPDDR2 Case
ldr	r1, =0x00000086  @ (0x8<<4 | 0x6<<0)
str	r1, [r0, #DMC_PHYCONTROL1]

ldr	r1, =0x00101002  @PhyControl0 DLL on
str	r1, [r0, #DMC_PHYCONTROL0]

ldr	r1, =0x00101003  @PhyControl0 DLL start
str	r1, [r0, #DMC_PHYCONTROL0]
  1. APB_DMC_0_BASE = 0xF000_0000

    APB 是时钟

  2. DMC_PHYCONTROL0 物理控制寄存器0

    PHYCONTROL0 Bit 说明 初始值
    ctrl_inc [23:16] DLL延迟增量,增加起点数量
    此值应为0x10
    0x0
    ctrl_start_point [15:8] DLL锁定起始点
    初始DLL锁的起始点。这是延时单元的数量,是 “DLL “开始跟踪锁定的起始点。计算初始延迟时间,用延迟单元格的单位延迟与此值相乘来计算初始延迟时间。
    这个值应该是0x10
    0x0
    ctrl_dll_on [1] 打开DLL
    激活DLL的启动信号。该信号应保持高电平,以便正常工作。如果此信号变低,则DLL关闭,并且ctrl_clockctrl_flock 变高。此位应在ctrl_start设置为打开DLL之前设置
    0x0
    ctrl_start [0] 启动 DLL
    启动DLL运行和锁定的信号。在正常运行期间,该信号应保持高电平。如果此信号变低,DLL将停止运行。要重新运行DLL,请将此信号再次调高。在重新运行的情况下,DLL会丢失以前的锁信息。在设置 ctrl_start 之前,请确保 ctrl_dll_on处于高位。
    0x0
  3. DMC_PHYCONTROL1 物理控制寄存器1

    PHYCONTROL1 Bit 说明 初始值
    ctrl_ref [7:4] DLL锁定确认的引用计数 0x4
    ctrl_shiftc [2:0] DQS清洗的相位延迟,0x6 = T/2 (180’ shift)
    根据内存类型的建议值:
    0x5 when LPDDR/LPDDR2 @200MHz
    0x6 when DDR2 @200MHz
    0x0

确定DMC的状态

find_lock_val:
	ldr	r1, [r0, #DMC_PHYSTATUS]  @Load Phystatus register value
	and	r2, r1, #0x7
	cmp	r2, #0x7  @Loop until DLL is locked
	bne	find_lock_val
  1. DMC_PHYSTATUS = 0xF000_0040

  2. 寄存器功能

    DMC的物理状态

  3. 查看低3位,直到锁定

强制锁定

and	r1, #0x3fc0 
mov	r2, r1, LSL #18 r2 = 0xFF0000
orr	r2, r2, #0x100000
orr	r2 ,r2, #0x1000	

orr	r1, r2, #0x3  @Force Value locking
str	r1, [r0, #DMC_PHYCONTROL0]
  1. 第一部分 r2 = 0xFF101003

  2. 结果就是把 ctrl_force = 0xFF

    PHYCONTROL0 Bit 说明 初始值
    ctrl_force [31:24] DLL强制延迟
    ctrl_DLL_on低时,使用此字段代替PHY DLL中的 ctrl_lock_val[9:2]。(即,如果DLL关闭,则此字段用于生成270’时钟,并将dq偏移90’)
    0x0
    ctrl_inc [23:16] DLL延迟增量,增加起点数量 此值应为0x10 0x0
    ctrl_start_point [15:8] DLL锁定起始点 初始DLL锁的起始点。这是延时单元的数量,是 “DLL “开始跟踪锁定的起始点。计算初始延迟时间,用延迟单元格的单位延迟与此值相乘来计算初始延迟时间。 这个值应该是0x10 0x0
    ctrl_dll_on [1] 打开DLL 激活DLL的启动信号。该信号应保持高电平,以便正常工作。如果此信号变低,则DLL关闭,并且ctrl_clockctrl_flock 变高。此位应在ctrl_start设置为打开DLL之前设置 0x0
    ctrl_start [0] 启动 DLL 启动DLL运行和锁定的信号。在正常运行期间,该信号应保持高电平。如果此信号变低,DLL将停止运行。要重新运行DLL,请将此信号再次调高。在重新运行的情况下,DLL会丢失以前的锁信息。在设置 ctrl_start 之前,请确保 ctrl_dll_on处于高位。 0x0

配置 DMC 的控制寄存器

ldr	r0, =APB_DMC_1_BASE

ldr	r1, =0x0FFF2010	 @auto refresh off
str	r1, [r0, #DMC_CONCONTROL]

内存控制器的控制寄存器 ,DMC_CONCONTROL = 0x0FFF2010

CONCONTROL Bit 说明 设置值
timeout_cnt [27:16] 默认超时周期
0xn = n个ACLK周期 (ACLK: AXI时钟)
这个计数器可以防止命令队列中的事务被饿死。如果一个新的AXI事务进入队列中,该计数器就会启动。如果计数器变为零,相应的事务将成为命令队列中所有事务中的最高优先级命令。这是一个默认的超时计数器,如果与QoS ID匹配的ARID/AWID进入命令队列,则被QoS计数器覆盖。
0xFFF
rd_fetch [15:12] 读取数据获取周期
0xn = n个mclk周期 (mclk: 内存时钟)
该寄存器是针对由tDQSCK变化或板卡飞行时间造成的来自存储器设备的读取数据的不可预测的延迟。PHY读FIFO的读取延迟必须由该参数控制。控制器将在(read_latency + n)mclk循环后从PHY读取数据。
0x2
chip1_empty [9] 芯片1的命令队列状态:0x0=不为空;0x1=空
在命令队列条目中没有与chip1内存对应的AXI事务
0x0
chip0_empty [8] 不为空 0x0
aref_en [5] 自动刷新计数器:0x0=禁用;0x1=启用
启用此项可在mclk上升沿将自动刷新计数器减少1
0x0
out_of [4] 无序调度:0x0=禁用;0x1=启用
嵌入式调度器支持无序操作以提高SDRAM的利用率
Out of Order Scheduling
0x1
Reserved [3:0] Should be zero 0x0

配置内存控制器 MEMCONTROL

/* MemControl BL=4, 2 chip, DDR2 type, 
 * dynamic self refresh, force precharge, 
 * dynamic power down off */
ldr	r1, =DMC1_MEMCONTROL @ 0x00202400			
str	r1, [r0, #DMC_MEMCONTROL]
MEMCONTROL Bit 说明 设置值
Reserved [31:23] Should be zero 0x0
bl [22:20] 内存突发长度,0x2 = 4 0x2
num_chip [19:16] 存储芯片数量,0x0 = 1 片 0x0
mem_width [15:12] 存储器数据总线宽度,0x2 = 32-bit 0x2
mem_type [11:8] 存储器类型,0x4 = DDR2 0x4
add_lat_pall [7:6] PALL的附加潜伏期,0x0 = 0 cycle
如果发出所有芯片bank预充电命令,预充电的延迟将为tRP + add_lat_pall
0x0
dsref_en [5] 动态自刷新,0x0 = 禁用 0x0
tp_en [4] 预充超时,Timeout Precharge,0x0 = 禁用
如果启用了 tp_en,它会在打开页面策略中的指定mclk周期(如果在周期之间没有进行任何访问)之后自动为打开的芯片预充电。

如果预配置位字段PrechConfig.tp_cnt被设置,它指定等待直到超时预充电对开放组预充电的mclk循环量
0x0
dpwrdn_type [3:2] 动态断电类型:
0x0=激活/预充电电源关闭
0x1=强制预充电断电
0x0
dpwrdn_en [1] 动态断电Dynamic Power Down,0x0=禁用 0x0
clk_stop_en [0] 动态时钟控制:0x0 = 总是运行;0x1=空闲期间停止 0x0

设置内存配置器0/1 MEMCONFIG0/1

/* MemConfig0 512MB config, 8 banks,Mapping 
 * Method[12:15]0:linear, 1:linterleaved, 2:Mixed */
ldr	r1, =DMC1_MEMCONFIG_0 @ 0x40F01313		
str	r1, [r0, #DMC_MEMCONFIG0]
ldr	r1, =DMC1_MEMCONFIG_1 @ 0x00F01313
str	r1, [r0, #DMC_MEMCONFIG1]
MEMCONFIG0/1 Bit 说明 设置值
chip_base [31:24] AXI基址[31:24]=芯片基址
例如,如果 chip_base=0x20,那么存储芯片0的 AXI base address变为 0x2000_0000
0x40
chip_mask [23:16] AXI基址掩码
用于确定内存芯片0的AXI偏移地址的上地址位掩码。
1 = 屏蔽对应位
chip_mask = 0xF8,那么偏移地址为 0x0000_0000 ~ 0x07FF_FFFF
0xF0
chip_map [15:12] 地址映射方法(AXI到内存)
0x1 = Interleaved ({row, bank, column, width})
0x1
chip_col [11:8] 列地址位数,0x3 = 10 bits 0x3
chip_row [7:4] 行地址位数,0x1 = 13 bits 0x1
chip_bank [3:0] bank 块数量,0x3 = 8 banks 0x3

预充电策略配置寄存器

ldr	r1, =0xFF000000
str	r1, [r0, #DMC_PRECHCONFIG]
PRECHCONFIG Bit 说明 设置值
tp_cnt [31:24] 预充电循环超时周期
0xn = n mclk cycles
0xFF
chip1_policy [15:8] 存储芯片1预充电组选择策略:0x0=打开页策略
打开页策略:读或写之后,前面访问的行保持打开状态。
关闭页面(自动预充电)策略:在读或写命令之后,内存设备会自动为bank预充电。
0x0
chip0_policy [7:0] 存储芯片0预充电组选择策略:0x0=打开页策略 0x1

设置 TimingAref

/* TimingAref 7.8us*133MHz=1038(0x40E), 100MHz=780(0x30C), 
 * 20MHz=156(0x9C), 10MHz=78(0x4) */
ldr	r1, =DMC1_TIMINGA_REF @ 0x40E			
str	r1, [r0, #DMC_TIMINGAREF]

TimingAref:自动刷新存储器的交流定时寄存器

PRECHCONFIG Bit 说明 设置值
Reserved [31:16] Should be zero 0x0
t_refi [15:0] 平均定期刷新间隔
应为最小存储器 tREFI (all bank) < t_refi * T(mclk),例如,对于7.8us的全列刷新周期和133MHz的mclk频率,应编程以下值:7.8 us * 133 MHz = 1038
0x40E

设置 TimingRow

TimingRow:存储器行的交流定时寄存器

ldr	r1, =DMC1_TIMING_ROW @ 0x10233206
str	r1, [r0, #DMC_TIMINGROW]
ldr	r1, =DMC1_TIMING_DATA @TimingData CL=3
str	r1, [r0, #DMC_TIMINGDATA]
ldr	r1, =DMC1_TIMING_PWR @TimingPower
str	r1, [r0, #DMC_TIMINGPOWER]

设置各种延时参数

设置存储器直接指令寄存器 DirectCmd

向 DDR2 发送指令,初始化 DDR2 芯片

ldr	r1, =0x07000000	 @DirectCmd	chip0 Deselect
str	r1, [r0, #DMC_DIRECTCMD]
ldr	r1, =0x01000000 @DirectCmd chip0 PALL
str	r1, [r0, #DMC_DIRECTCMD]
# 省略 ........

剩下的就是按照 S5PV210 数据手册 P598 DDR2内存类型的初始化序列进行,其中有27步。这里不再分析。

配置 PwrdnConfig

ldr	r1, =0xFFFF00FF @PwrdnConfig
str	r1, [r0, #DMC_PWRDNCONFIG]

# 再重新设置,至此 DMC1 完成
ldr	r1, =0x00202400
str	r1, [r0, #DMC_MEMCONTROL]

PwrdnConfig:动态掉电配置寄存器

PWRDNCONFIG Bit 说明 设置值
dsref_cyc [31:16] 动态自刷新条目的周期数:0xn n个 aclk 周期 0xFFFF
Reserved [15:8] Should be zero 0x0
dpwrdn_cyc [7:0] 动态断电条目的周期数:0xn = n aclk cycles 0xFF

uart_asm_init 初始化分析

设置 GPIO

/* set GPIO(GPA) to enable UART */
@ GPIO setting for UART
ldr	r0, =ELFIN_GPIO_BASE @ 0xE020_0000
ldr	r1, =0x22222222
str   	r1, [r0, #GPA0CON_OFFSET]

ldr     r1, =0x2222
str     r1, [r0, #GPA1CON_OFFSET]
  1. 核心板原理图

    从核心板原理图可以看到,用于 UART 的是 GPA0[0:7] 管脚

    UART管脚

  2. S5PV210 数据手册

    1. GPA0CON

      GPA0CON Bit 说明 设置值
      GPA0CON[7] [31:28] 0010 = UART_1_RTSn 0x2
      GPA0CON[6] [27:24] 0010 = UART_1_CTSn 0x2
      GPA0CON[5] [23:20] 0010 = UART_1_TXD 0x2
      GPA0CON[4] [19:16] 0010 = UART_1_RXD 0x2
      GPA0CON[3] [15:12] 0010 = UART_0_RTSn 0x2
      GPA0CON[2] [11:8] 0010 = UART_0_CTSn 0x2
      GPA0CON[1] [7:4] 0010 = UART_0_TXD 0x2
      GPA0CON[0] [3:0] 0010 = UART_0_RXD 0x2
    2. GPA1CON

      GPA0CON Bit 说明 设置值
      GPA1CON[3] [15:12] 0010 = UART_3_TXD 0x2
      GPA1CON[2] [11:8] 0010 = UART_3_RXD 0x2
      GPA1CON[1] [7:4] 0010 = UART_2_TXD 0x2
      GPA1CON[0] [3:0] 0010 = UART_2_RXD 0x2

设置 UART 参数

ldr	r0, =ELFIN_UART_CONSOLE_BASE @ 0xE290_0800
mov	r1, #0x0
str	r1, [r0, #UFCON_OFFSET] @ 关闭 FIFO
str	r1, [r0, #UMCON_OFFSET] @ 禁用 AFC

mov	r1, #0x3 @ 无奇偶校验;1停止位;8 bit 内容
str	r1, [r0, #ULCON_OFFSET]

ldr	r1, =0x3c5
str	r1, [r0, #UCON_OFFSET]

ldr	r1, =UART_UBRDIV_VAL @ 设置波特率115200
str	r1, [r0, #UBRDIV_OFFSET]

ldr	r1, =UART_UDIVSLOT_VAL
str	r1, [r0, #UDIVSLOT_OFFSET]

ldr	r1, =0x4f4f4f4f
str	r1, [r0, #UTXH_OFFSET] @通过串口2发送'O'

mov	pc, lr
  1. V210 使用的是 UART3 口

  2. 关闭 UFCON 即 UART 的 FIFO 属性

    UFCONn Bit 说明 设置值
    Reserved [31:11] Reserved 0x0
    Tx FIFO Trigger Level [10:8] 决定Tx FIFO的触发电平。如果Tx FIFO的数据量小于或等于触发电平,则发生Tx中断。000 = 0 byte 0x0
    Reserved [7] Reserved 0x0
    Rx FIFO Trigger Level [6:4] 决定Rx FIFO的触发电平。如果Rx FIFO的数据数大于或等于触发电平,则发生Rx中断。
    [通道0] 000 = 32 byte;[通道1] 000 = 8 byte
    [通道2\3] 000 = 2 byte
    0x0
    FIFO Enable [0] 0 = Disables 0x0
  3. 设置 UMCON 即 UART 的 Modem

    UMCONn Bit 说明 设置值
    Reserved [31:8] Reserved 0x0
    RTS trigger Level [7:5] 决定Rx FIFO的触发电平,以控制nRTS信号。如果AFC位被启用,且Rx FIFO的字节数大于或等于触发电平,nRTS信号被停用。
    000 = 255 bytes
    0x0
    Auto Flow Control (AFC) [4] 0 = Disables 0x0
    Modem Interrupt Enable [3] 0 = Disables 0x0
    Request to Send [0] 如果AFC位已启用,则该值将被忽略。在这种情况下,S5PV210会自动控制nRTS信号。
    如果AFC位被禁用,软件必须控制nRTS信号。
    0x0
  4. 设置 ULCON 即 UART line 控制器

    ULCONn Bit 说明 设置值
    Reserved [31:7] Reserved 0x0
    Infrared Mode [6] 0 = 普通模式;1 = 红外模式 0x0
    Parity Mode [5:3] 0xx = 无奇偶校验
    100=奇数奇偶数
    101=偶数平价
    110=强制奇偶校验/检查为1
    111 = 强制奇偶校验/检查为0
    0x0
    Number of Stop Bit [2] 0 = 1位停止位;1 = 2位停止位 0x0
    Word Length [1:0] 11 = 8-bit 0x11
  5. 设置 UCON 即 UART 控制寄存器

    UCONn Bit 说明 设置值
    Reserved [31:21] Reserved 0x0
    Clock Selection [10] 0 = PCLK 0
    Tx Interrupt Type [9] 1=电平;0 = 脉冲 1
    Rx Interrupt Type [8] 1=电平(当Rx缓冲器在非FIFO模式下接收数据或当它在FIFO模式下达到Rx FIFO触发电平时,请求中断。) 1
    Rx Time Out Enable [7] 如果启用UART FIFO,则启用/禁用Rx超时中断。中断是一个接收中断。 1
    Rx Error Status Interrupt Enable [6] 在接收操作过程中,当出现异常时,如中断、帧错误、奇偶校验错误或超限错误等异常时,使UART能够产生中断。
    1 = 生成接收错误状态中断
    1
    Loop-back Mode [5] 将环回位设置为1会触发UART进入环回模式。此模式仅用于测试目的。 0
    Send Break Signal [4] 设置此位会触发UART在1帧时间内发送一个break(中断)。在发送break(中断)信号后,该位会自动清除。
    0 = 正常发送;1 = 发送 break 信号
    0
    Transmit Mode [3:2] 确定哪个函数能够将Tx数据写入UART发送缓冲寄存器
    01 = 中断请求或轮询模式
    01
    Receive Mode [1:0] 确定哪个功能能够从UART接收缓冲区寄存器中读取数据。
    01 = 中断请求或轮询模式
    01
  6. 设置 UART 的波特率

    UBRDIVn Bit 说明 设置值
    Reserved [31:16] Reserved 0
    UBRDIVn [15:0] 波特率分值 0x2
    • UART波特率配置
      • UART块中有4个UART波特率除法寄存器,分别是UBRDIV0、UBRDIV1、UBRDIV2和UBRDIV3。
      • 存储在波特率除法器寄存器(UBRDIVn)和除法槽寄存器(UDIVSLOTn)中的值,用于确定串行Tx/Rx时钟速率(波特率),具体如下:
        • DIV_VAL = (PCLK / (bps x 16)) − 1
        • (UDIVSLOTn 数值中1的个数)/16 = DIV_VAL 的余数
      • 举例:如果波特率为115200 bps,PCLK 为40 MHz
        • DIV_VAL = (40000000 / (115200 x 16)) -1 = [20.7] = 20
        • (UDIVSLOTn 数值中1的个数) / 16 = 0.7
          • UDIVSLOTn = 16'b1110_1110_1110_1010 或者
          • UDIVSLOTn = 16’b0111_0111_0111_0101
    • 这里设置
      • UBRDIV2 = 34, UDIVSLOT2 = 0xDDDD
      • PCLK = 66 MHz,那么 `DIV_VAL = 66000000 / (115200 x 16) - 1 = 34.8,取整数部分为 34
      • 查表 P880 可知 0xDDDD(1101_1101_1101_1101b)对应12个1,12 / 16 = 0.75 = 0.8

lowlevel_init.S 总结

总结回顾:lowlevel_init.S中总共做了哪些事情:检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印’O’、tzpc初始化、打印’K’。

再次设置栈(DDR中的栈)

/* 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 */


_TEXT_BASE:
	.word	TEXT_BASE

# board/samsumg/x210/cofig.mk
TEXT_BASE = 0xc3e00000

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

再次判断当前地址以决定是否重定位

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   */
  1. 再次用相同的代码判断运行地址是在SRAM中还是DDR中,不过本次判断的目的不同(上次判断是为了决定是否要执行初始化时钟和DDR的代码)这次判断是为了决定是否进行uboot的relocate。

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

uboot重定位详解

#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

	ldr	r0, =INF_REG_BASE // 322行
	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
  1. 0xD0037488 这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000。截图于《S5PV210_iROM_ApplicationNote_Preliminary_20091126》

    global

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

     /* Read booting information */
     ldr	r0, =PRO_ID_BASE
     ldr	r1, [r0,#OMR_OFFSET]
     bic	r2, r1, #0xffffffc1
    
     /* SD/MMC BOOT */
     cmp     r2, #0xc
     moveq   r3, #BOOT_MMCSD	
    
     ldr	r0, =INF_REG_BASE
     str	r3, [r0, #INF_REG3_OFFSET]     
    
  3. 真正的重定位是通过调用movi_bl2_copy()函数完成的,在uboot/cpu/s5pc11x/movi.c中

     mmcsd_boot:
         bl      movi_bl2_copy
         b       after_copy
    
  4. 分析 movi_bl2_copy() 函数

     void movi_bl2_copy(void)
     {
         ch = *(volatile u32 *)(0xD0037488);
         copy_sd_mmc_to_mem copy_bl2 =
             (copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));
    
         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);
         else
             return;
    
         if (ret == 0) while (1);
         else return;
     }
    
    • 0xD0037F98 是在 iRom 中就有了的 Flash 拷贝函数

      地址 名称 用法
      0xD0037F98 CopySDMMCtoMem 此内部功能可以将任何数据从SD/MMC设备复制到SDRAM。用户可以在IROM启动过程完成后使用此功能

      摘抄自 《S5PV210_iROM_ApplicationNote_Preliminary》

    • 函数的参数分析

        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).
      

打开 MMU

enable_mmu:
	/* enable domain access */
	ldr	r5, =0x0000ffff
	mcr	p15, 0, r5, c3, c0, 0		@load domain access register

	/* 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

	/* Enable the MMU */
mmu_on:
	mrc	p15, 0, r0, c1, c0, 0
	orr	r0, r0, #1
	mcr	p15, 0, r0, c1, c0, 0
#endif

什么是虚拟地址、物理地址

  1. 物理地址就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了。

  2. 虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行(虚拟地址到物理地址的映射是不可能通过软件来实现的)。

MMU单元的作用

  1. MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。

  2. MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。

地址映射的额外收益1:访问控制

  1. 访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)

  2. 回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。

地址映射的额外收益2:cache

  1. cache的工作和虚拟地址映射有关系。

  2. cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。

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

/* enable domain access */
ldr	r5, =0x0000ffff
mcr	p15, 0, r5, c3, c0, 0		@load domain access register
  1. cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。

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

设置TTB(cp15的c2寄存器)

  1. TTB就是translation table base,转换表基地址。首先要明白什么是TT(translation table转换表),TTB其实就是转换表的基地址。

  2. 转换表是建立一套虚拟地址映射的关键。转换表分2部分,

    • 表索引

      • 表索引对应虚拟地址
    • 表项

      • 表项对应物理地址

    一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大,要看你的MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。

  3. 整个建立虚拟地址映射的主要工作就是建立这张转换表

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

分析 _mmu_table_base 变量

_mmu_table_base:
	.word mmu_table

_mmu_table_base 就是 mmu_table 数组,所以分析 _mmu_table_base 就是分析 mmu_table

  1. 确定一级段地址

     /* 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
    
     mmu_table:
         .set __base,0
         // Access for iRAM
         .rept 0x100
         FL_SECTION_ENTRY __base,3,0,0,0
         .set __base,__base+1
         .endr
    
         // 省略 ......
    
    段基地址 AP Domain 1 C B 1
    base«20 ap « 10 d«5 1«4 c«3 b«2 1«1
    • ap: access permission 访问控制位

    • Domain:访问控制寄存器索引,Domain和ap配合,对访问权限进行检查

    • C:C = 1 时为 write-through(WT) 模式。

    • B:B = 1 时为 write-back(WB)模式

    虚拟地址 物理地址 空间大小 地址空间
    0x0000_0000 ~ 0x1000_0000 0x0000_0000 ~ 0x1000_0000 256MB 0-256MB
    0x1000_0000 ~ 0x2000_0000 0x0000_0000 256MB 256-512MB
    0x2000_0000 ~ 0x6000_0000 0x2000_0000 ~ 0x6000_0000 1GB 512 - 1.5G
    0x6000_0000 ~ 0x8000_0000 0x0000_0000 512MB 1.5G - 2G
    0x8000_0000 ~ 0xb000_0000 0x8000_0000 ~ 0xb000_0000 768MB 2G - 2.75G
    0xb000_0000 ~ 0xc000_0000 0xb000_0000 ~ 0xc000_0000 256MB 2.76G - 3G
    0xc000_0000 ~ 0xd000_0000 0x3000_0000 ~ 0x4000_0000 256MB 3G - 3.25G
    0xd000_0000 ~ 0xFFFF_FFFF 0xd000_0000 ~ 0xFFFF_FFFF 768MB 3.25G - 4G

    得到这张内存中的地址转换表,然后把表的首地址传给 cp15 的 c2寄存器,之后 cp15 就帮我们自动进行虚拟地址和物理地址的转换

  • 宏观上理解转换表:

    • 整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。

    • ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096.实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。

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

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

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

再次设置栈

stack_setup:
#if defined(CONFIG_MEMORY_UPPER_CODE)
	ldr	sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
	sub	sp, r0, #12		/* leave 3 words for abort-stack    */
#endif
  1. 第三次设置栈。

    • 这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
  2. 我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

清理bss

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

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

跳转到第二阶段

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

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

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

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

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