Skip to content

Dallas 1-Wire Bus Networks & Hardware-Timed PWM

Precise physical-world interaction requires two complementary capabilities: reading distributed multi-point environmental metrics with minimal wiring, and generating ultra-precise pulse waveforms to actuate motors, solenoids, and servos without CPU timing jitter.

The Dallas/Maxim 1-Wire protocol fulfills the first requirement by allowing dozens of digital temperature probes to share a single GPIO pin over hundreds of meters. Conversely, the Raspberry Pi's Hardware PWM controllers solve the timing jitter inherent to multi-tasking Linux kernels, delivering microsecond-accurate waveforms directly from dedicated silicon timers.


1. The Dallas 1-Wire Protocol Architecture

Originally developed by Dallas Semiconductor (now Analog Devices), the 1-Wire bus communicates asynchronously over a single data conductor (DQ) referenced to Ground.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph Host["Raspberry Pi Host"]
        VCC["3.3V Power Rail"]
        PULLUP["4.7 kΩ Pull-Up Resistor"]
        GPIO["GPIO 4 (w1-gpio Data Line)"]
        GND["Ground (GND)"]
    end

    subgraph MultiDrop["1-Wire Multi-Drop Bus Topology"]
        LineDQ["DQ (Data Line)"]
        LineVDD["VDD (3.3V Power)"]
        LineGND["GND (Ground)"]
    end

    subgraph Sensors["Unique 64-Bit Addressed Probes"]
        S1["DS18B20 (ROM: 28-00000abc1234)"]
        S2["DS18B20 (ROM: 28-00000def5678)"]
        S3["DS18B20 (ROM: 28-00000789cafe)"]
    end

    VCC --- PULLUP
    PULLUP --- LineDQ
    GPIO --- LineDQ
    VCC --- LineVDD
    GND --- LineGND

    LineDQ --- S1
    LineVDD --- S1
    LineGND --- S1

    LineDQ --- S2
    LineVDD --- S2
    LineGND --- S2

    LineDQ --- S3
    LineVDD --- S3
    LineGND --- S3

The 64-Bit Lasered ROM Registration

Every 1-Wire silicon device features a factory-lasered, globally unique 64-bit ROM address: - 8-Bit Family Code: Identifies the device type (0x28 for DS18B20 programmable resolution digital thermometers). - 48-Bit Serial Number: Unique per-chip serial identifier. - 8-Bit CRC Checksum: Guarantees corruption-free address discovery.

Because each probe is uniquely identifiable, you can solder 20 sensors in parallel along a single Cat5/Cat6 twisted pair running through an entire data center or heating plant without running individual signal wires back to the Pi.

Electrical Constraints & Pull-Up Dimensioning

1-Wire operates as an open-drain line. When idle, the line must be held HIGH at 3.3V through a 4.7 kΩ pull-up resistor. - For bus lengths under 5 meters: A standard 4.7 kΩ resistor is ideal. - For long cable runs (30 to 100 meters) or arrays with more than 10 sensors: The line capacitance requires a stronger pull-up of 2.2 kΩ to 1.8 kΩ to ensure fast signal rise times. - Parasitic Power vs. External Power: While the DS18B20 supports "parasitic power" (stealing current from the DQ line when HIGH), industrial deployments should always supply continuous external 3.3V power to the VDD pin to avoid voltage dips during 750 ms temperature conversion cycles.


2. Kernel Configuration & Sysfs Sensor Inspection

Linux provides native kernel support for 1-Wire through the w1-gpio and w1-therm kernel modules.

Enabling 1-Wire in Device Tree

Open /boot/firmware/config.txt:

sudo nano /boot/firmware/config.txt

Append the 1-Wire overlay (defaulting to BCM GPIO 4 / Physical Pin 7):

# Enable Dallas 1-Wire bus on BCM GPIO 4
dtoverlay=w1-gpio,gpiopin=4

Reboot the Raspberry Pi:

sudo reboot

Reading Raw Sensor Data from Sysfs

Upon booting, the kernel background daemon scans the bus, enumerates all connected devices, and mounts them under /sys/bus/w1/devices/:

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

Output:

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

