Introduction
In the embedded world, the question is no longer which OS should I use, but how can I make them work together?
Modern SoCs are blurring the line between real-time microcontrollers and application processors. Chips like the STM32MP2 from STMicroelectronics integrate both a Cortex-A35, capable of running a full Linux distribution, and a Cortex-M33, designed for deterministic, low-latency real-time tasks. Two cores, one chip: the best of both worlds.
This hybrid architecture is increasingly relevant for industrial applications. Consider a few common scenarios:
- Safety: a real-time core can monitor sensors and trigger safety responses with guaranteed latency, independently of what the application OS is doing.
- Low-power: the application processor can be suspended to save energy while the real-time core continues to monitor the environment and decide when to wake the system up.
- Offloading: hard real-time acquisition tasks (ADC sampling, sensor polling, motor control, etc.) live on the M33, while heavy processing, like FFT, AI inference, UI rendering, runs on the A35 under Linux.
However, this power comes with a cost: having two operating systems share the same chip introduces non-trivial configuration challenges: memory layout, security boundaries, and inter-core synchronization all need to be carefully coordinated across every layer of the stack.
Our demo focuses on the low-power scenario: Zephyr runs on the Cortex-M33, continuously reading sensor data and managing the i2c display, while Linux on the Cortex-A35 handles heavier processing and can be put to sleep and woken up transparently, without losing a single sample. What follows documents exactly what it took to make that work.
Demo Overview
The demo runs on a STM32MP257F-DK board and is split across two runtimes that communicate via OpenAMP over a shared memory region.
Zephyr: The Real-Time Side (Cortex-M33)
Zephyr owns the real-time responsibilities:
- Data acquisition (hc-sr04): data is fetched continuously, with strict timing guarantees.
- RT Displaying (ssd1306): real-time sensor values are displayed in the dedicated screen, giving immediate visual feedback independent of Linux.
- OpenAMP master: Zephyr acts as the OpenAMP master, initiating and managing the communication channel with Linux.
Linux: The Application Side (Cortex-A35)
Linux handles the heavier, less time-critical workload:
- Qt6 application: a graphical interface shows date/time to simulate computationally intensive data processing. The kind of task that would be unreasonable on a microcontroller.
- OpenAMP via serial: Linux communicates with Zephyr through the OpenAMP virtual serial interface, receiving sensor data for processing and display.
- libgpiod application: monitors the User-1 button to trigger entry into low-power sleep mode (s2idle).
- Wake-up trigger: a physical interrupt to WAKE-UP button brings the system back from sleep, resuming Linux and re-establishing communication with Zephyr.
OPTEE-OS: The security configuration (Cortex-A35)
OP-TEE is a TEE (Trusted Execution Environment), which runs in the A35 ARM TrustZone. It is responsible for the initialization of security peripherals like the RIF that we will see below.
The goal: demonstrate a credible low-power embedded system where Linux can sleep and wake up, while Zephyr seamlessly keeps running and buffering data.
The architecture diagram below captures the overall system topology. The interesting part is the glue: how both sides stay synchronized across a power state transition.
The Challenge Behind: Memory Configuration between the cores
Getting two OSes to share a chip is one thing. Getting them to survive a sleep cycle together is another. Here’s what had to be configured.
State of the Art: The STM32MP2 Security Model and Power Mode
The STM32MP2 introduces a hardware security model built around the concept of
TDCID (Trusted Domain Core ID). In this model, the Cortex-A35 acts as the security manager: it controls which cores and which software layers can access which peripherals.
This is enforced by the
RIF (Resource Isolation Framework) which is a firewall that grants or denies peripheral access to secure and non-secure worlds. The RIF is configured early in the boot chain, inside
OPTEE (the trusted execution environment running on the A35 secure world).
For our demo, we use:
- An open RIF configuration devicetree (which can be found in the OPTEE-OS in-tree files), no strict peripheral lockdown, suitable for a development/demo context.
- A custom Buildroot system aligned with the latest OpenSTLinux release, ensuring compatibility with upstream version.
Regarding the power mode, with the v6.2.0 of OpenSTLinux, the most low-power mode we can reach with the SoC is the LP_Stop2 mode (see in the table below). In this mode, the VDDCPU (vdd of the A35) is OFF and the DDR is in self-refresh mode. The self-refresh mode refers to the mode where the DDR retains data with very low consumptions, but it cannot be accessed. Only the VDDCORE (vdd of the m33) is kept ON.

