まとめ記事のご案内 [2024.11.20]
こちらの記事に前編と後編の記事内容をまとめましたので、こちらをおすすめします。
私、昔はクソみたいなスロカス学生でして、大学に行かずパチスロばかり行っておりました。
おかげで(そのせいで)留年もしております。(๑•﹏•)
その当時、私を泥沼に嵌めてくれた思い入れのある機種(実機)を先日手に入れました。

『パチスロ 新世紀エヴァンゲリオン〜まごころを、君に〜』です。
しかもアスカパネルVer.です。(◔‿◔)
日頃、暇なときにちょこちょこ打ってたんですが、やっぱり『データカウンター』がないと「今何ゲームなのか」「どのくらい出ているのか」がわかりません……。
ちなみに、データカウンターというのはパチンコホールでよく見かけるゲーム履歴を表示している機器のことです。

なので、今回はデータカウンターを作ってみます。ただ、今回はその前段階として、実機データのPCへの取り込みを検証していきます。

これが実は意外と簡単な仕組みなんですよね
データカウンターの仕組み
データカウンターには、ビッグボーナス(BB)/レギュラーボーナス(RB)の回数やスランプグラフが表示されているのが普通ですが、これはどのようにして実現しているのでしょうか。
その仕組みを覗いてみましょう。
パチスロ実機の内部には以下のような基板があります。

パチンコホールでは、この基板とお店のデータカウンターが接続されています。
そして、この基板上のリレー(黒いやつ)を電気信号でON/OFFさせることで、データカウンターへボーナス当選やコインのIN/OUT枚数を伝えています。
要は、パチスロの実機からはスイッチによってHigh(電源電圧)かLow(GND)の電気信号をデータカウンターに出しているだけなんです。
市販のデータカウンターはこの信号を検出してスランプグラフを出したり、ボーナス回数を表示したりしているのです。

動作解説
この電気信号について、もう少し詳しく解説します。
例えば、レバーオンした際にはメダル入力を表すINのリレーが「カチッ、カチッ、カチッ」と3回ONし、Arduinoへの入力電圧が下図のように+5V→0V→+5V→0V→+5V→0V→+5Vとなります。(メダル3枚がけですから。)

そして、(ベルなどの)入賞時には払い出しメダル枚数だけOUTのリレーがカチカチします。
その他にボーナスに当選した場合は、ボーナス図柄を揃えたときにリレーがONします。
構成
一般的なデータカウンターの仕組みが分かったところで、次はデータカウンターを自作するにあたっての構成を考えていきます。
単純な電気信号のHigh、Lowを検出してカウントできればOKなので、今回は実機のデータ送信基板にArduinoを接続し、Arduino経由でPCにデータを取り込もうと思います。
なお、Arduinoは実機内部に入れるので、なるべく小さいやつということで『Arduino nano every』を購入しました。
ちなみに、ウチの実機に付いているデータ送信基板のピンヘッダは以下のような配置になっています。

ただ、この基板、実は単に電磁リレーが付いているだけなので、外部のプルアップ回路から電源を入力してあげる必要があります。
そのようなことから、構成図はこんな感じになりました。

実物はこんな感じになります。

なお、Arduino nano every側とデータ送信基板側のピン対応は以下の通りです。
Arduino nano every側 | ピンNo. | データ送信基板側 | ピンNo. |
---|---|---|---|
D6 | 24 | IN | 1 |
D5 | 23 | OUT | 2 |
D4 | 22 | RB | 3 |
D3 | 21 | BB | 4 |
ソフトウェア開発
データ送信基板からデータをもらう準備ができましたので、Arduinoでデータを収集するためのプログラムを作っていきます。
なお、Visual Studio CodeでのArduino開発環境の構築の仕方は以下の記事をご参照ください。
フォルダ構成は、incフォルダに全てのヘッダファイル(.hファイル)、直下にソースファイル(.inoファイルと.cppファイル)となっています。
Arduinoのメインファイルは「.ino」という拡張子のファイルになっています。
それ以外にC言語のソースファイルを追加したい場合は拡張子を「.cpp」にする必要があります。

