Skip to content

Commit c45ed49

Browse files
committed
reorder_buffer: RTL cleanup, formal verification, and expanded cocotb coverage
Use the existing alloc_en signal in the tail pointer logic instead of repeating the allocation condition, preventing the two from drifting apart during maintenance. Add clarifying comments for the o_trap_pending overlap and the i_mepc stability contract at MRET commit. Add an ifdef FORMAL block with 8 combinational asserts (pointer/count consistency, commit invariants), 9 sequential asserts (reset, alloc commit, flush, serialization contracts), 2 structural assumes, and 8 cover properties. Create the corresponding reorder_buffer.sby and register it in test_run_formal.py. Add 6 new cocotb tests: simultaneous alloc/CDB/branch non-interference FP flags commit propagation, LR/SC stall and commit behavior, flush during WFI_WAIT and MRET_EXEC, sequential serializing instructions and alloc_ready deassertion during flush. All 37 tests pass. Fix README value field width from '64 bits' to 'FLEN (64) bits'.
1 parent 17d7a8a commit c45ed49

File tree

8 files changed

+728
-17
lines changed

8 files changed

+728
-17
lines changed

Dockerfile

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ ARG SBY_VERSION=0.62
3131
# Z3 SMT solver version (used by SymbiYosys for bounded model checking)
3232
ARG Z3_VERSION=4.15.0
3333

34+
# Boolector SMT solver version (word-level solver, efficient for bitvector-heavy designs)
35+
ARG BOOLECTOR_VERSION=3.2.4
36+
3437
# xPack RISC-V toolchain version (bare-metal, includes newlib)
3538
ARG XPACK_RISCV_VERSION=15.2.0-1
3639

@@ -42,8 +45,9 @@ RUN apt-get update && apt-get install -y \
4245
python3-pip \
4346
# HDL simulators (Verilator and Yosys built from source below)
4447
iverilog \
45-
# Build tools (shared by Verilator and Yosys)
48+
# Build tools (shared by Verilator, Yosys, and Boolector)
4649
make \
50+
cmake \
4751
git \
4852
xxd \
4953
gawk \
@@ -114,6 +118,30 @@ RUN git clone https://github.com/Z3Prover/z3.git /tmp/z3 \
114118
&& make install \
115119
&& rm -rf /tmp/z3
116120

121+
# Build Boolector SMT solver from source (word-level, efficient for memory arrays)
122+
# Lingeling (SAT dependency) needs -Wno-error=incompatible-pointer-types for GCC 14+
123+
# so we build it manually instead of using contrib/setup-lingeling.sh
124+
RUN git clone https://github.com/Boolector/boolector.git /tmp/boolector \
125+
&& cd /tmp/boolector \
126+
&& git checkout ${BOOLECTOR_VERSION} \
127+
&& mkdir -p deps/install/lib deps/install/include \
128+
&& cd deps \
129+
&& git clone https://github.com/arminbiere/lingeling.git \
130+
&& cd lingeling \
131+
&& git checkout 7d5db72420b95ab356c98ca7f7a4681ed2c59c70 \
132+
&& ./configure.sh -fPIC \
133+
&& sed -i 's/^CFLAGS=\(.*\)/CFLAGS=\1 -Wno-error=incompatible-pointer-types/' makefile \
134+
&& make -j$(nproc) \
135+
&& cp liblgl.a ../install/lib/ \
136+
&& cp lglib.h ../install/include/ \
137+
&& cd /tmp/boolector \
138+
&& ./contrib/setup-btor2tools.sh \
139+
&& ./configure.sh \
140+
&& cd build \
141+
&& make -j$(nproc) \
142+
&& make install \
143+
&& rm -rf /tmp/boolector
144+
117145
# Install xPack RISC-V GCC toolchain (bare-metal with newlib)
118146
RUN curl -fL https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v${XPACK_RISCV_VERSION}/xpack-riscv-none-elf-gcc-${XPACK_RISCV_VERSION}-linux-x64.tar.gz \
119147
| tar -xz -C /opt \

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Validated with these tool versions:
103103
| **Synthesis** | Yosys | 0.60 |
104104
| **Formal** | SymbiYosys | 0.62 |
105105
| | Z3 | 4.15.0 |
106+
| | Boolector | 3.2.4 |
106107
| **FPGA** | Vivado (optional) | 2025.2 |
107108
| **Linting** | pre-commit | 4.0 |
108109
| | clang-format | 19.0 |
@@ -129,7 +130,7 @@ The Docker image includes:
129130
- Verilator 5.044 (built from source)
130131
- Icarus Verilog 12.0
131132
- Yosys 0.60 (built from source)
132-
- SymbiYosys 0.62 + Z3 4.15.0 (formal verification)
133+
- SymbiYosys 0.62 + Z3 4.15.0 + Boolector 3.2.4 (formal verification)
133134
- RISC-V GCC 15.2.0 (xPack bare-metal toolchain)
134135
- Python 3.12 with Cocotb 2.0.1 and pytest
135136
- Pre-commit with all linters (clang-format, clang-tidy, Verible, ruff, mypy)

formal/.gitignore

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
# SymbiYosys working directories (generated per-run)
2-
*_bmc/
3-
*_cover/
4-
*_prove/
5-
6-
# SymbiYosys status database
7-
status.sqlite
1+
# SymbiYosys run output directories (e.g., hru_bmc/, trap_unit_cover/)
2+
# Source files (.sby, README.md) are all at the top level, not in subdirs.
3+
*/

formal/reorder_buffer.sby

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[tasks]
2+
bmc
3+
cover
4+
5+
[options]
6+
bmc: mode bmc
7+
bmc: depth 20
8+
cover: mode cover
9+
cover: depth 30
10+
11+
[engines]
12+
smtbmc boolector
13+
14+
[script]
15+
read -formal -sv riscv_pkg.sv
16+
read -formal -sv sdp_dist_ram.sv
17+
read -formal -sv mwp_dist_ram.sv
18+
read -formal -sv reorder_buffer.sv
19+
prep -top reorder_buffer
20+
21+
[files]
22+
../hw/rtl/cpu_and_mem/cpu/riscv_pkg.sv
23+
../hw/rtl/lib/ram/sdp_dist_ram.sv
24+
../hw/rtl/lib/ram/mwp_dist_ram.sv
25+
../hw/rtl/cpu_and_mem/cpu/tomasulo/reorder_buffer/reorder_buffer.sv

hw/rtl/cpu_and_mem/cpu/tomasulo/reorder_buffer/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ This saves approximately 5,500 FFs compared to a pure register-based design.
114114
| dest_rf | 1 bit | FF | 0=INT (x-reg), 1=FP (f-reg) |
115115
| dest_reg | 5 bits | LUTRAM | Architectural destination (rd) |
116116
| dest_valid | 1 bit | FF | Has destination register |
117-
| value | 64 bits | LUTRAM | Result value (FLEN for FP double support) |
117+
| value | FLEN (64) bits | LUTRAM | Result value (FLEN for FP double support) |
118118
| is_store | 1 bit | FF | Is store instruction |
119119
| is_fp_store | 1 bit | FF | Is FP store (FSW/FSD) |
120120
| is_branch | 1 bit | FF | Is branch/jump instruction |
@@ -144,9 +144,9 @@ returns the allocated tag (entry index) which becomes the instruction's identifi
144144
throughout the pipeline.
145145

146146
**Invariant**: Dispatch must not assert `alloc_valid` during flush cycles (`i_flush_en`
147-
or `i_flush_all`). The `alloc_ready` signal does not gate on flush; instead, dispatch
148-
is expected to be stalled by the flush controller. An assertion in simulation verifies
149-
this invariant.
147+
or `i_flush_all`). The `alloc_ready` signal gates on flush (deasserts during flush
148+
cycles), but dispatch is also expected to be independently stalled by the flush
149+
controller. An assertion in simulation verifies this invariant.
150150

151151
### CDB Write Interface (from Functional Units)
152152

hw/rtl/cpu_and_mem/cpu/tomasulo/reorder_buffer/reorder_buffer.sv

