Zum Inhalt

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:

sudo nano /boot/firmware/config.txt

Overlay für BCM GPIO 4 (Pin 7) eintragen:

# Dallas 1-Wire Bus an GPIO 4 aktivieren
dtoverlay=w1-gpio,gpiopin=4

Raspberry Pi neu starten:

sudo reboot

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:

ls -l /sys/bus/w1/devices/

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:

cat /sys/bus/w1/devices/28-00000a1b2c3d/w1_slave

Rohdaten-Ausgabe des Kernels:

72 01 4b 46 7f ff 0e 10 57 : crc=57 YES
72 01 4b 46 7f ff 0e 10 57 t=23125

  • Zeile 1: Zeigt die 9 Datenbytes des Speichers und die CRC-Prüfung (YES bedeutet fehlerfrei, NO zeigt 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:

sudo nano /boot/firmware/config.txt

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:

sudo reboot

Sysfs-PWM-Kanal exportieren und konfigurieren

Prüfen, ob der PWM-Chip vorhanden ist:

ls -l /sys/class/pwm/
# Ausgabe: pwmchip0 -> ...

Kanal 0 aktivieren:

echo 0 | sudo tee /sys/class/pwm/pwmchip0/export

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: