STM32H dual-core: FreeRTOS on both cores and communication via shared memory
The STM32H7® dual-core family, for example the H745 or H755, carries a Cortex-M7® and a Cortex-M4 on one chip. Both cores run independently. This mode is called AMP, asymmetric multiprocessing.
The split of duties is clear: the M7 handles the compute load at high clock rate. The M4 handles the deterministic peripheral work. For cooperation the cores need shared memory and a wake-up mechanism.
This article shows both. First one FreeRTOS™ per core. Then a directed channel via a FreeRTOS message buffer in shared memory, woken up via the hardware semaphore unit.
Why no dedicated DSP
A Cortex-M7 comes with a double-precision FPU and the DSP instruction set. Typical digital signal conditioning of a sensor runs on it in many cases without a separate DSP part. The M4 handles the deterministic peripheral and timing work in parallel.
Typical signal conditioning includes:
- FIR and IIR filters. Low-pass, high-pass, band-pass and notch filters against mains hum and out-of-band noise, median filters against outliers, high-pass against offset drift.
- FFT and spectral analysis. With windowing (Hann, Hamming), peak search and energy per band. For a single target frequency a Goertzel filter is enough.
- Correlation and matched filter. Time-of-flight measurement, pattern recognition, cross-correlation of two signals.
- Synchronous detection and lock-in. Phase-sensitive demodulation and I/Q mixing, to pull a weak signal at a known frequency out of the noise.
- Decimation and rate conversion. Oversampling with decimation, CIC filter, noise shaping for extra effective bits.
- Envelope and RMS. Envelope, RMS, peak value, moving and coherent averaging.
- CORDIC. atan2, magnitude and phase, angle for resolver and encoder.
- Adaptive filters. LMS or RLS for noise and echo cancellation.
- Kalman and sensor fusion. State estimation and fusion of multiple sensors.
- Calibration and linearisation. Offset, gain, curve, temperature compensation, bridge and cold-junction compensation.
- Feature extraction. RMS trend, crest factor, kurtosis for condition monitoring.
- Event and threshold detection. Zero crossing, edges, hysteresis, debouncing.
- Control. PID loop when the sensor is part of a closed loop.
For AI and edge computing with high throughput, that is neural inference, controllers of this class are not intended. Such tasks need a dedicated NPU or edge-AI part.
A dedicated DSP is worth its weight only when throughput or latency exceed the reserves of the M7. Below that, the dual-core solution saves a chip, a second toolchain and the coupling between two chips.
For hard real-time in I/O there is another route without a DSP: hardware parallelism instead of interrupt-driven software. This is described in our article Parallel instead of interrupted: XMOS xCORE compared to the classic MCU.
Two cores, two RTOS instances
Each core gets its own FreeRTOS image with its own heap and its own scheduler tick. There is no shared scheduler. A task on the M7 does not see the tasks on the M4.
The boot is staggered. The M7 starts first, configures the clock and the memory and then releases the M4. This way the shared environment is in place before the second core uses its buffer.
The H7 splits the memory into domains. The SRAM4 in domain D3 is reachable by both cores and by the DMA. It is well suited as the location for the shared buffer.
Place shared memory correctly
Cache coherence is the central pitfall. The M7 has an L1 data cache, the M4 has none. If the M7 writes to shared memory, the value stays in the cache first. The M4 then reads the old value from RAM.
There are two clean ways. The first marks the shared region as non-cacheable via the MPU. The second maintains the cache manually, with a clean after the write and an invalidate before the read.
The first variant is more robust. An MPU region for the buffer as non-cacheable and shareable eliminates the manual cache operations and their error sources. The linker places the buffer via its own section into SRAM4, at the same address in both images.
Wake-up via the hardware semaphore
Shared memory alone is not enough. The receiver has to be told that data is ready. Active polling costs CPU time and increases latency.
The H7 has a hardware semaphore unit (HSEM) with an interrupt. Core A raises a semaphore after the write. That produces an interrupt on core B. This way the receiver task is only woken up when data is actually pending.
Briefly summarised from reference manual RM0399, the registers you actually touch:
HSEM_R0…HSEM_R31(semaphore registers): one 32-bit register per semaphore. Lock by writingLOCK=1 | COREID | PROCIDin one access, release by writingLOCK=0with the same COREID/PROCID (otherwise the write is discarded).HSEM_RLR0…HSEM_RLR31(read-lock registers): 2-step lock by pure read; the lock bit is set automatically if the semaphore was free.HSEM_IER1/HSEM_IER2(interrupt enable): one 32-bit register per CPU, one bit per semaphore. Setting the bit enables the HSEM interrupt on the target core when that semaphore is released.HSEM_ISR1/HSEM_ISR2(interrupt status): read-only, shows which semaphore raised the interrupt on the respective core.HSEM_MISR1/HSEM_MISR2(masked interrupt status):ISR & IER, the view relevant to the ISR.HSEM_ICR1/HSEM_ICR2(interrupt clear): writing the bit clears the pending bit on the respective core.HSEM_KEYRandHSEM_CR: key-based bulk release of all semaphores held by a core (for reset scenarios).
Concrete path in our code: after sending, core A writes HSEM_R<id> = 0x0000_0000 (release), which sets the pending bit in core B’s HSEM_ISR2. Interrupt HSEM2_IRQn fires on the target core, the ISR acknowledges by writing to HSEM_ICR2 and calls xMessageBufferSendCompletedFromISR.
FreeRTOS message buffer across the core boundary
A FreeRTOS message buffer is a single-writer / single-reader construct. That fits exactly to a directed channel from core A to core B. For both directions you allocate two buffers.
The buffer, meaning the control structure and the data area, sits in the shared, non-cacheable SRAM4. FreeRTOS offers two hooks for the signalling: sbSEND_COMPLETED and sbRECEIVE_COMPLETED.
You override these hooks. A send on core A triggers an interrupt on core B via the hardware semaphore. The ISR on core B calls xMessageBufferSendCompletedFromISR and wakes up the waiting receiver task. The channel is thereby blocking and event-driven, without polling and without a global lock.
/* Shared, non-cacheable SRAM4, same address in both images */
extern MessageBufferHandle_t xCoreAtoB; /* created by CM7 */
/* Hook: after a send wake the other side via HSEM */
#define sbSEND_COMPLETED(pxBuf) vHsemNotify(HSEM_ID_A2B)
/* Runs on CM4 when CM7 has sent */
void HSEM1_IRQHandler(void)
{
BaseType_t xHigher = pdFALSE;
vHsemClear(HSEM_ID_A2B);
xMessageBufferSendCompletedFromISR(xCoreAtoB, &xHigher);
portYIELD_FROM_ISR(xHigher);
}
/* Sender task on CM7 */
xMessageBufferSend(xCoreAtoB, &frame, sizeof frame, portMAX_DELAY);
/* Receiver task on CM4 */
len = xMessageBufferReceive(xCoreAtoB, &frame, sizeof frame, portMAX_DELAY);
The names vHsemNotify, vHsemClear and HSEM_ID_A2B are placeholders for the semaphore primitives of the project. For the reverse direction you allocate a second buffer and a second semaphore ID.
Sequence
- Place the buffer via a linker section into SRAM4, at the same address in both images.
- Configure an MPU region for SRAM4 as non-cacheable, on both cores.
- Create the buffer on exactly one core, with
xMessageBufferCreateStatic. The other core waits until the buffer is ready. - Enable the semaphore interrupt on both cores, one ID per direction.
- Define
sbSEND_COMPLETEDso it raises the semaphore of the opposite direction. - In the ISR call
xMessageBufferSendCompletedFromISRwith the handle of the local buffer.
Pitfalls
- Cache. Buffer not configured as non-cacheable. The receiver reads old data. This is the most common mistake.
- Address mismatch. Both images do not see the buffer at the same address. Fix via a shared linker symbol.
- Struct layout. Both images do not compile the buffer struct identically. Use the same FreeRTOS version and the same configuration on both cores.
- Boot order. The second core accesses the buffer before the first has created it. The second core must wait for a ready flag.
- Interrupt enable. The semaphore interrupt is not enabled in the NVIC on one core. The wake signal never arrives, the receiver blocks forever.
Takeaway
The dual-core H7 splits compute and real-time peripherals cleanly across two cores. The coupling does not need external middleware. A FreeRTOS message buffer in the shared, non-cacheable SRAM4, woken via the hardware semaphore, is enough for a robust channel.
The biggest pitfall is not the logic of the channel but cache coherence. Whoever keeps the shared region non-cacheable from the start avoids the most common source of errors in a dual-core design.
The H7 plays its strengths where compute performance leads: FFT-heavy signal processing, high communication throughput, graphics HMI, several complex protocols in parallel. For that profile the dual-core split with one RTOS per core is the clean answer.
Keywords: STM32H7 dual-core, Cortex-M7, Cortex-M4, FreeRTOS message buffer, shared memory, SRAM4 non-cacheable, hardware semaphore HSEM, AMP asymmetric multiprocessing, cache coherence, dsPIC33CH, ASIL-B ASIL-C, embedded consulting Switzerland, swissEmbedded firmware engineering
Two cores coupled cleanly, without cache traps and without external middleware? We bring up FreeRTOS on both cores of the STM32H7, set up shared memory and HSEM, and deliver the whole thing with a test bench. Or we show you when the dsPIC33CH with ASIL hardware is the better grip.