[電子工作] センサーのバイアス補正を行う

制御工学

みなさんこんにちは
おかしょです.

この記事ではセンサーのバイアス補正のやり方を解説します.

バイアス補正を行わないと,出力されるデータにずれが生じてしまうので,センサーを使用する際にはバイアス補正を必ず行わなければなりません.

この記事を読むと以下のようなことがわかる・できるようになります.

  • バイアスとは何か
  • バイアスの与える影響
  • バイアス補正を行う方法

 

この記事を読む前に

この記事ではバイアス補正を行う方法を解説するために,加速度・ジャイロ・地磁気の三つを取得することのできるセンサーを使用します.

また,バイアス補正をするためにセンサーのデータを使用するので,センサーの取得方法・I2C通信のやり方についてはわかっているものとしてバイアス補正をするプログラムを書いています.

センサーのデータの取得方法やI2C通信のやり方などがわからない方は,まず以下の記事を読むことをおすすめします.

 

今回使用するもの

本記事で使用するのは9軸センサーで加速度・ジャイロ・地磁気の取得ができるLSM9DS1というセンサーを使用します.

  • Arduino Uno
  • USBケーブル(書き込み用)
  • ブレッドボード
  • ジャンパー線
  • XBee×2(シリアル通信用)
  • XBeeピッチ変換基板(3.3V電圧レギュレータ実装)
  • XBee USBインターフェースボード
  • USBケーブル(通信用)
  • 電池(9V)
  • LSM9DS1(9軸センサー)

シリアル通信はセンサーを動かしやすいようにXBeeで無線化しています.

特に問題がない方はUSBケーブルでシリアル通信を行っても大丈夫なので,XBee関連の部品と電池は必要ありません.

 

バイアスとは何か

バイアスとはセンサー開発時に生じるセンサーのズレのことを言います.

このずれによって,出力される値が実際の真値と異なることがあります.

例えば,加速度センサーを使用する場合,センサーを水平に置けばz軸の値は1gを示すはずです.しかし,バイアスが存在する場合,この値が1.2gを表すことがあります.

このようなずれは,センサーによって一定の値ではないため,使用者が自分でどのくらいずれているのかを測定しなければなりません.

 

バイアス補正をしないとどうなるか

それでは,バイアスの補正を行わないとどのようなことがあるのでしょうか.

先程の例で言えば,ズレは+0.2gであると言えます.

このずれをそのままにして姿勢角の算出を行ったとすると,当然ロボットの姿勢角にもずれが生じます.そのため正確ではないロボットの姿勢を用いて,ロボットの姿勢制御を行うことになります.

センサーの値が正確ではないためロボットは安定化することができず,暴走して壊れてしまう恐れがあります.

バイアスの補正を行わないとこのようなことが起きかねないので,しっかりバイアスの補正は行うようにしましょう.

 

バイアス補正の方法

先程の例では,バイアスは+0.2gであると言いました.しかし,同じ型番のセンサーであっても製品によっては0.8gを示し,ズレが-0.2gとなる場合もあります.

この+0.2gや-0.2gを測定しなければバイアスの補正は行えないので,測定する必要があります.

バイアスの測定は非常に簡単で,センサーを動かさずに置いた状態でいくつかデータを取得し,その平均値を求めます.その平均値と真値の誤差を求めればOKです.

例えば加速度センサーの場合は,z軸を地球中心方向に向けたとすれば,x軸,y軸の真値は0,z軸の真値は1gということになります.角速度センサーの場合は,動かしていないのですべての軸において真値は0となります.地磁気センサーに関しては少し特殊なため,別の記事で解説します.(地磁気センサーに関してはこちらを参照してください)

それでは,実際にやっていきましょう.

今回の回路図はこのようになります.
この回路は私が使用しているものと同じものを使用していた場合は使えますが,違った部品を使う場合は,電子部品が壊れる可能性もあるので確認してから使用してください.

例えば,XBeeのピッチ変換基板に3.3v電圧レギュレータが実装されていない場合はXBeeの1番ピンはArduino Unoの3.3Vと接続させなければなりませんのでお気を付けください.

実際に回路を組むとこのようになります.

まずは,センサーを動かさないようにしてデータの取得を行います.

データの取得方法についてはこちらを参照してください.

データを取得し,その平均値を求めます.

平均値を求めたら,想定される真値との誤差を求めます.この時に算出される値がバイアス値になります.式で表すと以下のようになります.

$$「バイアス値」=「平均値」-「想定される真値」$$

なので,バイアス補正を行うには出力されるデータからバイアス値を引けばOKです.式で表すと以下のようになります.

$$「バイアス補正後データ」=「バイアス補正前データ」-「バイアス値」$$

このようにすることで,バイアス補正を行うことができます.

プログラムを書くと以下のようになります.

#include <Wire.h>

#define AG_DEVICE_ADDRESS 0x6B
#define M_DEVICE_ADDRESS  0x1E
#define AG_WHO_AM_I       0x0F
#define CTRL_REG1_G       0x10
#define CTRL_REG2_G       0x11
#define CTRL_REG3_G       0x12
#define ORIENT_CFG_G      0x13
#define OUT_T             0x15
#define STATUS_REG_T      0x17
#define OUT_G             0x18
#define CTRL_REG4         0x1E
#define CTRL_REG5_XL      0x1F
#define CTRL_REG6_XL      0x20
#define CTRL_REG7_XL      0x21
#define STATUS_REG_AG     0x27
#define OUT_A             0x28
#define M_WHO_AM_I        0x0F
#define CTRL_REG1_M       0x20
#define CTRL_REG2_M       0x21
#define CTRL_REG3_M       0x22
#define CTRL_REG4_M       0x23
#define CTRL_REG5_M       0x25
#define STATUS_REG_M      0x27
#define OUT_M             0x28

#define PRINT_RATE        50    // PRINT_RATE Hzで出力する

  uint8_t ODR_G = 3;  // Output data rate: 1=14.9, 2=59.5, 3=119, 4=238, 5=476, 6=952 Hz
  uint8_t FS_G = 0;   // Full scale: 0=245, 1=500, 2=no, 3=2000 dps
  uint8_t BW_G = 0;   // cutoff frequency: table 47
  uint8_t LP_mode = 0;  // low power mode: 0=disable, 1=enable
  uint8_t HP_EN = 1;    // high pass filter: 0=disable, 1=enable
  uint8_t HPCF_G = 1;   // high pass cutoff frequency: table 52
  uint8_t Zen_G = 1;    // enable gyro Z:  0=disable, 1=enable
  uint8_t Yen_G = 1;    // enable gyro Y:  0=disable, 1=enable
  uint8_t Xen_G = 1;    // enable gyro X:  0=disable, 1=enable
  uint8_t LIR_XL1 = 1;  // latch interrupt: 0=no request, 1=request
  uint8_t FourD_XL1 = 0;
  uint8_t SignX_G = 0;  // x axis sign: 0=positive, 1=negative
  uint8_t SignY_G = 0;  // y axis sign: 0=positive, 1=negative
  uint8_t SignZ_G = 0;  // z axis sign: 0=positive, 1=negative
  uint8_t Orient = 0;
  uint8_t DEC_ = 0;
  uint8_t Zen_XL = 1;   // enable accel Z:  0=disable, 1=enable
  uint8_t Yen_XL = 1;   // enable accel Y:  0=disable, 1=enable
  uint8_t Xen_XL = 1;   // enable accel X:  0=disable, 1=enable
  uint8_t ODR_XL = 1;   // Output date rate: 1=10, 4=238, 2=50, 5=476, 3=119, 6=952 Hz
  uint8_t FS_XL = 3;   // Full scale: 0=2, 1=16, 2=4, 3=8 g
  uint8_t BW_SCAL_ODR = 1;  // 0=band width determined by ODR, 1=determined by BW_XL
  uint8_t BW_XL = 0;    // band width: 0=408, 1=211, 2=105, 3=50 Hz
  uint8_t HR = 0;       // high resolution mode: 0=disable, 1=enable
  uint8_t DCF = 0;      // low pass cutoff frequency: 0=ODR/50, 2=ODR/9, 1=ODR/100, 3=ODR/400
  uint8_t FDS = 0;
  uint8_t HPIS = 0;  
  uint8_t TEMP_COMP = 0;  // temperature compensation: 0=disable, 1=enable
  uint8_t OM = 0;         // operation mode: 0=continuous-conversion, 1=single-conversion, 2=power-down
  uint8_t DO = 5;         // Output data rate: 0=0.625, 1=1.25, 2=2.5, 3=5, 4=10, 5=20, 6=40, 7=80 Hz
  uint8_t FAST_ODR = 0;
  uint8_t ST = 0;
  uint8_t FS = 2;         // full scale: 0=4, 1=8, 2=12, 3=16 gauss
  uint8_t REBOOT = 0;
  uint8_t SOFT_RST = 0;
  uint8_t I2C_DISABLE = 0;  // I2c interface: 0=enable, 1=disable
  uint8_t LP = 0;           // low power mode: 0=disable, 1=enable
  uint8_t SIM = 0;          // SPI interface
  uint8_t MD = 0;           // operation mode: 0=continuous-conversion, 1=single-conversion, 2=power-down
  uint8_t OMZ = 3;    // z axis operation mode: 0=low power, 1=medium, 2=high, 3=ultra-high performance
  uint8_t BLE = 0;
  uint8_t FAST_READ = 0;
  uint8_t BDU = 0;

// 時間変数
unsigned long InitTime, RunTime, PrintTime;

int16_t AccX, AccY, AccZ;
int16_t GyrX, GyrY, GyrZ;
int16_t MagX, MagY, MagZ;
int16_t Temp;

int32_t AccBias[3], GyrBias[3];
int32_t AccReal[3] = {0, 0, 4098};
int32_t GyrReal[3] = {0, 0, 0};

uint8_t I2Cread1byte(uint8_t DeviceAddress, uint8_t SlaveAddress)
{
  Wire.beginTransmission(DeviceAddress); // 通信を開始する
  Wire.write(SlaveAddress);             // レジスターアドレスを送信
  Wire.endTransmission(false);  // リスタートメッセージをリクエストの後に送信し,接続を続ける
  Wire.requestFrom(DeviceAddress, (uint8_t) 1);  // 1 byte分のデータを要求する
  return Wire.read();         // データを返す
}

uint8_t I2CreadMULTIbyte(uint8_t DeviceAddress, uint8_t SlaveAddress, uint8_t num, uint8_t * data)
{
  Wire.beginTransmission(DeviceAddress);
  Wire.write(SlaveAddress|0x80);
  Wire.endTransmission(false);
  Wire.requestFrom(DeviceAddress, num);
  for (uint8_t i=0; i<num; i++)
  {
    data[i] = Wire.read();
  }
  return num;
}

void I2Cwrite1byte(uint8_t DeviceAddress, uint8_t SlaveAddress, uint8_t data)
{
  Wire.beginTransmission(DeviceAddress); // 通信を開始する
  Wire.write(SlaveAddress);             // レジスターアドレスを送信
  Wire.write(data);             // レジスターアドレスを送信
  Wire.endTransmission();       // 通信を終了する
}

bool LSM9DS1setting()
{
  uint8_t who_ag, who_m;

  uint8_t RegisterData = 0;

  RegisterData=ODR_G<<5;
  RegisterData|=FS_G<<3;
  RegisterData|=BW_G;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG1_G, RegisterData);
  RegisterData=0;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG2_G, RegisterData);
  RegisterData=LP_mode<<7;
  RegisterData|=HP_EN<<6;
  RegisterData|=HPCF_G;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG3_G, RegisterData);
  RegisterData=0;
  RegisterData=Zen_G<<5;
  RegisterData|=Yen_G<<4;
  RegisterData|=Xen_G<<3;
  RegisterData|=LIR_XL1<<2;
  RegisterData|=FourD_XL1;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG4, RegisterData);
  RegisterData=0;
  RegisterData=SignX_G<<5;
  RegisterData|=SignY_G<<4;
  RegisterData|=SignZ_G<<3;
  RegisterData|=Orient;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, ORIENT_CFG_G, RegisterData);
  RegisterData=0;
  RegisterData=DEC_<<6;
  RegisterData|=Zen_XL<<5;
  RegisterData|=Yen_XL<<4;
  RegisterData|=Xen_XL<<3;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG5_XL, RegisterData);
  RegisterData=0;
  RegisterData=ODR_XL<<5;
  RegisterData|=FS_XL<<3;
  RegisterData|=BW_SCAL_ODR<<2;
  RegisterData|=BW_XL;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG6_XL, RegisterData);
  RegisterData=0;
  RegisterData=HR<<7;
  RegisterData|=DCF<<5;
  RegisterData|=FDS<<2;
  RegisterData|=HPIS;
  I2Cwrite1byte(AG_DEVICE_ADDRESS, CTRL_REG7_XL, RegisterData);
  RegisterData=0;
  RegisterData=TEMP_COMP<<7;
  RegisterData|=OM<<5;
  RegisterData|=DO<<2;
  RegisterData|=FAST_ODR<<1;
  RegisterData|=ST;
  I2Cwrite1byte(M_DEVICE_ADDRESS, CTRL_REG1_M, RegisterData);
  RegisterData=0;
  RegisterData=FS<<5;
  RegisterData|=REBOOT<<3;
  RegisterData|=SOFT_RST<<2;
  I2Cwrite1byte(M_DEVICE_ADDRESS, CTRL_REG2_M, RegisterData);
  RegisterData=0;
  RegisterData=I2C_DISABLE<<7;
  RegisterData|=LP<<5;
  RegisterData|=SIM<<2;
  RegisterData|=MD;
  I2Cwrite1byte(M_DEVICE_ADDRESS, CTRL_REG3_M, RegisterData);
  RegisterData=0;
  RegisterData=OMZ<<2;
  RegisterData|=BLE<<1;
  I2Cwrite1byte(M_DEVICE_ADDRESS, CTRL_REG4_M, RegisterData);
  RegisterData=0;
  RegisterData=FAST_READ<<7;
  RegisterData|=BLE<<6;
  I2Cwrite1byte(M_DEVICE_ADDRESS, CTRL_REG5_M, RegisterData);


  who_ag = I2Cread1byte(AG_DEVICE_ADDRESS, AG_WHO_AM_I);
  who_m = I2Cread1byte(M_DEVICE_ADDRESS, M_WHO_AM_I);
  if (who_ag==0x68&&who_m==0x3D)
  {
    return true;
  }
  else
  {
    return false;
  }
}

void GetAccel()
{
  uint8_t data[6];
  if(I2Cread1byte(AG_DEVICE_ADDRESS, STATUS_REG_AG)&1)  // データの更新確認
  {
    if(I2CreadMULTIbyte(AG_DEVICE_ADDRESS, OUT_A, 6, data)==6)
    {
      AccX = (data[1]<<8)|data[0];
      AccY = (data[3]<<8)|data[2];
      AccZ = (data[5]<<8)|data[4];
    }
  }
}

void GetGyro()
{
  uint8_t data[6];
  if(I2Cread1byte(AG_DEVICE_ADDRESS, STATUS_REG_AG)&2)  // データの更新確認
  {
    if(I2CreadMULTIbyte(AG_DEVICE_ADDRESS, OUT_G, 6, data)==6)
    {
      GyrX = (data[1]<<8)|data[0];
      GyrY = (data[3]<<8)|data[2];
      GyrZ = (data[5]<<8)|data[4];
    }
  }
}

void CalculateBias()
{
  int32_t mean[3] = {0, 0, 0};
  for(uint8_t i=0; i<100; i++)
  {
    GetAccel();
    mean[0] += (int32_t)AccX;
    mean[1] += (int32_t)AccY;
    mean[2] += (int32_t)AccZ;
  }
  mean[0] /= 100;
  mean[1] /= 100;
  mean[2] = mean[2]/100;
  
  for(uint8_t i=0; i<3; i++)
  {
    AccBias[i] = mean[i]-AccReal[i];
  }

  for(uint8_t i=0; i<3; i++)
  {
    mean[i] = 0;
  }
  for(uint8_t i=0; i<100; i++)
  {
    GetGyro();
    mean[0] += (int32_t)GyrX;
    mean[1] += (int32_t)GyrY;
    mean[2] += (int32_t)GyrZ;
  }
  mean[0] /= 100;
  mean[1] /= 100;
  mean[2] /= 100;
  for(uint8_t i=0; i<3; i++)
  {
    GyrBias[i] = mean[i]-GyrReal[i];
  }
}

