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 <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2022-11-05 12:14:58 -04:00
parent 20dca056ad
commit f6f3f024e4
6 changed files with 392 additions and 25 deletions

166
rtl/phy_core.v Normal file
View File

@ -0,0 +1,166 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`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

View File

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

View File

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

188
tb/phy_core.py Normal file
View File

@ -0,0 +1,188 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
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

View File

@ -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 = []
async def recv():
for _ in ins:
await RisingEdge(scrambler.clk)
outs.append(scrambler.scrambled.value)
yield scrambler.scrambled.value
compare_lists(ins[idles-1:], list(descramble(outs)))
compare_lists(ins[idles-1:], await alist(descramble(recv())))

View File

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