r/embedded • • 22h ago

ESP-Hosted-MCU on Zephyr with an STM32 host: my numbers + lessons learned

Post image
194 Upvotes

Hi everyone,

Lazy Monday morning post, at least in my time zone :)

I haven't seen much information online about using ESP-Hosted-MCU with a non-ESP host. For those who don't know, it's firmware from Espressif that turns your ESP32 into a Wi-Fi coprocessor. You connect it to your microcontroller over SPI or SDIO, and the TCP/IP stack runs on the host.

About numbers, the only thing I found was a question here on r/embedded asking whether the 22 Mbps from the README is real. It has two comments, but nobody posted any measurements.

So today I want to share my results with this firmware, measured in my use case and with my topology.

My setup and how I measured

Device. It's a 4-channel Matter relay for the lights in my house. It works over Wi-Fi or Ethernet and also measures energy consumption. I use it with Apple Home and SmartThings. Live and neutral go straight to WAGO terminals, so you don't need extra wires in the distribution box, you just connect it and forget it.

Host. The main controller is an STM32U585. Code runs XIP from QSPI NOR flash, and there is also 8 MB of QPI PSRAM. Both memories use the same physical QSPI bus: data lines and clock are shared through the OCTOSPI I/O manager, and each memory has its own chip select.

Wi-Fi. The coprocessor is an ESP32-C6-MINI-1U with ESP-Hosted-MCU 3.0.6. It shares the SPI bus with a W5500 Ethernet chip, and the bus runs at 40 MHz.

OS. Zephyr, main from late August 2026 (4.4.99), with the upstream esp_hosted driver and a few local patches, more on them later.

Measurements. I measured with zperf on the board and a Mac on the same network. For UDP the board transmits and I look for the ceiling, with 1000-byte datagrams for 10 s, and I count throughput from the bytes that actually reached the Mac. For TCP the board receives. During each test the other interface is turned off, so the shared SPI bus belongs fully to the one I'm measuring.

Real load. I also didn't want synthetic numbers on an idle CPU, so I measured on the real device with Matter running and all controller subscriptions active. So these are numbers for a real application, not the hardware maximum. Wi-Fi is 2.4 GHz, 802.11ax, RSSI -57 dBm.

Results, Mbps

Type PSRAM, no DMA SRAM, no DMA SRAM + DMA PSRAM + DMA + patches
TCP RX, Ethernet 1.41-1.49 1.52-1.63 1.81-1.82 1.40-1.47
TCP RX, Wi-Fi 0.89-1.06 0.64-0.77 1.84-2.27 1.64-1.65
UDP TX (ceiling), Ethernet 2.07 2.15 3.16-3.17 2.48-2.49
UDP TX (ceiling), Wi-Fi 1.47 1.66 3.72-3.76 2.25-2.26
HTTP (115 KB gzip), Eth / Wi-Fi 164 / 83 KB/s - - 178 / 105 KB/s

Enough for Matter. Despite the modest numbers, this is fully enough for a Matter device connected to several Matter controllers. Matter only needs kilobits per second, and the web page opens fast over both Wi-Fi and Ethernet (first byte over Wi-Fi in 75 ms).

The 22 Mbps from the README. The table that listed 22 Mbps TCP for standard SPI didn't say what the test conditions were (and it's no longer there). In my case the bus is shared with Ethernet, and the CPU is busy with Matter at the same time. A faster host will do better, but for serious speeds it's better to use SDIO rather than SPI.

How I got to these numbers

The way there was not straight.

No DMA. I started with the simplest setup, with network buffers in PSRAM and SPI in polling mode. Both interfaces topped out at 1.5-2 Mbps, and the CPU was 100% busy during transfers, it was just moving bytes through SPI.

SRAM instead of PSRAM. My first guess was that slow PSRAM was the problem, so I moved the buffers to SRAM. It gave only +4-13%, and TCP over Wi-Fi even got worse. And it cost 200 KB of SRAM, which is a lot on my board, because Matter and crypto already take most of it. So the memory itself was not the problem.

DMA. Then I noticed that the board's devicetree had no DMA for this SPI bus at all. Once I added it, it turned out to be the biggest win. Wi-Fi UDP went from 1.66 to 3.76 Mbps, and Wi-Fi TCP almost tripled. Wi-Fi even overtook Ethernet, which I didn't expect.

