1. 从一个温度采集需求说起:为什么选这套组合
嵌入式温度监测这件事,看起来简单,真做起来坑不少。我最早接触这类需求是在一个暖通空调控制板的项目里,当时的要求是:本地要实时读取板载温度传感器,同时还要通过远程接口把温度数据汇总到上位机,用于楼宇级别的能耗分析和设备保护。难点不在于“读温度”本身,而在于本地与远程两路数据的一致性、采样时序的稳定性,以及通信链路的抗干扰能力。
这套方案的核心是两颗芯片:PJ85718DM和PIC18F87J10。前者是一颗带 I2C 接口的数字温度传感器,后者是 Microchip 经典的 8 位单片机,自带以太网控制器。一个负责“感知”,一个负责“处理与上报”,分工非常清晰。我选择这个组合的原因很直接:PIC18F87J10 内部集成了 10/100 Mbps 以太网 MAC 和 PHY,不需要外挂网络芯片就能实现远程数据上传,而 PJ85718DM 的 I2C 接口占用引脚少、精度够用,两者搭配在成本、功耗和开发难度上达到了一个很好的平衡点。
这篇文章适合谁看?如果你正在做嵌入式温度采集、HVAC 控制器开发,或者需要把本地传感器数据通过网络上报,那这套思路可以直接参考。我会从硬件连接、寄存器配置、采样逻辑、远程通信、问题排查几个维度,把整个实现过程拆开讲清楚。即使你用的是别的单片机和传感器,底层的设计逻辑也是通用的。
2. 硬件设计与器件选型背后的考量
2.1 PJ85718DM 的关键特性与选型理由
PJ85718DM 是一颗低压数字温度传感器,支持 I2C 总线通信,典型精度在 ±0.5°C 范围内,工作电压覆盖 2.7V 到 5.5V。这个电压范围很关键,因为 PIC18F87J10 的 I/O 电平是 3.3V 或 5V 可选,传感器能直接对接,不需要额外的电平转换电路。
选它而不是选模拟传感器加 ADC 的方案,主要考虑三点。第一,数字接口抗干扰能力强,HVAC 环境里继电器、风机、压缩机启停带来的电磁干扰很大,模拟信号走线稍长就容易耦合噪声,而 I2C 是数字信号,有明确的逻辑阈值,抗扰度好很多。第二,传感器出厂已经校准,不需要我在产线上做温度标定,省了一道工序。第三,I2C 总线可以挂多个器件,如果以后要在不同位置布多个测温点,只需要扩展地址就行,硬件改动极小。
PJ85718DM 的 I2C 地址通常由出厂设定,具体地址需要查对应批次的数据手册确认。实际使用时,我一般会在总线上加 4.7kΩ 的上拉电阻到 VCC,这是 I2C 的标准做法,阻值不能太小,否则功耗增加;也不能太大,否则上升沿变缓,高速通信时容易出错。
2.2 PIC18F87J10 的资源分配与网络能力
PIC18F87J10 是我个人比较喜欢的一颗片子。它有 128KB 闪存、3936 字节 RAM,对于温度采集加网络上报这种任务来说绰绰有余。最关键的是它内置了以太网模块,支持 RMII 接口连接外部 PHY,或者在某些封装里直接集成 PHY。实际项目中我用的是外接 PHY 的方案,因为布线更灵活。
引脚分配上,我把 I2C 的 SDA 和 SCL 分配到 RC3 和 RC4,这两个引脚支持 I2C 功能,复用起来方便。以太网部分占用 RB 组的部分引脚用于 RMII 数据线,具体分配要看硬件原理图。剩下的 GPIO 用来驱动本地显示(比如一个 1602 液晶)和几个状态指示灯。
这里有个细节值得注意:PIC18F87J10 的以太网模块和 I2C 模块在中断优先级上需要合理配置。温度采样是周期性的,实时性要求不算极高,但网络通信的中断可能比较频繁。我的做法是把 I2C 采样放在主循环里轮询,以太网收发用中断处理,这样不会因为网络突发流量导致温度采样被长时间阻塞。
2.3 本地与远程的硬件拓扑
整个系统的硬件拓扑可以这样理解:PJ85718DM 贴在需要测温的位置,通过两根线(SDA、SCL)连到 PIC18F87J10。单片机一边周期性地读温度,一边把数据打包成 UDP 或 TCP 报文,通过以太网口发到远程服务器。本地如果有一块小液晶,就直接显示当前温度;远程服务器收到数据后存数据库,做趋势分析。
这里“本地”和“远程”的区分很重要。本地温度是板载传感器直接测到的,反映的是设备附近的真实温度。远程温度则有两种含义:一种是把本地温度传到远端显示,另一种是远端也有传感器,数据回传到本地。我做的项目里两种都有,但核心链路是一样的——传感器采集、单片机处理、网络传输。
注意:I2C 总线的走线长度一般不要超过 1 米,如果传感器离单片机较远,建议改用差分总线或者把传感器放在离单片机近的位置,通过延长网络线来实现远程监测。
3. 软件架构与核心逻辑拆解
3.1 整体软件分层设计
软件部分我分成三层:驱动层、业务逻辑层、通信层。驱动层负责 I2C 读写和以太网控制器的寄存器操作;业务逻辑层处理温度数据的采集、滤波、阈值判断;通信层负责把数据封装成网络报文并发送。
这样分层的好处是,如果以后要换传感器,只需要改驱动层;如果要换通信协议,只动通信层。业务逻辑层基本不用动。我在实际项目里吃过“一锅粥”代码的亏,后来强制自己按这个结构写,维护成本低了很多。
驱动层的 I2C 部分,我用的是软件模拟 I2C,而不是硬件 I2C 模块。原因是我这颗 PIC18F87J10 的硬件 I2C 在低速率下偶尔会出现总线锁死,软件模拟虽然占一点 CPU,但时序完全可控,出问题也容易排查。软件模拟的延时用 NOP 指令微调,确保 SCL 频率在 100kHz 左右。
3.2 温度采集的时序与滤波策略
PJ85718DM 的转换时间通常在几十毫秒量级,具体要看分辨率和配置。我一般设置成 12 位分辨率,转换时间大约 50ms 到 100ms。采样周期我定在 500ms,也就是每 500ms 读一次温度。这个周期对于 HVAC 应用足够了,因为温度变化本身很慢,采样太快没有意义,反而增加功耗和总线负载。
滤波方面,我用的是滑动平均加中值滤波的组合。具体做法是:连续采 5 个点,去掉最大值和最小值,剩下 3 个取平均。这样既能抑制突发噪声,又不会引入太大滞后。实测下来,在继电器频繁动作的环境里,原始数据偶尔会跳变 1°C 到 2°C,经过滤波后波动控制在 0.2°C 以内。
提示:如果传感器和单片机共用同一个电源,电源纹波会影响温度读数。建议在传感器 VCC 引脚旁边加一个 0.1uF 的去耦电容,位置尽量靠近引脚。
3.3 远程通信协议的选择与实现
远程通信我选的是 UDP,而不是 TCP。原因很简单:温度数据是周期性的,偶尔丢一两个包不影响整体趋势分析,UDP 的开销小、延迟低。如果用 TCP,每次重传都会增加延迟,而且连接维护在嵌入式端也比较麻烦。
UDP 报文的格式我定义得很简单:前两个字节是帧头 0xAA55,接着一个字节是设备 ID,然后两个字节是温度值(放大 10 倍,用有符号整数表示,比如 253 表示 25.3°C),最后两个字节是校验和。整个报文 9 个字节,非常紧凑。
发送频率和采样频率一致,也是 500ms 一次。远程服务器收到后解析并存储。这里有个细节:如果网络暂时断开,单片机不会阻塞,继续采样和本地显示,只是发送失败而已。等网络恢复后,新数据继续发,旧数据不补发。这是有意设计的,因为温度监测更关注实时性,历史数据补发意义不大。
4. 实操过程:从寄存器配置到数据上报
4.1 I2C 驱动编写与传感器初始化
先讲 I2C 驱动的关键代码。软件模拟 I2C 需要实现起始条件、停止条件、字节发送、字节接收、应答检测这几个基本函数。我用的是位操作,直接操作 PIC 的 LAT 和 PORT 寄存器。
// I2C 起始条件 void I2C_Start(void) { SDA = 1; SCL = 1; __delay_us(5); SDA = 0; __delay_us(5); SCL = 0; __delay_us(5); } // 发送一个字节,返回应答位 unsigned char I2C_WriteByte(unsigned char data) { unsigned char i, ack; for(i = 0; i < 8; i++) { SDA = (data & 0x80) ? 1 : 0; data <<= 1; __delay_us(5); SCL = 1; __delay_us(5); SCL = 0; __delay_us(5); } SDA = 1; __delay_us(5); // 释放 SDA 等待应答 SCL = 1; __delay_us(5); ack = SDA; // 读应答位 SCL = 0; __delay_us(5); return ack; }传感器初始化主要是配置它的配置寄存器,设置分辨率、工作模式等。PJ85718DM 的配置寄存器地址和位定义需要查数据手册,不同批次可能略有差异。我一般会先读一次器件 ID 确认通信正常,再写配置。
// 初始化温度传感器 void TempSensor_Init(void) { I2C_Start(); I2C_WriteByte(0x90); // 写地址 I2C_WriteByte(0x01); // 配置寄存器 I2C_WriteByte(0x60); // 12 位分辨率,连续转换模式 I2C_Stop(); __delay_ms(100); // 等待第一次转换完成 }4.2 温度读取与数据转换
读温度的过程是:先发写地址指向温度寄存器,再发读地址,然后连续读两个字节。高字节在前,低字节在后。原始数据是 12 位有符号数,需要转换成实际温度值。
float TempSensor_Read(void) { unsigned char msb, lsb; int raw; float temp; I2C_Start(); I2C_WriteByte(0x90); // 写地址 I2C_WriteByte(0x00); // 温度寄存器 I2C_Start(); // 重复起始 I2C_WriteByte(0x91); // 读地址 msb = I2C_ReadByte(1); // 读高字节,发应答 lsb = I2C_ReadByte(0); // 读低字节,发非应答 I2C_Stop(); raw = (msb << 8) | lsb; raw >>= 4; // 12 位数据在高端 if(raw & 0x800) { // 负温度判断 raw |= 0xF000; } temp = raw * 0.0625; // 分辨率 0.0625°C return temp; }这里的分辨率 0.0625°C 是 12 位模式下的 LSB 对应值,计算方式是 1/16。如果是 9 位模式,LSB 就是 0.5°C。实际项目中,0.0625°C 的分辨率已经远超 HVAC 的需求,但高分辨率对滤波有帮助,所以我还是用 12 位。
4.3 以太网初始化和 UDP 发送
PIC18F87J10 的以太网初始化比较繁琐,需要配置 MAC 寄存器、PHY 寄存器、缓冲区等。Microchip 提供了 TCP/IP 协议栈,但我这个项目比较简单,直接裸写寄存器更可控。
关键步骤包括:设置 MAC 地址、配置 PHY 为 100Mbps 全双工、初始化接收和发送缓冲区、使能收发。PHY 的配置通过 SMI 接口读写寄存器,需要等待协商完成。
// 简化的 UDP 发送函数 void UDP_Send(float temp) { unsigned char buf[9]; int temp_int = (int)(temp * 10); buf[0] = 0xAA; buf[1] = 0x55; buf[2] = DEVICE_ID; buf[3] = (temp_int >> 8) & 0xFF; buf[4] = temp_int & 0xFF; // 校验和计算 buf[5] = buf[0] ^ buf[1] ^ buf[2] ^ buf[3] ^ buf[4]; // 实际发送需要填充以太网帧头、IP 头、UDP 头 // 这里省略底层细节,重点是把数据交给 MAC 发送 MAC_SendFrame(buf, 9); }实际发送时,需要在数据前面加上 UDP 头、IP 头和以太网帧头。目的 IP 和端口在初始化时设定好。如果服务器在局域网内,可以直接用 MAC 地址发送,不需要 ARP;如果跨网段,就需要先 ARP 解析。
注意:以太网发送缓冲区是有限的,如果发送频率太高,缓冲区会满。我的做法是在发送前检查缓冲区状态,如果满就丢弃当前帧,不阻塞主循环。
4.4 本地显示与阈值报警
本地显示我用的是一个简单的 1602 液晶,通过 4 位数据线驱动。每 500ms 更新一次温度值。如果温度超过设定的上限或下限,液晶上会显示报警标志,同时点亮一个红色 LED。
阈值判断的逻辑放在业务逻辑层,用滞回比较,避免温度在阈值附近波动时报警频繁切换。比如上限设 30°C,滞回 1°C,那么温度超过 30°C 触发报警,降到 29°C 以下才解除报警。
// 阈值判断与报警 void CheckAlarm(float temp) { static unsigned char alarm_state = 0; if(!alarm_state && temp > TEMP_HIGH_LIMIT) { alarm_state = 1; ALARM_LED = 1; } else if(alarm_state && temp < (TEMP_HIGH_LIMIT - TEMP_HYSTERESIS)) { alarm_state = 0; ALARM_LED = 0; } }5. 常见问题与排查技巧实录
5.1 I2C 通信失败排查
I2C 通信失败是最常见的问题,表现是读不到数据或者读到的全是 0xFF。排查顺序我一般是这样的:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无应答 | 传感器未供电或地址错误 | 用万用表测 VCC,确认地址 |
| 偶尔应答 | 上拉电阻不合适 | 示波器看波形上升沿 |
| 数据错误 | 时序太快或干扰 | 降低 SCL 频率,加屏蔽 |
| 总线锁死 | 从机拉低 SDA 不放 | 发送 9 个时钟脉冲复位 |
我遇到过最坑的一次是上拉电阻用了 10kΩ,在 100kHz 下波形上升沿太缓,导致数据偶尔出错。换成 4.7kΩ 后问题消失。所以上拉电阻不是随便选的,要根据总线电容和速率算。
5.2 温度读数跳变与滤波参数调整
温度读数跳变通常有两个来源:电源噪声和总线干扰。电源噪声可以通过加去耦电容解决,总线干扰则需要检查走线是否远离高频信号线。
滤波参数方面,滑动窗口的大小需要权衡。窗口太大,响应慢;窗口太小,滤波效果差。我一般先用 5 点窗口试,如果还是跳,加到 7 点或 9 点。但要注意,窗口越大,滞后越明显,对于需要快速响应的报警场景,窗口不能太大。
提示:如果温度跳变呈现周期性,比如每隔几秒跳一次,很可能是附近有周期性干扰源,比如变频器或开关电源。这种情况下,除了滤波,还要考虑物理隔离或屏蔽。
5.3 以太网通信不稳定的处理
以太网通信不稳定表现为丢包率高或者连接时断时续。常见原因包括:PHY 协商失败、网线质量差、电磁干扰、缓冲区溢出。
我的排查步骤是:先看 PHY 的状态寄存器,确认连接速率和双工模式是否正常;然后用 ping 测试基础连通性;如果 ping 正常但 UDP 丢包,检查发送缓冲区是否溢出;如果缓冲区经常满,降低发送频率或者增大缓冲区。
还有一个容易被忽略的点:PIC18F87J10 的以太网模块对时钟精度有要求,如果外部晶振偏差太大,会导致通信失败。我用的是 25MHz 晶振,负载电容按手册推荐值选,实测频偏在 20ppm 以内,通信很稳定。
5.4 常见问题速查表
| 问题 | 排查方向 | 解决措施 |
|---|---|---|
| 传感器无响应 | 供电、地址、上拉 | 测电压、查手册、换电阻 |
| 温度值固定不变 | 转换模式配置 | 检查配置寄存器 |
| 温度值偏差大 | 传感器自热、位置 | 远离热源,增加采样间隔 |
| 网络丢包严重 | 缓冲区、网线、干扰 | 降频、换线、加屏蔽 |
| 单片机死机 | 看门狗、堆栈溢出 | 使能看门狗,优化代码 |
| 本地显示闪烁 | 刷新频率太高 | 降低刷新率,加缓冲 |
6. 实操心得与扩展思路
这套方案我在两个项目里实际用过,累计运行超过一年,稳定性没问题。有几个心得值得分享。
第一,采样周期不要设太短。温度本身变化慢,500ms 足够了。设成 100ms 除了增加功耗和总线负载,没有任何好处。我试过 100ms 采样,结果 I2C 总线偶尔出错,改回 500ms 后一次问题都没出过。
第二,远程通信一定要做超时处理。UDP 发送虽然不阻塞,但如果 PHY 异常,发送函数可能卡住。我在发送函数里加了超时计数,超过一定时间就强制返回,避免整个系统卡死。
第三,本地和远程的数据要分开处理。本地显示用滤波后的数据,远程上报可以用原始数据或者滤波后的数据,看需求。我一般上报滤波后的数据,因为远程端通常不做二次滤波。
扩展方面,这套架构很容易加多个传感器。I2C 总线可以挂多个 PJ85718DM,只要地址不冲突。单片机轮流读取,然后打包成一个报文发出去。如果要监测湿度,也可以挂一个湿度传感器,报文格式稍微改一下就行。
另外,如果远程服务器需要更复杂的数据分析,可以在服务器端做,单片机只负责采集和上报。这样单片机的固件保持简单,维护成本低。我个人的原则是:能在服务器端做的,不要放在单片机端,因为服务器端的算力和存储资源丰富得多,改起来也方便。
最后分享一个小技巧:调试阶段,我会在单片机里加一个串口输出,把原始温度数据、滤波后数据、发送状态都打印出来。这样一旦出问题,看串口日志就能快速定位。等系统稳定后,再把串口输出关掉,节省资源。这个习惯帮我省了很多排查时间。