これらファイルの詳細は以下の通りです。
フォルダ | ファイル名 | 内容 |
---|---|---|
.vscode | arduino.json | Arduinoボードなどの設定を記述したファイル |
c_cpp_properties.json | C++の拡張機能が自動生成するファイル | |
settings.json | VSCodeの設定ファイル | |
inc | DataManager.h | データ管理機能のヘッダファイル |
Interrupt.h | 割り込み機能のヘッダファイル | |
PinDefine.h | ピン定義のヘッダファイル | |
Port.h | ポート設定のヘッダファイル | |
Serial_Com.h | シリアル通信設定のヘッダファイル | |
vtype.h | 型定義のヘッダファイル | |
ー(フォルダ直下) | DataManager.cpp | データ管理機能のソースファイル |
Interrupt.cpp | 割り込み機能のソースファイル | |
Pachislot_DataGet.ino | Arduinoのメインファイル | |
Port.cpp | ポート設定のソースファイル | |
Serial_Com.cpp | シリアル通信設定のソースファイル |
C言語でいうmain()的な役割を行うのが「.inoファイル」です。Arduinoのプログラムは.inoファイルから始まります。
.inoファイルに書いた初期化を行う関数『setup()』と、無限ループで延々繰り返し処理を行う関数『loop()』で基本的な処理が行われます。
ただ、今回はArduinoへの入力信号が変わったときに『IN枚数を更新したり』、『ボーナス回数を増やしたり』したいので、入力信号を外部割込みとして機能させて割込み発生時に処理を行いたいと思います。
それではソースコードを具体的に見ていきましょう。
Pachislot_DataGet.ino [メインファイル]
Pachislot_DataGet.inoでは、setup()に各機能の初期化を行う関数をそれぞれ呼び出しています。
一方のloop()では何もしません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * ======================================================= * @fn setup * @brief 初期化を行う * @date 2024-06-08 * ======================================================= */ void setup( void ) { Port_Init( ); // ポートを初期化する Intr_Init( ); // 割り込み機能を初期化する Serial_Init( ); // シリアル通信を初期化する Data_Init( ); // データ管理を初期化する } /** * ======================================================= * @fn loop * @brief 繰り返し処理を行う * @date 2024-06-08 * ======================================================= */ void loop( void ) { // メインループでは何もしない } |
Port.cpp
Port_Init()では、ArduinoのライブラリにあるpinMode()関数を使ってポートの設定をしています。
ここで、第1引数にはデジタル入力の番号を設定するのですが、INの入力はデジタル入力のD6なので、IN_PINを『6』と定義のうえ設定しています。
同様にOUT_PINなら『5』になります。
そして、第2引数には入力モードか出力モードかを設定するのですが、今回は外部割込みの入力となるので『INPUT』と設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * ======================================================= * @fn Port_Init * @brief ポートの初期設定を行う * @date 2024-06-08 * ======================================================= */ void Port_Init( void ) { pinMode( IN_PIN, INPUT ); // 6番ピンを"IN"信号の入力モードに設定する pinMode( OUT_PIN, INPUT ); // 5番ピンを"OUT"信号の入力モードに設定する pinMode( RB_PIN, INPUT ); // 4番ピンを"RB"信号の入力モードに設定する pinMode( BB_PIN, INPUT ); // 3番ピンを"BB"信号の入力モードに設定する } |
Serial_Com.cpp
Serial_Init()
Serial_Init()ではシリアル通信の初期化を行います。
ここでは、Serialクラスのbegin()メソッドを使用してボーレートを『9600bps』に設定しています。
Serial_Write()
Serial_Write()では、Serialクラスのprint()メソッドとprintln()メソッドを使用してPCへシリアル通信で文字列を送信しています。
print()で”IN枚数:”を送り、次にprintln()で”1”を送ります。
println()は最後に改行を入れてくれるので、以下のような表示になります。
1 2 3 |
IN_Coin:1 IN_Coin:2 … |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * ======================================================= * @fn Serial_Init * @brief シリアル通信の初期設定を行う * @date 2024-06-08 * ======================================================= */ void Serial_Init( void ) { Serial.begin( 9600 ); // PCとのシリアル通信ボーレートを9600bpsに設定 } /** * ======================================================= * @fn Serial_Write * @brief シリアル通信でPCにメッセージを送信する * @param pMsg メッセージ前文 * @param pNum 数値 * @date 2024-06-12 * ======================================================= */ void Serial_Write( const char pMsg[], const uint32 pNum ) { Serial.print( pMsg ); // メッセージ前文 Serial.println( pNum ); // 数値 } |
Interrupt.cpp
Intr_Init()
Intr_Init()では割り込み機能の初期化を行います。
ここでは、attachInterrupt()関数で外部入力による割り込みを登録します。
attachInterrupt()の第1引数には割り込み番号が必要なので、digitalPinToInterrupt()関数でデジタル入力番号を割り込み番号に変換しています。
また、第2引数には割り込み発生時に呼び出す関数(コールバック関数)を設定します。
最後に、第3引数には割り込みのきっかけを設定しますので、立下がりエッジを表す『FALLING』を設定します。
in_intr_occur()
このコールバック関数は、IN枚数が1枚増えたときの外部割込みによって呼び出されます。
あまり他の割込みと同時に発生する(多重割込み)ことはないと思いますが、一応noInterrupts()で割込み禁止にします。
次に、DataManager.cppのGet_IN_Coin()で現在のIN枚数を取得します。
そして、これを+1したうえでSet_IN_Coin()でIN枚数を更新します。
あとはSerial_Com.cppのSerial_Write()でPCへデータを送信します。
最後にinterrupts()で割込みを許可に戻します。
※他のout_intr_occur()などについてもほぼ同じなので省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// ======================================================= // ローカル関数 // ======================================================= static void in_intr_occur( void ); static void out_intr_occur( void ); static void rb_intr_occur( void ); static void bb_intr_occur( void ); /** * ======================================================= * @fn Intr_Init * @brief 割り込み関係を初期化する * @date 2024-06-11 * ======================================================= */ void Intr_Init( void ) { // 3番~6番のピンを外部割込みに設定する // 割り込み発生は立ち下がりエッジ(FALLING)発生時とする attachInterrupt( digitalPinToInterrupt( IN_PIN ), in_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( OUT_PIN ), out_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( RB_PIN ), rb_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( BB_PIN ), bb_intr_occur, FALLING ); } /** * ======================================================= * @fn in_intr_occur * @brief INの立ち下がりエッジ発生時にINを+1する * @date 2024-06-10 * ======================================================= */ static void in_intr_occur( void ) { noInterrupts( ); // 他の割り込みを禁止する uint32 l_curr_in; l_curr_in = Get_IN_Coin( ); // IN枚数を取得する l_curr_in++; // 現在のIN枚数に+1する Set_IN_Coin( l_curr_in ); // IN枚数を更新する Serial_Write( "IN_COIN:", l_curr_in ); // 更新後のIN枚数をシリアル通信でPCへ送る interrupts( ); // 他の割り込みを許可する } |
DataManager.cpp
静的変数
DataManager.cppで管理しておくデータを宣言しています。
C言語はオブジェクト指向の言語ではないものの、DataManager.cpp外から好き勝手にこの変数にアクセスして欲しくないので、『static』で宣言することで隠蔽しています。
そのため、例えばmIN_CoinにアクセスするときはGet_IN_Coin()かSet_IN_Coin()を介する必要があります。(いわゆるgetter、setterというやつです。)
Data_Init()
Data_Init()ではデータの初期化を行います。なので、すべての変数がゼロリセットされます。
Get_IN_Coin()
Get_IN_Coin()はいわゆるgetterです。単純に現在のmIN_Coinの値を返すだけです。
Set_IN_Coin()
Set_IN_Coin()はいわゆるsetterです。ここでは一度0以上かどうかを確認しています。しかし、符号なし型の変数なので正直あまり意味はないです。ただ、このようなsetterの形をとることで、プログラムによっては不正な値への更新やバグの作り込みの防止が期待できます。
※他のGet_OUT_Coin()などについてもほぼ同じなので省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// ======================================================= // 静的変数 // ======================================================= static uint32 mIN_Coin; static uint32 mOUT_Coin; static uint32 mRB; static uint32 mBB; /** * ======================================================= * @fn Data_Init * @brief データ管理の初期化を行う * @date 2024-06-10 * ======================================================= */ void Data_Init( void ) { mIN_Coin = 0U; mOUT_Coin = 0U; mRB = 0U; mBB = 0U; } /** * ======================================================= * @fn Get_IN_Coin * @brief 入力枚数を取得する * @date 2024-06-10 * ======================================================= */ uint32 Get_IN_Coin ( void ) { return mIN_Coin; } /** * ======================================================= * @fn Set_IN_Coin * @brief 入力枚数を設定する * @date 2024-06-10 * ======================================================= */ void Set_IN_Coin( uint32 pIN_Coin ) { if ( pIN_Coin >= 0U ) { mIN_Coin = pIN_Coin; } } |
動作確認
作ったプログラムを動かしてみます。


カウントしすぎてます・・・おかしいな・・・
3枚掛けなのでレバーオン時にはまず『IN_COIN:3』までログが出るはずが、『IN_COIN:9』までカウントしてますね…。
もしやチャタリングを起こしているのではと思い、Arduinoの入力波形を測定してみました。
ぱっと見は問題なさそうですが…。

拡大してみると…

ほらね・・・。(´;ω;`)
電磁リレーと言えど、機械的なリレーではよく起こることです。
やはりチャタリングによって、余計な外部割込みが発生してカウントし過ぎていました。
ソフトウェア修正
電磁リレーのチャタリングを無視する必要があるので、一度割込みを発生させたら一定時間以内に同じ割込みが発生してもカウンティングを行わないようにします。
ローカル変数
前回外部割込みが発生した時の時間記録用の変数を追加しました。
Intr_Init()
時間記録用の変数の0リセットを追加しました。
in_intr_occur()
前回の割込みからの経過時間を計算し、時間がまだ経過していなければ何も処理しないように修正しました。
このときのINTR_WAIT[ms]は80[ms]に設定しています。
理由としては、信号の立下がった後の戻るときにもチャタリングをしてしまうからです。
したがって、次の信号立下がりが発生する直前(80[ms]くらい)を狙っています。
また、時間記録用の変数は、割込みが入ってちゃんとカウントされたときにのみ更新されるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// ======================================================= // ローカル変数 // ======================================================= static ulong64 mIN_PrevTime; static ulong64 mOUT_PrevTime; static ulong64 mRB_PrevTime; static ulong64 mBB_PrevTime; /** * ======================================================= * @fn Intr_Init * @brief 割り込み関係を初期化する * @date 2024-06-11 * ======================================================= */ void Intr_Init( void ) { // 3番~6番のピンを外部割込みに設定する // 割り込み発生は立ち下がりエッジ(FALLING)発生時とする attachInterrupt( digitalPinToInterrupt( IN_PIN ), in_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( OUT_PIN ), out_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( RB_PIN ), rb_intr_occur, FALLING ); attachInterrupt( digitalPinToInterrupt( BB_PIN ), bb_intr_occur, FALLING ); mIN_PrevTime = 0U; mOUT_PrevTime = 0U; mRB_PrevTime = 0U; mBB_PrevTime = 0U; } /** * ======================================================= * @fn in_intr_occur * @brief INの立ち下がりエッジ発生時にINを+1する * @date 2024-06-10 * ======================================================= */ static void in_intr_occur( void ) { ulong64 l_interval; uint32 l_curr_in; l_interval = millis() - mIN_PrevTime; // 前回の割込みからの経過時間を計算する if ( l_interval >= INTR_WAIT ) // 割込み待ち時間がINTR_WAIT[ms]を超えていたら { noInterrupts( ); // 他の割り込みを禁止する l_curr_in = Get_IN_Coin( ); // IN枚数を取得する l_curr_in++; // 現在のIN枚数に+1する Set_IN_Coin( l_curr_in ); // IN枚数を更新する Serial_Write( "IN_COIN:", l_curr_in ); // 更新後のIN枚数をシリアル通信でPCへ送る mIN_PrevTime = millis( ); // 割込み実施時間を更新する interrupts( ); // 他の割り込みを許可する } } |
本記事に掲載したソースコードは一部を抜粋したものになっていますので、全ソースコードが見たい方は、以下のGitHubのリポジトリをご参照ください。
動作確認(再)

レバーオンしたときにIN枚数が、小役入賞時にOUT枚数が増えています。特に問題なさそうですね。(^_-)-☆
(あと、ボーナスもちゃんとカウントしてくれていました。)
ということで、とりあえず自作データカウンターの前哨戦は完了。
次回は、PC側のデータカウンターアプリケーションを作っていきます。