Compare commits

9 Commits

Author SHA1 Message Date
e85d51488e Update filesystem tooling and LittleFS defaults 2026-05-11 14:01:56 +02:00
12e82d35d2 sync 2026-05-11 13:04:32 +02:00
7a2428f54d Merge branch 'buzzy_integration' of gitea.iten.pro:edi/buzzer_2 into buzzy_integration 2026-05-11 12:21:09 +02:00
fca4aeb115 sync 2026-05-11 12:18:14 +02:00
35639a2615 Zwischenstand 2026-05-11 11:49:20 +02:00
5d20d2820a feat(logging): board-specific log backends (RTT for buzzy, UART for DK) 2026-05-07 11:23:12 +02:00
ad2dc19641 feat(buzzy): add buzzy board support with SPI flash, fix build warnings
- Register BOARD_ROOT in CMakeLists.txt and sysbuild/CMakeLists.txt
- Add boardRoots to VS Code settings for board picker
- buzzy.dts: add nordic,pm-ext-flash chosen, external-flash and i2s-audio aliases
- nrf52840dk overlay: add external-flash alias (mirrors qspi-flash)
- fs_mgmt/Kconfig: select SPI_NOR for BOARD_BUZZY, NORDIC_QSPI_NOR for DK
- fs_mgmt.c: use external-flash alias instead of qspi-flash
- audio.c: bound snprintf to avoid truncation warning
- prj.conf: remove STACK_SENTINEL (conflicts with MPU_STACK_GUARD)
- mcuboot.conf: remove UART_CONSOLE (no SERIAL on buzzy)
- Delete mcuboot.overlay (no serial recovery needed)

Both buzzy/nrf52840 and nrf52840dk/nrf52840 build cleanly with zero warnings.
2026-05-07 11:00:14 +02:00
79676e2e1e added buzzy board to firmware 2026-05-07 09:34:09 +02:00
e8373bd0c0 Tried to improve audio 2026-05-07 09:24:26 +02:00
33 changed files with 1117 additions and 215 deletions

View File

@@ -2,5 +2,9 @@
"svelte.plugin.svelte.format.config.printWidth": 300, "svelte.plugin.svelte.format.config.printWidth": 300,
"nrf-connect.applications": [ "nrf-connect.applications": [
"${workspaceFolder}/firmware" "${workspaceFolder}/firmware"
] ],
"nrf-connect.boardRoots": [
"${workspaceFolder}/firmware"
],
"cmake.sourceDirectory": "C:/Projekte/buzzer_2/firmware/libs/ble_mgmt"
} }

View File

@@ -1,6 +1,6 @@
VERSION_MAJOR = 0 VERSION_MAJOR = 0
VERSION_MINOR = 0 VERSION_MINOR = 0
PATCHLEVEL = 2 PATCHLEVEL = 6
VERSION_TWEAK = 0 VERSION_TWEAK = 0
#if (IS_ENABLED(CONFIG_LOG)) #if (IS_ENABLED(CONFIG_LOG))
EXTRAVERSION = debug EXTRAVERSION = debug

View File

@@ -0,0 +1,11 @@
### Console / Logging: RTT
CONFIG_USE_SEGGER_RTT=y
CONFIG_CONSOLE=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n
CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_BACKEND_RTT=y
# Keep SPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096

View File

@@ -0,0 +1,2 @@
config BOARD_BUZZY
select SOC_NRF52840_QIAA

View File

@@ -0,0 +1,9 @@
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)

View File

@@ -0,0 +1,5 @@
board:
name: buzzy
vendor: iten
socs:
- name: nrf52840

View File

@@ -0,0 +1,64 @@
#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 3)>, /* BCLK an P0.03 */
<NRF_PSEL(I2S_LRCK_M, 0, 28)>, /* LRCLK an P0.28 */
<NRF_PSEL(I2S_SDOUT, 1, 10)>; /* DIN an P1.10 */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 3)>,
<NRF_PSEL(I2S_LRCK_M, 0, 28)>,
<NRF_PSEL(I2S_SDOUT, 1, 10)>;
low-power-enable;
};
};
spi3_default: spi3_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 2)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 30)>;
};
};
spi3_sleep: spi3_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 2)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 30)>;
low-power-enable;
};
};
/*
* Optional future QSPI pinctrl states (keep disabled for now).
* Use these when switching from &spi3 to &qspi in buzzy.dts.
*/
// qspi_default: qspi_default {
// group1 {
// psels = <NRF_PSEL(QSPI_SCK, 0, 2)>,
// <NRF_PSEL(QSPI_CSN, 0, 5)>,
// <NRF_PSEL(QSPI_IO0, 0, 29)>,
// <NRF_PSEL(QSPI_IO1, 0, 30)>,
// <NRF_PSEL(QSPI_IO2, 0, 31)>,
// <NRF_PSEL(QSPI_IO3, 1, 13)>;
// };
// };
// qspi_sleep: qspi_sleep {
// group1 {
// psels = <NRF_PSEL(QSPI_SCK, 0, 2)>,
// <NRF_PSEL(QSPI_CSN, 0, 5)>,
// <NRF_PSEL(QSPI_IO0, 0, 29)>,
// <NRF_PSEL(QSPI_IO1, 0, 30)>,
// <NRF_PSEL(QSPI_IO2, 0, 31)>,
// <NRF_PSEL(QSPI_IO3, 1, 13)>;
// low-power-enable;
// };
// };
};

View File

