ADS1220(24位ADC)+PT100(RTD):高精度(0.1℃)测温实战(二):STM32F103 软件实现全解析,附源码
欢迎关注:公众号:「单片机那点事」
回顾上篇:我们已经搞懂了 ADS1220 的硬件特性、PT100 的工作原理,以及三线制比例测量的"抵消魔法"。
本篇目标:手把手教你用 STM32F103 把这套硬件"唤醒",从 SPI 通信到温度计算,附完整源码解析。
一、软件架构总览:从裸硬件到精准温度值的全链路
在进入代码细节之前,先看一张完整的软件数据流图:
┌────────────┐
│ STM32F103 │
│ 主控逻辑 │
└──────┬──────┘
│
├──► 1. SPI驱动层 (ADS1220.c)
│ ├─ 硬件SPI / 软件SPI
│ ├─ 寄存器读写
│ └─ DRDY状态检测
│
├──► 2. PT100测量层 (PT100.c)
│ ├─ 电阻计算(比例/绝对测量)
│ ├─ 温度转换(查表+插值)
│ └─ 非阻塞状态机
│
└──► 3. 数字滤波层 (filter_int.c)
├─ 简单平均
├─ IIR低通
└─ 去极值平均
核心思路:
- ADS1220.c 负责"说硬件的话"(SPI时序、命令字)
- PT100.c 负责"把数字变成温度"(物理模型)
- filter_int.c 负责"让数据更稳定"(工程优化)
二、第一战:驯服 ADS1220 —— SPI 通信与寄存器配置
2.1 SPI 时序的"坑"与修复
ADS1220 要求 SPI Mode 1(CPOL=0, CPHA=1),这是很多人翻车的第一个坑。

时序关键点:
|
参数 |
说明 |
|---|---|
| CPOL=0 |
空闲时 SCK 保持低电平 |
| CPHA=1 |
数据在第二个边沿(下降沿)采样 |
| MSB First |
高位先传 |
软件SPI实现:
/**
* @brief 软件SPI字节传输 (SPI Mode 1 严格时序)
* @note 时序说明:
* - 初始状态: SCK Low
* - 边沿1 (Rising): Host写MOSI, Slave写MISO
* - 边沿2 (Falling): Host读MISO, Slave读MOSI ← 采样点!
*/
staticuint8_tADS1220_SPI_TransferByte(uint8_t data)
{
uint8_t i;
uint8_t recv = 0;
ADS1220_SCK_Low(); // 确保起始状态为低电平
for (i = 0; i < 8; i++)
{
/* 1. 准备MOSI数据(在上升沿前准备好,保证Setup Time) */
ADS1220_MOSI_Write(data & 0x80);
data <<= 1;
ADS1220_Delay_us(1);
/* 2. SCK上升沿(Shift Edge):Slave输出MISO数据 */
ADS1220_SCK_High();
ADS1220_Delay_us(1);
/* 3. SCK下降沿(Sample Edge):这是Mode 1的采样时刻! */
ADS1220_SCK_Low();
/* 4. 主机采样MISO(在下降沿后立即读取) */
recv <<= 1;
if (ADS1220_MISO_Read())
recv |= 0x01;
ADS1220_Delay_us(1);
}
return recv;
}
硬件SPI配置(STM32标准库):
staticvoidADS1220_SPI_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(ADS1220_SPI_CLK, ENABLE);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // ← CPOL=0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // ← CPHA=1
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure. SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // ~2. 25MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(ADS1220_SPI, &SPI_InitStructure);
SPI_Cmd(ADS1220_SPI, ENABLE);
}
提示:如果你用的是 HAL 库,对应参数是:
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1
2.2 寄存器配置:让 ADS1220 "听懂"你的意图

ADS1220 有 4 个配置寄存器(Reg0-Reg3),每个寄存器控制不同功能:
寄存器功能速查表
|
寄存器 |
主要功能 |
关键位域 |
|---|---|---|
| Reg0 |
输入通道 + PGA |
MUX[3:0]
+ |
| Reg1 |
采样率 + 工作模式 |
DR[2:0]
+ |
| Reg2 |
参考源 + IDAC电流 |
VREF[1:0]
+ |
| Reg3 |
IDAC路由 |
I1MUX[2:0]
+ |
三线制比例测量的标准配置

