Implement and test a "phased write port" memory
[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
20 from nmigen.back import rtlil
21 from nmigen.sim import Simulator
22 from nmigen.asserts import Assert, 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)
44 """ write data"""
45 self.q = Signal(data_width)
46 """read data"""
47 self.a = Signal(addr_width)
48 """ read/write address"""
49 self.we = Signal(we_width)
50 """write enable"""
51 self.dbg_a = Signal(addr_width)
52 """debug read port address"""
53 self.dbg_q = Signal(data_width)
54 """debug read port data"""
55
56 def elaborate(self, _):
57 m = Module()
58 # backing memory
59 depth = 1 << self.addr_width
60 granularity = self.data_width // self.we_width
61 mem = Memory(width=self.data_width, depth=depth)
62 # create read and write ports
63 # By connecting the same address to both ports, they behave, in fact,
64 # as a single, "half-duplex" port.
65 # The transparent attribute means that, on a write, we read the new
66 # value, on the next cycle
67 # Note that nmigen memories have a one cycle delay, for reads,
68 # by default
69 m.submodules.rdport = rdport = mem.read_port(transparent=True)
70 m.submodules.wrport = wrport = mem.write_port(granularity=granularity)
71 # duplicate the address to both ports
72 m.d.comb += wrport.addr.eq(self.a)
73 m.d.comb += rdport.addr.eq(self.a)
74 # write enable
75 m.d.comb += wrport.en.eq(self.we)
76 # read and write data
77 m.d.comb += wrport.data.eq(self.d)
78 m.d.comb += self.q.eq(rdport.data)
79 # the debug port is an asynchronous read port, allowing direct access
80 # to a given memory location by the formal engine
81 m.submodules.dbgport = dbgport = mem.read_port(domain="comb")
82 m.d.comb += dbgport.addr.eq(self.dbg_a)
83 m.d.comb += self.dbg_q.eq(dbgport.data)
84 return m
85
86 def ports(self):
87 return [
88 self.d,
89 self.a,
90 self.we,
91 self.q
92 ]
93
94
95 def create_ilang(dut, ports, test_name):
96 vl = rtlil.convert(dut, name=test_name, ports=ports)
97 with open("%s.il" % test_name, "w") as f:
98 f.write(vl)
99
100
101 class SinglePortSRAMTestCase(FHDLTestCase):
102 @staticmethod
103 def test_simple_rtlil():
104 """
105 Generate a simple SRAM. Try ``read_rtlil mem_simple.il; proc; show``
106 from a yosys prompt, to see the memory primitives, and
107 ``read_rtlil mem_simple.il; synth; show`` to see it implemented as
108 flip-flop RAM
109 """
110 dut = SinglePortSRAM(2, 4, 2)
111 create_ilang(dut, dut.ports(), "mem_simple")
112
113 @staticmethod
114 def test_blkram_rtlil():
115 """
116 Generates a bigger SRAM.
117 Try ``read_rtlil mem_blkram.il; synth_ecp5; show`` from a yosys
118 prompt, to see it implemented as block RAM
119 """
120 dut = SinglePortSRAM(10, 16, 2)
121 create_ilang(dut, dut.ports(), "mem_blkram")
122
123 def test_sram_model(self):
124 """
125 Simulate some read/write/modify operations on the SRAM model
126 """
127 dut = SinglePortSRAM(7, 32, 4)
128 sim = Simulator(dut)
129 sim.add_clock(1e-6)
130
131 def process():
132 # 1) write 0x12_34_56_78 to address 0
133 yield dut.a.eq(0)
134 yield dut.d.eq(0x12_34_56_78)
135 yield dut.we.eq(0b1111)
136 yield
137 # 2) write 0x9A_BC_DE_F0 to address 1
138 yield dut.a.eq(1)
139 yield dut.d.eq(0x9A_BC_DE_F0)
140 yield dut.we.eq(0b1111)
141 yield
142 # ... and read value just written to address 0
143 self.assertEqual((yield dut.q), 0x12_34_56_78)
144 # 3) prepare to read from address 0
145 yield dut.d.eq(0)
146 yield dut.we.eq(0b0000)
147 yield dut.a.eq(0)
148 yield
149 # ... and read value just written to address 1
150 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
151 # 4) prepare to read from address 1
152 yield dut.a.eq(1)
153 yield
154 # ... and read value from address 0
155 self.assertEqual((yield dut.q), 0x12_34_56_78)
156 # 5) write 0x9A and 0xDE to bytes 1 and 3, leaving
157 # bytes 0 and 2 unchanged
158 yield dut.a.eq(0)
159 yield dut.d.eq(0x9A_FF_DE_FF)
160 yield dut.we.eq(0b1010)
161 yield
162 # ... and read value from address 1
163 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
164 # 6) nothing more to do
165 yield dut.d.eq(0)
166 yield dut.we.eq(0)
167 yield
168 # ... other than confirm that bytes 1 and 3 were modified
169 # correctly
170 self.assertEqual((yield dut.q), 0x9A_34_DE_78)
171
172 sim.add_sync_process(process)
173 traces = ['rdport.clk', 'a[6:0]', 'we[3:0]', 'd[31:0]', 'q[31:0]']
174 write_gtkw('test_sram_model.gtkw', 'test_sram_model.vcd',
175 traces, module='top')
176 sim_writer = sim.write_vcd('test_sram_model.vcd')
177 with sim_writer:
178 sim.run()
179
180 def test_model_sram_proof(self):
181 """
182 Formal proof of the single port SRAM model
183 """
184 m = Module()
185 # 128 x 32-bit, 8-bit granularity
186 m.submodules.dut = dut = SinglePortSRAM(7, 32, 4)
187 gran = len(dut.d) // len(dut.we) # granularity
188 # choose a single random memory location to test
189 a_const = AnyConst(dut.a.shape())
190 # choose a single byte lane to test (one-hot encoding)
191 we_mask = Signal.like(dut.we)
192 # ... by first creating a random bit pattern
193 we_const = AnyConst(dut.we.shape())
194 # ... and zeroing all but the first non-zero bit
195 m.d.comb += we_mask.eq(we_const & (-we_const))
196 # holding data register
197 d_reg = Signal(gran)
198 # for some reason, simulated formal memory is not zeroed at reset
199 # ... so, remember whether we wrote it, at least once.
200 wrote = Signal()
201 # if our memory location and byte lane is being written
202 # ... capture the data in our holding register
203 with m.If(dut.a == a_const):
204 for i in range(len(dut.we)):
205 with m.If(we_mask[i] & dut.we[i]):
206 m.d.sync += d_reg.eq(dut.d[i*gran:i*gran+gran])
207 m.d.sync += wrote.eq(1)
208 # if our memory location is being read
209 # ... and the holding register has valid data
210 # ... then its value must match the memory output, on the given lane
211 with m.If((Past(dut.a) == a_const) & wrote):
212 for i in range(len(dut.we)):
213 with m.If(we_mask[i]):
214 m.d.sync += Assert(d_reg == dut.q[i*gran:i*gran+gran])
215
216 # the following is needed for induction, where an unreachable state
217 # (memory and holding register differ) is turned into an illegal one
218 # first, get the value stored in our memory location, using its debug
219 # port
220 stored = Signal.like(dut.q)
221 m.d.comb += dut.dbg_a.eq(a_const)
222 m.d.comb += stored.eq(dut.dbg_q)
223 # now, ensure that the value stored in memory is always in sync
224 # with the holding register
225 with m.If(wrote):
226 for i in range(len(dut.we)):
227 with m.If(we_mask[i]):
228 m.d.sync += Assert(d_reg == stored[i*gran:i*gran+gran])
229
230 self.assertFormal(m, mode="prove", depth=2)
231
232
233 class PhasedDualPortRegfile(Elaboratable):
234 """
235 Builds, from a pair of 1RW blocks, a pseudo 1W/1R RAM, where the
236 read port works every cycle, but the write port is only available on
237 either even (1eW/1R) or odd (1oW/1R) cycles.
238
239 :param addr_width: width of the address bus
240 :param data_width: width of the data bus
241 :param we_width: number of write enable lines
242 :param write_phase: indicates on which phase the write port will
243 accept data
244 """
245
246 def __init__(self, addr_width, data_width, we_width, write_phase):
247 self.addr_width = addr_width
248 self.data_width = data_width
249 self.we_width = we_width
250 self.write_phase = write_phase
251 self.wr_addr_i = Signal(addr_width)
252 """write port address"""
253 self.wr_data_i = Signal(data_width)
254 """write port data"""
255 self.wr_we_i = Signal(we_width)
256 """write port enable"""
257 self.rd_addr_i = Signal(addr_width)
258 """read port address"""
259 self.rd_data_o = Signal(data_width)
260 """read port data"""
261 self.phase = Signal()
262 """even/odd cycle indicator"""
263
264 def elaborate(self, _):
265 m = Module()
266 # instantiate the two 1RW memory blocks
267 mem1 = SinglePortSRAM(self.addr_width, self.data_width, self.we_width)
268 mem2 = SinglePortSRAM(self.addr_width, self.data_width, self.we_width)
269 m.submodules.mem1 = mem1
270 m.submodules.mem2 = mem2
271 # wire write port to first memory, and its output to the second
272 m.d.comb += mem1.d.eq(self.wr_data_i)
273 m.d.comb += mem2.d.eq(mem1.q)
274 # holding registers for the write port of the second memory
275 last_wr_addr = Signal(self.addr_width)
276 last_wr_we = Signal(self.we_width)
277 with m.If(self.phase == self.write_phase):
278 # write phase, start a write on the first memory
279 m.d.comb += mem1.a.eq(self.wr_addr_i)
280 m.d.comb += mem1.we.eq(self.wr_we_i)
281 # save write address and write select for repeating the write
282 # on the second memory, later
283 m.d.sync += last_wr_we.eq(self.wr_we_i)
284 m.d.sync += last_wr_addr.eq(self.wr_addr_i)
285 # start a read on the second memory
286 m.d.comb += mem2.a.eq(self.rd_addr_i)
287 # output previously read data from the first memory
288 m.d.comb += self.rd_data_o.eq(mem1.q)
289 with m.Else():
290 # read phase, write last written data on second memory
291 m.d.comb += mem2.a.eq(last_wr_addr)
292 m.d.comb += mem2.we.eq(last_wr_we)
293 # start a read on the first memory
294 m.d.comb += mem1.a.eq(self.rd_addr_i)
295 # output previously read data from the second memory
296 m.d.comb += self.rd_data_o.eq(mem2.q)
297 return m
298
299
300 class PhasedDualPortRegfileTestCase(FHDLTestCase):
301
302 def do_test_phased_dual_port_regfile(self, write_phase):
303 """
304 Simulate some read/write/modify operations on the phased write memory
305 """
306 dut = PhasedDualPortRegfile(7, 32, 4, write_phase)
307 sim = Simulator(dut)
308 sim.add_clock(1e-6)
309
310 # compare read data with previously written data
311 # and start a new read
312 def read(rd_addr_i, expected=None):
313 if expected is not None:
314 self.assertEqual((yield dut.rd_data_o), expected)
315 yield dut.rd_addr_i.eq(rd_addr_i)
316
317 # start a write, and set write phase
318 def write(wr_addr_i, wr_we_i, wr_data_i):
319 yield dut.wr_addr_i.eq(wr_addr_i)
320 yield dut.wr_we_i.eq(wr_we_i)
321 yield dut.wr_data_i.eq(wr_data_i)
322 yield dut.phase.eq(write_phase)
323
324 # disable writes, and start read phase
325 def skip_write():
326 yield dut.wr_addr_i.eq(0)
327 yield dut.wr_we_i.eq(0)
328 yield dut.wr_data_i.eq(0)
329 yield dut.phase.eq(~write_phase)
330
331 # writes a few values on the write port, and read them back
332 # ... reads can happen every cycle
333 # ... writes, only every two cycles.
334 # since reads have a one cycle delay, the expected value on
335 # each read refers to the last read performed, not the
336 # current one, which is in progress.
337 def process():
338 yield from read(0)
339 yield from write(0x42, 0b1111, 0x12345678)
340 yield
341 yield from read(0x42)
342 yield from skip_write()
343 yield
344 yield from read(0x42)
345 yield from write(0x43, 0b1111, 0x9ABCDEF0)
346 yield
347 yield from read(0x43, 0x12345678)
348 yield from skip_write()
349 yield
350 yield from read(0x42, 0x12345678)
351 yield from write(0x43, 0b1001, 0xF0FFFF9A)
352 yield
353 yield from read(0x43, 0x9ABCDEF0)
354 yield from skip_write()
355 yield
356 yield from read(0x43, 0x12345678)
357 yield from write(0x42, 0b0110, 0xFF5634FF)
358 yield
359 yield from read(0x42, 0xF0BCDE9A)
360 yield from skip_write()
361 yield
362 yield from read(0, 0xF0BCDE9A)
363 yield from write(0, 0, 0)
364 yield
365 yield from read(0, 0x12563478)
366 yield from skip_write()
367
368 sim.add_sync_process(process)
369 traces = ['clk', 'phase',
370 'wr_addr_i[6:0]', 'wr_we_i[3:0]', 'wr_data_i[31:0]',
371 'rd_addr_i[6:0]', 'rd_data_o[31:0]']
372 write_gtkw(f'test_phased_dual_port_{write_phase}.gtkw',
373 f'test_phased_dual_port_{write_phase}.vcd',
374 traces, module='top', zoom=-22)
375 sim_writer = sim.write_vcd(f'test_phased_dual_port_{write_phase}.vcd')
376 with sim_writer:
377 sim.run()
378
379 def test_phased_dual_port_regfile(self):
380 """test both types (odd and even write ports) of phased write memory"""
381 with self.subTest("writes happen on phase 0"):
382 self.do_test_phased_dual_port_regfile(0)
383 with self.subTest("writes happen on phase 1"):
384 self.do_test_phased_dual_port_regfile(1)
385
386
387 if __name__ == "__main__":
388 unittest.main()