1. 项目缘起与整体设计思路
嵌入式温度监测这个方向,看起来简单,实际上手才知道坑有多密。我这次做的项目,核心是用PJ85718DM这颗远程温度传感器配合PIC18F47K40单片机,搭一套能同时盯住本地和远程温度的监测系统,目标场景是暖通空调(HVAC)设备的状态监控。说白了,就是让设备自己知道“我这儿热不热、远处那个探头凉不凉”,然后根据温度做出相应动作。
为什么选这两颗器件?先说 PIC18F47K40,它是 Microchip 家 PIC18 系列里比较新的一颗,带 128KB Flash、3.5KB RAM,外设资源很够用:4 个 UART、2 个 SPI、2 个 I2C,还有 12 位 ADC 和一堆定时器。做温度监测,I2C 和 ADC 是刚需,它都有,而且引脚数 40 个,做小系统不浪费,做复杂系统也撑得住。PJ85718DM 则是一颗远程温度传感器,走 I2C 或者 SMBus 接口,能接外部二极管或者三极管做远距离测温,典型精度在 1°C 以内,分辨率可以到 0.0625°C。它最大的好处是把远程测温点从单片机旁边挪开,HVAC 里风道、换热器、回风口这些位置离控制板很远,用这颗芯片就能把探头甩过去,信号在板级走 I2C 回来,比直接拉模拟线抗干扰强得多。
整体设计思路分三层:感知层用 PJ85718DM 挂远程探头,同时用 PIC18F47K40 内部 ADC 采本地热敏电阻或者内部温度传感器;处理层在单片机里做温度换算、滤波、阈值判断;执行与通信层通过 UART 或者 I2C 把数据送到上位机或者显示屏,同时驱动继电器、风扇这类执行器。这样一套下来,本地和远程温度都能实时看到,超限还能自动响应。
注意:PJ85718DM 的 I2C 地址是可以通过引脚配置的,默认 0x18 或者 0x4C 这类,具体看型号后缀。上手前一定先翻数据手册的地址表,别直接抄别人的代码,地址不对死活读不出数据。
适合谁来参考?如果你在做嵌入式温度采集、HVAC 控制板、机房环境监控,或者单纯想学 I2C 多器件挂载和远程测温,这套方案都能直接抄作业。下面我把设计取舍、硬件细节、代码实现和踩坑记录全部分享出来。
2. 核心器件选型与硬件设计细节
2.1 为什么是 PJ85718DM 而不是普通热敏电阻
普通 NTC 热敏电阻便宜,但线性度差,需要查表或者复杂的 Steinhart-Hart 公式换算,而且长线传输时引线电阻会直接影响精度。PJ85718DM 内部集成了 ADC 和调理电路,直接输出数字温度值,远程探头走的是恒流源激励,引线电阻的影响被大幅抵消。HVAC 设备里电磁环境不干净,风机、压缩机启停都会带来干扰,数字接口比模拟线稳得多。
这颗芯片支持两种测温模式:本地温度(测芯片自身)和远程温度(测外接三极管)。远程测温的物理原理是测量三极管 Vbe 在不同电流下的压差,这个压差和绝对温度成正比。芯片内部会交替注入两个不同电流,测两次 Vbe,算出差值,再换算成温度。这个过程对噪声不敏感,因为差值抵消了大部分共模干扰。
2.2 PIC18F47K40 的外设分配
PIC18F47K40 在这个项目里承担主控角色,外设分配如下:
| 外设 | 用途 | 引脚示例 |
|---|---|---|
| I2C1 | 挂 PJ85718DM 和 OLED 屏 | RC3/RC4 |
| ADC | 采本地 NTC 或内部温度 | RA0 |
| UART1 | 上传数据到上位机 | RC6/RC7 |
| Timer1 | 1 秒定时采样 | 内部 |
| GPIO | 继电器、LED 指示 | RB0-RB3 |
I2C 总线上挂两个器件没问题,PJ85718DM 和 OLED 地址不冲突就行。如果还要加 EEPROM 存阈值,注意地址别撞。PIC18F47K40 的 I2C 支持标准模式 100kHz 和快速模式 400kHz,PJ85718DM 最高支持 400kHz,所以跑 400kHz 能缩短读取时间,减少总线占用。
2.3 远程探头的选择和布线
远程测温三极管建议用 MMBT3904 这类小信号 NPN,或者专用二极管接法的三极管。PCB 上走线要尽量短,如果探头离板子超过 30cm,建议用屏蔽线,屏蔽层单端接地。探头和芯片之间的走线避免和电机、继电器驱动线平行,交叉时尽量垂直。
实操心得:远程三极管的基极和集电极短接,当二极管用,发射极接芯片的 D+,基极-集电极接 D-。如果接反了,读出来的温度会离谱,通常是负几十度或者上百度,遇到这种先查接线。
本地温度采集我用的是 10k NTC 加 10k 上拉电阻分压,接到 ADC 通道。NTC 选 B 值 3950 的,精度够用。分压后电压范围在 0.5V 到 3V 之间,避开 ADC 的上下轨非线性区。ADC 参考电压用内部 4.096V 或者 VDD,看供电稳定性。
3. 软件架构与关键代码实现
3.1 I2C 驱动层:先搞定底层读写
PIC18F47K40 的 I2C 用 MCC 生成初始化代码最省事,但建议自己把读写函数封装一遍,方便调试。下面是我用的 I2C 读写基础函数,基于 MCC 的 API 再包一层:
#include "mcc_generated_files/i2c1.h" #define PJ85718_ADDR 0x18 uint8_t pj_read_reg(uint8_t reg) { uint8_t data; I2C1_Write(PJ85718_ADDR, ®, 1); I2C1_Read(PJ85718_ADDR, &data, 1); return data; } void pj_write_reg(uint8_t reg, uint8_t val) { uint8_t buf[2] = {reg, val}; I2C1_Write(PJ85718_ADDR, buf, 2); }这里有个细节:PJ85718DM 的寄存器地址是 8 位的,写寄存器时先发地址再发数据,读的时候先写地址再重启读。MCC 的 I2C1_Write 和 I2C1_Read 已经处理了起始和停止条件,直接用就行。如果自己写底层,注意在写地址后要发 Repeated Start,不能发 Stop 再 Start,否则时序不对。
3.2 温度数据读取与换算
PJ85718DM 的远程温度寄存器通常是 16 位,高字节整数部分,低字节小数部分,分辨率 0.0625°C。本地温度寄存器类似。读取代码如下:
float pj_read_remote_temp(void) { uint8_t msb = pj_read_reg(0x01); uint8_t lsb = pj_read_reg(0x10); int16_t raw = (msb << 8) | lsb; raw >>= 4; // 低4位是标志位,右移掉 return raw * 0.0625f; }注意不同型号寄存器地址可能不一样,PJ85718DM 的远程温度高字节在 0x01,低字节在 0x10,本地温度在 0x00 和 0x15。这个地址表一定要以手头的数据手册为准,我见过同系列不同后缀地址完全不同的情况。
本地 NTC 温度换算用查表加线性插值,或者直接上 Steinhart-Hart 公式。我为了省 Flash,用的是分段线性插值,每 10°C 一个点,误差在 0.5°C 以内,够 HVAC 用了。
float ntc_to_temp(uint16_t adc_val) { // 分压公式反推电阻 float v = adc_val * 4.096f / 4095.0f; float r = 10000.0f * v / (4.096f - v); // 查表插值,表省略 return lookup_temp(r); }3.3 主循环与定时采样
主循环用 Timer1 做 1 秒定时,到点采一次本地和远程温度,做滑动平均滤波,然后判断阈值。滑动平均窗口取 8 个点,既能滤掉突发干扰,又不会太迟钝。
#define WINDOW_SIZE 8 float remote_buf[WINDOW_SIZE]; uint8_t buf_idx = 0; float moving_average(float new_val) { remote_buf[buf_idx] = new_val; buf_idx = (buf_idx + 1) % WINDOW_SIZE; float sum = 0; for (int i = 0; i < WINDOW_SIZE; i++) sum += remote_buf[i]; return sum / WINDOW_SIZE; }阈值判断做迟滞,比如高温报警设 60°C,恢复设 55°C,避免在阈值附近反复跳变。继电器输出加最小切换间隔,比如 30 秒内不重复动作,保护压缩机。
3.4 通信协议与上位机对接
UART 上传数据用简单的 ASCII 帧,格式T:25.3,R:48.7\n,上位机解析方便,调试时串口助手直接看。如果要更规范,可以上 Modbus RTU,但 HVAC 小系统里 ASCII 够用。
printf("T:%.1f,R:%.1f\n", local_temp, remote_temp);PIC18F47K40 的 UART 波特率用 115200,误差在 1% 以内。printf 重定向到 UART 需要实现_putch函数,MCC 里有现成的。
注意:I2C 读取失败时要有重试机制,连续 3 次失败就报传感器故障,别让程序卡死在 while 循环里。我一般加超时计数,超过 10ms 没响应就跳过本次采样。
4. 常见问题与排查技巧实录
4.1 温度读数跳变或固定不变
这是最常见的问题。先分清楚是跳变还是固定。固定不变通常是 I2C 没通,用示波器看 SDA/SCL 有没有波形,或者用逻辑分析仪抓包。如果波形有但数据不对,查地址。跳变厉害的话,先看电源纹波,PJ85718DM 的 VDD 要加 0.1uF 和 10uF 去耦,靠近引脚放。远程探头线太长也会跳,加屏蔽或者降低 I2C 速率试试。
4.2 远程温度偏差大
远程测温对三极管型号敏感,MMBT3904 和 2N3904 的 Vbe 特性有差异,芯片内部有补偿但有限。如果偏差稳定在几度,可以在软件里加偏移校准。偏差不稳定的,检查三极管焊接有没有虚焊,或者换一个批次的管子。另外,芯片的远程测温电流档位可以配置,默认档位如果噪声大,可以换低电流档,但灵敏度会降。
4.3 I2C 总线锁死
PIC18F47K40 的 I2C 在从器件拉低 SDA 不放时会锁死。解决办法是在初始化时发 9 个时钟脉冲解锁,或者直接复位 I2C 模块。MCC 生成的代码里一般有I2C1_Initialize(),重新调用一次能恢复。更稳妥的做法是加外部看门狗,或者用 GPIO 模拟 I2C 做备份。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读不到器件 | 地址错、上拉电阻缺失 | 查手册地址,测 SDA/SCL 上拉 |
| 温度固定 -64°C | 远程三极管未接或接反 | 检查 D+/D- 接线 |
| 温度跳变 ±5°C | 电源噪声、探头线太长 | 加去耦,缩短线,降速 |
| I2C 锁死 | 从器件异常拉低 | 发时钟脉冲解锁,复位模块 |
| UART 无输出 | 波特率错、TX/RX 接反 | 测波形,交换线序 |
避坑技巧:调试 I2C 时先把速率降到 100kHz,稳定后再升 400kHz。很多“玄学”问题都是速率太高导致的。另外,PJ85718DM 的配置寄存器写完后要等一个转换周期再读温度, datasheet 里写的是 100ms 左右,别写完立刻读,读出来是旧值。
5. 系统联调与实测数据
硬件焊好、代码烧进去之后,我做了几组实测。本地温度用 NTC 和标准温度计对比,25°C 时偏差 0.3°C,50°C 时偏差 0.8°C,在可接受范围。远程温度用 MMBT3904 探头,放在恒温水槽里,从 10°C 到 80°C 每隔 10°C 测一个点,偏差最大 1.2°C,出现在 80°C 附近,可能是三极管自热导致。整体精度满足 HVAC 监控需求。
响应速度方面,I2C 400kHz 下读一次远程温度大概 2ms,加上本地 ADC 采样和滤波,1 秒周期绰绰有余。继电器动作延迟在 100ms 以内,对风机控制来说足够快。
功耗测了一下,PIC18F47K40 跑 16MHz,PJ85718DM 连续转换,整板电流约 12mA,如果用低功耗模式间歇采样,可以降到 2mA 以下。电池供电的场景可以这么玩。
这套方案我后来还扩展了 OLED 本地显示和蜂鸣器报警,代码结构没大改,只是加了显示刷新和 GPIO 控制。如果你也要做类似项目,建议先把 I2C 读温度跑通,再加显示和通信,一步一步来,别一上来就全功能联调,出了问题不好定位。