1. 内部温度传感器原理与工程定位
STM32F103系列微控制器集成了一个高精度的内部温度传感器,该传感器并非独立外设,而是作为ADC1的一个专用模拟输入通道(通道16)集成在芯片内部。这一设计显著降低了系统BOM成本与PCB布线复杂度,但同时也对开发者提出了明确的工程认知要求:温度读取本质上是一次特殊的ADC采样操作,其后续的电压-温度转换完全依赖于芯片数据手册中给出的标定参数与线性模型。
在实际嵌入式系统中,内部温度传感器常用于以下三类关键场景:
-芯片自检与热管理:实时监测MCU核心温度,防止因过热导致的时钟失锁、Flash写入失败或SRAM数据异常;
-环境温度粗略估计:当系统无外部高精度温感器件时,提供参考级环境温度(需注意PCB热耦合影响);
-多传感器校准基准:为外部NTC或数字温感提供动态偏移补偿依据。
必须清醒认识到其固有局限:典型精度为±1.5℃(-40℃~85℃),且受VDDA供电噪声、ADC参考电压稳定性及PCB局部热梯度影响显著。因此,它绝非替代工业级温度传感器的方案,而是一个“够用、可控、可预测”的片上诊断资源。
2. ADC通道16的硬件特性与配置约束
内部温度传感器连接至ADC1_IN16,其电气特性由ST官方数据手册严格定义。理解这些参数是编写可靠读取函数的前提:
| 参数 | 典型值 | 工程意义 |
|---|---|---|
| 输出电压范围 | 0.76V ~ 1.71V | 对应-40℃ ~ 125℃,超出此范围ADC读数无效 |
| 温度系数 | 4.3 mV/℃ | 每摄氏度变化引起的电压偏移量,是线性模型核心斜率 |
| 25℃标定点电压 | 1.43 V | 所有温度计算的基准锚点,非实测值,不可替代 |
关键约束在于ADC配置:
-仅ADC1支持:温度传感器专属通道,ADC2/ADC3无法访问;
-必须启用TSVREFE位:在ADC控制寄存器ADC_CR2中设置TSVREFE=1,否则通道16始终输出0;
-VREF+必须稳定:内部传感器以VREF+为基准,若使用外部VREF+,其纹波需<10mV;若使用VDDA,则VDDA必须经LC滤波并远离开关电源噪声源;
-采样时间强制要求:因传感器输出阻抗较高(典型10kΩ),最小采样周期必须≥17.1μs(对应ADC_SMPR1寄存器中SMP16=3,即239.5个ADC周期)。低于此值将导致电荷未充分建立,读数严重偏低。
这些约束不是可选项,而是芯片物理层的硬性规定。任何忽略TSVREFE使能或采样时间不足的配置,都会导致函数返回恒定的0或随机抖动值,调试时极易误判为软件逻辑错误。
3. 温度读取函数的设计哲学与实现细节
3.1 函数接口设计:为什么返回int而非float?
int Get_Temperture(void)的接口设计直指嵌入式开发的核心权衡——确定性与资源效率。
-确定性:浮点运算在Cortex-M3上依赖软件库(如ARM CMSIS DSP),执行时间非恒定,且易受FPU未使能或编译器优化等级影响;而整数运算是硬件原生支持,耗时精确可控,符合实时系统要求;
-内存与栈空间:float变量占4字节,double占8字节,频繁调用会增加栈压力;int仅需2或4字节,且避免了浮点数在不同编译器下的二进制表示差异风险;
-精度表达:通过“放大100倍”策略(即单位为0.01℃),既保留了两位小数的实用精度,又完全规避了浮点舍入误差。例如23.56℃存储为整数2356,解码时仅需temp_int / 100.0f即可还原,全程无精度损失。
此设计体现了嵌入式工程师的务实思维:不追求数学上的“完美”,而追求工程上的“可靠”与“可预测”。
3.2 ADC读取函数的复用与重构
温度读取依赖于稳定的ADC采样能力。我们复用已验证的通用ADC读取函数,但必须进行关键重构:
// 原始ADC读取函数(ADC实验中已验证) u16 Get_Adc_Average(u8 ch, u8 times) { u32 temp_val = 0; u8 t; for(t = 0; t < times; t++) { // 1. 配置规则组通道 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); // 2. 软件触发转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 3. 等待转换完成(超时保护) while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 4. 读取结果并累加 temp_val += ADC_GetConversionValue(ADC1); // 5. 两次采样间加入延时,避免总线竞争 delay_ms(5); } return (u16)(temp_val / times); }重构要点解析:
-通道参数化:ch参数从固定值改为可变,直接传入ADC_CHANNEL_16(即16),消除硬编码;
-采样时间锁定:ADC_SampleTime_239Cycles5是唯一满足传感器阻抗要求的选项,不可替换为71Cycles5等短周期;
-延时必要性:delay_ms(5)并非随意添加。ADC转换本身毫秒级,但连续触发会导致ADC模拟前端电荷泵来不及恢复,引入共模误差。5ms间隔确保传感器输出稳定;
-超时保护缺失:原始代码中while(!ADC_GetFlagStatus(...))存在死循环风险。实际工程中必须加入计数器超时退出,否则总线错误或ADC故障将导致系统挂起。此处为教学简化,但生产代码应补充。
3.3 温度计算的数学推导与代码实现
温度计算公式源于数据手册的线性模型:V_sense = V_25 - (T - 25) * k
其中V_sense为通道16读出的电压,V_25 = 1.43V为25℃标定点电压,k = 0.0043 V/℃为温度系数。
解出温度T:T = 25 + (V_25 - V_sense) / k
代入数值:T = 25 + (1.43 - V_sense) / 0.0043
代码实现的关键陷阱与规避:
-浮点中间计算:V_sense必须先由ADC值转换为浮点电压,否则整数除法将丢失全部精度。转换公式为:V_sense = (float)adc_value * 3.3f / 4096.0f
此处3.3f和4096.0f后缀强制浮点运算,避免整数截断;
-运算顺序优化:(1.43f - V_sense) / 0.0043f + 25.0f中,0.0043f建议预计算为倒数232.5581f,将除法转为乘法,提升性能并减少浮点误差累积;
-放大100倍的整数安全转换:c float temp_float = 25.0f + (1.43f - vsense) * 232.5581f; int temp_int = (int)(temp_float * 100.0f + 0.5f); // +0.5f实现四舍五入+0.5f是关键!避免C语言默认的向零截断(如23.999→23),确保23.995℃正确显示为24.00℃。
完整函数实现如下:
// 温度读取函数:返回温度值×100(单位:0.01℃) int Get_Temperture(void) { u16 adc_value; float vsense, temp_float; // 1. 读取通道16的ADC值(10次平均) adc_value = Get_Adc_Average(ADC_CHANNEL_16, 10); // 2. ADC值转电压(V) vsense = (float)adc_value * 3.3f / 4096.0f; // 3. 电压转温度(℃),并放大100倍 temp_float = 25.0f + (1.43f - vsense) * 232.5581f; return (int)(temp_float * 100.0f + 0.5f); }4. 系统级配置与初始化流程
温度传感器功能的启用,绝非仅靠一个读取函数即可实现。它依赖于整个ADC子系统的协同配置,任何环节疏漏都将导致读数失效。
4.1 RCC时钟使能链
ADC1的时钟路径为:HSE/HSI → AHB → APB2 → ADC1。标准库中需显式开启:
RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1 | RCC_APB2PERIPH_GPIOA, ENABLE);关键点:GPIOA使能不可省略。因ADC1_IN16复用功能映射到PA0引脚(部分F103型号),即使不作为GPIO使用,其模拟输入缓冲器仍需时钟驱动。
4.2 ADC基础配置(ADC_InitTypeDef)
ADC_InitTypeDef ADC_InitStructure; ADC_DeInit(ADC1); // 复位ADC1寄存器 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道,非扫描 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;// 单次转换,非连续 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐(12位有效) ADC_InitStructure.ADC_NbrOfChannel = 1; // 仅1个通道 ADC_Init(ADC1, &ADC_InitStructure);为何禁用连续转换?
内部温度传感器响应慢(毫秒级),连续转换模式下ADC会以最高速率(>1MHz)反复采样,导致传感器输出跟不上,读数严重失真。单次+软件触发是唯一可靠模式。
4.3 关键寄存器位:TSVREFE的使能
这是最容易被忽略的致命步骤!标准库未提供ADC_TempSensorVrefintCmd()封装,必须手动操作寄存器:
// 使能温度传感器和内部参考电压 ADC_TempSensorVrefintCmd(ENABLE); // 等效于:ADC1->CR2 |= (uint32_t)ADC_CR2_TSVREFE;此宏展开后操作ADC_CR2寄存器的TSVREFE位。若遗漏,Get_Adc_Average(ADC_CHANNEL_16,...)永远返回0。
4.4 GPIO配置的隐含要求
尽管通道16是内部信号,但PA0引脚仍需配置为模拟输入:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_Mode_AIN关闭施密特触发器与上拉/下拉,将引脚置于高阻态,确保内部传感器信号无衰减接入ADC。
5. 实际工程中的校准与误差补偿
理论公式在理想条件下成立,但量产芯片存在个体差异。数据手册明确指出:“The temperature sensor is calibrated at 25°C and 110°C during production.” 这意味着标定点V_25=1.43V是批次平均值,单颗芯片可能为1.41V或1.45V。
5.1 两点校准法(推荐)
在已知两个温度点(如25℃恒温箱、85℃烘箱)下测量ADC值,建立新的线性方程:
- 测得25℃时ADC值 =ADC_25→V_25_actual = ADC_25 * 3.3 / 4096
- 测得85℃时ADC值 =ADC_85→V_85_actual = ADC_85 * 3.3 / 4096
- 新斜率k_new = (V_85_actual - V_25_actual) / (85 - 25)
- 新公式:T = 25 + (V_25_actual - V_sense) / k_new
此方法可将精度提升至±0.5℃内,适合对温度敏感的应用。
5.2 电源电压漂移补偿
V_sense计算中使用的3.3f是假设VDDA=3.3V。若实际VDDA为3.25V,则计算电压系统性偏低。解决方案:
- 使用ADC测量VDDA(通过ADC_CHANNEL_VREFINT读取内部1.2V基准,再反推VDDA);
- 或在系统启动时用万用表实测VDDA,将3.3f替换为实测值。
5.3 PCB热耦合效应量化
MCU自身功耗(如USB通信、LED驱动)会使芯片温度高于环境温度。实测表明:
- 100mA电流下,F103芯片表面温度比环境高3~5℃;
- 解决方案:在Get_Temperture()前插入delay_ms(100),让功耗尖峰过去;或在空闲任务中周期读取,避开高负载时段。
6. 调试技巧与常见故障排查
6.1 快速验证流程(5分钟定位问题)
- 检查TSVREFE:用调试器查看
ADC1->CR2寄存器,确认bit23(TSVREFE)为1; - 验证ADC基础功能:改用
ADC_CHANNEL_0(PA0接可调电位器),确认Get_Adc_Average能读出0~4095变化; - 通道16特殊值测试:屏蔽所有干扰,测量
Get_Adc_Average(ADC_CHANNEL_16, 1)返回值:
- 若恒为0 → TSVREFE未使能或GPIO配置错误;
- 若在1700~2100间波动 → 传感器工作正常,进入下一步;
- 若>3000 → VDDA过高或VREF+异常; - 计算验证:取ADC值=1900,计算
V_sense=1900*3.3/4096≈1.528V,代入公式T=25+(1.43-1.528)/0.0043≈2.7℃,若室温25℃则说明校准偏差大。
6.2 典型错误现象与根源
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 恒定返回0 | TSVREFE=0,或GPIOA时钟未使能 | 检查RCC_APB2PeriphClockCmd和ADC_TempSensorVrefintCmd |
| 读数剧烈跳变(±10℃) | 采样时间不足(SMP16<3),或未加采样间隔延时 | 强制SMP16=3,delay_ms(5)不可省略 |
| 读数系统性偏高/偏低 | VDDA偏离3.3V,或未做两点校准 | 测量实际VDDA,更新电压系数;或执行校准 |
| 读数随USB插拔变化 | USB电源噪声耦合至VDDA/VREF+ | 在VDDA和VREF+引脚就近加10μF钽电容+100nF陶瓷电容 |
6.3 生产环境下的鲁棒性增强
在批量产品中,需进一步加固:
-启动自检:系统上电后立即读取温度,若|T - 25| > 15℃(即<-10℃或>40℃),判定传感器异常,点亮故障LED;
-软件看门狗喂狗:在Get_Temperture()内加入IWDG_ReloadCounter(),防止单次ADC超时导致WDT复位;
-结果滤波:对连续5次读数进行中值滤波,剔除突发干扰毛刺。
7. 完整工程示例:主程序集成
以下为main.c中温度功能的最小可行集成:
#include "stm32f10x.h" #include "adc.h" // 包含Get_Adc_Average声明 #include "usart.h" // 用于串口打印 // 声明温度读取函数 int Get_Temperture(void); int main(void) { // 1. 系统初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); Delay_Init(); // SysTick初始化 USART1_Init(115200); // 串口1初始化 ADC1_Init(); // ADC1初始化(含TSVREFE使能) printf("STM32 Temperature Sensor Demo\r\n"); while(1) { int temp_x100 = Get_Temperture(); float temp_c = temp_x100 / 100.0f; // 格式化输出:XX.XX℃ printf("Temp: %d.%02d C\r\n", (int)temp_c, (int)(temp_c * 100) % 100); Delay_ms(1000); // 每秒更新一次 } } // 在adc.c中实现 int Get_Temperture(void) { u16 adc_val = Get_Adc_Average(ADC_CHANNEL_16, 10); float vsense = (float)adc_val * 3.3f / 4096.0f; float temp_f = 25.0f + (1.43f - vsense) * 232.5581f; return (int)(temp_f * 100.0f + 0.5f); }关键实践提示:
-printf重定向至串口是调试利器,但生产环境中应替换为更轻量的USART_SendData();
-Delay_ms(1000)提供稳定采样间隔,避免ADC与其它外设(如SPI Flash)争抢总线;
- 输出格式%d.%02d精确控制小数位,避免printf浮点库带来的巨大代码体积。
我在实际项目中曾遇到一个典型案例:某工控板在高温车间运行时,温度读数持续偏高3℃。排查发现是PCB上大功率MOSFET紧邻MCU放置,热传导导致芯片结温远高于环境。最终方案是在外壳内壁安装NTC传感器,用其读数动态补偿内部传感器偏差——这印证了一个真理:再完美的算法,也需扎根于对物理世界的敬畏与实测。