[internal_temperature] Read the RP2 on-die sensor directly instead of via the Arduino API (#18262)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Jesse Hills
2026-08-12 21:42:05 +12:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent 58a42fe5c2
commit 3f490fe1ed
@@ -3,17 +3,76 @@
#include "esphome/core/log.h"
#include "internal_temperature.h"
#include "Arduino.h"
#include <cmath>
#include <hardware/adc.h>
#include <pico/time.h>
// The RP2 variant headers (pulled in transitively by Arduino.h) define
// ADC_RESOLUTION as the pin-level ADC bit count, which would be substituted
// into the constant below. Nothing here uses the Arduino definition, so drop
// it for this file. Not restored with pop_macro: the uses below would then be
// substituted again.
#undef ADC_RESOLUTION
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.rp2";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
// than four.
//
// This deliberately does not use the SDK's ADC_TEMPERATURE_CHANNEL_NUM. That
// derives from NUM_ADC_CHANNELS, which <pico.h> settles from a board header, and
// arduino-pico supplies a fixed B-die one for every RP2350 build. The real die
// is only declared later, by the variant's pins_arduino.h, so the SDK constant
// reads 8 on A-die boards. PICO_RP2350A itself is correct by the time this file
// is compiled, on both arduino-pico and pico-sdk builds.
#if defined(PICO_RP2350) && !defined(PICO_RP2350A)
#error "PICO_RP2350A is not defined, so the RP2350 die is unknown and the temperature ADC channel cannot be chosen"
#endif
#if defined(PICO_RP2350) && !PICO_RP2350A
static constexpr uint8_t TEMPERATURE_ADC_INPUT = 8;
#else
static constexpr uint8_t TEMPERATURE_ADC_INPUT = 4;
#endif
static constexpr float ADC_VREF = 3.3f;
static constexpr float ADC_RESOLUTION = 4096.0f; // 12-bit
// RP2040 datasheet 4.9.5 / RP2350 datasheet 12.4.6: T = 27 - (V - 0.706) / 0.001721
static constexpr float TEMPERATURE_AT_REFERENCE = 27.0f;
static constexpr float REFERENCE_VOLTAGE = 0.706f;
static constexpr float VOLTS_PER_DEGREE = 0.001721f;
// The sensor is powered down again after each read, so every conversion is the
// first one after enabling. Let the bias circuitry settle first, matching what
// the adc component does for its own temperature readings.
static constexpr uint32_t SETTLE_TIME_US = 1000;
static float read_internal_temperature() {
// adc_init() resets the ADC block, so this runs at most once for this
// component. The adc component guards its own adc_init() the same way, so a
// redundant reset is still possible when both are used. That is harmless
// because both re-select their input on every read.
static bool adc_ready = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
if (!adc_ready) {
adc_init();
adc_ready = true;
}
adc_set_temp_sensor_enabled(true);
busy_wait_us(SETTLE_TIME_US);
adc_select_input(TEMPERATURE_ADC_INPUT);
const uint16_t raw = adc_read();
adc_set_temp_sensor_enabled(false);
const float voltage = raw * (ADC_VREF / ADC_RESOLUTION);
return TEMPERATURE_AT_REFERENCE - (voltage - REFERENCE_VOLTAGE) / VOLTS_PER_DEGREE;
}
void InternalTemperatureSensor::update() {
float temperature = NAN;
bool success = false;
temperature = analogReadTemp();
temperature = read_internal_temperature();
success = (temperature != 0.0f);
if (success && std::isfinite(temperature)) {