公開日:2018/11/29
title.png

2nd Season


#2-10 ミニキーボード付きゲームパッド

 「#2-2 スイッチを検出する」では、Arduino Microを使って、PCのキーボードをエミュレーションしてみました。今回は、3つのアイテムを組み合わせて、キーボードとマウスの機能を統合してみます。「#2-2 スイッチを検出する」でも説明したように、今回の実験には、機体をPCにUSB接続したときにHID(Human Interface Device)として機能する、「32U4またはSIMDベースの機体(Leonardo、Esplora、Zero、Due、MKR、ファミリ)が必要」なので、UNO(やMEGA)では動作しません。

4×4マトリクスキーパッド

 Iduinoスタータキットの中に入っていた4×4マトリクスキーパッドです。

ar2a_keypad.jpg

 マトリクスと言うだけあって、行方向と列方向に配線が4本づつ走っていて、交点に存在するスイッチを押すと、行線と列線がショートするようになっています。使い方も含めて回路図を起こしたのが下図です。

ar2a_schem.png

 青破線で囲った部分がキーパッドで、それ以外のGNDに落とす抵抗等は外付けに構成する必要があります。
 押されたキーを調べるときは、D15、D16、D4、D5の中から1本だけをHIGHにドライブして、他の3本をHIGH-Z(pinMode(Dx, INPUT))にしておきます。その状態でD6〜D9をdigitalRead()すると、スイッチが押された線からはHIGHが読み出され、スイッチが押されなかった線からはLOWが読み出されることで、判定できます。これを、D15 → D16 → D4 → D5と4回繰り返すと、16個のキーのどれとどれが押されたか、を知ることができます。

 複数のキーが同時に押されても、同時に押された全てのキーを検知できるのは、この回路構成の利点の一つでしょう。とは言え、スイッチ16個の検出にI/Oポートが8本とは、なかなか豪快な使いっぷりですね。I/Oポート数が足りれば良いけれど……。

 端子数以外の欠点としては、特定の複数キーを押したときに、押していないキーを「押した」と誤判定する場合があることでしょうか?
 例えば、上の回路図で、SW5+SWB+SW0の3つのボタンが押されると、SWDを押していないにもかかわらず、「押されている」と誤判定します。D15をHIGHにドライブした時に、D15 → SW0 → SW5 → SWB → D6のパスでHIGH状態が読み出せてしまうのが原因です。

キーの割り当て

 豪快にI/Oポートを使っていますが、所詮は4×4=16キーしかありません。QWERTYなんて夢のまた夢ですし、使うキーを厳選して、割り当てるしかありません。キーの割り当てを実行するために配列変数を定義しておきます。

  const uchar key_map[ ]=    
  {        
  KEY_TAB, 'w', 'e', 'r',
  'a', 's', 'd', 'f',
  '\0', '\0', 'n', 'g',
  KEY_ESC, ' ' /*space*/ '\0', '\0'
  };        

 ゲームをやる気満々のキーマップでしたね……(1^^;)、スミマセン。まあ、16キーでは、ゲームくらいにしか使い道が無くて……。
 4×4キーパッドの、左上→右、上→下へ、そのまま対応しています。キーを割り当てない部分には、'\0'を設定するようにしました。着色したキーは、キーとしては割り当てませんでしたが、紫の方はマウスの右クリックに、オレンジの方はマクロキーに、別途割り当てています。完成版プログラム(keymouse.ino)でご確認ください。

2ボタンマウスを構成する

 次は、2ボタンマウスを構成します。と言っても、使うのは、「#2-8 ロータリエンコーダを使う」で使ったロータリエンコーダと、「#2-9 抵抗値を測定する」で使用したジョイスティックです。ゲームパッドのアナログジョイスティックをマウス代わりに使う感覚ですね。ロータリエンコーダでマウスのホイール操作をエミュレートします。

 端子の接続先を確認しておきましょう。

装置 端子名 Arduino接続先 備考
キーパッド 1 D6 プルダウン
2 D7 プルダウン
3 D8 プルダウン
4 D9 プルダウン
5 D15  
6 D16  
7 D4  
8 D5  
ジョイスティック VRx A0 analog
VRy A1 analog
SW D10 プルアップ
ロータリエンコーダ SW D14 プルアップ
CLK D2 interrupt
DT D3  

