☰
本地+远程温度采集设计:PJ85718DM与RS-485实战方案
2026/10/22 17:02:23 网站建设 项目流程

今年在调试一套恒温恒湿机组的主控板时,遇到一个很常见却特别容易栽跟头的问题:控制板自己测得的温度非常准,但系统的送风口温度、房间实际温度,和主控做逻辑判断用的温度对不上。反复查下来,发现问题不在算法,而在温度信号的采集层次——本地温度归本地采集,分布在不同位置的温度节点却没有一套可靠的远程回传方案。

后来项目改用 PJ85718DM 采集本地温度,通过 RS-485 总线把分布在各个区域的远程温度节点统一汇聚到 dsPIC33EP512MU814 这颗主控上,整套系统的温度监测才真正稳定下来。这套思路在嵌入式设备、HVAC 控制、机房环境监控里都很通用,这篇就从头到尾拆一遍:为什么同时需要本地和远程两条温度链、PJ85718DM 的 I2C 驱动怎么写、dsPIC33EP512MU814 侧怎么配时钟和引脚、远程温度节点用 RS-485 怎么做协议,以及我在实测中踩过的几个坑。如果你正在做温控板、新风系统或者任何需要多点温度采集的嵌入式项目,这篇可以直接拿来当参考。

1. 为什么一块温控板需要同时维护本地和远程两条温度链

1.1 本地温度解决的是“板子自己知不知道热”

大多数 HVAC 主控板上,本地温度至少承担两个职责:一个是监测主控板自身的工作温度,防止 MCU 或电源模块过热;另一个是采集设备本体上的关键温度点,比如新风机组里的盘管温度、混风温度、压缩机吸气/排气温度。

板级温度这个事经常被低估。dsPIC33EP512MU814 这类 DSC 正常工作电流不小,加上旁边的功率器件、线性稳压器,如果散热带不走,板内温度会比环境温度高出不少。以前见过一块板,MCU 的 ADC 采样通道被临近的开关电源干扰,温度读值随负载波动,试了很多滤波手段都压不住,最后换数字温度传感器直接从根源上绕开了模拟采样的噪声问题。

设备本体的温度点也建议用数字传感器。NTC 加 ADC 的方案虽然便宜,但每个通道都要做分压电阻校准、查表线性化,还得处理线缆电阻带来的误差。PJ85718DM 这类 I2C 接口的数字传感器,出厂前做校准,读回来就是干净的温度数值,把“测量”这件事从 MCU 里彻底摘出去。

1.2 远程温度解决的是“房间到底热不热”

本地温度再准,也只能代表控制板附近的热状态。真正决定空调、新风、恒温恒湿设备怎么工作的,是分布在多个位置的远程温度:每个房间的室温、风管末端温度、冷热水管上的水温探头、室外环境温度。

这些远程节点和主控板之间往往隔着十几米甚至几十米线缆。如果直接在主控板上把所有温度传感器都挂到 I2C 总线上,先不说线缆长度对 I2C 信号的影响,光是每个传感器都要不同的地址、每根线都要单独布线,安装和后期维护就已经够头疼了。更合理的做法是:远程节点各自带一颗小的控制芯片,就近完成温度采集,然后通过 RS-485 总线这样的差分信号链路,把数据统一送回主控。

1.3 两颗芯片的分工

这个项目里真正干活的组合是“PJ85718DM 负责感知,dsPIC33EP512MU814 负责汇聚和控制”。

PJ85718DM 是数字温度传感器,本地温度由它直接采集,挂在主控的 I2C 总线上;远程温度节点上也是同样的传感器,不过由各节点自己的小 MCU 采集,采集结果再通过 RS-485 打成协议包发给主控。dsPIC33EP512MU814 的 I2C 通道负责和本地传感器通信,UART 通道加上 RS-485 收发器负责和远程节点通信,最后把所有温度值汇总到控制算法里。

选 dsPIC33EP512MU814 而不是普通 8 位 MCU,主要是因为这类控制板除了温度采集,往往还要同时跑多个控制回路、通信协议栈和故障处理逻辑。512KB 的程序存储空间足够放下这些代码,外设资源里多个 UART、I2C、SPI、CAN 可以同时工作,PPS 引脚重映射又让 PCB 布局灵活很多——这些在后续设计里都会体现出优势。

2. 本地温度通道的硬件连接与 I2C 总线初始化

2.1 传感器挂载方式和上拉电阻选型

PJ85718DM 的标准接法不复杂:VCC 接 3.3V,GND 接地,SDA 和 SCL 分别接 dsPIC33EP512MU814 的 I2C1 引脚,再在两根线上各接一个上拉电阻到 3.3V。I2C 是开漏结构,上拉电阻是必须的,没人能省掉这一步。

