From 052dc19246698ca69f6d9b406b41a800bb33e5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Boczar?= Date: Mon, 15 Feb 2021 16:24:46 +0100 Subject: [PATCH] lpddr4: improve documentation --- litedram/phy/lpddr4/basephy.py | 37 ++++++++++++++ litedram/phy/lpddr4/commands.py | 36 ++++++++++++-- litedram/phy/lpddr4/sim.py | 87 ++++++++++++++++++++++++++++----- litedram/phy/lpddr4/simphy.py | 24 +++++++++ litedram/phy/lpddr4/simsoc.py | 13 +++-- litedram/phy/lpddr4/utils.py | 8 +++ 6 files changed, 183 insertions(+), 22 deletions(-) diff --git a/litedram/phy/lpddr4/basephy.py b/litedram/phy/lpddr4/basephy.py index 7746a6b..b491531 100644 --- a/litedram/phy/lpddr4/basephy.py +++ b/litedram/phy/lpddr4/basephy.py @@ -14,6 +14,7 @@ from litedram.phy.lpddr4.commands import DFIPhaseAdapter class Latency: + """Used to specify latency for LPDDR4PHY""" def __init__(self, sys, sys8x=0): self.sys = sys + sys8x//8 self.sys8x = sys8x % 8 @@ -47,6 +48,42 @@ class LPDDR4Output: class LPDDR4PHY(Module, AutoCSR): + """Core of LPDDR4 PHYs. + + This class implements all the logic required to convert DFI to/from pads. + It works in a single clock domain. Signals for DRAM pads are stored in + LPDDR4Output (self.out). Concrete implementations of LPDDR4 PHYs derive + from this class and perform (de-)serialization of LPDDR4Output to pads. + + DFI commands + ------------ + Not all LPDDR4 commands map directly to DFI commands. For this reason ZQC + is treated specially in that DFI ZQC is translated into LPDDR4 MPC and has + different interpretation depending on DFI.address. + + Also, due to the fact that LPDDR4 has 256-bit Mode Register space, the DFI + MRS command encodes both register address *and* value in DFI.address (instead + of the default in LiteDRAM to split them between DFI.address and DFI.bank). + + Refer to the documentation in `commands.py` for further information. + + Parameters + ---------- + pads : object + Object containing LPDDR4 pads. + sys_clk_freq : float + Frequency of memory controller's clock. + ser_latency : Latency + Additional latency introduced due to signal serialization. + des_latency : Latency + Additional latency introduced during signal deserialization. + phytype : str + Name of the PHY (concrete implementation). + masked_write : bool + Use MASKED-WRITE commands if True else use WRITE commands (data masking will not work). + cmd_delay : int + Used to force cmd delay during initialization in BIOS. + """ def __init__(self, pads, *, sys_clk_freq, ser_latency, des_latency, phytype, masked_write=True, cmd_delay=None): diff --git a/litedram/phy/lpddr4/commands.py b/litedram/phy/lpddr4/commands.py index 460e338..a6bba83 100644 --- a/litedram/phy/lpddr4/commands.py +++ b/litedram/phy/lpddr4/commands.py @@ -32,8 +32,25 @@ class DFIPhaseAdapter(Module): consist only of a single "small command". To make counting DRAM timings easier, such a "small command" shall be sent on the 2nd slot (i.e. 3rd and 4th cycle). All timings are then counted starting from CS low on the 4th cycle. - """ + Parameters + ---------- + dfi_phase : Record(dfi.phase_description), in + Input from a single DFI phase. + masked_write : bool + Specifies how DFI write command (cas_n=0, ras_n=1, we_n=0) is interpreted, either + as LPDDR4 WRITE or MASKED-WRITE. MASKED-WRITE requires larger tCCD, but WRITE does + not permit masking of data, so if masking is needed MASKED-WRITE has to be used. + + Attributes + ---------- + cs : Signal(4), out + Values of CS on 4 subsequent DRAM SDR clock cycles. + ca : Array(4, Signal(6)), out + Values of CA[5:0] on 4 subsequent DRAM SDR clock cycles. + valid : Signal, out + Indicates that a valid command is presented on the `cs` and `ca` outputs. + """ def __init__(self, dfi_phase, masked_write=True): # CS/CA values for 4 SDR cycles self.cs = Signal(4) @@ -90,11 +107,20 @@ class Command(Module): Decodes a command from single DFI phase into LPDDR4 "small command" consisting of 2 CS values and 2 CA[5:0] values. - LPDDR4 "small commands" are transmited over 2 clock cycles. In first + LPDDR4 "small commands" are transmited over 2 clock cycles. In the first cycle CS is driven high and in the second cycle it stays low. In each of the cycles the bits on CA[5:0] are latched and interpreted differently. This module translates a DFI command into the values of CS/CA that shall be transmitted over 2 DRAM clock cycles. + + Attributes + ---------- + dfi : Record(dfi.phase_description), in + Input from single DFI phase. + cs : Signal(2), out + CS values over 2 subsequent DRAM SDR clock cycles. + ca : Array(2, Signal(6)), out + CA[5:0] values over 2 subsequent DRAM SDR clock cycles. """ # String description of 1st and 2nd edge of each command, later parsed to @@ -126,9 +152,9 @@ class Command(Module): def set(self, cmd): ops = [] - for i, description in enumerate(self.TRUTH_TABLE[cmd]): - for j, bit in enumerate(description.split()): - ops.append(self.ca[i][j].eq(self.parse_bit(bit, is_mpc=cmd == "MPC"))) + for cyc, description in enumerate(self.TRUTH_TABLE[cmd]): + for bit, bit_desc in enumerate(description.split()): + ops.append(self.ca[cyc][bit].eq(self.parse_bit(bit_desc))) if cmd != "DESELECT": ops.append(self.cs[0].eq(1)) return ops diff --git a/litedram/phy/lpddr4/sim.py b/litedram/phy/lpddr4/sim.py index 54424bf..edaccde 100644 --- a/litedram/phy/lpddr4/sim.py +++ b/litedram/phy/lpddr4/sim.py @@ -14,18 +14,58 @@ from litedram.phy.lpddr4.commands import MPC def log_level_getter(log_level): + """Parse logging level description + + Log level can be presented in a simple form (e.g. `--log-level=DEBUG`) to specify + the same level for all modules, or can set different levels for different modules + e.g. `--log-level=all=INFO,data=DEBUG`. + """ def get_level(name): return getattr(SimLogger, name.upper()) - # simple log_level, e.g. "INFO" - if "=" not in log_level: + + if "=" not in log_level: # simple log_level, e.g. "INFO" return lambda _: get_level(log_level) + # parse log_level in the per-module form, e.g. "--log-level=all=INFO,data=DEBUG" per_module = dict(part.split("=") for part in log_level.strip().split(",")) return lambda module: get_level(per_module.get(module, per_module.get("all", None))) class LPDDR4Sim(Module, AutoCSR): - def __init__(self, pads, *, sys_clk_freq, disable_delay, settings, log_level): + """LPDDR4 DRAM simulator + + This module simulates an LPDDR4 DRAM chip to aid LPDDR4 PHY development/testing. + It does not aim to simulate the internals of an LPDDR4 chip, rather it's behavior + as seen by the PHY. + + The simulator monitors CS/CA pads listening for LPDDR4 commands and updates the module + state depending on the command received. Any unexpected sequences are logged in simulation + as errors/warnings. On read/write commands the data simulation module is triggered + after CL/CWL and a data burst is handled, updating memory state. + + The simulator requires the following clock domains: + sys8x: 8x the memory controller clock frequency, phase aligned. + sys8x_90: Phase shifted by 90 degrees vs sys8x. + sys8x_ddr: Phase aligned with sys8x, double the frequency. + sys8x_90_ddr: Phase aligned with sys8x_90, double the frequency. + + Parameters + ---------- + pads : LPDDR4SimulationPads + DRAM pads + sys_clk_freq : float + System clock frequency + cl : int + LPDDR4 read latency (RL). + cwl : int + LPDDR4 write latency (WL). + disable_delay : bool + Disable checking of timings that rely on long CPU delays (mostly init sequence + timings). This is useful when running LiteX BIOS with CONFIG_DISABLE_DELAYS on. + log_level : str + SimLogger initial logging level (formatted for parsing with `log_level_getter`). + """ + def __init__(self, pads, *, sys_clk_freq, cl, cwl, disable_delay, log_level): log_level = log_level_getter(log_level) cd_cmd = "sys8x_90" @@ -34,12 +74,12 @@ class LPDDR4Sim(Module, AutoCSR): cd_dq_rd = "sys8x_90_ddr" cd_dqs_rd = "sys8x_ddr" - self.submodules.data = ClockDomainCrossing( + self.submodules.data_cdc = ClockDomainCrossing( [("we", 1), ("masked", 1), ("bank", 3), ("row", 17), ("col", 10)], cd_from=cd_cmd, cd_to=cd_dq_wr) cmd = CommandsSim(pads, - data_cdc = self.data, + data_cdc = self.data_cdc, clk_freq = 8*sys_clk_freq, log_level = log_level("cmd"), init_delays = not disable_delay, @@ -52,8 +92,8 @@ class LPDDR4Sim(Module, AutoCSR): cd_dq_rd = cd_dq_rd, cd_dqs_rd = cd_dqs_rd, clk_freq = 2*8*sys_clk_freq, - cl = settings.phy.cl, - cwl = settings.phy.cwl, + cl = cl, + cwl = cwl, log_level = log_level("data"), ) self.submodules.data = ClockDomainsRenamer(cd_dq_wr)(data) @@ -61,10 +101,14 @@ class LPDDR4Sim(Module, AutoCSR): # Commands ----------------------------------------------------------------------------------------- class PulseTiming(Module): - # Works like tXXDController with the following differences: - # * countdown triggered by a low to high pulse on `trigger` - # * `ready` is initially low, only after a trigger it can become high - # * provides `ready_p` which is high only for 1 cycle when `ready` becomes high + """Timing monitor with pulse input/output + + This module works like `tXXDController` with the following differences: + + * countdown triggered by a low to high pulse on `trigger` + * `ready` is initially low, only after a trigger it can become high + * provides `ready_p` which is high only for 1 cycle when `ready` becomes high + """ def __init__(self, t): self.trigger = Signal() self.ready = Signal() @@ -83,7 +127,16 @@ class PulseTiming(Module): ] -class CommandsSim(Module, AutoCSR): # clock domain: clk_p +class CommandsSim(Module, AutoCSR): + """Command simulation + + This module interprets LPDDR4 commands found on the CS/CA pads. It keeps track of currently + opened rows (per bank) and stores the values of Mode Registers. It also checks that the DRAM + initialization sequence is performed according to specification. On any read/write commands + signals indicating a burst are sent to the data simulator for handling. + + Command simulator should work in the clock domain of `pads.clk_p` (SDR). + """ def __init__(self, pads, data_cdc, *, clk_freq, log_level, init_delays=False): self.submodules.log = log = SimLogger(log_level=log_level, clk_freq=clk_freq) self.log.add_csrs() @@ -439,7 +492,15 @@ class CommandsSim(Module, AutoCSR): # clock domain: clk_p # Data --------------------------------------------------------------------------------------------- -class DataSim(Module, AutoCSR): # clock domain: ddr +class DataSim(Module, AutoCSR): + """Data simulator + + This module is responsible for handling read/write bursts. It's operation has to be triggered + by the command simulator. Data is stored in an internal memory, no state is verified (row + open/closed, etc.), this must be checked by command simulation. + + This module runs with DDR clocks (simulation clocks with double the frequency of `pads.clk_p`). + """ def __init__(self, pads, cmds_sim, *, cd_dq_wr, cd_dq_rd, cd_dqs_wr, cd_dqs_rd, cl, cwl, clk_freq, log_level): self.submodules.log = log = SimLogger(log_level=log_level, clk_freq=clk_freq) self.log.add_csrs() diff --git a/litedram/phy/lpddr4/simphy.py b/litedram/phy/lpddr4/simphy.py index 8b5bf4f..a156e2e 100644 --- a/litedram/phy/lpddr4/simphy.py +++ b/litedram/phy/lpddr4/simphy.py @@ -5,6 +5,13 @@ from litedram.phy.lpddr4.basephy import LPDDR4PHY, DoubleRateLPDDR4PHY, Latency class LPDDR4SimulationPads(Module): + """Pads for simulation purpose + + To avoid simulate tristate behavior of DQ/DQS/DMI pins separate input and output + pins (_i/_o) are provided. Output pins are to be driven by the PHY and input pins + are to be driven by the DRAM simulator. This module sets the actual values on pins + `dq`, `dqs` and `dmi` based on output enable signals. + """ def __init__(self, databits=16): self.clk_p = Signal() self.clk_n = Signal() @@ -36,6 +43,17 @@ class LPDDR4SimulationPads(Module): class _LPDDR4SimPHYMixin: + """Common serialization logic for simulation PHYs + + This mixin provides `do_serialization` method for constructing the boilerplate + serialization/deserialization paths for a simulation PHY. This can serve as a + reference for implemeing PHYs for concrete FPGAs. + + To make the (de-)serialization work in simulation two additional clock domains + are required: `sys8x_ddr` and `sys8x_90_ddr`. These correspond to `sys8x` and + `sys8x_90`, are phase aligned with them and at twice their frequency. These + clock domains are requried to implement DDR (de-)serialization at 8x sys clock. + """ def _add_name(self, prefix, kwargs): name = prefix + "_" + kwargs.pop("name", "") kwargs["name"] = name.strip("_") @@ -102,6 +120,7 @@ class _LPDDR4SimPHYMixin: class LPDDR4SimPHY(_LPDDR4SimPHYMixin, LPDDR4PHY): + """LPDDR4 simulation PHY with direct 16:1 serializers""" def __init__(self, aligned_reset_zero=False, **kwargs): pads = LPDDR4SimulationPads() self.submodules += pads @@ -119,6 +138,11 @@ class LPDDR4SimPHY(_LPDDR4SimPHYMixin, LPDDR4PHY): class DoubleRateLPDDR4SimPHY(_LPDDR4SimPHYMixin, DoubleRateLPDDR4PHY): + """LPDDR4 simulation PHY basing of DoubleRateLPDDR4PHY + + `DoubleRateLPDDR4PHY` performs a single serialization step between `sys` and `sys2x`, + so this PHY wrapper has to do the serialization between `sys2x` and `sys8x` (SDR/DDR). + """ def __init__(self, aligned_reset_zero=False, **kwargs): pads = LPDDR4SimulationPads() self.submodules += pads diff --git a/litedram/phy/lpddr4/simsoc.py b/litedram/phy/lpddr4/simsoc.py index 446857f..ee911ae 100644 --- a/litedram/phy/lpddr4/simsoc.py +++ b/litedram/phy/lpddr4/simsoc.py @@ -103,9 +103,13 @@ def get_clocks(sys_clk_freq): # SoC ---------------------------------------------------------------------------------------------- class SimSoC(SoCCore): - def __init__(self, clocks, log_level, auto_precharge=False, with_refresh=True, trace_reset=0, - disable_delay=False, masked_write=True, double_rate_phy=False, finish_after_memtest=False, - **kwargs): + """Simulation of SoC with LPDDR4 DRAM + + This is a SoC used to run Verilator-based simulations of LiteDRAM with a simulated LPDDR4 chip. + """ + def __init__(self, clocks, log_level, + auto_precharge=False, with_refresh=True, trace_reset=0, disable_delay=False, + masked_write=True, double_rate_phy=False, finish_after_memtest=False, **kwargs): platform = Platform() sys_clk_freq = clocks["sys"]["freq_hz"] @@ -161,7 +165,8 @@ class SimSoC(SoCCore): # LPDDR4 Sim ------------------------------------------------------------------------------- self.submodules.lpddr4sim = LPDDR4Sim( pads = self.ddrphy.pads, - settings = self.sdram.controller.settings, + cl = self.sdram.controller.settings.phy.cl, + cwl = self.sdram.controller.settings.phy.cwl, sys_clk_freq = sys_clk_freq, log_level = log_level, disable_delay = disable_delay, diff --git a/litedram/phy/lpddr4/utils.py b/litedram/phy/lpddr4/utils.py index a3ed008..848a70f 100644 --- a/litedram/phy/lpddr4/utils.py +++ b/litedram/phy/lpddr4/utils.py @@ -172,6 +172,14 @@ class Deserializer(Module): class SimLogger(Module, AutoCSR): + """Logger for use in simulation + + This module allows for easier message logging when running simulation designs. + The logger can be used from `comb` context so it the methods can be directly + used inside `FSM` code. It also provides logging levels that can be used to + filter messages, either by specifying the default `log_level` or in runtime + by driving to the `level` signal or using a corresponding CSR. + """ # Allows to use Display inside FSM and to filter log messages by level (statically or dynamically) DEBUG = 0 INFO = 1