STM32F103内部温度传感器ADC采样与串口输出实战
简介:这份资源面向正在学习STM32嵌入式开发、需要动手实践ADC与温度采集的初学者与进阶者,基于野火STM32F103指南者开发板,提供一套可直接编译运行的简易温度读取工程。压缩包共366个文件,约17.18MB,以C源码与头文件为核心,包含52个.c与58个.h文件,覆盖stm32f10x_adc、tim、rcc、usart、i2c、can等标准外设库模块;同时附带Keil工程文件(uvprojx、uvoptx、sln)、编译中间产物(o、d、crf、axf、hex、map)以及少量图片与说明文档,便于直接打开工程、烧录验证与二次修改。已有51人学习下载,适合作为ADC单通道采集、内部温度传感器读取或外接温度模块实验的参考模板。读者可借此理解STM32F103的时钟配置、ADC初始化与数据转换流程,并对照工程结构快速搭建自己的温度采集程序,减少从零建工程的调试成本。
1. 从一块野火STM32F103指南者开始:温度读取到底在练什么
手里有一块野火STM32F103指南者,想读个温度,这件事听起来简单到不值得写一篇文章。但真正动手的人很快会发现,卡住你的从来不是"温度"两个字,而是STM32F103的ADC怎么配、内部温度传感器通道是哪一个、参考电压从哪来、读出来的电压怎么换算成摄氏度、串口怎么把结果打出来。这套流程走通一遍,等于把STM32的时钟树、GPIO复用、ADC采样、串口收发、Keil MDK工程搭建全部串了一遍,是典型的"小需求撬动大知识面"。
这篇笔记面向三类人:刚拿到指南者板子、想找个最小项目练手的新手;做过STM32但没碰过ADC内部通道的熟手;以及想拿"基于STM32的简易温度读取"做课程设计或毕业设计起步的人。我会按"工程怎么建 → ADC怎么配 → 温度怎么算 → 串口怎么看 → 坑在哪"的顺序讲,每一步都给出可复现的配置和代码,参数为什么这么设也一并说清。读完你应该能在一块空板上从零跑出温度值,并且知道读数飘、跳变、卡死时该往哪查。
2. 工程搭建与ADC采样链路:先把数据通路打通
2.1 为什么选内部温度传感器而不是外接DS18B20
STM32F103芯片内部自带一个温度传感器,连在ADC1的通道16上,不需要任何外部元件,上电就能读。它的绝对精度一般,手册给的典型误差在±1.5℃左右,适合做趋势观察和练手,不适合做精密测温。如果你要的是±0.5℃以内的精度,那应该外接DS18B20或者PT100,走单总线或运放调理。但对于"简易温度读取"这个目标,内部通道是最省事、最能暴露ADC配置问题的入口——它逼你把采样时间、参考电压、通道切换这些事想明白。
选内部通道还有一个好处:它和外部通道共用同一套ADC配置逻辑。你把通道16读通了,换成通道0读PA0上的外部电压,只需要改通道号和采样时间,其余代码不动。所以这个项目真正的价值不是温度,是把ADC这条链路跑通。
2.2 用Keil MDK建工程:从芯片包到可编译
野火指南者用的是STM32F103VET6,属于F103大容量系列。建工程第一步是装芯片包,也就是常说的Pack包。Keil MDK5之后芯片支持包和IDE分离,没装Pack的话新建工程时器件列表里根本找不到F103。
操作顺序是这样的:打开Keil,点Pack Installer,在Devices里找STMicroelectronics → STM32F1 Series → STM32F103,安装对应的DFP包。装完新建工程,选STM32F103VE。然后勾选运行环境里的CMSIS Core和Device Startup,标准外设库或HAL库按你的习惯选。野火早期教程多用标准库,现在新项目建议直接上HAL,CubeMX生成初始化代码更省心。
# 工程目录建议这样组织,别把所有文件堆在根目录 TempReader/ ├── Core/ │ ├── Inc/ # 头文件 │ └── Src/ # main.c、adc.c、usart.c ├── Drivers/ │ ├── CMSIS/ # 内核头文件 │ └── STM32F1xx_HAL_Driver/ ├── MDK-ARM/ # Keil工程文件 └── TempReader.ioc # CubeMX配置文件目录分层的意义在于:CubeMX重新生成代码时只覆盖Core目录,你写在Drivers里的东西不会被冲掉。很多人把代码全写在main.c里,一重新生成就全没了,这是血泪经验。
2.3 ADC时钟与采样时间:两个决定读数稳不稳的参数
STM32F103的ADC挂在APB2总线上,最高时钟不能超过14MHz。系统跑72MHz时,APB2分频后通常是72MHz,ADC预分频器要设成6分频,得到12MHz,落在安全范围内。这一步如果配错,比如ADC时钟给到36MHz,读数会明显偏差甚至乱跳。
采样时间决定采样电容充电是否充分。内部温度传感器输出阻抗较高,手册建议采样时间不低于17.1μs。在12MHz ADC时钟下,一个采样周期是1/12MHz≈83ns,239.5个周期约20μs,满足要求。所以内部通道的采样时间要设成239.5周期,这是最稳妥的选择。
// adc.c 关键配置片段(HAL库) ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 单通道,不扫描 hadc1.Init.ContinuousConvMode = DISABLE; // 单次转换,按需触发 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐,读出来就是原始值 hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_TEMPSENSOR; // 通道16,内部温度传感器 sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 高阻源必须长采样 HAL_ADC_ConfigChannel(&hadc1, &sConfig);参数说明:ScanConvMode关掉是因为只读一个通道;ContinuousConvMode关掉是为了按需采样,避免DMA一直搬数据;DataAlign右对齐让12位结果直接落在寄存器的低12位,换算时不用移位;SamplingTime选239.5周期是给内部传感器的高输出阻抗留足充电时间。这几项里,采样时间是最容易被忽略又最影响结果的,设短了读数会偏低且不稳。
2.4 单次采样与多次平均:读一次和读得准是两回事
配好ADC只是能读,读得准还得处理噪声。内部温度传感器本身噪声不小,单次采样跳个两三度很正常。常见做法是连续采16次或32次,去掉最大最小再取平均,或者直接算均值。我一般采32次求平均,兼顾速度和稳定。
// 连续采样N次求平均,返回原始ADC值 #define SAMPLE_TIMES 32 uint16_t ADC_ReadAverage(void) { uint32_t sum = 0; for (uint8_t i = 0; i < SAMPLE_TIMES; i++) { HAL_ADC_Start(&hadc1); // 启动转换 HAL_ADC_PollForConversion(&hadc1, 10); // 等待完成,超时10ms sum += HAL_ADC_GetValue(&hadc1); // 累加原始值 HAL_ADC_Stop(&hadc1); } return (uint16_t)(sum / SAMPLE_TIMES); // 取平均 }逻辑说明:每次Start后必须Poll等待转换完成,否则GetValue读到的是上一次的旧值,这是新手最常翻车的地方。超时参数给10ms是防止硬件异常时死等。平均次数不是越多越好,32次在72MHz下耗时约几毫秒,再多人眼就能感觉到刷新变慢。如果你要更快的响应,可以降到8次,但读数会明显更跳。
3. 温度换算与串口输出:把ADC值变成能看的数字
3.1 从ADC原始值到电压:参考电压是换算的锚点
ADC读出来的是12位数字量,0到4095,对应0V到VREF+。STM32F103的VREF+在100脚以上封装里是独立引脚,指南者板上一般直接接到3.3V。所以换算公式是:电压 = 原始值 × 3.3 / 4095。
这里有个坑:如果你用的是VDD作为参考(没有独立VREF+引脚的封装),而VDD实际不是精确3.3V,比如USB供电时是3.28V,那换算就会带系统误差。要精确的话,应该用内部参考电压通道VREFINT(通道17)反推实际VREF。对练手项目,直接用3.3V够用;对要出数据的项目,这一步不能省。
// 原始值转电压,vref单位V float ADC_ToVoltage(uint16_t raw, float vref) { return (float)raw * vref / 4095.0f; // 12位ADC满量程4095 }参数说明:4095是12位满量程,不是4096,虽然差1影响极小,但严谨写法用4095。vref传3.3f。这个函数对内部温度和外部通道通用,换通道不用改。
3.2 内部温度传感器的换算公式:斜率与偏移从哪来
STM32F103手册给出的温度换算关系是:温度 = (V25 - Vsense) / Avg_Slope + 25。其中V25是25℃时的传感器输出电压,典型值1.43V;Avg_Slope是斜率,典型值4.3mV/℃。注意公式里是V25减Vsense,因为温度升高时传感器输出电压是下降的,斜率为负。
// 内部温度传感器换算,返回摄氏度 float Get_ChipTemperature(void) { uint16_t raw = ADC_ReadAverage(); float vsense = ADC_ToVoltage(raw, 3.3f); // 转成电压 float temp = (1.43f - vsense) / 0.0043f + 25.0f; return temp; }逻辑说明:1.43和0.0043是典型值,不是每颗芯片的实测值。同一批芯片之间V25可能差几十毫伏,对应好几度误差。如果你发现读数整体偏高或偏低一个固定值,大概率就是V25和实际不符,可以拿一个已知温度环境标定一下,把1.43改成实测值。斜率4.3mV/℃相对稳定,一般不用改。
3.3 串口打印:重定向printf和它的三个配置点
要把温度值看到,最方便的是串口。STM32F103指南者板载了USB转串口,接USART1的PA9/PA10。用printf需要重定向fputc,同时确保串口初始化正确、微库开启。
// 重定向fputc到USART1 #include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10); return ch; }三个必须检查的点:第一,Keil的Target选项里勾选Use MicroLIB,否则printf可能不工作或体积巨大;第二,串口波特率两边一致,常用115200;第三,PA9配成复用推挽输出,PA10配成浮空输入或上拉输入。这三点缺一个,串口就是黑匣子,什么都看不到。
// main循环里打印 while (1) { float t = Get_ChipTemperature(); printf("Chip Temp: %.2f C\r\n", t); HAL_Delay(500); // 500ms刷新一次,太快看不清 }参数说明:%.2f保留两位小数,实际有效精度到不了两位,但看着舒服。\r\n是回车换行,很多串口助手只认\r\n,只写\n会不换行。HAL_Delay(500)给500ms,既能看到变化又不刷屏。如果你要连续监测,可以改成100ms,但要注意printf本身耗时,太快会拖慢主循环。
4. 避坑与排查:温度读数不对时先查这五处
4.1 读数恒为0或恒为4095
现象:串口打出来的温度要么是-273℃附近,要么是几百度,原始值不是0就是4095。
原因:ADC根本没启动,或者通道配错。恒0通常是没调用HAL_ADC_Start,或者Poll超时后GetValue读到空;恒4095是输入电压超过参考电压,或者通道选成了VSS。
解决:先单独打印原始ADC值,确认它在合理范围(内部温度通道常温下大约在1700到1800之间)。如果原始值就是0或4095,回去查ADC初始化有没有执行、通道号是不是ADC_CHANNEL_TEMPSENSOR、VREF+引脚有没有接好。
4.2 读数跳变超过5℃
现象:温度值在几度范围内快速跳动,静置也稳不下来。
原因:采样时间太短,采样电容没充够;或者没有做平均,单次噪声直接暴露;也可能是电源纹波大,VDD不稳。
解决:把采样时间从默认的1.5周期改成239.5周期,加32次平均。如果还跳,用示波器看VDD纹波,必要时在VREF+和VDD上加0.1μF去耦电容。指南者板上一般已有,但如果你自己搭的最小系统,这一步不能省。
4.3 温度整体偏高或偏低一个固定值
现象:读数稳定,但和室温差3到5度,且不随环境变化。
原因:V25典型值1.43V和你的芯片实际值不符,这是芯片个体差异,不是bug。
解决:把板子放在已知温度环境(比如空调房稳定25℃)静置十分钟,读原始电压,反推你的V25:V25 = Vsense + 0.0043 × (T - 25)。把算出来的值替换代码里的1.43。这一步做完,误差能压到1度以内。
4.4 串口无输出或乱码
现象:程序下载成功,但串口助手一片空白,或者全是乱码。
原因:乱码基本是波特率不匹配,空白可能是fputc没重定向、MicroLIB没勾、或者串口引脚配错。
解决:先确认两边波特率都是115200。再检查Keil的Use MicroLIB有没有勾。然后确认PA9/PA10的复用配置,PA9必须是GPIO_MODE_AF_PP,PA10是GPIO_MODE_IN_FLOATING。最后用示波器或逻辑分析仪看PA9有没有波形,有波形说明发送正常,问题在接收端配置。
4.5 程序跑一段时间后卡死
现象:刚开始正常打印,几十秒后停住,复位又能跑一会。
原因:HAL_Delay在中断里被调用导致死锁,或者PollForConversion超时后没处理,或者printf阻塞在串口发送上。
解决:检查有没有在中断服务函数里调HAL_Delay,有的话换成非阻塞延时。PollForConversion的超时返回值要判断,超时就跳过这次采样而不是继续。串口发送如果量大,考虑用DMA或者降低打印频率。我一般把打印间隔放到500ms以上,基本不会卡。
5. 进阶:用VREFINT校准和外部通道扩展把精度提上去
前面用的是固定3.3V参考,实际USB供电时VDD可能在3.2到3.4之间飘,这会直接反映到温度上。STM32F103内部有一个参考电压通道VREFINT,连在ADC1的通道17上,它的电压出厂校准过,典型值1.20V,且不随VDD变化。用它反推实际VREF,能把电源波动带来的误差消掉。
做法是:先读通道17的原始值,用已知的VREFINT校准值(存在芯片信息区,地址0x1FFFF7BA,单位mV)算出实际VREF,再用这个VREF去换算温度通道。
// 用VREFINT反推实际参考电压 #define VREFINT_CAL_ADDR ((uint16_t*)0x1FFFF7BA) // 校准值存放地址 #define VREFINT_CAL_VREF 3300 // 校准时的参考电压mV float Get_ActualVref(void) { uint16_t vrefint_raw; // 切换到通道17采样 ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_VREFINT; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); vrefint_raw = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); // 实际VREF = 校准值 × 校准参考 / 当前原始值 float vref = (float)(*VREFINT_CAL_ADDR) * VREFINT_CAL_VREF / vrefint_raw / 1000.0f; return vref; // 单位V }逻辑说明:VREFINT的电压是固定的,ADC读出来的原始值和VREF成反比。校准值是在3.3V参考下测得的原始值,存进信息区。用校准值乘以3.3再除以当前原始值,就得到当前实际VREF。这个值通常在3.2到3.4之间,用它替换温度换算里的3.3,电源波动的影响就消掉了。
参数说明:0x1FFFF7BA是F103的VREFINT校准值地址,不同系列地址不同,F4在0x1FFF7A2A,用之前查对应手册。VREFINT_CAL_VREF固定3300mV,这是出厂校准条件。采样时间同样要239.5周期,VREFINT也是高阻源。
把VREFINT校准加进去之后,温度读数的重复性能到0.5℃以内,绝对值误差取决于你有没有标定V25。如果两个都做了,内部传感器做到±1℃是现实的。再往上就要外接传感器了,内部通道的天花板就在这。
外部通道扩展也顺带说一句:把sConfig.Channel改成ADC_CHANNEL_0,采样时间按外部源阻抗调整,就能读PA0上的电压。如果接的是NTC热敏电阻,换算公式换成Steinhart-Hart或者查表,其余代码不动。这就是先把内部通道跑通的价值——换传感器只是换个换算函数的事。
我自己的习惯是,每做一个新板子,先跑一遍内部温度和VREFINT,确认ADC链路和参考电压都正常,再去接外部传感器。这一步花不了十分钟,但能省掉后面"到底是传感器问题还是ADC问题"的反复排查。希望帮到你。
本文还有配套的精品资源,点击获取