From 8d94029c4e3e1d73c5c12e894d652b121e00b59b Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Tue, 14 Nov 2023 09:28:58 +0100 Subject: [PATCH 01/25] Autumn cleaning: layouts (#501) --- coreblocks/core.py | 4 +- coreblocks/frontend/decode.py | 2 +- coreblocks/frontend/fetch.py | 4 +- coreblocks/fu/fu_decoder.py | 6 +- coreblocks/lsu/dummyLsu.py | 18 +- coreblocks/params/genparams.py | 8 +- coreblocks/params/layouts.py | 563 ++++++++++++++++++---------- coreblocks/stages/backend.py | 15 +- coreblocks/stages/rs_func_block.py | 6 +- coreblocks/structs_common/csr.py | 14 +- coreblocks/structs_common/rs.py | 18 +- test/frontend/test_decode.py | 8 +- test/frontend/test_fetch.py | 4 +- test/lsu/test_dummylsu.py | 4 +- test/scheduler/test_rs_selection.py | 4 +- test/scheduler/test_scheduler.py | 4 +- test/stages/test_backend.py | 6 +- test/structs_common/test_csr.py | 2 +- test/structs_common/test_rs.py | 24 +- test/test_core.py | 2 +- transactron/utils/_typing.py | 3 +- transactron/utils/utils.py | 10 + 22 files changed, 461 insertions(+), 268 deletions(-) diff --git a/coreblocks/core.py b/coreblocks/core.py index 1cec20059..2adcbbe53 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -75,8 +75,8 @@ def __init__(self, *, gen_params: GenParams, wb_instr_bus: WishboneBus, wb_data_ gen=self.gen_params, get_result=self.func_blocks_unifier.get_result, rob_mark_done=self.ROB.mark_done, - rs_write_val=self.func_blocks_unifier.update, - rf_write_val=self.RF.write, + rs_update=self.func_blocks_unifier.update, + rf_write=self.RF.write, ) self.csr_generic = GenericCSRRegisters(self.gen_params) diff --git a/coreblocks/frontend/decode.py b/coreblocks/frontend/decode.py index e6513e158..aefd00d5f 100644 --- a/coreblocks/frontend/decode.py +++ b/coreblocks/frontend/decode.py @@ -42,7 +42,7 @@ def elaborate(self, platform): with Transaction().body(m): raw = self.get_raw(m) - m.d.top_comb += instr_decoder.instr.eq(raw.data) + m.d.top_comb += instr_decoder.instr.eq(raw.instr) # Jump-branch unit requires information if the instruction was # decoded from a compressed instruction. To avoid adding a new signal diff --git a/coreblocks/frontend/fetch.py b/coreblocks/frontend/fetch.py index 74c3861a1..96e0da3af 100644 --- a/coreblocks/frontend/fetch.py +++ b/coreblocks/frontend/fetch.py @@ -82,7 +82,7 @@ def stall(): m.d.sync += self.pc.eq(target.addr) m.d.comb += instr.eq(res.instr) - self.cont(m, data=instr, pc=target.addr, access_fault=fetch_error, rvc=0) + self.cont(m, instr=instr, pc=target.addr, access_fault=fetch_error, rvc=0) @def_method(m, self.verify_branch, ready=stalled) def _(from_pc: Value, next_pc: Value): @@ -210,7 +210,7 @@ def elaborate(self, platform) -> TModule: with m.If(~cache_resp.error): m.d.sync += current_pc.eq(current_pc + Mux(is_rvc, C(2, 3), C(4, 3))) - self.cont(m, data=instr, pc=current_pc, access_fault=cache_resp.error, rvc=is_rvc) + self.cont(m, instr=instr, pc=current_pc, access_fault=cache_resp.error, rvc=is_rvc) @def_method(m, self.verify_branch, ready=(stalled & ~flushing)) def _(from_pc: Value, next_pc: Value): diff --git a/coreblocks/fu/fu_decoder.py b/coreblocks/fu/fu_decoder.py index 0e3b7939a..510ee30f0 100644 --- a/coreblocks/fu/fu_decoder.py +++ b/coreblocks/fu/fu_decoder.py @@ -1,7 +1,7 @@ from typing import Sequence, Type from amaranth import * -from coreblocks.params import GenParams, CommonLayouts +from coreblocks.params import GenParams, CommonLayoutFields from enum import IntFlag @@ -19,9 +19,9 @@ class Decoder(Elaboratable): """ def __init__(self, gen_params: GenParams, decode_fn: Type[IntFlag], ops: Sequence[tuple], check_optype: bool): - layouts = gen_params.get(CommonLayouts) + layouts = gen_params.get(CommonLayoutFields) - self.exec_fn = Record(layouts.exec_fn) + self.exec_fn = Record(layouts.exec_fn_layout) self.decode_fn = Signal(decode_fn) self.ops = ops self.check_optype = check_optype diff --git a/coreblocks/lsu/dummyLsu.py b/coreblocks/lsu/dummyLsu.py index cb506aafc..d1530dc4c 100644 --- a/coreblocks/lsu/dummyLsu.py +++ b/coreblocks/lsu/dummyLsu.py @@ -204,9 +204,9 @@ def __init__(self, gen_params: GenParams, bus: WishboneMaster) -> None: self.fu_layouts = gen_params.get(FuncUnitLayouts) self.lsu_layouts = gen_params.get(LSULayouts) - self.insert = Method(i=self.lsu_layouts.rs_insert_in) - self.select = Method(o=self.lsu_layouts.rs_select_out) - self.update = Method(i=self.lsu_layouts.rs_update_in) + self.insert = Method(i=self.lsu_layouts.rs.insert_in) + self.select = Method(o=self.lsu_layouts.rs.select_out) + self.update = Method(i=self.lsu_layouts.rs.update_in) self.get_result = Method(o=self.fu_layouts.accept) self.precommit = Method(i=self.lsu_layouts.precommit) @@ -215,7 +215,7 @@ def __init__(self, gen_params: GenParams, bus: WishboneMaster) -> None: def elaborate(self, platform): m = TModule() reserved = Signal() # means that current_instr is reserved - current_instr = Record(self.lsu_layouts.rs_data_layout + [("valid", 1)]) + current_instr = Record(self.lsu_layouts.rs.data_layout + [("valid", 1)]) m.submodules.internal = internal = LSUDummyInternals(self.gen_params, self.bus, current_instr) @@ -233,12 +233,12 @@ def _(rs_data: Record, rs_entry_id: Value): m.d.sync += current_instr.valid.eq(1) @def_method(m, self.update) - def _(tag: Value, value: Value): - with m.If(current_instr.rp_s1 == tag): - m.d.sync += current_instr.s1_val.eq(value) + def _(reg_id: Value, reg_val: Value): + with m.If(current_instr.rp_s1 == reg_id): + m.d.sync += current_instr.s1_val.eq(reg_val) m.d.sync += current_instr.rp_s1.eq(0) - with m.If(current_instr.rp_s2 == tag): - m.d.sync += current_instr.s2_val.eq(value) + with m.If(current_instr.rp_s2 == reg_id): + m.d.sync += current_instr.s2_val.eq(reg_val) m.d.sync += current_instr.rp_s2.eq(0) @def_method(m, self.get_result, result_ready) diff --git a/coreblocks/params/genparams.py b/coreblocks/params/genparams.py index 2b6611aa9..9bf84c23c 100644 --- a/coreblocks/params/genparams.py +++ b/coreblocks/params/genparams.py @@ -7,6 +7,7 @@ from .icache_params import ICacheParameters from .fu_params import extensions_supported from ..peripherals.wishbone import WishboneParameters +from transactron.utils import make_hashable from typing import TYPE_CHECKING @@ -35,10 +36,11 @@ class DependentCache: """ def __init__(self): - self._depcache: dict[tuple[Type, frozenset[tuple[str, Any]]], Type] = {} + self._depcache: dict[tuple[Type, Any], Type] = {} def get(self, cls: Type[T], **kwargs) -> T: - v = self._depcache.get((cls, frozenset(kwargs.items())), None) + cache_key = make_hashable(kwargs) + v = self._depcache.get((cls, cache_key), None) if v is None: positional_count = cls.__init__.__code__.co_argcount @@ -50,7 +52,7 @@ def get(self, cls: Type[T], **kwargs) -> T: v = cls(self, **kwargs) else: v = cls(**kwargs) - self._depcache[(cls, frozenset(kwargs.items()))] = v + self._depcache[(cls, cache_key)] = v return v diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index 2a0f1a3e0..6df1970b7 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -1,11 +1,12 @@ from coreblocks.params import GenParams, OpType, Funct7, Funct3 from coreblocks.params.isa import ExceptionCause from transactron.utils.utils import layout_subset +from transactron.utils import LayoutList, LayoutListField __all__ = [ + "CommonLayoutFields", "SchedulerLayouts", "ROBLayouts", - "CommonLayouts", "FetchLayouts", "DecodeLayouts", "FuncUnitLayouts", @@ -20,170 +21,324 @@ ] -class CommonLayouts: +class CommonLayoutFields: + """Commonly used layout fields.""" + def __init__(self, gen_params: GenParams): - self.exec_fn = [ - ("op_type", OpType), - ("funct3", Funct3), - ("funct7", Funct7), - ] + self.op_type: LayoutListField = ("op_type", OpType) + """Decoded operation type.""" - self.regs_l = [ - ("rl_s1", gen_params.isa.reg_cnt_log), - ("rl_s2", gen_params.isa.reg_cnt_log), - ("rl_dst", gen_params.isa.reg_cnt_log), - ] + self.funct3: LayoutListField = ("funct3", Funct3) + """RISC V funct3 value.""" - self.regs_p = [ - ("rp_dst", gen_params.phys_regs_bits), - ("rp_s1", gen_params.phys_regs_bits), - ("rp_s2", gen_params.phys_regs_bits), - ] + self.funct7: LayoutListField = ("funct7", Funct7) + """RISC V funct7 value.""" + + self.rl_s1: LayoutListField = ("rl_s1", gen_params.isa.reg_cnt_log) + """Logical register number of first source operand.""" + + self.rl_s2: LayoutListField = ("rl_s2", gen_params.isa.reg_cnt_log) + """Logical register number of second source operand.""" + + self.rl_dst: LayoutListField = ("rl_dst", gen_params.isa.reg_cnt_log) + """Logical register number of destination operand.""" + + self.rp_s1: LayoutListField = ("rp_s1", gen_params.phys_regs_bits) + """Physical register number of first source operand.""" + + self.rp_s2: LayoutListField = ("rp_s2", gen_params.phys_regs_bits) + """Physical register number of second source operand.""" + + self.rp_dst: LayoutListField = ("rp_dst", gen_params.phys_regs_bits) + """Physical register number of destination operand.""" + + self.imm: LayoutListField = ("imm", gen_params.isa.xlen) + """Immediate value.""" + + self.csr: LayoutListField = ("csr", gen_params.isa.csr_alen) + """CSR number.""" + + self.pc: LayoutListField = ("pc", gen_params.isa.xlen) + """Program counter value.""" + + self.rob_id: LayoutListField = ("rob_id", gen_params.rob_entries_bits) + """Reorder buffer entry identifier.""" + + self.s1_val: LayoutListField = ("s1_val", gen_params.isa.xlen) + """Value of first source operand.""" + + self.s2_val: LayoutListField = ("s2_val", gen_params.isa.xlen) + """Value of second source operand.""" + + self.reg_val: LayoutListField = ("reg_val", gen_params.isa.xlen) + """Value of some physical register.""" + + self.addr: LayoutListField = ("addr", gen_params.isa.xlen) + """Memory address.""" + + self.data: LayoutListField = ("data", gen_params.isa.xlen) + """Piece of data.""" + + self.instr: LayoutListField = ("instr", gen_params.isa.ilen) + """RISC V instruction.""" + + self.exec_fn_layout: LayoutList = [self.op_type, self.funct3, self.funct7] + """Decoded instruction, in layout form.""" + + self.exec_fn: LayoutListField = ("exec_fn", self.exec_fn_layout) + """Decoded instruction.""" + + self.regs_l: LayoutListField = ("regs_l", [self.rl_s1, self.rl_s2, self.rl_dst]) + """Logical register numbers - as described in the RISC V manual. They index the RATs.""" + + self.regs_p: LayoutListField = ("regs_p", [self.rp_s1, self.rp_s2, self.rp_dst]) + """Physical register numbers. They index the register file.""" + + self.reg_id: LayoutListField = ("reg_id", gen_params.phys_regs_bits) + """Physical register ID.""" + + self.exception: LayoutListField = ("exception", 1) + """Exception is raised for this instruction.""" + + self.error: LayoutListField = ("error", 1) + """Request ended with an error.""" class SchedulerLayouts: + """Layouts used in the scheduler.""" + def __init__(self, gen_params: GenParams): - common = gen_params.get(CommonLayouts) - self.reg_alloc_in = [ - ("exec_fn", common.exec_fn), - ("regs_l", common.regs_l), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + fields = gen_params.get(CommonLayoutFields) + + self.rs_selected: LayoutListField = ("rs_selected", gen_params.rs_number_bits) + """Reservation Station number for the instruction.""" + + self.rs_entry_id: LayoutListField = ("rs_entry_id", gen_params.max_rs_entries_bits) + """Reservation station entry ID for the instruction.""" + + self.regs_p_alloc_out: LayoutListField = ("regs_p", [fields.rp_dst]) + """Physical register number for the destination operand, after allocation.""" + + self.regs_l_rob_in: LayoutListField = ( + "regs_l", + [ + fields.rl_dst, + ("rl_dst_v", 1), + ], + ) + """Logical register number for the destination operand, before ROB allocation.""" + + self.reg_alloc_in: LayoutList = [ + fields.exec_fn, + fields.regs_l, + fields.imm, + fields.csr, + fields.pc, ] - self.reg_alloc_out = self.renaming_in = [ - ("exec_fn", common.exec_fn), - ("regs_l", common.regs_l), - ("regs_p", [("rp_dst", gen_params.phys_regs_bits)]), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + + self.reg_alloc_out: LayoutList = [ + fields.exec_fn, + fields.regs_l, + self.regs_p_alloc_out, + fields.imm, + fields.csr, + fields.pc, ] - self.renaming_out = self.rob_allocate_in = [ - ("exec_fn", common.exec_fn), - ( - "regs_l", - [ - ("rl_dst", gen_params.isa.reg_cnt_log), - ("rl_dst_v", 1), - ], - ), - ("regs_p", common.regs_p), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + + self.renaming_in = self.reg_alloc_out + + self.renaming_out: LayoutList = [ + fields.exec_fn, + self.regs_l_rob_in, + fields.regs_p, + fields.imm, + fields.csr, + fields.pc, ] - self.rob_allocate_out = self.rs_select_in = [ - ("exec_fn", common.exec_fn), - ("regs_p", common.regs_p), - ("rob_id", gen_params.rob_entries_bits), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + + self.rob_allocate_in = self.renaming_out + + self.rob_allocate_out: LayoutList = [ + fields.exec_fn, + fields.regs_p, + fields.rob_id, + fields.imm, + fields.csr, + fields.pc, ] - self.rs_select_out = self.rs_insert_in = [ - ("exec_fn", common.exec_fn), - ("regs_p", common.regs_p), - ("rob_id", gen_params.rob_entries_bits), - ("rs_selected", gen_params.rs_number_bits), - ("rs_entry_id", gen_params.max_rs_entries_bits), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + + self.rs_select_in = self.rob_allocate_out + + self.rs_select_out: LayoutList = [ + fields.exec_fn, + fields.regs_p, + fields.rob_id, + self.rs_selected, + self.rs_entry_id, + fields.imm, + fields.csr, + fields.pc, ] - self.free_rf_layout = [("reg_id", gen_params.phys_regs_bits)] + + self.rs_insert_in = self.rs_select_out + + self.free_rf_layout: LayoutList = [fields.reg_id] class RFLayouts: + """Layouts used in the register file.""" + def __init__(self, gen_params: GenParams): - self.rf_read_in = self.rf_free = [("reg_id", gen_params.phys_regs_bits)] - self.rf_read_out = [("reg_val", gen_params.isa.xlen), ("valid", 1)] - self.rf_write = [("reg_id", gen_params.phys_regs_bits), ("reg_val", gen_params.isa.xlen)] + fields = gen_params.get(CommonLayoutFields) + + self.valid: LayoutListField = ("valid", 1) + """Physical register was assigned a value.""" + + self.rf_read_in: LayoutList = [fields.reg_id] + self.rf_free: LayoutList = [fields.reg_id] + self.rf_read_out: LayoutList = [fields.reg_val, self.valid] + self.rf_write: LayoutList = [fields.reg_id, fields.reg_val] class RATLayouts: + """Layouts used in the register alias tables.""" + def __init__(self, gen_params: GenParams): - self.rat_rename_in = [ - ("rl_s1", gen_params.isa.reg_cnt_log), - ("rl_s2", gen_params.isa.reg_cnt_log), - ("rl_dst", gen_params.isa.reg_cnt_log), - ("rp_dst", gen_params.phys_regs_bits), + fields = gen_params.get(CommonLayoutFields) + + self.old_rp_dst: LayoutListField = ("old_rp_dst", gen_params.phys_regs_bits) + """Physical register previously associated with the given logical register in RRAT.""" + + self.rat_rename_in: LayoutList = [ + fields.rl_s1, + fields.rl_s2, + fields.rl_dst, + fields.rp_dst, ] - self.rat_rename_out = [("rp_s1", gen_params.phys_regs_bits), ("rp_s2", gen_params.phys_regs_bits)] - self.rat_commit_in = [("rl_dst", gen_params.isa.reg_cnt_log), ("rp_dst", gen_params.phys_regs_bits)] - self.rat_commit_out = [("old_rp_dst", gen_params.phys_regs_bits)] + self.rat_rename_out: LayoutList = [fields.rp_s1, fields.rp_s2] + + self.rat_commit_in: LayoutList = [fields.rl_dst, fields.rp_dst] + + self.rat_commit_out: LayoutList = [self.old_rp_dst] class ROBLayouts: + """Layouts used in the reorder buffer.""" + def __init__(self, gen_params: GenParams): - self.data_layout = [ - ("rl_dst", gen_params.isa.reg_cnt_log), - ("rp_dst", gen_params.phys_regs_bits), - ] + fields = gen_params.get(CommonLayoutFields) - self.id_layout = [ - ("rob_id", gen_params.rob_entries_bits), + self.data_layout: LayoutList = [ + fields.rl_dst, + fields.rp_dst, ] - self.internal_layout = [ - ("rob_data", self.data_layout), - ("done", 1), - ("exception", 1), + self.rob_data: LayoutListField = ("rob_data", self.data_layout) + """Data stored in a reorder buffer entry.""" + + self.done: LayoutListField = ("done", 1) + """Instruction has executed, but is not committed yet.""" + + self.start: LayoutListField = ("start", gen_params.rob_entries_bits) + """Index of the first (the earliest) entry in the reorder buffer.""" + + self.end: LayoutListField = ("end", gen_params.rob_entries_bits) + """Index of the entry following the last (the latest) entry in the reorder buffer.""" + + self.id_layout: LayoutList = [fields.rob_id] + + self.internal_layout: LayoutList = [ + self.rob_data, + self.done, + fields.exception, ] - self.mark_done_layout = [ - ("rob_id", gen_params.rob_entries_bits), - ("exception", 1), + self.mark_done_layout: LayoutList = [ + fields.rob_id, + fields.exception, ] - self.peek_layout = self.retire_layout = [ - ("rob_data", self.data_layout), - ("rob_id", gen_params.rob_entries_bits), - ("exception", 1), + self.peek_layout: LayoutList = [ + self.rob_data, + fields.rob_id, + fields.exception, ] - self.get_indices = [("start", gen_params.rob_entries_bits), ("end", gen_params.rob_entries_bits)] + self.retire_layout: LayoutList = self.peek_layout + self.get_indices: LayoutList = [self.start, self.end] -class RSInterfaceLayouts: - def __init__(self, gen_params: GenParams, *, rs_entries_bits: int): - common = gen_params.get(CommonLayouts) - self.data_layout = [ - ("rp_s1", gen_params.phys_regs_bits), - ("rp_s2", gen_params.phys_regs_bits), + +class RSLayoutFields: + """Layout fields used in the reservation station.""" + + def __init__(self, gen_params: GenParams, *, rs_entries_bits: int, data_layout: LayoutList): + self.rs_data: LayoutListField = ("rs_data", data_layout) + """Data about an instuction stored in a reservation station (RS).""" + + self.rs_entry_id: LayoutListField = ("rs_entry_id", rs_entries_bits) + """Index in a reservation station (RS).""" + + +class RSFullDataLayout: + """Full data layout for functional blocks. Blocks can use a subset.""" + + def __init__(self, gen_params: GenParams): + fields = gen_params.get(CommonLayoutFields) + + self.data_layout: LayoutList = [ + fields.rp_s1, + fields.rp_s2, ("rp_s1_reg", gen_params.phys_regs_bits), ("rp_s2_reg", gen_params.phys_regs_bits), - ("rp_dst", gen_params.phys_regs_bits), - ("rob_id", gen_params.rob_entries_bits), - ("exec_fn", common.exec_fn), - ("s1_val", gen_params.isa.xlen), - ("s2_val", gen_params.isa.xlen), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + fields.rp_dst, + fields.rob_id, + fields.exec_fn, + fields.s1_val, + fields.s2_val, + fields.imm, + fields.csr, + fields.pc, ] - self.select_out = [("rs_entry_id", rs_entries_bits)] - self.insert_in = [("rs_data", self.data_layout), ("rs_entry_id", rs_entries_bits)] +class RSInterfaceLayouts: + """Layouts used in functional blocks.""" + + def __init__(self, gen_params: GenParams, *, rs_entries_bits: int, data_layout: LayoutList): + fields = gen_params.get(CommonLayoutFields) + rs_fields = gen_params.get(RSLayoutFields, rs_entries_bits=rs_entries_bits, data_layout=data_layout) + + self.data_layout: LayoutList = data_layout - self.update_in = [("tag", gen_params.phys_regs_bits), ("value", gen_params.isa.xlen)] + self.select_out: LayoutList = [rs_fields.rs_entry_id] + + self.insert_in: LayoutList = [rs_fields.rs_data, rs_fields.rs_entry_id] + + self.update_in: LayoutList = [fields.reg_id, fields.reg_val] class RetirementLayouts: + """Layouts used in the retirement module.""" + def __init__(self, gen_params: GenParams): - self.precommit = [ - ("rob_id", gen_params.rob_entries_bits), - ] + fields = gen_params.get(CommonLayoutFields) + + self.precommit: LayoutList = [fields.rob_id] class RSLayouts: + """Layouts used in the reservation station.""" + def __init__(self, gen_params: GenParams, *, rs_entries_bits: int): - rs_interface = gen_params.get(RSInterfaceLayouts, rs_entries_bits=rs_entries_bits) + data = gen_params.get(RSFullDataLayout) - self.data_layout = layout_subset( - rs_interface.data_layout, + self.ready_list: LayoutListField = ("ready_list", 2**rs_entries_bits) + """Bitmask of reservation station entries containing instructions which are ready to run.""" + + data_layout = layout_subset( + data.data_layout, fields={ "rp_s1", "rp_s2", @@ -197,16 +352,13 @@ def __init__(self, gen_params: GenParams, *, rs_entries_bits: int): }, ) - self.insert_in = [("rs_data", self.data_layout), ("rs_entry_id", rs_entries_bits)] - - self.select_out = rs_interface.select_out + self.rs = gen_params.get(RSInterfaceLayouts, rs_entries_bits=rs_entries_bits, data_layout=data_layout) + rs_fields = gen_params.get(RSLayoutFields, rs_entries_bits=rs_entries_bits, data_layout=data_layout) - self.update_in = rs_interface.update_in - - self.take_in = [("rs_entry_id", rs_entries_bits)] + self.take_in: LayoutList = [rs_fields.rs_entry_id] self.take_out = layout_subset( - rs_interface.data_layout, + data.data_layout, fields={ "s1_val", "s2_val", @@ -218,113 +370,137 @@ def __init__(self, gen_params: GenParams, *, rs_entries_bits: int): }, ) - self.get_ready_list_out = [("ready_list", 2**rs_entries_bits)] + self.get_ready_list_out: LayoutList = [self.ready_list] class ICacheLayouts: + """Layouts used in the instruction cache.""" + def __init__(self, gen_params: GenParams): - self.issue_req = [ - ("addr", gen_params.isa.xlen), - ] + fields = gen_params.get(CommonLayoutFields) + + self.error: LayoutListField = ("last", 1) + """This is the last cache refill result.""" + + self.issue_req: LayoutList = [fields.addr] - self.accept_res = [ - ("instr", gen_params.isa.ilen), - ("error", 1), + self.accept_res: LayoutList = [ + fields.instr, + fields.error, ] - self.start_refill = [ - ("addr", gen_params.isa.xlen), + self.start_refill: LayoutList = [ + fields.addr, ] - self.accept_refill = [ - ("addr", gen_params.isa.xlen), - ("data", gen_params.isa.xlen), - ("error", 1), - ("last", 1), + self.accept_refill: LayoutList = [ + fields.addr, + fields.data, + fields.error, + self.error, ] class FetchLayouts: + """Layouts used in the fetcher.""" + def __init__(self, gen_params: GenParams): - self.raw_instr = [ - ("data", gen_params.isa.ilen), - ("pc", gen_params.isa.xlen), - ("access_fault", 1), - ("rvc", 1), + fields = gen_params.get(CommonLayoutFields) + + self.access_fault: LayoutListField = ("access_fault", 1) + """Instruction fetch failed.""" + + self.rvc: LayoutListField = ("rvc", 1) + """Instruction is a compressed (two-byte) one.""" + + self.raw_instr: LayoutList = [ + fields.instr, + fields.pc, + self.access_fault, + self.rvc, ] - self.branch_verify = [ + self.branch_verify: LayoutList = [ ("from_pc", gen_params.isa.xlen), ("next_pc", gen_params.isa.xlen), ] class DecodeLayouts: + """Layouts used in the decoder.""" + def __init__(self, gen_params: GenParams): - common = gen_params.get(CommonLayouts) - self.decoded_instr = [ - ("exec_fn", common.exec_fn), - ("regs_l", common.regs_l), - ("imm", gen_params.isa.xlen), - ("csr", gen_params.isa.csr_alen), - ("pc", gen_params.isa.xlen), + fields = gen_params.get(CommonLayoutFields) + + self.decoded_instr: LayoutList = [ + fields.exec_fn, + fields.regs_l, + fields.imm, + fields.csr, + fields.pc, ] class FuncUnitLayouts: + """Layouts used in functional units.""" + def __init__(self, gen_params: GenParams): - common = gen_params.get(CommonLayouts) - - self.issue = [ - ("s1_val", gen_params.isa.xlen), - ("s2_val", gen_params.isa.xlen), - ("rp_dst", gen_params.phys_regs_bits), - ("rob_id", gen_params.rob_entries_bits), - ("exec_fn", common.exec_fn), - ("imm", gen_params.isa.xlen), - ("pc", gen_params.isa.xlen), + fields = gen_params.get(CommonLayoutFields) + + self.result: LayoutListField = ("result", gen_params.isa.xlen) + """The result value produced in a functional unit.""" + + self.issue: LayoutList = [ + fields.s1_val, + fields.s2_val, + fields.rp_dst, + fields.rob_id, + fields.exec_fn, + fields.imm, + fields.pc, ] - self.accept = [ - ("rob_id", gen_params.rob_entries_bits), - ("result", gen_params.isa.xlen), - ("rp_dst", gen_params.phys_regs_bits), - ("exception", 1), + self.accept: LayoutList = [ + fields.rob_id, + self.result, + fields.rp_dst, + fields.exception, ] class UnsignedMulUnitLayouts: def __init__(self, gen_params: GenParams): - self.issue = [ + self.issue: LayoutList = [ ("i1", gen_params.isa.xlen), ("i2", gen_params.isa.xlen), ] - self.accept = [ + self.accept: LayoutList = [ ("o", 2 * gen_params.isa.xlen), ] class DivUnitLayouts: def __init__(self, gen: GenParams): - self.issue = [ + self.issue: LayoutList = [ ("dividend", gen.isa.xlen), ("divisor", gen.isa.xlen), ] - self.accept = [ + self.accept: LayoutList = [ ("quotient", gen.isa.xlen), ("remainder", gen.isa.xlen), ] class LSULayouts: + """Layouts used in the load-store unit.""" + def __init__(self, gen_params: GenParams): - self.rs_entries_bits = 0 + data = gen_params.get(RSFullDataLayout) - rs_interface = gen_params.get(RSInterfaceLayouts, rs_entries_bits=self.rs_entries_bits) - self.rs_data_layout = layout_subset( - rs_interface.data_layout, + data_layout = layout_subset( + data.data_layout, fields={ "rp_s1", "rp_s2", @@ -337,11 +513,9 @@ def __init__(self, gen_params: GenParams): }, ) - self.rs_insert_in = [("rs_data", self.rs_data_layout), ("rs_entry_id", self.rs_entries_bits)] - - self.rs_select_out = rs_interface.select_out + self.rs_entries_bits = 0 - self.rs_update_in = rs_interface.update_in + self.rs = gen_params.get(RSInterfaceLayouts, rs_entries_bits=self.rs_entries_bits, data_layout=data_layout) retirement = gen_params.get(RetirementLayouts) @@ -349,23 +523,27 @@ def __init__(self, gen_params: GenParams): class CSRLayouts: + """Layouts used in the control and status registers.""" + def __init__(self, gen_params: GenParams): + data = gen_params.get(RSFullDataLayout) + fields = gen_params.get(CommonLayoutFields) + self.rs_entries_bits = 0 - self.read = [ - ("data", gen_params.isa.xlen), + self.read: LayoutList = [ + fields.data, ("read", 1), ("written", 1), ] - self.write = [("data", gen_params.isa.xlen)] + self.write: LayoutList = [fields.data] - self._fu_read = [("data", gen_params.isa.xlen)] - self._fu_write = [("data", gen_params.isa.xlen)] + self._fu_read: LayoutList = [fields.data] + self._fu_write: LayoutList = [fields.data] - rs_interface = gen_params.get(RSInterfaceLayouts, rs_entries_bits=self.rs_entries_bits) - self.rs_data_layout = layout_subset( - rs_interface.data_layout, + data_layout = layout_subset( + data.data_layout, fields={ "rp_s1", "rp_s1_reg", @@ -379,11 +557,7 @@ def __init__(self, gen_params: GenParams): }, ) - self.rs_insert_in = [("rs_data", self.rs_data_layout), ("rs_entry_id", self.rs_entries_bits)] - - self.rs_select_out = rs_interface.select_out - - self.rs_update_in = rs_interface.update_in + self.rs = gen_params.get(RSInterfaceLayouts, rs_entries_bits=self.rs_entries_bits, data_layout=data_layout) retirement = gen_params.get(RetirementLayouts) @@ -391,8 +565,17 @@ def __init__(self, gen_params: GenParams): class ExceptionRegisterLayouts: + """Layouts used in the exception register.""" + def __init__(self, gen_params: GenParams): - self.get = self.report = [ - ("cause", ExceptionCause), - ("rob_id", gen_params.rob_entries_bits), + fields = gen_params.get(CommonLayoutFields) + + self.cause: LayoutListField = ("cause", ExceptionCause) + """Exception cause.""" + + self.get: LayoutList = [ + self.cause, + fields.rob_id, ] + + self.report = self.get diff --git a/coreblocks/stages/backend.py b/coreblocks/stages/backend.py index 4ba245baf..00bfb7940 100644 --- a/coreblocks/stages/backend.py +++ b/coreblocks/stages/backend.py @@ -20,7 +20,7 @@ class ResultAnnouncement(Elaboratable): """ def __init__( - self, *, gen: GenParams, get_result: Method, rob_mark_done: Method, rs_write_val: Method, rf_write_val: Method + self, *, gen: GenParams, get_result: Method, rob_mark_done: Method, rs_update: Method, rf_write: Method ): """ Parameters @@ -34,20 +34,17 @@ def __init__( from different FUs are already serialized. rob_mark_done : Method Method which is invoked to mark that instruction ended without exception. - It uses layout with one field `rob_id`, - rs_write_val : Method + rs_update : Method Method which is invoked to pass value which is an output of finished instruction to RS, so that RS can save it if there are instructions which wait for it. - It uses layout with two fields `tag` and `value`. - rf_write_val : Method + rf_write : Method Method which is invoked to save value which is an output of finished instruction to RF. - It uses layout with two fields `reg_id` and `reg_val`. """ self.m_get_result = get_result self.m_rob_mark_done = rob_mark_done - self.m_rs_write_val = rs_write_val - self.m_rf_write_val = rf_write_val + self.m_rs_update = rs_update + self.m_rf_write_val = rf_write def debug_signals(self): return [self.m_get_result.debug_signals()] @@ -62,6 +59,6 @@ def elaborate(self, platform): with m.If(result.exception == 0): self.m_rf_write_val(m, reg_id=result.rp_dst, reg_val=result.result) with m.If(result.rp_dst != 0): - self.m_rs_write_val(m, tag=result.rp_dst, value=result.result) + self.m_rs_update(m, reg_id=result.rp_dst, reg_val=result.result) return m diff --git a/coreblocks/stages/rs_func_block.py b/coreblocks/stages/rs_func_block.py index e2ff47983..9b3a45c4b 100644 --- a/coreblocks/stages/rs_func_block.py +++ b/coreblocks/stages/rs_func_block.py @@ -47,9 +47,9 @@ def __init__(self, gen_params: GenParams, func_units: Iterable[tuple[FuncUnit, s self.fu_layouts = gen_params.get(FuncUnitLayouts) self.func_units = list(func_units) - self.insert = Method(i=self.rs_layouts.insert_in) - self.select = Method(o=self.rs_layouts.select_out) - self.update = Method(i=self.rs_layouts.update_in) + self.insert = Method(i=self.rs_layouts.rs.insert_in) + self.select = Method(o=self.rs_layouts.rs.select_out) + self.update = Method(i=self.rs_layouts.rs.update_in) self.get_result = Method(o=self.fu_layouts.accept) def elaborate(self, platform): diff --git a/coreblocks/structs_common/csr.py b/coreblocks/structs_common/csr.py index 3dc4763d6..f1a6d89f1 100644 --- a/coreblocks/structs_common/csr.py +++ b/coreblocks/structs_common/csr.py @@ -194,9 +194,9 @@ def __init__(self, gen_params: GenParams): # Standard RS interface self.csr_layouts = gen_params.get(CSRLayouts) self.fu_layouts = gen_params.get(FuncUnitLayouts) - self.select = Method(o=self.csr_layouts.rs_select_out) - self.insert = Method(i=self.csr_layouts.rs_insert_in) - self.update = Method(i=self.csr_layouts.rs_update_in) + self.select = Method(o=self.csr_layouts.rs.select_out) + self.insert = Method(i=self.csr_layouts.rs.insert_in) + self.update = Method(i=self.csr_layouts.rs.update_in) self.get_result = Method(o=self.fu_layouts.accept) self.precommit = Method(i=self.csr_layouts.precommit) @@ -223,7 +223,7 @@ def elaborate(self, platform): current_result = Signal(self.gen_params.isa.xlen) - instr = Record(self.csr_layouts.rs_data_layout + [("valid", 1)]) + instr = Record(self.csr_layouts.rs.data_layout + [("valid", 1)]) m.d.comb += ready_to_process.eq(rob_sfx_empty & instr.valid & (instr.rp_s1 == 0)) @@ -310,9 +310,9 @@ def _(rs_entry_id, rs_data): m.d.sync += instr.valid.eq(1) @def_method(m, self.update) - def _(tag, value): - with m.If(tag == instr.rp_s1): - m.d.sync += instr.s1_val.eq(value) + def _(reg_id, reg_val): + with m.If(reg_id == instr.rp_s1): + m.d.sync += instr.s1_val.eq(reg_val) m.d.sync += instr.rp_s1.eq(0) @def_method(m, self.get_result, done) diff --git a/coreblocks/structs_common/rs.py b/coreblocks/structs_common/rs.py index eb045c3a4..255f48a63 100644 --- a/coreblocks/structs_common/rs.py +++ b/coreblocks/structs_common/rs.py @@ -19,15 +19,15 @@ def __init__( self.rs_entries_bits = (rs_entries - 1).bit_length() self.layouts = gen_params.get(RSLayouts, rs_entries_bits=self.rs_entries_bits) self.internal_layout = [ - ("rs_data", self.layouts.data_layout), + ("rs_data", self.layouts.rs.data_layout), ("rec_full", 1), ("rec_ready", 1), ("rec_reserved", 1), ] - self.insert = Method(i=self.layouts.insert_in) - self.select = Method(o=self.layouts.select_out) - self.update = Method(i=self.layouts.update_in) + self.insert = Method(i=self.layouts.rs.insert_in) + self.select = Method(o=self.layouts.rs.select_out) + self.update = Method(i=self.layouts.rs.update_in) self.take = Method(i=self.layouts.take_in, o=self.layouts.take_out) self.ready_for = [list(op_list) for op_list in ready_for] @@ -70,16 +70,16 @@ def _(rs_entry_id: Value, rs_data: Value) -> None: m.d.sync += self.data[rs_entry_id].rec_reserved.eq(1) @def_method(m, self.update) - def _(tag: Value, value: Value) -> None: + def _(reg_id: Value, reg_val: Value) -> None: for record in self.data: with m.If(record.rec_full.bool()): - with m.If(record.rs_data.rp_s1 == tag): + with m.If(record.rs_data.rp_s1 == reg_id): m.d.sync += record.rs_data.rp_s1.eq(0) - m.d.sync += record.rs_data.s1_val.eq(value) + m.d.sync += record.rs_data.s1_val.eq(reg_val) - with m.If(record.rs_data.rp_s2 == tag): + with m.If(record.rs_data.rp_s2 == reg_id): m.d.sync += record.rs_data.rp_s2.eq(0) - m.d.sync += record.rs_data.s2_val.eq(value) + m.d.sync += record.rs_data.s2_val.eq(reg_val) @def_method(m, self.take, ready=take_possible) def _(rs_entry_id: Value) -> RecordDict: diff --git a/test/frontend/test_decode.py b/test/frontend/test_decode.py index 1128df9e8..f728152f5 100644 --- a/test/frontend/test_decode.py +++ b/test/frontend/test_decode.py @@ -40,7 +40,7 @@ def setUp(self) -> None: def decode_test_proc(self): # testing an OP_IMM instruction (test copied from test_decoder.py) - yield from self.test_module.io_in.call(data=0x02A28213) + yield from self.test_module.io_in.call(instr=0x02A28213) decoded = yield from self.test_module.io_out.call() self.assertEqual(decoded["exec_fn"]["op_type"], OpType.ARITHMETIC) @@ -52,7 +52,7 @@ def decode_test_proc(self): self.assertEqual(decoded["imm"], 42) # testing an OP instruction (test copied from test_decoder.py) - yield from self.test_module.io_in.call(data=0x003100B3) + yield from self.test_module.io_in.call(instr=0x003100B3) decoded = yield from self.test_module.io_out.call() self.assertEqual(decoded["exec_fn"]["op_type"], OpType.ARITHMETIC) @@ -63,7 +63,7 @@ def decode_test_proc(self): self.assertEqual(decoded["regs_l"]["rl_s2"], 3) # testing an illegal - yield from self.test_module.io_in.call(data=0x0) + yield from self.test_module.io_in.call(instr=0x0) decoded = yield from self.test_module.io_out.call() self.assertEqual(decoded["exec_fn"]["op_type"], OpType.EXCEPTION) @@ -73,7 +73,7 @@ def decode_test_proc(self): self.assertEqual(decoded["regs_l"]["rl_s1"], 0) self.assertEqual(decoded["regs_l"]["rl_s2"], 0) - yield from self.test_module.io_in.call(data=0x0, access_fault=1) + yield from self.test_module.io_in.call(instr=0x0, access_fault=1) decoded = yield from self.test_module.io_out.call() self.assertEqual(decoded["exec_fn"]["op_type"], OpType.EXCEPTION) diff --git a/test/frontend/test_fetch.py b/test/frontend/test_fetch.py index 3817c0312..02c9726d5 100644 --- a/test/frontend/test_fetch.py +++ b/test/frontend/test_fetch.py @@ -105,7 +105,7 @@ def cache_process(): self.instr_queue.append( { - "data": data, + "instr": data, "pc": addr, "is_branch": is_branch, "next_pc": next_pc, @@ -134,7 +134,7 @@ def fetch_out_check(self): v = yield from self.m.io_out.call() self.assertEqual(v["pc"], instr["pc"]) - self.assertEqual(v["data"], instr["data"]) + self.assertEqual(v["instr"], instr["instr"]) def test(self): issue_req_mock, accept_res_mock, cache_process = self.cache_processes() diff --git a/test/lsu/test_dummylsu.py b/test/lsu/test_dummylsu.py index bfa29d532..4ac3c059a 100644 --- a/test/lsu/test_dummylsu.py +++ b/test/lsu/test_dummylsu.py @@ -23,7 +23,7 @@ def generate_register(max_reg_val: int, phys_regs_bits: int) -> tuple[int, int, rp = random.randint(1, 2**phys_regs_bits - 1) val = 0 real_val = random.randint(0, max_reg_val // 4) * 4 - ann_data = {"tag": rp, "value": real_val} + ann_data = {"reg_id": rp, "reg_val": real_val} else: rp = 0 val = random.randint(0, max_reg_val // 4) * 4 @@ -338,7 +338,7 @@ def generate_instr(self, max_reg_val, max_imm_val): rp_s2, s2_val, ann_data2, data = generate_register(0xFFFFFFFF, self.gp.phys_regs_bits) if rp_s1 == rp_s2 and ann_data1 is not None and ann_data2 is not None: ann_data2 = None - data = ann_data1["value"] + data = ann_data1["reg_val"] # decide in which order we would get announcments if random.randint(0, 1): self.announce_queue.append((ann_data1, ann_data2)) diff --git a/test/scheduler/test_rs_selection.py b/test/scheduler/test_rs_selection.py index 65c5dac2b..df6d7641f 100644 --- a/test/scheduler/test_rs_selection.py +++ b/test/scheduler/test_rs_selection.py @@ -31,8 +31,8 @@ def elaborate(self, platform): # mocked input and output m.submodules.instr_in = self.instr_in = TestbenchIO(AdapterTrans(instr_fifo.write)) m.submodules.instr_out = self.instr_out = TestbenchIO(AdapterTrans(out_fifo.read)) - m.submodules.rs1_alloc = self.rs1_alloc = TestbenchIO(Adapter(o=rs_layouts.select_out)) - m.submodules.rs2_alloc = self.rs2_alloc = TestbenchIO(Adapter(o=rs_layouts.select_out)) + m.submodules.rs1_alloc = self.rs1_alloc = TestbenchIO(Adapter(o=rs_layouts.rs.select_out)) + m.submodules.rs2_alloc = self.rs2_alloc = TestbenchIO(Adapter(o=rs_layouts.rs.select_out)) # rs selector m.submodules.selector = self.selector = RSSelection( diff --git a/test/scheduler/test_scheduler.py b/test/scheduler/test_scheduler.py index 72c9c5d3d..939cef7bd 100644 --- a/test/scheduler/test_scheduler.py +++ b/test/scheduler/test_scheduler.py @@ -60,8 +60,8 @@ def elaborate(self, platform): # mocked RS for i, rs in enumerate(self.rs): - alloc_adapter = Adapter(o=rs_layouts.select_out) - insert_adapter = Adapter(i=rs_layouts.insert_in) + alloc_adapter = Adapter(o=rs_layouts.rs.select_out) + insert_adapter = Adapter(i=rs_layouts.rs.insert_in) select_test = TestbenchIO(alloc_adapter) insert_test = TestbenchIO(insert_adapter) diff --git a/test/stages/test_backend.py b/test/stages/test_backend.py index b230ca61b..047d70517 100644 --- a/test/stages/test_backend.py +++ b/test/stages/test_backend.py @@ -23,7 +23,7 @@ def elaborate(self, platform): self.lay_result = self.gen.get(FuncUnitLayouts).accept self.lay_rob_mark_done = self.gen.get(ROBLayouts).mark_done_layout - self.lay_rs_write = self.gen.get(RSLayouts, rs_entries_bits=self.gen.max_rs_entries_bits).update_in + self.lay_rs_write = self.gen.get(RSLayouts, rs_entries_bits=self.gen.max_rs_entries_bits).rs.update_in self.lay_rf_write = self.gen.get(RFLayouts).rf_write # Initialize for each FU an FIFO which will be a stub for that FU @@ -59,8 +59,8 @@ def elaborate(self, platform): gen=self.gen, get_result=serialized_results_fifo.read, rob_mark_done=self.rob_mark_done_tbio.adapter.iface, - rs_write_val=self.rs_announce_val_tbio.adapter.iface, - rf_write_val=self.rf_announce_val_tbio.adapter.iface, + rs_update=self.rs_announce_val_tbio.adapter.iface, + rf_write=self.rf_announce_val_tbio.adapter.iface, ) return m diff --git a/test/structs_common/test_csr.py b/test/structs_common/test_csr.py index d1b1e64e4..d7fac6dc9 100644 --- a/test/structs_common/test_csr.py +++ b/test/structs_common/test_csr.py @@ -127,7 +127,7 @@ def process_test(self): yield from self.random_wait() if op["exp"]["rs1"]["rp_s1"]: - yield from self.dut.update.call(tag=op["exp"]["rs1"]["rp_s1"], value=op["exp"]["rs1"]["value"]) + yield from self.dut.update.call(reg_id=op["exp"]["rs1"]["rp_s1"], reg_val=op["exp"]["rs1"]["value"]) yield from self.random_wait() yield from self.dut.precommit.call() diff --git a/test/structs_common/test_rs.py b/test/structs_common/test_rs.py index 9cb678946..3de32528b 100644 --- a/test/structs_common/test_rs.py +++ b/test/structs_common/test_rs.py @@ -206,7 +206,7 @@ def simulation_process(self): # Update second entry first SP, instruction should be not ready value_sp1 = 1010 self.assertEqual((yield self.m.rs.data[1].rec_ready), 0) - yield from self.m.io_update.call(tag=2, value=value_sp1) + yield from self.m.io_update.call(reg_id=2, reg_val=value_sp1) yield Settle() self.assertEqual((yield self.m.rs.data[1].rs_data.rp_s1), 0) self.assertEqual((yield self.m.rs.data[1].rs_data.s1_val), value_sp1) @@ -214,18 +214,18 @@ def simulation_process(self): # Update second entry second SP, instruction should be ready value_sp2 = 2020 - yield from self.m.io_update.call(tag=3, value=value_sp2) + yield from self.m.io_update.call(reg_id=3, reg_val=value_sp2) yield Settle() self.assertEqual((yield self.m.rs.data[1].rs_data.rp_s2), 0) self.assertEqual((yield self.m.rs.data[1].rs_data.s2_val), value_sp2) self.assertEqual((yield self.m.rs.data[1].rec_ready), 1) - # Insert new insturction to entries 0 and 1, check if update of multiple tags works - tag = 4 + # Insert new instruction to entries 0 and 1, check if update of multiple registers works + reg_id = 4 value_spx = 3030 data = { - "rp_s1": tag, - "rp_s2": tag, + "rp_s1": reg_id, + "rp_s2": reg_id, "rp_dst": 1, "rob_id": 12, "exec_fn": { @@ -243,7 +243,7 @@ def simulation_process(self): yield Settle() self.assertEqual((yield self.m.rs.data[index].rec_ready), 0) - yield from self.m.io_update.call(tag=tag, value=value_spx) + yield from self.m.io_update.call(reg_id=reg_id, reg_val=value_spx) yield Settle() for index in range(2): self.assertEqual((yield self.m.rs.data[index].rs_data.rp_s1), 0) @@ -302,9 +302,9 @@ def simulation_process(self): self.assertEqual((yield self.m.rs.take.ready), 0) # Update second instuction and take it - tag = 2 + reg_id = 2 value_spx = 1 - yield from self.m.io_update.call(tag=tag, value=value_spx) + yield from self.m.io_update.call(reg_id=reg_id, reg_val=value_spx) yield Settle() self.assertEqual((yield self.m.rs.take.ready), 1) data = yield from self.m.io_take.call(rs_entry_id=1) @@ -314,11 +314,11 @@ def simulation_process(self): self.assertEqual((yield self.m.rs.take.ready), 0) # Insert two new ready instructions and take them - tag = 0 + reg_id = 0 value_spx = 3030 entry_data = { - "rp_s1": tag, - "rp_s2": tag, + "rp_s1": reg_id, + "rp_s2": reg_id, "rp_dst": 1, "rob_id": 12, "exec_fn": { diff --git a/test/test_core.py b/test/test_core.py index a44d92c12..ed5efb337 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -111,7 +111,7 @@ def get_phys_reg_val(self, reg_id): return (yield self.m.core.RF.entries[reg_id].reg_val) def push_instr(self, opcode): - yield from self.m.io_in.call(data=opcode) + yield from self.m.io_in.call(instr=opcode) def compare_core_states(self, sw_core): for i in range(self.gp.isa.reg_cnt): diff --git a/transactron/utils/_typing.py b/transactron/utils/_typing.py index d12c2611b..f82aba30c 100644 --- a/transactron/utils/_typing.py +++ b/transactron/utils/_typing.py @@ -30,7 +30,8 @@ # Internal Coreblocks types SignalBundle: TypeAlias = Signal | Record | View | Iterable["SignalBundle"] | Mapping[str, "SignalBundle"] -LayoutList: TypeAlias = list[tuple[str, "ShapeLike | LayoutList"]] +LayoutListField: TypeAlias = tuple[str, "ShapeLike | LayoutList"] +LayoutList: TypeAlias = list[LayoutListField] RecordIntDict: TypeAlias = Mapping[str, Union[int, "RecordIntDict"]] RecordIntDictRet: TypeAlias = Mapping[str, Any] # full typing hard to work with diff --git a/transactron/utils/utils.py b/transactron/utils/utils.py index 2f7b78cb6..13491cd7b 100644 --- a/transactron/utils/utils.py +++ b/transactron/utils/utils.py @@ -13,6 +13,7 @@ "assign", "OneHotSwitchDynamic", "OneHotSwitch", + "make_hashable", "flatten_signals", "align_to_power_of_two", "align_down_to_power_of_two", @@ -336,6 +337,15 @@ def layout_subset(layout: LayoutList, *, fields: set[str]) -> LayoutList: return [item for item in layout if item[0] in fields] +def make_hashable(val): + if isinstance(val, Mapping): + return frozenset(((k, make_hashable(v)) for k, v in val.items())) + elif isinstance(val, Iterable): + return (make_hashable(v) for v in val) + else: + return val + + def flatten_signals(signals: SignalBundle) -> Iterable[Signal]: """ Flattens input data, which can be either a signal, a record, a list (or a dict) of SignalBundle items. From 8454fb8a1f180ef090157f723a617f94c26cfb5c Mon Sep 17 00:00:00 2001 From: Julia Matuszewska <47438768+JumiDeluxe@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:56:50 +0100 Subject: [PATCH 02/25] Cache regression tests in workflow (#495) --- .github/workflows/main.yml | 45 +++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 957e2b1c5..41e20abc5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,20 +16,39 @@ jobs: name: Build regression tests runs-on: ubuntu-latest container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.10.08_v + outputs: + cache_hit: ${{ steps.cache-regression.outputs.cache-hit }} steps: - name: Checkout uses: actions/checkout@v3 with: submodules: recursive - - name: Build riscv-tests + - name: Cache regression-tests + id: cache-regression + uses: actions/cache@v3 + env: + cache-name: cache-regression-tests + with: + path: test/external/riscv-tests/test-* + + key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles( + '**/test/external/riscv-tests/environment/**', + '**/test/external/riscv-tests/Makefile', + '**/.git/modules/test/external/riscv-tests/riscv-tests/HEAD', + '**/docker/riscv-toolchain.Dockerfile' + ) }} + restore-keys: | + ${{ env.cache-name }}-${{ runner.os }}- + + - if: ${{ steps.cache-regression.outputs.cache-hit != 'true' }} run: cd test/external/riscv-tests && make - - uses: actions/upload-artifact@v3 + - if: ${{ steps.cache-regression.outputs.cache-hit != 'true' }} + name: Upload riscv-tests + uses: actions/upload-artifact@v3 with: - name: "riscv-tests" - path: | - test/external/riscv-tests/test-* + path: test/external/riscv-tests run-regression-tests: name: Run regression tests @@ -40,6 +59,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + submodules: recursive - name: Set up Python uses: actions/setup-python@v4 @@ -58,10 +79,18 @@ jobs: . venv/bin/activate PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full - - uses: actions/download-artifact@v3 + - uses: actions/cache@v3 + env: + cache-name: cache-regression-tests with: - name: "riscv-tests" - path: test/external/riscv-tests + path: test/external/riscv-tests/test-* + key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles( + '**/test/external/riscv-tests/environment/**', + '**/test/external/riscv-tests/Makefile', + '**/.git/modules/test/external/riscv-tests/riscv-tests/HEAD', + '**/docker/riscv-toolchain.Dockerfile' + ) }} + fail-on-cache-miss: true - name: Run tests run: | From 261e6f6b958aabed7488597398ae6399ca61c3fe Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:56:29 +0100 Subject: [PATCH 03/25] Data dependent method calling (#478) --- test/transactions/test_methods.py | 88 +++++++++++++++++++++++++++++++ transactron/core.py | 62 +++++++++++++++++----- 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/test/transactions/test_methods.py b/test/transactions/test_methods.py index 971eaaa00..df05d259f 100644 --- a/test/transactions/test_methods.py +++ b/test/transactions/test_methods.py @@ -1,3 +1,4 @@ +import random from amaranth import * from amaranth.sim import * @@ -529,3 +530,90 @@ def process(): with self.run_simulation(circ) as sim: sim.add_sync_process(process) + + +class DataDependentConditionalCircuit(Elaboratable): + def __init__(self, n=2, ready_function=lambda arg: arg.data != 3): + self.method = Method(i=data_layout(n)) + self.ready_function = ready_function + + self.in_t1 = Record(data_layout(n)) + self.in_t2 = Record(data_layout(n)) + self.ready = Signal() + self.req_t1 = Signal() + self.req_t2 = Signal() + + self.out_m = Signal() + self.out_t1 = Signal() + self.out_t2 = Signal() + + def elaborate(self, platform): + m = TModule() + + @def_method(m, self.method, self.ready, validate_arguments=self.ready_function) + def _(data): + m.d.comb += self.out_m.eq(1) + + with Transaction().body(m, request=self.req_t1): + m.d.comb += self.out_t1.eq(1) + self.method(m, self.in_t1) + + with Transaction().body(m, request=self.req_t2): + m.d.comb += self.out_t2.eq(1) + self.method(m, self.in_t2) + + return m + + +class TestDataDependentConditionalMethod(TestCaseWithSimulator): + def setUp(self): + self.test_number = 200 + self.bad_number = 3 + self.n = 2 + + def base_random(self, f): + random.seed(14) + self.circ = DataDependentConditionalCircuit(n=self.n, ready_function=f) + + def process(): + for _ in range(self.test_number): + in1 = random.randrange(0, 2**self.n) + in2 = random.randrange(0, 2**self.n) + m_ready = random.randrange(2) + req_t1 = random.randrange(2) + req_t2 = random.randrange(2) + + yield self.circ.in_t1.eq(in1) + yield self.circ.in_t2.eq(in2) + yield self.circ.req_t1.eq(req_t1) + yield self.circ.req_t2.eq(req_t2) + yield self.circ.ready.eq(m_ready) + yield Settle() + yield Delay(1e-8) + + out_m = yield self.circ.out_m + out_t1 = yield self.circ.out_t1 + out_t2 = yield self.circ.out_t2 + + if not m_ready or (not req_t1 or in1 == self.bad_number) and (not req_t2 or in2 == self.bad_number): + self.assertEqual(out_m, 0) + self.assertEqual(out_t1, 0) + self.assertEqual(out_t2, 0) + continue + # Here method global ready signal is high and we requested one of the transactions + # we also know that one of the transactions request correct input data + + self.assertEqual(out_m, 1) + self.assertEqual(out_t1 ^ out_t2, 1) + # inX == self.bad_number implies out_tX==0 + self.assertTrue(in1 != self.bad_number or not out_t1) + self.assertTrue(in2 != self.bad_number or not out_t2) + + with self.run_simulation(self.circ, 100) as sim: + sim.add_process(process) + + def test_random_arg(self): + self.base_random(lambda arg: arg.data != self.bad_number) + + def test_random_kwarg(self): + self.base_random(lambda data: data != self.bad_number) diff --git a/transactron/core.py b/transactron/core.py index a4b3f3b52..ba0f11596 100644 --- a/transactron/core.py +++ b/transactron/core.py @@ -75,15 +75,17 @@ class MethodMap: def __init__(self, transactions: Iterable["Transaction"]): self.methods_by_transaction = dict[Transaction, list[Method]]() self.transactions_by_method = defaultdict[Method, list[Transaction]](list) + self.readiness_by_method_and_transaction = dict[tuple[Transaction, Method], ValueLike]() def rec(transaction: Transaction, source: TransactionBase): - for method in source.method_uses.keys(): + for method, (arg_rec, _) in source.method_uses.items(): if not method.defined: raise RuntimeError(f"Trying to use method '{method.name}' which is not defined yet") if method in self.methods_by_transaction[transaction]: raise RuntimeError(f"Method '{method.name}' can't be called twice from the same transaction") self.methods_by_transaction[transaction].append(method) self.transactions_by_method[method].append(transaction) + self.readiness_by_method_and_transaction[(transaction, method)] = method._validate_arguments(arg_rec) rec(transaction, method) for transaction in transactions: @@ -139,7 +141,10 @@ def eager_deterministic_cc_scheduler( ccl = list(cc) ccl.sort(key=lambda transaction: porder[transaction]) for k, transaction in enumerate(ccl): - ready = [method.ready for method in method_map.methods_by_transaction[transaction]] + ready = [ + method_map.readiness_by_method_and_transaction[(transaction, method)] + for method in method_map.methods_by_transaction[transaction] + ] runnable = Cat(ready).all() conflicts = [ccl[j].grant for j in range(k) if ccl[j] in gr[transaction]] noconflict = ~Cat(conflicts).any() @@ -175,11 +180,11 @@ def trivial_roundrobin_cc_scheduler( sched = Scheduler(len(cc)) m.submodules.scheduler = sched for k, transaction in enumerate(cc): - methods = method_map.methods_by_transaction[transaction] - ready = Signal(len(methods)) - for n, method in enumerate(methods): - m.d.comb += ready[n].eq(method.ready) - runnable = ready.all() + ready = [ + method_map.readiness_by_method_and_transaction[(transaction, method)] + for method in method_map.methods_by_transaction[transaction] + ] + runnable = Cat(ready).all() m.d.comb += sched.requests[k].eq(transaction.request & runnable) m.d.comb += transaction.grant.eq(sched.grant[k] & sched.valid) return m @@ -689,13 +694,13 @@ class TransactionBase(Owned, Protocol): def_order: int defined: bool = False name: str - method_uses: dict["Method", Tuple[ValueLike, ValueLike]] + method_uses: dict["Method", Tuple[Record, ValueLike]] relations: list[RelationBase] simultaneous_list: list[TransactionOrMethod] independent_list: list[TransactionOrMethod] def __init__(self): - self.method_uses: dict["Method", Tuple[ValueLike, ValueLike]] = dict() + self.method_uses: dict["Method", Tuple[Record, ValueLike]] = dict() self.relations: list[RelationBase] = [] self.simultaneous_list: list[TransactionOrMethod] = [] self.independent_list: list[TransactionOrMethod] = [] @@ -735,7 +740,7 @@ def schedule_before(self, end: TransactionOrMethod) -> None: RelationBase(end=end, priority=Priority.LEFT, conflict=False, silence_warning=self.owner != end.owner) ) - def use_method(self, method: "Method", arg: ValueLike, enable: ValueLike): + def use_method(self, method: "Method", arg: Record, enable: ValueLike): if method in self.method_uses: raise RuntimeError(f"Method '{method.name}' can't be called twice from the same transaction '{self.name}'") self.method_uses[method] = (arg, enable) @@ -998,6 +1003,7 @@ def __init__( self.data_out = Record(o) self.nonexclusive = nonexclusive self.single_caller = single_caller + self.validate_arguments: Optional[Callable[..., ValueLike]] = None if nonexclusive: assert len(self.data_in) == 0 @@ -1041,7 +1047,14 @@ def _(arg): return method(m, arg) @contextmanager - def body(self, m: TModule, *, ready: ValueLike = C(1), out: ValueLike = C(0, 0)) -> Iterator[Record]: + def body( + self, + m: TModule, + *, + ready: ValueLike = C(1), + out: ValueLike = C(0, 0), + validate_arguments: Optional[Callable[..., ValueLike]] = None, + ) -> Iterator[Record]: """Define method body The `body` context manager can be used to define the actions @@ -1064,6 +1077,12 @@ def body(self, m: TModule, *, ready: ValueLike = C(1), out: ValueLike = C(0, 0)) Data generated by the `Method`, which will be passed to the caller (a `Transaction` or another `Method`). Assigned combinationally to the `data_out` attribute. + validate_arguments: Optional[Callable[..., ValueLike]] + Function that takes input arguments used to call the method + and checks whether the method can be called with those arguments. + It instantiates a combinational circuit for each + method caller. By default, there is no function, so all arguments + are accepted. Returns ------- @@ -1085,6 +1104,7 @@ def body(self, m: TModule, *, ready: ValueLike = C(1), out: ValueLike = C(0, 0)) if self.defined: raise RuntimeError(f"Method '{self.name}' already defined") self.def_order = next(TransactionBase.def_counter) + self.validate_arguments = validate_arguments try: m.d.av_comb += self.ready.eq(ready) @@ -1095,6 +1115,11 @@ def body(self, m: TModule, *, ready: ValueLike = C(1), out: ValueLike = C(0, 0)) finally: self.defined = True + def _validate_arguments(self, arg_rec: Record) -> ValueLike: + if self.validate_arguments is not None: + return self.ready & method_def_helper(self, self.validate_arguments, arg_rec) + return self.ready + def __call__( self, m: TModule, arg: Optional[RecordDict] = None, enable: ValueLike = C(1), /, **kwargs: RecordDict ) -> Record: @@ -1166,7 +1191,12 @@ def debug_signals(self) -> SignalBundle: return [self.ready, self.run, self.data_in, self.data_out] -def def_method(m: TModule, method: Method, ready: ValueLike = C(1)): +def def_method( + m: TModule, + method: Method, + ready: ValueLike = C(1), + validate_arguments: Optional[Callable[..., ValueLike]] = None, +): """Define a method. This decorator allows to define transactional methods in an @@ -1191,6 +1221,12 @@ def def_method(m: TModule, method: Method, ready: ValueLike = C(1)): Signal to indicate if the method is ready to be run. By default it is `Const(1)`, so the method is always ready. Assigned combinationally to the `ready` attribute. + validate_arguments: Optional[Callable[..., ValueLike]] + Function that takes input arguments used to call the method + and checks whether the method can be called with those arguments. + It instantiates a combinational circuit for each + method caller. By default, there is no function, so all arguments + are accepted. Examples -------- @@ -1226,7 +1262,7 @@ def decorator(func: Callable[..., Optional[RecordDict]]): out = Record.like(method.data_out) ret_out = None - with method.body(m, ready=ready, out=out) as arg: + with method.body(m, ready=ready, out=out, validate_arguments=validate_arguments) as arg: ret_out = method_def_helper(method, func, arg) if ret_out is not None: From bf8b6775e9d159b2af87c8806fa45cfc0a453040 Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Fri, 17 Nov 2023 10:26:41 +0100 Subject: [PATCH 04/25] DummyLSU refactored again (#511) --- coreblocks/lsu/dummyLsu.py | 256 +++++++++++++++++++---------------- coreblocks/params/layouts.py | 17 ++- 2 files changed, 151 insertions(+), 122 deletions(-) diff --git a/coreblocks/lsu/dummyLsu.py b/coreblocks/lsu/dummyLsu.py index d1530dc4c..9047fc542 100644 --- a/coreblocks/lsu/dummyLsu.py +++ b/coreblocks/lsu/dummyLsu.py @@ -3,6 +3,7 @@ from transactron import Method, def_method, Transaction, TModule from coreblocks.params import * from coreblocks.peripherals.wishbone import WishboneMaster +from transactron.lib.connectors import Forwarder from transactron.utils import assign, ModuleLike from coreblocks.utils.protocols import FuncBlock @@ -10,159 +11,139 @@ __all__ = ["LSUDummy", "LSUBlockComponent"] -class LSUDummyInternals(Elaboratable): +class LSURequesterWB(Elaboratable): """ - Internal implementation of `LSUDummy` logic, which should be embedded into `LSUDummy` - class to expose transactional interface. After the instruction is processed, - `result_ready` bit is set to 1. `LSUDummy` is expected to put - `result_ack` high for at least 1 cycle after the results have - been read and can be cleared. + Wishbone request logic for the load/store unit. Its job is to interface + between the LSU and the Wishbone bus. Attributes ---------- - get_result_ack : Signal, in - Instructs to clean the internal state after processing an instruction. - result_ready : Signal, out - Signals that `resultData` is valid. + issue : Method + Issues a new request to the bus. + accept : Method + Retrieves a result from the bus. """ - def __init__(self, gen_params: GenParams, bus: WishboneMaster, current_instr: Record) -> None: + def __init__(self, gen_params: GenParams, bus: WishboneMaster) -> None: """ Parameters ---------- gen_params : GenParams Parameters to be used during processor generation. bus : WishboneMaster - An instance of the Wishbone master for interfacing with the data memory. - current_instr : Record, in - Reference to signal containing instruction currently processed by LSU. + An instance of the Wishbone master for interfacing with the data bus. """ self.gen_params = gen_params - self.current_instr = current_instr self.bus = bus - self.dependency_manager = self.gen_params.get(DependencyManager) - self.report = self.dependency_manager.get_dependency(ExceptionReportKey()) - - self.loadedData = Signal(self.gen_params.isa.xlen) - self.get_result_ack = Signal() - self.result_ready = Signal() - self.execute = Signal() - self.op_exception = Signal() + lsu_layouts = gen_params.get(LSULayouts) - def calculate_addr(self, m: ModuleLike): - addr = Signal(self.gen_params.isa.xlen) - m.d.comb += addr.eq(self.current_instr.s1_val + self.current_instr.imm) - return addr + self.issue = Method(i=lsu_layouts.issue, o=lsu_layouts.issue_out) + self.accept = Method(o=lsu_layouts.accept) - def prepare_bytes_mask(self, m: ModuleLike, addr: Signal) -> Signal: + def prepare_bytes_mask(self, m: ModuleLike, funct3: Value, addr: Value) -> Signal: mask_len = self.gen_params.isa.xlen // self.bus.wb_params.granularity mask = Signal(mask_len) - with m.Switch(self.current_instr.exec_fn.funct3): + with m.Switch(funct3): with m.Case(Funct3.B, Funct3.BU): - m.d.comb += mask.eq(0x1 << addr[0:2]) + m.d.av_comb += mask.eq(0x1 << addr[0:2]) with m.Case(Funct3.H, Funct3.HU): - m.d.comb += mask.eq(0x3 << (addr[1] << 1)) + m.d.av_comb += mask.eq(0x3 << (addr[1] << 1)) with m.Case(Funct3.W): - m.d.comb += mask.eq(0xF) + m.d.av_comb += mask.eq(0xF) return mask - def postprocess_load_data(self, m: ModuleLike, raw_data: Signal, addr: Signal): + def postprocess_load_data(self, m: ModuleLike, funct3: Value, raw_data: Value, addr: Value): data = Signal.like(raw_data) - with m.Switch(self.current_instr.exec_fn.funct3): + with m.Switch(funct3): with m.Case(Funct3.B, Funct3.BU): tmp = Signal(8) - m.d.comb += tmp.eq((raw_data >> (addr[0:2] << 3)) & 0xFF) - with m.If(self.current_instr.exec_fn.funct3 == Funct3.B): - m.d.comb += data.eq(tmp.as_signed()) + m.d.av_comb += tmp.eq((raw_data >> (addr[0:2] << 3)) & 0xFF) + with m.If(funct3 == Funct3.B): + m.d.av_comb += data.eq(tmp.as_signed()) with m.Else(): - m.d.comb += data.eq(tmp) + m.d.av_comb += data.eq(tmp) with m.Case(Funct3.H, Funct3.HU): tmp = Signal(16) - m.d.comb += tmp.eq((raw_data >> (addr[1] << 4)) & 0xFFFF) - with m.If(self.current_instr.exec_fn.funct3 == Funct3.H): - m.d.comb += data.eq(tmp.as_signed()) + m.d.av_comb += tmp.eq((raw_data >> (addr[1] << 4)) & 0xFFFF) + with m.If(funct3 == Funct3.H): + m.d.av_comb += data.eq(tmp.as_signed()) with m.Else(): - m.d.comb += data.eq(tmp) + m.d.av_comb += data.eq(tmp) with m.Case(): - m.d.comb += data.eq(raw_data) + m.d.av_comb += data.eq(raw_data) return data - def prepare_data_to_save(self, m: ModuleLike, raw_data: Signal, addr: Signal): + def prepare_data_to_save(self, m: ModuleLike, funct3: Value, raw_data: Value, addr: Value): data = Signal.like(raw_data) - with m.Switch(self.current_instr.exec_fn.funct3): + with m.Switch(funct3): with m.Case(Funct3.B): - m.d.comb += data.eq(raw_data[0:8] << (addr[0:2] << 3)) + m.d.av_comb += data.eq(raw_data[0:8] << (addr[0:2] << 3)) with m.Case(Funct3.H): - m.d.comb += data.eq(raw_data[0:16] << (addr[1] << 4)) + m.d.av_comb += data.eq(raw_data[0:16] << (addr[1] << 4)) with m.Case(): - m.d.comb += data.eq(raw_data) + m.d.av_comb += data.eq(raw_data) return data - def check_align(self, m: TModule, addr: Signal): + def check_align(self, m: TModule, funct3: Value, addr: Value): aligned = Signal() - with m.Switch(self.current_instr.exec_fn.funct3): + with m.Switch(funct3): with m.Case(Funct3.W): - m.d.comb += aligned.eq(addr[0:2] == 0) + m.d.av_comb += aligned.eq(addr[0:2] == 0) with m.Case(Funct3.H, Funct3.HU): - m.d.comb += aligned.eq(addr[0] == 0) + m.d.av_comb += aligned.eq(addr[0] == 0) with m.Case(): - m.d.comb += aligned.eq(1) + m.d.av_comb += aligned.eq(1) return aligned def elaborate(self, platform): m = TModule() - instr_ready = ( - (self.current_instr.rp_s1 == 0) - & (self.current_instr.rp_s2 == 0) - & self.current_instr.valid - & ~self.result_ready - ) - - is_load = self.current_instr.exec_fn.op_type == OpType.LOAD - - addr = self.calculate_addr(m) - aligned = self.check_align(m, addr) - bytes_mask = self.prepare_bytes_mask(m, addr) - data = self.prepare_data_to_save(m, self.current_instr.s2_val, addr) - - with m.FSM("Start"): - with m.State("Start"): - with m.If(instr_ready & (self.execute | is_load)): - with m.If(aligned): - with Transaction().body(m): - self.bus.request(m, addr=addr >> 2, we=~is_load, sel=bytes_mask, data=data) - m.next = "End" - with m.Else(): - with Transaction().body(m): - m.d.sync += self.op_exception.eq(1) - m.d.sync += self.result_ready.eq(1) - - cause = Mux( - is_load, ExceptionCause.LOAD_ADDRESS_MISALIGNED, ExceptionCause.STORE_ADDRESS_MISALIGNED - ) - self.report(m, rob_id=self.current_instr.rob_id, cause=cause) - - m.next = "End" - - with m.State("End"): - with Transaction().body(m): - fetched = self.bus.result(m) - - m.d.sync += self.loadedData.eq(self.postprocess_load_data(m, fetched.data, addr)) - - with m.If(fetched.err): - cause = Mux(is_load, ExceptionCause.LOAD_ACCESS_FAULT, ExceptionCause.STORE_ACCESS_FAULT) - self.report(m, rob_id=self.current_instr.rob_id, cause=cause) - - m.d.sync += self.op_exception.eq(fetched.err) - m.d.sync += self.result_ready.eq(1) - - with m.If(self.get_result_ack): - m.d.sync += self.result_ready.eq(0) - m.d.sync += self.op_exception.eq(0) - m.next = "Start" + addr_reg = Signal(self.gen_params.isa.xlen) + funct3_reg = Signal(Funct3) + store_reg = Signal() + request_sent = Signal() + + @def_method(m, self.issue, ~request_sent) + def _(addr: Value, data: Value, funct3: Value, store: Value): + exception = Signal() + cause = Signal(ExceptionCause) + + aligned = self.check_align(m, funct3, addr) + bytes_mask = self.prepare_bytes_mask(m, funct3, addr) + wb_data = self.prepare_data_to_save(m, funct3, data, addr) + + with m.If(aligned): + self.bus.request(m, addr=addr >> 2, we=store, sel=bytes_mask, data=wb_data) + m.d.sync += request_sent.eq(1) + m.d.sync += addr_reg.eq(addr) + m.d.sync += funct3_reg.eq(funct3) + m.d.sync += store_reg.eq(store) + with m.Else(): + m.d.av_comb += exception.eq(1) + m.d.av_comb += cause.eq( + Mux(store, ExceptionCause.STORE_ADDRESS_MISALIGNED, ExceptionCause.LOAD_ADDRESS_MISALIGNED) + ) + + return {"exception": exception, "cause": cause} + + @def_method(m, self.accept, request_sent) + def _(): + exception = Signal() + cause = Signal(ExceptionCause) + + fetched = self.bus.result(m) + m.d.sync += request_sent.eq(0) + + data = self.postprocess_load_data(m, funct3_reg, fetched.data, addr_reg) + + with m.If(fetched.err): + m.d.av_comb += exception.eq(1) + m.d.av_comb += cause.eq( + Mux(store_reg, ExceptionCause.STORE_ACCESS_FAULT, ExceptionCause.LOAD_ACCESS_FAULT) + ) + + return {"data": data, "exception": exception, "cause": cause} return m @@ -197,13 +178,16 @@ def __init__(self, gen_params: GenParams, bus: WishboneMaster) -> None: gen_params : GenParams Parameters to be used during processor generation. bus : WishboneMaster - An instance of the Wishbone master for interfacing with the data memory. + An instance of the Wishbone master for interfacing with the data bus. """ self.gen_params = gen_params self.fu_layouts = gen_params.get(FuncUnitLayouts) self.lsu_layouts = gen_params.get(LSULayouts) + dependency_manager = self.gen_params.get(DependencyManager) + self.report = dependency_manager.get_dependency(ExceptionReportKey()) + self.insert = Method(i=self.lsu_layouts.rs.insert_in) self.select = Method(o=self.lsu_layouts.rs.select_out) self.update = Method(i=self.lsu_layouts.rs.update_in) @@ -214,12 +198,23 @@ def __init__(self, gen_params: GenParams, bus: WishboneMaster) -> None: def elaborate(self, platform): m = TModule() - reserved = Signal() # means that current_instr is reserved - current_instr = Record(self.lsu_layouts.rs.data_layout + [("valid", 1)]) + reserved = Signal() # current_instr is reserved + valid = Signal() # current_instr is valid + execute = Signal() # start execution + issued = Signal() # instruction was issued to the bus + current_instr = Record(self.lsu_layouts.rs.data_layout) + + m.submodules.requester = requester = LSURequesterWB(self.gen_params, self.bus) - m.submodules.internal = internal = LSUDummyInternals(self.gen_params, self.bus, current_instr) + m.submodules.results = results = self.forwarder = Forwarder(self.lsu_layouts.accept) - result_ready = internal.result_ready | ((current_instr.exec_fn.op_type == OpType.FENCE) & current_instr.valid) + instr_ready = (current_instr.rp_s1 == 0) & (current_instr.rp_s2 == 0) & valid + + instr_is_fence = Signal() + m.d.comb += instr_is_fence.eq(current_instr.exec_fn.op_type == OpType.FENCE) + + instr_is_load = Signal() + m.d.comb += instr_is_load.eq(current_instr.exec_fn.op_type == OpType.LOAD) @def_method(m, self.select, ~reserved) def _(): @@ -230,7 +225,7 @@ def _(): @def_method(m, self.insert) def _(rs_data: Record, rs_entry_id: Value): m.d.sync += assign(current_instr, rs_data) - m.d.sync += current_instr.valid.eq(1) + m.d.sync += valid.eq(1) @def_method(m, self.update) def _(reg_id: Value, reg_val: Value): @@ -241,26 +236,51 @@ def _(reg_id: Value, reg_val: Value): m.d.sync += current_instr.s2_val.eq(reg_val) m.d.sync += current_instr.rp_s2.eq(0) - @def_method(m, self.get_result, result_ready) + # Issues load/store requests when the instruction is known, is a LOAD/STORE, and just before commit. + # Memory loads can be issued speculatively. + with Transaction().body(m, request=instr_ready & ~issued & ~instr_is_fence & (execute | instr_is_load)): + m.d.sync += issued.eq(1) + res = requester.issue( + m, + addr=current_instr.s1_val + current_instr.imm, + data=current_instr.s2_val, + funct3=current_instr.exec_fn.funct3, + store=~instr_is_load, + ) + with m.If(res["exception"]): + results.write(m, data=0, exception=res["exception"], cause=res["cause"]) + + # Handles FENCE as a no-op. + with Transaction().body(m, request=instr_ready & ~issued & instr_is_fence): + m.d.sync += issued.eq(1) + results.write(m, data=0, exception=0, cause=0) + + with Transaction().body(m): + res = requester.accept(m) # can happen only after issue + results.write(m, res) + + @def_method(m, self.get_result) def _(): - m.d.comb += internal.get_result_ack.eq(1) + res = results.read(m) + + with m.If(res["exception"]): + self.report(m, rob_id=current_instr.rob_id, cause=res["cause"]) - m.d.sync += current_instr.eq(0) + m.d.sync += issued.eq(0) + m.d.sync += valid.eq(0) m.d.sync += reserved.eq(0) return { "rob_id": current_instr.rob_id, "rp_dst": current_instr.rp_dst, - "result": internal.loadedData, - "exception": internal.op_exception, + "result": res["data"], + "exception": res["exception"], } @def_method(m, self.precommit) def _(rob_id: Value): - with m.If( - current_instr.valid & (rob_id == current_instr.rob_id) & (current_instr.exec_fn.op_type != OpType.FENCE) - ): - m.d.comb += internal.execute.eq(1) + with m.If(valid & (rob_id == current_instr.rob_id) & ~instr_is_fence): + m.d.comb += execute.eq(1) return m diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index 6df1970b7..5952889eb 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -100,6 +100,9 @@ def __init__(self, gen_params: GenParams): self.exception: LayoutListField = ("exception", 1) """Exception is raised for this instruction.""" + self.cause: LayoutListField = ("cause", ExceptionCause) + """Exception cause.""" + self.error: LayoutListField = ("error", 1) """Request ended with an error.""" @@ -497,6 +500,7 @@ class LSULayouts: """Layouts used in the load-store unit.""" def __init__(self, gen_params: GenParams): + fields = gen_params.get(CommonLayoutFields) data = gen_params.get(RSFullDataLayout) data_layout = layout_subset( @@ -521,6 +525,14 @@ def __init__(self, gen_params: GenParams): self.precommit = retirement.precommit + self.store: LayoutListField = ("store", 1) + + self.issue: LayoutList = [fields.addr, fields.data, fields.funct3, self.store] + + self.issue_out: LayoutList = [fields.exception, fields.cause] + + self.accept: LayoutList = [fields.data, fields.exception, fields.cause] + class CSRLayouts: """Layouts used in the control and status registers.""" @@ -570,11 +582,8 @@ class ExceptionRegisterLayouts: def __init__(self, gen_params: GenParams): fields = gen_params.get(CommonLayoutFields) - self.cause: LayoutListField = ("cause", ExceptionCause) - """Exception cause.""" - self.get: LayoutList = [ - self.cause, + fields.cause, fields.rob_id, ] From 53c03fb6803eac541a64a45bf75a74721e530167 Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Sat, 18 Nov 2023 17:28:37 +0100 Subject: [PATCH 05/25] retirement: Disable side effects on exception (proposed corrections) (#512) Co-authored-by: Arusekk --- coreblocks/core.py | 1 + coreblocks/lsu/dummyLsu.py | 15 +++++++---- coreblocks/params/layouts.py | 7 +++-- coreblocks/stages/retirement.py | 46 ++++++++++++++++++++++++++------ coreblocks/structs_common/csr.py | 20 +++++++++----- coreblocks/structs_common/rat.py | 5 ++-- test/asm/exception.asm | 5 ++++ test/asm/exception_mem.asm | 11 ++++++++ test/lsu/test_dummylsu.py | 2 +- test/stages/test_retirement.py | 6 +++-- test/structs_common/test_csr.py | 4 +-- test/test_core.py | 2 ++ 12 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 test/asm/exception.asm create mode 100644 test/asm/exception_mem.asm diff --git a/coreblocks/core.py b/coreblocks/core.py index 2adcbbe53..b0c6257db 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -136,6 +136,7 @@ def elaborate(self, platform): rf_free=rf.free, precommit=self.func_blocks_unifier.get_extra_method(InstructionPrecommitKey()), exception_cause_get=self.exception_cause_register.get, + frat_rename=frat.rename, ) m.submodules.csr_generic = self.csr_generic diff --git a/coreblocks/lsu/dummyLsu.py b/coreblocks/lsu/dummyLsu.py index 9047fc542..562c76988 100644 --- a/coreblocks/lsu/dummyLsu.py +++ b/coreblocks/lsu/dummyLsu.py @@ -202,6 +202,7 @@ def elaborate(self, platform): valid = Signal() # current_instr is valid execute = Signal() # start execution issued = Signal() # instruction was issued to the bus + flush = Signal() # exception handling, requests are not issued current_instr = Record(self.lsu_layouts.rs.data_layout) m.submodules.requester = requester = LSURequesterWB(self.gen_params, self.bus) @@ -238,7 +239,8 @@ def _(reg_id: Value, reg_val: Value): # Issues load/store requests when the instruction is known, is a LOAD/STORE, and just before commit. # Memory loads can be issued speculatively. - with Transaction().body(m, request=instr_ready & ~issued & ~instr_is_fence & (execute | instr_is_load)): + do_issue = ~issued & instr_ready & ~flush & ~instr_is_fence & (execute | instr_is_load) + with Transaction().body(m, request=do_issue): m.d.sync += issued.eq(1) res = requester.issue( m, @@ -250,8 +252,9 @@ def _(reg_id: Value, reg_val: Value): with m.If(res["exception"]): results.write(m, data=0, exception=res["exception"], cause=res["cause"]) - # Handles FENCE as a no-op. - with Transaction().body(m, request=instr_ready & ~issued & instr_is_fence): + # Handles FENCE and flushed instructions as a no-op. + do_skip = ~issued & (instr_ready & ~flush & instr_is_fence | valid & flush) + with Transaction().body(m, request=do_skip): m.d.sync += issued.eq(1) results.write(m, data=0, exception=0, cause=0) @@ -278,8 +281,10 @@ def _(): } @def_method(m, self.precommit) - def _(rob_id: Value): - with m.If(valid & (rob_id == current_instr.rob_id) & ~instr_is_fence): + def _(rob_id: Value, side_fx: Value): + with m.If(~side_fx): + m.d.comb += flush.eq(1) + with m.Elif(valid & (rob_id == current_instr.rob_id) & ~instr_is_fence): m.d.comb += execute.eq(1) return m diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index 5952889eb..7e13b74b2 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -106,6 +106,9 @@ def __init__(self, gen_params: GenParams): self.error: LayoutListField = ("error", 1) """Request ended with an error.""" + self.side_fx: LayoutListField = ("side_fx", 1) + """Side effects are enabled.""" + class SchedulerLayouts: """Layouts used in the scheduler.""" @@ -221,7 +224,7 @@ def __init__(self, gen_params: GenParams): self.rat_rename_out: LayoutList = [fields.rp_s1, fields.rp_s2] - self.rat_commit_in: LayoutList = [fields.rl_dst, fields.rp_dst] + self.rat_commit_in: LayoutList = [fields.rl_dst, fields.rp_dst, fields.side_fx] self.rat_commit_out: LayoutList = [self.old_rp_dst] @@ -328,7 +331,7 @@ class RetirementLayouts: def __init__(self, gen_params: GenParams): fields = gen_params.get(CommonLayoutFields) - self.precommit: LayoutList = [fields.rob_id] + self.precommit: LayoutList = [fields.rob_id, fields.side_fx] class RSLayouts: diff --git a/coreblocks/stages/retirement.py b/coreblocks/stages/retirement.py index f8f4aeac7..28ac8983f 100644 --- a/coreblocks/stages/retirement.py +++ b/coreblocks/stages/retirement.py @@ -1,10 +1,12 @@ from amaranth import * from coreblocks.params.dependencies import DependencyManager from coreblocks.params.keys import GenericCSRRegistersKey +from coreblocks.params.layouts import CommonLayoutFields from transactron.core import Method, Transaction, TModule from coreblocks.params.genparams import GenParams from coreblocks.structs_common.csr_generic import CSRAddress, DoubleCounterCSR +from transactron.lib.connectors import Forwarder class Retirement(Elaboratable): @@ -18,7 +20,8 @@ def __init__( free_rf_put: Method, rf_free: Method, precommit: Method, - exception_cause_get: Method + exception_cause_get: Method, + frat_rename: Method, ): self.gen_params = gen_params self.rob_peek = rob_peek @@ -28,6 +31,7 @@ def __init__( self.rf_free = rf_free self.precommit = precommit self.exception_cause_get = exception_cause_get + self.rename = frat_rename self.instret_csr = DoubleCounterCSR(gen_params, CSRAddress.INSTRET, CSRAddress.INSTRETH) @@ -36,18 +40,30 @@ def elaborate(self, platform): m.submodules.instret_csr = self.instret_csr + side_fx = Signal(reset=1) + side_fx_comb = Signal() + + m.d.comb += side_fx_comb.eq(side_fx) + + fields = self.gen_params.get(CommonLayoutFields) + m.submodules.frat_fix = frat_fix = Forwarder([fields.rl_dst, fields.rp_dst]) + with Transaction().body(m): # TODO: do we prefer single precommit call per instruction? # If so, the precommit method should send an acknowledge signal here. # Just calling once is not enough, because the insn might not be in relevant unit yet. rob_entry = self.rob_peek(m) - self.precommit(m, rob_id=rob_entry.rob_id) + self.precommit(m, rob_id=rob_entry.rob_id, side_fx=side_fx) with Transaction().body(m): rob_entry = self.rob_retire(m) # TODO: Trigger InterruptCoordinator (handle exception) when rob_entry.exception is set. - with m.If(rob_entry.exception): + with m.If(rob_entry.exception & side_fx): + m.d.sync += side_fx.eq(0) + m.d.comb += side_fx_comb.eq(0) + # TODO: only set mcause/trigger IC if cause is actual exception and not e.g. + # misprediction or pipeline flush after some fence.i or changing ISA mcause = self.gen_params.get(DependencyManager).get_dependency(GenericCSRRegistersKey()).mcause cause = self.exception_cause_get(m).cause entry = Signal(self.gen_params.isa.xlen) @@ -56,14 +72,28 @@ def elaborate(self, platform): mcause.write(m, entry) # set rl_dst -> rp_dst in R-RAT - rat_out = self.r_rat_commit(m, rl_dst=rob_entry.rob_data.rl_dst, rp_dst=rob_entry.rob_data.rp_dst) + rat_out = self.r_rat_commit( + m, rl_dst=rob_entry.rob_data.rl_dst, rp_dst=rob_entry.rob_data.rp_dst, side_fx=side_fx + ) - self.rf_free(m, rat_out.old_rp_dst) + rp_freed = Signal(self.gen_params.phys_regs_bits) + with m.If(side_fx_comb): + m.d.av_comb += rp_freed.eq(rat_out.old_rp_dst) + self.instret_csr.increment(m) + with m.Else(): + m.d.av_comb += rp_freed.eq(rob_entry.rob_data.rp_dst) + # free the phys_reg with computed value and restore old reg into FRAT as well + # FRAT calls are in a separate transaction to avoid locking the rename method + frat_fix.write(m, rl_dst=rob_entry.rob_data.rl_dst, rp_dst=rat_out.old_rp_dst) + + self.rf_free(m, rp_freed) # put old rp_dst to free RF list - with m.If(rat_out.old_rp_dst): # don't put rp0 to free list - reserved to no-return instructions - self.free_rf_put(m, rat_out.old_rp_dst) + with m.If(rp_freed): # don't put rp0 to free list - reserved to no-return instructions + self.free_rf_put(m, rp_freed) - self.instret_csr.increment(m) + with Transaction().body(m): + data = frat_fix.read(m) + self.rename(m, rl_s1=0, rl_s2=0, rl_dst=data["rl_dst"], rp_dst=data["rp_dst"]) return m diff --git a/coreblocks/structs_common/csr.py b/coreblocks/structs_common/csr.py index f1a6d89f1..d7d5419f2 100644 --- a/coreblocks/structs_common/csr.py +++ b/coreblocks/structs_common/csr.py @@ -219,13 +219,13 @@ def elaborate(self, platform): done = Signal() accepted = Signal() exception = Signal() - rob_sfx_empty = Signal() + precommitting = Signal() current_result = Signal(self.gen_params.isa.xlen) instr = Record(self.csr_layouts.rs.data_layout + [("valid", 1)]) - m.d.comb += ready_to_process.eq(rob_sfx_empty & instr.valid & (instr.rp_s1 == 0)) + m.d.comb += ready_to_process.eq(precommitting & instr.valid & (instr.rp_s1 == 0)) # RISCV Zicsr spec Table 1.1 should_read_csr = Signal() @@ -251,6 +251,8 @@ def elaborate(self, platform): # Temporary, until privileged spec is implemented priv_level = Signal(PrivilegeLevel, reset=PrivilegeLevel.MACHINE) + exe_side_fx = Signal() + # Methods used within this Tranaction are CSRRegister internal _fu_(read|write) handlers which are always ready with Transaction().body(m, request=(ready_to_process & ~done)): with m.Switch(instr.csr): @@ -266,7 +268,8 @@ def elaborate(self, platform): with m.If(priv_valid): read_val = Signal(self.gen_params.isa.xlen) with m.If(should_read_csr & ~done): - m.d.comb += read_val.eq(read(m)) + with m.If(exe_side_fx): + m.d.comb += read_val.eq(read(m)) m.d.sync += current_result.eq(read_val) if read_only: @@ -283,7 +286,8 @@ def elaborate(self, platform): m.d.comb += write_val.eq(read_val | instr.s1_val) with m.Case(Funct3.CSRRC, Funct3.CSRRCI): m.d.comb += write_val.eq(read_val & (~instr.s1_val)) - write(m, write_val) + with m.If(exe_side_fx): + write(m, write_val) with m.Else(): # Missing privilege @@ -338,10 +342,12 @@ def _(): def _(): return {"from_pc": instr.pc, "next_pc": instr.pc + self.gen_params.isa.ilen_bytes} - # Generate rob_sfx_empty signal from precommit + # Generate precommitting signal from precommit @def_method(m, self.precommit) - def _(rob_id): - m.d.comb += rob_sfx_empty.eq(instr.rob_id == rob_id) + def _(rob_id: Value, side_fx: Value): + with m.If(instr.rob_id == rob_id): + m.d.comb += precommitting.eq(1) + m.d.comb += exe_side_fx.eq(side_fx) return m diff --git a/coreblocks/structs_common/rat.py b/coreblocks/structs_common/rat.py index 4902c1b99..1ed3f1a8f 100644 --- a/coreblocks/structs_common/rat.py +++ b/coreblocks/structs_common/rat.py @@ -42,8 +42,9 @@ def elaborate(self, platform): m = TModule() @def_method(m, self.commit) - def _(rp_dst: Value, rl_dst: Value): - m.d.sync += self.entries[rl_dst].eq(rp_dst) + def _(rp_dst: Value, rl_dst: Value, side_fx: Value): + with m.If(side_fx): + m.d.sync += self.entries[rl_dst].eq(rp_dst) return {"old_rp_dst": self.entries[rl_dst]} return m diff --git a/test/asm/exception.asm b/test/asm/exception.asm new file mode 100644 index 000000000..e3840fc8a --- /dev/null +++ b/test/asm/exception.asm @@ -0,0 +1,5 @@ +li x2, 2 +li x1, 1 +.4byte 0 /* should be unimp, but it would test nothing since unimp is system and stalls the fetcher >:( */ +li x2, 9 + diff --git a/test/asm/exception_mem.asm b/test/asm/exception_mem.asm new file mode 100644 index 000000000..c3556e795 --- /dev/null +++ b/test/asm/exception_mem.asm @@ -0,0 +1,11 @@ +# Data adress space: +# 0x0 - one +# 0x4 - two +li x1, 1 +sw x1, 0(x0) +li x2, 2 +sw x2, 4(x0) +.4byte 0 /* should be unimp, but it would test nothing since unimp is system and stalls the fetcher >:( */ +sw x1, 4(x0) /* TODO: actually check the side fx */ +li x2, 9 + diff --git a/test/lsu/test_dummylsu.py b/test/lsu/test_dummylsu.py index 4ac3c059a..a04037be8 100644 --- a/test/lsu/test_dummylsu.py +++ b/test/lsu/test_dummylsu.py @@ -431,7 +431,7 @@ def precommiter(self): while len(self.precommit_data) == 0: yield rob_id = self.precommit_data[-1] # precommit is called continously until instruction is retired - yield from self.test_module.precommit.call(rob_id=rob_id) + yield from self.test_module.precommit.call(rob_id=rob_id, side_fx=1) def test(self): @def_method_mock(lambda: self.test_module.exception_report) diff --git a/test/stages/test_retirement.py b/test/stages/test_retirement.py index 05f4cd411..30687a77c 100644 --- a/test/stages/test_retirement.py +++ b/test/stages/test_retirement.py @@ -3,7 +3,7 @@ from coreblocks.structs_common.csr_generic import GenericCSRRegisters from transactron.lib import FIFO, Adapter -from coreblocks.structs_common.rat import RRAT +from coreblocks.structs_common.rat import FRAT, RRAT from coreblocks.params import ROBLayouts, RFLayouts, GenParams, LSULayouts, SchedulerLayouts from coreblocks.params.configurations import test_core_config @@ -26,6 +26,7 @@ def elaborate(self, platform): exception_layouts = self.gen_params.get(ExceptionRegisterLayouts) m.submodules.r_rat = self.rat = RRAT(gen_params=self.gen_params) + m.submodules.f_rat = self.frat = FRAT(gen_params=self.gen_params) m.submodules.free_rf_list = self.free_rf = FIFO( scheduler_layouts.free_rf_layout, 2**self.gen_params.phys_regs_bits ) @@ -51,6 +52,7 @@ def elaborate(self, platform): rf_free=self.mock_rf_free.adapter.iface, precommit=self.mock_precommit.adapter.iface, exception_cause_get=self.mock_exception_cause.adapter.iface, + frat_rename=self.frat.rename, ) m.submodules.free_rf_fifo_adapter = self.free_rf_adapter = TestbenchIO(AdapterTrans(self.free_rf.read)) @@ -123,7 +125,7 @@ def rf_free_process(reg_id): self.assertEqual(reg_id, self.rf_free_q.popleft()) @def_method_mock(lambda: retc.mock_precommit, sched_prio=2) - def precommit_process(rob_id): + def precommit_process(rob_id, side_fx): self.assertEqual(rob_id, self.precommit_q.popleft()) @def_method_mock(lambda: retc.mock_exception_cause) diff --git a/test/structs_common/test_csr.py b/test/structs_common/test_csr.py index d7fac6dc9..ffcdaf74a 100644 --- a/test/structs_common/test_csr.py +++ b/test/structs_common/test_csr.py @@ -130,7 +130,7 @@ def process_test(self): yield from self.dut.update.call(reg_id=op["exp"]["rs1"]["rp_s1"], reg_val=op["exp"]["rs1"]["value"]) yield from self.random_wait() - yield from self.dut.precommit.call() + yield from self.dut.precommit.call(side_fx=1) yield from self.random_wait() res = yield from self.dut.accept.call() @@ -183,7 +183,7 @@ def process_exception_test(self): ) yield from self.random_wait() - yield from self.dut.precommit.call(rob_id=rob_id) + yield from self.dut.precommit.call(rob_id=rob_id, side_fx=1) yield from self.random_wait() res = yield from self.dut.accept.call() diff --git a/test/test_core.py b/test/test_core.py index ed5efb337..f5a1d7a1c 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -243,6 +243,8 @@ def test_randomized(self): ("fibonacci", "fibonacci.asm", 1200, {2: 2971215073}, basic_core_config), ("fibonacci_mem", "fibonacci_mem.asm", 610, {3: 55}, basic_core_config), ("csr", "csr.asm", 200, {1: 1, 2: 4}, full_core_config), + ("exception", "exception.asm", 200, {1: 1, 2: 2}, basic_core_config), + ("exception_mem", "exception_mem.asm", 200, {1: 1, 2: 2}, basic_core_config), ], ) class TestCoreAsmSource(TestCoreBase): From 41d1b8cfea22a6af4a3714a0b79ac1b96ff343cb Mon Sep 17 00:00:00 2001 From: Julia Matuszewska <47438768+JumiDeluxe@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:07:40 +0100 Subject: [PATCH 06/25] RAT tests (#520) --- test/structs_common/test_rat.py | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 test/structs_common/test_rat.py diff --git a/test/structs_common/test_rat.py b/test/structs_common/test_rat.py new file mode 100644 index 000000000..2119a9ced --- /dev/null +++ b/test/structs_common/test_rat.py @@ -0,0 +1,85 @@ +from ..common import TestCaseWithSimulator, SimpleTestCircuit + +from coreblocks.structs_common.rat import FRAT, RRAT +from coreblocks.params import GenParams +from coreblocks.params.configurations import test_core_config + +from collections import deque +from random import Random + + +class TestFrontendRegisterAliasTable(TestCaseWithSimulator): + def gen_input(self): + for _ in range(self.test_steps): + rl = self.rand.randrange(self.gen_params.isa.reg_cnt) + rp = self.rand.randrange(1, 2**self.gen_params.phys_regs_bits) if rl != 0 else 0 + rl_s1 = self.rand.randrange(self.gen_params.isa.reg_cnt) + rl_s2 = self.rand.randrange(self.gen_params.isa.reg_cnt) + + self.to_execute_list.append({"rl": rl, "rp": rp, "rl_s1": rl_s1, "rl_s2": rl_s2}) + + def do_rename(self): + for _ in range(self.test_steps): + to_execute = self.to_execute_list.pop() + res = yield from self.m.rename.call( + rl_dst=to_execute["rl"], rp_dst=to_execute["rp"], rl_s1=to_execute["rl_s1"], rl_s2=to_execute["rl_s2"] + ) + self.assertEqual(res["rp_s1"], self.expected_entries[to_execute["rl_s1"]]) + self.assertEqual(res["rp_s2"], self.expected_entries[to_execute["rl_s2"]]) + + self.expected_entries[to_execute["rl"]] = to_execute["rp"] + + def test_single(self): + self.rand = Random(0) + self.test_steps = 2000 + self.gen_params = GenParams(test_core_config.replace(phys_regs_bits=5, rob_entries_bits=6)) + m = SimpleTestCircuit(FRAT(gen_params=self.gen_params)) + self.m = m + + self.log_regs = self.gen_params.isa.reg_cnt + self.phys_regs = 2**self.gen_params.phys_regs_bits + + self.to_execute_list = deque() + self.expected_entries = [0 for _ in range(self.log_regs)] + + self.gen_input() + with self.run_simulation(m) as sim: + sim.add_sync_process(self.do_rename) + + +class TestRetirementRegisterAliasTable(TestCaseWithSimulator): + def gen_input(self): + for _ in range(self.test_steps): + rl = self.rand.randrange(self.gen_params.isa.reg_cnt) + rp = self.rand.randrange(1, 2**self.gen_params.phys_regs_bits) if rl != 0 else 0 + side_fx = self.rand.randrange(0, 2) + + self.to_execute_list.append({"rl": rl, "rp": rp, "side_fx": side_fx}) + + def do_commit(self): + for _ in range(self.test_steps): + to_execute = self.to_execute_list.pop() + res = yield from self.m.commit.call( + rl_dst=to_execute["rl"], rp_dst=to_execute["rp"], side_fx=to_execute["side_fx"] + ) + self.assertEqual(res["old_rp_dst"], self.expected_entries[to_execute["rl"]]) + + if to_execute["side_fx"]: + self.expected_entries[to_execute["rl"]] = to_execute["rp"] + + def test_single(self): + self.rand = Random(0) + self.test_steps = 2000 + self.gen_params = GenParams(test_core_config.replace(phys_regs_bits=5, rob_entries_bits=6)) + m = SimpleTestCircuit(RRAT(gen_params=self.gen_params)) + self.m = m + + self.log_regs = self.gen_params.isa.reg_cnt + self.phys_regs = 2**self.gen_params.phys_regs_bits + + self.to_execute_list = deque() + self.expected_entries = [0 for _ in range(self.log_regs)] + + self.gen_input() + with self.run_simulation(m) as sim: + sim.add_sync_process(self.do_commit) From 964065c29bd937475cc1bf7d5969e167560b8456 Mon Sep 17 00:00:00 2001 From: JumiDeluxe <47438768+JumiDeluxe@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:46:45 +0100 Subject: [PATCH 07/25] Transaction graph and docs cleanup (#521) --- docs/Current_graph.md | 5 ----- docs/{Assumptions.md => assumptions.md} | 0 docs/current-graph.md | 12 ++++++++++ ...ironment.md => development-environment.md} | 0 docs/{Home.md => home.md} | 0 docs/index.md | 22 +++++++++---------- ...ptionsSummary.md => exceptions-summary.md} | 0 ...blem-checklist.md => problem-checklist.md} | 0 docs/scheduler/{Overview.md => overview.md} | 0 .../implementation/rs-impl.md} | 0 .../RS.md => shared-structs/rs.md} | 0 docs/synthesis/{Synthesis.md => synthesis.md} | 0 docs/{Transactions.md => transactions.md} | 0 13 files changed, 23 insertions(+), 16 deletions(-) delete mode 100644 docs/Current_graph.md rename docs/{Assumptions.md => assumptions.md} (100%) create mode 100644 docs/current-graph.md rename docs/{Development_environment.md => development-environment.md} (100%) rename docs/{Home.md => home.md} (100%) rename docs/miscellany/{exceptionsSummary.md => exceptions-summary.md} (100%) rename docs/{Problem-checklist.md => problem-checklist.md} (100%) rename docs/scheduler/{Overview.md => overview.md} (100%) rename docs/{shared_structs/Implementation/RS_impl.md => shared-structs/implementation/rs-impl.md} (100%) rename docs/{shared_structs/RS.md => shared-structs/rs.md} (100%) rename docs/synthesis/{Synthesis.md => synthesis.md} (100%) rename docs/{Transactions.md => transactions.md} (100%) diff --git a/docs/Current_graph.md b/docs/Current_graph.md deleted file mode 100644 index 1d64977b4..000000000 --- a/docs/Current_graph.md +++ /dev/null @@ -1,5 +0,0 @@ -# Full transaction-method graph - -```{eval-rst} -.. include:: auto_graph.rst -``` diff --git a/docs/Assumptions.md b/docs/assumptions.md similarity index 100% rename from docs/Assumptions.md rename to docs/assumptions.md diff --git a/docs/current-graph.md b/docs/current-graph.md new file mode 100644 index 000000000..c176682f2 --- /dev/null +++ b/docs/current-graph.md @@ -0,0 +1,12 @@ +# Full transaction-method graph + +
+
+ + ```{eval-rst} + .. include:: auto_graph.rst + + ``` + +
+
diff --git a/docs/Development_environment.md b/docs/development-environment.md similarity index 100% rename from docs/Development_environment.md rename to docs/development-environment.md diff --git a/docs/Home.md b/docs/home.md similarity index 100% rename from docs/Home.md rename to docs/home.md diff --git a/docs/index.md b/docs/index.md index 85d7b3e2c..0e16a25ec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,17 +5,17 @@ maxdepth: 3 --- -Home.md -Assumptions.md -Development_environment.md -Transactions.md -scheduler/Overview.md -shared_structs/Implementation/RS_impl.md -shared_structs/RS.md -Current_graph.md -Problem-checklist.md -synthesis/Synthesis.md +home.md +assumptions.md +development-environment.md +transactions.md +scheduler/overview.md +shared-structs/implementation/rs-impl.md +shared-structs/rs.md +current-graph.md +problem-checklist.md +synthesis/synthesis.md components/icache.md -miscellany/exceptionsSummary.md +miscellany/exceptions-summary.md api.md ``` diff --git a/docs/miscellany/exceptionsSummary.md b/docs/miscellany/exceptions-summary.md similarity index 100% rename from docs/miscellany/exceptionsSummary.md rename to docs/miscellany/exceptions-summary.md diff --git a/docs/Problem-checklist.md b/docs/problem-checklist.md similarity index 100% rename from docs/Problem-checklist.md rename to docs/problem-checklist.md diff --git a/docs/scheduler/Overview.md b/docs/scheduler/overview.md similarity index 100% rename from docs/scheduler/Overview.md rename to docs/scheduler/overview.md diff --git a/docs/shared_structs/Implementation/RS_impl.md b/docs/shared-structs/implementation/rs-impl.md similarity index 100% rename from docs/shared_structs/Implementation/RS_impl.md rename to docs/shared-structs/implementation/rs-impl.md diff --git a/docs/shared_structs/RS.md b/docs/shared-structs/rs.md similarity index 100% rename from docs/shared_structs/RS.md rename to docs/shared-structs/rs.md diff --git a/docs/synthesis/Synthesis.md b/docs/synthesis/synthesis.md similarity index 100% rename from docs/synthesis/Synthesis.md rename to docs/synthesis/synthesis.md diff --git a/docs/Transactions.md b/docs/transactions.md similarity index 100% rename from docs/Transactions.md rename to docs/transactions.md From bfc0445eb94f134c87fb6971223742b155e3b904 Mon Sep 17 00:00:00 2001 From: Maksymilian Komarnicki <70346597+makz00@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:22:55 +0100 Subject: [PATCH 08/25] Add fork repositories support for reporting regression tests (#508) --- .github/workflows/main.yml | 10 +++------- scripts/check_test_results.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100755 scripts/check_test_results.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41e20abc5..49a7c4398 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,12 +97,8 @@ jobs: . venv/bin/activate scripts/run_tests.py -a regression - - name: Test Report - uses: EnricoMi/publish-unit-test-result-action@v2 - with: - files: test/regression/cocotb/results.xml - check_name: cocotb test results - comment_mode: off + - name: Check for test failure + run: ./scripts/check_test_results.py unit-test: name: Run unit tests @@ -131,7 +127,7 @@ jobs: run: ./scripts/run_tests.py --verbose - name: Check traces - run: ./scripts/run_tests.py -t -c 1 TestCore + run: ./scripts/run_tests.py -t -c 1 TestCore lint: name: Check code formatting and typing diff --git a/scripts/check_test_results.py b/scripts/check_test_results.py new file mode 100755 index 000000000..c10af9bc2 --- /dev/null +++ b/scripts/check_test_results.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import os +import pathlib +import xml.etree.ElementTree as eT + +FAILURE_TAG = "failure" +TOP_DIR = pathlib.Path(__file__).parent.parent +TEST_RESULTS_FILE = TOP_DIR.joinpath("test/regression/cocotb/results.xml") + +if not os.path.exists(TEST_RESULTS_FILE): + print("File not found: ", TEST_RESULTS_FILE) + sys.exit(1) + +tree = eT.parse(TEST_RESULTS_FILE) + +if len(list(tree.iter(FAILURE_TAG))) > 0: + print("Some regression tests failed") + sys.exit(1) + +print("All regression tests pass") From 6b4dbf32f348a3cf1a7f4ad54eae79a5c6762447 Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Tue, 28 Nov 2023 10:57:57 +0100 Subject: [PATCH 09/25] Simpler way of using method transformations (#525) --- test/transactions/test_transaction_lib.py | 28 ++++----- transactron/lib/transformers.py | 71 ++++++++++++++++------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/test/transactions/test_transaction_lib.py b/test/transactions/test_transaction_lib.py index d43540860..6daa6517c 100644 --- a/test/transactions/test_transaction_lib.py +++ b/test/transactions/test_transaction_lib.py @@ -351,7 +351,7 @@ def test_many_out(self): sim.add_sync_process(self.generate_producer(i)) -class MethodTransformerTestCircuit(Elaboratable): +class MethodMapTestCircuit(Elaboratable): def __init__(self, iosize: int, use_methods: bool, use_dicts: bool): self.iosize = iosize self.use_methods = use_methods @@ -399,25 +399,21 @@ def _(arg: Record): def _(arg: Record): return otransform(m, arg) - trans = MethodTransformer( - self.target.adapter.iface, i_transform=(layout, imeth), o_transform=(layout, ometh) - ) + trans = MethodMap(self.target.adapter.iface, i_transform=(layout, imeth), o_transform=(layout, ometh)) else: - trans = MethodTransformer( + trans = MethodMap( self.target.adapter.iface, i_transform=(layout, itransform), o_transform=(layout, otransform), ) - m.submodules.trans = trans - - m.submodules.source = self.source = TestbenchIO(AdapterTrans(trans.method)) + m.submodules.source = self.source = TestbenchIO(AdapterTrans(trans.use(m))) return m class TestMethodTransformer(TestCaseWithSimulator): - m: MethodTransformerTestCircuit + m: MethodMapTestCircuit def source(self): for i in range(2**self.m.iosize): @@ -430,19 +426,19 @@ def target(self, data): return {"data": (data << 1) | (data >> (self.m.iosize - 1))} def test_method_transformer(self): - self.m = MethodTransformerTestCircuit(4, False, False) + self.m = MethodMapTestCircuit(4, False, False) with self.run_simulation(self.m) as sim: sim.add_sync_process(self.source) sim.add_sync_process(self.target) def test_method_transformer_dicts(self): - self.m = MethodTransformerTestCircuit(4, False, True) + self.m = MethodMapTestCircuit(4, False, True) with self.run_simulation(self.m) as sim: sim.add_sync_process(self.source) sim.add_sync_process(self.target) def test_method_transformer_with_methods(self): - self.m = MethodTransformerTestCircuit(4, True, True) + self.m = MethodMapTestCircuit(4, True, True) with self.run_simulation(self.m) as sim: sim.add_sync_process(self.source) sim.add_sync_process(self.target) @@ -517,9 +513,9 @@ def elaborate(self, platform): if self.add_combiner: combiner = (layout, lambda _, vs: {"data": sum(vs)}) - m.submodules.product = product = MethodProduct(methods, combiner) + product = MethodProduct(methods, combiner) - m.submodules.method = self.method = TestbenchIO(AdapterTrans(product.method)) + m.submodules.method = self.method = TestbenchIO(AdapterTrans(product.use(m))) return m @@ -704,9 +700,9 @@ def elaborate(self, platform): if self.add_combiner: combiner = (layout, lambda _, vs: {"data": sum(Mux(s, r, 0) for (s, r) in vs)}) - m.submodules.product = product = MethodTryProduct(methods, combiner) + product = MethodTryProduct(methods, combiner) - m.submodules.method = self.method = TestbenchIO(AdapterTrans(product.method)) + m.submodules.method = self.method = TestbenchIO(AdapterTrans(product.use(m))) return m diff --git a/transactron/lib/transformers.py b/transactron/lib/transformers.py index b3d1e2470..18c3ac73a 100644 --- a/transactron/lib/transformers.py +++ b/transactron/lib/transformers.py @@ -1,28 +1,56 @@ +from abc import ABC from amaranth import * from ..core import * from ..core import RecordDict from typing import Optional from collections.abc import Callable -from transactron.utils import ValueLike, assign, AssignType +from transactron.utils import ValueLike, assign, AssignType, ModuleLike from .connectors import Forwarder, ManyToOneConnectTrans, ConnectTrans __all__ = [ - "MethodTransformer", + "Transformer", + "MethodMap", "MethodFilter", "MethodProduct", "MethodTryProduct", "Collector", "CatTrans", - "ConnectAndTransformTrans", + "ConnectAndMapTrans", ] -class MethodTransformer(Elaboratable): - """Method transformer. +class Transformer(ABC): + """Method transformer abstract class. + + Method transformers construct a new method which utilizes other methods. + + Attributes + ---------- + method: Method + The method. + """ + + method: Method + + def use(self, m: ModuleLike): + """ + Returns the method and adds the transformer to a module. + + Parameters + ---------- + m: Module or TModule + The module to which this transformer is added as a submodule. + """ + m.submodules += self + return self.method + + +class MethodMap(Transformer, Elaboratable): + """Bidirectional map for methods. Takes a target method and creates a transformed method which calls the - original target method, transforming the input and output values. - The transformation functions take two parameters, a `Module` and the + original target method, mapping the input and output values with + functions. The mapping functions take two parameters, a `Module` and the `Record` being transformed. Alternatively, a `Method` can be passed. @@ -45,13 +73,13 @@ def __init__( target: Method The target method. i_transform: (record layout, function or Method), optional - Input transformation. If specified, it should be a pair of a + Input mapping function. If specified, it should be a pair of a function and a input layout for the transformed method. - If not present, input is not transformed. + If not present, input is passed unmodified. o_transform: (record layout, function or Method), optional - Output transformation. If specified, it should be a pair of a + Output mapping function. If specified, it should be a pair of a function and a output layout for the transformed method. - If not present, output is not transformed. + If not present, output is passed unmodified. """ if i_transform is None: i_transform = (target.data_in.layout, lambda _, x: x) @@ -73,7 +101,7 @@ def _(arg): return m -class MethodFilter(Elaboratable): +class MethodFilter(Transformer, Elaboratable): """Method filter. Takes a target method and creates a method which calls the target method @@ -129,7 +157,7 @@ def _(arg): return m -class MethodProduct(Elaboratable): +class MethodProduct(Transformer, Elaboratable): def __init__( self, targets: list[Method], @@ -177,7 +205,7 @@ def _(arg): return m -class MethodTryProduct(Elaboratable): +class MethodTryProduct(Transformer, Elaboratable): def __init__( self, targets: list[Method], @@ -229,7 +257,7 @@ def _(arg): return m -class Collector(Elaboratable): +class Collector(Transformer, Elaboratable): """Single result collector. Creates method that collects results of many methods with identical @@ -308,14 +336,13 @@ def elaborate(self, platform): return m -class ConnectAndTransformTrans(Elaboratable): - """Connecting transaction with transformations. +class ConnectAndMapTrans(Elaboratable): + """Connecting transaction with mapping functions. Behaves like `ConnectTrans`, but modifies the transferred data using - functions or `Method`s. Equivalent to a combination of - `ConnectTrans` and `MethodTransformer`. The transformation - functions take two parameters, a `Module` and the `Record` being - transformed. + functions or `Method`s. Equivalent to a combination of `ConnectTrans` + and `MethodMap`. The mapping functions take two parameters, a `Module` + and the `Record` being transformed. """ def __init__( @@ -346,7 +373,7 @@ def __init__( def elaborate(self, platform): m = TModule() - m.submodules.transformer = transformer = MethodTransformer( + m.submodules.transformer = transformer = MethodMap( self.method2, i_transform=(self.method1.data_out.layout, self.i_fun), o_transform=(self.method1.data_in.layout, self.o_fun), From 324ce64afeb0936f558444522e533c6a019890f6 Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Fri, 1 Dec 2023 22:37:56 +0100 Subject: [PATCH 10/25] Update dockerfiles (#515) --- .github/workflows/benchmark.yml | 6 +++--- .github/workflows/main.yml | 4 ++-- docker/AmaranthSynthECP5.Dockerfile | 2 +- docker/Verilator.Dockerfile | 4 ++-- docker/riscv-toolchain.Dockerfile | 32 +++++++++++++++++++++++++---- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index c515414c2..cbba538a7 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,7 +14,7 @@ jobs: name: Synthesis benchmarks runs-on: ubuntu-latest timeout-minutes: 40 - container: ghcr.io/kuznia-rdzeni/amaranth-synth:ecp5-3.11 + container: ghcr.io/kuznia-rdzeni/amaranth-synth:ecp5-2023.11.19_v steps: - uses: actions/checkout@v3 @@ -63,7 +63,7 @@ jobs: build-perf-benchmarks: name: Build performance benchmarks runs-on: ubuntu-latest - container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.10.08_v + container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.11.19_v steps: - name: Checkout uses: actions/checkout@v3 @@ -83,7 +83,7 @@ jobs: name: Run performance benchmarks runs-on: ubuntu-latest timeout-minutes: 60 - container: ghcr.io/kuznia-rdzeni/verilator:v5.008-3.11 + container: ghcr.io/kuznia-rdzeni/verilator:v5.008-2023.11.19_v needs: build-perf-benchmarks steps: - name: Checkout diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49a7c4398..1bcdac433 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: build-regression-tests: name: Build regression tests runs-on: ubuntu-latest - container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.10.08_v + container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.11.19_v outputs: cache_hit: ${{ steps.cache-regression.outputs.cache-hit }} steps: @@ -54,7 +54,7 @@ jobs: name: Run regression tests runs-on: ubuntu-latest timeout-minutes: 10 - container: ghcr.io/kuznia-rdzeni/verilator:v5.008-3.11 + container: ghcr.io/kuznia-rdzeni/verilator:v5.008-2023.11.19_v needs: build-regression-tests steps: - name: Checkout diff --git a/docker/AmaranthSynthECP5.Dockerfile b/docker/AmaranthSynthECP5.Dockerfile index 3b9326ccf..3ae726972 100644 --- a/docker/AmaranthSynthECP5.Dockerfile +++ b/docker/AmaranthSynthECP5.Dockerfile @@ -3,7 +3,7 @@ FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - python3.11 python3-pip git yosys lsb-release \ + python3.11 python3-pip python3.11-venv git yosys lsb-release \ build-essential cmake python3-dev libboost-all-dev libeigen3-dev && \ rm -rf /var/lib/apt/lists/* diff --git a/docker/Verilator.Dockerfile b/docker/Verilator.Dockerfile index 64c60c5e4..785e76b26 100644 --- a/docker/Verilator.Dockerfile +++ b/docker/Verilator.Dockerfile @@ -3,12 +3,12 @@ FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - python3.11 libpython3.11 python3-pip git lsb-release \ + python3.11 libpython3.11 python3-pip python3.11-venv git lsb-release \ perl perl-doc help2man make autoconf g++ flex bison ccache numactl \ libgoogle-perftools-dev libfl-dev zlib1g-dev && \ rm -rf /var/lib/apt/lists/* -RUN git clone --recursive \ +RUN git clone --recursive --shallow-since=2023.03.01 \ https://github.com/verilator/verilator.git \ verilator && \ cd verilator && \ diff --git a/docker/riscv-toolchain.Dockerfile b/docker/riscv-toolchain.Dockerfile index d35c604b9..957141eb0 100644 --- a/docker/riscv-toolchain.Dockerfile +++ b/docker/riscv-toolchain.Dockerfile @@ -3,15 +3,39 @@ FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - autoconf automake autotools-dev curl python3 bc lsb-release \ + autoconf automake autotools-dev curl python3.11 python3.11-venv python3-pip bc lsb-release \ libmpc-dev libmpfr-dev libgmp-dev gawk build-essential \ - bison flex texinfo gperf libtool patchutils zlib1g-dev \ - libexpat-dev ninja-build git ca-certificates python-is-python3 && \ + bison flex texinfo gperf libtool patchutils zlib1g-dev device-tree-compiler \ + libexpat-dev ninja-build git ca-certificates python-is-python3 \ + libssl-dev libbz2-dev libreadline-dev libsqlite3-dev libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev && \ rm -rf /var/lib/apt/lists/* -RUN git clone https://github.com/riscv/riscv-gnu-toolchain && \ +RUN git clone --shallow-since=2023.05.01 https://github.com/riscv/riscv-gnu-toolchain && \ cd riscv-gnu-toolchain && \ git checkout 2023.05.14 && \ ./configure --with-multilib-generator="rv32i-ilp32--a*zifence*zicsr;rv32im-ilp32--a*zifence*zicsr;rv32ic-ilp32--a*zifence*zicsr;rv32imc-ilp32--a*zifence*zicsr;rv32imfc-ilp32f--a*zifence;rv32i_zmmul-ilp32--a*zifence*zicsr;rv32ic_zmmul-ilp32--a*zifence*zicsr" && \ make -j$(nproc) && \ cd / && rm -rf riscv-gnu-toolchain + +RUN git clone --shallow-since=2023.10.01 https://github.com/riscv-software-src/riscv-isa-sim.git spike && \ + cd spike && \ + git checkout eeef09ebb894c3bb7e42b7b47aae98792b8eef79 && \ + mkdir build/ install/ && \ + cd build/ && \ + ../configure --prefix=/spike/install/ && \ + make -j$(nproc) && \ + make install && \ + cd .. && \ + rm -rf build/ + +RUN git clone --depth=1 https://github.com/pyenv/pyenv.git .pyenv && \ + export PATH=/.pyenv/bin:$PATH && \ + export PYENV_ROOT=/root/.pyenv && \ + eval "$(pyenv init --path)" && \ + pyenv install 3.6.15 && \ + pyenv global 3.6.15 && \ + python -m venv venv3.6 && \ + . venv3.6/bin/activate && \ + python -m pip install --upgrade pip && \ + python -m pip install riscof && \ + pyenv global system From e898af5394c8a436ac9f711d7c87a3cc3245a151 Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:19:48 +0100 Subject: [PATCH 11/25] BasicFifo tweaks (#516) --- test/utils/test_fifo.py | 2 ++ transactron/utils/fifo.py | 31 ++++++++++++++----------------- transactron/utils/utils.py | 10 ++++++++++ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/test/utils/test_fifo.py b/test/utils/test_fifo.py index 2f5608141..b74667fc3 100644 --- a/test/utils/test_fifo.py +++ b/test/utils/test_fifo.py @@ -1,4 +1,5 @@ from amaranth import * +from amaranth.sim import Settle from transactron.utils.fifo import BasicFifo from transactron.lib import AdapterTrans @@ -55,6 +56,7 @@ def source(): if random.random() < 0.005: yield from fifoc.fifo_clear.call() + yield Settle() expq.clear() self.done = True diff --git a/transactron/utils/fifo.py b/transactron/utils/fifo.py index 94e136e13..1ee3ffa90 100644 --- a/transactron/utils/fifo.py +++ b/transactron/utils/fifo.py @@ -2,6 +2,7 @@ from transactron import Method, def_method, Priority, TModule from transactron._utils import MethodLayout from transactron.utils._typing import ValueLike +from transactron.utils.utils import mod_incr class BasicFifo(Elaboratable): @@ -51,25 +52,21 @@ def __init__(self, layout: MethodLayout, depth: int) -> None: # current fifo depth self.level = Signal((self.depth).bit_length()) - self.clear.add_conflict(self.read, Priority.LEFT) - self.clear.add_conflict(self.write, Priority.LEFT) + # for interface compatibility with MultiportFifo + self.read_methods = [self.read] + self.write_methods = [self.write] def elaborate(self, platform): - def mod_incr(sig: Value, mod: int) -> Value: - # perform (sig+1)%mod operation - if mod == 2 ** len(sig): - return sig + 1 - return Mux(sig == mod - 1, 0, sig + 1) - m = TModule() - m.submodules.buff_rdport = self.buff_rdport = self.buff.read_port( - domain="comb", transparent=True - ) # FWFT behaviour + next_read_idx = Signal.like(self.read_idx) + m.d.comb += next_read_idx.eq(mod_incr(self.read_idx, self.depth)) + + m.submodules.buff_rdport = self.buff_rdport = self.buff.read_port(domain="sync", transparent=True) m.submodules.buff_wrport = self.buff_wrport = self.buff.write_port() - m.d.comb += self.read_ready.eq(self.level > 0) - m.d.comb += self.write_ready.eq(self.level < self.depth) + m.d.comb += self.read_ready.eq(self.level != 0) + m.d.comb += self.write_ready.eq(self.level != self.depth) with m.If(self.read.run & ~self.write.run): m.d.sync += self.level.eq(self.level - 1) @@ -78,20 +75,20 @@ def mod_incr(sig: Value, mod: int) -> Value: with m.If(self.clear.run): m.d.sync += self.level.eq(0) - m.d.comb += self.buff_rdport.addr.eq(self.read_idx) + m.d.comb += self.buff_rdport.addr.eq(Mux(self.read.run, next_read_idx, self.read_idx)) m.d.comb += self.head.eq(self.buff_rdport.data) @def_method(m, self.write, ready=self.write_ready) def _(arg: Record) -> None: - m.d.comb += self.buff_wrport.addr.eq(self.write_idx) - m.d.comb += self.buff_wrport.data.eq(arg) + m.d.top_comb += self.buff_wrport.addr.eq(self.write_idx) + m.d.top_comb += self.buff_wrport.data.eq(arg) m.d.comb += self.buff_wrport.en.eq(1) m.d.sync += self.write_idx.eq(mod_incr(self.write_idx, self.depth)) @def_method(m, self.read, self.read_ready) def _() -> ValueLike: - m.d.sync += self.read_idx.eq(mod_incr(self.read_idx, self.depth)) + m.d.sync += self.read_idx.eq(next_read_idx) return self.head @def_method(m, self.clear) diff --git a/transactron/utils/utils.py b/transactron/utils/utils.py index 13491cd7b..bbd1e7310 100644 --- a/transactron/utils/utils.py +++ b/transactron/utils/utils.py @@ -23,9 +23,19 @@ "popcount", "count_leading_zeros", "count_trailing_zeros", + "mod_incr", ] +def mod_incr(sig: Value, mod: int) -> Value: + """ + Perform `(sig+1) % mod` operation. + """ + if mod == 2 ** len(sig): + return sig + 1 + return Mux(sig == mod - 1, 0, sig + 1) + + @contextmanager def OneHotSwitch(m: ModuleLike, test: Value): """One-hot switch. From 4904a075825f7e464dc2ca7a3b7b5eaef517f45b Mon Sep 17 00:00:00 2001 From: piotro888 Date: Sun, 3 Dec 2023 16:29:54 +0100 Subject: [PATCH 12/25] Run linker on internal asm tests (#522) --- test/asm/link.ld | 11 +++++++++++ test/test_core.py | 20 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 test/asm/link.ld diff --git a/test/asm/link.ld b/test/asm/link.ld new file mode 100644 index 000000000..9ceab42eb --- /dev/null +++ b/test/asm/link.ld @@ -0,0 +1,11 @@ +OUTPUT_ARCH( "riscv" ) + +start = 0; + +SECTIONS +{ + .text : { *(.text) } + . = 0x100000000; /* start from 2**32 - trick to emulate Harvard architecture (.bss addresses will start from 0) */ + .bss : { *(.bss) } + _end = .; +} diff --git a/test/test_core.py b/test/test_core.py index f5a1d7a1c..9951d608c 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -265,7 +265,11 @@ def test_asm_source(self): self.base_dir = "test/asm/" self.bin_src = [] - with tempfile.NamedTemporaryFile() as asm_tmp, tempfile.NamedTemporaryFile() as bin_tmp: + with ( + tempfile.NamedTemporaryFile() as asm_tmp, + tempfile.NamedTemporaryFile() as ld_tmp, + tempfile.NamedTemporaryFile() as bin_tmp, + ): subprocess.check_call( [ "riscv64-unknown-elf-as", @@ -279,7 +283,19 @@ def test_asm_source(self): ] ) subprocess.check_call( - ["riscv64-unknown-elf-objcopy", "-O", "binary", "-j", ".text", asm_tmp.name, bin_tmp.name] + [ + "riscv64-unknown-elf-ld", + "-m", + "elf32lriscv", + "-T", + self.base_dir + "link.ld", + asm_tmp.name, + "-o", + ld_tmp.name, + ] + ) + subprocess.check_call( + ["riscv64-unknown-elf-objcopy", "-O", "binary", "-j", ".text", ld_tmp.name, bin_tmp.name] ) code = bin_tmp.read() for word_idx in range(0, len(code), 4): From c86b7fab642abf70f9dae78cbed60d622716887a Mon Sep 17 00:00:00 2001 From: piotro888 Date: Sun, 3 Dec 2023 17:26:32 +0100 Subject: [PATCH 13/25] Run riscv-arch-test suite in CI (#513) --- .github/workflows/main.yml | 134 ++++++++++++- ci/riscof_compare.sh | 53 ++++++ ci/riscof_run_makefile.sh | 22 +++ scripts/run_signature.py | 2 +- test/external/riscof/config.ini | 19 ++ .../riscof/coreblocks/coreblocks_isa.yaml | 7 + .../coreblocks/coreblocks_platform.yaml | 4 + .../riscof/coreblocks/riscof_coreblocks.py | 180 ++++++++++++++++++ test/external/riscof/spike_simple/__init__.py | 2 + test/external/riscof/spike_simple/env/link.ld | 17 ++ .../riscof/spike_simple/env/model_test.h | 60 ++++++ .../spike_simple/riscof_spike_simple.py | 98 ++++++++++ .../riscof/spike_simple/spike_simple_isa.yaml | 28 +++ .../spike_simple/spike_simple_platform.yaml | 10 + test/regression/signature.py | 2 +- 15 files changed, 629 insertions(+), 9 deletions(-) create mode 100755 ci/riscof_compare.sh create mode 100755 ci/riscof_run_makefile.sh create mode 100644 test/external/riscof/config.ini create mode 100644 test/external/riscof/coreblocks/coreblocks_isa.yaml create mode 100644 test/external/riscof/coreblocks/coreblocks_platform.yaml create mode 100644 test/external/riscof/coreblocks/riscof_coreblocks.py create mode 100644 test/external/riscof/spike_simple/__init__.py create mode 100644 test/external/riscof/spike_simple/env/link.ld create mode 100644 test/external/riscof/spike_simple/env/model_test.h create mode 100644 test/external/riscof/spike_simple/riscof_spike_simple.py create mode 100644 test/external/riscof/spike_simple/spike_simple_isa.yaml create mode 100644 test/external/riscof/spike_simple/spike_simple_platform.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1bcdac433..d32c7ce1b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,128 @@ on: workflow_dispatch: jobs: + build-core: + name: Synthethise full core + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Coreblocks dependencies + run: | + python3 -m venv venv + . venv/bin/activate + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-dev.txt + + - name: Generate Verilog + run: | + . venv/bin/activate + PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + + - uses: actions/upload-artifact@v3 + with: + name: "verilog-full-core" + path: core.v + + + build-riscof-tests: + name: Build regression tests (riscv-arch-test) + runs-on: ubuntu-latest + container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.11.19_v + timeout-minutes: 10 + env: + PYENV_ROOT: "/root/.pyenv" + LC_ALL: "C.UTF8" + LANG: "C.UTF8" + defaults: + run: + working-directory: test/external/riscof/ + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PATH + run: echo "/.pyenv/bin" >> $GITHUB_PATH + + - name: Setup pyenv python + run: | + eval "$(pyenv init --path)" + pyenv global 3.6.15 + . /venv3.6/bin/activate + + - name: Setup arch test suite + run: | + . /venv3.6/bin/activate + riscof --verbose info arch-test --clone + riscof testlist --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env + + - name: Build and run tests on reference and generate Makefiles + run: | + . /venv3.6/bin/activate + riscof run --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env + + - name: Build tests for Coreblocks + run: | + MAKEFILE_PATH=riscof_work/Makefile.build-DUT-coreblocks ../../../ci/riscof_run_makefile.sh + + - uses: actions/upload-artifact@v3 + with: + name: "riscof-tests" + path: | + test/external/riscof/riscv-arch-test/**/*.elf + test/external/riscof/riscof_work/**/*.signature + test/external/riscof/**/*Makefile* + + run-riscof-tests: + name: Run regression tests (riscv-arch-test) + runs-on: ubuntu-latest + container: ghcr.io/kuznia-rdzeni/verilator:v5.008-2023.11.19_v + needs: [ build-riscof-tests, build-core ] + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Coreblocks dependencies + run: | + python3 -m venv venv + . venv/bin/activate + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-dev.txt + + - uses: actions/download-artifact@v3 + with: + name: "verilog-full-core" + path: . + + - uses: actions/download-artifact@v3 + with: + name: "riscof-tests" + path: test/external/riscof/ + + - name: Run tests on Coreblocks + run: | + . venv/bin/activate + MAKEFILE_PATH=test/external/riscof/riscof_work/Makefile.run-DUT-coreblocks NPROC=1 ./ci/riscof_run_makefile.sh + + - name: Compare signatures (test results) + run: MAKEFILE_PATH=test/external/riscof/riscof_work/Makefile.run-DUT-coreblocks ./ci/riscof_compare.sh + + build-regression-tests: - name: Build regression tests + name: Build regression tests (riscv-tests) runs-on: ubuntu-latest container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.11.19_v outputs: @@ -31,7 +151,7 @@ jobs: cache-name: cache-regression-tests with: path: test/external/riscv-tests/test-* - + key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles( '**/test/external/riscv-tests/environment/**', '**/test/external/riscv-tests/Makefile', @@ -51,7 +171,7 @@ jobs: path: test/external/riscv-tests run-regression-tests: - name: Run regression tests + name: Run regression tests (riscv-tests) runs-on: ubuntu-latest timeout-minutes: 10 container: ghcr.io/kuznia-rdzeni/verilator:v5.008-2023.11.19_v @@ -74,10 +194,10 @@ jobs: python3 -m pip install --upgrade pip python3 -m pip install -r requirements-dev.txt - - name: Generate Verilog - run: | - . venv/bin/activate - PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + - uses: actions/download-artifact@v3 + with: + name: "verilog-full-core" + path: . - uses: actions/cache@v3 env: diff --git a/ci/riscof_compare.sh b/ci/riscof_compare.sh new file mode 100755 index 000000000..db5e76d90 --- /dev/null +++ b/ci/riscof_compare.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +if [ -z "$MAKEFILE_PATH" ]; then + echo "Makefile path not specifed. Exiting... " + exit 1 +fi + +[ ! -z "$DIFF_QUIET" ] && [ "$DIFF_QUIET" -eq 1 ] && diff_add_args="-q" + +RED="\033[1;31m" +GREEN="\033[1;32m" +RED_BG="\033[0;41m" +GREEN_BG="\033[0;42m" +NO_COLOR="\033[0m" + +signature_files="$(cat $MAKEFILE_PATH | sed -n 's/^.*-o=\(.*\.signature\).*$/\1/p')" + +REFERENCE_DIR_SUFF="/../ref/Reference-Spike.signature" + +echo "> Veryfing signatures" +target_cnt=0 +fail_cnt=0 + +for sig in $signature_files +do + ref="$(dirname "$sig")$REFERENCE_DIR_SUFF" + echo ">> Comparing $sig (TARGET$target_cnt) to $ref" + + diff -b --strip-trailing-cr $diff_add_args "$sig" "$ref" + res=$? + + [ -f "$ref" ] || echo -e "${RED}!${NO_COLOR} Reference signature file not found!" + [ -f "$sig" ] || echo -e "${RED}!${NO_COLOR} Coreblocks signature file not found! Check signature run logs" + [ -s "$sig" ] || echo -e "${RED}!${NO_COLOR} Coreblock signature file is empty! Check signature run logs" + + if [ $res = 0 ] + then + echo -e "${GREEN}[PASS] Signature verification passed (TARGET$target_cnt)${NO_COLOR}" + else + echo -e "${RED}[FAIL] Signature verification failed (TARGET$target_cnt)${NO_COLOR}" + fail_cnt=$(( $fail_cnt+1 )) + fi + + target_cnt=$(( $target_cnt+1 )) +done + +rc=1 +[ $fail_cnt -eq 0 ] && rc=0 + +bg=${GREEN_BG} +[ $rc = 1 ] && bg=$RED_BG +echo -e "${bg}>>> Compared $target_cnt signatures, FAILED=$fail_cnt, PASSED=$(($target_cnt-$fail_cnt))${NO_COLOR}" +exit $rc diff --git a/ci/riscof_run_makefile.sh b/ci/riscof_run_makefile.sh new file mode 100755 index 000000000..7363893cc --- /dev/null +++ b/ci/riscof_run_makefile.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +if [ -z "$MAKEFILE_PATH" ]; then + echo "Makefile path not specifed. Exiting... " + exit 1 +fi + +[ -z "$NPROC" ] && NPROC=$(nproc) + +target_cnt=$(cat $MAKEFILE_PATH | grep TARGET | tail -n 1 | tr -d -c 0-9) + +echo "> Running for $target_cnt Makefile targets" + +targets="" + +for i in $(seq 0 $target_cnt) +do + targets="$targets TARGET$i" +done + +echo "Starting for targets: $targets" +make -f $MAKEFILE_PATH -i -j $NPROC $targets diff --git a/scripts/run_signature.py b/scripts/run_signature.py index 6acb4e311..7d45bae7f 100755 --- a/scripts/run_signature.py +++ b/scripts/run_signature.py @@ -19,7 +19,7 @@ def run_with_cocotb(test_name: str, traces: bool, output: str) -> bool: arglist = [ "make", "-C", - parent + "/" if parent else "" + "test/regression/cocotb", + (parent + "/" if parent else "") + "test/regression/cocotb", "-f", "signature.Makefile", "--no-print-directory", diff --git a/test/external/riscof/config.ini b/test/external/riscof/config.ini new file mode 100644 index 000000000..82ac943be --- /dev/null +++ b/test/external/riscof/config.ini @@ -0,0 +1,19 @@ +[RISCOF] +ReferencePlugin=spike_simple +ReferencePluginPath=./spike_simple +DUTPlugin=coreblocks +DUTPluginPath=./coreblocks + +[coreblocks] +pluginpath=./coreblocks +PATH=../../../../ +ispec=./coreblocks/coreblocks_isa.yaml +pspec=./coreblocks/coreblocks_platform.yaml +target_run=0 + +[spike_simple] +pluginpath=./spike_simple +PATH=/spike/install/bin/ +ispec=./spike_simple/spike_simple_isa.yaml +pspec=./spike_simple/spike_simple_platform.yaml +compare_run=0 diff --git a/test/external/riscof/coreblocks/coreblocks_isa.yaml b/test/external/riscof/coreblocks/coreblocks_isa.yaml new file mode 100644 index 000000000..483e8b41f --- /dev/null +++ b/test/external/riscof/coreblocks/coreblocks_isa.yaml @@ -0,0 +1,7 @@ +hart_ids: [0] +hart0: + ISA: RV32I + physical_addr_sz: 32 + + User_Spec_Version: '2.3' + supported_xlen: [32] diff --git a/test/external/riscof/coreblocks/coreblocks_platform.yaml b/test/external/riscof/coreblocks/coreblocks_platform.yaml new file mode 100644 index 000000000..21b02790b --- /dev/null +++ b/test/external/riscof/coreblocks/coreblocks_platform.yaml @@ -0,0 +1,4 @@ +reset: + label: reset_vector +nmi: + label: nmi_vector diff --git a/test/external/riscof/coreblocks/riscof_coreblocks.py b/test/external/riscof/coreblocks/riscof_coreblocks.py new file mode 100644 index 000000000..549e24934 --- /dev/null +++ b/test/external/riscof/coreblocks/riscof_coreblocks.py @@ -0,0 +1,180 @@ +import os +import logging + +import riscof.utils as utils +from riscof.pluginTemplate import pluginTemplate + +logger = logging.getLogger() + + +# This is a slightly modified default configuration for RISCOF DUT +# Changes: +# * adapt to other toolchain and run scripts +# * produce two makefiles instead of one +# * always generate all makefiles and just skip execution on target_run=0 option + + +class coreblocks(pluginTemplate): # noqa: N801 + __model__ = "coreblocks" + + __version__ = "XXX" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + config = kwargs.get("config") + + # If the config node for this DUT is missing or empty. Raise an error. At minimum we need + # the paths to the ispec and pspec files + if config is None: + print("Please enter input file paths in configuration.") + raise SystemExit(1) + + # In case of an RTL based DUT, this would be point to the final binary executable of your + # test-bench produced by a simulator (like verilator, vcs, incisive, etc). In case of an iss or + # emulator, this variable could point to where the iss binary is located. If 'PATH variable + # is missing in the config.ini we can hardcode the alternate here. + # temporary! + coreblocks_path = config["PATH"] if "PATH" in config else "coreblocks" + self.dut_exe = "python3 " + os.path.join(coreblocks_path, "scripts", "run_signature.py") + self.dut_exe += " -b cocotb" + + # Number of parallel jobs that can be spawned off by RISCOF + # for various actions performed in later functions, specifically to run the tests in + # parallel on the DUT executable. Can also be used in the build function if required. + self.num_jobs = str(config["jobs"] if "jobs" in config else 1) + + # Path to the directory where this python file is located. Collect it from the config.ini + self.pluginpath = os.path.abspath(config["pluginpath"]) + + # Collect the paths to the riscv-config absed ISA and platform yaml files. One can choose + # to hardcode these here itself instead of picking it from the config.ini file. + self.isa_spec = os.path.abspath(config["ispec"]) + self.platform_spec = os.path.abspath(config["pspec"]) + + # We capture if the user would like the run the tests on the target or + # not. If you are interested in just compiling the tests and not running + # them on the target, then following variable should be set to False + if "target_run" in config and config["target_run"] == "0": + self.target_run = False + else: + self.target_run = True + + def initialise(self, suite, work_dir, archtest_env): + # capture the working directory. Any artifacts that the DUT creates should be placed in this + # directory. Other artifacts from the framework and the Reference plugin will also be placed + # here itself. + self.work_dir = work_dir + + # capture the architectural test-suite directory. + self.suite_dir = suite + + # Note the march is not hardwired here, because it will change for each + # test. Similarly the output elf name and compile macros will be assigned later in the + # runTests function + # Change: Always use riscv64 + self.compile_cmd = ( + "riscv64-unknown-elf-gcc -march={0} \ + -static -mcmodel=medany -fvisibility=hidden -nostdlib -nostartfiles -g\ + -T " + + self.pluginpath + + "/env/link.ld\ + -I " + + self.pluginpath + + "/env/\ + -I " + + archtest_env + + " {2} -o {3} {4}" + ) + + # add more utility snippets here + + def build(self, isa_yaml, platform_yaml): + # load the isa yaml as a dictionary in python. + ispec = utils.load_yaml(isa_yaml)["hart0"] + + # capture the XLEN value by picking the max value in 'supported_xlen' field of isa yaml. This + # will be useful in setting integer value in the compiler string (if not already hardcoded); + self.xlen = "64" if 64 in ispec["supported_xlen"] else "32" + + # for coreblocks start building the '--isa' argument. the self.isa is dut specific and may not be + # useful for all DUTs + self.isa = "rv" + self.xlen + if "I" in ispec["ISA"]: + self.isa += "i" + if "M" in ispec["ISA"]: + self.isa += "m" + if "F" in ispec["ISA"]: + self.isa += "f" + if "D" in ispec["ISA"]: + self.isa += "d" + if "C" in ispec["ISA"]: + self.isa += "c" + if "B" in ispec["ISA"]: + self.isa += "b" + + # TODO: The following assumes you are using the riscv-gcc toolchain. If + # not please change appropriately + self.compile_cmd = self.compile_cmd + " -mabi=" + ("lp64 " if 64 in ispec["supported_xlen"] else "ilp32 ") + + def runTests(self, testList): # noqa: N802 N803 + # Delete Makefile if it already exists. + if os.path.exists(self.work_dir + "/Makefile." + self.name[:-1]): + os.remove(self.work_dir + "/Makefile." + self.name[:-1]) + + # For coreblocks generate two makefiles - one for build and one for run. + # It is needed because of use of separate containers, and allows caching built tests + make_build = utils.makeUtil(makefilePath=os.path.join(self.work_dir, "Makefile.build-" + self.name[:-1])) + make_run = utils.makeUtil(makefilePath=os.path.join(self.work_dir, "Makefile.run-" + self.name[:-1])) + + # set the make command that will be used. The num_jobs parameter was set in the __init__ + # function earlier + make_build.makeCommand = "make -k -j" + self.num_jobs + make_run.makeCommand = "make -k -j" + self.num_jobs + + # we will iterate over each entry in the testList. Each entry node will be refered to by the + # variable testname. + for testname in testList: + # for each testname we get all its fields (as described by the testList format) + testentry = testList[testname] + + # we capture the path to the assembly file of this test + test = testentry["test_path"] + + # capture the directory where the artifacts of this test will be dumped/created. RISCOF is + # going to look into this directory for the signature files + test_dir = testentry["work_dir"] + + # name of the elf file after compilation of the test + elf = testname + ".elf" + + # name of the signature file as per requirement of RISCOF. RISCOF expects the signature to + # be named as DUT-.signature. The below variable creates an absolute path of + # signature file. + sig_file = os.path.join(test_dir, self.name[:-1] + ".signature") + + # for each test there are specific compile macros that need to be enabled. The macros in + # the testList node only contain the macros/values. For the gcc toolchain we need to + # prefix with "-D". The following does precisely that. + compile_macros = " -D" + " -D".join(testentry["macros"]) + + # substitute all variables in the compile command that we created in the initialize + # function + buildcmd = self.compile_cmd.format(testentry["isa"].lower(), self.xlen, test, elf, compile_macros) + + simcmd = self.dut_exe + " -o={0} {1}".format(sig_file, elf) + + # concatenate all commands that need to be executed within a make-target. + target_build = "cd {0}; {1};".format(testentry["work_dir"], buildcmd) + target_run = "mkdir -p {0}; cd {1}; {2};".format(testentry["work_dir"], self.work_dir, simcmd) + + # create a target. The makeutil will create a target with the name "TARGET" where num + # starts from 0 and increments automatically for each new target that is added + make_build.add_target(target_build) + make_run.add_target(target_run) + + if self.target_run: + # once the make-targets are done and the makefile has been created, run all the targets in + # parallel using the make command set above. + make_build.execute_all(self.work_dir) + make_run.execute_all(self.work_dir) diff --git a/test/external/riscof/spike_simple/__init__.py b/test/external/riscof/spike_simple/__init__.py new file mode 100644 index 000000000..3ad9513f4 --- /dev/null +++ b/test/external/riscof/spike_simple/__init__.py @@ -0,0 +1,2 @@ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/test/external/riscof/spike_simple/env/link.ld b/test/external/riscof/spike_simple/env/link.ld new file mode 100644 index 000000000..26538d5bd --- /dev/null +++ b/test/external/riscof/spike_simple/env/link.ld @@ -0,0 +1,17 @@ +OUTPUT_ARCH( "riscv" ) +ENTRY(rvtest_entry_point) + +SECTIONS +{ + . = 0x80000000; + .text.init : { *(.text.init) } + . = ALIGN(0x1000); + .tohost : { *(.tohost) } + . = ALIGN(0x1000); + .text : { *(.text) } + . = ALIGN(0x1000); + .data : { *(.data) } + .data.string : { *(.data.string)} + .bss : { *(.bss) } + _end = .; +} diff --git a/test/external/riscof/spike_simple/env/model_test.h b/test/external/riscof/spike_simple/env/model_test.h new file mode 100644 index 000000000..b93e53a01 --- /dev/null +++ b/test/external/riscof/spike_simple/env/model_test.h @@ -0,0 +1,60 @@ +#ifndef _COMPLIANCE_MODEL_H +#define _COMPLIANCE_MODEL_H +#if XLEN == 64 + #define ALIGNMENT 3 +#else + #define ALIGNMENT 2 +#endif + +#define RVMODEL_DATA_SECTION \ + .pushsection .tohost,"aw",@progbits; \ + .align 8; .global tohost; tohost: .dword 0; \ + .align 8; .global fromhost; fromhost: .dword 0; \ + .popsection; \ + .align 8; .global begin_regstate; begin_regstate: \ + .word 128; \ + .align 8; .global end_regstate; end_regstate: \ + .word 4; + +//RV_COMPLIANCE_HALT +#define RVMODEL_HALT \ + li x1, 1; \ + write_tohost: \ + sw x1, tohost, t2; \ + j write_tohost; + +#define RVMODEL_BOOT + +//RV_COMPLIANCE_DATA_BEGIN +#define RVMODEL_DATA_BEGIN \ + RVMODEL_DATA_SECTION \ + .align ALIGNMENT;\ + .global begin_signature; begin_signature: + +//RV_COMPLIANCE_DATA_END +#define RVMODEL_DATA_END \ + .global end_signature; end_signature: + +//RVTEST_IO_INIT +#define RVMODEL_IO_INIT +//RVTEST_IO_WRITE_STR +#define RVMODEL_IO_WRITE_STR(_R, _STR) +//RVTEST_IO_CHECK +#define RVMODEL_IO_CHECK() +//RVTEST_IO_ASSERT_GPR_EQ +#define RVMODEL_IO_ASSERT_GPR_EQ(_S, _R, _I) +//RVTEST_IO_ASSERT_SFPR_EQ +#define RVMODEL_IO_ASSERT_SFPR_EQ(_F, _R, _I) +//RVTEST_IO_ASSERT_DFPR_EQ +#define RVMODEL_IO_ASSERT_DFPR_EQ(_D, _R, _I) + +#define RVMODEL_SET_MSW_INT + +#define RVMODEL_CLEAR_MSW_INT + +#define RVMODEL_CLEAR_MTIMER_INT + +#define RVMODEL_CLEAR_MEXT_INT + + +#endif // _COMPLIANCE_MODEL_H diff --git a/test/external/riscof/spike_simple/riscof_spike_simple.py b/test/external/riscof/spike_simple/riscof_spike_simple.py new file mode 100644 index 000000000..5e06de990 --- /dev/null +++ b/test/external/riscof/spike_simple/riscof_spike_simple.py @@ -0,0 +1,98 @@ +import os +import re +import shutil +import subprocess +import shlex +import logging +import random +import string +from string import Template +import sys + +import riscof.utils as utils +from riscof.pluginTemplate import pluginTemplate +import riscof.constants as constants + +logger = logging.getLogger() + +class spike_simple(pluginTemplate): + __model__ = "Spike" + __version__ = "0.5.0" + + def __init__(self, *args, **kwargs): + sclass = super().__init__(*args, **kwargs) + + config = kwargs.get('config') + self.spike_exe = os.path.join(config['PATH'] if 'PATH' in config else "","spike") + if config is None: + print("Please enter input file paths in configuration.") + raise SystemExit + try: + self.isa_spec = os.path.abspath(config['ispec']) + self.platform_spec = os.path.abspath(config['pspec']) + self.pluginpath = os.path.abspath(config['pluginpath']) + except KeyError as e: + logger.error("Please check the spike_simple section in config for missing values.") + logger.error(e) + raise SystemExit + logger.debug("SPIKE Simple plugin initialised using the following configuration.") + for entry in config: + logger.debug(entry+' : '+config[entry]) + + self.compare_run = not ("compare_run" in config and config["compare_run"] == "0") + + return sclass + + def initialise(self, suite, work_dir, compliance_env): + self.work_dir = work_dir + self.compile_cmd = 'riscv64-unknown-elf-gcc -march={0} \ + -static -mcmodel=medany -fvisibility=hidden -nostdlib -nostartfiles\ + -T '+self.pluginpath+'/env/link.ld\ + -I '+self.pluginpath+'/env/\ + -I ' + compliance_env + + def build(self, isa_yaml, platform_yaml): + ispec = utils.load_yaml(isa_yaml)['hart0'] + self.xlen = ('64' if 64 in ispec['supported_xlen'] else '32') + self.isa = 'rv' + self.xlen + if "64I" in ispec["ISA"]: + self.compile_cmd = self.compile_cmd+' -mabi='+'lp64 ' + elif "32I" in ispec["ISA"]: + self.compile_cmd = self.compile_cmd+' -mabi='+'ilp32 ' + elif "32E" in ispec["ISA"]: + self.compile_cmd = self.compile_cmd+' -mabi='+'ilp32e ' + if "I" in ispec["ISA"]: + self.isa += 'i' + if "M" in ispec["ISA"]: + self.isa += 'm' + if "C" in ispec["ISA"]: + self.isa += 'c' + compiler = "riscv64-unknown-elf-gcc".format(self.xlen) + if shutil.which(compiler) is None: + logger.error(compiler+": executable not found. Please check environment setup.") + raise SystemExit + if shutil.which(self.spike_exe) is None: + logger.error(self.spike_exe+ ": executable not found. Please check environment setup.") + raise SystemExit + + def runTests(self, testList): + for file in testList: + testentry = testList[file] + test = testentry['test_path'] + test_dir = testentry['work_dir'] + + elf = 'my.elf' + sig_file = os.path.join(test_dir, self.name[:-1] + ".signature") + + cmd = self.compile_cmd.format(testentry['isa'].lower(), self.xlen) + ' ' + test + ' -o ' + elf + compile_cmd = cmd + ' -D' + " -D".join(testentry['macros']) + logger.debug('Compiling test: ' + test) + utils.shellCommand(compile_cmd).run(cwd=test_dir) + + execute = self.spike_exe + ' --isa={0} +signature={1} +signature-granularity=4 {2}'.format(self.isa, sig_file, elf) + logger.debug('Executing on Spike ' + execute) + utils.shellCommand(execute).run(cwd=test_dir) + + if not self.compare_run: + # exit now if we don't want to run compare of signatures + raise SystemExit(0) diff --git a/test/external/riscof/spike_simple/spike_simple_isa.yaml b/test/external/riscof/spike_simple/spike_simple_isa.yaml new file mode 100644 index 000000000..dad55a4f1 --- /dev/null +++ b/test/external/riscof/spike_simple/spike_simple_isa.yaml @@ -0,0 +1,28 @@ +hart_ids: [0] +hart0: + ISA: RV32IMCZicsr_Zifencei + physical_addr_sz: 32 + User_Spec_Version: '2.3' + supported_xlen: [32] + misa: + reset-val: 0x40001104 + rv32: + accessible: true + mxl: + implemented: true + type: + warl: + dependency_fields: [] + legal: + - mxl[1:0] in [0x1] + wr_illegal: + - Unchanged + extensions: + implemented: true + type: + warl: + dependency_fields: [] + legal: + - extensions[25:0] bitmask [0x0001104, 0x0000000] + wr_illegal: + - Unchanged diff --git a/test/external/riscof/spike_simple/spike_simple_platform.yaml b/test/external/riscof/spike_simple/spike_simple_platform.yaml new file mode 100644 index 000000000..8e1a3d8e3 --- /dev/null +++ b/test/external/riscof/spike_simple/spike_simple_platform.yaml @@ -0,0 +1,10 @@ +mtime: + implemented: true + address: 0xbff8 +mtimecmp: + implemented: true + address: 0x4000 +nmi: + label: nmi_vector +reset: + label: reset_vector diff --git a/test/regression/signature.py b/test/regression/signature.py index 96b661199..e741d3493 100644 --- a/test/regression/signature.py +++ b/test/regression/signature.py @@ -46,7 +46,7 @@ async def run_test(sim_backend: SimulationBackend, test_path: str, signature_pat mem_segments.append(signature_ram) mem_model = CoreMemoryModel(mem_segments) - success = await sim_backend.run(mem_model, timeout_cycles=100000) + success = await sim_backend.run(mem_model, timeout_cycles=200000) if not success: raise RuntimeError(f"{test_path}: Simulation timed out") From ee9097713261ca903096ded92555a050ee100d1b Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Sun, 3 Dec 2023 18:07:21 +0100 Subject: [PATCH 14/25] Fix riscv-test job dependencies (#533) --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d32c7ce1b..56b3dbed2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ on: jobs: build-core: - name: Synthethise full core + name: Synthesize full core runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -175,7 +175,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 container: ghcr.io/kuznia-rdzeni/verilator:v5.008-2023.11.19_v - needs: build-regression-tests + needs: [ build-regression-tests, build-core ] steps: - name: Checkout uses: actions/checkout@v3 From a053bb5c2700ae674dd966ca37af664bc02126dc Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Sun, 3 Dec 2023 19:49:34 +0100 Subject: [PATCH 15/25] Add `condition` based MethodFilter (#504) --- test/transactions/test_transaction_lib.py | 12 ++++++--- transactron/lib/transformers.py | 33 ++++++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/test/transactions/test_transaction_lib.py b/test/transactions/test_transaction_lib.py index 6daa6517c..e096f7860 100644 --- a/test/transactions/test_transaction_lib.py +++ b/test/transactions/test_transaction_lib.py @@ -466,23 +466,27 @@ def target_mock(self, data): def cmeth_mock(self, data): return {"data": data % 2} - def test_method_filter_with_methods(self): + @parameterized.expand([(True,), (False,)]) + def test_method_filter_with_methods(self, use_condition): self.initialize() self.cmeth = TestbenchIO(Adapter(i=self.layout, o=data_layout(1))) - self.tc = SimpleTestCircuit(MethodFilter(self.target.adapter.iface, self.cmeth.adapter.iface)) + self.tc = SimpleTestCircuit( + MethodFilter(self.target.adapter.iface, self.cmeth.adapter.iface, use_condition=use_condition) + ) m = ModuleConnector(test_circuit=self.tc, target=self.target, cmeth=self.cmeth) with self.run_simulation(m) as sim: sim.add_sync_process(self.source) sim.add_sync_process(self.target_mock) sim.add_sync_process(self.cmeth_mock) - def test_method_filter(self): + @parameterized.expand([(True,), (False,)]) + def test_method_filter(self, use_condition): self.initialize() def condition(_, v): return v[0] - self.tc = SimpleTestCircuit(MethodFilter(self.target.adapter.iface, condition)) + self.tc = SimpleTestCircuit(MethodFilter(self.target.adapter.iface, condition, use_condition=use_condition)) m = ModuleConnector(test_circuit=self.tc, target=self.target) with self.run_simulation(m) as sim: sim.add_sync_process(self.source) diff --git a/transactron/lib/transformers.py b/transactron/lib/transformers.py index 18c3ac73a..5bcf0a4d3 100644 --- a/transactron/lib/transformers.py +++ b/transactron/lib/transformers.py @@ -6,6 +6,7 @@ from collections.abc import Callable from transactron.utils import ValueLike, assign, AssignType, ModuleLike from .connectors import Forwarder, ManyToOneConnectTrans, ConnectTrans +from .simultaneous import condition __all__ = [ "Transformer", @@ -109,9 +110,10 @@ class MethodFilter(Transformer, Elaboratable): parameters, a module and the input `Record` of the method. Non-zero return value is interpreted as true. Alternatively to using a function, a `Method` can be passed as a condition. - - Caveat: because of the limitations of transaction scheduling, the target - method is locked for usage even if it is not called. + By default, the target method is locked for use even if it is not called. + If this is not the desired effect, set `use_condition` to True, but this will + cause that the provided method will be `single_caller` and all other `condition` + drawbacks will be in place (e.g. risk of exponential complexity). Attributes ---------- @@ -120,7 +122,11 @@ class MethodFilter(Transformer, Elaboratable): """ def __init__( - self, target: Method, condition: Callable[[TModule, Record], ValueLike], default: Optional[RecordDict] = None + self, + target: Method, + condition: Callable[[TModule, Record], ValueLike], + default: Optional[RecordDict] = None, + use_condition: bool = False, ): """ Parameters @@ -133,12 +139,16 @@ def __init__( default: Value or dict, optional The default value returned from the filtered method when the condition is false. If omitted, zero is returned. + use_condition : bool + Instead of `m.If` use simultaneus `condition` which allow to execute + this filter if the condition is False and target is not ready. """ if default is None: default = Record.like(target.data_out) self.target = target - self.method = Method.like(target) + self.use_condition = use_condition + self.method = Method(i=target.data_in.layout, o=target.data_out.layout, single_caller=self.use_condition) self.condition = condition self.default = default @@ -150,8 +160,17 @@ def elaborate(self, platform): @def_method(m, self.method) def _(arg): - with m.If(self.condition(m, arg)): - m.d.comb += ret.eq(self.target(m, arg)) + if self.use_condition: + cond = Signal() + m.d.top_comb += cond.eq(self.condition(m, arg)) + with condition(m, nonblocking=False, priority=False) as branch: + with branch(cond): + m.d.comb += ret.eq(self.target(m, arg)) + with branch(~cond): + pass + else: + with m.If(self.condition(m, arg)): + m.d.comb += ret.eq(self.target(m, arg)) return ret return m From 9e8ba449ce960288bfb948768d748e844803f7f7 Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Mon, 4 Dec 2023 12:41:12 +0100 Subject: [PATCH 16/25] Update Amaranth version (#530) --- requirements.txt | 4 +- stubs/amaranth/hdl/ast.pyi | 6 +- stubs/amaranth/hdl/dsl.pyi | 2 +- stubs/amaranth/hdl/ir.pyi | 4 +- stubs/amaranth/hdl/rec.pyi | 2 +- stubs/amaranth/lib/coding.pyi | 2 +- stubs/amaranth/lib/data.pyi | 11 ++- stubs/amaranth/lib/enum.pyi | 141 ++++++++++++++++++++++++++++--- stubs/amaranth/lib/fifo.pyi | 2 +- stubs/amaranth/lib/scheduler.pyi | 2 +- transactron/lib/transformers.py | 12 +-- 11 files changed, 153 insertions(+), 35 deletions(-) diff --git a/requirements.txt b/requirements.txt index 278864342..c08979f0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -amaranth-yosys==0.25.0.0.post77 -git+https://github.com/amaranth-lang/amaranth@ccf7aaf00db54c7647b2f0f0cfdf34835c16fa8f +amaranth-yosys==0.35.0.0.post81 +git+https://github.com/amaranth-lang/amaranth@ab6503e352825b36bb29f1a8622b9e98aac9a6c6 diff --git a/stubs/amaranth/hdl/ast.pyi b/stubs/amaranth/hdl/ast.pyi index abb57120c..b22901c29 100644 --- a/stubs/amaranth/hdl/ast.pyi +++ b/stubs/amaranth/hdl/ast.pyi @@ -6,7 +6,7 @@ from abc import ABCMeta, abstractmethod from collections.abc import Callable, MutableMapping, MutableSequence, MutableSet from typing import Any, Generic, Iterable, Iterator, Mapping, NoReturn, Optional, Sequence, TypeVar, final, overload from enum import Enum -from coreblocks.utils import ValueLike, ShapeLike, StatementLike +from transactron.utils import ValueLike, ShapeLike, StatementLike __all__ = ["Shape", "ShapeCastable", "signed", "unsigned", "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl", "Array", "ArrayProxy", "Signal", "ClockSignal", "ResetSignal", "ValueCastable", "Sample", "Past", "Stable", "Rose", "Fell", "Initial", "Statement", "Switch", "Property", "Assign", "Assert", "Assume", "Cover", "ValueKey", "ValueDict", "ValueSet", "SignalKey", "SignalDict", "SignalSet", "ValueLike", "ShapeLike", "StatementLike", "SwitchKey"] @@ -50,7 +50,7 @@ class Shape: ... @staticmethod - def cast(obj: ShapeLike, *, src_loc_at=...): + def cast(obj: ShapeLike, *, src_loc_at=...) -> Shape: ... def __repr__(self) -> str: @@ -411,7 +411,7 @@ class Signal(Value, DUID, metaclass=_SignalMeta): """Create Signal based on another. """ ... - + def shape(self) -> Shape: ... diff --git a/stubs/amaranth/hdl/dsl.pyi b/stubs/amaranth/hdl/dsl.pyi index 41fdd8330..9d0c07f76 100644 --- a/stubs/amaranth/hdl/dsl.pyi +++ b/stubs/amaranth/hdl/dsl.pyi @@ -5,7 +5,7 @@ This type stub file was generated by pyright. from contextlib import _GeneratorContextManager, contextmanager from typing import Callable, ContextManager, Iterator, NoReturn, OrderedDict, ParamSpec, TypeVar, Optional from typing_extensions import Self -from coreblocks.utils import HasElaborate +from transactron.utils import HasElaborate from .ast import * from .ast import Flattenable from .ir import * diff --git a/stubs/amaranth/hdl/ir.pyi b/stubs/amaranth/hdl/ir.pyi index f5c7ba509..63acd1e3c 100644 --- a/stubs/amaranth/hdl/ir.pyi +++ b/stubs/amaranth/hdl/ir.pyi @@ -2,10 +2,10 @@ This type stub file was generated by pyright. """ -from abc import ABCMeta, abstractmethod +from abc import abstractmethod from .ast import * from .cd import * -from coreblocks.utils import HasElaborate +from transactron.utils import HasElaborate __all__ = ["Elaboratable", "DriverConflict", "Fragment", "Instance"] diff --git a/stubs/amaranth/hdl/rec.pyi b/stubs/amaranth/hdl/rec.pyi index a03644901..62887dd68 100644 --- a/stubs/amaranth/hdl/rec.pyi +++ b/stubs/amaranth/hdl/rec.pyi @@ -5,7 +5,7 @@ This type stub file was generated by pyright. from enum import Enum from typing import NoReturn, OrderedDict from .ast import * -from coreblocks.utils import LayoutLike +from transactron.utils import LayoutLike __all__ = ["Direction", "DIR_NONE", "DIR_FANOUT", "DIR_FANIN", "Layout", "Record"] Direction = Enum('Direction', ('NONE', 'FANOUT', 'FANIN')) diff --git a/stubs/amaranth/lib/coding.pyi b/stubs/amaranth/lib/coding.pyi index 9fe2bc3f3..aa0f3ffd3 100644 --- a/stubs/amaranth/lib/coding.pyi +++ b/stubs/amaranth/lib/coding.pyi @@ -3,7 +3,7 @@ This type stub file was generated by pyright. """ from .. import * -from coreblocks.utils import HasElaborate +from transactron.utils import HasElaborate __all__ = ["Encoder", "Decoder", "PriorityEncoder", "PriorityDecoder", "GrayEncoder", "GrayDecoder"] class Encoder(Elaboratable): diff --git a/stubs/amaranth/lib/data.pyi b/stubs/amaranth/lib/data.pyi index 84ea4684f..46d13be39 100644 --- a/stubs/amaranth/lib/data.pyi +++ b/stubs/amaranth/lib/data.pyi @@ -3,18 +3,17 @@ This type stub file was generated by pyright. """ from abc import ABCMeta, abstractmethod -from collections.abc import Callable, Iterator, Mapping -from enum import Enum -from typing import Optional, TypeVar, Generic +from collections.abc import Iterator, Mapping +from typing import TypeVar, Generic from typing_extensions import Self from amaranth.hdl import * from amaranth.hdl.ast import Assign, ShapeCastable, ValueCastable -from coreblocks.utils._typing import ShapeLike, ValueLike +from transactron.utils._typing import ShapeLike, ValueLike __all__ = ["Field", "Layout", "StructLayout", "UnionLayout", "ArrayLayout", "FlexibleLayout", "View", "Struct", "Union"] -_T_ShapeCastable = TypeVar("_T_ShapeCastable", bound=ShapeCastable) +_T_ShapeCastable = TypeVar("_T_ShapeCastable", bound=ShapeCastable, covariant=True) class Field: @@ -186,7 +185,7 @@ class View(ValueCastable, Generic[_T_ShapeCastable]): def as_value(self) -> Value: ... - def shape(self) -> Layout: + def shape(self) -> _T_ShapeCastable: ... def eq(self, other: ValueLike) -> Assign: diff --git a/stubs/amaranth/lib/enum.pyi b/stubs/amaranth/lib/enum.pyi index 2cd19fdbd..9c2d71e73 100644 --- a/stubs/amaranth/lib/enum.pyi +++ b/stubs/amaranth/lib/enum.pyi @@ -3,18 +3,23 @@ This type stub file was generated by pyright. """ import enum as py_enum +from typing import Generic, Optional, TypeVar, overload from typing_extensions import Self from amaranth import * -from ..hdl.ast import ShapeCastable +from ..hdl.ast import Assign, ValueCastable, ShapeCastable, ValueLike -__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'auto', 'unique'] +__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'EnumView', 'FlagView', 'auto', 'unique'] + + +_T = TypeVar("_T") +_T_ViewClass = TypeVar("_T_ViewClass", bound=None | ValueCastable) auto = py_enum.auto unique = py_enum.unique -class EnumMeta(ShapeCastable, py_enum.EnumMeta): +class EnumMeta(ShapeCastable, py_enum.EnumMeta, Generic[_T_ViewClass]): """Subclass of the standard :class:`enum.EnumMeta` that implements the :class:`ShapeCastable` protocol. @@ -26,10 +31,10 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta): or the values of its members. """ @classmethod - def __prepare__(metacls, name, bases, shape=..., **kwargs) -> py_enum._EnumDict: + def __prepare__(metacls, name, bases, shape: Shape=..., view_class:_T_ViewClass=..., **kwargs) -> py_enum._EnumDict: ... - def __new__(cls, name, bases, namespace, shape=..., **kwargs) -> Self: + def __new__(cls, name, bases, namespace, shape: Shape=..., view_class:_T_ViewClass=..., **kwargs) -> Self: ... def as_shape(cls) -> Shape: @@ -48,35 +53,149 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta): """ ... - def __call__(cls, value) -> Value: + @overload + def __call__(cls: type[_T], value: int) -> _T: + ... + + @overload + def __call__(cls: type[_T], value: _T) -> _T: + ... + + @overload + def __call__(cls: EnumMeta[None], value: int | ValueLike) -> Value: + ... + + @overload + def __call__(cls: EnumMeta[_T_ViewClass], value: int | ValueLike) -> _T_ViewClass: + ... + + def __call__(cls, value: int | ValueLike) -> Value | ValueCastable: ... def const(cls, init) -> Const: ... - -class Enum(py_enum.Enum, metaclass=EnumMeta): +class Enum(py_enum.Enum, metaclass=EnumMeta[EnumView]): """Subclass of the standard :class:`enum.Enum` that has :class:`EnumMeta` as its metaclass.""" ... -class IntEnum(py_enum.IntEnum, metaclass=EnumMeta): +class IntEnum(py_enum.IntEnum, metaclass=EnumMeta[None]): """Subclass of the standard :class:`enum.IntEnum` that has :class:`EnumMeta` as its metaclass.""" ... -class Flag(py_enum.Flag, metaclass=EnumMeta): +class Flag(py_enum.Flag, metaclass=EnumMeta[FlagView]): """Subclass of the standard :class:`enum.Flag` that has :class:`EnumMeta` as its metaclass.""" ... -class IntFlag(py_enum.IntFlag, metaclass=EnumMeta): +class IntFlag(py_enum.IntFlag, metaclass=EnumMeta[None]): """Subclass of the standard :class:`enum.IntFlag` that has :class:`EnumMeta` as its metaclass.""" ... +class EnumView(ValueCastable, Generic[_T]): + """The view class used for :class:`Enum`. + + Wraps a :class:`Value` and only allows type-safe operations. The only operators allowed are + equality comparisons (``==`` and ``!=``) with another :class:`EnumView` of the same enum type. + """ + + def __init__(self, enum: _T, target: ValueLike): + ... + + def shape(self) -> _T: + ... + + @ValueCastable.lowermethod + def as_value(self) -> Value: + ... + + def eq(self, other: ValueLike) -> Assign: + ... + + def __eq__(self, other: FlagView[_T] | _T) -> Value: + """Compares the underlying value for equality. + + The other operand has to be either another :class:`EnumView` with the same enum type, or + a plain value of the underlying enum. + + Returns + ------- + :class:`Value` + The result of the equality comparison, as a single-bit value. + """ + ... + + def __ne__(self, other: FlagView[_T] | _T) -> Value: + ... + + + +class FlagView(EnumView[_T], Generic[_T]): + """The view class used for :class:`Flag`. + + In addition to the operations allowed by :class:`EnumView`, it allows bitwise operations among + values of the same enum type.""" + + def __invert__(self) -> FlagView[_T]: + """Inverts all flags in this value and returns another :ref:`FlagView`. + + Note that this is not equivalent to applying bitwise negation to the underlying value: + just like the Python :class:`enum.Flag` class, only bits corresponding to flags actually + defined in the enumeration are included in the result. + + Returns + ------- + :class:`FlagView` + """ + ... + + def __and__(self, other: FlagView[_T] | _T) -> FlagView[_T]: + """Performs a bitwise AND and returns another :class:`FlagView`. + + The other operand has to be either another :class:`FlagView` of the same enum type, or + a plain value of the underlying enum type. + + Returns + ------- + :class:`FlagView` + """ + ... + + def __or__(self, other: FlagView[_T] | _T) -> FlagView[_T]: + """Performs a bitwise OR and returns another :class:`FlagView`. + + The other operand has to be either another :class:`FlagView` of the same enum type, or + a plain value of the underlying enum type. + + Returns + ------- + :class:`FlagView` + """ + ... + + def __xor__(self, other: FlagView[_T] | _T) -> FlagView[_T]: + """Performs a bitwise XOR and returns another :class:`FlagView`. + + The other operand has to be either another :class:`FlagView` of the same enum type, or + a plain value of the underlying enum type. + + Returns + ------- + :class:`FlagView` + """ + ... + + __rand__ = __and__ + __ror__ = __or__ + __rxor__ = __xor__ + + + diff --git a/stubs/amaranth/lib/fifo.pyi b/stubs/amaranth/lib/fifo.pyi index 5da2d3659..a64c5e8e5 100644 --- a/stubs/amaranth/lib/fifo.pyi +++ b/stubs/amaranth/lib/fifo.pyi @@ -4,7 +4,7 @@ This type stub file was generated by pyright. from .. import * from ..asserts import * -from coreblocks.utils import HasElaborate +from transactron.utils import HasElaborate """First-in first-out queues.""" __all__ = ["FIFOInterface", "SyncFIFO", "SyncFIFOBuffered", "AsyncFIFO", "AsyncFIFOBuffered"] diff --git a/stubs/amaranth/lib/scheduler.pyi b/stubs/amaranth/lib/scheduler.pyi index 3bd76c34e..b9662f19e 100644 --- a/stubs/amaranth/lib/scheduler.pyi +++ b/stubs/amaranth/lib/scheduler.pyi @@ -1,5 +1,5 @@ from .. import * -from coreblocks.utils import HasElaborate +from transactron.utils import HasElaborate class RoundRobin(Elaboratable): count: int diff --git a/transactron/lib/transformers.py b/transactron/lib/transformers.py index 5bcf0a4d3..8053180bd 100644 --- a/transactron/lib/transformers.py +++ b/transactron/lib/transformers.py @@ -20,7 +20,7 @@ ] -class Transformer(ABC): +class Transformer(ABC, Elaboratable): """Method transformer abstract class. Method transformers construct a new method which utilizes other methods. @@ -46,7 +46,7 @@ def use(self, m: ModuleLike): return self.method -class MethodMap(Transformer, Elaboratable): +class MethodMap(Transformer): """Bidirectional map for methods. Takes a target method and creates a transformed method which calls the @@ -102,7 +102,7 @@ def _(arg): return m -class MethodFilter(Transformer, Elaboratable): +class MethodFilter(Transformer): """Method filter. Takes a target method and creates a method which calls the target method @@ -176,7 +176,7 @@ def _(arg): return m -class MethodProduct(Transformer, Elaboratable): +class MethodProduct(Transformer): def __init__( self, targets: list[Method], @@ -224,7 +224,7 @@ def _(arg): return m -class MethodTryProduct(Transformer, Elaboratable): +class MethodTryProduct(Transformer): def __init__( self, targets: list[Method], @@ -276,7 +276,7 @@ def _(arg): return m -class Collector(Transformer, Elaboratable): +class Collector(Transformer): """Single result collector. Creates method that collects results of many methods with identical From 2dc56f8d1fb565233b20a69fba30532a92c35733 Mon Sep 17 00:00:00 2001 From: piotro888 Date: Mon, 4 Dec 2023 14:40:43 +0100 Subject: [PATCH 17/25] Exceptions handling (#523) --- coreblocks/core.py | 26 ++++++++---- coreblocks/frontend/fetch.py | 39 ++++++++++++++---- coreblocks/fu/exception.py | 2 +- coreblocks/fu/jumpbranch.py | 6 ++- coreblocks/lsu/dummyLsu.py | 2 +- coreblocks/params/layouts.py | 8 ++++ coreblocks/stages/retirement.py | 39 +++++++++++++++--- coreblocks/structs_common/csr.py | 8 +++- coreblocks/structs_common/csr_generic.py | 31 +++++++++++++-- coreblocks/structs_common/exception.py | 14 ++++++- coreblocks/structs_common/instr_counter.py | 46 ++++++++++++++++++++++ test/asm/exception_handler.asm | 25 ++++++++++++ test/fu/functional_common.py | 2 +- test/fu/test_jb_unit.py | 3 +- test/lsu/test_dummylsu.py | 2 + test/stages/test_retirement.py | 17 +++++++- test/structs_common/test_csr.py | 2 +- test/structs_common/test_exception.py | 3 +- test/test_core.py | 1 + 19 files changed, 241 insertions(+), 35 deletions(-) create mode 100644 coreblocks/structs_common/instr_counter.py create mode 100644 test/asm/exception_handler.asm diff --git a/coreblocks/core.py b/coreblocks/core.py index b0c6257db..b0793e06d 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -2,6 +2,7 @@ from coreblocks.params.dependencies import DependencyManager from coreblocks.stages.func_blocks_unifier import FuncBlocksUnifier +from coreblocks.structs_common.instr_counter import CoreInstructionCounter from transactron.core import Transaction, TModule from transactron.lib import FIFO, ConnectTrans from coreblocks.params.layouts import * @@ -20,6 +21,7 @@ from coreblocks.frontend.icache import ICache, SimpleWBCacheRefiller, ICacheBypass from coreblocks.peripherals.wishbone import WishboneMaster, WishboneBus from coreblocks.frontend.fetch import Fetch, UnalignedFetch +from transactron.lib.transformers import MethodMap, MethodProduct from transactron.utils.fifo import BasicFifo __all__ = ["Core"] @@ -41,6 +43,8 @@ def __init__(self, *, gen_params: GenParams, wb_instr_bus: WishboneBus, wb_data_ self.gen_params.get(SchedulerLayouts).free_rf_layout, 2**self.gen_params.phys_regs_bits ) + self.core_counter = CoreInstructionCounter(self.gen_params) + cache_layouts = self.gen_params.get(ICacheLayouts) if gen_params.icache_params.enable: self.icache_refiller = SimpleWBCacheRefiller( @@ -50,11 +54,6 @@ def __init__(self, *, gen_params: GenParams, wb_instr_bus: WishboneBus, wb_data_ else: self.icache = ICacheBypass(cache_layouts, gen_params.icache_params, self.wb_master_instr) - if Extension.C in gen_params.isa.extensions: - self.fetch = UnalignedFetch(self.gen_params, self.icache, self.fifo_fetch.write) - else: - self.fetch = Fetch(self.gen_params, self.icache, self.fifo_fetch.write) - self.FRAT = FRAT(gen_params=self.gen_params) self.RRAT = RRAT(gen_params=self.gen_params) self.RF = RegisterFile(gen_params=self.gen_params) @@ -97,11 +96,21 @@ def elaborate(self, platform): m.submodules.RF = rf = self.RF m.submodules.ROB = rob = self.ROB - m.submodules.fifo_fetch = self.fifo_fetch if self.icache_refiller: m.submodules.icache_refiller = self.icache_refiller m.submodules.icache = self.icache - m.submodules.fetch = self.fetch + + drop_args_transform = (self.gen_params.get(FetchLayouts).raw_instr, lambda _a, _b: {}) + core_counter_increment_discard_map = MethodMap(self.core_counter.increment, i_transform=drop_args_transform) + fetch_continue = MethodProduct([self.fifo_fetch.write, core_counter_increment_discard_map.use(m)]) + + if Extension.C in self.gen_params.isa.extensions: + m.submodules.fetch = self.fetch = UnalignedFetch(self.gen_params, self.icache, fetch_continue.use(m)) + else: + m.submodules.fetch = self.fetch = Fetch(self.gen_params, self.icache, fetch_continue.use(m)) + + m.submodules.fifo_fetch = self.fifo_fetch + m.submodules.core_counter = self.core_counter m.submodules.fifo_decode = fifo_decode = FIFO(self.gen_params.get(DecodeLayouts).decoded_instr, 2) m.submodules.decode = Decode( @@ -137,6 +146,9 @@ def elaborate(self, platform): precommit=self.func_blocks_unifier.get_extra_method(InstructionPrecommitKey()), exception_cause_get=self.exception_cause_register.get, frat_rename=frat.rename, + fetch_continue=self.fetch.verify_branch, + fetch_stall=self.fetch.stall_exception, + instr_decrement=self.core_counter.decrement, ) m.submodules.csr_generic = self.csr_generic diff --git a/coreblocks/frontend/fetch.py b/coreblocks/frontend/fetch.py index 96e0da3af..b2ab6af95 100644 --- a/coreblocks/frontend/fetch.py +++ b/coreblocks/frontend/fetch.py @@ -30,6 +30,7 @@ def __init__(self, gen_params: GenParams, icache: ICacheInterface, cont: Method) self.cont = cont self.verify_branch = Method(i=self.gp.get(FetchLayouts).branch_verify) + self.stall_exception = Method() # PC of the last fetched instruction. For now only used in tests. self.pc = Signal(self.gp.isa.xlen) @@ -44,16 +45,23 @@ def elaborate(self, platform): speculative_pc = Signal(self.gp.isa.xlen, reset=self.gp.start_pc) stalled = Signal() + stalled_unsafe = Signal() + stalled_exception = Signal() spin = Signal() + m.d.av_comb += stalled.eq(stalled_unsafe | stalled_exception) + with Transaction().body(m, request=~stalled): self.icache.issue_req(m, addr=speculative_pc) self.fetch_target_queue.write(m, addr=speculative_pc, spin=spin) m.d.sync += speculative_pc.eq(speculative_pc + self.gp.isa.ilen_bytes) - def stall(): - m.d.sync += stalled.eq(1) + def stall(exception=False): + if exception: + m.d.sync += stalled_exception.eq(1) + else: + m.d.sync += stalled_unsafe.eq(1) with m.If(~stalled): m.d.sync += spin.eq(~spin) @@ -85,9 +93,15 @@ def stall(): self.cont(m, instr=instr, pc=target.addr, access_fault=fetch_error, rvc=0) @def_method(m, self.verify_branch, ready=stalled) - def _(from_pc: Value, next_pc: Value): + def _(from_pc: Value, next_pc: Value, resume_from_exception: Value): m.d.sync += speculative_pc.eq(next_pc) - m.d.sync += stalled.eq(0) + m.d.sync += stalled_unsafe.eq(0) + with m.If(resume_from_exception): + m.d.sync += stalled_exception.eq(0) + + @def_method(m, self.stall_exception) + def _(): + stall(exception=True) return m @@ -115,6 +129,7 @@ def __init__(self, gen_params: GenParams, icache: ICacheInterface, cont: Method) self.cont = cont self.verify_branch = Method(i=self.gp.get(FetchLayouts).branch_verify) + self.stall_exception = Method() # PC of the last fetched instruction. For now only used in tests. self.pc = Signal(self.gp.isa.xlen) @@ -131,6 +146,9 @@ def elaborate(self, platform) -> TModule: flushing = Signal() stalled = Signal() + stalled_unsafe = Signal() + stalled_exception = Signal() + m.d.av_comb += stalled.eq(stalled_unsafe | stalled_exception) with Transaction().body(m, request=~stalled): aligned_pc = Cat(Repl(0, 2), cache_req_pc[2:]) @@ -203,7 +221,7 @@ def elaborate(self, platform) -> TModule: with m.If((resp_valid & ready_to_dispatch) | (cache_resp.error & ~stalled)): with m.If(unsafe_instr | cache_resp.error): - m.d.sync += stalled.eq(1) + m.d.sync += stalled_unsafe.eq(1) m.d.sync += flushing.eq(1) m.d.sync += self.pc.eq(current_pc) @@ -213,9 +231,16 @@ def elaborate(self, platform) -> TModule: self.cont(m, instr=instr, pc=current_pc, access_fault=cache_resp.error, rvc=is_rvc) @def_method(m, self.verify_branch, ready=(stalled & ~flushing)) - def _(from_pc: Value, next_pc: Value): + def _(from_pc: Value, next_pc: Value, resume_from_exception: Value): m.d.sync += cache_req_pc.eq(next_pc) m.d.sync += current_pc.eq(next_pc) - m.d.sync += stalled.eq(0) + m.d.sync += stalled_unsafe.eq(0) + with m.If(resume_from_exception): + m.d.sync += stalled_exception.eq(0) + + @def_method(m, self.stall_exception) + def _(): + m.d.sync += stalled_exception.eq(1) + m.d.sync += flushing.eq(1) return m diff --git a/coreblocks/fu/exception.py b/coreblocks/fu/exception.py index 4ef733e8c..811423b0b 100644 --- a/coreblocks/fu/exception.py +++ b/coreblocks/fu/exception.py @@ -83,7 +83,7 @@ def _(arg): with OneHotCase(ExceptionUnitFn.Fn.INSTR_PAGE_FAULT): m.d.comb += cause.eq(ExceptionCause.INSTRUCTION_PAGE_FAULT) - self.report(m, rob_id=arg.rob_id, cause=cause) + self.report(m, rob_id=arg.rob_id, cause=cause, pc=arg.pc) fifo.write(m, result=0, exception=1, rob_id=arg.rob_id, rp_dst=arg.rp_dst) diff --git a/coreblocks/fu/jumpbranch.py b/coreblocks/fu/jumpbranch.py index b42320a23..cb98c79f0 100644 --- a/coreblocks/fu/jumpbranch.py +++ b/coreblocks/fu/jumpbranch.py @@ -164,13 +164,15 @@ def _(arg): with m.If((decoder.decode_fn != JumpBranchFn.Fn.AUIPC) & jb.taken & jmp_addr_misaligned): m.d.comb += exception.eq(1) report = self.dm.get_dependency(ExceptionReportKey()) - report(m, rob_id=arg.rob_id, cause=ExceptionCause.INSTRUCTION_ADDRESS_MISALIGNED) + report(m, rob_id=arg.rob_id, cause=ExceptionCause.INSTRUCTION_ADDRESS_MISALIGNED, pc=arg.pc) fifo_res.write(m, rob_id=arg.rob_id, result=jb.reg_res, rp_dst=arg.rp_dst, exception=exception) # skip writing next branch target for auipc with m.If(decoder.decode_fn != JumpBranchFn.Fn.AUIPC): - fifo_branch.write(m, from_pc=jb.in_pc, next_pc=Mux(jb.taken, jb.jmp_addr, jb.reg_res)) + fifo_branch.write( + m, from_pc=jb.in_pc, next_pc=Mux(jb.taken, jb.jmp_addr, jb.reg_res), resume_from_exception=0 + ) return m diff --git a/coreblocks/lsu/dummyLsu.py b/coreblocks/lsu/dummyLsu.py index 562c76988..ce2d19aa4 100644 --- a/coreblocks/lsu/dummyLsu.py +++ b/coreblocks/lsu/dummyLsu.py @@ -267,7 +267,7 @@ def _(): res = results.read(m) with m.If(res["exception"]): - self.report(m, rob_id=current_instr.rob_id, cause=res["cause"]) + self.report(m, rob_id=current_instr.rob_id, cause=res["cause"], pc=current_instr.pc) m.d.sync += issued.eq(0) m.d.sync += valid.eq(0) diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index 7e13b74b2..abec94fda 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -429,6 +429,7 @@ def __init__(self, gen_params: GenParams): self.branch_verify: LayoutList = [ ("from_pc", gen_params.isa.xlen), ("next_pc", gen_params.isa.xlen), + ("resume_from_exception", 1), ] @@ -517,6 +518,7 @@ def __init__(self, gen_params: GenParams): "s1_val", "s2_val", "imm", + "pc", }, ) @@ -588,6 +590,12 @@ def __init__(self, gen_params: GenParams): self.get: LayoutList = [ fields.cause, fields.rob_id, + fields.pc, ] self.report = self.get + + +class CoreInstructionCounterLayouts: + def __init__(self, gen_params: GenParams): + self.decrement = [("empty", 1)] diff --git a/coreblocks/stages/retirement.py b/coreblocks/stages/retirement.py index 28ac8983f..288300b45 100644 --- a/coreblocks/stages/retirement.py +++ b/coreblocks/stages/retirement.py @@ -22,6 +22,9 @@ def __init__( precommit: Method, exception_cause_get: Method, frat_rename: Method, + fetch_continue: Method, + fetch_stall: Method, + instr_decrement: Method ): self.gen_params = gen_params self.rob_peek = rob_peek @@ -32,12 +35,16 @@ def __init__( self.precommit = precommit self.exception_cause_get = exception_cause_get self.rename = frat_rename + self.fetch_continue = fetch_continue + self.fetch_stall = fetch_stall + self.instr_decrement = instr_decrement self.instret_csr = DoubleCounterCSR(gen_params, CSRAddress.INSTRET, CSRAddress.INSTRETH) def elaborate(self, platform): m = TModule() + m_csr = self.gen_params.get(DependencyManager).get_dependency(GenericCSRRegistersKey()).m_mode m.submodules.instret_csr = self.instret_csr side_fx = Signal(reset=1) @@ -45,8 +52,12 @@ def elaborate(self, platform): m.d.comb += side_fx_comb.eq(side_fx) + # Use Forwarders for calling those methods, because we don't want them to block + # Retirement Transaction (because of un-readiness) during normal operation (both + # methods are unused when not handling exceptions) fields = self.gen_params.get(CommonLayoutFields) m.submodules.frat_fix = frat_fix = Forwarder([fields.rl_dst, fields.rp_dst]) + m.submodules.fetch_continue_fwd = fetch_continue_fwd = Forwarder([fields.pc]) with Transaction().body(m): # TODO: do we prefer single precommit call per instruction? @@ -58,18 +69,20 @@ def elaborate(self, platform): with Transaction().body(m): rob_entry = self.rob_retire(m) - # TODO: Trigger InterruptCoordinator (handle exception) when rob_entry.exception is set. with m.If(rob_entry.exception & side_fx): + # Start flushing the core m.d.sync += side_fx.eq(0) m.d.comb += side_fx_comb.eq(0) + self.fetch_stall(m) + # TODO: only set mcause/trigger IC if cause is actual exception and not e.g. # misprediction or pipeline flush after some fence.i or changing ISA - mcause = self.gen_params.get(DependencyManager).get_dependency(GenericCSRRegistersKey()).mcause - cause = self.exception_cause_get(m).cause + cause_register = self.exception_cause_get(m) entry = Signal(self.gen_params.isa.xlen) # MSB is exception bit - m.d.comb += entry.eq(cause | (1 << (self.gen_params.isa.xlen - 1))) - mcause.write(m, entry) + m.d.comb += entry.eq(cause_register.cause | (1 << (self.gen_params.isa.xlen - 1))) + m_csr.mcause.write(m, entry) + m_csr.mepc.write(m, cause_register.pc) # set rl_dst -> rp_dst in R-RAT rat_out = self.r_rat_commit( @@ -92,8 +105,24 @@ def elaborate(self, platform): with m.If(rp_freed): # don't put rp0 to free list - reserved to no-return instructions self.free_rf_put(m, rp_freed) + core_empty = self.instr_decrement(m) + # cycle when fetch_stop is called, is the last cycle when new instruction can be fetched. + # in this case, counter will be incremented and result correct (non-empty). + with m.If(~side_fx_comb & core_empty): + # Resume core operation from exception handler + + # mtvec without mode is [mxlen-1:2], mode is two last bits. Only direct mode is supported + resume_pc = m_csr.mtvec.read(m) & ~(0b11) + fetch_continue_fwd.write(m, pc=resume_pc) + + m.d.sync += side_fx.eq(1) + with Transaction().body(m): data = frat_fix.read(m) self.rename(m, rl_s1=0, rl_s2=0, rl_dst=data["rl_dst"], rp_dst=data["rp_dst"]) + with Transaction().body(m): + pc = fetch_continue_fwd.read(m).pc + self.fetch_continue(m, from_pc=0, next_pc=pc, resume_from_exception=1) + return m diff --git a/coreblocks/structs_common/csr.py b/coreblocks/structs_common/csr.py index d7d5419f2..59e726139 100644 --- a/coreblocks/structs_common/csr.py +++ b/coreblocks/structs_common/csr.py @@ -328,7 +328,7 @@ def _(): with m.If(exception): report = self.dependency_manager.get_dependency(ExceptionReportKey()) - report(m, rob_id=instr.rob_id, cause=ExceptionCause.ILLEGAL_INSTRUCTION) + report(m, rob_id=instr.rob_id, cause=ExceptionCause.ILLEGAL_INSTRUCTION, pc=instr.pc) m.d.sync += exception.eq(0) return { @@ -340,7 +340,11 @@ def _(): @def_method(m, self.fetch_continue, accepted) def _(): - return {"from_pc": instr.pc, "next_pc": instr.pc + self.gen_params.isa.ilen_bytes} + return { + "from_pc": instr.pc, + "next_pc": instr.pc + self.gen_params.isa.ilen_bytes, + "resume_from_exception": False, + } # Generate precommitting signal from precommit @def_method(m, self.precommit) diff --git a/coreblocks/structs_common/csr_generic.py b/coreblocks/structs_common/csr_generic.py index 45f319fa7..4c7234f87 100644 --- a/coreblocks/structs_common/csr_generic.py +++ b/coreblocks/structs_common/csr_generic.py @@ -9,7 +9,10 @@ class CSRAddress(IntEnum, shape=12): + MTVEC = 0x305 + MEPC = 0x341 MCAUSE = 0x342 + CYCLE = 0xC00 TIME = 0xC01 INSTRET = 0xC02 @@ -67,20 +70,42 @@ def _(): return m +class MachineModeCSRRegisters(Elaboratable): + def __init__(self, gp: GenParams): + self.mcause = CSRRegister(CSRAddress.MCAUSE, gp) + + # SPEC: The mtvec register must always be implemented, but can contain a read-only value. + # set `MODE` as fixed to 0 - Direct mode "All exceptions set pc to BASE" + self.mtvec = CSRRegister(CSRAddress.MTVEC, gp, ro_bits=0b11) + + # TODO: set low bits R/O based on gp align + self.mepc = CSRRegister(CSRAddress.MEPC, gp) + + def elaborate(self, platform): + m = Module() + + m.submodules.mcause = self.mcause + m.submodules.mtvec = self.mtvec + m.submodules.mepc = self.mepc + + return m + + class GenericCSRRegisters(Elaboratable): def __init__(self, gp: GenParams): + self.m_mode = MachineModeCSRRegisters(gp) + self.csr_cycle = DoubleCounterCSR(gp, CSRAddress.CYCLE, CSRAddress.CYCLEH) # TODO: CYCLE should be alias to TIME self.csr_time = DoubleCounterCSR(gp, CSRAddress.TIME, CSRAddress.TIMEH) - self.mcause = CSRRegister(CSRAddress.MCAUSE, gp) - def elaborate(self, platform): m = TModule() + m.submodules.m_mode = self.m_mode + m.submodules.csr_cycle = self.csr_cycle m.submodules.csr_time = self.csr_time - m.submodules.mcause = self.mcause with Transaction().body(m): self.csr_cycle.increment(m) diff --git a/coreblocks/structs_common/exception.py b/coreblocks/structs_common/exception.py index 98b3a76dd..9e17bc6f9 100644 --- a/coreblocks/structs_common/exception.py +++ b/coreblocks/structs_common/exception.py @@ -38,11 +38,20 @@ def should_update_prioriy(m: TModule, current_cause: Value, new_cause: Value) -> class ExceptionCauseRegister(Elaboratable): + """ExceptionCauseRegister + + Stores parameters of earliest (in instruction order) exception, to save resources in the `ReorderBuffer`. + All FUs that report exceptions should `report` the details to `ExceptionCauseRegister` and set `exception` bit in + result data. Exception order and priority is computed in this module. + If `exception` bit is set in the ROB, `Retirement` stage fetches exception details from this module. + """ + def __init__(self, gp: GenParams, rob_get_indices: Method): self.gp = gp self.cause = Signal(ExceptionCause) self.rob_id = Signal(gp.rob_entries_bits) + self.pc = Signal(gp.isa.xlen) self.valid = Signal() self.report = Method(i=gp.get(ExceptionRegisterLayouts).report) @@ -61,7 +70,7 @@ def elaborate(self, platform): m = TModule() @def_method(m, self.report) - def _(cause, rob_id): + def _(cause, rob_id, pc): should_write = Signal() with m.If(self.valid & (self.rob_id == rob_id)): @@ -77,12 +86,13 @@ def _(cause, rob_id): with m.If(should_write): m.d.sync += self.rob_id.eq(rob_id) m.d.sync += self.cause.eq(cause) + m.d.sync += self.pc.eq(pc) m.d.sync += self.valid.eq(1) @def_method(m, self.get) def _(): - return {"rob_id": self.rob_id, "cause": self.cause} + return {"rob_id": self.rob_id, "cause": self.cause, "pc": self.pc} @def_method(m, self.clear) def _(): diff --git a/coreblocks/structs_common/instr_counter.py b/coreblocks/structs_common/instr_counter.py new file mode 100644 index 000000000..b8c38447a --- /dev/null +++ b/coreblocks/structs_common/instr_counter.py @@ -0,0 +1,46 @@ +from amaranth import * +from coreblocks.params.genparams import GenParams +from coreblocks.params.layouts import CoreInstructionCounterLayouts +from transactron.core import Method, TModule, def_method + + +class CoreInstructionCounter(Elaboratable): + """ + Counts instructions currently processed in core. + Used in exception handling, to wait for core flush to finish. + + Attributes + ---------- + increment : Method + Increments the counter. Should be called when new instruction leaves fetch stage. + decrement : Method + Decrements the counter, and returns if the counter will be equal to zero after that cycle (it was the + last instruction in core and no new instruction is fetched). Should be called when instruction is retired. + """ + + def __init__(self, gp: GenParams): + self.gp = gp + + self.increment = Method() + self.decrement = Method(o=gp.get(CoreInstructionCounterLayouts).decrement) + + def elaborate(self, platform) -> TModule: + m = TModule() + + count = Signal(self.gp.rob_entries_bits + 1) + + with m.If(self.increment.run & ~self.decrement.run): + m.d.sync += count.eq(count + 1) + + with m.If(self.decrement.run & ~self.increment.run): + m.d.sync += count.eq(count - 1) + + @def_method(m, self.increment, ready=(count != (2 ** count.shape().width) - 1)) + def _(): + pass + + @def_method(m, self.decrement) + def _(): + return (count == 1) & ~self.increment.run + + return m diff --git a/test/asm/exception_handler.asm b/test/asm/exception_handler.asm new file mode 100644 index 000000000..2a0f9c1de --- /dev/null +++ b/test/asm/exception_handler.asm @@ -0,0 +1,25 @@ + li x2, 1 + li x4, 987 # target fibonnaci number + li x15, 0 + + la x6, exception_handler + csrw mtvec, x6 # set-up handler +loop: + add x3, x2, x1 +.4byte 0 # raise exception without stalling the fetcher + mv x1, x2 + mv x2, x3 + bne x2, x4, loop +infloop: + j infloop + +exception_handler: + mv x6, x2 + li x2, 42 # do some register activity + mv x2, x6 + + addi x15, x15, 1 # count exceptions + + csrr x6, mepc # resume program execution, + addi x6, x6, 4 # but skip unimplemented instruction + jr x6 diff --git a/test/fu/functional_common.py b/test/fu/functional_common.py index 5b81970fd..481aee667 100644 --- a/test/fu/functional_common.py +++ b/test/fu/functional_common.py @@ -154,7 +154,7 @@ def setUp(self): self.responses.append({"rob_id": rob_id, "rp_dst": rp_dst, "exception": int(cause is not None)} | results) if cause is not None: - self.exceptions.append({"rob_id": rob_id, "cause": cause}) + self.exceptions.append({"rob_id": rob_id, "cause": cause, "pc": pc}) def random_wait(self): for i in range(random.randint(0, self.max_wait)): diff --git a/test/fu/test_jb_unit.py b/test/fu/test_jb_unit.py index a37a4cb0c..b1e728cf6 100644 --- a/test/fu/test_jb_unit.py +++ b/test/fu/test_jb_unit.py @@ -30,6 +30,7 @@ def _(arg): return { "from_pc": br.from_pc, "next_pc": br.next_pc, + "resume_from_exception": 0, "result": res.result, "rob_id": res.rob_id, "rp_dst": res.rp_dst, @@ -80,7 +81,7 @@ def compute_result(i1: int, i2: int, i_imm: int, pc: int, fn: JumpBranchFn.Fn, x if next_pc & 0b11 != 0: exception = ExceptionCause.INSTRUCTION_ADDRESS_MISALIGNED - return {"result": res, "from_pc": pc, "next_pc": next_pc} | ( + return {"result": res, "from_pc": pc, "next_pc": next_pc, "resume_from_exception": 0} | ( {"exception": exception} if exception is not None else {} ) diff --git a/test/lsu/test_dummylsu.py b/test/lsu/test_dummylsu.py index a04037be8..b93365b8f 100644 --- a/test/lsu/test_dummylsu.py +++ b/test/lsu/test_dummylsu.py @@ -152,6 +152,7 @@ def generate_instr(self, max_reg_val, max_imm_val): "s1_val": s1_val, "s2_val": 0, "imm": imm, + "pc": 0, } self.instr_queue.append(instr) self.mem_data_queue.append( @@ -172,6 +173,7 @@ def generate_instr(self, max_reg_val, max_imm_val): "cause": ExceptionCause.LOAD_ADDRESS_MISALIGNED if misaligned else ExceptionCause.LOAD_ACCESS_FAULT, + "pc": 0, } ) diff --git a/test/stages/test_retirement.py b/test/stages/test_retirement.py index 30687a77c..9c50142f8 100644 --- a/test/stages/test_retirement.py +++ b/test/stages/test_retirement.py @@ -1,4 +1,4 @@ -from coreblocks.params.layouts import ExceptionRegisterLayouts +from coreblocks.params.layouts import CoreInstructionCounterLayouts, ExceptionRegisterLayouts, FetchLayouts from coreblocks.stages.retirement import * from coreblocks.structs_common.csr_generic import GenericCSRRegisters @@ -24,6 +24,8 @@ def elaborate(self, platform): lsu_layouts = self.gen_params.get(LSULayouts) scheduler_layouts = self.gen_params.get(SchedulerLayouts) exception_layouts = self.gen_params.get(ExceptionRegisterLayouts) + fetch_layouts = self.gen_params.get(FetchLayouts) + core_instr_counter_layouts = self.gen_params.get(CoreInstructionCounterLayouts) m.submodules.r_rat = self.rat = RRAT(gen_params=self.gen_params) m.submodules.f_rat = self.frat = FRAT(gen_params=self.gen_params) @@ -43,6 +45,14 @@ def elaborate(self, platform): m.submodules.generic_csr = self.generic_csr = GenericCSRRegisters(self.gen_params) self.gen_params.get(DependencyManager).add_dependency(GenericCSRRegistersKey(), self.generic_csr) + m.submodules.mock_fetch_stall = self.mock_fetch_stall = TestbenchIO(Adapter()) + m.submodules.mock_fetch_continue = self.mock_fetch_continue = TestbenchIO( + Adapter(i=fetch_layouts.branch_verify) + ) + m.submodules.mock_instr_decrement = self.mock_instr_decrement = TestbenchIO( + Adapter(o=core_instr_counter_layouts.decrement) + ) + m.submodules.retirement = self.retirement = Retirement( self.gen_params, rob_retire=self.mock_rob_retire.adapter.iface, @@ -53,6 +63,9 @@ def elaborate(self, platform): precommit=self.mock_precommit.adapter.iface, exception_cause_get=self.mock_exception_cause.adapter.iface, frat_rename=self.frat.rename, + fetch_stall=self.mock_fetch_stall.adapter.iface, + fetch_continue=self.mock_fetch_continue.adapter.iface, + instr_decrement=self.mock_instr_decrement.adapter.iface, ) m.submodules.free_rf_fifo_adapter = self.free_rf_adapter = TestbenchIO(AdapterTrans(self.free_rf.read)) @@ -93,6 +106,8 @@ def setUp(self): def test_rand(self): retc = RetirementTestCircuit(self.gen_params) + yield from retc.mock_fetch_stall.enable() + @def_method_mock(lambda: retc.mock_rob_retire, enable=lambda: bool(self.submit_q), sched_prio=1) def retire_process(): return self.submit_q.popleft() diff --git a/test/structs_common/test_csr.py b/test/structs_common/test_csr.py index ffcdaf74a..fcf8eac83 100644 --- a/test/structs_common/test_csr.py +++ b/test/structs_common/test_csr.py @@ -191,7 +191,7 @@ def process_exception_test(self): self.assertEqual(res["exception"], 1) report = yield from self.dut.exception_report.call_result() assert report is not None - self.assertDictEqual({"rob_id": rob_id, "cause": ExceptionCause.ILLEGAL_INSTRUCTION}, report) + self.assertDictEqual({"rob_id": rob_id, "cause": ExceptionCause.ILLEGAL_INSTRUCTION, "pc": 0}, report) def test_exception(self): self.gp = GenParams(test_core_config) diff --git a/test/structs_common/test_exception.py b/test/structs_common/test_exception.py index 12238c8f0..2f88a0190 100644 --- a/test/structs_common/test_exception.py +++ b/test/structs_common/test_exception.py @@ -75,7 +75,8 @@ def process_test(): cause = random.choice(list(ExceptionCause)) report_rob = random.randint(0, self.rob_max) - report_arg = {"cause": cause, "rob_id": report_rob} + report_pc = random.randrange(2**self.gp.isa.xlen) + report_arg = {"cause": cause, "rob_id": report_rob, "pc": report_pc} yield from self.dut.report.call(report_arg) diff --git a/test/test_core.py b/test/test_core.py index 9951d608c..6414fd5de 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -245,6 +245,7 @@ def test_randomized(self): ("csr", "csr.asm", 200, {1: 1, 2: 4}, full_core_config), ("exception", "exception.asm", 200, {1: 1, 2: 2}, basic_core_config), ("exception_mem", "exception_mem.asm", 200, {1: 1, 2: 2}, basic_core_config), + ("exception_handler", "exception_handler.asm", 1100, {2: 987, 15: 15}, full_core_config), ], ) class TestCoreAsmSource(TestCoreBase): From 1dd13389ebb309430dfe8d691ba65023a9ee6a1d Mon Sep 17 00:00:00 2001 From: kindlermikolaj <62593571+kindlermikolaj@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:32:57 +0100 Subject: [PATCH 18/25] Implement physical memory attributes (PMA) for memory-mapped IO (MMIO) (#510) --- coreblocks/lsu/dummyLsu.py | 18 +++- coreblocks/lsu/pma.py | 69 +++++++++++++++ coreblocks/params/configurations.py | 8 +- coreblocks/params/genparams.py | 2 + coreblocks/params/layouts.py | 6 ++ test/lsu/test_pma.py | 128 ++++++++++++++++++++++++++++ 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 coreblocks/lsu/pma.py create mode 100644 test/lsu/test_pma.py diff --git a/coreblocks/lsu/dummyLsu.py b/coreblocks/lsu/dummyLsu.py index ce2d19aa4..a2099d873 100644 --- a/coreblocks/lsu/dummyLsu.py +++ b/coreblocks/lsu/dummyLsu.py @@ -7,6 +7,7 @@ from transactron.utils import assign, ModuleLike from coreblocks.utils.protocols import FuncBlock +from coreblocks.lsu.pma import PMAChecker __all__ = ["LSUDummy", "LSUBlockComponent"] @@ -200,11 +201,12 @@ def elaborate(self, platform): m = TModule() reserved = Signal() # current_instr is reserved valid = Signal() # current_instr is valid - execute = Signal() # start execution + precommiting = Signal() # start execution issued = Signal() # instruction was issued to the bus flush = Signal() # exception handling, requests are not issued current_instr = Record(self.lsu_layouts.rs.data_layout) + m.submodules.pma_checker = pma_checker = PMAChecker(self.gen_params) m.submodules.requester = requester = LSURequesterWB(self.gen_params, self.bus) m.submodules.results = results = self.forwarder = Forwarder(self.lsu_layouts.accept) @@ -217,6 +219,12 @@ def elaborate(self, platform): instr_is_load = Signal() m.d.comb += instr_is_load.eq(current_instr.exec_fn.op_type == OpType.LOAD) + addr = Signal(self.gen_params.isa.xlen) + m.d.comb += addr.eq(current_instr.s1_val + current_instr.imm) + + m.d.comb += pma_checker.addr.eq(addr) + pmas = pma_checker.result + @def_method(m, self.select, ~reserved) def _(): # We always return 0, because we have only one place in instruction storage. @@ -239,12 +247,14 @@ def _(reg_id: Value, reg_val: Value): # Issues load/store requests when the instruction is known, is a LOAD/STORE, and just before commit. # Memory loads can be issued speculatively. - do_issue = ~issued & instr_ready & ~flush & ~instr_is_fence & (execute | instr_is_load) + can_reorder = instr_is_load & ~pmas["mmio"] + want_issue = ~instr_is_fence & (precommiting | can_reorder) + do_issue = ~issued & instr_ready & ~flush & want_issue with Transaction().body(m, request=do_issue): m.d.sync += issued.eq(1) res = requester.issue( m, - addr=current_instr.s1_val + current_instr.imm, + addr=addr, data=current_instr.s2_val, funct3=current_instr.exec_fn.funct3, store=~instr_is_load, @@ -285,7 +295,7 @@ def _(rob_id: Value, side_fx: Value): with m.If(~side_fx): m.d.comb += flush.eq(1) with m.Elif(valid & (rob_id == current_instr.rob_id) & ~instr_is_fence): - m.d.comb += execute.eq(1) + m.d.comb += precommiting.eq(1) return m diff --git a/coreblocks/lsu/pma.py b/coreblocks/lsu/pma.py new file mode 100644 index 000000000..8e474a6bf --- /dev/null +++ b/coreblocks/lsu/pma.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass +from functools import reduce +from operator import or_ +from amaranth import * + +from coreblocks.params import * +from transactron.utils import HasElaborate +from transactron.core import TModule + + +@dataclass +class PMARegion: + """ + Data class for physical memory attributes contiguous region of memory. Region of memory + includes both start and end address. + + Attributes + ---------- + start : int + Defines beginning of region, start address is included in the region. + end : int + Defines end of region, end address is included in the region. + mmio : bool + Value True for this field indicates that memory region is MMIO. + """ + + start: int + end: int + mmio: bool = False + + +class PMAChecker(Elaboratable): + """ + Implementation of physical memory attributes checker. It may or may not be a part of LSU. + This is a combinational circuit with return value read from `result` output. + + Attributes + ---------- + addr : Signal + Memory address, for which PMAs are requested. + result : Record + PMAs for given address. + """ + + def __init__(self, gen_params: GenParams) -> None: + # poor man's interval list + self.segments = gen_params.pma + self.attr_layout = gen_params.get(PMALayouts).pma_attrs_layout + self.result = Record(self.attr_layout) + self.addr = Signal(gen_params.isa.xlen) + + def elaborate(self, platform) -> HasElaborate: + m = TModule() + + outputs = [Record(self.attr_layout) for _ in self.segments] + + # zero output if addr not in region, propagate value if addr in region + for i, segment in enumerate(self.segments): + start = segment.start + end = segment.end + + # check if addr in region + with m.If((self.addr >= start) & (self.addr <= end)): + m.d.comb += outputs[i].eq(segment.mmio) + + # OR all outputs + m.d.comb += self.result.eq(reduce(or_, outputs, 0)) + + return m diff --git a/coreblocks/params/configurations.py b/coreblocks/params/configurations.py index 7d463a03f..c87a9cda4 100644 --- a/coreblocks/params/configurations.py +++ b/coreblocks/params/configurations.py @@ -1,6 +1,8 @@ from collections.abc import Collection + import dataclasses -from dataclasses import dataclass +from dataclasses import dataclass, field +from coreblocks.lsu.pma import PMARegion from coreblocks.params.isa import Extension from coreblocks.params.fu_params import BlockComponentParams @@ -59,6 +61,8 @@ class CoreConfiguration: Allow partial support of extensions. _implied_extensions: Extenstion Bit flag specifing enabled extenstions that are not specified by func_units_config. Used in internal tests. + pma : list[PMARegion] + Definitions of PMAs per contiguous segments of memory. """ xlen: int = 32 @@ -80,6 +84,8 @@ class CoreConfiguration: _implied_extensions: Extension = Extension(0) + pma: list[PMARegion] = field(default_factory=list) + def replace(self, **kwargs): return dataclasses.replace(self, **kwargs) diff --git a/coreblocks/params/genparams.py b/coreblocks/params/genparams.py index 9bf84c23c..113a90706 100644 --- a/coreblocks/params/genparams.py +++ b/coreblocks/params/genparams.py @@ -72,6 +72,8 @@ def __init__(self, cfg: CoreConfiguration): self.isa = ISA(self.isa_str) + self.pma = cfg.pma + bytes_in_word = self.isa.xlen // 8 self.wb_params = WishboneParameters( data_width=self.isa.xlen, addr_width=self.isa.xlen - log2_int(bytes_in_word) diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index abec94fda..3238e9ba7 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -16,6 +16,7 @@ "UnsignedMulUnitLayouts", "RATLayouts", "LSULayouts", + "PMALayouts", "CSRLayouts", "ICacheLayouts", ] @@ -539,6 +540,11 @@ def __init__(self, gen_params: GenParams): self.accept: LayoutList = [fields.data, fields.exception, fields.cause] +class PMALayouts: + def __init__(self, gen_params: GenParams): + self.pma_attrs_layout = [("mmio", 1)] + + class CSRLayouts: """Layouts used in the control and status registers.""" diff --git a/test/lsu/test_pma.py b/test/lsu/test_pma.py new file mode 100644 index 000000000..3513ba360 --- /dev/null +++ b/test/lsu/test_pma.py @@ -0,0 +1,128 @@ +from amaranth.sim import Settle +from coreblocks.lsu.pma import PMAChecker, PMARegion + +from transactron.lib import Adapter +from coreblocks.params import OpType, GenParams +from coreblocks.lsu.dummyLsu import LSUDummy +from coreblocks.params.configurations import test_core_config +from coreblocks.params.isa import * +from coreblocks.params.keys import ExceptionReportKey +from coreblocks.params.dependencies import DependencyManager +from coreblocks.params.layouts import ExceptionRegisterLayouts +from coreblocks.peripherals.wishbone import * +from test.common import TestbenchIO, TestCaseWithSimulator, def_method_mock +from test.peripherals.test_wishbone import WishboneInterfaceWrapper + + +class TestPMADirect(TestCaseWithSimulator): + def verify_region(self, region: PMARegion): + for i in range(region.start, region.end + 1): + yield self.test_module.addr.eq(i) + yield Settle() + mmio = yield self.test_module.result["mmio"] + self.assertEqual(mmio, region.mmio) + + def process(self): + for r in self.pma_regions: + yield from self.verify_region(r) + + def test_pma_direct(self): + self.pma_regions = [ + PMARegion(0x0, 0xF, False), + PMARegion(0x10, 0xFF, True), + PMARegion(0x100, 0x10F, False), + PMARegion(0x110, 0x120, True), + PMARegion(0x121, 0x130, False), + ] + + self.gp = GenParams(test_core_config.replace(pma=self.pma_regions)) + self.test_module = PMAChecker(self.gp) + + with self.run_simulation(self.test_module) as sim: + sim.add_sync_process(self.process) + + +class PMAIndirectTestCircuit(Elaboratable): + def __init__(self, gen: GenParams): + self.gen = gen + + def elaborate(self, platform): + m = Module() + + wb_params = WishboneParameters( + data_width=self.gen.isa.ilen, + addr_width=32, + ) + + self.bus = WishboneMaster(wb_params) + + m.submodules.exception_report = self.exception_report = TestbenchIO( + Adapter(i=self.gen.get(ExceptionRegisterLayouts).report) + ) + + self.gen.get(DependencyManager).add_dependency(ExceptionReportKey(), self.exception_report.adapter.iface) + + m.submodules.func_unit = func_unit = LSUDummy(self.gen, self.bus) + + m.submodules.select_mock = self.select = TestbenchIO(AdapterTrans(func_unit.select)) + m.submodules.insert_mock = self.insert = TestbenchIO(AdapterTrans(func_unit.insert)) + m.submodules.update_mock = self.update = TestbenchIO(AdapterTrans(func_unit.update)) + m.submodules.get_result_mock = self.get_result = TestbenchIO(AdapterTrans(func_unit.get_result)) + m.submodules.precommit_mock = self.precommit = TestbenchIO(AdapterTrans(func_unit.precommit)) + self.io_in = WishboneInterfaceWrapper(self.bus.wbMaster) + m.submodules.bus = self.bus + return m + + +class TestPMAIndirect(TestCaseWithSimulator): + def get_instr(self, addr): + return { + "rp_s1": 0, + "rp_s2": 0, + "rp_dst": 1, + "rob_id": 1, + "exec_fn": {"op_type": OpType.LOAD, "funct3": Funct3.B, "funct7": 0}, + "s1_val": 0, + "s2_val": 1, + "imm": addr, + } + + def verify_region(self, region: PMARegion): + for addr in range(region.start, region.end + 1): + instr = self.get_instr(addr) + yield from self.test_module.select.call() + yield from self.test_module.insert.call(rs_data=instr, rs_entry_id=1) + if region.mmio is True: + wb = self.test_module.io_in.wb + for i in range(100): # 100 cycles is more than enough + wb_requested = (yield wb.stb) and (yield wb.cyc) + self.assertEqual(wb_requested, False) + + yield from self.test_module.precommit.call(rob_id=1, side_fx=1) + + yield from self.test_module.io_in.slave_wait() + yield from self.test_module.io_in.slave_respond((addr << (addr % 4) * 8)) + yield Settle() + v = yield from self.test_module.get_result.call() + self.assertEqual(v["result"], addr) + + def process(self): + for region in self.pma_regions: + yield from self.verify_region(region) + + def test_pma_indirect(self): + self.pma_regions = [ + PMARegion(0x0, 0xF, True), + PMARegion(0x10, 0x1F, False), + PMARegion(0x20, 0x2F, True), + ] + self.gp = GenParams(test_core_config.replace(pma=self.pma_regions)) + self.test_module = PMAIndirectTestCircuit(self.gp) + + @def_method_mock(lambda: self.test_module.exception_report) + def exception_consumer(arg): + self.assertTrue(False) + + with self.run_simulation(self.test_module) as sim: + sim.add_sync_process(self.process) + sim.add_sync_process(exception_consumer) From 6e5e9d5f78e58b7b9a941e4366dfe31005edbd02 Mon Sep 17 00:00:00 2001 From: piotro888 Date: Fri, 15 Dec 2023 15:16:55 +0100 Subject: [PATCH 19/25] Clear ExcepionCauseRegister after flushing core (#534) --- coreblocks/core.py | 1 + coreblocks/stages/retirement.py | 5 +++++ coreblocks/structs_common/exception.py | 4 +--- test/asm/exception_handler.asm | 27 ++++++++++++++++++++++++++ test/stages/test_retirement.py | 3 +++ test/test_core.py | 2 +- 6 files changed, 38 insertions(+), 4 deletions(-) diff --git a/coreblocks/core.py b/coreblocks/core.py index b0793e06d..1c636edd0 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -145,6 +145,7 @@ def elaborate(self, platform): rf_free=rf.free, precommit=self.func_blocks_unifier.get_extra_method(InstructionPrecommitKey()), exception_cause_get=self.exception_cause_register.get, + exception_cause_clear=self.exception_cause_register.clear, frat_rename=frat.rename, fetch_continue=self.fetch.verify_branch, fetch_stall=self.fetch.stall_exception, diff --git a/coreblocks/stages/retirement.py b/coreblocks/stages/retirement.py index 288300b45..dd2784d6d 100644 --- a/coreblocks/stages/retirement.py +++ b/coreblocks/stages/retirement.py @@ -21,6 +21,7 @@ def __init__( rf_free: Method, precommit: Method, exception_cause_get: Method, + exception_cause_clear: Method, frat_rename: Method, fetch_continue: Method, fetch_stall: Method, @@ -34,6 +35,7 @@ def __init__( self.rf_free = rf_free self.precommit = precommit self.exception_cause_get = exception_cause_get + self.exception_cause_clear = exception_cause_clear self.rename = frat_rename self.fetch_continue = fetch_continue self.fetch_stall = fetch_stall @@ -115,6 +117,9 @@ def elaborate(self, platform): resume_pc = m_csr.mtvec.read(m) & ~(0b11) fetch_continue_fwd.write(m, pc=resume_pc) + # Release pending trap state - allow accepting new reports + self.exception_cause_clear(m) + m.d.sync += side_fx.eq(1) with Transaction().body(m): diff --git a/coreblocks/structs_common/exception.py b/coreblocks/structs_common/exception.py index 9e17bc6f9..29e51afab 100644 --- a/coreblocks/structs_common/exception.py +++ b/coreblocks/structs_common/exception.py @@ -5,7 +5,7 @@ from coreblocks.params.isa import ExceptionCause from coreblocks.params.layouts import ExceptionRegisterLayouts from coreblocks.params.keys import ExceptionReportKey -from transactron.core import Priority, TModule, def_method, Method +from transactron.core import TModule, def_method, Method def should_update_prioriy(m: TModule, current_cause: Value, new_cause: Value) -> Value: @@ -62,8 +62,6 @@ def __init__(self, gp: GenParams, rob_get_indices: Method): self.clear = Method() - self.clear.add_conflict(self.report, Priority.LEFT) - self.rob_get_indices = rob_get_indices def elaborate(self, platform): diff --git a/test/asm/exception_handler.asm b/test/asm/exception_handler.asm index 2a0f9c1de..d1dfca9fa 100644 --- a/test/asm/exception_handler.asm +++ b/test/asm/exception_handler.asm @@ -10,6 +10,33 @@ loop: mv x1, x2 mv x2, x3 bne x2, x4, loop + + # report another exception after full rob_idx overflow + # so it has the same rob index as previous report + li x10, 0 + li x11, 13 +rob_loop: + addi x10, x10, 1 + nop + nop + nop + nop + nop + nop + bne x10, x11, rob_loop + + nop + nop + nop + nop + nop + nop + nop + +.4byte 0 # exception + + li x11, 0xaaaa # verify exception return + infloop: j infloop diff --git a/test/stages/test_retirement.py b/test/stages/test_retirement.py index 9c50142f8..62a08c4a2 100644 --- a/test/stages/test_retirement.py +++ b/test/stages/test_retirement.py @@ -42,6 +42,8 @@ def elaborate(self, platform): m.submodules.mock_precommit = self.mock_precommit = TestbenchIO(Adapter(i=lsu_layouts.precommit)) m.submodules.mock_exception_cause = self.mock_exception_cause = TestbenchIO(Adapter(o=exception_layouts.get)) + m.submodules.mock_exception_clear = self.mock_exception_clear = TestbenchIO(Adapter()) + m.submodules.generic_csr = self.generic_csr = GenericCSRRegisters(self.gen_params) self.gen_params.get(DependencyManager).add_dependency(GenericCSRRegistersKey(), self.generic_csr) @@ -62,6 +64,7 @@ def elaborate(self, platform): rf_free=self.mock_rf_free.adapter.iface, precommit=self.mock_precommit.adapter.iface, exception_cause_get=self.mock_exception_cause.adapter.iface, + exception_cause_clear=self.mock_exception_clear.adapter.iface, frat_rename=self.frat.rename, fetch_stall=self.mock_fetch_stall.adapter.iface, fetch_continue=self.mock_fetch_continue.adapter.iface, diff --git a/test/test_core.py b/test/test_core.py index 6414fd5de..301fbbb5f 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -245,7 +245,7 @@ def test_randomized(self): ("csr", "csr.asm", 200, {1: 1, 2: 4}, full_core_config), ("exception", "exception.asm", 200, {1: 1, 2: 2}, basic_core_config), ("exception_mem", "exception_mem.asm", 200, {1: 1, 2: 2}, basic_core_config), - ("exception_handler", "exception_handler.asm", 1100, {2: 987, 15: 15}, full_core_config), + ("exception_handler", "exception_handler.asm", 1500, {2: 987, 11: 0xAAAA, 15: 16}, full_core_config), ], ) class TestCoreAsmSource(TestCoreBase): From a485e5a5e16b972d74135dd10ef13556f1bb9935 Mon Sep 17 00:00:00 2001 From: Marek Materzok Date: Fri, 15 Dec 2023 15:36:35 +0100 Subject: [PATCH 20/25] Transactron statistics and info (#535) --- .github/workflows/benchmark.yml | 2 +- .github/workflows/main.yml | 2 +- transactron/core.py | 47 ++++++++++++++++++++++++++++----- transactron/utils/utils.py | 10 +++++-- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cbba538a7..bee80912d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -109,7 +109,7 @@ jobs: - name: Generate Verilog run: | . venv/bin/activate - PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + PYTHONHASHSEED=0 TRANSACTRON_VERBOSE=1 ./scripts/gen_verilog.py --verbose --config full - uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 56b3dbed2..4227aae64 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: - name: Generate Verilog run: | . venv/bin/activate - PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + PYTHONHASHSEED=0 TRANSACTRON_VERBOSE=1 ./scripts/gen_verilog.py --verbose --config full - uses: actions/upload-artifact@v3 with: diff --git a/transactron/core.py b/transactron/core.py index ba0f11596..3b799c107 100644 --- a/transactron/core.py +++ b/transactron/core.py @@ -1,5 +1,5 @@ from collections import defaultdict, deque -from collections.abc import Sequence, Iterable, Callable, Mapping, Iterator +from collections.abc import Collection, Sequence, Iterable, Callable, Mapping, Iterator from contextlib import contextmanager from enum import Enum, auto from typing import ( @@ -14,6 +14,7 @@ Protocol, runtime_checkable, ) +from os import environ from graphlib import TopologicalSorter from typing_extensions import Self from amaranth import * @@ -22,7 +23,7 @@ from amaranth.hdl.dsl import FSM, _ModuleBuilderDomain from transactron.utils import AssignType, assign, ModuleConnector, silence_mustuse -from transactron.utils.utils import OneHotSwitchDynamic +from transactron.utils.utils import OneHotSwitchDynamic, average_dict_of_lists from ._utils import * from transactron.utils._typing import ValueLike, SignalBundle, HasElaborate, SwitchKey, ModuleLike from .graph import Owned, OwnershipGraph, Direction @@ -92,18 +93,18 @@ def rec(transaction: Transaction, source: TransactionBase): self.methods_by_transaction[transaction] = [] rec(transaction, transaction) - def transactions_for(self, elem: TransactionOrMethod) -> Iterable["Transaction"]: + def transactions_for(self, elem: TransactionOrMethod) -> Collection["Transaction"]: if isinstance(elem, Transaction): return [elem] else: return self.transactions_by_method[elem] @property - def methods(self) -> Iterable["Method"]: + def methods(self) -> Collection["Method"]: return self.transactions_by_method.keys() @property - def transactions(self) -> Iterable["Transaction"]: + def transactions(self) -> Collection["Transaction"]: return self.methods_by_transaction.keys() @property @@ -434,8 +435,9 @@ def elaborate(self, platform): m = Module() m.submodules.merge_manager = merge_manager + ccs = _graph_ccs(rgr) m.submodules._transactron_schedulers = ModuleConnector( - *[self.cc_scheduler(method_map, cgr, cc, porder) for cc in _graph_ccs(rgr)] + *[self.cc_scheduler(method_map, cgr, cc, porder) for cc in ccs] ) method_enables = self._method_enables(method_map) @@ -457,8 +459,41 @@ def elaborate(self, platform): for i in OneHotSwitchDynamic(m, runs): m.d.comb += method.data_in.eq(method_args[method][i]) + if "TRANSACTRON_VERBOSE" in environ: + self.print_info(cgr, porder, ccs, method_map) + return m + def print_info( + self, cgr: TransactionGraph, porder: PriorityOrder, ccs: list[GraphCC["Transaction"]], method_map: MethodMap + ): + print("Transactron statistics") + print(f"\tMethods: {len(method_map.methods)}") + print(f"\tTransactions: {len(method_map.transactions)}") + print(f"\tIndependent subgraphs: {len(ccs)}") + print(f"\tAvg callers per method: {average_dict_of_lists(method_map.transactions_by_method):.2f}") + print(f"\tAvg conflicts per transaction: {average_dict_of_lists(cgr):.2f}") + print("") + print("Transaction subgraphs") + for cc in ccs: + ccl = list(cc) + ccl.sort(key=lambda t: porder[t]) + for t in ccl: + print(f"\t{t.name}") + print("") + print("Calling transactions per method") + for m, ts in method_map.transactions_by_method.items(): + print(f"\t{m.owned_name}") + for t in ts: + print(f"\t\t{t.name}") + print("") + print("Called methods per transaction") + for t, ms in method_map.methods_by_transaction.items(): + print(f"\t{t.name}") + for m in ms: + print(f"\t\t{m.owned_name}") + print("") + def visual_graph(self, fragment): graph = OwnershipGraph(fragment) method_map = MethodMap(self.transactions) diff --git a/transactron/utils/utils.py b/transactron/utils/utils.py index bbd1e7310..77ed3c864 100644 --- a/transactron/utils/utils.py +++ b/transactron/utils/utils.py @@ -1,7 +1,8 @@ from contextlib import contextmanager from enum import Enum -from typing import Literal, Optional, TypeAlias, cast, overload -from collections.abc import Iterable, Mapping +from typing import Any, Literal, Optional, TypeAlias, cast, overload +from collections.abc import Iterable, Mapping, Sized +from statistics import fmean from amaranth import * from amaranth.hdl.ast import Assign, ArrayProxy from amaranth.lib import data @@ -24,6 +25,7 @@ "count_leading_zeros", "count_trailing_zeros", "mod_incr", + "average_dict_of_lists", ] @@ -423,6 +425,10 @@ def bits_from_int(num: int, lower: int, length: int): return (num >> lower) & ((1 << (length)) - 1) +def average_dict_of_lists(d: Mapping[Any, Sized]) -> float: + return fmean(map(lambda xs: len(xs), d.values())) + + class ModuleConnector(Elaboratable): """ An Elaboratable to create a new module, which will have all arguments From 9e99b5b6d68e067d879ac652344fc044e0c8507a Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Sat, 16 Dec 2023 12:59:07 +0100 Subject: [PATCH 21/25] Autumn cleaning - remove _utils.py and utils.py in transactron (#519) --- coreblocks/core.py | 2 +- coreblocks/frontend/fetch.py | 2 +- coreblocks/fu/alu.py | 2 +- coreblocks/fu/div_unit.py | 1 - coreblocks/params/layouts.py | 3 +- coreblocks/peripherals/wishbone.py | 5 +- scripts/gen_verilog.py | 2 +- scripts/synthesize.py | 2 +- test/common/__init__.py | 2 +- test/common/testbenchio.py | 2 +- test/fu/test_alu.py | 2 +- test/fu/test_div_unit.py | 2 +- test/fu/test_jb_unit.py | 2 +- test/fu/test_mul_unit.py | 2 +- test/gtkw_extension.py | 2 +- test/lsu/test_dummylsu.py | 2 +- test/regression/memory.py | 2 +- test/structs_common/test_exception.py | 2 +- test/transactions/test_assign.py | 3 +- test/transactions/test_simultaneous.py | 2 +- test/transactions/test_transactions.py | 2 +- test/utils/test_fifo.py | 3 +- transactron/_utils.py | 227 --------- transactron/core.py | 6 +- transactron/lib/__init__.py | 1 + transactron/{utils => lib}/fifo.py | 5 +- transactron/lib/reqres.py | 2 +- transactron/utils/__init__.py | 5 +- transactron/utils/_typing.py | 30 ++ transactron/utils/amaranth_ext/__init__.py | 2 + .../utils/amaranth_ext/elaboratables.py | 185 +++++++ transactron/utils/amaranth_ext/functions.py | 99 ++++ transactron/utils/assign.py | 177 +++++++ transactron/utils/data_repr.py | 142 ++++++ transactron/utils/transactron_helpers.py | 106 ++++ transactron/utils/utils.py | 468 ------------------ 36 files changed, 774 insertions(+), 730 deletions(-) delete mode 100644 transactron/_utils.py rename transactron/{utils => lib}/fifo.py (97%) create mode 100644 transactron/utils/amaranth_ext/__init__.py create mode 100644 transactron/utils/amaranth_ext/elaboratables.py create mode 100644 transactron/utils/amaranth_ext/functions.py create mode 100644 transactron/utils/assign.py create mode 100644 transactron/utils/data_repr.py create mode 100644 transactron/utils/transactron_helpers.py delete mode 100644 transactron/utils/utils.py diff --git a/coreblocks/core.py b/coreblocks/core.py index 1c636edd0..a424f0a4e 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -22,7 +22,7 @@ from coreblocks.peripherals.wishbone import WishboneMaster, WishboneBus from coreblocks.frontend.fetch import Fetch, UnalignedFetch from transactron.lib.transformers import MethodMap, MethodProduct -from transactron.utils.fifo import BasicFifo +from transactron.lib import BasicFifo __all__ = ["Core"] diff --git a/coreblocks/frontend/fetch.py b/coreblocks/frontend/fetch.py index b2ab6af95..ecec48f3f 100644 --- a/coreblocks/frontend/fetch.py +++ b/coreblocks/frontend/fetch.py @@ -1,5 +1,5 @@ from amaranth import * -from transactron.utils.fifo import BasicFifo, Semaphore +from transactron.lib import BasicFifo, Semaphore from coreblocks.frontend.icache import ICacheInterface from coreblocks.frontend.rvc import InstrDecompress, is_instr_compressed from transactron import def_method, Method, Transaction, TModule diff --git a/coreblocks/fu/alu.py b/coreblocks/fu/alu.py index f9a2bad21..7714ab35e 100644 --- a/coreblocks/fu/alu.py +++ b/coreblocks/fu/alu.py @@ -12,7 +12,7 @@ from coreblocks.utils.protocols import FuncUnit -from transactron.utils.utils import popcount, count_leading_zeros +from transactron.utils import popcount, count_leading_zeros __all__ = ["AluFuncUnit", "ALUComponent"] diff --git a/coreblocks/fu/div_unit.py b/coreblocks/fu/div_unit.py index a39390af8..a4767a0b0 100644 --- a/coreblocks/fu/div_unit.py +++ b/coreblocks/fu/div_unit.py @@ -13,7 +13,6 @@ from coreblocks.fu.fu_decoder import DecoderManager from transactron.utils import OneHotSwitch -from transactron.utils.fifo import BasicFifo from coreblocks.utils.protocols import FuncUnit from coreblocks.fu.division.long_division import LongDivider diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index 3238e9ba7..b2e606be7 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -1,7 +1,6 @@ from coreblocks.params import GenParams, OpType, Funct7, Funct3 from coreblocks.params.isa import ExceptionCause -from transactron.utils.utils import layout_subset -from transactron.utils import LayoutList, LayoutListField +from transactron.utils import LayoutList, LayoutListField, layout_subset __all__ = [ "CommonLayoutFields", diff --git a/coreblocks/peripherals/wishbone.py b/coreblocks/peripherals/wishbone.py index ae88e74ec..e6a9bc2ff 100644 --- a/coreblocks/peripherals/wishbone.py +++ b/coreblocks/peripherals/wishbone.py @@ -7,9 +7,8 @@ from transactron import Method, def_method, TModule from transactron.core import Transaction -from transactron.lib import AdapterTrans -from transactron.utils.utils import OneHotSwitchDynamic, assign -from transactron.utils.fifo import BasicFifo +from transactron.lib import AdapterTrans, BasicFifo +from transactron.utils import OneHotSwitchDynamic, assign from transactron.lib.connectors import Forwarder diff --git a/scripts/gen_verilog.py b/scripts/gen_verilog.py index 42bf94e4e..964b52654 100755 --- a/scripts/gen_verilog.py +++ b/scripts/gen_verilog.py @@ -16,7 +16,7 @@ from coreblocks.peripherals.wishbone import WishboneBus from coreblocks.core import Core from transactron import TransactionModule -from transactron.utils.utils import flatten_signals +from transactron.utils import flatten_signals from coreblocks.params.configurations import * diff --git a/scripts/synthesize.py b/scripts/synthesize.py index 572e4b77a..5e14d019f 100755 --- a/scripts/synthesize.py +++ b/scripts/synthesize.py @@ -13,7 +13,7 @@ sys.path.insert(0, parent) -from transactron.utils.utils import ModuleConnector +from transactron.utils import ModuleConnector from coreblocks.params.genparams import GenParams from coreblocks.params.fu_params import FunctionalComponentParams from coreblocks.core import Core diff --git a/test/common/__init__.py b/test/common/__init__.py index 20eb4e095..4b44c291f 100644 --- a/test/common/__init__.py +++ b/test/common/__init__.py @@ -2,4 +2,4 @@ from .infrastructure import * # noqa: F401 from .sugar import * # noqa: F401 from .testbenchio import * # noqa: F401 -from transactron._utils import data_layout # noqa: F401 +from transactron.utils import data_layout # noqa: F401 diff --git a/test/common/testbenchio.py b/test/common/testbenchio.py index 0ff3e9d74..8f9cc253d 100644 --- a/test/common/testbenchio.py +++ b/test/common/testbenchio.py @@ -3,7 +3,7 @@ from typing import Optional, Callable from transactron.lib import AdapterBase from transactron.core import ValueLike, SignalBundle -from transactron._utils import mock_def_helper +from transactron.utils import mock_def_helper from transactron.utils._typing import RecordIntDictRet, RecordValueDict, RecordIntDict from .functions import set_inputs, get_outputs, TestGen diff --git a/test/fu/test_alu.py b/test/fu/test_alu.py index 0aad519a0..f350af49c 100644 --- a/test/fu/test_alu.py +++ b/test/fu/test_alu.py @@ -3,7 +3,7 @@ from test.fu.functional_common import ExecFn, FunctionalUnitTestCase -from transactron._utils import signed_to_int +from transactron.utils import signed_to_int class AluUnitTest(FunctionalUnitTestCase[AluFn.Fn]): diff --git a/test/fu/test_div_unit.py b/test/fu/test_div_unit.py index baef39d17..c8c3e9b4c 100644 --- a/test/fu/test_div_unit.py +++ b/test/fu/test_div_unit.py @@ -5,7 +5,7 @@ from test.fu.functional_common import ExecFn, FunctionalUnitTestCase -from transactron._utils import signed_to_int, int_to_signed +from transactron.utils import signed_to_int, int_to_signed @parameterized_class( diff --git a/test/fu/test_jb_unit.py b/test/fu/test_jb_unit.py index b1e728cf6..974f2699c 100644 --- a/test/fu/test_jb_unit.py +++ b/test/fu/test_jb_unit.py @@ -7,7 +7,7 @@ from coreblocks.params.layouts import FuncUnitLayouts, FetchLayouts from coreblocks.utils.protocols import FuncUnit -from transactron._utils import signed_to_int +from transactron.utils import signed_to_int from test.fu.functional_common import ExecFn, FunctionalUnitTestCase diff --git a/test/fu/test_mul_unit.py b/test/fu/test_mul_unit.py index e97c89912..34f87e7a8 100644 --- a/test/fu/test_mul_unit.py +++ b/test/fu/test_mul_unit.py @@ -3,7 +3,7 @@ from coreblocks.params import * from coreblocks.fu.mul_unit import MulFn, MulComponent, MulType -from transactron._utils import signed_to_int, int_to_signed +from transactron.utils import signed_to_int, int_to_signed from test.fu.functional_common import ExecFn, FunctionalUnitTestCase diff --git a/test/gtkw_extension.py b/test/gtkw_extension.py index d548ad6e7..1229bad2c 100644 --- a/test/gtkw_extension.py +++ b/test/gtkw_extension.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from amaranth.sim.pysim import _VCDWriter from amaranth import * -from transactron.utils.utils import flatten_signals +from transactron.utils import flatten_signals class _VCDWriterExt(_VCDWriter): diff --git a/test/lsu/test_dummylsu.py b/test/lsu/test_dummylsu.py index b93365b8f..93a00dd3d 100644 --- a/test/lsu/test_dummylsu.py +++ b/test/lsu/test_dummylsu.py @@ -5,7 +5,7 @@ from amaranth.sim import Settle, Passive from transactron.lib import Adapter -from transactron._utils import int_to_signed, signed_to_int +from transactron.utils import int_to_signed, signed_to_int from coreblocks.params import OpType, GenParams from coreblocks.lsu.dummyLsu import LSUDummy from coreblocks.params.configurations import test_core_config diff --git a/test/regression/memory.py b/test/regression/memory.py index 38abacd63..d09ea2c10 100644 --- a/test/regression/memory.py +++ b/test/regression/memory.py @@ -6,7 +6,7 @@ from elftools.elf.constants import P_FLAGS from elftools.elf.elffile import ELFFile, Segment from coreblocks.params.configurations import CoreConfiguration -from transactron.utils.utils import align_to_power_of_two, align_down_to_power_of_two +from transactron.utils import align_to_power_of_two, align_down_to_power_of_two all = [ "ReplyStatus", diff --git a/test/structs_common/test_exception.py b/test/structs_common/test_exception.py index 2f88a0190..afc746ef8 100644 --- a/test/structs_common/test_exception.py +++ b/test/structs_common/test_exception.py @@ -6,7 +6,7 @@ from coreblocks.params.isa import ExceptionCause from coreblocks.params.configurations import test_core_config from transactron.lib import Adapter -from transactron.utils.utils import ModuleConnector +from transactron.utils import ModuleConnector from ..common import * diff --git a/test/transactions/test_assign.py b/test/transactions/test_assign.py index 7f472b57f..0659a01e4 100644 --- a/test/transactions/test_assign.py +++ b/test/transactions/test_assign.py @@ -4,7 +4,8 @@ from amaranth.hdl.ast import ArrayProxy, Slice from transactron.utils._typing import LayoutLike -from transactron.utils.utils import AssignArg, AssignType, AssignFields, assign +from transactron.utils import AssignType, assign +from transactron.utils.assign import AssignArg, AssignFields from unittest import TestCase from parameterized import parameterized_class, parameterized diff --git a/test/transactions/test_simultaneous.py b/test/transactions/test_simultaneous.py index 0f33d2021..173fbd61c 100644 --- a/test/transactions/test_simultaneous.py +++ b/test/transactions/test_simultaneous.py @@ -3,7 +3,7 @@ from amaranth import * from amaranth.sim import * -from transactron.utils.utils import ModuleConnector +from transactron.utils import ModuleConnector from ..common import SimpleTestCircuit, TestCaseWithSimulator, TestbenchIO, def_method_mock diff --git a/test/transactions/test_transactions.py b/test/transactions/test_transactions.py index ab901cc8a..f45de1f46 100644 --- a/test/transactions/test_transactions.py +++ b/test/transactions/test_transactions.py @@ -13,7 +13,7 @@ from transactron import * from transactron.lib import Adapter, AdapterTrans -from transactron._utils import Scheduler +from transactron.utils import Scheduler from transactron.core import ( Priority, diff --git a/test/utils/test_fifo.py b/test/utils/test_fifo.py index b74667fc3..934a58475 100644 --- a/test/utils/test_fifo.py +++ b/test/utils/test_fifo.py @@ -1,8 +1,7 @@ from amaranth import * from amaranth.sim import Settle -from transactron.utils.fifo import BasicFifo -from transactron.lib import AdapterTrans +from transactron.lib import AdapterTrans, BasicFifo from test.common import TestCaseWithSimulator, TestbenchIO, data_layout from collections import deque diff --git a/transactron/_utils.py b/transactron/_utils.py deleted file mode 100644 index 1ad803e1f..000000000 --- a/transactron/_utils.py +++ /dev/null @@ -1,227 +0,0 @@ -import itertools -import sys -from inspect import Parameter, signature -from typing import Any, Concatenate, Optional, TypeAlias, TypeGuard, TypeVar -from collections.abc import Callable, Iterable, Mapping -from amaranth import * -from transactron.utils._typing import LayoutLike, ShapeLike -from transactron.utils import OneHotSwitchDynamic - -__all__ = [ - "Scheduler", - "_graph_ccs", - "MethodLayout", - "ROGraph", - "Graph", - "GraphCC", - "get_caller_class_name", - "def_helper", - "method_def_helper", -] - - -T = TypeVar("T") -U = TypeVar("U") - - -class Scheduler(Elaboratable): - """Scheduler - - An implementation of a round-robin scheduler, which is used in the - transaction subsystem. It is based on Amaranth's round-robin scheduler - but instead of using binary numbers, it uses one-hot encoding for the - `grant` output signal. - - Attributes - ---------- - requests: Signal(count), in - Signals that something (e.g. a transaction) wants to run. When i-th - bit is high, then the i-th agent requests the grant signal. - grant: Signal(count), out - Signals that something (e.g. transaction) is granted to run. It uses - one-hot encoding. - valid : Signal(1), out - Signal that `grant` signals are valid. - """ - - def __init__(self, count: int): - """ - Parameters - ---------- - count : int - Number of agents between which the scheduler should arbitrate. - """ - if not isinstance(count, int) or count < 0: - raise ValueError("Count must be a non-negative integer, not {!r}".format(count)) - self.count = count - - self.requests = Signal(count) - self.grant = Signal(count, reset=1) - self.valid = Signal() - - def elaborate(self, platform): - m = Module() - - grant_reg = Signal.like(self.grant) - - for i in OneHotSwitchDynamic(m, grant_reg, default=True): - if i is not None: - m.d.comb += self.grant.eq(grant_reg) - for j in itertools.chain(reversed(range(i)), reversed(range(i + 1, self.count))): - with m.If(self.requests[j]): - m.d.comb += self.grant.eq(1 << j) - else: - m.d.comb += self.grant.eq(0) - - m.d.comb += self.valid.eq(self.requests.any()) - - m.d.sync += grant_reg.eq(self.grant) - - return m - - -ROGraph: TypeAlias = Mapping[T, Iterable[T]] -Graph: TypeAlias = dict[T, set[T]] -GraphCC: TypeAlias = set[T] - - -def _graph_ccs(gr: ROGraph[T]) -> list[GraphCC[T]]: - """_graph_ccs - - Find connected components in a graph. - - Parameters - ---------- - gr : Mapping[T, Iterable[T]] - Graph in which we should find connected components. Encoded using - adjacency lists. - - Returns - ------- - ccs : List[Set[T]] - Connected components of the graph `gr`. - """ - ccs = [] - cc = set() - visited = set() - - for v in gr.keys(): - q = [v] - while q: - w = q.pop() - if w in visited: - continue - visited.add(w) - cc.add(w) - q.extend(gr[w]) - if cc: - ccs.append(cc) - cc = set() - - return ccs - - -MethodLayout: TypeAlias = LayoutLike - - -def has_first_param(func: Callable[..., T], name: str, tp: type[U]) -> TypeGuard[Callable[Concatenate[U, ...], T]]: - parameters = signature(func).parameters - return ( - len(parameters) >= 1 - and next(iter(parameters)) == name - and parameters[name].kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY} - and parameters[name].annotation in {Parameter.empty, tp} - ) - - -def def_helper(description, func: Callable[..., T], tp: type[U], arg: U, /, **kwargs) -> T: - parameters = signature(func).parameters - kw_parameters = set( - n for n, p in parameters.items() if p.kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY} - ) - if len(parameters) == 1 and has_first_param(func, "arg", tp): - return func(arg) - elif kw_parameters <= kwargs.keys(): - return func(**kwargs) - else: - raise TypeError(f"Invalid {description}: {func}") - - -def mock_def_helper(tb, func: Callable[..., T], arg: Mapping[str, Any]) -> T: - return def_helper(f"mock definition for {tb}", func, Mapping[str, Any], arg, **arg) - - -def method_def_helper(method, func: Callable[..., T], arg: Record) -> T: - return def_helper(f"method definition for {method}", func, Record, arg, **arg.fields) - - -def get_caller_class_name(default: Optional[str] = None) -> tuple[Optional[Elaboratable], str]: - caller_frame = sys._getframe(2) - if "self" in caller_frame.f_locals: - owner = caller_frame.f_locals["self"] - return owner, owner.__class__.__name__ - elif default is not None: - return None, default - else: - raise RuntimeError("Not called from a method") - - -def data_layout(val: ShapeLike) -> LayoutLike: - return [("data", val)] - - -def neg(x: int, xlen: int) -> int: - """ - Computes the negation of a number in the U2 system. - - Parameters - ---------- - x: int - Number in U2 system. - xlen : int - Bit width of x. - - Returns - ------- - return : int - Negation of x in the U2 system. - """ - return (-x) & (2**xlen - 1) - - -def int_to_signed(x: int, xlen: int) -> int: - """ - Converts a Python integer into its U2 representation. - - Parameters - ---------- - x: int - Signed Python integer. - xlen : int - Bit width of x. - - Returns - ------- - return : int - Representation of x in the U2 system. - """ - return x & (2**xlen - 1) - - -def signed_to_int(x: int, xlen: int) -> int: - """ - Changes U2 representation into Python integer - - Parameters - ---------- - x: int - Number in U2 system. - xlen : int - Bit width of x. - - Returns - ------- - return : int - Representation of x as signed Python integer. - """ - return x | -(x & (2 ** (xlen - 1))) diff --git a/transactron/core.py b/transactron/core.py index 3b799c107..6410ab773 100644 --- a/transactron/core.py +++ b/transactron/core.py @@ -22,11 +22,9 @@ from itertools import count, chain, filterfalse, product from amaranth.hdl.dsl import FSM, _ModuleBuilderDomain -from transactron.utils import AssignType, assign, ModuleConnector, silence_mustuse -from transactron.utils.utils import OneHotSwitchDynamic, average_dict_of_lists -from ._utils import * -from transactron.utils._typing import ValueLike, SignalBundle, HasElaborate, SwitchKey, ModuleLike from .graph import Owned, OwnershipGraph, Direction +from transactron.utils import * +from transactron.utils.transactron_helpers import _graph_ccs __all__ = [ "MethodLayout", diff --git a/transactron/lib/__init__.py b/transactron/lib/__init__.py index 1caa56d21..c814b5e93 100644 --- a/transactron/lib/__init__.py +++ b/transactron/lib/__init__.py @@ -1,3 +1,4 @@ +from .fifo import * # noqa: F401 from .connectors import * # noqa: F401 from .buttons import * # noqa: F401 from .adapters import * # noqa: F401 diff --git a/transactron/utils/fifo.py b/transactron/lib/fifo.py similarity index 97% rename from transactron/utils/fifo.py rename to transactron/lib/fifo.py index 1ee3ffa90..a39d006a9 100644 --- a/transactron/utils/fifo.py +++ b/transactron/lib/fifo.py @@ -1,8 +1,7 @@ from amaranth import * from transactron import Method, def_method, Priority, TModule -from transactron._utils import MethodLayout -from transactron.utils._typing import ValueLike -from transactron.utils.utils import mod_incr +from transactron.utils._typing import ValueLike, MethodLayout +from transactron.utils.amaranth_ext import mod_incr class BasicFifo(Elaboratable): diff --git a/transactron/lib/reqres.py b/transactron/lib/reqres.py index 3587edc3e..5f2e20ba6 100644 --- a/transactron/lib/reqres.py +++ b/transactron/lib/reqres.py @@ -1,7 +1,7 @@ from amaranth import * from ..core import * from .connectors import Forwarder, FIFO -from transactron.utils.fifo import BasicFifo +from transactron.lib import BasicFifo from amaranth.utils import * __all__ = [ diff --git a/transactron/utils/__init__.py b/transactron/utils/__init__.py index a1cd9d2e0..8b437c355 100644 --- a/transactron/utils/__init__.py +++ b/transactron/utils/__init__.py @@ -1,3 +1,6 @@ -from .utils import * # noqa: F401 +from .data_repr import * # noqa: F401 from ._typing import * # noqa: F401 from .debug_signals import * # noqa: F401 +from .assign import * # noqa: F401 +from .amaranth_ext import * # noqa: F401 +from .transactron_helpers import * # noqa: F401 diff --git a/transactron/utils/_typing.py b/transactron/utils/_typing.py index f82aba30c..e4a532697 100644 --- a/transactron/utils/_typing.py +++ b/transactron/utils/_typing.py @@ -18,6 +18,28 @@ from amaranth.hdl.dsl import _ModuleBuilderSubmodules, _ModuleBuilderDomainSet, _ModuleBuilderDomain, FSM from amaranth.hdl.rec import Direction, Layout +__all__ = [ + "FragmentLike", + "ValueLike", + "StatementLike", + "LayoutLike", + "SwitchKey", + "MethodLayout", + "SignalBundle", + "LayoutListField", + "LayoutList", + "RecordIntDict", + "RecordIntDictRet", + "RecordValueDict", + "ROGraph", + "Graph", + "GraphCC", + "_ModuleBuilderDomainsLike", + "ModuleLike", + "HasElaborate", + "HasDebugSignals", +] + # Types representing Amaranth concepts FragmentLike: TypeAlias = Fragment | Elaboratable ValueLike: TypeAlias = Value | int | Enum | ValueCastable @@ -27,6 +49,7 @@ Layout | Sequence[tuple[str, "ShapeLike | LayoutLike"] | tuple[str, "ShapeLike | LayoutLike", Direction]] ) SwitchKey: TypeAlias = str | int | Enum +MethodLayout: TypeAlias = LayoutLike # Internal Coreblocks types SignalBundle: TypeAlias = Signal | Record | View | Iterable["SignalBundle"] | Mapping[str, "SignalBundle"] @@ -37,6 +60,13 @@ RecordIntDictRet: TypeAlias = Mapping[str, Any] # full typing hard to work with RecordValueDict: TypeAlias = Mapping[str, Union[ValueLike, "RecordValueDict"]] +T = TypeVar("T") +U = TypeVar("U") + +ROGraph: TypeAlias = Mapping[T, Iterable[T]] +Graph: TypeAlias = dict[T, set[T]] +GraphCC: TypeAlias = set[T] + class _ModuleBuilderDomainsLike(Protocol): def __getattr__(self, name: str) -> _ModuleBuilderDomain: diff --git a/transactron/utils/amaranth_ext/__init__.py b/transactron/utils/amaranth_ext/__init__.py new file mode 100644 index 000000000..2b8533b12 --- /dev/null +++ b/transactron/utils/amaranth_ext/__init__.py @@ -0,0 +1,2 @@ +from .functions import * # noqa: F401 +from .elaboratables import * # noqa: F401 diff --git a/transactron/utils/amaranth_ext/elaboratables.py b/transactron/utils/amaranth_ext/elaboratables.py new file mode 100644 index 000000000..2f6e1ede9 --- /dev/null +++ b/transactron/utils/amaranth_ext/elaboratables.py @@ -0,0 +1,185 @@ +import itertools +from contextlib import contextmanager +from typing import Literal, Optional, overload +from collections.abc import Iterable +from amaranth import * +from transactron.utils._typing import HasElaborate, ModuleLike + +__all__ = [ + "OneHotSwitchDynamic", + "OneHotSwitch", + "ModuleConnector", + "Scheduler", +] + + +@contextmanager +def OneHotSwitch(m: ModuleLike, test: Value): + """One-hot switch. + + This function allows one-hot matching in the style similar to the standard + Amaranth `Switch`. This allows to get the performance benefit of using + the one-hot representation. + + Example:: + + with OneHotSwitch(m, sig) as OneHotCase: + with OneHotCase(0b01): + ... + with OneHotCase(0b10): + ... + # optional default case + with OneHotCase(): + ... + + Parameters + ---------- + m : Module + The module for which the matching is defined. + test : Signal + The signal being tested. + """ + + @contextmanager + def case(n: Optional[int] = None): + if n is None: + with m.Case(): + yield + else: + # find the index of the least significant bit set + i = (n & -n).bit_length() - 1 + if n - (1 << i) != 0: + raise ValueError("%d not in one-hot representation" % n) + with m.Case(n): + yield + + with m.Switch(test): + yield case + + +@overload +def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: Literal[True]) -> Iterable[Optional[int]]: + ... + + +@overload +def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: Literal[False] = False) -> Iterable[int]: + ... + + +def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: bool = False) -> Iterable[Optional[int]]: + """Dynamic one-hot switch. + + This function allows simple one-hot matching on signals which can have + variable bit widths. + + Example:: + + for i in OneHotSwitchDynamic(m, sig): + # code dependent on the bit index i + ... + + Parameters + ---------- + m : Module + The module for which the matching is defined. + test : Signal + The signal being tested. + default : bool, optional + Whether the matching includes a default case (signified by a None). + """ + count = len(test) + with OneHotSwitch(m, test) as OneHotCase: + for i in range(count): + with OneHotCase(1 << i): + yield i + if default: + with OneHotCase(): + yield None + return + + +class ModuleConnector(Elaboratable): + """ + An Elaboratable to create a new module, which will have all arguments + added as its submodules. + """ + + def __init__(self, *args: HasElaborate, **kwargs: HasElaborate): + """ + Parameters + ---------- + *args + Modules which should be added as anonymous submodules. + **kwargs + Modules which will be added as named submodules. + """ + self.args = args + self.kwargs = kwargs + + def elaborate(self, platform): + m = Module() + + for elem in self.args: + m.submodules += elem + + for name, elem in self.kwargs.items(): + m.submodules[name] = elem + + return m + + +class Scheduler(Elaboratable): + """Scheduler + + An implementation of a round-robin scheduler, which is used in the + transaction subsystem. It is based on Amaranth's round-robin scheduler + but instead of using binary numbers, it uses one-hot encoding for the + `grant` output signal. + + Attributes + ---------- + requests: Signal(count), in + Signals that something (e.g. a transaction) wants to run. When i-th + bit is high, then the i-th agent requests the grant signal. + grant: Signal(count), out + Signals that something (e.g. transaction) is granted to run. It uses + one-hot encoding. + valid : Signal(1), out + Signal that `grant` signals are valid. + """ + + def __init__(self, count: int): + """ + Parameters + ---------- + count : int + Number of agents between which the scheduler should arbitrate. + """ + if not isinstance(count, int) or count < 0: + raise ValueError("Count must be a non-negative integer, not {!r}".format(count)) + self.count = count + + self.requests = Signal(count) + self.grant = Signal(count, reset=1) + self.valid = Signal() + + def elaborate(self, platform): + m = Module() + + grant_reg = Signal.like(self.grant) + + for i in OneHotSwitchDynamic(m, grant_reg, default=True): + if i is not None: + m.d.comb += self.grant.eq(grant_reg) + for j in itertools.chain(reversed(range(i)), reversed(range(i + 1, self.count))): + with m.If(self.requests[j]): + m.d.comb += self.grant.eq(1 << j) + else: + m.d.comb += self.grant.eq(0) + + m.d.comb += self.valid.eq(self.requests.any()) + + m.d.sync += grant_reg.eq(self.grant) + + return m diff --git a/transactron/utils/amaranth_ext/functions.py b/transactron/utils/amaranth_ext/functions.py new file mode 100644 index 000000000..fba8bb8ed --- /dev/null +++ b/transactron/utils/amaranth_ext/functions.py @@ -0,0 +1,99 @@ +from amaranth import * +from amaranth.utils import bits_for, log2_int +from amaranth.lib import data +from collections.abc import Iterable, Mapping +from transactron.utils._typing import SignalBundle + +__all__ = [ + "mod_incr", + "popcount", + "count_leading_zeros", + "count_trailing_zeros", + "flatten_signals", +] + + +def mod_incr(sig: Value, mod: int) -> Value: + """ + Perform `(sig+1) % mod` operation. + """ + if mod == 2 ** len(sig): + return sig + 1 + return Mux(sig == mod - 1, 0, sig + 1) + + +def popcount(s: Value): + sum_layers = [s[i] for i in range(len(s))] + + while len(sum_layers) > 1: + if len(sum_layers) % 2: + sum_layers.append(C(0)) + sum_layers = [a + b for a, b in zip(sum_layers[::2], sum_layers[1::2])] + + return sum_layers[0][0 : bits_for(len(s))] + + +def count_leading_zeros(s: Value) -> Value: + def iter(s: Value, step: int) -> Value: + # if no bits left - return empty value + if step == 0: + return C(0) + + # boudaries of upper and lower halfs of the value + partition = 2 ** (step - 1) + current_bit = 1 << (step - 1) + + # recursive call + upper_value = iter(s[partition:], step - 1) + lower_value = iter(s[:partition], step - 1) + + # if there are lit bits in upperhalf - take result directly from recursive value + # otherwise add 1 << (step - 1) to lower value and return + result = Mux(s[partition:].any(), upper_value, lower_value | current_bit) + + return result + + try: + xlen_log = log2_int(len(s)) + except ValueError: + raise NotImplementedError("CountLeadingZeros - only sizes aligned to power of 2 are supperted") + + value = iter(s, xlen_log) + + # 0 number edge case + # if s == 0 then iter() returns value off by 1 + # this switch negates this effect + high_bit = 1 << xlen_log + + result = Mux(s.any(), value, high_bit) + return result + + +def count_trailing_zeros(s: Value) -> Value: + try: + log2_int(len(s)) + except ValueError: + raise NotImplementedError("CountTrailingZeros - only sizes aligned to power of 2 are supperted") + + return count_leading_zeros(s[::-1]) + + +def flatten_signals(signals: SignalBundle) -> Iterable[Signal]: + """ + Flattens input data, which can be either a signal, a record, a list (or a dict) of SignalBundle items. + + """ + if isinstance(signals, Mapping): + for x in signals.values(): + yield from flatten_signals(x) + elif isinstance(signals, Iterable): + for x in signals: + yield from flatten_signals(x) + elif isinstance(signals, Record): + for x in signals.fields.values(): + yield from flatten_signals(x) + elif isinstance(signals, data.View): + for x, _ in signals.shape(): + yield from flatten_signals(signals[x]) + else: + yield signals diff --git a/transactron/utils/assign.py b/transactron/utils/assign.py new file mode 100644 index 000000000..b28ac738f --- /dev/null +++ b/transactron/utils/assign.py @@ -0,0 +1,177 @@ +from enum import Enum +from typing import Optional, TypeAlias, cast +from collections.abc import Iterable, Mapping +from amaranth import * +from amaranth.hdl.ast import Assign, ArrayProxy +from amaranth.lib import data +from ._typing import ValueLike + +__all__ = [ + "AssignType", + "assign", +] + + +class AssignType(Enum): + COMMON = 1 + RHS = 2 + ALL = 3 + + +AssignFields: TypeAlias = AssignType | Iterable[str] | Mapping[str, "AssignFields"] +AssignArg: TypeAlias = ValueLike | Mapping[str, "AssignArg"] + + +def arrayproxy_fields(proxy: ArrayProxy) -> Optional[set[str]]: + def flatten_elems(proxy: ArrayProxy): + for elem in proxy.elems: + if isinstance(elem, ArrayProxy): + yield from flatten_elems(elem) + else: + yield elem + + elems = list(flatten_elems(proxy)) + if elems and all(isinstance(el, Record) for el in elems): + return set.intersection(*[set(cast(Record, el).fields) for el in elems]) + + +def assign_arg_fields(val: AssignArg) -> Optional[set[str]]: + if isinstance(val, ArrayProxy): + return arrayproxy_fields(val) + elif isinstance(val, Record): + return set(val.fields) + elif isinstance(val, data.View): + layout = val.shape() + if isinstance(layout, data.StructLayout): + return set(k for k, _ in layout) + elif isinstance(val, dict): + return set(val.keys()) + + +def assign( + lhs: AssignArg, rhs: AssignArg, *, fields: AssignFields = AssignType.RHS, lhs_strict=False, rhs_strict=False +) -> Iterable[Assign]: + """Safe record assignment. + + This function recursively generates assignment statements for + field-containing structures. This includes: Amaranth `Record`\\s, + Amaranth `View`\\s using `StructLayout`, Python `dict`\\s. In case of + mismatching fields or bit widths, error is raised. + + When both `lhs` and `rhs` are field-containing, `assign` generates + assignment statements according to the value of the `field` parameter. + If either of `lhs` or `rhs` is not field-containing, `assign` checks for + the same bit width and generates a single assignment statement. + + The bit width check is performed if: + + - Any of `lhs` or `rhs` is a `Record` or `View`. + - Both `lhs` and `rhs` have an explicitly defined shape (e.g. are a + `Signal`, a field of a `Record` or a `View`). + + Parameters + ---------- + lhs : Record or View or Value-castable or dict + Record, signal or dict being assigned. + rhs : Record or View or Value-castable or dict + Record, signal or dict containing assigned values. + fields : AssignType or Iterable or Mapping, optional + Determines which fields will be assigned. Possible values: + + AssignType.COMMON + Only fields common to `lhs` and `rhs` are assigned. + AssignType.RHS + All fields in `rhs` are assigned. If one of them is not present + in `lhs`, an exception is raised. + AssignType.ALL + Assume that both records have the same layouts. All fields present + in `lhs` or `rhs` are assigned. + Mapping + Keys are field names, values follow the format for `fields`. + Iterable + Items are field names. For subrecords, AssignType.ALL is assumed. + + Returns + ------- + Iterable[Assign] + Generated assignment statements. + + Raises + ------ + ValueError + If the assignment can't be safely performed. + """ + lhs_fields = assign_arg_fields(lhs) + rhs_fields = assign_arg_fields(rhs) + + if lhs_fields is not None and rhs_fields is not None: + # asserts for type checking + assert ( + isinstance(lhs, Record) + or isinstance(lhs, ArrayProxy) + or isinstance(lhs, Mapping) + or isinstance(lhs, data.View) + ) + assert ( + isinstance(rhs, Record) + or isinstance(rhs, ArrayProxy) + or isinstance(rhs, Mapping) + or isinstance(rhs, data.View) + ) + + if fields is AssignType.COMMON: + names = lhs_fields & rhs_fields + elif fields is AssignType.RHS: + names = rhs_fields + elif fields is AssignType.ALL: + names = lhs_fields | rhs_fields + else: + names = set(fields) + + if not names and (lhs_fields or rhs_fields): + raise ValueError("There are no common fields in assigment lhs: {} rhs: {}".format(lhs_fields, rhs_fields)) + + for name in names: + if name not in lhs_fields: + raise KeyError("Field {} not present in lhs".format(name)) + if name not in rhs_fields: + raise KeyError("Field {} not present in rhs".format(name)) + + subfields = fields + if isinstance(fields, Mapping): + subfields = fields[name] + elif isinstance(fields, Iterable): + subfields = AssignType.ALL + + yield from assign( + lhs[name], + rhs[name], + fields=subfields, + lhs_strict=not isinstance(lhs, Mapping), + rhs_strict=not isinstance(rhs, Mapping), + ) + else: + if not isinstance(fields, AssignType): + raise ValueError("Fields on assigning non-records") + if not isinstance(lhs, ValueLike) or not isinstance(rhs, ValueLike): + raise TypeError("Unsupported assignment lhs: {} rhs: {}".format(lhs, rhs)) + + lhs_val = Value.cast(lhs) + rhs_val = Value.cast(rhs) + + def has_explicit_shape(val: ValueLike): + return isinstance(val, Signal) or isinstance(val, ArrayProxy) + + if ( + isinstance(lhs, Record) + or isinstance(rhs, Record) + or isinstance(lhs, data.View) + or isinstance(rhs, data.View) + or (lhs_strict or has_explicit_shape(lhs)) + and (rhs_strict or has_explicit_shape(rhs)) + ): + if lhs_val.shape() != rhs_val.shape(): + raise ValueError( + "Shapes not matching: lhs: {} {} rhs: {} {}".format(lhs_val.shape(), lhs, rhs_val.shape(), rhs) + ) + yield lhs_val.eq(rhs_val) diff --git a/transactron/utils/data_repr.py b/transactron/utils/data_repr.py new file mode 100644 index 000000000..d5d4029db --- /dev/null +++ b/transactron/utils/data_repr.py @@ -0,0 +1,142 @@ +from collections.abc import Iterable, Mapping +from ._typing import LayoutList, ShapeLike, LayoutLike +from typing import Any, Sized +from statistics import fmean + + +__all__ = [ + "make_hashable", + "align_to_power_of_two", + "align_down_to_power_of_two", + "bits_from_int", + "layout_subset", + "data_layout", + "signed_to_int", + "int_to_signed", + "neg", + "average_dict_of_lists", +] + + +def layout_subset(layout: LayoutList, *, fields: set[str]) -> LayoutList: + return [item for item in layout if item[0] in fields] + + +def make_hashable(val): + if isinstance(val, Mapping): + return frozenset(((k, make_hashable(v)) for k, v in val.items())) + elif isinstance(val, Iterable): + return (make_hashable(v) for v in val) + else: + return val + + +def align_to_power_of_two(num: int, power: int) -> int: + """Rounds up a number to the given power of two. + + Parameters + ---------- + num : int + The number to align. + power : int + The power of two to align to. + + Returns + ------- + int + The aligned number. + """ + mask = 2**power - 1 + if num & mask == 0: + return num + return (num & ~mask) + 2**power + + +def align_down_to_power_of_two(num: int, power: int) -> int: + """Rounds down a number to the given power of two. + + Parameters + ---------- + num : int + The number to align. + power : int + The power of two to align to. + + Returns + ------- + int + The aligned number. + """ + mask = 2**power - 1 + + return num & ~mask + + +def bits_from_int(num: int, lower: int, length: int): + """Returns [`lower`:`lower`+`length`) bits from integer `num`.""" + return (num >> lower) & ((1 << (length)) - 1) + + +def data_layout(val: ShapeLike) -> LayoutLike: + return [("data", val)] + + +def neg(x: int, xlen: int) -> int: + """ + Computes the negation of a number in the U2 system. + + Parameters + ---------- + x: int + Number in U2 system. + xlen : int + Bit width of x. + + Returns + ------- + return : int + Negation of x in the U2 system. + """ + return (-x) & (2**xlen - 1) + + +def int_to_signed(x: int, xlen: int) -> int: + """ + Converts a Python integer into its U2 representation. + + Parameters + ---------- + x: int + Signed Python integer. + xlen : int + Bit width of x. + + Returns + ------- + return : int + Representation of x in the U2 system. + """ + return x & (2**xlen - 1) + + +def signed_to_int(x: int, xlen: int) -> int: + """ + Changes U2 representation into Python integer + + Parameters + ---------- + x: int + Number in U2 system. + xlen : int + Bit width of x. + + Returns + ------- + return : int + Representation of x as signed Python integer. + """ + return x | -(x & (2 ** (xlen - 1))) + + +def average_dict_of_lists(d: Mapping[Any, Sized]) -> float: + return fmean(map(lambda xs: len(xs), d.values())) diff --git a/transactron/utils/transactron_helpers.py b/transactron/utils/transactron_helpers.py new file mode 100644 index 000000000..d7e610d6c --- /dev/null +++ b/transactron/utils/transactron_helpers.py @@ -0,0 +1,106 @@ +import sys +from contextlib import contextmanager +from typing import Optional, Any, Concatenate, TypeGuard, TypeVar +from collections.abc import Callable, Mapping +from ._typing import ROGraph, GraphCC +from inspect import Parameter, signature +from amaranth import * + + +__all__ = [ + "silence_mustuse", + "get_caller_class_name", + "def_helper", + "method_def_helper", + "mock_def_helper", +] + +T = TypeVar("T") +U = TypeVar("U") + + +def _graph_ccs(gr: ROGraph[T]) -> list[GraphCC[T]]: + """_graph_ccs + + Find connected components in a graph. + + Parameters + ---------- + gr : Mapping[T, Iterable[T]] + Graph in which we should find connected components. Encoded using + adjacency lists. + + Returns + ------- + ccs : List[Set[T]] + Connected components of the graph `gr`. + """ + ccs = [] + cc = set() + visited = set() + + for v in gr.keys(): + q = [v] + while q: + w = q.pop() + if w in visited: + continue + visited.add(w) + cc.add(w) + q.extend(gr[w]) + if cc: + ccs.append(cc) + cc = set() + + return ccs + + +def has_first_param(func: Callable[..., T], name: str, tp: type[U]) -> TypeGuard[Callable[Concatenate[U, ...], T]]: + parameters = signature(func).parameters + return ( + len(parameters) >= 1 + and next(iter(parameters)) == name + and parameters[name].kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY} + and parameters[name].annotation in {Parameter.empty, tp} + ) + + +def def_helper(description, func: Callable[..., T], tp: type[U], arg: U, /, **kwargs) -> T: + parameters = signature(func).parameters + kw_parameters = set( + n for n, p in parameters.items() if p.kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY} + ) + if len(parameters) == 1 and has_first_param(func, "arg", tp): + return func(arg) + elif kw_parameters <= kwargs.keys(): + return func(**kwargs) + else: + raise TypeError(f"Invalid {description}: {func}") + + +def mock_def_helper(tb, func: Callable[..., T], arg: Mapping[str, Any]) -> T: + return def_helper(f"mock definition for {tb}", func, Mapping[str, Any], arg, **arg) + + +def method_def_helper(method, func: Callable[..., T], arg: Record) -> T: + return def_helper(f"method definition for {method}", func, Record, arg, **arg.fields) + + +def get_caller_class_name(default: Optional[str] = None) -> tuple[Optional[Elaboratable], str]: + caller_frame = sys._getframe(2) + if "self" in caller_frame.f_locals: + owner = caller_frame.f_locals["self"] + return owner, owner.__class__.__name__ + elif default is not None: + return None, default + else: + raise RuntimeError("Not called from a method") + + +@contextmanager +def silence_mustuse(elaboratable: Elaboratable): + try: + yield + except Exception: + elaboratable._MustUse__silence = True # type: ignore + raise diff --git a/transactron/utils/utils.py b/transactron/utils/utils.py deleted file mode 100644 index 77ed3c864..000000000 --- a/transactron/utils/utils.py +++ /dev/null @@ -1,468 +0,0 @@ -from contextlib import contextmanager -from enum import Enum -from typing import Any, Literal, Optional, TypeAlias, cast, overload -from collections.abc import Iterable, Mapping, Sized -from statistics import fmean -from amaranth import * -from amaranth.hdl.ast import Assign, ArrayProxy -from amaranth.lib import data -from amaranth.utils import bits_for, log2_int -from ._typing import ValueLike, LayoutList, SignalBundle, HasElaborate, ModuleLike - -__all__ = [ - "AssignType", - "assign", - "OneHotSwitchDynamic", - "OneHotSwitch", - "make_hashable", - "flatten_signals", - "align_to_power_of_two", - "align_down_to_power_of_two", - "bits_from_int", - "ModuleConnector", - "silence_mustuse", - "popcount", - "count_leading_zeros", - "count_trailing_zeros", - "mod_incr", - "average_dict_of_lists", -] - - -def mod_incr(sig: Value, mod: int) -> Value: - """ - Perform `(sig+1) % mod` operation. - """ - if mod == 2 ** len(sig): - return sig + 1 - return Mux(sig == mod - 1, 0, sig + 1) - - -@contextmanager -def OneHotSwitch(m: ModuleLike, test: Value): - """One-hot switch. - - This function allows one-hot matching in the style similar to the standard - Amaranth `Switch`. This allows to get the performance benefit of using - the one-hot representation. - - Example:: - - with OneHotSwitch(m, sig) as OneHotCase: - with OneHotCase(0b01): - ... - with OneHotCase(0b10): - ... - # optional default case - with OneHotCase(): - ... - - Parameters - ---------- - m : Module - The module for which the matching is defined. - test : Signal - The signal being tested. - """ - - @contextmanager - def case(n: Optional[int] = None): - if n is None: - with m.Case(): - yield - else: - # find the index of the least significant bit set - i = (n & -n).bit_length() - 1 - if n - (1 << i) != 0: - raise ValueError("%d not in one-hot representation" % n) - with m.Case(n): - yield - - with m.Switch(test): - yield case - - -@overload -def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: Literal[True]) -> Iterable[Optional[int]]: - ... - - -@overload -def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: Literal[False] = False) -> Iterable[int]: - ... - - -def OneHotSwitchDynamic(m: ModuleLike, test: Value, *, default: bool = False) -> Iterable[Optional[int]]: - """Dynamic one-hot switch. - - This function allows simple one-hot matching on signals which can have - variable bit widths. - - Example:: - - for i in OneHotSwitchDynamic(m, sig): - # code dependent on the bit index i - ... - - Parameters - ---------- - m : Module - The module for which the matching is defined. - test : Signal - The signal being tested. - default : bool, optional - Whether the matching includes a default case (signified by a None). - """ - count = len(test) - with OneHotSwitch(m, test) as OneHotCase: - for i in range(count): - with OneHotCase(1 << i): - yield i - if default: - with OneHotCase(): - yield None - return - - -class AssignType(Enum): - COMMON = 1 - RHS = 2 - ALL = 3 - - -AssignFields: TypeAlias = AssignType | Iterable[str] | Mapping[str, "AssignFields"] -AssignArg: TypeAlias = ValueLike | Mapping[str, "AssignArg"] - - -def arrayproxy_fields(proxy: ArrayProxy) -> Optional[set[str]]: - def flatten_elems(proxy: ArrayProxy): - for elem in proxy.elems: - if isinstance(elem, ArrayProxy): - yield from flatten_elems(elem) - else: - yield elem - - elems = list(flatten_elems(proxy)) - if elems and all(isinstance(el, Record) for el in elems): - return set.intersection(*[set(cast(Record, el).fields) for el in elems]) - - -def assign_arg_fields(val: AssignArg) -> Optional[set[str]]: - if isinstance(val, ArrayProxy): - return arrayproxy_fields(val) - elif isinstance(val, Record): - return set(val.fields) - elif isinstance(val, data.View): - layout = val.shape() - if isinstance(layout, data.StructLayout): - return set(k for k, _ in layout) - elif isinstance(val, dict): - return set(val.keys()) - - -def assign( - lhs: AssignArg, rhs: AssignArg, *, fields: AssignFields = AssignType.RHS, lhs_strict=False, rhs_strict=False -) -> Iterable[Assign]: - """Safe record assignment. - - This function recursively generates assignment statements for - field-containing structures. This includes: Amaranth `Record`\\s, - Amaranth `View`\\s using `StructLayout`, Python `dict`\\s. In case of - mismatching fields or bit widths, error is raised. - - When both `lhs` and `rhs` are field-containing, `assign` generates - assignment statements according to the value of the `field` parameter. - If either of `lhs` or `rhs` is not field-containing, `assign` checks for - the same bit width and generates a single assignment statement. - - The bit width check is performed if: - - - Any of `lhs` or `rhs` is a `Record` or `View`. - - Both `lhs` and `rhs` have an explicitly defined shape (e.g. are a - `Signal`, a field of a `Record` or a `View`). - - Parameters - ---------- - lhs : Record or View or Value-castable or dict - Record, signal or dict being assigned. - rhs : Record or View or Value-castable or dict - Record, signal or dict containing assigned values. - fields : AssignType or Iterable or Mapping, optional - Determines which fields will be assigned. Possible values: - - AssignType.COMMON - Only fields common to `lhs` and `rhs` are assigned. - AssignType.RHS - All fields in `rhs` are assigned. If one of them is not present - in `lhs`, an exception is raised. - AssignType.ALL - Assume that both records have the same layouts. All fields present - in `lhs` or `rhs` are assigned. - Mapping - Keys are field names, values follow the format for `fields`. - Iterable - Items are field names. For subrecords, AssignType.ALL is assumed. - - Returns - ------- - Iterable[Assign] - Generated assignment statements. - - Raises - ------ - ValueError - If the assignment can't be safely performed. - """ - lhs_fields = assign_arg_fields(lhs) - rhs_fields = assign_arg_fields(rhs) - - if lhs_fields is not None and rhs_fields is not None: - # asserts for type checking - assert ( - isinstance(lhs, Record) - or isinstance(lhs, ArrayProxy) - or isinstance(lhs, Mapping) - or isinstance(lhs, data.View) - ) - assert ( - isinstance(rhs, Record) - or isinstance(rhs, ArrayProxy) - or isinstance(rhs, Mapping) - or isinstance(rhs, data.View) - ) - - if fields is AssignType.COMMON: - names = lhs_fields & rhs_fields - elif fields is AssignType.RHS: - names = rhs_fields - elif fields is AssignType.ALL: - names = lhs_fields | rhs_fields - else: - names = set(fields) - - if not names and (lhs_fields or rhs_fields): - raise ValueError("There are no common fields in assigment lhs: {} rhs: {}".format(lhs_fields, rhs_fields)) - - for name in names: - if name not in lhs_fields: - raise KeyError("Field {} not present in lhs".format(name)) - if name not in rhs_fields: - raise KeyError("Field {} not present in rhs".format(name)) - - subfields = fields - if isinstance(fields, Mapping): - subfields = fields[name] - elif isinstance(fields, Iterable): - subfields = AssignType.ALL - - yield from assign( - lhs[name], - rhs[name], - fields=subfields, - lhs_strict=not isinstance(lhs, Mapping), - rhs_strict=not isinstance(rhs, Mapping), - ) - else: - if not isinstance(fields, AssignType): - raise ValueError("Fields on assigning non-records") - if not isinstance(lhs, ValueLike) or not isinstance(rhs, ValueLike): - raise TypeError("Unsupported assignment lhs: {} rhs: {}".format(lhs, rhs)) - - lhs_val = Value.cast(lhs) - rhs_val = Value.cast(rhs) - - def has_explicit_shape(val: ValueLike): - return isinstance(val, Signal) or isinstance(val, ArrayProxy) - - if ( - isinstance(lhs, Record) - or isinstance(rhs, Record) - or isinstance(lhs, data.View) - or isinstance(rhs, data.View) - or (lhs_strict or has_explicit_shape(lhs)) - and (rhs_strict or has_explicit_shape(rhs)) - ): - if lhs_val.shape() != rhs_val.shape(): - raise ValueError( - "Shapes not matching: lhs: {} {} rhs: {} {}".format(lhs_val.shape(), lhs, rhs_val.shape(), rhs) - ) - yield lhs_val.eq(rhs_val) - - -def popcount(s: Value): - sum_layers = [s[i] for i in range(len(s))] - - while len(sum_layers) > 1: - if len(sum_layers) % 2: - sum_layers.append(C(0)) - sum_layers = [a + b for a, b in zip(sum_layers[::2], sum_layers[1::2])] - - return sum_layers[0][0 : bits_for(len(s))] - - -def count_leading_zeros(s: Value) -> Value: - def iter(s: Value, step: int) -> Value: - # if no bits left - return empty value - if step == 0: - return C(0) - - # boudaries of upper and lower halfs of the value - partition = 2 ** (step - 1) - current_bit = 1 << (step - 1) - - # recursive call - upper_value = iter(s[partition:], step - 1) - lower_value = iter(s[:partition], step - 1) - - # if there are lit bits in upperhalf - take result directly from recursive value - # otherwise add 1 << (step - 1) to lower value and return - result = Mux(s[partition:].any(), upper_value, lower_value | current_bit) - - return result - - try: - xlen_log = log2_int(len(s)) - except ValueError: - raise NotImplementedError("CountLeadingZeros - only sizes aligned to power of 2 are supperted") - - value = iter(s, xlen_log) - - # 0 number edge case - # if s == 0 then iter() returns value off by 1 - # this switch negates this effect - high_bit = 1 << xlen_log - - result = Mux(s.any(), value, high_bit) - return result - - -def count_trailing_zeros(s: Value) -> Value: - try: - log2_int(len(s)) - except ValueError: - raise NotImplementedError("CountTrailingZeros - only sizes aligned to power of 2 are supperted") - - return count_leading_zeros(s[::-1]) - - -def layout_subset(layout: LayoutList, *, fields: set[str]) -> LayoutList: - return [item for item in layout if item[0] in fields] - - -def make_hashable(val): - if isinstance(val, Mapping): - return frozenset(((k, make_hashable(v)) for k, v in val.items())) - elif isinstance(val, Iterable): - return (make_hashable(v) for v in val) - else: - return val - - -def flatten_signals(signals: SignalBundle) -> Iterable[Signal]: - """ - Flattens input data, which can be either a signal, a record, a list (or a dict) of SignalBundle items. - - """ - if isinstance(signals, Mapping): - for x in signals.values(): - yield from flatten_signals(x) - elif isinstance(signals, Iterable): - for x in signals: - yield from flatten_signals(x) - elif isinstance(signals, Record): - for x in signals.fields.values(): - yield from flatten_signals(x) - elif isinstance(signals, data.View): - for x, _ in signals.shape(): - yield from flatten_signals(signals[x]) - else: - yield signals - - -def align_to_power_of_two(num: int, power: int) -> int: - """Rounds up a number to the given power of two. - - Parameters - ---------- - num : int - The number to align. - power : int - The power of two to align to. - - Returns - ------- - int - The aligned number. - """ - mask = 2**power - 1 - if num & mask == 0: - return num - return (num & ~mask) + 2**power - - -def align_down_to_power_of_two(num: int, power: int) -> int: - """Rounds down a number to the given power of two. - - Parameters - ---------- - num : int - The number to align. - power : int - The power of two to align to. - - Returns - ------- - int - The aligned number. - """ - mask = 2**power - 1 - - return num & ~mask - - -def bits_from_int(num: int, lower: int, length: int): - """Returns [`lower`:`lower`+`length`) bits from integer `num`.""" - return (num >> lower) & ((1 << (length)) - 1) - - -def average_dict_of_lists(d: Mapping[Any, Sized]) -> float: - return fmean(map(lambda xs: len(xs), d.values())) - - -class ModuleConnector(Elaboratable): - """ - An Elaboratable to create a new module, which will have all arguments - added as its submodules. - """ - - def __init__(self, *args: HasElaborate, **kwargs: HasElaborate): - """ - Parameters - ---------- - *args - Modules which should be added as anonymous submodules. - **kwargs - Modules which will be added as named submodules. - """ - self.args = args - self.kwargs = kwargs - - def elaborate(self, platform): - m = Module() - - for elem in self.args: - m.submodules += elem - - for name, elem in self.kwargs.items(): - m.submodules[name] = elem - - return m - - -@contextmanager -def silence_mustuse(elaboratable: Elaboratable): - try: - yield - except Exception: - elaboratable._MustUse__silence = True # type: ignore - raise From c63beb4f66598b94f4823ce92e5b183a258720e4 Mon Sep 17 00:00:00 2001 From: piotro888 Date: Sat, 16 Dec 2023 19:50:13 +0100 Subject: [PATCH 22/25] Asynchronous interrupts (#532) --- coreblocks/core.py | 28 ++- coreblocks/frontend/fetch.py | 3 + coreblocks/fu/jumpbranch.py | 46 +++-- coreblocks/fu/priv.py | 112 ++++++++++++ coreblocks/params/configurations.py | 8 +- coreblocks/params/isa.py | 3 +- coreblocks/params/keys.py | 13 ++ coreblocks/params/layouts.py | 1 + coreblocks/params/optypes.py | 2 +- coreblocks/stages/retirement.py | 36 +++- coreblocks/structs_common/csr.py | 39 ++++- .../structs_common/interrupt_controller.py | 44 +++++ test/asm/interrupt.asm | 47 ++++++ test/frontend/test_decoder.py | 2 +- test/fu/functional_common.py | 5 +- test/params/test_configurations.py | 11 +- test/stages/test_retirement.py | 2 + test/structs_common/test_csr.py | 3 +- test/test_core.py | 159 ++++++++++++++---- 19 files changed, 490 insertions(+), 74 deletions(-) create mode 100644 coreblocks/fu/priv.py create mode 100644 coreblocks/structs_common/interrupt_controller.py create mode 100644 test/asm/interrupt.asm diff --git a/coreblocks/core.py b/coreblocks/core.py index a424f0a4e..12c100bc1 100644 --- a/coreblocks/core.py +++ b/coreblocks/core.py @@ -3,6 +3,7 @@ from coreblocks.params.dependencies import DependencyManager from coreblocks.stages.func_blocks_unifier import FuncBlocksUnifier from coreblocks.structs_common.instr_counter import CoreInstructionCounter +from coreblocks.structs_common.interrupt_controller import InterruptController from transactron.core import Transaction, TModule from transactron.lib import FIFO, ConnectTrans from coreblocks.params.layouts import * @@ -37,14 +38,21 @@ def __init__(self, *, gen_params: GenParams, wb_instr_bus: WishboneBus, wb_data_ self.wb_master_instr = WishboneMaster(self.gen_params.wb_params) self.wb_master_data = WishboneMaster(self.gen_params.wb_params) - # make fifo_fetch visible outside the core for injecting instructions + self.core_counter = CoreInstructionCounter(self.gen_params) + + # make fetch_continue visible outside the core for injecting instructions self.fifo_fetch = FIFO(self.gen_params.get(FetchLayouts).raw_instr, 2) + + drop_args_transform = (self.gen_params.get(FetchLayouts).raw_instr, lambda _a, _b: {}) + self.core_counter_increment_discard_map = MethodMap( + self.core_counter.increment, i_transform=drop_args_transform + ) + self.fetch_continue = MethodProduct([self.fifo_fetch.write, self.core_counter_increment_discard_map.method]) + self.free_rf_fifo = BasicFifo( self.gen_params.get(SchedulerLayouts).free_rf_layout, 2**self.gen_params.phys_regs_bits ) - self.core_counter = CoreInstructionCounter(self.gen_params) - cache_layouts = self.gen_params.get(ICacheLayouts) if gen_params.icache_params.enable: self.icache_refiller = SimpleWBCacheRefiller( @@ -78,6 +86,8 @@ def __init__(self, *, gen_params: GenParams, wb_instr_bus: WishboneBus, wb_data_ rf_write=self.RF.write, ) + self.interrupt_controller = InterruptController(self.gen_params) + self.csr_generic = GenericCSRRegisters(self.gen_params) connections.add_dependency(GenericCSRRegistersKey(), self.csr_generic) @@ -100,17 +110,14 @@ def elaborate(self, platform): m.submodules.icache_refiller = self.icache_refiller m.submodules.icache = self.icache - drop_args_transform = (self.gen_params.get(FetchLayouts).raw_instr, lambda _a, _b: {}) - core_counter_increment_discard_map = MethodMap(self.core_counter.increment, i_transform=drop_args_transform) - fetch_continue = MethodProduct([self.fifo_fetch.write, core_counter_increment_discard_map.use(m)]) - if Extension.C in self.gen_params.isa.extensions: - m.submodules.fetch = self.fetch = UnalignedFetch(self.gen_params, self.icache, fetch_continue.use(m)) + m.submodules.fetch = self.fetch = UnalignedFetch(self.gen_params, self.icache, self.fetch_continue.use(m)) else: - m.submodules.fetch = self.fetch = Fetch(self.gen_params, self.icache, fetch_continue.use(m)) + m.submodules.fetch = self.fetch = Fetch(self.gen_params, self.icache, self.fetch_continue.use(m)) m.submodules.fifo_fetch = self.fifo_fetch m.submodules.core_counter = self.core_counter + m.submodules.args_discard_map = self.core_counter_increment_discard_map m.submodules.fifo_decode = fifo_decode = FIFO(self.gen_params.get(DecodeLayouts).decoded_instr, 2) m.submodules.decode = Decode( @@ -150,8 +157,11 @@ def elaborate(self, platform): fetch_continue=self.fetch.verify_branch, fetch_stall=self.fetch.stall_exception, instr_decrement=self.core_counter.decrement, + trap_entry=self.interrupt_controller.entry, ) + m.submodules.interrupt_controller = self.interrupt_controller + m.submodules.csr_generic = self.csr_generic # push all registers to FreeRF at reset. r0 should be skipped, stop when counter overflows to 0 diff --git a/coreblocks/frontend/fetch.py b/coreblocks/frontend/fetch.py index ecec48f3f..d04a6c08a 100644 --- a/coreblocks/frontend/fetch.py +++ b/coreblocks/frontend/fetch.py @@ -1,4 +1,5 @@ from amaranth import * +from transactron.core import Priority from transactron.lib import BasicFifo, Semaphore from coreblocks.frontend.icache import ICacheInterface from coreblocks.frontend.rvc import InstrDecompress, is_instr_compressed @@ -31,6 +32,7 @@ def __init__(self, gen_params: GenParams, icache: ICacheInterface, cont: Method) self.verify_branch = Method(i=self.gp.get(FetchLayouts).branch_verify) self.stall_exception = Method() + self.stall_exception.add_conflict(self.verify_branch, Priority.LEFT) # PC of the last fetched instruction. For now only used in tests. self.pc = Signal(self.gp.isa.xlen) @@ -130,6 +132,7 @@ def __init__(self, gen_params: GenParams, icache: ICacheInterface, cont: Method) self.verify_branch = Method(i=self.gp.get(FetchLayouts).branch_verify) self.stall_exception = Method() + self.stall_exception.add_conflict(self.verify_branch, Priority.LEFT) # PC of the last fetched instruction. For now only used in tests. self.pc = Signal(self.gp.isa.xlen) diff --git a/coreblocks/fu/jumpbranch.py b/coreblocks/fu/jumpbranch.py index cb98c79f0..1a8ad0c1c 100644 --- a/coreblocks/fu/jumpbranch.py +++ b/coreblocks/fu/jumpbranch.py @@ -3,12 +3,15 @@ from enum import IntFlag, auto from typing import Sequence +from coreblocks.params.layouts import ExceptionRegisterLayouts from transactron import * from transactron.core import def_method from transactron.lib import * +from transactron.utils import assign from coreblocks.params import * +from coreblocks.params.keys import AsyncInterruptInsertSignalKey from transactron.utils import OneHotSwitch from coreblocks.utils.protocols import FuncUnit @@ -156,23 +159,46 @@ def _(arg): m.d.top_comb += jb.in_rvc.eq(arg.exec_fn.funct7) - # Spec: "[...] if the target address is not four-byte aligned. This exception is reported on the branch - # or jump instruction, not on the target instruction. No instruction-address-misaligned exception is - # generated for a conditional branch that is not taken." + is_auipc = decoder.decode_fn == JumpBranchFn.Fn.AUIPC + jump_result = Mux(jb.taken, jb.jmp_addr, jb.reg_res) + exception = Signal() + exception_report = self.dm.get_dependency(ExceptionReportKey()) + jmp_addr_misaligned = (jb.jmp_addr & (0b1 if Extension.C in self.gen.isa.extensions else 0b11)) != 0 - with m.If((decoder.decode_fn != JumpBranchFn.Fn.AUIPC) & jb.taken & jmp_addr_misaligned): + + async_interrupt_active = self.gen.get(DependencyManager).get_dependency(AsyncInterruptInsertSignalKey()) + + exception_entry = Record(self.gen.get(ExceptionRegisterLayouts).report) + + with m.If(~is_auipc & jb.taken & jmp_addr_misaligned): + # Spec: "[...] if the target address is not four-byte aligned. This exception is reported on the branch + # or jump instruction, not on the target instruction. No instruction-address-misaligned exception is + # generated for a conditional branch that is not taken." m.d.comb += exception.eq(1) - report = self.dm.get_dependency(ExceptionReportKey()) - report(m, rob_id=arg.rob_id, cause=ExceptionCause.INSTRUCTION_ADDRESS_MISALIGNED, pc=arg.pc) + m.d.comb += assign( + exception_entry, + {"rob_id": arg.rob_id, "cause": ExceptionCause.INSTRUCTION_ADDRESS_MISALIGNED, "pc": arg.pc}, + ) + with m.Elif(async_interrupt_active & ~is_auipc): + # Jump instructions are entry points for async interrupts. + # This way we can store known pc via report to global exception register and avoid it in ROB. + # Exceptions have priority, because the instruction that reports async interrupt is commited + # and exception would be lost. + m.d.comb += exception.eq(1) + m.d.comb += assign( + exception_entry, + {"rob_id": arg.rob_id, "cause": ExceptionCause._COREBLOCKS_ASYNC_INTERRUPT, "pc": jump_result}, + ) + + with m.If(exception): + exception_report(m, exception_entry) fifo_res.write(m, rob_id=arg.rob_id, result=jb.reg_res, rp_dst=arg.rp_dst, exception=exception) # skip writing next branch target for auipc - with m.If(decoder.decode_fn != JumpBranchFn.Fn.AUIPC): - fifo_branch.write( - m, from_pc=jb.in_pc, next_pc=Mux(jb.taken, jb.jmp_addr, jb.reg_res), resume_from_exception=0 - ) + with m.If(~is_auipc): + fifo_branch.write(m, from_pc=jb.in_pc, next_pc=jump_result, resume_from_exception=0) return m diff --git a/coreblocks/fu/priv.py b/coreblocks/fu/priv.py new file mode 100644 index 000000000..48f2bbe9f --- /dev/null +++ b/coreblocks/fu/priv.py @@ -0,0 +1,112 @@ +from amaranth import * + +from enum import IntFlag, auto +from typing import Sequence + + +from transactron import * +from transactron.lib import BasicFifo + +from coreblocks.params import * +from coreblocks.params.keys import MretKey +from coreblocks.utils.protocols import FuncUnit + +from coreblocks.fu.fu_decoder import DecoderManager + + +class PrivilegedFn(DecoderManager): + @unique + class Fn(IntFlag): + MRET = auto() + + @classmethod + def get_instructions(cls) -> Sequence[tuple]: + return [(cls.Fn.MRET, OpType.MRET)] + + +class PrivilegedFuncUnit(Elaboratable): + def __init__(self, gp: GenParams): + self.gp = gp + + self.layouts = layouts = gp.get(FuncUnitLayouts) + self.dm = gp.get(DependencyManager) + + self.issue = Method(i=layouts.issue) + self.accept = Method(o=layouts.accept) + self.precommit = Method(i=gp.get(RetirementLayouts).precommit) + + self.branch_resolved_fifo = BasicFifo(self.gp.get(FetchLayouts).branch_verify, 2) + + def elaborate(self, platform): + m = TModule() + + instr_valid = Signal() + finished = Signal() + + instr_rob = Signal(self.gp.rob_entries_bits) + instr_pc = Signal(self.gp.isa.xlen) + + mret = self.dm.get_dependency(MretKey()) + async_interrupt_active = self.dm.get_dependency(AsyncInterruptInsertSignalKey()) + exception_report = self.dm.get_dependency(ExceptionReportKey()) + csr = self.dm.get_dependency(GenericCSRRegistersKey()) + + m.submodules.branch_resolved_fifo = self.branch_resolved_fifo + + @def_method(m, self.issue, ready=~instr_valid) + def _(arg): + m.d.sync += [ + instr_valid.eq(1), + instr_rob.eq(arg.rob_id), + instr_pc.eq(arg.pc), + ] + + @def_method(m, self.precommit) + def _(rob_id, side_fx): + with m.If(instr_valid & (rob_id == instr_rob)): + m.d.sync += finished.eq(1) + with m.If(side_fx): + mret(m) + + @def_method(m, self.accept, ready=instr_valid & finished) + def _(): + m.d.sync += instr_valid.eq(0) + m.d.sync += finished.eq(0) + + ret_pc = csr.m_mode.mepc.read(m).data + + exception = Signal() + with m.If(async_interrupt_active): + # SPEC: "These conditions for an interrupt trap to occur [..] must also be evaluated immediately + # following the execution of an xRET instruction." + # mret() method is called from precommit() that was executed at least one cycle earlier (because + # of finished condition). If calling mret() caused interrupt to be active, it is already represented + # by updated async_interrupt_active singal. + # Interrupt is reported on this xRET instruction with return address set to instruction that we + # would normally return to (mepc value is preserved) + m.d.comb += exception.eq(1) + exception_report(m, cause=ExceptionCause._COREBLOCKS_ASYNC_INTERRUPT, pc=ret_pc, rob_id=instr_rob) + with m.Else(): + # Unstall the fetch to return address (MRET is SYSTEM opcode) + self.branch_resolved_fifo.write(m, next_pc=ret_pc, from_pc=0, resume_from_exception=0) + + return { + "rob_id": instr_rob, + "exception": exception, + "rp_dst": 0, + "result": 0, + } + + return m + + +class PrivilegedUnitComponent(FunctionalComponentParams): + def get_module(self, gp: GenParams) -> FuncUnit: + unit = PrivilegedFuncUnit(gp) + connections = gp.get(DependencyManager) + connections.add_dependency(InstructionPrecommitKey(), unit.precommit) + connections.add_dependency(BranchResolvedKey(), unit.branch_resolved_fifo.read) + return unit + + def get_optypes(self) -> set[OpType]: + return PrivilegedFn().get_op_types() diff --git a/coreblocks/params/configurations.py b/coreblocks/params/configurations.py index c87a9cda4..a289c09e5 100644 --- a/coreblocks/params/configurations.py +++ b/coreblocks/params/configurations.py @@ -16,14 +16,19 @@ from coreblocks.fu.zbc import ZbcComponent from coreblocks.fu.zbs import ZbsComponent from coreblocks.fu.exception import ExceptionUnitComponent +from coreblocks.fu.priv import PrivilegedUnitComponent from coreblocks.lsu.dummyLsu import LSUBlockComponent from coreblocks.structs_common.csr import CSRBlockComponent __all__ = ["CoreConfiguration", "basic_core_config", "tiny_core_config", "full_core_config", "test_core_config"] basic_configuration: tuple[BlockComponentParams, ...] = ( - RSBlockComponent([ALUComponent(), ShiftUnitComponent(), JumpComponent(), ExceptionUnitComponent()], rs_entries=4), + RSBlockComponent( + [ALUComponent(), ShiftUnitComponent(), JumpComponent(), ExceptionUnitComponent(), PrivilegedUnitComponent()], + rs_entries=4, + ), LSUBlockComponent(), + CSRBlockComponent(), ) @@ -116,6 +121,7 @@ def replace(self, **kwargs): ZbsComponent(), JumpComponent(), ExceptionUnitComponent(), + PrivilegedUnitComponent(), ], rs_entries=4, ), diff --git a/coreblocks/params/isa.py b/coreblocks/params/isa.py index 0bc8b0fcf..8ca7466e4 100644 --- a/coreblocks/params/isa.py +++ b/coreblocks/params/isa.py @@ -140,7 +140,7 @@ class FenceFm(IntEnum, shape=4): @unique -class ExceptionCause(IntEnum, shape=4): +class ExceptionCause(IntEnum, shape=5): INSTRUCTION_ADDRESS_MISALIGNED = 0 INSTRUCTION_ACCESS_FAULT = 1 ILLEGAL_INSTRUCTION = 2 @@ -155,6 +155,7 @@ class ExceptionCause(IntEnum, shape=4): INSTRUCTION_PAGE_FAULT = 12 LOAD_PAGE_FAULT = 13 STORE_PAGE_FAULT = 15 + _COREBLOCKS_ASYNC_INTERRUPT = 16 @unique diff --git a/coreblocks/params/keys.py b/coreblocks/params/keys.py index ec11adb4f..a3e2e0ead 100644 --- a/coreblocks/params/keys.py +++ b/coreblocks/params/keys.py @@ -5,6 +5,7 @@ from transactron import Method from transactron.lib import MethodTryProduct, Collector from coreblocks.peripherals.wishbone import WishboneMaster +from amaranth import Signal if TYPE_CHECKING: from coreblocks.structs_common.csr_generic import GenericCSRRegisters # noqa: F401 @@ -15,6 +16,8 @@ "BranchResolvedKey", "ExceptionReportKey", "GenericCSRRegistersKey", + "AsyncInterruptInsertSignalKey", + "MretKey", ] @@ -41,3 +44,13 @@ class ExceptionReportKey(SimpleKey[Method]): @dataclass(frozen=True) class GenericCSRRegistersKey(SimpleKey["GenericCSRRegisters"]): pass + + +@dataclass(frozen=True) +class AsyncInterruptInsertSignalKey(SimpleKey[Signal]): + pass + + +@dataclass(frozen=True) +class MretKey(SimpleKey[Method]): + pass diff --git a/coreblocks/params/layouts.py b/coreblocks/params/layouts.py index b2e606be7..78874ea07 100644 --- a/coreblocks/params/layouts.py +++ b/coreblocks/params/layouts.py @@ -10,6 +10,7 @@ "DecodeLayouts", "FuncUnitLayouts", "RSInterfaceLayouts", + "RetirementLayouts", "RSLayouts", "RFLayouts", "UnsignedMulUnitLayouts", diff --git a/coreblocks/params/optypes.py b/coreblocks/params/optypes.py index 413fd3ad8..72ca461b8 100644 --- a/coreblocks/params/optypes.py +++ b/coreblocks/params/optypes.py @@ -99,7 +99,7 @@ class OpType(IntEnum): ], Extension.XINTMACHINEMODE: [ OpType.MRET, - OpType.WFI, + # OpType.WFI, - uncomment when WFI implemented, to not break fully supported extensions check ], Extension.XINTSUPERVISOR: [ OpType.SRET, diff --git a/coreblocks/stages/retirement.py b/coreblocks/stages/retirement.py index dd2784d6d..e356ef30d 100644 --- a/coreblocks/stages/retirement.py +++ b/coreblocks/stages/retirement.py @@ -1,5 +1,6 @@ from amaranth import * from coreblocks.params.dependencies import DependencyManager +from coreblocks.params.isa import ExceptionCause from coreblocks.params.keys import GenericCSRRegistersKey from coreblocks.params.layouts import CommonLayoutFields @@ -25,7 +26,8 @@ def __init__( frat_rename: Method, fetch_continue: Method, fetch_stall: Method, - instr_decrement: Method + instr_decrement: Method, + trap_entry: Method, ): self.gen_params = gen_params self.rob_peek = rob_peek @@ -40,6 +42,7 @@ def __init__( self.fetch_continue = fetch_continue self.fetch_stall = fetch_stall self.instr_decrement = instr_decrement + self.trap_entry = trap_entry self.instret_csr = DoubleCounterCSR(gen_params, CSRAddress.INSTRET, CSRAddress.INSTRETH) @@ -51,6 +54,7 @@ def elaborate(self, platform): side_fx = Signal(reset=1) side_fx_comb = Signal() + last_commited = Signal() m.d.comb += side_fx_comb.eq(side_fx) @@ -77,15 +81,30 @@ def elaborate(self, platform): m.d.comb += side_fx_comb.eq(0) self.fetch_stall(m) - # TODO: only set mcause/trigger IC if cause is actual exception and not e.g. - # misprediction or pipeline flush after some fence.i or changing ISA cause_register = self.exception_cause_get(m) - entry = Signal(self.gen_params.isa.xlen) - # MSB is exception bit - m.d.comb += entry.eq(cause_register.cause | (1 << (self.gen_params.isa.xlen - 1))) - m_csr.mcause.write(m, entry) + + cause_entry = Signal(self.gen_params.isa.xlen) + + with m.If(cause_register.cause == ExceptionCause._COREBLOCKS_ASYNC_INTERRUPT): + # Async interrupts are inserted only by JumpBranchUnit and conditionally by MRET and CSR operations. + # The PC field is set to address of instruction to resume from interrupt (e.g. for jumps it is + # a jump result). + # Instruction that reported interrupt is the last one that is commited. + m.d.comb += side_fx_comb.eq(1) + # Another flag is needed to resume execution if it was the last instruction in core + m.d.comb += last_commited.eq(1) + + # TODO: set correct interrupt id (from InterruptController) when multiple interrupts are supported + # Set MSB - the Interrupt bit + m.d.comb += cause_entry.eq(1 << (self.gen_params.isa.xlen - 1)) + with m.Else(): + m.d.comb += cause_entry.eq(cause_register.cause) + + m_csr.mcause.write(m, cause_entry) m_csr.mepc.write(m, cause_register.pc) + self.trap_entry(m) + # set rl_dst -> rp_dst in R-RAT rat_out = self.r_rat_commit( m, rl_dst=rob_entry.rob_data.rl_dst, rp_dst=rob_entry.rob_data.rp_dst, side_fx=side_fx @@ -110,7 +129,7 @@ def elaborate(self, platform): core_empty = self.instr_decrement(m) # cycle when fetch_stop is called, is the last cycle when new instruction can be fetched. # in this case, counter will be incremented and result correct (non-empty). - with m.If(~side_fx_comb & core_empty): + with m.If((~side_fx_comb | last_commited) & core_empty): # Resume core operation from exception handler # mtvec without mode is [mxlen-1:2], mode is two last bits. Only direct mode is supported @@ -127,6 +146,7 @@ def elaborate(self, platform): self.rename(m, rl_s1=0, rl_s2=0, rl_dst=data["rl_dst"], rp_dst=data["rp_dst"]) with Transaction().body(m): + # Implicitly depends on fetch_stall and fetch_verify method conflict! pc = fetch_continue_fwd.read(m).pc self.fetch_continue(m, from_pc=0, next_pc=pc, resume_from_exception=1) diff --git a/coreblocks/structs_common/csr.py b/coreblocks/structs_common/csr.py index 59e726139..38ffc7b89 100644 --- a/coreblocks/structs_common/csr.py +++ b/coreblocks/structs_common/csr.py @@ -7,9 +7,14 @@ from coreblocks.params.genparams import GenParams from coreblocks.params.dependencies import DependencyManager, ListKey from coreblocks.params.fu_params import BlockComponentParams -from coreblocks.params.layouts import FetchLayouts, FuncUnitLayouts, CSRLayouts +from coreblocks.params.layouts import ExceptionRegisterLayouts, FetchLayouts, FuncUnitLayouts, CSRLayouts from coreblocks.params.isa import Funct3, ExceptionCause -from coreblocks.params.keys import BranchResolvedKey, ExceptionReportKey, InstructionPrecommitKey +from coreblocks.params.keys import ( + AsyncInterruptInsertSignalKey, + BranchResolvedKey, + ExceptionReportKey, + InstructionPrecommitKey, +) from coreblocks.params.optypes import OpType from coreblocks.utils.protocols import FuncBlock @@ -326,20 +331,44 @@ def _(): m.d.sync += instr.valid.eq(0) m.d.sync += done.eq(0) + report = self.dependency_manager.get_dependency(ExceptionReportKey()) + interrupt = self.dependency_manager.get_dependency(AsyncInterruptInsertSignalKey()) + exception_entry = Record(self.gen_params.get(ExceptionRegisterLayouts).report) + with m.If(exception): - report = self.dependency_manager.get_dependency(ExceptionReportKey()) - report(m, rob_id=instr.rob_id, cause=ExceptionCause.ILLEGAL_INSTRUCTION, pc=instr.pc) + m.d.comb += assign( + exception_entry, + {"rob_id": instr.rob_id, "cause": ExceptionCause.ILLEGAL_INSTRUCTION, "pc": instr.pc}, + ) + with m.Elif(interrupt): + # SPEC: "These conditions for an interrupt trap to occur [..] must also be evaluated immediately + # following [..] an explicit write to a CSR on which these interrupt trap conditions expressly depend." + # At this time CSR operation is finished. If it caused triggering an interrupt, it would be represented + # by interrupt signal in this cycle. + # CSR instructions are never compressed, PC+4 is always next instruction + m.d.comb += assign( + exception_entry, + { + "rob_id": instr.rob_id, + "cause": ExceptionCause._COREBLOCKS_ASYNC_INTERRUPT, + "pc": instr.pc + self.gen_params.isa.ilen_bytes, + }, + ) + with m.If(exception | interrupt): + report(m, exception_entry) + m.d.sync += exception.eq(0) return { "rob_id": instr.rob_id, "rp_dst": instr.rp_dst, "result": current_result, - "exception": exception, + "exception": exception | interrupt, } @def_method(m, self.fetch_continue, accepted) def _(): + # CSR instructions are never compressed, PC+4 is always next instruction return { "from_pc": instr.pc, "next_pc": instr.pc + self.gen_params.isa.ilen_bytes, diff --git a/coreblocks/structs_common/interrupt_controller.py b/coreblocks/structs_common/interrupt_controller.py new file mode 100644 index 000000000..2fa9c932d --- /dev/null +++ b/coreblocks/structs_common/interrupt_controller.py @@ -0,0 +1,44 @@ +from amaranth import * +from coreblocks.params.dependencies import DependencyManager +from coreblocks.params.genparams import GenParams +from coreblocks.params.keys import AsyncInterruptInsertSignalKey, MretKey + +from transactron.core import Method, TModule, def_method + + +class InterruptController(Elaboratable): + def __init__(self, gp: GenParams): + dm = gp.get(DependencyManager) + + self.interrupt_insert = Signal() + dm.add_dependency(AsyncInterruptInsertSignalKey(), self.interrupt_insert) + + self.report_interrupt = Method() + + self.mret = Method() + dm.add_dependency(MretKey(), self.mret) + + self.entry = Method() + + self.interrupts_enabled = Signal(reset=1) # Temporarily needed globally accessibletests + + def elaborate(self, platform): + m = TModule() + + interrupt_pending = Signal() + m.d.comb += self.interrupt_insert.eq(interrupt_pending & self.interrupts_enabled) + + @def_method(m, self.report_interrupt) + def _(): + m.d.sync += interrupt_pending.eq(1) + + @def_method(m, self.mret) + def _(): + m.d.sync += self.interrupts_enabled.eq(1) + + @def_method(m, self.entry) + def _(): + m.d.sync += interrupt_pending.eq(0) + m.d.sync += self.interrupts_enabled.eq(0) + + return m diff --git a/test/asm/interrupt.asm b/test/asm/interrupt.asm new file mode 100644 index 000000000..1a90969db --- /dev/null +++ b/test/asm/interrupt.asm @@ -0,0 +1,47 @@ +# fibonacci spiced with interrupt handler (also with fibonacci) + li x1, 0x100 + csrw mtvec, x1 + li x30, 0 # interrupt count + li x31, 0xde # branch guard + li x1, 0 + li x2, 1 + li x5, 4 + li x6, 7 + li x7, 0 +loop: + add x3, x2, x1 + mv x1, x2 + mv x2, x3 + bne x2, x4, loop +infloop: + j infloop + +int_handler: + # save main loop register state + mv x9, x1 + mv x10, x2 + mv x11, x3 + addi x30, x30, 1 + # load state + mv x1, x5 + mv x2, x6 + mv x3, x7 + # fibonacci step + beq x3, x8, skip + add x3, x2, x1 + mv x1, x2 + mv x2, x3 + # store state + mv x5, x1 + mv x6, x2 + mv x7, x3 +skip: + # restore main loop register state + mv x1, x9 + mv x2, x10 + mv x3, x11 + mret + +.org 0x100 + j int_handler + li x31, 0xae # should never happen diff --git a/test/frontend/test_decoder.py b/test/frontend/test_decoder.py index 8e6c99f70..df5effbe5 100644 --- a/test/frontend/test_decoder.py +++ b/test/frontend/test_decoder.py @@ -140,7 +140,7 @@ def __init__( # MRET InstrTest(0x30200073, Opcode.SYSTEM, Funct3.PRIV, funct12=Funct12.MRET, op=OpType.MRET), # WFI - InstrTest(0x10500073, Opcode.SYSTEM, Funct3.PRIV, funct12=Funct12.WFI, op=OpType.WFI), + # InstrTest(0x10500073, Opcode.SYSTEM, Funct3.PRIV, funct12=Funct12.WFI, op=OpType.WFI), ] DECODER_TESTS_XINTSUPERVISOR = [ # SRET diff --git a/test/fu/functional_common.py b/test/fu/functional_common.py index 481aee667..45cb1d505 100644 --- a/test/fu/functional_common.py +++ b/test/fu/functional_common.py @@ -4,7 +4,7 @@ from collections import deque from typing import Generic, TypeVar -from amaranth import Elaboratable, Module +from amaranth import Elaboratable, Module, Signal from amaranth.sim import Passive from coreblocks.params import GenParams @@ -12,7 +12,7 @@ from coreblocks.params.dependencies import DependencyManager from coreblocks.params.fu_params import FunctionalComponentParams from coreblocks.params.isa import Funct3, Funct7 -from coreblocks.params.keys import ExceptionReportKey +from coreblocks.params.keys import AsyncInterruptInsertSignalKey, ExceptionReportKey from coreblocks.params.layouts import ExceptionRegisterLayouts from coreblocks.params.optypes import OpType from transactron.lib import AdapterTrans, Adapter @@ -42,6 +42,7 @@ def elaborate(self, platform): Adapter(i=self.gen.get(ExceptionRegisterLayouts).report) ) self.gen.get(DependencyManager).add_dependency(ExceptionReportKey(), self.report_mock.adapter.iface) + self.gen.get(DependencyManager).add_dependency(AsyncInterruptInsertSignalKey(), Signal()) m.submodules.func_unit = func_unit = self.func_unit.get_module(self.gen) diff --git a/test/params/test_configurations.py b/test/params/test_configurations.py index f73f1b98e..786dbad93 100644 --- a/test/params/test_configurations.py +++ b/test/params/test_configurations.py @@ -16,8 +16,15 @@ class ISAStrTest: gp_str: str TEST_CASES = [ - ISAStrTest(basic_core_config, "rv32i", "rv32i", "rv32i"), - ISAStrTest(full_core_config, "rv32imcbzicsr", "rv32imcbzicsr", "rv32imcbzicsr"), + ISAStrTest( + basic_core_config, "rv32izicsr_xintmachinemode", "rv32izicsr_xintmachinemode", "rv32izicsr_xintmachinemode" + ), + ISAStrTest( + full_core_config, + "rv32imcbzicsr_xintmachinemode", + "rv32imcbzicsr_xintmachinemode", + "rv32imcbzicsr_xintmachinemode", + ), ISAStrTest(tiny_core_config, "rv32e", "rv32", "rv32e"), ISAStrTest(test_core_config, "rv32", "rv32", "rv32i"), ] diff --git a/test/stages/test_retirement.py b/test/stages/test_retirement.py index 62a08c4a2..99ee3bf8d 100644 --- a/test/stages/test_retirement.py +++ b/test/stages/test_retirement.py @@ -54,6 +54,7 @@ def elaborate(self, platform): m.submodules.mock_instr_decrement = self.mock_instr_decrement = TestbenchIO( Adapter(o=core_instr_counter_layouts.decrement) ) + m.submodules.mock_trap_entry = self.mock_trap_entry = TestbenchIO(Adapter()) m.submodules.retirement = self.retirement = Retirement( self.gen_params, @@ -69,6 +70,7 @@ def elaborate(self, platform): fetch_stall=self.mock_fetch_stall.adapter.iface, fetch_continue=self.mock_fetch_continue.adapter.iface, instr_decrement=self.mock_instr_decrement.adapter.iface, + trap_entry=self.mock_trap_entry.adapter.iface, ) m.submodules.free_rf_fifo_adapter = self.free_rf_adapter = TestbenchIO(AdapterTrans(self.free_rf.read)) diff --git a/test/structs_common/test_csr.py b/test/structs_common/test_csr.py index fcf8eac83..caf263c4f 100644 --- a/test/structs_common/test_csr.py +++ b/test/structs_common/test_csr.py @@ -6,7 +6,7 @@ from coreblocks.params.isa import Funct3, ExceptionCause from coreblocks.params.configurations import test_core_config from coreblocks.params.layouts import ExceptionRegisterLayouts -from coreblocks.params.keys import ExceptionReportKey +from coreblocks.params.keys import AsyncInterruptInsertSignalKey, ExceptionReportKey from coreblocks.params.dependencies import DependencyManager from coreblocks.frontend.decoder import OpType @@ -35,6 +35,7 @@ def elaborate(self, platform): Adapter(i=self.gen_params.get(ExceptionRegisterLayouts).report) ) self.gen_params.get(DependencyManager).add_dependency(ExceptionReportKey(), self.exception_report.adapter.iface) + self.gen_params.get(DependencyManager).add_dependency(AsyncInterruptInsertSignalKey(), Signal()) m.submodules.fetch_continue = self.fetch_continue = TestbenchIO(AdapterTrans(self.dut.fetch_continue)) diff --git a/test/test_core.py b/test/test_core.py index 301fbbb5f..b92f2d759 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -1,7 +1,7 @@ from amaranth import Elaboratable, Module from transactron.lib import AdapterTrans -from transactron.utils import align_to_power_of_two +from transactron.utils import align_to_power_of_two, signed_to_int from .common import TestCaseWithSimulator, TestbenchIO @@ -57,14 +57,16 @@ def elaborate(self, platform): wb_params=self.gp.wb_params, width=32, depth=len(self.data_mem), init=self.data_mem ) self.core = Core(gen_params=self.gp, wb_instr_bus=wb_instr_bus, wb_data_bus=wb_data_bus) - self.io_in = TestbenchIO(AdapterTrans(self.core.fifo_fetch.write)) + self.io_in = TestbenchIO(AdapterTrans(self.core.fetch_continue.method)) self.rf_write = TestbenchIO(AdapterTrans(self.core.RF.write)) + self.interrupt = TestbenchIO(AdapterTrans(self.core.interrupt_controller.report_interrupt)) m.submodules.wb_mem_slave = self.wb_mem_slave m.submodules.wb_mem_slave_data = self.wb_mem_slave_data m.submodules.c = self.core m.submodules.io_in = self.io_in m.submodules.rf_write = self.rf_write + m.submodules.interrupt = self.interrupt m.d.comb += wb_instr_bus.connect(self.wb_mem_slave.bus) m.d.comb += wb_data_bus.connect(self.wb_mem_slave_data.bus) @@ -119,6 +121,16 @@ def compare_core_states(self, sw_core): unsigned_val = reg_val & 0xFFFFFFFF self.assertEqual((yield from self.get_arch_reg_val(i)), unsigned_val) + def push_register_load_imm(self, reg_id, val): + addi_imm = signed_to_int(val & 0xFFF, 12) + lui_imm = (val & 0xFFFFF000) >> 12 + # handle addi sign extension, see: https://stackoverflow.com/a/59546567 + if val & 0x800: + lui_imm = (lui_imm + 1) & (0xFFFFF) + + yield from self.push_instr(InstructionLUI(reg_id, lui_imm).encode()) + yield from self.push_instr(InstructionADDI(reg_id, reg_id, addi_imm).encode()) + class TestCoreSimple(TestCoreBase): def simple_test(self): @@ -237,35 +249,11 @@ def test_randomized(self): sim.add_sync_process(self.randomized_input) -@parameterized_class( - ("name", "source_file", "cycle_count", "expected_regvals", "configuration"), - [ - ("fibonacci", "fibonacci.asm", 1200, {2: 2971215073}, basic_core_config), - ("fibonacci_mem", "fibonacci_mem.asm", 610, {3: 55}, basic_core_config), - ("csr", "csr.asm", 200, {1: 1, 2: 4}, full_core_config), - ("exception", "exception.asm", 200, {1: 1, 2: 2}, basic_core_config), - ("exception_mem", "exception_mem.asm", 200, {1: 1, 2: 2}, basic_core_config), - ("exception_handler", "exception_handler.asm", 1500, {2: 987, 11: 0xAAAA, 15: 16}, full_core_config), - ], -) -class TestCoreAsmSource(TestCoreBase): - source_file: str - cycle_count: int - expected_regvals: dict[int, int] - configuration: CoreConfiguration - - def run_and_check(self): - for i in range(self.cycle_count): - yield - - for reg_id, val in self.expected_regvals.items(): - self.assertEqual((yield from self.get_arch_reg_val(reg_id)), val) - - def test_asm_source(self): - self.gp = GenParams(self.configuration) - self.base_dir = "test/asm/" - self.bin_src = [] +class TestCoreAsmSourceBase(TestCoreBase): + base_dir: str = "test/asm/" + def prepare_source(self, filename): + bin_src = [] with ( tempfile.NamedTemporaryFile() as asm_tmp, tempfile.NamedTemporaryFile() as ld_tmp, @@ -280,7 +268,7 @@ def test_asm_source(self): "-march=rv32im_zicsr", "-o", asm_tmp.name, - self.base_dir + self.source_file, + self.base_dir + filename, ] ) subprocess.check_call( @@ -302,8 +290,113 @@ def test_asm_source(self): for word_idx in range(0, len(code), 4): word = code[word_idx : word_idx + 4] bin_instr = int.from_bytes(word, "little") - self.bin_src.append(bin_instr) + bin_src.append(bin_instr) - self.m = TestElaboratable(self.gp, instr_mem=self.bin_src) + return bin_src + + +@parameterized_class( + ("name", "source_file", "cycle_count", "expected_regvals", "configuration"), + [ + ("fibonacci", "fibonacci.asm", 1200, {2: 2971215073}, basic_core_config), + ("fibonacci_mem", "fibonacci_mem.asm", 610, {3: 55}, basic_core_config), + ("csr", "csr.asm", 200, {1: 1, 2: 4}, full_core_config), + ("exception", "exception.asm", 200, {1: 1, 2: 2}, basic_core_config), + ("exception_mem", "exception_mem.asm", 200, {1: 1, 2: 2}, basic_core_config), + ("exception_handler", "exception_handler.asm", 1500, {2: 987, 11: 0xAAAA, 15: 16}, full_core_config), + ], +) +class TestCoreBasicAsm(TestCoreAsmSourceBase): + source_file: str + cycle_count: int + expected_regvals: dict[int, int] + configuration: CoreConfiguration + + def run_and_check(self): + for _ in range(self.cycle_count): + yield + + for reg_id, val in self.expected_regvals.items(): + self.assertEqual((yield from self.get_arch_reg_val(reg_id)), val) + + def test_asm_source(self): + self.gp = GenParams(self.configuration) + + bin_src = self.prepare_source(self.source_file) + self.m = TestElaboratable(self.gp, instr_mem=bin_src) with self.run_simulation(self.m) as sim: sim.add_sync_process(self.run_and_check) + + +# test interrupts with varying triggering frequency (parametrizable amount of cycles between +# returning from an interrupt and triggering it again with 'lo' and 'hi' parameters) +@parameterized_class( + ("source_file", "main_cycle_count", "start_regvals", "expected_regvals", "lo", "hi"), + [ + ("interrupt.asm", 400, {4: 2971215073, 8: 29}, {2: 2971215073, 7: 29, 31: 0xDE}, 300, 500), + ("interrupt.asm", 700, {4: 24157817, 8: 199}, {2: 24157817, 7: 199, 31: 0xDE}, 100, 200), + ("interrupt.asm", 600, {4: 89, 8: 843}, {2: 89, 7: 843, 31: 0xDE}, 30, 50), + # interrupts are only inserted on branches, we always have some forward progression. 15 for trigger variantion. + ("interrupt.asm", 80, {4: 21, 8: 9349}, {2: 21, 7: 9349, 31: 0xDE}, 0, 15), + ], +) +class TestCoreInterrupt(TestCoreAsmSourceBase): + source_file: str + main_cycle_count: int + start_regvals: dict[int, int] + expected_regvals: dict[int, int] + lo: int + hi: int + + def setUp(self): + self.configuration = full_core_config + self.gp = GenParams(self.configuration) + random.seed(1500100900) + + def run_with_interrupt(self): + main_cycles = 0 + int_count = 0 + + # set up fibonacci max numbers + for reg_id, val in self.start_regvals.items(): + yield from self.push_register_load_imm(reg_id, val) + # wait for caches to fill up so that mtvec is written - very important + # TODO: replace with interrupt enable via CSR + yield from self.tick(200) + + early_interrupt = False + while main_cycles < self.main_cycle_count or early_interrupt: + if not early_interrupt: + # run main code for some semi-random amount of cycles + c = random.randrange(self.lo, self.hi) + main_cycles += c + yield from self.tick(c) + # trigger an interrupt + yield from self.m.interrupt.call() + yield + int_count += 1 + + # wait for the interrupt to get registered + while (yield self.m.core.interrupt_controller.interrupts_enabled) == 1: + yield + + # trigger interrupt during execution of ISR handler (blocked-pending) with some chance + early_interrupt = random.random() < 0.4 + if early_interrupt: + yield from self.m.interrupt.call() + yield + int_count += 1 + + # wait until ISR returns + while (yield self.m.core.interrupt_controller.interrupts_enabled) == 0: + yield + + self.assertEqual((yield from self.get_arch_reg_val(30)), int_count) + for reg_id, val in self.expected_regvals.items(): + self.assertEqual((yield from self.get_arch_reg_val(reg_id)), val) + + def test_interrupted_prog(self): + bin_src = self.prepare_source(self.source_file) + self.m = TestElaboratable(self.gp, instr_mem=bin_src) + with self.run_simulation(self.m) as sim: + sim.add_sync_process(self.run_with_interrupt) From 723072ba39dce1c8d5d4e016e6148037775644e1 Mon Sep 17 00:00:00 2001 From: lekcyjna123 <34948061+lekcyjna123@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:53:35 +0100 Subject: [PATCH 23/25] Update synthesis docs (#454) Co-authored-by: Marek Materzok Co-authored-by: Kristopher38 --- docs/synthesis/synthesis.md | 177 +++++++++++++++++++++++++++++------- 1 file changed, 145 insertions(+), 32 deletions(-) diff --git a/docs/synthesis/synthesis.md b/docs/synthesis/synthesis.md index 7f3112be1..c1286fd6c 100644 --- a/docs/synthesis/synthesis.md +++ b/docs/synthesis/synthesis.md @@ -1,49 +1,162 @@ -# Synthesis +# Core verification -CoreBlocks synthesizes `Core` circuit to test how many resources it consumes as the project -grows and more functionalities are added. +Coreblocks is verified at several levels of abstraction. Beside of unit tests and module tests, we also +synthesise the core to the ECP5 FPGA target, to check that it can work in reality. Performance is verified +using synthesis results and a set of benchmarks simulated with cycle precision in cocotb. We also verify +correctness of the core behaviour by running assembler tests from [riscv-tests](https://github.com/riscv-software-src/riscv-tests/tree/master) +and [riscv-arch-tests](https://github.com/riscv-non-isa/riscv-arch-test). -## Documentation +These three verification steps are automatically run by CI on every commit delivered to the `master` branch. Running +the checks in CI allow us to collect historical data, which are available in the form of the graphs +on a dedicated [benchmark subpage](https://kuznia-rdzeni.github.io/coreblocks/dev/benchmark/). -### Requirements +In CI we use pre-built docker containers, which are publicly available on our [github page](https://github.com/orgs/kuznia-rdzeni/packages). +In the following subsections we provide the instructions on how to manually run verification steps using these containers. +They can be recreated using standard docker build commands: -In order to perform synthesis you will need to install following tools: - * [yosys](https://github.com/YosysHQ/yosys) - * [prjtrellis](https://github.com/YosysHQ/prjtrellis) - * [nextpnr-ecp5](https://github.com/YosysHQ/nextpnr.git) - -These tools may need manual compilation from git repository, that can take some time. - -You can use docker images that have installed all required tools to perform synthesis: - * [vuush/amaranth-synth:ecp5](https://hub.docker.com/r/vuush/amaranth-synth/tags) - -To build the `AmaranthSynthECP5.Dockerfile` yourself use following command: ``` -docker build --platform linux/amd64 -t "amaranth-synth:ecp5" -f ./docker/AmaranthSynthECP5.Dockerfile . +docker build --platform linux/amd64 -t "amaranth-synth:latest" -f ./docker/AmaranthSynthECP5.Dockerfile . ``` -### Usage - -Script named `synthesize.py` is used to perform the `Core` synthesis. - -Example usage: -``` -./scripts/synthesize.py --help -./scripts/synthesize.py --platform ecp5 --verbose -``` +## Synthesis -To collect synthesis information we use script named `parse_benchmark_info.py`. +The basic step in verification is to see if it is possible to synthesise the `Core` circuit. This allows us to +control the level of complexity of the core. Although Coreblocks is an educational core, we want it to be practical. +It should have an acceptable maximum frequency and shouldn't use too many resources, so it can be run +on a FPGA. The synthesis step ensures that these requirements are met. In addition, it checks whether the code that is acceptable +for Amaranth is also acceptable for the synthesis tools. -This script parses the output of the synthesis tool and extracts the -following information: +The main properties collected in the synthesis step: - Max clock frequency - Number of logic cells used - Number of carry cells used - Number of RAM cells used - Number of DFF cells used -## Benchmarks +The configuration of the docker container is described in the `AmaranthSynthECP5.Dockerfile`, which can be found in +[our repo](https://github.com/orgs/kuznia-rdzeni/packages/container/package/amaranth-synth). + +### Manual reproduction + +```bash +sudo docker pull ghcr.io/kuznia-rdzeni/amaranth-synth:latest +sudo docker run -it --rm ghcr.io/kuznia-rdzeni/amaranth-synth:latest +git clone --depth=1 https://github.com/kuznia-rdzeni/coreblocks.git +cd coreblocks +apt update +apt install python3.11-venv +python3 -m venv venv +. venv/bin/activate +python3 -m pip install --upgrade pip +pip3 install -r requirements-dev.txt +PYTHONHASHSEED=0 ./scripts/synthesize.py --verbose --config full +./scripts/parse_benchmark_info.py +cat benchmark.json +``` + +The main point of the above listing is the `synthesize.py` script, which creates an instance of the `Core` object with +the configuration provided by the user and then passes it to Amaranth to generate a Verilog description from that instance. +This description is then processed by Yosys and nextpnr-ecp5 to generate the ECP5 bitstream. + +A strength of Coreblocks is its modularity, so we can provide different configurations with little effort. You can choose +a configuration to synthesise using the `--config` argument to the `synthesise.py` script. + +### Dependencies -For each commit on `master` branch, CI runs the synthesis and saves the parameters collected by `parse_benchmark_info` script. +In order to perform synthesis we use: + * [yosys](https://github.com/YosysHQ/yosys) - to synthesise the Verilog generated by Amaranth up to gate level; + * [nextpnr-ecp5](https://github.com/YosysHQ/nextpnr.git) - to perform the "Place and Route" step; + * [prjtrellis](https://github.com/YosysHQ/prjtrellis) - provides the description of the ECP5 bitstream format. -Graphs generated from this information are available on a dedicated [subpage](https://kuznia-rdzeni.github.io/coreblocks/dev/benchmark/). +## Benchmarking + +The maximum clock frequency determined by synthesis isn't the only measure of performance. In theory, it is always +possible to increase Fmax by increasing latency. To avoid the pitfall of too long latency affecting the core +throughput, we monitor the number of instructions executed per clock cycle (IPC). We simulate the core with cycle +accuracy and run benchmarks written in C inside the simulation. The benchmarks are taken from +[embench](https://github.com/embench/embench-iot/tree/master). + + +The benchmarking is done in two steps. First, we compile the C programs into binary format. Second, we run the binaries +on the simulated core. To compile the code we use [riscv-gnu-toolchain](https://github.com/riscv/riscv-gnu-toolchain), +with glibc configured for different architectural subsets of RISC-V extensions (you can check the exact configuration in +[riscv-toolchain.Dockerfile](https://github.com/kuznia-rdzeni/coreblocks/blob/master/docker/riscv-toolchain.Dockerfile)). + +Once we have the binaries, we can run them in simulation. This is done using [Cocotb](https://github.com/cocotb/cocotb) and +[Verilator](https://github.com/verilator/verilator). We use Amaranth features to generate Verilog code describing Coreblocks instance, +which is passed to Verilator for compilation. Cocotb controls the simulation and execution of the program by stubbing external +interfaces. Pre-compiled Verilator in a compatible version is available in [Verilator.Dockerfile](https://github.com/kuznia-rdzeni/coreblocks/blob/master/docker/Verilator.Dockerfile). + + +### Benchmarks manual execution +```bash +# ========== STEP 1: Compilation ========== +# Clone coreblocks into host file system +git clone --depth=1 https://github.com/kuznia-rdzeni/coreblocks.git +cd coreblocks +git submodule update --init --recursive +cd .. +sudo docker pull ghcr.io/kuznia-rdzeni/riscv-toolchain:latest +# Run docker with the coreblocks directory mounted into it +sudo docker run -v ./coreblocks:/coreblocks -it --rm ghcr.io/kuznia-rdzeni/riscv-toolchain:latest +cd /coreblocks/test/external/embench +# Compilation will put binaries in the subdirectory of the /coreblocks directory, which is shared with the host +# so that binaries survive after the docker container is closed +make +exit + +# ========== STEP 2: Execution ========== +sudo docker pull ghcr.io/kuznia-rdzeni/verilator:latest +# Run docker with the coreblocks directory mounted into it. This directory contains +# benchmark binaries after running the first step. +sudo docker run -v ./coreblocks:/coreblocks -it --rm ghcr.io/kuznia-rdzeni/verilator:latest +apt update +apt install python3.11-venv +python3 -m venv venv +. venv/bin/activate +python3 -m pip install --upgrade pip +cd coreblocks +pip3 install -r requirements-dev.txt +PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full +./scripts/run_benchmarks.py +``` + +## Regression tests + +Regression tests should ensure that Coreblocks is compliant with RISC-V specification requirements. Tests include +assembler programs that tests entire RISC-V instruction set. We execute these programs in a similar way to benchmarks. +So, as a first step, we compile the programs to the binary format and then we run them on core simulated by Verilator +and Cocotb. + +### Regression tests manual execution +```bash +# ========== STEP 1: Compilation ========== +# Clone coreblocks into host file system +git clone --depth=1 https://github.com/kuznia-rdzeni/coreblocks.git +cd coreblocks +git submodule update --init --recursive +cd .. +sudo docker pull ghcr.io/kuznia-rdzeni/riscv-toolchain:latest +# Run docker with the coreblocks directory mounted into it +sudo docker run -v ./coreblocks:/coreblocks -it --rm ghcr.io/kuznia-rdzeni/riscv-toolchain:latest +cd /coreblocks/test/external/riscv-tests +# Compilation will put binaries in the subdirectory of the /coreblocks directory, which is shared with the host +# so that binaries survive after the docker container is closed +make +exit + +# ========== STEP 2: Execution ========== +sudo docker pull ghcr.io/kuznia-rdzeni/verilator:latest +# Run docker with the coreblocks directory mounted into it. This directory contains +# regression test binaries after running the first step. +sudo docker run -v ./coreblocks:/coreblocks -it --rm ghcr.io/kuznia-rdzeni/verilator:latest +apt update +apt install python3.11-venv +python3 -m venv venv +. venv/bin/activate +python3 -m pip install --upgrade pip +cd coreblocks +pip3 install -r requirements-dev.txt +PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full +./scripts/run_tests.py -a regression +``` From f93ae2f98c3221dee0d51a15f9b703fd030cc5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Marsza=C5=82ek?= <73073559+Durchbruchswagen@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:18:48 +0100 Subject: [PATCH 24/25] AMBA AXI4-Lite master (#524) --- coreblocks/peripherals/axi_lite.py | 285 +++++++++++++++++++++++++++++ test/peripherals/test_axi_lite.py | 263 ++++++++++++++++++++++++++ 2 files changed, 548 insertions(+) create mode 100644 coreblocks/peripherals/axi_lite.py create mode 100644 test/peripherals/test_axi_lite.py diff --git a/coreblocks/peripherals/axi_lite.py b/coreblocks/peripherals/axi_lite.py new file mode 100644 index 000000000..6fd3fac01 --- /dev/null +++ b/coreblocks/peripherals/axi_lite.py @@ -0,0 +1,285 @@ +from amaranth import * +from amaranth.hdl.rec import DIR_FANIN, DIR_FANOUT +from transactron import Method, def_method, TModule +from transactron.core import Transaction +from transactron.lib.connectors import Forwarder + +__all__ = ["AXILiteParameters", "AXILiteMaster"] + + +class AXILiteParameters: + """Parameters of the AXI-Lite bus. + + Parameters + ---------- + data_width: int + Width of "data" signals for "write data" and "read data" channels. Must be either 32 or 64 bits. Defaults to 64 + addr_width: int + Width of "addr" signals for "write address" and "read address" channels. Defaults to 64 bits. + """ + + def __init__(self, *, data_width: int = 64, addr_width: int = 64): + self.data_width = data_width + self.addr_width = addr_width + + +class AXILiteLayout: + """AXI-Lite bus layout generator + + Parameters + ---------- + axil_params: AXILiteParameters + Patameters used to generate AXI-Lite layout + master: Boolean + Whether the layout should be generated for master side + (if false it's generatd for the slave side) + + Attributes + ---------- + axil_layout: Record + Record of a AXI-Lite bus. + """ + + def __init__(self, axil_params: AXILiteParameters, *, master: bool = True): + write_address = [ + ("valid", 1, DIR_FANOUT if master else DIR_FANIN), + ("rdy", 1, DIR_FANIN if master else DIR_FANOUT), + ("addr", axil_params.addr_width, DIR_FANOUT if master else DIR_FANIN), + ("prot", 3, DIR_FANOUT if master else DIR_FANIN), + ] + + write_data = [ + ("valid", 1, DIR_FANOUT if master else DIR_FANIN), + ("rdy", 1, DIR_FANIN if master else DIR_FANOUT), + ("data", axil_params.data_width, DIR_FANOUT if master else DIR_FANIN), + ("strb", axil_params.data_width // 8, DIR_FANOUT if master else DIR_FANIN), + ] + + write_response = [ + ("valid", 1, DIR_FANIN if master else DIR_FANOUT), + ("rdy", 1, DIR_FANOUT if master else DIR_FANIN), + ("resp", 2, DIR_FANIN if master else DIR_FANOUT), + ] + + read_address = [ + ("valid", 1, DIR_FANOUT if master else DIR_FANIN), + ("rdy", 1, DIR_FANIN if master else DIR_FANOUT), + ("addr", axil_params.addr_width, DIR_FANOUT if master else DIR_FANIN), + ("prot", 3, DIR_FANOUT if master else DIR_FANIN), + ] + + read_data = [ + ("valid", 1, DIR_FANIN if master else DIR_FANOUT), + ("rdy", 1, DIR_FANOUT if master else DIR_FANIN), + ("data", axil_params.data_width, DIR_FANIN if master else DIR_FANOUT), + ("resp", 2, DIR_FANIN if master else DIR_FANOUT), + ] + + self.axil_layout = [ + ("write_address", write_address), + ("write_data", write_data), + ("write_response", write_response), + ("read_address", read_address), + ("read_data", read_data), + ] + + +class AXILiteMasterMethodLayouts: + """AXI-Lite master layouts for methods + + Parameters + ---------- + axil_params: AXILiteParameters + Patameters used to generate AXI-Lite master layouts + + Attributes + ---------- + ra_request_layout: Layout + Layout for ra_request method of AXILiteMaster. + + wa_request_layout: Layout + Layout for wa_request method of AXILiteMaster. + + wd_request_layout: Layout + Layout for wd_request method of AXILiteMaster. + + rd_response_layout: Layout + Layout for rd_response method of AXILiteMaster. + + wr_response_layout: Layout + Layout for wr_response method of AXILiteMaster. + """ + + def __init__(self, axil_params: AXILiteParameters): + self.ra_request_layout = [ + ("addr", axil_params.addr_width, DIR_FANIN), + ("prot", 3, DIR_FANIN), + ] + + self.wa_request_layout = [ + ("addr", axil_params.addr_width, DIR_FANIN), + ("prot", 3, DIR_FANIN), + ] + + self.wd_request_layout = [ + ("data", axil_params.data_width, DIR_FANIN), + ("strb", axil_params.data_width // 8, DIR_FANIN), + ] + + self.rd_response_layout = [ + ("data", axil_params.data_width), + ("resp", 2), + ] + + self.wr_response_layout = [ + ("resp", 2), + ] + + +class AXILiteMaster(Elaboratable): + """AXI-Lite master interface. + + Parameters + ---------- + axil_params: AXILiteParameters + Parameters for bus generation. + + Attributes + ---------- + ra_request: Method + Transactional method for initiating request on read address channel. + Ready when no request or only one is being executed. + Takes 'ra_request_layout' as argument. + + rd_response: Method + Transactional method for reading response from read data channel. + Ready when there is request response availabe. + Returns data and response state as 'rd_response_layout'. + + wa_request: Method + Transactional method for initiating request on write address channel. + Ready when no request or only one is being executed. + Takes 'wa_request_layout' as argument. + + wd_request: Method + Transactional method for initiating request on write data channel. + Ready when no request or only one is being executed. + Takes 'wd_request_layout' as argument. + + wr_response: Method + Transactional method for reading response from write response channel. + Ready when there is request response availabe. + Returns response state as 'wr_response_layout'. + """ + + def __init__(self, axil_params: AXILiteParameters): + self.axil_params = axil_params + + self.method_layouts = AXILiteMasterMethodLayouts(self.axil_params) + + self.ra_request = Method(i=self.method_layouts.ra_request_layout) + self.rd_response = Method(o=self.method_layouts.rd_response_layout) + self.wa_request = Method(i=self.method_layouts.wa_request_layout) + self.wd_request = Method(i=self.method_layouts.wd_request_layout) + self.wr_response = Method(o=self.method_layouts.wr_response_layout) + + def start_request_transaction(self, m, arg, *, channel, is_address_channel): + if is_address_channel: + m.d.sync += channel.addr.eq(arg.addr) + m.d.sync += channel.prot.eq(arg.prot) + else: + m.d.sync += channel.data.eq(arg.data) + m.d.sync += channel.strb.eq(arg.strb) + m.d.sync += channel.valid.eq(1) + + def state_machine_request(self, m: TModule, method: Method, *, channel: Record, request_signal: Signal): + with m.FSM("Idle"): + with m.State("Idle"): + m.d.sync += channel.valid.eq(0) + m.d.comb += request_signal.eq(1) + with m.If(method.run): + m.next = "Active" + + with m.State("Active"): + with m.If(channel.rdy): + m.d.comb += request_signal.eq(1) + with m.If(~method.run): + m.d.sync += channel.valid.eq(0) + m.next = "Idle" + with m.Else(): + m.d.comb += request_signal.eq(0) + + def result_handler(self, m: TModule, forwarder: Forwarder, *, data: bool, channel: Record): + with m.If(channel.rdy & channel.valid): + m.d.sync += channel.rdy.eq(forwarder.read.run) + with Transaction().body(m): + if data: + forwarder.write(m, data=channel.data, resp=channel.resp) + else: + forwarder.write(m, resp=channel.resp) + with m.Else(): + m.d.sync += channel.rdy.eq(forwarder.write.ready) + + def elaborate(self, platform): + m = TModule() + + self.axil_layout = AXILiteLayout(self.axil_params).axil_layout + self.axil_master = Record(self.axil_layout) + + m.submodules.rd_forwarder = rd_forwarder = Forwarder(self.method_layouts.rd_response_layout) + m.submodules.wr_forwarder = wr_forwarder = Forwarder(self.method_layouts.wr_response_layout) + + ra_request_ready = Signal() + wa_request_ready = Signal() + wd_request_ready = Signal() + # read_address + self.state_machine_request( + m, + self.ra_request, + channel=self.axil_master.read_address, + request_signal=ra_request_ready, + ) + + @def_method(m, self.ra_request, ready=ra_request_ready) + def _(arg): + self.start_request_transaction(m, arg, channel=self.axil_master.read_address, is_address_channel=True) + + # read_data + self.result_handler(m, rd_forwarder, data=True, channel=self.axil_master.read_data) + + @def_method(m, self.rd_response) + def _(): + return rd_forwarder.read(m) + + # write_adress + self.state_machine_request( + m, + self.wa_request, + channel=self.axil_master.write_address, + request_signal=wa_request_ready, + ) + + @def_method(m, self.wa_request, ready=wa_request_ready) + def _(arg): + self.start_request_transaction(m, arg, channel=self.axil_master.write_address, is_address_channel=True) + + # write_data + self.state_machine_request( + m, + self.wd_request, + channel=self.axil_master.write_data, + request_signal=wd_request_ready, + ) + + @def_method(m, self.wd_request, ready=wd_request_ready) + def _(arg): + self.start_request_transaction(m, arg, channel=self.axil_master.write_data, is_address_channel=False) + + # write_response + self.result_handler(m, wr_forwarder, data=False, channel=self.axil_master.write_response) + + @def_method(m, self.wr_response) + def _(): + return wr_forwarder.read(m) + + return m diff --git a/test/peripherals/test_axi_lite.py b/test/peripherals/test_axi_lite.py new file mode 100644 index 000000000..9d887ce9f --- /dev/null +++ b/test/peripherals/test_axi_lite.py @@ -0,0 +1,263 @@ +from coreblocks.peripherals.axi_lite import * +from transactron import Method, def_method, TModule +from transactron.lib import AdapterTrans + +from ..common import * + + +class AXILiteInterfaceWrapper: + def __init__(self, axi_lite_master: Record): + self.axi_lite = axi_lite_master + + def slave_ra_ready(self, rdy=1): + yield self.axi_lite.read_address.rdy.eq(rdy) + + def slave_ra_wait(self): + while not (yield self.axi_lite.read_address.valid): + yield + + def slave_ra_verify(self, exp_addr, prot): + assert (yield self.axi_lite.read_address.valid) + assert (yield self.axi_lite.read_address.addr) == exp_addr + assert (yield self.axi_lite.read_address.prot) == prot + + def slave_rd_wait(self): + while not (yield self.axi_lite.read_data.rdy): + yield + + def slave_rd_respond(self, data, resp=0): + assert (yield self.axi_lite.read_data.rdy) + yield self.axi_lite.read_data.data.eq(data) + yield self.axi_lite.read_data.resp.eq(resp) + yield self.axi_lite.read_data.valid.eq(1) + yield + yield self.axi_lite.read_data.valid.eq(0) + + def slave_wa_ready(self, rdy=1): + yield self.axi_lite.write_address.rdy.eq(rdy) + + def slave_wa_wait(self): + while not (yield self.axi_lite.write_address.valid): + yield + + def slave_wa_verify(self, exp_addr, prot): + assert (yield self.axi_lite.write_address.valid) + assert (yield self.axi_lite.write_address.addr) == exp_addr + assert (yield self.axi_lite.write_address.prot) == prot + + def slave_wd_ready(self, rdy=1): + yield self.axi_lite.write_data.rdy.eq(rdy) + + def slave_wd_wait(self): + while not (yield self.axi_lite.write_data.valid): + yield + + def slave_wd_verify(self, exp_data, strb): + assert (yield self.axi_lite.write_data.valid) + assert (yield self.axi_lite.write_data.data) == exp_data + assert (yield self.axi_lite.write_data.strb) == strb + + def slave_wr_wait(self): + while not (yield self.axi_lite.write_response.rdy): + yield + + def slave_wr_respond(self, resp=0): + assert (yield self.axi_lite.write_response.rdy) + yield self.axi_lite.write_response.resp.eq(resp) + yield self.axi_lite.write_response.valid.eq(1) + yield + yield self.axi_lite.write_response.valid.eq(0) + + +class TestAXILiteMaster(TestCaseWithSimulator): + class AXILiteMasterTestModule(Elaboratable): + def __init__(self, params: AXILiteParameters): + self.params = params + self.write_request_layout = [ + ("addr", self.params.addr_width), + ("prot", 3), + ("data", self.params.data_width), + ("strb", self.params.data_width // 8), + ] + + self.write_request = Method(i=self.write_request_layout) + + def elaborate(self, platform): + m = TModule() + m.submodules.alm = alm = self.axi_lite_master = AXILiteMaster(self.params) + m.submodules.rar = self.read_address_request_adapter = TestbenchIO(AdapterTrans(alm.ra_request)) + m.submodules.rdr = self.read_data_response_adapter = TestbenchIO(AdapterTrans(alm.rd_response)) + m.submodules.war = self.write_address_request_adapter = TestbenchIO(AdapterTrans(alm.wa_request)) + m.submodules.wdr = self.write_data_request_adapter = TestbenchIO(AdapterTrans(alm.wd_request)) + m.submodules.wrr = self.write_response_response_adapter = TestbenchIO(AdapterTrans(alm.wr_response)) + + @def_method(m, self.write_request, ready=alm.wa_request.ready & alm.wd_request.ready) + def _(arg): + alm.wa_request(m, addr=arg.addr, prot=arg.prot) + alm.wd_request(m, data=arg.data, strb=arg.strb) + + m.submodules.wr = self.write_request_adapter = TestbenchIO(AdapterTrans(self.write_request)) + + return m + + def test_manual(self): + almt = TestAXILiteMaster.AXILiteMasterTestModule(AXILiteParameters()) + + def master_process(): + # read request + yield from almt.read_address_request_adapter.call(addr=5, prot=0) + + yield from almt.read_address_request_adapter.call(addr=10, prot=1) + + yield from almt.read_address_request_adapter.call(addr=15, prot=1) + + yield from almt.read_address_request_adapter.call(addr=20, prot=0) + + yield from almt.write_request_adapter.call(addr=6, prot=0, data=10, strb=3) + + yield from almt.write_request_adapter.call(addr=7, prot=0, data=11, strb=3) + + yield from almt.write_request_adapter.call(addr=8, prot=0, data=12, strb=3) + + yield from almt.write_request_adapter.call(addr=9, prot=1, data=13, strb=4) + + yield from almt.read_address_request_adapter.call(addr=1, prot=1) + + yield from almt.read_address_request_adapter.call(addr=2, prot=1) + + def slave_process(): + slave = AXILiteInterfaceWrapper(almt.axi_lite_master.axil_master) + + # 1st request + yield from slave.slave_ra_ready(1) + yield from slave.slave_ra_wait() + yield from slave.slave_ra_verify(5, 0) + yield Settle() + + # 2nd request and 1st respond + yield from slave.slave_ra_wait() + yield from slave.slave_rd_wait() + yield from slave.slave_ra_verify(10, 1) + yield from slave.slave_rd_respond(10, 0) + yield Settle() + + # 3rd request and 2nd respond + yield from slave.slave_ra_wait() + yield from slave.slave_rd_wait() + yield from slave.slave_ra_verify(15, 1) + yield from slave.slave_rd_respond(15, 0) + yield Settle() + + # 4th request and 3rd respond + yield from slave.slave_ra_wait() + yield from slave.slave_rd_wait() + yield from slave.slave_ra_verify(20, 0) + yield from slave.slave_rd_respond(20, 0) + yield Settle() + + # 4th respond and 1st write request + yield from slave.slave_ra_ready(0) + yield from slave.slave_wa_ready(1) + yield from slave.slave_wd_ready(1) + yield from slave.slave_rd_wait() + yield from slave.slave_wa_wait() + yield from slave.slave_wd_wait() + yield from slave.slave_wa_verify(6, 0) + yield from slave.slave_wd_verify(10, 3) + yield from slave.slave_rd_respond(25, 0) + yield Settle() + + # 2nd write request and 1st respond + yield from slave.slave_wa_wait() + yield from slave.slave_wd_wait() + yield from slave.slave_wr_wait() + yield from slave.slave_wa_verify(7, 0) + yield from slave.slave_wd_verify(11, 3) + yield from slave.slave_wr_respond(1) + yield Settle() + + # 3nd write request and 2st respond + yield from slave.slave_wa_wait() + yield from slave.slave_wd_wait() + yield from slave.slave_wr_wait() + yield from slave.slave_wa_verify(8, 0) + yield from slave.slave_wd_verify(12, 3) + yield from slave.slave_wr_respond(1) + yield Settle() + + # 4th write request and 3rd respond + yield from slave.slave_wr_wait() + yield from slave.slave_wa_verify(9, 1) + yield from slave.slave_wd_verify(13, 4) + yield from slave.slave_wr_respond(1) + yield Settle() + + # 4th respond + yield from slave.slave_wa_ready(0) + yield from slave.slave_wd_ready(0) + yield from slave.slave_wr_wait() + yield from slave.slave_wr_respond(0) + yield Settle() + + yield from slave.slave_ra_wait() + for _ in range(2): + yield + yield from slave.slave_ra_ready(1) + yield from slave.slave_ra_verify(1, 1) + # wait for next rising edge + yield + yield + + yield from slave.slave_ra_wait() + yield from slave.slave_ra_verify(2, 1) + yield from slave.slave_rd_wait() + yield from slave.slave_rd_respond(3, 1) + yield Settle() + + yield from slave.slave_rd_wait() + yield from slave.slave_rd_respond(4, 1) + + def result_process(): + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 10) + self.assertEqual(resp["resp"], 0) + + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 15) + self.assertEqual(resp["resp"], 0) + + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 20) + self.assertEqual(resp["resp"], 0) + + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 25) + self.assertEqual(resp["resp"], 0) + + resp = yield from almt.write_response_response_adapter.call() + self.assertEqual(resp["resp"], 1) + + resp = yield from almt.write_response_response_adapter.call() + self.assertEqual(resp["resp"], 1) + + resp = yield from almt.write_response_response_adapter.call() + self.assertEqual(resp["resp"], 1) + + resp = yield from almt.write_response_response_adapter.call() + self.assertEqual(resp["resp"], 0) + + for _ in range(5): + yield + + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 3) + self.assertEqual(resp["resp"], 1) + + resp = yield from almt.read_data_response_adapter.call() + self.assertEqual(resp["data"], 4) + self.assertEqual(resp["resp"], 1) + + with self.run_simulation(almt) as sim: + sim.add_sync_process(master_process) + sim.add_sync_process(slave_process) + sim.add_sync_process(result_process) From 34073708ce2b03cf66bb3c5ca709f6f592ccdb0b Mon Sep 17 00:00:00 2001 From: kindlermikolaj <62593571+kindlermikolaj@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:46:08 +0100 Subject: [PATCH 25/25] fixed autodoc scanning of lsu module (#546) --- coreblocks/lsu/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 coreblocks/lsu/__init__.py diff --git a/coreblocks/lsu/__init__.py b/coreblocks/lsu/__init__.py new file mode 100644 index 000000000..e69de29bb