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.3V,I2C 电平天然匹配,省掉电平转换芯片。如果主控是 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 内核,主频 48MHz,Flash 256KB,RAM 32KB,对于这个项目来说资源很充裕。我一般这样分配外设:I2C1 用来接 PJ85718DM,跑 400kHz;ADC1 的通道 0 用来采远程温度,配合 DMA 做连续采样;USART1 做通信,115200 波特率;TIM3 做 1ms 系统滴答,用来做软定时和超时判断;另外留一个 GPIO 做报警输出,一个 GPIO 做状态指示。
时钟配置上,我用的是外部 8MHz 晶振,PLL 倍频到 48MHz。这里有个坑,STM32F030 的 I2C 时钟源是从 PCLK 分频来的,如果 PCLK 设成 48MHz,I2C 的时钟分频寄存器要算准,否则实际速率会偏。我一般把 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 这类对成本敏感、对精度要求不极端的场景。如果你也在做类似的项目,可以直接参考这套架构,把精力放在探头选型和现场标定上,那才是真正影响体验的地方。