レトロでハードな物語

レトロなゲーム機・マイコン・中古デバイスなどをArduinoやAVRで再活用する方法を模索しています。

エミュレータ用に作ったI2Cゲームコントローラー

2024年11月25日 | 電子工作

前回のゲームボーイエミュレータや以前に移植したゲームなどで使えて少ない配線でつなげられるゲームコントローラーが欲しかったので、I2C接続のコントローラーを作ってみました。

使用したマイコンはPIC16F819です。別に他のマイコンでもいいのですが、このPICを使った理由は数年前の秋月電子のお楽しみ袋に大量に入っていたので出来るだけ消費してしまいたかったからです。とはいえPIC16F819はレジスタ数が少なくシンプルなのにSPIやI2C(スレーブのみ)機能もあって使い勝手のいいPICです。今回のようなI2Cスレーブデバイスに向いているマイコンだと思います。

コントローラーのスティック部分は、以前にも使用したAE-JY-DIP(KIT)を使いました。アナログジョイスティックなのですが、簡単に扱えるように押した方向のみデータ出力するようにしました。でもアナログのデータが使えないのはもったいないので、モードを切り替えてアナログデータも出力できるようになっています。スティックのバーは長時間使っていると指先が痛くなってくるので、ノートPCのトラックポイント用のゴムキャップを取り付けました。

ボタンにはこれを使いました。タクトスイッチなのでゲーム向きかどうかは微妙なところですが、小さな基板に納めるのにびったりなサイズでした。

はずは配線から。電源はESP32からの3.3V。PIC16F819のスペックだと5Vのマイコンに接続しても動作すると思います。

[ボタンの配置]
D C
B A


スペースの都合でパスコンは裏側に配線

PICのプログラムはこちら。MPLAB X IDE v6.20でコンパイルしました。

