From af23c19dc3a9ad0bc796bf3c802c33b16a08e27f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:45 +0100 Subject: [PATCH 01/10] zephyr/shell: add core shell infrastructure, Kconfig menu, and syscall architecture Introduce the modular SOF Zephyr shell framework under CONFIG_SOF_SHELL. Split implementation into supervisor (kernel.c) and user-mode (user.c) command handlers, connected via system call wrappers in sof_shell_syscall.h. Annotate shell commands with __cold to optimize SRAM usage, and add QEMU test support. --- app/shell_overlay.conf | 9 +- app/src/main.c | 4 + scripts/sof-qemu-run.py | 20 + scripts/sof-qemu-run.sh | 1 + scripts/sof-shell-validate.py | 405 ++++++ src/include/sof/sof_shell_syscall.h | 282 ++++ zephyr/CMakeLists.txt | 6 +- zephyr/Kconfig | 425 ++++++ zephyr/shell/kernel.c | 2070 +++++++++++++++++++++++++++ zephyr/shell/syscall.c | 686 +++++++++ zephyr/shell/user.c | 1409 ++++++++++++++++++ zephyr/sof_shell.c | 84 -- 12 files changed, 5315 insertions(+), 86 deletions(-) create mode 100755 scripts/sof-shell-validate.py create mode 100644 src/include/sof/sof_shell_syscall.h create mode 100644 zephyr/shell/kernel.c create mode 100644 zephyr/shell/syscall.c create mode 100644 zephyr/shell/user.c delete mode 100644 zephyr/sof_shell.c diff --git a/app/shell_overlay.conf b/app/shell_overlay.conf index 963acd1d6e60..319857a70ba5 100644 --- a/app/shell_overlay.conf +++ b/app/shell_overlay.conf @@ -1,7 +1,7 @@ CONFIG_SHELL=y CONFIG_SHELL_HELP=y CONFIG_SHELL_CMDS=y -CONFIG_SHELL_LOG_BACKEND=n +CONFIG_SHELL_LOG_BACKEND=y CONFIG_SHELL_AUTOSTART=y CONFIG_SHELL_BACKEND_ADSP_MEMORY_WINDOW=y @@ -18,3 +18,10 @@ CONFIG_WINSTREAM_CONSOLE=y # these must be disabled in order to use the console. CONFIG_SOF_TELEMETRY_PERFORMANCE_MEASUREMENTS=n CONFIG_SOF_TELEMETRY_IO_PERFORMANCE_MEASUREMENTS=n +CONFIG_SOF_SHELL_LLEXT_LOAD=y +CONFIG_SOF_SHELL_LLEXT_LIST=y +CONFIG_SOF_SHELL_LLEXT_PURGE=y +CONFIG_SOF_SHELL_CORE_POWER=y + +# Disable REBOOT since qemu_xtensa does not implement sys_arch_reboot +CONFIG_REBOOT=n diff --git a/app/src/main.c b/app/src/main.c index aa9ee6960d8d..1b9c3f60137e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -91,6 +91,9 @@ void test_main(void) sof_app_main(); #if CONFIG_SOF_BOOT_TEST && (defined(QEMU_BOOT_TESTS) || CONFIG_SOF_BOOT_TEST_STANDALONE) sof_run_boot_tests(); +#ifdef CONFIG_SHELL + k_sleep(K_FOREVER); +#else #if defined(QEMU_BOOT_TESTS) /* qemu_xtensa_exit() only exists for QEMU targets; a standalone * boot test (e.g. native_sim) just returns from test_main() @@ -98,6 +101,7 @@ void test_main(void) qemu_xtensa_exit(0); #endif #endif +#endif } #else int main(void) diff --git a/scripts/sof-qemu-run.py b/scripts/sof-qemu-run.py index 4fc40ac439e7..36793012e611 100755 --- a/scripts/sof-qemu-run.py +++ b/scripts/sof-qemu-run.py @@ -200,6 +200,18 @@ def main(): crashed = check_for_crash(full_output) + shell_enabled = False + config_file = os.path.join(build_dir, "zephyr", ".config") + if not os.path.isfile(config_file): + config_file = os.path.join("zephyr", ".config") + + if os.path.isfile(config_file): + with open(config_file, "r") as f: + for line in f: + if line.strip() == "CONFIG_SHELL=y": + shell_enabled = True + break + if crashed: print("\n[sof-qemu-run] Detected crash signature in standard output!") # Stop QEMU if it's still running @@ -208,6 +220,14 @@ def main(): child.close(force=True) run_sof_crash_decode(build_dir, full_output) + elif shell_enabled: + print("\n[sof-qemu-run] Shell is enabled. Entering interactive mode...") + if child.isalive(): + try: + child.interact() + except Exception as e: + print(f"Error during interaction: {e}") + child.close(force=True) else: if is_native_sim: print("\n[sof-qemu-run] No crash detected. (Skipping QEMU monitor interaction for native_sim)") diff --git a/scripts/sof-qemu-run.sh b/scripts/sof-qemu-run.sh index ef915fbb1318..ba981f819944 100755 --- a/scripts/sof-qemu-run.sh +++ b/scripts/sof-qemu-run.sh @@ -59,3 +59,4 @@ source ${VENV_DIR}/bin/activate # Finally run the python script which will now correctly inherit 'west' from the sourced environment. python3 "${SCRIPT_DIR}/sof-qemu-run.py" --build-dir "${BUILD_DIR}" $VALGRIND_ARG + diff --git a/scripts/sof-shell-validate.py b/scripts/sof-shell-validate.py new file mode 100755 index 000000000000..154732bfbe64 --- /dev/null +++ b/scripts/sof-shell-validate.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation. All rights reserved. +""" +sof-shell-validate.py - Validate SOF Zephyr shell commands on the spider DUT. + +Deploys the shell-enabled firmware to the NFS rootfs, triggers a firmware +reload on the DUT, then exercises every 'sof' shell command documented in +SHELL.md via cavstool.py's --shell-pty mechanism. Prints a PASS/FAIL summary. + +Usage +----- + ./sof/scripts/sof-shell-validate.py [options] + +Options +------- + --build-dir DIR Shell-enabled build directory (default: build-tgl-shell-llvm) + --dut HOST DUT SSH hostname (default: spider) + --nfs-root PATH NFS rootfs on the host (default: /srv/nfs/spider-rootfs) + --no-deploy Skip firmware copy step + --no-reload Skip rmmod/modprobe step + --cavstool PATH cavstool.py path on the DUT (default: auto-detected) + --timeout SEC Per-command reply timeout in seconds (default: 4) +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import textwrap +import time + +# --------------------------------------------------------------------------- +# Command table: (shell_command, [strings that must appear OR-wise in output]) +# An empty list means any non-error output is acceptable. +# --------------------------------------------------------------------------- +COMMANDS = [ + # Basic info + ("sof version", ["SOF Version"]), + # Heap + ("sof module heap usage", ["No components found", "comp id"]), + # Pipeline / module introspection + # pipeline_list always prints "ID Core Status Priority Period" header + ("sof pipeline list", ["ID"]), + ("sof pipeline status", ["No pipelines found", "ppl_id"]), + ("sof module status", ["No components found", "comp_id"]), + # Hardware / platform (sof sram_status / sof clock_status require + # CONFIG_SOF_SHELL_SRAM_STATUS / _CLOCK_STATUS which are not in this build) + ("sof core status", ["core", "enabled"]), + # IPC + ("sof ipc stats", ["rx_count", "tx_count"]), + ("sof ipc last", []), # ok even when empty (no IPC yet) + # Buffers / scheduler + ("sof buffer list", ["Audio buffers"]), + ("sof sched tasks", ["Active scheduler"]), + ("sof sched load", ["Scheduler task cycle"]), + # Logging + ("sof log status", ["Log backends"]), + # Controls + ("sof kctl list", ["No components found", "comp_id"]), + # DAI and DMA + ("sof dai list", ["DAI", "dai", "SSP", "HDA", "DMIC", "ALH", "UAOL", "SAI", "ESAI"]), + ("sof dma status", ["No DMA controllers", "DMA controller"]), + # Verbose/multi-line commands last + ("sof module list", ["Module", "module", "UUID"]), + ("sof mailbox hex", ["Mailbox regions", "exception"]), + ("sof dbgwin dump", ["ADSP debug window"]), +] + +# Patterns that indicate a command went wrong regardless of output +ERROR_PATTERNS = [ + "shell: command not found", + "shell: unknown command", + "-EINVAL", + "-ENOMEM", + "Traceback", +] + +FIRMWARE_RELPATH = "zephyr/zephyr.ri" +FIRMWARE_NFS_SUBPATH = "lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri" +SHELL_PROMPT = "~$ " + +# --------------------------------------------------------------------------- +# On-DUT Python snippet template. +# Substitution uses {name} placeholders; literal braces are doubled. +# Triple-quoted strings inside the snippet use single quotes to avoid +# conflicting with the outer r""" delimiter. +# Sent via SSH stdin; prints a single JSON list to stdout. +# --------------------------------------------------------------------------- +_DUT_SNIPPET_TMPL = ( + "import os, sys, select, json, time, subprocess, termios, atexit\n" + "\n" + "TIMEOUT = {timeout}\n" + "PROMPT = '~$ '\n" + "CAVSTOOL = {cavstool!r}\n" + "COMMANDS = {commands!r}\n" + "ERROR_PATTERNS = {error_patterns!r}\n" + "\n" + "def find_cavstool():\n" + " import shutil\n" + " for p in [CAVSTOOL,\n" + " '/usr/bin/cavstool.py',\n" + " '/usr/sbin/cavstool.py',\n" + " '/usr/local/bin/cavstool.py']:\n" + " if p and os.path.isfile(p):\n" + " return p\n" + " return shutil.which('cavstool.py')\n" + "\n" + "cavstool_path = find_cavstool()\n" + "if not cavstool_path:\n" + " print(json.dumps({{'error': 'cavstool.py not found on DUT'}}))\n" + " sys.exit(1)\n" + "\n" + "# Wake the SOF audio PCI device from runtime PM before attaching cavstool.\n" + "# The device suspends when idle; cavstool reads 0xffffffff from MMIO otherwise.\n" + "import glob\n" + "_sof_pci = glob.glob('/sys/bus/pci/devices/*/driver')\n" + "_sof_pci = [os.path.dirname(d) for d in _sof_pci\n" + " if 'sof-audio' in os.path.basename(os.readlink(d))]\n" + "for _dev in _sof_pci:\n" + " _ctrl = os.path.join(_dev, 'power', 'control')\n" + " if os.path.exists(_ctrl):\n" + " with open(_ctrl, 'w') as _f:\n" + " _f.write('on\\n')\n" + "time.sleep(1) # allow device to resume\n" + "\n" + "proc = subprocess.Popen(\n" + " [sys.executable, cavstool_path, '--log-only', '--shell-pty', '--no-history'],\n" + " stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n" + " text=True, bufsize=1,\n" + ")\n" + "atexit.register(proc.terminate)\n" + "\n" + "pty_path = None\n" + "deadline = time.time() + 15\n" + "while time.time() < deadline:\n" + " line = proc.stdout.readline()\n" + " if not line:\n" + " break\n" + " if 'shell PTY at:' in line:\n" + " pty_path = line.split('shell PTY at:')[-1].strip()\n" + " break\n" + "\n" + "if not pty_path:\n" + " print(json.dumps({{'error': 'cavstool did not report a shell PTY'}}))\n" + " proc.terminate()\n" + " sys.exit(1)\n" + "\n" + "fd = os.open(pty_path, os.O_RDWR | os.O_NOCTTY)\n" + "old_attrs = termios.tcgetattr(fd)\n" + "new_attrs = list(old_attrs)\n" + "new_attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)\n" + "new_attrs[6][termios.VMIN] = 0\n" + "new_attrs[6][termios.VTIME] = 0\n" + "termios.tcsetattr(fd, termios.TCSANOW, new_attrs)\n" + "atexit.register(lambda: termios.tcsetattr(fd, termios.TCSANOW, old_attrs))\n" + "\n" + "def drain(fd, secs):\n" + " # Read all available bytes within secs seconds.\n" + " buf = b''\n" + " deadline = time.time() + secs\n" + " while True:\n" + " remaining = deadline - time.time()\n" + " if remaining <= 0:\n" + " break\n" + " ready, _, _ = select.select([fd], [], [], min(0.1, remaining))\n" + " if not ready:\n" + " continue\n" + " chunk = os.read(fd, 4096)\n" + " if not chunk:\n" + " break\n" + " buf += chunk\n" + " return buf.decode('utf-8', errors='replace')\n" + "\n" + "drain(fd, 3) # wait for first prompt\n" + "\n" + "results = []\n" + "for (cmd, patterns) in COMMANDS:\n" + " drain(fd, 0.3)\n" + " os.write(fd, (cmd + '\\r\\n').encode())\n" + " collected = ''\n" + " deadline = time.time() + TIMEOUT\n" + " while time.time() < deadline:\n" + " chunk = drain(fd, 0.3)\n" + " collected += chunk\n" + " if PROMPT in collected:\n" + " break\n" + " output = collected\n" + " for s in [cmd, PROMPT, '\\r\\n', '\\r', '\\n\\n']:\n" + " output = output.replace(s, '\\n').strip()\n" + " has_error = any(ep in collected for ep in ERROR_PATTERNS)\n" + " has_match = (not patterns) or any(\n" + " p.lower() in collected.lower() for p in patterns\n" + " )\n" + " results.append({{'cmd': cmd, 'output': output,\n" + " 'pass': has_match and not has_error}})\n" + "\n" + "print(json.dumps(results))\n" + "proc.terminate()\n" + "# Restore runtime PM\n" + "for _dev in _sof_pci:\n" + " _ctrl = os.path.join(_dev, 'power', 'control')\n" + " if os.path.exists(_ctrl):\n" + " with open(_ctrl, 'w') as _f:\n" + " _f.write('auto\\n')\n" +) + +# --------------------------------------------------------------------------- +# Host-side helpers +# --------------------------------------------------------------------------- + +def deploy_firmware(build_dir: str, nfs_root: str) -> None: + src = os.path.join(build_dir, FIRMWARE_RELPATH) + dst = os.path.join(nfs_root, FIRMWARE_NFS_SUBPATH) + if not os.path.isfile(src): + sys.exit(f"[error] Firmware not found: {src}") + os.makedirs(os.path.dirname(dst), exist_ok=True) + print(f"[deploy] {src} -> {dst}") + shutil.copy2(src, dst) + + +def reload_firmware(dut: str) -> None: + print(f"[reload] Reloading SOF firmware on {dut} ...") + # Try a soft reload first (non-destructive) + soft = ( + "for f in /sys/bus/platform/devices/*/firmware_reload " + " /sys/kernel/debug/sof/reload_fw; do " + " [ -f \"$f\" ] && echo 1 > \"$f\" && exit 0; " + "done; " + # Fallback: rmmod + modprobe + "rmmod snd_sof_pci_intel_tgl 2>/dev/null; " + "modprobe snd_sof_pci_intel_tgl" + ) + result = subprocess.run( + ["ssh", "-o", "StrictHostKeyChecking=no", + "-o", "PasswordAuthentication=no", + f"root@{dut}", soft], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + print(f"[reload] Warning: reload command returned {result.returncode}") + print(result.stderr.strip()) + else: + print("[reload] OK — waiting 4 s for DSP to boot ...") + time.sleep(4) + + +def find_cavstool_on_dut(dut: str) -> str: + """Return the first cavstool.py path found on the DUT, or empty string.""" + result = subprocess.run( + ["ssh", "-o", "StrictHostKeyChecking=no", + "-o", "PasswordAuthentication=no", + f"root@{dut}", + "for p in /usr/bin/cavstool.py /usr/sbin/cavstool.py " + " /usr/local/bin/cavstool.py; do " + " [ -f \"$p\" ] && echo \"$p\" && exit 0; done; " + "command -v cavstool.py 2>/dev/null || true"], + capture_output=True, text=True, timeout=10, + ) + return result.stdout.strip() + + +def run_on_dut(dut: str, snippet: str) -> str: + """Execute a Python snippet on the DUT via SSH stdin piping.""" + result = subprocess.run( + ["ssh", "-o", "StrictHostKeyChecking=no", + "-o", "PasswordAuthentication=no", + "-o", "ConnectTimeout=10", + f"root@{dut}", "python3"], + input=snippet.encode(), + capture_output=True, + timeout=180, + ) + return result.stdout.decode(), result.stderr.decode() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + ap = argparse.ArgumentParser( + description="Validate SOF Zephyr shell commands on the spider DUT.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + ap.add_argument("--build-dir", default="build-tgl-shell-llvm", + help="Shell-enabled build directory (default: %(default)s)") + ap.add_argument("--dut", default="spider", + help="DUT SSH hostname (default: %(default)s)") + ap.add_argument("--nfs-root", default="/srv/nfs/spider-rootfs", + help="NFS rootfs path on this host (default: %(default)s)") + ap.add_argument("--no-deploy", action="store_true", + help="Skip firmware copy to NFS rootfs") + ap.add_argument("--no-reload", action="store_true", + help="Skip rmmod/modprobe on DUT") + ap.add_argument("--cavstool", default="", + help="cavstool.py path on the DUT (default: auto-detect)") + ap.add_argument("--timeout", type=int, default=4, + help="Per-command reply timeout in seconds (default: %(default)s)") + args = ap.parse_args() + + # Change to workspace root (two levels up from sof/scripts/) so that + # relative build-dir paths like 'build-tgl-shell-llvm' resolve correctly. + script_dir = os.path.dirname(os.path.abspath(__file__)) # sof/scripts/ + sof_dir = os.path.dirname(script_dir) # sof/ + workspace = os.path.dirname(sof_dir) # workspace root + os.chdir(workspace) + + print("=" * 60) + print("SOF Shell Command Validator") + print("=" * 60) + + if not args.no_deploy: + deploy_firmware(args.build_dir, args.nfs_root) + else: + print("[deploy] Skipped.") + + if not args.no_reload: + reload_firmware(args.dut) + else: + print("[reload] Skipped.") + + # Auto-detect cavstool on DUT if not given + cavstool_path = args.cavstool + if not cavstool_path: + cavstool_path = find_cavstool_on_dut(args.dut) + if cavstool_path: + print(f"[cavstool] Found: {cavstool_path}") + else: + print("[cavstool] Warning: could not find cavstool.py on DUT; " + "the on-DUT snippet will try common locations.") + + # Build the on-DUT snippet by substituting parameters into the template + snippet = _DUT_SNIPPET_TMPL.format( + timeout=args.timeout, + cavstool=cavstool_path, + commands=COMMANDS, + error_patterns=ERROR_PATTERNS, + ) + + print(f"\n[validate] Running {len(COMMANDS)} shell commands on {args.dut} ...") + stdout, stderr = run_on_dut(args.dut, snippet) + + # Debug: always print raw DUT output to help diagnose issues + if stderr.strip(): + print("[dut stderr]") + for line in stderr.strip().splitlines(): + print(" " + line) + if not stdout.strip(): + print("[error] DUT produced no stdout. Check above for errors.") + sys.exit(1) + + # Parse JSON results + results = None + for line in stdout.splitlines(): + line = line.strip() + if not line: + continue + try: + parsed = json.loads(line) + if isinstance(parsed, list): + results = parsed + break + if isinstance(parsed, dict) and "error" in parsed: + sys.exit(f"[error] DUT reported: {parsed['error']}") + except json.JSONDecodeError: + pass + + if results is None: + print("[error] Could not parse results from DUT.") + print("DUT stdout was:") + print(stdout) + sys.exit(1) + + # Print report + print() + print("=" * 60) + print(f"{'Command':<40} {'Result'}") + print("-" * 60) + passed = 0 + failed = 0 + for r in results: + status = "PASS" if r["pass"] else "FAIL" + print(f" {r['cmd']:<38} {status}") + if not r["pass"]: + failed += 1 + # Show first 3 lines of output for context + snippet_lines = [l for l in r["output"].splitlines() if l.strip()][:3] + for line in snippet_lines: + print(f" > {line}") + else: + passed += 1 + + print("-" * 60) + print(f" Total: {len(results)} PASS: {passed} FAIL: {failed}") + print("=" * 60) + + sys.exit(0 if failed == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/src/include/sof/sof_shell_syscall.h b/src/include/sof/sof_shell_syscall.h new file mode 100644 index 000000000000..cf1d72379a75 --- /dev/null +++ b/src/include/sof/sof_shell_syscall.h @@ -0,0 +1,282 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/** + * \file + * \brief System call wrappers for privileged data accessed by the kernel + * (RTOS/infrastructure) SOF shell commands. + * + * The kernel shell commands in zephyr/shell/kernel.c read privileged global + * state (IPC counters, scheduler info, ...). When the shell command thread + * runs as a Zephyr user-mode thread (CONFIG_SOF_SHELL_USERSPACE) these + * accessors must be reached through system calls. On supervisor-only builds + * the wrappers compile to direct calls to the z_impl_* implementations with no + * overhead. + */ + +#ifndef __SOF_SOF_SHELL_SYSCALL_H__ +#define __SOF_SOF_SHELL_SYSCALL_H__ + +#include +#include + +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) + +/** \brief Snapshot of per-core enabled state for "sof core_status". */ +struct sof_shell_core_status { + uint32_t core_count; /* number of valid entries in enabled[] */ + uint32_t current; /* id of the core servicing the request */ + uint8_t enabled[CONFIG_CORE_COUNT]; /* 1 if core is enabled, 0 otherwise */ +}; + +/** \brief Snapshot of current clock frequencies for "sof clock_status". */ +struct sof_shell_clock_status { + uint32_t valid; /* 0 if clock info is unavailable */ + uint32_t num_clocks; /* number of valid entries in freq_hz[] */ + uint32_t freq_hz[CONFIG_CORE_COUNT]; /* current frequency of each clock */ +}; + +/** \brief Maximum number of scheduler tasks captured in one snapshot. */ +#define SOF_SHELL_SCHED_MAX_TASKS 48 + +/** \brief Per-task snapshot entry for "sof sched_tasks"/"sof sched_load". */ +struct sof_shell_sched_task { + uint32_t sch_type; /* SOF_SCHEDULE_ type of the owning scheduler */ + uint32_t core; + uint32_t priority; + uint32_t state; /* enum task_state */ + uint32_t flags; + uint32_t cycles_cnt; + uint32_t cycles_sum; + uint32_t cycles_max; + uintptr_t uid; /* opaque uuid pointer, printed as a value */ + uintptr_t data; /* opaque task data pointer, printed as a value */ +}; + +/** \brief Snapshot of the scheduler task list for the sched shell commands. */ +struct sof_shell_sched_snapshot { + uint32_t no_schedulers; /* 1 if no schedulers are registered */ + uint32_t count; /* number of valid entries in tasks[] */ + struct sof_shell_sched_task tasks[SOF_SHELL_SCHED_MAX_TASKS]; +}; + +/** \brief Maximum number of log backends captured in one snapshot. */ +#define SOF_SHELL_LOG_MAX_BACKENDS 8 +/** \brief Maximum stored length (including NUL) of a log backend name. */ +#define SOF_SHELL_LOG_NAME_MAX 48 + +/** \brief Per-backend snapshot entry for "sof log_status". */ +struct sof_shell_log_backend { + uint32_t id; + uint32_t active; /* 1 if the backend is active */ + char name[SOF_SHELL_LOG_NAME_MAX]; +}; + +/** \brief Snapshot of the logging subsystem state for "sof log_status". */ +struct sof_shell_log_status { + uint32_t backend_count; /* total backends reported by the core */ + uint32_t source_count; /* number of log sources */ + uint32_t filled; /* number of valid entries in backends[] */ + struct sof_shell_log_backend backends[SOF_SHELL_LOG_MAX_BACKENDS]; +}; + +/** \brief Maximum number of mapped memory regions captured in a TLB snapshot. */ +#define SOF_SHELL_TLB_MAX_REGIONS 16 + +/** \brief One mapped memory region entry for "sof mmu_status". */ +struct sof_shell_mm_region { + uint32_t addr; + uint32_t size; + uint32_t attr; +}; + +/** + * \brief Snapshot of TLB / virtual-memory metadata for the MMU shell commands. + * + * All values are read from privileged TLB MMIO and the memory-management + * driver; the decoding (per-entry flags, paddr) is done by the shell using the + * bit indices and base addresses reported here. + */ +struct sof_shell_tlb_meta { + uint32_t vm_base; /* virtual memory base address */ + uint32_t page_size; /* page size in bytes */ + uint32_t total_entries; /* number of TLB entries */ + uint32_t enabled_entries; /* number of active (mapped) TLB entries */ + uint32_t tlb_mmio_base; /* TLB MMIO table base address */ + uint32_t phys_base; /* base physical address for the region */ + uint32_t paddr_size; /* paddr field width; enable bit = BIT(paddr_size) */ + uint32_t exec_bit_idx; /* index of the exec permission bit */ + uint32_t write_bit_idx; /* index of the write permission bit */ + uint32_t region_count; /* number of valid entries in regions[] */ + uint32_t regions_truncated; /* 1 if more regions exist than fit */ + struct sof_shell_mm_region regions[SOF_SHELL_TLB_MAX_REGIONS]; +}; + +/** \brief Maximum llext libraries captured in one snapshot. */ +#define SOF_SHELL_LLEXT_MAX_LIBS 16 +/** \brief Maximum module files captured per library. */ +#define SOF_SHELL_LLEXT_MAX_MODS 8 +/** \brief Maximum stored length (including NUL) of a module name. */ +#define SOF_SHELL_LLEXT_NAME_MAX 32 +/** \brief Maximum length (including NUL) of a symbol name passed to llext_call. */ +#define SOF_SHELL_LLEXT_SYM_MAX 64 + +/** \brief Per-module-file entry for "sof llext_list". */ +struct sof_shell_llext_mod { + uint32_t mapped; /* 1 if mapped in SRAM */ + int32_t use; /* llext use count */ + uint32_t dep; /* number of dependents */ + char name[SOF_SHELL_LLEXT_NAME_MAX]; +}; + +/** \brief Per-library entry for "sof llext_list". */ +struct sof_shell_llext_lib { + uint32_t lib_id; + uint32_t base_addr; + uint32_t store_bytes; + uint32_t manifest_mods; /* num_module_entries from the manifest */ + uint32_t elf_files; /* n_mod */ + uint32_t mod_count; /* valid entries in mods[] */ + struct sof_shell_llext_mod mods[SOF_SHELL_LLEXT_MAX_MODS]; +}; + +/** \brief Snapshot of the llext libraries held in IMR/DRAM. */ +struct sof_shell_llext_list { + uint32_t enabled; /* 1 if the library manager is available */ + uint32_t count; /* valid entries in libs[] */ + struct sof_shell_llext_lib libs[SOF_SHELL_LLEXT_MAX_LIBS]; +}; + +/** \brief Per-module result of a ctor/dtor/call operation. */ +struct sof_shell_llext_op_mod { + int32_t ret; /* per-module result code */ + uint32_t flag; /* op-specific: ctor/dtor unused; call: symbol found */ + uintptr_t addr; /* op-specific: call: resolved symbol address */ + char name[SOF_SHELL_LLEXT_NAME_MAX]; +}; + +/** \brief Result of "sof llext_ctor/dtor" or "sof llext_call". */ +struct sof_shell_llext_op_result { + int32_t status; /* overall status: 0, -ENOENT, -ENOSYS, or first error */ + uint32_t count; /* valid entries in mods[] */ + struct sof_shell_llext_op_mod mods[SOF_SHELL_LLEXT_MAX_MODS]; +}; + +/** \brief Copy the current IPC statistics snapshot (wraps ipc_stats_get()). */ +__syscall void sof_shell_ipc_stats_get(struct ipc_stats *out); + +/** \brief Reset all IPC statistics counters (wraps ipc_stats_reset()). */ +__syscall void sof_shell_ipc_stats_reset(void); + +/** \brief Copy a snapshot of per-core enabled/current state. */ +__syscall void sof_shell_core_status_get(struct sof_shell_core_status *out); + +/** \brief Copy a snapshot of the current per-clock frequencies. */ +__syscall void sof_shell_clock_status_get(struct sof_shell_clock_status *out); + +/** \brief Copy a snapshot of the scheduler task list. */ +__syscall void sof_shell_sched_snapshot_get(struct sof_shell_sched_snapshot *out); + +/** \brief Copy a snapshot of the logging subsystem backends. */ +__syscall void sof_shell_log_status_get(struct sof_shell_log_status *out); + +/** \brief Copy TLB/VM metadata and the mapped memory region list. */ +__syscall void sof_shell_tlb_meta_get(struct sof_shell_tlb_meta *out); + +/** + * \brief Copy a range of raw 16-bit TLB entries into a user buffer. + * \param start index of the first entry to copy. + * \param count number of entries requested. + * \param out buffer receiving up to \p count entries. + * \return number of entries actually copied (clamped to the valid range). + */ +__syscall uint32_t sof_shell_tlb_entries_get(uint32_t start, uint32_t count, + uint16_t *out); + +/** \brief Report whether secondary DSP core \p id is currently enabled. */ +__syscall int sof_shell_core_is_enabled(uint32_t id); + +/** \brief Power on secondary DSP core \p id. \return 0 on success. */ +__syscall int sof_shell_core_enable(uint32_t id); + +/** \brief Power off secondary DSP core \p id. */ +__syscall void sof_shell_core_disable(uint32_t id); + +/** \brief Block audio scheduling for \p block_time_us microseconds. */ +__syscall void sof_shell_inject_sched_gap(uint32_t block_time_us); + +/** \brief Copy a snapshot of the llext libraries held in IMR/DRAM. */ +__syscall void sof_shell_llext_list_get(struct sof_shell_llext_list *out); + +/** \brief Purge an llext library from IMR/DRAM. \return 0 on success. */ +__syscall int sof_shell_llext_purge(uint32_t lib_id); + +/** + * \brief Run constructors (is_ctor=1) or destructors (is_ctor=0) for every + * module in a library, capturing per-module results. + */ +__syscall int sof_shell_llext_ctor_dtor(uint32_t lib_id, uint32_t is_ctor, + struct sof_shell_llext_op_result *out); + +/** + * \brief Find a named symbol in every module of a library and invoke it as a + * void(*)(void), capturing per-module results. + */ +__syscall int sof_shell_llext_call(uint32_t lib_id, const char *sym_name, + struct sof_shell_llext_op_result *out); + +#else /* !__ZEPHYR__ || !CONFIG_SOF_FULL_ZEPHYR_APPLICATION */ + +struct sof_shell_core_status; +struct sof_shell_clock_status; +struct sof_shell_sched_snapshot; +struct sof_shell_log_status; +struct sof_shell_tlb_meta; + +void z_impl_sof_shell_ipc_stats_get(struct ipc_stats *out); +void z_impl_sof_shell_ipc_stats_reset(void); +void z_impl_sof_shell_core_status_get(struct sof_shell_core_status *out); +void z_impl_sof_shell_clock_status_get(struct sof_shell_clock_status *out); +void z_impl_sof_shell_sched_snapshot_get(struct sof_shell_sched_snapshot *out); +void z_impl_sof_shell_log_status_get(struct sof_shell_log_status *out); +void z_impl_sof_shell_tlb_meta_get(struct sof_shell_tlb_meta *out); +uint32_t z_impl_sof_shell_tlb_entries_get(uint32_t start, uint32_t count, + uint16_t *out); +int z_impl_sof_shell_core_is_enabled(uint32_t id); +int z_impl_sof_shell_core_enable(uint32_t id); +void z_impl_sof_shell_core_disable(uint32_t id); +void z_impl_sof_shell_inject_sched_gap(uint32_t block_time_us); +struct sof_shell_llext_list; +struct sof_shell_llext_op_result; +void z_impl_sof_shell_llext_list_get(struct sof_shell_llext_list *out); +int z_impl_sof_shell_llext_purge(uint32_t lib_id); +int z_impl_sof_shell_llext_ctor_dtor(uint32_t lib_id, uint32_t is_ctor, + struct sof_shell_llext_op_result *out); +int z_impl_sof_shell_llext_call(uint32_t lib_id, const char *sym_name, + struct sof_shell_llext_op_result *out); +#define sof_shell_ipc_stats_get z_impl_sof_shell_ipc_stats_get +#define sof_shell_ipc_stats_reset z_impl_sof_shell_ipc_stats_reset +#define sof_shell_core_status_get z_impl_sof_shell_core_status_get +#define sof_shell_clock_status_get z_impl_sof_shell_clock_status_get +#define sof_shell_sched_snapshot_get z_impl_sof_shell_sched_snapshot_get +#define sof_shell_log_status_get z_impl_sof_shell_log_status_get +#define sof_shell_tlb_meta_get z_impl_sof_shell_tlb_meta_get +#define sof_shell_tlb_entries_get z_impl_sof_shell_tlb_entries_get +#define sof_shell_core_is_enabled z_impl_sof_shell_core_is_enabled +#define sof_shell_core_enable z_impl_sof_shell_core_enable +#define sof_shell_core_disable z_impl_sof_shell_core_disable +#define sof_shell_inject_sched_gap z_impl_sof_shell_inject_sched_gap +#define sof_shell_llext_list_get z_impl_sof_shell_llext_list_get +#define sof_shell_llext_purge z_impl_sof_shell_llext_purge +#define sof_shell_llext_ctor_dtor z_impl_sof_shell_llext_ctor_dtor +#define sof_shell_llext_call z_impl_sof_shell_llext_call + +#endif + +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#include +#endif + +#endif /* __SOF_SOF_SHELL_SYSCALL_H__ */ diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 4b61a9517d46..db569c113e9b 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -617,9 +617,13 @@ zephyr_library_sources_ifdef(CONFIG_SOF_BOOT_TEST ) zephyr_library_sources_ifdef(CONFIG_SHELL - sof_shell.c + shell/kernel.c + shell/user.c + shell/syscall.c ) +zephyr_syscall_header_ifdef(CONFIG_SHELL ${SOF_SRC_PATH}/include/sof/sof_shell_syscall.h) + zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/generic.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/ipc/ipc_reply.h) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index ab88efcb9a6a..e32d610f278e 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -297,6 +297,431 @@ config SOF_ZEPHYR_NO_SOF_CLOCK Do not use SOF clk.h interface to set the DSP clock frequency. Requires implementation of platform/lib/clk.h. +menu "SOF shell commands" + depends on SHELL + +config SOF_SHELL_USERSPACE + bool "Run the SOF shell command thread in user mode" + depends on USERSPACE + help + When enabled, the SOF shell command thread is intended to run as a + Zephyr user-mode thread. The kernel (RTOS/infrastructure) shell + commands in zephyr/shell/kernel.c reach privileged global state + (IPC counters, scheduler info, ...) through system calls so that a + misbehaving shell command cannot corrupt supervisor state. + + This option enables the system-call plumbing used by those commands. + On supervisor-only builds (or with this option disabled) the system + call wrappers compile to direct function calls with no overhead. + + If unsure, say n. + +config SOF_SHELL_HEAP_USAGE + bool "Module heap usage command" + default y + depends on SHELL + help + Enables the 'sof module_heap_usage' shell command which prints + per-module heap allocation and high-water-mark for all active + IPC4 components. + +config SOF_SHELL_PIPELINE_STATUS + bool "Pipeline status command" + default y + depends on SHELL + help + Enables the 'sof pipeline_status' shell command which lists all + active IPC4 pipelines with their core, priority, period and state. + +config SOF_SHELL_MODULE_STATUS + bool "Module (component) status command" + default y + depends on SHELL + help + Enables the 'sof module_status' shell command which lists all + active IPC4 components with their pipeline, core and state. + +config SOF_SHELL_CORE_STATUS + bool "Core status command" + default y + depends on SHELL + help + Enables the 'sof core_status' shell command which reports the + enabled/active state of each DSP core. + +config SOF_SHELL_CORE_POWER + bool "Secondary core power on/off commands" + default y + depends on SHELL && MULTICORE && SMP + help + Enables the 'sof core_on ' and 'sof core_off ' shell + commands for powering secondary DSP cores on and off at runtime. + + Core 0 (primary) is not a valid target — these commands only accept + core IDs in the range [1 .. CONFIG_CORE_COUNT-1]. + + core_on calls cpu_enable_core() which starts the secondary Zephyr + CPU and runs secondary_core_init(). core_off calls + cpu_disable_core() which requests PM_STATE_SOFT_OFF on the core + and waits for it to halt before asserting the reset line. + +config SOF_SHELL_SRAM_STATUS + bool "SRAM heap status command" + default y + depends on SHELL && SYS_HEAP_RUNTIME_STATS + help + Enables the 'sof sram_status' shell command which reports HPSRAM + heap allocated, free and peak-allocated bytes. + Requires CONFIG_SYS_HEAP_RUNTIME_STATS=y. + +config SOF_SHELL_CLOCK_STATUS + bool "Clock status command" + default y + depends on SHELL && !SOF_ZEPHYR_NO_SOF_CLOCK + help + Enables the 'sof clock_status' shell command which reports the + current CPU clock frequency for each DSP core. + +config SOF_SHELL_MODULE_LIST + bool "Module list command" + default y + depends on SHELL + help + Enables the 'sof module_list' shell command which prints all modules + known to the firmware. On IPC4 Intel platforms (IPC4_BASE_FW_INTEL) + the manifest is consulted and the output includes: + - module name (8-char from manifest) + - UUID + - maximum instance count + - BSS memory per instance (bytes) + - text segment size (bytes) + - affinity mask + - CPC (cycles per copy), IBS and OBS from module config + On other platforms the registered component driver list is shown + with UUID and name from the trace context. + +config SOF_SHELL_PIPELINE_OPS + bool "Pipeline construction and control commands" + default n + depends on SHELL && IPC_MAJOR_4 + help + Enables IPC4 pipeline construction and control commands accessible + from the SOF shell. These are intended for debug and bring-up use: + + sof ppl_create [prio] [pages] [core] [lp] + Create a new IPC4 pipeline. + + sof ppl_delete + Delete a pipeline and all its module instances. + + sof ppl_state + Transition a pipeline to the given state. + + sof mod_init [core] [dp] + Instantiate a module (by module_id from module_list) into a + pipeline. No init-data is sent; only modules that have a + working zero-parameter default config will init successfully. + + sof mod_delete + Delete a module instance. + + sof mod_bind [sq] [dq] + Bind (connect) the output of src module to the input of dst. + + sof mod_unbind [sq] [dq] + Unbind (disconnect) two previously bound module instances. + +config SOF_SHELL_MMU_DBG + bool "MMU / TLB debug commands" + default y + depends on SHELL && (MM_DRV_INTEL_ADSP_MTL_TLB || XTENSA_MMU) + help + Enables MMU/TLB debug shell commands. + + On Intel ADSP MTL/PTL (CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB=y): + sof mmu_status — VM layout, mapped/free pages, region list + sof tlb_dump — dump all active 16-bit MMIO TLB entries + sof tlb_lookup — query a page or page range (VA/PA/R/W/X/bank) + + On Xtensa MMU platforms (CONFIG_XTENSA_MMU=y, e.g. PTL): + sof rasid — decode the RASID hardware register (ring→ASID map) + sof page_info — probe DTLB for a page or range; report + physical address, ring, ASID, cache mode, R/W/X + (PTL has both sets of commands). + +config SOF_SHELL_LLEXT_LOAD + bool "Interactive llext module load command" + default y + depends on SHELL && LIBRARY_MANAGER + help + Enables the 'sof llext_load' shell command which lets a developer + load a compiled llext module from the host file system into the DSP + at run time without a full firmware restart. + + Usage (2-step interactive flow): + + Step 1 — on the DSP shell: + uart:~$ sof llext_load mymodule 1 + + The DSP allocates an ADSP debug window slot, sets the handshake + state to REQUESTING, and waits (up to 120 s) for the host. + + Step 2 — on the Linux host: + $ cat mymodule.ri > /sys/kernel/debug/sof/llext_load + + The kernel driver reads the slot, performs the HDA DMA transfer + (IPC4 LOAD_LIBRARY_PREPARE + LOAD_LIBRARY sequence), and writes + the result back to the slot. The DSP shell command then wakes up + and prints the outcome. + + Requires CONFIG_SND_SOC_SOF_CLIENT_LLEXT_LOAD in the Linux kernel. + +config SOF_SHELL_LLEXT_LOAD_SLOT_NUM + int "Debug window slot for llext_load (fallback without slot manager)" + default 2 + depends on SOF_SHELL_LLEXT_LOAD && !INTEL_ADSP_DEBUG_SLOT_MANAGER + help + Index of the ADSP debug window slot (0-14) reserved for the + shell llext_load handshake when CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER + is not available. Must not conflict with TRACE/TELEMETRY/SHELL slots. + Default slot 2 is normally free on this configuration. + +config SOF_SHELL_LLEXT_LIST + bool "llext library listing command" + default y + depends on SHELL && LIBRARY_MANAGER + help + Enables the 'sof llext_list' shell command. Iterates over all + loaded llext libraries in IMR/DRAM and prints, for each one: + base address, storage size, and per-module-file SRAM mapping state + (mapped/unmapped), Zephyr llext use-count, and dependency count. + + Useful for verifying that an llext load succeeded and for checking + whether any module is still active before attempting a purge. + +config SOF_SHELL_LLEXT_PURGE + bool "llext library purge command" + default y + depends on SHELL && LIBRARY_MANAGER + help + Enables the 'sof llext_purge ' shell command. Removes the + specified llext library from IMR/DRAM storage and frees all memory + associated with it. + + The command refuses if any module within the library is still mapped + into SRAM (i.e. an active pipeline is using it) and returns -EBUSY. + Tear down all pipelines that reference the library before purging. + +config SOF_SHELL_LLEXT_CTOR + bool "llext constructor/destructor call commands" + default y + depends on SHELL && LLEXT && LIBRARY_MANAGER + help + Enables the 'sof llext ctor ' and 'sof llext dtor ' + shell commands. + + 'ctor' calls constructors and __attribute__((constructor)) functions + for every module in the specified library by invoking + llext_bringup() on each module's llext handle. + + 'dtor' calls destructors and __attribute__((destructor)) functions + via llext_teardown(), then unmaps each module from SRAM. + + These are developer/test commands intended to exercise module + initialisation without instantiating a full pipeline. + +config SOF_SHELL_LLEXT_CALL + bool "llext explicit symbol-call command" + default y + depends on SHELL && LLEXT && LIBRARY_MANAGER + help + Enables the 'sof llext call ' shell command. + + The command looks up in sym_tab and exp_tab for every + module in the specified library, then calls the first match as a + void (*)(void) function. + + This is useful for test modules that export a run function but do + not register it as a constructor. The module must be mapped to SRAM + first with 'sof llext ctor '. + +config SOF_SHELL_BUFFER_INFO + bool "Audio buffer list/info commands" + default y + depends on SHELL + help + Enables the 'sof buffer_list' and 'sof buffer_info ' shell + commands. buffer_list walks all components and prints every + downstream comp_buffer with its source/sink component IDs, current + fill level (size/avail/free), channel count, sample rate and + frame format. buffer_info dumps the same fields plus rptr/wptr, + flags and core for a single buffer selected by ID. + + Useful for diagnosing where audio is stuck in a pipeline (xrun, + underrun, mis-bind) without rebuilding firmware. + +config SOF_SHELL_SCHED_INFO + bool "Scheduler task list/load commands" + default y + depends on SHELL + help + Enables the 'sof sched_tasks' and 'sof sched_load' shell commands. + + sched_tasks walks every registered SOF scheduler (LL timer, LL DMA, + EDF, DP, TWB) and prints each task's scheduler type, core, priority, + state, flags and uid. + + sched_load prints per-task execution cycle counters (cycles_cnt, + cycles_sum, cycles_max, derived average) plus aggregate totals, + using the cycle counters that the schedulers already maintain in + struct task. Pairs naturally with 'sof test_inject_sched_gap' for + diagnosing scheduling latency. + + Each scheduler implements scheduler_dump_tasks() which takes its + own lock during the walk; schedulers without an implementation are + silently skipped. + +config SOF_SHELL_LOG_INFO + bool "Log subsystem status command" + default y + depends on SHELL && LOG + help + Enables the 'sof log_status' shell command, which lists every + registered Zephyr log backend with its index, internal id, + active/inactive state and name, plus the total number of + registered log sources in the local domain. + + Read-only and cheap; useful for confirming which backends + (uart_console, adsp_mtrace, adsp_hda, ...) are alive without + having to enable CONFIG_LOG_RUNTIME_FILTERING and the much + larger Zephyr 'log' shell module. + +config SOF_SHELL_MTRACE_DUMP + bool "mtrace SRAM buffer snapshot command" + default y + depends on SHELL && LOG_BACKEND_ADSP_MTRACE + help + Enables the 'sof mtrace_dump' shell command which prints the + unread portion of the ADSP mtrace SRAM ring buffer (the same + buffer normally consumed by host-side mtrace-reader.py). + + The snapshot does NOT advance the host_ptr, so it is safe to + use while a host consumer is running: the host will still + receive every byte. Useful when no host driver is attached + (e.g. early bring-up over UART shell only). + +config SOF_SHELL_MAILBOX_HEX + bool "Hex-dump SOF mailbox regions" + default y + depends on SHELL + help + Enables 'sof mailbox_hex [offset] [length]' which hex + dumps one of the SOF mailbox regions (exception, dspbox, hostbox, + debug). With no arguments lists the regions and their sizes. The + length is clamped to the region size; default 256 bytes. + + Useful for inspecting the panic dump area after a fatal error and + for low-level IPC payload debugging. + +config SOF_SHELL_DBGWIN_DUMP + bool "Hex-dump ADSP debug-window slots" + default y + depends on SHELL && SOC_FAMILY_INTEL_ADSP + help + Enables 'sof dbgwin_dump [slot] [length]' which lists the + descriptors of all ADSP debug-window slots (slot manager + metadata in window 2 page 0) with a friendly type name, and + hex-dumps the slot contents when a slot index is given. + + This makes it possible to peek at gdb_stub, telemetry, + critical_log, broken-marker and other slots from the shell + without host-side tooling. + +config SOF_SHELL_PERF_STATUS + bool "Performance / telemetry status command" + default y + depends on SHELL && SOF_TELEMETRY + help + Enables 'sof perf_status [reset|start|stop|pause]' which prints + the current ipc4_perf_measurements_state plus per-active-core + systick counters from the SOF telemetry slot: + count, last/max time elapsed (cycles), avg/peak KCPS and the + 4k/8k peak utilization buckets. + + With one of the optional sub-commands the command transitions + the perf measurement state machine ('start' calls + enable_performance_counters(), 'reset' calls + reset_performance_counters(); 'stop'/'pause' just update the + state) - useful for taking before/after snapshots without IPC. + +config SOF_SHELL_DAI_LIST + bool "DAI introspection command" + default y + depends on SHELL && ZEPHYR_NATIVE_DRIVERS + help + Enables 'sof dai_list' which iterates dai_get_device_list() + and prints, per DAI, the Zephyr device name, decoded type + (ssp/dmic/hda/alh/uaol/sai/esai/...), index, current channel + count, sample rate, format and word size from + dai_config_get(), plus per-direction fifo address, fifo depth, + DMA handshake id and stream id from dai_get_properties(). + + Useful for verifying which DAIs were registered for a topology + without going through host-side debug. + +config SOF_SHELL_DMA_STATUS + bool "DMA controller / channel status command" + default y + depends on SHELL && ZEPHYR_NATIVE_DRIVERS + help + Enables 'sof dma_status [dma_idx [chan]]': + + 'sof dma_status' - list every DMA controller + registered with SOF (id, + channel count, busy count, + caps/devs bitmasks, base, + Zephyr device name). + 'sof dma_status ' - per-channel status for one + controller. + 'sof dma_status ' - status for one channel. + + Per-channel status comes from sof_dma_get_status() (Zephyr + dma_get_status()) and shows busy/idle, direction, pending and + free bytes, read/write positions and total_copied. Pairs well + with 'sof dai_list' for diagnosing P2M/M2P paths. + +config SOF_SHELL_KCTL_LIST + bool "kcontrol introspection command" + default y + depends on SHELL + help + Enables 'sof kctl_list' which walks the IPC component list and + prints, per component, the comp_id, pipeline_id, core, decoded + module name (volume / gain / mixin / mixout / eqiir / src / + ...) taken from the trace UUID context, a coarse 'kind' tag + (volume / mixer / blob / config) for control-bearing modules, + and the current comp_state. + + This is read-only on purpose. Actual kcontrol get/set values + flow through per-module IPC4 large_config blobs + (set_configuration / get_configuration) which need + module-specific marshalling and are intentionally left to host + tools (tinymix, sof-ctl). Use this command together with 'sof + module_status' to identify which comp_id carries which control + before going through the host path. + +config SOF_SHELL_PROBE_MIRROR + bool "Mirror shell output to probes extraction stream" + default n + depends on SHELL && PROBE + help + Mirrors shell text output to the probe extraction stream while keeping the + selected shell backend (e.g. UART) active. This lets host tools capture + verbose shell output via compressed PCM probes without losing interactivity + on a slow TTY console. + +endmenu # SOF shell commands + config SOF_VREGIONS bool "Enable virtual memory regions" default y if ACE && !ACE_VERSION_1_5 && !ACE_VERSION_2_0 diff --git a/zephyr/shell/kernel.c b/zephyr/shell/kernel.c new file mode 100644 index 000000000000..246fea27a776 --- /dev/null +++ b/zephyr/shell/kernel.c @@ -0,0 +1,2070 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2024 Intel Corporation. + * + * Author: Kai Vehmanen + */ + +/* + * SOF shell - RTOS / firmware infrastructure commands. + * + * This file hosts the root "sof" shell command and all commands that deal + * with RTOS and low level firmware facilities (scheduling, memory, cores, + * IPC transport, logging, debug windows, library/llext management, ...). + * Audio domain commands (pipelines, modules, buffers, DAI/DMA, kcontrols) + * live in shell/user.c and are attached to the same "sof" root via the + * shell iterable-section subcommand mechanism. + */ + +#include /* sof_get() */ +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_SOF_SHELL_PROBE_MIRROR +#include +#endif +#include +#include +#include +#include + +#include +#include +#include +#if CONFIG_SYS_HEAP_RUNTIME_STATS +#include +#endif +#if CONFIG_SOF_SHELL_MMU_DBG +#include +#include +#include +#if CONFIG_XTENSA_MMU +#include +#endif /* CONFIG_XTENSA_MMU */ +#endif /* CONFIG_SOF_SHELL_MMU_DBG */ + +#if CONFIG_SOF_SHELL_LLEXT_LOAD +#include +#include +#include +#endif /* CONFIG_SOF_SHELL_LLEXT_LOAD */ + +#if (CONFIG_SOF_SHELL_LLEXT_LIST || CONFIG_SOF_SHELL_LLEXT_PURGE) && CONFIG_LLEXT +#include +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +#include +#include +#include +#include +#endif + +#ifndef _SHELL_MOD_PAGE_SZ +#ifdef CONFIG_MM_DRV_PAGE_SIZE +#define _SHELL_MOD_PAGE_SZ CONFIG_MM_DRV_PAGE_SIZE +#else +#define _SHELL_MOD_PAGE_SZ 4096 +#endif +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +#include +#include +#include +#include +#endif + +#include +#include + +/* + * Root "sof" command set. The set is created here and shared with shell/user.c + * through the shell iterable-section mechanism so both translation units can + * register top level subcommands under "sof" using SHELL_SUBCMD_ADD((sof), ...). + * The command itself is registered at the bottom of this file. + */ +SHELL_SUBCMD_SET_CREATE(sub_sof, (sof)); + +#define SOF_TEST_INJECT_SCHED_GAP_USEC 1500 + +#include + +#if CONFIG_SOF_SHELL_PROBE_MIRROR +static void sof_shell_mirror_output(const char *text, size_t len) +{ + if (!len) + return; + + probe_shell_output((const uint8_t *)text, len); +} + +static void sof_shell_fprintf_mirror(const struct shell *sh, + enum shell_vt100_color color, + const char *fmt, ...) +{ + char out[512]; + va_list ap; + int n; + size_t len; + + va_start(ap, fmt); + n = vsnprintk(out, sizeof(out), fmt, ap); + va_end(ap); + + if (n < 0) + return; + + len = MIN((size_t)n, sizeof(out) - 1); + + shell_fprintf(sh, color, "%s", out); + sof_shell_mirror_output(out, len); +} + +#define shell_fprintf sof_shell_fprintf_mirror +#endif + +__cold static int cmd_sof_test_inject_sched_gap(const struct shell *sh, + size_t argc, char *argv[]) +{ + uint32_t block_time = SOF_TEST_INJECT_SCHED_GAP_USEC; + char *endptr = NULL; + +#ifndef CONFIG_CROSS_CORE_STREAM + shell_fprintf(sh, SHELL_NORMAL, "Domain blocking not supported, not reliable on SMP\n"); +#endif + + if (argc > 1) { + block_time = strtol(argv[1], &endptr, 0); + if (endptr == argv[1]) + return -EINVAL; + } + + sof_shell_inject_sched_gap(block_time); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), test_inject_sched_gap, NULL, + "Inject a gap to audio scheduling\n", + cmd_sof_test_inject_sched_gap, 0, 0); + +#if CONFIG_SOF_SHELL_CORE_STATUS +__cold static int cmd_sof_core_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_core_status cs; + unsigned int i; + + sof_shell_core_status_get(&cs); + + shell_print(sh, "%-6s %-8s %s", "core", "enabled", "current"); + + for (i = 0; i < cs.core_count; i++) { + shell_print(sh, "%-6u %-8s %s", + i, + cs.enabled[i] ? "yes" : "no", + (i == cs.current) ? "<--" : ""); + } + + return 0; +} + +SHELL_SUBCMD_ADD((sof), core_status, NULL, + "Print enabled/active state of each DSP core\n", + cmd_sof_core_status, 0, 0); +#endif /* CONFIG_SOF_SHELL_CORE_STATUS */ + +#if CONFIG_SOF_SHELL_SRAM_STATUS +__cold static int cmd_sof_sram_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct k_heap *h = sof_sys_heap_get(); + struct sys_memory_stats stats; + + if (!h) { + shell_print(sh, "Heap not available"); + return 0; + } + + sys_heap_runtime_stats_get(&h->heap, &stats); + + shell_print(sh, "HPSRAM heap (sof_heap):"); + shell_print(sh, " allocated: %zu bytes", stats.allocated_bytes); + shell_print(sh, " free: %zu bytes", stats.free_bytes); + shell_print(sh, " max allocated:%zu bytes", stats.max_allocated_bytes); + shell_print(sh, " total: %zu bytes", + stats.allocated_bytes + stats.free_bytes); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), sram_status, NULL, + "Print HPSRAM heap usage statistics\n", + cmd_sof_sram_status, 0, 0); +#endif /* CONFIG_SOF_SHELL_SRAM_STATUS */ + +#if CONFIG_SOF_SHELL_CLOCK_STATUS +__cold static int cmd_sof_clock_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_clock_status cl; + unsigned int i; + + sof_shell_clock_status_get(&cl); + + if (!cl.valid) { + shell_print(sh, "Clock info not available"); + return 0; + } + + shell_print(sh, "%-6s %-12s %s", "clock", "freq_hz", "freq_mhz"); + + for (i = 0; i < cl.num_clocks; i++) { + uint32_t freq = cl.freq_hz[i]; + + shell_print(sh, "%-6u %-12u %.1f", + i, freq, (double)freq / 1000000.0); + } + + return 0; +} + +SHELL_SUBCMD_ADD((sof), clock_status, NULL, + "Print current clock frequency for each DSP core\n", + cmd_sof_clock_status, 0, 0); +#endif /* CONFIG_SOF_SHELL_CLOCK_STATUS */ + + +#if CONFIG_SOF_SHELL_MMU_DBG + +#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB + +/* + * The privileged TLB MMIO reads and the memory-region query are performed by + * the sof_shell_tlb_*() system calls (see zephyr/shell/syscall.c). The shell + * only decodes the snapshot returned by those calls, so it works unchanged + * when the shell thread runs in user mode. + */ + +/* Convert virtual-address index → physical address using snapshot metadata */ +__cold static uintptr_t shell_tlb_idx_to_pa(const struct sof_shell_tlb_meta *m, + uint16_t entry) +{ + uint32_t paddr_mask = (1u << m->paddr_size) - 1; + + return m->phys_base + ((uintptr_t)(entry & paddr_mask) * m->page_size); +} + +/* Decode 16-bit TLB entry permission bits into a short string */ +__cold static void shell_tlb_flags_str(const struct sof_shell_tlb_meta *m, + uint16_t entry, char *buf) +{ + uint16_t write_bit = (uint16_t)(1u << m->write_bit_idx); + uint16_t exec_bit = (uint16_t)(1u << m->exec_bit_idx); + + buf[0] = 'R'; + buf[1] = (entry & write_bit) ? 'W' : '-'; + buf[2] = (entry & exec_bit) ? 'X' : '-'; + buf[3] = '\0'; +} + +/* sof mmu_status */ +__cold static int cmd_sof_mmu_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_tlb_meta meta; + uint32_t i; + + sof_shell_tlb_meta_get(&meta); + + shell_print(sh, "Intel ADSP MTL TLB / Virtual Memory Status"); + shell_print(sh, " VM base: 0x%08x", meta.vm_base); + shell_print(sh, " VM size: 0x%08x (%u KB)", + meta.total_entries * meta.page_size, + meta.total_entries * meta.page_size / 1024); + shell_print(sh, " page size: %u B", meta.page_size); + shell_print(sh, " total entries: %u", meta.total_entries); + shell_print(sh, " mapped pages: %u (%u KB)", + meta.enabled_entries, meta.enabled_entries * meta.page_size / 1024); + shell_print(sh, " free pages: %u (%u KB)", + meta.total_entries - meta.enabled_entries, + (meta.total_entries - meta.enabled_entries) * meta.page_size / 1024); + shell_print(sh, " TLB MMIO base: 0x%08x", meta.tlb_mmio_base); + shell_print(sh, " paddr_size: %u enable_bit:%u exec_bit:%u write_bit:%u", + meta.paddr_size, meta.paddr_size, + meta.exec_bit_idx, meta.write_bit_idx); + + shell_print(sh, ""); + shell_print(sh, "Mapped memory regions (sys_mm_drv):"); + shell_print(sh, " %-10s %-10s %s", "address", "size", "attr"); + + if (meta.region_count) { + for (i = 0; i < meta.region_count; i++) + shell_print(sh, " 0x%08x 0x%08x 0x%08x", + meta.regions[i].addr, meta.regions[i].size, + meta.regions[i].attr); + if (meta.regions_truncated) + shell_print(sh, " ... (truncated)"); + } else { + shell_print(sh, " (not available)"); + } + return 0; +} + +/* sof tlb_dump */ +__cold static int cmd_sof_tlb_dump(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_tlb_meta meta; + uint16_t buf[128]; + uint32_t enable_bit; + uint32_t count = 0; + uint32_t base; + + sof_shell_tlb_meta_get(&meta); + enable_bit = 1u << meta.paddr_size; + + shell_print(sh, " idx vaddr paddr flags entry"); + + for (base = 0; base < meta.total_entries; base += ARRAY_SIZE(buf)) { + uint32_t got = sof_shell_tlb_entries_get(base, ARRAY_SIZE(buf), buf); + uint32_t j; + + if (!got) + break; + + for (j = 0; j < got; j++) { + uint16_t entry = buf[j]; + uint32_t idx = base + j; + uintptr_t vaddr, paddr; + char flags[4]; + + if (!(entry & enable_bit)) + continue; + + vaddr = meta.vm_base + (uintptr_t)idx * meta.page_size; + paddr = shell_tlb_idx_to_pa(&meta, entry); + shell_tlb_flags_str(&meta, entry, flags); + shell_print(sh, " %-5u 0x%08x 0x%08x %s 0x%04x", + idx, (uint32_t)vaddr, (uint32_t)paddr, + flags, (uint32_t)entry); + count++; + } + } + + shell_print(sh, "Total: %u/%u entries active", count, meta.total_entries); + return 0; +} + +/* sof tlb_lookup [end_vaddr] */ +__cold static int cmd_sof_tlb_lookup(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_tlb_meta meta; + uintptr_t vstart, vend, vm_base, vm_end; + uint32_t enable_bit, page_mask; + + sof_shell_tlb_meta_get(&meta); + enable_bit = 1u << meta.paddr_size; + page_mask = meta.page_size - 1; + + /* Parse start address */ + { + char *ep; + unsigned long v = strtoul(argv[1], &ep, 0); + + if (ep == argv[1]) { + shell_print(sh, "error: invalid address '%s'", argv[1]); + return -EINVAL; + } + vstart = (uintptr_t)v & ~(uintptr_t)page_mask; + } + + if (argc > 2) { + char *ep; + unsigned long v = strtoul(argv[2], &ep, 0); + + if (ep == argv[2]) { + shell_print(sh, "error: invalid address '%s'", argv[2]); + return -EINVAL; + } + vend = (uintptr_t)v & ~(uintptr_t)page_mask; + } else { + vend = vstart; + } + + if (vend < vstart) + vend = vstart; + + vm_base = meta.vm_base; + vm_end = vm_base + (uintptr_t)meta.total_entries * meta.page_size - 1; + + shell_print(sh, " vaddr paddr mapped flags bank entry"); + + for (uintptr_t va = vstart; va <= vend; va += meta.page_size) { + uint16_t entry; + uint32_t idx; + uintptr_t pa; + uint32_t bank; + char flags[4]; + + if (va < vm_base || va > vm_end) { + shell_print(sh, " 0x%08x (outside VM range)", + (uint32_t)va); + continue; + } + + idx = (uint32_t)((va - vm_base) / meta.page_size); + sof_shell_tlb_entries_get(idx, 1, &entry); + + if (!(entry & enable_bit)) { + shell_print(sh, " 0x%08x (not mapped)", + (uint32_t)va); + continue; + } + + pa = shell_tlb_idx_to_pa(&meta, entry); + bank = (uint32_t)((pa - meta.phys_base) / (128 * 1024)); + shell_tlb_flags_str(&meta, entry, flags); + shell_print(sh, " 0x%08x 0x%08x yes %s %-4u 0x%04x", + (uint32_t)va, (uint32_t)pa, + flags, bank, (uint32_t)entry); + } + return 0; +} + +#endif /* CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB */ + +#endif /* CONFIG_SOF_SHELL_MMU_DBG */ + +#if CONFIG_SOF_SHELL_MMU_DBG +#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +SHELL_SUBCMD_ADD((sof), mmu_status, NULL, + "Print Intel ADSP MTL TLB / virtual memory status\n", + cmd_sof_mmu_status, 0, 0); +SHELL_SUBCMD_ADD((sof), tlb_dump, NULL, + "Dump all active TLB entries (vaddr/paddr/flags)\n", + cmd_sof_tlb_dump, 0, 0); +SHELL_SUBCMD_ADD((sof), tlb_lookup, NULL, + "Query TLB for a page or range: [end_vaddr]\n", + cmd_sof_tlb_lookup, 2, 1); +#endif +#endif + +#if CONFIG_XTENSA_MMU + +/* + * Xtensa hardware MMU helpers. + * + * PDTLB result layout (Xtensa ISA §4.6.5): + * bit[4] = HIT + * bits[3:0] = TLB way (valid when HIT) + * + * RDTLB0 result: + * bits[31:12] = Virtual Page Number + * bits[5:4] = ring (0=kernel, 1=unused, 2=user, 3=shared) + * bits[3:0] = CA (cache + access attributes) + * + * RDTLB1 result: + * bits[31:12] = Physical Page Number + * bits[3:0] = CA (same as RDTLB0) + * + * CA bits (from ): + * bit 0 = XTENSA_MMU_PERM_X (executable) + * bit 1 = XTENSA_MMU_PERM_W (writable) + * bit 2 = XTENSA_MMU_CACHED_WB (write-back cache) + * bit 3 = XTENSA_MMU_CACHED_WT (write-through cache) + * bits 2+3 both set = illegal / not-present + */ +#define SHELL_PDTLB_HIT 0x10U /* bit 4 of pdtlb result */ +#define SHELL_PTE_RING_SHIFT 4U +#define SHELL_PTE_RING_MASK 0x30U +#define SHELL_PTE_CA_MASK 0x0FU +#define SHELL_PTE_PPN_MASK 0xFFFFF000U + +static inline uint32_t shell_pdtlb(void *vaddr) +{ + uint32_t r; + __asm__ __volatile__("pdtlb %0, %1\n\t" : "=a"(r) : "a"((uint32_t)vaddr)); + return r; +} + +static inline uint32_t shell_rdtlb0(uint32_t entry) +{ + uint32_t r; + __asm__ volatile("rdtlb0 %0, %1\n\t" : "=a"(r) : "a"(entry)); + return r; +} + +static inline uint32_t shell_rdtlb1(uint32_t entry) +{ + uint32_t r; + __asm__ volatile("rdtlb1 %0, %1\n\t" : "=a"(r) : "a"(entry)); + return r; +} + +static inline uint32_t shell_rsr_rasid(void) +{ + uint32_t r; + __asm__ volatile("rsr %0, rasid" : "=a"(r)); + return r; +} + +/* Decode 4-bit CA cache field into a short string */ +__cold static const char *ca_cache_str(uint32_t ca) +{ + switch (ca & (XTENSA_MMU_CACHED_WB | XTENSA_MMU_CACHED_WT)) { + case 0: return "uncached"; + case XTENSA_MMU_CACHED_WB: return "WB "; + case XTENSA_MMU_CACHED_WT: return "WT "; + default: return "illegal "; + } +} + +__cold_rodata static const char * const ring_name[] = { + "kernel", "unused", "user", "shared" +}; + +/* sof rasid */ +__cold static int cmd_sof_rasid(const struct shell *sh, + size_t argc, char *argv[]) +{ + uint32_t rasid = shell_rsr_rasid(); + int ring; + + shell_print(sh, "RASID: 0x%08x", rasid); + for (ring = 0; ring < 4; ring++) { + uint8_t asid = (uint8_t)((rasid >> (ring * 8)) & 0xff); + + shell_print(sh, " ring %d (%s):\tASID 0x%02x", + ring, ring_name[ring], asid); + } + return 0; +} + +/* sof page_info [end_vaddr] */ +__cold static int cmd_sof_page_info(const struct shell *sh, + size_t argc, char *argv[]) +{ + uintptr_t vstart, vend; + uint32_t rasid; + + { + char *ep; + unsigned long v = strtoul(argv[1], &ep, 0); + + if (ep == argv[1]) { + shell_print(sh, "error: invalid address '%s'", argv[1]); + return -EINVAL; + } + vstart = (uintptr_t)v & ~(uintptr_t)(KB(4) - 1); + } + + if (argc > 2) { + char *ep; + unsigned long v = strtoul(argv[2], &ep, 0); + + if (ep == argv[2]) { + shell_print(sh, "error: invalid address '%s'", argv[2]); + return -EINVAL; + } + vend = (uintptr_t)v & ~(uintptr_t)(KB(4) - 1); + } else { + vend = vstart; + } + + if (vend < vstart) + vend = vstart; + + rasid = shell_rsr_rasid(); + shell_print(sh, "RASID: 0x%08x", rasid); + shell_print(sh, " %-12s %-12s ring asid perms cache"); + + for (uintptr_t va = vstart; va <= vend; va += KB(4)) { + uint32_t probe = shell_pdtlb((void *)va); + + if (!(probe & SHELL_PDTLB_HIT)) { + shell_print(sh, + " 0x%08x (DTLB miss — not in TLB cache)", + (uint32_t)va); + continue; + } + + uint32_t pte0 = shell_rdtlb0(probe); + uint32_t pte1 = shell_rdtlb1(probe); + uint32_t ring = (pte0 & SHELL_PTE_RING_MASK) >> SHELL_PTE_RING_SHIFT; + uint32_t ca = pte0 & SHELL_PTE_CA_MASK; + uint32_t paddr = pte1 & SHELL_PTE_PPN_MASK; + uint8_t asid = (uint8_t)((rasid >> (ring * 8)) & 0xff); + char perm[4] = { + 'R', + (ca & XTENSA_MMU_PERM_W) ? 'W' : '-', + (ca & XTENSA_MMU_PERM_X) ? 'X' : '-', + '\0' + }; + + shell_print(sh, + " 0x%08x 0x%08x %u 0x%02x %s %s", + (uint32_t)va, paddr, + ring, asid, + perm, ca_cache_str(ca)); + } + return 0; +} + +#endif /* CONFIG_XTENSA_MMU */ + +#if CONFIG_XTENSA_MMU +SHELL_SUBCMD_ADD((sof), rasid, NULL, + "Decode RASID register: ring 0-3 to ASID mapping\n", + cmd_sof_rasid, 0, 0); +SHELL_SUBCMD_ADD((sof), page_info, NULL, + "Probe DTLB for a page or range: [end_vaddr]\n" + "Reports physical address, ring, ASID, R/W/X permissions" + " and cache mode for each page currently in the DTLB.\n", + cmd_sof_page_info, 2, 1); +#endif + +#if CONFIG_SOF_SHELL_LLEXT_LOAD || (CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT) || \ + (CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT) +/* parse_long: shared numeric argument parser for kernel shell commands. + * Kept outside individual feature guards so all callers (LLEXT load/ctor/call) + * can see it. + */ +__cold static int parse_long(const struct shell *sh, const char *s, long *out, + long min_val, long max_val) +{ + char *endptr; + long v = strtol(s, &endptr, 0); + + if (endptr == s || v < min_val || v > max_val) { + shell_print(sh, "error: invalid value '%s' (allowed %ld..%ld)", + s, min_val, max_val); + return -EINVAL; + } + *out = v; + return 0; +} +#endif + +#if CONFIG_SOF_SHELL_LLEXT_LOAD + +#define SOF_SHELL_LLEXT_TIMEOUT_MSEC 120000U +#define SOF_SHELL_LLEXT_POLL_MSEC 500U + +__cold static int cmd_sof_llext_load(const struct shell *sh, + size_t argc, char *argv[]) +{ + const char *name = argv[1]; + uint32_t lib_id = 1; + volatile struct sof_shell_llext_slot *slot; + uint32_t elapsed = 0; + uint32_t state; + + if (argc > 2) { + long val; + int ret = parse_long(sh, argv[2], &val, 1, LIB_MANAGER_MAX_LIBS - 1); + + if (ret) + return ret; + lib_id = (uint32_t)val; + } + + /* Acquire or reuse the LLEXT_LOAD debug window slot */ +#if CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER + { + struct adsp_dw_desc slot_desc = { .type = ADSP_DW_SLOT_LLEXT_LOAD }; + size_t slot_size; + + slot = adsp_dw_request_slot(&slot_desc, &slot_size); + if (!slot) { + shell_error(sh, "Failed to acquire debug window slot"); + return -ENOMEM; + } + } +#else + /* Fall back to a compile-time fixed slot index */ + slot = (volatile struct sof_shell_llext_slot *) + ADSP_DW->slots[CONFIG_SOF_SHELL_LLEXT_LOAD_SLOT_NUM]; + ADSP_DW->descs[CONFIG_SOF_SHELL_LLEXT_LOAD_SLOT_NUM].type = + ADSP_DW_SLOT_LLEXT_LOAD; +#endif + + state = slot->state; + if (state != SOF_SHELL_LLEXT_IDLE) { + shell_error(sh, "llext_load slot busy (state=%u) — try again later", + state); + return -EBUSY; + } + + /* Initialise the shared slot word-by-word (MMIO/uncached region) */ + { + volatile uint32_t *p = (volatile uint32_t *)slot; + size_t nwords = sizeof(struct sof_shell_llext_slot) / sizeof(uint32_t); + + for (size_t i = 0; i < nwords; i++) + p[i] = 0; + } + strncpy((char *)slot->name, name, sizeof(slot->name) - 1); + slot->lib_id = lib_id; + slot->magic = SOF_SHELL_LLEXT_MAGIC; + /* Publish state last so the host only sees REQUESTING once all fields are set */ + slot->state = SOF_SHELL_LLEXT_REQUESTING; + + shell_print(sh, "Slot ready: name=%s lib_id=%u timeout=%us", + name, lib_id, SOF_SHELL_LLEXT_TIMEOUT_MSEC / 1000); + shell_print(sh, "On host: dd if= of=/sys/kernel/debug/sof/llext_load bs=$(stat -c%%s ) count=1"); + + /* Poll waiting for the host to finish DMA + library load */ + while (elapsed < SOF_SHELL_LLEXT_TIMEOUT_MSEC) { + k_msleep(SOF_SHELL_LLEXT_POLL_MSEC); + elapsed += SOF_SHELL_LLEXT_POLL_MSEC; + + state = slot->state; + + if (state == SOF_SHELL_LLEXT_DMA_DONE) { + shell_print(sh, + "llext_load OK: lib_id=%u %u bytes transferred", + lib_id, slot->xfer_bytes); + slot->state = SOF_SHELL_LLEXT_IDLE; + return 0; + } + + if (state == SOF_SHELL_LLEXT_ERROR) { + shell_error(sh, "llext_load FAILED: result=%d", slot->result); + slot->state = SOF_SHELL_LLEXT_IDLE; + return (int)slot->result; + } + } + + shell_error(sh, "llext_load timeout after %us", + SOF_SHELL_LLEXT_TIMEOUT_MSEC / 1000); + slot->state = SOF_SHELL_LLEXT_IDLE; + return -ETIMEDOUT; +} + +SHELL_SUBCMD_ADD((sof), llext_load, NULL, + "Load llext module from host: [lib_id=1]\n" + "Sets up the DMA handshake slot then waits for:\n" + " dd if= of=/sys/kernel/debug/sof/llext_load\\\n" + " bs=$(stat -c%s ) count=1\n" + "on the host. Prints result when DMA and IPC4 load complete.\n", + cmd_sof_llext_load, 2, 1); +#endif /* CONFIG_SOF_SHELL_LLEXT_LOAD */ + +#if CONFIG_SOF_SHELL_LLEXT_LIST + +/* + * sof llext_list + * + * Lists all llext libraries currently held in IMR/DRAM. For each library the + * DRAM base address, total storage size and per-module-file SRAM state are + * printed. + * + * Example output: + * llext libs in IMR/DRAM: + * [1] base=0x89000000 size=49152 B modules=1 + * [1:0] TESTER mapped=NO use=0 dep=0 + */ +__cold static int cmd_sof_llext_list(const struct shell *sh, + size_t argc, char *argv[]) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + +#if CONFIG_LIBRARY_MANAGER + static struct sof_shell_llext_list list; + uint32_t l; + + sof_shell_llext_list_get(&list); + + shell_print(sh, "llext libs in IMR/DRAM:"); + + if (!list.count) { + shell_print(sh, " (none)"); + return 0; + } + + for (l = 0; l < list.count; l++) { + const struct sof_shell_llext_lib *lib = &list.libs[l]; + uint32_t i; + + shell_print(sh, "[%u] base=0x%08x size=%u B manifest_mods=%u elf_files=%u", + lib->lib_id, lib->base_addr, lib->store_bytes, + lib->manifest_mods, lib->elf_files); + + for (i = 0; i < lib->mod_count; i++) { + const struct sof_shell_llext_mod *m = &lib->mods[i]; + + shell_print(sh, + " [%u:%u] %-8s" + " DRAM=yes SRAM=%-3s" + " use=%-2d dep=%u", + lib->lib_id, i, m->name, + m->mapped ? "yes" : "no", + m->use, m->dep); + } + } +#else + shell_print(sh, "Library manager not enabled."); +#endif /* CONFIG_LIBRARY_MANAGER */ + return 0; +} + +#endif /* CONFIG_SOF_SHELL_LLEXT_LIST */ + +#if CONFIG_SOF_SHELL_LLEXT_PURGE + +/* + * sof llext_purge + * + * Removes a loadable llext library from IMR/DRAM storage and frees its memory. + * All module files belonging to the library must be unloaded from SRAM first + * (i.e., all pipeline instances using the library must be torn down). + * + * Example: + * uart:~$ sof llext_purge 1 + * llext_purge: lib 1 freed OK + */ +__cold static int cmd_sof_llext_purge(const struct shell *sh, + size_t argc, char *argv[]) +{ +#if CONFIG_LIBRARY_MANAGER + char *endptr = NULL; + long lib_id; + int ret; + + lib_id = strtol(argv[1], &endptr, 0); + if (endptr == argv[1] || lib_id < 1 || lib_id >= LIB_MANAGER_MAX_LIBS) { + shell_error(sh, "lib_id must be 1..%d", LIB_MANAGER_MAX_LIBS - 1); + return -EINVAL; + } + + ret = sof_shell_llext_purge((uint32_t)lib_id); + switch (ret) { + case 0: + shell_print(sh, "llext_purge: lib %ld freed OK", lib_id); + break; + case -ENOENT: + shell_error(sh, "llext_purge: lib %ld not loaded", lib_id); + break; + case -EBUSY: + shell_error(sh, "llext_purge: lib %ld still active in SRAM — " + "destroy all pipelines using it first", lib_id); + break; + default: + shell_error(sh, "llext_purge: lib %ld failed: %d", lib_id, ret); + break; + } + return ret; +#else + ARG_UNUSED(argc); + ARG_UNUSED(argv); + shell_print(sh, "Library manager not enabled."); + return -ENOSYS; +#endif /* CONFIG_LIBRARY_MANAGER */ +} + +#endif /* CONFIG_SOF_SHELL_LLEXT_PURGE */ + +#if CONFIG_SOF_SHELL_LLEXT_LIST +SHELL_SUBCMD_ADD((sof), llext_list, NULL, + "List llext libraries stored in IMR/DRAM.\n" + "For each library shows base address, storage size and per-module\n" + "SRAM mapping state (yes/no), use count and dependency count.\n", + cmd_sof_llext_list, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_LLEXT_PURGE +SHELL_SUBCMD_ADD((sof), llext_purge, NULL, + "Purge llext library from IMR/DRAM: \n" + "Fails with -EBUSY if any module in the library is still\n" + "mapped in SRAM (i.e. a pipeline using it is still active).\n", + cmd_sof_llext_purge, 2, 0); +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT + +/* + * sof llext ctor + * sof llext dtor + */ +__cold static int cmd_sof_llext_ctor_dtor(const struct shell *sh, + size_t argc, char *argv[], + bool is_ctor) +{ +#if CONFIG_LIBRARY_MANAGER + static struct sof_shell_llext_op_result res; + const char *cmd = is_ctor ? "llext_ctor" : "llext_dtor"; + long lib_id; + uint32_t i; + int ret; + + ARG_UNUSED(argc); + + if (parse_long(sh, argv[1], &lib_id, 1, LIB_MANAGER_MAX_LIBS - 1) < 0) + return -EINVAL; + + ret = sof_shell_llext_ctor_dtor((uint32_t)lib_id, is_ctor ? 1 : 0, &res); + + if (res.status == -ENOENT && res.count == 0) { + shell_error(sh, "%s: lib %ld not loaded", cmd, lib_id); + return -ENOENT; + } + + for (i = 0; i < res.count; i++) { + const struct sof_shell_llext_op_mod *m = &res.mods[i]; + const char *name = m->name[0] ? m->name : "?"; + + if (m->ret < 0) + shell_error(sh, "%s: lib %ld mod %u (%s): failed %d", + cmd, lib_id, i, name, m->ret); + else + shell_print(sh, "%s: lib %ld mod %u (%s): OK", + cmd, lib_id, i, name); + } + + return ret; +#else + ARG_UNUSED(argc); + ARG_UNUSED(argv); + ARG_UNUSED(is_ctor); + shell_print(sh, "Library manager not enabled."); + return -ENOSYS; +#endif /* CONFIG_LIBRARY_MANAGER */ +} + +__cold static int cmd_sof_llext_ctor(const struct shell *sh, + size_t argc, char *argv[]) +{ + return cmd_sof_llext_ctor_dtor(sh, argc, argv, true); +} + +__cold static int cmd_sof_llext_dtor(const struct shell *sh, + size_t argc, char *argv[]) +{ + return cmd_sof_llext_ctor_dtor(sh, argc, argv, false); +} + +#endif /* CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT */ + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT + +/* + * sof llext call + * + * Find a global symbol by name in any module belonging to the given library + * and invoke it as a void (*)(void) function. + */ +__cold static int cmd_sof_llext_call(const struct shell *sh, + size_t argc, char *argv[]) +{ +#if CONFIG_LIBRARY_MANAGER + static struct sof_shell_llext_op_result res; + const char *sym_name = argv[2]; + long lib_id; + uint32_t i; + int ret; + + ARG_UNUSED(argc); + + if (parse_long(sh, argv[1], &lib_id, 1, LIB_MANAGER_MAX_LIBS - 1) < 0) + return -EINVAL; + + ret = sof_shell_llext_call((uint32_t)lib_id, sym_name, &res); + + if (res.status == -ENOENT && res.count == 0) { + shell_error(sh, "llext_call: lib %ld not loaded", lib_id); + return -ENOENT; + } + + for (i = 0; i < res.count; i++) { + const struct sof_shell_llext_op_mod *m = &res.mods[i]; + const char *name = m->name[0] ? m->name : "?"; + + if (m->ret == -ENODEV) { + shell_print(sh, + "llext_call: lib %ld mod %u: not allocated - run 'sof llext ctor %ld' first", + lib_id, i, lib_id); + } else if (m->flag) { + shell_print(sh, + "llext_call: lib %ld mod %u (%s) sym %s @ 0x%lx: done", + lib_id, i, name, sym_name, m->addr); + } else { + shell_print(sh, + "llext_call: lib %ld mod %u (%s): symbol '%s' not found", + lib_id, i, name, sym_name); + } + } + + return ret; +#else + ARG_UNUSED(argc); + ARG_UNUSED(argv); + shell_print(sh, "Library manager not enabled."); + return -ENOSYS; +#endif /* CONFIG_LIBRARY_MANAGER */ +} + +#endif /* CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT */ + +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT +SHELL_SUBCMD_ADD((sof), llext_ctor, NULL, + "Call constructors for all modules in a library: \n", + cmd_sof_llext_ctor, 2, 0); +SHELL_SUBCMD_ADD((sof), llext_dtor, NULL, + "Call destructors for all modules in a library: \n", + cmd_sof_llext_dtor, 2, 0); +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT +SHELL_SUBCMD_ADD((sof), llext_call, NULL, + "Call a named symbol in all modules of a library: \n", + cmd_sof_llext_call, 3, 0); +#endif + +#if CONFIG_SOF_SHELL_CORE_POWER + +/* + * sof core_on + * sof core_off + * + * Power a secondary DSP core on or off. Core 0 (primary) cannot be + * controlled via these commands. + */ +__cold static int cmd_sof_core_on(const struct shell *sh, + size_t argc, char *argv[]) +{ + char *endptr = NULL; + long id; + int ret; + + id = strtol(argv[1], &endptr, 0); + if (endptr == argv[1] || id < 1 || id >= CONFIG_CORE_COUNT) { + shell_error(sh, "core_id must be 1..%d", CONFIG_CORE_COUNT - 1); + return -EINVAL; + } + + if (sof_shell_core_is_enabled((uint32_t)id)) { + shell_print(sh, "core %ld already active", id); + return 0; + } + + ret = sof_shell_core_enable((uint32_t)id); + if (ret) + shell_error(sh, "core_on: failed to enable core %ld: %d", id, ret); + else + shell_print(sh, "core_on: core %ld enabled", id); + + return ret; +} + +__cold static int cmd_sof_core_off(const struct shell *sh, + size_t argc, char *argv[]) +{ + char *endptr = NULL; + long id; + + id = strtol(argv[1], &endptr, 0); + if (endptr == argv[1] || id < 1 || id >= CONFIG_CORE_COUNT) { + shell_error(sh, "core_id must be 1..%d", CONFIG_CORE_COUNT - 1); + return -EINVAL; + } + + if (!sof_shell_core_is_enabled((uint32_t)id)) { + shell_print(sh, "core %ld already inactive", id); + return 0; + } + + sof_shell_core_disable((uint32_t)id); + + if (sof_shell_core_is_enabled((uint32_t)id)) { + shell_error(sh, "core_off: core %ld did not power down", id); + return -EIO; + } + + shell_print(sh, "core_off: core %ld disabled", id); + return 0; +} + +#endif /* CONFIG_SOF_SHELL_CORE_POWER */ + +#if CONFIG_SOF_SHELL_CORE_POWER +SHELL_SUBCMD_ADD((sof), core_on, NULL, + "Power on a secondary DSP core: \n" + "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", + cmd_sof_core_on, 2, 0); +SHELL_SUBCMD_ADD((sof), core_off, NULL, + "Power off a secondary DSP core: \n" + "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", + cmd_sof_core_off, 2, 0); +#endif + +__cold static int cmd_sof_version(const struct shell *sh, size_t argc, char *argv[]) +{ + shell_print(sh, "SOF Version: %d.%d.%d-%s (Build %d)", + SOF_MAJOR, SOF_MINOR, SOF_MICRO, SOF_TAG, SOF_BUILD); + shell_print(sh, "Git Tag: %s", SOF_GIT_TAG); + shell_print(sh, "Source Hash: 0x%08x", SOF_SRC_HASH); + return 0; +} + +SHELL_SUBCMD_ADD((sof), version, NULL, + "Print the current SOF software version\n", + cmd_sof_version, 0, 0); + +#if CONFIG_SOF_VREGIONS +__cold static void vpage_alloc_print_cb(unsigned int idx, unsigned int vpage, + unsigned int pages, void *ctx) +{ + const struct shell *sh = ctx; + + shell_fprintf(sh, SHELL_NORMAL, " [%u] vpage %u, pages %u\n", + idx, vpage, pages); +} + +struct vregion_print_ctx { + const struct shell *sh; + int count; +}; + +__cold static void vregion_print_cb(int idx, const struct vregion_snapshot *s, + void *ctx) +{ + struct vregion_print_ctx *c = ctx; + + shell_fprintf(c->sh, SHELL_NORMAL, + " [%d] Base: 0x%lx, Size: %#zx bytes, Pages: %u\n", + idx, (unsigned long)s->base, s->size, s->pages); + shell_fprintf(c->sh, SHELL_NORMAL, + " Lifetime Used: %#zx bytes, Free Count: %d\n", + s->lifetime_used, s->lifetime_free_count); + shell_fprintf(c->sh, SHELL_NORMAL, " Use Count: %u\n", + s->use_count); + c->count++; +} +#endif /* CONFIG_SOF_VREGIONS */ + +__cold static int cmd_sof_vpage_info(const struct shell *sh, size_t argc, char *argv[]) +{ +#if CONFIG_SOF_VREGIONS + struct vpage_stats stats; + + vpage_get_stats(&stats); + + shell_fprintf(sh, SHELL_NORMAL, "Virtual Page Allocator Status:\n"); + shell_fprintf(sh, SHELL_NORMAL, + " Region Base: %p, Size: %#zx bytes, Total Pages: %u\n", + stats.region_base, stats.region_size, stats.total_pages); + shell_fprintf(sh, SHELL_NORMAL, " Free Pages: %u\n", stats.free_pages); + shell_fprintf(sh, SHELL_NORMAL, " Allocated Elements in use: %u / %u\n", + stats.num_elems_in_use, stats.max_allocs); + + vpage_for_each_alloc(vpage_alloc_print_cb, (void *)sh); +#else + shell_fprintf(sh, SHELL_NORMAL, "Virtual regions not enabled\n"); +#endif + return 0; +} + +__cold static int cmd_sof_vregion_info(const struct shell *sh, size_t argc, char *argv[]) +{ +#if CONFIG_SOF_VREGIONS + struct vregion_print_ctx ctx = { .sh = sh, .count = 0 }; + + shell_fprintf(sh, SHELL_NORMAL, "Virtual Regions Status:\n"); + vregion_for_each(vregion_print_cb, &ctx); + if (ctx.count == 0) + shell_fprintf(sh, SHELL_NORMAL, + " No active virtual regions found.\n"); +#else + shell_fprintf(sh, SHELL_NORMAL, "Virtual regions not enabled\n"); +#endif + return 0; +} + + +SHELL_SUBCMD_ADD((sof), vpage_status, NULL, + "Print virtual page allocator status\n", + cmd_sof_vpage_info, 0, 0); + +SHELL_SUBCMD_ADD((sof), vregion_status, NULL, + "Print virtual regions status\n", + cmd_sof_vregion_info, 0, 0); + +__cold static int cmd_sof_ipc_stats(const struct shell *sh, size_t argc, char *argv[]) +{ + struct ipc_stats s; + + if (argc > 1 && !strcmp(argv[1], "reset")) { + sof_shell_ipc_stats_reset(); + shell_print(sh, "ipc stats reset"); + return 0; + } + + sof_shell_ipc_stats_get(&s); + shell_print(sh, "IPC statistics:"); + shell_print(sh, " rx_count : %u", s.rx_count); + shell_print(sh, " rx_errors : %u", s.rx_errors); + shell_print(sh, " tx_count : %u", s.tx_count); + shell_print(sh, " tx_direct_count : %u", s.tx_direct_count); + shell_print(sh, " tx_errors : %u", s.tx_errors); + return 0; +} + +__cold static int cmd_sof_ipc_last(const struct shell *sh, size_t argc, char *argv[]) +{ + struct ipc_stats s; + + sof_shell_ipc_stats_get(&s); + shell_print(sh, "Last IPC RX: pri=0x%08x ext=0x%08x @ %llu cycles", + s.last_rx_pri, s.last_rx_ext, (unsigned long long)s.last_rx_time); + shell_print(sh, "Last IPC TX: pri=0x%08x ext=0x%08x @ %llu cycles", + s.last_tx_pri, s.last_tx_ext, (unsigned long long)s.last_tx_time); + return 0; +} + +SHELL_SUBCMD_ADD((sof), ipc_stats, NULL, + "Print IPC RX/TX counters; 'sof ipc stats reset' clears them\n", + cmd_sof_ipc_stats, 1, 1); + +SHELL_SUBCMD_ADD((sof), ipc_last, NULL, + "Print the last received and sent IPC headers\n", + cmd_sof_ipc_last, 0, 0); + +#if CONFIG_SOF_SHELL_SCHED_INFO + +__cold static const char *sched_type_str(int type) +{ + switch (type) { + case SOF_SCHEDULE_EDF: return "edf"; + case SOF_SCHEDULE_LL_TIMER: return "ll_timer"; + case SOF_SCHEDULE_LL_DMA: return "ll_dma"; + case SOF_SCHEDULE_DP: return "dp"; + case SOF_SCHEDULE_TWB: return "twb"; + default: return "?"; + } +} + +__cold static const char *sched_state_str(enum task_state s) +{ + switch (s) { + case SOF_TASK_STATE_INIT: return "init"; + case SOF_TASK_STATE_QUEUED: return "queued"; + case SOF_TASK_STATE_PENDING: return "pending"; + case SOF_TASK_STATE_RUNNING: return "running"; + case SOF_TASK_STATE_PREEMPTED: return "preempt"; + case SOF_TASK_STATE_COMPLETED: return "done"; + case SOF_TASK_STATE_FREE: return "free"; + case SOF_TASK_STATE_CANCEL: return "cancel"; + case SOF_TASK_STATE_RESCHEDULE: return "resched"; + default: return "?"; + } +} + +struct sched_walk_ctx { + uint32_t total_sum; + uint32_t total_cnt; + uint32_t total_max; + int task_count; +}; + +__cold static int sched_walk(const struct shell *sh, bool show_load) +{ + /* + * The snapshot is too large for the 2 KB shell stack, so use a + * file-scope-lifetime buffer. Shell commands run serialized on the + * single shell thread, so there is no reentrancy, and the buffer + * lives in the shell module's (user-accessible) memory. + */ + static struct sof_shell_sched_snapshot snap; + struct sched_walk_ctx ctx = { 0 }; + uint32_t i; + + sof_shell_sched_snapshot_get(&snap); + + if (snap.no_schedulers) { + shell_print(sh, "No schedulers registered"); + return 0; + } + + for (i = 0; i < snap.count; i++) { + const struct sof_shell_sched_task *t = &snap.tasks[i]; + uint32_t avg = t->cycles_cnt ? t->cycles_sum / t->cycles_cnt : 0; + + if (show_load) { + shell_print(sh, + " %-9s core %u prio %3u state %-7s" + " count %u avg %u max %u sum %u cyc", + sched_type_str(t->sch_type), t->core, + t->priority, sched_state_str(t->state), + t->cycles_cnt, avg, t->cycles_max, + t->cycles_sum); + } else { + shell_print(sh, + " %-9s core %u prio %3u state %-7s" + " flags 0x%04x uid 0x%lx data 0x%lx", + sched_type_str(t->sch_type), t->core, + t->priority, sched_state_str(t->state), + t->flags, (unsigned long)t->uid, + (unsigned long)t->data); + } + + ctx.total_sum += t->cycles_sum; + ctx.total_cnt += t->cycles_cnt; + if (t->cycles_max > ctx.total_max) + ctx.total_max = t->cycles_max; + ctx.task_count++; + } + + if (!ctx.task_count) + shell_print(sh, " (no tasks)"); + + if (show_load) { + uint32_t avg = ctx.total_cnt ? ctx.total_sum / ctx.total_cnt : 0; + + shell_print(sh, + "Total: %d tasks count %u avg %u peak max %u cyc", + ctx.task_count, ctx.total_cnt, avg, ctx.total_max); + } + + return 0; +} + +__cold static int cmd_sof_sched_tasks(const struct shell *sh, + size_t argc, char *argv[]) +{ + shell_print(sh, "Active scheduler tasks:"); + return sched_walk(sh, false); +} + +__cold static int cmd_sof_sched_load(const struct shell *sh, + size_t argc, char *argv[]) +{ + shell_print(sh, "Scheduler task cycle counters:"); + return sched_walk(sh, true); +} + +#endif /* CONFIG_SOF_SHELL_SCHED_INFO */ + +#if CONFIG_SOF_SHELL_SCHED_INFO +SHELL_SUBCMD_ADD((sof), sched_tasks, NULL, + "List all scheduler tasks (type, core, prio, state)\n", + cmd_sof_sched_tasks, 0, 0); +SHELL_SUBCMD_ADD((sof), sched_load, NULL, + "Show per-task cycle counters and totals\n", + cmd_sof_sched_load, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_LOG_INFO + +__cold static int cmd_sof_log_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_log_status ls; + uint32_t i; + + sof_shell_log_status_get(&ls); + + shell_print(sh, "Log backends: %u, sources: %u", + ls.backend_count, ls.source_count); + shell_print(sh, " idx id active name"); + + for (i = 0; i < ls.filled; i++) { + const struct sof_shell_log_backend *be = &ls.backends[i]; + + shell_print(sh, " %3u %3u %-3s %s", + i, be->id, + be->active ? "yes" : "no", + be->name); + } + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_LOG_INFO */ + +#if CONFIG_SOF_SHELL_MTRACE_DUMP + +#include + +/* must match the layout used by zephyr/subsys/logging/backends/log_backend_adsp_mtrace.c */ +struct sof_shell_mtrace_slot { + uint32_t host_ptr; + uint32_t dsp_ptr; + uint8_t data[ADSP_DW_SLOT_SIZE - 2 * sizeof(uint32_t)]; +} __packed; + +#define SOF_SHELL_MTRACE_BUF_SIZE (ADSP_DW_SLOT_SIZE - 2 * sizeof(uint32_t)) +#define SOF_SHELL_MTRACE_TYPE(core) \ + (ADSP_DW_SLOT_DEBUG_LOG | ((core) & ADSP_DW_SLOT_CORE_MASK)) + +__cold static int cmd_sof_mtrace_dump(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct sof_shell_mtrace_slot *slot; + uint32_t r, w, len, i; + +#ifdef CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER + struct adsp_dw_desc desc = { .type = SOF_SHELL_MTRACE_TYPE(0) }; + + slot = adsp_dw_request_slot(&desc, NULL); +#else + slot = (struct sof_shell_mtrace_slot *) + ADSP_DW->slots[ADSP_DW_SLOT_NUM_MTRACE]; +#endif + if (!slot) { + shell_print(sh, "mtrace slot not available"); + return -ENODEV; + } + + r = slot->host_ptr; + w = slot->dsp_ptr; + + if (r == w) { + shell_print(sh, "mtrace: empty (host_ptr=dsp_ptr=%u)", r); + return 0; + } + + if (w > r) + len = w - r; + else + len = SOF_SHELL_MTRACE_BUF_SIZE - r + w; + + shell_print(sh, + "mtrace: host_ptr=%u dsp_ptr=%u unread=%u bytes (snapshot)", + r, w, len); + + /* print byte-by-byte without advancing host_ptr; preserves host consumer */ + for (i = 0; i < len; i++) { + uint32_t off = (r + i) % SOF_SHELL_MTRACE_BUF_SIZE; + + shell_fprintf(sh, SHELL_NORMAL, "%c", slot->data[off]); + } + shell_fprintf(sh, SHELL_NORMAL, "\n"); + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_MTRACE_DUMP */ + +#if CONFIG_SOF_SHELL_LOG_INFO +SHELL_SUBCMD_ADD((sof), log_status, NULL, + "List Zephyr log backends with state and source count\n", + cmd_sof_log_status, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_MTRACE_DUMP +SHELL_SUBCMD_ADD((sof), mtrace_dump, NULL, + "Snapshot the mtrace SRAM ring buffer (does not advance host_ptr)\n", + cmd_sof_mtrace_dump, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_MAILBOX_HEX || CONFIG_SOF_SHELL_DBGWIN_DUMP + +__cold static void sof_shell_hex_dump(const struct shell *sh, uintptr_t base, + size_t off, size_t len) +{ + const uint8_t *p = (const uint8_t *)(base + off); + size_t i, j; + + for (i = 0; i < len; i += 16) { + size_t row = MIN((size_t)16, len - i); + char ascii[17]; + + shell_fprintf(sh, SHELL_NORMAL, "%08lx ", + (unsigned long)(off + i)); + for (j = 0; j < 16; j++) { + if (j < row) + shell_fprintf(sh, SHELL_NORMAL, " %02x", p[i + j]); + else + shell_fprintf(sh, SHELL_NORMAL, " "); + ascii[j] = (j < row && p[i + j] >= 0x20 && p[i + j] < 0x7f) ? + (char)p[i + j] : '.'; + } + ascii[16] = '\0'; + shell_fprintf(sh, SHELL_NORMAL, " %s\n", ascii); + } +} + +#endif + +#if CONFIG_SOF_SHELL_MAILBOX_HEX + +#include + +struct sof_shell_mb_region { + const char *name; + uintptr_t base; + size_t size; +}; + +__cold_rodata static const struct sof_shell_mb_region sof_shell_mb_regions[] = { +#ifdef MAILBOX_EXCEPTION_BASE + { "exception", MAILBOX_EXCEPTION_BASE, MAILBOX_EXCEPTION_SIZE }, +#endif + { "dspbox", MAILBOX_DSPBOX_BASE, MAILBOX_DSPBOX_SIZE }, + { "hostbox", MAILBOX_HOSTBOX_BASE, MAILBOX_HOSTBOX_SIZE }, +#ifdef MAILBOX_DEBUG_BASE + { "debug", MAILBOX_DEBUG_BASE, MAILBOX_DEBUG_SIZE }, +#endif +}; + +__cold static int cmd_sof_mailbox_hex(const struct shell *sh, + size_t argc, char *argv[]) +{ + const struct sof_shell_mb_region *r = NULL; + size_t off = 0, len; + char *end = NULL; + int i; + + if (argc < 2) { + shell_print(sh, "Mailbox regions:"); + for (i = 0; i < ARRAY_SIZE(sof_shell_mb_regions); i++) + shell_print(sh, " %-10s base 0x%08lx size %zu", + sof_shell_mb_regions[i].name, + (unsigned long)sof_shell_mb_regions[i].base, + sof_shell_mb_regions[i].size); + shell_print(sh, "Usage: sof mailbox hex [offset] [length]"); + return 0; + } + + for (i = 0; i < ARRAY_SIZE(sof_shell_mb_regions); i++) { + if (!strcmp(argv[1], sof_shell_mb_regions[i].name)) { + r = &sof_shell_mb_regions[i]; + break; + } + } + if (!r) { + shell_print(sh, "Unknown region '%s'", argv[1]); + return -EINVAL; + } + + if (argc > 2) { + off = strtoul(argv[2], &end, 0); + if (end == argv[2] || off >= r->size) { + shell_print(sh, "Bad offset (max 0x%zx)", r->size); + return -EINVAL; + } + } + + len = MIN((size_t)256, r->size - off); + if (argc > 3) { + len = strtoul(argv[3], &end, 0); + if (end == argv[3]) + return -EINVAL; + len = MIN(len, r->size - off); + } + + shell_print(sh, "%s @ 0x%08lx + 0x%zx, %zu bytes:", + r->name, (unsigned long)r->base, off, len); + sof_shell_hex_dump(sh, r->base, off, len); + return 0; +} + +#endif /* CONFIG_SOF_SHELL_MAILBOX_HEX */ + +#if CONFIG_SOF_SHELL_DBGWIN_DUMP + +#include +#include + +/* Mirror struct used by zephyr/soc/intel/intel_adsp/common/debug_window.c. + * We map window 2 directly so we can read descriptors and slot data without + * depending on slot-manager internals. + */ +struct sof_shell_dw { + struct adsp_dw_desc descs[ADSP_DW_DESC_COUNT]; + uint8_t reserved[ADSP_DW_PAGE0_SLOT_OFFSET - + ADSP_DW_DESC_COUNT * sizeof(struct adsp_dw_desc)]; + uint8_t partial_page0[ADSP_DW_SLOT_SIZE - ADSP_DW_PAGE0_SLOT_OFFSET]; + uint8_t slots[ADSP_DW_SLOT_COUNT][ADSP_DW_SLOT_SIZE]; +} __packed; + +#define SOF_SHELL_DW_BASE \ + (DT_REG_ADDR(DT_PHANDLE(DT_NODELABEL(mem_window2), memory)) + WIN2_OFFSET) + +__cold static const char *dw_type_name(uint32_t type) +{ + switch (type & ADSP_DW_SLOT_TYPE_MASK) { + case ADSP_DW_SLOT_UNUSED & ADSP_DW_SLOT_TYPE_MASK: + return type ? "?" : "unused"; + case ADSP_DW_SLOT_CRITICAL_LOG & ADSP_DW_SLOT_TYPE_MASK: + return "critical_log"; + case ADSP_DW_SLOT_DEBUG_LOG & ADSP_DW_SLOT_TYPE_MASK: + return "debug_log"; + case ADSP_DW_SLOT_GDB_STUB & ADSP_DW_SLOT_TYPE_MASK: + return "gdb_stub"; + case ADSP_DW_SLOT_TELEMETRY & ADSP_DW_SLOT_TYPE_MASK: + return "telemetry"; + case ADSP_DW_SLOT_TRACE & ADSP_DW_SLOT_TYPE_MASK: + return "trace"; + case ADSP_DW_SLOT_SHELL & ADSP_DW_SLOT_TYPE_MASK: + return "shell"; + case ADSP_DW_SLOT_DEBUG_STREAM & ADSP_DW_SLOT_TYPE_MASK: + return "debug_stream"; + case ADSP_DW_SLOT_BROKEN & ADSP_DW_SLOT_TYPE_MASK: + return "broken"; + default: + return "?"; + } +} + +__cold static int cmd_sof_dbgwin_dump(const struct shell *sh, + size_t argc, char *argv[]) +{ + volatile struct sof_shell_dw *dw = + (volatile struct sof_shell_dw *) + sys_cache_uncached_ptr_get((__sparse_force void __sparse_cache *) + SOF_SHELL_DW_BASE); + int slot, i; + size_t len = 256; + char *end = NULL; + + if (argc < 2) { + shell_print(sh, + "ADSP debug window @ 0x%08lx (%d slots, %u bytes each)", + (unsigned long)SOF_SHELL_DW_BASE, + ADSP_DW_SLOT_COUNT, ADSP_DW_SLOT_SIZE); + shell_print(sh, " slot res_id type vma name"); + for (i = 0; i < ADSP_DW_SLOT_COUNT; i++) { + shell_print(sh, + " %3d 0x%08x 0x%08x 0x%08x %s (core %u)", + i, dw->descs[i].resource_id, dw->descs[i].type, + dw->descs[i].vma, dw_type_name(dw->descs[i].type), + (unsigned int)(dw->descs[i].type & ADSP_DW_SLOT_CORE_MASK)); + } + shell_print(sh, "Usage: sof dbgwin dump [length]"); + return 0; + } + + slot = strtol(argv[1], &end, 0); + if (end == argv[1] || slot < 0 || slot >= ADSP_DW_SLOT_COUNT) { + shell_print(sh, "Bad slot (0..%d)", ADSP_DW_SLOT_COUNT - 1); + return -EINVAL; + } + + if (argc > 2) { + len = strtoul(argv[2], &end, 0); + if (end == argv[2]) + return -EINVAL; + } + len = MIN(len, (size_t)ADSP_DW_SLOT_SIZE); + + shell_print(sh, "Slot %d type=0x%08x (%s, core %u) vma=0x%08x; %zu bytes:", + slot, dw->descs[slot].type, dw_type_name(dw->descs[slot].type), + (unsigned int)(dw->descs[slot].type & ADSP_DW_SLOT_CORE_MASK), + dw->descs[slot].vma, len); + sof_shell_hex_dump(sh, (uintptr_t)dw->slots[slot], 0, len); + return 0; +} + +#endif /* CONFIG_SOF_SHELL_DBGWIN_DUMP */ + +#if CONFIG_SOF_SHELL_MAILBOX_HEX +SHELL_SUBCMD_ADD((sof), mailbox_hex, NULL, + "Hex-dump a mailbox region: [offset] [length]\n", + cmd_sof_mailbox_hex, 1, 3); +#endif + +#if CONFIG_SOF_SHELL_DBGWIN_DUMP +SHELL_SUBCMD_ADD((sof), dbgwin_dump, NULL, + "List ADSP debug-window slots, or hex-dump one: [slot] [length]\n", + cmd_sof_dbgwin_dump, 1, 2); +#endif + +#if CONFIG_SOF_SHELL_PERF_STATUS + +#include +#include +#include + +__cold static const char *perf_state_str(enum ipc4_perf_measurements_state_set s) +{ + switch (s) { + case IPC4_PERF_MEASUREMENTS_DISABLED: return "disabled"; + case IPC4_PERF_MEASUREMENTS_STOPPED: return "stopped"; + case IPC4_PERF_MEASUREMENTS_STARTED: return "started"; + case IPC4_PERF_MEASUREMENTS_PAUSED: return "paused"; + default: return "?"; + } +} + +__cold static int cmd_sof_perf_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct system_tick_info *systick; + int core_id, ret; + + if (argc > 1) { + if (!strcmp(argv[1], "reset")) { + ret = reset_performance_counters(); + shell_print(sh, "perf: reset_performance_counters() = %d", ret); + return ret; + } + if (!strcmp(argv[1], "start")) { + ret = enable_performance_counters(); + if (!ret) + perf_meas_set_state(IPC4_PERF_MEASUREMENTS_STARTED); + shell_print(sh, "perf: enable_performance_counters() = %d", ret); + return ret; + } + if (!strcmp(argv[1], "stop")) { + perf_meas_set_state(IPC4_PERF_MEASUREMENTS_STOPPED); + shell_print(sh, "perf: stopped"); + return 0; + } + if (!strcmp(argv[1], "pause")) { + perf_meas_set_state(IPC4_PERF_MEASUREMENTS_PAUSED); + shell_print(sh, "perf: paused"); + return 0; + } + shell_print(sh, "Usage: sof perf status [reset|start|stop|pause]"); + return -EINVAL; + } + + shell_print(sh, "Performance measurements: %s", + perf_state_str(perf_meas_get_state())); + +#ifdef CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER + systick = telemetry_get_systick_info_ptr(); + if (!systick) { + shell_print(sh, "telemetry slot not allocated"); + return 0; + } +#else + { + struct telemetry_wnd_data *wnd = + (struct telemetry_wnd_data *) + ADSP_DW->slots[SOF_DW_TELEMETRY_SLOT]; + systick = (struct system_tick_info *)wnd->system_tick_info; + } +#endif + + shell_print(sh, "Per-core systick (count, last_us_cyc, max_us_cyc, avg_kcps, peak_kcps):"); + for (core_id = 0; core_id < CONFIG_MAX_CORE_COUNT; core_id++) { + if (!(cpu_enabled_cores() & BIT(core_id))) + continue; + shell_print(sh, + " core %u: count=%u last=%u max=%u avg_kcps=%u peak_kcps=%u peak4k=%u peak8k=%u", + core_id, + systick[core_id].count, + systick[core_id].last_time_elapsed, + systick[core_id].max_time_elapsed, + systick[core_id].avg_utilization, + systick[core_id].peak_utilization, + systick[core_id].peak_utilization_4k, + systick[core_id].peak_utilization_8k); + } + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_PERF_STATUS */ + +#if CONFIG_SOF_SHELL_PERF_STATUS +SHELL_SUBCMD_ADD((sof), perf_status, NULL, + "Show telemetry perf state and per-core systick;" + " optional [reset|start|stop|pause]\n", + cmd_sof_perf_status, 1, 1); +#endif + +/* + * Space-separated aliases for underscore commands. + * + * Keep legacy underscore names for compatibility while exposing the + * preferred tokenized form, e.g. "sof core on" for "sof core_on". + */ + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test_inject_sched, + SHELL_CMD(gap, NULL, + "Inject a gap to audio scheduling\n", + cmd_sof_test_inject_sched_gap), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test_inject, + SHELL_CMD(sched, &sof_cmd_test_inject_sched, + "Scheduler injection commands\n", NULL), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test, + SHELL_CMD(inject, &sof_cmd_test_inject, + "Injection test commands\n", NULL), + SHELL_SUBCMD_SET_END +); + +#if CONFIG_SOF_SHELL_CORE_STATUS || CONFIG_SOF_SHELL_CORE_POWER +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_core, +#if CONFIG_SOF_SHELL_CORE_STATUS + SHELL_CMD(status, NULL, + "Print enabled/active state of each DSP core\n", + cmd_sof_core_status), +#endif +#if CONFIG_SOF_SHELL_CORE_POWER + SHELL_CMD_ARG(on, NULL, + "Power on a secondary DSP core: \n" + "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", + cmd_sof_core_on, 2, 0), + SHELL_CMD_ARG(off, NULL, + "Power off a secondary DSP core: \n" + "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", + cmd_sof_core_off, 2, 0), +#endif + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_SRAM_STATUS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_sram, + SHELL_CMD(status, NULL, + "Print HPSRAM heap usage statistics\n", + cmd_sof_sram_status), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_CLOCK_STATUS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_clock, + SHELL_CMD(status, NULL, + "Print current clock frequency for each DSP core\n", + cmd_sof_clock_status), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_MMU_DBG +#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mmu, + SHELL_CMD(status, NULL, + "Print Intel ADSP MTL TLB / virtual memory status\n", + cmd_sof_mmu_status), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_tlb, + SHELL_CMD(dump, NULL, + "Dump all active TLB entries (vaddr/paddr/flags)\n", + cmd_sof_tlb_dump), + SHELL_CMD_ARG(lookup, NULL, + "Query TLB for a page or range: [end_vaddr]\n", + cmd_sof_tlb_lookup, 2, 1), + SHELL_SUBCMD_SET_END +); +#endif +#if CONFIG_XTENSA_MMU +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_page, + SHELL_CMD_ARG(info, NULL, + "Probe DTLB for a page or range: [end_vaddr]\n" + "Reports physical address, ring, ASID, R/W/X permissions" + " and cache mode for each page currently in the DTLB.\n", + cmd_sof_page_info, 2, 1), + SHELL_SUBCMD_SET_END +); +#endif +#endif + +#if CONFIG_SOF_SHELL_LLEXT_LOAD || CONFIG_SOF_SHELL_LLEXT_LIST || CONFIG_SOF_SHELL_LLEXT_PURGE || CONFIG_SOF_SHELL_LLEXT_CTOR || CONFIG_SOF_SHELL_LLEXT_CALL +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_llext, +#if CONFIG_SOF_SHELL_LLEXT_LOAD + SHELL_CMD_ARG(load, NULL, + "Load llext module from host: [lib_id=1]\n" + "Sets up the DMA handshake slot then waits for:\n" + " dd if= of=/sys/kernel/debug/sof/llext_load\\\n" + " bs=$(stat -c%s ) count=1\n" + "on the host. Prints result when DMA and IPC4 load complete.\n", + cmd_sof_llext_load, 2, 1), +#endif +#if CONFIG_SOF_SHELL_LLEXT_LIST + SHELL_CMD(list, NULL, + "List llext libraries stored in IMR/DRAM.\n" + "For each library shows base address, storage size and per-module\n" + "SRAM mapping state (yes/no), use count and dependency count.\n", + cmd_sof_llext_list), +#endif +#if CONFIG_SOF_SHELL_LLEXT_PURGE + SHELL_CMD_ARG(purge, NULL, + "Purge llext library from IMR/DRAM: \n" + "Fails with -EBUSY if any module in the library is still\n" + "mapped in SRAM (i.e. a pipeline using it is still active).\n", + cmd_sof_llext_purge, 2, 0), +#endif +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT + SHELL_CMD_ARG(ctor, NULL, + "Call constructors for all modules in a library: \n", + cmd_sof_llext_ctor, 2, 0), + SHELL_CMD_ARG(dtor, NULL, + "Call destructors for all modules in a library: \n", + cmd_sof_llext_dtor, 2, 0), +#endif +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT + SHELL_CMD_ARG(call, NULL, + "Call a named symbol in all modules of a library: \n", + cmd_sof_llext_call, 3, 0), +#endif + SHELL_SUBCMD_SET_END +); +#endif + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_vpage, + SHELL_CMD(status, NULL, + "Print virtual page allocator status\n", + cmd_sof_vpage_info), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_vregion, + SHELL_CMD(status, NULL, + "Print virtual regions status\n", + cmd_sof_vregion_info), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_ipc, + SHELL_CMD_ARG(stats, NULL, + "Print IPC RX/TX counters; 'sof ipc stats reset' clears them\n", + cmd_sof_ipc_stats, 1, 1), + SHELL_CMD(last, NULL, + "Print the last received and sent IPC headers\n", + cmd_sof_ipc_last), + SHELL_SUBCMD_SET_END +); + +#if CONFIG_SOF_SHELL_SCHED_INFO +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_sched, + SHELL_CMD(tasks, NULL, + "List all scheduler tasks (type, core, prio, state)\n", + cmd_sof_sched_tasks), + SHELL_CMD(load, NULL, + "Show per-task cycle counters and totals\n", + cmd_sof_sched_load), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_LOG_INFO +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_log, + SHELL_CMD(status, NULL, + "List Zephyr log backends with state and source count\n", + cmd_sof_log_status), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_MTRACE_DUMP +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mtrace, + SHELL_CMD(dump, NULL, + "Snapshot the mtrace SRAM ring buffer (does not advance host_ptr)\n", + cmd_sof_mtrace_dump), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_MAILBOX_HEX +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mailbox, + SHELL_CMD_ARG(hex, NULL, + "Hex-dump a mailbox region: [offset] [length]\n", + cmd_sof_mailbox_hex, 1, 3), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_DBGWIN_DUMP +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dbgwin, + SHELL_CMD_ARG(dump, NULL, + "List ADSP debug-window slots, or hex-dump one: [slot] [length]\n", + cmd_sof_dbgwin_dump, 1, 2), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_PERF_STATUS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_perf, + SHELL_CMD_ARG(status, NULL, + "Show telemetry perf state and per-core systick;" + " optional [reset|start|stop|pause]\n", + cmd_sof_perf_status, 1, 1), + SHELL_SUBCMD_SET_END +); +#endif + +SHELL_SUBCMD_ADD((sof), test, &sof_cmd_test, + "Test commands\n", NULL, 0, 0); + +#if CONFIG_SOF_SHELL_CORE_STATUS || CONFIG_SOF_SHELL_CORE_POWER +SHELL_SUBCMD_ADD((sof), core, &sof_cmd_core, + "Core commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_SRAM_STATUS +SHELL_SUBCMD_ADD((sof), sram, &sof_cmd_sram, + "SRAM commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_CLOCK_STATUS +SHELL_SUBCMD_ADD((sof), clock, &sof_cmd_clock, + "Clock commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_MMU_DBG +#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +SHELL_SUBCMD_ADD((sof), mmu, &sof_cmd_mmu, + "MMU status commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), tlb, &sof_cmd_tlb, + "TLB commands\n", NULL, 0, 0); +#endif +#if CONFIG_XTENSA_MMU +SHELL_SUBCMD_ADD((sof), page, &sof_cmd_page, + "Page-table commands\n", NULL, 0, 0); +#endif +#endif + +#if CONFIG_SOF_SHELL_LLEXT_LOAD || CONFIG_SOF_SHELL_LLEXT_LIST || CONFIG_SOF_SHELL_LLEXT_PURGE || CONFIG_SOF_SHELL_LLEXT_CTOR || CONFIG_SOF_SHELL_LLEXT_CALL +SHELL_SUBCMD_ADD((sof), llext, &sof_cmd_llext, + "LLEXT commands\n", NULL, 0, 0); +#endif + +SHELL_SUBCMD_ADD((sof), vpage, &sof_cmd_vpage, + "Virtual page commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), vregion, &sof_cmd_vregion, + "Virtual region commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), ipc, &sof_cmd_ipc, + "IPC commands\n", NULL, 0, 0); + +#if CONFIG_SOF_SHELL_SCHED_INFO +SHELL_SUBCMD_ADD((sof), sched, &sof_cmd_sched, + "Scheduler commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_LOG_INFO +SHELL_SUBCMD_ADD((sof), log, &sof_cmd_log, + "Log commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_MTRACE_DUMP +SHELL_SUBCMD_ADD((sof), mtrace, &sof_cmd_mtrace, + "Mtrace commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_MAILBOX_HEX +SHELL_SUBCMD_ADD((sof), mailbox, &sof_cmd_mailbox, + "Mailbox commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_DBGWIN_DUMP +SHELL_SUBCMD_ADD((sof), dbgwin, &sof_cmd_dbgwin, + "Debug-window commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_PERF_STATUS +SHELL_SUBCMD_ADD((sof), perf, &sof_cmd_perf, + "Performance commands\n", NULL, 0, 0); +#endif +SHELL_CMD_REGISTER(sof, &sub_sof, + "SOF application commands", NULL); diff --git a/zephyr/shell/syscall.c b/zephyr/shell/syscall.c new file mode 100644 index 000000000000..f79ea7a4ae73 --- /dev/null +++ b/zephyr/shell/syscall.c @@ -0,0 +1,686 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +/* + * System call implementations for the SOF kernel (RTOS/infrastructure) shell + * commands. The z_impl_* functions run in supervisor context and forward to + * the underlying privileged accessors. The z_vrfy_* handlers validate + * user-supplied pointers before dispatching when the shell thread runs in + * user mode (CONFIG_USERSPACE). + */ + +#include +#include +#include +#include +#include +#include +#include + +#if CONFIG_SOF_SHELL_SCHED_INFO +#include +#include +#include +#endif + +#if CONFIG_SOF_SHELL_LOG_INFO +#include +#include +#include +#include +#endif + +#if (CONFIG_SOF_SHELL_LLEXT_LIST || CONFIG_SOF_SHELL_LLEXT_PURGE || \ + CONFIG_SOF_SHELL_LLEXT_CTOR || CONFIG_SOF_SHELL_LLEXT_CALL) && \ + CONFIG_LIBRARY_MANAGER +#include +#include +#include +#ifndef _SHELL_MOD_PAGE_SZ +#ifdef CONFIG_MM_DRV_PAGE_SIZE +#define _SHELL_MOD_PAGE_SZ CONFIG_MM_DRV_PAGE_SIZE +#else +#define _SHELL_MOD_PAGE_SZ 4096 +#endif +#endif +#endif + +#if (CONFIG_SOF_SHELL_LLEXT_CTOR || CONFIG_SOF_SHELL_LLEXT_CALL) && \ + CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +#include +#include +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +#include +#endif + +#if CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +#include +#include +#include +#include + +/* + * Lightweight wrappers around the Intel ADSP MTL TLB MMIO table. Mirrors + * mm_drv_intel_adsp.h without pulling in the driver-internal header. These + * accesses are privileged (MMIO and the memory-management driver) and so live + * here behind system calls; the shell only consumes the decoded snapshot. + */ +#define _SHELL_TLB_NODE DT_NODELABEL(tlb) +#define _SHELL_TLB_BASE ((volatile uint16_t *)(uintptr_t)DT_REG_ADDR(_SHELL_TLB_NODE)) +#define _SHELL_PADDR_SIZE DT_PROP(_SHELL_TLB_NODE, paddr_size) +#define _SHELL_TLB_ENTRY_NUM BIT(_SHELL_PADDR_SIZE) +#define _SHELL_PADDR_MASK (_SHELL_TLB_ENTRY_NUM - 1) +#define _SHELL_ENABLE_BIT ((uint16_t)BIT(_SHELL_PADDR_SIZE)) + +/* + * Base physical address for the HPSRAM region (mirrors TLB_PHYS_BASE in the + * driver). + */ +#define _SHELL_PHYS_BASE \ + (((CONFIG_KERNEL_VM_BASE / CONFIG_MM_DRV_PAGE_SIZE) & ~_SHELL_PADDR_MASK) * \ + CONFIG_MM_DRV_PAGE_SIZE) +#endif /* CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB */ + +void z_impl_sof_shell_ipc_stats_get(struct ipc_stats *out) +{ + ipc_stats_get(out); +} + +void z_impl_sof_shell_ipc_stats_reset(void) +{ + ipc_stats_reset(); +} + +void z_impl_sof_shell_core_status_get(struct sof_shell_core_status *out) +{ + unsigned int i; + + out->core_count = CONFIG_CORE_COUNT; + out->current = cpu_get_id(); + for (i = 0; i < CONFIG_CORE_COUNT; i++) + out->enabled[i] = cpu_is_core_enabled(i) ? 1 : 0; +} + +#if CONFIG_SOF_SHELL_CLOCK_STATUS +void z_impl_sof_shell_clock_status_get(struct sof_shell_clock_status *out) +{ + struct clock_info *clocks = clocks_get(); + unsigned int i; + + if (!clocks) { + out->valid = 0; + out->num_clocks = 0; + return; + } + + out->valid = 1; + out->num_clocks = NUM_CLOCKS; + for (i = 0; i < NUM_CLOCKS && i < CONFIG_CORE_COUNT; i++) + out->freq_hz[i] = clocks[i].freqs[clocks[i].current_freq_idx].freq; +} +#endif + +#if CONFIG_SOF_SHELL_SCHED_INFO +static void sof_shell_sched_cb(struct task *task, void *_ctx) +{ + struct sof_shell_sched_snapshot *out = _ctx; + struct sof_shell_sched_task *e; + + if (out->count >= SOF_SHELL_SCHED_MAX_TASKS) + return; + + e = &out->tasks[out->count++]; + e->sch_type = task->sch ? task->sch->type : 0; + e->core = task->core; + e->priority = task->priority; + e->state = task->state; + e->flags = task->flags; + e->cycles_cnt = task->cycles_cnt; + e->cycles_sum = task->cycles_sum; + e->cycles_max = task->cycles_max; + e->uid = (uintptr_t)task->uid; + e->data = (uintptr_t)task->data; +} + +void z_impl_sof_shell_sched_snapshot_get(struct sof_shell_sched_snapshot *out) +{ + struct schedulers *schedulers = *arch_schedulers_get(); + struct schedule_data *sch; + struct list_item *slist; + + out->count = 0; + out->no_schedulers = 0; + + if (!schedulers) { + out->no_schedulers = 1; + return; + } + + list_for_item(slist, &schedulers->list) { + sch = container_of(slist, struct schedule_data, list); + if (!sch->ops->scheduler_dump_tasks) + continue; + sch->ops->scheduler_dump_tasks(sch->data, sof_shell_sched_cb, out); + } +} +#endif /* CONFIG_SOF_SHELL_SCHED_INFO */ + +#if CONFIG_SOF_SHELL_LOG_INFO +void z_impl_sof_shell_log_status_get(struct sof_shell_log_status *out) +{ + int n = log_backend_count_get(); + int i; + + out->backend_count = n; + out->source_count = log_src_cnt_get(Z_LOG_LOCAL_DOMAIN_ID); + out->filled = 0; + + for (i = 0; i < n && out->filled < SOF_SHELL_LOG_MAX_BACKENDS; i++) { + const struct log_backend *be = log_backend_get(i); + struct sof_shell_log_backend *e; + const char *name; + + if (!be) + continue; + + e = &out->backends[out->filled++]; + e->id = log_backend_id_get(be); + e->active = log_backend_is_active(be) ? 1 : 0; + name = be->name ? be->name : "?"; + strncpy(e->name, name, SOF_SHELL_LOG_NAME_MAX - 1); + e->name[SOF_SHELL_LOG_NAME_MAX - 1] = '\0'; + } +} +#endif /* CONFIG_SOF_SHELL_LOG_INFO */ + +#if CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +void z_impl_sof_shell_tlb_meta_get(struct sof_shell_tlb_meta *out) +{ + volatile uint16_t *tlb = _SHELL_TLB_BASE; + uint32_t total = _SHELL_TLB_ENTRY_NUM; + const struct sys_mm_drv_region *regions, *r; + uint32_t enabled = 0; + uint32_t i; + + for (i = 0; i < total; i++) { + if (tlb[i] & _SHELL_ENABLE_BIT) + enabled++; + } + + out->vm_base = CONFIG_KERNEL_VM_BASE; + out->page_size = CONFIG_MM_DRV_PAGE_SIZE; + out->total_entries = total; + out->enabled_entries = enabled; + out->tlb_mmio_base = (uint32_t)(uintptr_t)_SHELL_TLB_BASE; + out->phys_base = (uint32_t)_SHELL_PHYS_BASE; + out->paddr_size = _SHELL_PADDR_SIZE; + out->exec_bit_idx = DT_PROP(_SHELL_TLB_NODE, exec_bit_idx); + out->write_bit_idx = DT_PROP(_SHELL_TLB_NODE, write_bit_idx); + out->region_count = 0; + out->regions_truncated = 0; + + regions = sys_mm_drv_query_memory_regions(); + if (regions) { + SYS_MM_DRV_MEMORY_REGION_FOREACH(regions, r) { + struct sof_shell_mm_region *e; + + if (out->region_count >= SOF_SHELL_TLB_MAX_REGIONS) { + out->regions_truncated = 1; + break; + } + e = &out->regions[out->region_count++]; + e->addr = (uint32_t)(uintptr_t)r->addr; + e->size = (uint32_t)r->size; + e->attr = (uint32_t)r->attr; + } + sys_mm_drv_query_memory_regions_free(regions); + } +} + +uint32_t z_impl_sof_shell_tlb_entries_get(uint32_t start, uint32_t count, + uint16_t *out) +{ + volatile uint16_t *tlb = _SHELL_TLB_BASE; + uint32_t total = _SHELL_TLB_ENTRY_NUM; + uint32_t i; + + if (start >= total) + return 0; + if (count > total - start) + count = total - start; + + for (i = 0; i < count; i++) + out[i] = tlb[start + i]; + + return count; +} +#endif /* CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB */ + +#if CONFIG_SOF_SHELL_CORE_POWER +int z_impl_sof_shell_core_is_enabled(uint32_t id) +{ + return cpu_is_core_enabled((int)id) ? 1 : 0; +} + +int z_impl_sof_shell_core_enable(uint32_t id) +{ + return cpu_enable_core((int)id); +} + +void z_impl_sof_shell_core_disable(uint32_t id) +{ + cpu_disable_core((int)id); +} +#endif /* CONFIG_SOF_SHELL_CORE_POWER */ + +void z_impl_sof_shell_inject_sched_gap(uint32_t block_time_us) +{ + struct ll_schedule_domain *domain = sof_get()->platform_timer_domain; + + domain_block(domain); + k_busy_wait(block_time_us); + domain_unblock(domain); +} + +#if CONFIG_SOF_SHELL_LLEXT_LIST && CONFIG_LIBRARY_MANAGER +void z_impl_sof_shell_llext_list_get(struct sof_shell_llext_list *out) +{ + struct ext_library *ext_lib = ext_lib_get(); + int lib_id; + + out->enabled = 1; + out->count = 0; + + for (lib_id = 1; lib_id < LIB_MANAGER_MAX_LIBS && + out->count < SOF_SHELL_LLEXT_MAX_LIBS; lib_id++) { + const struct lib_manager_mod_ctx *ctx = ext_lib->desc[lib_id]; + const struct sof_man_fw_desc *desc; + struct sof_shell_llext_lib *lib; + + if (!ctx || !ctx->base_addr) + continue; + + desc = (const struct sof_man_fw_desc *) + ((const uint8_t *)ctx->base_addr + SOF_MAN_ELF_TEXT_OFFSET); + + lib = &out->libs[out->count++]; + lib->lib_id = lib_id; + lib->base_addr = (uint32_t)(uintptr_t)ctx->base_addr; + lib->store_bytes = desc->header.preload_page_count * + (uint32_t)_SHELL_MOD_PAGE_SZ; + lib->manifest_mods = desc->header.num_module_entries; + lib->elf_files = ctx->n_mod; + lib->mod_count = 0; + +#if CONFIG_LLEXT + if (ctx->mod) { + unsigned int i; + + for (i = 0; i < ctx->n_mod && + lib->mod_count < SOF_SHELL_LLEXT_MAX_MODS; i++) { + const struct lib_manager_module *m = ctx->mod + i; + struct sof_shell_llext_mod *me = + &lib->mods[lib->mod_count++]; + const uint8_t *nm; + + me->mapped = m->mapped ? 1 : 0; + me->use = m->llext ? (int)m->llext->use_count : 0; + me->dep = m->n_dependent; + + if (m->mod_manifest) { + nm = m->mod_manifest->module.name; + } else { + const struct sof_man_module *mm = + (const struct sof_man_module *) + ((const uint8_t *)desc + + SOF_MAN_MODULE_OFFSET(m->start_idx)); + nm = mm->name; + } + memcpy(me->name, nm, SOF_MAN_MOD_NAME_LEN); + me->name[SOF_MAN_MOD_NAME_LEN] = '\0'; + } + } +#endif /* CONFIG_LLEXT */ + } +} +#endif /* CONFIG_SOF_SHELL_LLEXT_LIST && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_PURGE && CONFIG_LIBRARY_MANAGER +int z_impl_sof_shell_llext_purge(uint32_t lib_id) +{ + return lib_manager_purge_library(lib_id); +} +#endif /* CONFIG_SOF_SHELL_LLEXT_PURGE && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +int z_impl_sof_shell_llext_ctor_dtor(uint32_t lib_id, uint32_t is_ctor, + struct sof_shell_llext_op_result *out) +{ + struct ext_library *ext_lib = ext_lib_get(); + struct lib_manager_mod_ctx *ctx = ext_lib->desc[lib_id]; + unsigned int i; + int ret = 0; + + out->status = 0; + out->count = 0; + + if (!ctx || !ctx->base_addr || !ctx->mod) { + out->status = -ENOENT; + return -ENOENT; + } + + for (i = 0; i < ctx->n_mod && out->count < SOF_SHELL_LLEXT_MAX_MODS; i++) { + struct lib_manager_module *m = &ctx->mod[i]; + uint32_t module_id = (lib_id << LIB_MANAGER_LIB_ID_SHIFT) | m->start_idx; + struct sof_shell_llext_op_mod *me = &out->mods[out->count++]; + + me->flag = 0; + me->addr = 0; + me->name[0] = '\0'; + + if (is_ctor) { + struct comp_ipc_config fake_ipc = { .id = module_id }; + uintptr_t entry = llext_manager_allocate_module(&fake_ipc, NULL); + + if (!entry) { + me->ret = -ENOMEM; + out->status = -ENOMEM; + return -ENOMEM; + } + + if (!m->llext) { + me->ret = -ENODEV; + out->status = -ENODEV; + return -ENODEV; + } + + if (m->llext->name[0]) { + strncpy(me->name, m->llext->name, + SOF_SHELL_LLEXT_NAME_MAX - 1); + me->name[SOF_SHELL_LLEXT_NAME_MAX - 1] = '\0'; + } + + ret = llext_bringup(m->llext); + } else { + if (!m->llext) { + me->ret = -ENODEV; + out->status = -ENODEV; + return -ENODEV; + } + + if (m->llext->name[0]) { + strncpy(me->name, m->llext->name, + SOF_SHELL_LLEXT_NAME_MAX - 1); + me->name[SOF_SHELL_LLEXT_NAME_MAX - 1] = '\0'; + } + + ret = llext_teardown(m->llext); + if (ret < 0) { + me->ret = ret; + out->status = ret; + return ret; + } + + ret = llext_manager_free_module(module_id); + } + + me->ret = ret; + if (ret < 0) { + out->status = ret; + return ret; + } + } + + return ret; +} +#endif /* CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +int z_impl_sof_shell_llext_call(uint32_t lib_id, const char *sym_name, + struct sof_shell_llext_op_result *out) +{ + struct ext_library *ext_lib = ext_lib_get(); + struct lib_manager_mod_ctx *ctx = ext_lib->desc[lib_id]; + unsigned int i; + int found = 0; + + out->status = 0; + out->count = 0; + + if (!ctx || !ctx->base_addr || !ctx->mod) { + out->status = -ENOENT; + return -ENOENT; + } + + for (i = 0; i < ctx->n_mod && out->count < SOF_SHELL_LLEXT_MAX_MODS; i++) { + struct lib_manager_module *m = &ctx->mod[i]; + struct sof_shell_llext_op_mod *me = &out->mods[out->count++]; + const void *fn_addr = NULL; + + me->ret = 0; + me->flag = 0; + me->addr = 0; + me->name[0] = '\0'; + + if (!m->llext) { + me->ret = -ENODEV; + continue; + } + + if (m->llext->name[0]) { + strncpy(me->name, m->llext->name, SOF_SHELL_LLEXT_NAME_MAX - 1); + me->name[SOF_SHELL_LLEXT_NAME_MAX - 1] = '\0'; + } + + fn_addr = llext_find_sym(&m->llext->sym_tab, sym_name); + if (!fn_addr) + fn_addr = llext_find_sym(&m->llext->exp_tab, sym_name); + + /* + * Fallback to the raw ELF symbol table in the persistent DRAM + * buffer (covers cases where sym_tab was freed or exp_tab + * metadata is insufficient). + */ + if (!fn_addr && m->ebl) { + const struct llext_loader *raw_ldr = &m->ebl->loader; + const elf_shdr_t *symtab_hdr = &raw_ldr->sects[LLEXT_MEM_SYMTAB]; + const elf_shdr_t *strtab_hdr = &raw_ldr->sects[LLEXT_MEM_STRTAB]; + + if (symtab_hdr->sh_size && strtab_hdr->sh_size) { + const elf_sym_t *syms = (const elf_sym_t *) + (m->ebl->buf + symtab_hdr->sh_offset); + const char *strs = (const char *) + (m->ebl->buf + strtab_hdr->sh_offset); + uint32_t sym_cnt = symtab_hdr->sh_size / sizeof(elf_sym_t); + uint32_t k; + + for (k = 0; k < sym_cnt; k++) { + uint16_t shndx; + const elf_shdr_t *sect; + + if (ELF_ST_TYPE(syms[k].st_info) != STT_FUNC) + continue; + if (strcmp(strs + syms[k].st_name, sym_name) != 0) + continue; + + shndx = syms[k].st_shndx; + if (shndx >= m->llext->sect_cnt) + break; + + sect = m->llext->sect_hdrs + shndx; + fn_addr = (const void *)(m->ebl->buf + + sect->sh_offset + syms[k].st_value); + break; + } + } + } + + if (!fn_addr) { + me->ret = -ENOENT; + continue; + } + + me->flag = 1; + me->addr = (uintptr_t)fn_addr; + + ((void (*)(void))fn_addr)(); + + found++; + } + + out->status = found ? 0 : -ENOENT; + return out->status; +} +#endif /* CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER */ + +#ifdef CONFIG_USERSPACE +#include + +void z_vrfy_sof_shell_ipc_stats_get(struct ipc_stats *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_ipc_stats_get(out); +} +#include + +void z_vrfy_sof_shell_ipc_stats_reset(void) +{ + z_impl_sof_shell_ipc_stats_reset(); +} +#include + +void z_vrfy_sof_shell_core_status_get(struct sof_shell_core_status *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_core_status_get(out); +} +#include + +#if CONFIG_SOF_SHELL_CLOCK_STATUS +void z_vrfy_sof_shell_clock_status_get(struct sof_shell_clock_status *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_clock_status_get(out); +} +#include +#endif + +#if CONFIG_SOF_SHELL_SCHED_INFO +void z_vrfy_sof_shell_sched_snapshot_get(struct sof_shell_sched_snapshot *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_sched_snapshot_get(out); +} +#include +#endif /* CONFIG_SOF_SHELL_SCHED_INFO */ + +#if CONFIG_SOF_SHELL_LOG_INFO +void z_vrfy_sof_shell_log_status_get(struct sof_shell_log_status *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_log_status_get(out); +} +#include +#endif /* CONFIG_SOF_SHELL_LOG_INFO */ + +#if CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +void z_vrfy_sof_shell_tlb_meta_get(struct sof_shell_tlb_meta *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_tlb_meta_get(out); +} +#include + +uint32_t z_vrfy_sof_shell_tlb_entries_get(uint32_t start, uint32_t count, + uint16_t *out) +{ + K_OOPS(K_SYSCALL_MEMORY_ARRAY_WRITE(out, count, sizeof(uint16_t))); + return z_impl_sof_shell_tlb_entries_get(start, count, out); +} +#include +#endif /* CONFIG_SOF_SHELL_MMU_DBG && CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB */ + +#if CONFIG_SOF_SHELL_CORE_POWER +int z_vrfy_sof_shell_core_is_enabled(uint32_t id) +{ + K_OOPS(K_SYSCALL_VERIFY_MSG(id < CONFIG_CORE_COUNT, "invalid core id")); + return z_impl_sof_shell_core_is_enabled(id); +} +#include + +int z_vrfy_sof_shell_core_enable(uint32_t id) +{ + K_OOPS(K_SYSCALL_VERIFY_MSG(id >= 1 && id < CONFIG_CORE_COUNT, + "invalid core id")); + return z_impl_sof_shell_core_enable(id); +} +#include + +void z_vrfy_sof_shell_core_disable(uint32_t id) +{ + K_OOPS(K_SYSCALL_VERIFY_MSG(id >= 1 && id < CONFIG_CORE_COUNT, + "invalid core id")); + z_impl_sof_shell_core_disable(id); +} +#include +#endif /* CONFIG_SOF_SHELL_CORE_POWER */ + +void z_vrfy_sof_shell_inject_sched_gap(uint32_t block_time_us) +{ + z_impl_sof_shell_inject_sched_gap(block_time_us); +} +#include + +#if CONFIG_SOF_SHELL_LLEXT_LIST && CONFIG_LIBRARY_MANAGER +void z_vrfy_sof_shell_llext_list_get(struct sof_shell_llext_list *out) +{ + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + z_impl_sof_shell_llext_list_get(out); +} +#include +#endif /* CONFIG_SOF_SHELL_LLEXT_LIST && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_PURGE && CONFIG_LIBRARY_MANAGER +int z_vrfy_sof_shell_llext_purge(uint32_t lib_id) +{ + K_OOPS(K_SYSCALL_VERIFY_MSG(lib_id >= 1 && lib_id < LIB_MANAGER_MAX_LIBS, + "invalid lib_id")); + return z_impl_sof_shell_llext_purge(lib_id); +} +#include +#endif /* CONFIG_SOF_SHELL_LLEXT_PURGE && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +int z_vrfy_sof_shell_llext_ctor_dtor(uint32_t lib_id, uint32_t is_ctor, + struct sof_shell_llext_op_result *out) +{ + K_OOPS(K_SYSCALL_VERIFY_MSG(lib_id >= 1 && lib_id < LIB_MANAGER_MAX_LIBS, + "invalid lib_id")); + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + return z_impl_sof_shell_llext_ctor_dtor(lib_id, is_ctor, out); +} +#include +#endif /* CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER */ + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER +int z_vrfy_sof_shell_llext_call(uint32_t lib_id, const char *sym_name, + struct sof_shell_llext_op_result *out) +{ + char kname[SOF_SHELL_LLEXT_SYM_MAX]; + int len; + + K_OOPS(K_SYSCALL_VERIFY_MSG(lib_id >= 1 && lib_id < LIB_MANAGER_MAX_LIBS, + "invalid lib_id")); + K_OOPS(K_SYSCALL_MEMORY_WRITE(out, sizeof(*out))); + + len = k_usermode_string_copy(kname, (char *)sym_name, sizeof(kname)); + K_OOPS(K_SYSCALL_VERIFY_MSG(len == 0, "symbol name too long or invalid")); + + return z_impl_sof_shell_llext_call(lib_id, kname, out); +} +#include +#endif /* CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT && CONFIG_LIBRARY_MANAGER */ +#endif /* CONFIG_USERSPACE */ diff --git a/zephyr/shell/user.c b/zephyr/shell/user.c new file mode 100644 index 000000000000..d97926edb464 --- /dev/null +++ b/zephyr/shell/user.c @@ -0,0 +1,1409 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2024 Intel Corporation. + * + * Author: Kai Vehmanen + */ + +/* + * SOF shell - audio domain commands. + * + * This file hosts the user/audio facing shell commands (pipelines, modules, + * buffers, DAI/DMA, kcontrols). They attach to the root "sof" command that is + * created in shell/kernel.c via the shell iterable-section subcommand + * mechanism. + */ + +#include /* sof_get() */ +#include +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_SOF_SHELL_MODULE_LIST +#include +#include +#if CONFIG_LIBRARY_MANAGER +#include +#endif +#endif /* CONFIG_SOF_SHELL_MODULE_LIST */ +#if CONFIG_SOF_SHELL_PIPELINE_OPS +#include +#include +#include +#include +#endif /* CONFIG_SOF_SHELL_PIPELINE_OPS */ + +#include +#include +#include + +#include + +#if CONFIG_SOF_SHELL_HEAP_USAGE +__cold static int cmd_sof_module_heap_usage(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist, *_clist; + struct ipc_comp_dev *icd; + int count = 0; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + list_for_item_safe(clist, _clist, &ipc->comp_list) { + size_t usage, hwm; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_COMPONENT) + continue; + + usage = module_adapter_heap_usage(comp_mod(icd->cd), &hwm); + shell_print(sh, "comp id 0x%08x%9zu usage%9zu hwm\tbytes", + icd->id, usage, hwm); + count++; + } + + if (!count) + shell_print(sh, "No components found. Start an audio stream first."); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), module_heap_usage, NULL, + "Print heap memory usage of each module\n", + cmd_sof_module_heap_usage, 0, 0); +#endif /* CONFIG_SOF_SHELL_HEAP_USAGE */ + +#if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_MODULE_STATUS + +__cold_rodata static const char * const comp_state_names[] = { + [COMP_STATE_NOT_EXIST] = "not_exist", + [COMP_STATE_INIT] = "init", + [COMP_STATE_READY] = "ready", + [COMP_STATE_SUSPEND] = "suspend", + [COMP_STATE_PREPARE] = "prepare", + [COMP_STATE_PAUSED] = "paused", + [COMP_STATE_ACTIVE] = "active", + [COMP_STATE_PRE_ACTIVE] = "pre_active", +}; + +__cold static const char *comp_state_str(uint16_t state) +{ + if (state < ARRAY_SIZE(comp_state_names) && comp_state_names[state]) + return comp_state_names[state]; + return "unknown"; +} + +#endif /* CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_MODULE_STATUS */ + +#if CONFIG_SOF_SHELL_PIPELINE_STATUS +__cold static int cmd_sof_pipeline_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "%-8s %-5s %-8s %-10s %-10s %s", + "ppl_id", "core", "priority", "period_us", "status", "state"); + + list_for_item(clist, &ipc->comp_list) { + struct pipeline *p; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_PIPELINE) + continue; + + p = icd->pipeline; + shell_print(sh, "%-8u %-5u %-8u %-10u %-10u %s", + p->pipeline_id, p->core, p->priority, + p->period, p->status, + comp_state_str((uint16_t)p->status)); + count++; + } + + if (!count) + shell_print(sh, "No pipelines found."); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), pipeline_status, NULL, + "Print status of all active pipelines\n", + cmd_sof_pipeline_status, 0, 0); +#endif /* CONFIG_SOF_SHELL_PIPELINE_STATUS */ + +#if CONFIG_SOF_SHELL_MODULE_STATUS +__cold static int cmd_sof_module_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "%-12s %-8s %-5s %s", + "comp_id", "ppl_id", "core", "state"); + + list_for_item(clist, &ipc->comp_list) { + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_COMPONENT) + continue; + + shell_print(sh, "0x%-10x %-8u %-5u %s", + icd->id, + icd->cd->pipeline ? icd->cd->pipeline->pipeline_id : 0, + icd->core, + comp_state_str(icd->cd->state)); + count++; + } + + if (!count) + shell_print(sh, "No components found. Start an audio stream first."); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), module_status, NULL, + "Print status of all active components\n", + cmd_sof_module_status, 0, 0); +#endif /* CONFIG_SOF_SHELL_MODULE_STATUS */ + +#if CONFIG_SOF_SHELL_MODULE_LIST + +/* Page size in DSP manifest entries (instance_bss_size, segment lengths) */ +#ifdef CONFIG_MM_DRV_PAGE_SIZE +#define _SHELL_MOD_PAGE_SZ CONFIG_MM_DRV_PAGE_SIZE +#else +#define _SHELL_MOD_PAGE_SZ 4096 +#endif + +#if CONFIG_IPC4_BASE_FW_INTEL +__cold static void print_manifest_modules(const struct shell *sh, + const struct sof_man_fw_desc *desc, + int lib_id, bool verbose) +{ + const struct sof_man_mod_config *cfg_base; + int i; + + if (!desc) + return; + + cfg_base = (const struct sof_man_mod_config *) + ((const uint8_t *)desc + + SOF_MAN_MODULE_OFFSET(desc->header.num_module_entries)); + + for (i = 0; i < (int)desc->header.num_module_entries; i++) { + const struct sof_man_module *mod; + const struct sof_man_mod_config *cfg = NULL; + uint32_t text_sz, bss_sz; + char name[SOF_MAN_MOD_NAME_LEN + 1]; + + mod = (const struct sof_man_module *) + ((const uint8_t *)desc + SOF_MAN_MODULE_OFFSET(i)); + + /* name is not null-terminated in the manifest */ + memcpy(name, mod->name, SOF_MAN_MOD_NAME_LEN); + name[SOF_MAN_MOD_NAME_LEN] = '\0'; + + if (mod->cfg_count > 0) + cfg = cfg_base + mod->cfg_offset; + + text_sz = (uint32_t)mod->segment[0].flags.r.length * _SHELL_MOD_PAGE_SZ; + bss_sz = (uint32_t)mod->instance_bss_size * _SHELL_MOD_PAGE_SZ; + + if (!verbose) { + shell_print(sh, + "[%d:%d] %-8s inst:%-3u cpc:%-8u text:%-7u bss:%u", + lib_id, i, name, + mod->instance_max_count, + cfg ? cfg->cpc : 0U, + text_sz, bss_sz); + continue; + } + + shell_print(sh, + "[%d:%d] %-8s" + " uuid:%08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x", + lib_id, i, name, + mod->uuid.a, mod->uuid.b, mod->uuid.c, + mod->uuid.d[0], mod->uuid.d[1], + mod->uuid.d[2], mod->uuid.d[3], + mod->uuid.d[4], mod->uuid.d[5], + mod->uuid.d[6], mod->uuid.d[7]); + shell_print(sh, + " inst_max:%-3u bss/inst:%6u B text:%6u B" + " affinity:0x%02x", + mod->instance_max_count, bss_sz, text_sz, + mod->affinity_mask); + if (cfg) + shell_print(sh, + " cpc:%-8u cps:%-9u ibs:%-6u obs:%u", + cfg->cpc, cfg->cps, cfg->ibs, cfg->obs); + else + shell_print(sh, " cpc:N/A"); + } +} +#endif /* CONFIG_IPC4_BASE_FW_INTEL */ + +__cold static int cmd_sof_module_list(const struct shell *sh, + size_t argc, char *argv[]) +{ +#if CONFIG_IPC4_BASE_FW_INTEL + const struct sof_man_fw_desc *desc; + bool verbose = (argc >= 2 && strcmp(argv[1], "-v") == 0); + int total = 0; + + shell_print(sh, "Built-in modules:"); + desc = basefw_vendor_get_manifest(); + if (desc) { + print_manifest_modules(sh, desc, 0, verbose); + total += (int)desc->header.num_module_entries; + } else { + shell_print(sh, " (manifest not available)"); + } + +#if CONFIG_LIBRARY_MANAGER + { + int lib_id; + + for (lib_id = 1; lib_id < LIB_MANAGER_MAX_LIBS; lib_id++) { + desc = lib_manager_get_library_manifest( + LIB_MANAGER_PACK_LIB_ID(lib_id)); + if (!desc) + continue; + shell_print(sh, "Library %d modules:", lib_id); + print_manifest_modules(sh, desc, lib_id, verbose); + total += (int)desc->header.num_module_entries; + } + } +#endif /* CONFIG_LIBRARY_MANAGER */ + + if (!total) + shell_print(sh, "No modules found."); + +#else /* !CONFIG_IPC4_BASE_FW_INTEL */ + /* Generic fallback: list registered component drivers */ + struct comp_driver_list *drivers = comp_drivers_get(); + struct list_item *clist; + struct comp_driver_info *info; + int count = 0; + + shell_print(sh, "%-5s %-24s %s", "type", "name", "uuid"); + + list_for_item(clist, &drivers->list) { + const struct sof_uuid *uid; + const char *name; + + info = container_of(clist, struct comp_driver_info, list); + uid = info->drv->uid; + name = (info->drv->tctx && info->drv->tctx->uuid_p) + ? info->drv->tctx->uuid_p->name : "?"; + + shell_print(sh, + "%-5u %-24s" + " %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x", + info->drv->type, name, + uid->a, uid->b, uid->c, + uid->d[0], uid->d[1], uid->d[2], uid->d[3], + uid->d[4], uid->d[5], uid->d[6], uid->d[7]); + count++; + } + + if (!count) + shell_print(sh, "No drivers registered."); +#endif /* CONFIG_IPC4_BASE_FW_INTEL */ + + return 0; +} + +SHELL_SUBCMD_ADD((sof), module_list, NULL, + "List all available modules (name, inst, cpc, text, bss)\n" + " [-v] also show uuid, affinity, cps, ibs, obs\n", + cmd_sof_module_list, 1, 1); +#endif /* CONFIG_SOF_SHELL_MODULE_LIST */ + +#if CONFIG_SOF_SHELL_PIPELINE_OPS + +__cold static int parse_long(const struct shell *sh, const char *s, long *out, + long min_val, long max_val) +{ + char *endptr; + long v = strtol(s, &endptr, 0); + + if (endptr == s || v < min_val || v > max_val) { + shell_print(sh, "error: invalid value '%s' (allowed %ld..%ld)", + s, min_val, max_val); + return -EINVAL; + } + *out = v; + return 0; +} + +/* + * Resolve a module argument that may be either: + * - a numeric module_id (e.g. "2", "0x02") + * - a module name string (e.g. "COPIER", "copier") — IPC4/Intel only + * + * Returns 0 on success, -EINVAL on failure. + */ +__cold static int parse_module_id(const struct shell *sh, const char *s, + long *module_id) +{ + char *endptr; + long v = strtol(s, &endptr, 0); + + /* Numeric: accepted if the whole string was consumed */ + if (endptr != s && *endptr == '\0') { + if (v < 0 || v > 0xFFFF) { + shell_print(sh, "error: module id 0x%lx out of range", v); + return -EINVAL; + } + *module_id = v; + return 0; + } + +#if CONFIG_IPC4_BASE_FW_INTEL + /* Name lookup: search built-in manifest then loaded libraries */ + { + char upper[SOF_MAN_MOD_NAME_LEN + 1]; + const struct sof_man_fw_desc *desc; + uint32_t i; + int k; + + /* Upper-case the input for case-insensitive compare */ + for (k = 0; k < SOF_MAN_MOD_NAME_LEN && s[k]; k++) + upper[k] = (char)toupper((unsigned char)s[k]); + upper[k] = '\0'; + + desc = basefw_vendor_get_manifest(); + if (desc) { + for (i = 0; i < desc->header.num_module_entries; i++) { + const struct sof_man_module *mod = + (const struct sof_man_module *) + ((const uint8_t *)desc + + SOF_MAN_MODULE_OFFSET(i)); + char mname[SOF_MAN_MOD_NAME_LEN + 1]; + int j; + + for (j = 0; j < SOF_MAN_MOD_NAME_LEN; j++) + mname[j] = (char)toupper( + (unsigned char)mod->name[j]); + mname[SOF_MAN_MOD_NAME_LEN] = '\0'; + + if (!strncmp(upper, mname, + SOF_MAN_MOD_NAME_LEN)) { + *module_id = (long)mod->module_id; + return 0; + } + } + } + +#if CONFIG_LIBRARY_MANAGER + { + int lib_id; + + for (lib_id = 1; lib_id < LIB_MANAGER_MAX_LIBS; + lib_id++) { + uint32_t pack_id = LIB_MANAGER_PACK_LIB_ID( + lib_id); + + desc = lib_manager_get_library_manifest( + pack_id); + if (!desc) + continue; + for (i = 0; + i < desc->header.num_module_entries; + i++) { + const struct sof_man_module *mod = + (const struct sof_man_module *) + ((const uint8_t *)desc + + SOF_MAN_MODULE_OFFSET(i)); + char mname[SOF_MAN_MOD_NAME_LEN + 1]; + int j; + + for (j = 0; + j < SOF_MAN_MOD_NAME_LEN; j++) + mname[j] = (char)toupper( + (unsigned char) + mod->name[j]); + mname[SOF_MAN_MOD_NAME_LEN] = '\0'; + + if (!strncmp(upper, mname, + SOF_MAN_MOD_NAME_LEN)) { + *module_id = + (long)mod->module_id; + return 0; + } + } + } + } +#endif /* CONFIG_LIBRARY_MANAGER */ + } +#endif /* CONFIG_IPC4_BASE_FW_INTEL */ + + shell_print(sh, "error: unknown module '%s' (use name or numeric id)", s); + return -EINVAL; +} + +/* sof ppl_create [priority=0] [pages=2] [core=0] [lp=0] */ +__cold static int cmd_sof_ppl_create(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc4_pipeline_create msg = {}; + struct ipc *ipc = sof_get()->ipc; + long ppl_id, priority = 0, pages = 2, core = 0, lp = 0; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_long(sh, argv[1], &ppl_id, 0, 255) < 0) return -EINVAL; + if (argc > 2 && parse_long(sh, argv[2], &priority, 0, 7) < 0) return -EINVAL; + if (argc > 3 && parse_long(sh, argv[3], &pages, 1, 2047) < 0) return -EINVAL; + if (argc > 4 && parse_long(sh, argv[4], &core, 0, 7) < 0) return -EINVAL; + if (argc > 5 && parse_long(sh, argv[5], &lp, 0, 1) < 0) return -EINVAL; + + msg.primary.r.ppl_mem_size = (uint32_t)pages; + msg.primary.r.ppl_priority = (uint32_t)priority; + msg.primary.r.instance_id = (uint32_t)ppl_id; + msg.primary.r.type = SOF_IPC4_GLB_CREATE_PIPELINE; + msg.extension.r.lp = (uint32_t)lp; + msg.extension.r.core_id = (uint32_t)core; + + ret = ipc_pipeline_new(ipc, (ipc_pipe_new *)&msg); + if (ret < 0) + shell_print(sh, "ppl_create %ld failed: %d", ppl_id, ret); + else + shell_print(sh, "pipeline %ld created (prio=%ld pages=%ld core=%ld lp=%ld)", + ppl_id, priority, pages, core, lp); + return 0; +} + +/* sof ppl_delete */ +__cold static int cmd_sof_ppl_delete(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + long ppl_id; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_long(sh, argv[1], &ppl_id, 0, 255) < 0) return -EINVAL; + + ret = ipc_pipeline_free(ipc, (uint32_t)ppl_id); + if (ret < 0) + shell_print(sh, "ppl_delete %ld failed: %d", ppl_id, ret); + else + shell_print(sh, "pipeline %ld deleted", ppl_id); + return 0; +} + +/* sof ppl_state */ +__cold static int cmd_sof_ppl_state(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct ipc_comp_dev *ppl_icd; + bool delayed = false; + long ppl_id; + uint32_t cmd; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_long(sh, argv[1], &ppl_id, 0, 255) < 0) return -EINVAL; + + if (!strcmp(argv[2], "running")) + cmd = SOF_IPC4_PIPELINE_STATE_RUNNING; + else if (!strcmp(argv[2], "paused")) + cmd = SOF_IPC4_PIPELINE_STATE_PAUSED; + else if (!strcmp(argv[2], "reset")) + cmd = SOF_IPC4_PIPELINE_STATE_RESET; + else { + shell_print(sh, "unknown state '%s' (running|paused|reset)", argv[2]); + return -EINVAL; + } + + ppl_icd = ipc_get_comp_by_ppl_id(ipc, COMP_TYPE_PIPELINE, + (uint32_t)ppl_id, IPC_COMP_IGNORE_REMOTE); + if (!ppl_icd) { + shell_print(sh, "pipeline %ld not found", ppl_id); + return 0; + } + + ret = ipc4_pipeline_prepare(ppl_icd, cmd); + if (ret < 0) { + shell_print(sh, "ppl_state %ld prepare failed: %d", ppl_id, ret); + return 0; + } + + ret = ipc4_pipeline_trigger(ppl_icd, cmd, &delayed); + if (ret < 0) + shell_print(sh, "ppl_state %ld trigger failed: %d", ppl_id, ret); + else + shell_print(sh, "pipeline %ld -> %s%s", ppl_id, argv[2], + delayed ? " (delayed)" : ""); + return 0; +} + +/* sof mod_init [core=0] [dp=0] */ +__cold static int cmd_sof_mod_init(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc4_module_init_instance msg = {}; + struct comp_dev *dev; + long mod_id, inst_id, ppl_id, core = 0, dp = 0; + + if (parse_module_id(sh, argv[1], &mod_id) < 0) return -EINVAL; + if (parse_long(sh, argv[2], &inst_id, 0, 255) < 0) return -EINVAL; + if (parse_long(sh, argv[3], &ppl_id, 0, 255) < 0) return -EINVAL; + if (argc > 4 && parse_long(sh, argv[4], &core, 0, 7) < 0) return -EINVAL; + if (argc > 5 && parse_long(sh, argv[5], &dp, 0, 1) < 0) return -EINVAL; + + msg.primary.r.module_id = (uint32_t)mod_id; + msg.primary.r.instance_id = (uint32_t)inst_id; + msg.primary.r.type = SOF_IPC4_MOD_INIT_INSTANCE; + msg.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_MODULE_MSG; + msg.extension.r.ppl_instance_id = (uint32_t)ppl_id; + msg.extension.r.core_id = (uint32_t)core; + msg.extension.r.proc_domain = (uint32_t)dp; + msg.extension.r.param_block_size = 0; + + dev = comp_new_ipc4(&msg); + if (!dev) + shell_print(sh, "mod_init module=0x%lx inst=%ld failed", + mod_id, inst_id); + else + shell_print(sh, + "module 0x%lx inst %ld created in pipeline %ld" + " comp_id=0x%08x", + mod_id, inst_id, ppl_id, + IPC4_COMP_ID((uint32_t)mod_id, (uint32_t)inst_id)); + return 0; +} + +/* sof mod_delete */ +__cold static int cmd_sof_mod_delete(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + long mod_id, inst_id; + uint32_t comp_id; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_module_id(sh, argv[1], &mod_id) < 0) return -EINVAL; + if (parse_long(sh, argv[2], &inst_id, 0, 255) < 0) return -EINVAL; + + comp_id = IPC4_COMP_ID((uint32_t)mod_id, (uint32_t)inst_id); + ret = ipc_comp_free(ipc, comp_id); + if (ret < 0) + shell_print(sh, "mod_delete module=0x%lx inst=%ld failed: %d", + mod_id, inst_id, ret); + else + shell_print(sh, "module 0x%lx inst %ld deleted", mod_id, inst_id); + return 0; +} + +/* sof mod_bind [sq=0] [dq=0] */ +__cold static int cmd_sof_mod_bind(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc4_module_bind_unbind msg = {}; + struct ipc *ipc = sof_get()->ipc; + long src_mod, src_inst, dst_mod, dst_inst, src_q = 0, dst_q = 0; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_module_id(sh, argv[1], &src_mod) < 0) return -EINVAL; + if (parse_long(sh, argv[2], &src_inst, 0, 255) < 0) return -EINVAL; + if (parse_module_id(sh, argv[3], &dst_mod) < 0) return -EINVAL; + if (parse_long(sh, argv[4], &dst_inst, 0, 255) < 0) return -EINVAL; + if (argc > 5 && parse_long(sh, argv[5], &src_q, 0, 7) < 0) return -EINVAL; + if (argc > 6 && parse_long(sh, argv[6], &dst_q, 0, 7) < 0) return -EINVAL; + + msg.primary.r.module_id = (uint32_t)src_mod; + msg.primary.r.instance_id = (uint32_t)src_inst; + msg.primary.r.type = SOF_IPC4_MOD_BIND; + msg.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_MODULE_MSG; + msg.extension.r.dst_module_id = (uint32_t)dst_mod; + msg.extension.r.dst_instance_id = (uint32_t)dst_inst; + msg.extension.r.src_queue = (uint32_t)src_q; + msg.extension.r.dst_queue = (uint32_t)dst_q; + + ret = ipc_comp_connect(ipc, (ipc_pipe_comp_connect *)&msg); + if (ret < 0) + shell_print(sh, "mod_bind failed: %d", ret); + else + shell_print(sh, "bound 0x%lx:%ld[q%ld] -> 0x%lx:%ld[q%ld]", + src_mod, src_inst, src_q, + dst_mod, dst_inst, dst_q); + return 0; +} + +/* sof mod_unbind [sq=0] [dq=0] */ +__cold static int cmd_sof_mod_unbind(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc4_module_bind_unbind msg = {}; + struct ipc *ipc = sof_get()->ipc; + long src_mod, src_inst, dst_mod, dst_inst, src_q = 0, dst_q = 0; + int ret; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (parse_module_id(sh, argv[1], &src_mod) < 0) return -EINVAL; + if (parse_long(sh, argv[2], &src_inst, 0, 255) < 0) return -EINVAL; + if (parse_module_id(sh, argv[3], &dst_mod) < 0) return -EINVAL; + if (parse_long(sh, argv[4], &dst_inst, 0, 255) < 0) return -EINVAL; + if (argc > 5 && parse_long(sh, argv[5], &src_q, 0, 7) < 0) return -EINVAL; + if (argc > 6 && parse_long(sh, argv[6], &dst_q, 0, 7) < 0) return -EINVAL; + + msg.primary.r.module_id = (uint32_t)src_mod; + msg.primary.r.instance_id = (uint32_t)src_inst; + msg.primary.r.type = SOF_IPC4_MOD_UNBIND; + msg.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_MODULE_MSG; + msg.extension.r.dst_module_id = (uint32_t)dst_mod; + msg.extension.r.dst_instance_id = (uint32_t)dst_inst; + msg.extension.r.src_queue = (uint32_t)src_q; + msg.extension.r.dst_queue = (uint32_t)dst_q; + + ret = ipc_comp_disconnect(ipc, (ipc_pipe_comp_connect *)&msg); + if (ret < 0) + shell_print(sh, "mod_unbind failed: %d", ret); + else + shell_print(sh, "unbound 0x%lx:%ld[q%ld] -/- 0x%lx:%ld[q%ld]", + src_mod, src_inst, src_q, + dst_mod, dst_inst, dst_q); + return 0; +} + +#endif /* CONFIG_SOF_SHELL_PIPELINE_OPS */ + +#if CONFIG_SOF_SHELL_PIPELINE_OPS +SHELL_SUBCMD_ADD((sof), ppl_create, NULL, + "Create IPC4 pipeline: [priority=0] [pages=2] [core=0] [lp=0]\n", + cmd_sof_ppl_create, 2, 4); +SHELL_SUBCMD_ADD((sof), ppl_delete, NULL, + "Delete IPC4 pipeline: \n", + cmd_sof_ppl_delete, 2, 0); +SHELL_SUBCMD_ADD((sof), ppl_state, NULL, + "Set IPC4 pipeline state: \n", + cmd_sof_ppl_state, 3, 0); +SHELL_SUBCMD_ADD((sof), mod_init, NULL, + "Instantiate module: [core=0] [dp=0]\n", + cmd_sof_mod_init, 4, 2); +SHELL_SUBCMD_ADD((sof), mod_delete, NULL, + "Delete module instance: \n", + cmd_sof_mod_delete, 3, 0); +SHELL_SUBCMD_ADD((sof), mod_bind, NULL, + "Bind two module instances: [src_q=0] [dst_q=0]\n", + cmd_sof_mod_bind, 5, 2); +SHELL_SUBCMD_ADD((sof), mod_unbind, NULL, + "Unbind two module instances: [src_q=0] [dst_q=0]\n", + cmd_sof_mod_unbind, 5, 2); +#endif /* CONFIG_SOF_SHELL_PIPELINE_OPS */ + +__cold static int cmd_sof_pipeline_list(const struct shell *sh, size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + struct pipeline *p; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "ID Core Status Priority Period"); + list_for_item(clist, &ipc->comp_list) { + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_PIPELINE) + continue; + + p = icd->pipeline; + shell_print(sh, "0x%08x %d %d %d %d", + p->pipeline_id, p->core, p->status, p->priority, p->period); + } + return 0; +} + +SHELL_SUBCMD_ADD((sof), pipeline_list, NULL, + "List all active audio pipelines\n", + cmd_sof_pipeline_list, 0, 0); + +#if CONFIG_SOF_SHELL_BUFFER_INFO + +__cold static void shell_print_buffer(const struct shell *sh, struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id) +{ + const struct audio_stream *s = &buf->stream; + uint32_t size = audio_stream_get_size(s); + uint32_t avail = audio_stream_get_avail(s); + uint32_t freeb = audio_stream_get_free(s); + + shell_print(sh, + " buf 0x%08x src 0x%08x -> sink 0x%08x" + " size %u avail %u free %u ch %u rate %u fmt %d", + buf_get_id(buf), src_id, sink_id, + size, avail, freeb, + audio_stream_get_channels(s), + audio_stream_get_rate(s), + (int)audio_stream_get_frm_fmt(s)); +} + +/* + * Walk every component in the IPC topology and visit each downstream + * (bsink_list) buffer once. cb() is called for every (buffer, source, sink) + * tuple. Returns the number of buffers visited. + */ +__cold static int shell_for_each_buffer(struct ipc *ipc, + void (*cb)(const struct shell *sh, + struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, + void *ctx), + const struct shell *sh, void *ctx) +{ + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + + list_for_item(clist, &ipc->comp_list) { + struct comp_dev *cd; + struct comp_buffer *buf; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_COMPONENT) + continue; + + cd = icd->cd; + buf = comp_dev_get_first_data_consumer(cd); + while (buf) { + struct comp_dev *sink = comp_buffer_get_sink_component(buf); + + cb(sh, buf, cd->ipc_config.id, + sink ? sink->ipc_config.id : 0, ctx); + count++; + buf = comp_dev_get_next_data_consumer(cd, buf); + } + } + + return count; +} + +__cold static void buf_list_cb(const struct shell *sh, struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, void *ctx) +{ + ARG_UNUSED(ctx); + shell_print_buffer(sh, buf, src_id, sink_id); +} + +__cold static int cmd_sof_buffer_list(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + int n; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "Audio buffers:"); + n = shell_for_each_buffer(ipc, buf_list_cb, sh, NULL); + if (!n) + shell_print(sh, " (none)"); + + return 0; +} + +struct buf_find_ctx { + uint32_t want_id; + struct comp_buffer *found; + uint32_t src_id; + uint32_t sink_id; +}; + +__cold static void buf_find_cb(const struct shell *sh, struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, void *ctx) +{ + struct buf_find_ctx *c = ctx; + + ARG_UNUSED(sh); + if (c->found) + return; + if (buf_get_id(buf) == c->want_id) { + c->found = buf; + c->src_id = src_id; + c->sink_id = sink_id; + } +} + +__cold static int cmd_sof_buffer_info(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct buf_find_ctx ctx = {0}; + const struct audio_stream *s; + char *endptr = NULL; + long id; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + id = strtol(argv[1], &endptr, 0); + if (endptr == argv[1]) { + shell_error(sh, "buffer_info: invalid id"); + return -EINVAL; + } + + ctx.want_id = (uint32_t)id; + shell_for_each_buffer(ipc, buf_find_cb, sh, &ctx); + + if (!ctx.found) { + shell_print(sh, "buffer 0x%08x not found", (uint32_t)id); + return -ENOENT; + } + + s = &ctx.found->stream; + shell_print(sh, "Buffer 0x%08x:", buf_get_id(ctx.found)); + shell_print(sh, " source comp : 0x%08x", ctx.src_id); + shell_print(sh, " sink comp : 0x%08x", ctx.sink_id); + shell_print(sh, " core : %u", ctx.found->core); + shell_print(sh, " flags : 0x%08x", ctx.found->flags); + shell_print(sh, " size bytes : %u", audio_stream_get_size(s)); + shell_print(sh, " avail bytes : %u", audio_stream_get_avail(s)); + shell_print(sh, " free bytes : %u", audio_stream_get_free(s)); + shell_print(sh, " rptr : %p", audio_stream_get_rptr(s)); + shell_print(sh, " wptr : %p", audio_stream_get_wptr(s)); + shell_print(sh, " channels : %u", audio_stream_get_channels(s)); + shell_print(sh, " rate : %u", audio_stream_get_rate(s)); + shell_print(sh, " frame fmt : %d", (int)audio_stream_get_frm_fmt(s)); + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_BUFFER_INFO */ + +#if CONFIG_SOF_SHELL_BUFFER_INFO +SHELL_SUBCMD_ADD((sof), buffer_list, NULL, + "List all audio buffers (id, source/sink, fill, format)\n", + cmd_sof_buffer_list, 0, 0); +SHELL_SUBCMD_ADD((sof), buffer_info, NULL, + "Detailed info for a single buffer: \n", + cmd_sof_buffer_info, 2, 0); +#endif + +#if CONFIG_SOF_SHELL_DAI_LIST || CONFIG_SOF_SHELL_DMA_STATUS +#include +#include +#include +#endif + +#if CONFIG_SOF_SHELL_DAI_LIST + +__cold static const char *zephyr_dai_type_str(int t) +{ + switch (t) { + case DAI_LEGACY_I2S: return "i2s"; + case DAI_INTEL_SSP: return "ssp"; + case DAI_INTEL_DMIC: return "dmic"; + case DAI_INTEL_HDA: return "hda"; + case DAI_INTEL_ALH: return "alh"; + case DAI_IMX_SAI: return "sai"; + case DAI_IMX_ESAI: return "esai"; + case DAI_AMD_BT: return "amd_bt"; + case DAI_AMD_SP: return "amd_sp"; + case DAI_AMD_DMIC: return "amd_dmic"; + case DAI_MEDIATEK_AFE: return "mtk_afe"; + case DAI_INTEL_SSP_NHLT: return "ssp_nhlt"; + case DAI_INTEL_DMIC_NHLT: return "dmic_nhlt"; + case DAI_INTEL_HDA_NHLT: return "hda_nhlt"; + case DAI_INTEL_ALH_NHLT: return "alh_nhlt"; + case DAI_IMX_MICFIL: return "micfil"; + case DAI_INTEL_UAOL: return "uaol"; + case DAI_AMD_SDW: return "amd_sdw"; + default: return "?"; + } +} + +__cold static int cmd_sof_dai_list(const struct shell *sh, + size_t argc, char *argv[]) +{ + const struct device **list; + size_t count = 0; + bool verbose = (argc >= 2 && strcmp(argv[1], "-v") == 0); + int i; + + list = dai_get_device_list(&count); + if (!list || !count) { + shell_print(sh, "No DAIs registered"); + return 0; + } + + shell_print(sh, "%zu DAI(s) registered:", count); + shell_print(sh, " idx name type index channels rate fmt word"); + for (i = 0; i < count; i++) { + const struct device *dev = list[i]; + struct dai_config cfg = {0}; + const struct dai_properties *props; + + if (dai_config_get(dev, &cfg, DAI_DIR_BOTH)) { + /* try TX-only then RX-only */ + if (dai_config_get(dev, &cfg, DAI_DIR_TX) && + dai_config_get(dev, &cfg, DAI_DIR_RX)) { + shell_print(sh, " %3d %-26s (config_get failed)", + i, dev->name ? dev->name : "?"); + continue; + } + } + + shell_print(sh, + " %3d %-26s %-10s %5u %8u %7u 0x%04x %4u", + i, dev->name ? dev->name : "?", + zephyr_dai_type_str(cfg.type), cfg.dai_index, + cfg.channels, cfg.rate, cfg.format, cfg.word_size); + + if (!verbose) + continue; + + props = dai_get_properties(dev, DAI_DIR_TX, 0); + if (props) + shell_print(sh, + " TX: fifo 0x%08x depth %u hs %u stream %d", + props->fifo_address, props->fifo_depth, + props->dma_hs_id, props->stream_id); + props = dai_get_properties(dev, DAI_DIR_RX, 0); + if (props) + shell_print(sh, + " RX: fifo 0x%08x depth %u hs %u stream %d", + props->fifo_address, props->fifo_depth, + props->dma_hs_id, props->stream_id); + } + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_DAI_LIST */ + +#if CONFIG_SOF_SHELL_DMA_STATUS + +__cold static const char *dma_dir_str(enum dma_channel_direction d) +{ + switch (d) { + case MEMORY_TO_MEMORY: return "M2M"; + case MEMORY_TO_PERIPHERAL: return "M2P"; + case PERIPHERAL_TO_MEMORY: return "P2M"; + case PERIPHERAL_TO_PERIPHERAL: return "P2P"; + case HOST_TO_MEMORY: return "H2M"; + case MEMORY_TO_HOST: return "M2H"; + default: return "?"; + } +} + +__cold static void dma_print_one(const struct shell *sh, struct sof_dma *dma, + int dma_idx, int chan) +{ + struct dma_status st = {0}; + int ret = sof_dma_get_status(dma, chan, &st); + + if (ret) { + shell_print(sh, " dma %d ch %d: get_status -> %d", + dma_idx, chan, ret); + return; + } + shell_print(sh, + " dma %d ch %d: %s dir=%s pending=%u free=%u rd=%u wr=%u total=%llu", + dma_idx, chan, st.busy ? "BUSY" : "idle", + dma_dir_str(st.dir), st.pending_length, st.free, + st.read_position, st.write_position, + (unsigned long long)st.total_copied); +} + +__cold static int cmd_sof_dma_status(const struct shell *sh, + size_t argc, char *argv[]) +{ + const struct dma_info *info = dma_info_get(); + struct sof_dma *dma; + int i, ch; + + if (!info || !info->num_dmas) { + shell_print(sh, "No DMA controllers registered"); + return 0; + } + + if (argc == 1) { + shell_print(sh, "%zu DMA controller(s):", info->num_dmas); + shell_print(sh, " idx id channels busy caps devs base"); + for (i = 0; i < info->num_dmas; i++) { + dma = &info->dma_array[i]; + shell_print(sh, + " %3d %2u %8u %4u 0x%04x 0x%04x 0x%08x (%s)", + i, dma->plat_data.id, + dma->plat_data.channels, + (unsigned int)atomic_get(&dma->num_channels_busy), + dma->plat_data.caps, + dma->plat_data.devs, + dma->plat_data.base, + dma->z_dev && dma->z_dev->name ? + dma->z_dev->name : "?"); + } + shell_print(sh, + "Usage: sof dma status [chan] (omit chan to walk all)"); + return 0; + } + + { + char *end = NULL; + long idx = strtol(argv[1], &end, 0); + + if (end == argv[1] || idx < 0 || idx >= (long)info->num_dmas) { + shell_print(sh, "Bad DMA index (0..%zu)", + info->num_dmas - 1); + return -EINVAL; + } + dma = &info->dma_array[idx]; + + if (argc > 2) { + ch = strtol(argv[2], &end, 0); + if (end == argv[2] || ch < 0 || + ch >= (int)dma->plat_data.channels) { + shell_print(sh, "Bad channel (0..%u)", + dma->plat_data.channels - 1); + return -EINVAL; + } + dma_print_one(sh, dma, (int)idx, ch); + return 0; + } + + shell_print(sh, "DMA %ld (%s): %u channels", + idx, dma->z_dev && dma->z_dev->name ? + dma->z_dev->name : "?", + dma->plat_data.channels); + for (ch = 0; ch < (int)dma->plat_data.channels; ch++) + dma_print_one(sh, dma, (int)idx, ch); + } + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_DMA_STATUS */ + +#if CONFIG_SOF_SHELL_DAI_LIST +SHELL_SUBCMD_ADD((sof), dai_list, NULL, + "List all registered DAIs (name, type, channels, rate)\n" + " [-v] also show TX/RX fifo address, depth, hs, stream\n", + cmd_sof_dai_list, 1, 1); +#endif + +#if CONFIG_SOF_SHELL_DMA_STATUS +SHELL_SUBCMD_ADD((sof), dma_status, NULL, + "List DMA controllers, or per-channel status: " + "[dma_idx] [chan]\n", + cmd_sof_dma_status, 1, 2); +#endif + +#if CONFIG_SOF_SHELL_KCTL_LIST + +/* + * Best-effort decoded driver name. Module-adapter components share + * SOF_COMP_MODULE_ADAPTER for drv->type, so the only stable per-module + * label available in firmware is the UUID name string from the trace + * context (which carries the same name printed by the LDC tool). + */ +__cold static const char *kctl_drv_name(const struct comp_dev *cd) +{ + if (cd && cd->drv && cd->drv->tctx && cd->drv->tctx->uuid_p && + cd->drv->tctx->uuid_p->name[0]) + return cd->drv->tctx->uuid_p->name; + return "?"; +} + +/* + * Tag the modules that are known to expose ALSA-style kcontrols + * (volume / gain / mixer-style switches and enums). This is purely a + * UI hint -- the actual control values live behind per-module + * config_id blobs that need IPC4 large_config marshalling, which is + * intentionally out of scope here (see shell.md). + */ +__cold static const char *kctl_drv_kind(const char *name) +{ + if (!name) + return ""; + if (!strcmp(name, "volume") || !strcmp(name, "gain")) + return "volume"; + if (!strcmp(name, "mixin") || !strcmp(name, "mixout") || + !strcmp(name, "mixer")) + return "mixer"; + if (!strcmp(name, "eqiir") || !strcmp(name, "eqfir") || + !strcmp(name, "drc") || !strcmp(name, "multiband_drc") || + !strcmp(name, "dcblock") || !strcmp(name, "tdfb") || + !strcmp(name, "crossover") || !strcmp(name, "google_rtc_audio_processing")) + return "blob"; + if (!strcmp(name, "selector") || !strcmp(name, "src") || + !strcmp(name, "asrc")) + return "config"; + return ""; +} + +__cold static int cmd_sof_kctl_list(const struct shell *sh, + size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "%-12s %-8s %-5s %-24s %-8s %s", + "comp_id", "ppl_id", "core", "module", "kind", "state"); + + list_for_item(clist, &ipc->comp_list) { + const struct comp_dev *cd; + const char *name; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_COMPONENT) + continue; + + cd = icd->cd; + name = kctl_drv_name(cd); + + shell_print(sh, "0x%-10x %-8u %-5u %-24s %-8s %s", + icd->id, + cd->pipeline ? cd->pipeline->pipeline_id : 0, + icd->core, name, kctl_drv_kind(name), + comp_state_str(cd->state)); + count++; + } + + if (!count) { + shell_print(sh, + "No components found. Start an audio stream first."); + return 0; + } + + shell_print(sh, ""); + shell_print(sh, + "kctl get/set is intentionally not exposed here -- control"); + shell_print(sh, + "values flow through per-module IPC4 large_config blobs"); + shell_print(sh, + "(set_configuration / get_configuration). Use tinymix /"); + shell_print(sh, + "sof-ctl on the host, or 'sof module status' for raw state."); + + return 0; +} + +#endif /* CONFIG_SOF_SHELL_KCTL_LIST */ + +#if CONFIG_SOF_SHELL_KCTL_LIST +SHELL_SUBCMD_ADD((sof), kctl_list, NULL, + "List components and their decoded module name / kind\n", + cmd_sof_kctl_list, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module_heap, +#if CONFIG_SOF_SHELL_HEAP_USAGE + SHELL_CMD(usage, NULL, + "Print heap memory usage of each module\n", + cmd_sof_module_heap_usage), +#endif + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module, +#if CONFIG_SOF_SHELL_HEAP_USAGE + SHELL_CMD(heap, &sof_cmd_module_heap, + "Module heap commands\n", NULL), +#endif +#if CONFIG_SOF_SHELL_MODULE_STATUS + SHELL_CMD(status, NULL, + "Print status of all active components\n", + cmd_sof_module_status), +#endif +#if CONFIG_SOF_SHELL_MODULE_LIST + SHELL_CMD_ARG(list, NULL, + "List all available modules (name, inst, cpc, text, bss)\n" + " [-v] also show uuid, affinity, cps, ibs, obs\n", + cmd_sof_module_list, 1, 1), +#endif + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_PIPELINE_OPS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_pipeline, +#if CONFIG_SOF_SHELL_PIPELINE_STATUS + SHELL_CMD(status, NULL, + "Print status of all active pipelines\n", + cmd_sof_pipeline_status), +#endif + SHELL_CMD(list, NULL, + "List all active audio pipelines\n", + cmd_sof_pipeline_list), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_PIPELINE_OPS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_ppl, + SHELL_CMD_ARG(create, NULL, + "Create IPC4 pipeline: [priority=0] [pages=2] [core=0] [lp=0]\n", + cmd_sof_ppl_create, 2, 4), + SHELL_CMD_ARG(delete, NULL, + "Delete IPC4 pipeline: \n", + cmd_sof_ppl_delete, 2, 0), + SHELL_CMD_ARG(state, NULL, + "Set IPC4 pipeline state: \n", + cmd_sof_ppl_state, 3, 0), + SHELL_SUBCMD_SET_END +); + +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mod, + SHELL_CMD_ARG(init, NULL, + "Instantiate module: [core=0] [dp=0]\n", + cmd_sof_mod_init, 4, 2), + SHELL_CMD_ARG(delete, NULL, + "Delete module instance: \n", + cmd_sof_mod_delete, 3, 0), + SHELL_CMD_ARG(bind, NULL, + "Bind two module instances: " + " [src_q=0] [dst_q=0]\n", + cmd_sof_mod_bind, 5, 2), + SHELL_CMD_ARG(unbind, NULL, + "Unbind two module instances: " + " [src_q=0] [dst_q=0]\n", + cmd_sof_mod_unbind, 5, 2), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_BUFFER_INFO +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_buffer, + SHELL_CMD(list, NULL, + "List all audio buffers (id, source/sink, fill, format)\n", + cmd_sof_buffer_list), + SHELL_CMD_ARG(info, NULL, + "Detailed info for a single buffer: \n", + cmd_sof_buffer_info, 2, 0), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_DAI_LIST +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dai, + SHELL_CMD_ARG(list, NULL, + "List all registered DAIs (name, type, channels, rate)\n" + " [-v] also show TX/RX fifo address, depth, hs, stream\n", + cmd_sof_dai_list, 1, 1), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_DMA_STATUS +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dma, + SHELL_CMD_ARG(status, NULL, + "List DMA controllers, or per-channel status: " + "[dma_idx] [chan]\n", + cmd_sof_dma_status, 1, 2), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_KCTL_LIST +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_kctl, + SHELL_CMD(list, NULL, + "List components and their decoded module name / kind\n", + cmd_sof_kctl_list), + SHELL_SUBCMD_SET_END +); +#endif + +#if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST +SHELL_SUBCMD_ADD((sof), module, &sof_cmd_module, + "Module commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_PIPELINE_OPS +SHELL_SUBCMD_ADD((sof), pipeline, &sof_cmd_pipeline, + "Pipeline commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_PIPELINE_OPS +SHELL_SUBCMD_ADD((sof), ppl, &sof_cmd_ppl, + "Pipeline operation commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), mod, &sof_cmd_mod, + "Module operation commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_BUFFER_INFO +SHELL_SUBCMD_ADD((sof), buffer, &sof_cmd_buffer, + "Buffer commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_DAI_LIST +SHELL_SUBCMD_ADD((sof), dai, &sof_cmd_dai, + "DAI commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_DMA_STATUS +SHELL_SUBCMD_ADD((sof), dma, &sof_cmd_dma, + "DMA commands\n", NULL, 0, 0); +#endif + +#if CONFIG_SOF_SHELL_KCTL_LIST +SHELL_SUBCMD_ADD((sof), kctl, &sof_cmd_kctl, + "Kernel-control commands\n", NULL, 0, 0); +#endif diff --git a/zephyr/sof_shell.c b/zephyr/sof_shell.c deleted file mode 100644 index 9e90abe417bb..000000000000 --- a/zephyr/sof_shell.c +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -/* - * Copyright(c) 2024 Intel Corporation. - * - * Author: Kai Vehmanen - */ - -#include /* sof_get() */ -#include -#include - -#include -#include -#include - -#include - -#define SOF_TEST_INJECT_SCHED_GAP_USEC 1500 - -static int cmd_sof_test_inject_sched_gap(const struct shell *sh, - size_t argc, char *argv[]) -{ - uint32_t block_time = SOF_TEST_INJECT_SCHED_GAP_USEC; - char *endptr = NULL; - -#ifndef CONFIG_CROSS_CORE_STREAM - shell_fprintf(sh, SHELL_NORMAL, "Domain blocking not supported, not reliable on SMP\n"); -#endif - - domain_block(sof_get()->platform_timer_domain); - - if (argc > 1) { - block_time = strtol(argv[1], &endptr, 0); - if (endptr == argv[1]) - return -EINVAL; - } - - k_busy_wait(block_time); - - domain_unblock(sof_get()->platform_timer_domain); - - return 0; -} - -static int cmd_sof_module_heap_usage(const struct shell *sh, - size_t argc, char *argv[]) -{ - struct ipc *ipc = sof_get()->ipc; - struct list_item *clist, *_clist; - struct ipc_comp_dev *icd; - - if (!ipc) { - shell_print(sh, "No IPC"); - return 0; - } - - list_for_item_safe(clist, _clist, &ipc->comp_list) { - size_t usage, hwm; - - icd = container_of(clist, struct ipc_comp_dev, list); - if (icd->type != COMP_TYPE_COMPONENT) - continue; - - usage = module_adapter_heap_usage(comp_mod(icd->cd), &hwm); - shell_print(sh, "comp id 0x%08x%9zu usage%9zu hwm\tbytes", - icd->id, usage, hwm); - } - return 0; -} - -SHELL_STATIC_SUBCMD_SET_CREATE(sof_commands, - SHELL_CMD(test_inject_sched_gap, NULL, - "Inject a gap to audio scheduling\n", - cmd_sof_test_inject_sched_gap), - - SHELL_CMD(module_heap_usage, NULL, - "Print heap memory usage of each module\n", - cmd_sof_module_heap_usage), - - SHELL_SUBCMD_SET_END -); - -SHELL_CMD_REGISTER(sof, &sof_commands, - "SOF application commands", NULL); From 3249b840a3369bed00eb20c743ad0b92858d7890 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:47 +0100 Subject: [PATCH 02/10] ipc: add lightweight RX/TX statistics counters and shell commands Implement cycle-accurate IPC message tracking (ipc_stats) and last-message header snapshots (ipc_last). Record RX/TX statistics across IPC3 and IPC4 platform handlers, and route statistics accessors through Zephyr system calls for user-mode shell commands. --- src/include/sof/ipc/common.h | 60 +++++++++++++++++++++++++++++++ src/ipc/ipc-common.c | 66 +++++++++++++++++++++++++++++++++++ src/ipc/ipc-zephyr.c | 10 +++++- src/ipc/ipc4/handler-kernel.c | 29 +++++++++++++-- 4 files changed, 162 insertions(+), 3 deletions(-) diff --git a/src/include/sof/ipc/common.h b/src/include/sof/ipc/common.h index a910c6d42c92..d3c30e690f81 100644 --- a/src/include/sof/ipc/common.h +++ b/src/include/sof/ipc/common.h @@ -298,6 +298,66 @@ struct ipc_cmd_hdr *mailbox_validate(void); */ void ipc_cmd(struct ipc_cmd_hdr *_hdr); +/** + * \brief Lightweight IPC counters and last-message snapshot for diagnostics. + * + * Populated by the platform RX/TX hooks. Safe to read from any context; + * fields are 32-bit and updated under @ref ipc::lock when written. + */ +struct ipc_stats { + uint32_t rx_count; /**< total IPC messages received */ + uint32_t tx_count; /**< total IPC messages sent */ + uint32_t tx_direct_count; /**< messages sent via the direct path */ + uint32_t rx_errors; /**< RX path errors / unknown targets */ + uint32_t tx_errors; /**< TX path send failures */ + + /* last RX */ + uint32_t last_rx_pri; + uint32_t last_rx_ext; + uint64_t last_rx_time; /**< platform cycles */ + + /* last TX */ + uint32_t last_tx_pri; + uint32_t last_tx_ext; + uint64_t last_tx_time; +}; + +/** + * \brief Record an inbound IPC for the stats snapshot. + * + * @param[in] pri Primary header word. + * @param[in] ext Extension header word. + */ +void ipc_stats_record_rx(uint32_t pri, uint32_t ext); + +/** + * \brief Record an outbound IPC for the stats snapshot. + * + * @param[in] pri Primary header word. + * @param[in] ext Extension header word. + * @param[in] direct True if the message used the "direct" send path. + * @param[in] err Send result (negative on error). + */ +void ipc_stats_record_tx(uint32_t pri, uint32_t ext, bool direct, int err); + +/** + * \brief Increment the RX error counter without updating the last-message + * snapshot. Used for unknown targets / dispatch failures. + */ +void ipc_stats_inc_rx_error(void); + +/** + * \brief Read a copy of the current IPC statistics. + * + * @param[out] out Destination snapshot. + */ +void ipc_stats_get(struct ipc_stats *out); + +/** + * \brief Reset all IPC statistics counters. + */ +void ipc_stats_reset(void); + /** * \brief IPC message to be processed on other core. * @param[in] core Core id for IPC to be processed on. diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index afc8fe45de05..d517d0cbed12 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef __ZEPHYR__ #include @@ -46,6 +47,7 @@ #endif #include +#include LOG_MODULE_REGISTER(ipc, CONFIG_SOF_LOG_LEVEL); @@ -64,6 +66,70 @@ struct ipc *ipc_get(void) } #endif +/* Lightweight IPC stats. Protected by a dedicated spinlock so stats + * functions can be called from ipc_send_queued_msg (which already holds + * ipc->lock) without causing a deadlock. + */ +static struct ipc_stats g_ipc_stats; +static struct k_spinlock g_ipc_stats_lock; + +void ipc_stats_record_rx(uint32_t pri, uint32_t ext) +{ + k_spinlock_key_t key = k_spin_lock(&g_ipc_stats_lock); + + g_ipc_stats.rx_count++; + g_ipc_stats.last_rx_pri = pri; + g_ipc_stats.last_rx_ext = ext; + g_ipc_stats.last_rx_time = sof_cycle_get_64(); + k_spin_unlock(&g_ipc_stats_lock, key); +} + +void ipc_stats_record_tx(uint32_t pri, uint32_t ext, bool direct, int err) +{ + k_spinlock_key_t key = k_spin_lock(&g_ipc_stats_lock); + + if (err < 0) { + g_ipc_stats.tx_errors++; + } else { + if (direct) + g_ipc_stats.tx_direct_count++; + else + g_ipc_stats.tx_count++; + g_ipc_stats.last_tx_pri = pri; + g_ipc_stats.last_tx_ext = ext; + g_ipc_stats.last_tx_time = sof_cycle_get_64(); + } + k_spin_unlock(&g_ipc_stats_lock, key); +} + +void ipc_stats_inc_rx_error(void) +{ + k_spinlock_key_t key = k_spin_lock(&g_ipc_stats_lock); + + g_ipc_stats.rx_errors++; + k_spin_unlock(&g_ipc_stats_lock, key); +} + +void ipc_stats_get(struct ipc_stats *out) +{ + k_spinlock_key_t key; + + if (!out) + return; + + key = k_spin_lock(&g_ipc_stats_lock); + *out = g_ipc_stats; + k_spin_unlock(&g_ipc_stats_lock, key); +} + +void ipc_stats_reset(void) +{ + k_spinlock_key_t key = k_spin_lock(&g_ipc_stats_lock); + + memset(&g_ipc_stats, 0, sizeof(g_ipc_stats)); + k_spin_unlock(&g_ipc_stats_lock, key); +} + int ipc_process_on_core(uint32_t core, bool blocking) { struct ipc *ipc = ipc_get(); diff --git a/src/ipc/ipc-zephyr.c b/src/ipc/ipc-zephyr.c index 5661c72bb269..27d1e5b013af 100644 --- a/src/ipc/ipc-zephyr.c +++ b/src/ipc/ipc-zephyr.c @@ -231,6 +231,8 @@ enum task_state ipc_platform_do_cmd(struct ipc *ipc) hdr = ipc_compact_read_msg(); + ipc_stats_record_rx(((uint32_t *)hdr)[0], ((uint32_t *)hdr)[1]); + /* perform command */ ipc_cmd(hdr); @@ -274,13 +276,17 @@ void ipc_platform_complete_cmd(struct ipc *ipc) int ipc_platform_send_msg(const struct ipc_msg *msg) { + int ret; + if (ipc_service_get_tx_buffer_size(&sof_ipc_ept) == 0) return -EBUSY; /* prepare the message and copy to mailbox */ struct ipc_cmd_hdr *hdr = ipc_prepare_to_send(msg); - return ipc_service_send(&sof_ipc_ept, hdr, sizeof(*hdr)); + ret = ipc_service_send(&sof_ipc_ept, hdr, sizeof(*hdr)); + ipc_stats_record_tx(((uint32_t *)hdr)[0], ((uint32_t *)hdr)[1], false, ret); + return ret; } void ipc_platform_send_msg_direct(const struct ipc_msg *msg) @@ -289,6 +295,8 @@ void ipc_platform_send_msg_direct(const struct ipc_msg *msg) struct ipc_cmd_hdr *hdr = ipc_prepare_to_send(msg); int ret = ipc_service_send_critical(&sof_ipc_ept, hdr, sizeof(*hdr)); + ipc_stats_record_tx(((uint32_t *)hdr)[0], ((uint32_t *)hdr)[1], true, ret); + if (ret < 0) tr_err(&ipc_tr, "ipc_service_send_critical() failed: %d", ret); } diff --git a/src/ipc/ipc4/handler-kernel.c b/src/ipc/ipc4/handler-kernel.c index 03c2e6c81e86..5740b009ad64 100644 --- a/src/ipc/ipc4/handler-kernel.c +++ b/src/ipc/ipc4/handler-kernel.c @@ -46,6 +46,7 @@ #endif #include +#include #include #include #include @@ -261,8 +262,31 @@ __cold static int ipc4_load_library(struct ipc4_message_request *ipc4) ret = lib_manager_load_library(library.header.r.dma_id, library.header.r.lib_id, ipc4->primary.r.type); - if (ret != 0) - return (ret == -EINVAL) ? IPC4_ERROR_INVALID_PARAM : IPC4_FAILURE; + if (ret != 0) { + log_panic(); /* flush all pending log messages before replying */ + /* Encode specific error to allow diagnosis from dmesg IPC status code */ + switch (ret) { + case -EINVAL: + return IPC4_ERROR_INVALID_PARAM; /* 1 - invalid param */ + case -ENOMEM: + return IPC4_OUT_OF_MEMORY; /* 3 - out of memory */ + case -EFAULT: + return 4; /* 4 - EFAULT */ + case -ENOENT: + return 9; /* 9 - resource not found (symbol) */ + case -ENOEXEC: + return IPC4_INVALID_MANIFEST; /* 14 - invalid ELF */ + case -EPROTO: + return 15; /* 15 - protocol error (EPROTO) */ + case -EBUSY: + return 42; /* 42 - EBUSY */ + case -ETIMEDOUT: + return 43; /* 43 - DMA timeout */ + default: + /* encode negative errno as 100+ value for diagnosis */ + return (-ret < 50) ? (-ret + 100) : IPC4_FAILURE; + } + } return IPC4_SUCCESS; } @@ -623,6 +647,7 @@ void ipc_cmd(struct ipc_cmd_hdr *_hdr) /* should not reach here as we only have 2 message types */ ipc_cmd_err(&ipc_tr, "ipc4: invalid target %d", target); err = IPC4_UNKNOWN_MESSAGE_TYPE; + ipc_stats_inc_rx_error(); } /* FW sends an ipc message to host if request bit is clear */ From 42bac3e10f1bd65cc85145ec3a2fb271d33ad8e6 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:51 +0100 Subject: [PATCH 03/10] zephyr/shell: add virtual page and region diagnostic support Add Virtual Page (vpage) and Virtual Region (vregion) tracking structures and APIs. Allows the 'sof vpage_status' and 'sof vregion_status' shell commands to inspect virtual memory allocation, free lists, and reference counts. --- src/include/sof/lib/vregion.h | 29 ++++++++++++++++++++++ zephyr/include/sof/lib/vpage.h | 32 ++++++++++++++++++++++++ zephyr/lib/vpage.c | 30 +++++++++++++++++++++++ zephyr/lib/vregion.c | 45 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/src/include/sof/lib/vregion.h b/src/include/sof/lib/vregion.h index 5c066c90dbc8..c017935efc10 100644 --- a/src/include/sof/lib/vregion.h +++ b/src/include/sof/lib/vregion.h @@ -6,6 +6,7 @@ #define __SOF_LIB_VREGION_H__ #include +#include #ifdef __cplusplus extern "C" { @@ -13,6 +14,18 @@ extern "C" { struct vregion; +/** + * @brief Per-vregion snapshot returned by vregion_for_each(). + */ +struct vregion_snapshot { + uintptr_t base; + size_t size; + unsigned int pages; + size_t lifetime_used; + int lifetime_free_count; + unsigned int use_count; +}; + /** * @brief Memory types for virtual region allocations. * Used to specify the type of memory allocation within a virtual region. @@ -131,6 +144,19 @@ void vregion_info(struct vregion *vr); */ void vregion_mem_info(struct vregion *vr, size_t *size, uintptr_t *start); +/** + * @brief Dump all virtual regions info + * + * Iterate over every registered virtual region. The global vregion list lock + * is held for the duration of the walk; @p cb must not block or call back + * into the vregion API. + * + * @param[in] cb Callback invoked once per region with a stable snapshot. + * @param[in] ctx Opaque context passed through to @p cb. + */ +void vregion_for_each(void (*cb)(int idx, const struct vregion_snapshot *s, void *ctx), + void *ctx); + #else /* CONFIG_SOF_VREGIONS */ struct vregion { @@ -174,6 +200,9 @@ static inline void vregion_mem_info(struct vregion *vr, size_t *size, uintptr_t if (size) *size = 0; } +static inline void vregion_for_each(void (*cb)(int idx, + const struct vregion_snapshot *s, + void *ctx), void *ctx) {} #endif /* CONFIG_SOF_VREGIONS */ diff --git a/zephyr/include/sof/lib/vpage.h b/zephyr/include/sof/lib/vpage.h index f3fc8b89e968..17f5ff7e06e9 100644 --- a/zephyr/include/sof/lib/vpage.h +++ b/zephyr/include/sof/lib/vpage.h @@ -12,6 +12,18 @@ extern "C" { #endif +/** + * @brief Snapshot of virtual page allocator global statistics. + */ +struct vpage_stats { + void *region_base; + size_t region_size; + unsigned int total_pages; + unsigned int free_pages; + unsigned int num_elems_in_use; + unsigned int max_allocs; +}; + /** * @brief Allocate virtual pages * Allocates a specified number of contiguous virtual memory pages by mapping @@ -31,6 +43,26 @@ void *vpage_alloc(unsigned int pages); */ void vpage_free(void *ptr); +/** + * @brief Get a snapshot of virtual page allocator statistics. + * + * @param[out] stats Filled with current allocator state. + */ +void vpage_get_stats(struct vpage_stats *stats); + +/** + * @brief Iterate over all currently in-use virtual page allocations. + * + * The allocator lock is held for the duration of the walk; @p cb must not + * block or call back into the vpage allocator. + * + * @param[in] cb Callback invoked once per allocation element. + * @param[in] ctx Opaque context passed through to @p cb. + */ +void vpage_for_each_alloc(void (*cb)(unsigned int idx, unsigned int vpage, + unsigned int pages, void *ctx), + void *ctx); + #ifdef __cplusplus } #endif diff --git a/zephyr/lib/vpage.c b/zephyr/lib/vpage.c index ce0da7b5ac97..b324f6eb34a6 100644 --- a/zephyr/lib/vpage.c +++ b/zephyr/lib/vpage.c @@ -251,6 +251,36 @@ void vpage_free(void *ptr) vpage_ctx.total_pages); } +void vpage_get_stats(struct vpage_stats *stats) +{ + if (!stats) + return; + + k_mutex_lock(&vpage_ctx.lock, K_FOREVER); + stats->region_base = (void *)vpage_ctx.virtual_region->addr; + stats->region_size = vpage_ctx.virtual_region->size; + stats->total_pages = vpage_ctx.total_pages; + stats->free_pages = vpage_ctx.free_pages; + stats->num_elems_in_use = vpage_ctx.num_elems_in_use; + stats->max_allocs = VPAGE_MAX_ALLOCS; + k_mutex_unlock(&vpage_ctx.lock); +} +EXPORT_SYMBOL(vpage_get_stats); + +void vpage_for_each_alloc(void (*cb)(unsigned int idx, unsigned int vpage, + unsigned int pages, void *ctx), + void *ctx) +{ + if (!cb) + return; + + k_mutex_lock(&vpage_ctx.lock, K_FOREVER); + for (unsigned int i = 0; i < vpage_ctx.num_elems_in_use; i++) + cb(i, vpage_ctx.velems[i].vpage, vpage_ctx.velems[i].pages, ctx); + k_mutex_unlock(&vpage_ctx.lock); +} +EXPORT_SYMBOL(vpage_for_each_alloc); + /** * @brief Initialize virtual page allocator * diff --git a/zephyr/lib/vregion.c b/zephyr/lib/vregion.c index 9c8c94c23f97..1274cbd0d6f9 100644 --- a/zephyr/lib/vregion.c +++ b/zephyr/lib/vregion.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,9 @@ LOG_MODULE_REGISTER(vregion, CONFIG_SOF_LOG_LEVEL); +static sys_dlist_t vregion_list = SYS_DLIST_STATIC_INIT(&vregion_list); +static K_MUTEX_DEFINE(vregion_list_lock); + /* * Pre Allocated Contiguous Virtual Memory Region Allocator * @@ -81,6 +85,8 @@ struct interim_heap { * TODO: Add support to flag which heaps should have their contexts saved and restored. */ struct vregion { + sys_dnode_t node; + /* region context */ uint8_t *base; /* base address of entire region */ size_t size; /* size of whole region in bytes */ @@ -163,6 +169,10 @@ struct vregion *vregion_create(size_t memsize) LOG_INF("new at base %p size %#zx pages %u metadata at %p", (void *)vr->base, total_size, pages, (void *)vr); + k_mutex_lock(&vregion_list_lock, K_FOREVER); + sys_dlist_append(&vregion_list, &vr->node); + k_mutex_unlock(&vregion_list_lock); + return vr; } @@ -203,6 +213,11 @@ struct vregion *vregion_put(struct vregion *vr) /* log the vregion being destroyed */ LOG_DBG("destroy %p size %#zx pages %u", (void *)vr->base, vr->size, vr->pages); LOG_DBG(" lifetime used %zu free count %d", vr->lifetime.used, vr->lifetime.free_count); + + k_mutex_lock(&vregion_list_lock, K_FOREVER); + sys_dlist_remove(&vr->node); + k_mutex_unlock(&vregion_list_lock); + vpage_free(vr->base); rfree(vr); @@ -498,3 +513,33 @@ void vregion_mem_info(struct vregion *vr, size_t *size, uintptr_t *start) if (start) *start = (uintptr_t)vr->base; } + +void vregion_for_each(void (*cb)(int idx, const struct vregion_snapshot *s, void *ctx), + void *ctx) +{ + struct vregion *vr; + int idx = 0; + + if (!cb) + return; + + k_mutex_lock(&vregion_list_lock, K_FOREVER); + + SYS_DLIST_FOR_EACH_CONTAINER(&vregion_list, vr, node) { + struct vregion_snapshot s; + + k_mutex_lock(&vr->lock, K_FOREVER); + s.base = (uintptr_t)vr->base; + s.size = vr->size; + s.pages = vr->pages; + s.lifetime_used = vr->lifetime.used; + s.lifetime_free_count = vr->lifetime.free_count; + s.use_count = vr->use_count; + k_mutex_unlock(&vr->lock); + + cb(idx++, &s, ctx); + } + + k_mutex_unlock(&vregion_list_lock); +} +EXPORT_SYMBOL(vregion_for_each); From 8338b5e0a09ae4f6f73e08b6a73a04c6cface5f8 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:52 +0100 Subject: [PATCH 04/10] schedule: add scheduler task enumeration ops for shell diagnostics Implement scheduler_dump_tasks callbacks across LL timer, LL DMA, DP, and TWB scheduler domains. Exposes task list state, priority, and execution cycles for the 'sof sched_tasks' and 'sof sched_load' shell commands. --- src/include/sof/schedule/schedule.h | 15 +++++++++++++++ src/schedule/zephyr_dp_schedule.c | 18 ++++++++++++++++++ src/schedule/zephyr_ll.c | 18 ++++++++++++++++++ src/schedule/zephyr_twb_schedule.c | 18 ++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/src/include/sof/schedule/schedule.h b/src/include/sof/schedule/schedule.h index a68169736ef2..336b21918f7e 100644 --- a/src/include/sof/schedule/schedule.h +++ b/src/include/sof/schedule/schedule.h @@ -161,6 +161,21 @@ struct scheduler_ops { * This operation is optional. */ struct k_thread *(*scheduler_init_context)(void *data, struct task *task); + + /** + * Iterate over all tasks owned by the scheduler and invoke @p cb on each. + * The scheduler is responsible for taking its own lock around the walk. + * @param data Private data of selected scheduler. + * @param cb Callback called once per task; must not block. + * @param ctx Opaque context passed to @p cb. + * + * This operation is optional and exists only for diagnostics + * (e.g. shell commands). Schedulers that do not implement it are + * silently skipped by enumeration tools. + */ + void (*scheduler_dump_tasks)(void *data, + void (*cb)(struct task *task, void *ctx), + void *ctx); }; /** \brief Holds information about scheduler. */ diff --git a/src/schedule/zephyr_dp_schedule.c b/src/schedule/zephyr_dp_schedule.c index 25fa8b319457..a51131979d5b 100644 --- a/src/schedule/zephyr_dp_schedule.c +++ b/src/schedule/zephyr_dp_schedule.c @@ -337,6 +337,23 @@ static int scheduler_dp_task_shedule(void *data, struct task *task, uint64_t sta return 0; } +static void scheduler_dp_dump_tasks(void *data, + void (*cb)(struct task *task, void *ctx), + void *ctx) +{ + struct scheduler_dp_data *dp_sch = data; + struct list_item *tlist; + unsigned int lock_key; + + lock_key = scheduler_dp_lock(cpu_get_id()); + list_for_item(tlist, &dp_sch->tasks) { + struct task *task = container_of(tlist, struct task, list); + + cb(task, ctx); + } + scheduler_dp_unlock(lock_key); +} + static struct scheduler_ops schedule_dp_ops = { .schedule_task = scheduler_dp_task_shedule, #if CONFIG_SOF_USERSPACE_APPLICATION @@ -345,6 +362,7 @@ static struct scheduler_ops schedule_dp_ops = { .schedule_task_cancel = scheduler_dp_task_stop, #endif .schedule_task_free = scheduler_dp_task_free, + .scheduler_dump_tasks = scheduler_dp_dump_tasks, }; __cold int scheduler_dp_init(void) diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 9950c8c2050b..91169143446d 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -599,6 +599,23 @@ struct k_thread *zephyr_ll_init_context(void *data, struct task *task) } #endif +static void zephyr_ll_dump_tasks(void *data, + void (*cb)(struct task *task, void *ctx), + void *ctx) +{ + struct zephyr_ll *sch = data; + struct list_item *list; + uint32_t flags = 0; + + zephyr_ll_lock(sch, &flags); + list_for_item(list, &sch->tasks) { + struct task *task = container_of(list, struct task, list); + + cb(task, ctx); + } + zephyr_ll_unlock(sch, &flags); +} + static const struct scheduler_ops zephyr_ll_ops = { .schedule_task = zephyr_ll_task_schedule, .schedule_task_before = zephyr_ll_task_schedule_before, @@ -611,6 +628,7 @@ static const struct scheduler_ops zephyr_ll_ops = { #if CONFIG_SOF_USERSPACE_LL .scheduler_init_context = zephyr_ll_init_context, #endif + .scheduler_dump_tasks = zephyr_ll_dump_tasks, }; #if CONFIG_SOF_USERSPACE_LL diff --git a/src/schedule/zephyr_twb_schedule.c b/src/schedule/zephyr_twb_schedule.c index 5e85cbf61134..bb721106f865 100644 --- a/src/schedule/zephyr_twb_schedule.c +++ b/src/schedule/zephyr_twb_schedule.c @@ -343,10 +343,28 @@ static int scheduler_twb_task_free(void *data, struct task *task) return 0; } +static void scheduler_twb_dump_tasks(void *data, + void (*cb)(struct task *task, void *ctx), + void *ctx) +{ + struct scheduler_twb_data *twb_sch = data; + struct list_item *tlist; + unsigned int key; + + key = scheduler_twb_lock(); + list_for_item(tlist, &twb_sch->tasks) { + struct task *task = container_of(tlist, struct task, list); + + cb(task, ctx); + } + scheduler_twb_unlock(key); +} + static struct scheduler_ops schedule_twb_ops = { .schedule_task = scheduler_twb_task_shedule, .schedule_task_cancel = scheduler_twb_task_cancel, .schedule_task_free = scheduler_twb_task_free, + .scheduler_dump_tasks = scheduler_twb_dump_tasks, }; __cold int scheduler_twb_init(void) From ebfe5a0744663f31c5927fd9ad195681591c8b7c Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:54 +0100 Subject: [PATCH 05/10] library_manager: implement page-level VMA allocator for relocatable LLEXT modules Add virtual memory address (VMA) page-level allocation using sys_bitarray over the library region. Support relocatable LLEXT linking, SLID symbol linking, unsigned LLEXT modules, and interactive LLEXT shell management (llext_load, llext_list, llext_purge). --- app/llext_relocatable.conf | 1 + scripts/llext_link_helper.py | 23 +-- scripts/llext_offset_calc.py | 3 + src/include/sof/lib_manager.h | 17 +++ src/library_manager/Kconfig | 32 ++++- src/library_manager/lib_manager.c | 134 ++++++++++++++---- src/library_manager/llext_manager.c | 172 ++++++++++++++++++++++- src/library_manager/llext_manager_dram.c | 5 + zephyr/include/sof/shell_llext_load.h | 72 ++++++++++ 9 files changed, 416 insertions(+), 43 deletions(-) create mode 100644 zephyr/include/sof/shell_llext_load.h diff --git a/app/llext_relocatable.conf b/app/llext_relocatable.conf index 76b5339e1bb0..ce8dafe3a6aa 100644 --- a/app/llext_relocatable.conf +++ b/app/llext_relocatable.conf @@ -1 +1,2 @@ CONFIG_LLEXT_TYPE_ELF_RELOCATABLE=y +CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y diff --git a/scripts/llext_link_helper.py b/scripts/llext_link_helper.py index f10777c9918f..333c863d1d21 100755 --- a/scripts/llext_link_helper.py +++ b/scripts/llext_link_helper.py @@ -77,6 +77,8 @@ def main(): command = [args.command] + is_relocatable = '-r' in args.params + executable = [] writable = [] readonly = [] @@ -111,7 +113,8 @@ def main(): text_found = True text_addr = max_alignment(text_addr, 0x1000, s_alignment) text_size = s_size - command.append(f'-Wl,-Ttext=0x{text_addr:x}') + if not is_relocatable: + command.append(f'-Wl,-Ttext=0x{text_addr:x}') else: executable.append(section) @@ -164,7 +167,8 @@ def main(): dram_addr = align_up(dram_addr, s_alignment) - command.append(f'-Wl,--section-start={s_name}=0x{dram_addr:x}') + if not is_relocatable: + command.append(f'-Wl,--section-start={s_name}=0x{dram_addr:x}') dram_addr += section.header['sh_size'] @@ -177,7 +181,8 @@ def main(): dram_addr = align_up(dram_addr, s_alignment) - command.append(f'-Wl,--section-start={s_name}=0x{dram_addr:x}') + if not is_relocatable: + command.append(f'-Wl,--section-start={s_name}=0x{dram_addr:x}') dram_addr += section.header['sh_size'] @@ -189,7 +194,8 @@ def main(): start_addr = align_up(start_addr, s_alignment) - command.append(f'-Wl,--section-start={s_name}=0x{start_addr:x}') + if not is_relocatable: + command.append(f'-Wl,--section-start={s_name}=0x{start_addr:x}') start_addr += section.header['sh_size'] @@ -201,10 +207,11 @@ def main(): start_addr = align_up(start_addr, s_alignment) - if s_name == '.data': - command.append(f'-Wl,-Tdata=0x{start_addr:x}') - else: - command.append(f'-Wl,--section-start={s_name}=0x{start_addr:x}') + if not is_relocatable: + if s_name == '.data': + command.append(f'-Wl,-Tdata=0x{start_addr:x}') + else: + command.append(f'-Wl,--section-start={s_name}=0x{start_addr:x}') start_addr += section.header['sh_size'] diff --git a/scripts/llext_offset_calc.py b/scripts/llext_offset_calc.py index 0f302a8cbe12..2a07984b725f 100755 --- a/scripts/llext_offset_calc.py +++ b/scripts/llext_offset_calc.py @@ -47,6 +47,9 @@ def get_elf_size(elf_name): if section.header['sh_addr'] + section.header['sh_size'] > end: end = section.header['sh_addr'] + section.header['sh_size'] + if start == 0xffffffff: + return 0 + size = end - start return size diff --git a/src/include/sof/lib_manager.h b/src/include/sof/lib_manager.h index 29c226eb61a7..94d9472967c9 100644 --- a/src/include/sof/lib_manager.h +++ b/src/include/sof/lib_manager.h @@ -116,6 +116,8 @@ struct lib_manager_module { unsigned int n_dependent; /* For auxiliary modules: number of dependents */ bool mapped; struct lib_manager_segment_desc segment[LIB_MANAGER_N_SEGMENTS]; + uintptr_t vma_base; + size_t vma_size; }; struct lib_manager_mod_ctx { @@ -250,4 +252,19 @@ void lib_notif_msg_send(struct ipc_msg *msg); */ void lib_notif_msg_clean(bool leave_one_handle); +/* + * \brief Purge (free) a loadable library from IMR/DRAM storage + * + * param[in] lib_id - library slot id (1 .. LIB_MANAGER_MAX_LIBS-1) + * + * Removes the library binary from DRAM/IMR and releases the + * lib_manager_mod_ctx entry so that the slot can be reused by a + * future LOAD_LIBRARY call. Returns -EBUSY if any module file from + * the library is still mapped in SRAM (i.e., has active instances + * or is still linked as a dependency). + * + * Return: 0 on success, negative errno on error. + */ +int lib_manager_purge_library(uint32_t lib_id); + #endif /* __SOF_LIB_MANAGER_H__ */ diff --git a/src/library_manager/Kconfig b/src/library_manager/Kconfig index 8eac39e59970..e015e395ff14 100644 --- a/src/library_manager/Kconfig +++ b/src/library_manager/Kconfig @@ -24,15 +24,41 @@ config LIBCODE_MODULE_SUPPORT a multiple modules. This option adds support for modules of this type. +menu "LLEXT module authentication" + +comment "Module authentication options." + config LIBRARY_AUTH_SUPPORT bool "Library Authentication Support" default n help - This is support for dynamic modules authentication. - Externally developed modules both for SOF and Zephyr - could be used if enabled. + Enable ROM-based authentication of dynamically loaded LLEXT modules. + When enabled signed modules must carry a valid CSS signature verified by + the platform ROM auth engine before it is mapped into SRAM. + + Use an authenticated module if you need guranteed memory isolation for + module. e.g. to protect biometric data or other sensitive information. + If unsure say N. +config LIBRARY_ALLOW_UNSIGNED_MODULES + bool "Allow loading unsigned modules" + default n + depends on LIBRARY_AUTH_SUPPORT + help + When enabled, modules without a authentication signature may be loaded + as low privilege userspace code. These modules can exist alongside + authenticated modules, but do not receive hardware memory isolation + from other modules. + + Use unsigned modules if you have no need for memory isolation and + want to avoid the overhead of signing and domain switching. Note that + unsigned modules are not protected from other modules. + + If unsure say N. + +endmenu # LLEXT module authentication + config LIBRARY_DEFAULT_MODULAR bool "Build LLEXT modules by default" depends on LLEXT diff --git a/src/library_manager/lib_manager.c b/src/library_manager/lib_manager.c index 19c317ed1089..08ab0115b9a3 100644 --- a/src/library_manager/lib_manager.c +++ b/src/library_manager/lib_manager.c @@ -16,6 +16,8 @@ #include #include +#include + #include #include #include @@ -918,11 +920,16 @@ static int lib_manager_dma_deinit(struct lib_manager_dma_ext *dma_ext, uint32_t static int lib_manager_load_data_from_host(struct lib_manager_dma_ext *dma_ext, uint32_t size) { - uint64_t timeout = k_ms_to_cyc_ceil64(200); + uint64_t timeout = k_ms_to_cyc_ceil64(600); struct dma_status stat; + uint32_t init_wp; int ret; - /* Wait till whole data acquired with timeout of 200ms */ + /* Capture initial WP for diagnostics */ + ret = dma_get_status(dma_ext->chan->dma->z_dev, dma_ext->chan->index, &stat); + init_wp = stat.write_position; + + /* Wait till whole data acquired with timeout of 600ms */ timeout += sof_cycle_get_64(); for (;;) { @@ -937,7 +944,10 @@ static int lib_manager_load_data_from_host(struct lib_manager_dma_ext *dma_ext, k_usleep(100); } - tr_err(&lib_manager_tr, "timeout during DMA transfer"); + tr_err(&lib_manager_tr, + "DMA timeout: plen=%u wp=%u rp=%u sz=%u init_wp=%u", + stat.pending_length, stat.write_position, stat.read_position, size, + init_wp); return -ETIMEDOUT; } @@ -1029,13 +1039,15 @@ static int lib_manager_store_library(struct lib_manager_dma_ext *dma_ext, tr_dbg(&lib_manager_tr, "pointer: %p", (__sparse_force void *)library_base_address); #if CONFIG_LIBRARY_AUTH_SUPPORT - /* AUTH_PHASE_FIRST - checks library manifest only. */ - ret = lib_manager_auth_proc((__sparse_force const void *)man_buffer, - MAN_MAX_SIZE_V1_8, AUTH_PHASE_FIRST, auth_ctx); - if (ret < 0) { - rfree((__sparse_force void *)library_base_address); - return ret; - } + if (auth_ctx) { + /* AUTH_PHASE_FIRST - checks library manifest only. */ + ret = lib_manager_auth_proc((__sparse_force const void *)man_buffer, + MAN_MAX_SIZE_V1_8, AUTH_PHASE_FIRST, auth_ctx); + if (ret < 0) { + rfree((__sparse_force void *)library_base_address); + return ret; + } + } #endif /* CONFIG_LIBRARY_AUTH_SUPPORT */ /* Copy data from temp_mft_buf to destination memory (pointed by library_base_address) */ @@ -1046,6 +1058,7 @@ static int lib_manager_store_library(struct lib_manager_dma_ext *dma_ext, ret = lib_manager_store_data(dma_ext, (uint8_t __sparse_cache *)library_base_address + MAN_MAX_SIZE_V1_8, preload_size - MAN_MAX_SIZE_V1_8); if (ret < 0) { + tr_err(&lib_manager_tr, "lib_manager_store_data(rest) failed: %d", ret); rfree((__sparse_force void *)library_base_address); return ret; } @@ -1054,13 +1067,15 @@ static int lib_manager_store_library(struct lib_manager_dma_ext *dma_ext, dcache_writeback_region((__sparse_force void *)library_base_address, preload_size); #if CONFIG_LIBRARY_AUTH_SUPPORT - /* AUTH_PHASE_LAST - do final library authentication checks */ - ret = lib_manager_auth_proc((__sparse_force void *)library_base_address, - preload_size - MAN_MAX_SIZE_V1_8, AUTH_PHASE_LAST, auth_ctx); - if (ret < 0) { - rfree((__sparse_force void *)library_base_address); - return ret; - } + if (auth_ctx) { + /* AUTH_PHASE_LAST - do final library authentication checks */ + ret = lib_manager_auth_proc((__sparse_force void *)library_base_address, + preload_size - MAN_MAX_SIZE_V1_8, AUTH_PHASE_LAST, auth_ctx); + if (ret < 0) { + rfree((__sparse_force void *)library_base_address); + return ret; + } + } #endif /* CONFIG_LIBRARY_AUTH_SUPPORT */ /* Now update sof context with new library */ @@ -1178,6 +1193,7 @@ int lib_manager_load_library(uint32_t dma_id, uint32_t lib_id, uint32_t type) MAN_MAX_SIZE_V1_8, CONFIG_MM_DRV_PAGE_SIZE); if (!man_tmp_buffer) { ret = -ENOMEM; + LOG_ERR("man_tmp_buffer alloc failed"); goto cleanup; } @@ -1188,18 +1204,26 @@ int lib_manager_load_library(uint32_t dma_id, uint32_t lib_id, uint32_t type) #if CONFIG_LIBRARY_AUTH_SUPPORT struct auth_api_ctx auth_ctx; - void *auth_buffer; - - /* Initialize authentication support */ - ret = lib_manager_auth_init(&auth_ctx, &auth_buffer); - if (ret < 0) - goto stop_dma; - - ret = lib_manager_store_library(dma_ext, man_tmp_buffer, lib_id, &auth_ctx); - - lib_manager_auth_deinit(&auth_ctx, auth_buffer); + void *auth_buffer = NULL; + struct auth_api_ctx *auth_ctx_ptr = NULL; + + if (!IS_ENABLED(CONFIG_LIBRARY_ALLOW_UNSIGNED_MODULES)) { + /* Initialize authentication support */ + ret = lib_manager_auth_init(&auth_ctx, &auth_buffer); + if (ret < 0) + goto stop_dma; + auth_ctx_ptr = &auth_ctx; + } else { + tr_warn(&lib_manager_tr, + "Loading module without authentication (unsigned modules allowed)"); + } + + ret = lib_manager_store_library(dma_ext, man_tmp_buffer, lib_id, auth_ctx_ptr); + + if (auth_ctx_ptr) + lib_manager_auth_deinit(&auth_ctx, auth_buffer); #else - ret = lib_manager_store_library(dma_ext, man_tmp_buffer, lib_id, NULL); + ret = lib_manager_store_library(dma_ext, man_tmp_buffer, lib_id, NULL); #endif /* CONFIG_LIBRARY_AUTH_SUPPORT */ stop_dma: @@ -1231,6 +1255,60 @@ int lib_manager_load_library(uint32_t dma_id, uint32_t lib_id, uint32_t type) if (!ret) tr_info(&lib_manager_tr, "loaded library id: %u", lib_id); + else + LOG_ERR("lib_manager_load_library FAILED ret=%d", ret); return ret; } + +int lib_manager_purge_library(uint32_t lib_id) +{ + struct ext_library *ext_lib = ext_lib_get(); + struct lib_manager_mod_ctx *ctx; + unsigned int i; + + if (!lib_id || lib_id >= LIB_MANAGER_MAX_LIBS) + return -EINVAL; + + ctx = ext_lib->desc[lib_id]; + if (!ctx || !ctx->base_addr) + return -ENOENT; + +#if CONFIG_LLEXT + /* Refuse if any module file is still mapped in SRAM */ + if (ctx->mod) { + for (i = 0; i < ctx->n_mod; i++) { + if (ctx->mod[i].mapped) { + tr_err(&lib_manager_tr, + "lib %u mod[%u] still in SRAM", + lib_id, i); + return -EBUSY; + } + /* Auxiliary libs linked via llext_manager_add_library + * have an ebl/llext but no mapped SRAM; still in use if + * n_dependent > 0. */ + if (ctx->mod[i].n_dependent) { + tr_err(&lib_manager_tr, + "lib %u mod[%u] still has %u dependents", + lib_id, i, ctx->mod[i].n_dependent); + return -EBUSY; + } + } + rfree(ctx->mod); + ctx->mod = NULL; + } +#else + (void)i; +#endif /* CONFIG_LLEXT */ + + /* Free the DRAM/IMR storage buffer */ + rfree(ctx->base_addr); + ctx->base_addr = NULL; + + /* Free the context itself and clear the global descriptor slot */ + rfree(ctx); + ext_lib->desc[lib_id] = NULL; + + tr_info(&lib_manager_tr, "purged library id: %u", lib_id); + return 0; +} diff --git a/src/library_manager/llext_manager.c b/src/library_manager/llext_manager.c index 4c1e4f02d5b5..26c298f370a9 100644 --- a/src/library_manager/llext_manager.c +++ b/src/library_manager/llext_manager.c @@ -50,6 +50,101 @@ extern struct tr_ctx lib_manager_tr; #define PAGE_SZ CONFIG_MM_DRV_PAGE_SIZE +#include +#include + +#define LLEXT_LIB_PAGES (CONFIG_LIBRARY_REGION_SIZE / PAGE_SZ) +SYS_BITARRAY_DEFINE_STATIC(lib_vma_bitarray, LLEXT_LIB_PAGES); + +static uintptr_t llext_manager_alloc_vma(size_t size) +{ + size_t num_pages = ALIGN_UP(size, PAGE_SZ) / PAGE_SZ; + size_t offset; + int ret; + + ret = sys_bitarray_alloc(&lib_vma_bitarray, num_pages, &offset); + if (ret < 0) { + tr_err(&lib_manager_tr, "llext_manager_alloc_vma: failed to allocate %zu pages", num_pages); + return 0; + } + + return CONFIG_LIBRARY_BASE_ADDRESS + offset * PAGE_SZ; +} + +void llext_manager_free_vma(uintptr_t vma, size_t size) +{ + if (!vma || !size) + return; + + size_t num_pages = ALIGN_UP(size, PAGE_SZ) / PAGE_SZ; + size_t offset = (vma - CONFIG_LIBRARY_BASE_ADDRESS) / PAGE_SZ; + + sys_bitarray_free(&lib_vma_bitarray, num_pages, offset); +} + +static enum llext_mem llext_manager_get_sec_mem_idx(const char *name, const elf_shdr_t *shdr) +{ + if (strcmp(name, ".exported_sym") == 0) + return LLEXT_MEM_EXPORT; + + switch (shdr->sh_type) { + case SHT_NOBITS: + return LLEXT_MEM_BSS; + case SHT_PROGBITS: + if (shdr->sh_flags & SHF_EXECINSTR) + return LLEXT_MEM_TEXT; + else if (shdr->sh_flags & SHF_WRITE) + return LLEXT_MEM_DATA; + else + return LLEXT_MEM_RODATA; + case SHT_PREINIT_ARRAY: + return LLEXT_MEM_PREINIT; + case SHT_INIT_ARRAY: + return LLEXT_MEM_INIT; + case SHT_FINI_ARRAY: + return LLEXT_MEM_FINI; + default: + return LLEXT_MEM_COUNT; + } +} + +static size_t llext_manager_layout_sections(uint8_t *elf_buf, uintptr_t vma_base) +{ + elf_ehdr_t *hdr = (elf_ehdr_t *)elf_buf; + elf_shdr_t *shdrs = (elf_shdr_t *)(elf_buf + hdr->e_shoff); + elf_shdr_t *shstr_shdr = shdrs + hdr->e_shstrndx; + const char *shstrtab = (const char *)(elf_buf + shstr_shdr->sh_offset); + + uintptr_t current_vma = vma_base; + enum llext_mem last_region = LLEXT_MEM_COUNT; + + for (int i = 0; i < hdr->e_shnum; i++) { + elf_shdr_t *shdr = shdrs + i; + + if (!(shdr->sh_flags & SHF_ALLOC) || shdr->sh_size == 0) + continue; + + const char *name = shstrtab + shdr->sh_name; + enum llext_mem s_region = llext_manager_get_sec_mem_idx(name, shdr); + + if (s_region == LLEXT_MEM_COUNT) + continue; + + if (last_region != LLEXT_MEM_COUNT && last_region != s_region) { + current_vma = ALIGN_UP(current_vma, PAGE_SZ); + } + last_region = s_region; + + current_vma = ALIGN_UP(current_vma, shdr->sh_addralign); + if (vma_base) { + shdr->sh_addr = current_vma; + } + current_vma += shdr->sh_size; + } + + return current_vma - vma_base; +} + static int llext_manager_update_flags(void __sparse_cache *vma, size_t size, uint32_t flags) { size_t pre_pad_size = (uintptr_t)vma & (PAGE_SZ - 1); @@ -383,6 +478,26 @@ static int llext_manager_link(const char *name, } if (!*llext || mctx->mapped) { + if (!*llext) { + uint8_t *elf_buf = (uint8_t *)mctx->ebl->buf; + size_t total_size = llext_manager_layout_sections(elf_buf, 0); + if (total_size == 0) { + tr_err(&lib_manager_tr, "llext_manager_link: layout sections failed"); + return -EINVAL; + } + + uintptr_t vma_base = llext_manager_alloc_vma(total_size); + if (!vma_base) { + tr_err(&lib_manager_tr, "llext_manager_link: VMA allocation failed"); + return -ENOMEM; + } + + mctx->vma_base = vma_base; + mctx->vma_size = total_size; + + llext_manager_layout_sections(elf_buf, vma_base); + } + /* * Either the very first time loading this module, or the module * is already mapped, we just call llext_load() to refcount it @@ -395,8 +510,15 @@ static int llext_manager_link(const char *name, }; ret = llext_load(ldr, name, llext, &ldr_parm); - if (ret) + if (ret) { + tr_err(&lib_manager_tr, "llext_load failed: ret=%d", ret); + if (mctx->vma_base) { + llext_manager_free_vma(mctx->vma_base, mctx->vma_size); + mctx->vma_base = 0; + mctx->vma_size = 0; + } return ret; + } } /* All code sections */ @@ -426,6 +548,42 @@ static int llext_manager_link(const char *name, mctx->segment[LIB_MANAGER_DATA].addr, mctx->segment[LIB_MANAGER_DATA].size); + /* + * LLEXT classifies SHT_INIT_ARRAY sections as LLEXT_MEM_INIT, not + * LLEXT_MEM_DATA. When a module has an .init_array but an empty (or + * absent) .data section the DATA segment ends up with size=0, which + * causes the adjacency check for .bss in llext_manager_load_module() + * to fail with -EPROTO. Merge the INIT region into the DATA segment + * so that the check sees a contiguous writable range that covers both + * .init_array and .bss. + */ + { + const elf_shdr_t *init_hdr; + + llext_get_region_info(ldr, *llext, LLEXT_MEM_INIT, &init_hdr, NULL, NULL); + if (init_hdr->sh_size > 0) { + if (!mctx->segment[LIB_MANAGER_DATA].size) { + mctx->segment[LIB_MANAGER_DATA].addr = init_hdr->sh_addr; + mctx->segment[LIB_MANAGER_DATA].size = init_hdr->sh_size; + } else { + uintptr_t d_end = mctx->segment[LIB_MANAGER_DATA].addr + + mctx->segment[LIB_MANAGER_DATA].size; + uintptr_t i_end = init_hdr->sh_addr + init_hdr->sh_size; + uintptr_t new_start = + MIN(mctx->segment[LIB_MANAGER_DATA].addr, + init_hdr->sh_addr); + uintptr_t new_end = MAX(d_end, i_end); + + mctx->segment[LIB_MANAGER_DATA].addr = new_start; + mctx->segment[LIB_MANAGER_DATA].size = new_end - new_start; + } + tr_dbg(&lib_manager_tr, + ".init_array merged into .data: start: %#lx size %#x", + mctx->segment[LIB_MANAGER_DATA].addr, + mctx->segment[LIB_MANAGER_DATA].size); + } + } + /* Writable uninitialized data section */ llext_get_region_info(ldr, *llext, LLEXT_MEM_BSS, &hdr, NULL, NULL); mctx->segment[LIB_MANAGER_BSS].addr = hdr->sh_addr; @@ -449,7 +607,12 @@ static int llext_manager_link(const char *name, *mod_manifest = llext_peek(ldr, hdr->sh_offset); } - return *buildinfo && *mod_manifest ? 0 : -EPROTO; + int link_ret = *buildinfo && *mod_manifest ? 0 : -EPROTO; + + if (link_ret) + tr_err(&lib_manager_tr, "llext_manager_link: buildinfo=%p mod_manifest=%p ret=%d", + *buildinfo, *mod_manifest, link_ret); + return link_ret; } /* Count "module files" in the library, allocate and initialize memory for their descriptors */ @@ -618,7 +781,6 @@ static int llext_manager_link_single(uint32_t module_id, const struct sof_man_fw mod_array[entry_index].name, (*mod_manifest)->module.name); return -ENOEXEC; } - return mod_ctx_idx; } @@ -1086,8 +1248,10 @@ int llext_manager_add_library(uint32_t module_id) ret = llext_manager_link_single(module_id + i, desc, ctx, (const void **)&buildinfo, &mod_manifest); - if (ret < 0) + if (ret < 0) { + tr_err(&lib_manager_tr, "llext_manager_link_single failed: %d", ret); return ret; + } } } diff --git a/src/library_manager/llext_manager_dram.c b/src/library_manager/llext_manager_dram.c index 2f6cff2b3501..1cc8fad942f8 100644 --- a/src/library_manager/llext_manager_dram.c +++ b/src/library_manager/llext_manager_dram.c @@ -14,6 +14,8 @@ LOG_MODULE_DECLARE(lib_manager, CONFIG_SOF_LOG_LEVEL); +void llext_manager_free_vma(uintptr_t vma, size_t size); + struct lib_manager_dram_storage { struct ext_library ext_lib; struct lib_manager_mod_ctx *ctx; @@ -332,6 +334,9 @@ int llext_manager_restore_from_dram(void) if (mod[k].llext) llext_unload(&mod[k].llext); + if (mod[k].vma_base) + llext_manager_free_vma(mod[k].vma_base, mod[k].vma_size); + if (mod[k].ebl) rfree(mod[k].ebl); } diff --git a/zephyr/include/sof/shell_llext_load.h b/zephyr/include/sof/shell_llext_load.h new file mode 100644 index 000000000000..4cae2ee86e92 --- /dev/null +++ b/zephyr/include/sof/shell_llext_load.h @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2024 Intel Corporation. + * + * Author: Kai Vehmanen + */ + +/** + * @file shell_llext_load.h + * @brief Shared mailbox structure for the DSP shell "llext_load" command. + * + * The DSP shell command and the Linux "sof-client-llext-load" driver + * communicate through a single ADSP debug window slot identified by the + * ADSP_DW_SLOT_LLEXT_LOAD type. Both sides treat the first 96 bytes of the + * slot as a struct sof_shell_llext_slot. + * + * Handshake protocol + * ------------------ + * 1. DSP shell writes: magic, lib_id, name → state = REQUESTING + * 2. Host driver checks state == REQUESTING, reads lib_id + * 3. Host sets state = DMA_ACTIVE, starts HDA DMA + IPC4 load + * 4a. On success: host writes xfer_bytes, then state = DMA_DONE + * 4b. On failure: host writes result (errno), then state = ERROR + * 5. DSP shell detects state change, reports result, sets state = IDLE + * + * The binary layout must match the Linux copy in shell-llext-shm.h. + */ + +#ifndef __SOF_SHELL_LLEXT_LOAD_H__ +#define __SOF_SHELL_LLEXT_LOAD_H__ + +#include +#include /* ADSP_DW_SLOT_LLEXT_LOAD */ + +#ifndef ADSP_DW_SLOT_LLEXT_LOAD +#define ADSP_DW_SLOT_LLEXT_LOAD 0x4C454C44U /* 'LELD' */ +#endif + +/** + * Magic placed in ->magic to indicate the DSP has initialized the slot. + * Reuses the slot-type value so a single constant identifies both. + */ +#define SOF_SHELL_LLEXT_MAGIC ADSP_DW_SLOT_LLEXT_LOAD + +/** + * Handshake states stored in struct sof_shell_llext_slot::state. + * The DSP writes REQUESTING; the host writes DMA_ACTIVE, DMA_DONE or ERROR. + */ +enum sof_shell_llext_state { + SOF_SHELL_LLEXT_IDLE = 0, /* no request pending */ + SOF_SHELL_LLEXT_REQUESTING = 1, /* DSP ready, waiting for the host to DMA */ + SOF_SHELL_LLEXT_DMA_ACTIVE = 2, /* host: copy / DMA in progress */ + SOF_SHELL_LLEXT_DMA_DONE = 3, /* host: library DMA + IPC load complete */ + SOF_SHELL_LLEXT_ERROR = 4, /* host: load failed, ->result holds errno */ +}; + +/** + * struct sof_shell_llext_slot — placed at offset 0 of the debug window slot. + * + * Total size: 96 bytes (the slot is 4 KB; the remainder is unused). + */ +struct sof_shell_llext_slot { + uint32_t magic; /**< SOF_SHELL_LLEXT_MAGIC when valid */ + uint32_t state; /**< enum sof_shell_llext_state */ + uint32_t lib_id; /**< library slot [1 .. LIB_MANAGER_MAX_LIBS - 1] */ + uint32_t xfer_bytes; /**< bytes transferred (written by host on success) */ + int32_t result; /**< 0 on success, negative errno on error */ + uint32_t reserved[3]; /**< pad, must be zero */ + char name[64]; /**< filename hint, NUL-terminated, display only */ +} __packed; + +#endif /* __SOF_SHELL_LLEXT_LOAD_H__ */ From 646948c172a1cfd3dc5446de1bf078a15f027757 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 09:27:55 +0100 Subject: [PATCH 06/10] probe: add shell output probe mirroring, GDB bridge, and developer tooling Enable mirroring of shell output over probe extraction DMA streams. Add python helper scripts for probe framing, packet decoding, bridge adapters, shell command validation, and GDB debugging, alongside documentation in SHELL.md and GDB.md. --- GDB.md | 371 +++++++++++++++++++ SHELL.md | 431 +++++++++++++++++++++++ app/boards/intel_adsp_cavs25.conf | 2 + scripts/README.md | 29 ++ scripts/README.shell-probe-bridge.md | 100 ++++++ scripts/shell_probe_framing.py | 119 +++++++ scripts/sof-fw-gdb-attach.sh | 180 ++++++++++ scripts/sof-fw-gdb-bridge.py | 95 +++++ scripts/sof-shell-probe-bridge.py | 309 ++++++++++++++++ scripts/sof-shell-probe-compr-adapter.py | 145 ++++++++ scripts/sof-shell-probe-packet-decode.py | 90 +++++ scripts/sof-shell-probe-validate.py | 251 +++++++++++++ src/include/sof/probe/probe.h | 17 + src/probe/probe.c | 39 +- 14 files changed, 2167 insertions(+), 11 deletions(-) create mode 100644 GDB.md create mode 100644 SHELL.md create mode 100644 scripts/README.shell-probe-bridge.md create mode 100755 scripts/shell_probe_framing.py create mode 100755 scripts/sof-fw-gdb-attach.sh create mode 100755 scripts/sof-fw-gdb-bridge.py create mode 100755 scripts/sof-shell-probe-bridge.py create mode 100755 scripts/sof-shell-probe-compr-adapter.py create mode 100755 scripts/sof-shell-probe-packet-decode.py create mode 100755 scripts/sof-shell-probe-validate.py diff --git a/GDB.md b/GDB.md new file mode 100644 index 000000000000..406a74639057 --- /dev/null +++ b/GDB.md @@ -0,0 +1,371 @@ +# SOF Firmware GDB Architecture and Usage + +This document explains how SOF firmware GDB debugging works on Intel ADSP platforms, how the Linux SOF GDB client bridges to the DSP, and how to connect a host GDB session to a running firmware instance. + +It focuses on the Tiger Lake IPC4 flow used on Spider. + +## Quickstart (Spider) + +Use this as a minimal bring-up path once firmware and kernel artifacts are built. + +### DUT checks + +~~~bash +ssh root@spider 'modprobe snd_sof_fw_gdb || true' +ssh root@spider 'lsmod | grep snd_sof_fw_gdb' +ssh root@spider 'find /sys/kernel/debug -maxdepth 4 -type f -name fw_gdb' +~~~ + +### Verify firmware GDB slot + +~~~bash +# From your shell transport on Spider, confirm gdb_stub appears in slot table +sof dbgwin dump +~~~ + +### Start bridge on Spider + +~~~bash +ssh root@spider 'socat TCP-LISTEN:1235,reuseaddr,fork OPEN:/sys/kernel/debug/sof/fw_gdb,rdwr' +~~~ + +If `socat` is not installed on Spider, use the repo bridge helper script: + +~~~bash +# copy once +scp sof/scripts/sof-fw-gdb-bridge.py root@spider:/tmp/ + +# run on DUT +ssh root@spider 'python3 /tmp/sof-fw-gdb-bridge.py --port 1235' +~~~ + +### Connect host GDB + +~~~bash +xt-gdb /home/lrg/work/sof-tgl/build-tgl-shell-llvm/zephyr/zephyr.elf +~~~ + +In the GDB prompt: + +~~~text +target remote spider:1235 +monitor reset +info registers +bt +~~~ + +Or run the one-command helper from the SOF repo root: + +~~~bash +./sof/scripts/sof-fw-gdb-attach.sh --elf build-tgl-gdb-llvm/zephyr/zephyr.elf + +# non-interactive smoke test +./sof/scripts/sof-fw-gdb-attach.sh --elf build-tgl-gdb-llvm/zephyr/zephyr.elf --smoke + +# smoke test and auto-stop remote bridge on exit +./sof/scripts/sof-fw-gdb-attach.sh --elf build-tgl-gdb-llvm/zephyr/zephyr.elf --smoke --cleanup-bridge +~~~ + +VS Code task shortcut: + +1. Run task SOF FW GDB Smoke Attach (Spider) from the workspace task list. + +If attach fails: + +1. Re-check that snd_sof_fw_gdb is loaded. +2. Re-check that gdb_stub is present in sof dbgwin dump output. +3. Ensure only one process has fw_gdb open. + +### Troubleshooting Matrix + +| Symptom | Likely cause | Fix | +| --- | --- | --- | +| modprobe snd_sof_fw_gdb fails | Kernel built without CONFIG_SND_SOC_SOF_DEBUG_FW_GDB | Enable CONFIG_SND_SOC_SOF_DEBUG_FW_GDB, rebuild/deploy snd-sof-fw-gdb.ko, run depmod | +| /sys/kernel/debug/sof/fw_gdb missing | Module not loaded, SOF debugfs not present, or probe failed | Load module, verify debugfs mount, check dmesg for SOF/fw_gdb probe errors | +| GDB attach hangs or disconnects | Bridge process exited or fw_gdb not returning data | Keep socat in foreground first, verify fw_gdb path and permissions, retry target remote | +| GDB commands return no firmware data | Firmware image lacks CONFIG_GDBSTUB | Rebuild firmware with debug overlay or CONFIG_GDBSTUB=y and redeploy | +| fw_gdb opens but transport fails | No gdb_stub slot in debug window | Verify slot table with sof dbgwin dump and deploy a GDBSTUB-capable firmware build | +| GDB reports device busy | Another process already opened fw_gdb | Stop competing process and reconnect with a single bridge/client | +| Attach works but breakpoints fail | Software breakpoints unsupported in this path | Use hardware breakpoints, for example hbreak | +| Behavior is flaky after module reload | DSP or client state not clean | Reboot DUT, reload modules, and start bridge before attach | + +## 1. Big Picture + +SOF firmware debugging uses a layered transport: + +1. Host GDB speaks the Remote Serial Protocol packets. +2. Linux kernel SOF GDB client exposes a debugfs endpoint named fw_gdb. +3. The client proxies bytes into DSP shared-memory ring buffers in the debug window. +4. Firmware GDB stub consumes those packets, inspects DSP state, and returns replies. + +The debugfs file open operation also sends an ENTER_GDB IPC message to firmware. + +~~~mermaid +flowchart LR + A[Host GDB] --> B[Host bridge process] + B --> C[/sys/kernel/debug/sof/fw_gdb] + C --> D[Linux snd_sof_fw_gdb client] + D --> E[Debug window ring buffers] + E --> F[Firmware GDB stub] + F --> G[DSP registers and memory] +~~~ + +## 2. Firmware Side Architecture + +### 2.1 GDB Stub Core + +The firmware contains a GDB protocol parser and exception handling integration. + +Main behavior: + +1. Exception or breakpoint enters GDB handler. +2. Firmware exchanges packets in remote protocol format. +3. Commands include register and memory read/write, continue, single-step, and breakpoints. + +### 2.2 Transport Backend on Intel ADSP + +On Intel ADSP Zephyr platforms, the GDB backend uses shared SRAM rings located in the debug window region. + +Transport details: + +1. Two circular rings are used: host-to-fw and fw-to-host. +2. Ring metadata includes head and tail pointers. +3. A debug window slot type is marked as GDB_STUB. + +### 2.3 IPC Trigger into GDB Mode + +For IPC4, firmware handles ENTER_GDB global message and sets an internal enter-gdb flag. + +Kernel client sends this message when fw_gdb is opened. + +~~~mermaid +sequenceDiagram + participant K as Linux fw_gdb client + participant I as IPC4 global message + participant F as SOF firmware + participant S as GDB stub + + K->>I: ENTER_GDB request + I->>F: global message dispatch + F->>F: set enter-gdb flag + F->>S: route execution to stub path +~~~ + +## 3. Linux Kernel Side Architecture + +### 3.1 Module and Registration + +Kernel feature flag: + +1. CONFIG_SND_SOC_SOF_DEBUG_FW_GDB enables the fw_gdb client. +2. Module produced is snd-sof-fw-gdb. +3. SOF core registers an auxiliary client named fw_gdb. + +### 3.2 Debugfs Endpoint + +The client creates: + +1. /sys/kernel/debug/sof/fw_gdb + +Important semantics: + +1. Only one opener at a time. +2. Open sends ENTER_GDB IPC to firmware. +3. Read and write move bytes via debug-window rings. +4. Poll support is present for readability and writability. + +### 3.3 Ring Access Constraints + +The kernel client expects: + +1. Firmware debug window has a GDB_STUB slot. +2. Slot offsets are discoverable through IPC4 debug slot helpers. + +If no GDB_STUB slot exists, reads and writes fail. + +## 4. End-to-End Runtime Data Flow + +~~~mermaid +sequenceDiagram + participant G as Host GDB + participant S as Socat bridge + participant D as debugfs fw_gdb + participant K as snd_sof_fw_gdb + participant R as DSP ring buffers + participant F as FW GDB stub + + G->>S: target remote connect + S->>D: open fw_gdb + D->>K: file open callback + K->>F: ENTER_GDB IPC + G->>S: packet, for example read registers + S->>D: write packet bytes + D->>K: write callback + K->>R: host-to-fw ring write + F->>R: parse request and prepare reply + K->>R: fw-to-host ring read + D->>S: read callback returns bytes + S->>G: reply packet bytes +~~~ + +## 5. Spider Prerequisites Checklist + +Before trying to attach GDB, verify all of the following: + +1. Firmware is built with GDB stub enabled. +2. Firmware debug window reports a GDB_STUB slot. +3. Linux has snd_sof_fw_gdb module available and loaded. +4. debugfs is mounted and /sys/kernel/debug/sof/fw_gdb exists. +5. DSP firmware is running and SOF stack is healthy. + +Quick checks on DUT: + +~~~bash +uname -r +lsmod | grep snd_sof_fw_gdb +find /sys/kernel/debug -maxdepth 4 -type f -name fw_gdb +~~~ + +Quick slot inspection via shell path: + +~~~bash +# Use existing shell transport and run: +sof dbgwin dump +# Confirm a line with name gdb_stub is present +~~~ + +## 6. Build and Deploy Flow + +### 6.1 Firmware with GDB Stub + +Use a build overlay that enables GDBSTUB. In this tree, debug_overlay.conf already sets: + +1. CONFIG_GDBSTUB=y +2. CONFIG_GDBSTUB_ENTER_IMMEDIATELY=n + +Build and deploy firmware to the IPC4 path used by Spider. + +### 6.2 Kernel fw_gdb Client + +Ensure Linux config enables: + +1. CONFIG_SND_SOC_SOF_DEBUG_FW_GDB=m or y + +Then build SOF kernel modules and deploy snd-sof-fw-gdb to Spider module tree, followed by depmod and modprobe. + +## 7. Host Connection Procedure + +Run a bridge on Spider from debugfs to TCP: + +~~~bash +socat TCP-LISTEN:1235,reuseaddr,fork OPEN:/sys/kernel/debug/sof/fw_gdb,rdwr +~~~ + +If `socat` is unavailable on DUT: + +~~~bash +python3 /tmp/sof-fw-gdb-bridge.py --port 1235 +~~~ + +On host machine, launch Xtensa GDB with firmware ELF: + +~~~bash +xt-gdb path/to/zephyr.elf +~~~ + +Inside GDB: + +~~~text +target remote spider:1235 +~~~ + +Then use normal remote debugging commands: + +1. info registers +2. bt +3. x and p memory/register reads +4. continue +5. stepi +6. hbreak for hardware breakpoints + +## 8. Expected Failure Modes and Fixes + +### 8.1 snd_sof_fw_gdb module missing + +Symptom: + +1. modprobe snd_sof_fw_gdb fails with module not found. + +Fix: + +1. Enable CONFIG_SND_SOC_SOF_DEBUG_FW_GDB. +2. Rebuild and deploy module. + +### 8.2 fw_gdb file missing + +Symptom: + +1. /sys/kernel/debug/sof/fw_gdb does not exist. + +Fix: + +1. Load snd_sof_fw_gdb. +2. Verify SOF core debugfs root exists. +3. Confirm SOF probe completed without crash. + +### 8.3 No gdb_stub slot in debug window + +Symptom: + +1. Kernel client cannot locate slot offsets and communication fails. + +Fix: + +1. Rebuild firmware with CONFIG_GDBSTUB enabled. +2. Reboot and re-check slot table. + +### 8.4 GDB disconnects quickly + +Potential causes: + +1. Bridge process exits. +2. No data returned due to transport misconfiguration. +3. Firmware not in GDB path. + +Fix: + +1. Keep socat running in foreground first. +2. Check dmesg and SOF logs. +3. Re-open fw_gdb and retry attach. + +## 9. Relationship to SOF Shell TTY Path + +The shell transport and GDB transport are different clients: + +1. Shell uses serial client and ttysof0. +2. GDB uses fw_gdb debugfs client and GDB_STUB ring buffers. + +Both can coexist in a debug build, but they are independent endpoints. + +## 10. Practical Bring-up Plan + +Use this order to minimize churn: + +1. Enable and deploy fw_gdb kernel module. +2. Build and deploy GDBSTUB firmware image. +3. Verify gdb_stub debug-window slot appears. +4. Verify fw_gdb debugfs node exists. +5. Start socat bridge and connect GDB. +6. Validate basic commands: registers, memory read, continue, break, backtrace. + +~~~mermaid +flowchart TD + A[Deploy fw_gdb kernel module] --> B[Deploy GDBSTUB firmware] + B --> C{gdb_stub slot visible} + C -- no --> B + C -- yes --> D{fw_gdb debugfs exists} + D -- no --> A + D -- yes --> E[Start socat bridge] + E --> F[Connect host GDB target remote] + F --> G[Run smoke debug commands] +~~~ diff --git a/SHELL.md b/SHELL.md new file mode 100644 index 000000000000..9f706f87a5f5 --- /dev/null +++ b/SHELL.md @@ -0,0 +1,431 @@ +# SOF Zephyr Shell Commands + +This document describes all SOF-specific shell commands available in the Zephyr RTOS environment. These commands are grouped under the `sof` parent command and provide diagnostic visibility into the firmware's runtime state. + +## Usage + +Access the Zephyr shell through the QEMU terminal or a hardware UART console. Prefix all commands with `sof`. + +```shell +uart:~$ sof [arguments] +``` + +## Available SOF Commands + +### 1. `sof version` +- **Description**: Prints the firmware version details. +- **Usage**: `sof version` +- **Output**: Major/Minor/Micro version, Git tag, and source hash. + +### 2. `sof module_heap_usage` +- **Description**: Dumps heap memory usage for all active audio modules. +- **Usage**: `sof module_heap_usage` +- **Output**: Component ID, current heap usage, and high-water mark (HWM) in bytes. + +### 3. `sof pipeline_list` +- **Description**: Lists all active audio pipelines in the system. +- **Usage**: `sof pipeline_list` +- **Output**: Pipeline ID, Core affinity, Status, Priority, and Period (us). + +### 4. `sof module_list` +- **Description**: Lists all instantiated audio modules (components). +- **Usage**: `sof module_list` +- **Output**: Component ID, Module Type, State, Pipeline ID, and Core affinity. + +### 5. `sof vpage_status` +- **Description**: Reports the status of the virtual page allocator. +- **Usage**: `sof vpage_status` +- **Output**: Base address, total/free pages, and a list of active virtual page allocations. +- **Dependency**: Requires `CONFIG_SOF_VREGIONS=y`. + +### 6. `sof vregion_status` +- **Description**: Reports status and metrics for all active virtual memory regions. +- **Usage**: `sof vregion_status` +- **Output**: Region base addresses, sizes, lifetime bytes used/free, and current reference counts. +- **Dependency**: Requires `CONFIG_SOF_VREGIONS=y`. + +### 7. `sof test_inject_sched_gap` +- **Description**: Injects a timing delay into the audio scheduling domain for stress testing. +- **Usage**: `sof test_inject_sched_gap [usec]` +- **Arguments**: `usec` (optional, default 1500) - microseconds to block the domain. +- **Warning**: Not reliable on SMP systems without explicit cross-core support. + +--- + +## Enabling Shell Commands + +Ensure the following Kconfig symbols are enabled in your build configuration: +- `CONFIG_SHELL=y` +- `CONFIG_SOF_VREGIONS=y` (for `vpage` and `vregion` commands) + +--- + +## Functional Areas Missing Shell Coverage + +Tracking list of subsystems that lack runtime shell visibility for control, +debug or testing. Items get ticked off as commands land on `topic/shell`. + +### Control + +| Area | Suggested commands | Status | +|---|---|---| +| **kcontrols / mixer** | `kctl_list`, `kctl_get `, `kctl_set ` | **DONE (task 8)** — `kctl_list` decodes module names + kind for every component. `kctl_get/set` deferred (per-module IPC4 large_config blobs — use host tools). | +| Module runtime config | `mod_config_get/set ` | TODO | +| Stream / copier | `stream_list`, `stream_pause/resume `, `copier_gain_set` | TODO | +| Clocks (extend `clock_status`) | `clock_set `, `clock_force ` | TODO | +| Power management | `pm_state`, `pm_force `, `pg_status`, `idle_stats` | TODO | +| Cache | `dcache_flush `, `dcache_inv`, `icache_inv` | TODO | +| Watchdog | `wdt_status`, `wdt_kick`, `wdt_disable` | TODO | +| **DAI / link control** | `dai_list`, `dai_status `, `dai_trigger`, `dai_loopback` | **DONE (task 7)** — `dai_list` covers introspection; trigger/loopback deferred (writeable, needs careful tplg coordination). | +| **DMA** | `dma_list`, `dma_chan_status `, `dma_stop` | **DONE (task 7)** — `dma_status` covers list+per-channel. `dma_stop` deferred (would corrupt active stream). | + +### Debug + +| Area | Suggested commands | Status | +|---|---|---| +| **IPC counters / last message** | `ipc_stats`, `ipc_last` | **DONE (task 1)** | +| IPC inject | `ipc_inject `, `ipc_queue` | TODO | +| Audio buffers | `buffer_list`, `buffer_info ` | **DONE (task 2)** | +| **Scheduler** | `sched_tasks`, `sched_load`, `task_info ` | **DONE (task 3)** | +| Logging / trace | `log_status`, `mtrace_dump` (snapshot) | **DONE (task 4)** — runtime per-source `log_level` deferred (needs `CONFIG_LOG_RUNTIME_FILTERING`, see notes) | +| **Telemetry / perf** | `perf_status`, `perf_status reset`, `perf_status start/stop/pause` | **DONE (task 6)** | +| Notifications | `notify_subscribers`, `notify_stats` | TODO | +| **Debug window / mailbox** | `dbgwin_dump `, `mailbox_hex` | **DONE (task 5)** | +| Crash / panic | `crash_log`, `crash_clear`, `panic_info`, `bt`, `regs` | TODO | +| Heap walk | `heap_walk `, `heap_blocks`, `obj_pool_stats` | TODO | +| Probes | `probe_init`, `probe_add `, `probe_remove`, `probe_dma_status` | TODO | +| Locks / IRQ / IDC | `mutex_stats`, `irq_stats`, `idc_stats` | TODO | + +### Testing / fault injection + +| Area | Suggested commands | Status | +|---|---|---| +| Fault injection | `test_alloc_fail `, `test_ipc_drop`, `test_dma_stall`, `test_xrun `, `test_panic` | TODO | +| Self-test | `selftest dma`, `selftest mem`, `selftest cache`, `selftest mmu`, `selftest llext` | TODO | +| Module unit-tests | `test_module ` | TODO | +| Loopback / signal gen | `tone_play `, `loopback_start `, `pcm_capture_dump` | TODO | +| Mock IPC4 payloads | `ipc_replay ` (via DMA slot) | TODO | +| Coverage / hooks | `cov_dump`, `assert_count`, `assert_clear` | TODO | + +### Quick-win order + +1. **`ipc_stats` / `ipc_last`** — DONE. +2. **`buffer_list` / `buffer_info`** — DONE. +3. **`sched_tasks` / `sched_load`** — DONE. +4. **`log_status` / `mtrace_dump`** — DONE. +5. **`mailbox_hex` / `dbgwin_dump`** — DONE (was originally `crash_log`/`bt`; pivoted because SOF panic.c isn't built on Zephyr and `bt` of a running CPU from itself isn't meaningful). +6. **`perf_status`** — DONE. +7. **`dai_list` / `dma_status`** — DONE. +8. **`kctl_get/set`** — DONE (`kctl_list` only; values stay on host). + +--- + +## Task 1 — `ipc_stats` / `ipc_last` + +### Commands + +| Command | Description | +|---|---| +| `sof ipc_stats` | Print RX/TX counters (`rx_count`, `rx_errors`, `tx_count`, `tx_direct_count`, `tx_errors`). | +| `sof ipc_stats reset` | Clear all counters. | +| `sof ipc_last` | Print the most recent RX and TX `pri`/`ext` headers and their platform-cycle timestamps. | + +### Implementation + +- Public API in `src/include/sof/ipc/common.h`: + - `struct ipc_stats` + - `ipc_stats_record_rx(pri, ext)` + - `ipc_stats_record_tx(pri, ext, direct, err)` + - `ipc_stats_inc_rx_error()` + - `ipc_stats_get(out)` / `ipc_stats_reset()` +- Storage and locking in `src/ipc/ipc-common.c` (single global, protected by + `ipc->lock`, IPC-version agnostic). +- RX hook: `ipc_platform_do_cmd()` in `src/ipc/ipc-zephyr.c`, just before + dispatch — captures the IPC3/IPC4 `pri`/`ext` words. +- TX hooks: `ipc_platform_send_msg()` and `ipc_platform_send_msg_direct()` in + the same file, after the platform send returns. +- Error hook: `ipc_cmd()` default branch in `src/ipc/ipc4/handler-kernel.c` + for unknown message targets. +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- Counters are 32-bit; they wrap at ~4 billion messages. +- IPC3 dispatch errors are not yet routed through `ipc_stats_inc_rx_error()`. +- A future task can add a small ring buffer of last-N IPC headers and + per-target counters (FW_GEN_MSG vs MODULE_MSG, plus per opcode). + +## Task 2 — `buffer_list` / `buffer_info` + +### Commands + +| Command | Description | +|---|---| +| `sof buffer_list` | List every audio buffer in the pipeline with source/sink component IDs, size, avail, free, channels, rate and frame format. | +| `sof buffer_info ` | Detailed info for a single buffer: source/sink comps, core, flags, size/avail/free bytes, rptr, wptr, channels, rate, frame format. | + +### Implementation + +- Buffers are enumerated by walking `ipc->comp_list`; for each + `COMP_TYPE_COMPONENT` we walk its `bsink_list` via + `comp_dev_get_first_data_consumer()` / + `comp_dev_get_next_data_consumer()`. Each buffer therefore appears + exactly once (it is the sink of exactly one source component) and the + same enumeration works on both IPC3 and IPC4. +- `buf_get_id()` from [src/include/sof/audio/buffer.h](src/include/sof/audio/buffer.h) + is used as the buffer identifier. +- Stream metrics use the existing `audio_stream_get_*()` accessors from + [src/include/sof/audio/audio_stream.h](src/include/sof/audio/audio_stream.h). +- New Kconfig `CONFIG_SOF_SHELL_BUFFER_INFO` (default `y`). +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- Today only fill-level snapshots are reported; high-water mark and + underrun/overrun counters are not tracked in `comp_buffer` and would + require new instrumentation. +- `buffer_info` does not yet decode `flags` symbolically. +- A future enhancement could add `--core ` filtering and per-buffer + topology graph output. + +## Task 3 — `sched_tasks` / `sched_load` + +### Commands + +| Command | Description | +|---|---| +| `sof sched_tasks` | List every task across all SOF schedulers (LL timer, LL DMA, EDF, DP, TWB) with type, core, priority, state, flags and uid. | +| `sof sched_load` | Per-task cycle counters (cycles_cnt, cycles_sum, cycles_max, derived average) plus aggregate totals. Pairs with `test_inject_sched_gap`. | + +### Implementation + +- New optional op `scheduler_dump_tasks(data, cb, ctx)` added to + `struct scheduler_ops` in + [src/include/sof/schedule/schedule.h](src/include/sof/schedule/schedule.h). +- Implemented for the Zephyr schedulers under their own locks: + - [src/schedule/zephyr_ll.c](src/schedule/zephyr_ll.c) (LL timer / LL DMA) + - [src/schedule/zephyr_twb_schedule.c](src/schedule/zephyr_twb_schedule.c) + - [src/schedule/zephyr_dp_schedule.c](src/schedule/zephyr_dp_schedule.c) +- Shell walks the global scheduler list via `arch_schedulers_get()` and + invokes the op on every scheduler that provides one; schedulers + without an implementation are silently skipped. +- Cycle counters are read from existing `task->cycles_sum`, + `task->cycles_max`, `task->cycles_cnt` fields already maintained by + the schedulers. +- New Kconfig `CONFIG_SOF_SHELL_SCHED_INFO` (default `y`). +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- The xtos LL scheduler is not yet covered (not built on Zephyr ACE + targets). +- `task_info ` lookup, deadline-miss counts and per-core + aggregation could be added on top of the same op. + +## Task 4 — `log_status` / `mtrace_dump` + +### Commands + +| Command | Description | +|---|---| +| `sof log_status` | List every Zephyr log backend (idx, internal id, active state, name) plus the total number of registered log sources in the local domain. Read-only. | +| `sof mtrace_dump` | Print the unread portion of the ADSP mtrace SRAM ring buffer as a snapshot, *without* advancing `host_ptr`. Safe to use while host-side `mtrace-reader.py` is running. | + +### Implementation + +- `log_status` uses Zephyr's public log backend API + (`log_backend_count_get()`, `log_backend_get()`, + `log_backend_is_active()`, `log_backend_id_get()`, + `log_src_cnt_get()`); no new state is added. +- `mtrace_dump` re-acquires the existing mtrace slot: + - With `CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER=y` (default on PTL/MTL/LNL): + via `adsp_dw_request_slot()` with the same descriptor type + (`ADSP_DW_SLOT_DEBUG_LOG | core 0`); the slot manager returns the + already-allocated slot. + - Otherwise: directly indexes + `ADSP_DW->slots[ADSP_DW_SLOT_NUM_MTRACE]`. + - The slot layout (`{host_ptr, dsp_ptr, data[]}`) mirrors the one in + [zephyr/subsys/logging/backends/log_backend_adsp_mtrace.c](../../zephyr/subsys/logging/backends/log_backend_adsp_mtrace.c). + - We read from `host_ptr` to `dsp_ptr` byte-by-byte and write to the + shell, but never store back to `host_ptr`, so the host-side + consumer keeps seeing the same bytes. +- Two new Kconfigs (default `y`): + - `CONFIG_SOF_SHELL_LOG_INFO` (depends on `LOG`) + - `CONFIG_SOF_SHELL_MTRACE_DUMP` (depends on `LOG_BACKEND_ADSP_MTRACE`) +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- Per-source runtime `log_level` setting was deliberately deferred: it + requires `CONFIG_LOG_RUNTIME_FILTERING=y` (extra per-call overhead) + and Zephyr already ships an equivalent `log` shell command + (`CONFIG_LOG_CMDS=y`). If we ever need it we should reuse Zephyr's + command rather than reimplement it. +- `mtrace_dump` shows raw text exactly as the backend formatted it; a + future option could format output in pages or filter by severity. +- A `mtrace_dump --consume` mode (advance `host_ptr`) is intentionally + not provided to avoid silently breaking host-side tooling. + +## Task 5 — `mailbox_hex` / `dbgwin_dump` + +### Commands + +| Command | Description | +|---|---| +| `sof mailbox_hex` | List the four SOF mailbox regions (exception, dspbox, hostbox, debug) with their base address and size. | +| `sof mailbox_hex [off] [len]` | Hex-dump a mailbox region; offset and length are clamped to the region size. Default length 256 bytes. | +| `sof dbgwin_dump` | List all 15 ADSP debug-window slot descriptors (resource_id, type, vma, decoded type name, core). | +| `sof dbgwin_dump [len]` | Hex-dump a single slot (max `ADSP_DW_SLOT_SIZE` = 4096 bytes); default length 256. | + +### Implementation + +- `mailbox_hex` uses the `MAILBOX_*_BASE` / `MAILBOX_*_SIZE` macros + from [src/include/sof/lib/mailbox.h](src/include/sof/lib/mailbox.h); + the four region records are a static table. +- `dbgwin_dump` re-derives the window 2 base from the device tree + (`mem_window2`) plus `WIN2_OFFSET`, mirrors + `struct adsp_debug_window` from + [zephyr/soc/intel/intel_adsp/common/debug_window.c](../../zephyr/soc/intel/intel_adsp/common/debug_window.c) + and reads through an uncached pointer so we always see the + slot-manager state (works whether or not + `CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER=y`). +- A small shared `sof_shell_hex_dump()` helper handles the 16-byte + hex+ASCII rows and is built whenever either command is enabled. +- Two new Kconfigs (default `y`): + - `CONFIG_SOF_SHELL_MAILBOX_HEX` + - `CONFIG_SOF_SHELL_DBGWIN_DUMP` (depends on `SOC_FAMILY_INTEL_ADSP`) +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- The original quick-win was `crash_log`/`bt`; pivoted because SOF's + in-tree `panic_dump()` is not compiled on Zephyr (Zephyr installs + its own fatal handler) and a running shell can't backtrace its own + CPU after a panic. `mailbox_hex exception` still surfaces whatever + the platform-specific fatal path leaves there, so the same + diagnostic intent is covered as far as it can be from a live shell. +- A future `panic_decode` could parse a known on-target oops layout + (Zephyr coredump or telemetry slot) once one is standardised on + ACE. +- `dbgwin_dump` is read-only. We do not implement a write/seize + command to avoid corrupting host-visible state. + +## Task 6 — `perf_status` + +### Commands + +| Command | Description | +|---|---| +| `sof perf_status` | Print the SOF telemetry performance state (`disabled`/`stopped`/`started`/`paused`) and per-active-core systick counters (`count`, `last_time_elapsed`, `max_time_elapsed`, `avg_kcps`, `peak_kcps`, plus 4k/8k peak utilization). | +| `sof perf_status reset` | Call `reset_performance_counters()` to zero all counters. | +| `sof perf_status start` | Call `enable_performance_counters()` and set state to `STARTED`. | +| `sof perf_status stop` / `pause` | Transition state to `STOPPED` or `PAUSED` (stops sampling without zeroing counters). | + +### Implementation + +- Reads per-core systick info via + `telemetry_get_systick_info_ptr()` (with + `CONFIG_INTEL_ADSP_DEBUG_SLOT_MANAGER`) or directly from + `ADSP_DW->slots[SOF_DW_TELEMETRY_SLOT]` otherwise. +- Iterates only cores in `cpu_enabled_cores()` so the output matches + the active topology. +- Uses the existing `perf_meas_get_state()` / + `perf_meas_set_state()` / + `enable_performance_counters()` / + `reset_performance_counters()` API from + [src/include/sof/debug/telemetry/performance_monitor.h](src/include/sof/debug/telemetry/performance_monitor.h); + no new state added. +- New Kconfig `CONFIG_SOF_SHELL_PERF_STATUS` (default `y`, + depends on `SOF_TELEMETRY`). +- Shell command in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- We deliberately do not dump the full per-component + `perf_data_item_comp` array yet: it can grow large + (`CONFIG_MEMORY_WIN_3_SIZE` / item size, ~hundreds of items on PTL) + and would require a heap allocation. A future `perf_components` / + `perf_status -v` could iterate `performance_data_bitmap` and stream + one row per occupied slot. +- Zephyr already provides `kernel cpu_load` and `kernel threads`; + `cpu_load` was therefore not duplicated here. + +## Task 7 — `dai_list` / `dma_status` + +### Commands + +| Command | Description | +|---|---| +| `sof dai_list` | Iterate `dai_get_device_list()` and print, per DAI, the Zephyr device name, decoded type (ssp/dmic/hda/alh/uaol/sai/esai/...), index, current channel count, sample rate, format and word size, plus per-direction fifo address, fifo depth, DMA handshake id and stream id. | +| `sof dma_status` | List every SOF DMA controller (`dma_info_get()`), with id, channel count, busy count, caps/devs bitmasks, base address and Zephyr device name. | +| `sof dma_status ` | Walk all channels of one controller, calling `sof_dma_get_status()` on each. | +| `sof dma_status ` | Status of a single channel: busy/idle, direction, pending/free bytes, read/write positions, total_copied. | + +### Implementation + +- `dai_list` uses `dai_get_device_list()` from + [src/include/sof/lib/dai-zephyr.h](src/include/sof/lib/dai-zephyr.h) + and the Zephyr DAI API + (`dai_config_get()`, `dai_get_properties()`); it falls back to + TX-only or RX-only `config_get()` when `DAI_DIR_BOTH` is not + supported by a driver. +- `dma_status` walks `sof_get()->dma_info->dma_array[]` (via + `dma_info_get()` from + [zephyr/include/sof/lib/dma.h](zephyr/include/sof/lib/dma.h)) + and calls `sof_dma_get_status()` per channel; this re-uses the + same Zephyr `dma_get_status()` path the DSP itself uses, so the + numbers exactly match runtime audio state. +- Two new Kconfigs (default `y`, both depend on + `ZEPHYR_NATIVE_DRIVERS`): + - `CONFIG_SOF_SHELL_DAI_LIST` + - `CONFIG_SOF_SHELL_DMA_STATUS` +- Shell commands in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- Read-only on purpose. `dai_trigger`, `dai_loopback`, `dma_stop` + were intentionally not added in this pass — they would corrupt + in-flight streams and require coordination with topology / IPC + state machines. Pair with the existing `pipeline_state` (gated by + `CONFIG_SOF_SHELL_PIPELINE_OPS`) for stream control. +- `dma_status` only iterates SOF-registered DMACs. Zephyr also + ships its own `dma` shell when `CONFIG_DMA_SHELL=y`, but that one + walks Zephyr DMA devices and exposes raw register pokes, so the + two are complementary. + +## Task 8 — `kctl_list` + +### Commands + +| Command | Description | +|---|---| +| `sof kctl_list` | Walk every component in the IPC topology and print `comp_id`, `pipeline_id`, `core`, the decoded module name (`volume`, `gain`, `mixin`, `mixout`, `eqiir`, `src`, ...), a coarse `kind` tag for control-bearing modules (`volume` / `mixer` / `blob` / `config`) and the current `comp_state`. | + +### Implementation + +- Module-adapter components all share `SOF_COMP_MODULE_ADAPTER` for + `drv->type`, so the only stable per-module label available in + firmware is the UUID name string from + `cd->drv->tctx->uuid_p->name` (the same name the LDC tool prints). + `kctl_drv_name()` reads that, `kctl_drv_kind()` maps known module + names to a coarse control-family tag. +- New Kconfig (default `y`, depends on `SHELL`): + `CONFIG_SOF_SHELL_KCTL_LIST`. +- Shell command in [zephyr/sof_shell.c](zephyr/sof_shell.c). + +### Notes / follow-ups + +- Read-only on purpose. `kctl_get` / `kctl_set` are intentionally + not implemented in firmware. Control values flow through + per-module IPC4 large_config blobs + (`set_configuration` / `get_configuration` in + [src/include/module/module/interface.h](src/include/module/module/interface.h)), + each with their own `config_id` namespace and TLV layout. + Marshalling that from the shell would essentially duplicate the + host-side tplg / IPC code path. Use `tinymix` / + [sof-ctl](tools/ctl) on the host instead, and pair with + `sof module_status` for raw component state. +- This concludes the documented quick-win list. Future shell + commands should follow the same pattern: small, read-only, + Kconfig-gated, and complementary to (not a replacement for) the + host control plane. diff --git a/app/boards/intel_adsp_cavs25.conf b/app/boards/intel_adsp_cavs25.conf index 7cd938ec7ff8..a19e716d7702 100644 --- a/app/boards/intel_adsp_cavs25.conf +++ b/app/boards/intel_adsp_cavs25.conf @@ -3,6 +3,8 @@ CONFIG_RIMAGE_SIGNING_SCHEMA="tgl-cavs" # SOF / IPC configuration CONFIG_IPC_MAJOR_4=y +CONFIG_PROBE_DMA_MAX=8 +CONFIG_SOF_SHELL_PROBE_MIRROR=y # SOF / audio pipeline and module settings CONFIG_COMP_ARIA=y diff --git a/scripts/README.md b/scripts/README.md index 30441422899b..6bfd3d7707ef 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -99,6 +99,35 @@ Automated verifier for executing firmware builds under QEMU emulation. * Supported platforms are: `imx8`, `imx8x`, `imx8m`. * Runs all supported platforms by default if none are provided. +## Firmware GDB Helpers + +For Spider and similar DUTs, helper scripts are available for SOF fw_gdb transport and host attach. + +### DUT bridge (`sof-fw-gdb-bridge.py`) + +Run this on the DUT to bridge `/sys/kernel/debug/sof/fw_gdb` to a TCP port: + +```bash +python3 ./scripts/sof-fw-gdb-bridge.py --port 1235 +``` + +### Host one-command attach (`sof-fw-gdb-attach.sh`) + +This script: + +* verifies DUT and `fw_gdb` node +* copies and starts the DUT bridge script over SSH +* launches Xtensa GDB and connects with `target remote` + +```bash +./scripts/sof-fw-gdb-attach.sh --elf build-tgl-gdb-llvm/zephyr/zephyr.elf + +# Non-interactive smoke run and auto-cleanup of DUT bridge +./scripts/sof-fw-gdb-attach.sh --elf build-tgl-gdb-llvm/zephyr/zephyr.elf --smoke --cleanup-bridge +``` + +VS Code includes a one-click task named `SOF FW GDB Smoke Attach (Spider)`. + ## SDK Support There is some SDK support in this directory for speeding up or simplifying tasks with multiple steps. diff --git a/scripts/README.shell-probe-bridge.md b/scripts/README.shell-probe-bridge.md new file mode 100644 index 000000000000..fea59066a4ca --- /dev/null +++ b/scripts/README.shell-probe-bridge.md @@ -0,0 +1,100 @@ +# Shell Probe Bridge + +`sof-shell-probe-bridge.py` bridges SOF shell PTY traffic to a framed byte stream +that can be carried by probe/compressed PCM transport. + +## Why + +The existing shell path uses ADSP memory-window transport. This bridge isolates +shell framing/chunking/flow-control so compressed PCM transport can be wired in +without changing shell command parsing behavior. + +## Frame format + +Frame encoding is implemented in `shell_probe_framing.py`: + +- Magic: `SPBR` +- Version: `1` +- Types: `DATA`, `CREDIT`, `RESET` +- Sequence number per `DATA` frame +- 16-bit payload length +- Credit-based flow control for large output + +## Bridge usage + +The bridge is transport-agnostic and expects two files/FIFOs: + +- `--framed-in`: bytes heading toward shell input +- `--framed-out`: bytes coming from shell output + +Example: + +```bash +mkfifo /tmp/shell_probe.in /tmp/shell_probe.out +./sof/scripts/sof-shell-probe-bridge.py \ + --dut spider \ + --framed-in /tmp/shell_probe.in \ + --framed-out /tmp/shell_probe.out +``` + +A companion process should connect these FIFOs to compressed PCM endpoints. + +## Compressed PCM adapter + +`sof-shell-probe-compr-adapter.py` provides that companion process. It does not +hardcode tinycompress ioctls; instead it supervises helper commands so existing +or future compressed-PCM tools can be reused. + +Supported placeholders in helper command templates: + +- `{capture_dev}` +- `{playback_dev}` +- `{frame_in}` +- `{frame_out}` + +Example: + +```bash +mkfifo /tmp/shell_probe.in /tmp/shell_probe.out +./sof/scripts/sof-shell-probe-compr-adapter.py \ + --frame-in /tmp/shell_probe.in \ + --frame-out /tmp/shell_probe.out \ + --capture-dev /dev/snd/comprC1D0 \ + --playback-dev /dev/snd/comprC1D1 \ + --rx-cmd "my_capture_tool --dev {capture_dev} --stdout" \ + --tx-cmd "my_playback_tool --dev {playback_dev} --stdin" +``` + +## Framed transport validator + +`sof-shell-probe-validate.py` validates command/response handling over the +framed shell/probe transport. + +```bash +./sof/scripts/sof-shell-probe-validate.py --dut spider +``` + +It uses the same command-pass/fail style as `sof-shell-validate.py` and checks +that chunking plus credit-based flow control work while running shell commands. + +## UART + probe mirror workflow + +With `CONFIG_SOF_SHELL_PROBE_MIRROR=y`, shell output is mirrored to probe +extraction packets while UART shell interaction stays active. + +Typical usage: + +1. Terminal A (interactive shell on UART): + +```bash +picocom /dev/ttysof0 +``` + +2. Terminal B (capture and decode mirrored shell output from probes): + +```bash + | ./sof/scripts/sof-shell-probe-packet-decode.py +``` + +`` is any command that reads bytes from the probe +compressed capture endpoint and writes them to stdout. diff --git a/scripts/shell_probe_framing.py b/scripts/shell_probe_framing.py new file mode 100755 index 000000000000..b6b0210bdcfc --- /dev/null +++ b/scripts/shell_probe_framing.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +"""Shell/probe transport framing helpers. + +This module defines a small binary frame format suitable for carrying shell +traffic over probe transports that may need explicit chunking, sequencing and +credit-based flow control. +""" + +from __future__ import annotations + +import dataclasses +import io +import struct +from typing import Generator, Iterable + +MAGIC = 0x53504252 # 'SPBR' = Shell Probe BRidge +VERSION = 1 + +TYPE_DATA = 1 +TYPE_CREDIT = 2 +TYPE_RESET = 3 + +FLAG_EOC = 0x0001 # End-of-command / logical message marker + +_HEADER = struct.Struct(">IBBHIHH") +_HEADER_SIZE = _HEADER.size # 16 bytes + + +@dataclasses.dataclass +class Frame: + frame_type: int + seq: int = 0 + payload: bytes = b"" + flags: int = 0 + credits: int = 0 + + +def pack_frame(frame: Frame) -> bytes: + payload = frame.payload or b"" + if len(payload) > 0xFFFF: + raise ValueError("payload too large for single frame") + hdr = _HEADER.pack( + MAGIC, + VERSION, + frame.frame_type & 0xFF, + frame.flags & 0xFFFF, + frame.seq & 0xFFFFFFFF, + len(payload), + frame.credits & 0xFFFF, + ) + return hdr + payload + + +def unpack_frame(buf: bytes) -> Frame: + if len(buf) < _HEADER_SIZE: + raise ValueError("buffer too short for frame header") + + magic, version, frame_type, flags, seq, length, credits = _HEADER.unpack( + buf[:_HEADER_SIZE] + ) + + if magic != MAGIC: + raise ValueError(f"bad magic: 0x{magic:08x}") + if version != VERSION: + raise ValueError(f"unsupported version: {version}") + + end = _HEADER_SIZE + length + if len(buf) < end: + raise ValueError("buffer too short for frame payload") + + payload = buf[_HEADER_SIZE:end] + return Frame( + frame_type=frame_type, + seq=seq, + payload=payload, + flags=flags, + credits=credits, + ) + + +def read_frames(stream: io.BufferedReader) -> Generator[Frame, None, None]: + """Yield frames from a byte stream until EOF. + + The stream may return partial reads; this function handles reassembly. + """ + + while True: + hdr = stream.read(_HEADER_SIZE) + if not hdr: + return + if len(hdr) != _HEADER_SIZE: + raise EOFError("truncated frame header") + + magic, version, frame_type, flags, seq, length, credits = _HEADER.unpack(hdr) + + if magic != MAGIC: + raise ValueError(f"bad magic: 0x{magic:08x}") + if version != VERSION: + raise ValueError(f"unsupported version: {version}") + + payload = stream.read(length) + if len(payload) != length: + raise EOFError("truncated frame payload") + + yield Frame( + frame_type=frame_type, + seq=seq, + payload=payload, + flags=flags, + credits=credits, + ) + + +def chunk_bytes(data: bytes, max_payload: int) -> Iterable[bytes]: + if max_payload <= 0: + raise ValueError("max_payload must be > 0") + for i in range(0, len(data), max_payload): + yield data[i : i + max_payload] diff --git a/scripts/sof-fw-gdb-attach.sh b/scripts/sof-fw-gdb-attach.sh new file mode 100755 index 000000000000..00570a47ce62 --- /dev/null +++ b/scripts/sof-fw-gdb-attach.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +HOST="spider" +PORT="1235" +ELF_PATH="" +GDB_BIN=${GDB_BIN:-/home/lrg/zephyr-sdk-1.0.1/gnu/xtensa-intel_tgl_adsp_zephyr-elf/bin/xtensa-intel_tgl_adsp_zephyr-elf-gdb} +REMOTE_BRIDGE_PATH="/tmp/sof-fw-gdb-bridge.py" +REMOTE_LOG_PATH="/tmp/sof-fw-gdb-bridge.log" +REMOTE_PID_PATH="/tmp/sof-fw-gdb-bridge.pid" + +usage() { + cat < [options] + +Required: + --elf Path to firmware ELF with symbols + +Options: + --host DUT hostname (default: spider) + --port Bridge TCP port (default: 1235) + --gdb GDB binary path (default: $GDB_BIN) + --smoke Non-interactive smoke test: info registers + bt + quit + --cleanup-bridge Stop remote bridge after GDB exits + --no-start-bridge Do not install/start remote bridge + --keep-bridge Do not stop existing bridge before start + -h, --help Show this help + +Environment: + GDB_BIN Default GDB path override + +Examples: + $0 --elf /home/lrg/work/sof-tgl/build-tgl-gdb-llvm/zephyr/zephyr.elf + $0 --host spider --port 1235 --elf build-tgl-gdb-llvm/zephyr/zephyr.elf +EOF +} + +START_BRIDGE=1 +KILL_EXISTING=1 +SMOKE=0 +CLEANUP_BRIDGE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --host) + HOST="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + --elf) + ELF_PATH="$2" + shift 2 + ;; + --gdb) + GDB_BIN="$2" + shift 2 + ;; + --smoke) + SMOKE=1 + shift + ;; + --cleanup-bridge) + CLEANUP_BRIDGE=1 + shift + ;; + --no-start-bridge) + START_BRIDGE=0 + shift + ;; + --keep-bridge) + KILL_EXISTING=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +if [[ -z "$ELF_PATH" ]]; then + echo "--elf is required" >&2 + usage + exit 2 +fi + +if [[ ! -f "$ELF_PATH" ]]; then + echo "ELF not found: $ELF_PATH" >&2 + exit 2 +fi + +if [[ ! -x "$GDB_BIN" ]]; then + echo "GDB binary not executable: $GDB_BIN" >&2 + exit 2 +fi + +SSH=(ssh -o BatchMode=yes -o ConnectTimeout=8 "root@$HOST") + +kill_remote_bridges() { + "${SSH[@]}" " + if [ -f '$REMOTE_PID_PATH' ]; then + pid=\$(cat '$REMOTE_PID_PATH' 2>/dev/null || true) + [ -n \"\$pid\" ] && kill \"\$pid\" 2>/dev/null || true + rm -f '$REMOTE_PID_PATH' + fi + extra_pids=\$(ps -eo pid=,comm=,args= | awk '\$2==\"python3\" && (\$0 ~ /python3 -u \\/tmp\\/sof-fw-gdb-bridge.py/ || \$0 ~ /python3 \\/tmp\\/fw_gdb_bridge.py/) { print \$1 }') + [ -n \"\$extra_pids\" ] && kill \$extra_pids 2>/dev/null || true + " +} + +cleanup_remote_bridge() { + echo "Cleaning up remote bridge" + kill_remote_bridges +} + +echo "Checking DUT connectivity and fw_gdb node on $HOST" +"${SSH[@]}" "hostname; ls -l /sys/kernel/debug/sof/fw_gdb" + +if [[ "$START_BRIDGE" -eq 1 ]]; then + echo "Installing remote bridge script: $REMOTE_BRIDGE_PATH" + "${SSH[@]}" "cat > '$REMOTE_BRIDGE_PATH'" < "$SCRIPT_DIR/sof-fw-gdb-bridge.py" + "${SSH[@]}" "chmod +x '$REMOTE_BRIDGE_PATH'" + + if [[ "$KILL_EXISTING" -eq 1 ]]; then + echo "Stopping existing bridge, if any" + kill_remote_bridges + fi + + echo "Starting remote bridge on $HOST:$PORT" + "${SSH[@]}" "nohup python3 -u '$REMOTE_BRIDGE_PATH' --port '$PORT' > '$REMOTE_LOG_PATH' 2>&1 & echo \$! > '$REMOTE_PID_PATH'" + + for _ in $(seq 1 20); do + if "${SSH[@]}" "ss -ltn 2>/dev/null | grep -q ':$PORT '"; then + echo "Remote bridge is listening on port $PORT" + break + fi + sleep 0.2 + done + + if ! "${SSH[@]}" "ss -ltn 2>/dev/null | grep -q ':$PORT '"; then + echo "Bridge did not start on $HOST:$PORT" >&2 + echo "Recent bridge log:" >&2 + "${SSH[@]}" "tail -n 60 '$REMOTE_LOG_PATH' || true" >&2 + exit 1 + fi +fi + +echo "Launching GDB: $GDB_BIN" +echo "Target: $HOST:$PORT" + +if [[ "$CLEANUP_BRIDGE" -eq 1 ]]; then + trap cleanup_remote_bridge EXIT +fi + +if [[ "$SMOKE" -eq 1 ]]; then + "$GDB_BIN" "$ELF_PATH" \ + -q \ + -ex "set pagination off" \ + -ex "set confirm off" \ + -ex "set remotetimeout 8" \ + -ex "target remote $HOST:$PORT" \ + -ex "info registers" \ + -ex "bt" \ + -ex "quit" +else + "$GDB_BIN" "$ELF_PATH" \ + -ex "set pagination off" \ + -ex "set remotetimeout 8" \ + -ex "target remote $HOST:$PORT" +fi \ No newline at end of file diff --git a/scripts/sof-fw-gdb-bridge.py b/scripts/sof-fw-gdb-bridge.py new file mode 100755 index 000000000000..8f1804d94569 --- /dev/null +++ b/scripts/sof-fw-gdb-bridge.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Bridge a TCP port to SOF fw_gdb debugfs endpoint. + +Run this on the DUT. It forwards GDB remote protocol bytes between +TCP clients and /sys/kernel/debug/sof/fw_gdb. +""" + +import argparse +import os +import select +import socket +import sys + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="SOF fw_gdb TCP bridge") + parser.add_argument( + "--fw-path", + default="/sys/kernel/debug/sof/fw_gdb", + help="Path to fw_gdb debugfs file", + ) + parser.add_argument( + "--host", + default="0.0.0.0", + help="TCP listen host", + ) + parser.add_argument( + "--port", + type=int, + default=1235, + help="TCP listen port", + ) + return parser.parse_args() + + +def bridge_one_client(conn: socket.socket, fwfd: int) -> None: + conn.setblocking(False) + + while True: + readable, _, _ = select.select([conn, fwfd], [], [], 1.0) + + if conn in readable: + try: + data = conn.recv(4096) + except BlockingIOError: + data = None + + if data == b"": + return + + if data: + os.write(fwfd, data) + + if fwfd in readable: + try: + data = os.read(fwfd, 4096) + except BlockingIOError: + data = None + + if data: + conn.sendall(data) + + +def main() -> int: + args = parse_args() + + if not os.path.exists(args.fw_path): + print(f"fw_gdb path not found: {args.fw_path}", file=sys.stderr) + return 1 + + fwfd = os.open(args.fw_path, os.O_RDWR | os.O_NONBLOCK) + + listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listen_sock.bind((args.host, args.port)) + listen_sock.listen(1) + + print(f"listening on {args.host}:{args.port}, fw={args.fw_path}", flush=True) + + try: + while True: + conn, addr = listen_sock.accept() + print(f"client connected: {addr}", flush=True) + try: + bridge_one_client(conn, fwfd) + finally: + conn.close() + print("client disconnected", flush=True) + finally: + os.close(fwfd) + listen_sock.close() + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/sof-shell-probe-bridge.py b/scripts/sof-shell-probe-bridge.py new file mode 100755 index 000000000000..c58c4156a622 --- /dev/null +++ b/scripts/sof-shell-probe-bridge.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +"""Bridge Zephyr shell PTY traffic to a framed shell/probe stream. + +This tool is intentionally transport-agnostic for the probe side: it reads and +writes framed packets via files/FIFOs so it can be paired with any compressed +PCM bridge implementation. + +Typical setup: +1. Start a process that converts probe compressed PCM <-> framed byte stream. +2. Point this tool to those two files/FIFOs. +3. It will bridge shell PTY traffic to/from those framed streams. +""" + +from __future__ import annotations + +import argparse +import os +import queue +import select +import subprocess +import sys +import termios +import threading +import time +from typing import Optional + +from shell_probe_framing import ( + FLAG_EOC, + TYPE_CREDIT, + TYPE_DATA, + TYPE_RESET, + Frame, + chunk_bytes, + pack_frame, + read_frames, +) + + +def _find_cavstool_path(dut: str, preferred: str) -> str: + cmd = ( + "python3 - <<'PY'\n" + "import os, shutil\n" + "cands=[" + f"{preferred!r}," + "'/usr/local/bin/cavstool.py','/usr/bin/cavstool.py','/usr/sbin/cavstool.py']\n" + "for p in cands:\n" + " if p and os.path.isfile(p):\n" + " print(p); raise SystemExit(0)\n" + "w=shutil.which('cavstool.py')\n" + "print(w or '')\n" + "PY" + ) + out = subprocess.check_output(["ssh", f"root@{dut}", cmd], text=True).strip() + if not out: + raise RuntimeError("cavstool.py not found on DUT") + return out + + +def _open_shell_pty(dut: str, cavstool: str, timeout_s: int = 20): + ssh_cmd = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + f"root@{dut}", + f"python3 {cavstool} --log-only --shell-pty --no-history", + ] + proc = subprocess.Popen( + ssh_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + pty_path = None + deadline = time.time() + timeout_s + while time.time() < deadline: + line = proc.stdout.readline() + if not line: + break + if "shell PTY at:" in line: + pty_path = line.split("shell PTY at:", 1)[1].strip() + break + + if not pty_path: + proc.terminate() + raise RuntimeError("cavstool did not report shell PTY path") + + return proc, pty_path + + +def _open_remote_pty_fd(dut: str, pty_path: str): + # Keep a dedicated SSH process that proxies PTY bytes to local stdio. + # stdin -> PTY write ; PTY read -> stdout + script = ( + "python3 - <<'PY'\n" + "import os, pty, select, sys, termios\n" + f"p={pty_path!r}\n" + "fd=os.open(p, os.O_RDWR|os.O_NOCTTY)\n" + "attr=termios.tcgetattr(fd)\n" + "attr[3] &= ~(termios.ECHO|termios.ICANON|termios.ISIG)\n" + "attr[6][termios.VMIN]=0\n" + "attr[6][termios.VTIME]=0\n" + "termios.tcsetattr(fd, termios.TCSANOW, attr)\n" + "while True:\n" + " r,_,_=select.select([fd, sys.stdin.buffer], [], [], 0.1)\n" + " if fd in r:\n" + " b=os.read(fd,4096)\n" + " if b:\n" + " sys.stdout.buffer.write(b); sys.stdout.buffer.flush()\n" + " if sys.stdin.buffer in r:\n" + " b=os.read(sys.stdin.fileno(),4096)\n" + " if not b:\n" + " break\n" + " os.write(fd,b)\n" + "PY" + ) + + proc = subprocess.Popen( + ["ssh", f"root@{dut}", script], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return proc + + +class Bridge: + def __init__( + self, + pty_proc: subprocess.Popen, + framed_in_path: str, + framed_out_path: str, + max_payload: int, + initial_credits: int, + ): + self.pty_proc = pty_proc + self.max_payload = max_payload + self.tx_seq = 0 + self.rx_seq = 0 + self.tx_credits = initial_credits + self.tx_q: "queue.Queue[bytes]" = queue.Queue() + + self.framed_in = open(framed_in_path, "rb", buffering=0) + self.framed_out = open(framed_out_path, "wb", buffering=0) + + self.stop = threading.Event() + + def _send_frame(self, frame: Frame): + self.framed_out.write(pack_frame(frame)) + self.framed_out.flush() + + def _tx_from_shell(self): + fd = self.pty_proc.stdout.fileno() + while not self.stop.is_set(): + r, _, _ = select.select([fd], [], [], 0.1) + if fd not in r: + continue + data = os.read(fd, 4096) + if not data: + self.stop.set() + return + + for chunk in chunk_bytes(data, self.max_payload): + self.tx_q.put(chunk) + + def _rx_to_shell(self): + for frame in read_frames(self.framed_in): + if self.stop.is_set(): + return + + if frame.frame_type == TYPE_DATA: + if frame.seq != self.rx_seq: + # Resync on sequence mismatch by requesting reset. + self._send_frame(Frame(frame_type=TYPE_RESET, seq=self.rx_seq)) + continue + self.rx_seq = (self.rx_seq + 1) & 0xFFFFFFFF + + if frame.payload: + self.pty_proc.stdin.write(frame.payload) + self.pty_proc.stdin.flush() + + consumed = len(frame.payload) + if consumed: + self._send_frame(Frame(frame_type=TYPE_CREDIT, credits=consumed)) + + elif frame.frame_type == TYPE_CREDIT: + self.tx_credits += frame.credits + + elif frame.frame_type == TYPE_RESET: + self.tx_seq = 0 + self.rx_seq = 0 + self.tx_credits = 0 + self._send_frame(Frame(frame_type=TYPE_CREDIT, credits=0)) + + def _tx_loop(self): + while not self.stop.is_set(): + try: + chunk = self.tx_q.get(timeout=0.1) + except queue.Empty: + continue + + while self.tx_credits < len(chunk) and not self.stop.is_set(): + time.sleep(0.002) + + if self.stop.is_set(): + return + + self.tx_credits -= len(chunk) + self._send_frame( + Frame( + frame_type=TYPE_DATA, + seq=self.tx_seq, + payload=chunk, + flags=FLAG_EOC if chunk.endswith(b"\n") else 0, + ) + ) + self.tx_seq = (self.tx_seq + 1) & 0xFFFFFFFF + + def run(self): + t1 = threading.Thread(target=self._tx_from_shell, daemon=True) + t2 = threading.Thread(target=self._rx_to_shell, daemon=True) + t3 = threading.Thread(target=self._tx_loop, daemon=True) + + t1.start() + t2.start() + t3.start() + + try: + while not self.stop.is_set(): + if self.pty_proc.poll() is not None: + self.stop.set() + break + time.sleep(0.1) + finally: + self.stop.set() + t1.join(timeout=1) + t2.join(timeout=1) + t3.join(timeout=1) + + +def parse_args() -> argparse.Namespace: + ap = argparse.ArgumentParser( + description="Bridge shell PTY traffic to framed shell/probe transport streams", + ) + ap.add_argument("--dut", default="spider", help="DUT hostname (default: spider)") + ap.add_argument( + "--cavstool", + default="/usr/local/bin/cavstool.py", + help="Path to cavstool.py on DUT", + ) + ap.add_argument( + "--framed-in", + required=True, + help="Input file/FIFO for framed bytes heading to shell", + ) + ap.add_argument( + "--framed-out", + required=True, + help="Output file/FIFO for framed bytes coming from shell", + ) + ap.add_argument( + "--max-payload", + type=int, + default=512, + help="Maximum payload bytes per data frame (default: 512)", + ) + ap.add_argument( + "--initial-credits", + type=int, + default=16384, + help="Initial TX credits before remote credit frames arrive (default: 16384)", + ) + return ap.parse_args() + + +def main() -> int: + args = parse_args() + + cavstool_path = _find_cavstool_path(args.dut, args.cavstool) + cavstool_proc, pty_path = _open_shell_pty(args.dut, cavstool_path) + pty_io_proc = _open_remote_pty_fd(args.dut, pty_path) + + sys.stderr.write(f"[bridge] shell PTY path: {pty_path}\n") + sys.stderr.flush() + + bridge = Bridge( + pty_proc=pty_io_proc, + framed_in_path=args.framed_in, + framed_out_path=args.framed_out, + max_payload=args.max_payload, + initial_credits=args.initial_credits, + ) + + try: + bridge.run() + finally: + for p in (pty_io_proc, cavstool_proc): + try: + p.terminate() + except Exception: + pass + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sof-shell-probe-compr-adapter.py b/scripts/sof-shell-probe-compr-adapter.py new file mode 100755 index 000000000000..0a89f9b6dbb1 --- /dev/null +++ b/scripts/sof-shell-probe-compr-adapter.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +"""Bridge framed shell bytes to compressed PCM transport commands. + +This helper connects the framed byte streams used by sof-shell-probe-bridge.py +to any pair of user-provided processes that read/write probe compressed PCM +streams. + +Direction summary: +- framed-in -> playback command stdin (host -> FW) +- capture command stdout -> framed-out (FW -> host) +""" + +from __future__ import annotations + +import argparse +import os +import shlex +import subprocess +import threading +import time + + +def _format_cmd(template: str, dev: str) -> list[str]: + return shlex.split(template.format(dev=dev)) + + +def _pump_reader_to_writer(reader, writer, stop_evt: threading.Event, label: str): + try: + while not stop_evt.is_set(): + chunk = reader.read(4096) + if not chunk: + stop_evt.set() + return + writer.write(chunk) + writer.flush() + except BrokenPipeError: + stop_evt.set() + except Exception as e: + stop_evt.set() + raise RuntimeError(f"{label} pump failed: {e}") from e + + +def parse_args() -> argparse.Namespace: + ap = argparse.ArgumentParser( + description="Bridge framed shell bytes to compressed PCM commands", + ) + ap.add_argument( + "--framed-in", + required=True, + help="Path to framed input stream to send to playback command", + ) + ap.add_argument( + "--framed-out", + required=True, + help="Path to framed output stream read from capture command", + ) + ap.add_argument( + "--playback-dev", + required=True, + help="Playback compressed device path (e.g. /dev/snd/comprC1D0)", + ) + ap.add_argument( + "--capture-dev", + required=True, + help="Capture compressed device path (e.g. /dev/snd/comprC1D1)", + ) + ap.add_argument( + "--playback-cmd", + required=True, + help=( + "Playback command template. Must consume stdin and write to {dev}. " + "Example: 'my_playback_tool --device {dev} --stdin'" + ), + ) + ap.add_argument( + "--capture-cmd", + required=True, + help=( + "Capture command template. Must read from {dev} and write to stdout. " + "Example: 'my_capture_tool --device {dev} --stdout'" + ), + ) + return ap.parse_args() + + +def main() -> int: + args = parse_args() + + playback_cmd = _format_cmd(args.playback_cmd, args.playback_dev) + capture_cmd = _format_cmd(args.capture_cmd, args.capture_dev) + + print(f"[adapter] playback cmd: {' '.join(playback_cmd)}") + print(f"[adapter] capture cmd : {' '.join(capture_cmd)}") + + play_proc = subprocess.Popen(playback_cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL) + cap_proc = subprocess.Popen(capture_cmd, stdout=subprocess.PIPE, stdin=subprocess.DEVNULL) + + stop_evt = threading.Event() + + # Open framed streams in binary mode with buffering disabled. + # Use read-write mode to avoid FIFO open order deadlocks. + framed_in = open(args.framed_in, "r+b", buffering=0) + framed_out = open(args.framed_out, "r+b", buffering=0) + + t_in = threading.Thread( + target=_pump_reader_to_writer, + args=(framed_in, play_proc.stdin, stop_evt, "framed->playback"), + daemon=True, + ) + t_out = threading.Thread( + target=_pump_reader_to_writer, + args=(cap_proc.stdout, framed_out, stop_evt, "capture->framed"), + daemon=True, + ) + + t_in.start() + t_out.start() + + try: + while not stop_evt.is_set(): + if play_proc.poll() is not None: + print(f"[adapter] playback process exited with {play_proc.returncode}") + stop_evt.set() + break + if cap_proc.poll() is not None: + print(f"[adapter] capture process exited with {cap_proc.returncode}") + stop_evt.set() + break + time.sleep(0.1) + finally: + stop_evt.set() + t_in.join(timeout=1) + t_out.join(timeout=1) + for p in (play_proc, cap_proc): + try: + p.terminate() + except Exception: + pass + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sof-shell-probe-packet-decode.py b/scripts/sof-shell-probe-packet-decode.py new file mode 100755 index 000000000000..292399139377 --- /dev/null +++ b/scripts/sof-shell-probe-packet-decode.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +"""Decode probe extraction packets and print mirrored shell payload. + +Reads a raw probe extraction byte stream from stdin and writes shell payload +bytes to stdout for packets with PROBE_SHELL_BUFFER_ID. +""" + +from __future__ import annotations + +import argparse +import struct +import sys + +PROBE_EXTRACT_SYNC_WORD = 0xBABEBEBA +PROBE_LOGGING_BUFFER_ID = 0x01000000 +PROBE_SHELL_BUFFER_ID = 0x01000001 + +HDR = struct.Struct("<6I") +HDR_SIZE = HDR.size +CHECKSUM_SIZE = 8 + + +def parse_args() -> argparse.Namespace: + ap = argparse.ArgumentParser(description="Decode shell payload from probe packets") + ap.add_argument( + "--include-logging", + action="store_true", + help="Also print PROBE_LOGGING_BUFFER_ID packets", + ) + return ap.parse_args() + + +def _read_exact(reader, size: int) -> bytes | None: + data = bytearray() + while len(data) < size: + chunk = reader.read(size - len(data)) + if not chunk: + return None + data.extend(chunk) + return bytes(data) + + +def main() -> int: + args = parse_args() + + targets = {PROBE_SHELL_BUFFER_ID} + if args.include_logging: + targets.add(PROBE_LOGGING_BUFFER_ID) + + r = sys.stdin.buffer + w = sys.stdout.buffer + + while True: + hdr = _read_exact(r, HDR_SIZE) + if hdr is None: + return 0 + + sync, buffer_id, _fmt, _ts_lo, _ts_hi, data_len = HDR.unpack(hdr) + + if sync != PROBE_EXTRACT_SYNC_WORD: + # Resync by shifting one byte and retrying. + carry = hdr[1:] + nxt = _read_exact(r, 1) + if nxt is None: + return 0 + blob = carry + nxt + # Try immediate decode of shifted window; if still invalid, continue loop. + try: + sync2, buffer_id, _fmt, _ts_lo, _ts_hi, data_len = HDR.unpack(blob) + except struct.error: + continue + if sync2 != PROBE_EXTRACT_SYNC_WORD: + continue + + payload = _read_exact(r, data_len) + if payload is None: + return 0 + + checksum = _read_exact(r, CHECKSUM_SIZE) + if checksum is None: + return 0 + + if buffer_id in targets and payload: + w.write(payload) + w.flush() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sof-shell-probe-validate.py b/scripts/sof-shell-probe-validate.py new file mode 100755 index 000000000000..77fb605fa808 --- /dev/null +++ b/scripts/sof-shell-probe-validate.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation. All rights reserved. +"""Validate SOF shell commands through framed probe transport bridge. + +This validator exercises shell commands through the shell/probe framing path: +validator <-> framed stream <-> sof-shell-probe-bridge.py <-> cavstool shell PTY + +It verifies command responses and error patterns similarly to sof-shell-validate.py. +""" + +from __future__ import annotations + +import argparse +import json +import os +import selectors +import shutil +import subprocess +import sys +import tempfile +import time + +from shell_probe_framing import ( + TYPE_CREDIT, + TYPE_DATA, + Frame, + chunk_bytes, + pack_frame, + unpack_frame, +) + +COMMANDS = [ + ("sof version", ["SOF Version"]), + ("sof pipeline list", ["ID"]), + ("sof module status", ["No components found", "comp_id"]), + ("sof core status", ["core", "enabled"]), + ("sof ipc stats", ["rx_count", "tx_count"]), + ("sof buffer list", ["Audio buffers"]), + ("sof dma status", ["No DMA controllers", "DMA controller"]), +] + +ERROR_PATTERNS = [ + "shell: command not found", + "shell: unknown command", + "-EINVAL", + "-ENOMEM", + "Traceback", +] + +SHELL_PROMPT = "~$ " + + +class FrameClient: + def __init__(self, tx_path: str, rx_path: str, max_payload: int): + self.max_payload = max_payload + self.tx_seq = 0 + + # Use nonblocking IO + selector to avoid deadlocks with FIFOs. + self.tx_fd = os.open(tx_path, os.O_RDWR | os.O_NONBLOCK) + self.rx_fd = os.open(rx_path, os.O_RDWR | os.O_NONBLOCK) + self.sel = selectors.DefaultSelector() + self.sel.register(self.rx_fd, selectors.EVENT_READ) + + self._rx_buf = bytearray() + + def close(self): + self.sel.close() + os.close(self.tx_fd) + os.close(self.rx_fd) + + def _write_frame(self, frame: Frame): + data = pack_frame(frame) + off = 0 + while off < len(data): + try: + n = os.write(self.tx_fd, data[off:]) + except BlockingIOError: + time.sleep(0.002) + continue + off += n + + def send_text(self, text: str): + raw = text.encode("utf-8", errors="replace") + for chunk in chunk_bytes(raw, self.max_payload): + self._write_frame( + Frame(frame_type=TYPE_DATA, seq=self.tx_seq, payload=chunk, flags=0) + ) + self.tx_seq = (self.tx_seq + 1) & 0xFFFFFFFF + + def recv_until_prompt(self, timeout_s: float) -> str: + deadline = time.time() + timeout_s + collected = bytearray() + + while time.time() < deadline: + events = self.sel.select(timeout=0.2) + if not events: + continue + + for _key, _mask in events: + try: + chunk = os.read(self.rx_fd, 4096) + except BlockingIOError: + continue + + if not chunk: + continue + + self._rx_buf.extend(chunk) + + while True: + if len(self._rx_buf) < 16: + break + + payload_len = (self._rx_buf[12] << 8) | self._rx_buf[13] + frame_len = 16 + payload_len + if len(self._rx_buf) < frame_len: + break + + frame_bytes = bytes(self._rx_buf[:frame_len]) + del self._rx_buf[:frame_len] + + frame = unpack_frame(frame_bytes) + if frame.frame_type == TYPE_DATA: + collected.extend(frame.payload) + # Grant credits for consumed bytes so sender can continue. + if frame.payload: + self._write_frame(Frame(frame_type=TYPE_CREDIT, credits=len(frame.payload))) + + if SHELL_PROMPT.encode() in collected: + return collected.decode("utf-8", errors="replace") + + return collected.decode("utf-8", errors="replace") + + +def parse_args() -> argparse.Namespace: + ap = argparse.ArgumentParser(description="Validate shell over framed probe transport") + ap.add_argument("--dut", default="spider", help="DUT hostname (default: spider)") + ap.add_argument( + "--bridge-script", + default="sof/scripts/sof-shell-probe-bridge.py", + help="Path to sof-shell-probe-bridge.py", + ) + ap.add_argument( + "--cavstool", + default="/usr/local/bin/cavstool.py", + help="Path to cavstool.py on DUT", + ) + ap.add_argument("--timeout", type=float, default=5.0, help="Per-command timeout in seconds") + ap.add_argument("--max-payload", type=int, default=512, help="Max bytes per frame payload") + ap.add_argument( + "--initial-credits", + type=int, + default=16384, + help="Initial tx credits for bridge side", + ) + return ap.parse_args() + + +def _normalize_output(cmd: str, collected: str) -> str: + out = collected + for s in [cmd, SHELL_PROMPT, "\r\n", "\r", "\n\n"]: + out = out.replace(s, "\n") + return out.strip() + + +def main() -> int: + args = parse_args() + + bridge_script = args.bridge_script + if not os.path.isfile(bridge_script): + candidates = [ + bridge_script, + os.path.join("sof", "scripts", "sof-shell-probe-bridge.py"), + os.path.join("scripts", "sof-shell-probe-bridge.py"), + ] + bridge_script = next((p for p in candidates if os.path.isfile(p)), "") + if not bridge_script and shutil.which(args.bridge_script): + bridge_script = args.bridge_script + if not bridge_script: + print(f"[error] bridge script not found: {args.bridge_script}") + return 2 + + with tempfile.TemporaryDirectory(prefix="sof-shell-probe-") as td: + framed_in = os.path.join(td, "framed.in") + framed_out = os.path.join(td, "framed.out") + os.mkfifo(framed_in) + os.mkfifo(framed_out) + + bridge_cmd = [ + sys.executable, + bridge_script, + "--dut", + args.dut, + "--cavstool", + args.cavstool, + "--framed-in", + framed_in, + "--framed-out", + framed_out, + "--max-payload", + str(args.max_payload), + "--initial-credits", + str(args.initial_credits), + ] + + bridge = subprocess.Popen(bridge_cmd) + + # Give bridge some time to establish SSH and shell PTY. + time.sleep(2.0) + if bridge.poll() is not None: + print("[error] bridge exited before validation started") + return 2 + + client = FrameClient(tx_path=framed_in, rx_path=framed_out, max_payload=args.max_payload) + results = [] + + try: + # Prime shell prompt. + client.send_text("\r\n") + client.recv_until_prompt(timeout_s=args.timeout) + + for cmd, patterns in COMMANDS: + client.send_text(cmd + "\r\n") + collected = client.recv_until_prompt(timeout_s=args.timeout) + + has_error = any(ep in collected for ep in ERROR_PATTERNS) + has_match = (not patterns) or any(p.lower() in collected.lower() for p in patterns) + + results.append( + { + "cmd": cmd, + "pass": has_match and not has_error, + "output": _normalize_output(cmd, collected), + } + ) + finally: + client.close() + bridge.terminate() + + passed = sum(1 for r in results if r["pass"]) + total = len(results) + + print(f"[summary] PASS {passed}/{total}") + print(json.dumps(results, indent=2)) + + return 0 if passed == total else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/include/sof/probe/probe.h b/src/include/sof/probe/probe.h index de8873ad0a67..05b12a440b2e 100644 --- a/src/include/sof/probe/probe.h +++ b/src/include/sof/probe/probe.h @@ -11,6 +11,9 @@ #include #include +/* Buffer id used in probe extraction packets for mirrored shell text output. */ +#define PROBE_SHELL_BUFFER_ID 0x01000001 + /** * A buffer of logging data is available for processing. */ @@ -29,6 +32,20 @@ bool probe_is_backend_configured(void); */ void probe_logging_init(probe_logging_hook_t hook); +/** + * @brief Write shell output text to the probe extraction stream. + * + * The payload is packetized with the probe extraction header and can be + * consumed from compressed capture endpoints together with other probe data. + * If extraction DMA is not active, bytes are dropped and 0 is returned. + * + * @param buffer Shell output bytes to transmit. + * @param length Number of bytes in @p buffer. + * @return Number of bytes written (can be less than length when buffer is + * near full), 0 when probes are inactive, negative errno on error. + */ +ssize_t probe_shell_output(const uint8_t *buffer, size_t length); + /* * \brief Initialize probes subsystem * diff --git a/src/probe/probe.c b/src/probe/probe.c index 19f62bf81a14..213b818d6cc6 100644 --- a/src/probe/probe.c +++ b/src/probe/probe.c @@ -539,7 +539,7 @@ int probe_dma_add(uint32_t count, const struct probe_dma *probe_dma) err = probe_dma_init(&_probe->inject_dma[first_free], SOF_DMA_DIR_HMEM_TO_LMEM); if (err < 0) { - tr_err(&pr_tr, "probe_dma_init() failed"); + tr_err(&pr_tr, "probe_dma_init() failed"); _probe->inject_dma[first_free].stream_tag = PROBE_DMA_INVALID; return err; @@ -865,34 +865,51 @@ static void kick_probe_task(struct probe_pdata *_probe) reschedule_task(&_probe->dmap_work, 0); } -#if CONFIG_LOG_BACKEND_SOF_PROBE -static ssize_t probe_logging_hook(uint8_t *buffer, size_t length) +static ssize_t probe_write_payload(uint32_t buffer_id, const uint8_t *buffer, + size_t length) { struct probe_pdata *_probe = probe_get(); + const size_t overhead = sizeof(struct probe_data_packet) + sizeof(uint64_t); uint64_t checksum; - size_t max_len; + size_t free_space; int ret; - max_len = _probe->ext_dma.dmapb.avail - sizeof(struct probe_data_packet) - sizeof(checksum); - length = MIN(max_len, length); + if (!_probe || _probe->ext_dma.stream_tag == PROBE_DMA_INVALID || !buffer || !length) + return 0; + + free_space = _probe->ext_dma.dmapb.size - _probe->ext_dma.dmapb.avail; + if (free_space <= overhead) + return 0; + + length = MIN(length, free_space - overhead); - ret = probe_gen_header(PROBE_LOGGING_BUFFER_ID, length, 0, &checksum); + ret = probe_gen_header(buffer_id, length, 0, &checksum); if (ret < 0) return ret; - ret = copy_to_pbuffer(&_probe->ext_dma.dmapb, - buffer, length); + ret = copy_to_pbuffer(&_probe->ext_dma.dmapb, (void *)buffer, length); if (ret < 0) return ret; - ret = copy_to_pbuffer(&_probe->ext_dma.dmapb, - &checksum, sizeof(checksum)); + ret = copy_to_pbuffer(&_probe->ext_dma.dmapb, &checksum, sizeof(checksum)); if (ret < 0) return ret; kick_probe_task(_probe); + return length; } + +ssize_t probe_shell_output(const uint8_t *buffer, size_t length) +{ + return probe_write_payload(PROBE_SHELL_BUFFER_ID, buffer, length); +} + +#if CONFIG_LOG_BACKEND_SOF_PROBE +static ssize_t probe_logging_hook(uint8_t *buffer, size_t length) +{ + return probe_write_payload(PROBE_LOGGING_BUFFER_ID, buffer, length); +} #endif /** From a0f21daa182290d9abda5b8d8e1ad14490d89bfe Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 14:28:48 +0100 Subject: [PATCH 07/10] zephyr/shell: enforce 'sof ' naming scheme and direct command execution --- zephyr/shell/kernel.c | 243 +++++------------------ zephyr/shell/user.c | 439 +++++++++++++++++++++++++++++++----------- 2 files changed, 371 insertions(+), 311 deletions(-) diff --git a/zephyr/shell/kernel.c b/zephyr/shell/kernel.c index 246fea27a776..e94ff50454c0 100644 --- a/zephyr/shell/kernel.c +++ b/zephyr/shell/kernel.c @@ -149,9 +149,7 @@ __cold static int cmd_sof_test_inject_sched_gap(const struct shell *sh, return 0; } -SHELL_SUBCMD_ADD((sof), test_inject_sched_gap, NULL, - "Inject a gap to audio scheduling\n", - cmd_sof_test_inject_sched_gap, 0, 0); + #if CONFIG_SOF_SHELL_CORE_STATUS __cold static int cmd_sof_core_status(const struct shell *sh, @@ -165,18 +163,15 @@ __cold static int cmd_sof_core_status(const struct shell *sh, shell_print(sh, "%-6s %-8s %s", "core", "enabled", "current"); for (i = 0; i < cs.core_count; i++) { + bool is_curr = (i == cs.current); + shell_print(sh, "%-6u %-8s %s", - i, - cs.enabled[i] ? "yes" : "no", - (i == cs.current) ? "<--" : ""); + i, cs.enabled[i] ? "yes" : "no", + is_curr ? "<--" : ""); } return 0; } - -SHELL_SUBCMD_ADD((sof), core_status, NULL, - "Print enabled/active state of each DSP core\n", - cmd_sof_core_status, 0, 0); #endif /* CONFIG_SOF_SHELL_CORE_STATUS */ #if CONFIG_SOF_SHELL_SRAM_STATUS @@ -202,10 +197,6 @@ __cold static int cmd_sof_sram_status(const struct shell *sh, return 0; } - -SHELL_SUBCMD_ADD((sof), sram_status, NULL, - "Print HPSRAM heap usage statistics\n", - cmd_sof_sram_status, 0, 0); #endif /* CONFIG_SOF_SHELL_SRAM_STATUS */ #if CONFIG_SOF_SHELL_CLOCK_STATUS @@ -233,10 +224,6 @@ __cold static int cmd_sof_clock_status(const struct shell *sh, return 0; } - -SHELL_SUBCMD_ADD((sof), clock_status, NULL, - "Print current clock frequency for each DSP core\n", - cmd_sof_clock_status, 0, 0); #endif /* CONFIG_SOF_SHELL_CLOCK_STATUS */ @@ -443,17 +430,6 @@ __cold static int cmd_sof_tlb_lookup(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_MMU_DBG */ #if CONFIG_SOF_SHELL_MMU_DBG -#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB -SHELL_SUBCMD_ADD((sof), mmu_status, NULL, - "Print Intel ADSP MTL TLB / virtual memory status\n", - cmd_sof_mmu_status, 0, 0); -SHELL_SUBCMD_ADD((sof), tlb_dump, NULL, - "Dump all active TLB entries (vaddr/paddr/flags)\n", - cmd_sof_tlb_dump, 0, 0); -SHELL_SUBCMD_ADD((sof), tlb_lookup, NULL, - "Query TLB for a page or range: [end_vaddr]\n", - cmd_sof_tlb_lookup, 2, 1); -#endif #endif #if CONFIG_XTENSA_MMU @@ -619,17 +595,6 @@ __cold static int cmd_sof_page_info(const struct shell *sh, #endif /* CONFIG_XTENSA_MMU */ -#if CONFIG_XTENSA_MMU -SHELL_SUBCMD_ADD((sof), rasid, NULL, - "Decode RASID register: ring 0-3 to ASID mapping\n", - cmd_sof_rasid, 0, 0); -SHELL_SUBCMD_ADD((sof), page_info, NULL, - "Probe DTLB for a page or range: [end_vaddr]\n" - "Reports physical address, ring, ASID, R/W/X permissions" - " and cache mode for each page currently in the DTLB.\n", - cmd_sof_page_info, 2, 1); -#endif - #if CONFIG_SOF_SHELL_LLEXT_LOAD || (CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT) || \ (CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT) /* parse_long: shared numeric argument parser for kernel shell commands. @@ -748,13 +713,6 @@ __cold static int cmd_sof_llext_load(const struct shell *sh, return -ETIMEDOUT; } -SHELL_SUBCMD_ADD((sof), llext_load, NULL, - "Load llext module from host: [lib_id=1]\n" - "Sets up the DMA handshake slot then waits for:\n" - " dd if= of=/sys/kernel/debug/sof/llext_load\\\n" - " bs=$(stat -c%s ) count=1\n" - "on the host. Prints result when DMA and IPC4 load complete.\n", - cmd_sof_llext_load, 2, 1); #endif /* CONFIG_SOF_SHELL_LLEXT_LOAD */ #if CONFIG_SOF_SHELL_LLEXT_LIST @@ -873,19 +831,9 @@ __cold static int cmd_sof_llext_purge(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_LLEXT_PURGE */ #if CONFIG_SOF_SHELL_LLEXT_LIST -SHELL_SUBCMD_ADD((sof), llext_list, NULL, - "List llext libraries stored in IMR/DRAM.\n" - "For each library shows base address, storage size and per-module\n" - "SRAM mapping state (yes/no), use count and dependency count.\n", - cmd_sof_llext_list, 0, 0); #endif #if CONFIG_SOF_SHELL_LLEXT_PURGE -SHELL_SUBCMD_ADD((sof), llext_purge, NULL, - "Purge llext library from IMR/DRAM: \n" - "Fails with -EBUSY if any module in the library is still\n" - "mapped in SRAM (i.e. a pipeline using it is still active).\n", - cmd_sof_llext_purge, 2, 0); #endif #if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT @@ -1014,18 +962,9 @@ __cold static int cmd_sof_llext_call(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT */ #if CONFIG_SOF_SHELL_LLEXT_CTOR && CONFIG_LLEXT -SHELL_SUBCMD_ADD((sof), llext_ctor, NULL, - "Call constructors for all modules in a library: \n", - cmd_sof_llext_ctor, 2, 0); -SHELL_SUBCMD_ADD((sof), llext_dtor, NULL, - "Call destructors for all modules in a library: \n", - cmd_sof_llext_dtor, 2, 0); #endif #if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT -SHELL_SUBCMD_ADD((sof), llext_call, NULL, - "Call a named symbol in all modules of a library: \n", - cmd_sof_llext_call, 3, 0); #endif #if CONFIG_SOF_SHELL_CORE_POWER @@ -1095,14 +1034,6 @@ __cold static int cmd_sof_core_off(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_CORE_POWER */ #if CONFIG_SOF_SHELL_CORE_POWER -SHELL_SUBCMD_ADD((sof), core_on, NULL, - "Power on a secondary DSP core: \n" - "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", - cmd_sof_core_on, 2, 0); -SHELL_SUBCMD_ADD((sof), core_off, NULL, - "Power off a secondary DSP core: \n" - "core_id must be 1..CONFIG_CORE_COUNT-1 (core 0 is primary).\n", - cmd_sof_core_off, 2, 0); #endif __cold static int cmd_sof_version(const struct shell *sh, size_t argc, char *argv[]) @@ -1189,13 +1120,6 @@ __cold static int cmd_sof_vregion_info(const struct shell *sh, size_t argc, char } -SHELL_SUBCMD_ADD((sof), vpage_status, NULL, - "Print virtual page allocator status\n", - cmd_sof_vpage_info, 0, 0); - -SHELL_SUBCMD_ADD((sof), vregion_status, NULL, - "Print virtual regions status\n", - cmd_sof_vregion_info, 0, 0); __cold static int cmd_sof_ipc_stats(const struct shell *sh, size_t argc, char *argv[]) { @@ -1229,13 +1153,6 @@ __cold static int cmd_sof_ipc_last(const struct shell *sh, size_t argc, char *ar return 0; } -SHELL_SUBCMD_ADD((sof), ipc_stats, NULL, - "Print IPC RX/TX counters; 'sof ipc stats reset' clears them\n", - cmd_sof_ipc_stats, 1, 1); - -SHELL_SUBCMD_ADD((sof), ipc_last, NULL, - "Print the last received and sent IPC headers\n", - cmd_sof_ipc_last, 0, 0); #if CONFIG_SOF_SHELL_SCHED_INFO @@ -1353,12 +1270,6 @@ __cold static int cmd_sof_sched_load(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_SCHED_INFO */ #if CONFIG_SOF_SHELL_SCHED_INFO -SHELL_SUBCMD_ADD((sof), sched_tasks, NULL, - "List all scheduler tasks (type, core, prio, state)\n", - cmd_sof_sched_tasks, 0, 0); -SHELL_SUBCMD_ADD((sof), sched_load, NULL, - "Show per-task cycle counters and totals\n", - cmd_sof_sched_load, 0, 0); #endif #if CONFIG_SOF_SHELL_LOG_INFO @@ -1454,15 +1365,9 @@ __cold static int cmd_sof_mtrace_dump(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_MTRACE_DUMP */ #if CONFIG_SOF_SHELL_LOG_INFO -SHELL_SUBCMD_ADD((sof), log_status, NULL, - "List Zephyr log backends with state and source count\n", - cmd_sof_log_status, 0, 0); #endif #if CONFIG_SOF_SHELL_MTRACE_DUMP -SHELL_SUBCMD_ADD((sof), mtrace_dump, NULL, - "Snapshot the mtrace SRAM ring buffer (does not advance host_ptr)\n", - cmd_sof_mtrace_dump, 0, 0); #endif #if CONFIG_SOF_SHELL_MAILBOX_HEX || CONFIG_SOF_SHELL_DBGWIN_DUMP @@ -1667,15 +1572,9 @@ __cold static int cmd_sof_dbgwin_dump(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_DBGWIN_DUMP */ #if CONFIG_SOF_SHELL_MAILBOX_HEX -SHELL_SUBCMD_ADD((sof), mailbox_hex, NULL, - "Hex-dump a mailbox region: [offset] [length]\n", - cmd_sof_mailbox_hex, 1, 3); #endif #if CONFIG_SOF_SHELL_DBGWIN_DUMP -SHELL_SUBCMD_ADD((sof), dbgwin_dump, NULL, - "List ADSP debug-window slots, or hex-dump one: [slot] [length]\n", - cmd_sof_dbgwin_dump, 1, 2); #endif #if CONFIG_SOF_SHELL_PERF_STATUS @@ -1768,10 +1667,6 @@ __cold static int cmd_sof_perf_status(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_PERF_STATUS */ #if CONFIG_SOF_SHELL_PERF_STATUS -SHELL_SUBCMD_ADD((sof), perf_status, NULL, - "Show telemetry perf state and per-core systick;" - " optional [reset|start|stop|pause]\n", - cmd_sof_perf_status, 1, 1); #endif /* @@ -1781,32 +1676,15 @@ SHELL_SUBCMD_ADD((sof), perf_status, NULL, * preferred tokenized form, e.g. "sof core on" for "sof core_on". */ -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test_inject_sched, - SHELL_CMD(gap, NULL, - "Inject a gap to audio scheduling\n", - cmd_sof_test_inject_sched_gap), - SHELL_SUBCMD_SET_END -); - -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test_inject, - SHELL_CMD(sched, &sof_cmd_test_inject_sched, - "Scheduler injection commands\n", NULL), - SHELL_SUBCMD_SET_END -); - SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_test, - SHELL_CMD(inject, &sof_cmd_test_inject, - "Injection test commands\n", NULL), + SHELL_CMD_ARG(inject-sched-gap, NULL, + "Inject a gap to audio scheduling: [usec]\n", + cmd_sof_test_inject_sched_gap, 1, 1), SHELL_SUBCMD_SET_END ); #if CONFIG_SOF_SHELL_CORE_STATUS || CONFIG_SOF_SHELL_CORE_POWER SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_core, -#if CONFIG_SOF_SHELL_CORE_STATUS - SHELL_CMD(status, NULL, - "Print enabled/active state of each DSP core\n", - cmd_sof_core_status), -#endif #if CONFIG_SOF_SHELL_CORE_POWER SHELL_CMD_ARG(on, NULL, "Power on a secondary DSP core: \n" @@ -1821,33 +1699,8 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_core, ); #endif -#if CONFIG_SOF_SHELL_SRAM_STATUS -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_sram, - SHELL_CMD(status, NULL, - "Print HPSRAM heap usage statistics\n", - cmd_sof_sram_status), - SHELL_SUBCMD_SET_END -); -#endif - -#if CONFIG_SOF_SHELL_CLOCK_STATUS -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_clock, - SHELL_CMD(status, NULL, - "Print current clock frequency for each DSP core\n", - cmd_sof_clock_status), - SHELL_SUBCMD_SET_END -); -#endif - #if CONFIG_SOF_SHELL_MMU_DBG #if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mmu, - SHELL_CMD(status, NULL, - "Print Intel ADSP MTL TLB / virtual memory status\n", - cmd_sof_mmu_status), - SHELL_SUBCMD_SET_END -); - SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_tlb, SHELL_CMD(dump, NULL, "Dump all active TLB entries (vaddr/paddr/flags)\n", @@ -1912,19 +1765,7 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_llext, ); #endif -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_vpage, - SHELL_CMD(status, NULL, - "Print virtual page allocator status\n", - cmd_sof_vpage_info), - SHELL_SUBCMD_SET_END -); -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_vregion, - SHELL_CMD(status, NULL, - "Print virtual regions status\n", - cmd_sof_vregion_info), - SHELL_SUBCMD_SET_END -); SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_ipc, SHELL_CMD_ARG(stats, NULL, @@ -1948,14 +1789,7 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_sched, ); #endif -#if CONFIG_SOF_SHELL_LOG_INFO -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_log, - SHELL_CMD(status, NULL, - "List Zephyr log backends with state and source count\n", - cmd_sof_log_status), - SHELL_SUBCMD_SET_END -); -#endif + #if CONFIG_SOF_SHELL_MTRACE_DUMP SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_mtrace, @@ -1984,38 +1818,38 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dbgwin, ); #endif -#if CONFIG_SOF_SHELL_PERF_STATUS -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_perf, - SHELL_CMD_ARG(status, NULL, - "Show telemetry perf state and per-core systick;" - " optional [reset|start|stop|pause]\n", - cmd_sof_perf_status, 1, 1), - SHELL_SUBCMD_SET_END -); -#endif + SHELL_SUBCMD_ADD((sof), test, &sof_cmd_test, "Test commands\n", NULL, 0, 0); #if CONFIG_SOF_SHELL_CORE_STATUS || CONFIG_SOF_SHELL_CORE_POWER SHELL_SUBCMD_ADD((sof), core, &sof_cmd_core, - "Core commands\n", NULL, 0, 0); + "Core status & power control\n", +#if CONFIG_SOF_SHELL_CORE_STATUS + cmd_sof_core_status, 1, 0); +#else + NULL, 0, 0); +#endif #endif #if CONFIG_SOF_SHELL_SRAM_STATUS -SHELL_SUBCMD_ADD((sof), sram, &sof_cmd_sram, - "SRAM commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), sram, NULL, + "Print HPSRAM heap usage statistics\n", + cmd_sof_sram_status, 1, 0); #endif #if CONFIG_SOF_SHELL_CLOCK_STATUS -SHELL_SUBCMD_ADD((sof), clock, &sof_cmd_clock, - "Clock commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), clock, NULL, + "Print current clock frequency for each DSP core\n", + cmd_sof_clock_status, 1, 0); #endif #if CONFIG_SOF_SHELL_MMU_DBG #if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB -SHELL_SUBCMD_ADD((sof), mmu, &sof_cmd_mmu, - "MMU status commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), mmu, NULL, + "Print Intel ADSP MTL TLB / virtual memory status\n", + cmd_sof_mmu_status, 1, 0); SHELL_SUBCMD_ADD((sof), tlb, &sof_cmd_tlb, "TLB commands\n", NULL, 0, 0); #endif @@ -2027,13 +1861,20 @@ SHELL_SUBCMD_ADD((sof), page, &sof_cmd_page, #if CONFIG_SOF_SHELL_LLEXT_LOAD || CONFIG_SOF_SHELL_LLEXT_LIST || CONFIG_SOF_SHELL_LLEXT_PURGE || CONFIG_SOF_SHELL_LLEXT_CTOR || CONFIG_SOF_SHELL_LLEXT_CALL SHELL_SUBCMD_ADD((sof), llext, &sof_cmd_llext, - "LLEXT commands\n", NULL, 0, 0); + "LLEXT commands\n", +#if CONFIG_SOF_SHELL_LLEXT_LIST + cmd_sof_llext_list, 1, 0); +#else + NULL, 0, 0); +#endif #endif -SHELL_SUBCMD_ADD((sof), vpage, &sof_cmd_vpage, - "Virtual page commands\n", NULL, 0, 0); -SHELL_SUBCMD_ADD((sof), vregion, &sof_cmd_vregion, - "Virtual region commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), vpage, NULL, + "Print virtual page allocator status\n", + cmd_sof_vpage_info, 1, 0); +SHELL_SUBCMD_ADD((sof), vregion, NULL, + "Print virtual regions status\n", + cmd_sof_vregion_info, 1, 0); SHELL_SUBCMD_ADD((sof), ipc, &sof_cmd_ipc, "IPC commands\n", NULL, 0, 0); @@ -2043,8 +1884,9 @@ SHELL_SUBCMD_ADD((sof), sched, &sof_cmd_sched, #endif #if CONFIG_SOF_SHELL_LOG_INFO -SHELL_SUBCMD_ADD((sof), log, &sof_cmd_log, - "Log commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), log, NULL, + "List Zephyr log backends with state and source count\n", + cmd_sof_log_status, 1, 0); #endif #if CONFIG_SOF_SHELL_MTRACE_DUMP @@ -2063,8 +1905,9 @@ SHELL_SUBCMD_ADD((sof), dbgwin, &sof_cmd_dbgwin, #endif #if CONFIG_SOF_SHELL_PERF_STATUS -SHELL_SUBCMD_ADD((sof), perf, &sof_cmd_perf, - "Performance commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), perf, NULL, + "Show telemetry perf state and per-core systick; optional [reset|start|stop|pause]\n", + cmd_sof_perf_status, 1, 1); #endif SHELL_CMD_REGISTER(sof, &sub_sof, "SOF application commands", NULL); diff --git a/zephyr/shell/user.c b/zephyr/shell/user.c index d97926edb464..b9aa8264fab9 100644 --- a/zephyr/shell/user.c +++ b/zephyr/shell/user.c @@ -42,6 +42,7 @@ #include #include +#include #if CONFIG_SOF_SHELL_HEAP_USAGE __cold static int cmd_sof_module_heap_usage(const struct shell *sh, @@ -76,9 +77,7 @@ __cold static int cmd_sof_module_heap_usage(const struct shell *sh, return 0; } -SHELL_SUBCMD_ADD((sof), module_heap_usage, NULL, - "Print heap memory usage of each module\n", - cmd_sof_module_heap_usage, 0, 0); + #endif /* CONFIG_SOF_SHELL_HEAP_USAGE */ #if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_MODULE_STATUS @@ -141,11 +140,193 @@ __cold static int cmd_sof_pipeline_status(const struct shell *sh, return 0; } -SHELL_SUBCMD_ADD((sof), pipeline_status, NULL, - "Print status of all active pipelines\n", - cmd_sof_pipeline_status, 0, 0); + #endif /* CONFIG_SOF_SHELL_PIPELINE_STATUS */ +#if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_BUFFER_INFO + +__cold static const char *comp_dev_name(const struct comp_dev *cd) +{ + if (cd && cd->drv && cd->drv->tctx && cd->drv->tctx->uuid_p && + cd->drv->tctx->uuid_p->name[0]) + return cd->drv->tctx->uuid_p->name; + return "?"; +} + +__cold static const char *comp_id_name(struct ipc *ipc, uint32_t comp_id) +{ + struct ipc_comp_dev *icd = ipc_get_comp_by_id(ipc, comp_id); + + if (icd && icd->cd) + return comp_dev_name(icd->cd); + + return "?"; +} + +__cold static int shell_for_each_buffer(struct ipc *ipc, + void (*cb)(const struct shell *sh, + struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, + void *ctx), + const struct shell *sh, void *ctx); + +struct ppl_lat_calc_ctx { + uint32_t ppl_id; + uint64_t total_lat_us; + uint64_t total_max_lat_us; + int buf_count; + bool verbose; +}; + +__cold static void ppl_lat_buf_cb(const struct shell *sh, struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, void *ctx) +{ + struct ppl_lat_calc_ctx *calc = ctx; + const struct audio_stream *s = &buf->stream; + bool matches = false; + + if (buf->source && buf->source->pipeline && + buf->source->pipeline->pipeline_id == calc->ppl_id) + matches = true; + else if (buf->sink && buf->sink->pipeline && + buf->sink->pipeline->pipeline_id == calc->ppl_id) + matches = true; + + if (matches) { + uint32_t rate = audio_stream_get_rate(s); + uint32_t channels = audio_stream_get_channels(s); + uint32_t sample_bytes = audio_stream_sample_bytes(s); + uint32_t bytes_per_sec = rate * channels * sample_bytes; + uint32_t avail = audio_stream_get_avail(s); + uint32_t size = audio_stream_get_size(s); + uint64_t lat_us = 0; + uint64_t max_lat_us = 0; + + if (bytes_per_sec > 0) { + lat_us = ((uint64_t)avail * 1000000ULL) / bytes_per_sec; + max_lat_us = ((uint64_t)size * 1000000ULL) / bytes_per_sec; + } + + calc->total_lat_us += lat_us; + calc->total_max_lat_us += max_lat_us; + calc->buf_count++; + + if (calc->verbose) { + struct ipc *ipc = sof_get()->ipc; + const char *src_name = comp_id_name(ipc, src_id); + const char *sink_name = comp_id_name(ipc, sink_id); + + shell_print(sh, " [Buf 0x%08x] Comp 0x%08x (%s) -> Comp 0x%08x (%s)", + buf_get_id(buf), src_id, src_name, sink_id, sink_name); + shell_print(sh, " Format: %u Hz, %u ch, %u B/sample | Fill: %u / %u B", + rate, channels, sample_bytes, avail, size); + shell_print(sh, " Stage Latency: %u.%02u ms (%llu us) [Max Depth: %u.%02u ms]", + (uint32_t)(lat_us / 1000), (uint32_t)((lat_us % 1000) / 10), + (unsigned long long)lat_us, + (uint32_t)(max_lat_us / 1000), (uint32_t)((max_lat_us % 1000) / 10)); + } + } +} + +__cold static int cmd_sof_pipeline_latency(const struct shell *sh, size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + bool single_ppl = false; + uint32_t target_ppl_id = 0; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + if (argc > 1) { + char *endptr; + target_ppl_id = (uint32_t)strtoul(argv[1], &endptr, 0); + if (endptr != argv[1]) + single_ppl = true; + } + + if (!single_ppl) { + shell_print(sh, "%-10s %-5s %-10s %-10s %-20s %-20s %s", + "ppl_id", "core", "state", "period", "buffered_latency", "max_depth", "xrun_b"); + } + + list_for_item(clist, &ipc->comp_list) { + struct pipeline *p; + struct ppl_lat_calc_ctx calc = {0}; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_PIPELINE) + continue; + + p = icd->pipeline; + + if (single_ppl && p->pipeline_id != target_ppl_id) + continue; + + calc.ppl_id = p->pipeline_id; + calc.verbose = single_ppl; + + if (single_ppl) { + shell_print(sh, "Pipeline 0x%08x Latency Trace (Source -> Sink):", p->pipeline_id); + shell_print(sh, " Pipeline State: %s | Core: %u | Period: %u us | Priority: %u", + comp_state_str((uint16_t)p->status), p->core, p->period, p->priority); + if (p->source_comp) + shell_print(sh, " Source Comp: 0x%08x (%s)", + p->source_comp->ipc_config.id, + comp_dev_name(p->source_comp)); + } + + shell_for_each_buffer(ipc, ppl_lat_buf_cb, sh, &calc); + + if (single_ppl) { + if (p->sink_comp) + shell_print(sh, " Sink Comp: 0x%08x (%s)", + p->sink_comp->ipc_config.id, + comp_dev_name(p->sink_comp)); + + shell_print(sh, "Total Pipeline Latency: %u.%02u ms (%llu us) [Max Capacity: %u.%02u ms (%llu us)]", + (uint32_t)(calc.total_lat_us / 1000), (uint32_t)((calc.total_lat_us % 1000) / 10), + (unsigned long long)calc.total_lat_us, + (uint32_t)(calc.total_max_lat_us / 1000), (uint32_t)((calc.total_max_lat_us % 1000) / 10), + (unsigned long long)calc.total_max_lat_us); + } else { + char lat_str[24]; + char max_str[24]; + char period_str[12]; + + snprintf(lat_str, sizeof(lat_str), "%u.%02u ms (%llu us)", + (uint32_t)(calc.total_lat_us / 1000), (uint32_t)((calc.total_lat_us % 1000) / 10), + (unsigned long long)calc.total_lat_us); + + snprintf(max_str, sizeof(max_str), "%u.%02u ms (%llu us)", + (uint32_t)(calc.total_max_lat_us / 1000), (uint32_t)((calc.total_max_lat_us % 1000) / 10), + (unsigned long long)calc.total_max_lat_us); + + snprintf(period_str, sizeof(period_str), "%u us", p->period); + + shell_print(sh, "0x%-8x %-5u %-10s %-10s %-20s %-20s %d B", + p->pipeline_id, p->core, + comp_state_str((uint16_t)p->status), + period_str, lat_str, max_str, p->xrun_bytes); + } + count++; + } + + if (!count) { + if (single_ppl) + shell_print(sh, "Pipeline 0x%08x not found.", target_ppl_id); + else + shell_print(sh, "No active pipelines found."); + } + + return 0; +} +#endif + #if CONFIG_SOF_SHELL_MODULE_STATUS __cold static int cmd_sof_module_status(const struct shell *sh, size_t argc, char *argv[]) @@ -160,18 +341,19 @@ __cold static int cmd_sof_module_status(const struct shell *sh, return 0; } - shell_print(sh, "%-12s %-8s %-5s %s", - "comp_id", "ppl_id", "core", "state"); + shell_print(sh, "%-12s %-8s %-5s %-24s %s", + "comp_id", "ppl_id", "core", "module", "state"); list_for_item(clist, &ipc->comp_list) { icd = container_of(clist, struct ipc_comp_dev, list); if (icd->type != COMP_TYPE_COMPONENT) continue; - shell_print(sh, "0x%-10x %-8u %-5u %s", + shell_print(sh, "0x%-10x %-8u %-5u %-24s %s", icd->id, icd->cd->pipeline ? icd->cd->pipeline->pipeline_id : 0, icd->core, + comp_dev_name(icd->cd), comp_state_str(icd->cd->state)); count++; } @@ -182,9 +364,7 @@ __cold static int cmd_sof_module_status(const struct shell *sh, return 0; } -SHELL_SUBCMD_ADD((sof), module_status, NULL, - "Print status of all active components\n", - cmd_sof_module_status, 0, 0); + #endif /* CONFIG_SOF_SHELL_MODULE_STATUS */ #if CONFIG_SOF_SHELL_MODULE_LIST @@ -335,10 +515,7 @@ __cold static int cmd_sof_module_list(const struct shell *sh, return 0; } -SHELL_SUBCMD_ADD((sof), module_list, NULL, - "List all available modules (name, inst, cpc, text, bss)\n" - " [-v] also show uuid, affinity, cps, ibs, obs\n", - cmd_sof_module_list, 1, 1); + #endif /* CONFIG_SOF_SHELL_MODULE_LIST */ #if CONFIG_SOF_SHELL_PIPELINE_OPS @@ -720,27 +897,7 @@ __cold static int cmd_sof_mod_unbind(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_PIPELINE_OPS */ #if CONFIG_SOF_SHELL_PIPELINE_OPS -SHELL_SUBCMD_ADD((sof), ppl_create, NULL, - "Create IPC4 pipeline: [priority=0] [pages=2] [core=0] [lp=0]\n", - cmd_sof_ppl_create, 2, 4); -SHELL_SUBCMD_ADD((sof), ppl_delete, NULL, - "Delete IPC4 pipeline: \n", - cmd_sof_ppl_delete, 2, 0); -SHELL_SUBCMD_ADD((sof), ppl_state, NULL, - "Set IPC4 pipeline state: \n", - cmd_sof_ppl_state, 3, 0); -SHELL_SUBCMD_ADD((sof), mod_init, NULL, - "Instantiate module: [core=0] [dp=0]\n", - cmd_sof_mod_init, 4, 2); -SHELL_SUBCMD_ADD((sof), mod_delete, NULL, - "Delete module instance: \n", - cmd_sof_mod_delete, 3, 0); -SHELL_SUBCMD_ADD((sof), mod_bind, NULL, - "Bind two module instances: [src_q=0] [dst_q=0]\n", - cmd_sof_mod_bind, 5, 2); -SHELL_SUBCMD_ADD((sof), mod_unbind, NULL, - "Unbind two module instances: [src_q=0] [dst_q=0]\n", - cmd_sof_mod_unbind, 5, 2); + #endif /* CONFIG_SOF_SHELL_PIPELINE_OPS */ __cold static int cmd_sof_pipeline_list(const struct shell *sh, size_t argc, char *argv[]) @@ -768,9 +925,7 @@ __cold static int cmd_sof_pipeline_list(const struct shell *sh, size_t argc, cha return 0; } -SHELL_SUBCMD_ADD((sof), pipeline_list, NULL, - "List all active audio pipelines\n", - cmd_sof_pipeline_list, 0, 0); + #if CONFIG_SOF_SHELL_BUFFER_INFO @@ -927,14 +1082,7 @@ __cold static int cmd_sof_buffer_info(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_BUFFER_INFO */ -#if CONFIG_SOF_SHELL_BUFFER_INFO -SHELL_SUBCMD_ADD((sof), buffer_list, NULL, - "List all audio buffers (id, source/sink, fill, format)\n", - cmd_sof_buffer_list, 0, 0); -SHELL_SUBCMD_ADD((sof), buffer_info, NULL, - "Detailed info for a single buffer: \n", - cmd_sof_buffer_info, 2, 0); -#endif + #if CONFIG_SOF_SHELL_DAI_LIST || CONFIG_SOF_SHELL_DMA_STATUS #include @@ -1131,19 +1279,7 @@ __cold static int cmd_sof_dma_status(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_DMA_STATUS */ -#if CONFIG_SOF_SHELL_DAI_LIST -SHELL_SUBCMD_ADD((sof), dai_list, NULL, - "List all registered DAIs (name, type, channels, rate)\n" - " [-v] also show TX/RX fifo address, depth, hs, stream\n", - cmd_sof_dai_list, 1, 1); -#endif -#if CONFIG_SOF_SHELL_DMA_STATUS -SHELL_SUBCMD_ADD((sof), dma_status, NULL, - "List DMA controllers, or per-channel status: " - "[dma_idx] [chan]\n", - cmd_sof_dma_status, 1, 2); -#endif #if CONFIG_SOF_SHELL_KCTL_LIST @@ -1247,26 +1383,14 @@ __cold static int cmd_sof_kctl_list(const struct shell *sh, #endif /* CONFIG_SOF_SHELL_KCTL_LIST */ -#if CONFIG_SOF_SHELL_KCTL_LIST -SHELL_SUBCMD_ADD((sof), kctl_list, NULL, - "List components and their decoded module name / kind\n", - cmd_sof_kctl_list, 0, 0); -#endif + #if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module_heap, +SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module, #if CONFIG_SOF_SHELL_HEAP_USAGE - SHELL_CMD(usage, NULL, + SHELL_CMD(heap, NULL, "Print heap memory usage of each module\n", cmd_sof_module_heap_usage), -#endif - SHELL_SUBCMD_SET_END -); - -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module, -#if CONFIG_SOF_SHELL_HEAP_USAGE - SHELL_CMD(heap, &sof_cmd_module_heap, - "Module heap commands\n", NULL), #endif #if CONFIG_SOF_SHELL_MODULE_STATUS SHELL_CMD(status, NULL, @@ -1293,6 +1417,9 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_pipeline, SHELL_CMD(list, NULL, "List all active audio pipelines\n", cmd_sof_pipeline_list), + SHELL_CMD_ARG(latency, NULL, + "Calculate pipeline latency from sink to source: [ppl_id]\n", + cmd_sof_pipeline_latency, 1, 1), SHELL_SUBCMD_SET_END ); #endif @@ -1342,43 +1469,26 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_buffer, ); #endif -#if CONFIG_SOF_SHELL_DAI_LIST -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dai, - SHELL_CMD_ARG(list, NULL, - "List all registered DAIs (name, type, channels, rate)\n" - " [-v] also show TX/RX fifo address, depth, hs, stream\n", - cmd_sof_dai_list, 1, 1), - SHELL_SUBCMD_SET_END -); -#endif -#if CONFIG_SOF_SHELL_DMA_STATUS -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_dma, - SHELL_CMD_ARG(status, NULL, - "List DMA controllers, or per-channel status: " - "[dma_idx] [chan]\n", - cmd_sof_dma_status, 1, 2), - SHELL_SUBCMD_SET_END -); -#endif - -#if CONFIG_SOF_SHELL_KCTL_LIST -SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_kctl, - SHELL_CMD(list, NULL, - "List components and their decoded module name / kind\n", - cmd_sof_kctl_list), - SHELL_SUBCMD_SET_END -); -#endif #if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST SHELL_SUBCMD_ADD((sof), module, &sof_cmd_module, - "Module commands\n", NULL, 0, 0); + "Module commands\n", +#if CONFIG_SOF_SHELL_MODULE_STATUS + cmd_sof_module_status, 1, 0); +#else + NULL, 0, 0); +#endif #endif #if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_PIPELINE_OPS SHELL_SUBCMD_ADD((sof), pipeline, &sof_cmd_pipeline, - "Pipeline commands\n", NULL, 0, 0); + "Pipeline commands\n", +#if CONFIG_SOF_SHELL_PIPELINE_STATUS + cmd_sof_pipeline_status, 1, 0); +#else + NULL, 0, 0); +#endif #endif #if CONFIG_SOF_SHELL_PIPELINE_OPS @@ -1390,20 +1500,127 @@ SHELL_SUBCMD_ADD((sof), mod, &sof_cmd_mod, #if CONFIG_SOF_SHELL_BUFFER_INFO SHELL_SUBCMD_ADD((sof), buffer, &sof_cmd_buffer, - "Buffer commands\n", NULL, 0, 0); + "Buffer commands\n", + cmd_sof_buffer_list, 1, 0); #endif #if CONFIG_SOF_SHELL_DAI_LIST -SHELL_SUBCMD_ADD((sof), dai, &sof_cmd_dai, - "DAI commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), dai, NULL, + "List all registered DAIs (name, type, channels, rate)\n" + " [-v] also show TX/RX fifo address, depth, hs, stream\n", + cmd_sof_dai_list, 1, 1); #endif #if CONFIG_SOF_SHELL_DMA_STATUS -SHELL_SUBCMD_ADD((sof), dma, &sof_cmd_dma, - "DMA commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), dma, NULL, + "List DMA controllers, or per-channel status: [dma_idx] [chan]\n", + cmd_sof_dma_status, 1, 2); #endif #if CONFIG_SOF_SHELL_KCTL_LIST -SHELL_SUBCMD_ADD((sof), kctl, &sof_cmd_kctl, - "Kernel-control commands\n", NULL, 0, 0); +SHELL_SUBCMD_ADD((sof), kctl, NULL, + "List components and their decoded module name / kind\n", + cmd_sof_kctl_list, 1, 0); +#endif + +#if CONFIG_SOF_SHELL_PIPELINE_STATUS || CONFIG_SOF_SHELL_BUFFER_INFO + +struct stream_agg_ctx { + uint32_t ppl_id; + uint32_t total_size; + uint32_t total_avail; + uint32_t sample_rate; + uint32_t channels; + int fmt; + int buf_count; +}; + +__cold static void stream_buf_cb(const struct shell *sh, struct comp_buffer *buf, + uint32_t src_id, uint32_t sink_id, void *ctx) +{ + struct stream_agg_ctx *agg = ctx; + const struct audio_stream *s = &buf->stream; + bool matches = false; + + if (buf->source && buf->source->pipeline && + buf->source->pipeline->pipeline_id == agg->ppl_id) + matches = true; + else if (buf->sink && buf->sink->pipeline && + buf->sink->pipeline->pipeline_id == agg->ppl_id) + matches = true; + + if (matches) { + agg->buf_count++; + agg->total_size += audio_stream_get_size(s); + agg->total_avail += audio_stream_get_avail(s); + if (!agg->sample_rate) { + agg->sample_rate = audio_stream_get_rate(s); + agg->channels = audio_stream_get_channels(s); + agg->fmt = (int)audio_stream_get_frm_fmt(s); + } + } +} + +__cold static int cmd_sof_stream(const struct shell *sh, size_t argc, char *argv[]) +{ + struct ipc *ipc = sof_get()->ipc; + struct list_item *clist; + struct ipc_comp_dev *icd; + int count = 0; + + if (!ipc) { + shell_print(sh, "No IPC"); + return 0; + } + + shell_print(sh, "%-10s %-6s %-5s %-10s %-14s %-16s %s", + "ppl_id", "dir", "core", "state", "fmt/rate", "fill_level", "xrun_b"); + + list_for_item(clist, &ipc->comp_list) { + struct pipeline *p; + struct stream_agg_ctx agg = {0}; + const char *dir_str = "PB"; + + icd = container_of(clist, struct ipc_comp_dev, list); + if (icd->type != COMP_TYPE_PIPELINE) + continue; + + p = icd->pipeline; + agg.ppl_id = p->pipeline_id; + + shell_for_each_buffer(ipc, stream_buf_cb, sh, &agg); + + if ((p->source_comp && p->source_comp->direction == SOF_IPC_STREAM_CAPTURE) || + (p->sink_comp && p->sink_comp->direction == SOF_IPC_STREAM_CAPTURE)) + dir_str = "CAP"; + + if (agg.sample_rate) { + char fmt_str[16]; + char fill_str[20]; + + snprintf(fmt_str, sizeof(fmt_str), "%uHz/%uch", agg.sample_rate, agg.channels); + snprintf(fill_str, sizeof(fill_str), "%u/%u B", agg.total_avail, agg.total_size); + + shell_print(sh, "0x%-8x %-6s %-5u %-10s %-14s %-16s %d B", + p->pipeline_id, dir_str, p->core, + comp_state_str((uint16_t)p->status), + fmt_str, fill_str, p->xrun_bytes); + } else { + shell_print(sh, "0x%-8x %-6s %-5u %-10s %-14s %-16s %d B", + p->pipeline_id, dir_str, p->core, + comp_state_str((uint16_t)p->status), + "-", "-/0 B", p->xrun_bytes); + } + count++; + } + + if (!count) + shell_print(sh, "No active audio streams. Start playback/capture on host."); + + return 0; +} + +SHELL_SUBCMD_ADD((sof), stream, NULL, + "List active audio streams: direction, rate/ch, fill level, xruns\n", + cmd_sof_stream, 1, 0); #endif From 3381670f6a03263a83883da01bd83cf7aec2a797 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 14:28:50 +0100 Subject: [PATCH 08/10] zephyr/shell: add 'sof stream' and 'sof pipeline latency' diagnostic commands --- scripts/sof-shell-validate.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/sof-shell-validate.py b/scripts/sof-shell-validate.py index 154732bfbe64..245b54e08ec5 100755 --- a/scripts/sof-shell-validate.py +++ b/scripts/sof-shell-validate.py @@ -41,15 +41,16 @@ # Basic info ("sof version", ["SOF Version"]), # Heap - ("sof module heap usage", ["No components found", "comp id"]), + ("sof module heap", ["No components found", "comp id"]), # Pipeline / module introspection - # pipeline_list always prints "ID Core Status Priority Period" header + # pipeline list always prints "ID Core Status Priority Period" header ("sof pipeline list", ["ID"]), - ("sof pipeline status", ["No pipelines found", "ppl_id"]), - ("sof module status", ["No components found", "comp_id"]), - # Hardware / platform (sof sram_status / sof clock_status require - # CONFIG_SOF_SHELL_SRAM_STATUS / _CLOCK_STATUS which are not in this build) - ("sof core status", ["core", "enabled"]), + ("sof pipeline", ["No pipelines found", "ppl_id"]), + ("sof pipeline latency", ["No active pipelines", "ppl_id"]), + ("sof module", ["No components found", "comp_id"]), + ("sof stream", ["ppl_id", "No active audio streams"]), + # Hardware / platform + ("sof core", ["core", "enabled"]), # IPC ("sof ipc stats", ["rx_count", "tx_count"]), ("sof ipc last", []), # ok even when empty (no IPC yet) @@ -58,12 +59,12 @@ ("sof sched tasks", ["Active scheduler"]), ("sof sched load", ["Scheduler task cycle"]), # Logging - ("sof log status", ["Log backends"]), + ("sof log", ["Log backends"]), # Controls ("sof kctl list", ["No components found", "comp_id"]), # DAI and DMA ("sof dai list", ["DAI", "dai", "SSP", "HDA", "DMIC", "ALH", "UAOL", "SAI", "ESAI"]), - ("sof dma status", ["No DMA controllers", "DMA controller"]), + ("sof dma", ["No DMA controllers", "DMA controller"]), # Verbose/multi-line commands last ("sof module list", ["Module", "module", "UUID"]), ("sof mailbox hex", ["Mailbox regions", "exception"]), From c49c17d468909260c53435354ed08f2d28f2b3a5 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 14:28:51 +0100 Subject: [PATCH 09/10] doc: add comprehensive SOF Zephyr shell documentation (SHELL.md) --- SHELL.md | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/SHELL.md b/SHELL.md index 9f706f87a5f5..049d286b10b4 100644 --- a/SHELL.md +++ b/SHELL.md @@ -17,36 +17,35 @@ uart:~$ sof [arguments] - **Usage**: `sof version` - **Output**: Major/Minor/Micro version, Git tag, and source hash. -### 2. `sof module_heap_usage` -- **Description**: Dumps heap memory usage for all active audio modules. -- **Usage**: `sof module_heap_usage` -- **Output**: Component ID, current heap usage, and high-water mark (HWM) in bytes. - -### 3. `sof pipeline_list` -- **Description**: Lists all active audio pipelines in the system. -- **Usage**: `sof pipeline_list` -- **Output**: Pipeline ID, Core affinity, Status, Priority, and Period (us). - -### 4. `sof module_list` -- **Description**: Lists all instantiated audio modules (components). -- **Usage**: `sof module_list` -- **Output**: Component ID, Module Type, State, Pipeline ID, and Core affinity. - -### 5. `sof vpage_status` +### 2. `sof module` / `sof module heap` / `sof module list` +- **Description**: Dumps component status, heap memory usage, or instantiated module list. +- **Usage**: `sof module`, `sof module heap`, or `sof module list` + +### 3. `sof stream` +- **Description**: Inspects active host audio streams (direction, core, state, rate/channels, buffer fill level, and XRUN bytes). +- **Usage**: `sof stream` + +### 4. `sof pipeline` / `sof pipeline list` / `sof pipeline latency` +- **Description**: Reports pipeline status, lists active audio pipelines, or calculates total buffered audio latency from sink to source. +- **Usage**: `sof pipeline`, `sof pipeline list`, or `sof pipeline latency [ppl_id]` + +### 4. `sof core` +- **Description**: Prints enabled/active state of DSP cores (`sof core`) or powers secondary cores on/off (`sof core on `). +- **Usage**: `sof core` + +### 5. `sof vpage` - **Description**: Reports the status of the virtual page allocator. -- **Usage**: `sof vpage_status` -- **Output**: Base address, total/free pages, and a list of active virtual page allocations. +- **Usage**: `sof vpage` - **Dependency**: Requires `CONFIG_SOF_VREGIONS=y`. -### 6. `sof vregion_status` +### 6. `sof vregion` - **Description**: Reports status and metrics for all active virtual memory regions. -- **Usage**: `sof vregion_status` -- **Output**: Region base addresses, sizes, lifetime bytes used/free, and current reference counts. +- **Usage**: `sof vregion` - **Dependency**: Requires `CONFIG_SOF_VREGIONS=y`. -### 7. `sof test_inject_sched_gap` +### 7. `sof test inject-sched-gap` - **Description**: Injects a timing delay into the audio scheduling domain for stress testing. -- **Usage**: `sof test_inject_sched_gap [usec]` +- **Usage**: `sof test inject-sched-gap [usec]` - **Arguments**: `usec` (optional, default 1500) - microseconds to block the domain. - **Warning**: Not reliable on SMP systems without explicit cross-core support. From 301517b012e2101a852a86711ba175c6d687c200 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Sun, 26 Jul 2026 14:31:54 +0100 Subject: [PATCH 10/10] zephyr/shell: attach pipeline lifecycle and binding subcommands to sof pipeline and sof module --- zephyr/shell/user.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/zephyr/shell/user.c b/zephyr/shell/user.c index b9aa8264fab9..d11247c67425 100644 --- a/zephyr/shell/user.c +++ b/zephyr/shell/user.c @@ -1385,7 +1385,7 @@ __cold static int cmd_sof_kctl_list(const struct shell *sh, -#if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST +#if CONFIG_SOF_SHELL_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST || CONFIG_SOF_SHELL_PIPELINE_OPS SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module, #if CONFIG_SOF_SHELL_HEAP_USAGE SHELL_CMD(heap, NULL, @@ -1402,6 +1402,22 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_module, "List all available modules (name, inst, cpc, text, bss)\n" " [-v] also show uuid, affinity, cps, ibs, obs\n", cmd_sof_module_list, 1, 1), +#endif +#if CONFIG_SOF_SHELL_PIPELINE_OPS + SHELL_CMD_ARG(init, NULL, + "Instantiate module: [core=0] [dp=0]\n", + cmd_sof_mod_init, 4, 2), + SHELL_CMD_ARG(delete, NULL, + "Delete module instance: \n", + cmd_sof_mod_delete, 3, 0), + SHELL_CMD_ARG(bind, NULL, + "Bind two module instances: " + " [src_q=0] [dst_q=0]\n", + cmd_sof_mod_bind, 5, 2), + SHELL_CMD_ARG(unbind, NULL, + "Unbind two module instances: " + " [src_q=0] [dst_q=0]\n", + cmd_sof_mod_unbind, 5, 2), #endif SHELL_SUBCMD_SET_END ); @@ -1420,6 +1436,17 @@ SHELL_STATIC_SUBCMD_SET_CREATE(sof_cmd_pipeline, SHELL_CMD_ARG(latency, NULL, "Calculate pipeline latency from sink to source: [ppl_id]\n", cmd_sof_pipeline_latency, 1, 1), +#if CONFIG_SOF_SHELL_PIPELINE_OPS + SHELL_CMD_ARG(create, NULL, + "Create IPC4 pipeline: [priority=0] [pages=2] [core=0] [lp=0]\n", + cmd_sof_ppl_create, 2, 4), + SHELL_CMD_ARG(delete, NULL, + "Delete IPC4 pipeline: \n", + cmd_sof_ppl_delete, 2, 0), + SHELL_CMD_ARG(state, NULL, + "Set IPC4 pipeline state: \n", + cmd_sof_ppl_state, 3, 0), +#endif SHELL_SUBCMD_SET_END ); #endif