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:
Leon Schuermann 2021-08-14 16:16:34 +02:00
parent c6c8be703b
commit ea55332d26
2 changed files with 322 additions and 0 deletions

View File

@ -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

321
liteeth/phy/xgmii.py Normal file
View File

@ -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