#include // --- デバッグモード有効 --- #define DEBUG // --- モールスデコーダ設定パラメータ --- #define WPM 20 // 受信速度 (Words Per Minute) #define GOERTZEL_N 48 // Goertzelアルゴリズムのサンプル数 #define THRESHOLD 10000 // トーン検出の閾値 #define LED_PIN 13 // デバッグ用LED出力ピン #define SAMPLE_RATE 8900 // サンプリングレート(Hz) #define TARGET_FREQ 700 // 検出対象周波数(Hz) #define AUDIO_PIN A0 // オーディオ入力ピン #define MAX_MORSE_LEN 10 // 1文字あたりの最大モールス長 #define ADC_READ_US 100 // ADC読み取りにかかる時間(マイクロ秒) #define LOOP_DELAY_MS 2 // メインループ遅延(ミリ秒) #define VARIABLE_DELAY_COEFF 0.05 // 可変遅延補正係数 #define ADAPTIVE_BUF_LEN 10 // ドット長適応用バッファサイズ // --- タイミング補正値 --- const float goertzelDelay = (float)GOERTZEL_N * 1000.0 / SAMPLE_RATE; const float adcDelay = (float)GOERTZEL_N * ADC_READ_US / 1000.0; const float baseDelay = goertzelDelay + adcDelay + LOOP_DELAY_MS; // --- モールス符号バッファ --- char morseBuffer[MAX_MORSE_LEN + 1]; // 1文字分の.と-を記録 uint8_t morseLen = 0; // morseBufferの長さ // --- タイミング管理 --- unsigned long lastOnTime = 0; // 直前のON開始時刻 unsigned long lastOffTime = 0; // 直前のOFF開始時刻 unsigned long spaceStartTime = 0; // スペース開始時刻 bool lastTone = false; // 直前のトーン状態 // --- ドット長適応用バッファ --- float dotBuf[ADAPTIVE_BUF_LEN] = {0}; uint8_t dotIdx = 0, dotCount = 0; float adaptiveDotLen = 1200.0 / WPM; // 適応的ドット長(ms) // --- ON区間一時バッファ --- float pendingOnDuration = 0; // ON区間長の一時保存 bool pendingOnValid = false; // pendingOnDurationが有効か // --- デバッグ出力ON/OFFトグル用 --- bool debugEnabled = true; // --- モールス符号テーブル(線形探索) --- struct MorseTable { const char* code; char letter; }; const MorseTable morseTable[] = { {".-", 'A'}, {"-...", 'B'}, {"-.-.", 'C'}, {"-..", 'D'}, {".", 'E'}, {"..-.", 'F'}, {"--.", 'G'}, {"....", 'H'}, {"..", 'I'}, {".---", 'J'}, {"-.-", 'K'}, {".-..", 'L'}, {"--", 'M'}, {"-.", 'N'}, {"---", 'O'}, {".--.", 'P'}, {"--.-", 'Q'}, {".-.", 'R'}, {"...", 'S'}, {"-", 'T'}, {"..-", 'U'}, {"...-", 'V'}, {".--", 'W'}, {"-..-", 'X'},{"-.--", 'Y'}, {"--..", 'Z'}, {"-----", '0'},{".----", '1'},{"..---", '2'},{"...--", '3'},{"....-", '4'}, {".....", '5'},{"-....", '6'},{"--...", '7'},{"---..", '8'},{"----.", '9'}, {".-.-.-", '.'},{"--..--", ','},{"..--..", '?'},{"-..-.", '/'}, {"-...=", '='},{".-.-.", '+'},{"-....-", '-'},{"---...", ':'}, {".----.", '\''},{"-.--.", '('},{"-.--.-", ')'}, {".--.-.", '@'},{"-.-.--", '!'} }; const int morseTableSize = sizeof(morseTable) / sizeof(MorseTable); // --- バッファの中央値計算(ドット長適応用) --- float median(float* arr, uint8_t len) { float temp[ADAPTIVE_BUF_LEN]; for (uint8_t i = 0; i < len; i++) temp[i] = arr[i]; for (uint8_t i = 0; i < len - 1; i++) for (uint8_t j = i + 1; j < len; j++) if (temp[i] > temp[j]) { float t = temp[i]; temp[i] = temp[j]; temp[j] = t; } if (len % 2 == 1) return temp[len / 2]; else return (temp[len / 2 - 1] + temp[len / 2]) / 2.0; } // --- Goertzelアルゴリズムによるトーン検出 --- float goertzel(uint8_t pin) { float s_prev = 0, s_prev2 = 0, coeff, Q; float omega = (2.0 * 3.141592653589793 * TARGET_FREQ) / SAMPLE_RATE; coeff = 2.0 * cos(omega); for (int i = 0; i < GOERTZEL_N; i++) { int sample = analogRead(pin) - 512; Q = coeff * s_prev - s_prev2 + sample; s_prev2 = s_prev; s_prev = Q; delayMicroseconds(1000000 / SAMPLE_RATE); } float power = s_prev2 * s_prev2 + s_prev * s_prev - coeff * s_prev * s_prev2; return power; } // --- モールス符号列をテーブル照合でデコード --- char decodeMorse(const char* code) { for (int i = 0; i < morseTableSize; i++) { if (strcmp(code, morseTable[i].code) == 0) return morseTable[i].letter; } return '?'; } // --- モールスバッファクリア --- void clearMorseBuffer() { for (uint8_t i = 0; i < MAX_MORSE_LEN + 1; i++) morseBuffer[i] = 0; morseLen = 0; } // --- バッファ内容をデコード・出力しクリア --- void decodeAndClearBuffer() { morseBuffer[morseLen] = '\0'; if (morseLen >= 1 && morseLen <= 6) { char decoded = decodeMorse(morseBuffer); Serial.print(decoded); #ifdef DEBUG if (debugEnabled) Serial.println(); #endif } clearMorseBuffer(); } // --- ドット長適応処理(移動中央値) --- void updateAdaptiveDotLen(float pulse) { if (pulse > 30 && pulse < 1.5 * adaptiveDotLen) { dotBuf[dotIdx] = pulse; dotIdx = (dotIdx + 1) % ADAPTIVE_BUF_LEN; if (dotCount < ADAPTIVE_BUF_LEN) dotCount++; adaptiveDotLen = median(dotBuf, dotCount); } } // --- ON→OFFエッジ処理:ON区間長バッファリングとノイズ除去 --- void handleOnToOffEdge(unsigned long now, unsigned long &lastOnTime, unsigned long &lastOffTime, unsigned long &spaceStartTime, bool &lastTone, float power) { unsigned long rawOnDuration = now - lastOnTime; float variableOnDelay = VARIABLE_DELAY_COEFF * rawOnDuration; unsigned long onDuration = (rawOnDuration > baseDelay + variableOnDelay) ? rawOnDuration - baseDelay - variableOnDelay : 0; #ifdef DEBUG if (debugEnabled) { Serial.print(" [ON→OFF] now:"); Serial.print(now); Serial.print(" lastOnTime:"); Serial.print(lastOnTime); Serial.print(" rawOnDuration:"); Serial.print(rawOnDuration); Serial.print(" onDuration:"); Serial.print(onDuration); Serial.print(" ("); Serial.print((float)onDuration / adaptiveDotLen, 2); Serial.print("dot)"); Serial.print(" power:"); Serial.print(power); Serial.print(" morseBuffer:["); for (uint8_t i = 0; i < morseLen; i++) Serial.print(morseBuffer[i]); Serial.print("]"); } #endif // --- 短いONノイズは無視 --- if (onDuration < 0.2 * adaptiveDotLen) { pendingOnValid = false; #ifdef DEBUG if (debugEnabled) Serial.println(" [NOISE: short ON] (reset pending)"); #endif return; } #ifdef DEBUG if (debugEnabled) Serial.println(); #endif // --- ON区間長を一時バッファに蓄積 --- if (pendingOnValid) { pendingOnDuration += onDuration; } else { pendingOnDuration = onDuration; pendingOnValid = true; } lastOffTime = now; spaceStartTime = now; } // --- OFF→ONエッジ処理:点・線判定とスペース判定 --- void handleOffToOnEdge(unsigned long now, unsigned long &lastOnTime, unsigned long &lastOffTime, unsigned long &spaceStartTime, float power) { unsigned long rawOffDuration = now - lastOffTime; float variableOffDelay = VARIABLE_DELAY_COEFF * rawOffDuration; unsigned long offDuration = (rawOffDuration > baseDelay + variableOffDelay) ? rawOffDuration - baseDelay - variableOffDelay : 0; #ifdef DEBUG if (debugEnabled) { Serial.print(" [OFF→ON] now:"); Serial.print(now); Serial.print(" lastOffTime:"); Serial.print(lastOffTime); Serial.print(" rawOffDuration:"); Serial.print(rawOffDuration); Serial.print(" offDuration:"); Serial.print(offDuration); Serial.print(" ("); Serial.print((float)offDuration / adaptiveDotLen, 2); Serial.print("dot)"); Serial.print(" power:"); Serial.print(power); } #endif // --- 短いOFFノイズは無視 --- if (offDuration < 0.2 * adaptiveDotLen) { pendingOnValid = false; #ifdef DEBUG if (debugEnabled) Serial.println(" [NOISE: short OFF] (reset pending)"); #endif return; } lastOnTime = now; lastOffTime = now; // --- pendingOnDurationを点・線としてバッファに追加 --- if (pendingOnValid) { if (pendingOnDuration < 1.5 * adaptiveDotLen) { updateAdaptiveDotLen(pendingOnDuration); if (morseLen < MAX_MORSE_LEN) morseBuffer[morseLen++] = '.'; } else { if (morseLen < MAX_MORSE_LEN) morseBuffer[morseLen++] = '-'; } pendingOnValid = false; } #ifdef DEBUG if (debugEnabled) { Serial.print(" morseBuffer:["); for (uint8_t i = 0; i < morseLen; i++) Serial.print(morseBuffer[i]); Serial.print("]"); } #endif // --- スペース長で文字・単語区切り判定 --- if (spaceStartTime > 0) { unsigned long spaceLen = now - spaceStartTime; #ifdef DEBUG if (debugEnabled) { Serial.print(" spaceStartTime:"); Serial.print(spaceStartTime); Serial.print(" spaceLen:"); Serial.print(spaceLen); Serial.print(" ("); Serial.print((float)spaceLen / adaptiveDotLen, 2); Serial.print("dot)"); } #endif if ((spaceStartTime == 0 || spaceLen > 12 * adaptiveDotLen) && morseLen == 1) { #ifdef DEBUG if (debugEnabled) Serial.println(" [clear morseBuffer:1char]"); #endif clearMorseBuffer(); } else if (spaceLen >= 6 * adaptiveDotLen) { #ifdef DEBUG if (debugEnabled) Serial.print(" [単語間スペース]"); #endif if (morseLen > 0) { #ifdef DEBUG if (debugEnabled) Serial.println(" decodeAndClearBuffer()"); #endif decodeAndClearBuffer(); } Serial.print(' '); #ifdef DEBUG if (debugEnabled) Serial.println(); #endif } else if (spaceLen >= 2.2 * adaptiveDotLen) { #ifdef DEBUG if (debugEnabled) Serial.print(" [文字間スペース]"); #endif if (morseLen > 0) { #ifdef DEBUG if (debugEnabled) Serial.println(" decodeAndClearBuffer()"); #endif decodeAndClearBuffer(); } } else { #ifdef DEBUG if (debugEnabled) Serial.println(" [文字中スペース]"); #endif } } else { #ifdef DEBUG if (debugEnabled) Serial.println(); #endif } spaceStartTime = 0; } // --- 長いOFFで強制バッファ確定・デコード --- void handleLongOff(unsigned long now, unsigned long &spaceStartTime) { if (!lastTone && spaceStartTime > 0) { unsigned long spaceLen = now - spaceStartTime; if (spaceLen >= 14 * adaptiveDotLen) { #ifdef DEBUG if (debugEnabled) { Serial.print(" [OFF長] now:"); Serial.print(now); Serial.print(" spaceStartTime:"); Serial.print(spaceStartTime); Serial.print(" spaceLen:"); Serial.print(spaceLen); Serial.print(" ("); Serial.print((float)spaceLen / adaptiveDotLen, 2); Serial.print("dot) "); } #endif if (pendingOnValid) { if (pendingOnDuration < 1.5 * adaptiveDotLen) { updateAdaptiveDotLen(pendingOnDuration); if (morseLen < MAX_MORSE_LEN) morseBuffer[morseLen++] = '.'; } else { if (morseLen < MAX_MORSE_LEN) morseBuffer[morseLen++] = '-'; } pendingOnValid = false; pendingOnDuration = 0; } #ifdef DEBUG if (debugEnabled) { Serial.print(" morseBuffer:["); for (uint8_t i = 0; i < morseLen; i++) Serial.print(morseBuffer[i]); Serial.print("]"); } #endif if (morseLen > 0) { #ifdef DEBUG if (debugEnabled) { Serial.print(" 強制デコード"); Serial.println(); } #endif decodeAndClearBuffer(); Serial.print(' '); #ifdef DEBUG if (debugEnabled) Serial.println(); #endif } spaceStartTime = 0; } } } // --- Arduinoセットアップ --- void setup() { pinMode(LED_PIN, OUTPUT); Serial.begin(115200); lastOnTime = 0; lastOffTime = 0; spaceStartTime = 0; clearMorseBuffer(); #ifdef DEBUG Serial.println("CW Decoder (Goertzel, Table Matching) started."); Serial.print("WPM: "); Serial.println(WPM); Serial.print("dotLen: "); Serial.print(adaptiveDotLen); Serial.print(" ms (adaptive) "); Serial.print("GOERTZEL_N: "); Serial.println(GOERTZEL_N); Serial.print("THRESHOLD: "); Serial.println(THRESHOLD); Serial.print("goertzelDelay: "); Serial.print(goertzelDelay); Serial.print(" ms "); Serial.print("adcDelay: "); Serial.print(adcDelay); Serial.print(" ms "); Serial.print("baseDelay: "); Serial.print(baseDelay); Serial.print(" ms "); Serial.print("VARIABLE_DELAY_COEFF: "); Serial.print(VARIABLE_DELAY_COEFF); Serial.print(" "); Serial.println(); #endif } // --- メインループ --- void loop() { // デバッグトグル処理(シリアルでd/D入力で切替) if (Serial.available() > 0) { char c = Serial.read(); if (c == 'd' || c == 'D') { debugEnabled = !debugEnabled; Serial.print("Debug mode: "); Serial.println(debugEnabled ? "ON" : "OFF"); } } float power; bool tone; // Goertzelでトーン検出 while (true) { power = goertzel(AUDIO_PIN); if (power < THRESHOLD) { tone = false; break; } else if (power >= THRESHOLD * 2.0) { tone = true; break; } } digitalWrite(LED_PIN, tone ? HIGH : LOW); unsigned long now = millis(); // エッジ検出と処理 if (lastTone && !tone) { handleOnToOffEdge(now, lastOnTime, lastOffTime, spaceStartTime, lastTone, power); } else if (!lastTone && tone) { handleOffToOnEdge(now, lastOnTime, lastOffTime, spaceStartTime, power); } // 長いOFFで強制デコード handleLongOff(now, spaceStartTime); lastTone = tone; }