Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions doc/renode-tier2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<!--
Copyright (c) 2026 Golioth, Inc.
SPDX-License-Identifier: Apache-2.0
-->

# Renode Tier-2: real remoteproc + virtio-rpmsg for the Pouch rpmsg transport

This directory holds the work-in-progress harness for validating the Pouch
**rpmsg (OpenAMP) transport** against a *real* Linux `remoteproc` +
`virtio-rpmsg` stack, with no hardware, using [Renode](https://renode.io).

It complements the in-repo native_sim tests (`tests/pouch/rpmsg/exchange`),
which validate the serial-core protocol + UART framing but cannot exercise the
`ipc_service`/OpenAMP adapter. This tier exercises the actual on-die rpmsg path.

> Status: **mechanism validated; full R5-side rpmsg deferred to hardware.**
> In Renode we have proven the real Linux `remoteproc` + `virtio-rpmsg` stack
> (the RFC's `/dev/rpmsg_ctrl0` interface) and worked out the complete Zephyr
> R5 firmware bring-up (below). The last step — a fault-free R5 OpenAMP endpoint
> announcing an rpmsg channel — is blocked by an emulator-model limitation (an
> IPI interrupt-storm/stall after `remoteproc start`), so the end-to-end Pouch
> round-trip over rpmsg is left to be finished on real MPU+MCU hardware, where
> the IPI is not a partial model. This cannot run in the pouch CI (no
> OpenAMP/ZynqMP there); it is validated locally in Renode.
>
> What this establishes: the Linux/gateway side of the RFC works exactly as
> designed against a real kernel rpmsg stack, and the Zephyr R5 firmware
> placement, IPM wiring, and MPU requirements for ZynqMP are documented and
> captured — a concrete head start for the hardware bring-up.

## Platform: AMD Kria KV260 (ZynqMP), Cortex-R5

Renode ships a ZynqMP OpenAMP demo (`scripts/single-node/zynqmp_openamp.resc`)
that boots Linux on the Cortex-A53 and drives an OpenAMP peer on the Cortex-R5
over real `remoteproc` + `virtio-rpmsg`. Zephyr's in-tree `kv260_r5` board
(SoC `zynqmp_rpu`) targets that same R5, so it is the vehicle here.

RZ/G2L (`rzg2l_openamp.resc` + Zephyr `rzg2l_smarc` cm33) is a second candidate
and an actual RFC target family; ZynqMP was chosen first because its Renode
demo is the most mature.

## What is validated

- **Renode v1.16.1** runs headless (portable, self-contained .NET).
- The shipped **`Should Run OpenAMP Echo Sample`** robot test passes — real
Linux `remoteproc` (`modprobe zynqmp_r5_remoteproc` → set firmware → `echo
start`) + `virtio_rpmsg_bus` round-trips payloads with the R5. The mechanism
works in Renode on this host.
- A Zephyr **`hello_world` built for `kv260_r5`** boots on the emulated R5
(`rpu0`) — see `smoke_r5_boot.robot`. Console is **uart1** on this board.
- The Zephyr **`openamp_rsc_table`** sample builds for `kv260_r5` using the
overlay/conf here.
- Linux **`remoteproc` loads and starts** that firmware on the R5
(`state` → `running`) — see `remoteproc_load.robot`. The firmware is injected
into a copy of the demo rootfs with `debugfs` (no root needed):

```sh
debugfs -w -R "write build/zephyr/zephyr_openamp_rsc_table.elf \
/lib/firmware/rpmsg-echo.out" rootfs.ext2
POUCH_ROOTFS=$PWD/rootfs.ext2 ./renode-test <pouch>/doc/renode-tier2/remoteproc_load.robot
```

### Firmware placement note (important)

The ZynqMP R5 **boots and executes from TCM** (reset vector at address `0`), not
from DDR. Firmware must therefore be **TCM-resident** — the known-good
`rpmsg-echo.out` links to `0x0` (ATCM) + `0x20000` (BTCM), ~87 KiB total, with
only its resource table in DDR. A firmware relinked entirely into the DDR
`rproc` carve-out loads (remoteproc reports `running`) but never executes — the
R5 boots into empty TCM and sits silent. Confirmed both ways: a TCM-linked
`hello_world` prints over remoteproc; a DDR-linked one is silent.

Consequently the overlay keeps the default `0x0` (TCM) link address and only
places the vrings/buffers in DDR (`zephyr,ipc_shm` @ `0x3ed40000`). The OpenAMP
sample must be trimmed to fit TCM: with `CONFIG_SHELL=n`, `CONFIG_LOG=n`,
`CONFIG_BOOT_BANNER=n` it drops from ~155 KiB to **~45 KiB**, which fits ATCM as
a single segment. The eventual Pouch device firmware must likewise fit TCM
(~128 KiB across ATCM+BTCM) — a real constraint to design around.

## The OpenAMP memory map (from the demo's Linux DTB)

Decompiled from the demo's device tree, the RPU carve-out is:

| Region | Address | Size |
|-----------------------|---------------|---------|
| R5 firmware (rproc) | `0x3ed00000` | 256 KiB |
| vring0 | `0x3ed40000` | 16 KiB |
| vring1 | `0x3ed44000` | 16 KiB |
| rpmsg buffer pool | `0x3ed48000` | 1 MiB |

The R5 firmware itself runs from TCM (the `kv260_r5` default link address `0x0`);
the shared rings/buffers live in DDR at `0x3ed40000`. The IPI mailbox is the
`rpu0_ipi` node (`xlnx,zynqmp-ipi-mailbox`), driven on the Zephyr side by
`drivers/ipm/ipm_xlnx_ipi.c`.

`kv260_r5-openamp.overlay` / `.conf` encode this: `zephyr,ipc_shm` →
`memory@3ed40000`, `zephyr,ipc` → `&rpu0_ipi`.

## How to run

Install Renode (portable) and its test deps, then:

```sh
# Build the R5 OpenAMP firmware for kv260_r5
export ZEPHYR_SDK_INSTALL_DIR=<sdk>
west build -b kv260_r5 zephyr/samples/subsys/ipc/openamp_rsc_table \
-- -DEXTRA_DTC_OVERLAY_FILE=$PWD/doc/renode-tier2/kv260_r5-openamp.overlay \
-DEXTRA_CONF_FILE=$PWD/doc/renode-tier2/kv260_r5-openamp.conf

# Smoke-test that a kv260_r5 Zephyr image boots on the emulated R5
cd <renode-dir>
./renode-test <pouch>/doc/renode-tier2/smoke_r5_boot.robot # uses uart1, rpu0
```

Renode must be driven via `renode-test`/robot; a bare `renode script.resc`
hangs headless. The R5 core is `rpu0`; reference it after
`using sysbus.cluster1`.

## Current status / remaining work

The Linux side is fully up: `remoteproc` loads the (TCM-linked, trimmed) R5
firmware, `virtio_rpmsg_bus` comes online, and `/dev/rpmsg_ctrl0` +
`/sys/bus/rpmsg/devices/{rpmsg_ctrl,rpmsg_ns}` appear — the RFC's exact
interface. The R5 now **executes** the firmware (prints on `uart0`).

**R5-side bring-up fix chain** (each step found the next fault; all real
ZynqMP-R5-OpenAMP requirements this board did not have out of the box):

1. **TCM link** — firmware must be TCM-resident (above).
2. **`zephyr,ipc = &rpu0_apu_mailbox`** — the `ipm_xlnx_ipi` driver puts the IPM
API on the *child* mailbox device; the parent `rpu0_ipi` has a NULL api.
Pointing `zephyr,ipc` at the parent faults in `ipm_set_enabled`.
3. **MPU region for the shared memory** — the ZynqMP SoC uses a *static* MPU
table (`soc/xlnx/zynqmp/arm_mpu_regions.c`) that does not map the DDR
OpenAMP carve-out, so `zephyr,memory-attr` is ignored and the R5 data-aborts
in `virtqueue`/`rpmsg_init_vdev`. See `zynqmp-r5-openamp-mpu.patch` (a
non-cacheable region for the carve-out; candidate to upstream or move to a
board-level MPU region).

With all three, the R5 no longer faults and runs the OpenAMP firmware.

**Known limitation (why the last step is deferred to hardware):** after
`remoteproc start` the emulation nearly stalls — only ~12 s of virtual time
advances and then it makes essentially no progress (a subsequent `ls` cannot
complete), consistent with an IPI notify / interrupt-storm in Renode's
`ZynqMP_IPI` model rather than a firmware config error (the R5 no longer
faults). Because the stall also prevents observing/iterating, no R5-announced
rpmsg channel is confirmed. On real MPU+MCU silicon the IPI is not a partial
model, so this is the natural point to continue on hardware.

Remaining work to finish on hardware (or a higher-fidelity model):
1. Complete the R5 rpmsg handshake (a channel appears on the Linux side; an
echo/round-trip succeeds).
2. Swap the echo app for the **Pouch device + rpmsg adapter** firmware. Note the
firmware must fit the R5 TCM (~128 KiB across ATCM+BTCM); the Pouch stack +
OpenAMP may need aggressive trimming or a DDR-execution setup — validate the
size budget early.
3. Replace the demo's Buildroot rootfs with an **Ubuntu userspace** rootfs.
4. Add a **mock broker** (or connect-agent) on the Linux side and an automated
test asserting a Pouch round-trip over real rpmsg.
5 changes: 5 additions & 0 deletions doc/renode-tier2/kv260_r5-openamp.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CONFIG_SHELL=n
CONFIG_LOG=n
CONFIG_PRINTK=y
CONFIG_BOOT_BANNER=n
CONFIG_KERNEL_SHELL=n
15 changes: 15 additions & 0 deletions doc/renode-tier2/kv260_r5-openamp.overlay
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/ {
chosen {
zephyr,ipc_shm = &shram;
zephyr,ipc = &rpu0_apu_mailbox;
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
shram: memory@3ed40000 {
compatible = "mmio-sram";
reg = <0x3ed40000 0x108000>;
};
};
&rpu0_ipi { status = "okay"; };
&rpu0_apu_mailbox { status = "okay"; };
&uart0 { status = "okay"; current-speed = <115200>; clock-frequency = <99999901>; };
33 changes: 33 additions & 0 deletions doc/renode-tier2/remoteproc_load.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
*** Variables ***
${LINUX_UART} sysbus.uart1
${LINUX_PROMPT} \#${SPACE}
${ROOTFS} %{POUCH_ROOTFS}

*** Keywords ***
Boot Linux And Login
[Arguments] ${testerId}=0
Wait For Line On Uart Booting Linux on physical CPU testerId=${testerId}
Wait For Prompt On Uart buildroot login: testerId=${testerId} timeout=120
Write Line To Uart root testerId=${testerId}
Wait For Prompt On Uart ${LINUX_PROMPT} testerId=${testerId}

Linux Command
[Arguments] ${command} ${timeout}=15
Write Line To Uart ${command}
Wait For Prompt On Uart ${LINUX_PROMPT} timeout=${timeout}

*** Test Cases ***
Should Load Pouch OpenAMP Firmware Via Remoteproc
Execute Command $rootfs=@${ROOTFS}
Execute Command include @scripts/single-node/zynqmp_openamp.resc
Execute Command machine SetSerialExecution True
Create Terminal Tester ${LINUX_UART} timeout=300 defaultPauseEmulation=true
Start Emulation
Boot Linux And Login
Linux Command modprobe zynqmp_r5_remoteproc
Linux Command echo rpmsg-echo.out > /sys/class/remoteproc/remoteproc0/firmware
Linux Command echo start > /sys/class/remoteproc/remoteproc0/state timeout=45
Write Line To Uart cat /sys/class/remoteproc/remoteproc0/state
Wait For Line On Uart running timeout=15
Write Line To Uart dmesg | tail -25
Wait For Prompt On Uart ${LINUX_PROMPT} timeout=15
18 changes: 18 additions & 0 deletions doc/renode-tier2/smoke_r5_boot.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
*** Variables ***
${ELF} %{ZEPHYR_ELF=build/zephyr/zephyr.elf}

*** Test Cases ***
Should Boot Zephyr Hello World On R5
Execute Command mach create
Execute Command machine LoadPlatformDescription @platforms/cpus/zynqmp.repl
Execute Command machine SetSerialExecution True
Execute Command using sysbus
Execute Command using sysbus.cluster0
Execute Command using sysbus.cluster1
Execute Command cluster0 ForEach IsHalted true
Execute Command cluster1 ForEach IsHalted true
Execute Command rpu0 IsHalted false
Execute Command sysbus LoadELF @${ELF} cpu=rpu0
Create Terminal Tester sysbus.uart1 timeout=30
Start Emulation
Wait For Line On Uart Hello World
26 changes: 26 additions & 0 deletions doc/renode-tier2/zynqmp-r5-openamp-mpu.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/soc/xlnx/zynqmp/arm_mpu_regions.c b/soc/xlnx/zynqmp/arm_mpu_regions.c
index 1851c42da67..9cff2796a56 100644
--- a/soc/xlnx/zynqmp/arm_mpu_regions.c
+++ b/soc/xlnx/zynqmp/arm_mpu_regions.c
@@ -60,6 +60,21 @@ static const struct arm_mpu_region mpu_regions[] = {
{.rasr = FULL_ACCESS_Msk |
STRONGLY_ORDERED_SHAREABLE |
NOT_EXEC}),
+#endif
+#if DT_HAS_CHOSEN(zephyr_ipc_shm)
+ /*
+ * OpenAMP/rpmsg shared memory (vrings + buffers) lives in DDR and must be
+ * mapped for the R5, non-cacheable so it stays coherent with the Linux
+ * host. 4 MiB at 0x3ec00000 covers the ZynqMP OpenAMP carve-out
+ * (~0x3ed00000-0x3ee48000).
+ */
+ MPU_REGION_ENTRY(
+ "ipc_shm",
+ 0x3ec00000,
+ REGION_4M,
+ {.rasr = P_RW_U_NA_Msk |
+ NORMAL_OUTER_INNER_NON_CACHEABLE_NON_SHAREABLE |
+ NOT_EXEC}),
#endif
/*
* The address of the vectors is determined by arch/arm/core/cortex_a_r/prep_c.c
14 changes: 14 additions & 0 deletions examples/zephyr/rpmsg_device/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Golioth, Inc.
#
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(rpmsg_device)

target_sources(app PRIVATE
src/main.c
src/credentials.c
src/fw_relay.c
)
15 changes: 15 additions & 0 deletions examples/zephyr/rpmsg_device/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) 2026 Golioth, Inc.
#
# SPDX-License-Identifier: Apache-2.0

mainmenu "Pouch rpmsg device example"

config EXAMPLE_FW_COMPONENT
string "OTA component to use for firmware update"
default "rpmsg_device"
help
Name of the component in the OTA manifest carrying this core's firmware.
The image is relayed to the host, which verifies and applies it through
remoteproc - this core has no flash of its own.

source "Kconfig.zephyr"
5 changes: 5 additions & 0 deletions examples/zephyr/rpmsg_device/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
VERSION_MAJOR = 1
VERSION_MINOR = 0
PATCHLEVEL = 2
VERSION_TWEAK = 0
EXTRAVERSION =
71 changes: 71 additions & 0 deletions examples/zephyr/rpmsg_device/boards/imx95_evk_mimx9596_m7.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) 2026 Golioth, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# i.MX95 M7: Pouch over rpmsg against a Linux virtio-rpmsg host
# (remoteproc + resource table). Platform config mirrors the
# openamp_rsc_table sample's imx95_evk fragment.

CONFIG_CLOCK_CONTROL=y
CONFIG_ARM_SCMI=y
CONFIG_IPM=y
CONFIG_IPM_MBOX=y
CONFIG_MBOX_NXP_IMX_MU=y
CONFIG_MBOX_INIT_PRIORITY=0

# System heap must cover libmetal/open-amp virtqueue state, the
# malloc(CONFIG_POUCH_SERVER_CERT_MAX_LEN) the server-cert endpoint does when
# the broker pushes the Golioth server certificate (4096 by default), AND the
# encrypted downlink blocks in flight - pouch_downlink_push() heap-allocates
# one per block and hands it to the decrypt work queue, so a fast link keeps
# many alive at once. Sizing the heap at exactly the cert length makes the
# cert allocation fail, which aborts the session right after the info
# exchange; sizing it for a slow link makes the block allocation fail mid
# firmware download, which puts the downlink channel into a permanent error
# state (the broker then sees "peer rejected transfer" on every retry).
CONFIG_HEAP_MEM_POOL_SIZE=16384

# The firmware relay buffer. malloc() draws from whatever RAM is left after
# static allocations (CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=-1), and the
# downlink heap-allocates one buffer per encrypted block in flight, so making
# this buffer bigger shrinks the pool the downlink itself needs. 32 KB leaves
# a comfortable arena.
CONFIG_POUCH_SERIAL_FW_BUF_SIZE=32768

# The rpmsg management thread must run BELOW the Pouch work queue
# (CONFIG_POUCH_THREAD_PRIORITY, default 5). At equal priority the management
# thread drains the whole receive vring without ever yielding - a fast host
# pushed 137 KB in 6 ms - while the decrypt work queue never runs, so
# pouch_downlink_push() keeps heap-allocating encrypted blocks until malloc
# fails and the downlink channel errors out permanently.
CONFIG_POUCH_RPMSG_RSC_DEVICE_THREAD_PRIORITY=6

# The rpmsg rx path runs the whole serial-core receive chain (including the
# server-cert endpoint's 4 KB malloc and certificate handling) in the adapter's
# management thread. 2 KB overflows it and faults with "Illegal load of
# EXC_RETURN into PC" partway through the server-cert transfer.
CONFIG_POUCH_RPMSG_RSC_DEVICE_STACK_SIZE=8192

# The default of 2 blocks is enough for plain telemetry, but with OTA enabled
# the uplink handlers (telemetry + per-component OTA status) can hold every
# block while the transport is still waiting for the first encrypted one, which
# stalls the uplink for the whole session.
CONFIG_POUCH_BLOCK_COUNT=8

CONFIG_OPENAMP=y
CONFIG_OPENAMP_MASTER=n
CONFIG_OPENAMP_RSC_TABLE=y
CONFIG_OPENAMP_RSC_TABLE_NUM_RPMSG_BUFF=8
CONFIG_OPENAMP_COPY_RSC_TABLE=y
CONFIG_OPENAMP_WITH_DCACHE=y

# Use the resource-table rpmsg adapter instead of the UART one.
CONFIG_POUCH_SERIAL_UART_DEVICE=n
CONFIG_POUCH_RPMSG_RSC_DEVICE=y

# Keep the M7 console (LPUART3) for logs.
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LOG=y
CONFIG_EARLY_CONSOLE=y
CONFIG_PRINTK=y
Loading
Loading