Add MII elastic buffer

In order to move data between MIIs without implementing a MAC, we need
some kind of elastic buffer to bring the data into the transmit "clock
(enable) domain." Implement one. It's based on a classic shift-register
FIFO, with the main difference being the MII interfaces and the
elasticity (achieved by delaying asserting RX_DV until we reach the
WATERMARK). We use a register-based buffer because we only need to deal
with an under-/over-flow of 5 or so clocks for a 2000-byte packet. The
per-stage resource increase works out to 6 FFs and 1 LUT, which is
pretty much optimal.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2023-02-12 19:53:44 -05:00
parent 5cf02e9490
commit 16b639aad2
3 changed files with 249 additions and 0 deletions

View File

@ -116,6 +116,7 @@ MODULES += descramble
MODULES += mdio
MODULES += mdio_io
MODULES += mdio_regs
MODULES += mii_elastic_buffer
MODULES += mii_io_rx
MODULES += mii_io_tx
MODULES += nrzi_decode

145
rtl/mii_elastic_buffer.v Normal file
View File

@ -0,0 +1,145 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2023 Sean Anderson <seanga2@gmail.com>
*
* This is a classic shift-register FIFO (a la XAPP 005.002) adapted for MII
* semantics. The semantics of err/valid are slightly different from dv/er
* when we have err and not valid. We remove this scenario from the input
* (since it represents false carrier/LPI and we don't care about those when
* repeating) and reuse this scenario as filler to indicate underflow.
*
* There is a delay of BUF_SIZE - 1 clocks between when data enters via txd
* and when it is offered on rxd.
*
* Note that because this module reacts to rx_ce/rx_dv from the previous clock,
* the maximum duty cycle of rx_ce is 50%. Otherwise, it is possible for the
* same data to be offered more than once.
*/
`include "common.vh"
module mii_elastic_buffer (
input clk,
input tx_ce,
input tx_en,
input tx_er,
input [3:0] txd,
input rx_ce,
output reg rx_dv,
output reg rx_er,
output reg [3:0] rxd,
output reg overflow,
output reg underflow
);
parameter BUF_SIZE = 5;
/*
* The amount of data in the buffer before we assert RX_DV. The
* default slightly favors overflow (tx_ce faster than rx_ce) over
* underflow. This is because we can generally get more data through
* with overflow, since the slower rx_ce allows the data in the buffer
* to propegate more.
*/
parameter WATERMARK = (BUF_SIZE + 1) / 2;
integer i;
reg [BUF_SIZE - 1:0] valid, valid_next, err, err_next;
reg [3:0] data [BUF_SIZE - 1:0], data_next [BUF_SIZE - 1:0];
reg shift, overflow_next, underflow_next;
reg in, in_next, out, out_next, rx_ce_last, rx_dv_last;
reg [BUF_SIZE - 1:0] debug;
initial begin
valid = 0;
err = 0;
overflow = 0;
underflow = 0;
in = 0;
out = 0;
rx_dv_last = 0;
rx_ce_last = 0;
end
always @(*) begin
if (out)
rx_dv = valid[BUF_SIZE - 1];
else
rx_dv = &valid[BUF_SIZE - 1:BUF_SIZE - WATERMARK - 1];
rx_er = err[BUF_SIZE - 1];
underflow_next = 0;
if (err[BUF_SIZE - 1] && !valid[BUF_SIZE - 1]) begin
rx_dv = 1;
underflow_next = rx_ce;
end
rxd = data[BUF_SIZE - 1];
out_next = rx_ce ? rx_dv : out;
valid_next = valid;
err_next = err;
shift = rx_ce_last && rx_dv_last;
debug[BUF_SIZE - 1] = shift;
for (i = BUF_SIZE - 1; i > 0; i = i - 1) begin
data_next[i] = data[i];
if (shift || !valid[i]) begin
valid_next[i] = valid[i - 1];
err_next[i] = err[i - 1];
data_next[i] = data[i - 1];
shift = 1;
end
debug[i - 1] = shift;
end
data_next[0] = data[0];
if (shift) begin
valid_next[0] = 0;
err_next[0] = in;
data_next[0] = 4'hX;
end
overflow_next = 0;
in_next = in;
if (tx_ce) begin
if (tx_en) begin
valid_next[0] = 1;
err_next[0] = tx_er;
if (valid[0] && !shift) begin
overflow_next = 1;
err_next[0] = 1;
end
in_next = 1;
end else begin
valid_next[0] = 0;
err_next[0] = 0;
in_next = 0;
end
data_next[0] = txd;
end
end
always @(posedge clk) begin
valid <= valid_next;
err <= err_next;
for (i = 0; i < BUF_SIZE; i = i + 1)
data[i] <= data_next[i];
overflow <= overflow_next;
underflow <= underflow_next;
in <= in_next;
out <= out_next;
rx_ce_last <= rx_ce;
rx_dv_last <= rx_dv;
end
`ifndef SYNTHESIS
/* This is the only way to look into a buffer... */
genvar j;
generate for (j = 0; j < BUF_SIZE; j = j + 1) begin
wire tmpv = valid[j];
wire tmpe = err[j];
wire [3:0] tmpd = data[j];
end endgenerate
`endif
endmodule

103
tb/mii_elastic_buffer.py Normal file
View File

@ -0,0 +1,103 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2023 Sean Anderson <seanga2@gmail.com>
import cocotb
from cocotb.binary import BinaryValue
from cocotb.clock import Clock
from cocotb.regression import TestFactory
from cocotb.triggers import ClockCycles, FallingEdge, RisingEdge, Timer
from .pcs_rx import mii_recv_packet
from .pcs_tx import mii_send_packet
from .util import alist, ClockEnable, lookahead, timeout
@cocotb.test(timeout_time=50, timeout_unit='us')
async def test_elastic(buf):
buf.clk.value = BinaryValue('Z')
buf.tx_ce.value = 0
buf.tx_en.value = 0
buf.rx_ce.value = 0
await Timer(1)
await cocotb.start(Clock(buf.clk, 8, units='ns').start())
await FallingEdge(buf.clk)
await cocotb.start(ClockEnable(buf.clk, buf.tx_ce, 5))
await FallingEdge(buf.clk)
rx_ce = await cocotb.start(ClockEnable(buf.clk, buf.rx_ce, 5))
underflows = 0
overflows = 0
async def count_excursions():
nonlocal underflows, overflows
while True:
await RisingEdge(buf.clk)
underflows += buf.underflow.value
overflows += buf.overflow.value
await cocotb.start(count_excursions())
in_signals = {
'ce': buf.tx_ce,
'enable': buf.tx_en,
'err': buf.tx_er,
'data': buf.txd,
}
out_signals = {
'ce': buf.rx_ce,
'err': buf.rx_er,
'data': buf.rxd,
'valid': buf.rx_dv,
}
for packet in (list(range(10)), [0, 1, 2, None, 4, 5]):
await cocotb.start(mii_send_packet(buf, packet, in_signals))
assert packet == await alist(mii_recv_packet(buf, out_signals))
packet = list(range(10))
for ratio in (2, 12):
rx_ce.kill()
while not buf.tx_ce.value:
await RisingEdge(buf.clk)
rx_ce = await cocotb.start(ClockEnable(buf.clk, buf.rx_ce, ratio))
underflows = 0
overflows = 0
await cocotb.start(mii_send_packet(buf, packet, in_signals))
outs = await alist(mii_recv_packet(buf, out_signals))
if ratio > 5:
assert overflows
else:
assert underflows
last = None
for nibble in outs:
if nibble is not None:
try:
int(nibble)
except:
raise
assert nibble != last
last = nibble
packet = list(range(5))
for ratio in (4, 6):
# Wait for a CE before shutting it off; we need to output at least one idle
while not buf.rx_ce.value:
await FallingEdge(buf.clk)
await FallingEdge(buf.clk)
rx_ce.kill()
underflows = 0
overflows = 0
await cocotb.start(mii_send_packet(buf, packet, in_signals))
# Set up a worst-case scenario
while not buf.rx_dv.value:
await FallingEdge(buf.clk)
if ratio == 6:
await ClockCycles(buf.clk, 5, False)
rx_ce = await cocotb.start(ClockEnable(buf.clk, buf.rx_ce, ratio))
# And make sure everything works out
assert packet == await alist(mii_recv_packet(buf, out_signals))