diff --git a/Makefile b/Makefile index 7c6303c..24181c7 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 mdio_io mii_io_rx +MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio mdio_io mii_io_rx mii_io_tx .PHONY: test test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES)) diff --git a/rtl/mii_io_tx.v b/rtl/mii_io_tx.v new file mode 100644 index 0000000..79eaa99 --- /dev/null +++ b/rtl/mii_io_tx.v @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" +`include "io.vh" + +module mii_io_tx ( + /* On-chip */ + input clk, + output reg ce, + output reg enable, + output reg err, + output reg [3:0] data, + + /* Off-chip */ + output reg tx_clk, + input tx_en, + input tx_er, + input [3:0] txd +); + + reg ce_next; + reg tx_clk_p_next, tx_clk_n, tx_clk_n_next; + reg [2:0] counter, counter_next; + /* I have no idea why we need to use initial... */ + initial counter = 4; + + always @(*) begin + tx_clk_p_next = 0; + tx_clk_n_next = 0; + ce_next = 0; + counter_next = counter - 1; + case (counter) + 4, 3: begin + tx_clk_p_next = 1; + tx_clk_n_next = 1; + end + 2: tx_clk_p_next = 1; + 1: ; + 0: begin + ce_next = 1; + counter_next = 4; + end + endcase + end + + always @(posedge clk) begin + counter <= counter_next; + tx_clk_n <= tx_clk_n_next; + ce <= ce_next; + end + +`ifdef SYNTHESIS + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_DDR) + ) tx_clk_pin ( + .PACKAGE_PIN(tx_clk), + .OUTPUT_CLK(clk), + .D_OUT_0(tx_clk_p_next), + .D_OUT_1(tx_clk_n) + ); + + SB_IO #( + .PIN_TYPE(`PIN_INPUT_REGISTERED) + ) tx_en_pin ( + .PACKAGE_PIN(tx_en), + .CLOCK_ENABLE(ce_next), + .INPUT_CLK(clk), + .D_IN_0(enable) + ); + + SB_IO #( + .PIN_TYPE(`PIN_INPUT_REGISTERED) + ) tx_er_pin ( + .PACKAGE_PIN(tx_er), + .CLOCK_ENABLE(ce_next), + .INPUT_CLK(clk), + .D_IN_0(err) + ); + + genvar i; + generate for (i = 0; i < 4; i = i + 1) begin + SB_IO #( + .PIN_TYPE(`PIN_INPUT_REGISTERED) + ) txd_pin ( + .PACKAGE_PIN(txd[i]), + .CLOCK_ENABLE(ce_next), + .INPUT_CLK(clk), + .D_IN_0(data[i]) + ); + end + endgenerate +`else + always @(posedge clk) begin + tx_clk <= tx_clk_p_next; + if (ce_next) begin + enable <= tx_en; + err <= tx_er; + data <= txd; + end + end + + always @(negedge clk) + tx_clk <= tx_clk_n; +`endif + + `DUMP(0) + +endmodule diff --git a/tb/mii_io_tx.py b/tb/mii_io_tx.py new file mode 100644 index 0000000..2fef5d8 --- /dev/null +++ b/tb/mii_io_tx.py @@ -0,0 +1,51 @@ +# 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 + +from .util import ClockEnable + +@cocotb.test(timeout_time=500, timeout_unit='ns') +async def test_io(io): + io.tx_en.value = LogicArray('X') + io.tx_er.value = LogicArray('X') + io.txd.value = LogicArray('X' * 4) + await Timer(1) + await cocotb.start(Clock(io.clk, 8, units='ns').start()) + + async def send_datum(enable, err, data): + await RisingEdge(io.tx_clk) + io.tx_en.value = LogicArray('X') + io.tx_er.value = LogicArray('X') + io.txd.value = LogicArray('X' * 4) + await Timer(25, 'ns') + io.tx_en.value = enable + io.tx_er.value = err + io.txd.value = data + + async def send_data(): + await send_datum(0, 1, 1) + await send_datum(1, 0, 2) + await send_datum(0, 1, 3) + await send_datum(1, 0, 4) + await send_datum(0, 1, 5) + await cocotb.start(send_data()) + + async def recv_datum(enable, err, data): + await RisingEdge(io.ce) + await RisingEdge(io.clk) + assert io.ce.value + assert io.enable.value == enable + assert io.err.value == err + assert io.data.value == data + + await recv_datum(0, 1, 1) + await recv_datum(1, 0, 2) + await recv_datum(0, 1, 3) + await recv_datum(1, 0, 4) + await recv_datum(0, 1, 5)