欢迎关注:公众号:「单片机那点事」

回顾上篇:我们已经搞懂了 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]

 + GAIN[2:0] + PGA_EN

Reg1

采样率 + 工作模式

DR[2:0]

 + MODE[1:0] + CM

Reg2

参考源 + IDAC电流

VREF[1:0]

 + IDAC[2:0]

Reg3

IDAC路由

I1MUX[2:0]

 + I2MUX[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Ω)

  1. 查找区间

    103903 < 105000 < 107794

    → low=1 (10°C), high=2 (20°C)

  2. 插值计算

    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()

源码:https://ahhh111.notion.site/ADS1220-24-ADC-PT100-RTD-0-1-STM32F103-2f5a84b2a47b801aa46cd44c280d5289?source=copy_link

欢迎关注:公众号:「单片机那点事」

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