Add (de)scrambling support
This adds support for (de)scrambling as described in X3.263. The scrambler is fairly straightforward. Because we only have to recognize idles, and because the timing constraints are more relaxed (than e.g. the PCS), we can make several simplifications not found in other designs (e.g. X3.263 Annex G or DP83222). First, we can reuse the same register for the lfsr as for the input ciphertext. This is because we only need to record the scrambled data when we are unlocked, and we can easily recover the unscrambled data just by an inversion (as opposed to needing to align with /H/ etc). Second, it is not critical what the exact thresholds are for locking an unlocking, as long as certain minimums are met. This allows us to ignore edge cases, such as if we have data=10 and valid=2. Without these relaxed constraints, we would need to special-case this input to ensure we didn't miss the last necessary consecutive idle. But instead we just set the threshold such that one missed bit does not matter. To support easier testing, a test input may be used to cause the descramble to become unlocked after only 5us, instead of the mandated 361. This makes simulation go much faster. Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
parent
c6f95ce26f
commit
12a4678442
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
|
||||
MODULES := pcs pmd nrzi_encode nrzi_decode scramble descramble
|
||||
|
||||
.PHONY: test
|
||||
test: $(addsuffix .fst,$(MODULES)) $(addsuffix .post.fst,$(MODULES))
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-Only
|
||||
/*
|
||||
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
*/
|
||||
|
||||
`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
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-Only
|
||||
/*
|
||||
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
*/
|
||||
|
||||
`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
|
|
@ -0,0 +1,92 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-Only
|
||||
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
|
||||
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)
|
|
@ -0,0 +1,54 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-Only
|
||||
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
|
||||
|
||||
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)))
|
Loading…
Reference in New Issue