Skip to content

Commit ec7d19e

Browse files
Jean-François Nguyenjfng
Jean-François Nguyen
authored andcommitted
memory: use a ResourceInfo data class to answer resource queries.
Breaking changes: * MemoryMap.all_resources() and MemoryMap.find_resource() return an instance of ResourceInfo describing the resource and its assigned name and address range, instead of a tuple. Fixes #20.
1 parent 9a7232b commit ec7d19e

File tree

3 files changed

+186
-38
lines changed

3 files changed

+186
-38
lines changed

nmigen_soc/memory.py

+86-22
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from nmigen.utils import bits_for
44

55

6-
__all__ = ["MemoryMap"]
6+
__all__ = ["ResourceInfo", "MemoryMap"]
77

88

99
class _RangeMap:
@@ -47,6 +47,71 @@ def items(self):
4747
yield key, self._values[key]
4848

4949

50+
class ResourceInfo:
51+
"""Resource metadata.
52+
53+
A wrapper class for resource objects, with their assigned name and address range.
54+
55+
Parameters
56+
----------
57+
resource : object
58+
Arbitrary object representing a resource. See :meth:`MemoryMap.add_resource` for details.
59+
name : iter(str)
60+
Name assigned to the resource. It is prefixed by the name of each window sitting between
61+
the resource and the memory map from which this :class:`ResourceInfo` was obtained.
62+
See :meth:`MemoryMap.add_window` for details.
63+
start : int
64+
Start of the address range assigned to the resource.
65+
end : int
66+
End of the address range assigned to the resource.
67+
width : int
68+
Amount of data bits accessed at each address. It may be equal to the data width of the
69+
memory map from which this :class:`ResourceInfo` was obtained, or less if the resource
70+
is located behind a window that uses sparse addressing.
71+
"""
72+
def __init__(self, resource, name, start, end, width):
73+
if isinstance(name, str):
74+
name = (name,)
75+
if not name or not all(isinstance(part, str) and part for part in name):
76+
raise TypeError("Name must be a non-empty sequence of non-empty strings, not {!r}"
77+
.format(name))
78+
if not isinstance(start, int) or start < 0:
79+
raise TypeError("Start address must be a non-negative integer, not {!r}"
80+
.format(start))
81+
if not isinstance(end, int) or end <= start:
82+
raise TypeError("End address must be an integer greater than the start address, "
83+
"not {!r}".format(end))
84+
if not isinstance(width, int) or width < 0:
85+
raise TypeError("Width must be a non-negative integer, not {!r}"
86+
.format(width))
87+
88+
self._resource = resource
89+
self._name = tuple(name)
90+
self._start = start
91+
self._end = end
92+
self._width = width
93+
94+
@property
95+
def resource(self):
96+
return self._resource
97+
98+
@property
99+
def name(self):
100+
return self._name
101+
102+
@property
103+
def start(self):
104+
return self._start
105+
106+
@property
107+
def end(self):
108+
return self._end
109+
110+
@property
111+
def width(self):
112+
return self._width
113+
114+
50115
class MemoryMap:
51116
"""Memory map.
52117
@@ -450,15 +515,17 @@ def window_patterns(self):
450515
yield window, (pattern, window_range.step)
451516

452517
@staticmethod
453-
def _translate(start, end, width, window, window_range):
454-
assert (end - start) % window_range.step == 0
518+
def _translate(resource_info, window, window_range):
519+
assert (resource_info.end - resource_info.start) % window_range.step == 0
455520
# Accessing a resource through a dense and then a sparse window results in very strange
456521
# layouts that cannot be easily represented, so reject those.
457-
assert window_range.step == 1 or width == window.data_width
458-
size = (end - start) // window_range.step
459-
start += window_range.start
460-
width *= window_range.step
461-
return start, start + size, width
522+
assert window_range.step == 1 or resource_info.width == window.data_width
523+
524+
name = resource_info.name if window.name is None else (window.name, *resource_info.name)
525+
size = (resource_info.end - resource_info.start) // window_range.step
526+
start = resource_info.start + window_range.start
527+
width = resource_info.width * window_range.step
528+
return ResourceInfo(resource_info.resource, name, start, start + size, width)
462529

463530
def all_resources(self):
464531
"""Iterate all resources and their address ranges.
@@ -468,17 +535,16 @@ def all_resources(self):
468535
469536
Yield values
470537
------------
471-
A tuple ``resource, (start, end, width)`` describing the address range assigned to
472-
the resource. ``width`` is the amount of data bits accessed at each address, which may be
473-
equal to ``self.data_width``, or less if the resource is located behind a window that
474-
uses sparse addressing.
538+
An instance of :class:`ResourceInfo` describing the resource and its address range.
475539
"""
476540
for addr_range, assignment in self._ranges.items():
477541
if id(assignment) in self._resources:
478-
yield assignment, (addr_range.start, addr_range.stop, self.data_width)
542+
_, resource_name, _ = self._resources[id(assignment)]
543+
yield ResourceInfo(assignment, resource_name, addr_range.start, addr_range.stop,
544+
self.data_width)
479545
elif id(assignment) in self._windows:
480-
for sub_resource, sub_descr in assignment.all_resources():
481-
yield sub_resource, self._translate(*sub_descr, assignment, addr_range)
546+
for resource_info in assignment.all_resources():
547+
yield self._translate(resource_info, assignment, addr_range)
482548
else:
483549
assert False # :nocov:
484550

@@ -495,22 +561,20 @@ def find_resource(self, resource):
495561
496562
Return value
497563
------------
498-
A tuple ``(start, end, width)`` describing the address range assigned to the resource.
499-
``width`` is the amount of data bits accessed at each address, which may be equal to
500-
``self.data_width``, or less if the resource is located behind a window that uses sparse
501-
addressing.
564+
An instance of :class:`ResourceInfo` describing the resource and its address range.
502565
503566
Exceptions
504567
----------
505568
Raises :exn:`KeyError` if the resource is not found.
506569
"""
507570
if id(resource) in self._resources:
508-
_, _, resource_range = self._resources[id(resource)]
509-
return resource_range.start, resource_range.stop, self.data_width
571+
_, resource_name, resource_range = self._resources[id(resource)]
572+
return ResourceInfo(resource, resource_name, resource_range.start, resource_range.stop,
573+
self.data_width)
510574

