Add mdio module

This module implements the MII management interface ("MDIO"), and
translates frames into classic wishbone reads/writes. We use a
"state_counter" to keep track of how many additional bits we expect to
recieve before continuing on to the next field in the frame. We require
a preamble because it prevents ambiguity, and omitting it doesn't seem
to be very popular (seeing as it was removed for c45). Generally, even
if we find an error in the frame, we still procede through the states as
usual. This prevents any spurious reads/writes caused by misinterpreting
an unaligned data stream.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2022-08-27 13:10:59 -04:00
parent 4646500973
commit f1b345299e
3 changed files with 384 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
MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio
.PHONY: test
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES))

214
rtl/mdio.v Normal file
View File

@ -0,0 +1,214 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`include "common.vh"
module mdio (
input clk,
input ce,
input mdi,
output reg mdo,
output reg mdo_valid,
input ack,
output cyc,
output reg stb, we,
output reg [4:0] addr,
output reg [15:0] data_write,
input [15:0] data_read
);
parameter [PHYAD_BITS-1:0] ADDRESS = 0;
localparam IDLE = 0;
localparam PREAMBLE = 1;
localparam ST = 2;
localparam OP = 3;
localparam PHYAD = 4;
localparam REGAD = 5;
localparam TA = 6;
localparam DATA = 7;
localparam PREAMBLE_BITS = 32;
localparam OP_BITS = 2;
localparam PHYAD_BITS = 5;
localparam REGAD_BITS = 5;
localparam TA_BITS = 2;
localparam DATA_BITS = 16;
localparam OP_READ = 2'b10;
localparam OP_WRITE = 2'b01;
reg mdo_next, mdo_valid_next;
reg stb_next, we_next;
initial stb = 0;
reg [4:0] addr_next;
reg [15:0] data_next;
reg bad, bad_next;
reg [2:0] state = IDLE, state_next;
reg [4:0] state_counter, state_counter_next;
/*
* NB: stb_next and data_next are assigned to stb and data_write every
* clock, whereas the other signals are only assigned if ce is high.
* This ensures that no duplicate reads/writes are issued, and that
* data_read is sampled promptly. However, it also means that any
* assignments to these regs in the state machine must be qualified
* by ce.
*/
always @(*) begin
mdo_next = 1'bX;
mdo_valid_next = 0;
stb_next = stb;
we_next = we;
addr_next = addr;
data_next = data_write;
if (stb && ack) begin
stb_next = 0;
data_next = data_read;
end
state_next = state;
state_counter_next = state_counter - 1;
bad_next = bad;
case (state)
IDLE: begin
bad_next = 0;
state_counter_next = PREAMBLE_BITS - 1;
if (mdi)
state_next = PREAMBLE;
end
PREAMBLE: begin
if (!state_counter)
state_counter_next = 0;
if (!mdi) begin
if (state_counter)
state_next = IDLE;
else
state_next = ST;
end
end
ST: begin
if (!mdi)
bad_next = 1;
state_next = OP;
state_counter_next = OP_BITS - 1;
end
OP: begin
/* This is a bit of an abuse of we :) */
we_next = mdi;
if (!state_counter) begin
case ({ we, mdi })
OP_READ: we_next = 0;
OP_WRITE: we_next = 1;
default: bad_next = 1;
endcase
state_next = PHYAD;
state_counter_next = PHYAD_BITS - 1;
end
end
PHYAD: begin
if (mdi != ADDRESS[state_counter])
bad_next = 1;
if (!state_counter) begin
/* Cancel any outstanding transaction */
if (ce)
stb_next = 0;
state_next = REGAD;
state_counter_next = REGAD_BITS - 1;
end
end
REGAD: begin
addr_next = { addr[3:0], mdi };
if (!state_counter) begin
if (ce && !we && !bad)
stb_next = 1;
state_next = TA;
state_counter_next = TA_BITS - 1;
end
end
TA: begin
if (!state_counter) begin
if (!we && !bad) begin
mdo_next = 0;
mdo_valid_next = 1;
if (stb) begin
/* No response */
stb_next = !ce;
bad_next = 1;
mdo_valid_next = 0;
end
end
state_next = DATA;
state_counter_next = DATA_BITS - 1;
end
end
DATA: begin
if (ce && we) begin
data_next = { data_write[14:0], mdi };
end else if (!bad) begin
/* More data_write abuse */
mdo_next = data_write[15];
mdo_valid_next = 1;
if (ce)
data_next = { data_write[14:0], 1'bX };
end
if (!state_counter) begin
if (ce && we && !bad)
stb_next = 1;
bad_next = 0;
state_next = IDLE;
end
end
endcase
end
always @(posedge clk) begin
stb <= stb_next;
data_write <= data_next;
if (ce) begin
mdo <= mdo_next;
mdo_valid <= mdo_valid_next;
we <= we_next;
addr <= addr_next;
state <= state_next;
state_counter <= state_counter_next;
bad <= bad_next;
end
end
/* No multi-beat transactions */
assign cyc = stb;
`ifndef SYNTHESIS
reg [255:0] state_text;
always @(*) begin
case (state)
IDLE: state_text = "IDLE";
PREAMBLE: state_text = "PREAMBLE";
ST: state_text = "ST";
OP: state_text = "OP";
PHYAD: state_text = "PHYAD";
REGAD: state_text = "REGAD";
TA: state_text = "TA";
DATA: state_text = "DATA";
endcase
end
`endif
`DUMP(0)
endmodule

169
tb/mdio.py Normal file
View File

@ -0,0 +1,169 @@
# 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
def to_bits(val, width):
for bit in range(width - 1, -1, -1):
yield (val >> bit) & 1
def frame(phyad, regad, data=None, *, st=0b01, op=None, preamble_bits=32):
for _ in range(preamble_bits):
yield 1
yield from to_bits(st, 2)
if op is None:
op = 0b10 if data is None else 0b01
yield from to_bits(op, 2)
yield from to_bits(phyad, 5)
yield from to_bits(regad, 5)
if data is None:
return
yield 1
yield 0
yield from to_bits(data, 16)
# Should be 50, but reduced to simulate faster
MDIO_RATIO = 3
async def mdio_read(mdio, phyad, regad, **kwargs):
ret = 0
for bit in frame(phyad, regad, **kwargs):
await RisingEdge(mdio.ce)
mdio.mdi.value = bit
await FallingEdge(mdio.ce)
mdio.mdi.value = LogicArray('X')
for bit in range(19):
await RisingEdge(mdio.ce)
await RisingEdge(mdio.clk)
if bit < 2:
continue
if bit == 2:
if not mdio.mdo_valid.value:
ret = None
continue
if ret is None:
assert not mdio.mdo_valid.value
continue
else:
assert mdio.mdo_valid.value
ret <<= 1
ret |= mdio.mdo.value
return ret
async def mdio_write(mdio, phyad, regad, data, **kwargs):
for bit in frame(phyad, regad, data, **kwargs):
await RisingEdge(mdio.ce)
mdio.mdi.value = bit
await FallingEdge(mdio.ce)
mdio.mdi.value = LogicArray('X')
async def wb_read(mdio, addr, data):
while not (mdio.cyc.value and mdio.stb.value):
await FallingEdge(mdio.clk)
assert not mdio.we.value
assert mdio.addr.value == addr
mdio.data_read.value = data
mdio.ack.value = 1
await RisingEdge(mdio.clk)
mdio.ack.value = 0
mdio.data_read.value = LogicArray('X' * 16)
await FallingEdge(mdio.clk)
assert not mdio.stb.value
async def wb_write(mdio, addr, data):
while not (mdio.cyc.value and mdio.stb.value):
await FallingEdge(mdio.clk)
mdio.ack.value = 1
await RisingEdge(mdio.clk)
assert mdio.we.value
assert mdio.addr.value == addr
assert mdio.data_write.value == data
mdio.ack.value = 0
await FallingEdge(mdio.clk)
assert not mdio.stb.value
async def setup(mdio):
mdio.mdi.value = 0
mdio.ack.value = 0
mdio.data_read.value = LogicArray('X' * 16)
await cocotb.start(ClockEnable(mdio.clk, mdio.ce, MDIO_RATIO))
await Timer(1)
await cocotb.start(Clock(mdio.clk, 8, units='ns').start())
@cocotb.test(timeout_time=50, timeout_unit='us')
async def test_mdio(mdio):
await setup(mdio)
reads = [(i, random.randrange(0, 0xFFFF)) for i in range(16)]
writes = [(i, random.randrange(0, 0xFFFF)) for i in range(16)]
random.shuffle(reads)
random.shuffle(writes)
async def rw_mdio():
for (read, write) in zip(reads, writes):
assert await mdio_read(mdio, 0, read[0]) == read[1]
await mdio_write(mdio, 0, write[0], write[1])
await cocotb.start(rw_mdio())
for (read, write) in zip(reads, writes):
await wb_read(mdio, read[0], read[1])
await wb_write(mdio, write[0], write[1])
@cocotb.test(timeout_time=20, timeout_unit='us')
async def test_badmdio(mdio):
await setup(mdio)
async def nowb():
await First(RisingEdge(mdio.cyc), RisingEdge(mdio.stb))
assert False, "Unexpected wishbone transaction"
await cocotb.start(nowb())
# Force mdi low to ensure we get exactly 31 bits in the preamble
async def rw_mdio(phyad, **kwargs):
mdio.mdi.value = 0
await FallingEdge(mdio.clk)
assert await mdio_read(mdio, phyad, 0, **kwargs) is None
mdio.mdi.value = 0
await FallingEdge(mdio.clk)
await mdio_write(mdio, phyad, 0, 0, **kwargs)
await rw_mdio(0, preamble_bits=31)
await rw_mdio(0, st=0b00)
await rw_mdio(0, op=0b00)
await rw_mdio(1)
await rw_mdio(0x10)
@cocotb.test(timeout_time=20, timeout_unit='us')
async def test_badwb(mdio):
await setup(mdio)
async def try_mdio():
assert await mdio_read(mdio, 0, 0) is None
await mdio_write(mdio, 0, 0, 0)
assert await mdio_read(mdio, 0, 0) is None
await cocotb.start(try_mdio())
await ClockCycles(mdio.stb, 3, False)