Zephyr RTOS auf dem Teensy 4.1 mit PlatformIO

Zephyr RTOS ist ein Open-Source-Echtzeitbetriebssystem für Mikrocontroller -- unterstützt von der Linux Foundation, mit Treibern für hunderte Boards und einem mächtigen Device-Tree-System. Der offizielle Weg ist das West-Build-System, das eine eigene Welt aus Manifest-Dateien, Toolchains und SDK-Downloads mitbringt.

Es geht auch einfacher: PlatformIO kann Zephyr als Framework einbinden. Kein West, kein SDK-Download von Hand -- platformio.ini schreiben, bauen, fertig. In diesem Post zeige ich das am Beispiel eines Teensy 4.1 (NXP iMXRT1062, ARM Cortex-M7 @ 600 MHz).

Wichtig: PlatformIO liefert für den Teensy aktuell Zephyr 2.7 (von 2021). Das aktuelle Zephyr ist 3.7+. Die API hat sich seitdem deutlich geändert -- der Code in diesem Artikel nutzt die 2.7-API, die mit PlatformIO funktioniert. Einen Vergleich zur modernen API gibt es am Ende.

Warum Zephyr auf dem Teensy?

Der Teensy 4.1 hat mit dem iMXRT1062 einen der schnellsten Cortex-M7 auf dem Markt. Die meisten Leute nutzen ihn mit dem Arduino-Framework (Teensyduino). Das funktioniert, aber:

Zephyr bringt das alles mit: Threads, Semaphoren, Kernel-Timer, ein USB-Stack, und ein sauberes GPIO-API über Device Tree. Für Projekte, die über loop() hinausgehen, lohnt sich der Umstieg.

Setup: PlatformIO + Zephyr

platformio.ini

[env:teensy41]
platform = teensy
board = teensy41
framework = zephyr

Das ist alles. PlatformIO lädt beim ersten Build das Zephyr-SDK, die Toolchain und den Board-Support herunter. Kein west init, kein Manifest.

Projektstruktur

mein_projekt/
├── platformio.ini
├── src/
│   └── main.c
└── zephyr/
    ├── CMakeLists.txt
    ├── prj.conf
    └── app.overlay      (optional, für Device Tree)

Der zephyr/-Ordner enthält die Zephyr-spezifischen Konfigurationsdateien. prj.conf aktiviert Kernel-Features (GPIO, Serial, USB), CMakeLists.txt bindet die Quellen ein, und app.overlay überschreibt Device-Tree-Knoten.

Das "Hello World" von Embedded -- eine LED blinken lassen. Auf dem Teensy 4.1 ist die Onboard-LED an led0 im Device Tree definiert.

zephyr/prj.conf

CONFIG_GPIO=y

zephyr/CMakeLists.txt

cmake_minimum_required(VERSION 3.13.1)
include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(blink)

