STM32+MAX31865+PT100/PT1000高精度测温方案实战详解
2026/8/31 15:53:36 网站建设 项目流程

简介:本资源是一套基于STM32F103系列单片机(采用TPYBoard STM32F103RBT6最小系统板)通过SPI接口驱动MAX31865高精度RTD温度传感器模块的完整嵌入式开发方案,面向嵌入式初学者、硬件工程师及工业测温应用开发者,解决铂电阻(如PT100/PT1000)温度采集中的通信配置、寄存器操作、冷端补偿与线性化处理等核心问题。压缩包共608个文件,总计17.17MB,涵盖Keil工程源码(69个.c + 73个.h文件)、编译中间产物(.o/.lst/.map等)、硬件连接图与实物照片(14张PNG/JPG)、库函数使用说明(docs/下rst文档为主)、以及HTML格式的模块参考手册和配置指南。已有1376人学习下载,提供开箱即用的Project.uvproj工程、完整SPI底层驱动、MAX31865初始化与温度读取函数封装,并附带清晰的目录结构与注释详尽的C源码,便于快速移植、调试验证与原理理解。 做测温项目,尤其是指望精度能打的PT100/PT1000电阻式传感器,很多人第一反应都是用STM32的ADC口直接测。真这么干过的人基本都栽过跟头——冷端补偿、基准电压漂移、导线电阻、PCB走线噪声,随便一个因素就能让温度跳好几度。这也是我在项目里最终落到STM32F103+SPI+MAX31865这套组合的原因:MAX31865是专为RTD传感器设计的数字转换芯片,内置恒流源激励、24位ADC、参考电阻匹配和故障检测,MCU只负责通过SPI把数据读回来做换算。这篇文章会把整个方案的硬件选型、寄存器配置、SPI驱动、温标换算和实测踩坑全部过一遍,适合手里有STM32F103最小系统板、想用PT100/PT1000做温度采集,又不想在模拟前端上花太多时间的朋友参考。

1. 从PT100到MAX31865:这套方案到底解决了什么

1.1 为什么直接拿ADC测电阻型传感器不靠谱

PT100这种热电阻,本质是个随温度变化阻值的电阻,0℃时刚好100Ω,100℃时138.5Ω左右,平均每摄氏度才变化约0.385Ω。如果直接用单片机的12位ADC去测,通常是先搭一个分压电路,把电阻变化转成电压变化,再接给ADC采样。问题出在哪?分压电路里另一个电阻的精度和温漂,会直接叠加进测量结果,电源电压稍微波动一点,基准源也跟着漂。再加上STM32F103内置ADC本身的量化噪声和参考电压精度有限,最终能稳住±0.5℃已经算烧高香,实际场景里常常是±1℃以上甚至更多。

更麻烦的是接线。工业现场或实验台面上导线动不动一两米,铜导线每米大概0.01到0.05Ω,两根线串进去就是0.1Ω级别的误差,换算成温度已经有零点几摄氏度了。你要是用普通杜邦线或者细电缆,误差更大。所以专业测温方案不会这么玩,而是用恒流源激励RTD,再用高精度ADC同时采集RTD上的电压和参考电阻上的电压,两者相除把激励电流的影响抵消掉。MAX31865就是把这一整套模拟前端全部封装好的芯片。

1.2 MAX31865内部工作原理与选型优势

MAX31865内部集成了一个可选的恒流激励源,经过FORCE+和FORCE-引脚给外部RTD供电,同时芯片内部通过一个精度非常高的参考电阻(RREF)来确定ADC的满量程。RTD两端的电压经RTDIN+和RTDIN-输入到内部24位ADC,ADC结果实际上是RTD电阻和参考电阻的比值:

ADC_code = (R_RTD / R_REF) × 32768

这里32768对应15位ADC的满量程。注意芯片虽然内置24位ADC,但输出给MCU的转换结果是15位有效数据。因为换算结果与激励电流大小无关,电源电压的波动对精度影响被大幅削弱,这也是它敢声称最高0.05%精度、温度分辨率能做到0.03125℃(配合PT100)的原因。

选MAX31865而不是MAX31855,最核心的区别是传感器类型不同:后者配热电偶,输出的是热端对冷端的温差电动势,还要自己做冷端补偿,而且热电偶本身是非线性器件,低温段尤其难做准。而MAX31865配PT100/PT1000,在全温区范围内可以用标准分度表或者方程式精确换算,稳定性、重复性都好得多。做精密环境监测、工业过程控制、设备温度补偿这类场景,PT100+MAX31865是远比热电偶省心的路线。

1.3 适用场景和整体架构

这套方案的定位比较清晰:测温范围覆盖-200℃到+850℃(取决于RTD型号),精度可以达到0.1℃级别,成本也完全可控,非常适合做:

  • 温度数据记录仪
  • 工业设备温度监测
  • 电池包或电机的温度采集节点
  • 需要温度补偿的精密测量装置
  • 实验室恒温环境监控

整体硬件架构非常简洁,就是一颗STM32F103最小系统板加一块MAX31865模块,SPI四根线连过去,模块上再接好PT100或者PT1000。MCU主频72MHz,SPI跑2.25MHz,读一次温度数据只需要几毫秒,而MAX31865内部一次转换要几十毫秒到几百毫秒,所以性能上完全不构成瓶颈。

2. 硬件准备:器件选型、参考电阻计算与接线

2.1 PT100还是PT1000,参考电阻怎么选

传感器选PT100还是PT1000,直接影响参考电阻的取值和电路的抗干扰表现。PT100在0℃时100Ω,PT1000则是1000Ω,后者灵敏度更高、自热更小、对导线电阻的容忍度也更好,但价格略高,而且很多通用工业仪表默认只支持PT100。我的经验是:如果整个系统自己说了算,又希望线缆长一点、抗干扰好一点,直接上PT1000;如果是替换老设备或者手头全是PT100探头,就老老实实用PT100。

参考电阻RREF是MAX31865外围最关键的元件,它的精度直接决定测量误差。官方推荐PT100配430Ω,PT1000配4300Ω。为什么不是刚好100Ω或1000Ω?因为RTD在满量程时电阻会升高,以PT100在850℃时约390Ω为例,参考电阻必须大于最大量程的RTD电阻,否则ADC容易饱和。430Ω给到0到850℃全程留出了余量,也兼顾了0℃附近的分辨率。

参考电阻的精度至少要选0.1%低温漂,最好是0.05%或更高。你到淘宝买那些十几块钱的MAX31865模块,板子上的参考电阻质量参差不齐,直接读数可能整体偏个零点几摄氏度,所以想要仪器级精度,建议自己在模块上改成独立的高精度电阻,或者先按后文的方法做校准。

2.2 二线制、三线制、四线制接线方式

RTD传感器引线方式分二线制、三线制和四线制,这套方案全部支持:

  • 二线制:RTD两端各接一根线,分别连到FORCE+与RTDIN+短接的一端、以及FORCE-与RTDIN-短接的另一端。虽然后端芯片可以补偿一部分,但两根导线的电阻完全叠加进测量回路,误差随线长线性增加。只适合短线、精度要求不高的场合。
  • 三线制:RTD一端只出一根线,接到FORCE+与RTDIN+的公共端;另一端出两根线,分别接RTDIN-和FORCE-。因为芯片同时采集了RTDIN-和FORCE-之间的电位关系,导线电阻造成的压降能够被抵消一部分,是工业现场最常用的接法,也是我项目里的首选。
  • 四线制:RTD每个端点接两根线,一根走激励电流,一根测电压,理论上完全消除导线电阻影响,精度最高,但多一根线也意味着成本增加。实验室和高精度应用中比较常见。

我实际测试时用两米长的普通屏蔽电缆,二线制读数比三线制约高0.4℃到0.8℃,这个偏差正好对应电缆回路的串联电阻,换成三线制以后偏差降到0.1℃以内。所以哪怕只是桌上实验,也建议优先采用三线制。

2.3 STM32F103引脚映射与供电注意事项

STM32F103的硬件SPI1默认引脚是PA5(SCK)、PA6(MISO)、PA7(MOSI),这组引脚在最小系统板上基本都引出来了,直接用就好。片选CS我用PA4做软件控制,原因后面代码部分细说。DRDY引脚是数据就绪输出,转换完成后会拉低,可以接任意GPIO或者干脆悬空,用查询或延时方式代替。完整接线如下:

MAX31865模块引脚STM32F103引脚说明
VIN/VCC3.3V注意必须是3.3V,严禁接5V
GNDGND共地
CLKPA5SPI1_SCK
SDIPA7SPI1_MOSI
SDOPA6SPI1_MISO
CSPA4软件片选,可换任意GPIO
DRDYPA3(可选)数据就绪标志,低有效

关于供电有一个特别容易踩的雷:绝大部分市面上常见的MAX31865模块板上没有3.3V稳压器,直接把5V接进去,芯片大概率当场报废。它的工作电压范围是3.0V到3.6V,只能从STM32F103最小系统板的3.3V引脚取电。焊接之前先拿万用表确认模块上VIN和GND之间不是直通的,再上电。

3. 寄存器与SPI时序:MAX31865的关键bit逐个拆

3.1 寄存器地址一览

MAX31865的寄存器不多,但每个bit都有实际意义,先列个表方便对照:

寄存器地址名称读写属性用途
0x00Configuration读/写配置转换模式、采样次数、接线方式
0x01RTD MSBsRTD转换结果高8位
0x02RTD LSBsRTD转换结果低8位
0x03High Fault Threshold MSB读/写高温故障阈值高字节
0x04High Fault Threshold LSB读/写高温故障阈值低字节
0x05Low Fault Threshold MSB读/写低温故障阈值高字节
0x06Low Fault Threshold LSB读/写低温故障阈值低字节
0x07Fault Status故障状态寄存器

SPI协议上,地址字节最高位为1表示写操作,为0表示读操作。所以读配置寄存器发送0x00,写配置寄存器发送0x80;读RTD数据发送0x01;读故障状态发送0x07。

3.2 配置寄存器各bit怎么设

配置寄存器的每一位含义如下:

  • D7(VBIAS):置1开启内部偏置电流,也就是RTD激励。测量时必须为1,否则RTD上无电流,读数没意义。
  • D6(Conversion Mode):置1为自动转换模式,芯片持续周期性转换;置0为单次模式,需要写D5触发一次转换。我建议日常用自动模式,省事。
  • D5(One-shot):只在单次模式下有效,写1启动一次转换,转换完成后自动清0。
  • D4(3-Wire):置1表示三线制RTD,置0表示二线制或四线制。一定要和实际接线对应上,否则数据会明显偏大或偏小。
  • D3到D1(Fault Cycle):设置采样次数,000为1次,001为2次,010为4次,011为8次,100为16次。采样次数越多,内部数字滤波效果越好,但转换时间也越长,后面实测数据说明。
  • D0(Fault Clear):写1清除故障状态位,写入后自动回到0。

我常用的配置值有两个:

  • 二线制/四线制、自动转换、2次采样:0xC2
  • 三线制、自动转换、2次采样:0xD2

如果环境电磁干扰比较大,可以把低三位改成011(8次采样),配置值相应变成0xCA或0xDA。

3.3 SPI读写时序:写配置、读RTD数据、读故障

写配置寄存器时,CS拉低后连续发送两个字节:第一个是0x80(写配置寄存器地址),第二个是配置值,然后拉高CS。整个过程一气呵成,中间CS不能抖动。

读RTD数据时,CS拉低,先发送0x01作为读地址,然后连续读取两个字节。这里最容易犯的错是读完第一个字节就拉高CS,或者用两次独立的HAL_SPI_Receive,导致第二个字节读到的是垃圾数据。正确做法是CS在整个事务期间保持低电平,连续接收两个字节后再拉高。第一个字节是RTD转换结果的高8位,第二个字节是低8位。

读故障状态更简单,发送0x07后读取一个字节即可。如果这个字节非零,说明当前有故障,需要结合具体bit判断原因。

4. STM32F103驱动代码:从CubeMX配置到核心读写函数

4.1 CubeMX初始化要点

用STM32CubeMX生成工程时,SPI的初始化有几个关键点:

  • SPI模式选Master,双线全双工(2 Lines Full Duplex)
  • 数据宽度8位,MSB先行
  • CPOL置High,CPHA置2 Edge,也就是SPI Mode 3。MAX31865支持Mode 1和Mode 3,我用Mode 3比较顺。
  • NSS选Software,不要启用硬件片选
  • 波特率分频选SPI_BAUDRATEPRESCALER_32,即72MHz分频后2.25MHz

PA4作为CS引脚时,在CubeMX中直接把它配置成GPIO_Output,初始电平设为High,然后SPI设置里NSS保持Disabled。代码里用两个宏控制片选:

#define MAX31865_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MAX31865_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

这样做的原因是MAX31865的CS在整个读写事务期间要一直保持低电平,中间不能拉高,而硬件NSS在8位模式下控制节奏不如软件来得直接,软件片选最稳妥。

4.2 基础读写函数

写配置寄存器:

void MAX31865_WriteConfig(uint8_t config) { uint8_t tx[2] = {0x80, config}; MAX31865_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx, 2, 10); MAX31865_CS_HIGH(); }

读RTD原始值:

uint16_t MAX31865_ReadRTD(void) { uint8_t tx[3] = {0x01, 0x00, 0x00}; uint8_t rx[3] = {0, 0, 0}; MAX31865_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 3, 10); MAX31865_CS_HIGH(); return (uint16_t)((rx[1] << 8) | rx[2]); }

之所以不用HAL_SPI_Transmit再HAL_SPI_Receive分开做,是因为在片选保持低电平的前提下,两次调用之间的时间间隔虽然不长,但遇到中断或调度延迟时,CS低电平时间拉长不影响正确性,问题不大;但有些HAL库版本在连续调用时如果DMA或中断处理不当,容易出诡异问题。干脆一次TransmitReceive搞定,简单可靠。

读故障状态:

uint8_t MAX31865_ReadFault(void) { uint8_t tx[2] = {0x07, 0x00}; uint8_t rx[2] = {0, 0}; MAX31865_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); MAX31865_CS_HIGH(); return rx[1]; }

清故障:

void MAX31865_ClearFault(void) { uint8_t cfg = 0xD2; // 或0xC2,取决于接线 cfg |= 0x01; // 置FAULTCL位 MAX31865_WriteConfig(cfg); MAX31865_WriteConfig(cfg & 0xFE); // 清回0 }

4.3 主循环采样状态机

初始化阶段,先调用一次MAX31865_ClearFault(),再写一遍配置,然后延时至少100ms等待第一次转换完成。主循环中每次读取的顺序是:读RTD值、读故障状态、根据故障状态决定是否采用数据。

因为MAX31865自动模式下内部一直在跑转换,所以主循环没必要等DRDY,直接按固定的间隔读取即可。比如我习惯500ms读一次,与2次采样的转换时间(约110ms)相比,肯定不会读到同一个转换周期的重复值。如果对更新时间要求更高,可以把采样次数降为1次,转换时间约55ms,读取周期可以压到100ms。

实际项目里我一般还会用一个简单的中值滤波:连续读5次,排序后取中间值,作为这一轮的有效温度。这个逻辑放在后面软件滤波部分细讲。

5. 从16位原始值到温度:换算公式与查表法

5.1 15位ADC数据与bit0的坑

MAX31865返回的两字节拼成一个16位数,但真正有效的ADC数据是bit15到bit1这15位,最低位bit0是故障标志位,不是数据。如果直接用这个16位数除以32768计算,温度会整整偏大一倍,测0℃能算出199℃这种离谱值。所以读回来的raw值必须先右移一位,或者用raw >> 1处理后再参与计算:

uint16_t raw = MAX31865_ReadRTD(); uint16_t adc_code = raw >> 1;

同时可以检查raw & 0x01,如果为1说明存在故障,这个值就不应该被使用。

5.2 电阻值计算

得到15位ADC值后,结合参考电阻RREF计算实际电阻:

float resistance = (float)adc_code * RREF / 32768.0f;

以PT100配430Ω参考电阻为例,0℃时100Ω,对应ADC值约7620;100℃时138.5Ω,对应ADC值约10555。这个公式其实就是前面讲的ADC_code = (R_RTD / R_REF) × 32768的反推,理解之后就不用死记。

5.3 查表插值与Callendar-Van Dusen方程两种实现

得到电阻值后,换算温度有两种常用方法。

第一种是查IEC 60751分度表加线性插值。把PT100在几个典型温度点的电阻值做成常量表:

const float pt100_table[][2] = { {-40, 84.27f}, {0, 100.00f}, {50, 119.40f}, {100, 138.51f}, {150, 157.33f}, {200, 175.86f} };

在相邻两个电阻值之间做线性插值:

float ResistanceToTemp(float r) { uint8_t i; for(i = 0; i < 5; i++) { if(r >= pt100_table[i][1] && r <= pt100_table[i+1][1]) { float ratio = (r - pt100_table[i][1]) / (pt100_table[i+1][1] - pt100_table[i][1]); return pt100_table[i][0] + ratio * (pt100_table[i+1][0] - pt100_table[i][0]); } } return -999.0f; // 超出表格范围 }

查表法的优点是实现直观、没有任何数学公式,负温区也能直接处理,只要表格点数足够密,插值误差完全可以忽略。缺点是需要占用一点Flash,但如果只放十几个点,基本可以忽略不计。

第二种是用Callendar-Van Dusen方程反解。IEC 60751给出的0℃以上温区关系为:

R_T = R_0 × (1 + A×T + B×T²)

其中A = 3.9083×10⁻³,B = -5.775×10⁻⁷。反解一元二次方程:

float cvd_temp(float resistance, float r0) { float A = 3.9083e-3f; float B = -5.775e-7f; float c = 1.0f - resistance / r0; float disc = A * A - 4.0f * B * c; if(disc < 0.0f) return -999.0f; return (-A + sqrtf(disc)) / (2.0f * B); }

注意这个公式只适用于0℃以上。低温段需要引入C系数的三次项,直接用方程反解会偏,所以0℃以下我推荐查表法。

5.4 两种方法精度对比

在我实测中,把两种方法放在同一批RTD原始值上对比,0℃到150℃范围内最大偏差不超过0.02℃,都已经远高于传感器本身的重复性极限。所以选哪种纯粹看个人

本文还有配套的精品资源,点击获取

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

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

立即咨询