拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+ADS1247+PT100高精度三线制测温方案详解

STM32+ADS1247+PT100高精度三线制测温方案详解

做嵌入式开发的人,迟早都会碰到测温这个需求。便宜的选择有NTC、DS18B20,但真要谈稳定和精度,工业现场基本绕不开PT100。PT100测温本身不复杂,难点在于PT100的电阻变化量太小,0.1℃对应大约0.04Ω的变化,要从这么小的信号里把温度准确抠出来,普通单片机内置ADC根本不够看,必须上一颗高精度ADC。

这篇文章我会把STM32+ADS1247+PT100这套测温方案从头到尾拆开讲。内容包括方案原理、原理图设计、寄存器配置、STM32完整驱动代码、温度换算和校准,以及我实际调板子时踩过的坑。不管你是做毕业设计、工业温控,还是仪器仪表,这套思路都能直接用。

1. 方案选型:为什么偏偏是ADS1247

1.1 PT100测温这件事的真正难点

先算一笔账。PT100是铂热电阻,0℃时阻值100Ω,随着温度升高阻值近似按0.385Ω/℃增加。如果目标是0.5℃精度,那电阻分辨率至少要做到0.19Ω;如果目标是0.1℃分辨率,对应就是0.0385Ω。

假设激励电流取500μA,0.1℃对应的电压变化只有19μV。普通STM32内置12位ADC,参考电压3.3V,LSB约为0.8mV,对应温度约2℃,完全没法做到毫伏级别以下的测量。想要把微伏级信号测出来,只有两条路:要么先用仪表放大器把信号放大到足够幅度,要么直接用高分辨率ADC加片内PGA,把微弱信号在AD前端就处理掉。

用外部仪表放大器方案不是不行,但电路结构会复杂很多。INA128一类的仪表放大器本身价格不便宜,还要额外搭电流源、基准源、滤波电路,信号链路长了,每个环节都会引入误差。对于产品设计来说,能用一个高集成度ADC解决问题,就不要去堆分立器件。

1.2 ADS1247的核心优势

ADS1247是TI出的一颗24位Delta-Sigma ADC,定位就是给传感器测量用的。它在单颗芯片里集成了PGA可编程增益放大器、两个恒流源IDAC、内部基准、温度传感器和SPI接口,几乎把RTD测温需要的所有前端电路都做进去了。

重点看这几个和PT100强相关的特性:

  • 24位分辨率,内部PGA最大128倍增益,微伏级信号直接采。
  • 两路可配置恒流源IDAC,支持50μA到1.5mA多档输出,专门用来做RTD激励。
  • 通过比例测量法,能达到0.1%以下的电阻测量精度,激励电流漂移和电源波动可以被自动抵消。
  • SPI接口,单片机通讯简单,SPI速度和时序配置在通用范围内。

用ADS1247做三线制PT100,原理图上基本就是“传感器+一个参考电阻+几个电容”就能跑起来,BOM成本也压得很低。

1.3 和ADS1220、其它方案的对比

选型时很多朋友会纠结ADS1247和ADS1220。这两颗芯片确实非常像,都是24位高精度ADC,都支持PT100三线制方案。直观区别主要是通道数和封装:

  • ADS1220:4路输入,2路差分,不支持低功耗待机,价格略低。
  • ADS1247:2路差分输入,带两个IDAC,测量三线PT100时IDAC数量刚好够用,性能在噪声指标上略好一点。
  • 如果以后想同时测4路PT100,可以选ADS1248,它是4路差分输入版本。

我个人选ADS1247的理由很简单:本人项目中温度采样通道固定两路,AD1247的双通道够用,而且它和ADS1248软件寄存器地址兼容,以后真要扩展,代码改动量也不大。至于直接用STM32内部ADC加运放,适合对精度不敏感、只测个大概的应用,比如环境温度显示;但工业设备、医疗仪器、数据记录仪这类场景,还是老老实实用专用ADC。

2. 硬件设计:原理图与关键器件

2.1 三线制PT100的接线逻辑

PT100传感器分为两线制、三线制和四线制。两线制最简单,传感器两根线直接接到测量电路,但导线本身有电阻,普通铜线每米阻值大约0.01Ω到0.1Ω不等,长距离传输时线阻误差直接叠加进测量结果,可能带来好几度的偏差。

三线制的思路是用两根等长的导线走同一端的信号,再利用ADS1247的两个IDAC产生等量电流,把线阻诱导出来的误差在差分测量中抵消掉。四线制精度最高,但PT100四线制探头价格也贵,一般应用用不上。

我在这套方案里强烈建议用三线制PT100,接线方式如下:

  • PT100一端的引线A接到ADS1247的AIN0,引线B接到AIN1。
  • PT100另一端的引线C接到电流回流的公共端。
  • IDAC1和IDAC2分别配置到AIN0和AIN1输出等量激励电流。

由于两根引线A和B材质、长度一致,它们在各自支路上产生的压降基本相等,差分输入测得的就是纯PT100电阻上的电压,线阻误差被自动消除。实际画原理图时要注意,AIN0和AIN1之间的差分走线要尽量等长,避免因为PCB走线电阻不一致引入新的偏差。

2.2 比例测量法与参考电阻选型

ADS1247做PT100测量,我强烈建议用比例测量法,而不是直接用内部基准测量。比例测量的本质是:让同一个激励电流既流过PT100,也流过一颗精密参考电阻RREF,然后把RREF两端的电压当ADC的参考电压。这样ADC输出的码值直接等于PT100电阻与RREF的比值,激励电流的漂移、电源电压的波动都被抵消掉。

计算公式可以简化成:

R_RTD ≈ (ADC_code × RREF) / (PGA × 2^23)

所以关键就在于RREF的精度。这里我建议选精度0.1%、温漂系数5ppm/℃以内的精密电阻,阻值选2kΩ或者2.4kΩ。电流源取640μA时,2kΩ电阻上的压降约1.28V,作为ADC参考电压比较合适,不至于超过ADS1247参考输入范围。

选型时候多花了大概2块钱,但换来了整机温漂大幅降低,这个投资非常值。RREF的焊盘建议放宽一点,方便后续用低温漂电阻替换。

2.3 电源、滤波与STM32最小电路

STM32和ADS1247都工作在3.3V,一颗AMS1117-3.3就够供电。电源部分有两个细节容易被人忽略:

  • AMS1117的输入输出电容,我习惯用10μF钽电容并联0.1μF陶瓷电容,钽电容ESR低、稳定性好,陶瓷电容负责高频去耦。
  • ADS1247的模拟电源AVDD和数字电源DVDD要分别用0.1μF和10μF电容就近接地,模拟地和数字地建议单点连接,避免数字开关噪声串进模拟前端。

STM32与ADS1247之间走SPI口,引脚分配如下:

STM32引脚功能
PA4CS片选
PA5SCLK时钟
PA6MISO数据输出
PA7MOSI数据输入
PA8DRDY数据就绪
PB0RESET复位
PB1START启动

这组引脚在STM32F103C8T6上都能直接映射到SPI1,接线比较顺手。实际使用中如果引脚不够,RESET和START也可以直接绑到3.3V或者用普通GPIO控制,DRDY必须要接外部中断或输入捕获口,因为读数据时要等它拉低。

3. STM32驱动开发:寄存器配置与代码

3.1 SPI时序和初始化

ADS1247的SPI接口在SCLK下降沿输出数据,上升沿采样数据,推荐工作模式是SPI Mode 1(CPOL=0,CPHA=1)。SPI时钟频率不要拉太高,我习惯配置成1MHz左右,距离短、时序余量足,稳定性优先。

STM32这边我用标准库或者HAL库都行,这里示例用HAL库。初始化SPI1的代码如下:

void ADS1247_SPI_Init(void) { SPI_InitTypeDef spi; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; gpio.Mode = GPIO_MODE_AF_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); gpio.Pin = GPIO_PIN_6; gpio.Mode = GPIO_MODE_AF_IN; HAL_GPIO_Init(GPIOA, &gpio); hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }

如果你的板子是标准库工程,把SPI初始化函数换成库对应的结构体配置就行,SPI Mode 1这个关键点千万别改错。

3.2 寄存器配置详解

ADS1247的寄存器不多,和PT100测温相关的就这几个:

  • MUX0:输入多路复用器,配置AIN0和AIN1作为正负输入。
  • MUX1:选择参考电压源,这里选外部参考,即REFP0/REFN0。
  • SYS0:配置PGA增益和输出数据速率。
  • IDAC0:配置IDAC1和IDAC2的电流大小。
  • IDAC1:配置IDAC1和IDAC2的输出引脚。

我实际用的配置如下:

void ADS1247_InitRegs(void) { ADS1247_WriteReg(0x00, 0x01); // MUX0: AIN0=正输入, AIN1=负输入 ADS1247_WriteReg(0x01, 0x00); // VBIAS: 关闭 ADS1247_WriteReg(0x02, 0x20); // MUX1: 外部参考 REFP0/REFN0 ADS1247_WriteReg(0x03, 0x2A); // SYS0: PGA=8倍,输出速率20SPS ADS1247_WriteReg(0x04, 0x0C); // IDAC0: IDAC1=IDAC2=640uA ADS1247_WriteReg(0x05, 0x05); // IDAC1: IDAC1输出AIN2, IDAC2输出AIN3 }

其中的SYS0寄存器我没法给出所有位的完整说明(不同批次数据手册略有差异),但大原则是:PGA增益取8倍,对应PT100在0℃到300℃范围内的信号幅度都不会超量程;输出速率20SPS在低噪声和响应速度之间比较平衡。

IDAC配置这里特别提醒一句:IDAC不能输出到REFP/REFN引脚,也不能输出到没有选择作为输入的引脚,否则测量路径会乱。我习惯把IDAC输出固定到AIN2和AIN3,和差分输入通道分开,省心。

3.3 完整驱动代码

下面给出一套能直接编译烧录的核心驱动。先看SPI读写命令封装:

#define ADS1247_CMD_RESET 0x06 #define ADS1247_CMD_START 0x08 #define ADS1247_CMD_STOP 0x0A #define ADS1247_CMD_RDATA 0x12 #define ADS1247_CMD_WREG 0x40 #define ADS1247_CMD_RREG 0x20 void ADS1247_CS_Low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void ADS1247_CS_High(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } void ADS1247_WriteReg(uint8_t addr, uint8_t data) { uint8_t cmd[3]; cmd[0] = ADS1247_CMD_WREG | (addr & 0x0F); cmd[1] = 0x00; // 只写1个寄存器 cmd[2] = data; ADS1247_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 3, 10); ADS1247_CS_High(); } uint8_t ADS1247_ReadReg(uint8_t addr) { uint8_t cmd[2] = { ADS1247_CMD_RREG | (addr & 0x0F), 0x00 }; uint8_t rx[2] = {0, 0}; ADS1247_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 2, 10); HAL_SPI_Receive(&hspi1, rx, 2, 10); ADS1247_CS_High(); return rx[0]; }

读ADC转换结果的函数,核心是等DRDY变低,再发RDATA命令取三个字节:

int32_t ADS1247_ReadADC(void) { uint8_t buf[3]; uint8_t cmd = ADS1247_CMD_RDATA; uint32_t timeout = 1000; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET) { if (--timeout == 0) return 0; // 超时保护 HAL_Delay(1); } ADS1247_CS_Low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); HAL_SPI_Receive(&hspi1, buf, 3, 10); ADS1247_CS_High(); int32_t code = ((int32_t)buf[0] << 16) | ((int32_t)buf[1] << 8) | buf[2]; // ADS1247输出是二进制补码,24位有符号数 if (code & 0x800000) code |= 0xFF000000; return code; }

主流程里初始化完ADS1247之后,发送START命令让它连续转换,然后主循环里读数据换算温度即可:

void ADS1247_Start(void) { uint8_t cmd = ADS1247_CMD_START; ADS1247_CS_Low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); ADS1247_CS_High(); }

整套流程跑通以后,ADS1247会自动在每次转换完成后把DRDY拉低,STM32这边通过查询或者外部中断读取都行。我建议在中断里置标志位,主循环里处理换算,这样可以避免查询阻塞影响其它实时任务。

4. 从ADC码值到温度:换算与校准

4.1 码值换算成电阻值

ADS1247的24位ADC输出是二进制补码,满量程对应±Vref/PGA。当PGA=8、Vref=1.28V时,LSB对应的电压是:

V_LSB = Vref / (PGA × 2^23)

代入计算:1.28 / (8 × 8388608) ≈ 19.07nV,这个分辨率相当可观。但实际系统噪声会把有效分辨率限制在几个微伏级别,所以不要被理论值迷惑,还是要靠滤波和校准。

电阻值的换算公式:

R_RTD = ADC_code × RREF / (PGA × 2^23)

这里的ADC_code是带符号的24位码值,PGA是寄存器里配置的增益,RREF是实际焊接的参考电阻阻值。比如ADC读出码值是138000,PGA=8,RREF=2000Ω:

R_RTD = 138000 × 2000 / (8 × 8388608) ≈ 411.5Ω

这个电阻值对应的温度大概是850℃左右,说明当前信号已经接近满量程附近。实际应用中PT100测温范围通常不超过500℃,所以正常情况下码值不会顶格。

4.2 电阻换算成温度公式

PT100的温度-电阻关系有两个精度等级。工业上普遍采用IEC 60751标准,0℃以上用二次多项式就够了:

R(t) = R0 × (1 + A×t + B×t²)

其中R0=100Ω,A=3.9083×10⁻³,B=-5.775×10⁻⁷。

反过来由电阻求温度,可以把方程整理成一元二次方程:

B×t² + A×t + (1 - R/R0) = 0

解出来:

t = (-A + sqrt(A² - 4B×(1 - R/R0))) / (2B)

这段代码直接在STM32上浮点运行完全没问题:

float PT100_ResToTemp(float R) { const float A = 3.9083e-3f; const float B = -5.775e-7f; float r = R / 100.0f; float delta = A * A - 4.0f * B * (1.0f - r); if (delta < 0.0f) delta = 0.0f; return (-A + sqrtf(delta)) / (2.0f * B); }

低于0℃时,标准曲线里还有一个三次项修正。如果你要测-50℃甚至更低的温度,我建议用查表法分段插值,代码提前把PT100分度表按10℃间隔做成数组,测出来的电阻值先查区间再做线性插值。查表法对单片机的算力要求极低,代码也直观。

4.3 两点校准实战

即使比例测量法消除了大部分系统误差,ADC输入失调、PCB铜箔电阻、参考电阻初始精度还会留下固定偏差。这时候就需要校准。

我的做法是在批量生产中做零点和增益两点校准,配方不复杂:

  1. 把PT100探头放进冰水混合物中,记录ADC读数作为零点校准点,温度参考值0℃。
  2. 把探头放进沸水环境中,记录第二个读数,温度参考值100℃。
  3. 计算出增益修正系数:

K = (100 - 0) / (Temp_raw_boil - Temp_raw_ice) Offset = 0 - K × Temp_raw_ice

实际测量温度 = K × 原始计算温度 + Offset。这套线性校正对PT100这种近似线性的传感器效果很好,可以在0~100℃范围内把误差压到±0.2℃以内。

校准参数我习惯存到STM32内部Flash里,掉电不丢失。每片板子在生产时写一遍校准参数,后面使用就不用再重复标定。

5. 常见问题与调试实录

5.1 读数不稳定,跳动幅度大

这是我最常遇到的问题。读数乱跳,先不要怀疑ADC本身,优先检查几个方向:

  • 激励电流路径上有没有接触不良,比如PT100端子松动、接线端子氧化。
  • SPI的MISO线上有没有受到干扰,特别是长排线连接时,建议降低SPI时钟频率并加一个10kΩ上拉。
  • 电源纹波是否偏大,示波器看AVDD是否有毛刺,必要时在模拟电源入口加磁珠和LC滤波。
  • DRDY信号线上如果有高频开关干扰,可以串一个几百欧姆的电阻,或者用RC滤波后进STM32外部中断。

我的调试经验是:先把SPI时钟降到100kHz,排除通信问题;再把PGA增益调到1确认信号范围;最后看数据均值方差,逐段缩小范围。

5.2 温度总体偏差固定,要么偏高要么偏低

固定的温度偏移一般指向线阻误差或者校准偏移。用二线制接法时,假设两根线一共0.5Ω,那在0℃时测出来的电阻会变成100.5Ω,等效温度偏差约1.3℃,非常明显。

解决办法有两类:

  • 硬件上改用三线制,利用两个IDAC抵消线阻。
  • 软件上把线阻测出来,在电阻值里做减法。方法是短路PT100端子,读一次ADC码值,换算成等效线阻R_wire,然后每次测量时 R_true = R_measured - R_wire。

批量校准时,如果发现每台板子的偏差范围一致,直接把偏移量写死在标定参数里也行,但前提是连接线长度和生产工艺必须稳定一致。

5.3 SPI通信异常,读回来全是0xFFFFFF或0x000000

SPI时序错了最容易读到这种全0或全1的数据。ADS1247代码里有个容易忽略的点:寄存器写命令是“命令字节+寄存器数量-1+数据字节”,如果你只发命令和数据两个字节,漏了中间的count字节,那所有写入都会错位。

另外ADS1247的RDATA命令在读数据时,CS要保持低电平,读三个字节之后再拉高CS,不能在字节之间拉高。我刚开始写驱动时不熟悉,每个字节读完后都把CS拉高了一下,结果读回来的数据永远不对。

如果要排查SPI通信是否正常,可以在初始化后回读所有寄存器的内容,和写入值比对一遍,这个自检步骤能第一时间暴露物理连接或者时序问题。

5.4 代码里常踩的其它坑

  • DRDY引脚没有配置为输入模式,导致一直读到高电平。这种情况多半是GPIO初始化时钟没打开,或者引脚配置成了复用推挽输出。
  • 上电后忘记发START命令,芯片处于待机状态,DRDY永远不拉低。ADS1247上电默认是停止状态,必须先写寄存器再发START。
  • 参考电阻虚焊,导致REF+和REF-之间没有电压,ADC码值会直接顶到满量程。焊接后用万用表量一下RREF两端电压,这是一个非常有效的硬件自检方法。

6. 这套方案还能怎么扩展

最后聊一点我自己项目里的扩展经验。ADS1247除了测PT100电阻,还能利用片内温度传感器做冷端补偿,配合热电偶使用。虽然本文主要讲PT100,但理解这套寄存器配置和读数流程之后,切换到热电偶测温只是改一下输入配置和换算公式的事情。

另外,如果以后要把测量温度通过4-20mA电流环传到PLC,或者用RS485走Modbus协议,ADS1247负责的高精度采集部分完全不用动,只需要在STM32端加一个DAC或RS485芯片,把结果转成标准工业信号就行。ADC采集的稳定性是整个系统的地基,地基打好了,上面接什么通讯外设都不慌。

我自己在项目中最终实测的三线制PT100在0~200℃范围内,配合两点校准和256次滑动平均滤波,精度稳定在±0.2℃以内,连续运行72小时漂移小于0.1℃,这个结果对大多数工业采集场景已经非常够用。ADS1247用顺手之后,再回头去看那些靠运放堆出来的测温电路,是真的想感叹集成方案省心太多。

返回列表