1. 项目概述

FiltroIIR 是一个面向嵌入式实时系统的定点数 IIR(无限冲激响应)低通滤波器轻量级实现库,核心设计目标是在资源受限的 MCU(如 Cortex-M0/M3/M4)上以极低开销完成高精度、高稳定性的信号滤波。该库不依赖任何标准数学库(如 math.h )、不使用浮点运算,全部基于 IQ12 定点数格式 实现——即 1 位符号位 + 3 位整数位 + 12 位小数位(Q3.12),数值范围为 ([-8, 8)),分辨率达 (2^{-12} \approx 0.000244)。这一选择直接规避了 ARM Cortex-M 系列中无硬件 FPU 的低端芯片(如 STM32F0、GD32F103C8T6)的浮点性能瓶颈,同时避免了通用定点库(如 CMSIS-DSP 中的 q31_t )在系数缩放、溢出防护和相位延迟控制上的工程妥协。

与通用数字信号处理库不同, FiltroIIR 并非提供全阶数、多拓扑(Butterworth/Chebyshev/Elliptic)的参数化生成器,而是聚焦于 二阶直接Ⅱ型(Direct Form II Transposed)结构 的硬编码实现。该结构具有最优的数值稳定性(尤其对高 Q 值低频滤波)、最少的状态变量(仅需 2 个状态寄存器)、最简的计算路径(每采样点仅需 3 次乘加 MAC 运算),是工业传感器信号调理(如压力、温度、振动)中最成熟、最可靠的部署方案。其本质是一个“可配置的滤波器内核”,而非“滤波器生成工具”——用户需预先通过 MATLAB/Python 计算出符合目标截止频率 (f_c) 和采样率 (f_s) 的归一化二阶系数,并将其转换为 IQ12 格式后传入初始化函数。

该库的典型应用场景包括:

  • 模拟传感器(如 PT100、热电偶、MEMS 加速度计)的 ADC 采样值去噪;
  • PWM 输出信号的数字平滑(如电机电流环中的参考信号整形);
  • 串口或 CAN 总线接收数据的速率限制与突变抑制;
  • 低功耗唤醒检测电路中对微弱周期信号的信噪比增强。

其价值不在于算法新颖性,而在于将经典理论转化为 零堆栈、零动态内存、确定性执行时间、抗饱和鲁棒 的生产就绪代码模块,可无缝集成至裸机系统或 RTOS 任务中,且满足 IEC 61508 SIL-2 等功能安全基础要求。

2. 核心原理与结构设计

2.1 IIR 滤波器的数学基础

IIR 滤波器的离散时间域差分方程为:

[ y[n] = b_0 x[n] + b_1 x[n-1] + b_2 x[n-2] - a_1 y[n-1] - a_2 y[n-2] ]

其中 (x[n]) 为输入序列,(y[n]) 为输出序列,({b_0, b_1, b_2}) 为前向路径系数,({a_1, a_2}) 为反馈路径系数。对于低通滤波器,所有系数均为实数,且通常满足 (a_0 = 1)(已隐含在方程中)。

FiltroIIR 采用 Direct Form II Transposed 结构实现,其信号流图如下(状态变量记为 (w[n])):

x[n] → [+] → w[n] → [z⁻¹] → w[n-1] → [z⁻¹] → w[n-2]
         ↑      ↓          ↓          ↓
        b0    -a1        -a2        (无)
         |      |          |          |
         ↓      ↓          ↓          ↓
       y[n] ← [+] ← b1x[n-1] ← b2x[n-2]

对应的状态更新方程为:

[ \begin{aligned} w[n] &= x[n] + (-a_1) \cdot w[n-1] + (-a_2) \cdot w[n-2] \ y[n] &= b_0 \cdot w[n] + b_1 \cdot w[n-1] + b_2 \cdot w[n-2] \end{aligned} ]

该结构的优势在于:所有乘法均作用于状态变量(而非输入/输出本身),极大降低了中间结果的数值范围;反馈路径系数 (-a_1, -a_2) 直接参与状态更新,使极点位置对系数量化误差更不敏感;且仅需两个状态寄存器 (w[n-1], w[n-2]),内存占用最小。

2.2 IQ12 定点数格式详解

IQ12 是一种固定小数点的有符号定点数格式,其二进制布局为:

Bit 15 Bits 14–12 Bits 11–0
Sign Integer Fraction
  • 数值表示 :(v = (-1)^s \times (i + f \times 2^{-12})),其中 (s) 为符号位,(i) 为 3 位整数部分(0–7),(f) 为 12 位小数部分(0–4095)。
  • 取值范围 :([-8.0, +7.999755859375)),即 ([-2^3, 2^3 - 2^{-12}))。
  • 量化步长(LSB) :(2^{-12} = \frac{1}{4096} \approx 0.000244140625)。
  • 溢出行为 :当运算结果超出范围时,发生 模 wrap-around (即二进制补码自然溢出),而非饱和(saturation)。 FiltroIIR 明确采用此行为,因其在 IIR 反馈环中比饱和更不易引发极限环振荡(limit cycle oscillation),且符合 ARM Cortex-M 的默认 ALU 行为。

所有滤波器系数((b_0, b_1, b_2, a_1, a_2))及状态变量 (w[n], w[n-1], w[n-2]) 均以 int16_t 存储,按 IQ12 解释。例如,系数 (b_0 = 0.5) 编码为: [ \text{IQ12}(0.5) = \text{round}(0.5 \times 4096) = 2048 = 0x0800 ] 而 (a_1 = -1.2) 编码为: [ \text{IQ12}(-1.2) = \text{round}(-1.2 \times 4096) = -4915 = 0xECC5 \quad (\text{补码}) ]

2.3 系数预计算与稳定性保障

IIR 滤波器的稳定性完全由其极点位置决定:所有极点必须严格位于 Z 平面单位圆内。对于二阶系统,等价于以下三个条件必须同时成立:

[ \begin{cases} 1 - a_1 + a_2 > 0 \ 1 + a_1 + a_2 > 0 \ 1 - a_2 > 0 \end{cases} ]

FiltroIIR 不负责验证 这些条件,而是将责任完全交给用户。这是工程实践中的关键设计决策:在嵌入式系统中,滤波器参数通常是离线设计、一次性烧录的常量,其稳定性已在 MATLAB/Simulink 或 Python(scipy.signal.iirfilter)中经过严格验证。运行时检查会引入不可预测的分支和额外开销,违背了“确定性执行时间”的核心目标。

因此,库的 API 文档明确要求:用户必须确保传入的 IQ12 系数满足上述稳定性判据。一个典型的稳健设计流程是:

  1. 在 MATLAB 中调用 designfilt('lowpassiir', 'FilterOrder', 2, 'HalfPowerFrequency', fc, 'SampleRate', fs) 生成滤波器对象;
  2. 提取其 sos (second-order sections)矩阵,取第一行 [b0,b1,b2,1,a1,a2]
  3. 将系数乘以 (2^{12}) 并四舍五入为 int16_t
  4. 手动验证 (1-a_1+a_2>0) 等不等式(MATLAB 中 1 - d.Coefficients(1,5) + d.Coefficients(1,6) > 0 );
  5. 将验证后的 int16_t 数组作为常量传入初始化函数。

此流程将设计阶段的数学严谨性与运行时的极致效率完美结合。

3. API 接口规范与使用详解

FiltroIIR 提供极简的 C 语言 API,全部定义在单头文件 filtro_iir.h 中,无外部依赖。其核心数据结构与函数如下:

3.1 核心数据结构

typedef struct {
    int16_t b0;     // IQ12, 前向系数 b0
    int16_t b1;     // IQ12, 前向系数 b1
    int16_t b2;     // IQ12, 前向系数 b2
    int16_t a1;     // IQ12, 反馈系数 a1 (注意:方程中为 -a1)
    int16_t a2;     // IQ12, 反馈系数 a2 (注意:方程中为 -a2)
    int16_t w1;     // IQ12, 状态变量 w[n-1]
    int16_t w2;     // IQ12, 状态变量 w[n-2]
} filtro_iir_t;
  • w1 w2 有状态的 ,在每次 filtro_iir_update() 调用后被更新,代表滤波器的内部记忆。
  • 所有系数字段均为 int16_t ,但语义为 IQ12 定点数,用户必须按此规则赋值。

3.2 主要 API 函数

函数名 原型 功能说明
filtro_iir_init void filtro_iir_init(filtro_iir_t *f, int16_t b0, int16_t b1, int16_t b2, int16_t a1, int16_t a2); 初始化滤波器实例。将系数写入结构体,并将状态 w1 , w2 清零。 必须在首次调用 update 前执行。
filtro_iir_update int16_t filtro_iir_update(filtro_iir_t *f, int16_t x); 执行一次滤波运算。输入 x 为 IQ12 格式的当前采样值(如 ADC 原始值经缩放后),返回 IQ12 格式的滤波后值 y[n] 这是唯一的核心计算函数。
filtro_iir_reset void filtro_iir_reset(filtro_iir_t *f); 将状态 w1 w2 重置为 0,相当于“清空滤波器记忆”。适用于信号源切换或故障恢复场景。
3.2.1 filtro_iir_init 详解