Each sensor directory contains a virtual file named w1_slave. Inspecting this file triggers a temperature conversion and outputs the 9-byte scratchpad:

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

Raw kernel output:

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

  • Line 1: Displays the 9 raw scratchpad bytes followed by the CRC verification result (YES means data is valid; NO indicates transmission corruption).
  • Line 2: Concludes with t=23125, representing the measured temperature in millidegrees Celsius (23125 ÷ 1000 = 23.125 °C).

3. Production Multi-Sensor Python Telemetry Engine

Here is an enterprise-grade multi-threaded parser that discovers all connected sensors, performs CRC validation, handles transient disconnects, and computes statistical summaries:

#!/usr/bin/env python3
"""
Enterprise Dallas 1-Wire Sensor Network Monitor
Continuously discovers, validates, and polls all DS18B20 digital temperature probes.
"""

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]:
        """Scans the virtual sysfs tree for DS18B20 family devices (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"Discovered {len(sensors)} DS18B20 sensor(s) on 1-Wire bus: {sensors}")
        return sensors

    def read_sensor_temperature(self, sensor_id: str, max_retries: int = 3) -> Optional[float]:
        """
        Reads raw w1_slave file, verifies CRC-16, and returns Celsius float.
        Retries up to max_retries on CRC failure or transient disconnects.
        """
        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

                # Verify CRC on line 1
                if not lines[0].strip().endswith("YES"):
                    logging.warning(f"[{sensor_id}] CRC check failed (Attempt {attempt}/{max_retries})")
                    time.sleep(0.2)
                    continue

                # Extract temperature integer from line 2
                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

                    # Sanity check: DS18B20 operating range is -55°C to +125°C
                    # 85.0°C is the power-on reset default scratchpad value
                    if temp_c == 85.0 and attempt == 1:
                        logging.warning(f"[{sensor_id}] Power-on 85°C reset value detected. Re-sampling...")
                        time.sleep(0.5)
                        continue

                    return round(temp_c, 2)

            except OSError as err:
                logging.warning(f"[{sensor_id}] I/O error reading device file: {err}")
                time.sleep(0.2)

        logging.error(f"[{sensor_id}] Failed to acquire valid telemetry after {max_retries} attempts.")
        return None

def main() -> None:
    monitor = W1TemperatureArray()
    sensors = monitor.discover_sensors()

    if not sensors:
        logging.error("No 1-Wire sensors detected. Check 4.7kΩ pull-up wiring and config.txt.")
        return

    try:
        logging.info("Starting telemetry loop. Press Ctrl+C to terminate.")
        while True:
            readings = {}
            for sensor in sensors:
                temp = monitor.read_sensor_temperature(sensor)
                if temp is not None:
                    readings[sensor] = temp

            # Format status output
            formatted = " | ".join([f"{sid[-6:]}: {t}°C" for sid, t in readings.items()])
            logging.info(f"Sensors: [ {formatted} ]")
            time.sleep(2.0)

    except KeyboardInterrupt:
        logging.info("Terminating 1-Wire telemetry daemon.")

if __name__ == "__main__":
    main()

4. Hardware PWM vs. Software Jitter

In standard non-realtime Linux distributions (like Raspberry Pi OS), userspace programs run under the kernel CFS (Completely Fair Scheduler). If an application attempts to toggle a GPIO pin in a tight loop with time.sleep(0.001) to drive a servo motor:

  • Background disk I/O, network packet interrupts, or garbage collection will delay the sleep wakeup by several milliseconds.
  • This creates timing jitter, causing analog RC servos to twitch erratically and DC motor controllers to stutter with an audible whine.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph Software["Software PWM (CPU-Driven)"]
        CPU["Linux CPU (CFS Scheduler)"] -->|Interrupted by OS tasks| JITTER["GPIO Line: Jittery Waveform (±2 ms timing errors)"]
    end

    subgraph Hardware["Hardware PWM (Silicon Timer)"]
        OSC["Oscillator Clock Source"] --> PWM_HW["Dedicated PWM Peripheral Silicon"]
        PWM_HW -->|Autonomous DMA/Timer| CLEAN["GPIO Line: Microsecond-Clean Waveform (0% Jitter)"]
    end

The Broadcom SoC and RP1 controller integrate dedicated Hardware PWM modules with independent clock dividers. Once configured, the hardware generates the high-frequency waveform autonomously without consuming a single CPU cycle.

Raspberry Pi Hardware PWM Channel Matrix

Hardware Channel BCM Pin (Primary) Physical Pin Alternate BCM Pin Physical Pin
PWM0 (Channel 0) GPIO 12 Pin 32 GPIO 18 Pin 12
PWM1 (Channel 1) GPIO 13 Pin 33 GPIO 19 Pin 35

5. Kernel Enablement & Linux Sysfs PWM Control

Enable the dual-channel hardware PWM controller in /boot/firmware/config.txt:

sudo nano /boot/firmware/config.txt

Add the following overlay:

# Enable 2-channel hardware PWM on GPIO 12 (PWM0) and GPIO 13 (PWM1)
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

(Note: On Raspberry Pi 5, the RP1 controller manages PWM channels via dtoverlay=pwm-2chan or native sysfs export on /sys/class/pwm/pwmchip0).

Reboot the system:

sudo reboot

Sysfs PWM Export & Configuration

Verify that the PWM controller is registered:

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

To control PWM0, export channel 0:

# Export channel 0
echo 0 | sudo tee /sys/class/pwm/pwmchip0/export

This creates the configuration directory /sys/class/pwm/pwmchip0/pwm0/: - period: Total waveform period in nanoseconds (T = 1 ÷ f). - duty_cycle: Active HIGH pulse width in nanoseconds. - enable: 1 to activate output, 0 to disable. - polarity: normal (starts HIGH, transitions LOW) or inversed.

Servo Math: 50 Hz Standard Control Signal

Standard RC servos (such as the SG90, MG996R, or industrial throttle valves) require a 50 Hz frame frequency (20 ms period):

Servo Timing Calculations:
Period (50 Hz) = 1 ÷ 50 = 0.020 s = 20,000,000 ns
0° Position (1.0 ms pulse) = 1,000,000 ns
90° Neutral (1.5 ms pulse) = 1,500,000 ns
180° Position (2.0 ms pulse) = 2,000,000 ns

Configure via bash:

# Set 20ms period (50 Hz)
echo 20000000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/period

# Set 1.5ms duty cycle (Neutral center)
echo 1500000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/duty_cycle

# Enable waveform generation
echo 1 | sudo tee /sys/class/pwm/pwmchip0/pwm0/enable

6. Industrial Python Controller: Jitter-Free Actuation

This Python module interfaces directly with the Linux Sysfs PWM subsystem to position robotic servos or control DC motor H-bridge speed smoothly:

#!/usr/bin/env python3
"""
Industrial Hardware PWM Controller via Linux Sysfs
Provides jitter-free pulse generation for precision servos and motor speed control.
"""

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)

        # Export channel if not already active
        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)

        # Configure base period
        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:
        """Sets raw pulse duration in nanoseconds."""
        if duty_ns > self.period_ns:
            raise ValueError(f"Duty cycle {duty_ns}ns cannot exceed period {self.period_ns}ns")
        self._write("duty_cycle", str(duty_ns))

    def set_servo_angle(self, angle_degrees: float) -> None:
        """
        Maps 0.0 to 180.0 degrees to 1.0 ms (1,000,000 ns) .. 2.0 ms (2,000,000 ns).
        """
        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("Initializing Hardware PWM channel 0 on GPIO 12...")
    pwm = HardwarePWM(channel=0, frequency_hz=50)
    pwm.enable()

    try:
        print("Sweeping servo from 0° to 180° with zero CPU jitter...")
        while True:
            for angle in [0, 45, 90, 135, 180, 90]:
                print(f"Positioning to {angle}°")
                pwm.set_servo_angle(angle)
                time.sleep(1.0)

    except KeyboardInterrupt:
        print("\nHalting servo controller.")
    finally:
        pwm.cleanup()
        print("PWM channel released.")

if __name__ == "__main__":
    main()

Next Steps

With GPIO and low-level bus engineering mastered, proceed to production appliance reliability and containerization: