使用 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 通道。
/**
* 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”
#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;
}