PT100_Config_t pt100_config =
{
.type = PT100_TYPE, /* PT100传感器 */
.wire_mode = PT100_3WIRE_RATIOMETRIC, /* 3线制比例测量 */
.mux = ADS1220_MUX_AIN1_AIN0, /* 差分输入: AIN0-AIN1 */
. gain = 8, /* PGA增益 ×8 */
.idac = PT100_IDAC_500UA, /* IDAC电流 500µA */
.idac1_pin = ADS1220_I1MUX_AIN2, /* IDAC1 → AIN2 */
. idac2_pin = ADS1220_I2MUX_AIN3, /* IDAC2 → AIN3 */
.vref_sel = ADS1220_VREF_EXT_REF0, /* 外部参考电阻Rref */
.rref_mohm = 1650000U, /* Rref = 1650Ω = 1650000mΩ */
};
配置写入函数(核心):
voidPT100_Init(PT100_Config_t *config)
{
ADS1220_Config_t ads;
uint8_t gain_reg;
/* 1. PGA增益映射:物理倍数 → 寄存器编码 */
switch (config->gain)
{
case1: gain_reg = ADS1220_GAIN_1; break;
case2: gain_reg = ADS1220_GAIN_2; break;
case4: gain_reg = ADS1220_GAIN_4; break;
case8: gain_reg = ADS1220_GAIN_8; break; // ← 我们用这个
case16: gain_reg = ADS1220_GAIN_16; break;
case32: gain_reg = ADS1220_GAIN_32; break;
case64: gain_reg = ADS1220_GAIN_64; break;
case128: gain_reg = ADS1220_GAIN_128; break;
default: gain_reg = ADS1220_GAIN_8; break;
}
/* 2. Reg0: 输入MUX + PGA配置 */
ads.reg0 = config->mux | // 差分通道选择
gain_reg | // PGA增益
ADS1220_PGA_ENABLED; // 启用PGA
/* 3. Reg1: 采样率 + 工作模式 */
ads.reg1 = ADS1220_DR_1000SPS | // 1000 SPS(你可以改成20SPS降噪)
ADS1220_MODE_NORMAL | // 正常功耗模式
ADS1220_CM_SINGLE | // 单次转换模式
ADS1220_TS_DISABLED; // 关闭内部温度传感器
/* 4. Reg2: 参考源 + IDAC电流 */
ads.reg2 = config->vref_sel | // 外部参考(Rref)
ADS1220_FIR_50HZ_60HZ | // 数字滤波(抑制工频干扰)
(config->idac & 0x07); // IDAC电流档位
/* 5. Reg3: IDAC电流路由(三线制的核心!) */
ads.reg3 = config->idac1_pin | // IDAC1 → AIN2
config->idac2_pin | // IDAC2 → AIN3
ADS1220_DRDYM_DRDY_ONLY;// DRDY引脚仅输出数据就绪信号
/* 6. 写入ADS1220寄存器 */
ADS1220_WriteConfig(&ads);
Delay_ms(10); // 等待IDAC稳定
}
2.3 非阻塞式数据读取:告别"死等DRDY"
传统阻塞式代码的问题:
// ❌ 不推荐:主循环卡死在这里
ADS1220_StartSync();
while (! ADS1220_IsDataReady()); // 死等100ms
int32_t raw = ADS1220_ReadData();
改进方案:状态机轮询
/**
* @brief 轮询ADC转换状态(非阻塞)
* @param timeout_ms: 超时时间(毫秒)
* @param start_time_ms: 转换开始时的时间戳
* @retval ADS1220_CONV_WAITING/READY/TIMEOUT
*/
ADS1220_ConvState_t ADS1220_PollConversion(uint32_t timeout_ms, uint32_t start_time_ms)
{
/* 检查是否超时(使用无符号减法,自动处理溢出) */
if ((GetMillis() - start_time_ms) >= timeout_ms)
{
return ADS1220_CONV_TIMEOUT;
}
/* 检查DRDY引脚 */
if (ADS1220_IsDataReady())
{
return ADS1220_CONV_READY;
}
return ADS1220_CONV_WAITING; // 仍在等待,主循环可以干别的
}
使用示例(main.c中的实战代码):
// 定义状态机控制结构
typedefstruct
{
uint8_t state; // 0=IDLE, 1=WAIT_DRDY
uint32_t start_time; // 转换开始时间戳
uint32_t last_poll_time; // 上次测量时间
int32_t result_temp; // 最新温度结果
uint8_t new_data_flag; // 新数据标志
} PT100_Async_Ctrl_t;
PT100_Async_Ctrl_t g_pt100_ctrl = {0};
voidPT100_Task_3Wire_Async(PT100_Config_t *config, uint32_t interval_ms)
{
uint32_t now = GetMillis();
switch (g_pt100_ctrl. state)
{
case STATE_IDLE:
// 检查是否到达采样时间
if (now - g_pt100_ctrl. last_poll_time >= interval_ms)
{
PT100_StartMeasurement(); // 发送START命令
g_pt100_ctrl.start_time = now;
g_pt100_ctrl.state = STATE_WAIT_DRDY;
}
break;
case STATE_WAIT_DRDY:
{
int32_t resistance;
PT100_ConvState_t status = PT100_PollResistance(
config, 100, g_pt100_ctrl.start_time, &resistance);
if (status == PT100_CONV_READY)
{
// 数据就绪,计算温度
g_pt100_ctrl.result_temp =
PT100_ResistanceToTemperature_Int(resistance, config->type);
g_pt100_ctrl.new_data_flag = 1;
g_pt100_ctrl.last_poll_time = now;
g_pt100_ctrl. state = STATE_IDLE;
}
elseif (status == PT100_CONV_TIMEOUT)
{
// 超时处理
g_pt100_ctrl.last_poll_time = now;
g_pt100_ctrl. state = STATE_IDLE;
}
// WAITING状态下直接return,CPU可以执行其他任务
}
break;
}
}
三、第二战:从 ADC 码值到电阻值 —— 比例测量的数学实现
3.1 三线制比例测量公式推导
根据上篇文章的原理,我们得到了终极公式:
公式解释:
- 2倍系数:因为流过Rref的电流是PT100的2倍(IDAC1+IDAC2汇合)
- 223:ADS1220是24位ADC,满量程=8388608(223,因为是有符号数)
- Gain:PGA增益,我们配置的是8
3.2 代码实现(防溢出版本)
/**
* @brief 从ADC原始值计算电阻(3线制硬件比例测量法)
* @param config: PT100配置参数指针
* @param raw: ADC原始值(24位有符号,已扩展到int32_t)
* @retval 电阻值(mΩ),失败返回-1
*/
staticint32_tPT100_CalcResistance_3Wire_Ratiometric(PT100_Config_t *config, int32_t raw)
{
/* 检查参考电阻值是否有效 */
if (config->rref_mohm == 0)
return-1;
/* 使用64位中间变量防止溢出
* 公式: Rpt100 = (ADC_code * 2 * Rref) / (2^23 * Gain)
*
* 数值范围分析:
* - raw: ±8388608
* - rref_mohm: 1650000 (1650Ω)
* - 中间结果: 8388608 × 2 × 1650000 ≈ 2.77×10^13
* - 需要64位整数存储
*/
int64_t numerator = (int64_t)raw * (int64_t)config->rref_mohm;
/* 3线制下,流过Rref的电流是PT100的2倍 */
if (config->wire_mode == PT100_3WIRE_RATIOMETRIC)
{
numerator *= 2;
}
int64_t denominator = 8388608LL * (int64_t)config->gain;
return (int32_t)(numerator / denominator);
}
实测数据验证:
假设:
-
ADC读数 = 800000
-
Rref = 1650Ω = 1650000mΩ
-
Gain = 8
计算:
R = (800000 × 2 × 1650000) / (8388608 × 8)
= 2640000000000 / 67108864
= 39337mΩ
≈ 39.337Ω
对应温度约 -153°C(查表可得)
四、第三战:从电阻到温度 —— 查表+插值算法
4.1 为什么不用 Callendar-Van Dusen 公式直接算?
理论公式:
问题:
-
需要浮点运算(STM32F103 无 FPU)
-
三次方程求解复杂
-
计算耗时长
解决方案:预计算表 + 线性插值
4.2 查找表设计
表格参数:
|
参数 |
值 |
|---|---|
|
温度范围 |
-200°C ~ +850°C |
|
步进 |
10°C |
|
数据点数 |
106个 |
|
单位 |
mΩ(毫欧姆) |
表格片段(PT100.c):
staticconstint32_t PT100_R_TABLE[] = {
/* -200°C to -110°C */
18520, /* -200°C: 18.520Ω */
22830, /* -190°C: 22.830Ω */
27100, /* -180°C: 27.100Ω */
...
/* 0°C to 90°C */
100000, /* 0°C: 100.000Ω */
103903, /* 10°C: 103.903Ω */
107794, /* 20°C: 107.794Ω */
111673, /* 30°C: 111.673Ω */
...
/* 800°C to 850°C */
375426, /* 800°C: 375.426Ω */
378390, /* 810°C: 378.390Ω */
...
390126/* 850°C: 390.126Ω */
};
4.3 二分查找 + 线性插值实现
/**
* @brief 电阻值转换为温度值
* @param resistance_mohm: 电阻值(mΩ)
* @param type: PT100类型
* @retval 温度值(0.01°C) ← 注意单位!2350表示23. 50°C
*/
int32_tPT100_ResistanceToTemperature_Int(int32_t resistance_mohm, PT100_Type_t type)
{
int32_t r_mohm = resistance_mohm;
/* 对于PT1000,电阻缩小10倍后使用PT100表 */
if (type == PT1000_TYPE)
{
r_mohm = resistance_mohm / 10;
}
/* 1. 边界检查与线性外推 */
if (r_mohm <= PT100_R_TABLE[0])
{
/* 低于-200°C,使用线性外推 */
int32_t delta_r = r_mohm - PT100_R_TABLE[0];
return PT100_TABLE_T_MIN + (delta_r * 100) / PT100_DRDT_AT_MINUS200;
}
if (r_mohm >= PT100_R_TABLE[PT100_TABLE_SIZE - 1])
{
/* 高于+850°C,使用线性外推 */
int32_t delta_r = r_mohm - PT100_R_TABLE[PT100_TABLE_SIZE - 1];
return PT100_TABLE_T_MAX + (delta_r * 100) / PT100_DRDT_AT_PLUS850;
}
/* 2. 二分查找找到电阻所在区间 */
int32_t low = 0;
int32_t high = PT100_TABLE_SIZE - 1;
while (low < high - 1)
{
int32_t mid = (low + high) / 2;
if (r_mohm < PT100_R_TABLE[mid])
high = mid;
else
low = mid;
}
/* 3. 线性插值计算温度 */
/*
* 公式: T = T_low + (R - R_low) * (T_high - T_low) / (R_high - R_low)
*
* 示例: R = 105000mΩ (105Ω),落在 [103903, 107794] 区间
* 对应温度 [1000, 2000] (10°C ~ 20°C,单位0.01°C)
*/
int32_t r_low = PT100_R_TABLE[low];
int32_t r_high = PT100_R_TABLE[high];
int32_t t_low = PT100_TABLE_T_MIN + low * PT100_TABLE_T_STEP;
/* 使用64位防止溢出 */
int64_t delta_r = (int64_t)(r_mohm - r_low);
int64_t range_r = (int64_t)(r_high - r_low);
int32_tdelta_t = (int32_t)((delta_r * PT100_TABLE_T_STEP) / range_r);
return t_low + delta_t;
}
计算示例:
假设电阻 R = 105000mΩ(105Ω)
-
查找区间:
103903 < 105000 < 107794
→ low=1 (10°C), high=2 (20°C)
-
插值计算:
delta_r = 105000 - 103903 = 1097 range_r = 107794 - 103903 = 3891 delta_t = (1097 × 1000) / 3891 ≈ 282 T = 1000 + 282 = 1282 (0.01°C) = 12.82°C
五、第四战:数字滤波 —— 让温度数据"丝滑稳定"
即使硬件再精密,实际测量中仍会遇到:
- 随机噪声:ADC本身的噪声
- 工频干扰:50Hz/60Hz(虽然ADS1220有硬件FIR滤波,但不是100%)
- 瞬态突变:电磁干扰、接触不良
5.1 三种滤波器对比
|
滤波器类型 |
优点 |
缺点 |
适用场景 |
|---|---|---|---|
| 简单平均 |
实现简单,抗随机噪声 |
响应慢,无法去除尖峰 |
温度变化缓慢场景 |
| IIR低通 |
响应速度可调,计算量小 |
对突变响应慢 |
实时性要求高的场景 |
| 去极值平均 |
抗尖峰干扰强 |
需要缓存数组,延时大 |
强干扰环境 |
5.2 简单平均滤波器(整数版本)
/**
* @brief 简单平均滤波器结构体
*/
typedefstruct
{
int64_t sum; /* 累加和(使用64位防止溢出) */
uint16_t count; /* 当前样本计数 */
uint16_t N; /* 平均窗口大小 */
#ifdef USE_SHIFT_DIV
uint8_t shift; /* 如果N是2的幂,可以用移位代替除法 */
#endif
} AvgFilter_Int_t;
/**
* @brief 初始化平均滤波器
* @param f: 滤波器结构体指针
* @param N: 平均窗口大小(建议2的幂,如4/8/16)
*/
voidAvgFilter_Int_Init(AvgFilter_Int_t *f, uint16_t N)
{
#ifdef USE_SHIFT_DIV
/* 优化模式:检查N是否为2的幂 */
uint8_t shift = calc_log2(N);
if (shift == 0)
{
N = 16; // 不是2的幂,默认16
shift = 4;
}
f->shift = shift;
#else
if (N == 0) N = 1; // 防止除零
#endif
f->sum = 0;
f->count = 0;
f->N = N;
}
/**
* @brief 输入样本,累积N个后输出平均值
* @param f: 滤波器结构体指针
* @param x: 输入样本值(int32_t,单位0.01°C)
* @param out: 输出均值指针(仅当返回1时有效)
* @retval 0=正在累积, 1=输出有效
*/
uint8_tAvgFilter_Int_Put(AvgFilter_Int_t *f, int32_t x, int32_t *out)
{
f->sum += x;
if (++f->count < f->N)
return0; // 样本未满
#ifdef USE_SHIFT_DIV
*out = (int32_t)(f->sum >> f->shift); // sum / 2^shift
#else
*out = (int32_t)(f->sum / f->N); // 普通除法
#endif
/* 重置状态 */
f->sum = 0;
f->count = 0;
return1;
}
使用示例:
AvgFilter_Int_t g_pt100_avg;
voidsetup()
{
AvgFilter_Int_Init(&g_pt100_avg, 8); // 8点平均
}
voidloop()
{
int32_t temp = PT100_ReadTemperature_Int(&pt100_config);
int32_t filtered;
if (AvgFilter_Int_Put(&g_pt100_avg, temp, &filtered))
{
// 每8个样本输出一次
printf("Temp: %.2f°C\\n", filtered / 100.0f);
}
}
5.3 去极值平均滤波器(抗尖峰干扰)
原理:采集N个样本 → 排序 → 去掉最大/最小各M个 → 剩余数据平均
/**
* @brief 去极值滤波器结构体
*/
typedefstruct
{
int32_t *pBuffer; /* 外部缓存数组指针 */
uint16_t N; /* 采样窗口大小 */
uint16_t count; /* 当前样本计数 */
uint16_t remove; /* 单侧去除的极值个数 */
#ifdef USE_SHIFT_DIV
uint8_t shift; /* 移位除法优化 */
#endif
} TrimFilter_Int_t;
/**
* @brief 初始化去极值滤波器
* @param f: 滤波器结构体指针
* @param pBuffer: 外部定义的缓存数组
* @param N: 采样窗口大小
* @param remove: 单侧去除的极值个数
*/
voidTrimFilter_Int_Init(TrimFilter_Int_t *f, int32_t *pBuffer, uint16_t N, uint16_t remove)
{
if (N == 0) N = 1;
if (remove * 2 >= N)
{
remove = (N - 1) / 2; // 至少保留1个数据
}
f->pBuffer = pBuffer;
f->N = N;
f->count = 0;
#ifdef USE_SHIFT_DIV
/* 优化逻辑:有效数据量必须是2的幂 */
uint16_t valid_count = N - (remove * 2);
while ((! is_power_of_2(valid_count)) && (valid_count > 1))
{
remove++;
valid_count -= 2;
}
if (valid_count == 0)
{
remove = 0;
valid_count = N;
}
f->remove = remove;
f->shift = calc_log2(valid_count);
#else
f->remove = remove;
#endif
}
/**
* @brief 输入样本,积攒N个后排序并去极值平均
* @retval 0=正在积攒, 1=计算完成
*/
uint8_tTrimFilter_Int_Put(TrimFilter_Int_t *f, int32_t x, int32_t *out)
{
/* 1. 存入缓冲区 */
if (f->count < f->N)
{
f->pBuffer[f->count] = x;
f->count++;
}
if (f->count < f->N)
return0; // 未满
/* 2. 冒泡排序 */
_int_sort(f->pBuffer, f->N);
/* 3. 求中间部分和 */
int64_t sum = 0;
for (uint16_t i = f->remove; i < f->N - f->remove; i++)
{
sum += f->pBuffer[i];
}
/* 4. 计算平均值 */
#ifdef USE_SHIFT_DIV
*out = (int32_t)(sum >> f->shift);
#else
uint16_t valid_count = f->N - (f->remove * 2);
if (valid_count == 0) valid_count = 1;
*out = (int32_t)(sum / valid_count);
#endif
/* 5. 重置计数 */
f->count = 0;
return1;
}
实战效果对比:
假设采集到10个温度值(单位0.01°C):
原始: 2350, 2348, 2352, 2890, 2349, 2351, 2348, 2350, 2349, 2310
↑ 正常温度范围 ↑ 干扰尖峰 ↑ 接触不良
|
方法 |
结果 |
说明 |
|---|---|---|
| 简单平均 |
2410 (24.10°C) |
被尖峰拉偏 |
| 去极值平均(去2个) |
2349 (23.49°C) |
去掉2890和2310后平均 |
5.4 IIR一阶低通滤波器(实时性更好)
优点:不需要缓存,实时输出,响应速度可调
/**
* @brief IIR一阶低通滤波器结构体
*/
typedefstruct
{
int16_t alpha; /* 平滑系数(Q15格式:0-32767对应0.0-1.0) */
int32_t y_prev; /* 上次输出值 */
uint8_t first_run; /* 首次运行标志 */
} IIR1_LPF_Int_t;
/**
* @brief 使用截止频率初始化
* @param f: 滤波器结构体指针
* @param fs: 采样频率(Hz)
* @param fc: 截止频率(Hz)
*/
voidIIR1_LPF_Int_Init_Fc(IIR1_LPF_Int_t *f, uint16_t fs, uint16_t fc)
{
/* 计算 alpha = (2*π*fc/fs) / (1 + 2*π*fc/fs) */
float dt = 1.0f / fs;
float rc = 1.0f / (2.0f * 3.14159265f * fc);
float alpha_f = dt / (rc + dt);
/* 转换为Q15定点数 */
int16_t alpha_q15 = (int16_t)(alpha_f * 32768.0f);
f->alpha = alpha_q15;
f->first_run = 1;
}
/**
* @brief 输入样本,返回滤波后的值
* @param f: 滤波器结构体指针
* @param x: 输入样本值(int32_t)
* @retval 滤波后的输出值
* @note 公式: y = y_prev + alpha * (x - y_prev)
*/
int32_tIIR1_LPF_Int_Put(IIR1_LPF_Int_t *f, int32_t x)
{
if (f->first_run)
{
f->first_run = 0;
f->y_prev = x;
return x;
}
/* EWMA公式(定点实现) */
int32_t diff = x - f->y_prev;
int64_t delta = ((int64_t)f->alpha * (int64_t)diff) >> 15; // Q15右移
int32_t y = f->y_prev + (int32_t)delta;
f->y_prev = y;
return y;
}
5.5 三种滤波效果对比
温度值:2262左右,22.62°
绿色:原始数据
黄色:IIR一阶低通滤波器(平滑系数0.5)
红色:简单平均滤波器(采样4个平均一次)
紫色:去极值平均滤波器(采样4个平均一次)