该函数仅执行赋值与清零操作,无任何计算开销:

void filtro_iir_init(filtro_iir_t *f, int16_t b0, int16_t b1, int16_t b2, int16_t a1, int16_t a2) {
    f->b0 = b0;
    f->b1 = b1;
    f->b2 = b2;
    f->a1 = a1;
    f->a2 = a2;
    f->w1 = 0;
    f->w2 = 0;
}

工程要点

  • 系数 a1 , a2 是直接从设计工具导出的原始值(如 MATLAB 的 d.Coefficients(1,5) ), 无需取负 。库内部在计算时会显式使用 -f->a1
  • 初始化后,滤波器处于“冷启动”状态,前几个输出会因初始零状态而存在启动瞬态(start-up transient),这属于 IIR 滤波器固有特性,可通过 filtro_iir_reset() 后喂入若干个稳态值来预热。
3.2.2 filtro_iir_update 源码解析

这是整个库的精华所在,其实现完全展开,无任何隐藏调用:

int16_t filtro_iir_update(filtro_iir_t *f, int16_t x) {
    // 步骤1: 计算新状态 w[n] = x + (-a1)*w1 + (-a2)*w2
    // 使用 32-bit 中间量防止 IQ12 乘法溢出 (16x16 -> 32)
    int32_t w_n = (int32_t)x;
    w_n += (int32_t)(-f->a1) * f->w1;  // (-a1) * w1
    w_n += (int32_t)(-f->a2) * f->w2;  // (-a2) * w2

    // 步骤2: 截断回 IQ12 (保留低16位,利用补码 wrap-around)
    int16_t w_n_iq12 = (int16_t)w_n;

    // 步骤3: 计算输出 y[n] = b0*w_n + b1*w1 + b2*w2
    int32_t y_n = (int32_t)f->b0 * w_n_iq12;
    y_n += (int32_t)f->b1 * f->w1;
    y_n += (int32_t)f->b2 * f->w2;

    // 步骤4: 截断回 IQ12 并更新状态
    int16_t y_n_iq12 = (int16_t)y_n;
    f->w2 = f->w1;
    f->w1 = w_n_iq12;

    return y_n_iq12;
}

关键工程细节

  • 32 位中间累加 :所有 int16_t 乘法均提升至 int32_t 进行,这是 IQ12 运算的黄金法则。因为两个 IQ12 数相乘,结果为 IQ24(小数位 24),其整数部分可能达 6 位,16 位无法容纳。 int32_t 提供了充足的动态范围(±2G),确保累加过程无截断误差。
  • 截断(Truncation)而非舍入(Rounding) (int16_t)w_n 是直接丢弃高 16 位,等效于右移 16 位。这比 round(x/65536) 更快,且在 IIR 反馈环中,截断引入的偏置误差远小于舍入可能带来的非线性失真。
  • 状态更新顺序 :先计算 w_n ,再更新 w2 = w1 ,最后 w1 = w_n_iq12 ,严格遵循 Direct Form II Transposed 的数据流。
  • 执行时间 :在 Cortex-M3/M4 上,该函数典型汇编指令数约为 35–40 条,全部为 ALU 操作,无分支预测失败风险, 最坏情况执行时间(WCET)恒定,约 1.2 μs @ 72 MHz

3.3 典型应用示例

3.3.1 裸机环境下的 ADC 滤波(STM32 HAL)

假设使用 STM32F103,ADC 采样速率为 1 kHz,目标低通截止频率为 50 Hz。经 MATLAB 设计得 IQ12 系数为: b0=1220, b1=2440, b2=1220, a1=-2440, a2=1220 (对应标准二阶巴特沃斯)。

#include "filtro_iir.h"
#include "stm32f1xx_hal.h"

// 全局滤波器实例
static filtro_iir_t adc_filter;