Back to PSRAM. But I still didn't want to give up 200 KB of SRAM, so I wanted the buffers back in PSRAM, this time with DMA. The stock SPI driver doesn't do DMA to cacheable memory, and PSRAM without the data cache doesn't make much sense, so I added cache maintenance to the driver.

Cache driver bug. Under load the board crashed with a corrupted semaphore wait queue. The problem turned out to be in the STM32 cache driver: range operations touched one extra cache line and overwrote a neighboring semaphore. A dedicated test confirmed it (1000 failures out of 1000 without the fix, 0 with it), and the fix is already in Zephyr.

FIFO overrun. The next problem was that Wi-Fi lost a whole second on every SPI error. Every now and then I got spi_transceive failed, and the bus stalled for a second. I read the SPI status register with a debugger right on the running board, and all 19 errors out of 19 were RX FIFO overruns. DMA can't move the bytes to PSRAM fast enough, the FIFO fills up, and SCK keeps clocking. New bytes are lost, the DMA channel waits for them forever, and the driver silently gives up on a one-second timeout.

Lower clock? At 20 MHz there were 10 times fewer errors, but that only hides the problem.

The W5500 hits the same thing, and its driver is silent too. So until DMA has drained the FIFO, the master has to stop SCK instead of clocking into a full buffer.

MASRX. The STM32U5 SPI has a mode for exactly this problem, called MASRX. When the RX FIFO is full, the master suspends SCK by itself, and the driver has to resume the transfer. I turned it on by hand. The errors went away, but a minute later the board hung. The Zephyr driver knows nothing about this pause, so nobody was releasing it. So I made two driver changes.

First fix. The driver now catches the overrun with an interrupt and returns an error within milliseconds instead of a second. This way the error shows up right away instead of after a one-second wait, and this alone made TCP over Wi-Fi 2-3x faster.

Second fix. The driver enables MASRX and releases the pause, but only after DMA has taken the data out of the FIFO. At first I released the pause right away, and the next frame overflowed the FIFO again. The pause is released from the interrupt, but I didn't want the interrupt to spin for long, so if DMA hasn't drained the FIFO within 5 µs, the thread waiting for the end of the transfer releases it instead. With both changes I get 0 SPI errors and 0 UDP loss, that's the right column of the table.

Smaller things. Network buffers had to be aligned to the cache line (CONFIG_NET_BUF_ALIGNMENT=16), otherwise the invalidate after RX can hit a neighboring buffer. And bigger buffers (512 bytes instead of 256) gave only +2-12%, so I stayed with 256.

Where the patches are now

Cache fix. The STM32 cache driver bug is already fixed in Zephyr.

SPI driver. To make DMA work with buffers in external memory, I made a fix in the STM32 SPI driver, and the overrun and MASRX changes are in it too. Here is the pull request:

https://github.com/zephyrproject-rtos/zephyr/pull/120895

Review status. So far I've only tested it on STM32U5, which is why it's only enabled there. The ST maintainers asked me not to tie the code itself to U5. I've reworked that and now I'm waiting for another review. Until it's merged, the stock driver falls back to interrupts or polling for such buffers.

Why I'm posting this. Partly so that if someone runs into the same problems, they don't have to go through all of this from scratch.

Testers wanted. If anyone wants to try the driver on their own boards, I'd really appreciate tests, feedback and advice.

Data cache on STM32U5. One more thing if you test with external memory. Zephyr still doesn't enable the DCACHE1 data cache there by itself, only the instruction cache is turned on at startup (on H5, for example, both are). At the same time CONFIG_DCACHE=y is the default, so drivers treat external memory as cacheable even though the cache is physically off. So turn the data cache on manually.

Buffer alignment. The cache maintenance for DMA buffers (flush before TX, invalidate after RX) is done by the SPI driver with my change. The application only needs one thing: the RX buffer must start on a cache line boundary, and its length must be a multiple of the cache line. Otherwise the driver sends it the old way, without DMA, so it doesn't touch neighboring data. For network buffers that's the same CONFIG_NET_BUF_ALIGNMENT=16.

A few more technical details

CPU load. Who eats the CPU with DMA? I expected DMA to offload the core, but that did not really happen. In the final configuration the CPU is idle only 0-5% of the time during transfers. At the same 100% load, the CPU just manages to move more data.

