Skip to content

Portable C MAX31865 API

The first device-specific portable C API is declared in c/include/rtd_acquire/max31865.h. It covers configuration/register encoding, native RTD/fault-register decoding, and fault-checked one-shot acquisition through the portable SPI and blocking-delay HALs.

rtd_acquire_max31865_result_t

Introduced in: rtd-acquire 0.2.0

Public MAX31865 operations use a discriminated result enum instead of collapsing all failures into one Boolean:

typedef enum {
    RTD_ACQUIRE_MAX31865_RESULT_OK = 0,
    RTD_ACQUIRE_MAX31865_RESULT_INVALID_ARGUMENT = 1,
    RTD_ACQUIRE_MAX31865_RESULT_CONFIGURATION_ERROR = 2,
    RTD_ACQUIRE_MAX31865_RESULT_INSUFFICIENT_STORAGE = 3,
    RTD_ACQUIRE_MAX31865_RESULT_SPI_IO_ERROR = 4,
    RTD_ACQUIRE_MAX31865_RESULT_DELAY_ERROR = 5,
    RTD_ACQUIRE_MAX31865_RESULT_INTERNAL_ERROR = 6
} rtd_acquire_max31865_result_t;

The categories deliberately keep API/execution failures separate from Measurement diagnostics:

  • INVALID_ARGUMENT is programmer misuse such as a required null output pointer;
  • CONFIGURATION_ERROR means caller configuration is invalid or unsupported;
  • INSUFFICIENT_STORAGE means caller-owned result arrays cannot preserve the complete diagnostic/evidence result;
  • SPI_IO_ERROR and DELAY_ERROR are acquisition-operation failures from the HAL-driven sequence; and
  • INTERNAL_ERROR is a defensive library-invariant failure, not a device diagnostic.

SPI-setting incompatibility belongs to CONFIGURATION_ERROR; a transfer that fails after configuration has been accepted belongs to SPI_IO_ERROR. The SPI and delay categories are the portable-C counterparts of Python AcquisitionError at a more specific cause level.

The pure Boolean rtd_acquire_max31865_config_is_valid() remains a predicate: it answers only whether one static device configuration is valid and does not represent execution of an acquisition operation.

rtd_acquire_max31865_config_t

Introduced in: rtd-acquire 0.2.0

typedef struct {
    rtd_acquire_real_t reference_resistance_ohms;
    uint8_t wire_count;
    uint8_t filter_frequency_hz;
    bool has_low_fault_threshold;
    rtd_acquire_real_t low_fault_threshold_ohms;
    bool has_high_fault_threshold;
    rtd_acquire_real_t high_fault_threshold_ohms;
} rtd_acquire_max31865_config_t;

The structure mirrors the electrical semantics of Python MAX31865Config. The threshold presence flags distinguish an omitted threshold from a real zero-ohm value; zero is valid only for the low threshold.

rtd_acquire_max31865_config_is_valid()

bool rtd_acquire_max31865_config_is_valid(
    const rtd_acquire_max31865_config_t *config
);

Returns true only when the configuration satisfies the MAX31865 contract:

  • reference resistance is finite and from 350 through 10,000 ohms;
  • wire_count is 2, 3, or 4;
  • filter_frequency_hz is 50 or 60;
  • present thresholds are finite and below the reference resistance;
  • low threshold may be zero, while high threshold must be greater than zero;
  • low threshold is below high threshold when both are present; and
  • a high threshold can be directionally encoded without rounding downward.

rtd_acquire_max31865_base_config_byte()

rtd_acquire_max31865_result_t rtd_acquire_max31865_base_config_byte(
    const rtd_acquire_max31865_config_t *config,
    uint8_t *value
);

Validates the configuration and writes only the static configuration bits: three-wire compensation and 50 Hz filtering. BIAS, one-shot, fault-cycle, and fault-clear bits are operational state and are intentionally excluded.

A null required pointer returns INVALID_ARGUMENT; an invalid static configuration returns CONFIGURATION_ERROR.

rtd_acquire_max31865_encode_threshold_registers()

rtd_acquire_max31865_result_t
rtd_acquire_max31865_encode_threshold_registers(
    const rtd_acquire_max31865_config_t *config,
    uint16_t *high_threshold_register,
    uint16_t *low_threshold_register
);

Valid configurations encode to complete 16-bit MAX31865 threshold-register values. Omitted thresholds use 0xFFFF for high and 0x0000 for low. Low thresholds round downward and high thresholds round upward so quantization does not move the native threshold inside the requested diagnostic-free window.

The shared conformance/v1/max31865_threshold_encoding.json vectors execute against this C implementation as well as the independent Python implementation.

Native measurement decoding