六、完整main. c实战代码解析
#include"stm32f10x.h"
#include"board.h"
#include"bsp_uart.h"
#include"stdio.h"
#include"vofa_uart.h"
#include"Delay.h"
#include"ADS1220.h"
#include"PT100.h"
#include"filter_int.h"
// 定义非阻塞测量的状态机结构体
typedefstruct
{
uint8_t state; // 当前状态: 0=IDLE, 1=WAITing
uint32_t start_time; // 记录开始转换的时间戳
uint32_t last_poll_time; // 上次完成测量的时间(用于控制采样频率)
int32_t result_temp; // 存储最新的温度结果
int32_t result_res; // 存储最新的电阻结果
uint8_t new_data_flag; // 新数据标志位
} PT100_Async_Ctrl_t;
// 状态枚举
enum
{
STATE_IDLE = 0,
STATE_WAIT_DRDY
};
PT100_Async_Ctrl_t g_pt100_ctrl = {0}; // 实例化控制块
typedefstruct
{
int32_t resistance_mohm; /* 电阻,单位 mΩ */
int32_t temperature_c01; /* 温度,单位 0.01°C */
} PT100_Value_t;
float vofa_tx[20];
#define PT100_CH_NUM 12
#define PT100_AVG_N 4 /* 平均窗口大小 */
#define PT100_FILT_N 4 /* 去极值平均窗口大小 */
/* 12 路整数平均滤波器实例 */
AvgFilter_Int_t g_pt100_avg[PT100_CH_NUM];
/* 滤波后的温度输出(单位:0.01°C) */
int32_t g_pt100_temp_filt[PT100_CH_NUM];
int32_t g_pt100_temp_inst_filt[PT100_CH_NUM];
int32_t g_pt100_temp_TrimFilter[PT100_FILT_N];
TrimFilter_Int_t g_pt100_temp_TrimFilter_inst[PT100_FILT_N];
IIR1_LPF_Int_t g_pt100_temp_IIR1_LPF;
PT100_Value_t pt100_val;
PT100_Config_t pt100_config =
{
.type = PT100_TYPE, /* PT100 */
.wire_mode = PT100_3WIRE_RATIOMETRIC, /* 3线制硬件比例测量 */
.mux = ADS1220_MUX_AIN1_AIN0, /* AIN0-AIN1 差分输入 */
.gain = 8, /* PGA 增益 ×8 */
.idac = PT100_IDAC_500UA, /* IDAC 激励电流 500µA */
.idac1_pin = ADS1220_I1MUX_AIN2, /* IDAC1 → AIN2 */
.idac2_pin = ADS1220_I2MUX_AIN3, /* IDAC2 → AIN3 */
.vref_sel = ADS1220_VREF_EXT_REF0, /* 外部参考(Rref) */
.vref_mv = 0, /* 比例测量下不使用 */
.rref_mohm = 1650000U, /* Rref = 1650Ω(mΩ) */
};
/**
* @brief PA0 输出翻转(直接操作 ODR)
* @note 速度快,适合 LED 翻转、调试
*/
staticinlinevoidPA0_Toggle_Fast(void)
{
GPIOA->ODR ^= GPIO_Pin_0;
}
/**
* @brief 设置 PA0 输出电平(快速,原子操作)
* @param level: 1 - 高电平,0 - 低电平
*/
staticinlinevoidPA0_SetLevel_Fast(uint8_t level)
{
if (level)
{
GPIOA->BSRR = GPIO_Pin_0; /* 置位 */
}
else
{
GPIOA->BRR = GPIO_Pin_0; /* 复位 */
}
}
/**
* @brief PT100 非阻塞任务 (3线制比例测量模式)
* @param config: PT100配置指针
* @param interval_ms: 测量间隔 (例如 1000ms 测一次)
*/
voidPT100_Task_3Wire_Async(PT100_Config_t *config, uint32_t interval_ms)
{
PT100_ConvState_t status;
uint32_t now = GetMillis();
switch (g_pt100_ctrl.state)
{
case STATE_IDLE:
// 检查是否到达下一次测量时间
if (now - g_pt100_ctrl.last_poll_time >= interval_ms)
{
// 1. 发起测量 (只发送命令,不等待)
PT100_StartMeasurement();
// 2. 记录开始时间和状态切换
g_pt100_ctrl.start_time = now;
g_pt100_ctrl.state = STATE_WAIT_DRDY;
}
break;
case STATE_WAIT_DRDY:
// 3. 轮询状态 (利用你库中已有的非阻塞接口)
// 注意:这里传入 100ms 是超时阈值,不是等待时间
status = PT100_PollResistance(config, 100,
g_pt100_ctrl.start_time,
&g_pt100_ctrl.result_res);
if (status == PT100_CONV_READY)
{
// --- 数据已就绪 ---
// 4. 计算温度 (纯计算,耗时极短,视为非阻塞)
g_pt100_ctrl.result_temp = PT100_ResistanceToTemperature_Int(
g_pt100_ctrl.result_res,
config->type);
// 5. 更新标志位和时间
g_pt100_ctrl.new_data_flag = 1;
g_pt100_ctrl.last_poll_time = g_pt100_ctrl.start_time;
g_pt100_ctrl.state = STATE_IDLE; // 回到空闲状态
}
elseif (status == PT100_CONV_TIMEOUT || status == PT100_CONV_ERROR)
{
// 处理超时错误
g_pt100_ctrl.last_poll_time = now;
g_pt100_ctrl.state = STATE_IDLE; // 重置状态
}
// 如果状态是 PT100_CONV_WAITING,直接 return,把 CPU 让给主循环其他任务
break;
}
}
/**
* @brief PA0 GPIO 初始化为推挽输出
* @param 无
* @retval 无
*/
voidPA0_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 1. 使能 GPIOA 时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* 2. 配置 PA0 为推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; /* 推挽输出 */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; /* 输出速度 */
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 3. 默认输出低电平 */
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
}
/**
* @brief PT100 多通道平均滤波初始化
*/
voidPT100_Filter_Init(void)
{
for (uint8_t ch = 0; ch < PT100_CH_NUM; ch++)
{
AvgFilter_Int_Init(&g_pt100_avg[ch], PT100_AVG_N);
g_pt100_temp_filt[ch] = 0;
}
}
voidPT100_MeasureExample_3Wire_Ratiometric(void)
{
int32_t temperature, resistance;
int32_t avg_out, inst_out;
resistance = PT100_ReadResistance_Int(&pt100_config);
if (resistance > 0)
{
/* 原始计算 */
pt100_val.resistance_mohm = resistance;
pt100_val.temperature_c01 =
PT100_ResistanceToTemperature_Int(resistance, PT100_TYPE);
}
else
{
pt100_val.resistance_mohm = 0;
pt100_val.temperature_c01 = 0;
}
}
intmain(void)
{
uart1_init(1152000);
ADS1220_Init();
PA0_GPIO_Init();
PT100_Init(&pt100_config);
TrimFilter_Int_Init(&g_pt100_temp_TrimFilter_inst[0], g_pt100_temp_TrimFilter, PT100_FILT_N, 2);
AvgFilter_Int_Init(&g_pt100_avg[0], PT100_AVG_N);
IIR1_LPF_Int_Init(&g_pt100_temp_IIR1_LPF, ALPHA_Q15_0_5);
while (1)
{
PA0_SetLevel_Fast(1);
// PT100_MeasureExample_3Wire_Ratiometric();
PT100_Task_3Wire_Async(&pt100_config, 1);
if (g_pt100_ctrl.new_data_flag)
{
int32_t avg_out, inst_out;
g_pt100_ctrl.new_data_flag = 0; // 清除标志
// 打印结果
int32_t t = g_pt100_ctrl.result_temp;
/* 输入原始温度(0.01°C),满 N 点后输出 */
if (AvgFilter_Int_Put(&g_pt100_avg[0], t, &avg_out))
{
g_pt100_temp_filt[0] = avg_out; /* 更新滤波输出 */
}
if (TrimFilter_Int_Put(&g_pt100_temp_TrimFilter_inst[0], t, &inst_out))
{
g_pt100_temp_inst_filt[0] = inst_out; /* 更新滤波输出 */
}
IIR1_LPF_Int_Put(&g_pt100_temp_IIR1_LPF, t);
}
PA0_SetLevel_Fast(0);
}
}
七、使用万用表进行对比测试(验证趋势是否正确)
为了验证 PT100 测温链路是否工作正常,这里采用 万用表对水温进行简单对比测试,主要目的是观察温度变化趋势是否一致,而非追求严格的绝对精度。


