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
This commit is contained in:
Jason
2026-04-29 19:37:37 +05:45
parent 95aed35d89
commit 59f3c82fbb
4 changed files with 267 additions and 5 deletions

View File

@@ -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 (

View File

@@ -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: ;

View File

@@ -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 ""
# ===========================================================================

View File

@@ -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