基于单总线数字温度传感器与PIC32的HVAC双路温度采集实践

📅 发布时间:2026/10/22 1:06:45
基于单总线数字温度传感器与PIC32的HVAC双路温度采集实践
做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的组合我已经在几个项目里连续跑了很长时间稳定性对得起当时的选型判断。