1-Wire Protocol: Temperature Sensor Networks with DS18B20

Building a temperature monitoring system with a handful of wires often pushes beginners toward analog sensors or I2C parts, but the moment you need several points of measurement spread across a cabinet, a greenhouse, or along a length of pipe, the 1-Wire bus and the DS18B20 start to make a lot of sense. I have deployed DS18B20 networks in everything from remote cold-chain loggers to hydronic heating controllers, and the appeal has always been the same: one data line plus ground, each sensor with a unique factory-programmed ID, and digital temperature data with built-in error checking. This article walks through how the protocol actually works at the signal level, how to wire and power the bus reliably, and how to write firmware that handles multiple sensors without locking up your microcontroller.

Why 1-Wire Still Earns Its Place on a Single GPIO

1-Wire is exactly what it sounds like: a half-duplex, open-drain bus that uses a single data line for bidirectional communication and a shared ground as reference. Every device on the bus is a slave except for the microcontroller, which acts as the bus master. Unlike I2C, there is no separate clock line. Timing is derived from precise delays generated by the master, which makes the protocol sensitive to interrupts and clock accuracy but extremely wire-efficient.

In my experience, the decision to use 1-Wire usually comes down to wiring cost and topology, not raw speed. At its standard speed, a full temperature conversion and read takes 750 ms at 12-bit resolution, which is slow compared to the burst reads you get from an SPI temperature sensor. If you need high-speed sampling for a control loop, SPI Communication: Full-Duplex Data Transfer for High-Speed Sensors will serve you better. And if you need a bus with hardware arbitration and multiple masters, 1-Wire is the wrong choice entirely — that is where you should be looking at I2C Protocol Tutorial: Addressing, Clock Stretching and Multi-Master. Where 1-Wire wins is distributed sensing. You can run a single twisted pair across several meters and tap in DS18B20s wherever you need them, with no address jumpers or chip-select lines.

The bus is open-drain, which means every device can only pull the line low. The idle state is held high by a single pull-up resistor. That resistor is not optional. Most failed 1-Wire projects I have debugged were traced to a weak or missing pull-up, or to using a push-pull GPIO mode that fought with slaves trying to pull low.

Anatomy of the DS18B20: ROM, Scratchpad and That 64-Bit Address

Each DS18B20 contains a 64-bit ROM code that serves as its unique address on the bus. The code is structured as an 8-bit family code (0x28 for the DS18B20), followed by a 48-bit serial number, and an 8-bit CRC. You never have to set this address yourself, which eliminates address conflicts but requires you to discover addresses in firmware if you have more than one sensor.

Beyond the ROM, the sensor exposes a 9-byte scratchpad RAM that holds the temperature and configuration data:

Scratchpad Layout You Will Actually Use

Bytes 0 and 1 contain the temperature register in little-endian format. Byte 2 and 3 hold the TH and TL alarm registers (rarely used now that we do thresholding in software). Byte 4 is the configuration register that sets resolution from 9 to 12 bits. Bytes 5 through 7 are reserved, and byte 8 is the CRC of the first eight bytes. I have found that validating this CRC on every read eliminates nearly all of the phantom 85°C and -127°C readings that plague beginner code.

The configuration register deserves attention. At 12-bit resolution you get 0.0625°C increments with a 750 ms conversion time. At 9-bit you get 0.5°C steps in just 93.75 ms. For room monitoring, 12-bit is fine. For a thermal protection loop where you need faster feedback, dropping to 10 or 11 bits is a practical trade-off. The resolution setting is stored in EEPROM, so you only need to write it once unless you intentionally change it.

Power Modes and the EEPROM Caveat

The DS18B20 can run from an external VDD pin (3.0V to 5.5V) or from parasitic power harvested from the data line itself. During a temperature conversion the sensor can draw up to 1.5 mA, which must be supplied through the pull-up if you are in parasitic mode. More on that in the wiring section, but the key takeaway here is that parasitic power forces you to handle bus power delivery carefully in software.

