続ラズパイpicoでDTMFデコーダを作った

プログラミング

 昨日の続きです。メモ書きしていた回路図をKiCADで入力しました。Geminiと相談しながら作ったソースコードを以下に示します。

// DTMF detector
#include <Arduino.h>

// --- 定数定義 ---
#define SENSOR_PIN 26        // PicoのA0 (GPIO26) 
#define SAMPLING_RATE 8000   // 8ksps 
#define N 160                // 窓幅 (20ms) 
#define THRESHOLD_ROW 1000.0 // 低群の閾値 
#define THRESHOLD_COL 400.0  // 高群の閾値(減衰を考慮して下げる)
#define MIN_DURATION 300     // 最低継続時間 (ms) 
#define MAX_DURATION 3000    // 最大継続時間 (ms) 

// DTMF周波数 
const float TARGET_FREQS[] = {697, 770, 852, 941, 1209, 1336, 1477, 1633};
const char DTMF_MAP[4][4] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

float coefficients[8]; 

// --- 共有バッファとフラグ ---
volatile int samples[N];
volatile int sample_index = 0;
volatile bool buffer_ready = false;
struct repeating_timer timer;

// タイマー割り込みハンドラ
bool timer_callback(struct repeating_timer *t) {
  if (!buffer_ready) {
    samples[sample_index] = analogRead(SENSOR_PIN) - 512; 
    sample_index++;
    if (sample_index >= N) {
      sample_index = 0;
      buffer_ready = true; // メインループへ通知
    }
  }
  return true;
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(10);

  // Goertzel係数の計算
  for (int i = 0; i < 8; i++) {
    coefficients[i] = 2.0 * cos(2.0 * PI * TARGET_FREQS[i] / SAMPLING_RATE);
  }

  // タイマー割り込みの設定 (125us = 8000Hz)
  // ネガティブな数値(-125)を指定すると、実行時間を含めて正確に125us間隔で実行されます
  add_repeating_timer_us(-125, timer_callback, NULL, &timer);
}

// Goertzelアルゴリズム本体 
float goertzel(volatile int* s, float coeff) {
  float q0 = 0, q1 = 0, q2 = 0;
  for (int i = 0; i < N; i++) {
    q0 = coeff * q1 - q2 + s[i];
    q2 = q1;
    q1 = q0;
  }
  return (q1 * q1 + q2 * q2 - coeff * q1 * q2);
}

char last_detected = '\0';
unsigned long start_time = 0;
bool is_processed = false;

void loop() {
  if (buffer_ready) {
    // 1. 各周波数の強度計算
    float magnitudes[8];
    for (int i = 0; i < 8; i++) {
      magnitudes[i] = goertzel(samples, coefficients[i]);
    }

    // 2. 行と列の最大値を探す 
    int row = -1, col = -1;
    float max_row_mag = 0, max_col_mag = 0; 

    for (int i = 0; i < 4; i++) {
      // 低群(行)
      if (magnitudes[i] > THRESHOLD_ROW && magnitudes[i] > max_row_mag) {
        max_row_mag = magnitudes[i];
        row = i;
      }
      // 高群(列)
      if (magnitudes[i + 4] > THRESHOLD_COL && magnitudes[i + 4] > max_col_mag) {
        max_col_mag = magnitudes[i + 4];
        col = i; 
      }
    }

    char current_detected = (row != -1 && col != -1) ? DTMF_MAP[row][col] : '\0';

    // 3. 状態判定ロジック
    if (current_detected != '\0') {
      if (current_detected == last_detected) {
        unsigned long duration = millis() - start_time;
        if (!is_processed && duration >= MAX_DURATION) {
          Serial.print("Detected (Timeout): ");
          Serial.println(current_detected); 
          is_processed = true; 
        }
      } else {
        unsigned long duration = millis() - start_time;
        if (!is_processed && last_detected != '\0' && duration >= MIN_DURATION) {
          Serial.print("Detected: ");
          Serial.println(last_detected);
        }
        last_detected = current_detected;
        start_time = millis();
        is_processed = false;
      }
    } else {
      unsigned long duration = millis() - start_time;
      if (!is_processed && last_detected != '\0' && duration >= MIN_DURATION) {
        Serial.print("Detected: ");
        Serial.println(last_detected);
      }
      last_detected = '\0';
      is_processed = false;
    }

    // 次のバッファ準備許可
    buffer_ready = false;
  }
}

コメント