SQUASHED: Add new uvm test to hit hardware breakpoints coverpoints

Generate test_done: and test_fail: sections using handshake mechanism

Adding this behaviour to ibex_asm_program_gen allows all test to benefit
from the option of jumping directly to these label. Previously, ECALL was
used to provide a single path to this code.

Redefine ECALL handler to no-longer jump to 'write_tohost:'

This prevents the simulation from entering an infinite loop which it can no
longer detect and terminate from.

Add new uvm test to hit hardware breakpoints coverpoints

Overrides some riscv-dv classes to create a custom debug_rom for this test,
which is used to setup the breakpoint registers.
I have found it difficult to get stimulus of this hardware feature without
a more directed test. Improvements or ideas are welcome here.

Test-specific timeout of 5min within which I see >90% pass rate.

Change defaults for bad_intg on uninit accesses for Dmem/Imem

Imem : never create bad_intg on uninit access
Dmem : by default, enable bad_intg on uninit access. Plusarg to change behaviour.
This commit is contained in:
Harry Callahan 2022-10-10 17:32:34 +01:00
parent b8d8e23380
commit 6234aafe35
9 changed files with 311 additions and 18 deletions

View file

@ -21,7 +21,10 @@ class ibex_mem_intf_response_agent extends uvm_agent;
super.build_phase(phase);
monitor = ibex_mem_intf_monitor::type_id::create("monitor", this);
cfg = ibex_mem_intf_response_agent_cfg::type_id::create("cfg", this);
if (cfg == null)
if(!uvm_config_db #(ibex_mem_intf_response_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal(`gfn, "Could not locate mem_intf cfg object in uvm_config_db!")
if(get_is_active() == UVM_ACTIVE) begin
driver = ibex_mem_intf_response_driver::type_id::create("driver", this);
sequencer = ibex_mem_intf_response_sequencer::type_id::create("sequencer", this);

View file

@ -37,7 +37,7 @@ class ibex_mem_intf_response_agent_cfg extends uvm_object;
int unsigned zero_delay_pct = 50;
// CONTROL_KNOB : enable/disable to generation of bad integrity upon uninit accesses
bit enable_bad_intg_on_uninit_access = 1;
bit enable_bad_intg_on_uninit_access = 0;
constraint zero_delays_c {
zero_delays dist {1 :/ zero_delay_pct,
@ -56,7 +56,6 @@ class ibex_mem_intf_response_agent_cfg extends uvm_object;
function new(string name = "");
super.new(name);
void'($value$plusargs("enable_bad_intg_on_uninit_access=%0d", enable_bad_intg_on_uninit_access));
endfunction
endclass

View file

@ -12,6 +12,8 @@ class ibex_asm_program_gen extends riscv_asm_program_gen;
`uvm_object_new
virtual function void gen_program();
string instr[$];
default_include_csr_write = {
MSCRATCH,
MVENDORID,
@ -53,6 +55,35 @@ class ibex_asm_program_gen extends riscv_asm_program_gen;
riscv_csr_instr::create_csr_filter(cfg);
super.gen_program();
// Override the main gen_program() routine to append our own custom test_done/test_fail routines
// Generate test_done and test_fail routines at the end of the baseclass program.
gen_test_end(.result(TEST_PASS), .instr(instr));
instr_stream = {instr_stream,
{format_string("test_done:", LABEL_STR_LEN)},
instr};
instr.delete();
gen_test_end(.result(TEST_FAIL), .instr(instr));
instr_stream = {instr_stream,
{format_string("test_fail:", LABEL_STR_LEN)},
instr};
endfunction
// ECALL trap handler
// For riscv-dv in Ibex, ECALL is no-longer used to end the test.
// Hence, redefine a simple version here that just increments
// MEPC+4 then calls 'mret'. (ECALL is always 4-bytes in RV32)
virtual function void gen_ecall_handler(int hart);
string instr[$];
dump_perf_stats(instr);
gen_register_dump(instr);
instr = {instr,
$sformatf("csrr x%0d, 0x%0x", cfg.gpr[0], MEPC),
$sformatf("addi x%0d, x%0d, 4", cfg.gpr[0], cfg.gpr[0]),
$sformatf("csrw 0x%0x, x%0d", MEPC, cfg.gpr[0])
};
instr.push_back("mret");
gen_section(get_label("ecall_handler", hart), instr);
endfunction
virtual function void gen_program_header();
@ -87,31 +118,30 @@ class ibex_asm_program_gen extends riscv_asm_program_gen;
instr.push_back("csrwi 0x7c0, 1");
endfunction
// Generate "test_done" section.
// Re-define gen_test_done() to override the base-class with an empty implementation.
// Then, our own overrided gen_program() can append new test_done code.
virtual function void gen_test_done();
// empty
endfunction
// The test is ended from the UVM testbench, which awaits the following write to
// the special test-control signature address (signature_addr - 0x4).
// The ECALL trap handler will handle the clean up procedure while awaiting the
// simulation to be ended.
virtual function void gen_test_done();
// #FIXME# The existing ECALL trap handler will handle the clean up procedure
// while awaiting the simulation to be ended.
virtual function void gen_test_end(input test_result_t result,
ref string instr[$]);
bit [XLEN-1:0] test_control_addr;
string str;
string i = indent; // lint-hack
test_control_addr = cfg.signature_addr - 4'h4;
str = {str,
{format_string("test_done:", LABEL_STR_LEN), "\n"},
{i, "li gp, 1", "\n"}
};
if (cfg.bare_program_mode) begin
str = {str,
{i, "j write_tohost", "\n"}
};
str = {i, "j write_tohost", "\n"};
end else begin
// The testbench will await a write of TEST_PASS, and use that to end the test.
str = {str,
str = {
{i, $sformatf( "li x%0d, 0x%0h", cfg.gpr[1], test_control_addr), "\n"},
{i, $sformatf( "li x%0d, 0x%0h", cfg.gpr[0], TEST_PASS), "\n"},
{i, $sformatf( "li x%0d, 0x%0h", cfg.gpr[0], result), "\n"},
{i, $sformatf("slli x%0d, x%0d, 8", cfg.gpr[0], cfg.gpr[0]), "\n"},
{i, $sformatf("addi x%0d, x%0d, 0x%0h", cfg.gpr[0], cfg.gpr[0], TEST_RESULT), "\n"},
{i, $sformatf( "sw x%0d, 0(x%0d)", cfg.gpr[0], cfg.gpr[1]), "\n"},
@ -120,7 +150,7 @@ class ibex_asm_program_gen extends riscv_asm_program_gen;
};
end
instr_stream.push_back(str);
instr.push_back(str);
endfunction
endclass

View file

@ -0,0 +1,137 @@
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
class ibex_hardware_triggers_asm_program_gen extends ibex_asm_program_gen;
`uvm_object_utils(ibex_hardware_triggers_asm_program_gen)
`uvm_object_new
// Same implementation as the parent class, except substitute for our custom debug_rom class.
virtual function void gen_debug_rom(int hart);
`uvm_info(`gfn, "Creating debug ROM", UVM_LOW)
debug_rom = ibex_hardware_triggers_debug_rom_gen::
type_id::create("debug_rom", , {"uvm_test_top", ".", `gfn});
debug_rom.cfg = cfg;
debug_rom.hart = hart;
debug_rom.gen_program();
instr_stream = {instr_stream, debug_rom.instr_stream};
endfunction
endclass
class ibex_hardware_triggers_debug_rom_gen extends riscv_debug_rom_gen;
`uvm_object_utils(ibex_hardware_triggers_debug_rom_gen)
`uvm_object_new
int unsigned ibex_trigger_idx = 0; // See. [DbgHwBreakNum]
virtual function void gen_program();
string instr[$];
// Don't save off GPRs (ie. this WILL modify program flow)
// (We want to capture a register value (gpr[1]) from the directed_instr_streams in
// main() that contains the address for our next trigger.)
// This works in tandem with 'ibex_breakpoint_stream' which stores the address of
// the instruction to trigger on in a fixed register, then executes an EBREAK to
// enter debug mode via the dcsr.ebreakm=1 functionality. The debug rom code
// then sets up the breakpoint trigger to this address, and returns, allowing main
// to continue executing until we hit the trigger.
// riscv-debug-1.0.0-STABLE
// 5.5 Trigger Registers
// <..>
// As as result, a debugger can write any supported trigger as follows..
//
// 1. Write 0 to TDATA1. (This will result in TDATA1 containing a non-zero value,
// since the register is WARL).
// 2. Write desired values to TDATA2 and TDATA3.
// 3. Write desired value to TDATA1.
// <..>
instr = {// Check DCSR.cause (DCSR[8:6]) to branch to the next block of code.
$sformatf("csrr x%0d, 0x%0x", cfg.scratch_reg, DCSR),
$sformatf("slli x%0d, x%0d, 0x17", cfg.scratch_reg, cfg.scratch_reg),
$sformatf("srli x%0d, x%0d, 0x1d", cfg.scratch_reg, cfg.scratch_reg),
$sformatf("li x%0d, 0x1", cfg.gpr[0]), // EBREAK = 1
$sformatf("beq x%0d, x%0d, 1f", cfg.scratch_reg, cfg.gpr[0]),
$sformatf("li x%0d, 0x2", cfg.gpr[0]), // TRIGGER = 2
$sformatf("beq x%0d, x%0d, 2f", cfg.scratch_reg, cfg.gpr[0]),
$sformatf("li x%0d, 0x3", cfg.gpr[0]), // HALTREQ = 3
$sformatf("beq x%0d, x%0d, 3f", cfg.scratch_reg, cfg.gpr[0]),
// DCSR.cause == EBREAK
"1: nop",
// 'ibex_breakpoint_stream' inserts EBREAKs such that cfg.gpr[1]
// now contain the address of the next trigger.
// Enable the trigger and set to this address.
$sformatf("csrrwi zero, 0x%0x, %0d", TSELECT, ibex_trigger_idx),
$sformatf("csrrw zero, 0x%0x, x0", TDATA1),
$sformatf("csrrw zero, 0x%0x, x%0d", TDATA2, cfg.gpr[1]),
$sformatf("csrrwi zero, 0x%0x, 5", TDATA1),
// Increment the PC + 4 (EBREAK does not do this for you.)
$sformatf("csrr x%0d, 0x%0x", cfg.gpr[0], DPC),
$sformatf("addi x%0d, x%0d, 4", cfg.gpr[0], cfg.gpr[0]),
$sformatf("csrw 0x%0x, x%0d", DPC, cfg.gpr[0]),
"j 4f",
// DCSR.cause == TRIGGER
"2: nop",
// Disable the trigger until the next breakpoint is known.
$sformatf("csrrwi zero, 0x%0x, %0d", TSELECT, ibex_trigger_idx),
$sformatf("csrrw zero, 0x%0x, x0", TDATA1),
$sformatf("csrrw zero, 0x%0x, x0", TDATA2),
"j 4f",
// DCSR.cause == HALTREQ
"3: nop",
// Use this once near the start of the test to configure ebreakm/u to enter debug mode
// Set DCSR.ebreakm (DCSR[15]) = 1
// Set DCSR.ebreaku (DCSR[12]) = 1
$sformatf("li x%0d, 0x9000", cfg.scratch_reg),
$sformatf("csrs 0x%0x, x%0d", DCSR, cfg.scratch_reg),
"4: nop"
};
debug_main = {instr,
$sformatf("la x%0d, debug_end", cfg.scratch_reg),
$sformatf("jalr x0, x%0d, 0", cfg.scratch_reg)
};
format_section(debug_main);
gen_section($sformatf("%0sdebug_rom", hart_prefix(hart)), debug_main);
debug_end = {dret};
format_section(debug_end);
gen_section($sformatf("%0sdebug_end", hart_prefix(hart)), debug_end);
gen_debug_exception_handler();
endfunction
// If we get an exception in debug_mode, fail the test immediately.
// (something has gone wrong with our stimulus generation)
virtual function void gen_debug_exception_handler();
string instr[$];
instr = {$sformatf("la x%0d, test_fail", cfg.scratch_reg),
$sformatf("jalr x1, x%0d, 0", cfg.scratch_reg)};
format_section(instr);
gen_section($sformatf("%0sdebug_exception", hart_prefix(hart)), instr);
endfunction
endclass
class ibex_hardware_triggers_illegal_instr extends riscv_illegal_instr;
`uvm_object_utils(ibex_hardware_triggers_illegal_instr)
`uvm_object_new
// Make it super-obvious where the illegal instructions are in the assembly.
function void post_randomize();
super.post_randomize();
comment = "INVALID";
endfunction
endclass // ibex_illegal_instr

View file

@ -0,0 +1,62 @@
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
// Define a short riscv-dv directed instruction stream to setup the hardware breakpoint (TSELECT/TDATA) functionality, and then trigger on an instruction at the end of this stream.
class ibex_breakpoint_stream extends riscv_directed_instr_stream;
riscv_pseudo_instr la_instr;
riscv_instr ebreak_insn;
rand int unsigned num_of_instr;
constraint instr_c {
num_of_instr inside {[5:10]};
}
`uvm_object_utils(ibex_breakpoint_stream)
function new(string name = "");
super.new(name);
endfunction
function void post_randomize();
riscv_instr instr;
string trigger_label, gn;
// Setup a randomized main body of instructions.
initialize_instr_list(num_of_instr);
setup_allowed_instr(1, 1); // no_branch=1/no_load_store=1
foreach(instr_list[i]) begin
instr = riscv_instr::type_id::create("instr");
randomize_instr(instr);
instr_list[i] = instr;
// Copy this from the base-class behaviour
instr_list[i].atomic = 1'b1;
instr_list[i].has_label = 1'b0;
end
// Give the last insn of the main body a label, then set the breakpoint address to that label.
gn = get_name();
trigger_label = {gn};
instr_list[$].label = trigger_label;
instr_list[$].has_label = 1'b1;
// Load the address of the trigger point as the (last insn of the stream + 4)
// Store in gpr[1] ()
la_instr = riscv_pseudo_instr::type_id::create("la_instr");
la_instr.pseudo_instr_name = LA;
la_instr.imm_str = $sformatf("%0s+4", trigger_label);
la_instr.rd = cfg.gpr[1];
// Create the ebreak insn which will cause us to enter debug mode, and run the
// special code in the debugrom.
ebreak_insn = riscv_instr::get_instr(EBREAK);
// Add the instructions into the stream.
instr_list = {la_instr,
ebreak_insn,
instr_list};
endfunction
endclass

View file

@ -147,6 +147,38 @@
compare_final_value_only: 1
verbose: 1
- test: riscv_debug_triggers_test
description: >
Test stimulus includes directed routine which set the breakpoint addr forwards a few instructions.
This test has no pass/fail criteria, but instead is used to gather appropriate coverage.
// rtl_test:core_ibex_single_debug_pulse_test
// In combination with the test-program, this HALTREQ triggers code that then
// enables the DCSR.ebreakm/u functionality. From then on, EBREAK instructions trigger the
// DUT to run some debug-mode code to setup the next breakpoint address.
iterations: 5
gen_test: riscv_instr_base_test
gen_opts: >
+require_signature_addr=1
+gen_debug_section=1
+no_ebreak=1
+no_branch_jump=1
+instr_cnt=10000
+no_csr_instr=1
+illegal_instr_ratio=20
+no_fence=1
+num_of_sub_program=0
+randomize_csr=0
+directed_instr_0=ibex_breakpoint_stream,20
+uvm_set_type_override=ibex_asm_program_gen,ibex_hardware_triggers_asm_program_gen
+uvm_set_type_override=riscv_debug_rom_gen,ibex_hardware_triggers_debug_rom_gen
+uvm_set_type_override=riscv_illegal_instr,ibex_hardware_triggers_illegal_instr
rtl_test: core_ibex_single_debug_pulse_test
sim_opts: >
+require_signature_addr=1
+enable_debug_seq=1
+enable_bad_intg_on_uninit_access=0
timeout_s: 300
- test: riscv_debug_stress_test
description: >
Randomly assert debug_req_i more often, debug_rom is empty, with only a dret instruction

View file

@ -1 +1,3 @@
`include "ibex_asm_program_gen.sv"
`include "ibex_directed_instr_lib.sv"
`include "ibex_debug_triggers_overrides.sv"

View file

@ -9,6 +9,8 @@ class core_ibex_base_test extends uvm_test;
core_ibex_env env;
core_ibex_env_cfg cfg;
core_ibex_cosim_cfg cosim_cfg;
ibex_mem_intf_response_agent_cfg imem_cfg;
ibex_mem_intf_response_agent_cfg dmem_cfg;
virtual clk_rst_if clk_vif;
virtual core_ibex_dut_probe_if dut_vif;
virtual core_ibex_instr_monitor_if instr_vif;
@ -146,6 +148,17 @@ class core_ibex_base_test extends uvm_test;
uvm_config_db#(core_ibex_cosim_cfg)::set(null, "*cosim_agent*", "cosim_cfg", cosim_cfg);
imem_cfg = ibex_mem_intf_response_agent_cfg::type_id::create("imem_cfg", this);
dmem_cfg = ibex_mem_intf_response_agent_cfg::type_id::create("dmem_cfg", this);
// Never create bad integrity bits in response to accessing uninit memory
// on the Iside, as the Ibex can fetch speculatively.
imem_cfg.enable_bad_intg_on_uninit_access = 0;
// By default, enable bad_intg on the Dside (read plusarg to overwrite this behaviour)
dmem_cfg.enable_bad_intg_on_uninit_access = 1;
void'($value$plusargs("enable_bad_intg_on_uninit_access=%0d", dmem_cfg.enable_bad_intg_on_uninit_access));
uvm_config_db#(ibex_mem_intf_response_agent_cfg)::set(this, "*instr_if_response_agent*", "cfg", imem_cfg);
uvm_config_db#(ibex_mem_intf_response_agent_cfg)::set(this, "*data_if_response_agent*", "cfg", dmem_cfg);
uvm_config_db#(core_ibex_env_cfg)::set(this, "*", "cfg", cfg);
mem = mem_model_pkg::mem_model#()::type_id::create("mem");
// Create virtual sequence and assign memory handle

View file

@ -1162,6 +1162,21 @@ class core_ibex_debug_single_step_test extends core_ibex_directed_test;
endclass
class core_ibex_single_debug_pulse_test extends core_ibex_directed_test;
`uvm_component_utils(core_ibex_single_debug_pulse_test)
`uvm_component_new
virtual task check_stimulus();
vseq.debug_seq_single_h.max_interval = 0;
// Start as soon as device is initialized.
vseq.start_debug_single_seq();
wait (test_done === 1'b1);
endtask
endclass
// Memory interface error test class
class core_ibex_mem_error_test extends core_ibex_directed_test;