working ADC input + FFT analysis + py logger

This commit is contained in:
2026-02-22 11:09:12 -07:00
parent 96d952c84d
commit 0a4785f341
14 changed files with 676 additions and 118 deletions

View File

@@ -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
)

View File

@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
mainmenu "First App"
mainmenu "Audio App"
# Your application configuration options go here

View File

@@ -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

View File

@@ -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 = <ADC_ACQ_TIME(ADC_ACQ_TIME_TICKS, 641)>;
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";
};
};

132
plotter.py Normal file
View File

@@ -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]} <COM_PORT>")
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()

View File

@@ -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
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

19
serial_debug.py Normal file
View File

@@ -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]} <COM_PORT>")
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)")

172
src/audio_capture.c Normal file
View File

@@ -0,0 +1,172 @@
#include "audio_capture.h"
#include <zephyr/kernel.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/irq.h>
#include <stm32g4xx_ll_adc.h>
#include <stm32g4xx_ll_bus.h>
#include <stm32g4xx_ll_dma.h>
#include <stm32g4xx_ll_dmamux.h>
#include <stm32g4xx_ll_tim.h>
/* ── 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;
}

40
src/audio_capture.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef AUDIO_CAPTURE_H
#define AUDIO_CAPTURE_H
#include <stdint.h>
// ── 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

59
src/audio_output.c Normal file
View File

@@ -0,0 +1,59 @@
#include "audio_output.h"
#include <zephyr/kernel.h>
#include <stdio.h>
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");
}

28
src/audio_output.h Normal file
View File

@@ -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

62
src/audio_process.c Normal file
View File

@@ -0,0 +1,62 @@
#include "audio_process.h"
#include <arm_math.h>
#include <math.h>
// 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 (04095) 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;
}

30
src/audio_process.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef AUDIO_PROCESS_H
#define AUDIO_PROCESS_H
#include <stdint.h>
#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

View File

@@ -1,130 +1,79 @@
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/ring_buffer.h>
#include <string.h>
#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 (05.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;
}