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):