Memory maps
The amaranth_soc.memory
module provides primitives for organizing the address space of a bus interface.
Introduction
The purpose of MemoryMap
is to provide a hierarchical description of the address space of a System-on-Chip, from its bus interconnect to the registers of its peripherals. It is composed of resources (representing registers, memories, etc) and windows (representing bus bridges), and may be queried afterwards in order to enumerate its contents, or determine the address of a resource.
Resources
A resource is a Component
previously added to a MemoryMap
. Each resource occupies an unique range of addresses within the memory map, and represents a device that is a target for bus transactions.
Adding resources
Resources are added with MemoryMap.add_resource()
, which returns a (start, end)
tuple describing their address range:
memory_map = MemoryMap(addr_width=3, data_width=8)
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
reg_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
>>> memory_map.add_resource(reg_ctrl, size=4, addr=0x0, name=("ctrl",))
(0, 4)
>>> memory_map.add_resource(reg_data, size=4, addr=0x4, name=("data",))
(4, 8)
Note
The addr
parameter of MemoryMap.add_resource()
and MemoryMap.add_window()
is optional.
To simplify address assignment, each MemoryMap
has an implicit next address, starting at 0. If a resource or a window is added without an explicit address, the implicit next address is used. In any case, the implicit next address is set to the address immediately following the newly added resource or window.
Accessing resources
Memory map resources can be iterated with MemoryMap.resources()
:
>>> for resource, name, (start, end) in memory_map.resources():
... print(f"name={name}, start={start:#x}, end={end:#x}, resource={resource}")
name=Name('ctrl'), start=0x0, end=0x4, resource=<...>
name=Name('data'), start=0x4, end=0x8, resource=<...>
A memory map can be queried with MemoryMap.find_resource()
to get the name and address range of a given resource:
>>> memory_map.find_resource(reg_ctrl)
ResourceInfo(path=(Name('ctrl'),), start=0x0, end=0x4, width=8)
The resource located at a given address can be retrieved with MemoryMap.decode_address()
:
>>> memory_map.decode_address(0x4) is reg_data
True
Alignment
The value of MemoryMap.alignment
constrains the layout of a memory map. If unspecified, it defaults to 0.
Each resource or window added to a memory map is placed at an address that is a multiple of 2 ** alignment
, and its size is rounded up to a multiple of 2 ** alignment
.
For example, the resources of this memory map are 64-bit aligned:
memory_map = MemoryMap(addr_width=8, data_width=8, alignment=3)
reg_foo = csr.Register(csr.Field(csr.action.RW, 32), "rw")
reg_bar = csr.Register(csr.Field(csr.action.RW, 32), "rw")
reg_baz = csr.Register(csr.Field(csr.action.RW, 32), "rw")
>>> memory_map.add_resource(reg_foo, size=4, name=("foo",))
(0, 8)
>>> memory_map.add_resource(reg_bar, size=4, name=("bar",), addr=0x9)
Traceback (most recent call last):
...
ValueError: Explicitly specified address 0x9 must be a multiple of 0x8 bytes
MemoryMap.add_resource()
takes an optional alignment
parameter. If a value greater than MemoryMap.alignment
is given, it becomes the alignment of this resource:
>>> memory_map.add_resource(reg_bar, size=4, name=("bar",), alignment=4)
(16, 32)
MemoryMap.align_to()
can be used to align the implicit next address. Its alignment is modified if a value greater than MemoryMap.alignment
is given.
>>> memory_map.align_to(6)
64
>>> memory_map.add_resource(reg_baz, size=4, name=("baz",))
(64, 72)
Note
MemoryMap.align_to()
has no effect on the size of the next resource or window.
Windows
A window is a MemoryMap
nested inside another memory map. Each window occupies an unique range of addresses within the memory map, and represents a bridge to a subordinate bus.
Adding windows
Windows are added with MemoryMap.add_window()
, which returns a (start, end, ratio)
tuple describing their address range:
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
reg_rx_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
reg_tx_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
memory_map = MemoryMap(addr_width=14, data_width=32)
rx_window = MemoryMap(addr_width=12, data_width=32)
tx_window = MemoryMap(addr_width=12, data_width=32)
>>> memory_map.add_resource(reg_ctrl, size=1, name=("ctrl",))
(0, 1)
>>> rx_window.add_resource(reg_rx_data, size=1, name=("data",))
(0, 1)
>>> memory_map.add_window(rx_window, name=("rx",))
(4096, 8192, 1)
The third value returned by MemoryMap.add_window()
represents the number of addresses that are accessed in the bus described by rx_window
for one transaction in the bus described by memory_map
. It is 1 in this case, as both busses have the same width.
>>> tx_window.add_resource(reg_tx_data, size=1, name=("data",))
(0, 1)
>>> memory_map.add_window(tx_window, name=("tx",))
(8192, 12288, 1)
Accessing windows
Memory map windows can be iterated with MemoryMap.windows()
:
>>> for window, name, (start, end, ratio) in memory_map.windows():
... print(f"{name}, start={start:#x}, end={end:#x}, ratio={ratio}")
Name('rx'), start=0x1000, end=0x2000, ratio=1
Name('tx'), start=0x2000, end=0x3000, ratio=1
Windows can also be iterated with MemoryMap.window_patterns()
, which encodes their address ranges as bit patterns compatible with the match operator and the Case block:
>>> for window, name, (pattern, ratio) in memory_map.window_patterns():
... print(f"{name}, pattern='{pattern}', ratio={ratio}")
Name('rx'), pattern='01------------', ratio=1
Name('tx'), pattern='10------------', ratio=1
Memory map resources can be recursively iterated with MemoryMap.all_resources()
, which yields instances of ResourceInfo
:
>>> for res_info in memory_map.all_resources():
... print(res_info)
ResourceInfo(path=(Name('ctrl'),), start=0x0, end=0x1, width=32)
ResourceInfo(path=(Name('rx'), Name('data')), start=0x1000, end=0x1001, width=32)
ResourceInfo(path=(Name('tx'), Name('data')), start=0x2000, end=0x2001, width=32)
Address translation
When a memory map resource is accessed through a window, address translation may happen in three different modes.
Transparent mode
In transparent mode, each transaction on the primary bus results in one transaction on the subordinate bus without loss of data. This mode is selected when MemoryMap.add_window()
is given sparse=None
, which will fail if the window and the memory map have a different data widths.
Note
In practice, transparent mode is identical to other modes; it can only be used with equal data widths, which results in the same behavior regardless of the translation mode. However, it causes MemoryMap.add_window()
to fail if the data widths are different.
Sparse mode
In sparse mode, each transaction on the wide primary bus results in one transaction on the narrow subordinate bus. High data bits on the primary bus are ignored, and any contiguous resource on the subordinate bus becomes discontiguous on the primary bus. This mode is selected when MemoryMap.add_window()
is given sparse=True
.
Dense mode
In dense mode, each transaction on the wide primary bus results in several transactions on the narrow subordinate bus, and any contiguous resource on the subordinate bus stays contiguous on the primary bus. This mode is selected when MemoryMap.add_window()
is given sparse=False
.
Freezing
The state of a memory map can become immutable by calling MemoryMap.freeze()
:
memory_map = MemoryMap(addr_width=3, data_width=8)
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
>>> memory_map.freeze()
>>> memory_map.add_resource(reg_ctrl, size=4, addr=0x0, name=("ctrl",))
Traceback (most recent call last):
...
ValueError: Memory map has been frozen. Cannot add resource <...>
It is recommended to freeze a memory map before passing it to external logic, as a preventive measure against TOCTTOU bugs.
- class amaranth_soc.memory.MemoryMap
- freeze()
Freeze the
MemoryMap
.Once the
MemoryMap
is frozen, its visible state becomes immutable. Resources and windows cannot be added anymore.
- align_to(alignment)
Align the implicit next address.
- add_resource(resource, *, name, size, addr=None, alignment=None)
Add a resource.
A resource is any device on the bus that is a destination for bus transactions, e.g. a register or a memory block.
- Parameters:
resource (
amaranth.lib.wiring.Component
) – The resource to be added.name (
MemoryMap.Name
) – Name of the resource. It must not conflict with the name of other resources or windows present in this memory map.addr (
int
) – Address of the resource. Optional. IfNone
, the implicit next address will be used. Otherwise, the exact specified address (which must be a multiple of2 ** max(alignment, self.alignment)
) will be used.size (
int
) – Size of the resource, in minimal addressable units. Rounded up to a multiple of2 ** max(alignment, self.alignment)
.alignment (
int
, power-of-2 exponent) – Alignment of the resource. Optional. IfNone
, the memory map alignment is used.
- Returns:
A tuple
(start, end)
describing the address range assigned to the resource.- Return type:
- Raises:
ValueError – If the memory map is frozen.
ValueError – If the requested address and size, after alignment, would overlap with any resources or windows that have already been added, or would be out of bounds.
ValueError – If
resource
has already been added to this memory map.ValueError – If the requested name would conflict with the name of other resources or windows that have already been added.
- resources()
Iterate local resources and their address ranges.
Non-recursively iterate resources in ascending order of their address.
- Yields:
tuple
of (amaranth.lib.wiring.Component
,MemoryMap.Name
,tuple
of (int
,int
)) – A tupleresource, name, (start, end)
describing the address range assigned to the resource.
- add_window(window, *, name=None, addr=None, sparse=None)
Add a window.
A window is a device on a bus that provides access to a different bus, i.e. a bus bridge. It performs address translation, such that the devices on a subordinate bus have different addresses; the memory map reflects this address translation when resources are looked up through the window.
- Parameters:
window (
MemoryMap
) – AMemoryMap
describing the layout of the window. It is frozen as a side-effect of being added to this memory map.name (
MemoryMap.Name
) – Name of the window. Optional. It must not conflict with the name of other resources or windows present in this memory map.addr (
int
) – Address of the window. Optional. IfNone
, the implicit next address will be used after aligning it to2 ** window.addr_width
. Otherwise, the exact specified address (which must be a multiple of2 ** window.addr_width
) will be used.sparse (
bool
) – Address translation type. Optional. Ignored if the datapath widths of both memory maps are equal; must be specified otherwise.
- Returns:
A tuple
(start, end, ratio)
describing the address range assigned to the window. When bridging buses of unequal data width,ratio
is the amount of contiguous addresses on the narrower bus that are accessed for each transaction on the wider bus. Otherwise, it is always 1.- Return type:
- Raises:
ValueError – If the memory map is frozen.
ValueError – If the requested address and size, after alignment, would overlap with any resources or windows that have already been added, or would be out of bounds.
ValueError – If
window.data_width
is wider thandata_width
.ValueError – If the address translation mode is unspecified and
window.data_width
is different thandata_width
.ValueError – If dense address translation is used and
data_width
is not an integer multiple ofwindow.data_width
.ValueError – If dense address translation is used and the ratio of
data_width
towindow.data_width
is not a power of 2.ValueError – If dense address translation is used and the ratio of
data_width
towindow.data_width
is lesser than 2 raised to the power ofalignment
.ValueError – If the requested name would conflict with the name of other resources or windows that have already been added.
ValueError – If
window
is anonymous and the name of one of its resources or windows would conflict with the name of any resources or windows that have already been added.
- windows()
Iterate local windows and their address ranges.
Non-recursively iterate windows in ascending order of their address.
- Yields:
tuple
of (MemoryMap
,MemoryMap.Name
,tuple
of (int
,int
,int
)) – A tuplewindow, name, (start, end, ratio)
describing the address range assigned to the window. When bridging busses of unequal data widths,ratio
is the amount of contiguous addresses on the narrower bus that are accessed for each transaction on the wider bus. Otherwise, it is always 1.
- window_patterns()
Iterate local windows and patterns that match their address ranges.
Non-recursively iterate windows in ascending order of their address.
- Yields:
tuple
of (MemoryMap
,MemoryMap.Name
,tuple
of (str
,int
)) – A tuplewindow, name, (pattern, ratio)
describing the address range assigned to the window.pattern
is aaddr_width
wide pattern that may be used inCase
ormatch
to determine if a value is within the address range ofwindow
. When bridging busses of unequal data widths,ratio
is the amount of contiguous addresses on the narrower bus that are accessed for each transaction on the wider bus. Otherwise, it is always 1.
- all_resources()
Iterate all resources and their address ranges.
Recursively iterate all resources in ascending order of their address, performing address translation for resources that are located behind a window.
- Yields:
ResourceInfo
– A description of the resource and its address range.
- find_resource(resource)
Find address range corresponding to a resource.
Recursively find the address range of a resource, performing address translation for resources that are located behind a window.
- Parameters:
resource (
amaranth.lib.wiring.Component
) – Resource previously added to thisMemoryMap
or one of its windows.- Returns:
A description of the resource and its address range.
- Return type:
- Raises:
KeyError – If the resource is not found.
- decode_address(address)
Decode an address to a resource.
- Parameters:
address (
int
) – Address of interest.- Returns:
A resource mapped to the provided address, or
None
if there is no such resource.- Return type:
amaranth.lib.wiring.Component
orNone
- class amaranth_soc.memory.ResourceInfo
Resource metadata.
A description of a
MemoryMap
resource with its assigned path and address range.- Parameters:
resource (
amaranth.lib.wiring.Component
) – A resource located in theMemoryMap
. SeeMemoryMap.add_resource()
for details.path (
tuple
ofMemoryMap.Name
) – Path of the resource. It is composed of the names of each window sitting between the resource and theMemoryMap
from which thisResourceInfo
was obtained. SeeMemoryMap.add_window()
for details.start (
int
) – Start of the address range assigned to the resource.end (
int
) – End of the address range assigned to the resource.width (
int
) – Amount of data bits accessed at each address. It may be equal to the data width of theMemoryMap
from which thisResourceInfo
was obtained, or less if the resource is located behind a window that uses sparse addressing.