diff --git a/docs/limitations.rst b/docs/limitations.rst index 3b888e8..bdd97b3 100644 --- a/docs/limitations.rst +++ b/docs/limitations.rst @@ -18,18 +18,41 @@ Registers instantiated using the ``alias`` keyword are not supported yet. Unaligned Registers ------------------- -All address offsets & strides shall be a multiple of the accesswidth used. Specifically: +All address offsets & strides shall be a multiple of the cpuif bus width used. Specifically: -* Each register's address and array stride shall be aligned to it's accesswidth. -* Each regfile or addrmap shall use an offset and stride that is a multiple of the largest accesswidth it encloses. +* Bus width is inferred by the maximum accesswidth used in the regblock. +* Each component's address and array stride shall be aligned to the bus width. -Register width, Access width and CPUIF bus width ------------------------------------------------- -To keep the initial architecture simpler, currently ``regwidth``, ``accesswidth`` -and the resulting CPU bus width has some limitations: +Uniform accesswidth +------------------- +All registers within a register block shall use the same accesswidth. -* All registers shall have ``regwidth`` == ``accesswidth`` -* ``regwidth`` shall be the same across all registers within the block being exported. +One exception is that registers with regwidth that is narrower than the cpuif +bus width are permitted, provided that their regwidth is equal to their accesswidth. -I have plans to remove these restrictions and allow for more flexibility in the future. +For example: + +.. code-block:: systemrdl + + // (Largest accesswidth used is 32, therefore the CPUIF bus width is 32) + + reg { + regwidth = 32; + accesswidth = 32; + } reg_a @ 0x00; // OK. Regular 32-bit register + + reg { + regwidth = 64; + accesswidth = 32; + } reg_b @ 0x08; // OK. "Wide" register of 64-bits, but is accessed using 32-bit subwords + + reg { + regwidth = 8; + accesswidth = 8; + } reg_c @ 0x10; // OK. Is aligned to the cpuif bus width + + reg { + regwidth = 32; + accesswidth = 8; + } bad_reg @ 0x14; // NOT OK. accesswidth conflicts with cpuif width diff --git a/src/peakrdl_regblock/addr_decode.py b/src/peakrdl_regblock/addr_decode.py index 3dfaf25..79eb656 100644 --- a/src/peakrdl_regblock/addr_decode.py +++ b/src/peakrdl_regblock/addr_decode.py @@ -30,21 +30,47 @@ class AddressDecode: assert s is not None return s - def get_access_strobe(self, node: Union[RegNode, FieldNode]) -> str: + def get_access_strobe(self, node: Union[RegNode, FieldNode], reduce_substrobes: bool=True) -> str: """ Returns the Verilog string that represents the register/field's access strobe. """ if isinstance(node, FieldNode): - node = node.parent + field = node + path = get_indexed_path(self.top_node, node.parent) + + regwidth = node.parent.get_property('regwidth') + accesswidth = node.parent.get_property('accesswidth') + if regwidth > accesswidth: + # Is wide register. + # Determine the substrobe(s) relevant to this field + sidx_hi = field.msb // accesswidth + sidx_lo = field.lsb // accesswidth + if sidx_hi == sidx_lo: + suffix = f"[{sidx_lo}]" + else: + suffix = f"[{sidx_hi}:{sidx_lo}]" + path += suffix + + if sidx_hi != sidx_lo and reduce_substrobes: + return "|decoded_reg_strb." + path + + else: + path = get_indexed_path(self.top_node, node) - path = get_indexed_path(self.top_node, node) return "decoded_reg_strb." + path class DecodeStructGenerator(RDLStructGenerator): def enter_Reg(self, node: 'RegNode') -> None: - self.add_member(kwf(node.inst_name), array_dimensions=node.array_dimensions) + # if register is "wide", expand the strobe to be able to access the sub-words + n_subwords = node.get_property("regwidth") // node.get_property("accesswidth") + + self.add_member( + kwf(node.inst_name), + width=n_subwords, + array_dimensions=node.array_dimensions, + ) # Stub out def exit_Reg(self, node: 'RegNode') -> None: @@ -79,16 +105,26 @@ class DecodeLogicGenerator(RDLForLoopGenerator): self._array_stride_stack.extend(strides) - def _get_address_str(self, node:AddressableNode) -> str: - a = f"'h{(node.raw_absolute_address - self.addr_decode.top_node.raw_absolute_address):x}" + def _get_address_str(self, node:AddressableNode, subword_offset: int=0) -> str: + a = f"'h{(node.raw_absolute_address - self.addr_decode.top_node.raw_absolute_address + subword_offset):x}" for i, stride in enumerate(self._array_stride_stack): a += f" + i{i}*'h{stride:x}" return a def enter_Reg(self, node: RegNode) -> None: - s = f"{self.addr_decode.get_access_strobe(node)} = cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)});" - self.add_content(s) + regwidth = node.get_property('regwidth') + accesswidth = node.get_property('accesswidth') + + if regwidth == accesswidth: + s = f"{self.addr_decode.get_access_strobe(node)} = cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)});" + self.add_content(s) + else: + n_subwords = regwidth // accesswidth + subword_stride = accesswidth // 8 + for i in range(n_subwords): + s = f"{self.addr_decode.get_access_strobe(node)}[{i}] = cpuif_req_masked & (cpuif_addr == {self._get_address_str(node, subword_offset=(i*subword_stride))});" + self.add_content(s) def exit_AddressableComponent(self, node: 'AddressableNode') -> None: diff --git a/src/peakrdl_regblock/dereferencer.py b/src/peakrdl_regblock/dereferencer.py index b50488d..5a464bc 100644 --- a/src/peakrdl_regblock/dereferencer.py +++ b/src/peakrdl_regblock/dereferencer.py @@ -195,11 +195,11 @@ class Dereferencer: raise NotImplementedError - def get_access_strobe(self, obj: Union[RegNode, FieldNode]) -> str: + def get_access_strobe(self, obj: Union[RegNode, FieldNode], reduce_substrobes: bool=True) -> str: """ Returns the Verilog string that represents the register's access strobe """ - return self.address_decode.get_access_strobe(obj) + return self.address_decode.get_access_strobe(obj, reduce_substrobes) def get_resetsignal(self, obj: Optional[SignalNode]) -> str: """ diff --git a/src/peakrdl_regblock/exporter.py b/src/peakrdl_regblock/exporter.py index 12489fd..af05bed 100644 --- a/src/peakrdl_regblock/exporter.py +++ b/src/peakrdl_regblock/exporter.py @@ -15,6 +15,7 @@ from .cpuif.apb4 import APB4_Cpuif from .hwif import Hwif from .utils import get_always_ff_event from .scan_design import DesignScanner +from .validate_design import DesignValidator class RegblockExporter: def __init__(self, **kwargs: Any) -> None: @@ -120,18 +121,17 @@ class RegblockExporter: if retime_read_response: self.min_read_latency += 1 - # Scan the design for any unsupported features - # Also collect pre-export information + # Scan the design for pre-export information scanner = DesignScanner(self) scanner.do_scan() + # Construct exporter components self.cpuif = cpuif_cls( self, cpuif_reset=self.top_node.cpuif_reset, data_width=scanner.cpuif_data_width, addr_width=self.top_node.size.bit_length() ) - self.hwif = Hwif( self, package_name=package_name, @@ -139,12 +139,15 @@ class RegblockExporter: out_of_hier_signals=scanner.out_of_hier_signals, reuse_typedefs=reuse_hwif_typedefs, ) - self.readback = Readback( self, retime_read_fanin ) + # Validate that there are no unsupported constructs + validator = DesignValidator(self) + validator.do_validate() + # Build Jinja template context context = { "module_name": module_name, diff --git a/src/peakrdl_regblock/field_logic/sw_onwrite.py b/src/peakrdl_regblock/field_logic/sw_onwrite.py index 0d2c6c9..a8038cd 100644 --- a/src/peakrdl_regblock/field_logic/sw_onwrite.py +++ b/src/peakrdl_regblock/field_logic/sw_onwrite.py @@ -24,101 +24,107 @@ class _OnWrite(NextStateConditional): return f"{strb} && decoded_req_is_wr" + def _wbus_bitslice(self, field: 'FieldNode', subword_idx: int) -> str: + # Get the source bitslice range from the internal cpuif's data bus + # For normal fields this ends up passing-through the field's low/high + # values unchanged. + # For fields within a wide register (accesswidth < regwidth), low/high + # may be shifted down and clamped depending on which sub-word is being accessed + accesswidth = field.parent.get_property('accesswidth') + + # Shift based on subword + high = field.high - (subword_idx * accesswidth) + low = field.low - (subword_idx * accesswidth) + + # clamp to accesswidth + high = max(min(high, accesswidth), 0) + low = max(min(low, accesswidth), 0) + + return f"[{high}:{low}]" + + def _wr_data(self, field: 'FieldNode', subword_idx: int=0) -> str: + bslice = self._wbus_bitslice(field, subword_idx) - def _wr_data(self, field: 'FieldNode') -> str: if field.msb < field.lsb: # Field gets bitswapped since it is in [low:high] orientation - value = f"{{<<{{decoded_wr_data[{field.high}:{field.low}]}}}}" + value = f"{{<<{{decoded_wr_data{bslice}}}}}" else: - value = f"decoded_wr_data[{field.high}:{field.low}]" + value = f"decoded_wr_data{bslice}" return value - def _wr_biten(self, field: 'FieldNode') -> str: + def _wr_biten(self, field: 'FieldNode', subword_idx: int=0) -> str: + bslice = self._wbus_bitslice(field, subword_idx) + if field.msb < field.lsb: # Field gets bitswapped since it is in [low:high] orientation - value = f"{{<<{{decoded_wr_biten[{field.high}:{field.low}]}}}}" + value = f"{{<<{{decoded_wr_biten{bslice}}}}}" else: - value = f"decoded_wr_biten[{field.high}:{field.low}]" + value = f"decoded_wr_biten{bslice}" return value + def get_assignments(self, field: 'FieldNode') -> List[str]: + accesswidth = field.parent.get_property("accesswidth") + + # Due to 10.6.1-f, it is impossible for a field with an onwrite action to + # be split across subwords. + # Therefore it is ok to get the subword idx from only one of the bit offsets + sidx = field.low // accesswidth + + # field does not get split between subwords + R = self.exp.field_logic.get_storage_identifier(field) + D = self._wr_data(field, sidx) + S = self._wr_biten(field, sidx) + lines = [ + f"next_c = {self.get_onwrite_rhs(R, D, S)};", + "load_next_c = '1;", + ] + return lines + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + raise NotImplementedError + class WriteOneSet(_OnWrite): comment = "SW write 1 set" onwritetype = OnWriteType.woset - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} | ({D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} | ({data} & {strb})" class WriteOneClear(_OnWrite): comment = "SW write 1 clear" onwritetype = OnWriteType.woclr - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} & ~({D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} & ~({data} & {strb})" class WriteOneToggle(_OnWrite): comment = "SW write 1 toggle" onwritetype = OnWriteType.wot - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} ^ ({D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} ^ ({data} & {strb})" class WriteZeroSet(_OnWrite): comment = "SW write 0 set" onwritetype = OnWriteType.wzs - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} | (~{D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} | (~{data} & {strb})" class WriteZeroClear(_OnWrite): comment = "SW write 0 clear" onwritetype = OnWriteType.wzc - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} & ({D} | ~{S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} & ({data} | ~{strb})" class WriteZeroToggle(_OnWrite): comment = "SW write 0 toggle" onwritetype = OnWriteType.wzt - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = {R} ^ (~{D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} ^ (~{data} & {strb})" class WriteClear(_OnWrite): comment = "SW write clear" @@ -144,11 +150,5 @@ class Write(_OnWrite): comment = "SW write" onwritetype = None - def get_assignments(self, field: 'FieldNode') -> List[str]: - R = self.exp.field_logic.get_storage_identifier(field) - D = self._wr_data(field) - S = self._wr_biten(field) - return [ - f"next_c = ({R} & ~{S}) | ({D} & {S});", - "load_next_c = '1;", - ] + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"({reg} & ~{strb}) | ({data} & {strb})" diff --git a/src/peakrdl_regblock/readback/generators.py b/src/peakrdl_regblock/readback/generators.py index cd4ec5a..2ca0542 100644 --- a/src/peakrdl_regblock/readback/generators.py +++ b/src/peakrdl_regblock/readback/generators.py @@ -57,37 +57,6 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator): offset_parts.append(str(self.current_offset)) return " + ".join(offset_parts) - def enter_Reg(self, node: 'RegNode') -> None: - # TODO: account for smaller regs that are not aligned to the bus width - # - offset the field bit slice as appropriate - # - do not always increment the current offset - if node.has_sw_readable: - current_bit = 0 - rd_strb = f"({self.exp.dereferencer.get_access_strobe(node)} && !decoded_req_is_wr)" - # Fields are sorted by ascending low bit - for field in node.fields(): - if field.is_sw_readable: - # insert reserved assignment before if needed - if field.low != current_bit: - self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low-1}:{current_bit}] = '0;") - - if field.msb < field.lsb: - # Field gets bitswapped since it is in [low:high] orientation - value = f"{{<<{{{self.exp.dereferencer.get_value(field)}}}}}" - else: - value = self.exp.dereferencer.get_value(field) - - self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;") - - current_bit = field.high + 1 - - # Insert final reserved assignment if needed - bus_width = self.exp.cpuif.data_width - if current_bit < bus_width: - self.add_content(f"assign readback_array[{self.current_offset_str}][{bus_width-1}:{current_bit}] = '0;") - - self.current_offset += 1 - def push_loop(self, dim: int) -> None: super().push_loop(dim) self.start_offset_stack.append(self.current_offset) @@ -105,3 +74,173 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator): # Advance current scope's offset to account for loop's contents self.current_offset = start_offset + n_regs * dim + + + def enter_Reg(self, node: 'RegNode') -> None: + if not node.has_sw_readable: + return + + accesswidth = node.get_property('accesswidth') + regwidth = node.get_property('regwidth') + if accesswidth < regwidth: + self.process_wide_reg(node, accesswidth) + else: + self.process_reg(node) + + + def process_reg(self, node: 'RegNode') -> None: + current_bit = 0 + rd_strb = f"({self.exp.dereferencer.get_access_strobe(node)} && !decoded_req_is_wr)" + # Fields are sorted by ascending low bit + for field in node.fields(): + if not field.is_sw_readable: + continue + + # insert reserved assignment before this field if needed + if field.low != current_bit: + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low-1}:{current_bit}] = '0;") + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = f"{{<<{{{self.exp.dereferencer.get_value(field)}}}}}" + else: + value = self.exp.dereferencer.get_value(field) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + + # Insert final reserved assignment if needed + bus_width = self.exp.cpuif.data_width + if current_bit < bus_width: + self.add_content(f"assign readback_array[{self.current_offset_str}][{bus_width-1}:{current_bit}] = '0;") + + self.current_offset += 1 + + + def process_wide_reg(self, node: 'RegNode', accesswidth: int) -> None: + bus_width = self.exp.cpuif.data_width + + subword_idx = 0 + current_bit = 0 # Bit-offset within the wide register + access_strb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False) + # Fields are sorted by ascending low bit + for field in node.fields(): + if not field.is_sw_readable: + continue + + # insert zero assignment before this field if needed + if field.low >= accesswidth*(subword_idx+1): + # field does not start in this subword + if current_bit > accesswidth * subword_idx: + # current subword had content. Assign remainder + low = current_bit % accesswidth + high = bus_width - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + self.current_offset += 1 + + # Advance to subword that contains the start of the field + subword_idx = field.low // accesswidth + current_bit = accesswidth * subword_idx + + if current_bit != field.low: + # assign zero up to start of this field + low = current_bit % accesswidth + high = (field.low % accesswidth) - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + current_bit = field.low + + + # Assign field + # loop until the entire field's assignments have been generated + field_pos = field.low + while current_bit <= field.high: + # Assign the field + rd_strb = f"({access_strb}[{subword_idx}] && !decoded_req_is_wr)" + if (field_pos == field.low) and (field.high < accesswidth*(subword_idx+1)): + # entire field fits into this subword + low = field.low - accesswidth * subword_idx + high = field.high - accesswidth * subword_idx + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = f"{{<<{{{self.exp.dereferencer.get_value(field)}}}}}" + else: + value = self.exp.dereferencer.get_value(field) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + + if current_bit == accesswidth*(subword_idx+1): + # Field ends at the subword boundary + subword_idx += 1 + self.current_offset += 1 + elif field.high >= accesswidth*(subword_idx+1): + # only a subset of the field can fit into this subword + # high end gets truncated + + # assignment slice + r_low = field_pos - accesswidth * subword_idx + r_high = accesswidth - 1 + + # field slice + f_low = field_pos - field.low + f_high = accesswidth * (subword_idx + 1) - 1 - field.low + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + # Mirror the low/high indexes + f_low = field.width - 1 - f_low + f_high = field.width - 1 - f_high + f_low, f_high = f_high, f_low + + value = f"{{<<{{{self.exp.dereferencer.get_value(field)}[{f_high}:{f_low}]}}}}" + else: + value = self.exp.dereferencer.get_value(field) + f"[{f_high}:{f_low}]" + + self.add_content(f"assign readback_array[{self.current_offset_str}][{r_high}:{r_low}] = {rd_strb} ? {value} : '0;") + + # advance to the next subword + subword_idx += 1 + current_bit = accesswidth * subword_idx + field_pos = current_bit + self.current_offset += 1 + else: + # only a subset of the field can fit into this subword + # finish field + + # assignment slice + r_low = field_pos - accesswidth * subword_idx + r_high = field.high - accesswidth * subword_idx + + # field slice + f_low = field_pos - field.low + f_high = field.high - field.low + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + # Mirror the low/high indexes + f_low = field.width - 1 - f_low + f_high = field.width - 1 - f_high + f_low, f_high = f_high, f_low + + value = f"{{<<{{{self.exp.dereferencer.get_value(field)}[{f_high}:{f_low}]}}}}" + else: + value = self.exp.dereferencer.get_value(field) + f"[{f_high}:{f_low}]" + + self.add_content(f"assign readback_array[{self.current_offset_str}][{r_high}:{r_low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + if current_bit == accesswidth*(subword_idx+1): + # Field ends at the subword boundary + subword_idx += 1 + self.current_offset += 1 + + # insert zero assignment after the last field if needed + if current_bit > accesswidth * subword_idx: + # current subword had content. Assign remainder + low = current_bit % accesswidth + high = bus_width - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + self.current_offset += 1 diff --git a/src/peakrdl_regblock/scan_design.py b/src/peakrdl_regblock/scan_design.py index 047453a..580c547 100644 --- a/src/peakrdl_regblock/scan_design.py +++ b/src/peakrdl_regblock/scan_design.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING, Set, List, Optional +from typing import TYPE_CHECKING, Set, Optional from collections import OrderedDict from systemrdl.walker import RDLListener, RDLWalker, WalkerAction -from systemrdl.node import SignalNode, AddressableNode +from systemrdl.node import SignalNode if TYPE_CHECKING: from systemrdl.node import Node, RegNode, FieldNode @@ -21,9 +21,6 @@ class DesignScanner(RDLListener): self.cpuif_data_width = 0 self.msg = exp.top_node.env.msg - # Keep track of max accesswidth encountered in a given block - self.max_accesswidth_stack = [] # type: List[int] - # Collections of signals that were actually referenced by the design self.in_hier_signal_paths = set() # type: Set[str] self.out_of_hier_signals = OrderedDict() # type: OrderedDict[str, SignalNode] @@ -65,73 +62,19 @@ class DesignScanner(RDLListener): self.msg.fatal( "Unable to export due to previous errors" ) - raise ValueError - - def enter_Reg(self, node: 'RegNode') -> None: - accesswidth = node.get_property('accesswidth') - - self.max_accesswidth_stack[-1] = max(self.max_accesswidth_stack[-1], accesswidth) - - # The CPUIF's bus width is sized according to the largest accesswidth in the design - self.cpuif_data_width = max(self.cpuif_data_width, accesswidth) - - # TODO: remove this limitation eventually - if accesswidth != self.cpuif_data_width: - self.msg.error( - "register blocks with non-uniform accesswidth are not supported yet", - node.inst.property_src_ref.get('accesswidth', node.inst.inst_src_ref) - ) - - # TODO: remove this limitation eventually - if accesswidth != node.get_property('regwidth'): - self.msg.error( - "Registers that have an accesswidth different from the register width are not supported yet", - node.inst.property_src_ref.get('accesswidth', node.inst.inst_src_ref) - ) - - def enter_AddressableComponent(self, node: AddressableNode) -> None: - self.max_accesswidth_stack.append(0) - - def exit_AddressableComponent(self, node: AddressableNode) -> None: - max_block_accesswidth = self.max_accesswidth_stack.pop() - if self.max_accesswidth_stack: - self.max_accesswidth_stack[-1] = max(self.max_accesswidth_stack[-1], max_block_accesswidth) - - alignment = int(max_block_accesswidth / 8) - if (node.raw_address_offset % alignment) != 0: - self.msg.error( - f"Unaligned registers are not supported. Address offset of instance '{node.inst_name}' must be a multiple of {alignment}", - node.inst.inst_src_ref - ) - - if node.is_array and (node.array_stride % alignment) != 0: - self.msg.error( - f"Unaligned registers are not supported. Address stride of instance array '{node.inst_name}' must be a multiple of {alignment}", - node.inst.inst_src_ref - ) def enter_Component(self, node: 'Node') -> Optional[WalkerAction]: if node.external and (node != self.exp.top_node): - self.msg.error( - "Exporter does not support external components", - node.inst.inst_src_ref - ) # Do not inspect external components. None of my business return WalkerAction.SkipDescendants return None - def enter_Signal(self, node: 'SignalNode') -> None: - # If encountering a CPUIF reset that is nested within the register model, - # warn that it will be ignored. - # Only cpuif resets in the top-level node or above will be honored - if node.get_property('cpuif_reset') and (node.parent != self.exp.top_node): - self.msg.warning( - "Only cpuif_reset signals that are instantiated in the top-level " - + "addrmap or above will be honored. Any cpuif_reset signals nested " - + "within children of the addrmap being exported will be ignored.", - node.inst.inst_src_ref - ) + def enter_Reg(self, node: 'RegNode') -> None: + # The CPUIF's bus width is sized according to the largest accesswidth in the design + accesswidth = node.get_property('accesswidth') + self.cpuif_data_width = max(self.cpuif_data_width, accesswidth) + def enter_Signal(self, node: 'SignalNode') -> None: if node.get_property('field_reset'): path = node.get_path() self.in_hier_signal_paths.add(path) @@ -146,25 +89,3 @@ class DesignScanner(RDLListener): self.out_of_hier_signals[path] = value else: self.in_hier_signal_paths.add(path) - - - # 10.6.1-f: Any field that is software-writable or clear on read shall - # not span multiple software accessible sub-words (e.g., a 64-bit - # register with a 32-bit access width may not have a writable field with - # bits in both the upper and lower half of the register). - # - # Interpreting this further - this rule applies any time a field is - # software-modifiable by any means, including rclr, rset, ruser - # TODO: suppress this check for registers that have the appropriate - # buffer_writes/buffer_reads UDP set - parent_accesswidth = node.parent.get_property('accesswidth') - parent_regwidth = node.parent.get_property('regwidth') - if ((parent_accesswidth < parent_regwidth) - and (node.lsb // parent_accesswidth) != (node.msb // parent_accesswidth) - and (node.is_sw_writable or node.get_property('onread') is not None)): - # Field spans across sub-words - self.msg.error( - "Software-modifiable field '%s' shall not span multiple software-accessible subwords." - % node.inst_name, - node.inst.inst_src_ref - ) diff --git a/src/peakrdl_regblock/validate_design.py b/src/peakrdl_regblock/validate_design.py new file mode 100644 index 0000000..377d585 --- /dev/null +++ b/src/peakrdl_regblock/validate_design.py @@ -0,0 +1,99 @@ +from typing import TYPE_CHECKING, Optional + +from systemrdl.walker import RDLListener, RDLWalker, WalkerAction + +if TYPE_CHECKING: + from systemrdl.node import Node, RegNode, FieldNode, SignalNode, AddressableNode + from .exporter import RegblockExporter + +class DesignValidator(RDLListener): + """ + Performs additional rule-checks on the design that check for limitations + imposed by this exporter. + """ + def __init__(self, exp:'RegblockExporter') -> None: + self.exp = exp + self.msg = exp.top_node.env.msg + + def do_validate(self) -> None: + RDLWalker().walk(self.exp.top_node, self) + if self.msg.had_error: + self.msg.fatal( + "Unable to export due to previous errors" + ) + + def enter_Component(self, node: 'Node') -> Optional[WalkerAction]: + if node.external and (node != self.exp.top_node): + self.msg.error( + "Exporter does not support external components", + node.inst.inst_src_ref + ) + # Do not inspect external components. None of my business + return WalkerAction.SkipDescendants + return None + + def enter_Signal(self, node: 'SignalNode') -> None: + # If encountering a CPUIF reset that is nested within the register model, + # warn that it will be ignored. + # Only cpuif resets in the top-level node or above will be honored + if node.get_property('cpuif_reset') and (node.parent != self.exp.top_node): + self.msg.warning( + "Only cpuif_reset signals that are instantiated in the top-level " + "addrmap or above will be honored. Any cpuif_reset signals nested " + "within children of the addrmap being exported will be ignored.", + node.inst.inst_src_ref + ) + + def enter_AddressableComponent(self, node: 'AddressableNode') -> None: + # All registers must be aligned to the internal data bus width + alignment = self.exp.cpuif.data_width_bytes + if (node.raw_address_offset % alignment) != 0: + self.msg.error( + "Unaligned registers are not supported. Address offset of " + f"instance '{node.inst_name}' must be a multiple of {alignment}", + node.inst.inst_src_ref + ) + if node.is_array and (node.array_stride % alignment) != 0: + self.msg.error( + "Unaligned registers are not supported. Address stride of " + f"instance array '{node.inst_name}' must be a multiple of {alignment}", + node.inst.inst_src_ref + ) + + def enter_Reg(self, node: 'RegNode') -> None: + # accesswidth of wide registers must be consistent within the register block + accesswidth = node.get_property('accesswidth') + regwidth = node.get_property('regwidth') + + if accesswidth < regwidth: + # register is 'wide' + if accesswidth != self.exp.cpuif.data_width: + self.msg.error( + f"Multi-word registers that have an accesswidth ({accesswidth}) " + "that is inconsistent with this regblock's CPU bus width " + f"({self.exp.cpuif.data_width}) are not supported.", + node.inst.inst_src_ref + ) + + + def enter_Field(self, node: 'FieldNode') -> None: + # 10.6.1-f: Any field that is software-writable or clear on read shall + # not span multiple software accessible sub-words (e.g., a 64-bit + # register with a 32-bit access width may not have a writable field with + # bits in both the upper and lower half of the register). + # + # Interpreting this further - this rule applies any time a field is + # software-modifiable by any means, including rclr, rset, ruser + # TODO: suppress this check for registers that have the appropriate + # buffer_writes/buffer_reads UDP set + parent_accesswidth = node.parent.get_property('accesswidth') + parent_regwidth = node.parent.get_property('regwidth') + if ((parent_accesswidth < parent_regwidth) + and (node.lsb // parent_accesswidth) != (node.msb // parent_accesswidth) + and (node.is_sw_writable or node.get_property('onread') is not None)): + # Field spans across sub-words + self.msg.error( + f"Software-modifiable field '{node.inst_name}' shall not span " + "multiple software-accessible subwords.", + node.inst.inst_src_ref + ) diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000..f664bca --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,18 @@ +[run] +branch = True +#relative_files = True + +[paths] +source = + ../src/peakrdl_regblock/ + */site-packages/*/peakrdl_regblock + */site-packages/peakrdl_regblock + +[report] +exclude_lines = + pragma: no cover + raise RuntimeError + raise NotImplementedError + if TYPE_CHECKING: + +precision = 1 diff --git a/tests/requirements.txt b/tests/requirements.txt index 35cae59..357f5bc 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,3 +4,5 @@ pytest-parallel jinja2-simple-tags pylint mypy +pytest-cov +coverage diff --git a/tests/run.sh b/tests/run.sh index 17df909..2852d94 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -15,12 +15,17 @@ pip install -r $this_dir/requirements.txt # Install dut cd $this_dir/../ -pip install . +python $this_dir/../setup.py install cd $this_dir # Run unit tests export SKIP_SYNTH_TESTS=1 -pytest --workers auto +#export STUB_SIMULATOR=1 +export NO_XSIM=1 +pytest --workers auto --cov=peakrdl_regblock + +# Generate coverage report +coverage html -i -d $this_dir/htmlcov # Run lint pylint --rcfile $this_dir/pylint.rc ../src/peakrdl_regblock diff --git a/tests/test_structural_sw_rw/regblock.rdl b/tests/test_structural_sw_rw/regblock.rdl index b3726f8..30488d1 100644 --- a/tests/test_structural_sw_rw/regblock.rdl +++ b/tests/test_structural_sw_rw/regblock.rdl @@ -31,4 +31,14 @@ addrmap regblock { }; subrf sub2[2] @ 0x2000 += 0x40; subreg r3 @ 0x2080; + + reg { + field {} f1[19:12] = 0; + field {} f2[30:20] = 0; + } rw_reg @ 0x3000; + + reg { + field {} f1[12:19] = 0; + field {} f2[20:30] = 0; + } rw_reg_lsb0 @ 0x3004; }; diff --git a/tests/test_structural_sw_rw/tb_template.sv b/tests/test_structural_sw_rw/tb_template.sv index c91144e..2df4cbb 100644 --- a/tests/test_structural_sw_rw/tb_template.sv +++ b/tests/test_structural_sw_rw/tb_template.sv @@ -61,4 +61,20 @@ assert(cb.hwif_out.r2.a.value == 'h0); assert(cb.hwif_out.r2.b.value == 'h0); assert(cb.hwif_out.r2.c.value == 'h0); + + // rw_reg + cpuif.assert_read('h3000, 0); + cpuif.write('h3000, 'h4DEAB000); + @cb; + assert(cb.hwif_out.rw_reg.f1.value == 8'hAB); + assert(cb.hwif_out.rw_reg.f2.value == 11'h4DE); + cpuif.assert_read('h3000, 'h4DEAB000); + + // rw_reg_lsb0 + cpuif.assert_read('h3004, 0); + cpuif.write('h3004, 'h4DEAB000); + @cb; + assert({<<{cb.hwif_out.rw_reg_lsb0.f1.value}} == 8'hAB); + assert({<<{cb.hwif_out.rw_reg_lsb0.f2.value}} == 11'h4DE); + cpuif.assert_read('h3004, 'h4DEAB000); {% endblock %} diff --git a/tests/test_structural_sw_rw/testcase.py b/tests/test_structural_sw_rw/testcase.py index c7315bf..fa9db57 100644 --- a/tests/test_structural_sw_rw/testcase.py +++ b/tests/test_structural_sw_rw/testcase.py @@ -19,7 +19,7 @@ class TestSynth(SynthTestCase): self.run_synth() -@pytest.mark.skipif(os.environ.get("STUB_SIMULATOR", False), reason="user skipped") +@pytest.mark.skipif(os.environ.get("STUB_SIMULATOR", False) or os.environ.get("NO_XSIM", False), reason="user skipped") @parameterized_class(TEST_PARAMS) class TestVivado(SimTestCase): """ diff --git a/tests/test_wide_regs/__init__.py b/tests/test_wide_regs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_wide_regs/regblock.rdl b/tests/test_wide_regs/regblock.rdl new file mode 100644 index 0000000..32b54f0 --- /dev/null +++ b/tests/test_wide_regs/regblock.rdl @@ -0,0 +1,93 @@ +addrmap top { + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[7:0] = 0; + field {} f2[14:12] = 0; + field {} f3[36:36] = 0; + field {} f4[47:40] = 0; + } rw_reg1; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[19:16] = 0; + field {} f2[63:48] = 0; + } rw_reg2; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[0:7] = 0; + field {} f2[12:14] = 0; + field {} f3[36:36] = 0; + field {} f4[40:47] = 0; + } rw_reg1_lsb0; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[16:19] = 0; + field {} f2[48:63] = 0; + } rw_reg2_lsb0; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=w; + + field { + sw=w; hw=r; + } f0[3:3] = 0; + field {} f1[19:12]; + field {} f2[30:20]; + } r_reg; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=w; + + field {} f1[12:19]; + field {} f2[20:30]; + } r_reg_lsb0; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=r; + default hw=w; + + field {} f1[31:12]; + field {} f2[49:48]; + } r_reg2; + + reg { + regwidth=16; + field { + sw=r; hw=na; + counter; + } f1_cnt[7:0] = 0; + field { + sw=r; hw=na; + counter; + } f2_cnt[15:8] = 0; + } counter_reg; + counter_reg.f1_cnt->incr = r_reg2.f1->swacc; + counter_reg.f2_cnt->incr = r_reg2.f2->swacc; +}; diff --git a/tests/test_wide_regs/tb_template.sv b/tests/test_wide_regs/tb_template.sv new file mode 100644 index 0000000..001db27 --- /dev/null +++ b/tests/test_wide_regs/tb_template.sv @@ -0,0 +1,111 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // rw_reg1 + assert(cb.hwif_out.rw_reg1.f1.value == 0); + assert(cb.hwif_out.rw_reg1.f2.value == 0); + assert(cb.hwif_out.rw_reg1.f3.value == 0); + assert(cb.hwif_out.rw_reg1.f4.value == 0); + cpuif.write('h0, 'h1234); + cpuif.write('h2, 'h5678); + cpuif.write('h4, 'h9ABC); + cpuif.write('h6, 'hDEF1); + @cb; + assert(cb.hwif_out.rw_reg1.f1.value == 8'h34); + assert(cb.hwif_out.rw_reg1.f2.value == 3'h1); + assert(cb.hwif_out.rw_reg1.f3.value == 1'h1); + assert(cb.hwif_out.rw_reg1.f4.value == 8'h9A); + cpuif.assert_read('h0, 'h1034); + cpuif.assert_read('h2, 'h0000); + cpuif.assert_read('h4, 'h9A10); + cpuif.assert_read('h6, 'h0000); + + // rw_reg2 + assert(cb.hwif_out.rw_reg2.f1.value == 0); + assert(cb.hwif_out.rw_reg2.f2.value == 0); + cpuif.write('h8, 'h1234); + cpuif.write('hA, 'h5678); + cpuif.write('hC, 'h9ABC); + cpuif.write('hE, 'hDEF1); + @cb; + assert(cb.hwif_out.rw_reg2.f1.value == 4'h8); + assert(cb.hwif_out.rw_reg2.f2.value == 16'hDEF1); + cpuif.assert_read('h8, 'h0000); + cpuif.assert_read('hA, 'h0008); + cpuif.assert_read('hC, 'h0000); + cpuif.assert_read('hE, 'hDEF1); + + // rw_reg1_lsb0 + assert(cb.hwif_out.rw_reg1_lsb0.f1.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f2.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f3.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f4.value == 0); + cpuif.write('h10, 'h1234); + cpuif.write('h12, 'h5678); + cpuif.write('h14, 'h9ABC); + cpuif.write('h16, 'hDEF1); + @cb; + assert({<<{cb.hwif_out.rw_reg1_lsb0.f1.value}} == 8'h34); + assert({<<{cb.hwif_out.rw_reg1_lsb0.f2.value}} == 3'h1); + assert({<<{cb.hwif_out.rw_reg1_lsb0.f3.value}} == 1'h1); + assert({<<{cb.hwif_out.rw_reg1_lsb0.f4.value}} == 8'h9A); + cpuif.assert_read('h10, 'h1034); + cpuif.assert_read('h12, 'h0000); + cpuif.assert_read('h14, 'h9A10); + cpuif.assert_read('h16, 'h0000); + + // rw_reg2_lsb0 + assert(cb.hwif_out.rw_reg2_lsb0.f1.value == 0); + assert(cb.hwif_out.rw_reg2_lsb0.f2.value == 0); + cpuif.write('h18, 'h1234); + cpuif.write('h1A, 'h5678); + cpuif.write('h1C, 'h9ABC); + cpuif.write('h1E, 'hDEF1); + @cb; + assert({<<{cb.hwif_out.rw_reg2_lsb0.f1.value}} == 4'h8); + assert({<<{cb.hwif_out.rw_reg2_lsb0.f2.value}} == 16'hDEF1); + cpuif.assert_read('h18, 'h0000); + cpuif.assert_read('h1A, 'h0008); + cpuif.assert_read('h1C, 'h0000); + cpuif.assert_read('h1E, 'hDEF1); + + // r_reg + cpuif.assert_read('h20, 0); + cpuif.assert_read('h22, 0); + cb.hwif_in.r_reg.f1.next <= 8'hAB; + cb.hwif_in.r_reg.f2.next <= 11'h4DE; + @cb; + cpuif.assert_read('h20, 'hB000); + cpuif.assert_read('h22, 'h4DEA); + + // r_reg_lsb0 + cpuif.assert_read('h24, 0); + cpuif.assert_read('h26, 0); + cb.hwif_in.r_reg_lsb0.f1.next <= {<<{8'hAB}}; + cb.hwif_in.r_reg_lsb0.f2.next <= {<<{11'h4DE}}; + @cb; + cpuif.assert_read('h24, 'hB000); + cpuif.assert_read('h26, 'h4DEA); + + // r_reg2 + cpuif.assert_read('h28, 0); + cpuif.assert_read('h2a, 0); + cpuif.assert_read('h2c, 0); + cpuif.assert_read('h2e, 0); + cb.hwif_in.r_reg2.f1.next <= 20'hABCDE; + cb.hwif_in.r_reg2.f2.next <= 2'h3; + @cb; + cpuif.assert_read('h28, 'hE000); + cpuif.assert_read('h2a, 'hABCD); + cpuif.assert_read('h2c, 'h0000); + cpuif.assert_read('h2e, 'h0003); + + // counter_reg + cpuif.assert_read('h30, 16'h0204); + +{% endblock %} diff --git a/tests/test_wide_regs/testcase.py b/tests/test_wide_regs/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_wide_regs/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test()