Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions hw/top_chip/dv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,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 `<name>.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

Expand Down
58 changes: 58 additions & 0 deletions hw/top_chip/dv/env/seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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]. Collect the bytes upward from
// byte[0] until the null terminator, then reverse them with the streaming operator to restore
// 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);
byte unsigned bytes[$];
for (int i = 0; i < ($bits(val) / 8); i++) begin
logic [7:0] c = val[i*8 +: 8];
if (c == 8'h00) break;
bytes.push_back(c);
end
return string'({<<8{bytes}});
endfunction : arg_t_to_string

task top_chip_dv_dv_log_smoketest_vseq::body();
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
string actual;
@(cfg.sw_logger_vif.printed_log_event);
actual = arg_t_to_string(arg_t'(cfg.sw_logger_vif.printed_log));
Comment thread
martin-velay marked this conversation as resolved.
if (actual != exp_messages[i]) begin
`uvm_error(get_full_name(),
$sformatf("Log message mismatch at index %0d. Expected '%0s' but saw '%0s'.",
i, exp_messages[i], actual))
end
end
endtask : body
1 change: 1 addition & 0 deletions hw/top_chip/dv/env/seq_lib/top_chip_dv_vseq_list.sv
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
`include "top_chip_dv_gpio_smoke_vseq.sv"
`include "top_chip_dv_i2c_tx_rx_vseq.sv"
`include "top_chip_dv_i2c_host_tx_rx_vseq.sv"
`include "top_chip_dv_dv_log_smoketest_vseq.sv"
1 change: 1 addition & 0 deletions hw/top_chip/dv/env/top_chip_dv_env.core
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ filesets:
- seq_lib/top_chip_dv_gpio_smoke_vseq.sv: {is_include_file: true}
- seq_lib/top_chip_dv_i2c_tx_rx_vseq.sv: {is_include_file: true}
- seq_lib/top_chip_dv_i2c_host_tx_rx_vseq.sv: {is_include_file: true}
- seq_lib/top_chip_dv_dv_log_smoketest_vseq.sv: {is_include_file: true}
file_type: systemVerilogSource

targets:
Expand Down
14 changes: 12 additions & 2 deletions hw/top_chip/dv/env/top_chip_dv_env_pkg.sv
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,18 @@ package top_chip_dv_env_pkg;
// 50 MHz Peripheral clock
parameter int unsigned PeriClkFreq = 50_000_000;

