Seeeduino XIAOでジョイスティックの情報を取得する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

GroveモジュールのGrove-Thumb JoystickでスティックをX方向とY方向に円を描くようにスティックを回すと位置に応じたアナログ電圧を取得することができます。Seeeduino XIAO用の拡張ボードを使って動作確認を行いました。

Seeeduino XIAO用拡張ボード(Seeed Studio)を使用しています。ジョイスティックを操作して拡張基板のブザーの音程と長さを変更して動作確認を行います。

Seeeduino XIAOを使って動作確認を行ったことを下記リンクにまとめています。

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

Grove-Thumb Joystickを使用する

Grove-Thumb JoystickはSeeed Studioが製作しているGroveモジュールです。X方向とY方向に円を描くようにスティックを回すと操作位置に応じた電圧を出力します。詳細は下記のリンクを参考にしてください。

Grove – Thumb Joystick | Seeed Studio Wiki

Seeeduino XIAOにGrove-Thumb Joystickの電圧を入力すると操作量に応じた電圧を取得することができます。本記事では取得したアナログ値を使ってブザーの音程と長さを操作します。

Grove-Thumb Joystickを以下ではジョイスティックとします。

広告

モジュールの特性の確認

モジュールの特性の確認
モジュールの特性の確認

ジョイスティックのWikiページに標準値が記載されていますが、使用するモジュールによって中央値と最大/最小値は若干変化することから使用するモジュールの特性を確認します。

シリアルモニターにジョイスティックを±X方向と±Y方向に倒したときの値を表示して確認します。

ジョイスティックの動作特性の確認
ジョイスティックの動作特性の確認

X軸のシリアルモニターの結果と同様にしてY軸もシリアルモニターで値を確認しました。X軸とY軸の結果をまとめると以下の通りになりました。

最小値(-側)中央値(操作なし)最大値(+側)
X軸252525791
Y軸264519772
モジュールの特性の確認

シリアルモニタでの確認の結果から本記事で使用するジョイスティックの基準値はX軸:525、Y軸:519とします。X軸よりもY軸の方が最小値と最大値のレンジが狭い結果になりました。

広告

アナログ値をフィルタリングする

void TimerCnt(void){
  uint8_t i;
  uint16_t sumx=0;
  uint16_t sumy=0;

  meas.bufx[meas.wp] = analogRead(JOY_AO_X); //X軸の値を取得
  meas.bufy[meas.wp] = analogRead(JOY_AO_Y); //Y軸の値を取得

  if( ++meas.wp >= AIFILT_MAX ){ //保管先を更新
    meas.wp = 0;
  }

  for( i=0; i < AIFILT_MAX; i++ ){ //平均をとるため合計値を算出
    sumx += meas.bufx[i];
    sumy += meas.bufy[i];    
  }

  meas.x = sumx / AIFILT_MAX; //平均値を計算
  meas.y = sumy / AIFILT_MAX; //平均値を計算
}

アナログ値は外来ノイズの影響を受けるため取得したデータを平均化(ローパスフィルタの効果)して使用します。

TimerInt()関数は10ms毎にタイマー割り込みでコールされる自作の関数です。アナログ値のサンプリングタイミングを一定に保つため割り込みでアナログ値の取得と平均値を計算します。

analogRead()関数は引数で指定したピンのアナログ変換値を取得します。X軸、Y軸のアナログ値をそれぞれmeas.bufx[]、meas.bufy[]に格納します。

meas.wpはmeas.bufx[]及びmeas.bufy[]の配列にアナログ値を格納する場所を示したポインタ値です。meas.wpを更新することで配列に順番にアナログ値を格納します。

アナログ値の平均値を計算するため配列の合計値を計算し、配列の数(AIFILT_MAX)で割っています。meas.xとmeas.yに平均値を格納しアナログ値として使用します。

スポンサーリンク

Seeeduino XIAOの拡張ボードを使用する

Seeeduino XIAO専用の拡張ボードを使用するとOLEDに文字を表示したりSDカードを使用したりGrove端子を利用してセンサーと接続することができます。下記記事に拡張ボードについてとOLEDライブラリ追加の方法をまとめています。

Seeeduino XIAOの拡張ボードのOLEDを使用する

標準ライブラリでブザーを鳴らす

拡張ボードにはブザーが実装されておりArduinoの標準ライブラリであるtone()関数を使ってブザーを鳴らすことができます。以下に一例を示します。

#define PIN_PULSE  3 //拡張基板では3を指定する

void setup() {
  uint8_t i;

  //初期化時に3回ピピピとブザーを鳴らす例
  for( i=0; i < 3; i++ ){
    tone(PIN_PULSE,3000, 100);
    delay(200);
  } 
}

void loop() {

 //音程を変化させながらブザーを鳴らす例 
  if( timbuz == TIME_UP){
    timbuz = BUZ_WAIT;
    x_val = BASE_VALUE_X - meas.x; //アナログ値の変化を算出
    y_val = BASE_VALUE_Y - meas.y; //アナログ値の変化を算出
    tone(PIN_PULSE,BASE_FREQ + x_val, BASE_DUR + y_val);
  }
}

