做 HVAC 温控板的朋友应该都有过这种经历传感器读数不是不能用而是你不太确定它反映的到底是哪里的温度。最近我把一套嵌入式温度监测方案从模拟 NTC 换成了 PJ85718DM 数字温度传感器主控用 PIC32MX675F256L同时维护本地温度和远程温度两路数据。项目跑了一阵子该踩的坑基本都踩了一遍现在把硬件连接、驱动代码、校准思路和排查步骤整理出来给正在做嵌入式温控或者 HVAC 控制器的人当个参考。这里说的本地温度指的是主控板工作环境附近的温度远程温度指的是传感器被放到风道、回风口或者室外机旁边测到的温度。两者物理位置不同、热惯性不同控制语义也完全不同。如果你也在纠结到底以哪路温度为准来控制风机这篇文章应该能给你一个比较务实的答案。1. 温度为什么非要分成本地和远程两路来测1.1 HVAC 项目里的真实温度场景大多数 HVAC 控制器并不是只装在一个恒温环境里。控制器装在电控箱内箱体里还有开关电源、继电器、驱动器这些东西一工作箱内温度比环境温度高个三五度很正常。而真正要控的是风道里或房间里的温度传感器往往要拉出去一两米甚至更远。如果把传感器放在控制板上测到的是电控箱热不热不是房间热不热如果只把传感器放远端主控板过热时没人管。所以实际项目里两路温度缺一不可远程温度参与正常温控本地温度做保护与故障兜底。1.2 数字温度传感器相比模拟 NTC 省在哪以前用 NTC 分压采样电路不复杂但麻烦在标定。NTC 的 B 值分散性大同一批料装上去读数能差一两度再加上分压电阻的精度、ADC 基准电压的温漂想把误差压到 ±0.5°C 以内每一路都要单独校准量产时相当痛苦。PJ85718DM 这类数字温度传感器把感温元件、ADC、I2C 接口、出厂校准全部封在一个器件里MCU 直接通过 I2C 读寄存器得到的就是经过芯片内部校准的温度数据。对嵌入式项目来说最大的好处是省掉了模拟链路里一堆不确定性两路之间的一致性也远比 NTC 好。1.3 PIC32MX675F256L 在这套方案里承担什么选 PIC32MX675F256L 倒不是因为它有多强的算力而是它的外设组合很合适硬件 I2C 模块可以直接挂 PJ85718DMADC 内部有温度指示通道可以做本地参考UART 可以把两路温度上报给上位机或楼宇自控网关。256KB Flash 对存储标定参数、历史温度记录、控制策略绰绰有余。另一个实际原因是可靠性。PIC32MX 系列跑 MIPS 内核工业级温度范围抗干扰能力比普通 8 位机强一大截。HVAC 电控箱里继电器通断、风机启停电磁环境并不友好主控的容错性值得多花一点成本。2. 硬件连接I2C 总线怎么把传感器送进风道2.1 引脚分配与传感器从机地址PIC32MX675F256L 的 64 脚封装上有多组 I2C 引脚我这边用 I2C1 挂 PJ85718DMULART 留作调试和上报。具体引脚编号看板子的原理图和器件手册SCL 和 SDA 一对线直接拉到传感器模块的对应引脚即可。PJ85718DM 这类器件一般带地址选择脚常见的是 A0/A1/A2 三根脚。全部接地时7 位从机地址通常落在 0x48 附近。这个地址决定了 I2C 总线上的写字节和读字节从机地址7位写操作字节读操作字节0x480x900x91这个细节最容易翻车。很多人把0x48直接填进写函数结果地址发出去是 0x48从机根本不认。I2C 地址字节最低位是读写方向位发送时必须左移一位读操作还要把最低位置 1。2.2 上拉电阻、线缆长度和供电去耦I2C 总线是开漏结构SCL 和 SDA 必须有上拉电阻。短距离测试板之间用 4.7kΩ 问题不大但传感器要拉进风道线缆长度超过半米后总线电容上升建议把上拉降到 2.2kΩ同时把通信速率从 400kHz 降到 100kHz 甚至 10kHz。我做第一版时偷懒直接用了现成开发板上的 4.7kΩ线长 1.2 米结果波形上升沿软得像一条抛物线偶尔整个总线卡死。后来换成 2.2kΩ 上拉、100kHz 速率、双绞线传输才算稳定。线缆如果超过两米老老实实考虑转 RS485 或者单独做传感器子板不要硬扛 I2C。供电方面传感器模块要就近放一颗 0.1μF 陶瓷电容如果传感器和主控之间电压有落差还要在传感器端加一颗低压差稳压器。风道里的风机一起动供电电压瞬间波动去耦电容能挡住大部分干扰。2.3 本地温度PIC32 内部温度指示器的接法本地温度我直接用 PIC32MX675F256L 内部温度指示器不需要额外接传感器。不过必须说清楚这个通道量的是芯片结温不是 PCB 表面温度CPU 负载一高、主频一拉读数会明显上涨。所以它更适合做主控板有没有过热这类保护判断不适合做高精度的环境温度采样。如果你的项目要求本地温度也精确到 ±0.5°C更稳的做法是在板上给 MCU 旁边贴一颗 PJ85718DM让本地和远程使用同一型号传感器这样两路的误差方向一致控制逻辑更好写。内部温度指示器就当第二道保护使用。3. 驱动与换算PIC32MX675F256L 读取 PJ85718DM 的完整代码3.1 I2C 外设初始化PIC32MX 的老用户一般已经习惯用寄存器操作代码也好维护。假设外设总线时钟 40MHz目标通信速率 100kHz初始化如下#define FPB 40000000UL #define I2C_CLOCK 100000UL #define PJ85718DM_ADDR 0x48 // I2C1 波特率寄存器计算BRG (FPB / (2 * I2C_CLK)) - 1 #define I2C1_BRG_VALUE ((FPB / (2UL * I2C_CLOCK)) - 1UL) void i2c1_init(void) { I2C1CON 0; // 先关闭 I2C1BRG I2C1_BRG_VALUE; // 写波特率 I2C1CONbits.ON 1; // 使能 I2C1 I2C1CONbits.SCLREL 1; // 释放 SCL }如果你用的是 Harmony 或者 PLIB本质也是设定这几个寄存器只是封装了一层。我建议至少能在寄存器层面看懂I2C1BRG和I2C1CON排查总线问题时能省很多时间。3.2 读温度寄存器的时序PJ85718DM 这类传感器的标准读取流程是先发设备地址加写位再发要读的寄存器地址然后发起重复起始条件改发设备地址加读位最后连续读两个字节。代码如下static void i2c1_write_byte(uint8_t data) { I2C1CONbits.SEN 1; // 启动条件 while (I2C1CONbits.SEN); I2C1TRN data; while (I2C1STATbits.TRSTAT); I2C1STATbits.ACKSTAT 1; // 清零 ACK 状态 while (I2C1STATbits.ACKSTAT); // 等待 ACK I2C1CONbits.PEN 1; // 停止条件 while (I2C1CONbits.PEN); } static uint8_t i2c1_read_byte(int ack) { I2C1CONbits.RCEN 1; while (!I2C1STATbits.RBF); uint8_t data I2C1RCV; if (ack) { I2C1CONbits.ACKDT 0; } else { I2C1CONbits.ACKDT 1; } I2C1CONbits.ACKEN 1; while (I2C1CONbits.ACKEN); return data; } uint16_t pj85718_read_temp_raw(void) { // 配置寄存器也要按数据手册初始化这里假设已经设置好 12 位分辨率 uint8_t reg 0x00; // 温度寄存器地址 uint8_t msb, lsb; I2C1CONbits.SEN 1; while (I2C1CONbits.SEN); I2C1TRN (PJ85718DM_ADDR 1) | 0; // 写地址 while (I2C1STATbits.TRSTAT); I2C1TRN reg; // 写寄存器指针 while (I2C1STATbits.TRSTAT); I2C1CONbits.RSEN 1; // 重复起始 while (I2C1CONbits.RSEN); I2C1TRN (PJ85718DM_ADDR 1) | 1; // 读地址 while (I2C1STATbits.TRSTAT); msb i2c1_read_byte(1); // 第一个字节回 ACK lsb i2c1_read_byte(0); // 最后一个字节回 NACK I2C1CONbits.PEN 1; // 停止条件 while (I2C1CONbits.PEN); return ((uint16_t)msb 8) | lsb; }3.3 原始码换算成摄氏度并做符号处理温度寄存器的数据格式各家略有差别但主流做法是 16 位寄存器里左对齐放 12 位温度数据低 4 位为 0。每个 LSB 对应 0.0625°C也就是 1/16°C。负数温度用二进制补码表示。float pj85718_raw_to_celsius(uint16_t raw) { int16_t val (int16_t)raw; // 强制有符号 val (val 4); // 去掉低 4 位 return val * 0.0625f; }重点在于先把原始值转成int16_t再做右移这样负温度才能正确扩展符号位。我见过有人直接用无符号整型右移温度降到零下之后读数直接变成一百多度排查了整整一个晚上。3.4 本地温度的 ADC 读取和校准PIC32MX675F256L 的 ADC 模块内部温度指示通道具体通道号要查你手头这颗芯片的数据手册不同型号不一样。读取流程就是把 ADC 切到这个通道采样、转换、取平均值#define ADC_TEMP_CHANNEL 28 // 替换成你芯片手册里的实际通道号 #define ADC_SAMPLE_COUNT 32 uint16_t internal_temp_sample_avg(uint16_t count) { uint32_t sum 0; for (uint16_t i 0; i count; i) { AD1CHS ADC_TEMP_CHANNEL; // 选择内部温度通道 AD1CON1bits.SAMP 1; // 开始采样 AD1CON1bits.SAMP 0; // 立即触发转换 while (!AD1CON1bits.DONE); // 等待转换结束 sum ADC1BUF0; } return (uint16_t)(sum / count); }出厂校准公式同样要以数据手册为准每颗芯片出厂时会在特定存储位置写入校准字拿那个值配合手册给出的公式换算。不建议用网上随便找的公式硬套不同批次、不同型号的系数可能根本对不上。4. 实测翻车记录三条最容易踩的坑与排查链路4.1 没有任何 ACK总线静默第一版样板贴完上电示波器看 SCL/SDA 都有波形但 SDA 上从头到尾没有 ACK。第一反应是传感器坏了换了一块还是一样。后来按链路排查发现问题出在从机地址。我想当然用了网上例程里的 7 位地址没和实际板子上的 A0/A1/A2 电平对应。IJ85718DM 的地址脚悬空和接地效果不同我当时把 A0 悬空了实际地址从 0x48 变成了别的值自然不回 ACK。这个坑的正确排查顺序是确认 VDD 和 GND 电压正常传感器供电在手册范围内。用示波器量 SCL 和 SDA 的静态电平确认上拉电阻到位。对照数据手册的地址表结合实际板子的地址脚接法算出真实 7 位地址。先用示波器抓一次完整波形确认主机发出的地址字节低位的读写方向位正确。I2C 总线上没有 ACK大多数情况不是器件坏了而是地址写错或者上拉缺失。别一上来就怀疑硬件损坏。4.2 读数跳变正负好几度传感器读数稳定了几天某次调试时发现远程温度偶尔瞬间跳动 3 到 5 度几秒后又自己恢复。用示波器看 SCL 波形发现毛刺严重尤其是风机继电器动作的瞬间总线被干扰得很厉害。远程线缆从主控板拉进风道经过继电器和风机线束附近干扰不可避免。处理方案分成几层线缆换成双绞线SDA 和 GND 绞在一起SCL 单独走屏蔽I2C 速率从 100kHz 降到 10kHz给总线更大的噪声容限软件做中值滤波连续读 7 次取中位数再滑动平均在传感器电源入口加 RC 滤波比如 10Ω 电阻加 1μF 电容。实际测下来最起作用的是降速和双绞线软件滤波只是兜底。HVAC 环境里别指望 400kHz 速率稳定比速度重要得多。4.3 远程温度系统性偏高或偏低还有一次是读数很稳定但比参考温度计低了两度多。一开始以为是传感器精度问题后来发现是传感器放在了风道出风口旁边风机工作时的风速带走了传感器自身的热量导致读数偏低。这是本地自热和气流冷却共同作用的结果。数字温度传感器自身有功耗静止空气中本来就会比真实环境温度略高一旦放在气流里冷却效果又会让读数偏低。方向相反必须先弄清楚传感器的实际安装位置和散热条件。系统性的偏差用软件修正。我的做法是把传感器和参考温度计放到同一个封闭箱体里等温度稳定 10 到 15 分钟记录两个读数的差值然后把差值作为修正偏移量写进 Flash每次读取后加上这个偏移。这样只做一点线性偏移修正对 HVAC 场景已经足够。5. 温度数据如何真正参与 HVAC 控制5.1 双路温控策略别让两路打架有了两路温度之后最忌讳的是控制逻辑里两路都拿来做主控一会儿按这个一会儿按那个。我的策略很直接远程温度是控制变量本地温度只做保护。正常工作时风机和阀门的启停完全以远程温度为基准。本地温度超过某个阈值比如 70°C说明电控箱散热出问题不论远程温度多少强制让风机全速运行同时上报故障。5.2 用迟滞避免继电器频繁吸合空调也好、风机盘管也好继电器频繁吸合是大忌。我在代码里用迟滞控制目标温度 25°C回差 1°Cfloat remote_temp; float local_temp; uint8_t fan_state 0; #define TARGET_SETPOINT 25.0f #define HYSTERESIS 1.0f #define LOCAL_OVERTEMP 70.0f for (;;) { remote_temp pj85718_raw_to_celsius(pj85718_read_temp_raw()); local_temp sample_local_temp(); if (remote_temp TARGET_SETPOINT HYSTERESIS) { fan_state 1; // 强制开启 } else if (remote_temp TARGET_SETPOINT - HYSTERESIS) { fan_state 0; // 保持关闭 } if (local_temp LOCAL_OVERTEMP) { fan_state 1; // 本地过热兜底 report_fault(FAULT_OVERTEMP); } }迟滞的关键在于开启和关闭的触发点不一样避免温度在设定点附近抖动导致继电器在一个周期内反复动作。远程温度的热惯性本来就大加上迟滞后系统会稳定很多。5.3 简单上报帧设计HVAC 项目一般还要把温度数据上报给上位机或者楼宇自控网关。串口上报帧不用做得太复杂关键是定长、可校验、能区分通道typedef struct { uint8_t header; // 0xAA uint8_t dev_id; // 设备 ID int16_t remote_temp; // 0.01°C 精度 int16_t local_temp; // 0.01°C 精度 uint8_t status; // 位标记传感器故障、本地过热等 uint8_t crc; // 简单 CRC8 } temp_report_frame_t;上报周期我习惯设 1 秒一次。换算成 0.01°C 精度后20000 就代表 200°C能覆盖大部分工业温度范围负数用补码表示。5.4 传感器故障时怎么兜底数字传感器也会失效最常见的是 I2C 无应答或读数锁死在某个极端值。我在代码里维护一个连续无效计数读到无效值或超出合理范围比如 -40°C 以下或 125°C 以上就加一连续三次无效直接判传感器故障切换到本地温度作为控制参考并向上位机报告故障位。HVAC 控制有个基本原则传感器失效时设备宁可继续运行也不要停机不动作。风机长期不转比风机一直转后果更严重所以故障时我的默认动作是按保护模式全速运行而不是关掉一切。6. 收尾经验校准、功耗与多点位扩展6.1 一阶两点校准比单点偏移更稳妥前面说的单点偏移修正适合要求不高的风机盘管控制。如果设备精度要求更高我会做两点校准分别在 10°C 和 40°C 附近用恒温箱稳定半小时得到增益和偏移两个系数然后套用线性修正。校准的要点是传感器必须和参考温度计放在同一热场里开箱换温时等足够长时间。我这边吃过教训恒温箱温度刚到设定值就急着读数传感器和温度计响应速度不一样校准完反而把误差放大了。6.2 降低功耗和更新频率嵌入式设备如果靠电池供电PJ85718DM 这类传感器一般支持单次转换模式。平时处于关机状态需要读温度时发一条命令触发单次转换等待几十毫秒再读结果平均电流能压到很低。PIC32MX 本身也有 idle 模式两路温度每秒读一次足够。实测下来设备大部分时间在 idle功耗比一直跑主循环低一个数量级。6.3 多传感器扩展地址脚是最大的网格如果项目要扩展到多个点位PJ85718DM 的地址脚就能派上用场。A0/A1/A2 组合出最多 8 个不同地址一根 I2C 总线上最多挂 8 路。楼宇自控里常见的做法是主控板旁边挂 1 路本地温度风道里挂 2 到 3 路远程温度分别覆盖回风、送风、室外温度控制逻辑会丰富很多。多路读取时要注意缩短每路的转换周期避免同一总线上设备太多导致时序紧张。我记得第一版挂 4 个传感器时总线负载明显上升后来把速率降到 50kHz 才稳定。这套方案前前后后调了两个多月最深的体会是数字温度传感器的优势在于把准这件事提前做掉了但能不能可靠地把数据送到 MCU这件事还是得靠硬件设计和软件容错来保证。如果你正在做类似的 HVAC 温控项目建议先从短距离、单传感器跑通整条链路再慢慢往风道里拉线。稳定可靠地测出两路温度控制策略才有发挥的空间。