嵌入式教程中LED灯以及程序教程中的”Hello world”都有其特殊的意义,意味着入门。笔者此处也不例外,分别以汇编、c语言在交叉编译环境下点LED灯作为NanoPi-NEO2的入门程序。点LED灯之前必须对芯片有基本的认识,包括其指令集、流水线等内核架构,基本的启动流程,基本的编译器开发特性等,只有这样点亮的LED灯才算实现其意义。
1. 指令集
NapoPi-NEO2采用了Allwinner H5处理器,该处理器是基于64位、四核Cortex-A53的ARMv8-A内核架构。支持两个最主要的指令集:32位指令集A32/T32(ARM/Thumb指令集)和64位指令集A64。A64为ARMv8-A新增的指令集,用于支持64位的ARM核,A32/T32用于向前兼容ARMv7架构,使之支持现存的32位指令集。其定义了AArch64和AArch32两套运行环境,分别执行64位和32位指令集,软件可以在需要的时候,进行运行环境的切换。目前ARM指令集是向前兼容的,即ARMv8-A的处理器几乎可以直接执行ARMv4架构的ARM指令集代码(如ARM7的应用代码)。因此,只需要支持ARMv4以上指令集的编译器即可编译出Cortex-A53可运行的二进制代码。
2. 流水线
Allwinner H5内核Cortex-A53配置了先进的超标量体系结构管线,能够同时执行多条指令,提供2.3 MIPS/MHz的运算性能,集成了32k/32k的指令/数据一级缓存以及512k的二级缓存,从而达到最快的读取速度和最大的吞吐量,使用了先进的分支预测技术,并且具有专用的NEON整形和浮点型管线进行媒体和信号处理。
Cortex-A53流水线架构基于双对称、顺序发射的8级流水线,硬件上具有I/D Cache、分支预测结构,因此指令在流水线的流入流出过程变得不明确,但仍可以通过统计分析其大概的过程。
Allwinner H5在上电启动后,最先启动其中之一的CPU核,该CPU核尝试从各个储存设备加载代码启动,当从某一储存设备找到正确的启动代码,会从该储存设备加载代码到内部的SRAM,并启动执行。Allwinner H5在启动阶段是处于默认的CPU状态,其 I/D Cache,L2 Cache、分支预测等均是关闭的,运行时钟采用的是外部晶体时钟,针对于NanoPi-NEO2,为24M。我们可以根据以上信息,开启I Cache,设计一个较精确的软件延时函数,每次访问I Cache均会命中,每次访问D Cache均从主存读取,需要相应周期的等待延时,每次跳转均会分支预测失败,清流水线需额外8个CPU时钟。在一个实用的系统中,I/D Cache、分支预测等硬件功能必须开启,不然CPU性能大打折扣。
3. 汇编实现
汇编代码中有两点需要注意:
1) CPU启动时需要验证启动代码,只有验证通过才会加载执行,因此启动代码需要加入相应的启动头。
2) 此处避开链接器功能,不使用链接文件,编写的闪烁灯代码应该是位置无关的,即代码加载进任意RAM位置都是可以正确执行的。
#define PA_BASE (0x01c20800)
#define CFG0_REG_OFS 0x00
#define CFG1_REG_OFS 0x04
#define CFG2_REG_OFS 0x08
#define CFG3_REG_OFS 0x0c
#define DAT_REG_OFS 0x10
.text
.global_start
_start:
B reset // one intruction jumping to realcode
.ascii"eGON.BT0" // magic="eGON.BT0"
.word 0 // check_sum generated by PC
.word 0x2000 // length generated by PC
.word 48 // the size of boot_file_head_t
.word 0 // the version of boot_file_head_t
.word 0x10000 // the return value
.word 0x10000 // run addr
.word 0 // eGON version
.space8 // platform information
reset:
//开启i-cache
MRC P15, 0, R0, C1, C0, 0
ORR R0, R0, #0x00001000
MCR P15, 0, R0, C1, C0, 0
BLGpio_Init
Loop:
LDR R0, =PA_BASE
LDR R1, [R0, #DAT_REG_OFS]
ORR R1, R1, #(1<<10)
STR R1, [R0, #DAT_REG_OFS] // LED Blue on
BL Delay_s // delay 1s
BIC R1, R1, #(1<<10)
STR R1, [R0, #DAT_REG_OFS] // LED Blue off
BL Delay_s // delay 1s
B Loop
Gpio_Init:
LDR R0, =PA_BASE
LDR R1, [R0, #CFG1_REG_OFS]
BIC R1, R1, #(0x7<<8) // pin 10
ORR R1, R1, #(0x1<<8) // output
STR R1, [R0, #CFG1_REG_OFS]
BX LR
// CPU CLOCK 24M
Delay_s:
LDR R6, =1200000; // 延时1s
// 跳转清流水线,以下指令均只用作填充流水线
Delay2:
SUBS R6, R6, #1 // 双发射
MOV R0, R0 // 单发射, cycle 1
MOV R0, R0 // 单发射, cycle 2
MOV R0, R0 // 单发射, cycle 3
MOV R0, R0 // 单发射, cycle 4
MOV R0, R0 // 单发射, cycle 5
MOV R0, R0 // 单发射, cycle 6
MOV R0, R0 // 单发射, cycle 7
MOV R0, R0 // 单发射, cycle 8
MOV R0, R0 // 单发射, cycle 9
MOV R0, R0 // 单发射, cycle 10
MOV R0, R0 // 单发射, cycle 11
MOV R0, R0 // 单发射, cycle 12
BNE Delay2 // 跳转会清流水线,8个ARMCLOCK,cycle 20
BX LR
编译汇编文件led.S
arm-linux-gcc -c led.S -o led.o
链接生成elf可执行文件
arm-linux-ld led.o -o led.elf
生成二进制可执行文件
arm-linux-objcopy -O binary -S led.elf led.bin
注:arm-linux-gcc为32位arm交叉编译器,可以编译ARMv4指令集即可。针对NanoPi-NEO2,相应可用的交叉编译器位于\lichee\brandy\toolchain\gcc-arm\bin\ arm-linux-gnueabi-gcc,用arm-linux-gnueabi-gcc相应的位置路径替换掉arm-linux-gcc即可。
4. C实现
C代码中需要注意两点:
1) 需要汇编指令跳转到c函数,链接器默认链接_start符号做为代码的开头,除了相应的启动头,需一条跳转汇编指令链接到代码起启位置,用来跳转到c入口。
.text
.global _start
_start:
B reset// one intruction jumping to real code
.ascii "eGON.BT0" //magic="eGON.BT0"
.word 0// check_sum generated by PC
.word 0x2000// length generated by PC
.word 48// the size of boot_file_head_t
.word 0// the version of boot_file_head_t
.word 0x10000// the return value
.word 0x10000// run addr
.word 0// eGON version
.space 8 // platform information
reset:
// 开启i-cache
MRC P15,0, R0, C1, C0, 0
ORR R0,R0, #0x00001000
MCR P15,0, R0, C1, C0, 0
LDR SP,=0x17000
.extern main
BL main
2)c文件中不要尝试使用c库以及使用全局变量、静态变量等,因为此处避开链接器功能,不使用链接文件,编写的闪烁灯代码c运行环境是位置无关的,只有栈是有效的。
#define PA_BASE (0x01c20800)
#define CFG0_REG_OFS0x00
#define CFG1_REG_OFS0x04
#define CFG2_REG_OFS0x08
#define CFG3_REG_OFS0x0c
#define DAT_REG_OFS 0x10
#define writel(value,reg) *(volatile unsigned int*)(reg)=value
#define readl(reg) (*(volatile unsigned int *)(reg))
void Delay_s(void)
{
// CPU CLOCK 24M, 循环体每次20个Arm clock, 延时一秒
unsigned int temp1 = 1200000;
unsigned int temp2 = 0;
asm volatile (
"1:\n"
"subs %0,%0, #1\n" // 单发射 cycle 1
// 跳转清流水线,以下指令均只用作填充流水线
"mov %1, %1\n" // 双发射 cycle 1
"mov %1, %1\n" // 单发射 cycle 2
"mov %1, %1\n" // 单发射 cycle 3
"mov %1, %1\n" // 单发射 cycle 4
"mov %1, %1\n" // 单发射 cycle 5
"mov %1, %1\n" // 单发射 cycle 6
"mov %1, %1\n" // 单发射 cycle 7
"mov %1, %1\n" // 单发射 cycle 8
"mov %1, %1\n" // 单发射 cycle 9
"mov %1, %1\n" // 单发射 cycle 10
"mov %1, %1\n" // 单发射 cycle 11
"mov %1, %1\n" // 单发射 cycle 12
"bne 1b\n" // 跳转会清流水线,8级流水线,cycle20
: "+r"(temp1):"r"(temp2): "cc"
);
}
void Gpio_Init(void)
{
unsigned int value;
value = readl(PA_BASE+CFG1_REG_OFS);
value &= ~(7<<8); // pin10
value |= (1<<8);
writel(value, PA_BASE+CFG1_REG_OFS);
}
void main(void)
{
unsigned int value;
Gpio_Init();
while (1) {
value = readl(PA_BASE+DAT_REG_OFS);
value |= (1<<10); // LED Blue on
writel(value, PA_BASE+DAT_REG_OFS);
Delay_s();
value &= ~(1<<10); // LED Blueoff
writel(value, PA_BASE+DAT_REG_OFS);
Delay_s();
}
}
编译汇编文件start.S
arm-linux-gcc -c start.S -o start.o
编译c文件led.c
arm-linux-gcc -c led.c -nostdlib -o led.o
链接生成elf可执行文件
arm-linux-ld start.o led.o -o led.elf
生成二进制可执行文件
arm-linux-objcopy -O binary -S led.elf led.bin
注:arm-linux-gcc为32位arm交叉编译器,可以编译ARMv4指令集即可。针对NanoPi-NEO2,相应可用的交叉编译器位于\lichee\brandy\toolchain\gcc-arm\bin\ arm-linux-gnueabi-gcc,用arm-linux-gnueabi-gcc相应的位置路径替换掉arm-linux-gcc即可。
5. 闪烁灯烧写运行
编译器直接编译生成的二进制代码是不满足相应的启动格式的,需要通过Allwinner工具gen_check_sum添加代码校验和等等。gen_check_sum工具在uboot目录树中\lichee\brandy\u-boot-2014.07\tools。
把gen_check_sum工具拷贝到源码目录,制作启动代码。
./gen_check_sum led.bin led_boot.bin
把二进制启动代码led_boot.bin烧写进储存设备,对于sd/mmc卡,CPU从设备8k位置处加载启动代码。
用dd命令烧写进sd/mmc卡,/dev/sdb为sd/mmc卡设备文件。
dd bs=1k seek=8 if=./led_boot.bin of=/dev/sdb
6. 结语
在交叉编译环境下,可以编译相应的c库如newlib、uclibc等等,加入第三方中间件,如uCOS、FreeRTOS等RTOS,emWin等GUI,Fatfs、yaffs等文件系统,lwip等tcp/ip协议等等,可以构建最基本裸机开发架构。
交叉编译环境下汇编闪烁灯工程例程以及C闪烁灯工程例程,gen_check_sum相关工具。
源码:http://pan.baidu.com/s/1dFvh83n