chibimameのアイコン画像
chibimame 2025年01月31日作成 © GPL-3.0+
製作品 製作品 Lチカ Lチカ 閲覧数 73
chibimame 2025年01月31日作成 © GPL-3.0+ 製作品 製作品 Lチカ Lチカ 閲覧数 73

何回続くかな?卓球のラリーカウンター

概要

卓球は、試合形式でなく、ただの打ち合いだけでも楽しいものがあります。この打ち合いが、代わる代わる長く続くと、練習の励みになります。しかし、打ち合いをしながらこの回数を数えるのも結構面倒なもので、大抵は「今のは、長く続いたなぁ。」で終わってしまいます。そこで、何回続いたとわかるように打ち合いの回数を観測する道具をSPRESENSEを使用して製作してみました。
やり方は、左右の卓球台に音検出装置を置き、台への打撃音をカウントし、このカウント回数を4桁の7セグメントLEDに表示するようにします。通常、卓球台は、しまうために折りたためるようになっており、このため右と左の台は、分離されています。これを利用して左右の台個別に打撃音をカウントし、表示します。

キャプションを入力できます

部品

部品番号 型番 数量
SPRESENSEメインボード 1
JK11 MJ-355 1
LD1、LD2 LB-602 2
IC1 LM358N 1
Q1、Q2 2SK170 2
R1~R4 100 4
R5 10K 1
R6~R9 100 4
R10、R11、R14 10K 3
R12、R13 10K 2
R15 10 1
R16、R17 1M 2
TR1~TR4 2SA1015 4

回路図

大きく7セグメントLED部と打撃音検出部に分けられます。

  • 7セグメントLED部
    二つの2桁7セグメントLEDで構成されています。メインボードのD14~D21端子を7セグメントLEDの要素の制御に使用し、D23~D26端子をダイナミック点灯用の4桁の制御に使用しています。7セグメントLED点灯は、ダイナミック点灯方式で、アノードコモンでPNP(2SA1015)トランジスタで4桁の桁指定をします。
    普通のダイナミック点灯方式で、年内には、数字ぐらい出せるだろうと高をくくっていましたが、年が明けても数字が出せませんでした。全部の桁が同時に全点灯してしまうのです。「え、なんで?」とテスターで調べましたが、さっぱりわかりません。改めてSPERESENSEの説明資料を読み返し、メインボードの場合のデジタルピンのHIGHは、3.3Vではなく1.8Vとのこと。3.3Vをエミッタに供給しているのでベースに1.8Vでは、トランジスタがオフになりきらない様子です。
    試しにエミッタの供給電圧を1.8Vにしたらきちんと各桁に数字が表示されました。ロジックはあっていましたが、しかし、暗いです。セグメントの制御に接続している100Ωを試しにショートさせてみましたが、暗いです。しかたなく3.3Vに戻しましたが、全点灯で振り出しに戻るという感じでした。
    最終的に苦肉の策として3.3Vとエミッタの間に10Ωの抵抗をいれて、数字の点灯ができるようになりました。メインボードだけで、正式な点灯方式がわかれば、改善していきたいと思います。
  • 打撃音検出部
    圧電スピーカをマイク替わりに使用し、FET入力した打撃音をOPアンプのレベルコンパレータで整形します。SPRESENSEへは、パルスとして割り込み検出されます。

キャプションを入力できます

基板実装

基板の写真を以下に示します。95×72mmの片面ユニバーサル基板にSPRESENSEメインボード接続用のピンヘッダーと2桁7セグメントLED2つを実装しています。基板は、ケースに入るよう一部カットしました。
半田面配線は、2桁7セグメントLEDの桁間の端子の接続と2桁7セグメントLED間の接続があったため、配線が入り乱れ、拙い電子工作の極みですが、桁ごとにきちんと表示はしています。

キャプションを入力できます

基板、SPRESENSEをとったところ
キャプションを入力できます

打撃音検出

ラリーカウンターとして人の声やラケットにあたる音は、雑音になりカウント値が誤る原因になります。そのため、卓球台の打撃音だけを検出する必要があり、マイクを卓球台に直接接触させ、台の音だけを検出するようにします。また、マイクは、コンデンサマイクなど高感度であると声やラケット音の雑音を拾う結果になりますので、圧電スピーカをマイク替わりにして、台にクリップでとめることにしました。この様子を以下の写真で示します。通常、左右の卓球台は、分離されていますので、右用、左用の圧電スピーカを片方の台のネット支柱付近でとめています。

キャプションを入力できます

動作仕様

単に左右の卓球台の打撃音をラリーの回数として数えればいいと簡単に考えていましたが、意外と仕様が複雑になりました。まず、開始と終了の判断として以下のようにしました。

  • 開始
    交互に打撃音が続く想定ですが、X秒以内は、サーブと判断し、カウントを開始する
    通常のラリーでは、卓球台にあたってからラケットにあたって相手の卓球台にボールが当たります。サーブの場合は、ラケットを経由せず、台から台へボールが行くため、打撃音の時間間隔が短くなります。この短い時間の打撃音の移動をサーブと判断し、ラリー開始とします。
  • 終了
    終了の判断は、以下のようにします。
    -- 同じ側の卓球台で打撃音を検出したら、カウントを止める
    -- 打撃音がY秒以上なかったら、ラリー終了と判断し、カウントを止める

※X秒は、5秒、Y秒は、10秒と仮定していますが、適正値は、実際の試験を繰り返して決める予定です。

表示に関しては、4桁あるLEDの両端(左から1桁目と4桁目)に打撃音が入った場合のマーク用に表示し、間の2桁に現在のカウント数を表示します。ラリーがノーカウントとなった場合、全桁にハイフォンを表示します。
キャプションを入力できます

ソースコード

ソースコードを以下に示します。

  • 7セグメントLED関連
    7セグメントLEDのエレメントにD14~D21ピン、桁位置をD23~D26ピンを割り振りました。SetValue関数で表示数字を設定し、DispValue関数で桁を表示させます。DispValue関数がloop処理で繰り返し呼ばれるごとに表示桁がずれて4桁のダイナミック点灯になります。
    SetValue2関数は、ラリー中は、数字を中央の2桁目、3桁目に表示させるものです。
  • 打撃検出関連
    右D27、左D28として、立ち上がりエッジを各々D27_Interrupt_cb、D28_Interrupt_cbというコールバック関数で検出させます。検出時、右か左がわかるように7セグメントLEDの左から1桁目と4桁目にマークを表示させます。
  • 状態監視
    StateCheck関数は、状態を監視し、ラリーカウントを行うか、初期化するかを判断します。状態は以下の3種類です。
    -- 0:リセット状態(サーブ開始待ち)
    -- 1:サーブ完了待ち
    -- 2:カウント中
  • タイマー監視
    X秒以内のサーブかどうかの判定やY秒以上の判定に使います。また、将来的には、1回のラリーの時間を計測し、1打あたりの時間を表示させたいと思っています。

7セグメントLEDのラリーカウンター