@@ -0,0 +1,275 @@
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include <zephyr/dt-bindings/adc/adc.h>
#include <zephyr/dt-bindings/adc/nrf-saadc.h>
#include <zephyr/dt-bindings/gpio/gpio.h>
#include <zephyr/dt-bindings/regulator/nrf5x.h>
#include "buzzy-pinctrl.dtsi"
/ {
model = "Buzzy";
compatible = "iten,buzzy";
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
nordic,pm-ext-flash = &mx25r64;
};
/* SD_MODE pin for MAX98357A power gating */
dac_pwr: dac-pwr {
compatible = "regulator-fixed";
regulator-name = "max98357a-sd-mode";
enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>;
};
/* Simple SW1 setup: internal pull-up on P1.09, active low when pressed. */
buttons {
compatible = "gpio-keys";
sw1: sw1 {
gpios = <&gpio1 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
// gpios = <&gpio1 9 (GPIO_ACTIVE_LOW)>;
label = "SW1";
};
};
/*
* P0.24 is kept as a normal runtime-controlled GPIO (no gpio-hog).
* The app can drive this pin high/low to use it as external SW1 pull-up source.
*/
sw1_ctrl {
compatible = "gpio-leds";
sw1_pullup: sw1_pullup {
gpios = <&gpio0 24 GPIO_ACTIVE_HIGH>;
label = "SW1_PULLUP_CTRL";
};
};
/*
* ETA6003 charger status input on P0.13.
* Note: CHG status is routed through discrete logic (Q1 path), so polarity may
* need to be inverted in software. If needed, switch to GPIO_ACTIVE_LOW.
*/
charger_status {
compatible = "gpio-keys";
chg_status: chg_status {
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
label = "ETA6003_CHG_STATUS";
};
};
/*
* ETA6003 charge-current select (FCHG) on P0.09:
* - 0V -> 500 mA
* - 1.8V -> 1 A
*/
charger_ctrl {
compatible = "gpio-leds";
chg_fast: chg_fast {
gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
label = "ETA6003_FAST_CHARGE";
};
};
/*
* VDDH battery voltage measurement via internal VDDHDIV5 input.
* The SAADC measures VDDH/5; multiply by 5 in software to get the actual VDDH in mV.
* 3.1V threshold -> SAADC reads ~620 mV.
*/
zephyr_user: zephyr,user {
io-channels = <&adc 0>;
io-channel-names = "vddh";
};
aliases {
dac-pwr = &dac_pwr;
sw1 = &sw1;
sw1-pullup = &sw1_pullup;
chg-status = &chg_status;
chg-fast = &chg_fast;
external-flash = &mx25r64;
i2s-audio = &i2s0;
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(48)>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000c000 DT_SIZE_K(472)>;
};
slot1_partition: partition@82000 {
label = "image-1";
reg = <0x00082000 DT_SIZE_K(472)>;
};
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 DT_SIZE_K(32)>;
};
};
};
/* I2S0 is used for audio output; pins are configured in the pinctrl file. */
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};
/*
* External flash over classic SPI (bring-up-first path).
* Net mapping from hardware:
* SCLK -> P0.02
* SI/SIO0-> P0.29
* SO/SIO1-> P0.30
* CS -> P0.05
*/
&spi3 {
status = "okay";
pinctrl-0 = <&spi3_default>;
pinctrl-1 = <&spi3_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
mx25r64: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <8000000>;
jedec-id = [c2 28 17];
size = <DT_SIZE_M(64)>;
has-dpd;
t-enter-dpd = <10000>;
t-exit-dpd = <35000>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
ext_flash_lfs: partition@0 {
label = "ext-littlefs";
reg = <0x00000000 DT_SIZE_M(8)>;
};
};
};
};
/*
* Optional future QSPI variant (keep disabled for now):
* - Disable &spi3 block above.
* - Enable &qspi block below.
* - Keep the same flash partition layout.
*/
// &qspi {
// status = "okay";
// pinctrl-0 = <&qspi_default>;
// pinctrl-1 = <&qspi_sleep>;
// pinctrl-names = "default", "sleep";
// mx25r64: flash@0 {
// compatible = "nordic,qspi-nor";
// reg = <0>;
// jedec-id = [c2 28 17];
// size = <DT_SIZE_M(64)>;
// has-dpd;
// t-enter-dpd = <10000>;
// t-exit-dpd = <35000>;
// /* Net mapping from hardware: *
// * SCK=P0.02, CSN=P0.05, IO0=P0.29, IO1=P0.30, IO2=P0.31, IO3=P1.13
// */
// sck-pin = <2>;
// csn-pins = <5>;
// io-pins = <29>, <30>, <31>, <45>;
// partitions {
// compatible = "fixed-partitions";
// #address-cells = <1>;
// #size-cells = <1>;
// ext_flash_lfs: partition@0 {
// label = "ext-littlefs";
// reg = <0x00000000 DT_SIZE_M(8)>;
// };
// };
// };
// };
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&gpiote {
status = "okay";
};
/* DC/DC mode for regulator 1 (core voltage regulator). */
&reg1 {
regulator-initial-mode = <NRF5X_REG_MODE_DCDC>;
};
/*
* DC/DC mode for regulator 0 (I/O voltage regulator, VDD).
* Default output is 1.8V, suitable for on-board peripherals (flash).
*/
&reg0 {
status = "okay";
};
/*
* Optional gpio-hog variant for external pull-up via R8 from P0.24 to P1.09:
* - Keep this disabled when you want runtime control over P0.24.
* - Remove GPIO_PULL_UP from SW1 gpios above (keep only GPIO_ACTIVE_LOW).
* - Enable the node below to drive P0.24 high from boot.
*/
// &gpio0 {
// sw1_pullup_hog: sw1_pullup_hog {
// gpio-hog;
// gpios = <24 GPIO_ACTIVE_HIGH>;
// output-high;
// line-name = "SW1_PULLUP_SRC";
// };
// };
/*
* Battery voltage measurement via internal VDDHDIV5 channel.
* No external pin needed; VDDH is divided by 5 internally.
*/
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
zephyr,resolution = <12>;
zephyr,input-positive = <NRF_SAADC_VDDHDIV5>;
};
};
/* Use NFC pins as GPIOs. */
&uicr {
nfct-pins-as-gpios;
};

View File

@@ -0,0 +1,10 @@
identifier: buzzy/nrf52840
name: Buzzy
vendor: iten
type: mcu
arch: arm
ram: 256
flash: 1024
toolchain:
- zephyr
supported: []

View File

@@ -0,0 +1,5 @@
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y
CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y
CONFIG_CLOCK_CONTROL_NRF_K32SRC_20PPM=y

View File

@@ -0,0 +1,2 @@
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")

View File

@@ -0,0 +1,4 @@
### Console / Logging: UART
CONFIG_UART_CONSOLE=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_BACKEND_RTT=n

View File

@@ -3,6 +3,7 @@
nordic,pm-ext-flash = &mx25r64; nordic,pm-ext-flash = &mx25r64;
}; };
aliases { aliases {
external-flash = &mx25r64;
qspi-flash = &mx25r64; qspi-flash = &mx25r64;
i2s-audio = &i2s0; i2s-audio = &i2s0;
}; };

8
firmware/debug.conf Normal file
View File

@@ -0,0 +1,8 @@
### Logging
CONFIG_LOG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
CONFIG_DEBUG_OPTIMIZATIONS=y
CONFIG_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y

3
firmware/fs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
lfs_external_flash.hex
.tmp_lfs_build/
*.wav

86
firmware/fs/README.md Normal file
View File

@@ -0,0 +1,86 @@
# FS Tooling
This directory contains the tooling to build and flash the external LittleFS image used by the firmware.
## Python Version
Use Python 3.11.
The filesystem/audio build tool is currently tested with Python 3.11.
Python 3.13 is not supported at the moment, especially on Windows, because some dependencies in the TTS toolchain may fall back to source builds and fail to install.
In particular, the dependency chain around `kokoro`, `spacy`, `thinc`, and `blis` may fail to install on Windows when no compatible wheels are available.
## Install Dependencies
`ffmpeg` is required in addition to the Python packages.
```sh
python3.11 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
```
On Windows:
```bat
py -3.11 -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```
## Build LittleFS Image
```sh
./venv/bin/python build_lfs_audio.py
```
Example with imported WAV files and a custom system prompt YAML:
```sh
./venv/bin/python build_lfs_audio.py \
--wav-dir ./my_wavs \
--sys-yaml ./sys_prompts.yaml
```
Notes:
- `--wav-dir` imports all `.wav` files from the given directory into `/lfs/a`
- `--sys-yaml` overrides the default TTS prompt list for `/lfs/sys`
- generated audio is converted to mono 16 kHz PCM before it is written into the LittleFS image
This generates:
- `lfs_external_flash.hex`
The default image layout is aligned to a 4 KiB flash page layout.
After the build, the script also prints a usage summary for `/lfs/sys`, `/lfs/a`, and the combined image.
Example:
```text
Usage summary:
sys: files=4, raw=48.0 KiB, fs=56.0 KiB (14 blocks, standalone estimate)
audio: files=12, raw=224.0 KiB, fs=264.0 KiB (66 blocks, standalone estimate)
total: raw=272.0 KiB, fs=300.0 KiB (75 blocks in combined image)
```
`raw` is the sum of file sizes in the staging tree.
`fs` is the estimated or measured LittleFS space usage with the configured block size.
## Flash Image
On macOS/Linux:
```sh
./program.sh
```
On Windows:
```bat
program.bat
```
Both scripts use paths relative to their own location, so they can be started from any working directory.

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
import argparse
import shutil
import subprocess
import tempfile
from pathlib import Path
import soundfile as sf
import yaml
from intelhex import IntelHex
from kokoro import KPipeline
from littlefs import LittleFS
DEFAULT_SAMPLE_RATE = 16000
DEFAULT_VOICE = "af_bella"
DEFAULT_BLOCK_SIZE = 4096
DEFAULT_BLOCK_COUNT = 2048
DEFAULT_READ_SIZE = 512
DEFAULT_LOOKAHEAD_SIZE = 256
DEFAULT_FILTERS = [
"highpass=f=120",
"lowpass=f=6000",
"acompressor=threshold=-18dB:ratio=3:attack=5:release=80",
"loudnorm=I=-16:TP=-1.0",
]
DEFAULT_SYS_PROMPTS = [
{"id": "404", "text": "No sound sample was found on the device."},
{"id": "update", "text": "Firmware updated. Awaiting confirmation."},
{"id": "confirm", "text": "State confirmed."},
{"id": "voltest", "text": "Volume test. This is a sample of the current volume level."},
]
def run_ffmpeg(cmd: list[str]) -> None:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def export_raw_pcm(input_path: Path, output_path: Path, sample_rate: int, filters: list[str]) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
filter_str = ",".join(filters)
run_ffmpeg([
"ffmpeg",
"-y",
"-i",
str(input_path),
"-af",
filter_str,
"-ar",
str(sample_rate),
"-ac",
"1",
"-f",
"s16le",
"-acodec",
"pcm_s16le",
str(output_path),
])
def load_sys_prompts(yaml_path: Path | None) -> list[dict]:
if yaml_path is None:
return DEFAULT_SYS_PROMPTS
with open(yaml_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or []
prompts: list[dict] = []
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and item.get("id") and item.get("text"):
prompts.append({"id": item["id"], "text": item["text"]})
elif isinstance(data, dict):
for item in data.get("assets", []):
if isinstance(item, dict) and item.get("id") and item.get("text"):
prompts.append({"id": item["id"], "text": item["text"]})
return prompts
def build_sys_tts(prompts: list[dict], out_sys_dir: Path, sample_rate: int) -> int:
pipeline = KPipeline(lang_code="a")
generated = 0
with tempfile.TemporaryDirectory(prefix="tts_tmp_") as tmp_dir_name:
tmp_dir = Path(tmp_dir_name)
for prompt in prompts:
asset_id = prompt["id"]
text = prompt["text"]
tmp_wav = tmp_dir / f"{asset_id}.wav"
generator = pipeline(text, voice=DEFAULT_VOICE, speed=1.0)
for _, _, audio in generator:
sf.write(tmp_wav, audio, 24000)
break
out_file = out_sys_dir / asset_id
export_raw_pcm(tmp_wav, out_file, sample_rate=sample_rate, filters=DEFAULT_FILTERS)
generated += 1
return generated
def import_wavs_to_a(wav_dir: Path | None, out_a_dir: Path, sample_rate: int) -> int:
if wav_dir is None:
return 0
if not wav_dir.exists() or not wav_dir.is_dir():
raise FileNotFoundError(f"WAV directory not found: {wav_dir}")
count = 0
for wav_file in sorted(wav_dir.glob("*.wav")):
out_file = out_a_dir / wav_file.stem
export_raw_pcm(wav_file, out_file, sample_rate=sample_rate, filters=DEFAULT_FILTERS)
count += 1
return count
def add_files_recursive(fs: LittleFS, local_path: Path, lfs_path: str = "/") -> None:
if not local_path.exists():
return
for item in sorted(local_path.iterdir()):
target_path = f"{lfs_path.rstrip('/')}/{item.name}".replace("//", "/")
if item.is_file():
with open(item, "rb") as f:
with fs.open(target_path, "wb") as lfs_file:
lfs_file.write(f.read())
elif item.is_dir():
fs.mkdir(target_path)
add_files_recursive(fs, item, target_path)
def build_littlefs_image(source_folder: Path, block_size: int, block_count: int) -> tuple[LittleFS, int]:
fs = LittleFS(
block_size=block_size,
block_count=block_count,
read_size=DEFAULT_READ_SIZE,
prog_size=256,
lookahead_size=DEFAULT_LOOKAHEAD_SIZE,
cache_size=4096,
)
add_files_recursive(fs, source_folder)
used_blocks = 0
lfs_buffer = fs.context.buffer
for idx in range(block_count):
offset = idx * block_size
block_data = lfs_buffer[offset : offset + block_size]
if any(byte != 0xFF for byte in block_data):
used_blocks += 1
return fs, used_blocks
def build_littlefs_hex(source_folder: Path, output_hex: Path, block_size: int, block_count: int, start_addr: int) -> int:
fs, used_blocks = build_littlefs_image(source_folder, block_size, block_count)
ih = IntelHex()
lfs_buffer = fs.context.buffer
for idx in range(block_count):
offset = idx * block_size
block_data = lfs_buffer[offset : offset + block_size]
if any(byte != 0xFF for byte in block_data):
ih.frombytes(block_data, offset=start_addr + offset)
ih.tofile(str(output_hex), format="hex")
return used_blocks
def summarize_directory(directory: Path) -> tuple[int, int]:
if not directory.exists():
return 0, 0
file_count = 0
raw_bytes = 0
for file_path in directory.rglob("*"):
if file_path.is_file():
file_count += 1
raw_bytes += file_path.stat().st_size
return file_count, raw_bytes
def format_bytes(size: int) -> str:
units = ["B", "KiB", "MiB", "GiB"]
value = float(size)
for unit in units:
if value < 1024.0 or unit == units[-1]:
if unit == "B":
return f"{int(value)} {unit}"
return f"{value:.1f} {unit}"
value /= 1024.0
def report_directory_usage(label: str, directory: Path, block_size: int, block_count: int) -> None:
file_count, raw_bytes = summarize_directory(directory)
_, used_blocks = build_littlefs_image(directory, block_size, block_count)
fs_bytes = used_blocks * block_size
print(
f" {label}: files={file_count}, raw={format_bytes(raw_bytes)}, "
f"fs={format_bytes(fs_bytes)} ({used_blocks} blocks, standalone estimate)"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Generate Bella-only TTS in /lfs/sys, optionally import WAVs into /lfs/a, "
"then create LittleFS HEX."
)
)
parser.add_argument("--wav-dir", default=None, help="Optional directory with WAV files for /lfs/a")
parser.add_argument("--sys-yaml", default=None, help="Optional YAML with system prompts (id/text)")
parser.add_argument("--sample-rate", type=int, default=DEFAULT_SAMPLE_RATE)
parser.add_argument("--block-size", type=int, default=DEFAULT_BLOCK_SIZE)
parser.add_argument("--block-count", type=int, default=DEFAULT_BLOCK_COUNT)
parser.add_argument("--start-addr", type=lambda x: int(x, 0), default=0x12000000)
parser.add_argument(
"--output-hex",
default=str(Path(__file__).resolve().parent / "lfs_external_flash.hex"),
help="Output path for generated HEX",
)
parser.add_argument(
"--keep-staging",
action="store_true",
help="Keep temporary staging directory for inspection",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
fs_dir = Path(__file__).resolve().parent
output_hex = Path(args.output_hex).resolve()
wav_dir = Path(args.wav_dir).resolve() if args.wav_dir else None
sys_yaml = Path(args.sys_yaml).resolve() if args.sys_yaml else None
staging_root = fs_dir / ".tmp_lfs_build"
if staging_root.exists():
shutil.rmtree(staging_root)
out_sys = staging_root / "lfs" / "sys"
out_a = staging_root / "lfs" / "a"
out_sys.mkdir(parents=True, exist_ok=True)
out_a.mkdir(parents=True, exist_ok=True)
prompts = load_sys_prompts(sys_yaml)
if not prompts:
raise RuntimeError("No system prompts available. Provide --sys-yaml or use defaults.")
tts_count = build_sys_tts(prompts, out_sys, args.sample_rate)
wav_count = import_wavs_to_a(wav_dir, out_a, args.sample_rate)
total_used_blocks = build_littlefs_hex(
source_folder=staging_root,
output_hex=output_hex,
block_size=args.block_size,
block_count=args.block_count,
start_addr=args.start_addr,
)
print(f"Done. TTS assets: {tts_count}, WAV imports: {wav_count}, HEX: {output_hex}")
print("Usage summary:")
report_directory_usage("sys", out_sys, args.block_size, args.block_count)
report_directory_usage("audio", out_a, args.block_size, args.block_count)
print(
f" total: raw={format_bytes(summarize_directory(staging_root)[1])}, "
f"fs={format_bytes(total_used_blocks * args.block_size)} "
f"({total_used_blocks} blocks in combined image)"
)
if not args.keep_staging and staging_root.exists():
shutil.rmtree(staging_root)
if __name__ == "__main__":
main()

23
firmware/fs/buzzy.json Normal file
View File

@@ -0,0 +1,23 @@
{
"firmware_config": {
"peripheral": "QSPI",
"compress": true
},
"pins": {
"sck": 2,
"csn": 5,
"io0": 29,
"io1": 30,
"io2": 31,
"io3": 45
},
"flash_size": 8388608,
"sck_frequency": 8000000,
"address_mode": "MODE24BIT",
"readoc": "READ4IO",
"writeoc": "PP4IO",
"pp_size": "PPSIZE256",
"sck_delay": 0,
"rx_delay": 2,
"page_size": 4096
}

46
firmware/fs/buzzy.toml Normal file
View File

@@ -0,0 +1,46 @@
[qspi]
mem_size = 0x800000
read_mode = "READ4IO"
write_mode = "PP4IO"
address_mode = "BIT24"
frequency = "M16"
spi_mode = "MODE0"
rx_delay = 2
wip_index = 0
page_program_size = "PAGE256"
retain_ram = true
# Pin-Zuweisungen (angepasst an die Netlist)
[qspi.sck]
delay = 0x80
pin = 2
port = 0
[qspi.csn]
pin = 5
port = 0
[qspi.dio0]
pin = 29
port = 0
[qspi.dio1]
pin = 30
port = 0
[qspi.dio2]
pin = 31
port = 0
[qspi.dio3]
pin = 13
port = 1
[qspi.custom]
io2_level = "LEVEL_LOW"
io3_level = "LEVEL_HIGH"
# Aktiviert Quad-IO und High-Performance Mode für Macronix MX25R
instructions = [
{command=0x06, data=[]},
{command=0x01, data=[0x40, 0, 0x2]}
]

1
firmware/fs/program.bat Normal file
View File

@@ -0,0 +1 @@
nrfutil device --x-ext-mem-config-file "%~dp0buzzy.json" program --firmware "%~dp0lfs_external_flash.hex" --options verify=VERIFY_READ,reset=RESET_SYSTEM

4
firmware/fs/program.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
nrfutil device --x-ext-mem-config-file "$SCRIPT_DIR/buzzy.json" program --firmware "$SCRIPT_DIR/lfs_external_flash.hex" --options verify=VERIFY_READ,reset=RESET_SYSTEM

View File

@@ -0,0 +1,7 @@
kokoro>=0.1.0
soundfile
numpy
torch
PyYAML
littlefs-python
intelhex

View File

@@ -1,4 +1,4 @@
if(CONFIG_AUDIO) if(CONFIG_BUZZ_AUDIO)
zephyr_library() zephyr_library()
zephyr_library_sources(src/audio.c) zephyr_library_sources(src/audio.c)
zephyr_include_directories(include) zephyr_include_directories(include)

View File

@@ -1,10 +1,10 @@
menuconfig AUDIO menuconfig BUZZ_AUDIO
bool "Audio handling" bool "Audio handling"
default y default n
select I2S select I2S
select POLL select POLL
if AUDIO if BUZZ_AUDIO
config AUDIO_NO_SAMPLES_SAMPLE config AUDIO_NO_SAMPLES_SAMPLE
string "Audio no samples sample" string "Audio no samples sample"
default "404" default "404"
@@ -57,4 +57,4 @@ if AUDIO
module = AUDIO module = AUDIO
module-str = audio module-str = audio
source "subsys/logging/Kconfig.template.log_config" source "subsys/logging/Kconfig.template.log_config"
endif # AUDIO endif # BUZZ_AUDIO

View File

@@ -13,17 +13,12 @@ const struct device *i2s_dev = DEVICE_DT_GET(DT_ALIAS(i2s_audio));
K_MSGQ_DEFINE(audio_cmd_q, sizeof(struct audio_cmd_msg), 10, 4); K_MSGQ_DEFINE(audio_cmd_q, sizeof(struct audio_cmd_msg), 10, 4);
K_SEM_DEFINE(audio_files_count_sem, 0, 1); K_SEM_DEFINE(audio_files_count_sem, 0, 1);
K_SEM_DEFINE(audio_file_select_sem, 0, 1);
K_MEM_SLAB_DEFINE(audio_cache_slab, CONFIG_AUDIO_CACHE_SLAB_SIZE, CONFIG_AUDIO_CACHE_SLAB_COUNT, 4); K_MEM_SLAB_DEFINE(audio_cache_slab, CONFIG_AUDIO_CACHE_SLAB_SIZE, CONFIG_AUDIO_CACHE_SLAB_COUNT, 4);
struct k_work_q audio_work_q;
K_THREAD_STACK_DEFINE(audio_work_q_stack, CONFIG_AUDIO_WORKQUEUE_STACK_SIZE);
struct k_work select_next_file_work;
enum audio_thread_state_t enum audio_thread_state_t
{ {
AUDIO_ARMED, AUDIO_IDLE,
AUDIO_PRECACHING, AUDIO_PRECACHING,
AUDIO_WAIT_FOR_CACHE, AUDIO_WAIT_FOR_CACHE,
AUDIO_PLAYING, AUDIO_PLAYING,
@@ -35,8 +30,7 @@ enum audio_thread_state_t
#define EV_CACHE_READY BIT(2) #define EV_CACHE_READY BIT(2)
#define EV_CACHE_DONE BIT(3) #define EV_CACHE_DONE BIT(3)
#define EV_STATE_STEP BIT(4) #define EV_STATE_STEP BIT(4)
#define EV_AUTOSTART BIT(5) #define EV_ALL (EV_PLAY_RANDOM | EV_MSGQ_NOT_EMPTY | EV_CACHE_READY | EV_CACHE_DONE | EV_STATE_STEP)
#define EV_ALL (EV_PLAY_RANDOM | EV_MSGQ_NOT_EMPTY | EV_CACHE_READY | EV_CACHE_DONE | EV_STATE_STEP | EV_AUTOSTART)
K_EVENT_DEFINE(audio_events); K_EVENT_DEFINE(audio_events);
@@ -50,13 +44,15 @@ struct audio_ctx_t
char next_file_name[CONFIG_FS_MGMT_MAX_PATH_LENGTH]; char next_file_name[CONFIG_FS_MGMT_MAX_PATH_LENGTH];
struct fs_file_t file; struct fs_file_t file;
bool is_file_open; bool is_file_open;
bool cache_ready_signaled;
uint8_t cached_blocks;
ssize_t audio_size; ssize_t audio_size;
ssize_t cached_bytes; ssize_t cached_bytes;
} audio_ctx; } audio_ctx;
static struct i2s_config i2s_cfg = { static struct i2s_config i2s_cfg = {
.word_size = 16, .word_size = 16,
.channels = 2, .channels = 1,
.format = I2S_FMT_DATA_FORMAT_I2S, .format = I2S_FMT_DATA_FORMAT_I2S,
.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER, .options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER,
.frame_clk_freq = 16000, .frame_clk_freq = 16000,
@@ -70,7 +66,39 @@ K_MUTEX_DEFINE(audio_ctx_mutex);
atomic_t thread_state = ATOMIC_INIT(0); atomic_t thread_state = ATOMIC_INIT(0);
atomic_t num_files = ATOMIC_INIT(0); atomic_t num_files = ATOMIC_INIT(0);
static uint8_t audio_mono_stage[CONFIG_AUDIO_CACHE_SLAB_SIZE / 2]; static const char *audio_state_name(enum audio_thread_state_t state)
{
switch (state)
{
case AUDIO_IDLE:
return "IDLE";
case AUDIO_PRECACHING:
return "PRECACHING";
case AUDIO_WAIT_FOR_CACHE:
return "WAIT_FOR_CACHE";
case AUDIO_PLAYING:
return "PLAYING";
case AUDIO_DRAINING:
return "DRAINING";
default:
return "UNKNOWN";
}
}
static void audio_set_state(enum audio_thread_state_t new_state, const char *reason)
{
enum audio_thread_state_t old_state = atomic_get(&thread_state);
if (old_state != new_state)
{
LOG_INF("Audio state %s -> %s (%s)",
audio_state_name(old_state),
audio_state_name(new_state),
reason);
}
atomic_set(&thread_state, new_state);
}
int audio_queue_play(const char *filename, bool is_interrupt) int audio_queue_play(const char *filename, bool is_interrupt)
{ {
@@ -102,40 +130,32 @@ int audio_queue_play(const char *filename, bool is_interrupt)
} }
/* /*
* Wake immediately for interrupts or when not currently playing. * Wake immediately for interrupts or when idle.
* Non-interrupt commands during playback are picked up after drain. * Non-interrupt commands during active playback are picked up after drain.
*/ */
if (is_interrupt || (atomic_get(&thread_state) != AUDIO_PLAYING)) if (is_interrupt || (atomic_get(&thread_state) == AUDIO_IDLE))
{ {
k_event_set(&audio_events, EV_MSGQ_NOT_EMPTY); k_event_set(&audio_events, EV_MSGQ_NOT_EMPTY);
} }
LOG_DBG("Enqueued audio command: filename='%s', is_interrupt=%d", cmd.filename, cmd.is_interrupt); LOG_INF("Enqueued audio command: filename='%s', is_interrupt=%d, queued=%u", cmd.filename,
cmd.is_interrupt, (unsigned int)k_msgq_num_used_get(&audio_cmd_q));
return 0; return 0;
} }
static int audio_select_random_sound(void) static int audio_select_random_to_buf(char *buf, size_t buf_size)
{ {
k_sem_reset(&audio_file_select_sem);
if (k_sem_take(&audio_files_count_sem, K_FOREVER) != 0) if (k_sem_take(&audio_files_count_sem, K_FOREVER) != 0)
{ {
LOG_ERR("Failed to take audio files count semaphore");
k_sem_give(&audio_files_count_sem);
k_mutex_unlock(&audio_ctx_mutex);
return -EFAULT; return -EFAULT;
} }
int count = atomic_get(&num_files); int count = atomic_get(&num_files);
k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
if (count == 0) if (count == 0)
{ {
LOG_WRN("No audio files available to select, returning no files sound"); LOG_WRN("No audio files available, using fallback sound");
FS_MGMT_ASSEMBLE_PATH(audio_ctx.next_file_name, FS_SYSTEM_PATH, CONFIG_AUDIO_NO_SAMPLES_SAMPLE); snprintf(buf, buf_size, "%s/%s", FS_SYSTEM_PATH, CONFIG_AUDIO_NO_SAMPLES_SAMPLE);
k_sem_give(&audio_file_select_sem);
k_sem_give(&audio_files_count_sem); k_sem_give(&audio_files_count_sem);
k_mutex_unlock(&audio_ctx_mutex);
return -ENOENT; return -ENOENT;
} }
@@ -143,29 +163,22 @@ static int audio_select_random_sound(void)
struct fs_dir_t dir; struct fs_dir_t dir;
struct fs_dirent entry; struct fs_dirent entry;
int rc; int rc;
bool found = false;
fs_dir_t_init(&dir); fs_dir_t_init(&dir);
rc = fs_mgmt_pm_opendir(&dir, FS_AUDIO_PATH); rc = fs_mgmt_pm_opendir(&dir, FS_AUDIO_PATH);
if (rc < 0) if (rc < 0)
{ {
LOG_ERR("Failed to open audio directory '%s': %d", FS_AUDIO_PATH, rc); LOG_ERR("Failed to open audio directory '%s': %d", FS_AUDIO_PATH, rc);
k_sem_give(&audio_file_select_sem);
k_sem_give(&audio_files_count_sem); k_sem_give(&audio_files_count_sem);
k_mutex_unlock(&audio_ctx_mutex);
return rc; return rc;
} }
int current_index = 0; int current_index = 0;
bool found = false;
while (1) while (1)
{ {
rc = fs_readdir(&dir, &entry); rc = fs_readdir(&dir, &entry);
if (rc < 0) if (rc < 0 || entry.name[0] == '\0')
{
LOG_ERR("Directory read error: %d", rc);
break;
}
if (entry.name[0] == '\0')
{ {
break; break;
} }
@@ -173,8 +186,9 @@ static int audio_select_random_sound(void)
{ {
if (current_index == random_index) if (current_index == random_index)
{ {
FS_MGMT_ASSEMBLE_PATH(audio_ctx.next_file_name, FS_AUDIO_PATH, entry.name); snprintf(buf, buf_size, "%s/%.*s", FS_AUDIO_PATH,
LOG_DBG("Selected random audio file: %s", audio_ctx.next_file_name); (int)(buf_size - sizeof(FS_AUDIO_PATH) - 1U), entry.name);
LOG_DBG("Selected random audio file: %s", buf);
found = true; found = true;
break; break;
} }
@@ -183,9 +197,7 @@ static int audio_select_random_sound(void)
} }
fs_mgmt_pm_closedir(&dir); fs_mgmt_pm_closedir(&dir);
k_sem_give(&audio_file_select_sem);
k_sem_give(&audio_files_count_sem); k_sem_give(&audio_files_count_sem);
k_mutex_unlock(&audio_ctx_mutex);
return found ? 0 : -ENOENT; return found ? 0 : -ENOENT;
} }
@@ -230,31 +242,11 @@ void audio_refresh_files(void)
fs_mgmt_pm_closedir(&dir); fs_mgmt_pm_closedir(&dir);
k_sem_give(&audio_files_count_sem); k_sem_give(&audio_files_count_sem);
atomic_set(&num_files, count); atomic_set(&num_files, count);
audio_select_random_sound(); LOG_INF("Audio refresh found %d file(s) in %s", count, FS_AUDIO_PATH);
}
static void select_next_file_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
LOG_DBG("Select next file work handler");
audio_select_random_sound();
} }
static int audio_init(void) static int audio_init(void)
{ {
struct k_work_queue_config audio_work_q_config = {
.name = "audio_work_q",
.no_yield = false,
.essential = true,
.work_timeout_ms = 0};
k_work_queue_start(&audio_work_q,
audio_work_q_stack,
K_THREAD_STACK_SIZEOF(audio_work_q_stack),
CONFIG_AUDIO_WORKQUEUE_PRIORITY, &audio_work_q_config);
k_work_init(&select_next_file_work, select_next_file_work_handler);
if (!device_is_ready(i2s_dev)) if (!device_is_ready(i2s_dev))
{ {
LOG_ERR("I2S device not ready"); LOG_ERR("I2S device not ready");
@@ -272,14 +264,7 @@ static int audio_init(void)
return 0; return 0;
} }
SYS_INIT(audio_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); // Stelle sicher, dass dies nach der FS-Initialisierung erfolgt SYS_INIT(audio_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
static void audio_trigger_next_file_selection(void)
{
k_sem_reset(&audio_file_select_sem);
LOG_DBG("Triggering workq file selection");
k_work_submit_to_queue(&audio_work_q, &select_next_file_work);
}
void audio_start_random_playback(void) void audio_start_random_playback(void)
{ {
@@ -293,11 +278,12 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
ARG_UNUSED(arg2); ARG_UNUSED(arg2);
ARG_UNUSED(arg3); ARG_UNUSED(arg3);
LOG_INF("Audio thread waiting for filesystem ready event");
k_event_wait(&event_mgmt_events, EVENT_MGMT_FS_READY, false, K_FOREVER); k_event_wait(&event_mgmt_events, EVENT_MGMT_FS_READY, false, K_FOREVER);
LOG_INF("Audio thread received filesystem ready event");
audio_refresh_files(); audio_refresh_files();
atomic_set(&thread_state, AUDIO_PRECACHING); audio_set_state(AUDIO_IDLE, "filesystem ready");
k_event_set(&audio_events, EV_STATE_STEP);
while (1) while (1)
{ {
@@ -311,65 +297,89 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
k_event_clear(&audio_events, EV_STATE_STEP); k_event_clear(&audio_events, EV_STATE_STEP);
} }
/*
* EV_PLAY_RANDOM: button press requests a random sound.
* If idle, start it immediately. Otherwise queue it as a non-interrupt command
* so it plays after the current sound finishes.
*/
if (active_events & EV_PLAY_RANDOM) if (active_events & EV_PLAY_RANDOM)
{ {
LOG_DBG("Play random event received");
k_event_clear(&audio_events, EV_PLAY_RANDOM); k_event_clear(&audio_events, EV_PLAY_RANDOM);
if (state == AUDIO_ARMED) char random_file[CONFIG_FS_MGMT_MAX_PATH_LENGTH];
if (audio_select_random_to_buf(random_file, sizeof(random_file)) == 0)
{ {
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); if (state == AUDIO_IDLE)
atomic_set(&thread_state, AUDIO_PLAYING); {
k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
strncpy(audio_ctx.next_file_name, random_file, sizeof(audio_ctx.next_file_name) - 1);
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex);
audio_set_state(AUDIO_PRECACHING, "play random while idle");
k_event_set(&audio_events, EV_STATE_STEP);
} }
else else
{ {
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP); struct audio_cmd_msg cmd;
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); strncpy(cmd.filename, random_file, sizeof(cmd.filename) - 1);
cmd.filename[sizeof(cmd.filename) - 1] = '\0';
audio_select_random_sound(); cmd.is_interrupt = false;
atomic_set(&thread_state, AUDIO_PRECACHING); if (k_msgq_put(&audio_cmd_q, &cmd, K_NO_WAIT) != 0)
k_event_set(&audio_events, EV_STATE_STEP); {
LOG_WRN("Random play queue full, discarding");
}
else
{
LOG_INF("Random play queued: '%s'", random_file);
}
}
} }
continue; continue;
} }
switch (state) switch (state)
{ {
case AUDIO_PRECACHING: case AUDIO_IDLE:
LOG_DBG("Audio thread starting precache task");
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_START);
atomic_set(&thread_state, AUDIO_WAIT_FOR_CACHE);
break;
case AUDIO_WAIT_FOR_CACHE:
if (active_events & EV_CACHE_READY)
{
k_event_clear(&audio_events, EV_CACHE_READY);
atomic_set(&thread_state, AUDIO_ARMED);
if (k_event_wait(&audio_events, EV_AUTOSTART, false, K_NO_WAIT) & EV_AUTOSTART)
{
k_event_clear(&audio_events, EV_AUTOSTART);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START);
atomic_set(&thread_state, AUDIO_PLAYING);
LOG_DBG("Autostarting queued audio playback");
}
else
{
LOG_DBG("System Armed. Waiting for Buzzer...");
audio_trigger_next_file_selection();
}
}
break;
case AUDIO_ARMED:
if (active_events & EV_MSGQ_NOT_EMPTY) if (active_events & EV_MSGQ_NOT_EMPTY)
{ {
k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP);
struct audio_cmd_msg cmd; struct audio_cmd_msg cmd;
if (k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT) != 0)
{
LOG_WRN("EV_MSGQ_NOT_EMPTY set but queue empty");
break;
}
k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1);
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex);
LOG_INF("Dequeued command while idle: '%s'", cmd.filename);
audio_set_state(AUDIO_PRECACHING, "command while idle");
k_event_set(&audio_events, EV_STATE_STEP);
}
break;
case AUDIO_PRECACHING:
LOG_INF("Precaching '%s'", audio_ctx.next_file_name);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_PREPARE);
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_START);
audio_set_state(AUDIO_WAIT_FOR_CACHE, "cache start requested");
break;
case AUDIO_WAIT_FOR_CACHE:
if (active_events & EV_MSGQ_NOT_EMPTY)
{
k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY);
struct audio_cmd_msg cmd;
if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt)
{
/* Interrupt while caching: flush and restart for the new file */
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_PREPARE);
k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT); k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT);
k_mutex_lock(&audio_ctx_mutex, K_FOREVER); k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
@@ -377,9 +387,20 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex); k_mutex_unlock(&audio_ctx_mutex);
k_event_set(&audio_events, EV_AUTOSTART); LOG_INF("Interrupt while caching, switching to '%s'", cmd.filename);
atomic_set(&thread_state, AUDIO_PRECACHING); audio_set_state(AUDIO_PRECACHING, "interrupt while caching");
k_event_set(&audio_events, EV_STATE_STEP); k_event_set(&audio_events, EV_STATE_STEP);
break;
}
/* Non-interrupt: already queued, will play after current */
}
if (active_events & EV_CACHE_READY)
{
k_event_clear(&audio_events, EV_CACHE_READY);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START);
audio_set_state(AUDIO_PLAYING, "cache primed, starting");
LOG_INF("Playback started: '%s'", audio_ctx.next_file_name);
} }
break; break;
@@ -389,31 +410,25 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY);
struct audio_cmd_msg cmd; struct audio_cmd_msg cmd;
if (k_msgq_peek(&audio_cmd_q, &cmd) == 0) if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt)
{ {
if (cmd.is_interrupt)
{
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP); k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT);
if (k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT) == 0)
{
k_mutex_lock(&audio_ctx_mutex, K_FOREVER); k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1);
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex); k_mutex_unlock(&audio_ctx_mutex);
k_event_set(&audio_events, EV_AUTOSTART); LOG_INF("Interrupting playback with '%s'", cmd.filename);
atomic_set(&thread_state, AUDIO_PRECACHING); audio_set_state(AUDIO_PRECACHING, "interrupt during playback");
k_event_set(&audio_events, EV_STATE_STEP); k_event_set(&audio_events, EV_STATE_STEP);
} }
}
else else
{ {
LOG_DBG("Non-interrupt command queued during playback; will process after drain"); LOG_DBG("Non-interrupt command queued; will play after drain");
} }
}
break; break;
} }
@@ -421,27 +436,61 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
{ {
k_event_clear(&audio_events, EV_CACHE_DONE); k_event_clear(&audio_events, EV_CACHE_DONE);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
atomic_set(&thread_state, AUDIO_DRAINING); audio_set_state(AUDIO_DRAINING, "cache complete");
} }
break; break;
case AUDIO_DRAINING: case AUDIO_DRAINING:
{
if (active_events & EV_MSGQ_NOT_EMPTY)
{
k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY);
struct audio_cmd_msg cmd;
if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt)
{
k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT);
k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1);
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex);
LOG_INF("Interrupt during drain, switching to '%s'", cmd.filename);
audio_set_state(AUDIO_PRECACHING, "interrupt during drain");
k_event_set(&audio_events, EV_STATE_STEP);
break;
}
}
if (k_mem_slab_num_free_get(&audio_cache_slab) == CONFIG_AUDIO_CACHE_SLAB_COUNT) if (k_mem_slab_num_free_get(&audio_cache_slab) == CONFIG_AUDIO_CACHE_SLAB_COUNT)
{ {
LOG_DBG("Audio for file drained, ready for next file"); LOG_DBG("Drain complete");
if (k_msgq_num_used_get(&audio_cmd_q) > 0) if (k_msgq_num_used_get(&audio_cmd_q) > 0)
{ {
k_event_set(&audio_events, EV_MSGQ_NOT_EMPTY); struct audio_cmd_msg cmd;
k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT);
k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1);
audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0';
k_mutex_unlock(&audio_ctx_mutex);
LOG_INF("Drain complete, next: '%s'", cmd.filename);
audio_set_state(AUDIO_PRECACHING, "drain complete, queued command");
k_event_set(&audio_events, EV_STATE_STEP);
} }
else else
{ {
atomic_set(&thread_state, AUDIO_PRECACHING); audio_set_state(AUDIO_IDLE, "drain complete, no more commands");
k_event_set(&audio_events, EV_STATE_STEP);
} }
} }
break; break;
} }
} }
}
} }
K_THREAD_DEFINE(audio_thread_id, CONFIG_AUDIO_THREAD_STACK_SIZE, audio_thread, NULL, NULL, NULL, K_THREAD_DEFINE(audio_thread_id, CONFIG_AUDIO_THREAD_STACK_SIZE, audio_thread, NULL, NULL, NULL,
@@ -453,7 +502,6 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
ARG_UNUSED(arg2); ARG_UNUSED(arg2);
ARG_UNUSED(arg3); ARG_UNUSED(arg3);
uint8_t num_channels = 1;
void *mem_slab; void *mem_slab;
while (1) while (1)
{ {
@@ -487,10 +535,13 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
if (rc == 0) if (rc == 0)
{ {
bool signal_cache_ready = false;
k_mutex_lock(&audio_ctx_mutex, K_FOREVER); k_mutex_lock(&audio_ctx_mutex, K_FOREVER);
if (!audio_ctx.is_file_open) if (!audio_ctx.is_file_open)
{ {
LOG_INF("Opening audio file '%s' for playback", audio_ctx.next_file_name);
rc = fs_mgmt_pm_open(&audio_ctx.file, audio_ctx.next_file_name, FS_O_READ); rc = fs_mgmt_pm_open(&audio_ctx.file, audio_ctx.next_file_name, FS_O_READ);
if (rc < 0) if (rc < 0)
{ {
@@ -502,8 +553,10 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
} }
audio_ctx.is_file_open = true; audio_ctx.is_file_open = true;
audio_ctx.cache_ready_signaled = false;
audio_ctx.cached_blocks = 0;
// NEU: Größen- und Zähler-Initialisierung exklusiv hier! /* File length and cache position are initialized once per playback start. */
audio_ctx.audio_size = fs_mgmt_get_audio_data_len(&audio_ctx.file); audio_ctx.audio_size = fs_mgmt_get_audio_data_len(&audio_ctx.file);
audio_ctx.cached_bytes = 0; audio_ctx.cached_bytes = 0;
@@ -533,8 +586,8 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
continue; continue;
} }
ssize_t bytes_to_read = MIN(CONFIG_AUDIO_CACHE_SLAB_SIZE / 2, remaining_bytes); ssize_t bytes_to_read = MIN(CONFIG_AUDIO_CACHE_SLAB_SIZE, remaining_bytes);
ssize_t bytes_read = fs_read(&audio_ctx.file, audio_mono_stage, bytes_to_read); ssize_t bytes_read = fs_read(&audio_ctx.file, mem_slab, bytes_to_read);
if (bytes_read <= 0) // <= 0, um EOF (0) und Fehler (< 0) abzufangen if (bytes_read <= 0) // <= 0, um EOF (0) und Fehler (< 0) abzufangen
{ {
@@ -556,9 +609,9 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
audio_ctx.cached_bytes += bytes_read; audio_ctx.cached_bytes += bytes_read;
// LOG_DBG("Cached %u%% ", (int)((audio_ctx.cached_bytes * 100) / audio_ctx.audio_size)); // LOG_DBG("Cached %u%% ", (int)((audio_ctx.cached_bytes * 100) / audio_ctx.audio_size));
if (bytes_read > CONFIG_AUDIO_CACHE_SLAB_SIZE / 2) if (bytes_read > CONFIG_AUDIO_CACHE_SLAB_SIZE)
{ {
LOG_ERR("Read size %d exceeds half slab size %d", (int)bytes_read, CONFIG_AUDIO_CACHE_SLAB_SIZE / 2); LOG_ERR("Read size %d exceeds slab size %d", (int)bytes_read, CONFIG_AUDIO_CACHE_SLAB_SIZE);
k_mem_slab_free(&audio_cache_slab, &mem_slab); k_mem_slab_free(&audio_cache_slab, &mem_slab);
fs_close(&audio_ctx.file); fs_close(&audio_ctx.file);
audio_ctx.is_file_open = false; audio_ctx.is_file_open = false;
@@ -569,9 +622,9 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
if ((bytes_read & 0x1) != 0) if ((bytes_read & 0x1) != 0)
{ {
if (bytes_read >= (CONFIG_AUDIO_CACHE_SLAB_SIZE / 2)) if (bytes_read >= (CONFIG_AUDIO_CACHE_SLAB_SIZE))
{ {
LOG_ERR("Odd mono byte count at half-slab boundary: %d", (int)bytes_read); LOG_ERR("Odd PCM byte count at slab boundary: %d", (int)bytes_read);
k_mem_slab_free(&audio_cache_slab, &mem_slab); k_mem_slab_free(&audio_cache_slab, &mem_slab);
fs_close(&audio_ctx.file); fs_close(&audio_ctx.file);
audio_ctx.is_file_open = false; audio_ctx.is_file_open = false;
@@ -580,31 +633,23 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
continue; continue;
} }
audio_mono_stage[bytes_read] = 0; ((uint8_t *)mem_slab)[bytes_read] = 0;
bytes_read++; bytes_read++;
} }
if (bytes_read < CONFIG_AUDIO_CACHE_SLAB_SIZE / 2) if (bytes_read < CONFIG_AUDIO_CACHE_SLAB_SIZE)
{ {
memset(audio_mono_stage + bytes_read, 0, (CONFIG_AUDIO_CACHE_SLAB_SIZE / 2) - bytes_read); memset((uint8_t *)mem_slab + bytes_read, 0, (CONFIG_AUDIO_CACHE_SLAB_SIZE) - bytes_read);
bytes_read = CONFIG_AUDIO_CACHE_SLAB_SIZE / 2; bytes_read = CONFIG_AUDIO_CACHE_SLAB_SIZE;
} }
if (num_channels == 1) audio_ctx.cached_blocks++;
{
uint8_t *dst = (uint8_t *)mem_slab;
for (size_t i = 0; i < (size_t)bytes_read; i += 2) if (!audio_ctx.cache_ready_signaled &&
(audio_ctx.cached_blocks >= 2 || audio_ctx.cached_bytes >= audio_ctx.audio_size))
{ {
uint8_t lo = audio_mono_stage[i]; audio_ctx.cache_ready_signaled = true;
uint8_t hi = audio_mono_stage[i + 1]; signal_cache_ready = true;
size_t out = i * 2;
dst[out] = lo;
dst[out + 1] = hi;
dst[out + 2] = lo;
dst[out + 3] = hi;
}
} }
k_mutex_unlock(&audio_ctx_mutex); k_mutex_unlock(&audio_ctx_mutex);
@@ -614,6 +659,13 @@ void audio_pump_thread(void *arg1, void *arg2, void *arg3)
k_mem_slab_free(&audio_cache_slab, &mem_slab); k_mem_slab_free(&audio_cache_slab, &mem_slab);
continue; continue;
} }
if (signal_cache_ready)
{
LOG_DBG("Cache ready for '%s' after %u block(s)", audio_ctx.next_file_name,
(unsigned int)audio_ctx.cached_blocks);
k_event_set(&audio_events, EV_CACHE_READY);
}
} }
} }
} }

View File

@@ -246,8 +246,10 @@ int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name)
} }
const char *name_to_use = (device_name != NULL) ? device_name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME; const char *name_to_use = (device_name != NULL) ? device_name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME;
LOG_INF("BLE init: set_device_name");
set_device_name(name_to_use); set_device_name(name_to_use);
LOG_INF("BLE init: bt_le_adv_start");
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (rc) if (rc)
{ {

View File

@@ -7,7 +7,9 @@ menuconfig FS_MGMT
select FILE_SYSTEM_LITTLEFS select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS select FILE_SYSTEM_MKFS
select FLASH_PAGE_LAYOUT select FLASH_PAGE_LAYOUT
select NORDIC_QSPI_NOR if SOC_SERIES_NRF52X && (SOC_NRF52840_QIAA || SOC_NRF52833_QIAA) select SPI_NOR if BOARD_BUZZY
select PM_OVERRIDE_EXTERNAL_DRIVER_CHECK if BOARD_BUZZY
select NORDIC_QSPI_NOR if BOARD_NRF52840DK_NRF52840
help help
Library for initializing and managing the file system. Library for initializing and managing the file system.
@@ -56,13 +58,13 @@ if FS_MGMT
endif # SOC_SERIES_NRF52X endif # SOC_SERIES_NRF52X
config FS_LITTLEFS_READ_SIZE config FS_LITTLEFS_READ_SIZE
default 256 default 512
config FS_LITTLEFS_PROG_SIZE config FS_LITTLEFS_PROG_SIZE
default 256 default 256
config FS_LITTLEFS_CACHE_SIZE config FS_LITTLEFS_CACHE_SIZE
default 4096 default 4096
config FS_LITTLEFS_LOOKAHEAD_SIZE config FS_LITTLEFS_LOOKAHEAD_SIZE
default 512 default 256
module = FS_MGMT module = FS_MGMT
module-str = fs_mgmt module-str = fs_mgmt

View File

@@ -1,5 +1,6 @@
#include <zephyr/fs/littlefs.h> #include <zephyr/fs/littlefs.h>
#include <zephyr/fs/fs.h> #include <zephyr/fs/fs.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/sys/byteorder.h> #include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/pm/device.h> #include <zephyr/pm/device.h>
@@ -11,11 +12,18 @@
LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL); LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
#define FS_PARTITION_ID FLASH_AREA_ID(littlefs_storage) /* Prefer external LittleFS partition when present, otherwise internal storage partition. */
#if DT_NODE_EXISTS(DT_NODELABEL(ext_flash_lfs))
#define FS_PARTITION_ID FIXED_PARTITION_ID(ext_flash_lfs)
#elif DT_NODE_EXISTS(DT_NODELABEL(storage_partition))
#define FS_PARTITION_ID FIXED_PARTITION_ID(storage_partition)
#else
#error "No compatible LittleFS partition node label found (expected ext_flash_lfs or storage_partition)"
#endif
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash) #define EXTERNAL_FLASH_NODE DT_ALIAS(external_flash)
static const struct device *flash_dev = DEVICE_DT_GET(QSPI_FLASH_NODE); static const struct device *flash_dev = DEVICE_DT_GET(EXTERNAL_FLASH_NODE);
#define TAG_MAGIC "TAG!" #define TAG_MAGIC "TAG!"
#define TAG_FORMAT_VERSION 1U #define TAG_FORMAT_VERSION 1U

