Fix ddc_input_interface 18->16 bit overflow: add saturation at positive full scale

Bug: rounding logic 'adc_i <= ddc_i[17:2] + ddc_i[1]' overflows when
ddc_i[17:2]=0x7FFF and ddc_i[1]=1, causing 0x7FFF+1=0x8000 (sign flip
from max positive to most negative value).

Fix: add explicit saturation — clamp to 0x7FFF when truncated value is
max positive and round bit is set. Negative values cannot overflow since
rounding only moves toward zero.

New testbench: tb_ddc_input_interface.v with 26 tests covering rounding,
truncation, overflow saturation at positive boundary, negative full scale,
valid synchronization, and sync error detection.
This commit is contained in:
Jason
2026-03-16 18:14:06 +02:00
parent 17731dd482
commit a5a5e96a57
2 changed files with 393 additions and 8 deletions
+17 -5
View File
@@ -39,13 +39,25 @@ always @(posedge clk or negedge reset_n) begin
end end
end end
// Scale 18-bit to 16-bit with rounding // Scale 18-bit to 16-bit with convergent rounding + saturation
// Option: Keep most significant 16 bits with rounding // ddc_i[17:2] extracts the upper 16 bits; ddc_i[1] is the rounding bit.
// Without saturation, 0x7FFF + 1 = 0x8000 (sign flip at positive full scale).
// Fix: saturate to 0x7FFF when rounding would overflow a positive value.
// Negative values cannot overflow: the most negative 18-bit value (-131072)
// truncates to -8192 (0x8000 as 16-bit) and rounding only moves toward zero.
wire [15:0] trunc_i = ddc_i[17:2];
wire [15:0] trunc_q = ddc_q[17:2];
wire round_i = ddc_i[1];
wire round_q = ddc_q[1];
// Overflow occurs only when truncated value is max positive AND round bit set
wire sat_i = (trunc_i == 16'h7FFF) & round_i;
wire sat_q = (trunc_q == 16'h7FFF) & round_q;
always @(posedge clk) begin always @(posedge clk) begin
if (valid_sync) begin if (valid_sync) begin
// Round to nearest: add 0.5 LSB before truncation adc_i <= sat_i ? 16'sh7FFF : (trunc_i + {15'b0, round_i});
adc_i <= ddc_i[17:2] + ddc_i[1]; // Rounding adc_q <= sat_q ? 16'sh7FFF : (trunc_q + {15'b0, round_q});
adc_q <= ddc_q[17:2] + ddc_q[1]; // Rounding
end end
end end
@@ -0,0 +1,373 @@
`timescale 1ns / 1ps
/**
* tb_ddc_input_interface.v
*
* Testbench for ddc_input_interface.v 18-bit to 16-bit rounding + saturation.
*
* Tests:
* 1. Reset state (outputs zero, adc_valid deasserted)
* 2. Basic passthrough (small positive/negative values)
* 3. Rounding up (bit[1]=1 adds 1 to truncated value)
* 4. Rounding down (bit[1]=0, truncation only)
* 5. Positive overflow saturation (0x1FFFC..0x1FFFF -> 0x7FFF, not 0x8000)
* 6. Negative full scale (no overflow on negative side)
* 7. Valid synchronization (both I and Q must be valid)
* 8. Sync error detection (I valid without Q, and vice versa)
* 9. Pipeline latency (2 cycles: valid_sync then adc_valid)
* 10. Sweep from near-max-positive through overflow boundary
*
* Compile:
* iverilog -g2001 -DSIMULATION -o tb/tb_ddc_input_interface.vvp \
* tb/tb_ddc_input_interface.v ddc_input_interface.v
*
* vvp tb/tb_ddc_input_interface.vvp
*/
module tb_ddc_input_interface;
// ============================================================================
// Parameters
// ============================================================================
localparam CLK_PERIOD = 10.0; // 100 MHz
// ============================================================================
// Clock and reset
// ============================================================================
reg clk;
reg reset_n;
initial clk = 0;
always #(CLK_PERIOD / 2) clk = ~clk;
// ============================================================================
// DUT signals
// ============================================================================
reg signed [17:0] ddc_i;
reg signed [17:0] ddc_q;
reg valid_i;
reg valid_q;
wire signed [15:0] adc_i;
wire signed [15:0] adc_q;
wire adc_valid;
wire data_sync_error;
// ============================================================================
// DUT instantiation
// ============================================================================
ddc_input_interface dut (
.clk(clk),
.reset_n(reset_n),
.ddc_i(ddc_i),
.ddc_q(ddc_q),
.valid_i(valid_i),
.valid_q(valid_q),
.adc_i(adc_i),
.adc_q(adc_q),
.adc_valid(adc_valid),
.data_sync_error(data_sync_error)
);
// ============================================================================
// Test infrastructure
// ============================================================================
integer pass_count;
integer fail_count;
integer test_count;
task check;
input cond;
input [511:0] label;
begin
test_count = test_count + 1;
if (cond) begin
$display("[PASS] %0s", label);
pass_count = pass_count + 1;
end else begin
$display("[FAIL] %0s", label);
fail_count = fail_count + 1;
end
end
endtask
// Helper: apply one I/Q sample and wait for output.
//
// The DUT has a 2-stage valid pipeline + 1 cycle data capture:
// Cycle 0: present ddc_i/q + valid_i/q = 1
// Cycle 1: valid_i_reg/valid_q_reg capture (NBA).
// Cycle 2: valid_sync = 1 (NBA). adc_i/q computed from ddc_i/q at posedge.
// DATA MUST STILL BE STABLE on ddc_i/q at this edge!
// adc_i/q result available AFTER this posedge (NBA -> next cycle).
// Cycle 3: adc_i/q hold the result. adc_valid = 1.
//
// Total: 4 posedge waits from data presentation to result availability.
task apply_sample;
input signed [17:0] in_i;
input signed [17:0] in_q;
begin
@(posedge clk);
ddc_i <= in_i;
ddc_q <= in_q;
valid_i <= 1;
valid_q <= 1;
@(posedge clk);
// valid can drop but data must stay stable for valid_sync cycle
valid_i <= 0;
valid_q <= 0;
// ddc_i/q stay at in_i/in_q (held by regs)
@(posedge clk);
// valid_sync fires this edge, data sampled into adc_i/q (NBA)
@(posedge clk);
// adc_i/q now hold the result. adc_valid = 1.
@(posedge clk);
// One more edge to ensure NBA from cycle 3 is visible
end
endtask
// ============================================================================
// VCD dump
// ============================================================================
initial begin
$dumpfile("tb/tb_ddc_input_interface.vcd");
$dumpvars(0, tb_ddc_input_interface);
end
// ============================================================================
// Expected value computation (matches RTL: truncate [17:2] + round [1],
// saturate if positive overflow)
// ============================================================================
function signed [15:0] expected_output;
input signed [17:0] val;
reg [15:0] trunc;
reg rnd;
begin
trunc = val[17:2];
rnd = val[1];
if (trunc == 16'h7FFF && rnd)
expected_output = 16'sh7FFF; // saturate
else
expected_output = trunc + {15'b0, rnd};
end
endfunction
// ============================================================================
// Main test sequence
// ============================================================================
integer i;
reg signed [17:0] test_val;
reg signed [15:0] exp_i, exp_q;
initial begin
// ---- Init ----
pass_count = 0;
fail_count = 0;
test_count = 0;
ddc_i = 0;
ddc_q = 0;
valid_i = 0;
valid_q = 0;
reset_n = 0;
// ---- Reset ----
#(CLK_PERIOD * 5);
reset_n = 1;
#(CLK_PERIOD * 3);
$display("============================================================");
$display("ddc_input_interface Testbench");
$display("18-bit to 16-bit rounding with overflow saturation");
$display("============================================================");
// ---- Test 1: Reset state ----
check(adc_valid == 0, "adc_valid deasserted after reset");
check(data_sync_error == 0, "No sync error after reset");
// ---- Test 2: Basic passthrough small positive ----
// ddc_i = 18'd100 = 0x00064. [17:2] = 25, [1] = 0. Expected: 25.
apply_sample(18'sd100, 18'sd200);
exp_i = expected_output(18'sd100);
exp_q = expected_output(18'sd200);
check(adc_i == exp_i,
"Small positive I: 100 -> expected truncated+round");
check(adc_q == exp_q,
"Small positive Q: 200 -> expected truncated+round");
// ---- Test 3: Small negative ----
apply_sample(-18'sd100, -18'sd200);
exp_i = expected_output(-18'sd100);
exp_q = expected_output(-18'sd200);
check(adc_i == exp_i,
"Small negative I: -100 -> expected truncated+round");
check(adc_q == exp_q,
"Small negative Q: -200 -> expected truncated+round");
// ---- Test 4: Rounding UP (bit[1]=1 on positive value) ----
// 18'd6 = 0b000000000000000110. [17:2]=1, [1]=1. Expected: 1+1=2.
apply_sample(18'sd6, 18'sd6);
check(adc_i == 16'sd2,
"Rounding up I: 6 -> [17:2]=1, [1]=1, result=2");
check(adc_q == 16'sd2,
"Rounding up Q: 6 -> [17:2]=1, [1]=1, result=2");
// ---- Test 5: Rounding DOWN (bit[1]=0) ----
// 18'd4 = 0b000000000000000100. [17:2]=1, [1]=0. Expected: 1.
apply_sample(18'sd4, 18'sd4);
check(adc_i == 16'sd1,
"Truncation (no round) I: 4 -> [17:2]=1, [1]=0, result=1");
check(adc_q == 16'sd1,
"Truncation (no round) Q: 4 -> [17:2]=1, [1]=0, result=1");
// ---- Test 6: CRITICAL Positive overflow saturation ----
// Maximum 18-bit positive = 131071 = 18'h1FFFF
// [17:2] = 16'h7FFF = 32767, [1] = 1
// WITHOUT saturation: 32767 + 1 = -32768 (sign flip!)
// WITH saturation: 32767 (clamped)
apply_sample(18'sd131071, 18'sd131071);
check(adc_i == 16'sh7FFF,
"Positive overflow saturation I: 131071 -> 0x7FFF (not 0x8000)");
check(adc_q == 16'sh7FFF,
"Positive overflow saturation Q: 131071 -> 0x7FFF (not 0x8000)");
// ---- Test 7: Near-overflow, no saturation needed ----
// 18'sd131068 = 0x1FFFC. [17:2] = 0x7FFF, [1] = 0. Expected: 0x7FFF.
apply_sample(18'sd131068, 18'sd131068);
check(adc_i == 16'sh7FFF,
"Near-overflow no-round I: 131068 -> 0x7FFF (no saturation needed)");
check(adc_q == 16'sh7FFF,
"Near-overflow no-round Q: 131068 -> 0x7FFF (no saturation needed)");
// ---- Test 8: Just below overflow boundary ----
// 18'sd131066 = 0x1FFFA. [17:2] = 0x7FFE, [1] = 1. Expected: 0x7FFE+1=0x7FFF.
apply_sample(18'sd131066, 18'sd131066);
check(adc_i == 16'sh7FFF,
"Below overflow, round up I: 131066 -> 0x7FFF");
check(adc_q == 16'sh7FFF,
"Below overflow, round up Q: 131066 -> 0x7FFF");
// ---- Test 9: Negative full scale ----
// Minimum 18-bit signed = -131072 = 18'h20000
// [17:2] = 0x8000 = -32768, [1] = 0. Expected: -32768.
apply_sample(-18'sd131072, -18'sd131072);
check(adc_i == -16'sd32768,
"Negative full scale I: -131072 -> -32768 (0x8000)");
check(adc_q == -16'sd32768,
"Negative full scale Q: -131072 -> -32768 (0x8000)");
// ---- Test 10: Negative near-min with rounding ----
// -131071 = 18'sh20001. [17:2] = 0x8000 = -32768, [1] = 0. Expected: -32768.
apply_sample(-18'sd131071, -18'sd131071);
exp_i = expected_output(-18'sd131071);
check(adc_i == exp_i,
"Negative near-min I: -131071 -> expected");
// ---- Test 11: Zero ----
apply_sample(18'sd0, 18'sd0);
check(adc_i == 16'sd0, "Zero I: 0 -> 0");
check(adc_q == 16'sd0, "Zero Q: 0 -> 0");
// ---- Test 12: Valid synchronization only I valid ----
@(posedge clk);
ddc_i <= 18'sd999;
ddc_q <= 18'sd999;
valid_i <= 1;
valid_q <= 0;
@(posedge clk);
// data_sync_error = valid_i_reg ^ valid_q_reg, 1 cycle after inputs
valid_i <= 0;
@(posedge clk);
check(data_sync_error == 1,
"Sync error detected: valid_i=1, valid_q=0");
@(posedge clk);
// ---- Test 13: Valid synchronization only Q valid ----
@(posedge clk);
ddc_i <= 18'sd999;
ddc_q <= 18'sd999;
valid_i <= 0;
valid_q <= 1;
@(posedge clk);
valid_q <= 0;
@(posedge clk);
check(data_sync_error == 1,
"Sync error detected: valid_i=0, valid_q=1");
@(posedge clk);
// ---- Test 14: No sync error when both valid ----
@(posedge clk);
valid_i <= 1;
valid_q <= 1;
@(posedge clk);
valid_i <= 0;
valid_q <= 0;
@(posedge clk);
check(data_sync_error == 0,
"No sync error when both valid");
@(posedge clk);
// ---- Test 15: Sweep near overflow boundary ----
// Test values from 131064 to 131071 covers all rounding/saturation cases
begin : sweep_test
integer sweep_pass;
reg signed [17:0] sv;
reg signed [15:0] exp_sv;
sweep_pass = 1;
for (i = 131064; i <= 131071; i = i + 1) begin
sv = i;
apply_sample(sv, sv);
exp_sv = expected_output(sv);
if (adc_i !== exp_sv) begin
sweep_pass = 0;
$display(" SWEEP FAIL: input=%0d, expected=%0d, got=%0d",
i, exp_sv, adc_i);
end
end
check(sweep_pass == 1,
"Overflow boundary sweep (131064..131071) all correct");
end
// ---- Test 16: Sweep near negative boundary ----
begin : neg_sweep_test
integer neg_sweep_pass;
reg signed [17:0] sv;
reg signed [15:0] exp_sv;
neg_sweep_pass = 1;
for (i = -131072; i <= -131064; i = i + 1) begin
sv = i;
apply_sample(sv, sv);
exp_sv = expected_output(sv);
if (adc_i !== exp_sv) begin
neg_sweep_pass = 0;
$display(" NEG SWEEP FAIL: input=%0d, expected=%0d, got=%0d",
i, exp_sv, adc_i);
end
end
check(neg_sweep_pass == 1,
"Negative boundary sweep (-131072..-131064) all correct");
end
// ---- Summary ----
$display("\n============================================================");
$display("RESULTS: %0d / %0d passed", pass_count, test_count);
$display("============================================================");
if (fail_count == 0) begin
$display("ALL TESTS PASSED");
end else begin
$display("SOME TESTS FAILED");
end
$display("============================================================");
#(CLK_PERIOD * 5);
$finish;
end
// ============================================================================
// Watchdog
// ============================================================================
initial begin
#(CLK_PERIOD * 100_000);
$display("WATCHDOG TIMEOUT");
$display("SOME TESTS FAILED");
$finish;
end
endmodule