做 HVAC 和嵌入式设备联调的人大概都遇到过同一个问题:传感器读出来的数没问题,但一到远程看温度数据就不踏实。现场面板上的本地温度明明正常,后端监控屏上的远程温度却要么跳变要么离线,两边还总对不上。这个矛盾的根子其实不在某一个设备上,而在“本地采集”和“远程传输”被当成了一件事来设计。我最近在一个模拟 HVAC 温控项目中完整搭过一遍方案,用 PJ85718DM 数字温度传感器和 STM32F469II 微控制器,把本地和远程温度监测拆成两层处理:本地走 LCD 和控制逻辑,远程走 RS-485/Modbus 上报,中间用同一份温度原始值衔接。这套思路适合刚接触嵌入式温控、或者想把 HVAC 测温和后台监控打通的人参考。全文不依赖远程云平台,重点在协议、时序、滤波和现场排错,属于照着就能落地的工程笔记。
1. 需求定位:嵌入式和 HVAC 场景里“本地/远程温度”到底差在哪
先说为什么要分开设计。很多人日常会认为温度采集的难点在传感器精度,传感器一换问题就解决,现实里这个方向往往错了。嵌入式设备和 HVAC 系统遇到最多的问题,恰恰发生在数据从本地走向远端的这条链路上。本地读数是电信号和寄存器之间的关系,远程读数则是通信链路、协议、容错和时钟叠加在一起的问题,两者必须分开看待。
1.1 三个典型场景
第一个是我最常遇到的:控制器机柜内部温度。变频器、继电器、电源模块挤在一个密封铁箱里,夏天内部温度比环境高十几度都不稀奇。这类测量的要求很简单,量程在 0 到 70 摄氏度,更新周期 1 秒左右,重点是长期稳定和超温报警,偶尔温度跳一下不能触发误报。第二个是 HVAC 的风管和水管温度。风管回风温度是空调柜机 PID 调节的反馈量,水管供回水温差直接影响换热效率。这类测量要求量程覆盖负温,分辨率至少 0.1 摄氏度,采样周期 300 毫秒到 1 秒,响应速度要跟得上管路里水的温度变化。第三个是远程集中监控。设备分布在不同楼层或者不同机房,监控室里的值班人员不可能挨个到现场看面板,远程温度要能周期上报,并且在断线时快速判断是通信故障还是设备停机。
1.2 三个场景的测量指标对比
把三个场景的参数放在一张表里会看得更清楚:
| 测量场景 | 量程要求 | 分辨率 | 常用采样周期 | 核心关注点 |
|---|---|---|---|---|
| 控制器机柜内部 | 0℃ ~ 70℃ | 0.1℃ | 1s | 长期稳定、超温报警 |
| HVAC 风管/水管 | -20℃ ~ 80℃ | 0.1℃ 或更高 | 300ms ~ 1s | 响应速度、多点一致性 |
| 远程集中监控 | 与本地一致 | 0.1℃ | 1s ~ 30s | 丢包容忍、离线判断 |
注意远程监控的采样周期远比本地采集宽松。原因很简单:远程侧关心的是趋势和异常状态,不是每一个瞬时值;而本地控制逻辑恰恰需要对瞬时变化敏感。如果远程链路把 300ms 的数据全部原样上报,反而会让监控端被高频变化刷屏,所以我会在远程上报前做一个降频聚合,每 5 秒取一次平均值再发送。
1.3 为什么选 PJ85718DM 和 STM32F469II 这个组合
选择传感器和主控时,我主要看三点。
第一是传感器端。PJ85718DM 是带 I2C 接口的数字温度传感器,内部集成 ADC 和比较逻辑,量程能覆盖 -40 到 125 摄氏度,出厂精度大约在 ±1 摄氏度以内,分辨率默认 12 位,也就是 0.0625 摄氏度。这个精度对 HVAC 来说并不夸张,但足够把温差控制做到 0.5 摄氏度量级。数字接口最大的优势是不需要像 NTC 那样做分压校准,地址也可以通过引脚设置,一条总线上挂多个传感器时不会冲突。
第二是主控端。STM32F469II 属于 Cortex-M4 内核,主频 180MHz,带 FPU,做温度采集这种轻负载非常充裕;更重要的是它自带 LCD-TFT 控制器,可以直接驱动 RGB 接口液晶屏,把本地温度、报警状态、历史曲线都画出来,省掉一块串口屏的物料成本和通信开销。它还带有多个串口、以太网 MAC 和大量定时器,后续扩展远程 TCP 网关、扩展多路采集都不用换主控。
第三是整体开发效率。用一个带人机界面的高主频 MCU 去干一个本来可以用 8 位单片机完成的小任务,看起来冗余,但在 HVAC 场景里一点都不浪费,因为同一台设备往往要同时承担本地显示、逻辑控制和远程上报,正好落在 F469II 的发力范围里。选型不是越贵越好,而是让每部分资源都有明确用途。
2. PJ85718DM 的接入:引脚分配、通信协议与初始化
传感器接入是整个项目的地基。这部分做扎实了,后面的本地显示和远程上报才有意义。我会把从硬件连接到软件读取的完整过程展开,顺带说明几个容易踩的细节。
2.1 传感器引脚与 I2C 地址配置
PJ85718DM 常见封装下的引脚比较规整:VDD、GND、SDA、SCL,外加两个地址引脚 A0/A1 和一个报警输出 ALERT。VDD 直接接 F469II 的 3.3V,GND 共地;SDA、SCL 接到 MCU 的 I2C1 引脚 PB9/PB8,也可以根据实际板型调整到其他 I2C 引脚。上拉电阻我通常会放两个 4.7kΩ,分别把 SDA 和 SCL 拉高到 3.3V。总线速率用 100kHz 或者 400kHz 都行,HVAC 场景没有高吞吐需求,我一般固定到 100kHz,换来更好的抗干扰余量。
I2C 地址的确定方法需要单独说明:芯片的基础地址是 0x48,A0 和 A1 分别接地或接高后,地址会变化。比如两个地址脚都接低,最终地址是 0x48;A1 接高 A0 接低,地址就是 0x49。一条总线上要挂多个传感器,可以按 0x48 到 0x4F 的范围分配。注意一点:I2C 地址和寄存器地址是两个概念,PJ85718DM 的温度寄存器地址通常是 0x00,对芯片执行的是“写寄存器地址,再读温度数据”这样的两次操作,而不是直接把从机地址当成数据寄存器来读,这一点新手特别容易搞混。
2.2 用 HAL 库配置 F469II 的 I2C1
初始化代码直接基于 HAL 库写,工程复杂度不高:
I2C_HandleTypeDef hi2c1; static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1); }F469II 的 I2C 外设时钟频率和 APB 时钟配置直接影响时序。如果 APB1 跑的是 45MHz,上面的 100kHz 配置在 HAL 库内部换算后能得到稳定时序;如果改了系统时钟,要回头检查这里的时钟树,否则实际波特率会偏到 80kHz 或者 120kHz,短距离测试没问题,挂到几十米线缆上就容易出现偶发 NACK。这个坑我在现场踩过很多次,每次都是先查时钟配置,再查上拉电阻。
2.3 温度寄存器读取与原始数据解析
PJ85718DM 温度寄存器是 16 位格式,高字节在前。12 位有效数据左移 4 位存放在两个字节里,读取后需要右移 4 位再乘以 0.0625 才能得到摄氏温度:
#define PJ85718DM_ADDR 0x48 #define REG_TEMP 0x00 uint8_t ReadTemperature(float *temp) { uint8_t reg = REG_TEMP; uint8_t buf[2] = {0}; if (HAL_I2C_Mem_Read(&hi2c1, PJ85718DM_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, buf, 2, 100) != HAL_OK) { return 0; } int16_t raw = (int16_t)((buf[0] << 8) | buf[1]); raw >>= 4; /* 去掉低 4 位无效位 */ *temp = (float)raw * 0.0625f; return 1; }负数温度是按补码存储的,比如 -5 摄氏度时原始值是 0xFB00 这类带符号数据。第 8 行先拼成 int16_t 再做算术右移,能保证负数解析正确。读取失败时也不要立刻重试,先等 10ms 再看总线状态,反复连续发送恢复序列反而会把总线搞得更乱。另一个建议是上电后先读一遍配置寄存器并写回期望值,强制设置成连续转换模式,防止传感器在上电默认状态和我们的预期不一致。
3. 本地通道:LCD-TFT 显示和控制逻辑
本地通道的价值在于现场可见、可操作。F469II 自带 LCD-TFT 控制器,这个优势不用就浪费了。我把本地温度显示、刷新策略、联动控制和掉电日志放在同一个单元里设计。
3.1 本地温度在 LCD 上如何组织
F469II 的 LTDC 控制器管理图层,把需要显示的内容填充到显存,再做行列同步输出。我通常会分配一个 240x320 的窗口做温度主区域,另一个半透明图层做状态条。文字更新不需要整屏重绘,只更新局部图层,等垂直回扫信号到来后再切换显示,避免画面撕裂。
温度数值格式化这一层,我固定用带有两位小数的字符串:
char line[32]; snprintf(line, sizeof(line), "LOCAL:%5.2f C", temperature); LCD_SetLayer(1); LCD_DrawString(20, 40, line); LCD_SwitchLayer();值得多说一句的是字体问题。F469II 有 2MB 内置 Flash,我直接把 16x16 ASCII 字体表放在内部 Flash 里,不需要外挂存储器。如果后面要做多语言界面或者大字号数字,再把字体放到外部 QSPI Flash,用内存映射方式读取,两种方式在代码上可以共用一套绘制接口。
3.2 刷新策略和抗屏幕闪烁
直接每次采样后都重绘字符串,屏幕会闪,而且温度显示会被刷成一片模糊。我把采样和显示拆成了两个速率:底层每 300ms 采一次温度,显示层只在温度变化超过 0.1 摄氏度,或者距离上次刷新超过 1 秒时才重绘。这样既保证了变化及时可见,又不会因为传感器数值在小数点后几位轻微抖动而让 LCD 反复刷新。
报警状态使用单独的刷新标志。比如温度超过 60 度时,状态条立刻从绿色变成红色,即使数值还没变化,颜色翻转也要做到毫秒级响应。F469II 的 LTDC 支持图层混合,切换颜色时只改图层的背景色寄存器,比重绘字符串快得多,也稳得多。
3.3 以温度为中心的本地控制逻辑
本地显示之外,温度值还要驱动控制逻辑。HVAC 项目里最常见的是一组迟滞比较:
static float temp = 0.0f; static uint8_t state = STATE_IDLE; if (temp < 10.0f) state = STATE_HEATING; else if (temp > 28.0f) state = STATE_COOLING; else if (state != STATE_IDLE) state = STATE_IDLE;关键在迟滞区间。10 度和 28 度是进入加热和制冷的下限,但退出条件要比进入条件更严,否则设备会在临界点频繁启停。实际工程中我会把加热退出点设到 12 度,制冷退出点设到 26 度,中间留出 2 度左右的死区。这个死区不是控制不精确,而是保护继电器和压缩机,本质上是用小范围的温度滞后换设备寿命。
3.4 本地日志与掉电保存
本地日志的主要价值是现场维护时能看到设备停机前的温度曲线。F469II 有 2MB Flash,但温度记录不能每一条都直接写,因为 Flash 的擦写寿命是有限的。实现时我会用 4 个扇区做循环队列,每 10 分钟记一条,一个扇区写满再切下一个,四个扇区全部接近填满时擦掉最早的那个。写入操作放在主循环里一个低优先级任务中,避免在 I2C 读取或 LCD 刷新时发生阻塞。每次断电重启后,上电初始化要把当前扇区索引和偏移量读出来,保证日志不会覆盖掉未读旧数据。
4. 远程通道:RS-485 Modbus 帧与监控端解析
本地通道做得再好,远程通道不通,整个项目也只是完成了一半。远程温度要解决的是“数据走出去”的问题。我在这里选了 RS-485 物理层加 Modbus RTU 协议的组合,下面解释为什么,以及具体怎么搭。
4.1 为什么远程优先选 RS-485
远程方案有几个候选:以太网 TCP、Wi-Fi、LoRa、RS-485。在 HVAC 这种对可靠性和维护成本敏感的现场,RS-485 的优势主要体现在这几个方面:
| 传输方案 | 典型距离 | 节点数 | 线缆成本 | 抗干扰能力 | 适用场景 |
|---|---|---|---|---|---|
| RS-485 | 1200m | 32~128 | 低 | 较好 | HVAC 多点集中监控 |
| 以太网 | 100m | 不限 | 中 | 中 | 需要大带宽或 HTTP |
| Wi-Fi | 数十米 | 多 | 中 | 较弱 | 无布线的临时点位 |
| LoRa | 数公里 | 多 | 高 | 中 | 分散偏远点位 |
F469II 本身带有多个 UART,外接一个 RS-485 收发器,再用一个 GPIO 控制方向切换就能把上报链路搭起来。以太网也不是不行,但现场设备如果都在同一个弱电井里,RS-485 往往比以太网更少受交换机配置的影响,断了一根线也只影响一个点位。
4.2 Modbus RTU 帧与寄存器映射
远程温度的数据帧不推荐自己设计私有协议,除非有特殊要求。Modbus RTU 在暖通行业接受度高,成熟工具多,MCU 端只要用一个定时器判断 3.5 字符间隔就能完成帧解析。寄存器映射定义如下:
| 寄存器地址 | 内容 | 单位 | 属性 |
|---|---|---|---|
| 0x1000 | 本地温度 | 0.1℃,有符号 | 只读 |
| 0x1001 | 远程温度 | 0.1℃,有符号 | 只读 |
| 0x2000 | 传感器状态 | bit0=在线,bit1=故障 | 只读 |
查询帧的格式用例子说明:设备地址 0x01,功能码 0x03,起始寄存器 0x1000,读取数量 0x0002,CRC 校验字节按 Modbus 标准多项式 0xA001 计算。完整请求帧是:
01 03 10 00 00 02 BE D5响应的数据段是 4 个字节,两个寄存器各占 2 字节,第一个寄存器的值除以 10 就是实际温度。比如响应帧数据段是00 FA 00 E6,对应本地温度 25.0℃,远程温度 23.0℃。CRC 不是只算数据段,而是从地址开始覆盖整帧,两端算法必须一致。
4.3 监控端解析示例
监控端我用 Python 写了个最小解析程序,方便快速验证链路:
import serial ser = serial.Serial("COM3", 9600, timeout=1) req = bytes.fromhex("01 03 10 00 00 02 BE D5") ser.write(req) resp = ser.read(9) if len(resp) == 9: addr, func, length = resp[0], resp[1], resp[2] d0, d1, d2, d3 = resp[3], resp[4], resp[5], resp[6] raw_local = (d0 << 8) | d1 raw_remote = (d2 << 8) | d3 if raw_local >= 0x8000: raw_local -= 0x10000 if raw_remote >= 0x8000: raw_remote -= 0x10000 local_c = raw_local / 10.0 remote_c = raw_remote / 10.0 print(f"local={local_c:.1f}C remote={remote_c:.1f}C")实际项目里我会把 CRC 校验补上,并且用容忍超时的方式轮询。串口 read 返回空时,不要立刻判设备离线,连续 3 个轮询周期无响应再报警,这样能避开现场偶发的总线冲突。
4.4 远程温度链路的可靠性设计
远程温度最难处理的不是正常通信,而是异常时的状态判定。三个工程习惯值得坚持:通信失败时记录失败计数,而不是把温度清 0 或者保留旧值不变;主站轮询超时后要有退避,从站不能一直占用总线;每帧响应都带传感器状态字,让远程端能区分“设备在线但传感器故障”和“设备完全离线”。这些细节直接决定远程温度页面是摆着一堆看似实际却没法用于值班监控的数据,还是能真实反映现场状态的可信界面。
5. 校准、滤波与误差边界:把温度数据转成可用数据
温度采集链路通起来之后,下一件事是让数据更准、更稳。很多项目提问题都是从“读得到温度”到“温度能不能信”,中间隔着的就是校准和滤波这两步。
5.1 出厂精度与两点校准法
PJ85718DM 出厂有精度,但批量安装时不同批次的偏差可能达到 0.3~0.5 摄氏度。HVAC 控制要求高时,我会做一次两点校准。具体做法:把传感器探头放进 0℃ 冰水混合液和 60℃ 恒温水槽,稳定后分别记录原始读数,再用两个点求出斜率和偏移:
gain = (t_true_hi - t_true_lo) / (t_raw_hi - t_raw_lo) offset = t_true_lo - t_raw_lo * gain t_corrected = t_raw * gain + offset校准结果要存进 MCU 的非易失参数区,不要写死在代码里。批量烧录时固件是同一份,每台设备的校准参数不同,一旦写进固件,后面个别调整就要重刷整个程序,维护成本立刻变大。我一般把校准参数放在最后一个 Flash 扇区,上电时读取,读取失败才用默认增益 1 和偏移 0。
5.2 软件滤波:中值加低通的两级处理
现场最典型的温度干扰现象是:远程监控端显示温度每隔几十秒跳一下,特征不是整体偏高,而是个别采样点异常。处理上我会分两级。
第一级是滑窗中值滤波,取最近 5 次采样,排序后取中间值。中值滤波能很干净地干掉偶发尖峰,比如继电器动作瞬间产生的干扰。第二级是一阶低通滤波,把中值滤波后的结果再做平滑,公式是out = out_old * alpha + new_value * (1 - alpha)。alpha 取 0.7 时,响应比较快;alpha 取 0.9 时,曲线很平滑但滞后明显。HVAC 现场我用 0.8 比较折中。
我必须提醒一句:滤波不是越多越好。过度滤波会把真实温度变化也抹掉,导致 PID 控制反应迟钝。判断滤波参数是否合理的标准很简单:传感器从 25℃ 移到 40℃ 水槽中,显示值应该在 3 秒内跟上真实值,同时稳态波动不超过 0.1℃。
5.3 误差预算和一组实测数据
校准加滤波之后,我拿到过这样一组虚构测试点的数据:
| 测试点 | 参考温度(℃) | 原始读数(℃) | 校准后(℃) | 绝对偏差(℃) |
|---|---|---|---|---|
| 冰水混合液 | 0.0 | -0.3 | 0.0 | 0.0 |
| 室温环境 | 25.0 | 24.7 | 25.1 | 0.1 |
| 恒温水槽 | 60.0 | 59.5 | 60.0 | 0.0 |
这个结果说明原始读数的线性度本身是好的,主要是整体偏移。只要增益和偏移算准,整个量程内的误差可以压到 0.1 到 0.2 摄氏度,足够满足大多数 HVAC 场景。
5.4 量程边界和数据合法性检查
最后还要做量程边界判断。当环境温度低于 -40℃ 或高于 125℃ 时,芯片寄存器会进入饱和状态,解析出来的数可能偏离真实值。所以读取函数里除了返回温度,还要判断原始数据是否落在合理窗口内,比如raw > 0x0FC0表示超过量程上限,raw < 0xF000表示低于下限。这类检查要和传感器在线状态一起上报,远程端才能真正区分“温度 255℃”和“传感器故障”。
6. 实测调试中绕不开的几个坑与完整排查链路
这部分全是现场经验。设备在实验室跑得很好,一到 HVAC 现场就出问题的情况太常见了。我把最高频的几类问题整理成排查链路,按顺序走基本十分钟内能定位。
6.1 I2C 总线无应答的排查链路
传感器接好上电之后读不到数据,第一步不要改代码,先拿示波器或者逻辑分析仪看 SCL 和 SDA。确保上电后 SCL 有连续 9 个时钟脉冲,SDA 有对应的地址应答 ACK。看不到脉冲,检查 MCU 的引脚复用功能配置和引脚时钟;看到脉冲但 ACK 一直为 NACK,检查 I2C 地址对不对,地址引脚有没有虚焊,以及代码里是否把从机地址左移了一位。还有一类常见问题是总线上只用了一个传感器,但忘了画上拉电阻,100kHz 下可能偶尔还能工作,400kHz 下就会频繁 NACK。
6.2 本地显示正常、远程没有数据的排查方向
这类问题在 HVAC 现场最折磨人,因为本地显示正常说明采集链路是通的,问题全部卡在通信侧。我处理时会按固定顺序确认:
- RS-485 收发器的方向控制引脚是否在发送结束后正确拉回接收状态;
- A/B 线是否接反,两端有没有共地;
- 波特率两边是否一致,特别是改过系统时钟之后串口波特率会跟着漂移;
- Modbus 帧里的 CRC 算法是否一致,CRC 计算的起始字节有没有错位;
- 远程端如果显示 NaN,优先检查浮点解析,把寄存器值先打印成原始整数,一看就能判断分度值是否用错。
6.3 远程温度周期性跳变的成因与处理
如果一个点位每隔固定时间出现一次异常尖峰,通常不是随机干扰,而是周期事件和采样碰撞。温度采样正好发生在 PID 调节器动作、LCD 刷屏或者 RS-485 发送瞬间,电源被短暂拉低,地电位产生波动。排查时用逻辑分析仪同时抓 I2C SDA 和 GPIO 控制信号,可以找到时间上的重合点。解决思路是隔离采样窗口:把温度采样安排在定时器预留的毫秒级静默区内,避开 LCD 刷新和串口发包;同时给传感器供电增加 100nF 去耦电容,并在 PCB 走线时让传感器线远离继电器或风阀驱动线。
6.4 收尾的工程习惯与个人体会
最后写几条我们会写进代码规范的工程习惯,都不难,但能救回很多返工。开机后先读传感器配置寄存器并写回期望值,防止强干扰环境把配置位改写;读取失败时把上一次有效温度作为输出,而不是输出 0,系统里任何 0 都可能让 PID 立刻进入异常状态;每次温度上报都带设备状态字,远程端看到“在线但传感器故障”比看到“显示 255℃”有意义得多。
我调试这类项目最大的体会是,温度值本身并不复杂,复杂的是它在不同链路里被重复解释和传输。把本地显示、控制逻辑、远程上报三者的数据格式统一成同一种单位明确的有符号摄氏温度,后边能少吵很多架。如果后续想扩展成以太网远程网关,F469II 自带的 MAC 正好可以接物理层芯片,寄存器映射和协议层基本不用改动,这就是当初把数据格式统一带来的好处。