IAR中cortex-m4启动流程分析
2014-04-11 19:28
609 查看
软件环境:windows7旗舰版,IAR V6105(EWARM-EV-WEB-6105)
ARM芯片:飞思卡尔K60N512VMD100 (cortex-m4核心)
示例程序:飞思卡尔官方的 KINETIS512_SC
======================
最近分析了一下飞思卡尔官方提供的k60系列demo程序在IAR上的启动流程,现写一下笔记,以备以后参考。先看一下K60N512VMD100内部存储器的分布情况,飞思卡尔K60N512VMD100有512K的flash和128k的SRAM.其中:
Flash地址空间:
0x00000000--0x00080000,共512k
SRAM地址空间:
SRAM1 0x1FFF0000--0x20000000 64k
SRAM2 0x20000000--0x20010000 64k
总共的SRAM大小是128k
我要在RAM中调试代码,下面以代码的执行过程为顺序分析一下启动流程。
首先看一下源文件中提供的128KB_Ram.icf文件。*.icf文件是IAR中的分散描述文件,相当于ADS中的*.src文件或keil中的*.sct文件或GNU中的*.lds链接脚本文件。
这个文件中前面部分是各个变量的定义,关键看后面部分:
/*128KB_Ram.icf后面部分*/
***********
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
place at address mem:__code_start__ { readonly section .noinit };
place in RAM_region { readonly, block CodeRelocate };
place in RAM_region { readwrite, block CodeRelocateRam,
block CSTACK, block HEAP };
************
①place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec }
这段代码表示要把.intvec代码段中的只读部分放在存储空间(mem,前面已定义的名称)中__ICFEDIT_intvec_start__ 地址上,前面部分已经定义__ICFEDIT_intvec_start__=0x1fff0000,是SRAM的起始地址。也就是先把向量表放到内存中的最前面。 .intvec 这个段是在vectors.c文件中出现的,
/*vectors.c片段*/
typedef void (*vector_entry)(void);
#pragma location = ".intvec"
const vector_entry __vector_table[] = //@ ".intvec" =
{
VECTOR_000, /* Initial SP */
VECTOR_001, /* Initial PC */
VECTOR_002,
VECTOR_003,
......(中间省略)
VECTOR_254,
VECTOR_255,
CONFIG_1,
CONFIG_2,
CONFIG_3,
CONFIG_4,
};
从源文件中可以看到这里定义了一个向量表__vector_table(前面的const 很重要不能省,这样才能保证向量表是只读的),向量表中的每一项都是一个指向函数的指针,这里总共有256+4=260个指针,所以占据空间为260*4=1040=0x410.
所以SRAM空间的前0x410的空间已经被向量表占据。即占据了0x1fff0000--0x1fff0410.
②place at address mem:__code_start__ { readonly section .noinit }
这段代码表示要把 .noinit段中的只读部分放到地址空间 __code_start__ 开始的地址上,前面有定义 __code_start__= 0x1fff0410 ,也就是把 .noinit段放到0x1fff0410开始的地址上。所以在内存中代码就连续了,先是向量表,接着的是.noinitd 段。
.noinit 段在crt0.s汇编文件中出现:
SECTION .noinit : CODE
EXPORT __startup
__startup
MOV r0,#0 ; Initialize the GPRs
MOV r1,#0
MOV r2,#0
MOV r3,#0
MOV r4,#0
MOV r5,#0
MOV r6,#0
MOV r7,#0
MOV r8,#0
MOV r9,#0
MOV r10,#0
MOV r11,#0
MOV r12,#0
CPSIE i ; Unmask interrupts
import start
BL start ; call the C code
__done
B __done
END
这段代码算是芯片复位后执行的第一段代码(如果没有其他异常的话)。作为一个通常的规则,推荐先把通用寄存器(R0-R12)清零。然后是使能中断,跳转到start标号(或函数)处继续执行。
========================
在start.c文件中找到了start函数:
/*start.c片段*/
void start(void)
{
/* Disable the watchdog timer */
wdog_disable();
/* Copy any vector or data sections that need to be in RAM */
common_startup();
/* Perform processor initialization */
sysinit();
printf("\n\n");
/* Determine the last cause(s) of reset */
if (MC_SRSH & MC_SRSH_SW_MASK)
printf("Software Reset\n");
if (MC_SRSH & MC_SRSH_LOCKUP_MASK)
printf("Core Lockup Event Reset\n");
if (MC_SRSH & MC_SRSH_JTAG_MASK)
printf("JTAG Reset\n");
if (MC_SRSL & MC_SRSL_POR_MASK)
printf("Power-on Reset\n");
if (MC_SRSL & MC_SRSL_PIN_MASK)
printf("External Pin Reset\n");
if (MC_SRSL & MC_SRSL_COP_MASK)
printf("Watchdog(COP) Reset\n");
if (MC_SRSL & MC_SRSL_LOC_MASK)
printf("Loss of Clock Reset\n");
if (MC_SRSL & MC_SRSL_LVD_MASK)
printf("Low-voltage Detect Reset\n");
if (MC_SRSL & MC_SRSL_WAKEUP_MASK)
printf("LLWU Reset\n");
/* Determine specific Kinetis device and revision */
cpu_identify();
/* Jump to main process */
main();
/* No actions to perform after this so wait forever */
while(1);
}
start函数中,首先执行 wdog_disable()函数来禁用看门狗,然后调用 common_startup()函数初始化RAM(复制向量表、清零.bss段等,为C语言运行环境做准备),接着执行sysinit()函数初始化芯片(时钟、用到的外设等)。下面依次分析这3个函数。
①wdog_disable()
对系统的设定无非是对各个寄存器值的修改。wdog_disable()函数在wdog.c文件中
/*wdog.c片段*/
void wdog_disable(void)
{
/* First unlock the watchdog so that we can write to registers */
wdog_unlock();
/* Clear the WDOGEN bit to disable the watchdog */
WDOG_STCTRLH &= ~WDOG_STCTRLH_WDOGEN_MASK;
}
void wdog_unlock(void)
{
/* NOTE: DO NOT SINGLE STEP THROUGH
THIS */
/* There are timing requirements for the execution of the unlock. If
* you single step through the code you will cause the CPU to reset.
*/
/* This sequence must execute within 20 clock cycles, so disable
* interrupts will keep the code atomic and ensure the timing.
*/
DisableInterrupts;
/* Write 0xC520 to the unlock register */
WDOG_UNLOCK = 0xC520;
/* Followed by 0xD928 to complete the unlock */
WDOG_UNLOCK = 0xD928;
/* Re-enable interrupts now that we are done */
EnableInterrupts;
}
禁用看门狗流程很简单:先是解锁寄存器,然后更改看门狗寄存器里面的值来禁用看门狗。解锁看门狗寄存器:向解锁寄存器里连续写入0xC520和0xD928,两次写入的时间必须小于20个时钟周期。所以在解锁过程中不能单步运行,期间也不能被中断打断,解锁函数是 wdog_unlock()。上面DisableInterrupts和EnableInterrupts已经在arm_cm4.h中定义过:
#define DisableInterrupts asm(" CPSID i");
#define EnableInterrupts asm(" CPSIE i");
解锁看门狗寄存器后,向看门狗寄存器里写入适当的值就可以禁用看门狗了。
也就是把WDOG_STCTRLH 寄存器(地址是0x40052000)的第0位置0.
②common_startup
初始化RAM(复制向量表、清零.bss段等,为C语言运行环境做准备)。
1 /* File: startup.c */
2 #include "common.h"
3 #pragma section = ".data"
4 #pragma section = ".data_init"
5 #pragma section = ".bss"
6 #pragma section = "CodeRelocate"
7 #pragma section = "CodeRelocateRam"
8 /********************************************************************/
9 void
10 common_startup(void)
11 {
12 /* Declare a counter we'll use in all of the copy loops */
13 uint32 n;
14 /* Declare pointers for various data sections. These pointers
15 * are initialized using values pulled in from the linker file
16 */
17 uint8 * data_ram, * data_rom, * data_rom_end;
18 uint8 * bss_start, * bss_end;
19 /* Addresses for VECTOR_TABLE and VECTOR_RAM come from the linker file */
20 extern uint32 __VECTOR_TABLE[];
21 extern uint32 __VECTOR_RAM[];
22 /* Copy the vector table to RAM */
23 if (__VECTOR_RAM != __VECTOR_TABLE)
24 {
25 for (n = 0; n < 0x410; n++)
26 __VECTOR_RAM[n] = __VECTOR_TABLE[n];
27 }
28 /* Point the VTOR to the new copy of the vector table */
29 write_vtor((uint32)__VECTOR_RAM);
30 /* Get the addresses for the .data section (initialized
data section) */
31 data_ram = __section_begin(".data");
32 data_rom = __section_begin(".data_init");
33 data_rom_end = __section_end(".data_init");
34 n = data_rom_end - data_rom;
35 /* Copy initialized data from ROM to RAM */
36 while (n--)
37 *data_ram++ = *data_rom++;
38 /* Get the addresses for the .bss section (zero-initialized
data) */
39 bss_start = __section_begin(".bss");
40 bss_end = __section_end(".bss");
41 /* Clear the zero-initialized data section */
42 n = bss_end - bss_start;
43 while(n--)
44 *bss_start++ = 0;
45 /* Get addresses for any code sections that need to be
copied from ROM to RAM.
46 * The IAR tools have a predefined keyword that can be used to mark individual
47 * functions for execution from RAM. Add "__ramfunc" before the return type in
48 * the function prototype for any routines you need to execute from RAM instead
49 * of ROM. ex: __ramfunc void foo(void);
50 */
51 uint8* code_relocate_ram = __section_begin("CodeRelocateRam");
52 uint8* code_relocate = __section_begin("CodeRelocate");
53 uint8* code_relocate_end = __section_end("CodeRelocate");
54 /* Copy functions from ROM to RAM */
55 n = code_relocate_end - code_relocate;
56 while (n--)
57 *code_relocate_ram++ = *code_relocate++;
58 }
在IAR中, #pragma section="NAME" 用来在C语言中指定一个名称是NAME的段,align指定对齐方式。指定的段可以被段操作符来引用,段操作符包括
__section_begin, __section_end, 和 __section_size. 个人理解.date、.date_init和.bss应该是IAR中保留的段名称,.date代表数据段中的常量,.date_init代表数据段中已初始化的变量,.bss代表未初始化的变量(zero)。
上面代码中,先是指定了5个不同名称的段(前3个是保留段名称,代表这些段是从这里开始的),CodeRelocate和CodeRelocateRam是在*.icf文件中定义的块(block):
define block CodeRelocate { section .textrw_init }; define block CodeRelocateRam { section .textrw };
quote:
The _
_ramfunc keyword makes a function execute in RAM. Two code
sections will be created: one for the RAM execution (.textrw), and one for the ROM initialization (.textrw_init).
外部变量引用
extern uint32 __VECTOR_TABLE[]; extern uint32 __VECTOR_RAM[];
来自IAR的链接文件(.icf),在.icf文件中已经定义了变量 __VECTOR_TABLE 和 __VECTOR_RAM 其值都是0x1fff0000."Copy the vector table to RAM"这段代码进行判断,如果向量表不在RAM中,则把向量表拷贝到RAM开始的地址上,这里由于在RAM中调试,代码是直接下载到RAM中的,所以不用拷贝。
向量表已经在RAM中了,接下来要重定向向量表,以便在发生异常时到RAM中取得异常入口地址(默认情况下是在0x0取)。 write_vtor((uint32)__VECTOR_RAM) 这个函数用来写向量表偏移寄存器(VTOR,地址0xE000_ED08),这里写入的是RAM起始地址0x1FFF0000。注意这个地址是有要求的,并不是所有地址都能作为向量表起始地址,0x1FFF0000满足要求(这个要求就是:必须先求出系统中共有多少个向量,再把这个数字向上增大到是 2 的整次幂,而起始地址必须对齐到后者的边界上。例如,如果一共有
32 个中断,则共有 32+16(系统异常)=48个向量,向上增大到 2的整次幂后值为 64,因此地址地址必须能被 64*4=256整除,从而合法的起始地址可以是:0x0,0x100,0x200 等----参见ARM Contex-M3权威指南)。另外,如果向量表在RAM区(相对于code区),需把bit[29]置位,这里0x1FFF0000也满足要求。
后面的代码是拷贝数据到RAM中,搭建好C语言运行环境。
上次写了飞思卡尔官方给出的demo程序的启动流程的前面部分:
在存储器最前面放置好向量表-->把通用寄存器清零-->开中断-->跳转到start函数继续执行初始化。在start函数中,顺次执行三个函数:禁用看门狗-->初始化C语言环境(向量表重定向、拷贝数据段到RAM、清零bss段等)-->系统外设初始化。
③系统外设初始化函数 sysinit()
#include "common.h"
#include "sysinit.h"
#include "uart.h"
/********************************************************************/
/* Actual system clock frequency */
int core_clk_khz;
int core_clk_mhz;
int periph_clk_khz;
/********************************************************************/
void sysinit (void)
{
/*
* Enable all of the port clocks. These
have to be enabled to configure
* pin muxing options, so
most code will need all of these on anyway.
*/
SIM_SCGC5 |= (SIM_SCGC5_PORTA_MASK
| SIM_SCGC5_PORTB_MASK
| SIM_SCGC5_PORTC_MASK
| SIM_SCGC5_PORTD_MASK
| SIM_SCGC5_PORTE_MASK );
/* Ramp up the system clock */
core_clk_mhz = pll_init(CORE_CLK_MHZ, REF_CLK);
/*
* Use the value obtained from the pll_init function to define
variables
* for the core clock in kHz and also
the peripheral clock. These
* variables can be used by other functions that need awareness of the
* system frequency.
*/
core_clk_khz = core_clk_mhz * 1000;
periph_clk_khz = core_clk_khz / (((SIM_CLKDIV1 & SIM_CLKDIV1_OUTDIV2_MASK) >> 24)+ 1);
/* For debugging
purposes, enable the trace clock and/or FB_CLK
so that
* we'll be able to monitor
clocks and know the PLL is at the frequency
* that we expect.
*/
trace_clk_init();
fb_clk_init();
/* Enable the pins for the
selected UART */
if (TERM_PORT == UART0_BASE_PTR)
{
/* Enable the UART0_TXD function on PTD6 */
PORTD_PCR6 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART0_RXD function on PTD7 */
PORTD_PCR7 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART1_BASE_PTR)
{
/* Enable the UART1_TXD function on PTC4 */
PORTC_PCR4 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART1_RXD function on PTC3 */
PORTC_PCR3 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART2_BASE_PTR)
{
/* Enable the UART2_TXD function on PTD3 */
PORTD_PCR3 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART2_RXD function on PTD2 */
PORTD_PCR2 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART3_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTC_PCR17 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTC_PCR16 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART4_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTE_PCR24 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTE_PCR25 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART5_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTE_PCR8 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTE_PCR9 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
/* UART0 and UART1
are clocked from the core clock, but all other UARTs are
* clocked from the peripheral clock. So
we have to determine which clock
* to send to the
uart_init function.
*/
if ((TERM_PORT == UART0_BASE_PTR) | (TERM_PORT == UART1_BASE_PTR))
uart_init (TERM_PORT, core_clk_khz, TERMINAL_BAUD);
else
uart_init (TERM_PORT, periph_clk_khz, TERMINAL_BAUD);
}
/********************************************************************/
void trace_clk_init(void)
{
/* Set the
trace clock to the core clock frequency */
SIM_SOPT2 |= SIM_SOPT2_TRACECLKSEL_MASK;
/* Enable the TRACE_CLKOUT pin function on PTA6 (alt7 function) */
PORTA_PCR6 = ( PORT_PCR_MUX(0x7));
}
/********************************************************************/
void fb_clk_init(void)
{
/* Enable the clock to the
FlexBus module */
SIM_SCGC7 |= SIM_SCGC7_FLEXBUS_MASK;
/* Enable the FB_CLKOUT function on PTC3 (alt5 function) */
PORTC_PCR3 = ( PORT_PCR_MUX(0x5));
}
/********************************************************************/
首先写SIM_SCGC5寄存器。SCGC5表示的是System
Clock Gating Control Register 5(系统时钟门控制寄存器5).系统集成模块 --System integration module (SIM)--控制寄存器组中共有8个SCGC寄存器,它们分别控制不同外设所需要的时钟的开关,SCGC5寄存器控制的是PORTA~PORTE、TSI、REGFILE和LPTIMER时钟的开关,向对应的位写入1表示使能时钟。第一段代码分别打开了PORTA~PORTE的时钟开关。
上面pll_init(unsigned
char,unsigned char)函数用来增加系统时钟。
unsigned char pll_init(unsigned char clk_option, unsigned
char crystal_val)
{
unsigned char pll_freq;
if (clk_option > 3) {return
0;} //return
0 if one of the available options is not selected
if (crystal_val > 15) {return
1;} // return
1 if one of the available crystal options is not available
//This assumes that the MCG is in default
FEI mode out of reset.
// First move to FBE
mode
#if (defined(K60_CLK) || defined(ASB817))
MCG_C2 = 0;
#else
// Enable external oscillator, RANGE=2, HGO=1, EREFS=1, LP=0, IRCS=0
MCG_C2 = MCG_C2_RANGE(2) | MCG_C2_HGO_MASK | MCG_C2_EREFS_MASK;
#endif
// after initialization of oscillator
release latched state of oscillator and GPIO
SIM_SCGC4 |= SIM_SCGC4_LLWU_MASK;
LLWU_CS |= LLWU_CS_ACKISO_MASK;
// Select external
oscilator and Reference Divider and clear IREFS to start
ext osc
// CLKS=2, FRDIV=3, IREFS=0, IRCLKEN=0, IREFSTEN=0
MCG_C1 = MCG_C1_CLKS(2) | MCG_C1_FRDIV(3);
/* if we
aren't using an osc input we don't need to wait for the
osc to init */
#if (!defined(K60_CLK) && !defined(ASB817))
while (!(MCG_S & MCG_S_OSCINIT_MASK)){}; // wait for oscillator to initialize
#endif
while (MCG_S & MCG_S_IREFST_MASK){}; // wait for Reference
clock Status bit to clear
while (((MCG_S & MCG_S_CLKST_MASK) >> MCG_S_CLKST_SHIFT) != 0x2){}; // Wait for clock
status bits to show clock source is ext ref clk
// Now in FBE
#if (defined(K60_CLK))
MCG_C5 = MCG_C5_PRDIV(0x18);
#else
// Configure PLL Ref Divider, PLLCLKEN=0, PLLSTEN=0, PRDIV=5
// The crystal frequency is used to select the
PRDIV value. Only even frequency crystals are supported
// that will produce a 2MHz reference
clock to the PLL.
MCG_C5 = MCG_C5_PRDIV(crystal_val); // Set PLL
ref divider to match the crystal used
#endif
// Ensure MCG_C6 is at
the reset default of 0. LOLIE disabled, PLL disabled, clk
monitor disabled, PLL VCO divider is clear
MCG_C6 = 0x0;
// Select the
PLL VCO divider and system clock dividers depending on clocking option
switch (clk_option) {
case 0:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG, FlexBus = MCG, Flash
clock= MCG/2
set_sys_dividers(0,0,0,1);
// Set the
VCO divider and enable the PLL for 50MHz, LOLIE=0, PLLS=1, CME=0, VDIV=1
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(1); //VDIV = 1 (x25)
pll_freq = 50;
break;
case 1:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG/2, FlexBus = MCG/2, Flash
clock= MCG/4
set_sys_dividers(0,1,1,3);
// Set the
VCO divider and enable the PLL for 100MHz, LOLIE=0, PLLS=1, CME=0, VDIV=26
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(26); //VDIV = 26 (x50)
pll_freq = 100;
break;
case 2:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG/2, FlexBus = MCG/2, Flash
clock= MCG/4
set_sys_dividers(0,1,1,3);
// Set the
VCO divider and enable the PLL for 96MHz, LOLIE=0, PLLS=1, CME=0, VDIV=24
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(24); //VDIV = 24 (x48)
pll_freq = 96;
break;
case 3:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG, FlexBus = MCG, Flash
clock= MCG/2
set_sys_dividers(0,0,0,1);
// Set the
VCO divider and enable the PLL for 48MHz, LOLIE=0, PLLS=1, CME=0, VDIV=0
MCG_C6 = MCG_C6_PLLS_MASK; //VDIV = 0 (x24)
pll_freq = 48;
break;
}
while (!(MCG_S & MCG_S_PLLST_MASK)){}; // wait for PLL
status bit to set
while (!(MCG_S & MCG_S_LOCK_MASK)){}; // Wait for LOCK
bit to set
// Now running
PBE Mode
// Transition into PEE by setting
CLKS to 0
// CLKS=0, FRDIV=3, IREFS=0, IRCLKEN=0, IREFSTEN=0
MCG_C1 &= ~MCG_C1_CLKS_MASK;
// Wait for clock
status bits to update
while (((MCG_S & MCG_S_CLKST_MASK) >> MCG_S_CLKST_SHIFT) != 0x3){};
// Now running
PEE Mode
return pll_freq;
} //pll_init
这个函数中两个参数clk_option和crystal_val是两个枚举变量(enum),从它们的定义来看它们的取值范围是0~3和0~15.所以函数开始先进行有效性判断。
MCG表示的是 Multipurpose Clock Generator(多用途时钟发生器).MCG模块向MCU提供几个时钟源选择。这个模块包含了一个 frequency-locked loop (FLL)和一个
phase-locked loop (PLL).FLL可以通过内部的或外部的参考时钟源来控制,PLL可以通过外部参考时钟来控制。模块可以选择FLL或PLL的输出时钟或者内部或外部时钟源作为MCU的系统时钟。上面MCG_2指的是 MCG Control 2 Register. MCG一共有9种操作模式:FEI, FEE, FBI, FBE, PBE, PEE, BLPI,BLPE,and Stop. 不同运行模式的功耗互不相同,要进入各个模式需要写MCG_1~MCG_6寄存器组中某几个寄存器中的某几个位。 我们可以这样说,采用内部组件的模式所消耗的功率少于采用外部组件的模式。而MCG包括两种专门为低功率应用设计的模式——旁通低功耗内部(BLPI)和旁通低功耗外部(BLPE)。驱动BLPI模式的总线频率要比BLPE
模式低,因此BLPI 模式消耗的功率最小。下图2总结了每一种运行模式的功耗。
ARM芯片:飞思卡尔K60N512VMD100 (cortex-m4核心)
示例程序:飞思卡尔官方的 KINETIS512_SC
======================
最近分析了一下飞思卡尔官方提供的k60系列demo程序在IAR上的启动流程,现写一下笔记,以备以后参考。先看一下K60N512VMD100内部存储器的分布情况,飞思卡尔K60N512VMD100有512K的flash和128k的SRAM.其中:
Flash地址空间:
0x00000000--0x00080000,共512k
SRAM地址空间:
SRAM1 0x1FFF0000--0x20000000 64k
SRAM2 0x20000000--0x20010000 64k
总共的SRAM大小是128k
我要在RAM中调试代码,下面以代码的执行过程为顺序分析一下启动流程。
首先看一下源文件中提供的128KB_Ram.icf文件。*.icf文件是IAR中的分散描述文件,相当于ADS中的*.src文件或keil中的*.sct文件或GNU中的*.lds链接脚本文件。
这个文件中前面部分是各个变量的定义,关键看后面部分:
/*128KB_Ram.icf后面部分*/
***********
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
place at address mem:__code_start__ { readonly section .noinit };
place in RAM_region { readonly, block CodeRelocate };
place in RAM_region { readwrite, block CodeRelocateRam,
block CSTACK, block HEAP };
************
①place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec }
这段代码表示要把.intvec代码段中的只读部分放在存储空间(mem,前面已定义的名称)中__ICFEDIT_intvec_start__ 地址上,前面部分已经定义__ICFEDIT_intvec_start__=0x1fff0000,是SRAM的起始地址。也就是先把向量表放到内存中的最前面。 .intvec 这个段是在vectors.c文件中出现的,
/*vectors.c片段*/
typedef void (*vector_entry)(void);
#pragma location = ".intvec"
const vector_entry __vector_table[] = //@ ".intvec" =
{
VECTOR_000, /* Initial SP */
VECTOR_001, /* Initial PC */
VECTOR_002,
VECTOR_003,
......(中间省略)
VECTOR_254,
VECTOR_255,
CONFIG_1,
CONFIG_2,
CONFIG_3,
CONFIG_4,
};
从源文件中可以看到这里定义了一个向量表__vector_table(前面的const 很重要不能省,这样才能保证向量表是只读的),向量表中的每一项都是一个指向函数的指针,这里总共有256+4=260个指针,所以占据空间为260*4=1040=0x410.
所以SRAM空间的前0x410的空间已经被向量表占据。即占据了0x1fff0000--0x1fff0410.
②place at address mem:__code_start__ { readonly section .noinit }
这段代码表示要把 .noinit段中的只读部分放到地址空间 __code_start__ 开始的地址上,前面有定义 __code_start__= 0x1fff0410 ,也就是把 .noinit段放到0x1fff0410开始的地址上。所以在内存中代码就连续了,先是向量表,接着的是.noinitd 段。
.noinit 段在crt0.s汇编文件中出现:
SECTION .noinit : CODE
EXPORT __startup
__startup
MOV r0,#0 ; Initialize the GPRs
MOV r1,#0
MOV r2,#0
MOV r3,#0
MOV r4,#0
MOV r5,#0
MOV r6,#0
MOV r7,#0
MOV r8,#0
MOV r9,#0
MOV r10,#0
MOV r11,#0
MOV r12,#0
CPSIE i ; Unmask interrupts
import start
BL start ; call the C code
__done
B __done
END
这段代码算是芯片复位后执行的第一段代码(如果没有其他异常的话)。作为一个通常的规则,推荐先把通用寄存器(R0-R12)清零。然后是使能中断,跳转到start标号(或函数)处继续执行。
========================
在start.c文件中找到了start函数:
/*start.c片段*/
void start(void)
{
/* Disable the watchdog timer */
wdog_disable();
/* Copy any vector or data sections that need to be in RAM */
common_startup();
/* Perform processor initialization */
sysinit();
printf("\n\n");
/* Determine the last cause(s) of reset */
if (MC_SRSH & MC_SRSH_SW_MASK)
printf("Software Reset\n");
if (MC_SRSH & MC_SRSH_LOCKUP_MASK)
printf("Core Lockup Event Reset\n");
if (MC_SRSH & MC_SRSH_JTAG_MASK)
printf("JTAG Reset\n");
if (MC_SRSL & MC_SRSL_POR_MASK)
printf("Power-on Reset\n");
if (MC_SRSL & MC_SRSL_PIN_MASK)
printf("External Pin Reset\n");
if (MC_SRSL & MC_SRSL_COP_MASK)
printf("Watchdog(COP) Reset\n");
if (MC_SRSL & MC_SRSL_LOC_MASK)
printf("Loss of Clock Reset\n");
if (MC_SRSL & MC_SRSL_LVD_MASK)
printf("Low-voltage Detect Reset\n");
if (MC_SRSL & MC_SRSL_WAKEUP_MASK)
printf("LLWU Reset\n");
/* Determine specific Kinetis device and revision */
cpu_identify();
/* Jump to main process */
main();
/* No actions to perform after this so wait forever */
while(1);
}
start函数中,首先执行 wdog_disable()函数来禁用看门狗,然后调用 common_startup()函数初始化RAM(复制向量表、清零.bss段等,为C语言运行环境做准备),接着执行sysinit()函数初始化芯片(时钟、用到的外设等)。下面依次分析这3个函数。
①wdog_disable()
对系统的设定无非是对各个寄存器值的修改。wdog_disable()函数在wdog.c文件中
/*wdog.c片段*/
void wdog_disable(void)
{
/* First unlock the watchdog so that we can write to registers */
wdog_unlock();
/* Clear the WDOGEN bit to disable the watchdog */
WDOG_STCTRLH &= ~WDOG_STCTRLH_WDOGEN_MASK;
}
void wdog_unlock(void)
{
/* NOTE: DO NOT SINGLE STEP THROUGH
THIS */
/* There are timing requirements for the execution of the unlock. If
* you single step through the code you will cause the CPU to reset.
*/
/* This sequence must execute within 20 clock cycles, so disable
* interrupts will keep the code atomic and ensure the timing.
*/
DisableInterrupts;
/* Write 0xC520 to the unlock register */
WDOG_UNLOCK = 0xC520;
/* Followed by 0xD928 to complete the unlock */
WDOG_UNLOCK = 0xD928;
/* Re-enable interrupts now that we are done */
EnableInterrupts;
}
禁用看门狗流程很简单:先是解锁寄存器,然后更改看门狗寄存器里面的值来禁用看门狗。解锁看门狗寄存器:向解锁寄存器里连续写入0xC520和0xD928,两次写入的时间必须小于20个时钟周期。所以在解锁过程中不能单步运行,期间也不能被中断打断,解锁函数是 wdog_unlock()。上面DisableInterrupts和EnableInterrupts已经在arm_cm4.h中定义过:
#define DisableInterrupts asm(" CPSID i");
#define EnableInterrupts asm(" CPSIE i");
解锁看门狗寄存器后,向看门狗寄存器里写入适当的值就可以禁用看门狗了。
也就是把WDOG_STCTRLH 寄存器(地址是0x40052000)的第0位置0.
②common_startup
初始化RAM(复制向量表、清零.bss段等,为C语言运行环境做准备)。
1 /* File: startup.c */
2 #include "common.h"
3 #pragma section = ".data"
4 #pragma section = ".data_init"
5 #pragma section = ".bss"
6 #pragma section = "CodeRelocate"
7 #pragma section = "CodeRelocateRam"
8 /********************************************************************/
9 void
10 common_startup(void)
11 {
12 /* Declare a counter we'll use in all of the copy loops */
13 uint32 n;
14 /* Declare pointers for various data sections. These pointers
15 * are initialized using values pulled in from the linker file
16 */
17 uint8 * data_ram, * data_rom, * data_rom_end;
18 uint8 * bss_start, * bss_end;
19 /* Addresses for VECTOR_TABLE and VECTOR_RAM come from the linker file */
20 extern uint32 __VECTOR_TABLE[];
21 extern uint32 __VECTOR_RAM[];
22 /* Copy the vector table to RAM */
23 if (__VECTOR_RAM != __VECTOR_TABLE)
24 {
25 for (n = 0; n < 0x410; n++)
26 __VECTOR_RAM[n] = __VECTOR_TABLE[n];
27 }
28 /* Point the VTOR to the new copy of the vector table */
29 write_vtor((uint32)__VECTOR_RAM);
30 /* Get the addresses for the .data section (initialized
data section) */
31 data_ram = __section_begin(".data");
32 data_rom = __section_begin(".data_init");
33 data_rom_end = __section_end(".data_init");
34 n = data_rom_end - data_rom;
35 /* Copy initialized data from ROM to RAM */
36 while (n--)
37 *data_ram++ = *data_rom++;
38 /* Get the addresses for the .bss section (zero-initialized
data) */
39 bss_start = __section_begin(".bss");
40 bss_end = __section_end(".bss");
41 /* Clear the zero-initialized data section */
42 n = bss_end - bss_start;
43 while(n--)
44 *bss_start++ = 0;
45 /* Get addresses for any code sections that need to be
copied from ROM to RAM.
46 * The IAR tools have a predefined keyword that can be used to mark individual
47 * functions for execution from RAM. Add "__ramfunc" before the return type in
48 * the function prototype for any routines you need to execute from RAM instead
49 * of ROM. ex: __ramfunc void foo(void);
50 */
51 uint8* code_relocate_ram = __section_begin("CodeRelocateRam");
52 uint8* code_relocate = __section_begin("CodeRelocate");
53 uint8* code_relocate_end = __section_end("CodeRelocate");
54 /* Copy functions from ROM to RAM */
55 n = code_relocate_end - code_relocate;
56 while (n--)
57 *code_relocate_ram++ = *code_relocate++;
58 }
在IAR中, #pragma section="NAME" 用来在C语言中指定一个名称是NAME的段,align指定对齐方式。指定的段可以被段操作符来引用,段操作符包括
__section_begin, __section_end, 和 __section_size. 个人理解.date、.date_init和.bss应该是IAR中保留的段名称,.date代表数据段中的常量,.date_init代表数据段中已初始化的变量,.bss代表未初始化的变量(zero)。
上面代码中,先是指定了5个不同名称的段(前3个是保留段名称,代表这些段是从这里开始的),CodeRelocate和CodeRelocateRam是在*.icf文件中定义的块(block):
define block CodeRelocate { section .textrw_init }; define block CodeRelocateRam { section .textrw };
quote:
The _
_ramfunc keyword makes a function execute in RAM. Two code
sections will be created: one for the RAM execution (.textrw), and one for the ROM initialization (.textrw_init).
外部变量引用
extern uint32 __VECTOR_TABLE[]; extern uint32 __VECTOR_RAM[];
来自IAR的链接文件(.icf),在.icf文件中已经定义了变量 __VECTOR_TABLE 和 __VECTOR_RAM 其值都是0x1fff0000."Copy the vector table to RAM"这段代码进行判断,如果向量表不在RAM中,则把向量表拷贝到RAM开始的地址上,这里由于在RAM中调试,代码是直接下载到RAM中的,所以不用拷贝。
向量表已经在RAM中了,接下来要重定向向量表,以便在发生异常时到RAM中取得异常入口地址(默认情况下是在0x0取)。 write_vtor((uint32)__VECTOR_RAM) 这个函数用来写向量表偏移寄存器(VTOR,地址0xE000_ED08),这里写入的是RAM起始地址0x1FFF0000。注意这个地址是有要求的,并不是所有地址都能作为向量表起始地址,0x1FFF0000满足要求(这个要求就是:必须先求出系统中共有多少个向量,再把这个数字向上增大到是 2 的整次幂,而起始地址必须对齐到后者的边界上。例如,如果一共有
32 个中断,则共有 32+16(系统异常)=48个向量,向上增大到 2的整次幂后值为 64,因此地址地址必须能被 64*4=256整除,从而合法的起始地址可以是:0x0,0x100,0x200 等----参见ARM Contex-M3权威指南)。另外,如果向量表在RAM区(相对于code区),需把bit[29]置位,这里0x1FFF0000也满足要求。
后面的代码是拷贝数据到RAM中,搭建好C语言运行环境。
上次写了飞思卡尔官方给出的demo程序的启动流程的前面部分:
在存储器最前面放置好向量表-->把通用寄存器清零-->开中断-->跳转到start函数继续执行初始化。在start函数中,顺次执行三个函数:禁用看门狗-->初始化C语言环境(向量表重定向、拷贝数据段到RAM、清零bss段等)-->系统外设初始化。
③系统外设初始化函数 sysinit()
#include "common.h"
#include "sysinit.h"
#include "uart.h"
/********************************************************************/
/* Actual system clock frequency */
int core_clk_khz;
int core_clk_mhz;
int periph_clk_khz;
/********************************************************************/
void sysinit (void)
{
/*
* Enable all of the port clocks. These
have to be enabled to configure
* pin muxing options, so
most code will need all of these on anyway.
*/
SIM_SCGC5 |= (SIM_SCGC5_PORTA_MASK
| SIM_SCGC5_PORTB_MASK
| SIM_SCGC5_PORTC_MASK
| SIM_SCGC5_PORTD_MASK
| SIM_SCGC5_PORTE_MASK );
/* Ramp up the system clock */
core_clk_mhz = pll_init(CORE_CLK_MHZ, REF_CLK);
/*
* Use the value obtained from the pll_init function to define
variables
* for the core clock in kHz and also
the peripheral clock. These
* variables can be used by other functions that need awareness of the
* system frequency.
*/
core_clk_khz = core_clk_mhz * 1000;
periph_clk_khz = core_clk_khz / (((SIM_CLKDIV1 & SIM_CLKDIV1_OUTDIV2_MASK) >> 24)+ 1);
/* For debugging
purposes, enable the trace clock and/or FB_CLK
so that
* we'll be able to monitor
clocks and know the PLL is at the frequency
* that we expect.
*/
trace_clk_init();
fb_clk_init();
/* Enable the pins for the
selected UART */
if (TERM_PORT == UART0_BASE_PTR)
{
/* Enable the UART0_TXD function on PTD6 */
PORTD_PCR6 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART0_RXD function on PTD7 */
PORTD_PCR7 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART1_BASE_PTR)
{
/* Enable the UART1_TXD function on PTC4 */
PORTC_PCR4 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART1_RXD function on PTC3 */
PORTC_PCR3 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART2_BASE_PTR)
{
/* Enable the UART2_TXD function on PTD3 */
PORTD_PCR3 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART2_RXD function on PTD2 */
PORTD_PCR2 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART3_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTC_PCR17 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTC_PCR16 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART4_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTE_PCR24 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTE_PCR25 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
if (TERM_PORT == UART5_BASE_PTR)
{
/* Enable the UART3_TXD function on PTC17 */
PORTE_PCR8 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
/* Enable the UART3_RXD function on PTC16 */
PORTE_PCR9 = PORT_PCR_MUX(0x3); // UART is alt3 function for this
pin
}
/* UART0 and UART1
are clocked from the core clock, but all other UARTs are
* clocked from the peripheral clock. So
we have to determine which clock
* to send to the
uart_init function.
*/
if ((TERM_PORT == UART0_BASE_PTR) | (TERM_PORT == UART1_BASE_PTR))
uart_init (TERM_PORT, core_clk_khz, TERMINAL_BAUD);
else
uart_init (TERM_PORT, periph_clk_khz, TERMINAL_BAUD);
}
/********************************************************************/
void trace_clk_init(void)
{
/* Set the
trace clock to the core clock frequency */
SIM_SOPT2 |= SIM_SOPT2_TRACECLKSEL_MASK;
/* Enable the TRACE_CLKOUT pin function on PTA6 (alt7 function) */
PORTA_PCR6 = ( PORT_PCR_MUX(0x7));
}
/********************************************************************/
void fb_clk_init(void)
{
/* Enable the clock to the
FlexBus module */
SIM_SCGC7 |= SIM_SCGC7_FLEXBUS_MASK;
/* Enable the FB_CLKOUT function on PTC3 (alt5 function) */
PORTC_PCR3 = ( PORT_PCR_MUX(0x5));
}
/********************************************************************/
首先写SIM_SCGC5寄存器。SCGC5表示的是System
Clock Gating Control Register 5(系统时钟门控制寄存器5).系统集成模块 --System integration module (SIM)--控制寄存器组中共有8个SCGC寄存器,它们分别控制不同外设所需要的时钟的开关,SCGC5寄存器控制的是PORTA~PORTE、TSI、REGFILE和LPTIMER时钟的开关,向对应的位写入1表示使能时钟。第一段代码分别打开了PORTA~PORTE的时钟开关。
上面pll_init(unsigned
char,unsigned char)函数用来增加系统时钟。
unsigned char pll_init(unsigned char clk_option, unsigned
char crystal_val)
{
unsigned char pll_freq;
if (clk_option > 3) {return
0;} //return
0 if one of the available options is not selected
if (crystal_val > 15) {return
1;} // return
1 if one of the available crystal options is not available
//This assumes that the MCG is in default
FEI mode out of reset.
// First move to FBE
mode
#if (defined(K60_CLK) || defined(ASB817))
MCG_C2 = 0;
#else
// Enable external oscillator, RANGE=2, HGO=1, EREFS=1, LP=0, IRCS=0
MCG_C2 = MCG_C2_RANGE(2) | MCG_C2_HGO_MASK | MCG_C2_EREFS_MASK;
#endif
// after initialization of oscillator
release latched state of oscillator and GPIO
SIM_SCGC4 |= SIM_SCGC4_LLWU_MASK;
LLWU_CS |= LLWU_CS_ACKISO_MASK;
// Select external
oscilator and Reference Divider and clear IREFS to start
ext osc
// CLKS=2, FRDIV=3, IREFS=0, IRCLKEN=0, IREFSTEN=0
MCG_C1 = MCG_C1_CLKS(2) | MCG_C1_FRDIV(3);
/* if we
aren't using an osc input we don't need to wait for the
osc to init */
#if (!defined(K60_CLK) && !defined(ASB817))
while (!(MCG_S & MCG_S_OSCINIT_MASK)){}; // wait for oscillator to initialize
#endif
while (MCG_S & MCG_S_IREFST_MASK){}; // wait for Reference
clock Status bit to clear
while (((MCG_S & MCG_S_CLKST_MASK) >> MCG_S_CLKST_SHIFT) != 0x2){}; // Wait for clock
status bits to show clock source is ext ref clk
// Now in FBE
#if (defined(K60_CLK))
MCG_C5 = MCG_C5_PRDIV(0x18);
#else
// Configure PLL Ref Divider, PLLCLKEN=0, PLLSTEN=0, PRDIV=5
// The crystal frequency is used to select the
PRDIV value. Only even frequency crystals are supported
// that will produce a 2MHz reference
clock to the PLL.
MCG_C5 = MCG_C5_PRDIV(crystal_val); // Set PLL
ref divider to match the crystal used
#endif
// Ensure MCG_C6 is at
the reset default of 0. LOLIE disabled, PLL disabled, clk
monitor disabled, PLL VCO divider is clear
MCG_C6 = 0x0;
// Select the
PLL VCO divider and system clock dividers depending on clocking option
switch (clk_option) {
case 0:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG, FlexBus = MCG, Flash
clock= MCG/2
set_sys_dividers(0,0,0,1);
// Set the
VCO divider and enable the PLL for 50MHz, LOLIE=0, PLLS=1, CME=0, VDIV=1
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(1); //VDIV = 1 (x25)
pll_freq = 50;
break;
case 1:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG/2, FlexBus = MCG/2, Flash
clock= MCG/4
set_sys_dividers(0,1,1,3);
// Set the
VCO divider and enable the PLL for 100MHz, LOLIE=0, PLLS=1, CME=0, VDIV=26
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(26); //VDIV = 26 (x50)
pll_freq = 100;
break;
case 2:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG/2, FlexBus = MCG/2, Flash
clock= MCG/4
set_sys_dividers(0,1,1,3);
// Set the
VCO divider and enable the PLL for 96MHz, LOLIE=0, PLLS=1, CME=0, VDIV=24
MCG_C6 = MCG_C6_PLLS_MASK | MCG_C6_VDIV(24); //VDIV = 24 (x48)
pll_freq = 96;
break;
case 3:
// Set system
options dividers
//MCG=PLL, core = MCG, bus = MCG, FlexBus = MCG, Flash
clock= MCG/2
set_sys_dividers(0,0,0,1);
// Set the
VCO divider and enable the PLL for 48MHz, LOLIE=0, PLLS=1, CME=0, VDIV=0
MCG_C6 = MCG_C6_PLLS_MASK; //VDIV = 0 (x24)
pll_freq = 48;
break;
}
while (!(MCG_S & MCG_S_PLLST_MASK)){}; // wait for PLL
status bit to set
while (!(MCG_S & MCG_S_LOCK_MASK)){}; // Wait for LOCK
bit to set
// Now running
PBE Mode
// Transition into PEE by setting
CLKS to 0
// CLKS=0, FRDIV=3, IREFS=0, IRCLKEN=0, IREFSTEN=0
MCG_C1 &= ~MCG_C1_CLKS_MASK;
// Wait for clock
status bits to update
while (((MCG_S & MCG_S_CLKST_MASK) >> MCG_S_CLKST_SHIFT) != 0x3){};
// Now running
PEE Mode
return pll_freq;
} //pll_init
这个函数中两个参数clk_option和crystal_val是两个枚举变量(enum),从它们的定义来看它们的取值范围是0~3和0~15.所以函数开始先进行有效性判断。
MCG表示的是 Multipurpose Clock Generator(多用途时钟发生器).MCG模块向MCU提供几个时钟源选择。这个模块包含了一个 frequency-locked loop (FLL)和一个
phase-locked loop (PLL).FLL可以通过内部的或外部的参考时钟源来控制,PLL可以通过外部参考时钟来控制。模块可以选择FLL或PLL的输出时钟或者内部或外部时钟源作为MCU的系统时钟。上面MCG_2指的是 MCG Control 2 Register. MCG一共有9种操作模式:FEI, FEE, FBI, FBE, PBE, PEE, BLPI,BLPE,and Stop. 不同运行模式的功耗互不相同,要进入各个模式需要写MCG_1~MCG_6寄存器组中某几个寄存器中的某几个位。 我们可以这样说,采用内部组件的模式所消耗的功率少于采用外部组件的模式。而MCG包括两种专门为低功率应用设计的模式——旁通低功耗内部(BLPI)和旁通低功耗外部(BLPE)。驱动BLPI模式的总线频率要比BLPE
模式低,因此BLPI 模式消耗的功率最小。下图2总结了每一种运行模式的功耗。
相关文章推荐
- IAR中cortex-m4启动流程分析
- IAR中cortex-m4启动流程分析
- IAR中cortex-m4启动流程分析
- IAR中cortex-m4启动流程分析
- Cortex-M4的启动过程分析从GCC开始-Kinetis K60为例
- u-boot第一阶段启动流程分析
- Linux2.6.38内核启动流程分析
- ARM+Linux系统启动流程分析----ARM处理器的启动流程
- 内核启动流程分析之配置
- 1.移植uboot-分析uboot启动流程(详解)
- MySQL源码分析及核心内幕之4 -- 源码服务端main函数开始及启动流程
- ARM-Linux内核移植之(一)——内核启动流程分析 .
- Uboot启动流程分析
- linux启动流程分析-内核解压缩过程
- ubuntu init启动流程分析
- ARM Linux启动流程分析——内核自解压阶段
- nginx源码分析—启动流程
- 2014.4新版uboot启动流程分析