From 0a4785f34190f77d014afb7dc4d0968f1f5efda5 Mon Sep 17 00:00:00 2001 From: pjanik Date: Sun, 22 Feb 2026 11:09:12 -0700 Subject: [PATCH] working ADC input + FFT analysis + py logger --- CMakeLists.txt | 9 +- Kconfig | 2 +- README.md | 2 +- boards/nucleo_g474re.overlay | 48 +++++++++- plotter.py | 132 ++++++++++++++++++++++++++ prj.conf | 18 +++- serial_debug.py | 19 ++++ src/audio_capture.c | 172 ++++++++++++++++++++++++++++++++++ src/audio_capture.h | 40 ++++++++ src/audio_output.c | 59 ++++++++++++ src/audio_output.h | 28 ++++++ src/audio_process.c | 62 +++++++++++++ src/audio_process.h | 30 ++++++ src/main.c | 173 ++++++++++++----------------------- 14 files changed, 676 insertions(+), 118 deletions(-) create mode 100644 plotter.py create mode 100644 serial_debug.py create mode 100644 src/audio_capture.c create mode 100644 src/audio_capture.h create mode 100644 src/audio_output.c create mode 100644 src/audio_output.h create mode 100644 src/audio_process.c create mode 100644 src/audio_process.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1089c13..3087aef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20.0) set(BOARD nucleo_g474re) find_package(Zephyr REQUIRED) -project(app) +project(audio_analysis) -target_sources(app PRIVATE src/main.c) +target_sources(app PRIVATE + src/main.c + src/audio_capture.c + src/audio_process.c + src/audio_output.c +) diff --git a/Kconfig b/Kconfig index 3e836c5..2f8f281 100644 --- a/Kconfig +++ b/Kconfig @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -mainmenu "First App" +mainmenu "Audio App" # Your application configuration options go here diff --git a/README.md b/README.md index b4c5530..b0d3a03 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,4 @@ Type a line and press Enter: ## License -[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) +[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) - Similar to diff --git a/boards/nucleo_g474re.overlay b/boards/nucleo_g474re.overlay index 299b4df..0a6354e 100644 --- a/boards/nucleo_g474re.overlay +++ b/boards/nucleo_g474re.overlay @@ -8,7 +8,7 @@ leds { compatible = "gpio-leds"; ld2: led_0 { - gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; /* LD2 -> PA5 */ + gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; // LD2 -> PA5 label = "LD2"; }; }; @@ -20,4 +20,50 @@ &gpioc { status = "okay"; +}; + +/* ── Audio ADC ───────────────────────────────────────── + * + * ADC1 is already enabled by the board DTS with: + * - pinctrl on PA0 (ADC1_IN1) — Arduino header A0 + * - SYNC clock, prescaler /4 → 42.5 MHz ADC clock + * + * We just add the channel config here. + * + * Conversion time = (641 + 12.5) / 42.5 MHz = 15.4 µs + * Max sample rate ≈ 65 kHz — plenty for 44.1 kHz + * Actual 44.1 kHz rate is set by a timer in code. + */ +&adc1 { + #address-cells = <1>; + #size-cells = <0>; + + channel@1 { + reg = <1>; // IN1 = PA0 + zephyr,gain = "ADC_GAIN_1"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,resolution = <12>; + }; +}; + +/* ── TIM6 — ADC sample clock ───────────────────────── + * + * Basic timer, no PWM/capture — just counting. + * TRGO output triggers ADC1 conversions. + * + * Timer clock = 170 MHz (APB1, no prescaler) + * ARR = (170,000,000 / 44,100) - 1 = 3854 + * Actual rate = 170,000,000 / 3855 = ~44,099 Hz + * + * Trigger routing (TIM6_TRGO → ADC1) is done in code + * via STM32 LL/HAL — devicetree just enables the clock. + */ +&timers6 { + status = "okay"; + st,prescaler = <0>; + + counter { + status = "okay"; + }; }; \ No newline at end of file diff --git a/plotter.py b/plotter.py new file mode 100644 index 0000000..0a80491 --- /dev/null +++ b/plotter.py @@ -0,0 +1,132 @@ +""" +Live serial plotter for audio_analysis firmware. + +Reads prefixed lines from UART: + RAW:s0,s1,s2,... → time-domain waveform (decimated by 4) + FFT:m0,m1,m2,... → frequency-domain spectrum (first 128 bins) + +Usage: + python plotter.py COM5 # Windows + python plotter.py /dev/ttyACM0 # Linux + +Requirements: + pip install pyserial matplotlib numpy PyQt5 +""" + +import sys +import threading +import matplotlib +matplotlib.use("Qt5Agg") +import serial +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation + +# ── Config ────────────────────────────────────────── + +BAUD = 115200 +SAMPLE_RATE = 44100 +BUF_SIZE = 1024 +DECIMATION = 4 +DISPLAY_SAMPLES = BUF_SIZE // DECIMATION # 256 +DISPLAY_BINS = 128 + +# ── Serial setup ──────────────────────────────────── + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +port = sys.argv[1] +ser = serial.Serial(port, BAUD, timeout=0.1) + +# ── Plot setup ────────────────────────────────────── + +fig, (ax_time, ax_freq) = plt.subplots(2, 1, figsize=(10, 6)) +fig.suptitle("Audio Analysis — Live") + +# Time domain (decimated) +time_x = np.arange(DISPLAY_SAMPLES) * DECIMATION / SAMPLE_RATE * 1000 # ms +time_line, = ax_time.plot(time_x, np.zeros(DISPLAY_SAMPLES), color="cyan") +ax_time.set_xlim(0, time_x[-1]) +ax_time.set_ylim(-1.1, 1.1) +ax_time.set_xlabel("Time (ms)") +ax_time.set_ylabel("Amplitude") +ax_time.set_title("Waveform") +ax_time.grid(True, alpha=0.3) + +# Frequency domain (first DISPLAY_BINS bins) +freq_x = np.arange(DISPLAY_BINS) * SAMPLE_RATE / BUF_SIZE # Hz +freq_line, = ax_freq.plot(freq_x, np.zeros(DISPLAY_BINS), color="orange") +ax_freq.set_xlim(0, freq_x[-1]) +ax_freq.set_ylim(0, 1) +ax_freq.set_xlabel("Frequency (Hz)") +ax_freq.set_ylabel("Magnitude") +ax_freq.set_title("Spectrum") +ax_freq.grid(True, alpha=0.3) + +plt.tight_layout() + +# ── Thread-safe data storage ─────────────────────── + +lock = threading.Lock() +raw_data = np.zeros(DISPLAY_SAMPLES) +fft_data = np.zeros(DISPLAY_BINS) + +# ── Background serial reader thread ─────────────── + +def serial_reader(): + global raw_data, fft_data + + while True: + try: + raw_line = ser.readline() + if not raw_line: + continue + line = raw_line.decode("utf-8", errors="ignore").strip() + except Exception: + continue + + if line.startswith("RAW:"): + try: + values = line[4:].split(",") + samples = np.array([int(v) for v in values if v], + dtype=np.float32) + normalized = (samples - 2048.0) / 2048.0 + with lock: + raw_data = normalized[:DISPLAY_SAMPLES] + except (ValueError, IndexError): + pass + + elif line.startswith("FFT:"): + try: + values = line[4:].split(",") + mags = np.array([float(v) for v in values if v]) + with lock: + fft_data = mags[:DISPLAY_BINS] + except (ValueError, IndexError): + pass + +reader_thread = threading.Thread(target=serial_reader, daemon=True) +reader_thread.start() + +# ── Animation update ──────────────────────────────── + +def update(frame): + with lock: + rd = raw_data.copy() + fd = fft_data.copy() + + time_line.set_ydata(rd) + + freq_line.set_ydata(fd) + peak = np.max(fd) + if peak > 0: + ax_freq.set_ylim(0, peak * 1.2) + + return time_line, freq_line + +ani = FuncAnimation(fig, update, interval=100, blit=False, cache_frame_data=False) + +plt.show() +ser.close() diff --git a/prj.conf b/prj.conf index 52234ed..2bf48b8 100644 --- a/prj.conf +++ b/prj.conf @@ -3,6 +3,22 @@ CONFIG_GPIO=y CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_UART_CONSOLE=y +CONFIG_PRINTK=y CONFIG_UART_INTERRUPT_DRIVEN=y + CONFIG_RING_BUFFER=y -CONFIG_PRINTK=y \ No newline at end of file + +CONFIG_ADC=y +CONFIG_ADC_ASYNC=y + +CONFIG_CMSIS_DSP=y +CONFIG_CMSIS_DSP_TRANSFORM=y +CONFIG_CMSIS_DSP_COMPLEXMATH=y +CONFIG_CMSIS_DSP_STATISTICS=y +CONFIG_FPU=y + +CONFIG_THREAD_ANALYZER=y +CONFIG_THREAD_ANALYZER_USE_PRINTK=y +CONFIG_THREAD_ANALYZER_AUTO=y +CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5 +CONFIG_THREAD_NAME=y \ No newline at end of file diff --git a/serial_debug.py b/serial_debug.py new file mode 100644 index 0000000..52c9c6f --- /dev/null +++ b/serial_debug.py @@ -0,0 +1,19 @@ +"""Quick diagnostic — just print what comes over serial.""" +import sys +import serial + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +ser = serial.Serial(sys.argv[1], 115200, timeout=2) +print(f"Listening on {sys.argv[1]}...") + +while True: + raw = ser.readline() + if raw: + line = raw.decode("utf-8", errors="replace").strip() + prefix = line[:20] if len(line) > 20 else line + print(f"[{len(line):5d} chars] {prefix}...") + else: + print("(timeout — no data)") diff --git a/src/audio_capture.c b/src/audio_capture.c new file mode 100644 index 0000000..cbbf7f2 --- /dev/null +++ b/src/audio_capture.c @@ -0,0 +1,172 @@ +#include "audio_capture.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +/* ── Ping-pong DMA buffer ────────────────────────────── + * + * DMA transfers into one contiguous buffer of 2 * AUDIO_BUF_SAMPLES. + * Half-transfer IRQ → first half ready (buf_a) + * Transfer-complete IRQ → second half ready (buf_b) + * + * While one half is being filled by DMA, the processing + * thread works on the other half. + */ +static int16_t dma_buf[2 * AUDIO_BUF_SAMPLES]; + +// Semaphore: DMA ISR gives, processing thread takes +static K_SEM_DEFINE(buf_ready_sem, 0, 1); + +// Points to whichever half just finished filling +static volatile int16_t *ready_buf; + +// ── DMA1 Channel 1 ISR ────────────────────────────────── + +static void dma1_ch1_isr(const void *arg) +{ + ARG_UNUSED(arg); + + if (LL_DMA_IsActiveFlag_HT1(DMA1)) { + LL_DMA_ClearFlag_HT1(DMA1); + ready_buf = &dma_buf[0]; + k_sem_give(&buf_ready_sem); + } + + if (LL_DMA_IsActiveFlag_TC1(DMA1)) { + LL_DMA_ClearFlag_TC1(DMA1); + ready_buf = &dma_buf[AUDIO_BUF_SAMPLES]; + k_sem_give(&buf_ready_sem); + } +} + +// ── DMA setup: DMA1 Ch1 ← ADC1 ─────────────────────── + +static void dma_init(void) +{ + LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1); + LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMAMUX1); + + LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1); + + LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_ADC1); + + LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, + LL_DMA_DIRECTION_PERIPH_TO_MEMORY); + + LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR); + + LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, + LL_DMA_PERIPH_NOINCREMENT); + LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, + LL_DMA_MEMORY_INCREMENT); + + LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, + LL_DMA_PDATAALIGN_HALFWORD); + LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, + LL_DMA_MDATAALIGN_HALFWORD); + + LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, + 2 * AUDIO_BUF_SAMPLES); + + LL_DMA_SetPeriphAddress(DMA1, LL_DMA_CHANNEL_1, + (uint32_t)&ADC1->DR); + LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_1, + (uint32_t)dma_buf); + + // Enable half-transfer and transfer-complete interrupts + LL_DMA_EnableIT_HT(DMA1, LL_DMA_CHANNEL_1); + LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); + + // Connect our ISR — DMA1_Channel1_IRQn = 11 on STM32G4 + IRQ_CONNECT(DMA1_Channel1_IRQn, 2, dma1_ch1_isr, NULL, 0); + irq_enable(DMA1_Channel1_IRQn); + + LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); +} + +// ── ADC1 setup: external trigger from TIM6 + DMA ───── + +static void adc_hw_init(void) +{ + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_ADC12); + + // Wait for ADC voltage regulator startup + LL_ADC_EnableInternalRegulator(ADC1); + k_busy_wait(20); // RM says max 10 µs + + // Calibrate (single-ended) + LL_ADC_StartCalibration(ADC1, LL_ADC_SINGLE_ENDED); + while (LL_ADC_IsCalibrationOnGoing(ADC1)) { + } + + // Single channel: IN1 (PA0), longest sample time + LL_ADC_REG_SetSequencerLength(ADC1, LL_ADC_REG_SEQ_SCAN_DISABLE); + LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, + LL_ADC_CHANNEL_1); + LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, + LL_ADC_SAMPLINGTIME_640CYCLES_5); + LL_ADC_SetChannelSingleDiff(ADC1, LL_ADC_CHANNEL_1, + LL_ADC_SINGLE_ENDED); + + // 12-bit resolution, right-aligned + LL_ADC_SetResolution(ADC1, LL_ADC_RESOLUTION_12B); + + // External trigger: TIM6 TRGO, rising edge + LL_ADC_REG_SetTriggerSource(ADC1, LL_ADC_REG_TRIG_EXT_TIM6_TRGO); + + // DMA circular mode — ADC issues DMA request after each conversion + LL_ADC_REG_SetDMATransfer(ADC1, LL_ADC_REG_DMA_TRANSFER_UNLIMITED); + + // Enable and start + LL_ADC_Enable(ADC1); + while (!LL_ADC_IsActiveFlag_ADRDY(ADC1)) { + } + + // Start ADC — it will wait for TIM6 triggers + LL_ADC_REG_StartConversion(ADC1); +} + +// ── TIM6 setup: TRGO at 44.1 kHz ───────────────────── + +static void tim6_init(void) +{ + LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6); + + LL_TIM_SetPrescaler(TIM6, 0); + LL_TIM_SetAutoReload(TIM6, TIM6_ARR); + LL_TIM_SetUpdateSource(TIM6, LL_TIM_UPDATESOURCE_COUNTER); + LL_TIM_SetTriggerOutput(TIM6, LL_TIM_TRGO_UPDATE); +} + +// ── Public API ────────────────────────────────────── + +int audio_capture_init(void) +{ + dma_init(); + adc_hw_init(); + tim6_init(); + return 0; +} + +void audio_capture_start(void) +{ + LL_TIM_EnableCounter(TIM6); +} + +void audio_capture_stop(void) +{ + LL_TIM_DisableCounter(TIM6); +} + +int16_t *audio_capture_wait(void) +{ + k_sem_take(&buf_ready_sem, K_FOREVER); + return (int16_t *)ready_buf; +} diff --git a/src/audio_capture.h b/src/audio_capture.h new file mode 100644 index 0000000..2e207db --- /dev/null +++ b/src/audio_capture.h @@ -0,0 +1,40 @@ +#ifndef AUDIO_CAPTURE_H +#define AUDIO_CAPTURE_H + +#include + +// ── Configuration ──────────────────────────────────── + +#define SAMPLE_RATE 44100 +#define TIMER_CLOCK 170000000 +#define TIM6_ARR ((TIMER_CLOCK / SAMPLE_RATE) - 1) // 3854 + +/* + * Buffer size in samples. + * 1024 samples @ 44.1 kHz ≈ 23.2 ms per half-buffer. + * Must be power of 2 for FFT later. + */ +#define AUDIO_BUF_SAMPLES 1024 + +// ── API ────────────────────────────────────────────── + +/* + * Initialize TIM6, ADC1, and DMA for continuous capture. + * Returns 0 on success. + */ +int audio_capture_init(void); + +// Start TIM6 — ADC conversions begin flowing via DMA. +void audio_capture_start(void); + +// Stop TIM6. +void audio_capture_stop(void); + +/* + * Block until a full buffer is ready. + * Returns pointer to AUDIO_BUF_SAMPLES worth of 16-bit samples. + * The pointer alternates between two internal ping-pong buffers. + */ +int16_t *audio_capture_wait(void); + +#endif diff --git a/src/audio_output.c b/src/audio_output.c new file mode 100644 index 0000000..20f9d35 --- /dev/null +++ b/src/audio_output.c @@ -0,0 +1,59 @@ +#include "audio_output.h" + +#include +#include + +void audio_output_summary(const struct audio_result *r) +{ + printk("RMS: %.4f | Peak: %.1f Hz (bin %u)\n", + (double)r->rms, + (double)r->peak_freq_hz, + r->peak_bin); + printk("Min: %.4f | Max: %.4f\n", (double)r->min, (double)r->max); +} + +void audio_output_csv(const struct audio_result *r, int num_bins) +{ + if (num_bins > FFT_NUM_BINS) + { + num_bins = FFT_NUM_BINS; + } + + for (int i = 0; i < num_bins; i++) + { + if (i > 0) + { + printk(","); + } + printk("%.2f", (double)r->magnitude[i]); + } + printk("\n"); +} + +void audio_output_raw(const int16_t *samples, int count) +{ + printk("RAW:"); + for (int i = 0; i < count; i++) + { + if (i > 0) + { + printk(","); + } + printk("%d", samples[i]); + } + printk("\n"); +} + +void audio_output_raw_decimated(const int16_t *samples, int count, int step) +{ + printk("RAW:"); + for (int i = 0; i < count; i += step) + { + if (i > 0) + { + printk(","); + } + printk("%d", samples[i]); + } + printk("\n"); +} diff --git a/src/audio_output.h b/src/audio_output.h new file mode 100644 index 0000000..784f851 --- /dev/null +++ b/src/audio_output.h @@ -0,0 +1,28 @@ +#ifndef AUDIO_OUTPUT_H +#define AUDIO_OUTPUT_H + +#include "audio_process.h" + +// Print a few key stats over UART (RMS, peak freq, peak magnitude). +void audio_output_summary(const struct audio_result *r); + +/* + * Print CSV line of bin magnitudes for external graphing. + * Format: "mag0,mag1,mag2,...\n" + */ +void audio_output_csv(const struct audio_result *r, int num_bins); + +/* + * Print raw ADC samples as CSV for time-domain plotting. + * Format: "RAW:s0,s1,s2,...\n" + * Prefix lets Python distinguish from other output. + */ +void audio_output_raw(const int16_t *samples, int count); + +/* + * Print every Nth raw sample. Reduces UART bandwidth. + * Format: "RAW:s0,sN,s2N,...\n" + */ +void audio_output_raw_decimated(const int16_t *samples, int count, int step); + +#endif diff --git a/src/audio_process.c b/src/audio_process.c new file mode 100644 index 0000000..27b8b7a --- /dev/null +++ b/src/audio_process.c @@ -0,0 +1,62 @@ +#include "audio_process.h" + +#include +#include + +// CMSIS-DSP real FFT instance +static arm_rfft_fast_instance_f32 fft_inst; + +// Working buffers +static float fft_input[AUDIO_BUF_SAMPLES]; +static float fft_output[AUDIO_BUF_SAMPLES]; + +int audio_process_init(void) +{ + arm_status status = arm_rfft_fast_init_f32(&fft_inst, + AUDIO_BUF_SAMPLES); + return (status == ARM_MATH_SUCCESS) ? 0 : -1; +} + +void audio_process_run(const int16_t *samples, struct audio_result *out) +{ + // Convert 12-bit ADC values (0–4095) to float centered around 0 + float temp; + out->min = 1.0f; + out->max = -1.0f; + for (int i = 0; i < AUDIO_BUF_SAMPLES; i++) + { + temp = ((float)samples[i] - 2048.0f) / 2048.0f; + fft_input[i] = temp; + + if (temp < out->min) + out->min = temp; + else if (temp > out->max) + out->max = temp; + } + + // RMS of the time-domain signal + arm_rms_f32(fft_input, AUDIO_BUF_SAMPLES, &out->rms); + + // Real FFT + arm_rfft_fast_f32(&fft_inst, fft_input, fft_output, 0); + + // Compute magnitude of each complex bin. + // fft_output layout: [re0, re_nyquist, re1, im1, re2, im2, ...] + // Bin 0 (DC) and bin N/2 (Nyquist) are real-only. + out->magnitude[0] = fabsf(fft_output[0]); + + // Bins 1..N/2-1 are complex pairs + arm_cmplx_mag_f32(&fft_output[2], &out->magnitude[1], + FFT_NUM_BINS - 1); + + // Find the peak bin (skip bin 0 = DC) + float peak_val = 0.0f; + uint32_t peak_idx = 1; + + arm_max_f32(&out->magnitude[1], FFT_NUM_BINS - 1, &peak_val, + &peak_idx); + peak_idx += 1; // offset because we started at bin 1 + + out->peak_bin = peak_idx; + out->peak_freq_hz = (float)peak_idx * SAMPLE_RATE / AUDIO_BUF_SAMPLES; +} diff --git a/src/audio_process.h b/src/audio_process.h new file mode 100644 index 0000000..0c0d2a2 --- /dev/null +++ b/src/audio_process.h @@ -0,0 +1,30 @@ +#ifndef AUDIO_PROCESS_H +#define AUDIO_PROCESS_H + +#include +#include "audio_capture.h" + +// Number of FFT output bins (half of input size, real-valued) +#define FFT_NUM_BINS (AUDIO_BUF_SAMPLES / 2) + +/* Processed result from one buffer */ +struct audio_result +{ + float magnitude[FFT_NUM_BINS]; // FFT bin magnitudes + float rms; // RMS level (0.0 – 1.0) + uint32_t peak_bin; // Index of loudest bin + float peak_freq_hz; // Frequency of loudest bin + float max; + float min; +}; + +// One-time init (FFT instance). Returns 0 on success. +int audio_process_init(void); + +/* + * Run FFT + analysis on a buffer of raw ADC samples. + * Writes results into *out. + */ +void audio_process_run(const int16_t *samples, struct audio_result *out); + +#endif diff --git a/src/main.c b/src/main.c index ecdf420..7faf781 100644 --- a/src/main.c +++ b/src/main.c @@ -1,130 +1,79 @@ #include -#include -#include -#include -#include -#include -#include +#include "audio_capture.h" +#include "audio_process.h" +#include "audio_output.h" -#define LED_NODE DT_ALIAS(led0) +// ── Processing thread ─────────────────────────────── -#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart) +#define PROC_STACK_SIZE 4096 +#define PROC_PRIORITY 5 -#define RX_BUF_SIZE 256 -#define LINE_BUF_SIZE 128 -#define THREAD_STACK 1024 -#define THREAD_PRIO 5 +static struct k_thread proc_thread_data; +static K_THREAD_STACK_DEFINE(proc_stack, PROC_STACK_SIZE); -// Ring buffer: callback writes, thread reads (could probably be a msg_queue too) -RING_BUF_DECLARE(rx_buf, RX_BUF_SIZE); +static struct audio_result result; -// Semaphore to wake the thread (name, initial count, max count) -K_SEM_DEFINE(rx_sem, 0, 1); - -K_THREAD_STACK_DEFINE(rx_stack, THREAD_STACK); -static struct k_thread rx_thread_data; - -static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED_NODE, gpios); -static const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(lpuart1)); - -void uart_print(const char buffer[]); -void uart_println(const char buffer[]); -void uart_cb(const struct device *dev, void *user_data); -static void rx_thread(void *p1, void *p2, void *p3); - -int main(void) +static void proc_thread(void *p1, void *p2, void *p3) { - if (!device_is_ready(uart)) - { - printk("UART not ready\n"); - return -1; - } - - // Set up interrupt-driven RX - uart_irq_callback_set(uart, uart_cb); - uart_irq_rx_enable(uart); - - // Spawn the processing thread - k_thread_create(&rx_thread_data, rx_stack, - THREAD_STACK, - rx_thread, NULL, NULL, NULL, - THREAD_PRIO, 0, K_NO_WAIT); - - printk("UART ready — type something!\r\n"); - - return 0; -} - -// ============= Helpers ============= - -void uart_print(const char buffer[]) -{ - size_t len = strlen(buffer); - for (size_t i = 0; i < len; i++) - uart_poll_out(uart, buffer[i]); -} - -void uart_println(const char buffer[]) -{ - uart_print(buffer); - uart_poll_out(uart, '\r'); - uart_poll_out(uart, '\n'); -} - -// ============= Callbacks ============= -void uart_cb(const struct device *dev, void *user_data) -{ - uint8_t byte; - while (uart_irq_update(dev) && uart_irq_rx_ready(dev)) - { - if (uart_fifo_read(dev, &byte, 1) == 1) - { - ring_buf_put(&rx_buf, &byte, 1); - } - } - - // wake up thread - k_sem_give(&rx_sem); -} - -// ============= Threads ============= -static void rx_thread(void *p1, void *p2, void *p3) -{ - char line[LINE_BUF_SIZE]; - size_t pos = 0; + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); while (1) { - k_sem_take(&rx_sem, K_FOREVER); + // Block until DMA has a full buffer + int16_t *buf = audio_capture_wait(); - uint8_t byte; - while (ring_buf_get(&rx_buf, &byte, 1) == 1) - { + // Run FFT + analysis + audio_process_run(buf, &result); - // Echo the character back - uart_poll_out(uart, byte); + // Send every 4th raw sample (256 values instead of 1024) + // audio_output_raw_decimated(buf, AUDIO_BUF_SAMPLES, 4); - if (byte == '\r' || byte == '\n') - { - if (pos > 0) - { - line[pos] = '\0'; + // Send first 128 FFT bins (0–5.5 kHz, most useful range) + // printk("FFT:"); + // audio_output_csv(&result, 128); - printk("\r\n"); - printk("─────────────────────────────\r\n"); - printk("│ RX: %-24s \r\n", line); - printk("│ Len: %-3d, Time: %-10u \r\n", - pos, k_uptime_get_32()); - printk("─────────────────────────────\r\n"); + // Send summary stats + // audio_output_summary(&result); - pos = 0; - } - } - else if (pos < LINE_BUF_SIZE - 1) - { - line[pos++] = byte; - } - } + // ~2 updates/sec so UART can keep up + k_msleep(500); } } + +// ── Main ──────────────────────────────────────────── + +int main(void) +{ + printk("Audio analysis starting...\n"); + + int ret = audio_process_init(); + if (ret) + { + printk("FFT init failed: %d\n", ret); + return ret; + } + + ret = audio_capture_init(); + if (ret) + { + printk("Capture init failed: %d\n", ret); + return ret; + } + + // Spawn the processing thread + k_thread_create(&proc_thread_data, proc_stack, + PROC_STACK_SIZE, + proc_thread, NULL, NULL, NULL, + PROC_PRIORITY, 0, K_NO_WAIT); + + // Start sampling — TIM6 begins, DMA fills buffers + audio_capture_start(); + + printk("Capture running at ~%d Hz, %d samples/buffer\n", + SAMPLE_RATE, AUDIO_BUF_SAMPLES); + + return 0; +}