訊號處理:指數移動平均 (EMA) 濾波器

訊號處理簡介中已介紹兩種類型的濾波器:有限脈衝響應 (FIR) 和無限脈衝響應 (IIR) 濾波器。移動平均濾波器能以 FIR 和 IIR 形式表示,何者較有優勢呢?

回顧我在部落格舊文章中所舉的例子,展開的 FIR 濾波器為以下形式:

y[5] = (x[5]+x[4]+x[3]+x[2]+x[1]+x[0])/5

在此,我們需要執行

  1. 5 次乘法和
  2. 4 次加法運算

乘法的運算成本特別高。當我們再次查看 IIR 形式時,發現它只需要:

  1. 3 次乘法和
  2. 2 次加法運算

y[6]=(x[6]+y[5]-x[1])/5

這能大幅降低運算成本!這對微控制器等嵌入式裝置而言是一大優勢,因為在每個離散時間步長上執行運算所耗用的資源較少。

例如,在使用 Python 函數 'time.time' 時,針對 FIR 和 IIR 形式的 11 點移動平均濾波器,所有參數 (視窗大小、取樣率、樣本大小等) 相同,分別得到以下執行時間:51 ms、27 ms。

離散時間 IIR 濾波器範例

現在我們已經瞭解 IIR 濾波器在微控制器上表現更好的原因。接著看看使用 Arduino UNODFRobot 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 濾波器數位電路!

關於作者

Image of Mustahsin Zarif

Electrical Engineering student at The University of California, San Diego.

More posts by Mustahsin Zarif
 TechForum

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

Visit TechForum