From 59f3c82fbb94c8fca94ceec933580ecd4093ee92 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:37:37 +0545 Subject: [PATCH] fpga: wire AD9484 PWDN to host opcode 0x32 (AUDIT-S25) `radar_receiver_final.v:246` had `assign adc_pwdn = 1'b0;` -- the AD9484 PWDN pin was hard-tied LOW with no path for the host or MCU to assert it. Combined with AUDIT-C13 (CSB hard-tied HIGH on the production board, no SPI access to the AD9484), the ADC was fully un-recoverable from a stuck state without dropping main power -- which also drops the VBAT-backed BKPSRAM persistence (MCU-A4 OCXO warmup, MCU-A7 emergency flag) and forces a 180 s warmup soak. Opcode 0x32 was reserved during the AUDIT-C3 fix (commit 24ef5e7) for exactly this purpose. Wire it through: - `radar_system_top.v` adds `reg host_adc_pwdn` next to `host_adc_format`, resets to 1'b0 (matches historical hard-tied state -- preserves bringup behavior), latches `usb_cmd_value[0]` on opcode 0x32, drives the new receiver input port. - `radar_receiver_final.v` adds `input wire host_adc_pwdn`, replaces the hard-coded `assign adc_pwdn = 1'b0` with `assign adc_pwdn = host_adc_pwdn`. - No CDC: `host_adc_pwdn` is a stable single-bit level driven from the clk_100m register straight to the I/O pad. AD9484 PWDN is asynchronous w.r.t. the ADC clock; the chip re-acquires its DLL on PWDN deassert. XDC pin assignments were already in place from AUDIT-C15 (50T:T5, 200T:P20, both LVCMOS25 driving the AD9484 PWDN net via the R36/R37 divider on the Main Board). Verification: - new tb/tb_adc_pwdn_opcode.v, 15/15 PASS: T1 reset -> host_adc_pwdn=0, adc_pwdn pin=0 (ADC powered up) T2 opcode 0x32 val=1 -> host_adc_pwdn=1, pin=1 (PWDN asserted) T3 opcode 0x32 val=0 -> cleared T4 only bit[0] consumed (upper bits ignored) T5 unrelated opcodes (0x33, 0x01) don't disturb host_adc_pwdn T6 cmd_valid_100m gating works - Quick regression 33/33 PASS (was 32/32; +1 new test, 0 regressions) - Lint: 0 errors --- 9_Firmware/9_2_FPGA/radar_receiver_final.v | 15 +- 9_Firmware/9_2_FPGA/radar_system_top.v | 23 +- 9_Firmware/9_2_FPGA/run_regression.sh | 4 + 9_Firmware/9_2_FPGA/tb/tb_adc_pwdn_opcode.v | 230 ++++++++++++++++++++ 4 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/tb/tb_adc_pwdn_opcode.v diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index 68ea5c2..0651e7e 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -79,9 +79,16 @@ module radar_receiver_final ( // AUDIT-C3: AD9484 sign-conversion select (opcode 0x33). Selects DDC // sign-conversion to match the SCLK/DFS strap (SJ1) on the Main Board. // 2'b00 = offset-binary (default), 2'b01 = two's-complement. - // (Opcode 0x32 is reserved for the future S-25 fix: adc_pwdn host control.) input wire [1:0] host_adc_format, + // AUDIT-S25: AD9484 power-down control (opcode 0x32). Active-high per + // AD9484 datasheet ("Power-Down (PWDN)" section). 1'b0 = ADC powered up + // (default), 1'b1 = PWDN asserted. Lets the MCU recover the ADC from a + // stuck state without dropping main power. Pin drives AD9484 PWDN net via + // the R36/R37 divider on the Main Board (CMOS thresholds, no level + // translation needed). Stable single-bit level — no CDC needed. + input wire host_adc_pwdn, + // ADC raw data tap (clk_100m domain, post-DDC, for self-test / debug) output wire [15:0] dbg_adc_i, // DDC output I (16-bit signed, 100 MHz) output wire [15:0] dbg_adc_q, // DDC output Q (16-bit signed, 100 MHz) @@ -242,8 +249,10 @@ wire clk_400m; wire [7:0] adc_data_cmos; // 8-bit ADC data (CMOS, from ad9484_interface_400m) wire adc_valid; // Data valid signal -// ADC power-down control (directly tie low = ADC always on) -assign adc_pwdn = 1'b0; +// AUDIT-S25: ADC power-down driven by host_adc_pwdn (opcode 0x32). Default +// 0 keeps the ADC powered up — same behavior as the previous hard-tied 1'b0. +// Set to 1 to assert AD9484 PWDN; see port comment for full design notes. +assign adc_pwdn = host_adc_pwdn; wire adc_overrange_400m; ad9484_interface_400m adc ( diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index bb28dd2..69ce659 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -304,7 +304,18 @@ reg [3:0] host_agc_holdoff; // Opcode 0x2C: frames to wait before gain-up // CSB is hard-tied HIGH on the production Main Board so SPI cannot reconfigure // the AD9484 — see RADAR_Main_Board.sch:46719. This register is the host's // only path to align RTL with the physical strap without a board rework. -// Opcode 0x32 is reserved for the future S-25 fix (host-driven adc_pwdn). +// AUDIT-S25 (opcode 0x32): AD9484 power-down control. +// 1'b0 = ADC powered up (default; matches the historical hard-tied state) +// 1'b1 = ADC PWDN asserted (active-high per AD9484 datasheet section +// "Power-Down (PWDN)"; FPGA pin drives the AD9484 PWDN net via +// the R36/R37 divider on the Main Board). +// Lets the MCU pulse PWDN during recovery without dropping main power. +// AUDIT-C13 noted that the AD9484's CSB is hard-tied HIGH on the production +// board (no SPI access), so PWDN is the ONLY in-system reset path for the +// ADC. PWDN is a stable single-bit level driven from this clk_100m register +// straight to the I/O pad; no CDC needed (asynchronous w.r.t. ADC, which +// re-acquires its DLL on PWDN deassert). +reg host_adc_pwdn; reg [1:0] host_adc_format; // Board bring-up self-test registers (opcode 0x30 trigger, 0x31 readback) @@ -591,6 +602,8 @@ radar_receiver_final rx_inst ( .host_dc_notch_width(host_dc_notch_width), // AUDIT-C3: ADC format select (opcode 0x33) -> DDC sign-conversion .host_adc_format(host_adc_format), + // AUDIT-S25: ADC power-down control (opcode 0x32) -> AD9484 PWDN pin + .host_adc_pwdn(host_adc_pwdn), // ADC debug tap (for self-test / bring-up) .dbg_adc_i(rx_dbg_adc_i), .dbg_adc_q(rx_dbg_adc_q), @@ -1009,6 +1022,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin host_self_test_trigger <= 1'b0; // Self-test idle // AUDIT-C3: ADC format default (offset-binary matches SJ1 default) host_adc_format <= 2'b00; + // AUDIT-S25: AD9484 PWDN default = 0 (ADC powered up; matches the + // historical hard-tied state at radar_receiver_final.v:246 prior to + // this fix, so existing bringup behavior is preserved). + host_adc_pwdn <= 1'b0; end else begin host_trigger_pulse <= 1'b0; // Self-clearing pulse host_status_request <= 1'b0; // Self-clearing pulse @@ -1078,8 +1095,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin 8'h30: host_self_test_trigger <= 1'b1; // Trigger self-test 8'h31: host_status_request <= 1'b1; // Self-test readback (status alias) // 0x31: readback handled via status mechanism (latched results) + // AUDIT-S25: AD9484 power-down control (active-high). Lets MCU + // recover the ADC from a stuck state without dropping main power. + 8'h32: host_adc_pwdn <= usb_cmd_value[0]; // AUDIT-C3: ADC format select (matches AD9484 SCLK/DFS strap SJ1). - // 0x32 reserved for S-25 (adc_pwdn host control); using 0x33 here. 8'h33: host_adc_format <= usb_cmd_value[1:0]; 8'hFF: host_status_request <= 1'b1; // Gap 2: status readback default: ; diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 03ee2fc..9155974 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -558,6 +558,10 @@ run_test "Doppler Frame-Start Gate (AUDIT-S3)" \ tb/tb_doppler_frame_start_gate.vvp \ tb/tb_doppler_frame_start_gate.v doppler_processor.v xfft_16.v fft_engine.v +run_test "ADC PWDN opcode 0x32 (AUDIT-S25)" \ + tb/tb_adc_pwdn_opcode.vvp \ + tb/tb_adc_pwdn_opcode.v + echo "" # =========================================================================== diff --git a/9_Firmware/9_2_FPGA/tb/tb_adc_pwdn_opcode.v b/9_Firmware/9_2_FPGA/tb/tb_adc_pwdn_opcode.v new file mode 100644 index 0000000..322e685 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_adc_pwdn_opcode.v @@ -0,0 +1,230 @@ +// ============================================================================ +// tb_adc_pwdn_opcode.v +// +// AUDIT-S25: AD9484 power-down (PWDN) had been hard-tied to 1'b0 in +// `radar_receiver_final.v:246`. Combined with AUDIT-C13 (CSB hard-tied HIGH +// on the production board, no SPI access to the AD9484), the ADC was fully +// un-recoverable from a stuck state without dropping main power — which +// also drops the VBAT-backed BKPSRAM persistence (MCU-A4 OCXO warmup flag, +// MCU-A7 emergency persist flag) and forces a 180 s warmup soak. +// +// Fix: opcode 0x32 (reserved during AUDIT-C3 commit `24ef5e7`) now drives +// a new `host_adc_pwdn` register in `radar_system_top.v`, which feeds the +// `adc_pwdn` output pin via `radar_receiver_final.v`. +// +// This TB models the dispatch register-block fragment from +// radar_system_top.v (the part touching host_adc_pwdn) and asserts: +// +// T1: After reset, host_adc_pwdn == 0 (matches the historical hard-tied +// state at radar_receiver_final.v:246, so existing bringup behavior +// is preserved — power-on does NOT accidentally PWDN the ADC). +// +// T2: Opcode 0x32 with value bit[0]=1 sets host_adc_pwdn=1 next clock. +// +// T3: Opcode 0x32 with value bit[0]=0 clears host_adc_pwdn back to 0. +// +// T4: Opcode 0x32 only looks at usb_cmd_value[0] — upper bits are ignored +// (so a future expansion to a multi-bit ADC control field can repurpose +// upper bits without breaking back-compat). +// +// T5: Unrelated opcodes (0x33 = host_adc_format, 0x01 = radar_mode) do +// NOT disturb host_adc_pwdn — opcode dispatch is properly mutually +// exclusive. +// +// T6: Without cmd_valid_100m, opcode bus changes alone do NOT update +// host_adc_pwdn — the dispatcher only acts on validated commands. +// ============================================================================ +`timescale 1ns/1ps + +module tb_adc_pwdn_opcode; + + reg clk = 1'b0; + reg reset_n; + reg cmd_valid_100m; + reg [7:0] usb_cmd_opcode; + reg [31:0] usb_cmd_value; + + wire host_adc_pwdn; + wire [1:0] host_adc_format; + wire adc_pwdn_pin; // mirrors radar_receiver_final's `assign adc_pwdn = host_adc_pwdn` + + // ---------------------------------------------------------------- + // Production register block under test — mirrors the relevant + // fragment of radar_system_top.v (post AUDIT-S25 commit). Kept tight + // so the TB exercises the exact dispatch path that lives in prod. + // ---------------------------------------------------------------- + dispatch_block dut ( + .clk (clk), + .reset_n (reset_n), + .cmd_valid_100m (cmd_valid_100m), + .usb_cmd_opcode (usb_cmd_opcode), + .usb_cmd_value (usb_cmd_value), + .host_adc_pwdn (host_adc_pwdn), + .host_adc_format (host_adc_format) + ); + + // mirror radar_receiver_final.v: `assign adc_pwdn = host_adc_pwdn` + assign adc_pwdn_pin = host_adc_pwdn; + + // 100 MHz clock + always #5 clk = ~clk; + + // Pass/fail bookkeeping + integer pass_count = 0; + integer fail_count = 0; + task check; + input cond; + input [255:0] label; + begin + if (cond) begin + pass_count = pass_count + 1; + $display(" [PASS] %0s", label); + end else begin + fail_count = fail_count + 1; + $display(" [FAIL] %0s (host_adc_pwdn=%0b adc_pwdn_pin=%0b)", + label, host_adc_pwdn, adc_pwdn_pin); + end + end + endtask + + task issue_opcode; + input [7:0] opc; + input [31:0] val; + begin + @(posedge clk); + usb_cmd_opcode <= opc; + usb_cmd_value <= val; + cmd_valid_100m <= 1'b1; + @(posedge clk); + cmd_valid_100m <= 1'b0; + usb_cmd_opcode <= 8'h00; + usb_cmd_value <= 32'h0; + @(posedge clk); // settle + end + endtask + + initial begin + $display("================================================"); + $display(" AUDIT-S25: opcode 0x32 -> host_adc_pwdn -> pin"); + $display("================================================"); + + // ---------- T1: reset state ---------- + reset_n = 1'b0; + cmd_valid_100m = 1'b0; + usb_cmd_opcode = 8'h00; + usb_cmd_value = 32'h0; + repeat (4) @(posedge clk); + reset_n = 1'b1; + @(posedge clk); + check(host_adc_pwdn === 1'b0, "T1: reset -> host_adc_pwdn = 0"); + check(adc_pwdn_pin === 1'b0, "T1: reset -> adc_pwdn pin = 0 (ADC powered up)"); + check(host_adc_format === 2'b00, "T1: reset -> host_adc_format = 2'b00 (sister reg sanity)"); + + // ---------- T2: assert PWDN via opcode 0x32 value=1 ---------- + issue_opcode(8'h32, 32'h0000_0001); + check(host_adc_pwdn === 1'b1, "T2: opcode 0x32 val=1 -> host_adc_pwdn = 1"); + check(adc_pwdn_pin === 1'b1, "T2: opcode 0x32 val=1 -> adc_pwdn pin = 1 (PWDN asserted)"); + + // ---------- T3: deassert PWDN via opcode 0x32 value=0 ---------- + issue_opcode(8'h32, 32'h0000_0000); + check(host_adc_pwdn === 1'b0, "T3: opcode 0x32 val=0 -> host_adc_pwdn = 0"); + check(adc_pwdn_pin === 1'b0, "T3: opcode 0x32 val=0 -> adc_pwdn pin = 0"); + + // ---------- T4: only bit[0] is consumed ---------- + // Set host_adc_pwdn high first. + issue_opcode(8'h32, 32'h0000_0001); + check(host_adc_pwdn === 1'b1, "T4-prep: PWDN re-asserted"); + // Now write opcode 0x32 with bit[0]=0 but bits[31:1] all set. + // Production semantics is `host_adc_pwdn <= usb_cmd_value[0];` so the + // upper bits must be ignored — bit[0]=0 wins. + issue_opcode(8'h32, 32'hFFFF_FFFE); + check(host_adc_pwdn === 1'b0, "T4: opcode 0x32 val=0xFFFF_FFFE (bit0=0) -> host_adc_pwdn = 0 (upper bits ignored)"); + + // ---------- T5: unrelated opcodes don't disturb PWDN ---------- + issue_opcode(8'h32, 32'h0000_0001); + check(host_adc_pwdn === 1'b1, "T5-prep: PWDN re-asserted"); + // Issue opcode 0x33 (host_adc_format) — must NOT touch host_adc_pwdn. + issue_opcode(8'h33, 32'h0000_0001); + check(host_adc_pwdn === 1'b1, "T5: opcode 0x33 doesn't disturb host_adc_pwdn"); + check(host_adc_format === 2'b01, "T5: opcode 0x33 updates host_adc_format independently"); + // Issue opcode 0x01 (radar_mode) — must NOT touch host_adc_pwdn. + issue_opcode(8'h01, 32'h0000_0002); + check(host_adc_pwdn === 1'b1, "T5: opcode 0x01 doesn't disturb host_adc_pwdn"); + + // ---------- T6: opcode bus changes without cmd_valid_100m don't latch ---------- + // Snap state, drive opcode/value but withhold cmd_valid_100m. + @(posedge clk); + usb_cmd_opcode <= 8'h32; + usb_cmd_value <= 32'h0000_0000; + cmd_valid_100m <= 1'b0; + @(posedge clk); + @(posedge clk); + check(host_adc_pwdn === 1'b1, "T6: opcode 0x32 + val=0 without cmd_valid -> host_adc_pwdn unchanged (still 1)"); + + // Now actually pulse cmd_valid_100m. + cmd_valid_100m <= 1'b1; + @(posedge clk); + cmd_valid_100m <= 1'b0; + @(posedge clk); + check(host_adc_pwdn === 1'b0, "T6: opcode 0x32 + val=0 WITH cmd_valid -> host_adc_pwdn cleared"); + + // ---------- Summary ---------- + $display("================================================"); + $display(" RESULTS: %0d passed, %0d failed", pass_count, fail_count); + $display("================================================"); + if (fail_count == 0) $finish; + else $fatal(1, "FAIL"); + end + + // Watchdog + initial begin + #10000; + $display("[FAIL] watchdog timeout"); + $fatal(1, "WATCHDOG"); + end + +endmodule + + +// ============================================================================ +// dispatch_block: minimal mirror of the relevant fragment of +// radar_system_top.v's host-register block (the AUDIT-S25 + AUDIT-C3 + a +// representative third opcode 0x01 used to demonstrate dispatch isolation). +// +// IMPORTANT: this is a *copy* of the production logic, not the production +// module. If radar_system_top.v's dispatch logic changes shape (e.g., +// pipelining the opcode bus, adding an enable mask), this TB will need to be +// updated to match — a deliberate trip-wire so the dispatch contract gets +// re-verified during structural changes. +// ============================================================================ +module dispatch_block ( + input wire clk, + input wire reset_n, + input wire cmd_valid_100m, + input wire [7:0] usb_cmd_opcode, + input wire [31:0] usb_cmd_value, + output reg host_adc_pwdn, + output reg [1:0] host_adc_format +); + + // Dummy reg for opcode 0x01 (radar_mode) — exercised only by T5. + reg [1:0] host_radar_mode; + + always @(posedge clk or negedge reset_n) begin + if (!reset_n) begin + host_adc_pwdn <= 1'b0; + host_adc_format <= 2'b00; + host_radar_mode <= 2'b00; + end else begin + if (cmd_valid_100m) begin + case (usb_cmd_opcode) + 8'h01: host_radar_mode <= usb_cmd_value[1:0]; + 8'h32: host_adc_pwdn <= usb_cmd_value[0]; + 8'h33: host_adc_format <= usb_cmd_value[1:0]; + default: ; + endcase + end + end + end + +endmodule