511575
for window, window_range in self._windows.values():
512576
try:
513-
return self._translate(*window.find_resource(resource), window, window_range)
577+
return self._translate(window.find_resource(resource), window, window_range)
514578
except KeyError:
515579
pass
516580

nmigen_soc/test/test_csr_bus.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,10 @@ def test_sim(self):
364364
mux_2.add(elem_2, addr=2)
365365
self.dut.add(mux_2.bus)
366366

367-
elem_1_addr, _, _ = self.dut.bus.memory_map.find_resource(elem_1)
368-
elem_2_addr, _, _ = self.dut.bus.memory_map.find_resource(elem_2)
367+
elem_1_info = self.dut.bus.memory_map.find_resource(elem_1)
368+
elem_2_info = self.dut.bus.memory_map.find_resource(elem_2)
369+
elem_1_addr = elem_1_info.start
370+
elem_2_addr = elem_2_info.start
369371
self.assertEqual(elem_1_addr, 0x0000)
370372
self.assertEqual(elem_2_addr, 0x0402)
371373

nmigen_soc/test/test_memory.py

+96-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
from ..memory import _RangeMap, MemoryMap
3+
from ..memory import _RangeMap, ResourceInfo, MemoryMap
44

55

66
class RangeMapTestCase(unittest.TestCase):
@@ -40,6 +40,54 @@ def test_get(self):
4040
self.assertEqual(range_map.get(15), None)
4141

4242

43+
class ResourceInfoTestCase(unittest.TestCase):
44+
def test_simple(self):
45+
info = ResourceInfo("a", name=("foo", "bar"), start=0, end=1, width=8)
46+
self.assertEqual(info.name, ("foo", "bar"))
47+
self.assertEqual(info.start, 0)
48+
self.assertEqual(info.end, 1)
49+
self.assertEqual(info.width, 8)
50+
51+
def test_name_cast(self):
52+
info = ResourceInfo("a", name="foo", start=0, end=1, width=8)
53+
self.assertEqual(info.name, ("foo",))
54+
55+
def test_wrong_name(self):
56+
with self.assertRaisesRegex(TypeError,
57+
r"Name must be a non-empty sequence of non-empty strings, not \(1,\)"):
58+
ResourceInfo("a", name=(1,), start=0, end=1, width=8)
59+
with self.assertRaisesRegex(TypeError,
60+
r"Name must be a non-empty sequence of non-empty strings, not \(\)"):
61+
ResourceInfo("a", name=(), start=0, end=1, width=8)
62+
with self.assertRaisesRegex(TypeError,
63+
r"Name must be a non-empty sequence of non-empty strings, not \('foo', ''\)"):
64+
ResourceInfo("a", name=("foo", ""), start=0, end=1, width=8)
65+
66+
def test_wrong_start_addr(self):
67+
with self.assertRaisesRegex(TypeError,
68+
r"Start address must be a non-negative integer, not 'foo'"):
69+
ResourceInfo("a", name="b", start="foo", end=1, width=8)
70+
with self.assertRaisesRegex(TypeError,
71+
r"Start address must be a non-negative integer, not -1"):
72+
ResourceInfo("a", name="b", start=-1, end=1, width=8)
73+
74+
def test_wrong_end_addr(self):
75+
with self.assertRaisesRegex(TypeError,
76+
r"End address must be an integer greater than the start address, not 'foo'"):
77+
ResourceInfo("a", name="b", start=0, end="foo", width=8)
78+
with self.assertRaisesRegex(TypeError,
79+
r"End address must be an integer greater than the start address, not 0"):
80+
ResourceInfo("a", name="b", start=0, end=0, width=8)
81+
82+
def test_wrong_width(self):
83+
with self.assertRaisesRegex(TypeError,
84+
r"Width must be a non-negative integer, not 'foo'"):
85+
ResourceInfo("a", name="b", start=0, end=1, width="foo")
86+
with self.assertRaisesRegex(TypeError,
87+
r"Width must be a non-negative integer, not -1"):
88+
ResourceInfo("a", name="b", start=0, end=1, width=-1)
89+
90+
4391
class MemoryMapTestCase(unittest.TestCase):
4492
def test_name(self):
4593
memory_map_0 = MemoryMap(addr_width=1, data_width=8)
@@ -375,28 +423,62 @@ def setUp(self):
375423
self.root.add_window(self.win3, sparse=False)
376424