void setup() {
  Serial.begin(9600); // シリアル通信のレートを設定
  Wire.begin();         // I2C通信の初期化

  if(LSM9DS1setting())
  {
    Serial.println("LSM9DS1 setting success");
  }
  else
  {
    Serial.println("LSM9DS1 setting failure");
    while(1);
  }
  CalculateBias();

  InitTime = millis();
}

void loop() {
  RunTime = millis()-InitTime;
  GetAccel();
  GetGyro();
  if((millis()-PrintTime)>1/PRINT_RATE*1000)
  {
    Serial.print("AccelX:\t");
    Serial.print(AccX-AccBias[0]);
    Serial.print("\tAccelY:\t");
    Serial.print(AccY-AccBias[1]);
    Serial.print("\tAccelZ:\t");
    Serial.println(AccZ-AccBias[2]);
    Serial.print("GyroX:\t");
    Serial.print(GyrX-GyrBias[0]);
    Serial.print("\tGyroY:\t");
    Serial.print(GyrY-GyrBias[1]);
    Serial.print("\tGyroZ:\t");
    Serial.println(GyrZ-GyrBias[2]);
    PrintTime = millis();
  }
}

プログラムではsetup関数内のCalculateBias()で平均値の算出を行っています.
CalculateBias関数は231行目から268行目で定義してあります.

バイアス値を求める際に使用する想定される真値は,加速度はx軸とy軸は0としています.z軸は1gとしたいのですが,センサーから出力される数値は単位がgではありません.なので,出力される値の単位をgにするか,想定される真値である1gの単位を出力されるデータの単位に合わせる必要があります.ここでは出力されるデータの単位に合わせたいと思います.

LSM9DS1のデータシートを確認すると,12ページ目にセンサーの感度が記載されています.

今回はfull scaleを±8gとした(プログラム8行目)ため0.244mg/LSBとなっています.つまり,

$$1 LSB = 0.244 mg$$

ということになります.したがって,1gは

$$ \begin{eqnarray}1 g &=& 1\times\frac{1000}{0.244} \\ &=& 4098 LSB \end{eqnarray}$$

となります.この時の右辺の値が想定されるz軸の真値ということになります.

loop関数ではバイアス補正をした数値を出力するようにしています.

バイアス補正を行う前が以下のようになります.

見てわかるように,加速度のx軸とy軸が0になっておらず,z軸も1gを表す4098になっていないことが確認できます.ここではLSM9DS1から取得できるすべてのデータを出力していますが,今回バイアス補正をするのは加速度とジャイロのみです.

バイアス補正をした結果が以下のようになります.

上図のようにセンサーから出力されるデータが想定される真値に近づいたことがわかります.

しかし,完全に真値と一致することはありません.センサーから出力されるデータには必ず,ノイズが存在するため少し乱れた値が出力されるからです.

 

まとめ

この記事ではバイアスとは何か,バイアス補正のやり方を解説しました.

記事内のプログラムを利用することで,バイアスを補正できることが確認できました.

しかし,センサーのノイズの影響でデータがぶれるため,バイアス補正を行っても正確な値を出力させることはできません.このようなノイズの影響を低減させるためには,カルマンフィルタ―のようなアルゴリズムを使用する必要があります.

 

続けて読む

バイアス補正ができたので,センサーの値を使って正確な姿勢角を求めることができるようになりました.

加速度センサーから姿勢角を求める方法は以下で解説をしています.

角速度センサーから姿勢角を算出する方法は以下で解説しています.

Twitterでは記事の更新情報や活動の進捗などをつぶやいているので,フォローしていただけると嬉しいです.

それでは最後まで読んでいただきありがとうございました.

コメント

  1. […] センサーとI2C通信を行う加速度・ジャイロ・地磁気センサーのデータを取得するセンサーのバイアス補正方法加速度センサーから姿勢角算出ジャイロセンサーから姿勢角を算出センサーフュージョンをして姿勢角の正確な値を求める […]

タイトルとURLをコピーしました