【Raspberry Pi Pico入門 – 19】ジョイスティックを使ってみる

2023年10月23日Raspberry Pi Pico入門,基本編Arduino,Raspberry Pi Pico

概要

今回はジョイスティックがどの方向にどれくらい傾いているかを測定してみます。この2つの値を様々な部品のパラメータとして使うことでジョイスティックをコントローラとして使うことが出来ます。(モータのスピードをコントロールする、tone関数の音程に使う、keyboardライブラリを使ってPCのマウス代わりにする等)

実行環境

IDE:Arduino IDE 2.2.1
MCU:Raspberry Pi Pico

ジョイスティックはスターターキットのものを使います。型式はわかりませんが、おそらく↓これだと思います。

ジョイスティック(型式不明)

回路

以下のように配線します。ブレッドボードにジョイスティックを直接指してもいいですが、図のようにジャンパ線を使って接続したほうが使いやすいです。

コーディング

新しくスケッチを作成し、以下の内容をコピペしてください。

/* GPIO設定 */
const int STICK_X_PIN = 27;
const int STICK_Y_PIN = 26;
const int STICK_SW_PIN = 22;

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(STICK_X_PIN, INPUT);
  pinMode(STICK_Y_PIN, INPUT);
  pinMode(STICK_SW_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  int x_val = analogRead(STICK_X_PIN);
  int y_val = analogRead(STICK_Y_PIN);
  Serial.println("X = " + String(x_val) + ", Y = " + String(y_val));

  if(digitalRead(STICK_SW_PIN) == LOW){
    digitalWrite(LED_BUILTIN, HIGH);
  }else{
    digitalWrite(LED_BUILTIN, LOW);
  }

  delay(200);
}

動作確認

Arduino IDEのシリアルコンソールを開くとXとYの値が0.2秒毎に出力されていると思います。ジョイスティックを倒したときにXとYの値が変われば成功です。またジョイスティックを押し込むとRaspberry Pi Pico内蔵のLEDが点灯するはずです。

解説

ジョイスティックの仕組み

大雑把に説明すると、X軸とY軸にそれぞれ可変抵抗が入っており、ジョイスティックを倒すと抵抗の値が変わる仕組みになっています。可変抵抗については【Raspberry Pi Pico入門 – 16】で解説しています。

ジョイスティックの中身を回路図にすると下のようになります。ジョイスティックを倒すと可変抵抗の抵抗値が変わり、VRX・VRYの電圧が変わります。可変抵抗とスイッチはむき出しになっていてジョイスティックを横から覗くと見えるので、横から見た状態でスティックをグリグリ動かすとイメージしやすいかもしれません。

角度と倒し具合を計算する

サンプルコードではADCで電圧を測定しただけなので、このままではどの方向にどれくらい倒したのかわかりません。そこで三角関数を使ってXY座標のデータを極座標に変換してみます。

まずは完成品のサンプルから。このコードを使うとシリアルモニタの表示に角度Θと倒し具合rが追加されます。

#include <math.h>

/* GPIO設定 */
const int STICK_X_PIN = 27;
const int STICK_Y_PIN = 26;
const int STICK_SW_PIN = 22;

/* ジョイスティックのパラメータ */
const int DIV = 10;
int x_0 = 0;
int y_0 = 0;

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(STICK_X_PIN, INPUT);
  pinMode(STICK_Y_PIN, INPUT);
  pinMode(STICK_SW_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);

  /* キャリブレーション */
  int x_sum = 0;
  int y_sum = 0;
  for (int i = 0; i < DIV; i++) {
    x_sum += analogRead(STICK_X_PIN);
    y_sum += analogRead(STICK_Y_PIN);
    delay(50);
  }
  x_0 = x_sum / DIV;
  y_0 = y_sum / DIV;
}

