pcs: Split into rx/tx

For easier integration, split the PCS into its rx and tx components.
This was already done on the module level, but now they live in separate
files.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2022-10-30 21:32:02 -04:00
parent c02d3f3ad0
commit 494ef2a2a9
6 changed files with 390 additions and 404 deletions

36
rtl/pcs.vh Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`ifndef PCS_VH
`define PCS_VH
/* 4b5b code groups */
`define CODE_0 5'b11110
`define CODE_1 5'b01001
`define CODE_2 5'b10100
`define CODE_3 5'b10101
`define CODE_4 5'b01010
`define CODE_5 5'b01011
`define CODE_6 5'b01110
`define CODE_7 5'b01111
`define CODE_8 5'b10010
`define CODE_9 5'b10011
`define CODE_A 5'b10110
`define CODE_B 5'b10111
`define CODE_C 5'b11010
`define CODE_D 5'b11011
`define CODE_E 5'b11100
`define CODE_F 5'b11101
`define CODE_I 5'b11111
`define CODE_J 5'b11000
`define CODE_K 5'b10001
`define CODE_T 5'b01101
`define CODE_R 5'b00111
`define CODE_H 5'b00100
`endif /* PCS_VH */

View File

@ -4,246 +4,10 @@
*/
`include "common.vh"
/* 4b5b code groups */
`define CODE_0 5'b11110
`define CODE_1 5'b01001
`define CODE_2 5'b10100
`define CODE_3 5'b10101
`define CODE_4 5'b01010
`define CODE_5 5'b01011
`define CODE_6 5'b01110
`define CODE_7 5'b01111
`define CODE_8 5'b10010
`define CODE_9 5'b10011
`define CODE_A 5'b10110
`define CODE_B 5'b10111
`define CODE_C 5'b11010
`define CODE_D 5'b11011
`define CODE_E 5'b11100
`define CODE_F 5'b11101
`define CODE_I 5'b11111
`define CODE_J 5'b11000
`define CODE_K 5'b10001
`define CODE_T 5'b01101
`define CODE_R 5'b00111
`define CODE_H 5'b00100
`include "pcs.vh"
`timescale 1ns/1ns
module pcs (
/* MII */
input tx_clk,
input tx_ce,
input tx_en,
input [3:0] txd,
input tx_er,
input rx_clk,
output rx_ce,
output rx_dv,
output [3:0] rxd,
output rx_er,
output crs,
output col,
/* PMA */
output pma_tx_data,
input [1:0] pma_rx_data,
input [1:0] pma_rx_data_valid,
input link_status
);
wire transmitting, receiving;
pcs_tx tx (
.clk(tx_clk),
.ce(tx_ce),
.enable(tx_en),
.data(txd),
.err(tx_er),
.bits(pma_tx_data),
.link_status(link_status),
.tx(transmitting)
);
pcs_rx rx (
.clk(rx_clk),
.ce(rx_ce),
.valid(rx_dv),
.data(rxd),
.err(rx_er),
.bits(pma_rx_data),
.bits_valid(pma_rx_data_valid),
.link_status(link_status),
.rx(receiving)
);
/*
* NB: These signals are not required to be in any particular clock
* domain.
*/
assign col = transmitting && receiving;
assign crs = transmitting || receiving;
endmodule
/* Transmit process */
module pcs_tx (
/* MII */
input clk,
input ce,
input enable,
input [3:0] data,
input err,
/* PMA */
output bits,
input link_status,
/* Internal */
output reg tx
);
localparam IDLE = 0;
localparam START_J = 1;
localparam START_K = 2;
localparam ERROR_J = 3;
localparam ERROR_K = 4;
localparam ERROR = 5;
localparam DATA = 6;
localparam END_T = 7;
localparam END_R = 8;
reg [3:0] last_data;
reg tx_next;
reg [4:0] code, code_next;
reg [3:0] state, state_next;
initial tx = 0;
initial code = `CODE_I;
initial state = IDLE;
always @(*) begin
case (last_data)
4'h0: code_next = `CODE_0;
4'h1: code_next = `CODE_1;
4'h2: code_next = `CODE_2;
4'h3: code_next = `CODE_3;
4'h4: code_next = `CODE_4;
4'h5: code_next = `CODE_5;
4'h6: code_next = `CODE_6;
4'h7: code_next = `CODE_7;
4'h8: code_next = `CODE_8;
4'h9: code_next = `CODE_9;
4'hA: code_next = `CODE_A;
4'hB: code_next = `CODE_B;
4'hC: code_next = `CODE_C;
4'hD: code_next = `CODE_D;
4'hE: code_next = `CODE_E;
4'hF: code_next = `CODE_F;
endcase
tx_next = tx;
if (enable) begin
if (err)
state_next = ERROR;
else
state_next = DATA;
end else begin
state_next = END_T;
end
case (state)
IDLE: begin
tx_next = 0;
code_next = `CODE_I;
state_next = IDLE;
if (enable) begin
tx_next = 1;
if (err)
state_next = ERROR_J;
else
state_next = START_J;
end
end
START_J: begin
code_next = `CODE_J;
if (err)
state_next = ERROR_K;
else
state_next = START_K;
end
START_K: begin
code_next = `CODE_K;
end
ERROR_J: begin
code_next = `CODE_J;
state_next = ERROR_K;
end
ERROR_K: begin
code_next = `CODE_K;
state_next = ERROR;
end
ERROR: begin
code_next = `CODE_H;
end
DATA: ;
END_T: begin
tx_next = 0;
code_next = `CODE_T;
state_next = END_R;
end
END_R: begin
code_next = `CODE_R;
state_next = IDLE;
end
endcase
if (!link_status) begin
tx_next = 0;
code_next = `CODE_I;
state_next = IDLE;
end
end
always @(posedge clk) begin
if (ce) begin
last_data <= data;
tx <= tx_next;
code <= code_next;
state <= state_next;
end else begin
code <= code << 1;
end
end
`ifndef SYNTHESIS
reg [255:0] state_text;
always @(*) begin
case (state)
IDLE: state_text = "IDLE";
START_J: state_text = "START_J";
START_K: state_text = "START_K";
ERROR_J: state_text = "ERROR_J";
ERROR_K: state_text = "ERROR_K";
ERROR: state_text = "ERROR";
DATA: state_text = "DATA";
END_T: state_text = "END_T";
END_R: state_text = "END_R";
endcase
end
`endif
/* Transmit bits process */
assign bits = code[4];
endmodule
module pcs_rx_bits (
input clk,
/*

161
rtl/pcs_tx.v Normal file
View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`include "common.vh"
`include "pcs.vh"
`timescale 1ns/1ns
/* Transmit process */
module pcs_tx (
/* MII */
input clk,
input ce,
input enable,
input [3:0] data,
input err,
/* PMA */
output bits,
input link_status,
/* Internal */
output reg tx
);
localparam IDLE = 0;
localparam START_J = 1;
localparam START_K = 2;
localparam ERROR_J = 3;
localparam ERROR_K = 4;
localparam ERROR = 5;
localparam DATA = 6;
localparam END_T = 7;
localparam END_R = 8;
reg [3:0] last_data;
reg tx_next;
reg [4:0] code, code_next;
reg [3:0] state, state_next;
initial tx = 0;
initial code = `CODE_I;
initial state = IDLE;
always @(*) begin
case (last_data)
4'h0: code_next = `CODE_0;
4'h1: code_next = `CODE_1;
4'h2: code_next = `CODE_2;
4'h3: code_next = `CODE_3;
4'h4: code_next = `CODE_4;
4'h5: code_next = `CODE_5;
4'h6: code_next = `CODE_6;
4'h7: code_next = `CODE_7;
4'h8: code_next = `CODE_8;
4'h9: code_next = `CODE_9;
4'hA: code_next = `CODE_A;
4'hB: code_next = `CODE_B;
4'hC: code_next = `CODE_C;
4'hD: code_next = `CODE_D;
4'hE: code_next = `CODE_E;
4'hF: code_next = `CODE_F;
endcase
tx_next = tx;
if (enable) begin
if (err)
state_next = ERROR;
else
state_next = DATA;
end else begin
state_next = END_T;
end
case (state)
IDLE: begin
tx_next = 0;
code_next = `CODE_I;
state_next = IDLE;
if (enable) begin
tx_next = 1;
if (err)
state_next = ERROR_J;
else
state_next = START_J;
end
end
START_J: begin
code_next = `CODE_J;
if (err)
state_next = ERROR_K;
else
state_next = START_K;
end
START_K: begin
code_next = `CODE_K;
end
ERROR_J: begin
code_next = `CODE_J;
state_next = ERROR_K;
end
ERROR_K: begin
code_next = `CODE_K;
state_next = ERROR;
end
ERROR: begin
code_next = `CODE_H;
end
DATA: ;
END_T: begin
tx_next = 0;
code_next = `CODE_T;
state_next = END_R;
end
END_R: begin
code_next = `CODE_R;
state_next = IDLE;
end
endcase
if (!link_status) begin
tx_next = 0;
code_next = `CODE_I;
state_next = IDLE;
end
end
always @(posedge clk) begin
if (ce) begin
last_data <= data;
tx <= tx_next;
code <= code_next;
state <= state_next;
end else begin
code <= code << 1;
end
end
`ifndef SYNTHESIS
reg [255:0] state_text;
always @(*) begin
case (state)
IDLE: state_text = "IDLE";
START_J: state_text = "START_J";
START_K: state_text = "START_K";
ERROR_J: state_text = "ERROR_J";
ERROR_K: state_text = "ERROR_K";
ERROR: state_text = "ERROR";
DATA: state_text = "DATA";
END_T: state_text = "END_T";
END_R: state_text = "END_R";
endcase
end
`endif
/* Transmit bits process */
assign bits = code[4];
endmodule

168
tb/pcs.py
View File

@ -4,14 +4,7 @@
import enum
import itertools
import cocotb
from cocotb.clock import Clock
from cocotb.regression import TestFactory
from cocotb.triggers import ClockCycles, Edge, RisingEdge, FallingEdge, Timer
from cocotb.types import LogicArray
from .util import alist, ClockEnable, classproperty, ReverseList, send_recovered_bits, \
timeout, with_valids
from .util import classproperty
class Code(enum.Enum):
_0 = (0b11110, '0')
@ -103,162 +96,3 @@ def as_nibbles(data):
for byte in data:
yield byte >> 4
yield byte & 0xf
def as_codes(nibbles):
for nibble in nibbles:
if nibble is None:
yield Code('H')
else:
yield Code.encode[nibble]
def frame(data):
return itertools.chain(
(Code('J'), Code('K')),
# Chop off the SSD
as_codes(data[2:]),
(Code('T'), Code('R')),
)
async def mii_send_packet(pcs, nibbles):
await FallingEdge(pcs.tx_ce)
for nibble in nibbles:
pcs.tx_en.value = 1
pcs.tx_er.value = 0
if nibble is None:
pcs.tx_er.value = 1
else:
pcs.txd.value = nibble
await FallingEdge(pcs.tx_ce)
pcs.tx_en.value = 0
pcs.tx_er.value = 0
pcs.txd.value = LogicArray("XXXX")
await FallingEdge(pcs.tx_ce)
async def mii_recv_packet(pcs):
while not (pcs.rx_ce.value and pcs.rx_dv.value):
await RisingEdge(pcs.rx_clk)
while pcs.rx_dv.value:
if pcs.rx_ce.value:
if pcs.rx_er.value:
yield None
else:
yield pcs.rxd.value
await RisingEdge(pcs.rx_clk)
class PCSError(Exception):
pass
class BadSSD(PCSError):
pass
class PrematureEnd(PCSError):
pass
async def pcs_recv_packet(pcs):
rx_bits = ReverseList([1] * 10)
async def read_bit():
await RisingEdge(pcs.tx_clk)
rx_bits.append(pcs.pma_tx_data.value)
async def read_code():
for _ in range(5):
await read_bit()
async def bad_ssd():
while not all(rx_bits[9:0]):
await read_bit()
raise BadSSDError()
while all(rx_bits[9:2]) or rx_bits[0]:
await read_bit()
if Code.decode(rx_bits[9:5]) != Code('I') or \
Code.decode(rx_bits[4:0]) != Code('J'):
await bad_ssd()
await read_code()
if Code.decode(rx_bits[4:0]) != Code('K'):
await bad_ssd()
yield 0x5
await read_code()
yield 0x5
while any(rx_bits[9:0]):
await read_code()
code = Code.decode(rx_bits[9:5])
if code == Code('T') and Code.decode(rx_bits[4:0]) == Code('R'):
return
yield code.data
raise PrematureEndError()
async def pcs_send_codes(pcs, codes, valids):
await send_recovered_bits(pcs.rx_clk, pcs.pma_rx_data, pcs.pma_rx_data_valid,
itertools.chain(*codes), valids)
@cocotb.test(timeout_time=10, timeout_unit='us')
async def test_tx(pcs):
pcs.tx_en.value = 0
pcs.tx_er.value = 0
pcs.txd.value = LogicArray("XXXX")
pcs.link_status.value = 1
await cocotb.start(ClockEnable(pcs.tx_clk, pcs.tx_ce, 5))
await Timer(1)
await cocotb.start(Clock(pcs.tx_clk, 8, units='ns').start())
await FallingEdge(pcs.tx_ce)
# Test that all bytes can be transmitted
packet = list(as_nibbles((0x55, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF)))
# And ensure errors are propagated
packet.insert(10, None)
await cocotb.start(mii_send_packet(pcs, packet))
assert packet == await alist(pcs_recv_packet(pcs))
# Test start errors
await cocotb.start(mii_send_packet(pcs, [None]))
assert [0x5, 0x5, None] == await alist(pcs_recv_packet(pcs))
await cocotb.start(mii_send_packet(pcs, [0x5, None]))
assert [0x5, 0x5, None] == await alist(pcs_recv_packet(pcs))
@timeout(10, 'us')
async def test_rx(pcs, valids):
pcs.pma_rx_data.value = LogicArray('11')
pcs.pma_rx_data_valid.value = 2
pcs.link_status.value = 1
await Timer(1)
await cocotb.start(Clock(pcs.rx_clk, 8, units='ns').start())
packet = list(as_nibbles((0x55, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF)))
# And test errors too
packet.insert(10, None)
await cocotb.start(pcs_send_codes(pcs, itertools.chain(
frame(packet),
# Bad SSDs
(Code('C'), Code('I'), Code('I')),
(Code('J'), Code('I'), Code('I')),
(Code('J'), Code('H'), Code('I'), Code('I')),
# Premature end, plus two clocks since we don't have instant turnaround
(Code('J'), Code('K'), Code('I'), Code('I'), (1,1)),
# Packet spacing
*((*frame([0x55, 0x55]), (1,) * i) for i in range(10))
), valids))
assert packet == await alist(mii_recv_packet(pcs))
for _ in range(3):
while not (pcs.receiving.value and pcs.rx_er.value and pcs.rx_ce.value):
await RisingEdge(pcs.rx_clk)
assert pcs.rxd.value == 0xE
await FallingEdge(pcs.receiving)
assert [0x5, 0x5, None] == await alist(mii_recv_packet(pcs))
# Test packet spacing
for _ in range(10):
assert [0x5, 0x5] == await alist(mii_recv_packet(pcs))
with_valids(globals(), test_rx)

83
tb/pcs_rx.py Normal file
View File

@ -0,0 +1,83 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
import itertools
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, FallingEdge, Timer
from cocotb.types import LogicArray
from .pcs import Code, as_nibbles
from .util import alist, send_recovered_bits, timeout, with_valids
def as_codes(nibbles):
for nibble in nibbles:
if nibble is None:
yield Code('H')
else:
yield Code.encode[nibble]
def frame(data):
return itertools.chain(
(Code('J'), Code('K')),
# Chop off the SSD
as_codes(data[2:]),
(Code('T'), Code('R')),
)
async def mii_recv_packet(pcs):
while not (pcs.ce.value and pcs.valid.value):
await RisingEdge(pcs.clk)
while pcs.valid.value:
if pcs.ce.value:
if pcs.err.value:
yield None
else:
yield pcs.data.value
await RisingEdge(pcs.clk)
async def pcs_send_codes(pcs, codes, valids):
await send_recovered_bits(pcs.clk, pcs.bits, pcs.bits_valid,
itertools.chain(*codes), valids)
@timeout(10, 'us')
async def test_rx(pcs, valids):
pcs.bits.value = LogicArray('11')
pcs.bits_valid.value = 2
pcs.link_status.value = 1
await Timer(1)
await cocotb.start(Clock(pcs.clk, 8, units='ns').start())
packet = list(as_nibbles((0x55, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF)))
# And test errors too
packet.insert(10, None)
await cocotb.start(pcs_send_codes(pcs, itertools.chain(
frame(packet),
# Bad SSDs
(Code('C'), Code('I'), Code('I')),
(Code('J'), Code('I'), Code('I')),
(Code('J'), Code('H'), Code('I'), Code('I')),
# Premature end, plus two clocks since we don't have instant turnaround
(Code('J'), Code('K'), Code('I'), Code('I'), (1,1)),
# Packet spacing
*((*frame([0x55, 0x55]), (1,) * i) for i in range(10))
), valids))
assert packet == await alist(mii_recv_packet(pcs))
for _ in range(3):
while not (pcs.rx.value and pcs.err.value and pcs.ce.value):
await RisingEdge(pcs.clk)
assert pcs.data.value == 0xE
await FallingEdge(pcs.rx)
assert [0x5, 0x5, None] == await alist(mii_recv_packet(pcs))
# Test packet spacing
for _ in range(10):
assert [0x5, 0x5] == await alist(mii_recv_packet(pcs))
with_valids(globals(), test_rx)

108
tb/pcs_tx.py Normal file
View File

@ -0,0 +1,108 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import ClockCycles, RisingEdge, FallingEdge, Timer
from cocotb.types import LogicArray
from .pcs import Code, as_nibbles
from .util import alist, ClockEnable, ReverseList
async def mii_send_packet(pcs, nibbles):
await FallingEdge(pcs.ce)
for nibble in nibbles:
pcs.enable.value = 1
pcs.err.value = 0
if nibble is None:
pcs.err.value = 1
else:
pcs.data.value = nibble
await FallingEdge(pcs.ce)
pcs.enable.value = 0
pcs.err.value = 0
pcs.data.value = LogicArray("XXXX")
await FallingEdge(pcs.ce)
class PCSError(Exception):
pass
class BadSSD(PCSError):
pass
class PrematureEnd(PCSError):
pass
async def pcs_recv_bits(pcs, data=None):
if data is None:
data = pcs.bits
while True:
await RisingEdge(pcs.clk)
yield data.value
async def pcs_recv_packet(pcs, bits=None):
if bits is None:
bits = pcs_recv_bits(pcs)
rx_bits = ReverseList([1] * 10)
async def read_bit():
rx_bits.append(await anext(bits))
async def read_code():
for _ in range(5):
await read_bit()
async def bad_ssd():
while not all(rx_bits[9:0]):
await read_bit()
raise BadSSDError()
while all(rx_bits[9:2]) or rx_bits[0]:
await read_bit()
if Code.decode(rx_bits[9:5]) != Code('I') or \
Code.decode(rx_bits[4:0]) != Code('J'):
await bad_ssd()
await read_code()
if Code.decode(rx_bits[4:0]) != Code('K'):
await bad_ssd()
yield 0x5
await read_code()
yield 0x5
while any(rx_bits[9:0]):
await read_code()
code = Code.decode(rx_bits[9:5])
if code == Code('T') and Code.decode(rx_bits[4:0]) == Code('R'):
return
yield code.data
raise PrematureEndError()
@cocotb.test(timeout_time=10, timeout_unit='us')
async def test_tx(pcs):
pcs.enable.value = 0
pcs.err.value = 0
pcs.data.value = LogicArray("XXXX")
pcs.link_status.value = 1
await cocotb.start(ClockEnable(pcs.clk, pcs.ce, 5))
await Timer(1)
await cocotb.start(Clock(pcs.clk, 8, units='ns').start())
await FallingEdge(pcs.ce)
# Test that all bytes can be transmitted
packet = list(as_nibbles((0x55, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF)))
# And ensure errors are propagated
packet.insert(10, None)
await cocotb.start(mii_send_packet(pcs, packet))
assert packet == await alist(pcs_recv_packet(pcs))
# Test start errors
await cocotb.start(mii_send_packet(pcs, [None]))
assert [0x5, 0x5, None] == await alist(pcs_recv_packet(pcs))
await cocotb.start(mii_send_packet(pcs, [0x5, None]))
assert [0x5, 0x5, None] == await alist(pcs_recv_packet(pcs))