Tprrt's Blog

Embedded Linux, Zephyr RTOS, open-source hardware, Linux gaming, retro gaming, and competitive fitness

Oct 22, 2026

Plugging a Mini-HDMI into a Running Game: Six Wrong Turns on an RG353M

Introduction

The Anbernic RG353M is a handheld built around a Rockchip RK3568, with a 640x480 DSI panel and a mini-HDMI connector. Recalbox runs on it, and until now the HDMI output only half worked: plug the cable before powering on and the menu appears on the TV, but plug it afterwards and nothing follows. This article is about making the menu, the boot video, and above all a running game follow the cable, in both directions.

The fix is a merge request on the Recalbox side and a few patches on RetroArch's KMS context. It took six detours. In each one my first explanation was wrong, and a log line or a file date told me so.

The setup, for reference:

  • Recalbox, a Buildroot external tree, target rg353x, Linux 6.16.5;
  • RetroArch 1.22.2, plus a stack of local patches;
  • the Recalbox frontend, an EmulationStation fork, drawing through SDL's KMSDRM backend;
  • two connectors: DSI-1 (the panel) and HDMI-A-1.

A note on method: I build in a rootless Podman container, flash an SD card, test on the real device and paste the logs back. A cycle takes several minutes, so each wrong guess is expensive, and that shaped how this went.

Wrong turn 1: the game that exits right away

The first piece was easy. The frontend reads /sys/class/drm/card*-HDMI-A-*/status every half second, waits for three identical reads (about 1.5 s) to ignore a sink that bounces while the link comes up, and then relaunches itself. SDL's KMSDRM backend picks its connector once, at init, so a new process is the simplest way to follow the cable. After that, the menu showed up on the TV.

Then I started a game on the TV and it quit immediately, back to the menu. RetroArch's log had the reason:

[WARN] [DRM] Couldn't find DRM encoder.

RetroArch's KMS context looks for the encoder the selected connector is currently using. When the board boots without a cable, HDMI-A-1 has no encoder at all, because nothing ever drove it. Plugged in later, the connector is connected but has none, and the context gives up.

The fix is what a modeset does anyway: take one of the connector's possible encoders, and a CRTC that is off, falling back to a lit one only when none is free. A second small fix goes with it. RetroArch asks for the size the game was started with, 640x480 on the panel; the TV has no such mode, and the start failed again. Falling back to the display's first mode costs nothing.

Wrong turn 2: the patch that never reached the image

I added the patch, rebuilt, flashed. The same error. The RetroArch log printed its build date:

[INFO] Built: Oct 20 2026

Two days old. The patch was in the source tree, the package had been rebuilt, and the image still had the old binary.

Recalbox builds with BR2_PER_PACKAGE_DIRECTORIES=y. Each package gets its own copy of the target tree, assembled from its dependencies. Three packages that depend on retroarch (retroarch-assets, libretro-holani and libretro-mojozork) carried their own copy of the old /usr/bin/retroarch, and when the final tree is put together theirs won. Rebuilding only the dependency leaves the stale files in the dependents.

The fix is to clean the dependents too, and the check is cheap: the Built: line, or strings on the binary in output/target. I should have looked at that line before touching the code. It is the first thing I do now.

Wrong turn 3: restarting everything just to change the screen

With games starting on the TV, I wanted the next step: plug or unplug the cable during a game and have the game move. RetroArch already knows how to restart its video driver; what it lacked was a reason to do it.

The context polls the connectors once a second from check_window, and asks for a restart when the monitor index would now pick another display. Three details made it behave:

  • Debounce: a restart only on two identical polls in a row, as with the frontend side;
  • A loop guard: a connector already tried is not requested again, so a display with no usable mode cannot cause an endless restart;
  • Where it runs: the poll runs on the video thread, which cannot restart itself. It sets a flag, and the main loop does the restart.

It worked. The log showed the connector change four times in one game, HDMI to panel to HDMI to panel, and the game kept running. But the sound cut at each switch. The reason was one argument: command_event(CMD_EVENT_REINIT, NULL) restarts all the drivers, audio included, and I had also suspended audio explicitly around it. Passing a mask of video, input and menu was enough. In the next log Started synchronous audio driver appears once for the whole game, not once per switch.

Wrong turn 4: the panel that stayed black

This one cost the most. Boot with the cable plugged, unplug it, and the frontend relaunches on the panel (Trying Resolution: 640,480), but the panel stays black. The backlight reads 128. The DRM state, dumped from debugfs, is identical to the working case. Stopping the frontend does not bring the console back either, so the panel is not being scanned out whatever userspace does.

I proposed three explanations to myself and had to retract all of them:

  • the date of /dev/dri/card0 looked like the device had been recreated. It is the last access time;
  • the plane assignment looked different. The working case had the same one;
  • the Dim screensaver sets brightness to a smaller step, and brightness 8 fits (7 >> 1) = 3 then 1 << 3. It was not the cause here, since the backlight read 128 with a black panel.

