
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只要地址不冲突。单片机轮流读取然后打包成一个报文发出去。如果要监测湿度也可以挂一个湿度传感器报文格式稍微改一下就行。另外如果远程服务器需要更复杂的数据分析可以在服务器端做单片机只负责采集和上报。这样单片机的固件保持简单维护成本低。我个人的原则是能在服务器端做的不要放在单片机端因为服务器端的算力和存储资源丰富得多改起来也方便。最后分享一个小技巧调试阶段我会在单片机里加一个串口输出把原始温度数据、滤波后数据、发送状态都打印出来。这样一旦出问题看串口日志就能快速定位。等系统稳定后再把串口输出关掉节省资源。这个习惯帮我省了很多排查时间。