void loop() {
  /* XY座標で測定 */
  int x_val = analogRead(STICK_X_PIN) - x_0;
  int y_val = analogRead(STICK_Y_PIN) - y_0;
  Serial.println("X = " + String(x_val) + ", Y = " + String(y_val));

  /* 極座標に変換 */
  float r = sqrt((x_val * x_val) + (y_val * y_val));
  float theta_radian, theta_degree;
  if (r > 50) {
    theta_radian = atan2(y_val, x_val) + PI;
    theta_degree = theta_radian * 360.0 / (2.0 * PI);
  } else {
    /* スティックを倒していない時は誤差が大きいので強制的に0°にする */
    theta_radian = 0;
    theta_degree = 0;
  }
  Serial.println("r = " + String(r) + ", θ = " + String(theta_degree));

  Serial.println();
  delay(200);
}

キャリブレーション

元々のXY座標は左上が原点(x=0、y=0)になっているので、倒していない時の値を測定して補正値とします。サンプルプログラムでは10回測定の平均値を補正値にしています。

  const int DIV = 10;

  /* キャリブレーション */
  int x_sum = 0;
  int y_sum = 0;
  for (int i = 0; i < DIV; i++) {
    x_sum += analogRead(STICK_X_PIN);
    y_sum += analogRead(STICK_Y_PIN);
    delay(50);
  }
  x_0 = x_sum / DIV;
  y_0 = y_sum / DIV;

XY座標のデータを補正

先ほど計算した補正値を引くことで、XY座標を「0~4096」の範囲から「約-2048~約2048」の範囲に変換します。(多少誤差はあります)
シリアルモニタを確認すると、倒していない時のX座標・Y座標はおおよそ0になっています。

  /* XY座標で測定 */
  int x_val = analogRead(STICK_X_PIN) - x_0;
  int y_val = analogRead(STICK_Y_PIN) - y_0;
  Serial.println("X = " + String(x_val) + ", Y = " + String(y_val));

XY座標を極座標に変換

ここからは三角関数の話になります。直角三角形の角度と、二つの辺の比率の関係をtan(タンジェント)と呼びます。
つまり、角度θからAB:BCの比率を計算することが出来ます。

$$\tan \theta=\frac{BC}{AB}$$

逆にAB:BCの比率から角度Aを求めるものをarctan(アークタンジェント)と呼びます。Aの座標が[x=0、y=0]だと決めたとき、x座標=ABの長さ、y座標=BCの長さと考えることが出来ます。

$$\theta=\arctan (\frac{BC}{AB})=\arctan (\frac{y}{x})$$

Arduinoでarctanの計算をするには「math.h」で定義されている「atan2()」を使います。引数の一つ目にy座標、二つ目にx座標を代入すると角度を返してくれます。
ただ、角度はラジアンという単位で返ってくるので要注意です。ラジアンとは弧度法とも呼ばれており、1周を360°ではなく2π(円周率の2倍)で表したものです。物理の計算ではラジアンで計算することが多いのでこのような仕様になっています。
角度の単位を[°]にしたいときは換算が必要です。

ここまで解説した式をプログラムにすると下のようになります。

  theta_radian = atan2(y_val, x_val);
  theta_degree = theta_radian * 360.0 / (2.0 * PI);

異常値をブロック

変換式そのままのプログラムで動かしてみると、スティックを倒していない時の角度が安定しない不具合が発生します。これはx座標とy座標が0周辺(図のA点の周り)をウロウロしているので、少し値が変わっただけでも角度が大きく変わってしまうからです。
これを防ぐために、スティックをある程度倒したときにだけ角度を計算するように場合分けしています。

コードの解説

今回のサンプルコードで新しく出てきた部分は、Serial.println()の中で文字列の足し算をしている部分です。C言語では文字列の足し算はありませんが、C++では足し算で文字列を結合する機能が実装されています。(Arduino言語はC++をベースに作られています)

ただ、下のように書いてしまうと「数字+文字列」となり、型の違うデータの足し算になってしまうのでコンパイルエラーが出てしまいます。かなり乱暴な例えですが、それぞれ単位が違うので「1kmに1秒を足せ」というプログラムになってしまうイメージです。

Serial.println("r = " + r);

そこで下のように、数字を文字列に変換するString()を使います。これで「文字列+文字列」の足し算になり、問題なく出力できます。

Serial.println("r = " + String(r));