RK3399——裸机大全

版权声明:本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,欢迎转载,但转载请注明来自hceng blog(www.hceng.cn),并保持转载后文章内容的完整。本人保留所有版权相关权利。 https://blog.csdn.net/hceng_linux/article/details/89913950

CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2018/08/16/RK3399——裸机大全/#more

以64位的RK3399为例,实现裸机的启动、中断、串口(printf移植)、定时器、ADC、PWM、I2C、SPI、LCD(MIPI)等;

这应该是最后一次写裸机代码了,老是写裸机,都要写吐了。
这次选的是64位平台(ARMv8架构)的Firefly-RK3399,注定坑多,也更有挑战性一点。

1.ARMv8基础

1.1 基本概念

** 1.架构和内核型号 **

  • 架构(Architecture):就是常说的ARMv5(32bits)、ARMv6(32bits)、ARMv7(32bits)、ARMv8(32/64bits);
  • 内核型号:就是常说的ARM7、ARM9、Cortex-A系列(Aplication)、Cortex-R系列(Runtime)、Cortex-M系列(MCU);
  • 举例:单片机STM32F103C8T6采用Cortex-M3内核,采用ARMv7-M架构;
        瑞芯微RK3288采用4个Cortex-A17,采用ARMv7-A架构;
        瑞芯微RK3399采用2个Cortex-A72和4个Cortex-A53组成,Cortex-A72和Cortex-A53都是ARMv8-A架构。
        高通骁龙845处理器由4个Cortex-A75和4个Cortex-A55组成,Cortex-A75和Cortex-A55都是ARMv8-A架构。
  • 发展迭代

** 2.AArch64/AArch32/A64/A32/T32 **

名字 类型 说明
AArch64 架构 指基于64bits运作的ARMv8架构(通用寄存器X0-X30)
AArch32 架构 指基于32bits运作的ARMv8架构,并且兼容之前的ARMv7架构(通用寄存器R0-R15)
A64 指令集 指在AArch64模式下支持的ARM 64bits指令集
A32 指令集 指ARMv7架构下支持的ARM 32bits指令集,在ARMv8中也有新加入的A32指令集
T32 指令集 指ARMv7架构下支持的Thumb2 16/32bits指定集,在ARMv8中也有新加入的T32指令集。

1.2 AArch64/32寄存器

AArch64 Special Role in the procedure call standard
x0…x7 Parameter/result registers(参数传入/返回结果)
x8 Indirect result location register
x9…x15 Temporary registers(临时寄存器)
x16 IP0 The first intra-procedure-call scratch register (can be used by call veneers and PLT code); at other times may be used as a temporary register.
x17 IP1 The second intra-procedure-call temporary register (can be used by call veneers and PLT code); at other times may be used as a temporary register.
x18 The Platform Register, if needed; otherwise a temporary register.
x19…x28 Callee-saved registers(由被调用者保存的寄存器)
x29 FP The Frame Pointer(栈帧指针)
x30 LR The Link Register(链接寄存器)
SP The Stack Pointer(栈指针)
AArch32 Special Role in the procedure call standard
r0…r3 Parameter/result registers
r4…r11 Temporary registers (r9 also as platform register)
r12 IP The Intra-Procedure-call scratch register.
r13 SP The second intra-procedure-call temporary register (can be used by call veneers and PLT code); at other times may be used as a temporary register.
r14 LR The Platform Register, if needed; otherwise a temporary register.
r15 PC Callee-saved registers

两者区别:

Execution State Note
AArch64 1. 提供31个64bits的通用寄存器(x0~x30,其中x30可作为LR)
2. 提供64bits程序计数器(PC)、栈指针(SP)、异常链接寄存器(ELR)
3. 提供32个128bits 的SIMD Vector与Scalar Floating-Point寄存器
4. 定义ARMv8 EL0~EL3共4个执行权限(Execution Privilege)
5. 支持64bits Virtual-Addressing
6. 定义一组PSTATE用以保存PE(Processing Element)状态
AArch32 1. 提供16个32bits的通用寄存器(r0~r12,其中r13=SP、r14=LR、r15=PC,且r14需要同时供ELR与LR之用)
2. 提供一个ELR,用以作为从Hyp-Mode的Exception返回之用
3. 提供32个64bits的Advanced SIMD Vector与Scalar Floating-Point寄存器
4. 提供A32与T32两种指令集的组合
5. 使用32bits Virtual-Addressing
6. 只使用CPSR(当前程序状态寄存器)保存PE(Processing Element)状态。

1.3 ARMv8 Exception Level

针对Security的需求,ARMv8的系统软件设计可以提供安全模式与非安全模式的状态。
ARMv8规定了CPU有4种运行级别。每种运行级别下标的数字越大,其权力级别越高。其中EL0为非特权等级,即平时应用程序运行时的级别;EL1为特权等级,即操作系统运行时的级别;EL2为虚拟机监视器运行级别,即虚拟机的控制层运行的级别;EL3为切换EL1和EL2级别时需要进入的一个级别,为CPU的最高级别。

若底层EL(Exception Level)为32bits,则上层EL的软件就只能是32位。

若底层的EL为64bits,则上层EL就可以依据需求选择为32bits或是64bits。

2.RK3399启动

先看一下RK3399的启动流程图[1]

从图中可以得到以下几个结论:

  • 1.RK3399上电后,会从0xffff0000获取romcode并运行;
  • 2.然后依次从Nor Flash、Nand Flash、eMMC、SD/MMC获取ID BLOCKID BLOCK正确则启动,都不正确则从USB端口下载;
  • 3.如果emmc启动,则先读取SDRAM(DDR)初始化代码到内部SRAM,然后初始化DDR,再将emmc上的代码(剩下的用户代码)复制到DDR运行;
  • 4.如果从USB下载,则先获取DDR初始化代码,下载到内部SRAM中,然后运行代码初始化DDR,再获取loader代码(用户代码),放到DDR中并运行;
  • 5.无论是何种方式,都需要DDR的初始化代码,结合前面RK3288的经验,就是向自己写的代码加上"头部信息",这个"头部信息"就包含DDR初始化操作;

2.1 官方启动分析

如何分析一款芯片的启动方式?
前面的一篇博客iMX6ULL上手体验,里面已经分析过了,大致就是先用厂家提供的资料,配置相关环境、编译、烧写,运行起来。然后就有了U-boot源码,从U-boot就可以几乎提取出所有的裸机代码,本文也是这样做的。

分析U-Boot的编译流程,可以看到如下内容:

./tools/boot_merger ./tools/rk_tools/RKBOOT/RK3399MINIALL.ini
out:rk3399_loader_v1.09.109.bin
fix opt:rk3399_loader_v1.09.109.bin
merge success(rk3399_loader_v1.09.109.bin)
./tools/trust_merger  ./tools/rk_tools/RKTRUST/RK3399TRUST.ini
out:trust.img
merge success(trust.img)
./tools/loaderimage --pack --uboot u-boot.bin uboot.img
pack input u-boot.bin 
pack file size: 315128 
crc = 0xb4d13cd6
uboot version: U-Boot 2014.10-RK3399-06 (Aug 16 2018 - 04:00:27)
pack uboot.img success! 
/work/firefly-rk3399
Firefly-RK3399 make images finish!

可以看出这里使用了三个工具,产生了三个文件:
①:使用boot_merger,参数为RK3399MINIALL.ini,得到loader文件rk3399_loader_v1.09.109.bin,打开RK3399MINIALL.ini内容为:

[CHIP_NAME]
NAME=RK330C
[VERSION]
MAJOR=1
MINOR=09
[CODE471_OPTION]
NUM=1
Path1=tools/rk_tools/bin/rk33/rk3399_ddr_800MHz_v1.09.bin
Sleep=1
[CODE472_OPTION]
NUM=1
Path1=tools/rk_tools/bin/rk33/rk3399_usbplug_v1.09.bin
[LOADER_OPTION]
NUM=2
LOADER1=FlashData
LOADER2=FlashBoot
FlashData=tools/rk_tools/bin/rk33/rk3399_ddr_800MHz_v1.09.bin
FlashBoot=tools/rk_tools/bin/rk33/rk3399_miniloader_v1.09.bin
[OUTPUT]
PATH=rk3399_loader_v1.09.109.bin

得知依赖的文件有:DDR相关的rk3399_ddr_800MHz_v1.09.bin、USB相关的rk3399_usbplug_v1.09.bin、miniloader(瑞芯微修改的一个bootloader)相关的rk3399_miniloader_v1.09.bin
boot_merger将这三个bin文件最后合并成rk3399_loader_v1.09.109.bin

②:使用trust_merger,参数为RK3399TRUST.ini,生成trust.img

③:使用loaderimageu-boot.bin变成uboot.img

最后使用Android Tools,烧写rk3399_loader_v1.09.109.bintrust.imguboot.img即可启动U-Boot。

对以上过程进行分析,再加上实验测试和结合RK3288的经验,得出裸机启动文件的制作结论如下:
使用boot_mergerrk3399_ddr_800MHz_v1.09.binrk3399_usbplug_v1.09.bin和自己的裸机文件rk3399.bin合并出新文件即可。

2.2 制作裸机启动文件

经过分析和测试,现实现了emmc和TF卡启动裸机程序,并把整个过程整理了一个工程模板。
工程模板见GitHub,里面包含两个文件夹和两个文件。

