前阵子给一套 HVAC 控制柜做温度监测,需求一句话就能说完:本地要读主控板附近的回风温度,远程要汇总几台末端设备里的温度。可真正动手之后才发现,选传感器、配 MCU、写驱动、调通信,每一环都有小坑。我最后是用 PJ85718DM 做现场温度采集,TM4C123GH6PMI 做主控和远程总线汇聚,把本地与远程温度监测一起打通了。这篇就完整记录一下这套方案的选型逻辑、硬件接线、固件实现和实测坑点,给正在做嵌入式温控或者 HVAC 环境监测的朋友一个参考。
文章不会只给你贴代码。我更想把“为什么要这么选”“哪些地方容易想当然”讲清楚,尤其是 HVAC 这种有风机、继电器、动力线干扰的环境,和桌面上的开发板完全是两回事。你会看到我从需求拆解聊到硬件电路,再到 I2C 驱动、RS485 远程通信,最后是现场踩坑和校准方法,按真实项目的推进顺序来写。
1. 先想清楚:项目里到底要采什么温度
1.1 本地与远程是两种完全不同的需求
很多新手拿到“温度监测”四个字就急着选传感器,其实第一步应该是拆需求。本地温度在这里指主控板上通过短距离总线直连传感器,距离通常只有几厘米到几十厘米;远程温度指传感器在另外一台设备或者另一个控制柜里,距离可能是几米到几十米,中间还要过线槽、端子排、变频器、接触器这些干扰源。
这两种测温的电气实现思路完全不同。本地可以走 I2C、SPI 甚至模拟电压输出,MCU 直接读就行;远程如果把 I2C 硬拉长到几十米,总线电容会拖慢边沿、地电位差会造成误码、再有强电干扰直接就跑飞了。所以远程必须换成差分总线,比如 RS485 或者 CAN,把温度封装成数据帧再传回主控。
我做了个简单的对比,方便理解两者的分工:
| 维度 | 本地温度 | 远程温度 |
|---|---|---|
| 传输距离 | 厘米级 | 几十米到上千米 |
| 电气接口 | I2C/SPI/模拟量 | RS485 差分或 CAN |
| 数据形态 | MCU 直接读寄存器 | 按协议帧打包,带地址和校验 |
| 实时性 | 毫秒级可取 | 轮询周期决定,一般百毫秒到秒级 |
| 主要风险 | 自热、电源噪声 | 干扰、线缆压降、总线竞争 |
| 故障处理 | 读到坏值重试 | 超时、CRC 错误、从站掉线 |
在 HVAC 场景里,本地温度往往用来做控制闭环,比如回风温度参与风机转速或冷阀开度调节;远程温度更多用于集中显示、报警和联动。两者对数据的可信度要求都很高,但远程更强调“通信可靠”,本地更强调“采样准确”。这也是我后面分别处理的原因。
1.2 PJ85718DM 和 TM4C123GH6PMI 各自解决什么问题
先把两个主角的性格说清楚。PJ85718DM 是 I2C 接口的数字温度传感器,寄存器模型很典型:有温度寄存器、配置寄存器、温度阈值和滞回寄存器,支持可编程分辨率和告警输出引脚。这种传感器的好处是 MCU 不用自己做 ADC 换算和冷端补偿,直接按 I2C 时序把寄存器读出来,再除以对应的分辨率系数就是摄氏度。板上走线短、传感器数量不多时,I2C 是最省事的选择。
TM4C123GH6PMI 是 Cortex-M4F 内核的 MCU,80MHz 主频,256KB Flash、32KB SRAM,片上有多个 I2C、UART、定时器和足够用的 GPIO。做 HVAC 控制器时它早就是核心主控了,温度监测只是其中一个分支功能,而不是单独为测温去选的芯片。这一点很重要:很多帖子的误区是“测温就要选个低端单片机”,但工业控制里 MCU 选型往往由整体功能决定,测温模块只是“顺手挂在外设总线上的一个小任务”。
选 PJ85718DM 的理由也很直接。它单芯片就能完成本地数字测温,自带阈值比较功能,不用额外加比较器;I2C 地址可以通过引脚配置,一条总线上最多能挂多片,适合做多个测点。配合 TM4C123GH6PMI 这种本身就有硬件 I2C 模块和 DMA 能力的 MCU,固件里几乎不用做复杂的软件模拟时序,驱动代码非常薄。
2. 硬件连接与外围电路:容易出错的地方不在芯片本身
2.1 I2C 上拉电阻:不是随便焊一个就行
PJ85718DM 的 SCL、SDA 是开漏输出,所以总线上必须外部上拉。很多人在这里吃过亏:随手焊个 10kΩ,总线短时也能跑,一旦频率提到 400kHz 或者线上多挂了一片传感器,就开始偶发读错值。原因是上拉电阻太大时,总线电容充放电变慢,SCL 上升沿会超过协议要求的最大时间。
I2C 标准模式(100kHz)要求上升时间不超过 1μs,快速模式(400kHz)要求不超过 300ns。上拉电阻的最小值由灌电流决定,不能太小,否则低电平拉不下去;最大值由上升时间和总线电容决定。粗略估算可以用一个公式:上升沿时间约等于 0.8473 × R × Cb。这个 Cb 是总线总电容,包括传感器输入电容、MCU 引脚电容和 PCB 走线电容,通常取 100pF 到 400pF 之间。
| 总线场景 | 建议上拉电阻 | 说明 |
|---|---|---|
| 标准模式 100kHz,短线 <10cm | 4.7kΩ | 波形够用,功耗低 |
| 快速模式 400kHz,短线 | 2.2kΩ | 400kHz 下上升沿更稳 |
| 多片传感器或线长 >30cm | 1kΩ 左右 | 降低总线容性影响,优先保时序 |
我实际调试时试过 10kΩ 上拉,400kHz 下 SCL 上升沿明显变钝,温度寄存器偶发读到 0xFF 这种异常值;换成 2.2kΩ 之后示波器看波形干净很多,问题消失。所以这块别省事,按总线实际情况算一下,或者直接按表格选,比自己瞎试快。
2.2 电源、去耦与告警引脚的接法
PJ85718DM 的 VCC 可以直接用 TM4C123GH6PMI 的 3.3V,但不建议直接把传感器电源接到风机、继电器共用的那一轨上。HVAC 控制柜里的 PWM 风机和继电器通断会产生明显的电源毛刺,虽然数字温度传感器不像 ADC 那样直接受参考电压影响,但电源噪声会污染 I2C 电平判定,严重时读数会跳动 1 到 2 个 LSB。
我的做法是传感器电源单独经过一个小磁珠,再从磁珠后面分出 100nF 和 1μF 陶瓷电容,电容尽量靠近传感器 VCC 引脚。这样高频噪声被磁珠吸收,低频纹波被电容稳住,成本很低但效果很明显。如果板上还有电机驱动这类强干扰源,PCB 布线时还要注意传感器地线不要和功率地共用一段细走线,避免地弹电压抬升传感器的逻辑地。
告警引脚 ALERT 也要处理。它是开漏输出,必须接上拉电阻到 3.3V,如果接到了 MCU 的中断输入脚,还要确认 MCU 内部是否启用弱上拉。注意这个上拉和 I2C 总线上拉不是一回事,别共用。ALERT 可以配置成比较器模式,温度超过上限或者低于下限时主动拉低,主控不需要轮询,直接进中断响应,对 HVAC 这种需要报警联动的场景很有用。
如果一条总线上有多片 PJ85718DM,优先把它们配置成不同地址。地址引脚的电平组合决定从机地址,同一总线上有两片地址一样的传感器,通信时会发生 ACK 冲突,表现出来就是随机坏数据,很难排查。硬件上把地址引脚用电阻拉到高或低就可以了,但要注意这些引脚内部有上拉或下拉的话,分立电阻值要合适,别和内部拉电阻打架。
3. 固件驱动的核心:把寄存器读出来并翻译成摄氏度
3.1 初始化流程:先写配置寄存器再谈读数
PJ85718DM 这类芯片的寄存器模型很一致:先写指针寄存器指定要访问的目标寄存器,然后通过数据寄存器读或写。上电后默认分辨率往往是 9 位,对应 0.5°C 的 LSB,这对 HVAC 的精确控温来说不够。我在初始化里第一件事就是把配置寄存器改成 12 位分辨率,让 LSB 变成 0.0625°C。
以下是基于 TM4C123GH6PMI 的 I2C 外设操作示例,为了让代码不绑定具体的 SDK 版本,我写成通用时序风格,接口名换成你手里的库函数即可:
#define SENSOR_ADDR 0x4E // 由地址引脚决定,这里举例 #define REG_TEMPERATURE 0x00 #define REG_CONFIG 0x01 // 写配置寄存器:12-bit 分辨率,比较器模式 static void sensor_set_resolution_12bit(void) { i2c_start(SENSOR_ADDR << 1 | 0); // 写方向 i2c_write(REG_CONFIG); // 指向配置寄存器 i2c_write(0x60); // 配置值,具体位域以数据手册为准 i2c_stop(); }要注意的是,写完配置寄存器后不要立刻去读温度。传感器需要一定的转换时间,12 位分辨率下一般要等几十到一百毫秒量级,具体看数据手册。如果间隔太短,读到的还是上一个转换周期的旧值,控制逻辑里就会看到温度值“卡住不动”,然后突然跳变。
3.2 读数、负温度处理与简单滤波
读取温度的核心是连续读两个字节。先写指针到温度寄存器,然后重新发起读操作,先读高字节、再读低字节。12 位数据的高 12 位有效,低 4 位通常是 0,但这不代表可以直接当整数处理。我习惯把两个字节拼成一个有符号 16 位数,再除以 16 得到摄氏度,这样负温度不用自己做补码转换。
static int16_t sensor_read_temp_raw(void) { uint8_t buf[2]; i2c_start(SENSOR_ADDR << 1 | 0); i2c_write(REG_TEMPERATURE); i2c_stop(); i2c_start(SENSOR_ADDR << 1 | 1); // 读方向 buf[0] = i2c_read_ack(); buf[1] = i2c_read_nak(); i2c_stop(); return (int16_t)((buf[0] << 8) | buf[1]); } static float sensor_read_temp_c(void) { int16_t raw = sensor_read_temp_raw(); return raw / 16.0f; }举个例子:读到的原始值是 0x01A0,十进制是 416,除以 16 就是 26.0°C。如果读到的值是 0xFE70,按有符号数解释是 -400,除以 16 就是 -25°C。很多同学直接在无符号数上做除法,负温度就变成了两百多度,这个坑几乎每次都会有人踩。
软件滤波我建议用 5 到 8 次滑动平均。HVAC 里风机启动瞬间会造成测温点附近气流突变,温度短时波动很正常,但也不要为了平滑把窗口设得太大,否则风机启动后回风温度真实上升,显示值却要滞后好几秒才跟上,控制环路会变得迟钝。
4. 远程监测的实现:数据包设计比通信方式更关键
4.1 为什么把温度“拉远”要换 RS485
本地温度用 I2C 直连没问题,远程就不能这么干了。I2C 是一种为板内短距离设计的同步总线,一旦拉出 PCB,线上电容和地电位差就会让通信变得极其脆弱。我见过有人尝试用几十厘米的杜邦线把 I2C 传感器接到主控,结果高速时数据错乱、低速时勉强能用,这还没算工业现场的强电干扰。远程测温的正解是换差分总线。
RS485 是我在这个项目里的选择,原因很简单:它只需要一对双绞线,支持多点组网,传输距离在几百米到上千米量级,而且 HVAC 现场已经有大量基于 Modbus RTU 的总线设备,把温度传感器节点挂到同一条总线上,集成成本最低。温度变化本身是个慢变量,RS485 的通信速率根本不是什么瓶颈,反而可靠性才是重点。
整体架构可以这样理解:主控板上 TM4C123GH6PMI 通过 I2C 本地读 PJ85718DM;远端每个采集节点也用同款传感器,通过自身的 MCU 采集后挂到 RS485 总线上,主控按 Modbus RTU 帧定时轮询。固件里不需要为本地和远程维护两套完全不同的逻辑,只要抽象出一个“温度通道”列表:本地通道直接读寄存器,远程通道走协议解析,对外表现都是同一个温度值来源。
4.2 帧结构、CRC 校验和 RS485 收发切换
Modbus RTU 的帧结构很清晰:从站地址、功能码、寄存器地址、数据、CRC 校验。读温度这种只读量,用功能码 0x04 读输入寄存器最合适。比如主控发出去的请求帧是:
| 字节 | 内容 | 说明 |
|---|---|---|
| 0 | 从站地址 | 如 0x01 |
| 1 | 功能码 | 0x04 读输入寄存器 |
| 2-3 | 寄存器起始地址 | 0x0000 |
| 4-5 | 寄存器数量 | 0x0001 |
| 6-7 | CRC16 | 低字节在前,高字节在后 |
CRC 这里最容易翻车。Modbus 的 CRC16 算法是多项式 0xA001、初值 0xFFFF,且发送时 CRC 低字节在前。很多人把高字节先发出去,从站直接判定校验失败。我惯用的实现贴出来,你可以直接抄:
uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; }计算 CRC 时的数据范围是从站地址到数据字节结束,不包括 CRC 本身。校验时从站对接收到的整帧重新算一遍 CRC,如果结果不是 0,说明帧在传输中被干扰,直接丢弃。
RS485 是半双工总线,所以还有个 TX/RX 方向切换的问题。很多 MCU 的串口发送空标志只表示数据进了发送 FIFO,不代表最后一个字节已经离开了 TX 引脚。如果这时候立刻把方向切换回接收,最后一个字节的后半段会被自己拉断,对端接收到错误数据。我见过最典型的现象就是从站偶发不应答,重新上电又正常。
static void rs485_send_frame(const uint8_t *buf, uint16_t len) { RS485_DE = 1; // 拉高 DE,进入发送状态 uart_write_buf(buf, len); // 必须等最后一个字节真正从引脚发完再释放总线 while (uart_tx_busy()); // 等待发送完成标志 // 再补至少一个字节的时间,按波特率算 delay_us(1000); // 9600bps 下约 1ms,取有余量 RS485_DE = 0; // 释放总线,回到接收状态 }这个延时不是随便拍的。9600bps 下一个字节大约 1.04ms,如果你的库只有一个“FIFO 空”标志,那就至少要再等一个完整字节时间,才能确保移位寄存器也发完了。宁可多等一点,也不要省这几百微秒,RS485 半双工总线上两个节点同时收发,是会烧收发器芯片的。
5. HVAC 场景里实测遇到的坑:从“读到温度”到“正确读到温度”
5.1 自热效应:传感器会高估环境温度
数字温度传感器自身工作时会有功耗,这功耗会转化成热量,导致传感器读出的温度比实际环境温度高。这个偏差在静止空气里尤其明显,因为空气不流动,热量散不出去。很多实验室开发板上感觉不到这个问题,因为走线短、空气流通、旁边大块铜箔帮忙散热;但在塑料外壳的 HVAC 控制器里,传感器周围空气几乎不动,自热就会变成实打实的系统误差。
我在这项目里做过一个简单测试:把传感器所在区域用保温棉包住,记录通电后读数从冷态到稳定的过程,能明显看到温度曲线往上飘。软件上可以做固定偏移补偿,但这个补偿值受风速、安装方式影响,并不普适。更可靠的做法是结构上让传感器探头离开发热元件,用细引线把它引到回风通道里,让传感头直接接触被测空气。如果 PCB 上位置改不了,至少保证传感器底下不要有大面积接地铜箔直接连到功率器件的地。
顺便提一句,MCU 本身也是热源。TM4C123GH6PMI 跑起来之后,PCB 板上的局部温度会比环境空气高 1 到 2°C 很正常。本地温度传感器如果离 MCU 太近,读出来的实际上是“板温”而不是“回风温度”。做 HVAC 精确控制时,这个误差很要命,最好先把本地测温点的位置框定好再画板。
5.2 放置位置决定数据可信度
测回风温度时,探头一定要放在回风路径上,而不是贴在控制面板后面、更不要对着出风口。我调试时遇到过一种情况:传感器读数在风机启动后会下降 2°C,很明显是被出风口的冷空气吹到了。这种误差不是软件滤波能解决的,根源在安装位置。
风机的启停还会造成温度短时波动。我的处理方法是控制逻辑里加迟滞判断,比如设定 24°C 开风机、22.5°C 关机,而不是在 23.9°C 和 24.1°C 之间反复抖动。这属于 HVAC 控制策略的常规操作,但很多人只盯着传感器精度,忽略了判断阈值本身的稳定性需求。
另外,I2C 线缆和远程 485 线缆的走线也要注意。HVAC 控制柜里 220V 动力线是常见的干扰源,I2C 线如果非要和动力线并行一段距离,速度尽量降到 100kHz,线越短越好。远程 485 线必须用双绞屏蔽线,屏蔽层单端接地,不要两端都接,否则会形成地环路,反而引入更大的共模干扰。
5.3 多测点数据的异常处理
远程轮询一旦上了总线,就不能默认每个从站每次都正常应答。我处理远程通道时做了三件事:超时重试、坏值剔除、离线标记。主控发请求帧后等待应答,超过 50ms 没收到就重试,连续三次失败就把这个通道标记为离线,HMI 上显示“--”,而不是保留最后一次温度值继续参与控制。
CRC 错误也算坏值。有些干扰不会让从站完全不应答,而是让一帧数据里某几个字节被翻转,CRC 校验就是用来挡这些坏帧的。温度值本身也要做合法性检查,比如读到 -127°C 或者超过 150°C,一律判定无效。这类值在寄存器异常、芯片损坏、供电不稳时都可能出现,不能拿去做控制。
我最终给每个温度通道都维护了一个“最近 N 次有效值”的缓存,只有连续 M 次采样都正常,才把新值更新到显示和控制变量里。第一次写这个逻辑时觉得是多余,跑现场之后发现真香:通信抖动造成的毛刺被完全挡在控制闭环外面,设备误动作少了很多。
最后再分享一点实际操作体会
这套方案落地之后,现场跑了一个多月,温度读数基本稳定在 ±0.3°C 以内,远程轮询没有再出现掉线。回头复盘,真正花时间的地方不是把温度读出来,而是把上拉电阻、电源隔离、RS485 时序、探头位置这些“物理层”问题理顺。如果你也准备用 PJ85718DM 或者类似 I2C 传感器配合 TM4C123GH6PMI 做嵌入式或者 HVAC 温度监测,我建议先把硬件边界和安装位置定清楚,再动手写驱动,顺序反了会多做很多无用功。
温度监测本质上是个慢变量系统,对实时性要求没那么苛刻,但对长期稳定性的要求很高。芯片本身只是决定数据上限的一半,另一半取决于你有没有把自热、干扰、坏帧这些边界条件提前想好。