From 11a2badedbf95fda6d62e28e8730ae6d138940a7 Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Tue, 24 Feb 2026 17:36:20 +0100 Subject: [PATCH] sync --- CMakeLists.txt | 11 +- VERSION | 5 + bench_lfs.py | 238 ++++++++++++++++++ boards/nrf52840dk_nrf52840.conf | 1 + boards/nrf52840dk_nrf52840.overlay | 53 ++++ pm_static.yml | 4 + prj.conf | 46 ++++ send_file.py | 321 ++++++++++++++++++++++++ src/audio.c | 387 +++++++++++++++++++++++++++++ src/audio.h | 35 +++ src/fs.c | 23 ++ src/fs.h | 10 + src/io.c | 91 +++++++ src/io.h | 23 ++ src/main.c | 77 +++++- src/usb.c | 184 ++++++++++++++ src/usb.h | 9 + 17 files changed, 1515 insertions(+), 3 deletions(-) create mode 100644 VERSION create mode 100644 bench_lfs.py create mode 100644 boards/nrf52840dk_nrf52840.conf create mode 100644 boards/nrf52840dk_nrf52840.overlay create mode 100644 pm_static.yml create mode 100644 send_file.py create mode 100644 src/audio.c create mode 100644 src/audio.h create mode 100644 src/fs.c create mode 100644 src/fs.h create mode 100644 src/io.c create mode 100644 src/io.h create mode 100644 src/usb.c create mode 100644 src/usb.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 58c04cc..5a10101 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,4 +3,13 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(buzzer) -target_sources(app PRIVATE src/main.c) +target_sources(app PRIVATE + src/main.c + src/fs.c + src/audio.c + src/io.c + src/usb.c +) +zephyr_include_directories(src) + + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6657576 --- /dev/null +++ b/VERSION @@ -0,0 +1,5 @@ +VERSION_MAJOR = 0 +VERSION_MINOR = 0 +PATCHLEVEL = 1 +VERSION_TWEAK = 0 +EXTRAVERSION = 0 \ No newline at end of file diff --git a/bench_lfs.py b/bench_lfs.py new file mode 100644 index 0000000..ed08613 --- /dev/null +++ b/bench_lfs.py @@ -0,0 +1,238 @@ +import argparse +import time + +import serial + + +DEFAULT_SWEEP_CASES = [ + (4096, 100, 100), + (4096, 100, 1), + (512, 400, 400), + (1024, 200, 200), + (2048, 100, 100), + (4096, 500, 500), +] + + +def wait_line(serial_port, timeout_s): + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + raw = serial_port.readline() + if not raw: + continue + line = raw.decode("utf-8", errors="ignore").strip() + if line: + return line + return "" + + +def parse_bench_result(line): + parts = line.split(";") + if len(parts) < 8 or parts[0] != "BENCH": + return None + + try: + return { + "block": int(parts[1]), + "count": int(parts[2]), + "sync_every": int(parts[3]), + "total_bytes": int(parts[4]), + "sync_ms": int(parts[5]), + "total_ms": int(parts[6]), + "kib_s": int(parts[7]), + } + except ValueError: + return None + + +def run_bench(port, baudrate, block_size, count, sync_every, timeout_s, verbose=True): + command = f"BENCH {block_size} {count} {sync_every}\n" + + with serial.Serial(port, baudrate, timeout=0.2, write_timeout=None) as serial_port: + serial_port.dtr = True + time.sleep(0.1) + serial_port.reset_input_buffer() + + if verbose: + print(f"Verbindung: {port} @ {baudrate}") + print(f"Sende Befehl: {command.strip()}") + + serial_port.write(command.encode("utf-8")) + serial_port.flush() + + bench_line = "" + bench_result = None + final_status = "" + + start = time.monotonic() + while time.monotonic() - start < timeout_s: + line = wait_line(serial_port, 0.5) + if not line: + continue + + if line.startswith("BENCH;"): + candidate = parse_bench_result(line) + if candidate is None: + continue + + if ( + candidate["block"] == block_size + and candidate["count"] == count + and candidate["sync_every"] == sync_every + ): + bench_line = line + bench_result = candidate + if verbose: + print(f"Ergebnis: {bench_line}") + elif verbose: + print(f"Info: Fremdes BENCH-Ergebnis ignoriert: {line}") + continue + + if line in {"OK", "ERR"}: + final_status = line + break + + if verbose: + print(f"Info: {line}") + + if final_status != "OK": + if verbose: + print(f"Fehler: Kein OK erhalten (Status: '{final_status or 'timeout'}')") + return 1, None + + if not bench_line: + if verbose: + print("Hinweis: Kein BENCH-Datensatz empfangen.") + return 0, None + + result = bench_result + if verbose and result is not None: + print( + "Zusammenfassung: " + f"block={result['block']}, runs={result['count']}, sync_every={result['sync_every']}, " + f"bytes={result['total_bytes']}, sync_ms={result['sync_ms']}, " + f"total_ms={result['total_ms']}, speed={result['kib_s']} KiB/s" + ) + + return 0, result + + +def parse_case_string(case_text): + cleaned = case_text.lower().replace(" ", "") + for separator in ("x", ":", ","): + cleaned = cleaned.replace(separator, ";") + parts = cleaned.split(";") + if len(parts) != 3: + raise ValueError(f"Ungültiges Case-Format: '{case_text}'") + block_size = max(int(parts[0]), 1) + count = max(int(parts[1]), 1) + sync_every = max(int(parts[2]), 1) + return block_size, count, sync_every + + +def run_sweep(port, baudrate, timeout_s, case_strings): + if case_strings: + cases = [parse_case_string(text) for text in case_strings] + else: + cases = DEFAULT_SWEEP_CASES + + print(f"Starte Sweep mit {len(cases)} Fällen ...") + rows = [] + + for index, (block_size, count, sync_every) in enumerate(cases, start=1): + print(f"[{index}/{len(cases)}] BENCH {block_size} {count} {sync_every}") + status, result = run_bench( + port, + baudrate, + block_size, + count, + sync_every, + timeout_s, + verbose=False, + ) + + if status != 0 or result is None: + rows.append({ + "block": block_size, + "count": count, + "sync_every": sync_every, + "kib_s": "FAIL", + "total_ms": "-", + "sync_ms": "-", + }) + continue + + rows.append({ + "block": result["block"], + "count": result["count"], + "sync_every": result["sync_every"], + "kib_s": result["kib_s"], + "total_ms": result["total_ms"], + "sync_ms": result["sync_ms"], + }) + + print("\nErgebnis-Tabelle") + print("block count sync_every speed(KiB/s) total_ms sync_ms") + print("----- ----- ---------- ------------ -------- -------") + for row in rows: + print( + f"{str(row['block']).rjust(5)} " + f"{str(row['count']).rjust(5)} " + f"{str(row['sync_every']).rjust(10)} " + f"{str(row['kib_s']).rjust(12)} " + f"{str(row['total_ms']).rjust(8)} " + f"{str(row['sync_ms']).rjust(7)}" + ) + + numeric_rows = [row for row in rows if isinstance(row["kib_s"], int)] + if numeric_rows: + best = max(numeric_rows, key=lambda row: row["kib_s"]) + print( + "\nBestes Ergebnis: " + f"block={best['block']}, count={best['count']}, sync_every={best['sync_every']}, " + f"speed={best['kib_s']} KiB/s" + ) + + return 0 if numeric_rows else 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="LittleFS write benchmark über CDC-Protokoll") + parser.add_argument("-p", "--port", required=True, help="Serieller Port (z.B. COM12)") + parser.add_argument("-b", "--baud", type=int, default=2500000, help="Baudrate (Standard: 2500000)") + parser.add_argument("--block", type=int, default=4096, help="Blockgröße in Bytes (Standard: 4096)") + parser.add_argument("--count", type=int, default=100, help="Anzahl Writes (Standard: 100)") + parser.add_argument( + "--sync-every", + type=int, + default=100, + help="fs_sync Intervall in Writes (Standard: 100, also nur am Ende)", + ) + parser.add_argument("--timeout", type=float, default=45.0, help="Timeout in Sekunden (Standard: 45)") + parser.add_argument( + "--sweep", + action="store_true", + help="Führt mehrere vordefinierte BENCH-Fälle nacheinander aus", + ) + parser.add_argument( + "--case", + action="append", + default=[], + help="Eigener Sweep-Fall als block,count,sync_every (mehrfach möglich)", + ) + + args = parser.parse_args() + + if args.sweep: + exit(run_sweep(args.port, args.baud, args.timeout, args.case)) + + status, _ = run_bench( + args.port, + args.baud, + max(args.block, 1), + max(args.count, 1), + max(args.sync_every, 1), + args.timeout, + verbose=True, + ) + exit(status) diff --git a/boards/nrf52840dk_nrf52840.conf b/boards/nrf52840dk_nrf52840.conf new file mode 100644 index 0000000..76eeb5d --- /dev/null +++ b/boards/nrf52840dk_nrf52840.conf @@ -0,0 +1 @@ +CONFIG_NRFX_POWER=y \ No newline at end of file diff --git a/boards/nrf52840dk_nrf52840.overlay b/boards/nrf52840dk_nrf52840.overlay new file mode 100644 index 0000000..861f4cc --- /dev/null +++ b/boards/nrf52840dk_nrf52840.overlay @@ -0,0 +1,53 @@ +/ { + aliases { + sleep-led = &led0; + usb-led = &led1; + + status-led = &led2; + buzzer-button = &button0; + audio-i2s = &i2s0; + }; + + chosen { + nordic,pm-ext-flash = &mx25r64; + }; + + zephyr,user { + usb-detect-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + }; +}; + +&usbd { + status = "okay"; + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + status = "okay"; + }; +}; + + +&pinctrl { + i2s0_default: i2s0_default { + group1 { + psels = , /* SCK/Bit Clock */ + , /* WS/Word Select */ + ; /* SD/Serial Data */ + }; + }; + + i2s0_sleep: i2s0_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; + +&i2s0 { + status = "okay"; + pinctrl-0 = <&i2s0_default>; + pinctrl-1 = <&i2s0_sleep>; + pinctrl-names = "default", "sleep"; +}; \ No newline at end of file diff --git a/pm_static.yml b/pm_static.yml new file mode 100644 index 0000000..f78811f --- /dev/null +++ b/pm_static.yml @@ -0,0 +1,4 @@ +littlefs_storage: + address: 0x0 + size: 0x800000 + region: external_flash diff --git a/prj.conf b/prj.conf index 8b13789..5cac18d 100644 --- a/prj.conf +++ b/prj.conf @@ -1 +1,47 @@ +# --- GPIO & Logging --- +CONFIG_GPIO=y +CONFIG_LOG=y +CONFIG_POLL=y +# --- Power Management (Fix für HAS_PM & Policy) --- +# CONFIG_PM=y +CONFIG_PM_DEVICE=y + +# --- Flash & Filesystem --- +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FILE_SYSTEM_MKFS=y +CONFIG_CRC=y +CONFIG_FS_LITTLEFS_READ_SIZE=64 +CONFIG_FS_LITTLEFS_PROG_SIZE=256 +CONFIG_FS_LITTLEFS_CACHE_SIZE=512 +CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128 +CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512 + +# --- USB Device & CDC ACM --- +CONFIG_USB_DEVICE_STACK=y +CONFIG_DEPRECATION_TEST=y +CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten" +CONFIG_USB_DEVICE_PRODUCT="Edi's Buzzer" +CONFIG_USB_DEVICE_PID=0x0001 +CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y +CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y +CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n +CONFIG_USB_DEVICE_STACK_NEXT=n + +# --- UART (für USB-CDC) --- +CONFIG_SERIAL=y +CONFIG_UART_INTERRUPT_DRIVEN=y +CONFIG_UART_LINE_CTRL=y + +# --- I2S & Audio --- +CONFIG_I2S=y +CONFIG_NRFX_I2S=y + +# --- Random & HW Info (für Audio-File-Auswahl) --- +CONFIG_HWINFO=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_BOOT_BANNER=n +CONFIG_NCS_BOOT_BANNER=n \ No newline at end of file diff --git a/send_file.py b/send_file.py new file mode 100644 index 0000000..80a9b89 --- /dev/null +++ b/send_file.py @@ -0,0 +1,321 @@ +import argparse +import os +import sys +import time +import zlib + +import serial + +REQUIRED_PROTOCOL_VERSION = 3 + + +def calculate_crc32(file_path): + with open(file_path, "rb") as file_handle: + data = file_handle.read() + return data, len(data), zlib.crc32(data) & 0xFFFFFFFF + + +def wait_for_tokens(serial_port, timeout_s, accepted_tokens): + deadline = time.monotonic() + timeout_s + last_line = "" + + while time.monotonic() < deadline: + raw = serial_port.readline() + if not raw: + continue + + line = raw.decode("utf-8", errors="ignore").strip() + if not line: + continue + + last_line = line + if line in accepted_tokens: + return True, line + if line == "ERR": + return False, line + + return False, last_line + + +def query_device_info(serial_port, timeout_s): + attempts = 3 + per_attempt_timeout = max(timeout_s / attempts, 1.5) + + for _ in range(attempts): + serial_port.reset_input_buffer() + serial_port.write(b"INFO\n") + serial_port.flush() + + deadline = time.monotonic() + per_attempt_timeout + parsed_info = None + + while time.monotonic() < deadline: + raw = serial_port.readline() + if not raw: + continue + + line = raw.decode("utf-8", errors="ignore").strip() + if not line: + continue + + if line == "ERR": + break + + if line == "OK": + if parsed_info is not None: + return parsed_info + continue + + parts = line.split(";") + if len(parts) < 5: + continue + + try: + version = int(parts[0]) + recommended = int(parts[4]) + ack_window = int(parts[5]) if len(parts) >= 6 else 1 + except ValueError: + continue + + if recommended <= 0: + recommended = None + + if ack_window <= 0: + ack_window = 1 + + parsed_info = (version, recommended, ack_window) + + time.sleep(0.15) + + return None, None, None + + +def send_file_once( + port, + baudrate, + target_path, + data, + file_size, + crc, + chunk_size, + timeout_s, + write_timeout_s, + pace_us, +): + with serial.Serial(port, baudrate, timeout=0.2, write_timeout=write_timeout_s) as serial_port: + serial_port.dtr = True + time.sleep(0.1) + serial_port.reset_input_buffer() + + protocol_version, recommended_chunk, ack_window = query_device_info(serial_port, timeout_s) + if protocol_version is None: + print("Fehler: Konnte INFO-Antwort des Geräts nicht auswerten.") + return 1 + + if protocol_version != REQUIRED_PROTOCOL_VERSION: + print( + f"Fehler: Inkompatible Protokollversion {protocol_version} " + f"(erwartet {REQUIRED_PROTOCOL_VERSION})." + ) + return 1 + + if recommended_chunk is not None: + selected_chunk = recommended_chunk + else: + selected_chunk = chunk_size + + selected_chunk = max(64, selected_chunk) + print( + f"Gerät meldet Protokoll v{protocol_version}, " + f"Chunk={selected_chunk}, ACK-Window={ack_window}" + ) + + wait_window = ack_window + + command = f"SEND {target_path} {file_size} {crc} {selected_chunk}\n" + print(f"Verbindung: {port} @ {baudrate}") + print(f"Sende Befehl: {command.strip()} (CRC32=0x{crc:08X})") + serial_port.write(command.encode("utf-8")) + serial_port.flush() + + ready_ok, ready_response = wait_for_tokens(serial_port, timeout_s, {"OK"}) + if not ready_ok: + print(f"Fehler: Gerät nicht bereit. Antwort: '{ready_response}'") + return 1 + + print(f"Übertrage {file_size} Bytes in Blöcken à {selected_chunk} Bytes ...") + print(f"Warte auf CONT alle {wait_window} Chunks") + sent = 0 + chunks_since_ack = 0 + start = time.monotonic() + + while sent < file_size: + end = min(sent + selected_chunk, file_size) + try: + serial_port.write(data[sent:end]) + except serial.SerialTimeoutException: + time.sleep(0.02) + continue + + sent = end + chunks_since_ack += 1 + + if pace_us > 0: + time.sleep(pace_us / 1_000_000.0) + + progress = int((sent * 100) / file_size) if file_size else 100 + sys.stdout.write(f"\rFortschritt: {sent}/{file_size} Bytes ({progress}%)") + sys.stdout.flush() + + if sent < file_size and chunks_since_ack >= wait_window: + cont_ok, cont_response = wait_for_tokens(serial_port, timeout_s, {"CONT"}) + if not cont_ok: + print(f"\nFehler beim Chunk-Ack: '{cont_response}'") + return 1 + chunks_since_ack = 0 + + serial_port.flush() + duration = max(time.monotonic() - start, 0.001) + rate_kib_s = (file_size / 1024.0) / duration + print(f"\nUpload beendet: {rate_kib_s:.1f} KiB/s") + + final_ok, final_response = wait_for_tokens(serial_port, timeout_s, {"OK"}) + if final_ok: + print("Übertragung erfolgreich abgeschlossen (CRC geprüft).") + return 0 + + print(f"Fehler beim Abschluss: '{final_response}'") + return 1 + + +def send_file( + port, + baudrate, + file_path, + target_path, + chunk_size, + timeout_s, + retries, + write_timeout_s, + pace_us, +): + if not os.path.exists(file_path): + print(f"Fehler: Lokale Datei '{file_path}' nicht gefunden.") + return 1 + + if not target_path.startswith("/"): + print("Fehler: Zielpfad muss mit '/' beginnen (z.B. /lfs/test).") + return 1 + + data, file_size, crc = calculate_crc32(file_path) + attempts = retries + 1 + + for attempt in range(1, attempts + 1): + if attempt > 1: + print(f"\nNeuer Versuch {attempt}/{attempts} ...") + + try: + result = send_file_once( + port, + baudrate, + target_path, + data, + file_size, + crc, + chunk_size, + timeout_s, + write_timeout_s, + pace_us, + ) + if result == 0: + return 0 + except serial.SerialException as error: + print(f"Serial Fehler: {error}") + except Exception as error: + print(f"Allgemeiner Fehler: {error}") + + if attempt < attempts: + time.sleep(0.4) + + print(f"Upload fehlgeschlagen nach {attempts} Versuch(en).") + return 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Datei über Serial-Protokoll an Zephyr/nRF senden" + ) + + parser.add_argument( + "-p", + "--port", + required=True, + help="Serieller Port (z.B. COM12 oder /dev/ttyACM0)", + ) + parser.add_argument( + "-f", + "--file", + required=True, + help="Lokaler Pfad zur Datei", + ) + parser.add_argument( + "-t", + "--target", + default="/lfs/test", + help="Zielpfad auf dem Gerät (Standard: /lfs/test)", + ) + parser.add_argument( + "-b", + "--baud", + type=int, + default=115200, + help="Baudrate (Standard: 115200)", + ) + parser.add_argument( + "--chunk-size", + type=int, + default=1024, + help="Fallback-Chunkgröße in Bytes falls INFO keine Empfehlung liefert (Standard: 1024)", + ) + parser.add_argument( + "--timeout", + type=float, + default=8.0, + help="Timeout für Geräteantworten in Sekunden (Standard: 8)", + ) + parser.add_argument( + "--retries", + type=int, + default=0, + help="Anzahl automatischer Wiederholungen bei Fehlern (Standard: 0)", + ) + parser.add_argument( + "--write-timeout", + type=float, + default=0.0, + help="Serial write timeout in Sekunden (0 = blockierend, Standard: 0)", + ) + parser.add_argument( + "--pace-us", + type=int, + default=300, + help="Pause nach jedem Block in Mikrosekunden (Standard: 300)", + ) + arguments = parser.parse_args() + retry_count = max(arguments.retries, 0) + chunk_size = max(arguments.chunk_size, 64) + pace_us = max(arguments.pace_us, 0) + write_timeout_s = None if arguments.write_timeout <= 0 else arguments.write_timeout + sys.exit( + send_file( + arguments.port, + arguments.baud, + arguments.file, + arguments.target, + chunk_size, + arguments.timeout, + retry_count, + write_timeout_s, + pace_us, + ) + ) diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..bfeaa37 --- /dev/null +++ b/src/audio.c @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG); + +K_MEM_SLAB_DEFINE(audio_slab, AUDIO_BLOCK_SIZE, AUDIO_BLOCK_COUNT, 4); +K_MSGQ_DEFINE(free_slab_msgq, sizeof(void *), AUDIO_BLOCK_COUNT, 4); +K_MSGQ_DEFINE(filled_slab_msgq, sizeof(void *), AUDIO_BLOCK_COUNT, 4); + +K_SEM_DEFINE(audio_start_sem, 0, 1); // Buzzer +K_SEM_DEFINE(fs_sync_sem, 0, 1); // USB Sync + +/* Get the node identifier for the alias "audio-i2s" */ +#define I2S_NODE DT_ALIAS(audio_i2s) + +/* Verify the node exists to avoid cryptic compiler errors */ +#if !DT_NODE_EXISTS(I2S_NODE) +#error "Audio I2S alias not defined in devicetree" +#endif + +static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE); +static struct k_poll_event poll_events[3]; +static struct fs_file_t cached_file; +static bool cached_file_open; +static bool cached_file_eof; +static char cached_filename[64]; + +int get_random_file(const char *path, char *out_filename, size_t max_len); + +static int audio_prepare_random_file(void) +{ + int rc; + + if (cached_file_open) + { + fs_close(&cached_file); + cached_file_open = false; + } + + rc = get_random_file(AUDIO_PATH, cached_filename, sizeof(cached_filename)); + if (rc < 0) + { + LOG_ERR("No random file available in %s (%d)", AUDIO_PATH, rc); + return rc; + } + + fs_file_t_init(&cached_file); + rc = fs_open(&cached_file, cached_filename, FS_O_READ); + if (rc < 0) + { + LOG_ERR("Failed to open cache file %s (%d)", cached_filename, rc); + return rc; + } + + cached_file_open = true; + cached_file_eof = false; + LOG_DBG("Priming from file: %s", cached_filename); + + return 0; +} + +static int audio_fill_slab_from_cache(void *block) +{ + ssize_t bytes_read; + uint8_t *bytes = block; + + if (!cached_file_open || cached_file_eof) + { + return -ENODATA; + } + + bytes_read = fs_read(&cached_file, bytes, AUDIO_BLOCK_SIZE); + if (bytes_read < 0) + { + LOG_ERR("fs_read failed: %d", (int)bytes_read); + return (int)bytes_read; + } + + if (bytes_read == 0) + { + cached_file_eof = true; + return -ENODATA; + } + + if (bytes_read < AUDIO_BLOCK_SIZE) + { + memset(&bytes[bytes_read], 0, AUDIO_BLOCK_SIZE - bytes_read); + cached_file_eof = true; + } + + return 0; +} + +static void audio_prime_prefill(uint32_t target_blocks) +{ + uint32_t primed = 0; + + while (primed < target_blocks) + { + void *block; + + if (k_msgq_get(&free_slab_msgq, &block, K_MSEC(20)) != 0) + { + break; + } + + if (audio_fill_slab_from_cache(block) == 0) + { + if (k_msgq_put(&filled_slab_msgq, &block, K_NO_WAIT) == 0) + { + primed++; + } + else + { + k_mem_slab_free(&audio_slab, &block); + break; + } + } + else + { + k_mem_slab_free(&audio_slab, &block); + break; + } + } + + LOG_DBG("Prefilled %u/%u slabs", primed, target_blocks); +} + +int get_random_file(const char *path, char *out_filename, size_t max_len) +{ + struct fs_dir_t dirp; + struct fs_dirent entry; + int file_count = 0; + int rc; + + fs_dir_t_init(&dirp); + + rc = fs_opendir(&dirp, path); + if (rc < 0) + return rc; + + while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') + { + if (entry.type == FS_DIR_ENTRY_FILE) + { + file_count++; + } + } + fs_closedir(&dirp); + + if (file_count == 0) + return -ENOENT; + + uint32_t random_index = sys_rand32_get() % file_count; + + rc = fs_opendir(&dirp, path); + if (rc < 0) + return rc; + + int current_index = 0; + while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') + { + if (entry.type == FS_DIR_ENTRY_FILE) + { + if (current_index == random_index) + { + snprintf(out_filename, max_len, "%s/%s", path, entry.name); + break; + } + current_index++; + } + } + + fs_closedir(&dirp); + return 0; +} + +void audio_thread(void *arg1, void *arg2, void *arg3) +{ + LOG_DBG("Audio thread started"); + int rc; + + k_poll_event_init(&poll_events[0], K_POLL_TYPE_SEM_AVAILABLE, K_POLL_MODE_NOTIFY_ONLY, &audio_start_sem); + k_poll_event_init(&poll_events[1], K_POLL_TYPE_SEM_AVAILABLE, K_POLL_MODE_NOTIFY_ONLY, &fs_sync_sem); + k_poll_event_init(&poll_events[2], K_POLL_TYPE_MSGQ_DATA_AVAILABLE, K_POLL_MODE_NOTIFY_ONLY, &free_slab_msgq); + + while (1) + { + bool is_playing = false; + io_status(false); + + uint32_t queued = 0; + + rc = audio_prepare_random_file(); + if (rc == 0) + { + audio_prime_prefill(AUDIO_BLOCK_COUNT); + } + else + { + LOG_ERR("Failed to prepare audio file, will retry on play event: %d", rc); + k_sleep(K_SECONDS(5)); + continue; + } + + while (1) + { + k_poll(poll_events, ARRAY_SIZE(poll_events), K_FOREVER); + if (poll_events[0].state & K_POLL_STATE_SEM_AVAILABLE) + { + int trigger_rc; + int drop_rc; + void *block; + + queued = 0; + + LOG_DBG("Handling PLAY event"); + k_sem_take(&audio_start_sem, K_NO_WAIT); + poll_events[0].state = K_POLL_STATE_NOT_READY; + + if (is_playing) + { + LOG_DBG("Audio already playing, canceling current playback and restarting"); + drop_rc = i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + if (drop_rc < 0) + { + LOG_WRN("I2S drop trigger failed: %d", drop_rc); + } + + while (k_msgq_get(&filled_slab_msgq, &block, K_NO_WAIT) == 0) + { + k_mem_slab_free(&audio_slab, &block); + } + + rc = audio_prepare_random_file(); + if (rc == 0) + { + audio_prime_prefill(MIN(2, AUDIO_BLOCK_COUNT)); // Sofort mit 2 Blöcken vorfüllen für schnelleren Restart + } + else + { + LOG_ERR("Failed to prepare audio file, will retry on play event: %d", rc); + break; + } + } + LOG_INF("PLAY: %u slabs ready (prefilled)", k_msgq_num_used_get(&filled_slab_msgq)); + + + while (k_msgq_get(&filled_slab_msgq, &block, K_NO_WAIT) == 0) + { + rc = i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE); + if (rc == 0) + { + queued++; + } + else + { + LOG_ERR("i2s_write prefilled block failed: %d", rc); + k_mem_slab_free(&audio_slab, &block); + } + } + + if (queued > 0) + { + trigger_rc = i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); + if (trigger_rc < 0) + { + LOG_DBG("I2S start trigger failed: %d", trigger_rc); + } + else + { + is_playing = true; + io_status(true); + } + } + else + { + LOG_WRN("PLAY requested, but no prefilled slabs available"); + } + } + if (poll_events[1].state & K_POLL_STATE_SEM_AVAILABLE) + { + LOG_DBG("Handling FS SYNC event"); + k_sem_take(&fs_sync_sem, K_NO_WAIT); + poll_events[1].state = K_POLL_STATE_NOT_READY; + } + if (poll_events[2].state & K_POLL_STATE_MSGQ_DATA_AVAILABLE) + { + void *block; + + while (k_msgq_get(&free_slab_msgq, &block, K_NO_WAIT) == 0) + { + if (audio_fill_slab_from_cache(block) == 0) + { + if (is_playing) + { + rc = i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE); + if (rc != 0) + { + LOG_ERR("i2s_write refill block failed: %d", rc); + k_mem_slab_free(&audio_slab, &block); + } + } + else if (k_msgq_put(&filled_slab_msgq, &block, K_NO_WAIT) != 0) + { + k_mem_slab_free(&audio_slab, &block); + LOG_ERR("Audio not playing, but filled queue is full, freeing slab"); + } + } + else + { + k_mem_slab_free(&audio_slab, &block); + } + } + poll_events[2].state = K_POLL_STATE_NOT_READY; + } + if (is_playing && cached_file_eof) + { + LOG_INF("Reached end of file, draining I2S"); + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); + break; + } + } + } +} + +K_THREAD_DEFINE(audio_thread_id, AUDIO_THREAD_STACK_SIZE, audio_thread, NULL, NULL, NULL, AUDIO_THREAD_PRIORITY, 0, 0); + +void slab_thread(void *arg1, void *arg2, void *arg3) +{ + LOG_DBG("Slab thread started"); + void *block; + while (1) + { + k_mem_slab_alloc(&audio_slab, &block, K_FOREVER); + k_msgq_put(&free_slab_msgq, &block, K_FOREVER); + } +} + +K_THREAD_DEFINE(slab_thread_id, 512, slab_thread, NULL, NULL, NULL, AUDIO_THREAD_PRIORITY + 1, 0, 0); + +int audio_init(void) +{ + LOG_DBG("Initializing audio subsystem..."); + if (!device_is_ready(i2s_dev)) + { + LOG_ERR("I2S device not ready"); + return -ENODEV; + } + + /* Initial configuration of the I2S peripheral */ + struct i2s_config config = { + .word_size = AUDIO_WORD_WIDTH, + .channels = 2, + .format = I2S_FMT_DATA_FORMAT_I2S, + .options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER, + .frame_clk_freq = AUDIO_SAMPLE_RATE, + .mem_slab = &audio_slab, + .block_size = AUDIO_BLOCK_SIZE, + .timeout = SYS_FOREVER_MS, + }; + + int ret = i2s_configure(i2s_dev, I2S_DIR_TX, &config); + if (ret < 0) + { + LOG_ERR("Failed to configure I2S: %d", ret); + return ret; + } + + LOG_INF("Audio subsystem initialized successfully, %u bits @ %u.%03u kHz", + config.word_size, config.frame_clk_freq / 1000, config.frame_clk_freq % 1000); + return 0; +} + +void audio_play(void) +{ + LOG_DBG("Posting PLAY event"); + k_sem_give(&audio_start_sem); +} \ No newline at end of file diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..9328e23 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,35 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#define AUDIO_PATH "/lfs/a" +#define AUDIO_EVENT_PLAY BIT(0) +#define AUDIO_EVENT_STOP BIT(1) +#define AUDIO_EVENT_SYNC BIT(8) + +#define AUDIO_THREAD_STACK_SIZE 2048 +#define AUDIO_THREAD_PRIORITY 5 +#define AUDIO_EVENTS_MASK (AUDIO_EVENT_PLAY | AUDIO_EVENT_STOP | AUDIO_EVENT_SYNC) + +#define AUDIO_BLOCK_SIZE 1024 +#define AUDIO_BLOCK_COUNT 4 +#define AUDIO_WORD_WIDTH 16 +#define AUDIO_SAMPLE_RATE 16000 + +/** + * @brief Initializes the audio subsystem + * + * @return 0 on success, negative error code on failure + */ +int audio_init(void); + +/** + * @brief Plays an audio file from the filesystem + */ +void audio_play(void); + +/** + * @brief Stops the currently playing audio + */ +void audio_stop(void); + +#endif // AUDIO_H \ No newline at end of file diff --git a/src/fs.c b/src/fs.c new file mode 100644 index 0000000..dde5774 --- /dev/null +++ b/src/fs.c @@ -0,0 +1,23 @@ +#include +#include +LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG); + +#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage) +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); + +static struct fs_mount_t fs_storage_mnt = { + .type = FS_LITTLEFS, + .fs_data = &fs_storage_data, + .storage_dev = (void *)STORAGE_PARTITION_ID, + .mnt_point = "/lfs", +}; + +int fs_init(void) { + int rc = fs_mount(&fs_storage_mnt); + if (rc < 0) { + LOG_ERR("Error mounting filesystem: %d", rc); + return rc; + } + LOG_DBG("Filesystem mounted successfully"); + return 0; +} \ No newline at end of file diff --git a/src/fs.h b/src/fs.h new file mode 100644 index 0000000..7591ea7 --- /dev/null +++ b/src/fs.h @@ -0,0 +1,10 @@ +#ifndef FS_H +#define FS_H + +#include + +/** + * @brief Initializes the filesystem by mounting it + */ +int fs_init(void); +#endif // FS_H \ No newline at end of file diff --git a/src/io.c b/src/io.c new file mode 100644 index 0000000..fe3b1fb --- /dev/null +++ b/src/io.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +LOG_MODULE_REGISTER(io, LOG_LEVEL_DBG); + +#define STATUS_LED_NODE DT_ALIAS(status_led) +#define USB_LED_NODE DT_ALIAS(usb_led) +#define BUZZER_BUTTON_NODE DT_ALIAS(buzzer_button) + +static const struct gpio_dt_spec led_spec = GPIO_DT_SPEC_GET(STATUS_LED_NODE, gpios); +static const struct gpio_dt_spec usb_led_spec = GPIO_DT_SPEC_GET(USB_LED_NODE, gpios); +static const struct gpio_dt_spec button_spec = GPIO_DT_SPEC_GET(BUZZER_BUTTON_NODE, gpios); + +static struct gpio_callback button_cb_data; +static struct k_work_delayable debounce_work; + +void button_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { + gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_DISABLE); + + LOG_DBG("Button pressed, triggering audio play"); + audio_play(); + + k_work_reschedule(&debounce_work, K_MSEC(50)); +} + +static void debounce_work_handler(struct k_work *work) +{ + gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_EDGE_TO_ACTIVE); + LOG_DBG("Button debounce expired, re-enabling interrupt"); +} + +int io_init(void) +{ + LOG_DBG("Initializing I/O subsystem..."); + + if (!device_is_ready(led_spec.port)) + { + LOG_ERR("LED GPIO device not ready"); + return -ENODEV; + } + int ret = gpio_pin_configure_dt(&led_spec, GPIO_OUTPUT_INACTIVE); + if (ret < 0) + { + LOG_ERR("Failed to configure LED GPIO: %d", ret); + return ret; + } + + if (!device_is_ready(usb_led_spec.port)) + { + LOG_ERR("USB LED GPIO device not ready"); + return -ENODEV; + } + ret = gpio_pin_configure_dt(&usb_led_spec, GPIO_OUTPUT_INACTIVE); + if (ret < 0) + { + LOG_ERR("Failed to configure USB LED GPIO: %d", ret); + return ret; + } + + if (!device_is_ready(button_spec.port)) + { + LOG_ERR("Button GPIO device not ready"); + return -ENODEV; + } + ret = gpio_pin_configure_dt(&button_spec, GPIO_INPUT); + if (ret < 0) { + LOG_ERR("Failed to configure Button GPIO: %d", ret); + return ret; + } + + k_work_init_delayable(&debounce_work, debounce_work_handler); + gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_EDGE_TO_ACTIVE); + gpio_init_callback(&button_cb_data, button_isr, BIT(button_spec.pin)); + gpio_add_callback(button_spec.port, &button_cb_data); + + LOG_DBG("I/O subsystem initialized successfully"); + LOG_DBG("Button: %s.%02u, LED: %s.%02u", button_spec.port->name, button_spec.pin, led_spec.port->name, led_spec.pin); + return 0; +} + +void io_status(bool status) +{ + gpio_pin_set_dt(&led_spec, status ? 1 : 0); +} + +void io_usb_status(bool connected) +{ + gpio_pin_set_dt(&usb_led_spec, connected ? 1 : 0); +} \ No newline at end of file diff --git a/src/io.h b/src/io.h new file mode 100644 index 0000000..790cce7 --- /dev/null +++ b/src/io.h @@ -0,0 +1,23 @@ +#ifndef IO_H +#define IO_H +#include +#include + +/** + * @brief Initializes the I/O subsystem, including GPIOs and any related hardware + * @return 0 on success, negative error code on failure + */ +int io_init(void); + +/** + * @brief Sets the status of the I/O subsystem + * @param status The status to set + */ +void io_status(bool status); + +/** + * @brief Sets the USB connection status indicator + * @param connected True if USB is connected, false otherwise + */ +void io_usb_status(bool connected); +#endif \ No newline at end of file diff --git a/src/main.c b/src/main.c index 75a9c07..de58a7e 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,79 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); + +void print_device_id(void) { + uint8_t device_id[8]; // 64 Bit = 8 Bytes + ssize_t length; + + // Device ID auslesen + length = hwinfo_get_device_id(device_id, sizeof(device_id)); + + if (length > 0) { + char id_str[17]; // 16 Zeichen + Null-Terminator + for (int i = 0; i < length; i++) { + sprintf(&id_str[i * 2], "%02x", device_id[i]); + } + LOG_INF("Board Device ID: %s", id_str); + } else { + LOG_ERR("Konnte Device ID nicht lesen"); + } +} + +static int print_custom_banner(void) +{ + + printk("\x1b[44m\x1b[2J\x1b[H"); + + // Oberer Rahmen + printk("\x1b[1;37m┌───────────────────────────────────────────┐\n"); + printk("│ Edis Buzzer Version: %-20s │\n", APP_VERSION_STRING); + printk("├───────────────────────────────────────────┤\n"); + printk("│ \x1b[22;37mZephyr Version: \x1b[1;37m%-20s │\n", KERNEL_VERSION_STRING); + printk("│ \x1b[22;37mNCS Version: \x1b[1;37m%-20s │\n", NCS_VERSION_STRING); + printk("│ \x1b[22;37mBuild Time: \x1b[1;37m%-10s %8s │\n", __DATE__, __TIME__); + printk("└───────────────────────────────────────────┘\x1b[0m\n"); + + return 0; +} + +SYS_INIT(print_custom_banner, PRE_KERNEL_1, 0); int main(void) { - return 0; -} + LOG_INF("Starting Edis Buzzer Application"); + print_device_id(); + + int rc; + + rc = fs_init(); + if (rc < 0) { + LOG_ERR("Filesystem initialization failed: %d", rc); + return rc; + } + + rc = audio_init(); + if (rc < 0) { + LOG_ERR("Audio initialization failed: %d", rc); + return rc; + } + + rc = io_init(); + if (rc < 0) { + LOG_ERR("I/O initialization failed: %d", rc); + return rc; + } +} \ No newline at end of file diff --git a/src/usb.c b/src/usb.c new file mode 100644 index 0000000..93c13f5 --- /dev/null +++ b/src/usb.c @@ -0,0 +1,184 @@ +#include +#include +#include +#include +#include +#if defined(CONFIG_SOC_SERIES_NRF52X) +#include +#elif defined(CONFIG_SOC_SERIES_STM32G0X) +// STM32 spezifische Header hier +#else +#error "Unsupported SOC Series for VBUS detection" +#endif + +#include + +LOG_MODULE_REGISTER(usb, LOG_LEVEL_DBG); + +/* Semaphore oder Event-Flag zur Signalisierung der VBUS-Präsenz */ +K_SEM_DEFINE(usb_vbus_detected_sem, 0, 1); +K_SEM_DEFINE(usb_vbus_removed_sem, 0, 1); +static atomic_t usb_vbus_present = ATOMIC_INIT(0); + +/* Forward Declarations */ +static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param); + +/* Hardware-spezifische VBUS Detection für nRF52 */ +#if defined(CONFIG_SOC_SERIES_NRF52X) +static void vbus_handler(nrfx_power_usb_evt_t event) +{ + if (event == NRFX_POWER_USB_EVT_DETECTED) { + if (atomic_cas(&usb_vbus_present, 0, 1)) { + LOG_INF("VBUS detected (nRF52)"); + k_sem_give(&usb_vbus_detected_sem); + } + } else if (event == NRFX_POWER_USB_EVT_READY) { + LOG_DBG("VBUS ready event (nRF52)"); + } else if (event == NRFX_POWER_USB_EVT_REMOVED) { + if (atomic_cas(&usb_vbus_present, 1, 0)) { + LOG_INF("VBUS removed (nRF52)"); + k_sem_give(&usb_vbus_removed_sem); + } + } +} +#endif + +int usb_cdc_acm_init(void) +{ + LOG_DBG("Initializing USB handling..."); + + /* nRF52 benötigt die Aktivierung der VBUS-Events */ + #if defined(CONFIG_SOC_SERIES_NRF52X) + nrfx_err_t err; + + if (!nrfx_power_init_check()) { + err = nrfx_power_init(NULL); + if ((err != NRFX_SUCCESS) && (err != NRFX_ERROR_ALREADY)) { + LOG_ERR("nrfx_power_init failed: %d", err); + return -EIO; + } + } + + static const nrfx_power_usbevt_config_t usb_config = { .handler = vbus_handler }; + nrfx_power_usbevt_init(&usb_config); + + nrfx_power_usbevt_enable(); + + if (nrfx_power_usbstatus_get() != NRFX_POWER_USB_STATE_DISCONNECTED) { + LOG_DBG("VBUS already present at boot"); + atomic_set(&usb_vbus_present, 1); + k_sem_give(&usb_vbus_detected_sem); + } + #endif + + return 0; +} + +int usb_cdc_acm_start(void) +{ + int ret = usb_enable(usb_status_cb); + if (ret != 0) { + LOG_ERR("Failed to enable USB (%d)", ret); + return ret; + } + +#if DT_NODE_HAS_STATUS(DT_NODELABEL(cdc_acm_uart0), okay) + const struct device *cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0)); + + if (!device_is_ready(cdc_dev)) { + LOG_ERR("CDC ACM device not ready"); + return -ENODEV; + } + + (void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DCD, 1); + (void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DSR, 1); + #else + LOG_ERR("CDC ACM UART device not found in devicetree"); + return -ENODEV; + #endif + + io_usb_status(true); + LOG_DBG("USB CDC ACM enabled"); + return 0; +} + +int usb_cdc_acm_stop(void) +{ + int ret = usb_disable(); + if (ret != 0) { + LOG_ERR("Failed to disable USB (%d)", ret); + return ret; + } + io_usb_status(false); + LOG_DBG("USB CDC ACM disabled"); + return 0; +} + +/* Der USB Management Thread */ +void usb_thread(void *p1, void *p2, void *p3) +{ + bool usb_enabled = false; + + if (usb_cdc_acm_init() != 0) { + LOG_ERR("USB init failed"); + return; + } + + while (1) { + if (!usb_enabled) { + k_sem_take(&usb_vbus_detected_sem, K_FOREVER); + + if (!atomic_get(&usb_vbus_present)) { + continue; + } + + k_sleep(K_MSEC(100)); + + if (!atomic_get(&usb_vbus_present)) { + continue; + } + + if (usb_cdc_acm_start() == 0) { + usb_enabled = true; + } + } else { + k_sem_take(&usb_vbus_removed_sem, K_FOREVER); + + if (atomic_get(&usb_vbus_present)) { + continue; + } + + if (usb_cdc_acm_stop() == 0) { + usb_enabled = false; + } + } + } +} + +K_THREAD_DEFINE(usb_mgmt_tid, 1024, usb_thread, NULL, NULL, NULL, 7, 0, 0); + +static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param) +{ + switch (cb_status) { + case USB_DC_CONNECTED: + LOG_INF("USB device connected"); + break; + case USB_DC_CONFIGURED: + LOG_INF("USB device configured"); + break; + case USB_DC_RESET: + LOG_INF("USB bus reset"); + break; + case USB_DC_DISCONNECTED: + LOG_INF("USB device disconnected"); + break; + case USB_DC_SUSPEND: + LOG_DBG("USB device suspended"); + break; + case USB_DC_RESUME: + LOG_DBG("USB device resumed"); + break; + default: + break; + } +} \ No newline at end of file diff --git a/src/usb.h b/src/usb.h new file mode 100644 index 0000000..4c6425c --- /dev/null +++ b/src/usb.h @@ -0,0 +1,9 @@ +#ifndef USB_CDC_ACM_H +#define USB_CDC_ACM_H + +/** + * @brief Initializes the USB CDC ACM device + * @return 0 on success, negative error code on failure + */ +int usb_cdc_acm_init(void); +#endif // USB_CDC_ACM_H \ No newline at end of file