cocotbext-umi 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cocotbext/umi/__init__.py +7 -0
- cocotbext/umi/drivers/__init__.py +0 -0
- cocotbext/umi/drivers/sumi_driver.py +92 -0
- cocotbext/umi/models/__init__.py +0 -0
- cocotbext/umi/models/umi_memory_device.py +123 -0
- cocotbext/umi/monitors/__init__.py +0 -0
- cocotbext/umi/monitors/sumi_monitor.py +68 -0
- cocotbext/umi/sumi.py +335 -0
- cocotbext/umi/tumi.py +55 -0
- cocotbext/umi/utils/bit_utils.py +90 -0
- cocotbext/umi/utils/generators.py +25 -0
- cocotbext/umi/utils/vrd_transaction.py +10 -0
- cocotbext_umi-0.0.2.dist-info/METADATA +485 -0
- cocotbext_umi-0.0.2.dist-info/RECORD +17 -0
- cocotbext_umi-0.0.2.dist-info/WHEEL +5 -0
- cocotbext_umi-0.0.2.dist-info/licenses/LICENSE +201 -0
- cocotbext_umi-0.0.2.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from cocotb.types import LogicArray
|
|
4
|
+
from cocotb.triggers import RisingEdge
|
|
5
|
+
from cocotb.handle import SimHandleBase
|
|
6
|
+
|
|
7
|
+
from cocotb_bus.drivers import ValidatedBusDriver
|
|
8
|
+
|
|
9
|
+
from cocotbext.umi.sumi import SumiTransaction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SumiDriver(ValidatedBusDriver):
|
|
13
|
+
|
|
14
|
+
_signals = [
|
|
15
|
+
"valid",
|
|
16
|
+
"cmd",
|
|
17
|
+
"dstaddr",
|
|
18
|
+
"srcaddr",
|
|
19
|
+
"data",
|
|
20
|
+
"ready"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_optional_signals = []
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
entity: SimHandleBase,
|
|
28
|
+
name: str,
|
|
29
|
+
clock: SimHandleBase,
|
|
30
|
+
*,
|
|
31
|
+
config={},
|
|
32
|
+
**kwargs: Any
|
|
33
|
+
):
|
|
34
|
+
ValidatedBusDriver.__init__(self, entity, name, clock, **kwargs)
|
|
35
|
+
|
|
36
|
+
self.clock = clock
|
|
37
|
+
self.bus.valid.value = 0
|
|
38
|
+
|
|
39
|
+
self.addr_width = len(self.bus.dstaddr)
|
|
40
|
+
self.data_width = len(self.bus.data)
|
|
41
|
+
|
|
42
|
+
def get_bus_width(self) -> int:
|
|
43
|
+
return self.data_width
|
|
44
|
+
|
|
45
|
+
def get_addr_width(self) -> int:
|
|
46
|
+
return self.addr_width
|
|
47
|
+
|
|
48
|
+
async def _driver_send(self, transaction: SumiTransaction, sync: bool = True) -> None:
|
|
49
|
+
"""Implementation for BusDriver.
|
|
50
|
+
Args:
|
|
51
|
+
transaction: The transaction to send.
|
|
52
|
+
sync: Synchronize the transfer by waiting for a rising edge.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
clk_re = RisingEdge(self.clock)
|
|
56
|
+
|
|
57
|
+
if sync:
|
|
58
|
+
await clk_re
|
|
59
|
+
|
|
60
|
+
# Insert a gap where valid is low
|
|
61
|
+
if not self.on:
|
|
62
|
+
self.bus.valid.value = 0
|
|
63
|
+
for _ in range(self.off):
|
|
64
|
+
await clk_re
|
|
65
|
+
|
|
66
|
+
# Grab the next set of on/off values
|
|
67
|
+
self._next_valids()
|
|
68
|
+
|
|
69
|
+
# Consume a valid cycle
|
|
70
|
+
if self.on is not True and self.on:
|
|
71
|
+
self.on -= 1
|
|
72
|
+
|
|
73
|
+
def ready() -> bool:
|
|
74
|
+
return bool(self.bus.ready.value)
|
|
75
|
+
|
|
76
|
+
bus_size = len(self.bus.data)//8
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
self.bus.valid.value = 1
|
|
80
|
+
self.bus.cmd.value = int(transaction.cmd)
|
|
81
|
+
self.bus.data.value = LogicArray.from_bytes(
|
|
82
|
+
value=transaction.data + bytearray([0]*(bus_size - len(transaction.data))),
|
|
83
|
+
range=len(self.bus.data),
|
|
84
|
+
byteorder="little"
|
|
85
|
+
)
|
|
86
|
+
self.bus.dstaddr.value = int(transaction.da)
|
|
87
|
+
self.bus.srcaddr.value = int(transaction.sa)
|
|
88
|
+
await clk_re
|
|
89
|
+
if ready():
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
self.bus.valid.value = 0
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from cocotbext.umi.sumi import SumiCmd, SumiCmdType, SumiTransaction
|
|
2
|
+
from cocotbext.umi.tumi import TumiTransaction
|
|
3
|
+
from cocotbext.umi.drivers.sumi_driver import SumiDriver
|
|
4
|
+
from cocotbext.umi.monitors.sumi_monitor import SumiMonitor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UmiMemoryDevice:
|
|
8
|
+
"""
|
|
9
|
+
Virtual memory device that responds to UMI read/write requests.
|
|
10
|
+
|
|
11
|
+
Uses a SumiMonitor to receive requests and a SumiDriver to send responses.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
monitor: SumiMonitor,
|
|
17
|
+
driver: SumiDriver,
|
|
18
|
+
log=None
|
|
19
|
+
):
|
|
20
|
+
self.monitor = monitor
|
|
21
|
+
self.driver = driver
|
|
22
|
+
self.log = log
|
|
23
|
+
self.memory: dict[int, int] = {}
|
|
24
|
+
|
|
25
|
+
self.dw = self.driver.get_bus_width()
|
|
26
|
+
self.aw = self.driver.get_addr_width()
|
|
27
|
+
|
|
28
|
+
self.monitor.add_callback(self._on_transaction)
|
|
29
|
+
|
|
30
|
+
def _on_transaction(self, transaction: SumiTransaction):
|
|
31
|
+
"""Callback invoked when the monitor receives a transaction."""
|
|
32
|
+
cmd_type = int(transaction.cmd.cmd_type)
|
|
33
|
+
|
|
34
|
+
if cmd_type == SumiCmdType.UMI_REQ_WRITE:
|
|
35
|
+
self._handle_write(transaction, send_response=True)
|
|
36
|
+
elif cmd_type == SumiCmdType.UMI_REQ_POSTED:
|
|
37
|
+
self._handle_write(transaction, send_response=False)
|
|
38
|
+
elif cmd_type == SumiCmdType.UMI_REQ_READ:
|
|
39
|
+
self._handle_read(transaction)
|
|
40
|
+
else:
|
|
41
|
+
if self.log:
|
|
42
|
+
self.log.warning(f"Unhandled UMI command type: 0x{cmd_type:02x}")
|
|
43
|
+
|
|
44
|
+
def _handle_write(self, transaction: SumiTransaction, send_response: bool = True):
|
|
45
|
+
"""Handle a write request by storing data and optionally sending response."""
|
|
46
|
+
dstaddr = int(transaction.da)
|
|
47
|
+
data = transaction.data
|
|
48
|
+
size = int(transaction.cmd.size)
|
|
49
|
+
length = int(transaction.cmd.len)
|
|
50
|
+
data_size = (length + 1) << size
|
|
51
|
+
|
|
52
|
+
if self.log:
|
|
53
|
+
self.log.info(
|
|
54
|
+
f"MEM WRITE: addr=0x{dstaddr:08x} size={data_size} "
|
|
55
|
+
f"data={data[:data_size].hex()}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
for i in range(data_size):
|
|
59
|
+
self.memory[dstaddr + i] = data[i]
|
|
60
|
+
|
|
61
|
+
if send_response:
|
|
62
|
+
resp_cmd = SumiCmd.from_fields(
|
|
63
|
+
cmd_type=SumiCmdType.UMI_RESP_WRITE,
|
|
64
|
+
size=0,
|
|
65
|
+
len=0,
|
|
66
|
+
eom=1
|
|
67
|
+
)
|
|
68
|
+
resp = SumiTransaction(
|
|
69
|
+
cmd=resp_cmd,
|
|
70
|
+
da=int(transaction.sa),
|
|
71
|
+
sa=int(transaction.da),
|
|
72
|
+
data=bytes([0]),
|
|
73
|
+
addr_width=transaction._addr_width
|
|
74
|
+
)
|
|
75
|
+
self.driver.append(resp)
|
|
76
|
+
|
|
77
|
+
def _handle_read(self, transaction: SumiTransaction):
|
|
78
|
+
"""Handle a read request by returning data from memory."""
|
|
79
|
+
srcaddr = int(transaction.da)
|
|
80
|
+
size = int(transaction.cmd.size)
|
|
81
|
+
length = int(transaction.cmd.len)
|
|
82
|
+
data_size = (length + 1) << size
|
|
83
|
+
|
|
84
|
+
data = bytes(self.memory.get(srcaddr + i, 0) for i in range(data_size))
|
|
85
|
+
|
|
86
|
+
if self.log:
|
|
87
|
+
self.log.info(
|
|
88
|
+
f"MEM READ: addr=0x{srcaddr:08x} size={data_size} "
|
|
89
|
+
f"data={data.hex()}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
resp_cmd = SumiCmd.from_fields(
|
|
93
|
+
cmd_type=SumiCmdType.UMI_RESP_READ,
|
|
94
|
+
size=size,
|
|
95
|
+
len=length,
|
|
96
|
+
eom=1
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
tumi_trans = TumiTransaction(
|
|
100
|
+
cmd=resp_cmd,
|
|
101
|
+
da=int(transaction.sa),
|
|
102
|
+
sa=int(transaction.da),
|
|
103
|
+
data=data
|
|
104
|
+
)
|
|
105
|
+
for sumi_trans in tumi_trans.to_sumi(data_bus_size=self.dw//8, addr_width=self.aw):
|
|
106
|
+
self.driver.append(sumi_trans)
|
|
107
|
+
|
|
108
|
+
def read(self, address: int, length: int = 1) -> bytes:
|
|
109
|
+
"""Read bytes from virtual memory directly."""
|
|
110
|
+
return bytes(self.memory.get(address + i, 0) for i in range(length))
|
|
111
|
+
|
|
112
|
+
def write(self, address: int, data: bytes):
|
|
113
|
+
"""Write bytes to virtual memory directly (for test setup)."""
|
|
114
|
+
for i, byte in enumerate(data):
|
|
115
|
+
self.memory[address + i] = byte
|
|
116
|
+
|
|
117
|
+
def dump_memory(self) -> list[tuple[int, int]]:
|
|
118
|
+
"""Return a sorted list of (address, value) tuples."""
|
|
119
|
+
return sorted(self.memory.items())
|
|
120
|
+
|
|
121
|
+
def clear(self):
|
|
122
|
+
"""Clear all memory contents."""
|
|
123
|
+
self.memory.clear()
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from cocotb.types import LogicArray
|
|
2
|
+
from cocotb.triggers import RisingEdge
|
|
3
|
+
|
|
4
|
+
from cocotb_bus.monitors import BusMonitor
|
|
5
|
+
|
|
6
|
+
from cocotbext.umi.sumi import SumiCmd, SumiTransaction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SumiMonitor(BusMonitor):
|
|
10
|
+
|
|
11
|
+
_signals = [
|
|
12
|
+
"valid",
|
|
13
|
+
"cmd",
|
|
14
|
+
"dstaddr",
|
|
15
|
+
"srcaddr",
|
|
16
|
+
"data",
|
|
17
|
+
"ready"
|
|
18
|
+
]
|
|
19
|
+
_optional_signals = []
|
|
20
|
+
|
|
21
|
+
def __init__(self, entity, name, clock, **kwargs):
|
|
22
|
+
BusMonitor.__init__(self, entity, name, clock, **kwargs)
|
|
23
|
+
self.addr_width = len(self.bus.dstaddr)
|
|
24
|
+
self.data_width = len(self.bus.data)
|
|
25
|
+
|
|
26
|
+
def get_bus_width(self) -> int:
|
|
27
|
+
return self.data_width
|
|
28
|
+
|
|
29
|
+
def get_addr_width(self) -> int:
|
|
30
|
+
return self.addr_width
|
|
31
|
+
|
|
32
|
+
async def _monitor_recv(self):
|
|
33
|
+
clk_re = RisingEdge(self.clock)
|
|
34
|
+
|
|
35
|
+
def valid_handshake() -> bool:
|
|
36
|
+
return bool(self.bus.valid.value) and bool(self.bus.ready.value)
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
await clk_re
|
|
40
|
+
|
|
41
|
+
if self.in_reset:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
if valid_handshake():
|
|
45
|
+
sumi_cmd: SumiCmd = SumiCmd.from_int(int(self.bus.cmd.value))
|
|
46
|
+
|
|
47
|
+
# Only slice data if the command type carries data
|
|
48
|
+
if sumi_cmd.has_data():
|
|
49
|
+
data: LogicArray = self.bus.data.value
|
|
50
|
+
# Limit slice to actual data size, but don't exceed bus width
|
|
51
|
+
data_bits = min(
|
|
52
|
+
(((int(sumi_cmd.len) + 1) << int(sumi_cmd.size)) * 8),
|
|
53
|
+
self.data_width
|
|
54
|
+
)
|
|
55
|
+
data = data[data_bits - 1:0]
|
|
56
|
+
data_bytes = data.to_bytes(byteorder="little") if data.is_resolvable else None
|
|
57
|
+
else:
|
|
58
|
+
data_bytes = None
|
|
59
|
+
|
|
60
|
+
self._recv(SumiTransaction(
|
|
61
|
+
cmd=sumi_cmd,
|
|
62
|
+
da=int(self.bus.dstaddr.value) if self.bus.dstaddr.value.is_resolvable
|
|
63
|
+
else None,
|
|
64
|
+
sa=int(self.bus.srcaddr.value) if self.bus.srcaddr.value.is_resolvable
|
|
65
|
+
else None,
|
|
66
|
+
data=data_bytes,
|
|
67
|
+
addr_width=self.addr_width
|
|
68
|
+
))
|
cocotbext/umi/sumi.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import dataclasses
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
from cocotbext.umi.utils.bit_utils import BitField, BitVector
|
|
7
|
+
from cocotbext.umi.utils.vrd_transaction import VRDTransaction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SumiCmdType(IntEnum):
|
|
11
|
+
"""UMI Command Opcodes (CMD[4:0])"""
|
|
12
|
+
# Invalid transaction indicator
|
|
13
|
+
UMI_INVALID = 0x00
|
|
14
|
+
|
|
15
|
+
# Requests (host -> device)
|
|
16
|
+
UMI_REQ_READ = 0x01 # read/load
|
|
17
|
+
UMI_REQ_WRITE = 0x03 # write/store with ack
|
|
18
|
+
UMI_REQ_POSTED = 0x05 # posted write (no response)
|
|
19
|
+
UMI_REQ_RDMA = 0x07 # remote DMA command
|
|
20
|
+
UMI_REQ_ATOMIC = 0x09 # atomic read-modify-write
|
|
21
|
+
UMI_REQ_USER0 = 0x0B # reserved for user
|
|
22
|
+
UMI_REQ_FUTURE0 = 0x0D # reserved for future use
|
|
23
|
+
UMI_REQ_ERROR = 0x0F # error message (SIZE=0x0)
|
|
24
|
+
UMI_REQ_LINK = 0x2F # link ctrl (SIZE=0x1)
|
|
25
|
+
|
|
26
|
+
# Responses (device -> host)
|
|
27
|
+
UMI_RESP_READ = 0x02 # response to read request (with data)
|
|
28
|
+
UMI_RESP_WRITE = 0x04 # response (ack) from write request
|
|
29
|
+
UMI_RESP_USER0 = 0x06 # reserved for user (no data)
|
|
30
|
+
UMI_RESP_USER1 = 0x08 # reserved for user (with data)
|
|
31
|
+
UMI_RESP_FUTURE0 = 0x0A # reserved for future use (no data)
|
|
32
|
+
UMI_RESP_FUTURE1 = 0x0C # reserved for future use (with data)
|
|
33
|
+
UMI_RESP_LINK = 0x0E # link ctrl response (SIZE=0x0)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def supports_streaming(cls, value):
|
|
37
|
+
"""Check if the command type supports streaming (multiple data transfers)."""
|
|
38
|
+
return value in [
|
|
39
|
+
SumiCmdType.UMI_REQ_WRITE,
|
|
40
|
+
SumiCmdType.UMI_REQ_POSTED,
|
|
41
|
+
SumiCmdType.UMI_RESP_READ
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def is_request(cls, value):
|
|
46
|
+
"""Check if the command is a request (host -> device)."""
|
|
47
|
+
return value in [
|
|
48
|
+
SumiCmdType.UMI_REQ_READ,
|
|
49
|
+
SumiCmdType.UMI_REQ_WRITE,
|
|
50
|
+
SumiCmdType.UMI_REQ_POSTED,
|
|
51
|
+
SumiCmdType.UMI_REQ_RDMA,
|
|
52
|
+
SumiCmdType.UMI_REQ_ATOMIC,
|
|
53
|
+
SumiCmdType.UMI_REQ_USER0,
|
|
54
|
+
SumiCmdType.UMI_REQ_FUTURE0,
|
|
55
|
+
SumiCmdType.UMI_REQ_ERROR,
|
|
56
|
+
SumiCmdType.UMI_REQ_LINK,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def is_response(cls, value):
|
|
61
|
+
"""Check if the command is a response (device -> host)."""
|
|
62
|
+
return value in [
|
|
63
|
+
SumiCmdType.UMI_RESP_READ,
|
|
64
|
+
SumiCmdType.UMI_RESP_WRITE,
|
|
65
|
+
SumiCmdType.UMI_RESP_USER0,
|
|
66
|
+
SumiCmdType.UMI_RESP_USER1,
|
|
67
|
+
SumiCmdType.UMI_RESP_FUTURE0,
|
|
68
|
+
SumiCmdType.UMI_RESP_FUTURE1,
|
|
69
|
+
SumiCmdType.UMI_RESP_LINK,
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def has_data(cls, value):
|
|
74
|
+
"""Check if the command type carries data."""
|
|
75
|
+
return value in [
|
|
76
|
+
SumiCmdType.UMI_REQ_WRITE,
|
|
77
|
+
SumiCmdType.UMI_REQ_POSTED,
|
|
78
|
+
SumiCmdType.UMI_REQ_ATOMIC,
|
|
79
|
+
SumiCmdType.UMI_REQ_USER0,
|
|
80
|
+
SumiCmdType.UMI_REQ_FUTURE0,
|
|
81
|
+
SumiCmdType.UMI_RESP_READ,
|
|
82
|
+
SumiCmdType.UMI_RESP_USER1,
|
|
83
|
+
SumiCmdType.UMI_RESP_FUTURE1,
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def has_source_addr(cls, value):
|
|
88
|
+
"""Check if the command type includes source address (SA)."""
|
|
89
|
+
return value in [
|
|
90
|
+
SumiCmdType.UMI_REQ_READ,
|
|
91
|
+
SumiCmdType.UMI_REQ_WRITE,
|
|
92
|
+
SumiCmdType.UMI_REQ_POSTED,
|
|
93
|
+
SumiCmdType.UMI_REQ_RDMA,
|
|
94
|
+
SumiCmdType.UMI_REQ_ATOMIC,
|
|
95
|
+
SumiCmdType.UMI_REQ_USER0,
|
|
96
|
+
SumiCmdType.UMI_REQ_FUTURE0,
|
|
97
|
+
SumiCmdType.UMI_REQ_ERROR,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SumiAtomicType(IntEnum):
|
|
102
|
+
"""Atomic Transaction Types (ATYPE[7:0]) - used in LEN field for REQ_ATOMIC"""
|
|
103
|
+
UMI_ATOMIC_ADD = 0x00
|
|
104
|
+
UMI_ATOMIC_AND = 0x01
|
|
105
|
+
UMI_ATOMIC_OR = 0x02
|
|
106
|
+
UMI_ATOMIC_XOR = 0x03
|
|
107
|
+
UMI_ATOMIC_MAX = 0x04
|
|
108
|
+
UMI_ATOMIC_MIN = 0x05
|
|
109
|
+
UMI_ATOMIC_MAXU = 0x06
|
|
110
|
+
UMI_ATOMIC_MINU = 0x07
|
|
111
|
+
UMI_ATOMIC_SWAP = 0x08
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SumiErrorCode(IntEnum):
|
|
115
|
+
"""Error Codes (ERR[1:0]) - used in U field for responses"""
|
|
116
|
+
UMI_ERR_OK = 0b00 # OK (no error)
|
|
117
|
+
UMI_ERR_EXOK = 0b01 # Successful exclusive access
|
|
118
|
+
UMI_ERR_DEVERR = 0b10 # Device error
|
|
119
|
+
UMI_ERR_NETERR = 0b11 # Network error
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SumiProtMode(IntEnum):
|
|
123
|
+
"""Protection Mode (PROT[1:0])"""
|
|
124
|
+
UMI_PROT_UNPRIVILEGED_SECURE = 0b00
|
|
125
|
+
UMI_PROT_PRIVILEGED_SECURE = 0b01
|
|
126
|
+
UMI_PROT_UNPRIVILEGED_NONSECURE = 0b10
|
|
127
|
+
UMI_PROT_PRIVILEGED_NONSECURE = 0b11
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SumiSize(IntEnum):
|
|
131
|
+
"""Transaction Word Size (SIZE[2:0]) - bytes per word = 2^SIZE"""
|
|
132
|
+
UMI_SIZE_1 = 0b000 # 1 byte
|
|
133
|
+
UMI_SIZE_2 = 0b001 # 2 bytes
|
|
134
|
+
UMI_SIZE_4 = 0b010 # 4 bytes
|
|
135
|
+
UMI_SIZE_8 = 0b011 # 8 bytes
|
|
136
|
+
UMI_SIZE_16 = 0b100 # 16 bytes
|
|
137
|
+
UMI_SIZE_32 = 0b101 # 32 bytes
|
|
138
|
+
UMI_SIZE_64 = 0b110 # 64 bytes
|
|
139
|
+
UMI_SIZE_128 = 0b111 # 128 bytes
|
|
140
|
+
|
|
141
|
+
def bytes_per_word(self) -> int:
|
|
142
|
+
"""Return the number of bytes per word for this SIZE value."""
|
|
143
|
+
return 1 << self.value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclasses.dataclass
|
|
147
|
+
class SumiCmd(BitVector):
|
|
148
|
+
"""
|
|
149
|
+
UMI Command Header (32 bits)
|
|
150
|
+
|
|
151
|
+
Bit layout:
|
|
152
|
+
[4:0] - opcode (cmd_type): Command opcode
|
|
153
|
+
[7:5] - size: Word size (bytes = 2^SIZE)
|
|
154
|
+
[15:8] - len: Word transfers per message (transfers = LEN+1)
|
|
155
|
+
For REQ_ATOMIC, this field is ATYPE (atomic operation type)
|
|
156
|
+
[19:16] - qos: Quality of service
|
|
157
|
+
[21:20] - prot: Protection mode
|
|
158
|
+
[22] - eom: End of message indicator
|
|
159
|
+
[23] - eof: End of frame indicator
|
|
160
|
+
[24] - ex: Exclusive access indicator
|
|
161
|
+
[26:25] - u: User bits (for requests) or ERR (for responses)
|
|
162
|
+
[31:27] - hostid: Host ID
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Bits [4:0] - Command opcode
|
|
166
|
+
cmd_type: BitField = dataclasses.field(
|
|
167
|
+
default_factory=lambda: BitField(value=0, width=5, offset=0)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Bits [7:5] - Word size (bytes per word = 2^SIZE)
|
|
171
|
+
size: BitField = dataclasses.field(
|
|
172
|
+
default_factory=lambda: BitField(value=0, width=3, offset=5)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Bits [15:8] - Transfer count (LEN+1 words) or ATYPE for atomics
|
|
176
|
+
len: BitField = dataclasses.field(
|
|
177
|
+
default_factory=lambda: BitField(value=0, width=8, offset=8)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Bits [19:16] - Quality of service
|
|
181
|
+
qos: BitField = dataclasses.field(
|
|
182
|
+
default_factory=lambda: BitField(value=0, width=4, offset=16)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Bits [21:20] - Protection mode
|
|
186
|
+
prot: BitField = dataclasses.field(
|
|
187
|
+
default_factory=lambda: BitField(value=0, width=2, offset=20)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Bit [22] - End of message
|
|
191
|
+
eom: BitField = dataclasses.field(
|
|
192
|
+
default_factory=lambda: BitField(value=0, width=1, offset=22)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Bit [23] - End of frame
|
|
196
|
+
eof: BitField = dataclasses.field(
|
|
197
|
+
default_factory=lambda: BitField(value=0, width=1, offset=23)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Bit [24] - Exclusive access
|
|
201
|
+
ex: BitField = dataclasses.field(
|
|
202
|
+
default_factory=lambda: BitField(value=0, width=1, offset=24)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Bits [26:25] - User bits (requests) or error code (responses)
|
|
206
|
+
u: BitField = dataclasses.field(
|
|
207
|
+
default_factory=lambda: BitField(value=0, width=2, offset=25)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Bits [31:27] - Host ID
|
|
211
|
+
hostid: BitField = dataclasses.field(
|
|
212
|
+
default_factory=lambda: BitField(value=0, width=5, offset=27)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def opcode(self) -> int:
|
|
217
|
+
"""Alias for cmd_type field."""
|
|
218
|
+
return int(self.cmd_type)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def atype(self) -> int:
|
|
222
|
+
"""Get ATYPE field (alias for len, used in atomic operations)."""
|
|
223
|
+
return int(self.len)
|
|
224
|
+
|
|
225
|
+
@atype.setter
|
|
226
|
+
def atype(self, value: int):
|
|
227
|
+
"""Set ATYPE field (alias for len, used in atomic operations)."""
|
|
228
|
+
self.len.from_int(value)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def err(self) -> int:
|
|
232
|
+
"""Get ERR field (alias for u, used in responses)."""
|
|
233
|
+
return int(self.u)
|
|
234
|
+
|
|
235
|
+
@err.setter
|
|
236
|
+
def err(self, value: int):
|
|
237
|
+
"""Set ERR field (alias for u, used in responses)."""
|
|
238
|
+
self.u.from_int(value)
|
|
239
|
+
|
|
240
|
+
def bytes_per_word(self) -> int:
|
|
241
|
+
"""Return the number of bytes per word based on SIZE field."""
|
|
242
|
+
return 1 << int(self.size)
|
|
243
|
+
|
|
244
|
+
def transfer_count(self) -> int:
|
|
245
|
+
"""Return the number of word transfers (LEN+1)."""
|
|
246
|
+
return int(self.len) + 1
|
|
247
|
+
|
|
248
|
+
def total_bytes(self) -> int:
|
|
249
|
+
"""Return the total number of bytes in the transaction."""
|
|
250
|
+
return self.bytes_per_word() * self.transfer_count()
|
|
251
|
+
|
|
252
|
+
def is_request(self) -> bool:
|
|
253
|
+
"""Check if this command is a request."""
|
|
254
|
+
return SumiCmdType.is_request(int(self.cmd_type))
|
|
255
|
+
|
|
256
|
+
def is_response(self) -> bool:
|
|
257
|
+
"""Check if this command is a response."""
|
|
258
|
+
return SumiCmdType.is_response(int(self.cmd_type))
|
|
259
|
+
|
|
260
|
+
def has_data(self) -> bool:
|
|
261
|
+
"""Check if this command carries data."""
|
|
262
|
+
return SumiCmdType.has_data(int(self.cmd_type))
|
|
263
|
+
|
|
264
|
+
def has_source_addr(self) -> bool:
|
|
265
|
+
"""Check if this command includes source address."""
|
|
266
|
+
return SumiCmdType.has_source_addr(int(self.cmd_type))
|
|
267
|
+
|
|
268
|
+
def __repr__(self):
|
|
269
|
+
return f"SumiCmd({super().__repr__()})"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class SumiTransaction:
|
|
273
|
+
|
|
274
|
+
def __init__(
|
|
275
|
+
self,
|
|
276
|
+
cmd: SumiCmd,
|
|
277
|
+
da: Optional[int],
|
|
278
|
+
sa: Optional[int],
|
|
279
|
+
data: Optional[bytes],
|
|
280
|
+
addr_width: int = 64
|
|
281
|
+
):
|
|
282
|
+
self.cmd = copy.deepcopy(cmd)
|
|
283
|
+
self.da = BitField(value=da, width=addr_width, offset=0)
|
|
284
|
+
self.sa = BitField(value=sa, width=addr_width, offset=0)
|
|
285
|
+
self.data = data
|
|
286
|
+
self._addr_width = addr_width
|
|
287
|
+
|
|
288
|
+
def header_to_bytes(self) -> bytes:
|
|
289
|
+
return (bytes(self.cmd)
|
|
290
|
+
+ int.to_bytes(int(self.da), length=self._addr_width//8, byteorder='little')
|
|
291
|
+
+ int.to_bytes(int(self.sa), length=self._addr_width//8, byteorder='little'))
|
|
292
|
+
|
|
293
|
+
def to_lumi(self, lumi_size, inc_header=True, override_last=None):
|
|
294
|
+
raw = self.data[:(int(self.cmd.len)+1 << int(self.cmd.size))]
|
|
295
|
+
if inc_header:
|
|
296
|
+
raw = self.header_to_bytes() + raw
|
|
297
|
+
# Break raw into LUMI bus sized chunks
|
|
298
|
+
chunks = [raw[i:i+lumi_size] for i in range(0, len(raw), lumi_size)]
|
|
299
|
+
# Zero pad last chunk
|
|
300
|
+
chunks[-1] = chunks[-1] + bytes([0] * (lumi_size - len(chunks[-1])))
|
|
301
|
+
vrd_transactions = []
|
|
302
|
+
for i, chunk in enumerate(chunks):
|
|
303
|
+
# Set last true for the last chunk
|
|
304
|
+
last = (i == len(chunks)-1)
|
|
305
|
+
# Allow user to override last (useful for simulating streaming mode)
|
|
306
|
+
if last and (override_last is not None):
|
|
307
|
+
last = override_last
|
|
308
|
+
# Convert data to a valid ready transaction type
|
|
309
|
+
vrd_transactions.append(VRDTransaction(
|
|
310
|
+
data=chunk,
|
|
311
|
+
last=last
|
|
312
|
+
))
|
|
313
|
+
return vrd_transactions
|
|
314
|
+
|
|
315
|
+
def trunc_and_pad_zeros(self):
|
|
316
|
+
data_len = ((int(self.cmd.len)+1) << int(self.cmd.size))
|
|
317
|
+
self.data = bytes([0] * (len(self.data) - data_len)) + self.data[:data_len]
|
|
318
|
+
|
|
319
|
+
def __eq__(self, other):
|
|
320
|
+
if isinstance(other, SumiTransaction):
|
|
321
|
+
# For all command types CMD's must match
|
|
322
|
+
if int(self.cmd) == int(other.cmd):
|
|
323
|
+
# For RESP_WRITE only compare header fields DA
|
|
324
|
+
if int(self.cmd.cmd_type) == SumiCmdType.UMI_RESP_WRITE:
|
|
325
|
+
return int(self.da) == int(other.da)
|
|
326
|
+
else:
|
|
327
|
+
my_pkt = self.header_to_bytes() + self.data
|
|
328
|
+
other_pkt = other.header_to_bytes() + other.data
|
|
329
|
+
return my_pkt == other_pkt
|
|
330
|
+
return False
|
|
331
|
+
else:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
def __repr__(self):
|
|
335
|
+
return f"header = {self.header_to_bytes().hex()} data = {self.data.hex()} {self.cmd}"
|
cocotbext/umi/tumi.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from cocotbext.umi.sumi import SumiCmd, SumiTransaction
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TumiTransaction:
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
cmd: SumiCmd,
|
|
10
|
+
da: int,
|
|
11
|
+
sa: int,
|
|
12
|
+
data: bytes
|
|
13
|
+
):
|
|
14
|
+
self._cmd = cmd
|
|
15
|
+
self._data = data
|
|
16
|
+
self._da = da
|
|
17
|
+
self._sa = sa
|
|
18
|
+
|
|
19
|
+
def to_sumi(self, data_bus_size: int, addr_width: int = 64) -> List[SumiTransaction]:
|
|
20
|
+
sa = self._sa
|
|
21
|
+
da = self._da
|
|
22
|
+
|
|
23
|
+
sumi_size = 0
|
|
24
|
+
|
|
25
|
+
data_grouped = [
|
|
26
|
+
self._data[i:i+data_bus_size]
|
|
27
|
+
for i in range(0, len(self._data), data_bus_size)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
rtn = []
|
|
31
|
+
for idx, grouping in enumerate(data_grouped):
|
|
32
|
+
group_len = len(grouping)
|
|
33
|
+
|
|
34
|
+
for size in reversed(range(0, (1 << 3)-1)):
|
|
35
|
+
if group_len % (2**size) == 0:
|
|
36
|
+
sumi_size = size
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
group_len = int(group_len / (2**sumi_size))
|
|
40
|
+
|
|
41
|
+
self._cmd.size.from_int(sumi_size)
|
|
42
|
+
self._cmd.len.from_int(group_len-1)
|
|
43
|
+
self._cmd.eom.from_int(1 if idx == len(data_grouped)-1 else 0)
|
|
44
|
+
|
|
45
|
+
trans = SumiTransaction(
|
|
46
|
+
cmd=self._cmd,
|
|
47
|
+
da=da,
|
|
48
|
+
sa=sa,
|
|
49
|
+
data=grouping,
|
|
50
|
+
addr_width=addr_width
|
|
51
|
+
)
|
|
52
|
+
rtn.append(trans)
|
|
53
|
+
da += (int(self._cmd.len) + 1) << int(self._cmd.size)
|
|
54
|
+
sa += (int(self._cmd.len) + 1) << int(self._cmd.size)
|
|
55
|
+
return rtn
|