大多数编写固件的嵌入式工程师都使用某种数字
滤波器来清理来自各种输入的数据,例如 ADC、具有数字输出的
传感器、其他
处理器等。很多时候,使用的滤波器是移动平均值 (boxcar)、有限脉冲响应(FIR) 或无限脉冲响应 (IIR) 滤波器架构。这些滤波器是线性的,即输出与输入的幅度成线性比例。也就是说,如果将输入流的幅度加倍,滤波器的输出也会加倍(忽略任何偏移)。但有许多非线性滤波器 (NLF) 在嵌入式系统中非常有用,我敢打赌,你们中的许多人以前都使用过其中的一些。NLF 不一定以数学线性方式响应其输入。
在某些情况下,FIR 和 IIR 可能会遇到脉冲噪声和突发噪声等问题,这些噪声可能导致输出以不可接受的方式做出反应。非线性滤波器可以为数据流提供保护。根据您的应用,它们可以用作独立滤波器或用作 FIR、IIR 或 Boxcar 滤波器之前的预滤波器。
本文中的示例假设是一维流式、有符号或无符号整数(包括 long 和 long long)。一些示例可能适用于浮动,但其他示例则不然。提到流式传输是因为假设数据将连续来自源,并且这些过滤器将处理数据并将其实时一对一地发送出去。换句话说,我们不能只是扔掉坏数据,我们需要发送一些值来替换输入。不过,某些示例可能允许过采样,在这种情况下,它可能会抽取数据。例如,传感器可以以比所需速度快 10 倍的速率发送数据,然后在将 1 个样本发送到下一阶段之前处理 10 个样本。
本讨论的另一个假设是,我们正在设计需要实时处理传入样本的小型嵌入式系统。小是指我们不会有大量
内存或高 MIPS 等级。因此,我们将避免使用浮动。
那么,让我们看一下一些非线性滤波器,看看它们在哪里有用。
边界检查过滤器 您以前可能使用过此过滤器,但可能不认为它是过滤器。这些过滤器通常也称为边界检查、裁剪、范围检查、限制,甚至健全性检查。我们指的不是指针检查,而是对传入数据或已被先前代码修改的数据的数据检查。
这是一段简单的示例代码:
#define Upper_Limit 1000
#define Lower_Limit -1000
int limit_check(int n)
{
if (n < Lower_Limit) n = Lower_Limit;
else if (n > Upper_Limit) n = Upper_Limit;
return n;
}
清单 1
您可以看到,如果整数 n 大于 1000,则返回 1000。如果小于 -1000,则返回 -1000。如果介于 1000 和 -1000(含)之间,则返回 n 的原始值。这将限制大的脉冲噪声值通过您的系统,即它会过滤数据。
当与 FIR、IIR 或时间滤波器(如下所述)等其他滤波器结合使用时,可以根据运行的滤波器值缩放限制值。如果检测到超出范围的样本,则基于此移动限制,边界检查器可以返回的过滤器输出,而不是固定限制或可疑样本。
某些系统可能会提供边界检查的某些变体作为预定义的函数调用或宏。
软削波滤波器
这与边界检查有关,但它不仅仅是在达到一定水平后限制值,而是随着输入接近值或值而慢慢开始回退输出值。这种类型的软削波通常用于音频信号处理应用中。
软裁剪可以通过 sigmoid 函数或双曲正切函数等函数来完成。这里的问题是这些方法需要强大的处理能力并且需要快速逼近方法。
软削波通常会扭曲输入与输出关系的很大一部分,因此它不适合用于大多数测量温度、电路电压、电流、光照水平或其他计量输入等的传感器输入。因此,我们不会进一步讨论它,只是说如果你搜索“软剪辑”,网上有很多信息。
截断均值滤波器 截断平均值或截尾平均值是一种方法,您获取一组(至少 3 个)读数,丢弃和读数,然后对其余读数进行平均。这与您在某些奥运会评审中看到的方法类似。对于嵌入式项目,它擅长消除脉冲噪声。实现此过滤器的一种方法是排序,但在小型处理器的大多数应用中,这可能在计算上很昂贵,因此对于大于 5 个样本的任何样本,我建议扫描列表以查找值和值。运行扫描时,还计算所有条目的总数。,从总数中减去值和值,然后将该值除以条目数减 2。下面是在输入值数组上执行此类函数的示例。在代码末尾有一个可选行,可以根据需要进行舍入。
#include
int TruncatedMean(int inputArray[], unsigned int arraySize) {
int i = 0;
int min = INT_MAX;
int max = 0;
int total = 0;
int mean = 0;
for (i = 0; I < arraySize; i++) {
if (inputArray[i] < min) min = inputArray[i];
if (inputArray[i] > max) max = inputArray[i];
total = total + inputArray[i];
}
//mean = (total - min - max) / (arraySize - 2);
// The previous truncates down. To assist in rounding use the following line
mean = (total - min - max + ((arraySize - 2)/2)) / (arraySize - 2);
return mean ;
}
清单 2
如果只有 3 个值,则在计算时间上重写 C 代码以消除循环可能会更有利,如本代码示例中针对 3 个值的情况所示。
int TruncatedMean_3(int a, int b, int c) {
int mean = 0;
if ((a<=b) && (a>=c) || ((a<=c) && (a>=b)) ) mean = a;
else if ((b<=c) && (b>=a) || ((b<=a) && (b>=c)) ) mean = b;
else mean = c;
return mean;
}
清单 3
请注意,如果需要,还可以使用至少 5 个样本来实现截断平均值,以去除多个值和一个值,这对于突发噪声很有好处。另请注意,您可以将其实现为滑动函数或过采样函数。滑动函数(如移动平均值)会滑出旧的输入并插入新的输入,然后再次执行该函数。因此,每个输入都会得到一个输出。或者,过采样函数接受一个值数组,找到平均值,然后获取一个新的新值数组来处理。因此,每个输入样本数组仅生成一个输出,然后您需要在计算新平均值之前获取一组新的输入值。
中值过滤 中值滤波器查找一组样本中的中间值。这对于各种类型的噪声源可能有用。在大量样本中,将对样本数组进行排序,然后读取中间索引变量。例如,假设我们有一个包含 7 个样本的数组(样本[0 到 6]),我们对它们进行排序,然后中位数是样本[3]。请注意,由于执行速度的原因,在小型嵌入式系统中排序可能会出现问题,因此应明智地使用中值过滤。对于 3 个样本,代码与上面的代码示例函数“TruncatedMean_3”(清单 3)相同。对于较大的组,清单 4 显示了一段用于查找中位数的 C 代码示例。在代码底部,您将看到基于样本数量奇数或偶数的中位数设置。这是必需的,因为偶数个样本的中位数被定义为中间两个值的平均值。根据您的需要,您可能需要对此平均值进行四舍五入。
#define numSamples 6
int sample[numSamples] = {5,4,3,3,1,0};
int Median( int sample[], int n) {
int i = 0;
int j = 0;
int temp = 0;
int median = 0;
// First sort the array of samples
for (i = 0; i < n; ++i){
for (j = i + 1; j < n; ++j){
if (sample[i] > sample[j]){
temp = sample[i];
sample[i] = sample[j];
sample[j] = temp;
}
}
}
// If numSamples is odd, take the middle number
// If numSamples is even, average the middle two
if ( (n & 1) == 0) {
median = (sample[(n / 2) - 1] + sample[n / 2]) / 2; // Even
}
else median = sample[n / 2]; // Odd
return(median);
}
清单 4
正如截断均值滤波器一样,您可以将其实现为滑动函数或过采样函数。
多数过滤
多数过滤器,也称为模式过滤器,从一组出现次数多的样本中提取值——多数投票。(这不应与“多数元素”混淆,“多数元素”是出现次数超过样本数/2 的值。)清单 5 显示了 5 个样本的多数过滤器。
#define numSamples 5
int Majority( int sample[], int n) {
unsigned int count = 0;
unsigned int oldcount = 0;
int majoritysample = sample[0];
int i = 0;
int j = 0;
for (i = 0; i < numSamples; i++) {
count = 0;
for (j = i; j < numSamples; j++) {
if (sample[i] == sample[j]) count++;
}
if (count > oldcount) {
majoritysample = sample[i];
oldcount = count;
}
}
return majoritysample;
}
清单 5
该代码使用两个循环,个循环抓取一个元素,第二个循环然后单步遍历列表并计算有多少样本与个循环索引的值匹配。第二个循环保留沿途找到的匹配计数及其样本值,直到个循环遍历整个数组。如果有多于一组具有相同计数的样本值(即,{1, 2, 2, 0, 1},两个 2 和两个 1),则个找到的样本值将作为多数返回。
请注意,多数过滤器可能不适用于典型的嵌入数据,因为数字的动态范围(来自传感器)通常为 8、10、12 位或更大。如果接收到的样本使用动态范围的大部分,则来自一小组的样本匹配的机会很小,除非所测量的信号非常稳定。由于这种动态范围问题,对多数滤波器进行修改可能会有用。通过对二进制符号进行右移,过滤器可以匹配彼此接近的符号。例如,假设我们有数字(二进制??)00100000、00100011、01000011、00100001。这些数字都不匹配,它们都是不同的。但是,如果我们将它们全部右移 2 位,我们会得到 00001000、00100011、00010000、00001000。现在其中三个匹配。
同样,与截断均值滤波器一样,您可以将其实现为滑动函数或过采样函数。
时间过滤
这是一组滤波器,它们对信号的反应更多地基于时间而不是幅度。一分钟后这一点就会变得更清楚。我们将这些不同的时间滤波器称为模式 1 到模式 4,我们从模式 1 开始:
模式 1的工作原理是将输入样本与起始过滤值(“filterout”)进行比较,然后,如果样本大于当前过滤值,则当前过滤值增加 1。同样,如果样本小于当前过滤值,则当前过滤值增加 1。过滤后的值,当前过滤后的值减1。(增减数也可以是任意合理的固定值(如2、5、10……)。该过滤器的输出为“filterout”。可以看到输出将慢慢向信号电平移动,因此变化与时间(样本数量)比与样本值更相关。
现在,如果我们得到不需要的脉冲,无论样本的幅度是多少,它都只能将输出移动 1。这意味着突发噪声和脉冲噪声大大减轻。这种类型的滤波器非常适合相对于采样率缓慢移动的信号。它可以很好地过滤 ADC 的温度读数等数据,尤其是在电噪声环境中。它在我从事的一个项目中表现非常好,该项目提取了在电力线上发送的非常缓慢的移动信号(非常嘈杂的环境,信号比线路电压低约 -120 dB)。此外,它非常适合创建动态数字参考电平,例如交流信号的直流偏置电平或控制 PLL 的信号。清单 6 说明了使用模式 1 时间滤波器来平滑值“filterout”。
#define IncDecValue 1
int sample = 0;
int filterout = 512; // Starting value
call your “getsample” function here…
if (sample > filterout) filterout = filterout + IncDecValue;
else if (sample < filterout) filterout = filterout - IncDecValue;
清单 6
如果您要过滤的样本是 int,您可能需要进行检查以确保过滤后的值不会上溢/下溢和回绕。如果您的样本来自 10 或 12 位的传感器或 ADC,则这不是问题,无需检查。
模式2与模式1相同,但是增加/减少数不是使用单个值,而是使用两个或更多个值。一个示例是根据样本与当前过滤值(“过滤”)之间的差异使用不同的增加/减少值。如果它们很接近,我们使用±1,如果它们相距较远,我们使用±10。这已成功用于加速 VCO 的时间滤波控制的启动,该 VCO 用于匹配
GPS 信号的频率。
#define IncDecValueSmall 1
#define IncDecValueBig 10
#define BigDiff 100
int sample = 0;
int filterout = 100; // Starting value
call your “getsample” function here…
if (sample > filterout) {
if ((sample - filterout) > BigDiff) filterout = filterout + IncDecValueBig;
else filterout = filterout + IncDecValueSmall;
}
else if (sample < filterout) {
if ((filterout - sample) > BigDiff) filterout = filterout - IncDecValueBig;
else filterout = filterout - IncDecValueSmall;
}
清单 7
增量/减量值还可以是由固件根据各种内部因素或直接由用户调整的变量。
模式 3也与模式 1 非常相似,但如果样本大于当前滤波信号,则当前滤波信号将增加当前滤波信号与样本之间差异的固定百分比,而不是增加 ±1。如果样本小于当前滤波信号,则当前滤波信号减少一定百分比。让我们看一个例子。假设我们从当前过滤值(“filterout”)1000 开始,并使用 10% 的变化值。然后我们得到一个新的样本 1500。这将导致 1500-100 或 50 增加 10%。所以当前的过滤值现在是 1050。如果下一个样本是 500,并且我们使用 -10%,我们会得到过滤后的新电流为 995(1050 减去 1050-500 的 10%)。
#define IncPercent 10 // 10%
#define DecPercent 10 // 10%
int sample = 0;
int filterout = 1000; // Starting value
call your “getsample” function here…
if (sample > filterout) {
filterout = filterout + (((sample - filterout) * IncPercent) / 100);
}
else if (sample < filterout) {
filterout = filterout - (((filterout - sample) * DecPercent) / 100);
}
清单 8
需要注意的一件事是乘法中的溢出。进行这些计算时您可能需要使用长整型。另请注意,将“IncPercent”和“DecPercent”设置为可通过内??部算法或用户干预进行调整的变量可能很有用。
要在缺乏 1 或 2 个周期划分的系统上加速此代码:不要将 IncPercent 和 DecPercent 缩放 100,而是将其缩放 128(10 % 约为 13)然后代码中的“/100”将是“/128” ” 编译器会将其优化为移位操作。
模式 4与模式 3 类似,只不过与模式 2 一样,根据样本和当前输出值(“过滤器”)之间的差异,可以发挥两个或多个级别的作用。在清单 9 的代码中,有两个级别。
#define IncPctBig 25 // 25%
#define DecPctBig 25 // 25%
#define IncPctSmall 10 // 10%
#define DecPctSmall 10 // 10%
int sample = 0;
int filterout = 1000; // Stating value
call your “getsample” function here…
if (sample > filterout) {
if ((sample - filterout) > BigDiff) {
filterout = filterout + (((sample - filterout) * IncPctBig) / 100);
}
else filterout = filterout + (((sample - filterout) * IncPctSmall) / 100);
}
else if (sample < filterout) {
if ((filterout - sample) > BigDiff){
filterout = filterout - (((filterout - sample) * DecPctBig) / 100);
}
else filterout = filterout - (((filterout - sample) * DecPctSmall) / 100);
}
清单 9
一个有趣的想法是,时间滤波器也可用于生成脉冲噪声和突发噪声等统计数据。他们可以计算一段时间内发生的次数并计算脉冲/秒等统计数据。这可以通过添加另一个比“过滤”值大得多或小得多的样本进行比较来完成。