Skip to content

Commit

Permalink
[ESI] Make MMIO data 64-bit
Browse files Browse the repository at this point in the history
Since MMIO will be used to transmit things like pointers, we should
probably make it 64-bit data. This seemingly simple change had a bunch
of ripple effects.
  • Loading branch information
teqdruid committed Jul 4, 2024
1 parent b72a97b commit e218a94
Show file tree
Hide file tree
Showing 16 changed files with 117 additions and 217 deletions.
1 change: 1 addition & 0 deletions frontends/PyCDE/integration_test/esitester.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# RUN: mkdir %t && cd %t
# RUN: %PYTHON% %s %t 2>&1
# RUN: esi-cosim.py --source %t -- esitester cosim env wait | FileCheck %s
# RUN: ESI_COSIM_MANIFEST_MMIO=1 esi-cosim.py --source %t -- esiquery cosim env info

import pycde
from pycde import AppID, Clock, Module, Reset, generator
Expand Down
2 changes: 1 addition & 1 deletion frontends/PyCDE/integration_test/test_software/esi_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
mmio = acc.get_service_mmio()
data = mmio.read(8)
print(f"mmio data@8: {data:X}")
assert data == 0xe5100e51
assert data == 0x207D98E5E5100E51

assert acc.sysinfo().esi_version() == 1
m = acc.manifest()
Expand Down
45 changes: 20 additions & 25 deletions frontends/PyCDE/src/pycde/bsp/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@

from typing import Dict, Tuple

MagicNumberLo = 0xE5100E51 # ESI__ESI
MagicNumberHi = 0x207D98E5 # Random
MagicNumber = 0x207D98E5_E5100E51 # random + ESI__ESI
VersionNumber = 0 # Version 0: format subject to change


Expand All @@ -23,33 +22,30 @@ class ESI_Manifest_ROM(Module):
module_name = "__ESI_Manifest_ROM"

clk = Clock()
address = Input(Bits(30))
address = Input(Bits(29))
# Data is two cycles delayed after address changes.
data = Output(Bits(32))
data = Output(Bits(64))


class ChannelMMIO(esi.ServiceImplementation):
"""MMIO service implementation with an AXI-lite protocol. This assumes a 20
bit address bus for 1MB of addressable MMIO space. Which should be fine for
now, though nothing should assume this limit. It also only supports 32-bit
aligned accesses and just throws away the lower two bits of address.
"""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.
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.
Implementation-defined MMIO layout:
- 0x0: 0 constant
- 0x4: 0 constant
- 0x8: Magic number low (0xE5100E51)
- 0xC: Magic number high (random constant: 0x207D98E5)
- 0x10: ESI version number (0)
- 0x14: Location of the manifest ROM (absolute address)
- 0x8: Magic number (0x207D98E5_E5100E51)
- 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.
- addr(Manifest ROM) + 0: Size of compressed manifest
- addr(Manifest ROM) + 4: Start 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.
Expand Down Expand Up @@ -81,10 +77,10 @@ def build_table(
for bundle in bundles.to_client_reqs:
if bundle.direction == ChannelDirection.Input:
read_table[offset] = bundle
offset += 4
offset += 8
elif bundle.direction == ChannelDirection.Output:
write_table[offset] = bundle
offset += 4
offset += 8

manifest_loc = 1 << offset.bit_length()
return read_table, write_table, manifest_loc
Expand All @@ -95,7 +91,7 @@ def build_read(self, manifest_loc: int, bundles):
# Currently just exposes the header and manifest. Not any of the possible
# service requests.

i32 = Bits(32)
i64 = Bits(64)
i2 = Bits(2)
i1 = Bits(1)

Expand All @@ -121,12 +117,12 @@ def build_read(self, manifest_loc: int, bundles):
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[2:] # Lop off the lower two bits.
address_words = address[3:] # Lop off the lower three bits.

# 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(i32, "data_pipeline")
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],
Expand All @@ -148,17 +144,16 @@ def build_read(self, manifest_loc: int, bundles):
header_sel = (header_upper == header_upper.type(0))
header_sel.name = "header_sel"
# Layout the header as an array.
header = Array(Bits(32), 6)(
[0, 0, MagicNumberLo, MagicNumberHi, VersionNumber, manifest_loc])
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[2:5]]
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 >> 2)).as_bits(30),
(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(
Expand All @@ -168,10 +163,10 @@ def build_read(self, manifest_loc: int, bundles):
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)
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(~header_sel, "sel")
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]
Expand Down
137 changes: 9 additions & 128 deletions frontends/PyCDE/src/pycde/bsp/xrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
from ..types import Array, Bits
from .. import esi

from .common import (ESI_Manifest_ROM, MagicNumberHi, MagicNumberLo,
VersionNumber)

import glob
import pathlib
import shutil
Expand All @@ -22,6 +19,7 @@
__dir__ = pathlib.Path(__file__).parent


# Purposely breaking this. TODO: fix it by using ChannelMMIO.
class AxiMMIO(esi.ServiceImplementation):
"""MMIO service implementation with an AXI-lite protocol. This assumes a 20
bit address bus for 1MB of addressable MMIO space. Which should be fine for
Expand Down Expand Up @@ -49,8 +47,6 @@ class AxiMMIO(esi.ServiceImplementation):
different restrictions.
"""

# Moved from bsp/common.py. TODO: adapt this to use ChannelMMIO.

clk = Clock()
rst = Input(Bits(1))

Expand Down Expand Up @@ -85,131 +81,16 @@ class AxiMMIO(esi.ServiceImplementation):

@generator
def generate(self, bundles: esi._ServiceGeneratorBundles):
read_table, write_table, manifest_loc = AxiMMIO.build_table(self, bundles)
AxiMMIO.build_read(self, manifest_loc, read_table)
AxiMMIO.build_write(self, write_table)
self.arready = Bits(1)(0)
self.rvalid = Bits(1)(0)
self.rdata = Bits(32)(0)
self.rresp = Bits(2)(0)
self.awready = Bits(1)(0)
self.wready = Bits(1)(0)
self.bvalid = Bits(1)(0)
self.bresp = Bits(2)(0)
return True

def build_table(
self,
bundles) -> Tuple[Dict[int, BundleSignal], Dict[int, BundleSignal], int]:
"""Build a table of read and write addresses to BundleSignals."""
offset = AxiMMIO.initial_offset
read_table = {}
write_table = {}
for bundle in bundles.to_client_reqs:
if bundle.direction == ChannelDirection.Input:
read_table[offset] = bundle
offset += 4
elif bundle.direction == ChannelDirection.Output:
write_table[offset] = bundle
offset += 4

manifest_loc = 1 << offset.bit_length()
return read_table, write_table, manifest_loc

def build_read(self, manifest_loc: int, bundles):
"""Builds the read side of the MMIO service."""

# Currently just exposes the header and manifest. Not any of the possible
# service requests.

i32 = Bits(32)
i2 = Bits(2)
i1 = Bits(1)

address_written = NamedWire(i1, "address_written")
response_written = NamedWire(i1, "response_written")

# 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")
self.arready = ~req_outstanding

# Capture the address if a the bus transaction occured.
address_written.assign(self.arvalid & ~req_outstanding)
address = self.araddr.reg(self.clk, ce=address_written, name="address")
address_valid = address_written.reg(name="address_valid")
address_words = address[2:] # Lop off the lower two bits.

# 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(i32, "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")
self.rvalid = data_out_valid
self.rdata = data_pipeline.reg(self.clk,
self.rst,
ce=data_pipeline_valid,
name="data_pipeline_reg")
self.rresp = data_pipeline_rresp.reg(self.clk,
self.rst,
ce=data_pipeline_valid,
name="data_pipeline_rresp_reg")
# Clear the `req_outstanding` flag when the response has been transmitted.
response_written.assign(data_out_valid & self.rready)

# Handle reads from the header (< 0x100).
header_upper = address_words[AxiMMIO.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(32), 6)(
[0, 0, MagicNumberLo, MagicNumberHi, VersionNumber, manifest_loc])
header.name = "header"
header_response_valid = address_valid # Zero latency read.
header_out = header[address[2:5]]
header_out.name = "header_out"
header_rresp = i2(0)

# Handle reads from the manifest.
rom_address = NamedWire(
(address_words.as_uint() - (manifest_loc >> 2)).as_bits(30),
"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)

# Mux the output depending on whether or not the address is in the header.
sel = NamedWire(~header_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))

def build_write(self, bundles):
# TODO: this.

# So that we don't wedge the AXI-lite for writes, just ack all of them.
write_happened = Wire(Bits(1))
latched_aw = ControlReg(self.clk, self.rst, [self.awvalid],
[write_happened])
latched_w = ControlReg(self.clk, self.rst, [self.wvalid], [write_happened])
write_happened.assign(latched_aw & latched_w)

self.awready = 1
self.wready = 1
self.bvalid = write_happened
self.bresp = 0


def XrtBSP(user_module):
"""Use the Xilinx RunTime (XRT) shell to implement ESI services and build an
image or emulation package.
Expand Down
2 changes: 1 addition & 1 deletion frontends/PyCDE/src/pycde/esi.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ def param(name: str, type: Type = None):
esi.ESIPureModuleParamOp(name, type_attr)


MMIOReadDataResponse = Bits(32)
MMIOReadDataResponse = Bits(64)


@ServiceDecl
Expand Down
12 changes: 6 additions & 6 deletions frontends/PyCDE/test/test_esi.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,20 @@ def build(self):


# CHECK-LABEL: hw.module @MMIOReq()
# CHECK-NEXT: %c0_i32 = hw.constant 0 : i32
# 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<i32> from "data"]>
# CHECK-NEXT: %chanOutput, %ready = esi.wrap.vr %c0_i32, %false : i32
# CHECK-NEXT: %offset = esi.bundle.unpack %chanOutput from [[B]] : !esi.bundle<[!esi.channel<i32> to "offset", !esi.channel<i32> from "data"]>
# 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: %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"]>
@unittestmodule(esi_sys=True)
class MMIOReq(Module):

@generator
def build(ports):
c32 = Bits(32)(0)
c64 = Bits(64)(0)
c1 = Bits(1)(0)

read_bundle = MMIO.read(AppID("mmio_req"))

data, _ = Channel(Bits(32)).wrap(c32, c1)
data, _ = Channel(Bits(64)).wrap(c64, c1)
_ = read_bundle.unpack(data=data)
1 change: 1 addition & 0 deletions frontends/PyCDE/test/test_xrt.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# XFAIL: *
# RUN: rm -rf %t
# RUN: %PYTHON% %s %t 2>&1
# RUN: ls %t/hw/XrtTop.sv
Expand Down
4 changes: 2 additions & 2 deletions lib/Dialect/ESI/ESIStdServices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ void MMIOServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
{BundledChannel{StringAttr::get(ctxt, "offset"), ChannelDirection::to,
ChannelType::get(ctxt, IntegerType::get(ctxt, 32))},
BundledChannel{StringAttr::get(ctxt, "data"), ChannelDirection::from,
ChannelType::get(ctxt, IntegerType::get(ctxt, 32))}},
ChannelType::get(ctxt, IntegerType::get(ctxt, 64))}},
/*resettable=*/UnitAttr())});
// Write only port.
ports.push_back(ServicePortInfo{
Expand All @@ -112,6 +112,6 @@ void MMIOServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
{BundledChannel{StringAttr::get(ctxt, "offset"), ChannelDirection::to,
ChannelType::get(ctxt, IntegerType::get(ctxt, 32))},
BundledChannel{StringAttr::get(ctxt, "data"), ChannelDirection::to,
ChannelType::get(ctxt, IntegerType::get(ctxt, 32))}},
ChannelType::get(ctxt, IntegerType::get(ctxt, 64))}},
/*resettable=*/UnitAttr())});
}
Loading

0 comments on commit e218a94

Please sign in to comment.