Cortex-M4的启动过程分析从GCC开始-Kinetis K60为例

做了一年NXP智能汽车竞赛,对ARM的理解也都只停留在使用某宝商家提供的库和近几年比赛一直在使用的K60上,对ARM单片机Cortex-M4认识也是一直未识庐山真面目。手边有很多比赛留下的K60,也就以K60DN512ZVLQ(不带Z的版本在时钟初始化上不太一致)为例了。

你可能需要安装 arm-none-eabi-gcc,make等GCC交叉编译工具链

储存映射

Cortex-M4的启动过程分析从GCC开始-Kinetis K60为例_第1张图片

K60的flash从0x0000_0000开始,所谓的二进制程序也是烧录在这里。简单的来说,我们要做的第一步就是把中断向量表放到程序的最开始。

Cortex-M4的启动过程分析从GCC开始-Kinetis K60为例_第2张图片

Cortex-M4也是从0x0000_0000开始执行的,中断向量表也是从这里起始,中断向量标的第一个地址是sp(Stack Top),第二个地址是Reset_Handler(复位中断),上电之后先赋值sp,然后跳转的Reset_Handler。

$ arm-none-eabi-objdump -S a.elf

a.elf:     file format elf32-littlearm


Disassembly of section .text:

00000410 :
 410:   b672            cpsid   i
 412:   f000 f8ef       bl      5f4 
 416:   e7fe            b.n     416 0x6>

通过objdump反汇编 按照小端编码读法,上一个图Reset_Handler在内存中就是0x410

编写启动的汇编码 startup.s

从NXP官方的示例中截取了一部分汇编码 因为我们只需要做到最简单的启动,所以只需要前两个中断向量

  • 向量表部分代码
    .section .isr_vector, "a" /*单独定义一个.isr_vector段,在ld script中设定向量表的内存位置*/
    .align 2
    .globl __isr_vector
__isr_vector:
    .long   __StackTop  /*Top Stack定义在ld script中, 之后会说到*/
    .long   Reset_Handler  /* Reset Handler */

等效于

typedef void (*vector_handler)(void);
extern void Reset_Handler();
extern char __StackTop[]; // __StackTop在ld script中需要声明

__attribute__((section(".isr_vector"))) // 将VectroTable放入.isr_vector段中
vector_handler VectorTable[] = {
    (vector_handler)__StackTop,
    Reset_Handler
};

//.FlashConfig段也不可忽略,汇编中并没有写出来
__attribute__((section(".FlashConfig")))
long FlashConfig[] = {
    0xFFFFFFFF,
    0xFFFFFFFF,
    0xFFFFFFFF,
    0xFFFFFFFE
};
  • Reset_Handler 部分代码
.text
.thumb
.thumb_func
.global Reset_Handler
.align 2
.weak    Reset_Handler
.type    Reset_Handler, %function
Reset_Handler:
    cpsid   i /*关闭全局中断*/
    BL     start /*跳转的C语言start函数*/
    B .

等效于

void Reset_Handler() {
    __asm__("cpsid   i");
    start();
    while(1);
}

点亮LED

点亮LED很简单但是,前提工作却不少。当然了不要忘记引用头文件方便读写寄存器,虽然手动定义地址也同样可以做到。

#include "MK60DZ10.h"
  1. 初始化段
    一般来说,运行于操作系统下的应用程序由系统初始化(.bss .data的详细解释自行搜索)段,但是单片机上的逻辑程序需要自力更生,自己初始化自己。一般是bss段的清零和.data段的初始数据拷贝,这些函数的数据也都不在这些段上(都在text段中作为常量),所以在运行时并不会出错。
// 这些值都在ld script中,只有连接时才能确认其值,所以先声明
extern char _bss_start[], _bss_end[]; 
extern char _DATA_RAM[], _DATA_ROM[], _DATA_LEN[];

void clear_bss() {
  for(char * i = _bss_start; i < _bss_end; i+=4) {
      *(uint32_t *)i = 0x0;
  }
}

void load_data() {
  uint32_t i = (uint32_t)_DATA_LEN;
  while(i--) {
      ((uint32_t *)_DATA_RAM)[i] = ((uint32_t*)_DATA_ROM)[i];
  }
}
  1. 关看门狗初始化时钟
    这两项工作都是初始单片机最必要的工作,同样采取最简单的方法来完成
// 关看门狗
void dis_wdog() {
    WDOG->UNLOCK = (uint16_t)0xC520u;     /* Key 1 */
    WDOG->UNLOCK  = (uint16_t)0xD928u;    /* Key 2 */
    WDOG->STCTRLH = (uint16_t)0x01D2u;
}

/*
如下所述使用内部时钟源,核心时钟,总线时钟都是41.94Mhz
Multipurpose Clock Generator (MCG) in FLL Engaged Internal (FEI) mode
Reference clock source for MCG module is the slow internal clock source 32.768kHz
Core clock = 41.94MHz, BusClock = 41.94MHz
*/
void clock_setup() {
    SIM->CLKDIV1 = (uint32_t)0x00110000u;
    MCG->C1 = (uint8_t)0x06u;
    MCG->C2 = (uint8_t)0x00u;
    MCG->C4 = (uint8_t)((MCG->C4 & (uint8_t)~(uint8_t)0xC0u) | (uint8_t)0x20u);
    MCG->C5 = (uint8_t)0x00u;
    MCG->C6 = (uint8_t)0x00u;
    while((MCG->S & MCG_S_IREFST_MASK) == 0u) {
    }
    while((MCG->S & 0x0Cu) != 0x00u) {
    }
}
  1. 点亮LED
    LED使用了PTB20引脚,LED外部接3.3v,输出低电平点亮
int pin = 20; // 位于.data 段

void led() {
    SIM->SCGC5 |= SIM_SCGC5_PORTB_MASK; // PTB时钟
    PORTB->ISFR = (1 << pin);
    PORTB->PCR[pin] = (0x01 << PORT_PCR_MUX_SHIFT); // ALT1复用GPIO

    PTB->PDDR |= (1 << pin); // GPIO方向为输出
    PTB->PDOR &= ~(1 << pin); // 输出低电平
}

void start() {
    dis_wdog();
    clock_setup();
    clear_bss();
    load_data();
    led();
}

重要 分析ld script与连接过程

ld script 段内存分配

ENTRY(Reset_Handler) /*入口点*/

/*RX意味只读,片内flash也不可以直接写入操作 RW意味读写,同时也位于SRAM*/
MEMORY
{
  m_interrupts   (RX)  : ORIGIN = 0x00000000, LENGTH = 0x00000400
  m_flash_config (RX)  : ORIGIN = 0x00000400, LENGTH = 0x00000010
  m_text         (RX)  : ORIGIN = 0x00000410, LENGTH = 0x0007FBF0
  m_data         (RW)  : ORIGIN = 0x1FFF0000, LENGTH = 0x00010000
  m_data_2       (RW)  : ORIGIN = 0x20000000, LENGTH = 0x00010000
}

.isr_vector,.FlashConfig与.text

从上面可以看出 .isr_vector 长度1024字节,.FlashConfig长度16字节 占据了 Flash最开始的一部分(前0x410)。

/*KEEP 在使用--gc-sections时不会去掉该段*/
/* '>' 输出到 MEMORY对应的段区域而ALIGN是强制对齐*/
.interrupts :
{
    . = ALIGN(4);
    KEEP(*(.isr_vector))
} > m_interrupts

.flash_config :
{
    . = ALIGN(4);
    KEEP(*(.FlashConfig))
} > m_flash_config

.text是代码段 同时存放了.rodata(常量)段

.text :
{
    . = ALIGN(4);
    *(.text)
    *(.rodata)
    . = ALIGN(4);
    _DATA_ROM = .;
} > m_text

与此同时_DATA_ROM记录了.text的地址结束,每个段都有两个地址,LMA(虚拟地址)和VMA(虚拟地址),当LMA不指定时缺省为VMA相同。_DATA_ROM也是.data的LMA的起始。
如上文

int pin = 20;

pin是一个内存中的变量,20为pin的初始值,20存放在.data的LMA(FLASH)中,pin在.data的VMA(RAM)中,启动时需要吧20从LMA搬运到VMA,也就是之前提到的load_data函数

内存布局

Cortex-M4的启动过程分析从GCC开始-Kinetis K60为例_第3张图片
上面给出了RAM地址的计算方法,我以128K为例,上下64K。一般来说还需要将中断向量表像.data一样搬运到RAM中重新设置中断向量表的起始地址才可以在代码中设置各种中断,但为了简化并没有这么做。我们把栈顶放到RAM的末尾即可。

.data : AT(_DATA_ROM) /*设定.dataLMA地址,位于.text之后*/
 {
     . = ALIGN(4);
     _data_start = .;
     _DATA_RAM = .; /*.dataVMA起始地址*/
     *(.data)
     . = ALIGN(4);
     _data_end = .;
 } > m_data
 _DATA_LEN = _data_end - _data_start; /*计算.data的长度*/

 .bss :
 {
     . = ALIGN(4);
     _bss_start = .;
     *(.bss)
     . = ALIGN(4);
     _bss_end = .;
 } > m_data

 __StackTop = ORIGIN(m_data_2) + LENGTH(m_data_2); //计算出 栈顶的地址
  • 如果对ld语法不是很熟悉,这里给出完整的代码
ENTRY(Reset_Handler)

MEMORY
{
  m_interrupts          (RX)  : ORIGIN = 0x00000000, LENGTH = 0x00000400
  m_flash_config        (RX)  : ORIGIN = 0x00000400, LENGTH = 0x00000010
  m_text                (RX)  : ORIGIN = 0x00000410, LENGTH = 0x0007FBF0
  m_data                (RW)  : ORIGIN = 0x1FFF0000, LENGTH = 0x00010000
  m_data_2              (RW)  : ORIGIN = 0x20000000, LENGTH = 0x00010000
}

SECTIONS
{
    .interrupts :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
    } > m_interrupts

    .flash_config :
    {
        . = ALIGN(4);
        KEEP(*(.FlashConfig))
    } > m_flash_config

    .text :
    {
        . = ALIGN(4);
        *(.text)                 /* .text sections (code) */
        *(.rodata)
        . = ALIGN(4);
        _DATA_ROM = .;
    } > m_text

    .data : AT(_DATA_ROM)
    {
        . = ALIGN(4);
        _data_start = .;
        _DATA_RAM = .;
        *(.data)
        . = ALIGN(4);
        _data_end = .;
    } > m_data
    _DATA_LEN = _data_end - _data_start;

    .bss :
    {
        . = ALIGN(4);
        _bss_start = .;
        *(.bss)
        . = ALIGN(4);
        _bss_end = .;
    } > m_data

    __StackTop = ORIGIN(m_data_2) + LENGTH(m_data_2);
}

makefile

到了最后编译的时候,用简单的makefile作为结尾

arm = arm-none-eabi-
cc = $(arm)gcc

objcpy = $(arm)objcopy
readelf = $(arm)readelf
objdump = $(arm)objdump

flags = -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections 
link = -Xlinker --gc-sections
cpu = -mcpu=cortex-m4 -mthumb -O0

all:
    $(cc) $(cpu) $(flags) -I./include -std=c99 -O0 -c -o start.o start.c
    #$(cc) $(cpu) $(flags) -x assembler-with-cpp -c -o startup.o startup.s #都写成C就不需要了 
    $(cc) $(cpu) $(flags) $(link) -T flash.ld  -o a.elf start.o startup.o -Wl,-Map a.map
    $(objcpy) -O binary a.elf a.bin

elf和bin文件还是不同的,具体不在说明,可以通过jflash或者open OCD将bin文件下载至K60,也不再详细解释

说在最后

这里的代码大多参考自NXP官方例程,同时而受到了《深入理解BootLoader》 胡尔佳著 的启发,其中对ld script做了详尽的解释,值得一读。这是我第一篇比较长的原创博客,希望支持

你可能感兴趣的:(ARM)