377425
def test_iter_all_resources(self):
378-
self.assertEqual(list(self.root.all_resources()), [
379-
(self.res1, (0x00000000, 0x00000010, 32)),
380-
(self.res2, (0x00010000, 0x00010020, 32)),
381-
(self.res3, (0x00010020, 0x00010040, 32)),
382-
(self.res4, (0x00020000, 0x00020001, 32)),
383-
(self.res5, (0x00030000, 0x00030010, 8)),
384-
(self.res6, (0x00040000, 0x00040004, 32)),
385-
])
426+
res_info = list(self.root.all_resources())
427+
428+
self.assertIs(res_info[0].resource, self.res1)
429+
self.assertEqual(res_info[0].name, ("name1",))
430+
self.assertEqual(res_info[0].start, 0x00000000)
431+
self.assertEqual(res_info[0].end, 0x00000010)
432+
self.assertEqual(res_info[0].width, 32)
433+
434+
self.assertIs(res_info[1].resource, self.res2)
435+
self.assertEqual(res_info[1].name, ("name2",))
436+
self.assertEqual(res_info[1].start, 0x00010000)
437+
self.assertEqual(res_info[1].end, 0x00010020)
438+
self.assertEqual(res_info[1].width, 32)
439+
440+
self.assertIs(res_info[2].resource, self.res3)
441+
self.assertEqual(res_info[2].name, ("name3",))
442+
self.assertEqual(res_info[2].start, 0x00010020)
443+
self.assertEqual(res_info[2].end, 0x00010040)
444+
self.assertEqual(res_info[2].width, 32)
445+
446+
self.assertIs(res_info[3].resource, self.res4)
447+
self.assertEqual(res_info[3].name, ("name4",))
448+
self.assertEqual(res_info[3].start, 0x00020000)
449+
self.assertEqual(res_info[3].end, 0x00020001)
450+
self.assertEqual(res_info[3].width, 32)
451+
452+
self.assertIs(res_info[4].resource, self.res5)
453+
self.assertEqual(res_info[4].name, ("name5",))
454+
self.assertEqual(res_info[4].start, 0x00030000)
455+
self.assertEqual(res_info[4].end, 0x00030010)
456+
self.assertEqual(res_info[4].width, 8)
457+
458+
self.assertIs(res_info[5].resource, self.res6)
459+
self.assertEqual(res_info[5].name, ("win3", "name6"))
460+
self.assertEqual(res_info[5].start, 0x00040000)
461+
self.assertEqual(res_info[5].end, 0x00040004)
462+
self.assertEqual(res_info[5].width, 32)
386463

387464
def test_find_resource(self):
388-
for res, loc in self.root.all_resources():
389-
self.assertEqual(self.root.find_resource(res), loc)
465+
for res_info in self.root.all_resources():
466+
other = self.root.find_resource(res_info.resource)
467+
self.assertIs(other.resource, res_info.resource)
468+
self.assertEqual(other.name, res_info.name)
469+
self.assertEqual(other.start, res_info.start)
470+
self.assertEqual(other.end, res_info.end)
471+
self.assertEqual(other.width, res_info.width)
390472

391473
def test_find_resource_wrong(self):
392474
with self.assertRaises(KeyError) as error:
393475
self.root.find_resource("resNA")
394476
self.assertEqual(error.exception.args, ("resNA",))
395477

396478
def test_decode_address(self):
397-
for res, (start, end, width) in self.root.all_resources():
398-
self.assertEqual(self.root.decode_address(start), res)
399-
self.assertEqual(self.root.decode_address(end - 1), res)
479+
for res_info in self.root.all_resources():
480+
self.assertEqual(self.root.decode_address(res_info.start), res_info.resource)
481+
self.assertEqual(self.root.decode_address(res_info.end - 1), res_info.resource)
400482

401483
def test_decode_address_missing(self):
402484
self.assertIsNone(self.root.decode_address(address=0x00000100))

0 commit comments

Comments
 (0)