1. 从一颗温度传感器说起:为什么HVAC场景需要双通道测温
做过嵌入式暖通空调控制板的人都有一个共识:温度采样不准,后面所有的PID调节、风机调速、阀门开度控制全是空中楼阁。我最早接触这类项目时,用的是单点NTC热敏电阻加分压电路,成本确实低,但问题也很明显——本地控制板附近的温度受功率器件发热影响,测出来的值比实际房间温度高出好几度,导致压缩机频繁启停。后来换成数字温度传感器方案,才真正把精度和抗干扰能力拉上来。
这次要聊的方案,核心是PJ85718DM这颗温度传感芯片配合ATmega644P这款经典8位MCU,搭建一套能同时监测本地温度和远程温度的采集系统。所谓本地温度,指的是控制板自身所处环境的温度,通常用于板级热管理和冷端补偿;远程温度则是指通过探头延伸到被控区域(比如风管、水箱、房间)的温度,这才是真正参与控制决策的数据。两路温度各有各的用途,缺一不可。
PJ85718DM是一颗支持本地与远程双通道测温的传感器,远程通道通常外接一个二极管接法的三极管(比如常见的MMBT3904)作为感温元件,利用三极管基极-发射极电压与温度的线性关系来换算温度值。ATmega644P则是AVR系列里资源比较充裕的一款,64KB Flash、4KB SRAM、2KB EEPROM,双USART、硬件I2C(TWI)、8通道10位ADC,用来做这种多路温度采集加通信上报的任务绰绰有余。
这套组合适合谁?如果你正在做中央空调控制器、新风系统、地暖分集水器控制、冷库温控、机房精密空调这类产品,或者你只是想在开发板上验证一下远程测温的精度和稳定性,这篇文章里的思路和代码都能直接拿去改。我会把选型理由、硬件连接、寄存器配置、温度换算、校准方法、常见坑点全部拆开讲清楚,尽量让你少走弯路。
2. PJ85718DM的测温原理与ATmega644P的接口选型
2.1 远程测温为什么要用三极管而不是热敏电阻
先解释一个很多人会问的问题:远程温度为什么不用NTC热敏电阻,非要用三极管?原因在于信号形式和抗干扰能力。NTC是电阻型器件,长距离引线带来的线阻会直接叠加到测量结果上,几十米的线缆引入几欧姆的误差,换算成温度可能就是好几度。而三极管测温利用的是PN结正向压降与温度的线性关系,传感器工作在电流驱动模式下,引线电阻的影响被恒流源大幅削弱,同时输出的是电压信号,配合差分输入可以很好地抑制共模干扰。
PJ85718DM的远程通道就是基于这个原理设计的。它内部会轮流给远程三极管注入两个不同大小的电流(通常是10uA和100uA左右,具体以数据手册为准),分别测量对应的VBE,然后通过计算两个VBE的差值ΔVBE来推导温度。这个ΔVBE与绝对温度成正比(PTAT),与三极管本身的工艺参数关系不大,所以一致性很好,不需要每颗都单独校准。
2.2 ATmega644P这边用什么总线接
PJ85718DM一般提供I2C(或SMBus)兼容的串行接口,ATmega644P自带硬件TWI模块,直接用它就行,没必要软件模拟。硬件TWI的好处是时序稳定、不占用CPU、支持中断和总线仲裁。ATmega644P的TWI引脚是PC0(SCL)和PC1(SDA),注意这两个脚需要外接上拉电阻,典型值4.7kΩ到10kΩ,电源3.3V时用4.7kΩ比较稳妥。
这里有个细节:ATmega644P的TWI时钟频率由TWBR寄存器和预分频位共同决定,公式是:
SCL频率 = F_CPU / (16 + 2 * TWBR * 4^TWPS)假设F_CPU是8MHz,想要100kHz的SCL,TWPS取0,那么TWBR = (8M / 100k - 16) / 2 = 32。如果跑400kHz,TWBR = (8M / 400k - 16) / 2 = 6。实际项目里100kHz足够用,抗干扰余量更大,没必要追求高速。
| 参数 | 取值 | 说明 |
|---|---|---|
| 主控 | ATmega644P | 8位AVR,硬件TWI |
| 温度传感器 | PJ85718DM | 本地+远程双通道 |
| 远程感温元件 | 二极管接法三极管 | 如MMBT3904,基极与集电极短接 |
| 总线 | I2C | SCL=PC0,SDA=PC1 |
| 上拉电阻 | 4.7kΩ | 3.3V供电时 |
| 总线速率 | 100kHz | TWBR=32(8MHz晶振) |
2.3 供电与去耦的讲究
PJ85718DM的供电范围通常在2.7V到5.5V之间,和ATmega644P的3.3V或5V系统都能兼容。但要注意,传感器的供电质量直接影响测温精度。我实测过,如果电源纹波超过50mV,本地温度读数会跳动±0.5℃以上。所以每个电源引脚旁边必须放一个0.1uF的陶瓷电容,紧贴芯片引脚,再并一个1uF到10uF的钽电容做低频滤波。
另外,远程三极管的走线要尽量短,如果实在需要延长,建议用屏蔽线,屏蔽层单端接地。三极管的基极和集电极短接后接到传感器的D+引脚,发射极接到D-引脚,这条差分走线最好等长、靠近,避免形成环路拾取噪声。
3. 寄存器级驱动:从初始化到读出第一组温度
3.1 TWI初始化的完整代码
很多教程只给个大概,我这里把ATmega644P的TWI初始化写全,包括波特率设置、使能、上拉使能。注意ATmega644P的TWI上拉是内部弱上拉,真正起作用的是外部电阻,内部上拉只是辅助。
#include <avr/io.h> #define F_CPU 8000000UL void twi_init(void) { // SCL频率100kHz,TWBR=32,预分频1 TWBR = 32; TWSR &= ~((1<<TWPS1) | (1<<TWPS0)); // 预分频=1 TWCR = (1<<TWEN); // 使能TWI // 内部上拉使能(辅助,主要靠外部4.7k) PORTC |= (1<<PC0) | (1<<PC1); }这段代码里,TWBR=32是根据前面公式算出来的,TWSR的低两位清零表示预分频为1。PORTC的上拉使能是为了在总线空闲时保持高电平,但如果外部已经有4.7k上拉,内部上拉可以不开,开了也无妨,只是增加一点功耗。
3.2 写寄存器与读寄存器的封装
PJ85718DM的寄存器地址和具体命令字需要参照它的数据手册,我这里用通用的I2C读写函数来演示,你把设备地址和寄存器地址替换成实际值即可。设备地址通常是7位,左移一位后最低位表示读写方向。
#include <util/twi.h> #define SENSOR_ADDR 0x48 // 示例地址,以实际手册为准 // 等待TWI操作完成 static uint8_t twi_wait(void) { uint16_t timeout = 0; while (!(TWCR & (1<<TWINT))) { if (++timeout > 60000) return 1; // 超时 } return 0; } // 向传感器写一个字节到指定寄存器 uint8_t sensor_write_reg(uint8_t reg, uint8_t val) { // 发送START TWCR = (1<<TWINT) | (1<<TWSTA) | (1<<TWEN); if (twi_wait()) return 1; // 发送地址+写 TWDR = (SENSOR_ADDR << 1) | 0; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; // 发送寄存器地址 TWDR = reg; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; // 发送数据 TWDR = val; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; // 发送STOP TWCR = (1<<TWINT) | (1<<TWSTO) | (1<<TWEN); return 0; } // 从指定寄存器读一个字节 uint8_t sensor_read_reg(uint8_t reg, uint8_t *val) { TWCR = (1<<TWINT) | (1<<TWSTA) | (1<<TWEN); if (twi_wait()) return 1; TWDR = (SENSOR_ADDR << 1) | 0; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; TWDR = reg; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; // 重复START TWCR = (1<<TWINT) | (1<<TWSTA) | (1<<TWEN); if (twi_wait()) return 1; TWDR = (SENSOR_ADDR << 1) | 1; TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; // 读数据,发送NACK后STOP TWCR = (1<<TWINT) | (1<<TWEN); if (twi_wait()) return 1; *val = TWDR; TWCR = (1<<TWINT) | (1<<TWSTO) | (1<<TWEN); return 0; }这套读写函数是阻塞式的,加了超时保护,避免总线被拉死时程序卡住。实际产品里如果对实时性要求高,可以改成中断或状态机方式,但温度采集本身对时间不敏感,阻塞式完全够用。
3.3 配置本地与远程通道
PJ85718DM上电后需要配置几个关键寄存器:配置寄存器用来选择测温通道、设置转换速率和分辨率;状态寄存器用来判断转换是否完成;温度寄存器存放结果。典型的初始化流程是:
- 写配置寄存器,使能本地和远程两个通道,设置转换速率为每秒4次(降低自发热)。
- 写远程三极管配置寄存器,设置理想因子和串联电阻补偿(如果手册支持)。
- 等待第一次转换完成,读取状态寄存器确认。
- 读取本地温度寄存器和远程温度寄存器。
void sensor_init(void) { // 配置:本地+远程使能,4次/秒,12位分辨率 sensor_write_reg(0x01, 0x60); // 远程通道配置:理想因子1.008,无串联电阻补偿 sensor_write_reg(0x02, 0x1F); // 其他配置按手册补充 }这里的寄存器地址和数值是示例,实际一定要对照你手上那颗芯片的手册来写。不同厂家的寄存器映射差别很大,照抄代码很容易读不出数据。
4. 温度换算、校准与精度提升的实战细节
4.1 原始值到摄氏度的换算
PJ85718DM输出的温度通常是12位或16位有符号数,格式可能是每LSB等于0.0625℃或0.125℃。假设读到的高字节和低字节组合成一个16位有符号整数raw,换算公式是:
float raw_to_celsius(int16_t raw) { // 假设每LSB为0.0625℃ return raw * 0.0625f; }如果是12位左对齐的格式,需要先右移4位再乘。这一步一定要看清楚手册里的数据格式说明,我见过有人直接把16位原始值当温度用,结果读出来几千度,排查半天才发现是格式没对齐。
4.2 远程通道的理想因子校准
远程三极管的理想因子(η)不是精确的1.000,常见型号在1.004到1.008之间。如果传感器支持理想因子寄存器,写入正确值可以显著提升精度。如果不支持,就需要用软件校准:在已知温度下(比如冰水混合物0℃和沸水100℃,注意气压修正)读取远程温度,计算偏差,然后在代码里加一个偏移量。
我一般用两点校准法:先在一个稳定温度点(比如25℃恒温箱)记录读数,再在另一个温度点(比如50℃)记录读数,算出斜率和截距,用线性修正。这样比单点偏移更准,因为理想因子误差是增益误差,不是固定偏移。
| 校准方法 | 适用场景 | 精度提升 | 操作复杂度 |
|---|---|---|---|
| 单点偏移 | 精度要求不高 | ±0.5℃ | 低 |
| 两点线性 | 宽温区 | ±0.2℃ | 中 |
| 理想因子寄存器 | 传感器支持 | ±0.1℃ | 低 |
| 多点查表 | 高精度场合 | ±0.05℃ | 高 |
4.3 自发热与采样率的权衡
温度传感器自身也会发热,尤其是转换速率高的时候。PJ85718DM在每秒8次转换时,自发热可能让本地温度读数比环境高0.3℃到0.5℃。对于HVAC应用,温度变化本身很慢,完全没必要高速采样。我通常把转换速率设成每秒1次或每秒4次,既降低自发热,又减少总线占用。
另外,ATmega644P本身也会发热,如果传感器离MCU太近,本地温度会被MCU的热量污染。布局时尽量让传感器远离稳压器、功率MOS和MCU本体,至少保持1厘米以上距离,有条件的话在传感器下方开槽隔离。
4.4 远程三极管的选型与走线
远程三极管不是什么型号都行。要选低噪声、高hFE、一致性好的小信号三极管,比如MMBT3904、BC847这类。有些三极管内部有保护二极管或电阻,会破坏测温线性度,绝对不能用。基极和集电极短接后作为正端,发射极作为负端,这个接法不能搞反。
走线方面,如果远程探头距离超过30厘米,建议用双绞线,并且每根线串联一个100Ω电阻靠近传感器端,配合传感器输入端的滤波电容,可以大幅抑制射频干扰。我做过一个风管温度探头,线长2米,没加滤波时读数每隔几秒跳一次,加了RC滤波后稳定在±0.1℃以内。
5. 总线通信的稳定性设计与故障恢复
5.1 I2C总线锁死的成因与解锁
I2C总线最让人头疼的问题就是锁死:某个从机在传输过程中被复位或掉电,把SDA拉低不放,主机再也发不出START。ATmega644P的TWI模块遇到这种情况会一直等TWINT,如果没加超时,程序就卡死了。我在前面代码里加了超时返回,但光超时还不够,还得有解锁机制。
解锁的原理是:主机手动发送9个SCL脉冲,让从机把剩余的数据位发完,然后发一个STOP。具体做法是把SCL和SDA临时切成普通GPIO,模拟时钟脉冲。ATmega644P上可以这样写:
void i2c_bus_recover(void) { // 切为GPIO DDRC |= (1<<PC0) | (1<<PC1); PORTC |= (1<<PC0) | (1<<PC1); // 发9个时钟 for (uint8_t i = 0; i < 9; i++) { PORTC &= ~(1<<PC0); _delay_us(5); PORTC |= (1<<PC0); _delay_us(5); } // 发STOP条件:SCL高时SDA由低变高 PORTC &= ~(1<<PC1); _delay_us(5); PORTC |= (1<<PC0); _delay_us(5); PORTC |= (1<<PC1); _delay_us(5); // 恢复TWI twi_init(); }这段代码在每次通信失败后调用一次,实测能解决绝大多数锁死问题。注意_delay_us需要包含<util/delay.h>并定义F_CPU。
5.2 数据校验与滤波
温度数据虽然变化慢,但偶尔也会因为干扰出现野值。我通常做两级处理:第一级是范围校验,如果读数超出-40℃到125℃的合理范围,直接丢弃;第二级是滑动平均,用最近4次或8次采样做均值滤波。对于远程温度,还可以加一个变化率限制,如果两次采样之间变化超过5℃,认为是干扰,用上次的值代替。
#define FILTER_LEN 8 static float temp_buf[FILTER_LEN]; static uint8_t buf_idx = 0; float filter_temp(float new_val) { temp_buf[buf_idx] = new_val; buf_idx = (buf_idx + 1) % FILTER_LEN; float sum = 0; for (uint8_t i = 0; i < FILTER_LEN; i++) { sum += temp_buf[i]; } return sum / FILTER_LEN; }滑动平均会引入一点滞后,但对于HVAC这种时间常数几十秒的系统,完全无感。
5.3 通信失败的重试策略
不是每次读失败都要立刻恢复总线。我的做法是:连续失败3次才触发总线恢复,恢复后重新初始化传感器。如果恢复后还是失败,就上报故障,让上层逻辑决定是否降级运行(比如用本地温度代替远程温度,或者切换到固定占空比模式)。这样既不会因为偶发干扰频繁复位,又能在真正故障时及时响应。
6. 本地与远程温度的协同应用逻辑
6.1 本地温度在板级热管理中的角色
本地温度虽然不直接参与房间温度控制,但它是系统健康度的重要指标。比如控制板安装在风机盘管附近,夏季制冷时本地温度应该在30℃到45℃之间,如果读到60℃以上,说明要么是功率器件异常发热,要么是风道堵塞导致散热不良。我一般会设两级阈值:超过55℃降额运行(比如降低风机转速或减少压缩机负载),超过75℃直接停机保护。
另外,本地温度还可以用来补偿远程温度的冷端误差。虽然PJ85718DM内部已经做了冷端补偿,但如果本地温度和远程温度差异很大(比如超过20℃),说明传感器所处环境有局部热源,这时候可以加一个经验补偿系数,让远程读数更接近真实值。
6.2 远程温度参与控制时的滤波与响应
远程温度是控制回路的核心输入,但它的噪声也最大,因为探头在远端,走线长,容易受电磁干扰。除了前面的滑动平均,我还会在PID入口前加一个一阶低通滤波,时间常数根据系统热惯性来定。比如房间温度控制,时间常数取30秒到60秒;风管温度控制,取5秒到10秒。
// 一阶低通滤波 float lpf_temp(float new_val, float old_val, float alpha) { return old_val + alpha * (new_val - old_val); } // alpha = dt / (dt + tau),dt是采样周期,tau是时间常数这个滤波器的好处是计算量小,参数物理意义明确,调起来很直观。
6.3 双通道数据的融合与故障切换
正常运行时,控制逻辑用远程温度;当远程通道故障(比如三极管开路、短路、读数超范围)时,自动切换到本地温度,同时上报故障码。切换逻辑要加迟滞,避免在故障边缘反复切换。我一般设:远程故障持续5秒才切换,恢复后持续10秒正常才切回。
| 场景 | 本地温度 | 远程温度 | 控制策略 |
|---|---|---|---|
| 正常运行 | 监测 | 参与控制 | PID用远程值 |
| 远程故障 | 监测 | 无效 | 降级用本地值 |
| 本地过温 | 保护 | 参与控制 | 降额或停机 |
| 双通道故障 | 无效 | 无效 | 固定输出+报警 |
7. 实测中踩过的坑与排查思路
7.1 读数一直是0xFFFF或0x0000
第一次调试时,读出来的温度寄存器全是0xFFFF,一开始以为是传感器坏了,换了一颗还是一样。后来用逻辑分析仪抓I2C波形,发现地址发出去后从机根本没有ACK。排查下来是两个问题:一是上拉电阻没焊,二是设备地址搞错了。PJ85718DM的地址可能由ADDR引脚决定,悬空、接地、接VCC对应不同地址,手册里一般会列一个表。把ADDR引脚按需求接好,上拉电阻补上,立刻就读到了正常数据。
提示:遇到I2C读不到数据,先抓波形看ACK位,比盲猜地址高效得多。
7.2 远程温度比实际高十几度
这个问题困扰了我一个下午。远程三极管接的是MMBT3904,按理说精度应该不错,但读数一直偏高。后来发现是三极管的基极和集电极没有短接,我直接把基极当正端、发射极当负端用了。正确的接法是把基极和集电极焊在一起,再接到D+。改过来之后,读数立刻正常。这个坑很隐蔽,因为不短接也能读出数据,只是数值完全不对。
7.3 温度值每隔几秒跳变一次
板子装在风机旁边,远程温度每隔几秒跳2℃到3℃。用示波器看三极管两端的电压,发现上面叠加了几十毫伏的高频噪声,明显是风机电机换向产生的干扰。解决办法是在三极管两端并一个100nF电容,靠近传感器端串两个100Ω电阻,同时把走线改成双绞线。改完之后跳变降到0.2℃以内。
7.4 长时间运行后总线锁死
产品连续跑了一个星期后,偶尔出现温度不再更新。加了前面说的超时和总线恢复机制后,再没出现过。这里要强调:任何I2C通信都必须加超时,不能依赖从机永远正常。工业现场电磁环境复杂,从机被干扰到状态机跑飞是常有的事。
7.5 校准时的参考温度不准
用冰水混合物做0℃参考时,如果冰太少或水太多,实际温度可能偏离0℃。正确做法是:用碎冰和少量水混合成糊状,插入探头后搅拌,等稳定后再读数。100℃参考要注意当地气压,海拔高的地方沸点低于100℃,需要查表修正。如果条件允许,用经过计量的恒温槽最省事。
8. 从原型到产品的几点经验
8.1 PCB布局的优先级
温度采集板的布局,优先级最高的是传感器远离热源和模拟走线短而直。PJ85718DM尽量放在板边,远离稳压器、MCU、功率驱动。远程三极管的差分走线要等长,下面铺地平面,但不要铺到传感器正下方,避免引入板级热梯度。去耦电容紧贴电源引脚,地回路尽量短。
8.2 出厂校准的流程设计
如果产品精度要求高,建议每台出厂前做一次单点校准。产线用一个恒温槽稳定在25℃,等温度稳定后,把当前读数与标准值比较,把偏差写入EEPROM。ATmega644P有2KB EEPROM,存几个校准系数绰绰有余。校准系数读取时要做校验和,防止EEPROM位翻转导致错误补偿。
8.3 长期运行的漂移监测
三极管测温的长期漂移很小,但焊点老化、探头受潮等因素仍可能导致缓慢偏移。我一般会在固件里加一个漂移监测:如果本地温度和远程温度的差值在设备稳定运行后持续增大,超过历史均值一定范围,就上报维护提醒。这样可以在精度明显劣化前提前干预。
8.4 代码的可移植性考虑
虽然这套代码是给ATmega644P写的,但TWI读写和温度换算的逻辑可以平移到其他MCU。建议把硬件相关部分(TWI底层、延时)和业务逻辑(温度换算、滤波、控制)分开,用函数指针或宏做抽象。这样换平台时只需要改底层,上层逻辑不用动。我在几个项目之间复用这套结构,移植时间从一天缩短到一两个小时。
最后分享一个我常用的调试技巧:在开发阶段,把本地温度、远程温度、原始寄存器值、总线错误计数全部通过串口打印出来,格式化成一行CSV。用串口工具记录成文件,导入表格软件画曲线,一眼就能看出是噪声、漂移还是逻辑错误。这个习惯帮我省下了大量猜测的时间。