```cpp /******************************************************************** 作成日 : 2025/1/28 作成者 : 笠木 信司 説明 : 7セグメントLEDの制御を行う ********************************************************************/ #define DETECT_COUNT (30) // LRの音を検出してLR表示を続けるカウント static unsigned int uc100msecCount = 0; static unsigned char ucCounter = 0; static unsigned int uiMaxCounter = 0; static unsigned char ucDispTiming = 0; // 打検出表示など数字以外を表示するタイミング static unsigned char ucDispCounter = 0; // サーブ完了までの間、前の回の数字を表示するときの表示用カウンダ static unsigned char ucFlagLR = 0; // 左:0ビット目 右:1ビット目 static unsigned char ucStage = 0; // 0:リセット状態(サーブ開始待ち) // 1:サーブ完了待ち // 2:カウント中 static unsigned char ucDispVal[4]; // 右 void D27_Interrupt_cb(){ //ucCounter++; ucDispVal[0] = 0xFF; ucDispVal[1] = 0xFF; ucDispVal[2] = 0xFF; ucDispVal[3] = 0xF0; ucFlagLR = 0x02; ucDispTiming = DETECT_COUNT; } // 左 void D28_Interrupt_cb(){ //ucCounter++; ucDispVal[0] = 0xF0; ucDispVal[1] = 0xFF; ucDispVal[2] = 0xFF; ucDispVal[3] = 0xFF; ucFlagLR = 0x01; ucDispTiming = DETECT_COUNT; } // 100msタイマー割り込み void TimerInterrupt_cb(){ uc100msecCount++; } /* DIG1:D23 DIG2:D24 DIG3:D25 DIG3:D26 A A :D15 1 ---- B :D14 0 F / G / B C :D18 4 ---- D :D21 7 E / / C E :D20 6 ---- . DP F :D16 2 D G :D17 3 DP :D19 5 */ const unsigned char ucDispCode[10] = { 0x28, // 0 0xEE, // 1 0x34, // 2 0x64, // 3 0xE2, // 4 0x61, // 5 0x21, // 6 0xEC, // 7 0x20, // 8 0xE0 // 9 //0x00 // 表示無し }; // 小数点のドットを表示する void SetDot( unsigned char ucDotPos){ ucDispVal[ucDotPos] |= 0x10; } void SetHyphen(){ for(int i=0; i<4; i++){ ucDispVal[i] = 0xF7; } } void SetValue( unsigned int uiLedVal){ unsigned char i; // 上位桁を表示させないため for(i=0; i<3 ;i++){ ucDispVal[i] = 0xFF; } for(i=0; i<4 ;i++){ ucDispVal[3-i] = ucDispCode[uiLedVal%10]; uiLedVal /= 10; if(uiLedVal==0) break; } } // 数字だけ2桁表示 void SetValue2( unsigned int uiLedVal){ unsigned char i; // 上位桁を表示させないため for(i=1; i<2 ;i++){ ucDispVal[i] = 0xFF; } for(i=1; i<3 ;i++){ ucDispVal[3-i] = ucDispCode[uiLedVal%10]; uiLedVal /= 10; if(uiLedVal==0) break; } } static unsigned char ucDigit=0; void DispValue( void ){ unsigned int i; unsigned char dispCode = 0; /* DIG1:D23、DIG2:D24、DIG3:D25、DIG3:D26 */ dispCode = ucDispVal[ucDigit]; //delay(1); for(i=0; i<8; i++){ if((dispCode>>i)&0x01){ digitalWrite(PIN_D14+i, HIGH); }else{ digitalWrite(PIN_D14+i, LOW); } } //前の桁の数字が残るため一旦表示を消す。 for(i=0; i<4; i++){ if(ucDigit == i){ digitalWrite(PIN_D23+i, LOW); }else{ digitalWrite(PIN_D23+i, HIGH); } } delay(5); ucDigit++; if(ucDigit > 3) ucDigit = 0; } void setup() { //start serial connection Serial.begin(115200); while (!Serial); Serial.printf("Start setup\r\n"); // 7 Segment LED Element pinMode(PIN_D14, OUTPUT); pinMode(PIN_D15, OUTPUT); pinMode(PIN_D16, OUTPUT); pinMode(PIN_D17, OUTPUT); pinMode(PIN_D18, OUTPUT); pinMode(PIN_D19, OUTPUT); pinMode(PIN_D20, OUTPUT); pinMode(PIN_D21, OUTPUT); Serial.printf("OUTPUT1\r\n"); // 7 Segment LED Degit pinMode(PIN_D23, OUTPUT); pinMode(PIN_D24, OUTPUT); pinMode(PIN_D25, OUTPUT); pinMode(PIN_D26, OUTPUT); Serial.printf("OUTPUT2\r\n"); // Pingpon Input pinMode(PIN_D27, INPUT_PULLUP); pinMode(PIN_D28, INPUT_PULLUP); attachInterrupt(PIN_D27, D27_Interrupt_cb, RISING);// RISING: ピンの状態がLOWからHIGHに変わったとき attachInterrupt(PIN_D28, D28_Interrupt_cb, RISING);// RISING: ピンの状態がLOWからHIGHに変わったとき /* void attachTimerInterrupt(unsigned int (*isr)(void), unsigned int us); isr: タイマー割り込みが発生したときに呼ばれる関数。 us: マイクロ秒 */ attachTimerInterrupt(TimerInterrupt_cb, 100000); // 100ms周期 ucCounter = 0; // 計測回数表示 Serial.printf("Finish setup\r\n"); } ///////////////////////////////// void StateCheck(unsigned char ucLR){ static unsigned char ucPrevLR = 0; ucLR &= 0x03; switch(ucStage){ case (0):// 0:リセット状態(サーブ開始待ち) ucStage = 1; //ucMaxDispTime = 0; break; case (1):// 1:サーブ完了待ち if((ucLR^ucPrevLR) == 3){ ucCounter = 1; ucDispCounter = ucCounter; ucStage = 2; //uc10msecCount = 200;// 120ms×200 = 4秒 //ucPlayCount = 0; }else{ ucCounter = 0; ucStage = 0; } break; case (2):// 2:カウント中 if((ucLR^ucPrevLR) == 3){ if(100<uc100msecCount){ // 10秒を超えていればノーカウントに ucCounter = 0; // 計測回数表示 ucStage = 0; SetHyphen(); ucDispTiming = DETECT_COUNT; }else{ ucCounter++; ucDispCounter = ucCounter; if(ucCounter > uiMaxCounter){ uiMaxCounter = (unsigned int)ucCounter; // ucSavePlayCount = ucPlayCount; } uc100msecCount = 0; } }else{//同じ側の場合 ucCounter = 0; // 計測回数表示 ucStage = 0; SetHyphen(); ucDispTiming = DETECT_COUNT*4; } break; default: ucStage = 0; break; } ucPrevLR = ucLR; } // the loop function runs over and over again forever void loop() { static unsigned int timingCount = 0; static unsigned int uiLedVal=0; if(ucFlagLR>0){ StateCheck(ucFlagLR); ucFlagLR = 0; } if(ucDispTiming>0){ ucDispTiming--; if(ucDispTiming<15){ ucDispVal[0] = 0xFF; ucDispVal[3] = 0xFF; } //Serial.printf("ucDispTiming=%d\r\n", ucDispTiming); }else{ //fnSetValue(uiLedVal); if(ucStage != 2){ SetValue(ucDispCounter); }else{ SetValue2(ucDispCounter%100); } } DispValue(); }

動作状況

治具を使用しての右左検出とカウント表示を確認しました。実際の卓球台を使用しての試験では、人の声やラケット音は、検出しませんが、ネットの片側だけにマイクを設置したため、卓球台にボールが当たる位置により、完全には打撃音を検出できない状況でした。今後、マイクの設置位置、向き、感度を調整して改善を図りたいと思います。
短い動画ですが、添付します。卓球台に設置している状況と治具で左右のパルスを出して表示したものです。治具での表示は、カウント100回で、ハイフォン表示になり、また、1からカウントします。治具の動画は、画面が横になっていますが、ご了承ください。

今後の展開

ラリーカウントが安定して動作したら、1回のラリーにかかった時間を計測し、1打あたりの時間も表示できるようにしたいと思います。また、ある程度ラリー音がなかった場合は、過去のラリーの最高カウントと1打あたりの時間も表示するようにしたいと思います。

1
ログインしてコメントを投稿する