※プルダウン:抵抗を介して接地するのを、便宜上、このように表現してみました。

 気が付かれましたか?Arduino Micro(互換機)の端子をほぼ、ギリギリ使い切りました。後はアナログ端子が2本と、UART用の2本を残すのみ。筆者の手元にあるMicro互換機は、本家より少し端子数が少ないのです……(「#2-1 Arduinoを使った電子工作の部屋(2nd Season)」)。

 ジョイスティックとロータリエンコーダ部分の回路図はこちら(↓)。

ar2a_schem2.png

 3ボタンマウスにしたい場合は、キーパッドのどこかのキーに3つ目のボタン(MOUSE_MIDDLE)を割り当ててください。

ジョイスティック

 いよいよ、マウスのインプリです。
 「HIDを使ってみたくて、わざわざArduino Microを購入した」話を「#2-1 Arduinoを使った電子工作の部屋(2nd Season)」でさせていただきましたが、ここでようやく、マウスとして使ってみることができます。
 マウスとして機能させるためには、Mouseクラスを使います。

 Arduino HP ⇒ REFERENCEによると、Mouseクラスを使用するには、「32U4またはSIMDベースの機体(Leonardo、Esplora、Zero、Due、MKR、ファミリ)が必要」なので、UNO(やMEGA)では動作しません。

 まず、プログラムの先頭で、「#include <Mouse.h>」として「Mouseクラスを使いますよ」と、宣言する必要があります。setup()の中で、「Mouse.begin();」を実行したら、もう準備完了。以下のようなメソッドを使って、マウスとしての機能をインプリします。

 今回は、ジョイスティックの値を読み取って、マウスみたいにカーソルを動かしてみます。

  // joy-stick
  static ulong msec_prev;
  ulong msec;
  msec=millis ();
  if (msec - msec_prev>2)
  {
    // read joy-stick status
    int vrx;
    int vry;
    vrx=analogRead (ana_VRX);
    vry=analogRead (ana_VRY);
    vrx=(vrx - 512)/(512/128); // L(-128) <--> (127)R
    vry=(512 - vry)/(512/128); // U(-128) <--> (127)D
    vrx=(abs (vrx)<4)? 0: (vrx<-128)? -128: (127<vrx)? 127: vrx;
    vry=(abs (vry)<4)? 0: (vry<-128)? -128: (127<vry)? 127: vry;
    // mouse move
    Mouse.move (vrx/16,vry/16, cnt_ccw - cnt_cw);
    cnt_cw=0;
    cnt_ccw=0;
    msec_prev=msec;
  }


 まず、アナログ端子に接続した、ジョイスティックのVRxとVRy端子値をanalogRead()します。

  vrx=analogRead (ana_VRX);
  vry=analogRead (ana_VRY);


 中央値512が0になるように換算します。さらに、Mouse.move()の変位量がchar(符号付8ビット)なため、−128〜127の範囲に入るようにスケーリングします。

  vrx=(vrx - 512)/(512/128); // L(-128) <--> (127)R
  vry=(512 - vry)/(512/128); // U(-128) <--> (127)D


 最後に、−128〜127を超えないようにクリッピングと、スティックをニュートラルにした時にマウスがドリフトしないよう、絶対値が4以下だった時に0と見なす処理を行います。

  vrx=(abs (vrx)<4)? 0: (vrx<-128)? -128: (127<vrx)? 127: vrx;
  vry=(abs (vry)<4)? 0: (vry<-128)? -128: (127<vry)? 127: vry;


 Mouse.move()では、x-y座標の変位量と、ホイールの変位量を同時に送るようになっているので、ここでロータリエンコーダで測定した回転量も送信します(ロータリエンコーダについては次節)。

  Mouse.move (vrx/16,vry/16, cnt_ccw - cnt_cw);

 スティックを一杯に倒したときのカーソルの移動速度は、上行の「/16」の除数値と、そもそもこれらの処理を囲んでいるif分の条件で決まっています。どちらかの値で調整してみてください。

  if (msec - msec_prev>2)
  {
    :
  }


 この処理はマウスの変位量を2ms毎にしかPCに送信しない処理です。

 尚、プログラムは、本来のジョイスティックの指示値と逆になっているかもしれませんのでご注意ください。個人的に握り易かった方向でつじつまが合うようにプログラムしてしまったので……。その辺りは各自、調整して使ってください。

ロータリエンコーダ

 ロータリエンコーダは「#2-8 ロータリエンコーダを使う」の回で、デジタルのLPFを構成して、ようやくチャタリングを除去したのですが、なんと、今回の用途では全く使い物になりませんでしたorz。

 理由は、「処理が重くなり過ぎた」ためです。「#2-8 ロータリエンコーダを使う」の回では、ロータリエンコーダだけを面倒見ていたため、かなりサンプリングレートが速く、LPFでチャタリングを除去しないと、まともに回転量をカウントできない状態でした。
 今回は、キーパッドやジョイスティックなどの処理が一気に増えて、ロータリエンコーダのサンプリングレートが著しく低下したため、なんと、CLKの立ち上がりエッジを取りこぼす事態となってしまいました。肝心な立ち上がりエッジを取りこぼしてしまっては、もはやチャタリング除去以前の問題ですo(1>_<)o。

 こんな時は、外部割り込みを使います。CLK端子の立ち上がりエッジで割り込みをかけ、割り込みサービスルーチン(ISR、Interrupt Service Routine)内でDT端子をサンプリングして回転方向をカウントします。
 Arduino Micro(Leonardo)で、外部割り込みに設定可能な端子は、デジタル端子のD0、D1、D2、D3、D7だけですので、ロータリエンコーダのCLK端子をD2に割り当てました。……お陰で、キーマトリクスのrowに割り当てていた端子を変更する羽目になってしまいました。row側の端子番号が不自然に飛んでいたのはこのためです。
 割り込みは既に「#2-2 スイッチを検出する」で使用していますので、そちらの解説をご覧ください。

 割り込みにしたところで、チャタリング問題は残っているので、LPFとは別な方法で除去する必要が生じました。
 と言っても、今度はISR内が戦場です。ISR内では、なるべく早く処理を完了して、リターンする必要があります。あまり込み入った処理はできません。

  void int_rotenc (void)
  {
    // interrupt service routine for CLK(D2) rising edge
    uchar clk;

    get_rotenc (&clk,&rotenc_dt);
    if (clk==HIGH)
    {
      rotenc_clk=true;
      usec_clk=micros ();
    }
  }


 ISR、int_rotenc()内では、DT端子状態の読み込み(rotenc_dt)、割り込みがあったことのフラグ(rotenc_clk)立て、時間micros()を取得(usec_clk)、という最小限の処理だけをして抜けます。

※int_rotenc()より手前(上の行)でget_rotenc()を定義してあるので、恐らくこの構成なら、GCCが(関数コールではなくて)インライン展開してくれているはず。micros()内の処理がちょっと未知数だけれど、まあ、最短の処理時間で抜けられているでしょう。

 後はゆっくり、loop()内の処理で、500μs以内の連続割り込み(CLK立ち上がりエッジ)を無視して、最後の割り込みで取得したDT端子の値(rotenc_dt)から、回転方向の判定を行います。500μs以下の細いパルスも、チャタリングとみなして除去しています。

  const uint usec_rotenc=500;  // renc: chattering cancel [us]

  // rotary encoder
  if (rotenc_clk && micros () - usec_clk>usec_rotenc)
  {
    // CLK rising edge
    // remove narrow CLK pulse (<=usec_rotenc[us])
    uchar clk;
    uchar dt_discard;
    get_rotenc (&clk,&dt_discard);
    if (clk==HIGH)
    {
      if (rotenc_dt==LOW)
        cnt_cw++;
      else
        cnt_ccw++;
    }
    rotenc_clk=false;
  }


 LPFを使う方法だと、パラメータに時間の定数が入っていなかったために、ちょうど良いパラメータを選ぶのが困難でした。今回の方法は、ばっちり時間(上の例だと500μs)がパラメータになっているため、比較的容易にパラメータを確定することができます。しかも、処理量やMCUのパフォーマンスが変わっても、今回のパラメータは変更する必要がありませんので、良いこと尽くしです。……ただし、「割り込み処理」であることには、細心の注意を払う必要があると思われます。

ミニキーボード付きゲームパッド?

 ミニキーボード付きゲームパッドが完成しました。と言っても、完成した装置はこちら(↓)、むき出し&ブレッドボードですが……。

ar2a_gmpad.jpg

 完成したプログラムをリンクしておきます。

  完成プログラム:keymouse.ino

 手持ちの「Biohazard Revelations UE」でプレイしてみました。マクロキーも思い通りに機能しましたし、ゲームはプレイできましたが、如何せん、ペラペラなキーパッドでは、キーが上手く押せず。平らなところに置いて、しっかり真ん中を押し込まないと、反応しなかったりしました。まあ、それは、安価なキーパッドのせいなので仕方ないです。

※実験が終わったら、忘れずに当たり障りのないプログラムをArduinoに書き込んでから、撤収しましょう。うっかり、回路を分解してしまった後で、ArduinoをPCに接続してしまうと、マウスとキーボードが暴走して、大変なことになります。
※一応、Arduinoの起動時にjoy-stickがニュートラル位置に無かった場合、HIDを無効にするコードにしておきましたが……。


 Arduinoの能力(I/Oポート)を使い切って、キーボードとマウスの機能を実装したことに関しては、何だかやり切った感(充実感?)があります。また、ロータリエンコーダのチャタリング除去機構の件など、実際にアプリケーションに組み込んで使ってみないとわからないことも多いと知りました。まだまだ修行が足りませんね……。


btn_arduino.png

btn_prev.png btn_next.png

home