Add MII output receive interface

This generates the appropriate output for MII receive signals. Because
we don't have a clock synchronous to the recieved data, we may
occasionally have some cycles which are 32 ns or 48 ns long (instead of
the nominal 40 ns). This distorts the duty cycle to 38% or 58%,
respectively, which is within the specified 35% to 65%. This does change
the frequency to either 31 MHz or 21 MHz, respectively, which *is* a
violation of the spec. This could be avoided by introducing a FIFO to
smooth out any variations in jitter, like what RMII does.

The generation of rx_clk is a bit tricky. We can use a combinatorial
signal for the posedge, since that is what the rest of the logic is
referenced to, However, we need to register the negedge to prevent an
early (or late) ce from modifying the duty cycle.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2022-08-28 12:44:19 -04:00
parent 27d4c6457e
commit e2544d702f
3 changed files with 194 additions and 1 deletions

View File

@ -63,7 +63,7 @@ endef
%.post.fst: rtl/%.post.vvp tb/%.py FORCE
$(run-vvp)
MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio mdio_io
MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio mdio_io mii_io_rx
.PHONY: test
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES))

113
rtl/mii_io_rx.v Normal file
View File

@ -0,0 +1,113 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`include "common.vh"
`include "io.vh"
module mii_io_rx (
/* On-chip */
input clk,
input ce,
input valid,
input err,
input [3:0] data,
/* Off-chip */
output reg rx_clk,
output reg rx_dv,
output reg rx_er,
output reg [3:0] rxd
);
reg rx_clk_p_next, rx_clk_n, rx_clk_n_next;
reg [1:0] state = HIGH, state_next;
parameter LOW = 2;
parameter RISING = 1;
parameter HIGH = 0;
always @(*) begin
rx_clk_p_next = 0;
rx_clk_n_next = 0;
if (ce) begin
state_next = LOW;
end else case (state)
LOW: begin
state_next = RISING;
end
RISING: begin
state_next = HIGH;
rx_clk_n_next = 1;
end
HIGH: begin
state_next = HIGH;
rx_clk_p_next = 1;
rx_clk_n_next = 1;
end
endcase
end
always @(posedge clk) begin
state <= state_next;
rx_clk_n <= rx_clk_n_next;
end
`ifdef SYNTHESIS
SB_IO #(
.PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_DDR)
) rx_clk_pin (
.PACKAGE_PIN(rx_clk),
.OUTPUT_CLK(clk),
.D_OUT_0(rx_clk_p_next),
.D_OUT_1(rx_clk_n)
);
SB_IO #(
.PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED)
) rx_dv_pin (
.PACKAGE_PIN(rx_dv),
.CLOCK_ENABLE(ce),
.OUTPUT_CLK(clk),
.D_OUT_0(valid)
);
SB_IO #(
.PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED)
) rx_er_pin (
.PACKAGE_PIN(rx_er),
.CLOCK_ENABLE(ce),
.OUTPUT_CLK(clk),
.D_OUT_0(err)
);
genvar i;
generate for (i = 0; i < 4; i = i + 1) begin
SB_IO #(
.PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED)
) rxd_pin (
.PACKAGE_PIN(rxd[i]),
.CLOCK_ENABLE(ce),
.OUTPUT_CLK(clk),
.D_OUT_0(data[i])
);
end
endgenerate
`else
always @(posedge clk) begin
rx_clk <= rx_clk_p_next;
if (ce) begin
rx_dv <= valid;
rx_er <= err;
rxd <= data;
end
end
always @(negedge clk)
rx_clk <= rx_clk_n;
`endif
`DUMP(0)
endmodule

80
tb/mii_io_rx.py Normal file
View File

@ -0,0 +1,80 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import ClockCycles, Edge, FallingEdge, First, RisingEdge, Timer
from cocotb.types import LogicArray
from .util import ClockEnable
@cocotb.test(timeout_time=500, timeout_unit='ns')
async def test_io(io):
io.valid.value = LogicArray('X')
io.err.value = LogicArray('X')
io.data.value = LogicArray('X' * 4)
io.ce.value = 0
await Timer(1)
await cocotb.start(Clock(io.clk, 8, units='ns').start())
await ClockCycles(io.clk, 1)
async def clock_monitor():
await FallingEdge(io.rx_clk)
fall = cocotb.utils.get_sim_time('ns')
while True:
await RisingEdge(io.rx_clk)
rise = cocotb.utils.get_sim_time('ns')
assert round(rise - fall) == 20
await FallingEdge(io.rx_clk)
fall = cocotb.utils.get_sim_time('ns')
high = round(fall - rise)
assert high >= 12 and high <= 28
await cocotb.start(clock_monitor())
async def send_datum(valid, err, data, delay, early=True):
if early:
await Timer(1)
io.valid.value = valid
io.err.value = err
io.data.value = data
io.ce.value = 1
await ClockCycles(io.clk, 2 if early else 1, False)
io.ce.value = 0
io.valid.value = LogicArray('X')
io.err.value = LogicArray('X')
io.data.value = LogicArray('X' * 4)
await ClockCycles(io.clk, delay - 1, early)
async def send_data():
await send_datum(1, 0, 1, 5, True)
await send_datum(0, 1, 2, 4, True)
await send_datum(1, 0, 3, 5, True)
await send_datum(0, 1, 4, 6, True)
await send_datum(1, 0, 5, 5, True)
await FallingEdge(io.clk)
await send_datum(0, 1, 6, 5, False)
await send_datum(1, 0, 7, 4, False)
await send_datum(0, 1, 8, 5, False)
await send_datum(1, 0, 9, 6, False)
await send_datum(0, 1, 10, 5, False)
await cocotb.start(send_data())
async def recv_datum(valid, err, data):
await RisingEdge(io.rx_clk)
assert io.rx_dv.value == valid
assert io.rx_er.value == err
assert io.rxd.value == data
await RisingEdge(io.rx_clk)
await recv_datum(1, 0, 1)
await recv_datum(0, 1, 2)
await recv_datum(1, 0, 3)
await recv_datum(0, 1, 4)
await recv_datum(1, 0, 5)
await recv_datum(0, 1, 6)
await recv_datum(1, 0, 7)
await recv_datum(0, 1, 8)
await recv_datum(1, 0, 9)
await recv_datum(0, 1, 10)