做HVAC控制箱时经常遇到一个很现实的问题:箱体内部的温度和你真正想控制的那个位置的温度,压根不是一回事。控制器装在配电柜里,柜内发热导致本地温度偏高,而风管远端、回风口、换热盘管那边的温度才决定系统怎么调节阀开度和风机转速。所以一套测温系统至少要覆盖两类测点——本地(控制器附近)和远程(管道/盘管/室外环境),这就正好落在标题这个方案里:用PJ85718DM这种模拟电压输出温度传感器做采集,用STM32L021K4这颗超低功耗MCU完成ADC采样、滤波、故障判断和上报。这篇文章把我整个调试过程和踩过的坑写清楚,硬件接线、软件换算、校准思路、低功耗策略都覆盖到,适合正在做嵌入式测温节点或者HVAC控制器的小伙伴参考。
1. HVAC里"本地温度"和"远程温度"到底分别指什么
1.1 两个测温点背后的工程含义
先说结论:本地和远程不是简单的"板子上一个传感器、外面拉一个传感器",它们解决的是两个完全不同的问题。
本地温度测的是设备自身热环境。控制柜里的变频器、继电器、开关电源都会发热,控制器电路板周围的环境温度可能比房间实际温度高出好几度。这个数据用来做两件事:一是过温保护,柜内超过比如说70°C就要报警甚至降载;二是温度补偿,很多高精度控制算法需要根据电子元器件实际工作温度修正参考电压、晶振漂移带来的误差。本地测温探头离MCU近,信号链短,但对长期稳定性和重复性要求高,因为它要作为整个系统的基准点之一。
远程温度测的是被控对象的真实状态。在HVAC系统里,这通常是送风管温度、回风管温度、冷冻水管进出口温度、冷凝器盘管温度或者室外环境温度。控制器要根据这些点决定压缩机启停、电子膨胀阀开度、风机转速。远程探头的特点是线缆长、环境复杂——可能从控制柜穿到吊顶里,沿着风管走十几米,旁边还并排着220V动力线。
这就是为什么我会选择PJ85718DM这类模拟线性温度传感器而不是DS18B20。不是说数字传感器不好,而是在某些场景下,模拟器件反而更有优势:
- 模拟电压输出直接进ADC,固件里一个线性公式就换算成温度,不需要复杂的一线协议时序
- 传感器响应速度快,电压输出没有协议转换延时,适合HVAC控制环路
- 单芯片、封装小、成本低,坏一个换一个,维护成本可控
- 没有数字通信被干扰后"卡死"的问题,ADC读到异常值可以通过软件判断
当然数字传感器在走线极长时更有优势,但那是另一个话题,本文不展开。
1.2 为什么选STM32L021K4而不是更"大"的MCU
STM32L021K4是我在这种节点上比较喜欢的一颗料,原因有三点:
第一,功耗等级完全不同。Cortex-M0+内核,32MHz主频,Stop模式典型电流只有不到1µA。HVAC中的分布式测温节点很多时候是靠24V直流母线或者电池供电的,长期在线运行的场景下,一个常亮LED就能吃掉整个MCU的睡眠功耗,更别说一颗功耗上百毫安的"大"芯片了。
第二,外设刚好够用。内置12位ADC、LPTIM低功耗定时器、USART、I2C、SPI,16KB Flash、2KB SRAM。对于"采两个温度、发一帧数据"这种任务,这些资源绰绰有余,不浪费也不紧张。
第三,封装小、引脚少,适合做在温控器或者传感节点的紧凑板卡上。UFQFPN32封装,4个ADC通道完全够用,剩下GPIO还能控制电源、接LED指示灯或者做拨码地址。
如果你只是做一个桌面Demo,用任何STM32F1系列都可以。但如果要往产品里走,尤其是要考虑功耗、成本、板面积的场景,L0系列比F1系列合适得多。记住一个判断标准:系统里每个毫安都很宝贵的时候,才值得选这种低功耗MCU;如果设备常年插电不care功耗,那就无所谓了。
2. 硬件接线:本地传感器直连,远程传感器怎么拉线才不被干扰
2.1 PJ85718DM的引脚与典型参数
先说传感器本身。PJ85718DM是一颗三引脚模拟电压输出温度传感器,典型封装类似SOT-23,引脚功能就是电源、地、输出三根线,没有通信地址、没有寄存器,上电即输出。
我手里这颗的典型参数如下(不同批次请以你自己拿到的数据手册为准):
| 参数 | 典型值 | 说明 |
|---|---|---|
| 供电电压 | 2.3V ~ 5.5V | 直接用3.3V即可 |
| 静态电流 | 约几十µA | 远低于DS18B20工作电流 |
| 输出电压斜率 | 10mV/°C | 温度每升高1°C,电压升高10mV |
| 0°C基准电压 | 500mV | 代入换算公式的截距 |
| 测温范围 | -40°C ~ +125°C | 覆盖HVAC全工况 |
| 精度 | 典型±1°C,最大±2°C | 无需外部校准即可接受 |
换算公式非常简单:
T(°C) = (VOUT - 0.5V) / 0.01V/°C
比如输出1.0V,温度就是50°C。这也是我选它的核心原因——换算不需要查表,不需要多项式拟合,一个减法一个除法就完事。
2.2 传感器到ADC的三线制接线与滤波
本地传感器没什么好说的,1cm走线,传感器三个引脚直接连到MCU的VDD、GND和ADC通道。重点在远程传感器。
远程测温最容易犯的错误是图省事只拉两根线——信号线和地线,电源从本地偷偷借。这种接法在小电流场景偶尔能跑,但温度信号是模拟电压,信号线回路里任何一点压降都会直接叠加到测量结果上。传感器输出阻抗不是零,线缆电阻再串联进去,ADC读到的电压一定是偏低的。
正确的做法是三线制:电源线、地线、信号线三根都从主机拉到传感器端。其中地线的地位比电源线还重要,因为传感器的VOUT是以它自己那一端的GND为参考的,而ADC的参考是主机端的GND。两端GND之间有电位差,这个误差会原封不动进入测量结果。
我的实际做法是:
- 电源线和地线用0.5mm²以上线径,降低远端供电压降
- 信号线用双绞线或者带屏蔽层线缆,和电源线分开走,不平行捆扎
- 在MCU的ADC引脚对地并联一个100nF陶瓷电容,和MCU内部采样电容配合,压制高频噪声
- ADC引脚串联一个100Ω电阻,和100nF组成RC低通,截止频率约16kHz,不会影响温度变化速率,但能滤掉大部分传导噪声
- 如果距离超过5米,建议传感器端也放一个100nF去耦电容,避免线缆上的电压波动反馈到传感器电源引脚
远程传感器供电还要考虑远端压降。PJ85718DM静态电流才几十µA,压降通常不是问题,但如果和别的负载共用馈线,就要用万用表实测一下远端VDD、GND之间的电压,确保传感器工作在2.3V以上。
2.3 VREF和地参考的关系
STM32的ADC参考电压VREF一般直接接VDD(3.3V),问题在于VDD不是一个绝对精准的基准。不同板子、不同供电条件下,实际VDD可能是3.28V,也可能是3.35V。如果你在程序里硬编码3.3V去计算温度,整体会有百分之零点几到百分之一的误差。
两种解决办法:
第一,用ADC内置的VREFINT(内部参考电压通道)实时反推VDD。先读内部基准对应的ADC值,再根据出厂校准数据算出真实VDD,然后把这个真实电压作为后续传感器换算的VREF。
第二,如果你对精度要求更高,外部加一个精密基准电压源,或者用STM32的Verf引脚接外部基准芯片。HVAC测温一般到±0.5°C已经足够,不太值得为这个加一颗基准芯片,我用第一种方法就够了。
另外注意,远程传感器如果供电也是同一路3.3V,而传感器端GND和主机端GND之间已经有压差,那么即便你用VREFINT校准了本地的VDD,传感器自己的参考零点还是偏的。所以前面说的三线制不是可选项,是好结果的前提。地线压降的数值测量方式很简单:上电后,万用表分别量主机端GND到传感器端GND之间的电压差,只要是mV级以下就不用太担心。
3. 从ADC原始码到温度值:滤波、换算和线性化
3.1 12位ADC采样配置与采样时间
STM32L021K4的内置ADC是12位SAR型,采样寄存器里有采样时间可配置。很多人直接HAL库默认配置一把梭,结果发现ADC读出来的值抖动得很厉害,其实很多时候不是传感器不行,是采样时间太短。
ADC采样的本质是内部采样电容充电,采样时间不够,采样电容还没充到和外部电压一致,转换结果自然偏低并且随机跳动。尤其远程传感器走线很长、线缆电容大,相当于ADC输入源阻抗更大了,充电时间更要留足。
我的配置是这样的:
- ADC时钟用HSI16分频,典型配置到4MHz以下
- 采样时间配置为最长档或者接近最长的档位
- 12位分辨率,右对齐
初始化代码大致如下(HAL库风格):
ADC_HandleTypeDef hadc; ADC_ChannelConfTypeDef sConfig = {0}; hadc.Instance = ADC1; hadc.Init.OversamplingMode = DISABLE; hadc.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1; hadc.Init.Resolution = ADC_RESOLUTION_12B; hadc.Init.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; HAL_ADC_Init(&hadc); sConfig.Channel = ADC_CHANNEL_0; // PA0 sConfig.Rank = ADC_REGULAR_RANK_1; HAL_ADC_ConfigChannel(&hadc, &sConfig);每次读取时用轮询方式:
HAL_ADC_Start(&hadc); HAL_ADC_PollForConversion(&hadc, 10); uint16_t adc_value = HAL_ADC_GetValue(&hadc); HAL_ADC_Stop(&hadc);多花一点CPU时间去换稳定的ADC读数,这笔交易非常划算。
3.2 电压到温度换算的公式和系数
拿到ADC原始码以后,先转电压,再转温度,每一步的思考要清晰。
第一步,原始码转电压:
V_measured = (ADC_CODE / 4095) × VREF
这里的VREF如果是硬编码的3.3V,就忽略了板间差异。我建议用上一节说的VREFINT来反推:
// 简要示意:先读VREFINT通道,再计算实际VREF uint16_t vrefint_raw = read_adc_channel(ADC_CHANNEL_VREFINT); float vref_actual = 3.3f * 4095.0f / vrefint_raw * (VREFINT_CAL / 4095.0f);VREFINT_CAL是芯片出厂时存储在Flash特定地址的内部基准校准值,不同芯片不一样,从数据手册指定的地址读取即可。
第二步,电压转温度:
T = (V_measured - 0.5V) / 0.01V/°C
完整代码:
float voltage = (float)adc_value * vref_actual / 4095.0f; float temperature = (voltage - 0.500f) / 0.010f;这一步完事后,你已经拿到一个"裸温度值"了。接下来要处理它抖不抖、准不准的问题。
3.3 滤波策略:HVAC环境下的中值+滑动平均
HVAC环境里有一个非常讨厌的干扰源:压缩机启停瞬间、继电器吸合释放瞬间,会产生很强的电磁脉冲,可能让ADC在某一次采集时读到完全离谱的值,比如正常25°C却突然冒出0°C。
如果直接用算术平均,一个异常尖峰会把平均值拉偏好几度,而且这个偏置要到后续很多次采样才会被消化掉。我的方案是分段处理:先做中值滤波剔除尖峰,再做滑动平均平滑输出。
#define SAMPLE_PER_READ 5 uint16_t samples[SAMPLE_PER_READ]; uint16_t median_filter(uint16_t *buf, uint8_t len) { // 简单选择排序,取中值 for (uint8_t i = 0; i < len - 1; i++) { for (uint8_t j = i + 1; j < len; j++) { if (buf[j] < buf[i]) { uint16_t tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } } } return buf[len >> 1]; } uint16_t read_temperature_raw(uint32_t channel) { for (uint8_t i = 0; i < SAMPLE_PER_READ; i++) { samples[i] = read_adc_channel(channel); delay_ms(2); } return median_filter(samples, SAMPLE_PER_READ); }每次读数前连续采5次,中间间隔2ms,取中值。这个做法可以保证即使5次里有1次被干扰打飞,中值依然是正常值。
中值滤波之后再叠加一个软件滑动平均:
float temp_filtered = temp_filtered * 0.7f + temp_new * 0.3f;这个一阶低通滤波器的响应时间大约是0.7/0.3≈2.3个采样周期,如果每2秒采一次,大约5秒之内能跟上温度变化。HVAC风管温度变化本身就慢,这个响应速度完全够,而且能明显降低显示值的跳动。
4. 校准、故障检测与实测对比
4.1 两点校准:为什么HVAC测温不需要精密校准却能保持±1°C
很多人一上来就琢磨高精度校准,其实HVAC的温度监测精度要求没那么极端。环境舒适性控制里,±0.5°C的误差已经可以让用户感觉"很准";工业空调回风控制一般±1°C都问题不大。PJ85718DM这类器件出厂已经做了片内修调,一致性比热敏电阻好得多,主要误差来源是器件本身的增益偏差和零点偏移。
我实际会做的最简单校准是两点偏移校准。找个稳定的温度环境,比如把传感器和标准温度计放一起,等稳定后记录差值delta1;再换一个温度点(比如手心里或者恒温箱),记录差值delta2。两个delta如果差别不大,说明主要是固定偏移,在软件里减掉这个offset就行:
float calibrated_temp = raw_temp - offset;如果两个delta差别较大,说明有增益误差,就需要修正系数:
float calibrated_temp = offset + (raw_temp - offset) * gain_coef;gain_coef = delta基准温差 / delta实测温差。
为了验证这套方案的可行性,我在一个通风测试台上做过一组本地/远程传感器同步读数,把数据列在这里供参考:
| 场景 | 本地传感器(°C) | 远程传感器(°C) | 标准温度计(°C) |
|---|---|---|---|
| 静态停机1小时 | 25.3 | 25.1 | 25.2 |
| 风机运行、风管送风 | 26.8 | 27.0 | 27.1 |
| 压缩机启动15秒后 | 27.4 | 27.9 | 27.6 |
| 压缩机稳定运行5分钟 | 28.2 | 28.7 | 28.5 |
远程传感器在压缩机运行时会比标准温度计偏高0.2°C左右,主要原因是远程探头放在送风口,直接被风吹着,表面换热更充分,响应比裸放在静态空气中的标准温度计稍微超前一点。这个偏差在控制逻辑上反而有利,因为PID反馈追求的就是尽快感知变化。
4.2 传感器开路/短路的软件判别
做过现场维护的人都知道,传感器故障是HVAC系统报修的大头。线缆被老鼠咬断、端子松脱、进水短路,任何情况都可能发生。所以固件里必须能够区分"这是个很低的温度"和"传感器坏了"。
PJ85718DM正常输出电压范围在0.1V~1.75V之间,对应-40°C到+125°C。那么:
- 如果ADC读到接近0V(比如低于0.05V),大概率是信号线对地短路
- 如果ADC读到接近VDD(比如高于VREF-0.1V),大概率是信号线对电源短路
- 如果读数稳定在某个极端值而且怎么吹风都不变,可能是开路
为了增强开路检测的可靠性,我习惯在ADC引脚对地放一颗100kΩ下拉电阻。这样在电源正常但信号线断掉的时候,ADC输入会被下拉到0V,和"信号线对地短路"表现一致,软件统一判断为故障。虽然这两种情况物理原因不同,但对控制系统来说结论是一样的:这个测点不可信,停止控制,报警。
软件里我会给每个传感器返回一个状态值,而不是直接返回温度:
#define SENSOR_OK 0x00 #define SENSOR_OPEN_FAULT 0x01 #define SENSOR_SHORT_FAULT 0x02 uint8_t temp_status_local; uint8_t temp_status_remote; float temp_local; float temp_remote;判断逻辑如下:
- 温度低于-50°C:置OPEN_FAULT(开路或者对地短路)
- 温度高于130°C:置SHORT_FAULT(对电源短路)
- 读数正常但连续多次读数波动小于0.01°C且绝对值偏离初始值超过20°C:也判故障,这种情况通常是传感器失效了
这样HVAC主控制器拿到状态码以后可以明确显示"传感器故障"而不是显示一个-45°C的离谱数字,现场排查效率会高很多。
5. 低功耗采集与HVAC主控通信
5.1 让STM32L021K4在两次采集之间深度睡眠
STM32L021K4最大的优势就是低功耗,但如果代码写得不讲究,功耗照样能跑到毫安级。核心思路是:绝大部分时间睡在Stop模式,用LPTIM定时唤醒,醒来后快速采集、快速上报、快速睡回去。
我的典型功耗预算:
| 状态 | 电流 | 持续时间 |
|---|---|---|
| Stop模式(LPTIM运行) | 约1.5µA(含LPTIM和电源纹波) | 2秒 |
| 唤醒后ADC采集 | 约1mA~2mA | 10ms以内 |
| UART发送一帧数据 | 约3mA | 20ms以内 |
平均功耗 = (1.5µA × 2s + 2mA × 0.01s + 3mA × 0.02s) / 2s,算下来平均不到50µA。这个数量级在电池供电或者母线取电的场景里几乎可以忽略。
关键配置点:
- 唤醒源用LPTIM,不用RTC。LPTIM可以在Stop模式下运行,功耗比RTC更低,配置也简单
- ADC、UART、GPIO全部在采集完以后用HAL库的反初始化或者DeInit关闭,或者直接把外设时钟关闭
- 所有未使用的GPIO引脚必须配置成模拟输入或者带上拉/下拉的输入模式,绝不能悬空,悬空引脚在CMOS工艺里是漏电重灾区
- 进入Stop模式前要PWR_EnterSTOPMode,唤醒后重新配置系统时钟,因为Stop模式下HSI16会停掉,代码要回到MAIN之后再Init
一个容易踩的坑是唤醒后立刻读ADC。Stop模式唤醒后,内部电源域还没完全稳定,寄存器虽然复位了但模拟部分需要一点建立时间。我的做法是唤醒后先加一个1ms左右的延时,再重新初始化ADC,然后再采样。虽然多花1ms,但对于2秒周期来说完全无所谓。
5.2 UART输出Modbus/ASCII给HVAC控制器
传感器节点采集完数据总要上报给HVAC主控。PJ85718DM本身没有通信能力,所以上报路径完全由STM32决定。
我常用两种方式:
第一种是简单的ASCII帧,适合调试和演示:
T_LOCAL=25.3,T_REMOTE=27.1,S=00主控端用字符串解析即可,但现场可靠性一般,没有校验。
第二种是自定义二进制帧,带CRC校验,适合正式系统:
typedef struct { uint8_t head[2]; // 0xAA 0x55 uint8_t len; // 数据长度 uint8_t addr; // 节点地址 int16_t temp_local; // 本地温度×10 int16_t temp_remote; // 远程温度×10 uint8_t status; // 传感器状态 uint8_t crc; // CRC8 } sensor_frame_t;温度乘以10存成整数,传输过程中不会因为浮点数编码问题出差错。CRC8可以自己实现多项式校验,几十行代码的事,但能防止通信链路噪声导致主控收到错误温度。
串口参数我固定用9600、8N1。HVAC现场总线环境往往嘈杂,没必要跑高波特率。9600波特率下发送一帧不到10字节的数据只需要约10ms,时间和功耗占比很小。
我这里没有去跑完整的Modbus协议栈,因为节点只上报数据、不需要被读写寄存器,自定义帧更轻量。如果将来需要接入标准监控系统,可以后期在固件里增加一个Modbus RTU从站映射层,把温度值映射到保持寄存器里即可。
6. 实测踩坑:远程传感器漂移和交流噪声这几个问题
6.1 现象一:远程温度读数比本地高0.5°C,且随负荷变化
现象描述:静态时两个传感器读数基本一致,一旦风机或水泵运行起来,远程温度读数明显比实际偏高,而且偏高量随着负载电流变化。
排查过程:我先怀疑是传感器位置受热辐射,但用标准温度计并排测量发现标准温度计读数正常,说明不是热源问题。然后量传感器端VDD和GND之间的电压,发现和主机端差了将近0.1V——地线太细了,负载电流在地线上产生了压降,传感器自己的"地"被抬高了,VOUT相应被抬高,ADC读数自然偏高。
解决方式:把地线换成更大线径的线缆,传感器端GND和主机端GND之间再用一根短粗线并联,相当于星型接地。改完之后压差降到10mV以内,读数恢复正常。
经验总结:远程传感器电源和信号线一定要分开考虑,地线压降是模拟信号链路里最容易忽略却影响最大的一个因素。
6.2 现象二:压缩机启停瞬间,ADC读数跳变
现象描述:压缩机一启动,两个传感器读数偶尔会跳变好几度,随后又自己恢复正常。跳变没有固定规律,但和继电器动作、压缩机启动电流有明显时间关联。
排查过程:用示波器挂在ADC引脚上,发现压缩机启动时信号线上有几百mV的尖峰噪声,频率很高,持续时间几十微秒。这些尖峰被ADC捕获到,就变成了毛刺。
解决方式:软件上靠中值滤波已经可以挡住绝大多数尖峰,但硬件上也做了加固。把ADC引脚到地的100nF电容换成了1nF+100nF并联,高频衰减更好;信号线改用屏蔽双绞线,屏蔽层只在主机端单点接地,避免屏蔽层形成地环路。
经验总结:软件滤波不是万能的,根治电磁干扰还得靠硬件。屏蔽层单点接地这个细节,处理不好反而会引入新的噪声。
6.3 现象三:一上电就读到-128°C
现象描述:首次给节点上电,串口输出来的远程温度竟然是-128°C,但多复位几次又正常了。本地传感器从来没出现过这个问题。
排查过程:看代码发现是上电初始化顺序的问题。传感器VDD是受MCU控制的GPIO引脚,初始化的时候先初始化了ADC并进行了第一次采样,之后才打开传感器电源。第一次采样时传感器根本没上电,VOUT悬空,ADC读到随意值,转换出来就是极端温度。
解决方式:把传感器电源控制放在最前面,上电后延时200ms等待传感器输出稳定,再初始化ADC并开始采样。另外,如前文所说,在ADC引脚加了100kΩ下拉,即使传感器没上电,ADC至少读到0V而不是悬空值。
经验总结:上电时序对模拟传感器的系统来说就是个常见坑。有些传感器带内部上电自检,在此期间输出无效,你不等它稳定就读取,拿到的就是垃圾数据。
最后再分享一个我自己保留的习惯:板子上一路留一个测试点,用两个精密电阻把3.3V分压出大约1.0V和1.6V分别接到两个ADC通道,开机自检时读一下,如果换算出来不是50°C和110°C附近,就说明ADC链路或者参考电压出了问题。这个自检测试点成本几乎为零,但现场排查"到底是传感器坏了还是板子坏了"的时候,能省掉大量时间。做嵌入式测温,稳定和可维护性往往比单纯堆精度更重要。