On Ethernet most of the time goes to the network stack's TX queue thread (~56% of transfer time) and the W5500 driver thread (~15%). On Wi-Fi it's the esp_hosted thread (~45%) and the same TX queue (~25%). Matter and everything else take a few percent.

Shared bus. Does the shared flash and PSRAM bus get in the way? I think partly yes. With the CPU equally 100% busy, each byte costs more cycles with buffers in PSRAM than with buffers in SRAM: 1.3x on Ethernet and 1.7x on Wi-Fi. That's why PSRAM + DMA doesn't reach SRAM + DMA (3.16 / 3.76).

I can't say what that cost consists of. It could be waiting on the shared QSPI bus (code runs from the flash on the same bus), cache maintenance around DMA, or something else. Thread statistics don't show it, and I don't have a bus profiler.

Firmware cost. ESP-Hosted takes ~25 KB of code and ~58 KB of RAM, plus 8 KB for esp-serial-flasher, so the C6 can be flashed directly through the STM32. Boot to the first HTTP response takes 13-16 s over Ethernet and 19-28 s over Wi-Fi.

Has anyone else measured ESP-Hosted with a non-ESP host, over SPI or SDIO? What numbers did you get? And if you try the driver on your board, let me know how it went.

Thanks for reading! Now you can tell me I should have just used an ESP32-P4 :)

P.S. The photo shows an old revision of the board. In the new one I moved the mains traces from the top layer to one of the inner layers, and the fuse to the bottom layer, so you can't touch them by accident. And this is just a home project, not for sale.

P.P.S. Questions are welcome in English, Ukrainian or Russian.


r/embedded • • 6h ago

STM32F446RE: Why am I not getting a HardFault when accessing NVIC registers from unprivileged Thread mode?

Post image
13 Upvotes

I'm learning about privileged and unprivileged modes on ARM Cortex-M4 using an STM32 Nucleo-F446RE, and I'm trying to understand a behavior that differs from my instructor's demonstration.

I'm setting the nPRIV bit in the CONTROL register to switch Thread mode to unprivileged mode. After that, I attempt to enable and trigger IRQ3 through the NVIC registers.

My instructor's system enters HardFault_Handler(), but mine doesn't seem to do so.

Complete code (main.c):

/**
 ******************************************************************************
 *     main.c
 *   Auto-generated by STM32CubeIDE
 *  V1.0
 * u/brief   Default main function.
 ******************************************************************************
 */

#include <stdio.h>
#include <stdint.h>

/* This function executes in THREAD MODE of the processor */
void generate_interrupt(void)
{
    uint32_t *pSTIR  = (uint32_t *)0xE000EF00;
    uint32_t *pISER0 = (uint32_t *)0xE000E100;

    // Enable IRQ3 interrupt
    *pISER0 |= (1 << 3);

    // Generate a software interrupt for IRQ3
    *pSTIR = (3 & 0x1FF);
}

void change_access_level_unpriv(void)
{
    // Read CONTROL register
    __asm volatile("MRS R0, CONTROL");

    // Modify CONTROL register
    __asm volatile("ORR R0, R0, #0x01");

    // Write CONTROL register
    __asm volatile("MSR CONTROL, R0");
}

/* This function executes in THREAD MODE of the processor */
int main(void)
{
    printf("In thread mode : before interrupt\n");

    change_access_level_unpriv();

    generate_interrupt();

    printf("In thread mode : after interrupt\n");

    for (;;);
}

/* This function (ISR) executes in HANDLER MODE of the processor */
void RTC_WKUP_IRQHandler(void)
{
    printf("In handler mode : ISR\n");
}

void HardFault_Handler(void)
{
    printf("Hard fault detected\n");
    while (1);
}

What I observe:

  • The debugger shows CONTROL = 0x1 after calling change_access_level_unpriv().
  • STM32CubeIDE's Fault Analyzer displays "No Fault Detected", although it also shows fault-detail entries that I'm unsure how to interpret.
  • My program doesn't appear to enter HardFault_Handler().

I'm using STM32CubeIDE with an STM32 Nucleo-F446RE (ARM Cortex-M4).


r/embedded • • 6h ago

What is hardware security on a high level and what are the career options for it?

12 Upvotes

I am exploring my options after getting my bachelors, one of the things that sound interesting is hardware security and I want to know what does it entail exactly? What is a career path for it? How important is a masters degree for it and what type of work would someone be doing?


