From 468eef91a0429e6c7f36ddc0075a5e3bc30a561f Mon Sep 17 00:00:00 2001 From: martin-velay Date: Thu, 4 Jun 2026 17:27:42 +0200 Subject: [PATCH 1/4] [dv] Add SW logger support and smoketest sequence Signed-off-by: martin-velay --- .../top_chip_dv_dv_log_smoketest_vseq.sv | 54 +++++++++++++++++++ .../dv/env/seq_lib/top_chip_dv_vseq_list.sv | 1 + hw/top_chip/dv/env/top_chip_dv_env.core | 1 + hw/top_chip/dv/env/top_chip_dv_env_pkg.sv | 6 ++- hw/top_chip/dv/top_chip_sim_cfg.hjson | 24 +++++++-- 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 hw/top_chip/dv/env/seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv diff --git a/hw/top_chip/dv/env/seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv b/hw/top_chip/dv/env/seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv new file mode 100644 index 000000000..ceca9dda0 --- /dev/null +++ b/hw/top_chip/dv/env/seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv @@ -0,0 +1,54 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +class top_chip_dv_dv_log_smoketest_vseq extends top_chip_dv_base_vseq; + `uvm_object_utils(top_chip_dv_dv_log_smoketest_vseq) + + string exp_messages[] = '{ + "SW-DV log smoketest starting...", + "a = 10, b = 20, c = 12", + "a + b + c = 42", + "Smoke test for the SW-DV log interface, completed successfully!" + }; + + typedef bit [1024*8-1:0] arg_t; + + extern function new(string name = ""); + extern task body(); + // Per SV LRM 6.16, a string shorter than the packed type is left-padded with zeros: the last + // character sits at byte[0] (LSB) and the first at byte[N-1], with zeros above. Scan upward + // from byte[0] and prepend each character to reconstruct left-to-right order. + extern function string arg_t_to_string(arg_t val); +endclass : top_chip_dv_dv_log_smoketest_vseq + + +function top_chip_dv_dv_log_smoketest_vseq::new(string name = ""); + super.new(name); +endfunction : new + +function string top_chip_dv_dv_log_smoketest_vseq::arg_t_to_string(arg_t val); + string result = ""; + for (int i = 0; i < ($bits(val) / 8); i++) begin + logic [7:0] c = val[i*8 +: 8]; + if (c == 8'h00) break; + result = {$sformatf("%c", c), result}; + end + return result; +endfunction : arg_t_to_string + +task top_chip_dv_dv_log_smoketest_vseq::body(); + string actual; + + if (cfg == null) begin + set_handles(); + end + + // Check each expected message inline as the event fires. This ensures we sample printed_log + // in the same delta as the event. + for (int i = 0; i < exp_messages.size(); i++) begin + @(cfg.sw_logger_vif.printed_log_event); + actual = arg_t_to_string(arg_t'(cfg.sw_logger_vif.printed_log)); + `DV_CHECK_STREQ(actual, exp_messages[i], $sformatf("Log message [%0d] mismatch", i)) + end +endtask : body diff --git a/hw/top_chip/dv/env/seq_lib/top_chip_dv_vseq_list.sv b/hw/top_chip/dv/env/seq_lib/top_chip_dv_vseq_list.sv index 151f6eb4b..89caeb6ec 100644 --- a/hw/top_chip/dv/env/seq_lib/top_chip_dv_vseq_list.sv +++ b/hw/top_chip/dv/env/seq_lib/top_chip_dv_vseq_list.sv @@ -5,3 +5,4 @@ `include "top_chip_dv_base_vseq.sv" `include "top_chip_dv_uart_base_vseq.sv" `include "top_chip_dv_gpio_smoke_vseq.sv" +`include "top_chip_dv_dv_log_smoketest_vseq.sv" diff --git a/hw/top_chip/dv/env/top_chip_dv_env.core b/hw/top_chip/dv/env/top_chip_dv_env.core index 7083a2a41..cd5d8ab4d 100644 --- a/hw/top_chip/dv/env/top_chip_dv_env.core +++ b/hw/top_chip/dv/env/top_chip_dv_env.core @@ -24,6 +24,7 @@ filesets: - seq_lib/top_chip_dv_base_vseq.sv: {is_include_file: true} - seq_lib/top_chip_dv_uart_base_vseq.sv: {is_include_file: true} - seq_lib/top_chip_dv_gpio_smoke_vseq.sv: {is_include_file: true} + - seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv: {is_include_file: true} file_type: systemVerilogSource targets: diff --git a/hw/top_chip/dv/env/top_chip_dv_env_pkg.sv b/hw/top_chip/dv/env/top_chip_dv_env_pkg.sv index e65aaab3d..48f1645af 100644 --- a/hw/top_chip/dv/env/top_chip_dv_env_pkg.sv +++ b/hw/top_chip/dv/env/top_chip_dv_env_pkg.sv @@ -41,11 +41,13 @@ package top_chip_dv_env_pkg; // 50 MHz Peripheral clock parameter int unsigned PeriClkFreq = 50_000_000; - // SW DV special write locations for test status and logging will always fit in 32-bits + // SW DV special write locations for test status and logging will always fit in 32-bits. + // Addresses must be 8-byte aligned: AxiDataWidth=64, so a 32-bit store at offset 4 + // within a bus word lands in req.w.data[63:32] (upper lane), not req.w.data[31:0]. parameter bit [31:0] SW_DV_START_ADDR = 'h2002_0000; parameter bit [31:0] SW_DV_SIZE = 'h0000_0100; // 256 bytes reserved for SW DV parameter bit [31:0] SW_DV_TEST_STATUS_ADDR = SW_DV_START_ADDR + 'h00; - parameter bit [31:0] SW_DV_LOG_ADDR = SW_DV_START_ADDR + 'h04; + parameter bit [31:0] SW_DV_LOG_ADDR = SW_DV_START_ADDR + 'h08; // File includes `include "mem_clear_util.sv" diff --git a/hw/top_chip/dv/top_chip_sim_cfg.hjson b/hw/top_chip/dv/top_chip_sim_cfg.hjson index 115ab003a..22656080f 100644 --- a/hw/top_chip/dv/top_chip_sim_cfg.hjson +++ b/hw/top_chip/dv/top_chip_sim_cfg.hjson @@ -117,6 +117,20 @@ run_opts: ["+ChipMemSRAM_image_file={run_dir}/test_framework_exception_test_cheri_sram.vmem", "+ChipMemROM_image_file={run_dir}/bootrom.vmem"] } + { + name: dv_log_smoketest + uvm_test_seq: top_chip_dv_dv_log_smoketest_vseq + sw_images: ["dv_log_smoketest_vanilla_sram:5" "bootrom:5"] + run_opts: ["+ChipMemSRAM_image_file={run_dir}/dv_log_smoketest_vanilla_sram.vmem", + "+ChipMemROM_image_file={run_dir}/bootrom.vmem"] + } + { + name: dv_log_smoketest_cheri + uvm_test_seq: top_chip_dv_dv_log_smoketest_vseq + sw_images: ["dv_log_smoketest_cheri_sram:5" "bootrom:5"] + run_opts: ["+ChipMemSRAM_image_file={run_dir}/dv_log_smoketest_cheri_sram.vmem", + "+ChipMemROM_image_file={run_dir}/bootrom.vmem"] + } { name: spi_device_smoke uvm_test_seq: top_chip_dv_base_vseq @@ -256,7 +270,7 @@ name: rv_dm_access_after_wakeup uvm_test_seq: top_chip_dv_rv_dm_access_after_wakeup_vseq sw_images: ["rv_dm_access_after_wakeup_test_vanilla_sram:5" "bootrom:5"] - run_opts: ["+use_jtag_dmi=1", + run_opts: ["+use_jtag_dmi=1", "+ChipMemSRAM_image_file={run_dir}/rv_dm_access_after_wakeup_test_vanilla_sram.vmem", "+ChipMemROM_image_file={run_dir}/bootrom.vmem"] } @@ -264,7 +278,7 @@ name: rv_dm_access_after_escalation_reset uvm_test_seq: "top_chip_dv_rv_dm_access_after_escalation_reset_vseq" sw_images: ["rv_dm_access_after_escalation_reset_test_vanilla_sram:5" "bootrom:5"] - run_opts: ["+use_jtag_dmi=1", + run_opts: ["+use_jtag_dmi=1", "+ChipMemSRAM_image_file={run_dir}/rv_dm_access_after_escalation_reset_test_vanilla_sram.vmem", "+ChipMemROM_image_file={run_dir}/bootrom.vmem"] } @@ -341,6 +355,8 @@ "rv_timer_irq_cheri", "test_framework_exception_test", "test_framework_exception_test_cheri", + "dv_log_smoketest", + "dv_log_smoketest_cheri", "spi_device_smoke", "spi_device_smoke_cheri", "spi_host_smoke", @@ -390,7 +406,9 @@ name: test_framework tests: [ "test_framework_exception_test", - "test_framework_exception_test_cheri" + "test_framework_exception_test_cheri", + "dv_log_smoketest", + "dv_log_smoketest_cheri" ] } { From d46088d8cbbd9806bb2c824a6ed0ca4a0ca8eec6 Mon Sep 17 00:00:00 2001 From: martin-velay Date: Fri, 5 Jun 2026 08:25:19 +0200 Subject: [PATCH 2/4] [dv,doc] Update README.md files for logging Signed-off-by: martin-velay --- hw/top_chip/dv/README.md | 7 +++++-- hw/top_chip/dv/sim_sram_axi/README.md | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/hw/top_chip/dv/README.md b/hw/top_chip/dv/README.md index b46426348..aa414e20c 100644 --- a/hw/top_chip/dv/README.md +++ b/hw/top_chip/dv/README.md @@ -71,8 +71,11 @@ For more details, see: [sim_sram_axi/README.md](./sim_sram_axi/README.md) * **Test Status:** The SW writes Pass/Fail status to `SW_DV_TEST_STATUS_ADDR`. The `sw_test_status_if` detects this and signals the UVM environment to terminate the simulation. -* **Logging:** The SW writes debug strings to `SW_DV_LOG_ADDR`. - The `sw_logger_if` captures these characters and prints them to the simulation log, avoiding the latency of the UART peripheral. +* **Logging:** The SW uses the `DV_LOG_INFO` / `DV_LOG_WARNING` / `DV_LOG_ERROR` / `DV_LOG_FATAL` macros (defined in `sw/device/lib/test_framework/dv_log.h`) to emit log messages. + Each call allocates a static `log_fields_t` struct (severity, file, line, argument count, format string) in the `.logs.fields` ELF section, then writes its address followed by any variadic arguments to `SW_DV_LOG_ADDR`. + At build time, `util/build_sw_collateral_for_sim.py` invokes `extract_sw_logs.py` on the firmware ELF to produce a `.logs.txt` database mapping each struct address to its decoded fields. + At simulation time, `sw_logger_if` intercepts the write, looks up the struct address in the database, substitutes the arguments into the format string, and prints the fully formatted message to the simulation log. + This bypasses the slowness of the UART peripheral and requires no string formatting on the CPU. ## Simulation commands diff --git a/hw/top_chip/dv/sim_sram_axi/README.md b/hw/top_chip/dv/sim_sram_axi/README.md index 3d9b9d9d1..139fcf7a7 100644 --- a/hw/top_chip/dv/sim_sram_axi/README.md +++ b/hw/top_chip/dv/sim_sram_axi/README.md @@ -72,7 +72,10 @@ These signals are used to generate the `wr_valid` signal, qualifying valid AXI w In the Testbench Top (`tb.sv`), higher-level verification interfaces are `bind`-ed to this signal and their virtual handle is added into the `uvm_config_db`: 1. **`sw_test_status_if`**: Monitors writes to `SW_DV_TEST_STATUS_ADDR` to detect if the test Passed or Failed. -2. **`sw_logger_if`**: Monitors writes to `SW_DV_LOG_ADDR` to capture `printf` characters and display them in the simulation log. +2. **`sw_logger_if`**: Monitors writes to `SW_DV_LOG_ADDR` to decode SW log messages. + The SW writes the address of a static `log_fields_t` struct (severity, file, line, nargs, format string) plus any variadic arguments. + A pre-generated `.logs.txt` database (produced by `extract_sw_logs.py` at build time) maps each struct address to its fields; + the interface looks up the address, substitutes the arguments, and prints the formatted message to the simulation log. ## Usage From b498caba3769c3cdd1ade3b79f190e1b304c5969 Mon Sep 17 00:00:00 2001 From: martin-velay Date: Thu, 4 Jun 2026 17:45:30 +0200 Subject: [PATCH 3/4] [utils] Add rv64 ELF support to extract_sw_logs.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream script assumes a 32-bit struct layout (5×uint32, 20 B, LONG header). Mocha's rv64 log_fields_t uses 8-byte const char * pointer fields, giving a 32-byte struct and an 8-byte QUAD section-address header. Add a patch that detects the ELF class at runtime and selects the correct header format, entry size, and unpack string accordingly. Wire the script into build_sw_collateral_for_sim.py so each CMake install step produces the .logs.txt and .rodata.txt files that sw_logger_if needs at simulation start. Signed-off-by: martin-velay --- hw/vendor/lowrisc_ip.vendor.hjson | 2 +- .../util/device_sw_utils/extract_sw_logs.py | 25 ++++++++--- .../0001-rv64-elf-support.patch | 40 +++++++++++++++++ util/build_sw_collateral_for_sim.py | 44 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 hw/vendor/patches/lowrisc_ip/util_device_sw_utils/0001-rv64-elf-support.patch diff --git a/hw/vendor/lowrisc_ip.vendor.hjson b/hw/vendor/lowrisc_ip.vendor.hjson index 840f601d0..8af645d8c 100644 --- a/hw/vendor/lowrisc_ip.vendor.hjson +++ b/hw/vendor/lowrisc_ip.vendor.hjson @@ -16,7 +16,7 @@ // Utilities. {from: "util/basegen", to: "util/basegen"}, // Dependency of top generator. {from: "util/design", to: "util/design"}, // Dependency of IP generator. - {from: "util/device_sw_utils", to: "util/device_sw_utils"}, + {from: "util/device_sw_utils", to: "util/device_sw_utils", patch_dir: "util_device_sw_utils"}, {from: "util/ipgen.py", to: "util/ipgen.py"}, // IP generator. {from: "util/ipgen", to: "util/ipgen"}, {from: "util/raclgen", to: "util/raclgen"}, // Dependency of top generator. diff --git a/hw/vendor/lowrisc_ip/util/device_sw_utils/extract_sw_logs.py b/hw/vendor/lowrisc_ip/util/device_sw_utils/extract_sw_logs.py index b4fa8da06..d94ee55a2 100755 --- a/hw/vendor/lowrisc_ip/util/device_sw_utils/extract_sw_logs.py +++ b/hw/vendor/lowrisc_ip/util/device_sw_utils/extract_sw_logs.py @@ -222,17 +222,30 @@ def extract_sw_logs(elf_file, logs_fields_section): logs_fields_section, elf_file)) sys.exit(1) - header_size = 4 - logs_offset, = struct.unpack('I', logs_data[0:header_size]) + # rv64 ELF: struct layout is 32 bytes with 8-byte pointer fields and a + # QUAD section-address header. rv32 ELF: 20 bytes, all uint32, LONG header. + is_64bit = elf.elfclass == 64 + if is_64bit: + header_size = 8 + entry_size = 32 + header_fmt = ' None: + """Run extract_sw_logs.py to generate the DV logger database files. + + Generates .logs.txt and .rodata.txt in out_dir. + The sw_logger_if monitor reads these at simulation start to decode + printf-style log messages written by firmware to SW_DV_LOG_ADDR. + + A missing .logs.fields ELF section is non-fatal: it means the SW image + was built without logging support, so we emit a warning and continue. + """ + if not _EXTRACT_SW_LOGS.exists(): + print(f"Warning: extract_sw_logs.py not found at {_EXTRACT_SW_LOGS}, " + "skipping DV log database generation.") + return + + name = elf_file.stem if elf_file.suffix else elf_file.name + cmd = [ + sys.executable, str(_EXTRACT_SW_LOGS), + "--elf-file", str(elf_file), + "--name", name, + "--outdir", str(out_dir), + ] + result = subprocess.run(cmd, capture_output=True) + if result.returncode != 0: + stderr = result.stderr.decode(errors="replace").strip() + print(f"Warning: DV log extraction skipped for '{name}' " + f"(no .logs.fields section or parse error): {stderr}") + def generate(args) -> None: """Build with cmake""" @@ -26,6 +64,12 @@ def generate(args) -> None: cmd = [*install_cmd, target] subprocess.run(cmd, capture_output=False, check=True) + # Generate .logs.txt / .rodata.txt for the sw_logger_if DV monitor. + # The ELF is installed without an extension (CMake default). + elf_file = out_dir / target + if elf_file.exists(): + _extract_logs(elf_file, out_dir) + print("Finished") From b88ffc4cd9c2cf3ee1b3359ed950a79b3f5a6407 Mon Sep 17 00:00:00 2001 From: martin-velay Date: Thu, 4 Jun 2026 17:53:03 +0200 Subject: [PATCH 4/4] [sw] Add DV_LOG_* macros for SW-to-DV logging Each call site places a log_fields_t entry in a new .logs.fields ELF section; extract_sw_logs.py produces the database consumed by sw_logger_if to decode printf-style messages at simulation start, with no UART overhead. Add mocha_system_dv_log() to the HAL for CHERI-safe access to the log write port (0x2002_0008). Add the .logs.fields section to the linker script after .rodata so string constants are resolved before log entries are laid out. Add dv_log.h/c implementing the DV_LOG_INFO/WARNING/ERROR/ FATAL macros and the runtime dv_log_write() function. Add dv_log_smoketest to verify the end-to-end path. Signed-off-by: martin-velay --- sw/device/lib/boot/mocha.ld | 11 ++++ sw/device/lib/hal/mocha.c | 10 ++++ sw/device/lib/hal/mocha.h | 1 + sw/device/lib/test_framework/CMakeLists.txt | 2 +- sw/device/lib/test_framework/dv_log.c | 21 ++++++++ sw/device/lib/test_framework/dv_log.h | 54 +++++++++++++++++++ sw/device/tests/CMakeLists.txt | 1 + .../tests/test_framework/dv_log_smoketest.c | 20 +++++++ 8 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 sw/device/lib/test_framework/dv_log.c create mode 100644 sw/device/lib/test_framework/dv_log.h create mode 100644 sw/device/tests/test_framework/dv_log_smoketest.c diff --git a/sw/device/lib/boot/mocha.ld b/sw/device/lib/boot/mocha.ld index 93b18914e..4e145e185 100644 --- a/sw/device/lib/boot/mocha.ld +++ b/sw/device/lib/boot/mocha.ld @@ -30,6 +30,17 @@ SECTIONS { . = ALIGN(8); } > ram + /* DV simulation log database. Each call site puts a log_fields_t entry here. + * extract_sw_logs.py reads the ELF section to produce a .logs.txt database + * consumed by sw_logger_if at simulation start. The first word is the section + * load address (header used by extract_sw_logs.py to map each entry to its + * runtime address). */ + .logs.fields : { + QUAD(LOADADDR(.logs.fields)); + KEEP(*(.logs.fields)) + . = ALIGN(8); + } > ram + /* capability relocation entries */ /* __start___cap_relocs and __stop___cap_relocs are provided by the toolchain */ __cap_relocs : { diff --git a/sw/device/lib/hal/mocha.c b/sw/device/lib/hal/mocha.c index 46a9d583d..fead8e550 100644 --- a/sw/device/lib/hal/mocha.c +++ b/sw/device/lib/hal/mocha.c @@ -14,6 +14,7 @@ static const uintptr_t rom_base = 0x80000ul; static const uintptr_t mailbox_base = 0x20010000ul; static const uintptr_t dv_test_status_base = 0x20020000ul; +static const uintptr_t dv_log_base = 0x20020008ul; static const uintptr_t ethernet_base = 0x30000000ul; static const uintptr_t gpio_base = 0x40000000ul; static const uintptr_t clkmgr_base = 0x40020000ul; @@ -201,3 +202,12 @@ void *mocha_system_dv_test_status(void) return (void *)dv_test_status_base; #endif /* defined(__riscv_zcherihybrid) */ } + +void *mocha_system_dv_log(void) +{ +#if defined(__riscv_zcherihybrid) + return create_mmio_capability(dv_log_base, 0x4u); +#else /* !defined(__riscv_zcherihybrid) */ + return (void *)dv_log_base; +#endif /* defined(__riscv_zcherihybrid) */ +} diff --git a/sw/device/lib/hal/mocha.h b/sw/device/lib/hal/mocha.h index be8f33ffa..e917de870 100644 --- a/sw/device/lib/hal/mocha.h +++ b/sw/device/lib/hal/mocha.h @@ -52,3 +52,4 @@ plic_t mocha_system_plic(void); void *mocha_system_dram(void); void *mocha_system_dv_test_status(void); +void *mocha_system_dv_log(void); diff --git a/sw/device/lib/test_framework/CMakeLists.txt b/sw/device/lib/test_framework/CMakeLists.txt index 20c70d13f..defd328db 100644 --- a/sw/device/lib/test_framework/CMakeLists.txt +++ b/sw/device/lib/test_framework/CMakeLists.txt @@ -2,7 +2,7 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -set(SRCS main.c) +set(SRCS main.c dv_log.c) set(LIBS hal startup runtime) mocha_add_library(NAME test_framework SOURCES ${SRCS} LIBRARIES ${LIBS}) diff --git a/sw/device/lib/test_framework/dv_log.c b/sw/device/lib/test_framework/dv_log.c new file mode 100644 index 000000000..a22632812 --- /dev/null +++ b/sw/device/lib/test_framework/dv_log.c @@ -0,0 +1,21 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "test_framework/dv_log.h" +#include "hal/mmio.h" +#include "hal/mocha.h" +#include + +void dv_log_write(const log_fields_t *log, ...) +{ + void *port = mocha_system_dv_log(); + DEV_WRITE(port, (uint32_t)(uintptr_t)log); + + va_list args; + va_start(args, log); + for (uint32_t i = 0; i < log->nargs; i++) { + DEV_WRITE(port, va_arg(args, uint32_t)); + } + va_end(args); +} diff --git a/sw/device/lib/test_framework/dv_log.h b/sw/device/lib/test_framework/dv_log.h new file mode 100644 index 000000000..38f10fe31 --- /dev/null +++ b/sw/device/lib/test_framework/dv_log.h @@ -0,0 +1,54 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +// SW-to-DV logging: writes a log_fields_t entry address + arguments to +// SW_DV_LOG_ADDR; sw_logger_if reconstructs the formatted string in simulation. + +// const char * for string fields: required for a valid C99 static initialiser +// (pointer-to-integer casts are not constant expressions). extract_sw_logs.py +// detects the rv64 ELF class and uses '