做嵌入式温度采集,我见过不少项目把“温度传感器”和“测温”混为一谈。对 HVAC 这类现场来说,真正的麻烦不是把某颗传感器读通,而是搞清楚你要保护哪几个位置:控制板自己的体温、风管出口或盘管表面的换热温度,可能相隔几十厘米甚至几米。这篇文章讲的就是我用 PJ85718DM 数字温度前端加 PIC18F87K22 单片机搭起来的一套双通道温度监测方案:PJ85718DM 内部完成本地温度采集,同时通过一对远端二极管接口测量远离主控板的换热面温度,最后经 I2C 总线把两路数据交给 PIC18F87K22 处理,单片机负责本地显示、告警逻辑和通过 RS-485 把数据上送到楼宇监控系统。内容会覆盖硬件接线、底层 I2C 读取、无浮点换算、Modbus 寄存器映射,以及几个现场调试时踩过的坑,适合正在做 HVAC 控制器、嵌入式环境监测设备的工程师参考。
1. 双路测温的现场痛点:本地温度和远程温度到底在保护什么
1.1 为什么控制器必须盯着自己的“体温”
控制器里最容易发热的是电源模块、继电器组、可控硅或者固态继电器,还有驱动 MOS 管这一类功率器件。柜体一旦封闭,热量散不出去,板级温度会慢慢升高。PJ85718DM 的本地通道靠芯片内部带隙基准测量,贴装在 PCB 上,实际读到的就是“主控板所在局部环境的温度”。这个值不只是拿来显示的,它更重要的作用是给程序做保护判定:超过阈值就降低输出功率、关断执行器,或者至少发出告警。如果只盯着远端温度而不管本地,等程序因为高温出现定时器漂移、继电器误动作时,问题往往已经持续了好几个小时,排查起来非常难受。
我一般把控制板的报警阈值设在 85°C,90°C 以上直接强制停机。这只是一个通用做法,具体阈值要按控制器里功率器件的结温限制和柜体散热条件重新算。多了这路本地测温之后,很多现场故障在发生之前就能被拦下来,这是“本地”这个通道最重要的价值。
1.2 远程温度点位的价值:风管、盘管、压缩机管壁
HVAC 系统本质是热量和冷量的交换,检测点必须放在物理换热真正发生的位置。远程通道就是为了解决“测量点离控制板远”这个问题。最常见的几个监测点:
- 送风管出口,用于判断电加热器或热水盘管是否真的把温度提上来了;
- 冷水回水管表面,用于做二通阀开度闭环或冬季防冻保护;
- 压缩机排气管壁,用于过温保护,避免冷媒异常导致排气温度过高。
这些点如果全部用 NTC 热敏电阻加模拟线拉过来,每一路都要运放调理、滤波和单独的校准,而且长线在变频器、电机启停时特别容易引入干扰。用 PJ85718DM 的远端二极管方案,远端感温点变成一对二极管引脚,芯片内部完成激励和测量,单片机拿到的直接是数字量,省掉一整套模拟调理链路。
| 检测目标 | 典型位置 | 使用通道 | 常见控制/保护动作 |
|---|---|---|---|
| 控制器板卡 | 主控 PCB 上 | 本地通道 | 温度过高降额、关断输出 |
| 送风管温度 | 风道内或管壁 | 远程通道 | 判断加热/制冷是否到位 |
| 冷水回水管 | 管壁表面 | 远程通道 | 阀门开度闭环、防冻保护 |
| 压缩机排气管 | 管壁表面 | 远程通道 | 排气过温报警、停机 |
这里要特别提醒:PJ85718DM 的“远端感温元件”不是 NTC,而是一个二极管连接的小信号三极管,比如 2N3904,把基极和集电极短接后当二极管用,或者直接用专门的温敏二极管。安装时用导热硅脂贴在被测表面,然后压紧固定。
1.3 为什么控制器用 PIC18F87K22 而不是更“高级”的器件
可能有朋友看到 8 位单片机就摇头,觉得应该上更复杂的处理器。我的选型逻辑很简单:这类 HVAC 控制器不需要跑复杂系统,状态机加定时器完全够用。PIC18F87K22 的优势在几个方面:
- 外设够用:I2C/SPI、多路 UART、多个定时器、独立看门狗都有,引脚数量能同时挂 LCD、按键、RS-485 和温度传感器;
- 工业温度范围宽,供电兼容 3.3V 和 5V 系统;
- 8 位机功耗低、供应链成熟、单颗成本可控,做产品时换料也方便;
- 代码可控性强,现场出问题拿到串口日志就能定位。
不是说更高级的处理器不行,而是没必要。做产品的第一原则是用最少的东西解决问题。PJ85718DM 负责把模拟世界数字化,PIC18F87K22 负责逻辑和信息流转,分工很清楚。
2. 硬件链路设计:从传感器引脚到 MCU 引脚怎么接才稳
2.1 PJ85718DM 的基本工作原理与引脚安排
PJ85718DM 这类数字温度前端,内部一般有两条测温路径。本地路径是封装里的带隙基准电路,直接得到芯片附近的温度;远程路径则是向外部二极管灌一个已知激励电流,再测量二极管正向压降的变化,换算成温度。二极管正向压降在常温附近大约随温度变化 -2mV/°C,经过内部模数转换后,最终以寄存器形式提供给 MCU 读取。
引脚安排通常包括 VDD、GND、SDA、SCL、D+、D-,以及可能的地址选择脚。我手里这颗器件的丝印就是 PJ85718DM,是项目选型时定下来的型号,如果你拿到同厂其他丝印的类似芯片,重点以手册为准,博客里我会按我们验证过的寄存器方式来展开,原理和流程是通用的。
| 引脚 | 功能 | 接法建议 |
|---|---|---|
| VDD | 电源 | 按手册选 3.3V 或 5V,就近加 100nF 去耦 |
| GND | 地 | 单点接地,避免与强电回路共用走线 |
| SDA/SCL | I2C 数据/时钟 | 各接 4.7k~10k 上拉到 VDD |
| D+/D- | 远端二极管正负 | 接外部二极管,优先使用双绞线 |
| A0/A1 | 地址选择 | 接地或接电源,决定 I2C 从地址 |
2.2 本地与远程感温的差异:封装内带隙与外部二极管
同一个芯片为什么要区分“本地”和“远程”?本地测温优点是电路简单,感温元件和 MCU 在同一块板上,能快速反映板卡自身热负荷;缺点是它永远只等于板温,不等于空气温度或者被控物体表面温度。远程测温把感温元件放到真正要控制的位置上,但它多了布线、焊装和标定这几道工序。
| 对比项 | 本地通道 | 远程通道 |
|---|---|---|
| 感温位置 | 芯片封装内部 | 外部二极管表面 |
| 典型响应速度 | 较快,约 1~3s | 较慢,受热阻影响,约 10~30s |
| 接线成本 | 无 | 一对线,最好双绞 |
| 受干扰影响 | 小 | 明显,需重点注意走线 |
| 标定难度 | 基本无 | 需要机械贴合和偏置补偿 |
这也解释了为什么标题里强调“本地与远程”:嵌入式端需要本地测温来保护板卡,HVAC 应用真正关心的是远端物理点的温度。两个通道放在一颗芯片里,是最经济的组合方式。
2.3 降噪与走线:电源、I2C 上拉与远端二极管走线长度
现场最容易出问题的往往不是原理图,而是 PCB 走线和柜内布线。按优先级说一下我的做法:
- 电源:PJ85718DM 的 VDD 引脚就近放 100nF 瓷片电容,电源入口再放 10μF 电解电容。如果系统里还有继电器、电机,VDD 线上串一颗磁珠效果更好。
- I2C:标准模式 100kHz 时上拉电阻用 4.7k~10k;如果跑到 400kHz,上拉改到 2.2k~4.7k。SCL 和 SDA 尽量贴着地走,不要绕大圈。
- 远端二极管:D+/D- 两条线尽量短,建议控制在 1m 以内,用双绞线或带屏蔽的线。屏蔽层只在一端接地,不要两端都接,避免形成地环流。
- 远离干扰源:D+/D- 不要和风机驱动线、继电器输出线并行走线,尤其不要在同一个线槽里长距离敷设。
- 一个很容易踩的坑:不要在 D+/D- 两端并一只大电容来滤波。电容会让二极管状态翻转变慢,芯片反而会读出带偏差的温度,看上去像“温度偏低”。
2.4 PIC18F87K22 的外设分配:一张清晰的资源表
PIC18F87K22 在项目里的外设占用,用一张表格说清楚:
| 外设 | 用途 | 补充说明 |
|---|---|---|
| MSSP/I2C 或软件 I2C | 读取 PJ85718DM | 建议先用软件 I2C 调试,稳定后再换外设 |
| GPIO | LCD1602 数据线、RS、EN | 4bit 模式,省 4 个 IO |
| EUSART0 | RS-485 数据上送 | 加一个 GPIO 做方向控制 |
| EUSART1 | 调试串口 | 打印读到的原始寄存器值 |
| Timer0 | 1s 轮询基准 | 中断里只放计数器,数据处理放主循环 |
| 独立看门狗 | 现场防死机 | 主循环里定期清零 |
外设分配的核心思想是让主循环保持“空转”,所有耗时操作比如 LCD 刷屏、Modbus 组帧都不塞进中断。
3. 固件实现:用 XC8 把两路温度读回来的完整过程
3.1 为什么先用软件 I2C 而不是直接上硬件外设
PIC18F87K22 本身有 MSSP 硬件 I2C,为什么我一开始却用手工的软件 I2C?因为 PJ85718DM 的规格细节和实际板子不一定完全一致,软件 I2C 方便用示波器逐比特观察时序,遇到 NACK 或总线卡死可以直接在调试器里断点处理;硬件外设一旦配置错,问题反而更难定位。等软件 I2C 在项目里跑通、时序确认无误之后,再考虑换 MSSP 来释放 CPU 占用。
先展示一套基础位流函数。下面的引脚定义按 RC3/RC4 举例,实际工程里换成自己板子对应的 TRIS、LAT 和 PORT 位即可。记得在编译器里定义_XTAL_FREQ,__delay_us才能正常工作。
#define TEMP_ADDR 0x48 #define SDA_TRIS TRISC4 #define SCL_TRIS TRISC3 #define SDA_LAT LATC4 #define SCL_LAT LATC3 #define SDA_PORT PORTC4 #define SCL_PORT PORTC3 void i2c_delay(void) { __delay_us(2); } void i2c_start(void) { SDA_TRIS = 0; SCL_TRIS = 0; SDA_LAT = 1; SCL_LAT = 1; i2c_delay(); SDA_LAT = 0; i2c_delay(); SCL_LAT = 0; i2c_delay(); } void i2c_stop(void) { SDA_TRIS = 0; SDA_LAT = 0; i2c_delay(); SCL_LAT = 1; i2c_delay(); SDA_LAT = 1; i2c_delay(); } unsigned char i2c_write_byte(unsigned char b) { unsigned char i, ack; SDA_TRIS = 0; SCL_TRIS = 0; for (i = 0; i < 8; i++) { SDA_LAT = (b & 0x80) ? 1 : 0; b <<= 1; SCL_LAT = 1; i2c_delay(); SCL_LAT = 0; i2c_delay(); } SDA_TRIS = 1; // 释放 SDA,准备读 ACK SCL_LAT = 1; i2c_delay(); ack = SDA_PORT; SCL_LAT = 0; i2c_delay(); return ack; // 0 表示 ACK,1 表示 NACK } unsigned char i2c_read_byte(unsigned char ack_flag) { unsigned char i, d = 0; SDA_TRIS = 1; SCL_TRIS = 0; for (i = 0; i < 8; i++) { SCL_LAT = 1; i2c_delay(); d = (d << 1) | (unsigned char)SDA_PORT; SCL_LAT = 0; i2c_delay(); } SDA_TRIS = 0; SDA_LAT = ack_flag ? 1 : 0; // 主机应答:0 发 ACK,1 发 NACK SCL_LAT = 1; i2c_delay(); SCL_LAT = 0; i2c_delay(); SDA_TRIS = 1; return d; }代码里有个容易被忽略的细节:读最后一个字节时,主机要回 NACK,总线上的传感器才会终止本帧传输。如果你的读取函数里最后一个字节也回 ACK,某些器件会一直不解散总线,后面所有读操作都可能卡住。
3.2 读取本地/远程温度的寄存器序列
PJ85718DM 的寄存器地址映射,我按项目里验证过的形式来定,常见布局如下:
| 寄存器地址 | 内容 |
|---|---|
| 0x00 | 本地温度高字节 |
| 0x01 | 本地温度低字节 |
| 0x02 | 远程温度高字节 |
| 0x03 | 远程温度低字节 |
| 0x08 | 配置/转换速率寄存器 |
读取流程是标准的“写寄存器指针 + 重复起始位 + 读数据”。先向器件写入目标寄存器地址,再用重复起始位切到读方向,读回两个字节。低字节的高 3 位通常放扩展精度,低字节的低 5 位是无效位。核心读取函数如下:
int read_temp_reg(unsigned char reg) { unsigned char h, l; i2c_start(); if (i2c_write_byte((TEMP_ADDR << 1) | 0)) return -1; // 写地址 if (i2c_write_byte(reg)) return -1; // 写寄存器指针 i2c_start(); // 重复起始位 if (i2c_write_byte((TEMP_ADDR << 1) | 1)) return -1; // 读地址 h = i2c_read_byte(0); // 主机回 ACK l = i2c_read_byte(1); // 主机回 NACK i2c_stop(); return (int)((h << 3) | (l >> 5)); // 组合成 11 位原始值 }如果i2c_write_byte返回 1,说明器件没在总线上应答,基本可以断定从地址或硬件连接有问题。这种时候不要急着改时序,先用万用表测 SDA、SCL 的静态电平和地址脚的上下拉。
3.3 数据定标与避免浮点运算
PJ85718DM 这类芯片一般输出 11 位有符号温度值,分辨率 0.125°C。把高字节左移 3 位、低字节右移 5 位合并后,如果第 10 位为 1,还要做符号扩展,否则负温度会错得离谱。接着做一步固定点换算:把温度乘 10,再除以 8,也就是乘以 5 再除以 4,得到单位 0.1°C 的整数值。
int raw = read_temp_reg(0x02); // 读取远程通道 if (raw < 0) return; // 判断总线错误 if (raw & 0x0400) raw |= 0xF800; // 11 位符号扩展成 16 位 int temp_x10 = (raw * 5) / 4; // 单位 0.1°C举几个例子:原始值 0x00E0 等于 224,224×5/4=280,即 28.0°C;原始值是负数时,例如 -16,-16×5/4=-20,即 -2.0°C。整型运算在 PIC18 上比浮点省太多资源,而且后面拼 Modbus 帧、发 LCD 显示都不用转字符。XC8 的浮点库在 8 位机上体积不小,能不用就不用。
3.4 定时轮询、均值滤波与主循环调度
HVAC 控制器的温度变化不会很快,但也不能太迟钝。我习惯用 Timer0 产生 100ms 中断,主循环里累计到 10 次也就是 1s 触发一次轮询。轮询里把两路温度都读一遍,做 4 次移动平均,然后刷新 LCD、组 Modbus 帧。伪代码如下:
volatile unsigned int tick100ms = 0; void __interrupt() isr(void) { if (TMR0IF) { TMR0IF = 0; tick100ms++; } } void main(void) { unsigned int last_tick = 0; init_mcu(); i2c_init(); lcd_init(); uart_init(); while (1) { if ((tick100ms - last_tick) >= 10) { last_tick = tick100ms; int lt = read_temp_reg(0x00); int rt = read_temp_reg(0x02); if (lt != -1) local_fifo_put(lt); if (rt != -1) remote_fifo_put(rt); int avg_lt = fifo_average(local_fifo); int avg_rt = fifo_average(remote_fifo); update_lcd(avg_lt, avg_rt); build_modbus_frame(avg_lt, avg_rt); } wdt_reset(); } }中断里只做计数器自增,绝对不碰 LCD、UART 或 I2C,这是嵌入式老规矩。跨中断访问的tick100ms必须声明成volatile。(tick100ms - last_tick)这种写法利用无符号整数回绕特性,即使溢出也不会出问题,比硬写tick100ms >= last_tick + 10要稳。
4. 把温度送到“远端”:本地显示与 RS-485 数据上送
4.1 本地显示:LCD1602 字符屏的驱动要点
本地显示我用最普通的 LCD1602,4 位数据模式,省 4 个 IO。驱动要点其实就三条:上电后按 8 位模式初始化时序先跑一遍,然后切到 4 位模式;写命令和数据都要先拉高 EN,再拉低 EN,形成下降沿锁存;读忙标志位比较费代码,我直接用一个固定延时替代,简单可控。
显示格式我做成类似L:25.3C R:21.8C这样,一行放本地和远程。格式化字符串在 PIC18 上尽量别用现成的sprintf,它会把很大的库函数拖进来。我自己写了一个小的整数转字符串函数,用 0.1°C 的整型值直接从高位开始取数字:
void lcd_put_temp(unsigned char line, int temp_x10) { char buf[16]; unsigned int i = 0; int t = temp_x10; if (t < 0) { buf[i++] = '-'; t = -t; } buf[i++] = '0' + (t / 100) % 10; // 十位 buf[i++] = '0' + (t / 10) % 10; // 个位 buf[i++] = '.'; buf[i++] = '0' + t % 10; // 十分位 buf[i++] = 0xDF; // 度符号 buf[i++] = 'C'; buf[i] = 0; lcd_write_string(line, buf); }这里有个视觉细节:温度跑到 100°C 以上时,(t / 100) % 10会正常显示百位;0.1°C 精度在小数点后一位,已经够 HVAC 现场看趋势了。
4.2 远程上送:Modbus RTU 与寄存器映射
楼宇监控系统最常用的是 Modbus RTU,走 RS-485 总线,设备地址、功能码、寄存器映射都要提前固定。我给出的映射示例:
| 寄存器地址 | 含义 | 数据格式 |
|---|---|---|
| 0x1000 | 本地温度 | 有符号整数,单位 0.1°C |
| 0x1001 | 远程温度 | 有符号整数,单位 0.1°C |
| 0x1002 | 状态字 | bit0 本地异常,bit1 远程异常 |
比如上位机发一条读取指令:设备地址 01,功能码 03,起始寄存器 0x1000,读 2 个寄存器。帧结构大概是:
01 03 10 00 00 02 C? C?最后两个字节是 CRC16-Modbus 校验。CRC 算法可以用查表法,也可以按位算。现场调试时,我一般先用串口助手连调试串口看原始帧,再接 RS-485 总线看转换后的波形。CRC 写不对是最常见的联调失败原因,建议在实机上用已知帧测一遍。
4.3 RS-485 方向控制与看门狗:现场系统怎么保持稳定
RS-485 收发器有方向脚,必须在发数据前拉高进入发送模式,等最后一个字节真正移出移位寄存器之后,再拉低回到接收模式。如果发的最后几个字节被方向切换硬生生切断,上位机收到的 CRC 永远不对。我的做法是发送前把宏定义的RS485_DIR拉高,发完后再额外延时一个字符的时长再拉低,比如 9600 波特率下 1 字节约 1.04ms。
看门狗的作用是防止 I2C 总线挂死或者主循环意外卡住。PIC18F87K22 的独立看门狗配置好之后,主循环只要定期wdt_reset()就行。同时给 I2C 读取函数加一层超时保护,比如连续 NACK 超过几次就强制输出无效温度,不至于让控制逻辑用脏数据做判断。
5. HVAC 现场调试实录:温差、长线与电磁干扰
5.1 远程温度整体偏低:问题出在远端二极管走线电阻
有一回在项目现场,远程通道读数总是比水银温度计低 3~4°C,不是偶发,而是持续偏低。排查下来,发现安装人员图省事用了普通 22AWG 单芯线拉了三米,又插了几处镀锡端子,线阻加接触电阻加起来不小。D+/D- 回路的额外串联电阻影响了芯片对二极管压降的采样,最终表现为系统性低温偏差。
处理办法是把远端二极管换成双绞屏蔽线,线长缩短到一米以内,端子的氧化层重新处理。同一个点上再贴一只参考温度计验证,偏差立刻降回 0.3°C 以内。这个案例给我的教训是:远端二极管的安装质量直接影响精度,省线就是省精度。如果现场确实无法缩短线长,就必须把偏置补偿加进软件。
5.2 风机继电器吸合瞬间读数跳变:EMI 干扰
另一个让我印象深刻的现场问题是:风机继电器一吸合,LCD 上的远程温度瞬间跳 2~3°C,过几十毫秒又恢复。这个现象几乎可以肯定与继电器触点电弧和电机启停的感性浪涌耦合进 D+/D- 有关。我当时做了三处改动:
- 把 D+/D- 双绞线移出与继电器出线共用的线槽,拉开物理距离;
- 在 PJ85718DM 的 VDD 脚加磁珠,把电源入口的高频噪声挡掉;
- I2C 上拉从 10k 换成 4.7k,增强总线对噪声的容限。
改动之后,再用继电器反复启停测试,温度值纹丝不动。这类问题很难从原理图上看出来,必须在整机布局阶段就预留整改空间,比如在 PCB 上留磁珠位、留滤波电容位。
5.3 温度校准:两点偏移标定策略
每一块的远端探头输出都会因为焊装应力、导热硅脂厚度、走线电阻不同而有微小偏差,量产时做两点标定最省事。方法不复杂:把远端二极管分别放进 0°C 冰水混合物和 50°C 恒温块,记录 PJ85718DM 的读数,算出修正量,写到 PIC18F87K22 的 EEPROM 里。软件里读出后叠加一个随温度作线性插值的修正值即可。
举一个简单例子:0°C 时读数为 -0.2°C,50°C 时读数为 49.7°C。那么修正函数可以近似成corrected = raw + offset(0°C) + (offset(50°C) - offset(0°C)) / 50 * raw。实际项目里一般存两个标定点对应的两个偏置系数,标定界面和组产流程要在硬件设计时就规划,不然量产品质检会很痛苦。
5.4 恒温箱台阶测试:一组典型结果
把整套控制器放进恒温箱,远端二极管贴在箱内一块铝板上,跑一组台阶测试,最典型的记录如下:
| 参考温度(°C) | 本地通道读数(°C) | 远程通道读数(°C) | 最终显示(°C) | 误差(°C) |
|---|---|---|---|---|
| 0.0 | 0.1 | -0.2 | 0.0 | ±0.2 |
| 25.0 | 24.9 | 25.2 | 25.0 | ±0.2 |
| 50.0 | 50.1 | 49.8 | 50.0 | ±0.3 |
| 85.0 | 84.8 | 85.2 | 85.0 | ±0.2 |
从这个结果能看到,本地通道因为紧贴 PCB,反应快;远程通道因为外面套了热阻和铝板导热,稳定后精度依然可以。误差控制在 ±0.3°C 对 HVAC 控制足够,不管是加热、通风还是制冷逻辑都不会被传感器抖动误导。
6. 这套方案的实际体会与可扩展方向
最后聊聊我的个人体会。做这类项目,最容易犯的错是上来就写一大堆底层驱动,忽略了测量点的物理贴合和布线抗干扰。PJ85718DM 这类数字前端的价值,是把模拟调理和标定的大部分痛苦挡在芯片后面,但要把系统真正做稳,功夫还是花在二极管怎么贴、线怎么走这些“脏活”上。
这几天我在项目里还有一个经验:给远端传感器焊一段细铜片,贴在风管外壁,再用保温棉压住,得到的温度读数比直接把探头伸进气流里稳定得多。虽然响应会慢一些,但可重复性非常好,尤其适合做防冻保护和盘管状态判断。
如果后续需要更多通道,同一根 I2C 总线上可以挂多颗 PJ85718DM,每颗用 A0/A1 设置不同地址即可;需要无线远程上报时,给 PIC18F87K22 加一颗串口无线模块,就能把现在的 Modbus 帧直接转发出去。这套方案的边界很清晰,扩展起来也不难。