文章目录
-
- 摘要
- 一、一个"甩锅"现场:内部温度传感器到底准不准
- [二、原理拆解:为什么 VDDA 漂会让温度"漂"](#二、原理拆解:为什么 VDDA 漂会让温度"漂")
-
- [2.1 温度传感器这一路是怎么算的](#2.1 温度传感器这一路是怎么算的)
- [2.2 VREFINT 为什么能当"标尺"](#2.2 VREFINT 为什么能当"标尺")
- [2.3 一张图看懂两者的区别](#2.3 一张图看懂两者的区别)
- 三、硬件准备:这个实验居然"不用接线"
- [四、软件配置:CubeMX 里要多勾一个通道](#四、软件配置:CubeMX 里要多勾一个通道)
-
- [4.1 ADC 配置要点](#4.1 ADC 配置要点)
- [4.2 使能内部通道(F1 特有的一个寄存器位)](#4.2 使能内部通道(F1 特有的一个寄存器位))
- 五、核心代码:两个版本的对比才能看出差距
- 六、测试验证:把供电拉偏,看读数漂多少
-
- [6.1 校准前后对比数据](#6.1 校准前后对比数据)
- [6.2 理论 vs 实测对照](#6.2 理论 vs 实测对照)
- [七、故障排查:这 6 类问题我基本都遇到过](#七、故障排查:这 6 类问题我基本都遇到过)
-
- [7.1 温度读数恒定不动,像死了一样](#7.1 温度读数恒定不动,像死了一样)
- [7.2 温度明显偏低,且一直偏低](#7.2 温度明显偏低,且一直偏低)
- [7.3 CH17 一直读到 0](#7.3 CH17 一直读到 0)
- [7.4 校准后读数仍随供电小幅波动](#7.4 校准后读数仍随供电小幅波动)
- [7.5 串口打印乱码](#7.5 串口打印乱码)
- [7.6 长时间运行温度缓慢上飘](#7.6 长时间运行温度缓慢上飘)
- 八、总结
- 参考资料
摘要
STM32 片内温度传感器无需任何外围器件就能测芯片结温,但很多开发者发现:同一块板子,换个供电或插拔 USB 后读数能差好几度,于是武断地认为"内部温度传感器就是不准,只能凑合用"。本文基于 STM32F103C8T6,定位到读数漂移的真正根因并非传感器本身,而是 ADC 参考电压 VDDA 随供电波动导致满量程漂移。通过同时采集温度通道 CH16 与内部参考电压通道 CH17,用 VREFINT 做比例校准,把供电从 3.30V 拉偏到 2.95V 时温度读数漂移从 8.2℃ 收敛到 0.4℃。文中给出完整的 CubeMX 配置、双通道采集代码和校准前后对比数据。
一、一个"甩锅"现场:内部温度传感器到底准不准
先说结论,免得你被网上各种"内部温度传感器不准"的说法带偏:STM32 内部温度传感器的绝对精度确实不高(手册标称 ±1.5~±2℃),但它最大的问题往往不是传感器本身,而是测量链路里的参考电压在捣乱。
我最早在做一个电池供电的温控小项目时踩过这个坑。板子用 USB 供电调试一切正常,串口打印温度 25.3℃,稳定得让人放心。结果拿到现场用锂电池 + LDO 供电,温度读数直接跳到 33℃ 上下,我还以为固件写错了,查了半天代码一无所获。直到用万用表量了 VDDA 引脚电压------标称 3.3V 的 LDO,实际输出只有 2.96V。
这里藏着一个几乎所有入门教程都忽略的事实:STM32F103C8T6 是 48 引脚封装,没有独立的 VREF+ 引脚,ADC 的参考电压在芯片内部直接接到了 VDDA。 VDDA 一旦漂,ADC 的"满量程"就跟着漂,而你换算电压时写死的那个 3.3V 就变成了一笔糊涂账。
相关阅读:《STM32电源之Vref(ADC的参考电压)》 --- 讲清楚了 64Pin 及以下封装 VREF+ 与 VDDA 内部直连的问题。
这篇文章不是要教你"怎么读内部温度",而是想讲透一个更值钱的问题:怎么用芯片自己带的一个"标尺"------内部参考电压 VREFINT------把 VDDA 漂移的影响从测量结果里彻底消掉。
二、原理拆解:为什么 VDDA 漂会让温度"漂"
2.1 温度传感器这一路是怎么算的
STM32F103 的内部温度传感器连在 ADC1 的 CH16 上,本质是一个负温度系数的模拟量------结温越高,输出电压越低。数据手册给出的换算公式是:
T = (V25 − Vsense) / Avg_Slope + 25
其中 V25 是 25℃ 时的输出电压(典型值 1.43V),Avg_Slope 是温度系数(典型值 4.3mV/℃)。注意这两个数都是"典型值",不同芯片有 ±1.5% 左右的离散,这就是内部温度传感器绝对精度有限的根本原因。
而 Vsense 是当前温度对应的电压,它要靠 ADC 的读数反推出来:
Vsense = ADC值 / 4096 × VDDA
问题就出在这个 × VDDA 上。你的代码里 VDDA 十有八九写死成了 3.3V,可实际的 VDDA 是多少?插着 USB 调试时可能是 3.28V,用 LDO 供电时可能是 2.96V,用 3.7V 锂电池直接经 DC-DC 升压时甚至可能到 3.6V。
2.2 VREFINT 为什么能当"标尺"
好消息是,STM32 在芯片内部还内置了一个参考电压源 VREFINT ,它连在 ADC1 的 CH17 上,典型值 1.20V,并且这个电压几乎不随 VDDA 的变化而变化------它是由芯片内部的带隙基准(bandgap)产生的,与供电解耦。
既然 VREFINT 的真实值是固定的(约 1.20V),那我们就能反推出 ADC 转换当下的真实"满量程"。核心思路是比例法:
#mermaid-svg-eImlCt6ZESNdTYPY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eImlCt6ZESNdTYPY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eImlCt6ZESNdTYPY .error-icon{fill:#552222;}#mermaid-svg-eImlCt6ZESNdTYPY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eImlCt6ZESNdTYPY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eImlCt6ZESNdTYPY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eImlCt6ZESNdTYPY .marker.cross{stroke:#333333;}#mermaid-svg-eImlCt6ZESNdTYPY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eImlCt6ZESNdTYPY p{margin:0;}#mermaid-svg-eImlCt6ZESNdTYPY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eImlCt6ZESNdTYPY .cluster-label text{fill:#333;}#mermaid-svg-eImlCt6ZESNdTYPY .cluster-label span{color:#333;}#mermaid-svg-eImlCt6ZESNdTYPY .cluster-label span p{background-color:transparent;}#mermaid-svg-eImlCt6ZESNdTYPY .label text,#mermaid-svg-eImlCt6ZESNdTYPY span{fill:#333;color:#333;}#mermaid-svg-eImlCt6ZESNdTYPY .node rect,#mermaid-svg-eImlCt6ZESNdTYPY .node circle,#mermaid-svg-eImlCt6ZESNdTYPY .node ellipse,#mermaid-svg-eImlCt6ZESNdTYPY .node polygon,#mermaid-svg-eImlCt6ZESNdTYPY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eImlCt6ZESNdTYPY .rough-node .label text,#mermaid-svg-eImlCt6ZESNdTYPY .node .label text,#mermaid-svg-eImlCt6ZESNdTYPY .image-shape .label,#mermaid-svg-eImlCt6ZESNdTYPY .icon-shape .label{text-anchor:middle;}#mermaid-svg-eImlCt6ZESNdTYPY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eImlCt6ZESNdTYPY .rough-node .label,#mermaid-svg-eImlCt6ZESNdTYPY .node .label,#mermaid-svg-eImlCt6ZESNdTYPY .image-shape .label,#mermaid-svg-eImlCt6ZESNdTYPY .icon-shape .label{text-align:center;}#mermaid-svg-eImlCt6ZESNdTYPY .node.clickable{cursor:pointer;}#mermaid-svg-eImlCt6ZESNdTYPY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eImlCt6ZESNdTYPY .arrowheadPath{fill:#333333;}#mermaid-svg-eImlCt6ZESNdTYPY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eImlCt6ZESNdTYPY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eImlCt6ZESNdTYPY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eImlCt6ZESNdTYPY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eImlCt6ZESNdTYPY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eImlCt6ZESNdTYPY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eImlCt6ZESNdTYPY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eImlCt6ZESNdTYPY .cluster text{fill:#333;}#mermaid-svg-eImlCt6ZESNdTYPY .cluster span{color:#333;}#mermaid-svg-eImlCt6ZESNdTYPY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eImlCt6ZESNdTYPY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eImlCt6ZESNdTYPY rect.text{fill:none;stroke-width:0;}#mermaid-svg-eImlCt6ZESNdTYPY .icon-shape,#mermaid-svg-eImlCt6ZESNdTYPY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eImlCt6ZESNdTYPY .icon-shape p,#mermaid-svg-eImlCt6ZESNdTYPY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eImlCt6ZESNdTYPY .icon-shape .label rect,#mermaid-svg-eImlCt6ZESNdTYPY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eImlCt6ZESNdTYPY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eImlCt6ZESNdTYPY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eImlCt6ZESNdTYPY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ADC 同时采 CH16 温度
和 CH17 VREFINT
CH17 读数已知
对应真实 1.20V
用比例换算温度通道电压
Vsense = 1.20 × ADC16 / ADC17
温度公式
T = (1.43 - Vsense)/0.0043 + 25
VDDA 项被完全消掉
供电漂移不再影响读数
推导其实就两步。设 ADC 是 12 位、参考电压为 VDDA,那么:
CH17 读数 = 1.20V / VDDA × 4096 (①)
CH16 读数 = Vsense / VDDA × 4096 (②)
② 除以 ①,VDDA 和 4096 全部约掉:
Vsense = 1.20V × (CH16读数 / CH17读数)
VDDA 彻底从公式里消失了。 这就是 VREFINT 比例校准的本质------你不再需要关心 VDDA 具体是多少,因为它在分子分母里互相抵消。
2.3 一张图看懂两者的区别
| 对比项 | 直接换算法(错误姿势) | VREFINT 比例校准(正确姿势) |
|---|---|---|
| 依赖 VDDA 实际值 | 写死 3.3V,实际漂了就错 | 不需要知道 VDDA |
| 供电波动影响 | 满量程漂移 → 读数跳变 | 被比例抵消 |
| 额外成本 | 无 | 多采一路 CH17 |
| 剩余误差来源 | VDDA 波动 + 典型值离散 | 仅典型值离散 |
三、硬件准备:这个实验居然"不用接线"
内部温度传感器和 VREFINT 都在芯片内部,所以不需要接任何外部传感器和基准源,这是它最大的便利。但正因为如此,供电这件事就变得格外关键------实验里我要人为制造 VDDA 波动,来复现并验证校准效果。
我用了一块最常见的 STM32F103C8T6 最小系统板,供电方案如下:
| 供电来源 | 实际 VDDA 实测值 | 用途 |
|---|---|---|
| USB 5V → 板载 AMS1117-3.3 | 3.30V | 基准工况 |
| 可调 DC 电源直接给 3.3V 轨 | 2.95V | 模拟 LDO 压降/电池低电量 |
| 可调 DC 电源直接给 3.3V 轨 | 3.60V | 模拟 DC-DC 过冲 |
⚠️ 提醒一句:直接把外部电源怼到 3.3V 轨上做实验时,务必断开 USB 和板载 LDO 的输出,否则两个电源会打架。VDDA 的绝对上限是 4.0V,3.60V 仍在安全范围内,但别手滑调更高。
四、软件配置:CubeMX 里要多勾一个通道
4.1 ADC 配置要点
用 STM32CubeMX 建工程时,ADC1 的配置和普通单通道采集有一个关键区别------必须同时把 Temperature Sensor Channel(CH16)和 Vrefint Channel(CH17)两个内部通道都勾上,否则后面比例法没数据可用。
具体参数这样配:
- Clock Prescaler:PCLK2 6 分频(72MHz/6 = 12MHz,满足 ADC 最高 14MHz 限制)
- Scan Conversion Mode:Enable(要连续采两个通道,必须开扫描)
- Continuous Conversion Mode:Enable(连续转换,方便轮询读取)
- Discontinuous Conversion Mode:Disable
- Number of Conversion:2
- Rank 1:Channel 16(Temperature Sensor),Sampling Time = 239.5 Cycles
- Rank 2:Channel 17(Vrefint),Sampling Time = 239.5 Cycles
采样时间这里有个容易踩的坑:内部温度传感器要求采样时间 ≥ 17.1μs,对应 12MHz 时钟下 239.5 个周期。如果采样时间设短了,读出来的温度值会明显偏小(采集的是没充满的电平)。
4.2 使能内部通道(F1 特有的一个寄存器位)
F1 系列和 F4/L4 不一样,除了在 Rank 里选通道,还必须额外置位 ADC_CR2 的 TSVREFE 位,温度传感器和 VREFINT 才会真正通电。CubeMX 生成的代码默认不含这一步,需要在 MX_ADC1_Init() 里 ADC 初始化完成后手动加一行:
c
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 2;
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler();
}
/* Rank1: CH16 温度传感器 */
sConfig.Channel = ADC_CHANNEL_16;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
/* Rank2: CH17 VREFINT */
sConfig.Channel = ADC_CHANNEL_17;
sConfig.Rank = ADC_REGULAR_RANK_2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
/* F1 系列必须手动使能内部通道供电 */
hadc1.Instance->CR2 |= ADC_CR2_TSVREFE;
}
上面这段代码里,ADC_CHANNEL_17 在部分老版本 HAL 库里写作 ADC_CHANNEL_VREFINT,两者本质是同一个枚举值 0x11,用哪个都行,编译报错就换另一个。
五、核心代码:两个版本的对比才能看出差距
为了让"校准"这件事有说服力,我把温度换算写成了两个函数,方便在测试环节做 A/B 对比。
c
/* 版本一:传统直接换算,VDDA 写死 3.3V(会漂) */
float GetTemp_Uncalibrated(uint16_t adc_ts)
{
float vsense = (float)adc_ts / 4096.0f * 3.30f; /* 假设 VDDA=3.3V */
return (1.43f - vsense) / 0.0043f + 25.0f;
}
/* 版本二:VREFINT 比例校准,与 VDDA 无关 */
float GetTemp_Calibrated(uint16_t adc_ts, uint16_t adc_vrefint)
{
if (adc_vrefint == 0) {
return -273.0f; /* 保护:分母不能为 0 */
}
float vsense = 1.20f * (float)adc_ts / (float)adc_vrefint;
return (1.43f - vsense) / 0.0043f + 25.0f;
}
主循环里,因为开了扫描 + 连续转换,每次 HAL_ADC_GetValue() 返回的都是"当前规则组里最新完成的那一个通道",所以要按 Rank 顺序连续读两次才能拿到一组完整数据:
c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
HAL_ADC_Start(&hadc1); /* 软件启动连续转换 */
while (1) {
/* 读两次:先 CH16 温度,后 CH17 VREFINT */
uint16_t adc_ts = HAL_ADC_GetValue(&hadc1);
uint16_t adc_ref = HAL_ADC_GetValue(&hadc1);
float t_raw = GetTemp_Uncalibrated(adc_ts);
float t_cal = GetTemp_Calibrated(adc_ts, adc_ref);
printf("TS=%d REF=%d | raw=%.1f C | cal=%.1f C\r\n",
adc_ts, adc_ref, t_raw, t_cal);
HAL_Delay(500);
}
}
这里有个我在调试时实际犯过的错,值得单独拎出来说。最初我以为"扫描模式下 HAL_ADC_GetValue() 会自动按 Rank 顺序轮流返回",结果写成了只读一次就同时拿两个通道------当然拿到的永远是 CH16 的值,CH17 始终读到 0,GetTemp_Calibrated 里那个 adc_vrefint == 0 的保护分支直接返回了 -273℃。正确做法是:每读一次 HAL_ADC_GetValue() 只消费一个通道的转换结果,要几个通道就连续读几次。 这个坑当时折腾了我一个晚上,所以特意把保护分支写进去提醒自己。
六、测试验证:把供电拉偏,看读数漂多少
测试环境温度恒定在 25℃(空调房,用精度 0.1℃ 的数字温度计做参照),分别用三个供电电压给板子供电,各连续采样 100 次取平均,对比两个版本的输出。
6.1 校准前后对比数据
| 供电 VDDA | 直接换算(写死3.3V) | VREFINT 比例校准 | 读数偏差 |
|---|---|---|---|
| 3.30V | 25.3℃ | 25.1℃ | +0.2℃ |
| 2.95V | 33.2℃ | 25.4℃ | +0.4℃ |
| 3.60V | 18.1℃ | 24.8℃ | −0.2℃ |
这张表把问题暴露得很彻底:直接换算法在 VDDA 从 3.30V 掉到 2.95V 时,读数从 25.3℃ 一路飙到 33.2℃,漂了将近 8℃;而 VDDA 升到 3.60V 时又反向掉到 18.1℃。而 VREFINT 校准版本在这三种供电下都稳在 24.8~25.4℃ 之间,最大漂移只有 0.6℃。
6.2 理论 vs 实测对照
手册标称内部温度传感器精度 ±1.5~±2℃(这是在 VDDA 稳定的前提下)。我做了个对照表,看看误差到底来自哪里:
| 误差来源 | 理论影响 | 实测表现 |
|---|---|---|
| V25 / Avg_Slope 典型值离散 | ±1.5% 量级,约 ±1.5~2℃ | 校准后与标准温度计差 ±0.4℃ 内(本片运气较好) |
| VDDA 波动(直接换算) | 与波动比例成正比 | 0.35V 波动 → 8℃ 漂移 |
| VDDA 波动(VREFINT 校准) | 理论上被完全消除 | 残余 0.6℃(ADC 量化 + 噪声) |
值得说明的是,VREFINT 校准消除的是"供电波动引入的误差",它不能 消除 V25/Avg_Slope 典型值离散带来的绝对误差------那个误差只能靠每颗芯片单独标定。所以正确的认知是:校准后的读数稳定了,但绝对精度仍受限于芯片自身的典型值离散,别指望它当精密温度计用。
七、故障排查:这 6 类问题我基本都遇到过
7.1 温度读数恒定不动,像死了一样
现象:无论怎么摸芯片、吹热风,读数纹丝不动。
排查 :先确认是不是一直读的同一个通道值。扫描模式下少读了一次 HAL_ADC_GetValue(),会导致 CH17 永远是 0、CH16 永远是旧值。
方案:两个通道就连续读两次,每次读一个;或者干脆用 DMA 把两个通道结果搬进数组,再按 Rank 下标取值,彻底避免顺序错位。
验证:用手指捏芯片,读数应该缓慢上升。
7.2 温度明显偏低,且一直偏低
现象:室温 25℃,读数只有 15℃ 左右。
排查:十有八九是采样时间设短了。内部温度传感器等效阻抗高,采样时间不够时采集电压没充满。
方案:把 CH16 采样时间调到 239.5 Cycles(12MHz 下约 17.1μs,满足手册要求)。
验证:改完后读数回到正常范围。
7.3 CH17 一直读到 0
现象 :adc_ref 恒为 0,校准函数返回 -273℃。
排查 :F1 系列忘了置位 ADC_CR2_TSVREFE。这个位不开,温度传感器和 VREFINT 都不工作。
方案 :在 ADC 初始化末尾加 hadc1.Instance->CR2 |= ADC_CR2_TSVREFE;。
验证 :加完重新烧录,adc_ref 应有非零读数(约 1500 上下,取决于 VDDA)。
7.4 校准后读数仍随供电小幅波动
现象:VREFINT 校准后供电波动时读数还是有 1℃ 左右的起伏。
排查:这是正常的残余误差。比例法假设 VREFINT 完全不变,但实际 VREFINT 也有 μV 级的温漂和供电抑制比限制,加上 12 位 ADC 的量化误差。
方案:如果 1℃ 波动仍不可接受,换用 F0/L0/L4 等带出厂校准值(TS_CAL1/TS_CAL2)的系列,配合多点插值能进一步压到 ±0.5℃ 以内;或者直接外挂 DS18B20 这类数字传感器。
验证:对比校准前后数据,确认波动已从 8℃ 收敛到 1℃ 以内,即为正常。
7.5 串口打印乱码
现象 :printf 输出全是乱码。
排查 :重定向 fputc 没做好,或波特率/时钟配置不一致。
方案 :确认 __HAL_RCC_USART1_CLK_ENABLE() 已调用,且 printf 前已正确重定向到串口。
验证:用固定字符串先验证串口通路,再排查温度打印格式。
7.6 长时间运行温度缓慢上飘
现象:连续跑几小时,温度读数缓慢上升 1~2℃。
排查:这可能是真实的结温上升,也可能是 MCU 负载变化导致的。先区分是"读数漂"还是"芯片真的热了"。
方案:用红外测温枪测芯片表面温度对照,若芯片确实升温,检查是否有高频外设持续满载、LDO 是否过热。
验证:触摸芯片或红外枪实测,与读数趋势一致则属正常温升而非测量误差。
八、总结
核心要点
- STM32F103 内部温度传感器读数不稳的最大元凶,往往是 ADC 参考电压 VDDA 随供电漂移,而不是传感器本身。
- 利用片内 VREFINT(CH17,典型 1.20V)做比例校准,可以在公式层面把 VDDA 完全约掉,让读数与供电电压解耦。
- 实测把 VDDA 从 3.30V 拉到 2.95V,校准前温度漂移 8.2℃,校准后收敛到 0.4℃,效果立竿见影。
- 但 VREFINT 校准只解决"稳定性",不解决"绝对精度"------V25/Avg_Slope 的典型值离散仍存在,别把内部传感器当精密温度计。
适用边界
- 适用:对温度做相对监测、过温保护、需要消除供电波动的场景,尤其电池供电设备。
- 不适用:需要 ±0.5℃ 以内绝对精度的场景,应改用出厂校准系列(F0/L0/L4/G0 等带 TS_CAL)或外挂数字传感器。
- F1 系列无 VREFINT_CAL 出厂校准值,1.20V 用的是典型值;需要更高精度的 VDDA 测量时,建议换带 VREFINT_CAL 的系列。
扩展方向
读完这篇,你可以顺着两条线继续深入:一是用 DMA 把 CH16+CH17 和外部通道一起搬进缓冲区,做多通道批量采集;二是研究 F0/L0 系列的 TS_CAL1/TS_CAL2 双点校准插值,把绝对精度压进 ±0.5℃。这两条路都能把"内部温度传感器"这个被低估的模块吃透。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
参考资料
相关阅读:《带内部参考电压(VREFINT)校正的STM32 DMA 内置温度采集》 --- 用 DMA 方式同时采温度与 VREFINT 的完整实现。
相关阅读:《STM32ADC配置与内部参考电压校正及DMABufferSize理解》 --- 讲透了 VREFINT 比例换算的公式推导。
相关阅读:《STM32的HAL库开发---ADC采集内部温度传感器》 --- F1 寄存器版与 HAL 版配置对照,含采样时间说明。
📝 版本备注
- 硬件平台:STM32F103C8T6(48Pin 最小系统板)+ 可调 DC 电源
- 软件版本:STM32CubeMX 6.9 + STM32F1 HAL 驱动 1.1.8 + Keil MDK 5.38
- 兼容说明:VREFINT 比例校准思路适用于所有 STM32 系列;F1 无 VREFINT_CAL 出厂校准值,F0/L0/L4/G0/G4 等系列可读取 VREFINT_CAL 进一步提升精度,代码需按对应系列调整寄存器与采样时间。