From f6f3f024e4d0eb5ea317ee8a07ef0731f2ab49b9 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sat, 5 Nov 2022 12:14:58 -0400 Subject: [PATCH] Add phy_core This module integrates the PCS with the descrambler, implements the PMA (which is just the link monitor), and implements loopback and coltest functions. This is more of the PCS/PMA, but the descrambler is technically part of the PMD, so it's the "core" instead. We deviate from the standard in one important way: the link doesn't come up until the descambler is locked. I think this makes sense, since if the descrambler isn't locked, then the incoming data will be gibberish. I suspect this isn't part of the standard because the descrambler doesn't have a locked output in X3.263, so IEEE would have had to specify it. Loopback is actually implemented in the PMD, but it modifies the behavior in several places. It disables collisions (unless coltest is enabled). Additionally, we need to force the link up (to avoid the lengthy stabilization timer), but ensure it is down for at least once cycle (to ensure the descrambler desynchronizes). On the test side, we just go through the "happy path," as many of the edge conditions are tested for in the submodule tests. Several of those tests are modified so that their helper functions can be reused in this test. In particular, the rx path is now async so that we can feed it rx_data. Signed-off-by: Sean Anderson --- rtl/phy_core.v | 166 ++++++++++++++++++++++++++++++++++++++++ tb/nrzi_decode.py | 8 +- tb/pcs_tx.py | 36 +++++---- tb/phy_core.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++ tb/scramble.py | 15 ++-- tb/util.py | 4 + 6 files changed, 392 insertions(+), 25 deletions(-) create mode 100644 rtl/phy_core.v create mode 100644 tb/phy_core.py diff --git a/rtl/phy_core.v b/rtl/phy_core.v new file mode 100644 index 0000000..dd858ff --- /dev/null +++ b/rtl/phy_core.v @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" + +`timescale 1ns/1ps + +module phy_core ( + input clk, + + /* "PMD" */ + output tx_data, + input [1:0] rx_data, + input [1:0] rx_data_valid, + input signal_status, + + /* "MII" */ + input tx_ce, + input tx_en, + input [3:0] txd, + input tx_er, + + output rx_ce, + output rx_dv, + output [3:0] rxd, + output rx_er, + + output reg crs, + output reg col, + + /* Control */ + input loopback, + input coltest, + input link_monitor_test_mode, + input descrambler_test_mode, + output locked, + output reg link_status +); + + wire tx_bits, transmitting; + + pcs_tx pcs_tx ( + .clk(clk), + .ce(tx_ce), + .enable(tx_en), + .data(txd), + .err(tx_er), + .bits(tx_bits), + .link_status(link_status), + .tx(transmitting) + ); + + scramble scrambler ( + .clk(clk), + .unscrambled(tx_bits), + .scrambled(tx_data) + ); + + reg descrambler_enable, loopback_last; + initial loopback_last = 0; + wire [1:0] rx_bits, rx_bits_valid; + + /* Force desynchronization when entering/exiting loopback */ + always @(*) begin + if (loopback) + descrambler_enable = loopback_last; + else + descrambler_enable = signal_status && !loopback_last; + end + + always @(posedge clk) + loopback_last <= loopback; + + descramble descrambler ( + .clk(clk), + .signal_status(descrambler_enable), + .scrambled(rx_data), + .scrambled_valid(rx_data_valid), + .descrambled(rx_bits), + .descrambled_valid(rx_bits_valid), + .test_mode(descrambler_test_mode), + .locked(locked) + ); + + /* + * LFSR counter; see descramble.v for details on how these values were + * generated. + * + * 50000 cycles or 400 us at 125MHz + */ + localparam STABILIZE_VALUE = 17'h1ac86; + /* 16 cycles; there's no instability while testing */ + localparam TEST_STABILIZE_VALUE = 17'h11c71; + + reg link_status_next; + initial link_status = 0; + reg [16:0] stabilize_timer, stabilize_timer_next; + initial stabilize_timer = STABILIZE_VALUE; + + /* + * Link monitor process; this is the entirety of the (section 24.3) PMA + * + * Section 24.3.4.4 specifies that link_status is to be set to OK when + * stabilize_timer completes. However, I have also included whether + * the descrambler is locked. I think this matches the intent of the + * signal, which indicates whether "the receive channel is intact and + * enabled for reception." + */ + + always @(*) begin + link_status_next = 0; + stabilize_timer_next = stabilize_timer; + + if (signal_status) begin + if (&stabilize_timer) begin + link_status_next = locked; + end else begin + stabilize_timer_next[0] = stabilize_timer[16] ^ stabilize_timer[13]; + stabilize_timer_next[16:1] = stabilize_timer[15:0]; + end + end else if (link_monitor_test_mode) begin + stabilize_timer_next = TEST_STABILIZE_VALUE; + end else begin + stabilize_timer_next = STABILIZE_VALUE; + end + + if (loopback) + stabilize_timer_next = 17'h1ffff; + end + + always @(posedge clk) begin + stabilize_timer <= stabilize_timer_next; + link_status <= link_status_next; + end + + wire receiving; + + pcs_rx pcs_rx ( + .clk(clk), + .ce(rx_ce), + .valid(rx_dv), + .data(rxd), + .err(rx_er), + .bits(rx_bits), + .bits_valid(rx_bits_valid), + .link_status(link_status), + .rx(receiving) + ); + + /* + * NB: These signals are not required to be in any particular clock + * domain (not that it matters). + */ + always @(*) begin + crs = transmitting || receiving; + if (coltest) + col = transmitting; + else if (loopback) + col = 0; + else + col = transmitting && receiving; + end + +endmodule diff --git a/tb/nrzi_decode.py b/tb/nrzi_decode.py index fb06f7c..46a760b 100644 --- a/tb/nrzi_decode.py +++ b/tb/nrzi_decode.py @@ -8,11 +8,11 @@ from cocotb.clock import Clock from cocotb.regression import TestFactory from cocotb.triggers import FallingEdge, RisingEdge, Timer -from .util import compare_lists, timeout, send_recovered_bits, with_valids +from .util import alist, async_iter, compare_lists, timeout, send_recovered_bits, with_valids -def nrzi_decode(bits): +async def nrzi_decode(bits): last = 1 - for bit in bits: + async for bit in bits: yield bit ^ last last = bit @@ -40,6 +40,6 @@ async def test_rx(decoder, valids): outs.append(decoder.nrz[0].value) # Ignore the first bit, since it is influenced by the initial value - compare_lists(list(nrzi_decode(ins))[1:], outs[1:]) + compare_lists((await alist(nrzi_decode(async_iter(ins))))[1:], outs[1:]) with_valids(globals(), test_rx) diff --git a/tb/pcs_tx.py b/tb/pcs_tx.py index 550e02c..d2140a5 100644 --- a/tb/pcs_tx.py +++ b/tb/pcs_tx.py @@ -9,21 +9,29 @@ from cocotb.types import LogicArray from .pcs import Code, as_nibbles from .util import alist, ClockEnable, ReverseList -async def mii_send_packet(pcs, nibbles): - await FallingEdge(pcs.ce) - for nibble in nibbles: - pcs.enable.value = 1 - pcs.err.value = 0 - if nibble is None: - pcs.err.value = 1 - else: - pcs.data.value = nibble - await FallingEdge(pcs.ce) +async def mii_send_packet(pcs, nibbles, signals=None): + if signals is None: + signals = { + 'ce': pcs.ce, + 'enable': pcs.enable, + 'err': pcs.err, + 'data': pcs.data, + } - pcs.enable.value = 0 - pcs.err.value = 0 - pcs.data.value = LogicArray("XXXX") - await FallingEdge(pcs.ce) + await FallingEdge(signals['ce']) + for nibble in nibbles: + signals['enable'].value = 1 + signals['err'].value = 0 + if nibble is None: + signals['err'].value = 1 + else: + signals['data'].value = nibble + await FallingEdge(signals['ce']) + + signals['enable'].value = 0 + signals['err'].value = 0 + signals['data'].value = LogicArray("XXXX") + await FallingEdge(signals['ce']) class PCSError(Exception): pass diff --git a/tb/phy_core.py b/tb/phy_core.py new file mode 100644 index 0000000..b5431e5 --- /dev/null +++ b/tb/phy_core.py @@ -0,0 +1,188 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +import itertools +import random + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Event, FallingEdge, RisingEdge, Timer +from cocotb.types import LogicArray + +from .nrzi_encode import nrzi_encode +from .nrzi_decode import nrzi_decode +from .scramble import descramble +from .descramble import scramble +from .pcs_tx import as_nibbles, mii_send_packet, pcs_recv_packet +from .pcs_rx import frame, mii_recv_packet +from .util import alist, ClockEnable + +@cocotb.test(timeout_time=15, timeout_unit='us') +async def test_transfer(phy): + phy.coltest.value = 0 + phy.descrambler_test_mode.value = 0 + phy.tx_en.value = 0 + phy.rx_data_valid.value = 0 + phy.signal_status.value = 0 + phy.loopback.value = 0 + phy.link_monitor_test_mode.value = 1 + await cocotb.start(ClockEnable(phy.clk, phy.tx_ce, 5)) + await Timer(1) + phy.signal_status.value = 1 + await cocotb.start(Clock(phy.clk, 8, units='ns').start()) + await FallingEdge(phy.tx_ce) + + tx_data = list(as_nibbles([0x55, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef])) + rx_data = list(as_nibbles((0x55, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10))) + + async def send_rx_packets(): + def rx_bits(): + packet_bits = list(itertools.chain.from_iterable(frame(rx_data))) + + # First packet is OK, second is a collision + yield from itertools.repeat(1, 120) + yield from packet_bits + yield from itertools.repeat(1, 240) + yield from packet_bits + + while not phy.loopback.value: + yield 1 + + for bit in scramble(rx_bits()): + phy.rx_data.value = LogicArray((bit, 'X')) + phy.rx_data_valid.value = 1 + await FallingEdge(phy.clk) + + async def send_tx_packets(): + signals = { + 'ce': phy.tx_ce, + 'enable': phy.tx_en, + 'err': phy.tx_er, + 'data': phy.txd, + } + + # Send a packet, and then cause a collision + await ClockCycles(phy.clk, 240) + await mii_send_packet(phy, tx_data, signals) + await ClockCycles(phy.clk, 120) + await mii_send_packet(phy, tx_data, signals) + + while not phy.loopback.value: + await FallingEdge(phy.clk) + + # Loopback + await ClockCycles(phy.clk, 120) + await mii_send_packet(phy, tx_data, signals) + + # Collision test + await ClockCycles(phy.clk, 120) + phy.coltest.value = 1 + await mii_send_packet(phy, tx_data, signals) + + while phy.loopback.value: + await FallingEdge(phy.clk) + + await ClockCycles(phy.clk, 240) + await mii_send_packet(phy, tx_data, signals) + + async def loopback(): + while phy.loopback.value: + phy.rx_data.value = LogicArray((int(phy.tx_data.value), 'X')) + await FallingEdge(phy.clk) + + await cocotb.start(send_tx_packets()) + await cocotb.start(send_rx_packets()) + + rx_ready = Event() + tx_ready = Event() + + async def recv_rx_packets(): + async def packets(): + while True: + yield await alist(mii_recv_packet(phy, { + 'ce': phy.rx_ce, + 'err': phy.rx_er, + 'data': phy.rxd, + 'valid': phy.rx_dv, + })) + + packets = packets() + assert rx_data == await anext(packets) + assert rx_data == await anext(packets) + rx_ready.set() + assert tx_data == await anext(packets) + assert tx_data == await anext(packets) + rx_ready.set() + assert rx_data == await anext(packets) + rx_ready.set() + + async def recv_tx_packets(): + async def recv(): + while True: + await RisingEdge(phy.clk) + yield phy.tx_data.value + + async def packets(): + while True: + yield await alist(pcs_recv_packet(phy, descramble(recv()))) + + packets = packets() + assert tx_data == await anext(packets) + assert tx_data == await anext(packets) + tx_ready.set() + assert tx_data == await anext(packets) + assert tx_data == await anext(packets) + tx_ready.set() + assert tx_data == await anext(packets) + tx_ready.set() + + await cocotb.start(recv_rx_packets()) + await cocotb.start(recv_tx_packets()) + + crs = 0 + col = 0 + + async def count_crs(): + nonlocal crs + while True: + await RisingEdge(phy.crs) + crs += 1 + await FallingEdge(phy.crs) + + async def count_col(): + nonlocal col + while True: + await RisingEdge(phy.col) + col += 1 + await FallingEdge(phy.col) + + await cocotb.start(count_crs()) + await cocotb.start(count_col()) + + await rx_ready.wait() + await tx_ready.wait() + assert crs == 3 + assert col == 1 + rx_ready.clear() + tx_ready.clear() + + phy.loopback.value = 1 + await ClockCycles(phy.clk, 1) + await cocotb.start(loopback()) + + await rx_ready.wait() + await tx_ready.wait() + assert crs == 5 + assert col == 2 + rx_ready.clear() + tx_ready.clear() + + await FallingEdge(phy.clk) + phy.loopback.value = 0 + phy.coltest.value = 0 + await cocotb.start(send_rx_packets()) + + await rx_ready.wait() + await tx_ready.wait() + assert crs == 7 + assert col == 2 diff --git a/tb/scramble.py b/tb/scramble.py index 51a7b1e..5d2db29 100644 --- a/tb/scramble.py +++ b/tb/scramble.py @@ -12,10 +12,11 @@ from .util import alist, ReverseList, print_list_at, compare_lists threshold = 32 -def descramble(scrambled): +async def descramble(scrambled): + consecutive = 0 locked = False lfsr = ReverseList([0] * 11) - for s in scrambled: + async for s in scrambled: ldd = lfsr[8] ^ lfsr[10] if s ^ ldd: @@ -46,9 +47,9 @@ async def test_scramble(scrambler): scrambler.unscrambled.value = bit await cocotb.start(send()) - outs = [] - for _ in ins: - await RisingEdge(scrambler.clk) - outs.append(scrambler.scrambled.value) + async def recv(): + for _ in ins: + await RisingEdge(scrambler.clk) + yield scrambler.scrambled.value - compare_lists(ins[idles-1:], list(descramble(outs))) + compare_lists(ins[idles-1:], await alist(descramble(recv()))) diff --git a/tb/util.py b/tb/util.py index 59c8b57..331acac 100644 --- a/tb/util.py +++ b/tb/util.py @@ -12,6 +12,10 @@ from cocotb.types import LogicArray async def alist(xs): return [x async for x in xs] +async def async_iter(it): + for i in it: + yield i + # From https://stackoverflow.com/a/7864317/5086505 class classproperty(property): def __get__(self, cls, owner):