1. 项目背景与整体设计思路
最近在调一套嵌入式控制柜的温度监测功能,被要求同时读出“本地”和“远程”两个测温点,然后交给 TM4C129XNCZAD 主控做显示、上报和设备联动。传感器最终选的是 PJ85718DM。很多人第一次看到这个编号会有点陌生,它不是 DS18B20 那种一根线走天下的器件,而是一类把本地测温与远端测温集成在一起的数字温度调理芯片,通信接口走 I2C,一颗芯片就能同时管两个位置:本地通道测控制板附近的环境温度,远程通道通过外接的二极管或专用探头去测风道、水管、电机轴承这些离控制器比较远的位置。
在嵌入式和 HVAC 这两个场景里,“一芯双测”的价值非常明显。嵌入式系统要盯的是板卡本身会不会过热,比如功率器件旁边、电源模块附近、CPU 散热区域,这些位置放感温元件是最合理的,本地通道正好直接贴装在芯片内部或者靠近这些区域;而 HVAC 要监测的往往是设备内部空气温度、风盘出风温度、水系统回水温度,这些点一般很难放到控制板上,必须通过线缆引出去。如果用两颗独立传感器来做,除了多占一个 I2C 地址、多一份布线和物料成本之外,更麻烦的是两颗芯片的温漂和精度不完全一致,软件里还要分别校准。PJ85718DM 这类方案把一路测温做在片内,另一路用半导体结测温,两路读数在同一个寄存器模型下输出,校准和换算都方便很多。
主控选 TM4C129XNCZAD,我主要看中了三点。首先它是 Cortex-M4F 内核,带浮点运算单元,处理温度换算、滤波和 PID 控制一气呵成,完全不用像 Cortex-M0 那样小心翼翼做定点乘除。第二点是这颗芯片本身就带以太网 MAC 和 PHY,温度数据可以直接通过网口上报,在工业现场这种需求几乎是标配,省掉一颗外部网络控制器,BOM 和 PCB 面积都友好。第三点是它的 I2C、UART、CAN 外设数量充足,PJ85718DM 只是挂在 I2C 总线上的一个从设备,以后想挂更多传感器,或者扩展触摸屏、RS485 网关,引脚资源和外设资源都不会马上捉襟见肘。
整个系统的信息流其实不复杂。PJ85718DM 在本地和远程两个通道各输出一个温度寄存器数据,TM4C129XNCZAD 通过 I2C 总线把原始数字量读回来,换算成摄氏度,再做一次滑动滤波,存入内存里的温度状态结构体。显示任务、网络任务、控制任务各取所需:显示任务刷新 LCD 和串口日志,网络任务通过 HTTP 页面或 Modbus 协议把温度送出去,控制任务比较当前温度和设定阈值,输出 PWM 给风扇调速,或者驱动继电器给空调机组下指令。这个架构不需要跑操作系统,用定时器中断加一个简单的状态机就能撑起来,但代码必须按模块拆开,不然以后加功能、换传感器都要推倒重来。
1.1 为什么“本地”和“远程”要分开建模
很多第一次做温控的工程师习惯把所有温度都当成“环境温度”来读,这是后面系统不稳定的一大来源。本地和远程在物理意义和响应速度上完全不同。本地温度传感器紧贴控制板,MCU、电源、功率器件发热都会直接影响它,它更像是一个“设备最坏工况”的代言人;远程温度则反映实际被控对象,比如风道内空气温度或者盘管水温,这个量变化慢,惯性大,但真正决定舒适度和设备节能效果的是它。把两个量分开存、分开滤波、分开设阈值,控制策略才能有层次。比如远程温度已经降到设定值附近,但本地温度还很高,说明控制板散热不好,这种异常在通道混在一组的系统里很难早期发现。
1.2 温度调理芯片与 MCU 的分工边界
在最后定方案时,我特意把“采集”和“决策”两层拆开。PJ85718DM 负责最底层的电信号处理,把热敏元件或者半导体结的微小电压变化转换成数字量;TM4C129XNCZAD 只负责读寄存器、做决策、对外通信。这两个层级不要混在一起。有人偷懒用 MCU 的 ADC 直接采样 NTC,也不是不行,但硬件电路里要有精确参考电压、滤波电容和校准电阻,温漂问题会消耗大量调试时间。用数字温度调理芯片,这些问题在出厂前就被厂商处理掉一大半,嵌入式工程师的精力可以放在控制逻辑和设备联调上。
2. 硬件连接与关键电路设计
PJ85718DM 与 TM4C129XNCZAD 的连接核心是 I2C 总线。标准接法很简单:芯片的 VCC 接 3.3V 或 5V(看具体手册,我这边用的 3.3V),GND 共地,SDA 和 SCL 两根线分别挂一个上拉电阻到 VCC,然后接主控对应的 I2C 引脚。这里有三个容易踩的坑值得单独说。
第一个坑是上拉电阻值。I2C 总线上的上拉电阻不能用 MCU 内部弱上拉直接替代。内部上拉通常有几十 kΩ,如果线缆稍微长一点,信号上升沿变得很平缓,总线通信就会偶发失败。常规做法是在 SDA、SCL 上各接 4.7kΩ 到 3.3V。如果总线上挂的从设备比较多,或者远程探头线特别长,可以换成 2.2kΩ,但注意电阻太小会增大低电平输出电流,反而可能把主控、传感器这两端的低电平电压抬高,导致逻辑判断错误。稳妥做法:先用 4.7kΩ,如果用示波器看到上升沿太缓,再换 2.2kΩ。
第二个坑是地址冲突。PJ85718DM 这类 I2C 温度芯片通常有地址选择引脚,比如把某个地址引脚接 VCC 或 GND 来切换低位地址。如果你的系统里已经有其他 I2C 设备,比如 EEPROM、RTC,一定要先扫一遍,确认 7 位地址不冲突。最简单的办法是在初始化阶段写一个 I2C 扫描函数,逐个地址发送起始位并等待 ACK,把总线上所有从设备地址列出来。
第三个坑是远程通道的走线。PJ85718DM 的远程测温通道,本质上是通过测量外接二级管或三极管的 PN 结压降来推算出结温,精度很容易受到线阻、寄生电容和噪声干扰。我的做法是把远程探头用双绞线引出,长度控制在 1 米以内,并且在芯片端的远程通道引脚附近加一颗 100 nF 到 1 nF 的滤波电容到地,来吸收高频干扰。需要注意的是,这个电容不能太大,否则远程通道的测量建立时间会变长,你读到的温度变化会明显滞后又慢半拍。
2.1 参考引脚连接示例
| 功能 | 芯片引脚 | 主控/电源 | 说明 |
|---|---|---|---|
| 电源 | VCC | 3.3V | 与 MCU 同一电源域最好 |
| 地 | GND | GND | 远端探头接地要可靠 |
| SCL | I2C 时钟 | TM4C129XNCZAD I2C0SCL | 接 4.7kΩ 上拉 |
| SDA | I2C 数据 | TM4C129XNCZAD I2C0SDA | 接 4.7kΩ 上拉 |
| 地址选择 | A0/A1 | 接 3.3V 或 GND | 以实际手册为准 |
| 远程探头 | D+ / D- | 双绞线引出 | 差分线对,加滤波电容 |
TM4C129XNCZAD 的 I2C 引脚并不是随便挑两个 GPIO 就能用,需要看它的引脚复用表。我这边用的是 I2C0 模块,SCL 在 PA2,SDA 在 PA3。如果你在 PCB 上已经把这些引脚复用成串口或者其他功能,就要换一组,比如 I2C1 的 PB2、PB3,或者 I2C2 的 PE4、PE5。总之,先查完整引脚复用表再定原理图,别看到一个 I2C0 就直接接,最后固件怎么配都配不出来。
2.2 电源和地线的抗干扰设计
在 HVAC 设备里,风机、水泵、压缩机启动瞬间拉低的母线电压和串入的地线噪声,会直接影响温度传感器的读数。温度调理芯片虽然内部有滤波和 AD 转换,但地线噪声一旦大到超过其输入范围,读数会直接跳出正常区间。我在控制柜里尽量把温度芯片的 GND 和主控的 GND 用星形方式汇到电源地端子,避免让功率回路的强电流经过温度芯片周围的地平面。另外,给芯片的 VCC 加 100 nF 和 10 μF 双电容组合,前者滤高频,后者吸收低频波动。在风机驱动的 PWM 信号线上,也必须与 I2C 数据线保持足够距离,或者中间隔一层完整地平面,否则 PWM 边沿跳变会通过分布电容耦合到 SDA、SCL 上。
3. 固件实现:传感器驱动与主控逻辑
先明确一个背景:我用的 PJ85718DM 芯片遵循常见的 I2C 温度传感器寄存器模型,但不同批次、不同供应商的寄存器地址可能有一点差异。在写固件之前,一定要拿到对应数据手册,确认通道选择寄存器、温度寄存器地址、精度位数和符号位格式。这里以典型的 12 位温度数据格式来写示例代码,思路可以直接套用到大部分 I2C 温度芯片上。
3.1 TivaWare 下的 I2C 初始化
TM4C129XNCZAD 的固件开发,我习惯直接基于官方外设驱动库,也就是 TivaWare。初始化 I2C 的代码不复杂,但要保证时钟、GPIO 复用、外设使能这几步顺序正确。
#include <stdint.h> #include "inc/hw_memmap.h" #include "inc/hw_types.h" #include "driverlib/sysctl.h" #include "driverlib/gpio.h" #include "driverlib/pin_map.h" #include "driverlib/i2c.h" void I2C0_Init(void) { uint32_t clock = SysCtlClockGet(); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); SysCtlPeripheralEnable(SYSCTL_PERIPH_I2C0); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_I2C0)); GPIOPinConfigure(GPIO_PA2_I2C0SCL); GPIOPinConfigure(GPIO_PA3_I2C0SDA); GPIOPinTypeI2C(GPIO_PORTA_BASE, GPIO_PIN_2); GPIOPinTypeI2C(GPIO_PORTA_BASE, GPIO_PIN_3); I2CMasterInitExpClk(I2C0_BASE, clock, 100000); }我这边把 I2C 时钟设为 100kHz。温度采集本身不需要高带宽,100kHz 比 400kHz 对长线缆和较弱的上下拉更容易适应。实际测试中,400kHz 在短线缆也能跑,但稍微接上远程探头,误码率就开始上升。对于温度这种慢变量,没有必要追高速,通信稳定优先。
3.2 读取温度寄存器并换算
典型的寄存器读取流程是:主控先发送寄存器地址,然后连续读取两个字节(高字节、低字节),把 16 位原始数据取出,再根据精度位右移或掩码,最后乘上分辨率系数。我写了一个独立函数,只负责读一个通道的温度原始值。
int16_t ReadTempRaw(uint8_t devAddr, uint8_t regAddr) { int16_t msb = 0, lsb = 0; uint8_t data = 0; I2CMasterSlaveAddrSet(I2C0_BASE, devAddr, false); I2CMasterDataPut(I2C0_BASE, regAddr); I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_SINGLE_SEND); while(I2CMasterBusy(I2C0_BASE)); I2CMasterSlaveAddrSet(I2C0_BASE, devAddr, true); I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_RECEIVE_START); while(I2CMasterBusy(I2C0_BASE)); data = I2CMasterDataGet(I2C0_BASE); msb = data; I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_RECEIVE_CONT); while(I2CMasterBusy(I2C0_BASE)); data = I2CMasterDataGet(I2C0_BASE); lsb = data; I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_RECEIVE_FINISH); while(I2CMasterBusy(I2C0_BASE)); data = I2CMasterDataGet(I2C0_BASE); lsb = data; return (int16_t)((msb << 8) | lsb); }读回原始值之后,下一步是换算成摄氏度。以 12 位分辨率、分辨率为 0.0625℃ 为例,温度数据一般只取 16 位中的高 12 位,也就是把整体右移 4 位,然后再乘 0.0625。注意符号位,温度低于 0℃ 时符号位扩展要处理对,否则会读出 65400 这类离谱值。
float ConvertTemp(int16_t raw) { float temp; temp = (float)(raw >> 4) * 0.0625f; return temp; }这里有一个容易被忽视的细节:如果传感器数据是 10 位或者 11 位,右移位数要相应调整。假设某颗芯片是 11 位分辨率,数据只占 16 位中的高 11 位,那就要右移 5 位,分辨率变成 0.125℃。拿到手册后,第一件事就是确认数据格式,我吃过一次亏,用 0.0625 去套 11 位数据,温度在室温附近看起来差别不大,到了 85℃ 以上能差出好几度,单看寄存器根本发现不了问题。
3.3 本地通道与远程通道的轮询调度
本地与远程两个通道不能同时读取,一般通过寄存器选择通道。我在主循环里用了一个简单的计数器,每 100ms 轮换读一次通道,保证两路数据不会被同一个 I2C 传输周期卡死。更平滑的做法是给每个通道各分配一个软件定时器,各自定时读取,中间插入其他任务。不过对于温度采集这种慢系统,100ms 的轮询完全足够,而且实现简单。
读到原始温度后,我还会做一阶低通滤波。直接读取的值会有随机噪声,比如远程通道受电源波动影响,数值得跳来跳去。用滤波以后,曲线会平滑很多,但也必须接受温度响应变慢的现实。滤波系数可以这样选:新值占 0.3,旧值占 0.7,室温环境下测试,稳定读数大约几秒钟跟手,控制风扇不会因为一次噪声就误动作。
float filteredLocal = 0.0f; float filteredRemote = 0.0f; uint8_t firstRead = 1; const float alpha = 0.3f; void UpdateTemperatureFilter(void) { float rawLocal = ConvertTemp(ReadTempRaw(PJ85718DM_ADDR, REG_LOCAL_TEMP)); float rawRemote = ConvertTemp(ReadTempRaw(PJ85718DM_ADDR, REG_REMOTE_TEMP)); if(firstRead) { filteredLocal = rawLocal; filteredRemote = rawRemote; firstRead = 0; } else { filteredLocal = alpha * rawLocal + (1.0f - alpha) * filteredLocal; filteredRemote = alpha * rawRemote + (1.0f - alpha) * filteredRemote; } }需要注意,首次读到的原始值不能直接作为滤波初值,否则上电瞬间如果读到一次脉冲尖峰,后面相当一段时间内滤波值都被带偏。我在第一次读取时直接把原值赋给滤波结果,不进滤波器,这就是上面 firstRead 标志的作用。
4. 数据显示与 HVAC 控制联动
温度数据读出来之后,不能只停留在串口打印。在一个真正的嵌入式或 HVAC 项目里,要让现场人员看得见、让系统自己会动作,必须把显示和联动控制一起做起来。
4.1 串口与 LCD 的现场显示
调试阶段,我用 UART 把温度数据打成一行行文本输出到串口助手。固定格式如下:
[2025-02-01 10:30:15] LOCAL=23.5C REMOTE=45.2C PWM=65%这里的本地温度如果贴着控制板,必然比真正的环境温度高几度,因为主控和电源电路本身在发热。看到这个值不要害怕,这正是需要监测的意义。我在 LCD 上显示的时候会额外标一个“本机”和“远端”的标签,提醒现场人员理解两个通道的含义,避免把本地温度当成室温来读。
4.2 通过以太网输出温度数据
TM4C129XNCZAD 的以太网能力是这个方案里比较出彩的部分。由于芯片内部已经集成了以太网 MAC 和 PHY,固件里直接初始化网络接口,实现一个简单的最小 HTTP 服务器即可。当浏览器访问设备 IP 时,返回一段 JSON 数据,比如:
{ "device": "temp-monitor", "local_temp": 23.5, "remote_temp": 45.2, "pwm": 65 }这个接口可以极大方便现场调试。我在实际项目里甚至会先把温度数据用浏览器拉出来看,一边调整风机位置,一边观察远程通道读数变化,比守着串口线舒服太多。如果未来需要接入上位机或者物联网平台,只需要在现有 HTTP 服务器上再加一个 POST 请求处理逻辑,把 JSON 数据推送到远端即可。
4.3 阈值控制与 PID 调速
HVAC 场景里,远程温度通常是需要被控的对象。比如回风温度到了 30℃,就要加大风机转速或者打开冷水阀;到了 24℃,就要减小功率或者关闭阀门。最怕的是继电器在阈值附近反复跳,空调、风机厂的人最反感这种控制系统。解决办法是设置回差(死区),假设目标温度是 26℃,上限设为 27℃,下限设为 25℃,只有超过上限或低于下限才动作,在中间范围内保持现状。
如果调速细度要求更高,可以用 PWM 驱动风机,而不是简单的开/关。PWM 占空比可以通过 PID 控制器计算。我这边只用一个比例控制都能解决大多数情况:
int16_t error = targetTemp - (int16_t)filteredRemote; int16_t pwm = 50 + (error * 3); if(pwm < 20) pwm = 20; if(pwm > 100) pwm = 100;比例系数不能拍脑袋填。可以把系统实测一遍:远程温度稳定在某个值,手动给一个步进 PWM,看温度变化多少度,再算出响应系数。如果不做实测直接把系数调大,风机会满速运转,温度上下震荡,某些设备会把压缩机给搞坏。工业控制里有一句话,控制不稳的时候先回看采样滤波是否可靠,再怀疑 PID 参数,这句话在温度系统里尤其成立。
5. 调试过程与常见问题排查
这部分内容,我认为比前面的设计更值得花时间写。因为我调试这类温度芯片时踩过的坑,十个里面至少有七个不是芯片本身的问题,而是总线和电源的问题。
5.1 传感器一直读不到数据
表现是 I2C 读取超时,设备没有响应 ACK,或者读回固定值 0xFF。先不要怀疑芯片坏了,按下面顺序排查。
先用万用表量 VCC 引脚有没有 3.3V。很多传感器模块在 5V 和 3.3V 电压下行为完全不一样,供电不对,内部电路不工作。再量 GND 和 VCC 之间的压差,如果压差不对,说明板子有虚焊或者线缆断芯。最重要的是确认主控的 I2C 引脚是否真的复用成 I2C 功能。有些工程师在初始化时忘了调用 GPIOPinConfigure,导致引脚停留在 GPIO 模式,I2C 波形自然出不来。用示波器抓 SCL 有没有时钟信号,如果没有,检查初始化函数是否真的被执行,或者被某个条件分支跳过。
再有一个常见原因是地址不对。我把芯片地址写死为某个值后,连续几十次都读不到。最后用扫描脚本扫出真实地址后发现,芯片地址引脚默认钳高,地址与手册默认值不同。建议所有 I2C 项目第一步都做地址扫描,不要直接套手册默认值。
5.2 温度值上下跳变严重
如果读到的温度在正常值附近剧烈跳动,先看 I2C 数据线是否离 PWM 线太近。我遇到过数据线从风扇驱动 MOS 管旁边穿过的情况,MOS 开关瞬间的 dv/dt 耦合到 SDA 线上,导致读回的数据位翻转。把 I2C 走线调整远离功率线,并且在芯片电源引脚上加滤波电容后,现象消失。另外,远程探头线过长或者用了非屏蔽线,也会让远程通道读数跳动。建议改用双绞线,并且缩短距离,同时在远程通道芯片端并联电容。电容值不能太大,否则温度响应会很慢,我一般从 100 pF 开始试,最多到 1 nF。
还有一个容易被忽略的:传感器芯片和主控共地。如果远程探头在另外一个设备上,两边电源系统必须真的共地,否则远程通道电压参考点漂移,读数自然会乱。
5.3 校准结果始终偏差大
数字温度传感器出厂精度通常是够用的,但每次换探头、换线缆、换安装位置,都会引入新的系统误差。我习惯用一个已知可靠的参考温度计来对比校准。先把远程探头和参考温度计放在同一个恒温环境中,静置二十分钟,将两者读数差值记录下来。如果差值比较稳定,比如始终比参考温度计低 0.4℃,那就在软件里加一个固定偏移量。如果差值随温度变化而变化,就不是简单偏移,要考虑是不是探头线阻过大或传感器型号不匹配。HVAC 系统中,铝箔贴在管道表面时,由于接触热阻和辐射热的影响,读数比真实空气温度高或低都正常,测量位置一定要选在气流充分混合、无遮挡、无外界热辐射的区域。
5.4 一个实用的 I2C 扫描代码片段
这里附上一个简单的 I2C 地址扫描函数,用于上电自检时快速确认总线上有哪些设备。
void I2C_Scan(uint32_t base) { uint32_t addr; for(addr = 0x08; addr < 0x78; addr++) { I2CMasterSlaveAddrSet(base, addr, false); I2CMasterControl(base, I2C_MASTER_CMD_SINGLE_SEND); while(I2CMasterBusy(base)); if(!(HWREG(base + I2C_O_MCS) & I2C_MCS_ERROR)) { UARTprintf("I2C device found at 0x%02X\n", addr); } I2CMasterControl(base, I2C_MASTER_CMD_BUS_BUSY); } }这个扫描函数的原理很简单:给每个可能的地址发送一个起始位和从地址,如果收到 ACK,说明这个地址上有设备响应;如果收到 NACK,则跳过。注意扫描之前要确保 I2C 总线没有别的主设备占用,否则总线状态一直忙,扫描结果会失真。
6. 经验总结与后续扩展思路
这个项目做完之后,有几个心得对我后面做其他控制项目帮助很大。首先,温度采集系统的软件优先级应该是一稳二准三快。先把读数稳定住,滤波、接地、屏蔽都做好,再去谈精度校准,最后才去追求响应速度。这个顺序如果反过来,你会花非常多时间去分辨噪声到底是干扰还是真实温度波动。
其次,所有 I2C 程序都要预留超时和错误重试机制。I2C 总线的错误并不少见,特别是工业设备长期运行后,连接器氧化、接头松动都会导致偶发总线挂死。如果主控一直卡在 while 等待总线空闲,整机看起来就是死机。我的做法是给每次 I2C 访问加一个超时计数,比如连续重试三次失败就认为传感器故障,并把温度状态标记为无效,控制任务改用安全策略,例如强制风扇低速运行,而不是满速或者停转。
另外,代码结构值得多做一层抽象。虽然当前用的是 PJ85718DM 和 TM4C129XNCZAD,但采购断货之后可能要换兼容芯片。我在固件里把温度读取写成一个独立驱动文件,对外提供 GetLocalTemp 和 GetRemoteTemp 两个接口,主控逻辑只调用接口,不直接操作 I2C 寄存器。这样换芯片时只需改驱动文件,应用层基本不用动。事实证明,这个抽象在后期带来巨大帮助,因为同系列兼容芯片的寄存器地址居然不完全一样。
最后分享一个小技巧,调试温度系统时一定要留出足够长的“老化观察期”。某些温度传感器在上电初期的读数会缓慢漂移,特别是远程通道和外接探头刚接触时,热阻需要一定时间才能达到平衡。不要一上电就看到一个稳定值就赶紧写结论。我在做了三次对比实验之后才发现,真正贴合 HVAC 应用场景的做法是:上电后让系统空转五分钟,再做自动偏差校准,之后启动控制逻辑。整个过程写出来并不复杂,但实际调通并稳定运行,前前后后花了接近两个工作日。嵌入式系统最大的特点就是细节堆叠,温度监测这种看似简单的功能,真正做好同样需要从元器件选型、电路布局、固件架构到现场联调全面兼顾。