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:
Append the 1-Wire overlay (defaulting to BCM GPIO 4 / Physical Pin 7):
Reboot the Raspberry Pi:
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/:
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:
Raw kernel output:
- Line 1: Displays the 9 raw scratchpad bytes followed by the CRC verification result (
YESmeans data is valid;NOindicates 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:
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:
Sysfs PWM Export & Configuration
Verify that the PWM controller is registered:
To control PWM0, export channel 0:
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:
- Read-Only Root Filesystem & Power-Loss Immunity: Guarantee that your device survives sudden hard power cuts without filesystem corruption.
- Hardware Watchdog & Automatic Reboot Recovery: Ensure 24/7/365 continuous uptime in unmanned field deployments.