fix(pre-bringup): resolve P0 + quick-win P1 findings from 2026-04-19 audit

Addresses findings from docs/DEVELOP_AUDIT_2026-04-19.md:

P0 source-level:
- F-4.3 ADAR1000_Manager::adarSetTxPhase now writes REG_LOAD_WORKING
  with LD_WRK_REGS_LDTX_OVERRIDE (0x02) instead of 0x01. Previous value
  toggled the LDRX latch on a TX-phase write, so host TX phase updates
  never reached the working registers.
- F-6.1 DDC mixer_saturation / filter_overflow / diagnostics were deleted
  at the receiver boundary. Now plumbed to new outputs on
  radar_receiver_final (ddc_overflow_any, ddc_saturation_count) and
  aggregated into gpio_dig5 in radar_system_top. Added mark_debug
  attributes for ILA visibility. Test/debug inputs tied low explicitly.
- F-0.8 adc_clk_mmcm.xdc set_clock_uncertainty: removed invalid -add
  flag (Vivado silently rejected it, applying zero guardband). Now uses
  absolute 0.150 ns which covers 53 ps jitter + ~100 ps PVT margin.

P1:
- F-4.2 adarSetBit / adarResetBit reject broadcast=ON — the RMW sampled
  a single device but wrote to all four, clobbering the other three's
  state.
- F-4.4 initializeSingleDevice returns false and leaves initialized=false
  when scratchpad verification fails; previously marked the device
  initialized anyway so downstream PA enable could drive a dead bus.
- F-6.2 FIR I/Q filter_overflow ports, previously unconnected, now OR'd
  into the module-level filter_overflow output.
- F-6.3 mti_canceller exposes 8-bit saturation counter. Saturation was
  previously invisible and produces spurious Doppler harmonics.

Verification:
- 27/27 iverilog testbenches pass
- 228/228 pytest pass (cross-layer contract + cosim)
- MCU unit tests 51/51 + 24/24 pass
- Remote Vivado 2025.2 build: bitstream writes; 400 MHz mixer pipeline
  now shows WNS -0.109 ns which MATCHES the audit's F-0.9 prediction
  that the design only closed because F-0.8's guardband was silently
  dropped. ft_clkout F-0.9 remains a show-stopper (requires MRCC pin
  move), tracked separately.