What finally gave a direction was a line in dmesg, present on every boot with the cable plugged, around the start of fbdev:

rockchip-vop2 fe040000.vop: [drm:rk3568_vop2_setup_overlay] *ERROR*
    wait layer cfg done timeout: 0x54760213--0x54762013

On the RK3568 the VOP2 has several video ports sharing the layer and port select registers, and those registers are latched by the vsync of one of the ports. The 6.16.5 code waits for the wrong value, and switches the vsync source before the previous latch happened. With two ports started together, HDMI and DSI at boot, the configuration of one of them may never be applied.

Four upstream commits fix this, the last two merged in June 2026. I took them unchanged as kernel patches, in order, so that together they give the upstream rk3568_vop2_setup_layer_mixer(). After flashing, the error is gone from dmesg and the panel comes back after an unplug.

The link between that error and the black panel is only a hypothesis. An earlier boot logged the same error and the panel had worked. The patches are correct on their own merits and the symptom went away, but I cannot say one caused the other.

Wrong turn 5: a panel that was "on" at brightness 0

Next, a game on HDMI, then unplug. RetroArch switched to the panel (Connector 54 is now the display to use), the DSI link was enabled and connected, and the panel was black again:

# cat /sys/class/backlight/backlight/brightness
0
# cat /sys/class/drm/card0-DSI-1/enabled /sys/class/drm/card0-DSI-1/status
enabled
connected

Brightness zero, with the video path fine. The frontend keeps the panel dark while HDMI is in use, and a commit I had written was supposed to restore it on unplug. The code looked right. So I compared the file the build had compiled with the one in the source tree:

build/.../es-app/src/MainRunner.cpp   2026-10-21 13:03
projects/frontend/es-app/src/MainRunner.cpp   2026-10-21 21:06

The frontend is a Buildroot package with SITE_METHOD = local. It is copied into the build directory once, and a plain make never copies it again. My advice to rebuild with a plain make was wrong for this package; it needs recalbox-emulationstation2-rebuild, which clears the rsync stamp. A diff -r of the two trees, which must print nothing, is now part of my pre-flash checklist.

Wrong turn 6: a relaunch that took 25 seconds

With everything working, the one thing that felt wrong was time. Unplug in the menu and the screen stayed blank for ten to twenty seconds. The detection is instant (two lines in the log). What was slow was the frontend quitting.

The log of the quit gave the answer, once I looked at the gaps instead of the messages. The unplug came at 12:14:42, about eleven seconds after the frontend had started. The Hasher thread, which computes ROM hashes, exited at 12:15:01, about thirty seconds after that start, while every other thread had stopped within a couple of seconds.

// Initial delay
Thread::Sleep(30 * 1000);

The hasher waits thirty seconds before starting its work, and that sleep cannot be interrupted. Quitting inside that window waits for the end of it. Other threads had the same shape: the netplay and command threads sleep five seconds at the start, and the file watcher and others one second per loop. Together they made up the five seconds of teardown I could see even when the hasher was idle.

The fix is small: a Thread::SleepWhileRunning(ms) that sleeps in steps of at most 50 ms and returns as soon as the thread is stopped, used in those loops. The order of the sleeps is unchanged, only the exit is faster. Thread.h has Windows line endings in the repository, and my first scripted edit silently converted the whole file. The diff stat (240 lines for a 14-line change) caught it before the commit.

Epilogue: upstream got there first

When I went to rebase the RetroArch branch on the current master, the first commit conflicted. Upstream had landed, that morning, its own version of the same work: a free CRTC for a display nothing had set up, a fallback to the first mode, and a udev-based hotplug watch that restarts the video driver when the display in use goes away. Two of those commits credit me as co-author.

I had not looked at upstream between writing the patches and rebasing them, and that is the lesson I take from this: check what is already on master before the third commit, not after the fifth.

The upstream behaviour has one gap for my use case. If the panel is in use and HDMI is plugged in, the output stays where it is: the restart only happens when the display in use is lost. The change I could add on top is four lines: with the default monitor index, a display that now ranks before the one in use takes over, and an index set by the user is left alone. It has a unit test, and it worked on the board, but it needs a newer RetroArch than 1.22.2. Recalbox keeps its three patches for now.

One of my patches had an all-zero From line, written by hand when I ported it to the 1.22.2 tree. That is not a valid patch header. Apply the patch on a real branch of the upstream repository at the target tag, commit it, and regenerate the file with git format-patch so the commit ID, date and blob hashes are real.

