Reading STM32F0 internal temperature and voltage using ChibiOS

The STM32F0 series of 32-bit ARM Cortex M0 microcontrollers contain a huge number of internal peripherals despite their low price starting at 0,32€ @1pc. Amongst them is an internal, factory-calibrated temperature sensor and a supply voltage sensor (that specifically senses VDDA, the analog supply voltage rail) connect to channels 16 and 17 of the internal ADC.

While I usually like the STM32 documentation, it was quite hard to implement code that produced realistic values. While the STM32F0 reference manual contains both formulas and a short section of example code, I believe that some aspects of the calculation are understated in the computation:

Section 13.9 in RM0091 provides a formula for computing the temperature from the raw temperature sensor output and the factory calibration values. However it is not stated anywhere (at least in Rev7, the current RM0091 revision) that this formula is only correct for a VDDA of exactly 3.30V.

The example code in section A.7.16 uses a normalization factor VDD_APPLI / VDD_CALIB that is applied directly to the temperature sensor raw output. However, VDD_APPLI is not documented anywhere and define to 300 while VDDA_CALIB is defined to 330.

After some experimentation I found out that you need to actually continously measure the supply voltage in order to properly normalize the temperature output. By doing this, you can normalize the raw temperature sensor output. When running at VDDA=3.0V, the computed temperature sensor output without applying said normalization is about -14°C for my test circuit while actually being at about 18°C.

Computing the internal supply voltage is essentially free when also computing the temperature because the VDD_APPLI / VDDA_CALIB ratio is required for accurate temperature monitoring anyway. Therefore, my code also contains functionality to get both the temperature sensor output and the voltage sensor output at the same time.

In contrast to the RM0091 example, my code computes the temperature in m°C, yielding a resolution three orders of magnitude higher than the ST code example. However, I suspect there are still inaccuracies introduced by inappropriate ordering of integral arithmetic operations.

In order to provide a well-documented reference, I implemented an easy-to-use codebase that continously senses both temperature and voltage at the same time. In order to provide high interoperability, I implemented the hardware-specific part using the ChibiOS HAL.

The code was tested on a custom board utilizing the STM32F030F4 (that is, at the time of writing this, the cheapest STM32microcontroller I could find) and checked for accuracy using the Fluke 289 multimeter. For other microcontrollers in the STM32F0 series, the addresses for the three calibration values might need to be adjusted (although I suspect they are the same at least for a series of microcontrollers). Larger controllers like the STM32F4 use a slightly different ADC configuration. Maybe I’ll create some hybrid code that also works for other families in the future.

The header file contains extensive documentation on the implementation details of the code. Note that without modifying the code, it is not possible to continously monitor the temperature/VDDA and use other ADC channels in the same program.

/**
 * STM32F0 internal temperature sensor readout utility
 * Tested on custom STM32F030F4 board.
 * 
 * The STM32F0 ADC is used to continously convert
 * temperature sensor data and VREFINT data and DMA-copy
 * them to a static memory array.
 * In order to avoid spending significant amounts of time in
 * interrupt handlers, this implementation does not compute
 * the temperature in interrupt handlers. Instead,
 * the user must call readTemperatureData() in order to evaluate
 * the samples currently stored in the ADC and convert them to the temperature
 * value. Due to this strategy only the samples being present in the
 * ADC DMA buffer are averaged (yielding lower averaging sample count for
 * infrequent calls to readTemperatureData()) but the code does not require
 * CPU cycles (beyond those used by ChibiOS internals) inbetween calls to
 * readTemperatureData().
 *
 * In contrast to the ST example in RM0091, this code computes the temperature
 * in millicelsius using integer operations only.
 *
 * This implementation measures the VDDA analog supply voltage
 * quasi-synchronously to the temperature. Therefore,
 * no fixed or absolutely stable VDDA is required for proper operation
 * (tested with 3.0V and 3.3V). However, significant noise on the VDDA
 * line might also induce noise on the temperature output if
 * the sample buffer size is not large enough.
 *
 * Additionally, readCurrentVDDA() provides an utility method
 * to read the current VDDA voltage as millivolts.
 *
 * By using readTemperatureVDDA(), the user is able to compute both
 * the temperature and VDDA with only a single iteration over the sample
 * array. The actual VDDA is required for temperature normalization,
 * so returning it does not incur significant overhead.
 *
 * Designed for use with the ChibiOS HAL driver:
 * http://chibios.org
 * 
 * Revision 1.0
 *
 * Copyright (c) 2015 Uli Koehler
 * http://techoverflow.net
 * Released under Apache License v2.0
 */
#ifndef __STM32F0_TEMPERATURE_H
#define __STM32F0_TEMPERATURE_H

#include <stdint.h>

/*
 * Register addresses were taken from DM00088500 (STM32F030 datasheet)
 * For non-STM32F030 microcontrollers register addresses
 * might need to be modified according to the respective datasheet.
 */