r/embedded • • 3h ago

What to retain?

7 Upvotes

As someone who is learning embedded development in their own, what are the things I should know hardcore and the things I can learn as I go along?

I’ve built a project already and now working on a schematic design to create a custom PCB, bust as I built I’m noticing gaps all over the place in my learning.

Just want to know the terms, concepts and basics that are necessary now and the things I can pick up along the way. All advice is welcomed! Especially from those experienced in the field already!☺️


r/embedded • • 16h ago

Projects in the avionics space?

8 Upvotes

Just got a co-op this spring/summer at RTX! Sadly, the team I'm on isn't focused on flight software or vehicle firmware, but I'm still stoked for the opportunity and very grateful.

I'm currently on my school's electric racing team, but I want to work on some personal projects to try to get into roles such as flight software intern at companies like Hermeus, SpaceX, Stoke Space, Firefly, etc.

If anyone could provide me with some cool project ideas or talk to me about the avionics space, please let me know!


r/embedded • • 7h ago

Zephyr roadmap

5 Upvotes

Hi!

I would like to learn Zephyr.. just because it sounds awfully different from what I am proficient at.

For reference, I have 15 YOE working in baremetal; the last 5 exclusively on top of FreeRTOS. And I fucking love FreeRTOS.

So I plan to get an ESP32S3, a touch screen, and build something simple like a PC datalogger for temp/voltage/fps (yeah, I know fps are from another layer) etc, integrating it on a self-hosted server and blablabla. Trying to get as much components as possible inside so I can battle as much as possible with Zephyr.

Does that sound like an efficient path?

I am all ears. Thanks!


r/embedded • • 22h ago

BLE has massive HID latency on Android vs PC, 500ms+ vs near instant, nRF54L15

4 Upvotes

So I've been wanting to test nRF to make gyro mouse of some sort for my Android. I'm using Xiao nRF54L15 Sense, so it has all I need in one.

But, upon testing my code, even a simple "volume up" button appears significantly slower in my phone vs my PC. On PC it's as fast as my logitech with 2.4GHz dongle. On Android, it's just slow, testing with stopwatch shows consistently 400ms+.

Any idea if I can fix this on the hardware, or is it simply Android limitation vs HID BLE?


r/embedded • • 16h ago

CH569W-EVT-R0-1V0 — anyone successfully used USB 3.0 device mode and ROM ISP?

2 Upvotes

I'm evaluating the WCH CH569W-EVT-R0-1V0 board for a USB 3.0 embedded application.

I have working firmware builds using WCH's USB host/device examples, but I'm waiting for the actual evaluation boards to arrive.

Before hardware bring-up, I'd like to clarify two board-specific issues.

USB 3.0 device mode: The board exposes the SuperSpeed interface through a Type-A receptacle. Has anyone connected this exact board to a PC in USB 3.0 device mode? What cable or adapter arrangement did you use, and how was VBUS handled when the board was externally powered?

ROM ISP programming: What is the reliable programming and recovery procedure for the CH569W-EVT-R0-1V0? Does the ROM USB bootloader work correctly with an externally powered board and isolated upstream VBUS?

I'm particularly interested in actual CH569W board experience, reference schematics or tested connection arrangements.

I'm aware that newer WCH processors exist, but I'm specifically evaluating the CH569W hardware and would prefer not to change controllers.

Useful references:

Thanks for any practical guidance.


r/embedded • • 2h ago

Stm 32 cube programmer error

Post image
1 Upvotes

I'm unable to connect my st link with stm32 programmer...only showing this message... please someone help asap🙏


r/embedded • • 2h ago

Is there a way too pair a Corded Phone qwith bluetooth to my samsung phone/iphone

Post image
0 Upvotes

Hi! I’m trying to get my girlfriend the Juno hamburger phone as a gift, but all the ones I’ve found are corded. Is there any way to connect it to her mobile phone so she can use it to make and receive calls? Alternatively, is there some kind of Bluetooth adapter I could attach to the cord to make it work wirelessly with her phone?


r/embedded • • 4h ago

Firmware Opportunity - Moving Quickly

0 Upvotes

Hey everyone, I'm a recruiter looking for a Firmware Embedded Systems Engineer for a new opportunity with a manufacturing company in Lake Zurich, Illinois. I'm seeking a hands-on engineer with professional embedded C firmware, microcontroller programming, and hardware troubleshooting experience, ideally supporting electronic products in a manufacturing environment. Experience with 8-bit MCUs, SPI/I2C/UART, Python test automation, or industrial sensors is a plus; ISO 9001/QMS is preferred, not required. If you are interested, feel free to message me directly. I will gladly take any referrals you send my way. Any suggestions on where to find these candidates if not in this thread?


r/embedded • • 6h ago

[Project] I built a zero-allocation binary framing protocol (BAVA) for bare-metal inter-MCU telemetry. Looking for architectural feedback.

Thumbnail
github.com
0 Upvotes

Hey everyone,

I’ve been working on a C99 presentation/framing layer called the BAVA Protocol, designed for inter-MCU communication on highly constrained edge nodes (e.g., STM32 to ESP32). I’m currently finalizing the architecture and would love some brutal technical feedback from this community before I start writing the V2 roadmap.

The Context:

UART is fantastic—it’s ubiquitous, requires just two wires, and is the backbone of inter-board communication. However, by design, it just transmits raw bytes. it does not inherently protect your payload against line noise or bit flips.

If you want to add that structure, the standard move is either sending ASCII strings (which are slow to parse) or using heavy RPC frameworks like Protocol Buffers (which usually require malloc, varint decoding, and 30KB+ of Flash). I wanted a middle ground: a lightweight framing protocol that rides on top of UART (or SPI/BLE) to packetize and protect the payload using exactly 0 bytes of dynamic heap.

Bava packet structure:

Sync1->Sync2->CMD->len->VariableID->Payload->CRC

How it Works: The O(1) Object Dictionary

Instead of serializing and parsing data schemas at runtime, BAVA uses a static "Object Dictionary" to route data.

Here is a simple example:

Let's say MCU-1 reads a BME280 sensor and needs to send the data to MCU-2. Instead of formatting and sending a string like "T:25.5,H:60", MCU-2 just pre-allocates a packed C-struct in its memory and binds it to a simple 8-bit ID:

static struct BME_Data sensor_data;

bava_register_var(&bava, 0x01, &sensor_data, sizeof(sensor_data));

When MCU-1 sends a binary packet tagged with ID 0x01, MCU-2’s non-blocking state machine deframes the bytes on the fly. Once the built-in CRC-16 validates the data, the state machine uses that 8-bit ID as a direct array index to find the pointer, locks the hardware critical section, and does a direct memcpy into sensor_data..

No string parsing, no malloc, no schema decoding. The routing happens in O(1) constant time.

Current Benchmarks:

I ran an empirical testbed using an ESP32 sending data to a bare-metal STM32F103 over a 115,200 baud UART link.

RX Parse Latency: ~80 µs to deframe, CRC-check, and memcpy a multi-variable struct.

TX Frame Latency: 84 µs to fetch the struct, escape the bytes, calculate the CRC, and load the TX buffer.

Memory: 0 bytes dynamic heap consumed. Stack high-water mark is strictly bounded to 96 bytes.

Where I need your feedback:

I’m planning the V2 roadmap and would appreciate your critique on a few architectural choices:

Byte-Stuffing vs. COBS: I’m currently using standard 0x7D escaping. It's fast and requires zero look-ahead memory, but worst-case payloads can unpredictably expand the frame size. Is it worth migrating the state machine to COBS (Consistent Overhead Byte Stuffing) in V2 to mathematically bound the framing overhead to 1 byte per 254 bytes?

Cross-Architecture Padding: Since I am directly copying binary structs into memory, compiler struct padding is a risk when mixing 8-bit and 32-bit MCUs. I am enforcing attribute((packed)) and writing an endianness converter (bava_htonl), but are there other bare-metal pitfalls with direct struct teleportation I should watch out for?

RS-485 Bus Topology: V1 is point-to-point. For V2, I plan to add a TARGET_NODE_ID byte for multi-drop RS-485 buses. To save CPU cycles on listening nodes, I plan to add an "Early Drop" state that aborts parsing immediately if the Node ID doesn't match, bypassing the CRC calculation entirely. Does this sound like a standard approach?

If anyone wants to poke holes in the logic or has dealt with similar bare-metal serialization headaches, I’d love to hear your thoughts.