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..049d286b10b4 --- /dev/null +++ b/SHELL.md @@ -0,0 +1,430 @@ +# 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` / `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` +- **Dependency**: Requires `CONFIG_SOF_VREGIONS=y`. + +### 6. `sof vregion` +- **Description**: Reports status and metrics for all active virtual memory regions. +- **Usage**: `sof vregion` +- **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/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/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/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/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/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-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-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/scripts/sof-shell-validate.py b/scripts/sof-shell-validate.py new file mode 100755 index 000000000000..245b54e08ec5 --- /dev/null +++ b/scripts/sof-shell-validate.py @@ -0,0 +1,406 @@ +#!/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", ["No components found", "comp id"]), + # Pipeline / module introspection + # pipeline list always prints "ID Core Status Priority Period" header + ("sof pipeline list", ["ID"]), + ("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) + # Buffers / scheduler + ("sof buffer list", ["Audio buffers"]), + ("sof sched tasks", ["Active scheduler"]), + ("sof sched load", ["Scheduler task cycle"]), + # Logging + ("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", ["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/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/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/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/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/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/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/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 */ 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/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 /** 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) 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/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/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__ */ 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); diff --git a/zephyr/shell/kernel.c b/zephyr/shell/kernel.c new file mode 100644 index 000000000000..e94ff50454c0 --- /dev/null +++ b/zephyr/shell/kernel.c @@ -0,0 +1,1913 @@ +// 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; +} + + + +#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++) { + bool is_curr = (i == cs.current); + + shell_print(sh, "%-6u %-8s %s", + i, cs.enabled[i] ? "yes" : "no", + is_curr ? "<--" : ""); + } + + return 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; +} +#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; +} +#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 +#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_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; +} + +#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 +#endif + +#if CONFIG_SOF_SHELL_LLEXT_PURGE +#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 +#endif + +#if CONFIG_SOF_SHELL_LLEXT_CALL && CONFIG_LLEXT +#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 +#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; +} + + + +__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; +} + + +#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 +#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 +#endif + +#if CONFIG_SOF_SHELL_MTRACE_DUMP +#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 +#endif + +#if CONFIG_SOF_SHELL_DBGWIN_DUMP +#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 +#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, + 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_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_MMU_DBG +#if CONFIG_MM_DRV_INTEL_ADSP_MTL_TLB +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_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_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 + + + +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 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, 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, 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, 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 +#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", +#if CONFIG_SOF_SHELL_LLEXT_LIST + cmd_sof_llext_list, 1, 0); +#else + NULL, 0, 0); +#endif +#endif + +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); + +#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, NULL, + "List Zephyr log backends with state and source count\n", + cmd_sof_log_status, 1, 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, 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/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..d11247c67425 --- /dev/null +++ b/zephyr/shell/user.c @@ -0,0 +1,1653 @@ +// 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 +#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; +} + + +#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; +} + + +#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[]) +{ + 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 %-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 %-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++; + } + + if (!count) + shell_print(sh, "No components found. Start an audio stream first."); + + return 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; +} + + +#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 + +#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; +} + + + +#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_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_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_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, + "Print heap memory usage of each module\n", + cmd_sof_module_heap_usage), +#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 +#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 +); +#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_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 + +#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_HEAP_USAGE || CONFIG_SOF_SHELL_MODULE_STATUS || CONFIG_SOF_SHELL_MODULE_LIST +SHELL_SUBCMD_ADD((sof), module, &sof_cmd_module, + "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", +#if CONFIG_SOF_SHELL_PIPELINE_STATUS + cmd_sof_pipeline_status, 1, 0); +#else + NULL, 0, 0); +#endif +#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", + cmd_sof_buffer_list, 1, 0); +#endif + +#if CONFIG_SOF_SHELL_DAI_LIST +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, 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, 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 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);