Release 2017.1.29 / Update 2017.6.18

(日本語) Arduinoでジョイスティックを使う

Sorry, this entry is only available in 日本語. For the sake of viewer convenience, the content is shown below in the alternative language. You may click the link to switch the active language.

可変抵抗器の揺らぎを軽減する方法を以前紹介しましたが、この方法は他のパーツでも応用できたりします。「アナログジョイスティック」なんて呼ばれるパーツも、原理的には可変抵抗器が縦横で2つ並んでいるだけなので通用するはず。

そういったわけで、パーツショップで安くて小さいやつを手に入れたんですが、安物だけに、扱えるようになるまでにちょっと苦労しました。この記事ではそんなジョイスティックを使えるようになるまでの経緯とスケッチについて書いてきます。

配線図

必要なもの

  • Arudino
  • アナログジョイスティック
  • OLED Graphic Display

図はArduino Nanoですが特に種類の指定ではありません。ただ、Nanoの場合、A4がI2CのSDA、A5がSCLであることに倣って接続されています。そこだけ気をつけてください。

ジョイスティック入手先

ちなみにもう少しつくりのしっかりしたタイプでも試してみました。こちらはサイズは大きく、値段も違うんですが、数値は正確に出ます。

スケッチを書く

ジョイスティックをanalogRead

最初は単純に素の情報を見てみます。読み込んだ数値をシリアルモニタに表示します。

#define STX A1
#define STY A0

const byte stk_pins [2] = {STX, STY};

void setup() {
  for (byte i = 0 ; i < 2 ; i++) pinMode(stk_pins[i], INPUT);
  Serial.begin(38400);
}

void loop() {
  Serial.print("X:");
  Serial.print(analogRead(stk_pins[0]));
  Serial.print("  Y:");
  Serial.print(analogRead(stk_pins[1]));
  Serial.println();
}

こうして見ると、アナログ入力の揺らぎ以外に可変抵抗器と違う特徴があります。ジョイスティックは離せば中心に戻るような機構なので、触っていない時が1023段階を半分に割ったくらいの値になっています。

更に、安物のためかその中心付近の値がハッキリと合いません。スティックを傾けて最低値から戻ったときと、最高値から戻ったときでは中心になったときの値が違います。

X 大きい値から戻ったとき、Y 小さい値から戻ったとき

中心に戻った時の値を20回ほど拾って統計するとこんな感じなりました。

LOWが低い値から中心に戻ったとき、HIGHが高い値からの場合です。かなりのバラつきがあります。図で表すとこういうことになります。 

ということで、読み取ったanalogReadの値を取捨して正常に使えるように工夫していきます。対策としては下記のようにします。

  • 値を動かしていない状態を0中心とした+-(正負)として扱う。
  • バラつきのある中心値は一定の値であれば0にしてしまう。

*図の「中心点・0範囲」の割合が大きいですが、見やすいよう極端に描きました。ご了承を。

値を正負にする

中心を境に+-で表現するために読み取り時にアナログ最大値である「1023」の半分を引きます。ついでに、揺らぎ低減のために「10%更新」も。

void loop() {
  static short vr[2];

  for (byte i = 0 ; i < 2 ; i++)
  {
    vr[i] = vr[i] * 0.9 + (analogRead(stk_pins[i]) - 1023 / 2) * 0.1;
  }

  Serial.print("X:");
  Serial.print(analogRead(stk_pins[0]));
  Serial.print("  Y:");
  Serial.print(analogRead(stk_pins[1]));

  Serial.print("    x:");
  Serial.print(vr[0]);
  Serial.print("  y:");
  Serial.print(vr[1]);
  Serial.println();
}

X,Yの値はまとめて扱いやすいよう「vr」という変数配列にします。vr [0]はX座標、vr [1]はY座標です。

中心のバラつき範囲を0にする

0になる範囲は+側と-側で振れ幅が違います。なので、一定の数値以内に入ったものは0として除外してやります。その境界値はVR_IDLEという定数で+-60にしています。

void loop() {
  static short vr[2];
  short pos[2];

#define VR_IDLE 60

  for (byte i = 0 ; i < 2 ; i++)
  {
    vr[i] = vr[i] * 0.9 + (analogRead(stk_pins[i]) - 1023 / 2) * 0.1;
    char deg = (vr[i] > 0) ? 1 : -1;
    pos[i] = (abs(vr[i]) < VR_IDLE) ? 0 : vr[i] - (VR_IDLE * deg);
  }

  Serial.print("X:");
  Serial.print(analogRead(stk_pins[0]));
  Serial.print("  Y:");
  Serial.print(analogRead(stk_pins[1]));

  Serial.print("    x:");
  Serial.print(pos[0]);
  Serial.print("  y:");
  Serial.print(pos[1]);
  Serial.println();
}