Introduced in: rtd-acquire 0.2.0

#define RTD_ACQUIRE_MAX31865_MAX_DIAGNOSTICS 6U
#define RTD_ACQUIRE_MAX31865_MAX_NATIVE_EVIDENCE 6U

rtd_acquire_max31865_result_t
rtd_acquire_max31865_measurement_from_registers(
    const rtd_acquire_max31865_config_t *config,
    uint16_t rtd_register,
    uint8_t fault_status_register,
    rtd_acquire_measurement_t *measurement
);

The decoder converts one native RTD register and one fault-status register into the caller-owned C Measurement contract. It performs no allocation. A caller that uses the two MAX31865-specific maximum constants for its fixed arrays can represent every documented D7-through-D2 native fault bit simultaneously.

Smaller arrays are allowed. If they cannot preserve the complete result, the function returns INSUFFICIENT_STORAGE before modifying the measurement. A malformed storage binding, such as nonzero capacity with a null array pointer, returns INVALID_ARGUMENT instead of being confused with capacity exhaustion.

D7 and D6 map to normalized threshold warnings and retain a trustworthy resistance. D5 through D2 map to FAULT diagnostics and therefore produce a measurement with no resistance or uncertainty. Each diagnostic preserves the datasheet bit identifier and message as a non-owning NativeEvidence record. Reserved fault-status bits D1 and D0 are ignored, including when they are set alongside a documented warning/fault bit.

For a non-fault result, resistance is calculated from the 15-bit RTD ADC code and configured reference resistance. The RTD register's least-significant fault indicator bit is not part of that ADC code.

The shared conformance/v1/max31865.json measurement-decode vectors run against this implementation as well as Python. Under the frozen python-binary64-c-binary32 profile, nonzero resistance allows at most 2^-22 relative difference and expected zero remains exact. The vector set includes a non-binary32-exact reference value to exercise this comparison path. Status, resistance presence, diagnostics, native evidence, and native integer fields remain exact.

One-shot acquisition

Introduced in: rtd-acquire 0.2.0

#define RTD_ACQUIRE_MAX31865_DEFAULT_INPUT_FILTER_TIME_CONSTANT_US 1000U

typedef struct {
    uint32_t input_filter_time_constant_us;
} rtd_acquire_max31865_timing_t;

rtd_acquire_max31865_result_t rtd_acquire_max31865_read(
    const rtd_acquire_spi_t *spi,
    const rtd_acquire_delay_t *delay,
    const rtd_acquire_max31865_config_t *config,
    const rtd_acquire_max31865_timing_t *timing,
    rtd_acquire_measurement_t *measurement
);

The timing structure carries the external input-filter time constant as a whole number of microseconds. The default is 1000 us. If the physical time constant is not an integral number of microseconds, round it upward before supplying it so the required settle interval cannot be shortened by representation.

The driver computes and requests these minimum waits through rtd_acquire_delay_t:

  • bias settling: ceil(10.5 * tau_us) + 1000 us;
  • automatic fault cycle: 600 us;
  • post-fault settling: 5 * tau_us + 1000 us; and
  • one-shot conversion: 55,000 us at 60 Hz or 66,000 us at 50 Hz.

Timing values whose derived waits cannot fit in uint32_t are rejected with CONFIGURATION_ERROR before any SPI transaction occurs.

Before I/O, the function also validates the effective SPI settings. MAX31865 requires CPHA=1, a positive clock no faster than 5 MHz, MSB-first 8-bit words, and active-low chip select; CPOL may be 0 or 1. Incompatible settings return CONFIGURATION_ERROR. A null HAL callback or other required null pointer returns INVALID_ARGUMENT.

A successful read restores threshold registers, enables VBIAS while clearing latched faults, performs the documented settling and automatic fault cycle, triggers one conversion, reads RTD data and Fault Status when required, and then writes the static base configuration to turn VBIAS off before decoding the native registers. Operational register addresses and command-bit masks are private implementation details rather than public configuration fields.

SPI transfer failure returns SPI_IO_ERROR; delay callback failure returns DELAY_ERROR. Once the biased sequence has started, either failure causes a best-effort attempt to write the static base configuration and turn VBIAS off. Failure of that cleanup attempt does not replace the original error. Failure of the normal final bias-off write returns SPI_IO_ERROR.

The caller-owned Measurement is not rewritten for SPI, delay, or final bias-off failure because decoding happens only after normal I/O shutdown. Caller storage may be smaller than the MAX31865 worst-case constants; if the actual native fault state needs more storage than was supplied, the function returns INSUFFICIENT_STORAGE after acquisition and leaves the previous measurement contents untouched rather than truncating diagnostics or evidence.