Skip to content

RGMII Issue with Colorlight i9 7.2 #736

Description

@DatanoiseTV

The problem:

  • No IP/Ethernet Traffic, no DHCP, had couple ICMP replies a few times, but nothing reproducible.
  • [hb] BMSR=0x796d link=1 aneg=1 LPA=0xc1e1 PHY-spd=1000FD inband=0x00 FPGA-spd=link-down RXC delta=189015015 ALIVE IP=0.0.0.0
self.ethphy = LiteEthPHYRGMII(
                clock_pads = self.platform.request("eth_clocks", eth_port),
                pads       = self.platform.request("eth", eth_port),
                tx_delay = 0)

Tried with original Break Out Board and with Hans Baier Colorlight x QMTech board. @hansfbaier

Any hints very appreciated.

#!/usr/bin/env python3
"""Colorlight i9 signal-generator SoC (on-chip webserver edition).

Architecture:
  - VexRiscv softcore runs bare-metal firmware in DDR3.
  - One on-board RGMII Broadcom B50612D PHY (the U30-side jack on the
    board, the one closer to the FPGA's H2/G1 pins) is wired up at
    gigabit / auto-neg — same recipe as the upstream `colorlite`
    reference (https://github.com/enjoy-digital/colorlite). Forcing
    100BASE-TX is *not* used because LiteEth's RGMII PHY is a DDR
    gigabit core; at 100M the partner duplicates each nibble across
    the two RGMII edges, which the gigabit-only MAC mis-decodes as
    corrupted SFD bytes and silently drops every frame.
  - The hybrid `add_etherbone(... with_ethmac=True)` exposes:
      * a hardware Etherbone UDP/IP stack at static `etherbone_ip` for
        litex_server tooling, and
      * a software ethmac slot SRAM the lwIP netif reads from.
    A MAC-address dispatcher in gateware routes frames between the two.
  - DDS feeds I²S TX; I²S RX sampled into CSRs.
  - LED on L2 is driven by the upstream LedChaser at ~10 Hz so the
    user has a visual "gateware is alive" heartbeat.
  - Debug UART: stock `serial` pins (TX=J17, RX=H18 on i9 v7.2).
  - Pin map (user I²S connector — F3↔H3 swapped from v1 to match the
    codec schematic; codec DOUT/DIN are named from the codec's POV):
        E1 = BCLK                    (out)
        E4 = FSYNC/LRCLK             (out)
        H3 = FPGA→codec data (DIN)   (out, codec pin 5)
        F3 = codec→FPGA data (DOUT)  (in,  codec pin 4)

Usage:
    python3 soc.py --build                 # gateware
    make -C firmware                       # firmware.bin
    openFPGALoader -b colorlight-i9 build/gateware/colorlight_i5.bit
    litex_term /dev/ttyUSB0 --kernel firmware/firmware.bin
"""
import argparse

from migen                            import Module, Signal
from migen.genlib.cdc                 import MultiReg

from litex.soc.integration.builder    import Builder
from litex.soc.interconnect.csr       import AutoCSR, CSRStatus
from litex.build.generic_platform     import Subsignal, Pins, IOStandard
from litex.gen                        import LiteXModule

from liteeth.phy.ecp5rgmii            import LiteEthPHYRGMII

from litex_boards.targets.colorlight_i5 import BaseSoC as ColorlightBaseSoC

from dds import DDS
from i2s import I2SMaster


# --------------------------------------------------------------------------
# RXC tick counter — diagnostic for "is the PHY's RXC reaching the FPGA?"
# --------------------------------------------------------------------------
class RXClockMonitor(Module, AutoCSR):
    """Free-running 32-bit counter clocked by `eth_rx`.

    Sys-side reads the value via CSR; firmware reads twice with a delay
    and compares. If the value advances → RXC is toggling at the FPGA pin
    and the eth_rx clock domain is alive. If it stays at 0 forever → the
    PHY's RXC isn't reaching the FPGA (board / strap / trace issue) and
    no amount of MAC tweaking will help.

    The CDC isn't Gray-coded — we only care whether the count *changes*,
    not its exact value, so MultiReg's bit-skew at sample time is fine.
    """
    def __init__(self):
        self.count = CSRStatus(32, description="eth_rx clock tick count")
        cnt_rx = Signal(32)
        self.sync.eth_rx += cnt_rx.eq(cnt_rx + 1)
        self.specials += MultiReg(cnt_rx, self.count.status)


# --------------------------------------------------------------------------
# I²S connector extension
# --------------------------------------------------------------------------
# Pin direction is named from the FPGA's perspective:
#   `dout` = FPGA's output to codec  → wired to codec DIN  (pin 5 → board H3)
#   `din`  = FPGA's input from codec ← wired to codec DOUT (pin 4 → board F3)
_i2s_io = [
    ("i2s", 0,
        Subsignal("bclk",  Pins("E1")),
        Subsignal("fsync", Pins("E4")),
        Subsignal("dout",  Pins("H3")),
        Subsignal("din",   Pins("F3")),
        IOStandard("LVCMOS33"),
    ),
]


# --------------------------------------------------------------------------
# DDS → I²S glue, exposed as one CSR-bearing LiteX module.
# --------------------------------------------------------------------------
class SigGen(LiteXModule, AutoCSR):
    def __init__(self, pads, sys_freq, sample_rate=48000, bclk_ratio=64):
        self.dds = DDS(width=16)
        self.i2s = I2SMaster(sys_freq=sys_freq, sample_rate=sample_rate,
                             bclk_ratio=bclk_ratio, bits=16, slot_bits=32)

        # DDS advances once per stereo frame; same sample on both channels.
        self.comb += [
            self.dds.sample_stb.eq(self.i2s.sample_stb),
            self.i2s.left_tx .eq(self.dds.out),
            self.i2s.right_tx.eq(self.dds.out),
        ]

        # Pin drives.
        self.comb += [
            pads.bclk .eq(self.i2s.bclk),
            pads.fsync.eq(self.i2s.lrclk),
            pads.dout .eq(self.i2s.dout),
            self.i2s.din.eq(pads.din),
        ]

        # Read-back of last captured RX samples.
        self.rx_left  = CSRStatus(16)
        self.rx_right = CSRStatus(16)
        self.comb += [
            self.rx_left .status.eq(self.i2s.left_rx),
            self.rx_right.status.eq(self.i2s.right_rx),
        ]


# --------------------------------------------------------------------------
# Top-level SoC
# --------------------------------------------------------------------------
class SigGenSoC(ColorlightBaseSoC):
    def __init__(self,
                 revision          = "7.2",
                 # sys=48 MHz: at 49 MHz the placer couldn't satisfy
                 # *both* sys and eth_rx (gigabit, 125 MHz) at once —
                 # every seed produced a layout where one closed and
                 # the other didn't (see-saw failure). 48 MHz gives sys
                 # enough margin that eth_rx routing wins consistently.
                 # fs lands at 93.75 kHz (2.3% under 96 kHz), well inside
                 # the codec's auto-detect lock window — the codec was
                 # already locking at the same offset percentage at the
                 # 48 kHz fs we used earlier.
                 sys_clk_freq      = 48e6,
                 sample_rate       = 48000,
                 bclk_ratio        = 256,
                 etherbone_ip      = "192.168.1.50",
                 etherbone_mac     = 0x10e2d5000000,
                 etherbone_port    = 20000,
                 ethmac_mac        = 0x10e2d5000001,
                 **kwargs):

        # `lite` = RV32I + I$/D$. Plenty for lwIP (integer-only) and easier
        # on timing than `standard`. Earlier multi-MAC builds were already
        # close to the timing wall on `lite`; dropping back to single-PHY
        # gigabit gives plenty of margin.
        kwargs.setdefault("cpu_type",    "vexriscv")
        kwargs.setdefault("cpu_variant", "lite")
        kwargs.setdefault("integrated_rom_size", 0x10000)

        # We set up Ethernet ourselves (hybrid MAC+Etherbone), so tell the
        # upstream target to not instantiate its own PHY/MAC.
        kwargs["with_ethernet"]  = False
        kwargs["with_etherbone"] = False

        ColorlightBaseSoC.__init__(
            self,
            board         = "i9",
            revision      = revision,
            sys_clk_freq  = sys_clk_freq,
            **kwargs,
        )

        # ---- Single-PHY gigabit Ethernet -------------------------------
        # Match the colorlite reference (which we know works on this PHY):
        #   * stock LiteEthPHYRGMII (DDR / 125 MHz / auto-neg).
        #   * tx_delay = 0  -- the BCM PHY has its internal RGMII delay
        #     enabled by default, so the MAC must NOT add another 2 ns.
        #   * data_width = 32 on add_etherbone so the MAC keeps up with
        #     gigabit on a ~50 MHz sys clock (49.152 × 4 bytes/cycle ≈
        #     1.6 Gbps, comfortably above 1 Gbps line rate).
        # No MDIO speed-forcing — auto-negotiation picks the highest rate
        # both ends support (typically 1000BASE-T on a modern switch).
        # The platform constrains eth_clocks:rx 0 at 125 MHz; with a
        # single PHY this closes with margin.
        # Standard upstream recipe (litex-boards colorlight_i5 BaseSoC,
        # colorlite, etc.): only override tx_delay = 0 — the BCM PHY's
        # RGMII-ID strap adds the TX delay internally; rx_delay stays
        # at the LiteEth default (2 ns). Reportedly works on this PHY
        # everywhere; if it doesn't here we have an instrumented diag
        # to see exactly where it breaks.
        self.ethphy = LiteEthPHYRGMII(
            clock_pads = self.platform.request("eth_clocks", 0),
            pads       = self.platform.request("eth", 0),
            tx_delay   = 0,
        )
        self.add_ethernet(phy=self.ethphy, dynamic_ip=True, data_width=32)

        # RXC tick counter — diagnostic only.
        self.submodules.rxclkmon = RXClockMonitor()

        # I²S pin extension + peripheral.
        self.platform.add_extension(_i2s_io)
        self.submodules.siggen = SigGen(
            pads        = self.platform.request("i2s"),
            sys_freq    = sys_clk_freq,
            sample_rate = sample_rate,
            bclk_ratio  = bclk_ratio,
        )

        # ---- Flashboot ------------------------------------------------------
        # Tell the LiteX BIOS where to find a flash-resident firmware image.
        # On power-up the BIOS will (in this order) try ROM boot, *flash boot*,
        # SD-card boot, network boot, then drop to the prompt. We put the
        # firmware at SPI-flash offset 0x00100000 (1 MiB in), well clear of the
        # ECP5 bitstream that lives at offset 0. Spiflash region base is
        # 0x00800000 in this SoC's memory map, so the absolute address is:
        #     0x00800000 + 0x00100000 = 0x00900000
        # The image at that address is `tools/mkflashimg.py firmware.bin`
        # output: 4-byte little-endian length + 4-byte little-endian CRC32
        # + raw bytes (zlib/Ethernet poly 0xEDB88320, matches LiteX
        # libbase/crc32.c).
        self.add_constant("FLASH_BOOT_ADDRESS", 0x00900000)


# --------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------
def main():
    p = argparse.ArgumentParser(description="Colorlight i9 signal-generator SoC")
    p.add_argument("--revision",     default="7.2")
    p.add_argument("--sys-clk-freq", type=int, default=48_000_000)
    p.add_argument("--sample-rate",  type=int, default=48000)
    p.add_argument("--output-dir",   default="build")
    p.add_argument("--seed",         type=int, default=2,
                   help="nextpnr placer seed; sweep 1..N to find one that closes")
    p.add_argument("--build", action="store_true")
    p.add_argument("--load",  action="store_true")
    p.add_argument("--flash", action="store_true")
    args = p.parse_args()

    soc = SigGenSoC(
        revision     = args.revision,
        sys_clk_freq = args.sys_clk_freq,
        sample_rate  = args.sample_rate,
    )
    builder = Builder(soc,
                      output_dir = args.output_dir,
                      csr_csv    = "csr.csv")
    builder.build(run=args.build, seed=args.seed, timingstrict=True)

    if args.load or args.flash:
        prog = soc.platform.create_programmer()
        bits = builder.get_bitstream_filename(mode="sram" if args.load else "flash")
        if args.load:  prog.load_bitstream(bits)
        if args.flash: prog.flash(0, bits)


if __name__ == "__main__":
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions