xilinx AMP多核异构原理以及RPU开发以及RPU源代码分析
author:Swing
date:2024/1/4
前言
本文所有内容由自己语言简述,我认为这是一种自己的理解过程,并且在一些问题的论述上需要不断的去查询相关资料。希望某些人尊重劳动成果,转载标明出处。
可能在本文中会有错误,欢迎大家指出,有偿感谢费用(奶茶钱)。本文所使用的主控为zu7ev。
参考文档:
2:OpenAMP Base Hardware Configurations - Xilinx Wiki - Confluence (atlassian.net)
1. AMP相关
1.1. 介绍
AMP在芯片领域,一般指:非对称多处理(Asymmetric Multiprocessing)。既然有非对称,那就有对称多处理(SMP)。经常听说遥遥领先的麒麟9000是泰山架构,拥有多少个大核加上多少个小核,这也属于一种多核异构。
SMP:指所有的核心都拥有相同的性能以及处理能力,能处理其他核心能处理的任务。他们可以共享内存和性能。
AMP:指处理器核心有不相同的性能,不同的处理器拥有不同的功能,他可以处理不同类型的任务。
1.2. 用途
目前控制类领域国产芯片有流行的大核+小核组合,大核跑Linux系统,小核跑裸机或者跑实时系统,通过内存共享的当时进行通信。例如:豪威的SOC,君正的x2000,还有全志的mips架构的SOC,CanMV-K230等等。为了实时性,小核直接处理对实时性要求高的任务。
优点:不需要像以前一样,电路上一个SOC再加上单片机的组合,降低了物料上的成本。
缺点:若芯片厂商在这类开发没有给出详细的说明,下游的厂商开发难度会增大。
2. Xilinx AMP介绍
2.1. zu7ev介绍
这个为主控芯片的总体框图,拥有4个cortex-a53 + 2个cortex-r5 处理核心(硬核)。Xilinx官方适配了openamp和libmetal两种remote框架。R核的程序使用赛灵思的Vitis IDE进行开发,通过编译后的elf文件烧录到指定的内存,然后去启动他。启动R核原理主要去使能R核的控制器,并将R核的PC,SP指针指向启动的内存。
2.2. openamp介绍
本文使用openamp框架,是一个开源的远程控制子系统(rproc),属于协处理器的一个驱动程序。(下图来源于Xilinx UG1186)
openamp可以从Linux的内核空间与R核裸机交互,也可以从Linux的用户空间与R核裸机交互,他们通讯的最主要手段是 内存共享(IPC: Shared Memory)和核间中断(IPC: Interrupts)。在Xilinx的维基百科上有详细的介绍对这一方面。
2.3. 内存分区
ZYNQ的PS端,实际上R核和A53都存在于PS端内,只是R核没有参与Linux系统的CPU调度。一般R核程序运行的内存分区是写死的,因为R核跑另外一个裸机或者操作系统,需要bootload,以及通过链接脚本文件的指定内存端,例如bss,code,text端等。之前我在想是不是可以在Linux通过动态内存申请的方式,然后再映射的方式得到物理地址,但是这样的话每次申请都需要把R核的链接文件重新修改,在开发上面会非常麻烦。并且在内存高负载的情况下,在Linux上的动态内存管理可能会出现意想不到的问题。
以下为ZYNQ的内存分区情况,可以在Linux下通过看/proc/iomem的RAM分区情况。
cat /proc/iomem
所以可以得出ZYNQMP的大致分区为:
R核的内存区域是可以通过在设备树上指定划出一片分区出来。在设备树上划出来的reserved也属于system ram。
3. 使用方法
3.1. RPU 方面
RPU的程序是可以通过petalinux编译出来,也可以用xilinx vitis编译出来。通过编译后的elf文件,写入指定的内存并赋值给RPU的PC指针到指定的内存地址并使能运行。注意:这里的Cortex-R5是一个32位的核心,并不是64位的核心。所以RPU的寄存器寻址范围是(2^32): 0 ~ 0xffffffff。本文使用vitis的方式编译,因为用petalinux编译你得手动重新编写Makefile,费时费力不讨好效率低的话,咱不干。
3.1.1. Vitis 新建工程
新建vitis工程选择R5核心,这里我选择了R5_0,因为7EV是有两个R5核心。你可以选择split模式,也可以选择lockstep模式。
split模式:是指 把RPU分成多个处理器,处理不同的任务。
lockstep模式:指把相同的核心来执行相同的操作,由于lockstep模式笔者目前还没有使用过。这里使用的是split模式。
注意:这里的模式并不是在Vitis软件上配置,是在Linux的设备树上配置。
上图的操作系统选择standlone,不依赖任何,指裸机程序。
这里的例程选择open amp echo-test。新建完工程和例程后,打开rpmsg-echo.c文件可以看到他的int main函数所执行的任务。
在开发R核心的时候,基于他的官方例程,照葫芦画瓢对RPU进行开发。
3.1.2. Vitis 链接文件以及内存映射
在工程的.spr可以看到该平台的物理地址映射表,这里是硬件的空间地址映射。也就是R核可以访问到的物理地址。
在工程目录的src文件夹下,可以看到程序的运行地址。
这里写死了从0x3ED00000地址开始运行。所以你在linux的设备树上得给他弄这段地址给他使用。
3.1.3. Vitis 的编译
编译后的elf文件是我们所需要的,在工程目录下的DEBUG目录。
可以对他右键找到他的目录,拷贝到7ev板子上去运行。
3.2. Petalinux 方面
3.2.2. Kernel配置
进入Petalinux工程中,已经我已经新建了工程,就不重新建造一个了。进入内核配置菜单。
petalinux-config -c kernel
选中R核所需要的驱动选项(参考Xilinx UG1186)
CONFIG_UIO_PDRV_GENIRQ=m
CONFIG_RPMSG_CHAR=m
CONFIG_RPMSG_VIRTIO=m
CONFIG_RPMSG=m
CONFIG_VIRTIO=m
CONFIG_REMOTEPROC=y
CONFIG_ZYNQMP_R5_REMOTEPROC=m
CONFIG_SPARSEMEM_VMEMMAP=y
第二步需要修改设备树,给裸机程序所需要用到的内存进行预留。在3.1.2.章节可以看到他的链接地址。
这里的RPU控制器的寄存器地址说实话,我在Xilinx的官方没有找到相对应的寄存器手册,是根据Xilinx UG1186文档的说明直接给他贴上去。也就是这个0xFF9A0000 和 0xff990600 这两个地址,如果有这方面的懂哥,非常欢迎告诉我如何找到他这个寄存器的说明手册。
这里是在petalinux工程目录下,下面这个设备树上进行添加。
project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
rpu0vdev0vring0: rpu0vdev0vring0@3ed40000 {
no-map;
reg = <0x0 0x3ed40000 0x0 0x4000>;
};
rpu0vdev0vring1: rpu0vdev0vring1@3ed44000 {
no-map;
reg = <0x0 0x3ed44000 0x0 0x4000>;
};
rpu0vdev0buffer: rpu0vdev0buffer@3ed48000 {
no-map;
reg = <0x0 0x3ed48000 0x0 0x100000>;
};
rproc_0_reserved: rproc@3ed00000 {
no-map;
reg = <0x0 0x3ed00000 0x0 0x40000>;
};
};
zynqmp-rpu {
compatible = "xlnx,zynqmp-r5-remoteproc-1.0";
#address-cells = <2>;
#size-cells = <2>;
ranges;
core_conf = "lockstep";
reg = <0x0 0xFF9A0000 0x0 0x10000>;
r5_0: r5@0 {
#address-cells = <2>;
#size-cells = <2>;
ranges;
memory-region = <&rproc_0_reserved>, <&rpu0vdev0buffer>, <&rpu0vdev0vring0>, <&rpu0vdev0vring1>;
pnode-id = <0x7>;
mboxes = <&ipi_mailbox_rpu0 0>, <&ipi_mailbox_rpu0 1>;
mbox-names = "tx", "rx";
tcm_0_a: tcm_0@0 {
reg = <0x0 0xFFE00000 0x0 0x10000>;
pnode-id = <0xf>;
};
tcm_0_b: tcm_0@1 {
reg = <0x0 0xFFE20000 0x0 0x10000>;
pnode-id = <0x10>;
};
};
};
zynqmp_ipi1 {
compatible = "xlnx,zynqmp-ipi-mailbox";
interrupt-parent = <&gic>;
interrupts = <0 29 4>;
xlnx,ipi-id = <7>;
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* APU<->RPU0 IPI mailbox controller */
ipi_mailbox_rpu0: mailbox@ff90000 {
reg = <0xff990600 0x20>,
<0xff990620 0x20>,
<0xff9900c0 0x20>,
<0xff9900e0 0x20>;
reg-names = "local_request_region",
"local_response_region",
"remote_request_region",
"remote_response_region";
#mbox-cells = <1>;
xlnx,ipi-id = <1>;
};
};
3.2.2. Rootfs 配置(可选可不选)
看某一些博客说,选中他的Rootfs是为了用户空间有他的动态库,但是我并没有在Xilinx官方的测试程序看到有关于openamp的依赖库。
所有我把这个标题写为可选可不选。我测试时候并没有选中,直接把他编译后的文件直接ssh直接拉进系统内运行了。
在Petalinux工程中,输入命令
petalinux-config -c rootfs
这里有Xilinx官方的R核测试程序。
3.3. RPU固件烧录
r5_test.elf 文件是我从vitis编译后的文件,上电后先看RPU驱动是否加载起来了。
使用echo命令,把文件烧录进去。通过RPU驱动的sys虚拟文件写入到指定的内存地址。
把在vitis编译后的elf文件写入下图所示位置
然后再这样!
他就起来运行了。
4. Linux RPU驱动源代码分析
根据设备树的compatible,xlnx,zynqmp-r5-remoteproc-1.0 搜索内核源码可以找到Xilinx的这个rproc驱动代码。
源码使用的是一个platform框架
这个proc函数先从设备树里面搜索设备节点的各类属性,然后保存到变量里面
进入该函数后,这里是主要实现RPU程序运行和烧录的操作函数。
先从烧录固件函数开始分析,看是rproc_elf_load_segments函数,但先看看其他的函数是干嘛的。进入zynqmp_r5_parse_fw函数
先搜索了设备树的一个属性
也就是内存划分。
这里的函数操作是把RPU固件的内存保存进变量里面。再看rproc_elf_load_segments 函数的实现。
这里就是把固件的数据直接memcpy到了指定的内存,挺粗暴的。这里烧录固件没啥好看的,就是最主要memcpy到指定的内存。
再看一下zynqmp_r5_rproc_start 函数。一般来说启动核心的代码,是对RPU的寄存器进行使能,不知道RPU上处理器的通用寄存器和PC,SP的指针
是否在驱动内赋值;继续往下翻翻看。
这里可以看到eemi_ops结构体内有这些函数指针,进行设置。
看到这个指针在前面的probe被赋值了。
找一下这个函数的实现。
发现在这里
看了里面的代码,是跟ARM的CPU指令有关的操作。虽然对ARM的指令集并不是特别懂,但不妨碍我们继续往下分析。
跳入zynqmp_pm_invoke_fn 函数内。
看一下do_fw_call。
这里的备注说他可能调用do_fw_call_smc或者调用do_fw_call_hvc。看一下do_fw_call_smc
看一下arm_smccc_smc函数。
这是一个把指赋值给R5核心通用寄存器a0 ~ a7。这个函数的实现是由ARM的编译器提供。
这样思路就清晰了。
实际上这里的eemi_ops->ioctl是把形参1,2,3,4的值进行处理后
smc_arg[0] = PM_SIP_SVC | pm_api_id;
smc_arg[1] = ((u64)arg1 << 32) | arg0;
smc_arg[2] = ((u64)arg3 << 32) | arg2;
传给处理器的通用寄存器 A0 ~ A3。也就是所说的数据入栈。
并且返回结果到val这个变量内。
那么这时就会有一个问题,ZYNQMP有6个处理器,4个Cortex-a53和2个Cortex-r5。他这里的值是赋值给哪个CPU呢?
这里涉及到了ARM-A的系统架构原理,ATF-A以及Linux系统CPU调度的原理实现。
由于笔者知识目前够不到这里,这里的代码无法再进一步进行分析。等我学会了再发另一边笔记对该内容继续分析。
因为这里涉及到了CPU的中断陷入的原理,目前对ARM的体系化以及CPU运行原理的知识不足充足。
5. 额外小知识
5.1. Cortex-R5 寄存器介绍
Cortex-R5核心是一种针对实时应用设计的ARM处理器核心。它具有一组特定的寄存器,用于执行和控制处理器的操作。以下是Cortex-R5核心的一些常见寄存器:
- 通用寄存器(General-Purpose Registers):
- R0-R12:12个通用寄存器,用于存储通用数据。
- SP:栈指针,用于管理函数调用时的堆栈操作。
- LR:链接寄存器,存储函数调用返回地址。
- PC:程序计数器,存储当前执行指令的地址。
- 控制寄存器(Control Registers):
- CPSR(Current Program Status Register):当前程序状态寄存器,存储处理器的当前运行状态和条件码信息。
- SPSR(Saved Program Status Register):保存程序状态寄存器,用于存储中断或异常处理程序之前的状态。
- 中断控制寄存器(Interrupt Control Registers):
- ICCPMR(Interrupt Controller CPU Priority Mask Register):中断控制器CPU优先级屏蔽寄存器,用于控制中断的优先级。
- ICCEOIR(Interrupt Controller End Of Interrupt Register):中断控制器结束中断寄存器,用于通知中断控制器中断处理的完成。
5.2. Cortex-A53 寄存器介绍
Cortex-A53 是一种 ARMv8 架构下的 CPU 核心,它具有一组通用寄存器、特权级别寄存器和其他特殊寄存器。以下是 Cortex-A53 CPU 核心的主要寄存器:
- 通用寄存器(General-Purpose Registers):
- x0-x30(或者称为 r0-r30):共有 31 个通用寄存器,用于存储数据和计算。
- xzr(或者称为 r31):零寄存器,固定为零值。
- 程序状态寄存器(Program Status Registers):
- CPSR(Current Program Status Register):当前程序状态寄存器,包含条件标志位、中断使能位等。
- SPSR (Saved Program Status Register):保存的程序状态寄存器,在异常处理过程中保存之前的 CPSR 值。
- 异常相关寄存器:
- ELR (Exception Link Register):异常链接寄存器,用于保存异常返回地址。
- SP (Stack Pointer):堆栈指针,指向当前使用的堆栈区域。
- PC (Program Counter):程序计数器,指向当前正在执行的指令地址。
- LR (Link Register):链接寄存器,用于保存函数调用的返回地址。
- 特殊寄存器:
- TTBR0/TTBR1 (Translation Table Base Registers):用于虚拟地址转换的页表基地址寄存器。
- MAIR (Memory Attribute Indirection Register):内存属性间接寄存器,用于定义不同内存区域的访问属性。
- TCR (Translation Control Register):内存转换控制寄存器,用于配置页面大小、缓存策略等。
- ID_AA64PFR0_EL1(AArch64 Processor Feature Register 0):用于描述当前处理器的架构和功能。
5.3. 什么是smc,什么是hvc
SMC(Secure Monitor Call)和 HVC(Hypervisor Call)是 ARM 架构下的两种不同的调用方式。
SMC 是一种用于进入安全监控模式的指令,安全监控模式是 ARM 架构中特权级别最高的系统模式,用于执行与安全相关的任务,如信任环境的切换、加密解密等。SMC 指令可以使当前运行在非特权级别的代码进入安全监控模式,并执行受保护的操作。SMC 指令的参数和返回值通过寄存器传递。
HVC 是一种用于进入虚拟化模式的指令,虚拟化模式是 ARM 架构中用于支持虚拟化的一种特殊模式。HVC 指令可以使当前运行在非虚拟化模式下的代码进入虚拟化模式,并在虚拟环境中执行受保护的操作。HVC 指令的参数和返回值同样也是通过寄存器传递。