上拉电阻的取值直接决定总线能不能稳定工作。只挂一颗传感器、PCB 走线在 10cm 以内时,4.7kΩ 是安全值;如果板上挂了多颗传感器或者走线较长,建议降到 2.2kΩ。上拉电阻太大,信号上升沿太慢,高速模式下容易读回乱码;电阻太小,总线灌电流偏大,长期可靠性受影响。

我在实际项目里见过一种怪现象:传感器单独测试都正常,一装到整机上就偶发读回 0xFF。用示波器抓 SDA 波形才发现,线束里的寄生电容把上升沿拖到了接近 1μs,100kHz 速率下已经处于临界状态。后来把上拉电阻从 4.7k 换成 2.2k,波形立刻干净了。

2.2 dsPIC33EP512MU814 的时钟树与 I2C1 寄存器配置

dsPIC33EP512MU814 的 I2C 波特率由主时钟 FCY 和 I2CxBRG 寄存器共同决定,公式是:

FSCL = FCY / (2 × (I2CxBRG + 1))

反过来,给定目标速率就能算出 BRG 值:

I2C1BRG = (FCY / (2 × FSCL)) - 1

项目的系统主频按 40 MIPS 配置,也就是 FCY = 40MHz。想跑 100kHz 的标准 I2C 速率,计算如下:

I2C1BRG = (40,000,000 / (2 × 100,000)) - 1 = 199

这段逻辑落实到代码里,初始化函数大概是这个样子:

void I2C1_Init(void) { // 假设 FCY = 40MHz,I2C 时钟目标 100kHz I2C1BRG = 199; // 先关闭模块,再打开,确保寄存器配置生效 I2C1CON = 0; I2C1CONbits.I2CEN = 1; // 使能 I2C1 I2C1STAT = 0; // 清状态寄存器 }

一个很容易被忽略的细节是:I2C1BRG 寄存器的值必须和实际的 FCY 匹配。很多人直接用库生成工程,FCY 是多少自己心里没数,结果用公式反推寄存器时怎么都对不上,其实问题出在系统时钟配置上。建议初始化 I2C 之前,先确认 OSCCON 里 PLL 配置是否到位,必要时读 CLKDIV 寄存器核对当前 FCY。

2.3 PPS 引脚重映射:别想当然认为 I2C 引脚是固定的

dsPIC33EP 系列和很多单片机不太一样,片内外设并不绑定固定引脚。I2C1 的 SCL、SDA 信号要通过 PPS(外设引脚选择)模块映射到具体的 RP 引脚上。

这一步被坑的人非常多。直接照着别人的代码操作 I2C1CON,但自己的板子上引脚没映射,总线怎么都拉不低,示波器一看引脚状态压根没变化。正确做法是先把引脚配置成数字输入/输出,再设置 PPS 映射。

输入映射通过 RPINR 寄存器完成,输出映射通过 RPOR 寄存器完成:

// 输入映射:I2C1 SCL 来自 RP12,I2C1 SDA 来自 RP13 RPINR0bits.I2C1SCL = 12; RPINR1bits.I2C1SDA = 13; // 输出映射:RP12 输出 SCL1,RP13 输出 SDA1 // 具体的输出功能编码,需要查阅 dsPIC33EP 系列数据手册的 PPS 输出表 RPOR6bits.RP12R = 0x28; // 示例值,实际视手册编码而定 RPOR6bits.RP13R = 0x29; // 示例值,实际视手册编码而定

如果你觉得手写 PPS 映射太繁琐,用 Microchip 的开发配置工具生成也行,它会根据你在图形界面里选择的引脚自动生成映射代码。但你要看得懂它生成的代码含义,否则后面一旦改板子换引脚,很容易改漏一个地方,导致 I2C 整个不工作。

3. 从 PJ85718DM 读出真实温度:驱动时序与数据转换

3.1 单次转换读取流程

PJ85718DM 的读取流程和大多数 I2C 数字温度传感器一致:主机发起起始条件,发送设备地址加写位,然后写入内部寄存器指针地址,再发起重复起始条件,发送设备地址加读位,连续读取温度数据的高字节和低字节,最后产生停止条件。

用 dsPIC33EP512MU814 的硬件 I2C 模块手动操作,关键函数可以拆成这几步:

void I2C1_Start(void) { I2C1CONbits.SEN = 1; while (I2C1CONbits.SEN); // 等待起始条件完成 } void I2C1_Stop(void) { I2C1CONbits.PEN = 1; while (I2C1CONbits.PEN); // 等待停止条件完成 } void I2C1_WriteByte(uint8_t data) { I2C1TRN = data; while (I2C1STATbits.TBF); // 等待发送缓冲清空 } uint8_t I2C1_ReadByte(uint8_t ack) { I2C1CONbits.RCEN = 1; while (!I2C1STATbits.RBF); // 等待接收完成 uint8_t data = I2C1RCV; I2C1CONbits.ACKDT = ack ? 0 : 1; // 0 表示回应 ACK,1 表示回应 NACK I2C1CONbits.ACKEN = 1; while (I2C1CONbits.ACKEN); return data; }

有了这几个基本操作,读温度寄存器就很直接了:

int16_t PJ85718DM_ReadRaw(void) { uint8_t msb, lsb; I2C1_Start(); I2C1_WriteByte(SENSOR_ADDR_W); // 设备地址 + 写,实际地址查阅传感器数据手册 I2C1_WriteByte(0x00); // 内部寄存器指针指向温度数据寄存器 I2C1_Start(); // 重复起始条件 I2C1_WriteByte(SENSOR_ADDR_R); // 设备地址 + 读 msb = I2C1_ReadByte(1); // 读高字节,回应 ACK lsb = I2C1_ReadByte(0); // 读低字节,回应 NACK I2C1_Stop(); return (int16_t)((msb << 8) | lsb); }

注意“重复起始条件”这步有些人会忽略,直接把读地址写在第一帧后面。I2C 规范要求读操作必须先发一次重复起始,才能把总线从写模式切换成读模式。如果只发一次起始就接着写读地址,从机不会响应。

3.2 12 位数据的符号扩展和单位换算

PJ85718DM 这类传感器通常把温度数据放在 16 位寄存器的高 12 位,低 4 位为无效位。换算方法:

  • 如果 12 位数据的最高位是 0,表示正温度,直接把右移 4 位后的值乘以 0.5℃ 即可;
  • 如果最高位是 1,表示负温度,寄存器里是二进制补码形式,需要先做符号扩展再换算。

实际项目里我会尽量避免在控制算法里直接使用浮点格式,而是用“摄氏度的 100 倍整数”来传递温度,这样既没有浮点运算开销,也不会出现 0.1 这类二进制小数表达不清的问题:

int16_t PJ85718DM_ReadCelsiusX100(void) { int16_t raw = PJ85718DM_ReadRaw(); // 将 12 位数据右移 4 位,得到 0.5℃/LSB 的整数值 int16_t temp = raw >> 4; // 如果符号位为 1,做符号扩展 if (raw & 0x0800) { temp |= 0xF000; } // 转换成“摄氏度 × 100”的整数温度 // 0.5℃ 精度,换算系数为 50 return (int16_t)temp * 50; }

寄存器位宽和移位位数要严格按传感器数据手册来。有的传感器是 12 位右移 4 位,有的其实内部已经对齐好了。以手里的 datasheet 为准,不要盲目套代码。

3.3 连续模式下数据粘滞与异常值处理

HVAC 控制系统里,温度采集通常不会只读一次就完事,而是周期性读取。这里有一个很隐蔽的问题:如果读取频率和传感器内部 ADC 转换周期不匹配,可能连续两次读到完全相同的旧数据,或者读到转换到一半的新旧混合数据。

我处理这个问题的方式是加一层“有效性判断”:读回的数据如果和上一次完全相同,允许原样使用,但要记录连续相同次数;如果连续出现多次完全相同的数据同时超过了正常的数据刷新周期,就认为传感器异常,进入故障计数流程。

另外要重点关注读回值为 0xFFFF 或 0x0000 的情况。这两个值通常是总线异常或传感器未就绪的标志。直接把这种异常值丢进控制算法,会给 PID 带来巨大的扰动。所以读取函数应该带上返回值标志:

typedef struct { int16_t value_x100; // 温度值:摄氏度 × 100 uint8_t status; // 0:正常;1:无响应;2:数据未更新;3:无效读值 } temp_result_t;

驱动层就把数据质量分好类,上层控制逻辑只看 status,省去了很多重复判断。

4. 远程读数不是简单拉根线:RS-485 总线的可靠性设计

4.1 为什么远程温度不直接延长 I2C

有人问过:既然远程节点上也是 PJ85718DM,为什么不直接把 I2C 线拉到远程去?答案很实在:I2C 是为板内通信设计的,长距离传输有几个致命问题。

第一,I2C 是开漏结构,上拉电阻加在主机端,线越长电容越大,信号上升沿越慢,最后波形完全走形。第二,I2C 总线靠设备地址区分不同器件,远程多个节点挂在同一条总线上,地址冲突、时序冲突都会出现。第三,I2C 没有差分传输能力,抗共模干扰能力弱,在电机、压缩机、变频器附近跑长线,数据出错率非常高。

RS-485 是差分信号,抗干扰强,最大距离可达上百米,支持一主多从的典型架构。用在 HVAC 这种电机噪声大的环境里,比 I2C 拉长线靠谱得多。

4.2 通信电路和拓扑

远程温度节点的电路并不复杂,主控板的 UART 发送和接收引脚,接到 RS-485 收发器的 DI 和 RO 引脚上,DE/RE 引脚合并在一起,由 MCU 的一个 GPIO 控制。工作时序上,发送数据前拉高 DE,发送完成后必须拉低,否则总线会被自己占用,其他节点无法回应。

总线两端各放一个 120Ω 终端电阻,这是 RS-485 的基本要求。实际项目里如果只有两三个节点且线很短,不接终端电阻也能跑,但最好还是按规范来。A、B 线之间建议加偏置电阻,保证总线空闲时 A 相对 B 为高电平,避免所有节点都在接收状态时总线悬空产生乱码。

波特率我一般选 9600 或 19200。9600 在几十米线缆下非常稳,19200 够用且每个节点的温度数据量很小,没有性能压力。

4.3 协议帧与多节点地址分配

远程温度读取的典型协议可以直接借鉴 Modbus RTU 的简化实现。主机向某个从机发送读请求,从机收到后返回温度数据。一帧请求可以设计成:

[从机地址] [功能码] [寄存器地址高] [寄存器地址低] [数据长度] [CRC低] [CRC高]

例如读取 1 号从机的温度数据,寄存器地址从 0x0000 开始,读取 2 个字节,请求帧为:

01 03 00 00 00 01 CRC_L CRC_H

其中 CRC 是循环冗余校验,按 Modbus 标准的多项式计算填充。从机正常响应:

01 03 02 温度低字节 温度高字节 CRC_L CRC_H

如果从机检测到 CRC 错误,或者收到非法指令,就不做任何应答。主机侧通过超时判断该节点是否异常。

多节点地址分配,我习惯用硬件拨码开关设置从机地址,1 到 31 足够用。软件下发地址虽然也能做,但现场维护时没有硬件拨码直观,而且更换备件后还要重新配地址,麻烦。

4.4 UART 收发实现的几个要点

dsPIC33EP512MU814 的 UART 接收建议用中断加状态机处理。我见过有人用阻塞式查询接收,结果一个从机总线异常时,主机整个控制循环跟着卡住。中断方式下,主机只负责收字节,一帧数据的完整性判断放在状态机里。

超时判断很重要。Modbus 帧的结束通常用“静默间隔”判断:连续 3.5 个字符时间内没有新字节进来,就认为当前帧接收完毕。实现方法是在 UART 接收中断里开一个定时器,每次收到字节就重置计数,定时器溢出就触发帧结束标志。

__attribute__((interrupt, no_auto_psv)) void _U1RXInterrupt(void) { uint8_t byte = U1RXREG; // 将字节存入接收缓冲区 rx_buf[rx_len++] = byte; // 重置超时计数器 frame_timeout = 0; // 清中断标志 IFS0bits.U1RXIF = 0; }

数据量大了以后,也可以把 UART 收发改成 DMA 搬运,释放 CPU 处理控制算法。不过这类温度采集系统数据量很小,优先级不高,先用中断实现即可。

5. 温度数据的滤波、标定与 HVAC 控制可用性验证

5.1 滑动平均窗口怎么选

传感器原始数据读出后,不能直接送进控制算法。HVAC 环境里的温度变化本身是慢变量,但受压缩机启停、风门动作、人员走动影响,瞬时读值会跳动。滑动平均是最常用的方法。

窗口大小的选择要平衡两个方向:窗口越大,平滑效果越好,但延迟也越大;窗口太小,毛刺滤不掉。以 1 秒读一次为例,我用 5 到 10 点的滑动平均,对应 5 到 10 秒的响应延迟,这个延迟对 HVAC 温度控制完全可接受。

同时建议加一个异常点剔除逻辑:如果新读到的温度值与上一个有效值的差超过 2℃,直接丢弃而不参与平均。这一步能防止传感器偶发跳变把平均值拉偏。

#define FILTER_WINDOW 8 int16_t temp_filter_store[FILTER_WINDOW]; uint8_t temp_filter_idx = 0; int16_t temp_filter_push(int16_t new_val) { int32_t sum = 0; temp_filter_store[temp_filter_idx] = new_val; temp_filter_idx = (temp_filter_idx + 1) % FILTER_WINDOW; for (int i = 0; i < FILTER_WINDOW; i++) { sum += temp_filter_store[i]; } return (int16_t)(sum / FILTER_WINDOW); }

5.2 标定流程与偏差补偿

数字传感器出厂前已经校准,但实际安装位置、PCB 铜皮导热、传感器自身发热都会引入误差。我在项目里做过一次整机标定:把设备的温度探头插入恒温水槽,设定 0℃、25℃、50℃ 三个稳定点,记录传感器读值和标准温度计读值,计算出每个点的偏差。

标定发现,传感器读值普遍比标准温度高 0.3℃ 左右。原因很典型:传感器装在主控板上,靠近电源芯片,板上热量通过铜皮传导给传感器。这个偏差不是传感器本身的问题,而是安装位置引入的。

补偿方式有两种:如果偏差在不同温度点基本一致,直接做常数修正;如果随温度变化,就要做两点或三点线性插值。补偿系数我习惯存在 dsPIC33EP512MU814 的内部 Flash 模拟 EEPROM 区域,这样更换代码后不需要重新标定。要注意 Flash 写入次数有限,不要在运行时频繁写。

5.3 把读数喂给 PID 之前还要做的事

温度值经过滤波、标定之后,往控制算法里送之前还有一步容易漏掉:输出限幅和变化率限制。

PID 调节器对输入信号的突然跳变非常敏感。即使做了滑动平均,如果滤波窗口不够大,一次异常数据仍然可能让输出产生明显抖动。我的做法是在滤波函数之后再加一级变化率限制:

  • 每次温度更新,和上一次输出值比较;
  • 变化率超过设定上限(比如 0.5℃/s),就把本次输出限制在上次值加上限之内;
  • 持续超过上限若干次,才认为温度真的发生陡变,而不是传感器故障。

这种“限幅但不屏蔽趋势”的策略,既能滤除毛刺,又不会影响真实快速变化场景下的响应速度。

6. 调试过程中最容易被忽略的三个坑

6.1 I2C 总线被长线电容拖垮

第一个坑就是开头提到的长线问题。有一台设备的本地温度传感器放在机箱前面板,离主控板走线约 30cm,用的是普通排线。上电后传感器读值时好时坏,示波器一看,SCL 上升沿已经明显变圆。

解决的办法按优先级排序:先尝试把 I2C 速率从 400kHz 降到 100kHz,大多数情况下能立竿见影;同时把上拉电阻改小到 2.2kΩ;如果还不行,检查线缆是不是双绞、有没有靠近功率线。对 30cm 左右的 I2C 走线,降速基本都能解决。

提醒一点:不要在 I2C 线上随意串接电阻或磁珠来“滤波”,这会直接增加信号上升时间,让问题更严重。

6.2 传感器自身发热导致读数偏差

第二个坑和传感器封装有关。PJ85718DM 这类器件正常工作时自身功耗很小,但如果布局时把它放在发热元件旁边,或者铺铜时让它和电源区域共用大面积铜皮,读值就会系统性偏高。

有一次远程节点的读值比附近水银温度计高了约 0.8℃,排查了一圈都没找到电路问题。后来用手摸到传感器外壳有点温热,才意识到是旁边一颗 LDO 的热量传过来的。解决办法是调整布局,让传感器区域和功率器件之间开一条隔离槽,或者拉开距离,必要时在传感器底下挖空铜皮减少热传导。

6.3 远程巡检周期与看门狗的配合

第三个坑出在巡检逻辑上。主机轮询多个远程节点时,如果某个节点掉线,主机还在那儿等响应,整个采集循环会被拖死。必须给每次通信设超时,一般 200ms 到 500ms 就足够,超时后直接判定节点离线,继续处理下一个节点。

连续多次通信失败的节点,状态标记为故障,并在控制算法里把该温度点替换成安全值。我在恒温恒湿机组的控制里遇到过:一个远程温度传感器被老鼠咬断线缆,主控一直用最后读到的那组温度做 PID,结果房间温度一路飘到 28℃ 还傻乎乎地加热。后来加了掉线超时和故障替代,这个问题才彻底根治。

这套“本地 PJ85718DM 直连 + 远程 RS-485 汇入 dsPIC33EP512MU814”的方案,从原理到驱动再到调试,踩过一遍下来最大的体会是:温度采集看似简单,但每一环的数据质量都会直接传导到控制效果上。把传感器硬件选对、总线拓扑搞对、数据状态判断做扎实,整套系统才能在有干扰、有故障的真实工况里站得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询