Add AXI stream replay buffer
This implements an AXI stream buffer which allows replaying of the first portion of each packet. The intent is to simplify the implementation of CSMA/CD. This requires keeping 56 bytes of data to "replay" (slot time minus the preamble). After these bytes are transmitted, we can only get late collisions. We always read from the buffer, as this simplifies the implementation compared to some kind of hybrid fifo/skid buffer approach. The primary design problem faced is in determining when it's OK to overwrite the first byte in the packet. A naïve approach might be to allow overwriting whenever the slave reads the last byte. However, in the case of a 54-byte packet, we will still need to allow replaying at this point (in case there is a collision on the last byte). We can't just wait for m_axis_ready to go high, because that would violate the AXI stream protocol. To solve this, the slave must assert the done signal when it is finished with the packet. Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
parent
be2bded61e
commit
52325f241b
2
Makefile
2
Makefile
|
@ -91,7 +91,7 @@ endef
|
||||||
$(run-vvp)
|
$(run-vvp)
|
||||||
|
|
||||||
MODULES := pcs_rx pcs_tx pmd_dp83223_rx nrzi_encode nrzi_decode scramble descramble mdio mdio_io
|
MODULES := pcs_rx pcs_tx pmd_dp83223_rx nrzi_encode nrzi_decode scramble descramble mdio mdio_io
|
||||||
MODULES += mii_io_rx mii_io_tx mdio_regs
|
MODULES += mii_io_rx mii_io_tx mdio_regs phy_core axis_replay_buffer
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .synth.fst,$(MODULES))
|
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .synth.fst,$(MODULES))
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-Only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||||
|
*
|
||||||
|
* This module implements a "replay buffer" for an AXI stream, allowing the
|
||||||
|
* first BUF_SIZE cycles of a packet to be replayed. This may be done by
|
||||||
|
* asserting replay while replayable is true.
|
||||||
|
*
|
||||||
|
* replayable will remain true until BUF_SIZE + 1 handshakes have occured
|
||||||
|
* without a replay. In particular, it is possible to restart a packet
|
||||||
|
* even after a handshake with m_axis_last set. To support this late replay
|
||||||
|
* feature, done must be asserted when the consumer is does not wish to
|
||||||
|
* perform any more replays.
|
||||||
|
*
|
||||||
|
* In general, this buffer will add two cycles of latency. Additionally, there
|
||||||
|
* will may some latency when replayable goes low. This is because the slave
|
||||||
|
* interface stalls to avoid overwriting the first part of the packet. However,
|
||||||
|
* it will still read ahead to the physical end of the buffer. This will result
|
||||||
|
* in no stall as long as BUF_SIZE is at least three less than a power of two.
|
||||||
|
*
|
||||||
|
* Only axis_data is provided. For user, keep, etc. concatenate them into
|
||||||
|
* axis_data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
`include "common.vh"
|
||||||
|
|
||||||
|
module axis_replay_buffer (
|
||||||
|
input clk,
|
||||||
|
|
||||||
|
/* AXI Stream slave */
|
||||||
|
input [DATA_WIDTH - 1:0] s_axis_data,
|
||||||
|
input s_axis_valid,
|
||||||
|
output reg s_axis_ready,
|
||||||
|
input s_axis_last,
|
||||||
|
|
||||||
|
/* AXI Stream master */
|
||||||
|
output reg [DATA_WIDTH - 1:0] m_axis_data,
|
||||||
|
output reg m_axis_valid,
|
||||||
|
input m_axis_ready,
|
||||||
|
output reg m_axis_last,
|
||||||
|
|
||||||
|
/* Control */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Replay the packet. May be asserted any time replayable is high,
|
||||||
|
* including after BUF_SIZE handshakes have occured and after
|
||||||
|
* m_axis_last is high. Must not be asserted when replayable is low.
|
||||||
|
*/
|
||||||
|
input replay,
|
||||||
|
/*
|
||||||
|
* Force replayable low. This must be asserted for packets <= BUF_SIZE,
|
||||||
|
* since they may still be replayed even after the end of the packet.
|
||||||
|
*/
|
||||||
|
input done,
|
||||||
|
/*
|
||||||
|
* High when replay may be asserted.
|
||||||
|
*/
|
||||||
|
output reg replayable
|
||||||
|
);
|
||||||
|
|
||||||
|
parameter DATA_WIDTH = 9;
|
||||||
|
parameter BUF_SIZE = 54;
|
||||||
|
localparam BUF_WIDTH = $clog2(BUF_SIZE + 1);
|
||||||
|
|
||||||
|
reg [DATA_WIDTH - 1:0] s_axis_data_last;
|
||||||
|
reg s_axis_valid_last, s_axis_last_last, s_axis_ready_next;
|
||||||
|
reg m_axis_valid_next, m_axis_last_next;
|
||||||
|
reg sent_last, sent_last_next;
|
||||||
|
reg [DATA_WIDTH - 1:0] buffer [(2 ** BUF_WIDTH) - 1:0];
|
||||||
|
reg [BUF_WIDTH:0] m_ptr, m_ptr_next, s_ptr, s_ptr_next;
|
||||||
|
reg [BUF_WIDTH - 1:0] last_ptr, last_ptr_next;
|
||||||
|
reg [DATA_WIDTH - 1:0] s_data, m_data;
|
||||||
|
reg last, last_next;
|
||||||
|
reg full, empty, replayable_next, we, re;
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
m_ptr = 0;
|
||||||
|
s_ptr = 0;
|
||||||
|
last = 0;
|
||||||
|
replayable = 1;
|
||||||
|
s_axis_valid_last = 0;
|
||||||
|
s_axis_last_last = 0;
|
||||||
|
s_axis_ready = 1;
|
||||||
|
m_axis_valid = 0;
|
||||||
|
m_axis_last = 0;
|
||||||
|
sent_last = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
always @(*) begin
|
||||||
|
empty = s_ptr == m_ptr;
|
||||||
|
full = s_ptr == { ~m_ptr[BUF_WIDTH], m_ptr[BUF_WIDTH - 1:0] };
|
||||||
|
|
||||||
|
we = 0;
|
||||||
|
s_ptr_next = s_ptr;
|
||||||
|
last_next = last;
|
||||||
|
last_ptr_next = last_ptr;
|
||||||
|
if (s_axis_valid_last && s_axis_ready) begin
|
||||||
|
we = 1;
|
||||||
|
s_ptr_next = s_ptr + 1;
|
||||||
|
if (s_axis_last_last) begin
|
||||||
|
last_next = 1;
|
||||||
|
last_ptr_next = s_ptr;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if (replayable)
|
||||||
|
s_axis_ready_next = &s_ptr[BUF_WIDTH - 1:0] == s_ptr[BUF_WIDTH];
|
||||||
|
else
|
||||||
|
s_axis_ready_next = !full;
|
||||||
|
|
||||||
|
if (last_next)
|
||||||
|
s_axis_ready_next = 0;
|
||||||
|
|
||||||
|
/* read the next datum (if it's available)... */
|
||||||
|
m_axis_valid_next = !empty;
|
||||||
|
m_axis_last_next = last && m_ptr[BUF_WIDTH - 1:0] == last_ptr;
|
||||||
|
re = !empty;
|
||||||
|
m_ptr_next = m_ptr + !empty;
|
||||||
|
/* ...except if we need to stall */
|
||||||
|
if (m_axis_valid && !m_axis_ready) begin
|
||||||
|
m_axis_valid_next = m_axis_valid;
|
||||||
|
m_axis_last_next = m_axis_last;
|
||||||
|
re = 0;
|
||||||
|
m_ptr_next = m_ptr;
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
replayable_next = replayable;
|
||||||
|
sent_last_next = sent_last;
|
||||||
|
if (m_axis_valid && m_axis_ready) begin
|
||||||
|
replayable_next = replayable && (replay || m_ptr != BUF_SIZE + 1);
|
||||||
|
sent_last_next = sent_last || m_axis_last;
|
||||||
|
end
|
||||||
|
|
||||||
|
if (done)
|
||||||
|
replayable_next = 0;
|
||||||
|
|
||||||
|
if (sent_last && !replayable) begin
|
||||||
|
m_ptr_next = 0;
|
||||||
|
s_ptr_next = 0;
|
||||||
|
last_next = 0;
|
||||||
|
replayable_next = 1;
|
||||||
|
sent_last_next = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
if (replay) begin
|
||||||
|
m_ptr_next = 0;
|
||||||
|
sent_last_next = 0;
|
||||||
|
m_axis_valid_next = 0;
|
||||||
|
m_axis_last_next = 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (we)
|
||||||
|
buffer[s_ptr[BUF_WIDTH - 1:0]] <= { s_axis_data_last };
|
||||||
|
if (re)
|
||||||
|
{ m_axis_data } <= buffer[m_ptr[BUF_WIDTH - 1:0]];
|
||||||
|
|
||||||
|
s_axis_data_last <= s_axis_data;
|
||||||
|
s_axis_valid_last <= s_axis_valid;
|
||||||
|
s_axis_last_last <= s_axis_last;
|
||||||
|
s_axis_ready <= s_axis_ready_next;
|
||||||
|
m_axis_last <= m_axis_last_next;
|
||||||
|
m_axis_valid <= m_axis_valid_next;
|
||||||
|
sent_last <= sent_last_next;
|
||||||
|
m_ptr <= m_ptr_next;
|
||||||
|
s_ptr <= s_ptr_next;
|
||||||
|
last <= last_next;
|
||||||
|
last_ptr <= last_ptr_next;
|
||||||
|
replayable <= replayable_next;
|
||||||
|
end
|
||||||
|
|
||||||
|
`ifndef SYNTHESIS
|
||||||
|
/* This is the only way to look into a buffer... */
|
||||||
|
genvar i;
|
||||||
|
generate for (i = 0; i < 2 ** BUF_WIDTH; i = i + 1)
|
||||||
|
wire [DATA_WIDTH - 1:0] tmp = buffer[i];
|
||||||
|
endgenerate
|
||||||
|
`endif
|
||||||
|
|
||||||
|
endmodule
|
|
@ -0,0 +1,86 @@
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-Only
|
||||||
|
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||||
|
|
||||||
|
import cocotb
|
||||||
|
from cocotb.clock import Clock
|
||||||
|
from cocotb.regression import TestFactory
|
||||||
|
from cocotb.triggers import ClockCycles, FallingEdge, RisingEdge, Timer
|
||||||
|
|
||||||
|
from .util import ClockEnable, lookahead, timeout
|
||||||
|
|
||||||
|
BUF_SIZE = 54
|
||||||
|
|
||||||
|
@timeout(15, 'us')
|
||||||
|
async def test_replay(buf, in_ratio, out_ratio):
|
||||||
|
buf.s_axis_valid.value = 0
|
||||||
|
buf.s_axis_last.value = 0
|
||||||
|
buf.m_axis_ready.value = 1
|
||||||
|
buf.replay.value = 0
|
||||||
|
buf.done.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.m_axis_ready, out_ratio))
|
||||||
|
|
||||||
|
# A packet equal to BUF_SIZE, one around 2**BUF_WIDTH, and one around
|
||||||
|
# 2**(BUF_WIDTH + 1) (plus some extra). This should capture most of the fun
|
||||||
|
# conditions. We start at different data values to make sure we aren't
|
||||||
|
# reusing anything from the last test.
|
||||||
|
packets = [list(range(54)), list(range(64, 128)), list(range(128, 512))]
|
||||||
|
|
||||||
|
async def send():
|
||||||
|
for packet in packets:
|
||||||
|
for val, last in lookahead(packet):
|
||||||
|
buf.s_axis_data.value = val
|
||||||
|
buf.s_axis_valid.value = 1
|
||||||
|
buf.s_axis_last.value = last
|
||||||
|
while True:
|
||||||
|
await FallingEdge(buf.clk)
|
||||||
|
if buf.s_axis_ready.value:
|
||||||
|
break
|
||||||
|
buf.s_axis_valid.value = 0
|
||||||
|
if in_ratio != 1:
|
||||||
|
await ClockCycles(buf.clk, in_ratio - 1, rising=False)
|
||||||
|
|
||||||
|
async def recv(packet):
|
||||||
|
async def handshake():
|
||||||
|
while not buf.m_axis_valid.value or not buf.m_axis_ready.value:
|
||||||
|
await RisingEdge(buf.clk)
|
||||||
|
|
||||||
|
async def recv_len(length):
|
||||||
|
for i, val in enumerate(packet[:length]):
|
||||||
|
await handshake()
|
||||||
|
assert buf.m_axis_data.value == val
|
||||||
|
assert buf.m_axis_last == (i == len(packet) - 1)
|
||||||
|
await RisingEdge(buf.clk)
|
||||||
|
|
||||||
|
async def restart():
|
||||||
|
await FallingEdge(buf.clk)
|
||||||
|
assert buf.replayable.value
|
||||||
|
buf.replay.value = 1
|
||||||
|
await FallingEdge(buf.clk)
|
||||||
|
buf.replay.value = 0
|
||||||
|
|
||||||
|
buf.done.value = 0
|
||||||
|
replayable = min(len(packet), BUF_SIZE)
|
||||||
|
await recv_len(replayable - 3)
|
||||||
|
await restart()
|
||||||
|
await recv_len(replayable - 2)
|
||||||
|
# As long as the packet is <= BUF_SIZE we should be able to wait
|
||||||
|
# Try it out
|
||||||
|
if len(packet) <= BUF_SIZE:
|
||||||
|
await ClockCycles(buf.clk, 3)
|
||||||
|
await restart()
|
||||||
|
|
||||||
|
buf.done.value = 1
|
||||||
|
await recv_len(len(packet))
|
||||||
|
|
||||||
|
await cocotb.start(send())
|
||||||
|
for packet in packets:
|
||||||
|
await recv(packet)
|
||||||
|
|
||||||
|
replay_tests = TestFactory(test_replay)
|
||||||
|
replay_tests.add_option('in_ratio', (1, 2))
|
||||||
|
replay_tests.add_option('out_ratio', (1, 2))
|
||||||
|
replay_tests.generate_tests()
|
12
tb/util.py
12
tb/util.py
|
@ -140,8 +140,20 @@ def compare_lists(ins, outs):
|
||||||
|
|
||||||
async def ClockEnable(clk, ce, ratio):
|
async def ClockEnable(clk, ce, ratio):
|
||||||
ce.value = 1
|
ce.value = 1
|
||||||
|
if ratio == 1:
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await ClockCycles(clk, 1, False)
|
await ClockCycles(clk, 1, False)
|
||||||
ce.value = 0
|
ce.value = 0
|
||||||
await ClockCycles(clk, ratio - 1, False)
|
await ClockCycles(clk, ratio - 1, False)
|
||||||
ce.value = 1
|
ce.value = 1
|
||||||
|
|
||||||
|
# Adapted from https://stackoverflow.com/a/1630350/5086505
|
||||||
|
def lookahead(it):
|
||||||
|
it = iter(it)
|
||||||
|
last = next(it)
|
||||||
|
for val in it:
|
||||||
|
yield last, False
|
||||||
|
last = val
|
||||||
|
yield last, True
|
||||||
|
|
Loading…
Reference in New Issue