嵌入式教程中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