Skip to main content

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}