UART-Seriell-Architektur & Industrielles RS-485 / Modbus RTU
Asynchrone serielle Schnittstellen (Universal Asynchronous Receiver-Transmitter - UART) gehören zu den langlebigsten Standards der Computertechnik. Auf Einplatinencomputern erfüllt UART zwei wesentliche Aufgaben: als hardwarenahe Debug-Konsole für frühe Boot-Meldungen des Linux-Kernels und als Schnittstelle für industrielle Sensoren und Steuerungen.
In der Gebäudeautomation, Industriefertigung und bei Photovoltaik-Messungen halten einfache 3,3-V-Logiksignale langen Kabelstrecken und elektromagnetischen Störfeldern großer Elektromotoren nicht stand. Durch die Kopplung der Hardware-UART des Raspberry Pi mit einem differentiellen RS-485-Transceiver lassen sich Strecken von bis zu 1.200 Metern mit dem bewährten Modbus RTU-Industrieprotokoll störungssicher überbrücken.
1. Die Raspberry Pi UART-Hardware-Matrix (PL011 vs. Mini-UART)
Im SoC des Raspberry Pi arbeiten zwei grundlegend verschiedene UART-Controller:
- PL011 UART (
/dev/ttyAMA0): Ein vollwertiger ARM-Standard-UART mit separaten Taktgeneratoren und 32-Byte-FIFO-Puffern. Sein Takt ist vollkommen unabhängig von der CPU-Taktung oder thermischem Throttling. - Mini-UART (
/dev/ttyS0): Ein reduzierter Hilfs-UART. Bei älteren Modellen (Pi 3/4) leitet sich sein Takt direkt vom VPU-Kerntakt des VideoCore-Grafikprozessors ab. Taktet die CPU bei Hitze herunter, verschiebt sich der Kerntakt – die serielle Baudrate gerät aus dem Takt und Datenpakete werden zerstört.
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph SoC["Raspberry Pi SoC Hardware"]
PL011["PL011 Hardware-UART<br/>(Hohe Stabilitaet, unabhaengig)"]
MiniUART["Mini-UART<br/>(Abhaengiger Kerntakt,<br/>fehleranfaellig)"]
end
subgraph Routing["Linux Pin-Routing (/dev/serial0)"]
BT["Onboard Bluetooth-Modul"]
Header["40-Pin-Leiste:<br/>GPIO 14 (TX) & 15 (RX)"]
end
PL011 -.->|Standard auf Pi 3/4| BT
MiniUART -.->|Standard auf Pi 3/4| Header
PL011 ==>|Enterprise: disable-bt| Header
Den vollwertigen PL011-UART für die Stiftleiste freigeben
Standardmäßig verbindet Raspberry Pi OS den präzisen PL011-UART mit dem Bluetooth-Chip und legt den jitteranfälligen Mini-UART auf Pin 8 (GPIO 14 / TX) und Pin 10 (GPIO 15 / RX). Für industrielle Sensorbusse muss Bluetooth deaktiviert und der PL011-Controller auf die Stiftleiste gelegt werden.
Schritt 1: Firmware-Konfiguration anpassen
Datei /boot/firmware/config.txt öffnen:
Folgende Direktiven am Dateiende ergänzen:
# Primaeren UART auf Stiftleiste aktivieren
enable_uart=1
# Bluetooth deaktivieren und PL011-UART auf GPIO 14/15 routen
dtoverlay=disable-bt
Schritt 2: Serielle Kernel-Konsole entfernen
Der Linux-Kernel startet ab Werk eine interaktive Login-Konsole auf /dev/serial0 mit 115200 Baud. Sendet ein Sensor während des Systemstarts Daten, interpretiert systemd dies als Tastatureingaben, was den Bootvorgang blockieren kann.
Datei /boot/firmware/cmdline.txt bearbeiten:
Den Eintrag console=serial0,115200 (oder console=ttyAMA0,115200) vollständig löschen.
Die restlichen Boot-Parameter müssen auf einer einzigen Zeile verbleiben:
Schritt 3: Bluetooth-Dienst deaktivieren & Neustart
Nach dem Neustart kontrollieren, ob /dev/serial0 direkt auf ttyAMA0 verweist:
2. Differentielle Signalübertragung: Der RS-485 Physical Layer
Klassisches UART bezieht jedes Signal auf eine gemeinsame Masseleitung. Über weite Strecken führen Ausgleichsströme zu Potenzialunterschieden zwischen Gebäudeteilen, während parallele 400-V-Starkstromkabel Störimpulse induzieren.
RS-485 löst dieses Problem durch symmetrische, differentielle Signalübertragung über ein verdrilltes Adernpaar (Twisted Pair), bezeichnet als A (Data+) und B (Data-):
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph Host["Raspberry Pi + RS-485 Transceiver"]
direction TB
TX["TX-Pin (3,3 V Logik)"]
RX["RX-Pin (3,3 V Logik)"]
end
subgraph Cable["Verdrilltes Adernpaar (Bis zu 1200 Meter)"]
direction TB
LineA["Leitung A (Data +)"]
LineB["Leitung B (Data -)"]
end
subgraph PLC["Industrie-SPS / Modbus-Zaehler"]
direction TB
NodeA["Klemme A (+)"]
NodeB["Klemme B (-)"]
end
Host --> Cable
Cable --> PLC
- Logische 1 (Ruhezustand / Mark): VA - VB > +200 mV
- Logische 0 (Aktiv / Space): VA - VB < -200 mV
Da elektromagnetische Störfelder auf beide eng miteinander verdrillten Leiter exakt gleich wirken, hebt der Differenzverstärker des Empfängers das Gleichtakt-Rauschen vollständig auf:
Gleichtaktunterdrückung (Common Mode Rejection):
Vdiff = (VA + VStörung) - (VB + VStörung) = VA - VB
Bus-Terminierung & Abschlusswiderstände
- 120-Ω-Abschlusswiderstände: Bei langen Leitungen und hohen Baudraten reflektieren Signale an offenen Leitungsenden wie Wasserwellen an einer Betonwand. An den beiden äußersten Enden des Busses muss jeweils ein 120-Ω-Widerstand (0,25 W) parallel zwischen Leitung A und B geschaltet werden.
- Fail-Safe-Vorspannung: Wenn kein Sender aktiv ist, schweben die Leitungen. Ein 560-Ω-Pull-Up von Leitung A auf 3,3 V und ein 560-Ω-Pull-Down von Leitung B auf Masse verhindern fehlerhafte Geister-Startbits.
3. Hardware-Verdrahtung (SP3485 vs. MAX485)
Achtung bei 5V-Logikpegeln
Der klassische MAX485 benötigt 5 V Versorgungsspannung und gibt am Empfängerausgang (RO) 5 V TTL-Spannung aus. Wird ein 5V-Transceiver direkt mit GPIO 15 (RX) des Raspberry Pi verbunden, wird der Pi zerstört!
Verwende zwingend einen nativen 3,3-V-Transceiver wie den Exar/MaxLinear SP3485 oder TI SN65HVD72. Steht nur ein 5V-Modul zur Verfügung, muss zwingend ein Spannungsteiler oder bidirektionaler Level-Shifter vor den RX-Pin geschaltet werden.
Halbduplex-Richtungsumschaltung (DE/RE)
RS-485 arbeitet im Halbduplex-Betrieb: Über dasselbe Adernpaar wird abwechselnd gesendet und empfangen. Der Transceiver besitzt zwei Steuerpins: - DE (Driver Enable): Aktiv HIGH (schaltet den Sender ein). - RE (Receiver Enable): Aktiv LOW (schaltet den Empfänger ein).
Werden DE und RE miteinander verbunden und an einen freien GPIO-Pin (z. B. GPIO 18) angeschlossen, setzt der Raspberry Pi diesen Pin zum Senden auf HIGH und schaltet ihn sofort danach wieder auf LOW, um Antworten zu empfangen. Moderne Industrie-HATs nutzen automatische Hardware-Umschalter (z. B. MAX13487), die ohne manuelles Umschalten per GPIO auskommen.
| Raspberry Pi Pin | Transceiver-Pin (SP3485) | Bedeutung |
|---|---|---|
| Pin 1 (3,3 V) | VCC | 3,3 V Betriebsspannung |
| Pin 6 (GND) | GND | Logikmasse |
| Pin 8 (GPIO 14 / TXD) | DI (Driver In) | Sendedaten vom Pi zum Bus |
| Pin 10 (GPIO 15 / RXD) | RO (Receiver Out) | Empfangsdaten vom Bus zum Pi |
| Pin 12 (GPIO 18) | DE & RE (Verbunden) | Richtungsumschaltung (HIGH=Senden, LOW=Empfangen) |
| Busklemmen | Klemme A / Klemme B | Anschluss an verdrilltes Adernpaar |
4. Modbus RTU Protokollaufbau
Modbus RTU ist ein binäres Protokoll der Anwendungsschicht, das asynchron über RS-485 übertragen wird. Es basiert auf einem strikten Master-Slave-Prinzip: Der Raspberry Pi ist der Modbus Master (Client), während angeschlossene Sensoren oder Stromzähler als Slaves (Server) mit festen Geräteadressen (1 bis 247) arbeiten.
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph Frame["Modbus RTU Telegramm-Aufbau<br/>(Binaerstrom)"]
direction TB
F1["Slave-ID (1 Byte)"]
F2["Funktionscode (1 Byte)"]
F3["Nutzdaten / Register<br/>(0 - 252 Bytes)"]
F4["CRC-16 Pruefsumme<br/>(2 Bytes)"]
end
F1 --> F2 --> F3 --> F4
Wichtigste Funktionscodes
0x03(Read Holding Registers): Liest konfigurierbare 16-Bit-Register aus (Spannung, Leistung, Frequenz).0x04(Read Input Registers): Liest schreibgeschützte analoge Messwerte ein.0x06(Write Single Register): Schreibt einen einzelnen 16-Bit-Sollwert.0x10(Write Multiple Registers): Schreibt einen ganzen Block von Registern atomar.
5. Python-Praxis: Industriellen Stromzähler auslesen
Dieses Skript liest elektrische Messwerte (Spannung, Stromstärke, Wirkleistung, Netzfrequenz) von einem Hutschienen-Stromzähler (z. B. Eastron SDM630 / SDM120) über minimalmodbus ein:
sudo apt update
sudo apt install -y python3-serial python3-pip
pip3 install --break-system-packages minimalmodbus
Skript /usr/local/bin/modbus_energy_poller.py anlegen:
#!/usr/bin/env python3
"""
Industrieller Modbus RTU Energiemonitor
Liest Leistungswerte von einem Eastron Hutschienenzaehler ueber RS-485 (9600 8N1).
"""
import time
import sys
import logging
import minimalmodbus
import serial
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
class IndustrialEnergyMeter:
def __init__(self, port: str = "/dev/serial0", slave_address: int = 1, baudrate: int = 9600):
self.slave_address = slave_address
self.instrument = minimalmodbus.Instrument(port, slave_address)
# Schnittstellen-Parameter konfigurieren
self.instrument.serial.baudrate = baudrate
self.instrument.serial.bytesize = 8
self.instrument.serial.parity = serial.PARITY_NONE
self.instrument.serial.stopbits = 1
self.instrument.serial.timeout = 0.5 # 500 ms Timeout
# RTU-Modus aktivieren
self.instrument.mode = minimalmodbus.MODE_RTU
self.instrument.clear_buffers_before_each_transaction = True
self.instrument.close_port_after_each_call = False
logging.info(f"Modbus RTU Geraet auf {port} (Slave-ID: {slave_address}) bereit.")
def read_metrics(self) -> dict[str, float]:
"""
Liest 32-Bit IEEE 754 Gleitkomma-Register vom Stromzaehler.
Eastron Register-Zuordnung:
- 0x0000: Netzspannung (Volt)
- 0x0006: Stromstaerke (Ampere)
- 0x000C: Wirkleistung (Watt)
- 0x0046: Netzfrequenz (Hertz)
"""
try:
# Funktionscode 0x04 (Input Register), 2 Register je 32-Bit Float
voltage = self.instrument.read_float(0x0000, functioncode=4, number_of_registers=2)
current = self.instrument.read_float(0x0006, functioncode=4, number_of_registers=2)
power = self.instrument.read_float(0x000C, functioncode=4, number_of_registers=2)
freq = self.instrument.read_float(0x0046, functioncode=4, number_of_registers=2)
return {
"voltage_v": round(voltage, 2),
"current_a": round(current, 3),
"power_w": round(power, 1),
"frequency_hz": round(freq, 2)
}
except minimalmodbus.NoResponseError:
logging.error(f"Keine Antwort von Slave {self.slave_address}. Polung A/B pruefen!")
raise
except minimalmodbus.InvalidResponseError as err:
logging.error(f"CRC-Pruefsummenfehler oder defektes Datenpaket: {err}")
raise
def close(self) -> None:
if self.instrument.serial.is_open:
self.instrument.serial.close()
def main() -> None:
meter = IndustrialEnergyMeter(port="/dev/serial0", slave_address=1, baudrate=9600)
try:
logging.info("Starte kontinuierliche Messwerterfassung (Strg+C zum Abbrechen)...")
while True:
try:
metrics = meter.read_metrics()
logging.info(
f"Netz: {metrics['voltage_v']} V | "
f"Last: {metrics['current_a']} A | "
f"Leistung: {metrics['power_w']} W | "
f"Frequenz: {metrics['frequency_hz']} Hz"
)
except Exception:
time.sleep(1.0)
continue
time.sleep(2.0)
except KeyboardInterrupt:
logging.info("Messung beendet.")
finally:
meter.close()
if __name__ == "__main__":
main()
6. Fehleranalyse in der Praxis
Fehler 1: "NoResponseError" beim Leseversuch
- Vertauschte A/B-Polarität: Klemme A und B am Transceiver tauschen. Es gibt herstellerübergreifend keine verbindliche Konvention: Einige Hersteller bezeichnen A als
Data+und B alsData-, andere genau umgekehrt. Das Tauschen zerstört nichts und löst das Problem meist sofort. - Falsche Slave-Adresse oder Baudrate: Industrie-Geräte werden ab Werk mit festen Adressen (häufig 1 oder 247) und Standard-Baudraten (9600 oder 19200) ausgeliefert. Prüfe das Handbuch oder das Display am Gerät.
Fehler 2: Paketverluste & CRC-Prüfsummenfehler
- Fehlender Abschlusswiderstand: Bei Leitungslängen über 20 Meter müssen zwingend 120-Ω-Widerstände an beiden Endpunkten der Busleitung vorhanden sein.
- Topologie-Verletzung (Sternverkabelung): RS-485 verlangt zwingend eine lineare Bus-Struktur (Daisy-Chain). Stichleitungen und Sternverteiler führen zu massiven Signalreflexionen, die Telegramme unlesbar machen.
Nächste Schritte
Vervollständige dein Wissen über serielle Busse und Signalgenerierung:
- Dallas 1-Wire & Hardware-PWM: Digitale Mehrpunkt-Temperaturnetzwerke und präzise Hardware-Pulsweitensignale.
- Systemhärtung & Read-Only RootFS: Schütze dein Edge-System vor unvorhergesehenen Stromausfällen in Industrieanlagen.