diff --git a/Makefile b/Makefile index b6276a3..61a4420 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ endef %.post.fst: rtl/%.post.vvp tb/%.py FORCE $(run-vvp) -MODULES := pcs pmd nrzi_encode nrzi_decode +MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble .PHONY: test test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES)) diff --git a/rtl/descramble.v b/rtl/descramble.v new file mode 100644 index 0000000..620e065 --- /dev/null +++ b/rtl/descramble.v @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" + +module descramble ( + input clk, + input [1:0] scrambled, scrambled_valid, + input signal_status, test_mode, + output reg locked, + output reg [1:0] unscrambled, unscrambled_valid +); + + reg locked_next; + reg [1:0] ldd, unscrambled_next; + reg [10:0] lfsr, lfsr_next; + + /* + * The number of consecutive idle bits to require when locking, as + * well as the number necessary to prevent unlocking. For the first + * case, this must be less than 60 bits (7.2.3.1.1), including the + * bits necessary to initialize the lfsr. For the second, this must be + * less than 29 bits (7.2.3.3(f)). We use 29 to meet these requirements; + * it is increased by 1 to allow for an easier implementation of the + * counter, and decreased by 1 to allow easier implementation when + * scrambled_valid = 2. The end result is that only 28 bits might be + * required in certain situations. + */ + localparam CONSECUTIVE_IDLES = 5'd29; + reg [4:0] idle_counter = CONSECUTIVE_IDLES, idle_counter_next; + + /* + * The amount of time without recieving consecutive idles before we + * unlock. This must be greater than 361us (7.2.3.3(f)). 2^16-1 works + * out to around 524us at 125MHz. + */ + localparam UNLOCK_TIME = 16'hffff; + /* 5us, or around one minimum-length packet plus some extra */ + localparam TEST_UNLOCK_TIME = 16'd625; + reg [15:0] unlock_counter, unlock_counter_next; + + always @(*) begin + ldd = { lfsr[8] ^ lfsr[10], lfsr[7] ^ lfsr[9] }; + unscrambled_next = scrambled ^ ldd; + + /* + * We must invert scrambled before adding it to the lfsr in + * order to remove the ^1 from the input idle. This doesn't + * affect the output of the lfsr during the sample state + * because two bits from the lfsr are xor'd together, + * canceling out the inversion. + */ + lfsr_next = lfsr; + if (scrambled_valid[0]) + lfsr_next = { lfsr[9:0], locked ? ldd[1] : ~scrambled[1] }; + else if (scrambled_valid[1]) + lfsr_next = { lfsr[8:0], locked ? ldd : ~scrambled }; + + idle_counter_next = idle_counter; + if (scrambled_valid[1]) begin + if (unscrambled_next[1] && unscrambled_next[0]) + idle_counter_next = idle_counter - 2; + else if (unscrambled_next[0]) + idle_counter_next = idle_counter - 1; + else + idle_counter_next = CONSECUTIVE_IDLES; + end else if (scrambled_valid[0]) begin + if (unscrambled_next[1]) + idle_counter_next = idle_counter - 1; + else + idle_counter_next = CONSECUTIVE_IDLES; + end + + locked_next = 1; + unlock_counter_next = unlock_counter; + if (!idle_counter_next[4:1]) begin + unlock_counter_next = test_mode ? TEST_UNLOCK_TIME : UNLOCK_TIME; + /* + * Reset the counter to 2 to ensure we can always + * subtract idles without underflowing + */ + idle_counter_next = 2; + end else if (|unlock_counter) begin + unlock_counter_next = unlock_counter - 1; + end else begin + locked_next = 0; + end + end + + always @(posedge clk) begin + unscrambled <= unscrambled_next; + if (signal_status) begin + lfsr <= lfsr_next; + idle_counter <= idle_counter_next; + unlock_counter <= unlock_counter_next; + locked <= locked_next; + unscrambled_valid <= scrambled_valid; + end else begin + lfsr <= 0; + idle_counter <= CONSECUTIVE_IDLES; + unlock_counter <= 0; + locked <= 0; + unscrambled_valid <= 0; + end + end + + `DUMP(0) + +endmodule diff --git a/rtl/scramble.v b/rtl/scramble.v new file mode 100644 index 0000000..166e051 --- /dev/null +++ b/rtl/scramble.v @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" + +module scramble ( + input clk, + input unscrambled, + output reg scrambled +); + + reg lfsr_next; + reg [10:0] lfsr = 10'h3ff; + + always @(*) begin + lfsr_next = lfsr[8] ^ lfsr[10]; + scrambled = unscrambled ^ lfsr_next; + end + + always @(posedge clk) + lfsr = { lfsr[9:0], lfsr_next }; + + `DUMP(0) + +endmodule diff --git a/tb/descramble.py b/tb/descramble.py new file mode 100644 index 0000000..65f8174 --- /dev/null +++ b/tb/descramble.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +import itertools +import random + +import cocotb +from cocotb.clock import Clock +from cocotb.regression import TestFactory +from cocotb.result import SimTimeoutError +from cocotb.triggers import ClockCycles, FallingEdge, RisingEdge, Timer, with_timeout + +from .util import compare_lists, send_recovered_bits, timeout, with_valids + +def scramble(bits): + lfsr = random.randrange(1, 0x7ff) + for bit in bits: + ldd = (lfsr >> 10) ^ ((lfsr >> 8) & 1) + yield bit ^ ldd + lfsr <<= 1 + lfsr &= 0x7ff + lfsr |= ldd + +async def send_scrambled(descrambler, data, valids): + descrambler.signal_status.value = 1 + await send_recovered_bits(descrambler.clk, descrambler.scrambled, + descrambler.scrambled_valid, scramble(data), valids) + descrambler.signal_status.value = 0 + +@timeout(10, 'us') +async def test_unlock(descrambler, valids): + descrambler.signal_status.value = 0 + descrambler.scrambled_valid.value = 0 + descrambler.test_mode.value = 1 + await Timer(1) + await cocotb.start(Clock(descrambler.clk, 8, units='ns').start()) + + await cocotb.start(send_scrambled(descrambler, + itertools.chain(itertools.repeat(1, 60), + itertools.repeat(0, 625//2), + itertools.repeat(1, 29), + itertools.repeat(0)), + valids)) + + await ClockCycles(descrambler.clk, 60) + assert descrambler.locked.value + try: + await with_timeout(FallingEdge(descrambler.locked), 6, 'us') + except SimTimeoutError: + pass + else: + assert False + await FallingEdge(descrambler.locked) + +with_valids(globals(), test_unlock) + +@timeout(10, 'us') +async def test_descramble(descrambler, valids): + descrambler.signal_status.value = 0 + descrambler.scrambled_valid.value = 0 + descrambler.test_mode.value = 0 + await Timer(1) + await cocotb.start(Clock(descrambler.clk, 8, units='ns').start()) + + ins = [1] * 60 + [0] + [random.randrange(2) for _ in range(1000)] + await cocotb.start(send_scrambled(descrambler, ins, valids)) + + outs = [] + await RisingEdge(descrambler.locked) + while descrambler.locked.value: + await RisingEdge(descrambler.clk) + valid = descrambler.unscrambled_valid.value + if valid == 0: + pass + elif valid == 1: + outs.append(descrambler.unscrambled[1].value) + else: + outs.append(descrambler.unscrambled[1].value) + outs.append(descrambler.unscrambled[0].value) + + best_corr = -1 + best_off = None + for off in range(30, 42): + 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)}") + compare_lists(ins[best_off:], outs) + +with_valids(globals(), test_descramble) diff --git a/tb/scramble.py b/tb/scramble.py new file mode 100644 index 0000000..51a7b1e --- /dev/null +++ b/tb/scramble.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2022 Sean Anderson + +import itertools +import random + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from .util import alist, ReverseList, print_list_at, compare_lists + +threshold = 32 + +def descramble(scrambled): + locked = False + lfsr = ReverseList([0] * 11) + for s in scrambled: + ldd = lfsr[8] ^ lfsr[10] + + if s ^ ldd: + consecutive += 1 + else: + consecutive = 0 + + if consecutive >= threshold: + locked = True + + if locked: + yield s ^ ldd + lfsr.append(ldd) + else: + lfsr.append(0 if s else 1) + +@cocotb.test(timeout_time=10, timeout_unit='us') +async def test_scramble(scrambler): + scrambler.unscrambled.value = 1 + await Timer(1) + await cocotb.start(Clock(scrambler.clk, 8, units='ns').start()) + + idles = threshold + 10 + ins = [1] * idles + [0] + [random.randrange(2) for _ in range(1000)] + async def send(): + for bit in ins: + await FallingEdge(scrambler.clk) + scrambler.unscrambled.value = bit + await cocotb.start(send()) + + outs = [] + for _ in ins: + await RisingEdge(scrambler.clk) + outs.append(scrambler.scrambled.value) + + compare_lists(ins[idles-1:], list(descramble(outs)))