Newer
Older
from enum import Enum
class IndexDisambiguator(Enum):
LAST_OP_UNKNOWN = 0
LAST_OP_WAS_WRITE = 1
LAST_OP_WAS_READ = 2
class GearboxBusLayout(Layout):
def __init__(self, *, in_width, out_width):
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))
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class GearboxFlow(Elaboratable):
def __init__(self):
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 internal signals, which are
# used to gate the read/write index updates:
write_happens_this_cycle = Signal(1)
read_happens_this_cycle = Signal(1)
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)
m.d.comb += self.bus.fault.eq(1)
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
# We first calculate the number of valid bits:
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)
# We calculate the number of invalid bits:
numinvalid = Signal(range(len_storage))
m.d.comb += numinvalid.eq(len_storage - write_ptr + read_ptr)
with m.If(numinvalid >= self.in_width):
m.d.comb += can_write_this_cycle.eq(1)
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()
loop = Signal(1)
len_storage = self.in_width + self.out_width
#storage = Signal(len_storage, reset=0b001_010_011_100_101_110_111)
#storage = Signal(len_storage, reset= 0b111_110_101_100_011_010_001)
disambiguator = Signal(IndexDisambiguator, reset=IndexDisambiguator.LAST_OP_WAS_READ)
# 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
# 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
# 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# 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_happens_this_cycle == 1):
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_happens_this_cycle == 1):
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
))
return m
class DummyPlug(Elaboratable):
m.submodules.gearbox = gearbox = ArbitraryGearbox(in_width=(9-3), out_width=3)
counter = Signal(8)
m.d.sync += counter.eq(counter+1)
with m.If(counter == 3):
m.d.comb += gearbox.bus.data_in.eq(1)