最近在鼓捣一套基于 PJ85718DM 和 STM32F446ZE 的 HVAC 温度监测单元,需要同时盯住主控板本地温度和远端风管里的几个温度点。PJ85718DM 是颗很常见的 I2C 数字温度传感器,引脚和寄存器布局跟 LM75 系高度兼容,一颗贴片封装直接焊在 PCB 上,数据手册标称精度在 ±1°C 左右,自带过温报警输出引脚。STM32F446ZE 则是主频 180MHz 的 Cortex-M4F 芯片,三个 I2C 外设加一个快速模式 FMPI2C,资源对我来说非常充裕。这个组合在嵌入式和暖通控制器里都挺典型,尤其是远程温度点上,一条 I2C 总线靠地址区分,最多能挂 8 颗,MCU 端只需要留两个普通 I2C 外设。下面把这套方案的选型逻辑、硬件细节、驱动代码和调试过程都展开说说,给准备做温度采集或设备监控的朋友一个可以直接抄作业的参考。
1. 项目全景与方案拆解
1.1 为什么用数字 I2C 传感器而不是 NTC 或模拟输出芯片
PJ85718DM 这类数字温度传感器,本质是把小型测温元件、ADC、寄存器、I2C 接口逻辑封装在一颗芯片里。温度信号在芯片内部完成模拟到数字转换,MCU 通过 I2C 直接读取二进制结果。对比常见的 NTC 热敏电阻,它省掉了分压电阻网络、校准表和每个通道的 ADC 采样占用;对比 LM35 / PT100 这类模拟输出方案,它把增益放大和线性修正全部在芯片内做完,信号以数字形式传输,板上的电磁干扰很难直接叠加进温度值里。
在 HVAC 控制面板的实体项目里,一个温度点往往对应一个被测位置(回风口、送风管、水箱表面、室外机附近),如果每路都上 NTC,采集板每个通道都要配限流电阻和滤波电容,PCB 面积和 BOM 成本都不小。换成数字传感器后,所有传感器共用一组 SDA/SCL 走线,只要地址不同就能在一条总线上并存,布线简洁非常多。另一个隐藏优势是自诊断能力:I2C 设备会应答,设备掉线、线缆断线、芯片失效都能被 MCU 立刻感知,而 NTC 断线往往只能靠读值离谱来猜,故障判断效率差一大截。
1.2 本地与远程的温度监测架构差异
本地温度指 MCU 所在控制板的环境温度,通常用来做机柜过热保护、功率器件温升监测。PJ85718DM 贴到 PCB 上,跟 MCU、功率管共板,读取的就是铜箔和气流混合后的板温。远程温度指分布在现场线缆远端(风管内、水管表面、室外机等)的温度,必须通过端子排和线缆延伸出去。
我在这个模拟项目里把总线拆成两路,而不是全部串一条:I2C1 跑本地传感器,速率 400kHz,上拉 4.7kΩ,读取周期短;I2C2 跑远程传感器,速率降到 100kHz,原因是远程线缆的电容和现场干扰会明显影响通信。远程节点数量不多时,只需要让每颗 PJ85718DM 的 A0/A1/A2 引脚组合出唯一地址。本地那颗设置成 0x48,远程节点分别设成 0x49、0x4A……以此类推,一条总线最多 8 个地址(0x48~0x4F),一个系统最多可以接入 9 个温度点,如果本地这颗也算进去,那就是两组总线共 9 颗芯片。
为什么要拆两条总线而不是全部串在一条上?主要为了故障隔离和抗干扰。远程线缆一旦断线或者短路,最多让 I2C2 挂住,本地传感器还能继续工作,故障定位也更快。实际调试中这个决策救过我一次:远程线被老鼠咬断,本地过热保护逻辑仍然正常运行,面板上只报了“远程传感器故障”,而不是整机陷入不确定状态。
2. 硬件设计三项硬骨头:地址设置、上拉电阻、远程线缆
2.1 PJ85718DM 引脚、I2C 地址和寄存器透视
硬件上 PJ85718DM 的引脚不复杂:VDD、GND、SDA、SCL、A0、A1、A2、OS。供电 3.3V 即可,SDA 和 SCL 是开漏输出,必须外部上拉。OS 是过温报警输出,可以配置成比较器模式或者中断模式,MCU 端接一个 GPIO 输入读取,能用来做紧急过热保护。A0/A1/A2 是地址选择脚,I2C 7 位地址格式是 1001 A2 A1 A0,三个脚都接低电平时地址为 0x48,全接高是 0x4F,总共 8 个可分配地址。把这三只脚接到 GPIO 或者拨码开关上,就可以在硬件层面完成远程节点的地址分配,逻辑清晰不容易出错。
再聊寄存器。温度寄存器在 0x00,读取一次返回两个字节,是 12 位有效数据的原始结果。配置寄存器在 0x01,控制 OS 引脚的工作模式、故障队列数量和转换精度。上电默认一般工作在 12 位模式,对应 LSB 是 0.0625°C,这是这个项目最常用的配置,不需要每次上电都去改写配置寄存器,只要软件初始化时校验一次就好。另外还有 Thyst 和 Tos 两个阈值寄存器,分别是迟滞阈值和过温触发阈值,在 HVAC 启停控制中很实用,后面第五节再展开说。
换算逻辑给一个具体例子:某次读取温度寄存器得到 0x1A 0xE0,拼成 16 位数 0x1AE0,右移 3 位得到 0x035C,也就是十进制的 860,乘以 0.0625,结果是 53.75°C。整个换算不需要查表,不需要浮点库,MCU 上几行代码就能完成。
2.2 上拉电阻选择和远程线缆工程处理
I2C 的两个信号线是开漏结构,必须靠外部上拉电阻把线拉到高电平。阻值选大选小直接影响通信距离和速度,这是嵌入式工程师最容易忽略的问题。最小上拉电阻由低电平允许的最大灌电流决定:按 3.3V 供电、VOL 不超过 0.4V、灌电流 3mA 计算,(3.3 - 0.4) / 0.003 约等于 967Ω,所以下限就是 1kΩ 左右。
上限则由总线电容和允许的上升时间决定。快速模式 400kHz 要求上升时间小于 300ns,如果总线电容是 100pF,最大上拉约 3.5kΩ;标准模式 100kHz 允许上升时间 1µs,远程线缆加传感器输入电容到 300pF 时,上拉到 10kΩ 都还在范围内。实际操作中我本地总线用 4.7kΩ,远程总线用 3.3kΩ,原因很简单:远程线缆电容本来就大,再用 10kΩ 上拉,上升沿会钝得没法看。更保险的办法是远程线缆末端每一颗传感器附近也放一个 4.7kΩ 上拉,但要注意整条总线并联的上拉电阻不能太多,否则低电平拉不下去,我倒更推荐只在 MCU 模块端和总线远端各放一组,也就是两端上拉方案。
远程线缆选型上,优先用双绞线或者屏蔽线,屏蔽层在 MCU 端单点接地。每个远程传感器模块的 VDD 和 GND 之间必须放 100nF 陶瓷电容,去耦电源纹波。线缆长度在 1 米以内,100kHz 基本稳定;超过 1.5 米,环境里又有变频器或者接触器干扰的话,不要硬扛,加一颗 I2C 总线中继器,或者干脆换用 RS485 接口的数字温度传感器,工程上省心得多。I2C 天生不是为长距离设计的,硬拉长线到十几米还要求稳定,属于和物理规律对着干。
3. STM32F446ZE 驱动代码:初始化、轮询、温度换算一条龙
3.1 I2C 外设初始化与基础读取函数
STM32F446ZE 驱动我用 HAL 库写,工程结构清晰,也方便后期移植到其他芯片。I2C1 做本地总线,400kHz;I2C2 做远程总线,100kHz。初始化代码很常规,这里贴 I2C1 的部分作为示意:
static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }I2C2 的初始化唯一区别是 ClockSpeed 改成 100000,其他完全一致。这里提醒一句:I2C 引脚要确认复用功能映射到哪两个引脚,如果跟调试串口或者其他外设冲突,板子跑了才发现就晚了。
读取温度的函数用 HAL_I2C_Mem_Read,第一个参数是 I2C 句柄,第二个是设备地址左移一位后或上 1 表示读操作,第三个是寄存器地址,后面是读长度和超时时间。注意 HAL 库的地址参数要把 7 位地址左移一位,这是新手最容易踩的坑,地址 0x48 要写成 (0x48 << 1) | 1。
float read_temp(I2C_HandleTypeDef *hi2c, uint8_t addr) { uint8_t buf[2]; int16_t raw; if (HAL_I2C_Mem_Read(hi2c, (addr << 1) | 1, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 2, 100) != HAL_OK) { return TEMP_READ_ERROR; } raw = (int16_t)((buf[0] << 8) | buf[1]); raw >>= 3; /* 12-bit effective */ return raw * 0.0625f; }实际工程里我很少直接返回 float,因为浮点数在 printf 和网络传输上都要额外处理。更推荐用放大 100 倍的定点数:摄氏度乘以 100 后的整数,精度保留到小数点后两位,传输和显示都方便。既然 0.0625°C 正好等于 1/16°C,那换算就成了 raw * 100 / 16,整个写入判断就用整数运算,没有浮点开销。
int16_t read_temp_c100(I2C_HandleTypeDef *hi2c, uint8_t addr) { uint8_t buf[2]; int16_t raw; if (HAL_I2C_Mem_Read(hi2c, (addr << 1) | 1, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 2, 100) != HAL_OK) { return TEMP_READ_ERROR; } raw = (int16_t)((buf[0] << 8) | buf[1]); raw >>= 3; return (int16_t)(raw * 100 / 16); }3.2 多点轮询、在线扫描与滤波实现
远程总线上设备地址段是 0x48~0x4F,上电时先做一次扫描,把所有在线节点找出来。HAL 库提供了 HAL_I2C_IsDeviceReady 函数,发送地址并等待 ACK,返回 HAL_OK 说明这个地址上有设备应答。把有应答的地址存进数组,后面就只轮询在线列表,这样某个节点掉线后,下次扫描能自动把临时故障节点摘出去。
uint8_t scan_remote_devices(uint8_t *list, uint8_t max) { uint8_t n = 0; for (uint8_t addr = 0x48; addr <= 0x4F; addr++) { if (HAL_I2C_IsDeviceReady(&hi2c2, (addr << 1) | 1, 3, 50) == HAL_OK) { list[n++] = addr; if (n >= max) break; } } return n; }轮询节奏要克制。本地传感器我每 200ms 读一次,远程每个节点每 500ms 读一次。HVAC 应用的温度变化极为缓慢,1 秒一次都完全够用,太频繁刷新除了增加总线和 CPU 开销,没有任何实际意义。风管里的温度从 10°C 升到 30°C 需要好几分钟,你 10ms 读一次也改变不了什么物理现实。
滤波我用简单滑动平均,窗口取 8 次。对象是放大 100 倍后的整型温度,也就是 8 个 c100 值相加再除以 8,右移 3 位即可,零浮点、零除法(右移搞定)。滑窗缓冲区和累加值放在同一段代码里,每次新值进来先减去窗口最旧的值,再加上新值,避免每次都全量求和。窗口太大不推荐,8 次已经能把随机干扰压掉不少,再大只会让系统对真实温度变化的响应越来越迟钝。
#define FILTER_N 8 static int32_t sum; static int16_t ring[FILTER_N]; static uint8_t idx; int16_t temp_filtered(int16_t new_sample) { sum -= ring[idx]; ring[idx] = new_sample; sum += new_sample; idx = (idx + 1) & (FILTER_N - 1); return (int16_t)(sum >> 3); }4. HVAC 落地的工程细节:滤波、迟滞、断线检测
4.1 芯片级故障队列与软件防抖
HVAC 设备旁边到处是变频压缩机、接触器和继电器,这些设备在切换瞬间会产生强烈的电磁干扰。I2C 通信偶尔失败一次还不可怕,可怕的是温度值出现瞬时跳变,比如本来 25°C 突然读到 80°C,控制器就会误判过热触发保护,整个空调系统莫名其妙停机。防抖要分三个层次做。
第一层在芯片内部。PJ85718DM 的配置寄存器里有一个故障队列设置,可以让过温报警输出必须连续满足 1/2/4/6 次越限才真正翻转。这个功能对硬件 OS 引脚报警有效,可以消除单次尖峰干扰造成的误触发。第二层在软件滤波,就是上面说的滑窗平均,它能把随机单点异常数据抹平。第三层在应用逻辑,对滤波后的值再设置一个变化率限制,比如相邻两次合格采样之间温度跳变超过 10°C,直接丢弃第二次采样,等连续三个稳定值再恢复输出。
软件防抖的细节在于要区分“真跳变”和“假跳变”。真跳变比如坏件发热、管道堵塞导致温度骤升,这时候如果变化率限制设得太小,反而会拖慢保护响应。我这边做法是变化率限制只用来剔除异常值,不用于阻断报警,报警永远基于已经滤波完成的最新值判断,宁可滤掉几个坏点,也必须有最终一击触发的通道。
4.2 过温阈值、迟滞区间与传感器故障识别
HVAC 控制中,温度迟滞是必须做的。如果只设一个 28°C 的过温点,温度升到 28°C 开机,降到 28°C 以下就停机,那么系统会在临界点附近反复震荡,接触器和压缩机都能给你抖出火花来。正确做法是设两个阈值:触发阈值 Tos 设为 28°C,恢复阈值 Thyst 设为 24°C。温度升高到 28°C 启动风机,降到 24°C 才停止,中间这 4°C 就是迟滞死区。
这两个阈值可以写进 PJ85718DM 的 Tos 和 Thyst 寄存器里,由硬件 OS 引脚直接输出触发信号,也可以放在 MCU 端做软件逻辑。我的经验是都保留:芯片的 OS 引脚用于最紧急的硬件保护,比如过温直接拉掉加热器电源;MCU 端的软件阈值用于可调节的舒适度控制,比如动态改变目标温度。因为软件阈值可以通过人机界面随时修改,而芯片寄存器每次上电最好做一次初始化,两边分工明确。
传感器断线检测在数字温度方案里非常顺手。每次 I2C 读取如果返回超时,并不是立即判定故障,可能是干扰导致的偶发失败,我连续失败 3 次才把该节点标记为“传感器故障”,面板上报出具体是哪个地址掉线。温度数据合理性校验也放进同一个判断逻辑:HVAC 场景下正常工作温度范围是 -20°C 到 85°C,如果读回的值换算后超出这个范围,同样判为数据异常,不参与控制逻辑运算。这一招在风管温度传感器进水、短路时特别管用,系统能立刻隔离坏值,而不是用 125°C 的假数据把压缩机保护触发一遍。
5. 实测定标与问题排查实录
5.1 调试顺序与现场实施流程
调试这套双总线温度系统,我建议严格按照“先本地、再远程、先短接、再加长”的顺序来,不要一步到位把 9 颗传感器全接上再去定位问题。
第一步先只焊本地那颗 PJ85718DM,I2C1 速率保持 400kHz,烧入扫描代码,确认能读到 0x48,并且读出的温度跟室温计误差在 1°C 以内。温度值合理后,用热风枪吹芯片附近,观察温度读数上升,再放一块冰袋靠近,读数下降。这一步处理的是传感器本身和 PCB 布线问题,排除掉贴装虚焊、走线太细、铜皮散热不均这些基础故障。
第二步把一颗远程传感器用 15cm 短接线接到 I2C2 端子,扫描 0x49,确认应答并读出合理温度。然后把 A0 拨码改一下变成 0x4A,再扫,确认地址切换生效。第三步换 1 米长的双绞线做同样验证,这时我会把 I2C2 速率调成 100kHz,观察波形上升沿和下降沿是否干净。最后才把其他远程节点全部挂上去,逐一拨码、逐一登记地址和实际安装位置的对应表,这张表后面调试和交付维护都要用。
现场最容易出的问题是地址冲突:两个传感器拨到了同一个地址,扫描时只看到一个节点应答,温度值在两个位置之间随机切换。处理方式很简单,拔掉其他传感器,只保留一个,拨码后逐个扫描验证。这种问题在实验室阶段不暴露,一到现场往往让人头疼,所以我在测试台上就养成了“每颗传感器上贴地址标签”的习惯。
5.2 常见问题速查表与独家心得
我把这一路调试下来遇到的高频问题整理成一张表,能覆盖这类项目 80% 的现场故障:
| 故障现象 | 可能原因 | 排查方向与处理 |
|---|---|---|
| 某地址读不到,HAL 返回超时 | 传感器没上电、线缆断路、地址冲突 | 检查接线和供电,逐个断开节点重扫 |
| 温度值偶发跳变几度 | 干扰或电源纹波 | 远程端加 100nF 去耦,降低 I2C 速率,加软件滑窗滤波 |
| 总线上所有设备都读不到 | SDA/SCL 上拉电阻丢失或短路 | 量阻值,确认上拉到 3.3V,用示波器看时钟有无输出 |
| 读数固定在最大值或者 0xFF | 地址读取异常或芯片损坏 | 重写配置寄存器,换新芯片测试 |
| 线长超过 1.5 米后不稳定 | 线缆电容反射,I2C 自身限制 | 降速到 100kHz 以下,用屏蔽线,加总线中继器 |
| 本地读数正常,远程全废 | I2C2 初始化错误或 GPIO 复用冲突 | 检查 I2C2 引脚映射和初始化代码 |
最后一个独家心得:调试 I2C 总线时,手边备两把不同阻值的上拉电阻很有用。我习惯在 PCB 上留两个电阻位,默认焊接 4.7kΩ,现场通信不稳时先试着换成 3.3kΩ 或者 2.2kΩ,很多时候一换就好。原因前面算过了,远程线缆电容一增加,原本 4.7kΩ 的上升时间就不够用,换小电阻能让上升沿变陡,但又不能太小,太小会把低电平拉不下来。所以工程调试不要太依赖理论计算,电阻备齐了,现场试是最快的。
做了几次这类温控采集之后,我最大的体会是:数字温度传感器的软件其实非常简单,真正的功夫全在总线上拉、线缆屏蔽、地址分配和故障处理这些看起来不起眼的地方。PJ85718DM 加 STM32F446ZE 这套组合,硬件成本不高,代码量也不大,但把上述细节逐项做到位,系统在 HVAC 这种电磁环境恶劣的场景里也能稳定跑很久。下一回再扩展工程时,我可能会在远程线上加一个 I2C 总线中继器,把距离推到更长,顺便在 MCU 端增加不规则采样的掉线重连机制,让故障恢复更干脆。总之这套路子在嵌入式温度监测里算是稳妥又省心的一档选择。