1. 项目缘起与整体设计思路
嵌入式温度监测这件事,说起来简单,做起来坑不少。我最早接触这类需求是在一个环境控制类项目里,当时的要求很朴素:本地要能看到实时温度,远程也要能拿到数据,而且整套系统得足够稳,不能三天两头掉线。后来拿到 PJ85718DM 和 PIC18F87J10 这两颗料,才算把方案彻底跑通。这篇文章就把我踩过的坑、验证过的电路和代码逻辑完整梳理一遍,给正在做类似嵌入式温度采集或者 HVAC 控制的朋友一个可直接参考的模板。
先说说为什么选这两颗芯片。PJ85718DM 是一颗带 I2C 接口的远程温度传感器,本质上是一个远端二极管温度采集前端,它可以接一个外部的三极管或者二极管作为感温元件,然后把温度值通过 I2C 读出来。它的优势在于感温点可以离主控很远,比如你把感温三极管贴在散热器上、风道里、或者管道表面,主控板可以放在几米之外,中间只用两根线连过去,抗干扰能力比直接拉模拟线强太多。PIC18F87J10 则是 Microchip 的一款 8 位单片机,自带 I2C 外设、多路 ADC、足够的 Flash 和 RAM,跑温度采集、本地显示、串口上报这套逻辑绰绰有余。两者搭配,一个负责远端精确测温,一个负责本地采集、逻辑处理和通信调度,分工非常清晰。
整个系统的设计思路可以拆成三条线:第一条是本地温度采集线,用单片机自身的 ADC 接一个板载热敏电阻或者模拟温度传感器,负责监测主控板附近的温度;第二条是远程温度采集线,通过 I2C 挂载 PJ85718DM,读取远端感温点的温度;第三条是数据上报线,把本地和远程的温度值通过 UART 或者无线模块发出去,供上位机或者云端做进一步处理。三条线并行运行,互不干扰,主循环里做时间片轮询即可。
为什么不用单片机直接采远端模拟信号?这里涉及一个很实际的工程问题。远端模拟信号在长线传输时,线阻、分布电容、电磁干扰都会让 ADC 读数漂移,尤其是 HVAC 场景里风机、压缩机、继电器频繁动作,干扰非常严重。而 PJ85718DM 把模拟前端放在了远端附近,通过 I2C 数字接口回传,数字信号对干扰的容忍度远高于模拟信号。这个取舍是我在实际调试中体会最深的:一开始图省事直接用长线拉 NTC 到 ADC,结果温度读数跳变超过正负三度,换成 PJ85718DM 之后波动直接压到零点五度以内。
还有一点值得说,PIC18F87J10 的 I2C 外设支持标准模式和快速模式,PJ85718DM 也支持到 400kHz,两者速率匹配没问题。但实际布线时我建议还是跑 100kHz,因为长线加上多个从机时,400kHz 的上升沿容易变缓,导致通信误码。这个后面在实操部分会详细讲。
2. 核心器件解析与关键参数计算
2.1 PJ85718DM 的工作机制与配置要点
PJ85718DM 的核心是一个远端二极管温度采集通道,它通过交替注入不同电流到外部三极管,测量两次 VBE 的差值,从而换算出温度。这个原理的好处是对三极管本身的绝对参数不敏感,只跟电流比和理想因子有关,所以批次一致性很好。实际使用中,外部感温元件我推荐用 MMBT3904 这类小信号 NPN,把基极和集电极短接当二极管用,焊接在需要测温的位置。
配置上,PJ85718DM 内部有几个关键寄存器:配置寄存器用来设定转换速率、关断模式、报警阈值等;温度寄存器直接给出 11 位或者更高精度的温度值;还有状态寄存器和上下限寄存器。我一般把转换速率设成 8 次每秒,这个速率对 HVAC 这种热惯性大的场景完全够用,而且能降低自发热。这里有个细节:转换速率越高,芯片内部电流越大,自发热越明显,虽然 PJ85718DM 的自发热很小,但在高精度场合还是要注意。
温度值的读取格式需要特别注意。PJ85718DM 输出的温度数据是补码格式,高字节是整数部分,低字节的高几位是小数部分。比如读回来 0x19 和 0x80,组合起来是 0x1980,右移五位得到 0x00CC,也就是 204,对应 25.5 摄氏度。这个移位和符号处理如果搞错,温度会差出几十度,我第一次调试时就因为没处理符号位,负温度全读成了正的大数。
2.2 PIC18F87J10 的资源分配与 I2C 初始化
PIC18F87J10 跑在 8MHz 晶振下,经过 PLL 倍频到 32MHz,指令周期 8MIPS,处理温度采集和串口上报非常轻松。资源分配上,我把 RC3 和 RC4 分配给 I2C 的 SCL 和 SDA,RA0 分配给本地 NTC 的 ADC 输入,RC6 和 RC7 给 UART 做数据上报,RB 口留给 LCD 或者数码管做本地显示。
I2C 初始化有几个参数要算清楚。假设系统时钟 32MHz,I2C 目标速率 100kHz,那么 SSPADD 的值等于 Fosc 除以(4 乘以目标速率)再减一,也就是 32000000 除以 400000 再减一,等于 79。这个值写进 SSPADD 寄存器,实际速率就是 100kHz。如果算错了,通信要么太慢要么直接失败。我见过有人把 SSPADD 设成 0x27 结果速率跑到 400kHz 以上,从机根本来不及响应。
本地 NTC 采集这边,我用的是 10k 的 B3950 热敏电阻,配合 10k 上拉电阻分压。ADC 参考电压用单片机的 VDD,也就是 3.3V。温度换算用 Steinhart-Hart 公式或者查表法都行,我一般用查表加线性插值,速度快而且精度够。这里要注意 ADC 的采样时间,NTC 分压点的等效阻抗在 25 度时是 5k 左右,采样电容充电需要足够时间,我设的采样时间是 8 个 Tad,实测下来很稳。
2.3 本地与远程温度的精度对比与校准
本地 NTC 的精度受限于电阻公差和 ADC 分辨率。10 位 ADC 在 3.3V 参考下,每个 LSB 是 3.2mV,对应到 NTC 分压点大概 0.1 度左右的分辨率,但实际精度受 NTC 本身公差影响,不加校准大概正负 1 度。远程 PJ85718DM 的精度标称是正负 1 度,实际校准后能到正负 0.5 度以内。
校准方法很简单:把本地 NTC 和远程三极管放在同一个恒温环境里,比如用恒温水浴或者高精度恒温箱,分别读取两个温度值,记录差值,然后在代码里做偏移补偿。我一般取 25 度和 50 度两个点做线性校准,这样在整个工作区间内都能保持较好的一致性。校准数据存在单片机的 EEPROM 里,上电时读出来用。
这里有个经验:远程三极管的焊接质量对精度影响很大。如果焊锡不好或者三极管引脚太长,热阻会变大,导致读数滞后于实际温度变化。我建议把三极管尽量贴近被测表面,引脚剪短,必要时涂一点导热硅脂。
3. 硬件搭建与实操过程详解
3.1 电路连接与布线注意事项
硬件连接分三块:PIC18F87J10 最小系统、PJ85718DM 外围电路、本地 NTC 分压电路。PIC18F87J10 最小系统包括 8MHz 晶振、两个 22pF 负载电容、复位电路和去耦电容。去耦电容我用了 100nF 和 10uF 并联,放在 VDD 和 VSS 引脚最近的地方,这个对 I2C 通信稳定性影响很大,省不得。
PJ85718DM 的 SCL 和 SDA 分别接到单片机的 RC3 和 RC4,两根线各接一个 4.7k 上拉电阻到 3.3V。上拉电阻的值不能随便选:太小功耗大,太大上升沿变缓。4.7k 在 100kHz 和 3.3V 下是折中值,实测波形很干净。如果总线电容超过 200pF,比如线比较长或者挂了多个从机,上拉电阻要减小到 2.2k 左右。
远程三极管的连接是重点。PJ85718DM 的 D+ 和 D- 分别接三极管的基极加集电极和发射极。走线要用双绞线或者屏蔽线,D+ 和 D- 尽量靠近,减少环路面积。我在一个 HVAC 项目里因为把 D+ 和 D- 分开走线,结果风机一启动温度就跳变,后来改成双绞线并加了一个 100nF 的差分滤波电容,问题才解决。
本地 NTC 分压电路很简单:3.3V 经过 10k 上拉电阻到 NTC,NTC 另一端接地,分压点接 RA0。分压点并联一个 100nF 电容滤波,效果很明显。NTC 的引线也不宜过长,最好直接焊在板子上,如果必须延长,用屏蔽线。
3.2 固件框架与关键代码实现
固件用 C 语言写,编译器用 XC8。主循环采用时间片轮询,每 100ms 采集一次本地和远程温度,每 1s 通过串口上报一次。I2C 读写用状态机实现,避免阻塞主循环。
I2C 读 PJ85718DM 温度的核心代码如下:
unsigned int read_remote_temp(void) { unsigned char msb, lsb; unsigned int raw; I2C_Start(); I2C_Write(0x90); // PJ85718DM 写地址 I2C_Write(0x00); // 温度寄存器地址 I2C_Start(); I2C_Write(0x91); // 读地址 msb = I2C_Read(1); lsb = I2C_Read(0); I2C_Stop(); raw = ((unsigned int)msb << 8) | lsb; raw >>= 5; if (raw & 0x0400) { // 负温度处理 raw |= 0xF800; } return raw; }这段代码里,地址 0x90 和 0x91 取决于 PJ85718DM 的 A0 到 A2 引脚接法,我全接地所以是 0x90。温度值右移五位后是 11 位有效数据,最高位是符号位。负温度要扩展符号位,否则会读成很大的正数。
本地 NTC 的 ADC 读取和换算:
unsigned int read_local_adc(void) { ADCON0bits.CHS = 0; // 选择 AN0 ADCON0bits.GO = 1; while (ADCON0bits.GO); return ((unsigned int)ADRESH << 8) | ADRESL; } float adc_to_temp(unsigned int adc) { float voltage = adc * 3.3f / 1024.0f; float resistance = 10000.0f * voltage / (3.3f - voltage); // 查表加线性插值,返回摄氏度 return lookup_temp(resistance); }查表法比直接算对数快很多,表里存的是电阻值和对应温度,每 5 度一个点,中间线性插值。实测下来,从 ADC 读到温度输出,整个换算耗时不到 100 微秒,对主循环毫无压力。
3.3 串口上报协议与上位机对接
串口上报我定义了一个简单的帧格式:帧头两个字节 0xAA 和 0x55,然后一个字节表示数据长度,接着是本地温度高字节、本地温度低字节、远程温度高字节、远程温度低字节,最后是校验和。温度值统一放大 10 倍用整数传输,比如 25.5 度传 255,避免浮点传输的麻烦。
上位机用 Python 写一个简单的解析脚本,收到帧后校验、解析、显示。这个协议虽然简单,但足够可靠,我在多个项目里都用这套格式,没出过问题。如果要接云端,中间加一个无线模块或者网关,把串口数据转成网络数据即可。
这里提醒一点:串口波特率我用的 9600,虽然慢但抗干扰好,线长一点也没问题。如果数据量大可以提到 115200,但线不能太长,最好在板子附近。
4. 常见问题排查与避坑经验实录
4.1 I2C 通信失败与温度读数异常
I2C 通信失败是最常见的问题,表现是读回来的温度一直是 0 或者 0xFF。排查顺序我一般是这样:先用示波器看 SCL 和 SDA 波形,确认有没有上拉、上升沿是否够快、时钟频率是否正常。如果波形正常但读不到数据,检查从机地址对不对,PJ85718DM 的地址由 A0 到 A2 决定,接法不同地址就不同。如果地址对但数据不对,检查寄存器指针有没有写对,温度寄存器地址是 0x00,写错就读到别的寄存器了。
温度读数异常还有一种情况是符号位没处理好,负温度读成很大的正数。这个在代码里加一个符号扩展就能解决。另外,如果温度值一直不变或者跳变很大,检查感温三极管有没有焊好,D+ 和 D- 有没有接反。接反了读数会完全不对,但不会损坏芯片。
4.2 本地 NTC 读数漂移与干扰抑制
本地 NTC 读数漂移通常有两个原因:一是 ADC 参考电压不稳,二是分压点受干扰。参考电压不稳的话,可以在 VDD 上加一个稳压芯片,或者用外部基准。分压点受干扰的话,加滤波电容是最直接的办法,100nF 不够就上 1uF。如果干扰来自继电器或者电机,还要考虑在电源和地上加磁珠或者共模电感。
还有一个容易被忽略的点:NTC 的自发热。NTC 上一直有电流流过,会自己发热,导致读数偏高。10k 上拉在 3.3V 下,25 度时电流大概 0.16mA,自发热功率很小,一般可以忽略。但如果用更小的上拉电阻,比如 1k,电流就到 1.6mA,自发热就明显了。所以上拉电阻不要选太小。
4.3 长线传输与多从机场景的稳定性优化
长线传输时,I2C 的上升沿会变缓,因为线电容大了。解决办法是减小上拉电阻,但功耗会增加。我一般先算总线电容,然后根据上升沿时间要求选上拉电阻。上升沿时间要求是 1000ns 以内,总线电容假设 200pF,上拉电阻最大是 1000ns 除以 200pF 再除以 0.847,大概 5.9k,所以 4.7k 是安全的。如果线更长,电容到 400pF,上拉电阻就要降到 2.2k 左右。
多从机场景下,每个从机的地址不能冲突。PJ85718DM 支持 A0 到 A2 三个地址引脚,最多挂 8 个。如果不够,可以用 I2C 多路复用器扩展。另外,多从机时总线电容是累加的,上拉电阻要相应减小。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| I2C 读不到数据 | 上拉电阻缺失或过大 | 示波器看波形 | 加 4.7k 上拉 |
| 温度读数恒为 0 | 从机地址错误 | 核对 A0-A2 接法 | 修正地址 |
| 负温度读成大正数 | 符号位未扩展 | 检查代码移位逻辑 | 加符号扩展 |
| 本地温度漂移 | 参考电压不稳 | 测 VDD 纹波 | 加稳压或基准 |
| 远程温度跳变 | 感温线受干扰 | 检查走线 | 双绞线加滤波 |
| 通信偶发失败 | 总线电容过大 | 算总线电容 | 减小上拉电阻 |
这张表是我在实际调试中总结出来的,基本上覆盖了八成以上的问题。遇到新问题先查表,查不到再逐步排查。
5. 系统扩展与场景适配建议
5.1 多路远程温度采集的实现
如果项目需要监测多个远程点,比如 HVAC 系统里要同时监测回风、送风、盘管等多个位置,可以用多个 PJ85718DM 挂在同一条 I2C 总线上,通过 A0 到 A2 设置不同地址。每个 PJ85718DM 接一个感温三极管,分别放在不同位置。固件里轮询读取每个地址的温度值,然后打包上报。
多路采集时要注意总线负载。每个 PJ85718DM 的输入电容大概 10pF,加上走线电容,挂 8 个的话总线电容可能到 300pF 以上,上拉电阻要相应调整。另外,轮询频率不要太高,每个从机每秒读一次就够了,读太快反而增加总线冲突概率。
5.2 低功耗设计与电池供电场景
如果系统要用电池供电,低功耗设计就很重要。PIC18F87J10 支持多种低功耗模式,可以在两次采集之间进入睡眠,用定时器唤醒。PJ85718DM 也支持关断模式,不采集时关掉,功耗降到微安级。本地 NTC 分压电路在睡眠时也会耗电,可以在上拉电阻上串一个 MOS 管,睡眠时断开。
实测下来,采集间隔 10 秒、每次采集 100ms 的方案,平均电流可以做到 1mA 以下,用两节 AA 电池能跑几个月。如果采集间隔更长,功耗还能进一步降低。
5.3 与 HVAC 控制系统的联动
温度数据采集出来之后,最终要用于控制。比如当回风温度超过设定值,启动压缩机;当送风温度过低,防止盘管冻结。这些逻辑可以在 PIC18F87J10 里直接实现,通过 GPIO 控制继电器或者固态继电器。也可以把温度数据上报给上位机,由上位机做更复杂的控制策略。
联动控制时要注意迟滞设计,避免温度在设定点附近频繁波动导致执行器频繁动作。我一般设 1 度的迟滞,比如设定 25 度启动制冷,降到 24 度才停止。这个迟滞值可以根据实际系统惯性调整。
6. 个人实操体会与后续优化方向
这套方案我从最早的面包板验证到后来的 PCB 量产,前后迭代了三四版,最大的体会是:温度采集这件事,精度和稳定性往往不取决于芯片本身,而取决于外围电路和布线细节。PJ85718DM 和 PIC18F87J10 本身都很可靠,但上拉电阻选错、滤波电容省掉、感温线走不好,照样出问题。
另一个体会是校准不能省。哪怕芯片标称精度够用,实际装到板子上之后,受布局和周边器件影响,总会有偏差。花半个小时做两点校准,后面能省很多调试时间。校准数据存 EEPROM 里,产线上一块板子校准一次就行。
后续如果要做产品化,我会考虑几个方向:一是把本地 NTC 换成数字温度传感器,比如带 I2C 接口的型号,省掉 ADC 和查表逻辑;二是把上报接口从串口换成无线,方便组网;三是加一个看门狗和通信超时复位,提高长期运行可靠性。这些改动都不大,但能让系统更健壮。
最后分享一个小技巧:调试 I2C 的时候,如果手头没有示波器,可以用单片机的 GPIO 模拟 I2C 主机,配合逻辑分析仪抓波形,成本很低但非常实用。我早期就是用这个方法定位了好几个通信问题。