GY-521使用時の相補フィルターとカルマンフィルター
電子工作の実験結果。 2024年の7月頃にちょっと試した結果なのだが、全然まとめていないのでここで供養。GY-521型6軸センサーとArduino Uno R3で相補フィルターとカルマンフィルターの実験をした。
参考にしたサイトは以下のサイトと、あと2, 3のサイト*1。
実験結果
実験のセットアップは下の写真の通り。ブレットボードに乗せたセンサーを手でくるくる回して安定性を評価した。

まずは相補フィルターの実験結果。ジャイロセンサー(黄色)のオフセットの影響を十分排除できている。

つぎにカルマンフィルターの実験結果。こちらもジャイロセンサー(黄色)のオフセットの影響を十分排除できている。しかも、相補フィルターと比較してみると、加速度センサーの突発的な振動に対しては反応せず、安定した出力を返していることがわかる。もちろん相補フィルターでもフィルター定数の調整で同様の出力結果を出せそうだが、無調整のカルマンフィルターでここまでの出力ができたのはすごい。 センサーフュージョンでカルマンフィルターが第一選択に来るのも納得。

コードは以下に記載する*2。
相補フィルターを使用したときのArduinoスケッチ
// MPU6050 on ESP8266
#include <Wire.h>
#include <math.h>
#define MPU6050_ADDR 0x68
#define MPU6050_AX 0x3B
#define MPU6050_AY 0x3D
#define MPU6050_AZ 0x3F
#define MPU6050_TP 0x41
#define MPU6050_GX 0x43
#define MPU6050_GY 0x45
#define MPU6050_GZ 0x47
short int AccX, AccY, AccZ;
short int Temp;
short int GyroX, GyroY, GyroZ;
float AccXmeas, AccYmeas, AccZmeas;
float angleX_acc, angleY_acc;
float angleX_gyro = 0.0, angleY_gyro = 0.0;
float angleX_comp = 0.0;
unsigned long prevTime = 0;
float gyroX_offset = 0.0, gyroY_offset = 0.0, gyroZ_offset = 0.0;
// sampling time in milliseconds
float sample_wait = 200.0;
// cut off frequency in Hz
float cutoff_freq = 1.0;
// low pass time constant in seconds
float tc = 1.0 / (2.0*M_PI*cutoff_freq);
// 相補フィルターの定数(LPF定数)、Accelに適用
const float alpha = (sample_wait/1000) / ((sample_wait/1000) + tc);
// Furntion to write a byte to a register
uint8_t MPU6050_WriteRegister_Byte (int MPUAdd, int Reg, uint8_t Value){
Wire.beginTransmission(MPUAdd);
Wire.write(Reg); // Register for writing
Wire.write(Value); // Write value
Wire.endTransmission(true);
}
void calculateGyroOffsets() {
int numReadings = 1000;
long gyroX_sum = 0, gyroY_sum = 0, gyroZ_sum = 0;
for (int i = 0; i < numReadings; i++) {
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(MPU6050_AX);
Wire.endTransmission();
Wire.requestFrom(MPU6050_ADDR, 14);
Wire.read(); Wire.read(); // skip AccX
Wire.read(); Wire.read(); // skip AccY
Wire.read(); Wire.read(); // skip AccZ
Wire.read(); Wire.read(); // skip Temp
gyroX_sum += (Wire.read() << 8) | Wire.read();
gyroY_sum += (Wire.read() << 8) | Wire.read();
gyroZ_sum += (Wire.read() << 8) | Wire.read();
delay(5);
}
gyroX_offset = gyroX_sum / numReadings;
gyroY_offset = gyroY_sum / numReadings;
gyroZ_offset = gyroZ_sum / numReadings;
}
void setup() {
Serial.begin(9600);
// Serial.println("*** started");
Wire.begin();
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(0x6B);
Wire.write(0);
Wire.endTransmission();
//MPU-6050のレジスタ(26)を'6'に設定しDLPFの設定を変更する
MPU6050_WriteRegister_Byte (MPU6050_ADDR, 26 ,6);
calculateGyroOffsets();
}
void loop() {
unsigned long currTime = millis();
float dt = (currTime - prevTime) / 1000.0;
prevTime = currTime;
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(MPU6050_AX);
Wire.endTransmission();
Wire.requestFrom(MPU6050_ADDR, 14);
AccX = Wire.read() << 8; AccX |= Wire.read();
AccY = Wire.read() << 8; AccY |= Wire.read();
AccZ = Wire.read() << 8; AccZ |= Wire.read();
Temp = Wire.read() << 8; Temp |= Wire.read();
GyroX = Wire.read() << 8; GyroX |= Wire.read();
GyroY = Wire.read() << 8; GyroY |= Wire.read();
GyroZ = Wire.read() << 8; GyroZ |= Wire.read();
GyroX -= gyroX_offset;
GyroY -= gyroY_offset;
GyroZ -= gyroZ_offset;
// Full Scale Range +/-2g のとき 16384 LSB/gのため16384で割る
// AccX, AccY, AccZはADC出力ビット数のため、AccXmeas, AccYmeas, AccZmeasのg単位へ変換する
AccXmeas = AccX / 16384.0;
AccYmeas = AccY / 16384.0;
AccZmeas = AccZ / 16384.0;
float temperature = (Temp / 340) + 36.53;
angleX_acc = atan2(AccY, sqrt(pow(AccX, 2) + pow(AccZ, 2))) * 180.0 / M_PI;
angleY_acc = atan2(-AccX, sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180.0 / M_PI;
// Full Scale Range +/-250deg./s のとき 131.0 LSB/(deg./s)のため131で割る
angleX_gyro += GyroX * dt / 131.0;
angleY_gyro += GyroY * dt / 131.0;
// 相補フィルターの適用
angleX_comp = (1 - alpha) * (angleX_comp + GyroX * dt / 131.0) + (alpha) * angleX_acc;
Serial.print("AccX_Angle[deg.]:"); Serial.print(angleX_acc); Serial.print(",");
Serial.print("GyroX_Angle[deg.]:"); Serial.print(angleX_gyro); Serial.print(",");
Serial.print("CompX_Angle[deg.]:"); Serial.print(angleX_comp); Serial.print("");
Serial.println("");
delay(sample_wait);
}
カルマンフィルターを使用したときのArduinoスケッチ
// MPU6050 on ESP8266
#include <Wire.h>
#include <math.h>
#define MPU6050_ADDR 0x68
#define MPU6050_AX 0x3B
#define MPU6050_AY 0x3D
#define MPU6050_AZ 0x3F
#define MPU6050_TP 0x41
#define MPU6050_GX 0x43
#define MPU6050_GY 0x45
#define MPU6050_GZ 0x47
short int AccX, AccY, AccZ;
short int Temp;
short int GyroX, GyroY, GyroZ;
float AccXmeas, AccYmeas, AccZmeas;
float angleX_acc, angleY_acc;
float angleX_gyro = 0.0, angleY_gyro = 0.0;
float angleX_kalman = 0.0;
unsigned long prevTime = 0;
float gyroX_offset = 0.0, gyroY_offset = 0.0, gyroZ_offset = 0.0;
// カルマンフィルター用の変数
float Q_angle = 0.001; // 加速度計による角度測定の誤差共分散
float Q_bias = 0.003; // ジャイロバイアスの誤差共分散
float R_measure = 0.03; // 測定の誤差共分散
float angle = 0.0; // 角度
float bias = 0.0; // バイアス
float P[2][2] = {{0, 0}, {0, 0}}; // 誤差共分散行列
float rate = 0.0; // 角速度
// sampling time in milliseconds
float sample_wait = 200.0;
// 関数: レジスタにバイトを書き込む
uint8_t MPU6050_WriteRegister_Byte (int MPUAdd, int Reg, uint8_t Value) {
Wire.beginTransmission(MPUAdd);
Wire.write(Reg); // 書き込み先レジスタ
Wire.write(Value); // 書き込む値
Wire.endTransmission(true);
}
void calculateGyroOffsets() {
int numReadings = 1000;
long gyroX_sum = 0, gyroY_sum = 0, gyroZ_sum = 0;
for (int i = 0; i < numReadings; i++) {
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(MPU6050_AX);
Wire.endTransmission();
Wire.requestFrom(MPU6050_ADDR, 14);
Wire.read(); Wire.read(); // AccXスキップ
Wire.read(); Wire.read(); // AccYスキップ
Wire.read(); Wire.read(); // AccZスキップ
Wire.read(); Wire.read(); // Tempスキップ
gyroX_sum += (Wire.read() << 8) | Wire.read();
gyroY_sum += (Wire.read() << 8) | Wire.read();
gyroZ_sum += (Wire.read() << 8) | Wire.read();
delay(5);
}
gyroX_offset = gyroX_sum / numReadings;
gyroY_offset = gyroY_sum / numReadings;
gyroZ_offset = gyroZ_sum / numReadings;
}
void setup() {
Serial.begin(9600);
Wire.begin();
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(0x6B);
Wire.write(0);
Wire.endTransmission();
// MPU-6050のレジスタ(26)を'6'に設定しDLPFの設定を変更する
MPU6050_WriteRegister_Byte (MPU6050_ADDR, 26 ,6);
calculateGyroOffsets();
}
void loop() {
unsigned long currTime = millis();
float dt = (currTime - prevTime) / 1000.0;
prevTime = currTime;
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(MPU6050_AX);
Wire.endTransmission();
Wire.requestFrom(MPU6050_ADDR, 14);
AccX = Wire.read() << 8; AccX |= Wire.read();
AccY = Wire.read() << 8; AccY |= Wire.read();
AccZ = Wire.read() << 8; AccZ |= Wire.read();
Temp = Wire.read() << 8; Temp |= Wire.read();
GyroX = Wire.read() << 8; GyroX |= Wire.read();
GyroY = Wire.read() << 8; GyroY |= Wire.read();
GyroZ = Wire.read() << 8; GyroZ |= Wire.read();
GyroX -= gyroX_offset;
GyroY -= gyroY_offset;
GyroZ -= gyroZ_offset;
// Full Scale Range +/-2g のとき 16384 LSB/gのため16384で割る
// AccX, AccY, AccZはADC出力ビット数のため、AccXmeas, AccYmeas, AccZmeasのg単位へ変換する
AccXmeas = AccX / 16384.0;
AccYmeas = AccY / 16384.0;
AccZmeas = AccZ / 16384.0;
float temperature = (Temp / 340.0) + 36.53;
angleX_acc = atan2(AccY, sqrt(pow(AccX, 2) + pow(AccZ, 2))) * 180.0 / M_PI;
angleY_acc = atan2(-AccX, sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180.0 / M_PI;
// Full Scale Range +/-250deg./s のとき 131.0 LSB/(deg./s)のため131で割る
angleX_gyro += GyroX * dt / 131.0;
angleY_gyro += GyroY * dt / 131.0;
// カルマンフィルターの適用
// rateは角速度deg./s
float rate = GyroX / 131.0 - bias;
angleX_kalman += dt * rate;
P[0][0] += dt * (dt*P[1][1] - P[0][1] - P[1][0] + Q_angle);
P[0][1] -= dt * P[1][1];
P[1][0] -= dt * P[1][1];
P[1][1] += Q_bias * dt;
float S = P[0][0] + R_measure;
float K[2];
K[0] = P[0][0] / S;
K[1] = P[1][0] / S;
float y = angleX_acc - angleX_kalman;
angleX_kalman += K[0] * y;
bias += K[1] * y;
float P00_temp = P[0][0];
float P01_temp = P[0][1];
P[0][0] -= K[0] * P00_temp;
P[0][1] -= K[0] * P01_temp;
P[1][0] -= K[1] * P00_temp;
P[1][1] -= K[1] * P01_temp;
Serial.print("AccX_Angle[deg.]:"); Serial.print(angleX_acc); Serial.print(",");
Serial.print("GyroX_Angle[deg.]:"); Serial.print(angleX_gyro); Serial.print(",");
Serial.print("KalmanX_Angle[deg.]:"); Serial.print(angleX_kalman); Serial.println("");
delay(sample_wait);
}