- Start Date: 2024-06-28
- RFC PR: amaranth-lang/rfcs#70
- Amaranth SoC Issue: amaranth-lang/amaranth-soc#93
Unify the naming of MemoryMap
resources and windows
Summary
- Use a
MemoryMap.Name
class to represent resource and window names. - Assign window names in
MemoryMap.add_window()
instead ofMemoryMap.__init__()
.
Motivation
In a MemoryMap
, resources (e.g. registers of a peripheral) and windows (nested memory maps) are added with similar methods: MemoryMap.add_resource()
and .add_window()
.
However, their names are handled differently:
- a resource name is a tuple of strings, assigned as a parameter to
MemoryMap.add_resource()
. - a window name is a string, assigned at the creation of the window itself (as a parameter to
MemoryMap.__init__()
).
These differences add needless complexity to the API:
- the name of a window is only relevant from the context of the memory map to which it is added.
- window names may also benefit from being split into tuples, in order to let consumers of the memory map (such as a BSP generator) decide their format.
Additionally, support for integers in resource and window names is needed to represent indices. A BSP generator may choose to format them in a specific way (e.g. "foo[1]"
).
Guide-level explanation
Resource and window names are instances of MemoryMap.Name
, a subclass of tuple
which can only contain non-empty strings and non-negative integers.
Some examples:
>>> MemoryMap.Name(("rx", "status"))
Name('rx', 'status')
>>> MemoryMap.Name(("uart", 0))
Name('uart', 0)
>>> MemoryMap.Name(MemoryMap.Name(("uart", 0))
Name('uart', 0)
>>> MemoryMap.Name("foo")
Name('foo',)
Assigning resource names
The name of a resource is given by the name
parameter of MemoryMap.add_resource()
. For the sake of brevity, users can pass a tuple which is implicitly cast to a MemoryMap.Name
.
This example shows how names are assigned to the registers of an UART peripheral:
from amaranth import *
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out
from amaranth_soc import csr
from amaranth_soc.memory import MemoryMap
class UART(wiring.Component):
class RxConfig(csr.Register, access="rw"):
enable: csr.Field(csr.action.RW, 1)
class RxStatus(csr.Register, access="rw"):
ready: csr.Field(csr.action.R, 1)
error: csr.Field(csr.action.RW1C, 1)
class RxData(csr.Register, access="r"):
def __init__(self):
super().__init__(csr.Field(csr.action.R, 8))
csr_bus: In(csr.Signature(addr_width=10, data_width=32))
def __init__(self):
super().__init__()
memory_map = MemoryMap(addr_width=10, data_width=32)
memory_map.add_resource(self.RxConfig(), size=1, name=("rx", "config"))
memory_map.add_resource(self.RxStatus(), size=1, name=MemoryMap.Name(("rx", "status")))
memory_map.add_resource(self.RxData(), size=1, name=("rx", "data"))
memory_map.freeze()
self.csr_bus.memory_map = memory_map
def elaborate(self, platform):
m = Module()
# ...
return m
Assigning window names
Similarly, the name of a window is given by the name
parameter of MemoryMap.add_window()
. Unlike resource names, window names are optional.
This example shows how names are assigned to two UART peripherals, as their memory maps are added as windows to a bus decoder memory map:
class Decoder(wiring.Component):
csr_bus: In(csr.Signature(addr_width=20, data_width=32))
def __init__(self):
super().__init__()
self._subs = dict()
self.csr_bus.memory_map = MemoryMap(addr_width=20, data_width=32)
def add(self, sub_bus, *, name=None, addr=None):
self._subs[sub_bus.memory_map] = sub_bus
return self.csr_bus.memory_map.add_window(sub_bus.memory_map, name=name, addr=addr)
def elaborate(self, platform):
m = Module()
with m.Switch(self.csr_bus.addr):
for window, name, (pattern, ratio) in self.csr_bus.memory_map.window_patterns():
sub_bus = self._subs[window]
with m.Case(pattern):
pass # ... drive the subordinate bus interface
return m
uart_0 = UART()
uart_1 = UART()
decoder = Decoder()
decoder.add(uart_0.csr_bus, name=("uart", 0))
decoder.add(uart_1.csr_bus, name=MemoryMap.Name(("uart", 1)))
In a MemoryMap
hierarchy, each resource is identified by a path. The path of a resource is a tuple ending with its name, preceded by the name of each window that contains it:
>>> for res_info in decoder.csr_bus.memory_map.all_resources():
... print(res_info.path)
(Name('uart', 0), Name('rx', 'config'))
(Name('uart', 0), Name('rx', 'status'))
(Name('uart', 0), Name('rx', 'data'))
(Name('uart', 1), Name('rx', 'config'))
(Name('uart', 1), Name('rx', 'status'))
(Name('uart', 1), Name('rx', 'data'))
Reference-level explanation
The following changes to the amaranth_soc.memory
module are made:
-
Add a
MemoryMap.Name(name, /)
class. It is a subclass oftuple
, where:name
must either be a string or a tuple of strings and non-negative integers. Ifname
is a string, it is implicitly converted to(name,)
.
-
The following changes to
MemoryMap
are made:- The optional
name
parameter ofMemoryMap()
is removed. - The
MemoryMap.name
property is removed. - The
name
parameter ofMemoryMap.add_resource()
must be aMemoryMap.Name
. - An optional
name
parameter is added toMemoryMap.add_window()
, which must be aMemoryMap.Name
. - The yield values of
MemoryMap.windows()
are changed towindow, name, (start, end, ratio)
. - The yield values of
MemoryMap.window_patterns()
are changed towindow, name, (pattern, ratio)
.
- The optional
-
The following changes to
ResourceInfo
are made:- The
path
parameter ofResourceInfo()
must be a tuple ofMemoryMap.Name
objects.
- The
As a consequence of this proposal, the following changes are made to other modules:
-
amaranth_soc.csr.bus
andamaranth_soc.wishbone.bus
:- The optional
name
parameter ofDecoder()
is moved toDecoder.add()
.
- The optional
-
amaranth_soc.csr.reg
:- The optional
name
parameter ofBuilder()
is removed.
- The optional
-
amaranth_soc.csr.wishbone
:- The optional
name
parameter ofWishboneCSRBridge()
is assigned tocsr_bus.memory_map
(instead ofwb_bus.memory_map
).
- The optional
Drawbacks
- This will break codebases that make use of window names.
Rationale and alternatives
- Providing a
MemoryMap.Name
class for resource and window names facilitates their validation and documentation.Alternative #1: do not add a class, and use standard tuples instead. Names will have to be validated by other means.Alternative #2: useMemoryMap.Name
for resource names only. Window names remain limited to strings.
Prior art
- Resource names became tuples of strings as a consequence of RFC 16. However, array indices defined with
csr.Builder.Index()
were cast to strings when.as_memory_map()
was called.
Resolved questions
- Should we require the presence of at least one string in
MemoryMap.Name
?- Empty names are forbidden and transparent windows (i.e. without names) must use
None
instead. Further validation is deferred to consumers of the memory map (e.g. a BSP generator).
- Empty names are forbidden and transparent windows (i.e. without names) must use
- Should we specify the order between strings and integers ? In
csr.Builder
, array indices precede cluster and register names (e.g.('bar', 0, 'foo')
could be formatted as"bar.foo[0]"
).- No, this decision is left out to consumers of the memory map. They may interpret a name differently depending on what it is assigned to.
Future possibilities
None.