做HVAC控制器的时候,温度采样看着是最不起眼的功能,真正落地时坑却不少。之前为了同时盯住本地环境温度和远程管路温度,我试过不少方案,最终稳定跑在生产线上的是“PJ85718DM单总线数字温度传感器 + PIC32MX675F512L主控”这套组合。最初用NTC加运放调理电路,样机阶段没发现问题,一到产线就曝出两台设备读数差很多,追查下去才发现探头线缆长度、分压电阻精度都在贡献误差。
后来我把方案整体切到数字传感器路子上:本地放一颗,远程走长线放一颗,一版调通之后再没为温度采样的准确性和稳定性返过工。这篇文章把从选型、硬件连接、固件驱动到现场排查的完整过程写出来,适合正在做嵌入式温度采样、HVAC控制器,或者类似长线分布式测温需求的人参考。
1. 需求出发:为什么温度采样一定要把“本地”和“远程”分开考虑
1.1 方案对比:NTC调理电路与单总线数字传感器
我上一版控制器最头疼的地方在于,本地探头装在控制板附近,走线短,远程探头装到风道或者水管上,走线往往五米起步。NTC方案想做好,需要给每个探头做独立的恒流源或者分压电路,同时ADC采样基准要足够稳。产线反馈的问题几乎都集中在同一类:线缆导致的分压偏移。两根线内阻不一样,读数就不一样,每台设备还要单独标定,这个工作量在批量生产时会无限放大。
I2C接口的数字传感器(像常见温湿度一体的那种)也考虑过,但I2C长线传输能力弱,五米走线在时钟上升沿畸变后基本不可靠,还需要额外拉两根线做总线的地址区分,隔离也不好做。单总线方案的优势正好踩中需求:只用一根数据线,每颗传感器有唯一ROM地址,允许多点挂接,而且数据线本身就带时序协议,只要延时控制准确,抗干扰能力明显强于模拟量传输。
1.2 PJ85718DM与PIC32MX675F512L的契合点
PJ85718DM这颗传感器,和常见的单总线测温芯片兼容,出厂固化了64位ROM序列号,测量范围覆盖HVAC日常使用的整个温度区间,12位分辨率下温度转换结果可重复性很好。它的单总线协议对主控来说只需要一个带外部上拉的GPIO引脚,软件上做好复位、读写时隙即可。对于像HVAC这种需要分布式布点的场景,每颗传感器单独封装成防水探头,挂到总线上就能识别,非常省事。
主控选PIC32MX675F512L,核心原因有三个。第一,它在大约80MHz的MIPS核心下还能保持较强的外设响应能力,做1-Wire时序翻转、UART上报这些活儿轻轻松松。第二,512KB Flash和充足的RAM让我有余量把滤波、标定、诊断代码都塞进去,不用像小Flash单片机那样抠着用。第三,它对电平、IO口数量、定时器数量安排都比较宽松,我可以把“本地”和“远程”两条温度通道分开接两个IO,互不干扰。这颗芯片做中大型嵌入式控制器的温度采集和逻辑控制,资源完全够用。
2. 硬件设计:两条独立DQ通道的接法与远程长线处理
2.1 最小电路:供电、上拉电阻和IO口保护
先看最小连接。PJ85718DM有供电脚、地、数据脚三根线。数据脚DQ通过一个4.7kΩ电阻上拉到电源,主控再通过一个普通GPIO口去读。上拉电阻的作用是保证总线空闲时处于高电平,因为单总线协议靠主控拉低和释放总线来产生时序,线没有上拉,释放瞬间电平不确定,通信就无从谈起。
我在PIC32MX675F512L上给本地和远程分别安排了独立的GPIO,本地通道用RD5,远程通道用RD6。这样做的直接好处是:远程长线如果短路或者被干扰拉死,不会连带影响本地的温度读数。如果两条探头挂同一根总线,远程线缆一出问题,本地探头也跟着失联,现场排障成本会高很多。成本只是多占用一个IO口而已,非常划算。
上拉电阻的取值需要根据走线长度调整。短的本地通道我用4.7kΩ,远程走线超过五米时我会把上拉降到2.2kΩ。原因很简单,线缆本身有分布电容,上拉电阻越大,信号上升沿越慢,时序边沿容易变钝;降低上拉可以加快上升沿,代价是总线低电平时流过传感器的电流稍微增加,但对一颗微功耗传感器来说完全可以接受。
2.2 远程探头的走线、屏蔽与接地细节
远程探头我推荐用三线制接法:VDD、DQ、GND各用一根线。实际布线时不要把VDD和DQ走同一根双绞线的两芯,那样DQ线上的信号变化会耦合到供电上。更合理的做法是让DQ和GND组成一对双绞,VDD和GND再留一根,这样信号回路面积最小,抗干扰最强。如果现场必须和强电电缆并行,最好给DQ单独加屏蔽层,屏蔽层只在控制器端单点接地,不要两头都接地,避免形成地环路电流。
远程探头进入控制柜后,我一般在DQ线上串联一个100Ω电阻,再在传感器引脚位置对地加一个小电容。这个RC滤波对单总线协议几乎没有影响,但对变频器、压缩机和继电器开关产生的尖峰干扰有很好的抑制作用。第一次试产时我没有加这个滤波,远程探头在压缩机启动瞬间读数会跳一下,加了之后问题消失。
如果主控的IO口不支持直接吃5V电平,还要注意传感器供电和IO电平的匹配。PIC32MX675F512L的IO口本身比较皮实,但稳妥起见,我会在远程通道上并一颗TVS管到地,防止长线感应静电把IO口打坏。防雷击的等级要求更高的场合,还需要加气体放电管和PTC,一般控制器项目做到TVS这层就足够了。
3. 固件实现:单总线驱动、温度读取与CRC校验
3.1 基于PIC32的微秒级延时与时序控制
单总线驱动最核心的是微秒级的时序控制。复位脉冲要求拉低480μs以上,写一个“0”时隙要求拉低60~120μs,写“1”时隙只拉低1~15μs,读时隙也要精确控制在几十微秒内。用软件死循环延时最怕编译器优化把延时函数改掉,所以延时函数必须用硬件计数器来实现。
PIC32的MIPS核有一个COUNT寄存器,会按照固定频率递增。系统主频80MHz时,COUNT寄存器每微秒大概递增40次。我写了一个简单的微秒延时函数:
#include <xc.h> #define SYS_CLK_HZ 80000000UL #define COUNT_PER_US (SYS_CLK_HZ / 2000000UL) // COUNT递增约为频率的一半 static inline void delay_us(unsigned int us) { unsigned int start = _CP0_GET_COUNT(); unsigned int ticks = us * COUNT_PER_US; while ((_CP0_GET_COUNT() - start) < ticks) { ; } }注意COUNT寄存器是无符号的,回绕后减法仍然正确,所以这段代码在长时间运行后不会有问题。如果换用其它主频,只需要调整SYS_CLK_HZ。
3.2 完整驱动函数:复位、读写位、读取温度
底层读写位函数,我直接操作寄存器。GPIO配置是RD5作为本地通道数据脚,TRISDbits.TRISD5控制方向,LATDbits.LATD5控制输出,PORTDbits.RD5读取电平:
#define DQ_LOCAL_TRIS TRISDbits.TRISD5 #define DQ_LOCAL_LAT LATDbits.LATD5 #define DQ_LOCAL_PORT PORTDbits.RD5 static inline void dq_local_output_low(void) { DQ_LOCAL_LAT = 0; DQ_LOCAL_TRIS = 0; // 输出低电平 } static inline void dq_local_release(void) { DQ_LOCAL_TRIS = 1; // 输入模式,外部上拉拉高 } static inline unsigned char dq_local_read(void) { return DQ_LOCAL_PORT; }写一位的操作要严格区分写1和写0。写1时先拉低一小段时间再释放,写0时保持拉低足够长的时间,最后都要等整个时隙结束再返回:
static void dq_write_bit(unsigned char bit) { if (bit) { dq_local_output_low(); delay_us(6); dq_local_release(); delay_us(64); } else { dq_local_output_low(); delay_us(60); dq_local_release(); delay_us(6); } } static unsigned char dq_read_bit(void) { unsigned char bit; dq_local_output_low(); delay_us(2); dq_local_release(); delay_us(6); bit = dq_local_read(); delay_us(55); return bit; }读时序的关键是主控拉低后要尽快释放,然后在15μs前后读取电平,因为传感器在这个时间段内决定把总线拉低还是不拉低。读早了读不到结果,读晚了时序已经翻篇。
在此基础上封装字节读写和完整的温度读取函数。温度转换指令0x44,转换时间与分辨率有关,12位分辨率下最长为750ms,所以直接延时750ms再读取暂存器。读取暂存器指令0xBE,连续读9个字节,其中前两个字节是温度值,最后一个字节是CRC校验:
static void dq_write_byte(unsigned char byte) { for (int i = 0; i < 8; i++) { dq_write_bit((byte >> i) & 0x01); } } static unsigned char dq_read_byte(void) { unsigned char byte = 0; for (int i = 0; i < 8; i++) { if (dq_read_bit()) { byte |= (1 << i); } } return byte; } float read_temperature_local(void) { unsigned char scratchpad[9]; short raw; dq_local_output_low(); delay_us(500); dq_local_release(); delay_us(80); dq_write_byte(0xCC); // 跳过ROM匹配,线上只有一个设备 dq_write_byte(0x44); // 启动温度转换 delay_us(750000); // 12位分辨率转换时间 dq_local_output_low(); delay_us(500); dq_local_release(); delay_us(80); dq_write_byte(0xCC); dq_write_byte(0xBE); // 读取暂存器 for (int i = 0; i < 9; i++) { scratchpad[i] = dq_read_byte(); } // CRC校验:字节0~7的CRC应等于字节8 if (crc8_calc(scratchpad, 8) != scratchpad[8]) { return 0x7FFFFF; // 返回一个无效值,由上层处理 } raw = (scratchpad[1] << 8) | scratchpad[0]; return (float)raw * 0.0625f; }需要注意的是,单总线转换命令之后务必等待完整时间。有人为了省时间用9位或者10位分辨率把转换时间缩短到100ms级别,但那样温度分辨率会降到0.5℃甚至更低,HVAC控制往往需要0.1℃级别的波动判断,所以我还是保持12位。
3.3 多点测量安排:跳过ROM搜索的工程取舍
有人看到单总线支持多点测温,就想着把所有探头串到一根线上做搜索匹配。理论上一根总线上最多挂几十颗传感器,每颗都能通过64位ROM区分。但工程上如果传感器数量固定,我更建议给每颗独立IO口,省掉复杂的ROM搜索算法。
ROM搜索协议需要反复遍历位判断,写起来不复杂但这部分调试成本高,而且一旦某颗传感器物理损坏,整个总线上的通信都会受影响。本地一颗、远程一颗,两个IO口各自独立,软件上就是两个完全相同的驱动函数,故障隔离也好做。如果项目确实需要一条总线上挂很多颗,那就必须在初始化时扫描总线,把所有ROM地址记录到Flash里,之后用匹配ROM指令0x55精确寻址。
CRC校验是所有单总线通信里必须做的一步。信号在长线上传输,干扰窜改数据位并不罕见,没有校验,温度偶尔跳到离谱值很难查到原因。CRC算法用多项式0x8C反推的快速查找方式,实现很简单:
static unsigned char crc8_calc(const unsigned char *buf, unsigned char len) { unsigned char crc = 0; while (len--) { crc ^= *buf++; for (int i = 0; i < 8; i++) { if (crc & 0x01) { crc = (crc >> 1) ^ 0x8C; } else { crc = crc >> 1; } } } return crc; }4. 调试验证中的三个典型故障与完整排查链路
4.1 远程通道无应答:上电时序与上拉电阻排查
远程通道第一次上电时始终找不到设备,用示波器看复位波形,发现总线释放后电平没有回到5V,而是被拉在1V左右。第一时间怀疑是远程线缆问题,拔掉远程探头后波形恢复正常,说明问题在挂上远程负载之后。
继续排查,远程探头的三根线之间没有短路,传感器供电也正常。最后发现是远程通道的对地滤波电容取值过大,加上线缆分布电容,整体电容已经超过几百nF,释放总线后5V通过上拉电阻充电的时间常数为毫秒级,远远超过单总线读时隙的要求,等不到高电平复位就被判定为无应答。把滤波电容从104降到100pF,问题立刻消失。
这个坑提醒我,任何“滤波增强抗干扰”的措施都要考虑和通信时序的配合。单总线本身就是慢协议,但对边沿仍有要求,电容一大了上升沿会钝化,甚至直接吞掉波形。
4.2 读数跳到85℃:转换时序与POR值的区别
另一个现象是,设备上电后第一次读温度,偶尔会直接读到85℃左右,之后又恢复了正常。一开始以为是传感器坏了,实际追查发现是复位时序不彻底导致的。
单总线设备上电后,暂存器中的温度寄存器默认值是0x0550,换算过来正好是85℃。如果启动转换指令没有正常执行,或者转换时间不够就去读取暂存器,读到的就是默认值或者上一次残留值。用逻辑分析仪抓了读写时序后发现,问题是复位脉冲时间不够稳定:有时候只拉低了300μs,传感器没有正确响应,后面的指令全部无效。
修正办法是把复位时序统一放到独立函数里,确保复位脉冲拉低至少480μs,释放后延时60μs再读取存在脉冲。同时每次读取前强制做一次CRC校验,一旦CRC不过直接丢弃该次结果,重新发起一轮完整测量,而不是顺手把非法数据交给控制逻辑。
4.3 本地读数受远程干扰:地回路与滤波对策
试产阶段还遇到一个很隐蔽的问题,本地温度偶尔会跟着远程探头的插拔动作一起跳变。看起来不可思议,因为两个通道用的是不同的GPIO,软件上也完全独立。
排查链路是这样的:先确认本地探头自身的走线没有问题,然后把远程探头替换成电阻负载,故障还在,说明不是传感器本身问题。最后怀疑到地回路上。远程探头通过线缆连接到控制器,线缆越长,地线阻抗越高,远程侧受到扰动的电流会在地线上产生压降,而控制器内部本地探头的参考地正好是同一块地平面,这个压降就直接叠加进本地通道。
处理办法是让本地通道的走线尽量短而粗,远程通道的屏蔽层在控制器端单点接地,同时在远程探头地线进入控制柜的地方加一个磁珠或者小电感隔离高频扰动。实际改板之后,本地读数再也没跟着远程插拔抖动过。
5. 从原始读数到可靠测量值:滤波、标定和数值格式化
5.1 滑动滤波与异常值剔除
传感器读数在HVAC环境里会有正常的微小波动,几毫伏的电源噪声换算成温度就是零点零几度的变化,控制上没必要跟着抖。我在软件里做了一个16点滑动平均,每读完一个新样本就计算窗口内平均值。这个滤波对HVAC来说是合适的,因为温度本身就是慢变量,闭阀动作周期也要秒级,16点引入的延迟完全不影响控制效果。
只做平均值还不够,还要处理极端毛刺。单总线偶发干扰即使通过了CRC,也可能产生一个跳变幅度很大的值。我的做法是在滑动平均前先做一步剔除:如果新样本与上一次正常值的差超过3℃,就认为该样本可疑,连续两次超过阈值才采用,否则等待下一轮。这个简单规则实测下来效果很好,没有牺牲响应速度。
5.2 两点标定:冰水槽与恒温水槽
PJ85718DM这类传感器的出厂精度对一般商用设备够用,但项目要求两台设备之间同一测点读数差不大于0.3℃时,就必须做整机标定。
标定流程不复杂。先把探头放到冰水混合物中,等温度稳定后记录软件读数,记为t0;再把探头放到恒温槽里,恒温槽设在50℃,稳定后记录读数t1。假设真实温度分别是0℃和50℃,那么补偿公式就是一条直线:
// sensor_c是滤波后的原始测量值 // cal_gain和cal_offset存在EEPROM里 float cal_gain = 1.0f; float cal_offset = 0.0f; float apply_calibration(float sensor_c) { return sensor_c * cal_gain + cal_offset; }两点标定求gain和offset时,需要注意标定探头必须和实际使用状态一致,包括线缆长度和连接方式。因为长线电阻对单总线数字协议本身没有影响,但环境温度剧烈变化时,探头本身的封装热阻可能引入小偏差。
5.3 面向控制器的0.1℃定点输出格式
HVA C控制逻辑通常不需要浮点温度值,浮点在定点MCU上既慢又可读性差。我会把补偿后的浮点温度统一转为带符号0.1℃精度的整数,例如25.4℃就存为254,-3.2℃就存为-32。
static int16_t temp_to_q10(float temp_c) { if (temp_c > 3276.7f) { return 32767; } if (temp_c < -3276.8f) { return -32768; } return (int16_t)(temp_c * 10.0f + (temp_c >= 0.0f ? 0.5f : -0.5f)); }这种格式在控制逻辑里可以直接比较、加减,也可以直接打包进上报帧,上位机只需要除以10就能还原成摄氏度。内部计算时如果涉及温差,仍然用定点运算,完全够用,不需要浮点库参与。
6. HVAC集成经验:探头防护、地址维护与数据上报
6.1 风道和管路里探头的防凝露与热响应
HVAC环境有一个特殊问题:风道和冷水管表面容易凝露,普通环氧封装的传感器长期泡在潮湿环境里,读数和寿命都会打折。我一般会选用带不锈钢套管或者灌胶封装的探头,安装时用导热硅脂填充套管与传感器之间的空隙,既保证热传导,又阻止水汽直接接触引脚。
这里有个矛盾点需要注意:套管越厚,防水越好,但热响应越慢。如果控制器有快速的除霜或者防冻逻辑,探头热时间常数太大会导致控制滞后。经验做法是,把温度采样周期和探头的热时间常数对齐,风速变化大的风道里优先选薄壁小套管,水管表面安装则用弹片压接加强接触面积。
6.2 设备更换探头后的地址维护
如果远程通道使用的是独立IO口,换探头不需要维护地址。如果项目将来扩展成一条总线挂多颗探头,那就要在初始化上位每个设备维护64位ROM地址表。我处理的办法是在首次上电时启动一次总线扫描,把每颗探头的ROM地址打印到调试串口,同时烧录进片内Flash。
现场更换探头后需要重新扫描,扫描到的新地址如果不在表中,就提示维护人员确认是否新增探头,防止因为换探头导致控制逻辑读取了错误测点。程序里还要做最大重试次数限制,连续读取失败一定次数后标记该通道故障,并向主控上报故障码,而不是一直留在一个死循环里等数据。
6.3 给主控的温度上报帧设计
两路温度最终要交给主控逻辑或者上位机使用。我在本地模块里的做法是,每200ms把两个温度值打包成一帧数据,通过UART发给主控板。帧格式尽量简单,首字节用0xAA做帧头,尾字节用累加和校验,中间放两个温度定点值、状态位和温度通道序号。
一个典型的帧结构是这样:帧头、长度、命令字、本地温度高字节、本地温度低字节、远程温度高字节、远程温度低字节、状态字节、校验字节。状态字节里分别标记本地通道故障、远程通道故障、数据有效位。主控收到后先查长度和校验,校验通过才更新控制用的温度缓存。
这种设计的好处是,温度和状态天然分离,故障时主控能看到明确的故障标志,而不会把异常温度当成真实测量值参与控制。现场调试时我用调试串口打印原始帧,很快就能判断是传感器问题、线缆问题还是通讯协议问题。
最后再分享一个个人经验:如果手头项目也需要类似的双路温度采集,强烈建议在硬件上就把本地和远程彻底分开,多一个IO口和一次独立驱动函数的成本极低,后期排查问题却会舒服很多。这套PJ85718DM与PIC32MX675F512L的组合,我已经在几个项目里连续跑了很长时间,稳定性对得起当时的选型判断。