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:
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 ==========
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user