Linux runs on cores 0-2, Zephyr on core 3. Linux starts Zephyr with remoteproc, and the two talk rpmsg over virtio. No hypervisor: both share the GIC-400 and the memory system, and each owns its own peripherals.
| Range | Owner |
|---|---|
0x00200000 - 0x0fffffff |
Linux kernel, initrd and devicetree |
0x10000000 - 0x2fffffff |
Linux CMA pool (512 MiB) |
0x30000000 - 0x307fffff |
Zephyr image and RAM |
0x30800000 - 0x3080ffff |
Zephyr console |
0x30900000 - 0x3090ffff |
rpmsg vrings |
0x30910000 - 0x309fffff |
rpmsg buffers (shared-dma-pool) |
Linux sees 0x30000000 - 0x3090ffff and the buffer pool as no-map reserved memory. The
firmware's devicetree asks for a 512 MiB CMA pool inside 0x0 - 0x30000000; Zephyr sits just
above it, and U-Boot keeps the kernel, initrd and devicetree below 0x10000000 so the pool still
fits.
The firmware parks cores 1-3 in a wfe loop polling their spin-table release addresses
(0xd8, 0xe0, 0xe8, 0xf0). uboot/boot.cmd boots Linux with nr_cpus=3, which makes
core 3 impossible for Linux, so it never touches core 3's release address. (maxcpus=3 is not
enough: Linux still prepares every possible CPU and core 3 would end up in the kernel's holding
pen.) The script also adds the reserved memory and a zephyr-amp node to the firmware's
devicetree, on a copy: ${fdt_addr} is U-Boot's live control devicetree.
linux/rproc/cpu_rproc.c binds to that node, loads the Zephyr ELF into the reserved memory,
writes its entry point to the CPU's release address and wakes it with sev. Zephyr comes up after
Linux has set up the GIC distributor, and CONFIG_GIC_SAFE_CONFIG makes it leave the distributor
as it is.
The driver has nothing specific to the Pi 4: any arm64 SoC with a spin-table CPU and a GICv2 can use it. Everything comes from the node:
zephyr-amp {
compatible = "amp,cpu-rproc";
cpus = <&cpu3>;
memory-region = <&image>, <&vdev0vring0>, <&vdev0vring1>, <&vdev0buffer>;
interrupts = <GIC_SPI 221 IRQ_TYPE_EDGE_RISING>, /* remote CPU to Linux */
<GIC_SPI 220 IRQ_TYPE_EDGE_RISING>; /* Linux to remote CPU */
firmware-name = "zephyr-amp.elf";
};
Carveouts are named after their reserved-memory nodes, all no-map; vdev0buffer is a
shared-dma-pool for the rpmsg buffers. The kick is routed to the GIC CPU interface numbered by
the CPU's MPIDR Aff0.
Notifications use two SPIs that no device drives, through the zephyr,mbox-gic-sw mailbox:
Linux sets SPI 220 pending for core 3, Zephyr sets SPI 221 pending for Linux. The shared memory is
non-cacheable on both sides.
apps/rpmsg is Zephyr's openamp_rsc_table sample (src/main_remote.c, copied with larger
thread stacks for arm64): it answers the kernel's rpmsg_client_sample and echoes what is written
to /dev/ttyRPMSG0. Its overlay moves RAM to 0x30000000, adds the console and IPC regions and
disables every device that Linux owns (VideoCore mailbox and firmware, GPIO, UART, I2C, GENET,
USB, RNG); prj.conf turns off SMP, CPU power management, power domains and PCIe.
CONFIG_RAM_CONSOLE writes the console region and lists it in the resource table as a trace
buffer, which Linux shows as /sys/kernel/debug/remoteproc/remoteproc0/trace0.
zephyr/, drivers/, dts/ and modules/ make this directory a Zephyr module carrying a copy
of the zephyr,mbox-gic-sw driver and binding (zephyrproject-rtos/zephyr#124185). The copy is
used only when the Zephyr tree lacks them; delete it once the pinned Zephyr has the driver.
The trace entry does not build on 64-bit in Zephyr itself, so zephyr/patches/ carries the fix
until it is upstream; west patch apply applies it and west patch reverse takes it out again.
west patch apply
west build -b rpi_4b rpi4-amp/apps/rpmsg
strip --strip-debug -o zephyr-amp.elf build/zephyr/zephyr.elf
remoteproc needs the section headers to find .resource_table, so strip only the debug
information. In a workspace whose manifest is not this repository, point west patch at the
patches with -b <ws>/rpi4-amp/zephyr/patches -l <ws>/rpi4-amp/zephyr/patches.yml, and if the
workspace does not list them, add the modules by hand:
-- -DEXTRA_ZEPHYR_MODULES="<ws>/modules/hal/cmsis;<ws>/modules/hal/libmetal;<ws>/modules/lib/open-amp;<ws>/rpi4-amp".
The Pi firmware starts U-Boot instead of the kernel, and U-Boot runs uboot/boot.cmd. Build it
with rpi_arm64_defconfig: it uses the devicetree the firmware passes in, with the config.txt
overlays and the cmdline.txt command line, which the boot script edits. rpi_4_defconfig embeds
its own devicetree instead and does not work with the script.
git clone --depth 1 --branch v2026.07 https://source.denx.de/u-boot/u-boot.git
cd u-boot
make CROSS_COMPILE=aarch64-linux-gnu- rpi_arm64_defconfig
make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
Host packages: an aarch64 cross compiler (gcc-aarch64-linux-gnu), bison, flex, and the
OpenSSL and GnuTLS development headers (libssl-dev and libgnutls28-dev on Debian/Ubuntu,
openssl-devel and gnutls-devel on Fedora).
Copy u-boot.bin to the FAT partition of the Raspberry Pi OS card and add to the [all] section
of config.txt:
arm_64bit=1
kernel=u-boot.bin
enable_uart=1
enable_uart=1 puts the U-Boot and Linux console on the GPIO 14/15 serial pins (115200 baud).
Delete the kernel= line to boot Raspberry Pi OS directly again.
Target: Raspberry Pi OS (64-bit) with U-Boot as above. The boot script loads kernel8.img and
initramfs8 from the FAT partition (mmc 0:1) and uses the devicetree and command line the
firmware passes in ${fdt_addr}.
python3 scripts/mkscr.py uboot/boot.cmd boot.scr
Copy boot.scr and zephyr-amp.elf to the FAT partition, then at the U-Boot prompt:
load mmc 0:1 ${scriptaddr} boot.scr; source ${scriptaddr}
To run it on every boot, store it as the boot command (U-Boot keeps its environment in
uboot.env on the FAT partition):
setenv bootcmd 'load mmc 0:1 ${scriptaddr} boot.scr; source ${scriptaddr}'
saveenv
amp_extra_args is appended to the kernel command line; for a root shell on the serial console
without a login: setenv amp_extra_args "init=/bin/bash console=ttyS0,115200".
The Raspberry Pi OS kernel has no virtio, rpmsg or remoteproc. linux/rproc/ builds them as
external modules from the kernel sources of the running version, plus the remoteproc driver.
Upstream remoteproc is built-in only; the two glue files split its virtio half into a module of
its own and export what it needs. On the Pi (headers and gcc come with Raspberry Pi OS):
linux/rproc/get-sources.sh <linux-6.18.50 tree>
make -C linux/rproc
sudo linux/rproc/load.sh # zephyr-amp.elf in /boot/firmware
dmesg | grep rpmsg_client_sample # "incoming msg 100", "goodbye!"
sudo cat /sys/kernel/debug/remoteproc/remoteproc0/trace0 # Zephyr's console
The boot partition of the SD card (mmc 0:1 in U-Boot, /boot/firmware in Linux); everything
not marked new is stock Raspberry Pi OS:
bootfs/
├── config.txt arm_64bit=1, kernel=u-boot.bin, enable_uart=1 added
├── cmdline.txt unchanged; boot.scr appends nr_cpus=3
├── start4.elf, fixup4.dat firmware
├── bcm2711-rpi-4-b.dtb loaded by the firmware; boot.scr edits a copy
├── overlays/
├── kernel8.img loaded by boot.scr
├── initramfs8 loaded by boot.scr
├── u-boot.bin new: rpi_arm64_defconfig
├── uboot.env new: written by saveenv
├── boot.scr new: uboot/boot.cmd through scripts/mkscr.py
└── zephyr-amp.elf new: apps/rpmsg, strip --strip-debug
The remoteproc modules on the Pi, after get-sources.sh and make:
linux/rproc/
├── cpu_rproc.c, remoteproc_core_exports.c, remoteproc_virtio_module.c
├── Kbuild, Makefile, load.sh, get-sources.sh
├── virtio*.c, rpmsg_*.c, remoteproc_*.c, ... copied kernel sources (git-ignored)
└── virtio_ring.ko, virtio_core.ko, rpmsg_core.ko, rpmsg_ns.ko, virtio_rpmsg_bus.ko,
rpmsg_char.ko, rpmsg_ctrl.ko, rpmsg_tty.ko, rpmsg_client_sample.ko, remoteproc.ko,
rproc_virtio.ko, cpu_rproc.ko
- No isolation: either side can overwrite the other's memory.
- Core 3 cannot be stopped (no PSCI on the Pi 4): stopping the remote fails, and restarting Zephyr needs a reboot.
- The console wraps after 64 KiB.
- The doorbell SPIs must stay unused by hardware; SPI 220/221 are above every SPI the Pi 4 devicetree uses.
The kernel code in linux/rproc/ is GPL-2.0-only, as Linux requires for a module; the kernel
sources get-sources.sh copies keep their own licenses. apps/rpmsg/src/main_remote.c is
Zephyr's sample under its original copyright. Everything else is Apache-2.0.
doc/cover.png uses Tux by Larry Ewing, created with The GIMP, and the Zephyr logo. Linux is a
registered trademark of Linus Torvalds. Zephyr and the Zephyr logo are trademarks of The Linux
Foundation.