What is left

  • The frontend relaunch is still a whole process restart. The quit is fast now, but the init takes about eight seconds, two for the window and five and a half to load the systems, plus the time the TV needs to lock the HDMI link. An in-place display reinit, which another merge request on the Recalbox side proposes, would remove most of it.
  • Only RetroArch cores follow a live switch. A standalone emulator keeps the display it picked until it exits.
  • The audio output is not switched automatically, and it stops during a frontend relaunch.
  • I decided against pausing the game during a switch. The restart already blocks it for about a second.

The work is in the Recalbox merge request. A second board would tell me whether the black panel was really the VOP2 race.

posted at 23:00  · 10 min read  ·   ·  recalbox  retroarch  kms  drm  rockchip  buildroot  embedded  linux  retro  article

May 26, 2026

RIOT-OS 2026.04 "Fire Rizzlease" is out

RIOT-OS 2026.04, codenamed Fire Rizzlease, was released on May 6, 2026. RIOT is a multi-threading operating system targeting microcontrollers found in the Internet of Things — from 8-bit and 16-bit MCUs to lightweight 32-bit processors — with a focus on energy-efficiency, soft real-time capabilities, and a small memory footprint.

This release spans 84 days of development, 125 merged pull requests composed of 229 commits, and contributions from 27 people. 1,141,523 lines were deleted against 25,007 inserted, mostly from a vendor code cleanup.

Codeberg mirror

