【遊戯王真DM】ArduinoでチートリモコンLiteを製作する~ソフトウェア編~【封印されし記憶】

1年前に、遊戯王真デュエルモンスターズ~封印されし記憶~のポケットステーションで強力なカードを入手するためのツール:チートリモコンを製作記事を作成した。
今回は、誰でも作りやすいArduinoによるチートリモコンLiteを製作する。




<初代チートリモコン>
封印されし記憶を語る
概要編
ハードウェア編
ソフトウェア編①:動作確認
ソフトウェア編②:キー操作
ソフトウェア編③:チャタリング対策
ソフトウェア編④:NECフォーマット
ソースコード一覧
入手カード一覧


<チートリモコンLite>
回路図編
ソフトウェア編
基板実装編
ソースコード一覧


Arduino版チートリモコンLite>
回路図編
ソフトウェア編 ←いまここ




以下の内容は、ある程度Arduinoのスケッチを知っている前提となります。
何をしているのか分からない部分があったら、参考書やwebのArduino入門などを参照してください。




Arduinoのスケッチを書くのは初だが、文法はC言語によく似ているようなので、できるだけPIC版チートリモコンLiteのソースを流用する。



まずは各種定義で、スケッチの最初に記述する。

const uint8_t SEND_SW  = 2;
const uint8_t CUSTOM[] = {11, 9, 10, 12, 7, 5, 6, 8};
const uint8_t DATA[]   = {3, A1, A0, 4, A3, A5, A4, A2};
const uint8_t LED      = 13;

unsigned char nec_custom;
unsigned char nec_data;

最初の4行は入出力ピンの定義で、ピン番号を直接入力しても使えるが、見やすいように文字に置き換えた方が無難。
CUSTOMとDATAはそれぞれ8bitのロータリースイッチのため、配列として定義した。
左から順にbit7, bit6, …, bit0となるように配線をよく見て記述すること。
最後の2行はcustomコードとdataコードの定義。
ここに書くとグローバル変数になるはずなので、ローカル変数にしたい場合はvoid loop内に書くこと。と言うよりそっちの方が絶対良い。



void setup( )は最初に1回だけ実行するもの、つまり、C言語で言うところのint main( )内のメインループに入る前の部分に相当する。
ここではローカル変数の定義や各種IOピン、コンポーネントの定義などを行っていく。
今回のスケッチはこんな感じにした。

void setup() {
  for(int i=0; i<8; i++){
    pinMode(CUSTOM[i], INPUT_PULLUP);
    pinMode(DATA[i],   INPUT_PULLUP);
    digitalWrite(CUSTOM[i], HIGH);
    digitalWrite(DATA[i],   HIGH);
  }
  pinMode(SEND_SW, INPUT_PULLUP);
  digitalWrite(SEND_SW, HIGH);
  pinMode(LED, OUTPUT);
}

pinModeはIOピンの入出力設定関数で、LEDは出力のためOUTPUTにする。
CUSTOMとDATAとSEND_SWは、内部プルアップ付き入力にするためINPUT_PULLUPにした。
また、本来は設定不要のようだが、実験では内部プルアップが正常に働かないことがあったため、digitalWriteを使用して内部プルアップを確定させた。
CUSTOMやDATAは8bit配列のため、forループを使用することで8回書く必要があるものを1回に圧縮している。
最初に定義すれば、こういうところが簡単になるのでオススメ。



次はvoid loop( )で、これがC言語で言うところのint main( )内のメインループに相当する。
今回は以下のようにした。

void loop() {
  if(digitalRead(SEND_SW) == LOW){
    for (int i=0; i<8; i++){
      nec_custom = (nec_custom << 1) | digitalRead(CUSTOM[i]);
      nec_data   = (nec_data   << 1) | digitalRead(DATA[i]);
    }
    do{
      NecSend(~nec_custom, nec_data);
      delay(40);
    }
    while(digitalRead(SEND_SW) == LOW);
  }
}

PIC版と大きく違うのはnec_customとnec_dataの代入部分で、PICはポート丸ごと代入すればOKだったが、今回は自分で定義した配列を使用するためやり方が異なる。
この動作がよく分からなかったが、いろいろと実験した感じだとこのようになっていた。

例:CUSTOMが0x95(0b10010101)の場合
ループ1回目
           00000000 (nec_custom)
                <<1 (1bitシフト)
-------------------
           00000000 (シフト後)
or 0000000000000001 (bit7)
-------------------
           00000001 (演算結果)

ループ2回目
           00000001 (nec_custom)
                <<1 (1bitシフト)
-------------------
           00000010 (シフト後)
