sync
This commit is contained in:
@@ -3,4 +3,13 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
|||||||
|
|
||||||
project(buzzer)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
VERSION
Normal file
5
VERSION
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
VERSION_MAJOR = 0
|
||||||
|
VERSION_MINOR = 0
|
||||||
|
PATCHLEVEL = 1
|
||||||
|
VERSION_TWEAK = 0
|
||||||
|
EXTRAVERSION = 0
|
||||||
238
bench_lfs.py
Normal file
238
bench_lfs.py
Normal file
@@ -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)
|
||||||
1
boards/nrf52840dk_nrf52840.conf
Normal file
1
boards/nrf52840dk_nrf52840.conf
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CONFIG_NRFX_POWER=y
|
||||||
53
boards/nrf52840dk_nrf52840.overlay
Normal file
53
boards/nrf52840dk_nrf52840.overlay
Normal file
@@ -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 = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK/Bit Clock */
|
||||||
|
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/Word Select */
|
||||||
|
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD/Serial Data */
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
i2s0_sleep: i2s0_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||||
|
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||||
|
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&i2s0 {
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&i2s0_default>;
|
||||||
|
pinctrl-1 = <&i2s0_sleep>;
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
};
|
||||||
4
pm_static.yml
Normal file
4
pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
littlefs_storage:
|
||||||
|
address: 0x0
|
||||||
|
size: 0x800000
|
||||||
|
region: external_flash
|
||||||
46
prj.conf
46
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
|
||||||
321
send_file.py
Normal file
321
send_file.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
387
src/audio.c
Normal file
387
src/audio.c
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/random/random.h>
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/i2s.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <audio.h>
|
||||||
|
#include <fs.h>
|
||||||
|
#include <io.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
35
src/audio.h
Normal file
35
src/audio.h
Normal file
@@ -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
|
||||||
23
src/fs.c
Normal file
23
src/fs.c
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include <zephyr/fs/littlefs.h>
|
||||||
|
#include <fs.h>
|
||||||
|
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;
|
||||||
|
}
|
||||||
10
src/fs.h
Normal file
10
src/fs.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef FS_H
|
||||||
|
#define FS_H
|
||||||
|
|
||||||
|
#include <zephyr/fs/fs.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes the filesystem by mounting it
|
||||||
|
*/
|
||||||
|
int fs_init(void);
|
||||||
|
#endif // FS_H
|
||||||
91
src/io.c
Normal file
91
src/io.c
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#include <io.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include <audio.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
23
src/io.h
Normal file
23
src/io.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#ifndef IO_H
|
||||||
|
#define IO_H
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
77
src/main.c
77
src/main.c
@@ -1,6 +1,79 @@
|
|||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/drivers/hwinfo.h>
|
||||||
|
#include <zephyr/init.h>
|
||||||
|
#include <zephyr/sys/printk.h>
|
||||||
|
#include <app_version.h>
|
||||||
|
#include <version.h>
|
||||||
|
#include <ncs_version.h>
|
||||||
|
|
||||||
|
#include <fs.h>
|
||||||
|
#include <audio.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <usb.h>
|
||||||
|
|
||||||
|
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)
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/usb.c
Normal file
184
src/usb.c
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/usb/usb_device.h>
|
||||||
|
#include <zephyr/drivers/uart.h>
|
||||||
|
#include <zephyr/sys/atomic.h>
|
||||||
|
#if defined(CONFIG_SOC_SERIES_NRF52X)
|
||||||
|
#include <nrfx_power.h>
|
||||||
|
#elif defined(CONFIG_SOC_SERIES_STM32G0X)
|
||||||
|
// STM32 spezifische Header hier
|
||||||
|
#else
|
||||||
|
#error "Unsupported SOC Series for VBUS detection"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <io.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user