使用 ChibiOS 读取 STM32F0 内部温度和电压

STM32F0 系列 32 位 ARM Cortex M0 微控制器尽管起价低至 0.32€ @1pc,但包含大量内部外设。其中包括内部工厂校准温度传感器和电源电压传感器(专门检测 VDDA,模拟电源电压轨),连接到内部 ADC 的通道 16 和 17。

虽然我通常喜欢 STM32 文档,但实现产生实际值的代码相当困难。虽然 STM32F0 参考手册包含公式和一小段示例代码,但我认为计算中的某些方面在计算中被低估了:

RM0091 中的 13.9 节提供了从原始温度传感器输出和工厂校准值计算温度的公式。然而,任何地方都没有说明(至少在 Rev7,当前 RM0091 修订版中)此公式仅对于正好 3.30V 的 VDDA 是正确的。

A.7.16 节中的示例代码使用直接应用于温度传感器原始输出的归一化因子 VDD_APPLI / VDD_CALIB。然而,VDD_APPLI 没有在任何地方记录并定义为 300,而 VDDA_CALIB 定义为 330

经过一些实验,我发现你需要实际持续测量电源电压才能正确归一化温度输出。通过这样做,你可以归一化原始温度传感器输出。在 VDDA=3.0V 下运行时,对于我的测试电路,不应用所述归一化的计算温度传感器输出约为 -14°C,而实际约为 18°C

计算内部电源电压在计算温度时基本上是免费的,因为准确温度监测反正需要 VDD_APPLI / VDDA_CALIB 比率。因此,我的代码还包含同时获取温度传感器输出和电压传感器输出的功能。

与 RM0091 示例相比,我的代码以 m°C 计算温度,产生的分辨率比 ST 代码示例高三个数量级。然而,我怀疑整数算术操作的不适当排序仍然会引入不准确。

为了提供良好文档的参考,我实现了一个易于使用的代码库,同时持续感测温度和电压。为了提供高互操作性,我使用 ChibiOS HAL 实现了硬件特定部分。

代码在使用 STM32F030F4 的定制板上测试(即撰写本文时我能找到的最便宜的 STM32 微控制器),并使用 Fluke 289 万用表检查准确性。对于 STM32F0 系列中的其他微控制器,三个校准值的地址可能需要调整(虽然我怀疑至少对于一系列微控制器它们是相同的)。像 STM32F4 这样更大的控制器使用稍微不同的 ADC 配置。也许将来我会创建一些也适用于其他系列的混合代码。

头文件包含代码实现细节的详细文档。注意,不修改代码,无法在同一程序中持续监控温度/VDDA 并使用其他 ADC 通道。

STM32F0Temperature.h
/**
 * STM32F0 内部温度传感器读取工具
 * 在定制 STM32F030F4 板上测试。
 *
 * STM32F0 ADC 用于持续转换
 * 温度传感器数据和 VREFINT 数据并 DMA 复制
 * 它们到静态内存数组。
 * 为了避免在中断处理程序中花费大量时间,
 * 此实现不在中断处理程序中计算温度。相反,
 * 用户必须调用 readTemperatureData() 来评估
 * 当前存储在 ADC 中的样本并将它们转换为温度
 * 值。由于此策略,只有 ADC DMA 缓冲区中存在的
 * 样本被平均(对于不频繁调用 readTemperatureData() 产生较低的
 * 平均样本计数),但代码不需要
 * CPU 周期(超出 ChibiOS 内部使用的)在调用
 * readTemperatureData() 之间。
 *
 * 与 RM0091 中的 ST 示例相比,此代码计算温度
 * 仅使用整数运算,以毫摄氏度为单位。
 *
 * 此实现准同步测量 VDDA 模拟电源电压
 * 与温度。因此,
 * 正确操作不需要固定或绝对稳定的 VDDA
 *(在 3.0V 和 3.3V 下测试)。然而,VDDA
 * 线上的显著噪声也可能在温度输出上引入噪声,如果
 * 样本缓冲区大小不够大。
 *
 * 此外,readCurrentVDDA() 提供了一个实用方法
 * 以毫伏为单位读取当前 VDDA 电压。
 *
 * 通过使用 readTemperatureVDDA(),用户能够仅通过对样本
 * 数组的单次迭代计算温度和 VDDA。
 * 实际 VDDA 是温度归一化所需的,
 * 因此返回它不会产生显著开销。
 *
 * 设计用于 ChibiOS HAL 驱动程序:
 * http://chibios.org
 *
 * 版本 1.0
 *
 * 版权所有 (c) 2015 Uli Koehler
 * https://techoverflow.net
 * 在 Apache License v2.0 下发布
 */
#ifndef __STM32F0_TEMPERATURE_H
#define __STM32F0_TEMPERATURE_H

#include <stdint.h>

/*
 * 寄存器地址取自 DM00088500 (STM32F030 数据手册)
 * 对于非 STM32F030 微控制器,寄存器地址
 * 可能需要根据相应数据手册修改。
 */
//30 摄氏度时的温度传感器原始值,VDDA=3.3V
#define TEMP30_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7B8))
//110 摄氏度时的温度传感器原始值,VDDA=3.3V
#define TEMP110_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7C2))
//30 摄氏度时的内部电压参考原始值,VDDA=3.3V
#define VREFINT_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7BA))

/**
 * 配置:
 * 定义 ADC 样本数组的大小。
 * 在此处定义更大的值将显著增加
 * 静态 RAM 使用量,但更多值
 * 将用于平均,导致更低噪声。
 */
#define ADC_TMPGRP_BUF_DEPTH 96

/**
 * 初始化 ADC 并开始持续采样和 DMA 复制
 * 温度数据和 vrefint 数据。
 */
void initializeTemperatureADC(void);

/**
 * 通过评估当前存储的 ADC 样本计算当前温度。
 * @return 毫摄氏度绝对温度,例如 12345 表示 12.345 摄氏度
 */
int32_t readTemperatureData(void);

/**
 * 通过评估存储的 ADC 样本计算当前 VDDA。
 * @return 绝对 VDDA,以 mV 为单位,例如 3234 表示 3.234V
 */
int16_t readCurrentVDDA(void);

/**
 * 同时存储温度和
 */
typedef struct {
    //温度,以毫摄氏度为单位
    int32_t temperature;
    //模拟电源电压,以 mV 为单位
    int32_t vdda;
} TemperatureVDDAResult;

/**
 * 通过评估存储的 ADC 样本数组
 * 一次性计算温度和 VDDA
 */
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 样本缓冲区
 */
static adcsample_t temperatureVRefSamples[ADC_TEMPGRP_NUM_CHANNELS * ADC_TMPGRP_BUF_DEPTH];

/**
 * CH16(温度)和 CH17(VREFINT)的持续 12 位转换
 * 无回调。
 * 使用最慢的可能采样率以尽可能
 * 减少噪声。
 */
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) {
    //注意:计算以 32 位执行,但结果稍后转换为 16 位。
    /**
     * 计算温度传感器原始输出
     * 和 vrefint 原始输出的平均值
     */
    int32_t tempAvg = 0;
    int32_t vrefintAvg = 0;
    //样本交替: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;
    /**
     * 以毫摄氏度计算温度。
     * 注意我们需要先通过应用
     * (实际 VDDA / VDDARef) 比率来归一化值。
     *
     * 注意:VDDA_Actual = 3.3V * VREFINT_CAL / vrefintAvg
     * 因此,上述比率等于
     * q = VREFINT_CAL / vrefintAvg
     */
    //参见 RM0091 第 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