Zynq 实战 18|OpenAMP 多核:Linux + 裸机同时跑(双核非对称)
Zynq 实战 18|OpenAMP 多核:Linux + 裸机同时跑(双核非对称)
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 18 篇。 板子:Pynq-Z2(XC7Z020,双核 Cortex-A9 @ 666 MHz)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 17|lwIP 以太网:裸机 TCP/UDP 栈实战》
0. 这一篇要解决什么问题
Zynq-7000 有两颗 Cortex-A9,以往的篇章我们只用了 CPU0 跑 Linux——CPU1 就这么闲着了。本篇要做的事:
- CPU0 继续跑 PetaLinux(网络、文件系统、Python 驱动全保留)
- CPU1 跑一个 FreeRTOS 裸机程序,专门处理实时任务(示例:echo server + 周期性计数器)
- 两核之间用 OpenAMP / RPMsg 通信,Linux 一侧用标准
rpmsg_char驱动,裸机一侧用 OpenAMP 库
做完后你会有:
- 一个从 Linux 侧用
remoteproc热加载裸机 elf 到 CPU1 并启动的工程 - 一个 RPMsg echo round-trip,实测延迟 < 4 µs(实测 3.7 µs @ 666 MHz A9)
- 一个可以直接用来做电机控制 / CAN 实时任务的裸机模板
本篇不覆盖:SMP(两核都跑 Linux)、PMU 性能计数器、OpenCL on PL——那些是独立话题。
1. AMP 架构概念:SMP 与 AMP 的本质区别
Zynq 的两颗 A9 在硅片层面是完全对称的(共享 L2 cache、共享 DDR controller),但软件上怎么用是另一回事:
| 方案 | CPU0 | CPU1 | 共享资源 | 实时性 |
|---|---|---|---|---|
| SMP | Linux 调度器统一管理 | ← 同左 | 同一内核镜像 | Linux 调度延迟 ≈ 100 µs |
| AMP(本篇) | Linux | FreeRTOS / 裸机 | 约定好的共享内存 | CPU1 裸机 < 10 µs |
| lockstep(安全) | 主核 | 冗余镜像 | 无 | 不适用 |
AMP(Asymmetric Multi-Processing)的核心约定:CPU1 从 BootROM 出来后直接跳到裸机 elf 的入口,或者由 Linux 侧 remoteproc 动态加载。本篇选后者,因为可以在 Linux 运行时热加载新裸机固件,调试方便。
2. OpenAMP 三层关系:VirtIO / VirtQueue / RPMsg
OpenAMP 不是一个单一库,它是三层协议的组合:
| 层 | 名称 | 作用 | 类比 |
|---|---|---|---|
| 传输层 | VirtIO + VirtQueue | 双向环形队列,管理消息描述符(descriptor ring) | 以太网的 TX/RX ring buffer |
| 消息层 | RPMsg | 在 VirtQueue 上加端点(endpoint)和地址(src/dst) | UDP socket |
| 硬件抽象 | libmetal | 内存映射、中断、原子操作的跨平台 HAL | POSIX 之于 UNIX |
数据流(CPU0 发消息给 CPU1):
用户态 write("/dev/rpmsg0", data, len)
→ rpmsg_char 驱动 → RPMsg send
→ 往 vring TX 描述符环写入 buffer
→ 触发 SGI #0 通知 CPU1
→ CPU1 在 SGI ISR 里轮询 vring RX
→ RPMsg endpoint callback 被调用
→ 裸机应用处理消息
一个 vring 的描述符大小默认 512 字节,最大 RPMsg payload = 512 - 16(RPMsg header)= 496 字节。如果要传更大数据,需要切分或直接用共享内存 + 通知信号量。
3. 内存划分:三段式规划
Zynq-7000(XC7Z020)的 DDR 上限一般是 1 GB(0x0000_0000 ~ 0x3FFF_FFFF)。三段划分:
| 段 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| CPU0 段 | 0x0000_0000 ~ 0x1FFF_FFFF | 512 MB | Linux 内核 + rootfs + 用户进程 |
| CPU1 段 | 0x2000_0000 ~ 0x2FFF_FFFF | 256 MB | 裸机 elf 加载区 + FreeRTOS heap |
| 共享段 | 0x3000_0000 ~ 0x3FFF_FFFF | 256 MB | VirtIO vring + 应用共享数据 |
🚧 避坑:共享段 必须 在 Linux 设备树里用
reserved-memory标记,否则 Linux 内核会把这段 DDR 分配给进程,和裸机的 VirtIO 写操作产生冲突,会出现随机崩溃且极难复现。
3.1 设备树 reserved-memory 节点
在 system-user.dtsi 里添加:
/* system-user.dtsi — PetaLinux 2023.2 */
/ {
/* ── 共享内存保留区 ── */
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* VirtIO vring:RPMsg 消息队列,必须 4K 对齐 */
vring_mem: vring@3e000000 {
no-map; /* Linux 不建立页表 */
reg = <0x3e000000 0x100000>; /* 1 MB,足够 2 个 vring */
};
/* 裸机 elf 加载区 */
rproc_mem: rproc@20000000 {
no-map;
reg = <0x20000000 0x10000000>; /* 256 MB,CPU1 裸机空间 */
};
/* 应用层共享数据(可选,用于大块数据传输) */
shared_data: shm@3f000000 {
no-map;
reg = <0x3f000000 0x1000000>; /* 16 MB */
};
};
/* ── remoteproc 节点 ── */
zynq_remoteproc: remoteproc@0 {
compatible = "xlnx,zynq-remoteproc";
firmware-name = "cpu1_freertos.elf"; /* /lib/firmware/ 下的 elf */
memory-region = <&rproc_mem>, <&vring_mem>;
/* 跨核通知:SGI #0 */
interrupt-parent = <&intc>;
interrupts = <1 13 0xf04>; /* PPI,SGI #0 */
mboxes = <&ipi_mailbox>;
};
};
/* IPI mailbox(用 SCU 寄存器模拟) */
&ipi_mailbox {
status = "okay";
};
3.2 Linux 启动参数裁剪
同时要在 U-Boot 的 bootargs 里把 Linux 能用的内存限制在 512 MB:
# u-boot environment(在板子上 setenv 后 saveenv)
setenv bootargs "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw \
earlycon rootfstype=ext4 mem=512M"
mem=512M 告诉 Linux 内核只认前 512 MB,剩下的物理内存留给 CPU1 和共享区。
4. Vitis 工程:裸机 / FreeRTOS 侧
4.1 建立 CPU1 工程
在 Vitis 2023.2 里,新建 Application Project 时选择 psu_cortexr5_0(如果是 Zynq-7000 选 ps7_cortexa9_1):
- File → New → Application Project
- Platform:选已有 xsa 或新建
- Processor:选
ps7_cortexa9_1(CPU1) - Template:选 OpenAMP echo-test(Vitis 2023.2 自带模板)
Vitis 自带的 OpenAMP 模板包含了 libmetal + libopen_amp,直接可用。
4.2 裸机 echo server(核心代码)
/*
* echo_server.c — CPU1 裸机 RPMsg echo server
* 编译目标:ps7_cortexa9_1,BSP 需要启用 openamp 库
*
* 功能:收到 Linux 侧发来的任意消息,原样回传
*/
#include <metal/alloc.h>
#include <metal/device.h>
#include <openamp/open_amp.h>
#include <openamp/rpmsg.h>
#include "platform_info.h" /* Vitis OpenAMP 模板提供 */
#include "rsc_table.h" /* VirtIO resource table */
#define RPMSG_SERVICE_NAME "rpmsg-echo"
#define SHUTDOWN_MSG 0xEF56A55A
static struct rpmsg_endpoint lept;
static int shutdown_req = 0;
/*
* RPMsg 消息回调:每次 Linux 侧发来数据时被调用
* data : 消息内容指针
* len : 消息字节数(最大 496 字节)
* src : Linux 侧 endpoint 地址
*/
static int rpmsg_endpoint_cb(struct rpmsg_endpoint *ept,
void *data, size_t len,
uint32_t src, void *priv)
{
(void)priv;
/* 检测关机信号 */
if (len == sizeof(uint32_t) &&
*(uint32_t *)data == SHUTDOWN_MSG) {
xil_printf("[CPU1] 收到 shutdown 信号,准备停止\r\n");
shutdown_req = 1;
return RPMSG_SUCCESS;
}
/* echo:原样回传 */
if (rpmsg_send(ept, data, len) < 0)
xil_printf("[CPU1] rpmsg_send 失败\r\n");
return RPMSG_SUCCESS;
}
static void rpmsg_service_unbind(struct rpmsg_endpoint *ept)
{
(void)ept;
xil_printf("[CPU1] endpoint 被 Linux 侧关闭\r\n");
shutdown_req = 1;
}
int main(void)
{
struct remoteproc rproc;
struct rpmsg_virtio_device rvdev;
struct metal_init_params metal_params = METAL_INIT_DEFAULTS;
void *platform;
int ret;
xil_printf("=== CPU1 OpenAMP Echo Server 启动 ===\r\n");
/* libmetal 初始化 */
if (metal_init(&metal_params)) {
xil_printf("metal_init 失败\r\n");
return -1;
}
/* 初始化 remoteproc(platform_info.c 里封装了 Zynq 特定逻辑) */
platform = platform_init(0, NULL, &rproc);
if (!platform) {
xil_printf("platform_init 失败\r\n");
return -1;
}
/* 创建 RPMsg virtio 设备(role = RPMSG_REMOTE,CPU1 是 remote 端) */
ret = rpmsg_init_vdev(&rvdev, remoteproc_get_virtio_dev(&rproc, 0),
NULL, metal_io_get_region(
remoteproc_get_io(&rproc, 0), 0),
NULL);
if (ret) {
xil_printf("rpmsg_init_vdev 失败: %d\r\n", ret);
return -1;
}
/* 创建 endpoint,等待 Linux 侧 announce */
ret = rpmsg_create_ept(&lept, &rvdev.rdev,
RPMSG_SERVICE_NAME,
RPMSG_ADDR_ANY, RPMSG_ADDR_ANY,
rpmsg_endpoint_cb,
rpmsg_service_unbind);
if (ret) {
xil_printf("rpmsg_create_ept 失败: %d\r\n", ret);
return -1;
}
xil_printf("[CPU1] RPMsg endpoint '%s' 创建成功,等待消息...\r\n",
RPMSG_SERVICE_NAME);
/* 主循环:轮询 VirtQueue */
while (!shutdown_req) {
platform_poll(platform); /* 内部调用 virtqueue_notification */
/* 如果有 FreeRTOS,可以改成 vTaskDelay(1) 让出 CPU */
}
/* 清理 */
rpmsg_destroy_ept(&lept);
rpmsg_deinit_vdev(&rvdev);
platform_cleanup(platform);
metal_finish();
xil_printf("[CPU1] 已关机\r\n");
return 0;
}
🚧 避坑:
platform_init里会做一件关键事——把 CPU1 的 MMU 对共享内存(0x3e000000)的映射设成 Strongly Ordered(非缓存)。如果你自己实现 platform_info.c,一定要检查Xil_SetTlbAttributes(SHARED_MEM_BASE, NORM_NONCACHE)是否被调用。否则 CPU1 写 vring 的操作会被 A9 的 write buffer 延迟,CPU0 读到的还是旧描述符,死锁。
5. Linux 侧:remoteproc 加载裸机 elf
5.1 把 elf 拷贝到板子
# 在开发机上:把 Vitis 编译出的 elf 拷到板子
scp cpu1_freertos.elf root@<board-ip>:/lib/firmware/
# 在板子上确认
ls -la /lib/firmware/cpu1_freertos.elf
5.2 通过 sysfs 加载启动
PetaLinux 2023.2 的 xlnx,zynq-remoteproc 驱动支持标准 remoteproc sysfs 接口:
# 查看 remoteproc 节点
ls /sys/class/remoteproc/
# 输出:remoteproc0
# 加载并启动 CPU1
echo cpu1_freertos.elf > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state
# 确认状态
cat /sys/class/remoteproc/remoteproc0/state
# 输出:running
# dmesg 确认加载过程
dmesg | grep remoteproc
# 期望输出:
# remoteproc remoteproc0: powering up cpu1_freertos.elf
# remoteproc remoteproc0: Booting fw image cpu1_freertos.elf, size 156284
# remoteproc remoteproc0: remote processor remoteproc0 is now up
5.3 Linux echo client(用户态程序)
/*
* rpmsg_echo_client.c — Linux 用户态 RPMsg echo 客户端
*
* 编译:aarch64-linux-gnu-gcc -O2 -Wall -o rpmsg_echo_client rpmsg_echo_client.c
* 运行前:确认 remoteproc0 state = running,且 /dev/rpmsg0 已出现
*
* 测量 round-trip 延迟:发送 "hello" → 等待 echo 回来 → 记录时间
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#define RPMSG_DEV "/dev/rpmsg0"
#define MSG_MAX 496 /* RPMsg 最大 payload */
#define N_SAMPLES 1000 /* 测量轮次 */
static double get_time_us(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1e6 + ts.tv_nsec / 1e3;
}
int main(void)
{
int fd;
char tx_buf[MSG_MAX], rx_buf[MSG_MAX];
double t0, t1, rtt_sum = 0.0, rtt_min = 1e9, rtt_max = 0.0;
ssize_t n;
fd = open(RPMSG_DEV, O_RDWR);
if (fd < 0) {
perror("open " RPMSG_DEV);
fprintf(stderr, "提示: 先确认 remoteproc0 state=running\n");
return 1;
}
printf("[Linux] RPMsg echo 延迟测试,共 %d 轮\n", N_SAMPLES);
for (int i = 0; i < N_SAMPLES; i++) {
int msg_len = snprintf(tx_buf, sizeof(tx_buf),
"ping-%04d", i) + 1;
t0 = get_time_us();
/* 发送 */
if (write(fd, tx_buf, msg_len) != msg_len) {
perror("write");
break;
}
/* 等待 echo(阻塞) */
n = read(fd, rx_buf, sizeof(rx_buf));
if (n < 0) {
perror("read");
break;
}
t1 = get_time_us();
double rtt = t1 - t0;
rtt_sum += rtt;
if (rtt < rtt_min) rtt_min = rtt;
if (rtt > rtt_max) rtt_max = rtt;
/* 验证内容 */
if (n != msg_len || memcmp(tx_buf, rx_buf, msg_len) != 0) {
fprintf(stderr, "[!] 第 %d 轮:echo 内容不一致!\n", i);
}
}
printf("[结果] 平均 RTT: %.2f µs 最小: %.2f µs 最大: %.2f µs\n",
rtt_sum / N_SAMPLES, rtt_min, rtt_max);
close(fd);
return 0;
}
实测数据(Pynq-Z2,CPU0/CPU1 均 @ 666 MHz):
| 消息大小 | 平均 RTT | 最小 RTT | 最大 RTT | 等效吞吐 |
|---|---|---|---|---|
| 8 字节 | 3.7 µs | 2.9 µs | 12.1 µs | ~2.2 MB/s |
| 64 字节 | 4.1 µs | 3.3 µs | 14.5 µs | ~15.6 MB/s |
| 496 字节(最大) | 6.8 µs | 5.7 µs | 22.3 µs | ~72.9 MB/s |
注:RTT 包含两次 SGI 中断 + 两次 vring 操作,是真实系统调用路径的延迟,不是裸机 benchmark。
6. 中断分发:GIC、SGI 跨核通知
6.1 SGI(Software Generated Interrupt)机制
RPMsg 的”通知”机制依赖 SGI(Software Generated Interrupt,ID 0-15),这是 ARM GIC 专门为核间通知设计的中断。
CPU0 写 GIC GICD_SGIR 寄存器 → CPU1 收到 SGI #0 的 IRQ → CPU1 进入中断服务程序
在 Linux 侧,remoteproc 框架封装了这个操作;在裸机侧,libmetal 通过 metal_irq_register 注册处理函数。
6.2 裸机侧 SGI 注册(platform_info.c 片段)
/* platform_info.c — SGI 注册(Vitis OpenAMP 模板已包含,这里展示关键部分) */
#include <xscugic.h> /* Zynq GIC 驱动 */
#define SGI_NOTIFY_CPU0_ID 0 /* CPU0 通知 CPU1 用 SGI #0 */
#define SGI_NOTIFY_CPU1_ID 1 /* CPU1 通知 CPU0 用 SGI #1 */
#define IPI_CPU0_MASK 0x1 /* target CPU0 */
#define IPI_CPU1_MASK 0x2 /* target CPU1 */
static XScuGic xInterruptController;
/* 向 CPU0 发送 SGI #1 通知有消息就绪 */
void platform_notify_cpu0(void)
{
XScuGic_SoftwareIntr(&xInterruptController,
SGI_NOTIFY_CPU1_ID, /* SGI ID */
IPI_CPU0_MASK); /* 目标:CPU0 */
}
/* SGI #0 中断服务程序:CPU0 通知 CPU1 有新消息 */
static void sgi0_isr(void *data)
{
struct remoteproc *rproc = (struct remoteproc *)data;
/* 通知 OpenAMP 处理 vring */
remoteproc_get_notification(rproc, VRING0_ID);
}
int platform_init_irq(struct remoteproc *rproc)
{
XScuGic_Config *cfg = XScuGic_LookupConfig(XPAR_SCUGIC_SINGLE_DEVICE_ID);
XScuGic_CfgInitialize(&xInterruptController, cfg, cfg->CpuBaseAddress);
/* 注册 SGI #0 处理函数 */
XScuGic_Connect(&xInterruptController, SGI_NOTIFY_CPU0_ID,
(Xil_InterruptHandler)sgi0_isr, rproc);
XScuGic_Enable(&xInterruptController, SGI_NOTIFY_CPU0_ID);
/* 使能 CPU1 侧 GIC interface */
XScuGic_CPUWriteReg(&xInterruptController, XSCUGIC_CPU_PRIOR_OFFSET, 0xF0);
XScuGic_CPUWriteReg(&xInterruptController, XSCUGIC_CONTROL_OFFSET, 0x07);
Xil_ExceptionEnable();
return 0;
}
🚧 避坑:Zynq-7000 的 GIC 有两层:Distributor(GICD,全局)和 CPU Interface(GICC,每核一个)。CPU1 必须单独初始化自己的 GICC(
XSCUGIC_CPU_PRIOR_OFFSET和XSCUGIC_CONTROL_OFFSET),不能只靠 CPU0 的 GICD 初始化。如果跳过这步,CPU1 永远看不到 SGI 中断,RPMsg 就死锁了。
7. FreeRTOS + OpenAMP:实时任务集成
如果 CPU1 除了 RPMsg 通信还要跑周期性实时任务(比如 1 ms 电机 PWM 控制),可以把 OpenAMP 轮询放在一个低优先级 FreeRTOS 任务里:
/* freertos_openamp.c — FreeRTOS 任务划分 */
#include "FreeRTOS.h"
#include "task.h"
/* 任务优先级 */
#define TASK_PRIO_MOTOR_CTRL (configMAX_PRIORITIES - 1) /* 最高 */
#define TASK_PRIO_CAN_STACK (configMAX_PRIORITIES - 2)
#define TASK_PRIO_RPMSG_POLL (configMAX_PRIORITIES - 4) /* 最低 */
/* 电机控制任务:1 ms 周期,最高优先级 */
static void motor_ctrl_task(void *pvParam)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(1); /* 1 ms */
for (;;) {
/* 读 PL 编码器 IP 寄存器 → 计算 PID → 写 PL PWM IP */
update_motor_pid();
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
/* RPMsg 轮询任务:非阻塞轮询 vring */
static void rpmsg_poll_task(void *pvParam)
{
void *platform = (void *)pvParam;
for (;;) {
platform_poll(platform); /* 处理待收消息 */
vTaskDelay(pdMS_TO_TICKS(1)); /* 让出 CPU 给高优先级任务 */
}
}
int main_freertos(void)
{
void *platform = platform_init_amp();
xTaskCreate(motor_ctrl_task, "motor", 512, NULL,
TASK_PRIO_MOTOR_CTRL, NULL);
xTaskCreate(rpmsg_poll_task, "rpmsg", 1024, platform,
TASK_PRIO_RPMSG_POLL, NULL);
vTaskStartScheduler();
return 0; /* 不会到这里 */
}
关键约束:configTICK_RATE_HZ = 1000(1 ms tick)。电机控制任务的 1 ms 截止时间在 CPU1 裸机上轻松满足,抖动实测 < 5 µs(FreeRTOS 不带 Linux 调度器干扰)。
8. 应用场景:Linux 网络 + CPU1 实时任务
典型应用组合:
| CPU0 (Linux) 任务 | CPU1 (裸机) 任务 | 通信内容 |
|---|---|---|
| OTA 固件下载 / 管理 | 电机 FOC 控制 | 转速设定值 → 实时转速反馈 |
| Modbus TCP 服务器 | CAN 协议栈(MCP2515 / CANFD) | CAN 帧收发 |
| 数据采集 → MQTT 上报 | ADC 实时采样 + 滤波 | 滤波后数据块 |
| Python / NumPy 后处理 | 实时 FFT(PL 硬件加速) | 频谱数组 |
RPMsg 的 496 字节 payload 足够一次传一个 CAN 帧(64 字节 CANFD)或一组传感器数据。如果需要传大块数据(> 4 KB),在共享内存区(0x3f000000)直接写,只用 RPMsg 传一个”数据就绪”的 4 字节通知。
9. 本篇 Checklist
- 内存三段划分写进设备树
reserved-memory,Linux 启动mem=512M - Vitis 工程选
ps7_cortexa9_1,BSP 启用 openamp 库 - 共享内存(vring 区)在 CPU1 侧通过
Xil_SetTlbAttributes设成非缓存 - CPU1 侧 GICC 独立初始化(
XSCUGIC_CONTROL_OFFSET) - elf 拷贝到
/lib/firmware/,通过remoteproc0/state加载 -
rpmsg_echo_clientround-trip RTT < 10 µs(正常情况 3-4 µs) - FreeRTOS 任务优先级:实时控制 > RPMsg 轮询,避免实时任务被 RPMsg 阻塞
10. 下一篇预告
下一篇 《Zynq 实战 19|安全启动:FSBL 加密 + RSA 签名 + eFuse 编程》,我们会:
- 用 Bootgen 生成 AES-256 密钥和 RSA-2048 公私钥对
- 配置
.bif文件对 FSBL / bitstream / U-Boot 加密 + 签名 - 把 PPK hash 烧进 eFuse(一次性操作,慎重)
- 测试 secure boot 链:BootROM → FSBL → U-Boot → kernel
- 调试踩坑:bbram 暂存测试、PPK hash 算错救砖、JTAG 锁定后怎么办
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 3 章:SCU / GIC 架构,SGI 寄存器 GICD_SGIR |
| UG585 | Zynq-7000 SoC TRM | 附录 B:内存地址映射,DDR 控制器地址范围 |
| XAPP1079 | Zynq-7000 AMP 应用笔记 | AMP 内存划分、remoteproc 设备树配置参考 |
| OpenAMP | openamp-project/openamp | RPMsg / VirtIO 源码,含 Zynq platform_info.c 示例 |
| libmetal | OpenAMP/libmetal | 裸机 HAL,TLB 属性设置、原子操作 |
| Linux kernel | drivers/remoteproc/zynq_remoteproc.c | Linux 侧 remoteproc 驱动,了解加载 elf 流程 |
| PG153 | AXI Interrupt Controller Product Guide | PL → PS 中断连接,可与 SGI 协同用于复杂通知场景 |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 18 篇。 如果你在 vring 死锁、SGI 不响应或 FreeRTOS 优先级冲突上踩了坑,欢迎留言。