FILE(GLOB app_sources ../src/*.c*)
target_sources(app PRIVATE ${app_sources})

src/main.c

#include <zephyr.h>
#include <drivers/gpio.h>

/* Teensy 4.1 onboard LED */
#define LED_NODE  DT_ALIAS(led0)
#define LED_LABEL DT_GPIO_LABEL(LED_NODE, gpios)
#define LED_PIN   DT_GPIO_PIN(LED_NODE, gpios)
#define LED_FLAGS DT_GPIO_FLAGS(LED_NODE, gpios)

void main(void)
{
    const struct device *dev = device_get_binding(LED_LABEL);
    if (!dev) {
        return;
    }

    gpio_pin_configure(dev, LED_PIN, GPIO_OUTPUT_ACTIVE | LED_FLAGS);

    while (1) {
        gpio_pin_toggle(dev, LED_PIN);
        k_msleep(500);
    }
}
pio run                    # bauen
pio run --target upload    # flashen

Die LED blinkt. Drei Dinge fallen auf:

  1. Device Tree: Kein pinMode(13, OUTPUT) -- der Pin kommt aus dem Device Tree (DT_ALIAS(led0)). Das Board-Support-Paket weiß, welcher Pin die LED ist.
  2. device_get_binding(): Holt das GPIO-Device über seinen Device-Tree-Label. Pin-Nummer und Flags kommen ebenfalls aus dem DT.
  3. k_msleep(): Zephyr Kernel Sleep -- gibt die CPU frei (im Gegensatz zu delay(), das busy-wartet).

Schritt 2: USB Serial (CDC ACM)

Der Teensy 4.1 hat einen USB-Port, der als CDC ACM Device (virtuelle serielle Schnittstelle) konfiguriert werden kann. In Zephyr braucht das etwas mehr Konfiguration.

zephyr/prj.conf

CONFIG_GPIO=y
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_LINE_CTRL=y
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Mein Teensy Projekt"

zephyr/app.overlay

&zephyr_udc0 {
    cdc_acm_uart0 {
        compatible = "zephyr,cdc-acm-uart";
        label = "CDC_ACM_0";
    };
};

Das Overlay registriert eine CDC-ACM-UART-Instanz am USB Device Controller. Das label brauchen wir, um das Device in der alten API über device_get_binding() zu finden.

src/main.c

#include <zephyr.h>
#include <device.h>
#include <drivers/gpio.h>
#include <drivers/uart.h>
#include <usb/usb_device.h>
#include <string.h>

/* LED */
#define LED_NODE  DT_ALIAS(led0)
#define LED_LABEL DT_GPIO_LABEL(LED_NODE, gpios)
#define LED_PIN   DT_GPIO_PIN(LED_NODE, gpios)
#define LED_FLAGS DT_GPIO_FLAGS(LED_NODE, gpios)

static const struct device *led_dev;
static const struct device *usb_dev;

static void uart_send(const char *str)
{
    while (*str) {
        uart_poll_out(usb_dev, *str++);
    }
}

void main(void)
{
    /* LED init */
    led_dev = device_get_binding(LED_LABEL);
    if (!led_dev) {
        return;
    }
    gpio_pin_configure(led_dev, LED_PIN, GPIO_OUTPUT_ACTIVE | LED_FLAGS);

    /* USB CDC Device holen und USB-Stack starten */
    usb_dev = device_get_binding("CDC_ACM_0");
    if (!usb_dev) {
        return;
    }

    int ret = usb_enable(NULL);
    if (ret != 0) {
        return;
    }

    /* Warten auf Host-Verbindung (DTR Signal) */
    uint32_t dtr = 0;
    while (!dtr) {
        uart_line_ctrl_get(usb_dev, UART_LINE_CTRL_DTR, &dtr);
        k_msleep(500);
        gpio_pin_toggle(led_dev, LED_PIN);  /* blinken = warte auf Verbindung */
    }

    /* Verbunden */
    gpio_pin_set(led_dev, LED_PIN, 1);
    uart_send("READY\r\n");

    /* Echo-Loop: empfangene Zeilen zurückschicken */
    char buf[64];
    int pos = 0;
    unsigned char c;

    while (1) {
        if (uart_poll_in(usb_dev, &c) == 0) {
            uart_poll_out(usb_dev, c);  /* Echo */

            if (c == '\n' || c == '\r') {
                buf[pos] = '\0';
                if (pos > 0) {
                    uart_send("GOT: ");
                    uart_send(buf);
                    uart_send("\r\n");
                    gpio_pin_toggle(led_dev, LED_PIN);
                }
                pos = 0;
            } else if (pos < (int)sizeof(buf) - 1) {
                buf[pos++] = c;
            }
        }
        k_usleep(100);
    }
}

Nach dem Flashen erscheint der Teensy als /dev/cu.usbmodemXXXX (Mac) bzw. /dev/ttyACM0 (Linux). Im Terminal verbinden:

screen /dev/cu.usbmodem2101 115200

Eingabe tippen → Board schickt GOT: ... zurück, LED toggelt.

Was hier passiert

  1. USB Device Stack: Zephyr bringt einen kompletten USB Stack mit. usb_enable() startet den USB Controller, die CDC ACM Class meldet sich beim Host als serielle Schnittstelle an.
  2. DTR-Handshake: Das Programm wartet, bis der Host die Verbindung öffnet (DTR-Signal). Erst dann werden Daten gesendet. Ohne diesen Check gehen die ersten Nachrichten ins Leere.
  3. Polling statt Interrupt: Der Echo-Loop nutzt uart_poll_in() -- einfacher als Interrupt-Driven RX, für diesen Zweck ausreichend.

Schritt 3: Python-Steuerung über Serial

Wenn die serielle Verbindung steht, kann man das Board von Python aus fernsteuern. Hier eine minimale Tkinter-GUI die sich verbindet und Befehle schickt:

#!/usr/bin/env python3
"""Minimale GUI zur seriellen Steuerung eines Mikrocontrollers."""

import tkinter as tk
from tkinter import ttk
import serial
import serial.tools.list_ports
import threading
import time


class SerialControlUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Serial Control")
        self.ser = None
        self.connected = False

        self._build_ui()
        self._refresh_ports()
        threading.Thread(target=self._rx_loop, daemon=True).start()

    def _build_ui(self):
        main = ttk.Frame(self.root, padding=10)
        main.grid()

        # Connection
        cf = ttk.LabelFrame(main, text="Connection", padding=8)
        cf.grid(row=0, column=0, sticky="ew", padx=6, pady=4)

        self.port_var = tk.StringVar()
        self.port_cb = ttk.Combobox(cf, textvariable=self.port_var,
                                     width=28, state="readonly")
        self.port_cb.grid(row=0, column=0, padx=4)
        ttk.Button(cf, text="Refresh", command=self._refresh_ports).grid(
            row=0, column=1, padx=4)
        self.conn_btn = ttk.Button(cf, text="Connect",
                                    command=self._toggle_conn)
        self.conn_btn.grid(row=0, column=2, padx=4)

        # Send
        sf = ttk.LabelFrame(main, text="Send", padding=8)
        sf.grid(row=1, column=0, sticky="ew", padx=6, pady=4)

        self.cmd_var = tk.StringVar()
        self.cmd_entry = ttk.Entry(sf, textvariable=self.cmd_var, width=40)
        self.cmd_entry.grid(row=0, column=0, padx=4)
        self.cmd_entry.bind("<Return>", lambda e: self._send_cmd())
        ttk.Button(sf, text="Send", command=self._send_cmd).grid(
            row=0, column=1, padx=4)

        # Log
        lf = ttk.LabelFrame(main, text="Log", padding=8)
        lf.grid(row=2, column=0, sticky="ew", padx=6, pady=4)

        self.log = tk.Text(lf, height=12, width=55, font=("Menlo", 10),
                           state="disabled", bg="#1e1e1e", fg="#d4d4d4")
        self.log.grid(row=0, column=0)
        self.log.tag_configure("tx", foreground="#569cd6")
        self.log.tag_configure("rx", foreground="#4ec9b0")
        self.log.tag_configure("info", foreground="#ce9178")

    def _refresh_ports(self):
        ports = [p.device for p in serial.tools.list_ports.comports()]
        self.port_cb["values"] = ports
        usb = [p for p in ports if "usbmodem" in p or "ttyACM" in p]
        if usb:
            self.port_var.set(usb[0])
        elif ports:
            self.port_var.set(ports[0])

    def _toggle_conn(self):
        if self.connected:
            if self.ser:
                self.ser.close()
            self.connected = False
            self.conn_btn.config(text="Connect")
            self._write_log("Disconnected", "info")
        else:
            try:
                self.ser = serial.Serial(self.port_var.get(), 115200,
                                          timeout=0.05)
                self.connected = True
                self.conn_btn.config(text="Disconnect")
                self._write_log(f"Connected: {self.port_var.get()}", "info")
            except Exception as e:
                self._write_log(f"Error: {e}", "info")

    def _send_cmd(self):
        cmd = self.cmd_var.get().strip()
        if not cmd or not self.connected:
            return
        self.ser.write((cmd + "\n").encode())
        self._write_log(f"-> {cmd}", "tx")
        self.cmd_var.set("")

    def _rx_loop(self):
        buf = ""
        while True:
            if self.connected and self.ser:
                try:
                    data = self.ser.read(256).decode(errors="ignore")
                    for ch in data:
                        if ch == "\n":
                            line = buf.strip()
                            if line:
                                self.root.after(0, self._write_log,
                                                f"<- {line}", "rx")
                            buf = ""
                        else:
                            buf += ch
                except Exception:
                    self.root.after(0, self._toggle_conn)
            else:
                time.sleep(0.05)

    def _write_log(self, msg, tag="info"):
        self.log.config(state="normal")
        ts = time.strftime("%H:%M:%S")
        self.log.insert("end", f"[{ts}] {msg}\n", tag)
        self.log.see("end")
        self.log.config(state="disabled")


if __name__ == "__main__":
    root = tk.Tk()
    SerialControlUI(root)
    root.mainloop()

Die GUI hat drei Bereiche: Port-Auswahl mit Connect/Disconnect, ein Eingabefeld zum Senden, und ein farbcodiertes Log (blau = gesendet, grün = empfangen). Braucht nur pyserial:

pip install pyserial
python serial_control.py

Zephyr-API: PlatformIO (2.7) vs. aktuell (3.7+)

Der Code in diesem Artikel nutzt die Zephyr 2.7 API, weil das die Version ist, die PlatformIO für den Teensy liefert. Aktuelles Zephyr (3.7+) hat die API deutlich vereinfacht. Wenn du mit West statt PlatformIO arbeitest, nutze die neue API:

PlatformIO / Zephyr 2.7 West / Zephyr 3.7+
#include <zephyr.h> #include <zephyr/kernel.h>
device_get_binding("GPIO_1") DEVICE_DT_GET(DT_NODELABEL(gpio1))
DT_GPIO_LABEL(node, prop) GPIO_DT_SPEC_GET(node, prop)
gpio_pin_configure(dev, pin, flags) gpio_pin_configure_dt(&spec, flags)
gpio_pin_toggle(dev, pin) gpio_pin_toggle_dt(&spec)
void main(void) int main(void)
device_get_binding("CDC_ACM_0") DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart)

Die _dt-Varianten in Zephyr 3.x fassen Device, Pin und Flags in einer gpio_dt_spec Struct zusammen -- weniger Boilerplate, weniger Fehlerquellen, und kein String-basiertes device_get_binding() mehr.

Wenn PlatformIO irgendwann ein Zephyr-Update für den Teensy nachzieht, muss der Code entsprechend angepasst werden. Bis dahin funktioniert die 2.7-API zuverlässig.

Weiterführendes


Erstellt: 07.04.2026