XIAO MG24 sense to detect a beep of a specific frequency

Hi there,

I would say yes, but only given certain parameters. What is the Frequency you want to detect? WIll it be a constant amplitude and close up or FAR away (these mic’s don’t do long range vs, fidelity :grin:

Given a constant tone or even a BEEP pulse, and constant distance to the source at a fixed volume level. VERY doable.

There is an Arduino Example MIC , with a TFFT display. look at that example and others to get a feel on how the mic performs in YOUR situation. to get the most reliable and repeatable outcome.

There is a algorithm ro filter called a “Goertzel” it’s what you use for this type of detection. O(N) per block, tiny RAM/CPU, Tunable: just change
𝑓0 sample rate, or frame length, it is very Robust under noise with proper thresholds. it is a "a digital signal processing technique for efficiently calculating a specific frequency component of a Discrete Fourier Transform (DFT) "

https://docs.arduino.cc/libraries/goertzel/

check the basics here;

and here is a basic starting point that compiles AOK :+1:

// ============================================================
// XIAO MG24: Beep Detector (Goertzel) for analog MEMS mic
// Revision: 1.0 (2025-09-29)
// Target tone: 2000 Hz @ 8 kHz sample rate
// ============================================================

#include <Arduino.h>

// ---- User config ----
static const uint8_t  MIC_PIN        = A0;     // Your ADC pin
static const float    FS             = 8000.0; // Sample rate (Hz)
static const uint16_t N              = 205;    // Samples per frame (~25.6 ms)
static const float    F0             = 2000.0; // Target frequency (Hz)
static const uint8_t  FRAMES_HYST    = 3;      // Require this many consecutive "hits"
static const float    DETECT_MULT    = 8.0;    // Threshold = noise_floor * DETECT_MULT


// ---- Derived Goertzel coeff ----
static float coeff;
static float omega;
static float sine_;
static float cosine_;

// ---- State ----
static float noise_floor = 0.0f;
static uint8_t consecutive_hits = 0;

// Optional: simple DC removal (high-pass) parameters
static const float HP_ALPHA = 0.995f; // closer to 1.0 => more aggressive DC tracking
static float dc_est = 0.0f;

inline float goertzel_run(const int16_t *x, uint16_t len) {
  float s_prev = 0.0f;
  float s_prev2 = 0.0f;

  for (uint16_t i = 0; i < len; i++) {
    float s = x[i] + coeff * s_prev - s_prev2;
    s_prev2 = s_prev;
    s_prev  = s;
  }

  float real = s_prev - s_prev2 * cosine_;
  float imag = s_prev2 * sine_;
  return real * real + imag * imag; // power at F0
}

void setup_goertzel() {
  // k = round(N * f0 / fs)
  float k = roundf((N * F0) / FS);
  omega   = (2.0f * PI * k) / N;
  sine_   = sinf(omega);
  cosine_ = cosf(omega);
  coeff   = 2.0f * cosine_;
}

void calibrate_noise_floor(uint16_t frames = 30) {
  Serial.println(F("[Cal] Measuring noise floor..."));
  int16_t buf[N];

  float acc = 0.0f;
  for (uint16_t f = 0; f < frames; f++) {
    // collect a frame
    uint32_t t0 = micros();
    for (uint16_t i = 0; i < N; i++) {
      // hold precise sampling interval
      uint32_t target = t0 + (uint32_t)((i * (1000000.0f / FS)));
      while ((int32_t)(micros() - target) < 0) { /* wait */ }

      int raw = analogRead(MIC_PIN); // 12-bit typical
      // map to signed centered around mid-scale (assume ~2048 mid)
      // You can tweak midpoint if you know your bias exactly.
      int16_t s = (int16_t)raw - 2048;

      // DC removal (simple one-pole)
      dc_est = HP_ALPHA * dc_est + (1.0f - HP_ALPHA) * s;
      float s_hp = s - dc_est;

      buf[i] = (int16_t)s_hp; // store filtered
    }
    float p = goertzel_run(buf, N);
    acc += p;
  }
  noise_floor = (acc / frames);
  Serial.print(F("[Cal] Noise floor (power): "));
  Serial.println(noise_floor, 2);
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println(F("\n=== XIAO MG24 Beep Detector (Goertzel) ==="));
  Serial.println(F("Mic: MSM381ACT001 -> A0, fs=8kHz, N=205, f0=2kHz"));

  analogReadResolution(12); // If supported by MG24 core (usually 12 bits)
  // analogReference(...) // set if your core supports and you need a known Vref

  setup_goertzel();
  calibrate_noise_floor(30);
}

void loop() {
  static int16_t buf[N];

  // Collect one frame at ~exact FS using busy-wait timing
  uint32_t t0 = micros();
  for (uint16_t i = 0; i < N; i++) {
    uint32_t target = t0 + (uint32_t)((i * (1000000.0f / FS)));
    while ((int32_t)(micros() - target) < 0) { /* wait */ }

    int raw = analogRead(MIC_PIN);
    int16_t s = (int16_t)raw - 2048;

    // DC removal
    dc_est = HP_ALPHA * dc_est + (1.0f - HP_ALPHA) * s;
    float s_hp = s - dc_est;

    buf[i] = (int16_t)s_hp;
  }

  // Compute power at F0
  float power_f0 = goertzel_run(buf, N);

  // Simple neighbor guard (optional): compute at F0+Δf to ensure narrowband tone
  // Δf ~ FS/N; here ~39 Hz. We can skip for first pass to keep CPU low.

  // Detection
  float threshold = max(noise_floor * DETECT_MULT, noise_floor + 1.0f);
  bool hit = (power_f0 > threshold);

  // Debounce with consecutive frames
  if (hit) {
    consecutive_hits++;
  } else if (consecutive_hits > 0) {
    consecutive_hits--;
  }

  bool detected = (consecutive_hits >= FRAMES_HYST);

  // Print a light debug stream
  static uint32_t lastPrint = 0;
  if (millis() - lastPrint > 200) {
    lastPrint = millis();
    Serial.print(F("P:"));
    Serial.print(power_f0, 1);
    Serial.print(F("  Th:"));
    Serial.print(threshold, 1);
    Serial.print(F("  NF:"));
    Serial.print(noise_floor, 1);
    Serial.print(F("  Hits:"));
    Serial.print(consecutive_hits);
    Serial.print(F("  "));
    if (detected) Serial.print(F("DETECTED"));
    Serial.println();
  }

  // Optional: periodically refresh noise floor when not detecting
  static uint32_t lastRecal = 0;
  if (!detected && (millis() - lastRecal > 5000)) {
    lastRecal = millis();
    // compute quick running noise estimate from last power
    // (conservative: exponential average)
    noise_floor = 0.95f * noise_floor + 0.05f * power_f0;
  }
}

here is a basic starting point with some suggestions as well.

This will:

  • Sample A0 at ~8 kHz using a tight timing loop (good enough to start).

  • Detect a 2 kHz beep.

  • Print rolling magnitudes and a clean “DETECTED” line when the tone is present.

  • Includes a quick noise-floor calibration at boot.

  • If the mic is very quiet or your tone is far away, you’ll need gain and a clean bias. Don’t expect miracles from a raw high-impedance feed.

  • Timing with a busy-wait loop is “good enough” to prove it works; for production, move sampling to a hardware timer + ADC DMA to get rock-solid FS and free CPU.

  • If the beep can drift ± a few percent, two choices

  • Widen the detection by testing two or three close bins (F0−Δ, F0, F0+Δ) and summing power

  • lower level and loose a little f S/N

The hardware can do it. you can use the serial plotter to see if it’s working also and use your cell phone to generate the tone burst.

HTH

GL :slight_smile: PJ :v:

2 Likes