Not addressed in this PR (larger scope, follow-up tickets):
F-0.4, F-0.5, F-0.6, F-0.7, F-0.9, F-1.1, F-1.2, F-2.2, F-3.2, F-4.1,
F-4.7, F-6.4, F-6.5.
This commit is contained in:
Jason
2026-04-20 13:48:36 +05:45
parent c82b25f7a0
commit 3f47d1ef71
6 changed files with 190 additions and 92 deletions
@@ -433,10 +433,15 @@ bool ADAR1000Manager::initializeSingleDevice(uint8_t deviceIndex) {
adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN, BROADCAST_OFF); adarWrite(deviceIndex, REG_ADC_CONTROL, ADAR1000_ADC_2MHZ_CLK | ADAR1000_ADC_EN, BROADCAST_OFF);
// Verify communication with scratchpad test // Verify communication with scratchpad test
// Audit F-4.4: on SPI failure, previously marked the device initialized
// anyway, so downstream (e.g. PA enable) could drive PA gates out-of-spec
// on a dead bus. Now propagate the failure so initializeAllDevices aborts.
DIAG("BF", " dev[%u] verifying SPI communication...", deviceIndex); DIAG("BF", " dev[%u] verifying SPI communication...", deviceIndex);
bool comms_ok = verifyDeviceCommunication(deviceIndex); bool comms_ok = verifyDeviceCommunication(deviceIndex);
if (!comms_ok) { if (!comms_ok) {
DIAG_WARN("BF", " dev[%u] scratchpad verify FAILED but marking initialized anyway", deviceIndex); DIAG_ERR("BF", " dev[%u] scratchpad verify FAILED -- device NOT marked initialized", deviceIndex);
devices_[deviceIndex]->initialized = false;
return false;
} }
devices_[deviceIndex]->initialized = true; devices_[deviceIndex]->initialized = true;
@@ -835,12 +840,26 @@ uint8_t ADAR1000Manager::adarRead(uint8_t deviceIndex, uint32_t mem_addr) {
} }
void ADAR1000Manager::adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { void ADAR1000Manager::adarSetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) {
// Audit F-4.2: broadcast-RMW is unsafe. The read samples a single device
// but the write fans out to all four, overwriting the other three with
// deviceIndex's state. Reject and surface the mistake.
if (broadcast == BROADCAST_ON) {
DIAG_ERR("BF", "adarSetBit: broadcast RMW is unsafe, ignored (dev=%u addr=0x%03lX bit=%u)",
deviceIndex, (unsigned long)mem_addr, bit);
return;
}
uint8_t temp = adarRead(deviceIndex, mem_addr); uint8_t temp = adarRead(deviceIndex, mem_addr);
uint8_t data = temp | (1 << bit); uint8_t data = temp | (1 << bit);
adarWrite(deviceIndex, mem_addr, data, broadcast); adarWrite(deviceIndex, mem_addr, data, broadcast);
} }
void ADAR1000Manager::adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) { void ADAR1000Manager::adarResetBit(uint8_t deviceIndex, uint32_t mem_addr, uint8_t bit, uint8_t broadcast) {
// Audit F-4.2: see adarSetBit.
if (broadcast == BROADCAST_ON) {
DIAG_ERR("BF", "adarResetBit: broadcast RMW is unsafe, ignored (dev=%u addr=0x%03lX bit=%u)",
deviceIndex, (unsigned long)mem_addr, bit);
return;
}
uint8_t temp = adarRead(deviceIndex, mem_addr); uint8_t temp = adarRead(deviceIndex, mem_addr);
uint8_t data = temp & ~(1 << bit); uint8_t data = temp & ~(1 << bit);
adarWrite(deviceIndex, mem_addr, data, broadcast); adarWrite(deviceIndex, mem_addr, data, broadcast);
@@ -904,7 +923,7 @@ void ADAR1000Manager::adarSetTxPhase(uint8_t deviceIndex, uint8_t channel, uint8
adarWrite(deviceIndex, mem_addr_i, i_val, broadcast); adarWrite(deviceIndex, mem_addr_i, i_val, broadcast);
adarWrite(deviceIndex, mem_addr_q, q_val, broadcast); adarWrite(deviceIndex, mem_addr_q, q_val, broadcast);
adarWrite(deviceIndex, REG_LOAD_WORKING, 0x1, broadcast); adarWrite(deviceIndex, REG_LOAD_WORKING, LD_WRK_REGS_LDTX_OVERRIDE, broadcast);
} }
void ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) { void ADAR1000Manager::adarSetRxVgaGain(uint8_t deviceIndex, uint8_t channel, uint8_t gain, uint8_t broadcast) {
@@ -88,8 +88,9 @@ set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p]
# Timing margin for 400 MHz critical paths # Timing margin for 400 MHz critical paths
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Extra setup uncertainty forces Vivado to leave margin for temperature/voltage/ # Extra setup uncertainty forces Vivado to leave margin for temperature/voltage/
# aging variation. Reduced from 200 ps to 100 ps after NCO→mixer pipeline # aging variation. 150 ps absolute covers the built-in jitter-based value
# register fix eliminated the dominant timing bottleneck (WNS went from +0.002ns # (~53 ps) plus ~100 ps temperature/voltage/aging guardband.
# to comfortable margin). 100 ps still provides ~4% guardband on the 2.5ns period. # NOTE: Vivado's set_clock_uncertainty does NOT accept -add; prior use of
# This is additive to the existing jitter-based uncertainty (~53 ps). # -add 0.100 was silently rejected as a CRITICAL WARNING, so no guardband
set_clock_uncertainty -setup -add 0.100 [get_clocks clk_mmcm_out0] # was applied. Use an absolute value. (audit finding F-0.8)
set_clock_uncertainty -setup 0.150 [get_clocks clk_mmcm_out0]
+8 -2
View File
@@ -634,6 +634,11 @@ cdc_adc_to_processing #(
// FIR Filter Instances // FIR Filter Instances
// ============================================================================ // ============================================================================
// FIR overflow flags (audit F-6.2 previously dangling, now OR'd into
// module-level filter_overflow so the receiver can see FIR arithmetic overflow)
wire fir_i_overflow;
wire fir_q_overflow;
// FIR I channel // FIR I channel
fir_lowpass_parallel_enhanced fir_i_inst ( fir_lowpass_parallel_enhanced fir_i_inst (
.clk(clk_100m), .clk(clk_100m),
@@ -643,7 +648,7 @@ fir_lowpass_parallel_enhanced fir_i_inst (
.data_out(fir_i_out), .data_out(fir_i_out),
.data_out_valid(fir_valid_i), .data_out_valid(fir_valid_i),
.fir_ready(fir_i_ready), .fir_ready(fir_i_ready),
.filter_overflow() .filter_overflow(fir_i_overflow)
); );
// FIR Q channel // FIR Q channel
@@ -655,10 +660,11 @@ fir_lowpass_parallel_enhanced fir_q_inst (
.data_out(fir_q_out), .data_out(fir_q_out),
.data_out_valid(fir_valid_q), .data_out_valid(fir_valid_q),
.fir_ready(fir_q_ready), .fir_ready(fir_q_ready),
.filter_overflow() .filter_overflow(fir_q_overflow)
); );
assign fir_valid = fir_valid_i & fir_valid_q; assign fir_valid = fir_valid_i & fir_valid_q;
assign filter_overflow = fir_i_overflow | fir_q_overflow;
// ============================================================================ // ============================================================================
// Enhanced Output Stage // Enhanced Output Stage
+18 -1
View File
@@ -58,7 +58,12 @@ module mti_canceller #(
input wire mti_enable, // 1=MTI active, 0=pass-through input wire mti_enable, // 1=MTI active, 0=pass-through
// ========== STATUS ========== // ========== STATUS ==========
output reg mti_first_chirp // 1 during first chirp (output muted) output reg mti_first_chirp, // 1 during first chirp (output muted)
// Audit F-6.3: count of saturated samples since last reset. Saturation
// here produces spurious Doppler harmonics (phantom targets at ±fs/2)
// and was previously invisible to the MCU. Saturates at 0xFF.
output reg [7:0] mti_saturation_count
); );
// ============================================================================ // ============================================================================
@@ -104,6 +109,11 @@ assign diff_q_sat = (diff_q_full > $signed({{2{1'b0}}, {(DATA_WIDTH-1){1'b1}}}))
? $signed({1'b1, {(DATA_WIDTH-1){1'b0}}}) ? $signed({1'b1, {(DATA_WIDTH-1){1'b0}}})
: diff_q_full[DATA_WIDTH-1:0]; : diff_q_full[DATA_WIDTH-1:0];
// Saturation detection (F-6.3): the top two bits of the DATA_WIDTH+1 signed
// difference disagree iff the value exceeds the DATA_WIDTH signed range.
wire diff_i_overflow = (diff_i_full[DATA_WIDTH] != diff_i_full[DATA_WIDTH-1]);
wire diff_q_overflow = (diff_q_full[DATA_WIDTH] != diff_q_full[DATA_WIDTH-1]);
// ============================================================================ // ============================================================================
// MAIN LOGIC // MAIN LOGIC
// ============================================================================ // ============================================================================
@@ -115,7 +125,14 @@ always @(posedge clk or negedge reset_n) begin
range_bin_out <= 6'd0; range_bin_out <= 6'd0;
has_previous <= 1'b0; has_previous <= 1'b0;
mti_first_chirp <= 1'b1; mti_first_chirp <= 1'b1;
mti_saturation_count <= 8'd0;
end else begin end else begin
// Count saturated MTI-active samples (F-6.3). Clamp at 0xFF.
if (range_valid_in && mti_enable && has_previous
&& (diff_i_overflow || diff_q_overflow)
&& (mti_saturation_count != 8'hFF)) begin
mti_saturation_count <= mti_saturation_count + 8'd1;
end
// Default: no valid output // Default: no valid output
range_valid_out <= 1'b0; range_valid_out <= 1'b0;
+39 -3
View File
@@ -74,7 +74,16 @@ module radar_receiver_final (
// AGC status outputs (for status readback / STM32 outer loop) // AGC status outputs (for status readback / STM32 outer loop)
output wire [7:0] agc_saturation_count, // Per-frame clipped sample count output wire [7:0] agc_saturation_count, // Per-frame clipped sample count
output wire [7:0] agc_peak_magnitude, // Per-frame peak (upper 8 bits) output wire [7:0] agc_peak_magnitude, // Per-frame peak (upper 8 bits)
output wire [3:0] agc_current_gain // Effective gain_shift encoding output wire [3:0] agc_current_gain, // Effective gain_shift encoding
// DDC overflow diagnostics (audit F-6.1 previously deleted at boundary).
// Not yet plumbed into the USB status packet (protocol contract is frozen);
// exposed here for gpio aggregation and ILA mark_debug visibility.
output wire ddc_overflow_any,
output wire [2:0] ddc_saturation_count,
// MTI 2-pulse canceller saturation count (audit F-6.3).
output wire [7:0] mti_saturation_count_out
); );
// ========== INTERNAL SIGNALS ========== // ========== INTERNAL SIGNALS ==========
@@ -211,6 +220,16 @@ wire signed [17:0] ddc_out_q;
wire ddc_valid_i; wire ddc_valid_i;
wire ddc_valid_q; wire ddc_valid_q;
// DDC diagnostic signals (audit F-6.1 all outputs previously unconnected)
wire [1:0] ddc_status_w;
wire [7:0] ddc_diagnostics_w;
wire ddc_mixer_saturation;
wire ddc_filter_overflow;
(* mark_debug = "true" *) wire ddc_mixer_saturation_dbg = ddc_mixer_saturation;
(* mark_debug = "true" *) wire ddc_filter_overflow_dbg = ddc_filter_overflow;
(* mark_debug = "true" *) wire [7:0] ddc_diagnostics_dbg = ddc_diagnostics_w;
ddc_400m_enhanced ddc( ddc_400m_enhanced ddc(
.clk_400m(clk_400m), // 400MHz clock from ADC DCO .clk_400m(clk_400m), // 400MHz clock from ADC DCO
.clk_100m(clk), // 100MHz system clock //used by the 2 FIR .clk_100m(clk), // 100MHz system clock //used by the 2 FIR
@@ -222,9 +241,25 @@ ddc_400m_enhanced ddc(
.baseband_q(ddc_out_q), // Q output at 100MHz .baseband_q(ddc_out_q), // Q output at 100MHz
.baseband_valid_i(ddc_valid_i), // Valid at 100MHz .baseband_valid_i(ddc_valid_i), // Valid at 100MHz
.baseband_valid_q(ddc_valid_q), .baseband_valid_q(ddc_valid_q),
.mixers_enable(1'b1) .mixers_enable(1'b1),
// Diagnostics (audit F-6.1) previously all unconnected
.ddc_status(ddc_status_w),
.ddc_diagnostics(ddc_diagnostics_w),
.mixer_saturation(ddc_mixer_saturation),
.filter_overflow(ddc_filter_overflow),
// Test/debug inputs explicit tie-low (were floating)
.test_mode(2'b00),
.test_phase_inc(16'h0000),
.force_saturation(1'b0),
.reset_monitors(1'b0),
.debug_sample_count(),
.debug_internal_i(),
.debug_internal_q()
); );
assign ddc_overflow_any = ddc_mixer_saturation | ddc_filter_overflow;
assign ddc_saturation_count = ddc_diagnostics_w[7:5];
ddc_input_interface ddc_if ( ddc_input_interface ddc_if (
.clk(clk), .clk(clk),
.reset_n(reset_n), .reset_n(reset_n),
@@ -391,7 +426,8 @@ mti_canceller #(
.range_valid_out(mti_range_valid), .range_valid_out(mti_range_valid),
.range_bin_out(mti_range_bin), .range_bin_out(mti_range_bin),
.mti_enable(host_mti_enable), .mti_enable(host_mti_enable),
.mti_first_chirp(mti_first_chirp) .mti_first_chirp(mti_first_chirp),
.mti_saturation_count(mti_saturation_count_out)
); );
// ========== FRAME SYNC FROM TRANSMITTER ========== // ========== FRAME SYNC FROM TRANSMITTER ==========
+21 -2
View File
@@ -198,6 +198,14 @@ wire [7:0] rx_agc_saturation_count;
wire [7:0] rx_agc_peak_magnitude; wire [7:0] rx_agc_peak_magnitude;
wire [3:0] rx_agc_current_gain; wire [3:0] rx_agc_current_gain;
// DDC overflow diagnostics (audit F-6.1) plumbed out of receiver so the
// DDC mixer_saturation / filter_overflow ports are no longer deleted at
// the boundary. Aggregated into gpio_dig5 alongside AGC saturation.
wire rx_ddc_overflow_any;
wire [2:0] rx_ddc_saturation_count;
// MTI saturation count (audit F-6.3). OR'd into gpio_dig5 for MCU visibility.
wire [7:0] rx_mti_saturation_count;
// Data packing for USB // Data packing for USB
wire [31:0] usb_range_profile; wire [31:0] usb_range_profile;
wire usb_range_valid; wire usb_range_valid;
@@ -562,7 +570,12 @@ radar_receiver_final rx_inst (
// AGC status outputs // AGC status outputs
.agc_saturation_count(rx_agc_saturation_count), .agc_saturation_count(rx_agc_saturation_count),
.agc_peak_magnitude(rx_agc_peak_magnitude), .agc_peak_magnitude(rx_agc_peak_magnitude),
.agc_current_gain(rx_agc_current_gain) .agc_current_gain(rx_agc_current_gain),
// DDC overflow diagnostics (audit F-6.1)
.ddc_overflow_any(rx_ddc_overflow_any),
.ddc_saturation_count(rx_ddc_saturation_count),
// MTI saturation count (audit F-6.3)
.mti_saturation_count_out(rx_mti_saturation_count)
); );
// ============================================================================ // ============================================================================
@@ -1040,7 +1053,13 @@ assign system_status = status_reg;
// DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC // DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC
// tracks the FPGA register as single source of truth. // tracks the FPGA register as single source of truth.
// DIG_7: Reserved (tied low for future use). // DIG_7: Reserved (tied low for future use).
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0); // gpio_dig5: "signal-chain clipped" — asserts on AGC saturation, DDC mixer/FIR
// overflow, or MTI 2-pulse saturation. Audit F-6.1/F-6.3: these were all
// previously invisible to the MCU.
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0)
| rx_ddc_overflow_any
| (rx_ddc_saturation_count != 3'd0)
| (rx_mti_saturation_count != 8'd0);
assign gpio_dig6 = host_agc_enable; assign gpio_dig6 = host_agc_enable;
assign gpio_dig7 = 1'b0; assign gpio_dig7 = 1'b0;