simple-max31865 1.0.0

Easy-to-use driver for the MAX31865 RTD to Digital converter (Raspberry Pi focus)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
//! A simplified driver for the MAX31865 RTD to Digital converter (Raspberry Pi focus)
//!
//! # References
//! - Datasheet: https://datasheets.maximintegrated.com/en/ds/MAX31865.pdf
//! - Wiring diagrams:  https://www.playingwithfusion.com/docs/1203
//! - SPECIAL NOTE: The chip does _not_ implement continuous mode, in spite of the docs.
//!

// TODO: Update and improve README (see other branches), esp sample code.
// TODO: Add PT-1000 support.
// TODO: Improve and test fault handling, add to README test case
// TODO: Enhance RtdError to differentiate between Pin and Spi (transfer) errors.
// TODO: get down to a single Error type: Use RtdError directly in private code.
// TODO: Enable no_std => ![cfg_attr(not(test), no_std)]
//
// TODO: Stub off hardware access by creating abstract implementations of Trait(s) and
//       create minimal Mock unit tests to validate basic abstract operations.
//       This implementation shall be available only under a "mock" feature.
//
//  Requirements for Traits
//      1. All traits must use RTDError as their error class
//      2. Must include an SPI abstraction and a Pin abstraction
//      3. Pin abstraction must implement raise and lower APIs, and include raise and lower APIs,
//         and implement at least OutputPins, with the ability create them with pullups or pulldowns
//         Creation must check for range of pin number.
//      4. SPI abstraction must implement transfer
//          (pub fn new(cs_pin: u8, leads: RTDLeads, filter: FilterHz) -> Result<Self, RtdError>)
//      5. SPI constructor/new must take current SPI parameters
//      6. SPI abstraction must implement transfer API
//      7. Traits shall have zero effect on top level (public) API
//      8. Traits shall not change interactions with real hardware.
//         Do not "improve" the real hardware interactions. That code is well-proven.
//         This should be an "of course" kind of thing.
//
// TODO: Create mock implementation of SPI and Pin abstractions
//
//      1. switching between mock and real APIs shall be controlled by a feature called "mock".
//         Without the mock feature, the hardware implementation of the traits shall be used
//         and with it enabled, the mock version shall be used.
//      2. The mock implementation of SPI transfers shall assume transfer is to known
//         MAX31865 registers and verify correct interaction with the mocked hardware
//         by callers.
//      4. Minimal mock tests to verify basic mocked calls don't fail shall be included.
//         See also next to-do item. These tests are to validate the mock implementation.
//      5. "Real" hardware shall not be available when "mock" feature is selected.
//      5. Mock hardware shall not change interactions with real hardware at all.
//         This should be an "of-course" kind of thing
//
// TODO: Create "mock" hardware tests which exercise and test the APIs with mock feature.
//       The purpose of this is to test our normal interactions with the hardware and
//       also exercise error legs that are impossible to create automatically in real hardware.
//
//      1. Mocked hardware tests shall exercise underlying hardware and create error
//         situations which are difficult to create in real hardware
//      2. Mock test only API calls shall be created which inject faults and control
//         hardware for the benefit of mock tests. Ability to inject faults and control
//         contents of hardware registers will be added as needed for tests.
//
use embedded_hal::digital::OutputPin;
use embedded_hal::spi::{Mode, Phase, Polarity, SpiBus};
extern crate alloc;

// Public enums and helpers (crate-level)
#[derive(Debug, Clone, Copy)]
/// RTD lead configurations supported by the MAX31865.
pub enum RTDLeads {
    Two = 2,
    Three = 3,
    Four = 4,
}

#[derive(Debug, Clone, Copy)]
/// Noise filter settings based on mains frequency.
pub enum FilterHz {
    /// 50 Hz filter (updates ~16 Hz).
    Fifty = 1,
    /// 60 Hz filter (updates ~19 Hz).
    Sixty = 0,
}

#[derive(Debug)]
/// An enumeration of all the different faults the API can report back.
pub enum RtdError {
    InvalidChipSelect, // The chip select lead given is out of range
    Init(String),      // Initialization failed
    Read(String),      // Reading or writing the SPI bus failed
    Fault(u8),         // An error was reported by the MAX31865
}

