diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index da9db799d387..832d4eb12098 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -10,6 +10,6 @@ target_sources(app PRIVATE ) zephyr_library_include_directories(app PUBLIC - ${sof_module}/src/arch/xtensa/include - ${sof_module}/src/include + ${CMAKE_CURRENT_SOURCE_DIR}/../src/arch/xtensa/include + ${CMAKE_CURRENT_SOURCE_DIR}/../src/include ) diff --git a/app/boards/qemu_xtensa_dc233c.conf b/app/boards/qemu_xtensa_dc233c.conf new file mode 100644 index 000000000000..effed89802af --- /dev/null +++ b/app/boards/qemu_xtensa_dc233c.conf @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: BSD-3-Clause +CONFIG_IPC_MAJOR_4=y +CONFIG_ZTEST=y +CONFIG_MM_DRV=y +CONFIG_ZEPHYR_NATIVE_DRIVERS=y + +# Ensure the kernel can exit QEMU on shutdown/panic +CONFIG_REBOOT=y diff --git a/app/boards/qemu_xtensa_dc233c_mmu.conf b/app/boards/qemu_xtensa_dc233c_mmu.conf new file mode 100644 index 000000000000..b2975fc0bc6c --- /dev/null +++ b/app/boards/qemu_xtensa_dc233c_mmu.conf @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: BSD-3-Clause +CONFIG_IPC_MAJOR_4=y +CONFIG_USERSPACE=y +CONFIG_ZTEST=y +CONFIG_TEST_USERSPACE=y +CONFIG_MM_DRV=y +CONFIG_ZEPHYR_NATIVE_DRIVERS=y +CONFIG_SOF_USERSPACE_LL=y +CONFIG_SOF_BOOT_TEST_STANDALONE=y + +# Ensure the kernel can exit QEMU on shutdown/panic +CONFIG_REBOOT=y diff --git a/app/prj.conf b/app/prj.conf index 260488e7852b..75e07dfde05c 100644 --- a/app/prj.conf +++ b/app/prj.conf @@ -50,3 +50,4 @@ CONFIG_SCHED_CPU_MASK=y CONFIG_SYS_CLOCK_TICKS_PER_SEC=15000 CONFIG_DAI=y CONFIG_HEAP_MEM_POOL_SIZE=2048 +CONFIG_SPIN_VALIDATE=n diff --git a/app/src/main.c b/app/src/main.c index 30f755f6a9c6..b41b7e16e429 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -5,10 +5,17 @@ */ #include - +#include #include + LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); +/* define qemu boot tests if any qemu target is defined, add targets to end */ +#if defined(CONFIG_BOARD_QEMU_XTENSA_DC233C) ||\ + defined(CONFIG_BOARD_QEMU_XTENSA_DC233C_MMU) +#define QEMU_BOOT_TESTS +#endif + /** * Should be included from sof/schedule/task.h * but triggers include chain issue @@ -50,10 +57,29 @@ static int sof_app_main(void) return 0; } +#if CONFIG_SOF_BOOT_TEST && defined(QEMU_BOOT_TESTS) +/* cleanly exit qemu so CI can continue and check test results */ +static inline void qemu_xtensa_exit(int status) { + register int syscall_id __asm__ ("a2") = 1; /* SYS_exit is 1 */ + register int exit_status __asm__ ("a3") = status; + + __asm__ __volatile__ ( + "simcall\n" + : + : "r" (syscall_id), "r" (exit_status) + : "memory" + ); +} +#endif + #if CONFIG_ZTEST void test_main(void) { sof_app_main(); +#if CONFIG_SOF_BOOT_TEST && defined(QEMU_BOOT_TESTS) + sof_run_boot_tests(); + qemu_xtensa_exit(0); +#endif } #else int main(void) diff --git a/scripts/set_xtensa_params.sh b/scripts/set_xtensa_params.sh index 4bdaf93c809d..a3932818edc1 100644 --- a/scripts/set_xtensa_params.sh +++ b/scripts/set_xtensa_params.sh @@ -150,6 +150,12 @@ case "$platform" in HOST="xtensa-mt8365-elf" TOOLCHAIN_VER="RG-2018.9-linux" ;; + qemu_xtensa | qemu_xtensa_mmu) + PLATFORM="$1" + XTENSA_CORE="" + HOST="xtensa-zephyr-elf" + TOOLCHAIN_VER="" + ;; *) >&2 printf 'Unknown xtensa platform=%s\n' "$platform" return 1 @@ -166,6 +172,8 @@ esac # For Zephyr unit tests case "$platform" in + qemu_xtensa | qemu_xtensa_mmu) + ZEPHYR_TOOLCHAIN_VARIANT='zephyr';; imx8*|mtl|lnl) ZEPHYR_TOOLCHAIN_VARIANT='xt-clang';; *) # The previous, main case/esac already caught invalid input. diff --git a/scripts/sof-crash-decode.py b/scripts/sof-crash-decode.py new file mode 100755 index 000000000000..753a7f2f4882 --- /dev/null +++ b/scripts/sof-crash-decode.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2024 Intel Corporation. All rights reserved. +""" +decode_crash.py - Zephyr Xtensa Crash Dump Decoder + +Parses a Zephyr crash dump, extracts CPU registers/backtraces, and correlates them +to source code files and function names using the `objdump` output of the ELF file. + +Dependencies: + - python3 + - binutils for your target architecture (e.g. xtensa-zephyr-elf-objdump) + - Optional: `xclip`, `xsel`, or `wl-paste` (for --clipboard support on Linux) + +Usage Examples: + # 1. Provide the ELF and read crash from stdin + cat crash.txt | ./decode_crash.py --elf zephyr.elf + + # 2. Automatically locate ELF/objdump from a Zephyr build directory, read crash from file + ./decode_crash.py --build-dir build-qemu_xtensa/ --dump crash.txt + + # 3. Read directly from the system clipboard + ./decode_crash.py --build-dir build-qemu_xtensa/ --clipboard + + # 4. Pipe a live trace to the decoder + tail -f log.txt | ./decode_crash.py --build-dir build_dir/ + +""" + +import sys +import re +import argparse +import subprocess +import bisect +import os +import json +import shlex + +XTENSA_EXCCAUSE = { + 0: "No Error (or IllegalInstruction)", + 1: "Syscall", + 2: "InstructionFetchError", + 3: "LoadStoreError", + 4: "Level1Interrupt", + 5: "Alloca", + 6: "IntegerDivideByZero", + 8: "Privileged", + 9: "LoadStoreAlignment", + 12: "InstrPIFDataError", + 13: "LoadStorePIFDataError", + 14: "InstrPIFAddrError", + 15: "LoadStorePIFAddrError", + 16: "InstTLBMiss", + 17: "InstTLBMultiHit", + 18: "InstFetchPrivilege", + 20: "InstFetchProhibited", + 24: "LoadStoreTLBMiss", + 25: "LoadStoreTLBMultiHit", + 26: "LoadStorePrivilege", + 28: "LoadStoreProhibited", + 32: "Coprocessor0Disabled", + 33: "Coprocessor1Disabled", + 34: "Coprocessor2Disabled", + 35: "Coprocessor3Disabled", + 36: "Coprocessor4Disabled", + 37: "Coprocessor5Disabled", + 38: "Coprocessor6Disabled", + 39: "Coprocessor7Disabled", +} + +def parse_crash_log(content): + + registers = {} + backtraces = [] + + # Detect QEMU format + if re.search(r'\bPC=[0-9a-fA-F]+\b', content): + reg_pattern = re.compile(r'\b([A-Z0-9]+)=([0-9a-fA-F]+)\b') + for match in reg_pattern.finditer(content): + reg = match.group(1) + val = int(match.group(2), 16) + + if reg == 'EXCVADDR': + reg = 'VADDR' + elif re.match(r'^A\d{2}$', reg): + reg = f"A{int(reg[1:])}" + elif re.match(r'^AR\d{2}$', reg): + reg = f"AR{int(reg[2:])}" + + if re.match(r'^(PC|PR|SP|A\d+|AR\d+|EXCCAUSE|VADDR|LBEG|LEND|SAR|EPC\d+|EPS\d+|PS)$', reg): + registers[reg] = val + else: + # Standard format + # regex for registers: we want standalone pairs like PC 0x123 or A0 0x123 or EXCCAUSE 9 + reg_pattern = re.compile(r'\b([A-Z0-9]+)\s+(0x[0-9a-fA-F]+|\d+|(?:nil))\b') + for match in reg_pattern.finditer(content): + reg = match.group(1) + val_str = match.group(2) + if val_str == "(nil)": + val = 0 + elif val_str.startswith("0x"): + val = int(val_str, 16) + else: + val = int(val_str) + + # Keep only known registers or likely candidates + if re.match(r'^(PC|PR|SP|A\d+|AR\d+|EXCCAUSE|VADDR|LBEG|LEND|SAR|EPC\d+|EPS\d+|PS)$', reg): + registers[reg] = val + + # Backtrace parsing + bt_idx = content.find("Backtrace:") + if bt_idx != -1: + bt_line = content[bt_idx:content.find('\n', bt_idx)] + bt_pattern = re.compile(r'(0x[0-9a-fA-F]+):(0x[0-9a-fA-F]+)') + for match in bt_pattern.finditer(bt_line): + pc = int(match.group(1), 16) + sp = int(match.group(2), 16) + backtraces.append((pc, sp)) + + return registers, backtraces, content + +def get_objdump_output(elf_path, objdump_cmd): + print(f"Running {objdump_cmd} -d -S -l \"{elf_path}\" ...") + try: + # Check if objdump exists + result = subprocess.run([objdump_cmd, "-d", "-S", "-l", elf_path], + capture_output=True, text=True, check=True) + return result.stdout + except FileNotFoundError: + print(f"Error: {objdump_cmd} not found. Please provide the correct objdump command using --objdump.") + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"Error running objdump: {e}") + sys.exit(1) + +def parse_linker_cmd(filepath): + regions = [] + try: + with open(filepath, 'r') as f: + content = f.read() + m_block = re.search(r'MEMORY\s*\{(.*?)\}', content, re.DOTALL) + if m_block: + for line in m_block.group(1).splitlines(): + line = line.strip() + if not line or ':' not in line: continue + name, rest = line.split(':', 1) + name = name.strip() + m_org = re.search(r'org\s*=\s*(.*?),', rest) + m_len = re.search(r'len\s*=\s*(.*)', rest) + if m_org and m_len: + org_expr = m_org.group(1).strip() + len_expr = m_len.group(1).strip() + try: + org_val = eval(org_expr) + len_val = eval(len_expr) + # Ignore debug regions + if not (name.startswith('.debug') or name.startswith('.stab')): + regions.append({'name': name, 'start': org_val, 'end': org_val + len_val}) + except Exception: + pass + except Exception as e: + print(f"Warning: Failed to parse {filepath}: {e}") + return regions + +def parse_zephyr_stat(filepath): + sections = [] + try: + with open(filepath, 'r') as f: + for line in f: + m = re.match(r'^\s*\[\s*\d+\]\s+(\S+)\s+[A-Z0-9]+\s+([0-9a-fA-F]+)\s+[0-9a-fA-F]+\s+([0-9a-fA-F]+)', line) + if m: + name = m.group(1) + start = int(m.group(2), 16) + size = int(m.group(3), 16) + # Ignore debug sections + if size > 0 and not (name.startswith('.debug') or name.startswith('.stab')): + sections.append({'name': name, 'start': start, 'end': start + size}) + except Exception as e: + print(f"Warning: Failed to parse {filepath}: {e}") + return sections + +def parse_zephyr_dts(filepath): + regions = [] + try: + with open(filepath, 'r') as f: + lines = f.read().splitlines() + + current_node_path = [] + for line in lines: + line = line.strip() + # Simple node match: node_name: some_name@addr { + m_node = re.match(r'^(?:[a-zA-Z0-9_]+:\s*)?([a-zA-Z0-9_\-]+(?:@[0-9a-fA-Fx]+)?)\s*\{', line) + if m_node: + node_name = m_node.group(1) + current_node_path.append(node_name) + continue + + if line == "};": + if current_node_path: + current_node_path.pop() + continue + + # match reg = < ... > + m_reg = re.match(r'^reg\s*=\s*<\s*(.*?)\s*>;', line) + if m_reg and current_node_path: + reg_vals = m_reg.group(1).split() + if len(reg_vals) >= 2: + try: + addr = int(reg_vals[0], 16) if reg_vals[0].startswith('0x') else int(reg_vals[0]) + size = int(reg_vals[1], 16) if reg_vals[1].startswith('0x') else int(reg_vals[1]) + if size > 0: + node_name = current_node_path[-1] + regions.append({'name': node_name, 'start': addr, 'end': addr + size}) + except ValueError: + pass + except Exception as e: + print(f"Warning: Failed to parse {filepath}: {e}") + return regions + +def build_address_map(objdump_text): + current_func = "" + current_context = [] + last_was_asm = False + address_map = {} + + func_re = re.compile(r'^([0-9a-fA-F]+)\s+<([^>]+)>:$') + asm_re = re.compile(r'^\s*([0-9a-fA-F]+):\s+(.*)$') + + for line in objdump_text.splitlines(): + line = line.rstrip() + if not line or line.startswith("Disassembly of section"): + continue + + m_func = func_re.match(line) + if m_func: + current_func = m_func.group(2) + current_context = [] + last_was_asm = False + continue + + m_asm = asm_re.match(line) + if m_asm: + addr = int(m_asm.group(1), 16) + address_map[addr] = { + 'func': current_func, + 'context': list(current_context), + 'asm': m_asm.group(2) + } + last_was_asm = True + continue + + if last_was_asm: + current_context = [] + last_was_asm = False + current_context.append(line) + + return address_map + +def find_closest_instruction(addr, address_map, sorted_addresses): + if addr in address_map: + return addr, address_map[addr] + + # Extract lower 29 bits for physical address mappings on Xtensa + physical = addr & 0x1FFFFFFF + if physical in address_map: + return physical, address_map[physical] + + idx = bisect.bisect_right(sorted_addresses, physical) + if idx > 0: + closest = sorted_addresses[idx-1] + # Return if within 16 bytes (typical small instruction offset) + if physical - closest < 16: + return closest, address_map[closest] + + return addr, None + +def decode_ps_bits(val): + intlevel = val & 0xF + excm = (val >> 4) & 1 + um = (val >> 5) & 1 + ring = (val >> 6) & 3 + owb = (val >> 8) & 0xF + callinc = (val >> 16) & 3 + woe = (val >> 18) & 1 + + flags = [] + flags.append(f"INTLEVEL:{intlevel}") + if excm: flags.append("EXCM") + flags.append(f"UM:{um}") + flags.append(f"RING:{ring}") + flags.append(f"OWB:{owb}") + flags.append(f"CALLINC:{callinc}") + flags.append(f"WOE:{woe}") + + return " | ".join(flags) + +def main(): + # Set default color explicitly at start + print("\x1b[0m", end='', flush=True) + + parser = argparse.ArgumentParser(description="Decode Xtensa/Zephyr crash dump using objdump.") + parser.add_argument("--elf", required=False, help="Path to the ELF file. Overridden if --build-dir is provided.") + parser.add_argument("--build-dir", required=False, help="Path to the Zephyr build directory.") + parser.add_argument("--dump", default="-", help="Path to the crash dump file. Default is '-' for stdin.") + parser.add_argument("--clipboard", action="store_true", help="Read crash dump from the clipboard instead of file/stdin.") + parser.add_argument("--objdump", default="xtensa-sof-zephyr-elf-objdump", help="Objdump command to use. e.g. xtensa-zephyr-elf-objdump") + args = parser.parse_args() + + objdump_cmd = args.objdump + + if args.build_dir: + # Resolve zephyr.elf + default_elf = os.path.join(args.build_dir, "zephyr", "zephyr.elf") + if os.path.isfile(default_elf): + args.elf = default_elf + + # Try to find objdump from compile_commands + cc_path = os.path.join(args.build_dir, "compile_commands.json") + if not os.path.isfile(cc_path): + cc_path = os.path.join(args.build_dir, "compile_commands.txt") + + if os.path.isfile(cc_path): + try: + with open(cc_path, 'r') as f: + cc_data = json.load(f) + if cc_data and len(cc_data) > 0 and 'command' in cc_data[0]: + # The command might contain arguments, we extract the first token + cmd_tokens = shlex.split(cc_data[0]['command']) + compiler_path = cmd_tokens[0] + # Replace gcc, g++, clang, etc. with objdump + if compiler_path.endswith('gcc') or compiler_path.endswith('g++') or compiler_path.endswith('cc'): + new_cmd = re.sub(r'(g?cc|g\+\+)$', 'objdump', compiler_path) + if os.path.isfile(new_cmd) and os.access(new_cmd, os.X_OK): + objdump_cmd = new_cmd + except Exception as e: + print(f"Warning: Failed to parse {cc_path} to deduce objdump: {e}") + + linker_regions = [] + stat_sections = [] + dts_regions = [] + if args.build_dir: + linker_cmd_path = os.path.join(args.build_dir, "zephyr", "linker.cmd") + zephyr_stat_path = os.path.join(args.build_dir, "zephyr", "zephyr.stat") + zephyr_dts_path = os.path.join(args.build_dir, "zephyr", "zephyr.dts") + + if os.path.isfile(linker_cmd_path): + linker_regions = parse_linker_cmd(linker_cmd_path) + if os.path.isfile(zephyr_stat_path): + stat_sections = parse_zephyr_stat(zephyr_stat_path) + if os.path.isfile(zephyr_dts_path): + dts_regions = parse_zephyr_dts(zephyr_dts_path) + + if not args.elf: + print("Error: --elf or --build-dir must be provided.") + sys.exit(1) + + if not os.path.isfile(args.elf): + print(f"Cannot find ELF file: {args.elf}") + sys.exit(1) + + if args.clipboard: + try: + dump_content = subprocess.check_output(['xclip', '-o', '-selection', 'clipboard'], text=True) + except (subprocess.CalledProcessError, FileNotFoundError): + try: + dump_content = subprocess.check_output(['xsel', '--clipboard', '--output'], text=True) + except (subprocess.CalledProcessError, FileNotFoundError): + try: + dump_content = subprocess.check_output(['wl-paste'], text=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("Error: Could not read from clipboard. Make sure xclip, xsel, or wl-paste is installed.") + sys.exit(1) + elif args.dump == "-": + dump_content = sys.stdin.read() + else: + if not os.path.isfile(args.dump): + print(f"Cannot find Dump file: {args.dump}") + sys.exit(1) + with open(args.dump, 'r') as f: + dump_content = f.read() + + registers, backtraces, raw_content = parse_crash_log(dump_content) + + print(f"Found {len(registers)} registers and {len(backtraces)} backtrace elements in crash dump.") + + print("Parsing objdump (this may take a few seconds)...") + + # Actually, many systems might use standard xtensa-zephyr-elf-objdump + # We can try to dynamically choose if the user just provided a prefix or left default + + # Try running the objdump to ensure it exists + import shutil + if not os.path.isfile(objdump_cmd) and not shutil.which(objdump_cmd) and "zephyr" in objdump_cmd: + # try without sof if user has a different one + alt_cmds = [ + "xtensa-zephyr-elf-objdump", + "xtensa-intel-elf-objdump", + "zephyr-sdk/xtensa-zephyr-elf-objdump", + "objdump" + ] + for alt in alt_cmds: + if shutil.which(alt): + print(f"Warning: {objdump_cmd} not found, falling back to {alt}") + objdump_cmd = alt + break + + objdump_text = get_objdump_output(args.elf, objdump_cmd) + address_map = build_address_map(objdump_text) + sorted_addresses = sorted(address_map.keys()) + + print("\n--- Summary ---") + print("PS Register Legend:") + print(" INTLEVEL : Interrupt Level EXCM : Exception Mode") + print(" UM : User Mode (1=User) RING : Privilege Ring") + print(" OWB : Old Window Base WOE : Window Overflow Enable") + print(" CALLINC : Call Increment") + print() + + def print_decoded(name, val): + if val == 0: + print(f"{name:5}: 0x00000000 -> (nil)") + return + + addr, info = find_closest_instruction(val, address_map, sorted_addresses) + if info: + print(f"{name:5}: 0x{val:08x} -> <{info['func']}>") + for ctx in info['context']: + ctx_strip = ctx.strip() + if re.match(r'^[^ \t:]+:\d+', ctx_strip): + print(f" \x1b[35m{ctx_strip}\x1b[0m") + else: + print(f" \x1b[93m{ctx_strip}\x1b[0m") + print(f" \x1b[93m{addr:08x}: {info['asm']}\x1b[0m") + print() + else: + dts_str = "" + for d in dts_regions: + if d['start'] <= val < d['end']: + dts_str = f", DT: {d['name']}" + break + region_str = "" + for r in linker_regions: + if r['start'] <= val < r['end']: + region_str = f", Region: {r['name']}" + break + sec_str = "" + for s in stat_sections: + if s['start'] <= val < s['end']: + sec_str = f", Section: {s['name']}" + break + + if dts_str or region_str or sec_str: + print(f"{name:5}: 0x{val:08x} -> ") + else: + print(f"{name:5}: 0x{val:08x} -> ") + + # Prioritize specific registers + for reg in ['PC', 'EXCCAUSE', 'VADDR', 'SP', 'PS']: + if reg in registers: + if reg == 'EXCCAUSE': + cause_code = registers[reg] + cause_str = XTENSA_EXCCAUSE.get(cause_code, "Unknown/Unassigned") + print(f"EXCCAUSE: {cause_code} ({cause_str})") + elif reg == 'VADDR': + print(f"{reg:5}: 0x{registers[reg]:08x}") + elif reg == 'PS': + print(f"{reg:5}: 0x{registers[reg]:08x} -> [{decode_ps_bits(registers[reg])}]\n") + else: + print_decoded(reg, registers[reg]) + + for i in range(1, 8): + reg = f"EPC{i}" + if reg in registers: + print_decoded(reg, registers[reg]) + + print() + for i in range(2, 8): + reg = f"EPS{i}" + if reg in registers: + print(f"{reg:5}: 0x{registers[reg]:08x} -> [{decode_ps_bits(registers[reg])}]") + + print("\n--- Physical Windowed Registers (A) ---") + for i in range(16): + reg = f"A{i}" + if reg in registers: + print_decoded(reg, registers[reg]) + + print("\n--- Saved Stack Registers (AR) ---") + for i in range(64): + reg = f"AR{i}" + if reg in registers: + print_decoded(reg, registers[reg]) + + print("\n--- Backtrace Decode ---") + # Backtraces: + for i, (pc, sp) in enumerate(backtraces): + print(f"Frame {i}: SP = 0x{sp:08x}") + print_decoded("PC", pc) + +if __name__ == '__main__': + main() diff --git a/scripts/sof-qemu-run.py b/scripts/sof-qemu-run.py new file mode 100755 index 000000000000..e515b7767a43 --- /dev/null +++ b/scripts/sof-qemu-run.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2024 Intel Corporation. All rights reserved. +""" +sof-qemu-run.py - Automated QEMU test runner and crash analyzer + +This script runs `west -v build -t run` for Zephyr, monitors the output, and +waits for 2 seconds after the last log event. If a Zephyr/QEMU crash occurs, +it decodes it using `sof-crash-decode.py`. If no crash occurred, it enters +the QEMU monitor (Ctrl-A c), issues `info registers`, and passes the output +to the crash decoder. +""" + +import sys +import pexpect +import subprocess +import argparse +import os +import sys +import re + +# ANSI Color Codes +COLOR_RED = "\x1b[31;1m" +COLOR_YELLOW = "\x1b[33;1m" +COLOR_RESET = "\x1b[0m" + +def colorize_line(line): + """Colorize significant Zephyr log items.""" + if "" in line or "[ERR]" in line or "Fatal fault" in line or "Oops" in line: + return COLOR_RED + line + COLOR_RESET + elif "" in line or "[WRN]" in line: + return COLOR_YELLOW + line + COLOR_RESET + return line + +def check_for_crash(output): + """ + Check if the collected output indicates a crash. + Look for known Zephyr/Xtensa oops or exceptions, or QEMU crash register dumps. + """ + crash_keywords = [ + "Fatal fault", + "Oops", + "ZEPHYR FATAL ERROR", + "Exception", + "PC=", # QEMU PC output format + "EXCCAUSE=", + "Backtrace:" + ] + for keyword in crash_keywords: + if keyword in output: + return True + return False + +def run_sof_crash_decode(build_dir, dump_content): + """ + Invokes sof-crash-decode.py and feeds it the crash dump via stdin. + """ + # Find sof-crash-decode.py in the same directory as this script, or fallback to relative path + decoder_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sof-crash-decode.py") + if not os.path.isfile(decoder_script): + decoder_script = "sof-crash-decode.py" + + print("\n====================================") + print("Running sof-crash-decode.py Analysis") + print("====================================\n") + + cmd = [sys.executable, decoder_script, "--build-dir", build_dir] + + try: + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) + proc.communicate(input=dump_content.encode('utf-8')) + except Exception as e: + print(f"Failed to run sof-crash-decode.py: {e}") + +def main(): + parser = argparse.ArgumentParser(description="Run QEMU via west and automatically decode crashes.") + parser.add_argument("--build-dir", default="build", help="Path to the build directory containing zephyr.elf, linker.cmd, etc. Defaults to 'build'.") + parser.add_argument("--log-file", default="qemu-run.log", help="Path to save the QEMU output log. Defaults to 'qemu-run.log'.") + args = parser.parse_args() + + # Make absolute path just in case + build_dir = os.path.abspath(args.build_dir) + + print(f"Starting QEMU test runner. Monitoring for crashes (Build Dir: {args.build_dir})...") + + # We will use pexpect to spawn the west command to get PTY features + import shutil + west_path = shutil.which("west") + if not west_path: + print("[sof-qemu-run] Error: 'west' command not found in PATH.") + print("Please ensure you have sourced the Zephyr environment (e.g., source zephyr-env.sh).") + sys.exit(1) + + child = pexpect.spawn(west_path, ["-v", "build", "-t", "run"], encoding='utf-8') + + # We will accumulate output to check for crashes + full_output = "" + last_output = "" + + with open(args.log_file, "w") as log_file: + try: + # Loop reading output until EOF or a timeout occurs + qemu_started = False + while True: + try: + # Read character by character or line by line + # Pexpect's readline() doesn't consistently trigger timeout on idle + # We can use read_nonblocking and an explicit exceptTIMEOUT + index = child.expect([r'\r\n', pexpect.TIMEOUT, pexpect.EOF], timeout=2) + if index == 0: + line = child.before + '\n' + # Strip ANSI escape codes from output to write raw text to log file + clean_line = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', line) + log_file.write(clean_line) + log_file.flush() + + colored_line = colorize_line(line) + sys.stdout.write(colored_line) + sys.stdout.flush() + + full_output += line + if not qemu_started and ("Booting Zephyr OS" in line or "To exit from QEMU" in line or "qemu-system-" in line): + qemu_started = True + elif index == 1: # TIMEOUT + if qemu_started or check_for_crash(full_output): + print("\n\n[sof-qemu-run] 2 seconds passed since last log event. Checking status...") + break + else: + # Still building or loading, continue waiting + pass + elif index == 2: # EOF + print("\n\n[sof-qemu-run] QEMU process terminated.") + break + + except pexpect.TIMEOUT: + if qemu_started or check_for_crash(full_output): + print("\n\n[sof-qemu-run] 2 seconds passed since last log event. Checking status...") + break + else: + # Still building or loading, continue waiting + pass + except pexpect.EOF: + print("\n\n[sof-qemu-run] QEMU process terminated.") + break + + except KeyboardInterrupt: + print("\n[sof-qemu-run] Interrupted by user.") + # Proceed with what we have + + crashed = check_for_crash(full_output) + + if crashed: + print("\n[sof-qemu-run] Detected crash signature in standard output!") + # Stop QEMU if it's still running + if child.isalive(): + child.sendline("\x01x") # Ctrl-A x to quit qemu + child.close(force=True) + + run_sof_crash_decode(build_dir, full_output) + else: + print("\n[sof-qemu-run] No crash detected. Interacting with QEMU Monitor to grab registers...") + + # We need to send Ctrl-A c to enter the monitor + if child.isalive(): + child.send("\x01c") # Ctrl-A c + try: + # Wait for (qemu) prompt + child.expect(r"\(qemu\)", timeout=5) + # Send "info registers" + child.sendline("info registers") + # Wait for the next prompt + child.expect(r"\(qemu\)", timeout=5) + + info_regs_output = child.before + print("\n[sof-qemu-run] Successfully extracted registers from QEMU monitor.\n") + + # Quit qemu safely + child.sendline("quit") + child.expect(pexpect.EOF, timeout=2) + child.close() + + # Run the decoder on the intercepted register output + run_sof_crash_decode(build_dir, info_regs_output) + except pexpect.TIMEOUT: + print("\n[sof-qemu-run] Timed out waiting for QEMU monitor. Is it running?") + child.close(force=True) + except pexpect.EOF: + print("\n[sof-qemu-run] QEMU terminated before we could run monitor commands.") + else: + print("\n[sof-qemu-run] Process is no longer alive, cannot extract registers.") + +if __name__ == "__main__": + main() diff --git a/scripts/sof-qemu-run.sh b/scripts/sof-qemu-run.sh new file mode 100755 index 000000000000..321a8b518dae --- /dev/null +++ b/scripts/sof-qemu-run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2024 Intel Corporation. All rights reserved. + +# Define the build directory from the first argument (or default) +BUILD_DIR="${1:-build}" + +# Find and source the zephyr environment script, typically via the sof-venv wrapper +# or directly if running in a known zephyrproject layout. +# We will use the existing helper sof-venv.sh to get the right environment. + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOF_WORKSPACE="$(dirname "$(dirname "$SCRIPT_DIR")")" + +# check if Zephyr environment is set up +if [ ! -z "$SOF_WORKSPACE" ]; then + VENV_DIR="$SOF_WORKSPACE/.venv" + echo "Using SOF environment at $SOF_WORKSPACE" +else + # default to the local workspace + VENV_DIR="${SOF_WORKSPACE}/.venv" + echo "Using default SOF environment at $VENV_DIR" +fi + +# start the virtual environment +source ${VENV_DIR}/bin/activate + +# Execute the QEMU runner from within the correct build directory +cd "${BUILD_DIR}" || exit 1 + +# 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}" + diff --git a/scripts/xtensa-build-zephyr.py b/scripts/xtensa-build-zephyr.py index 7610f81c1b81..b6a2b8e91a90 100755 --- a/scripts/xtensa-build-zephyr.py +++ b/scripts/xtensa-build-zephyr.py @@ -227,6 +227,14 @@ class PlatformConfig: "imx", "imx95_evk/mimx9596/m7/ddr", "", "", "", "" ), + "qemu_xtensa" : PlatformConfig( + "zephyr", "qemu_xtensa/dc233c", + "", "", "zephyr" + ), + "qemu_xtensa_mmu" : PlatformConfig( + "zephyr", "qemu_xtensa/dc233c/mmu", + "", "", "zephyr" + ), } platform_configs = platform_configs_all.copy() @@ -894,6 +902,7 @@ def build_platforms(): platform_build_dir_name = f"build-{platform}" PLAT_CONFIG = platform_dict["PLAT_CONFIG"] + build_cmd = ["west"] build_cmd += ["-v"] * args.verbose if args.menuconfig: @@ -1144,8 +1153,9 @@ def install_platform(platform, sof_output_dir, platf_build_environ, platform_wco install_key_dir = install_key_dir / args.key_type_subdir os.makedirs(install_key_dir, exist_ok=True) - # looses file owner and group - file is commonly accessible - shutil.copy2(abs_build_dir / "zephyr.ri", install_key_dir / output_fwname) + # looses file owner and group - file is commonly accessible, dont install qemu. + if platform not in ("qemu_xtensa", "qemu_xtensa_mmu"): + shutil.copy2(abs_build_dir / "zephyr.ri", install_key_dir / output_fwname) if args.deployable_build and platform_configs[platform].ipc4: # IPC4 deployable builds are using separate directories per platforms @@ -1298,6 +1308,8 @@ def gzip_compress(fname, gzdst=None): RI_INFO_UNSUPPORTED += ['imx8', 'imx8x', 'imx8m', 'imx8ulp', 'imx95'] RI_INFO_UNSUPPORTED += ['rn', 'acp_6_0'] RI_INFO_UNSUPPORTED += ['mt8186', 'mt8188', 'mt8195', 'mt8196', 'mt8365'] +RI_INFO_UNSUPPORTED += ['qemu_xtensa', 'qemu_xtensa_mmu'] + # For temporary workarounds. Unlike _UNSUPPORTED above, the platforms below will print a warning. RI_INFO_FIXME = [ ] diff --git a/src/audio/module_adapter/module/generic.c b/src/audio/module_adapter/module/generic.c index f9bf5d52bef3..2e6c786552b8 100644 --- a/src/audio/module_adapter/module/generic.c +++ b/src/audio/module_adapter/module/generic.c @@ -412,6 +412,8 @@ EXPORT_SYMBOL(z_impl_mod_free); #ifdef CONFIG_USERSPACE #include + +#if CONFIG_FAST_GET const void *z_vrfy_mod_fast_get(struct processing_module *mod, const void * const dram_ptr, size_t size) { @@ -424,6 +426,7 @@ const void *z_vrfy_mod_fast_get(struct processing_module *mod, const void * cons return z_impl_mod_fast_get(mod, dram_ptr, size); } #include +#endif void *z_vrfy_mod_alloc_ext(struct processing_module *mod, uint32_t flags, size_t size, size_t alignment) diff --git a/src/include/ipc4/pipeline.h b/src/include/ipc4/pipeline.h index 198918cf8577..584cc1e7fd1c 100644 --- a/src/include/ipc4/pipeline.h +++ b/src/include/ipc4/pipeline.h @@ -453,4 +453,9 @@ struct ipc4_chain_dma { } extension; } __attribute__((packed, aligned(4))); +struct ipc4_message_request; + +int ipc4_new_pipeline(struct ipc4_message_request *ipc4); +int ipc4_delete_pipeline(struct ipc4_message_request *ipc4); + #endif diff --git a/src/ipc/ipc4/handler.c b/src/ipc/ipc4/handler.c index fc64c904ef80..b55428bf1a9a 100644 --- a/src/ipc/ipc4/handler.c +++ b/src/ipc/ipc4/handler.c @@ -127,7 +127,7 @@ static inline const struct ipc4_pipeline_set_state_data *ipc4_get_pipeline_data( /* * Global IPC Operations. */ -__cold static int ipc4_new_pipeline(struct ipc4_message_request *ipc4) +__cold int ipc4_new_pipeline(struct ipc4_message_request *ipc4) { struct ipc *ipc = ipc_get(); @@ -136,7 +136,7 @@ __cold static int ipc4_new_pipeline(struct ipc4_message_request *ipc4) return ipc_pipeline_new(ipc, (ipc_pipe_new *)ipc4); } -__cold static int ipc4_delete_pipeline(struct ipc4_message_request *ipc4) +__cold int ipc4_delete_pipeline(struct ipc4_message_request *ipc4) { struct ipc4_pipeline_delete *pipe; struct ipc *ipc = ipc_get(); diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt index 1d0d7596cf3b..78aee20ee31a 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -25,4 +25,6 @@ elseif(CONFIG_MT8196) add_subdirectory(mt8196) elseif(CONFIG_MT8365) add_subdirectory(mt8365) +elseif(PLATFORM STREQUAL "qemu_xtensa") + add_subdirectory(qemu_xtensa) endif() diff --git a/src/platform/qemu_xtensa/CMakeLists.txt b/src/platform/qemu_xtensa/CMakeLists.txt new file mode 100644 index 000000000000..8688947cf0c6 --- /dev/null +++ b/src/platform/qemu_xtensa/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: BSD-3-Clause + +add_local_sources(sof platform.c) diff --git a/src/platform/qemu_xtensa/include/platform/lib/clk.h b/src/platform/qemu_xtensa/include/platform/lib/clk.h new file mode 100644 index 000000000000..c9f05cdf405b --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/lib/clk.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_LIB_CLK_H__ +#define __PLATFORM_LIB_CLK_H__ + +/* Dummy clk header for qemu_xtensa */ +#define CLK_MAX_CPU_HZ 10000000 +#define CPU_LOWEST_FREQ_IDX 0 + +#endif /* __PLATFORM_LIB_CLK_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/lib/dai.h b/src/platform/qemu_xtensa/include/platform/lib/dai.h new file mode 100644 index 000000000000..418c383789a8 --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/lib/dai.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_LIB_DAI_H__ +#define __PLATFORM_LIB_DAI_H__ + +/* Dummy dai header for qemu_xtensa */ + +#endif /* __PLATFORM_LIB_DAI_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/lib/dma.h b/src/platform/qemu_xtensa/include/platform/lib/dma.h new file mode 100644 index 000000000000..4c4068b99392 --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/lib/dma.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_LIB_DMA_H__ +#define __PLATFORM_LIB_DMA_H__ + +/* Dummy dma header for qemu_xtensa */ +struct dma; + +struct sof_dma { + const struct device *z_dev; +}; + +#endif /* __PLATFORM_LIB_DMA_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/lib/mailbox.h b/src/platform/qemu_xtensa/include/platform/lib/mailbox.h new file mode 100644 index 000000000000..47c97744fe6d --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/lib/mailbox.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_LIB_MAILBOX_H__ +#define __PLATFORM_LIB_MAILBOX_H__ + +/* Dummy mailbox header for qemu_xtensa */ +#define MAILBOX_HOSTBOX_BASE 0x10000000 +#define MAILBOX_HOSTBOX_SIZE 0x1000 +#define MAILBOX_DSPBOX_BASE 0x10005000 +#define MAILBOX_DSPBOX_SIZE 0x1000 +#define MAILBOX_STREAM_BASE 0x10001000 +#define MAILBOX_STREAM_SIZE 0x1000 +#define MAILBOX_TRACE_BASE 0x10002000 +#define MAILBOX_TRACE_SIZE 0x1000 +#define MAILBOX_EXCEPTION_BASE 0x10003000 +#define MAILBOX_EXCEPTION_SIZE 0x1000 +#define MAILBOX_DEBUG_BASE 0x10004000 +#define MAILBOX_DEBUG_SIZE 0x1000 +#define MAILBOX_SW_REG_BASE 0x10005000 +#define MAILBOX_SW_REG_SIZE 0x1000 + +#include +#include + +static inline void mailbox_sw_regs_write(size_t offset, const void *src, size_t bytes) {} +static inline void mailbox_sw_reg_write(size_t offset, uint32_t val) {} +static inline void mailbox_sw_reg_write64(size_t offset, uint64_t val) {} +static inline uint32_t mailbox_sw_reg_read(size_t offset) { return 0; } +static inline uint64_t mailbox_sw_reg_read64(size_t offset) { return 0; } + +#endif /* __PLATFORM_LIB_MAILBOX_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/lib/memory.h b/src/platform/qemu_xtensa/include/platform/lib/memory.h new file mode 100644 index 000000000000..d0843904f563 --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/lib/memory.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_LIB_MEMORY_H__ +#define __PLATFORM_LIB_MEMORY_H__ + +/* Dummy memory header for qemu_xtensa */ + +static inline void *platform_shared_get(void *ptr, int bytes) +{ + return ptr; +} + +#define PLATFORM_DCACHE_ALIGN sizeof(void *) +#define HOST_PAGE_SIZE 4096 +#define SHARED_DATA + +#endif /* __PLATFORM_LIB_MEMORY_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/platform.h b/src/platform/qemu_xtensa/include/platform/platform.h new file mode 100644 index 000000000000..5f89152251b4 --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/platform.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_PLATFORM_H__ +#define __PLATFORM_PLATFORM_H__ + +/* Dummy platform header for qemu_xtensa */ +#define PLATFORM_CORE_COUNT 1 +#define PLATFORM_MAX_CHANNELS 8 +#define PLATFORM_MAX_STREAMS 8 + +#define HW_CFG_VERSION 0x010000 +#define DMA_TRACE_LOCAL_SIZE HOST_PAGE_SIZE + +struct ipc_msg; +static inline void ipc_platform_send_msg_direct(const struct ipc_msg *msg) {} + +#endif /* __PLATFORM_PLATFORM_H__ */ diff --git a/src/platform/qemu_xtensa/include/platform/trace/trace.h b/src/platform/qemu_xtensa/include/platform/trace/trace.h new file mode 100644 index 000000000000..65499099cd52 --- /dev/null +++ b/src/platform/qemu_xtensa/include/platform/trace/trace.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +#ifndef __PLATFORM_TRACE_TRACE_H__ +#define __PLATFORM_TRACE_TRACE_H__ + +/* Dummy trace header for qemu_xtensa */ +#define PLATFORM_TRACE_DICT_FRONT 0 + +#endif /* __PLATFORM_TRACE_TRACE_H__ */ diff --git a/src/platform/qemu_xtensa/platform.c b/src/platform/qemu_xtensa/platform.c new file mode 100644 index 000000000000..2930f133635d --- /dev/null +++ b/src/platform/qemu_xtensa/platform.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +#include +#include +#include +#include + +void ipc_platform_complete_cmd(struct ipc *ipc) +{ +} + +int platform_boot_complete(uint32_t boot_message) +{ + return 0; +} + +int platform_init(struct sof *sof) +{ + ipc_init(sof); + return 0; +} + +int platform_ipc_init(struct ipc *ipc) +{ + return 0; +} + +int ipc_platform_send_msg(const struct ipc_msg *msg) +{ + return 0; +} diff --git a/src/schedule/zephyr_ll_user.c b/src/schedule/zephyr_ll_user.c index aa33807b4aa3..bf768599d3c5 100644 --- a/src/schedule/zephyr_ll_user.c +++ b/src/schedule/zephyr_ll_user.c @@ -42,8 +42,11 @@ static struct k_heap *zephyr_ll_heap_init(void) k_panic(); } + uintptr_t cached_ptr = (uintptr_t)sys_cache_cached_ptr_get(heap->heap.init_mem); + uintptr_t uncached_ptr = (uintptr_t)sys_cache_uncached_ptr_get(heap->heap.init_mem); + /* Create memory partition for sch_data array */ - mem_partition.start = (uintptr_t)sys_cache_cached_ptr_get(heap->heap.init_mem); + mem_partition.start = cached_ptr; mem_partition.size = heap->heap.init_bytes; mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW | XTENSA_MMU_CACHED_WB; @@ -53,13 +56,15 @@ static struct k_heap *zephyr_ll_heap_init(void) if (ret) k_panic(); - mem_partition.start = (uintptr_t)sys_cache_uncached_ptr_get(heap->heap.init_mem); - mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW; - ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &mem_partition); - tr_dbg(&ll_tr, "init ll heap %p, size %u (uncached), ret %d", - (void *)mem_partition.start, heap->heap.init_bytes, ret); - if (ret) - k_panic(); + if (cached_ptr != uncached_ptr) { + mem_partition.start = uncached_ptr; + mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW; + ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &mem_partition); + tr_dbg(&ll_tr, "init ll heap %p, size %u (uncached), ret %d", + (void *)mem_partition.start, heap->heap.init_bytes, ret); + if (ret) + k_panic(); + } return heap; } diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 61ed49a3d15f..bb25230f89be 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -184,6 +184,8 @@ set_property(TARGET modules_sof PROPERTY CXX_STANDARD 17) zephyr_include_directories(include) zephyr_include_directories(${ZEPHYR_BASE}/kernel/include) zephyr_include_directories(${ZEPHYR_BASE}/arch/${ARCH}/include) +zephyr_include_directories(${SOF_SRC_PATH}/include/sof/audio/module_adapter/iadk/) +zephyr_include_directories(${SOF_SRC_PATH}/include/sof/audio/module_adapter/library/) # SOC level sources # Files that are commented may not be needed. @@ -485,6 +487,11 @@ zephyr_library_sources_ifdef(CONFIG_ZEPHYR_POSIX ${SOF_PLATFORM_PATH}/posix/fuzz.c ) +if (CONFIG_BOARD_QEMU_XTENSA) + set(PLATFORM "qemu_xtensa") + zephyr_library_sources(${SOF_PLATFORM_PATH}/qemu_xtensa/platform.c) +endif() + if(NOT DEFINED PLATFORM) message(FATAL_ERROR "PLATFORM is not defined, check your Kconfiguration?") endif() diff --git a/zephyr/test/CMakeLists.txt b/zephyr/test/CMakeLists.txt index c5b66c83bbaa..4d47a3701a0d 100644 --- a/zephyr/test/CMakeLists.txt +++ b/zephyr/test/CMakeLists.txt @@ -22,4 +22,5 @@ endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_LL) zephyr_library_sources(userspace/test_ll_task.c) + zephyr_library_sources(userspace/test_ipc4_pipeline.c) endif() diff --git a/zephyr/test/userspace/README.md b/zephyr/test/userspace/README.md index f6d0cc1a8dc3..6c14300ee333 100644 --- a/zephyr/test/userspace/README.md +++ b/zephyr/test/userspace/README.md @@ -32,6 +32,14 @@ Running test: sudo ./cavstool.py sof-ptl.ri - Test results printed to cavstool.py +Running test on QEMU (dc233c MMU): +- Tests can also be built and run locally using Zephyr's QEMU simulator. +- First, build the test application using `xtensa-build-zephyr.py`: + ./scripts/xtensa-build-zephyr.py qemu_xtensa_mmu --cmake-args=-DCONFIG_SOF_BOOT_TEST_STANDALONE=y \ + --cmake-args=-DCONFIG_SOF_USERSPACE_INTERFACE_DMA=y --cmake-args=-DCONFIG_SOF_USERSPACE_LL=y +- Once built, run the test in QEMU: + west build -d build-qemu_xtensa_mmu -t run + References to related assets in Zephyr codebase: - cavstool.py - zephyr/soc/intel/intel_adsp/tools/cavstool.py diff --git a/zephyr/test/userspace/test_intel_hda_dma.c b/zephyr/test/userspace/test_intel_hda_dma.c index dd54e6d85f2a..6c5c352d83de 100644 --- a/zephyr/test/userspace/test_intel_hda_dma.c +++ b/zephyr/test/userspace/test_intel_hda_dma.c @@ -57,6 +57,7 @@ static void intel_hda_dma_user(void *p1, void *p2, void *p3) * cannot access */ dma = sof_dma_get(SOF_DMA_DIR_LMEM_TO_HMEM, 0, SOF_DMA_DEV_HOST, SOF_DMA_ACCESS_SHARED); + zassert_not_null(dma, "dma get failed"); k_sem_take(&ipc_sem_wake_user, K_FOREVER); LOG_INF("configure DMA channel"); @@ -181,6 +182,7 @@ static void intel_hda_dma_kernel(void) k_thread_access_grant(&user_thread, &ipc_sem_wake_kernel); dma = DEVICE_DT_GET(DT_NODELABEL(hda_host_in)); + zassert_not_null(dma, "hda_host_in device not found"); k_thread_access_grant(&user_thread, dma); hda_ipc_msg(INTEL_ADSP_IPC_HOST_DEV, IPCCMD_HDA_RESET, diff --git a/zephyr/test/userspace/test_intel_ssp_dai.c b/zephyr/test/userspace/test_intel_ssp_dai.c index 6c700c3839bb..9ce7aa4049b1 100644 --- a/zephyr/test/userspace/test_intel_ssp_dai.c +++ b/zephyr/test/userspace/test_intel_ssp_dai.c @@ -130,13 +130,16 @@ static void intel_ssp_dai_user(void *p1, void *p2, void *p3) * cannot access */ dma_in = sof_dma_get(SOF_DMA_DIR_DEV_TO_MEM, 0, SOF_DMA_DEV_SSP, SOF_DMA_ACCESS_SHARED); + zassert_not_null(dma_in, "dma_in get failed"); dma_out = sof_dma_get(SOF_DMA_DIR_MEM_TO_DEV, 0, SOF_DMA_DEV_SSP, SOF_DMA_ACCESS_SHARED); + zassert_not_null(dma_out, "dma_out get failed"); k_sem_take(&ipc_sem_wake_user, K_FOREVER); LOG_INF("create a DAI device for %s", STRINGIFY(SSP_DEVICE)); dai_dev = DEVICE_DT_GET(DT_NODELABEL(SSP_DEVICE)); + zassert_not_null(dai_dev, "dai_dev DT struct not found"); err = dai_probe(dai_dev); zassert_equal(err, 0); @@ -297,8 +300,11 @@ static void intel_ssp_dai_kernel(void) k_thread_access_grant(&user_thread, &ipc_sem_wake_kernel); dma_out = DEVICE_DT_GET(DT_NODELABEL(hda_link_out)); + zassert_not_null(dma_out, "hda_link_out not found"); dma_in = DEVICE_DT_GET(DT_NODELABEL(hda_link_in)); + zassert_not_null(dma_in, "hda_link_in not found"); dai_dev = DEVICE_DT_GET(DT_NODELABEL(SSP_DEVICE)); + zassert_not_null(dai_dev, "SSP_DEVICE not found"); k_thread_access_grant(&user_thread, dma_out); k_thread_access_grant(&user_thread, dma_in); diff --git a/zephyr/test/userspace/test_ipc4_pipeline.c b/zephyr/test/userspace/test_ipc4_pipeline.c new file mode 100644 index 000000000000..aab38793efb8 --- /dev/null +++ b/zephyr/test/userspace/test_ipc4_pipeline.c @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2026 Intel Corporation. + */ + +/* + * Test case for creation and destruction of IPC4 pipelines. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); + +ZTEST(userspace_ipc4_pipeline, test_pipeline_create_destroy_helpers) +{ + struct ipc *ipc = ipc_get(); + struct ipc4_pipeline_create msg = { 0 }; + struct ipc_comp_dev *ipc_pipe; + int ret; + + LOG_INF("Starting IPC4 pipeline test (helpers)"); + + /* 1. Setup msg */ + msg.primary.r.instance_id = 1; + msg.primary.r.ppl_priority = SOF_IPC4_PIPELINE_PRIORITY_0; + msg.primary.r.ppl_mem_size = 1; + msg.primary.r.type = SOF_IPC4_GLB_CREATE_PIPELINE; + msg.extension.r.core_id = 0; + + /* 2. Create pipeline */ + ret = ipc_pipeline_new(ipc, (ipc_pipe_new *)&msg); + zassert_equal(ret, 0, "ipc_pipeline_new failed with %d", ret); + + /* 3. Validate pipeline exists */ + ipc_pipe = ipc_get_pipeline_by_id(ipc, 1); + zassert_not_null(ipc_pipe, "pipeline 1 not found after creation"); + + /* 4. Destroy pipeline */ + ret = ipc_pipeline_free(ipc, 1); + zassert_equal(ret, 0, "ipc_pipeline_free failed with %d", ret); + + /* 5. Validate pipeline is destroyed */ + ipc_pipe = ipc_get_pipeline_by_id(ipc, 1); + zassert_is_null(ipc_pipe, "pipeline 1 still exists after destruction"); + + LOG_INF("IPC4 pipeline test (helpers) complete"); +} + +ZTEST(userspace_ipc4_pipeline, test_pipeline_create_destroy_handlers) +{ + struct ipc *ipc = ipc_get(); + struct ipc4_pipeline_create create_msg = { 0 }; + struct ipc4_message_request req = { 0 }; + struct ipc_comp_dev *ipc_pipe; + int ret; + + LOG_INF("Starting IPC4 pipeline test (handlers)"); + + /* 1. Setup create message */ + create_msg.primary.r.instance_id = 2; + create_msg.primary.r.ppl_priority = SOF_IPC4_PIPELINE_PRIORITY_0; + create_msg.primary.r.ppl_mem_size = 1; + create_msg.primary.r.type = SOF_IPC4_GLB_CREATE_PIPELINE; + create_msg.extension.r.core_id = 0; + + /* Pack the create message into a generic request */ + req.primary.dat = create_msg.primary.dat; + req.extension.dat = create_msg.extension.dat; + + /* 2. Create pipeline using handler */ + ret = ipc4_new_pipeline(&req); + zassert_equal(ret, 0, "ipc4_new_pipeline failed with %d", ret); + + /* 3. Validate pipeline exists */ + ipc_pipe = ipc_get_pipeline_by_id(ipc, 2); + zassert_not_null(ipc_pipe, "pipeline 2 not found after creation"); + + /* 4. Setup delete message */ + struct ipc4_pipeline_delete delete_msg = { 0 }; + + delete_msg.primary.r.instance_id = 2; + delete_msg.primary.r.type = SOF_IPC4_GLB_DELETE_PIPELINE; + + /* Pack the delete message into a generic request */ + req.primary.dat = delete_msg.primary.dat; + req.extension.dat = delete_msg.extension.dat; + + /* Destroy pipeline using handler */ + ret = ipc4_delete_pipeline(&req); + zassert_equal(ret, 0, "ipc4_delete_pipeline failed with %d", ret); + + /* 5. Validate pipeline is destroyed */ + ipc_pipe = ipc_get_pipeline_by_id(ipc, 2); + zassert_is_null(ipc_pipe, "pipeline 2 still exists after destruction"); + + LOG_INF("IPC4 pipeline test (handlers) complete"); +} + +static void *ipc4_pipeline_setup(void) +{ + struct sof *sof = sof_get(); + + /* SOF_BOOT_TEST_STANDALONE skips IPC init. We must allocate it manually for testing. */ + if (!sof->ipc) { + sof->ipc = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*sof->ipc)); + zassert_not_null(sof->ipc, "IPC allocation failed"); + sof->ipc->comp_data = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, + SOF_IPC_MSG_MAX_SIZE); + zassert_not_null(sof->ipc->comp_data, "IPC comp_data allocation failed"); + k_spinlock_init(&sof->ipc->lock); + list_init(&sof->ipc->msg_list); + list_init(&sof->ipc->comp_list); + } + + return NULL; +} + +ZTEST(userspace_ipc4_pipeline, test_ipc4_pipeline_with_dp) +{ + struct ipc *ipc = ipc_get(); + struct ipc4_pipeline_create create_msg = { 0 }; + struct ipc4_message_request req = { 0 }; + struct ipc_comp_dev *ipc_pipe; + struct comp_dev *src_mod; + struct ipc_comp_dev *ipc_src_mod; + int ret; + + LOG_INF("Starting IPC4 pipeline with DP test"); + + /* 1. Setup create pipeline message */ + create_msg.primary.r.instance_id = 3; + create_msg.primary.r.ppl_priority = SOF_IPC4_PIPELINE_PRIORITY_0; + create_msg.primary.r.ppl_mem_size = 1; + create_msg.primary.r.type = SOF_IPC4_GLB_CREATE_PIPELINE; + create_msg.extension.r.core_id = 0; + + req.primary.dat = create_msg.primary.dat; + req.extension.dat = create_msg.extension.dat; + + /* 2. Create pipeline */ + ret = ipc4_new_pipeline(&req); + zassert_equal(ret, 0, "ipc4_new_pipeline failed with %d", ret); + + /* 3. Validate pipeline exists */ + ipc_pipe = ipc_get_pipeline_by_id(ipc, 3); + zassert_not_null(ipc_pipe, "pipeline 3 not found after creation"); + zassert_not_null(NULL, 3, "pipeline 3 id is not 3"); + + /* 4. Instantiate a real SRC module */ + /* Since we don't have a manifest in the test environment, we need to manually + * call the module constructor to register the SRC component driver and instantiate it. + */ + extern void sys_comp_module_src_interface_init(void); + /* SRC4 UUID from uuid-registry.txt */ + const struct sof_uuid SRC_UUID = { + 0xe61bb28d, 0x149a, 0x4c1f, + { 0xb7, 0x09, 0x46, 0x82, 0x3e, 0xf5, 0xf5, 0xae } + }; + struct comp_driver_list *drivers; + struct list_item *clist; + const struct comp_driver *src_drv = NULL; + + sys_comp_module_src_interface_init(); + drivers = comp_drivers_get(); + list_for_item(clist, &drivers->list) { + struct comp_driver_info *info = container_of(clist, struct comp_driver_info, list); + zassert_not_null(info, "info is null"); + zassert_not_null(info->drv, "info->drv is null"); + zassert_not_null(info->drv->uid, "info->drv->uid is null"); + printk("Driver info: %p\n", info); + printk("Driver: %p\n", info->drv); + printk("Driver uid: %p\n", info->drv->uid); + printk("Driver uid: %p\n", &SRC_UUID); + if (!memcmp(info->drv->uid, &SRC_UUID, sizeof(struct sof_uuid))) { + src_drv = info->drv; + break; + } + } + zassert_not_null(src_drv, "SRC driver not found"); + + struct comp_ipc_config src_ipc_config = { + .id = IPC4_COMP_ID(1, 0), + .pipeline_id = 3, + .core = 0, + .proc_domain = COMP_PROCESSING_DOMAIN_DP, + .type = SOF_COMP_MODULE_ADAPTER, + .ipc_config_size = 0, + .ipc_extended_init = false, + }; + struct ipc4_base_module_cfg base_cfg = { + .cpc = 1, + .ibs = 100, + .obs = 100, + .is_pages = 0, + .audio_fmt = { + .sampling_frequency = IPC4_FS_48000HZ, + .depth = IPC4_DEPTH_16BIT, + .ch_map = 0, + .ch_cfg = IPC4_CHANNEL_CONFIG_STEREO, + .interleaving_style = IPC4_CHANNELS_INTERLEAVED, + .channels_count = 2, + .valid_bit_depth = 16, + .s_type = IPC4_TYPE_SIGNED_INTEGER, + } + }; + struct ipc_config_process spec = { + .data = (const unsigned char *)&base_cfg, + .size = sizeof(base_cfg), + }; + src_mod = src_drv->ops.create(src_drv, &src_ipc_config, &spec); + zassert_not_null(src_mod, "module creation failed for SRC"); + + list_init(&src_mod->bsource_list); + list_init(&src_mod->bsink_list); + + /* Create ipc container for SRC module */ + ipc_src_mod = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, sizeof(*ipc_src_mod)); + zassert_not_null(ipc_src_mod, "ipc SRC module container allocation failed"); + + ipc_src_mod->cd = src_mod; + ipc_src_mod->type = COMP_TYPE_COMPONENT; + ipc_src_mod->id = src_mod->ipc_config.id; + ipc_src_mod->core = src_mod->ipc_config.core; + ipc_src_mod->pipeline = ipc_pipe->pipeline; + + /* Add SRC module to ipc component list */ + list_item_append(&ipc_src_mod->list, &ipc->comp_list); + + /* Connect module to pipeline (in real scenario handled by pipeline_connect buffers etc - mock just list append) */ + + /* 5. Cleanup */ + /* Destroy pipeline using IPC pipeline free which cascades to frees component list */ + /* Note: This will call ipc_comp_free which loops freeing modules in the pipeline. */ + ret = ipc_pipeline_free(ipc, 3); + zassert_equal(ret, 0, "ipc_pipeline_free failed with %d", ret); + LOG_INF("IPC4 pipeline with DP test complete"); +} + +ZTEST_SUITE(userspace_ipc4_pipeline, NULL, ipc4_pipeline_setup, NULL, NULL, NULL); + + diff --git a/zephyr/test/userspace/test_ll_task.c b/zephyr/test/userspace/test_ll_task.c index ec4439cfea6c..4dbcfa91df1e 100644 --- a/zephyr/test/userspace/test_ll_task.c +++ b/zephyr/test/userspace/test_ll_task.c @@ -58,9 +58,6 @@ static void ll_task_test(void) task = zephyr_ll_task_alloc(); zassert_not_null(task, "task allocation failed"); - /* allow user space to report status via 'test_runs' */ - k_mem_domain_add_partition(zephyr_ll_mem_domain(), &userspace_ll_part); - /* work in progress, see pipeline-schedule.c */ ret = schedule_task_init_ll(task, SOF_UUID(test_task_uuid), SOF_SCHEDULE_LL_TIMER, priority, task_callback, @@ -91,19 +88,13 @@ static void ll_task_test(void) ZTEST(userspace_ll, ll_task_test) { +#ifndef CONFIG_QEMU_TARGET ll_task_test(); +#else + ztest_test_skip(); +#endif } ZTEST_SUITE(userspace_ll, NULL, NULL, NULL, NULL, NULL); -/** - * SOF main has booted up and IPC handling is stopped. - * Run test suites with ztest_run_all. - */ -static int run_tests(void) -{ - ztest_run_test_suite(userspace_ll, false, 1, 1, NULL); - return 0; -} -SYS_INIT(run_tests, APPLICATION, 99); diff --git a/zephyr/test/userspace/test_mailbox.c b/zephyr/test/userspace/test_mailbox.c index 766dababb553..adce22f66d7e 100644 --- a/zephyr/test/userspace/test_mailbox.c +++ b/zephyr/test/userspace/test_mailbox.c @@ -79,6 +79,7 @@ static void mailbox_test(void) ZTEST(userspace_mailbox, mailbox_test) { +#ifndef CONFIG_QEMU_TARGET /* first test from kernel */ mailbox_write_to_pipeline_regs(); @@ -86,18 +87,11 @@ ZTEST(userspace_mailbox, mailbox_test) mailbox_test(); ztest_test_pass(); +#else + ztest_test_skip(); +#endif } ZTEST_SUITE(userspace_mailbox, NULL, NULL, NULL, NULL, NULL); -/** - * SOF main has booted up and IPC handling is stopped. - * Run test suites with ztest_run_all. - */ -static int run_tests(void) -{ - ztest_run_test_suite(userspace_mailbox, false, 1, 1, NULL); - return 0; -} -SYS_INIT(run_tests, APPLICATION, 99);