みなさん,こんにちは.
おかしょです.
ロボットの姿勢角を求めるには加速度センサーを使う方法と角速度センサーを使う方法があります.
しかし,加速度センサーのみを使って姿勢角を求めるのは問題があります.
その問題とはロボットが移動するとき,重力加速度以外の加速度がかかってしまう事です.
これによって,加速度センサーから出力される値は重力加速度とは異なったものとなってしまいます.
加速度センサーから姿勢角を求めるために,加速度センサーで観測される加速度は重力加速度のみであると仮定していました.
そのため,ロボットが移動する場合は姿勢角の算出が困難となってしまいます.
このようなときに使えるのが,ジャイロセンサーです.
ジャイロセンサーはロボットの角速度を各軸周りに測定します.
そのため,ロボットが移動をしてもジャイロセンサーには影響はありません.
さらに,角速度を測定するため出力された値を積分するだけで姿勢角を求めることができます.
この記事では,ジャイロセンサーから姿勢角を求める方法を解説していきます.
この記事を読むと以下のようなことがわかる・できるようになります.
- 角速度を姿勢角に変換する方法
- ジャイロセンサーで姿勢角を求める方法
この記事を読む前に
この記事ではLSM9DS1というセンサーを使用します.
LSM9DS1からデータを取得する方法は以下の記事で解説しているので,そちらを先に読んでおくことをおすすめします.
姿勢角の表し方
この記事では座標軸や回転方向を以下の図に示すように定義します.
ほとんどの方はこれと同じ座標軸を使用すると思いますが,これとは違う座標軸で姿勢角を求めたい場合はご自分で変換してください.
ジャイロセンサーから姿勢角の求め方
冒頭でも述べましたが,ジャイロセンサーから姿勢角を求めるには,出力された値を積分すればいいだけです.
しかし,ロボットが1軸のみの回転運動で済むのであれば簡単ですが,2軸や3軸の回転運動を行う場合はやや複雑な計算が必要となります.
いきなり,3軸の回転運動を考慮すると理解しづらいと思うので,1軸のみの場合から順に解説していきます.
1軸のみ回転
まずはy軸方向のみ回転した場合を考えます.
上の図において\(g_{y}\)はy軸周りの角速度を表しています.
この角速度\(g_{y}\)とこれによって生じる角度\(\theta\)には以下のような関係があります.
$$ \dot{\theta} = g_{y} $$
これの両辺を積分することで角度\(\theta\)を求めることができます.
$$ \theta=\int_{t_{0}}^{t_{f}} g_{y} dt $$
このときの積分の方法は様々あり,積分の仕方によって姿勢角を求められる制度に影響してきます.
積分の方法は,例えば長方形則や台形則,ルンゲクッタなどがあります.
あとで説明する,プログラムでは長方形則で積分を行っています.
2軸以上の回転
次に2つの軸が回転した場合の姿勢角の求め方を解説します.
先程の1軸のみ回転する場合の姿勢角の求め方は非常に簡単でした.
しかし,2つ以上になると難易度が一気に上がります.
と言っても,最初が簡単すぎただけで,2つ以上になってもそこまで難しくはないのでご安心ください.
さて,2つ以上の軸周りに回転すると,なぜ難易度が上がるのかというと,センサーの軸を慣性軸に変換しなければならないからです.
そもそも,ロボットの姿勢角というのは慣性軸を基準にして,その慣性軸からどれだけ角度がついているかを表します.
例えば,ロボットの姿勢が以下のように\(\phi=90^{\circ}\)となっていたとします.
このような姿勢でジャイロセンサーから出力される値というのは回転後の座標軸周りの角速度になります.
この姿勢でジャイロセンサーからの出力が\(g_{y}=30^{\circ}/sec\)であったとします.
ただ積分をするだけなのであれば,1秒後のロボットの姿勢は\(\phi=90^{\circ},\theta=30^{\circ} \)になります.
しかし,ただの積分では正しい姿勢は求められません.
加速度センサーから姿勢角を算出する記事に詳しいことは記載しましたが,回転には順番があります.
本記事では回転は3-2-1系で記載します.
先程の姿勢\(\phi=90^{\circ},\theta=30^{\circ} \)を3-2-1系で記載すると以下のようになります.
先程の姿勢からy軸周りに回転させてもこのような姿勢にはならないはずです.
このことから,ただの積分では姿勢角を求めることはできないということがわかったと思います.
それでは,どのように計算すれば正しい姿勢角を求めることができるのかを解説していきます.
ジャイロセンサーから姿勢角を求めるには,センサーから得られた回転ベクトルを慣性座標系に変換すればできます.
センサーから出力される値というのは,先程も述べましたが,ロボットに固定された座標系で表されています.
その値を基準となる慣性座標系に変換し,それを積分することでロボットの姿勢角を求めることができます.
センサーから出力される角速度を\(g_{x}, g_{y}, g_{z}\)とし,慣性座標系に変換した角速度を\(\dot{\phi}, \dot{\theta}, \dot{\psi}\)とします.
このとき,センサーの値を慣性座標系に変換する式は以下のようになります.
$$ \begin{bmatrix} \dot{\phi}\\ \dot{\theta}\\ \dot{\psi} \end{bmatrix}=\begin{bmatrix} 1&\sin \phi \tan \theta&\cos \phi \tan \theta\\ 0&\cos \phi &-\sin \phi\\ 0&\sin \phi / \cos \theta&\cos \phi / \cos \theta \end{bmatrix} \begin{bmatrix} g_{x}\\ g_{y}\\ g_{z} \end{bmatrix}$$
この式を利用することで,ジャイロセンサーから姿勢角を求めることができます.
さて,この式を使って実際にジャイロセンサーから姿勢角を求めてみます.
姿勢角を実際に求める
使用したもの
姿勢角を求める際に必要となったものは,以下のものになります.
- Arduino Uno
- USBケーブル(書き込み用)
- ブレッドボード
- ジャンパー線
- XBee×2(シリアル通信用)
- XBeeピッチ変換基板(3.3V電圧レギュレータ実装)
- XBee USBインターフェースボード
- USBケーブル(通信用)
- 電池(9V)
- LSM9DS1(9軸センサー)
センサーを動かす際にコードが邪魔になるので,XBeeで無線化を行っています.XBeeがなくても動作の確認はできるので問題はありません.
回路図
今回の動作確認のために使用した回路を以下に示します.
実際に配線するとこのようになります.
ジャイロンサーから姿勢角を求めるプログラム
今回,ジャイロセンサーとして使用したLSM9DS1のデータを取得し,姿勢角に変換するプログラムを以下に示します.
#include <Wire.h>
#include <math.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 40 // PRINT_RATE Hzで出力する
#define ACCELERATION_SENSITYVITY 0.000244 // scale 2g=0.061, 4g=0.122, 8g=0.244, 16g=0.732 mg/LSB
#define MAGNETIC_SENSITYVITY 0.00043 // scale 4=0.14, 8=0.29, 12=0.43, 16=0.58 mgauss/LSB
#define ANGULAR_SENSITYVITY 0.00875 // scale 245=8.75, 500=17.5, 2000=70 mdps/LSB
#define TEMPERATURE_SENSITYVITY 16 // LSB/℃
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 = 3; // Output date rate: 1=10, 2=50, 3=119, 4=238, 5=476, 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 = 7; // 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, sample;
uint8_t PrintRate = 1000/PRINT_RATE;
int16_t AccX, AccY, AccZ;
int16_t GyrX, GyrY, GyrZ;
float Gx, Gy, Gz;
int16_t MagX, MagY, MagZ;
int16_t Temp;
float Te;
double phi = 0;
double theta = 0;
double psi = 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 GetTemp()
{
uint8_t data[6];
if(I2Cread1byte(AG_DEVICE_ADDRESS, STATUS_REG_AG)&4) // データの更新確認
{
if(I2CreadMULTIbyte(AG_DEVICE_ADDRESS, OUT_T, 2, data)==2)
{
Temp = (data[1]<<8)|data[0];
}
}
}
void GetMag()
{
uint8_t data[6];
if(I2Cread1byte(M_DEVICE_ADDRESS, STATUS_REG_M)&8) // データの更新確認
{
if(I2CreadMULTIbyte(M_DEVICE_ADDRESS, OUT_M, 6, data)==6)
{
MagX = (data[1]<<8)|data[0];
MagY = (data[3]<<8)|data[2];
MagZ = (data[5]<<8)|data[4];
}
}
}
void setup() {
Serial.begin(9600); // シリアル通信のレートを設定
Wire.begin(); // I2C通信の初期化
if(LSM9DS1setting())
{
Serial.println("LSM9DS1 setting success");
}
else
{
Serial.println("LSM9DS1 setting failure");
}
InitTime = millis();
sample = 0;
}
void loop() {
RunTime = millis()-InitTime;
GetGyro();
if((millis()-PrintTime)>PrintRate)
{
Gx = GyrX*ANGULAR_SENSITYVITY*PI/180;
Gy = GyrY*ANGULAR_SENSITYVITY*PI/180;
Gz = GyrZ*ANGULAR_SENSITYVITY*PI/180;
phi = phi+(Gx+sin(phi)*tan(theta)*Gy+cos(phi)*tan(theta)*Gz)*(millis()-PrintTime)/1000;
theta = theta+(cos(phi)*Gy-sin(phi)*Gz)*(millis()-PrintTime)/1000;
psi = psi+(sin(phi)/cos(theta)*Gy+cos(phi)/cos(theta)*Gz)*(millis()-PrintTime)/1000;
Serial.print("sample:\t");
Serial.print(sample);
Serial.print("\tphi:\t,");
Serial.print((int)(phi*180/PI));
Serial.print("\ttheta:\t,");
Serial.print((int)(theta*180/PI));
Serial.print("\tpsi:\t");
Serial.println((int)(psi*180/PI));
PrintTime = millis();
sample++;
}
}
これの動作結果はこちらで紹介しています.
まとめ
この記事ではジャイロセンサーを使用して,姿勢角を算出する方法を解説しました.
上記のプログラムを実際に使用した方はわかるかと思いますが,時間が経つにつれて実際の姿勢角とは異なる値が出力されるようになったかと思います.
これは積分によって生じるドリフトが原因です.
加速度センサーにもデメリットはありましたが,このジャイロセンサーにもこのようなデメリットがあります.
続けて読む
ジャイロセンサーで姿勢角を算出するデメリットは加速度センサーとジャイロセンサーの2つのセンサーを同時に使用することで,より正確な姿勢角の算出が可能となります.
2つのセンサーを利用して姿勢角を求める方法をセンサーフュージョンと言います.
以下の記事ではそのセンサーフュージョンについて解説しているので,参考にしてください.
Twitterでは記事の更新情報や活動の進捗などをつぶやいているので気が向いたらフォローしてください.
それでは最後まで読んでいただきありがとうございました.
コメント
[…] また,別の記事ではジャイロセンサーを使って姿勢角を求める方法を解説しました. […]
[…] 以前,このブログでは加速度センサーやジャイロセンサーを使って姿勢角を求める方法を解説しました.加速度センサーから姿勢角を算出ジャイロセンサーから姿勢角を算出センサーフュージョンをして正確な姿勢角を算出 […]