Lines changed: 168 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ module reorder_buffer (
549549
// Partial flush: set tail to flush_tag + 1
550550
// Use age-based arithmetic to handle wrap correctly (extend 5-bit age to 6-bit)
551551
tail_ptr <= head_ptr + {1'b0, flush_age} + 1'b1;
552-
end else if (i_alloc_req.alloc_valid && !full && !i_flush_all && !i_flush_en) begin
552+
end else if (alloc_en) begin
553553
// Normal allocation: advance tail
554554
tail_ptr <= tail_ptr + 1'b1;
555555
end
@@ -868,7 +868,11 @@ module reorder_buffer (
868868
head_is_mret && !head_exception &&
869869
!i_flush_en && !i_flush_all;
870870

871-
// Trap pending signal - asserted when exception at head
871+
// Trap pending signal - asserted when exception at head.
872+
// Note: during the IDLE->TRAP_WAIT transition, both the state check and the
873+
// combinational path assert o_trap_pending simultaneously. This overlap is
874+
// intentional and benign (result is still 1'b1); the state check sustains
875+
// the signal while the combinational term covers the initial detection cycle.
872876
assign o_trap_pending = (serial_state == SERIAL_TRAP_WAIT) ||
873877
(head_ready && head_exception && !i_flush_all);
874878
assign o_trap_pc = head_pc;
@@ -914,6 +918,9 @@ module reorder_buffer (
914918
// - Mispredicted taken: redirect to branch_target (actual taken target)
915919
// - Mispredicted not-taken: redirect to pc+4 (fall-through)
916920
if (head_is_mret) begin
921+
// i_mepc is guaranteed stable here: the MRET handshake
922+
// (o_mret_start/i_mret_done) completes before commit_en asserts,
923+
// so the trap unit has finished updating mepc by this point.
917924
o_commit.redirect_pc = i_mepc;
918925
end else if (head_branch_taken) begin
919926
// Mispredicted as not-taken but actually taken -> go to taken target
@@ -960,6 +967,7 @@ module reorder_buffer (
960967
// ===========================================================================
961968

962969
`ifndef SYNTHESIS
970+
`ifndef FORMAL
963971
// Check that we don't allocate when full
964972
always @(posedge i_clk) begin
965973
if (i_rst_n && i_alloc_req.alloc_valid && full) begin
@@ -968,7 +976,7 @@ module reorder_buffer (
968976
end
969977

970978
// Check that dispatch doesn't allocate during flush (invariant: dispatch must be stalled)
971-
// Note: alloc_ready doesn't gate on flush, so this assertion documents the required behavior
979+
// Note: alloc_ready also deasserts during flush, but dispatch should be independently stalled
972980
always @(posedge i_clk) begin
973981
if (i_rst_n && i_alloc_req.alloc_valid && (i_flush_en || i_flush_all)) begin
974982
$error("Reorder Buffer: Allocation attempted during flush!");
@@ -995,6 +1003,162 @@ module reorder_buffer (
9951003
$warning("Reorder Buffer: Serialization state %0d but head not ready", serial_state);
9961004
end
9971005
end
998-
`endif
1006+
`endif // FORMAL
1007+
`endif // SYNTHESIS
1008+
1009+
// ===========================================================================
1010+
// Formal Verification
1011+
// ===========================================================================
1012+
1013+
`ifdef FORMAL
1014+
1015+
initial assume (!i_rst_n);
1016+
1017+
reg f_past_valid;
1018+
initial f_past_valid = 1'b0;
1019+
always @(posedge i_clk) f_past_valid <= 1'b1;
1020+
1021+
// Force reset to deassert after the initial cycle and stay deasserted.
1022+
// Without this, the solver can hold i_rst_n low forever, making all
1023+
// i_rst_n-gated asserts vacuously true.
1024+
always @(posedge i_clk) begin
1025+
if (f_past_valid) assume (i_rst_n);
1026+
end
1027+
1028+
// -------------------------------------------------------------------------
1029+
// Structural constraints (assumes)
1030+
// -------------------------------------------------------------------------
1031+
// These are interface contracts — the upstream dispatch/CDB/branch units
1032+
// guarantee these conditions. They are intentionally kept as assumes
1033+
// (not relaxed) because the ROB's correctness depends on them.
1034+
1035+
// CDB write and branch update cannot target the same tag simultaneously
1036+
always_comb begin
1037+
assume (!(i_cdb_write.valid && i_branch_update.valid &&
1038+
i_cdb_write.tag == i_branch_update.tag));
1039+
end
1040+
1041+
// alloc_valid not asserted during flush (matches existing simulation assertion)
1042+
always_comb begin
1043+
assume (!(i_alloc_req.alloc_valid && (i_flush_en || i_flush_all)));
1044+
end
1045+
1046+
// -------------------------------------------------------------------------
1047+
// Combinational properties (asserts, active when i_rst_n)
1048+
// -------------------------------------------------------------------------
1049+
1050+
always @(posedge i_clk) begin
1051+
if (i_rst_n) begin
1052+
// full and empty cannot both be true
1053+
p_full_empty_mutex : assert (!(full && empty));
1054+
1055+
// count == tail_ptr - head_ptr
1056+
p_count_consistent : assert (count == (tail_ptr - head_ptr));
1057+
1058+
// full iff pointers match with different MSB
1059+
p_full_matches_ptrs :
1060+
assert (full ==
1061+
((head_ptr[ReorderBufferTagWidth] != tail_ptr[ReorderBufferTagWidth]) &&
1062+
(head_idx == tail_idx)));
1063+
1064+
// empty iff pointers exactly equal
1065+
p_empty_matches_ptrs : assert (empty == (head_ptr == tail_ptr));
1066+
1067+
// alloc_en implies !full
1068+
p_alloc_not_when_full : assert (!alloc_en || !full);
1069+
1070+
// commit_en implies head_valid && head_done
1071+
p_commit_requires_valid_done : assert (!commit_en || (head_valid && head_done));
1072+
1073+
// commit output tag equals head_idx
1074+
p_commit_only_at_head : assert (!commit_en || (o_commit.tag == head_idx));
1075+
1076+
// commit_stall implies !commit_en
1077+
p_serial_stall_blocks_commit : assert (!commit_stall || !commit_en);
1078+
end
1079+
end
1080+
1081+
// -------------------------------------------------------------------------
1082+
// Sequential properties (asserts, require f_past_valid)
1083+
// -------------------------------------------------------------------------
1084+
1085+
always @(posedge i_clk) begin
1086+
if (f_past_valid && i_rst_n && $past(i_rst_n)) begin
1087+
// After allocation, rob_valid at $past(tail_idx) is set
1088+
if ($past(alloc_en)) begin
1089+
p_alloc_sets_valid : assert (rob_valid[$past(tail_idx)]);
1090+
end
1091+
1092+
// After commit, rob_valid at $past(head_idx) is cleared
1093+
if ($past(commit_en) && !$past(i_flush_all) && !$past(i_flush_en)) begin
1094+
p_commit_clears_valid : assert (!rob_valid[$past(head_idx)]);
1095+
end
1096+
1097+
// After flush_all, buffer is empty
1098+
if ($past(i_flush_all)) begin
1099+
p_flush_all_empties : assert (empty);
1100+
end
1101+
1102+
// o_csr_start only in IDLE with CSR at head
1103+
if ($past(o_csr_start)) begin
1104+
p_csr_start_contract : assert ($past(serial_state) == SERIAL_IDLE && $past(head_is_csr));
1105+
end
1106+
1107+
// o_mret_start only in IDLE with MRET at head
1108+
if ($past(o_mret_start)) begin
1109+
p_mret_start_contract : assert ($past(serial_state) == SERIAL_IDLE && $past(head_is_mret));
1110+
end
1111+
1112+
// o_fence_i_flush is registered (one cycle after commit of FENCE.I)
1113+
p_fence_i_flush_delayed :
1114+
assert (o_fence_i_flush == ($past(commit_en) && $past(head_is_fence_i)));
1115+
end
1116+
1117+
// Reset properties (check state after reset deasserts)
1118+
if (f_past_valid && i_rst_n && !$past(i_rst_n)) begin
1119+
// After reset, all rob_valid bits are 0
1120+
p_reset_clears_valid : assert (rob_valid == '0);
1121+
1122+
// After reset, head_ptr and tail_ptr are 0
1123+
p_reset_clears_ptrs : assert (head_ptr == '0 && tail_ptr == '0);
1124+
1125+
// After reset, serial_state is IDLE
1126+
p_reset_serial_idle : assert (serial_state == SERIAL_IDLE);
1127+
end
1128+
end
1129+
1130+
// -------------------------------------------------------------------------
1131+
// Cover properties
1132+
// -------------------------------------------------------------------------
1133+
1134+
always @(posedge i_clk) begin
1135+
if (i_rst_n) begin
1136+
// Allocation and commit in same cycle
1137+
cover_alloc_and_commit : cover (alloc_en && commit_en);
1138+
1139+
// Buffer reaches full state
1140+
cover_buffer_full : cover (full);
1141+
1142+
// Partial flush occurs
1143+
cover_partial_flush : cover (i_flush_en);
1144+
1145+
// CSR serialization completes
1146+
cover_csr_serialize : cover (serial_state == SERIAL_CSR_EXEC && i_csr_done);
1147+
1148+
// WFI wakes on interrupt
1149+
cover_wfi_wakeup : cover (serial_state == SERIAL_WFI_WAIT && i_interrupt_pending);
1150+
1151+
// MRET completes
1152+
cover_mret_complete : cover (serial_state == SERIAL_MRET_EXEC && i_mret_done);
1153+
1154+
// Exception triggers trap
1155+
cover_exception_trap : cover (serial_state == SERIAL_TRAP_WAIT);
1156+
1157+
// FENCE.I commit generates flush pulse
1158+
cover_fence_i_flush : cover (o_fence_i_flush);
1159+
end
1160+
end
1161+
1162+
`endif // FORMAL
9991163

10001164
endmodule : reorder_buffer

tests/test_run_formal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def name(self) -> str:
6767
FormalTarget("cache_hit.sby", "Cache hit detector - L0 cache hit logic"),
6868
FormalTarget("cache_write.sby", "Cache write controller - L0 cache writes"),
6969
FormalTarget("data_mem_arb.sby", "Data memory arbiter - memory interface mux"),
70+
FormalTarget(
71+
"reorder_buffer.sby",
72+
"Reorder buffer - in-order commit with serialization",
73+
),
7074
]
7175

7276
# SymbiYosys task types (for CLI --task filter and pytest parametrize)

0 commit comments

Comments
 (0)