f843ddf4efff933e38c7a8c66e6fdfc12f03a084
[soc.git] / src / soc / regfile / sram_wrapper.py
1 # SPDX-License-Identifier: LGPLv3+
2 # Copyright (C) 2022 Cesar Strauss <cestrauss@gmail.com>
3 # Sponsored by NLnet and NGI POINTER under EU Grants 871528 and 957073
4 # Part of the Libre-SOC Project.
5
6 """
7 Wrapper around a single port (1R or 1W) SRAM, to make a multi-port regfile.
8
9 This SRAM primitive has one cycle delay for reads, and, after a write,
10 it reads the value just written. The goal is to use it to make at least an
11 1W2R regfile.
12
13 See https://bugs.libre-soc.org/show_bug.cgi?id=781 and
14 https://bugs.libre-soc.org/show_bug.cgi?id=502
15 """
16
17 import unittest
18
19 from nmigen import Elaboratable, Module, Memory, Signal, Repl, Mux
20 from nmigen.back import rtlil
21 from nmigen.sim import Simulator
22 from nmigen.asserts import Assert, Assume, Past, AnyConst
23
24 from nmutil.formaltest import FHDLTestCase
25 from nmutil.gtkw import write_gtkw
26
27
28 class SinglePortSRAM(Elaboratable):
29 """
30 Model of a single port SRAM, which can be simulated, verified and/or
31 synthesized to an FPGA.
32
33 :param addr_width: width of the address bus
34 :param data_width: width of the data bus
35 :param we_width: number of write enable lines
36
37 .. note:: The debug read port is meant only to assist in formal proofs!
38 """
39 def __init__(self, addr_width, data_width, we_width):
40 self.addr_width = addr_width
41 self.data_width = data_width
42 self.we_width = we_width
43 self.d = Signal(data_width); """ write data"""
44 self.q = Signal(data_width); """read data"""
45 self.a = Signal(addr_width); """ read/write address"""
46 self.we = Signal(we_width); """write enable"""
47 self.dbg_a = Signal(addr_width); """debug read port address"""
48 self.dbg_q = Signal(data_width); """debug read port data"""
49
50 def elaborate(self, _):
51 m = Module()
52 # backing memory
53 depth = 1 << self.addr_width
54 granularity = self.data_width // self.we_width
55 mem = Memory(width=self.data_width, depth=depth)
56 # create read and write ports
57 # By connecting the same address to both ports, they behave, in fact,
58 # as a single, "half-duplex" port.
59 # The transparent attribute means that, on a write, we read the new
60 # value, on the next cycle
61 # Note that nmigen memories have a one cycle delay, for reads,
62 # by default
63 m.submodules.rdport = rdport = mem.read_port(transparent=True)
64 m.submodules.wrport = wrport = mem.write_port(granularity=granularity)
65 # duplicate the address to both ports
66 m.d.comb += wrport.addr.eq(self.a)
67 m.d.comb += rdport.addr.eq(self.a)
68 # write enable
69 m.d.comb += wrport.en.eq(self.we)
70 # read and write data
71 m.d.comb += wrport.data.eq(self.d)
72 m.d.comb += self.q.eq(rdport.data)
73 # the debug port is an asynchronous read port, allowing direct access
74 # to a given memory location by the formal engine
75 m.submodules.dbgport = dbgport = mem.read_port(domain="comb")
76 m.d.comb += dbgport.addr.eq(self.dbg_a)
77 m.d.comb += self.dbg_q.eq(dbgport.data)
78 return m
79
80 def ports(self):
81 return [
82 self.d,
83 self.a,
84 self.we,
85 self.q
86 ]
87
88
89 def create_ilang(dut, ports, test_name):
90 vl = rtlil.convert(dut, name=test_name, ports=ports)
91 with open("%s.il" % test_name, "w") as f:
92 f.write(vl)
93
94
95 class SinglePortSRAMTestCase(FHDLTestCase):
96 @staticmethod
97 def test_simple_rtlil():
98 """
99 Generate a simple SRAM. Try ``read_rtlil mem_simple.il; proc; show``
100 from a yosys prompt, to see the memory primitives, and
101 ``read_rtlil mem_simple.il; synth; show`` to see it implemented as
102 flip-flop RAM
103 """
104 dut = SinglePortSRAM(2, 4, 2)
105 create_ilang(dut, dut.ports(), "mem_simple")
106
107 @staticmethod
108 def test_blkram_rtlil():
109 """
110 Generates a bigger SRAM.
111 Try ``read_rtlil mem_blkram.il; synth_ecp5; show`` from a yosys
112 prompt, to see it implemented as block RAM
113 """
114 dut = SinglePortSRAM(10, 16, 2)
115 create_ilang(dut, dut.ports(), "mem_blkram")
116
117 def test_sram_model(self):
118 """
119 Simulate some read/write/modify operations on the SRAM model
120 """
121 dut = SinglePortSRAM(7, 32, 4)
122 sim = Simulator(dut)
123 sim.add_clock(1e-6)
124
125 def process():
126 # 1) write 0x12_34_56_78 to address 0
127 yield dut.a.eq(0)
128 yield dut.d.eq(0x12_34_56_78)
129 yield dut.we.eq(0b1111)
130 yield
131 # 2) write 0x9A_BC_DE_F0 to address 1
132 yield dut.a.eq(1)
133 yield dut.d.eq(0x9A_BC_DE_F0)
134 yield dut.we.eq(0b1111)
135 yield
136 # ... and read value just written to address 0
137 self.assertEqual((yield dut.q), 0x12_34_56_78)
138 # 3) prepare to read from address 0
139 yield dut.d.eq(0)
140 yield dut.we.eq(0b0000)
141 yield dut.a.eq(0)
142 yield
143 # ... and read value just written to address 1
144 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
145 # 4) prepare to read from address 1
146 yield dut.a.eq(1)
147 yield
148 # ... and read value from address 0
149 self.assertEqual((yield dut.q), 0x12_34_56_78)
150 # 5) write 0x9A and 0xDE to bytes 1 and 3, leaving
151 # bytes 0 and 2 unchanged
152 yield dut.a.eq(0)
153 yield dut.d.eq(0x9A_FF_DE_FF)
154 yield dut.we.eq(0b1010)
155 yield
156 # ... and read value from address 1
157 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
158 # 6) nothing more to do
159 yield dut.d.eq(0)
160 yield dut.we.eq(0)
161 yield
162 # ... other than confirm that bytes 1 and 3 were modified
163 # correctly
164 self.assertEqual((yield dut.q), 0x9A_34_DE_78)
165
166 sim.add_sync_process(process)
167 traces = ['rdport.clk', 'a[6:0]', 'we[3:0]', 'd[31:0]', 'q[31:0]']
168 write_gtkw('test_sram_model.gtkw', 'test_sram_model.vcd',
169 traces, module='top')
170 sim_writer = sim.write_vcd('test_sram_model.vcd')
171 with sim_writer:
172 sim.run()
173
174 def test_model_sram_proof(self):
175 """
176 Formal proof of the single port SRAM model
177 """
178 m = Module()
179 # 128 x 32-bit, 8-bit granularity
180 m.submodules.dut = dut = SinglePortSRAM(7, 32, 4)
181 gran = len(dut.d) // len(dut.we) # granularity
182 # choose a single random memory location to test
183 a_const = AnyConst(dut.a.shape())
184 # choose a single byte lane to test (one-hot encoding)
185 we_mask = Signal.like(dut.we)
186 # ... by first creating a random bit pattern
187 we_const = AnyConst(dut.we.shape())
188 # ... and zeroing all but the first non-zero bit
189 m.d.comb += we_mask.eq(we_const & (-we_const))
190 # holding data register
191 d_reg = Signal(gran)
192 # for some reason, simulated formal memory is not zeroed at reset
193 # ... so, remember whether we wrote it, at least once.
194 wrote = Signal()
195 # if our memory location and byte lane is being written
196 # ... capture the data in our holding register
197 with m.If(dut.a == a_const):
198 for i in range(len(dut.we)):
199 with m.If(we_mask[i] & dut.we[i]):
200 m.d.sync += d_reg.eq(dut.d[i*gran:i*gran+gran])
201 m.d.sync += wrote.eq(1)
202 # if our memory location is being read
203 # ... and the holding register has valid data
204 # ... then its value must match the memory output, on the given lane
205 with m.If((Past(dut.a) == a_const) & wrote):
206 for i in range(len(dut.we)):
207 with m.If(we_mask[i]):
208 m.d.sync += Assert(d_reg == dut.q[i*gran:i*gran+gran])
209
210 # the following is needed for induction, where an unreachable state
211 # (memory and holding register differ) is turned into an illegal one
212 # first, get the value stored in our memory location, using its debug
213 # port
214 stored = Signal.like(dut.q)
215 m.d.comb += dut.dbg_a.eq(a_const)
216 m.d.comb += stored.eq(dut.dbg_q)
217 # now, ensure that the value stored in memory is always in sync
218 # with the holding register
219 with m.If(wrote):
220 for i in range(len(dut.we)):
221 with m.If(we_mask[i]):
222 m.d.sync += Assert(d_reg == stored[i*gran:i*gran+gran])
223
224 self.assertFormal(m, mode="prove", depth=2)
225
226
227 class PhasedDualPortRegfile(Elaboratable):
228 """
229 Builds, from a pair of 1RW blocks, a pseudo 1W/1R RAM, where the
230 read port works every cycle, but the write port is only available on
231 either even (1eW/1R) or odd (1oW/1R) cycles.
232
233 :param addr_width: width of the address bus
234 :param data_width: width of the data bus
235 :param we_width: number of write enable lines
236 :param write_phase: indicates on which phase the write port will
237 accept data
238 :param transparent: whether a simultaneous read and write returns the
239 new value (True) or the old value (False)
240
241 .. note:: The debug read port is meant only to assist in formal proofs!
242 """
243
244 def __init__(self, addr_width, data_width, we_width, write_phase,
245 transparent=False):
246 self.addr_width = addr_width
247 self.data_width = data_width
248 self.we_width = we_width
249 self.write_phase = write_phase
250 self.transparent = transparent
251 self.wr_addr_i = Signal(addr_width); """write port address"""
252 self.wr_data_i = Signal(data_width); """write port data"""
253 self.wr_we_i = Signal(we_width); """write port enable"""
254 self.rd_addr_i = Signal(addr_width); """read port address"""
255 self.rd_data_o = Signal(data_width); """read port data"""
256 self.phase = Signal(); """even/odd cycle indicator"""
257 self.dbg_a = Signal(addr_width); """debug read port address"""
258 self.dbg_q1 = Signal(data_width); """debug read port data (1st mem)"""
259 self.dbg_q2 = Signal(data_width); """debug read port data (2nd mem)"""
260
261 def elaborate(self, _):
262 m = Module()
263 # instantiate the two 1RW memory blocks
264 mem1 = SinglePortSRAM(self.addr_width, self.data_width, self.we_width)
265 mem2 = SinglePortSRAM(self.addr_width, self.data_width, self.we_width)
266 m.submodules.mem1 = mem1
267 m.submodules.mem2 = mem2
268 # wire write port to first memory, and its output to the second
269 m.d.comb += mem1.d.eq(self.wr_data_i)
270 m.d.comb += mem2.d.eq(mem1.q)
271 # holding registers for the write port of the second memory
272 last_wr_addr = Signal(self.addr_width)
273 last_wr_we = Signal(self.we_width)
274 # do the read and write address coincide?
275 same_read_write = Signal()
276 with m.If(self.phase == self.write_phase):
277 # write phase, start a write on the first memory
278 m.d.comb += mem1.a.eq(self.wr_addr_i)
279 m.d.comb += mem1.we.eq(self.wr_we_i)
280 # save write address and write select for repeating the write
281 # on the second memory, later
282 m.d.sync += last_wr_we.eq(self.wr_we_i)
283 m.d.sync += last_wr_addr.eq(self.wr_addr_i)
284 # start a read on the second memory
285 m.d.comb += mem2.a.eq(self.rd_addr_i)
286 # output previously read data from the first memory
287 m.d.comb += self.rd_data_o.eq(mem1.q)
288 if self.transparent:
289 # remember whether we are reading from the same location we are
290 # writing
291 m.d.sync += same_read_write.eq(self.rd_addr_i == self.wr_addr_i)
292 with m.Else():
293 # read phase, write last written data on second memory
294 m.d.comb += mem2.a.eq(last_wr_addr)
295 m.d.comb += mem2.we.eq(last_wr_we)
296 # start a read on the first memory
297 m.d.comb += mem1.a.eq(self.rd_addr_i)
298 if self.transparent:
299 with m.If(same_read_write):
300 # when transparent, and read and write addresses coincide,
301 # output the data just written
302 m.d.comb += self.rd_data_o.eq(mem1.q)
303 with m.Else():
304 # otherwise, output previously read data
305 # from the second memory
306 m.d.comb += self.rd_data_o.eq(mem2.q)
307 else:
308 # always output the read data from the second memory,
309 # if not transparent
310 m.d.comb += self.rd_data_o.eq(mem2.q)
311 # our debug port allow the formal engine to inspect the content of
312 # a fixed, arbitrary address, on our memory blocks.
313 # wire it to their debug ports.
314 m.d.comb += mem1.dbg_a.eq(self.dbg_a)
315 m.d.comb += mem2.dbg_a.eq(self.dbg_a)
316 m.d.comb += self.dbg_q1.eq(mem1.dbg_q)
317 m.d.comb += self.dbg_q2.eq(mem2.dbg_q)
318
319 return m
320
321
322 class PhasedDualPortRegfileTestCase(FHDLTestCase):
323
324 def do_test_phased_dual_port_regfile(self, write_phase, transparent):
325 """
326 Simulate some read/write/modify operations on the phased write memory
327 """
328 dut = PhasedDualPortRegfile(7, 32, 4, write_phase, transparent)
329 sim = Simulator(dut)
330 sim.add_clock(1e-6)
331
332 # compare read data with previously written data
333 # and start a new read
334 def read(rd_addr_i, expected=None):
335 if expected is not None:
336 self.assertEqual((yield dut.rd_data_o), expected)
337 yield dut.rd_addr_i.eq(rd_addr_i)
338
339 # start a write, and set write phase
340 def write(wr_addr_i, wr_we_i, wr_data_i):
341 yield dut.wr_addr_i.eq(wr_addr_i)
342 yield dut.wr_we_i.eq(wr_we_i)
343 yield dut.wr_data_i.eq(wr_data_i)
344 yield dut.phase.eq(write_phase)
345
346 # disable writes, and start read phase
347 def skip_write():
348 yield dut.wr_addr_i.eq(0)
349 yield dut.wr_we_i.eq(0)
350 yield dut.wr_data_i.eq(0)
351 yield dut.phase.eq(~write_phase)
352
353 # writes a few values on the write port, and read them back
354 # ... reads can happen every cycle
355 # ... writes, only every two cycles.
356 # since reads have a one cycle delay, the expected value on
357 # each read refers to the last read performed, not the
358 # current one, which is in progress.
359 def process():
360 yield from read(0)
361 yield from write(0x42, 0b1111, 0x12345678)
362 yield
363 yield from read(0x42)
364 yield from skip_write()
365 yield
366 yield from read(0x42)
367 yield from write(0x43, 0b1111, 0x9ABCDEF0)
368 yield
369 yield from read(0x43, 0x12345678)
370 yield from skip_write()
371 yield
372 yield from read(0x42, 0x12345678)
373 yield from write(0x43, 0b1001, 0xF0FFFF9A)
374 yield
375 yield from read(0x43, 0x9ABCDEF0)
376 yield from skip_write()
377 yield
378 yield from read(0x43, 0x12345678)
379 yield from write(0x42, 0b0110, 0xFF5634FF)
380 yield
381 yield from read(0x42, 0xF0BCDE9A)
382 yield from skip_write()
383 yield
384 yield from read(0, 0xF0BCDE9A)
385 yield from write(0, 0, 0)
386 yield
387 yield from read(0, 0x12563478)
388 yield from skip_write()
389 yield
390 # try reading and writing to the same location, simultaneously
391 yield from read(0x42)
392 yield from write(0x42, 0b1111, 0x55AA9966)
393 yield
394 # ... and read again
395 yield from read(0x42)
396 yield from skip_write()
397 yield
398 if transparent:
399 # returns the value just written
400 yield from read(0, 0x55AA9966)
401 else:
402 # returns the old value
403 yield from read(0, 0x12563478)
404 yield from write(0, 0, 0)
405 yield
406 # after a cycle, always returns the new value
407 yield from read(0, 0x55AA9966)
408 yield from skip_write()
409
410 sim.add_sync_process(process)
411 debug_file = f'test_phased_dual_port_{write_phase}'
412 if transparent:
413 debug_file += '_transparent'
414 traces = ['clk', 'phase',
415 'wr_addr_i[6:0]', 'wr_we_i[3:0]', 'wr_data_i[31:0]',
416 'rd_addr_i[6:0]', 'rd_data_o[31:0]']
417 write_gtkw(debug_file + '.gtkw',
418 debug_file + '.vcd',
419 traces, module='top', zoom=-22)
420 sim_writer = sim.write_vcd(debug_file + '.vcd')
421 with sim_writer:
422 sim.run()
423
424 def test_phased_dual_port_regfile(self):
425 """test both types (odd and even write ports) of phased write memory"""
426 with self.subTest("writes happen on phase 0"):
427 self.do_test_phased_dual_port_regfile(0, False)
428 with self.subTest("writes happen on phase 1"):
429 self.do_test_phased_dual_port_regfile(1, False)
430 """test again, with a transparent read port"""
431 with self.subTest("writes happen on phase 0 (transparent reads)"):
432 self.do_test_phased_dual_port_regfile(0, True)
433 with self.subTest("writes happen on phase 1 (transparent reads)"):
434 self.do_test_phased_dual_port_regfile(1, True)
435
436 def do_test_phased_dual_port_regfile_proof(self, write_phase, transparent):
437 """
438 Formal proof of the pseudo 1W/1R regfile
439 """
440 m = Module()
441 # 128 x 32-bit, 8-bit granularity
442 dut = PhasedDualPortRegfile(7, 32, 4, write_phase, transparent)
443 m.submodules.dut = dut
444 gran = dut.data_width // dut.we_width # granularity
445 # choose a single random memory location to test
446 a_const = AnyConst(dut.addr_width)
447 # choose a single byte lane to test (one-hot encoding)
448 we_mask = Signal(dut.we_width)
449 # ... by first creating a random bit pattern
450 we_const = AnyConst(dut.we_width)
451 # ... and zeroing all but the first non-zero bit
452 m.d.comb += we_mask.eq(we_const & (-we_const))
453 # drive alternating phases
454 m.d.comb += Assume(dut.phase != Past(dut.phase))
455 # holding data register
456 d_reg = Signal(gran)
457 # for some reason, simulated formal memory is not zeroed at reset
458 # ... so, remember whether we wrote it, at least once.
459 wrote = Signal()
460 # if our memory location and byte lane is being written,
461 # capture the data in our holding register
462 with m.If((dut.wr_addr_i == a_const)
463 & (dut.phase == dut.write_phase)):
464 for i in range(dut.we_width):
465 with m.If(we_mask[i] & dut.wr_we_i[i]):
466 m.d.sync += d_reg.eq(
467 dut.wr_data_i[i * gran:i * gran + gran])
468 m.d.sync += wrote.eq(1)
469 # if our memory location is being read,
470 # and the holding register has valid data,
471 # then its value must match the memory output, on the given lane
472 with m.If(Past(dut.rd_addr_i) == a_const):
473 if transparent:
474 with m.If(wrote):
475 for i in range(dut.we_width):
476 rd_lane = dut.rd_data_o.word_select(i, gran)
477 with m.If(we_mask[i]):
478 m.d.sync += Assert(d_reg == rd_lane)
479 else:
480 # with a non-transparent read port, the read value depends
481 # on whether there is a simultaneous write, or not
482 with m.If((Past(dut.wr_addr_i) == a_const)
483 & Past(dut.phase) == dut.write_phase):
484 # simultaneous write -> check against last written value
485 with m.If(Past(wrote)):
486 for i in range(dut.we_width):
487 rd_lane = dut.rd_data_o.word_select(i, gran)
488 with m.If(we_mask[i]):
489 m.d.sync += Assert(Past(d_reg) == rd_lane)
490 with m.Else():
491 # otherwise, check against current written value
492 with m.If(wrote):
493 for i in range(dut.we_width):
494 rd_lane = dut.rd_data_o.word_select(i, gran)
495 with m.If(we_mask[i]):
496 m.d.sync += Assert(d_reg == rd_lane)
497
498 # the following is needed for induction, where an unreachable state
499 # (memory and holding register differ) is turned into an illegal one
500 # first, get the values stored in our memory location, using its
501 # debug port
502 stored1 = Signal(dut.data_width)
503 stored2 = Signal(dut.data_width)
504 m.d.comb += dut.dbg_a.eq(a_const)
505 m.d.comb += stored1.eq(dut.dbg_q1)
506 m.d.comb += stored2.eq(dut.dbg_q2)
507 # now, ensure that the value stored in the first memory is always
508 # in sync with the holding register
509 with m.If(wrote):
510 for i in range(dut.we_width):
511 with m.If(we_mask[i]):
512 m.d.comb += Assert(
513 d_reg == stored1[i * gran:i * gran + gran])
514 # same for the second memory, but one cycle later
515 with m.If(Past(wrote)):
516 for i in range(dut.we_width):
517 with m.If(we_mask[i]):
518 m.d.comb += Assert(
519 Past(d_reg) == stored2[i * gran:i * gran + gran])
520
521 self.assertFormal(m, mode="prove", depth=2)
522
523 def test_phased_dual_port_regfile_proof(self):
524 """test both types (odd and even write ports) of phased write memory"""
525 with self.subTest("writes happen on phase 0"):
526 self.do_test_phased_dual_port_regfile_proof(0, False)
527 with self.subTest("writes happen on phase 1"):
528 self.do_test_phased_dual_port_regfile_proof(1, False)
529 # test again, with transparent read ports
530 with self.subTest("writes happen on phase 0 (transparent reads)"):
531 self.do_test_phased_dual_port_regfile_proof(0, True)
532 with self.subTest("writes happen on phase 1 (transparent reads)"):
533 self.do_test_phased_dual_port_regfile_proof(1, True)
534
535
536 class DualPortRegfile(Elaboratable):
537 """
538 Builds, from a pair of phased 1W/1R blocks, a true 1W/1R RAM, where both
539 read and write ports work every cycle.
540 It employs a Last Value Table, that tracks to which memory each address was
541 last written.
542
543 :param addr_width: width of the address bus
544 :param data_width: width of the data bus
545 :param we_width: number of write enable lines
546 :param transparent: whether a simultaneous read and write returns the
547 new value (True) or the old value (False)
548 """
549
550 def __init__(self, addr_width, data_width, we_width, transparent=True):
551 self.addr_width = addr_width
552 self.data_width = data_width
553 self.we_width = we_width
554 self.transparent = transparent
555 self.wr_addr_i = Signal(addr_width); """write port address"""
556 self.wr_data_i = Signal(data_width); """write port data"""
557 self.wr_we_i = Signal(we_width); """write port enable"""
558 self.rd_addr_i = Signal(addr_width); """read port address"""
559 self.rd_data_o = Signal(data_width); """read port data"""
560
561 def elaborate(self, _):
562 m = Module()
563 # depth and granularity
564 depth = 1 << self.addr_width
565 gran = self.data_width // self.we_width
566 # instantiate the two phased 1R/1W memory blocks
567 mem0 = PhasedDualPortRegfile(
568 self.addr_width, self.data_width, self.we_width, 0,
569 self.transparent)
570 mem1 = PhasedDualPortRegfile(
571 self.addr_width, self.data_width, self.we_width, 1,
572 self.transparent)
573 m.submodules.mem0 = mem0
574 m.submodules.mem1 = mem1
575 # instantiate the backing memory (FFRAM or LUTRAM)
576 # for the Last Value Table
577 # it should have the same number and port types of the desired
578 # memory, but just one bit per write lane
579 lvt_mem = Memory(width=self.we_width, depth=depth)
580 lvt_wr = lvt_mem.write_port(granularity=1)
581 lvt_rd = lvt_mem.read_port(transparent=self.transparent)
582 m.submodules.lvt_wr = lvt_wr
583 m.submodules.lvt_rd = lvt_rd
584 # generate and wire the phases for the phased memories
585 phase = Signal()
586 m.d.sync += phase.eq(~phase)
587 m.d.comb += [
588 mem0.phase.eq(phase),
589 mem1.phase.eq(phase),
590 ]
591 m.d.comb += [
592 # wire the write ports, directly
593 mem0.wr_addr_i.eq(self.wr_addr_i),
594 mem1.wr_addr_i.eq(self.wr_addr_i),
595 mem0.wr_we_i.eq(self.wr_we_i),
596 mem1.wr_we_i.eq(self.wr_we_i),
597 mem0.wr_data_i.eq(self.wr_data_i),
598 mem1.wr_data_i.eq(self.wr_data_i),
599 # also wire the read addresses
600 mem0.rd_addr_i.eq(self.rd_addr_i),
601 mem1.rd_addr_i.eq(self.rd_addr_i),
602 # wire read and write ports to the LVT
603 lvt_wr.addr.eq(self.wr_addr_i),
604 lvt_wr.en.eq(self.wr_we_i),
605 lvt_rd.addr.eq(self.rd_addr_i),
606 # the data for the LVT is the phase on which the value was
607 # written
608 lvt_wr.data.eq(Repl(phase, self.we_width)),
609 ]
610 for i in range(self.we_width):
611 # select the right memory to assign to the output read port,
612 # in this byte lane, according to the LVT contents
613 m.d.comb += self.rd_data_o.word_select(i, gran).eq(
614 Mux(
615 lvt_rd.data[i],
616 mem1.rd_data_o.word_select(i, gran),
617 mem0.rd_data_o.word_select(i, gran)))
618 return m
619
620
621 class DualPortRegfileTestCase(FHDLTestCase):
622
623 def do_test_dual_port_regfile(self, transparent):
624 """
625 Simulate some read/write/modify operations on the dual port register
626 file
627 """
628 dut = DualPortRegfile(7, 32, 4, transparent)
629 sim = Simulator(dut)
630 sim.add_clock(1e-6)
631
632 expected = None
633 last_expected = None
634
635 # compare read data with previously written data
636 # and start a new read
637 def read(rd_addr_i, next_expected=None):
638 nonlocal expected, last_expected
639 if expected is not None:
640 self.assertEqual((yield dut.rd_data_o), expected)
641 yield dut.rd_addr_i.eq(rd_addr_i)
642 # account for the read latency
643 expected = last_expected
644 last_expected = next_expected
645
646 # start a write
647 def write(wr_addr_i, wr_we_i, wr_data_i):
648 yield dut.wr_addr_i.eq(wr_addr_i)
649 yield dut.wr_we_i.eq(wr_we_i)
650 yield dut.wr_data_i.eq(wr_data_i)
651
652 def process():
653 # write a pair of values, one for each memory
654 yield from read(0)
655 yield from write(0x42, 0b1111, 0x87654321)
656 yield
657 yield from read(0x42, 0x87654321)
658 yield from write(0x43, 0b1111, 0x0FEDCBA9)
659 yield
660 # skip a beat
661 yield from read(0x43, 0x0FEDCBA9)
662 yield from write(0, 0, 0)
663 yield
664 # write again, but now they switch memories
665 yield from read(0)
666 yield from write(0x42, 0b1111, 0x12345678)
667 yield
668 yield from read(0x42, 0x12345678)
669 yield from write(0x43, 0b1111, 0x9ABCDEF0)
670 yield
671 yield from read(0x43, 0x9ABCDEF0)
672 yield from write(0, 0, 0)
673 yield
674 # test partial writes
675 yield from read(0)
676 yield from write(0x42, 0b1001, 0x78FFFF12)
677 yield
678 yield from read(0)
679 yield from write(0x43, 0b0110, 0xFFDEABFF)
680 yield
681 yield from read(0x42, 0x78345612)
682 yield from write(0, 0, 0)
683 yield
684 yield from read(0x43, 0x9ADEABF0)
685 yield from write(0, 0, 0)
686 yield
687 yield from read(0)
688 yield from write(0, 0, 0)
689 yield
690 if transparent:
691 # returns the value just written
692 yield from read(0x42, 0x55AA9966)
693 else:
694 # returns the old value
695 yield from read(0x42, 0x78345612)
696 yield from write(0x42, 0b1111, 0x55AA9966)
697 yield
698 # after a cycle, always returns the new value
699 yield from read(0x42, 0x55AA9966)
700 yield from write(0, 0, 0)
701 yield
702 yield from read(0)
703 yield from write(0, 0, 0)
704 yield
705 yield from read(0)
706 yield from write(0, 0, 0)
707
708 sim.add_sync_process(process)
709 debug_file = 'test_dual_port_regfile'
710 if transparent:
711 debug_file += '_transparent'
712 traces = ['clk', 'phase',
713 {'comment': 'write port'},
714 'wr_addr_i[6:0]', 'wr_we_i[3:0]', 'wr_data_i[31:0]',
715 {'comment': 'read port'},
716 'rd_addr_i[6:0]', 'rd_data_o[31:0]',
717 {'comment': 'LVT write port'},
718 'phase', 'lvt_mem_w_addr[6:0]', 'lvt_mem_w_en[3:0]',
719 'lvt_mem_w_data[3:0]',
720 {'comment': 'LVT read port'},
721 'lvt_mem_r_addr[6:0]', 'lvt_mem_r_data[3:0]',
722 {'comment': 'backing memory'},
723 'mem0.rd_data_o[31:0]',
724 'mem1.rd_data_o[31:0]',
725 ]
726 write_gtkw(debug_file + '.gtkw',
727 debug_file + '.vcd',
728 traces, module='top', zoom=-22)
729 sim_writer = sim.write_vcd(debug_file + '.vcd')
730 with sim_writer:
731 sim.run()
732
733 def test_dual_port_regfile(self):
734 with self.subTest("non-transparent reads"):
735 self.do_test_dual_port_regfile(False)
736 with self.subTest("transparent reads"):
737 self.do_test_dual_port_regfile(True)
738
739
740 if __name__ == "__main__":
741 unittest.main()