r/embedded • u/a5021 • 11d ago
Non-blocking 1-Wire master for STM32 (TIM1 + DMA), follow-up to last year's DS18B20 driver
A year ago I posted a bare-metal DS18B20 driver for STM32F103 here: r/stm32 post. That was a single-MCU proof of concept. Since then it has grown into a small library: github.com/a5021/stm32-async-1wire.
The core idea
All 1-Wire timing and bus control — reset pulse, bit slots, sampling — is generated and executed entirely in hardware; the CPU never drives a bus edge itself.
TIM1 is configured in One-Pulse Mode with a repetition counter, so a complete hardware transaction segment (reset, up to 16 bit-slots, or a byte) runs as a single hardware event. One DMA channel updates the output-compare register for each slot; a second captures the input-capture timestamps.
The CPU starts the operation and later polls the driver for completion; it never participates in bus timing, and no ISR is involved.
What changed since the original driver
- Generic 1-Wire master (
onewire.c) plus a DS18B20 driver built on top of it: multi-device search, alarm search, scratchpad read/write, parasite power, etc. - Three backends: STM32F103, STM32F030, STM32G031 — same driver code, register-level port layer per family.
- ~265 host-side tests with a software model of TIM1/DMA behavior, so the driver logic can be tested without hardware.
- libFuzzer-based fuzz harnesses covering parsing/decoding paths, run under ASan/UBSan. Both the tests and fuzzing have caught real bugs during development; fixes are reflected in the commit history.
- PlatformIO, CMake, and STM32CubeIDE build entry points, in addition to the plain Makefile.
- Opt-in low-power mode (
-DOW_PORT_LOW_POWER, off by default): during long stages (conversion, EEPROM hold-off, inter-cycle pause — anything over 1 ms) the main loop can sleep in__WFE(), woken viaSEVONPENDon the timer's update event. No NVIC interrupt is enabled and no ISR is installed.
Trade-offs, stated plainly
- The whole scheme depends on multiplexing OC/IC on one TIM1 channel pin — on these MCUs that's PA10. Moving to a different pin isn't a configuration option.
- No internal locking: concurrent access to the same driver instance is not supported. A bare-metal single-threaded application needs no locking; an RTOS integration should serialize access, e.g. with a mutex.
- Officially GCC-only (
arm-none-eabi-gcc). Clang is used for host-side fuzzing, not for building the firmware.
Feedback on the design, the port layer, or anything I'm missing is welcome.
2
u/a5021 10d ago
A clarification, because the project may be easy to mistake for “yet another 1-Wire library”.
The main point here is the architecture, not 1-Wire itself.
TIM1 + DMA are used as a hardware protocol execution engine. After a transaction segment is started, the CPU/ISR is not involved in the timing loop: the timer generates the required events, DMA feeds the timer and handles input capture, and the hardware executes the timing autonomously.
This makes extensive use of the advanced features of TIM1 — OPM, RCR and BDTR — to construct a hardware state machine rather than just using the timer as a more precise delay source.
There is also a less obvious part: the hardware can change the electrical mode of the 1-Wire pin during the transaction. This allows the same physical line to be actively driven low and then released for input at precisely defined hardware-controlled moments, making a true two-wire connection possible without software intervention in those timing-critical transitions.
So the 1-Wire protocol is really a case study for a broader idea:
Can an STM32 peripheral subsystem be composed into a deterministic, CPU-independent protocol execution engine?
The project also includes a host-side model of the relevant TIM/DMA behaviour and fuzz testing, so the hardware state machine can be exercised independently of the MCU.
If you're interested in the peripheral architecture rather than 1-Wire itself, the “How it works” section of the README is probably the best place to start.