简介STM32f103PT100.zip是基于STM32F103C8T6的PT100温度采集与串口传输完整工程面向嵌入式入门及进阶开发者解决使用内部ADC采集PT100电阻信号并发送至上位机的核心问题。压缩包共253个文件大小约7.42MB包含40个.h头文件、38个.c源文件、Keil工程文件uvprojx/uvoptx、Hex固件、PDF说明及编译过程生成的中间文件结构完整既可直接打开工程运行也可按模块研读代码。已有1214人学习下载。工程示例展示了STM32F103C8T6的ADC通道配置、采样时间与参考电压设置、连续转换与DMA/中断读取方式以及USART串口波特率、数据位等参数配置同时涵盖PT100传感器阻值与温度的换算思路和计算机端帧解析逻辑。通过阅读源码可掌握从模拟信号采集、数字转换到串行通信的完整嵌入式数据链路理解HAL库函数调用与DMA传输特点并能在实际调试中快速定位配置难点适合需要快速上手温度测量项目或学习STM32外设配合的开发者参考。1. 这个项目是什么一块STM32F103C8T6最小板怎么把PT100变成温度数据我在给某实验室的恒温槽做温度记录时手头只有一块STM32F103C8T6最小板和一支PT100探头。翻遍网上代码要么只给了ADC读取要么用了昂贵的专用芯片很难直接跑起来。后来拿到这份STM32f103PT100工程包发现它把采集、滤波、换算、串口输出全串好了解压后按电路接上就能看到稳定温度。这个工程适合两类人第一次接触PT100的嵌入式开发者以及做低成本温度仪表时需要可改造基线的人。它解决的不只是“ADC采电压”而是从电阻变化到温度数值的完整链路包括采样抖动怎么压、接线误差怎么补、数据和上位机怎么对接。2. 硬件选型与电路设计PT100电阻变化到ADC电压的完整链路2.1 PT100阻温特性与分压采样方案的选择PT100是铂热电阻IEC标准下0℃标称阻值100Ω0~100℃之间每升高1℃阻值增加约0.385Ω。这个变化量非常小如果直接把PT100接在ADC引脚上读到的只是几百毫伏级别的小信号。所以PT100测温的关键不是代码而是先把阻值变化转化成ADC能分辨的电压变化。工程里采用的是分压电路3.3V电源经过一个参考电阻R_ref再经过PT100到地ADC引脚接在PT100和参考电阻的中间。温度变化时PT100阻值变化分压点电压也跟着变化ADC通过测量这个电压反推PT100阻值。这个方案的优点是元件少、调试简单只需要一个参考电阻缺点是灵敏度受电流限制且电源电压波动会直接影响测量结果。实际产品里更严谨的方案是恒流源。给PT100注入1mA恒定电流电阻两端的电压就是0.1V~0.138V再用差分运放放大到ADC的0~3.3V范围内。恒流源线性好也不受VCC波动影响但要多用一颗精密基准或恒流二极管还要配运放和反馈电阻PCB面积和成本都上来了。如果是做一个低成本板子硬件就会刻意收紧。这支探头的测量范围是-50~150℃在这个区间PT100阻值大约80.31Ω到157.33Ω。分压电路在-50℃和150℃时的输出电压范围决定了ADC能不能覆盖全量程。比如用3.3V供电、R_ref1kΩ-50℃时分压点电压是0.245V150℃时是0.393VADC值分别约303和488。可以看到动态范围很小但通过后面的查表和滑动滤波能把有效分辨率从1个LSB提升到0.1℃级别。这就是软件侧要做的工作而不是盲目换更大倍数的运放。另一个容易被忽略的点是PT100的引线电阻。如果是两线制接法导线本身的铜电阻会直接串联进PT100每米几十毫欧到上百毫欧线一长就会造成正温度误差。这个工程默认按短线连接如果你要用长线要么改成三线制补偿要么在软件里做固定补偿。2.2 参数计算上拉电阻选多大才合适分压电路里除了PT100另一个关键元件就是参考电阻R_ref阻值大小直接决定分辨率和自热功耗。参考电阻选得越小流经PT100的电流越大分压点电压变化越大ADC分辨率越高但PT100自热也越明显反而引入误差。R_ref0℃电流0~100℃电压差ADC跨度近似温度分辨率2.2kΩ1.4mA52mV64 LSB约1.6℃/LSB1kΩ3.0mA101mV126 LSB约0.8℃/LSB510Ω5.4mA165mV204 LSB约0.5℃/LSB上表中的ADC跨度是12位ADC在3.3V参考电压下的理论值。可以看到R_ref2.2k时功耗最小但温度分辨率只有1.6℃/LSBR_ref510Ω时分辨率明显改善但0℃时PT100上的自热功率约为2.9mW已经接近普通薄膜PT100的推荐上限。工程代码里默认按R_ref1kΩ来算这是折中分辨率够用自热风险不大电阻放在板上也不会烫手。分压公式的推导也不复杂V_ADC VCC * R_pt / (R_ref R_pt)。整理出R_pt V_ADC * R_ref / (VCC - V_ADC)。在代码里V_ADC由ADC数值换算VCC和R_ref作为常量放在头文件中。这里有个细节VCC不是芯片手册标的3.3V精确值而是你实际供电电压。如果VCC是3.28V你在代码里写死3.3就会引入约0.6%的电阻误差换算成温度大约是1.5℃。因此我一般建议用万用表实测板子供电电压填入宏定义这也是第一次跑板子要留意的玄学问题。另一个关键是对参考电阻精度的要求。普通5%贴片电阻温度系数可能到±200ppm/℃环境温度一变化参考电阻值跟着变测温结果自然漂。我一般会选0.1%精度、25ppm/℃的电阻一颗不贵但对长期稳定性帮助很大。如果手上只有5%电阻那就要用校准流程把它的偏差修正掉。2.3 工程目录结构从.ioc到算法的文件边界压缩包解压后目录结构大致如下STM32f103PT100/ ├── STM32f103PT100.ioc # CubeMX 配置可重新生成外设代码 ├── Core/ │ ├── Inc/ │ │ ├── main.h # 全局宏、引脚定义 │ │ ├── adc.h # ADC句柄声明 │ │ └── pt100.h # 滤波与换算接口 │ └── Src/ │ ├── main.c # 初始化调用与主循环 │ ├── adc.c # ADC1 规则组配置、MspInit │ ├── usart.c # USART1 引脚与printf重定向 │ └── pt100.c # 滑动滤波、电压/温度换算 ├── Drivers/ │ ├── CMSIS │ └── STM32F1xx_HAL_Driver └── MDK-ARM/ # MDK 工程文件双击可直接打开这个拆分有一个明显好处外设初始化和业务算法分在两个目录里。CubeMX重新生成代码时只要不动adc.c和main.c里标记为USER CODE的区域pt100.c里的算法不会丢失。我把pt100相关的东西全放在一个文件里后续改成多点校准或者换恒流源电路只改这个文件就行。pt100.h里定义了几个最关键的量ADC_REF_VOLT、R_REF、PT100_R0、ADC_MAX还有滤波窗口长度FILTER_LEN。所有换算函数都只依赖这几个宏不直接碰HAL的句柄。这样做的目的是让算法文件便于单独验证也方便以后移植到其它Cortex-M3平台时只重写adc.c和usart.c。如果你打开main.c会看到初始化顺序是HAL_Init - SystemClock_Config - MX_GPIO_Init - MX_ADC1_Init - MX_USART1_UART_Init。之后进入主循环调用pt100.c提供的两个函数sensor_reset()和sensor_read_temp()。sensor_reset用于首次上电清空滤波缓冲区sensor_read_temp返回一个float温度值。这个接口非常小二次开发时不需要翻太多底层代码。3. 核心代码拆解一次温度采样从触发到上抛的完整链路3.1 初始化流程ADC1规则组和USART1的配置工程用HAL库实现ADC初始化的入口在adc.c。STM32F103C8T6只有一个ADC1最多可以测10个外部通道这里只用PA1作为采样输入。初始化的关键参数如下void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; // 单通道不是扫描 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换 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_1; // PA1 对应 ADC1_IN1 sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); }这里最容易被忽略的是SamplingTime。PT100分压点的源阻抗是参考电阻与PT100并联后的等效阻抗通常在几百欧配1k~2.2k电阻时不能直接用最快的1.5周期采样。常见做法是选择239CYCLE给内部采样电容足够的充电时间否则ADC结果会整体偏小且不稳定。如果你用10k的高阻分压这个采样时间可能还要更长。在HAL_ADC_MspInit里CubeMX会为ADC1使能时钟并把PA1配置为模拟输入。这个函数是HAL_ADC_Init自动回调的不需要手动调用但要注意PA1之前如果被初始化成其它复用要用MX_GPIO_Init覆盖回去。代码大致是void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }F103的ADC时钟由APB2分出最高不要超过14MHz。如果你在SystemClock_Config里看到ADC分频设成6APB2是72MHz那ADC时钟就是12MHz这是稳妥值。分频太小会导致部分通道采样周期不准分频太大则会让ADC无法满速转换。这些参数都能在.ioc文件里重新配置但要注意改完后重新生成代码可能覆盖你的用户区最好提前看一下哪些是自动生成的不能动。3.2 读取ADC与滑动平均滤波主循环里读取ADC的方式是查询法调用HAL_ADC_Start开始转换然后阻塞等待转换完成标志再拿结果。代码在pt100.c里封装了两个函数一个是单次采样一个是滑动平均滤波#define FILTER_LEN 16 static uint16_t sample_buf[FILTER_LEN]; static uint32_t sample_sum; static uint8_t sample_idx; uint16_t sensor_read_adc(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); return HAL_ADC_GetValue(hadc1); } uint16_t sensor_filtered_adc(void) { uint16_t value sensor_read_adc(); if (sample_sum 0) { for (uint8_t i 0; i FILTER_LEN; i) sample_buf[i] value; sample_sum (uint32_t)value * FILTER_LEN; sample_idx 0; } sample_sum - sample_buf[sample_idx]; sample_buf[sample_idx] value; sample_sum value; sample_idx (sample_idx 1) % FILTER_LEN; return (uint16_t)(sample_sum / FILTER_LEN); }滑动平均的本质是维护一个长度为FILTER_LEN的环形队列每次用新采样值替换最旧的值然后求和除以长度。这样输出值对瞬时突发干扰不敏感同时跟随趋势的滞后也只有FILTER_LEN个采样周期。工程里设16点如果每50ms采样一次滤波响应时间约800ms温度突变时不会立刻看到最终值改成64点会更平滑但滞后明显需要根据自己的控制周期调整。为什么不用中值滤波中值滤波擅长剔除脉冲噪声但对连续的缓慢变化信号效果和平均差不多而且每轮要触发排序16个点做冒泡比较虽然不大但在C8T6上不划算。PT100本身是热惯性器件信号变化本来就慢滑动平均这种简洁数字滤波更适合。如果嫌查表插值结果还有抖动可以再接一个一阶低通但注意累计延时不要超过你想要的响应速度。这里有一个非常容易翻车的点sample_sum如果是uint16_t16个点加3000左右的ADC值会溢出。工程里把它定义成uint32_t计算完再截断回uint16_t。你可能觉得这是小事但我见过好多份代码在这里用uint16_t堆叠换了几十个温度点后平均值开始漂。这种事排查起来很费劲。3.3 电压到电阻再到温度的换算采样值拿到后第一步从ADC码值换回分压点电压第二步反推PT100阻值第三步把阻值换算成温度。代码float resistance_from_adc(uint16_t adc_value) { float v_pt (float)adc_value * ADC_REF_VOLT / 4095.0f; if (v_pt 0.0f || v_pt ADC_REF_VOLT) return -1.0f; return v_pt * R_REF / (ADC_REF_VOLT - v_pt); } float temperature_from_resistance(float r_pt) { if (r_pt PT100_R0 - 20.0f || r_pt PT100_R0 200.0f) return 0.0f; return (r_pt - PT100_R0) / 0.385f; }线性近似在0~50℃范围内误差不大但到100℃附近会偏小约1℃。如果只要求测热水温度这个误差还能接受如果要做恒温槽控制我一般不会直接用线性公式而是把几个温度点做成查表再线性插值。工程里pt100.c保留了一个简化版本的PT100表用5个点覆盖-50~100℃typedef struct { float r_pt; float temp; } pt100_cal_t; static const pt100_cal_t pt100_table[] { { 80.31f, -50.0f}, { 90.19f, -25.0f}, { 100.00f, 0.0f}, { 119.40f, 50.0f}, { 138.51f, 100.0f}, }; float temp_from_table(float r_pt) { for (uint8_t i 0; i 5; i) { if (r_pt pt100_table[i].r_pt) { if (i 0) return pt100_table[0].temp; float r0 pt100_table[i-1].r_pt; float r1 pt100_table[i].r_pt; float t0 pt100_table[i-1].temp; float t1 pt100_table[i].temp; return t0 (r_pt - r0) * (t1 - t0) / (r1 - r0); } } return pt100_table[4].temp; }注意查表插值时每个点的阻值必须和温度一一对应且要覆盖探头可能出现的全程。如果测量范围到150℃表格最高只有100℃超出的r_pt会被最后一个点钳制在100℃看起来很“稳定”实际已经失真。如果你想要更高的精度可以查标准分度表生成一个51个点的数组每个点5℃间隔再用线性插值。51个float是204字节对F103的Flash来说完全不是负担。曲线拟合用的Callendar-Van Dusen方程在三阶项上有开方和乘法MCU做这些运算虽然也算得快但查表插值的中间值更直观调试时还能手动算一遍验证。3.4 串口输出与上位机对接换算出的温度值通过USART1输出。工程里用printf重定向把标准库的输出函数映射到HAL_UART_Transmit这样主循环只要打印就行。usart.c里的实现如下#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 10); return ch; }主循环里配合HAL_Delay每500ms输出一次while (1) { uint16_t adc sensor_filtered_adc(); float r_pt resistance_from_adc(adc); float temp temp_from_table(r_pt); printf(ADC:%d R:%.2f T:%.2f\n, adc, r_pt, temp); HAL_Delay(500); }串口输出格式建议固定成“ADC:%d R:%.2f T:%.2f”上位机按行读取用逗号或空格分隔字段。如果后续要接显示屏或物联网模块可以把printf直接换成HAL_UART_Transmit发送一个二进制帧比如帧头0xAA、长度、温度整数和小数单独封装一次。这样上位机解析更快也不会被标准库占空间。串口这边有两个常见注意点一是STM32F103C8T6的USART1默认引脚PA9/PA10如果板子上引出的串口是PB6/PB7需要回到CubeMX重映射二是波特率必须和上位机一致工程默认115200、8N1。连接串口模块时收发交叉并且要和开发板共地否则会收到一堆乱码。4. 编译烧录与常见问题排查让PT100数据稳定输出的几个坑4.1 用STM32CubeIDE打开工程并烧录这个zip里带了.ioc和MDK-ARM工程文件用STM32CubeIDE也能直接导入。如果使用MDK双击MDK-ARM目录下的uvprojx文件即可如果使用纯CubeIDEFile - Open Projects from File System选择解压后的工程根目录CubeIDE会识别.ioc并生成新的Makefile工程。打开工程后的第一件事不是急着编译而是确认两个地方一是芯片型号打开CubeMX视图看Device是否是STM32F103C8T6如果不是切到Package Pinout页面重新选型二是烧录器配置在Debug Configuration里选择ST-LINK或J-LinkF103的SWD默认引脚PA13/PA14接线是SWDIO、SWCLK、GND、3.3V四根线即可。编译下载后复位板子用串口助手打开COM口如果能看到“ADC:xxx R:xx.xx T:xx.xx”说明工程已经跑通。如果没有任何输出先查板子的BOOT0跳线是不是接地再查串口线有没有接错引脚。我一般还会顺手打开MDK的Options for Target - Debug设置把下载速度降到1MHz很多下载失败其实是线太长的关系。4.2 上电验证怎么判断数据可信跑通只是第一步判断数据准不准更重要。我的验证顺序是先停在室温下用万用表电阻档直接量PT100两端记录下来再和串口打印的R值对比。比如室温25℃时万用表量到109.7Ω串口也报109.7Ω说明电压到电阻的链路没问题。如果R值对温度值不对问题在换算表或公式如果R值也不对问题在电路或分压公式。测量PT100阻值时最好把它从电路上断开一端否则你量到的是PT100和R_ref的并联或分压结构不是真实阻值。如果嫌麻烦可以在板子上焊一个排针验证时拔掉跳线帽方便得多。串口打印里的原始ADC值也有用。在分压电路下温度升高时PT100阻值增大、分压点电压升高ADC值应随温度上升而上升。如果接反了ADC值随温度下降说明分压采样点接错或公式里分母方向反了。你也可以用手捏住PT100看温度和ADC是否同时上升这是最快的手感测试。判断稳定性可以把串口数据连续采几十秒看温度波动范围。室内环境下波动超过±1℃时先检查电源纹波和滤波参数而不是怀疑PT100本身。PT100的响应时间通常几百毫秒不会秒级跳动看到跳变基本是电气问题。4.3 踩坑记录现象、原因、解决第一条ADC结果整体偏小温度读数偏低。现象是室温下计算出的温度只有十几摄氏度而万用表量PT100阻值却是正常的。原因是PA1的模式被配置成了GPIO_OUTPUT而不是模拟输入或者采样时间太短内部采样电容没充满。解决是在HAL_ADC_MspInit里检查PA1的GPIO_MODE把采样时间改到ADC_SAMPLETIME_239CYCLES。第二条温漂回零点变了读数整体偏移。现象是上电一段时间后温度读数缓慢向上走稳定后又回不来。原因是分压电流通过PT100产生自热R_ref选得太小或者电路板布局让PT100紧挨着发热元件。解决是提高R_ref值或者把PT100探头从板上引出去不要焊在PCB上。这也是我强烈建议用1kΩ以上参考电阻的原因。第三条电源电压波动引起读值抖动。现象是手靠近板子温度就跳关掉USB供电改成稳压源供电后仍然跳。原因是F103的ADC参考电压就是VDDVDD上的纹波会通过分压链路直接叠加到采样结果。解决是在VDD和GND之间加106电容或者用固定的外部参考给分压电路供电但F103没有独立参考引脚所以更彻底的方案是软件校准在每次采样前顺便读一个固定的基准通道把电源变化补偿掉。第四条换了一根探头后温度误差变大。现象是同一个工程、同一个连接点换上另一支PT100后偏差了2℃。原因是PT100本身有精度等级之分而且探头线缆的铜电阻也算进了PT100阻值。解决是在接线端子处做两点校准用标准温度计标定0℃和100℃把两个修正量存下来软件里做平移。不要指望一支未经标定的PT100直接输出高精度。5. 二次开发思路从单点测温到多点校准的量产化改造如果要把这个工程用于实际产品第一个要改的不是电路而是标定流程。我一般会在pt100.h里加一个校准结构体定义两个校准点的ADC值和温度值运行时线性插值替换掉原来的查表typedef struct { uint16_t adc_value; float temperature; } cal_point_t; static cal_point_t cal[2] { { 180, 0.0f }, { 460, 100.0f } }; float calibrated_temperature(uint16_t adc) { float ratio (float)(adc - cal[0].adc_value) / (cal[1].adc_value - cal[0].adc_value); return cal[0].temperature ratio * (cal[1].temperature - cal[0].temperature); }使用时把探头分别放到冰水混合物和沸水里按一下按键记录ADC值再按一下进入下一档最终把两组数据写入片内Flash。这样每台设备在出厂时只需要几分钟标定就能抵消探头和电路的全部固定误差。注意这个插值建立在ADC值和温度近似线性的基础上如果测量范围超过100℃最好用三个校准点做分段插值。第二个值得改的是采样链路。现在主循环里每次调用HAL_ADC_PollForConversion是阻塞式的如果还要处理按键和显示CPU会一直被占压。可以改成ADC1开启DMA循环模式让采样结果自动写入一个16位缓冲区DMA完成中断里做滑动平均主循环只负责读取计算后的结果。F103的ADCDMA配置不算复杂配好之后CPU占用率能降下一大截。第三个改动是断线检测。PT100断路时分压点会被电源拉高到近似VCCADC值接近4095短路时ADC值接近0。在温度换算之前加一个区间判断超范围就输出错误码上位机据此报警这是现场仪表必备的功能。做完这三步这个工程就不再只是实验板Demo而是可以往量产方向走了。每次我从这套代码里换板子或者换探头都会强制走一遍“室温比对-零点标定-沸点标定-连续跑30分钟看漂移”的流程。因为省掉哪一步后面都会在数据里翻车。PT100看着简单但真正让它稳定输出靠的还是这些不起眼的细节。希望帮到你。本文还有配套的精品资源点击获取