Skip to content

Commit

Permalink
[ESI] FIFO with ESI channels (#8004)
Browse files Browse the repository at this point in the history
Adds an op which provides a FIFO with ESI channels (FIFO signaling semantics only for now) on both ends.
  • Loading branch information
teqdruid authored Dec 18, 2024
1 parent 44ea9a8 commit 6cabe3c
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 4 deletions.
21 changes: 21 additions & 0 deletions include/circt/Dialect/ESI/ESIChannels.td
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,27 @@ def PipelineStageOp : ESI_Physical_Op<"stage", [
// Misc operations
//===----------------------------------------------------------------------===//

def FIFOOp : ESI_Physical_Op<"fifo", []> {
let summary = "A FIFO with ESI channel connections";
let description = [{
A FIFO is a first-in-first-out buffer. This operation is a simple FIFO
which can be used to connect two ESI channels. The ESI channels MUST have
FIFO signaling semantics.
}];

let arguments = (ins
ClockType:$clk, I1:$rst, ChannelType:$input,
ConfinedAttr<I64Attr, [IntMinValue<1>]>:$depth
);
let results = (outs ChannelType:$output);

let assemblyFormat = [{
`in` $input `clk` $clk `rst` $rst `depth` $depth attr-dict
`:` type($input) `->` type($output)
}];
let hasVerifier = 1;
}

def CosimToHostEndpointOp : ESI_Physical_Op<"cosim.to_host", []> {
let summary = "Co-simulation endpoint sending data to the host.";
let description = [{
Expand Down
13 changes: 13 additions & 0 deletions integration_test/Dialect/ESI/widgets/esi_widgets.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// REQUIRES: iverilog,cocotb

// Test the original HandshakeToHW flow.

// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --lower-esi-to-hw --lower-seq-fifo --lower-seq-hlmem --lower-seq-to-sv --lower-verif-to-sv --sv-trace-iverilog --prettify-verilog --export-verilog -o %t.hw.mlir > %t.sv
// RUN: circt-cocotb-driver.py --objdir=%T --topLevel=fifo1 --pythonModule=esi_widgets --pythonFolder="%S" %t.sv %esi_prims

module attributes {circt.loweringOptions = "disallowLocalVariables"} {
hw.module @fifo1(in %clk: !seq.clock, in %rst: i1, in %in: !esi.channel<i32, FIFO>, out out: !esi.channel<i32, FIFO(2)>) {
%fifo = esi.fifo in %in clk %clk rst %rst depth 12 : !esi.channel<i32, FIFO> -> !esi.channel<i32, FIFO(2)>
hw.output %fifo : !esi.channel<i32, FIFO(2)>
}
}
151 changes: 151 additions & 0 deletions integration_test/Dialect/ESI/widgets/esi_widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from typing import List, Optional
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge


async def init(dut, timeout: Optional[int] = None):
# Create a 10ns period (100MHz) clock on port clock
clk = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clk.start()) # Start the clock

# Reset
dut.rst.value = 1
for i in range(10):
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)

if timeout is None:
return

async def timeout_fn():
for i in range(timeout):
await RisingEdge(dut.clk)
assert False, "Timeout"

cocotb.start_soon(timeout_fn())


class ESIInputPort:

async def write(self, data):
raise RuntimeError("Must be implemented by subclass")


class ESIFifoInputPort(ESIInputPort):

def __init__(self, dut, name):
self.dut = dut
self.name = name
self.data = getattr(dut, name)
self.rden = getattr(dut, f"{name}_rden")
self.empty = getattr(dut, f"{name}_empty")
# Configure initial state
self.empty.value = 1

async def write(self, data: int):
self.data.value = data
self.empty.value = 0
await RisingEdge(self.dut.clk)
while self.rden.value == 0:
await RisingEdge(self.dut.clk)
self.empty.value = 1


class ESIOutputPort:

async def read(self) -> Optional[int]:
raise RuntimeError("Must be implemented by subclass")

async def cmd_read(self):
raise RuntimeError("Must be implemented by subclass")


class ESIFifoOutputPort(ESIOutputPort):

def __init__(self, dut, name: str, latency: int = 0):
self.dut = dut
self.name = name
self.data = getattr(dut, name)
self.rden = getattr(dut, f"{name}_rden")
self.empty = getattr(dut, f"{name}_empty")
self.latency = latency
# Configure initial state
self.rden.value = 0
self.running = 0
self.q: List[int] = []

async def init_read(self):

async def read_after_latency():
for i in range(self.latency):
await RisingEdge(self.dut.clk)
self.q.append(self.data.value)

self.running = 1
self.empty.value = 0
while await RisingEdge(self.dut.clk):
if self.rden.value == 1:
cocotb.start_soon(read_after_latency())

async def read(self) -> Optional[int]:
if len(self.q) == 0:
await RisingEdge(self.dut.clk)
return self.q.pop(0)

async def cmd_read(self):
if self.running == 0:
cocotb.start_soon(self.init_read())

while self.empty.value == 1:
await RisingEdge(self.dut.clk)
self.rden.value = 1
await RisingEdge(self.dut.clk)
self.rden.value = 0


@cocotb.test()
async def fillAndDrain(dut):
in_port = ESIFifoInputPort(dut, "in")
out_port = ESIFifoOutputPort(dut, "out", 2)
await init(dut, timeout=10000)

for i in range(10):
for i in range(12):
await in_port.write(i)
for i in range(12):
await out_port.cmd_read()
for i in range(12):
data = await out_port.read()
# print(f"{i:2}: 0x{int(data):016x}")
assert data == i

for i in range(4):
await RisingEdge(dut.clk)


@cocotb.test()
async def backToBack(dut):
in_port = ESIFifoInputPort(dut, "in")
out_port = ESIFifoOutputPort(dut, "out", 2)
await init(dut)

NUM_ITERS = 500

async def write():
for i in range(NUM_ITERS):
await in_port.write(i)

cocotb.start_soon(write())

for i in range(NUM_ITERS):
await out_port.cmd_read()

for i in range(NUM_ITERS):
data = await out_port.read()
# print(f"{i:2}: 0x{int(data):016x}")
assert data == i

for i in range(4):
await RisingEdge(dut.clk)
16 changes: 16 additions & 0 deletions lib/Dialect/ESI/ESIOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ LogicalResult ChannelBufferOp::verify() {
return success();
}

//===----------------------------------------------------------------------===//
// FIFO functions.
//===----------------------------------------------------------------------===//

LogicalResult FIFOOp::verify() {
ChannelType inputType = getInput().getType();
if (inputType.getSignaling() != ChannelSignaling::FIFO)
return emitOpError("only supports FIFO signaling on input");
ChannelType outputType = getOutput().getType();
if (outputType.getSignaling() != ChannelSignaling::FIFO)
return emitOpError("only supports FIFO signaling on output");
if (outputType.getInner() != inputType.getInner())
return emitOpError("input and output types must match");
return success();
}

//===----------------------------------------------------------------------===//
// PipelineStageOp functions.
//===----------------------------------------------------------------------===//
Expand Down
67 changes: 63 additions & 4 deletions lib/Dialect/ESI/Passes/ESILowerPhysical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

#include "../PassDetails.h"

#include "circt/Dialect/Comb/CombOps.h"
#include "circt/Dialect/ESI/ESIOps.h"
#include "circt/Dialect/HW/HWOps.h"
#include "circt/Dialect/Seq/SeqOps.h"
#include "circt/Support/BackedgeBuilder.h"
#include "circt/Support/LLVM.h"

Expand Down Expand Up @@ -78,6 +80,64 @@ LogicalResult ChannelBufferLowering::matchAndRewrite(
return success();
}

namespace {
/// Lower `ChannelBufferOp`s, breaking out the various options. For now, just
/// replace with the specified number of pipeline stages (since that's the only
/// option).
struct FIFOLowering : public OpConversionPattern<FIFOOp> {
public:
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(FIFOOp, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const final;
};
} // anonymous namespace

LogicalResult
FIFOLowering::matchAndRewrite(FIFOOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const {
auto loc = op.getLoc();
auto outputType = op.getType();
BackedgeBuilder bb(rewriter, loc);
auto i1 = rewriter.getI1Type();
auto c1 = rewriter.create<hw::ConstantOp>(loc, rewriter.getI1Type(),
rewriter.getBoolAttr(true));
if (op.getInput().getType().getDataDelay() != 0)
return op.emitOpError(
"currently only supports input channels with zero data delay");

Backedge inputEn = bb.get(i1);
auto unwrapPull = rewriter.create<UnwrapFIFOOp>(loc, op.getInput(), inputEn);

Backedge outputRdEn = bb.get(i1);
auto seqFifo = rewriter.create<seq::FIFOOp>(
loc, outputType.getInner(), i1, i1, Type(), Type(), unwrapPull.getData(),
outputRdEn, inputEn, op.getClk(), op.getRst(), op.getDepthAttr(),
rewriter.getI64IntegerAttr(outputType.getDataDelay()), IntegerAttr(),
IntegerAttr());
auto inputNotEmpty =
rewriter.create<comb::XorOp>(loc, unwrapPull.getEmpty(), c1);
inputNotEmpty->setAttr("sv.namehint",
rewriter.getStringAttr("inputNotEmpty"));
auto seqFifoNotFull =
rewriter.create<comb::XorOp>(loc, seqFifo.getFull(), c1);
seqFifoNotFull->setAttr("sv.namehint",
rewriter.getStringAttr("seqFifoNotFull"));
inputEn.setValue(
rewriter.create<comb::AndOp>(loc, inputNotEmpty, seqFifoNotFull));
static_cast<Value>(inputEn).getDefiningOp()->setAttr(
"sv.namehint", rewriter.getStringAttr("inputEn"));

auto output =
rewriter.create<WrapFIFOOp>(loc, mlir::TypeRange{outputType, i1},
seqFifo.getOutput(), seqFifo.getEmpty());
outputRdEn.setValue(output.getRden());

rewriter.replaceOp(op, output.getChanOutput());
return success();
}

namespace {
/// Lower pure modules into hw.modules.
struct PureModuleLowering : public OpConversionPattern<ESIPureModuleOp> {
Expand Down Expand Up @@ -186,13 +246,12 @@ void ESIToPhysicalPass::runOnOperation() {
// Set up a conversion and give it a set of laws.
ConversionTarget target(getContext());
target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
target.addIllegalOp<ChannelBufferOp>();
target.addIllegalOp<ESIPureModuleOp>();
target.addIllegalOp<ChannelBufferOp, ESIPureModuleOp, FIFOOp>();

// Add all the conversion patterns.
RewritePatternSet patterns(&getContext());
patterns.insert<ChannelBufferLowering>(&getContext());
patterns.insert<PureModuleLowering>(&getContext());
patterns.insert<ChannelBufferLowering, PureModuleLowering, FIFOLowering>(
&getContext());

// Run the conversion.
if (failed(
Expand Down
8 changes: 8 additions & 0 deletions test/Dialect/ESI/errors.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ hw.module @test(in %m : !sv.modport<@IData::@Noexist>) {

// -----

hw.module @testFifoTypes(in %clk: !seq.clock, in %rst: i1, in %a: !esi.channel<i32, FIFO>, in %b: !esi.channel<i16, FIFO(2)>) {
// expected-error @+1 {{input and output types must match}}
%fifo = esi.fifo in %a clk %clk rst %rst depth 12 : !esi.channel<i32, FIFO> -> !esi.channel<i16, FIFO(2)>
hw.output %fifo : !esi.channel<i16, FIFO(2)>
}

// -----

esi.service.decl @HostComms {
esi.service.port @Send : !esi.bundle<[!esi.channel<i16> from "send"]>
}
Expand Down
14 changes: 14 additions & 0 deletions test/Dialect/ESI/lowering.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,17 @@ hw.module @i3LoopbackOddNames(in %in: !esi.channel<i3>, out out: !esi.channel<i3
esi.portReadySuffix="_letErRip", esi.portOutSuffix="_out"} {
hw.output %in : !esi.channel<i3>
}

// CHECK-LABEL: hw.module @fifo1(in %clk : !seq.clock, in %rst : i1, in %in : !esi.channel<i32, FIFO>, out out : !esi.channel<i32, FIFO(2)>) {
// CHECK-NEXT: %true = hw.constant true
// CHECK-NEXT: %data, %empty = esi.unwrap.fifo %in, [[R2:%.+]] : !esi.channel<i32, FIFO>
// CHECK-NEXT: %out, %full, %empty_0 = seq.fifo depth 12 rd_latency 2 in %data rdEn %rden wrEn [[R2]] clk %clk rst %rst : i32
// CHECK-NEXT: [[R0:%.+]] = comb.xor %empty, %true {sv.namehint = "inputNotEmpty"} : i1
// CHECK-NEXT: [[R1:%.+]] = comb.xor %full, %true {sv.namehint = "seqFifoNotFull"} : i1
// CHECK-NEXT: [[R2]] = comb.and [[R0]], [[R1]] {sv.namehint = "inputEn"} : i1
// CHECK-NEXT: %chanOutput, %rden = esi.wrap.fifo %out, %empty_0 : !esi.channel<i32, FIFO(2)>
// CHECK-NEXT: hw.output %chanOutput : !esi.channel<i32, FIFO(2)>
hw.module @fifo1(in %clk: !seq.clock, in %rst: i1, in %in: !esi.channel<i32, FIFO>, out out: !esi.channel<i32, FIFO(2)>) {
%fifo = esi.fifo in %in clk %clk rst %rst depth 12 : !esi.channel<i32, FIFO> -> !esi.channel<i32, FIFO(2)>
hw.output %fifo : !esi.channel<i32, FIFO(2)>
}

0 comments on commit 6cabe3c

Please sign in to comment.