Skip to content

DDR4 Calibration Succeeds But Memtest Hangs Without Printf Statements #369

Open

Description

@MojoJojo

Hi,
I am trying to upgrade my project Microwatt SoC to utilize DDR4 on an Xilinx VCU-118 board. I successfully ran the processor core on the board without DRAM support, confirming that the board and setup are stable. The Microwat SoC previously used DDR3 memory with a Litex-generated core (using gen.py ), but was never tested on the Xilinx VCU-118 board.

The VCU-118 board features an interesting DRAM setup, utilizing two DRAM modules (MT40A256M16GE-083E), each with 2.5 GB of memory. The board features two independent 80-bit DDR4 memory channels (C1 and C2), although typically only channel C1 is utilized in most designs. Each channel consists of five 256Mbit × 16-bit DDR4 SDRAM chips arranged as an 80-bit wide interface, where the extra bits beyond 64 are used for ECC or left unused.
In the configuration used for this issue, the design utilizes only the first 64 data bits (8-byte lanes × 8 bits each) of channel C1, requiring four of the five available chips to form a standard 64-bit-wide memory interface. The memory connects to FPGA I/O banks 71, 72, and 73, with each byte lane having its own DQS (data strobe) pair and DM (data mask) signal. The interface utilizes the POD12_DCI I/O standard for data signals and the DIFF_POD12 standard for differential strobe signals, with pre-emphasis and equalization settings to ensure signal integrity at high speeds.

I generated the litedram core using gen.py script with the following parameters:

{
    

    # General ------------------------------------------------------------------
    "speedgrade": -2,         
    "cpu":        None,       
    "memtype":    "DDR4",     
    
    # PHY - UltraScale+ DDR PHY (correct one!)
    "sdram_phy":       "USPDDRPHY",          # UltraScale+ DDR PHY
    "cmd_latency":     1,                    
    "sdram_module":    "MT40A256M16",  # VCU118 specific part
    "sdram_module_speedgrade": "2400", 
    "sdram_module_nb": 8,                    # 5 devices (80-bit)
    "sdram_rank_nb":   1,
               
    
    # Electrical ---------------------------------------------------------------
    "rtt_nom": "60ohm",  
    "rtt_wr":  "120ohm", 
    "ron":     "34ohm",  
    
    # Frequency - Match the pattern you found
    "input_clk_freq":   125e6, 
    "sys_clk_freq":     125e6,
    "iodelay_clk_freq": 500e6, 
    
    # Core ---------------------------------------------------------------------
    "cmd_buffer_depth": 16,    
    
    # User Ports ---------------------------------------------------------------
    "user_ports": {
        "native_0" : {
            "type": "native",
            "block_until_ready": False,
        },
    },
    "l2_cache_size": 8192
}

When I integrated the code into my VHDL code, I was able to get LiteDRAM calibration routines to work. I could see the read/write levelling output, and it all works fine. There are no address errors, but there are bus errors and memory errors.
In fact, upon deeper inspection, I realized that the very first write to memory freezes:

The memtest code in litex/soc/software/libbase/memtest.c performs:

c// Write pattern
for(i=0; i<size/4; i++) {
    array[i] = ONEZERO;  // <----- fails right here 
}

flush_cpu_dcache();
flush_l2_cache();

// Read/verify pattern  
for(i=0; i<size/4; i++) {
    rdata = array[i];
    // verification...
}

The amusing part was that if I inserted a printf statement inside the for loop, it would not freeze and continue until it reached the next read/verify loop. I added a printf statement, and that worked too. I could get the litex prompt by placing printfs in each loop.

I am using a VHDL wrapper between the generated core and my top-level file, which I can provide if needed.

I have been trying to debug this for many nights, but I am now running out of ideas. Is this a timing or a boundary issue? But not sure. Any tips, suggestions, or guidance on troubleshooting this would be great.

Thank you for your help and assistance.

Activity

  1. enjoy-digital commented on Nov 6, 2026

    @enjoy-digital
    Owner

    Hi @MojoJojo,

    sorry for the late answer. The symptoms point at the user port path rather than at the DRAM/PHY: the calibration runs through the wb_ctrl CSR bus and passes, while memtest goes through native_0 and your VHDL wrapper and fails. And "hangs with back-to-back writes, works with a printf between them" is the classic signature of a missed write-data handshake on the native port.

    The native port protocol was only visible in the code so far; it is now documented in doc/gen.md, "Native port protocol". The point that bites here: the controller ignores wdata_valid. For every accepted write command it pulses wdata_ready for one cycle when the write is sent to the DRAM and samples wdata_data/wdata_we on that cycle, so the data has to be presented from the acceptance of the command until that pulse. A master that presents it later writes garbage (your bus errors) and then waits forever for a pulse that already happened (your hang); a printf between the writes hides it.

    Microwatt's litedram-wrapper-l2.vhdl handles this with a store queue filled at command acceptance (see its "Current litedram ignores user_port0_wdata_valid" comment), so if you adapted it, check the store/refill paths and the generics first. I generated your YAML on current master: the port is cmd_addr[24:0] (64-byte words), wdata_data[511:0], wdata_we[63:0], i.e. DRAM_PORT_WIDTH => 512 and DRAM_ABITS => 25 (Genesys2 uses 256/25), and 512-bit is wider than any board Microwatt has been run on. Also make sure the whole SoC runs on user_clk/user_rst of the core (as Microwatt does with system_clk), not on the board clock, and that the 512-bit datapath meets timing at 125 MHz.

    To localize it quickly:

    1. litedram_gen --sim your.yml gives the same core around the DFI-level DRAM model: simulate your wrapper + core in your usual simulator and watch the cmd/wdata/rdata handshakes on the first write (Microwatt has extras/sim_litedram.vhdl and sim_dram_verilate.mk for this).
    2. Or replace the native port by a Wishbone one ("wishbone_0": {"type": "wishbone", "data_width": 64}): LiteDRAM then owns the native handshake and your 64-bit Wishbone master drives the core directly. If memtest passes this way, the issue is in the wrapper.

    Small config notes: sdram_module_speedgrade was not a litedram_gen key when you generated this core, so it was ignored and the module's default timings were used (it is a real key since #470, and an unknown value now raises an error) and l2_cache_size is a Microwatt generate.py key; the rest (cmd_latency: 1, 125 MHz sys, 500 MHz IDELAYCTRL) is in line with our UltraScale+ example.

    If the issue is still current, share the wrapper and ideally a simulation trace of the first write and I'll have a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions