#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Self
from hwt.code import If, Concat
from hwt.constants import READ_WRITE, WRITE, READ
from hwt.hObjList import HObjList
from hwt.hdl.types.bits import HBits
from hwt.hwIOs.hwIOArray import HwIOArray
from hwt.hwIOs.std import HwIOBramPort, HwIOClk, HwIOBramPort_noClk
from hwt.hwModule import HwModule
from hwt.hwParam import HwParam
from hwt.pyUtils.typingFuture import override
from hwt.serializer.mode import serializeParamsUniq
from hwt.synthesizer.rtlLevel.rtlSignal import RtlSignal
[docs]
@serializeParamsUniq
class RamSingleClock(HwModule):
"""
RAM/ROM with only one clock signal.
It can be configured to have arbitrary number of ports.
It can also be configured to have write mask or to be composed from multiple smaller memories.
:note: This memory may not be mapped to RAM
if synthesis tool consider it to be too small.
:ivar PORT_CNT: HwParam which specifies number of ram ports,
it can be int or tuple of READ_WRITE, WRITE, READ
to specify rw access for each port separately
:ivar HAS_BE: HwParam, if True the write ports will have byte enable signal
:ivar READ_LATENCY: latency in clock cycles from read enable=1 to read data appear on the output
0=distmem, 1=BRAM
.. hwt-autodoc::
"""
PORT_CLS = HwIOBramPort_noClk
@override
def hwConfig(self):
self.ADDR_WIDTH = HwParam(10)
self.DATA_WIDTH = HwParam(64)
self.PORT_CNT = HwParam(1)
self.HAS_BE = HwParam(False)
self.MAX_BLOCK_DATA_WIDTH = HwParam(None)
self.INIT_DATA = HwParam(None)
self.READ_LATENCY = HwParam(1)
[docs]
def _declr_ports(self):
PORTS = self.PORT_CNT
with self._hwParamsShared():
ports = HwIOArray()
if isinstance(PORTS, int):
for _ in range(PORTS):
p = self.PORT_CLS()
ports.append(p)
else:
for access_mode in PORTS:
p = self.PORT_CLS()
if access_mode == READ_WRITE:
pass
elif access_mode == READ:
p.HAS_W = False
elif access_mode == WRITE:
p.HAS_R = False
else:
raise ValueError(access_mode)
ports.append(p)
self.port = ports
[docs]
def _declr_children(self):
# for the case where this memory will be realized using multiple memory blocks
children: HObjList[Self] = HObjList()
MAX_DW = self.MAX_BLOCK_DATA_WIDTH
if MAX_DW is not None and MAX_DW < self.DATA_WIDTH:
DW = self.DATA_WIDTH
while DW > 0:
c = self.__class__()
c._updateHwParamsFrom(self, exclude=({"DATA_WIDTH"}, {}))
c.DATA_WIDTH = min(DW, MAX_DW)
if self.INIT_DATA is not None:
raise NotImplementedError()
children.append(c)
DW -= MAX_DW
self.children = children
@override
def hwDeclr(self):
self.clk = HwIOClk()
self._declr_ports()
self._declr_children()
[docs]
@staticmethod
def mem_write(mem, port: HwIOBramPort_noClk):
drive = []
if port.HAS_BE:
assert port.DATA_WIDTH % 8 == 0, port.DATA_WIDTH
# we for each byte separate
# drive.append(
# mem[port.addr](apply_write_with_mask(mem[port.addr], port.din, port.we))
# )
for b_i, be in enumerate(port.we):
low = b_i * 8
drive.append(
If(be,
mem[port.addr][low + 8: low](port.din[low + 8: low])
)
)
elif port.HAS_R and port.HAS_W:
# explicit we
drive.append(
If(port.we,
mem[port.addr](port.din)
)
)
else:
# en used as we
drive.append(mem[port.addr](port.din))
return drive
[docs]
@staticmethod
def connect_HwIOBramPort_noClk_to_mem(clk: RtlSignal, read_latency, port: HwIOBramPort_noClk, mem: RtlSignal):
if port.HAS_R and port.HAS_W:
rStm = \
If(port.en,
*RamSingleClock.mem_write(mem, port),
port.dout(mem[port.addr]),
).Else(
port.dout(None),
)
elif port.HAS_R:
rStm = \
If(port.en,
port.dout(mem[port.addr])
)
elif port.HAS_W:
rStm = \
If(port.en,
*RamSingleClock.mem_write(mem, port),
)
else:
raise AssertionError("Bram port has to have at least write or read part")
if read_latency == 0:
pass
elif read_latency == 1:
If(clk._onRisingEdge(),
rStm
)
else:
raise NotImplementedError(read_latency)
[docs]
def delegate_to_children(self):
MAX_DW = self.MAX_BLOCK_DATA_WIDTH
DW = self.DATA_WIDTH
for ports in zip(self.port, *[c.port for c in self.children]):
dout = []
p = ports[0]
clk = getattr(p, "clk", None)
for i, cp in enumerate(ports[1:]):
cp.en(p.en)
cp.addr(p.addr)
if p.HAS_R:
dout.append(cp.dout)
if p.HAS_W:
if p.HAS_BE:
cp.we(p.we[min((i + 1) * (MAX_DW // 8), DW // 8):i * (MAX_DW // 8)])
elif p.HAS_R:
cp.we(p.we)
cp.din(p.din[min((i + 1) * (MAX_DW), DW):i * (MAX_DW)])
if clk is not None:
cp.clk(clk)
if dout:
p.dout(Concat(*reversed(dout)))
clk = getattr(self, "clk", None)
for c in self.children:
c.clk(clk)
@override
def hwImpl(self):
if self.children:
self.delegate_to_children()
else:
dt = HBits(self.DATA_WIDTH)[2 ** self.ADDR_WIDTH]
self._mem = self._sig("ram_memory", dt, def_val=self.INIT_DATA)
for p in self.port:
self.connect_HwIOBramPort_noClk_to_mem(self.clk, self.READ_LATENCY, p, self._mem)
[docs]
@serializeParamsUniq
class RamMultiClock(HwModule):
"""
RAM where each port has an independent clock.
It can be configured to true dual port RAM etc.
It can also be configured to have write mask or to be composed from multiple smaller memories.
:note: write-first variant
.. hwt-autodoc::
"""
PORT_CLS = HwIOBramPort
@override
def hwConfig(self):
RamSingleClock.hwConfig(self)
self.PORT_CNT = 2
@override
def hwDeclr(self):
RamSingleClock._declr_ports(self)
RamSingleClock._declr_children(self)
@override
def hwImpl(self):
if self.children:
self.delegate_to_children()
else:
dt = HBits(self.DATA_WIDTH)[2 ** self.ADDR_WIDTH]
self._mem = self._sig("ram_memory", dt, self.INIT_DATA)
for p in self.port:
RamSingleClock.connect_HwIOBramPort_noClk_to_mem(p.clk, self.READ_LATENCY, p, self._mem)
if __name__ == "__main__":
from hwt.synth import to_rtl_str
m = RamSingleClock()
m.HAS_BE = True
m.ADDR_WIDTH = 10
m.DATA_WIDTH = 17 * 8
print(to_rtl_str(m))