由于本次测试 未进行系统级标定(如参考电阻精度校正、零点/斜率修正等),因此测量结果仅作为功能验证参考。

测试过程中可以明显看到:
-
随着水温从高慢慢降低,PT100 计算得到的温度值同步下降
-
温度变化方向与万用表读数保持一致
-
说明 硬件连接 + ADC 采样 + 软件计算逻辑是正确的
这一步的意义在于:
先验证“算得对不对”,再谈“算得准不准”
只要趋势正确,后续通过标定和参数优化,即可进一步提升测量精度。
八、总结与下篇预告
本篇要点回顾
✅ SPI驱动:严格遵守Mode 1时序,软硬件SPI双实现
✅ 寄存器配置:四个寄存器的每一位都有讲究
✅ 非阻塞设计:状态机轮询,不卡主循环
✅ 比例测量算法:64位运算防溢出,公式严格推导
✅ 查表+插值:纯整数运算,精度±0.1°C
✅ 数字滤波:三种滤波器适应不同场景
关键代码文件
|
文件 |
功能 |
核心函数 |
|---|---|---|
ADS1220.c/h |
ADC驱动 |
ADS1220_PollConversion() |
PT100.c/h |
温度测量 |
PT100_ResistanceToTemperature_Int() |
filter_int.c/h |
数字滤波 |
AvgFilter_Int_Put() |
main.c |
主控逻辑 |
PT100_Task_3Wire_Async() |
欢迎关注:公众号:「单片机那点事」
更多推荐



所有评论(0)