The Sleep Mode and the Memory Problem
When Linux loads the Zephyr firmware onto the M33 via remoteproc, the firmware binary is placed in DDR by default, as specified in the Linux device tree. That works fine in the usual setup. But when the system enters deep sleep, the DDR goes into self-refresh mode. The M33 then tries to reach the DDR which completely breaks the system triggering a panic error.
To resolve this issue, the firmware should use another memory which is kept alive during the A35 sleep mode. So the SRAM1 and RETRAM have been selected. But even in the open RIF configuration, the SRAM1 is configured for Secure usage. So all Linux, OPTEE-OS and Zephyr devicetrees need to be updated to reflect these changes.
The fix is a three-layer change:
1. Move the M33 firmware to SRAM1 and RETRAM (Linux device tree)
The remoteproc memory region is redirected from DDR to SRAM1 and RETRAM, which remains powered during sleep. The memory-region of the m33_rproc node is updated to reflect the change:
/* stm32mp257f-dk-resmem.dtsi */
reserved-memory {
cm33_sram1: cm33-sram1@a043000 {
reg = <0x0 0xa043000 0x0 0x1d000>;
no-map;
};
/* stm32mp257f-dk.dts */
&m33_rproc {
mboxes = <&ipcc1 0x100>, <&ipcc1 0x101>, <&ipcc1 2>;
mbox-names = "vq0", "vq1", "shutdown";
- memory-region = <&cm33_cube_fw>, <&cm33_cube_data>,
+ memory-region = <&cm33_sram1>, <&cm33_retram>,
<&ipc_shmem_1>, <&vdev0vring0>,
<&vdev0vring1>, <&vdev0buffer>,
<&cm33_sram2>;
st,syscfg-nsvtor = <&a35ss_syscfg 0x20a8 0xffffff80>;
+ keep-power-in-suspend;
status = "okay";
};
The keep-power-in-suspend property tells the kernel to keep the M33 running through the suspend cycle.
2. Grant non-secure world access to SRAM1 (OP-TEE device tree)
By default, SRAM1 may not be accessible from the non-secure world where Linux and Zephyr operate. The RIF configuration must explicitly open access via the &cm33_sram1 node, using RIF_NSEC (non-secure) flag:
/* stm32mp257f-dk-ca35tdcid-rif.dtsi */
&cm33_sram1 {
st,protreg = <RISABPROT(
RIF_DDCID_DIS, /* domain CID disabled */
RIF_UNUSED,
- RIF_SEC, /* accessible from secure world */
+ RIF_NSEC, /* accessible from non-secure world */
RIF_NPRIV, /* non-privileged access allowed */
RIF_CFDIS, /* CID filtering disabled */
RIF_UNUSED, RIF_UNUSED, RIF_UNUSED
)>;
};
3. Configure Zephyr to link against SRAM1 and RETRAM addresses (Zephyr devicetree overlay)
Zephyr’s linker must know it is running from SRAM1 and RETRAM, not DDR. The DDR code/data regions are deleted and replaced with the correct SRAM1 and RETRAM addresses in the board overlay:
/* stm32mp257f_dk_stm32mp257fxx_m33.overlay */
+/delete-node/ &ddr_code;
+/delete-node/ &ddr_sys;
+
+/ {
+ /* Remap to SRAM1 physical address */
+ ddr_code: memory0@a043000 {
+ reg = <0xa043000 0x1d000>;
+ ranges = <0x0 0xa043000 0x1d000>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+ };
+
+ /* Remap to RETRAM physical address */
+ ddr_sys: memory1@a080000 {
+ reg = <0xa080000 0x1f000>;
+ ranges = <0x0 0xa080000 0x1f000>;
+ #address-cells = <1>;
+ #size-cells = <1>;
+ };
+};
Note: the node names keep the ddr_* label for compatibility with upstream Zephyr board definitions, but they now point to SRAM1 and RETRAM addresses.
The OpenAMP Vring Problem
Moving the firmware to SRAM1 and RETRAM keeps the M33 alive through sleep. But there’s a second issue: OpenAMP vring buffers.
The vring is the shared memory ring buffer used by OpenAMP to exchange messages between Zephyr and Linux. These buffers are allocated in DDR that is not accessible during the sleep. If Zephyr tries to access OpenAMP vrings during the sleep mode, the OS will panic.
You can see this clearly in the reserved memory declarations, that all vring and IPC buffers live at DDR addresses (0x812xxxxx):
/* stm32mp257f-dk-resmem.dtsi */
ipc_shmem_1: ipc-shmem-1@81200000 {
compatible = "shared-dma-pool";
reg = <0x0 0x81200000 0x0 0xf8000>; /* DDR */
no-map;
};
vdev0vring0: vdev0vring0@812f8000 {
compatible = "shared-dma-pool";
reg = <0x0 0x812f8000 0x0 0x1000>; /* DDR */
no-map;
};
vdev0vring1: vdev0vring1@812f9000 {
compatible = "shared-dma-pool";
reg = <0x0 0x812f9000 0x0 0x1000>; /* DDR */
no-map;
};
vdev0buffer: vdev0buffer@812fa000 {
compatible = "shared-dma-pool";
reg = <0x0 0x812fa000 0x0 0x6000>; /* DDR */
no-map;
};
The solution: a SLEEP/WAKE handshake.
Rather than deinitialize and reinitialize the OpenAMP connection at each sleep cycle, which would be hard to synchronize between OSes, the system uses a coordinated protocol:
- Before entering deep sleep, Linux sends a SLEEP signal to Zephyr over OpenAMP.
- Zephyr receives the signal, acknowledges it, and takes over full responsibility: it continues reading
sensor data and buffers it locally. Zephyr stops sending data through OpenAMP. - After waking up, Linux sends a WAKE signal.
- Zephyr flushes its buffer back to Linux, restoring continuity of data as if the sleep never happened.

This approach keeps DDR usage minimal during active operation, allows a clean sleep mode, and ensures no data is lost across the sleep boundary.
Conclusion
The system works. Linux sleeps, Zephyr keeps sampling, and when Linux wakes up, it gets back every data point it missed.
The more interesting takeaway is what it took to get there: a coordinated change spanning the Linux device tree, the OP-TEE security configuration, and Zephyr’s linker map. None of these three layers is hard in isolation, however, together they form a pattern you will encounter on any heterogeneous SoC where power management and inter-core communication need to coexist.
One last note on the demo setup: sleep is triggered by a physical button press, which makes it easy to demonstrate on stage. In a real product, that button can be replaced by any interrupt source (for e.g., a distance threshold detected by Zephyr itself and signalled to Linux over OpenAMP, a timer, an external GPIO, etc). The sleep/wake handshake described here is agnostic to the trigger, so the architecture supports autonomous operation without modification.