多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于PJ85718DM与STM32F303RC的嵌入式温度监测系统设计与实现

基于PJ85718DM与STM32F303RC的嵌入式温度监测系统设计与实现 1. 项目缘起与整体设计思路嵌入式温度监测这个方向看起来简单实际上坑特别多。我最早接触这类需求是在一个暖通空调控制器的改造项目上当时的要求很明确本地要能实时看到机房回风温度远程中控室也要同步拿到数据而且两路数据不能互相干扰。最开始想用单颗数字温度芯片搞定结果发现本地显示和远程传输对采样率、通信接口、抗干扰能力的要求完全不一样硬凑在一起反而把系统搞得很脆弱。后来我把方案拆成了两条独立又协同的链路本地温度采集用 PJ85718DM 这颗红外温度传感器远程温度监测则依托 STM32F303RC 的片上外设和通信资源来搭建。这样分工的好处是本地测量专注在“准”和“快”上远程传输专注在“稳”和“远”上各司其职互不拖累。PJ85718DM 是一颗数字输出的红外温度传感器支持 I2C 接口内部集成了热电堆传感单元和信号调理电路出厂时做了温度校准读数直接就是摄氏度不需要外部再做冷端补偿。它的视场角比较窄适合做点式非接触测量比如风管表面温度、换热器翅片温度这类场景。而 STM32F303RC 是带 FPU 的 Cortex-M4 内核主频 72MHz片上资源对于温度监测这种任务来说绰绰有余关键是它的定时器、ADC、USART、I2C 这些外设组合起来刚好能把本地采集和远程上报串成一条完整的链路。整个系统的设计思路可以概括为三层感知层负责把温度物理量变成数字量处理层负责数据滤波、单位换算、阈值判断通信层负责把处理后的数据打包送到远端。三层之间通过明确的数据结构解耦任何一层出问题都不会把整个系统拖死。这个思路在后面调试阶段帮了大忙因为你可以单独测试每一层快速定位故障点。注意红外温度传感器测的是目标表面的辐射温度不是空气温度。如果你要测气温得让传感器对着一个经过气流充分冲刷的黑色薄片或者直接选接触式方案。这一点在 HVAC 场景里特别容易搞混。2. 核心器件选型与关键参数解析2.1 PJ85718DM 的测温原理与接口特性PJ85718DM 的核心是一个热电堆传感器它把目标物体发出的红外辐射转换成微弱的电压信号内部再经过低噪声放大、模数转换和线性化处理最终通过 I2C 输出数字温度值。它的测温范围覆盖 -20°C 到 120°C在这个区间内精度可以做到 ±0.5°C 左右分辨率是 0.02°C。这个精度对于 HVAC 应用来说完全够用因为暖通系统里温度控制的目标精度通常也就是 ±0.5°C 到 ±1°C。I2C 接口的好处是接线简单两根线就能挂多个器件而且 STM32F303RC 的 I2C 外设支持标准模式 100kHz 和快速模式 400kHz跟 PJ85718DM 的通信速率完全匹配。实际用的时候我建议跑在 100kHz因为红外传感器的内部转换时间大概在 100ms 量级通信速率再高也没意义反而增加总线上的噪声耦合风险。这颗传感器的视场角是 35 度左右意味着在 10cm 距离上测量光斑直径大约是 6cm。这个光斑大小决定了你安装时要注意什么如果被测目标比光斑小读数就会被背景辐射污染。我在一个风管温度监测项目里就吃过这个亏传感器对着一个细铜管测结果读数一直偏高后来加了一个遮光筒把视场限制住才解决。2.2 STM32F303RC 的资源分配与通信规划STM32F303RC 有 256KB Flash 和 48KB RAM对于温度监测这种任务来说资源非常宽裕。我一般会把程序分成三个任务I2C 采集任务、数据处理任务、通信上报任务。这三个任务可以用裸机的前后台架构跑也可以用 FreeRTOS 做任务调度。如果系统里还有其他控制逻辑建议上 RTOS因为温度采集的实时性要求不高但通信上报的时序要求比较严格用 RTOS 可以更好地保证优先级。外设分配上I2C1 用来接 PJ85718DMUSART2 用来做远程通信TIM2 用来做采集周期的定时触发。USART2 的波特率我通常设 115200这个速率在工业现场的抗干扰能力和传输距离之间比较平衡。如果你需要更远的传输距离可以在 USART 后面加一颗 RS485 收发器用差分信号传输几百米没问题。ADC 在这个方案里不是必须的但如果你还想监测本地空气温度可以外接一个 NTC 热敏电阻分压后接到 ADC 通道上。STM32F303RC 的 ADC 是 12 位的采样率最高 5Msps测热敏电阻这种慢变量绰绰有余。我一般会用 ADC1 的通道 1 接分压电路然后在软件里做查表或者用 Steinhart-Hart 公式换算成温度。2.3 本地与远程测温的差异化设计本地测温的核心诉求是“看得见、反应快”。PJ85718DM 的采样周期我设在 200ms这个速度对于人眼观察来说已经足够流畅同时也不会让 I2C 总线太忙。本地显示可以用 OLED 或者段码屏通过 I2C 或者 SPI 接口接在 STM32 上。如果只是做调试用直接通过 USART 打印到串口助手也行。远程测温的核心诉求是“传得远、不丢包”。我在通信协议上做了一个简单的帧结构帧头 0xAA 0x55后面跟设备地址、温度值高字节、温度值低字节、校验和。校验和用累加和取反计算量小检错能力对于这种短帧来说够用。每帧数据 8 个字节115200 波特率下传输一帧不到 1ms即使每秒上报 10 次总线占用率也很低。本地和远程的数据流是并行处理的但共享同一个温度值变量。我用了一个双缓冲机制I2C 中断里把新采集的温度值写入缓冲区 A主循环里从缓冲区 A 读取并更新到全局变量同时把全局变量写入缓冲区 B 供通信任务使用。这样即使通信任务偶尔被阻塞也不会影响采集任务的实时性。3. 硬件连接与电路设计要点3.1 PJ85718DM 的外围电路PJ85718DM 的典型应用电路很简单VCC 接 3.3VGND 接地SDA 和 SCL 分别接 STM32 的 I2C 引脚另外 SDA 和 SCL 各需要一颗 4.7kΩ 的上拉电阻到 3.3V。这个上拉电阻的阻值不是随便选的阻值太小总线电容充电太快上升沿过冲阻值太大上升沿变缓高速通信时容易误码。4.7kΩ 是 100kHz 下的经典值如果你跑 400kHz可以降到 2.2kΩ。电源去耦也很关键。我在 VCC 和 GND 之间并了一颗 100nF 的陶瓷电容和一颗 10μF 的钽电容前者滤高频噪声后者提供瞬态电流。红外传感器对电源纹波比较敏感如果电源不干净读数会跳。实测下来加了这两颗电容之后读数的峰峰值噪声从 0.3°C 降到了 0.05°C 以内。注意PJ85718DM 的 SDA 和 SCL 引脚是开漏输出必须加上拉电阻才能正常工作。如果你直接接到 STM32 的 I2C 引脚上而不加上拉总线会一直处于低电平通信根本起不来。这个坑我见过不止一个新手踩过。3.2 STM32F303RC 的最小系统与调试接口STM32F303RC 的最小系统包括3.3V 稳压电路、8MHz 晶振用于 PLL 倍频到 72MHz、复位电路、BOOT 选择电路。稳压芯片我一般用 AMS1117-3.3输入 5V输出 3.3V最大电流 800mA带 STM32 和几个传感器绰绰有余。晶振的负载电容根据晶振规格书选通常是 20pF 左右但实际值需要根据 PCB 走线电容微调。调试接口用 SWD只需要 SWDIO、SWCLK、GND 三根线就能下载和调试。我习惯在 PCB 上留一个 4 针的排针除了这三根线再加一根 3.3V方便给调试器供电。SWD 接口的好处是占用的引脚少而且 STM32F303RC 的 SWD 引脚和 GPIO 是复用的不调试的时候可以当普通 IO 用。USART2 的引脚是 PA2TX和 PA3RX我一般会在这两个引脚上各串一颗 100Ω 的电阻然后再接到外部连接器上。这颗电阻的作用是限流保护万一外部接错线或者短路不至于把 STM32 的引脚烧掉。虽然 STM32 的 IO 口有一定的过流保护但加一颗电阻成本几乎为零可靠性提升很明显。3.3 电源与抗干扰设计HVAC 现场的环境比较恶劣电机启停、继电器动作都会在电源线上产生尖峰干扰。我在电源入口处加了一颗 TVS 管和一颗共模电感TVS 管用来钳位浪涌电压共模电感用来抑制共模噪声。这两个器件加起来不到两块钱但能显著降低系统死机的概率。PCB 布局上模拟部分和数字部分要分开。PJ85718DM 虽然输出的是数字信号但它的内部模拟前端对噪声很敏感所以我在 PCB 上把传感器的地线和 STM32 的地线分开走最后在电源入口处单点汇合。这个做法叫“单点接地”可以避免数字地上的噪声电流流过模拟地影响传感器读数。通信线如果走的是 RS485建议用双绞线并且 A、B 线要尽量靠近走减少差模干扰。如果传输距离超过 100 米最好在末端加一个 120Ω 的终端电阻匹配线缆特性阻抗减少反射。这些细节在实验室里可能看不出差别但到了现场就是稳定和不稳定的分界线。4. 软件架构与核心代码实现4.1 I2C 驱动与温度读取流程STM32F303RC 的 I2C 外设用 HAL 库驱动比较方便但 HAL 库的 I2C 函数在异常情况下容易卡死所以我一般会加一个超时机制。具体做法是在调用HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive时传入超时参数比如 100ms如果超时了就重新初始化 I2C 外设。这个做法虽然粗暴但在现场环境下非常有效因为 I2C 总线被拉死的情况并不罕见。读取 PJ85718DM 的流程是先发送从机地址和寄存器地址然后重启 I2C 总线发送读命令读取两个字节的数据。第一个字节是温度值的高字节第二个字节是低字节组合起来是一个 16 位有符号整数单位是 0.02°C。换算成摄氏度就是temperature raw_value * 0.02。#define PJ85718DM_ADDR 0x5A 1 #define PJ85718DM_TEMP_REG 0x07 float read_pj85718dm_temperature(I2C_HandleTypeDef *hi2c) { uint8_t reg PJ85718DM_TEMP_REG; uint8_t data[2]; int16_t raw; if (HAL_I2C_Master_Transmit(hi2c, PJ85718DM_ADDR, reg, 1, 100) ! HAL_OK) { HAL_I2C_DeInit(hi2c); HAL_I2C_Init(hi2c); return -273.15f; } if (HAL_I2C_Master_Receive(hi2c, PJ85718DM_ADDR, data, 2, 100) ! HAL_OK) { HAL_I2C_DeInit(hi2c); HAL_I2C_Init(hi2c); return -273.15f; } raw (int16_t)((data[0] 8) | data[1]); return raw * 0.02f; }这段代码里返回 -273.15°C 表示读取失败因为绝对零度在物理上不可能达到所以这个值可以安全地作为错误标志。实际用的时候我会在调用这个函数之后判断一下如果返回值小于 -100°C就认为读取失败使用上一次的有效值。4.2 定时采集与数据滤波采集周期用 TIM2 来定时配置成 200ms 中断一次。在中断服务函数里置一个标志位主循环检测到这个标志位就去读传感器。这样做的好处是采集周期由硬件定时器保证不受主循环其他任务的影响。读回来的原始数据不能直接用因为红外传感器容易受到环境温度波动和电磁干扰的影响读数会有毛刺。我一般用滑动平均滤波窗口大小取 8。具体做法是维护一个长度为 8 的环形缓冲区每次新数据进来就替换最老的数据然后求平均值。这个滤波算法计算量小对周期性噪声的抑制效果很好。#define FILTER_WINDOW 8 float temperature_buffer[FILTER_WINDOW]; uint8_t buffer_index 0; float filter_temperature(float new_value) { float sum 0; temperature_buffer[buffer_index] new_value; buffer_index (buffer_index 1) % FILTER_WINDOW; for (int i 0; i FILTER_WINDOW; i) { sum temperature_buffer[i]; } return sum / FILTER_WINDOW; }滑动平均的窗口大小需要根据实际噪声情况调整。窗口越大滤波效果越好但响应速度越慢。200ms 采集周期下8 点滑动平均的响应时间大约是 1.6 秒对于 HVAC 这种慢过程来说完全够用。如果你需要更快的响应可以把窗口降到 4但噪声会稍微大一点。4.3 远程通信协议与数据打包远程通信我用的是自定义的简单协议帧结构如下字节位置内容说明00xAA帧头110x55帧头22设备地址0x01~0xFE3温度高字节温度值整数部分4温度低字节温度值小数部分5校验和字节2~4累加和取反60x0D帧尾170x0A帧尾2温度值的编码方式我做了简化高字节存整数部分低字节存小数部分乘以 100。比如 25.6°C高字节是 25低字节是 60。这样接收端解析起来很直观不需要做浮点数运算。校验和用累加和取反接收端收到后重新计算一遍如果对不上就丢弃这一帧。发送的时候用HAL_UART_Transmit阻塞发送因为一帧只有 8 个字节115200 波特率下发送时间不到 1ms对主循环的影响可以忽略。如果你用的是 RTOS也可以放到一个独立的发送任务里通过队列传递数据。void send_temperature_frame(float temperature) { uint8_t frame[8]; int16_t temp_int (int16_t)temperature; int16_t temp_frac (int16_t)((temperature - temp_int) * 100); frame[0] 0xAA; frame[1] 0x55; frame[2] DEVICE_ADDRESS; frame[3] (uint8_t)temp_int; frame[4] (uint8_t)temp_frac; frame[5] ~(frame[2] frame[3] frame[4]); frame[6] 0x0D; frame[7] 0x0A; HAL_UART_Transmit(huart2, frame, 8, 100); }5. 本地显示与远程监控的协同5.1 本地 OLED 显示的实现本地显示我用的是 0.96 寸的 OLED 屏I2C 接口分辨率为 128x64。这颗屏的驱动芯片通常是 SSD1306网上有现成的驱动库移植过来就能用。显示内容我一般分三行第一行显示当前温度值第二行显示温度单位第三行显示通信状态。刷新频率不用太高500ms 刷一次就够了因为温度变化本身就很慢刷太快反而浪费 CPU 资源。OLED 的 I2C 地址和 PJ85718DM 不能冲突SSD1306 的默认地址是 0x3CPJ85718DM 是 0x5A两者不冲突可以挂在同一条 I2C 总线上。注意OLED 和温度传感器挂同一条 I2C 总线时总线的电容负载会增加。如果发现通信不稳定可以适当减小上拉电阻的阻值比如从 4.7kΩ 降到 2.2kΩ。另外OLED 刷新的时候电流波动比较大建议在它的 VCC 引脚旁边就近放一颗 10μF 的电容。5.2 远程数据接收与可视化远程端我用的是另一块 STM32 板子做接收通过 USART 接收数据帧解析出温度值后再通过 USB 转串口送到电脑上用串口助手或者自己写的小工具做可视化。如果你需要接入 SCADA 系统可以在接收端加一个 Modbus RTU 协议转换把温度值映射到保持寄存器里这样上位机就能用标准的 Modbus 协议来读取了。Modbus RTU 的帧格式比自定义协议复杂一些但好处是通用性强很多组态软件都原生支持。转换的时候只需要把温度值乘以 10 变成整数存到寄存器里上位机读出来再除以 10 就是实际温度。这个做法在工业现场非常常见因为 Modbus 的可靠性经过了长期验证。5.3 数据同步与异常处理本地显示和远程上报的数据必须一致否则操作员会困惑。我在软件里做了一个简单的同步机制每次采集到新数据并滤波后先更新本地显示缓冲区再更新远程发送缓冲区两个缓冲区共用同一个温度值变量。这样只要采集任务正常本地和远程的数据就是一致的。异常处理方面我定义了三种状态正常、传感器故障、通信故障。传感器故障的判断依据是连续 5 次读取失败通信故障的判断依据是连续 10 帧发送失败。一旦进入故障状态本地显示会闪烁提示远程端也会收到一个特殊的故障帧。故障恢复后自动回到正常状态不需要人工复位。6. 常见问题与排查技巧实录6.1 温度读数跳变或偏差大这是最常见的问题原因通常有三个电源噪声、视场污染、环境温度突变。排查的时候先看电源用示波器测一下传感器 VCC 引脚上的纹波如果峰峰值超过 50mV就要加强滤波。然后检查视场看看传感器镜头有没有灰尘或者水汽用无水酒精棉签轻轻擦拭。最后看环境温度如果传感器本身被阳光直射或者靠近热源读数也会偏高需要加遮光罩或者调整安装位置。我遇到过一个案例传感器读数每隔几秒就跳一下后来发现是旁边的一个继电器动作时产生的电磁干扰。解决方法是在继电器线圈两端加一颗续流二极管同时在传感器的电源线上加一颗磁珠。这两个措施加起来成本不到一块钱但问题彻底解决了。6.2 I2C 通信失败或总线锁死I2C 总线锁死通常是因为从机在通信过程中被复位导致 SDA 线被拉低。解决方法是在初始化 I2C 之前先把 SCL 引脚配置成推挽输出发送 9 个时钟脉冲然后再配置成 I2C 模式。这 9 个脉冲可以把从机的移位寄存器清空释放 SDA 线。void i2c_bus_recovery(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); }这段代码在 I2C 初始化失败的时候调用一次实测下来能解决 90% 以上的总线锁死问题。剩下的 10% 通常是硬件问题比如上拉电阻没焊或者引脚短路那就得查 PCB 了。6.3 远程通信丢包或误码远程通信丢包的原因比较多常见的有波特率不匹配、地线环路、线缆过长、终端电阻缺失。排查的时候先用示波器看波形如果波形畸变严重说明线缆太长或者终端电阻不对。如果波形正常但误码率高检查一下两端的波特率是否一致以及地线是否共地。我在一个项目里遇到过 RS485 通信时好时坏的问题后来发现是 A、B 线接反了。RS485 的 A 线应该接对方的 A 线B 线接 B 线如果接反了空闲状态下总线电平是反的通信就会时断时续。这个错误很隐蔽因为有时候居然能通信只是误码率高很容易被忽略。6.4 常见问题速查表现象可能原因排查方法解决措施温度读数跳变电源噪声示波器测 VCC 纹波加去耦电容、磁珠温度读数偏高视场污染检查镜头是否干净清洁镜头、加遮光筒I2C 通信失败总线锁死测 SDA、SCL 电平执行总线恢复程序远程丢包波特率不匹配核对两端配置统一波特率远程误码线缆过长测波形畸变加终端电阻、缩短线缆系统死机电源浪涌测电源入口电压加 TVS 管、共模电感7. 实操心得与避坑经验7.1 传感器安装位置的选择红外温度传感器的安装位置直接决定了测量精度。我的一般原则是传感器要正对被测目标距离控制在 5cm 到 20cm 之间太近了光斑太小容易受局部影响太远了光斑太大容易混入背景辐射。如果被测目标表面比较光亮比如抛光金属最好在表面贴一块黑色胶带增加发射率否则读数会偏低。在 HVAC 风管里安装的时候传感器要避开风管弯头和阀门因为这些地方气流不稳定温度分布不均匀。我通常会把传感器安装在直管段的中部距离上游弯头至少 5 倍管径的距离。这个位置的气流最稳定测出来的温度最有代表性。7.2 通信线缆的选型与布线RS485 通信线我推荐用屏蔽双绞线屏蔽层单端接地接在接收端。双绞线的作用是抑制差模干扰屏蔽层的作用是抑制共模干扰。如果现场电磁环境特别恶劣还可以用带铠装的线缆机械强度更高但成本也上去了。布线的时候要远离动力线至少保持 30cm 以上的距离。如果必须交叉尽量垂直交叉不要平行走线。平行走线会导致动力线上的噪声耦合到通信线上轻则误码重则烧毁收发器。这个坑我在一个工厂项目里踩过后来重新布线才解决费时费力。7.3 软件看门狗与异常恢复工业现场的环境不可预测软件看门狗是最后一道防线。STM32F303RC 内置了独立看门狗和窗口看门狗我一般用独立看门狗超时时间设 2 秒。主循环里每 500ms 喂一次狗如果程序跑飞了2 秒后自动复位。除了看门狗我还建议加一个软件复位计数器存在备份寄存器里。每次复位后读取这个计数器如果连续复位超过 5 次就进入安全模式只保留最基本的通信功能不再执行控制逻辑。这个机制可以防止程序陷入“复位-跑飞-复位”的死循环给维护人员争取排查时间。7.4 温度校准的实操方法PJ85718DM 出厂时已经校准过了但在高精度应用里还是建议做一次现场校准。校准方法很简单用一个经过计量的标准温度计作为参考把传感器和标准温度计放在同一个恒温环境里等温度稳定后记录两者的读数差把这个差值作为偏移量存在 STM32 的 Flash 里每次读数时减去这个偏移量。校准点的选择也有讲究最好在量程的低端、中端、高端各选一个点比如 0°C、25°C、50°C分别测出偏移量然后用线性插值的方法计算任意温度下的偏移量。这样校准后的精度可以做到 ±0.2°C 以内比出厂精度提升了一倍多。8. 方案扩展与后续优化方向这套方案目前跑在裸机前后台架构上稳定运行了半年多没有出现过死机或者数据丢失。如果后续要扩展我觉得有几个方向值得考虑。第一个是增加无线通信模块把 RS485 换成 LoRa 或者 Zigbee这样就不用布线了安装更灵活。不过无线通信的可靠性受环境影响比较大需要做更多的现场测试。第二个方向是增加数据存储功能在 STM32 上挂一颗 SPI Flash 或者 SD 卡把温度数据按时间戳存下来方便事后分析。这个功能在故障诊断的时候特别有用可以回溯温度变化的趋势判断是传感器问题还是工艺问题。第三个方向是增加多路温度采集用 I2C 多路复用器扩展总线挂多颗 PJ85718DM同时监测多个点的温度。这个在大型 HVAC 系统里很有用比如同时监测送风、回风、新风、排风四个温度计算焓值和能效比。多路复用的关键是地址分配和采集时序需要仔细规划避免总线冲突。我个人在实际操作中的体会是嵌入式温度监测这个方向硬件设计占三分软件设计占七分。硬件只要按规格书来一般不会出大问题软件才是真正体现功力的地方滤波算法、异常处理、通信协议每一个细节都决定了系统在现场能不能稳定运行。多花时间在软件上比反复改硬件划算得多。
返回列表