这两年做HVAC控制器,我越来越觉得“测温”这件事有点像家务里的扫地:看起来谁都会,但真正要在一个常年震动、温差大、还有电磁干扰的环境里长期稳定读数,坑比想象中多。我手头这个项目,用的是PJ85718DM数字温度传感器加PIC18F87J50单片机,解决的就是暖通空调和嵌入式场景里最常见的“本地温度要准、远端温度也要能拿回来”的问题。
写这篇文章,主要是想把整个项目的设计思路、硬件要点、固件实现和踩坑过程整理出来。内容适合刚接触单片机测温的开发者,也适合做HVAC控制系统或楼宇自控的工程师参考。我会尽量把关键的计算、代码片段和调试方法写清楚,原则是“照着做能跑起来,出了问题知道去哪查”。
1. 项目定位与整体设计思路
1.1 核心需求解析
先拆解一下标题里的信息量。PJ85718DM是一颗数字温度传感器,I2C接口,内部集成ADC和校准逻辑,可以直接输出数字温度值;PIC18F87J50则是一颗8位单片机,资源在同类MCU里算是比较充足的。这两个器件组合在一起,核心任务就是“测温度”和“传温度”。
项目里“本地温度”通常指控制器电路板上或控制柜内的温度,“远程温度”指数米外甚至几十米外某个测点的温度,比如风管内部、回风口、设备表面。放在HVAC场景里,典型应用就是机房空调的控制器需要同时知道自身环境温度(防止控制板过热)和房间/管道温度(用于制冷制热逻辑),然后把两个温度都上报给上位机或楼宇网关。
实际设计时,还要考虑几个隐性需求:传感器长期工作是否漂移、温度读数会不会被MCU自身发热影响、远距离传输时数据会不会丢、多个设备共用一个总线时地址怎么分配。这些在后文都会展开。
1.2 为什么选PJ85718DM和PIC18F87J50
选PJ85718DM,而不是用热敏电阻加ADC,主要原因有三点。第一,数字输出省掉了后级调理电路,温度值直接通过I2C读取,精度由芯片出厂校准保证,批量生产时省去了逐个标定的麻烦。第二,它体积小,可以贴在PCB的任何位置,灵活度高。第三,它和PIC18F87J50的MSSP模块能无缝对接,底层驱动代码量很少。
PIC18F87J50的选择则更偏向综合考量。这颗MCU的工作电压范围宽,适合和3.3V传感器直连;内置多个通信外设,I2C、UART、USB都能用,给“远程上报”留下了充足接口。相比同级别其他MCU,它的程序存储空间和RAM足够跑通信协议栈和简单的任务调度,不需要外扩存储器。
选型还有一个很重要的点:供应链稳定和长期供货。工业HVAC产品的生命周期很长,一颗主控芯片可能要卖五六年。PIC系列在工业控制领域用量大、资料多,后来接手维护的工程师上手成本低。这一点在项目规划阶段就该想清楚。
1.3 整体系统架构
整个系统的数据流可以这样理解:两个PJ85718DM温度传感器(一个本地、一个远程)挂在同一条I2C总线上,PIC18F87J50作为主控周期性轮询它们,读取原始温度数据,经过滤波和标定换算成实际温度,一方面驱动本地显示和告警,另一方面通过RS-485总线以Modbus RTU协议上报给上位机。
如果是更远距离的测点,I2C线长超过3米就不太靠谱了,项目里我一般会用两种方案:一是在远端放一块小型采集板,用PJ85718DM测温,然后通过RS-485把温度值发回主控;二是直接在主控上扩展一路远程传感器接口,用屏蔽双绞线加专用驱动芯片延长I2C。第一种方案工程上更稳妥,后面章节我会以这种方式为主来讲解。
从软件架构上说,主控程序不跑复杂RTOS,就一个主循环加定时中断,用状态机把I2C读取、滤波、显示、上报这几个任务串起来。原因很简单:逻辑足够简单时,状态机比RTOS更好排查问题,也更加可控。
2. 硬件设计与原理图关键点
2.1 PJ85718DM典型电路与I2C地址配置
PJ85718DM的典型外围电路非常简洁,基本就是一颗去耦电容和两个上拉电阻。传感器供电电压我用的是3.3V,和PIC18F87J50保持同一个电源轨,避免电平不匹配。VDD引脚旁放一个0.1uF陶瓷电容,位置尽量贴近芯片,如果电路板上数字电路比较多,建议再加一个4.7uF的钽电容做低频滤波。
I2C总线上的上拉电阻阻值需要根据总线长度和速率来选。项目里总线线缆不超过1米,SCL和SDA各接4.7kΩ上拉到3.3V。如果线缆变长或者总线上挂了4个以上的设备,阻值要降到2.2kΩ左右,否则信号上升沿会变缓,导致通信不稳定。这个只要用示波器看一眼波形就知道该往哪个方向调。
I2C地址方面,PJ85718DM通常允许通过地址引脚配置一个或多个可选地址位。本地传感器和远程传感器如果共用一条I2C总线,必须给它们设置不同地址。我习惯把本地模块地址设为0x48,远程模块地址设为0x49,这样固件里读取时根据地址就能区分数据来源。硬件上对应引脚接高或接低要在原理图里标注清楚,否则后面调试时容易怀疑人生。
2.2 PIC18F87J50最小系统与接口分配
主控最小系统包括电源、晶振、复位和下载调试接口四部分。PIC18F87J50工作电压范围宽,项目直接使用3.3V供电。晶振用一个8MHz或12MHz的外部晶振,内部PLL倍频后跑48MHz或更高频率,I2C和UART波特率都用定时器分频得到,晶振精度会影响串口通信的波特率误差,所以不要图便宜买劣质晶振。
复位引脚接一个10kΩ上拉电阻和0.1uF电容到地,实现上电复位。ICSP接口引出MCLR、PGD、PGC和VDD、GND五个引脚,方便生产时烧录固件和后续现场升级。注意PGD和PGC尽量不要复用为普通IO,调试时频繁插拔下载器,复用引脚容易造成干扰。
IO资源分配上,SDA和SCL接MSSP模块的对应引脚,RS-485收发芯片的方向控制脚接一个普通GPIO,本地显示模块如果用的是I2C OLED,可以与温度传感器挂同一条总线(地址错开即可)。剩下几个关键IO留给告警继电器和状态LED。建议在程序里做一个pin_map.h头文件统一管理引脚定义,后期改板子只需要改一个文件。
2.3 PCB布局与热设计细节
这个项目里最容易忽略的是热设计。PJ85718DM虽然封装小,但它的读数反映的是芯片自身温度,如果传感器旁边就是MCU或者电源芯片,读到的温度会比实际环境温度高好几度。我在第一版PCB上就吃过这亏,传感器离一颗LDO太近,静止空气中读数偏高3.5℃,直接导致设备频繁误告警。
解决方法是把温度传感器放在PCB角落,远离发热器件和电源路径,四周不要铺大面积铜皮。如果结构上允许,用开槽的方式把传感器区域的PCB挖空,削弱热量传导。还有一点,传感器焊盘附近不要走大电流回路,这一点很多PCB布局教程不会特别强调,但实测影响非常大。
HVAC应用里还要考虑冷凝问题。空调设备内部温度低、湿度大,PCB表面容易结露。传感器引脚间距比较小,一旦结露可能形成漏电通路,导致I2C电平异常。项目量产版本在PCB正面刷了三防漆,传感器和接口区域也做了涂覆处理,实际运行一年没有再出现因湿度导致的通信故障。
3. 固件架构与传感器驱动实现
3.1 软件分层与任务调度
固件整体分为三层:驱动层、应用层、通信层。驱动层封装对PJ85718DM的操作,包括I2C读写、寄存器配置、温度数据解析;应用层负责滤波、告警逻辑和显示刷新;通信层处理Modbus RTU协议和RS-485收发控制。层与层之间通过函数接口调用,全局变量数量严格限制。
主循环采用一个5ms周期时基,由定时器中断产生一个标志位。每次主循环检查标志位,按顺序处理温度采样、滤波、显示、通信几个任务。温度采样不需要每5ms做一次,PJ85718DM完成一次转换需要几十毫秒,所以我在任务计数里做分频,每200ms读取一次温度,每秒更新一次滤波输出。
之所以不用RTOS,是因为这个项目任务简单,且PIC18F87J50的RAM也不算大。用状态机实现时序逻辑,通过一个全局结构体保存每个传感器的当前状态、原始值、滤波值、错误计数,代码既直观又不容易死锁。
3.2 I2C驱动与温度读取代码实现
PIC18F87J50的MSSP模块配置成I2C主模式很简单,关键在于读写时序要严格按照数据手册操作。下面给出我用的初始化函数和读取温度函数,实际移植时根据芯片头文件调整即可。
void i2c1_init(void) { // 将SCL和SDA引脚配置为开漏输出 TRISDbits.TRISD0 = 1; TRISDbits.TRISD1 = 1; // I2C 主模式,100kHz SSP1CON1 = 0x28; // SSPEN=1, I2C Master mode SSP1ADD = 0x4F; // 根据时钟频率计算得到100kHz // 中断可选用,这里用轮询方式 } ```c // 读取PJ85718DM温度寄存器,返回原始12位ADC值 int16_t read_temperature(uint8_t i2c_addr) { int16_t raw = 0; uint8_t msb = 0; uint8_t lsb = 0; i2c_start(); i2c_write(i2c_addr << 1); // 写地址 i2c_write(0x02); // 温度寄存器高字节地址 i2c_stop(); i2c_start(); i2c_write((i2c_addr << 1) | 1); // 读地址 msb = i2c_read(1); // 读取MSB,随后发送ACK lsb = i2c_read(0); // 读取LSB,发送NACK i2c_stop(); raw = ((int16_t)msb << 8) | lsb; // 如果芯片是12位分辨率,低4位为0或者无效位,直接右移4位 return (int16_t)(raw >> 4); }读取频率方面,我设置连续转换模式,每200ms读取一次。需要注意的是,刚上电时传感器可能需要几百毫秒稳定时间,主控启动后不要立刻去读,先在main函数里延时300ms再初始化I2C总线。
3.3 温度换算、滤波与多点校正
PJ85718DM的12位分辨率通常对应0.0625℃的LSB,也就是16个LSB等于1℃。工程代码里不想用浮点运算,我直接用“温度值乘以16”作为内部表示,显示或上报时再整除16。这样既避免了浮点库带来的代码膨胀,又保证了0.0625℃的分辨率。
滤波策略我用了两步:先做中值滤波,再做滑动平均。中值滤波取最近5次采样的中间值,能去掉偶发的异常毛刺;滑动平均取最近10个中值结果求平均,平滑掉气流导致的短时波动。这套组合在HVAC环境里表现很好,既不会过度滞后,又能抑制干扰。
标定方面,PJ85718DM出厂前做过校准,常规场景不需要用户重新标定。但如果传感器安装位置固定后存在系统性偏差,可以在固件里做两点校正:读取已知的两个温度点,计算偏置和比例系数,然后对每个原始温度做线性变换。批量产品建议在产线上一并写入校准参数,存到Flash数据区,不要烧死在代码里,这样以后调整标定值不用重新烧录程序。
4. 本地监测与远程上报的实现
4.1 本地温度显示与告警逻辑
本地显示我用了一颗I2C接口的小尺寸OLED屏,显示内容包括两个测点的实时温度、滤波后的温度、通信状态和告警状态。OLED和两个温度传感器共用I2C总线,地址不同,不会冲突。
告警逻辑要注意“回差”设计。如果只在温度超过阈值时触发告警,温度在阈值附近波动时会频繁吸合继电器,损坏触点。我的做法是:当温度超过上限阈值时进入告警状态,只有温度降到上限阈值减去回差值(比如3℃)时才解除告警。这样系统有了滞回特性,动作稳定多了。
本地告警输出使用一个继电器驱动外部声光报警器。继电器控制引脚需要通过三极管或ULN2003驱动,不要直接用MCU引脚驱动继电器线圈,否则引脚电流不够还会烧IO。继电器的续流二极管一定不能省,否则关断瞬间的反向电动势会顺着走线干扰传感器读数。
4.2 远程通信协议设计与实现
远程上报我选的是RS-485总线加Modbus RTU协议。这是HVAC和楼宇自控里最常见的做法,兼容性好,上位机或触摸屏不用额外开发就能直接读取数据。
Modbus RTU的帧结构是:地址码、功能码、数据、CRC16校验。项目中我实现了两个功能码,03读保持寄存器用于读取温度值,06写单个保持寄存器用于修改告警阈值。温度值以整数形式上传,单位是0.01℃,比如23.56℃就上传2356。两个测点分别映射到寄存器地址0x0001和0x0002,告警阈值映射到0x0010。
RS-485收发切换需要特别留意。发送时先置高方向控制脚,延时几个微秒再开始发送数据;发送完成后把方向脚拉低,再延时至少一个字节的时间才能接收。这个时序如果处理不好,最后一个字节会被截断,上位机收到CRC错误。
我这里附带一个简化的Modbus发温度帧示例,方便理解数据组织方式。
void modbus_send_temperature(uint8_t slave_addr, uint16_t reg_addr, int16_t temp) { uint8_t frame[8]; frame[0] = slave_addr; frame[1] = 0x03; frame[2] = (reg_addr >> 8) & 0xFF; frame[3] = reg_addr & 0xFF; frame[4] = 0x00; frame[5] = 0x01; uint16_t crc = modbus_crc16(frame, 6); frame[6] = crc & 0xFF; frame[7] = (crc >> 8) & 0xFF; rs485_send_bytes(frame, 8); }CRC16的实现就不贴全代码了,网上标准查表法很多。有一点经验可以分享:CRC计算的是从地址码到数据字段为止的所有字节,校验值本身低字节在前、高字节在后,千万别把顺序搞反,我见过很多人卡在这上面。
4.3 HVAC场景中的联动控制
远程温度不光是拿来显示的,还要参与控制逻辑。项目里比较典型的联动场景是这样的:远程温度测的是房间回风温度,本地温度测的是控制板内部温度。当远程回风温度高于设定值时,控制器要加大制冷输出;当本地温度过高时,即使远程温度还没超标,也要强制降低输出功率,保护控制器本身。
这个“双重温度参与控制”的思路在HVAC设备里是很实用的。因为控制板内部温升往往意味着散热不良或者环境温度过高,如果不处理,MCU自己都可能热死机。把两个温度都纳入控制逻辑,比只看远程温度更安全。
联动控制的具体输出形式我用的是PWM控制风机转速和压缩机启停。PWM的占空比由温度偏差经过比例计算得到,为了平滑,每秒刷新一次,不做过快的调整。这里不建议用PID,因为HVAC热惯性很大,简单的比例控制加死区已经足够稳定。
5. 常见问题排查与经验技巧
5.1 I2C总线不通信的排查流程
项目调试中I2C出问题的情况非常频繁,我总结了一套排查顺序。第一步,检查SDA和SCL上的波形,用示波器或逻辑分析仪。如果SCL没有时钟,问题在单片机配置;如果SCL有波形但SDA一直为高,多半是传感器没应答,检查地址对不对、供电脚有没有焊好;如果SDA为低电平拉不上去,基本是某个设备把总线卡住了。
总线卡死有个经典原因:系统上电时传感器还没准备好,主控就开始通信,传感器可能会把SDA拉低。解决方法是上电后等待传感器初始化完成再访问,或者把对传感器的I2C读操作加上超时判断,连续失败超过5次就报通信错误并复位I2C模块。不要在一个死循环里无脑等ACK,否则总线一锁,整个系统就瘫痪了。
从实际经验看,I2C总线上挂多个设备时,地址冲突也是常见问题。建议在固件里加一个“总线扫描”函数,上电后打印出所有应答的I2C地址,排查冲突非常方便。
5.2 读数跳变与精度偏差的应对办法
读数跳变通常不是传感器坏了,而是干扰进来了。常见的来源有三个:电源纹波、I2C线路耦合噪声、传感器附近强电磁干扰。我处理过的一个案例是变频器启动瞬间温度读数跳了2℃多,排查后发现是传感器走线紧挨着动力线,变频器启动时产生强干扰耦合进了I2C信号。
应对措施有几条:传感器走线尽量短,并保持和动力线至少10mm间距;I2C线做成等长走线,如果需要跨接板子,使用屏蔽线并把屏蔽层单端接地;传感器供电和主控之间加一个磁珠和10uF电容隔离。软件上,前面提到的中值滤波也能挡住大部分瞬时跳变。
至于读数偏高,先检查安装位置的热干扰,比如是不是挨着电感或者LDO。再确认传感器处于通风良好的位置,不要装在密闭的小腔体里。如果确认存在固定偏差,就启动固件里的偏移校正参数,不需要换硬件。
5.3 远程通信丢包与总线冲突问题
RS-485通信不稳定时,首先要排除总线末端电阻的问题。线路较长或者总线设备较多时,必须在总线两端各加一个120Ω终端电阻,否则信号反射会造成误码。其次检查波特率误差。PIC的UART波特率由外部晶振决定,晶振不准会导致累积误差,长时间通信后出现帧错误。
软件上要增加帧超时和重发机制。我自己的做法是:如果一条温度上报指令发出去后500ms内没有收到应答,就重发一次,连续重发3次没有应答,把通信状态标志置为离线,同时保留最后一次有效温度值显示。这样上位机虽然收不到最新数据,但至少不会因为通信中断而误判为设备故障。
还有一点容易被忽视:RS-485总线是多主半双工结构,如果总线上有两个主机会同时发数据,就会冲突。项目里所有从设备都是被动应答,主从设备之间还要约定好轮询周期,不要让从设备主动上报。从设备只有在接到主设备查询指令时才回复,这是最稳妥的。
5.4 HVAC环境下的长期运行稳定性
这类设备装到空调机房后,往往一年到头不停机。长期稳定性考验的是电源设计和看门狗策略。我强烈建议项目中启用PIC18F87J50的看门狗定时器,喂狗放在主循环末尾,而不是中断里。这样做的好处是,一旦任务卡死在某个状态机上,看门狗也能把系统复位。
Flash写入要特别注意减少次数。告警阈值、校准参数这类变量会频繁修改,如果直接写Flash,几百次就把Flash寿命耗尽。我通常用EEPROM或外挂存储来放参数,内部Flash只用做强加密的固件代码存储。如果MCU内部没有EEPROM,可以考虑模拟EEPROM的磨损均衡算法,但复杂度会上去。
最后说一个被人忽视的小细节:固件版本号。HVAC现场设备多,版本管理混乱会导致排查问题非常困难。我在程序里固定一个字符串数组保存版本号和编译日期,通过Modbus寄存器可以读出来。现场哪怕出一个小问题,先读版本号,往往能少走很多弯路。
回到这个项目的整体体验,PJ85718DM加PIC18F87J50这套组合在做HVAC温度监测时,成本、功耗、稳定性三者之间平衡得比较理想。如果你接下来也要做类似项目,我的建议是不要一上来就把硬件和软件全部铺开,先把一个传感器接到MCU上,打通I2C读取到串口输出的最小链路,再扩展第二个传感器、显示、RS-485,一步一步来。测温这件“小事”,把地基打牢了,后面的所有功能都是水到渠成。