【遊戯王真DM】チートリモコンを作ってみた~ソフトウェア編②:キー操作~【封印されし記憶】

遊戯王真デュエルモンスターズ~封印されし記憶~のポケットステーションで強力なカードを入手するためのツール:チートリモコンを製作した。
今回はチートリモコンのソフトウェア設計②:キー操作を行う。




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




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




チートリモコンのキー操作は全部で8種類。

  1. RESETスイッチ        :CustomコードとDataコードを0x00にする。
  2. CUSTOMエンコーダー時計回り :Customコードをアップする。
  3. CUSTOMエンコーダー反時計回り:Customコードをダウンする。
  4. CUSTOMエンコーダースイッチ :Customコードの変化させる桁を変える。
  5. DATAエンコーダー時計回り   :Dataコードをアップする。
  6. DATAエンコーダー反時計回り  :Dataコードをダウンする。
  7. DATAエンコーダースイッチ   :Dataコードの変化させる桁を変える。
  8. SENDスイッチ         :LEDをNECフォーマットで点滅させる。


今回は、これらを操作する関数を作成する。




まずは必要な変数をまとめてみる。

int main() {
  // 変数定義
  int nec_cust_cur = 0; // Customコードの桁設定、0:一桁、1:十桁
  int nec_data_cur = 0; // Dataコードの桁設定、0:一桁、1:十桁

  unsigned char nec_custom = 0; // Customコード
  unsigned char nec_data   = 0; // Dataコード

  int lcd_status = 0; // コード変化フラグ、bit0:Custom、bit1:Data

  char NEC_CUSTOMT[2]; // Customコマンド用テキストデータ、2文字分
  char NEC_DATAT[2];   // Dataコマンド用テキストデータ、2文字分

PIC18Fの場合、intは16bitになる。
まぁ1~2bitしか使わないけど、こだわりがなければintにするようにしている。
nec_customとnec_dataがコードの変数で、符号なし8bitにしたかったのでunsigned charで定義した。
unsignedで定義しないときれいに値が変化しないので注意。




これらの変数を操作するため、keys.hというヘッダーを作成した。

#ifndef KEYS_H
#define	KEYS_H

// NEC Format
// RESETスイッチ
int  NecDataRst(unsigned char *custom, unsigned char *data);
// CUSTOMエンコーダー時計回り
int  NecCustCw(int cursor, unsigned char *custom);
// CUSTOMエンコーダー時計回り
int  NecCustCcw(int cursor, unsigned char *custom);
// CUSTOMエンコーダースイッチ
void NecCustSw(int *cursor);
// DATAエンコーダー時計回り
int  NecDataCw(int cursor, unsigned char *data);
// DATAエンコーダー反時計回り
int  NecDataCcw(int cursor, unsigned char *data);
// DATAエンコーダースイッチ
void NecDataSw(int *cursor);
// SENDスイッチ
void NecDataSend(unsigned char custom, unsigned char data);

#endif	/* KEYS_H */

KEYS_H関係は複数のファイルから同じヘッダーを読み込まないようにするための処置。
intで定義した関数はlcd_statusを変化させるもの、voidはその必要がないものと使い分けている。
関数内の変数定義で*があるのは参照渡しという手法で、変数の値ではなくアドレスを参照させ、関数内で値を変更することができる。
一方、*がないものは値渡しで、関数内で値を変更することは出来ない。
そのため、変数の値を変えるものは参照渡し、変えないものは値渡しと使い分けている。





この中身は後にして、とりあえずmain.cのwhileループに組み込んでみる。

  // Main Loop
  while(1){
    // Keys
    if(RST_SW == 0){
      lcd_status = NecDataRst(&nec_custom, &nec_data);
    }
    else if((CUST_A == 0) && (CUST_B == 1)){
      lcd_status = NecCustCw(nec_cust_cur, &nec_custom);
    }
    else if((CUST_A == 1) && (CUST_B == 0)){
      lcd_status = NecCustCcw(nec_cust_cur, &nec_custom);
    }
    else if(CUST_SW == 0){
      NecCustSw(&nec_cust_cur);
    }
    else if((DATA_A == 0) && (DATA_B == 1)){
      lcd_status = NecDataCw(nec_data_cur, &nec_data);
    }
    else if((DATA_A == 1) && (DATA_B == 0)){
      lcd_status = NecDataCcw(nec_data_cur, &nec_data);
    }
    else if(DATA_SW == 0){
      NecDataSw(&nec_data_cur);
    }
    else if(SEND_SW == 0){
      lcd_str(2, 1, "Send");
      NecDataSend(nec_custom, nec_data);
      lcd_str(2, 1, "    ");
    }
  }

変数の前の&は、アドレスを渡すという意味。
前回書いた通り、各スイッチは0が押した状態となる。
ロータリーエンコーダーは時計回りの場合はA側が先に0になり、反時計回りの場合はB側が先に0になる。
そのため、A側だけが押されていると時計回り、B側だけが押されていると反時計回りと判断するようにした。





ここからは、今回のメインであるキー操作の関数。
まず、RESETスイッチは以下のようにしてみた。

int  NecDataRst(unsigned char *custom, unsigned char *data){
  // 変数のリセット
  *custom = 0;
  *data   = 0;

  // RESETスイッチが戻るまで待ち
  while(RST_SW == 0);

  // 終了処理
  return 3;
}

コメントの通り、変数のリセット、スイッチが戻るまでの待ち、lcd_statusにフラグ返しの順に処理している。
returnを3にすることで、bit0(Customフラグ)とbit1(Dataフラグ)が同時に1になる。


次はCUSTOMエンコーダーの時計回り。

int  NecCustCw(int cursor, unsigned char *custom){
  // 値の変更
  switch(cursor){
    case 0  : *custom = *custom + 0x01; break;
    case 1  : *custom = *custom + 0x10; break;
    defoult : *custom = *custom + 0x01; break;
  }

  // エンコーダーが戻るまで待ち
  while(CUST_A == 0 || CUST_B == 0);

  // 終了処理
  return 1;
}

回転方向はmain.cで判別しているので、値を変更してエンコーダーが戻るのを待つだけ。
反時計回りの場合は+を-に変えればいい。
また、DATAエンコーダーも上記のコードをDATA用に変えるだけでOK。


CUSTOMエンコーダーのスイッチは以下の通り。

void NecCustSw(int *cursor){
  // 値の変更
  *cursor = *cursor + 1;
  if(*cursor >= 2) *cursor = 0;

  // スイッチが戻るまで待ち
  while(CUST_SW == 0);
}

これぐらいだったらもっといい書き方があるけど、当初はもっと桁が多かったりしたのでこのようにした。
DATAエンコーダーのスイッチもほぼ同じ作り。


SENDスイッチは以下のようにした。

void NecDataSend(unsigned char custom, unsigned char data){
  // スイッチが戻るまでループ
  while(SEND_SW == 0){
//    NecSend(custom, data);
    __delay_ms(40);
  }
}

NecSend関数はNECフォーマット編で作るので、今回はコメントにする。
SENDスイッチは押している間LEDを点滅し続ける予定なので、最初からwhileで押している間ループするようにした。
待ち時間の40msは、リモコンの送信間隔が108msとなっているため。
送信にかかる時間は計算上は68msぐらいのため、これで送信間隔は108msぐらいになる。
まぁ送信間隔が長くなっても問題ないはず。




もう1つ重要なのがLCDの表示。
まずは初期化と最初の文字出力。

  // LCD Init
  lcd_init();
  lcd_str(1, 1, "NEC  Custom Data");
  lcd_str(2, 1, "       0x00 0x00");

これは起動時にLCDの初期化と共に実行する。
Customコード、Dataコードの初期値は両方0x00のため固定文字にした。
これ以降、lcd_statusでコードが変化したかを判定し、数値だけを更新する。


という訳で、while(1){ }の無限ループ内にLCDの更新処理を追加する。
場所は上記キー操作の次。

    // LCD
    if(lcd_status & 1){
      sprintf(NEC_CUSTOMT, "%02X", nec_custom);
      lcd_str(2, 10, NEC_CUSTOMT);
    }
    if(lcd_status & 2){
      sprintf(NEC_DATAT, "%02X", nec_data);
      lcd_str(2, 15, NEC_DATAT);
    }
    lcd_status = 0;

if(lcd_status & 1)について説明すると、ifの( )内はtrueか0以外ならtrue判定となる。
&は各ビットのANDのため、lcd_statusのbit0が1なら結果は1となり、{ }内の処理が行われる。
sprintfは文字列を作る関数で、nec_customの値を16進数の文字データに変換し、NEC_CUSTOMTに格納する。
Dataコード側もほぼ同じ処理をしていて、lcd_statusのbit1で変化したかを判定する。
最後にlcd_statusをリセットし、変化があった時だけLCDを更新するようにした。




これらによって作ったソースコードコンパイルし、動作確認してみた。

分かってはいたけど、チャタリング防止処理を入れていないのでエンコーダーの変化量が安定しない。
チャタリング防止関係だけでもそれなりに長くなるので、次回に回す。




最後に、今回作成したソースコード一覧です。
pic18f25k22_config.hとLCDlib.hはソフトウェア編①を参照してください。

main.c

#include <stdio.h>
#include "xc.h"
#include "LCDlib.h"
#include "pic18f25k22_config.h"
#include "keys.h"

#define _XTAL_FREQ 40000000

// 文字列の置き換え
#define RST_SW  PORTAbits.RA0
#define CUST_A  PORTAbits.RA1
#define CUST_B  PORTAbits.RA2
#define CUST_SW PORTAbits.RA3
#define DATA_A  PORTCbits.RC0
#define DATA_B  PORTCbits.RC1
#define DATA_SW PORTCbits.RC2
#define SEND_SW PORTCbits.RC3
#define LED_ON  LATB1 = 1;
#define LED_OFF LATB1 = 0;

int main() {
  // 変数定義
  int nec_cust_cur = 0; // Customコードの桁設定、0:一桁、1:十桁
  int nec_data_cur = 0; // Dataコードの桁設定、0:一桁、1:十桁

  unsigned char nec_custom = 0; // Customコード
  unsigned char nec_data   = 0; // Dataコード

  int lcd_status = 0; // コード変化フラグ、bit1:Data、bit0:Custom

  char NEC_CUSTOMT[2]; // Customコード用テキストデータ、2文字分
  char NEC_DATAT[2];   // Dataコード用テキストデータ、2文字分

  // I/Oポート初期化
  ANSELA = 0x00; //全てデジタル
  ANSELB = 0x00; //全てデジタル
  ANSELC = 0x00; //全てデジタル
  TRISA  = 0xCF; //RA7-6,3-0が入力
  TRISB  = 0x00; //RB7-0が出力
  TRISC  = 0x0F; //RC3-0が入力
  LED_OFF;

  // LCD初期化
  lcd_init();
  lcd_str(1, 1, "NEC  Custom Data");
  lcd_str(2, 1, "       0x00 0x00");

  // Main Loop
  while(1){
    // Keys
    if(RST_SW == 0){
      lcd_status = NecDataRst(&nec_custom, &nec_data);
    }
    else if((CUST_A == 0) && (CUST_B == 1)){
      lcd_status = NecCustCw(nec_cust_cur, &nec_custom);
    }
    else if((CUST_A == 1) && (CUST_B == 0)){
      lcd_status = NecCustCcw(nec_cust_cur, &nec_custom);
    }
    else if(CUST_SW == 0){
      NecCustSw(&nec_cust_cur);
    }
    else if((DATA_A == 0) && (DATA_B == 1)){
      lcd_status = NecDataCw(nec_data_cur, &nec_data);
    }
    else if((DATA_A == 1) && (DATA_B == 0)){
      lcd_status = NecDataCcw(nec_data_cur, &nec_data);
    }
    else if(DATA_SW == 0){
      NecDataSw(&nec_data_cur);
    }
    else if(SEND_SW == 0){
      lcd_str(2, 1, "Send");
      NecDataSend(nec_custom, nec_data);
      lcd_str(2, 1, "    ");
    }

    // LCD
    if(lcd_status & 1){
      sprintf(NEC_CUSTOMT, "%02X", nec_custom);
      lcd_str(2, 10, NEC_CUSTOMT);
    }
    if(lcd_status & 2){
      sprintf(NEC_DATAT, "%02X", nec_data);
      lcd_str(2, 15, NEC_DATAT);
    }
    lcd_status = 0;
  }

  return (EXIT_SUCCESS);
}


keys.h

#ifndef KEYS_H
#define	KEYS_H

// NEC Format
// RESETスイッチ
int  NecDataRst(unsigned char *custom, unsigned char *data);
// CUSTOMエンコーダー時計回り
int  NecCustCw(int cursor, unsigned char *custom);
// CUSTOMエンコーダー反時計回り
int  NecCustCcw(int cursor, unsigned char *custom);
// CUSTOMエンコーダースイッチ
void NecCustSw(int *cursor);
// DATAエンコーダー時計回り
int  NecDataCw(int cursor, unsigned char *data);
// DATAエンコーダー反時計回り
int  NecDataCcw(int cursor, unsigned char *data);
// DATAエンコーダースイッチ
void NecDataSw(int *cursor);
// SENDスイッチ
void NecDataSend(unsigned char custom, unsigned char data);

#endif	/* KEYS_H */


keys.c

#include "xc.h"

#define RST_SW  PORTAbits.RA0
#define CUST_A  PORTAbits.RA1
#define CUST_B  PORTAbits.RA2
#define CUST_SW PORTAbits.RA3
#define DATA_A  PORTCbits.RC0
#define DATA_B  PORTCbits.RC1
#define DATA_SW PORTCbits.RC2
#define SEND_SW PORTCbits.RC3

#define _XTAL_FREQ 40000000



int  NecDataRst(unsigned char *custom, unsigned char *data){
  // 変数のリセット
  *custom = 0;
  *data   = 0;

  // RESETスイッチが戻るまで待ち
  while(RST_SW == 0);

  // 終了処理
  return 3;
}

int  NecCustCw(int cursor, unsigned char *custom){
  // 値の変更
  switch(cursor){
    case 0  : *custom = *custom + 0x01; break;
    case 1  : *custom = *custom + 0x10; break;
    defoult : *custom = *custom + 0x01; break;
  }

  // エンコーダーが戻るまで待ち
  while(CUST_A == 0 || CUST_B == 0);

  // 終了処理
  return 1;
}

int  NecCustCcw(int cursor, unsigned char *custom){
  // 値の変更
  switch(cursor){
    case 0  : *custom = *custom - 0x01; break;
    case 1  : *custom = *custom - 0x10; break;
    defoult : *custom = *custom - 0x01; break;
  }

  // エンコーダーが戻るまで待ち
  while(CUST_A == 0 || CUST_B == 0);

  // 終了処理
  return 1;
}

void NecCustSw(int *cursor){
  // 値の変更
  *cursor = *cursor + 1;
  if(*cursor >= 2) *cursor = 0;

  // スイッチが戻るまで待ち
  while(CUST_SW == 0);
}

int  NecDataCw(int cursor, unsigned char *data){
  // 値の変更
  switch(cursor){
    case 0  : *data = *data + 0x01; break;
    case 1  : *data = *data + 0x10; break;
    defoult : *data = *data + 0x01; break;
  }

  // エンコーダーが戻るまで待ち
  while(DATA_A == 0 || DATA_B == 0);

  // 終了処理
  return 2;
}

int  NecDataCcw(int cursor, unsigned char *data){
  // 値の変更
  switch(cursor){
    case 0  : *data = *data - 0x01; break;
    case 1  : *data = *data - 0x10; break;
    defoult : *data = *data - 0x01; break;
  }

  // エンコーダーが戻るまで待ち
  while(DATA_A == 0 || DATA_B == 0);

  // 終了処理
  return 2;
}

void NecDataSw(int *cursor){
  // 値の変更
  *cursor = *cursor + 1;
  if(*cursor >= 2) *cursor = 0;

  // スイッチが戻るまで待ち
  while(DATA_SW == 0);
}

void NecDataSend(unsigned char custom, unsigned char data){
  // スイッチが戻るまでループ
  while(SEND_SW == 0){
//    NecSend(custom, data);
    __delay_ms(40);
  }
}