code文件夹存放裸机源码;
tools存放制作“头部”的工具和配置文件;
rk3399_hardware_tool.sh是一个shell脚本,用于自动生成加“头部”后的裸机文件;
README.txt是操作说明;

以后只需要先进入code文件夹修改裸机源码,然后执行make生成rk399.bin,再退出到工程目录下,执行脚本rk3399_hardware_tool.sh即可生成rk3288_emmc.binrk3288_sd.bin

  • rk3288_emmc.bin用于emmc启动:Windows下使用AndroidTool.exe,开发板进入MaskRom模式,烧入Loader位置;
  • rk3288_sd.bin用于SD卡启动:Linux下,插上SD卡,执行sudo dd if=rk3399_sd.bin of=/dev/sdb seek=$(((0x000040)))(其中/dev/sdb为SD卡)

3.Uboot启动部分分析

为了方便后面从U-boot提取所需裸机代码,有必要先对U-boot进行分析,本节只分析启动部分的,后续具体某个模块,如LCD,将在后面对应的章节分析。
另外,本次分析是的RK3399,64位的ARMv8架构,与市面上较多的32位ARMv7架构SOC略有区别,注意不要混淆。
RK3399编译过的U-boot已上传GitHub。U-boot执行的第一个文件是start.S,下面开始对其进行分析。

3.1 start.S

所在文件路径:u-boot/arch/arm/cpu/armv8/start.S

  • 1.检查loader tag [unimportant]
    {% codeblock lang:asm %}
    .globl _start
    _start:
    nop
    b reset //hceng:首先跳到reset
    ……

reset:

#ifdef CONFIG_ROCKCHIP
/*
* check loader tag
*/
ldr x0, =__loader_tag
ldr w1, [x0]
ldr x0, =LoaderTagCheck
ldr w2, [x0]
cmp w1, w2
b.eq checkok //hceng:LoaderTag正常则跳到checkok ,反之退出U-Boot进入maskrom or miniloader

ret	                   /* return to maskrom or miniloader */

checkok:
#endif
{% endcodeblock %}
这里检查loader tag对后面写裸机没什么用。

  • 2.设置中断向量等 [important]
    {% codeblock lang:asm %}
    adr x0, vectors //hceng:将中断向量地址保存到x0
    switch_el x1, 3f, 2f, 1f //hceng:根据CurrentEL的bit[3:2]位得知当前的EL级别,跳转到不同的分支进行处理,这里实测跳到3f,即上电为EL3
    3: msr vbar_el3, x0 //hceng:将中断向量保存到vbar_el3(Vector Base Address Register (EL3))
    mrs x0, scr_el3 //hceng:获取scr_el3(Secure Configuration Register)的值
    orr x0, x0, #0xf //hceng:将低四位设置为1:EA|FIQ|IRQ|NS
    msr scr_el3, x0 //hceng:写入scr_el3
    msr cptr_el3, xzr //hceng:清除cptr_el3(Architectural Feature Trap Register (EL3)),Enable FP/SIMD
    ldr x0, =COUNTER_FREQUENCY //hceng:晶振频率:24000000hz
    msr cntfrq_el0, x0 //hceng:将晶振频率写入cntfrq_el0(Counter-timer Frequency register)
    #ifdef CONFIG_ROCKCHIP
    msr cntvoff_el2, xzr /* clear cntvoff_el2 for kernel /
    #endif
    b 0f //hceng:跳到本段结尾的0f,后面的未执行
    2: msr vbar_el2, x0
    mov x0, #0x33ff //hceng:FP为Float Processor(浮点运算器);SIMD为Single Instruction Multiple Data(采用一个控制器来控制多个处理器)
    msr cptr_el2, x0 /
    Enable FP/SIMD /
    b 0f
    1: msr vbar_el1, x0
    mov x0, #3 << 20
    msr cpacr_el1, x0 /
    Enable FP/SIMD */
    0:
    {% endcodeblock %}

注:
1.switch_el这一宏定义伪指令在u-boot/arch/arm/include/asm/macro.h定义;
2.vbar_el3等寄存器定义在文档ARMv8-A_Architecture_Reference_Manual_(Issue_A.a).pdf[2]中;
3.XZR/WZR(word zero rigiser)分别代表64/32位,zero register的作用就是0,写进去代表丢弃结果,拿出来是0;

中断向量的定义在文件u-boot/arch/arm/cpu/armv8/exceptions.S中,内容如下:
{% codeblock lang:asm %}
/*

  • Exception vectors.
    /
    .align 11 //hceng:注意这里的对齐11,是因为vbar_el3的低11为是Reserved,需要为0
    //因此需要从2^11=2k的倍数位置起存放vectors
    .globl vectors
    vectors:
    .align 7 //hceng:每个中断向量的偏移为32字节
    b _do_bad_sync /
    Current EL Synchronous Thread */

    .align 7
    b _do_bad_irq /* Current EL IRQ Thread */

    .align 7
    b _do_bad_fiq /* Current EL FIQ Thread */

    .align 7
    b _do_bad_error /* Current EL Error Thread */

    .align 7
    b _do_sync /* Current EL Synchronous Handler */

    .align 7
    b _do_irq /* Current EL IRQ Handler */

    .align 7
    b _do_fiq /* Current EL FIQ Handler */

    .align 7
    b _do_error /* Current EL Error Handler */

_do_bad_sync: //hceng:对应的异常处理函数
exception_entry
bl do_bad_sync

_do_bad_irq:
exception_entry
bl do_bad_irq

_do_bad_fiq:
exception_entry
bl do_bad_fiq

_do_bad_error:
exception_entry
bl do_bad_error

_do_sync:
exception_entry
bl do_sync

_do_irq:
exception_entry //hceng:保护现场,把ELR/X0~X30保存到堆栈
bl do_irq
exception_exit //hceng:恢复现场,从堆栈恢复ELR/X0~X30

_do_fiq:
exception_entry
bl do_fiq

_do_error:
exception_entry
bl do_error

{% endcodeblock %}
这一部分功能就是根据当前的EL级别,配置中断向量、MMU、Endian、i/d Cache等,比较重要。

  • 3.配置ARM核心特定勘误表 [unimportant]
    {% codeblock lang:asm %}
    /* Apply ARM core specific erratas */
    bl apply_core_errata
    {% endcodeblock %}
    看样子是对ARM做一些勘误,实测没有用到,不重要。

  • 4.lowlevel_init [important]
    {% codeblock lang:asm %}
    /* Processor specific initialization */
    bl lowlevel_init

    ……

WEAK(lowlevel_init)
mov x29, lr /* Save LR */

#if defined(CONFIG_ROCKCHIP)

/* switch to el1 secure */

#if defined(CONFIG_SWITCH_EL3_TO_EL1) //hceng:实测没有定义,不需要从EL3切换到EL1,从前面可以看出,现在已经是EL1
/*
* Switch to EL1 from EL3
/
mrs x0, CurrentEL /
check currentEL /
cmp x0, 0xc
b.ne el1_start /
currentEL != EL3 */

ldr	x0, =0xd00	       /* ST, bit[11] | RW, bit[10] | HCE, bit[8] */
msr	scr_el3, x0
ldr	x0, =0x3c5	       /* D, bit[9] | A, bit[8] | I, bit[7] | F, bit[6] | 0b0101 EL1h */
msr	spsr_el3, x0
ldr	x0, =el1_start
msr	elr_el3, x0
eret

el1_start:
nop
#endif /* CONFIG_SWITCH_EL3_TO_EL1 /
#endif /
CONFIG_ROCKCHIP */

#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3) //hceng:实测定义的是CONFIG_GICV3
branch_if_slave x0, 1f //hceng:通过mpidr_el1寄存器,判断当前处理器是否是从属CPU,如果是选择所有affinity为0的作为主CPU
ldr x0, =GICD_BASE //hceng:把GICD基地址作为参数传给gic_init_secure
bl gic_init_secure //hceng:初始化主CPU的中断寄存器
1:
#if defined(CONFIG_GICV3)
ldr x0, =GICR_BASE //hceng:把GICR基地址作为参数传给gic_init_secure_percpu
bl gic_init_secure_percpu //hceng:初始化其它各个CPU的中断寄存器
#elif defined(CONFIG_GICV2) //hceng:未执行
ldr x0, =GICD_BASE
ldr x1, =GICC_BASE
bl gic_init_secure_percpu
#endif

#if defined(CONFIG_ROCKCHIP)
/*
* Setting HCR_EL2.TGE AMO IMO FMO for exception rounting to EL2
/
mrs x0, CurrentEL /
check currentEL /
cmp x0, 0x8 //hceng:根据CurrentEL的bir[3:2]判断当前运行级别,0xC(EL3)、0x8(EL2)、0x4(EL1)、0x0(EL0),实测并没处于EL2,后面的内容不执行
b.ne endseting /
currentEL != EL2 */

mrs	x9, hcr_el2            //hceng:hcr_el2(Hypervisor Configuration Register)
orr	x9, x9, #(7 << 3)      /* HCR_EL2.AMO IMO FMO set */
orr	x9, x9, #(1 << 27)     /* HCR_EL2.TGE set */
msr	hcr_el2, x9

endseting:
nop
#endif /* CONFIG_ROCKCHIP */

branch_if_master x0, x1, 2f    //hceng:通过mpidr_el1寄存器,判断当前处理器是否是主CPU,如果是选择所有affinity为0的作为主CPU;实测跳到2f

/*
 * Slave should wait for master clearing spin table.
 * This sync prevent salves observing incorrect
 * value of spin table and jumping to wrong place.
 */

#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
#ifdef CONFIG_GICV2
ldr x0, =GICC_BASE
#endif
bl gic_wait_for_interrupt
#endif

/*
 * All slaves will enter EL2 and optionally EL1.
 */
bl	armv8_switch_to_el2  

#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
bl armv8_switch_to_el1
#endif

#endif /* CONFIG_ARMV8_MULTIENTRY */

2: //hceng:前面的都没执行,跳到这,返回
mov lr, x29 /* Restore LR */
ret
ENDPROC(lowlevel_init)
{% endcodeblock %}

注:
1.branch_if_slavebranch_if_masteru-boot/arch/arm/include/asm/macro.h定义;
2.gic_init_securegic_init_secure_percpu这两个中断初始化的关键函数在u-boot/arch/arm/lib/gic_64.S定义;
3.armv8_switch_to_el2armv8_switch_to_el1u-boot/arch/arm/cpu/armv8/exceptions.S定义;

lowlevel_init的主要功能就是中断的初始化,后面写中断服务程序的使用会用到。

  • 5.是否需要在U-Boot开启多核CPU [unimportant]
    {% codeblock lang:asm %}
    branch_if_master x0, x1, master_cpu

    /*

    • Slave CPUs
      /
      slave_cpu:
      wfe
      ldr x1, =CPU_RELEASE_ADDR
      ldr x0, [x1]
      cbz x0, slave_cpu
      br x0 /
      branch to the given address /
      master_cpu:
      /
      On the master CPU /
      #endif /
      CONFIG_ARMV8_MULTIENTRY */
      {% endcodeblock %}
      实测没有定义,不用管。
  • 6.跳转到_main [important]
    到此start.S的工作就基本完成了,接下来就交给ARM公共的_main

3.2 crt0_64.S

所在文件路径:u-boot/arch/arm/cpu/armv8/start.S
_maincrt0_64.S里,crt0C-runtime Startup Code的简称,意思就是运行C代码之前的准备工作,包括设置栈、重定位、清理BSS段等;

  • 1.设置栈 [important]
    {% codeblock lang:asm %}
    /*
  • Set up initial C runtime environment and call board_init_f(0).
    /
    ldr x0, =(CONFIG_SYS_INIT_SP_ADDR) //hceng:设置栈顶为0x80000000=2G
    sub x0, x0, #GD_SIZE /
    allocate one GD above SP /
    bic sp, x0, #0xf /
    16-byte alignment for ABI compliance /
    mov x18, sp /
    GD is above SP */
    {% endcodeblock %}

这里栈需要16字节对齐,即要求地址为16的倍数,只需要二进制位最后四位为0(2的4次方),与前面中断向量地址需要2K对齐,实现原理类似。
另外U-Boot在SP上面分配了一块GD(global data),后面写裸机用不到。

  • 2.board_init_f [important]
    {% codeblock lang:asm %}
    mov x0, #0 //hceng:将0作为参数传入board_init_f
    bl board_init_f
    {% endcodeblock %}
    board_init_f所在文件路径:u-boot/common/board_f.c
    board_init_f中调用initcall_run_list(init_sequence_f)init_sequence_f是个数组,里面是将要进行初始化的函数列表,完成一些前期的初始化工作,比如board相关的early的初始化board_early_init_f、环境变量初始化env_init、串口初始化的serial_init、I2C初始化init_func_i2c、设备树相关准备工作fdtdec_prepare_fdt、打印CPU信息print_cpuinfo、SDRAM初始化dram_init、计算重定位信息setup_reloc等;

  • 3.重定位 [important]
    {% codeblock lang:asm %}
    /*

  • Set up intermediate environment (new sp and gd) and call
  • relocate_code(addr_moni). Trick here is that we’ll return
  • ‘here’ but relocated.
    /
    ldr x0, [x18, #GD_START_ADDR_SP] /
    x0 <- gd->start_addr_sp /
    bic sp, x0, #0xf /
    16-byte alignment for ABI compliance /
    ldr x18, [x18, #GD_BD] /
    x18 <- gd->bd /
    sub x18, x18, #GD_SIZE /
    new GD is below bd */

