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()
The problem:
Tried with original Break Out Board and with Hans Baier Colorlight x QMTech board. @hansfbaier
Any hints very appreciated.