Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1925de0

Browse files
committedMay 21, 2024
docs/memory: add guide-level documentation.
1 parent fc9c6cf commit 1925de0

File tree

2 files changed

+258
-45
lines changed

2 files changed

+258
-45
lines changed
 

‎amaranth_soc/memory.py

+19-36
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ def width(self):
195195
"""
196196
return self._width
197197

198+
def __repr__(self):
199+
return f"ResourceInfo(path={self.path}, start={self.start:#x}, end={self.end:#x}, " \
200+
f"width={self.width})"
201+
198202

199203
class MemoryMap:
200204
"""Memory map.
@@ -206,13 +210,6 @@ class MemoryMap:
206210
(range allocations for bus bridges), and can be queried later to determine the address of
207211
any given resource from a specific vantage point in the design.
208212
209-
.. note::
210-
211-
To simplify address assignment, each :class:`MemoryMap` has an implicit next address,
212-
starting at 0. If a resource or a window is added without specifying an address explicitly,
213-
the implicit next address is used. In any case, the implicit next address is set to the
214-
address immediately following the newly added resource or window.
215-
216213
Arguments
217214
---------
218215
addr_width : :class:`int`
@@ -304,7 +301,7 @@ def _align_up(value, alignment):
304301
return value
305302

306303
def align_to(self, alignment):
307-
"""Align the implicit next address.
304+
"""Align the :ref:`implicit next address <memory-implicit-next-address>`.
308305
309306
Arguments
310307
---------
@@ -372,14 +369,14 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
372369
name : :class:`tuple` of (:class:`str`)
373370
Name of the resource. It must not conflict with the name of other resources or windows
374371
present in this memory map.
375-
addr : int
376-
Address of the resource. Optional. If ``None``, the implicit next address will be used.
377-
Otherwise, the exact specified address (which must be a multiple of
378-
``2 ** max(alignment, self.alignment)``) will be used.
379-
size : int
372+
addr : :class:`int`
373+
Address of the resource. Optional. If ``None``, the :ref:`implicit next address
374+
<memory-implicit-next-address>` will be used. Otherwise, the exact specified address
375+
(which must be a multiple of ``2 ** max(alignment, self.alignment)``) will be used.
376+
size : :class:`int`
380377
Size of the resource, in minimal addressable units. Rounded up to a multiple of
381378
``2 ** max(alignment, self.alignment)``.
382-
alignment : int, power-of-2 exponent
379+
alignment : :class:`int`, power-of-2 exponent
383380
Alignment of the resource. Optional. If ``None``, the memory map alignment is used.
384381
385382
Returns
@@ -390,17 +387,15 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
390387
Raises
391388
------
392389
:exc:`ValueError`
393-
If the :class:`MemoryMap` is frozen.
394-
:exc:`TypeError`
395-
If the resource is not a :class:`wiring.Component`.
390+
If the memory map is frozen.
396391
:exc:`ValueError`
397392
If the requested address and size, after alignment, would overlap with any resources or
398393
windows that have already been added, or would be out of bounds.
399394
:exc:`ValueError`
400-
If the resource has already been added to this :class:`MemoryMap`.
395+
If ``resource`` has already been added to this memory map.
401396
:exc:`ValueError`
402-
If the resource name conflicts with the name of other resources or windows present in
403-
this :class:`MemoryMap`.
397+
If the requested name would conflict with the name of other resources or windows that
398+
have already been added.
404399
"""
405400
if self._frozen:
406401
raise ValueError(f"Memory map has been frozen. Cannot add resource {resource!r}")
@@ -466,28 +461,16 @@ def add_window(self, window, *, addr=None, sparse=None):
466461
addresses; the memory map reflects this address translation when resources are looked up
467462
through the window.
468463
469-
.. note::
470-
471-
If a narrow bus is bridged to a wide bus, the bridge can perform *sparse* or *dense*
472-
address translation.
473-
474-
In the sparse case, each transaction on the wide bus results in one transaction on the
475-
narrow bus; high data bits on the wide bus are ignored, and any contiguous resource on
476-
the narrow bus becomes discontiguous on the wide bus.
477-
478-
In the dense case, each transaction on the wide bus results in several transactions on
479-
the narrow bus, and any contiguous resource on the narrow bus stays contiguous on the
480-
wide bus.
481-
482464
Arguments
483465
---------
484466
window : :class:`MemoryMap`
485467
A :class:`MemoryMap` describing the layout of the window. It is frozen as a side-effect
486468
of being added to this memory map.
487469
addr : :class:`int`
488-
Address of the window. Optional. If ``None``, the implicit next address will be used
489-
after aligning it to ``2 ** window.addr_width``. Otherwise, the exact specified address
490-
(which must be a multiple of ``2 ** window.addr_width``) will be used.
470+
Address of the window. Optional. If ``None``, the :ref:`implicit next address
471+
<memory-implicit-next-address>` will be used after aligning it to
472+
``2 ** window.addr_width``. Otherwise, the exact specified address (which must be a
473+
multiple of ``2 ** window.addr_width``) will be used.
491474
sparse : :class:`bool`
492475
Address translation type. Optional. Ignored if the datapath widths of both memory maps
493476
are equal; must be specified otherwise.

‎docs/memory.rst

+239-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,250 @@
11
Memory maps
2-
===========
3-
4-
.. warning::
5-
6-
This manual is a work in progress and is seriously incomplete!
2+
###########
73

84
.. py:module:: amaranth_soc.memory
95
106
The :mod:`amaranth_soc.memory` module provides primitives for organizing the address space of a bus interface.
117

8+
.. testsetup::
9+
10+
from amaranth import *
11+
12+
from amaranth_soc import csr
13+
from amaranth_soc.memory import *
14+
15+
.. _memory-introduction:
16+
17+
Introduction
18+
============
19+
20+
The purpose of :class:`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 :ref:`resources <memory-resources>` (representing registers, memories, etc) and :ref:`windows <memory-windows>` (representing bus bridges), and may be :ref:`queried <memory-accessing-windows>` afterwards in order to enumerate its contents, or determine the address of a resource.
21+
22+
.. _memory-resources:
23+
24+
Resources
25+
=========
26+
27+
A *resource* is a :class:`~amaranth.lib.wiring.Component` previously added to a :class:`MemoryMap`. Each resource occupies an unique range of addresses within the memory map, and represents a device that is a target for bus transactions.
28+
29+
Adding resources
30+
++++++++++++++++
31+
32+
Resources are added with :meth:`MemoryMap.add_resource`, which returns a ``(start, end)`` tuple describing their address range:
33+
34+
.. testcode::
35+
36+
memory_map = MemoryMap(addr_width=3, data_width=8)
37+
38+
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
39+
reg_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
40+
41+
.. doctest::
42+
43+
>>> memory_map.add_resource(reg_ctrl, size=4, addr=0x0, name=("ctrl",))
44+
(0, 4)
45+
>>> memory_map.add_resource(reg_data, size=4, addr=0x4, name=("data",))
46+
(4, 8)
47+
48+
.. _memory-implicit-next-address:
49+
50+
.. note::
51+
52+
The ``addr`` parameter of :meth:`MemoryMap.add_resource` and :meth:`MemoryMap.add_window` is optional.
53+
54+
To simplify address assignment, each :class:`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.
55+
56+
Accessing resources
57+
+++++++++++++++++++
58+
59+
Memory map resources can be iterated with :meth:`MemoryMap.resources`:
60+
61+
.. doctest::
62+
63+
>>> for resource, name, (start, end) in memory_map.resources():
64+
... print(f"name={name}, start={start:#x}, end={end:#x}, resource={resource}")
65+
name=('ctrl',), start=0x0, end=0x4, resource=<...>
66+
name=('data',), start=0x4, end=0x8, resource=<...>
67+
68+
A memory map can be queried with :meth:`MemoryMap.find_resource` to get the name and address range of a given resource:
69+
70+
.. doctest::
71+
72+
>>> memory_map.find_resource(reg_ctrl)
73+
ResourceInfo(path=(('ctrl',),), start=0x0, end=0x4, width=8)
74+
75+
The resource located at a given address can be retrieved with :meth:`MemoryMap.decode_address`:
76+
77+
.. doctest::
78+
79+
>>> memory_map.decode_address(0x4) is reg_data
80+
True
81+
82+
Alignment
83+
=========
84+
85+
The value of :attr:`MemoryMap.alignment` constrains the layout of a memory map. If unspecified, it defaults to 0.
86+
87+
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``.
88+
89+
For example, the resources of this memory map are 64-bit aligned:
90+
91+
.. testcode::
92+
93+
memory_map = MemoryMap(addr_width=8, data_width=8, alignment=3)
94+
95+
reg_foo = csr.Register(csr.Field(csr.action.RW, 32), "rw")
96+
reg_bar = csr.Register(csr.Field(csr.action.RW, 32), "rw")
97+
reg_baz = csr.Register(csr.Field(csr.action.RW, 32), "rw")
98+
99+
.. doctest::
100+
101+
>>> memory_map.add_resource(reg_foo, size=4, name=("foo",))
102+
(0, 8)
103+
>>> memory_map.add_resource(reg_bar, size=4, name=("bar",), addr=0x9)
104+
Traceback (most recent call last):
105+
...
106+
ValueError: Explicitly specified address 0x9 must be a multiple of 0x8 bytes
107+
108+
:meth:`MemoryMap.add_resource` takes an optional ``alignment`` parameter. If a value greater than :attr:`MemoryMap.alignment` is given, it becomes the alignment of this resource:
109+
110+
.. doctest::
111+
112+
>>> memory_map.add_resource(reg_bar, size=4, name=("bar",), alignment=4)
113+
(16, 32)
114+
115+
:meth:`MemoryMap.align_to` can be used to align the :ref:`implicit next address <memory-implicit-next-address>`. Its alignment is modified if a value greater than :attr:`MemoryMap.alignment` is given.
116+
117+
.. doctest::
118+
119+
>>> memory_map.align_to(6)
120+
64
121+
>>> memory_map.add_resource(reg_baz, size=4, name=("baz",))
122+
(64, 72)
123+
124+
.. note:: :meth:`MemoryMap.align_to` has no effect on the size of the next resource or window.
125+
126+
.. _memory-windows:
127+
128+
Windows
129+
=======
130+
131+
A *window* is a :class:`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.
132+
133+
Adding windows
134+
++++++++++++++
135+
136+
Windows are added with :meth:`MemoryMap.add_window`, which returns a ``(start, end, ratio)`` tuple describing their address range:
137+
138+
.. testcode::
139+
140+
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
141+
reg_rx_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
142+
reg_tx_data = csr.Register(csr.Field(csr.action.RW, 32), "rw")
143+
144+
memory_map = MemoryMap(addr_width=14, data_width=32)
145+
rx_window = MemoryMap(addr_width=12, data_width=32, name="rx")
146+
tx_window = MemoryMap(addr_width=12, data_width=32, name="tx")
147+
148+
.. doctest::
149+
150+
>>> memory_map.add_resource(reg_ctrl, size=1, name=("ctrl",))
151+
(0, 1)
152+
153+
>>> rx_window.add_resource(reg_rx_data, size=1, name=("data",))
154+
(0, 1)
155+
>>> memory_map.add_window(rx_window)
156+
(4096, 8192, 1)
157+
158+
The third value returned by :meth:`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.
159+
160+
.. doctest::
161+
162+
>>> tx_window.add_resource(reg_tx_data, size=1, name=("data",))
163+
(0, 1)
164+
>>> memory_map.add_window(tx_window)
165+
(8192, 12288, 1)
166+
167+
.. _memory-accessing-windows:
168+
169+
Accessing windows
170+
-----------------
171+
172+
Memory map windows can be iterated with :meth:`MemoryMap.windows`:
173+
174+
.. doctest::
175+
176+
>>> for window, (start, end, ratio) in memory_map.windows():
177+
... print(f"{window}, start={start:#x}, end={end:#x}, ratio={ratio}")
178+
MemoryMap(name='rx'), start=0x1000, end=0x2000, ratio=1
179+
MemoryMap(name='tx'), start=0x2000, end=0x3000, ratio=1
180+
181+
Windows can also be iterated with :meth:`MemoryMap.window_patterns`, which encodes their address ranges as bit patterns compatible with the :ref:`match operator <lang-matchop>` and the :ref:`Case block <lang-switch>`:
182+
183+
.. doctest::
184+
185+
>>> for window, (pattern, ratio) in memory_map.window_patterns():
186+
... print(f"{window}, pattern='{pattern}', ratio={ratio}")
187+
MemoryMap(name='rx'), pattern='01------------', ratio=1
188+
MemoryMap(name='tx'), pattern='10------------', ratio=1
189+
190+
Memory map resources can be recursively iterated with :meth:`MemoryMap.all_resources`, which yields instances of :class:`ResourceInfo`:
191+
192+
.. doctest::
193+
194+
>>> for res_info in memory_map.all_resources():
195+
... print(res_info)
196+
ResourceInfo(path=(('ctrl',),), start=0x0, end=0x1, width=32)
197+
ResourceInfo(path=('rx', ('data',)), start=0x1000, end=0x1001, width=32)
198+
ResourceInfo(path=('tx', ('data',)), start=0x2000, end=0x2001, width=32)
199+
200+
Address translation
201+
+++++++++++++++++++
202+
203+
When a memory map resource is accessed through a window, address translation may happen in three different modes.
204+
205+
Transparent mode
206+
----------------
207+
208+
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 :meth:`MemoryMap.add_window` is given ``sparse=None``, which will fail if the window and the memory map have a different data widths.
209+
210+
.. note::
211+
212+
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 :meth:`MemoryMap.add_window` to fail if the data widths are different.
213+
214+
Sparse mode
215+
-----------
216+
217+
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 :meth:`MemoryMap.add_window` is given ``sparse=True``.
218+
219+
Dense mode
220+
----------
221+
222+
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 :meth:`MemoryMap.add_window` is given ``sparse=False``.
223+
224+
Freezing
225+
========
226+
227+
The state of a memory map can become immutable by calling :meth:`MemoryMap.freeze`:
228+
229+
.. testcode::
230+
231+
memory_map = MemoryMap(addr_width=3, data_width=8)
232+
233+
reg_ctrl = csr.Register(csr.Field(csr.action.RW, 32), "rw")
234+
235+
.. doctest::
236+
237+
>>> memory_map.freeze()
238+
>>> memory_map.add_resource(reg_ctrl, size=4, addr=0x0, name=("ctrl",))
239+
Traceback (most recent call last):
240+
...
241+
ValueError: Memory map has been frozen. Cannot add resource <...>
242+
243+
It is recommended to freeze a memory map before passing it to external logic, as a preventive measure against TOCTTOU bugs.
244+
12245
.. autoclass:: MemoryMap()
13246
:no-members:
14247

15-
.. autoattribute:: addr_width
16-
.. autoattribute:: data_width
17-
.. autoattribute:: alignment
18-
.. autoattribute:: name
19248
.. automethod:: freeze
20249
.. automethod:: align_to
21250
.. automethod:: add_resource
@@ -28,3 +257,4 @@ The :mod:`amaranth_soc.memory` module provides primitives for organizing the add
28257
.. automethod:: decode_address
29258

30259
.. autoclass:: ResourceInfo()
260+
:no-members:

0 commit comments

Comments
 (0)
Please sign in to comment.