ADC 读回来的数字只是”某个时刻的某个电压”,从它到”可信的物理量”之间还有采样、滤波、校准三段路。这三段各自有各自的坑,而且互相耦合——采样率定错了,后面再好的滤波器也救不回来。
奈奎斯特与为什么要过采样
奈奎斯特采样定理说的是:采样频率必须大于信号最高频率的两倍,才能无失真地重建信号。低于这个频率,高频成分会混叠成低频假信号——你在数据里看到的”缓慢波动”,可能根本不存在,只是混叠的产物。
定理给的是理论下界,工程上通常取 5~10 倍甚至更高,理由是:
抗混叠滤波器不可能是理想砖墙。真实滤波器有过渡带,要在采样率一半处把信号彻底衰减掉,需要留出过渡带。采样率越高,过渡带越宽裕,滤波器越好做。
相位延迟。滤波器阶数越高、截止频率相对采样率越低,延迟越大。提高采样率可以换来更小的相对延迟。
过采样换分辨率。在噪声近似白噪声的前提下,对同一个量做 4 倍过采样再平均,有效位数大约能提升 1 位;16 倍提升 2 位。这是用时间换精度的经典做法,代价是采样时间变长、功耗上升。很多 MCU 的 ADC 自带硬件过采样单元,比软件平均省事。
需要注意过采样换分辨率有个前提:输入信号上要有足够幅度且随机的噪声。如果信号太干净,量化误差是相关的,平均也白搭。
采样率的权衡
采样率不是越高越好,它直接牵动三件事:
- 功耗:ADC 转换、传感器激励、MCU 唤醒都有成本。电池设备里,采样率是续航的一阶影响因素。
- 存储与带宽:原始数据存储下来再处理,每秒 N 个点 × 通道数 × 位数,几天就能吃满 flash。
- 处理量:滤波、FFT 都是按点算的。
反过来采样率过低会漏掉事件。一个只在几十毫秒内出现的冲击,用 1 Hz 采样是抓不到的。
实用的取法是:按要捕捉的最短事件定下界,按功耗和带宽预算定上界,然后在这个区间里选一个,再验证滤波后的延迟是否满足控制需求。对缓慢变化的量(温度、液位、土壤湿度),采样率在 1 Hz 甚至更低量级就够;对振动、声音、电流波形,往往是 kHz 甚至更高量级。
三种常用滤波
滑动平均:维护一个长度 N 的环形缓冲,输出算术平均。对白噪声抑制明显,代价是延迟约 N/2 个采样周期,且对脉冲干扰几乎无效——一个尖峰会被摊平成一个宽一点的小鼓包,不会消失。
#define N 8
static int32_t buf[N];
static uint8_t idx;
static int32_t sum;
int32_t moving_avg(int32_t x) {
sum -= buf[idx];
buf[idx] = x;
sum += x;
idx = (idx + 1) % N;
return sum / N;
}
用 sum 增量维护避免每次求和,N 较大时差别明显。注意 sum 的溢出:N 乘以满量程要在 32 位范围内,或者用 64 位累加。
中值滤波:取窗口内的中位数。对脉冲干扰(偶发的跳变、毛刺)效果极好,且不像平均那样把阶跃抹平。代价是需要排序,N 通常取 3、5、7 这类小奇数。
int32_t median3(int32_t a, int32_t b, int32_t c) {
int32_t t;
if (a > b) { t = a; a = b; b = t; }
if (b > c) { t = b; b = c; c = t; }
if (a > b) { t = a; a = b; b = t; }
return b;
}
中值滤波适合”偶尔蹦个野点”的场景,比如接近开关、超声测距、被电磁干扰的模拟量。
一阶低通(指数平滑):y[n] = α·x[n] + (1-α)·y[n-1]。只需保存一个状态,内存最省,计算最轻。α 越小越平滑但延迟越大。截止频率与 α、采样周期的关系可以近似成 fc ≈ α / (2π·T·(1-α)),够用来选型。
/* alpha 用整数定点:Q15 格式,alpha_q15 = alpha * 32768 */
int32_t lpf(int32_t x, int32_t prev, int32_t alpha_q15) {
return prev + ((alpha_q15 * (x - prev)) >> 15);
}
用定点实现避免在无 FPU 的 MCU 上跑浮点。定点运算要注意中间结果溢出,alpha_q15 * (x - prev) 可能超过 32 位,用 int64_t 中转或者限制输入范围。
三者的适用场景可以粗略划分:随机噪声用平均或一阶低通;偶发毛刺用中值;既要去噪又要保留阶跃边缘,用中值打头、低通收尾的两级组合。
卡尔曼滤波的门槛
卡尔曼的直觉是”预测 + 观测的加权”:系统模型给出一个预测值,传感器给出一个观测值,两者的不确定度(方差)谁小就多信谁,权重由一个叫卡尔曼增益的系数动态决定。测量噪声大时更信模型,模型不确定度累积变大时更信测量。
一维情形下代码不长,难的是参数整定:过程噪声方差 Q 和测量噪声方差 R 决定了滤波器的行为。Q 大表示不相信模型、响应快但抖;R 大表示不相信传感器、平滑但迟钝。这两个数通常靠实测数据的统计特性来定,而不是拍脑袋。
多变量卡尔曼涉及矩阵运算和协方差传播,在 MCU 上要处理矩阵求逆的数值稳定性,代码量和内存占用都不小。如果系统模型本身不线性,还得上 EKF,线性化误差又会引入新的问题。
实际选型建议:如果只是为了平滑,一阶低通足够;如果需要融合多个传感器(比如加速度计和陀螺仪),卡尔曼才有明确收益。
两点校准
传感器输出的电压或码值到物理量之间是线性关系 物理值 = k · 码值 + b,k 和 b 会因器件离散性、温漂而变。两点校准就是在两个已知标准点读取码值,解出 k 和 b:
k = (P2 - P1) / (C2 - C1)
b = P1 - k · C1
选取两个标准点时有讲究:两点跨度越大,量程内的误差越小。用 0 ℃ 和 100 ℃ 标定温度计,比用 20 ℃ 和 30 ℃ 定出来的 k 准得多。同时两点要落在常用量程内,外推到量程之外误差会放大。
标定用的标准源精度必须高于被测传感器一个量级,否则校出来的系数只是把标准源的误差刻了进去。
实现上 k 和 b 存到 flash 或 EEPROM,上电加载。存之前加 CRC,避免写入失败导致校准系数变成垃圾值,让设备读数整体偏移。
温漂补偿
零点和灵敏度都会随温度漂移。补偿有两条路:
硬件上,把温度传感器放在尽量靠近敏感元件的位置,保证测到的温度是传感器自己的温度而不是板子另一头的温度。
软件上,建立温度与偏移量的关系。简单做法是分段线性:标定若干个温度点下的零点偏移,运行时插值。复杂一点拟合一条二次曲线。如果有温度传感器和被测量的联合标定数据,可以写成 P = (k0 + k1·ΔT) · C + (b0 + b1·ΔT) 的形式。
一个容易忽略的细节:上电自热。器件刚上电时温度还在爬升,这时候做的零点校准会把冷机状态的偏移记进去。通常让设备上电预热一段时间(量级在几十秒到几分钟,视器件而定)再采集。
异常值识别
滤波能压噪声,但识别”这个数据根本不能用”是另一件事。几种判据:
超量程:码值贴着 0 或满量程,通常是传感器断线、短路或者饱和。直接判无效,不要当成真值滤波。
斜率限幅:物理量有变化速率上限(温度不可能在 1 秒内跳 50 ℃)。相邻两次的差值超过阈值就判为跳变。注意区分真跳变和干扰——连续两拍都跳,可能是真实事件。
连续跳变计数:连续 N 次超阈值才报警,避免单次干扰误报。同时维护一个连续无效计数,超过上限就上报传感器故障。
合理性交叉验证:多个相关量互相印证,比如流量为 0 但压差很大,其中一个是错的。
识别出异常之后的处理策略要提前定:用上一次有效值填充、标记为无效不参与聚合、或者触发传感器故障上报。这三种选择对上层业务的含义完全不同,别让异常数据静悄悄地被平均掉。
评论(2)
说实话第一次见有人把这块讲得这么不装。
看完去查了下我们的配置,果然也是错的。
请 登录 后参与评论。