Non-blocking 1-Wire master for STM32, with a DS18B20 temperature driver built on top. A generic bus layer (src/onewire.c) owns the 1-Wire timing — a hybrid of a hardware timer (TIM1) and DMA automates every slot, so the CPU never performs timing-critical busy-waits inside a transaction and never enters an interrupt; operations advance by polling hardware completion flags. The first driver on that layer is src/ds18b20.c, and other 1-Wire slaves (DS2413, DS2431, ...) can ride it as-is.
The core (src/onewire.c + src/ds18b20.c) is MCU-independent and rides on a small port interface (inc/ow_port.h); per-MCU backends are header-only implementations under port/. Three backends ship today:
port/stm32f1/ow_port_f1.h— STM32F103C8T6 (Blue Pill): bus on PA10, TIM1 CH3 output / CH4 capture, DMA1 channels 3/4.port/stm32f0/ow_port_f0.h— STM32F030x6 (e.g. TSSOP20 STM32F030F4P6): bus on PA10, TIM1 CH3 output / CH4 capture, DMA1 channels 3/4.port/stm32g0/ow_port_g0.h— STM32G031x6 (e.g. TSSOP20 STM32G031F6P6): bus on PA10 via the SYSCFG PA12 remap, TIM1 CH3 output / CH4 capture, DMA1 channels 3/4 through DMAMUX (requests 21/23).
- Features
- Requirements
- File Structure
- Examples
- Hardware Verified
- Hardware Connections
- Quick Start
- Building
- VSCode Integration
- Comparison with Common 1-Wire Techniques
- Architecture
- RTOS Integration
- API Reference
- Performance
- Configuration
- Troubleshooting
- License
- Contributing
- Support
- References
- Pure Bare-Metal: direct register manipulation, no HAL or LL libraries.
- Universal 1-Wire Layer:
inc/onewire.h+src/onewire.c— a reusable, non-blocking 1-Wire master (bus primitives + Maxim Search ROM) scheduled on TIM1/DMA; the DS18B20 driver is built on it, and other 1-Wire slaves (DS2413, DS2431, ...) can reuse it as-is — see 1-Wire Layer (shared). - Multi-MCU Backend: one MCU-independent core over a
ow_port_*interface; header-only backends for STM32F1, STM32F0 and STM32G0, all on the shared CH3/CH4 scheme. Select at build time withmake OW_TARGET=f0/make OW_TARGET=g0(F1 is the default). - Zero NVIC Interrupts: no NVIC interrupts and no ISRs — fully polled
operation. The optional
-DOW_PORT_LOW_POWER=1mode enables the timer update interrupt source (UIE) only to generate a pending event that wakesWFE()viaSEVONPEND— no NVIC interrupt is enabled and no ISR is installed. - RTOS-Ready: the strict 1-Wire bit timing is generated entirely by TIM1+DMA,
so
ds18b20_poll()can be called at any rate from an RTOS task without corrupting the bus. Not thread-safe by itself — see RTOS Integration. - Hardware Automation: TIM1 output compare + input capture with DMA automate waveform generation and data capture — see Architecture.
- State Machine Architecture: event-driven operation controlled by hardware completion signals — see State Machine Flow.
- Weak Function Callbacks (
ds18b20_busy(),ds18b20_complete()) and CRC-8 validation of every sensor reading — see Weak Callbacks and Error Codes. - Resolution-Aware Conversion Wait: the driver waits exactly as long as the configured conversion resolution requires (93.75ms @ 9-bit … 750ms @ 12-bit), so lowering the resolution speeds up the measurement cycle.
- Non-blocking across the board — each operation advances one hardware
operation per
*_poll()call from the main loop:- Device search:
ds18b20_search_start()/_poll()/_count()— Device Search. - Alarm search:
ds18b20_alarm_search_*()reports only devices in alarm — Alarm Search. - Command transactions: Read ROM, TH/TL, scratchpad, Copy/Recall EEPROM,
parasite detect — Command Transactions,
examples/5_commands. - Per-device addressing:
ds18b20_select()(Match ROM 0x55) — Per-Device Addressing. - Resolution change:
ds18b20_set_resolution()/_poll()— Resolution Change. - Simultaneous multi-device conversion: one broadcast
Convert T, read back via Match ROM — Simultaneous Multi-Device Conversion.
- Device search:
- Optional Signal Statistics Module (
ow_stats,-DOW_STATS_ENABLE=1): per-sensor pulse-width min/max, a global histogram, error counters and a non-blocking UART dump; all stubs inline to nothing in production builds — see Signal Statistics Module.
- Microcontroller: any STM32 with a single advanced-control timer instance
that satisfies the complete Required Timer Capabilities
and DMA topology (currently supported: STM32F103C8T6, STM32F030x6,
STM32G031x6; see port backends in
port/). - Sensor: DS18B20 digital temperature sensor
- Toolchain: GCC ARM (arm-none-eabi)
- Clock Configuration: STM32F103 — 72MHz via HSE+PLL (default) or 8MHz via internal RC (
make SYSCLK_MHZ=8); STM32F030 — 48MHz via HSI+PLL (default) or 8MHz via internal RC. Both targets takeSYSCLK_MHZ=8; STM32G031 — 64MHz via HSI16+PLL (default) or 16MHz via internal RC (SYSCLK_MHZ=16). The portableOW_PORT_SYSCLK_MHZdefine carries the value to every clock-dependent setting.
├── inc/ # Project header files
│ ├── ds18b20.h # Driver interface (high-level API) and constants
│ ├── onewire.h # Shared 1-Wire layer (bus primitives + Search ROM)
│ ├── ow_config.h # Compile-time tunables (pulse widths, feature flags, limits)
│ ├── ow_stats.h # Optional signal statistics module (histogram, per-sensor)
│ ├── ow_port.h # 1-Wire port layer interface (+ backend select)
│ └── ow_bits.h # STM32 register access macros (shared)
├── port/ # Per-MCU backends for the ow_port_* interface
│ ├── stm32f1/ # STM32F1: TIM1 + DMA1 + PA10 (header-only static inline)
│ │ ├── ow_port_f1.h # Register-level ow_port_* implementation for STM32F1
│ │ ├── STM32F103XB_FLASH.ld # Linker script, STM32F103xB (with .noinit section)
│ │ ├── stm32f103cb.jflash # J-Flash project file (make jprogram)
│ │ └── project.jdebug # SEGGER Ozone project (STM32F103C8, SWD)
│ ├── stm32f0/ # STM32F0: TIM1 + DMA1 + PA10 (header-only static inline)
│ │ ├── ow_port_f0.h # Register-level ow_port_* implementation for STM32F0
│ │ ├── STM32F030X6_FLASH.ld # Linker script, STM32F030x6 (16KB flash / 4KB RAM)
│ │ ├── stm32f030f4.jflash # J-Flash project file
│ │ └── project.jdebug # SEGGER Ozone project (STM32F030F4, SWD)
│ └── stm32g0/ # STM32G0: TIM1 + DMA1 + DMAMUX + PA10 via PA12 remap (header-only static inline)
│ │ ├── ow_port_g0.h # Register-level ow_port_* implementation for STM32G0
│ │ ├── STM32G031X6_FLASH.ld # Linker script, STM32G031x6 (32KB flash / 8KB RAM)
│ │ ├── stm32g031f6.jflash # J-Flash project file
│ │ └── project.jdebug # SEGGER Ozone project (STM32G031F6, SWD)
├── src/ # Project source files
│ ├── ow_stats.c # Signal statistics implementation (histogram, UART dump)
│ ├── onewire.c # 1-Wire layer: state machine + bus primitives
│ │ # + non-blocking Search ROM engine
│ ├── ds18b20.c # Driver: DS18B20 command set on the 1-Wire layer.
│ │ # Compiles as ONE translation unit: #includes its
│ │ # four functional parts in dependency order.
│ ├── ds18b20_search.c # (include-only part) ROM device table + search/alarm
│ ├── ds18b20_txn.c # (include-only part) command transactions + parasite
│ ├── ds18b20_resolution.c # (include-only part) non-blocking resolution change
│ └── ds18b20_measure.c # (include-only part) DS18B20_ST_* measurement machine
├── examples/ # Example applications
│ ├── app/ # Shared application layer (UART, clock, init)
│ │ ├── app.c # app_init(), UART TX ring buffer, busy LED
│ │ └── app.h # Shared application layer interface
│ ├── 1_basic/main.c # Single sensor, unconditional (Skip ROM)
│ ├── 2_device_search/main.c # Device search + per-device poll (no broadcast convert)
│ ├── 3_round_robin/main.c # Device search + sequential poll of all
│ ├── 4_scan_mode/main.c # Device search + simultaneous conversion
│ ├── 5_commands/main.c # Device search + command transactions
│ │ # (ROM, power supply, TH/TL, Copy/Recall EEPROM)
│ ├── 6_statistics/main.c # Device search + stats dump every N cycles
│ └── 7_low_power/main.c # Device search + WFE low-power sleep on long stages
├── tests/ # Host test suite (no hardware required)
│ ├── mock/ # Behavioural TIM1/DMA model + register mocks
│ ├── fuzz/ # libFuzzer harnesses (ASAN/UBSAN, 10 harnesses)
│ └── test/ # Unity-based test cases
├── cmake/ # CMake toolchain
│ └── arm-none-eabi-gcc.cmake # Bare-metal cross-compilation toolchain file
├── docs/ # Documentation assets
│ ├── api/ # Doxygen-generated API reference
│ └── screenshots/ # UART capture screenshots
├── .github/ # GitHub configuration
│ ├── workflows/ # CI (build.yml, ci.yml) and release (release.yml)
│ ├── ISSUE_TEMPLATE/ # Bug report / feature request templates
│ └── PULL_REQUEST_TEMPLATE.md
├── CMSIS/ # Build-time dependencies (gitignored)
│ ├── core/ # ARM CMSIS 5 core headers
│ └── device/ # STM32 device headers and startup (F1/F0/G0) + SVD
├── .vscode/ # VSCode workspace configuration
│ ├── tasks.json # Build tasks (Ctrl+Shift+B)
│ ├── launch.json # Debug configuration (F5, J-Link / ST-Link)
│ ├── c_cpp_properties.json # IntelliSense paths
│ ├── extensions.json # Recommended extensions
│ └── settings.json # Editor settings
├── build/ # Build artifacts (generated)
├── CMakeLists.txt # CMake build (FetchContent for CMSIS)
├── library.json # PlatformIO library metadata
├── library.properties # Arduino Library Manager metadata
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE # MIT
├── Makefile
├── README.md
└── SECURITY.md
Seven ready-to-run example applications are provided; select one with APP:
| APP | File | Behaviour |
|---|---|---|
| (default) | examples/1_basic/main.c |
Unconditional polling of a single DS18B20 via Skip ROM (0xCC). |
2_device_search |
examples/2_device_search/main.c |
Startup device search + per-device polling: each sensor is converted and read back individually via Match ROM (one Convert T per device, no broadcast conversion). |
3_round_robin |
examples/3_round_robin/main.c |
Startup device search + sequential polling of every sensor found (up to DS18B20_MAX_DEVICES). |
4_scan_mode |
examples/4_scan_mode/main.c |
Startup device search + simultaneous broadcast conversion: one Convert T (Skip ROM) converts all sensors in parallel, then each is read back via Match ROM. |
5_commands |
examples/5_commands/main.c |
Startup device search + non-blocking command transactions on the first sensor: Read Power Supply (0xB4), raw Read Scratchpad (0xBE), Write Scratchpad TH/TL (0x4E), Copy Scratchpad (0x48) to the EEPROM, Recall EEPROM (0xB8), single-device Read ROM (0x33), then steady-state measurement of the selected device. |
6_statistics |
examples/6_statistics/main.c |
Startup device search + sequential measurement with signal statistics. The 6_statistics target auto-enables -DOW_STATS_ENABLE=1. Accumulates per-sensor pulse-width min/max, a global histogram and error counters over N cycles (shipped build default 5000 via STATS_DUMP_INTERVAL, overridable), then streams the full report over UART as a non-blocking dump. |
7_low_power |
examples/7_low_power/main.c |
Low-power example (same search + sequential loop as 2_device_search): with -DOW_PORT_LOW_POWER=1 the main loop enters __WFE() while a long 1-Wire stage (> 1 ms: temperature conversion, scratchpad read, EEPROM hold-off, inter-cycle pause) is running. Without the define, the example uses the standard polling loop. |
make # build 1_basic -> build/ds18b20_1_basic.elf
make APP=2_device_search # build 2_device_search -> build/ds18b20_2_device_search.elf
make APP=3_round_robin # build 3_round_robin -> build/ds18b20_3_round_robin.elf
make APP=4_scan_mode # build 4_scan_mode -> build/ds18b20_4_scan_mode.elf
make APP=5_commands # build 5_commands -> build/ds18b20_5_commands.elf
make APP=6_statistics # build 6_statistics -> build/ds18b20_6_statistics.elf (OW_STATS_ENABLE auto-added)
make APP=7_low_power EXT="-DOW_PORT_LOW_POWER=1" # build 7_low_power -> build/ds18b20_7_low_power.elf (WFE low-power)
make debug APP=3_round_robin # debug build of 3_round_robin (for J-Link/ST-Link)
# STM32F030 target (same examples, bus on PA10):
make OW_TARGET=f0 APP=4_scan_modeNotes:
- All examples use
app_init()(fromexamples/app/app.h) to set up the system clock, USART1 TX and the busy LED in a single call. 1_basicuses Skip ROM, so it is meant for a single sensor on the bus. With several sensors connected, all of them respond to the read command and the bus data collides (CRC failures are expected).2_device_searchperforms a startup Search ROM, then measures each discovered sensor individually via Match ROM (oneConvert Tper device, no broadcast conversion) in round-robin order. A separator--------------------------------is printed between full rounds. With one sensor it behaves like1_basicbut with ROM addressing; with N sensors a round costsN × conversion. Supports-DOW_PARASITE_POWER=1(strong pull-up handled per conversion).3_round_robinmeasures the devices found at startup one at a time, in round-robin order. With exactly one sensor it behaves like1_basic.4_scan_mode(scan mode) converts every discovered sensor in parallel: a single conversion wait covers all devices, so N devices take1 x conversion + N x readinstead ofN x conversion. Each reading is reported throughds18b20_complete()in device-table order;ds18b20_scan_index()/ds18b20_device_rom()identify the sensor. Scan mode assumes a uniform resolution (the config is written broadcast) and is mutually exclusive withds18b20_select().5_commandstargets the first sensor found by the search (Match ROM) and runs the non-blocking command sequence once at startup: power supply, raw scratchpad, TH/TL write with a Copy/Recall pair to demonstrate EEPROM persistence, and the single-device Read ROM. Each command advances by one hardware operation per*_poll()call;ds18b20_last_command_ok()verifies the result.6_statisticsextends the3_round_robinsequential loop with signal statistics (-DOW_STATS_ENABLE=1, auto-enabled bymake APP=6_statistics). AfterSTATS_DUMP_INTERVALfull rounds (source default 100; theMakefileand CMake6_statisticsbuild targets compile it with 5000) the accumulated per-sensor pulse-width min/max, 13-bucket histogram (0–60+ µs) and error counters are streamed over UART byow_stats_dump_poll()(one line per call, non-blocking); the measurement loop is paused during the dump and resumed afterwards viaow_stats_reset(). Supports-DOW_PARASITE_POWER=1.- Programming targets (
make jprogram/make program) flash whichever example is currently selected byAPP.
The following captures were taken on real hardware: STM32F103C8T6 (Blue Pill), 8 × DS18B20 on one 1-Wire bus (PA10), flashed via ST-Link, USART1 TX at 115200 8N1 read through a CP2102 USB-UART adapter. The shared 1-Wire layer found all 8 sensors, and every measurement round reported all of them — no missing devices, no CRC failures.
3_round_robin — device search + round-robin + resolution cycling (examples/3_round_robin/main.c):
the startup Search ROM finds all 8 devices, then each sensor is measured in
turn while the resolution cycles 9 → 10 → 11 → 12 bit between measurements.
4_scan_mode — simultaneous multi-device conversion (examples/4_scan_mode/main.c): one broadcast
Convert T converts all sensors in parallel, then each is read back via
Match ROM — 8 readings per round in device-table order.
5_commands — command transactions (examples/5_commands/main.c): after the startup search, the
first sensor (Match ROM) answers every non-blocking command in turn — external
power confirmed, raw scratchpad read with CRC ok and the resolution auto-derived
from the config byte, TH/TL written (0x19/0x0F), copied to the EEPROM, then a
volatile write (0x05/0x02) and Recall restoring the persisted values, and the
bare Read ROM reporting a CRC failure as expected with 8 devices on the bus
(0x33 is single-device only).
The same examples were validated on an STM32F030F4P6 minimum board: the 1-Wire bus on PA10 (TIM1 CH3/CH4 pair — PA8 is not bonded out in this package), USART1 TX on PA9, busy LED on PA4, flashed via ST-Link SWD. The bus again carried 8 × DS18B20; all rounds complete with valid CRCs and no errors:
| Test | Clock | Result |
|---|---|---|
3_round_robin — search + round-robin + resolution cycling |
HSI+PLL 48MHz | 163 samples / 7+ sensors, 0 CRC or timeout errors |
3_round_robin — same |
HSI 8MHz | 161 samples, 0 errors |
4_scan_mode — simultaneous conversion scan |
HSI+PLL 48MHz | all 8 devices found, 56 readings (7 × 8), 0 errors |
4_scan_mode — same |
HSI 8MHz | all 8 devices found, 56 readings, 0 errors |
5_commands — command transactions validator |
both clocks | all checks pass (power supply, TH/TL write, Copy/Recall EEPROM round-trip, expected multi-device Read ROM CRC failure) |
Validated on a WeAct STM32G031F6P6 minimum board: 6 × DS18B20 in parasite
power mode on one 1-Wire bus (logical PA10 on the physical PA12 pad), USART1
TX on logical PA9 (physical PA11), flashed via ST-Link SWD. 4_scan_mode
(simultaneous conversion scan) runs with every device reported each round,
valid CRCs and zero errors at both supported clocks — the default 64MHz
(HSI16+PLL) and the raw-HSI16 SYSCLK_MHZ=16 build, which exercises the
slow-clock timing path natively. The bus pads are reachable only through the
SYSCFG remap described in Hardware Connections below; the USB-C connector of
this board is wired to PA11/PA12 and must stay unplugged while the driver
owns the bus.
6_statistics — signal statistics (examples/6_statistics/main.c): startup device search +
sequential measurement with the optional ow_stats module. By default the
module accumulates after every STATS_DUMP_INTERVAL full rounds (source
default 100; the make APP=6_statistics Makefile target and the CMake example
build compile it with 5000) — per-sensor pulse-width min/max, a 13-bucket
logarithmic histogram (0–60+ µs) and error counters (CRC, presence, other),
then streams the full report over UART. Validated on STM32G031@64MHz with
6 × DS18B20 in parasite power mode — all six sensors detected, 0 errors, pulse
widths 5–32 µs, histogram buckets populated across the normal decode range.
Build and run:
make OW_TARGET=g0 APP=6_statistics EXT="-DOW_STATS_ENABLE=1 -DOW_PARASITE_POWER=1"7_low_power — low power (examples/7_low_power/main.c): the same search + sequential loop as
2_device_search, but built with -DOW_PORT_LOW_POWER=1.
What low-power mode does not change. Low-power mode does not change 1-Wire execution. TIM+DMA continue to control all bus timing;
WFEallows the CPU to sleep during sufficiently long hardware-controlled transaction stages.
The one-wire driver then enables the TIM1 update interrupt source (UIE)
and the SEVONPEND system-control bit. This is not a real interrupt: UIE is
enabled only to generate a pending event that wakes WFE() — no ISR is
ever installed and no NVIC_EnableIRQ call is made. The application main loop
can block in __WFE() while a long 1-Wire stage is running and is woken by
the timer's update event; the driver itself stays fully non-blocking. Stages
treated as "long" (strictly more than 1 ms) are the temperature conversion (up to 750 ms), the scratchpad read
(~5 ms), an EEPROM hold-off (10 ms) and the inter-measurement pause; short
stages (reset, commands, search reads) are still handled by standard polling.
Power is not measured yet — this example's goal is only to establish the
mechanism and measure the CPU-time saving.
Verified on hardware (STM32F103C8 Blue Pill). With
-DOW_PORT_LOW_POWER=1 -DOW_PARASITE_POWER=1and six DS18B20 sensors powered in parasite mode, 7_low_power found all six devices, read them in turn (24.0 °C / 85.0 °C / 23.8 °C ...) and the core demonstrably entered__WFE(): a temporary instrumented run printed[WFE iters=1]before every measurement, i.e. the first__WFE()after arming the long stage blocked and woke exactly once on the timer's update event. The sleep path keeps the driver fully functional (no ISR, noNVIC_EnableIRQ), only the CPU stops sleeping while a stage longer than 1 ms runs.
Build and run:
make OW_TARGET=g0 APP=7_low_power EXT="-DOW_PORT_LOW_POWER=1" # (append -DOW_PARASITE_POWER=1 on a parasite bus)
make OW_TARGET=g0 APP=7_low_power # same example, but standard polling (define omitted)| STM32F103 Pin | Function | DS18B20 Pin |
|---|---|---|
| PA10 | 1-Wire Data | DQ (Data) |
| 3.3V | Power | VDD |
| GND | Ground | GND |
Note: A 4.7kΩ pull-up resistor is required between the PA10 and 3.3V lines.
| STM32F103 Pin | Function | USB-UART Adapter |
|---|---|---|
| PA9 | USART1 TX (115200) | RX |
| GND | Ground | GND |
Connect a USB-UART adapter to see diagnostic output (sensor errors, temperature readings). No RX connection is needed — the firmware is transmit-only.
The UART output uses a ring buffer with polled TX (TXE flag checked in main loop) — fully non-blocking, no interrupts.
| Pin | Function | Notes |
|---|---|---|
| PA10 | 1-Wire Data | TIM1_CH3, open-drain AF2 (default topology) |
| PA9 | USART1 TX (115200) | RX line of the USB-UART adapter |
| PA4 | Busy LED (optional) | Active-low |
| PA13/PA14 | SWDIO/SWCLK | ST-Link SWD programming |
Note: the same 4.7kΩ pull-up is required between PA10 and 3.3V.
The STM32G0 backend is hardware-validated (see Hardware Verified above); the notes below cover the TSSOP20 package wiring:
| Pin | Function | Notes |
|---|---|---|
| PA12 | 1-Wire Data (logical PA10) | TIM1_CH3 AF2 after the SYSCFG PA12_RMP remap; open-drain AF (default topology) |
| PA11 | USART1 TX (logical PA9) | AF1 after the PA11_RMP remap |
| PA4 | Busy LED (optional) | Active-low |
| PA13/PA14 | SWDIO/SWCLK | ST-Link SWD programming |
Important: while the driver is initialised, pads PA11/PA12 must not be used as standalone GPIOs - configuring them as PA11/PA12 clears the SYSCFG remap bits and silently disconnects the bus.
Note: the same 4.7kΩ pull-up is required between the bus pin and 3.3V.
Note: "open-drain" above describes the default/idle bus topology, not a
static pin configuration. With the optional active-drive write mode
(-DOW_DRIVE_ACTIVE=1) the pin is temporarily switched to push-pull during
master-only write slots and restored to open-drain afterwards — see
Bus Electrical Model.
#include "ds18b20.h"Working with a non-DS18B20 1-Wire slave (DS2413, DS2431, ...)? Include
"onewire.h"instead and build directly on the shared bus primitives (onewire_reset(),onewire_write_then_read(), the Search ROM engine) — no DS18B20 code is pulled in.
Before ds18b20_init(), the driver must know the system-clock frequency: the
TIM1 prescaler that generates the 1-Wire timing derives from
OW_PORT_SYSCLK_MHZ. Each backend has a built-in default (72 on STM32F1,
48 on STM32F0, 64 on STM32G0), and the examples enable that clock in their
app_init(). If your firmware runs the MCU at a different frequency, provide
it explicitly — e.g. -DOW_PORT_SYSCLK_MHZ=8 for an 8 MHz HSI build.
Single sensor (Skip ROM). With one DS18B20 on the bus there is no need for a ROM search — the driver uses Skip ROM (broadcast) addressing by default:
#include "ds18b20.h"
int main(void) {
ds18b20_init(); // One-time initialization
while (1) {
ds18b20_poll(); // Call repeatedly from main loop
// Other application code...
}
}Multiple sensors (bus search). To find every device, run the non-blocking
Search ROM state machine, then address each sensor by ROM with
ds18b20_select():
#include "ds18b20.h"
static uint8_t found_roms[8][DS18B20_ROM_BYTES];
static uint8_t found_count = 0;
// Called for every DS18B20 the search finds; return 0 to keep searching.
static uint8_t on_device_found(const uint8_t* rom) {
for (uint8_t i = 0; i < DS18B20_ROM_BYTES; i++) {
found_roms[found_count][i] = rom[i];
}
found_count++;
return 0;
}
int main(void) {
ds18b20_init(); // One-time initialization
// The search owns the bus until it finishes: do not call ds18b20_poll()
// while it is running.
ds18b20_search_start(on_device_found, 8);
while (!ds18b20_search_poll()) {
// Repeatedly advance the search; returns 1 when it is finished.
}
if (found_count > 0) {
ds18b20_select(found_roms[0]); // Measure the first sensor
while (1) {
ds18b20_poll(); // Call repeatedly from main loop
// Other application code...
}
}
return 0;
}See examples/1_basic/main.c for a complete single-sensor setup and
examples/2_device_search/main.c for the search + round-robin loop.
Both callbacks are optional. Default weak implementations are provided by the
driver, and examples/app/app.c supplies per-MCU defaults: ds18b20_busy()
drives the onboard LED (F1: PC13, F0/G0: PA4) and ds18b20_complete() formats
and prints the result.
The callback example below is STM32F1-specific (Blue Pill onboard LED on PC13, active-low). Pin and polarity differ per MCU — see Hardware Connections: F1 uses PC13 active-low; F0/G0 use PA4 active-low. On F0/G0 the same logic targets
GPIOAinstead ofGPIOC.
// Busy indicator — e.g. LED toggling during measurement
// STM32F1 (Blue Pill): onboard LED on PC13, active-low.
void ds18b20_busy(unsigned action) {
if (action) {
// Turn LED on (measurement in progress)
GPIOC->BSRR = GPIO_BSRR_BR13;
} else {
// Turn LED off (measurement complete)
GPIOC->BSRR = GPIO_BSRR_BS13;
}
}
// Measurement complete callback — handle result or error
void ds18b20_complete(int16_t temp) {
if (temp >= -550 && temp <= 1250) {
// Valid temperature in tenths of °C
printf("Temperature: %d.%d°C\n", temp/10, abs(temp%10));
} else {
// Error condition
switch (temp) {
case DS18B20_TEMP_ERROR_NO_SENSOR:
printf("Error: No sensor detected\n");
break;
case DS18B20_TEMP_ERROR_CRC_FAIL:
printf("Error: CRC check failed\n");
break;
}
}
}- Toolchain:
arm-none-eabi-gcc(GCC 12+ recommended) and related utilities (objcopy,size). Clang is not a supported firmware toolchain — Clang references in this project refer to optional host-side tooling only (fuzz testing,clang-format, static analysis). - wget (or curl): Required for downloading CMSIS build dependencies.
- POSIX shell: On Linux/macOS any shell works; on Windows the
Makefiletargets (includingmake download-deps) need a Linux-like environment — Git Bash, MSYS2 or WSL.cmd/PowerShell are not supported for builds. - Programmer tools:
- ST-LINK:
st-flash(Linux/macOS) orST-LINK_CLI.exe(Windows) - J-LINK:
JFlashExe/JFlash.Exe/JLinkGDBServerCL.exe
- ST-LINK:
Building on Windows. The
Makefileuses POSIX shell constructs (mkdir -p,rm -rf,awk,sleep,command -v) and downloads dependencies viawget/curl, so the build itself cannot run from nativecmd/PowerShell. Use Git Bash, MSYS2 or WSL and ensuremake,wget(orcurl) andarm-none-eabi-gccare onPATH. The Windows-native flashers listed above are only invoked bymake program/make jprogram— they do not replace the POSIX build environment.
ARM CMSIS core headers and STM32 device files (F1/F0/G0) are not stored in the
repository. They are downloaded automatically at build time to
CMSIS/core/ and CMSIS/device/:
make download-depsTo remove them:
make clean-depsLicense files are also downloadable:
make download-licensesmake # Release build (-Os -flto -g0)
make debug # Debug build (-Og -g3 -gdwarf)Output goes to build/ (ds18b20_<app>.elf, .hex, .bin — e.g. ds18b20_1_basic.elf for the default APP=1_basic).
| Target | Description |
|---|---|
make / make all |
Build release |
make debug |
Build with debug symbols |
make test |
Build and run host tests (PC toolchain) |
make clean |
Remove build artifacts |
make download-deps |
Download CMSIS dependencies |
make clean-deps |
Remove downloaded dependencies |
make program |
Flash via ST-LINK |
make jprogram |
Flash via J-LINK |
make test-f0 |
Build and run host tests against the STM32F0 backend mock |
make test-g0 |
Build and run host tests against the STM32G0 backend mock |
make test COVERAGE=1 |
Host tests with gcov instrumentation (coverage report) |
make test-active |
Build and run host tests for the active-drive write path (-DOW_DRIVE_ACTIVE=1) |
make test-active-f0 |
Same as above against the STM32F0 backend mock |
make test-active-g0 |
Same as above against the STM32G0 backend mock |
make test-lowpower |
Same suite rebuilt with -DOW_PORT_LOW_POWER=1 (WFE path, F1) |
make test-lowpower-f0 |
Same as above against the STM32F0 backend mock |
make test-lowpower-g0 |
Same as above against the STM32G0 backend mock |
make test-ndebug |
Same suite with -DNDEBUG -DOW_TEST_PARAM_GUARD: asserts compiled out, so the rejected-size returns (0) are observable; adds test_param_guard |
make test-ndebug-f0 |
Same as above against the STM32F0 backend mock |
make test-ndebug-g0 |
Same as above against the STM32G0 backend mock |
make fuzz-all |
Build and run all fuzz harnesses (requires host-side Clang; FUZZ_TIME=N for duration) |
make fuzz-crc8 |
Fuzz onewire_crc8 alone |
make help |
Show all targets |
Optional build flags (append via EXT="..." or OW_DRIVE_ACTIVE=1):
| Flag | Effect |
|---|---|
OW_DRIVE_ACTIVE=1 |
Enable the optional active-drive write path (-DOW_DRIVE_ACTIVE=1): during master-only write slots the bus pin is temporarily switched to push-pull (see Bus Electrical Model). The default remains open-drain. |
TIMING=SLOW |
Apply a compile-time timing preset (default STANDARD; also FAST/SLOW/ROBUST/CUSTOM). Expands into -DONEWIRE_ONE_PULSE=... -DONEWIRE_ZERO_PULSE=... -DONEWIRE_GUARD_BAND=... -DONEWIRE_SHORT_PULSE_MAX=... for that preset. Override any single value with EXT="-DONEWIRE_GUARD_BAND=100". See Configuration → Timing. |
EXT="-DOW_PARASITE_POWER=1" |
Parasite-powered bus: raises the default guard band from 5 µs to 100 µs and builds every example with ds18b20_set_parasite(1) — the strong-pull-up window is engaged at runtime per conversion. See 6_statistics. |
EXT="-DOW_PORT_LOW_POWER=1" |
Enable the opt-in low-power path: the TIM1 update interrupt source (UIE) is enabled only to generate a pending event that wakes WFE() via SEVONPEND, so the application can sleep during long 1-Wire stages (> 1 ms) while the hardware completes the transaction. No ISR is installed and NVIC_EnableIRQ is never called; the driver itself stays non-blocking. Without this define builds are byte-identical to the original. |
- ST-LINK:
make program(usesst-flash/ST-LINK_CLI.exe) - J-LINK:
make jprogram(usesJFlashExe/JFlash.Exe)
The driver ships with a host test suite that runs entirely on the PC, no hardware required:
make test # host tests against the STM32F1 backend mock
make test-f0 # same suite against the STM32F0 backend mock
make test-g0 # same suite against the STM32G0 backend mockBoth src/onewire.c and src/ds18b20.c are compiled as a single translation
unit (tests/mock/ds18b20_test_access.c) against a behavioural model of the
TIM1/DMA hardware (tests/mock/hw_model.c) and a register mock of the target
CMSIS header — each suite runs the full driver against its own backend's
channel/DMA wiring. (The driver itself is an amalgamated translation unit
too: src/ds18b20.c #includes its four functional parts, so the whole
driver shares the ctx/txn_ctx/res_ctx/dev_roms statics in one
object file.) 297 tests run per backend (299 on G0, which adds two
DMAMUX request-routing tests). The suite covers:
- State machine transitions (idle → start → measure → read → decode)
- Non-blocking device search (Search ROM, ROM CRC validation, multi-device)
- Non-blocking alarm search (Alarm Search ROM, 0xEC command feed, scan-table isolation)
- Non-blocking resolution change (
ds18b20_set_resolution_*): exact wait timings for 9/10/11/12 bit, Skip ROM and Match ROM config writes, CCR3-feed bus release, ownership guards, presence-abort and scratchpad auto-derivation - Non-blocking command transactions (
ds18b20_read_rom,ds18b20_set_alarm_thresholds,ds18b20_read_scratchpad,ds18b20_copy_scratchpad,ds18b20_recall_eeprom,ds18b20_detect_parasite): command feed builds (Skip/Match ROM), resolution-preserving TH/TL writes, raw scratchpad read + CRC + resolution auto-derivation, 10 ms Copy/Recall hold-offs, power-supply decode, ownership guards, presence-abort and result reporting - CRC-8 (Dallas/Maxim) verification
- 1-Wire pulse encoding and presence detection
- 1-Wire layer coverage: reset/presence timing, write-then-read merge, multi-slot writes, multi-byte reads, search engine (device + alarm), ownership guards and the merged search capture buffers
- DMA buffer/transfer contracts (
test_dma), DMAMUX request routing on G0 (test_dmamux) - Scratchpad decode and temperature conversion (incl. negative values)
- Timing configuration and register setup
- Bus release behaviour between slots
- Channel broadcast (
test_broadcast), UART app driver (test_app_uart) and test-scaffold accessors/harness edge-case branches (test_harness_api) ow_statscapture and error counters plus the non-blocking dump protocol (test_ow_stats)
Separate opt-in builds extend the suite: make test-active (active-drive,
8 tests) and make test-lowpower (full suite + low-power WFE path, 309 tests).
The repository ships with library.json and library.properties so
PlatformIO can discover the library automatically.
# One-time: fetch CMSIS headers (the ststm32 platform provides its own
# copy, but the driver's ow_port_* headers expect the standard layout
# under CMSIS/core/ and CMSIS/device/).
make download-depsMinimal platformio.ini:
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = stm32cube
lib_deps = a5021/stm32-async-1wireOr point to a local checkout:
lib_deps = symlink:///path/to/stm32-async-1wireThe library is also compatible with the Arduino Library Manager (see
library.properties).
The root CMakeLists.txt provides a stm32_async_1wire static library
target with FetchContent-managed CMSIS dependencies — no pre-downloaded
headers needed. A bare-metal toolchain file is included.
# ARM GCC must be on PATH
cmake -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake \
-DOW_TARGET=f1 -B build .
cmake --build buildSelect the MCU family with -DOW_TARGET=f1 (default), f0, or g0.
In a downstream project:
FetchContent_Declare(stm32_1wire
GIT_REPOSITORY https://github.com/a5021/stm32-async-1wire.git
GIT_TAG v1.8.1
)
FetchContent_MakeAvailable(stm32_1wire)
target_link_libraries(your_app PRIVATE stm32_async_1wire)- Run
make download-depsonce to fetch CMSIS headers. - In STM32CubeIDE: File → New → STM32 Project from an Existing Makefile.
- Point to the repository root directory.
- The IDE auto-generates the CDT project; select your MCU target (e.g. STM32F103C8).
- Build and flash as usual.
-
Target Name: The firmware target name is
ds18b20_$(APP)(e.g.ds18b20_1_basic). -
Build Directory: Default is
build/. -
Optimization Level:
- Release:
-Os -flto -g0(default). - Debug:
-Og -g3 -gdwarf.
- Release:
-
MCU Flags:
STM32F103xB(Cortex-M3) by default;STM32F030x6(Cortex-M0) withOW_TARGET=f0;STM32G031xx(Cortex-M0+) withOW_TARGET=g0. -
Target Selection:
make OW_TARGET=f0builds for the STM32F0 backend (48MHz default clock,port/stm32f0/STM32F030X6_FLASH.ld),make OW_TARGET=g0for the STM32G0 backend (64MHz default clock,port/stm32g0/STM32G031X6_FLASH.ld). The default target is STM32F103 (bus on PA10 for F1/F0, logical PA10 via PA12 remap for G0). -
8MHz RC Build: By default the firmware runs on HSE 8MHz + PLL ×9 = 72MHz. Pass
SYSCLK_MHZ=8to use the internal RC oscillator (HSI) at 8MHz without an external crystal or PLL:make SYSCLK_MHZ=8 make debug SYSCLK_MHZ=8
On the STM32F030 target the default clock is already HSI+PLL (48MHz); there
SYSCLK_MHZ=8selects the raw 8MHz HSI instead.The knob maps to the portable
OW_PORT_SYSCLK_MHZdefine — a single value in MHz that every clock-dependent setting derives from: the timer prescaler, the input-capture filter and the USART baud rate adjust automatically. Useful for testing on bare minimum hardware (no HSE crystal).
All genuinely tunable build constants live in inc/ow_config.h. Every
macro carries a #ifndef guard so that a -D on the command line (Makefile
EXT, PlatformIO build_flags) overrides the default without editing the
header. (These are preprocessor macros, not CMake options: a cmake -DOW_STATS_ENABLE=1 variable would not reach the compiler. The CMake
example build enables stats automatically for 6_statistics — see below.)
Protocol-inherent values (ONEWIRE_MAX_SLOTS,
DS18B20_RES_MIN/MAX/DEFAULT) and the per-family system clock default
(OW_PORT_SYSCLK_MHZ) remain in their respective headers and are NOT
listed here.
The four feature flags use value style: define to 1 to enable,
omit or set to 0 to disable. Old presence-only style
(-DOW_PORT_LOW_POWER without =1) no longer compiles correctly.
| Macro | Default | Notes |
|---|---|---|
ONEWIRE_ONE_PULSE |
5 | '1'-bit duration in µs — HW-validated on every clock |
ONEWIRE_ZERO_PULSE |
60 | '0'-bit duration in µs |
ONEWIRE_GUARD_BAND |
5 (100 when OW_PARASITE_POWER=1) |
Slot release margin in µs |
ONEWIRE_SHORT_PULSE_MAX |
10 | Short-pulse detection window in µs |
OW_PARASITE_POWER |
0 | 1 = set parasite timing defaults (guard band) |
OW_PORT_LOW_POWER |
0 | 1 = enable opt-in WFE sleep path |
OW_DRIVE_ACTIVE |
0 | 1 = enable push-pull write path |
OW_STATS_ENABLE |
0 | 1 = compile in per-sensor pulse statistics |
DS18B20_MAX_DEVICES |
8 | Max devices in the device table (8 B each) |
DS18B20_CYCLE_PAUSE_US |
5000000 | Inter-measurement pause in µs (0 = none) |
Override examples
make # defaults, all flags =0
make EXT="-DOW_PORT_LOW_POWER=1" # enable WFE sleep path
make EXT="-DOW_PARASITE_POWER=1" # parasite guard-band default
make EXT="-DONEWIRE_SHORT_PULSE_MAX=15 -DDS18B20_MAX_DEVICES=16"CMake: the OW_STATS_ENABLE/DS18B20_CYCLE_PAUSE_US flags are not CMake
options. With -DOW_BUILD_EXAMPLES=ON they are applied automatically to
the 6_statistics example only (its dedicated library variant), mirroring
make APP=6_statistics:
cmake -DOW_TARGET=f0 -DOW_BUILD_EXAMPLES=ON -B build .
cmake --build build --target 6_statisticsOther CMake feature flags can be forwarded to the compiler via a standard
CMAKE_C_FLAGS (or a toolchain-file edit) — the #ifndef guard in
ow_config.h picks them up:
PlatformIO (platformio.ini):
build_flags = -DOW_PORT_LOW_POWER=1 -DOW_STATS_ENABLE=1Feature flags can also be set via a Makefile knob (no EXT needed):
make OW_DRIVE_ACTIVE=1 # → -DOW_DRIVE_ACTIVE=1The repository includes .vscode/ workspace configuration for a
convenient development workflow.
Press Ctrl+Shift+B to run the default build task (make). Other
tasks are available via Ctrl+Shift+P → "Tasks: Run Task":
Build (release)—make(default)Build (debug)—make debugBuild F0 (debug)—make OW_TARGET=f0 debug(debug build for the STM32F030 target)Build G0 (debug)—make OW_TARGET=g0 debug(debug build for the STM32G031 target)Clean—make cleanProgram (J-Link)/Program (ST-Link)— flash the deviceDownload dependencies—make download-deps
- In the Run and Debug panel (
Ctrl+Shift+D), select the debug configuration: "Debug F1 (J-Link)" / "Debug F1 (ST-Link)" for the STM32F103 target, or "Debug F0 (J-Link)" / "Debug F0 (ST-Link)" for the STM32F030 target, or "Debug G0 (J-Link)" / "Debug G0 (ST-Link)" for the STM32G031 target. The F0 configurations build withOW_TARGET=f0automatically, the G0 ones withOW_TARGET=g0. - Open
examples/1_basic/main.cand set a breakpoint inmain(). - Press F5 — Cortex-Debug will build the firmware in debug mode,
flash it, run to
main(), and halt.
The SVD file for the selected family is downloaded by make download-deps
and loaded automatically for peripheral register views in the debug
sidebar. Standalone SEGGER Ozone users can open port/<mcu>/project.jdebug
from either backend directory; the project resolves its SVD and ELF paths
relative to its own location.
J-Link: Connect a SEGGER J-Link debugger via SWD.
ST-Link: Connect an ST-Link programmer (built into most Blue Pill
boards) via SWD.
The DS18B20 uses the 1-Wire bus protocol, which communicates over a single data line with strict timing requirements. Several approaches exist to handle this protocol on embedded systems:
| Technique | How it works | Blocking? | Timing precision | Typical use |
|---|---|---|---|---|
| Bit-banging + delay (e.g. OneWire Arduino) | GPIO toggling with delayMicroseconds(), interrupts disabled |
Yes | Low (compiler/optimization dependent) | Hobbyist Arduino projects |
| Bit-banging + timer ISR | Timer interrupt drives GPIO transitions | Semi-blocking | Medium | RTOS-based firmware |
| UART bit-banging | UART at 9600/115200 baud emulates 1-Wire timings | Depends | Medium | Systems with spare UARTs |
| Hardware 1-Wire master | Dedicated IC (DS2482) or kernel subsystem (Linux w1-gpio) | No | High | Linux SBCs, complex systems |
| Timer + DMA + One-Pulse Mode (this driver) | DMA feeds CCR values to the timer, which generates bus timing without CPU-driven edges | No | Hardware-timed, 1 µs timer resolution; no CPU-induced edge jitter | STM32 firmware with available timer and DMA resources |
Cost. This driver consumes dedicated hardware resources — TIM1 and two DMA1 channels (the capture drain from CCR4 and the marker feed into CCR3) — that cannot be used for other purposes. The 1-Wire data line itself occupies one GPIO (PA10), but any approach needs a GPIO pin for the bus, so that is not an extra cost. Bit-banging approaches, by contrast, need only that one pin and no DMA, making them more portable across MCUs with limited peripherals.
Precision vs. portability. Timer+DMA keeps the CPU out of the timing-critical path. The timer schedules bus edges at a nominal 1 µs resolution, so CPU activity does not introduce edge jitter. Actual timing accuracy depends on the timer clock and the electrical characteristics of the bus. Software delays degrade under interrupt load, and even timer-ISR approaches incur jitter from preemption. The trade-off is complexity: this driver's hardware configuration is ~150 lines of register-level code versus ~20 lines for a typical bit-bang implementation.
The 1-Wire layer uses a hybrid of several hardware features:
- Timer-Driven Sequences: TIM1 is configured in One-Pulse Mode (OPM). Each state machine step configures the timer for a specific operation (reset, write byte, read byte, wait) and starts it.
- DMA for Data Transfer: DMA is used in two key ways:
- Transmit: Feeds a pre-calculated sequence of Compare Register (CCR) values to TIM1->CCR3 to automatically generate the precise waveform for writing commands or bits. The feed request comes from the CH2 slot-end marker compare and rides DMA1 channel 3.
- Capture: Automatically stores values from the TIM1->CCR4 capture register into memory to record pulse timings during read operations or presence detection; the capture drain rides DMA1 channel 4.
- Update Event as Completion Signal: The core polling mechanism checks the Timer Update Flag (TIM1->SR UIF). This flag is set when the timer completes its one-pulse countdown, signaling that the autonomous hardware operation (e.g., sending a reset pulse, waiting 750ms) is finished.
- True Zero-ISR Overhead: The ds18b20_poll() function checks this flag. When set, it clears the flag and advances the state machine to the next step. This makes the entire driver event-driven by hardware completion signals without using interrupts.
- No Software Delays: No
delay_us()or similar functions. - No Interrupts: Does not configure or use the NVIC. Fully deterministic.
- Hardware Completion Events: The state machine advances only when the hardware timer signals that its current automated task is complete.
- Minimal CPU During Operations: The CPU is only actively involved to set up a hardware operation and to process the result once it completes.
The driver ships with a built-in non-blocking device search (
ds18b20_search_*) for multi-sensor buses. The Maxim Search ROM (0xF0) algorithm is implemented as a compact state machine in the shared 1-Wire layer; it performs exactly one hardware-timed operation per poll call, consistent with the non-blocking measurement path. Seeexamples/3_round_robin/main.cfor a complete Search ROM example.
All bus-level protocol lives in src/onewire.c (interface in inc/onewire.h),
a reusable 1-Wire master that the DS18B20 driver builds on:
onewire_init(),onewire_reset(),onewire_present(),onewire_write_slots(),onewire_write_bit(),onewire_encode_byte(),onewire_read_pair(),onewire_write_then_read(),onewire_pair_bits(),onewire_read_data(),onewire_decode_pulses(),onewire_start_timer(),onewire_bus_done()— the TIM1/DMA bus primitives.onewire_search_start(),onewire_search_poll(),onewire_search_count(),onewire_search_active()— the generic Maxim Search ROM engine, shared byds18b20_search_*andds18b20_alarm_search_*.onewire_crc8()— the Dallas/Maxim CRC-8 utility.
Each bus transaction is timed and executed by TIM1 and DMA, without CPU-driven
timing. The application calls onewire_bus_done() / onewire_search_poll() to
let the non-blocking state machine check hardware completion and advance to the
next step; no busy-wait is used. Helper functions such as CRC calculation and
pulse encoding/decoding process data in software and do not start bus
transactions. The layer owns its own capture
buffers, keeps the line released to idle HIGH after every transaction, and
is fully covered by the host test suite. See the API Reference below for the
complete onewire_* surface.
The contract describes the functional peripheral topology required by the driver. A new backend is valid only if the target MCU can realise the complete topology simultaneously on one timer instance and its DMA routing; having the individual features somewhere in the MCU is not sufficient. The topology is:
ONE TIMER
│
┌───────────┼───────────┐
▼ ▼ ▼
CH2 CH3 CH4
│ │ │
DMA GPIO IC
│ │
└── TI3 ◄───┘
The driver does not care which timer is used — it cares about what the timer
can do. The following capabilities are hard requirements; a timer that lacks
any one of them cannot run this driver, which is why the port layer
(port/stm32f*/ow_port_*.h) pins each backend to one specific timer rather
than letting the user choose.
| Capability | Why it is required |
|---|---|
| RCR (Repetition Counter) | Batches N PWM periods into a single Update Event. Without RCR, every bit slot generates its own update — a 16-bit command would need 16 interrupt handlers or 16 poll rounds instead of one. RCR=15 lets the timer autonomously generate 16 slots, then assert UIF once so the state machine advances in a single step. Only advanced-control timers (TIM1/TIM8 on STM32) have RCR. |
| BDTR + MOE (Main Output Enable) | Gates the entire output stage. The driver sets MOE once at init and never touches it again; when the timer stops (OPM) the output goes high-impedance and the external pull-up takes over. General-purpose timers lack BDTR — their output is always driven. |
| One-Pulse Mode (OPM) | The timer auto-stops after the scheduled operation completes (one reset pulse, one byte write, one long conversion wait). Each bus transaction is a self-contained hardware run; OPM guarantees the timer does not free-run and re-enter a spurious slot. |
The driver requires three channels on the same timer, with a specific capture-routing relationship:
| Channel | Role | Requirement |
|---|---|---|
| CH3 | PWM output (active-low) | PWM Mode 2, pin drives the 1-Wire bus. Each bit slot is one PWM period: short low (~5 µs) = '1', long low (~60 µs) = '0'. Reset is an extended low within a ~960 µs slot. |
| CH4 | Input capture (indirect) | Must be routable to TI3 (the CH3 pin) via CC4S=01. This is the constraint that eliminates many timer/pin combinations: only one input capture channel on each STM32 timer can watch a given output channel's pin, and on every supported family that channel is IC4→TI3. CH4 captures presence pulses and read-slot timings after CH3 releases the bus to idle-HIGH. |
| CH2 | Compare (end-of-slot marker) | A plain compare at ONEWIRE_ONE_PULSE + ONEWIRE_ZERO_PULSE µs whose DMA request feeds the next CCR3 value from a precomputed pulse buffer. CH2's pin is unused — only its compare event and DMA request matter. |
The indirect-capture constraint (IC4 must see TI3) is why the driver cannot
be moved to an arbitrary pin: the chosen GPIO must be the CH3/CH4 pair's
output/capture pin on the selected timer. On STM32F1 this is PA10
(default AFIO map); on STM32F0, PA10 (AF2); on STM32G0, PA12 pad remapped
to logical PA10 via SYSCFG_CFGR1.PA12_RMP.
Two DMA channels are required, each carrying a specific peripheral request:
| Channel | Direction | Request | Purpose |
|---|---|---|---|
| DMA channel A | Memory → Peripheral | TIM1_CC2 (CH2 compare) | Feeds CCR3 with the next pulse width on each slot boundary (the "feed" path). |
| DMA channel B | Peripheral → Memory | TIM1_CH4 (capture) | Drains CCR4 capture values into a memory buffer (the "capture" path). |
F0/F1 have a fixed request map (no DMA_CSELR mux) — channel 3 = CC2,
channel 4 = CC4, confirmed empirically. G0 uses a DMAMUX: TIM1_CC2 = request
21, TIM1_CH4 = request 23. A new backend must verify the DMA request
numbers for its target; the channel roles are identical across all families.
The APB prescaler feeding the timer must be /1. STM32 timers double
their clock when the APB prescaler is >1 (timer clock = 2 × PCLK), which
would break every µs-based timing constant in the driver. All three
supported families satisfy this by construction: F1 keeps PPRE2=/1 (TIM1 is
on APB2), F0 and G0 have a single APB bus at /1.
The bus pin must support alternate-function open-drain (for normal bus operation) and runtime switching to alternate-function push-pull (for parasite-power strong pull-up and the optional active-drive write path). The pin never leaves alternate function — only the output-stage topology changes.
| Family | Timer | Bus pin | DMA routing | Notes |
|---|---|---|---|---|
| STM32F1 | TIM1 | PA10 (default AFIO) | Fixed: CC2→DMA1 ch3 (feeds CCR3), CH4→DMA1 ch4 | APB2=/1 by default |
| STM32F0 | TIM1 | PA10 (AF2) | Fixed: same mapping | TSSOP20: PA8 not bonded out, CH3/CH4 is the only viable pair |
| STM32G0 | TIM1 | PA10 via PA12 remap | DMAMUX: CC2=#21, CH4=#23 | SYSCFG PA12_RMP; PA11/PA12 cannot be used as GPIO while driver is active |
-
Open-drain bus. PA10 is configured as an alternate-function open-drain pin and idles HIGH; a single external pull-up resistor on the bus is required. Both the master and every slave are open-drain, so the bus is a wired-AND: if any device drives the line LOW the bus is LOW, otherwise it is pulled HIGH.
-
Normal slot signaling (master never drives HIGH). A
write-0or bus reset is the master actively pulling the line LOW; awrite-1or read slot is the master releasing the pin to Hi-Z and letting the external pull-up return the line HIGH. The master therefore never actively drives the line HIGH during a normal slot —write-1is a release, not a push-pull HIGH. -
Parasite strong-pull-up (the only unconditional push-pull usage in the default build).
onewire_strong_pullup()(ow_port_strong_pullup()) is the only place the pin is switched to push-pull in the default build; it actively sources current by driving the line HIGH during the temperature-conversion / EEPROM-programming window — a phase where the DS18B20 is silent and cannot respond on the bus. It is restored to open-drain afterwards. -
Active-drive write mode (optional). During master-only write slots the bus pin may be placed in alternate-function push-pull, actively driving both HIGH and LOW levels instead of relying on the pull-up for HIGH. The driver automatically retains open-drain operation for reset, presence, read slots and write/read transactions where the slave may drive the bus. This is what makes push-pull acceptable on a 1-Wire bus at all: it is confined to phases where no slave can answer. Enabled with
-DOW_DRIVE_ACTIVE=1; the published default remains open-drain.
Kickstart behavior
-
After ds18b20_init(), the timer update flag (UIF) is already set. This ensures the very first call to ds18b20_poll() advances the state machine immediately without any extra priming step.
-
IDLE (state 0)
- Immediately falls through into START with no events required. Ensures LED is off and initialises the data union.
- Set state=1.
-
START (state 1)
- LED on. Run reset_bus():
- CH3 issues active-low reset pulse (~480µs within ~960µs slot).
- CH4 (indirect input) captures presence timing into ctx.capture[0..1] via DMA from CCR4.
- Set state=2.
- LED on. Run reset_bus():
-
CONVERT (state 2)
- On UIF, check_presence() with ctx.capture[].
- If present: send convert command via CH3+DMA. With no selected device,
this is "Skip ROM 0xCC + Convert T 0x44" (16 slots, RCR=15). With a
device selected via ds18b20_select(), it is "Match ROM 0x55 + 8-byte ROM
- Convert T 0x44" (80 slots, RCR=79). Set state=3.
- Else: report NO_SENSOR; start 5s pause; set state=0.
- If present: send convert command via CH3+DMA. With no selected device,
this is "Skip ROM 0xCC + Convert T 0x44" (16 slots, RCR=15). With a
device selected via ds18b20_select(), it is "Match ROM 0x55 + 8-byte ROM
- On UIF, check_presence() with ctx.capture[].
-
WAIT (state 3)
- On UIF: wait_conversion() schedules exactly the conversion time of the
configured resolution
ctx.resolution(9-bit: 10×9.375ms; 10-bit: 10×18.75ms; 11-bit: 20×18.75ms; 12-bit: 12×62.5ms = 750ms). The resolution is auto-derived from each valid scratchpad read and updated byds18b20_set_resolution(); set state=4.
- On UIF: wait_conversion() schedules exactly the conversion time of the
configured resolution
-
CONTINUE (state 4)
- On UIF: run reset_bus() again; set state=5.
-
REQUEST (state 5)
- On UIF, check_presence().
- If present: send read command via CH3+DMA. With no selected device, this is "Skip ROM 0xCC + Read Scratchpad 0xBE" (16 slots). With a device selected, it is "Match ROM 0x55 + 8-byte ROM + Read Scratchpad 0xBE" (80 slots). Set state=6.
- Else: report NO_SENSOR; start 5s pause; set state=0.
- On UIF, check_presence().
-
READ (state 6)
- On UIF: read_data() schedules 72 slots (RCR=71; ARR=70µs). CH3 emits ~5µs active-low kick at each slot start and then releases; CH4 captures sensor pulse timing; DMA fills ctx.pulse[72]. Set state=7.
-
DECODE (state 7)
- On UIF: decode_scratchpad() from ctx.pulse[] into ctx.scratchpad[], LED off; verify CRC; report temperature or CRC_FAIL; start 5s pause; set state=0.
Verified on hardware (this driver, TIM1+DMA one-pulse bus) against the DS18B20
datasheet. Relevant if ds18b20_search_poll() / ds18b20_poll() are called
from an RTOS task and scheduling delays can land between 1-Wire slots:
- Idle-HIGH is harmless. The datasheet states the 1-Wire bus must be left in the inactive (high) state when suspending a transaction and that "infinite recovery time can occur between bits so long as the bus is in the inactive (high) state during the recovery period." The DS18B20 re-synchronises to the next falling edge; it has no internal timeout that expires during an idle-high pause.
- Measured: injecting real idle-high gaps of 10 µs … 5 ms between Search ROM slots finds all 5 devices in 100/100 runs at every gap size.
- Idle-LOW > 480 µs resets all devices (datasheet: "if the bus is left low for more than 480 µs, all components on the bus will be reset"). This is the only real hazard.
Practical consequences for RTOS use:
- A delayed poll is safe because the line is released to HIGH in hardware,
not by software. Every bus operation now ends with the line idle-HIGH
automatically: the CCR3-fed writes (
ow_port_feed, including the merged search op) append a trailing 0 to the DMA feed, and the direct-write/capture operations (reset, read, single-slot write) use an OC3PE preload of 0 — both applied at the instant the one-pulse timer stops. ThoseT1.CCR3 = 0preloads are the only software CCR3 writes on the direct paths, latched at the stopping update event; the bus cannot be left LOW by a stale compare value, no matter how long the RTOS delays the next poll. - The usable scheduling latency budget is ~480 µs of LOW, not a tight microsecond window. Any RTOS that resumes the poll within hundreds of µs is fine; longer delays only require that the bus idles HIGH, which the hardware release guarantees by construction.
The driver is safe to call from an RTOS task, but it is not re-entrant and
not thread-safe by itself. All driver state is global and shared: the DS18B20
state machine, the 1-Wire search context (search_ctx, in onewire.c), and
the TIM1/DMA1 peripherals. There is no internal lock. The ownership guards
(txn_can_start, the ds18b20_search_* checks) only prevent logical
conflicts within a single-threaded model — they are plain flag checks, not
atomic across tasks.
Rules for correct RTOS use:
-
Confine every
ds18b20_*call to a single task, or serialise with a lock. Either drive the whole driver from one task (the same one that callsds18b20_poll()), or wrap every entry point — each*_start, each*_poll,ds18b20_select(),ds18b20_scan_start(),ds18b20_search_start()— in a mutex/semaphore taken for the entire sequence (start + poll loop). Two tasks touching the driver at once corrupt the shared state machine and the TIM1/DMA registers. Example (FreeRTOS):xSemaphoreTake(ow_mutex, portMAX_DELAY); ds18b20_set_alarm_thresholds(0x19, 0x0F); while (!ds18b20_set_alarm_thresholds_poll()) { osDelay(1); /* the transaction owns TIM1/DMA; just yield */ } xSemaphoreGive(ow_mutex);
While a search, a resolution change or a command transaction is running,
ds18b20_poll()returns immediately without doing anything, so the operation must be advanced by its own*_poll()(as in the loop above) — not byds18b20_poll(). -
TIM1 and DMA1 (channels 3 and 4) are owned by the driver. No other task or peripheral may use them while the driver is initialised.
-
Poll cadence vs latency. Because the 1-Wire bit timing is generated entirely by hardware,
ds18b20_poll()may be called at any rate — slow polling only increases latency, never causes errors (see Bus Idle Behaviour). Two practical patterns:- Dedicated polling task: loop
ds18b20_poll(); osDelay(1);(orvTaskDelay(1)). This gives low latency without saturating the CPU; the 750 ms conversion wait is a hardware timer, so the task yields during it. If other tasks also use the driver, take the mutex around eachds18b20_poll()call (or skip the poll when the mutex is busy). - Periodic timer / idle hook: call
ds18b20_poll()from a high-frequency timer callback or the RTOS idle hook. Avoid a tightwhile (!done) poll();busy loop — it consumes the task's entire timeslice.
- Dedicated polling task: loop
-
Callbacks run in task context, not in an ISR.
ds18b20_complete()andds18b20_busy()are invoked synchronously from insideds18b20_poll(), which runs in your task. You may therefore use ordinary RTOS primitives there — e.g.xSemaphoreGive()/xTaskNotifyGive()fromds18b20_complete()to wake a consumer task. No...FromISRvariant is needed. -
No built-in blocking API. The driver never blocks; there is no
ds18b20_read_temperature_blocking(). If a task must sleep until a result is ready, start the operation (ds18b20_scan_start(),ds18b20_search_start()or a command transaction), then either poll in a loop that yields (osDelay(1)), or block on a semaphore thatds18b20_complete()releases. Do not add a TIM1 interrupt just to signal completion — polling is sufficient and preserves the zero-interrupt design. -
Preemption mid-byte is safe. A task may be preempted while a byte is being transmitted: the DMA completes the whole byte in hardware and the bus returns idle-HIGH, so resuming later is harmless (it complements Bus Idle Behaviour). Only the next operation must be scheduled by a
poll()call, which any task may do once it holds the lock from rule 1.
void ds18b20_init(void);Initialize the DS18B20 driver. Enables peripherals (GPIOA, TIM1, DMA1) and sets up the timer prescaler for 1µs resolution. System clock configuration is handled separately in the application (see app.c). This function does NOT start the state machine.
void ds18b20_poll(void);The Core Driver Function: Must be called from the main loop. It checks the Timer Update Flag (UIF). If the flag is set, it means the hardware has finished the previous operation (e.g., sending a command, waiting for conversion). The function then clears the flag and advances the internal state machine to the next step. The driver's state is persistent, so this function can be called at any rate without risk of getting stuck.
The 1-Wire bus primitives and the Search ROM engine are not part of the
driver — they live in the shared 1-Wire layer (inc/onewire.h +
src/onewire.c) that src/ds18b20.c is built on. Full interface:
void onewire_init(void);
uint8_t onewire_bus_done(void);
void onewire_reset(volatile uint16_t *reset_pulses);
uint8_t onewire_present(const volatile uint16_t *pulses);
uint8_t onewire_write_slots(const uint8_t *pulses, uint16_t slots);
uint8_t onewire_write_bit(uint8_t bit);
void onewire_encode_byte(uint8_t *out, uint8_t byte);
void onewire_read_pair(volatile uint16_t *pair_pulses);
void onewire_write_then_read(uint8_t bit);
void onewire_pair_bits(const volatile uint16_t *pair_pulses,
uint8_t *id_bit, uint8_t *cmp_bit);
uint8_t onewire_read_data(volatile uint8_t *dst, uint8_t bytes);
void onewire_decode_pulses(uint8_t *dst, const volatile uint8_t *pulse,
uint8_t nbytes);
void onewire_start_timer(uint16_t arr, uint8_t rcr);
uint8_t onewire_crc8(const uint8_t *data, uint8_t len);
void onewire_search_start(onewire_search_sink_t sink,
uint8_t max_devices, uint8_t command,
uint8_t family);
uint8_t onewire_search_poll(void);
uint8_t onewire_search_count(void);
uint8_t onewire_search_active(void);onewire_write_slots(), onewire_write_bit() and onewire_read_data()
report whether the operation was scheduled: they return 1 on success and
0 when the size argument is out of range and nothing was started. Debug
builds additionally trap the reject path with assert; with NDEBUG the
caller observes the 0 instead of a silent no-op, so an invalid size can never
turn into an undiscovered onewire_bus_done() hang.
Slot timing is fixed at compile time (inc/ow_config.h): four ONEWIRE_*
macros define the bit-slot geometry, and every bus operation is scheduled with
those exact durations. There is no runtime profile switching and no timing
state — the values fold into the TIM1/DMA register arithmetic and can be
overridden individually via -D.
#define ONEWIRE_ONE_PULSE 5 /* µs: short low = write-1 / read pulse */
#define ONEWIRE_ZERO_PULSE 60 /* µs: long low = write-0 */
#define ONEWIRE_GUARD_BAND 5 /* µs: slot tail (external-power default) */
#define ONEWIRE_SHORT_PULSE_MAX 10 /* µs: pulse <= this decodes as bit '1' */
void onewire_strong_pullup(uint8_t on); /* parasite power: drive bus HIGH */On a parasite-powered bus the release margin must be wider: define
OW_PARASITE_POWER as 1 (-DOW_PARASITE_POWER=1) to select the 100µs guard
band default, or pass -DONEWIRE_GUARD_BAND=... explicitly. The Makefile
presets in Configuration → Timing select whole value sets.
Any other 1-Wire slave driver can use the same layer. The DS18B20 driver calls
onewire_init() from ds18b20_init() and keeps the layer's Search ROM engine
for its own ds18b20_search_* / ds18b20_alarm_search_* wrappers.
uint8_t ds18b20_crc8(const uint8_t *data, uint8_t len);Calculates the Dallas/Maxim CRC-8 used by the driver to validate ROM codes and
scratchpad data. Exposed publicly as a small utility (e.g., for host tools).
It is a thin DS18B20-namespaced wrapper around the shared onewire_crc8()
(see 1-Wire Layer); both compute the same checksum.
void ds18b20_search_start(ds18b20_search_sink_t sink, uint8_t max_devices);
uint8_t ds18b20_search_poll(void);
uint8_t ds18b20_search_count(void);Non-blocking Maxim Search ROM (0xF0) over the whole bus, implemented as a
compact state machine that performs exactly one hardware operation per
ds18b20_search_poll() call. sink is invoked once per found DS18B20 device
with its 8-byte ROM address; max_devices caps the reported count. Poll
ds18b20_search_poll() from the main loop until it returns 1 — it restores
ds18b20_poll() state automatically. ds18b20_search_count() returns how many
devices were found. Only devices with family code DS18B20_FAMILY_CODE (0x28)
are reported. See examples/3_round_robin/main.c.
void ds18b20_alarm_search_start(ds18b20_search_sink_t sink, uint8_t max_devices);
uint8_t ds18b20_alarm_search_poll(void);
uint8_t ds18b20_alarm_search_count(void);Non-blocking Maxim Alarm Search ROM (0xEC): reports only the DS18B20 devices
currently in alarm state, i.e. whose last measured temperature is outside the
TH/TL thresholds configured with Write Scratchpad (0x4E). It shares the device
search engine, so the callback, max_devices cap, family filter and ownership
rules are identical; only the command byte and the reported set differ. Unlike
ds18b20_search_start(), the alarm search never repopulates the scan-mode
device table (ds18b20_device_count() / ds18b20_device_rom() keep the
addresses from the last device search). Poll ds18b20_alarm_search_poll() from
the main loop until it returns 1, then read ds18b20_alarm_search_count().
void ds18b20_select(const uint8_t *rom);Selects which DS18B20 device the non-blocking measurement path targets, using
its 64-bit ROM address (LSB first, e.g. from a bus search). With a device
selected, the driver sends the Match ROM (0x55) command plus the device ROM
before every Convert T / Read Scratchpad operation, so only that device
responds. Pass NULL to clear the selection and return to the legacy Skip
ROM (0xCC) single-sensor behaviour.
ds18b20_select() is only accepted while the measurement state machine is
IDLE — calls made while a cycle is running, during a device/alarm search,
a resolution change or another command transaction are ignored. In particular
it is rejected from the per-device scan callback (ds18b20_complete() in
scan mode): that callback runs at decode time mid-round, so a select there is
ignored and the scan round continues. To switch out of scan mode, call
ds18b20_select() from the main loop after the scan completes, then
ds18b20_scan_start() to resume simultaneous conversion.
ds18b20_select() targets one device and does not automatically cycle through
the devices found by a search. For round-robin measurements, the application
stores the ROM addresses returned by the search and selects the next device
between measurement cycles; see examples/3_round_robin/main.c.
To convert all discovered devices in parallel, use the separate scan mode:
ds18b20_scan_start() broadcasts one Convert T command, then reads each device
by ROM address and reports results in device-table order. See
examples/4_scan_mode/main.c. Scan mode and single-device selection are
mutually exclusive.
The remaining DS18B20 commands run with the same non-blocking discipline as the
device search and the resolution change: each transaction owns TIM1/DMA while
it runs (reset → presence → write → read | timed wait) and hands the timer
back to ds18b20_poll() when it finishes.
void ds18b20_read_rom(uint8_t *rom);
uint8_t ds18b20_read_rom_poll(void);
void ds18b20_set_alarm_thresholds(uint8_t th, uint8_t tl);
uint8_t ds18b20_set_alarm_thresholds_poll(void);
void ds18b20_read_scratchpad(uint8_t *buf);
uint8_t ds18b20_read_scratchpad_poll(void);
void ds18b20_copy_scratchpad(void);
uint8_t ds18b20_copy_scratchpad_poll(void);
void ds18b20_recall_eeprom(void);
uint8_t ds18b20_recall_eeprom_poll(void);
void ds18b20_set_parasite(uint8_t parasite);
void ds18b20_detect_parasite(void);
uint8_t ds18b20_detect_parasite_poll(void);
uint8_t ds18b20_parasite_mode(void);
uint8_t ds18b20_last_command_ok(void);- Every command is a
start/pollpair: call the start function, then poll the matching*_poll()from the main loop until it returns 1, then resumeds18b20_poll(). Commands are ignored mid-cycle, while a device search, an alarm search or a resolution change owns the timer, or while another command transaction is still running. Result buffers must stay valid until the transaction finishes. - With a device selected via
ds18b20_select(), the command is preceded by Match ROM (0x55) + device ROM so only that device responds; without a selection it broadcasts via Skip ROM (0xCC).ds18b20_read_rom()is always sent bare (0x33) — valid only when exactly one device is on the bus. ds18b20_read_scratchpad()returns the raw 9 scratchpad bytes; verify withds18b20_last_command_ok()or the CRC byte (buf[8] == ds18b20_crc8(buf, 8)). A valid read also updates the auto-derived resolution (ds18b20_get_resolution()).ds18b20_set_alarm_thresholds(th, tl)writes TH/TL into the volatile scratchpad with Write Scratchpad (0x4E), keeping the current resolution in the config byte. The DS18B20 8-bit sign-extended temperature code is used (e.g. 0x19 = +25°C, 0x0F = +15°C).ds18b20_copy_scratchpad()persists TH/TL/CFG to the EEPROM andds18b20_recall_eeprom()loads the EEPROM copy back into the scratchpad. Both wait the datasheet hold-off (10 ms) with the timer before finishing. Recall is a write-only command: it does not return the restored config, so the driver's trackedctx.resolutionis not updated. If the EEPROM resolution may differ from the current one, followds18b20_recall_eeprom()withds18b20_read_scratchpad()to resynchroniseds18b20_get_resolution()before the next conversion (seeexamples/5_commands/main.c, which chains recall → scratchpad read for this reason).ds18b20_detect_parasite()runs a Read Power Supply query and stores the answer straight into the driver state — afterds18b20_detect_parasite_poll()returns 1 (checkds18b20_last_command_ok()) the wiring is configured andds18b20_parasite_mode()reports it. On a mixed bus the open-drain answer is a wired-AND: any externally powered device masks the parasite report, so detect per-device in Match ROM addressing mode for heterogeneous wiring.ds18b20_set_parasite(1)enables parasite-power support (default 0 — external VDD wiring, no pin mode changes). The strong pull-up is harmless for externally powered devices, so a mixed bus works with the flag set — mechanism and wiring budget in Parasite Power.ds18b20_last_command_ok()reports whether the last transaction found a device present (and, for read commands, read its data back).
Example — set TH/TL and persist them to the EEPROM:
ds18b20_set_alarm_thresholds(0x19, 0x0F); // +25°C / +15°C
while (!ds18b20_set_alarm_thresholds_poll()) {
/* keep calling from the main loop */
}
ds18b20_copy_scratchpad(); // persist to the EEPROM
while (!ds18b20_copy_scratchpad_poll()) {
/* keep calling from the main loop */
}void ds18b20_scan_start(void);
uint8_t ds18b20_device_count(void);
const uint8_t* ds18b20_device_rom(uint8_t index);
uint8_t ds18b20_scan_index(void);Convert every discovered device in parallel. ds18b20_scan_start() schedules one
broadcast Convert T (Skip ROM 0xCC) so all sensors convert simultaneously, then
reads each one back via Match ROM in device-table order, reporting every result
through ds18b20_complete(). N devices take one conversion wait plus N reads
instead of N conversion waits. A missing device reports
DS18B20_TEMP_ERROR_NO_SENSOR and the scan continues. See examples/4_scan_mode/main.c.
- The device table must be populated first by the non-blocking device search
(
ds18b20_search_*). - Scan mode assumes a single resolution across all sensors (the config is written
broadcast) and is mutually exclusive with the single-device
ds18b20_select()addressing — callingds18b20_select()clears scan mode, callds18b20_scan_start()again to resume. ds18b20_device_count()returns how many DS18B20 devices are stored;ds18b20_device_rom(index)returns the 8-byte ROM (LSB first) of one of them, or NULL for an out-of-range index (the pointer is valid until the next search).ds18b20_scan_index()returns the index of the device whose resultds18b20_complete()just reported (valid during scan mode).
Example:
ds18b20_scan_start(); // begin simultaneous conversion of all sensors
while (1) {
ds18b20_poll(); // scan reports each device via ds18b20_complete()
}
// inside ds18b20_complete(): identify the sensor
void ds18b20_complete(int16_t temp) {
uint8_t idx = ds18b20_scan_index();
printf("Sensor %u: %d.%d C\n", idx, temp / 10, abs(temp % 10));
}With VDD tied to GND the DS18B20 draws its operating current from the data
line. The bus pull-up then has to deliver the conversion current — about
1.5mA per converting sensor — for the whole conversion window (up to
750ms at 12-bit), which a passive resistor cannot do. The driver solves this
by switching the bus pin to push-pull HIGH for every conversion and EEPROM
hold-off window (ds18b20_set_parasite(1)), releasing it back to the passive
pull-up before any further bus activity.
ds18b20_init();
ds18b20_detect_parasite(); // query the wiring (0xCC + Read Power Supply)
while (!ds18b20_detect_parasite_poll()) {
ds18b20_poll(); // keep advancing the state machine
}
/* ds18b20_parasite_mode() now reflects the detected wiring */Wiring guidance, from bench validation on an STM32F103 with a 2.2kΩ pull-up — deliberately stronger than the 4.7kΩ recommended for externally powered buses (see Hardware Connections), because on a parasite bus the pull-up must also source the conversion current:
- A handful of sensors on short wires (<30cm) converts reliably on the MCU pin alone: a six-device broadcast cycle completed without a single CRC error.
- Budget ~1.5mA per simultaneously converting sensor against the pin's drive capability (~25mA source on F1/F0/G0) and the VOH droop across your pull-up arrangement; keep the bus HIGH above the DS18B20's ~2.96V minimum.
- For longer buses or larger fleets add an external P-MOSFET (or a dedicated strong pull-up IC) as the high-side switch and treat the MCU pin as its gate driver — the software interface stays exactly the same.
The example applications accept a compile-time flag to run over parasite wiring out of the box:
make APP=1_basic EXT="-DOW_PARASITE_POWER=1" # or 2_device_search / 4_scan_mode / 5_commands / 6_statisticsAn optional compile-in module that collects per-sensor pulse-width statistics
and a global histogram across measurement cycles. Enabled by defining
OW_STATS_ENABLE=1 at build time. When the macro is 0, every inline
body compiles away to nothing — zero overhead in production builds.
#include "ow_stats.h"
void ow_stats_init(void);
void ow_stats_capture_pulse(const volatile uint8_t *pulse, uint8_t n,
const uint8_t *rom);
void ow_stats_count_error(int16_t error, const uint8_t *rom);
void ow_stats_dump_start(void);
uint8_t ow_stats_dump_poll(void);
void ow_stats_reset(void);
uint32_t ow_stats_tick(void);ow_stats_init()— zero-initialise the statistics context. Call once at startup.ow_stats_capture_pulse()— snapshot raw pulse widths beforedecode_scratchpad()overwrites the capture buffer via the union alias. Updates the 13-bucket logarithmic histogram (0–60+ µs; 13 of the 16OW_STATS_HIST_BUCKETSarray slots are populated, indices 0–12; buckets 0–2, 3–4, 5–6, 7–9, 10–12, 13–14, 15–19, 20–24, 25–29, 30–39, 40–49, 50–59, 60+ µs) and per-sensor min/max pulse counters. Called automatically fromds18b20.cwhenOW_STATS_ENABLE=1is set.ow_stats_count_error()— record a CRC mismatch, missing presence pulse or other error event. Called automatically fromds18b20.c.ow_stats_dump_start()— begin a non-blocking UART dump. Call from the main loop after the desired number of cycles (tracked viaow_stats_tick()).ow_stats_dump_poll()— advance the dump by one line (header, sensor line, histogram or total). Non-blocking: each call enqueues one line into the UART TX ring buffer; the main loop drains it viauart_poll_tx(). Returns 1 when the dump is complete.ow_stats_reset()— zero all counters and the histogram, keep the sensor ROM table. Call afterow_stats_dump_poll()returns 1.ow_stats_tick()— increment the cycle counter; returns the new value.
The dump also needs five weak output hooks the application provides as strong
definitions: ow_stats_putchar(), ow_stats_puts(), ow_stats_print_int(),
ow_stats_print_hex() and ow_stats_tx_enqueue(). Defaults in ow_stats.c
are no-ops, so without them the dump stays silent; examples/6_statistics
implements them on top of the UART TX ring buffer.
RAM cost: ~300 bytes (8 sensors × 28 B + 16-entry uint32_t histogram [64 B] +
cycle/error counters + 8 B dump state; 13 of the 16 histogram buckets, indices
0–12, are populated).
Example — dump after STATS_DUMP_INTERVAL full rounds (source default 100):
#include "ow_stats.h"
#ifndef STATS_DUMP_INTERVAL
#define STATS_DUMP_INTERVAL 100u
#endif
static uint8_t dump_busy = 0;
void ds18b20_complete(int16_t temp) {
// ... handle temperature reading ...
uint32_t cycles = ow_stats_tick();
if (cycles >= STATS_DUMP_INTERVAL && !dump_busy) {
ow_stats_dump_start();
dump_busy = 1;
}
}
int main(void) {
ow_stats_init();
// ... ds18b20_init(), device search ...
for (;;) {
if (dump_busy) {
if (ow_stats_dump_poll()) {
dump_busy = 0;
ow_stats_reset();
}
} else {
ds18b20_poll();
}
}
}Build with the statistics module:
make APP=6_statistics # external power (OW_STATS_ENABLE auto-added)
make APP=6_statistics EXT="-DOW_PARASITE_POWER=1" # parasite powerNote: the
6_statisticstarget already injects-DOW_STATS_ENABLE=1plus-DSTATS_DUMP_INTERVAL=5000 -DDS18B20_CYCLE_PAUSE_US=10000, so the shipped 6_statistics dumps every 5000 cycles with a 10 ms inter-cycle pause. Override either macro viaEXT=if you want the module defaults instead.
UART output format (compact, one sensor per line):
--- stats [100 c] ---
28 20 78 92 07 00 00 67:5-29 n17 e0
28 78 B8 AC 0B 00 00 2C:5-31 n17 e0
28 64 69 AB 0B 00 00 1F:5-29 n17 e0
28 FC AE AA 0B 00 00 F3:5-30 n17 e0
28 7E 63 AD 0B 00 00 3F:5-30 n16 e0
28 F1 39 AD 0B 00 00 D9:5-30 n16 e0
h:2=3304 8=2023 9=1873
t=100c 0e
Fields per sensor line: ROM:min-max n=count e=errors (errors = CRC + no
presence + other combined). Histogram shows only non-empty buckets;
h:B=count where B is the bucket index. Total line: t=Nc Ee where N =
cycle count, E = total error count.
void ds18b20_set_resolution(uint8_t bits);
uint8_t ds18b20_set_resolution_poll(void);
uint8_t ds18b20_get_resolution(void);Change the temperature conversion resolution between measurement cycles, non-blocking and without interrupts, mirroring the device search state machine:
bitsis the new resolution in bits —DS18B20_RES_MIN(9) …DS18B20_RES_MAX(12). Out-of-range values are ignored. The change is only accepted while the measurement state machine is IDLE and no device search is running; it is ignored otherwise.- The configuration is written to the volatile scratchpad with Write Scratchpad (0x4E) (TH/TL are reset to 0, disabling the alarm triggers) and takes effect immediately; it is not persisted to the EEPROM (Copy Scratchpad would need a strong pull-up under parasitic power).
- Poll
ds18b20_set_resolution_poll()from the main loop until it returns 1, then resumeds18b20_poll(). The next measurement waits exactly as long as the new resolution requires (e.g. 93.75ms at 9-bit instead of 750ms). ds18b20_get_resolution()returns the current resolution. It is updated by a successful resolution change and auto-derived from every valid scratchpad read (byte 4, R1/R0), so it also tracks a resolution changed externally.
Example — drop to 9-bit to measure 8× faster:
ds18b20_set_resolution(9);
while (!ds18b20_set_resolution_poll()) {
/* keep calling from the main loop; never blocks */
}
/* ds18b20_get_resolution() == 9; ds18b20_poll() resumes with the fast wait */void ds18b20_busy(unsigned action);Called to indicate busy/idle status — toggle an LED, for example. action is non-zero for busy (measurement in progress), 0 for idle.
void ds18b20_complete(int16_t temp);Called when a measurement cycle completes — provides temperature data in tenths of degrees Celsius, or an error code (DS18B20_TEMP_ERROR_*).
DS18B20_TEMP_ERROR_NO_SENSOR: No sensor detected on the bus.DS18B20_TEMP_ERROR_CRC_FAIL: Data corruption detected via CRC mismatch.DS18B20_TEMP_ERROR_GENERIC: Unspecified communication error.
- Time to result (one measurement): 93.75ms @ 9-bit … ~0.76 s @ 12-bit
(conversion + protocol overhead; the conversion wait follows the configured
resolution, see
ds18b20_set_resolution()) - Inter-measurement pause: 5 s, configurable via
DS18B20_CYCLE_PAUSE_US(default 5000000 µs; the 6_statistics build overrides it to 10000 µs) - Precision: 0.1°C reported (API tenths; the sensor step at 12-bit is 0.0625°C — coarser steps at lower resolutions)
- Accuracy: ±0.5°C (typical)
- CPU Usage: Minimal; CPU is free to perform other tasks during waits.
Slot timing is fixed at compile time via the ONEWIRE_* macros in
inc/ow_config.h (see also the API Reference → Timing). Each macro is
overridable with -D, so a specific board or bus length pins its values without
any runtime state. The reset-pulse bounds are defined in src/onewire.c:
/* inc/ow_config.h — defaults */
#define ONEWIRE_ONE_PULSE 5 // µs (short low = write-1)
#define ONEWIRE_ZERO_PULSE 60 // µs (long low = write-0)
#define ONEWIRE_GUARD_BAND 5 // µs (built into slot formula)
#define ONEWIRE_SHORT_PULSE_MAX 10 // µs (pulse <= this reads as bit '1')
/* src/onewire.c */
#define RESET_PULSE_MIN 480U // µs
#define RESET_PULSE_MAX 540U // µsSlot formula:
ARR = one_pulse + zero_pulse + guard_band
The Makefile presets set all four values at once (make TIMING=SLOW):
| Preset | one |
zero |
guard |
short≤ |
Slot |
|---|---|---|---|---|---|
| FAST | 5µs | 60µs | 3µs | 10µs | 68µs |
| STANDARD | 5µs | 60µs | 5µs | 10µs | 70µs |
| SLOW | 8µs | 90µs | 20µs | 15µs | 118µs |
| ROBUST | 10µs | 110µs | 30µs | 18µs | 150µs |
| CUSTOM | 1µs | 60µs | 1µs | 15µs | 62µs |
SLOW / ROBUST trade conversion throughput for timing margin and are intended for long wiring, parasite buses or electrically noisy setups.
On a parasite-powered bus the strong-pullup release must not clip the sensor's
slot sampling, so the guard band needs to be wider than the 5µs external-power
default. Compile with -DOW_PARASITE_POWER=1: when ONEWIRE_GUARD_BAND is not
defined by a preset or an explicit -D, it defaults to 100µs. Presets always
pin the guard explicitly, so on long or noisy parasite wiring pass the value
directly instead — e.g. make TIMING=ROBUST EXT="-DONEWIRE_GUARD_BAND=250".
CUSTOM uses the minimum slot timing allowed by the 1-Wire standard
(t_LOW1 = 1µs, t_LOW0 = 60µs, t_REC = 1µs). It is an experimental setting:
a 1µs read/write pulse is below the values validated on hardware (a 2µs
pulse already broke slot decoding on an F030 at 8MHz — see also the note in
inc/ow_config.h). Use it only for experiments or electrically ideal setups.
-
"No sensor detected" or "CRC check failed" errors
- Cause: The most common cause is electrical. The presence pulse captured by the DMA/timer did not meet the timing criteria, or noise corrupted the data during the 72-bit read.
- Fix:
- Check all wiring connections.
- Ensure a 4.7kΩ pull-up resistor is between the 1-Wire data line (PA10; logical PA10 via PA12 remap on G0) and 3.3V.
- Verify stable power is supplied to the DS18B20 sensor.
- Keep data lines short to minimize noise and capacitance.
-
Temperature readings are infrequent
- Cause: The ds18b20_poll() function is called slowly from the main loop. The driver operates correctly but advances through its states (e.g., the 750ms conversion wait) at a slower pace.
- Fix: This is often not a problem if a slow update rate is acceptable. If faster updates are needed, ensure the main loop runs frequently and avoids other blocking code. The driver itself is non-blocking and will not cause this slowdown.
- Use Debug Build: The release build (
-Os -flto -g0) aggressively optimizes the driver, which may inline or eliminate static variables likectx. Usemake debug(-Og -g3 -gdwarf) for debugging. - VSCode: Press F5 to build (debug) and launch a J-Link debug session. The SVD file provides peripheral register views.
- Monitor the State Variable: Check
ctx.current_statein a debugger to see the current step in the communication sequence. - Check the Update Flag: Read
TIM1->SR. If the driver seems idle, a set UIF bit indicates a completed operation waiting to be processed byds18b20_poll(). - Inspect the GPIO: Use an oscilloscope on the data pin (PA10) to verify the 1-Wire
waveforms. Look for:
- A clean ~480µs reset pulse (MCU pulls low, then releases).
- A presence pulse ~60-240µs after the reset pulse (sensor pulls low).
- Precise write slots: a short ~5µs low for a '1', a long ~60µs low for a '0' (slot = 5 + 60 + 5 = 70µs).
- Inspect Captured Data: Examine the driver context's
ctx.capture[]after a reset orctx.pulse[]after a read (insrc/ds18b20.c) to see the raw timing data.
This project is released under the MIT License. See the LICENSE file for details.
See CONTRIBUTING.md for the full guide (bug reports via the issue templates, feature requests, Code of Conduct, PR workflow). Quick version:
- Fork and create a feature branch (
git checkout -b fix/my-change). - Make your changes and keep the host tests green:
make test, plusmake test-f0/make test-g0for the other backends. - Format touched sources with the repo's
.clang-format(clang-format --dry-run --Werror <files>must pass). - Open a Pull Request — CI builds every target and runs the test suite.
For issues and questions, please open an issue on GitHub.
-
DS18B20 Datasheet
https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf -
STM32F103 Reference Manual
https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf -
STM32F030 Reference Manual
https://www.st.com/resource/en/reference_manual/rm0091-stm32f0x1stm32f0x2stm32f0x8-advanced-armbased-32bit-mcus-stmicroelectronics.pdf -
STM32G031 Reference Manual
https://www.st.com/resource/en/reference_manual/rm0444-stm32g0x1stm32g0x2-advanced-armbased-32bit-mcus-stmicroelectronics.pdf -
1-Wire Protocol Specification
https://www.analog.com/en/resources/technical-articles/1wire-communication.html


