1. 项目缘起与整体设计思路嵌入式温度监测这个方向看起来简单实际上坑特别多。我最早接触这类需求是在一个 HVAC暖通空调控制板的项目里当时的需求很朴素板子上要同时测本地环境温度和一路远程探头温度数据要能上传到上位机还要在本地做简单的阈值报警。选型的时候我试过几种方案最后落在了PJ85718DM这颗 I2C 接口的数字温度传感器搭配STM32F030RC这颗性价比极高的 Cortex-M0 主控上。这套组合我前后在三个不同的板子上用过稳定性确实可以所以把整套设计思路和踩过的坑整理出来。先说这套方案到底解决什么问题。HVAC 场景里温度监测通常有两个诉求一个是本地温度也就是控制板所在位置的温度用来做板级补偿或者机箱内环境判断另一个是远程温度探头往往要拉到几米甚至十几米外比如风管里、水箱里、房间角落。这两路温度的量程、精度、响应速度要求都不一样但都需要主控统一采集、统一处理。STM32F030RC 有 12 位 ADC、多个定时器、I2C 和 UART 外设资源上完全够用而且价格便宜适合这种量大面广的控制器产品。为什么本地温度选 PJ85718DM 而不是常见的模拟 NTC 或者 DS18B20这里有几个考量。PJ85718DM 是一颗 I2C 接口的数字温度传感器出厂已经校准直接输出数字量省掉了 ADC 采样和校准的麻烦。它的测温范围覆盖 -40 到 125 摄氏度精度在常温段能做到正负 0.5 摄氏度以内对于 HVAC 这种不需要医疗级精度的场景绰绰有余。更关键的是 I2C 接口可以挂多颗地址可配本地和远程如果都用同款一条总线就能搞定布线非常干净。远程温度这块如果距离超过两三米I2C 直接拉线就不太靠谱了总线电容会上去信号完整性变差。我的做法是远程探头用一颗带数字输出的传感器可以是同系列或者带更长线驱动的型号通过差分或者缓冲的方式把信号引回来或者干脆在远端放一颗小 MCU 做预处理再用 UART 或者 RS-485 把数据传回来。这个项目里为了控制成本远程那路我用的是模拟探头加运放缓冲回到 STM32F030RC 的 ADC 通道上采样本地那路走 I2C 读 PJ85718DM。这样两路温度在同一个主控里汇合统一做滤波、标定和上报。整体架构上STM32F030RC 跑一个轻量的前后台程序主循环里定时触发两路采集本地温度走 I2C 读寄存器远程温度走 ADC 加 DMA 采样采完做滑动平均滤波然后通过 UART 按自定义协议打包发给上位机或者网关。报警逻辑放在本地超过阈值就拉一个 GPIO 去驱动蜂鸣器或者继电器。整个方案没有跑 RTOS因为任务足够简单前后台加定时器中断完全够用代码量小维护起来也轻松。提示选型阶段一定要先确认供电和电平。PJ85718DM 常见是 3.3V 供电STM32F030RC 也是 3.3VI2C 电平天然匹配省掉电平转换芯片。如果主控是 5V 系统那就必须加电平转换否则容易烧传感器。这套设计的好处是模块化清晰本地温度、远程温度、主控、通信四块解耦任何一块出问题都好定位。而且成本压得很低STM32F030RC 加上一颗数字温度传感器BOM 成本控制得很好适合批量产品。下面我把每个环节拆开讲包括硬件连接、寄存器配置、采样滤波、通信协议以及我在实际调试中遇到的各种问题。2. 核心器件解析与硬件连接要点2.1 PJ85718DM 的关键特性与寄存器操作PJ85718DM 这颗传感器用起来最舒服的地方就是它把温度转换、校准、数字输出全做在片内了。它内部有一个带隙温度传感单元配合 ADC 和校准系数直接给出数字温度值。通信走标准 I2C支持 100kHz 和 400kHz 两种速率7 位地址可以通过引脚配置成多个值方便一条总线上挂多颗。它的温度寄存器通常是 16 位高字节是整数部分低字节是小数部分分辨率可以到 0.0625 摄氏度。实际读的时候我一般只取高字节加低字节的高 4 位这样精度到 0.0625 度已经足够剩下的位是噪声取了反而要额外滤波。转换时间方面单次转换大概几十毫秒连续转换模式下会更快具体要看配置寄存器的设置。配置寄存器里几个关键位要特别注意一个是分辨率设置位决定转换时间和精度HVAC 场景我一般设成 12 位转换时间约 60ms精度 0.0625 度够用另一个是工作模式位可以设成连续转换或者单次转换我倾向单次转换主循环里按需触发省电而且时序可控还有报警功能位PJ85718DM 支持温度超限报警可以配置上下限超限时有个报警引脚拉低这个功能在 HVAC 里很实用可以直接硬件联动不用主控一直轮询。读温度的流程是这样的先发一个写命令指定温度寄存器地址然后发一个读命令连续读两个字节。这里有个细节I2C 读操作中间要发一个重复起始条件Repeated Start不能先停止再起始否则有些传感器会复位内部指针。STM32 的 HAL 库用HAL_I2C_Mem_Read就能自动处理这个时序比较省心。注意PJ85718DM 上电后需要一点稳定时间datasheet 里写的是典型 1ms 到 5ms我实测下来给 10ms 比较保险。如果上电立刻读偶尔会读到 0 或者 0xFFFF 这种无效值。2.2 STM32F030RC 的资源分配与外设规划STM32F030RC 是 Cortex-M0 内核主频 48MHzFlash 256KBRAM 32KB对于这个项目来说资源很充裕。我一般这样分配外设I2C1 用来接 PJ85718DM跑 400kHzADC1 的通道 0 用来采远程温度配合 DMA 做连续采样USART1 做通信115200 波特率TIM3 做 1ms 系统滴答用来做软定时和超时判断另外留一个 GPIO 做报警输出一个 GPIO 做状态指示。时钟配置上我用的是外部 8MHz 晶振PLL 倍频到 48MHz。这里有个坑STM32F030 的 I2C 时钟源是从 PCLK 分频来的如果 PCLK 设成 48MHzI2C 的时钟分频寄存器要算准否则实际速率会偏。我一般把 I2C 设成 400kHz分频系数按公式算I2C_CK PCLK / (2 * (CCR 1))反推 CCR 值。实测下来CCR 设成 0x1E 左右能跑到接近 400kHz具体要看 PCLK 实际值。ADC 这块远程温度用的是模拟探头输出 0 到 3.3V 对应某个温度范围。STM32F030RC 的 ADC 是 12 位参考电压用 VDDA采样时间我设成 71.5 个周期这样对高阻抗信号源更友好。DMA 配置成循环模式一次采 16 个点采完触发中断在中断里做平均。这样 CPU 占用很低主循环可以专心处理 I2C 和通信。GPIO 分配上I2C 的 SCL 和 SDA 要配上拉电阻一般 4.7k 到 10k 都行我习惯用 4.7k上升沿更陡400kHz 下波形更干净。报警输出用推挽输出直接驱动小蜂鸣器或者光耦。状态指示用一个 LED闪烁频率表示系统状态调试的时候特别有用。2.3 本地与远程温度的硬件连接差异本地温度这块简单PJ85718DM 直接贴在主控板附近I2C 走线短干扰小。但要注意传感器不要靠近发热元件比如 LDO、功率 MOS 这些否则测的是板子局部温度不是环境温度。我一般把传感器放在板边远离电源区域底下铺铜做热隔离这样测出来的值更接近真实环境。远程温度就麻烦一些。如果距离在 1 米以内I2C 直接拉过去还能凑合但超过 2 米总线电容会超过 400pF 的规范上限波形会变圆通信容易出错。我的做法是远程用模拟探头比如 PT100 或者热敏电阻通过运放做缓冲和放大再用双绞线把模拟信号传回来。双绞线的好处是共模干扰抑制好配合运放的差分输入几米距离没问题。模拟探头选型上PT100 线性度好但需要恒流源激励电路复杂一点NTC 热敏电阻便宜但非线性严重需要查表或者拟合。HVAC 场景我一般用 NTC因为成本低而且温度范围窄非线性可以用分段线性拟合搞定。运放用轨到轨的比如常见的通用型号单电源 3.3V 供电输出直接进 ADC。提示远程模拟信号走线一定要远离强电和开关电源的走线最好用地线包起来。我踩过一次坑远程线和 24V 风机线捆在一起采回来的温度跳得厉害后来分开走线加屏蔽才好。硬件连接上还有一个细节PJ85718DM 的地址引脚。如果一条总线上挂多颗地址要错开。我一般本地一颗地址设成 0x48如果还有别的 I2C 器件地址要统一规划避免冲突。STM32 的 I2C 初始化之后最好先扫描一遍总线确认所有器件都能应答再进主循环。3. 固件实现与核心环节拆解3.1 I2C 读取本地温度的完整流程本地温度的读取我封装成一个函数输入是传感器地址输出是浮点温度值。流程分四步启动转换、等待转换完成、读寄存器、换算成温度。启动转换就是往配置寄存器写一个字节设置单次转换模式。等待转换完成可以轮询状态位也可以直接延时我一般延时 70ms比 datasheet 标称的 60ms 留点余量。读寄存器用HAL_I2C_Mem_Read从温度寄存器地址读两个字节。读回来之后高字节左移 8 位加上低字节组成 16 位原始值。然后判断符号位如果是负数要做补码转换。换算公式是温度 原始值 * 0.0625。这里要注意原始值是 16 位有符号数最高位是符号位负温度的时候要按补码处理不能直接当无符号数算。代码大概长这样float read_local_temp(uint8_t addr) { uint8_t cfg 0x60; // 12位分辨率单次转换 uint8_t raw[2]; HAL_I2C_Mem_Write(hi2c1, addr 1, 0x01, 1, cfg, 1, 100); HAL_Delay(70); HAL_I2C_Mem_Read(hi2c1, addr 1, 0x00, 1, raw, 2, 100); int16_t val (int16_t)((raw[0] 8) | raw[1]); return val * 0.0625f; }这段代码我用了很久稳定可靠。但有一个地方要注意HAL_Delay在中断里不能用如果这个函数在中断上下文调用要换成自己的延时。另外I2C 读写要加超时防止总线挂死。我一般超时设 100ms超了就返回一个错误码主循环里做重试。3.2 ADC 采样远程温度与滤波处理远程温度走 ADC配置上我用 DMA 循环采样一次采 16 个点采完中断里做平均。为什么要采 16 个点因为模拟信号上会叠加电源纹波和开关噪声单次采样抖动可能有几个 LSB平均之后能压到 1 个 LSB 以内。16 个点是折中再多会拖慢响应再少滤波效果不够。ADC 初始化的时候采样时间设成 71.5 周期这个值对高阻抗源比较友好。如果探头内阻大采样时间不够采回来的值会偏低。我实测过内阻 10k 的探头采样时间设 71.5 周期误差在 1 个 LSB 以内设成 7.5 周期误差能到 10 个 LSB。所以采样时间宁可设长一点。滤波这块除了 DMA 平均我还在主循环里做一层滑动平均窗口大小 8。这样两级滤波下来温度曲线非常平滑基本看不到跳变。滑动平均的实现用一个环形缓冲区每次新数据进来替换最老的数据重新算平均。代码不复杂但很实用。#define FILTER_WIN 8 static uint16_t buf[FILTER_WIN]; static uint8_t idx 0; uint16_t filter_avg(uint16_t new_val) { buf[idx] new_val; idx (idx 1) % FILTER_WIN; uint32_t sum 0; for (int i 0; i FILTER_WIN; i) sum buf[i]; return sum / FILTER_WIN; }换算成温度的时候要看探头的特性。NTC 的话用 Steinhart-Hart 公式或者查表。我一般用查表加线性插值速度快精度也够。表可以按 5 度一个点做插值之后精度能到 0.1 度以内。3.3 通信协议设计与数据上报数据上报走 UART协议我设计得很简单帧头两个字节 0xAA 0x55然后一个字节长度一个字节命令后面跟数据最后两个字节 CRC16。这样上位机解析起来容易也不容易丢帧。命令字我定义了两种一种是周期上报主循环每 500ms 发一帧包含本地温度和远程温度另一种是查询响应上位机发查询命令下位机回一帧。CRC16 我用的是 Modbus 那个多项式算起来快查表法几个周期就出结果。校验很重要串口通信在工业环境里干扰大没有校验很容易收到错数据。我踩过一次坑没加 CRC上位机偶尔显示温度 200 多度后来加了 CRC 就再没出现过。发送的时候用 DMA 或者中断都行我一般用中断发送把数据丢进缓冲区中断里一个个发。这样主循环不阻塞实时性好。波特率 115200一帧 20 个字节左右发送时间不到 2ms对系统没影响。注意UART 的 TX 和 RX 线如果走长线最好加 TVS 管做保护工业环境里浪涌很常见。我有个板子没加保护现场雷击之后串口就挂了后来加了 TVS 才稳定。3.4 报警逻辑与本地联动报警逻辑我放在主循环里每采集一次温度就判断一次。本地温度和远程温度各有上下限超过上限或者低于下限就置报警标志。报警标志置位后拉高报警 GPIO驱动蜂鸣器。同时上报的数据帧里带上报警状态位上位机也能看到。这里有个细节报警要做回差否则温度在阈值附近波动的时候报警会频繁开关。我一般设 1 度的回差比如上限 30 度超过 30 度报警降到 29 度以下才解除。这样报警稳定不会抖。报警输出我一般用开漏输出外接上拉这样可以兼容不同电平的蜂鸣器或者继电器。如果驱动大电流负载中间加一级三极管或者光耦保护 MCU 的 IO。整个固件的主循环大概是这样先读本地温度再读远程温度做滤波和换算然后判断报警最后组帧发送。整个过程几百微秒到几毫秒主循环跑得很快。定时器中断做 1ms 滴答用来做软定时和超时计数。没有 RTOS代码结构清晰调试也方便。4. 常见问题排查与实操避坑指南4.1 I2C 通信失败与总线挂死I2C 最常见的问题就是通信失败表现是读回来的数据全是 0xFF 或者 0x00或者 HAL 库返回 HAL_ERROR。原因一般有几个上拉电阻没接或者阻值太大、总线电容太大、地址不对、时序不匹配。排查的时候我第一步先用示波器看 SCL 和 SDA 波形。如果 SCL 没有波形说明主控没发出来检查 I2C 初始化如果 SCL 有波形但 SDA 一直是高说明从机没应答检查地址和供电如果波形上升沿很缓说明上拉电阻太大或者总线电容太大换小一点的上拉或者降低速率。总线挂死是另一个常见问题表现是 SCL 被从机拉低不放主控发不了时钟。这种情况一般是通信过程中被打断从机在等时钟。解决办法是手动发 9 个时钟脉冲让从机把数据移完然后发一个停止条件。STM32 的 I2C 外设有总线恢复功能但有时候不好用我一般用 GPIO 模拟时钟来恢复。void i2c_bus_recover(void) { GPIO_InitTypeDef gpio {0}; // 把 SCL 和 SDA 配成开漏输出 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); // 发 9 个时钟 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_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); // 重新初始化 I2C MX_I2C1_Init(); }这段代码我救过好几次现场非常实用。恢复之后重新初始化 I2C通信就能继续。4.2 ADC 采样值跳动与精度不足ADC 采样值跳动一般是参考电压不稳、采样时间不够、信号源内阻太大、地线干扰。排查的时候先测 VDDA用万用表看是不是稳定的 3.3V如果纹波大加滤波电容。然后看采样时间按前面说的设成 71.5 周期。信号源内阻大的话加运放缓冲。精度不足还有一个原因是 ADC 校准没做。STM32F030 的 ADC 上电后要做一次校准HAL 库里有HAL_ADCEx_Calibration_Start调一下就行。不校准的话零点误差可能有几个 LSB。地线干扰也常见模拟地和数字地要分开走最后单点接地。我有个板子模拟地和数字地混在一起ADC 采回来全是噪声后来割线分开加磁珠连接立马就好了。4.3 温度读数偏差与标定方法温度读数偏差可能是传感器本身误差、走线电阻、自热效应。PJ85718DM 出厂校准过偏差一般在 0.5 度以内如果偏差大先检查供电和走线。远程模拟探头偏差大一般是分压电阻精度不够或者运放失调。标定方法很简单用标准温度计做参考在几个温度点比如 0 度、25 度、50 度记录读数然后做线性拟合把斜率和截距写进固件。我一般做两点标定0 度和 50 度拟合出来的曲线在中间段误差很小。自热效应也要注意PJ85718DM 工作电流很小自热可以忽略。但如果是大电流的模拟探头比如 PT100 恒流源激励自热会明显要减小激励电流。4.4 常见问题速查表问题现象可能原因排查方法解决办法I2C 读回全 0xFF从机没应答示波器看 SDA检查地址、供电、上拉I2C 总线挂死通信被打断测 SCL 是否被拉低发 9 个时钟恢复ADC 值跳动大参考电压不稳万用表测 VDDA加滤波电容做校准温度偏差大探头误差或走线对比标准温度计两点标定换高精度电阻串口丢帧干扰或没校验看 CRC 错误计数加 CRC加 TVS报警频繁开关没有回差观察温度曲线加 1 度回差这张表是我这几年调试下来总结的基本覆盖了 90% 的问题。遇到问题先查表能省很多时间。提示调试的时候一定要留串口打印把关键变量打出来比用调试器单步快得多。我一般会打 I2C 读回的原始值、ADC 原始值、滤波后的值一眼就能看出哪一步出问题。4.5 实操心得与经验总结最后分享几个我踩过坑之后总结的经验。第一电源一定要干净温度传感器对电源纹波敏感LDO 后面加 10uF 加 0.1uF效果立竿见影。第二I2C 走线尽量短如果非要长用屏蔽线或者加缓冲器。第三ADC 采样时间宁长勿短多花几十微秒换来的是稳定。第四通信一定要加校验工业环境里没有校验就是裸奔。第五报警一定要加回差否则现场运维会被频繁报警搞疯。这套 PJ85718DM 加 STM32F030RC 的方案我前后用了三年多出货量也不小稳定性是经过验证的。成本低、开发快、维护简单适合 HVAC 这类对成本敏感、对精度要求不极端的场景。如果你也在做类似的项目可以直接参考这套架构把精力放在探头选型和现场标定上那才是真正影响体验的地方。