Porting¶
Run the driver on a new MCU by writing one class: the ADC interface.
The driver never calls your SDK directly. It calls seven methods, plus one optional hook, on a class you derive from
ntc::AdcInterface<Self> (in inc/ntc_adc_interface.hpp). The base class uses
CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to
allocate.
Figure description and sources
From top to bottom. Your application talks to one NtcThermistor
Drawn from: inc/ntc_thermistor.hpp NtcThermistor public calls; inc/ntc_adc_interface.hpp AdcInterface; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Contract |
|---|---|
IsInitialized() const |
true once the ADC is ready |
EnsureInitialized() |
Bring the ADC up if needed; true on success. Called by Initialize() |
IsChannelAvailable(channel) const |
true for a channel this port can read |
ReadChannelV(channel, &volts) |
One conversion, as the voltage at the pin in volts. Used for every temperature |
ReadChannelCount(channel, &count) |
One conversion, as the raw count. Used only by GetRawAdcValue() |
GetReferenceVoltage() const |
The ADC's full-scale voltage. Not used by the driver today |
GetResolutionBits() const |
The ADC's resolution. Not used by the driver today |
DelayMs(ms) (optional) |
Wait at least ms milliseconds (an RTOS sleep). Used between averaged samples when sample_delay_ms is set; without it the samples are taken back to back |
The read methods return ntc::AdcError: Success, or NotInitialized, InvalidChannel, ReadFailed,
Timeout, HardwareError. The driver maps them to NtcError (API guide).
The port¶
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;
};
Rules for ReadChannelV():
- Return calibrated volts. The driver's accuracy is the accuracy of this number. Use your SDK's ADC
calibration (on ESP32,
adc_cali_raw_to_voltage()), notcount × V_full / 4095. - One conversion per call. The driver averages
sample_countcalls itself. - Block until the result is ready, or return
Timeout. - Use an input range that covers the node. With a 3.3 V divider the node spans about 0.1 V to 3.1 V over −40 °C to +150 °C; pick the attenuation or gain that covers it.
Reference implementation¶
The ESP32 example's port, esp32_ntc_adc.hpp, is a complete
ESP-IDF port. EnsureInitialized() creates the ADC1 unit with adc_oneshot_new_unit(), configures the channel
at 12 dB attenuation (about 0 V to 3.3 V, the whole divider) with adc_oneshot_config_channel() and creates an
adc_cali curve-fitting scheme (line fitting on chips without it). ReadChannelV() reads with
adc_oneshot_read() and converts with adc_cali_raw_to_voltage(). DelayMs() sleeps with vTaskDelay().
Its component needs REQUIRES esp_adc.
Check your port¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/my_adc.hpp, with the driver instantiated over it. - Replace the thermistor with a 10 kΩ 0.1 % resistor:
GetVoltage()returns half the supply andReadTemperatureCelsius()returns 25 °C within a few tenths of a degree, set by the two resistors and the ADC calibration. - Short the node to ground:
ConversionFailed. Open the thermistor:ConversionFailed, orTemperatureOutOfRangeif ADC noise keeps the node just under the supply.
Threads¶
The driver is not thread-safe. Give each NtcThermistor one owner task, or guard every call on a driver object
(and its ADC port) with one mutex. Several thermistors on one port need that mutex around the port too.