Wiring the Bus: Pull-Ups, Parasitic Power and Cable Length Realities

A reliable 1-Wire bus starts with a solid physical layer. The official datasheet recommends a 4.7kΩ pull-up to VCC on the data line for short runs (under 3-5 meters with a few sensors). In my lab, that value works perfectly on a breadboard with 3.3V logic. When you move to longer cables, however, capacitance piles up. I've measured 50-100 pF per meter on standard CAT5, and that capacitance slows down the rising edge because the pull-up has to charge it.

For runs over 10 meters or networks with more than five sensors, I typically drop the pull-up to 2.2kΩ or even 1.5kΩ to maintain sharp edges. Do not go much lower than 1kΩ at 3.3V, or you will exceed the sink capability of the slaves when they pull low. Use a twisted pair — one conductor for DQ, one for GND — and keep VCC alongside if you are not using parasitic power. Avoid star topologies with long stubs if you can; a single daisy-chain or linear trunk with short 15cm drops is far more robust than a star that rings on every edge.

Parasitic power seems attractive because it saves a wire, but I have stopped using it for anything beyond a single sensor on a PCB. The problem is the period during conversion when the master must provide a strong pull-up. That requires either a MOSFET that actively pulls the line high right after issuing the Convert T command, or holding the line high through a low-value resistor while preventing other bus activity. If your firmware crashes or the bus is held low, the sensor can brown out and return corrupted data. If you have the extra wire, run external power and tie the VDD pin to your 3.3V rail with a 100 nF decoupling capacitor near each sensor. Your CRC error rate will drop noticeably, especially at elevated temperatures.

ESD and protection matter in industrial settings. A series 100Ω resistor on the DQ line close to the master, plus a 5.1V Zener or TVS diode to ground, protects the GPIO from field wiring faults. I add that on every board that leaves the enclosure.

Mastering the Time Slots: Reset, Presence Pulse and Bit-Level Signaling

1-Wire communication is built from four primitives: reset/presence, write 0, write 1, and read bit. All timing is initiated by the master. The slave only responds within narrow windows, so disabling interrupts during each time slot is essential. I've found that even a single UART interrupt during a write slot can corrupt a transaction on an 8-bit AVR running at 16 MHz.

A reset pulse holds the bus low for 480 µs minimum (I use 480-500 µs), then releases it. After 15-60 µs, any slaves on the bus will hold the line low for 60-240 µs to signal presence. If you do not see a presence pulse, stop — there is no point continuing to send ROM commands.

Write and read slots are 60-120 µs long. To write a 0, the master pulls low for at least 60 µs. To write a 1, it pulls low for 1-15 µs and then releases. To read a bit, the master pulls low for 1-15 µs, releases, and samples the line after about 15 µs. The slave will keep the line low if it wants to transmit a 0, or let the pull-up return high for a 1.

The entire protocol stack is built by sequencing these slots into bytes (LSB first). You will use the ROM commands: 0x33 Read ROM, 0x55 Match ROM, 0xCC Skip ROM, 0xEC Alarm Search, and the function commands: 0x44 Convert T, 0xBE Read Scratchpad, 0x4E Write Scratchpad. For a single-sensor bus, Skip ROM is a huge simplification. For multiple sensors, you must use Match ROM or the Search ROM procedure.

// Bit-banged 1-Wire primitives - STM32 HAL example, 3.3V, 72MHz
// Timing based on standard speed, adjust delay_us for your core clock
#define OW_PIN GPIO_PIN_5
#define OW_PORT GPIOA

void ow_set_low(void) {
  GPIO_InitTypeDef g = {0};
  g.Pin = OW_PIN; g.Mode = GPIO_MODE_OUTPUT_OD; g.Pull = GPIO_NOPULL;
  g.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(OW_PORT, &g);
  HAL_GPIO_WritePin(OW_PORT, OW_PIN, GPIO_PIN_RESET);
}

