Compare commits
4 Commits
96d952c84d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 07ff6d5915 | |||
| ac01c54f13 | |||
| 5b945a9983 | |||
| 0a4785f341 |
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
2
Kconfig
2
Kconfig
@@ -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
129
README.md
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -21,3 +21,49 @@
|
|||||||
&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
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()
|
||||||
20
prj.conf
20
prj.conf
@@ -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_UART_INTERRUPT_DRIVEN=y
|
|
||||||
CONFIG_RING_BUFFER=y
|
|
||||||
CONFIG_PRINTK=y
|
CONFIG_PRINTK=y
|
||||||
|
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||||
|
|
||||||
|
CONFIG_RING_BUFFER=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
|
||||||
171
src/main.c
171
src/main.c
@@ -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);
|
||||||
|
|
||||||
|
// Send every 4th raw sample (256 values instead of 1024)
|
||||||
|
// audio_output_raw_decimated(buf, AUDIO_BUF_SAMPLES, 4);
|
||||||
|
|
||||||
|
// Send first 128 FFT bins (0–5.5 kHz, most useful range)
|
||||||
|
// printk("FFT:");
|
||||||
|
// audio_output_csv(&result, 128);
|
||||||
|
|
||||||
|
// Send summary stats
|
||||||
|
// audio_output_summary(&result);
|
||||||
|
|
||||||
|
// ~2 updates/sec so UART can keep up
|
||||||
|
k_msleep(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main ────────────────────────────────────────────
|
||||||
|
|
||||||
|
int main(void)
|
||||||
{
|
{
|
||||||
|
printk("Audio analysis starting...\n");
|
||||||
|
|
||||||
// Echo the character back
|
int ret = audio_process_init();
|
||||||
uart_poll_out(uart, byte);
|
if (ret)
|
||||||
|
|
||||||
if (byte == '\r' || byte == '\n')
|
|
||||||
{
|
{
|
||||||
if (pos > 0)
|
printk("FFT init failed: %d\n", ret);
|
||||||
{
|
return ret;
|
||||||
line[pos] = '\0';
|
}
|
||||||
|
|
||||||
printk("\r\n");
|
ret = audio_capture_init();
|
||||||
printk("─────────────────────────────\r\n");
|
if (ret)
|
||||||
printk("│ RX: %-24s \r\n", line);
|
|
||||||
printk("│ Len: %-3d, Time: %-10u \r\n",
|
|
||||||
pos, k_uptime_get_32());
|
|
||||||
printk("─────────────────────────────\r\n");
|
|
||||||
|
|
||||||
pos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (pos < LINE_BUF_SIZE - 1)
|
|
||||||
{
|
{
|
||||||
line[pos++] = byte;
|
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