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:
- Kein echtes Threading / Task-Scheduling
- Kein standardisiertes Device-Tree-Modell
- Timer und Interrupts von Hand verwalten
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.
Schritt 1: LED Blink
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:
- 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. device_get_binding(): Holt das GPIO-Device über seinen Device-Tree-Label. Pin-Nummer und Flags kommen ebenfalls aus dem DT.k_msleep(): Zephyr Kernel Sleep -- gibt die CPU frei (im Gegensatz zudelay(), 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
- 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. - 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.
- 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
- Zephyr Dokumentation -- offizielle Doku
- PlatformIO Zephyr Guide -- PlatformIO + Zephyr Setup
- Teensy 4.1 -- Board-Info von PJRC
- Mikrocontroller -- Board-Vergleich in der Knowledgebase