|
| 1 | +classdef HardwareCFORobustnessTest < matlab.unittest.TestCase |
| 2 | + %HardwareCFORobustnessTest Hardware-in-the-loop sweep of the |
| 3 | + % cfo_phase_inc AXI4-Lite register (x"118") on the V5_cfo_inj |
| 4 | + % BOOT.BIN. cfo_phase_inc is a 16-bit unsigned value applied to a |
| 5 | + % phase accumulator that drives an 8-bit-index LUT-based NCO. The |
| 6 | + % NCO output (cos + j*sin) complex-multiplies the Tx samples before |
| 7 | + % the Receiver, injecting a frequency offset of approximately |
| 8 | + % f_offset = cfo_phase_inc * Fs / 2^16, Fs = 15.36 MHz |
| 9 | + % = cfo_phase_inc * 234.375 Hz. |
| 10 | + % Negative CFO is achieved via two's-complement wrap: write |
| 11 | + % 65536 - |value| for negative frequencies. |
| 12 | + % |
| 13 | + % Frequency -> register value reference: |
| 14 | + % 0 Hz -> 0 |
| 15 | + % +10 kHz -> 43 -10 kHz -> 65493 (0xFFD5) |
| 16 | + % +50 kHz -> 213 -50 kHz -> 65323 (0xFF2B) |
| 17 | + % +100 kHz -> 427 -100 kHz -> 65109 (0xFE55) |
| 18 | + % +200 kHz -> 853 -200 kHz -> 64683 (0xFCAB) |
| 19 | + % +240 kHz -> 1024 -240 kHz -> 64512 (0xFC00) |
| 20 | + % +300 kHz -> 1280 -300 kHz -> 64256 (0xFB00) |
| 21 | + % +500 kHz -> 2133 -500 kHz -> 63403 (0xF7AB) |
| 22 | + % |
| 23 | + % Tests assert BER < 1% within the stated +-240 kHz design range |
| 24 | + % (carrier-sync acquisition window) and characterise (information-only) |
| 25 | + % behaviour beyond that range. Also a recovery test: pull CFO to a |
| 26 | + % value the link can't lock, restore zero, assert re-lock + decode. |
| 27 | + % |
| 28 | + % cfo_phase_inc is write-only at the AXI level (HDL Coder default |
| 29 | + % for IP wrapper inputs). Detection of V5 via behavioural probe: |
| 30 | + % write a CFO value clearly outside lock range -> assert BIST stalls; |
| 31 | + % then write 0 and verify recovery. |
| 32 | + % |
| 33 | + % Run: runtests('HardwareCFORobustnessTest') |
| 34 | + |
| 35 | + properties (Constant) |
| 36 | + CfoAddr = '0x9D000118'; |
| 37 | + Fs = 15.36e6; |
| 38 | + PhaseAccumBits = 16; |
| 39 | + DataBitsPerPacket = 2240; |
| 40 | + BerThreshold = 0.01; |
| 41 | + SshTimeoutSec = 5; |
| 42 | + end |
| 43 | + |
| 44 | + properties (TestParameter) |
| 45 | + % Frequencies (in Hz) inside the QPSK Rx carrier-sync acquisition |
| 46 | + % range, where the link should decode at BER < 1%. |
| 47 | + passingCFOHz = struct( ... |
| 48 | + 'zero', 0, ... |
| 49 | + 'pos10k', 1e4, ... |
| 50 | + 'neg10k', -1e4, ... |
| 51 | + 'pos50k', 5e4, ... |
| 52 | + 'neg50k', -5e4, ... |
| 53 | + 'pos100k', 1e5, ... |
| 54 | + 'neg100k', -1e5, ... |
| 55 | + 'pos200k', 2e5, ... |
| 56 | + 'neg200k', -2e5); |
| 57 | + |
| 58 | + % Frequencies outside the stated design range -- behaviour |
| 59 | + % is informational. We just verify the FPGA doesn't crash (the |
| 60 | + % BIST counters keep responding to AXI reads even if no decode). |
| 61 | + characterizeCFOHz = struct( ... |
| 62 | + 'pos300k', 3e5, ... |
| 63 | + 'pos500k', 5e5, ... |
| 64 | + 'pos1MHz', 1e6, ... |
| 65 | + 'neg300k', -3e5, ... |
| 66 | + 'neg500k', -5e5); |
| 67 | + end |
| 68 | + |
| 69 | + methods (Static) |
| 70 | + function val = hzToReg(hzOffset) |
| 71 | + % Convert frequency in Hz to the 16-bit cfo_phase_inc value |
| 72 | + % (two's-complement for negatives). |
| 73 | + Fs_local = 15.36e6; |
| 74 | + inc = round(hzOffset / Fs_local * 2^16); % can be +/- |
| 75 | + val = mod(inc, 2^16); % wrap to 0..65535 |
| 76 | + end |
| 77 | + |
| 78 | + function snapWait(testCase, secs) |
| 79 | + S0 = BistRegisters.readAll(testCase.SshTimeoutSec); %#ok<NASGU> |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + methods (TestClassSetup) |
| 84 | + function checkBoardReachableAndV5(testCase) |
| 85 | + [rc,~] = BistRegisters.sshExec('true', testCase.SshTimeoutSec); |
| 86 | + testCase.assumeEqual(rc, 0, ... |
| 87 | + 'Jupiter at 10.0.0.146 not reachable -- skipping HW CFO suite'); |
| 88 | + |
| 89 | + % Behavioural probe for V5: write a large CFO well outside the |
| 90 | + % lock range (e.g. 1.5 MHz) and assert BIST packet rate falls |
| 91 | + % near zero. (Pure V3 / V4 BOOT.BINs ignore writes to 0x118 and |
| 92 | + % keep decoding -- so they FAIL this probe.) |
| 93 | + largeCfoReg = HardwareCFORobustnessTest.hzToReg(1.5e6); |
| 94 | + BistRegisters.write(testCase.CfoAddr, largeCfoReg, testCase.SshTimeoutSec); |
| 95 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 96 | + pause(0.05); |
| 97 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 98 | + pause(2); |
| 99 | + S0 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 100 | + pause(1.5); |
| 101 | + S1 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 102 | + dpHi = S1.packets - S0.packets; |
| 103 | + % Restore zero CFO + re-acquire |
| 104 | + BistRegisters.write(testCase.CfoAddr, 0, testCase.SshTimeoutSec); |
| 105 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 106 | + pause(0.05); |
| 107 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 108 | + pause(2); |
| 109 | + testCase.assumeLessThan(dpHi, 500, ... |
| 110 | + sprintf(['1.5 MHz CFO did not stall the link (dp=%d in 1.5s); ' ... |
| 111 | + 'deployed BOOT.BIN does not appear to be V5_cfo_inj'], dpHi)); |
| 112 | + end |
| 113 | + |
| 114 | + function leaveZeroOnExit(testCase) |
| 115 | + testCase.addTeardown(@() ... |
| 116 | + BistRegisters.write(testCase.CfoAddr, 0, testCase.SshTimeoutSec)); |
| 117 | + end |
| 118 | + end |
| 119 | + |
| 120 | + methods (Test, TestTags = {'Hardware'}) |
| 121 | + |
| 122 | + % --- inside-range: must decode at BER < 1% --- |
| 123 | + function testPassingCFOSweep(testCase, passingCFOHz) |
| 124 | + reg = HardwareCFORobustnessTest.hzToReg(passingCFOHz); |
| 125 | + BistRegisters.write(testCase.CfoAddr, reg, testCase.SshTimeoutSec); |
| 126 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 127 | + pause(0.05); |
| 128 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 129 | + pause(2); % carrier-sync + AGC settle |
| 130 | + S0 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 131 | + pause(3); |
| 132 | + S1 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 133 | + dp = S1.packets - S0.packets; |
| 134 | + de = S1.bit_errors - S0.bit_errors; |
| 135 | + bits = dp * testCase.DataBitsPerPacket; |
| 136 | + ber = de / max(1, bits); |
| 137 | + fprintf('cfo=%+.0f Hz (reg=0x%04X): +pkts=%d bits=%d BER=%.4f%%\n', ... |
| 138 | + passingCFOHz, reg, dp, bits, 100*ber); |
| 139 | + testCase.assertGreaterThanOrEqual(bits, 1e6, ... |
| 140 | + sprintf('cfo=%+.0f Hz: only %d bits in 3 s -- link stalled', passingCFOHz, bits)); |
| 141 | + testCase.verifyLessThan(ber, testCase.BerThreshold); |
| 142 | + end |
| 143 | + |
| 144 | + % --- characterize beyond design range: report behaviour, but only |
| 145 | + % assert that the FPGA continues to respond (BIST register |
| 146 | + % reads still work, so the design hasn't crashed) --- |
| 147 | + function testBeyondRangeCharacterization(testCase, characterizeCFOHz) |
| 148 | + reg = HardwareCFORobustnessTest.hzToReg(characterizeCFOHz); |
| 149 | + BistRegisters.write(testCase.CfoAddr, reg, testCase.SshTimeoutSec); |
| 150 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 151 | + pause(0.05); |
| 152 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 153 | + pause(2); |
| 154 | + S0 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 155 | + pause(2); |
| 156 | + S1 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 157 | + % We can only assert that the AXI reads return sensible |
| 158 | + % (different) values -- the FPGA must still be alive. |
| 159 | + testCase.verifyFalse(isnan(S1.packets), ... |
| 160 | + sprintf('AXI read failed after writing cfo=%.0f Hz', characterizeCFOHz)); |
| 161 | + dp = S1.packets - S0.packets; |
| 162 | + de = S1.bit_errors - S0.bit_errors; |
| 163 | + if dp > 0 |
| 164 | + ber = de / (dp * testCase.DataBitsPerPacket); |
| 165 | + fprintf('cfo=%+.0f Hz (out-of-range): +pkts=%d BER=%.4f%% (info)\n', ... |
| 166 | + characterizeCFOHz, dp, 100*ber); |
| 167 | + else |
| 168 | + fprintf('cfo=%+.0f Hz (out-of-range): link stalled (+pkts=0) (info)\n', characterizeCFOHz); |
| 169 | + end |
| 170 | + end |
| 171 | + |
| 172 | + % --- recovery: pull CFO to a value the link can't lock, then |
| 173 | + % restore zero, assert recovery --- |
| 174 | + function testCFORecoveryAfterLockLoss(testCase) |
| 175 | + % First write a CFO that clearly exceeds lock range |
| 176 | + reg = HardwareCFORobustnessTest.hzToReg(1.5e6); |
| 177 | + BistRegisters.write(testCase.CfoAddr, reg, testCase.SshTimeoutSec); |
| 178 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 179 | + pause(0.05); |
| 180 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 181 | + pause(2); |
| 182 | + S0 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 183 | + pause(1.5); |
| 184 | + S1 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 185 | + stallDp = S1.packets - S0.packets; |
| 186 | + % Restore zero CFO + rstCS for re-acquisition |
| 187 | + BistRegisters.write(testCase.CfoAddr, 0, testCase.SshTimeoutSec); |
| 188 | + BistRegisters.write(BistRegisters.RstCsAddr, 1, testCase.SshTimeoutSec); |
| 189 | + pause(0.05); |
| 190 | + BistRegisters.write(BistRegisters.RstCsAddr, 0, testCase.SshTimeoutSec); |
| 191 | + pause(2); |
| 192 | + S2 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 193 | + pause(2); |
| 194 | + S3 = BistRegisters.readAll(testCase.SshTimeoutSec); |
| 195 | + recoveredDp = S3.packets - S2.packets; |
| 196 | + fprintf('lock-lost stall=+%d/1.5s ; recovery=+%d/2s\n', stallDp, recoveredDp); |
| 197 | + testCase.verifyLessThan(stallDp, 500, ... |
| 198 | + 'expected link to fail to lock at 1.5 MHz CFO'); |
| 199 | + testCase.verifyGreaterThan(recoveredDp, 5000, ... |
| 200 | + 'expected link to recover after restoring zero CFO + rstCS'); |
| 201 | + end |
| 202 | + end |
| 203 | +end |
0 commit comments