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:
parent
4646500973
commit
f1b345299e
2
Makefile
2
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
|
||||
MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble mdio
|
||||
|
||||
.PHONY: test
|
||||
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES))
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Loading…
Reference in New Issue