main.c

  1. // PIC16F819 Configuration Bit Settings
  2.  
  3. // 'C' source line config statements
  4.  
  5. // CONFIG
  6. #pragma config FOSC = INTOSCIO // Oscillator Selection bits (INTRC oscillator; port I/O function on both RA6/OSC2/CLKO pin and RA7/OSC1/CLKI pin)
  7. #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
  8. #pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled)
  9. #pragma config MCLRE = ON // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is MCLR)
  10. #pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
  11. #pragma config LVP = OFF // Low-Voltage Programming Enable bit (RB3/PGM pin has digital I/O function, HV on MCLR must be used for programming)
  12. #pragma config CPD = OFF // Data EE Memory Code Protection bit (Code protection off)
  13. #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off)
  14. #pragma config CCPMX = RB2 // CCP1 Pin Selection bit (CCP1 function on RB2)
  15. #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)
  16.  
  17. // #pragma config statements should precede project file includes.
  18. // Use project enums instead of #define for ON and OFF.
  19.  
  20. #include
  21.  
  22. #define _XTAL_FREQ 8000000 // 8MHz
  23. #define I2CADDR 0xF
  24. #define BUFFER_SIZE 4
  25. #define XYSMAX 823
  26. #define XYSMIN 200
  27.  
  28. uint8_t skipreading;
  29. int stage = 0;
  30. uint8_t adHX, adLX, adHY, adLY;
  31. volatile uint8_t (*sendData)();
  32.  
  33. void getAN0() // Y軸
  34. {
  35.     ADCON0 = 0b01000001; // RA0を選択
  36.     __delay_us(20); // 充電待ち
  37.     // ADコンバート
  38.     ADCON0bits.GO = 1; // AD変換開始
  39.     while(ADCON0bits.GO); // AD変換終了待ち
  40. }
  41.  
  42. void getAN1() // X軸
  43. {
  44.     ADCON0 = 0b01001001; // RA1を選択
  45.     __delay_us(20); // 充電待ち
  46.     // ADコンバート
  47.     ADCON0bits.GO = 1; // AD変換開始
  48.     while(ADCON0bits.GO); // AD変換終了待ち
  49. }
  50.  
  51. uint8_t sendDigitalData()
  52. {
  53.     int adval;
  54.     uint8_t padData = 0;
  55.     
  56.     // X軸
  57.     getAN0();
  58.     adval = ADRESH;
  59.     adval <<= 8;
  60.     adval |= ADRESL;
  61.     if(adval < XYSMIN)
  62.         padData |= 1;
  63.     else if(adval > XYSMAX)
  64.         padData |= 0b10;
  65.     
  66.     // Y軸
  67.     getAN1();
  68.     adval = ADRESH;
  69.     adval <<= 8;
  70.     adval |= ADRESL;
  71.     if(adval < XYSMIN)
  72.         padData |= 0b1000;
  73.     else if(adval > XYSMAX)
  74.         padData |= 0b100;
  75.     
  76.     if(!PORTBbits.RB3)
  77.         padData |= 0b10000000;
  78.     if(!PORTBbits.RB2)
  79.         padData |= 0b1000000;
  80.     if(!PORTBbits.RB7)
  81.         padData |= 0b100000;
  82.     if(!PORTBbits.RB6)
  83.         padData |= 0b10000;
  84.     
  85.     return padData;
  86. }
  87.  
  88. uint8_t sendAnalogData()
  89. {
  90.     uint8_t padData = 0;
  91.     
  92.     switch(stage)
  93.     {
  94.         case 0:
  95.             // X軸
  96.             getAN0();
  97.             adHX = ADRESH;
  98.             adLX = ADRESL;
  99.             // Y軸
  100.             getAN1();
  101.             adHY = ADRESH;
  102.             adLY = ADRESL;
  103.             if(!PORTBbits.RB3)
  104.                 padData |= 0b10000000;
  105.             if(!PORTBbits.RB2)
  106.                 padData |= 0b1000000;
  107.             if(!PORTBbits.RB7)
  108.                 padData |= 0b100000;
  109.             if(!PORTBbits.RB6)
  110.                 padData |= 0b10000;
  111.             padData |= adHX << 2;
  112.             padData |= adHY;
  113.             stage = 1;
  114.             break;
  115.         case 1:
  116.             padData = adLX;
  117.             stage = 2;
  118.             break;
  119.         case 2:
  120.             padData = adLY;
  121.             stage = 0;
  122.             break;
  123.     }
  124.     
  125.     return padData;
  126. }
  127.  
  128. void __interrupt() isr(void)
  129. {
  130.     if (PIR1bits.SSPIF == 1) // I2C割込み発生
  131.     {
  132.         if(SSPSTATbits.R_W == 1) // マスターからの読み出し要求(スレーブは送信)
  133.             SSPBUF = (*sendData)(); // 送信データセット
  134.  
  135.         PIR1bits.SSPIF = 0 ; // 割込みフラグクリア
  136.         SSPCONbits.CKP = 1; // SCLラインを開放(通信再開)
  137.     }
  138.     else if(INTCONbits.INTF == 1) // RB0/INT割り込み
  139.     {
  140.         if(PORTBbits.RB0) // デジタルモード
  141.         {
  142.             sendData = sendDigitalData;
  143.             PORTAbits.RA7 = 0;
  144.             OPTION_REGbits.INTEDG = 0; // 立下りで割り込み
  145.         }
  146.         else // アナログモード
  147.         {
  148.             sendData = sendAnalogData;
  149.             PORTAbits.RA7 = 1;
  150.             OPTION_REGbits.INTEDG = 1; // 立上りで割り込み
  151.         }
  152.         INTCONbits.INTF = 0; // 割込みフラグクリア
  153.     }
  154. }
  155.  
  156. void main(void) {
  157.     OSCCONbits.IRCF = 0b111; // 周波数8MHz
  158.     CCP1CON = 0b00000000; // CCP1モジュール無効(リセット)
  159.     ADCON0 = 0b00000000;
  160.     ADCON1 = 0b11000100; // 数値は右揃え システムクロック1/2 Vref+使わない RA0/1/3はアナログ入力
  161.  
  162.     // I2C
  163.     SSPSTAT= 0b00000000;
  164.     SSPCON = 0b00110110; // SDA/SCLピンはI2Cスレーブモード 7ビットアドレス
  165.     SSPADD = I2CADDR << 1; // 7ビットアドレス
  166.     
  167.     // ポート設定
  168.     OPTION_REGbits.nRBPU = 0; // PORTBをプルアップ
  169.     TRISA = 0b00000011; // RA0/1入力
  170.     PORTA = 0b00000000;
  171.     TRISB = 0b11111111; // 全て入力 SDA/SCLピンはI2C
  172.     PORTB = 0b00000000;
  173.  
  174.     // 割り込み
  175.     if(PORTBbits.RB0) // デジタルモード
  176.     {
  177.         sendData = sendDigitalData;
  178.         PORTAbits.RA7 = 0;
  179.         OPTION_REGbits.INTEDG = 0; // 立下りで割り込み
  180.     }
  181.     else // アナログモード
  182.     {
  183.         sendData = sendAnalogData;
  184.         PORTAbits.RA7 = 1;
  185.         OPTION_REGbits.INTEDG = 1; // 立上りで割り込み
  186.     }
  187.     INTCONbits.INTE = 1; // RB0/INT割り込み許可
  188.     PIE1bits.SSPIE = 1; // I2C割り込み許可
  189.     INTCONbits.PEIE = 1; // 周辺装置割り込み許可
  190.     INTCONbits.GIE = 1; // 全割り込み許可
  191.     PIR1bits.SSPIF = 0; // I2C割り込みフラグクリア
  192.     
  193.     while(1)
  194.     {}
  195.     
  196.     return;
  197. }
  198.  

I2Cのアドレスは0xFにしてあります。配線にあるスイッチはI2Cの出力のデジタルモードとアナログモードの切り替えに使います。アナログモードになるとLEDが点灯します。それぞれのモードの時に出力されるデータの形式は次のようにしました。

部品は73mm×47mm基板に配置しましたが、むき出しでは使いにくいので100円ショップで売られていたミニチュア牛乳コンテナに納めました。

基板のサイズにぴったりですし、持った時にしっかりホールドできていい感じです。そのままケースに固定するとボタンを強く推したときに基板が少し沈むので、中に耐震シートを詰めて変形しないようにしています。

ESP32からの使用方法は前回のエミュレータのソースで確認してください。I2C端子はPIC内部でプルアップしてあるので、ESP32との接続にプルアップ抵抗は必要ありません。



最新の画像もっと見る

コメントを投稿