Add 64-bit XGMII PHY implementation for 10G Ethernet
Adds support for 64-bit wide XGMII PHYs in LiteEth. A 64-bit wide XGMII data path is a common method to interconnect multi-gigabit Ethernet inside FPGAs. This module expects a 64-bit MAC data path, which is to be added later. It has been tested locally using a rewritten XGMII module for the LiteX simulator as well as on a KCU116 board. This work has been inspired by enjoy-digital/liteeth#21 but is entirely rewritten using Migen FSMs and with respect to IEEE802.3-2018. Thanks to Florent Kermarrec (@enjoy-digital) and Vamsi Vytla (@jersey99) for providing the base implementation. This implementation does not yet support proper 32-bit (DDR) XGMII PHYs, although support can be easily added by an additional module which performs the DDR encoding / decoding of the data respectively. Signed-off-by: Leon Schuermann <leon@is.currently.online>
This commit is contained in:
parent
c6c8be703b
commit
ea55332d26
|
@ -26,6 +26,7 @@ from liteeth.phy.mii import LiteEthPHYMII
|
||||||
from liteeth.phy.rmii import LiteEthPHYRMII
|
from liteeth.phy.rmii import LiteEthPHYRMII
|
||||||
from liteeth.phy.gmii import LiteEthPHYGMII
|
from liteeth.phy.gmii import LiteEthPHYGMII
|
||||||
from liteeth.phy.gmii_mii import LiteEthPHYGMIIMII
|
from liteeth.phy.gmii_mii import LiteEthPHYGMIIMII
|
||||||
|
from liteeth.phy.xgmii import LiteEthPHYXGMII
|
||||||
|
|
||||||
from liteeth.phy.s6rgmii import LiteEthPHYRGMII as LiteEthS6PHYRGMII
|
from liteeth.phy.s6rgmii import LiteEthPHYRGMII as LiteEthS6PHYRGMII
|
||||||
from liteeth.phy.s7rgmii import LiteEthPHYRGMII as LiteEthS7PHYRGMII
|
from liteeth.phy.s7rgmii import LiteEthPHYRGMII as LiteEthS7PHYRGMII
|
||||||
|
|
|
@ -0,0 +1,321 @@
|
||||||
|
#
|
||||||
|
# This file is part of LiteEth.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2021 Leon Schuermann <leon@is.currently.online>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
|
||||||
|
from migen import Module
|
||||||
|
from liteeth.common import *
|
||||||
|
|
||||||
|
from functools import reduce
|
||||||
|
from operator import or_
|
||||||
|
|
||||||
|
XGMII_IDLE = Constant(0x07, bits_sign=8)
|
||||||
|
XGMII_START = Constant(0xFB, bits_sign=8)
|
||||||
|
XGMII_END = Constant(0xFD, bits_sign=8)
|
||||||
|
|
||||||
|
class LiteEthPHYXGMIITX(Module):
|
||||||
|
def __init__(self, pads, dw):
|
||||||
|
# Enforce 64-bit data path
|
||||||
|
assert dw == 64
|
||||||
|
|
||||||
|
# Sink for data to transmit
|
||||||
|
self.sink = sink = stream.Endpoint(eth_phy_description(dw))
|
||||||
|
|
||||||
|
# Transmit FSM
|
||||||
|
self.submodules.fsm = fsm = FSM(reset_state="IDLE")
|
||||||
|
|
||||||
|
fsm.act("IDLE",
|
||||||
|
If(sink.valid,
|
||||||
|
# Currently idling, but a new frame is ready for
|
||||||
|
# transmission. Thus transmit the preamble, but replace the
|
||||||
|
# first byte with the XGMII start of frame control
|
||||||
|
# character. Accept more data.
|
||||||
|
pads.tx_ctl.eq(0x01),
|
||||||
|
pads.tx_data.eq(Cat(XGMII_START, sink.data[8:dw])),
|
||||||
|
NextValue(sink.ready, 1),
|
||||||
|
NextState("TRANSMIT"),
|
||||||
|
).Else(
|
||||||
|
# Idling, transmit XGMII IDLE control characters
|
||||||
|
# only. Accept more data.
|
||||||
|
pads.tx_ctl.eq(0xFF),
|
||||||
|
pads.tx_data.eq(Cat(*([XGMII_IDLE] * 8))),
|
||||||
|
NextValue(sink.ready, 1),
|
||||||
|
NextState("IDLE"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fsm.act("TRANSMIT",
|
||||||
|
# Check whether the data is still valid first or we are are not
|
||||||
|
# ready to accept a new transmission.
|
||||||
|
If(~sink.valid | ~sink.ready,
|
||||||
|
# Data isn't valid, or we aren't ready to accept a new
|
||||||
|
# transmission yet as another one has ended but the XGMII end of
|
||||||
|
# frame control character has not been transmitted. We must
|
||||||
|
# transmit the end of frame marker and return to
|
||||||
|
# afterwards. Immediately accept more data, given we have
|
||||||
|
# transmitted the end of frame control character.
|
||||||
|
pads.tx_ctl.eq(0xFF),
|
||||||
|
pads.tx_data.eq(Cat(XGMII_END, Replicate(XGMII_IDLE, 7))),
|
||||||
|
NextValue(sink.ready, 1),
|
||||||
|
NextState("IDLE"),
|
||||||
|
).Else(
|
||||||
|
# The data is valid. For each byte, determine whether it is
|
||||||
|
# valid or must be an XGMII idle or end of frame control
|
||||||
|
# character based on the value of last_be.
|
||||||
|
*[
|
||||||
|
If(~sink.last | (sink.last_be >= (1 << i)),
|
||||||
|
# Either not the last data word or last_be indicates
|
||||||
|
# this byte is still valid
|
||||||
|
pads.tx_ctl[i].eq(0),
|
||||||
|
pads.tx_data[8*i:8*(i+1)].eq(sink.data[8*i:8*(i+1)]),
|
||||||
|
).Elif((sink.last_be == (1 << (i - 1))) if i > 0 else 0,
|
||||||
|
# last_be indicates that this byte is the first one
|
||||||
|
# which is no longer valid, hence transmit the XGMII end
|
||||||
|
# of frame character
|
||||||
|
pads.tx_ctl[i].eq(1),
|
||||||
|
pads.tx_data[8*i:8*(i+1)].eq(XGMII_END),
|
||||||
|
).Else(
|
||||||
|
# We must've transmitted the XGMII end of frame control
|
||||||
|
# character, all other bytes must be XGMII idle control
|
||||||
|
# character
|
||||||
|
pads.tx_ctl[i].eq(1),
|
||||||
|
pads.tx_data[8*i:8*(i+1)].eq(XGMII_IDLE),
|
||||||
|
)
|
||||||
|
for i in range(8)
|
||||||
|
],
|
||||||
|
# If this was the last data word, we must determine whether we
|
||||||
|
# have transmitted the XGMII end of frame control character. The
|
||||||
|
# only way this cannot happen is if every byte in the data word
|
||||||
|
# was valid. If this is the case, we must send an additional
|
||||||
|
# XGMII bus word containing the XGMII end of frame and idle
|
||||||
|
# control characters. This happens if we remain in the TRANSMIT
|
||||||
|
# state.
|
||||||
|
If(~sink.last,
|
||||||
|
NextValue(sink.ready, 1),
|
||||||
|
NextState("TRANSMIT"),
|
||||||
|
).Elif(sink.last_be == (1 << 7),
|
||||||
|
# Last data word, but all bytes were valid.
|
||||||
|
NextValue(sink.ready, 0),
|
||||||
|
NextState("TRANSMIT"),
|
||||||
|
).Else(
|
||||||
|
NextValue(sink.ready, 1),
|
||||||
|
NextState("IDLE"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class LiteEthPHYXGMIIRXAligner(Module):
|
||||||
|
def __init__(self, unaligned_ctl, unaligned_data):
|
||||||
|
# Aligned ctl and data characters
|
||||||
|
self.aligned_ctl = Signal.like(unaligned_ctl)
|
||||||
|
self.aligned_data = Signal.like(unaligned_data)
|
||||||
|
|
||||||
|
# Buffer for low-bytes of the last XGMII bus word
|
||||||
|
low_ctl = Signal(len(unaligned_ctl) // 2)
|
||||||
|
low_data = Signal(len(unaligned_data) // 2)
|
||||||
|
|
||||||
|
|
||||||
|
# Alignment FSM
|
||||||
|
self.submodules.fsm = fsm = FSM(reset_state="NOSHIFT")
|
||||||
|
|
||||||
|
fsm.act("NOSHIFT",
|
||||||
|
If(unaligned_ctl[4] & (unaligned_data[4*8:5*8] == XGMII_START),
|
||||||
|
# Report this bus word as entirely idle. This should
|
||||||
|
# not abort any existing transaction because of the
|
||||||
|
# 5-byte interpacket gap.
|
||||||
|
self.aligned_ctl.eq(0xFF),
|
||||||
|
self.aligned_data.eq(Replicate(XGMII_IDLE, 8)),
|
||||||
|
NextValue(low_ctl, unaligned_ctl[4:8]),
|
||||||
|
NextValue(low_data, unaligned_data[4*8:8*8]),
|
||||||
|
NextState("SHIFT"),
|
||||||
|
).Else(
|
||||||
|
# Data is aligned on the first octet of the XGMII bus
|
||||||
|
# word.
|
||||||
|
self.aligned_ctl.eq(unaligned_ctl),
|
||||||
|
self.aligned_data.eq(unaligned_data),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fsm.act("SHIFT",
|
||||||
|
If(unaligned_ctl[0] & (unaligned_data[0*8:1*8] == XGMII_START),
|
||||||
|
# Discard the previously recorded low bits,
|
||||||
|
# immediately transmit the full bus word.
|
||||||
|
self.aligned_ctl.eq(unaligned_ctl),
|
||||||
|
self.aligned_data.eq(unaligned_data),
|
||||||
|
NextState("NOSHIFT"),
|
||||||
|
).Else(
|
||||||
|
# Data is aligned on the fifth octet of the XGMII bus
|
||||||
|
# word. Store the low 4 octects and output the
|
||||||
|
# previous ones.
|
||||||
|
self.aligned_ctl.eq(Cat(low_ctl, unaligned_ctl[0:4])),
|
||||||
|
self.aligned_data.eq(Cat(low_data, unaligned_data[0*8:4*8])),
|
||||||
|
NextValue(low_ctl, unaligned_ctl[4:8]),
|
||||||
|
NextValue(low_data, unaligned_data[4*8:8*8]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class LiteEthPHYXGMIIRX(Module):
|
||||||
|
def __init__(self, pads, dw):
|
||||||
|
# Enforce 64-bit data path
|
||||||
|
assert dw == 64
|
||||||
|
|
||||||
|
# Source we need to feed data into. We assume the sink is always ready,
|
||||||
|
# given we can't really pause an incoming XGMII transfer.
|
||||||
|
self.source = source = stream.Endpoint(eth_phy_description(dw))
|
||||||
|
|
||||||
|
# As per IEEE802.3-2018, section eight, 126.3.2.2.10 Start, the XGMII
|
||||||
|
# start control character is only valid on the first octet of a 32-bit
|
||||||
|
# (DDR) XGMII bus word. This means that on a 64-bit XGMII bus word, a
|
||||||
|
# new XGMII transmission may start on the first or firth
|
||||||
|
# octect. However, this would require us to keep track of the current
|
||||||
|
# offset and overall make this implementation more complex. Instead we
|
||||||
|
# want all transmissions to be aligned on the first octect of a 64-bit
|
||||||
|
# XGMII bus word, which we can do without packet loss given 10G Ethernet
|
||||||
|
# mandates a 5-byte interpacket gap (which may be less at the receiver,
|
||||||
|
# but this assumption seems to work for now).
|
||||||
|
self.submodules.aligner = LiteEthPHYXGMIIRXAligner(pads.rx_ctl, pads.rx_data)
|
||||||
|
|
||||||
|
# We need to have a lookahead and buffer the XGMII bus to properly
|
||||||
|
# determine whether we are processing the last bus word in some
|
||||||
|
# cases. This means delaying the incoming data by one cycle.
|
||||||
|
xgmii_bus_layout = [ ("ctl", 8), ("data", 64) ]
|
||||||
|
xgmii_bus_next = Record(xgmii_bus_layout)
|
||||||
|
self.comb += [
|
||||||
|
xgmii_bus_next.ctl.eq(self.aligner.aligned_ctl),
|
||||||
|
xgmii_bus_next.data.eq(self.aligner.aligned_data),
|
||||||
|
]
|
||||||
|
xgmii_bus = Record(xgmii_bus_layout)
|
||||||
|
self.sync += [
|
||||||
|
xgmii_bus.ctl.eq(xgmii_bus_next.ctl),
|
||||||
|
xgmii_bus.data.eq(xgmii_bus_next.data),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Scan over the entire XGMII bus word and search for an XGMII_END
|
||||||
|
# control character. If found, the octet before that must've been the
|
||||||
|
# last valid byte.
|
||||||
|
encoded_last_be = Signal(8)
|
||||||
|
self.comb += [
|
||||||
|
reduce(lambda a, b: a.Else(b), [
|
||||||
|
If((xgmii_bus.ctl[i] == 1) & \
|
||||||
|
(xgmii_bus.data[i*8:(i+1)*8] == XGMII_END),
|
||||||
|
encoded_last_be.eq((1 << i - 1) if i > 0 else 0))
|
||||||
|
for i in range(8)
|
||||||
|
]).Else(encoded_last_be.eq(1 << 7)),
|
||||||
|
]
|
||||||
|
|
||||||
|
# If either the current XGMII bus word indicates an end of a XGMII bus
|
||||||
|
# transfer (i.e. the encoded last_be is not 1 << 7, so the XGMII bus
|
||||||
|
# word is only partially valid) OR the next bus word immediately
|
||||||
|
# _starts_ with an XGMII end control character, the current bus data
|
||||||
|
# must be last. Nonetheless, mask last by valid to avoid triggering
|
||||||
|
# source.last on an empty XGMII bus word.
|
||||||
|
xgmii_bus_next_immediate_end = Signal()
|
||||||
|
self.comb += [
|
||||||
|
xgmii_bus_next_immediate_end.eq(
|
||||||
|
xgmii_bus_next.ctl[0] & (xgmii_bus_next.data[0:8] == XGMII_END)
|
||||||
|
),
|
||||||
|
source.last.eq(
|
||||||
|
source.valid & (
|
||||||
|
(encoded_last_be != (1 << 7)) | xgmii_bus_next_immediate_end
|
||||||
|
),
|
||||||
|
),
|
||||||
|
If(source.last,
|
||||||
|
source.last_be.eq(encoded_last_be),
|
||||||
|
).Else(
|
||||||
|
source.last_be.eq(0),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Receive FSM
|
||||||
|
self.submodules.fsm = fsm = FSM(reset_state="IDLE")
|
||||||
|
|
||||||
|
fsm.act("IDLE",
|
||||||
|
# The Ethernet preamble and start of frame character must follow
|
||||||
|
# after the XGMII start control character, so we can simply match on
|
||||||
|
# the entire bus word. The aligner makes sure the XGMII start
|
||||||
|
# control character is always located on the first XGMII bus
|
||||||
|
# word. This also spares us of looking for XGMII end of frame
|
||||||
|
# characters, given we would need to immediately dismiss the
|
||||||
|
# transmission if we find one of those.
|
||||||
|
If((xgmii_bus.ctl == 0x01) & (xgmii_bus.data == Cat(
|
||||||
|
XGMII_START,
|
||||||
|
Constant(eth_preamble, bits_sign=64)[8:64],
|
||||||
|
)),
|
||||||
|
source.valid.eq(1),
|
||||||
|
source.first.eq(1),
|
||||||
|
source.data.eq(Constant(eth_preamble, bits_sign=64)),
|
||||||
|
source.error.eq(0),
|
||||||
|
If(source.last,
|
||||||
|
# It may happen that the lookahead concluded we're
|
||||||
|
# immediately ending the XGMII bus transfer. In this case,
|
||||||
|
# remain in IDLE
|
||||||
|
NextState("IDLE"),
|
||||||
|
).Else(
|
||||||
|
NextState("RECEIVE"),
|
||||||
|
),
|
||||||
|
).Else(
|
||||||
|
# In any other case, keep the RX FSM idle. While there could be
|
||||||
|
# a bus error condition, without a properly initiated Ethernet
|
||||||
|
# transmission we couldn't meaningfully handle or report it
|
||||||
|
# anyways.
|
||||||
|
source.valid.eq(0),
|
||||||
|
source.first.eq(0),
|
||||||
|
source.data.eq(0),
|
||||||
|
source.error.eq(0),
|
||||||
|
NextState("IDLE"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fsm.act("RECEIVE",
|
||||||
|
# Receive data and pass through to the source. Switch back to IDLE
|
||||||
|
# if we detect an XGMII end control character in this or at the
|
||||||
|
# start of the next XGMII bus word.
|
||||||
|
source.valid.eq(1),
|
||||||
|
source.first.eq(0),
|
||||||
|
source.data.eq(xgmii_bus.data),
|
||||||
|
source.error.eq(0),
|
||||||
|
If(source.last,
|
||||||
|
NextState("IDLE"),
|
||||||
|
).Else(
|
||||||
|
NextState("RECEIVE"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LiteEthPHYXGMIICRG(Module, AutoCSR):
|
||||||
|
def __init__(self, clock_pads, model=False):
|
||||||
|
self._reset = CSRStorage()
|
||||||
|
self.clock_domains.cd_eth_rx = ClockDomain()
|
||||||
|
self.clock_domains.cd_eth_tx = ClockDomain()
|
||||||
|
if model:
|
||||||
|
self.comb += [
|
||||||
|
self.cd_eth_rx.clk.eq(ClockSignal()),
|
||||||
|
self.cd_eth_tx.clk.eq(ClockSignal())
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.comb += [
|
||||||
|
self.cd_eth_rx.clk.eq(clock_pads.rx),
|
||||||
|
self.cd_eth_tx.clk.eq(clock_pads.tx)
|
||||||
|
]
|
||||||
|
|
||||||
|
class LiteEthPHYXGMII(Module, AutoCSR):
|
||||||
|
dw = 8
|
||||||
|
tx_clk_freq = 156.25e6
|
||||||
|
rx_clk_freq = 156.25e6
|
||||||
|
def __init__(self,
|
||||||
|
clock_pads,
|
||||||
|
pads,
|
||||||
|
model=False,
|
||||||
|
dw=64,
|
||||||
|
with_hw_init_reset=True):
|
||||||
|
self.dw = dw
|
||||||
|
self.cd_eth_tx, self.cd_eth_rx = "eth_tx", "eth_rx"
|
||||||
|
self.submodules.crg = LiteEthPHYXGMIICRG(clock_pads, model)
|
||||||
|
self.submodules.tx = ClockDomainsRenamer(self.cd_eth_tx)(
|
||||||
|
LiteEthPHYXGMIITX(pads, self.dw))
|
||||||
|
self.submodules.rx = ClockDomainsRenamer(self.cd_eth_rx)(
|
||||||
|
LiteEthPHYXGMIIRX(pads, self.dw))
|
||||||
|
self.sink, self.source = self.tx.sink, self.rx.source
|
Loading…
Reference in New Issue