// ADC DMA 回调,在每次 DMA 传输完成时调用
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    uint16_t raw_adc = *(uint16_t*)adc_buffer; // 假设 DMA 缓冲区首地址
    // 将 12-bit ADC 值 (0-4095) 映射到 IQ12 范围 [-4, +4)
    // 即:x_iq12 = (raw - 2048) << 1  (因为 2048*2 = 4096 = 2^12)
    int16_t x_iq12 = (int16_t)((int32_t)raw_adc - 2048) << 1;
    int16_t y_iq12 = filtro_iir_update(&adc_filter, x_iq12);
    // 将滤波后 IQ12 转回 12-bit 整数用于显示或控制
    int16_t filtered_value = (y_iq12 >> 1) + 2048; // 反向映射
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_ADC1_Init();
    MX_DMA_Init();

    // 初始化滤波器:使用预计算的 IQ12 系数
    filtro_iir_init(&adc_filter,
                    1220, 2440, 1220,  // b0, b1, b2
                    -2440, 1220);     // a1, a2

    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 1,
                      HAL_ADC_FORMAT_FIXED, HAL_ADC_UNIT_SAMPLE);

    while (1) {
        // 主循环可进行其他任务
    }
}
3.3.2 FreeRTOS 任务中的多通道滤波

在需要同时处理多个传感器的系统中,可为每个通道创建独立的 filtro_iir_t 实例,并在专用任务中轮询处理:

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

// 为三轴加速度计定义三个滤波器
static filtro_iir_t acc_x_filter, acc_y_filter, acc_z_filter;

// 预计算的 IQ12 系数(相同截止频率)
#define ACC_COEFF_B0  1000
#define ACC_COEFF_B1  2000
#define ACC_COEFF_B2  1000
#define ACC_COEFF_A1 -2000
#define ACC_COEFF_A2  1000

void acc_filter_task(void *pvParameters) {
    // 初始化所有滤波器
    filtro_iir_init(&acc_x_filter, ACC_COEFF_B0, ACC_COEFF_B1, ACC_COEFF_B2,
                    ACC_COEFF_A1, ACC_COEFF_A2);
    filtro_iir_init(&acc_y_filter, ACC_COEFF_B0, ACC_COEFF_B1, ACC_COEFF_B2,
                    ACC_COEFF_A1, ACC_COEFF_A2);
    filtro_iir_init(&acc_z_filter, ACC_COEFF_B0, ACC_COEFF_B1, ACC_COEFF_B2,
                    ACC_COEFF_A1, ACC_COEFF_A2);

    // 假设有一个队列接收原始加速度数据 (int16_t[3])
    QueueHandle_t acc_raw_queue = xQueueCreate(10, sizeof(int16_t[3]));

    while (1) {
        int16_t raw_acc[3];
        if (xQueueReceive(acc_raw_queue, raw_acc, portMAX_DELAY) == pdTRUE) {
            // 对每个轴分别滤波(输入已为 IQ12 格式)
            int16_t filtered_x = filtro_iir_update(&acc_x_filter, raw_acc[0]);
            int16_t filtered_y = filtro_iir_update(&acc_y_filter, raw_acc[1]);
            int16_t filtered_z = filtro_iir_update(&acc_z_filter, raw_acc[2]);

            // 发送滤波后数据到下一处理环节
            int16_t filtered_data[3] = {filtered_x, filtered_y, filtered_z};
            xQueueSend(filtered_queue, filtered_data, 0);
        }
    }
}

4. 系数设计与工程实践指南

4.1 从连续域到离散域的系数映射

IIR 滤波器设计始于模拟原型(如 Butterworth 低通),其传递函数为: [ H_a(s) = \frac{\omega_c^2}{s^2 + \sqrt{2}\omega_c s + \omega_c^2} ] 其中 (\omega_c = 2\pi f_c)。通过双线性变换(Bilinear Transform)映射到离散域: [ s = \frac{2}{T} \frac{1 - z^{-1}}{1 + z^{-1}}, \quad T = \frac{1}{f_s} ] 代入并整理后,即可得到标准的二阶差分方程系数。 FiltroIIR 的系数正是此过程的最终产物。

MATLAB 快速生成脚本

fs = 1000;    % 采样率 1 kHz
fc = 50;      % 截止频率 50 Hz
[b, a] = butter(2, fc/(fs/2)); % 设计二阶巴特沃斯
% 将系数转换为 IQ12
b_iq12 = round(b * 4096);
a_iq12 = round(a * 4096);
fprintf('b0=%d, b1=%d, b2=%d, a1=%d, a2=%d\n', b_iq12(1), b_iq12(2), b_iq12(3), a_iq12(2), a_iq12(3));
% 验证稳定性
if (1 - a_iq12(2) + a_iq12(3) > 0) && (1 + a_iq12(2) + a_iq12(3) > 0) && (1 - a_iq12(3) > 0)
    fprintf('Coefficients are stable.\n');
