訊號處理:指數移動平均 (EMA) 濾波器
在訊號處理簡介中已介紹兩種類型的濾波器:有限脈衝響應 (FIR) 和無限脈衝響應 (IIR) 濾波器。移動平均濾波器能以 FIR 和 IIR 形式表示,何者較有優勢呢?
回顧我在部落格舊文章中所舉的例子,展開的 FIR 濾波器為以下形式:
y[5] = (x[5]+x[4]+x[3]+x[2]+x[1]+x[0])/5
在此,我們需要執行
- 5 次乘法和
- 4 次加法運算
乘法的運算成本特別高。當我們再次查看 IIR 形式時,發現它只需要:
- 3 次乘法和
- 2 次加法運算
y[6]=(x[6]+y[5]-x[1])/5
這能大幅降低運算成本!這對微控制器等嵌入式裝置而言是一大優勢,因為在每個離散時間步長上執行運算所耗用的資源較少。
例如,在使用 Python 函數 'time.time' 時,針對 FIR 和 IIR 形式的 11 點移動平均濾波器,所有參數 (視窗大小、取樣率、樣本大小等) 相同,分別得到以下執行時間:51 ms、27 ms。
離散時間 IIR 濾波器範例
現在我們已經瞭解 IIR 濾波器在微控制器上表現更好的原因。接著看看使用 Arduino UNO 和 DFRobot MPU6050 慣性量測單元(IMU) (圖1) 的範例專案。將 IMU 資料用於指數移動平均 (EMA) 濾波器,查看原始資料和平滑處理資料之間的差異。
圖 1:MPU6050 與 Arduino Uno 的方塊圖連接。(圖片來源:Mustahsin Zarif)
圖 2:MPU6050 與 Arduino Uno 的連接。 (圖片來源:Mustahsin Zarif)
指數移動平均濾波器為遞歸形式:
y[n] = α*x[n] + (1- α)*y[n-1]
此為遞歸形式,因為當前測量的任何輸出取決於先前的輸出,即系統有記憶。
常數 alpha () 決定我們想要賦予當前輸入相對於當前輸出多大的權重。為了更清楚瞭解,讓我們展開方程式:
y[n] = α*x[n] + (1- α)*(α*x[n−1]+(1−α)*y[n−2])
y[n] = α*x[n] + (1- α)*x[n−1]+α*(1−α)2*x[n−2])+ ...
y[n] = k=0nα*(1−α)k*x[n−k]
可以看到,alpha 越大,當前輸入對當前輸出的影響就越大。這樣很好,因為如果系統持續發展,早期的值就不足以代表當前的系統。另一方面,如果系統突然瞬間異常,情況就會很糟;在這種情況下,我們希望輸出遵循先前輸出的趨勢。
現在,事不宜遲,讓我們看看 EMA 濾波器的程式碼如何為 MPU6050 作業。
EMA 濾波器程式碼:
複製#include <wire.h>
#include <mpu6050.h>
MPU6050 mpu;
#define BUFFER_SIZE 11 // Window size
float accelXBuffer[BUFFER_SIZE];
float accelYBuffer[BUFFER_SIZE];
float accelZBuffer[BUFFER_SIZE];
int bufferCount = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
mpu.initialize();
if (!mpu.testConnection()) {
Serial.println("MPU6050 connection failed!");
while (1);
}
int16_t ax, ay, az;
for (int i = 0; i < BUFFER_SIZE; i++) {
mpu.getMotion6(&ax, &ay, &az, NULL, NULL, NULL);
accelXBuffer[i] = ax / 16384.0;
accelYBuffer[i] = ay / 16384.0;
accelZBuffer[i] = az / 16384.0;
}
bufferCount = BUFFER_SIZE;
}
void loop() {
int16_t accelX, accelY, accelZ;
mpu.getMotion6(&accelX, &accelY, &accelZ, NULL, NULL, NULL);
float accelX_float = accelX / 16384.0;
float accelY_float = accelY / 16384.0;
float accelZ_float = accelZ / 16384.0;
if (bufferCount < BUFFER_SIZE) {
accelXBuffer[bufferCount] = accelX_float;
accelYBuffer[bufferCount] = accelY_float;
accelZBuffer[bufferCount] = accelZ_float;
bufferCount++;
} else {
for (int i = 1; i < BUFFER_SIZE; i++) {
accelXBuffer[i - 1] = accelXBuffer[i];
accelYBuffer[i - 1] = accelYBuffer[i];
accelZBuffer[i - 1] = accelZBuffer[i];
}
accelXBuffer[BUFFER_SIZE - 1] = accelX_float;
accelYBuffer[BUFFER_SIZE - 1] = accelY_float;
accelZBuffer[BUFFER_SIZE - 1] = accelZ_float;
}
//calculate EMA using acceleration values stored in buffer
float emaAccelX = accelXBuffer[0];
float emaAccelY = accelYBuffer[0];
float emaAccelZ = accelZBuffer[0];
float alpha = 0.2;
for (int i = 1; i < bufferCount; i++) {
emaAccelX = alpha * accelXBuffer[i] + (1 - alpha) * emaAccelX;
emaAccelY = alpha * accelYBuffer[i] + (1 - alpha) * emaAccelY;
emaAccelZ = alpha * accelZBuffer[i] + (1 - alpha) * emaAccelZ;
}
Serial.print(accelX_float); Serial.print(",");
Serial.print(accelY_float); Serial.print(",");
Serial.print(accelZ_float); Serial.print(",");
Serial.print(emaAccelX); Serial.print(",");
Serial.print(emaAccelY); Serial.print(",");
Serial.println(emaAccelZ);
delay(100);
}
</mpu6050.h></wire.h>
執行此程式碼並查看序列繪圖儀時,可以在 X、Y、Z 軸上,看到加速度成對的粗糙和平滑線,使用視窗大小為 11 和 alpha 值為 0.2 (圖 3 至 5)。
圖 3:X 方向的原始和濾波後的加速度值。(圖片來源:Mustahsin Zarif)
圖 4:Y 方向的原始和濾波後的加速度值。(圖片來源:Mustahsin Zarif)
圖 5:Z 方向的原始和濾波後的加速度值。(圖片來源:Mustahsin Zarif)
讓程式碼更有智慧
我們現在知道,相較於 FIR 濾波器,IIR 濾波器更適合用作控制器,因為所需的加法和乘法運算明顯較少。然而,在實作這段程式碼時,加法和乘法並不是唯一執行的運算:每當有新的時間樣本加入,都必須移動樣本,而這個過程在後台都需要運算能力。因此,可採行循環緩衝區,就不用在每個採樣時間間隔內移動所有樣本。
如何實作:使用指標記住傳入資料樣本的索引。然後,每當指標指向緩衝區中的最後一個元素時,接著會指向緩衝區的第一個元素,讓新資料取代原先儲存於此處的資料,因為那些是已不再需要的最舊資料 (圖 6)。這種方式可追蹤緩衝區中最舊的樣本並加以替換,而無須每次都要移動樣本,才能將新資料放入陣列中的最後一個元素。
圖 6:循環緩衝區範例圖。(圖片來源:Mustahsin Zarif)
使用循環緩衝區的 EMA 濾波器實作程式碼。您能嘗試用於陀螺儀而不是加速計嗎?也試看看調一調係數!
使用循環緩衝區程式碼的 EMA 濾波器:
複製#include <wire.h>
#include <mpu6050.h>
MPU6050 mpu;
#define BUFFER_SIZE 11 // Window size
float accelXBuffer[BUFFER_SIZE];
float accelYBuffer[BUFFER_SIZE];
float accelZBuffer[BUFFER_SIZE];
int bufferIndex = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
mpu.initialize();
if (!mpu.testConnection()) {
Serial.println("MPU6050 connection failed!");
while (1);
}
int16_t ax, ay, az;
for (int i = 0; i < BUFFER_SIZE; i++) {
mpu.getMotion6(&ax, &ay, &az, NULL, NULL, NULL);
accelXBuffer[i] = ax / 16384.0;
accelYBuffer[i] = ay / 16384.0;
accelZBuffer[i] = az / 16384.0;
}
}
void loop() {
int16_t accelX, accelY, accelZ;
mpu.getMotion6(&accelX, &accelY, &accelZ, NULL, NULL, NULL);
float accelX_float = accelX / 16384.0;
float accelY_float = accelY / 16384.0;
float accelZ_float = accelZ / 16384.0;
accelXBuffer[bufferIndex] = accelX_float;
accelYBuffer[bufferIndex] = accelY_float;
accelZBuffer[bufferIndex] = accelZ_float;
bufferIndex = (bufferIndex + 1) % BUFFER_SIZE; //circular buffer implementation
float emaAccelX = accelXBuffer[bufferIndex];
float emaAccelY = accelYBuffer[bufferIndex];
float emaAccelZ = accelZBuffer[bufferIndex];
float alpha = 0.2;
for (int i = 1; i < BUFFER_SIZE; i++) {
int index = (bufferIndex + i) % BUFFER_SIZE;
emaAccelX = alpha accelXBuffer[index] + (1 - alpha) emaAccelX;
emaAccelY = alpha accelYBuffer[index] + (1 - alpha) emaAccelY;
emaAccelZ = alpha accelZBuffer[index] + (1 - alpha) emaAccelZ;
}
Serial.print(accelX_float); Serial.print(",");
Serial.print(emaAccelX); Serial.print(",");
Serial.print(accelY_float); Serial.print(",");
Serial.print(emaAccelY); Serial.print(",");
Serial.print(accelZ_float); Serial.print(",");
Serial.println(emaAccelZ);
delay(100);
}
</mpu6050.h></wire.h>
結論
本文說明 IIR 和 FIR 濾波器的區別,並針對兩者的運算效率深入討論。比較 FIR 與 IIR 所需作業的數量為例,可以想像當應用擴展時,IIR 濾波器的效率會有多高,這對於硬體能力有限的即時應用而言非常重要。
此外,也研究一個使用 Arduino Uno 和 MPU6050 IMU 的範例專案,部署一個指數移動平均濾波器,以降低感測器資料中的雜訊,同時持續捕捉底層訊號行為。最後,為了提高效率,我們還檢視一個更有智慧的程式碼範例,即採用循環緩衝區,而不是在每個時間間隔移動資料。
在下一篇部落格文章中,將使用 Red Pitaya 的 FPGA 功能,實作 4 分接 FIR 濾波器數位電路!

Have questions or comments? Continue the conversation on TechForum, Digi-Key's online community and technical resource.
Visit TechForum