@@ -217,13 +217,26 @@ pub enum FpgaCmd {
217217 ///
218218 /// Asserts the canonical 200T configuration: IDCODE 0x03636093, SPI x1,
219219 /// CCLK startup, OSCFSEL=0, and no CRC register writes.
220+ ///
221+ /// With `--require-cable`, also detect the Digilent FTDI cable, load the
222+ /// bitstream into FPGA SRAM via openFPGALoader, and assert DONE=HIGH.
220223 SmokeGate {
221224 /// Bitstream to audit (default: fpga/verilog/ternary_mac_demo_top_200t.bit).
222225 #[ arg( long) ]
223226 bit : Option < PathBuf > ,
224227 /// Verilog top module to synthesize (default: ternary_mac_demo_top).
225228 #[ arg( long, default_value = "ternary_mac_demo_top" ) ]
226229 top : String ,
230+ /// Require a connected Digilent cable and load the bitstream into SRAM.
231+ /// If no device is detected, the gate fails. Board-less checks still run.
232+ #[ arg( long) ]
233+ require_cable : bool ,
234+ /// openFPGALoader cable profile for the cable-connected check.
235+ #[ arg( long, default_value = "digilent_hs2" ) ]
236+ cable : String ,
237+ /// FPGA part/package for openFPGALoader (default: xc7a200tfgg676).
238+ #[ arg( long, default_value = "xc7a200tfgg676" ) ]
239+ part : String ,
227240 } ,
228241 /// Print or interactively confirm the cold-POR boot protocol. This is the
229242 /// standalone version of the instructions embedded in `boot-log` and
@@ -524,7 +537,13 @@ pub fn run(cmd: &FpgaCmd) -> Result<()> {
524537 wait_seconds,
525538 log_dir,
526539 } => boot_log ( bit, cable, part, bridge. as_ref ( ) , * freq, * repeat, * wait_seconds, log_dir. as_ref ( ) ) ,
527- FpgaCmd :: SmokeGate { bit, top } => smoke_gate ( bit. as_ref ( ) , top) ,
540+ FpgaCmd :: SmokeGate {
541+ bit,
542+ top,
543+ require_cable,
544+ cable,
545+ part,
546+ } => smoke_gate ( bit. as_ref ( ) , top, * require_cable, cable, part) ,
528547 FpgaCmd :: BootProtocol { checklist } => boot_protocol ( * checklist) ,
529548 FpgaCmd :: PatchCor0 { bit, out, oscfsel } => patch_cor0 ( bit, out, * oscfsel) ,
530549 FpgaCmd :: CclkVariants { bit, output_dir, values } => {
@@ -2636,15 +2655,86 @@ fn boot_log(
26362655 }
26372656}
26382657
2639- fn smoke_gate ( bit : Option < & PathBuf > , top : & str ) -> Result < ( ) > {
2658+ /// Detect whether a target FPGA is reachable on the given openFPGALoader
2659+ /// cable profile. Returns false when the chain is empty / cable missing.
2660+ fn cable_detected ( cable : & str ) -> bool {
2661+ let Ok ( ( _, Some ( output) ) ) = run_openfpgaloader ( cable, & [ "--detect" ] , true ) else {
2662+ return false ;
2663+ } ;
2664+ output. contains ( "idcode" ) && output. contains ( "manufacturer" )
2665+ }
2666+
2667+ /// Assert that the decoded STAT register shows a successful boot/config.
2668+ fn assert_stat_boot_success ( bits : & StatBits , ctx : & str ) -> Result < ( ) > {
2669+ if !bits. done {
2670+ bail ! ( "{}: DONE=LOW (raw=0x{:08X}, {})" , ctx, bits. raw, bits. diagnose( ) ) ;
2671+ }
2672+ if bits. mode != 0b001 {
2673+ bail ! (
2674+ "{}: MODE=0b{:03b} != 0b001 (Master SPI x1)" ,
2675+ ctx,
2676+ bits. mode
2677+ ) ;
2678+ }
2679+ if bits. crc_error {
2680+ bail ! ( "{}: CRC_ERROR=1" , ctx) ;
2681+ }
2682+ if bits. id_error {
2683+ bail ! ( "{}: ID_ERROR=1" , ctx) ;
2684+ }
2685+ if bits. dec_error {
2686+ bail ! ( "{}: DEC_ERROR=1" , ctx) ;
2687+ }
2688+ Ok ( ( ) )
2689+ }
2690+
2691+ fn smoke_gate (
2692+ bit : Option < & PathBuf > ,
2693+ top : & str ,
2694+ require_cable : bool ,
2695+ cable : & str ,
2696+ part : & str ,
2697+ ) -> Result < ( ) > {
26402698 let root = repo_root ( ) ?;
26412699 let bit_path = bit. cloned ( ) . unwrap_or_else ( || {
26422700 root. join ( "fpga" )
26432701 . join ( "verilog" )
26442702 . join ( "ternary_mac_demo_top_200t.bit" )
26452703 } ) ;
26462704
2647- println ! ( "== FPGA board-less smoke gate ==" ) ;
2705+ println ! ( "== FPGA smoke gate ==" ) ;
2706+
2707+ // Cable-connected hardware check (optional, gated by --require-cable).
2708+ if require_cable {
2709+ println ! ( "[smoke-gate] require-cable: detecting FPGA via {}..." , cable) ;
2710+ if !cable_detected ( cable) {
2711+ bail ! (
2712+ "no FPGA detected on cable {} (is the board powered and connected?)" ,
2713+ cable
2714+ ) ;
2715+ }
2716+ println ! ( "[smoke-gate] cable OK (FPGA detected)" ) ;
2717+
2718+ if !bit_path. is_file ( ) {
2719+ bail ! (
2720+ "bitstream not found at {} (required for --require-cable)" ,
2721+ bit_path. display( )
2722+ ) ;
2723+ }
2724+
2725+ println ! (
2726+ "[smoke-gate] loading SRAM: {}" ,
2727+ bit_path. display( )
2728+ ) ;
2729+ load_sram ( & bit_path, cable, part, false , false ) ?;
2730+
2731+ println ! ( "[smoke-gate] reading STAT after SRAM load..." ) ;
2732+ let samples = capture_stat ( cable, false , 1 ) ?;
2733+ let bits = samples. first ( ) . cloned ( ) . expect ( "at least one STAT sample" ) ;
2734+ assert_stat_boot_success ( & bits, "SRAM load" )
2735+ . with_context ( || "hardware smoke-gate failed after SRAM load" ) ?;
2736+ println ! ( "[smoke-gate] hardware check OK (DONE=HIGH, mode=001, no errors)" ) ;
2737+ }
26482738
26492739 // 1. bit-config audit if the bitstream exists.
26502740 if bit_path. is_file ( ) {
0 commit comments