#[derive(Debug, Clone, Copy)]
/// All the errors the MAX31865 can report to us.
pub enum MaxFault {
    RtdInMinusUndervoltage,    // Bit 0: RTDIN- undervoltage
    RtdInPlusOvervoltage,      // Bit 1: RTDIN+ overvoltage
    RtdInMinusOvervoltage,     // Bit 2: RTDIN- overvoltage
    RtdInPlusOpen,             // Bit 3: RTDIN+ open circuit
    RtdInMinusOpen,            // Bit 4: RTDIN- open circuit
    RtdUnderOrOvertemp,        // Bit 5: RTD under/over temperature
    RtdOverOrUnderBiasVoltage, // Bit 6: RTD over/under bias voltage
    AutoConversionFault,       // Bit 7: Auto-conversion fault
}

impl MaxFault {
    /// Returns the bitmask (u8) for this MAX31865 fault type.
    pub fn bit(self) -> u8 {
        match self {
            MaxFault::RtdInMinusUndervoltage => 0b00000001,
            MaxFault::RtdInPlusOvervoltage => 0b00000010,
            MaxFault::RtdInMinusOvervoltage => 0b00000100,
            MaxFault::RtdInPlusOpen => 0b00001000,
            MaxFault::RtdInMinusOpen => 0b00010000,
            MaxFault::RtdUnderOrOvertemp => 0b00100000,
            MaxFault::RtdOverOrUnderBiasVoltage => 0b01000000,
            MaxFault::AutoConversionFault => 0b10000000,
        }
    }

    /// Returns a human-readable description for this MAX31865 fault.
    pub fn description(self) -> &'static str {
        match self {
            MaxFault::RtdInMinusUndervoltage => "RTD IN- Undervoltage",
            MaxFault::RtdInPlusOvervoltage => "RTD IN+ Overvoltage",
            MaxFault::RtdInMinusOvervoltage => "RTD IN- Overvoltage",
            MaxFault::RtdInPlusOpen => "RTD IN+ Open Circuit",
            MaxFault::RtdInMinusOpen => "RTD IN- Open Circuit",
            MaxFault::RtdUnderOrOvertemp => "RTD Under/Over Temperature",
            MaxFault::RtdOverOrUnderBiasVoltage => "RTD Over/Under Bias Voltage",
            MaxFault::AutoConversionFault => "Auto-Conversion Fault",
        }
    }
}

/// Public helper to decode a full fault status byte into a list of active faults (for users).
/// Returns a Vec of descriptions for set bits; empty if no faults.
pub fn decode_fault_status(status: u8) -> Vec<&'static str> {
    let mut faults = Vec::new();
    let all_faults = [
        (MaxFault::RtdInMinusUndervoltage, 0b00000001),
        (MaxFault::RtdInPlusOvervoltage, 0b00000010),
        (MaxFault::RtdInMinusOvervoltage, 0b00000100),
        (MaxFault::RtdInPlusOpen, 0b00001000),
        (MaxFault::RtdInMinusOpen, 0b00010000),
        (MaxFault::RtdUnderOrOvertemp, 0b00100000),
        (MaxFault::RtdOverOrUnderBiasVoltage, 0b01000000),
        (MaxFault::AutoConversionFault, 0b10000000),
    ];
    for (fault, bit) in all_faults {
        if status & bit != 0 {
            faults.push(fault.description());
        }
    }
    faults
}

pub const MODE: Mode = Mode {
    phase: Phase::CaptureOnSecondTransition,
    polarity: Polarity::IdleHigh,
};

pub mod temp_conversion;

// Public simplified wrapper API (contains only RTDReader)
pub mod rtd_reader {
    use crate::private::{Error as InternalError, Max31865};
    use crate::{FilterHz, RTDLeads, RtdError};
    use rppal::gpio::{Gpio, OutputPin as GpioOutputPin};
    use rppal::spi::{Bus, Mode as SpiMode, SlaveSelect, Spi}; // Root public enum

    /// Simplified high-level interface for Raspberry Pi (continuous mode only).
    /// Hides SPI/GPIO setup, RDY pin (unused), and low-level details.
    /// Assumes PT100 sensor; configure with CS pin, leads, and filter.
    pub struct RTDReader {
        inner: Max31865<Spi, GpioOutputPin>,
    }

    impl RTDReader {
        /// Create a new RTDReader (Raspberry Pi only).
        ///
        /// # Arguments
        /// * `cs_pin` - GPIO pin for Chip Select (NCS, active low).
        /// * `leads` - Number of wires in the RTD setup (2/3/4).
        /// * `filter` - Noise filter based on mains frequency (50/60 Hz).
        ///
        /// Configures continuous mode (vbias=true, auto-conversion=true, one-shot=false).
        /// Defaults to 400Ω calibration. RDY pin is not used (can float).
        pub fn new(cs_pin: u8, leads: RTDLeads, filter: FilterHz) -> Result<Self, RtdError> {
            let gpio =
                Gpio::new().map_err(|e| RtdError::Init(format!("GPIO init failed: {}", e)))?;
            let ncs = gpio
                .get(cs_pin)
                .map_err(|e| RtdError::Init(format!("NCS pin {} invalid: {}", cs_pin, e)))?
                .into_output_high();
            let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, SpiMode::Mode3)
                .map_err(|e| RtdError::Init(format!("SPI init failed: {}", e)))?;

            let mut inner = Max31865::new(spi, ncs).map_err(|e| {
                RtdError::Init(match e {
                    InternalError::GpioFault => "NCS pin setup failed".to_string(),
                    _ => "MAX31865 init failed".to_string(),
                })
            })?;
            inner
                .configure(leads, filter)
                .map_err(|e| RtdError::Init(format!("Configure failed: {:?}", e)))?;

            Ok(RTDReader { inner })
        }

        /// Read temperature in °C as f64 (PT100 lookup).
        pub fn get_temperature(&mut self) -> Result<f64, RtdError> {
            self.inner.read_temperature().map_err(map_internal_error)
        }

        /// Read resistance in ohms as f64.
        pub fn get_resistance(&mut self) -> Result<f64, RtdError> {
            self.inner.read_resistance().map_err(map_internal_error)
        }

        /// Read temperature as scaled integer (degrees Celsius * 100).
        pub fn read_temp_100(&mut self) -> Result<i32, RtdError> {
            self.inner
                .read_default_conversion()
                .map_err(map_internal_error)
        }

        /// Read resistance as scaled integer (ohms * 100).
        pub fn get_ohms_100(&mut self) -> Result<u32, RtdError> {
            self.inner.read_ohms().map_err(map_internal_error)
        }

        /// Read raw RTD value (u16, for testing/low-level).
        pub fn get_raw_data(&mut self) -> Result<u16, RtdError> {
            self.inner.read_raw().map_err(map_internal_error)
        }

        /// Check if an error is a MAX31865 fault (RtdError::Fault variant).
        pub fn is_max_fault(&self, e: &RtdError) -> bool {
            matches!(e, RtdError::Fault(_))
        }

        /// Read fault status (u8 from reg 0x07; auto-clears).
        pub fn read_fault_status(&mut self) -> Result<u8, RtdError> {
            self.inner.read_fault_status().map_err(map_internal_error)
        }

        /// Clear any latched faults (no-op if none).
        pub fn clear_fault(&mut self) -> Result<(), RtdError> {
            self.inner.clear_fault().map_err(|e| {
                RtdError::Read(match e {
                    InternalError::SpiErrorTransfer => "Clear fault SPI write failed".to_string(),
                    _ => "Clear fault failed".to_string(),
                })
            })
        }

        /// Set calibration (ohms * 100, e.g., 40000 for 400Ω).
        pub fn set_calibration(&mut self, calibration: u32) {
            self.inner.set_calibration(calibration);
        }
    }

    /// Map internal low-level errors to public RtdError.
    fn map_internal_error(e: InternalError) -> RtdError {
        match e {
            InternalError::SpiErrorTransfer | InternalError::GpioFault => {
                RtdError::Read("SPI/GPIO transfer failed".to_string())
            }
            InternalError::MAXFault => RtdError::Fault(0), // Placeholder; call read_fault_status() for real status
        }
    }
}

// Re-export RTDReader at root for flat imports (agreed API consistency)
pub use rtd_reader::RTDReader;

// Private module for low-level driver (opaque to users)
mod private {
    use super::*;

    #[derive(Debug)]
    pub enum Error {
        /// Error transferring data to/from Max31865 chip registers
        SpiErrorTransfer,
        /// Error setting the state of a pin in the GPIO bus
        GpioFault,
        /// The Max31865 chip declared an error when converting temperatures.
        /// Use `read_fault_status()` for details.
        MAXFault,
    }

    pub struct Max31865<SPI, NCS> {
        spi: SPI,
        ncs: NCS,
        calibration: u32,
        base_config: u8, // Set in configure
    }