void ow_release(void) {
  HAL_GPIO_WritePin(OW_PORT, OW_PIN, GPIO_PIN_SET);
  // Keep as open-drain output, line pulled high by external 4.7k resistor
}

uint8_t ow_reset(void) {
  uint8_t presence = 0;
  __disable_irq();
  ow_set_low();
  delay_us(480);
  ow_release();
  delay_us(70);
  presence = (HAL_GPIO_ReadPin(OW_PORT, OW_PIN) == GPIO_PIN_RESET);
  __enable_irq();
  delay_us(410); // complete the reset timeslot (480us low + 410us = ~960us total)
  return presence;
}

void ow_write_bit(uint8_t bit) {
  __disable_irq();
  ow_set_low();
  if (bit) {
    delay_us(6);   // pull low briefly for a '1'
    ow_release();
    delay_us(64);
  } else {
    delay_us(60);  // hold low for a '0'
    ow_release();
    delay_us(10);
  }
  __enable_irq();
}

uint8_t ow_read_bit(void) {
  uint8_t bit = 0;
  __disable_irq();
  ow_set_low();
  delay_us(6);
  ow_release();
  delay_us(9); // wait to sample window
  bit = (HAL_GPIO_ReadPin(OW_PORT, OW_PIN) == GPIO_PIN_SET) ? 1 : 0;
  __enable_irq();
  delay_us(55);
  return bit;
}

If you are using Zephyr RTOS, the timing constraints interact with thread scheduling. The Zephyr Project Documentation recommends using a dedicated high-priority thread or a kernel busy-wait for sub-100 µs bit-banging, and disabling preemption around the entire byte transaction. On FreeRTOS, I wrap the whole transaction in a critical section using taskENTER_CRITICAL() as described in the FreeRTOS Documentation to prevent a context switch mid-slot.

Driving Multiple DS18B20s: Search ROM and Simultaneous Conversion Strategies

With more than one device, you have to discover their ROM codes. The Search ROM algorithm (0xF0) is a binary tree search that walks through all 64 bits, handling conflicts where some slaves transmit 0 and others transmit 1. You resolve each conflict by choosing a branch, recording your choice, and backtracking until every device is found. It looks intimidating on paper, but once implemented you can enumerate an entire bus in a few hundred milliseconds.

For many applications you can avoid doing a search on every boot. Search once, store the ROM codes in flash or EEPROM on the master, and on subsequent boots just verify presence with Match ROM. In a production hydronic manifold I worked on, we printed a QR code with the ROM for each sensor point, so the installer could map physical location to ROM without running a discovery pass at all.

The other practical trick is simultaneous conversion. Instead of addressing each sensor individually, you can broadcast:

// Broadcast temperature conversion to all devices on the bus
if (!ow_reset()) return ERROR_NO_PRESENCE;
ow_write_byte(0xCC); // Skip ROM - address all devices
ow_write_byte(0x44); // Convert T

// For externally powered devices, you can poll for completion
// by reading a bit: 0 = busy, 1 = done
// or simply wait based on resolution: 750ms for 12-bit, 94ms for 9-bit
if (parasitic_power == 0) {
  // poll with timeout
  uint32_t start = HAL_GetTick();
  while (ow_read_bit() == 0) {
    if (HAL_GetTick() - start > 800) return ERROR_TIMEOUT;
  }
} else {
  // In parasitic mode you must provide strong pull-up within 10us after 0x44
  ow_enable_strong_pullup();
  delay_ms(750);
  ow_disable_strong_pullup();
}

// Then read each sensor by Match ROM
for (int i = 0; i < sensor_count; i++) {
  ow_reset();
  ow_write_byte(0x55); // Match ROM
  for (int b = 0; b < 8; b++) ow_write_byte(rom_codes[i][b]);
  ow_write_byte(0xBE); // Read Scratchpad
  for (int b = 0; b < 9; b++) scratchpad[i][b] = ow_read_byte();
}

