Dallas 1-Wire Bus-Netzwerke & Hardware-PWM
Die Interaktion mit der physikalischen Welt erfordert zwei Kernfähigkeiten: Das Erfassen weit verteilter Messpunkte mit minimalem Verkabelungsaufwand und das Erzeugen hochpräziser Steuerpulse für Motoren und Servos ohne CPU-Timing-Schwankungen (Jitter).
Das Dallas/Maxim 1-Wire-Protokoll löst die erste Herausforderung, indem es Dutzenden digitalen Temperatursensoren erlaubt, eine einzige GPIO-Datenleitung über hunderte Meter Leitungslänge zu teilen. Die Hardware-PWM-Controller des Raspberry Pi lösen die zweite Aufgabe: Sie erzeugen nanosekunden-genaue Signale direkt über dedizierte Silizium-Timer, völlig entkoppelt vom Linux-Betriebssystem.
1. Die Dallas 1-Wire Protokoll-Architektur
Ursprünglich von Dallas Semiconductor (heute Analog Devices) entwickelt, kommuniziert der 1-Wire-Bus asynchron über eine einzige Datenader (DQ) bezogen auf Masse (GND).
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph Host["Raspberry Pi Host"]
direction TB
VCC["3,3 V Spannungsversorgung"]
PULLUP["4,7 kΩ Pull-Up-Widerstand"]
GPIO["GPIO 4 (w1-gpio Datenleitung)"]
GND["Masse (GND)"]
end
subgraph MultiDrop["1-Wire Bus-Topologie"]
direction TB
LineVDD["VDD-Leitung (3,3 V)"]
LineDQ["DQ-Datenleitung (mit Pull-Up)"]
LineGND["GND-Leitung (Masse)"]
end
subgraph Sensors["Parallele 64-Bit-Sensoren"]
direction TB
S1["DS18B20 Sensor 1<br/>(ROM: 28-00000abc1234)"]
S2["DS18B20 Sensor 2<br/>(ROM: 28-00000def5678)"]
S3["DS18B20 Sensor 3<br/>(ROM: 28-00000789cafe)"]
end
Host --> MultiDrop
MultiDrop --> S1
S1 --> S2 --> S3
Die 64-Bit gelaserte ROM-Adresse
Jeder 1-Wire-Chip besitzt eine ab Werk im Silizium eingebrannte, weltweit einmalige 64-Bit-ROM-Adresse:
- 8-Bit Family-Code: Bestimmt den Sensortyp (0x28 für DS18B20 programmierbare digitale Thermometer).
- 48-Bit Seriennummer: Eindeutige Seriennummer des jeweiligen Chips.
- 8-Bit CRC-Prüfsumme: Verhindert Übertragungsfehler bei der Adresserkennung.
Da jeder Sensor eindeutig identifizierbar ist, können 20 oder mehr Sensoren parallel an ein einziges verdrilltes Netzwerkkabel (Cat5/Cat6) durch ein gesamtes Rechenzentrum oder Heizungsnetz geschaltet werden, ohne einzelne Leitungen zum Raspberry Pi zurückführen zu müssen.
Elektrische Grenzwerte & Pull-Up-Widerstand
1-Wire arbeitet als Open-Drain-Bus. Im Ruhezustand wird die Leitung über einen 4,7-kΩ-Pull-Up-Widerstand auf 3,3 V gezogen. - Bei Buslängen unter 5 Metern: Ein 4,7-kΩ-Widerstand ist optimal. - Bei langen Kabeln (30 bis 100 Meter) oder mehr als 10 Sensoren: Die Leitungskapazität erfordert einen stärkeren Pull-Up von 2,2 kΩ bis 1,8 kΩ, um steile Flankenanstiege zu garantieren. - Parasitäre Speisung vs. Externe Versorgung: Zwar beherrscht der DS18B20 die parasitäre Speisung über die DQ-Leitung, in industriellen Installationen sollte jedoch immer eine separate 3,3-V-Ader an Pin VDD anliegen, um Spannungseinbrüche während der 750 ms dauernden Temperaturwandlung zu vermeiden.
2. Kernel-Konfiguration & Sysfs-Dateisystem
Linux unterstützt 1-Wire nativ über die Kernel-Module w1-gpio und w1-therm.
1-Wire im Device Tree aktivieren
Datei /boot/firmware/config.txt bearbeiten:
Overlay für BCM GPIO 4 (Pin 7) eintragen:
Raspberry Pi neu starten:
Sensor-Rohdaten über Sysfs auslesen
Nach dem Booten sucht der Kernel-Treiber den Bus automatisch ab und hängt erkannte Sensoren unter /sys/bus/w1/devices/ ein:
Ausgabe:
lrwxrwxrwx 1 root root 0 ... 28-00000a1b2c3d -> ../../../devices/w1_bus_master1/28-00000a1b2c3d
lrwxrwxrwx 1 root root 0 ... 28-00000e4f5a6b -> ../../../devices/w1_bus_master1/28-00000e4f5a6b
lrwxrwxrwx 1 root root 0 ... w1_bus_master1
In jedem Sensorordner liegt eine Datei w1_slave. Das Lesen dieser Datei löst eine Temperaturmessung aus:
Rohdaten-Ausgabe des Kernels:
- Zeile 1: Zeigt die 9 Datenbytes des Speichers und die CRC-Prüfung (
YESbedeutet fehlerfrei,NOzeigt eine defekte Übertragung an). - Zeile 2: Endet mit
t=23125, was der gemessenen Temperatur in Milligrad Celsius entspricht (23125 ÷ 1000 = 23,125 °C).
3. Python-Telemetry-Engine für Multi-Sensor-Arrays
Dieses produktionsreife Skript erkennt alle angeschlossenen Sensoren dynamisch, prüft die CRC-Prüfsumme, filtert Fehlmessungen heraus und stellt saubere Messwerte bereit:
#!/usr/bin/env python3
"""
Industrieller Dallas 1-Wire Sensor-Monitor
Liest beliebig viele DS18B20 Temperatursensoren mit Fehlerkorrektur aus.
"""
import os
import glob
import time
import logging
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
W1_BASE_DIR = "/sys/bus/w1/devices"
class W1TemperatureArray:
def __init__(self, base_dir: str = W1_BASE_DIR):
self.base_dir = base_dir
def discover_sensors(self) -> list[str]:
"""Durchsucht das sysfs-Verzeichnis nach DS18B20-Geraeten (28-*)."""
device_folders = glob.glob(os.path.join(self.base_dir, "28-*"))
sensors = [os.path.basename(path) for path in device_folders]
logging.info(f"{len(sensors)} DS18B20 Sensor(en) auf dem Bus gefunden: {sensors}")
return sensors
def read_sensor_temperature(self, sensor_id: str, max_retries: int = 3) -> Optional[float]:
"""
Liest w1_slave Datei, verifiziert CRC-16 und gibt Temperatur in Grad Celsius zurueck.
"""
device_file = os.path.join(self.base_dir, sensor_id, "w1_slave")
for attempt in range(1, max_retries + 1):
try:
with open(device_file, "r") as f:
lines = f.readlines()
if len(lines) < 2:
time.sleep(0.1)
continue
# CRC-Pruefung in Zeile 1 auswerten
if not lines[0].strip().endswith("YES"):
logging.warning(f"[{sensor_id}] CRC-Pruefung fehlgeschlagen (Versuch {attempt}/{max_retries})")
time.sleep(0.2)
continue
# Temperaturwert aus Zeile 2 parsen
equals_pos = lines[1].find("t=")
if equals_pos != -1:
temp_string = lines[1][equals_pos + 2:].strip()
temp_c = float(temp_string) / 1000.0
# 85,0°C ist der Power-On Reset Standardwert bei Sensor-Neustart
if temp_c == 85.0 and attempt == 1:
logging.warning(f"[{sensor_id}] Power-On 85°C Reset-Wert erkannt. Wiederhole Messung...")
time.sleep(0.5)
continue
return round(temp_c, 2)
except OSError as err:
logging.warning(f"[{sensor_id}] I/O-Fehler beim Lesen der Geraetedatei: {err}")
time.sleep(0.2)
logging.error(f"[{sensor_id}] Keine gueltigen Sensordaten nach {max_retries} Versuchen.")
return None
def main() -> None:
monitor = W1TemperatureArray()
sensors = monitor.discover_sensors()
if not sensors:
logging.error("Keine 1-Wire Sensoren gefunden. Verdrahtung und Pull-Up pruefen.")
return
try:
logging.info("Starte Temperatur-Messschleife (Strg+C zum Beenden)...")
while True:
readings = {}
for sensor in sensors:
temp = monitor.read_sensor_temperature(sensor)
if temp is not None:
readings[sensor] = temp
formatted = " | ".join([f"{sid[-6:]}: {t}°C" for sid, t in readings.items()])
logging.info(f"Sensoren: [ {formatted} ]")
time.sleep(2.0)
except KeyboardInterrupt:
logging.info("Messschleife beendet.")
if __name__ == "__main__":
main()
4. Hardware-PWM vs. Software-Jitter
In einem Standard-Linux-System ohne Echtzeit-Erweiterung vergibt der CFS-Scheduler CPU-Zeiten nach Verfügbarkeit. Versucht ein Programm, einen Pin mit time.sleep(0.001) in Software als PWM-Signal zu schalten:
- Verzögern Hintergrund-Festplatten-I/O, Netzwerk-Interrupts oder Python-Garbage-Collection das Aufwachen des Threads um mehrere Millisekunden.
- Dieser Jitter führt bei Servomotoren zu heftigem Zittern und bei DC-Motoren zu lautem Pfeifen.
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
subgraph Software["Software-PWM (CPU-Getrieben)"]
direction TB
CPU["Linux CPU<br/>(CFS-Scheduler)"]
JITTER["GPIO-Pin: Jitterndes Signal<br/>(±2 ms Timing-Fehler)"]
CPU -->|Unterbrechung durch OS-Tasks| JITTER
end
subgraph Hardware["Hardware-PWM (Silizium-Timer)"]
direction TB
OSC["Oszillator Taktquelle"]
PWM_HW["Dedizierte Hardware-PWM Peripherie"]
CLEAN["GPIO-Pin: Absolut stabiles Signal<br/>(0% CPU-Jitter, mikrosekundengenau)"]
OSC --> PWM_HW
PWM_HW -->|Autonomer Hardware-Timer| CLEAN
end
Software -->|"Upgrade auf Hardware-Timer"| Hardware
Der Broadcom-SoC und der RP1-Controller besitzen integrierte Hardware-PWM-Kanäle mit eigenen Taktteilern. Einmal programmiert, läuft die Signalerzeugung vollkommen autonom in Hardware, ohne einen einzigen CPU-Zyklus zu belasten.
Hardware-PWM Pinbelegung auf dem Raspberry Pi
| Hardware-Kanal | BCM-Pin (Primär) | Physischer Pin | Alternativer BCM-Pin | Physischer Pin |
|---|---|---|---|---|
| PWM0 (Kanal 0) | GPIO 12 | Pin 32 | GPIO 18 | Pin 12 |
| PWM1 (Kanal 1) | GPIO 13 | Pin 33 | GPIO 19 | Pin 35 |
5. Kernel-Aktivierung & Sysfs-PWM-Steuerung
Das 2-Kanal-Hardware-PWM-Overlay in /boot/firmware/config.txt eintragen:
Overlay ergänzen:
# Hardware-PWM an GPIO 12 (PWM0) und GPIO 13 (PWM1) aktivieren
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
System neu starten:
Sysfs-PWM-Kanal exportieren und konfigurieren
Prüfen, ob der PWM-Chip vorhanden ist:
Kanal 0 aktivieren:
Dies erzeugt das Steuerungsverzeichnis /sys/class/pwm/pwmchip0/pwm0/:
- period: Gesamte Periodendauer in Nanosekunden (T = 1 ÷ f).
- duty_cycle: Einschaltdauer (Pulsbreite) in Nanosekunden.
- enable: 1 zum Aktivieren, 0 zum Abschalten.
- polarity: normal oder inversed.
Servo-Berechnung: 50 Hz Standard-Steuersignal
Klassische RC-Servos (wie SG90, MG996R oder Industrie-Stellventile) arbeiten mit einer Frequenz von 50 Hz (20 ms Periodendauer):
Servo-Timing-Berechnung:
Periode (50 Hz) = 1 ÷ 50 = 0,020 s = 20.000.000 ns
0°-Winkel (1,0 ms Pulsbreite) = 1.000.000 ns
90°-Neutralstellung (1,5 ms Pulsbreite) = 1.500.000 ns
180°-Winkel (2,0 ms Pulsbreite) = 2.000.000 ns
Manuelle Konfiguration im Terminal:
# 20 ms Periode setzen (50 Hz)
echo 20000000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/period
# 1,5 ms Pulsbreite setzen (Neutralstellung)
echo 1500000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/duty_cycle
# Signalausgabe einschalten
echo 1 | sudo tee /sys/class/pwm/pwmchip0/pwm0/enable
6. Python-Steuerung für jitterfreie Servo-Positionierung
Dieses Skript steuert Hardware-PWM direkt über das Linux-Sysfs an:
#!/usr/bin/env python3
"""
Industrielle Hardware-PWM-Steuerung ueber Linux Sysfs
Erzeugt jitterfreie Pulse fuer Servos und Motorregelung.
"""
import os
import time
import sys
SYSFS_PWM_BASE = "/sys/class/pwm/pwmchip0"
class HardwarePWM:
def __init__(self, channel: int = 0, frequency_hz: int = 50):
self.channel = channel
self.channel_path = os.path.join(SYSFS_PWM_BASE, f"pwm{channel}")
self.period_ns = int(1_000_000_000 / frequency_hz)
# Kanal exportieren, falls noch nicht erfolgt
if not os.path.exists(self.channel_path):
with open(os.path.join(SYSFS_PWM_BASE, "export"), "w") as f:
f.write(str(channel))
time.sleep(0.1)
self._write("period", str(self.period_ns))
def _write(self, attribute: str, value: str) -> None:
path = os.path.join(self.channel_path, attribute)
with open(path, "w") as f:
f.write(value)
def set_duty_cycle_ns(self, duty_ns: int) -> None:
"""Setzt die aktive Pulsbreite in Nanosekunden."""
if duty_ns > self.period_ns:
raise ValueError(f"Pulsbreite {duty_ns}ns darf Periode {self.period_ns}ns nicht ueberschreiten.")
self._write("duty_cycle", str(duty_ns))
def set_servo_angle(self, angle_degrees: float) -> None:
"""
Rechnet Winkel 0,0° bis 180,0° in 1,0 ms (1.000.000 ns) bis 2,0 ms (2.000.000 ns) um.
"""
clamped = max(0.0, min(180.0, angle_degrees))
duty_ns = int(1_000_000 + (clamped / 180.0) * 1_000_000)
self.set_duty_cycle_ns(duty_ns)
def enable(self) -> None:
self._write("enable", "1")
def disable(self) -> None:
self._write("enable", "0")
def cleanup(self) -> None:
self.disable()
with open(os.path.join(SYSFS_PWM_BASE, "unexport"), "w") as f:
f.write(str(self.channel))
def main() -> None:
print("Initialisiere Hardware-PWM Kanal 0 an GPIO 12...")
pwm = HardwarePWM(channel=0, frequency_hz=50)
pwm.enable()
try:
print("Bewege Servo von 0° bis 180° ohne CPU-Jitter...")
while True:
for angle in [0, 45, 90, 135, 180, 90]:
print(f"Stelle Winkel: {angle}°")
pwm.set_servo_angle(angle)
time.sleep(1.0)
except KeyboardInterrupt:
print("\nStoppe Servosteuerung.")
finally:
pwm.cleanup()
print("PWM-Kanal sauber freigegeben.")
if __name__ == "__main__":
main()
Nächste Schritte
Mit dem Abschluss des GPIO- und Bus-Engineerings geht es an die Systemhärtung und Hochverfügbarkeit:
- Read-Only RootFS & Stromausfallsicherheit: Schütze dein System vor Dateisystembeschädigungen bei plötzlichem Spannungsverlust.
- Hardware-Watchdog & Autonome Wiederherstellung: Garantiere 24/7/365 Dauerbetrieb bei Kernel-Deadlocks.