simple_max31865/lib.rs
1//! A simplified driver for the MAX31865 RTD to Digital converter (Raspberry Pi focus)
2//!
3//! # References
4//! - Datasheet: https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf
5//! - Wiring diagrams: https://www.playingwithfusion.com/docs/1203
6//! - SPECIAL NOTE: The chip does _not_ implement continuous mode, in spite of the docs.
7//!
8
9// TODO: Update and improve README (see other branches), esp sample code.
10// TODO: Add PT-1000 support.
11// TODO: Improve and test fault handling, add to README test case
12// TODO: Enhance RtdError to differentiate between Pin and Spi (transfer) errors.
13// TODO: get down to a single Error type: Use RtdError directly in private code.
14// TODO: Enable no_std => ![cfg_attr(not(test), no_std)]
15//
16// TODO: Stub off hardware access by creating abstract implementations of Trait(s) and
17// create minimal Mock unit tests to validate basic abstract operations.
18// This implementation shall be available only under a "mock" feature.
19//
20// Requirements for Traits
21// 1. All traits must use RTDError as their error class
22// 2. Must include an SPI abstraction and a Pin abstraction
23// 3. Pin abstraction must implement raise and lower APIs, and include raise and lower APIs,
24// and implement at least OutputPins, with the ability create them with pullups or pulldowns
25// Creation must check for range of pin number.
26// 4. SPI abstraction must implement transfer
27// (pub fn new(cs_pin: u8, leads: RTDLeads, filter: FilterHz) -> Result<Self, RtdError>)
28// 5. SPI constructor/new must take current SPI parameters
29// 6. SPI abstraction must implement transfer API
30// 7. Traits shall have zero effect on top level (public) API
31// 8. Traits shall not change interactions with real hardware.
32// Do not "improve" the real hardware interactions. That code is well-proven.
33// This should be an "of course" kind of thing.
34//
35// TODO: Create mock implementation of SPI and Pin abstractions
36//
37// 1. switching between mock and real APIs shall be controlled by a feature called "mock".
38// Without the mock feature, the hardware implementation of the traits shall be used
39// and with it enabled, the mock version shall be used.
40// 2. The mock implementation of SPI transfers shall assume transfer is to known
41// MAX31865 registers and verify correct interaction with the mocked hardware
42// by callers.
43// 4. Minimal mock tests to verify basic mocked calls don't fail shall be included.
44// See also next to-do item. These tests are to validate the mock implementation.
45// 5. "Real" hardware shall not be available when "mock" feature is selected.
46// 5. Mock hardware shall not change interactions with real hardware at all.
47// This should be an "of-course" kind of thing
48//
49// TODO: Create "mock" hardware tests which exercise and test the APIs with mock feature.
50// The purpose of this is to test our normal interactions with the hardware and
51// also exercise error legs that are impossible to create automatically in real hardware.
52//
53// 1. Mocked hardware tests shall exercise underlying hardware and create error
54// situations which are difficult to create in real hardware
55// 2. Mock test only API calls shall be created which inject faults and control
56// hardware for the benefit of mock tests. Ability to inject faults and control
57// contents of hardware registers will be added as needed for tests.
58//
59use embedded_hal::digital::OutputPin;
60use embedded_hal::spi::{Mode, Phase, Polarity, SpiBus};
61extern crate alloc;
62
63// Public enums and helpers (crate-level)
64#[derive(Debug, Clone, Copy)]
65/// RTD lead configurations supported by the MAX31865.
66pub enum RTDLeads {
67 Two = 2,
68 Three = 3,
69 Four = 4,
70}
71
72#[derive(Debug, Clone, Copy)]
73/// Noise filter settings based on mains frequency.
74pub enum FilterHz {
75 /// 50 Hz filter (updates ~16 Hz).
76 Fifty = 1,
77 /// 60 Hz filter (updates ~19 Hz).
78 Sixty = 0,
79}
80
81#[derive(Debug)]
82/// An enumeration of all the different faults the API can report back.
83pub enum RtdError {
84 InvalidChipSelect, // The chip select lead given is out of range
85 Init(String), // Initialization failed
86 Read(String), // Reading or writing the SPI bus failed
87 Fault(u8), // An error was reported by the MAX31865
88}
89
90#[derive(Debug, Clone, Copy)]
91/// All the errors the MAX31865 can report to us.
92pub enum MaxFault {
93 RtdInMinusUndervoltage, // Bit 0: RTDIN- undervoltage
94 RtdInPlusOvervoltage, // Bit 1: RTDIN+ overvoltage
95 RtdInMinusOvervoltage, // Bit 2: RTDIN- overvoltage
96 RtdInPlusOpen, // Bit 3: RTDIN+ open circuit
97 RtdInMinusOpen, // Bit 4: RTDIN- open circuit
98 RtdUnderOrOvertemp, // Bit 5: RTD under/over temperature
99 RtdOverOrUnderBiasVoltage, // Bit 6: RTD over/under bias voltage
100 AutoConversionFault, // Bit 7: Auto-conversion fault
101}
102
103impl MaxFault {
104 /// Returns the bitmask (u8) for this MAX31865 fault type.
105 pub fn bit(self) -> u8 {
106 match self {
107 MaxFault::RtdInMinusUndervoltage => 0b00000001,
108 MaxFault::RtdInPlusOvervoltage => 0b00000010,
109 MaxFault::RtdInMinusOvervoltage => 0b00000100,
110 MaxFault::RtdInPlusOpen => 0b00001000,
111 MaxFault::RtdInMinusOpen => 0b00010000,
112 MaxFault::RtdUnderOrOvertemp => 0b00100000,
113 MaxFault::RtdOverOrUnderBiasVoltage => 0b01000000,
114 MaxFault::AutoConversionFault => 0b10000000,
115 }
116 }
117
118 /// Returns a human-readable description for this MAX31865 fault.
119 pub fn description(self) -> &'static str {
120 match self {
121 MaxFault::RtdInMinusUndervoltage => "RTD IN- Undervoltage",
122 MaxFault::RtdInPlusOvervoltage => "RTD IN+ Overvoltage",
123 MaxFault::RtdInMinusOvervoltage => "RTD IN- Overvoltage",
124 MaxFault::RtdInPlusOpen => "RTD IN+ Open Circuit",
125 MaxFault::RtdInMinusOpen => "RTD IN- Open Circuit",
126 MaxFault::RtdUnderOrOvertemp => "RTD Under/Over Temperature",
127 MaxFault::RtdOverOrUnderBiasVoltage => "RTD Over/Under Bias Voltage",
128 MaxFault::AutoConversionFault => "Auto-Conversion Fault",
129 }
130 }
131}
132
133/// Public helper to decode a full fault status byte into a list of active faults (for users).
134/// Returns a Vec of descriptions for set bits; empty if no faults.
135pub fn decode_fault_status(status: u8) -> Vec<&'static str> {
136 let mut faults = Vec::new();
137 let all_faults = [
138 (MaxFault::RtdInMinusUndervoltage, 0b00000001),
139 (MaxFault::RtdInPlusOvervoltage, 0b00000010),
140 (MaxFault::RtdInMinusOvervoltage, 0b00000100),
141 (MaxFault::RtdInPlusOpen, 0b00001000),
142 (MaxFault::RtdInMinusOpen, 0b00010000),
143 (MaxFault::RtdUnderOrOvertemp, 0b00100000),
144 (MaxFault::RtdOverOrUnderBiasVoltage, 0b01000000),
145 (MaxFault::AutoConversionFault, 0b10000000),
146 ];
147 for (fault, bit) in all_faults {
148 if status & bit != 0 {
149 faults.push(fault.description());
150 }
151 }
152 faults
153}
154
155pub const MODE: Mode = Mode {
156 phase: Phase::CaptureOnSecondTransition,
157 polarity: Polarity::IdleHigh,
158};
159
160pub mod temp_conversion;
161
162// Public simplified wrapper API (contains only RTDReader)
163pub mod rtd_reader {
164 use crate::private::{Error as InternalError, Max31865};
165 use crate::{FilterHz, RTDLeads, RtdError};
166 use rppal::gpio::{Gpio, OutputPin as GpioOutputPin};
167 use rppal::spi::{Bus, Mode as SpiMode, SlaveSelect, Spi}; // Root public enum
168
169 /// Simplified high-level interface for Raspberry Pi (continuous mode only).
170 /// Hides SPI/GPIO setup, RDY pin (unused), and low-level details.
171 /// Assumes PT100 sensor; configure with CS pin, leads, and filter.
172 pub struct RTDReader {
173 inner: Max31865<Spi, GpioOutputPin>,
174 }
175
176 impl RTDReader {
177 /// Create a new RTDReader (Raspberry Pi only).
178 ///
179 /// # Arguments
180 /// * `cs_pin` - GPIO pin for Chip Select (NCS, active low).
181 /// * `leads` - Number of wires in the RTD setup (2/3/4).
182 /// * `filter` - Noise filter based on mains frequency (50/60 Hz).
183 ///
184 /// Configures continuous mode (vbias=true, auto-conversion=true, one-shot=false).
185 /// Defaults to 400Ω calibration. RDY pin is not used (can float).
186 pub fn new(cs_pin: u8, leads: RTDLeads, filter: FilterHz) -> Result<Self, RtdError> {
187 let gpio =
188 Gpio::new().map_err(|e| RtdError::Init(format!("GPIO init failed: {}", e)))?;
189 let ncs = gpio
190 .get(cs_pin)
191 .map_err(|e| RtdError::Init(format!("NCS pin {} invalid: {}", cs_pin, e)))?
192 .into_output_high();
193 let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, SpiMode::Mode3)
194 .map_err(|e| RtdError::Init(format!("SPI init failed: {}", e)))?;
195
196 let mut inner = Max31865::new(spi, ncs).map_err(|e| {
197 RtdError::Init(match e {
198 InternalError::GpioFault => "NCS pin setup failed".to_string(),
199 _ => "MAX31865 init failed".to_string(),
200 })
201 })?;
202 inner
203 .configure(leads, filter)
204 .map_err(|e| RtdError::Init(format!("Configure failed: {:?}", e)))?;
205
206 Ok(RTDReader { inner })
207 }
208
209 /// Read temperature in °C as f64 (PT100 lookup).
210 pub fn get_temperature(&mut self) -> Result<f64, RtdError> {
211 self.inner.read_temperature().map_err(map_internal_error)
212 }
213
214 /// Read resistance in ohms as f64.
215 pub fn get_resistance(&mut self) -> Result<f64, RtdError> {
216 self.inner.read_resistance().map_err(map_internal_error)
217 }
218
219 /// Read temperature as scaled integer (degrees Celsius * 100).
220 pub fn read_temp_100(&mut self) -> Result<i32, RtdError> {
221 self.inner
222 .read_default_conversion()
223 .map_err(map_internal_error)
224 }
225
226 /// Read resistance as scaled integer (ohms * 100).
227 pub fn get_ohms_100(&mut self) -> Result<u32, RtdError> {
228 self.inner.read_ohms().map_err(map_internal_error)
229 }
230
231 /// Read raw RTD value (u16, for testing/low-level).
232 pub fn get_raw_data(&mut self) -> Result<u16, RtdError> {
233 self.inner.read_raw().map_err(map_internal_error)
234 }
235
236 /// Check if an error is a MAX31865 fault (RtdError::Fault variant).
237 pub fn is_max_fault(&self, e: &RtdError) -> bool {
238 matches!(e, RtdError::Fault(_))
239 }
240
241 /// Read fault status (u8 from reg 0x07; auto-clears).
242 pub fn read_fault_status(&mut self) -> Result<u8, RtdError> {
243 self.inner.read_fault_status().map_err(map_internal_error)
244 }
245
246 /// Clear any latched faults (no-op if none).
247 pub fn clear_fault(&mut self) -> Result<(), RtdError> {
248 self.inner.clear_fault().map_err(|e| {
249 RtdError::Read(match e {
250 InternalError::SpiErrorTransfer => "Clear fault SPI write failed".to_string(),
251 _ => "Clear fault failed".to_string(),
252 })
253 })
254 }
255
256 /// Set calibration (ohms * 100, e.g., 40000 for 400Ω).
257 pub fn set_calibration(&mut self, calibration: u32) {
258 self.inner.set_calibration(calibration);
259 }
260 }
261
262 /// Map internal low-level errors to public RtdError.
263 fn map_internal_error(e: InternalError) -> RtdError {
264 match e {
265 InternalError::SpiErrorTransfer | InternalError::GpioFault => {
266 RtdError::Read("SPI/GPIO transfer failed".to_string())
267 }
268 InternalError::MAXFault => RtdError::Fault(0), // Placeholder; call read_fault_status() for real status
269 }
270 }
271}
272
273// Re-export RTDReader at root for flat imports (agreed API consistency)
274pub use rtd_reader::RTDReader;
275
276// Private module for low-level driver (opaque to users)
277mod private {
278 use super::*;
279
280 #[derive(Debug)]
281 pub enum Error {
282 /// Error transferring data to/from Max31865 chip registers
283 SpiErrorTransfer,
284 /// Error setting the state of a pin in the GPIO bus
285 GpioFault,
286 /// The Max31865 chip declared an error when converting temperatures.
287 /// Use `read_fault_status()` for details.
288 MAXFault,
289 }
290
291 pub struct Max31865<SPI, NCS> {
292 spi: SPI,
293 ncs: NCS,
294 calibration: u32,
295 base_config: u8, // Set in configure
296 }
297
298 impl<SPI, NCS> Max31865<SPI, NCS>
299 where
300 SPI: SpiBus<u8>,
301 NCS: OutputPin,
302 {
303 /// Create a new MAX31865 module (internal use only).
304 pub fn new(spi: SPI, mut ncs: NCS) -> Result<Max31865<SPI, NCS>, Error> {
305 let default_calibration = 40000;
306
307 ncs.set_high().map_err(|_| Error::GpioFault)?;
308 let max31865 = Max31865 {
309 spi,
310 ncs,
311 calibration: default_calibration,
312 base_config: 0, // Set in configure
313 };
314
315 Ok(max31865)
316 }
317
318 // From MAX31865 datasheet (page 16, Table 8):
319 //
320 // Bit 7 (V_BIAS): 1 = enable bias excitation (should be 1).
321 // Bit 6 (1-SHOT): 0 = continuous conversion (ongoing reads),
322 // 1 = one-shot (single conversion, then stop).
323 // Bit 5: Reserved (should be 0)
324 // Bit 4 (wires): 1 = 3-wire PT100, 0 = 2 or 4-wire
325 // Bit 3 (AUTO-CONVERT): 1 = auto-conversion enabled
326 // Bit 2: Reserved (should be 0)
327 // Bit 1: Reserved (should be 0)
328 // Bit 0 (50/60Hz): 0 = 60Hz, 1 = 50 hz
329
330 /// Updates the devices configuration (internal use only).
331 pub fn configure(
332 &mut self,
333 sensor_type_enum: RTDLeads, // From public RTDLeads cast
334 filter_mode_enum: FilterHz, // From public FilterHz cast
335 ) -> Result<(), Error> {
336 // Compute sensor type and filter mode bits directly
337 let sensor_type = match sensor_type_enum {
338 RTDLeads::Three => 1u8,
339 RTDLeads::Two | RTDLeads::Four => 0u8, // Two or Four = 0
340 };
341 let filter_mode = match filter_mode_enum {
342 FilterHz::Fifty => 1u8, // Fifty = 1 (low order bit)
343 FilterHz::Sixty => 0u8, // Sixty = 0 (no lower order bits)
344 };
345 // One-shot config: V_BIAS=1, 1-SHOT=1, wires, filter (no AUTO= bit 3=0)
346 self.base_config = (1u8 << 7) // V_BIAS=1
347 | (1u8 << 6) // 1-SHOT=1 (triggers on write)
348 | (sensor_type << 4) // Wires bit 4
349 | filter_mode; // Filter bit 0
350 self.write(Register::CONFIG, self.base_config)?; // Initial write (starts first conversion)
351 self.clear_fault()?; // Unlatch any boot faults (mimics Adafruit init)
352
353 Ok(())
354 }
355
356 /// Clear latched faults (config reg bit 1 = 1)
357 pub fn clear_fault(&mut self) -> Result<(), Error> {
358 self.write(Register::CONFIG, 0x02)
359 }
360
361 /// Read and clear fault status reg (0x07) for bit-level diagnostics (u8 LSB)
362 pub fn read_fault_status(&mut self) -> Result<u8, Error> {
363 let status = self.read(Register::FAULT_STATUS)?;
364 self.clear_fault()?; // Clear after read (if auto-clear needed)
365 Ok(status)
366 }
367
368 /// Set the calibration reference resistance (internal use only).
369 pub fn set_calibration(&mut self, calibration: u32) {
370 self.calibration = calibration;
371 }
372
373 /// Read the raw resistance value.
374 /// The output value is the value in Ohms multiplied by 100.
375 pub fn read_ohms(&mut self) -> Result<u32, Error> {
376 let raw = self.read_raw()?;
377 let ohms = ((raw >> 1) as u32 * self.calibration) >> 15;
378 Ok(ohms)
379 }
380
381 /// Read resistance in ohms as f64
382 pub fn read_resistance(&mut self) -> Result<f64, Error> {
383 let ohms_raw = self.read_ohms()?; // u32 *100;
384 Ok(ohms_raw as f64 / 100.0)
385 }
386
387 /// Read temperature in °C as f64
388 pub fn read_temperature(&mut self) -> Result<f64, Error> {
389 let temp_raw = self.read_default_conversion()?; // i32 *100
390 Ok(temp_raw as f64 / 100.0)
391 }
392
393 /// Read the raw resistance value and then perform conversion to degrees Celsius.
394 /// The output value is the value in degrees Celsius multiplied by 100.
395 pub fn read_default_conversion(&mut self) -> Result<i32, Error> {
396 let ohms = self.read_ohms()?;
397 let temp = temp_conversion::LOOKUP_VEC_PT100.lookup_temperature(ohms as i32);
398 Ok(temp)
399 }
400
401 /// Read the raw RTD value.
402 /// The raw value is the value of the combined MSB and LSB registers.
403 /// The first 15 bits specify the ohmic value in relation to the reference
404 /// resistor (i.e. 2^15 - 1 would be the exact same resistance as the reference
405 /// resistor). See manual for further information.
406 /// The last bit specifies if the conversion was successful.
407 pub fn read_raw(&mut self) -> Result<u16, Error> {
408 // Trigger new conversion: Write config (1-SHOT=1 starts it)
409 self.write(Register::CONFIG, self.base_config)?;
410
411 // Wait for conversion (100ms conservative >65ms datasheet min)
412 std::thread::sleep(std::time::Duration::from_millis(100));
413
414 // Read RTD
415 let buffer = self.read_two(Register::RTD_MSB)?;
416 let raw = ((buffer[0] as u16) << 8) | (buffer[1] as u16);
417 if raw & 1 != 0 {
418 // Fault: Clear + retry once
419 let _ = self.read_fault_status(); // Reads + clears faults
420 // Retry: Trigger again
421 self.write(Register::CONFIG, self.base_config)?;
422 std::thread::sleep(std::time::Duration::from_millis(100));
423 let retry_buffer = self.read_two(Register::RTD_MSB)?;
424 let retry_raw = ((retry_buffer[0] as u16) << 8) | (retry_buffer[1] as u16);
425 if retry_raw & 1 != 0 {
426 return Err(Error::MAXFault); // Retry failed
427 }
428 return Ok(retry_raw);
429 }
430 Ok(raw)
431 }
432
433 fn read(&mut self, reg: Register) -> Result<u8, Error> {
434 let mut read_buffer = [0u8; 2]; // 2 bytes: dummy + data
435 let mut write_buffer = [0u8; 2];
436 write_buffer[0] = reg.read_address(); // Read addr for reg (e.g., 0x81 for 0x01)
437 write_buffer[1] = 0; // Dummy data
438 self.ncs.set_low().map_err(|_| Error::GpioFault)?;
439 self.spi
440 .transfer(&mut read_buffer, &write_buffer)
441 .map_err(|_| Error::SpiErrorTransfer)?;
442 self.ncs.set_high().map_err(|_| Error::GpioFault)?;
443 Ok(read_buffer[1]) // Return result (ignore dummy [0])
444 }
445
446 fn read_two(&mut self, reg: Register) -> Result<[u8; 2], Error> {
447 // The hardware is full duplex - you have to read and write the same number of bytes.
448 // The first byte you write is the register offset, and the remaining
449 // bytes are ignored when reading. To read two bytes you write three.
450 // The two bytes we read are in the last two of the three bytes read.
451 // NOTE: It reads and writes the minimum size of the read and write buffers
452 let mut read_buffer = [0u8; 3]; // 3 bytes: dummy + MSB + LSB
453 let mut write_buffer = [0u8; 3];
454 write_buffer[0] = reg.read_address(); // Read addr for reg (e.g., 0x81 for 0x01)
455 write_buffer[1] = 0; // Dummy for MSB
456 write_buffer[2] = 0; // Dummy for LSB
457 self.ncs.set_low().map_err(|_| Error::GpioFault)?;
458 self.spi
459 .transfer(&mut read_buffer, &write_buffer)
460 .map_err(|_| Error::SpiErrorTransfer)?;
461 self.ncs.set_high().map_err(|_| Error::GpioFault)?;
462 Ok([read_buffer[1], read_buffer[2]]) // Return MSB, LSB (ignore dummy [0])
463 }
464
465 fn write(&mut self, reg: Register, val: u8) -> Result<(), Error> {
466 self.ncs.set_low().map_err(|_| Error::GpioFault)?;
467 self.spi
468 .write(&[reg.write_address(), val])
469 .map_err(|_| Error::SpiErrorTransfer)?;
470 self.ncs.set_high().map_err(|_| Error::GpioFault)?;
471 Ok(())
472 }
473 }
474
475 #[allow(non_camel_case_types)]
476 #[allow(dead_code)]
477 #[derive(Clone, Copy)]
478 enum Register {
479 // All the lovely Max31865 register offsets
480 CONFIG = 0x00,
481 RTD_MSB = 0x01,
482 RTD_LSB = 0x02,
483 HIGH_FAULT_THRESHOLD_MSB = 0x03,
484 HIGH_FAULT_THRESHOLD_LSB = 0x04,
485 LOW_FAULT_THRESHOLD_MSB = 0x05,
486 LOW_FAULT_THRESHOLD_LSB = 0x06,
487 FAULT_STATUS = 0x07,
488 }
489
490 const R: u8 = 0 << 7;
491 const W: u8 = 1 << 7;
492
493 impl Register {
494 fn read_address(&self) -> u8 {
495 *self as u8 | R
496 }
497
498 fn write_address(&self) -> u8 {
499 *self as u8 | W
500 }
501 }
502}