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:
parent
20dca056ad
commit
f6f3f024e4
|
@ -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
|
|
@ -8,11 +8,11 @@ from cocotb.clock import Clock
|
||||||
from cocotb.regression import TestFactory
|
from cocotb.regression import TestFactory
|
||||||
from cocotb.triggers import FallingEdge, RisingEdge, Timer
|
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
|
last = 1
|
||||||
for bit in bits:
|
async for bit in bits:
|
||||||
yield bit ^ last
|
yield bit ^ last
|
||||||
last = bit
|
last = bit
|
||||||
|
|
||||||
|
@ -40,6 +40,6 @@ async def test_rx(decoder, valids):
|
||||||
outs.append(decoder.nrz[0].value)
|
outs.append(decoder.nrz[0].value)
|
||||||
|
|
||||||
# Ignore the first bit, since it is influenced by the initial 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)
|
with_valids(globals(), test_rx)
|
||||||
|
|
36
tb/pcs_tx.py
36
tb/pcs_tx.py
|
@ -9,21 +9,29 @@ from cocotb.types import LogicArray
|
||||||
from .pcs import Code, as_nibbles
|
from .pcs import Code, as_nibbles
|
||||||
from .util import alist, ClockEnable, ReverseList
|
from .util import alist, ClockEnable, ReverseList
|
||||||
|
|
||||||
async def mii_send_packet(pcs, nibbles):
|
async def mii_send_packet(pcs, nibbles, signals=None):
|
||||||
await FallingEdge(pcs.ce)
|
if signals is None:
|
||||||
for nibble in nibbles:
|
signals = {
|
||||||
pcs.enable.value = 1
|
'ce': pcs.ce,
|
||||||
pcs.err.value = 0
|
'enable': pcs.enable,
|
||||||
if nibble is None:
|
'err': pcs.err,
|
||||||
pcs.err.value = 1
|
'data': pcs.data,
|
||||||
else:
|
}
|
||||||
pcs.data.value = nibble
|
|
||||||
await FallingEdge(pcs.ce)
|
|
||||||
|
|
||||||
pcs.enable.value = 0
|
await FallingEdge(signals['ce'])
|
||||||
pcs.err.value = 0
|
for nibble in nibbles:
|
||||||
pcs.data.value = LogicArray("XXXX")
|
signals['enable'].value = 1
|
||||||
await FallingEdge(pcs.ce)
|
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):
|
class PCSError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -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
|
|
@ -12,10 +12,11 @@ from .util import alist, ReverseList, print_list_at, compare_lists
|
||||||
|
|
||||||
threshold = 32
|
threshold = 32
|
||||||
|
|
||||||
def descramble(scrambled):
|
async def descramble(scrambled):
|
||||||
|
consecutive = 0
|
||||||
locked = False
|
locked = False
|
||||||
lfsr = ReverseList([0] * 11)
|
lfsr = ReverseList([0] * 11)
|
||||||
for s in scrambled:
|
async for s in scrambled:
|
||||||
ldd = lfsr[8] ^ lfsr[10]
|
ldd = lfsr[8] ^ lfsr[10]
|
||||||
|
|
||||||
if s ^ ldd:
|
if s ^ ldd:
|
||||||
|
@ -46,9 +47,9 @@ async def test_scramble(scrambler):
|
||||||
scrambler.unscrambled.value = bit
|
scrambler.unscrambled.value = bit
|
||||||
await cocotb.start(send())
|
await cocotb.start(send())
|
||||||
|
|
||||||
outs = []
|
async def recv():
|
||||||
for _ in ins:
|
for _ in ins:
|
||||||
await RisingEdge(scrambler.clk)
|
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())))
|
||||||
|
|
|
@ -12,6 +12,10 @@ from cocotb.types import LogicArray
|
||||||
async def alist(xs):
|
async def alist(xs):
|
||||||
return [x async for x in 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
|
# From https://stackoverflow.com/a/7864317/5086505
|
||||||
class classproperty(property):
|
class classproperty(property):
|
||||||
def __get__(self, cls, owner):
|
def __get__(self, cls, owner):
|
||||||
|
|
Loading…
Reference in New Issue