Newer
Older
from enum import Enum
# NEXT TASKS (IMMEDIATE)
# finish fleshing out code, attach buses from the Flow controller to the gearbox
# do the write-logic
# figure out how the Altera gearbox design works (number of pipeline stages and what each does)
# REMAINING WORK
#
# make a quick-and-dirty testbench that selects a random inwidth, random outwidth, and randomly
# starts/stops the input/output and verifies that the sequence of bits coming out is the same
# as that coming in (can do with PRBS) and that bus interface constraints are not violated
# can make a testbench only operating on the indices and the flowcontrol signals that keeps a
# model of the valid/invalid bits and verifies that when a transaction happens, the right bits
# get read/written and the right indices get moved in the right ways
class IndexDisambiguator(Enum):
LAST_OP_UNKNOWN = 0
LAST_OP_WAS_WRITE = 1
LAST_OP_WAS_READ = 2
def __init__(self, *, in_width, out_width): # the * forces keyword args
super().__init__([
# DATA
("data_in", unsigned(in_width)), # FROM SOURCE
("data_out", unsigned(out_width)), # TO DEST
# CONTROL
("valid_in", 1), # FROM SOURCE
("ready_out", 1), # TO SOURCE
("ready_in", 1), # FROM DEST
("valid_out", 1), # TO DEST
("fault", 1)
])
class GearboxBus(Record):
def __init__(self, *, in_width, out_width):
super().__init__(GearboxBusLayout(in_width=in_width, out_width=out_width))
class GearboxFCLayout(Layout):
def __init__(self, *, len_storage):
super().__init__([
# DATA
("read_ptr", unsigned(len(range(len_storage)))), # FROM GEARBOX
("write_ptr", unsigned(len(range(len_storage)))), # FROM GEARBOX
# CONTROL
("write_happens_this_cycle", 1), # TO GEARBOX
("read_happens_this_cycle", 1), # TO GEARBOX
("ready_in", 1), # FROM GEARBOX (FROM DOWNSTREAM)
("valid_in", 1), # FROM GEARBOX (FROM UPSTREAM)
])
class GearboxFCBus(Record):
def __init__(self, *, len_storage):
super().__init__(GearboxFCLayout(len_storage=len_storage))
class GearboxFlowControl(Elaboratable):
def __init__(self, *, in_width, out_width, len_storage):
self.in_width = in_width
self.out_width = out_width
self.len_storage = len_storage
self.bus = GearboxFCBus(len_storage=len_storage)
def elaborate(self, platform):
# The top-level flow control logic works as follows.
# First, we determine which operations are *possible* based on the read/write indices
# and the index disambiguator bit:
# 1) we determine if we have enough invalid bits in the buffer to write-in
# If so, we set ready_out to signal to upstream we're ready to ingest
can_write_this_cycle = Signal(1)
# 2) we determine if we have enough valid bits in the buffer to read-out
# If so, we set valid_out to signal to downstream we're ready to produce
can_read_this_cycle = Signal(1)
# Then, we look at the flow-control signals from upstream/downstream to see which
# transactions will happen, and set the following two bus signals, which are
# used to gate the read/write index updates:
# write_happens_this_cycle
# read_happens_this_cycle
# We replicate signals to avoid repeating "self.bus."
read_ptr = Signal(range(self.len_storage))
m.d.comb += read_ptr.eq(self.bus.read_ptr)
write_ptr = Signal(range(self.len_storage))
m.d.comb += write_ptr.eq(self.bus.write_ptr)
len_storage = self.len_storage
disambiguator = Signal(IndexDisambiguator, reset=IndexDisambiguator.LAST_OP_WAS_READ)
with m.If(read_ptr == write_ptr): # the special case first
with m.Switch(disambiguator):
with m.Case(IndexDisambiguator.LAST_OP_UNKNOWN): # fault
m.d.comb += can_read_this_cycle.eq(0)
m.d.comb += can_write_this_cycle.eq(0)
with m.Case(IndexDisambiguator.LAST_OP_WAS_WRITE): # completely full
m.d.comb += can_read_this_cycle.eq(1)
m.d.comb += can_write_this_cycle.eq(0)
with m.Case(IndexDisambiguator.LAST_OP_WAS_READ): # completely empty
m.d.comb += can_read_this_cycle.eq(0)
m.d.comb += can_write_this_cycle.eq(1)
with m.Elif(read_ptr < write_ptr):
# read_ptr < write_ptr. Here, the valid bits do not wrap, the invalid bits wrap.
# The valid bits are: inclusive [read_ptr, write_ptr) exclusive
# the invalid bits are inclusive [write_ptr, K] inclusive, union with inclusive [0, read_ptr) exclusive
numvalid = Signal(range(len_storage))
m.d.comb += numvalid.eq(write_ptr - read_ptr)
with m.If(numvalid >= self.out_width):
m.d.comb += can_read_this_cycle.eq(1)
with m.If(numinvalid >= self.in_width):
m.d.comb += can_write_this_cycle.eq(1)
with m.Elif(read_ptr > write_ptr):
# write_ptr < read_ptr. Here, the valid bits wrap, and the invalid bits do not wrap.
# The valid bits are: inclusive [read_ptr, K] inclusive, union with inclusive [0, write_ptr) exclusive
# the invalid bits are inclusive [write_ptr, read_ptr) exclusive
m.d.comb += numvalid.eq(len_storage - read_ptr + write_ptr)
with m.If(numvalid >= self.out_width):
m.d.comb += can_read_this_cycle.eq(1)
with m.If(numinvalid >= self.in_width):
m.d.comb += can_write_this_cycle.eq(1)
with m.Else(): # should never happen
m.d.sync += internalfault.eq(1)
return m
class ArbitraryGearbox(Elaboratable):
def __init__(self, *, in_width, out_width):
self.in_width = in_width
self.out_width = out_width
def elaborate(self, platform):
m = Module()
m.submodules.flow_controller = flow_controller = GearboxFlowControl(in_width=self.in_width, out_width=self.out_width, len_storage=len_storage)
#storage = Signal(len_storage, reset=0b001_010_011_100_101_110_111)
#storage = Signal(len_storage, reset= 0b111_110_101_100_011_010_001)
# The buffer is composed of two different flavor of bits:
# Invalid bits CANNOT BE READ OUT CAN BE WRITTEN TO
# Valid bits CAN BE READ OUT CANNOT BE WRITTEN TO
# and the location of those bits are given by the read_ptr and the write_ptr
# We only allow a read operation (which is not idempotent as the read_ptr is advanced) if:
# 1) the downstream interface is signaling ready
# 2) there are out_width valid bits in front of the read_ptr
# Likewise, we only allow a write operation (which is heavily non-idempotent as the write_ptr is advanced
# and the buffer is modified) if:
# 1) the upstream interface is signaling valid (we cannot allow ourselves to ingest invalid data!)
# 2) there are in_width invalid bits in front of the write_ptr
# Naturally, there is a tricky edge case which requires an extra bit (literally) of disambiguation.
# If write_ptr == read_ptr we don't know if the buffer is entirely full or entirely empty.
# If the last operation was a read, and we find write_ptr == read_ptr, we know it's empty
# If the last operation was a write, and we find write_ptr == read_ptr, we know it's full
# We keep a bit of state -- which is only relevant if write_ptr == read_ptr, to keep track of
# this: IndexDisambiguator, which can either be LAST_OP_WAS_WRITE or LAST_OP_WAS_READ
with m.If(self.bus.fault == 0):
# read index update:
with m.If(read_ptr + self.out_width >= len_storage):
m.d.sync += read_ptr.eq(read_ptr + self.out_width - len_storage)
with m.Else():
m.d.sync += read_ptr.eq(read_ptr + self.out_width)
# read-out bits case analysis:
with m.If(read_ptr + self.out_width <= len_storage):
m.d.comb += self.bus.data_out.eq(storage.bit_select(read_ptr, self.out_width))
with m.Else():
with m.Switch(read_ptr):
for cand_rptr in range(len_storage - self.out_width + 1, len_storage):
with m.Case(cand_rptr):
m.d.comb += loop.eq(1)
non_wrapping_bitwidth = len_storage - cand_rptr
wrapping_bitwidth = self.out_width + cand_rptr - len_storage
m.d.comb += self.bus.data_out.eq(Cat(
storage.bit_select(cand_rptr, non_wrapping_bitwidth), # the non-wrapping bits are less-significant
storage.bit_select(0, wrapping_bitwidth) # ...than the wrapping bit
))
# write index update:
with m.If(write_ptr + self.in_width <= len_storage):
m.d.sync += write_ptr.eq(write_ptr + self.in_width - len_storage)
with m.Else():
m.d.sync += write_ptr.eq(write_ptr + self.in_width)
# actually write-in the bits
with m.If(write_ptr + self.out_width <= len_storage):
m.d.comb += self.bus.data_out.eq(storage.bit_select(read_ptr, self.out_width))
with m.Else():
with m.Switch(write_ptr):
for cand_wptr in range(len_storage - self.out_width + 1, len_storage):
with m.Case(cand_wptr):
m.d.comb += loop.eq(1)
non_wrapping_bitwidth = len_storage - cand_wptr
wrapping_bitwidth = self.out_width + cand_wptr - len_storage
m.d.comb += self.bus.data_out.eq(Cat(
storage.bit_select(cand_wptr, non_wrapping_bitwidth), # the non-wrapping bits are less-significant
storage.bit_select(0, wrapping_bitwidth) # ...than the wrapping bit
))
#m.d.comb += Assume((self.bus.data_in == 3) |( self.bus.data_in==5))
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
class IdempotentGearbox(Elaboratable):
def __init__(self, *, in_width, out_width):
self.in_width = in_width
self.out_width = out_width
assert(in_width == out_width)
self.bus = GearboxBus(in_width=in_width, out_width=out_width)
def elaborate(self, platform):
m = Module()
skid_buffer = Signal(self.in_width)
skid_buffer_frozen = Signal(1)
# we signal ready to upstream if and only if the skid buffer is empty
m.d.comb += self.bus.ready_out.eq(~skid_buffer_frozen)
# should we capture a value in the skid buffer?
# we should if upstream has data for us *and* downstream cannot accept
# If we want to fill our skid buffer, we need:
# 1. valid input data (bus.valid_in == 1)
# 2. a buffer that is empty (skid_buffer_frozen == 0)
# 3. stalled downstream (bus.ready_in == 0) & (bus.valid_out == 1)
with m.If(self.bus.valid_in & (~skid_buffer_frozen) & (~self.bus.ready_in) & (self.bus.valid_out)):
m.d.sync += skid_buffer_frozen.eq(1)
# if downstream is accepting data again, we will flush our skid buffer
with m.If(self.bus.ready_in == 1):
m.d.sync += skid_buffer_frozen.eq(0)
# Stalled == (bus.ready_in == 0 & bus.valid_out == 1)
# so not stalled = !(bus.ready_in == 0 & bus.valid_out == 1)
# = bus.ready_in == 1 | bus.valid_out == 0
# by de Morgan
with m.If((self.bus.ready_in) | (~self.bus.valid_out)):
m.d.sync += self.bus.valid_out.eq(self.bus.valid_in | skid_buffer_frozen)
# data path
# always clock data into the skid buffer as long as the buffer isn't filled
with m.If(skid_buffer_frozen == 0):
m.d.sync += skid_buffer.eq(self.bus.data_in)
# not stalled condition
with m.If((self.bus.ready_in) | (~self.bus.valid_out)):
with m.If(skid_buffer_frozen == 1):
m.d.sync += self.bus.data_out.eq(skid_buffer)
with m.Elif(self.bus.valid_in == 1):
m.d.sync += self.bus.data_out.eq(self.bus.data_in)
with m.If(Past(ResetSignal()) == 0):
with m.If((self.bus.valid_in == 1) & (Past(self.bus.valid_in) == 1)):
m.d.comb += Assert(Rose(self.bus.valid_out))
return m
# This is non-synthesizable but is intended to provide a model for non-unitary ratio gearbox
# in order to do formal verification and model-checking against both the unitary-ratio gearbox
# and also the arbitrary ratio gearbox.
class GoldenGearboxModel(Elaboratable):
def __init__(self, *, in_width, out_width, sim_memory_size):
self.in_width = in_width
self.out_width = out_width
assert(in_width == out_width)
self.memory = Signal(sim_memory_size)
self.bus = GearboxBus(in_width=in_width, out_width=out_width)
def elaborate(self, platform):
write_ptr = Signal(range(sim_memory_size))
read_ptr = Signal(range(sim_memory_size))
m = Module()
m.submodules.gearbox = gearbox = IdempotentGearbox(in_width=(3), out_width=3)
#counter = Signal(8)
#m.d.sync += counter.eq(counter+1)
# with m.If(counter == 3):
# m.d.comb += gearbox.bus.valid_in.eq(1)
m.d.comb += gearbox.bus.data_in.eq(AnySeq(3))
m.d.comb += gearbox.bus.ready_in.eq(AnySeq(1))
m.d.comb += gearbox.bus.valid_in.eq(AnySeq(1))