Implement read buffering. (#22)
This commit is contained in:
@@ -121,4 +121,5 @@ Links
|
||||
:caption: Extended Properties
|
||||
|
||||
udps/intro
|
||||
udps/read_buffering
|
||||
udps/write_buffering
|
||||
|
||||
@@ -8,6 +8,8 @@ the language to be extended using "User Defined Properties" (UDPs). The
|
||||
PeakRDL-regblock tool understands several UDPs that are described in this
|
||||
section.
|
||||
|
||||
To enable these UDPs, compile this RDL file prior to the rest of your design:
|
||||
:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`.
|
||||
|
||||
.. list-table:: Summary of UDPs
|
||||
:header-rows: 1
|
||||
@@ -17,6 +19,20 @@ section.
|
||||
- Type
|
||||
- Description
|
||||
|
||||
* - buffer_reads
|
||||
- reg
|
||||
- boolean
|
||||
- If set, reads from the register are double-buffered.
|
||||
|
||||
See details here: :ref:`read_buffering`.
|
||||
|
||||
* - rbuffer_trigger
|
||||
- reg
|
||||
- reference
|
||||
- Defines the buffered read load trigger.
|
||||
|
||||
See details here: :ref:`read_buffering`.
|
||||
|
||||
* - buffer_writes
|
||||
- reg
|
||||
- boolean
|
||||
|
||||
158
docs/udps/read_buffering.rst
Normal file
158
docs/udps/read_buffering.rst
Normal file
@@ -0,0 +1,158 @@
|
||||
.. _read_buffering:
|
||||
|
||||
Read-buffered Registers
|
||||
=======================
|
||||
|
||||
Read buffering is a mechanism that allows for software accesses to read a
|
||||
snapshot of one or more registers atomically. When enabled on a register, a
|
||||
read buffer will latch the state of its fields when triggered such that software
|
||||
can read a coherent snapshot of one or more registers' value.
|
||||
|
||||
Some examples of when this is useful:
|
||||
* A wide 64-bit status register needs to be read atomically, but the CPU
|
||||
interface is only 32-bits.
|
||||
* Software needs to be able to read the state of multiple registers
|
||||
atomically.
|
||||
* A hardware event latches the software-visible state of one or more
|
||||
registers.
|
||||
|
||||
.. figure:: ../diagrams/rbuf.png
|
||||
|
||||
|
||||
Properties
|
||||
----------
|
||||
The behavior of read-buffered registers is defined using the following two
|
||||
properties:
|
||||
|
||||
.. literalinclude:: ../../hdl-src/regblock_udps.rdl
|
||||
:lines: 10-18
|
||||
|
||||
``buffer_reads``
|
||||
* Assigned value is a boolean.
|
||||
* If true, enables double-buffering of software reads of this register.
|
||||
* The read buffer will load the register's field values when it's trigger
|
||||
event is asserted.
|
||||
* Unless specified otherwise, the buffer trigger occurs when the lowest
|
||||
address of the buffered register is read.
|
||||
* When read by software the data returned is from the buffer contents, not
|
||||
directly from the register's fields.
|
||||
|
||||
``rbuffer_trigger``
|
||||
* Assigned value is a reference to a register, single-bit field, signal, or
|
||||
single-bit property.
|
||||
* Controls when the double-buffer loads the register's field vaues into the
|
||||
buffer storage element.
|
||||
* If reference is a single-bit value (signal, field, property reference),
|
||||
then the assertion of that value triggers the buffer to be evicted.
|
||||
* Signal references shall have either activehigh/activelow property set to
|
||||
define the polarity.
|
||||
* If the reference is a reg, then buffer is loaded when the register's
|
||||
lowest address is read.
|
||||
|
||||
Other Rules
|
||||
^^^^^^^^^^^
|
||||
* It is an error to set ``buffer_reads`` if the register does not contain any
|
||||
readable fields
|
||||
* If ``buffer_reads`` is false, then anything assigned to ``rbuffer_trigger``
|
||||
is ignored.
|
||||
* The buffered register and the trigger reference shall both be within the same
|
||||
internal device. ie: one cannot be in an external scope with respect to the
|
||||
other.
|
||||
* Unless it is a register, the reference assigned to ``rbuffer_trigger`` shall
|
||||
represent a single bit.
|
||||
* The software read operation considered to take place when the buffer is loaded
|
||||
This influences the behavior of properties like ``swmod`` and ``swacc`` -
|
||||
they are not asserted until the register's fields are actually sampled by the
|
||||
buffer.
|
||||
* If a read-buffered register is wide (accesswidth < regwidth) and is its own
|
||||
trigger, the first sub-word's buffer is bypassed to ensure the first read
|
||||
operation is atomically coherent with the rest of the sampled register.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
Below are several examples of what you can do with registers that are
|
||||
read-buffered.
|
||||
|
||||
Wide Atomic Register
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
In this example, a wide 64-bit read-clear counter is implemented.
|
||||
Without read-buffering, it is impossible to coherently read the state of the
|
||||
counter using a 32-bit CPU interface without risking a discontinuity. With
|
||||
read-buffering enabled, the read of the lower half of the register will trigger
|
||||
the upper half's value to be latched. A subsequent software access can then
|
||||
coherently read the rest of the register's buffered value.
|
||||
|
||||
.. code-block:: systemrdl
|
||||
:emphasize-lines: 4
|
||||
|
||||
reg {
|
||||
regwidth = 64;
|
||||
accesswidth = 32;
|
||||
buffer_reads = true;
|
||||
field {
|
||||
sw=r; hw=na;
|
||||
counter;
|
||||
incr;
|
||||
} my_counter[63:0] = 0;
|
||||
};
|
||||
|
||||
|
||||
Atomic Group of Registers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Perhaps you have a group of registers that monitor some rapidly-changing state
|
||||
within your design. Using the ``rbuffer_trigger`` property, you can define which
|
||||
reagister read operation triggers the buffered registers' values to be latched.
|
||||
|
||||
.. code-block:: systemrdl
|
||||
:emphasize-lines: 11-14
|
||||
|
||||
reg my_status_reg {
|
||||
field {
|
||||
sw=r; hw=w;
|
||||
} value[31:0];
|
||||
};
|
||||
|
||||
my_status_reg status1;
|
||||
my_status_reg status2;
|
||||
my_status_reg status3;
|
||||
|
||||
status2->buffer_reads = true;
|
||||
status2->rbuffer_trigger = status1;
|
||||
status3->buffer_reads = true;
|
||||
status3->rbuffer_trigger = status1;
|
||||
|
||||
In this example, when software reads status1, this triggers status2-status3
|
||||
registers to latch their values into their respective read buffers. Subsequent
|
||||
reads to status2 and status3 return the value that these registers contained at
|
||||
the moment that status1 was read. This makes it possible for software to read
|
||||
the state of multiple registers atomically.
|
||||
|
||||
|
||||
Externally Triggered Register Sampling
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
If needed, an external trigger can be used to load a read buffer.
|
||||
This can be useful if precise timing of software's view of the register state is
|
||||
required.
|
||||
|
||||
.. code-block:: systemrdl
|
||||
:emphasize-lines: 14-15
|
||||
|
||||
reg my_status_reg {
|
||||
buffer_reads = true;
|
||||
field {
|
||||
sw=r; hw=w;
|
||||
} value[31:0];
|
||||
};
|
||||
|
||||
my_status_reg status1;
|
||||
my_status_reg status2;
|
||||
|
||||
signal {
|
||||
activehigh;
|
||||
} trigger_signal;
|
||||
status1->rbuffer_trigger = trigger_signal;
|
||||
status2->rbuffer_trigger = trigger_signal;
|
||||
|
||||
When ``hwif_in..trigger_signal`` is asserted, the state of registers ``status1``
|
||||
and ``status2`` is buffered.
|
||||
@@ -3,12 +3,10 @@
|
||||
Write-buffered Registers
|
||||
========================
|
||||
|
||||
In some situations, your hardware design may requre that fields be
|
||||
updated atomically (on the same clock cycle), but your cpuif is not wide enough
|
||||
to do so natively. Using some UDP extensions, this regblock generator implements
|
||||
a way for you to add write-buffering to specific registers, which allows the
|
||||
regblock to delay the effect of a software write operation until a defined
|
||||
trigger event.
|
||||
In order to support larger software write accesses that are atomic, the
|
||||
regblock generator understands several UDPs that implement write-buffering to
|
||||
specific registers. This causes the regblock to delay the effect of a software
|
||||
write operation until a defined trigger event.
|
||||
|
||||
Some examples of when this is useful:
|
||||
* You need to have software update a wide 64-bit register atomically, but
|
||||
@@ -32,23 +30,30 @@ Properties
|
||||
The behavior of write-buffered registers is defined using the following two
|
||||
properties:
|
||||
|
||||
.. literalinclude:: ../../hdl-src/regblock_udps.rdl
|
||||
:lines: 20-28
|
||||
|
||||
``buffer_writes``
|
||||
* Assigned value is a boolean.
|
||||
* If true, enables double-buffering of writes to this register.
|
||||
* Any software write operation to a buffered register is held back in a storage element
|
||||
unique to the register.
|
||||
* The software write operation is committed to the register once triggered to do so.
|
||||
* Any software write operation to a buffered register is held back in a
|
||||
storage element unique to the register.
|
||||
* The software write operation is committed to the register once triggered
|
||||
to do so.
|
||||
* Unless specified otherwise, the buffer trigger occurs when the highest
|
||||
address of the buffered register is written.
|
||||
|
||||
``wbuffer_trigger``
|
||||
* Assigned value is a reference to a register, single-bit field, signal, or single-bit property.
|
||||
* Controls when the double-buffer commits the software write operation to the register's fields.
|
||||
* Assigned value is a reference to a register, single-bit field, signal,
|
||||
or single-bit property.
|
||||
* Controls when the double-buffer commits the software write operation to
|
||||
the register's fields.
|
||||
* If reference is a single-bit value (signal, field, property reference),
|
||||
then the assertion of that value triggers the buffer to be evicted.
|
||||
* Signal references shall have either activehigh/activelow property set to define the polarity.
|
||||
* Signal references shall have either activehigh/activelow property set to
|
||||
define the polarity.
|
||||
* If the reference is a reg, then buffer is evicted when the register's
|
||||
highest address is written
|
||||
highest address is written.
|
||||
|
||||
|
||||
Other Rules
|
||||
@@ -57,18 +62,18 @@ Other Rules
|
||||
writable fields
|
||||
* If ``buffer_writes`` is false, then anything assigned to ``wbuffer_trigger``
|
||||
is ignored.
|
||||
* The buffered register and the trigger reference shall both be within the same
|
||||
internal device. ie: one cannot be in an external scope with respect to the
|
||||
other.
|
||||
* The buffered register and the trigger reference shall both be within the
|
||||
same internal device. ie: one cannot be in an external scope with respect to
|
||||
the other.
|
||||
* Unless it is a register, the reference assigned to ``wbuffer_trigger`` shall
|
||||
represent a single bit.
|
||||
* If a buffered register was not written, any trigger events are ignored.
|
||||
* It is valid for a buffered register to be partially written (either via write
|
||||
strobes, or partial addressing).
|
||||
* The software write operation is not considered to take place until the buffer
|
||||
is evicted by the trigger. This influences the behavior of properties like
|
||||
``swmod`` and ``swacc`` - they are not asserted until the register's fields
|
||||
are actually written by the buffer.
|
||||
* It is valid for a buffered register to be partially written (either via
|
||||
write strobes, or partial addressing).
|
||||
* The software write operation is not considered to take place until the
|
||||
buffer is evicted by the trigger. This influences the behavior of properties
|
||||
like ``swmod`` and ``swacc`` - they are not asserted until the register's
|
||||
fields are actually written by the buffer.
|
||||
|
||||
|
||||
|
||||
@@ -105,8 +110,8 @@ Atomic Group of Registers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Perhaps you have a group of registers that need their state to be updated
|
||||
atomically. Using the ``wbuffer_trigger`` property, you can define which register
|
||||
write operation triggers the group to be updated.
|
||||
atomically. Using the ``wbuffer_trigger`` property, you can define which
|
||||
register write operation triggers the group to be updated.
|
||||
|
||||
|
||||
.. code-block:: systemrdl
|
||||
@@ -136,17 +141,18 @@ write operation triggers the group to be updated.
|
||||
|
||||
In this example software may pre-write information into reg1-reg3, but the
|
||||
register write operations do not take effect until software also writes to reg4.
|
||||
The write operation to reg4 triggers the buffered data to be committed to reg1-reg3.
|
||||
This is guaranteed to occur on the same clock-cycle.
|
||||
The write operation to reg4 triggers the buffered data to be committed to
|
||||
reg1-reg3. This is guaranteed to occur on the same clock-cycle.
|
||||
|
||||
|
||||
Externally Triggered Register Update
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Some applications may require precise timing for when a register (or group of registers)
|
||||
update their value. Often software cannot offer such timing precision.
|
||||
Some applications may require precise timing for when a register (or group of
|
||||
registers) update their value. Often software cannot offer such timing
|
||||
precision.
|
||||
|
||||
In this example, the trigger event is bound to an external signal. When asserted,
|
||||
any pending write operation the buffered register will be committed.
|
||||
In this example, the trigger event is bound to an external signal. When
|
||||
asserted, any pending write operation the buffered register will be committed.
|
||||
The hwif_out value presents the new register state on the clock cycle after the
|
||||
trigger is asserted.
|
||||
|
||||
@@ -169,5 +175,5 @@ trigger is asserted.
|
||||
reg1->wbuffer_trigger = trigger_signal;
|
||||
reg2->wbuffer_trigger = trigger_signal;
|
||||
|
||||
After software writes to ``reg1`` & ``reg2``, the written data is held back in the write
|
||||
buffer until ``hwif_in..trigger_signal`` is asserted by the hardware.
|
||||
After software writes to ``reg1`` & ``reg2``, the written data is held back in
|
||||
the write buffer until ``hwif_in..trigger_signal`` is asserted by the hardware.
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
* For more details, see: https://peakrdl-regblock.readthedocs.io/en/latest/udps/intro.html
|
||||
*/
|
||||
|
||||
property buffer_reads {
|
||||
component = reg;
|
||||
type = boolean;
|
||||
};
|
||||
|
||||
property rbuffer_trigger {
|
||||
component = reg;
|
||||
type = ref;
|
||||
};
|
||||
|
||||
property buffer_writes {
|
||||
component = reg;
|
||||
type = boolean;
|
||||
|
||||
@@ -17,6 +17,7 @@ from .cpuif import CpuifBase
|
||||
from .cpuif.apb4 import APB4_Cpuif
|
||||
from .hwif import Hwif
|
||||
from .write_buffering import WriteBuffering
|
||||
from .read_buffering import ReadBuffering
|
||||
|
||||
class RegblockExporter:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
@@ -31,7 +32,8 @@ class RegblockExporter:
|
||||
self.address_decode = AddressDecode(self)
|
||||
self.field_logic = FieldLogic(self)
|
||||
self.readback = None # type: Readback
|
||||
self.write_buffering = None # type: WriteBuffering
|
||||
self.write_buffering = WriteBuffering(self)
|
||||
self.read_buffering = ReadBuffering(self)
|
||||
self.dereferencer = Dereferencer(self)
|
||||
self.min_read_latency = 0
|
||||
self.min_write_latency = 0
|
||||
@@ -145,7 +147,6 @@ class RegblockExporter:
|
||||
self,
|
||||
retime_read_fanin
|
||||
)
|
||||
self.write_buffering = WriteBuffering(self)
|
||||
|
||||
# Validate that there are no unsupported constructs
|
||||
validator = DesignValidator(self)
|
||||
@@ -161,6 +162,7 @@ class RegblockExporter:
|
||||
"cpuif": self.cpuif,
|
||||
"hwif": self.hwif,
|
||||
"write_buffering": self.write_buffering,
|
||||
"read_buffering": self.read_buffering,
|
||||
"get_resetsignal": self.dereferencer.get_resetsignal,
|
||||
"address_decode": self.address_decode,
|
||||
"field_logic": self.field_logic,
|
||||
|
||||
@@ -175,6 +175,11 @@ class FieldLogic:
|
||||
"""
|
||||
Asserted when field is software accessed (read)
|
||||
"""
|
||||
buffer_reads = field.parent.get_property('buffer_reads')
|
||||
if buffer_reads:
|
||||
rstrb = self.exp.read_buffering.get_trigger(field.parent)
|
||||
return rstrb
|
||||
else:
|
||||
strb = self.exp.dereferencer.get_access_strobe(field)
|
||||
return f"{strb} && !decoded_req_is_wr"
|
||||
|
||||
@@ -186,6 +191,7 @@ class FieldLogic:
|
||||
w_modifiable = field.is_sw_writable
|
||||
r_modifiable = (field.get_property('onread') is not None)
|
||||
buffer_writes = field.parent.get_property('buffer_writes')
|
||||
buffer_reads = field.parent.get_property('buffer_reads')
|
||||
|
||||
if w_modifiable and not r_modifiable:
|
||||
# assert swmod only on sw write
|
||||
@@ -200,10 +206,18 @@ class FieldLogic:
|
||||
|
||||
if w_modifiable and r_modifiable:
|
||||
# assert swmod on both sw read and write
|
||||
if buffer_writes:
|
||||
astrb = self.exp.dereferencer.get_access_strobe(field)
|
||||
wstrb = self.exp.write_buffering.get_write_strobe(field)
|
||||
if buffer_writes or buffer_reads:
|
||||
if buffer_reads:
|
||||
rstrb = self.exp.read_buffering.get_trigger(field.parent)
|
||||
else:
|
||||
rstrb = f"{astrb} && !decoded_req_is_wr"
|
||||
|
||||
if buffer_writes:
|
||||
wstrb = self.exp.write_buffering.get_write_strobe(field)
|
||||
else:
|
||||
wstrb = f"{astrb} && decoded_req_is_wr"
|
||||
|
||||
return f"{wstrb} || {rstrb}"
|
||||
else:
|
||||
# Unbuffered. Use decoder strobe directly
|
||||
@@ -213,7 +227,11 @@ class FieldLogic:
|
||||
if not w_modifiable and r_modifiable:
|
||||
# assert swmod only on sw read
|
||||
astrb = self.exp.dereferencer.get_access_strobe(field)
|
||||
return f"{astrb} && !decoded_req_is_wr"
|
||||
if buffer_reads:
|
||||
rstrb = self.exp.read_buffering.get_trigger(field.parent)
|
||||
else:
|
||||
rstrb = f"{astrb} && !decoded_req_is_wr"
|
||||
return rstrb
|
||||
|
||||
# Not sw modifiable
|
||||
return "1'b0"
|
||||
|
||||
@@ -13,6 +13,12 @@ class _OnRead(NextStateConditional):
|
||||
return field.get_property('onread') == self.onreadtype
|
||||
|
||||
def get_predicate(self, field: 'FieldNode') -> str:
|
||||
if field.parent.get_property('buffer_reads'):
|
||||
# Is buffered read. Use alternate strobe
|
||||
rstrb = self.exp.read_buffering.get_trigger(field.parent)
|
||||
return rstrb
|
||||
else:
|
||||
# is regular register
|
||||
strb = self.exp.dereferencer.get_access_strobe(field)
|
||||
return f"{strb} && !decoded_req_is_wr"
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ module {{module_name}} (
|
||||
assign cpuif_wr_err = '0;
|
||||
|
||||
{%- if has_buffered_write_regs %}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Write double-buffers
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -116,7 +117,6 @@ module {{module_name}} (
|
||||
|
||||
{{write_buffering.get_implementation()|indent}}
|
||||
{%- endif %}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Field logic
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -126,6 +126,15 @@ module {{module_name}} (
|
||||
|
||||
{{field_logic.get_implementation()|indent}}
|
||||
|
||||
{%- if has_buffered_read_regs %}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Read double-buffers
|
||||
//--------------------------------------------------------------------------
|
||||
{{read_buffering.get_storage_struct()|indent}}
|
||||
|
||||
{{read_buffering.get_implementation()|indent}}
|
||||
{%- endif %}
|
||||
//--------------------------------------------------------------------------
|
||||
// Readback
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
58
src/peakrdl_regblock/read_buffering/__init__.py
Normal file
58
src/peakrdl_regblock/read_buffering/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from systemrdl.node import AddrmapNode, RegNode, FieldNode, SignalNode
|
||||
|
||||
from .storage_generator import RBufStorageStructGenerator
|
||||
from .implementation_generator import RBufLogicGenerator
|
||||
from ..utils import get_indexed_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..exporter import RegblockExporter
|
||||
|
||||
|
||||
class ReadBuffering:
|
||||
def __init__(self, exp:'RegblockExporter'):
|
||||
self.exp = exp
|
||||
|
||||
@property
|
||||
def top_node(self) -> 'AddrmapNode':
|
||||
return self.exp.top_node
|
||||
|
||||
def get_storage_struct(self) -> str:
|
||||
struct_gen = RBufStorageStructGenerator()
|
||||
s = struct_gen.get_struct(self.top_node, "rbuf_storage_t")
|
||||
assert s is not None
|
||||
return s + "\nrbuf_storage_t rbuf_storage;"
|
||||
|
||||
def get_implementation(self) -> str:
|
||||
gen = RBufLogicGenerator(self)
|
||||
s = gen.get_content(self.top_node)
|
||||
assert s is not None
|
||||
return s
|
||||
|
||||
def get_trigger(self, node: RegNode) -> str:
|
||||
trigger = node.get_property('rbuffer_trigger')
|
||||
|
||||
if isinstance(trigger, RegNode):
|
||||
# Trigger is a register.
|
||||
# trigger when lowermost address of the register is written
|
||||
regwidth = node.get_property('regwidth')
|
||||
accesswidth = node.get_property('accesswidth')
|
||||
strb_prefix = self.exp.dereferencer.get_access_strobe(trigger, reduce_substrobes=False)
|
||||
|
||||
if accesswidth < regwidth:
|
||||
return f"{strb_prefix}[0] && !decoded_req_is_wr"
|
||||
else:
|
||||
return f"{strb_prefix} && !decoded_req_is_wr"
|
||||
elif isinstance(trigger, SignalNode):
|
||||
s = self.exp.dereferencer.get_value(trigger)
|
||||
if trigger.get_property('activehigh'):
|
||||
return s
|
||||
else:
|
||||
return f"~{s}"
|
||||
else:
|
||||
# Trigger is a field or propref bit
|
||||
return self.exp.dereferencer.get_value(trigger)
|
||||
|
||||
def get_rbuf_data(self, node: RegNode) -> str:
|
||||
return "rbuf_storage." + get_indexed_path(self.top_node, node) + ".data"
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from systemrdl.component import Reg
|
||||
from systemrdl.node import RegNode
|
||||
|
||||
from ..forloop_generator import RDLForLoopGenerator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ReadBuffering
|
||||
|
||||
class RBufLogicGenerator(RDLForLoopGenerator):
|
||||
i_type = "genvar"
|
||||
def __init__(self, rbuf: 'ReadBuffering') -> None:
|
||||
super().__init__()
|
||||
self.rbuf = rbuf
|
||||
self.exp = rbuf.exp
|
||||
self.template = self.exp.jj_env.get_template(
|
||||
"read_buffering/template.sv"
|
||||
)
|
||||
|
||||
def enter_Reg(self, node: RegNode) -> None:
|
||||
super().enter_Reg(node)
|
||||
assert isinstance(node.inst, Reg)
|
||||
|
||||
if not node.get_property('buffer_reads'):
|
||||
return
|
||||
|
||||
context = {
|
||||
'node': node,
|
||||
'rbuf': self.rbuf,
|
||||
'get_assignments': self.get_assignments,
|
||||
}
|
||||
self.add_content(self.template.render(context))
|
||||
|
||||
|
||||
|
||||
def get_assignments(self, node: RegNode) -> str:
|
||||
data = self.rbuf.get_rbuf_data(node)
|
||||
bidx = 0
|
||||
s = []
|
||||
for field in node.fields():
|
||||
if bidx < field.low:
|
||||
# zero padding before field
|
||||
s.append(f"{data}[{field.low-1}:{bidx}] = '0;")
|
||||
|
||||
value = self.exp.dereferencer.get_value(field)
|
||||
if field.msb < field.lsb:
|
||||
# Field gets bitswapped since it is in [low:high] orientation
|
||||
value = f"{{<<{{{value}}}}}"
|
||||
s.append(f"{data}[{field.high}:{field.low}] = {value};")
|
||||
|
||||
bidx = field.high + 1
|
||||
|
||||
regwidth = node.get_property('regwidth')
|
||||
if bidx < regwidth:
|
||||
# zero padding after last field
|
||||
s.append(f"{data}[{regwidth-1}:{bidx}] = '0;")
|
||||
|
||||
return "\n".join(s)
|
||||
18
src/peakrdl_regblock/read_buffering/storage_generator.py
Normal file
18
src/peakrdl_regblock/read_buffering/storage_generator.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from systemrdl.node import FieldNode, RegNode
|
||||
|
||||
from ..struct_generator import RDLStructGenerator
|
||||
|
||||
class RBufStorageStructGenerator(RDLStructGenerator):
|
||||
|
||||
def enter_Field(self, node: FieldNode) -> None:
|
||||
# suppress parent class's field behavior
|
||||
pass
|
||||
|
||||
def enter_Reg(self, node: RegNode) -> None:
|
||||
super().enter_Reg(node)
|
||||
|
||||
if not node.get_property('buffer_reads'):
|
||||
return
|
||||
|
||||
regwidth = node.get_property('regwidth')
|
||||
self.add_member("data", regwidth)
|
||||
5
src/peakrdl_regblock/read_buffering/template.sv
Normal file
5
src/peakrdl_regblock/read_buffering/template.sv
Normal file
@@ -0,0 +1,5 @@
|
||||
always_ff @(posedge clk) begin
|
||||
if({{rbuf.get_trigger(node)}}) begin
|
||||
{{get_assignments(node)|indent(8)}}
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from systemrdl.node import RegNode
|
||||
|
||||
from ..forloop_generator import RDLForLoopGenerator, LoopBody
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..exporter import RegblockExporter
|
||||
from systemrdl.node import RegNode
|
||||
|
||||
class ReadbackLoopBody(LoopBody):
|
||||
def __init__(self, dim: int, iterator: str, i_type: str) -> None:
|
||||
@@ -76,19 +77,31 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator):
|
||||
self.current_offset = start_offset + n_regs * dim
|
||||
|
||||
|
||||
def enter_Reg(self, node: 'RegNode') -> None:
|
||||
def enter_Reg(self, node: RegNode) -> None:
|
||||
if not node.has_sw_readable:
|
||||
return
|
||||
|
||||
accesswidth = node.get_property('accesswidth')
|
||||
regwidth = node.get_property('regwidth')
|
||||
rbuf = node.get_property('buffer_reads')
|
||||
if rbuf:
|
||||
trigger = node.get_property('rbuffer_trigger')
|
||||
is_own_trigger = (isinstance(trigger, RegNode) and trigger == node)
|
||||
if is_own_trigger:
|
||||
if accesswidth < regwidth:
|
||||
self.process_buffered_reg_with_bypass(node, regwidth, accesswidth)
|
||||
else:
|
||||
# bypass cancels out. Behaves like a normal reg
|
||||
self.process_reg(node)
|
||||
else:
|
||||
self.process_buffered_reg(node, regwidth, accesswidth)
|
||||
elif accesswidth < regwidth:
|
||||
self.process_wide_reg(node, accesswidth)
|
||||
else:
|
||||
self.process_reg(node)
|
||||
|
||||
|
||||
def process_reg(self, node: 'RegNode') -> None:
|
||||
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
|
||||
@@ -100,11 +113,10 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator):
|
||||
if field.low != current_bit:
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low-1}:{current_bit}] = '0;")
|
||||
|
||||
value = self.exp.dereferencer.get_value(field)
|
||||
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)
|
||||
value = f"{{<<{{{value}}}}}"
|
||||
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;")
|
||||
|
||||
@@ -118,7 +130,98 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator):
|
||||
self.current_offset += 1
|
||||
|
||||
|
||||
def process_wide_reg(self, node: 'RegNode', accesswidth: int) -> None:
|
||||
def process_buffered_reg(self, node: RegNode, regwidth: int, accesswidth: int) -> None:
|
||||
rbuf = self.exp.read_buffering.get_rbuf_data(node)
|
||||
|
||||
if accesswidth < regwidth:
|
||||
# Is wide reg
|
||||
n_subwords = regwidth // accesswidth
|
||||
astrb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False)
|
||||
for i in range(n_subwords):
|
||||
rd_strb = f"({astrb}[{i}] && !decoded_req_is_wr)"
|
||||
bslice = f"[{(i + 1) * accesswidth - 1}:{i*accesswidth}]"
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}] = {rd_strb} ? {rbuf}{bslice} : '0;")
|
||||
self.current_offset += 1
|
||||
|
||||
else:
|
||||
# Is regular reg
|
||||
rd_strb = f"({self.exp.dereferencer.get_access_strobe(node)} && !decoded_req_is_wr)"
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{regwidth-1}:0] = {rd_strb} ? {rbuf} : '0;")
|
||||
|
||||
bus_width = self.exp.cpuif.data_width
|
||||
if regwidth < bus_width:
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{bus_width-1}:{regwidth}] = '0;")
|
||||
|
||||
self.current_offset += 1
|
||||
|
||||
|
||||
def process_buffered_reg_with_bypass(self, node: RegNode, regwidth: int, accesswidth: int) -> None:
|
||||
"""
|
||||
Special case for a buffered register when the register is its own trigger.
|
||||
First sub-word shall bypass the read buffer and assign directly.
|
||||
Subsequent subwords assign from the buffer.
|
||||
Caller guarantees this is a wide reg
|
||||
"""
|
||||
astrb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False)
|
||||
|
||||
# Generate assignments for first sub-word
|
||||
bidx = 0
|
||||
rd_strb = f"({astrb}[0] && !decoded_req_is_wr)"
|
||||
for field in node.fields():
|
||||
if not field.is_sw_readable:
|
||||
continue
|
||||
|
||||
if field.low >= accesswidth:
|
||||
# field is not in this subword.
|
||||
break
|
||||
|
||||
if bidx < field.low:
|
||||
# insert padding before
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low - 1}:{bidx}] = '0;")
|
||||
|
||||
if field.high >= accesswidth:
|
||||
# field gets truncated
|
||||
r_low = field.low
|
||||
r_high = accesswidth - 1
|
||||
f_low = 0
|
||||
f_high = accesswidth - 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;")
|
||||
bidx = accesswidth
|
||||
else:
|
||||
# field fits in subword
|
||||
value = self.exp.dereferencer.get_value(field)
|
||||
if field.msb < field.lsb:
|
||||
# Field gets bitswapped since it is in [low:high] orientation
|
||||
value = f"{{<<{{{value}}}}}"
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;")
|
||||
bidx = field.high + 1
|
||||
|
||||
# pad up remainder of subword
|
||||
if bidx < accesswidth:
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{accesswidth-1}:{bidx}] = '0;")
|
||||
self.current_offset += 1
|
||||
|
||||
# Assign remainder of subwords from read buffer
|
||||
n_subwords = regwidth // accesswidth
|
||||
rbuf = self.exp.read_buffering.get_rbuf_data(node)
|
||||
for i in range(1, n_subwords):
|
||||
rd_strb = f"({astrb}[{i}] && !decoded_req_is_wr)"
|
||||
bslice = f"[{(i + 1) * accesswidth - 1}:{i*accesswidth}]"
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}] = {rd_strb} ? {rbuf}{bslice} : '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
|
||||
@@ -162,11 +265,10 @@ class ReadbackAssignmentGenerator(RDLForLoopGenerator):
|
||||
low = field.low - accesswidth * subword_idx
|
||||
high = field.high - accesswidth * subword_idx
|
||||
|
||||
value = self.exp.dereferencer.get_value(field)
|
||||
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)
|
||||
value = f"{{<<{{{value}}}}}"
|
||||
|
||||
self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = {rd_strb} ? {value} : '0;")
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ class DesignScanner(RDLListener):
|
||||
accesswidth = node.get_property('accesswidth')
|
||||
self.cpuif_data_width = max(self.cpuif_data_width, accesswidth)
|
||||
|
||||
self.has_buffered_write_regs = self.has_buffered_write_regs or node.get_property('buffer_writes')
|
||||
self.has_buffered_read_regs = self.has_buffered_read_regs or node.get_property('buffer_reads')
|
||||
self.has_buffered_write_regs = self.has_buffered_write_regs or bool(node.get_property('buffer_writes'))
|
||||
self.has_buffered_read_regs = self.has_buffered_read_regs or bool(node.get_property('buffer_reads'))
|
||||
|
||||
def enter_Signal(self, node: 'SignalNode') -> None:
|
||||
if node.get_property('field_reset'):
|
||||
|
||||
0
tests/test_read_buffer/__init__.py
Normal file
0
tests/test_read_buffer/__init__.py
Normal file
117
tests/test_read_buffer/regblock.rdl
Normal file
117
tests/test_read_buffer/regblock.rdl
Normal file
@@ -0,0 +1,117 @@
|
||||
addrmap top {
|
||||
default regwidth = 8;
|
||||
default accesswidth = 8;
|
||||
default sw=r;
|
||||
default hw=r;
|
||||
|
||||
signal {} incr_en;
|
||||
//--------------------------------------------------------------------------
|
||||
// Wide registers
|
||||
//--------------------------------------------------------------------------
|
||||
reg {
|
||||
regwidth = 32;
|
||||
default counter;
|
||||
default incr = incr_en;
|
||||
buffer_reads;
|
||||
|
||||
field {} f1[3] = 0;
|
||||
field {} f2[3] = 0;
|
||||
field {} f3[3] = 0;
|
||||
field {} f4[3] = 0;
|
||||
field {} f5[3] = 0;
|
||||
field {} f6[3] = 0;
|
||||
field {} f7[3] = 0;
|
||||
field {} f8[3] = 0;
|
||||
field {} f9[3] = 0;
|
||||
field {} fa[3] = 0;
|
||||
} reg1;
|
||||
|
||||
reg {
|
||||
regwidth = 32;
|
||||
default counter;
|
||||
default incr = incr_en;
|
||||
buffer_reads;
|
||||
|
||||
field {} f1[28:30] = 0;
|
||||
field {} f2[3] = 0;
|
||||
field {} f3[3] = 0;
|
||||
field {} f4[3] = 0;
|
||||
field {} f5[3] = 0;
|
||||
field {} f6[3] = 0;
|
||||
field {} f7[3] = 0;
|
||||
field {} f8[3] = 0;
|
||||
field {} f9[3] = 0;
|
||||
field {} fa[3] = 0;
|
||||
} reg1_msb0;
|
||||
|
||||
|
||||
reg {
|
||||
regwidth = 32;
|
||||
default counter;
|
||||
default incr = incr_en;
|
||||
default rclr;
|
||||
buffer_reads;
|
||||
|
||||
field {} f1[4:0] = 0;
|
||||
field {} f2[14:10] = 0;
|
||||
field {} f3[26:22] = 0;
|
||||
field {} f4[31:27] = 0;
|
||||
} reg2;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Alternate Triggers
|
||||
//--------------------------------------------------------------------------
|
||||
reg myreg {
|
||||
buffer_reads;
|
||||
default counter;
|
||||
default incr = incr_en;
|
||||
field {} f1[7:0] = 0;
|
||||
};
|
||||
|
||||
reg myreg_wide {
|
||||
buffer_reads;
|
||||
default counter;
|
||||
default incr = incr_en;
|
||||
regwidth = 16;
|
||||
field {} f1[15:0] = 0xAAAA;
|
||||
};
|
||||
|
||||
// Trigger via another register
|
||||
myreg g1_r1;
|
||||
myreg g1_r2;
|
||||
g1_r2->rbuffer_trigger = g1_r1;
|
||||
|
||||
myreg_wide g2_r1 @ 0x10;
|
||||
myreg_wide g2_r2;
|
||||
g2_r2->rbuffer_trigger = g2_r1;
|
||||
|
||||
// triger from signal
|
||||
signal {
|
||||
activehigh;
|
||||
} trigger_sig;
|
||||
signal {
|
||||
activelow;
|
||||
} trigger_sig_n;
|
||||
reg ro_reg {
|
||||
buffer_reads;
|
||||
field {
|
||||
hw=w;
|
||||
} f1[7:0];
|
||||
};
|
||||
ro_reg g3_r1;
|
||||
ro_reg g3_r2;
|
||||
g3_r1->rbuffer_trigger = trigger_sig;
|
||||
g3_r2->rbuffer_trigger = trigger_sig_n;
|
||||
|
||||
// trigger from field/propref
|
||||
reg {
|
||||
field {
|
||||
sw=w; hw=r; singlepulse;
|
||||
} trig = 0;
|
||||
} g4_trig;
|
||||
myreg g4_r1;
|
||||
myreg g4_r2;
|
||||
g4_r1->rbuffer_trigger = g4_trig.trig;
|
||||
g4_r2->rbuffer_trigger = g4_trig.trig->swmod;
|
||||
|
||||
};
|
||||
146
tests/test_read_buffer/tb_template.sv
Normal file
146
tests/test_read_buffer/tb_template.sv
Normal file
@@ -0,0 +1,146 @@
|
||||
{% extends "lib/tb_base.sv" %}
|
||||
|
||||
{% block seq %}
|
||||
{% sv_line_anchor %}
|
||||
cb.hwif_in.incr_en <= '1;
|
||||
cb.hwif_in.trigger_sig_n <= '1;
|
||||
##1;
|
||||
cb.rst <= '0;
|
||||
##1;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Wide registers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// reg1
|
||||
// expect to read all counter values atomically
|
||||
begin
|
||||
logic [7:0] subword;
|
||||
logic [31:0] rdata;
|
||||
logic [2:0] fdata;
|
||||
cpuif.read('h0, subword);
|
||||
fdata = subword[2:0];
|
||||
rdata = {10{fdata}};
|
||||
assert(subword == rdata[7:0]);
|
||||
|
||||
cpuif.assert_read('h1, rdata[15:8]);
|
||||
cpuif.assert_read('h2, rdata[23:16]);
|
||||
cpuif.assert_read('h3, rdata[31:24]);
|
||||
end
|
||||
|
||||
// reg1_msb0
|
||||
// expect to read all counter values atomically
|
||||
begin
|
||||
logic [7:0] subword;
|
||||
logic [31:0] rdata;
|
||||
logic [2:0] fdata;
|
||||
cpuif.read('h4, subword);
|
||||
fdata = subword[3:1];
|
||||
rdata = {10{fdata}} << 1;
|
||||
assert(subword == rdata[7:0]);
|
||||
|
||||
cpuif.assert_read('h5, rdata[15:8]);
|
||||
cpuif.assert_read('h6, rdata[23:16]);
|
||||
cpuif.assert_read('h7, rdata[31:24]);
|
||||
end
|
||||
|
||||
cb.hwif_in.incr_en <= '0;
|
||||
@cb;
|
||||
|
||||
// check that msb0 ordering is correct
|
||||
begin
|
||||
logic [7:0] subword;
|
||||
logic [31:0] rdata;
|
||||
logic [2:0] fdata;
|
||||
cpuif.read('h0, subword);
|
||||
fdata = subword[2:0];
|
||||
rdata = {10{fdata}};
|
||||
assert(subword == rdata[7:0]);
|
||||
|
||||
cpuif.assert_read('h1, rdata[15:8]);
|
||||
cpuif.assert_read('h2, rdata[23:16]);
|
||||
cpuif.assert_read('h3, rdata[31:24]);
|
||||
|
||||
|
||||
fdata = {<<{fdata}};
|
||||
rdata = {10{fdata}} << 1;
|
||||
cpuif.assert_read('h4, rdata[7:0]);
|
||||
cpuif.assert_read('h5, rdata[15:8]);
|
||||
cpuif.assert_read('h6, rdata[23:16]);
|
||||
cpuif.assert_read('h7, rdata[31:24]);
|
||||
end
|
||||
|
||||
cb.hwif_in.incr_en <= '1;
|
||||
|
||||
// reg2
|
||||
// read-clear
|
||||
repeat(2) begin
|
||||
logic [7:0] subword;
|
||||
logic [4:0] fdata;
|
||||
logic [31:0] rdata;
|
||||
|
||||
cpuif.read('h8, subword);
|
||||
rdata[7:0] = subword;
|
||||
cpuif.read('h9, subword);
|
||||
rdata[15:8] = subword;
|
||||
cpuif.read('hA, subword);
|
||||
rdata[23:16] = subword;
|
||||
cpuif.read('hB, subword);
|
||||
rdata[31:24] = subword;
|
||||
|
||||
fdata = rdata[4:0];
|
||||
assert(rdata[14:10] == fdata);
|
||||
assert(rdata[26:22] == fdata);
|
||||
assert(rdata[31:27] == fdata);
|
||||
end
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Alternate Triggers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Trigger via another register
|
||||
// g1
|
||||
begin
|
||||
logic [7:0] rdata;
|
||||
cpuif.read('hC, rdata);
|
||||
cpuif.assert_read('hD, rdata);
|
||||
end
|
||||
|
||||
// g2
|
||||
begin
|
||||
logic [7:0] rdata1;
|
||||
logic [7:0] rdata2;
|
||||
cpuif.read('h10, rdata1);
|
||||
cpuif.read('h11, rdata2);
|
||||
cpuif.assert_read('h12, rdata1);
|
||||
cpuif.assert_read('h13, rdata2);
|
||||
end
|
||||
|
||||
// triger from signal
|
||||
// g3
|
||||
cb.hwif_in.g3_r1.f1.next <= 'hAB;
|
||||
cb.hwif_in.g3_r2.f1.next <= 'hCD;
|
||||
cb.hwif_in.trigger_sig <= '1;
|
||||
cb.hwif_in.trigger_sig_n <= '0;
|
||||
@cb;
|
||||
cb.hwif_in.g3_r1.f1.next <= 'h00;
|
||||
cb.hwif_in.g3_r2.f1.next <= 'h00;
|
||||
cb.hwif_in.trigger_sig <= '0;
|
||||
cb.hwif_in.trigger_sig_n <= '1;
|
||||
@cb;
|
||||
cpuif.assert_read('h14, 'hAB);
|
||||
cpuif.assert_read('h15, 'hCD);
|
||||
|
||||
// trigger from field/propref
|
||||
// g4
|
||||
begin
|
||||
logic [7:0] rdata;
|
||||
cpuif.write('h16, 'h1);
|
||||
repeat(5) @cb;
|
||||
cpuif.read('h17, rdata);
|
||||
repeat(5) @cb;
|
||||
cpuif.assert_read('h18, rdata - 1); // swmod happens one cycle earlier, so count is -1
|
||||
end
|
||||
|
||||
{% endblock %}
|
||||
5
tests/test_read_buffer/testcase.py
Normal file
5
tests/test_read_buffer/testcase.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from ..lib.sim_testcase import SimTestCase
|
||||
|
||||
class Test(SimTestCase):
|
||||
def test_dut(self):
|
||||
self.run_test()
|
||||
Reference in New Issue
Block a user