做了一年NXP智能汽车竞赛,对ARM的理解也都只停留在使用某宝商家提供的库和近几年比赛一直在使用的K60上,对ARM单片机Cortex-M4认识也是一直未识庐山真面目。手边有很多比赛留下的K60,也就以K60DN512ZVLQ(不带Z的版本在时钟初始化上不太一致)为例了。
你可能需要安装 arm-none-eabi-gcc,make等GCC交叉编译工具链
K60的flash从0x0000_0000开始,所谓的二进制程序也是烧录在这里。简单的来说,我们要做的第一步就是把中断向量表放到程序的最开始。
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
从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
};
.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很简单但是,前提工作却不少。当然了不要忘记引用头文件方便读写寄存器,虽然手动定义地址也同样可以做到。
#include "MK60DZ10.h"
// 这些值都在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];
}
}
// 关看门狗
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) {
}
}
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();
}
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 长度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函数
上面给出了RAM地址的计算方法,我以128K为例,上下64K。一般来说还需要将中断向量表像.data一样搬运到RAM中重新设置中断向量表的起始地址才可以在代码中设置各种中断,但为了简化并没有这么做。我们把栈顶放到RAM的末尾即可。
.data : AT(_DATA_ROM) /*设定.data的LMA地址,位于.text之后*/
{
. = ALIGN(4);
_data_start = .;
_DATA_RAM = .; /*.data的VMA起始地址*/
*(.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); //计算出 栈顶的地址
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作为结尾
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做了详尽的解释,值得一读。这是我第一篇比较长的原创博客,希望支持