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:
parent
5cf02e9490
commit
16b639aad2
1
Makefile
1
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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))
|
Loading…
Reference in New Issue