abs関数で整数にした値がVR_IDLEより低ければ、0としてしまいます。

if(abs(vr [0]) > 0) pos [0] = 0;

その境界に引っかからなければ動かしているということなので、余白として削ったVR_IDLEの60個分をシフトしつつ正規の読み取りとして代入。その時、シフトする方向が正負で違うので、degでマイナス方向かどうかの指定をしています。

char deg = (vr [0] > 0) ? 1 : -1;
pos =  vr [0] - (VR_IDLE * deg);

これをFor文で2回繰り返すことでX、Y用の値を処理させています。

値を揃える

最後に+と-で最大値が微妙に違うので、constrain関数で同じ幅になるよう制限しちゃいます。VRNGの数値がそれにあたります。

void loop() {
  static short vr[2];
  short pos[2];

#define VR_IDLE 60
#define VRNG (500 - VR_IDLE)

  for (byte i = 0 ; i < 2 ; i++)
  {
    vr[i] = vr[i] * 0.9 + (analogRead(stk_pins[i]) - 1023 / 2) * 0.1;
    char deg = (vr[i] > 0) ? 1 : -1;
    pos[i] = (abs(vr[i]) < VR_IDLE) ? 0 : vr[i] - (VR_IDLE * deg);
    pos[i] = constrain(pos[i], VRNG * -1, VRNG);
  }

この結果、+-でトータル880の分解能というジョイスティックになります。ただし、スティックの中心からの一定範囲と端は「クロップ」されることになるので、その機能を満遍なく使えていることにはなってません。まあ、そんな遜色はないですけど。

あとは獲得したposの数値をどう使うかです。

まとめ スケッチ

最後に、ジョイスティックの数値で、OLEDに描いた丸が動くスケッチを書きます。

ジョイスティックの数値獲得に関する処理はJSTICK関数にまとめてしまっています。JSTICK関数にピン番号(stk_pins [2] = {STX, STY})を送ることで、XかY軸の値が帰ってきます。分解能は+と-でそれぞれ440の範囲です。それを128*64のOLEDサイズにmapすることで丸の動きが描画しているわけです。ちなみに、更新回数を増やすことで、u8g描画での遅延対策をしています。

JSTICKから帰ってきた値をmap関数で範囲調整すれば、モータとか他の用途へ簡単に流用できると思います。

正確な値が出せるパーツを使っている方は、VR_IDLEの切捨て数値を下げていくとより細かい動きを拾えるようになっていくかと思います(分解能も変わってきます)。

アナログジョイスティックでOLEDの丸を動かす

#define STX A1
#define STY A0
#define VR_IDLE 60
#define VRNG (500 - VR_IDLE)

const byte stk_pins [2] = {STX, STY};

#include "U8glib.h"
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // OLED / I2C / TWI
// SDA 4/ SCL 5


//  read joystick value  //

short JSTICK(byte vr_pin) {
  static short vr[2];
  vr[vr_pin] = vr[vr_pin] * 0.9 + (analogRead(stk_pins[vr_pin]) - 1023 / 2) * 0.1;
  return vr[vr_pin];
}


// draw to OLED //

void DRAW(short x, short y) {
#define C_CIZE 4
  x = map(x, VRNG * -1, VRNG, C_CIZE, 127 - C_CIZE);
  y = map(y, VRNG * -1, VRNG, C_CIZE, 63 - C_CIZE);

  u8g.firstPage();
  do {
    u8g.drawDisc(127 - x, y, C_CIZE);
    for (byte i = 0 ; i < 2 ; i++) JSTICK(i);
  } while (u8g.nextPage());
}


void setup() {
  for (byte i = 0 ; i < 2 ; i++) pinMode(stk_pins[i], INPUT);
  Serial.begin(38400);
  u8g.setColorIndex(1);  // pixel on
}


void loop() {
  short pos[2];

  for (byte i = 0 ; i < 2 ; i++)
  {
    // get joystick value
    pos[i] = JSTICK(i);
    // get positive or negative direction
    char deg = (pos[i] > 0) ? 1 : -1;
    // cut off ambiguous range
    pos[i] = (abs(pos[i]) < VR_IDLE) ? 0 : pos[i] - (VR_IDLE * deg);
    // limit in range
    pos[i] = constrain(pos[i], VRNG * -1, VRNG);
  }

  // value to Serial monitor - comment out if joystick response is slow
  Serial.print("X:");
  Serial.print(analogRead(stk_pins[0]));
  Serial.print("  Y:");
  Serial.print(analogRead(stk_pins[1]));

  Serial.print("    x:");
  Serial.print(pos[0]);
  Serial.print("  y:");
  Serial.print(pos[1]);
  Serial.println();

  //draw to OLED
  DRAW(pos[0], pos[1]);
}

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA


This site uses Akismet to reduce spam. Learn how your comment data is processed.