From f475e402f9151f8b1bd8ff6539e3962b15d667ab Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Sat, 5 Sep 2026 13:03:00 -0700 Subject: [PATCH 1/6] [rtl] Do not count a dummy instruction as a step do_single_step_d looks only at instr_valid_i, which dummy instructions assert like real ones. A dummy that reaches ID right after DRET completes the step: the core re-enters debug mode having retired nothing, dpc is unchanged and the stepped instruction never runs. Pass dummy_instr_id to the controller and let a dummy neither arm nor clear the step; it still executes. SingleStepStableAcrossDummy checks the controller. StepDebugEntryHasArchitecturalProgress checks that a step debug entry follows exactly one instruction that completed or trapped. Signed-off-by: Kulan Palanichamy --- rtl/ibex_controller.sv | 5 ++++- rtl/ibex_core.sv | 39 +++++++++++++++++++++++++++++++++++++++ rtl/ibex_id_stage.sv | 2 ++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/rtl/ibex_controller.sv b/rtl/ibex_controller.sv index 4f29557c9c..b9bd92349c 100644 --- a/rtl/ibex_controller.sv +++ b/rtl/ibex_controller.sv @@ -36,6 +36,7 @@ module ibex_controller import ibex_pkg::*; #( // instr from IF-ID pipeline stage input logic instr_valid_i, // instr is valid + input logic dummy_instr_id_i, // instr in ID is a dummy input logic [31:0] instr_i, // uncompressed instr data for mtval input logic [15:0] instr_compressed_i, // instr compressed data for mtval input logic instr_is_compressed_i, // instr is compressed @@ -459,7 +460,9 @@ module ibex_controller import ibex_pkg::*; #( // single step mode. The first valid instruction on debug mode entry will clear it. Hold its value // when there is no valid instruction so `do_single_step_d` remains asserted until debug mode is // entered. - assign do_single_step_d = instr_valid_i ? ~debug_mode_q & debug_single_step_i : do_single_step_q; + // A dummy instruction is valid in ID but must not complete a step, so it holds the value. + assign do_single_step_d = (instr_valid_i & ~dummy_instr_id_i) ? + ~debug_mode_q & debug_single_step_i : do_single_step_q; // Enter debug mode due to: // * external `debug_req_i` // * core in single step mode (dcsr.step == 1). diff --git a/rtl/ibex_core.sv b/rtl/ibex_core.sv index 68b4493505..91ab2560e0 100644 --- a/rtl/ibex_core.sv +++ b/rtl/ibex_core.sv @@ -682,6 +682,7 @@ module ibex_core import ibex_pkg::*; import ibex_cheriot_pkg::*; #( // from/to IF-ID pipeline register .instr_valid_i (instr_valid_id), + .dummy_instr_id_i (dummy_instr_id), .instr_rdata_i (instr_rdata_id), .instr_rdata_alu_i (instr_rdata_alu_id), .instr_rdata_c_i (instr_rdata_c_id), @@ -2431,6 +2432,36 @@ module ibex_core import ibex_pkg::*; import ibex_cheriot_pkg::*; #( end end + // A step debug entry must follow exactly one instruction that completed or trapped, as seen + // on RVFI (which has no dummies). A Zcmp sequence counts once, at its last uop or a trap. DRET is + // left out by using the debug mode recorded with its RVFI item. The count saturates, so no wrap. + logic [1:0] step_rvfi_cnt_q; + logic step_arch_retire; + + assign step_arch_retire = rvfi_valid && !rvfi_ext_debug_mode && + (rvfi_trap || !rvfi_ext_expanded_insn_valid || + rvfi_ext_expanded_insn_last); + + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) begin + step_rvfi_cnt_q <= '0; + end else if (debug_mode) begin + step_rvfi_cnt_q <= '0; + end else if (step_arch_retire && !(&step_rvfi_cnt_q)) begin + step_rvfi_cnt_q <= step_rvfi_cnt_q + 2'd1; + end + end + + `COVER(StepCountedTrappedNonFinalUop, + rvfi_valid && rvfi_trap && rvfi_ext_expanded_insn_valid && + !rvfi_ext_expanded_insn_last && debug_single_step) + + // Checked when dcsr.cause is saved: the debug entry request comes before the stepped + // instruction's RVFI item. + `ASSERT(StepDebugEntryHasArchitecturalProgress, + debug_mode_entering && debug_csr_save && (debug_cause == DBG_CAUSE_STEP) + |=> (step_rvfi_cnt_q == 2'd1)) + `else logic unused_instr_new_id, unused_instr_id_done, unused_instr_done_wb, unused_instr_expanded_id, unused_instr_gets_expanded_id; @@ -2449,6 +2480,14 @@ module ibex_core import ibex_pkg::*; import ibex_cheriot_pkg::*; #( // If the ID stage signals its ready the mult/div FSMs must be idle in the following cycle // `ASSERT(MultDivFSMIdleOnIdReady, id_in_ready |=> ex_block_i.sva_multdiv_fsm_idle) + // A dummy instruction must neither arm nor complete a single step. + `ASSERT(SingleStepStableAcrossDummy, + instr_valid_id & dummy_instr_id |=> $stable(id_stage_i.controller_i.do_single_step_q)) + `COVER(SingleStepDummyWhileDisarmed, + instr_valid_id & dummy_instr_id & ~id_stage_i.controller_i.do_single_step_q) + `COVER(SingleStepDummyWhileArmed, + instr_valid_id & dummy_instr_id & id_stage_i.controller_i.do_single_step_q) + ////////// // FCOV // ////////// diff --git a/rtl/ibex_id_stage.sv b/rtl/ibex_id_stage.sv index a84d9df2be..e685bfe97a 100644 --- a/rtl/ibex_id_stage.sv +++ b/rtl/ibex_id_stage.sv @@ -38,6 +38,7 @@ module ibex_id_stage import ibex_cheriot_pkg::*; #( // Interface to IF stage input logic instr_valid_i, + input logic dummy_instr_id_i, // instr in ID is a dummy input logic [31:0] instr_rdata_i, // from IF-ID pipeline registers input logic [31:0] instr_rdata_alu_i, // from IF-ID pipeline registers input logic [15:0] instr_rdata_c_i, // from IF-ID pipeline registers @@ -631,6 +632,7 @@ module ibex_id_stage import ibex_cheriot_pkg::*; #( // from IF-ID pipeline .instr_valid_i (instr_valid_i), + .dummy_instr_id_i (dummy_instr_id_i), .instr_i (instr_rdata_i), .instr_compressed_i (instr_rdata_c_i), .instr_is_compressed_i (instr_is_compressed_i), From f1c3c68dc10652d57b74f96a5572ef26f0829084 Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Sat, 15 Aug 2026 11:30:27 -0700 Subject: [PATCH 2/6] [rtl] Exclude dummy instructions from minstret Dummy instructions must have no architectural effect, but WB counts them as retired, so they advance minstret: instr_perf_count_id_i does not exclude them. Mask them with dummy_instr_wb_o, the marker for the instruction in WB: in both WritebackStage variants, and in the speculative count that a minstret read uses. NoMinstretForDummyInstr and NoSpecMinstretForDummyInstr check it. Tested with dummy_instr_minstret_test on opentitan and on a local copy with WritebackStage = 0. No shipped configuration has dummy instructions without a WB stage. Signed-off-by: Kulan Palanichamy --- rtl/ibex_wb_stage.sv | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rtl/ibex_wb_stage.sv b/rtl/ibex_wb_stage.sv index 4379ee0334..02add4fdba 100644 --- a/rtl/ibex_wb_stage.sv +++ b/rtl/ibex_wb_stage.sv @@ -203,9 +203,11 @@ module ibex_wb_stage import ibex_cheriot_pkg::*; #( // Speculative versions of the signals do not factor in exceptions and whether the instruction // is done yet. These are used to get correct values for instructions reading the relevant // performance counters in the ID stage. - assign perf_instr_ret_wb_spec_o = wb_count_q & wb_valid_q; + // Dummy instructions do not count as retired; mask them with the marker for the WB instruction. + assign perf_instr_ret_wb_spec_o = wb_count_q & wb_valid_q & ~dummy_instr_wb_o; assign perf_instr_ret_compressed_wb_spec_o = perf_instr_ret_wb_spec_o & wb_compressed_q; assign perf_instr_ret_wb_o = instr_done_wb_o & wb_count_q & + ~dummy_instr_wb_o & ~(lsu_resp_valid_i & lsu_resp_err_i); assign perf_instr_ret_compressed_wb_o = perf_instr_ret_wb_o & wb_compressed_q; @@ -261,7 +263,9 @@ module ibex_wb_stage import ibex_cheriot_pkg::*; #( // values will be correct. assign perf_instr_ret_wb_spec_o = 1'b0; assign perf_instr_ret_compressed_wb_spec_o = 1'b0; + // Dummy instructions do not count as retired. assign perf_instr_ret_wb_o = instr_perf_count_id_i & en_wb_i & + ~dummy_instr_wb_o & ~(lsu_resp_valid_i & lsu_resp_err_i); assign perf_instr_ret_compressed_wb_o = perf_instr_ret_wb_o & instr_is_compressed_id_i; @@ -308,4 +312,15 @@ module ibex_wb_stage import ibex_cheriot_pkg::*; #( `DV_FCOV_SIGNAL_GEN_IF(logic, wb_valid, g_writeback_stage.wb_valid_q, WritebackStage) `ASSERT(RFWriteFromOneSourceOnly, $onehot0(rf_wdata_wb_mux_we)) + + // A dummy instruction must not advance minstret, through either retire indication. + `ASSERT(NoMinstretForDummyInstr, dummy_instr_wb_o |-> ~perf_instr_ret_wb_o) + `ASSERT(NoSpecMinstretForDummyInstr, dummy_instr_wb_o |-> ~perf_instr_ret_wb_spec_o) + + // A dummy actually in WB (the marker can outlive the entry). + if (WritebackStage) begin : g_dummy_cover_wb + `COVER(DummyInstrLiveInWb, dummy_instr_wb_o & g_writeback_stage.wb_valid_q) + end else begin : g_dummy_cover_bypass + `COVER(DummyInstrLiveInWb, dummy_instr_wb_o & en_wb_i) + end endmodule From 3b90ed421b31917a74eb6b4014dfdbc2fb2f4020 Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Wed, 14 Oct 2026 09:50:56 -0700 Subject: [PATCH 3/6] [rtl] Keep dummy instructions out of Zcmp sequences A dummy inserted during a Zcmp expansion takes the current micro-op's slot, and the expander drops that micro-op. The dummy also reaches ID as a plain instruction, so debug entry or an interrupt can split the sequence, and the restart repeats cm.popret's sp update. Hold insertion off while the next instruction is a Zcmp micro-op, and freeze the dummy counter meanwhile. Assertions check that debug entry and interrupts never split a sequence where the controller defers them. Signed-off-by: Kulan Palanichamy --- rtl/ibex_dummy_instr.sv | 8 +++++--- rtl/ibex_if_stage.sv | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/rtl/ibex_dummy_instr.sv b/rtl/ibex_dummy_instr.sv index 8951eb191e..2dedc38b54 100644 --- a/rtl/ibex_dummy_instr.sv +++ b/rtl/ibex_dummy_instr.sv @@ -26,6 +26,7 @@ module ibex_dummy_instr import ibex_pkg::*; #( // Interface to IF stage input logic fetch_valid_i, input logic id_in_ready_i, + input logic insert_hold_i, output logic insert_dummy_instr_o, output logic [31:0] dummy_instr_data_o ); @@ -99,8 +100,8 @@ module ibex_dummy_instr import ibex_pkg::*; #( // Clear the counter every time a new instruction is inserted assign dummy_cnt_d = insert_dummy_instr ? '0 : dummy_cnt_incr; // Increment the counter for each executed instruction while dummy instructions are - // enabled. - assign dummy_cnt_en = dummy_instr_en_i & id_in_ready_i & + // enabled. Freeze it while insertion is held, so a held dummy is delayed rather than skipped. + assign dummy_cnt_en = dummy_instr_en_i & id_in_ready_i & ~insert_hold_i & (fetch_valid_i | insert_dummy_instr); always_ff @(posedge clk_i or negedge rst_ni) begin @@ -112,7 +113,8 @@ module ibex_dummy_instr import ibex_pkg::*; #( end // Insert a dummy instruction each time the counter hits the threshold - assign insert_dummy_instr = dummy_instr_en_i & (dummy_cnt_q == dummy_cnt_threshold); + assign insert_dummy_instr = dummy_instr_en_i & ~insert_hold_i & + (dummy_cnt_q == dummy_cnt_threshold); // Encode instruction always_comb begin diff --git a/rtl/ibex_if_stage.sv b/rtl/ibex_if_stage.sv index ab2dc912d6..cc0c7e0af1 100644 --- a/rtl/ibex_if_stage.sv +++ b/rtl/ibex_if_stage.sv @@ -500,12 +500,28 @@ module ibex_if_stage import ibex_pkg::*; import ibex_cheriot_pkg::*; #( .illegal_instr_o (illegal_c_insn) ); + // Debug entry must not split a Zcmp sequence, nor an interrupt its committing micro-ops: the + // handler returns to the start of the instruction. + `ASSERT(NoDebugEntryInZcmpSequence, + pc_set_i && (pc_mux_i == PC_EXC) && (exc_pc_mux_i == EXC_PC_DBD) + |-> compressed_decoder_i.cm_state_q == compressed_decoder_i.CmIdle) + `ASSERT(NoIrqInZcmpCommit, + pc_set_i && (pc_mux_i == PC_EXC) && (exc_pc_mux_i == EXC_PC_IRQ) + |-> !(compressed_decoder_i.cm_state_q inside {compressed_decoder_i.CmPopZeroA0, + compressed_decoder_i.CmPopRetRa, + compressed_decoder_i.CmMvSecondReg})) + // Dummy instruction insertion if (DummyInstructions) begin : gen_dummy_instr // SEC_CM: CTRL_FLOW.UNPREDICTABLE logic insert_dummy_instr; + logic dummy_instr_hold; logic [31:0] dummy_instr_data; + // Hold dummies back while the next instruction is a Zcmp micro-op. The controller sees a dummy + // as a separate instruction, so one inside a sequence would let debug entry or an interrupt in. + assign dummy_instr_hold = instr_gets_expanded != INSTR_NOT_EXPANDED; + ibex_dummy_instr #( .RndCnstLfsrSeed (RndCnstLfsrSeed), .RndCnstLfsrPerm (RndCnstLfsrPerm) @@ -518,6 +534,7 @@ module ibex_if_stage import ibex_pkg::*; import ibex_cheriot_pkg::*; #( .dummy_instr_seed_i (dummy_instr_seed_i), .fetch_valid_i (fetch_valid), .id_in_ready_i (id_in_ready_i), + .insert_hold_i (dummy_instr_hold), .insert_dummy_instr_o (insert_dummy_instr), .dummy_instr_data_o (dummy_instr_data) ); @@ -534,6 +551,28 @@ module ibex_if_stage import ibex_pkg::*; import ibex_cheriot_pkg::*; #( // PC of the dummy instruction will match whatever is next from the prefetch buffer. assign stall_dummy_instr = insert_dummy_instr; + `ASSERT(NoDummyInZcmpSequence, + insert_dummy_instr |-> (instr_gets_expanded == INSTR_NOT_EXPANDED) && + (compressed_decoder_i.cm_state_q == compressed_decoder_i.CmIdle)) + +`ifdef INC_ASSERT + // A dummy was due, and held back, before each kind of Zcmp micro-op. + logic dummy_held; + logic [2:0] cm_state; + assign dummy_held = dummy_instr_hold & dummy_instr_en_i & + (dummy_instr_i.dummy_cnt_q == dummy_instr_i.dummy_cnt_threshold); + assign cm_state = compressed_decoder_i.cm_state_q; + + `COVER(DummyHeldCmIdle_C, dummy_held && cm_state == compressed_decoder_i.CmIdle) + `COVER(DummyHeldCmPushStoreReg_C, dummy_held && cm_state == compressed_decoder_i.CmPushStoreReg) + `COVER(DummyHeldCmPushDecrSp_C, dummy_held && cm_state == compressed_decoder_i.CmPushDecrSp) + `COVER(DummyHeldCmPopLoadReg_C, dummy_held && cm_state == compressed_decoder_i.CmPopLoadReg) + `COVER(DummyHeldCmPopIncrSp_C, dummy_held && cm_state == compressed_decoder_i.CmPopIncrSp) + `COVER(DummyHeldCmPopZeroA0_C, dummy_held && cm_state == compressed_decoder_i.CmPopZeroA0) + `COVER(DummyHeldCmPopRetRa_C, dummy_held && cm_state == compressed_decoder_i.CmPopRetRa) + `COVER(DummyHeldCmMvSecondReg_C, dummy_held && cm_state == compressed_decoder_i.CmMvSecondReg) +`endif + // Register the dummy instruction indication into the ID stage always_ff @(posedge clk_i or negedge rst_ni) begin if (!rst_ni) begin From b1ac48f3e4268dac82cfd38af218caa333e96228 Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Sat, 5 Sep 2026 13:05:16 -0700 Subject: [PATCH 4/6] [dv] Add a single-step test with dummies armed +toggle_dummy_instr only lets the random cpuctrlsts stream enable dummy instructions, about half the time. Add +force_dummy_instr, which always enables them at the highest rate. riscv_debug_single_step_dummy_test single steps a random program with it. Random CSR writes skip cpuctrlsts, so only the stream changes the dummy settings. The program has no ebreak, so every debug entry is a request or a step. Signed-off-by: Kulan Palanichamy --- .../ibex_directed_instr_lib.sv | 16 +++++++- .../riscv_dv_extension/testlist.yaml | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/dv/uvm/core_ibex/riscv_dv_extension/ibex_directed_instr_lib.sv b/dv/uvm/core_ibex/riscv_dv_extension/ibex_directed_instr_lib.sv index 8dd96e18a4..ef7cf68b75 100644 --- a/dv/uvm/core_ibex/riscv_dv_extension/ibex_directed_instr_lib.sv +++ b/dv/uvm/core_ibex/riscv_dv_extension/ibex_directed_instr_lib.sv @@ -100,6 +100,7 @@ class ibex_rand_cpuctrlsts_stream extends riscv_directed_instr_stream; riscv_instr instrs[4]; bit toggle_dit; bit toggle_dummy_instr; + bit force_dummy_instr; bit toggle_icache; bit icache_en; bit dit_en; @@ -122,6 +123,15 @@ class ibex_rand_cpuctrlsts_stream extends riscv_directed_instr_stream; toggle_dummy_instr = 1'b0; end + // +force_dummy_instr enables dummy instructions at the highest rate (mask 0) in every stream; + // +toggle_dummy_instr only allows them. + if (!$value$plusargs("force_dummy_instr=%d", force_dummy_instr)) begin + force_dummy_instr = 1'b0; + end + if (force_dummy_instr) begin + toggle_dummy_instr = 1'b1; + end + if (!$value$plusargs("toggle_icache=%d", toggle_icache)) begin toggle_icache = 1'b0; end @@ -129,9 +139,11 @@ class ibex_rand_cpuctrlsts_stream extends riscv_directed_instr_stream; `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(icache_en, if (!toggle_icache) icache_en == 0;); `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(dit_en, if (!toggle_dit) dit_en == 0;); `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(dummy_instr_en, - if (!toggle_dummy_instr) dummy_instr_en == 0;); + if (!toggle_dummy_instr) dummy_instr_en == 0; + if (force_dummy_instr) dummy_instr_en == 1;); `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(dummy_instr_mask, - if (!toggle_dummy_instr) dummy_instr_mask == 0;); + if (!toggle_dummy_instr) dummy_instr_mask == 0; + if (force_dummy_instr) dummy_instr_mask == 0;); cpuctrlsts_mask = {3'b111, {4{!dummy_instr_en}}, !dit_en, !icache_en}; cpuctrlsts_val = {dummy_instr_mask, dummy_instr_en, dit_en, icache_en}; diff --git a/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml b/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml index b5466c8b72..388198aeb0 100644 --- a/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml +++ b/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml @@ -671,6 +671,45 @@ +enable_debug_seq=1 +enable_bad_intg_on_uninit_access=0 +- test: riscv_debug_single_step_dummy_test + description: > + Single step through a random program whose cpuctrlsts stream enables dummy instructions at + the highest insertion rate. Random CSR accesses may trap. After boot setup only the stream + writes cpuctrlsts, though a random branch into it can write a stale value. Every debug entry + is a debug request or a completed step. + iterations: 20 + gen_test: riscv_instr_base_test + gen_opts: > + +require_signature_addr=1 + +gen_debug_section=1 + +no_dret=1 + +no_ebreak=1 + +no_ecall=1 + +no_branch_jump=0 + +instr_cnt=2000 + +no_csr_instr=0 + +randomize_csr=1 + +gen_all_csrs_by_default=1 + +add_csr_write=0x7c1 + +remove_csr_write=0x7c0 + +no_fence=0 + +no_wfi=1 + +num_of_sub_program=0 + +enable_debug_single_step=1 + +illegal_instr_ratio=0 + +hint_instr_ratio=1 + +directed_instr_0=ibex_rand_cpuctrlsts_stream,8 + +force_dummy_instr=1 + +boot_mode=m + rtl_test: core_ibex_debug_single_step_test + rtl_params: + SecureIbex: 1 + sim_opts: > + +require_signature_addr=1 + +max_interval=1500 + +enable_debug_seq=1 + +enable_bad_intg_on_uninit_access=0 + - test: riscv_reset_test description: > Randomly reset the core once in the middle of program execution From 13e21b743871439313eab0d8bccdc4e7f3e878cd Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Sun, 6 Sep 2026 09:11:27 -0700 Subject: [PATCH 5/6] [dv] Add dummy-instruction directed tests dummy_instr_minstret_test checks that minstret advances the same over a fixed block with dummies off, on and off again. dummy_instr_zcmp_test checks the frames, registers and returns of cm.push, cm.pop, cm.mv* and cm.popret* with dummies at the highest rate. Its debug and interrupt variants add random requests from the new core_ibex_directed_stress_test. The Zcmp tests set +disable_cosim=1 because the cosim does not enable Zcmp. Signed-off-by: Kulan Palanichamy --- .../directed_tests/directed_testlist.yaml | 52 ++++ .../dummy_instr_minstret_test.S | 63 +++++ .../dummy_instr_test/dummy_instr_zcmp_test.S | 252 ++++++++++++++++++ .../core_ibex/directed_tests/gen_testlist.py | 52 ++++ dv/uvm/core_ibex/tests/core_ibex_test_lib.sv | 28 ++ 5 files changed, 447 insertions(+) create mode 100644 dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_minstret_test.S create mode 100644 dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_zcmp_test.S diff --git a/dv/uvm/core_ibex/directed_tests/directed_testlist.yaml b/dv/uvm/core_ibex/directed_tests/directed_testlist.yaml index 597d48b64d..5ece005a9f 100644 --- a/dv/uvm/core_ibex/directed_tests/directed_testlist.yaml +++ b/dv/uvm/core_ibex/directed_tests/directed_testlist.yaml @@ -90,6 +90,58 @@ RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] sim_opts: +disable_cosim=1 +- test: dummy_instr_minstret_test + desc: > + Dummy instructions must not be counted in minstret: the minstret delta + over a fixed block is the same with dummies off, on and off again. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_minstret_test.S + config: riscv-tests + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + +- test: dummy_instr_zcmp_test + desc: > + Dummy instructions must not disturb an expanded Zcmp sequence. Cosim + mismatches are not fatal because the cosim does not enable Zcmp. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 + +- test: dummy_instr_zcmp_debug_test + desc: > + dummy_instr_zcmp_test under random debug requests. Debug entry must + not split a Zcmp sequence. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_test: core_ibex_directed_stress_test + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 +enable_debug_seq=1 +max_interval=500 + +- test: dummy_instr_zcmp_irq_test + desc: > + dummy_instr_zcmp_test under random interrupts. An interrupt must not + split the committing micro-ops of a Zcmp sequence. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_test: core_ibex_directed_stress_test + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 +enable_irq_single_seq=1 + - test: pmp_mseccfg_test_rlb1_l0_0_u0 desc: > mseccfg test diff --git a/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_minstret_test.S b/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_minstret_test.S new file mode 100644 index 0000000000..ce23fc718a --- /dev/null +++ b/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_minstret_test.S @@ -0,0 +1,63 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Dummy instructions must not be counted in minstret. +# +# minstret is read around a csrr and eight nops, so the two reads differ by +# nine, with dummy instructions off, on at the highest rate (mask 0) and off +# again. The cosim checks the read values too. + +#include "riscv_test.h" +#include "test_macros.h" + +# cpuctrlsts (0x7c0): bit 2 dummy_instr_en, bits 5:3 dummy_instr_mask. +#define CPUCTRLSTS 0x7c0 +#define CPUCTRLSTS_DUMMY_INSTR_EN 0x4 + +.macro CHECK_MINSTRET_DELTA idx + li gp, \idx + csrr t0, minstret + nop + nop + nop + nop + nop + nop + nop + nop + csrr t1, minstret + sub t2, t1, t0 + li t3, 9 + bne t2, t3, fail +.endm + +RVTEST_RV32M +RVTEST_CODE_BEGIN + + # Baseline with dummy instructions disabled. + CHECK_MINSTRET_DELTA 1 + + # Enable dummy instruction insertion at the highest rate and repeat the + # measurement; the insertion points walk across the window. + csrsi CPUCTRLSTS, CPUCTRLSTS_DUMMY_INSTR_EN + li t4, 32 +1: + CHECK_MINSTRET_DELTA 2 + addi t4, t4, -1 + bnez t4, 1b + + # Disable again and re-check the baseline. + csrci CPUCTRLSTS, CPUCTRLSTS_DUMMY_INSTR_EN + CHECK_MINSTRET_DELTA 3 + + j pass + + TEST_PASSFAIL + +RVTEST_CODE_END + + .data +RVTEST_DATA_BEGIN + TEST_DATA +RVTEST_DATA_END diff --git a/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_zcmp_test.S b/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_zcmp_test.S new file mode 100644 index 0000000000..9fe4137ee9 --- /dev/null +++ b/dv/uvm/core_ibex/directed_tests/dummy_instr_test/dummy_instr_zcmp_test.S @@ -0,0 +1,252 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Dummy instructions must not disturb an expanded Zcmp sequence. +# +# A round runs cm.push/cm.pop with the full register list, cm.mvsa01, +# cm.mva01s, and cm.popret/cm.popretz through a call, and checks the frame, +# sp, the registers and the returns. It runs once with dummy instructions off, +# then 48 times with them at the highest rate (mask 0), so a dummy falls due +# before every kind of uop and has to wait. A lost uop leaves a canary, a stale +# register, a wrong sp or a fall-through past the return. +# +# dummy_instr_zcmp_debug_test and dummy_instr_zcmp_irq_test run this program +# under random debug requests or interrupts, whose handlers only return. A +# restarted cm.popret leaves a wrong sp or returns to a bad address. +# +# The encodings are raw halfwords because the pinned toolchain has no Zcmp. +# Cosim mismatches are not fatal: the cosim does not enable Zcmp. + +#include "riscv_test.h" +#include "test_macros.h" + +# cpuctrlsts (0x7c0): bit 2 dummy_instr_en, bits 5:3 dummy_instr_mask. +#define CPUCTRLSTS 0x7c0 +#define CPUCTRLSTS_DUMMY_INSTR_EN 0x4 + +#define CANARY 0xdeadbeef + +# mie: fast interrupts 0-14, external, timer and software. +#define MIE_ALL 0x7fff0888 + +# Register use: +# gp (TESTNUM) index of the round in progress, for the failure trace +# t4 round counter for the loop with dummies enabled +# t0, t1 scratch inside a round +# ra and s0-s11 are the pushed/popped registers, a0/a1 and s0-s3 the moved ones. + +# The frame of cm.push {ra, s0-s11}, -64 is 64 bytes. The registers are stored +# from the top of the frame downwards in the order s11, s10 ... s0, ra (Zc spec +# 1.0, cm.push): s11 at 60(sp), s10 at 56(sp) ... s0 at 16(sp), ra at 12(sp). +# 0(sp)..8(sp) is padding the sequence must not write. +.macro CHECK_SLOT offset, reg + lw t0, \offset(sp) + bne t0, \reg, fail +.endm + +.macro CHECK_CANARY offset + lw t0, \offset(sp) + li t1, CANARY + bne t0, t1, fail +.endm + +.macro CHECK_REG reg, value + li t0, \value + bne t0, \reg, fail +.endm + +.macro ZCMP_ROUND idx + li gp, \idx + + # Fill the frame with a canary and give every pushed register a distinct value. + la sp, stack_top + li t0, CANARY + sw t0, -4(sp) + sw t0, -8(sp) + sw t0, -12(sp) + sw t0, -16(sp) + sw t0, -20(sp) + sw t0, -24(sp) + sw t0, -28(sp) + sw t0, -32(sp) + sw t0, -36(sp) + sw t0, -40(sp) + sw t0, -44(sp) + sw t0, -48(sp) + sw t0, -52(sp) + sw t0, -56(sp) + sw t0, -60(sp) + sw t0, -64(sp) + li ra, 0x1a000000 + \idx + li s0, 0x50000000 + \idx + li s1, 0x51000000 + \idx + li s2, 0x52000000 + \idx + li s3, 0x53000000 + \idx + li s4, 0x54000000 + \idx + li s5, 0x55000000 + \idx + li s6, 0x56000000 + \idx + li s7, 0x57000000 + \idx + li s8, 0x58000000 + \idx + li s9, 0x59000000 + \idx + li s10, 0x5a000000 + \idx + li s11, 0x5b000000 + \idx + + .2byte 0xb8f2 # cm.push {ra, s0-s11}, -64 + + la t0, stack_top + addi t0, t0, -64 + bne t0, sp, fail + CHECK_SLOT 60, s11 + CHECK_SLOT 56, s10 + CHECK_SLOT 52, s9 + CHECK_SLOT 48, s8 + CHECK_SLOT 44, s7 + CHECK_SLOT 40, s6 + CHECK_SLOT 36, s5 + CHECK_SLOT 32, s4 + CHECK_SLOT 28, s3 + CHECK_SLOT 24, s2 + CHECK_SLOT 20, s1 + CHECK_SLOT 16, s0 + CHECK_SLOT 12, ra + CHECK_CANARY 8 + CHECK_CANARY 4 + CHECK_CANARY 0 + + # Clobber everything the pop must restore. + li ra, 0 + li s0, 0 + li s1, 0 + li s2, 0 + li s3, 0 + li s4, 0 + li s5, 0 + li s6, 0 + li s7, 0 + li s8, 0 + li s9, 0 + li s10, 0 + li s11, 0 + + .2byte 0xbaf2 # cm.pop {ra, s0-s11}, 64 + + la t0, stack_top + bne t0, sp, fail + CHECK_REG ra, 0x1a000000 + \idx + CHECK_REG s0, 0x50000000 + \idx + CHECK_REG s1, 0x51000000 + \idx + CHECK_REG s2, 0x52000000 + \idx + CHECK_REG s3, 0x53000000 + \idx + CHECK_REG s4, 0x54000000 + \idx + CHECK_REG s5, 0x55000000 + \idx + CHECK_REG s6, 0x56000000 + \idx + CHECK_REG s7, 0x57000000 + \idx + CHECK_REG s8, 0x58000000 + \idx + CHECK_REG s9, 0x59000000 + \idx + CHECK_REG s10, 0x5a000000 + \idx + CHECK_REG s11, 0x5b000000 + \idx + + # cm.mvsa01 s0, s1: s0 = a0, s1 = a1. + li a0, 0xa0a0a0a0 + li a1, 0xa1a1a1a1 + li s0, 0 + li s1, 0 + .2byte 0xac26 # cm.mvsa01 s0, s1 + bne s0, a0, fail + bne s1, a1, fail + + # cm.mva01s s2, s3: a0 = s2, a1 = s3. + li s2, 0x52525252 + li s3, 0x53535353 + li a0, 0 + li a1, 0 + .2byte 0xad6e # cm.mva01s s2, s3 + bne a0, s2, fail + bne a1, s3, fail + + # cm.popret and cm.popretz through a call: the return must happen, a0 must + # carry the callee's value (or zero), and sp must be back where it started. + li a0, 0 + jal ra, popret_sub + li t0, 0x55 + bne t0, a0, fail + la t0, stack_top + bne t0, sp, fail + + li a0, 0 + jal ra, popretz_sub + bnez a0, fail + la t0, stack_top + bne t0, sp, fail +.endm + +# Debug entry: DmHaltAddr is the start of .text.init. Resume at once. The +# reset code of RVTEST_CODE_BEGIN starts at offset 0x80. + .section .text.init + dret + +RVTEST_RV32M +RVTEST_CODE_BEGIN + + la t0, irq_vector + csrw mtvec, t0 + li t0, MIE_ALL + csrw mie, t0 + csrsi mstatus, MSTATUS_MIE + + # Baseline with dummy instructions disabled. + ZCMP_ROUND 1 + + # Enable dummy instruction insertion at the highest rate and repeat. + csrsi CPUCTRLSTS, CPUCTRLSTS_DUMMY_INSTR_EN + li t4, 48 +1: + ZCMP_ROUND 2 + addi t4, t4, -1 + bnez t4, 1b + + csrci CPUCTRLSTS, CPUCTRLSTS_DUMMY_INSTR_EN + j pass + + TEST_PASSFAIL + + # The jump after each cm.popret/cm.popretz is only reached if the return + # micro-op was dropped. + .balign 4 +popret_sub: + .2byte 0xb842 # cm.push {ra}, -16 + li a0, 0x55 + .2byte 0xbe42 # cm.popret {ra}, 16 + j fail + + .balign 4 +popretz_sub: + .2byte 0xb842 # cm.push {ra}, -16 + li a0, 0x55 + .2byte 0xbc42 # cm.popretz {ra}, 16 + j fail + + # Vectored mtvec: exceptions (none are expected) at the base, interrupt n at + # base + 4n. The testbench drops each interrupt line on its own. + .balign 256 +irq_vector: + .option push + .option norvc + j fail + .rept 31 + mret + .endr + .option pop + +RVTEST_CODE_END + + .data +RVTEST_DATA_BEGIN + TEST_DATA + .balign 16 +stack_bot: + .fill 32, 4, 0 +stack_top: + .fill 4, 4, 0 +RVTEST_DATA_END diff --git a/dv/uvm/core_ibex/directed_tests/gen_testlist.py b/dv/uvm/core_ibex/directed_tests/gen_testlist.py index a328b1fcd8..13c8b10787 100644 --- a/dv/uvm/core_ibex/directed_tests/gen_testlist.py +++ b/dv/uvm/core_ibex/directed_tests/gen_testlist.py @@ -110,6 +110,58 @@ def add_configs_and_handwritten_directed_tests(): RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] sim_opts: +disable_cosim=1 +- test: dummy_instr_minstret_test + desc: > + Dummy instructions must not be counted in minstret: the minstret delta + over a fixed block is the same with dummies off, on and off again. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_minstret_test.S + config: riscv-tests + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + +- test: dummy_instr_zcmp_test + desc: > + Dummy instructions must not disturb an expanded Zcmp sequence. Cosim + mismatches are not fatal because the cosim does not enable Zcmp. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 + +- test: dummy_instr_zcmp_debug_test + desc: > + dummy_instr_zcmp_test under random debug requests. Debug entry must + not split a Zcmp sequence. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_test: core_ibex_directed_stress_test + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 +enable_debug_seq=1 +max_interval=500 + +- test: dummy_instr_zcmp_irq_test + desc: > + dummy_instr_zcmp_test under random interrupts. An interrupt must not + split the committing micro-ops of a Zcmp sequence. + iterations: 1 + test_srcs: dummy_instr_test/dummy_instr_zcmp_test.S + config: riscv-tests + rtl_test: core_ibex_directed_stress_test + rtl_params: + PMPEnable: 1 + SecureIbex: 1 + RV32ZC: ["ibex_pkg::RV32ZcaZcmp", "ibex_pkg::RV32ZcaZcbZcmp"] + sim_opts: +disable_cosim=1 +enable_irq_single_seq=1 + - test: pmp_mseccfg_test_rlb1_l0_0_u0 desc: > mseccfg test diff --git a/dv/uvm/core_ibex/tests/core_ibex_test_lib.sv b/dv/uvm/core_ibex/tests/core_ibex_test_lib.sv index 0186201c60..2d54911e35 100644 --- a/dv/uvm/core_ibex/tests/core_ibex_test_lib.sv +++ b/dv/uvm/core_ibex/tests/core_ibex_test_lib.sv @@ -2066,3 +2066,31 @@ class core_ibex_mcounteren_lock_test extends core_ibex_base_test; endtask endclass + +// Sends debug requests (+enable_debug_seq=1) or interrupts (+enable_irq_single_seq=1) at random +// times while a directed test runs, and drops each interrupt after a short time. The program's +// handlers only return, and the program checks its own results. +class core_ibex_directed_stress_test extends core_ibex_base_test; + + `uvm_component_utils(core_ibex_directed_stress_test) + `uvm_component_new + + virtual task send_stimulus(); + fork + vseq.start(env.vseqr); + begin + clk_vif.wait_clks(stimulus_delay); + if (cfg.enable_debug_seq) vseq.start_debug_stress_seq(); + end + begin + clk_vif.wait_clks(stimulus_delay); + while (cfg.enable_irq_single_seq && !test_done) begin + vseq.start_irq_raise_single_seq(.no_nmi(1'b1)); + clk_vif.wait_clks($urandom_range(5, 50)); + vseq.start_irq_drop_seq(); + end + end + join_none + endtask + +endclass From feb2dd350e2894b474708a2026199bc65eff7c7a Mon Sep 17 00:00:00 2001 From: Kulan Palanichamy Date: Wed, 14 Oct 2026 09:52:49 -0700 Subject: [PATCH 6/6] [doc] Describe dummy instruction side effects The Dummy Instruction Insertion section only says that dummies have no functional impact. State that they are not counted as retired or as a single step, and that cycle counters still see them. Also state that no dummy is inserted directly before or inside a Zcmp instruction, and that Zcmp instructions do not count towards the insertion interval. Signed-off-by: Kulan Palanichamy --- doc/03_reference/security.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/03_reference/security.rst b/doc/03_reference/security.rst index cc7f8563fe..84696f9fa2 100644 --- a/doc/03_reference/security.rst +++ b/doc/03_reference/security.rst @@ -47,6 +47,11 @@ When enabled (via the **dummy_instr_en** control bit in the **cpuctrl** register The dummy instructions have no functional impact on processor state, but add some difficult-to-predict timing and power disruption to executed code. This disruption makes it more difficult for an attacker to infer what is being executed, and also makes it more difficult to execute precisely timed fault injection attacks. +Dummy instructions do not count as a single step and do not increment ``minstret`` or ``mhpmcounter10`` (compressed instructions retired). +They do take cycles, so they can show in ``mcycle`` and, where implemented and enabled, in the multiply and divide wait counters ``mhpmcounter11`` and ``mhpmcounter12``. +No dummy instruction is inserted directly before a Zcmp instruction or between its micro-ops. +Zcmp instructions do not count towards the intervals below, so a run of them lengthens the gap between dummy instructions. + The frequency of injected instructions can be tuned via the **dummy_instr_mask** bits in the **cpuctrl** register. +----------------------+----------------------------------------------------------+