DEV Community

ai pics
ai pics

Posted on

STM32 Internal Temperature Sensor Reading (With DMA + Timer Trigger) — Complete Guide & Example Code

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;
}
Enter fullscreen mode Exit fullscreen mode

}
}

/* 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)