Compare commits

..

4 Commits

Author SHA1 Message Date
07ff6d5915 Update README.md 2026-02-22 18:18:30 +00:00
ac01c54f13 readme updt 2026-02-22 11:12:43 -07:00
5b945a9983 readme fix 2026-02-22 11:09:24 -07:00
0a4785f341 working ADC input + FFT analysis + py logger 2026-02-22 11:09:12 -07:00
14 changed files with 771 additions and 150 deletions

View File

@@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.20.0)
set(BOARD nucleo_g474re) set(BOARD nucleo_g474re)
find_package(Zephyr REQUIRED) 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 # SPDX-License-Identifier: Apache-2.0
mainmenu "First App" mainmenu "Audio App"
# Your application configuration options go here # Your application configuration options go here

129
README.md
View File

@@ -1,64 +1,127 @@
# Zephyr UART + LED - Nucleo G474RE # Audio Analysis on Zephyr - Nucleo G474RE
A learning project for [Zephyr RTOS](https://zephyrproject.org/) targeting the **STM32 Nucleo G474RE** dev board! I have always seen Zephyr projects in the wild and want to document my learning process. I've used FreeRTOS in the past and want to rewrite some projects using Zephyr 🙂 A project I built to learn Zephyr RTOS by doing something more involved than blinking an LED. It samples audio via ADC at 44.1 kHz using hardware timer triggering and DMA, runs a real-time FFT with CMSIS-DSP, and streams results over UART to a Python live plotter.
---- I've used FreeRTOS before and wanted to understand how Zephyr handles devicetree, Kconfig, and the kernel primitives. This project ended up touching all of those plus direct STM32 LL/HAL register work, which was a good way to see where Zephyr's abstractions end and the hardware begins.
---
## Hardware ## Hardware
| | | | | |
|---|---| |---|---|
| **Board** | ST Nucleo G474RE | | **Board** | ST Nucleo G474RE |
| **MCU** | STM32G474RE (ARM Cortex-M4F, 170 MHz) | | **MCU** | STM32G474RE (Cortex-M4F, 170 MHz, FPU) |
| **UART** | LPUART1 via onboard ST-Link VCP (PA2/PA3) | | **Audio Input** | Analog signal on PA0 (Arduino A0) |
| **LED** | LD2 on PA5 | | **Console** | LPUART1 via onboard ST-Link VCP (PA2/PA3) |
| **ADC Trigger** | TIM6 TRGO at 44,098.6 Hz |
Connect via USB to the Nucleo's ST-Link port For testing I used a waveform generator feeding a sine wave into PA0. Any 0-3.3V analog source works.
---- ---
## Features ## How It Works
- **Interrupt-driven UART RX** - ISR writes bytes into a ring buffer TIM6 overflows at ~44.1 kHz and triggers an ADC1 conversion via hardware TRGO. DMA transfers each sample into a ping-pong buffer (2 x 1024 samples). On half-transfer and transfer-complete interrupts, a semaphore wakes the processing thread, which runs a 1024-point real FFT using CMSIS-DSP and sends the results over UART.
- **Dedicated RX thread** - sleeps on a semaphore, wakes on data arrival
- **Line accumulation** - buffers input until `\r` or `\n`
- **Formatted output** - prints received line with length and uptime timestamp
- **LED** - on PA5 configured via devicetree overlay (for testing)
---- ```
TIM6 (44.1 kHz) -> ADC1 conversion -> DMA -> ping-pong buffer
|
DMA half/full IRQ
|
proc_thread wakes
|
FFT + RMS + peak detect
|
UART output
```
## Getting Started The CPU spends about 0% of its time on processing (verified with Zephyr's thread analyzer). 98% idle. The Cortex-M4F at 170 MHz handles a 1024-point float FFT in well under a millisecond.
### Prerequisites ---
- [Zephyr SDK](https://docs.zephyrproject.org/latest/develop/getting_started/index.html) installed ## Project Structure
- `west` installed and workspace initialized
### Build & Flash ```
audio_analysis/
├── src/
│ ├── main.c # thread setup, init sequence
│ ├── audio_capture.c/.h # TIM6 + ADC1 + DMA (STM32 LL)
│ ├── audio_process.c/.h # CMSIS-DSP FFT, RMS, peak detection
│ └── audio_output.c/.h # UART formatting (summary, CSV, raw)
├── boards/
│ └── nucleo_g474re.overlay # ADC channel + TIM6 devicetree config
├── prj.conf # Kconfig
├── plotter.py # Python live plotter (matplotlib + pyserial)
└── serial_debug.py # Quick serial diagnostic tool
```
---
## Build and Flash
```bash ```bash
west build # board is set in CMakeLists.txt west build -p
west flash west flash
(optionally)
west debug
``` ```
### Serial Monitor Board is set in CMakeLists.txt so no `-b` needed.
Connect a terminal to the ST-Link Virtual COM Port at a baud rate of **115200**: ## Serial Monitor
Personally, I just use PuTTY! Open a terminal on the ST-Link VCP at 115200 baud. Output looks like:
Type a line and press Enter:
``` ```
───────────────────────────── RAW:2048,2100,2200,...
│ RX: hello FFT:0.12,0.45,3.21,...
│ Len: 5, Time: 6741 RMS: 0.3412 | Peak: 1000.0 Hz (bin 23)
───────────────────────────── Min: -0.8123 | Max: 0.7945
``` ```
With the thread analyzer, the output looks like:
```
Thread analyze:
0x20000150 : STACK: unused 3608 usage 488 / 4096 (11 %); CPU: 0 %
: Total CPU cycles used: 200757869
thread_analyzer : STACK: unused 512 usage 512 / 1024 (50 %); CPU: 1 %
: Total CPU cycles used: 1373467186
sysworkq : STACK: unused 808 usage 216 / 1024 (21 %); CPU: 0 %
: Total CPU cycles used: 1446
idle : STACK: unused 320 usage 64 / 384 (16 %); CPU: 98 %
: Total CPU cycles used: 129000604158
ISR0 : STACK: unused 1832 usage 216 / 2048 (10 %)
```
Very useful for seeing how my threads are behaving, if I am over allocating stack sizing, or bottlenecks.
## Python Plotter
```bash
pip install pyserial matplotlib numpy PyQt5
python plotter.py COM5
```
Shows a live time-domain waveform and frequency spectrum. Data is decimated (every 4th raw sample, first 128 FFT bins) to fit within UART bandwidth at 115200 baud.
---
## What I Learned
**Devicetree and Kconfig** - Devicetree describes what hardware exists (ADC channel on PA0, TIM6 as a basic timer). Kconfig enables software features (CMSIS-DSP, FPU, thread analyzer). They answer different questions and you need both.
**Where Zephyr stops and HAL starts** - Zephyr's ADC API doesn't expose hardware timer triggering. For the TIM6 -> ADC1 trigger routing and DMA setup, I had to use STM32 LL functions directly. The devicetree still handles clock enablement and pin configuration, but the actual peripheral interconnection is done in C with register-level calls.
**DMA ping-pong buffering** - One contiguous buffer, DMA in circular mode, half-transfer and transfer-complete interrupts. While one half fills, the CPU processes the other. No memcpy, just pointer swapping.
**CMSIS-DSP on Cortex-M4F** - arm_rfft_fast_f32 is fast. The FPU matters. Needed to enable specific Kconfig modules (TRANSFORM, COMPLEXMATH, STATISTICS) for each function family used.
**UART is the bottleneck** - At 115200 baud you can push maybe 11 KB/s. Sending 1024 raw samples as ASCII text takes longer than the 500ms between frames. Had to decimate the output and move serial reading to a background thread in Python.
**Thread analyzer** - Adding a few Kconfig lines gives you per-thread CPU% and stack usage. My processing thread uses 11% of its stack and rounds to 0% CPU. The idle thread runs 98% of the time. Good to know before adding more features.
**Timing** - One sample period is 22.7 us (the ADC/DAC tick). One buffer of 1024 samples is 23.2 ms (the processing deadline). Easy to confuse. The per-sample timing is pure hardware. The CPU only needs to keep up at the buffer level.
--- ---
## License ## License

View File

@@ -8,7 +8,7 @@
leds { leds {
compatible = "gpio-leds"; compatible = "gpio-leds";
ld2: led_0 { ld2: led_0 {
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; /* LD2 -> PA5 */ gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; // LD2 -> PA5
label = "LD2"; label = "LD2";
}; };
}; };
@@ -20,4 +20,50 @@
&gpioc { &gpioc {
status = "okay"; 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_SERIAL=y
CONFIG_CONSOLE=y CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_UART_INTERRUPT_DRIVEN=y CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_RING_BUFFER=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/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 static struct k_thread proc_thread_data;
#define LINE_BUF_SIZE 128 static K_THREAD_STACK_DEFINE(proc_stack, PROC_STACK_SIZE);
#define THREAD_STACK 1024
#define THREAD_PRIO 5
// Ring buffer: callback writes, thread reads (could probably be a msg_queue too) static struct audio_result result;
RING_BUF_DECLARE(rx_buf, RX_BUF_SIZE);
// Semaphore to wake the thread (name, initial count, max count) static void proc_thread(void *p1, void *p2, void *p3)
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)
{ {
if (!device_is_ready(uart)) ARG_UNUSED(p1);
{ ARG_UNUSED(p2);
printk("UART not ready\n"); ARG_UNUSED(p3);
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;
while (1) 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; // Run FFT + analysis
while (ring_buf_get(&rx_buf, &byte, 1) == 1) audio_process_run(buf, &result);
{
// Echo the character back // Send every 4th raw sample (256 values instead of 1024)
uart_poll_out(uart, byte); // audio_output_raw_decimated(buf, AUDIO_BUF_SAMPLES, 4);
if (byte == '\r' || byte == '\n') // Send first 128 FFT bins (05.5 kHz, most useful range)
{ // printk("FFT:");
if (pos > 0) // audio_output_csv(&result, 128);
{
line[pos] = '\0';
printk("\r\n"); // Send summary stats
printk("─────────────────────────────\r\n"); // audio_output_summary(&result);
printk("│ RX: %-24s \r\n", line);
printk("│ Len: %-3d, Time: %-10u \r\n",
pos, k_uptime_get_32());
printk("─────────────────────────────\r\n");
pos = 0; // ~2 updates/sec so UART can keep up
} k_msleep(500);
}
else if (pos < LINE_BUF_SIZE - 1)
{
line[pos++] = byte;
}
}
} }
} }
// ── 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;
}