Skip to content

Quickstart

Read a temperature from your MCU: wire a divider, write a small ADC port class, call two driver functions.

1. Wire it

The thermistor sits between the ADC node and ground; the series resistor between the supply and the node.

Voltage divider: V_REF through the series resistor to the ADC node, the NTC thermistor from the node to
ground, and an optional filter capacitor from the node to ground

From To Notes
3.3 V R_SERIES 10 kΩ, 1 % or better; the supply value goes in reference_voltage
R_SERIES ADC input The divider node
ADC input NTC NTCG163JF103FT1S or any 10 kΩ NTC
NTC GND Same ground as the ADC

Keep the node voltage inside the ADC's input range. With 3.3 V and 10 kΩ / 10 kΩ it is 1.65 V at 25 °C and moves toward 0 V as the sensor heats. The Hardware page covers resistor choice, filtering and self-heating.

2. Add the driver

Add the repository as a submodule and link the hf_ntc_thermistor CMake target. Installation covers CMake and ESP-IDF.

git submodule add https://github.com/hardfoc/hf-ntc-thermistor-driver.git external/hf-ntc-thermistor-driver

3. Write the port

The port is the only platform code. It brings the ADC up and returns a channel's voltage and raw count. Replace the two platform_* calls with your SDK's.

my_adc.hpp
class MyAdc : public ntc::AdcInterface<MyAdc> {
public:
  static constexpr uint8_t kChannels = 7;       // ADC1 channels 0..6 on this MCU
  static constexpr uint8_t kBits = 12;
  static constexpr float kFullScaleV = 3.3F;    // input range at the chosen attenuation

  [[nodiscard]] bool IsInitialized() const { return ready_; }

  // Bring the ADC up once; later calls return true at once.
  bool EnsureInitialized() {
    if (!ready_) {
      ready_ = platform_adc_init();
    }
    return ready_;
  }

  [[nodiscard]] bool IsChannelAvailable(uint8_t channel) const { return channel < kChannels; }

  ntc::AdcError ReadChannelCount(uint8_t channel, uint32_t* count) {
    uint32_t mv = 0;
    return read(channel, count, &mv);
  }

  // The driver reads volts: return the calibrated voltage at the pin, not count * Vref / 4095.
  ntc::AdcError ReadChannelV(uint8_t channel, float* voltage_v) {
    uint32_t raw = 0;
    uint32_t mv = 0;
    const ntc::AdcError err = read(channel, &raw, &mv);
    if (err == ntc::AdcError::Success) {
      *voltage_v = static_cast<float>(mv) / 1000.0F;
    }
    return err;
  }

  [[nodiscard]] float GetReferenceVoltage() const { return kFullScaleV; }
  [[nodiscard]] uint8_t GetResolutionBits() const { return kBits; }

  // Optional: the driver waits sample_delay_ms between averaged samples through this.
  void DelayMs(uint32_t ms) { platform_delay_ms(ms); }

private:
  ntc::AdcError read(uint8_t channel, uint32_t* raw, uint32_t* mv) {
    if (!ready_) {
      return ntc::AdcError::NotInitialized;
    }
    if (!IsChannelAvailable(channel)) {
      return ntc::AdcError::InvalidChannel;
    }
    if (!platform_adc_read(channel, *raw, *mv)) {
      return ntc::AdcError::ReadFailed;
    }
    return ntc::AdcError::Success;
  }

  bool ready_ = false;
};

Return the calibrated voltage at the pin from ReadChannelV(): the driver computes the resistance from it. Porting explains each method.

4. Read the temperature

main.cpp
#include "ntc_thermistor.hpp"

#include <cstdio>

int read_board_temperature(MyAdc& adc, float& celsius) {
  ntc_config_t cfg = GetDefaultNtcConfig();   // NTCG163JF103FT1S: 10 kOhm at 25 C, B 3435 K
  cfg.adc_channel = 2;                        // 1. the ADC input on the divider node
  cfg.series_resistance = 10000.0F;           // 2. the resistor from the supply to the node
  cfg.reference_voltage = 3.3F;               // 3. the supply of the divider

  NtcThermistor<MyAdc> ntc(cfg, &adc);
  if (!ntc.Initialize()) {                    // 4. ADC up, channel present, config valid
    return -1;
  }
  const NtcError err = ntc.ReadTemperatureCelsius(&celsius);  // 5. volts -> ohms -> C
  if (err != NtcError::Success) {
    std::printf("NTC: %s\n", NtcThermistor<MyAdc>::GetErrorString(err));
    return -2;
  }
  return 0;
}

What each step does:

  1. adc_channel selects the input your port reads.
  2. series_resistance is the resistor between the supply and the node, in ohms.
  3. reference_voltage is the divider's supply voltage. The driver solves RNTC = RSERIES · V / (VREF − V) with it.
  4. Initialize() checks the configuration, brings the ADC up and checks the channel.
  5. ReadTemperatureCelsius() reads the voltage, converts it and checks the result against min_temperature / max_temperature (−40 °C to +125 °C by default).

This example is built by the host CI, so it compiles against the current headers.

If it does not work

Symptom First thing to check
Initialize() returns false EnsureInitialized() and IsChannelAvailable() in your port; a zero or negative value in the config
Temperature conversion failed The node reads 0 V (thermistor shorted) or the supply (thermistor open)
A few degrees off at every temperature reference_voltage or series_resistance differ from the board
Reading moves the wrong way Thermistor and series resistor swapped (Hardware)

Next

  • API guide: averaging, filtering, calibration and error handling.
  • Configuration: every field of ntc_config_t, and other thermistors.
  • Conversion methods: how accurate the Beta equation is for this part.