From dd4183991d6b4dadbf8e24411a261004ec7d0dc0 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sat, 27 Aug 2022 16:00:37 -0400 Subject: [PATCH] Add MDIO I/O module This module implements the I/O portion of the MII management interface. The output is delayed by 2 clocks in order to ensure that the external level shifter has switched directions before we drive it. The latency increase (around 16 ns) is not consequential, since we have around 300 ns from the rising edge of MDC before MDIO has to be valid. On the other end, the timing requirements for MDIO driven by the STA are very lenient (for them); MDIO only has to be valid for 10 ns on either side of the rising edge of MDC. This effectively means we must sample MDIO synchronously to MDC (not easy with nextpnr), or oversample by 50x. Fortunately, we have a 125 MHz clock which the rest of the phty runs off of. However, this basically makes 10x oversampling with the MII clock impossible. Signed-off-by: Sean Anderson --- Makefile | 2 +- rtl/mdio_io.v | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ tb/mdio_io.py | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 rtl/mdio_io.v create mode 100644 tb/mdio_io.py diff --git a/Makefile b/Makefile index 2157128..73931c2 100644 --- a/Makefile +++ b/Makefile @@ -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 +MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio mdio_io .PHONY: test test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES)) diff --git a/rtl/mdio_io.v b/rtl/mdio_io.v new file mode 100644 index 0000000..4571059 --- /dev/null +++ b/rtl/mdio_io.v @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" +`include "io.vh" + +module mdio_io ( + input clk, + + input mdc, + inout mdio, + output reg mdio_oe, + + input mdo, + input mdo_valid, + output reg ce, + output reg mdi +); + + wire ce_next; + reg mdi_next; + reg [1:0] last_mdc; + /* Two clock delay to allow the level shifter to reverse direction */ + reg [2:0] oe = 0; + +`ifdef SYNTHESIS + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_NEVER | `PIN_INPUT_REGISTERED) + ) mdc_pin ( + .PACKAGE_PIN(mdc), + .INPUT_CLK(clk), + .D_IN_0(last_mdc[0]) + ); + + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_REGISTERED | `PIN_OUTPUT_ENABLE | `PIN_INPUT_REGISTERED) + ) mdio_pin ( + .PACKAGE_PIN(mdio), + .INPUT_CLK(clk), + .OUTPUT_CLK(clk), + .OUTPUT_ENABLE(oe[2]), + .D_OUT_0(mdo), + .D_IN_0(mdi_next), + ); + + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED) + ) mdio_oe_pin ( + .PACKAGE_PIN(mdio_oe), + .OUTPUT_CLK(clk), + .D_OUT_0(mdo_valid), + ); +`else + reg mdio_next; + + always @(posedge clk) begin + last_mdc[0] <= mdc; + mdi_next <= mdio; + mdio_next <= mdo; + mdio_oe <= mdo_valid; + end + + assign mdio = oe[2] ? mdio_next : 1'bZ; +`endif + + assign ce_next = last_mdc[0] && !last_mdc[1]; + + always @(posedge clk) begin + mdi <= mdi_next; + last_mdc[1] <= last_mdc[0]; + ce <= ce_next; + mdio_oe <= mdo_valid; + if (mdo_valid) + oe <= { oe[1:0], mdo_valid }; + else + oe <= 0; + end + + `DUMP(0) + +endmodule diff --git a/tb/mdio_io.py b/tb/mdio_io.py new file mode 100644 index 0000000..b2ed0fb --- /dev/null +++ b/tb/mdio_io.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +import random + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Edge, FallingEdge, First, RisingEdge, Timer +from cocotb.types import LogicArray + +@cocotb.test(timeout_time=50, timeout_unit='us') +async def test_io(io): + io.mdio.value = LogicArray('X') + io.mdc.value = 0 + io.mdo_valid.value = 0 + await Timer(1) + await cocotb.start(Clock(io.clk, 8, units='ns').start()) + # random phase + await Timer(random.randrange(1, 9), units='ns') + await cocotb.start(Clock(io.mdc, 400, units='ns').start()) + + ins = [random.randrange(2) for _ in range(10)] + await FallingEdge(io.mdc) + + async def send_ins(): + for bit in ins: + await Timer(190, 'ns') + io.mdio.value = bit + await Timer(20, 'ns') + io.mdio.value = LogicArray('X') + await FallingEdge(io.mdc) + await cocotb.start(send_ins()) + + for bit in ins: + while not io.ce.value: + await RisingEdge(io.clk) + assert io.mdi.value == bit + await FallingEdge(io.clk) + + outs = [random.randrange(2) for _ in range(10)] + io.mdio.value = LogicArray('Z') + await FallingEdge(io.clk) + for bit in outs: + io.mdo.value = bit + io.mdo_valid.value = 1 + await FallingEdge(io.clk) + assert io.mdio_oe.value + await FallingEdge(io.clk) + await FallingEdge(io.clk) + assert io.mdio.value == bit + io.mdo_valid.value = 0 + await FallingEdge(io.clk) + assert io.mdio.value.binstr == 'z'