Recently I had to implement SDI-12 communication on a multi-parameter sensor. The PCB had an extension port on the PCB, exposing UART1 RX&TX, but also 2 GPIO’s. While I considered using the UART first, detecting the break was a bit problematic. So a different solution with bitbanging on one of the GPIO’s was implemented. Timing issues were critical and were solved using one of the AVR timers (Timer1).
The SDI-12 implementation on uRADMonitor CITY is entirely software-defined, requiring no additional hardware beyond the direct wire connection. On the ATmega1284P microcontroller running at 14.7456 MHz, the firmware uses a pin-change interrupt on GPIO1 to detect every signal transition, with Timer1 providing precise bit timing at 1200 baud.
One of the trickier aspects of the implementation was getting the bit timing right in a way that would work reliably across different dataloggers, not just the specific USB-to-SDI-12 adapter used during development.
The first approach
The first approach attempted to auto-calibrate the bit period by measuring the duration of the BREAK signal , since BREAK is a known minimum of 12ms at 1200 baud, dividing its measured length by the expected number of bits should yield the correct tick count per bit. In practice this failed consistently, because the BREAK measurement loop and the data capture loop had different execution speeds: the compiler generated subtly different code for each, introducing a systematic offset that made the calibrated value wrong for the very data it was supposed to decode. Hardcoding a constant that was empirically tuned for the development adapter worked, but left the firmware fragile on different hardware. So I was able to get it to work, but the chances were it wouldn’t work on anything else other than my test setup.
The solution came from switching to Timer1 as a hardware time reference, combined with a bit-reconstruction algorithm: instead of counting loop iterations, the interrupt fires on every pin transition and reads the free-running 16-bit timer, computing elapsed time by simple subtraction. This approach is immune to loop speed variations, compiler optimizations, and interrupt overhead and because it measures real elapsed time rather than iteration counts, it works correctly with any datalogger that respects the 1200 baud SDI-12 standard.
The solution
The receiver does a bit-reconstruction algorithm: rather than sampling at fixed intervals, it measures the elapsed time between transitions and fills in the corresponding number of Mark or Space bits , a technique that is robust to slight timing variations from different dataloggers. Transmission is handled by direct bit-banging with cycle-accurate delays, producing a clean 7E1 signal. The entire stack , timer driver, SDI-12 primitives, and application command handler is organized into three clean layers, making it straightforward to port to other AVR targets by changing six pin macros in the header file.
