Skip to content

Inferring True Dual-Port BRAMs with the new IR on Series 7 and ECP5 #1011

Closed
@fischermoseley

Description

@fischermoseley

I'm trying to infer a true dual port BRAM on a Xilinx Series 7 chip, but I'm not having much luck. I've written the following to try to map the ReadPort/WritePort construct to the addr/din/dout/en/wea that's native to the RAM36 primitives:

from amaranth import *
from amaranth_boards.nexys4ddr import Nexys4DDRPlatform
from amaranth.sim import Simulator
from random import randint

class TrueDualPort(Elaboratable):
    def __init__(self, width, depth):
        self.mem = Memory(
            width=width,
            depth=depth,
            init = [randint(0, 2**width-1) for _ in range(depth)])

        self.addra = Signal(range(depth))
        self.dina = Signal(width)
        self.douta = Signal(width)
        self.wea = Signal()
        self.ena = Signal()

        self.addrb = Signal(range(depth))
        self.dinb = Signal(width)
        self.doutb = Signal(width)
        self.web = Signal()
        self.enb = Signal()

    def elaborate(self, platform):
        m = Module()

        m.submodules["mem"] = self.mem
        rp0 = self.mem.read_port()
        wp0 = self.mem.write_port()

        rp1 = self.mem.read_port()
        wp1 = self.mem.write_port()

        m.d.comb += rp0.addr.eq(self.addra)
        m.d.comb += wp0.addr.eq(self.addra)
        m.d.comb += wp0.data.eq(self.dina)
        m.d.comb += self.douta.eq(rp0.data)
        m.d.comb += rp0.en.eq(self.ena)
        m.d.comb += wp0.en.eq(self.wea & self.ena)

        m.d.comb += rp1.addr.eq(self.addrb)
        m.d.comb += wp1.addr.eq(self.addrb)
        m.d.comb += wp1.data.eq(self.dinb)
        m.d.comb += self.doutb.eq(rp1.data)
        m.d.comb += rp1.en.eq(self.enb)
        m.d.comb += wp1.en.eq(self.web & self.enb)
        return m

And if I create a design in the following format, Vivado will recognize it as a True Dual Port BRAM. I'm building for the Nexys4DDR, which has 16 switches and 16 LEDs, and I'm using the top half of each for Port A, and the bottom half for Port B, just to prevent the memory from being optimized out.

class TrueDualPortTest(Elaboratable):
    def elaborate(self, platform):
        m = Module()
        m.submodules["tdp"] = tdp = TrueDualPort(8, 4096)

        sw_pins = Cat([platform.request("switch",i).i for i in range(8)])
        led_pins = Cat([platform.request("led",i).o for i in range(8)])

        counter = Signal(8)
        m.d.sync += counter.eq(counter + 1)

        # ---- Port A ----
        m.d.comb += tdp.ena.eq(1)
        m.d.comb += tdp.wea.eq(1)
        m.d.sync += tdp.dina.eq(counter) # <- uncommenting this makes Vivado unable to recognize it as a TDP!
        m.d.sync += tdp.addra.eq(sw_pins[8:])
        m.d.sync += led_pins[8:].eq(tdp.douta)


        # ---- Port B ----
        m.d.comb += tdp.enb.eq(1)
        m.d.comb += tdp.web.eq(0)
        m.d.sync += tdp.dinb.eq(counter + 3) #  <- uncommenting this makes Vivado unable to recognize it as a TDP!
        m.d.sync += tdp.addrb.eq(sw_pins[:8])
        m.d.sync += led_pins[:8].eq(tdp.doutb)

        return m

If I build this with Nexys4DDRPlatform().build(TrueDualPortTest()), then Vivado will happily recognize this as a true dual port BRAM, as is seen in the logs:

INFO: [Synth 8-3971] The signal "\top.tdp :/mem_reg" was recognized as a true dual port RAM template.

And the post-placement resource utilization report will show that block ram is being used. However, if I uncomment either (or both) of the two lines marked in the source above - Vivado will map everything to distributed RAM instead. This seems to hold true if I keep increasing the depth of the RAM.

I also noticed that if I assign the en and we signals synchronously instead of combinationally, Vivado will throw an Unrecognized RAM template error.

My question is: How do I create a true-dual port RAM in Amaranth? I'd like to avoid directly instantiating an inferred BRAM template, since I'm not able to use the built-in simulator on external Verilog.

Thanks everyone!

Related Info:

I did notice that a while back the Yosys memory interface was reworked and support for TDP memories was added - I'm not sure if that has any implications for Amaranth, though.
YosysHQ/yosys#1959

And that appears to be reflected in the yosys docs:
https://yosyshq.readthedocs.io/projects/yosys/en/latest/CHAPTER_Memorymap.html

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions