Add TX MAC (most of it)
This adds the transmit half of a MAC, supporting 100M and half-duplex. It's roughly analogous to the axis_(x)gmii_tx modules in Alex Forencich's ethernet repo. I've taken the approach of moving all state into the state variable. All decisions are made once and have a different state for each path. For example, instead of checking against a "bytes_sent" variable to determine what to do on collision, we have a different state for each set of actions. This whole module is heinously complex, especially because of the many corner cases caused by the spec. I have probably not tested it nearly enough, but the basics of sending packets have mostly had the bugs wrung out. Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
parent
1f925d39ff
commit
0495ae377c
|
@ -0,0 +1,559 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-Only
|
||||
/*
|
||||
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
*
|
||||
* This is a 100M, Half-duplex MAC (or most of the TX side, anyway). It takes
|
||||
* an AXI stream of input data, appends the FCS, and drives the MII. The first
|
||||
* 57 bytes of data are stored to allow retrying upon collisions.
|
||||
*
|
||||
* The following diagram shows two frames, one under 56 bytes, and another
|
||||
* greater than 64 bytes. It is adapted from figure 4-5, but reworked to
|
||||
* remove complications added for 1000M, and to show the important state
|
||||
* transitions.
|
||||
*
|
||||
* | PREAMBLE | DATA_EARLY | PAD_EARLY | PAD_LATE | FCS |
|
||||
* | PREAMBLE | DATA_EARLY | DATA_MIDDLE | DATA_LATE | FCS |
|
||||
* |<-- Slot time -->|
|
||||
* |<-- Minimum frame size -->|
|
||||
*/
|
||||
|
||||
module axis_mii_tx (
|
||||
input clk,
|
||||
|
||||
/* MII */
|
||||
output reg mii_tx_ce,
|
||||
output reg mii_tx_en,
|
||||
output reg [3:0] mii_txd,
|
||||
|
||||
input mii_col,
|
||||
input mii_crs,
|
||||
|
||||
/* AXI Stream */
|
||||
input [7:0] axis_data,
|
||||
input axis_valid,
|
||||
output axis_ready,
|
||||
input axis_last,
|
||||
input axis_err,
|
||||
|
||||
/* Use a slot time of one octet for backoff; test purposes only */
|
||||
input short_backoff,
|
||||
|
||||
/* Clause 4 TransmitStatus */
|
||||
output reg transmit_ok,
|
||||
output reg gave_up,
|
||||
output reg late_collision,
|
||||
output reg underflow
|
||||
);
|
||||
|
||||
/*
|
||||
* The inter-packet states. In IPG_EARLY, any assertion of CRS will
|
||||
* reset the state counter. In IPG_LATE, CRS is ignored. However, if
|
||||
* there is no packet waiting and CRS is asserted, then it will
|
||||
* transition back to IPG_EARLY. Interestingly, IPG_EARLY_BYTES can be
|
||||
* reduced to 0 if desired, removing this functionality altogether.
|
||||
* Similarly, we go directly to IPG_LATE after transmitting a packet.
|
||||
*/
|
||||
localparam IPG_EARLY = 0;
|
||||
localparam IPG_LATE = 1;
|
||||
/*
|
||||
* The preamble state. Even if we have a collision, we still transmit
|
||||
* the full preamble before jamming.
|
||||
*/
|
||||
localparam PREAMBLE = 2;
|
||||
/*
|
||||
* The states used to transmit data. They are divided as follows:
|
||||
* - DATA_EARLY: Collisions during this state are early, and we need
|
||||
* to pad to the minimum frame size.
|
||||
* - DATA_MIDDLE: Collisions during this state are late, but we
|
||||
* still need to pad to the minimum frame size.
|
||||
* - DATA_LATE: Collisions are late, and we don't need to pad.
|
||||
* An important consideration here is the management of the done
|
||||
* signal. It must be asserted whenever we transition from early
|
||||
* collisions to late collisions.
|
||||
*/
|
||||
localparam DATA_EARLY = 3;
|
||||
localparam DATA_MIDDLE = 4;
|
||||
localparam DATA_LATE = 5;
|
||||
/*
|
||||
* Padding states:
|
||||
* - PAD_EARLY: If we get a collision we jam and retry.
|
||||
* - PAD_LATE: Collisions are late.
|
||||
*/
|
||||
localparam PAD_EARLY = 6;
|
||||
localparam PAD_LATE = 7;
|
||||
/*
|
||||
* Transmit the FCS.
|
||||
*/
|
||||
localparam FCS = 8;
|
||||
/*
|
||||
* Because bytes are sent over the MII one byte-time after the state
|
||||
* machine moves on, we can have a late collision even after the FCS
|
||||
* state is done. To detect this, we use an additional state which is
|
||||
* either the first byte of the jam sequence or the first byte of the
|
||||
* IPG, depending on whether we get a collision.
|
||||
*/
|
||||
localparam IPG_OR_JAM = 9;
|
||||
/*
|
||||
* The various jam sequences. They all send the same jam pattern, but
|
||||
* differ on what happens next. JAM_FAIL just goes straight to the
|
||||
* IPG. JAM_DRAIN goes to DRAIN. JAM_RETRY goes to BACKOFF if we still
|
||||
* have retries, or to DRAIN if we don't.
|
||||
*/
|
||||
localparam JAM_RETRY = 10;
|
||||
localparam JAM_DRAIN = 11;
|
||||
localparam JAM_FAIL = 12;
|
||||
/*
|
||||
* Drain the replay buffer and whatever is feeding it until we get to
|
||||
* the last byte.
|
||||
*/
|
||||
localparam DRAIN = 13;
|
||||
/*
|
||||
* Perform backoff. The amount is set by JAM_RETRY.
|
||||
*/
|
||||
localparam BACKOFF = 14;
|
||||
|
||||
localparam PREAMBLE_BYTES = 8;
|
||||
localparam FCS_BYTES = 4;
|
||||
localparam JAM_BYTES = 4;
|
||||
localparam SLOT_BYTES = 64;
|
||||
localparam MIN_FRAME_BYTES = 64;
|
||||
localparam IPG_BYTES = 12;
|
||||
parameter IPG_EARLY_BYTES = 8;
|
||||
localparam IPG_LATE_BYTES = IPG_BYTES - IPG_EARLY_BYTES;
|
||||
/* +1 since we transition 8 bit times earlier than the MII */
|
||||
localparam DATA_EARLY_BYTES = SLOT_BYTES - PREAMBLE_BYTES + 1;
|
||||
localparam DATA_MIDDLE_BYTES = MIN_FRAME_BYTES - DATA_EARLY_BYTES - FCS_BYTES;
|
||||
localparam PAD_LATE_BYTES = DATA_MIDDLE_BYTES;
|
||||
|
||||
localparam MAX_RETRIES = 16;
|
||||
|
||||
parameter MII_100_RATIO = 5;
|
||||
|
||||
localparam DATA_PREAMBLE = 8'h55;
|
||||
localparam DATA_SFD = 8'hd5;
|
||||
parameter DATA_JAM = 8'hff;
|
||||
parameter DATA_PAD = 8'h00;
|
||||
|
||||
localparam CRC_INIT = 32'hffffffff;
|
||||
|
||||
reg [7:0] data, data_next;
|
||||
reg [31:0] crc_state, crc_state_next, crc_state_out;
|
||||
initial crc_state = CRC_INIT;
|
||||
|
||||
lfsr #(
|
||||
.LFSR_WIDTH(32),
|
||||
.LFSR_POLY(32'h04c11db7),
|
||||
.LFSR_CONFIG("GALOIS"),
|
||||
.REVERSE(1),
|
||||
.DATA_WIDTH(8),
|
||||
.STYLE("REDUCTION")
|
||||
) crc (
|
||||
.data_in(data),
|
||||
.state_in(crc_state),
|
||||
.state_out(crc_state_out)
|
||||
);
|
||||
|
||||
wire [7:0] buf_data;
|
||||
wire buf_err, buf_valid, buf_last;
|
||||
reg buf_ready, buf_ready_next, replay, replay_next, done, done_next;
|
||||
initial begin
|
||||
buf_ready = 0;
|
||||
done = 0;
|
||||
replay = 0;
|
||||
end
|
||||
|
||||
axis_replay_buffer #(
|
||||
.DATA_WIDTH(9),
|
||||
.BUF_SIZE(DATA_EARLY_BYTES)
|
||||
) replay_buffer (
|
||||
.clk(clk),
|
||||
.s_axis_data({ axis_data, axis_err }),
|
||||
.s_axis_valid(axis_valid),
|
||||
.s_axis_ready(axis_ready),
|
||||
.s_axis_last(axis_last),
|
||||
.m_axis_data({ buf_data, buf_err }),
|
||||
.m_axis_valid(buf_valid),
|
||||
.m_axis_ready(buf_ready),
|
||||
.m_axis_last(buf_last),
|
||||
.replay(replay),
|
||||
.done(done)
|
||||
);
|
||||
|
||||
reg [2:0] mii_tx_counter, mii_tx_counter_next;
|
||||
reg mii_tx_ce_next, mii_tx_ce_next_next, mii_tx_ce_next_next_next;
|
||||
reg mii_tx_en_next;
|
||||
reg do_crc, do_crc_next, do_state, odd, odd_next, collision, collision_next;
|
||||
reg [3:0] state, state_next, retries, retries_next;
|
||||
reg [5:0] state_counter, state_counter_next;
|
||||
reg [9:0] lfsr, lfsr_next, backoff, backoff_next;
|
||||
reg transmit_ok_next, gave_up_next, late_collision_next;
|
||||
reg underflow_next;
|
||||
|
||||
initial begin
|
||||
mii_tx_counter = MII_100_RATIO;
|
||||
mii_tx_ce = 0;
|
||||
mii_tx_ce_next = 0;
|
||||
mii_tx_ce_next_next = 0;
|
||||
mii_tx_en = 0;
|
||||
mii_txd = 0;
|
||||
do_crc = 0;
|
||||
odd = 0;
|
||||
collision = 0;
|
||||
state = IPG_EARLY;
|
||||
state_counter = IPG_EARLY_BYTES - 1;
|
||||
retries = MAX_RETRIES - 1;
|
||||
lfsr = 10'h3ff;
|
||||
transmit_ok = 0;
|
||||
gave_up = 0;
|
||||
late_collision = 0;
|
||||
underflow = 0;
|
||||
end
|
||||
|
||||
always @(*) begin
|
||||
mii_tx_ce_next_next_next = 0;
|
||||
mii_tx_counter_next = mii_tx_counter - 1;
|
||||
if (!mii_tx_counter) begin
|
||||
mii_tx_ce_next_next_next = 1;
|
||||
mii_tx_counter_next = MII_100_RATIO - 1;
|
||||
end
|
||||
|
||||
do_crc_next = 0;
|
||||
crc_state_next = crc_state;
|
||||
if (do_crc)
|
||||
crc_state_next = crc_state_out;
|
||||
|
||||
backoff_next = backoff;
|
||||
lfsr_next = { lfsr[8:0], lfsr[9] ^ lfsr[6] };
|
||||
|
||||
odd_next = odd ^ mii_tx_ce_next;
|
||||
mii_tx_en_next = mii_tx_en;
|
||||
|
||||
do_state = 0;
|
||||
state_next = state;
|
||||
state_counter_next = state_counter;
|
||||
if (mii_tx_ce_next && odd) begin
|
||||
do_state = 1;
|
||||
state_counter_next = state_counter - 1;
|
||||
end
|
||||
|
||||
transmit_ok_next = 0;
|
||||
gave_up_next = 0;
|
||||
late_collision_next = 0;
|
||||
underflow_next = 0;
|
||||
|
||||
buf_ready_next = 0;
|
||||
data_next = data;
|
||||
replay_next = 0;
|
||||
done_next = 0;
|
||||
retries_next = retries;
|
||||
collision_next = collision || mii_col;
|
||||
case (state)
|
||||
IPG_EARLY: begin
|
||||
collision_next = 0;
|
||||
crc_state_next = CRC_INIT;
|
||||
if (mii_crs)
|
||||
state_counter_next = IPG_EARLY_BYTES - 1;
|
||||
|
||||
if (do_state) begin
|
||||
mii_tx_en_next = 0;
|
||||
|
||||
if (!state_counter && !mii_crs) begin
|
||||
state_counter_next = IPG_LATE_BYTES - 1;
|
||||
state_next = IPG_LATE;
|
||||
end
|
||||
end
|
||||
end
|
||||
IPG_LATE: begin
|
||||
collision_next = 0;
|
||||
crc_state_next = CRC_INIT;
|
||||
if (do_state && !state_counter) begin
|
||||
state_counter_next = state_counter;
|
||||
if (buf_valid) begin
|
||||
state_next = PREAMBLE;
|
||||
state_counter_next = PREAMBLE_BYTES - 1;
|
||||
end else if (mii_crs) begin
|
||||
state_next = IPG_EARLY;
|
||||
state_counter_next = IPG_EARLY_BYTES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
PREAMBLE: begin
|
||||
crc_state_next = CRC_INIT;
|
||||
if (do_state) begin
|
||||
mii_tx_en_next = 1;
|
||||
data_next = DATA_PREAMBLE;
|
||||
if (!state_counter) begin
|
||||
data_next = DATA_SFD;
|
||||
state_next = DATA_EARLY;
|
||||
state_counter_next = DATA_EARLY_BYTES - 1;
|
||||
if (collision_next) begin
|
||||
state_next = JAM_RETRY;
|
||||
state_counter_next = JAM_BYTES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
DATA_EARLY,
|
||||
DATA_MIDDLE,
|
||||
DATA_LATE: begin
|
||||
buf_ready_next = mii_tx_ce_next_next && odd;
|
||||
if (do_state) begin
|
||||
data_next = buf_data;
|
||||
do_crc_next = 1;
|
||||
|
||||
case (state)
|
||||
DATA_EARLY: begin
|
||||
if (!state_counter) begin
|
||||
if (buf_last)
|
||||
state_next = PAD_LATE;
|
||||
else
|
||||
state_next = DATA_MIDDLE;
|
||||
state_counter_next = DATA_MIDDLE_BYTES - 1;
|
||||
done_next = 1;
|
||||
end else if (buf_last) begin
|
||||
state_next = PAD_EARLY;
|
||||
end
|
||||
end
|
||||
DATA_MIDDLE: begin
|
||||
if (!state_counter) begin
|
||||
if (buf_last)
|
||||
state_next = FCS;
|
||||
else
|
||||
state_next = DATA_LATE;
|
||||
state_counter_next = FCS_BYTES - 1;
|
||||
end else if (buf_last) begin
|
||||
state_next = PAD_LATE;
|
||||
end
|
||||
end
|
||||
DATA_LATE: begin
|
||||
if (buf_last)
|
||||
state_next = FCS;
|
||||
state_counter_next = FCS_BYTES - 1;
|
||||
end
|
||||
endcase
|
||||
|
||||
if (!buf_valid || buf_err) begin
|
||||
if (buf_valid && buf_last)
|
||||
state_next = JAM_FAIL;
|
||||
else
|
||||
state_next = JAM_DRAIN;
|
||||
|
||||
/*
|
||||
* Start jamming immediately to avoid
|
||||
* sending Xs
|
||||
*/
|
||||
state_counter_next = JAM_BYTES - 2;
|
||||
data_next = DATA_JAM;
|
||||
underflow_next = 1;
|
||||
if (state == DATA_EARLY)
|
||||
done_next = 1;
|
||||
end
|
||||
|
||||
if (collision_next) begin
|
||||
underflow_next = 0;
|
||||
if (state == DATA_EARLY) begin
|
||||
state_next = JAM_RETRY;
|
||||
done_next = 0;
|
||||
end else begin
|
||||
if (buf_last)
|
||||
state_next = JAM_FAIL;
|
||||
else
|
||||
state_next = JAM_DRAIN;
|
||||
late_collision_next = 1;
|
||||
end
|
||||
state_counter_next = JAM_BYTES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
PAD_EARLY,
|
||||
PAD_LATE: begin
|
||||
if (do_state) begin
|
||||
data_next = DATA_PAD;
|
||||
do_crc_next = 1;
|
||||
if (collision_next) begin
|
||||
if (state == PAD_EARLY) begin
|
||||
state_next = JAM_RETRY;
|
||||
end else begin
|
||||
state_next = JAM_FAIL;
|
||||
late_collision_next = 1;
|
||||
end
|
||||
state_counter_next = JAM_BYTES - 1;
|
||||
end else if (!state_counter) begin
|
||||
if (state == PAD_EARLY) begin
|
||||
state_next = PAD_LATE;
|
||||
state_counter_next =
|
||||
PAD_LATE_BYTES - 1;
|
||||
done_next = 1;
|
||||
end else begin
|
||||
state_next = FCS;
|
||||
state_counter_next =
|
||||
FCS_BYTES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
FCS: begin
|
||||
if (do_state) begin
|
||||
case (state_counter[1:0])
|
||||
2'd3: data_next = ~crc_state[7:0];
|
||||
2'd2: data_next = ~crc_state[15:8];
|
||||
2'd1: data_next = ~crc_state[23:16];
|
||||
2'd0: begin
|
||||
data_next = ~crc_state[31:24];
|
||||
state_next = IPG_OR_JAM;
|
||||
end
|
||||
endcase
|
||||
|
||||
if (collision_next) begin
|
||||
state_next = JAM_FAIL;
|
||||
state_counter_next = JAM_BYTES - 1;
|
||||
late_collision_next = 1;
|
||||
transmit_ok_next = 0;
|
||||
end
|
||||
end
|
||||
end
|
||||
IPG_OR_JAM: begin
|
||||
if (do_state) begin
|
||||
data_next = DATA_JAM;
|
||||
if (collision_next) begin
|
||||
state_next = JAM_FAIL;
|
||||
state_counter_next = JAM_BYTES - 2;
|
||||
late_collision_next = 1;
|
||||
end else begin
|
||||
mii_tx_en_next = 0;
|
||||
state_next = IPG_LATE;
|
||||
state_counter_next = IPG_BYTES - 2;
|
||||
transmit_ok_next = 1;
|
||||
retries_next = MAX_RETRIES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
JAM_RETRY,
|
||||
JAM_DRAIN,
|
||||
JAM_FAIL: begin
|
||||
if (do_state)
|
||||
data_next = DATA_JAM;
|
||||
|
||||
if (do_state && !state_counter) begin
|
||||
if (state == JAM_RETRY && retries) begin
|
||||
state_next = BACKOFF;
|
||||
/*
|
||||
* A value of 0 would otherwise mean
|
||||
* "wait one slot time", so start at
|
||||
* the "end" of a slot time so we
|
||||
* don't wait for no reason.
|
||||
*/
|
||||
state_counter_next = 0;
|
||||
replay_next = 1;
|
||||
retries_next = retries - 1;
|
||||
case (retries)
|
||||
15: backoff_next = lfsr[ 0];
|
||||
14: backoff_next = lfsr[1:0];
|
||||
13: backoff_next = lfsr[2:0];
|
||||
12: backoff_next = lfsr[3:0];
|
||||
11: backoff_next = lfsr[4:0];
|
||||
10: backoff_next = lfsr[5:0];
|
||||
9: backoff_next = lfsr[6:0];
|
||||
8: backoff_next = lfsr[7:0];
|
||||
7: backoff_next = lfsr[8:0];
|
||||
6, 5, 4, 3, 2, 1, 0:
|
||||
backoff_next = lfsr[9:0];
|
||||
endcase
|
||||
end else begin
|
||||
state_counter_next = IPG_BYTES - 1;
|
||||
if (state == JAM_FAIL) begin
|
||||
state_next = IPG_LATE;
|
||||
mii_tx_en_next = 0;
|
||||
end else begin
|
||||
state_next = DRAIN;
|
||||
end
|
||||
retries_next = MAX_RETRIES - 1;
|
||||
if (state == JAM_RETRY) begin
|
||||
gave_up_next = 1;
|
||||
done_next = 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
DRAIN: begin
|
||||
if (buf_ready && buf_valid && buf_last) begin
|
||||
state_next = IPG_EARLY;
|
||||
state_counter_next = IPG_EARLY_BYTES - 1;
|
||||
end else begin
|
||||
buf_ready_next = 1;
|
||||
end
|
||||
|
||||
if (do_state)
|
||||
mii_tx_en_next = 0;
|
||||
end
|
||||
BACKOFF: begin
|
||||
if (mii_tx_ce_next && odd) begin
|
||||
mii_tx_en_next = 0;
|
||||
if (!state_counter) begin
|
||||
backoff_next = backoff - 1;
|
||||
if (short_backoff)
|
||||
state_counter_next = 0;
|
||||
else
|
||||
state_counter_next = SLOT_BYTES - 1;
|
||||
|
||||
if (!backoff) begin
|
||||
state_next = IPG_EARLY;
|
||||
state_counter_next = IPG_EARLY_BYTES - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
always @(posedge clk) begin
|
||||
mii_tx_ce <= mii_tx_ce_next;
|
||||
mii_tx_ce_next <= mii_tx_ce_next_next;
|
||||
mii_tx_ce_next_next <= mii_tx_ce_next_next_next;
|
||||
mii_tx_counter <= mii_tx_counter_next;
|
||||
mii_tx_en <= mii_tx_en_next;
|
||||
mii_txd <= odd_next ? data_next[7:4] : data_next[3:0];
|
||||
do_crc <= do_crc_next;
|
||||
odd <= odd_next;
|
||||
state <= state_next;
|
||||
state_counter <= state_counter_next;
|
||||
buf_ready <= buf_ready_next;
|
||||
data <= data_next;
|
||||
replay <= replay_next;
|
||||
done <= done_next;
|
||||
retries <= retries_next;
|
||||
backoff <= backoff_next;
|
||||
lfsr <= lfsr_next;
|
||||
crc_state <= crc_state_next;
|
||||
collision <= collision_next;
|
||||
transmit_ok <= transmit_ok_next;
|
||||
gave_up <= gave_up_next;
|
||||
late_collision <= late_collision_next;
|
||||
underflow <= underflow_next;
|
||||
end
|
||||
|
||||
`ifndef SYNTHESIS
|
||||
reg [255:0] state_text;
|
||||
|
||||
always @(*) begin
|
||||
case (state)
|
||||
IPG_EARLY: state_text = "IPG_EARLY";
|
||||
IPG_LATE: state_text = "IPG_LATE";
|
||||
PREAMBLE: state_text = "PREAMBLE";
|
||||
DATA_EARLY: state_text = "DATA_EARLY";
|
||||
DATA_MIDDLE: state_text = "DATA_MIDDLE";
|
||||
DATA_LATE: state_text = "DATA_LATE";
|
||||
PAD_EARLY: state_text = "PAD_EARLY";
|
||||
PAD_LATE: state_text = "PAD_LATE";
|
||||
FCS: state_text = "FCS";
|
||||
IPG_OR_JAM: state_text = "IPG_OR_JAM";
|
||||
JAM_RETRY: state_text = "JAM_RETRY";
|
||||
JAM_DRAIN: state_text = "JAM_DRAIN";
|
||||
JAM_FAIL: state_text = "JAM_FAIL";
|
||||
DRAIN: state_text = "DRAIN";
|
||||
BACKOFF: state_text = "BACKOFF";
|
||||
endcase
|
||||
end
|
||||
`endif
|
||||
|
||||
endmodule
|
|
@ -0,0 +1,321 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-Only
|
||||
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
|
||||
import enum
|
||||
import random
|
||||
import zlib
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.regression import TestFactory
|
||||
from cocotb.triggers import ClockCycles, Edge, FallingEdge, First, RisingEdge, Timer
|
||||
from cocotb.types import LogicArray
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from . import axis_replay_buffer
|
||||
from .pcs_rx import mii_recv_packet
|
||||
from .util import alist, lookahead, timeout
|
||||
|
||||
import os
|
||||
|
||||
skip_slow = not os.environ.get('RUN_SLOW', False)
|
||||
|
||||
async def init(mac):
|
||||
mac.mii_col.value = 0
|
||||
mac.mii_crs.value = 0
|
||||
mac.axis_valid.value = 0
|
||||
mac.axis_err.value = 0
|
||||
mac.short_backoff.value = 1
|
||||
await Timer(1)
|
||||
await cocotb.start(Clock(mac.clk, 8, units='ns').start())
|
||||
|
||||
def send_packet(mac, packet, ratio=1):
|
||||
return axis_replay_buffer.send_packet({
|
||||
'clk': mac.clk,
|
||||
'data': mac.axis_data,
|
||||
'err': mac.axis_err,
|
||||
'valid': mac.axis_valid,
|
||||
'last': mac.axis_last,
|
||||
'ready': mac.axis_ready,
|
||||
}, packet, ratio)
|
||||
|
||||
class MACError(Exception):
|
||||
pass
|
||||
|
||||
class FrameCheckError(MACError):
|
||||
pass
|
||||
|
||||
class AlignmentError(MACError):
|
||||
pass
|
||||
|
||||
class PaddingError(MACError):
|
||||
pass
|
||||
|
||||
async def nibbles_to_bytes(nibbles):
|
||||
while True:
|
||||
try:
|
||||
lo = await anext(nibbles)
|
||||
except StopAsyncIteration:
|
||||
return
|
||||
|
||||
try:
|
||||
hi = await anext(nibbles)
|
||||
except StopAsyncIteration:
|
||||
raise AlignmentError
|
||||
|
||||
yield hi << 4 | lo
|
||||
|
||||
SFD = 0xd5
|
||||
|
||||
async def skip_preamble(packet):
|
||||
saw_ssd = False
|
||||
async for byte in packet:
|
||||
if saw_ssd:
|
||||
yield byte
|
||||
elif byte == SFD:
|
||||
saw_ssd = True
|
||||
|
||||
FCS_GOOD = zlib.crc32(b'\0\0\0\0')
|
||||
|
||||
async def check_fcs(packet):
|
||||
packet = await alist(packet)
|
||||
fcs = zlib.crc32(bytes(packet))
|
||||
if fcs != FCS_GOOD:
|
||||
raise FrameCheckError
|
||||
elif len(packet) < 64:
|
||||
raise PaddingError
|
||||
return packet[:-4]
|
||||
|
||||
def recv_packet(mac):
|
||||
return check_fcs(skip_preamble(nibbles_to_bytes(mii_recv_packet(mac, {
|
||||
'ce': mac.mii_tx_ce,
|
||||
'data': mac.mii_txd,
|
||||
'valid': mac.mii_tx_en,
|
||||
}))))
|
||||
|
||||
async def expect_bad_fcs(mac):
|
||||
try:
|
||||
await recv_packet(mac)
|
||||
except FrameCheckError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
class Status(enum.Enum):
|
||||
OK = enum.auto()
|
||||
GAVE_UP = enum.auto()
|
||||
LATE_COLLISION = enum.auto()
|
||||
UNDERFLOW = enum.auto()
|
||||
|
||||
async def get_status(mac):
|
||||
ok = 0
|
||||
gave_up = 0
|
||||
late = 0
|
||||
underflow = 0
|
||||
|
||||
while not (ok or gave_up or late or underflow) or mac.mii_tx_en.value:
|
||||
ok += mac.transmit_ok.value
|
||||
gave_up += mac.gave_up.value
|
||||
late += mac.late_collision.value
|
||||
underflow += mac.underflow.value
|
||||
await FallingEdge(mac.clk)
|
||||
|
||||
assert ok + gave_up + late + underflow == 1
|
||||
if ok:
|
||||
return Status.OK
|
||||
elif gave_up:
|
||||
return Status.GAVE_UP
|
||||
elif late:
|
||||
return Status.LATE_COLLISION
|
||||
elif underflow:
|
||||
return Status.UNDERFLOW
|
||||
|
||||
async def start(mac, packet, ratio=1):
|
||||
send = await cocotb.start(send_packet(mac, packet, ratio))
|
||||
status = await cocotb.start(get_status(mac))
|
||||
return send, status
|
||||
|
||||
BIT_TIME_NS = 10
|
||||
BYTE_TIME_NS = 8 * BIT_TIME_NS
|
||||
|
||||
async def collide(mac, ns, duration=16):
|
||||
while not mac.mii_tx_en.value:
|
||||
await FallingEdge(mac.clk)
|
||||
|
||||
if ns > 4:
|
||||
await Timer(ns - 4, 'ns')
|
||||
mac.mii_col.value = 1
|
||||
await Timer(duration, 'ns')
|
||||
mac.mii_col.value = 0
|
||||
|
||||
def randtime(min_bytes, max_bytes):
|
||||
return random.randrange(min_bytes * BYTE_TIME_NS, max_bytes * BYTE_TIME_NS)
|
||||
|
||||
async def restart(mac, ns):
|
||||
await cocotb.start(collide(mac, ns))
|
||||
await expect_bad_fcs(mac)
|
||||
|
||||
def compare(actual, expected):
|
||||
if actual[:len(expected)] != expected:
|
||||
print(actual)
|
||||
print(expected)
|
||||
raise AssertionError
|
||||
|
||||
async def ok(mac, packet, status, ns=None):
|
||||
if ns is not None:
|
||||
await cocotb.start(collide(mac, ns))
|
||||
compare(await recv_packet(mac), packet)
|
||||
assert await status.join() == Status.OK
|
||||
|
||||
@timeout(50, 'us')
|
||||
async def test_send(mac, ratio):
|
||||
await init(mac)
|
||||
|
||||
packets = (
|
||||
list(range(32)),
|
||||
list(range(56)),
|
||||
list(range(256)),
|
||||
)
|
||||
|
||||
async def send_packets():
|
||||
for packet in packets:
|
||||
await send_packet(mac, packet, ratio)
|
||||
|
||||
await cocotb.start(send_packets())
|
||||
for i, packet in enumerate(packets):
|
||||
status = await cocotb.start(get_status(mac))
|
||||
recv = await cocotb.start(recv_packet(mac))
|
||||
|
||||
# Measure the IPG to ensure throughput
|
||||
start = get_sim_time('ns')
|
||||
while not mac.mii_tx_en.value:
|
||||
await RisingEdge(mac.clk)
|
||||
# The first IPG may not be exact
|
||||
if i:
|
||||
assert get_sim_time('ns') - start == 12 * 80 - 4
|
||||
|
||||
compare(await recv.join(), packet)
|
||||
assert await status.join() == Status.OK
|
||||
|
||||
send_tests = TestFactory(test_send)
|
||||
send_tests.add_option('ratio', (1, BIT_TIME_NS))
|
||||
send_tests.generate_tests()
|
||||
|
||||
@cocotb.test(timeout_time=100, timeout_unit='us')
|
||||
async def test_underflow(mac):
|
||||
await init(mac)
|
||||
|
||||
async def underflow(mac, send, status):
|
||||
await expect_bad_fcs(mac)
|
||||
await send.join()
|
||||
assert await status.join() == Status.UNDERFLOW
|
||||
|
||||
send, status = await start(mac, range(32), 30)
|
||||
await underflow(mac, send, status)
|
||||
|
||||
send, status = await start(mac, [*range(56), None])
|
||||
await underflow(mac, send, status)
|
||||
send, status = await start(mac, [*range(58), None])
|
||||
await underflow(mac, send, status)
|
||||
send, status = await start(mac, [*range(60), None])
|
||||
await underflow(mac, send, status)
|
||||
|
||||
send, status = await start(mac, [*range(56), None, 1])
|
||||
await underflow(mac, send, status)
|
||||
send, status = await start(mac, [*range(58), None, 1])
|
||||
await underflow(mac, send, status)
|
||||
send, status = await start(mac, [*range(60), None, 1])
|
||||
await underflow(mac, send, status)
|
||||
|
||||
send, status = await start(mac, [*range(56), None])
|
||||
await restart(mac, (8 + 55) * BYTE_TIME_NS)
|
||||
await underflow(mac, send, status)
|
||||
send, status = await start(mac, [*range(58), None])
|
||||
await restart(mac, (8 + 57) * BYTE_TIME_NS)
|
||||
await send.join()
|
||||
assert await status.join() == Status.LATE_COLLISION
|
||||
send, status = await start(mac, [*range(60), None])
|
||||
await restart(mac, (8 + 59) * BYTE_TIME_NS)
|
||||
await send.join()
|
||||
assert await status.join() == Status.LATE_COLLISION
|
||||
|
||||
@cocotb.test(timeout_time=1250, timeout_unit='us', skip=skip_slow)
|
||||
async def test_backoff(mac):
|
||||
await init(mac)
|
||||
|
||||
packet = list(range(32))
|
||||
for collisions in (15, 16):
|
||||
send, status = await start(mac, packet)
|
||||
then = None
|
||||
for n in range(collisions):
|
||||
await restart(mac, 0)
|
||||
now = get_sim_time('ns')
|
||||
if then is not None:
|
||||
assert now - then <= (8 + 4 + 12 + 2 ** min(n, 10)) * BYTE_TIME_NS
|
||||
then = now
|
||||
|
||||
if collisions == 16:
|
||||
assert await status.join() == Status.GAVE_UP
|
||||
else:
|
||||
await ok(mac, packet, status)
|
||||
|
||||
@cocotb.test(timeout_time=10, timeout_unit='us')
|
||||
async def test_defer(mac):
|
||||
await init(mac)
|
||||
|
||||
# Skip to the end of IPG_LATE
|
||||
mac.mii_crs.value = 1
|
||||
await Timer(13 * BYTE_TIME_NS, 'ns')
|
||||
packet = list(range(32))
|
||||
send, status = await start(mac, packet)
|
||||
# Ensure IPG_EARLY works
|
||||
await Timer(13 * BYTE_TIME_NS, 'ns')
|
||||
assert not mac.mii_tx_en.value
|
||||
|
||||
# Test the early 2/3s; not exact since the last 1/3 takes the slack
|
||||
mac.mii_crs.value = 0
|
||||
await Timer(7 * BYTE_TIME_NS, 'ns')
|
||||
mac.mii_crs.value = 1
|
||||
await Timer(6 * BYTE_TIME_NS, 'ns')
|
||||
assert not mac.mii_tx_en.value
|
||||
|
||||
# And the late
|
||||
mac.mii_crs.value = 0
|
||||
await Timer(8 * BYTE_TIME_NS, 'ns')
|
||||
mac.mii_crs.value = 1
|
||||
await Timer(5 * BYTE_TIME_NS, 'ns')
|
||||
assert mac.mii_tx_en.value
|
||||
assert await status.join() == Status.OK
|
||||
|
||||
@cocotb.test(timeout_time=150, timeout_unit='us')
|
||||
async def test_collision(mac):
|
||||
await init(mac)
|
||||
|
||||
async def late(mac, packet, ns):
|
||||
send, status = await start(mac, packet)
|
||||
await restart(mac, ns)
|
||||
await send.join()
|
||||
assert await status.join() == Status.LATE_COLLISION
|
||||
|
||||
packet = list(range(32))
|
||||
send, status = await start(mac, packet)
|
||||
await restart(mac, randtime(0, 8))
|
||||
await restart(mac, randtime(8, 40))
|
||||
await restart(mac, randtime(40, 64))
|
||||
await restart(mac, 64 * BYTE_TIME_NS - 1)
|
||||
await ok(mac, packet, status, 72 * BYTE_TIME_NS)
|
||||
|
||||
await late(mac, packet, 64 * BYTE_TIME_NS)
|
||||
await late(mac, packet, randtime(64, 72))
|
||||
await late(mac, packet, (8 + 64) * BYTE_TIME_NS - 1)
|
||||
|
||||
packet = list(range(256))
|
||||
send, status = await start(mac, packet)
|
||||
await restart(mac, randtime(8, 40))
|
||||
await restart(mac, randtime(40, 64))
|
||||
await restart(mac, 64 * BYTE_TIME_NS - 1)
|
||||
await ok(mac, packet, status, (8 + 256 + 4) * BYTE_TIME_NS)
|
||||
|
||||
await late(mac, packet, 64 * BYTE_TIME_NS)
|
||||
await late(mac, packet, randtime(64, 256))
|
||||
await late(mac, packet, 72 * BYTE_TIME_NS - 1)
|
Loading…
Reference in New Issue