mirror of https://github.com/llvm/circt.git
[ESI] MMIO read service implementation in PyCDE (#7306)
Implements a channel-based MMIO read service generator. Changes the offset type on the MMIO std service to `ui32` from `i32`.
This commit is contained in:
parent
cdfcd96664
commit
34263bd91f
|
@ -5,12 +5,11 @@
|
|||
# RUN: esi-cosim.py -- %PYTHON% %S/test_software/esi_test.py cosim env
|
||||
|
||||
import pycde
|
||||
from pycde import (AppID, Clock, Input, Module, generator)
|
||||
from pycde import (AppID, Clock, Module, Reset, modparams, generator)
|
||||
from pycde.bsp import cosim
|
||||
from pycde.constructs import Wire
|
||||
from pycde.esi import FuncService
|
||||
from pycde.types import (Bits, Bundle, BundledChannel, Channel,
|
||||
ChannelDirection, UInt)
|
||||
from pycde.esi import FuncService, MMIO
|
||||
from pycde.types import (Bits, Channel, UInt)
|
||||
|
||||
import sys
|
||||
|
||||
|
@ -33,13 +32,38 @@ class LoopbackInOutAdd7(Module):
|
|||
loopback.assign(data_chan)
|
||||
|
||||
|
||||
@modparams
|
||||
def MMIOClient(add_amt: int):
|
||||
|
||||
class MMIOClient(Module):
|
||||
"""A module which requests an MMIO address space and upon an MMIO read
|
||||
request, returns the <address offset into its space> + add_amt."""
|
||||
|
||||
@generator
|
||||
def build(ports):
|
||||
mmio_read_bundle = MMIO.read(appid=AppID("mmio_client", add_amt))
|
||||
|
||||
address_chan_wire = Wire(Channel(UInt(32)))
|
||||
address, address_valid = address_chan_wire.unwrap(1)
|
||||
response_data = (address.as_uint() + add_amt).as_bits(64)
|
||||
response_chan, response_ready = Channel(Bits(64)).wrap(
|
||||
response_data, address_valid)
|
||||
|
||||
address_chan = mmio_read_bundle.unpack(data=response_chan)['offset']
|
||||
address_chan_wire.assign(address_chan)
|
||||
|
||||
return MMIOClient
|
||||
|
||||
|
||||
class Top(Module):
|
||||
clk = Clock()
|
||||
rst = Input(Bits(1))
|
||||
rst = Reset()
|
||||
|
||||
@generator
|
||||
def construct(ports):
|
||||
LoopbackInOutAdd7()
|
||||
for i in range(4, 18, 5):
|
||||
MMIOClient(i)()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -7,9 +7,40 @@ acc = esi.AcceleratorConnection(platform, sys.argv[2])
|
|||
|
||||
mmio = acc.get_service_mmio()
|
||||
data = mmio.read(8)
|
||||
print(f"mmio data@8: {data:X}")
|
||||
assert data == 0x207D98E5E5100E51
|
||||
|
||||
################################################################################
|
||||
# MMIOClient tests
|
||||
################################################################################
|
||||
|
||||
|
||||
def read_offset(mmio_offset: int, offset: int, add_amt: int):
|
||||
data = mmio.read(mmio_offset + offset)
|
||||
if data == add_amt + offset:
|
||||
print(f"PASS: read_offset({mmio_offset}, {offset}, {add_amt}) -> {data}")
|
||||
else:
|
||||
assert False, f"read_offset({mmio_offset}, {offset}, {add_amt}) -> {data}"
|
||||
|
||||
|
||||
# MMIO offset into mmio_client[9]. TODO: get this from the manifest. API coming.
|
||||
mmio_client_9_offset = 131072
|
||||
read_offset(mmio_client_9_offset, 0, 9)
|
||||
read_offset(mmio_client_9_offset, 13, 9)
|
||||
|
||||
# MMIO offset into mmio_client[4].
|
||||
mmio_client_4_offset = 65536
|
||||
read_offset(mmio_client_4_offset, 0, 4)
|
||||
read_offset(mmio_client_4_offset, 13, 4)
|
||||
|
||||
# MMIO offset into mmio_client[14].
|
||||
mmio_client_14_offset = 196608
|
||||
read_offset(mmio_client_14_offset, 0, 14)
|
||||
read_offset(mmio_client_14_offset, 13, 14)
|
||||
|
||||
################################################################################
|
||||
# Manifest tests
|
||||
################################################################################
|
||||
|
||||
assert acc.sysinfo().esi_version() == 0
|
||||
m = acc.manifest()
|
||||
assert m.api_version == 0
|
||||
|
@ -23,6 +54,10 @@ recv.connect()
|
|||
send = d.ports[esi.AppID("loopback_add7")].write_port("arg")
|
||||
send.connect()
|
||||
|
||||
################################################################################
|
||||
# Loopback add 7 tests
|
||||
################################################################################
|
||||
|
||||
data = 10234
|
||||
send.write(data)
|
||||
got_data = False
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
# See https://llvm.org/LICENSE.txt for license information.
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
|
||||
from ..common import Clock, Input, Output
|
||||
from ..constructs import ControlReg, Mux, NamedWire, Wire
|
||||
from ..common import Clock, Input, Output, Reset
|
||||
from ..constructs import AssignableSignal, NamedWire, Wire
|
||||
from .. import esi
|
||||
from ..module import Module, generator
|
||||
from ..signals import BundleSignal
|
||||
from ..types import Array, Bits, Channel, ChannelDirection
|
||||
from ..module import Module, generator, modparams
|
||||
from ..signals import BitsSignal, ChannelSignal
|
||||
from ..support import clog2
|
||||
from ..types import Array, Bits, Channel, UInt
|
||||
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
@ -27,10 +28,71 @@ class ESI_Manifest_ROM(Module):
|
|||
data = Output(Bits(64))
|
||||
|
||||
|
||||
class ESI_Manifest_ROM_Wrapper(Module):
|
||||
"""Wrap the manifest ROM with ESI bundle."""
|
||||
|
||||
clk = Clock()
|
||||
read = Input(esi.MMIO.read.type)
|
||||
|
||||
@generator
|
||||
def build(self):
|
||||
data, data_valid = Wire(Bits(64)), Wire(Bits(1))
|
||||
data_chan, data_ready = Channel(Bits(64)).wrap(data, data_valid)
|
||||
address_chan = self.read.unpack(data=data_chan)['offset']
|
||||
address, address_valid = address_chan.unwrap(data_ready)
|
||||
address_words = address.as_bits(32)[3:] # Lop off the lower three bits.
|
||||
|
||||
rom = ESI_Manifest_ROM(clk=self.clk, address=address_words)
|
||||
data.assign(rom.data)
|
||||
data_valid.assign(address_valid.reg(self.clk, name="data_valid", cycles=2))
|
||||
|
||||
|
||||
@modparams
|
||||
def HeaderMMIO(manifest_loc: int) -> Module:
|
||||
|
||||
class HeaderMMIO(Module):
|
||||
"""Construct the ESI header MMIO adhering to the MMIO layout specified in
|
||||
the ChannelMMIO service implementation."""
|
||||
|
||||
clk = Clock()
|
||||
rst = Reset()
|
||||
read = Input(esi.MMIO.read.type)
|
||||
|
||||
@generator
|
||||
def build(ports):
|
||||
data_chan_wire = Wire(Channel(esi.MMIODataType))
|
||||
input_bundles = ports.read.unpack(data=data_chan_wire)
|
||||
address_chan = input_bundles['offset']
|
||||
|
||||
address_ready = Wire(Bits(1))
|
||||
address, address_valid = address_chan.unwrap(address_ready)
|
||||
address_words = address.as_bits()[3:] # Lop off the lower three bits.
|
||||
|
||||
# Layout the header as an array.
|
||||
header = Array(Bits(64), 4)([0, MagicNumber, VersionNumber, manifest_loc])
|
||||
header.name = "header"
|
||||
header_response_valid = address_valid # Zero latency read.
|
||||
# Select the approptiate header index.
|
||||
header_out = header[address_words[:2]]
|
||||
header_out.name = "header_out"
|
||||
# Wrap the response.
|
||||
data_chan, data_chan_ready = Channel(esi.MMIODataType).wrap(
|
||||
header_out, header_response_valid)
|
||||
data_chan_wire.assign(data_chan)
|
||||
address_ready.assign(data_chan_ready)
|
||||
|
||||
return HeaderMMIO
|
||||
|
||||
|
||||
class ChannelMMIO(esi.ServiceImplementation):
|
||||
"""MMIO service implementation with an AXI-lite protocol. This assumes a 32
|
||||
bit address bus. It also only supports 64-bit aligned accesses and just throws
|
||||
away the lower three bits of address.
|
||||
"""MMIO service implementation with MMIO bundle interfaces. Should be
|
||||
relatively easy to adapt to physical interfaces by wrapping the wires to
|
||||
channels then bundles. Allows the implementation to be shared and (hopefully)
|
||||
platform independent.
|
||||
|
||||
Whether or not to support unaligned accesses is up to the clients. The header
|
||||
and manifest do not support unaligned accesses and throw away the lower three
|
||||
bits.
|
||||
|
||||
Only allows for one outstanding request at a time. If a client doesn't return
|
||||
a response, the MMIO service will hang. TODO: add some kind of timeout.
|
||||
|
@ -41,14 +103,15 @@ class ChannelMMIO(esi.ServiceImplementation):
|
|||
- 0x12: ESI version number (0)
|
||||
- 0x18: Location of the manifest ROM (absolute address)
|
||||
|
||||
- 0x100: Start of MMIO space for requests. Mapping is contained in the
|
||||
manifest so can be dynamically queried.
|
||||
- 0x10000: Start of MMIO space for requests. Mapping is contained in the
|
||||
manifest so can be dynamically queried.
|
||||
|
||||
- addr(Manifest ROM) + 0: Size of compressed manifest
|
||||
- addr(Manifest ROM) + 8: Start of compressed manifest
|
||||
|
||||
This layout _should_ be pretty standard, but different BSPs may have various
|
||||
different restrictions.
|
||||
different restrictions. Any BSP which uses this service implementation will
|
||||
have this layout, possibly with an offset or address window.
|
||||
"""
|
||||
|
||||
clk = Clock()
|
||||
|
@ -56,123 +119,116 @@ class ChannelMMIO(esi.ServiceImplementation):
|
|||
|
||||
read = Input(esi.MMIO.read.type)
|
||||
|
||||
# Amount of register space each client gets. This is a GIANT HACK and needs to
|
||||
# be replaced by parameterizable services.
|
||||
# TODO: make the amount of register space each client gets a parameter.
|
||||
# Supporting this will require more address decode logic.
|
||||
#
|
||||
# TODO: if the compressed manifest is larger than 'RegisterSpace', we won't be
|
||||
# allocating enough address space. This should be fixed with the more complex
|
||||
# address decode logic mentioned above.
|
||||
#
|
||||
# TODO: only supports one outstanding transaction at a time. This is NOT
|
||||
# enforced or checked! Enforce this.
|
||||
|
||||
RegisterSpace = 0x10000
|
||||
RegisterSpaceBits = RegisterSpace.bit_length() - 1
|
||||
AddressMask = 0xFFFF
|
||||
|
||||
# Start at this address for assigning MMIO addresses to service requests.
|
||||
initial_offset: int = 0x100
|
||||
initial_offset: int = RegisterSpace
|
||||
|
||||
@generator
|
||||
def generate(self, bundles: esi._ServiceGeneratorBundles):
|
||||
def generate(ports, bundles: esi._ServiceGeneratorBundles):
|
||||
read_table, write_table, manifest_loc = ChannelMMIO.build_table(
|
||||
self, bundles)
|
||||
ChannelMMIO.build_read(self, manifest_loc, read_table)
|
||||
ChannelMMIO.build_write(self, write_table)
|
||||
ports, bundles)
|
||||
ChannelMMIO.build_read(ports, manifest_loc, read_table)
|
||||
ChannelMMIO.build_write(ports, write_table)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def build_table(
|
||||
self,
|
||||
bundles) -> Tuple[Dict[int, BundleSignal], Dict[int, BundleSignal], int]:
|
||||
ports, bundles
|
||||
) -> Tuple[Dict[int, AssignableSignal], Dict[int, AssignableSignal], int]:
|
||||
"""Build a table of read and write addresses to BundleSignals."""
|
||||
offset = ChannelMMIO.initial_offset
|
||||
read_table = {}
|
||||
write_table = {}
|
||||
read_table: Dict[int, AssignableSignal] = {}
|
||||
write_table: Dict[int, AssignableSignal] = {}
|
||||
for bundle in bundles.to_client_reqs:
|
||||
if bundle.direction == ChannelDirection.Input:
|
||||
if bundle.port == 'read':
|
||||
read_table[offset] = bundle
|
||||
offset += 8
|
||||
elif bundle.direction == ChannelDirection.Output:
|
||||
write_table[offset] = bundle
|
||||
offset += 8
|
||||
bundle.add_record({"offset": offset})
|
||||
offset += ChannelMMIO.RegisterSpace
|
||||
else:
|
||||
assert False, "Unrecognized port name."
|
||||
|
||||
manifest_loc = 1 << offset.bit_length()
|
||||
manifest_loc = offset
|
||||
return read_table, write_table, manifest_loc
|
||||
|
||||
def build_read(self, manifest_loc: int, bundles):
|
||||
@staticmethod
|
||||
def build_read(ports, manifest_loc: int, read_table: Dict[int,
|
||||
AssignableSignal]):
|
||||
"""Builds the read side of the MMIO service."""
|
||||
|
||||
# Currently just exposes the header and manifest. Not any of the possible
|
||||
# service requests.
|
||||
# Instantiate the header and manifest ROM. Fill in the read_table with
|
||||
# bundle wires to be assigned identically to the other MMIO clients.
|
||||
header_bundle_wire = Wire(esi.MMIO.read.type)
|
||||
read_table[0] = header_bundle_wire
|
||||
HeaderMMIO(manifest_loc)(clk=ports.clk,
|
||||
rst=ports.rst,
|
||||
read=header_bundle_wire)
|
||||
|
||||
i64 = Bits(64)
|
||||
i2 = Bits(2)
|
||||
i1 = Bits(1)
|
||||
mani_bundle_wire = Wire(esi.MMIO.read.type)
|
||||
read_table[manifest_loc] = mani_bundle_wire
|
||||
ESI_Manifest_ROM_Wrapper(clk=ports.clk, read=mani_bundle_wire)
|
||||
|
||||
read_data_channel = Wire(Channel(esi.MMIOReadDataResponse),
|
||||
"resp_data_channel")
|
||||
read_addr_channel = self.read.unpack(data=read_data_channel)["offset"]
|
||||
arready = Wire(i1)
|
||||
(araddr, arvalid) = read_addr_channel.unwrap(arready)
|
||||
# Unpack the read bundle.
|
||||
data_resp_channel = Wire(Channel(esi.MMIODataType))
|
||||
counted_output = Wire(Channel(esi.MMIODataType))
|
||||
read_addr_channel = ports.read.unpack(data=counted_output)["offset"]
|
||||
counted_output.assign(data_resp_channel)
|
||||
|
||||
address_written = NamedWire(i1, "address_written")
|
||||
response_written = NamedWire(i1, "response_written")
|
||||
# Get the selection index and the address to hand off to the clients.
|
||||
sel_bits, client_address_chan = ChannelMMIO.build_addr_read(
|
||||
read_addr_channel)
|
||||
|
||||
# Only allow one outstanding request at a time. Don't clear it until the
|
||||
# output has been transmitted. This way, we don't have to deal with
|
||||
# backpressure.
|
||||
req_outstanding = ControlReg(self.clk,
|
||||
self.rst, [address_written],
|
||||
[response_written],
|
||||
name="req_outstanding")
|
||||
arready.assign(~req_outstanding)
|
||||
# Build the demux/mux and assign the results of each appropriately.
|
||||
read_clients_clog2 = clog2(len(read_table))
|
||||
client_addr_channels = esi.ChannelDemux(
|
||||
sel=sel_bits.pad_or_truncate(read_clients_clog2),
|
||||
input=client_address_chan,
|
||||
num_outs=len(read_table))
|
||||
client_data_channels = []
|
||||
for (idx, offset) in enumerate(sorted(read_table.keys())):
|
||||
bundle, bundle_froms = esi.MMIO.read.type.pack(
|
||||
offset=client_addr_channels[idx])
|
||||
client_data_channels.append(bundle_froms["data"])
|
||||
read_table[offset].assign(bundle)
|
||||
resp_channel = esi.ChannelMux(client_data_channels)
|
||||
data_resp_channel.assign(resp_channel)
|
||||
|
||||
# Capture the address if a the bus transaction occured.
|
||||
address_written.assign(arvalid & ~req_outstanding)
|
||||
address = araddr.reg(self.clk, ce=address_written, name="address")
|
||||
address_valid = address_written.reg(name="address_valid")
|
||||
address_words = address[3:] # Lop off the lower three bits.
|
||||
@staticmethod
|
||||
def build_addr_read(
|
||||
read_addr_chan: ChannelSignal) -> Tuple[BitsSignal, ChannelSignal]:
|
||||
"""Build a channel for the address read request. Returns the index to select
|
||||
the client and a channel for the masked address to be passed to the
|
||||
clients."""
|
||||
|
||||
# Set up the output of the data response pipeline. `data_pipeline*` are to
|
||||
# be connected below.
|
||||
data_pipeline_valid = NamedWire(i1, "data_pipeline_valid")
|
||||
data_pipeline = NamedWire(i64, "data_pipeline")
|
||||
data_pipeline_rresp = NamedWire(i2, "data_pipeline_rresp")
|
||||
data_out_valid = ControlReg(self.clk,
|
||||
self.rst, [data_pipeline_valid],
|
||||
[response_written],
|
||||
name="data_out_valid")
|
||||
rvalid = data_out_valid
|
||||
rdata = data_pipeline.reg(self.clk,
|
||||
self.rst,
|
||||
ce=data_pipeline_valid,
|
||||
name="data_pipeline_reg")
|
||||
read_resp_ch, rready = Channel(esi.MMIOReadDataResponse).wrap(rdata, rvalid)
|
||||
read_data_channel.assign(read_resp_ch)
|
||||
# Clear the `req_outstanding` flag when the response has been transmitted.
|
||||
response_written.assign(data_out_valid & rready)
|
||||
# Decoding the selection bits is very simple as of now. This might need to
|
||||
# change to support more flexibility in addressing. Not clear if what we're
|
||||
# doing now it sufficient or not.
|
||||
|
||||
# Handle reads from the header (< 0x100).
|
||||
header_upper = address_words[ChannelMMIO.initial_offset.bit_length() - 2:]
|
||||
# Is the address in the header?
|
||||
header_sel = (header_upper == header_upper.type(0))
|
||||
header_sel.name = "header_sel"
|
||||
# Layout the header as an array.
|
||||
header = Array(Bits(64), 4)([0, MagicNumber, VersionNumber, manifest_loc])
|
||||
header.name = "header"
|
||||
header_response_valid = address_valid # Zero latency read.
|
||||
header_out = header[address_words[:2]]
|
||||
header_out.name = "header_out"
|
||||
header_rresp = i2(0)
|
||||
|
||||
# Handle reads from the manifest.
|
||||
rom_address = NamedWire(
|
||||
(address_words.as_uint() - (manifest_loc >> 3)).as_bits(29),
|
||||
"rom_address")
|
||||
mani_rom = ESI_Manifest_ROM(clk=self.clk, address=rom_address)
|
||||
mani_valid = address_valid.reg(
|
||||
self.clk,
|
||||
self.rst,
|
||||
rst_value=i1(0),
|
||||
cycles=2, # Two cycle read to match the ROM latency.
|
||||
name="mani_valid_reg")
|
||||
mani_rresp = i2(0)
|
||||
mani_sel = (address.as_uint() >= manifest_loc).as_bits(1)
|
||||
|
||||
# Mux the output depending on whether or not the address is in the header.
|
||||
sel = NamedWire(mani_sel, "sel")
|
||||
data_mux_inputs = [header_out, mani_rom.data]
|
||||
data_pipeline.assign(Mux(sel, *data_mux_inputs))
|
||||
data_valid_mux_inputs = [header_response_valid, mani_valid]
|
||||
data_pipeline_valid.assign(Mux(sel, *data_valid_mux_inputs))
|
||||
rresp_mux_inputs = [header_rresp, mani_rresp]
|
||||
data_pipeline_rresp.assign(Mux(sel, *rresp_mux_inputs))
|
||||
addr_ready_wire = Wire(Bits(1))
|
||||
addr, addr_valid = read_addr_chan.unwrap(addr_ready_wire)
|
||||
addr = addr.as_bits()
|
||||
sel_bits = NamedWire(Bits(32 - ChannelMMIO.RegisterSpaceBits), "sel_bits")
|
||||
sel_bits.assign(addr[ChannelMMIO.RegisterSpaceBits:])
|
||||
client_addr = NamedWire(Bits(32), "client_addr")
|
||||
client_addr.assign(addr & Bits(32)(ChannelMMIO.AddressMask))
|
||||
client_addr_chan, client_addr_ready = Channel(UInt(32)).wrap(
|
||||
client_addr.as_uint(), addr_valid)
|
||||
addr_ready_wire.assign(client_addr_ready)
|
||||
return sel_bits, client_addr_chan
|
||||
|
||||
def build_write(self, bundles):
|
||||
# TODO: this.
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
|
||||
from .common import (AppID, Input, Output, _PyProxy, PortError)
|
||||
from .constructs import AssignableSignal
|
||||
from .module import Generator, Module, ModuleLikeBuilderBase, PortProxyBase
|
||||
from .signals import BundleSignal, ChannelSignal, Signal, _FromCirctValue
|
||||
from .constructs import AssignableSignal, Mux, Wire
|
||||
from .module import generator, Module, ModuleLikeBuilderBase, PortProxyBase
|
||||
from .signals import (BitsSignal, BundleSignal, ChannelSignal, Signal,
|
||||
_FromCirctValue)
|
||||
from .support import get_user_loc
|
||||
from .system import System
|
||||
from .types import (Bits, Bundle, BundledChannel, ChannelDirection, Type, types,
|
||||
_FromCirctType)
|
||||
from .types import (Bits, Bundle, BundledChannel, Channel, ChannelDirection,
|
||||
Type, UInt, types, _FromCirctType)
|
||||
|
||||
from .circt import ir
|
||||
from .circt.dialects import esi as raw_esi, hw, msft
|
||||
|
@ -467,16 +468,18 @@ class PureModule(Module):
|
|||
esi.ESIPureModuleParamOp(name, type_attr)
|
||||
|
||||
|
||||
MMIOReadDataResponse = Bits(64)
|
||||
MMIODataType = Bits(64)
|
||||
|
||||
|
||||
@ServiceDecl
|
||||
class MMIO:
|
||||
"""ESI standard service to request access to an MMIO region."""
|
||||
"""ESI standard service to request access to an MMIO region.
|
||||
|
||||
For now, each client request gets a 1KB region of memory."""
|
||||
|
||||
read = Bundle([
|
||||
BundledChannel("offset", ChannelDirection.TO, Bits(32)),
|
||||
BundledChannel("data", ChannelDirection.FROM, MMIOReadDataResponse)
|
||||
BundledChannel("offset", ChannelDirection.TO, UInt(32)),
|
||||
BundledChannel("data", ChannelDirection.FROM, MMIODataType)
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
|
@ -624,3 +627,120 @@ def package(sys: System):
|
|||
circt_lib_dir = build_dir / "tools" / "circt" / "lib"
|
||||
esi_lib_dir = circt_lib_dir / "Dialect" / "ESI"
|
||||
# shutil.copy(esi_lib_dir / "ESIPrimitives.sv", sys.hw_output_dir)
|
||||
|
||||
|
||||
def ChannelDemux2(data_type: Type):
|
||||
|
||||
class ChannelDemux2(Module):
|
||||
"""Combinational 2-way channel demultiplexer for valid/ready signaling."""
|
||||
|
||||
sel = Input(Bits(1))
|
||||
inp = Input(Channel(data_type))
|
||||
output0 = Output(Channel(data_type))
|
||||
output1 = Output(Channel(data_type))
|
||||
|
||||
@generator
|
||||
def generate(ports) -> None:
|
||||
input_ready = Wire(Bits(1))
|
||||
input, input_valid = ports.inp.unwrap(input_ready)
|
||||
|
||||
output0 = input
|
||||
output0_valid = input_valid & (ports.sel == Bits(1)(0))
|
||||
output0_ch, output0_ready = Channel(data_type).wrap(
|
||||
output0, output0_valid)
|
||||
ports.output0 = output0_ch
|
||||
|
||||
output1 = input
|
||||
output1_valid = input_valid & (ports.sel == Bits(1)(1))
|
||||
output1_ch, output1_ready = Channel(data_type).wrap(
|
||||
output1, output1_valid)
|
||||
ports.output1 = output1_ch
|
||||
|
||||
input_ready.assign((output0_ready & (ports.sel == Bits(1)(0))) |
|
||||
(output1_ready & (ports.sel == Bits(1)(1))))
|
||||
|
||||
return ChannelDemux2
|
||||
|
||||
|
||||
def ChannelDemux(input: ChannelSignal, sel: BitsSignal,
|
||||
num_outs: int) -> List[ChannelSignal]:
|
||||
"""Build a demultiplexer of ESI channels. Ideally, this would be a
|
||||
parameterized module with an array of output channels, but the current ESI
|
||||
channel-port lowering doesn't deal with arrays of channels. Independent of the
|
||||
signaling protocol."""
|
||||
|
||||
dmux2 = ChannelDemux2(input.type)
|
||||
|
||||
def build_tree(inter_input: ChannelSignal, inter_sel: BitsSignal,
|
||||
inter_num_outs: int, path: str) -> List[ChannelSignal]:
|
||||
"""Builds a binary tree of demuxes to demux the input channel."""
|
||||
if inter_num_outs == 0:
|
||||
return []
|
||||
if inter_num_outs == 1:
|
||||
return [inter_input]
|
||||
|
||||
demux2 = dmux2(sel=inter_sel[-1].as_bits(),
|
||||
inp=inter_input,
|
||||
instance_name=f"demux2_path{path}")
|
||||
next_sel = inter_sel[:-1].as_bits()
|
||||
tree0 = build_tree(demux2.output0, next_sel, (inter_num_outs + 1) // 2,
|
||||
path + "0")
|
||||
tree1 = build_tree(demux2.output1, next_sel, (inter_num_outs + 1) // 2,
|
||||
path + "1")
|
||||
return tree0 + tree1
|
||||
|
||||
return build_tree(input, sel, num_outs, "")
|
||||
|
||||
|
||||
def ChannelMux2(data_type: Channel):
|
||||
|
||||
class ChannelMux2(Module):
|
||||
"""2 channel arbiter with priority given to input0. Valid/ready only.
|
||||
Combinational."""
|
||||
# TODO: implement some fairness.
|
||||
|
||||
input0 = Input(data_type)
|
||||
input1 = Input(data_type)
|
||||
output_channel = Output(data_type)
|
||||
|
||||
@generator
|
||||
def generate(ports):
|
||||
input0_ready = Wire(Bits(1))
|
||||
input0_data, input0_valid = ports.input0.unwrap(input0_ready)
|
||||
input1_ready = Wire(Bits(1))
|
||||
input1_data, input1_valid = ports.input1.unwrap(input1_ready)
|
||||
|
||||
output_idx = ~input0_valid
|
||||
data_mux = Mux(output_idx, input0_data, input1_data)
|
||||
valid_mux = Mux(output_idx, input0_valid, input1_valid)
|
||||
output_channel, output_ready = data_type.wrap(data_mux, valid_mux)
|
||||
ports.output_channel = output_channel
|
||||
|
||||
input0_ready.assign(output_ready & ~output_idx)
|
||||
input1_ready.assign(output_ready & output_idx)
|
||||
|
||||
return ChannelMux2
|
||||
|
||||
|
||||
def ChannelMux(input_channels: List[ChannelSignal]) -> ChannelSignal:
|
||||
"""Build a channel multiplexer of ESI channels. Ideally, this would be a
|
||||
parameterized module with an array of output channels, but the current ESI
|
||||
channel-port lowering doesn't deal with arrays of channels. Independent of the
|
||||
signaling protocol."""
|
||||
|
||||
assert len(input_channels) > 0
|
||||
mux2 = ChannelMux2(input_channels[0].type)
|
||||
|
||||
def build_tree(inter_input_channels: List[ChannelSignal]) -> ChannelSignal:
|
||||
assert len(inter_input_channels) > 0
|
||||
if len(inter_input_channels) == 1:
|
||||
return inter_input_channels[0]
|
||||
if len(inter_input_channels) == 2:
|
||||
m = mux2(input0=inter_input_channels[0], input1=inter_input_channels[1])
|
||||
return m.output_channel
|
||||
m0_out = build_tree(inter_input_channels[:len(inter_input_channels) // 2])
|
||||
m1_out = build_tree(inter_input_channels[len(inter_input_channels) // 2:])
|
||||
m = mux2(input0=m0_out, input1=m1_out)
|
||||
return m.output_channel
|
||||
|
||||
return build_tree(input_channels)
|
||||
|
|
|
@ -4,6 +4,13 @@ from .circt import ir
|
|||
import os
|
||||
|
||||
|
||||
def clog2(x: int) -> int:
|
||||
"""Return the ceiling log base 2 of x."""
|
||||
if x == 0:
|
||||
return 0
|
||||
return (x - 1).bit_length()
|
||||
|
||||
|
||||
# PyCDE needs a custom version of this to support python classes.
|
||||
def _obj_to_attribute(obj) -> ir.Attribute:
|
||||
"""Create an MLIR attribute from a Python object for a few common cases."""
|
||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import annotations
|
|||
from collections import OrderedDict
|
||||
from functools import singledispatchmethod
|
||||
|
||||
from .support import get_user_loc
|
||||
from .support import clog2, get_user_loc
|
||||
|
||||
from .circt import ir, support
|
||||
from .circt.dialects import esi, hw, seq, sv
|
||||
|
@ -84,7 +84,7 @@ class Type:
|
|||
return self
|
||||
|
||||
@property
|
||||
def bitwidth(self):
|
||||
def bitwidth(self) -> int:
|
||||
return hw.get_bitwidth(self._type)
|
||||
|
||||
@property
|
||||
|
@ -311,6 +311,10 @@ class Array(Type):
|
|||
def size(self):
|
||||
return self._type.size
|
||||
|
||||
@property
|
||||
def select_bits(self) -> int:
|
||||
return clog2(self.size)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
_shape = [self.size]
|
||||
|
|
|
@ -183,9 +183,9 @@ class RecvBundleTest(Module):
|
|||
# CHECK-LABEL: hw.module @MMIOReq()
|
||||
# CHECK-NEXT: %c0_i64 = hw.constant 0 : i64
|
||||
# CHECK-NEXT: %false = hw.constant false
|
||||
# CHECK-NEXT: [[B:%.+]] = esi.service.req <@MMIO::@read>(#esi.appid<"mmio_req">) : !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>
|
||||
# CHECK-NEXT: [[B:%.+]] = esi.service.req <@MMIO::@read>(#esi.appid<"mmio_req">) : !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>
|
||||
# CHECK-NEXT: %chanOutput, %ready = esi.wrap.vr %c0_i64, %false : i64
|
||||
# CHECK-NEXT: %offset = esi.bundle.unpack %chanOutput from [[B]] : !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>
|
||||
# CHECK-NEXT: %offset = esi.bundle.unpack %chanOutput from [[B]] : !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>
|
||||
@unittestmodule(esi_sys=True)
|
||||
class MMIOReq(Module):
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ s.package()
|
|||
# TOP: output s_axi_control_BVALID,
|
||||
# TOP: output [1:0] s_axi_control_BRESP
|
||||
|
||||
# TOP: __ESI_Manifest_ROM ESI_Manifest_ROM (
|
||||
# TOP: ESI_Manifest_ROM_Wrapper ESI_Manifest_ROM_Wrapper (
|
||||
# TOP: .clk (ap_clk),
|
||||
# TOP: .address (rom_address),
|
||||
# TOP: .data (_ESI_Manifest_ROM_data)
|
||||
|
|
|
@ -99,8 +99,12 @@ void MMIOServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
|
|||
hw::InnerRefAttr::get(getSymNameAttr(), StringAttr::get(ctxt, "read")),
|
||||
ChannelBundleType::get(
|
||||
ctxt,
|
||||
{BundledChannel{StringAttr::get(ctxt, "offset"), ChannelDirection::to,
|
||||
ChannelType::get(ctxt, IntegerType::get(ctxt, 32))},
|
||||
{BundledChannel{
|
||||
StringAttr::get(ctxt, "offset"), ChannelDirection::to,
|
||||
ChannelType::get(
|
||||
ctxt,
|
||||
IntegerType::get(
|
||||
ctxt, 32, IntegerType::SignednessSemantics::Unsigned))},
|
||||
BundledChannel{StringAttr::get(ctxt, "data"), ChannelDirection::from,
|
||||
ChannelType::get(ctxt, IntegerType::get(ctxt, 64))}},
|
||||
/*resettable=*/UnitAttr())});
|
||||
|
|
|
@ -205,14 +205,14 @@ hw.module @CallableAccel1(in %clk: !seq.clock, in %rst: i1) {
|
|||
}
|
||||
|
||||
esi.service.std.mmio @mmio
|
||||
!mmioReq = !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>
|
||||
!mmioReq = !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>
|
||||
|
||||
// CONN-LABEL: hw.module @MMIOManifest(in %clk : !seq.clock, in %rst : i1, in %manifest : !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>) {
|
||||
// CONN-LABEL: hw.module @MMIOManifest(in %clk : !seq.clock, in %rst : i1, in %manifest : !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>) {
|
||||
// CONN-NEXT: %true = hw.constant true
|
||||
// CONN-NEXT: %c0_i64 = hw.constant 0 : i64
|
||||
// CONN-NEXT: esi.manifest.req #esi.appid<"manifest">, <@mmio::@read> std "esi.service.std.mmio", !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>
|
||||
// CONN-NEXT: esi.manifest.req #esi.appid<"manifest">, <@mmio::@read> std "esi.service.std.mmio", !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>
|
||||
// CONN-NEXT: %chanOutput, %ready = esi.wrap.vr %c0_i64, %true : i64
|
||||
// CONN-NEXT: %offset = esi.bundle.unpack %chanOutput from %manifest : !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i64> from "data"]>
|
||||
// CONN-NEXT: %offset = esi.bundle.unpack %chanOutput from %manifest : !esi.bundle<[!esi.channel<ui32> to "offset", !esi.channel<i64> from "data"]>
|
||||
hw.module @MMIOManifest(in %clk: !seq.clock, in %rst: i1) {
|
||||
%req = esi.service.req <@mmio::@read> (#esi.appid<"manifest">) : !mmioReq
|
||||
%data = hw.constant 0 : i64
|
||||
|
|
Loading…
Reference in New Issue