or 0000000000000000 (bit6)
-------------------
           00000010 (演算結果)

        ・
        ・
        ・

ループ8回目
           01001010 (nec_custom)
                <<1 (1bitシフト)
-------------------
           10010100 (シフト後)
or 0000000000000001 (bit0)
-------------------
           10010101 (演算結果)

簡単に説明すると、1bit左にシフトした後、スイッチの状態をORしている。
ちなみに、送信後に初期化を行っていないため、次回のループ開始時に前回の値が維持されている。
とはいえ、全部押し出されるので問題はない。
ここの落とし穴は、digitalReadの返り値がint(符号付き16bit整数)となっており、その値はHIGHだと0x0001、LOWだと0x0000になっている。
そのため、PICでやったように~digitalRead(CUSTOM[i])と記述すると、HIGHは0xFFFE、LOWだと0xFFFFになっってしまうため、ロータリースイッチの値を正常に読めなかった。
また、digitalRead(~CUSTOM[i])としても、なぜかよく分からないがビットがずれているような結果になっていた(そもそも文法的にNGじゃないの?と思うが)。
こことさっきのINPUT_PULLUPにハマり、なかなかうまく動いてくれなかった。



後はPIC版のIrREMlib.cに相当する各種関数を移植すれば完成。
基本的に、ディレイ関数とOutput関数をArduinoの関数に置き換えるだけでOKだった。
ディレイ時間は再調整する必要があり、特にLEDの点滅を制御するdigitalWrite関数は44サイクル(時間にして約4us)もかかる。
CPU系にありがちだけど、タイミング制御が必要な時はライブラリ関数使うより、レジスタを直接制御したりビット演算を駆使した方がいいということを再確認できた。




最後にスケッチ全体。
C言語だと、関数を定義前に呼び出すとエラーになるからプロトタイプ宣言する必要があるけど、Arduino言語では不要だった。

const uint8_t SEND_SW  = 2;
const uint8_t CUSTOM[] = {11, 9, 10, 12, 7, 5, 6, 8};
const uint8_t DATA[]   = {3, A1, A0, 4, A3, A5, A4, A2};
const uint8_t LED      = 13;

unsigned char nec_custom;
unsigned char nec_data;

void setup() {
  for(int i=0; i<8; i++){
    pinMode(CUSTOM[i], INPUT_PULLUP);
    pinMode(DATA[i],   INPUT_PULLUP);
    digitalWrite(CUSTOM[i], HIGH);
    digitalWrite(DATA[i],   HIGH);
  }
  pinMode(SEND_SW, INPUT_PULLUP);
  digitalWrite(SEND_SW, HIGH);
  pinMode(LED, OUTPUT);
}

void loop() {
  if(digitalRead(SEND_SW) == LOW){
    for (int i=0; i<8; i++){
      nec_custom = (nec_custom << 1) | digitalRead(CUSTOM[i]);
      nec_data   = (nec_data   << 1) | digitalRead(DATA[i]);
    }
    do{
      NecSend(~nec_custom, nec_data);
      delay(40);
    }
    while(digitalRead(SEND_SW) == LOW);
  }
}

void NecGenTon(void){
  int i;
  for(i=0; i<21; i++){
    digitalWrite(LED, HIGH);
    delayMicroseconds(6);
    digitalWrite(LED, LOW);
    delayMicroseconds(14);

  }
  delayMicroseconds(14);
}

void NecGenToff(void){
//  digitalWrite(LED, LOW);
  delayMicroseconds(562);
}

void NecGenBitOn(void){
  NecGenTon();
  NecGenToff();
  NecGenToff();
  NecGenToff();
}

void NecGenBitOff(void){
  NecGenTon();
  NecGenToff();
}

void NecSend(unsigned char custom, unsigned char data){
  int i;
  //Leader Code
  for(i=0; i<16; i++){
    NecGenTon();
  }
  for(i=0; i<8; i++){
    NecGenToff();
  }
  //Custom Code
  for(i=0; i<8; i++){
    if(custom>>i & 1){
      NecGenBitOn();
    }
    else{
      NecGenBitOff();
    }
  }
  //Custom Code(invert)
  for(i=0; i<8; i++){
    if(custom>>i & 1){
      NecGenBitOff();
    }
    else{
      NecGenBitOn();
    }
  }
  //Data Code
  for(i=0; i<8; i++){
    if(data>>i & 1){
      NecGenBitOn();
    }
    else{
      NecGenBitOff();
    }
  }
  //Data Code(invert)
  for(i=0; i<8; i++){
    if(data>>i & 1){
      NecGenBitOff();
    }
    else{
      NecGenBitOn();
    }
  }
  //Stop Bit
  NecGenTon();
}