    impl<SPI, NCS> Max31865<SPI, NCS>
    where
        SPI: SpiBus<u8>,
        NCS: OutputPin,
    {
        /// Create a new MAX31865 module (internal use only).
        pub fn new(spi: SPI, mut ncs: NCS) -> Result<Max31865<SPI, NCS>, Error> {
            let default_calibration = 40000;

            ncs.set_high().map_err(|_| Error::GpioFault)?;
            let max31865 = Max31865 {
                spi,
                ncs,
                calibration: default_calibration,
                base_config: 0, // Set in configure
            };

            Ok(max31865)
        }

        // From MAX31865 datasheet (page 16, Table 8):
        //
        // Bit 7 (V_BIAS): 1 = enable bias excitation (should be 1).
        // Bit 6 (1-SHOT): 0 = continuous conversion (ongoing reads),
        //                 1 = one-shot (single conversion, then stop).
        // Bit 5: Reserved (should be 0)
        // Bit 4 (wires): 1 = 3-wire PT100, 0 = 2 or 4-wire
        // Bit 3 (AUTO-CONVERT): 1 = auto-conversion enabled
        // Bit 2: Reserved (should be 0)
        // Bit 1: Reserved (should be 0)
        // Bit 0 (50/60Hz): 0 = 60Hz, 1 = 50 hz

        /// Updates the devices configuration (internal use only).
        pub fn configure(
            &mut self,
            sensor_type_enum: RTDLeads, // From public RTDLeads cast
            filter_mode_enum: FilterHz, // From public FilterHz cast
        ) -> Result<(), Error> {
            // Compute sensor type and filter mode bits directly
            let sensor_type = match sensor_type_enum {
                RTDLeads::Three => 1u8,
                RTDLeads::Two | RTDLeads::Four => 0u8, // Two or Four = 0
            };
            let filter_mode = match filter_mode_enum {
                FilterHz::Fifty => 1u8, // Fifty = 1 (low order bit)
                FilterHz::Sixty => 0u8, // Sixty = 0 (no lower order bits)
            };
            // One-shot config: V_BIAS=1, 1-SHOT=1, wires, filter (no AUTO= bit 3=0)
            self.base_config = (1u8 << 7)  // V_BIAS=1
                | (1u8 << 6)  // 1-SHOT=1 (triggers on write)
                | (sensor_type << 4)  // Wires bit 4
                | filter_mode; // Filter bit 0
            self.write(Register::CONFIG, self.base_config)?; // Initial write (starts first conversion)
            self.clear_fault()?; // Unlatch any boot faults (mimics Adafruit init)

            Ok(())
        }

        /// Clear latched faults (config reg bit 1 = 1)
        pub fn clear_fault(&mut self) -> Result<(), Error> {
            self.write(Register::CONFIG, 0x02)
        }

        /// Read and clear fault status reg (0x07) for bit-level diagnostics (u8 LSB)
        pub fn read_fault_status(&mut self) -> Result<u8, Error> {
            let status = self.read(Register::FAULT_STATUS)?;
            self.clear_fault()?; // Clear after read (if auto-clear needed)
            Ok(status)
        }

        /// Set the calibration reference resistance (internal use only).
        pub fn set_calibration(&mut self, calibration: u32) {
            self.calibration = calibration;
        }

        /// Read the raw resistance value.
        /// The output value is the value in Ohms multiplied by 100.
        pub fn read_ohms(&mut self) -> Result<u32, Error> {
            let raw = self.read_raw()?;
            let ohms = ((raw >> 1) as u32 * self.calibration) >> 15;
            Ok(ohms)
        }

        /// Read resistance in ohms as f64
        pub fn read_resistance(&mut self) -> Result<f64, Error> {
            let ohms_raw = self.read_ohms()?; // u32 *100;
            Ok(ohms_raw as f64 / 100.0)
        }

        /// Read temperature in °C as f64
        pub fn read_temperature(&mut self) -> Result<f64, Error> {
            let temp_raw = self.read_default_conversion()?; // i32 *100
            Ok(temp_raw as f64 / 100.0)
        }

        /// Read the raw resistance value and then perform conversion to degrees Celsius.
        /// The output value is the value in degrees Celsius multiplied by 100.
        pub fn read_default_conversion(&mut self) -> Result<i32, Error> {
            let ohms = self.read_ohms()?;
            let temp = temp_conversion::LOOKUP_VEC_PT100.lookup_temperature(ohms as i32);
            Ok(temp)
        }

        /// Read the raw RTD value.
        /// The raw value is the value of the combined MSB and LSB registers.
        /// The first 15 bits specify the ohmic value in relation to the reference
        /// resistor (i.e. 2^15 - 1 would be the exact same resistance as the reference
        /// resistor). See manual for further information.
        /// The last bit specifies if the conversion was successful.
        pub fn read_raw(&mut self) -> Result<u16, Error> {
            // Trigger new conversion: Write config (1-SHOT=1 starts it)
            self.write(Register::CONFIG, self.base_config)?;

            // Wait for conversion (100ms conservative >65ms datasheet min)
            std::thread::sleep(std::time::Duration::from_millis(100));

            // Read RTD
            let buffer = self.read_two(Register::RTD_MSB)?;
            let raw = ((buffer[0] as u16) << 8) | (buffer[1] as u16);
            if raw & 1 != 0 {
                // Fault: Clear + retry once
                let _ = self.read_fault_status(); // Reads + clears faults
                                                  // Retry: Trigger again
                self.write(Register::CONFIG, self.base_config)?;
                std::thread::sleep(std::time::Duration::from_millis(100));
                let retry_buffer = self.read_two(Register::RTD_MSB)?;
                let retry_raw = ((retry_buffer[0] as u16) << 8) | (retry_buffer[1] as u16);
                if retry_raw & 1 != 0 {
                    return Err(Error::MAXFault); // Retry failed
                }
                return Ok(retry_raw);
            }
            Ok(raw)
        }

        fn read(&mut self, reg: Register) -> Result<u8, Error> {
            let mut read_buffer = [0u8; 2]; // 2 bytes: dummy + data
            let mut write_buffer = [0u8; 2];
            write_buffer[0] = reg.read_address(); // Read addr for reg (e.g., 0x81 for 0x01)
            write_buffer[1] = 0; // Dummy data
            self.ncs.set_low().map_err(|_| Error::GpioFault)?;
            self.spi
                .transfer(&mut read_buffer, &write_buffer)
                .map_err(|_| Error::SpiErrorTransfer)?;
            self.ncs.set_high().map_err(|_| Error::GpioFault)?;
            Ok(read_buffer[1]) // Return result (ignore dummy [0])
        }

        fn read_two(&mut self, reg: Register) -> Result<[u8; 2], Error> {
            // The hardware is full duplex - you have to read and write the same number of bytes.
            // The first byte you write is the register offset, and the remaining
            // bytes are ignored when reading. To read two bytes you write three.
            // The two bytes we read are in the last two of the three bytes read.
            // NOTE: It reads and writes the minimum size of the read and write buffers
            let mut read_buffer = [0u8; 3]; // 3 bytes: dummy + MSB + LSB
            let mut write_buffer = [0u8; 3];
            write_buffer[0] = reg.read_address(); // Read addr for reg (e.g., 0x81 for 0x01)
            write_buffer[1] = 0; // Dummy for MSB
            write_buffer[2] = 0; // Dummy for LSB
            self.ncs.set_low().map_err(|_| Error::GpioFault)?;
            self.spi
                .transfer(&mut read_buffer, &write_buffer)
                .map_err(|_| Error::SpiErrorTransfer)?;
            self.ncs.set_high().map_err(|_| Error::GpioFault)?;
            Ok([read_buffer[1], read_buffer[2]]) // Return MSB, LSB (ignore dummy [0])
        }

        fn write(&mut self, reg: Register, val: u8) -> Result<(), Error> {
            self.ncs.set_low().map_err(|_| Error::GpioFault)?;
            self.spi
                .write(&[reg.write_address(), val])
                .map_err(|_| Error::SpiErrorTransfer)?;
            self.ncs.set_high().map_err(|_| Error::GpioFault)?;
            Ok(())
        }
    }

    #[allow(non_camel_case_types)]
    #[allow(dead_code)]
    #[derive(Clone, Copy)]
    enum Register {
        // All the lovely Max31865 register offsets
        CONFIG = 0x00,
        RTD_MSB = 0x01,
        RTD_LSB = 0x02,
        HIGH_FAULT_THRESHOLD_MSB = 0x03,
        HIGH_FAULT_THRESHOLD_LSB = 0x04,
        LOW_FAULT_THRESHOLD_MSB = 0x05,
        LOW_FAULT_THRESHOLD_LSB = 0x06,
        FAULT_STATUS = 0x07,
    }

    const R: u8 = 0 << 7;
    const W: u8 = 1 << 7;

    impl Register {
        fn read_address(&self) -> u8 {
            *self as u8 | R
        }

        fn write_address(&self) -> u8 {
            *self as u8 | W
        }
    }
}