← ブログ一覧へ
FPGAZynqOpenAMPRPMsgremoteprocFreeRTOS多核AMPLinuxPetaLinux

Zynq 实战 18|OpenAMP 多核:Linux + 裸机同时跑(双核非对称)

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →

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 库

做完后你会有:

  1. 一个从 Linux 侧用 remoteproc 热加载裸机 elf 到 CPU1 并启动的工程
  2. 一个 RPMsg echo round-trip,实测延迟 < 4 µs(实测 3.7 µs @ 666 MHz A9)
  3. 一个可以直接用来做电机控制 / CAN 实时任务的裸机模板

本篇不覆盖:SMP(两核都跑 Linux)、PMU 性能计数器、OpenCL on PL——那些是独立话题。


1. AMP 架构概念:SMP 与 AMP 的本质区别

Zynq 的两颗 A9 在硅片层面是完全对称的(共享 L2 cache、共享 DDR controller),但软件上怎么用是另一回事:

方案CPU0CPU1共享资源实时性
SMPLinux 调度器统一管理← 同左同一内核镜像Linux 调度延迟 ≈ 100 µs
AMP(本篇)LinuxFreeRTOS / 裸机约定好的共享内存CPU1 裸机 < 10 µs
lockstep(安全)主核冗余镜像不适用

AMP(Asymmetric Multi-Processing)的核心约定:CPU1 从 BootROM 出来后直接跳到裸机 elf 的入口,或者由 Linux 侧 remoteproc 动态加载。本篇选后者,因为可以在 Linux 运行时热加载新裸机固件,调试方便。

Zynq-7000 AMP 系统架构:OpenAMP 通信栈 CPU0 — Linux (PetaLinux 2023.2) 用户态:rpmsg_echo_client.c rpmsg_char 驱动 /dev/rpmsg_ctrl0 remoteproc 驱动 /sys/class/remoteproc OpenAMP 库(virtio / VirtQueue) libmetal + libopen_amp Linux 内核 remoteproc / virtio 框架 rproc_ops → zynq_rproc.c DDR — CPU0 空间:0x0000_0000 ~ 0x1FFF_FFFF Linux 内核 + rootfs(512 MB) CPU1 — FreeRTOS / 裸机 裸机应用:echo_server_main.c RPMsg / VirtIO(OpenAMP 库) rpmsg_send / rpmsg_recv callback libmetal(裸机 HAL) metal_io_map / metal_irq FreeRTOS 内核(可选) 任务调度 / 中断管理 DDR — CPU1 空间:0x2000_0000 ~ 0x2FFF_FFFF 裸机代码 + FreeRTOS heap(256 MB) 共享内存:0x3000_0000 ~ 0x3FFF_FFFF(256 MB) VirtIO vring(RPMsg 消息队列)+ 应用层共享数据区 设备树 reserved-memory 保留,Linux 不分配此区域 RPMsg SGI #0
图 1. Zynq-7000 AMP 软件架构:OpenAMP 三层结构(VirtIO / VirtQueue / RPMsg)

2. OpenAMP 三层关系:VirtIO / VirtQueue / RPMsg

OpenAMP 不是一个单一库,它是三层协议的组合:

名称作用类比
传输层VirtIO + VirtQueue双向环形队列,管理消息描述符(descriptor ring)以太网的 TX/RX ring buffer
消息层RPMsg在 VirtQueue 上加端点(endpoint)和地址(src/dst)UDP socket
硬件抽象libmetal内存映射、中断、原子操作的跨平台 HALPOSIX 之于 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_FFFF512 MBLinux 内核 + rootfs + 用户进程
CPU1 段0x2000_0000 ~ 0x2FFF_FFFF256 MB裸机 elf 加载区 + FreeRTOS heap
共享段0x3000_0000 ~ 0x3FFF_FFFF256 MBVirtIO 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):

  1. File → New → Application Project
  2. Platform:选已有 xsa 或新建
  3. Processor:选 ps7_cortexa9_1CPU1
  4. 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 µs2.9 µs12.1 µs~2.2 MB/s
64 字节4.1 µs3.3 µs14.5 µs~15.6 MB/s
496 字节(最大)6.8 µs5.7 µs22.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_OFFSETXSCUGIC_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_client round-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 锁定后怎么办

参考资料

文档号名称用途
UG585Zynq-7000 SoC TRM第 3 章:SCU / GIC 架构,SGI 寄存器 GICD_SGIR
UG585Zynq-7000 SoC TRM附录 B:内存地址映射,DDR 控制器地址范围
XAPP1079Zynq-7000 AMP 应用笔记AMP 内存划分、remoteproc 设备树配置参考
OpenAMPopenamp-project/openampRPMsg / VirtIO 源码,含 Zynq platform_info.c 示例
libmetalOpenAMP/libmetal裸机 HAL,TLB 属性设置、原子操作
Linux kerneldrivers/remoteproc/zynq_remoteproc.cLinux 侧 remoteproc 驱动,了解加载 elf 流程
PG153AXI Interrupt Controller Product GuidePL → PS 中断连接,可与 SGI 协同用于复杂通知场景

这是《Zynq FPGA 嵌入式系统设计实战》系列第 18 篇。 如果你在 vring 死锁、SGI 不响应或 FreeRTOS 优先级冲突上踩了坑,欢迎留言。