昨日の続きです。メモ書きしていた回路図を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;
}
}

コメント