tone()関数の引数1にパルス波形を出力するピン番号を指定します。拡張基板では3が割り振られているので3ピンを指定します。

引数2に振動波形の周波数を指定します。周波数を高くするほどブザーの音程が高くなります。ブザーの対象帯域の周波数を指定しますが低すぎるとブザーが鳴らない場合や高すぎるとブザーが聞こえなくなります。

引数3にパルス波形の出力期間を指定します。引数3に値を指定しない場合は出力を続ける指定となります。

例ではジョイスティックから取得したアナログ値を第2引数と第3引数の値に使用することでブザーの周波数と出力期間が可変するようにしています。

注意事項としてパルス波形が出力される前にtone()関数を上書きすると状態変化によるウェイトなどによって出力されない状態になることがあります。頻繁なtone()の上書きが発生しないようにすることが必要です。

広告

OLEDライブラリの使用例

#include <U8x8lib.h>

U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8( SCL, SDA, U8X8_PIN_NONE);

void setup() {
  u8x8.begin(); //初期化
  u8x8.setFlipMode(1);  // set number from 1 to 3
  u8x8.setFont(u8x8_font_5x8_r); //フォントを選択
}

OLEDライブラリの追加や初期化の方法は下記の記事を参考にしてください。

Seeeduino XIAOの拡張ボードのOLEDを使用する

U8x8libライブラリをインクルードしU8X8_SSD1306_128X64_NONAME_HW_I2Cクラスの変数としてu8x8(任意でよい)をインスタンス化します。setup()内でメンバー関数であるbegin()関数、setFlipMode()関数、setFont()関数などを使用してOLEDの初期化を行います。

u8x8.setCursor(0, 4); //カーソル移動
u8x8.print("X-->");
u8x8.print(meas.x); //アナログ値を文字列で出力
u8x8.setCursor(0, 6); //カーソル移動
u8x8.print("Y-->");
u8x8.print(meas.y); //アナログ値を文字列で出力

setCursor()関数で文字表示する座標を指定します。第1引数に行番号、第2引数に列番号を指定します。座標を指定した後はprint()関数で文字を書き込んで表示します。例ではX–>の後ろにアナログ値を文字列で出力しています。Y–>についても同様です。

種別内容
SetCursor()データを書き込む初期位置(表示スタート位置)にカーソルを合わせます。
print()
println()
文字列を指定して書き込み(表示)ます。
println()は文字列の最後に改行コードが入ります。
clearLine()指定した列の文字をクリアします。
今回使用したメンバー関数

clearLine()関数で上書き前の文字列を消してから表示した場合、ラインの文字列を消してから文字列を書き込むため表示がチラついて見えます。

clearLine()関数を使用しない場合上書きする文字列が短い場合、文字が残ったまま上書きするため表示がおかしくなることがあります。文字列の長さを統一している場合はそのまま上書きしても問題になりません。

スポンサーリンク

動作確認

動作確認用の回路図
動作確認用の回路図

Seeeduino XIAOをSeeeduino拡張ボードに挿入します。拡張ボードに実装されているButton(D1)を使用します。Button(D1)を使用するため1ピンをプルアップ付きのDIで初期化を行っています。

電源を入れるとOLEDの1段目に「Seeeduino」、2段目に「Thumb Joystick」、3段目に「Ver1.00」を表示します。4段目以降にジョイスティックから取得したアナログ値を表示します。

Button(D1)を押すとブザーをONして1秒毎に鳴らします。もう一度Button(D1)を押すとブザーをOFFして停止します。ジョイスティックを操作していない場合のブザーの条件は周波数が2000Hz、ブザー間隔が300msになります。

OLEDの表示(ジョイスティック操作)
OLEDの表示(ジョイスティック操作)

ブザーをONした状態でジョイスティックを軸方向に操作します。+X軸方向に倒すとアナログ値が792になりブザーの音程が低くなります。-X軸方向に倒すとアナログ値が250になりブザーの音程が高くなります。

+Y軸方向に倒すとアナログ値が772になりブザーの間隔の長さが短くなります。-Y軸方向に倒すとアナログ値が264になりブザーの間隔の長さが長くなります。

広告

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <TimerTC3.h>
#include <U8x8lib.h>

#define PIN_PULSE  3
#define JOY_AO_X 7
#define JOY_AO_Y 6
#define PIN_DI_1 1
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_MEAS 20
#define AIFILT_MAX 8
#define DIFILT_MAX 4
#define TIM_DIFILT 1
#define BASE_CNT 1 //ベースタイマカウント値
#define BASE_VALUE_X 524
#define BASE_VALUE_Y 519
#define BASE_FREQ 2000
#define BASE_DUR 300
#define BUZ_WAIT 100