#ifndef CONFIG_SKIP_RELOCATE_UBOOT
adr lr, relocation_return
ldr x9, [x18, #GD_RELOC_OFF] /* x9 <- gd->reloc_off /
add lr, lr, x9 /
new return address after relocation /
ldr x0, [x18, #GD_RELOCADDR] /
x0 <- gd->relocaddr */
b relocate_code
#endif

relocation_return:
{% endcodeblock %}
先是更新了gd结构体,然后根据宏CONFIG_SKIP_RELOCATE_UBOOT决定是否要重定位。
这里是不需要重定位的,因为链接脚本u-boot.lds里面的链接地址是0x00000000,而RK3399上电后,加头的boot code会自动将代码复制到DDR(0x00000000),两者地址相同,不需要重定位。
重定位的代码在u-boot/arch/arm/lib/relocate_64.S里面。

  • 4.重新设置异常向量表 [important]
    {% codeblock lang:asm %}
    /*
  • Set up final (full) environment
    /
    bl c_runtime_cpu_setup /
    still call old routine /
    {% endcodeblock %}
    如果发生了重定位,需要重新设置异常向量表。c_runtime_cpu_setup定义在start.S里面。
    {% codeblock lang:asm %}
    ENTRY(c_runtime_cpu_setup)
    /
    Relocate vBAR */
    adr x0, vectors
    switch_el x1, 3f, 2f, 1f
    3: msr vbar_el3, x0
    b 0f
    2: msr vbar_el2, x0
    b 0f
    1: msr vbar_el1, x0
    0:

    ret
    ENDPROC(c_runtime_cpu_setup)
    {% endcodeblock %}

  • 5.清理BSS段 [important]
    接下来就是清除BSS段,将未定义的全局变量设置为0。在以前使用Keil单片机编程时,未初始化的全局变量默认为0,那是因为集成开发环境为我们做了清理BSS段的操作,现在没有了集成开发环境,就需要我们自己做。
    {% codeblock lang:asm %}
    /*
  • Clear BSS section
    /
    ldr x0, =__bss_start /
    this is auto-relocated! /
    ldr x1, =__bss_end /
    this is auto-relocated! */
    mov x2, #0
    clear_loop:
    str x2, [x0]
    add x0, x0, #8
    cmp x0, x1
    b.lo clear_loop
    {% endcodeblock %}
  • 6.board_init_r [important]
    接下来就是板子的后半部分的初始化:
    {% codeblock lang:asm %}
    /* call board_init_r(gd_t *id, ulong dest_addr) /
    mov x0, x18 /
    gd_t /
    ldr x1, [x18, #GD_RELOCADDR] /
    dest_addr /
    b board_init_r /
    PC relative jump */

    /* NOTREACHED - board_init_r() does not return */
    {% endcodeblock %}
    board_init_r所在文件路径:u-boot/common/board_f.c
    与前面的board_init_f类似,board_init_r中调用initcall_run_list(init_sequence_r)init_sequence_r是个数组,里面是将要进行初始化的函数列表,又是一系列的初始化操作。之前遇到的LCD初始化就是在这里。
    初始化数组列表最后一个成员是run_main_loop,将最终跳到主循环main_loop

crt0_64.S主要就是为C语言运行设置栈和进行了重定位,以及两个阶段的初始化:board_init_f(front)和board_init_r(rear),最后进入主循环。

3.3 总结

U-Boot启动流程示意图:

4.中断

4.1 分析

在U-Boot中找到如下几个文件:

u-boot/arch/arm/cpu/armv8/rk33xx/irqs.c:包含中断的基本操作,如:初始化、注册、使能等;
u-boot/arch/arm/cpu/armv8/rk33xx/irqs-gic.c:包含非GPIO类型中断的使能、去能;
u-boot/arch/arm/cpu/armv8/rk33xx/irqs-gpio.c:包含GPIO类型中断的使能、去能、触发类型;
u-boot/board/rockchip/rk33xx/demo.c:包含一些测试代码,如:定时器中断测试、GPIO中断测试;

  • irqs.c里的函数:
    首先是**irq_init()里面包含gic中断初始化和gpio中断初始化,函数里注释gic has been init in Start.S和之前的猜测一样,在start.S里面已经gic初始化了;
    然后是
    irq_install_handler()里面实现了中断的注册,即把对应中断号放在g_irq_handler[]数组里;
    再是
    irq_handler_enable(),将对应的中断处理函数使能,具体实现的函数在irqs-gic.cirqs-gpio.c里面。此外还有使能总中断enable_interrupts()
    最后就是
    do_irq()**中断处理函数。

  • irqs-gic.c里的函数:
    包含gic_handler_enable()gic_handler_disable(),在前面irq_handler_enable()调用;

  • irqs-gpio.c里的函数:
    包含gic_handler_enable()gpio_irq_enablegpio_irq_set_type(),在前面irq_handler_enable()调用;

  • demo.c里的函数:
    包含定时器中断测试board_gic_test()和GPIO中断测试board_gpio_irq_test()

因此,除了start.S里的初始化,还需移植irq_install_handler()irq_handler_enabledo_irq()三个函数,此外还有定时器中断测试和GPIO测试函数。

4.2 启动和中断代码

因为start.S里面包含了中断初始化代码,即gic_init_securegic_init_secure_percpu,移植的时候直接复制过来的,因此也把start.S贴出来。
start.S是对U-boot的start.S进行了裁剪和修改,思路和前面U-Boot的流程差不多,几个重定位、绝对跳转、代码对齐的坑,都踩完了,下面的start.S有时间的话可以好好看下。
{% codeblock lang:asm [start.S] https://github.com/hceng/RK3399/tree/master/hardware/3_irq/code/start.S %}

#include “macro.h”

.text
.global _start
_start:
/* Could be EL3/EL2/EL1, Initial State: Little Endian, MMU Disabled, i/dCache Disabled */
ldr x0, =vectors //Exception vectors(Absolute address)
msr vbar_el3, x0 //RVBAR_EL3, Reset Vector Base Address Register (if EL3 implemented)

mrs	x0, scr_el3        
orr	x0, x0, #0xf            //SCR_EL3.NS|IRQ|FIQ|EA 
msr	scr_el3, x0

msr	cptr_el3, xzr           //Enable FP/SIMD 

ldr	x0, =24000000           //24MHz
msr	cntfrq_el0, x0          //Initialize CNTFRQ 

bl	lowlevel_init           //Processor specific initialization

ldr	x0, =0x80000000         //sp=2G
bic	sp, x0, #0xf	        //16-byte alignment for ABI compliance  

bl relocate      

relocate_complete:

bl clean_bss

clean_bss_complete:

//bl main 

ldr	lr, =main                //Absolute address
ret

halt:
b halt

/*******************************************************/
led_debug:
mov x0, #0xff720000
mov x1, #0xff720000
ldr w1, [x1,#4]
orr w1, w1, #0x2000
str w1, [x0,#4]
mov x0, #0xff720000
mov x1, #0xff720000
ldr w1, [x1]
orr w1, w1, #0x2000
str w1, [x0]
b halt

/*******************************************************/
lowlevel_init:
mov x29, lr //Save LR

ldr	x0, =0xFEE00000           //RKIO_GICD_PHYS   GIC DIST 
bl	gic_init_secure

ldr	x0, =0xFEF00000           //RKIO_GICR_PHYS
bl	gic_init_secure_percpu

mov	lr, x29                   //Restore LR
ret	

/*******************************************************/
//ref: u-boot/arch/arm/lib/gic_64.S

/*Initialize Distributor  x0: Distributor Base*/

gic_init_secure:
mov w9, #0x37 //EnableGrp0 | EnableGrp1NS
//EnableGrp1S | ARE_S | ARE_NS
str w9, [x0, 0x0000] //Secure GICD_CTLR
ldr w9, [x0, 0x0004]
and w10, w9, #0x1f //ITLinesNumber
cbz w10, 1f //No SPIs
add x11, x0, (0x0080 + 4)
add x12, x0, (0x0d00 + 4)
mov w9, #~0
0: str w9, [x11], #0x4
str wzr, [x12], #0x4 //Config SPIs as Group1NS
sub w10, w10, #0x1
cbnz w10, 0b
1:
ret

/*Initialize ReDistributor  x0: ReDistributor Base*/

gic_init_secure_percpu:
mrs x10, mpidr_el1
lsr x9, x10, #32
bfi x10, x9, #24, #8 //w10 is aff3:aff2:aff1:aff0
mov x9, x0
1: ldr x11, [x9, 0x0008]
lsr x11, x11, #32 //w11 is aff3:aff2:aff1:aff0
cmp w10, w11
b.eq 2f
add x9, x9, #(2 << 16)
b 1b

/* x9: ReDistributor Base Address of Current CPU */

2: mov w10, #~0x2
ldr w11, [x9, 0x0014]
and w11, w11, w10 //Clear ProcessorSleep
str w11, [x9, 0x0014]
dsb st
isb
3: ldr w10, [x9, 0x0014]
tbnz w10, #2, 3b //Wait Children be Alive

add	x10, x9, #(1 << 16)        //SGI_Base 
mov	w11, #~0
str	w11, [x10, 0x0080]
str	wzr, [x10, 0x0d00]	   //SGIs|PPIs Group1NS 
mov	w11, #0x1                  //Enable SGI 0 
str	w11, [x10, 0x0100]

/* Initialize Cpu Interface */
/* rockchip: first check elx for running on different el */
switch_el x0, el3_sre, el2_sre, el1_sre

el3_sre:
mrs x10, S3_6_C12_C12_5
orr x10, x10, #0xf //SRE & Disable IRQ/FIQ Bypass &
//Allow EL2 access to ICC_SRE_EL2
msr S3_6_C12_C12_5, x10
isb

el2_sre:
mrs x10, S3_4_C12_C9_5
orr x10, x10, #0xf //SRE & Disable IRQ/FIQ Bypass &
//Allow EL1 access to ICC_SRE_EL1
msr S3_4_C12_C9_5, x10
isb

el1_sre:
mrs x0, CurrentEL //check currentEL
cmp x0, 0xC
b.ne el1_ctlr //currentEL != EL3

el3_ctlr:
mov x10, #0x3 //EnableGrp1NS | EnableGrp1S
msr S3_6_C12_C12_7, x10
isb

msr	S3_6_C12_C12_4, xzr
isb

el1_ctlr:
mov x10, #0x3 //EnableGrp1NS | EnableGrp1S
msr S3_0_C12_C12_7, x10
isb

msr	S3_0_C12_C12_4, xzr      //NonSecure ICC_CTLR_EL1 
isb

mov	x10, #0xf0               //Non-Secure access to ICC_PMR_EL1 
msr	S3_0_C4_C6_0, x10
isb	

ret

/*******************************************************/
//ref:D:u-boot/arch/arm/cpu/armv8/exceptions.S

/* Enter Exception.

  • This will save the processor state that is ELR/X0~X30 to the stack frame.*/

.macro exception_entry
stp x29, x30, [sp, #-16]!
stp x27, x28, [sp, #-16]!
stp x25, x26, [sp, #-16]!
stp x23, x24, [sp, #-16]!
stp x21, x22, [sp, #-16]!
stp x19, x20, [sp, #-16]!
stp x17, x18, [sp, #-16]!
stp x15, x16, [sp, #-16]!
stp x13, x14, [sp, #-16]!
stp x11, x12, [sp, #-16]!
stp x9, x10, [sp, #-16]!
stp x7, x8, [sp, #-16]!
stp x5, x6, [sp, #-16]!
stp x3, x4, [sp, #-16]!
stp x1, x2, [sp, #-16]!

/* Could be running at EL3/EL2/EL1 */
switch_el x11, 3f, 2f, 1f

3: mrs x1, esr_el3
mrs x2, elr_el3
b 0f
2: mrs x1, esr_el2
mrs x2, elr_el2
b 0f
1: mrs x1, esr_el1
mrs x2, elr_el1
0:
stp x2, x0, [sp, #-16]!
mov x0, sp
.endm

/*

  • Exit Exception.

  • This will restore the processor state that is ELR/X0~X30

  • from the stack frame.
    */
    .macro exception_exit
    ldp x2, x0, [sp],#16

    /* Could be running at EL3/EL2/EL1 */
    switch_el x11, 3f, 2f, 1f
    3: msr elr_el3, x2
    b 0f
    2: msr elr_el2, x2
    b 0f
    1: msr elr_el1, x2
    0:
    ldp x1, x2, [sp],#16
    ldp x3, x4, [sp],#16
    ldp x5, x6, [sp],#16
    ldp x7, x8, [sp],#16
    ldp x9, x10, [sp],#16
    ldp x11, x12, [sp],#16
    ldp x13, x14, [sp],#16
    ldp x15, x16, [sp],#16
    ldp x17, x18, [sp],#16
    ldp x19, x20, [sp],#16
    ldp x21, x22, [sp],#16
    ldp x23, x24, [sp],#16
    ldp x25, x26, [sp],#16
    ldp x27, x28, [sp],#16
    ldp x29, x30, [sp],#16
    eret
    .endm

/* Exception vectors.*/
.align 11
vectors:
.align 7
b _do_bad_sync //Current EL Synchronous Thread

.align	7              
b	_do_bad_irq	        //Current EL IRQ Thread  
                        
.align	7               
b	_do_bad_fiq	        //Current EL FIQ Thread 
                        
.align	7               
b	_do_bad_error	        //Current EL Error Thread 
                       
.align	7               
b	_do_sync	        //Current EL Synchronous Handler 
                        
.align	7               
b	_do_irq		        //Current EL IRQ Handler 
                        
.align	7               
b	_do_fiq		        //Current EL FIQ Handler 
                       
.align	7              
b	_do_error	        //Current EL Error Handler 

_do_bad_sync:
exception_entry
bl halt //do_bad_sync

_do_bad_irq:
exception_entry
bl halt //do_bad_irq

_do_bad_fiq:
exception_entry
bl halt //do_bad_fiq

_do_bad_error:
exception_entry
bl halt //do_bad_error

_do_sync:
exception_entry
bl halt //do_sync

_do_irq:
exception_entry
bl do_irq //do_irq
exception_exit

_do_fiq:
exception_entry
bl halt //do_fiq

_do_error:
exception_entry
bl halt //do_error

/*******************************************************/
relocate:
adr x0, _start
ldr x1, =_start

cmp x0, x1     
b.eq relocate_complete  //No need relocate

ldr x2, =__bss_start    //relocate end addr

cpy:
ldr x3, [x0] //ldr x3, [x0], #8 //ldp x10, x11, [x0], #16 //copy from source address [x0]
add x0, x0, #8

str x3, [x1]            //str x3, [x1], #8  //stp	x10, x11, [x1], #16	//copy to   target address [x1] 

add x1, x1, #8

cmp x1, x2      
b.lo cpy       

b relocate_complete 

/*******************************************************/
clean_bss:
ldr x0, =__bss_start //bss start
ldr x1, =__bss_end //bss end
mov x2, #0

clean_loop:
str x2, [x0]
add x0, x0, #8
cmp x0, x1
b.lo clean_loop

b clean_bss_complete 	  

{% endcodeblock %}

前面的中断初始化完成了,接下来就是注册、使能、执行中断、中断测试几个函数:
{% codeblock lang:c [int.c] https://github.com/hceng/RK3399/tree/master/hardware/3_irq/code/int.c %}
/*************
Function:interrupt
**************/

#include “int.h”
#include “led.h”
#include “timer.h”

void irq_init(void)
{
/* gic has been init in Start.S */
}

void enable_interrupts(void)
{
asm volatile(“msr daifclr, #0x03”);
}

/* irq interrupt install handle */
void irq_install_handler(int irq, interrupt_handler_t *handler, void *data)
{
if (g_irq_handler[irq].m_func != handler)
g_irq_handler[irq].m_func = handler;
}

/* enable irq handler */
int irq_handler_enable(int irq)
{
unsigned long M, N;

if (irq >= NR_GIC_IRQS)
    return -1;

M = irq / 32;
N = irq % 32;

GICD->ISENABLER[M]  = (0x1 << N);

return 0;

}

void do_irq(void)
{
unsigned long nintid;
unsigned long long irqstat;

asm volatile("mrs %0, " __stringify(ICC_IAR1_EL1) : "=r" (irqstat));

nintid = (unsigned long)irqstat & 0x3FF;

/* here we use gic id checking, not include gpio pin irq */
if (nintid < NR_GIC_IRQS)
    g_irq_handler[nintid].m_func((void *)(unsigned long)nintid);

asm volatile("msr " __stringify(ICC_EOIR1_EL1) ", %0" : : "r" ((unsigned long long)nintid));
asm volatile("msr " __stringify(ICC_DIR_EL1) ", %0" : : "r" ((unsigned long long)nintid));
isb();

}

static void board_timer_isr(void)
{
static unsigned char led_flag = 0;

TIMER3->INTSTATUS = 0x01;  //clrear interrupt

if(led_flag == 0)
    led_mode(0);
else
    led_mode(1);

led_flag = !led_flag;

}

void test_timer_irq(void)
{
/* enable exceptions */
enable_interrupts();

/* timer set */
TIMER3->CURRENT_VALUE0 = 0x0FFFFFF;
TIMER3->LOAD_COUNT0    = 0x0FFFFFF;
TIMER3->CONTROL_REG    = 0x05; //auto reload & enable the timer

/* register and enable */
irq_install_handler(TIMER_INTR3, (interrupt_handler_t *)board_timer_isr, (void *)(0));
irq_handler_enable(TIMER_INTR3);

}

static void board_gpio_isr(void)
{
if (GPIO4->INT_STATUS & (0x01 << (3 * 8 + 5))) //Interrupt status
{
GPIO4->PORTA_EOI |= (0x01 << (3 * 8 + 5)); //Clear interrupt
//if ((GPIO4->EXT_PORTA & (0x01<<(3*8+5))) == 0)
led_mode(2);
}
}

//GPIO4_D5
void test_gpio_irq(void)
{
/* enable exceptions */
enable_interrupts();

/* GPIO set */
GPIO4->SWPORTA_DDR   &= ~(0x01 << (3 * 8 + 5)); //should be Input
GPIO4->INTEN         |=  (0x01 << (3 * 8 + 5)); //Interrupt enable
GPIO4->INTMASK       &= ~(0x01 << (3 * 8 + 5)); //Interrupt bits are unmasked
GPIO4->INTTYPE_LEVEL |=  (0x01 << (3 * 8 + 5)); //Edge-sensitive
GPIO4->INT_POLARITY  &= ~(0x01 << (3 * 8 + 5)); //Active-low
GPIO4->DEBOUNCE      |=  (0x01 << (3 * 8 + 5)); //Enable debounce

/* register and enable */
irq_install_handler(GPIO4_INTR, (interrupt_handler_t *)board_gpio_isr, (void *)(0));
irq_handler_enable(GPIO4_INTR);

}
{% endcodeblock %}

在主函数里对重定位的验证可以尝试定义一个全局变量检查是否正常,对清BSS段的验证可以尝试定义一个未初始化的全局变量检查是否正常,对中断的验证可以测试定时器中断是否正常,GPIO中断通过外接按键检测是否正常:
{% codeblock lang:c [main.c] https://github.com/hceng/RK3399/tree/master/hardware/3_irq/code/main.c%}
#include “led.h”

unsigned int test_a = 0x1234; //for test relocate
unsigned int test_b; //for test clean bss

int main(void)
{
led_mode(1); //YELLOW LED

if ((0x1234 != test_a) || (0 != test_b))
{
    led_mode(3);
    led_delay();

    led_mode(0);
    led_delay();
}

test_timer_irq();
test_gpio_irq();


while(1)
{

}

return 0;

}
{% endcodeblock %}

  • 测试效果:
    上电后,黄色LED1间隔闪烁;按下按键,蓝色LED2亮,随后熄灭。

5.串口

串口的移植和前面iMX6ULL上手体验里的移植,思路是差不多,先找到U-Boot的uart相关代码,移植好后为printf提供对应函数即可。

5.1 uart代码

U-Boot里uart相关代码路径:u-boot/drivers/serial/serial_rk.c
可以看到rk_uart_init()是串口初始化,里面依次调用了引脚复用rk_uart_iomux()、串口复位rk_uart_reset()、设置IRDA SIR功能rk_uart_set_iop()、设置串口属性rk_uart_set_lcr()、设置波特率rk_uart_set_baudrate()、设置串口FIFOrk_uart_set_fifo()
初始化完成后,就可以收发数据了,这里只实现了发生数据rk_uart_sendbyte()
移植的过程还是比较简单,寄存器比较少,注意在设置波特率函数里,需要用到除法,为了简便,可先算出来直接赋值。这里的波特率为最大的1.5M。

{% codeblock lang:c [uart.c] https://github.com/hceng/RK3399/tree/master/hardware/4_uart_printf/code/uart.c%}
#include “grf.h”
#include “uart.h”

static void rk_uart_iomux(void)
{
GRF_GPIO4B_IOMUX = (3 << 18) | (3 << 16) | (2 << 2) | (2 << 0);
}

static void rk_uart_reset()
{

/* UART reset, rx fifo & tx fifo reset */
UART2_SRR = (0x01 << 1) | (0x01 << 1) | (0x01 << 2);
led_mode(2);
/* interrupt disable */
UART2_IER = 0x00;

}

static void rk_uart_set_iop(void)
{
UART2_MCR = 0x00;
}

static void rk_uart_set_lcr(void)
{
UART2_LCR &= ~(0x03 << 0);
UART2_LCR |= (0x03 << 0); //8bits

UART2_LCR &= ~(0x01 << 3); //parity disabled

UART2_LCR &= ~(0x01 << 2); //1 stop bit

}

static void rk_uart_set_baudrate(void)
{
volatile unsigned long rate;
unsigned long baudrate = 1500000;

/* uart rate is div for 24M input clock */
//rate = 24000000 / 16 / baudrate;
rate = 1;

UART2_LCR |= (0x01 << 7);

UART2_DLL = (rate & 0xFF);
UART2_DLH = ((rate >> 8) & 0xFF);

UART2_LCR &= ~(0x01 << 7);

}

static void rk_uart_set_fifo(void)
{
/* shadow FIFO enable /
UART2_SFE = 0x01;
/
fifo 2 less than /
UART2_SRT = 0x03;
/
2 char in tx fifo */
UART2_STET = 0x01;
}

void uart_init(void)
{

rk_uart_iomux();
rk_uart_reset();

rk_uart_set_iop();
rk_uart_set_lcr();

rk_uart_set_baudrate();

rk_uart_set_fifo();

}

void rk_uart_sendbyte(unsigned char byte)
{

while((UART2_USR & (0x01 << 1)) == 0);

UART2_THR = byte;

}

void rk_uart_sendstring(char *ptr)
{
while(*ptr)
rk_uart_sendbyte(*ptr++);
}

/* 0xABCDEF12 */
void rk_uart_sendhex(unsigned int val)
{
int i;
unsigned int arr[8];

for (i = 0; i < 8; i++)
{
    arr[i] = val & 0xf;
    val >>= 4;   /* arr[0] = 2, arr[1] = 1, arr[2] = 0xF */
}

/* printf */
rk_uart_sendstring("0x");
for (i = 7; i >= 0; i--)
{
    if (arr[i] >= 0 && arr[i] <= 9)
        rk_uart_sendbyte(arr[i] + '0');
    else if(arr[i] >= 0xA && arr[i] <= 0xF)
        rk_uart_sendbyte(arr[i] - 0xA + 'A');
}

}
{% endcodeblock %}

5.2 printf移植

printf库移植的方法:
1.先在printf.h里,用__out_putchar替换成自己实现的字节发送函数rk_uart_sendbyte
2.然后在printf.c里,为其提供宏va_startva_argva_end_INTSIZEOFva_list

在第二步里,之前ARMv7的可以直接使用,现在使用ARMv8,实测发现打印有问题,找到交叉编译工具里对应宏的位置,直接加入头文件stdarg.h即可。
{% codeblock lang:c [printf.c] https://github.com/hceng/RK3399/tree/master/hardware/4_uart_printf/code/printf.c%}

#include “printf.h”
#include <stdarg.h>

/************************************************************************************************/
#if 0
typedef char *va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//#define va_arg(ap,t) ( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_arg(ap,t) ( (t )( ap=ap + _INTSIZEOF(t), ap- _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
#endif
/
********************************************************************************************/

unsigned char hex_tab[] = {‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’,
‘8’, ‘9’, ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’
};

static int outc(int c)
{
__out_putchar©;
return 0;
}

static int outs (const char *s)
{
while (*s != ‘\0’)
__out_putchar(*s++);
return 0;
}

static int out_num(long n, int base, char lead, int maxwidth)
{
unsigned long m = 0;
char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
int count = 0, i = 0;

*--s = '\0';

if (n < 0)
    m = -n;
else
    m = n;

do
{
    *--s = hex_tab[m % base];
    count++;
}
while ((m /= base) != 0);

if( maxwidth && count < maxwidth)
{
    for (i = maxwidth - count; i; i--)
        *--s = lead;
}

if (n < 0)
    *--s = '-';

return outs(s);

}

/*ref: int vprintf(const char *format, va_list ap); */
static int my_vprintf(const char *fmt, va_list ap)
{
char lead = ’ ';
int maxwidth = 0;

for(; *fmt != '\0'; fmt++)
{
    if (*fmt != '%')
    {
        outc(*fmt);
        continue;
    }
    lead = ' ';
    maxwidth = 0;

    //format : %08d, %8d,%d,%u,%x,%f,%c,%s
    fmt++;
    if(*fmt == '0')
    {
        lead = '0';
        fmt++;
    }

    while(*fmt >= '0' && *fmt <= '9')
    {
        maxwidth *= 10;
        maxwidth += (*fmt - '0');
        fmt++;
    }

    switch (*fmt)
    {
    case 'd':
        out_num(va_arg(ap, int),          10, lead, maxwidth);
        break;
    case 'o':
        out_num(va_arg(ap, unsigned int),  8, lead, maxwidth);
        break;
    case 'u':
        out_num(va_arg(ap, unsigned int), 10, lead, maxwidth);
        break;
    case 'x':
        out_num(va_arg(ap, unsigned int), 16, lead, maxwidth);
        break;
    case 'c':
        outc(va_arg(ap, int   ));
        break;
    case 's':
        outs(va_arg(ap, char *));
        break;

    default:
        outc(*fmt);
        break;
    }
}
return 0;

}

//ref: int printf(const char *format, …);
int printf(const char *fmt, …)
{
va_list ap;

va_start(ap, fmt);
my_vprintf(fmt, ap);
va_end(ap);
return 0;

}

int printf_test(void)
{
printf("=This is printf test=\n");
printf(“test char = %c,%c\n”, ‘H’, ‘c’);
printf(“test decimal1 number = %d\n”, 123456);
printf(“test decimal2 number = %d\n”, -123456);
printf(“test hex1 number = 0x%x\n”, 0x123456);
printf(“test hex2 number = 0x%08x\n”, 0x123456);
printf(“test string = %s\n”, “www.hceng.cn”);

return 0;

}

void puts(char *ptr)
{
while(*ptr)
rk_uart_sendbyte(*ptr++);
}
{% endcodeblock %}

  • 测试效果:

6.定时器

RK3399有12个通用定时器(timer0timer11)、12个安全定时器(stimer0stimer11)、2个PMU定时器(pmutimer0~pmutimer1)。
定时器部分比较简单,很多东西都是固定的,比如定时器的时钟来源都是24MHz的晶振,也就是定时器周期为1/24us。此外定时器的计数只能由小向大增加。
定时器支持两种模式:自由运行模式和用户自定义模式,其实就是前者计数达到设定值后,自动装载计数循环,后者需要手动重新装载,实现循环。

6.1 编程思路

这里希望通过用定时器实现一个比较准确的延时函数,包括us、ms、s的延时。
1.首先设置CONTROLREG,关闭定时器、设置为用户定义计数模式(用户确定循环次数)、中断屏蔽(不需要中断处理函数);
2.向LOAD_COUNT0LOAD_COUNT1放入计数结束值,向LOAD_COUNT2LOAD_COUNT3放入计数初始值,默认为0;
3.设置CONTROLREG,开启定时器,计数器开始运行;
4.读取中断状态INTSTATUS判断时候完成计数,清中断,本次计数完成;

6.2 实现代码

{% codeblock lang:c [timer.c] https://github.com/hceng/RK3399/tree/master/hardware/5_timer_delay/code/timer.c %}

#include “timer.h”

//timer4 is used for delay.
void delay_us(volatile unsigned long int i)
{
unsigned long int count_value = 24 * i; //24MHz; period=(1/24000000)*1000000=1/24us

TIMER4->CONTROL_REG &= ~(0x01 << 0);     //Timer disable
TIMER4->CONTROL_REG |=  (0x01 << 1);     //Timer mode:user-defined count mode
TIMER4->CONTROL_REG &= ~(0x01 << 2);     //Timer interrupt mask

TIMER4->LOAD_COUNT0 = count_value & 0xFFFFFFFF; //load_count_low bits
TIMER4->LOAD_COUNT1 = (count_value >> 32);      //load_count_high bits

TIMER4->CONTROL_REG |=  (0x01 << 0);     //Timer enable

while(!(TIMER4->INTSTATUS & (0x01 << 0)));
TIMER4->INTSTATUS |= (0x01 << 0);        //Write 1 clear the interrupt

TIMER4->CONTROL_REG &= ~(0x01 << 0);     //Timer enable disable

}

void delay_ms(volatile unsigned long int i)
{
for(; i > 0; i–)
delay_us(1000);
}

void delay_s(volatile unsigned long int i)
{
for(; i > 0; i–)
delay_ms(1000);
}
{% endcodeblock %}

7.ADC

RK3399有两类ADC:

  • TS-ADC(Temperature Sensor):
      内嵌的两路ADC,一路检测CPU温度,一路检测GPU温度;
      ADC精度10bit,时钟频率必须低于800KHZ;
      测量范围为-40℃~125℃,精度只有5℃;
      支持用户自定义和自动模式(前者用户自己控制,后者控制器自动查询);
  • SAR-ADC(Successive Approximation Register):
      六路ADC,精度10bit;
      时钟频率必须小于13MHZ;

7.1 编程思路

这里希望通过用SAR-ADC获取外部ADC值,通过TS-ADC获取内部CPU/GPU温度。

  • SAR-ADC
    1.首先设置SARADC_CTRL[3],关闭ADC;
    2.设置SARADC_CTRL[2:0],选择ADC通道;
    3.设置SARADC_CTRL[3],启动ADC转换;
    4.读取ADC状态SARADC_STAS判断是否转换完成;
    5.读取ADC数据SARADC_DATA

  • TS-ADC(User-Define Mode)
    1.首先设置TSADC_AUTO_CON为用户定义模式、ADC值与温度值负关系;
    2.设置TSADC_USER_CON选择通道、复位、转换开始;
    3.设置TSADC_INT_EN,使能ADC完成中断;
    4.读取ADC中断状态TSADC_INT_PD判断是否转换完成,并清理;
    5.根据选择的通道,从对应的TSADC_DATA0TSADC_DATA1读取ADC数据;

这里的TS-ADC得到的数值和温度并不是完全的线性关系,根据提供的表格,可以计算出一个大致的线性关系:y = 0.5823x - 273.62

7.2 实现代码

{% codeblock lang:c [timer.c] https://github.com/hceng/RK3399/tree/master/hardware/6_adc/code/adc.c %}

#include “uart.h”
#include “printf.h”
#include “timer.h”
#include “int.h”
#include “adc.h”

unsigned int get_saradc_val(unsigned char channel)
{
unsigned int val;

//delay between power up and start command
//SARADC_DLY_PU_SOC = 8; //DLY_PU_SOC + 2

SARADC_CTRL &= ~(0x01 << 3); //ADC power down control bit

SARADC_CTRL |= (channel << 0); //ADC input source selection

//SARADC_CTRL |= (0x01<<3); //Interrupt enable.

SARADC_CTRL |=  (0x01 << 3); //ADC power up and reset
delay_us(100); //不能立即就判断状态

while(SARADC_STAS & 0x01); //The status register of A/D Converter 1’b0: ADC stop

val = SARADC_DATA & 0x3FF; //A/D value of the last conversion (DOUT[9:0]).

return val;

}

//channel0: CPU temperature
//channel1: GPU temperature
int get_tsadc_temp(unsigned char channel)
{
int val;

if ((channel != 0) && (channel != 1))
{
    printf("get_tsadc_temp set channel error.\n");
    return -255;
}

//User-Define Mode
TSADC_AUTO_CON &= ~(0x01 << 0);     //TSADC controller works at user-define mode
TSADC_AUTO_CON |=  (0x01 << 1);     //RK3399 is negative temprature coefficient

TSADC_USER_CON &= ~(0x07 << 0);     //clear
TSADC_USER_CON |=  (channel << 0);  //PD_DVDD and ADC input source selection
TSADC_USER_CON |=  (0x01 << 3);     //CHSEL_DVDD and ADC power up and reset

TSADC_USER_CON |=  (0x01 << 4);     //the start_of_conversion will be controlled by TSADC_USER_CON[5].
TSADC_USER_CON |=  (0x01 << 5);     //start conversion

TSADC_INT_EN   |=  (0x01 << 16);    //eoc_interrupt enable in user defined mode
while(!(TSADC_INT_PD & (0x01 << 16))); //wait ADC conversion stop
TSADC_INT_PD &= ~(0x01 << 16);

if (0 == channel)
    val = (int)(0.5823 * (float)(TSADC_DATA0) - 273.62); //y = 0.5823x - 273.62
else
    val = (int)(0.5823 * (float)(TSADC_DATA1) - 273.62); //y = 0.5823x - 273.62

printf("get_tsadc_temp = %d \n", val);

return val;

}
{% endcodeblock %}

  • 测试效果:

8.I2C

RK3399拥有8个I2C,其功能和其它SOC的I2C差不多,这里通过I2C读写EEPROM,具体的操作和前面博客AM437x——I2C裸机差不多,也实现了两个版本:GPIO模拟和寄存器控制,这里主要介绍寄存器控制版本。

8.1 编程思路

0.首先是I2C引脚复用、设置SCK时钟、注册/使能中断(非必须)等;

  • 写EEPROM
    1.清空控制寄存器CON并使能;
    2.设置I2C模式transmit only
    3.设置CON启动开始信号,并读取IPD等待开始信号发送完成;
    4.设置TXDATA0实现从机地址、写地址、数据的设定,设置传输数据个数,等待传输完成;
    5.设置CON发送结束信号,并读取IPD等待结束信号发送完成;

  • 读EEPROM
    1.清空控制寄存器CON并使能;
    2.设置I2C模式transmit only + restart + transmit address + receive only
    3.设置MRXADDR设定从机地址,设置MRXRADDR设定从机寄存器地址;
    4.设置TXDATA0实现从机地址、写地址、数据的设定,设置传输数据个数,等待传输完成;
    5.设置MRXCNT值接收一个数据,读取IPD等待接收数据完成;
    6.设置CON发送结束信号,并读取IPD等待结束信号发送完成;
    7.读取RXDATA0获得数据;

8.2 实现代码

{% codeblock lang:c [i2c.c] https://github.com/hceng/RK3399/tree/master/hardware/7_I2C/I2C控制器/code/i2c.c %}
#include “i2c.h”
#include “uart.h”
#include “printf.h”
#include “timer.h”

//GPIO1_B3/I2C4_SDA
//GPIO1_B4/I2C4_SCL

void i2c_init(void)
{
//1.GPIO1_B3/I2C4_SDA、GPIO1_B4/I2C4_SCL设置为功能引脚,注意高位要先置为1才能写;
PMUGRF_GPIO1B_IOMUX |= ((0xFFFF0000 << 0) | (0x01 << 6) | (0x01 << 8));

//2.设置SCK时钟
//3.注册/使能中断

}

void eeprom_write(unsigned char addr, unsigned char data)
{
//0.清空控制寄存器并使能
I2C4->CON &= ~(0x7F << 0);
I2C4->IPD &= ~(0x7F << 0);
I2C4->CON |= 0x01 << 0; //使能

//1.设置模式:transmit only
I2C4->CON &= ~(0x03 << 1);

//2.开始信号
I2C4->CON |= 0x01 << 3; //开始信号
while(!(I2C4->IPD & (0x01 << 4))); //等待开始信号发完
I2C4->IPD |=  (0x01 << 4); //清开始信号标志

//3.I2C从机地址+写地址+数据 (3个字节)
I2C4->TXDATA0 = 0xA0 | (addr << 8) | (data << 16);
I2C4->MTXCNT = 3;
while(!(I2C4->IPD & (0x01 << 2))); //MTXCNT data transmit finished interrupt pending bit
I2C4->IPD |=  (0x01 << 2);

//4.结束信号
I2C4->CON &= ~(0x01 << 3); //手动清除start(注意:前面的开始信号控制位理论会自动清0,实测没有,这里必须手动清,否则是开始信号)
I2C4->CON |= (0x01 << 4);
while(!(I2C4->IPD & (0x01 << 5)));
I2C4->IPD |=  (0x01 << 5);

}

//自动发送从机地址和从机寄存器地址
unsigned char eeprom_read(unsigned char addr)
{
unsigned char data = 0;

//0.清空控制寄存器并使能
I2C4->CON &= ~(0x7F << 0);
I2C4->IPD &= ~(0x7F << 0);
I2C4->CON |= 0x01 << 0; //使能

//必须收到ack,否则停止传输(非必需)
//I2C4->CON |=  (0x01<<6); //stop transaction when NAK handshake is received

//1.设置模式:transmit address (device + register address) --> restart --> transmit address –> receive only
I2C4->CON |=  (0x01 << 1); //自动发送从机地址和从机寄存器地址

//2.从机地址
I2C4->MRXADDR = (0xA0 | (1 << 24));

//3.从机寄存器地址
I2C4->MRXRADDR = (addr | (1 << 24)); //地址只有6位,超过6位怎么办?

//4.开始信号
I2C4->CON |=  (0x01 << 3);
while(!(I2C4->IPD & (0x01 << 4)));
I2C4->IPD |=  (0x01 << 4);

//5.接收一个数据且不响应
I2C4->CON |= (0x01 << 5);
I2C4->MRXCNT = 1;
while(!(I2C4->IPD & (0x01 << 3)));
I2C4->IPD |=  (0x01 << 3);

//6.结束信号
I2C4->CON &= ~(0x01 << 3); //手动清除start
I2C4->CON |= (0x01 << 4);
while(!(I2C4->IPD & (0x01 << 5)));
I2C4->IPD |=  (0x01 << 5);

return (I2C4->RXDATA0 & 0xFF);

}
{% endcodeblock %}

主函数里先向EEPROM写数据,再读出数据并打印出来,是否是预期的值。
{% codeblock lang:c %}
i2c_init();

//write eeprom.
for(i=0; i<5; i++)
{
    eeprom_write(i,2*i);
    delay_ms(4);//Must be delayed more than 4ms.
}

printf("write eeprom ok\n\r");
delay_ms(10);

//read eeprom.
for(i=0; i<5; i++)
{
    printf("read_data%d = %d\n\r", i, eeprom_read(i));
    delay_ms(4);
}

{% endcodeblock %}

  • 测试效果:

9.SPI

RK3399有6组SPI,协议也是标准的,没什么好说的。通过SPI读取Flash,也实现了两个版本:GPIO模拟和寄存器控制,这里主要介绍寄存器控制版本。

9.1 编程思路

0.首先是SPI引脚复用、设置时钟、SPI模式(SCPH=1,SCPOL=1)等;
1.实现发送一字节函数:使能SPI、向TXDR[0]写入待发送的数据、根据SR等待发送完成及空闲、关闭SPI;
2.实现接收一字节函数:使能SPI、向TXDR[0]写入空数据、根据SR等待接收完成及空闲、读出RXDR[0]数据、关闭SPI;
3.实现片选函数;
4.剩下的就是SPI Flash(W25Q16DV)相关的操作,比如发送哪个指令读取ID,发送哪个指令擦除数据等,参考具体的Flash芯片手册;

值得注意的几点有:
1.SPI Flash(W25Q16DV)每次写操作某个分区前都得先擦除该分区;
2.注意片选的连续性,比如写使能指令(0x06)和写状态寄存器指令(0x01)之间的片选不能中断;

9.2 实现代码

{% codeblock lang:c [spi.c] https://github.com/hceng/RK3399/tree/master/hardware/8_SPI/SPI控制器/code/spi.c %}
#include “spi.h”
#include “uart.h”
#include “printf.h”
#include “timer.h”
#include “gpio.h”
#include “grf.h”

void spi_init(void)
{
//SPI1_CSn0/GPIO1_B2_U
//SPI1_CLK/GPIO1_B1_U
//SPI1_TXD/GPIO1_B0_U
//SPI1_RXD/GPIO1_A7_U

//1.IOMUX
PMUGRF_GPIO1A_IOMUX = 0xFFFF8000;
PMUGRF_GPIO1B_IOMUX = 0xFFFF002A;

SPI1->ENR &=  ~(0x01 << 0); //关闭SPI

//2.Clock Ratios   master mode:Fspi_clk>= 2 × (maximum Fsclk_out)
//CRU_CLKGATE2_CON &= ~(0x01<<9);  //默认SPI1 source clock开启
//CRU_CLKGATE6_CON &= ~(0x01<<4);  //默认SPI1 APB clock开启
SPI1->BAUDR = 24; //Fsclk_out = 48/24= 2M   48 >= 2x2

//3.注册/使能中断(本程序未使用,用的查询)
//register_irq(IRQ_SPI1, spi_irq_isr);
//irq_handler_enable(IRQ_SPI1);
//SPI1->IPR &= ~(0x01<<4); //Active Interrupt Polarity Level is HIGH(default)
//SPI1->IMR |=  ((0x01<<4) | (0x01<<3) | (0x01<<2) | (0x01<<1) | (0x01<<0)); //Interrupt Mask

//4.DMA(可以不用)
//SPI1->DMACR |= ((0x01<<1) | (0x01<<0)); // Transmit/Receive DMA enabled
//SPI1->DMATDLR = 1; //?
//SPI1->DMARDLR = 1; //?

//5.SPI模式
//[1:0]Data Frame Size:8bit data
//[5:2]Control Frame Size:8-bit serial data transfer
//[6]SCPH:Serial clock toggles at start of first data bit
//[7]SCPOL:Inactive state of serial clock is high
//[13]BHT:apb 8bit write/read, spi 8bit write/read
//[19:18]XFM(Transfer Mode):Transmit & Receive(default)
//[20]OPM(Operation Mode):Master Mode(default)

SPI1->CTRLR0 &= ~(0x03 << 0) ;
SPI1->CTRLR0 |= ((0x01 << 0) | (0x07 << 2) | (0x01 << 6) | (0x01 << 7) | (0x01 << 13)); //设置SPI模式

}

void spi_send_byte(unsigned char val)
{
SPI1->ENR |= (0x01 << 0); //SPI Enable

SPI1->TXDR[0] = val & 0xFFFF;
while(!(SPI1->SR & (0x01 << 2))); //Transmit FIFO is empty
while(SPI1->SR & (0x01 << 0));  //SPI is idle or disabled

SPI1->ENR &=  ~(0x01 << 0);     //SPI Disable

}

static unsigned char spi_recv_byte(void)
{
unsigned char val = 0;

SPI1->ENR |=  (0x01 << 0);    //SPI Enable

SPI1->TXDR[0] = 0;            //因为是发送接收模式,FIFO在发送时也会接收数据,这里发送空数据,就可读取数据

while(SPI1->SR & (0x01 << 3)); //SReceive FIFO is not empty
while(SPI1->SR & (0x01 << 0)); //SPI is idle or disabled

val = SPI1->RXDR[0] & 0xFF;  //读数据

SPI1->ENR &=  ~(0x01 << 0);  //SPI Disable,为了清空FIFO

return val;

}

void spi_flash_set_cs(unsigned char flag)
{
if(!flag)
SPI1->SER |= (0x01 << 0);
else
SPI1->SER &= ~(0x01 << 0);
}

/* 通用部分 */
static void spi_flash_send_addr(unsigned int addr)
{
spi_send_byte(addr >> 16);
spi_send_byte(addr >> 8);
spi_send_byte(addr & 0xff);
}

static void spi_flash_write_enable(int enable)
{
if (enable)
{
spi_flash_set_cs(0);
spi_send_byte(0x06);
spi_flash_set_cs(1);
}
else
{
spi_flash_set_cs(0);
spi_send_byte(0x04);
spi_flash_set_cs(1);
}

}

static unsigned char spi_flash_read_status_reg1(void)
{
unsigned char val;

spi_flash_set_cs(0);

spi_send_byte(0x05);
val = spi_recv_byte();

spi_flash_set_cs(1);

return val;

}

static unsigned char spi_flash_read_status_reg2(void)
{
unsigned char val;

spi_flash_set_cs(0);

spi_send_byte(0x35);
val = spi_recv_byte();

spi_flash_set_cs(1);

return val;

}

static void spi_flash_wait_when_busy(void)
{
while (spi_flash_read_status_reg1() & 1);
}

static void spi_flash_write_status_reg(unsigned char reg1, unsigned char reg2)
{
spi_flash_write_enable(1);

spi_flash_set_cs(0);

spi_send_byte(0x01);
spi_send_byte(reg1);
spi_send_byte(reg2);

spi_flash_set_cs(1);

spi_flash_wait_when_busy();

}

static void spi_flash_clear_protect_for_status_reg(void)
{
unsigned char reg1, reg2;

reg1 = spi_flash_read_status_reg1();
reg2 = spi_flash_read_status_reg2();

reg1 &= ~(1 << 7);
reg2 &= ~(1 << 0);

spi_flash_write_status_reg(reg1, reg2);

}

static void spi_flash_clear_protect_for_data(void)
{
/* cmp=0,bp2,1,0=0b000 */
unsigned char reg1, reg2;

reg1 = spi_flash_read_status_reg1();
reg2 = spi_flash_read_status_reg2();

reg1 &= ~(7 << 2);
reg2 &= ~(1 << 6);

spi_flash_write_status_reg(reg1, reg2);

}

/* erase 4K */
void spi_flash_erase_sector(unsigned int addr)
{
spi_flash_write_enable(1);

spi_flash_set_cs(0);

spi_send_byte(0x20);
spi_flash_send_addr(addr);

spi_flash_set_cs(1);

spi_flash_wait_when_busy();

}

/* program */
void spi_flash_program(unsigned int addr, unsigned char *buf, int len)
{
int i;

spi_flash_write_enable(1);

spi_flash_set_cs(0);

spi_send_byte(0x02);
spi_flash_send_addr(addr);

for (i = 0; i < len; i++)
    spi_send_byte(buf[i]);

spi_flash_set_cs(1);

spi_flash_wait_when_busy();

}

void spi_flash_read(unsigned int addr, unsigned char *buf, int len)
{
int i;

spi_flash_set_cs(0);

spi_send_byte(0x03);
spi_flash_send_addr(addr);
for (i = 0; i < len; i++)
    buf[i] = spi_recv_byte();

spi_flash_set_cs(1);

}

void spi_flash_init(void)
{
spi_flash_clear_protect_for_status_reg();
spi_flash_clear_protect_for_data();
}

void spi_flash_read_ID(unsigned int *pMID, unsigned int *pDID)
{

spi_flash_set_cs(0);

spi_send_byte(0x90);

spi_flash_send_addr(0);

*pMID = spi_recv_byte();
*pDID = spi_recv_byte();

spi_flash_set_cs(1);

}
{% endcodeblock %}

在主函数里先读取Flash的MID和PID,然后初始化Flash(去除写状态寄存器保护和写数据保护),再写入数据,读出数据检测是否一致。
{% codeblock lang:c %}
spi_init();

spi_flash_read_ID(&mid, &pid);
printf("SPI Flash : MID = 0x%02x, PID = 0x%02x\n\r", mid, pid);	

spi_flash_init();
while(1)
{
    spi_flash_erase_sector(4096);
    spi_flash_program(4096, "hceng", 7);
    spi_flash_read(4096, str, 7);
    printf("SPI Flash read from 4096: %s\n\r", str);
    
    delay_s(2);
}

{% endcodeblock %}

  • 测试效果:

10.PWM

RK3399有四组PWM,其中PWM3可以配置为第二功能用于红外IR。
这里使用MIPI接口的PWM1为例,输出指定占空比的波形。

10.1 编程思路

1.首先是PWM引脚复用;
2.关闭PWM控制器;
3.设置时钟源、比例系数、预分频(可默认);
4.设置模式:连续模式,占空比极性为高、左对齐(默认);
5.设置占空比,其中PERIOD_HPR是总周期,DUTY_LPR是有效周期,Duty cycle=DUTY_LPR/PERIOD_HPR
6.启动PWN控制器;

10.2 实现代码

{% codeblock lang:c [pwm.c] https://github.com/hceng/RK3399/tree/master/hardware/9_pwm/code/pwm.c %}
#include “pwm.h”
#include “gpio.h”
#include “timer.h”

//GPIO4_C6/PWM1
//0~100
void pwm1_out(unsigned char val)
{
#if 0 //test gpio ok
GPIO4->SWPORTA_DDR |= (0x01 << (2 * 8 + 6));

while(1)
{
	GPIO4->SWPORTA_DR  &= ~(0x01 << (2 * 8 + 6));
	delay_ms(10);
	GPIO4->SWPORTA_DR  |=  (0x01 << (2 * 8 + 6));
	delay_ms(10);	
}

#else
//1.IOMUX
GRF_GPIO4C_IOMUX |= ((0x03<<28) | (0x01<<12)); // 1’b1: pwm_1
//GRF_SOC_CON2 |= ((0x01<<16) | (0x01<<0)); //P136 1’b1: pwm_1

//2.Set PWM
PWM1->CTRL &= ~(0x01<<0); //PWM channel disabled
//2.1 设置时钟源、比例系数、预分频、
//PWM1->CTRL |= ((0x01<<16) | (0x01<<12) | (0x01<<9));//Scale Factor / Prescale Factor / Clock Source Select
//2.2 设置模式:连续模式,占空比极性为高、左对齐(默认)
PWM1->CTRL |=  (0x01<<1) | (0x01<<3); //PWM Operation Mode 01: Continuous mode.

//3.Set Duty Cycle
if(val > 100)
	val = 100;
PWM1->PERIOD_HPR = 100; //总周期
PWM1->DUTY_LPR   = val;  //占空比=val/总周期

//4.Start
PWM1->CTRL |=  (0x01<<0);

#endif
}

{% endcodeblock %}

11.LCD

待填坑……

参考资料:
[1]. Rockchip RK3399TRM V1.3 Part1.pdf
  Rockchip RK3399TRM V1.3 Part2.pdf
[2]. ARMv8-A_Architecture_ReferenceManual(Issue_A.a).pdf
[3]. u-boot源码阅读(二)
[4]. u-boot启动流程分析(1)_平台相关部分
[5]. ARMv8 与Linux的新手笔记

猜你喜欢

转载自blog.csdn.net/hceng_linux/article/details/89913950