Skip to content

RGMII Issue with Colorlight i9 7.2 #736

@DatanoiseTV

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions