简介:本资源面向FPGA嵌入式开发工程师与高校相关专业学习者,聚焦XCZU系列MPSoC(含XCZU2CG、XCZU2EG、XCZU4EV)上基于VITIS工具的双核AMP驱动实现,解决异构多核任务划分、核间通信与协同调度等工程问题,适合具备一定Zynq UltraScale+基础的开发者进阶实践。压缩包共约2000个文件、70.39MB,以C源码与头文件为主体,辅以汇编、链接脚本、Makefile、Tcl脚本、Vivado约束与比特流、XSA硬件描述及日志报告等,完整覆盖从硬件配置到软件编译调试的工程链路。目前已有108人学习下载。资源内含可直接参考的AMP驱动工程结构、双核任务划分与中断通信示例、VITIS构建与调试配置,以及性能分析与排错思路,便于读者快速复现并迁移到自动驾驶、图像处理、网络通信等低延迟并行场景。
1. 双核 AMP 不是把两个 CPU 跑起来那么简单
很多人第一次在 XCZU2EG 上跑双核,思路是「两个 A53 核各跑各的裸机程序,互不干扰」。真上手才发现,两个核抢同一块 DDR、抢同一个 UART、抢同一份外设寄存器,跑不了几秒就死锁或者数据错乱。这份资源包解决的正是这个问题:在 MPSoC 的 APU 里把两个 Cortex-A53 配成 AMP(Asymmetric Multiprocessing,非对称多处理)模式,CPU0 跑主控逻辑,CPU1 跑独立任务,两边通过共享内存和中断通信,用 VITIS 完成编译、链接、调试全流程。它适合已经能跑通单核裸机、想往异构多核方向走的 FPGA 开发者,也适合做嵌赛或项目实战时需要双核分工的团队。资源包里是可直接导入 VITIS 的工程,不是只给一份文档让你自己猜。
2. AMP 架构选型:为什么是 XCZU2EG 而不是软核
2.1 硬核 A53 与 MicroBlaze 的本质差别
在 Zynq UltraScale+ 里做多核,常见路线有两条:一条是用 PL 端的 MicroBlaze 软核拼多核,另一条是直接用 PS 端集成的 Cortex-A53 硬核。XCZU2EG 属于 Zynq UltraScale+ MPSoC 的入门型号,PS 端自带四核 A53(实际可用核数取决于具体器件配置)和双核 R5,做 AMP 的硬件底子是现成的。MicroBlaze 方案的问题在于每个软核都要吃 PL 逻辑资源,两个核就把 LUT 和 BRAM 啃掉一大块,而且软核主频通常只能跑到 100~200MHz 量级,跟 A53 的 1.2GHz 以上不在一个量级。
选 XCZU2EG 做 AMP 的核心理由是:硬核 CPU 不占 PL 资源,主频高,缓存和 MMU 都是完整的,跑裸机 AMP 时两个核可以各自独立配置 MMU 和缓存策略。代价是 PS 端的外设分配必须提前规划好,因为 A53 的很多外设是共享的,不像 MicroBlaze 那样可以随便挂 AXI 外设。
2.2 AMP 与 SMP 的边界:什么时候该用 AMP
SMP(对称多处理)是操作系统层面把多个核当一个大 CPU 用,任务调度由 OS 统一管。AMP 是每个核跑独立的程序镜像,各自有独立的入口地址、栈、堆,互不感知对方的 OS 或裸机逻辑。判断标准很简单:如果两个核的任务之间需要频繁共享大量数据结构、且对实时性要求一致,用 SMP 更省心;如果两个核的任务性质差异大——比如一个跑通信协议栈、一个跑实时控制循环,或者一个跑 Linux、一个跑裸机实时任务——AMP 更合适。
这份资源走的是纯裸机 AMP 路线,两个核都跑 standalone 程序,没有 Linux 参与。好处是启动流程简单、调试直观,坏处是没有 OS 帮你做资源隔离,共享内存的分配和中断路由必须手动规划清楚。
2.3 VITIS 工程结构:两个 domain 怎么建
在 VITIS 里做 AMP,核心操作是给同一个 platform 建两个 application domain,分别对应 CPU0 和 CPU1。常见做法是:
# 在 VITIS 的 XSCT 控制台里创建 platform 和两个 domain # 假设 platform 已经基于 XSA 建好 # 创建 CPU0 的 domain(默认就是 psu_cortexa53_0) set domain0 [create_domain -name domain_cpu0 -cpu psu_cortexa53_0 -os standalone] # 创建 CPU1 的 domain set domain1 [create_domain -name domain_cpu1 -cpu psu_cortexa53_1 -os standalone]逻辑说明:每个 domain 绑定一个具体的 CPU 核,standalone 表示裸机。两个 domain 共享同一个 hardware platform(同一个 XSA),但各自编译出独立的 ELF。参数上要注意-cpu必须写完整的核名,XCZU2EG 的 A53 核在 VITIS 里通常显示为psu_cortexa53_0和psu_cortexa53_1,写错了 domain 建不出来。
建完 domain 后,每个 domain 下建一个 application project,CPU0 的工程里放主控代码,CPU1 的工程里放从核代码。两个工程可以引用同一份共享头文件,但链接脚本要分开,因为两个核的运行地址不能重叠。
提示:VITIS 建 AMP 工程时,platform 的 BSP 里要确认
psu_cortexa53_0和psu_cortexa53_1都使能了,有些默认配置只开了 CPU0。
3. 双核启动与共享内存:地址分配和中断路由
3.1 CPU1 的启动流程:从 CPU0 手里放核
A53 上电后默认只有 CPU0 在跑,CPU1 处于待机状态。AMP 模式下,CPU1 的启动由 CPU0 负责「放核」。典型流程是:
// CPU0 侧:把 CPU1 的入口地址写到复位向量寄存器,然后触发 CPU1 启动 #include "xil_io.h" #define CPU1_ENTRY_ADDR 0x20000000UL // CPU1 程序入口,按实际链接地址改 #define RST_LPD_TOP 0xFF5E0000UL // LPD 复位控制寄存器基址 #define CRP_RST_CPU1 0x00000200UL // CPU1 复位控制位,具体位号查手册 // 把 CPU1 入口地址写入对应的复位向量寄存器 Xil_Out32(0xFF5E0000 + 0x00000040, CPU1_ENTRY_ADDR); // 释放 CPU1 复位 Xil_Out32(RST_LPD_TOP + 0x00000000, CRP_RST_CPU1);逻辑说明:CPU1 的入口地址必须和 CPU1 工程链接脚本里的起始地址一致,否则 CPU1 跑飞。参数上,CPU1_ENTRY_ADDR通常放在 DDR 的低地址段,避开 CPU0 的代码段和数据段。复位寄存器的具体偏移和位号在不同 MPSoC 型号上可能有差异,XCZU2EG 的 LPD 复位控制寄存器需要对照 TRM 确认,不能照抄别的型号。
CPU1 侧的代码开头要有一段「等待自己真正被放行」的逻辑,常见做法是轮询一个共享内存里的标志位,CPU0 写完标志位再放核,避免 CPU1 提前跑起来读到未初始化的数据。
3.2 共享内存的地址划分:别让两个核踩同一块 DDR
AMP 最怕的就是两个核的链接脚本把同一段 DDR 都划给自己。常见做法是在 DDR 里划出三段:CPU0 独占区、CPU1 独占区、共享区。共享区用volatile指针访问,或者用Xil_DCacheFlush手动刷缓存。
// 共享内存结构体定义,放在共享区起始地址 #define SHARED_BASE 0x1FF00000UL #define SHARED_SIZE 0x00100000UL // 1MB 共享区 typedef struct { volatile u32 flag_cpu0_ready; volatile u32 flag_cpu1_ready; volatile u32 cmd; volatile u32 data[256]; } shared_region_t; // 两个核都用这个指针访问共享区 shared_region_t *shr = (shared_region_t *)SHARED_BASE;逻辑说明:共享区的地址必须在两个核的链接脚本里都标记为「不可分配」,否则链接器可能把普通变量放到共享区里,造成冲突。参数上,SHARED_SIZE按实际通信数据量定,1MB 对大多数控制类通信够用。缓存一致性是 AMP 裸机的经典坑:A53 的 D-Cache 默认开着,CPU0 写共享内存后 CPU1 可能读到旧值,解决办法是共享区配置为 non-cacheable,或者每次读写前后手动 flush/invalidate。
3.3 核间中断:用 IPI 还是共享内存轮询
核间通信有两种常见方式:一种是共享内存加轮询标志位,另一种是用 IPI(Inter-Processor Interrupt)中断。轮询方式简单,但 CPU 空转浪费算力;IPI 方式实时性好,但配置复杂。
// 使用 IPI 触发 CPU1 中断的简化示例 #include "xipipsu.h" // 初始化 IPI 驱动,CPU0 作为发送方 XIpiPsu_Config *ipi_cfg = XIpiPsu_LookupConfig(XPAR_XIPIPSU_0_DEVICE_ID); XIpiPsu ipi_inst; XIpiPsu_CfgInitialize(&ipi_inst, ipi_cfg, ipi_cfg->BaseAddress); // 向 CPU1 发送 IPI 中断 u32 payload = 0x01; XIpiPsu_TriggerIpi(&ipi_inst, CPU1_MASK, payload);逻辑说明:IPI 的 target mask 要写对,XCZU2EG 的 IPI 通道和 CPU 的对应关系在 xparameters.h 里有定义。payload 可以传一个简单的命令字,复杂数据还是走共享内存。参数上,IPI 的中断号在 BSP 里要确认已经使能,否则触发后 CPU1 收不到。
注意:IPI 中断服务函数里不要做耗时操作,只置标志位,主循环里再处理数据。
4. 避坑排查:双核 AMP 调试中最容易翻车的五件事
4.1 现象:CPU1 跑飞,PC 停在 0x0 或随机地址
原因通常是 CPU1 的入口地址写错了,或者 CPU1 的 ELF 没有正确加载到 DDR。VITIS 调试时,两个核的 ELF 要分别 load,如果只 load 了 CPU0 的,CPU1 的代码段是空的。
解决:在 VITIS 的 Debug Configuration 里确认两个 application 都勾选了「Load」,并且 CPU1 的入口地址和链接脚本里的_start地址一致。可以在 CPU0 放核前先读一下 CPU1 入口地址处的指令,确认不是 0x00000000。
4.2 现象:共享内存数据错乱,CPU0 写 1 读到 0
原因是 D-Cache 没刷。A53 的缓存是按 cache line 操作的,CPU0 写共享内存后数据可能还在 cache 里没落到 DDR,CPU1 直接读 DDR 读到旧值。
解决:共享区配置为 non-cacheable(在 MMU 配置里把该段设为 Device 或 Normal Non-Cacheable),或者每次写共享内存后调用Xil_DCacheFlushRange,读之前调用Xil_DCacheInvalidateRange。血泪经验是:调试阶段先全设 non-cacheable,跑通了再优化性能。
4.3 现象:两个核抢 UART,输出乱码或丢字符
原因是两个核的 BSP 里都使能了同一个 UART 的驱动,各自初始化了一遍,寄存器被覆盖。
解决:AMP 模式下外设要明确归属。常见做法是 UART 归 CPU0 管,CPU1 的打印通过共享内存传给 CPU0 代打。如果非要两个核都用 UART,得用不同的 UART 控制器,不能共用一个。
4.4 现象:VITIS 编译报 collect2.exe 错误
这是 VITIS 在 Windows 下常见的链接错误,通常跟路径里有中文或空格、或者链接脚本里内存段溢出有关。
解决:工程路径全英文无空格;检查两个核的链接脚本,确认 DDR 地址段没有重叠;如果用了共享库,确认库的搜索路径在 BSP 设置里配对了。这个错误信息很泛,实际原因要看 VITIS 的 console 完整输出,别只看最后一行。
4.5 现象:CPU1 能跑但中断进不去
原因是 GIC(通用中断控制器)的配置没做。A53 的中断要经过 GIC 分发,CPU1 的中断使能和优先级配置如果没做,IPI 触发了也进不了 ISR。
解决:在 CPU1 的初始化代码里使能 GIC 的 CPU interface,配置 IPI 对应的中断号和优先级。XCZU2EG 的 GIC 配置在 BSP 里通常有现成的XScuGic驱动,照着单核中断的配置改一下 CPU ID 就行。
5. 进阶验证:用共享内存做双核数据吞吐测试
跑通基本 AMP 后,怎么确认两个核真的在并行工作、通信带宽够不够?我一般会写一个简单的吞吐测试:CPU0 往共享内存里连续写递增数据,CPU1 读出来校验,统计单位时间内的传输量。
// CPU0 侧:连续写共享内存,每写一块就置标志位 #define BLOCK_SIZE 1024 u32 *tx_buf = (u32 *)(SHARED_BASE + 0x1000); for (u32 i = 0; i < BLOCK_SIZE; i++) { tx_buf[i] = i; } shr->flag_cpu0_ready = 1; // 通知 CPU1 数据就绪 while (shr->flag_cpu1_ready == 0); // 等 CPU1 校验完 shr->flag_cpu1_ready = 0;// CPU1 侧:轮询标志位,读到数据后校验 u32 *rx_buf = (u32 *)(SHARED_BASE + 0x1000); while (1) { if (shr->flag_cpu0_ready) { u32 err = 0; for (u32 i = 0; i < BLOCK_SIZE; i++) { if (rx_buf[i] != i) err++; } shr->flag_cpu0_ready = 0; shr->flag_cpu1_ready = 1; // 告诉 CPU0 校验完成 } }逻辑说明:这个测试能同时验证共享内存的可访问性、缓存一致性、以及两个核的同步逻辑。参数上,BLOCK_SIZE可以逐步加大,观察传输时间的变化,找到缓存和 DDR 带宽的拐点。如果校验错误数不为零,先查缓存配置,再查地址是否对齐。
验证通过后,可以把轮询改成 IPI 中断触发,对比两种方式的延迟差异。我自己的习惯是:每次改完共享内存的地址分配或缓存策略,都强制跑一遍这个吞吐测试,确认没有回归。从那以后,双核工程里再也没出现过「单核能跑、双核就挂」的玄学问题。
希望帮到你。
本文还有配套的精品资源,点击获取