做嵌入式开发的人,迟早都会碰到测温这个需求。便宜的选择有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引脚 | 功能 |
|---|---|
| PA4 | CS片选 |
| PA5 | SCLK时钟 |
| PA6 | MISO数据输出 |
| PA7 | MOSI数据输入 |
| PA8 | DRDY数据就绪 |
| PB0 | RESET复位 |
| PB1 | START启动 |
这组引脚在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铜箔电阻、参考电阻初始精度还会留下固定偏差。这时候就需要校准。
我的做法是在批量生产中做零点和增益两点校准,配方不复杂:
- 把PT100探头放进冰水混合物中,记录ADC读数作为零点校准点,温度参考值0℃。
- 把探头放进沸水环境中,记录第二个读数,温度参考值100℃。
- 计算出增益修正系数:
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用顺手之后,再回头去看那些靠运放堆出来的测温电路,是真的想感叹集成方案省心太多。