update HyperRAM module and add unit test
authorLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Wed, 16 Mar 2022 20:20:50 +0000 (20:20 +0000)
committerLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Wed, 16 Mar 2022 20:20:50 +0000 (20:20 +0000)
lambdasoc/periph/hyperram.py
lambdasoc/test/test_hyperbus.py [new file with mode: 0644]

index 4e056d5dc7ffdf838b84b676306d3737c010037a..8220d7b9ebbbec24ad82d59915921872efca0c80 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright (c) 2019 Antti Lukats <antti.lukats@gmail.com>
 # Copyright (c) 2019 Florent Kermarrec <florent@enjoy-digital.fr>
 # Copyright (c) 2021 gatecat <gatecat@ds0.me> [nmigen-soc port]
-# Copyright (C) 2021 Luke Kenneth Casson Leighton <lkcl@lkcl.net>
+# Copyright (C) 2022 Luke Kenneth Casson Leighton <lkcl@lkcl.net>
 #
 # Code from Lukats, Kermarrec and gatecat is Licensed BSD-2-Clause
 #
@@ -11,7 +11,7 @@
 # under EU Grants 871528 and 957073, and Licensed under the LGPLv3+ License
 
 
-from nmigen import (Elaboratable, Module, Signal, Record, Cat)
+from nmigen import (Elaboratable, Module, Signal, Record, Cat, Const)
 from nmigen.cli import rtlil
 
 from nmigen_soc import wishbone
@@ -98,10 +98,11 @@ class HyperRAM(Peripheral, Elaboratable):
 
     This core favors portability and ease of use over performance.
     """
-    def __init__(self, *, io, phy_kls):
+    def __init__(self, *, io, phy_kls, latency=6):
         super().__init__()
         self.io = io
         self.phy = phy_kls(io)
+        self.latency = latency
         self.bus = wishbone.Interface(addr_width=21,
                                       data_width=32, granularity=8)
         mmap = MemoryMap(addr_width=23, data_width=8)
@@ -113,76 +114,159 @@ class HyperRAM(Peripheral, Elaboratable):
     def elaborate(self, platform):
         m = Module()
         m.submodules.phy = self.phy
+        bus = self.bus
+        comb, sync = m.d.comb, m.d.sync
 
         clk       = self.phy.clk
         clk_phase = Signal(2)
         cs        = self.phy.cs
         ca        = Signal(48)
+        ca_active = Signal()
         sr        = Signal(48)
+        sr_new    = Signal(48)
 
         dq_o = self.phy.dq_o
         dq_i = self.phy.dq_i
         dq_oe = self.phy.dq_oe
+        dw = len(dq_o) # data width
 
         rwds_o = self.phy.rwds_o
         rwds_oe = self.phy.rwds_oe
 
         # Clock Generation (sys_clk/4) -----------------------------------
-        m.d.sync += clk_phase.eq(clk_phase + 1)
+        sync += clk_phase.eq(clk_phase + 1)
         with m.Switch(clk_phase):
             with m.Case(1):
-                m.d.sync += clk.eq(cs)
+                sync += clk.eq(cs)
             with m.Case(3):
-                m.d.sync += clk.eq(0)
+                sync += clk.eq(0)
 
         # Data Shift Register (for write and read) ------------------------
-        dqi = Signal(8)
-        m.d.sync += dqi.eq(dq_i) # Sample on 90° and 270°
-        with m.Switch(clk_phase):
-            with m.Case(0, 2):
-                m.d.sync += sr.eq(Cat(dqi, sr[:-8]))
-
-        m.d.comb += [
-            self.bus.dat_r.eq(sr), # To Wisbone
-            dq_o.eq(sr[-8:]), # To HyperRAM
-        ]
+        dqi = Signal(dw)
+        sync += dqi.eq(dq_i) # Sample on 90° and 270°
+        with m.If(ca_active):
+            comb += sr_new.eq(Cat(dqi[:8], sr[:-dw]))
+        with m.Else():
+            comb += sr_new.eq(Cat(dqi, sr[:-8]))
+        with m.If(~clk_phase[0]):
+            sync += sr.eq(sr_new) # Shift on 0° and 180°
+
+        # Data shift-out register ----------------------------------------
+        comb += self.bus.dat_r.eq(sr_new), # To Wisbone
+        with m.If(dq_oe):
+            comb += dq_o.eq(sr[-dw:]), # To HyperRAM
+        with m.If(dq_oe & ca_active):
+            comb += dq_o.eq(sr[-8:]), # To HyperRAM, Only 8-bit during CMD/Addr.
 
         # Command generation ----------------------------------------------
-        m.d.comb += [
+        ashift = {8:1, 16:0}[dw]
+        la = 3-ashift
+        comb += [
             ca[47].eq(~self.bus.we),          # R/W#
             ca[45].eq(1),                     # Burst Type (Linear)
-            ca[16:35].eq(self.bus.adr[2:21]), # Row & Upper Column Address
+            ca[16:45].eq(self.bus.adr[la:]),  # Row & Upper Column Address
             ca[1:3].eq(self.bus.adr[0:2]),    # Lower Column Address
-            ca[0].eq(0),                      # Lower Column Address
+            ca[ashift:3].eq(bus.adr),         # Lower Column Address
         ]
 
+        # Latency count starts from the middle of the command (thus the -4).
+        # In fixed latency mode (default), latency is 2 x Latency count.
+        # We have 4 x sys_clk per RAM clock:
+        latency_cycles = (self.latency * 2 * 4) - 4
+
+        # Bus Latch ----------------------------------------------------
+        bus_adr   = Signal(32)
+        bus_we    = Signal()
+        bus_sel   = Signal(4)
+        bus_latch = Signal()
+        with m.If(bus_latch):
+            with m.If(bus.we):
+                sync += sr.eq(Cat(Const(0, 16), bus.dat_w))
+            sync += [ bus_we.eq(bus.we),
+                      bus_sel.eq(bus.sel),
+                      bus_adr.eq(bus.adr)
+                    ]
+
+
+
         # Sequencer -------------------------------------------------------
-        dt_seq = [
-         # DT,  Action
-         (3,    []),
-         (12,   [cs.eq(1), dq_oe.eq(1), sr.eq(ca)]), # Command: 6 clk
-         (44,   [dq_oe.eq(0)]),                      # Latency(dflt): 2*6 clk
-         (2,    [dq_oe.eq(self.bus.we),              # Wr/Rd data byte: 2 clk
-                 sr[:16].eq(0),
-                 sr[16:].eq(self.bus.dat_w),
-                 rwds_oe.eq(self.bus.we),
-                 rwds_o.eq(~self.bus.sel[3])]),
-         (2,    [rwds_o.eq(~self.bus.sel[2])]),      # Wr/Rd data byte: 2 clk
-         (2,    [rwds_o.eq(~self.bus.sel[1])]),      # Wr/Rd data byte: 2 clk
-         (2,    [rwds_o.eq(~self.bus.sel[0])]),      # Wr/Rd data byte: 2 clk
-         (2,    [cs.eq(0), rwds_oe.eq(0), dq_oe.eq(0)]),
-         (1,    [self.bus.ack.eq(1)]),
-         (1,    [self.bus.ack.eq(0)]),
-         (0,    []),
-        ]
-        # Convert delta-time sequencer to time sequencer
-        t_seq = []
-        t_seq_start = (clk_phase == 1)
-        t = 0
-        for dt, a in dt_seq:
-            t_seq.append((t, a))
-            t += dt
-        timeline(m, self.bus.cyc & self.bus.stb & t_seq_start, t_seq)
+        cycles = Signal(8)
+        first  = Signal()
+        count_inc = Signal()
+        dbg_cyc = Signal(8)
+
+        # when not idle run a cycles counter
+        with m.If(count_inc):
+            sync += dbg_cyc.eq(dbg_cyc+1)
+        with m.Else():
+            sync += dbg_cyc.eq(0)
+
+        # Main FSM
+        with m.FSM() as fsm:
+            comb += count_inc.eq(~fsm.ongoing("IDLE"))
+            with m.State("IDLE"):
+                sync += first.eq(1)
+                with m.If(bus.cyc & bus.stb & (clk_phase == 0)):
+                    sync += sr.eq(ca)
+                    m.next = "SEND-COMMAND-ADDRESS"
+                    sync += cycles.eq(0)
+
+            with m.State("SEND-COMMAND-ADDRESS"):
+                sync += cycles.eq(cycles+1)
+                comb += cs.eq(1)        # Set CSn.
+                comb += ca_active.eq(1) # Send Command on DQ.
+                comb += dq_oe.eq(1),    # Wait for 6*2 cycles...
+                with m.If(cycles == (6*2 - 1)):
+                    m.next = "WAIT-LATENCY"
+                    sync += cycles.eq(0)
+
+            with m.State("WAIT-LATENCY"):
+                sync += cycles.eq(cycles+1)
+                comb += cs.eq(1) # Set CSn.
+                # Wait for Latency cycles...
+                with m.If(cycles == (latency_cycles - 1)):
+                    comb += bus_latch.eq(1) # Latch Bus.
+                    # Early Write Ack (to allow bursting).
+                    comb += bus.ack.eq(bus.we)
+                    m.next = "READ-WRITE-DATA0"
+                    sync += cycles.eq(0)
+
+            states = {8:4, 16:2}[dw]
+            for n in range(states):
+                with m.State("READ-WRITE-DATA%d" % n):
+                    sync += cycles.eq(cycles+1)
+                    comb += cs.eq(1), # Set CSn.
+                    # Send Data on DQ/RWDS (for write).
+                    with m.If(bus_we):
+                        comb += dq_oe.eq(1)
+                        comb += rwds_oe.eq(1)
+                        for i in range(dw//8):
+                            seli = ~bus_sel[4-1-n*dw//8-i]
+                            comb += rwds_o[dw//8-1-i].eq(seli)
+                    # Wait for 2 cycles (since HyperRAM's Clk = sys_clk/4).
+                    with m.If(cycles == (2 - 1)):
+                        # Set next default state (with rollover for bursts).
+                        m.next = "READ-WRITE-DATA%d" % ((n + 1) % states)
+                        sync += cycles.eq(0)
+                        # On last state, see if we can continue the burst 
+                        # or if we should end it.
+                        with m.If(n == (states - 1)):
+                            sync += first.eq(0)
+                            # Continue burst when consecutive access ready.
+                            with m.If(bus.stb & bus.cyc & 
+                                      (bus.we == bus_we) & 
+                                      (bus.adr == (bus_adr + 1))):
+                                comb += bus_latch.eq(1), # Latch Bus.
+                                # Early Write Ack (to allow bursting).
+                                comb += bus.ack.eq(bus.we)
+                            # Else end the burst.
+                            with m.Elif(bus_we | ~first):
+                                m.next = "IDLE"
+                                sync += cycles.eq(0)
+                        # Read Ack (when dat_r ready).
+                        with m.If((n == 0) & ~first):
+                            comb += bus.ack.eq(~bus_we)
+
         return m
 
     def ports(self):
diff --git a/lambdasoc/test/test_hyperbus.py b/lambdasoc/test/test_hyperbus.py
new file mode 100644 (file)
index 0000000..2191e63
--- /dev/null
@@ -0,0 +1,220 @@
+# Test of HyperRAM class
+#
+# Copyright (c) 2019 Florent Kermarrec <florent@enjoy-digital.fr>
+# Copyright (c) 2021 Luke Kenneth Casson Leighton <lkcl@lkcl.net>
+# Based on code from Kermarrec, Licensed BSD-2-Clause
+#
+# Modifications for the Libre-SOC Project funded by NLnet and NGI POINTER
+# under EU Grants 871528 and 957073, under the LGPLv3+ License
+
+import unittest
+
+from nmigen import (Record, Module, Signal, Elaboratable)
+from nmigen.compat.sim import run_simulation
+
+from lambdasoc.periph.hyperram import HyperRAM
+
+def c2bool(c):
+    return {"-": 1, "_": 0}[c]
+
+
+def wb_write(bus, addr, data, sel):
+    yield bus.adr.eq(addr)
+    yield bus.dat_w.eq(data)
+    yield bus.sel.eq(sel)
+    yield bus.we.eq(1)
+    yield bus.cyc.eq(1)
+    yield bus.stb.eq(1)
+    yield
+    while not (yield bus.ack):
+        yield
+    yield bus.cyc.eq(0)
+    yield bus.stb.eq(0)
+
+    
+def wb_read(bus, addr, sel):
+    yield bus.adr.eq(addr)
+    yield bus.sel.eq(sel)
+    yield bus.cyc.eq(1)
+    yield bus.stb.eq(1)
+    yield
+    while not (yield bus.ack):
+        yield
+    yield bus.cyc.eq(0) 
+    yield bus.stb.eq(0)
+
+    return (yield bus.dat_r)
+
+    
+class Pads: pass
+
+
+class HyperRamPads:
+    def __init__(self, dw=8):
+        self.clk  = Signal()
+        self.cs_n = Signal()
+        self.dq   = Record([("oe", 1), ("o", dw),     ("i", dw)])
+        self.rwds = Record([("oe", 1), ("o", dw//8),  ("i", dw//8)])
+
+
+class TestHyperRAMPHY(Elaboratable):
+    def __init__(self, pads):
+        self.pads = pads
+        self.clk = pads.clk
+        self.cs = Signal()
+        self.dq_o = pads.dq.o
+        self.dq_i = pads.dq.i
+        self.dq_oe = pads.dq.oe
+        self.rwds_o = pads.rwds.o
+        self.rwds_oe = Signal()
+
+    def elaborate(self, platform):
+        m = Module()
+        m.d.comb += self.pads.cs_n.eq(~self.cs)
+        m.d.comb += self.pads.rwds.oe.eq(self.rwds_oe)
+        return m
+
+
+class TestHyperBusWrite(unittest.TestCase):
+
+    def test_hyperram_write(self):
+        def fpga_gen(dut):
+            yield from wb_write(dut.bus, 0x1234, 0xdeadbeef, sel=0b1001)
+            yield
+
+        def hyperram_gen(dut):
+            clk     = "___--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--_______"
+            cs_n    = "--________________________________________________________________------"
+            dq_oe   = "__------------____________________________________________--------______"
+            dq_o    = "002000048d000000000000000000000000000000000000000000000000deadbeef000000"
+            rwds_oe = "__________________________________________________________--------______"
+            rwds_o  = "____________________________________________________________----________"
+
+            #clk     = "___--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--_______"
+            #cs_n    = "--________________________________________________________________------"
+            #dq_oe   = "__------------____________________________________________--------______"
+            #dq_o    = "002000048d000000000000000000000000000000000000000000000000deadbeef000000"
+            #rwds_oe = "__________________________________________________________--------______"
+            #rwds_o  = "________________________________________________________________________"
+            for i in range(3):
+                yield
+            if False: # useful for printing out expected vs results
+              for i in range(len(clk)):
+                print ("i", i)
+                print ("clk", c2bool(clk[i]), (yield dut.phy.pads.clk))
+                print ("cs_n", c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                print ("dq_oe", c2bool(dq_oe[i]), (yield dut.phy.pads.dq.oe))
+                print ("dq_o", hex(int(dq_o[2*(i//2):2*(i//2)+2], 16)),
+                                    hex((yield dut.phy.pads.dq.o)))
+                print ("rwds_oe", c2bool(rwds_oe[i]),
+                                    (yield dut.phy.pads.rwds.oe))
+                print ("rwds_o", c2bool(rwds_o[i]),
+                                    (yield dut.phy.pads.rwds.o))
+                yield
+            for i in range(len(clk)):
+                self.assertEqual(c2bool(clk[i]), (yield dut.phy.pads.clk))
+                self.assertEqual(c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                self.assertEqual(c2bool(dq_oe[i]), (yield dut.phy.pads.dq.oe))
+                self.assertEqual(int(dq_o[2*(i//2):2*(i//2)+2], 16),
+                                    (yield dut.phy.pads.dq.o))
+                self.assertEqual(c2bool(rwds_oe[i]),
+                                    (yield dut.phy.pads.rwds.oe))
+                self.assertEqual(c2bool(rwds_o[i]),
+                                    (yield dut.phy.pads.rwds.o))
+                yield
+
+        dut = HyperRAM(io=HyperRamPads(), phy_kls=TestHyperRAMPHY)
+        run_simulation(dut, [fpga_gen(dut), hyperram_gen(dut)],
+                            vcd_name="sim.vcd")
+
+    def test_hyperram_read(self):
+        def fpga_gen(dut):
+            dat = yield from wb_read(dut.bus, 0x1234, 0b1111)
+            self.assertEqual(dat, 0xdeadbeef)
+            dat = yield from wb_read(dut.bus, 0x1235, 0b1111)
+            self.assertEqual(dat, 0xcafefade)
+
+        def hyperram_gen(dut):
+            clk     = "___--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--_"
+            cs_n    = "--________________________________________________________________________________"
+            dq_oe   = "__------------____________________________________________________________________"
+            dq_o    = "00a000048d000000000000000000000000000000000000000000000000000000000000000000000000"
+            dq_i    = "0000000000000000000000000000000000000000000000000000000000deadbeefcafefade00000000"
+            rwds_oe = "__________________________________________________________________________________"
+
+            for i in range(3):
+                print ("prep", i)
+                yield
+            for i in range(len(clk)):
+                print ("i", i)
+                yield dut.phy.pads.dq.i.eq(int(dq_i[2*(i//2):2*(i//2)+2], 16))
+                print ("clk", c2bool(clk[i]), (yield dut.phy.pads.clk))
+                print ("cs_n", c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                yield
+                continue
+                self.assertEqual(c2bool(clk[i]), (yield dut.phy.pads.clk))
+                self.assertEqual(c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                self.assertEqual(c2bool(dq_oe[i]), (yield dut.phy.pads.dq.oe))
+                self.assertEqual(int(dq_o[2*(i//2):2*(i//2)+2], 16),
+                            (yield dut.phy.pads.dq.o))
+                self.assertEqual(c2bool(rwds_oe[i]),
+                            (yield dut.phy.pads.rwds.oe))
+                yield
+
+        dut = HyperRAM(io=HyperRamPads(), phy_kls=TestHyperRAMPHY)
+        run_simulation(dut, [fpga_gen(dut), hyperram_gen(dut)],
+                            vcd_name="rd_sim.vcd")
+
+class TestHyperBusRead(unittest.TestCase):
+    def test_hyperram_read(self):
+        def fpga_gen(dut):
+            dat = yield from wb_read(dut.bus, 0x1234, 0b1111)
+            self.assertEqual(dat, 0xdeadbeef)
+            dat = yield from wb_read(dut.bus, 0x1235, 0b1111)
+            self.assertEqual(dat, 0xcafefade)
+            yield
+            yield
+            yield
+            yield
+            yield
+
+        def hyperram_gen(dut):
+            clk     = "___--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--__--_"
+            cs_n    = "--________________________________________________________________________________"
+            dq_oe   = "__------------____________________________________________________________________"
+            dq_o    = "00a000048d000000000000000000000000000000000000000000000000000000000000000000000000"
+            dq_i    = "0000000000000000000000000000000000000000000000000000000000deadbeefcafefade00000000"
+            rwds_oe = "__________________________________________________________________________________"
+
+            for i in range(3):
+                print ("prep", i)
+                yield
+            for i in range(len(clk)):
+                print ("i", i)
+                yield dut.phy.pads.dq.i.eq(int(dq_i[2*(i//2):2*(i//2)+2], 16))
+                print ("clk", c2bool(clk[i]), (yield dut.phy.pads.clk))
+                print ("cs_n", c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                print ("dq_oe", c2bool(dq_oe[i]), (yield dut.phy.pads.dq.oe))
+                print ("dq_o", hex(int(dq_o[2*(i//2):2*(i//2)+2], 16)),
+                                    hex((yield dut.phy.pads.dq.o)))
+                print ("rwds_oe", c2bool(rwds_oe[i]),
+                                    (yield dut.phy.pads.rwds.oe))
+                self.assertEqual(c2bool(clk[i]), (yield dut.phy.pads.clk))
+                self.assertEqual(c2bool(cs_n[i]), (yield dut.phy.pads.cs_n))
+                self.assertEqual(c2bool(dq_oe[i]), (yield dut.phy.pads.dq.oe))
+                self.assertEqual(int(dq_o[2*(i//2):2*(i//2)+2], 16),
+                            (yield dut.phy.pads.dq.o))
+                self.assertEqual(c2bool(rwds_oe[i]),
+                            (yield dut.phy.pads.rwds.oe))
+                yield
+
+        dut = HyperRAM(io=HyperRamPads(), phy_kls=TestHyperRAMPHY)
+        run_simulation(dut, [fpga_gen(dut), hyperram_gen(dut)],
+                            vcd_name="rd_sim.vcd")
+
+if __name__ == '__main__':
+    unittest.main()
+    #t = TestHyperBusRead()
+    #t.test_hyperram_read()
+    #t = TestHyperBusWrite()
+    #t.test_hyperram_write()