typedef struct AIFILT{
  uint8_t wp;
  uint16_t bufx[AIFILT_MAX];
  uint16_t bufy[AIFILT_MAX];
  uint16_t x;
  uint16_t y;
};

typedef struct DIFILT{
  uint8_t wp;
  uint8_t buf[DIFILT_MAX];
  uint8_t di;
};

int8_t cnt10ms;
int16_t timMeas;
int16_t timDifilter;
int16_t timbuz;
int16_t x_val;
int16_t y_val;
AIFILT meas;
DIFILT diData;
bool btnflg;
bool btnbit;
uint16_t freq;
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8( SCL, SDA, U8X8_PIN_NONE);

void TimerInt(void);
void mainTimer(void);
void mainApp(void);
void DiFilter(void);

void setup() {
  uint8_t i;

  Serial.begin(115200);
  pinMode(PIN_DI_1,INPUT_PULLUP);

  for(i=0; i < AIFILT_MAX; i++){
    meas.bufx[i] = analogRead(JOY_AO_X);
    meas.bufy[i] = analogRead(JOY_AO_Y);
    delay(10);
  }

  for(i=0; i < DIFILT_MAX; i++){
    timDifilter = TIME_UP;
    DiFilter();
    delay(10);
  }
  btnbit = true;

  u8x8.begin();
  u8x8.setFlipMode(1);
  u8x8.setFont(u8x8_font_5x8_r);
  u8x8.setCursor(0, 0);
  u8x8.print("Seeeduino     ");
  u8x8.setCursor(0, 1);
  u8x8.print("Thumb Joystick");
  u8x8.setCursor(0, 2);
  u8x8.print("       Ver1.00");

  for( i=0; i < 3; i++ ){
    tone(PIN_PULSE,3000, 100);
    delay(200);
  }  

  TimerTc3.initialize(10000); //10msでタイマ割り込み
  TimerTc3.attachInterrupt(TimerInt);  
}

void loop() {

  mainTimer();
  mainApp();
  DiFilter();
}

/* メイン処理 */
void mainApp(void){

  if( timMeas == TIME_UP ){
    timMeas = TIM_MEAS;
    //Serial.print("The X and Y coordinate is:");
    //Serial.print(meas.x, DEC);
    //Serial.print(",");
    //Serial.println(meas.y, DEC);

    u8x8.setCursor(0, 4);
    //u8x8.clearLine(4);
    u8x8.print("X-->");
    u8x8.print(meas.x);
    u8x8.setCursor(0, 6);
    //u8x8.clearLine(6);
    u8x8.print("Y-->");
    u8x8.print(meas.y);
  }

  if( diData.di == 1 ){
    if( btnflg == false ){
      btnflg = true;

      if(btnbit){
        btnbit = false;
      }
      else{
        btnbit = true;
      }
    }
  }
  else{
    btnflg = false;
  }

  if(btnbit){
    if( timbuz == TIME_UP){
      timbuz = BUZ_WAIT;
      x_val = BASE_VALUE_X - meas.x;
      y_val = BASE_VALUE_Y - meas.y;  
      tone(PIN_PULSE,BASE_FREQ + x_val, BASE_DUR + y_val);
    }
  }
  else{
    noTone(PIN_PULSE);
  }
}

/* callback function add */
void TimerInt(void){
  uint8_t i;
  uint16_t sumx=0;
  uint16_t sumy=0;

  ++cnt10ms;
  meas.bufx[meas.wp] = analogRead(JOY_AO_X);//X軸の値を取得
  meas.bufy[meas.wp] = analogRead(JOY_AO_Y);//Y軸の値を取得

  if( ++meas.wp >= AIFILT_MAX ){ //保管先を更新
    meas.wp = 0;
  }

  for( i=0; i < AIFILT_MAX; i++ ){ //平均をとるため合計値を算出
    sumx += meas.bufx[i];
    sumy += meas.bufy[i];    
  }

  meas.x = sumx / AIFILT_MAX; //平均値を計算
  meas.y = sumy / AIFILT_MAX; //平均値を計算
}
/* タイマ管理 */
void mainTimer(void){

  if( cnt10ms >= BASE_CNT ){
    cnt10ms -=BASE_CNT;

    if( timMeas > TIME_UP ){
      --timMeas;
    }

    if( timDifilter > TIME_UP ){
      --timDifilter;
    }

    if( timbuz > TIME_UP ){
      --timbuz;
    }
  }
}
/* DIフィルタ */
void DiFilter(void){
  bool boo = true;
  uint8_t i;

  if( timDifilter == TIME_UP ){
    timDifilter = TIM_DIFILT;

    diData.buf[diData.wp] = digitalRead(PIN_DI_1);

    for( i=1; i < sizeof(diData.buf);i++){
      if( diData.buf[i - 1] != diData.buf[i]){
        boo = false;
      }
    }

    if(boo){ //データがすべて一致なので採用する
      diData.di = diData.buf[0];
    }

    if( ++diData.wp >= sizeof(diData.buf)){
      diData.wp = 0;
    }
  }
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

広告

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました