先把我最近踩完的一个坑完整交代一遍。项目里要用 GD32F30x 采集 PT1000 铂电阻的温度,一开始图省事直接用单片机自带的 12 位 ADC,结果在 0°C 附近连 0.1°C 都分辨不出来,整个系统测出来的温度像在坐电梯。后来换成 CS1237 这颗 24 位 Sigma-Delta ADC,从硬件接线到软件读码,再到把 ADC 原始码值换算成 PT1000 阻值、最后算出温度,整条链路打通之后,问题才算真正收尾。这篇文章就是完整复现这条链路:GD32F30x 驱动 CS1237、比例法测电阻、IEC 60751 公式换算温度,以及我在实际调试里碰到的一堆坑和排查方法。
如果你正在做工业温度采集、暖通传感器、电池包热管理、电机保护这类需要“慢但准”的场景,或者刚接触高精度 ADC、对数字滤波和传感器标定没什么头绪,这篇文章应该能帮你少走不少弯路。文章里所有代码都基于 GD32 标准外设库,拿过去改个引脚就能跑。
1. 方案与测量原理
1.1 为什么是“GD32F30x + CS1237 + PT1000”
GD32F30x 是一颗 Cortex-M4 内核的国产 MCU,主频、Flash、外设资源都不错,价格也合适,很多量产项目里都拿它当主控。但它内置 ADC 只有 12 位,做一般的电压采集没问题,做 PT1000 精密测温就有点勉强。
PT1000 在 0°C 时的标称阻值是 1000Ω,温度每升高 1°C,阻值大约增加 3.85Ω。如果要求分辨率到 0.1°C,那就得能分辨 0.385Ω 的阻值变化。用 12 位 ADC 配合一个 2.2kΩ 的分压电阻去测,整个 -50°C 到 200°C 的温度范围内,ADC 码值变化大约只有 700~900 个 LSB,折合下来一个 LSB 对应 0.3°C 以上,这还没算噪声。也就是说,12 位 ADC 在这个场景里连“稳定显示 0.1°C”都做不到。
CS1237 是芯海科技一颗 24 位 Sigma-Delta ADC,内部带 PGA,可以放大 1~128 倍,接口非常精简,只有一根时钟线和一根双向数据线,成本也很低。它天生就是给称重传感器、电桥、精密电阻测量这类小信号高精度场景用的。配合 PT1000 本身信号幅度不算小,PGA 设成 1 倍就够,24 位分辨率能把这个测温方案的分辨率做到 0.001°C 级别,剩下的问题主要是噪声和标定,而不是 ADC 位数不够。
1.2 比例法测量,让结果不依赖 3.3V 精度
测温电路我选的是最常见的串联分压结构:3.3V 电源先经过一个精密电阻 Rref,再经过 PT1000 到 GND,CS1237 的差分输入端 VIN+ 接两个电阻的中间节点,VIN- 接 GND。REF+ 和 REF- 直接接 3.3V 和 GND,不另做基准源。
这里有个核心思路叫“比例测量”。ADC 输出码值本质上是输入电压和参考电压的比值:
raw / full = VIN / VREF在这个电路里:
VIN = VDD * R_pt1000 / (Rref + R_pt1000) VREF = VDD所以:
raw / full = R_pt1000 / (Rref + R_pt1000)你看,VDD 在分子分母里同时出现,最终约掉了。也就是说,3.3V 电源纹波大一点、LDO 输出偏一点,只要 VDD 和 VREF 是同一条电源轨,测量结果几乎不受影响。这就是比例测量比“固定稳压源 + 固定参考电压”省心的原因,也非常适合不追求极端精度但想省成本的嵌入式方案。
如果你换一种接法,比如用专门的恒流源给 PT1000 供电,再用高精度基准芯片做参考电压,那当然可以做得更准,但电路成本、PCB 面积、调试工作量都会上来。对于大多数非计量级的工业测温场景,比例法在“够用”和“简单”之间是最平衡的。
1.3 先算一笔账:Rref、分辨率与温度误差
Rref 的选择直接影响测量范围和灵敏度。假设温度范围是 -50°C 到 200°C,PT1000 阻值大概在 803Ω 到 1758Ω 之间。我选了 2.2kΩ 的精密电阻作为 Rref,原因有三个:
- 在 -50°C 时,中间节点电压约为 VDD 的 26.7%;在 200°C 时,中间节点电压约为 VDD 的 44.4%。输入信号不会超过 ADC 参考电压,也不会接近满量程饱和。
- 工作电流大约是 3.3V / (2.2k + 1k) ≈ 1mA,PT1000 自身的发热功率约 1mW,自热效应可以接受。
- 2.2kΩ 是 E96 系列里很容易买到的 0.1% 贴片电阻,不需要定制。
用 24 位 ADC 时,正满量程码值是 0x7FFFFF,也就是 8388607。温度从 -50°C 到 200°C,码值变化大约从 0.267 × 8388607 ≈ 224 万,到 0.444 × 8388607 ≈ 372 万,跨度约 148 万个码。250°C 的温度跨度分了 148 万份,理想分辨率远超 0.001°C。当然,Sigma-Delta ADC 有噪声,实际有效位数可能是 18~20 位,但依然足够。
Rref 的精度会直接转成温度误差。如果 Rref 是 ±0.1%,在 0°C 附近大约产生 1000 × 0.1% = 1Ω 的误差,对应温度误差约 0.26°C。所以我建议用 0.1% 起步,有条件就上 0.05% 或更高精度,温度采集的最终精度上限很大程度取决于这颗电阻。
2. 硬件接线
2.1 元件清单
整个实验需要的东西不多:
- GD32F30x 最小系统板或者你自己的 GD32F30x 核心板
- CS1237 模块,SOP-8 封装
- PT1000 铂电阻,A 级或 B 级都可以,A 级更好
- 2.2kΩ 精密电阻,0.1% 精度,温漂越低越好
- 4.7kΩ 上拉电阻,用在 SDA 数据线上
- 10μF 和 0.1μF 电容各几个,做电源去耦
- 3.3V LDO,比如 AMS1117-3.3 或者更好的低噪声 LDO
- 万用表或精密电阻箱,用来标定和验证
2.2 接线图与引脚对照
我的接线方式如下表:
| CS1237 引脚 | 连接到 |
|---|---|
| VDD / AVDD / DVDD | 3.3V,就近放 0.1μF + 10μF 电容 |
| VSS / AVSS / DVSS | GND |
| VIN+ | Rref 与 PT1000 的中间节点 |
| VIN- | GND |
| REF+ | 3.3V |
| REF- | GND |
| SCLK | GD32F30x PB0 |
| SDA / DOUT | GD32F30x PB1,另接 4.7kΩ 上拉到 3.3V |
对应的串联分压电路可以画成这样:
3.3V ---- Rref(2.2kΩ) ----+---- PT1000 ---- GND | CS1237 VIN+ VIN- = GND REF+ = 3.3V REF- = GND这里有一个容易忽略的点:PT1000 和 Rref 的中间节点同时接在 VIN+ 上,而 VIN- 接 GND,所以 ADC 测量的是 PT1000 两端的电压。REF+ 和 REF- 直接接整个串联电路的电源两端,这样 ADC 的参考电压就是整个串联回路的总电压。只要这两个电压来源于同一条电源轨,比例关系就成立。
如果你用的是集成好的 CS1237 模块,很多模块已经把 REF+、REF- 在 PCB 内部接好,只引出 VCC、GND、SCK、DOUT 四个脚。这种模块一般默认用 3.3V 做参考,我建议你确认一下模块原理图,如果模块上根本没引出 REF 引脚,那也说明它内部已经帮你处理了。
2.3 电源、去耦与布板注意
CS1237 是 Sigma-Delta ADC,对电源噪声比普通 MCU 敏感。不要直接用一个 DCDC 输出的 3.3V 裸奔,最好加一级 LDO,LDO 后面放一个 10μF 钽电容或者陶瓷电容,再在 CS1237 的 VDD 引脚旁边放一个 0.1μF 高频去耦电容。
SCLK 和 SDA 两根线要尽量短,不要和电机驱动、PWM 输出、WiFi 天线这些强干扰源平行走线。如果必须布长线,SDA 上可以串一个 100Ω 电阻,再接 4.7kΩ 上拉,这样能有效抑制振铃和 RF 干扰。
GD32F30x 和 CS1237 之间如果距离稍远,建议把 SCLK 频率放慢一点,代码里延时拉长到 2~5μs,别追求极限速率。测温本来就不需要高吞吐,稳定压倒一切。
还有一点是关于 PT1000 的引线。如果 PT1000 和主板距离很近,两线制没问题;如果距离超过几米,导线电阻就会叠加进测量结果,铜导线在温度变化时电阻也会漂。这时候要么改三线制,用两个 ADC 通道分别测量并程序补偿,要么直接用四线制铂电阻。这个教程先按两线制讲,长线场景后续可以单独展开。
3. GD32F30x 驱动 CS1237
3.1 初始化 GPIO:一个时钟线加一根双向数据线
CS1237 的接口不是标准 SPI,它只有 SCLK 和 SDA。SDA 是一根双向数据线:写命令时由 MCU 驱动,读数据时切换到输入模式,由 CS1237 把数据推出来。
GD32 的 GPIO 切换输入输出非常方便,用标准外设库就是 gpio_init 重新配置模式。这里我把 SCLK 放在 PB0,SDA 放在 PB1。
#include "gd32f30x.h" #define CS1237_SCLK_PORT GPIOB #define CS1237_SCLK_PIN GPIO_PIN_0 #define CS1237_SDA_PORT GPIOB #define CS1237_SDA_PIN GPIO_PIN_1 #define CS1237_SCLK_HIGH() gpio_bit_set(CS1237_SCLK_PORT, CS1237_SCLK_PIN) #define CS1237_SCLK_LOW() gpio_bit_reset(CS1237_SCLK_PORT, CS1237_SCLK_PIN) #define CS1237_SDA_HIGH() gpio_bit_set(CS1237_SDA_PORT, CS1237_SDA_PIN) #define CS1237_SDA_LOW() gpio_bit_reset(CS1237_SDA_PORT, CS1237_SDA_PIN) #define CS1237_SDA_READ() gpio_input_bit_get(CS1237_SDA_PORT, CS1237_SDA_PIN) void CS1237_Init(void) { rcu_periph_clock_enable(RCU_GPIOB); gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0); gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1); CS1237_SCLK_LOW(); CS1237_SDA_LOW(); }这里要注意一个细节:CS1237 是按 SCLK 的边沿来计数的,初始化时最好先把 SCLK 拉低,避免上电瞬间 GPIO 抖动导致 CS1237 内部移位寄存器误收几个时钟。我吃过这个亏,调试时第一帧数据总是不对,后来发现是初始化顺序的问题。
再写一个切换 SDA 方向的辅助函数:
static void CS1237_SDA_SetMode(uint8_t output) { if (output) { gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1); CS1237_SDA_HIGH(); } else { gpio_init(GPIOB, GPIO_MODE_IN_PU, GPIO_OSPEED_50MHZ, GPIO_PIN_1); } }读数据时用输入模式带内部上拉,配合外部 4.7kΩ 上拉,可以保证在 CS1237 释放 SDA 的时候这根线不会悬空。
3.2 写配置命令与读 24 位数据
CS1237 上电后默认配置不一定是我们要的,所以必须主动写一次命令字。CS1237 的命令字一共 26 位,按位从高到低发送。我这里只关心两件事:增益设成 1 倍、输出速率设成 10Hz。
命令字位定义我按下面的理解用:
| 位 | 含义 | 本工程取值 |
|---|---|---|
| bit25 | RESET 复位控制 | 0 |
| bit24 | 掉电控制 | 0 |
| bit23 | 通道选择 | 0 |
| bit22-20 | PGA 增益,000 表示 1 倍 | 000 |
| bit19-18 | 输出速率,00 表示 10Hz | 00 |
| bit17-16 | 保留位 | 00 |
| bit15-0 | 本芯片无 CRC,全部补 0 | 0 |
所以实际发送的命令字就是0x00000000。如果你用的是带 CRC 校验的 CS1238,那低位就要按 CRC 算法补齐;CS1237 这个工程直接发 26 个低电平时钟即可。
写命令的函数如下:
void CS1237_WriteCmd(uint32_t cmd) { uint8_t i; CS1237_SDA_SetMode(1); for (i = 0; i < 26; i++) { CS1237_SCLK_LOW(); if (cmd & 0x02000000) { CS1237_SDA_HIGH(); } else { CS1237_SDA_LOW(); } CS1237_SCLK_HIGH(); cmd <<= 1; } CS1237_SCLK_LOW(); CS1237_SDA_HIGH(); }读数据函数是本驱动的核心。CS1237 转换完成后会把 SDA 拉低,表示数据准备好了。这时候 MCU 把 SDA 切成输入,然后给出 24 个 SCLK 脉冲,在每个脉冲读取一位,MSB 在前。读完 24 位之后,再补 1 个 SCLK 脉冲,让 ADC 开始下一轮转换。
int32_t CS1237_ReadRaw(void) { int32_t raw = 0; uint8_t i; CS1237_SDA_SetMode(0); // 等待 CS1237 把 SDA 拉低,表示转换完成 for (i = 0; i < 200; i++) { if (CS1237_SDA_READ() == 0) { break; } delay_1ms(1); } if (i >= 200) { return 0x7FFFFFFF; // 超时 } // 读取 24 位数据,MSB 在前 for (i = 0; i < 24; i++) { CS1237_SCLK_HIGH(); delay_us(2); raw <<= 1; if (CS1237_SDA_READ()) { raw |= 1; } CS1237_SCLK_LOW(); delay_us(2); } // 补一个时钟,触发下一轮转换 CS1237_SCLK_HIGH(); delay_us(2); CS1237_SCLK_LOW(); raw <<= 8; raw >>= 8; // 24 位数据做符号扩展,得到 int32_t return raw; }函数返回的是 int32_t,不是 uint32_t。因为 CS1237 输出是二进制补码,正输入时最高位是 0,负输入时最高位是 1。做一次左移 8 位、再算术右移 8 位,就能把 24 位补码正确扩展成 32 位有符号数。
如果你的板子和我用的时序正好反过来,读出来总是乱码,可以把采样点从上升沿改成下降沿试试。也就是先采样,再拉升 SCLK,或者把读 SDA 的位置移到 SCLK 变低之后。这是个很常见的兼容性问题,不同批次模块上可能表现不一样。
3.3 数字滤波:去极值滑动平均
CS1237 在 10Hz 输出速率下,单独看每一帧数据还是会有少量抖动,尤其当电源不干净或者 PCB 布局不好的时候。温度信号本身变化慢,完全可以用滤波把它磨平。
我用的滤波方式是“去极值滑动平均”:先连续采 N 个点,丢掉一个最大值和一个最小值,剩下 N-2 个点取平均。这样既能滤掉随机噪声,又能干掉偶发的异常毛刺。
int32_t CS1237_ReadFiltered(uint8_t times) { int32_t sum = 0; int32_t min = 0x7FFFFFFF; int32_t max = -0x7FFFFFFF; int32_t v; uint8_t i; if (times < 3) { times = 3; } for (i = 0; i < times; i++) { v = CS1237_ReadRaw(); if (v == 0x7FFFFFFF) { i--; continue; } sum += v; if (v < min) min = v; if (v > max) max = v; } return (sum - min - max) / (times - 2); }注意if (v == 0x7FFFFFFF)这种超时判断不能丢,尤其是上电初期,CS1237 可能需要几百毫秒才输出第一帧有效数据。如果连续超时,直接死等会卡住主循环。
采样次数 times 我一般取 8。10Hz 输出速率下,采 8 个点大约需要 0.7~0.8 秒,响应速度对温度控制来说够用。如果你希望响应更快,可以把命令字里的速率改成 30Hz 或 60Hz,但噪声会略微变大,times 可以适当减少。
3.4 采样节奏控制
调用时机也是有讲究的。CS1237 配置成 10Hz 后,每 100ms 出一帧数据。如果你在 while(1) 里疯狂调用 ReadRaw,函数里等待 SDA 拉低的部分会主动阻塞到下一帧数据准备好,所以不会重复读到旧数据。
初始化完成后,代码流程可以这样写:
int main(void) { int32_t raw; float res; float temp; systick_config(); CS1237_Init(); CS1237_WriteCmd(0x00000000); // PGA = 1,输出速率 10Hz delay_1ms(200); // 等第一帧转换完成 while (1) { raw = CS1237_ReadFiltered(8); if (raw == 0x7FFFFFFF) { printf("CS1237 read timeout\r\n"); continue; } res = CS1237_RawToResistance(raw); temp = PT1000_ResistanceToTemp(res); printf("raw=%d, R=%.2f Ohm, T=%.2f C\r\n", raw, res, temp); } }CS1237_WriteCmd 只需要在初始化时发送一次。除非你需要切换增益或速率,否则不要每帧都发命令,否则会打乱它内部的转换节奏。读码函数末尾补的那个 SCLK 已经通知芯片开始下一轮转换了。
4. 从 ADC 码到阻值再到温度
4.1 阻值换算公式
前面推导过比例关系:
raw / full = R_pt1000 / (Rref + R_pt1000)其中full = 0x7FFFFF = 8388607,这是因为 CS1237 是带符号的 24 位 ADC,正满量程是 8388607。解方程得到:
R_pt1000 = Rref * raw / (full - raw)这段代码很直接:
#define CS1237_FULL_SCALE 8388607.0f #define RREF 2200.0f float CS1237_RawToResistance(int32_t raw) { float r; if (raw < 0) { raw = 0; // 输入接反或异常 } r = RREF * (float)raw / (CS1237_FULL_SCALE - (float)raw); return r; }如果 raw 变成负数,大概率是 VIN+ 和 VIN- 接反了,或者 RTD 两端虚焊导致输入悬空,先不要急着改算法,去查硬件。
raw 接近 0 说明 PT1000 阻值接近 0,可能是短路。raw 接近满量程说明 R_pt1000 非常大,常见原因就是 PT1000 断路,公式里分母接近 0,计算结果会爆炸成天文数字,正好可以作为断线故障的判断条件。
4.2 PT1000 的阻值—温度关系
PT1000 的阻值随温度变化不是完全线性的,工业上最常用的标准是 IEC 60751。0°C 以上用二次多项式就能描述:
R(T) = R0 * (1 + A*T + B*T^2)0°C 以下需要再加上一个三次修正项:
R(T) = R0 * (1 + A*T + B*T^2 + C*(T-100)*T^3)其中对于 PT1000:
- R0 = 1000Ω
- A = 3.9083e-3
- B = -5.775e-7
- C = -4.183e-12
几个关键温度点的标准阻值如下表,可以用来验证你的程序算得对不对:
| 温度 | PT1000 标准阻值 |
|---|---|
| -50°C | 803.06Ω |
| 0°C | 1000.00Ω |
| 25°C | 1097.35Ω |
| 50°C | 1193.97Ω |
| 100°C | 1385.06Ω |
| 150°C | 1573.25Ω |
| 200°C | 1758.56Ω |
网上也有很多人用简化线性公式,也就是认为每 1°C 对应 3.85Ω。这个公式在 0°C 附近误差不大,但到了 200°C 误差可能到 2~3°C。所以我建议要么用上面的 IEC 60751 多项式,要么做一个查找表加线性插值。查找表适合 Flash 充足、想避开浮点运算的场景;多项式适合代码简单、追求连续输出的场景。
4.3 C 语言温度求解代码
从阻值反算温度,0°C 以上可以直接解二次方程。方程是:
B * T^2 + A * T + (1 - R/R0) = 0求根公式取正根:
T = (-A + sqrt(A^2 - 4*B*(1 - R/R0))) / (2*B)0°C 以下因为有三次项,没法直接求解析解,我用牛顿迭代法,迭代十几次就能收敛到 0.001°C。完整代码如下:
#include <math.h> float PT1000_ResistanceToTemp(float r) { const float R0 = 1000.0f; const float A = 3.9083e-3f; const float B = -5.775e-7f; const float C = -4.183e-12f; float t; float disc; if (r >= R0) { // 0°C 及以上:二次方程求解 disc = A*A - 4.0f * B * (1.0f - r / R0); if (disc < 0.0f) { disc = 0.0f; } t = (-A + sqrtf(disc)) / (2.0f * B); } else { // 0°C 以下:牛顿迭代 t = -100.0f; // 初始猜测值 for (int i = 0; i < 20; i++) { float rt = R0 * (1.0f + A*t + B*t*t + C * (t - 100.0f) * t*t*t); float dRdT = R0 * (A + 2.0f * B*t + C * (4.0f*t*t*t - 300.0f*t*t)); float delta = (rt - r) / dRdT; t -= delta; if (fabsf(delta) < 0.0001f) { break; } } } return t; }如果你不太想维护浮点迭代,也可以把 -50°C 到 200°C、每 1°C 的阻值表提前烧进 Flash,运行时用二分查找加线性插值。精度取决于表的密度,1°C 步进的表插值误差可以控制在 0.05°C 以内。这个表可以用一段小脚本生成,把常数代入公式逐点打印即可。
4.4 标定与误差控制
即使程序换算公式完全正确,硬件误差依然会进到最终温度里。主要的误差来源有三个。
第一个是 Rref 的精度和温漂。前面算过,0.1% 的 Rref 就能带来大约 0.26°C 的温度误差,所以 Rref 必须用好电阻。如果项目允许,我会在出厂前做一次软件标定:用 0.01% 的精密电阻替换 PT1000,比如放一个 1000Ω 和一个 1400Ω 的标准电阻,记录 ADC 原始码,然后反推出实际 Rref 值,把软件里的 RREF 常量修正掉。
第二个是导线电阻。两线制下,PT1000 两端的引线电阻会直接加在测量值里。0.5m 的普通连接线可能带来 0.1Ω 左右的误差,对应 0.03°C 左右,短距离可以忽略,长距离就得改三线制。
第三个是自热效应。1mA 电流流过 PT1000,在 0°C 时大约产生 1mW 功率。在空气中,这个自热可能造成 0.1~0.4°C 的偏高;如果传感器埋在导热良好的介质里,影响会小很多。想要进一步压低自热,可以增大 Rref 到 4.7kΩ,让工作电流降到 0.6mA 左右,代价是分辨率略微下降,但 24 位 ADC 完全扛得住。
5. 调试踩坑与实测记录
5.1 读数为 0、满量程、乱跳的处理
我调试过程中最常见的故障现象就三种:读码永远是 0、读码接近满量程、读码上下乱跳。对应的排查思路也基本固定,整理成表:
| 现象 | 可能原因 | 处理措施 |
|---|---|---|
| raw 始终为 0 或很小 | VIN+ 与 VIN- 之间短路,PT1000 短路,虚焊 | 用万用表测 VIN+ 电压,应该接近 GND |
| raw 接近满量程或异常大 | PT1000 开路,或 VIN+ 悬空,或 Rref 断路 | 测中间节点电压,开路时接近 3.3V |
| 等待 SDA 拉低超时 | CS1237 没供电、SCLK 极性不对、模块损坏 | 查 VDD 电压,查 SCLK 是否正常翻转 |
| raw 乱跳 | 电源噪声、SDA 上拉缺失、采样边沿不对 | 加 LDO、加 4.7kΩ 上拉、换采样边沿 |
| 数据稳定但整体偏大 | 导线电阻、Rref 偏大、自热 | 软件标定 Rref,或改用三线制 |
还有一个特别容易踩的点:CS1237 的 SDA 是双向引脚,不是普通 SPI 的 DOUT。如果你把它当成只读引脚,一直配成输入模式,写命令的时候数据根本进不去,芯片就会一直保持默认配置,读数不可能对。我建议调试时先用示波器抓 SDA,写命令阶段能看到 MCU 拉出来的波形,读数据阶段能看到 CS1237 主动推出来的波形,一眼就能判断驱动方向切换对不对。
5.2 温度不准的排查
如果 ADC 读数稳定,换算出来的阻值也和万用表测到的 PT1000 阻值对得上,但最终温度不对,那问题基本出在阻值到温度这一步。
先用 0°C 验证。把 PT1000 放到冰水混合物里,程序应该算出约 1000Ω、温度 0°C。如果显示 1°C 以上,优先怀疑自热,断电后用万用表单独测阻值,应该接近 1000Ω。
再用 100°C 验证。把 PT1000 放到沸水里,程序应该算出约 1385Ω、温度 100°C。如果这里偏差大,检查你的公式是用的 PT100 常数还是 PT1000 常数,很多人把 R0 写成 100 就全偏了。
如果高低点都偏同一个固定值,比如都偏 0.5Ω,那是导线电阻或者接触电阻,软件里减一个固定偏移即可。如果偏移随温度变化,那基本是 Rref 实际值和理论值不一致,用标准电阻箱做两点修正。
5.3 实测数据对比
我在调试时用精密电阻箱模拟 PT1000 的阻值,再把程序和万用表读数做了一组对比。实测下来是这样的:
| 电阻箱设定值 | 程序计算阻值 | 程序计算温度 |
|---|---|---|
| 1000.0Ω | 1000.3Ω | 0.08°C |
| 1097.4Ω | 1097.7Ω | 25.10°C |
| 1194.0Ω | 1193.7Ω | 49.90°C |
| 1385.1Ω | 1386.2Ω | 100.30°C |
偏差主要来自 Rref 的 0.1% 误差和读数噪声,没有做软件标定之前能到这个水平,我认为这套方案已经非常可用了。如果你把 Rref 也换成 0.05% 以下的电阻,再在软件里做一次零点和增益校准,整体做到 ±0.15°C 不是问题。
5.4 几个容易被忽略的细节
最后分享几个我在项目里总结出来的小经验。
第一,调试阶段一定要先把原始 ADC 码值通过串口打出来,不要上来就直接看温度。原始码值能帮你区分是硬件问题还是换算问题。比如换算公式写错时,原始码可能是对的,但你看到的温度全错。
第二,CS1237 的 SCLK 和 SDA 在 GD32 上占用普通 GPIO 就行,不需要映射到 SPI 外设。GPIO 翻转速度足够,而且软件模拟时序在调试时更容易看清每一步。
第三,CS1237 是慢速高精度 ADC,别拿它做高速采样。10Hz 输出速率,加上 8 次滤波,一帧有效数据要 0.8 秒。如果你的系统需要更快的温度响应,可以把速率调到 30Hz 或者 60Hz,但滤波次数和噪声要重新平衡。
第四,给 CS1237 供电的 LDO 尽量选低噪声的,别用那种开关电源的 3.3V 直供。比例测量虽然能抵消 VDD 的慢漂移,但对 VDD 上的高频噪声没有免疫力,这部分噪声会直接进到 ADC 参考电压和输入电压里。
第五,如果你以后要量产,建议把 Rref 的选型和焊接管控写进工艺文件。这颗电阻的温漂系数比容差更关键,普通的 0805 贴片电阻温度系数可能有 100ppm/°C,而 25ppm/°C 的精密电阻也好找,价格差不了多少,但对最终温飘影响很大。
这套“GD32F30x + CS1237 + PT1000”方案做下来,整体成本很低,电路简单,精度和稳定性却比我预想的好很多。如果你只是需要做一个高精度的单点温度采集,不需要触摸、显示这些外设,这个组合完全可以直接搬进产品里。后续如果要做多通道测温,可以考虑多片 CS1237 挂在不同 GPIO 上,或者换带多通道输入的 CS1238,软件架构基本不用大改。