5
firmware/pm_static.yml Normal file
View File

@@ -0,0 +1,5 @@
# External Flash
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -1,7 +1,3 @@
### Logging
CONFIG_LOG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
### Bluetooth ### Bluetooth
CONFIG_BLE_MGMT=y CONFIG_BLE_MGMT=y
@@ -14,8 +10,3 @@ CONFIG_PM_DEVICE=y
### Stack ### Stack
CONFIG_MAIN_STACK_SIZE=2048 CONFIG_MAIN_STACK_SIZE=2048
CONFIG_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y
CONFIG_STACK_SENTINEL=y
# CONFIG_LOG_MODE_IMMEDIATE=y

View File

@@ -5,7 +5,7 @@
#include "fs_mgmt.h" #include "fs_mgmt.h"
#include "buzz_proto.h" #include "buzz_proto.h"
#include "fw_mgmt.h" #include "fw_mgmt.h"
#include "audio.h" // #include "audio.h"
LOG_MODULE_REGISTER(main); LOG_MODULE_REGISTER(main);
@@ -44,6 +44,7 @@ void ble_rx_cb(const uint8_t *data, uint16_t len)
buzz_proto_buf_free(&buf); /* Speicher bei Fehler sofort wieder freigeben */ buzz_proto_buf_free(&buf); /* Speicher bei Fehler sofort wieder freigeben */
} }
} }
#endif #endif
int main(void) int main(void)
@@ -55,16 +56,19 @@ int main(void)
LOG_ERR("Failed to initialize BLE management: %d", rc); LOG_ERR("Failed to initialize BLE management: %d", rc);
return rc; return rc;
} }
LOG_WRN("After BLE init");
#else
LOG_WRN("BLE not enabled");
#endif #endif
LOG_INF("Init complete. Starting audio playback test..."); LOG_WRN("Main park loop active");
k_sleep(K_SECONDS(1)); // k_sleep(K_MSEC(500));
audio_queue_play("/lfs/sys/update", false); // LOG_INF("Playing test audio files...");
k_sleep(K_SECONDS(1)); // audio_queue_play("/lfs/sys/update", false);
audio_start_random_playback(); // Starte die Wiedergabe eines zufälligen Sounds // audio_queue_play("/lfs/sys/confirm", false);
k_sleep(K_SECONDS(1)); for (;;) {
audio_queue_play("/lfs/sys/404", true); int32_t rem_ms = k_sleep(K_FOREVER);
LOG_WRN("main woke unexpectedly (remaining=%d ms)", rem_ms);
k_sleep(K_FOREVER); }
} }

View File

@@ -1,6 +1,5 @@
CONFIG_LOG=y CONFIG_LOG=y
# CONFIG_MCUBOOT_SERIAL=y # CONFIG_MCUBOOT_SERIAL=y
CONFIG_UART_CONSOLE=y
# CONFIG_SINGLE_APPLICATION_SLOT=n # CONFIG_SINGLE_APPLICATION_SLOT=n
# CONFIG_MCUBOOT_INDICATION_LED=y # CONFIG_MCUBOOT_INDICATION_LED=y
# CONFIG_BOOT_SERIAL_CDC_ACM=y # CONFIG_BOOT_SERIAL_CDC_ACM=y

View File

@@ -1,13 +0,0 @@
/ {
aliases {
mcuboot-button0 = &button0;
mcuboot-led0 = &led0;
};
};
/* Step 2.1 - Configure CDC ACM */
&zephyr_udc0 {
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
};
};