做温度监测这个事,在嵌入式里算是最常见、也最容易被低估的活儿。风扇转速控制要看环境温度,电池充电要看电池温度,HVAC 的新风机组、空调箱、风管系统更是离不开温度探点。温度这个东西不起眼,但它直接决定设备稳不稳定、节能策略能不能生效。这次我完整复盘一个实际做过的测温节点:主控用 STM32L442KC,前端配模拟温度传感器 PJ85718DM,一路做板载本地温度采集,一路做远端探头温度采集,最终把两路数据通过 Modbus 上报到上位机,正好覆盖嵌入式设备测温、暖通空调风管/回风温度监测这些典型场景。
文章会从方案选型讲起,接着是硬件电路、固件实现、标定和联调实录,最后附上我实际踩过的坑。适合准备做温控节点、想给现有 HVAC 设备加数字化测温功能的朋友参考,也适合想认真搞懂 ADC 测温链路细节的嵌入式入门开发者。
1. 方案选型:PJ85718DM 和 STM32L442KC 的组合逻辑
1.1 为什么不用数字温度传感器
做温度采集,很多人第一反应是 I2C 或 1-Wire 的数字温度传感器,比如常见的 DS18B20。这类传感器确实方便,直接给数字量,一根线还能挂多个节点。但这套方案在 HVAC 和嵌入式现场有两个不太舒服的地方:
一是总线时序容易受干扰。HVAC 现场常有变频器、接触器、风机电机,这些设备一动作,电源和信号线上就容易翻起噪声。数字传感器对时序要求高,线一长、环境一乱,就容易出现读不到设备或者数据跳变的问题,排查起来比较费劲。
二是灵活性不够。数字传感器出厂特性固定,标定参数锁在内部,想针对具体探头做两点校准、想按自己的 ADC 参考电压做补偿,都不太方便。而模拟输出的温度传感器,比如 PJ85718DM,直接输出电压信号,进 ADC 之前你可以随意加滤波、做屏蔽、搞校准,体系完全自己说了算。
PJ85718DM 是我手上这颗料的基本情况:模拟电压输出,供电范围可以覆盖常见 3.3V 系统,输出量与温度基本呈线性关系,标称灵敏度 10mV/℃,0℃ 附近输出电压接近 0.5V,量程能覆盖 -40℃ 到 +125℃。这种参数在暖通环境里非常舒服,因为室温范围大概就在 -20℃ 到 +60℃,正好落在线性区间最好的位置。和数字传感器相比,它没有地址配不配的问题、没有时序波形的讲究,输出阻抗低,一根导线就能把信号拉到 MCU 的 ADC 引脚,工作起来省心很多。
1.2 STM32L442KC 在这套方案里的角色
STM32L442KC 是一颗 Cortex-M4F 内核的低功耗 MCU,主频可以跑到 80MHz,片上 Flash 128KB、SRAM 64KB。做温度采集这种任务,性能其实有点富余,真正让我选它的原因是几个不太起眼的外设:
首先是 12 位 ADC 支持硬件过采样。常规 12 位 ADC 直接测模拟传感器,效果其实一般,噪声一上来,低位就会乱跳。STM32L4 系列的 ADC 自带硬件过采样功能,可以把多次转换结果做累加和移位,等效分辨率能往上提。配合软件上的数字滤波,最终温度读数能做到非常稳定。
其次是它有真正的随机数发生器 TRNG。这个在温度采集里看起来用不上,但如果你有标定参数、固件升级校验、或者 Modbus 通信需要做随机扰动或加密因子,这颗芯片不用额外挂外设就能搞定,省了一个器件。
再就是低功耗模式。STM32L442KC 支持多种低功耗状态,LPTIM、LPUART 这些外设可以在停止模式下工作。对 HVAC 设备来说,很多工况是 24 小时连续供电的,功耗不是最要紧的;但如果是电池供电的无线温度标签、或者需要在断电后靠小电池维持监控的设备,这就有明显优势了。我这次虽然没做极限功耗优化,但采集完进 Stop 模式、传感器电源单独切断这套机制是留好的。
顺手提一句,为什么不是 ESP32、不是 STM32F103。F103 的 ADC 没有硬件过采样,输入噪声也大,做高精度模拟信号采集得靠外部加运放和 AD7766 之类的高速转换器,成本一下子就上去了。ESP32 的 ADC 线性度就更是出了名的差,做电池电量检测还能凑合,做温度这种需要对电压精度较真的应用,实在不合适。STM32L442KC 的 12 位 ADC 虽然不能说顶级,但配合过采样、内部基准校准、以及软件上的两点标定,已经足够把模拟传感器榨出不错的精度。
1.3 本地温度和远程温度,到底指什么
标题里说的“本地与远程温度”,我理解成两层意思,实际做的时候也按两层来落地。
第一层是物理位置上的本地和远程:“本地温度”指设备机柜、控制器盒子或者板卡周围的环境温度,能反映设备自身的热环境,比如控制柜是不是过热了、风扇是不是失效了;“远程温度”指通过屏蔽线拉到远端现场的探头温度,比如空调箱的送风管、回风管、新风口,这些地方距离控制板好几米甚至十几米,探头装在风管里,信号通过线缆引回 MCU。
第二层是数据流向意义上的“远程”:本地采集到的温度,通过串口 Modbus 总线或者后续扩展的 4-20mA、CAN 总线,上传给上位机、组态软件或者云网关。这样运维人员可以在值班室看到设备现场的温度数据,而不必跑到机房去摸设备外壳。
我这次做的板子,就把这两件事写在一块。板载一颗 PJ85718DM 测控制器箱内温度,另外一个通过连接器外接的探头测风管温度,两路分别接 STM32 的 ADC1 不同通道,微控制器把两路温度算出来之后,通过 Modbus RTU 协议挂在 RS485 总线上,供上位机读取。整套结构不复杂,但覆盖了“本地、远程、采集、上报”四个关键环节。
2. 硬件电路设计:板载与远端探头的关键细节
2.1 传感器供电和去耦,别在这上面省钱
PJ85718DM 这类模拟传感器,看起来供电很简单,实际上最容易出问题的地方就在供电。传感器内部虽然会把电源纹波抑制掉一部分,但如果电源噪声太大,输出信号上照样会叠加上去,最终变成 ADC 读数的随机跳动。
我的做法是传感器电源独立走一路,不直接从 MCU 的数字 3.3V 引脚上取,而是从模拟电源区域分出来。如果板子没有独立的模拟电源,至少要保证传感器供电引脚旁边放一颗 100nF 陶瓷电容加一颗 10µF 钽电容。100nF 负责滤高频,10µF 负责稳住低频瞬态。这里有个细节:10µF 电容不要离传感器太远,最好在 5mm 以内,否则引线电感会让电容在高频下失效。
传感器输出端到 MCU 的 ADC 引脚之间,我加了一级简单的 RC 低通滤波,电阻 1kΩ、电容 100nF,截止频率大约 1.6kHz。这个频率对温度信号来说绰绰有余,因为温度变化本来就很慢,即使是风管里突然通热风,温度的变化速率也就是每秒零点几摄氏度。RC 滤波的主要作用是削掉高频噪声和电源毛刺,让 ADC 采到的是相对干净的电压。测温时 ADC 采样时间也要和这个源阻抗匹配,后面固件部分会细说。
然后要单独说一下参考电压。STM32 的 ADC 转换结果是以参考电压为基准算出来的,通常 VREF+ 直接接 VDDA,也就是 3.3V。如果你的 3.3V 电源本身波动大,那测出来的电压就会跟着波动。所以我强烈建议测量一下 VDDA 的纹波,如果用示波器看到超过 50mV 的毛刺,就得考虑给 ADC 供电加一级 LDO 或者单独的滤波。我这次测试用的板子 VDDA 纹波在 20mV 以内,最终效果比较理想。
2.2 ADC 通道分配:本地探头和远端探头的接线
STM32L442KC 的 ADC1 有多个外部输入通道,我把本地传感器接在 PC0,对应 ADC1 的通道 10;远端探头接在 PC1,对应 ADC1 的通道 11。这里补充一句:具体引脚和通道的对应关系,最好以你用的 CubeMX 配置为准,不同封装和不同型号会有细微差异,不要死记引脚号,我写出这套只是为了说明分配逻辑。
每个通道的输入前端都放了一颗 100nF 的采样电容,位置尽量靠近 STM32 引脚。这个电容有两个作用:一是给 ADC 内部采样开关提供一个本地电荷源,降低采样瞬间的电压跌落;二是和滤波电阻一起构成低通网络。注意这个电容也不能太大,太大的话充放电时间常数变大,反而会让信号跟踪变慢,100nF 在这个场景下是合适的。
远端探头那边是通过一个 3Pin 连接器引出去的,分别是电源、地、信号输出。连接器旁边的信号线要加一颗 TVS 管到地,以防现场拔插的时候产生静电或者感应电压损坏 MCU 引脚。另外信号线在进入 MCU 之前,我串了一颗 100Ω 的限流电阻,万一 TVS 动作或者外部误接高压,这颗电阻能保护 MCU 的 ESD 二极管。
2.3 远端探头的长线接线:屏蔽、接地和地压降
HVAC 现场最典型的场景是:控制器安装在电控箱里,风管温度探头装在离箱子五六米甚至十几米的管道上。这个距离对模拟电压信号来说还能接受,但必须处理两件事:屏蔽和地回路。
我用的是三芯屏蔽电缆,一根电源、一根地、一根信号,屏蔽层只在控制器这一端单点接地。这是最关键的接线原则,屏蔽层千万不能两端都接。因为现场设备之间往往存在地电位差,屏蔽层两端接地就会形成地环路,干扰电流会在屏蔽层上流动,反而把噪声耦合成信号。单端接地时,屏蔽层就是一个静电屏蔽罩,把外部电场干扰挡在外面,同时又不会形成大环路。
还有一个容易忽略的问题:远端探头的地线是经过长线拉回来的,如果远端探头那边的地和板子之间的参考地不一致,信号电压就会叠加一个额外的压差。好在 PJ85718DM 这类传感器的静态电流很小,我实际测量过大概 50µA 左右。用 10 米 0.5mm² 的线算,线电阻大约 0.35Ω,压降只有几十微伏,等效成温度误差不到 0.01℃,完全可以忽略。但如果是那种工作电流 1mA 甚至 10mA 的传感器,长距离压降就会变成不可忽略的误差源,那就得考虑四线制接法或者干脆换成数字温度变送器。
如果距离超过 50 米,我不建议继续用模拟电压信号传输。一个理由是线缆分布电容会让信号响应变慢,另一个理由是现场的电机启动电流会在长线上感应出比较大的干扰,屏蔽层也很难完全压住。这种情况更适合换成 4-20mA 电流环变送器,或者用 Modbus、CAN 之类的数字总线。我在项目里给这个测温节点预留了 RS485 接口,正是为了以后扩展多点长距离测温做准备。
2.4 PCB 布局的一件事:传感器位置要有讲究
板载本地传感器看起来很随意,实际上摆放位置特别重要。如果板子上有开关电源、继电器或者大电流走线,传感器放太近就会被局部加热,读出来的温度比周围环境高出好几度。我第一版打样就是把这颗传感器放在了一个 DC-DC 电感附近,结果整晚测试下来,板载温度比实际环境温度偏高了差不多 3℃,后来把传感器挪到板子边缘、远离功率器件之后,偏差才降下来。
另外传感器本身也有自热效应。任何有源传感器工作时都会消耗一点功率,这会让芯片内部比环境温度略高。PJ85718DM 的自热很小,静态电流几十微安,自热温升通常在 0.1℃ 以内,基本可以忽略。但如果你给传感器加了太大的滤波电容,或者把信号输出等级压得很低,就得重新核算一下静态电流和自热。对这种模拟传感器,散热焊盘、通风孔这类东西不用刻意做,留出空间就够了。
3. 固件实现:从 ADC 原始值到稳定的温度值
3.1 CubeMX 配置的几个关键位置
固件部分我用 STM32CubeMX 生成基础工程,然后用 HAL 库做应用层。ADC 的初始化看似简单,但有几个设置直接影响最终的温度稳定性。
ADC1 配置成 12 位分辨率,单端输入,软件触发,关闭连续转换模式。采样时间选择 19.5 个 ADC 时钟周期。这里为何不用更短的采样时间,是因为前一节提到信号源前面加了 1kΩ 的电阻和 100nF 的电容,等效源阻抗偏高,ADC 内部的采样电容需要足够时间充电到稳定值。如果采样时间太短,充电不充分,转换结果就会出现系统性的偏差,表现出来就是读数偏高或偏低,而且还会随机跳。STM32L4 系列手册里给了源阻抗和采样周期的对应表格,1kΩ 源阻抗选 19.5 周期是比较稳妥的起点。
然后是硬件过采样。我在 ADC 初始化里开启了过采样,倍率设为 16,右移 4 位。意思就是每 16 次采样自动累加求和,然后把结果右移 4 位得到最终读数,等效分辨率从 12 位提升到约 14 位。这一步对模拟传感器的稳定性帮助非常明显,开与不开的感受是两个档次。
连接的通道配置也要注意。每个传感器通道配置成同一个 rank,采样时间保持和公共采样时间一致,其他参数用默认值。我代码里留了一个通用函数,采集时动态切换通道,不需要占用 DMA,这样简单归简单,但能满足两路温度每秒钟刷新 10 次的需求。
初始化部分的代码,摘录最核心的一段:
static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc1.Init.LowPowerAutoWait = DISABLE; hadc1.Init.LowPowerAutoPowerOff = ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.DMAContinuousRequests = DISABLE; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; hadc1.Init.SamplingTimeCommon = ADC_SAMPLINGTIME_19CYCLES_5; hadc1.Init.OversamplingMode = ENABLE; hadc1.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_16; hadc1.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_4; hadc1.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE; hadc1.Init.Oversampling.ContinuousMode = DISABLE; hadc1.Init.Oversampling.DiscontinuousMode = DISABLE; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } }这段生成的初始化看起来有点参数轰炸,但核心就是两个:采样时间要够,过采样要开。
采集函数我封装成一个通用接口,每次传入通道号,返回 16 位转换结果。有一个细节要注意:切换通道之后,第一次转换结果往往偏大或偏小,因为内部采样电容需要重新建立正确的电平。所以真正运行时,我每个通道都先做一次“空转换”丢掉,再取第二次转换结果作为有效值。这个技巧很小,但实测下来能少掉不少毛刺。
3.2 用内部参考电压 VREFINT 纠正实际的 VDD
很多人做 ADC 测温时直接用固定 3.3V 作为参考电压计算电压,这是误差的主要来源之一。实际 VDDA 不一定是精确的 3.3V,可能是 3.28V 或者 3.35V,如果你板子上的电源芯片精度一般,这个误差直接反映到温度上。
STM32L4 系列内部有一个固定的参考电压 VREFINT,出厂时在特定条件下校准过。读取 VREFINT 通道的 ADC 原始值,再和出厂校准值对比,就能反推当前 VDDA 的真实电压。关键代码大致是这样:
static uint32_t read_adc_channel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLINGTIME_640CYCLES_5; sConfig.SingleDiff = ADC_SINGLE_ENDED; sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint32_t value = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return value; } static float get_vdd_voltage(void) { uint16_t vrefint_raw = read_adc_channel(ADC_CHANNEL_VREFINT); // 出厂校准值地址以参考手册为准,不同子型号有差异,不要直接搬 uint16_t vrefint_cal = *(uint16_t *)(0x1FFF75AA); return 3.0f * (float)vrefint_cal / (float)vrefint_raw; }计算逻辑是:VREFINT 在校准条件下是 3.0V 时测出来的值等于 vrefint_cal,而现在测出来的值是 vrefint_raw,两者成反比,所以真实 VDDA 等于 3.0V 乘以 vrefint_cal 除以 vrefint_raw。这里需要注意,0x1FFF75AA 这个地址只是 STM32L4 某个具体子系列的出厂校准值位置,换型号要先查参考手册或者用 HAL 自动定位,否则读到的数不对,整个换算就是错的。
有了精确的 VDD,电压值就是:
float voltage = (float)adc_value * vdd_voltage / 4096.0f;3.3 温度换算和两点标定
假设传感器输出 Vout = 0.5V + 0.01V × T,那么温度 T = (Vout - 0.5V) / 0.01V。这是理想线性模型,实际 PJ85718DM 也基本符合这个规律,但每一颗料都存在零点偏移和灵敏度偏差。我打样的这块主控现在还留着一组初始参数:默认 V0 = 0.5、TC = 0.01,标定之后就可以更新成实际参数。
标定的方式我用的是“两点标定”。第一步把板子和标准温度计放在一起,在室温附近记录一组稳定数据,第二部再用恒温槽或者干井炉设定一个高温点,比如 80℃ 或者 100℃,等温度稳定后记录第二组数据。这里有一个坑:家用沸水温度一般是 100℃,但实际沸点会受大气压影响,并不精确,所以我强烈建议有条件用恒温槽,没有恒温槽也要用电子温度计作为基准参考,而不要默认“沸水就是 100℃”。
假设室温点测得原始代码对应的电压是 0.757V,实际标准温度计读数是 25.7℃;高温点测得电压 1.006V,标准温度读数是 50.5℃。那么灵敏度 TC = (1.006 - 0.757) / (50.5 - 25.7) = 0.01004V/℃,零点偏移 V0 = 0.757 - 0.01004 × 25.7 = 0.499V。标定完把 V0、TC 这两个浮点数存进 MCU 的 Flash 数据区,程序启动时读取,如果 Flash 里没有标定值就回落到出厂默认参数。
代码里温度换算就变成:
static float adc_to_temperature(uint16_t adc_value) { float vdd = get_vdd_voltage(); float voltage = (float)adc_value * vdd / 4096.0f; return (voltage - cal_v0) / cal_tc; }注意标定两个点的温差不能太小,至少拉开 20℃ 以上,否则会把采样噪声一起拟合成斜率,导致标定结果不靠谱。我见过有人只用 25℃ 和 26℃ 两个点来算斜率的,算出来的 TC 值能偏到天上,这种标定做了等于没做。
3.4 数字滤波:中值加移动平均
过采样解决了大部分电源噪声问题,但一些突发性的尖峰干扰,比如远处继电器吸合带来的毛刺,仍然可能穿过模拟滤波器到达 ADC。对这种尖峰,最简单的办法是连续采样 5 次,取这 5 次值的中位数作为本次输出。中值滤波对付尖峰非常干净利落,不会像平均法那样把毛刺拉成一个抬高的平台。
中值输出的结果已经很稳,但如果直接以这个值做控制,还是能感觉到读数在最后一位附近轻微抖动。我又叠了一层 32 点的移动平均,也就是保存最近 32 个中值结果,求和除以 32 输出。这样做之后,温度读数在恒温条件下基本就锁定在正负 0.03℃ 以内了,肉眼在串口助手里几乎看不出变化。
不过移动平均有一个固有缺点:响应变慢。温度从 20℃ 突然跳到 40℃,滤波后的值要滚动一段时间才能赶上。对 HVAC 这种温度变化缓慢的场合,32 点平均完全够用,因为每秒采集 10 次,32 点平均只用 3.2 秒就能反映大半阶跃,远快于风管温度本身的变化速度。但如果用在反应釜、发动机这种温度变化很快的场合,窗口就得缩小到 8 点或者 4 点。我在程序里留了一个编译期宏,改起来很方便。
有一类极端情况要提醒:如果探头断了,信号线上会悬空,ADC 读到的值可能飘到满量程或者某个固定大数。这时候如果滤波窗口里积累了这些坏值,平均结果会非常离谱。所以我在滤波之前加了范围判断,超出 -40℃ 到 +125℃ 对应电压区间的值,直接丢弃而不过滤,并且记录一个探头故障标志位。这个做法虽然简单,却能省下很多现场排查时间。
3.5 低功耗采集策略:不用的时候把传感器喂电断掉
虽然这次项目大多数场景是 24 小时供电,但我在设计时还是把低功耗机制做上了,因为这类测温节点以后很容易被改造成电池供电的无线采集器。
思路很简单:PJ85718DM 的供电引脚不直接接 VCC,而是通过一颗 MOS 管或者直接接在 MCU 的一个 GPIO 上,用 GPIO 输出高电平时传感器才上电。进入测温流程时先拉高这个引脚,等待 20ms,这个时间足够传感器输出稳定,然后连续采集几轮,算完温度,立刻拉低引脚断电。整个采样周期大约 50ms,其余时间 MCU 进 Stop 模式,用 LPTIM 定时唤醒。这样平均功耗可以压到非常低。
实测数据是:常温下每秒钟唤醒一次做一次采集,整板平均电流大约在 20µA 左右,对比传感器持续上电的方案降低了差不多一个数量级。真正的功劳主要来自传感器电源切断,因为传感器虽然静态电流不大,但一直开着也是一笔开销。
还有个细节:STM32L4 的 ADC 有 LowPowerAutoPowerOff 功能,采集完成后 ADC 本身会自动断电,这个在上面的初始化代码里已经开了。配合低功耗模式,整机功耗很干净。
3.6 内部温度传感器作参考校验
STM32L442KC 自带一个内部温度传感器,因为出厂做过两点校准,读数精度大概在 ±5℃ 的水平,单独用来做精确测温是不够的,但很适合做趋势校验。比如程序运行中,如果 PJ85718DM 读到的板载温度和内部温度传感器相差超过 10℃,那大概率是外部传感器坏了或者线松了,可以自动报一个诊断故障。
内部温度传感器的换算公式是:
float chip_temp = ((float)ts_raw - TS_CAL1) * (130.0f - 30.0f) / ((float)TS_CAL2 - TS_CAL1) + 30.0f;其中 TS_CAL1 对应 30℃ 的出厂校准值,TS_CAL2 对应 130℃ 的出厂校准值。这两个值在芯片出厂时烧进系统存储区,直接用指针读取即可。我在固件里让它每 10 秒计算一次,如果和板载 PJ85718DM 的差值超限,日志里打一条警告。这个功能的好处是让你在远端探头上多一双“眼睛”,及时发现故障。
4. 远程上报与联调实录
4.1 Modbus RTU 上报:把温度变成上位机能读的寄存器
两路温度算出来之后,最终要交给上位机。我这次选择的是 Modbus RTU,RS485 物理层,这是 HVAC 和楼宇自控里最通用的协议之一,组态软件、触摸屏、PLC 基本上都认识。
寄存器表我定义得很简单:保持寄存器地址 0x0000 存放本地温度,0x0001 存放远端温度。为了保留小数点精度,温度值乘以 100 后以 16 位有符号整数存储,比如 25.43℃ 就存成 2543。另外 0x0002 寄存器放当前 VDDA 电压乘以 1000 后的值,方便上位机查看电源状态。
Modbus 的从站地址设成 1,波特率 9600,8 数据位、无校验、1 停止位。这个配置在 RS485 总线上非常保守,抗干扰能力好,即使现场线缆走线不太讲究也不会频繁出错。如果想要更快,可以上 38400,但距离超过 100 米时我不推荐,宁可慢一点稳一点。
读保持寄存器的响应帧格式是:设备地址、功能码 03、字节数、数据高字节、数据低字节,最后是 CRC16 校验。CRC 计算代码并不复杂,但现场调试时如果发现 Modbus 通信偶发失败,八成是 CRC 算错了或者波形问题,值得优先排查。我贴一个常用的 CRC16 函数:
uint16_t crc16_modbus(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; }现场如果用的是支持 RTU 主站的触摸屏,给主站配上同样的波特率和地址,就能直接读到温度。我在调试时测试过一台上位机组态软件,连续读取 24 小时没有出现过通信中断。
4.2 实测数据与误差分析
固件写完、标定做完之后,我把板子放进恒温槽做了几组对比测试。标准温度计放在同一水槽里,外部探头用一根 10 米屏蔽线接出去,板子放在恒温槽外面,避免板子自身发热影响读数。
实测数据整理如下:
| 恒温槽设定 | 标准温度计 | 未校准读数 | 两点校准后 |
|---|---|---|---|
| 5.0℃ | 4.98℃ | 6.12℃ | 5.02℃ |
| 15.0℃ | 14.91℃ | 16.35℃ | 14.94℃ |
| 25.0℃ | 24.88℃ | 26.68℃ | 24.85℃ |
| 35.0℃ | 34.90℃ | 37.24℃ | 35.01℃ |
| 45.0℃ | 44.87℃ | 47.52℃ | 44.93℃ |
未校准状态下整体偏高约 1.2℃,主要来自传感器零点偏移和 VDDA 不是精确的 3.3V 两个叠加因素。校准后误差控制在 ±0.15℃ 以内,这个水平对 HVAC 应用来说已经非常充足。
长线测试那一组,我也单独记了一笔:探头直接插在板子旁边时,恒温槽 25℃ 读数 25.01℃;通过 10 米屏蔽线接到同一个探头后,读数 25.18℃。线缆引入的偏差大约 0.17℃,原因是线上绕进了少量工频干扰和接触电阻。如果后续现场对长线偏差敏感,可以考虑用双绞屏蔽线、或者把采样时间再拉长来抑制工频噪声。
4.3 常见问题速查表
整理一下实际调试过程中最常遇到的现象和解决办法,这些坑我基本都踩过一遍。
| 现象 | 可能原因 | 处理手段 |
|---|---|---|
| 读数恒为 0 | 传感器供电没接好、信号线断路、ADC 通道配置错误 | 先测传感器输出引脚电压是否正常,再核对 CubeMX 引脚映射 |
| 读数跳动超过 1℃ | 采样时间太短、VDDA 纹波大、滤波窗口太小 | 采样时间改到 19.5 周期以上,检查 VDDA 纹波,加大滤波窗口 |
| 远端读数比本地明显偏高 | 外部探头被风管热风直吹、地线压降、线缆屏蔽层两端接地 | 改单端接地,检查探头安装位置,改用双绞屏蔽线 |
| 标定后偏差仍大 | 标定参考温度计不准、两点温差太小 | 换标准温度计,两点温差至少拉开 20℃ 以上,在恒温槽中重新标定 |
| Modbus 偶发无响应 | 波特率不一致、设备地址冲突、RS485 A/B 接反 | 上位机逐个检查参数,确认总线终端电阻,检查 A/B 是否反接 |
| 设备进入低功耗后无法唤醒 | LPTIM 配置异常、唤醒源没有清除标志 | 检查唤醒回调函数,进低功耗前完成所有外设时钟和中断标志清理 |
有一个印象特别深的问题:第一批板子打样回来后,串口打印的温度数据偶尔会出现一个明显的尖峰——正常 25℃,每隔几秒会跳到 40℃ 一两个样本。排查了整整半天,最后发现是 MCU 的调试工具 SWD 接口和 ADC 引脚共用了电源平面,调试状态下偶尔从调试器灌进来一点噪声。把 SWD 连接线重新整理并远离传感器信号线之后,尖峰消失。这种问题很难从原理图上直接看出来,只能靠示波器和分步排除法。
另一个经验是标定参数一定要存在掉电不丢失的地方,不要每次都重新标。这次固件里我预留了一个 Flash 存储块,标定完成后把 V0、TC 两个参数写进去,设备重启时自动加载。这个操作成本很低,但对现场维护帮助极大,因为换板子或者换探头之后,只需要重新标一次,不用改代码。
最后再分享一个我自己留了很久的小技巧。ADC 采集和滤波全都跑通了之后,不妨把原始 ADC 码、中间滤波值、最终温度值、VDDA 电压四路数据都打到调试串口,上位机画一条趋势曲线。调试的时候你会发现,很多问题不用猜,看曲线就能定位到具体在哪一环丢数据。温度采集这种系统,链路不算长,但每一环都有可能引入误差,能看到中间量才是排查效率最高的方式。
这套板子后续我只是把 RS485 换成了带隔离的版本,挂到了楼宇自控总线上,两路温度数据连续跑了一个多月没出过问题。如果以后要多点测温,我打算给每个节点挂两三个模拟探头,用模拟多路开关切换到 ADC,再通过 Modbus 统一上报,架构上也完全兼容现在这套设计。