Initial commit: RP2040 RS485 temperature/humidity node
RTIC 2 firmware for an RP2040 acting as an RS485 slave, reading a Sensirion SHT31 over I2C and answering a custom binary protocol. Split into two crates so the protocol is testable without hardware: - wiredsensor-core: framing, CRC-16/MODBUS, command dispatch and SHT3x data-sheet math. Pure computation, no I/O, no peripherals. Covered by 42 host tests including the CRC-16/MODBUS catalogue vector, the SHT3x data-sheet CRC-8 example, and an exhaustive single-bit-corruption sweep over every byte of a frame. - wiredsensor-fw: the RTIC application, PL011 register driver and SHT31 I2C driver. Requests are answered entirely from a cached reading. An SHT31 high-repeatability conversion takes up to 15 ms, well past the turnaround a master expects, so the sensor is polled at the lowest priority and the bus path never touches I2C. RTIC's priority ceilings make "a conversion cannot delay a reply" a checked property rather than an argument. Two PL011 details drive the low-level approach, since rp2040-hal exposes neither: the receive-timeout interrupt for prompt frame delimiting, and the BUSY flag, which is the only way to know the final stop bit has left the shift register before releasing DE. The HAL performs the fiddly baud-divisor setup once, then the raw peripheral is reclaimed via free(). Note that RTIM cannot be the sole frame delimiter: it only fires while the RX FIFO is non-empty, so a frame landing exactly on the FIFO watermark is drained by the watermark interrupt and never times out. The authoritative delimiter is a monotonic timer measured from the last received byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "wiredsensor-fw"
|
||||
description = "RP2040 firmware for an RS485 temperature/humidity slave with an SHT31"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "wiredsensor-fw"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
wiredsensor-core = { workspace = true, features = ["defmt"] }
|
||||
|
||||
# Concurrency framework. `thumbv6-backend` selects the Cortex-M0+ critical
|
||||
# section and priority implementation.
|
||||
rtic = { version = "2.3", features = ["thumbv6-backend"] }
|
||||
rtic-monotonics = { version = "2.2", features = ["rp2040"] }
|
||||
|
||||
rp2040-hal = { version = "0.12", features = ["rt", "critical-section-impl"] }
|
||||
rp2040-boot2 = "0.3"
|
||||
|
||||
cortex-m = "0.7"
|
||||
cortex-m-rt = "0.7"
|
||||
|
||||
# Cortex-M0+ has no atomic compare-exchange instruction, so the polyfill has to
|
||||
# be told to emulate one with a critical section. RTIC's executor needs CAS, and
|
||||
# without this the build fails with a bare "requires atomic CAS" error.
|
||||
portable-atomic = { version = "1", features = ["critical-section"] }
|
||||
|
||||
embedded-hal = "1.0"
|
||||
fugit = "0.3"
|
||||
|
||||
defmt = { workspace = true }
|
||||
defmt-rtt = "1.0"
|
||||
panic-probe = { version = "1.0", features = ["print-defmt"] }
|
||||
@@ -0,0 +1,19 @@
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
// Copy memory.x somewhere the linker will find it. Relying on the linker's
|
||||
// working directory does not work in a workspace, where cargo runs rustc
|
||||
// from the workspace root rather than this package's directory.
|
||||
let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
|
||||
fs::copy("memory.x", out.join("memory.x")).expect("copy memory.x into OUT_DIR");
|
||||
println!("cargo:rustc-link-search={}", out.display());
|
||||
|
||||
// --nmagic disables page alignment of sections; without it the vector table
|
||||
// is pushed away from the start of flash and the boot ROM cannot find it.
|
||||
println!("cargo:rustc-link-arg-bins=--nmagic");
|
||||
println!("cargo:rustc-link-arg-bins=-Tlink.x");
|
||||
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
|
||||
|
||||
println!("cargo:rerun-if-changed=memory.x");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* RP2040 with 2 MiB of QSPI flash.
|
||||
*
|
||||
* The boot ROM reads the first 256 bytes of flash into RAM and executes them to
|
||||
* bring up XIP, so .boot2 must sit at the very start and the vector table must
|
||||
* follow immediately after it. */
|
||||
MEMORY {
|
||||
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
|
||||
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
|
||||
|
||||
/* The four 64 KiB SRAM banks are striped, so treat them as one region. */
|
||||
RAM : ORIGIN = 0x20000000, LENGTH = 256K
|
||||
}
|
||||
|
||||
EXTERN(BOOT2_FIRMWARE)
|
||||
|
||||
SECTIONS {
|
||||
.boot2 ORIGIN(BOOT2) :
|
||||
{
|
||||
KEEP(*(.boot2));
|
||||
} > BOOT2
|
||||
} INSERT BEFORE .text;
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Board definition: pin assignment, bus parameters and per-unit identity.
|
||||
//!
|
||||
//! # Pinout
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────┬───────────┬─────────────────────────────────────────────────────┐
|
||||
//! │ GPIO │ Function │ Connection │
|
||||
//! ├────────┼───────────┼─────────────────────────────────────────────────────┤
|
||||
//! │ 0 │ UART0 TX │ transceiver DI │
|
||||
//! │ 1 │ UART0 RX │ transceiver RO │
|
||||
//! │ 2 │ SIO out │ transceiver DE and /RE, tied together │
|
||||
//! │ 14 │ I2C1 SDA │ SHT31 SDA │
|
||||
//! │ 15 │ I2C1 SCL │ SHT31 SCL │
|
||||
//! │ 25 │ SIO out │ status LED, active high │
|
||||
//! │ SWD │ – │ SWCLK/SWDIO on the dedicated pads, for probe-rs │
|
||||
//! └────────┴───────────┴─────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! Rationale for the layout, in case it needs revisiting during PCB work:
|
||||
//!
|
||||
//! - **GPIO 0/1/2 as one block.** The three RS485 signals sit adjacent at the
|
||||
//! corner of the package, so the transceiver can be placed right next to it
|
||||
//! with short, matched traces and no crossings. Keeping DE next to TX/RX also
|
||||
//! keeps the fast switching edges away from the sensor.
|
||||
//! - **UART0 rather than UART1.** Both are identical PL011 blocks; UART0 is the
|
||||
//! conventional choice and leaves UART1 free for a debug header if wanted.
|
||||
//! - **DE and /RE tied together on one pin.** /RE is active low and DE active
|
||||
//! high, so a single line puts the transceiver in transmit when high and
|
||||
//! receive when low. Costs one GPIO instead of two and makes it impossible to
|
||||
//! end up driving and listening at once. The consequence is that we cannot
|
||||
//! hear our own transmission, which is exactly what we want — see
|
||||
//! [`crate::rs485::Rs485::tx_disable`] for the one wrinkle it causes.
|
||||
//! - **I2C1 on GPIO 14/15, physically distant from the RS485 block.** The
|
||||
//! sensor is the noise-sensitive part; the differential driver is the noisy
|
||||
//! part. Using I2C1 leaves I2C0 free.
|
||||
//! - **GPIO 25 for the LED** matches the Raspberry Pi Pico, so this firmware
|
||||
//! blinks on a bare Pico too, which is convenient for bring-up.
|
||||
//!
|
||||
//! # Hardware notes not expressible in firmware
|
||||
//!
|
||||
//! - Fit the 120 Ω termination only at the two physical ends of the segment, and
|
||||
//! fail-safe bias resistors at one point on the segment.
|
||||
//! - The SHT31 ADDR pin selects the I2C address: low for `0x44`, high for
|
||||
//! `0x45`. Strap it deliberately rather than leaving it floating, and keep
|
||||
//! [`SENSOR_I2C_ADDR`] in agreement.
|
||||
//! - Fit 4.7 kΩ external I2C pull-ups. The internal pulls enabled below are
|
||||
//! roughly 50 kΩ and are a fallback, not a substitute.
|
||||
//! - The SHT31 nRESET pin can be left pulled high; the firmware uses the soft
|
||||
//! reset command instead.
|
||||
|
||||
use rp2040_hal::{
|
||||
gpio::{
|
||||
bank0::{Gpio0, Gpio1, Gpio14, Gpio15, Gpio2, Gpio25},
|
||||
FunctionI2c, FunctionSioOutput, FunctionUart, Pin, PullDown, PullNone, PullUp,
|
||||
},
|
||||
i2c::I2C,
|
||||
pac,
|
||||
};
|
||||
use wiredsensor_core::{sht3x, timing};
|
||||
|
||||
// ---------------------------------------------------------------- identity ---
|
||||
|
||||
/// This node's RS485 unit address.
|
||||
///
|
||||
/// Compile-time by design: with the address fixed per build there is no flash
|
||||
/// wear, no commissioning protocol and no way for a bus glitch to renumber a
|
||||
/// node. The cost is one firmware image per unit, so keep this the only thing
|
||||
/// that differs between them.
|
||||
pub const UNIT_ADDRESS: u8 = 0x01;
|
||||
|
||||
/// Board serial number reported by `READ_INFO`. Distinct from the sensor's own
|
||||
/// factory serial, which is read over I2C at start-up.
|
||||
pub const DEVICE_SERIAL: u32 = 0x0000_0001;
|
||||
|
||||
/// Firmware major version.
|
||||
pub const FW_MAJOR: u8 = 0;
|
||||
/// Firmware minor version.
|
||||
pub const FW_MINOR: u8 = 1;
|
||||
/// Firmware patch version.
|
||||
pub const FW_PATCH: u8 = 0;
|
||||
|
||||
// ------------------------------------------------------------------- clocks ---
|
||||
|
||||
/// Crystal fitted to the board.
|
||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
|
||||
|
||||
// -------------------------------------------------------------- bus timing ---
|
||||
|
||||
/// RS485 line rate. 19200 8N1 is the Modbus default and a safe starting point
|
||||
/// for long segments; every timing constant below derives from it.
|
||||
pub const BAUD_RATE: u32 = 19_200;
|
||||
|
||||
/// Idle time that delimits one frame from the next, in microseconds.
|
||||
pub const INTER_FRAME_GAP_US: u32 = timing::inter_frame_gap_us(BAUD_RATE);
|
||||
|
||||
/// Time to clock out one character at [`BAUD_RATE`], in microseconds.
|
||||
pub const CHAR_TIME_US: u32 = timing::char_time_us(BAUD_RATE);
|
||||
|
||||
// ------------------------------------------------------------ sensor timing ---
|
||||
|
||||
/// I2C bus rate. The SHT31 supports 1 MHz, but 100 kHz is kinder to the long-ish
|
||||
/// traces of a sensor board and costs nothing at a 1 Hz poll rate.
|
||||
pub const I2C_FREQ_HZ: u32 = 100_000;
|
||||
|
||||
/// SHT31 address, matching how the ADDR pin is strapped.
|
||||
pub const SENSOR_I2C_ADDR: u8 = sht3x::I2C_ADDR_DEFAULT;
|
||||
|
||||
/// How often to take a reading.
|
||||
pub const SENSOR_PERIOD_MS: u32 = 1_000;
|
||||
|
||||
/// A cached reading older than this is reported with the `DATA_STALE` flag. The
|
||||
/// master still gets the value and its exact age and can apply its own policy.
|
||||
pub const STALE_AFTER_MS: u32 = 3_000;
|
||||
|
||||
/// Consecutive failed polls before the node reports a hard sensor fault and
|
||||
/// starts attempting soft resets.
|
||||
pub const SENSOR_FAULT_THRESHOLD: u32 = 5;
|
||||
|
||||
/// Status LED blink half-period.
|
||||
pub const HEARTBEAT_MS: u32 = 500;
|
||||
|
||||
// -------------------------------------------------------------------- types ---
|
||||
|
||||
/// Instants from the 1 MHz timer backing the RTIC monotonic.
|
||||
pub type Instant = fugit::Instant<u64, 1, 1_000_000>;
|
||||
|
||||
/// Durations matching [`Instant`].
|
||||
pub type Duration = fugit::Duration<u64, 1, 1_000_000>;
|
||||
|
||||
/// UART0 TX. No pull: the transceiver's DI is a driven input.
|
||||
pub type Rs485TxPin = Pin<Gpio0, FunctionUart, PullNone>;
|
||||
|
||||
/// UART0 RX. Pulled up so the line reads as idle rather than floating while the
|
||||
/// transceiver's receiver is disabled during transmission.
|
||||
pub type Rs485RxPin = Pin<Gpio1, FunctionUart, PullUp>;
|
||||
|
||||
/// Driver enable, wired to DE and /RE together. Pulled down so the transceiver
|
||||
/// defaults to receive-only if the RP2040 is held in reset.
|
||||
pub type Rs485DePin = Pin<Gpio2, FunctionSioOutput, PullDown>;
|
||||
|
||||
/// The two UART pads, owned by the driver to keep them assigned.
|
||||
pub type Rs485Pins = (Rs485TxPin, Rs485RxPin);
|
||||
|
||||
/// I2C1 SDA to the SHT31.
|
||||
pub type SdaPin = Pin<Gpio14, FunctionI2c, PullUp>;
|
||||
|
||||
/// I2C1 SCL to the SHT31.
|
||||
pub type SclPin = Pin<Gpio15, FunctionI2c, PullUp>;
|
||||
|
||||
/// The configured I2C controller the sensor driver talks through.
|
||||
pub type SensorI2c = I2C<pac::I2C1, (SdaPin, SclPin)>;
|
||||
|
||||
/// Status LED.
|
||||
pub type StatusLedPin = Pin<Gpio25, FunctionSioOutput, PullDown>;
|
||||
@@ -0,0 +1,537 @@
|
||||
//! RS485 temperature and humidity node: RP2040 + SHT31.
|
||||
//!
|
||||
//! # Concurrency structure
|
||||
//!
|
||||
//! Three priority levels, chosen so the bus is never the thing that waits:
|
||||
//!
|
||||
//! | Prio | Task | Kind | Job |
|
||||
//! |------|---------------|----------|--------------------------------------------|
|
||||
//! | 3 | `uart0_irq` | hardware | Empty the 32-byte RX FIFO, timestamp bytes |
|
||||
//! | 2 | `frame_gap` | async | Detect the frame gap, answer the request |
|
||||
//! | 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
||||
//! | 1 | `heartbeat` | async | Blink the status LED |
|
||||
//!
|
||||
//! The key design decision is that **a request is answered entirely from a
|
||||
//! cached reading**. An SHT31 high-repeatability conversion takes up to 15 ms,
|
||||
//! which is far longer than the turnaround a master expects, so the sensor is
|
||||
//! polled on its own schedule at the lowest priority and `frame_gap` never
|
||||
//! touches I2C. RTIC's priority ceilings then guarantee that a conversion in
|
||||
//! progress cannot delay a reply, and the compiler checks the resulting resource
|
||||
//! sharing rather than us reasoning about it.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
mod board;
|
||||
mod rs485;
|
||||
mod sensor;
|
||||
|
||||
use defmt_rtt as _;
|
||||
use panic_probe as _;
|
||||
use rtic_monotonics::rp2040::prelude::*;
|
||||
|
||||
// A 1 MHz, 64-bit monotonic on the RP2040 TIMER peripheral. This claims
|
||||
// TIMER_IRQ_0, which is why the RTIC dispatchers below use the software-only
|
||||
// vectors instead.
|
||||
rp2040_timer_monotonic!(Mono);
|
||||
|
||||
/// Second-stage bootloader.
|
||||
///
|
||||
/// The boot ROM copies these 256 bytes into RAM and runs them to configure XIP
|
||||
/// for the attached QSPI flash before any of our code executes. `W25Q080` covers
|
||||
/// the parts fitted to the Pico and most RP2040 modules; change it if the board
|
||||
/// carries something else.
|
||||
#[link_section = ".boot2"]
|
||||
#[no_mangle]
|
||||
#[used]
|
||||
pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
|
||||
|
||||
#[rtic::app(device = rp2040_hal::pac, peripherals = true, dispatchers = [SW0_IRQ, SW1_IRQ])]
|
||||
mod app {
|
||||
use super::*;
|
||||
|
||||
use embedded_hal::digital::StatefulOutputPin;
|
||||
use fugit::RateExtU32;
|
||||
use rp2040_hal as hal;
|
||||
use rp2040_hal::Clock;
|
||||
use rtic::Mutex;
|
||||
use wiredsensor_core::{
|
||||
frame::{ParseError, MAX_FRAME},
|
||||
proto::{self, flag, Info, Measurement, Outcome, Snapshot, Status},
|
||||
sht3x,
|
||||
};
|
||||
|
||||
use crate::board::{self, Instant};
|
||||
use crate::rs485::{self, Rs485, RxBuffer, FIFO_DEPTH};
|
||||
use crate::sensor::Sht31;
|
||||
|
||||
/// A cached sensor reading and when it was taken.
|
||||
///
|
||||
/// `pub` because RTIC's generated resource proxies name it in their public
|
||||
/// signatures.
|
||||
pub struct Reading {
|
||||
temp_milli_c: i32,
|
||||
rh_milli_pct: i32,
|
||||
taken_at: Instant,
|
||||
}
|
||||
|
||||
/// Everything behind `READ_STATUS` and `READ_INFO` that is discovered at
|
||||
/// runtime rather than fixed at build time.
|
||||
#[derive(Default)]
|
||||
pub struct DeviceState {
|
||||
/// Factory serial read from the sensor at start-up, 0 if unavailable.
|
||||
sensor_serial: u32,
|
||||
/// Raw SHT31 status register from the most recent successful read.
|
||||
sensor_status: u16,
|
||||
i2c_errors: u16,
|
||||
sensor_crc_errors: u16,
|
||||
frame_errors: u16,
|
||||
crc_errors: u16,
|
||||
consecutive_failures: u32,
|
||||
sensor_ok: bool,
|
||||
ever_measured: bool,
|
||||
uart_error: bool,
|
||||
sensor_reset_seen: bool,
|
||||
}
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
/// The PL011 and the transceiver direction pin.
|
||||
bus: Rs485,
|
||||
/// Bytes received since the last inter-frame gap.
|
||||
rx: RxBuffer,
|
||||
/// Latest reading, `None` until the first successful poll.
|
||||
reading: Option<Reading>,
|
||||
/// Counters and health bits.
|
||||
state: DeviceState,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
sensor: Sht31<board::SensorI2c>,
|
||||
led: board::StatusLedPin,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(mut cx: init::Context) -> (Shared, Local) {
|
||||
let mut watchdog = hal::watchdog::Watchdog::new(cx.device.WATCHDOG);
|
||||
let clocks = hal::clocks::init_clocks_and_plls(
|
||||
board::XTAL_FREQ_HZ,
|
||||
cx.device.XOSC,
|
||||
cx.device.CLOCKS,
|
||||
cx.device.PLL_SYS,
|
||||
cx.device.PLL_USB,
|
||||
&mut cx.device.RESETS,
|
||||
&mut watchdog,
|
||||
)
|
||||
.expect("clock init failed — check XTAL_FREQ_HZ against the fitted crystal");
|
||||
|
||||
Mono::start(cx.device.TIMER, &cx.device.RESETS);
|
||||
|
||||
let sio = hal::Sio::new(cx.device.SIO);
|
||||
let pins = hal::gpio::Pins::new(
|
||||
cx.device.IO_BANK0,
|
||||
cx.device.PADS_BANK0,
|
||||
sio.gpio_bank0,
|
||||
&mut cx.device.RESETS,
|
||||
);
|
||||
|
||||
// ---- RS485 on UART0 ------------------------------------------------
|
||||
// Let the HAL do the baud-divisor and line-control setup, which is
|
||||
// fiddly and easy to get subtly wrong, then reclaim the raw peripheral:
|
||||
// the runtime path needs RTIM and BUSY, which the HAL does not expose.
|
||||
let uart_pins: board::Rs485Pins = (pins.gpio0.reconfigure(), pins.gpio1.reconfigure());
|
||||
let uart =
|
||||
hal::uart::UartPeripheral::new(cx.device.UART0, uart_pins, &mut cx.device.RESETS)
|
||||
.enable(
|
||||
hal::uart::UartConfig::new(
|
||||
board::BAUD_RATE.Hz(),
|
||||
hal::uart::DataBits::Eight,
|
||||
None,
|
||||
hal::uart::StopBits::One,
|
||||
),
|
||||
clocks.peripheral_clock.freq(),
|
||||
)
|
||||
.expect("uart baud rate not achievable from the peripheral clock");
|
||||
let (uart0, uart_pins) = uart.free();
|
||||
let bus = Rs485::new(uart0, uart_pins, pins.gpio2.reconfigure());
|
||||
|
||||
// ---- SHT31 on I2C1 --------------------------------------------------
|
||||
let i2c = hal::i2c::I2C::new_controller(
|
||||
cx.device.I2C1,
|
||||
pins.gpio14.reconfigure(),
|
||||
pins.gpio15.reconfigure(),
|
||||
board::I2C_FREQ_HZ.Hz(),
|
||||
&mut cx.device.RESETS,
|
||||
clocks.system_clock.freq(),
|
||||
);
|
||||
let sensor = Sht31::new(i2c, board::SENSOR_I2C_ADDR);
|
||||
|
||||
let led = pins.gpio25.reconfigure();
|
||||
|
||||
defmt::info!(
|
||||
"wiredsensor v{}.{}.{} up: unit {:#04x}, {} baud, gap {} us",
|
||||
board::FW_MAJOR,
|
||||
board::FW_MINOR,
|
||||
board::FW_PATCH,
|
||||
board::UNIT_ADDRESS,
|
||||
board::BAUD_RATE,
|
||||
board::INTER_FRAME_GAP_US,
|
||||
);
|
||||
|
||||
sensor_task::spawn().ok();
|
||||
heartbeat::spawn().ok();
|
||||
|
||||
(
|
||||
Shared {
|
||||
bus,
|
||||
rx: RxBuffer::new(),
|
||||
reading: None,
|
||||
state: DeviceState::default(),
|
||||
},
|
||||
Local { sensor, led },
|
||||
)
|
||||
}
|
||||
|
||||
/// Highest-priority work in the system: get bytes out of the FIFO before it
|
||||
/// overruns and record when the last one arrived.
|
||||
///
|
||||
/// Deliberately does no parsing. At 19200 baud a character arrives every
|
||||
/// ~520 µs — around 65,000 core cycles — so there is plenty of slack, but
|
||||
/// keeping the ISR to a FIFO drain is what bounds the jitter that everything
|
||||
/// else in the system sees.
|
||||
#[task(binds = UART0_IRQ, priority = 3, shared = [bus, rx])]
|
||||
fn uart0_irq(mut cx: uart0_irq::Context) {
|
||||
let now = Mono::now();
|
||||
|
||||
let start_gap_timer = cx.shared.bus.lock(|bus| {
|
||||
cx.shared.rx.lock(|rx| {
|
||||
bus.drain_into(rx, now);
|
||||
|
||||
// One gap-detection task per frame. It re-reads `last_byte_at`
|
||||
// itself, so bytes arriving later need no further spawn.
|
||||
if rx.len > 0 && !rx.gap_pending {
|
||||
rx.gap_pending = true;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if start_gap_timer {
|
||||
frame_gap::spawn().ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait out the inter-frame gap, then answer the frame.
|
||||
///
|
||||
/// Runs above the sensor so a conversion in progress can never delay a reply,
|
||||
/// and below the UART ISO so incoming bytes are never dropped while we work.
|
||||
#[task(priority = 2, shared = [bus, rx, reading, state])]
|
||||
async fn frame_gap(mut cx: frame_gap::Context) {
|
||||
let gap = board::Duration::micros(board::INTER_FRAME_GAP_US as u64);
|
||||
|
||||
// Bytes may still be arriving. Rather than cancelling and respawning a
|
||||
// timer on every byte, re-read the last arrival time and sleep again.
|
||||
loop {
|
||||
let deadline = cx.shared.rx.lock(|rx| rx.last_byte_at) + gap;
|
||||
if Mono::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
Mono::delay_until(deadline).await;
|
||||
}
|
||||
|
||||
// Take the frame out of the shared buffer so the ISR can begin the next.
|
||||
let mut req = [0u8; MAX_FRAME];
|
||||
let (len, overflow, line_error) = cx.shared.rx.lock(|rx| {
|
||||
let len = rx.len.min(MAX_FRAME);
|
||||
req[..len].copy_from_slice(&rx.buf[..len]);
|
||||
let taken = (len, rx.overflow, rx.line_error);
|
||||
rx.reset();
|
||||
taken
|
||||
});
|
||||
|
||||
// A frame that outgrew the buffer or arrived with line errors is not
|
||||
// trustworthy. Count it and stay off the wire: answering a frame whose
|
||||
// address byte we cannot trust risks colliding with the node that was
|
||||
// actually addressed.
|
||||
if overflow || line_error {
|
||||
cx.shared.state.lock(|s| {
|
||||
s.uart_error |= line_error;
|
||||
s.frame_errors = s.frame_errors.saturating_add(1);
|
||||
});
|
||||
defmt::warn!(
|
||||
"discarded frame: {} bytes, overflow={}, line_error={}",
|
||||
len,
|
||||
overflow,
|
||||
line_error
|
||||
);
|
||||
cx.shared.rx.lock(|rx| rx.gap_pending = false);
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = snapshot(&mut cx);
|
||||
|
||||
let mut out = [0u8; MAX_FRAME];
|
||||
let reply_len = match proto::dispatch(board::UNIT_ADDRESS, &req[..len], &snapshot, &mut out)
|
||||
{
|
||||
Outcome::Reply(reply) => Some(reply.len()),
|
||||
Outcome::Silent => None,
|
||||
Outcome::Malformed(e) => {
|
||||
cx.shared.state.lock(|s| match e {
|
||||
ParseError::BadCrc => s.crc_errors = s.crc_errors.saturating_add(1),
|
||||
_ => s.frame_errors = s.frame_errors.saturating_add(1),
|
||||
});
|
||||
defmt::debug!("rejected frame of {} bytes: {}", len, e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(n) = reply_len {
|
||||
transmit(&mut cx, &out[..n]).await;
|
||||
}
|
||||
|
||||
cx.shared.rx.lock(|rx| rx.gap_pending = false);
|
||||
}
|
||||
|
||||
/// Assemble the device state `dispatch` is allowed to see.
|
||||
///
|
||||
/// Locks are taken one resource at a time: a reading and the counters need
|
||||
/// not be mutually consistent, and holding both at once would raise the
|
||||
/// ceiling for no benefit.
|
||||
fn snapshot(cx: &mut frame_gap::Context<'_>) -> Snapshot {
|
||||
let now = Mono::now();
|
||||
|
||||
let measurement = cx.shared.reading.lock(|slot| {
|
||||
slot.as_ref().map(|r| {
|
||||
let age_ms = now
|
||||
.checked_duration_since(r.taken_at)
|
||||
.map(|d| d.to_millis())
|
||||
.unwrap_or(0);
|
||||
Measurement {
|
||||
temp_milli_c: r.temp_milli_c,
|
||||
rh_milli_pct: r.rh_milli_pct,
|
||||
// Saturating at ~65 s is fine: anything that old is already
|
||||
// far past the staleness threshold.
|
||||
age_ms: age_ms.min(u16::MAX as u64) as u16,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let stale = measurement
|
||||
.map(|m| u32::from(m.age_ms) >= board::STALE_AFTER_MS)
|
||||
.unwrap_or(true);
|
||||
|
||||
let (info, status) = cx.shared.state.lock(|s| {
|
||||
let mut flags = 0u8;
|
||||
if s.sensor_ok {
|
||||
flags |= flag::SENSOR_OK;
|
||||
}
|
||||
if stale {
|
||||
flags |= flag::DATA_STALE;
|
||||
}
|
||||
if s.consecutive_failures >= board::SENSOR_FAULT_THRESHOLD {
|
||||
flags |= flag::SENSOR_FAULT;
|
||||
}
|
||||
if s.uart_error {
|
||||
flags |= flag::UART_ERROR;
|
||||
}
|
||||
if s.ever_measured {
|
||||
flags |= flag::EVER_MEASURED;
|
||||
}
|
||||
if s.sensor_reset_seen {
|
||||
flags |= flag::SENSOR_RESET_SEEN;
|
||||
}
|
||||
if s.sensor_status & sht3x::status::HEATER_ON != 0 {
|
||||
flags |= flag::HEATER_ON;
|
||||
}
|
||||
|
||||
let info = Info {
|
||||
fw_major: board::FW_MAJOR,
|
||||
fw_minor: board::FW_MINOR,
|
||||
fw_patch: board::FW_PATCH,
|
||||
proto_version: proto::PROTOCOL_VERSION,
|
||||
device_serial: board::DEVICE_SERIAL,
|
||||
sensor_serial: s.sensor_serial,
|
||||
uptime_s: (now.ticks() / 1_000_000).min(u64::from(u32::MAX)) as u32,
|
||||
};
|
||||
|
||||
let status = Status {
|
||||
flags,
|
||||
sensor_status: s.sensor_status,
|
||||
i2c_errors: s.i2c_errors,
|
||||
sensor_crc_errors: s.sensor_crc_errors,
|
||||
frame_errors: s.frame_errors,
|
||||
crc_errors: s.crc_errors,
|
||||
};
|
||||
|
||||
(info, status)
|
||||
});
|
||||
|
||||
Snapshot {
|
||||
measurement,
|
||||
info,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `reply` on the wire, holding DE for exactly as long as it takes.
|
||||
///
|
||||
/// Between FIFO refills this yields rather than spinning, so the sensor task
|
||||
/// still gets to run. That is safe because DE stays asserted throughout: we
|
||||
/// own the bus for the whole call, and nothing else in the system is allowed
|
||||
/// to touch the transceiver.
|
||||
async fn transmit(cx: &mut frame_gap::Context<'_>, reply: &[u8]) {
|
||||
cx.shared.bus.lock(|bus| bus.tx_enable());
|
||||
|
||||
let mut remaining = reply;
|
||||
while !remaining.is_empty() {
|
||||
remaining = cx.shared.bus.lock(|bus| bus.push(remaining));
|
||||
if !remaining.is_empty() {
|
||||
// FIFO full — wait for roughly half of it to drain before
|
||||
// topping up.
|
||||
Mono::delay(board::Duration::micros(
|
||||
rs485::tx_time_us(FIFO_DEPTH / 2) as u64
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything is queued; at most a full FIFO is still to go. Sleep through
|
||||
// the bulk of it, then confirm with BUSY, which is the only signal that
|
||||
// the final stop bit has actually left the shift register.
|
||||
let queued = reply.len().min(FIFO_DEPTH);
|
||||
Mono::delay(board::Duration::micros(rs485::tx_time_us(queued) as u64)).await;
|
||||
|
||||
while !cx.shared.bus.lock(|bus| bus.tx_complete()) {
|
||||
Mono::delay(board::Duration::micros(board::CHAR_TIME_US as u64)).await;
|
||||
}
|
||||
|
||||
cx.shared.bus.lock(|bus| bus.tx_disable());
|
||||
}
|
||||
|
||||
/// Sensor bring-up followed by periodic polling.
|
||||
///
|
||||
/// Lowest priority on purpose: nothing here is time-critical, and the bus
|
||||
/// must always be able to preempt it.
|
||||
#[task(priority = 1, shared = [reading, state], local = [sensor])]
|
||||
async fn sensor_task(mut cx: sensor_task::Context) {
|
||||
let sensor = cx.local.sensor;
|
||||
|
||||
Mono::delay(board::Duration::millis(u64::from(sht3x::POWER_UP_MAX_MS))).await;
|
||||
|
||||
// Clear whatever state a warm reset left behind, so the sensor's
|
||||
// configuration is known regardless of how we got here.
|
||||
let _ = sensor.soft_reset();
|
||||
Mono::delay(board::Duration::millis(u64::from(sht3x::SOFT_RESET_MAX_MS))).await;
|
||||
|
||||
match sensor.read_serial() {
|
||||
Ok(serial) => {
|
||||
defmt::info!("SHT31 serial {=u32:#010x}", serial);
|
||||
cx.shared.state.lock(|s| s.sensor_serial = serial);
|
||||
}
|
||||
Err(e) => defmt::warn!("SHT31 serial read failed: {}", e),
|
||||
}
|
||||
let _ = sensor.clear_status();
|
||||
|
||||
let period = board::Duration::millis(u64::from(board::SENSOR_PERIOD_MS));
|
||||
let mut next = Mono::now();
|
||||
|
||||
loop {
|
||||
next += period;
|
||||
|
||||
match measure(sensor).await {
|
||||
Ok(raw) => {
|
||||
let taken_at = Mono::now();
|
||||
let temp_milli_c = sht3x::temp_milli_c(raw.temp_ticks);
|
||||
let rh_milli_pct = sht3x::rh_milli_pct(raw.rh_ticks);
|
||||
|
||||
defmt::debug!("{} m°C, {} m%RH", temp_milli_c, rh_milli_pct);
|
||||
|
||||
cx.shared.reading.lock(|slot| {
|
||||
*slot = Some(Reading {
|
||||
temp_milli_c,
|
||||
rh_milli_pct,
|
||||
taken_at,
|
||||
})
|
||||
});
|
||||
cx.shared.state.lock(|s| {
|
||||
s.sensor_ok = true;
|
||||
s.ever_measured = true;
|
||||
s.consecutive_failures = 0;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::warn!("sensor poll failed: {}", e);
|
||||
|
||||
let failures = cx.shared.state.lock(|s| {
|
||||
s.sensor_ok = false;
|
||||
s.consecutive_failures = s.consecutive_failures.saturating_add(1);
|
||||
match e {
|
||||
crate::sensor::Error::I2c => {
|
||||
s.i2c_errors = s.i2c_errors.saturating_add(1)
|
||||
}
|
||||
crate::sensor::Error::Crc => {
|
||||
s.sensor_crc_errors = s.sensor_crc_errors.saturating_add(1)
|
||||
}
|
||||
}
|
||||
s.consecutive_failures
|
||||
});
|
||||
|
||||
// Try to shake a wedged sensor loose rather than reporting a
|
||||
// fault forever. The reading itself stays cached and ages,
|
||||
// so a master watching `age_ms` sees the problem either way.
|
||||
if failures >= board::SENSOR_FAULT_THRESHOLD {
|
||||
defmt::warn!("{} consecutive failures, resetting sensor", failures);
|
||||
let _ = sensor.soft_reset();
|
||||
Mono::delay(board::Duration::millis(u64::from(sht3x::SOFT_RESET_MAX_MS)))
|
||||
.await;
|
||||
let _ = sensor.clear_status();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read the status register so an unexpected sensor reset shows up in
|
||||
// READ_STATUS instead of silently reverting the configuration.
|
||||
if let Ok(status) = sensor.read_status() {
|
||||
let saw_reset = status & sht3x::status::SYSTEM_RESET_DETECTED != 0;
|
||||
cx.shared.state.lock(|s| {
|
||||
s.sensor_status = status;
|
||||
s.sensor_reset_seen |= saw_reset;
|
||||
});
|
||||
if saw_reset {
|
||||
defmt::warn!("SHT31 reported a reset (status {=u16:#06x})", status);
|
||||
let _ = sensor.clear_status();
|
||||
}
|
||||
}
|
||||
|
||||
Mono::delay_until(next).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a conversion, wait out the data-sheet worst case, read it back.
|
||||
async fn measure<I: embedded_hal::i2c::I2c>(
|
||||
sensor: &mut Sht31<I>,
|
||||
) -> Result<sht3x::Raw, crate::sensor::Error> {
|
||||
sensor.start_measurement()?;
|
||||
// Clock stretching is disabled, so the sensor NACKs a read until the
|
||||
// conversion finishes. Waiting the worst case beats retry-until-ACK: it
|
||||
// keeps the I2C bus idle and the timing predictable.
|
||||
Mono::delay(board::Duration::millis(u64::from(
|
||||
sht3x::MEASURE_HIGH_MAX_MS,
|
||||
)))
|
||||
.await;
|
||||
sensor.read_measurement()
|
||||
}
|
||||
|
||||
/// Blink the status LED. Says the executor is alive; says nothing about the
|
||||
/// bus or the sensor, both of which are reported over the protocol instead.
|
||||
#[task(priority = 1, local = [led])]
|
||||
async fn heartbeat(cx: heartbeat::Context) {
|
||||
loop {
|
||||
cx.local.led.toggle().ok();
|
||||
Mono::delay(board::Duration::millis(u64::from(board::HEARTBEAT_MS))).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Half-duplex RS485 transport: PL011 register access plus driver-enable control.
|
||||
//!
|
||||
//! The HAL sets the baud rate and frame format once during init and then hands
|
||||
//! the raw peripheral over, because two things this driver depends on are not
|
||||
//! exposed by `rp2040-hal`:
|
||||
//!
|
||||
//! - **The receive-timeout interrupt (`RTIM`).** The PL011 raises it after 32
|
||||
//! idle bit periods with unread data in the FIFO, which is very close to the
|
||||
//! 3.5-character inter-frame gap the protocol uses as a frame delimiter. It
|
||||
//! gets us an interrupt at the end of a short frame without polling.
|
||||
//! - **The `BUSY` flag.** The PL011 has no transmit-complete interrupt, only
|
||||
//! FIFO-level ones, so `BUSY` is the only way to know the final stop bit has
|
||||
//! left the shift register and the transceiver may stop driving the bus.
|
||||
//!
|
||||
//! `RTIM` alone is not a sufficient frame delimiter: it only fires while the RX
|
||||
//! FIFO is non-empty, so a frame whose length happens to land exactly on the
|
||||
//! FIFO watermark can be drained by the watermark interrupt and never produce a
|
||||
//! timeout. The authoritative delimiter is therefore a timer measured from the
|
||||
//! last received byte; `RTIM` just makes the common case prompt.
|
||||
|
||||
use embedded_hal::digital::OutputPin;
|
||||
use rp2040_hal::pac;
|
||||
use wiredsensor_core::{frame::MAX_FRAME, timing};
|
||||
|
||||
use crate::board::{self, Instant, Rs485DePin, Rs485Pins};
|
||||
|
||||
/// Every interrupt source this driver ever unmasks, as a mask for `UARTICR`.
|
||||
///
|
||||
/// Bit positions are from the PL011 `UARTRIS`/`UARTICR` layout.
|
||||
const RX_IRQ_MASK: u32 = (1 << 4) // RXRIS – RX FIFO reached its watermark
|
||||
| (1 << 6) // RTRIS – receive timeout, line went idle
|
||||
| (1 << 7) // FERIS – framing error
|
||||
| (1 << 8) // PERIS – parity error
|
||||
| (1 << 9) // BERIS – break
|
||||
| (1 << 10); // OERIS – receive overrun
|
||||
|
||||
/// `UARTIFLS.RXIFLSEL` value selecting a half-full (16 of 32 bytes) watermark.
|
||||
const RX_WATERMARK_HALF: u8 = 0b010;
|
||||
|
||||
/// Depth of the PL011 transmit and receive FIFOs, in characters.
|
||||
pub const FIFO_DEPTH: usize = 32;
|
||||
|
||||
/// Time to clock out `chars` characters at the configured line rate.
|
||||
pub const fn tx_time_us(chars: usize) -> u32 {
|
||||
timing::tx_time_us(chars, board::BAUD_RATE)
|
||||
}
|
||||
|
||||
/// Bytes accumulated since the last inter-frame gap, filled from the UART ISR.
|
||||
pub struct RxBuffer {
|
||||
/// Received bytes.
|
||||
pub buf: [u8; MAX_FRAME],
|
||||
/// How many entries of [`Self::buf`] are valid.
|
||||
pub len: usize,
|
||||
/// More bytes arrived than a legal frame can hold, so the frame is unusable.
|
||||
pub overflow: bool,
|
||||
/// The PL011 flagged a line error: overrun, framing, parity or break.
|
||||
pub line_error: bool,
|
||||
/// When the most recent byte arrived. The gap timer measures from here.
|
||||
pub last_byte_at: Instant,
|
||||
/// A gap-detection task is already scheduled for the frame in progress, so
|
||||
/// further bytes must not spawn another one.
|
||||
pub gap_pending: bool,
|
||||
}
|
||||
|
||||
impl RxBuffer {
|
||||
/// An empty buffer.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
buf: [0; MAX_FRAME],
|
||||
len: 0,
|
||||
overflow: false,
|
||||
line_error: false,
|
||||
last_byte_at: Instant::from_ticks(0),
|
||||
gap_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the frame in progress and its error state.
|
||||
pub fn reset(&mut self) {
|
||||
self.len = 0;
|
||||
self.overflow = false;
|
||||
self.line_error = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RxBuffer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The RS485 side of the node: one PL011 plus the transceiver's direction pin.
|
||||
pub struct Rs485 {
|
||||
uart: pac::UART0,
|
||||
de: Rs485DePin,
|
||||
/// Held only so the pads stay assigned to the UART for as long as this
|
||||
/// driver exists; the peripheral itself is reached through `uart`.
|
||||
_pins: Rs485Pins,
|
||||
}
|
||||
|
||||
impl Rs485 {
|
||||
/// Take over an already-configured PL011 and put the transceiver in receive.
|
||||
pub fn new(uart: pac::UART0, pins: Rs485Pins, mut de: Rs485DePin) -> Self {
|
||||
// Receive mode first, before anything can make us transmit.
|
||||
de.set_low().ok();
|
||||
|
||||
// Interrupt when the RX FIFO is half full. Requests are far shorter than
|
||||
// 16 bytes, so in practice the receive timeout is what fires; this
|
||||
// watermark only matters for an unexpectedly long or back-to-back burst.
|
||||
uart.uartifls()
|
||||
.modify(|_, w| unsafe { w.rxiflsel().bits(RX_WATERMARK_HALF) });
|
||||
|
||||
// Transmission is driven by polling, so no TX interrupt is unmasked.
|
||||
uart.uartimsc().modify(|_, w| {
|
||||
w.rxim().set_bit();
|
||||
w.rtim().set_bit();
|
||||
w.feim().set_bit();
|
||||
w.peim().set_bit();
|
||||
w.beim().set_bit();
|
||||
w.oeim().set_bit();
|
||||
w
|
||||
});
|
||||
|
||||
// The HAL's `enable()` turns the DMA request lines on; we do not use DMA.
|
||||
uart.uartdmacr().reset();
|
||||
|
||||
// Start from a clean slate: discard anything that arrived during boot.
|
||||
while uart.uartfr().read().rxfe().bit_is_clear() {
|
||||
let _ = uart.uartdr().read();
|
||||
}
|
||||
uart.uarticr().write(|w| unsafe { w.bits(RX_IRQ_MASK) });
|
||||
|
||||
Self {
|
||||
uart,
|
||||
de,
|
||||
_pins: pins,
|
||||
}
|
||||
}
|
||||
|
||||
/// Move everything in the RX FIFO into `rx`, stamping each byte's arrival.
|
||||
///
|
||||
/// Called from the UART ISR at the highest priority in the system, so it does
|
||||
/// no more than it must: empty a 32-byte FIFO and record the time.
|
||||
pub fn drain_into(&mut self, rx: &mut RxBuffer, now: Instant) {
|
||||
// Catch error interrupts that carry no data of their own, such as a break
|
||||
// condition on an otherwise idle line.
|
||||
let mis = self.uart.uartmis().read();
|
||||
if mis.oemis().bit_is_set()
|
||||
|| mis.femis().bit_is_set()
|
||||
|| mis.pemis().bit_is_set()
|
||||
|| mis.bemis().bit_is_set()
|
||||
{
|
||||
rx.line_error = true;
|
||||
}
|
||||
|
||||
while self.uart.uartfr().read().rxfe().bit_is_clear() {
|
||||
let dr = self.uart.uartdr().read();
|
||||
|
||||
// The PL011 reports per-character errors in the same register as the
|
||||
// data, so a corrupt byte is identified individually rather than
|
||||
// condemning the whole burst.
|
||||
if dr.oe().bit_is_set()
|
||||
|| dr.fe().bit_is_set()
|
||||
|| dr.pe().bit_is_set()
|
||||
|| dr.be().bit_is_set()
|
||||
{
|
||||
rx.line_error = true;
|
||||
}
|
||||
|
||||
if rx.len < rx.buf.len() {
|
||||
rx.buf[rx.len] = dr.data().bits();
|
||||
rx.len += 1;
|
||||
} else {
|
||||
// Keep draining even once the frame is too long, or the FIFO
|
||||
// would overrun and corrupt the *next* frame as well.
|
||||
rx.overflow = true;
|
||||
}
|
||||
|
||||
rx.last_byte_at = now;
|
||||
}
|
||||
|
||||
// Clear the timeout and error latches. RXRIS deasserts by itself now that
|
||||
// the loop above has taken the FIFO below its watermark.
|
||||
self.uart
|
||||
.uarticr()
|
||||
.write(|w| unsafe { w.bits(RX_IRQ_MASK) });
|
||||
}
|
||||
|
||||
/// Assert DE so the transceiver starts driving the differential pair.
|
||||
///
|
||||
/// /RE is tied to DE on this board, so our own receiver is disabled for the
|
||||
/// whole transmission and cannot hear the echo of what we send.
|
||||
pub fn tx_enable(&mut self) {
|
||||
self.de.set_high().ok();
|
||||
}
|
||||
|
||||
/// Push as much of `data` as the TX FIFO accepts, returning what is left.
|
||||
pub fn push<'d>(&mut self, data: &'d [u8]) -> &'d [u8] {
|
||||
let mut sent = 0;
|
||||
while sent < data.len() && self.uart.uartfr().read().txff().bit_is_clear() {
|
||||
self.uart
|
||||
.uartdr()
|
||||
.write(|w| unsafe { w.data().bits(data[sent]) });
|
||||
sent += 1;
|
||||
}
|
||||
&data[sent..]
|
||||
}
|
||||
|
||||
/// True once the shift register has clocked out the final stop bit.
|
||||
///
|
||||
/// Releasing DE before this is set truncates the last character for every
|
||||
/// listener on the segment — a classic RS485 fault that looks like a CRC
|
||||
/// error at the master and is invisible at the slave.
|
||||
pub fn tx_complete(&self) -> bool {
|
||||
self.uart.uartfr().read().busy().bit_is_clear()
|
||||
}
|
||||
|
||||
/// Release the bus and discard whatever the receiver picked up on turnaround.
|
||||
///
|
||||
/// While DE was asserted the transceiver's /RE was disabled and its RO output
|
||||
/// was not driven, so the edge as it re-enables can clock a spurious
|
||||
/// character into the FIFO. Left in place that byte would become the first
|
||||
/// byte of the next frame and break it.
|
||||
pub fn tx_disable(&mut self) {
|
||||
self.de.set_low().ok();
|
||||
|
||||
while self.uart.uartfr().read().rxfe().bit_is_clear() {
|
||||
let _ = self.uart.uartdr().read();
|
||||
}
|
||||
self.uart
|
||||
.uarticr()
|
||||
.write(|w| unsafe { w.bits(RX_IRQ_MASK) });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! SHT31 driver: I2C transfers on top of the pure logic in
|
||||
//! [`wiredsensor_core::sht3x`].
|
||||
//!
|
||||
//! Measurements are issued with clock stretching **disabled**. The alternative
|
||||
//! would have the sensor hold SCL low for up to 15 ms, which blocks the whole
|
||||
//! I2C bus and makes the transfer's duration depend on the controller's stretch
|
||||
//! timeout. Instead the conversion is started, the caller awaits the data-sheet
|
||||
//! worst case, and the result is read in a second transfer — which keeps the
|
||||
//! bus free and the timing explicit.
|
||||
|
||||
use embedded_hal::i2c::I2c;
|
||||
use wiredsensor_core::sht3x::{self, DecodeError};
|
||||
|
||||
/// Why a sensor operation failed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
pub enum Error {
|
||||
/// The I2C transfer itself failed: NACK, arbitration loss or a stuck bus.
|
||||
I2c,
|
||||
/// The transfer completed but a CRC-8 over the returned data did not match.
|
||||
Crc,
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(_: DecodeError) -> Self {
|
||||
Error::Crc
|
||||
}
|
||||
}
|
||||
|
||||
/// An SHT31 on an I2C bus.
|
||||
pub struct Sht31<I> {
|
||||
i2c: I,
|
||||
addr: u8,
|
||||
}
|
||||
|
||||
impl<I: I2c> Sht31<I> {
|
||||
/// Bind to the sensor at `addr` (see [`sht3x::I2C_ADDR_DEFAULT`]).
|
||||
pub fn new(i2c: I, addr: u8) -> Self {
|
||||
Self { i2c, addr }
|
||||
}
|
||||
|
||||
/// Send a bare 16-bit command with no reply.
|
||||
pub fn command(&mut self, cmd: u16) -> Result<(), Error> {
|
||||
self.i2c
|
||||
.write(self.addr, &sht3x::command_bytes(cmd))
|
||||
.map_err(|_| Error::I2c)
|
||||
}
|
||||
|
||||
/// Start a high-repeatability conversion.
|
||||
///
|
||||
/// The caller must wait [`sht3x::MEASURE_HIGH_MAX_MS`] before calling
|
||||
/// [`Self::read_measurement`]; reading sooner is NACKed.
|
||||
pub fn start_measurement(&mut self) -> Result<(), Error> {
|
||||
self.command(sht3x::command::MEASURE_HIGH_NO_STRETCH)
|
||||
}
|
||||
|
||||
/// Read back and validate a conversion started earlier.
|
||||
pub fn read_measurement(&mut self) -> Result<sht3x::Raw, Error> {
|
||||
let mut buf = [0u8; sht3x::MEASUREMENT_LEN];
|
||||
self.i2c.read(self.addr, &mut buf).map_err(|_| Error::I2c)?;
|
||||
Ok(sht3x::decode_measurement(&buf)?)
|
||||
}
|
||||
|
||||
/// Read the 16-bit status register.
|
||||
pub fn read_status(&mut self) -> Result<u16, Error> {
|
||||
let mut buf = [0u8; sht3x::WORD_LEN];
|
||||
self.i2c
|
||||
.write_read(
|
||||
self.addr,
|
||||
&sht3x::command_bytes(sht3x::command::READ_STATUS),
|
||||
&mut buf,
|
||||
)
|
||||
.map_err(|_| Error::I2c)?;
|
||||
Ok(sht3x::decode_word(&buf)?)
|
||||
}
|
||||
|
||||
/// Clear the sticky bits in the status register, including the
|
||||
/// reset-detected flag.
|
||||
pub fn clear_status(&mut self) -> Result<(), Error> {
|
||||
self.command(sht3x::command::CLEAR_STATUS)
|
||||
}
|
||||
|
||||
/// Soft-reset the sensor. The caller must wait
|
||||
/// [`sht3x::SOFT_RESET_MAX_MS`] afterwards.
|
||||
pub fn soft_reset(&mut self) -> Result<(), Error> {
|
||||
self.command(sht3x::command::SOFT_RESET)
|
||||
}
|
||||
|
||||
/// Read the 32-bit factory serial number.
|
||||
pub fn read_serial(&mut self) -> Result<u32, Error> {
|
||||
let mut buf = [0u8; sht3x::MEASUREMENT_LEN];
|
||||
self.i2c
|
||||
.write_read(
|
||||
self.addr,
|
||||
&sht3x::command_bytes(sht3x::command::READ_SERIAL),
|
||||
&mut buf,
|
||||
)
|
||||
.map_err(|_| Error::I2c)?;
|
||||
Ok(sht3x::decode_serial(&buf)?)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user