From 1d65661bd31289ffd76050d50489b0ccf34a6c35 Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Mon, 23 May 2022 21:03:10 -0400 Subject: [PATCH] Add pmd --- rtl/io.vh | 19 +++++ rtl/pmd.v | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tb/pmd.py | 64 ++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 rtl/io.vh create mode 100644 rtl/pmd.v create mode 100644 tb/pmd.py diff --git a/rtl/io.vh b/rtl/io.vh new file mode 100644 index 0000000..c21e8d8 --- /dev/null +++ b/rtl/io.vh @@ -0,0 +1,19 @@ +`ifndef IO_VH +`define IO_VH + +`define PIN_INPUT_REGISTERED 6'b000000 +`define PIN_INPUT_UNREGISTERED 6'b000001 +`define PIN_INPUT_LATCH 6'b000010 +`define PIN_INPUT_DDR 6'b000000 + +`define PIN_OUTPUT_NEVER 6'b000000 +`define PIN_OUTPUT_ALWAYS 6'b010000 +`define PIN_OUTPUT_ENABLE 6'b100000 +`define PIN_OUTPUT_ENABLE_REGISTERED 6'b110000 + +`define PIN_OUTPUT_DDR 6'b000000 +`define PIN_OUTPUT_REGISTERED 6'b000100 +`define PIN_OUTPUT_UNREGISTERED 6'b001000 +`define PIN_OUTPUT_REGISTERED_INVERTED 6'b001100 + +`endif /* IO_VH */ diff --git a/rtl/pmd.v b/rtl/pmd.v new file mode 100644 index 0000000..cff7d7a --- /dev/null +++ b/rtl/pmd.v @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + * + * This roughly follows the design of XAPP225. However, we use a 2x rate DDR + * clock instead of two clocks 90 degrees out of phase. Yosys/nextpnr cannot + * guarantee the phase relationship of any clocks, even those from the same + * PLL. Because of this, we assume that rx_clk_250 and rx_clk_125 are unrelated. + */ + +`include "common.vh" +`include "io.vh" + +`timescale 1ns/1ps + +module pmd ( + input tx_clk, + input rx_clk_250, + input rx_clk_125, + + input signal_detect, + output reg tx_p, tx_n, + input rx, + + /* PMD */ + output reg signal_status, + input pmd_data_tx, + output reg [1:0] pmd_data_rx, + output reg [1:0] pmd_data_rx_valid +); + + reg [1:0] rx_p, rx_n; + +`ifdef SYNTHESIS + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_NEVER | `PIN_INPUT_REGISTERED), + .IO_STANDARD("SB_LVDS_INPUT") + ) signal_detect_pin ( + .PACKAGE_PIN(signal_detect), + .INPUT_rx_clk(rx_clk_125), + .D_IN_0(signal_status) + ); + + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_NEVER | `PIN_INPUT_DDR), + .IO_STANDARD("SB_LVDS_INPUT") + ) data_rx_pin ( + .PACKAGE_PIN(rx), + .INPUT_rx_clk(rx_clk_250), + .D_IN_0(rx_p[0]), + .D_IN_1(rx_n[0]) + ); +`else + always @(posedge rx_clk_125) + signal_status <= signal_detect; + + always @(posedge rx_clk_250) + rx_p[0] <= rx; + + always @(negedge rx_clk_250) + rx_n[0] <= rx; +`endif + + /* + * Get things into the rx_clk_250 domain so that we sample posedge before + * negedge. Without this we can have a negedge which happens before the + * posedge. + */ + always @(posedge rx_clk_250) begin + rx_p[1] = rx_p[0]; + rx_n[1] = rx_n[0]; + end + + reg [2:0] rx_a, rx_b, rx_c, rx_d; + + /* Get everything in the rx_clk_125 domain */ + always @(posedge rx_clk_125) begin + rx_a[0] <= rx_p[1]; + rx_b[0] <= rx_n[1]; + end + + always @(negedge rx_clk_125) begin + rx_c[0] <= rx_p[1]; + rx_d[0] <= rx_n[1]; + end + + /* + * Buffer things a bit. We wait a cycle to avoid metastability. After + * that, we need two cycles of history to detect edges. + */ + always @(posedge rx_clk_125) begin + rx_a[2:1] = rx_a[1:0]; + rx_b[2:1] = rx_b[1:0]; + rx_c[2:1] = rx_c[1:0]; + rx_d[2:1] = rx_d[1:0]; + end + + localparam A = 0; + localparam B = 1; + localparam C = 2; + localparam D = 3; + + reg [1:0] state, state_next; + initial state = A; + reg valid, valid_next; + initial valid = 0; + reg [1:0] pmd_data_rx_next, pmd_data_rx_valid_next; + reg [3:0] rx_r, rx_f; + + always @(*) begin + rx_r = { + rx_a[1] & ~rx_a[2], + rx_b[1] & ~rx_b[2], + rx_c[1] & ~rx_c[2], + rx_d[1] & ~rx_d[2] + }; + + rx_f = { + ~rx_a[1] & rx_a[2], + ~rx_b[1] & rx_b[2], + ~rx_c[1] & rx_c[2], + ~rx_d[1] & rx_d[2] + }; + + state_next = state; + valid_next = 1; + if (rx_r == 4'b1111 || rx_f == 4'b1111) + state_next = C; + else if (rx_r == 4'b1000 || rx_f == 4'b1000) + state_next = D; + else if (rx_r == 4'b1100 || rx_f == 4'b1100) + state_next = A; + else if (rx_r == 4'b1110 || rx_f == 4'b1110) + state_next = B; + else + valid_next = valid; + + if (!signal_status) + valid_next = 0; + + pmd_data_rx_next[0] = rx_d[2]; + pmd_data_rx_valid_next = 1; + case (state_next) + A: begin + pmd_data_rx_next[1] = rx_a[2]; + if (state == D) + pmd_data_rx_valid_next = 0; + end + B: begin + pmd_data_rx_next[1] = rx_b[2]; + end + C: begin + pmd_data_rx_next[1] = rx_c[2]; + end + D: begin + pmd_data_rx_next[1] = rx_d[2]; + if (state == A) begin + pmd_data_rx_next[1] = rx_a[2]; + pmd_data_rx_valid_next = 2; + end + end + endcase + + if (!valid_next) + pmd_data_rx_valid_next = 0; + end + + always @(posedge rx_clk_125) begin + state <= state_next; + valid <= valid_next; + pmd_data_rx <= pmd_data_rx_next; + pmd_data_rx_valid <= pmd_data_rx_valid_next; + end + +`ifdef SYNTHESIS + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED), + .IO_STANDARD("SB_LVDS_INPUT") + ) data_txp_pin ( + .PACKAGE_PIN(tx_p), + .OUTPUT_rx_clk(rx_clk_125), + .D_OUT_0(pmd_data_tx) + ); + + SB_IO #( + .PIN_TYPE(`PIN_OUTPUT_ALWAYS | `PIN_OUTPUT_REGISTERED_INVERTED), + .IO_STANDARD("SB_LVDS_INPUT") + ) data_txn_pin ( + .PACKAGE_PIN(tx_n), + .OUTPUT_rx_clk(rx_clk_125), + .D_OUT_0(pmd_data_tx) + ); +`else + always @(posedge tx_clk) begin + tx_p <= pmd_data_tx; + tx_n <= ~pmd_data_tx; + end +`endif + +`ifndef SYNTHESIS + reg [255:0] state_text; + input [13:0] delay; + + always @(*) begin + case (state) + A: state_text = "A"; + B: state_text = "B"; + C: state_text = "C"; + D: state_text = "D"; + endcase + end +`endif + + `DUMP + +endmodule diff --git a/tb/pmd.py b/tb/pmd.py new file mode 100644 index 0000000..d5b4a09 --- /dev/null +++ b/tb/pmd.py @@ -0,0 +1,64 @@ +import random +from statistics import NormalDist + +import cocotb +from cocotb.binary import BinaryValue +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer + +@cocotb.test(timeout_time=100, timeout_unit='us') +async def test_rx(pmd): + pmd.rx.value = BinaryValue('X') + await cocotb.start(Clock(pmd.rx_clk_125, 8, units='ns').start()) + # random phase + await Timer(random.randrange(0, 8000), units='ps') + await cocotb.start(Clock(pmd.rx_clk_250, 4, units='ns').start()) + + ins = [random.randrange(2) for _ in range(1000)] + async def generate_bits(): + # random phase + await Timer(random.randrange(0, 8000), units='ps') + pmd.signal_detect.value = 1 + # Target BER is 1e9 and the maximum jitter is 1.4ns + # This is just random jitter (not DDJ or DCJ) but it'll do + delay_dist = NormalDist(8000, 1400 / NormalDist().inv_cdf(1-2e-9)) + for i, delay in zip(ins, (int(delay) for delay in delay_dist.samples(len(ins)))): + pmd.rx.value = i + pmd.delay.value = delay + await Timer(delay, units='ps') + #await Timer(8100, units='ps') + pmd.signal_detect.value = 0 + await cocotb.start(generate_bits()) + + # Wait for things to stabilize + await RisingEdge(pmd.valid) + outs = [] + while pmd.signal_status.value: + await RisingEdge(pmd.rx_clk_125) + valid = pmd.pmd_data_rx_valid.value + if valid == 0: + pass + elif valid == 1: + outs.append(pmd.pmd_data_rx[1].value) + else: + outs.append(pmd.pmd_data_rx[1].value) + outs.append(pmd.pmd_data_rx[0].value) + + best_corr = -1 + best_off = None + for off in range(-7, 8): + corr = sum(i == o for i, o in zip(ins[off:], outs)) + if corr > best_corr: + best_corr = corr + best_off = off + + print(f"best offset is {best_off} correlation {best_corr/(len(ins) - best_off)}") + for idx, (i, o) in enumerate(zip(ins[best_off:], outs)): + if i != o: + print(idx) + print(*ins[idx+best_off-50:idx+best_off+50], sep='') + print(*outs[idx-50:idx+50], sep='') + assert False + # There will be a few bits at the end not recorded because signal_detect + # isn't delayed like the data signals + assert best_corr > len(ins) - best_off - 10