Bringing the bus high with a strong pull-up during conversion is mandatory for parasitic power, but even with external power I prefer a simple wait based on resolution rather than polling, because polling can add traffic and wake-up transients on a long bus. If you need frequent, staggered readings rather than a synchronized snapshot, address each sensor individually with Match ROM -> Convert T -> wait -> Match ROM -> Read Scratchpad, and interleave the waits with other tasks. Just never start a new reset while any device is still converting, or you will abort its conversion.

From Raw Scratchpad Bytes to Celsius: CRC Checks and Reliable Firmware

The temperature register is a signed 16-bit value where the LSB represents 1/16th of a degree. For 12-bit resolution you use all bits; for lower resolutions the lower bits read as zero. A common mistake is treating the value as unsigned, which creates huge positive spikes when measuring below 0°C.

Always check the CRC before you interpret the data. The DS18B20 uses a CRC-8 with polynomial 0x31 (x^8 + x^5 + x^4 + 1, initial 0x00). If the CRC fails, discard the reading and retry. I retry up to three times before flagging the sensor as offline. This eliminates most bus noise glitches. For context on improving analog sampling integrity more broadly, see ADC Sampling Techniques: Resolution, Noise Reduction and Oversampling — the same discipline of validating raw samples before filtering applies here even though the data is already digital.

// CRC-8 check and temperature conversion
uint8_t crc8(uint8_t *data, uint8_t len) {
  uint8_t crc = 0;
  for (uint8_t i = 0; i < len; i++) {
    uint8_t in = data[i];
    for (uint8_t j = 0; j < 8; j++) {
      uint8_t mix = (crc ^ in) & 0x01;
      crc >>= 1;
      if (mix) crc ^= 0x8C; // 0x8C is reversed 0x31
      in >>= 1;
    }
  }
  return crc;
}

int8_t ds18b20_read_temp(uint8_t *scratchpad, float *temp_c) {
  if (crc8(scratchpad, 8) != scratchpad[8]) return -1; // CRC error
  int16_t raw = (int16_t)((scratchpad[1] << 8) | scratchpad[0]);
  // Mask undefined lower bits based on resolution if you want strict behavior
  uint8_t cfg = scratchpad[4] & 0x60;
  if (cfg == 0x00) raw &= ~0x07; // 9-bit
  else if (cfg == 0x20) raw &= ~0x03; // 10-bit
  else if (cfg == 0x40) raw &= ~0x01; // 11-bit
  *temp_c = raw / 16.0f;
  // Power-on reset value is 85C - discard if conversion not yet complete
  if (raw == 0x0550 && scratchpad[7] == 0x10) return -2; // suspicious 85C
  return 0;
}

Two other defensive checks I include in every driver: first, reject 85°C if it appears on the first read after power-up — that is the default register value before a conversion finishes. Second, reject a raw value of 0xFFF8 to 0xFFFF extremes if they persist, which often indicates a short or disconnected bus. For system-level reliability, pair the CRC validation with a median filter over the last three valid samples rather than a simple average; it rejects single-point outliers without adding much latency.

Resolution configuration is an EEPROM write, so limit writes. Do not put a configuration write in your main loop. Set it once during commissioning and read it back to confirm. Writes to EEPROM take up to 10 ms and require a strong pull-up in parasitic mode as well.

Configuration Wiring Max Bus Length (typical) Conversion Handling When I Choose It
External Power, 4.7k pull-up DQ + GND + VDD (3 wires) 50-100 m on CAT5 with 5-10 sensors Simple wait or polling, no strong pull-up needed Default for all multi-sensor or long-run installations
External Power, 2.2k pull-up DQ + GND + VDD (3 wires) Up to ~80 m with lower capacitance margin Same as above, sharper edges Long cables or 10+ sensors where rise time suffers
Parasitic Power, 4.7k + MOSFET strong pull-up DQ + GND (2 wires) 5-10 m with 1-3 sensors Master must drive strong pull-up within 10 µs after 0x44 / EEPROM writes Space-constrained PCB or sealed probe where one wire is saved
Parasitic Power, no strong pull-up DQ + GND (2 wires) Unreliable beyond 1 m Conversions corrupt, intermittent CRC failures Never deployed - included only to illustrate failure mode

