Simulate some read/write/modify operations on the SRAM model
[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
23 from nmutil.formaltest import FHDLTestCase
24 from nmutil.gtkw import write_gtkw
25
26
27 class SinglePortSRAM(Elaboratable):
28 """
29 Model of a single port SRAM, which can be simulated, verified and/or
30 synthesized to an FPGA.
31
32 :param addr_width: width of the address bus
33 :param data_width: width of the data bus
34 :param we_width: number of write enable lines
35 """
36 def __init__(self, addr_width, data_width, we_width):
37 self.addr_width = addr_width
38 self.data_width = data_width
39 self.we_width = we_width
40 self.d = Signal(data_width)
41 """ write data"""
42 self.q = Signal(data_width)
43 """read data"""
44 self.a = Signal(addr_width)
45 """ read/write address"""
46 self.we = Signal(we_width)
47 """write enable"""
48
49 def elaborate(self, _):
50 m = Module()
51 # backing memory
52 depth = 1 << self.addr_width
53 granularity = self.data_width // self.we_width
54 mem = Memory(width=self.data_width, depth=depth)
55 # create read and write ports
56 # By connecting the same address to both ports, they behave, in fact,
57 # as a single, "half-duplex" port.
58 # The transparent attribute means that, on a write, we read the new
59 # value, on the next cycle
60 # Note that nmigen memories have a one cycle delay, for reads,
61 # by default
62 m.submodules.rdport = rdport = mem.read_port(transparent=True)
63 m.submodules.wrport = wrport = mem.write_port(granularity=granularity)
64 # duplicate the address to both ports
65 m.d.comb += wrport.addr.eq(self.a)
66 m.d.comb += rdport.addr.eq(self.a)
67 # write enable
68 m.d.comb += wrport.en.eq(self.we)
69 # read and write data
70 m.d.comb += wrport.data.eq(self.d)
71 m.d.comb += self.q.eq(rdport.data)
72 return m
73
74 def ports(self):
75 return [
76 self.d,
77 self.a,
78 self.we,
79 self.q
80 ]
81
82
83 def create_ilang(dut, ports, test_name):
84 vl = rtlil.convert(dut, name=test_name, ports=ports)
85 with open("%s.il" % test_name, "w") as f:
86 f.write(vl)
87
88
89 class SinglePortSRAMTestCase(FHDLTestCase):
90 @staticmethod
91 def test_simple_rtlil():
92 """
93 Generate a simple SRAM. Try ``read_rtlil mem_simple.il; proc; show``
94 from a yosys prompt, to see the memory primitives, and
95 ``read_rtlil mem_simple.il; synth; show`` to see it implemented as
96 flip-flop RAM
97 """
98 dut = SinglePortSRAM(2, 4, 2)
99 create_ilang(dut, dut.ports(), "mem_simple")
100
101 @staticmethod
102 def test_blkram_rtlil():
103 """
104 Generates a bigger SRAM.
105 Try ``read_rtlil mem_blkram.il; synth_ecp5; show`` from a yosys
106 prompt, to see it implemented as block RAM
107 """
108 dut = SinglePortSRAM(10, 16, 2)
109 create_ilang(dut, dut.ports(), "mem_blkram")
110
111 def test_sram_model(self):
112 """
113 Simulate some read/write/modify operations on the SRAM model
114 """
115 dut = SinglePortSRAM(7, 32, 4)
116 sim = Simulator(dut)
117 sim.add_clock(1e-6)
118
119 def process():
120 # 1) write 0x12_34_56_78 to address 0
121 yield dut.a.eq(0)
122 yield dut.d.eq(0x12_34_56_78)
123 yield dut.we.eq(0b1111)
124 yield
125 # 2) write 0x9A_BC_DE_F0 to address 1
126 yield dut.a.eq(1)
127 yield dut.d.eq(0x9A_BC_DE_F0)
128 yield dut.we.eq(0b1111)
129 yield
130 # ... and read value just written to address 0
131 self.assertEqual((yield dut.q), 0x12_34_56_78)
132 # 3) prepare to read from address 0
133 yield dut.d.eq(0)
134 yield dut.we.eq(0b0000)
135 yield dut.a.eq(0)
136 yield
137 # ... and read value just written to address 1
138 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
139 # 4) prepare to read from address 1
140 yield dut.a.eq(1)
141 yield
142 # ... and read value from address 0
143 self.assertEqual((yield dut.q), 0x12_34_56_78)
144 # 5) write 0x9A and 0xDE to bytes 1 and 3, leaving
145 # bytes 0 and 2 unchanged
146 yield dut.a.eq(0)
147 yield dut.d.eq(0x9A_FF_DE_FF)
148 yield dut.we.eq(0b1010)
149 yield
150 # ... and read value from address 1
151 self.assertEqual((yield dut.q), 0x9A_BC_DE_F0)
152 # 6) nothing more to do
153 yield dut.d.eq(0)
154 yield dut.we.eq(0)
155 yield
156 # ... other than confirm that bytes 1 and 3 were modified
157 # correctly
158 self.assertEqual((yield dut.q), 0x9A_34_DE_78)
159
160 sim.add_sync_process(process)
161 traces = ['rdport.clk', 'a[6:0]', 'we[3:0]', 'd[31:0]', 'q[31:0]']
162 write_gtkw('test_sram_model.gtkw', 'test_sram_model.vcd',
163 traces, module='top')
164 sim_writer = sim.write_vcd('test_sram_model.vcd')
165 with sim_writer:
166 sim.run()
167
168
169 if __name__ == "__main__":
170 unittest.main()