STM32 MCUs include a built-in temperature sensor wired to a dedicated ADC channel. It’s meant primarily for on-die temperature monitoring (trend/change detection), not precision ambient measurement. With the right sampling time, reference-voltage compensation, and a clean trigger, you can still get stable, repeatable readings that are good enough for system health checks, thermal throttling, and failsafe logic.
This tutorial shows you how to:
Enable the internal temperature sensor and VREFINT channels
Trigger conversions at 50 Hz via TIM3 TRGO
Stream two ADC channels via DMA (circular)
Compensate for VDD changes using the VREFINT reading
Convert VSENSE → °C using the datasheet equation
Calibrate and stabilize results for real projects
⚠️ Always check your exact MCU’s datasheet: the conversion equation and parameters (V25, Avg_Slope) vary by family/line and sometimes by revision.
Table of Contents
What the Internal Temp Sensor Measures
Reading Flow & Conversion Equation
Project Architecture (50 Hz pipeline)
Step-by-Step CubeMX Configuration
HAL Example Code (STM32F103-style)
Calibration & Accuracy Tips
Troubleshooting FAQ
Wrap-Up
1) What the Internal Temp Sensor Measures
The sensor reports a voltage (VSENSE) proportional to the die temperature.
It’s internally connected to a dedicated ADC channel.
A second internal channel (VREFINT) exposes a stable bandgap reference used to estimate actual VDD and correct readings.
It’s excellent for trend detection and thermal protection, but not meant as a lab-grade ambient probe.
2) Reading Flow & Conversion Equation
High-level steps:
Enable TempSensor ADC channel
Set sampling time ≥ 17 µs (per datasheet)
Start ADC (ideally with a timer trigger)
Read VSENSE and VREFINT
Convert VSENSE → temperature using datasheet constants
Typical equation style (family-specific):
Temperature (°C) = ((V25 - VSENSE) / Avg_Slope) + 25
Where:
V25 = sensor output at 25 °C (e.g., ~1.43 V on many F1 parts)
Avg_Slope = mV/°C (e.g., ~4.3 mV/°C on many F1 parts)
VSENSE = computed from raw ADC code with VREFINT-based VDD correction
For devices that provide factory temperature calibration points (e.g., TS_CAL1/TS_CAL2 at known temperatures), prefer those over the generic V25/Avg_Slope constants.
3) Project Architecture (50 Hz pipeline)
We’ll build a stable pipeline with deterministic sampling and voltage compensation:
TIM3 generates TRGO = Update at 50 Hz (period = 20 ms)
ADC1 (regular group) is externally triggered by TRGO
Regular conversions scan two internal channels: VREFINT then TempSensor
DMA (circular) moves both results into memory every trigger
In the ADC conversion complete callback, we flip a GPIO (rate probe) and set a flag
In the main loop, we compute VDD, then VSENSE, then °C, and print via UART (115200)
4) Step-by-Step CubeMX Configuration
MCU/Board: e.g., STM32F103C8 (Blue Pill). The flow applies broadly; names may differ by family.
RCC / Clock
Use HSE (external crystal) → PLL → SYSCLK 72 MHz (typical for F103)
Ensure ADC clock ≤ datasheet limit (e.g., 12 MHz)
ADC1
Regular conversions: 2 channels (VREFINT, TempSensor)
Sampling time: choose the nearest ≥ 17 µs.
Example: at 12 MHz ADC clock, 239.5 cycles ≈ 19.96 µs
External trigger: TIM3 TRGO (Update event)
DMA: Add 1 channel, circular, halfword, memory increment enabled
TIM3
Timer clock source = internal
Set PSC and ARR so Update = 20 ms (50 Hz)
Example at 72 MHz: PSC = 23, ARR = 59999
TRGO = Update Event
USART1
115200 8N1 for logging
GPIO
One output (e.g., PB0) for sampling-rate verification (toggle in ADC ISR)
NVIC
Enable ADC1 global interrupt
5) HAL Example Code (STM32F103-style)
This example uses V25 = 1.43 V and Avg_Slope = 4.3 mV/°C, which are common for many F1 parts. Adjust to your datasheet. If your family provides VREFINT calibration or TS_CAL1/TS_CAL2, prefer those for accuracy.
/*
- Demo: STM32 Internal Temperature Sensor (ADC + DMA + TIM3 TRGO @ 50 Hz)
- Target style: STM32F103 (adjust constants & addresses to your MCU) */ #include "main.h" #include #include
/* === Datasheet Parameters (adjust!) === */
define AVG_SLOPE_mV_per_C (4.3f) // mV/°C
define V_AT_25C_V (1.43f) // V @ 25°C
define VREFINT_TYP_V (1.20f) // Typical internal reference (only if no cal value)
/* HAL handles (CubeMX will generate these) */
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
TIM_HandleTypeDef htim3;
UART_HandleTypeDef huart1;
/* Double buffer: [0] = VREFINT ADC code, [1] = VSENSE ADC code */
static volatile uint16_t adc_buf[2];
static volatile uint8_t new_sample = 0;
/* App state */
static float vref_V = 0.0f;
static float vsense_V = 0.0f;
static float temperature_C = 0.0f;
static char line[48];
/* Prototypes generated by CubeMX */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
static void MX_TIM3_Init(void);
static void MX_USART1_UART_Init(void);
/* === Optional: If your device has VREFINT calibration, read it here ===
- Many non-F1 families define VREFINT_CAL_ADDR & VREFINT_CAL_VREF in headers.
- For plain F1, you may not have this and must use VREFINT_TYP_V. / // #define VREFINT_CAL_ADDR ((uint16_t)0x1FFFxxxx) // family-specific // #define VREFINT_CAL_VREF (3.0f) // e.g., 3.0 V or per docs
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
/* Start 50 Hz trigger */
HAL_TIM_Base_Start(&htim3);
/* Calibrate & start ADC in DMA circular mode /
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t)adc_buf, 2);
for (;;)
{
if (new_sample)
{
/* Compute VDD from VREFINT reading /
/ Without a factory cal value, approximate with typical Vrefint */
const float adc_fullscale = 4095.0f;
const float vrefint_code = (float)adc_buf[0];
const float vsense_code = (float)adc_buf[1];
/* Estimate effective VDD using VREFINT */
/* VREFINT_TYP_V = Vrefint (typical) at nominal VDD, so:
VDD ≈ (VREFINT_TYP_V * adc_fullscale) / ADC[VREFINT] */
float vdd_V = (VREFINT_TYP_V * adc_fullscale) / (vrefint_code > 0.5f ? vrefint_code : 0.5f);
/* Now compute VSENSE in volts using that VDD */
vref_V = vdd_V; // alias for clarity
vsense_V = (vsense_code * vref_V) / adc_fullscale;
/* Convert to temperature (°C). Avg_Slope is in mV/°C */
temperature_C = (((V_AT_25C_V - vsense_V) * 1000.0f) / AVG_SLOPE_mV_per_C) + 25.0f;
/* Print one line per sample (for Serial Plotter/Monitor) */
int n = snprintf(line, sizeof(line), "%.2f\r\n", temperature_C);
HAL_UART_Transmit(&huart1, (uint8_t*)line, (uint16_t)n, 50);
new_sample = 0;
}
}
}
/* ADC end-of-conversion callback: one pair (VREFINT, VSENSE) ready */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Scope this to verify 50 Hz rate
new_sample = 1;
}
}
Notes on accuracy upgrades (when your MCU supports it):
If your device provides VREFINT_CAL (a factory ADC code measured at a known VDD, e.g., 3.0 V), then compute:
VDD = (VREF_KNOWN * VREFINT_CAL_CODE) / ADC[VREFINT]
This removes the approximation of VREFINT_TYP_V.
If your device exposes TS_CAL1 (at ~30 °C) and TS_CAL2 (at ~110 °C), compute the slope from those two points and linearly interpolate the temperature. This is typically more accurate than using V25 + Avg_Slope.
6) Calibration & Accuracy Tips
Use the right sampling time
The temp channel needs ≥ 17 µs. If you sample faster, readings will jitter or skew low.
Compensate VDD with VREFINT
Always read VREFINT alongside VSENSE and correct for VDD changes.
Factory calibration beats typical constants
Prefer TS_CAL1/TS_CAL2 and VREFINT_CAL when your part provides them. They capture per-die variation.
Thermal reality check
The internal sensor reports die temperature. CPU load, flash waits, and DC/DC activity heat the silicon. It will not match a distant ambient probe.
Averaging & rate
A simple moving average (e.g., 8–16 samples) helps. Don’t oversample; 10–50 Hz is plenty for thermal trends.
One-time alignment
If absolute accuracy matters, co-calibrate with a known good external sensor placed near the MCU package and fit offset/slope.
7) Troubleshooting FAQ
Q: My reading is noisy or jumps a lot.
Increase sampling time (e.g., 239.5 cycles).
Average multiple samples.
Is TRGO configured? Software-triggered, irregular sampling can add jitter.
Q: Numbers drift when VDD changes.
You’re likely not using VREFINT compensation. Read VREFINT every cycle.
Q: I get obviously wrong temperatures (e.g., –20 °C at room).
Check channel order (VREFINT vs TempSensor).
Verify reference equation constants (V25, Avg_Slope) match your MCU.
Confirm ADC clock/dividers and resolution.
Q: How do I validate 50 Hz sampling?
Toggle a GPIO in HAL_ADC_ConvCpltCallback() and measure on a scope. You should see 20 ms between edges.
Q: Can I do this without DMA?
Yes, poll or interrupt per conversion, but DMA keeps the CPU free and guarantees deterministic double-channel reads.
8) Wrap-Up
With timer-triggered ADC, DMA, and VREFINT compensation, STM32’s internal temperature sensor becomes a reliable trend monitor for thermal management and safety logic. For tighter absolute numbers, lean on factory calibration points (when present) and perform a quick in-system alignment against a trusted external probe.
If you want, I can also provide an LL-driver version, a FreeRTOS task pattern, or a CSV logger to the serial port so you can chart measurements over time.
Top comments (0)