From 494ef2a2a99284d75cd23f6cf9d17b20d4d7e746 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sun, 30 Oct 2022 21:32:02 -0400 Subject: [PATCH] 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 --- rtl/pcs.vh | 36 ++++++ rtl/{pcs.v => pcs_rx.v} | 238 +--------------------------------------- rtl/pcs_tx.v | 161 +++++++++++++++++++++++++++ tb/pcs.py | 168 +--------------------------- tb/pcs_rx.py | 83 ++++++++++++++ tb/pcs_tx.py | 108 ++++++++++++++++++ 6 files changed, 390 insertions(+), 404 deletions(-) create mode 100644 rtl/pcs.vh rename rtl/{pcs.v => pcs_rx.v} (69%) create mode 100644 rtl/pcs_tx.v create mode 100644 tb/pcs_rx.py create mode 100644 tb/pcs_tx.py diff --git a/rtl/pcs.vh b/rtl/pcs.vh new file mode 100644 index 0000000..4c8fe71 --- /dev/null +++ b/rtl/pcs.vh @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`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 */ diff --git a/rtl/pcs.v b/rtl/pcs_rx.v similarity index 69% rename from rtl/pcs.v rename to rtl/pcs_rx.v index 7a343b9..a718a07 100644 --- a/rtl/pcs.v +++ b/rtl/pcs_rx.v @@ -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, /* diff --git a/rtl/pcs_tx.v b/rtl/pcs_tx.v new file mode 100644 index 0000000..3f7ec5b --- /dev/null +++ b/rtl/pcs_tx.v @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`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 diff --git a/tb/pcs.py b/tb/pcs.py index 08cf065..c3c0f84 100644 --- a/tb/pcs.py +++ b/tb/pcs.py @@ -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) diff --git a/tb/pcs_rx.py b/tb/pcs_rx.py new file mode 100644 index 0000000..50b4f36 --- /dev/null +++ b/tb/pcs_rx.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +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) diff --git a/tb/pcs_tx.py b/tb/pcs_tx.py new file mode 100644 index 0000000..550e02c --- /dev/null +++ b/tb/pcs_tx.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +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))