1. 从一颗温度传感器说起:为什么本地与远程双路监测值得单独做
嵌入式温度监测这件事,看起来简单,真做起来坑不少。我接触过不少 HVAC(暖通空调)和工业控制类的项目,客户最常提的需求就是"帮我测几个点的温度,超了阈值就报警或者联动"。听起来一句话的事,但落到电路板和固件上,涉及的东西远比想象中多:传感器选型、接口协议、本地与远程的区分、采样精度、抗干扰、通信可靠性,每一项都能单独写一篇。
这次要聊的方案,核心是围绕PJ85718DM这颗温度传感芯片,搭配STM32F091RC这颗 ARM Cortex-M0 微控制器,做一套能同时监测本地温度和远程温度的系统。所谓"本地",通常指传感器直接贴在主控板附近,测的是设备自身或机箱内部的温度;"远程"则是传感器通过一段线缆延伸到几米甚至十几米外,测的是另一个物理位置的温度,比如 HVAC 系统里的回风管、出风管、水箱或者室外机。
为什么要把本地和远程放在一套系统里做?因为很多实际场景下,这两个温度是相互关联的。举个例子,一个空调控制柜,本地温度反映的是控制板自身的工作环境,远程温度反映的是被控空间的真实温度。如果只看远程温度,你可能会忽略控制柜内部过热导致的器件漂移;如果只看本地温度,你又不知道实际被控对象的状态。两者结合,才能做出更靠谱的判断逻辑。
STM32F091RC 这颗芯片在这个场景里挺合适。它是 Cortex-M0 内核,主频 48MHz,带 256KB Flash 和 32KB SRAM,外设资源对于温度监测这种任务绰绰有余——多路 ADC、多个定时器、I2C、SPI、UART 都有。关键是它功耗低、成本可控,在 HVAC 这种量大面广的应用里,BOM 成本敏感,选它是有道理的。而 PJ85718DM 作为温度传感前端,负责把物理温度转换成可读的数字或模拟信号,是整个测量链路的起点。
这篇文章我会从硬件接口、通信协议、固件架构、本地与远程的差异化处理、实测踩坑几个角度,把这套方案讲透。不管你是刚接触嵌入式温度采集的新手,还是做过类似项目想找参考的老手,应该都能从中拿到能直接用的东西。
2. PJ85718DM 与 STM32F091RC 的接口设计:选型逻辑与硬件连接细节
2.1 为什么是这两颗芯片的组合
先说说选型背后的逻辑。温度传感器大致分几类:热敏电阻(NTC/PTC)、热电偶、RTD(铂电阻)、以及集成式温度传感 IC。热敏电阻便宜但线性度差、需要校准;热电偶适合高温但需要冷端补偿;RTD 精度高但成本也高、需要激励电流源。集成式温度传感 IC 则是把传感元件和信号调理电路封装在一起,输出数字信号或标准模拟电压,用起来最省心。
PJ85718DM 属于集成式温度传感这一类,它的优势在于输出接口灵活、精度够用、外围电路简单。对于 HVAC 这种温度范围通常在 -40°C 到 +125°C 之间的应用,它的量程和精度都能覆盖。而且它支持远距离布线时的抗干扰设计,这一点对"远程温度"监测至关重要。
STM32F091RC 这边,我选它的理由有三条。第一,它有多路 12 位 ADC,采样速率和分辨率对于温度这种慢变量完全够用,而且可以配置成扫描模式,同时采多路。第二,它有丰富的通信外设,I2C 可以接数字温度传感器,UART 可以做远程通信,SPI 可以接外部 ADC 或存储器。第三,Cortex-M0 的内核虽然简单,但对于温度采集这种任务,中断响应和低功耗模式都够用,而且开发工具链成熟,ST 的 HAL 库和 CubeMX 能省不少事。
提示:选型时不要只看参数表上的"最高精度",要看你的实际应用需不需要那么高。HVAC 场景下 ±0.5°C 的精度通常就够了,追求 ±0.1°C 只会让成本和调试难度飙升。
2.2 本地温度通道的硬件连接
本地温度这一路,因为传感器和 MCU 在同一块板子上,走线短、干扰小,可以用最简单的连接方式。PJ85718DM 如果输出的是模拟电压,直接接到 STM32F091RC 的 ADC 输入引脚即可。这里有几个细节要注意。
第一,ADC 的参考电压要稳。STM32F091RC 的 VREF+ 如果直接接 VDDA,而 VDDA 又和数字电源共用,那数字电路开关噪声会耦合到参考电压上,导致采样值跳动。我的做法是在 VDDA 和 VREF+ 引脚旁边各放一个 100nF 加一个 1uF 的电容,必要时再串一个磁珠隔离。实测下来,这样能把采样噪声压到 1 个 LSB 以内。
第二,模拟输入引脚要配置成模拟模式,关闭内部上拉下拉。这个在 CubeMX 里点一下就行,但很多人忘了,结果引脚内部的上拉电阻和传感器输出形成分压,读数直接偏掉。
第三,如果 PJ85718DM 输出的是数字信号(比如 I2C 或单总线),那本地通道就直接走数字接口,不经过 ADC。数字接口的好处是不受参考电压和模拟噪声影响,但要注意上拉电阻的选取和总线电容。I2C 在 100kHz 下,4.7kΩ 上拉是常见值;如果走 400kHz,上拉要减小到 2.2kΩ 左右,同时总线电容不能超过 400pF。
2.3 远程温度通道的布线策略与抗干扰
远程温度才是这套方案里真正考验设计功力的地方。传感器离 MCU 几米远,线缆上会引入各种干扰:工频噪声、开关电源的尖峰、电机启停的浪涌。如果处理不好,读数跳变、漂移、甚至通信失败都是常事。
我的经验是,远程通道优先选数字接口而不是模拟接口。模拟电压经过长线传输,压降和噪声耦合很难避免;数字信号虽然也有干扰问题,但可以通过差分传输、屏蔽线、协议校验来对抗。如果 PJ85718DM 支持差分输出或者电流环输出,那是最好的,电流信号在长线上的抗干扰能力远强于电压信号。
如果只能用模拟电压传输,那必须做几件事:用屏蔽双绞线,屏蔽层单端接地(接 MCU 侧的地,不要两端都接,否则形成地环路);在传感器输出端加 RC 低通滤波,截止频率根据你的采样率来定,比如采样 10Hz,截止频率设 1Hz 左右;在 MCU 的 ADC 输入端加钳位二极管,防止浪涌打坏引脚。
线缆长度和线径也要考虑。太细的线电阻大,如果传感器是电流输出型,线阻会影响精度;如果是电压输出型,线阻和 ADC 输入阻抗形成分压。一般 0.5mm² 的线,20 米以内问题不大,再长就要算一下压降了。
| 传输方式 | 适用距离 | 抗干扰能力 | 成本 | 推荐场景 |
|---|---|---|---|---|
| 模拟电压 | < 5m | 弱 | 低 | 板内或短距离 |
| 模拟电流 | < 50m | 强 | 中 | 工业现场 |
| I2C 数字 | < 1m | 中 | 低 | 板内多传感器 |
| RS-485 差分 | < 1000m | 很强 | 中高 | 远程多点 |
| 单总线 | < 20m | 中 | 低 | 分布式测温 |
注意:远程通道的采样不要和本地通道用同一个 ADC 扫描序列,最好分开采样、分开滤波。因为远程通道的噪声特性完全不同,用同一套滤波参数会顾此失彼。
3. 固件架构:从 ADC 采样到温度换算的完整链路
3.1 采样时序与多通道调度
STM32F091RC 的 ADC 支持规则通道和注入通道。规则通道适合按顺序扫描多路,注入通道可以打断规则通道做紧急采样。在温度监测里,我一般把本地和远程都放在规则通道里,用定时器触发,固定周期采样。比如用 TIM2 产生 100ms 的触发信号,ADC 每次触发扫描两路,采完产生 DMA 请求,把数据搬到内存数组里。
这样做的好处是采样周期严格由硬件定时器决定,不受程序执行时间影响,数据的时间戳是均匀的。如果你在 while 循环里手动启动 ADC,那采样间隔会随程序负载波动,做趋势分析时会有问题。
DMA 的配置要注意:ADC 的 DMA 请求是每次转换完成触发一次,所以 DMA 的缓冲区大小要等于通道数乘以采样次数。比如两路通道,每次触发采 16 次做平均,那缓冲区就是 32 个半字。DMA 配置成循环模式,半传输和传输完成中断各处理一半数据,这样能实现"乒乓缓冲",采样和处理互不干扰。
3.2 温度换算的数学处理
ADC 读回来的是 0 到 4095 的原始值,要变成温度值,中间要经过几步换算。假设 PJ85718DM 输出的是模拟电压,灵敏度是 10mV/°C,0°C 时输出 500mV,那换算公式是:
// 假设 VREF = 3.3V, ADC 12位 float voltage = (adc_value / 4095.0f) * 3.3f; float temperature = (voltage - 0.5f) / 0.01f;但实际代码里,我建议用定点数或者整数运算,避免浮点开销。Cortex-M0 没有硬件浮点单元,浮点运算是软件模拟的,一次除法可能几百个周期。对于 100ms 一次的采样,这点开销不算什么,但如果采样率提高到 1kHz,浮点就会成为瓶颈。
更好的做法是预先算好系数,用整数乘加:
// 预先计算: temperature = adc_value * K + B // K = (3.3 / 4095.0) / 0.01 = 0.080586 // B = -0.5 / 0.01 = -50 // 放大 1000 倍用整数: K_int = 81, B_int = -50000 int32_t temp_milli = ((int32_t)adc_value * 81 - 50000) / 1000;这样得到的是毫摄氏度,精度足够,而且运算快。注意除法那里,如果编译器支持,除以 1000 会被优化成乘法加移位,比直接浮点快得多。
3.3 滤波策略:本地与远程不能用同一套参数
本地通道因为走线短,噪声主要是高频开关噪声,用简单的滑动平均或者一阶低通就能压住。我一般用 8 点滑动平均,计算量小,效果够用。
远程通道就复杂了。除了高频噪声,还有工频干扰(50Hz 或 60Hz)和低频漂移。滑动平均对工频干扰的抑制取决于采样率和窗口长度的关系,如果采样率是工频的整数倍,那滑动平均能完美抵消工频;如果不是,就会有残余。更稳妥的做法是用中值滤波加滑动平均的组合:先取 5 个点做中值,去掉脉冲干扰,再做 8 点滑动平均,平滑随机噪声。
#define MEDIAN_WIN 5 #define AVG_WIN 8 int32_t remote_filter(int32_t new_sample) { static int32_t median_buf[MEDIAN_WIN] = {0}; static int32_t avg_buf[AVG_WIN] = {0}; static uint8_t idx_m = 0, idx_a = 0; static uint8_t count = 0; // 中值滤波 median_buf[idx_m] = new_sample; idx_m = (idx_m + 1) % MEDIAN_WIN; int32_t sorted[MEDIAN_WIN]; memcpy(sorted, median_buf, sizeof(sorted)); // 简单冒泡排序 for (int i = 0; i < MEDIAN_WIN - 1; i++) { for (int j = 0; j < MEDIAN_WIN - 1 - i; j++) { if (sorted[j] > sorted[j+1]) { int32_t t = sorted[j]; sorted[j] = sorted[j+1]; sorted[j+1] = t; } } } int32_t median = sorted[MEDIAN_WIN / 2]; // 滑动平均 avg_buf[idx_a] = median; idx_a = (idx_a + 1) % AVG_WIN; if (count < AVG_WIN) count++; int64_t sum = 0; for (int i = 0; i < count; i++) sum += avg_buf[i]; return (int32_t)(sum / count); }这段代码在 STM32F091RC 上跑,一次滤波大概几十微秒,完全不影响主循环。
提示:滤波窗口不是越大越好。窗口越大,平滑效果越好,但响应越慢。温度这种慢变量,窗口大一点没关系,但如果你的系统需要快速响应温度突变(比如保护逻辑),那窗口要适当减小,或者用两套滤波结果:一套平滑的用于显示,一套快速的用于报警判断。
4. 本地与远程的差异化处理:不只是距离的区别
4.1 校准策略的分野
本地温度传感器因为和 MCU 在同一环境,校准相对简单。你可以在已知温度下(比如冰水混合物 0°C,或者恒温箱 25°C)读一组数据,算出一个偏移量,写进 Flash 里,上电时加载。单点校准就能把精度提到 ±0.5°C 以内。
远程传感器就麻烦了。线缆电阻、接插件接触电阻、传感器本身的离散性,都会引入误差。而且远程传感器可能安装在难以拆卸的位置,现场校准不现实。我的做法是:出厂前在标准温度下做两点校准(比如 0°C 和 50°C),把斜率和截距都算出来,存进 MCU 的 Flash。现场安装后,如果发现偏差,可以通过通信接口远程下发一个修正值,叠加到原始校准上。
这里有个坑:远程传感器的线缆电阻会随温度变化,铜线的温度系数是 0.4%/°C。如果线缆很长,环境温度变化 30°C,线阻变化 12%,对于电流输出型传感器,这会导致读数漂移。解决办法是用三线制或四线制接法,把线阻的影响抵消掉。如果传感器不支持,那只能在软件里做温度补偿,用本地温度近似线缆温度,算出一个修正系数。
4.2 故障检测与容错
本地通道的故障检测相对直接:如果 ADC 读数一直是 0 或者一直是满量程,那大概率是传感器断线或短路。可以在固件里设一个合理范围,比如 -40°C 到 +125°C 之外的值都判为故障。
远程通道的故障模式更多:线缆断开、接插件氧化、传感器进水、屏蔽层破损引入干扰。除了范围判断,还可以做变化率判断:如果温度在 1 秒内跳变超过 10°C,那肯定是干扰或故障,不是真实温度变化。这时候不要立即报警,而是标记为"可疑",连续几次都这样再确认故障。
#define TEMP_MIN -40000 // -40.000°C #define TEMP_MAX 125000 // 125.000°C #define RATE_LIMIT 10000 // 10°C/s uint8_t check_remote_fault(int32_t temp, int32_t last_temp, uint32_t dt_ms) { if (temp < TEMP_MIN || temp > TEMP_MAX) return 1; // 超范围 int32_t rate = (temp - last_temp) * 1000 / dt_ms; if (rate > RATE_LIMIT || rate < -RATE_LIMIT) return 2; // 变化率异常 return 0; }故障确认后,系统要有一个降级策略。比如远程温度失效时,自动切换到本地温度作为控制依据,同时上报故障码。不要让整个系统因为一个传感器故障就停机,这在 HVAC 里是不可接受的。
4.3 通信上报的数据组织
本地和远程温度采集完之后,怎么上报给上位机或云端,也有讲究。我一般用 Modbus RTU 或者自定义的二进制协议。Modbus 的好处是通用,组态软件直接支持;自定义协议的好处是紧凑,适合低带宽场景。
不管用哪种,数据组织上我建议把本地温度、远程温度、状态字、时间戳打包成一帧。状态字里用位域表示每个通道的健康状态、是否在校准模式、是否有未确认的报警。这样上位机拿到一帧数据就能知道全部信息,不用再发查询命令。
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2字节 | 固定 0xAA55 |
| 本地温度 | 4字节 | 毫摄氏度,有符号 |
| 远程温度 | 4字节 | 毫摄氏度,有符号 |
| 状态字 | 2字节 | 位0-1本地状态,位2-3远程状态 |
| 时间戳 | 4字节 | 上电后的毫秒数 |
| CRC16 | 2字节 | 校验 |
帧长度固定 18 字节,用 DMA 加 UART 空闲中断发送,不占用 CPU 时间。
5. 实测中的那些坑:从噪声到温漂的完整排查记录
5.1 远程通道读数跳变的排查过程
项目第一次联调时,本地温度很稳,波动不超过 0.1°C,但远程温度一直在跳,幅度有 2-3°C。这种问题不能靠猜,得一步步排查。
第一步,我先用示波器看远程传感器输出端的波形。发现波形上叠加了明显的 50Hz 正弦,幅度大概 20mV。这说明工频干扰耦合进来了。耦合途径可能是空间电磁场,也可能是地环路。
第二步,我把传感器和 MCU 之间的连线换成屏蔽双绞线,屏蔽层接 MCU 侧的地。再测,50Hz 幅度降到 5mV 左右,但还有。说明空间耦合被屏蔽了大半,但地环路还在。
第三步,我检查了两端的地连接。发现传感器侧的地和 MCU 侧的地通过线缆里的地线连在一起,而两个设备又各自接了大地,形成了地环路。解决办法是在传感器侧做隔离,用一个小型的隔离放大器或者光耦。加上隔离后,50Hz 彻底消失,读数稳定在 ±0.2°C 以内。
这个排查过程告诉我,远程温度监测里,地环路是最隐蔽也最麻烦的问题。如果你的系统里两个设备都接了大地,那一定要考虑隔离。
5.2 本地通道的温漂问题
本地通道虽然稳,但有个问题:开机后半小时内,读数会慢慢漂移 1°C 左右,之后才稳定。一开始我以为是传感器的问题,换了几颗都一样。后来才想明白,是 MCU 自身的发热导致的。
STM32F091RC 在 48MHz 全速运行时,功耗大概几十毫安,芯片表面温度会比环境高几度。而本地传感器如果离 MCU 太近,测到的就是 MCU 的表面温度,不是环境温度。开机后 MCU 温度逐渐上升,传感器读数也跟着漂,直到热平衡。
解决办法有两个:一是把本地传感器远离 MCU,放在板子边缘或者用排线引出;二是在固件里做温度补偿,用 MCU 的内部温度传感器读芯片温度,算出一个修正值。我选了第一种,因为更直接,不增加软件复杂度。把传感器移到板边后,漂移从 1°C 降到 0.2°C 以内。
注意:如果你用 MCU 内部温度传感器做补偿,要知道它的精度很差,通常只有 ±1.5°C,只能用来做趋势判断,不能做精确补偿。
5.3 ADC 参考电压波动导致的批量偏差
小批量试产时,发现不同板子之间的温度读数有系统性偏差,有的偏高 0.5°C,有的偏低 0.3°C。这个偏差不是随机的,而是每块板子固定。排查后发现是 VREF+ 的实际电压和标称 3.3V 有差异。
STM32F091RC 的 VREF+ 如果直接接 VDDA,而 VDDA 来自 LDO,LDO 的输出精度通常是 ±2%,3.3V 的话就是 ±66mV。反映到温度上,就是 ±0.66°C 的偏差。这个偏差对于要求 ±0.5°C 精度的应用是不可接受的。
解决办法是用高精度基准源,比如 2.5V 或 3.0V 的基准芯片,精度 ±0.1%。或者,在出厂校准环节,用高精度万用表测出每块板子的实际 VREF+ 电压,写进 Flash,固件里用这个实测值做换算。我选了后者,因为不增加 BOM 成本,只是产线多一道工序。
// 从 Flash 读取校准的 VREF 值(单位 mV) uint16_t vref_mv = *(uint16_t*)VREF_CAL_ADDR; float voltage = (adc_value / 4095.0f) * (vref_mv / 1000.0f);这样处理后,板间偏差从 ±0.66°C 降到 ±0.1°C 以内。
5.4 通信超时与数据丢失的处理
系统跑起来后,偶尔会出现上位机收不到数据的情况。查下来是 UART 发送时如果正好遇到高优先级中断,DMA 传输会被打断,导致帧不完整。虽然概率很低,但一天出现几次,对于监控系统来说不可接受。
解决办法是给 UART 发送加一个软件队列,主循环把要发的帧放进队列,DMA 发送完成中断里从队列取下一帧。这样即使某一帧发送被延迟,也不会丢失,只是延后一点。同时,在帧里加序号,上位机发现序号不连续就知道有丢帧,可以请求重发。
#define TX_QUEUE_SIZE 8 typedef struct { uint8_t data[18]; uint8_t len; } tx_frame_t; tx_frame_t tx_queue[TX_QUEUE_SIZE]; volatile uint8_t tx_head = 0, tx_tail = 0; void uart_send_frame(uint8_t *data, uint8_t len) { uint8_t next = (tx_head + 1) % TX_QUEUE_SIZE; if (next == tx_tail) return; // 队列满,丢弃 memcpy(tx_queue[tx_head].data, data, len); tx_queue[tx_head].len = len; tx_head = next; if (!dma_busy) start_dma_tx(); }这个队列机制加上后,连续跑了一周,没有再出现丢帧。
6. 把这套方案用起来:几个可以直接抄的配置要点
6.1 CubeMX 里的关键配置
如果你用 STM32CubeMX 生成初始化代码,有几个地方要特别注意。ADC 配置里,Resolution 选 12 位,Scan Conversion Mode 使能,Continuous Conversion Mode 关闭(我们用定时器触发),DMA Continuous Requests 使能。定时器配置成 Update Event 触发 ADC,触发频率根据你的采样需求定,我一般用 100Hz 触发,然后软件做 10 次平均,得到 10Hz 的有效数据率。
UART 配置里,波特率 115200,8 数据位,1 停止位,无校验。DMA 发送使能,发送完成中断使能。NVIC 里把 UART 中断优先级设得比定时器低,避免通信打断采样。
6.2 低功耗模式的取舍
HVAC 设备很多是电池供电或者要求低功耗。STM32F091RC 支持 Sleep、Stop、Standby 三种低功耗模式。温度监测这种应用,如果采样率要求不高(比如 1 秒一次),可以用 Stop 模式,定时器唤醒,采样完继续睡。这样平均电流能降到几十微安。
但要注意,Stop 模式下 ADC 和 DMA 都停了,唤醒后要重新初始化。而且 Stop 模式下 UART 也停了,如果上位机这时候发命令,会丢失。所以低功耗和通信实时性要权衡。我的做法是:正常模式 100ms 采样一次,如果 5 分钟没有通信,自动进入 Stop 模式,1 秒采样一次;一旦收到通信请求,立即唤醒回正常模式。
6.3 产线校准的流程设计
前面提到出厂校准,这里说一下具体流程。产线需要一台恒温箱,能稳定在 0°C 和 50°C。每块板子先放 0°C 环境,等热平衡后(大概 10 分钟),通过 UART 发送校准命令,固件记录此时的 ADC 值。然后升到 50°C,再记录一次。用这两组数据算出斜率和截距,写进 Flash。
校准命令要加保护,比如需要先发送一个解锁序列,防止误触发。校准数据写 Flash 后要回读验证,确保写入成功。整个流程自动化,操作员只需要放板子、按开始、等结果。
typedef struct { int32_t adc_at_0c; int32_t adc_at_50c; uint16_t vref_mv; uint16_t crc; } cal_data_t; void save_calibration(cal_data_t *cal) { cal->crc = crc16((uint8_t*)cal, sizeof(cal_data_t) - 2); flash_unlock(); flash_erase_page(CAL_PAGE_ADDR); flash_write(CAL_PAGE_ADDR, (uint8_t*)cal, sizeof(cal_data_t)); flash_lock(); }6.4 现场安装的注意事项
最后说几个现场安装的经验。远程传感器的线缆不要和动力线走同一个线槽,如果必须交叉,要垂直交叉,不要平行走。传感器探头要完全浸入被测介质,如果测管道温度,要保证探头和管壁紧密接触,必要时涂导热硅脂。户外安装要做好防水,接线盒朝下,防止积水。
本地传感器如果装在控制柜里,要注意柜内空气流通,不要被其他发热器件烤着。如果柜内有变频器,传感器要远离,至少 20cm 以上,否则变频器的开关噪声会耦合进来。
这套 PJ85718DM 加 STM32F091RC 的方案,我从原型到小批量跑了大概半年,中间踩的坑基本都写在这里了。温度监测看起来简单,但要做到稳定可靠,细节非常多。希望这些经验能帮你少走点弯路。如果你也在做类似的项目,欢迎交流。