STM32F0 interne Temperatur und Spannung mit ChibiOS auslesen

English Deutsch

Die STM32F0-Serie von 32-Bit-ARM-Cortex-M0-Mikrocontrollern enthält trotz ihres niedrigen Preises ab 0,32€ @1 Stück eine große Anzahl interner Peripheriegeräte. Darunter befinden sich ein interner, werkskalibrierter Temperatursensor und ein Versorgungsspannungssensor (der speziell VDDA erfasst, die analoge Versorgungsspannung), die an die Kanäle 16 und 17 des internen ADC angeschlossen sind.

Obwohl ich die STM32-Dokumentation normalerweise schätze, war es ziemlich schwierig, Code zu implementieren, der realistische Werte lieferte. Während das STM32F0-Referenzhandbuch sowohl Formeln als auch einen kurzen Beispielcode-Abschnitt enthält, glaube ich, dass einige Aspekte der Berechnung in der Dokumentation unterrepräsentiert sind:

Abschnitt 13.9 in RM0091 liefert eine Formel zur Berechnung der Temperatur aus dem Rohausgang des Temperatursensors und den Werkseichwerten. Es wird jedoch nirgendwo erwähnt (zumindest nicht in Rev7, der aktuellen RM0091-Revision), dass diese Formel nur für ein VDDA von exakt 3,30V korrekt ist.

Der Beispielcode in Abschnitt A.7.16 verwendet einen Normierungsfaktor VDD_APPLI / VDD_CALIB, der direkt auf den Rohausgang des Temperatursensors angewendet wird. VDD_APPLI ist jedoch nirgendwo dokumentiert und als 300 definiert, während VDDA_CALIB als 330 definiert ist.

Nach einigem Experimentieren fand ich heraus, dass man die Versorgungsspannung tatsächlich kontinuierlich messen muss, um den Temperatursensorausgang ordnungsgemäß zu normieren. Bei VDDA=3.0V liegt der berechnete Temperatursensorausgang ohne Anwendung der Normierung bei etwa -14°C für meine Testschaltung, während er tatsächlich bei etwa 18°C liegt.

Die Berechnung der internen Versorgungsspannung ist im Wesentlichen kostenlos, wenn man auch die Temperatur berechnet, da das Verhältnis VDD_APPLI / VDDA_CALIB ohnehin für genaue Temperaturüberwachung erforderlich ist. Daher enthält mein Code auch Funktionalität, um sowohl den Temperatursensorausgang als auch den Spannungssensorausgang gleichzeitig zu erhalten.

Im Gegensatz zum RM0091-Beispiel berechnet mein Code die Temperatur in m°C, was eine um drei Größenordnungen höhere Auflösung als das ST-Codebeispiel liefert. Ich vermute jedoch, dass es noch Ungenauigkeiten gibt, die durch ungeeignete Reihenfolge der ganzzahligen Arithmetik-Operationen eingeführt werden.

Um eine gut dokumentierte Referenz bereitzustellen, implementierte ich eine einfach zu verwendende Codebasis, die kontinuierlich sowohl Temperatur als auch Spannung gleichzeitig erfasst. Um hohe Interoperabilität zu gewährleisten, implementierte ich den hardwarespezifischen Teil mit dem ChibiOS-HAL.

Der Code wurde auf einem benutzerdefinierten Board mit dem STM32F030F4 getestet (der zum Zeitpunkt der Erstellung dieses Textes der günstigste STM32-Mikrocontroller war, den ich finden konnte) und mit dem Fluke 289-Multimeter auf Genauigkeit überprüft. Für andere Mikrocontroller der STM32F0-Serie müssen die Adressen für die drei Kalibrierwerte möglicherweise angepasst werden (obwohl ich vermute, dass sie zumindest für eine Serie von Mikrocontrollern gleich sind). Größere Controller wie der STM32F4 verwenden eine etwas andere ADC-Konfiguration. Vielleicht erstelle ich in Zukunft Hybrid-Code, der auch für andere Familien funktioniert.

Die Header-Datei enthält umfangreiche Dokumentation zu den Implementierungsdetails des Codes. Beachte, dass es ohne Modifikation des Codes nicht möglich ist, kontinuierlich Temperatur/VDDA zu überwachen und gleichzeitig andere ADC-Kanäle im gleichen Programm zu verwenden.

STM32F0Temperature.h
/**
 * STM32F0 internen Temperatursensor-Auslese-Utility
 * Getestet auf benutzerdefiniertem STM32F030F4-Board.
 *
 * Der STM32F0-ADC wird verwendet, um kontinuierlich
 * Temperatursensordaten und VREFINT-Daten zu konvertieren
 * und per DMA in ein statisches Speicher-Array zu kopieren.
 * Um zu vermeiden, dass erhebliche Zeit in
 * Interrupt-Handlern verbracht wird, berechnet diese Implementierung
 * die Temperatur nicht in Interrupt-Handlern. Stattdessen
 * muss der Benutzer readTemperatureData() aufrufen, um die
 * aktuell im ADC gespeicherten Samples auszuwerten und in den Temperaturwert
 * zu konvertieren. Aufgrund dieser Strategie werden nur die im
 * ADC-DMA-Buffer vorhandenen Samples gemittelt (was zu einer geringeren
 * Mittelungs-Sample-Anzahl bei seltenen Aufrufen von readTemperatureData() führt),
 * aber der Code benötigt keine
 * CPU-Zyklen (über die von ChibiOS-Internals verwendeten hinaus) zwischen Aufrufen von
 * readTemperatureData().
 *
 * Im Gegensatz zum ST-Beispiel in RM0091 berechnet dieser Code die Temperatur
 * in Milligrad Celsius ausschließlich mit Integer-Operationen.
 *
 * Diese Implementierung misst die VDDA-analoge Versorgungsspannung
 * quasi-synchron zur Temperatur. Daher
 * wird kein festes oder absolut stabiles VDDA für den ordnungsgemäßen Betrieb benötigt
 * (getestet mit 3,0V und 3,3V). Jedoch kann signifikantes Rauschen auf der VDDA-Leitung
 * auch Rauschen im Temperatursensorausgang verursachen, wenn
 * die Buffer-Größe nicht groß genug ist.
 *
 * Zusätzlich stellt readCurrentVDDA() eine Utility-Methode
 * zum Auslesen der aktuellen VDDA-Spannung als Millivolt bereit.
 *
 * Durch Verwendung von readTemperatureVDDA() kann der Benutzer sowohl
 * die Temperatur als auch VDDA mit nur einem einzigen Durchlauf durch das Sample-Array
 * berechnen. Die tatsächliche VDDA ist für die Temperatur-Normierung erforderlich,
 * sodass deren Rückgabe keinen signifikanten Overhead verursacht.
 *
 * Entwickelt für die Verwendung mit dem ChibiOS-HAL-Treiber:
 * http://chibios.org
 *
 * Revision 1.0
 *
 * Copyright (c) 2015 Uli Koehler
 * https://techoverflow.net
 * Veröffentlicht unter der Apache License v2.0
 */
#ifndef __STM32F0_TEMPERATURE_H
#define __STM32F0_TEMPERATURE_H

#include <stdint.h>

/*
 * Register-Adressen wurden aus DM00088500 (STM32F030-Datenblatt) übernommen
 * Für Nicht-STM32F030-Mikrocontroller müssen die Register-Adressen
 * möglicherweise gemäß dem jeweiligen Datenblatt modifiziert werden.
 */
//Temperatursensor-Rohwert bei 30 Grad C, VDDA=3.3V
#define TEMP30_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7B8))
//Temperatursensor-Rohwert bei 110 Grad C, VDDA=3.3V
#define TEMP110_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7C2))
//Interne Referenzspannung-Rohwert bei 30 Grad C, VDDA=3.3V
#define VREFINT_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7BA))

/**
 * Konfiguration:
 * Definiert die Größe des ADC-Sample-Arrays.
 * Ein größerer Wert hier erhöht den statischen RAM-Verbrauch
 * erheblich, aber mehr Werte
 * werden für die Mittelung verwendet, was zu geringerem Rauschen führt.
 */
#define ADC_TMPGRP_BUF_DEPTH 96

/**
 * Initialisiert den ADC und startet kontinuierliches Sampling und DMA-Kopieren
 * von sowohl Temperaturdaten als auch VREFINT-Daten.
 */
void initializeTemperatureADC(void);

/**
 * Berechnet die aktuelle Temperatur durch Auswertung der aktuell gespeicherten ADC-Samples.
 * @return Die absolute Temperatur in Milligrad Celsius, z.B. 12345 für 12.345 Grad C
 */
int32_t readTemperatureData(void);

/**
 * Berechnet die aktuelle VDDA durch Auswertung der gespeicherten ADC-Samples.
 * @return Die absolute VDDA in mV, z.B. 3234 für 3.234V
 */
int16_t readCurrentVDDA(void);

/**
 * Datentyp, der sowohl eine Temperatur als auch
 */
typedef struct {
    //Temperatur in Milligrad C
    int32_t temperature;
    //Analoge Versorgungsspannung in mV
    int32_t vdda;
} TemperatureVDDAResult;

/**
 * Berechnet sowohl die Temperatur als auch die VDDA gleichzeitig durch
 * Auswertung des gespeicherten ADC-Sample-Arrays
 */
TemperatureVDDAResult readTemperatureVDDA(void);


#endif //__STM32F0_TEMPERATURE_H

#include “STM32F0Temperature.h”

STM32F0Temperature.cxx
#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];

/**
 * Kontinuierliche 12-Bit-Konvertierung von CH16 (Temperatur)
 * und CH17 (VREFINT) ohne Callbacks.
 * Verwendet die langsamste mögliche Sample-Rate, um das Rauschen
 * so stark wie möglich zu reduzieren.
 */
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) {
    //HINWEIS: Berechnung erfolgt in 32 Bit, aber das Ergebnis wird später auf 16 Bit konvertiert.
    /**
     * Durchschnitt des Temperatursensor-Rohausgangs
     * und des VREFINT-Rohausgangs berechnen
     */
    int32_t tempAvg = 0;
    int32_t vrefintAvg = 0;
    //Samples alternieren: 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;
    /**
     * Temperatur in Milligrad Celsius berechnen.
     * Beachte, dass wir den Wert zuerst normieren müssen durch Anwendung
     * des Verhältnisses (tatsächliche VDDA / VDDARef).
     *
     * Hinweis: VDDA_Actual = 3.3V * VREFINT_CAL / vrefintAvg
     * Daher ist das oben erwähnte Verhältnis gleich
     * q = VREFINT_CAL / vrefintAvg
     */
    //Siehe RM0091 Abschnitt 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;
}

Check out similar posts by category: C/C++, Embedded