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'