diff --git a/Makefile b/Makefile index b5d1c30..009558b 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ endef $(run-vvp) MODULES := pcs pmd_io nrzi_encode nrzi_decode scramble descramble mdio mdio_io mii_io_rx mii_io_tx +MODULES += mdio_regs .PHONY: test test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES)) diff --git a/rtl/mdio_regs.v b/rtl/mdio_regs.v new file mode 100644 index 0000000..e99d56a --- /dev/null +++ b/rtl/mdio_regs.v @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" + +module mdio_regs ( + /* Wishbone */ + input clk, + output reg ack, err, + input cyc, stb, we, + input [4:0] addr, + input [15:0] data_write, + output reg [15:0] data_read, + + /* Control signals */ + input link_status, + output reg loopback, + output reg pdown, + output reg isolate, + output reg coltest +); + + /* The current price of a CID is $805... */ + parameter [23:0] OUI = 0; + parameter [5:0] MODEL = 0; + parameter [3:0] REVISION = 0; + /* + * Normally, this module will assert err when read/writing to an + * unknown register. The master will detect this and won't drive MDIO + * line. However, this might be undesirable if there is no external + * MDIO bus. Setting this parameter to 0 will cause it to ack all + * transactions. Writes to unknown registers will be ignored, and + * reads from unknown registers will yield 16'hffff, emulating + * a pull-up on MDIO. + */ + parameter EMULATE_PULLUP = 0; + + localparam BMCR = 0; + localparam BMSR = 1; + localparam ID1 = 2; + localparam ID2 = 3; + + localparam BMCR_RESET = 15; + localparam BMCR_LOOPBACK = 14; + localparam BMCR_SPEED_LSB = 13; + localparam BMCR_PDOWN = 11; + localparam BMCR_ISOLATE = 10; + localparam BMCR_DUPLEX = 8; + localparam BMCR_COLTEST = 7; + localparam BMCR_SPEED_MSB = 6; + + localparam BMSR_100FULL = 14; + localparam BMSR_100HALF = 13; + localparam BMSR_LSTATUS = 2; + localparam BMSR_EXTCAP = 0; + + integer i; + reg duplex, link_status_latched; + reg loopback_next, pdown_next, isolate_next, duplex_next, coltest_next; + reg link_status_latched_next; + reg [15:0] data_read_next; + + initial begin + loopback = 0; + pdown = 0; + isolate = 1; + duplex = 0; + coltest = 0; + link_status_latched = 0; + end + + always @(*) begin + loopback_next = loopback; + pdown_next = pdown; + isolate_next = isolate; + duplex_next = duplex; + coltest_next = coltest; + link_status_latched_next = link_status_latched && link_status; + + data_read_next = 0; + ack = cyc && stb; + err = 0; + case (addr) + BMCR: begin + data_read_next[BMCR_LOOPBACK] = loopback; + data_read_next[BMCR_SPEED_LSB] = 1; /* 100 Mb/s */ + data_read_next[BMCR_PDOWN] = pdown; + data_read_next[BMCR_ISOLATE] = isolate; + data_read_next[BMCR_DUPLEX] = duplex; + data_read_next[BMCR_COLTEST] = coltest; + + if (cyc && stb && we) begin + loopback_next = data_write[BMCR_LOOPBACK]; + pdown_next = data_write[BMCR_PDOWN]; + isolate_next = data_write[BMCR_ISOLATE]; + duplex_next = data_write[BMCR_DUPLEX]; + coltest_next = data_write[BMCR_COLTEST]; + + if (data_write[BMCR_RESET]) begin + loopback_next = 0; + pdown_next = 0; + isolate_next = 1; + duplex_next = 0; + coltest_next = 0; + link_status_latched_next = link_status; + end + end + end + BMSR: begin + data_read_next[BMSR_100FULL] = 1; + data_read_next[BMSR_100HALF] = 1; + data_read_next[BMSR_LSTATUS] = link_status_latched; + data_read_next[BMSR_EXTCAP] = 1; + + if (cyc && stb && !we) + link_status_latched_next = link_status; + end + ID1: begin + for (i = 0; i < 16; i = i + 1) + data_read_next[i] = OUI[17 - i]; + end + ID2: begin + data_read_next[3:0] = REVISION; + data_read_next[9:4] = MODEL; + for (i = 0; i < 6; i = i + 1) + data_read_next[i + 4] = OUI[23 - i]; + end + default: begin + if (EMULATE_PULLUP) begin + data_read_next = 16'hFFFF; + end else begin + ack = 0; + err = stb && cyc; + data_read_next = 16'hXXXX; + end + end + endcase + end + + always @(posedge clk) begin + loopback <= loopback_next; + pdown <= pdown_next; + isolate <= isolate_next; + duplex <= duplex_next; + coltest <= coltest_next; + link_status_latched <= link_status_latched_next; + data_read <= data_read_next; + end + + `DUMP(0) + +endmodule diff --git a/tb/mdio_regs.py b/tb/mdio_regs.py new file mode 100644 index 0000000..194bedf --- /dev/null +++ b/tb/mdio_regs.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, Timer +from cocotb.types import LogicArray + +def BIT(n): + return 1 << n + +BMCR = 0 +BMSR = 1 +PHYID1 = 2 +PHYID2 = 3 +EXTSTATUS = 15 + +BMCR_RESET = BIT(15) +BMCR_LOOPBACK = BIT(14) +BMCR_SPEED_LSB = BIT(13) +BMCR_PDOWN = BIT(11) +BMCR_ISOLATE = BIT(10) +BMCR_DUPLEX = BIT(8) +BMCR_COLTEST = BIT(7) +BMCR_SPEED_MSB = BIT(6) + +BMSR_100BASEXFD = BIT(14) +BMSR_100BASEXHD = BIT(13) +BMSR_LSTATUS = BIT(2) +BMSR_EXTCAP = BIT(0) + +@cocotb.test(timeout_time=1, timeout_unit='us') +async def test_mdio(regs): + regs.cyc.value = 1 + regs.stb.value = 0 + regs.link_status.value = 1 + await Timer(1) + await cocotb.start(Clock(regs.clk, 8, units='ns').start()) + + async def xfer(regad, data=None): + await FallingEdge(regs.clk) + regs.stb.value = 1 + regs.addr.value = regad + if data is None: + regs.we.value = 0 + else: + regs.we.value = 1 + regs.data_write.value = data + + await FallingEdge(regs.clk) + assert regs.ack.value or regs.err.value + regs.stb.value = 0 + regs.we.value = LogicArray('X') + regs.addr.value = LogicArray('X' * 4) + regs.data_write.value = LogicArray('X' * 16) + if data is None and regs.ack.value: + return regs.data_read.value + + async def bmcr_toggle(bit, signal): + if signal: + assert not signal.value + await xfer(BMCR, bit) + if signal: + assert signal.value + assert await xfer(BMCR) == (BMCR_SPEED_LSB | bit) + await xfer(BMCR, 0) + if signal: + assert not signal.value + + assert await xfer(BMCR) == (BMCR_SPEED_LSB | BMCR_ISOLATE) + await bmcr_toggle(BMCR_LOOPBACK, regs.loopback) + await bmcr_toggle(BMCR_PDOWN, regs.pdown) + await bmcr_toggle(BMCR_ISOLATE, regs.isolate) + await bmcr_toggle(BMCR_DUPLEX, None) + await bmcr_toggle(BMCR_COLTEST, regs.coltest) + await xfer(BMCR, BMCR_RESET) + assert await xfer(BMCR) == (BMCR_SPEED_LSB | BMCR_ISOLATE) + + await xfer(BMSR, 0xffff) + assert await xfer(BMSR) == (BMSR_100BASEXFD | BMSR_100BASEXHD | BMSR_LSTATUS | BMSR_EXTCAP) + regs.link_status.value = 0 + assert not await xfer(BMSR) & BMSR_LSTATUS + regs.link_status.value = 1 + assert not await xfer(BMSR) & BMSR_LSTATUS + assert await xfer(BMSR) & BMSR_LSTATUS + + await xfer(PHYID1, 0xffff) + assert await xfer(PHYID1) == 0 + + await xfer(PHYID2, 0xffff) + assert await xfer(PHYID2) == 0 + + # I'm pretty sure this register will never be implemented + assert await xfer(EXTSTATUS) is None + assert await xfer(EXTSTATUS, 0) is None