1.前言

  在嵌入式Linux系统上进行嵌入式程序开发相当方便,但由于嵌入式Linux系统为非实时性系统,因此在一些实时性要求高的任务中,其无法保证程序满足实时性要求。为了保证实时性要求,通常我们采用裸机程序开发的方式,但有时候对于一个嵌入式程序而言,其只有某一部分的功能要求具有较高的实时性,而对于其它功能的实时性要求不高,如果全部采用裸机开发方式会消耗大量的时间与精力。这时肯定就会想是否可以让实时性要求高的功能采用裸机运行,而其它功能运行在操作系统上?如果可以这样操作,这一方面保证了实时性需求,另一方面也简化了程序开发时间与精力,实现了双赢效果。答案肯定是可以的,但只能在多核处理器上实现,让多核处理器每个核心分别运行裸机程序与操作系统即可。

2.开发环境

  本教程使用的是创龙ZYNQ7020系列开发板,创龙 SOM-TLZ7x 是一款基于 Xilinx Zynq-7000 系列 XC7Z010/XC7Z020 高性能低功耗处理器设计的异构多核 SoC 工业级核心板,处理器集成 PS 端双核 ARM Cortex-A9 + PL端 Artix-7 架构 28nm 可编程逻辑资源,通过工业级 B2B 连接器引出千兆网口、USB、CAN、UART 等通信接口,可通过 PS 端加载 PL 端程序,且 PS 端和 PL 端可独立开发。核心板经过专业的 PCB Layout 和高低温测试验证,稳定可靠,可满足各种工业应用环境。

图1 创龙ZYNQ7020开发板
创龙ZYNQ7020

3.程序设计目标

  为了说明使用ZYNQ7020实现双核AMP(Linux+裸机)方案开发流程,使用一简单例程来对该过程进行说明,本例程各CPU的主要功能划分如下:

  • CPU0:运行嵌入Linux系统,并通过应用程序启动CPU1。
  • CPU1:运行裸机程序,裸机程序为使用Private Timer定时1S实现LED的闪烁功能,同时通过UART输出LED状态。

4.裸机双核AMP

4.1 ZYNQ启动流程

   ZYNQ支持QSPI、NOR、NAND、SD等启动方式,同时启动分为三个阶段进行。

Stage 0 (BootROM):
  在上电复位或者热复位之后,处理器首先执行 BootRom 里的代码,这一步是最初始启动设置。BootRom 存放了一段用户不可更改的代码,当然是在非 JTAG 模式下才执行,代码里包含了最基本的 NAND,NOR,Quad-SPI,SD 和 PCAP 的驱动。另外一个很重要的作用就是把 stage 1 的代码搬运到 OCM 中,就是 FSBL 代码(First Stage Boot Loader),空间限制为 192KB。

Stage 1 (FSBL / User code):
  接下来进入最重要的一步,当 BootRom 搬运 FSBL 到 OCM 后,处理开始执行 FSBL 代码,FSBL 主要有以下几个作用:

  • 初始化 PS 端配置,这些配置也就是在 Vivado 工程中对 ZYNQ 核的配置。包括初始化 DDR,MIO,SLCR 寄存器。
  • 如果有 PL 端程序,加载 PL 端 bitstream加载 second stage bootloader 或者 bare-metal 应用程序到 DDR 存储器。
  • 交接给 second stage bootloader 或 bare-metal 应用程序。

Stage 2 (U-Boot / System / Application):
  这一阶段可以加载Uboot也可以加载其他裸机程序,如果加载裸机程序,那么这一阶段运行裸机程序后即结束,如果加载Uboot这运行执行Uboot进行系统的引导,直到最后系统运行。

图2 ZYNQ启动流程
ZYNQ启动流程

4.2 FSBL程序说明

   前面说过FSBL主要作用为PS与PL的初始化及程序加载功能,使用vivado SDK软件可以生成一默认的FSBL程序,具体流程如下:
   1)新建一个名为 fsbl 的 APP,特别注意硬件平台选择最新的那个
在这里插入图片描述
   2)模板选择 Zynq FSBL
在这里插入图片描述
   3)添加调试宏定义 FSBL_DEBUG_INFO,可以在启动输出 FSBL 的一些状态信息,有利于调试,但是会导致启动时间变长。
在这里插入图片描述
   4)修改后保存,SDK 默认会自动编译,生成 fsbl.elf 文件。

   接下来会以SD卡启动为例,分析FSBL的具体运行流程:
   a)对PS进行初始化
在这里插入图片描述
   b)获取BootModeRegister值,以确认具体启动方式
在这里插入图片描述
   c)当为SD卡启动模式时,会调用InitSD函数,InitSD函数会使用f_open打开filename对应的文件。之后将SDAccess函数指针传递给MoveImage,SDAccess函数用于将InitSD函数打开的文件内容复制到指定内存地址中。此处打开的是BOOT.bin文件。
在这里插入图片描述

图3 InitSD函数
在这里插入图片描述图4 SDAccess函数
在这里插入图片描述

   d)调用LoadBootImage函数加载程序镜像,并将程序启动地址传递给HandoffAddress。LoadBootImage用于加载BOOT.bin文件中内容,包括PS端的程序以及PL端的程序,其最大加载程序数量由MAX_PARTITION_NUMBER宏定义确定,默认值为0xE。LoadBootImage函数中的操作还会涉及到BOOT.bin文件的结构,BOOT.bin文件与应用程序编译获取的BIN文件不同,应用程序的BIN文件直接为ARM运行的二进制代码,而BOOT.bin文件包含BOOTROM头部,镜像头部以及镜像本身(具体可详见UG585文档的Ch.6章节)。
在这里插入图片描述
在这里插入图片描述
图5 BOOTROM头部
在这里插入图片描述   e)调用FsblHandoff函数跳转到镜像程序并执行该程序(CPU0执行程序)。
在这里插入图片描述

4.3 裸机双核AMP程序

   FSBL调用FsblHandoff函数后CPU0便会执行对应的应用程序,此时CPU1处于WFE状态,因此需要唤醒CPU1并执行CPU1的程序。由UG585文档可知唤醒CPU1并执行指定程序需要两个步骤,第一步是向 0Xffffffff0地址写入 CPU1 的访问内存基地址,第二步是通过 SEV 指令唤醒 CPU1 并且跳转到相应的程序。同时需要注意,如果CPU1需要使用中断,则中断GIC控制器必须在CPU0中完成初始化,否则CPU1无法进入中断。

4.3.1 CPU0程序

   a)新建工程,在 Processor 选择 ps7_cortexa9_0,也就是 CPU0,选择 Empty。
在这里插入图片描述
   b) lscript.ld 里修改CPU0访问的内存空间,即ps7_ddr_0访问空间。
在这里插入图片描述
   c) CPU0 函数如下所示,首先使用init_platform初始化设备,之后调用ScuTimerInit函数通过初始化Private Timer来完成GIC控制器的初始化(也可以只初始化GIC控制器,此处偷懒没有修改初始化函数,该函数看参照SDK中的scugic例程)。最后调用StartCpu1来启动CPU1,StartCpu1除了向 0Xffffffff0地址写入 CPU1 的访问内存基地址以及调用SEV指令外,还使用Xil_SetTlbAttributs 函数关闭访问 OCM 的 Cache,因为关闭 Cache,可以减少维护两个 CPU 访问 OCM 的一致性问题,如果不加此函数,FLASH 启动后,CPU1 不工作(可参考 XAPP1079 文档)。
在这里插入图片描述
图6 StartCpu1函数
在这里插入图片描述
在这里插入图片描述

4.3.2 CPU1程序

   a)新建 CPU1 工程,与 CPU0 工程不同的是选择 ps7_cortexa9_1。
在这里插入图片描述
   b) lscript.ld 里修改CPU1访问的内存空间,即ps7_ddr_0访问空间
   (注意:CPU1的内存访问空间不能与CPU0重合)。
在这里插入图片描述
  c)CPU1 的 BSP 设置界面,在 extra_compile_flags 内添加-DUSE_AMP=1,使其支持双核工作。
在这里插入图片描述
   d) CPU1的程序比较简单,首先使用Xil_SetTlbAttributes函数关闭访问 OCM 的 Cache,之后使用init_platform初始化设备,然后再对LED及Private Timer进行初始化。
在这里插入图片描述

4.3.3 CPU1程序

   右键单击CPU0的工程文件夹,点击Creat Boot Image,并添加FSBL,CPU0,CPU1的elf文件,最后单击Creat Image以生成BOOT.bin文件。
在这里插入图片描述
   至此我们就完成了裸机双核AMP的程序,只需要把BOOT.bin拷入到SD卡即可运行。

5.linux+裸机双核AMP

   通过前面的裸机双核AMP试验,我们可以很快得出linux+裸机双核AMP方案的初步思路,即CPU0先运行FSBL加载uboot与CPU1程序,之后运行uboot加载linux系统到CPU0,最后通过linux应用程序开发实现CPU1的启动。虽然过程与裸机双核AMP相似,但由于涉及到操作系统,我们不得不考虑以下几个问题:

  • uboot我们一般是在linux下编译得到的,我们如何才能让FSBL把uboot加载到DDR中?
  • 默认情况下linux内核会使用所有CPU,如何才能让Linux内核只在一个CPU上运行?
  • 应用程序开发使用的内存空间通常0x0000 0000 - 0x4000 0000,但CPU1启动地址需存贮在0Xffffffff0,应用程序无法直接访问该地址。
  • 启动CPU1后如果想复位或关闭CPU1该如何完成?(由于能力有限未能解决该问题)

   当我们解决了以上问题就可以实现一个核跑linux,另一个核跑裸机程序。

5.1 Linux单核运行

  在XAPP1078中提到,ZYNQ7020需要单核运行只需要修改设备树的内存访问空间和将maxcpus设置为1即可,本人按照上述方法进行设置,启动linux后发现linux内核还是可以使用两个CPU,因此该方法可能存在问题(也有可能是本人能力所限导致)。

  对于maxcpus设置其本质上是对NR_CPUS的值进行设置,通过使用make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig指令进入kernel图形化界面,并搜索NR_CPUS,从其说明中可知,NR_CPUS的取值范围为2-32,无法设置为1(怀疑这是maxcpus设置为1无法单核运行linux的原因)。另外还可以注意到,NR_CPUS有效的前提是SMP开启(SMP为对称多处理,即一个操作系统实例可以管理所有 CPU 内核,且应用并不绑定某一内核),是否只要将SMP关闭linux就使用单核运行。
在这里插入图片描述
  SMP关闭可以在defconfig进行,通过修改CONFIG_SMP由y修改为n即可,之后按照正常的内核编译流程进行编译,并将生成的uImage与设备树文件拷贝到SD卡运行即可。
在这里插入图片描述
  本人使用的是创龙开发板,使用的设备树文件为tlz7x-easyevm.dts,由于其原本的设备树文件就对内存进行了划分,所以本人未去修改设备树文件。对于其它的开发板,可能需要修改设备树文件,需要将内存空间划分一部分给CPU1使用。创龙ZYNQ的内存划分如下所示,内存总空间为0x0000 0000 - 0x4000 0000,未使用的内存空间在reserved-memory节点中划分,其中0x1800 0000 - 0x0x18FF FFFF用于PL MicroBlaze,0x19000000 - 0x19FF FFFF用于CPU1,0x1A00 0000 - 0x1FFF FFFF用于PL,其余为PS Linux使用。
在这里插入图片描述

在这里插入图片描述

5.2 FSBL修改

  FSBL的作用除了初始化PS与PL外,还需要加载uboot与cpu1的程序(main),因此对FSBL的修改主要是围绕该方面来进行。

  a)InitSD函数修改:在SDK默认生成的FSBL程序的InitSD函数只会加载一个文件,即BOOT.bin文件,为了让InitSD函数可以加载uboot与main两个程序,需对该函数进行修改。

main函数修改内容:
在这里插入图片描述
InitSD函数修改内容:
在这里插入图片描述
  b)增加SDAccessAPP函数与MoveImageAPP结构体:SDAccessAPP与SDAccess函数主要功能一样,只是SDAccessAPP用于读取main程序,而SDAccess用于读取uboot程序。
在这里插入图片描述
在这里插入图片描述
  c)使用MoveImage与MoveImageAPP函数分别将,u-boot.bin与main.bin文件内容加载到指定地址。
在这里插入图片描述
  d)使用FsblHandoff跳转到uboot执行。
在这里插入图片描述
  e)考虑到linux应用编程无法设置CPU1的启动地址,因此在FsblHandoff函数中增加设置CPU1启动地址的功能(WriteCpu1StartAddr函数)。
在这里插入图片描述
WriteCpu1StartAddr函数(与前面所诉的裸机双核AMP一样,先关闭OCM Cache,然后向0xffff fff0中写入CPU1程序起始地址):
在这里插入图片描述
  f)编译生成BOOT.bin文件即可(BOOT.bin文件只包好FSBL的elf文件)。

5.3 linux+裸机双核AMP程序

5.3.1 CPU1程序

  此处CPU1程序与裸机双核AMP的CPU1程序一样,但其生成bin文件的方式有所区别。由于使用vivado sdk只能生成BOOT.bin文件,而前面说过BOOT.bin文件不是纯粹的ARM应用程序,其包含了BOOTROM头部等信息,而vivado sdk编译后生成的文件为elf格式,因此还需要将elf文件转为bin文件,可以直接使用gcc编译工具进行转换,此处使用了一elf转bin文件的工具进行。
在这里插入图片描述

5.3.2 CPU0程序

  由于Linux内核启动后已初始化完成GIC控制器,同时CPU1的程序起始地址设置在FSBL中完成,因此对于CPU0的程序非常简单,只需要调用SEV指令即可启动CPU1。
在这里插入图片描述

5.3.3 功能验证

  将BOOT.bin、u-boot.bin、main.bin以及linux内核镜像与设备树文件拷贝到SD卡,启动开发板,通过Xshell软件查看输出信息如下:

FSBL输出信息:
在这里插入图片描述
  上图为FSBL输出信息,从该信息可知,FSBL成功读取u-boot.bin与main.bin文件,并将两个程序加载到指定地址,同时成功设置CPU1的起始地址到0xFFFF FFF0。
uboot输出信息:
在这里插入图片描述  之后CPU0顺利跳转到uboot程序,并通过uboot启动linux内核。
linux内核启动:
在这里插入图片描述
  进入目录/sys/devices/system/cpu,使用cat online指令查看占用CPU情况,可以发现linux只使用了CPU0。
在这里插入图片描述
  注:第一次启动后发现linux还是运行在两个核心上,但重启后就在一个核心上运行,原因未知。
  执行CPU0的程序,执行结果图所示,可见CPU1顺利启动且执行了裸机程序。
在这里插入图片描述

  说明:Xilinx官方实际上有给linux+裸机双核AMP教程,分别有两个方法,一个是与本文所述方法类似,只是需要将fsbl,uboot和main函数都放入BOOT.bin中,另一个方法则是CPU0 使用 remoteproc 加载 CPU1程序,并对 CPU1 进行配置。两个方法的详细过程可分别参考XAPP1078与XAPP1079文档。另外为了实现某个线程独占CPU还可以使用隔离核的方法。
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