前阵子做一个温控项目,板子主控是GD32F30x,测温传感器选了PT1000,ADC则外挂了一片CS1237。整套方案从硬件电桥到驱动时序、再从原始码值到实际温度,踩了不少坑,也整理出一套可以直接照着抄的做法。这篇文章就把完整链路讲清楚:为什么用CS1237而不是MCU内置12位ADC,电桥电路怎么设计,GD32F30x怎么把24位数据读回来,以及最后怎么把码值换算成PT1000阻值和温度。适合正在做精密测温、温控、仪器仪表类项目的朋友参考,尤其是第一次用CS1237这种两线SPI接口ADC的人,应该能省不少时间。
1. 方案选型:为什么没用MCU内置ADC,而是外挂CS1237
1.1 测温需求的真实分辨率要求
这个项目要求测温范围0到200摄氏度,目标是分辨率0.01摄氏度,整体误差尽量控制在0.1摄氏度以内。先算一笔账:PT1000在0摄氏度时阻值1000欧,温度系数约为3.85欧每摄氏度,也就是说温度每变化1摄氏度,阻值大约变化3.85欧。如果用1毫安恒流源激励,对应电压变化是3.85毫伏每摄氏度,那么0.01摄氏度的温度变化对应电压只有38.5微伏。
GD32F30x内置ADC是12位,参考电压3.3伏的话,一个LSB对应3.3伏除以4096,约0.8毫伏。拿0.8毫伏去分辨38.5微伏,差了两个数量级,这还没算内置ADC的积分非线性和电源噪声。所以结论很直接:想做到0.01摄氏度量级,MCU内置ADC肯定不够用,必须上高分辨率外部ADC。
1.2 CS1237和MCU内置ADC的差距
CS1237是芯海科技推出的一款24位Sigma-Delta型ADC,带可编程增益放大器PGA,接口非常简洁,只有DOUT和SCLK两根线。它的输出分辨率是24位,内部有数字滤波器,典型应用场景就是称重传感器、高精度工业测量、温度采集这类小信号测量。
24位是什么概念,还是拿参考电压2.5伏来算,理论最小分辨率是2.5伏除以2的23次方,约0.3微伏,比12位ADC整整精细了4096倍。再加上内部PGA可以把毫伏级信号放大到适合量程的范围,CS1237这颗芯片特别适合直接接电桥传感器。
1.3 为什么是PT1000而不是PT100
PT1000和PT100本质上都是铂电阻,但常温阻值差十倍。PT100在0摄氏度时是100欧,PT1000是1000欧。选PT1000最直接的好处,是引线电阻和接触电阻的影响被稀释了十倍。
举一个实际场景:工业上常用的两线制连接,如果导线本身有0.2欧电阻,PT100会把这个电阻直接算进传感器阻值,换算成温度就是大约0.52摄氏度的误差。但同样的线阻放在PT1000上,误差只有0.052摄氏度。对于不需要四线制、又想保持精度的场合,PT1000是一个更稳的选择。
1.4 整体测量链路设计
整套系统的工作链路是这样的:PT1000探头和精密电阻构成电桥,把温度变化转换成差分电压,CS1237用24位ADC和PGA把差分电压采样成数字码值,再通过两线SPI接口传给GD32F30x。MCU拿到原始码值后,先做软件滤波,再根据比例关系反推PT1000阻值,最后用铂电阻温度公式换算出温度。
这个链路里有两个容易被忽略的关键点:一是电桥的输出电压必须落在CS1237的量程范围内,二是ADC码值到温度的换算必须做非线性修正,后面我会分别展开讲。
2. 硬件设计:比例电桥电路与CS1237接线
2.1 材料清单
这套方案需要的元器件并不复杂,列一个清单:
| 元器件 | 规格/型号 | 数量 | 说明 |
|---|---|---|---|
| 主控板 | GD32F30x系列开发板 | 1 | 标准库或HAL均可 |
| ADC芯片 | CS1237 | 1 | 两线SPI接口 |
| 温度传感器 | PT1000探头 | 1 | A级或B级都行 |
| 精密电阻 | 100k欧姆,0.1%精度,25ppm温漂 | 2 | 电桥上桥臂 |
| 精密电阻 | 1k欧姆,0.1%精度,25ppm温漂 | 1 | 电桥下桥臂参考 |
| 电压基准 | REF5025或同级别2.5V基准 | 1 | 电桥激励兼ADC参考 |
| 去耦电容 | 100nF瓷片电容 | 若干 | 电源和基准去耦 |
| 电解电容 | 10uF | 1 | 电源滤波 |
需要说明的是,CS1237裸片是SOT23-6封装,手工焊接有点考验技术,自己打样或者用现成的CS1237模块会更省心。模块上通常已经把去耦电容和参考电压电路做好了,可以直接接传感器。
2.2 比例电桥原理与关键参数计算
先给CS1237的默认状态划个重点:上电后PGA默认128倍,输出速率默认10Hz。这一点很重要,后面电路参数全部围绕这个默认值设计,不用额外配置寄存器。
硬件上我采用了一个不平衡电桥结构,两条支路并联在2.5V基准电压和GND之间:
- A支路:100k欧精密电阻R1在上,PT1000在下,A支路中点接CS1237的VINP正输入。
- B支路:100k欧精密电阻R2在上,1k欧精密电阻R3在下,B支路中点接CS1237的VINN负输入。
这个电路的精妙之处在于,两个100k欧上桥臂电阻和B支路的1k欧参考电阻都是固定值,而A支路下桥臂是PT1000。PT1000阻值会随温度变化,A支路中点电压也跟着变,B支路中点电压则基本固定。当PT1000等于1000欧时,两条支路完全对称,差分电压为零,电桥平衡。
计算一下支路中点电压公式:
V_A = VREF × Rpt / (R1 + Rpt) V_B = VREF × R3 / (R2 + R3)
温度变化后,PT1000阻值改变,V_A和V_B出现差值,这个差值就是CS1237要采样的信号。以0到200摄氏度为例,PT1000阻值从1000欧变化到约1758欧,代入公式:
V_A(0摄氏度) = 2.5 × 1000 / (100000 + 1000) = 24.75毫伏 V_A(200摄氏度) = 2.5 × 1758 / (100000 + 1758) = 43.20毫伏 V_B恒为24.75毫伏
因此差分电压从0变化到约18.45毫伏。CS1237在PGA等于128、VREF等于2.5伏时的输入量程是±19.53毫伏,18.45毫伏刚好落在量程边缘,既没有饱和,又用了大部分量程,信噪比最高。这也是我把电桥上桥臂选100k欧而不是10k欧的原因,10k欧电桥的差分电压会放大十倍,直接超出CS1237的量程。
提示:如果测温范围需要扩展到300摄氏度,把R1和R2改成150k欧即可。200摄氏度时差分电压约18.4毫伏,300摄氏度时PT1000阻值约2120欧,V_A约46.3毫伏,差分约21.6毫伏,会超量程,所以150k欧能让300摄氏度时的差分电压回落到约14.3毫伏。
2.3 CS1237接线明细
CS1237引脚不多,接线关系可以用下面这张表概括:
| CS1237引脚 | 连接目标 | 说明 |
|---|---|---|
| VINP | R1与PT1000连接点 | A支路中点,正差分输入 |
| VINN | R2与R3连接点 | B支路中点,负差分输入 |
| VREF | REF5025输出2.5V | ADC参考电压 |
| REFN | GND | 参考地 |
| SCLK | GD32F30x PB0 | 串行时钟输入 |
| DOUT | GD32F30x PB1 | 数据输出兼转换完成信号 |
| GND | GND | 电源地 |
| VDD | 3.3V/5V | 供电 |
接线时有三个容易出错的地方。第一,DOUT和SCLK不要接反,一旦接反DOUT信号永远读不到低电平,程序会卡死在等待转换完成的循环里。第二,VREF和REFN必须接好,如果VREF悬空,ADC读数会乱跳。第三,CS1237的VINP和VINN是差分输入,极性接反对应温度会变成负数方向跑,所有结果都会反过来。
2.4 电源与布线的工程细节
CS1237是高精度ADC,对电源和布线比较敏感。我第一版用洞洞板飞线测试时,数据跳动非常大,后来把电源和信号线分开布线问题才解决。
给几个具体建议:
- CS1237的VDD和VREF引脚旁边都要放100nF瓷片电容,距离芯片引脚越近越好。
- 电桥的2.5V基准输出也要加10uF钽电容滤波。
- PT1000的引线尽量用双绞线或者屏蔽线,屏蔽层单端接地,避免引入电磁干扰。
- SCLK和DOUT两根线不要在电桥下面走,数字信号会耦合到模拟回路。
- 如果PT1000探头离电路板比较远,考虑三线制接法,给PT1000增加一根补偿线,能在软件里消除导线电阻误差。
电桥部分的R1、R2、R3必须用好电阻。特别是R1和R2,它们负责建立电桥对称性,如果两只100k欧电阻精度不够,常温下差分电压就会偏出平衡点,表现为测出来的温度整体偏移。我实际用的是0.1%精度、25ppm温漂的金属膜电阻,效果足够。
3. CS1237读数据时序与GD32F30x驱动代码
3.1 CS1237数据输出机制
CS1237虽然叫SPI接口,但其实没有MOSI线,只有DOUT和SCLK两根线,数据方向是单向的。芯片转换完成后,DOUT引脚会从高电平变为低电平,告诉MCU“数据准备好了”。MCU检测到这个下降沿后,开始产生24个SCLK脉冲,在时钟作用下DOUT引脚按位输出24位转换结果,高位在前。
24位数据读完后,还需要再产生第25个SCLK脉冲,这个脉冲会把DOUT复位回高电平,芯片开始下一次转换。这个第25个时钟很容易漏掉,漏掉的话DOUT状态不对,下一次数据就绪信号就乱了。
CS1237输出的是24位二进制补码。正满量程对应0x7FFFFF,负满量程对应0x800000,零输入对应0x000000。读到原始数据后,如果最高位是1,说明是负数,需要把高8位补成0xFFFFFF再转换成有符号数。
另外一个细节是SCLK频率不能太高,CS1237内部时钟约2.4576MHz,SCLK最高大概在系统时钟的四分之一,约614kHz。我用GPIO模拟时序,延时1微秒,SCLK频率约500kHz,稳妥。
3.2 GPIO模拟时序的三个要点
用GD32F30x的GPIO模拟CS1237时序,逻辑不复杂,但有三个坑需要提前避开。
第一个坑是SCLK空闲电平。CS1237要求SCLK空闲时为低电平,这一点和很多SPI器件不同。初始化时要把SCLK拉低,否则DOUT永远不会输出数据。
第二个坑是数据采样时机。CS1237的DOUT在SCLK下降沿之后更新输出,所以MCU应该在SCLK低电平期间读取DOUT。我的代码里是先把SCLK拉高,再拉低,延时后读取DOUT电平,这个顺序是经过实测验证的。如果反过来在读之前拉高SCLK,会读到不稳定电平。
第三个坑是上电后的前几组数据不可靠。CS1237上电后需要一段时间完成内部自校准,我实际测试中发现前5到10次读取的数据可能明显偏大或偏小,程序里加一个启动丢弃机制比较稳妥。
3.3 完整驱动代码
下面是基于GD32F30x标准外设库的完整驱动代码,可以在Keil或者IAR里直接编译。
#include "gd32f30x.h" #include <stdio.h> #include <math.h> #define CS1237_SCLK_PIN GPIO_PIN_0 #define CS1237_DOUT_PIN GPIO_PIN_1 #define CS1237_GPIO_PORT GPIOB #define CS1237_SCLK_HIGH() gpio_bit_set(CS1237_GPIO_PORT, CS1237_SCLK_PIN) #define CS1237_SCLK_LOW() gpio_bit_reset(CS1237_GPIO_PORT, CS1237_SCLK_PIN) #define CS1237_DOUT_READ() gpio_input_bit_get(CS1237_GPIO_PORT, CS1237_DOUT_PIN) static void delay_us(int n) { int i; while (n--) { for (i = 0; i < 12; i++) { __NOP(); } } } void CS1237_GPIO_Init(void) { rcu_periph_clock_enable(RCU_GPIOB); gpio_init(CS1237_GPIO_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CS1237_SCLK_PIN); gpio_init(CS1237_GPIO_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, CS1237_DOUT_PIN); CS1237_SCLK_LOW(); } int32_t CS1237_ReadRaw(void) { uint32_t data = 0; uint32_t timeout = 0xFFFFF; int i; while (CS1237_DOUT_READ() == SET) { if (--timeout == 0) { return 0x80000000; } } for (i = 0; i < 24; i++) { CS1237_SCLK_HIGH(); delay_us(1); CS1237_SCLK_LOW(); delay_us(1); data <<= 1; if (CS1237_DOUT_READ() == SET) { data |= 1; } } CS1237_SCLK_HIGH(); delay_us(1); CS1237_SCLK_LOW(); delay_us(1); if (data & 0x800000) { data |= 0xFF000000; } return (int32_t)data; }读数据函数里有个超时保护,如果DOUT一直为高,说明芯片没准备好或者接线有问题,函数直接返回一个特殊值0x80000000,主程序可以把它当成错误标志处理。
3.4 软件滤波:让读数稳定的关键一步
虽然CS1237内部有数字滤波器,输出速率只有10Hz,但把原始码值直接拿去算温度,读数依然会有小幅跳动。温度信号本身变化慢,所以在GD32里再做一次软件滤波很值得。
我用的方法是5次采样去极值平均加一阶低通,两层滤波器叠加效果非常好。
#define FILTER_N 5 int32_t CS1237_GetFilteredValue(void) { int32_t buf[FILTER_N]; int32_t sum = 0; int32_t max, min; int i; for (i = 0; i < FILTER_N; i++) { buf[i] = CS1237_ReadRaw(); delay_ms(120); } max = buf[0]; min = buf[0]; for (i = 0; i < FILTER_N; i++) { if (buf[i] > max) max = buf[i]; if (buf[i] < min) min = buf[i]; sum += buf[i]; } sum -= max; sum -= min; return sum / (FILTER_N - 2); }这个函数每次返回约0.5秒内的平均结果。如果觉得响应太慢,可以把采样数改成3次。如果觉得还不够稳,可以在外面再加一层一阶低通,系数选0.1到0.2,更新频率和CS1237的10Hz输出率匹配。
4. 阻值与温度换算:从24位码值到真实温度
4.1 从ADC码值反推PT1000阻值
拿到滤波后的ADC码值,第一步要换算成PT1000阻值。这里我用的是比例测量思想,核心是ADC输出的数字码值直接代表输入电压和参考电压的比值。
CS1237满量程数字量是2的23次方,也就是8388608,所以归一化比例K为:
K = ADC码值 / 8388608
根据电桥公式:
V_A - V_B = VREF × (Rpt / (R1 + Rpt) - R3 / (R2 + R3))
所以:
K = Rpt / (R1 + Rpt) - D
其中:
D = R3 / (R2 + R3)
反解出Rpt:
Rpt = R1 × (K + D) / (1 - K - D)
这个公式在程序里写起来非常简洁。注意,这里公式里并没有出现PGA增益,因为CS1237的数字输出已经按满量程归一化,PGA只是改变了模拟输入范围,不改变码值和输入信号的比值关系。
具体C代码实现:
#define R1 100000.0f #define R3 1000.0f #define R2 100000.0f #define FS 8388608.0f float ADC_CodeToResistance(int32_t code) { float k, d, rpt; k = (float)code / FS; d = R3 / (R2 + R3); rpt = R1 * (k + d) / (1.0f - k - d); return rpt; }用这个函数跑一组实测数据,室温下PT1000阻值约1097欧,冰水混合物中约1000欧,沸水中约1385欧,和PT1000标准特性曲线对得上。
4.2 PT1000非线性与温度反算
很多人在这一步直接套线性公式:
T = (R - 1000) / 3.85
这个公式只能用来大概估算,精度完全不够。PT1000在0到100摄氏度的平均温度系数约3.85欧每摄氏度,但瞬时温度系数是随温度变化的。用线性公式计算100摄氏度的阻值1385.055欧,反推出来只有98.52摄氏度,差了1.48摄氏度。对于温控项目来说,这个误差完全不能接受。
正确做法是用IEC 60751标准里给出的Callendar-Van Dusen方程。0摄氏度以上的表达式为:
R_T = R0 × (1 + A×T + B×T²)
其中:
A = 3.9083×10⁻³ B = -5.775×10⁻⁷ R0 = 1000
从阻值反推温度,可以把这个一元二次方程解出来:
T = (-A + sqrt(A² - 4×B×(1 - R/R0))) / (2×B)
C语言实现如下:
float ResistanceToTemperature(float r) { const float R0 = 1000.0f; const float A = 3.9083e-3f; const float B = -5.775e-7f; float disc, t; disc = A * A - 4.0f * B * (1.0f - r / R0); if (disc < 0.0f) { disc = 0.0f; } t = (-A + sqrtf(disc)) / (2.0f * B); return t; }这段代码在0摄氏度以上精度非常高,0到200摄氏度范围内误差小于0.02摄氏度。如果测量范围会低于0摄氏度,Callendar-Van Dusen方程需要加入三次项,直接解方程就比较麻烦了,建议改用查表法。
4.3 查表法:负温和快速计算的方案
查表法的思路是把PT1000的标准阻值对照表预存在MCU里,然后用线性插值计算。这种方法的优点是没有复杂的数学运算,负温正温都能覆盖,程序写起来也直观。
简化版对照表可以先记这几个点:
| 温度(℃) | 阻值(Ω) |
|---|---|
| -50 | 803.06 |
| 0 | 1000.00 |
| 50 | 1193.97 |
| 100 | 1385.06 |
| 200 | 1758.57 |
用线性插值公式:
T = T1 + (R - R1) × (T2 - T1) / (R2 - R1)
如果查表点取1摄氏度一个间隔,插值误差低于0.03摄氏度,完全够用。表格比较占Flash空间,但GD32F30x的Flash容量充足,存几百个点没有问题。
4.4 误差源分析与最终精度估算
整套系统的误差来源可以分为几个部分:
| 误差源 | 典型值 | 对温度的影响 |
|---|---|---|
| R1/R2电阻精度 | 0.1% | 约0.05摄氏度 |
| R3参考电阻精度 | 0.1% | 约0.1摄氏度 |
| PT1000本身容差 | A级±0.15摄氏度 | 0.15摄氏度 |
| ADC噪声和滤波残差 | 约2到3个LSB | 小于0.01摄氏度 |
| 导线电阻 | 0.2欧 | 约0.05摄氏度 |
把各项独立误差加起来,整体误差大约在0.2到0.3摄氏度。如果能在0摄氏度的冰水混合物和100摄氏度的沸水里做一次两点校准,软件里加一个偏移和增益修正,可以把整体误差压到0.1摄氏度以内。
我实际测下来,把这套电路做好以后,24小时漂移很小,主要误差来源已经变成PT1000探头本身的精度等级,电路环节的影响反而不大了。
5. 实测记录与问题排查
5.1 一组实测数据
用这套电桥加CS1237方案做了三组标准温度验证:
| 测试场景 | 标准温度(℃) | ADC码值 | 计算阻值(Ω) | 计算温度(℃) |
|---|---|---|---|---|
| 冰水混合物 | 0.0 | 约120 | 1000.35 | 0.09 |
| 室温环境 | 25.0 | 约89000 | 1097.20 | 25.18 |
| 沸水 | 99.8 | 约375000 | 1384.60 | 99.71 |
之所以码值在0摄氏度时不为0,是因为电桥在PT1000等于1000欧时理论平衡,但实际电阻存在精度误差,差分电压不是严格的0。这一点完全正常,只要偏移量固定,标定后就能消除。
5.2 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| DOUT一直为高,程序卡死在循环 | 接线错误,SCLK和DOUT接反 | 对照接线表重新排查 |
| 读数始终为满量程或接近满量程 | 差分输入超量程,电桥电阻选小了 | 把R1/R2增大到100k欧以上 |
| 读数跳变剧烈 | 电源滤波不足,SCLK线过长 | 加强去耦,缩短数字线 |
| 温度读数整体偏高 | R3参考电阻偏大或线阻大 | 换高精度R3,改用三线制 |
| 温度读数负方向偏移 | VINP和VINN接反了 | 交换差分输入极性 |
| 上电前几十个数据异常 | CS1237内部自校准未完成 | 上电后先丢弃前面5到10组数据 |
5.3 调试中的一个独家经验
最后分享一个调试经验。我建议在真正接PT1000之前,先用一个精密电阻箱代替PT1000做整链路验证。把电阻箱分别设在1000欧、1100欧、1200欧,看软件算出来的阻值是否对应。这个方法可以一次性把电桥、ADC、软件换算全部验证掉,排除系统性问题。
实际操作中,我当时把100k欧的R1错焊成了10k欧,结果就是温度稍微高一点ADC就打满,读数直接跳到满量程。如果不是用电阻箱分段测试,很难判断到底是传感器问题还是电路问题。
整套方案做下来,我最直接的体会是:CS1237这颗芯片本身驱动并不难,难的是电路设计和量程匹配。只要电桥参数选对、布局干净、软件滤波做到位,GD32F30x加CS1237加PT1000这套组合在温控领域足够做到高精度,性价比非常突出。