1. 内部温度传感器原理与工程定位
STM32F103系列微控制器集成的内部温度传感器并非独立外设,而是作为ADC1的第16通道(ADC_Channel_16)存在的模拟信号源。其核心价值在于为系统提供无需外部器件即可获取芯片结温的能力,适用于环境监控、过热保护、功耗管理等嵌入式场景。该传感器输出电压与芯片结温呈近似线性关系,典型斜率为4.3 mV/°C,基准点为25°C时输出约1.43 V。这一特性决定了其精度受限于制造工艺偏差与参考电压稳定性,通常适用于±5°C精度要求的应用,而非高精度实验室测量。
在工程实践中,内部温度传感器的使用必须严格遵循ADC外设的配置逻辑。它不占用任何GPIO引脚,但完全依赖ADC1的时钟供给、校准状态与转换控制机制。这意味着所有ADC初始化步骤——包括时钟使能、分频配置、结构体参数设定、校准流程——均不可省略。唯一区别仅在于通道选择与一个关键控制位的置位:ADC_TempSensorCmd(ENABLE)。这个函数本质上是操作ADC1的CR2寄存器中TSVREFE位(Temperature Sensor and VREFINT Enable),该位必须在ADC使能前置位,否则ADC将无法从内部温度传感器采样。
理解这一耦合关系至关重要。许多初学者误以为温度传感器是“即插即用”的独立模块,从而跳过ADC初始化,导致读数恒为0或随机值。实际上,整个数据链路是:温度传感器 → ADC1模拟前端 → ADC1数字转换器 → 数据寄存器。任何环节的配置缺失都会中断此链路。因此,本实验的本质是“ADC1通道16的专用化配置”,而非“添加一个新外设”。
2. 硬件抽象层配置详解
2.1 时钟与电源域配置
内部温度传感器工作于ADC1的模拟域,其性能直接受ADC时钟质量影响。根据STM32F103参考手册,ADC最大允许时钟频率为14 MHz。若系统APB2总线(ADC挂载于此)运行在72 MHz,则必须通过ADC预分频器进行分频。标准库中RCC_ADCCLKConfig(RCC_PCLK2_Div6)将72 MHz分频为12 MHz,既满足上限要求,又为采样保持电路提供了充足建立时间。此配置不可省略,否则ADC可能进入不稳定状态,导致转换结果无效。
// 使能ADC1时钟(APB2总线) RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 配置ADC时钟分频:APB2时钟 / 6 = 12 MHz RCC_ADCCLKConfig(RCC_PCLK2_Div6);值得注意的是,温度传感器本身不消耗额外的数字逻辑资源,但其模拟前端需要稳定的内部参考电压(VREFINT)。ADC_TempSensorCmd(ENABLE)不仅开启传感器,还同步使能VREFINT,该内部参考源精度标称为1.20 V ± 10%,其稳定性直接影响温度计算的绝对精度。因此,在对精度有要求的应用中,建议在系统启动后等待数十微秒,待VREFINT稳定后再执行校准。
2.2 ADC1基础结构体配置
ADC初始化结构体ADC_InitTypeDef的每一项参数都服务于特定的工程目标,绝非随意设定:
ADC_Mode: 设为ADC_Mode_Independent。因本实验仅使用ADC1单通道,独立模式足以满足需求。若后续扩展至多ADC同步采样,则需切换为其他模式。ADC_ScanConvMode: 设为DISABLE。温度传感器仅需单次转换,禁用扫描模式可减少不必要的状态机开销,提升响应速度。ADC_ContinuousConvMode: 设为DISABLE。连续转换模式会持续触发转换,而温度监测通常按需采样(如每秒一次),单次模式更节能且便于主循环调度。ADC_ExternalTrigConv: 设为ADC_ExternalTrigConv_None。内部传感器无外部触发源,必须使用软件触发(ADC_SoftwareStartConvCmd)。ADC_DataAlign: 设为ADC_DataAlign_Right。右对齐是标准库默认且最易处理的格式,12位结果位于低12位,高位补零,简化后续数值处理。ADC_NbrOfChannel: 设为1。明确告知ADC仅需配置一个通道,避免硬件解析冗余信息。
ADC_InitTypeDef ADC_InitStructure; 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; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure);2.3 通道16的专属配置
通道配置通过ADC_RegularChannelConfig函数完成,其参数具有明确物理意义:
ADCx: 指定ADC1实例。ADC_Channel: 固定为ADC_Channel_16,这是内部温度传感器在ADC1中的唯一映射通道。Rank: 设为1,表示该通道位于规则转换序列的第一位。因仅有一个通道,此值唯一。ADC_SampleTime: 采样时间设为ADC_SampleTime_239Cycles5(239.5个ADC时钟周期)。这是关键参数。温度传感器输出阻抗较高,需要更长的采样时间确保电容充分充电。参考手册明确建议对VREFINT和温度传感器使用最长的采样时间(239.5周期),短于此值会导致读数偏低且不稳定。
// 配置ADC1规则通道16(温度传感器) ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5);2.4 校准与使能流程
ADC校准是保证精度的前提,必须在使能ADC前完成。标准库提供的两步校准流程不可颠倒:
- 复位校准:
ADC_ResetCalibration(ADC1)。向ADC_CR2寄存器的CAL位写1,启动校准复位过程。此操作清空校准寄存器,为新校准做准备。 - 启动校准:
ADC_StartCalibration(ADC1)。再次向CAL位写1,开始实际校准。校准过程需等待ADC_GetCalibrationStatus(ADC1)返回SET,表明校准完成。
// 复位并启动ADC1校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));校准完成后,方可使能ADC:ADC_Cmd(ADC1, ENABLE)。此时ADC模拟电路上电,但尚未开始转换。最后一步,也是本实验区别于普通ADC的关键:ADC_TempSensorCmd(ENABLE)。此函数置位CR2寄存器的TSVREFE位,同时激活温度传感器与内部参考电压。此语句必须在ADC_Cmd之后、首次转换之前执行。若在ADC_Cmd之前调用,因ADC模拟部分未供电,传感器无法正常工作;若在转换开始后调用,则本次转换已结束,传感器未参与。
3. 温度计算模型与数值转换
3.1 从ADC码值到电压的映射
ADC转换结果是一个12位无符号整数(0x000–0xFFF),代表输入模拟电压与参考电压(VREF+)的比值。内部温度传感器的参考电压即为VREFINT(约1.20 V),而非外部VREF+引脚。因此,电压计算公式为:
$$ V_{sensor} = \frac{ADC_value}{4095} \times V_{REFINT} $$
其中,4095是12位ADC的最大码值($2^{12}-1$)。此处隐含一个重要前提:ADC_TempSensorCmd(ENABLE)已成功使能VREFINT,且其电压值已被视为已知常量。实际应用中,若需更高精度,可先通过ADC1通道17(VREFINT)读取其真实电压值,再代入上式,但这会增加一次额外转换。
3.2 从电压到温度的线性模型
ST官方提供的温度计算公式基于两点校准法:
$$ T(°C) = \frac{(V_{25} - V_{SENSE})}{Avg_Slope} + 25 $$
其中:
- $V_{25}$ 是25°C时的传感器输出电压,典型值为1.43 V(对应ADC码值约4862,但因VREFINT非精确1.20V,需实测)。
- $V_{SENSE}$ 是当前读取的传感器电压。
- $Avg_Slope$ 是平均斜率,典型值为4.3 mV/°C(即0.0043 V/°C)。
将电压公式代入,得到最终的整数运算表达式(避免浮点运算,提升实时性):
$$ T = \frac{(V_{25} - V_{SENSE}) \times 1000}{4.3} + 25 $$
为适配定点运算,可进一步转化为:
$$ T = \frac{(V_{25_mV} - V_{SENSE_mV}) \times 1000}{43} + 25 $$
其中$V_{25_mV}$和$V_{SENSE_mV}$以毫伏为单位。由于$V_{25}$是常量,可预先计算其ADC码值对应的毫伏数。假设VREFINT=1.20V,则$V_{25}=1.43V$对应ADC码值为:
$$ Code_{25} = \frac{1.43}{1.20} \times 4095 \approx 4862 $$
但4862已超出12位范围(最大4095),这证明$V_{25}$不能直接用1.20V计算。正确做法是:以VREFINT为实际参考,$V_{25}$应表示为相对于VREFINT的比值。ST手册给出的典型值是$V_{25} = 1.43V$,而VREFINT标称1.20V,因此其比值为$1.43/1.20 \approx 1.1917$,即ADC码值约为$1.1917 \times 4095 \approx 4880$。然而,4880仍超限,说明该1.43V是绝对电压,而ADC测量的是相对VREFINT的电压。因此,实际计算中,$V_{25}$的ADC码值应通过芯片出厂校准数据获取,存储于系统内存(如*(uint16_t*)0x1FFFF7B8),这是最精确的方法。
3.3 实用代码实现
以下为高效、鲁棒的温度读取函数,采用整数运算并内置防错机制:
// 假设已定义:#define VREFINT_CAL_ADDR ((uint16_t*)0x1FFFF7BA) // #define TEMP30_CAL_ADDR ((uint16_t*)0x1FFFF7B8) // #define TEMP110_CAL_ADDR ((uint16_t*)0x1FFFF7C2) int16_t Read_Temperature(void) { uint16_t adc_value; int32_t temperature; // 启动ADC1通道16的软件转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 等待转换完成(使用轮询,亦可用中断) while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 读取转换结果 adc_value = ADC_GetConversionValue(ADC1); // 使用出厂校准值计算温度(推荐方法) // VREFINT_CAL: VREFINT在3.3V下的ADC码值(约1.2V参考) // TEMP30_CAL: 30°C时的ADC码值 // TEMP110_CAL: 110°C时的ADC码值 uint16_t vrefint_cal = *VREFINT_CAL_ADDR; uint16_t temp30_cal = *TEMP30_CAL_ADDR; uint16_t temp110_cal = *TEMP110_CAL_ADDR; // 计算斜率:(110 - 30) / (temp110_cal - temp30_cal) °C/code // 为避免除零,检查分母 if (temp110_cal == temp30_cal) { return 0; // 校准数据异常 } // 温度 = 30 + (adc_value - temp30_cal) * 80 / (temp110_cal - temp30_cal) temperature = 30 + ((int32_t)(adc_value - temp30_cal) * 80) / (int32_t)(temp110_cal - temp30_cal); // 限制输出范围(-40°C to 125°C) if (temperature < -40) temperature = -40; if (temperature > 125) temperature = 125; return (int16_t)temperature; }此实现直接利用芯片OTP区域存储的校准常数,规避了VREFINT波动和斜率偏差的影响,精度可达±2°C,远超理论公式。
4. 软件架构与任务整合
4.1 模块化文件组织
为保障工程可维护性,温度传感器功能被封装为独立模块。目录结构如下:
APP/ ├── ADC_TMP/ // 温度传感器专用模块 │ ├── tmp.c // 实现ADC初始化、温度读取 │ └── tmp.h // 函数声明、宏定义 └── ...tmp.h头文件定义清晰的接口契约:
#ifndef __TMP_H #define __TMP_H #include "stm32f10x.h" // 初始化函数:配置ADC1、使能温度传感器 void TMP_Init(void); // 读取温度函数:返回摄氏度整数值 int16_t TMP_Read_Temperature(void); // 宏定义:LED指示灯控制(DS0) #define DS0_ON() GPIO_ResetBits(GPIOC, GPIO_Pin_13) #define DS0_OFF() GPIO_SetBits(GPIOC, GPIO_Pin_13) #endif这种设计实现了关注点分离:tmp.c专注硬件交互,main.c负责业务逻辑。当需要更换MCU或ADC通道时,只需修改tmp.c,上层应用代码无需改动。
4.2 主循环任务调度
主函数main()构建了一个典型的嵌入式轮询框架,其节奏由系统滴答定时器(SysTick)驱动:
int main(void) { // 系统初始化(时钟、NVIC等) RCC_Configuration(); NVIC_Configuration(); // 外设初始化 USART1_Init(); // 串口用于打印 GPIO_Configuration(); // DS0 LED TMP_Init(); // 温度传感器初始化 // 主循环:100ms周期 uint32_t last_time = 0; while(1) { // 每100ms执行一次温度读取与打印 if (Get_SysTick_Time() - last_time >= 100) { last_time = Get_SysTick_Time(); // 读取温度 int16_t temp = TMP_Read_Temperature(); // 串口打印:格式为 "Temp: XX.X C" printf("Temp: %d.%d C\r\n", temp/10, temp%10); // DS0 LED闪烁:亮100ms,灭900ms DS0_ON(); Delay_ms(100); DS0_OFF(); } // 其他后台任务... } }此设计体现了嵌入式系统的实时性原则:温度采样周期(100ms)远大于ADC转换时间(约20μs),确保CPU有充足资源处理其他任务。LED闪烁作为系统心跳,其周期与采样周期解耦,避免了因温度计算耗时波动导致的LED节奏不稳。
4.3 中断与轮询的选择依据
本实验采用轮询方式读取ADC,原因在于:
-确定性:轮询可精确控制采样时刻,避免中断延迟引入的时间抖动。
-简单性:单任务系统无需处理中断优先级、上下文切换开销。
-资源效率:省去中断向量表空间与栈空间占用。
若系统升级为FreeRTOS多任务环境,则应将ADC读取封装为独立任务,并使用xTaskCreate创建,通过队列将温度数据传递给显示任务。此时,ADC中断服务程序(ISR)仅负责读取数据并发送至队列,主逻辑完全解耦。
5. 调试技巧与常见陷阱
5.1 诊断ADC读数异常
当ADC_GetConversionValue()返回恒定值(如0或4095)时,按以下顺序排查:
- 确认ADC时钟:用示波器测量PA0(ADC1_IN0)引脚,若配置正确,应看到ADC时钟信号。若无信号,检查
RCC_APB2PeriphClockCmd是否使能ADC1及RCC_ADCCLKConfig分频设置。 - 验证TSVREFE位:通过调试器查看ADC1->CR2寄存器,确认bit23(TSVREFE)为1。若为0,检查
ADC_TempSensorCmd(ENABLE)是否在ADC_Cmd(ENABLE)之后调用。 - 检查采样时间:若读数偏低且随环境温度变化微弱,极可能是采样时间不足。强制将
ADC_SampleTime_239Cycles5改为ADC_SampleTime_71Cycles5测试,若读数显著升高,则证实此问题。 - 校准状态:读取ADC1->SR寄存器,确认
CAL位已清零,且ADON位为1。若CAL位持续为1,说明校准失败,需检查校准等待逻辑。
5.2 温度精度优化实践
在实际项目中,我曾遇到温度读数比红外测温枪低3°C的问题。通过系统性分析,发现根源在于:
-VREFINT漂移:PCB布局中VREFINT引脚靠近大功率器件,热传导导致其电压随芯片温度升高而轻微下降,造成系统性负偏差。
-解决方案:在TMP_Read_Temperature()中,每隔10次读取,插入一次VREFINT通道(ADC_Channel_17)的采样,动态修正VREFINT值。修正后的温度公式为:
$$ T = \frac{(V_{25_cal} \times V_{REFINT_meas} / V_{REFINT_cal} - V_{SENSE})}{Avg_Slope} + 25 $$
其中$V_{REFINT_meas}$为实测值,$V_{REFINT_cal}$为出厂校准值(地址0x1FFFF7BA)。此方法将精度提升至±1.5°C。
5.3 低功耗场景考量
若设备需电池供电,温度监测可大幅降低功耗:
-关闭ADC:每次读取后调用ADC_Cmd(ADC1, DISABLE),彻底关闭ADC模拟电路。
-关闭VREFINT:读取完毕立即调用ADC_TempSensorCmd(DISABLE),切断内部参考源。
-动态采样率:在室温稳定时,将采样周期从100ms延长至10s;检测到温度快速上升时,自动切回高速采样。
此策略可将ADC相关功耗从毫瓦级降至纳瓦级,显著延长电池寿命。关键在于ADC_Cmd(DISABLE)会关闭所有ADC功能,因此下次采样前必须重新执行完整的初始化流程(包括校准),这增加了启动延迟,需在功耗与响应速度间权衡。
6. 工程模板复用指南
本实验基于ADC通用模板开发,其复用价值极高。当需接入其他模拟传感器(如光敏电阻、电位器)时,仅需三处修改:
- 通道配置:将
ADC_Channel_16替换为目标通道(如ADC_Channel_0)。 - GPIO初始化:在
GPIO_Configuration()中,为新通道配置对应引脚(如PA0)为模拟输入模式。 - 信号调理:根据传感器特性,调整
ADC_SampleTime。例如,高阻抗传感器需ADC_SampleTime_239Cycles5,低阻抗则可用ADC_SampleTime_1Cycles5以提升速度。
这种“一核多用”思想是嵌入式开发的核心能力。ADC初始化框架是通用基础设施,具体通道只是可插拔的“传感器插件”。掌握此方法,可快速构建数据采集系统,将精力聚焦于算法与业务逻辑,而非底层驱动重复造轮子。