Finish the SRAM formal proof by implementing induction
[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 if __name__ == "__main__":
234 unittest.main()