Putting the Network Into Production: Polling cadence, Errors and Field Debugging

A DS18B20 does not need to be polled fast. Even at 12-bit, reading more than once per second per sensor wastes power and bus time. For environmental monitoring I settle on 5 to 30 seconds per point and synchronize reads so the bus is idle most of the time. That idle time is useful - it lets you run other protocols on the same microcontroller without contention.

Field debugging starts with an oscilloscope, not printf. A healthy bus idles near VCC, pulls cleanly to near 0V during slots, and rises back within about 5 µs with a 4.7k pull-up and short wiring. If you see a slow ramp that takes 15-20 µs to cross the logic threshold, your capacitance is too high or your pull-up is too weak. If you see that the low level only reaches 0.8-1.0V, something is driving against the bus or your GPIO is not in open-drain mode.

Log errors by type: no presence pulse, CRC mismatch, timeout waiting for conversion, and 85°C power-on value. In my loggers, a CRC error rate above 1-2% consistently predicts a wiring issue that will eventually become a hard failure. If a single sensor starts throwing CRC errors while others on the same trunk are clean, suspect a bad crimp, moisture in a splice, or a failing sensor rather than the master.

Finally, protect your bus handling against future firmware changes. Abstract the 1-Wire primitives behind a small driver layer with functions like ds18b20_convert_all(), ds18b20_read_scratchpad(rom, buffer), and ds18b20_rom_search(). That isolation made it straightforward when I ported a driver from bare-metal AVR to an STM32 running Zephyr - only the delay and GPIO primitives changed, while the search and CRC logic stayed identical. Keeping the timing-critical sections short and with interrupts masked also kept coexistence with Wi-Fi stacks and MQTT telemetry tasks predictable, since network interrupts would otherwise stretch time slots beyond spec.

Frequently Asked Questions

Why do I keep reading 85°C from the DS18B20?

85°C (raw 0x0550) is the power-on reset value in the scratchpad. It means you read the sensor before a temperature conversion completed. Ensure you wait long enough after the 0x44 Convert T command — 750 ms at 12-bit, 94 ms at 9-bit — or poll for completion, and discard an 85°C reading if it appears on the very first read after power-up.

Can I mix parasitic and externally powered DS18B20s on the same bus?

You can, but you must still provide a strong pull-up during conversion if any device is parasitic. The safest approach is to design for external power for all devices and run the extra VDD wire. If you must mix, issue the Convert T to all devices using Skip ROM and then enable the strong pull-up for the full conversion time, even though only the parasitic devices need it.

How long can the 1-Wire cable be for DS18B20 networks?

On a linear daisy-chain with CAT5 and a 4.7kΩ pull-up at 5V, 20-50 meters with a handful of sensors is common. With a stiffer 2.2kΩ pull-up, careful topology, and external power, some installations reach 80-100 meters. Star topologies, thin wire, and parasitic power shorten that limit dramatically. If you need longer or more robust distances, consider using a dedicated 1-Wire master like the DS2482S-100 and active pull-up.

Do I need to use the 64-bit ROM if I only have one sensor?

No. With a single DS18B20 you can use Skip ROM (0xCC) to address it without knowing its ROM code, which simplifies code. However, I still recommend reading and logging the ROM during provisioning. If a second sensor is ever added, you will need the addresses, and having it stored makes replacement and tracing much easier.

Related Articles

References & Standards: FreeRTOS Documentation · Zephyr Project Documentation · MQTT Specification