Skip to content

Add a discrete debug module simulation mode #2274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

hcallahan-lowrisc
Copy link
Contributor

Note. do not merge without coordination. Due to the way we handle the external spike dependency, lowRISC/riscv-isa-sim#26 will need to be merged in coordination with this PR to avoid CI breakage.


This PR adds a mechanism for testing ibex in a memory system that provisions code in a way to mimic the presence of a discrete external debug module.
This PR just adds the machinery to implement this. Follow up merges will add new tests that make use of it.

Desc.

When passing +is_discrete_debug_module to 'sim_opts' in the riscv-dv testlist
(dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml), this now results in the test
being compiled and loaded into the simulation memory models in a way that mimics
a discrete debug module mapped away from the main processor memory. Up to this
point, the riscv-dv binaries generate debug rom sections which are simply placed
within a single monolithic code section.

This requires a bit of machinery to achieve:

  • A new linker script to compile our test software for this layout
    (riscv_dv_extension/link.ld)
  • Test object files output into multiple binaries which are loaded seperately to
    initialize the sparse memory space. Two regions are supported, 'main' and 'dm'.
  • Initialize more testbench parameters via CLI args. This allows their values
    to be chosen dynamically in the future and to be test-dependent. For now, the
    various address / mask parameters are still set to fixed values for all tests.

One slightly unclean implementation detail is that the cosimulation model's
memory is loaded via binary .bin files, while the simulator's memory model is
loaded via verilog .vmem memory files. Ideally we would use .vmem files for
both, but the interface to the cosimulation model via DPI only implements
byte-writes, so loading a raw binary file is more convenient for that interface.

TODO
This could probably do with some standalone documentation, as there are still some
assumptions that glue the different steps together (.e.g .section names in the linker script).
However, this is broadly true for the entire DV architecture.

These assertions were a little too strong. While in practice the halt and
exception vector addresses would likely be within the range of the debug_module,
this is not mandated by the specification.
We actually do this is the current testbench configuration. The halt and
exception vectors are parameterized to `BOOT_ADDR and `BOOT_ADDR + 0x4
respectively, and the riscv-dv generated test binaries insert jump handlers
at those locations that target the .debug_rom and .debug_exception sections
somewhere else in the monolithic test binary. The assertions only worked because
the current parameterizations for Debug Module addresses put the debug module
right at the BOOT_ADDR parameter, which doesn't really make much sense.
  .DmBaseAddr (32'h`BOOT_ADDR)
  .DmAddrMask (32'h0000_0007 )
MTVEC is reset to `BOOT_ADDR, so this memory region would normally be used for
the mmode trap handler vector table.
In the future we probably want use Fusesoc to generate the filelist, which can
be dropped in place of the ibex_dv.f file now that it only contains file
includes.
In particular, this makes use of the new method
processor_t::set_debug_module_range(reg_t start_debug_val, reg_t end_debug_val)
When passing +is_discrete_debug_module to 'sim_opts' in the riscv-dv testlist
(dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml), this now results in the test
being compiled and loaded into the simulation memory models in a way that mimics
a discrete debug module mapped away from the main processor memory. Up to this
point, the riscv-dv binaries generate debug rom sections which are simply placed
within a single monolithic code section.

This requires a bit of machinery to achieve:

- A new linker script to compile our test software for this layout
  (riscv_dv_extension/link.ld
- Test binaries output into multiple binaries which are loaded seperately to
  initialize the sparse memory space. Two regions are supported, 'main' and 'dm'.
- Initialize more testbench parameters via CLI args. This allows their values
  to be chosen dynamically in the future and to be test-dependent. For now, the
  various address / mask parameters are still set to fixed values for all tests.

One slightly unclean implementation detail is that the cosimulation model's
memory is loaded via binary .bin files, while the simulator's memory model is
loaded via verilog .vmem memory files. Ideally we would use .vmem files for
both, but the interface to the cosimulation model via DPI only implements
byte-writes, so loading a raw binary file is more convenient for that interface.
This also matches an equivalent change in spike, which still configures this
address via a preprocessor macro.
@hcallahan-lowrisc hcallahan-lowrisc added Type:Enhancement Feature requests, enhancements Component:DV Design verification (DV) or testing issue labels May 13, 2025
@hcallahan-lowrisc
Copy link
Contributor Author

hcallahan-lowrisc commented May 13, 2025

Draft while I sort out CI breakage, but I don't anticipate huge structural changes from here.

@@ -10,9 +10,6 @@
// (2147483648), because an undecorated integer literal is interpreted as a
// signed 32-bit integer.
+define+BOOT_ADDR=8000_0000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this commit looks reasonable, but isn't the BOOT_ADDR define "left over"? Should that go in ibex_dv_defines.f too?

... Ah! I've just noticed that it gets dropped in two commits' time. Maybe just add a note to the commit log here that says it will happen?

@@ -72,7 +72,8 @@ class ibex_cosim_scoreboard extends uvm_scoreboard;

// TODO: Ensure log file on reset gets append rather than overwrite?
cosim_handle = spike_cosim_init(cfg.isa_string, cfg.start_pc, cfg.start_mtvec, cfg.log_file,
cfg.pmp_num_regions, cfg.pmp_granularity, cfg.mhpm_counter_num, cfg.secure_ibex, cfg.icache);
cfg.pmp_num_regions, cfg.pmp_granularity, cfg.mhpm_counter_num, cfg.secure_ibex, cfg.icache,
cfg.dm_start_addr, cfg.dm_end_addr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add a check here that the two addresses aren't both zero (because someone forgot to wire them up) ?

@@ -40,6 +40,11 @@ class SpikeCosim : public simif_t, public Cosim {
std::vector<std::string> errors;
bool nmi_mode;

// Start/End address of the debug module. The PMP module requires these
// addresses to implement a always-allow policy for accesses in debug mode.
uint32_t dm_start_addr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these values get set? I think e.g. the dm_start_addr argument to the constructor only gets passed to the processor object?

Is this the remnants of another design?


_dm_scratch_len = 0x100;

/* PHDRS */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code. Drop it?

if token.startswith("-T"):
cmd[index] = "-T" + str(md.ibex_riscvdv_customtarget / 'link.ld')

# Generate binary outputs for the different LOADABLE memory regions (main, dm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing "." at the end of this line? The rest of this comment looks like it could do with reflowing too: M-q?


trr.vmem_main = trr.dir_test / "test.main.vmem"
trr.vmem_dm = trr.dir_test / "test.dm.vmem"
vmem_dm_cmd = [env.get('RISCV_OBJCOPY'), "-O", "verilog", *only_sections, f"{trr.objectfile}", f"{trr.vmem_dm}"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. I'd probably be inclined to define a "objcopy" function to avoid having to repeat the env.get() stuff.

# If the +discrete_debug_module=1 argument is passed via sim_opts, a alternate
# memory heirarchy is enabled which seperates the debug rom memory sections
# from the main binary, emulating an external debug module
for plusarg, val in (trr.split('=') for trr in trr.sim_opts):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow: it's rather clever that this nesting is possible! It's not super easy to parse though: maybe define a variable for the generator / list that you get from splitting sim_opts?

# memory heirarchy is enabled which seperates the debug rom memory sections
# from the main binary, emulating an external debug module
for plusarg, val in (trr.split('=') for trr in trr.sim_opts):
if (plusarg == "+discrete_debug_module"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Stray ()

# from the main binary, emulating an external debug module
for plusarg, val in (trr.split('=') for trr in trr.sim_opts):
if (plusarg == "+discrete_debug_module"):
trr.is_discrete_debug_module = val # Value should be truthy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly surprised by this. Isn't val just a string? A value of zero ends up looking like this:

>>> print('yes' if '0' else 'no')
yes
>>> 

@@ -39,18 +39,20 @@ def _main() -> int:
trr.rtl_test = testopts['rtl_test']
trr.timeout_s = testopts.get('timeout_s') or md.run_rtl_timeout_s

if not os.path.exists(trr.binary):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to keep some (slightly generalised) version of this check?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component:DV Design verification (DV) or testing issue Type:Enhancement Feature requests, enhancements
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants