Weekend Project – Zephyr on the UNO Q: Like Bumping Into an Old Friend

You know that feeling when you run into someone on the street and you’re almost convinced you’ve met them before? Or maybe they just remind you of someone else?
That’s exactly the kind of familiar feeling I had, not with a person, but with a piece of technology.

The first time I started reading about Zephyr, scrolling through LinkedIn posts, internet forums, and technical docs, it felt strangely familiar. Not because I had actually used it before, but because its character, its tooling, the whole ecosystem of configs, devicetrees, Kconfig files, and structured build systems, reminded me so much of working with low-level Linux.

That’s how I got hooked. It felt like meeting an old friend in a new form. The more I explored, the more I realized Zephyr was worth investing time in. It felt cleaner and more thoughtfully designed than many other RTOSes I’d worked with. After that, I started watching Shawn Hymel’s videos, and honestly, they were fantastic. I learned a lot from them. But there was one small problem: I didn’t have an ESP32. I had promised myself not to buy any new boards until after the new year, so I started thinking about the Arduino UNO Q I had just bought instead. I thought: why not start there?
It’s a perfect playground, a board with both a microcontroller and a microprocessor sitting side by side. The MCU could handle the real-time and safety-critical tasks, while the Linux processor could take on heavier workloads like networking or AI.

In the official documentation for the UNO Q, Arduino describes a gRPC-based communication layer between the two cores. But I wanted to approach this differently. I wanted to run Zephyr on the microcontroller side ( not the default Arduino core), which means the official Arduino gRPC layer wouldn’t work.

Here’s the catch:
The entire gRPC setup only works when you keep the preloaded Arduino environment on the MCU. That means writing Arduino-style code and letting AppLab communicate with it through arduino-router. Once you flash Zephyr onto the STM32U585, that whole pipeline becomes incompatible.

I wasn’t planning to touch the Debian/Linux side at all, that part stays the same,but on the MCU side, I wanted a clean, full Zephyr environment.

So I started digging deeper into the hardware documentation. It turns out the board includes a physical UART link between the MCU (STM32U585) and the CPU (Qualcomm QRB2210), plus an SPI programming connection for flashing firmware. That was perfect — it meant I could use this internal UART as my communication bridge.

https://docs.arduino.cc/resources/schematics/ABX00162-schematics.pdf?utm_source=chatgpt.com

Exploring the Zephyr Device Tree

After confirming that arduino_uno_q is officially supported by Zephyr, I dove into the devicetree files.

zephyr/boards/arduino/uno_q

I first needed a simple physical indication that my firmware was running, so I looked up the LEDs defined in the merged DTS file:

build/zephyr/zephyr.dts

The LED definitions looked like:

leds {
    compatible = "gpio-leds";
    led0: led_0 {
        gpios = <&gpioX Y GPIO_ACTIVE_LOW>; /* example */
        label = "LED0";
    };
};

aliases {
    led0 = &led0;
};

So I picked LED0, the green LED, and used it in my Zephyr app as a heartbeat indicator.

Finding the MCU UART in Zephyr

I also needed to understand which STM32 UART Zephyr uses for communication.
From the merged DTS:

grep -n "lpuart1" build/zephyr/zephyr.dts

It shows something like:

lpuart1: serial@46002400 {
    compatible = "st,stm32-lpuart", "st,stm32-uart";
    ...
    pinctrl-0 = < &lpuart1_tx_pg7 &lpuart1_rx_pg8 ... >;
    current-speed = <115200>;
    status = "okay";
};

So LPUART1 on the STM32U585 is the UART intended for MCU↔CPU communication.

Finding the CPU-Side UART (Linux)

On the Linux side, it wasn’t immediately clear which UART corresponded to the MCU.
The schematics helped, but what made it crystal clear was the arduino-router.service file:

ExecStart=/usr/bin/arduino-router --serial-port /dev/ttyHS1 --serial-baudrate 115200

That told me exactly which Linux device node maps to the MCU UART:

/dev/ttyHS1

I verified it using:

adb shell dmesg | grep -i tty

Which printed:

4a88000.serial: ttyHS1 at MMIO...

At that point, the mapping was completely clear:

  • MCU UART: LPUART1
  • CPU UART: /dev/ttyHS1

Freeing the UART (Disabling the Arduino Router)

The problem:
arduino-router.service owns /dev/ttyHS1 by default.
That makes sense, it’s how AppLab communicates with the Arduino firmware.

But since I flashed Zephyr, that service became incompatible and blocked access to the UART.

So I had to disable it:

adb shell "systemctl stop arduino-router && systemctl disable arduino-router"

Immediately, the UART became free.

Flashing Zephyr and Testing Communication

I built my Zephyr app, pushed it to the board:

adb push build/zephyr/zephyr.hex /tmp

Then I flashed the MCU using OpenOCD through the Linux processor (using the SPI programming link):

  • ADB transfers the firmware
  • Linux runs arduino-debug.service
  • OpenOCD flashes the STM32

Once the board rebooted and the green LED started blinking, I knew Zephyr was running.

Then came the moment of truth:

adb shell "stty -F /dev/ttyHS1 115200 raw -echo"
adb shell "hexdump -C /dev/ttyHS1"

And there it was:

Seeing those UART frames come through felt like taking a deep breath.
The MCU and CPU were finally talking, through my own Zephyr firmware, not Arduino’s runtime.

What’s Next

The next step is to write a more robust Python service on the Linux side to:

  • parse framed binary data
  • validate CRC
  • handle ACK/NACK
  • expose a clean API for higher-level apps
  • optionally integrate with AppLab

And on the MCU side, I’ll evolve the Zephyr code into a proper, deterministic communication layer.

This was just the first milestone — the “LED blinks and UART works” moment, but it’s the foundation for everything that comes next.

Bye for now, and see you in the next Weekend Project.

Visited 291 times, 1 visit(s) today