FPGA - Zynq - 加载 - FSBL源码解析1

  • 前文回顾
  • FSBL的数据段和代码段如何链接
    • 建个Example工程,不要光顾着看,自己动动手掌握的更快。
    • 查看链接文件,原来存储空间是这样有条不紊的被分配
  • ARM要开始运行FSBL了,然而并不是main()
  • 终于要开始进入main()了,激动不?
  • 小节

前文回顾


最近想要同步CSDN和微信公众号的内容,各位看客们可以两边都关注一下,方便获取最新的信息。请扫描下面的的二维码添加关注,谢谢支持。

上一篇博客介绍了BootROM是如果搜索并利用BootROM Header来加载FSBL到OCM(on chip memory),最后将cpu的控制权交给FSBL。那么后续关于加载的研究就来到了FSBL的源码解析上,可能一篇博客写不完,会分成好几个小节来全面解析。 敬请期待,接下来话不多说,让我们把注意力集中到FSBL本身。

FSBL的数据段和代码段如何链接

搞懂数据段和代码段是如何被链接成一个二进制文件的,这应该是每一个ARM程序员必须搞清楚的一个事情。它会帮助程序员更加透彻的知道ARM是怎么被安排去工作的,所以数据段你和代码段如何链接在一起,是我们搞懂FSBL的第一步。

建个Example工程,不要光顾着看,自己动动手掌握的更快。

要回答这个问题其实必须要建一个工程,相关的软件操作流程可以参考各种开发板的实验手册,我这里见得描述一下:

  1. 现在VIVADO里面新建一个PL工程,可以自己搭,也可以用范例,本小节所涉及的PL来自范例,如下所示,整个PL实际上由:

    1.1 ARM部分(硬核+外设),如图中所示的processing_system,其中就包含了除APU以外,还有DDR,以及FIXED_IO。 DDR好理解,就是连接外部DDR存储器呗,那这个FIXED_IO是个啥呢?这个实际上就是arm的外设,包含了Q-spi的必要引脚,也包括了Debug Info所需的串口。总而言之都是ARM的外设

    1.2 复位部分,看名字就很好理解,该模块专门用于所以Zynq的PL部分部件的复位

    1.3 AXI Interconnect,这个模块非常重要,简单地说这就是一个总线解析器,一主(一个master AXI4)多从(两个slave AXI4)。我们之前提到过,AXI4将会用于连接Zynq的PS(ARM部分)和PL(FPGA部分),这里就是一个例子,后面每一个Slave AXI4都连着一个 Xilinx 的IP,或者是用户自定义的具备AXI4的IP。这样就简单了,只要用户定义的IP包含AXI4接口,同时将必要的可读或者可写数据映射到这个AXI4接口上,那么Zynq的ARM就能够通过总线接触到这些映射到总线上的数据,it means the ARM could read/write its content mapped on the Bus of AXI4.

    1.4 AXI GPIO & AXI BRAM Controller, 这两个就是上述的Xilinx的IP,自带有AXI4总线接口,这样ARM就能够通过总线解析器控制他们
    1.5 实际的应用,其实也不会比这个在复杂太多,只是再加一些自定义的IP

  2. 利用这个范例,我们进一步建立BSP,然后基于BSP建立APP(用户程序),以及FSBL(范例,Zynq的加载程序),如下图所示,其包含了app, bsp, platform, fsbl。通过任何一个开发版的用户手册都可以获得完整的工程建立流程,这里不再赘述。

  3. 其中bsp和fsbl里面,包含加载过程中所用到的所有源码,下面一一解析。

查看链接文件,原来存储空间是这样有条不紊的被分配

点击FSBL->src->lscript.ld,界面上将会呈现(这里的SDK是2017.2版本):
感谢这个SDK的开发工具,使得用户能够以图表的方式去查看数据段和代码段的具体分布(以前都是通过直接看源码,毕竟科技进步了~),不过老程序员可能更喜欢看源码,那我们就结合的看吧

这个图主要呈现了三部分内容:

  1. 定义了两个存储空间,包括offset和length,其源码表达如下
MEMORY
{ps7_ram_0_S_AXI_BASEADDR : ORIGIN = 0x00000000, LENGTH = 0x00030000ps7_ram_1_S_AXI_BASEADDR : ORIGIN = 0xFFFF0000, LENGTH = 0x0000FE00
}
  1. 接下来定义了堆栈的大小,忘了啥是堆栈的可以自行百度复习一下
_STACK_SIZE = DEFINED(_STACK_SIZE) ? _STACK_SIZE : 0x6000;
_HEAP_SIZE = DEFINED(_HEAP_SIZE) ? _HEAP_SIZE : 0x2000;
  1. 接下来就是将FSBL编译完成后的所有数据和代码,按照一定的顺序链接生成二进制文件,举个例子:
ENTRY(_vector_table)SECTIONS
{.text : {*(.vectors)*(.boot)*(.text)*(.text.*)*(.gnu.linkonce.t.*)*(.plt)*(.gnu_warning)*(.gcc_execpt_table)*(.glue_7)*(.glue_7t)*(.vfp11_veneer)*(.ARM.extab)*(.gnu.linkonce.armextab.*)
} > ps7_ram_0_S_AXI_BASEADDR

上面的源码的作用是:
(1)定义FSBL的程序入口在== _vector_table ==
(2)将代码段(.text*)链接到ps7_ram_0_S_AXI_BASEADDR的最前头,而这里的代码段实际包含了.vector等等内容,我们查看一下.vectors到底是个啥吧,搜索一下把,结果就在bsp的asm_vectors.S(汇编文件里面)

进到这个汇编程序,如下所示:

#include "xil_errata.h"#define __ARM_NEON__ 1.org 0
.text.globl _vector_table.section .vectors
_vector_table:B _bootB  UndefinedB  SVCHandlerB PrefetchAbortHandlerB   DataAbortHandlerNOP /* Placeholder for address exception vector*/B  IRQHandlerB FIQHandler

这里先关注两个名字,一个就是==.vectors==,另一个就是==_vector_table==
看下面的源码可知,.vectors就是一个.section,相当于下面所有的汇编源码取了一个别名,叫做.vectors,这些源码最终被放置到了上述位置!

第二个需要关注的是_vector_table,其实际上就是全局变量(看下面的源码.globl _vector_table ),这个全局变量在这里就是一个指针,指向了 B _boot 这个操作。

同时回过头看上面的源码ENTRY(_vector_table),这就是定义了FSBL的程序入口,也就是cpu执行的第一条指令保存在 _vector_table -----> B _boot

这里可以简单的小结一下, FSBL执行的第一条指令就是B _boot,这是通过查看(编写)FSBL->src->lscript.ld才获悉的,可想而知这个链接文件有多重要,后期等我们更加熟悉,可以尝试一下取修改它,这里做个记号,继续往下走!

ARM要开始运行FSBL了,然而并不是main()

上面已经提及实际FSBL程序最先被执行的语句是B _boot,这是一条汇编指令,意思就是说跳转到 _boot程序块,同时转跳指令B是无需返回的,所以后续B Undefined啥的实际上并不会被执行,看一下**_boot**是什么:

#if XPAR_CPU_ID==0/* only allow cpu0 through */mrc p15,0,r1,c0,c0,5and r1, r1, #0xfcmp r1, #0beq   CheckEFUSEEndlessLoop0:wfeb EndlessLoop0CheckEFUSE:ldr r0,=EFUSEStausldr r1,[r0]                             /* Read eFuse setting */ands r1,r1,#0x80                        /* Check whether device is having single core */beq OKToRun
......
......
......
......
......
......b _start              /* jump to C startup code */and r0, r0, r0          /* no op */.Ldone:  b   .Ldone              /* Paranoia: we should never get here */

汇编语言不是笔者的强项,因此只能大概说明一下(有兴趣的可以自己逐条查看作用,过程会比较痛苦。方式能收获更多CPU底层的细节,这里不展开):

  1. 针对CPU0和CPU1有不同的程序,基本就是CPU0干活,CPU1就是WFE
  2. CPU干的活就是初始化MMU和TLB等等,其中比较关键的就是初始化堆栈(SP寄存器指向栈顶),前面也提及过,在链接的时候分配了堆栈空间,而堆栈对C语言函数是非常重要的。的作用:一般来说函数调用或者中断,都会涉及到现场保护和现场恢复,被保护的现场实际上就是CPU的几个专用的reg,以及正在执行的函数的局部变量等数据,这些数据会被推进栈内,其相应的SP寄存器也会加上入栈数据的长度,在函数执行返回挥着中断返回时,栈内的数据按顺序再次出来,总体来说就是先进后出。而的作用一般就是给系统动态分配存储空间的,包括用户经常调用的malloc说分配的空间,就是在堆里。简而言之,堆栈的完成初始化是为了c语言函数提供了环境。否则C语言是无法正确被执行的。
  3. 完成上面一系列的功能后,开始一执行去第一条C语言函数**_start**,见下面的汇编代码
 .globl  _start
_start:bl      __cpu_init       /* Initialize the CPU first (BSP provides this) */mov   r0, #0/* clear sbss */ldr   r1,.Lsbss_start     /* calculate beginning of the SBSS */ldr    r2,.Lsbss_end       /* calculate end of the SBSS */.Lloop_sbss:cmp  r1,r2bge    .Lenclsbss      /* If no SBSS, no clearing required */str   r0, [r1], #4b   .Lloop_sbss.Lenclsbss:/* clear bss */ldr    r1,.Lbss_start      /* calculate beginning of the BSS */ldr r2,.Lbss_end        /* calculate end of the BSS */.Lloop_bss:cmp    r1,r2bge    .Lenclbss       /* If no BSS, no clearing required */str    r0, [r1], #4b   .Lloop_bss.Lenclbss:/* set stack pointer */ldr  r13,.Lstack     /* stack address *//* Reset and start Global Timer */mov    r0, #0x0mov r1, #0x0#if USE_AMP != 1bl XTime_SetTime
#endif#ifdef PROFILING          /* defined in Makefile *//* Setup profiling stuff */bl  _profile_init
#endif /* PROFILING *//* run global constructors */bl __libc_init_array/* make sure argc and argv are valid */mov   r0, #0mov   r1, #0/* Let her rip */bl   main/* Cleanup global constructors */bl __libc_fini_array#ifdef PROFILING/* Cleanup profiling stuff */bl    _profile_clean
#endif /* PROFILING *//* All done */bl  exit.Lexit: /* should never get here */b .Lexit.Lstart:.size    _start,.Lstart-_start

一样的,我们不仔细展开这段汇编,其实通过注释就能够明白,这里的主要功能就是初始化各种数据,包括bss等等。最后,汇编来到了main,这个main就是FSBL的主函数,也就是大家比较熟悉的c语言函数。

小结,实际上BSP在背后干了好多事情(上述所有的汇编都是bsp提供的),这是为了让用户能够忽略一些技术细节,直奔主题main。而这些技术细节已经有Xilinx官方为我们完整无误的准备好了,所以FSBL我们其实只用聚焦在main函数即可,其他的脏活累活BSP已经替我们完成了,我们用不用太操心。不过通过上面的一些展开,大伙儿应该也有了一个模糊的概念,也就是说虽然我们写的所有的函数都是从main函数开始,然后CPU执行的第一条指令,绝对不是main,而是最基础的汇编。这个汇编会替你搞定c语言环境,让我们的main能够很ojbk的运行。下次把目光回到main函数

终于要开始进入main()了,激动不?

费话不多讲,直接怼源码,如下所示

int main(void)
{u32 BootModeRegister = 0;u32 HandoffAddress = 0;u32 Status = XST_SUCCESS;/** PCW initialization for MIO,PLL,CLK and DDR*/Status = ps7_init();//......// to be continued next blog...//......
}

逐条怼:
一开始就定义了三个变量,这三个变量的作用请看下面的注释

 u32 BootModeRegister = 0;// 用来存放boot的模式,到底是Qspi还是NOR等等门后面会具体讲u32 HandoffAddress = 0;// 用来存放FSBL运行完毕以后,下一个镜像执行的地址,这个应该会很后面再讲u32 Status = XST_SUCCESS;//存放操作是否成功的标志,一旦失败基本就是打印信息然后non-POR

next, 接下来开始初始化MIO,PLL,CLK和DDR,调用的函数就是ps7_init()

 /** PCW initialization for MIO,PLL,CLK and DDR*/Status = ps7_init();if (Status != FSBL_PS7_INIT_SUCCESS) {fsbl_printf(DEBUG_GENERAL,"PS7_INIT_FAIL : %s\r\n",getPS7MessageInfo(Status));OutputStatus(PS7_INIT_FAIL);/** Calling FsblHookFallback instead of Fallback* since, devcfg driver is not yet initialized*/FsblHookFallback();}

如果看过我们上一篇blog应该有个印象,MIO不是已经被初始化过一遍吗,怎么又要?是的,就是这么灵活,也就是说你的FSBL可以在Qspi(这样BootROM只会初始化Qspi的接口MIO)里,你的BitStream可以保存在eMMC上,那这个多出来的eMMC的MIO也需要在初始化一下了。不多讲,直接看ps7_init()

int
ps7_init()
{// Get the PS_VERSION on run timeunsigned long si_ver = ps7GetSiliconVersion ();int ret;//int pcw_ver = 0;if (si_ver == PCW_SILICON_VERSION_1) {ps7_mio_init_data = ps7_mio_init_data_1_0;ps7_pll_init_data = ps7_pll_init_data_1_0;ps7_clock_init_data = ps7_clock_init_data_1_0;ps7_ddr_init_data = ps7_ddr_init_data_1_0;ps7_peripherals_init_data = ps7_peripherals_init_data_1_0;//pcw_ver = 1;} else if (si_ver == PCW_SILICON_VERSION_2) {ps7_mio_init_data = ps7_mio_init_data_2_0;ps7_pll_init_data = ps7_pll_init_data_2_0;ps7_clock_init_data = ps7_clock_init_data_2_0;ps7_ddr_init_data = ps7_ddr_init_data_2_0;ps7_peripherals_init_data = ps7_peripherals_init_data_2_0;//pcw_ver = 2;} else {ps7_mio_init_data = ps7_mio_init_data_3_0;ps7_pll_init_data = ps7_pll_init_data_3_0;ps7_clock_init_data = ps7_clock_init_data_3_0;ps7_ddr_init_data = ps7_ddr_init_data_3_0;ps7_peripherals_init_data = ps7_peripherals_init_data_3_0;//pcw_ver = 3;}// MIO initret = ps7_config (ps7_mio_init_data);  if (ret != PS7_INIT_SUCCESS) return ret;// PLL initret = ps7_config (ps7_pll_init_data); if (ret != PS7_INIT_SUCCESS) return ret;// Clock initret = ps7_config (ps7_clock_init_data);if (ret != PS7_INIT_SUCCESS) return ret;// DDR initret = ps7_config (ps7_ddr_init_data);if (ret != PS7_INIT_SUCCESS) return ret;// Peripherals initret = ps7_config (ps7_peripherals_init_data);if (ret != PS7_INIT_SUCCESS) return ret;//xil_printf ("\n PCW Silicon Version : %d.0", pcw_ver);return PS7_INIT_SUCCESS;
}

该函数主要完成:

  1. 读取PS版本,估计一些老料子的方式略有不同吧
  2. 根据PS版本,赋予相对应初始化数组的指针。这个数组基本上构成举例(ps7_mio_init_data_1_0)如下
unsigned long ps7_mio_init_data_1_0[] = {// START: top// .. START: SLCR SETTINGS// .. UNLOCK_KEY = 0XDF0D// .. ==> 0XF8000008[15:0] = 0x0000DF0DU// ..     ==> MASK : 0x0000FFFFU    VAL : 0x0000DF0DU// .. EMIT_WRITE(0XF8000008, 0x0000DF0DU),

可以简单的理解为,这个ps7_mio_init_data_1_0数组中的而每一个元素都是一种操作,这个操作包含了EMIT_WRITE,EMIT_READ等等。比如说EMIT_WRITE,为了完成这个操作,实际上包含了3个元素,操作指令码+地址+数据(不同的操作包含的数据不同,有些操作会有四个元素)。

#define EMIT_WRITE(addr,val)          ( (OPCODE_WRITE     << 4 ) | 2 ) , addr, val

其想要实现的功能就是往addr write val,比如说EMIT_WRITE(0XF8000008, 0x0000DF0DU),其想要实现的功能就是将地址0XF8000008上的数据0x0000DF0DU
而0XF8000008这个地址,通过查看TRM,实际上就是给SCLR_UNLOCK寄存器写入0xDF0D,目的就是为了解锁SCLR所有的寄存器,使其可写,也就是说没有完成这一步的话,SCLR的其余寄存器使不允许写操作的!

Xilinx希望通过这种比较奇怪的方式完成了一系列操作(EMIT_WRITE和其他操作)的封装成一个组合(ps7_mio_init_data_1_0),这一些列的操作共同完成了比如说MIO的初始化,DDR的初始化等等。同时Xilinx提供了一个函数去解读这些操作,**ps7_config ()**正是为了实现这个功能,如下所示,利用ps7_config ()和ps7_mio_init_data来完成MIO的初始化

  // MIO init , include in ps7_init()ret = ps7_config (ps7_mio_init_data);  if (ret != PS7_INIT_SUCCESS) return ret;

下面来看一下ps7_config()

int
ps7_config(unsigned long * ps7_config_init)
{unsigned long *ptr = ps7_config_init;unsigned long  opcode;            // current instruction ..unsigned long  args[16];           // no opcode has so many args ...int  numargs;           // number of arguments of this instructionint  j;                 // general purpose indexvolatile unsigned long *addr;         // some variable to make code readableunsigned long  val,mask;              // some variable to make code readableint finish = -1 ;           // loop while this is negative !int i = 0;                  // Timeout variablewhile( finish < 0 ) {numargs = ptr[0] & 0xF;opcode = ptr[0] >> 4;for( j = 0 ; j < numargs ; j ++ ) args[j] = ptr[j+1];ptr += numargs + 1;switch ( opcode ) {case OPCODE_EXIT:finish = PS7_INIT_SUCCESS;break;case OPCODE_CLEAR:addr = (unsigned long*) args[0];*addr = 0;break;case OPCODE_WRITE:addr = (unsigned long*) args[0];val = args[1];*addr = val;break;case OPCODE_MASKWRITE:addr = (unsigned long*) args[0];mask = args[1];val = args[2];*addr = ( val & mask ) | ( *addr & ~mask);break;case OPCODE_MASKPOLL:addr = (unsigned long*) args[0];mask = args[1];i = 0;while (!(*addr & mask)) {if (i == PS7_MASK_POLL_TIME) {finish = PS7_INIT_TIMEOUT;break;}i++;}break;case OPCODE_MASKDELAY:addr = (unsigned long*) args[0];mask = args[1];int delay = get_number_of_cycles_for_delay(mask);perf_reset_and_start_timer(); while ((*addr < delay)) {}break;default:finish = PS7_INIT_CORRUPT;break;}}return finish;
}

该函数很简单,实际上就是EMIT_WRITE,EMIT_EXIT,EMIT_READ等一系列操作的解包过程,有兴趣的可以深入查看一下。需要注意的是,最后一个操作一定是EMIT_EXIT,也就是说不管是ps7_mio_init_data还是ps7_pll_init_data,这些数组的最后一个元素(操作)一定是EMIT_EXIT,读者可自行检查。

小节

Xilinx利用了一种非常不常见的方式完成了部分(MIO或者DDR)初始化,究其原因可能是这部分初始化工作是固定的,所以什么可读性啊都不需要了? 既然xilinx这么干了,我们看得懂就行了,这种方式极其不推荐。

main的后续操作,会在以后的blog中在详细解释,敬请期待。

FPGA - Zynq - 加载 - FSBL源码解析1相关推荐

  1. Volley 图片加载相关源码解析

    转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47721631: 本文出自:[张鸿洋的博客] 一 概述 最近在完善图片加载方面的 ...

  2. 未能加载文件或程序集rsy3_abp vnext2.0之核心组件模块加载系统源码解析

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

  3. abp vnext2.0之核心组件模块加载系统源码解析

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

  4. FPGA - Zynq - 加载 - BootRom

    FPGA - Zynq - 加载 - BootROM 题外话 BootROM BootROM Header Definition BootROM Header Searching and Loadin ...

  5. Android开发之WebView加载HTML源码包含转义字符实现富文本显示的方法

    老套路先看效果图: WebView加载带有转移字符的HTML源码 再看转义后的字符的效果图: 先看WebView加载HTML源码的方法如下: webview.loadDataWithBaseURL(n ...

  6. 图片加载 二维码 解析

    图片加载 二维码 解析 1. layout布局文件 (1)activity_category.xml <?xml version="1.0" encoding="u ...

  7. MultiDex的加载dex源码分析

    工作流程 MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 "mult ...

  8. 使用base标签后图片无法加载_Spring 源码学习(二)-默认标签解析

    `Spring` 解析默认标签~ 从上一篇笔记可以看出,在容器注册 bean 信息的时候,做了很多解析操作,而 xml 文件中包含了很多标签.属性,例如 bean . import 标签, meta ...

  9. DataX Transformer从入口到加载的源码分析及UDF扩展与使用

    DataX GitHub DataX Transformer 目录 1 前言 2 需求说明 3 解决方案分析 4 解密算法 5 Hive UDF 5.1 测试数据 5.2 新建 Maven 项目 5. ...

最新文章

  1. C# 存储过程 分页
  2. C++极值minmax最大值最小值算法(附完整源码)
  3. 堆排序(heap_sort)
  4. 2020/Province_C_C++_A/F/成绩分析
  5. HDU多校7 - 6853 Jogging(bfs+结论)
  6. python max((1、2、3)*2)_Python functional.max_pool2d方法代码示例
  7. ubuntu下NDK环境配置
  8. 赵平C语言,赵平智与OIOIC
  9. python变量类型声明_python基础知识:变量的定义以及类型
  10. python怎么清理垃圾_【原创】python实现清理本地缓存垃圾
  11. Linux 命令汇总!【珍藏版】
  12. CDH中配置hive支持update delete
  13. 集中火力,专项击破!数据分析可视化广深线下培训火热来袭
  14. sina微博登录框和twitter的比较
  15. VMware OSP对比VMware Tools:简化Linux驱动更新
  16. 协同多智能体学习的价值分解网络的原理与代码复现
  17. uni-app 项目创建 (简单+明确!!!)
  18. 计算机网络原理-韩立刚-第四章 网络层
  19. 商户如何接入微信支付
  20. 索尼MUC-M2BT1换电池+爆改

热门文章

  1. 对数码相机的使用终于有点入门了
  2. 无法播放请确保你的计算机的,win10 groove无法播放提示错误0x8007007e的解法
  3. 百度快速收录,百度新网站快速收录服务是什么site首页收录
  4. java函数式编程好处_Java8的函数式编程怎么样?
  5. MRO工业品怎么做好供应链?
  6. (超详细)读取mnist数据集并保存成图片
  7. 动态加载Newtonsoft.Json
  8. 2010年最牛逼的50条QQ签名!
  9. ceph iscsi
  10. 什么是梯度消失和梯度爆炸