今回のレシピは、空中マウスの作り方、です。用意するものは以下の通り。
早速設計。と、その前に、「空中マウス」について調べておきましょう。
空中マウスを検索する(別ウインドウが開きます)
2.4GHz帯やBluetoothなどでワイヤレスになっていたり、マウス自体はトラックボールになっているものや、ジャイロスコープセンサ(以下単に、ジャイロセンサ)で回転を検知してマウスを動かす物など、色々な種類があるみたいです。
このページでは、加速度センサを使って、傾きによって、マウスポインタを操作する空中マウスを作ってみます。
そうか、ジャイロセンサを使うのね……。「先に調べてから作りなよ」って、ことですけど、まあ、色々と試してみて、より操作し易い様に修正して行けば良いことなので、今回は、ジャイロセンサではなくて、加速度センサを使います。
MPU9250には、MEMS(Micro Electro Mechanical Systems)センサが搭載されています。原理については以下から検索してみてください。
加速度センサの原理を検索(別ウインドウが開きます)
簡単にまとめておきます:
静的加速度(重力加速度)の検出、これが厄介。センサを机の上に静かに置いた状態でも、重力加速度gは検出されてしまいます。加速度a(=g)は検出されるのに、センサ自体は机の上で静止しており、動きません。
高校物理を思い出してみましょう。F=m×aだから、加速度a>0なら、力F>0で、物体mに力が働き続けていることになります。「力Fが働けば、物体mは、その方向に動く」はずなんだけど、今、センサは机の上に静止しています。
高校物理では、重力によって鉛直下方に力Fが働くのに対して、物体mが机を下方に押すことで、その反作用として、机が物体を抗力Nで押し返します。この力がバランスする(F=N)ので、物体mは机の上に静止しつづけます。
つまり、重力加速度gは検出されるけど、実際、「働いた力Fは抗力Nで打ち消されている」と言う状態(言い換えると、抗力Nに相当する加速度)を検出できないわけです。よって、静止しているにもかかわらず、重力加速度だけは検出され続けるという矛盾した状態を許容しなければならないことになります。
加速度センサを使うときには、重力加速度をどうやって扱うか?が肝になる様に思います。
※原理的に考えて、重力加速度を無いものとして計測することは、逆に出来なかったのだろうけど、実際には打ち消されて0になってしまう加速度が検出されてしまうことで、とても、扱い難くなっています。
なお、後ほどの予備実験で明らかとなりますが、MPU9250を使うと重力加速度は上向きに検出されます、もう、わけわかりません(1−−;)。
では、実際、どう測定されるのか?MPU9250を使って予備実験を実施してみました。
MPU9250のデータシートを検索(別ウインドウが開きます)
MPU9250モジュールは、「#9-10 電子コンパスの作成」でも使ったのですが、あの時はMPU9250に内蔵されたAK8963(磁気センサ)だけを使用しました。今回は予備実験として、MPU9250の加速度センサ、ジャイロセンサ、磁気センサにアクセスできるようにプログラムしておきました。
HIDのサポートがある、Arduino Microを使ってプログラムを組み始めた矢先、「Microが壊れたぁああぁああ……」。
USBコネクタがポロリと取れてしまいました(1;_;)。HIDライブラリが使える貴重なArduinoが……。さすがに、修復不能です。
止むを得ません。今回は、PCに接続して「マウス」にならないと意味が無いので、同じくHIDのサポート(ライブラリ)がある、STM32(以下図)を使うことにしました。
「スケッチメニュー ⇒ ライブラリをインクルード」の中に、「USBComposite for STM32F1」があるか?確かめてください。もし存在していなかった場合は、「スケッチメニュー ⇒ ライブラリをインクルード ⇒ ライブラリを管理」を選択後、「STM32+HID」で検索して、以下のライブラリをインストールしてください。
今回使用するインクルードファイルは「USBComposite.h」です。
回路図は以下となります。
既に、空中マウスの回路図となっています。MPU9250をArduino UNOで使った時はL/Sを挟む必要がありましたが、今回はSTM32を使用するので、VDD=3.3Vで直結できます。
MPU9250モジュールのI2Cスレーブアドレス(の最下位ビット)は、AD0端子で決定されます。上図ではAD0=LOWとしてあるので、スレーブアドレスは0x68です。AD0=HIGHに固定した場合は0x69となります。
予備実験用のプログラムをリンクしておきます。
実験用プログラム:air_mouse.ino
STM32でコンパイル&書き込むと、シリアルモニタから、以下のコマンドを受け付けるようになっています。
コマンド仕様:
シリアルモニタで、加速度センサのデータを読み出してみました。
実験サンプル:【 】内はシリアルモニタからの送信データ
【acl,10】
OK+ acl.mav=(0.063, -0.009, 0.997)
OK: show acl 10 times
OUT: acl=(0.062, -0.006, 0.992)
OUT: acl=(0.058, -0.012, 1.000)
OUT: acl=(0.066, -0.010, 0.992)
OUT: acl=(0.063, -0.010, 0.989)
OUT: acl=(0.059, -0.010, 0.998)
OUT: acl=(0.065, -0.010, 0.992)
OUT: acl=(0.062, -0.007, 0.989)
OUT: acl=(0.061, -0.010, 0.991)
OUT: acl=(0.066, -0.007, 0.986)
OUT: acl=(0.064, -0.012, 1.005)
表示されたのは、コマンド実行時点の移動平均と、その後、連続して測定した10回分の加速度データです。移動平均は、プログラム先頭付近のacl_RING(=16)で制御しています。
※プログラム(air_mouse.ino)は、空中マウスとしても使えるコードになっています。後で、空中マウスとして使う時、加速度センサの移動平均値を使っていますので、acl_RINGの設定値によって挙動が変わることに留意してください。
MPU9250を±2gレンジに設定しましたので、数値は×g[m/s2]です。紛らわしいのだけど、ここで使うgは、グラム(g)ではなくて、重力加速度g≒9.8m/s2となります。
MPU9250から読み出した符号付16ビットデータのフルスケールが2gとなるので、換算式は以下の通りです。
換算式:<16ビット読み出し値>×2/32,768×g[m/s2]
上の式にg≒9.8m/s2を代入すれば、加速度の絶対値[m/s2]に換算できるし、相対値で十分な場合はg=1として計算すると重力加速度と同じ大きさの加速度が"1.0"と換算されます。プログラム(air_mouse.ino)は、後者を選択しています。
※重力加速度gで正規化した数値は、遊園地のジェットコースタなどで、体に掛かる衝撃や負荷の大きさを表す時に使われる、"2G"とか"4G"とか言う時の値と同じものです。
上の実験サンプルで表示した数値を見ると、z軸正方向に≒1の値が読めていて、これが重力加速度に相当します。本来、重力加速度は下向きに生じるはずだけど、MPU9250では上向き(正の値)に出力されます。
【acl,-1】コマンドを実行すると加速度を表示し続けるので、センサ(の載ったブレッドボード)を動かして、横方向(x,y軸方向)の加速度を加え、測定値の変化を見てみてください。結構、値はばらつくし、かなり強く(速く)動かさないと、横方向の加速度値は大きな値で表示されないことが判りました。
今回、空中マウスには使用しませんでしたが、予備実験としてジャイロセンサの値も読み出してみました。
実験サンプル:【 】内はシリアルモニタからの送信データ
【gyr,10】
OK+ gyr.mav=(-2.604, 0.423, 0.254)
OK: show gyr 10 times
OUT: gyr=(-2.579, 0.404, 0.191)
OUT: gyr=(-2.678, 0.443, 0.465)
OUT: gyr=(-2.594, 0.366, 0.435)
OUT: gyr=(-2.403, 0.313, 0.282)
OUT: gyr=(-2.502, 0.389, 0.229)
OUT: gyr=(-2.769, 0.534, 0.397)
OUT: gyr=(-2.602, 0.298, 0.275)
OUT: gyr=(-2.838, 0.320, 0.244)
OUT: gyr=(-2.686, 0.465, 0.191)
OUT: gyr=(-2.563, 0.359, 0.275)
表示されたのは、コマンド実行時点の移動平均と、その後、連続して測定した10回分のジャイロセンサデータです。移動平均は、プログラム先頭付近のgyr_RING(=16)で制御しています。
数値の単位は[°/s]です。MPU9250を±250°/sレンジに設定してあるので、読み出した符号付16ビットデータからの換算式は以下となります。
換算式:<16ビット読み出し値>×250/32,768[°/s]
※「#21-7 空中マウスの別解」にもう少し詳細な説明を追加しました。
上の実験サンプルでは、机の上に静止させた状態で実行しました。つまり、静止していてどの方向にも回転していません。本来なら、全て0というデータが読み出されるはずですが、オフセットが付いているみたいです。
実際に使うときにはオフセットをキャンセルしておかないと、ずっと一定の方向に回転し続けているように検知されてしまいます。おそらく、オフセット量はチップ(個体)に依存するので、個体ごとにオフセット値は異なります。
プログラムでデータを読み出した後で、プログラム処理によってオフセットをキャンセルするか、あるいは、MPU9250の内部レジスタ(XG_OFFSET_H〜ZG_OFFSET_L)に設定する事でキャンセルすることができます。
※もしかして、加速度センサの方にもオフセットが付いているのかもしれませんね?加速度センサの方は、オフセット値を知ることは非常に難しいのですが、測定出来たなら、MPU9250の内部レジスタ(XA_OFFSET_H〜ZA_OFFSET_L)に設定する事でキャンセルすることができます。
こちらも、何かに応用する目的で予備実験するのであれば、【gyr,-1】コマンドを実行した状態で、くるくる回してみて、どの軸にどのような数値が出力されるか、確かめてみてください。
磁気センサの制御に関しては、「#9-10 電子コンパスの作成」で解説済みですので、そちらを参照してみてください。「#9-10」では、Arduino UNOをMPU9250の外部I2Cバス(ECL,EDA)に接続して、直接AK8963(磁気センサチップ)にアクセスしたのですが、今回はプライマリI2Cバス(SCL,SDA)の方に接続して、MPU9250越しにAK8963へアクセスしています。
このため、MPU9250の内部レジスタINT_PIN_CFGのBYPASS_ENビットに1を立てて、プライマリI2Cバスを外部I2Cバスにバイパスする設定にして使っています。
MPU9250にAK8963を制御させる方法も用意されていたのですが、設定が面倒な上に、あんまり便利だと思えなかったので、不採用としました。結局これ(バイパスして直接アクセス)が一番簡単で使い易いと思います
実験サンプル:【 】内はシリアルモニタからの送信データ
【mag,10】
OK+ mag.mav=(0.267, -0.710, 0.650)
OK: show mag 10 times
OUT: mag=(0.212, -0.734, 0.645)
OUT: mag=(0.291, -0.695, 0.657)
OUT: mag=(0.273, -0.708, 0.651)
OUT: mag=(0.327, -0.728, 0.603)
OUT: mag=(0.247, -0.714, 0.655)
OUT: mag=(0.270, -0.699, 0.662)
OUT: mag=(0.257, -0.722, 0.643)
OUT: mag=(0.283, -0.737, 0.614)
OUT: mag=(0.248, -0.709, 0.660)
OUT: mag=(0.261, -0.757, 0.599)
表示した数値は、地磁気の方向ベクタです。絶対的な磁束密度値[μT]ではありません。「#9-10 電子コンパスの作成」で解説した通り、磁束密度[μT]に換算することも可能です。必要な場合は換算式に倣って、プログラム(air_mouse.ino)を修正してください。
空中マウスの機能を設計して行きます。
今回は、加速度センサを使います。センサを動かした時の加速度を使って、マウスを動かすという方法も考えられますが、加速度の場合、動かし始めた時に発生した正方向の加速度と、動きを止める時に発生する負方向(逆向き)の加速度がセットで観測されるため、制御は複雑で難しくなるでしょう。
そこで、常に観測される鉛直上(z軸正)方向の静的加速度である、重力加速度の向き(ベクタ)を使って制御します。センサを机の上に水平に置いた場合、z軸と重力加速度gの方向は一致して観測されます。一方、センサを傾けた場合、下図の様にz軸と重力加速度gの方向は、ずれて観測されます。
適切な座標軸x,yを設定して、gのx-y平面上の座標値を計算し、この値を使ってマウスカーソルを動かすことにします。
加速度センサなので、傾きだけではなく、センサを動かした時の加速度も検出されてしまいますが、まあ、それも含めてマウスのカーソル移動に反映されると考えれば、このアルゴリズムで十分成立するはずです(以下、該当箇所のコード抜粋)。
int imx;
int imy;
float kk;
const float ux=0.; // u-axis vector
const float uy=-1.;
const float uz=0.;
const float vx=1.; // v-axis vector
const float vy=0.;
const float vz=0.;
const float wx=0.; // w-axis vector
const float wy=0.;
const float wz=1.;
// accelerometer
ax=sum_ax*(1./acl_RING);
ay=sum_ay*(1./acl_RING);
az=sum_az*(1./acl_RING);
kk=sqrt (sq (ax) + sq (ay) + sq (az));
if (kk!=0.)
{
kk=1./kk;
ax*=kk;
ay*=kk;
az*=kk;
}
// mouse move
float cth; // cos(theta)
cth=ax*wx + ay*wy + az*wz;
imx=256.*(ax*ux + ay*uy + az*uz)*(1. - sq (cth));
imy=256.*(ax*vx + ay*vy + az*vz)*(1. - sq (cth));
if (cth>=cos (2*M_PI*5/360))
{
// neutral position
imx=0;
imy=0;
}
imx=(imx<-128)? -128: (imx>127)? 127: imx;
imy=(imy<-128)? -128: (imy>127)? 127: imy;
Mouse.move (imx,imy);
筆者の実装では、以下図の様な位置関係で配置することを前提としました。
上図のx,y,z軸は、MPU9250モジュールのシルクに印刷された座標軸の方向です。上に示したコードの座標軸(u,v,w軸)は、以下の様に対応しています。
u軸 ⇔ −y軸
v軸 ⇔ x軸
w軸 ⇔ z軸
両手でブレッドボードをつかみ、左手親指でスイッチ#1、右手親指でスイッチ#2(と#0)を押すことを想定した配置となります。この配置で、センサ(ブレッドボード)を右に傾けるとカーソルが右に、左に傾けると左に、手前に傾けると上に、向こう側へ傾けると下に、動く様にプログラムしてあります。
別な方向で取り付けた場合は、プログラムの修正が必要となります。
マウスボタンは2ボタン(スイッチ#1と#2)にしました。タクトスイッチは3つ目(#0)を設置してあって、このスイッチでマウスモードのON/OFFができるようにしておきます。
特にデバッグ中、プログラムミスでマウスカーソルの制御が利かなくなると、PCの操作ができず、回復不能になってしまう危険性があるので、PCを介さずにマウスモードのON/OFFができるようにしておくと安心です。それに、プレゼンとかに使う時、マウス操作をする時以外はマウスモードOFFにしておくと、カーソルがふらふら動かないとか、U/Iとしての利点もあります。
PCにUSB接続した時の空中マウスの設置状況(傾き)によって、起動後いきなりカーソルが動いてしまうのは嬉しくないため、起動直後のモードはマウスモードOFFとします。
完成したプログラムをリンクしておきます。
完成版プログラム:air_mouse.ino
と、言うか、予備実験のプログラムと同じものです。実はこのままではマウスにはなりませんので、プログラム先頭付近の「#define USE_SERIAL」をコメントした上で、STM32へコンパイル&書き込んでください。
STM32へHIDを使ったプログラムをコンパイル&書き込みする時の注意事項:
それでは、空中マウスとして使ってみましょう。以下、完成した空中マウスの外観です。前節で示した配置図に倣って、830穴の大きめなブレッドボード上に構築しました。
ブレッドボード上に配置したスイッチ#0を押すと、空中マウスモードになってSTM32のオンボードLEDが光ります。もう一度押すと、空中マウスモードがOFFになってオンボードLEDが消灯します。以降、押す度に状態がトグルします。
空中マウスモードで、ブレッドボード(に搭載したセンサ)を傾けると、その方向にマウスカーソルが動きます。水平に戻すとカーソル移動が停止します。傾きを検知するので、ブレッドボードを水平位置から傾けてください。ブレッドボードを上下、左右に振り回したり、動かしたりしてもダメです。そりゃ、振り回せばちょっとは傾くので動くと思いますけど……、静かに、水平位置から少し傾けるだけでカーソルが動くはずです。
タクトスイッチの#1が右クリック、#2が左クリックに割り当ててあります。
動画とかには落とし難かったので、後は皆様の目で確かめてくださいね。
自分で作っておいて言うのもアレな感じですが、調整不足、と言うか、操作性は今一つ、な感じでした(1^^;)っ。一応、空中マウスとしての機能は確認できた、って感じです。
市販の空中マウスみたいに、ジャイロセンサを使って作ってみるのも、改定案の一つでしょうね。今後の課題としておきます。プログラム(air_mouse.ino)中では、ジャイロセンサの移動平均も取っているし、モード切り換えスイッチと、その機構も作り込んであるので、ジャイロセンサのデータからマウスの移動量に換算する部分を作り込んだら、対応可能です。よろしければ、皆様の手で改変してお使いください。
時々、MPU9250とのI2C通信がハングすることがありましたので、レポートしておきます。
イヤ、普通の通信は全く問題ありません。loop()関数内で毎回センサ値を読み出していますが、全く問題なくアクセスできます。ダメになるのは、Arduino IDEからプログラムを書き込む時にSTM32をリセットした後。書き込みが終了して、プログラム起動後の最初のアクセスでハングしてしまいます。
どうやら、STM32からMPU9250へアクセスしている最中に、STM32をリセットしてしまった場合等、変な状態でMPU9250との通信が中断してしまうと、この状態に陥る様です。
今回のプログラムでは、変動するセンサ測定値の移動平均を取るために、loop()関数内で毎回MPU9250の内部レジスタにアクセスしているため、リセットをかけた時、I2C通信中に中断してしまう確率(よって起動後にハングする確率)が高くなっています。
ハングした時のI2C端子状態を確認したところ、SCL=HIGH,SDA=LOWでした。明らかに、SDA=LOWに駆動しているのはMPU9250で、これがハングする原因だと推測できます。MPU9250がSDA端子をLOW駆動しているので、ACKを返そうとしたのか、何らかのデータを返そうとしたのか、いずれにせよ、マスタからのSCLクロックを待っているのだと思います。
ところが、マスタ側はそんなことはすっかり忘れて(リセットされちゃったからね……)、I2Cトランザクションを発行しようとするのだけど、SDAがLOWに張り付いているので、STARTコンディションを発行できず、ハングしてしまうのではないでしょうか?
そこで、起動後のsetup()関数内で、この状態を検知した時に手動でクロック(SCL)を生成して、SDA=LOWの状態を解除してから、Wire.begin()するコードを追加しました(以下抜粋)。
// i2c status recovery
pinMode (pin_SCL, INPUT_PULLUP);
pinMode (pin_SDA, INPUT_PULLUP);
delay (1);
while (digitalRead (pin_SDA)==LOW)
{
digitalWrite (pin_SCL, LOW);
pinMode (pin_SCL, OUTPUT);
delayMicroseconds (20);
pinMode (pin_SCL, INPUT_PULLUP);
delay (1);
}
// i2c setting
Wire.begin ();
SDAの状態なんかお構いなしに、トランザクションを発行するI2Cユニットだったり、ライブラリだったりすれば、読み出しデータが不正になったりエラーが発生したりしたとしても、ハングはしないと思われます。すなわち、AVR系のライブラリで同様にハングするとは限りません(筆者手元では未確認です)。
ジャイロセンサを使った空中マウスにも挑戦してみたいけど……、あんまり気が進まないな〜。HIDを起動してしまうとシリアルモニタでデバッグできないし、書き込みの度にリセットボタン押さないとならないし、デバッグがやり難くて面倒(1x_x)。まあ、気が向いたらやってみるかも?……それより、Micro壊れたの……ショック😭。
※ジャイロセンサを使って空中マウスの別解を作って見ました。「#21-7 空中マウスの別解」をご覧ください。