rp/pio: Add onewire strong pullups, parasite power

DS18B20 sensors require a strong pullup to be applied for the duration
of the temperature conversion, within 10us of the command. The rp2xxx
pins have sufficient drive strength to use as the pullup (no external
mosfet needed).

Add a new write_bytes_pullup() that will apply the pullup after
bytes are written. Existing read_bytes()/write_bytes() has no change to
onewire timing.

A pio_onewire_parasite example reads multiple sensors individually,
applying the strong pullup.
This commit is contained in:
Matt Johnston 2025-09-14 16:30:31 +08:00
parent be794533d3
commit 8f10e3638d
4 changed files with 133 additions and 3 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add PIO SPI
- Add PIO I2S input
- Add PIO onewire parasite power strong pullup
## 0.8.0 - 2025-08-26

View File

@ -52,7 +52,8 @@ impl<'a, PIO: Instance> PioOneWireProgram<'a, PIO> {
; The low pulse was already done, we only need to delay and poll the bit in case we are reading
write_1:
nop side 0 [( 6 / CLK) - 1] ; Delay before sampling the input pin
jmp y--, continue_1 side 0 [( 6 / CLK) - 1] ; Delay before sampling input. Always decrement y
continue_1:
in pins, 1 side 0 [(48 / CLK) - 1] ; This writes the state of the pin into the ISR
; Fallthrough
@ -61,9 +62,24 @@ impl<'a, PIO: Instance> PioOneWireProgram<'a, PIO> {
.wrap_target
out x, 1 side 0 [(12 / CLK) - 1] ; Stalls if no data available in TX FIFO and OSR
jmp x--, write_1 side 1 [( 6 / CLK) - 1] ; Do the always low part of a bit, jump to write_1 if we want to write a 1 bit
in null, 1 side 1 [(54 / CLK) - 1] ; Do the remainder of the low part of a 0 bit
; This writes 0 into the ISR so that the shift count stays in sync
jmp y--, continue_0 side 1 [(48 / CLK) - 1] ; Do the remainder of the low part of a 0 bit
jmp pullup side 1 [( 6 / CLK) - 1] ; Remain low while jumping
continue_0:
in null, 1 side 1 [( 6 / CLK) - 1] ; This writes 0 into the ISR so that the shift count stays in sync
.wrap
; Assume that strong pullup commands always have MSB (the last bit) = 0,
; since the rising edge can be used to start the operation.
; That's the case for DS18B20 (44h and 48h).
pullup:
set pins, 1 side 1[( 6 / CLK) - 1] ; Drive pin high output immediately.
; Strong pullup must be within 10us of rise.
in null, 1 side 1[( 6 / CLK) - 1] ; Keep ISR in sync. Must occur after the y--.
out null, 8 side 1[( 6 / CLK) - 1] ; Wait for write_bytes_pullup() delay to complete.
; The delay is hundreds of ms, so done externally.
set pins, 0 side 0[( 6 / CLK) - 1] ; Back to open drain, pin low when driven
in null, 8 side 1[( 6 / CLK) - 1] ; Inform write_bytes_pullup() it's ready
jmp next_bit side 0[( 6 / CLK) - 1] ; Continue
"#
);
@ -98,6 +114,7 @@ impl<'d, PIO: Instance, const SM: usize> PioOneWire<'d, PIO, SM> {
let mut cfg = Config::default();
cfg.use_program(&program.prg, &[&pin]);
cfg.set_in_pins(&[&pin]);
cfg.set_set_pins(&[&pin]);
let shift_cfg = ShiftConfig {
auto_fill: true,
@ -146,6 +163,7 @@ impl<'d, PIO: Instance, const SM: usize> PioOneWire<'d, PIO, SM> {
/// Write bytes to the onewire bus
pub async fn write_bytes(&mut self, data: &[u8]) {
unsafe { self.sm.set_y(u32::MAX as u32) };
let (rx, tx) = self.sm.rx_tx();
for b in data {
tx.wait_push(*b as u32).await;
@ -155,8 +173,29 @@ impl<'d, PIO: Instance, const SM: usize> PioOneWire<'d, PIO, SM> {
}
}
/// Write bytes to the onewire bus, then apply a strong pullup
pub async fn write_bytes_pullup(&mut self, data: &[u8], pullup_time: embassy_time::Duration) {
unsafe { self.sm.set_y(data.len() as u32 * 8 - 1) };
let (rx, tx) = self.sm.rx_tx();
for b in data {
tx.wait_push(*b as u32).await;
// Empty the buffer that is being filled with every write
let _ = rx.wait_pull().await;
}
// Perform the delay, usually hundreds of ms.
embassy_time::Timer::after(pullup_time).await;
// Signal that delay has completed
tx.wait_push(0 as u32).await;
// Wait until it's back at 0 low, open drain
let _ = rx.wait_pull().await;
}
/// Read bytes from the onewire bus
pub async fn read_bytes(&mut self, data: &mut [u8]) {
unsafe { self.sm.set_y(u32::MAX as u32) };
let (rx, tx) = self.sm.rx_tx();
for b in data {
// Write all 1's so that we can read what the device responds

View File

@ -1,4 +1,5 @@
//! This example shows how you can use PIO to read one or more `DS18B20` one-wire temperature sensors.
//! This uses externally powered sensors. For parasite power, see the pio_onewire_parasite.rs example.
#![no_std]
#![no_main]

View File

@ -0,0 +1,89 @@
//! This example shows how you can use PIO to read one or more `DS18B20`
//! one-wire temperature sensors using parasite power.
//! It applies a strong pullup during conversion, see "Powering the DS18B20" in the datasheet.
//! For externally powered sensors, use the pio_onewire.rs example.
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::bind_interrupts;
use embassy_rp::peripherals::PIO0;
use embassy_rp::pio::{InterruptHandler, Pio};
use embassy_rp::pio_programs::onewire::{PioOneWire, PioOneWireProgram, PioOneWireSearch};
use embassy_time::Duration;
use heapless::Vec;
use {defmt_rtt as _, panic_probe as _};
bind_interrupts!(struct Irqs {
PIO0_IRQ_0 => InterruptHandler<PIO0>;
});
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut pio = Pio::new(p.PIO0, Irqs);
let prg = PioOneWireProgram::new(&mut pio.common);
let mut onewire = PioOneWire::new(&mut pio.common, pio.sm0, p.PIN_2, &prg);
info!("Starting onewire search");
let mut devices = Vec::<u64, 10>::new();
let mut search = PioOneWireSearch::new();
for _ in 0..10 {
if !search.is_finished() {
if let Some(address) = search.next(&mut onewire).await {
if crc8(&address.to_le_bytes()) == 0 {
info!("Found address: {:x}", address);
let _ = devices.push(address);
} else {
warn!("Found invalid address: {:x}", address);
}
}
}
}
info!("Search done, found {} devices", devices.len());
loop {
// Read all devices one by one
for device in &devices {
onewire.reset().await;
onewire.write_bytes(&[0x55]).await; // Match rom
onewire.write_bytes(&device.to_le_bytes()).await;
// 750 ms delay required for default 12-bit resolution.
onewire.write_bytes_pullup(&[0x44], Duration::from_millis(750)).await;
onewire.reset().await;
onewire.write_bytes(&[0x55]).await; // Match rom
onewire.write_bytes(&device.to_le_bytes()).await;
onewire.write_bytes(&[0xBE]).await; // Read scratchpad
let mut data = [0; 9];
onewire.read_bytes(&mut data).await;
if crc8(&data) == 0 {
let temp = ((data[1] as u32) << 8 | data[0] as u32) as f32 / 16.;
info!("Read device {:x}: {} deg C", device, temp);
} else {
warn!("Reading device {:x} failed. {:02x}", device, data);
}
}
}
}
fn crc8(data: &[u8]) -> u8 {
let mut crc = 0;
for b in data {
let mut data_byte = *b;
for _ in 0..8 {
let temp = (crc ^ data_byte) & 0x01;
crc >>= 1;
if temp != 0 {
crc ^= 0x8C;
}
data_byte >>= 1;
}
}
crc
}