else
    error('Unstable coefficients!');
end

4.2 抗混叠与采样率选择

根据奈奎斯特采样定理, f_s 必须大于 2*f_max ,其中 f_max 是信号中最高有意义频率。对于低通滤波器, f_s 的选择需兼顾两点:

  • 下限 f_s > 10*f_c 是经验法则,确保过渡带足够陡峭,避免混叠。
  • 上限 :过高的 f_s 会增加 CPU 负载,且对 f_c 很低的滤波器(如 1 Hz), a1 , a2 会极度接近 -2 和 1,导致 IQ12 量化误差被严重放大。

推荐实践 :对 f_c < 10 Hz 的超低频滤波, f_s = 100 Hz ;对 10 Hz < f_c < 100 Hz f_s = 1 kHz ;对 f_c > 100 Hz f_s = 10 kHz 。始终在 ADC 前端加入模拟 RC 抗混叠滤波器( f_{3dB} < f_s/2 ),这是硬件层面的强制要求。

4.3 性能边界与调试技巧

  • 最大安全 f_c/f_s :当 f_c/f_s < 0.01 (即 f_c = 10 Hz @ f_s = 1 kHz )时,IQ12 的 12 位小数精度已足够。若 f_c/f_s < 0.001 ,建议改用更高精度格式(如 IQ15)或重新评估是否需要如此低的截止频率。
  • 调试状态变量 :在调试模式下,可临时将 w1 , w2 导出到 UART 或 SWO,观察其是否在合理范围内震荡(如 |w1| < 16384 ),若持续饱和则表明输入幅度过大或系数不稳定。
  • 启动瞬态处理 :若应用不允许启动时的输出跳变,可在初始化后,用一个代表“静默”的值(如 0)连续调用 filtro_iir_update() 5–10 次,使状态收敛。

5. 与其他嵌入式生态的集成

5.1 与 CMSIS-DSP 库的对比与协同

CMSIS-DSP 提供了 arm_biquad_cascade_df2T_init_q15() 等函数,其底层也是 Direct Form II Transposed,但使用 q15_t (Q15)格式。 FiltroIIR 的 IQ12 与之关键差异在于:

  • 数值范围 :Q15 为 ([-1, 1)),IQ12 为 ([-8, 8)),后者更适合处理未归一化的原始 ADC 值,省去反复的缩放/反缩放操作。
  • API 复杂度 :CMSIS-DSP 需要管理 arm_biquad_cascade_df2T_instance_q15 结构体及额外的数组, FiltroIIR 仅需一个 filtro_iir_t
  • 协同使用 :可将 FiltroIIR 作为 CMSIS-DSP 的“前端预处理器”,例如先用 IQ12 滤波器对高动态范围信号做粗滤,再将结果缩放到 ([-1,1)) 输入 CMSIS-DSP 进行精细处理。

5.2 在 Zephyr RTOS 中的封装

Zephyr 的设备树(Device Tree)模型可将滤波器参数声明为节点属性,实现硬件抽象:

&adc0 {
    filtro_iir@0 {
        compatible = "vendor,filtro-iir";
        reg = <0>;
        vendor,b0 = <1220>;
        vendor,b1 = <2440>;
        vendor,b2 = <1220>;
        vendor,a1 = <-2440>;
        vendor,a2 = <1220>;
        vendor,cutoff-freq-hz = <50>;
    };
};

驱动程序在 init() 中读取这些属性并调用 filtro_iir_init() ,使滤波器配置与硬件描述完全解耦。

6. 性能实测与基准数据

在 STM32F407VG(Cortex-M4 @ 168 MHz)上,对 filtro_iir_update() 进行精确计时(使用 DWT_CYCCNT):

输入类型 执行周期数 约定时间 (@168 MHz)
典型 IQ12 输入 42 250 ns
最坏情况(大数值乘法) 48 286 ns
平均(10000 次) 44.2 263 ns

内存占用:

  • 单个 filtro_iir_t 实例: 7 × int16_t = 14 bytes RAM;
  • 代码段( .text ): 126 bytes (GCC -O2)。

在 1 kHz 采样率下,CPU 占用率仅为 0.026% ,为其他任务留出充足余量。该数据证实了其作为“零成本抽象”的工程价值:它不是一个需要权衡的组件,而是一个可无感叠加的基础能力。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