// SW-DV special locations for test status, logging, and platform identification.
// In simulation, sim_sram_axi_sink is connected to the dedicated sw_dv crossbar port.
// SW-DV special locations for test status, logging, and platform identification. In simulation,
// sim_sram_axi_sink is connected to the dedicated sw_dv crossbar port and each location holds a
// 32-bit value.
//
// The sw_test_status_if and sw_logger_if monitors only sample the low 32 bits of the 64-bit AXI
// write data (req.w.data[31:0], enforced by the tb DataUpperBitsZero_A assertion). A 32-bit store
// must land in that lane, so written locations (test status, log) sit at 8-byte aligned offsets;
// a store to offset 4 would land in the upper lane and be missed. HW_ID is read-only, so it can
// sit at offset 4 (the sink returns it on the read-data upper half resp.r.data[63:32]). The log
// takes the 0x8 slot rather than sharing with HW_ID so it has room to grow to 8 bytes for 64-bit
// values.

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;
Expand Down
5 changes: 4 additions & 1 deletion hw/top_chip/dv/sim_sram_axi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,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 `<name>.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

Expand Down
20 changes: 19 additions & 1 deletion hw/top_chip/dv/top_chip_sim_cfg.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@
run_opts: ["+ChipMemSRAM_image_file={run_dir}/test_framework_exception_test_cheri_sram.vmem",
"+ChipMemROM_image_file={run_dir}/bootrom_scrambled.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_scrambled.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_scrambled.vmem"]
}
{
name: spi_device_smoke
uvm_test_seq: top_chip_dv_base_vseq
Expand Down Expand Up @@ -364,6 +378,8 @@
"test_framework_test_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",
Expand Down Expand Up @@ -417,7 +433,9 @@
"test_framework_test",
"test_framework_test_cheri",
"test_framework_exception_test",
"test_framework_exception_test_cheri"
"test_framework_exception_test_cheri",
"dv_log_smoketest",
"dv_log_smoketest_cheri"
]
}
{
Expand Down
2 changes: 1 addition & 1 deletion hw/vendor/lowrisc_ip.vendor.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
66 changes: 59 additions & 7 deletions hw/vendor/lowrisc_ip/util/device_sw_utils/extract_sw_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,33 @@ def get_str_at_addr(str_addr, addr_strings):
raise KeyError(f"string at addr {str_addr:x} not found")


# CHERI __cap_relocs entry: five little-endian uint64 words.
_CAP_RELOC_FMT = '<QQQQQ'
_CAP_RELOC_SIZE = struct.calcsize(_CAP_RELOC_FMT)


def get_cap_relocs(elf):
'''Build a {capability_location: target_addr} map from __cap_relocs.
On CHERI targets, pointer fields in initialised data (such as the file and
format pointers of the log_fields_t entries in .logs.fields) are stored as
capabilities whose address is materialised at runtime. The static ELF only
holds a placeholder, while the real target is recorded in __cap_relocs. Each
entry is five uint64 words: (location, object, offset, size, perms); the
resolved target address is object + offset. Returns an empty dict on
non-CHERI ELFs, where the section is absent or empty.'''
section = elf.get_section_by_name('__cap_relocs')
relocs = {}
if section is None:
return relocs
data = section.data()
for off in range(0, len(data), _CAP_RELOC_SIZE):
location, obj, offset, _size, _perms = struct.unpack(
_CAP_RELOC_FMT, data[off:off + _CAP_RELOC_SIZE])
relocs[location] = obj + offset
return relocs


def extract_sw_logs(elf_file, logs_fields_section):
'''This function extracts contents from the logs fields section, and the
read only sections, processes them and generates a tuple of (results) -
Expand Down Expand Up @@ -222,17 +249,42 @@ 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])
# Determine the log_fields_t binary layout, which depends on the target.
# - rv32: 20-byte entries (all uint32) behind a 4-byte header.
# - rv64: 32-byte entries with 8-byte pointer fields, 8-byte header.
# - CHERI: file/format pointers are 16-byte capabilities, which forces
# 16-byte section alignment and 64-byte entries behind a
# 16-byte header. The capabilities hold placeholders in the
# static image; their target addresses come from __cap_relocs
# (see get_cap_relocs), keyed by the in-image slot location.
cap_relocs = get_cap_relocs(elf)
is_cheri = int(section.header['sh_addralign']) >= 16
if is_cheri:
sec_addr = int(section.header['sh_addr'])
header_size, entry_size = 16, 64
file_off, format_off = 0x10, 0x30
logs_offset = sec_addr
elif elf.elfclass == 64:
header_size, entry_size, entry_fmt = 8, 32, '<I4xQIIQ'
logs_offset, = struct.unpack('<Q', logs_data[0:header_size])
else:
header_size, entry_size, entry_fmt = 4, LOGS_FIELDS_SIZE, '<IIIII'
logs_offset, = struct.unpack('<I', logs_data[0:header_size])

# Dump the logs with fields.
result = ""
num_logs = (logs_size - header_size) // LOGS_FIELDS_SIZE
num_logs = (logs_size - header_size) // entry_size
for i in range(num_logs):
start = header_size + i * LOGS_FIELDS_SIZE
end = start + LOGS_FIELDS_SIZE
severity, file_addr, line, nargs, format_addr = struct.unpack(
'IIIII', logs_data[start:end])
start = header_size + i * entry_size
if is_cheri:
severity, = struct.unpack('<I', logs_data[start:start + 4])
line, nargs = struct.unpack(
'<II', logs_data[start + 0x20:start + 0x28])
file_addr = cap_relocs[sec_addr + start + file_off]
format_addr = cap_relocs[sec_addr + start + format_off]
else:
severity, file_addr, line, nargs, format_addr = struct.unpack(
entry_fmt, logs_data[start:start + entry_size])
result += "addr: {}\n".format(hex(logs_offset + start)[2:])
result += "severity: {}\n".format(severity)
result += "file: {}\n".format(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
diff --git a/extract_sw_logs.py b/extract_sw_logs.py
--- a/extract_sw_logs.py
+++ b/extract_sw_logs.py
@@ -173,6 +173,33 @@
raise KeyError(f"string at addr {str_addr:x} not found")


+# CHERI __cap_relocs entry: five little-endian uint64 words.
+_CAP_RELOC_FMT = '<QQQQQ'
+_CAP_RELOC_SIZE = struct.calcsize(_CAP_RELOC_FMT)
+
+
+def get_cap_relocs(elf):
+ '''Build a {capability_location: target_addr} map from __cap_relocs.
+
+ On CHERI targets, pointer fields in initialised data (such as the file and
+ format pointers of the log_fields_t entries in .logs.fields) are stored as
+ capabilities whose address is materialised at runtime. The static ELF only
+ holds a placeholder, while the real target is recorded in __cap_relocs. Each
+ entry is five uint64 words: (location, object, offset, size, perms); the
+ resolved target address is object + offset. Returns an empty dict on
+ non-CHERI ELFs, where the section is absent or empty.'''
+ section = elf.get_section_by_name('__cap_relocs')
+ relocs = {}
+ if section is None:
+ return relocs
+ data = section.data()
+ for off in range(0, len(data), _CAP_RELOC_SIZE):
+ location, obj, offset, _size, _perms = struct.unpack(
+ _CAP_RELOC_FMT, data[off:off + _CAP_RELOC_SIZE])
+ relocs[location] = obj + offset
+ return relocs
+
+
def extract_sw_logs(elf_file, logs_fields_section):
'''This function extracts contents from the logs fields section, and the
read only sections, processes them and generates a tuple of (results) -
@@ -222,17 +249,42 @@
logs_fields_section, elf_file))
sys.exit(1)

- header_size = 4
- logs_offset, = struct.unpack('I', logs_data[0:header_size])
+ # Determine the log_fields_t binary layout, which depends on the target.
+ # - rv32: 20-byte entries (all uint32) behind a 4-byte header.
+ # - rv64: 32-byte entries with 8-byte pointer fields, 8-byte header.
+ # - CHERI: file/format pointers are 16-byte capabilities, which forces
+ # 16-byte section alignment and 64-byte entries behind a
+ # 16-byte header. The capabilities hold placeholders in the
+ # static image; their target addresses come from __cap_relocs
+ # (see get_cap_relocs), keyed by the in-image slot location.
+ cap_relocs = get_cap_relocs(elf)
+ is_cheri = int(section.header['sh_addralign']) >= 16
+ if is_cheri:
+ sec_addr = int(section.header['sh_addr'])
+ header_size, entry_size = 16, 64
+ file_off, format_off = 0x10, 0x30
+ logs_offset = sec_addr
+ elif elf.elfclass == 64:
+ header_size, entry_size, entry_fmt = 8, 32, '<I4xQIIQ'
+ logs_offset, = struct.unpack('<Q', logs_data[0:header_size])
+ else:
+ header_size, entry_size, entry_fmt = 4, LOGS_FIELDS_SIZE, '<IIIII'
+ logs_offset, = struct.unpack('<I', logs_data[0:header_size])

# Dump the logs with fields.
result = ""
- num_logs = (logs_size - header_size) // LOGS_FIELDS_SIZE
+ num_logs = (logs_size - header_size) // entry_size
for i in range(num_logs):
- start = header_size + i * LOGS_FIELDS_SIZE
- end = start + LOGS_FIELDS_SIZE
- severity, file_addr, line, nargs, format_addr = struct.unpack(
- 'IIIII', logs_data[start:end])
+ start = header_size + i * entry_size
+ if is_cheri:
+ severity, = struct.unpack('<I', logs_data[start:start + 4])
+ line, nargs = struct.unpack(
+ '<II', logs_data[start + 0x20:start + 0x28])
+ file_addr = cap_relocs[sec_addr + start + file_off]
+ format_addr = cap_relocs[sec_addr + start + format_off]
+ else:
+ severity, file_addr, line, nargs, format_addr = struct.unpack(
+ entry_fmt, logs_data[start:start + entry_size])
result += "addr: {}\n".format(hex(logs_offset + start)[2:])
result += "severity: {}\n".format(severity)
result += "file: {}\n".format(
11 changes: 11 additions & 0 deletions sw/device/lib/boot/mocha.ld
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment thread
martin-velay marked this conversation as resolved.
/* capability relocation entries */
/* __start___cap_relocs and __stop___cap_relocs are provided by the toolchain */
__cap_relocs : {
Expand Down
7 changes: 6 additions & 1 deletion sw/device/lib/hal/dv.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ typedef volatile struct [[gnu::aligned(4)]] dv_window_memory_layout {
/* hw_id (0x4) */
const uint32_t hw_id;

const uint8_t __reserved0[0x100 - 0x08];
/* log (0x8)*/
uint32_t log;

const uint8_t __reserved0[0x100 - 0x0c];
} *dv_window_t;


_Static_assert(__builtin_offsetof(struct dv_window_memory_layout, test_status) == 0x0ul,
"incorrect register test_status offset");
_Static_assert(__builtin_offsetof(struct dv_window_memory_layout, hw_id) == 0x4ul,
"incorrect register hw_id offset");
_Static_assert(__builtin_offsetof(struct dv_window_memory_layout, log) == 0x8ul,
"incorrect register log offset");
2 changes: 1 addition & 1 deletion sw/device/lib/test_framework/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Loading