//Temperature sensor raw value at 30 degrees C, VDDA=3.3V
#define TEMP30_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7B8))
//Temperature sensor raw value at 110 degrees C, VDDA=3.3V
#define TEMP110_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7C2))
//Internal voltage reference raw value at 30 degrees C, VDDA=3.3V
#define VREFINT_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7BA))

/**
 * Configuration:
 * Defines the size of the ADC sample array.
 * Defining a larger value here will significantly increase
 * the amount of static RAM usage, but more values
 * will be used for averaging, leading to lower noise.
 */
#define ADC_TMPGRP_BUF_DEPTH 96

/**
 * Initialize the ADC and start continous sampling and DMA copying
 * of both temperature data and vrefint data.
 */
void initializeTemperatureADC(void);

/**
 * Compute the current temperature by evaluating the currently stored ADC samples.
 * @return The millicelsius absolute temperature, e.g. 12345 for 12.345 degrees C
 */
int32_t readTemperatureData(void);

/**
 * Compute the current VDDA by evaluating the stored ADC samples.
 * @return The absolute VDDA in mV, e.g. 3234 for 3.234V
 */
int16_t readCurrentVDDA(void);

/**
 * Data type that stores both a temperature and 
 */
typedef struct {
    //Temperature in millidegrees C
    int32_t temperature;
    //Analog supply voltage in mV
    int32_t vdda;   
} TemperatureVDDAResult;

/**
 * Compute both the temperature and the VDDA at once by
 * evaluating the stored ADC sample array
 */
TemperatureVDDAResult readTemperatureVDDA(void);


#endif //__STM32F0_TEMPERATURE_H
#include "STM32F0Temperature.h"

#include <ch.h>
#include <hal.h>

#define ADC_TEMPGRP_NUM_CHANNELS   2 /* Temperature, VRefint */

/**
 * ADC sample buffer
 */
static adcsample_t temperatureVRefSamples[ADC_TEMPGRP_NUM_CHANNELS * ADC_TMPGRP_BUF_DEPTH];

/**
 * Continous 12-bit conversion of both CH16 (temperature)
 * and CH17 (VREFINT) without callbacks.
 * Uses the slowest possible sample rate in order to reduce
 * noise as much as possible.
 */
static const ADCConversionGroup adcTemperatureGroup = {
  TRUE,
  ADC_TEMPGRP_NUM_CHANNELS,
  NULL,
  NULL,
  ADC_CFGR1_CONT | ADC_CFGR1_RES_12BIT,             /* CFGR1 */
  ADC_TR(0, 0),                                     /* TR */
  ADC_SMPR_SMP_239P5,                               /* SMPR */
  ADC_CHSELR_CHSEL16 | ADC_CHSELR_CHSEL17           /* CHSELR */
};

void initializeTemperatureADC(void) {
    adcStart(&ADCD1, NULL);
    ADC->CCR |= ADC_CCR_TSEN | ADC_CCR_VREFEN;
    adcStartConversion(&ADCD1, &adcTemperatureGroup, temperatureVRefSamples, ADC_TMPGRP_BUF_DEPTH);
}

TemperatureVDDAResult readTemperatureVDDA(void) {
    //NOTE: Computation is performed in 32 bits, but result is converted to 16 bits later.s
    /**
     * Compute average of temperature sensor raw output
     * and vrefint raw output
     */
    int32_t tempAvg = 0;
    int32_t vrefintAvg = 0;
    //Samples are alternating: temp, vrefint, temp, vrefint, ...
    for(int i = 0; i < (ADC_TMPGRP_BUF_DEPTH * ADC_TEMPGRP_NUM_CHANNELS); i += 2) {
        tempAvg += temperatureVRefSamples[i];
        vrefintAvg += temperatureVRefSamples[i + 1];
    }
    tempAvg /= ADC_TMPGRP_BUF_DEPTH;
    vrefintAvg /= ADC_TMPGRP_BUF_DEPTH;
    /**
     * Compute temperature in millicelius.
     * Note that we need to normalize the value first by applying
     * the (actual VDDA / VDDARef) ratio.
     *
     * Note: VDDA_Actual = 3.3V * VREFINT_CAL / vrefintAvg
     * Therefore, the ratio mentioned above is equal to
     * q = VREFINT_CAL / vrefintAvg
     */
    //See RM0091 section 13.9
    int32_t temperature = ((tempAvg * (*VREFINT_CAL_ADDR)) / vrefintAvg) - (int32_t) *TEMP30_CAL_ADDR;
    temperature *= (int32_t)(110000 - 30000);
    temperature = temperature / (int32_t)(*TEMP110_CAL_ADDR - *TEMP30_CAL_ADDR);
    temperature += 30000;

    TemperatureVDDAResult ret = {
        temperature,
        (int16_t)((3300 * (*VREFINT_CAL_ADDR)) / vrefintAvg)
    };
    return ret;
}

int32_t readTemperatureData(void) {
    return readTemperatureVDDA().temperature;
}

int16_t readCurrentVDDA(void) {
    return readTemperatureVDDA().vdda;
}