working ADC input + FFT analysis + py logger
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
2
Kconfig
2
Kconfig
@@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mainmenu "First App"
|
||||
mainmenu "Audio App"
|
||||
|
||||
# Your application configuration options go here
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
132
plotter.py
Normal 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()
|
||||
18
prj.conf
18
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
|
||||
|
||||
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
19
serial_debug.py
Normal 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
172
src/audio_capture.c
Normal 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
40
src/audio_capture.h
Normal 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
59
src/audio_output.c
Normal 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
28
src/audio_output.h
Normal 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
62
src/audio_process.c
Normal 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 (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;
|
||||
}
|
||||
30
src/audio_process.h
Normal file
30
src/audio_process.h
Normal 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
|
||||
173
src/main.c
173
src/main.c
@@ -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 (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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user