RIOT is now synced to Codeberg (#21997), a non-profit community forge, alongside GitHub.

Massive EFM32 vendor code removal

Over one million lines of vendor code have been removed from the EFM32 family, replaced by a pkg/gecko_sdk dependency (#22040). This accounts for most of the deletions and shrinks the codebase for Silicon Labs EFM32-based boards.

Raspberry Pi Pico 2 / RP2350 improvements

RP2350 support was reworked (#21753):

  • Unified abstractions between the RISC-V and ARM cores of the RP2350.
  • Added the XH3IRQ interrupt controller.
  • Updated UART driver.
  • Added RISC-V support.

The scope of this work was large enough to inspire a bachelor's thesis at HAW Hamburg.

New board and CPU support

Three new targets join the supported hardware list:

  • pro-micro-nrf52840 (#22089) — a popular nRF52840-based Pro Micro form-factor board.
  • slstk3301a (#22069) — Silicon Labs EFM32 Tiny Gecko starter kit.
  • STM32H7 (#21978) — high-performance STM32 family, with additional peripheral support for the nucleo-h753zi (#22076).

New device drivers

  • AMG88xx (#22104) — infrared array sensor (thermal camera) from Panasonic.
  • ADS1X1X (#21694) — family of Texas Instruments I²C ADCs.

Guide site and documentation

The RIOT Guide Site is the entry point for new users and is replacing Doxygen for prose documentation. This release adds:

  • More tutorials.
  • An experimental Supported Boards section.
  • Unit tests in tutorials (#22042).
  • Updated Astro v6 framework (#22145).

The Doxygen API reference remains available at api.riot-os.org.

Networking improvements

Several additions to the GNRC networking stack:

  • New gnrc_pktshark module to pretty-print network traffic (#21284).
  • gnrc_ipv6_nib_dyn_lladdr_get() API (#22013).
  • ABR (Authoritative Border Router) now run-time configurable (#21081).
  • Generic UDP shell command (#22049).

Notable bug fixes

43 bugs were fixed in this release, including:

  • ESP8266 crashes on reboot and startup (#22014, #22010).
  • Potential buffer overflow in the atwinc15x0 driver (#22041).
  • NanoCoAP message corruption in coap_build_reply() (#22094).
  • Wrong byte order for gyro and accelerometer reads in the MPU-9x50 driver (#22135).
  • LVGL configuration and SDL issues on native (#22005, #22139).
posted at 12:00  · 2 min read  ·   ·  riot  rtos  embedded  iot  article

Jul 29, 2022

Zephyr Device Tree Guide

Introduction

The Zephyr project, hosted by the Linux Foundation since 2016, provides a real-time operating system (RTOS), under the Apache 2.0 license, for connected devices too small for Linux or acting as companions to a Linux core. It targets microcontrollers and IoT devices, is modular, and is written in C (C11, with C++17 supported).

It can be configured to run on devices with as little as 10 KB of memory. It supports 32-bit and 64-bit architectures (Cortex-A, Cortex-M, Cortex-R, RISC-V, x86-64, etc.), many boards (Feather, nRF52840, ST Discovery, ST Nucleo, ESP32, etc.), connectivity such as Bluetooth, Ethernet, Wi-Fi and LoRa, and network protocols including IPv4, IPv6, UDP, TCP, CoAP, LWM2M, MQTT and DNS.

Like Linux, Zephyr uses Kconfig, and its device model is mainly based on device tree.

Device tree

Device trees are tree data structures that describe the hardware components and their relationships in a system. They are stored in text files, the device tree sources (*.dts), which developers write to describe the hardware of SoCs and boards. The operating system uses them to determine how to initialize and interact with the hardware.

Each node describes a device of the system, has properties that describe its characteristics, and has exactly one parent (except the root node).

Each device driver is associated with a specific device tree node, which represents a hardware component in the system. The device driver provides the necessary code and data to control the behavior of the hardware component.

test_i2c_bme280: bme280@6 {
        compatible = "bosch,bme280";
        reg = <0x6>;
};

In the Linux kernel, device tree sources are compiled to device tree binaries (dtb) that the bootloader stages (U-Boot, TF-A...) and the kernel parse at boot, so the same binaries can support several hardware configurations.

In Zephyr, device tree sources are instead turned at build time into a devicetree_generated.h C header. It contains the macros and data structures that let drivers read the hardware description, such as a device's memory mapping, pin assignments and IRQ numbers:

#define DT_COMPAT_HAS_OKAY_bosch_bme280 1
#define DT_N_INST_bosch_bme280_NUM_OKAY 1
#define DT_FOREACH_OKAY_bosch_bme280(fn) fn(DT_N_S_soc_S_i2c_40005400_S_bme280_77)
#define DT_FOREACH_OKAY_VARGS_bosch_bme280(fn, ...) fn(DT_N_S_soc_S_i2c_40005400_S_bme280_77, __VA_ARGS__)
#define DT_FOREACH_OKAY_INST_bosch_bme280(fn) fn(0)
#define DT_FOREACH_OKAY_INST_VARGS_bosch_bme280(fn, ...) fn(0, __VA_ARGS__)
#define DT_COMPAT_bosch_bme280_BUS_i2c 1

Where:

  • DT_COMPAT_HAS_OKAY_bosch_bme280: indicates that there is at least one instance of BME280
  • DT_N_INST_bosch_bme280_NUM_OKAY: defines the number of BME280 instances that are marked okay
  • DT_FOREACH_OKAY_bosch_bme280: allows you to apply a function fn to each instance of the BME280
  • DT_FOREACH_OKAY_VARGS_bosch_bme280: also allows you to apply a function fn to each instance of the BME280, but with additional arguments
  • DT_FOREACH_OKAY_INST_bosch_bme280: allows you to apply a function fn to each instance of the BME280, passing the instance number as an argument
  • DT_FOREACH_OKAY_INST_VARGS_bosch_bme280: is similar to the previous macro, but this one allows for additional arguments
  • DT_COMPAT_bosch_bme280_BUS_i2c: indicates that the BME280 device is connected to an I2C bus.
  • DT_N_S_soc_S_i2c_40005400_S_bme280_77: refers to a specific node in the device tree, here it refers to the BME280 sensor connected to the I2C controller with the base address 0x40005400 within the SoC. The sensor's address on this I2C bus is 0x77.

In addition, device tree sources can be extended or overridden, for example to connect additional devices to a board, or to disable board devices which will not be used:

/ {
        aliases {
                bme280 = &bme280;
        };
};

&spi1 {
        status = "disabled";
};

&i2c1 {
        status = "okay";
        bme280: bme280@77 {
                compatible = "bosch,bme280";
                reg = <0x77>;
        };
};

Binding

The content of device tree sources is described in binding files, written in YAML. They are also used to validate device tree sources against the YAML description.

description: BME280 integrated environmental sensor

compatible: "bosch,bme280"

include: [sensor-device.yaml, i2c-device.yaml]

Device driver

A Zephyr driver reads the properties of its device tree node through macros defined in C headers. For example, this code initializes a BME280 sensor using properties from the device tree:

#include <device.h>
#include <drivers/i2c.h>
#include <devicetree.h>
#include <zephyr.h>

// Define the node identifier for the BME280 sensor
#define BME280_NODE DT_N_S_soc_S_i2c_40005400_S_bme280_77

// Function to initialize the BME280 sensor
static int bme280_init(const struct device *dev)
{
    // Check if the node is available
    if (!device_is_ready(dev)) {
        printk("Device %s is not ready\n", dev->name);
        return -ENODEV;
    }

    // Retrieve the I2C device associated with the BME280 node
    const struct device *i2c_dev = DEVICE_DT_GET(DT_BUS(BME280_NODE));

    if (!device_is_ready(i2c_dev)) {
        printk("I2C device not ready\n");
        return -ENODEV;
    }

    // Write some initialization code here, such as configuring registers

    printk("BME280 sensor initialized\n");
    return 0;
}

// Initialize the BME280 sensor at boot time
SYS_INIT(bme280_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);

Conclusion

Anyone who has written a BSP or driver for Linux will find this familiar. It is a steeper step for people coming from bare microcontroller development.

posted at 20:32  · 4 min read  ·   ·  zephyr  device-tree  rtos  embedded  article