ギタコンの自作 試作4

試作4の内容

ジャイロセンサーを追加して、wailing操作に対応します。

以下お願い: ジャイロセンサーをいくつか当たってみたのですが、はんだ付け無しで利用できるものはGroveモジュールしか見つけられませんでした。そのため、少々配線が面倒です。

もしも、はんだ付け不要のジャイロセンサー(加速度センサーじゃないです)をご存知の方がいらっしゃいましたら、教えてください。 例えば、秋月電子さんのAE-BMX055がはんだ付け不要で使えそうに見えますが、実は電源設定をジャンパーのはんだ付けで行う必要があるため、はんだ付け不要とはなりませんでした。

→ 2019/8/22追記: GroveじゃなくてQwiicでつなぐものだと、 Groveを使った部品よりお安くできるようです。 Groveだとモジュール(1700円)とケーブル(300円、5本入り)で2000円くらいのところ、 Qwiicだとモジュール (1111円) とケーブル (180円、1本入り) の組み合わせで1300円くらいで済みます。 ケーブルが5本セットか1本かの違いはありますが、今回の施策では1本しか使わないので問題なしです。 後日こちらを使った説明に全部置き換えた方がいいかな。

更に、ジャイロセンサーのチップをMPU6050以外にしてもよいなら、 STEMMA QT/Qwiic互換 LSM6DS33搭載 6自由度IMUを使うことで更に100円お安くなります。

・・・まあはんだ付けしてよいなら300円で済むのですけどね・・・。

試作4で使うもの一覧

試作1~3で使ったものに加えて、以下を使います。

追加費用は、合計で2500円+発送費用くらい。(別の方法で配線を整理するのであれば、線材の追加は不要)

名称値段通販コード
ジャイロセンサーモジュール (MPU-6050) x1 IMG_3756_.jpg 1700円程度 千石電商:Seeed Studio 101020080 Grove - IMU 9DOF v2.0
Groveのコネクタをバラのジャンパーピンに変換するケーブル x1 IMG_3755_.jpg 330円程度 千石電商: Seeed Studio 110990210 Grove 4ピンコネクタ - ジャンパーピン変換ケーブル(5本入り)

Wailingに対応させる

配線はこんな感じです。今回使用するジャイロモジュール (MPU-6050) は、I2Cという通信規格でArduino等他の機器と接続します。(I2CはInter-Integrated Circuitの略で、正式にはアイスクエアシーと読みますが、アイツーシーでも通じます。)
Arduino Microでは、D2ピンとD3ピンをI2Cでの通信に使用し、D2ピンをデータ送受信(SDA)、D3ピンがデータクロック(SCL)に用います。ですから、Arduino MicroのD2ピンとMPU-6050のSDAを、Arduino MIcroのD3ピンとMPU-6050のSCLを、それぞれつなぎます。また、MPU-6050のVCCとGNDは、それぞれArduino Microの5VとGNDにつなぎます。(このモジュールのVCCは、5Vと3.3Vのどちらをつないでもよいため、接続が簡単な5Vを使っています。)

スケッチ修正

この辺りを参考にして、スケッチを作成しました。

ちょっと長くなりますが、まずはスケッチ全体を引用します。 (スケッチの詳細に興味がない場合は、このページの最後にzipでまとめたものを置いておくので、そこまでは読み飛ばしていただいて結構です)

  1. // Simple example application that shows how to read four Arduino
  2. // digital pins and map them to the USB Joystick library.
  3. //
  4. // Ground digital pins 9, 10, 11, and 12 to press the joystick
  5. // buttons 0, 1, 2, and 3.
  6. //
  7. // NOTE: This sketch file is for use with Arduino Leonardo and
  8. // Arduino Micro only.
  9. //
  10. // by Matthew Heironimus
  11. // 2015-11-20
  12. //--------------------------------------------------------------------
  13. #define DEBUG // シリアルへのデバッグ情報出力用
  14. #include <Joystick.h>
  15. const int BUTTONS = 6; // RGBYP + Wailing (analogのPickは別)
  16. const int PIN_STICK = A0; // Pick用のジョイスティックを接続するピン
  17. const int BUTTON_WAILING = BUTTONS-1;
  18. // BUTTONの最後1つを仮想的なWailingボタンにする
  19. // 物理ボタンは無いが、ボタンとしては管理する
  20. Joystick_ Joystick(
  21. JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD,
  22. BUTTONS, 0, // Buttons, Hat Switch Count
  23. true, false, false, // X,Y,Z Axis
  24. false, false, false, // Rx, Ry, Rz Axis
  25. false, false, // rudder, throttle
  26. false, false, false // accelerator, break, steering
  27. );
  28. const int TH_WAILING = 400; // WAILINGと見なす角速度の閾値
  29. // MPU-6050 Accelerometer + Gyro
  30. #include <Wire.h>
  31. #define MPU6050_SMPLRT_DIV 0x19 // R/W
  32. #define MPU6050_CONFIG 0x1A // R/W
  33. #define MPU6050_GYRO_CONFIG 0x1B // R/W
  34. #define MPU6050_GYRO_XOUT_H 0x43 // R
  35. #define MPU6050_GYRO_YOUT_H 0x45 // R
  36. #define MPU6050_GYRO_ZOUT_H 0x47 // R
  37. #define MPU6050_WHO_AM_I 0x75 // R
  38. #define MPU6050_PWR_MGMT_1 0x6B // R/W
  39. #define MPU6050_I2C_ADDRESS 0x68
  40. typedef union accel_t_gyro_union{
  41. struct{
  42. uint8_t z_gyro_h;
  43. uint8_t z_gyro_l;
  44. }
  45. reg;
  46. struct{
  47. int16_t z_gyro;
  48. }
  49. value;
  50. };
  51. unsigned long last_t;
  52. void setup() {
  53. // Initialize Button Pins
  54. // Wailingの実ボタンはないのでBUTTONS-1すること
  55. for (int i = 12; i > 12-(BUTTONS-1); i--)
  56. {
  57. pinMode(i, INPUT_PULLUP);
  58. }
  59. // Initialize Joystick Library (AutoSendState=OFF)
  60. Joystick.begin(false);
  61. //Serial通信初期化
  62. Serial.begin(9600);
  63. // loop時間基準
  64. last_t = millis();
  65. // MPU-6050初期化
  66. Wire.begin();
  67. int error;
  68. uint8_t c;
  69. Serial.print("InvenSense MPU-6050");
  70. Serial.print("June 2012");
  71. error = MPU6050_read (MPU6050_WHO_AM_I, &c, 1);
  72. Serial.print("WHO_AM_I : ");
  73. Serial.print(c,HEX);
  74. Serial.print(", error = ");
  75. Serial.println(error,DEC);
  76. error = MPU6050_read (MPU6050_PWR_MGMT_1, &c, 1);
  77. Serial.print("PWR_MGMT_1 : ");
  78. Serial.print(c,HEX);
  79. Serial.print(", error = ");
  80. Serial.println(error,DEC);
  81. MPU6050_write_reg (MPU6050_PWR_MGMT_1, 0);
  82. // サンプルレートを最大化
  83. MPU6050_write_reg (MPU6050_SMPLRT_DIV, 0);
  84. // FSYNC input disabled, Gyrometer bandwidth=256Hz
  85. MPU6050_write_reg (MPU6050_CONFIG, 0);
  86. // FS_SELの設定
  87. error = MPU6050_read (MPU6050_GYRO_CONFIG, &c, 1);
  88. c |= 0x18; // FS_SEL 3
  89. MPU6050_write_reg (MPU6050_GYRO_CONFIG, c);
  90. }
  91. // Constant that maps the phyical pin to the joystick button.
  92. // Wailingは実ボタンではないので、ピンマップには含めない
  93. const int pinToButtonMap = 13 - (BUTTONS-1);
  94. // Last state of the button
  95. // 6個目のボタンは、Wailingボタンとして扱う
  96. int lastButtonState[BUTTONS] = {0,0,0,0,0,0};
  97. int lastAnalogInput[2] = {0,0};
  98. unsigned long lastButtonChangedTime[BUTTONS] = {0,0,0,0,0,0};
  99. float last_gyro_z = 0;
  100. void loop() {
  101. bool buttonStateChanged = false;
  102. unsigned long t = millis();
  103. // 1ms間隔でloop内処理が実行されるようにする
  104. // (前回の処理から1msに満たない時間しか経っていない場合は
  105. // 門前払いする)
  106. if (last_t >= t)
  107. {
  108. // 門前払いの前に、mills()のオーバーフロー対策もしてしまう
  109. if (last_t > (unsigned long)0xFFFF0000 &&
  110. t < (unsigned long)0x00001000)
  111. {
  112. for (int index = 0; index < BUTTONS; index++)
  113. {
  114. // 最終ボタン状態更新時刻を繰り上げて、正負が
  115. // 破綻しないようにする(この程度の対策で問題ないはず)
  116. lastButtonChangedTime[index] = 0;
  117. }
  118. }
  119. return;
  120. }
  121. else
  122. {
  123. last_t = t;
  124. }
  125. // Read digital pin values
  126. for (int index = 0; index < BUTTONS-1; index++)
  127. {
  128. int currentButtonState = !digitalRead(index + pinToButtonMap);
  129. if (currentButtonState != lastButtonState[index])
  130. {
  131. if (t > lastButtonChangedTime[index] + 10)
  132. {
  133. Joystick.setButton(index, currentButtonState);
  134. buttonStateChanged = true;
  135. lastButtonState[index] = currentButtonState;
  136. lastButtonChangedTime[index] = t;
  137. #ifdef DEBUG
  138. Serial.print("*");
  139. #endif
  140. }
  141. #ifdef DEBUG
  142. Serial.print(t);
  143. Serial.print(":");
  144. Serial.print(index);
  145. Serial.print("=");
  146. Serial.print(currentButtonState);
  147. Serial.print("/");
  148. Serial.print(lastButtonState[index]);
  149. Serial.println("");
  150. #endif
  151. }
  152. }
  153. // Read analog stick values
  154. {
  155. int currentAngularVelocity = analogRead(PIN_STICK);
  156. // ±5以下の入力ブレは無視する (LPF)
  157. if (abs(currentAngularVelocity - lastAnalogInput[0]) > 5)
  158. {
  159. Joystick.setXAxis(currentAngularVelocity);
  160. buttonStateChanged = true;
  161. lastAnalogInput[0] = currentAngularVelocity;
  162. #ifdef DEBUG
  163. Serial.print(t);
  164. Serial.print(":");
  165. Serial.print("A0");
  166. Serial.print("=");
  167. Serial.print(currentAngularVelocity);
  168. Serial.println("");
  169. #endif
  170. }
  171. }
  172. // ジャイロセンサー対応(MPU-6050)
  173. // 参考: http://cranberrytree.blogspot.com/2014/06/gy-521mpu-6050.html
  174. // (MPU6050へのアクセス方法の参考として。実際にはジャイロ情報しか使わなかったですが)
  175. // https://www.robotshop.com/jp/ja/mpu6050-6-dof-gyro-accelerometer-imu.html
  176. // から、ZIPでダウンロードできるPDFマニュアル。その中にレジストリマップあり
  177. // 追加のモード設定の参考にした
  178. //
  179. // また、I2C通信の高速化(100kHz -> 400kHz)のため、
  180. // C:\Program Files (x86)\Arduino\hardware\arduino\avr\
  181. // libraries\wire\src\utility\twi.h の最初にある
  182. // #define TWI_FREQ 100000L
  183. // の100000Lを400000Lに変更しておくこと。
  184. // 参考: https://sites.google.com/site/yamagajin/home/mpu6050
  185. int error;
  186. accel_t_gyro_union accel_t_gyro;
  187. //error = MPU6050_read (MPU6050_GYRO_XOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // X軸の回転を検出する場合
  188. //error = MPU6050_read (MPU6050_GYRO_YOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Y軸の回転を検出する場合
  189. error = MPU6050_read (MPU6050_GYRO_ZOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Z軸の回転を検出する場合
  190. uint8_t swap;
  191. #define SWAP(x,y) swap = x; x = y; y = swap
  192. SWAP (accel_t_gyro.reg.z_gyro_h, accel_t_gyro.reg.z_gyro_l);
  193. //FS_SEL_3 16.4 LSB / (°/s)
  194. float gyro_z = accel_t_gyro.value.z_gyro / 16.4;
  195. if (gyro_z < -TH_WAILING) // 時計回りをwailingとして検出する場合
  196. //if (gyro_z > TH_WAILING) // 反時計回りをwailingとして検出する場合
  197. {
  198. #ifdef DEBUG
  199. Serial.print(t);
  200. Serial.print("\t");
  201. Serial.print("[");
  202. Serial.print(gyro_z, 2);
  203. Serial.print("]");
  204. Serial.println("");
  205. #endif
  206. if (!lastButtonState[BUTTON_WAILING])
  207. {
  208. Joystick.setButton(BUTTON_WAILING, true);
  209. lastButtonState[BUTTON_WAILING] = true;
  210. lastButtonChangedTime[BUTTON_WAILING] = t;
  211. buttonStateChanged = true;
  212. #ifdef DEBUG
  213. Serial.print(t);
  214. Serial.println(":WAILING ON");
  215. #endif
  216. }
  217. }
  218. // WailingボタンをONにした後200ms経過したら、自動でオフにする
  219. if (lastButtonState[BUTTON_WAILING] &&
  220. t > lastButtonChangedTime[BUTTON_WAILING] + 200)
  221. {
  222. Joystick.setButton(BUTTON_WAILING, false);
  223. lastButtonState[BUTTON_WAILING] = false;
  224. lastButtonChangedTime[BUTTON_WAILING] = t;
  225. buttonStateChanged = true;
  226. #ifdef DEBUG
  227. Serial.print(t);
  228. Serial.println(":WAILING OFF");
  229. #endif
  230. }
  231. // ボタンの状態変化があった時にだけ、PCにHID入力を通知する
  232. // 同時に、ボタン状態変化の最終時刻を更新する
  233. if (buttonStateChanged)
  234. {
  235. Joystick.sendState();
  236. }
  237. //delay(1); // loop頭のtとlast_tの比較で代用
  238. }
  239. // MPU6050_read
  240. int MPU6050_read(int start, uint8_t *buffer, int size){
  241. int i, n, error;
  242. Wire.beginTransmission(MPU6050_I2C_ADDRESS);
  243. n = Wire.write(start);
  244. if (n != 1)
  245. return (-10);
  246. n = Wire.endTransmission(false); // hold the I2C-bus
  247. if (n != 0)
  248. return (n);
  249. // Third parameter is true: relase I2C-bus after data is read.
  250. Wire.requestFrom(MPU6050_I2C_ADDRESS, size, true);
  251. i = 0;
  252. while(Wire.available() && i<size){
  253. buffer[i++]=Wire.read();
  254. }
  255. if ( i != size)
  256. return (-11);
  257. return (0); // return : no error
  258. }
  259. // MPU6050_write
  260. int MPU6050_write(int start, const uint8_t *pData, int size){
  261. int n, error;
  262. Wire.beginTransmission(MPU6050_I2C_ADDRESS);
  263. n = Wire.write(start); // write the start address
  264. if (n != 1)
  265. return (-20);
  266. n = Wire.write(pData, size); // write data bytes
  267. if (n != size)
  268. return (-21);
  269. error = Wire.endTransmission(true); // release the I2C-bus
  270. if (error != 0)
  271. return (error);
  272. return (0); // return : no error
  273. }
  274. // MPU6050_write_reg
  275. int MPU6050_write_reg(int reg, uint8_t data){
  276. int error;
  277. error = MPU6050_write(reg, &data, 1);
  278. return (error);
  279. }

以下、スケッチのキーポイントを説明していきます。

Wailingの判定方法

まず、Wailingの判定は、単純にMPU-6050のZ軸の回転 (=Groveモジュールの面に法線を立てて、その法線を軸とした回転=ブレッドボードの上面か下面にGroveモジュールを張り付けつつブレッドボードをギターっぽく持ってWailingしたときの、ブレッドボードの回転) で、回転の角速度が一定値以上かどうかで判定しています。もしもジャイロセンサーではなく加速度センサーを使って同じことをしようとすると、慣性もセンサーが拾ってしまって、回転の判定が非常に大変なことになりますが、ジャイロセンサーを使う場合は単純にZ軸の角速度が一定値以上かどうかを判定するだけで済みます。

具体的には、Z軸の角速度取得と、角速度の判定を、loop()の216行目付近で行っています。 なおMPU-6050から取得した16bitの値は、エンディアンを変換する必要があるため、それも合わせて行っています。

  1. int error;
  2. accel_t_gyro_union accel_t_gyro;
  3. //error = MPU6050_read (MPU6050_GYRO_XOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // X軸の回転を検出する場合
  4. //error = MPU6050_read (MPU6050_GYRO_YOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Y軸の回転を検出する場合
  5. error = MPU6050_read (MPU6050_GYRO_ZOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Z軸の回転を検出する場合
  6. uint8_t swap;
  7. #define SWAP(x,y) swap = x; x = y; y = swap
  8. SWAP (accel_t_gyro.reg.z_gyro_h, accel_t_gyro.reg.z_gyro_l);
  9. //FS_SEL_3 16.4 LSB / (°/s)
  10. float gyro_z = accel_t_gyro.value.z_gyro / 16.4;
  11. if (gyro_z < -TH_WAILING) // 時計回りをwailingとして検出する場合
  12. //if (gyro_z > TH_WAILING) // 反時計回りをwailingとして検出する場合
  13. {
  14. //以下略

なお判定の閾値は、33行目のマクロで定義しています。反応が敏感すぎるようなら、400を500くらいに上げてみてください。 逆に、より敏感にするためには、300とか350とかにしてみて下さい。

  1. const int TH_WAILING = 400; // WAILINGと見なす角速度の閾値

また、ここでは「Groveモジュールを時計回りに回転させた場合」にWailingを検出するようにしています。 (先の写真のように、ブレッドボードの裏面に普通にGroveモジュールを張り付けた場合は、これでOKです)

もしもブレッドボードの表面にGroveモジュールを設置するなどの理由で「反時計回り」を検出したい場合は、回転検出部を反時計回りのものに置き換えてください。(注釈化しているif文のようにしてください)

  1. if (gyro_z < -TH_WAILING) // 時計回りをwailingとして検出する場合
  2. //if (gyro_z > TH_WAILING) // 反時計回りをwailingとして検出する場合
  3. {
  4. //以下略

もしもブレッドボードの「側面」にGroveモジュールを設置する場合は、検出すべき回転軸がZ軸ではなく、X軸やY軸になります。 その場合は、下記の部分も見直してください。

  1. //error = MPU6050_read (MPU6050_GYRO_XOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // X軸の回転を検出する場合
  2. //error = MPU6050_read (MPU6050_GYRO_YOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Y軸の回転を検出する場合
  3. error = MPU6050_read (MPU6050_GYRO_ZOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro)); // Z軸の回転を検出する場合

Wailingボタンを離す処理の追加

Wailingが発生したと判断された場合は、内部でBUTTON5を押す動作をします。BUTTON5に相当する物理ボタンは接続していないので、これは仮想的なボタンとなります。 Wailingもボタンとして扱う以上は、押すだけでなく、離す処理も必要になります。離す処理がないと、Wailingボタンが押しっぱなしの状態になり、 次のWailingが動作しなくなります。

loop()の249行目あたりで、この処理を入れています。 具体的には、(最初にWailingが発生したときにボタンが押され、) そこから200ms経過したらボタンを離すようにしています。 1度のWailing操作をすると、内部では1-2ms毎に複数のWailing判定が連続して発生しますが、それらはWailingボタンの追加押しになるので全て無視されます。 200ms経過しWailingボタンが離されると、再度Wailing操作でWailingボタンを押すことができるようになります。

  1. // WailingボタンをONにした後200ms経過したら、自動でオフにする
  2. if (lastButtonState[BUTTON_WAILING] &&
  3. t > lastButtonChangedTime[BUTTON_WAILING] + 200)
  4. {
  5. Joystick.setButton(BUTTON_WAILING, false);
  6. lastButtonState[BUTTON_WAILING] = false;
  7. lastButtonChangedTime[BUTTON_WAILING] = t;
  8. buttonStateChanged = true;
  9. #ifdef DEBUG
  10. Serial.print(t);
  11. Serial.println(":WAILING OFF");
  12. #endif
  13. }

処理の高速化

MPU-6050の情報はI2C通信を経由して取得するため、ほかのボタンやアナログスティックと比べると情報取得がどうしても遅くなります。 またMPU-6050のZ軸の角速度以外の情報 (X/Y/Z軸方向の加速度など) も取得して処理を行うと、1回の処理で5~6ms必要となるようでした。

一方で、ギタコンのボタン状態の読み出しは1ループあたり1~2ms程度で済ませたいですよね。そこで、処理の高速化が必要になります。

以下の高速化を一通り施すことで、1ループ当たりの時間が1~2ms程度となりました。

MPU-6050への余計なアクセスや、処理の削除

スケッチのベースにさせていただいた 加速度+ジャイロのGY-521(MPU-6050)を使ってみた -1- (特に縛りなく) や MPU-6050 Accelerometer + Gyro (The Arduino Playground) から、ギタコンで使う情報(Z軸の角速度)に関係しないところを全て削除しました。

そして、更に高速化するために、setup()の96行目近辺で以下の設定をしています。

    • MPU-6050のサンプリングレートを最大化
    • ジャイロセンサーの帯域幅を最大化
  1. // サンプルレートを最大化
  2. MPU6050_write_reg (MPU6050_SMPLRT_DIV, 0);
  3. // FSYNC input disabled, Gyrometer bandwidth=256Hz
  4. MPU6050_write_reg (MPU6050_CONFIG, 0);
I2C通信の帯域幅の拡張

通常、Arduino(のWireライブラリ)を使うと、I2C通信のクロックは100kHzとなります。しかし、I2C通信は規格上400kHzのクロックを使うことができ、MPU-6050もそれに対応しています。そこで、I2C通信のクロックを400kHzに変更することで、I2C通信の速度を4倍にします。

具体的には、C:\Program Files (x86)\Arduino\hardware\arduino\avr\libraries\wire\src\utility\twi.h を編集し、28行目あたりにあるTWI_FREQの値を100000Lから400000Lに変更します。(参考: MPU6050 (備忘録) )

  1. #ifndef TWI_FREQ
  2. #define TWI_FREQ 100000L // ここを400000Lにする
  3. #endif

なお通常、Program Files (x86) 以下にあるファイルを編集しようとすると、権限がなくファイルを開けなかったり書き込めなかったりします。いったんデスクトップにファイルをコピーして編集して元の場所に上書きコピーするとか、そのコピーをするにも管理者としてエクスプローラーを開いてやらないといけないとか、いくつかの工夫が必要です。

delay(1); の削除

試作3までは、loop()の最後で必ずdelay(1);を実行し、1msのウエイトを入れていました。
しかし、1ループ当たりの処理が例えば2ms掛かっているような状況でdelay(1)すると、合計で1ループあたり3ms掛かることになります。
そのため、直前のループ開始時刻と今回のループ開始時刻を比較し、1ms経過していない場合のみ、1ms経過するまで待つ、といった処理に変更しました。

そして、この部分で、ついでにmills()のオーバーフローの対策を加えています。mills()のオーバーフローはArduinoに通電して49~50日程度連続して通電しないと発生しないので通常はオーバーフローに遭遇しない(、だからオーバーフロー対策を端折っちゃおう)、と考えますが、一方でギタコンをUSBでつなぎっぱなしにしているとそのうちいつか必ず発生するということでもあるので、ボタン動作が破綻しない程度の簡単な対策を入れています。

  1. void loop() {
  2. bool buttonStateChanged = false;
  3. unsigned long t = millis();
  4. // 1ms間隔でloop内処理が実行されるようにする
  5. // (前回の処理から1msに満たない時間しか経っていない場合は
  6. // 門前払いする)
  7. if (last_t >= t)
  8. {
  9. // 門前払いの前に、mills()のオーバーフロー対策もしてしまう
  10. if (last_t > (unsigned long)0xFFFF0000 &&
  11. t < (unsigned long)0x00001000)
  12. {
  13. for (int index = 0; index < BUTTONS; index++)
  14. {
  15. // 最終ボタン状態更新時刻を繰り上げて、正負が
  16. // 破綻しないようにする(この程度の対策で問題ないはず)
  17. lastButtonChangedTime[index] = 0;
  18. }
  19. }
  20. return;
  21. }
  22. else
  23. {
  24. last_t = t;
  25. }
複数の入力をまとめて1回で出力する

試作3までは、ボタンやスティックの状態変化を検出したときは、「検出したタイミングでその都度」ギタコンのボタンやスティックの状態を一式、ごっそりとUSB経由でPCに出力していました。

しかしこれだと、スティック操作をしながらボタンを押すなどといった場合に、1回の状態確認ループの中でスティックとボタンの状態変化を検出する都度、ギタコン全体のボタン/スティックの状態をPCに送るので、USBの通信時間分、1回ループの処理時間が長くなり、1回のループ当たり1~2msよりもっと処理時間がかかるようになってしまっていました。

そのため、ボタン等の状態変化を検出してもすぐにはUSB経由で送信せず、ループの終りにまとめて送信するようにしました。そうすれば、1回のループ当たりのUSB送信は1回で済みます。

このため、まずsetup()の70行目付近で、Joysthick_のライブラリを開始する際に、「状態変化時にUSBで自動送信する」機能(AutoSendState)を無効化します。

  // Initialize Joystick Library (AutoSendState=OFF)
  Joystick.begin(false);

その上で、loop()の263行目付近(loop()の最後)で、ボタンの状態変化があった時にだけ、USB経由で現在のボタン等の状態を送信する処理を追加しています。

  1. // ボタンの状態変化があった時にだけ、PCにHID入力を通知する
  2. // 同時に、ボタン状態変化の最終時刻を更新する
  3. if (buttonStateChanged)
  4. {
  5. Joystick.sendState();
  6. last_button_state_changed_t = t;

ジャイロセンサーの取得値の最大値の最大化 (Full-scaleの最大化)

MPU-6050は、初期状態では、ジャイロセンサーで取得できる角速度の最大値は250度/秒です。しかしこれだと、それほど激しくWailingしてなくてもこの上限値になってしまい、回転の強弱を正しく判別できません。そのためこの値の最大値を最大化することで、取得値のレンジを広げ、回転の強弱を区別できるようにします。
具体的には、FS_SELというパラメータを3に設定することで、取得できる角速度の最大値を2000度/秒に拡大します。
setup()の処理がこれに該当します。

設定に必要な資料は、MPU6050 6 DOF ジャイロ加速度計 IMU (の中にある製品マニュアル) (ロボショップ) から得ました。

  1. // FS_SELの設定
  2. error = MPU6050_read (MPU6050_GYRO_CONFIG, &c, 1);
  3. c |= 0x18; // FS_SEL 3
  4. MPU6050_write_reg (MPU6050_GYRO_CONFIG, c);

まとめ

試作4では、Wailingに対応しました。

個人的には、PlayStation1用のKONAMI製ギターコントローラーと比べて、Wailingの反応が相当よく、きびきびと反応してくれるので気に入っています。

試作3のスケッチ全体のダウンロード: gtcon-08.zip (Wailing対応+アナログスティック対応あり)

注意: 高速化のため、このスケッチをArduinoのIDEで開く前に、C:\Program Files (x86)\Arduino\hardware\arduino\avr\libraries\wire\src\utility\twi.h を編集し、28行目あたりにあるTWI_FREQの値を100000Lから400000Lに変更することをお忘れなく。