diff --git a/ip/bin/aegis_genip.dart b/ip/bin/aegis_genip.dart index f80198e..024ce14 100644 --- a/ip/bin/aegis_genip.dart +++ b/ip/bin/aegis_genip.dart @@ -19,6 +19,11 @@ ArgParser buildParser() { abbr: 'c', help: 'Adds a clock domain for tile configuration', ) + ..addFlag( + 'jtag', + abbr: 'j', + help: 'Adds a JTAG TAP controller for configuration and debug', + ) ..addOption( 'output', abbr: 'o', @@ -115,6 +120,7 @@ Future main(List arguments) async { padIn: Logic(width: 2 * width + 2 * height), serialIn: Logic(width: serdesCount), configClk: results.flag('config-clk') ? Logic() : null, + enableJtag: results.flag('jtag'), configReadPort: DataPortInterface( int.parse(results.option('config-data-width') ?? '8'), int.parse(results.option('config-address-width') ?? '8'), @@ -227,6 +233,7 @@ Future main(List arguments) async { bramDataWidth: 8, bramAddrWidth: 7, bramColumnInterval: bramInterval, + hasJtag: results.flag('jtag'), ); File( '$outputDir/${fpga.name}_cells.v', @@ -252,6 +259,7 @@ Future main(List arguments) async { bramColumnInterval: bramInterval, dspColumnInterval: dspInterval, hasConfigClk: results.flag('config-clk'), + hasJtag: results.flag('jtag'), ); File( '$outputDir/${fpga.name}-openroad.tcl', diff --git a/ip/lib/src/components.dart b/ip/lib/src/components.dart index 287ac23..edcaf03 100644 --- a/ip/lib/src/components.dart +++ b/ip/lib/src/components.dart @@ -11,6 +11,7 @@ export 'components/digital/fabric_config_loader.dart'; export 'components/digital/fpga.dart'; export 'components/digital/io_fabric.dart'; export 'components/digital/io_tile.dart'; +export 'components/digital/jtag_tap.dart'; export 'components/digital/lut4.dart'; export 'components/digital/serdes_tile.dart'; export 'components/digital/tile.dart'; diff --git a/ip/lib/src/components/digital/fpga.dart b/ip/lib/src/components/digital/fpga.dart index a783494..8872468 100644 --- a/ip/lib/src/components/digital/fpga.dart +++ b/ip/lib/src/components/digital/fpga.dart @@ -8,6 +8,7 @@ import 'dsp_basic_tile.dart'; import 'fabric.dart'; import 'fabric_config_loader.dart'; import 'io_fabric.dart'; +import 'jtag_tap.dart'; import 'io_tile.dart'; import 'serdes_tile.dart'; import 'tile.dart'; @@ -24,6 +25,7 @@ class AegisFPGA extends Module { final int bramAddrWidth; final int dspColumnInterval; final int clockTileCount; + final bool enableJtag; /// Total I/O pads (2*width + 2*height). int get totalPads => 2 * width + 2 * height; @@ -41,6 +43,7 @@ class AegisFPGA extends Module { this.bramAddrWidth = 7, this.dspColumnInterval = 0, this.clockTileCount = 1, + this.enableJtag = false, required Logic padIn, required Logic serialIn, Logic? configClk, @@ -68,6 +71,32 @@ class AegisFPGA extends Module { configClk = addInput('configClk', configClk); } + // JTAG ports (optional) + JtagTap? jtag; + if (enableJtag) { + final tck = addInput('tck', Logic()); + final tms = addInput('tms', Logic()); + final tdi = addInput('tdi', Logic()); + final trst = addInput('trst', Logic()); + final userTdo = addInput('userTdo', Logic()); + addOutput('tdo'); + + // User data register interface - exposed to fabric for debug + addOutput('userTdi'); + addOutput('userShift'); + addOutput('userUpdate'); + addOutput('userCapture'); + addOutput('userReset'); + + jtag = JtagTap(tck, tms, tdi, trst, userTdo: userTdo); + output('tdo') <= jtag.tdo; + output('userTdi') <= jtag.userTdi; + output('userShift') <= jtag.userShift; + output('userUpdate') <= jtag.userUpdate; + output('userCapture') <= jtag.userCapture; + output('userReset') <= jtag.userReset; + } + configReadPort = configReadPort.clone() ..connectIO( this, @@ -97,24 +126,35 @@ class AegisFPGA extends Module { configReadPort, ); - configDone <= loader.done; + // When JTAG is enabled, a mux selects which source drives the config + // chain. The TAP's enableConfig output controls the switch. + final cfgIn = Logic(name: 'cfgIn'); + final cfgLoad = Logic(name: 'cfgLoad'); + final cfgReset = Logic(name: 'cfgReset'); + + if (jtag != null) { + cfgIn <= mux(jtag.enableConfig, jtag.cfgIn, loader.cfgIn); + cfgLoad <= mux(jtag.enableConfig, jtag.cfgLoad, loader.cfgLoad); + cfgReset <= reset | (jtag.enableConfig & jtag.cfgReset); + configDone <= loader.done; + } else { + cfgIn <= loader.cfgIn; + cfgLoad <= loader.cfgLoad; + cfgReset <= reset; + configDone <= loader.done; + } // Clock tiles - config chain: clock tiles -> IO fabric -> LUT fabric final clockTiles = []; - var cfgChain = loader.cfgIn; + var cfgChainSig = cfgIn; for (int i = 0; i < clockTileCount; i++) { final tileCfgIn = Logic(name: 'clkCfgIn_$i'); - tileCfgIn <= cfgChain; + tileCfgIn <= cfgChainSig; - final ct = ClockTile( - clk, - ~loader.done | reset, - tileCfgIn, - loader.cfgLoad, - ); + final ct = ClockTile(clk, cfgReset, tileCfgIn, cfgLoad); clockTiles.add(ct); - cfgChain = ct.cfgOut; + cfgChainSig = ct.cfgOut; } // Collect clock outputs @@ -134,7 +174,7 @@ class AegisFPGA extends Module { // IO fabric receives config chain after clock tiles final ioFabric = IOFabric( clk, - ~loader.done | reset, + cfgReset, width: width, height: height, tracks: tracks, @@ -143,8 +183,8 @@ class AegisFPGA extends Module { bramDataWidth: bramDataWidth, bramAddrWidth: bramAddrWidth, dspColumnInterval: dspColumnInterval, - cfgIn: cfgChain, - cfgLoad: loader.cfgLoad, + cfgIn: cfgChainSig, + cfgLoad: cfgLoad, padIn: padIn, serialIn: serialIn, ); @@ -218,6 +258,7 @@ class AegisFPGA extends Module { ), 'serdes': SerDesTile.descriptor(count: serdesCount), 'clock': ClockTile.descriptor(count: clockTileCount), + if (enableJtag) 'jtag': JtagTap.descriptor(idcode: 0x00000001), 'config': { 'total_bits': totalBits, 'chain_order': [ diff --git a/ip/lib/src/components/digital/jtag_tap.dart b/ip/lib/src/components/digital/jtag_tap.dart new file mode 100644 index 0000000..fa626b2 --- /dev/null +++ b/ip/lib/src/components/digital/jtag_tap.dart @@ -0,0 +1,341 @@ +import 'package:rohd/rohd.dart'; + +/// IEEE 1149.1 JTAG TAP controller. +/// +/// Provides standard JTAG access to the FPGA configuration chain via +/// TCK, TMS, TDI, TDO pins. Supports BYPASS, IDCODE, and CONFIG +/// instructions. +/// +/// Instruction register layout (4 bits): +/// 0000 = EXTEST (mapped to BYPASS) +/// 0001 = IDCODE +/// 0010 = CONFIG (shift bits into fabric config chain) +/// 1111 = BYPASS +/// +/// The CONFIG instruction connects TDI directly to the config chain +/// input (cfgIn) during Shift-DR, and asserts cfgLoad on Update-DR. +class JtagTap extends Module { + Logic get tdo => output('tdo'); + Logic get cfgIn => output('cfgIn'); + Logic get cfgLoad => output('cfgLoad'); + Logic get cfgReset => output('cfgReset'); + Logic get enableConfig => output('enableConfig'); + + // User data register interface - exposed to fabric for debug + Logic get userTdi => output('userTdi'); + Logic get userShift => output('userShift'); + Logic get userUpdate => output('userUpdate'); + Logic get userCapture => output('userCapture'); + Logic get userReset => output('userReset'); + Logic get enableUser => output('enableUser'); + + /// 32-bit device ID code. + final int idcode; + + static const int IR_WIDTH = 4; + + // Instruction opcodes + static const int EXTEST = 0x0; + static const int IDCODE_INST = 0x1; + static const int CONFIG = 0x2; + static const int USER = 0x3; + static const int BYPASS = 0xF; + + // TAP states + static const int TEST_LOGIC_RESET = 0; + static const int RUN_TEST_IDLE = 1; + static const int SELECT_DR_SCAN = 2; + static const int CAPTURE_DR = 3; + static const int SHIFT_DR = 4; + static const int EXIT1_DR = 5; + static const int PAUSE_DR = 6; + static const int EXIT2_DR = 7; + static const int UPDATE_DR = 8; + static const int SELECT_IR_SCAN = 9; + static const int CAPTURE_IR = 10; + static const int SHIFT_IR = 11; + static const int EXIT1_IR = 12; + static const int PAUSE_IR = 13; + static const int EXIT2_IR = 14; + static const int UPDATE_IR = 15; + + JtagTap( + Logic tck, + Logic tms, + Logic tdi, + Logic trst, { + this.idcode = 0x00000001, + Logic? userTdo, + }) : super(name: 'jtag_tap') { + tck = addInput('tck', tck); + tms = addInput('tms', tms); + tdi = addInput('tdi', tdi); + trst = addInput('trst', trst); + + if (userTdo != null) { + userTdo = addInput('userTdoIn', userTdo); + } + + addOutput('tdo'); + addOutput('cfgIn'); + addOutput('cfgLoad'); + addOutput('cfgReset'); + addOutput('enableConfig'); + addOutput('userTdi'); + addOutput('userShift'); + addOutput('userUpdate'); + addOutput('userCapture'); + addOutput('userReset'); + addOutput('enableUser'); + + // TAP state machine + final state = Logic(width: 4, name: 'state'); + + // Instruction register + final irShift = Logic(width: IR_WIDTH, name: 'irShift'); + final irReg = Logic(width: IR_WIDTH, name: 'irReg'); + + // Data registers + final bypassReg = Logic(name: 'bypassReg'); + final idcodeShift = Logic(width: 32, name: 'idcodeShift'); + + // Config data register - just passes through to cfgIn + final configBit = Logic(name: 'configBit'); + + // TAP state machine transitions (IEEE 1149.1) + final nextState = Logic(width: 4, name: 'nextState'); + + Combinational([ + Case( + state, + [ + CaseItem(Const(TEST_LOGIC_RESET, width: 4), [ + nextState < + mux( + tms, + Const(TEST_LOGIC_RESET, width: 4), + Const(RUN_TEST_IDLE, width: 4), + ), + ]), + CaseItem(Const(RUN_TEST_IDLE, width: 4), [ + nextState < + mux( + tms, + Const(SELECT_DR_SCAN, width: 4), + Const(RUN_TEST_IDLE, width: 4), + ), + ]), + CaseItem(Const(SELECT_DR_SCAN, width: 4), [ + nextState < + mux( + tms, + Const(SELECT_IR_SCAN, width: 4), + Const(CAPTURE_DR, width: 4), + ), + ]), + CaseItem(Const(CAPTURE_DR, width: 4), [ + nextState < + mux(tms, Const(EXIT1_DR, width: 4), Const(SHIFT_DR, width: 4)), + ]), + CaseItem(Const(SHIFT_DR, width: 4), [ + nextState < + mux(tms, Const(EXIT1_DR, width: 4), Const(SHIFT_DR, width: 4)), + ]), + CaseItem(Const(EXIT1_DR, width: 4), [ + nextState < + mux(tms, Const(UPDATE_DR, width: 4), Const(PAUSE_DR, width: 4)), + ]), + CaseItem(Const(PAUSE_DR, width: 4), [ + nextState < + mux(tms, Const(EXIT2_DR, width: 4), Const(PAUSE_DR, width: 4)), + ]), + CaseItem(Const(EXIT2_DR, width: 4), [ + nextState < + mux(tms, Const(UPDATE_DR, width: 4), Const(SHIFT_DR, width: 4)), + ]), + CaseItem(Const(UPDATE_DR, width: 4), [ + nextState < + mux( + tms, + Const(SELECT_DR_SCAN, width: 4), + Const(RUN_TEST_IDLE, width: 4), + ), + ]), + CaseItem(Const(SELECT_IR_SCAN, width: 4), [ + nextState < + mux( + tms, + Const(TEST_LOGIC_RESET, width: 4), + Const(CAPTURE_IR, width: 4), + ), + ]), + CaseItem(Const(CAPTURE_IR, width: 4), [ + nextState < + mux(tms, Const(EXIT1_IR, width: 4), Const(SHIFT_IR, width: 4)), + ]), + CaseItem(Const(SHIFT_IR, width: 4), [ + nextState < + mux(tms, Const(EXIT1_IR, width: 4), Const(SHIFT_IR, width: 4)), + ]), + CaseItem(Const(EXIT1_IR, width: 4), [ + nextState < + mux(tms, Const(UPDATE_IR, width: 4), Const(PAUSE_IR, width: 4)), + ]), + CaseItem(Const(PAUSE_IR, width: 4), [ + nextState < + mux(tms, Const(EXIT2_IR, width: 4), Const(PAUSE_IR, width: 4)), + ]), + CaseItem(Const(EXIT2_IR, width: 4), [ + nextState < + mux(tms, Const(UPDATE_IR, width: 4), Const(SHIFT_IR, width: 4)), + ]), + CaseItem(Const(UPDATE_IR, width: 4), [ + nextState < + mux( + tms, + Const(SELECT_DR_SCAN, width: 4), + Const(RUN_TEST_IDLE, width: 4), + ), + ]), + ], + defaultItem: [nextState < Const(TEST_LOGIC_RESET, width: 4)], + ), + ]); + + Sequential( + tck, + [ + If( + trst, + then: [ + state < Const(TEST_LOGIC_RESET, width: 4), + irReg < Const(IDCODE_INST, width: IR_WIDTH), + ], + orElse: [ + state < nextState, + + // IR shift register + If( + state.eq(Const(CAPTURE_IR, width: 4)), + then: [irShift < irReg], + orElse: [ + If( + state.eq(Const(SHIFT_IR, width: 4)), + then: [ + irShift < [tdi, irShift.slice(IR_WIDTH - 1, 1)].swizzle(), + ], + ), + ], + ), + + // IR update + If(state.eq(Const(UPDATE_IR, width: 4)), then: [irReg < irShift]), + + // DR shift registers + If( + state.eq(Const(CAPTURE_DR, width: 4)), + then: [ + // Load capture values + If( + irReg.eq(Const(IDCODE_INST, width: IR_WIDTH)), + then: [idcodeShift < Const(idcode, width: 32)], + ), + bypassReg < Const(0), + configBit < Const(0), + ], + orElse: [ + If( + state.eq(Const(SHIFT_DR, width: 4)), + then: [ + If( + irReg.eq(Const(IDCODE_INST, width: IR_WIDTH)), + then: [ + idcodeShift < [tdi, idcodeShift.slice(31, 1)].swizzle(), + ], + orElse: [ + If( + irReg.eq(Const(CONFIG, width: IR_WIDTH)), + then: [configBit < tdi], + orElse: [ + // BYPASS and EXTEST + bypassReg < tdi, + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + reset: trst, + resetValues: { + state: Const(TEST_LOGIC_RESET, width: 4), + irShift: Const(IDCODE_INST, width: IR_WIDTH), + irReg: Const(IDCODE_INST, width: IR_WIDTH), + bypassReg: Const(0), + idcodeShift: Const(idcode, width: 32), + configBit: Const(0), + }, + ); + + // TDO mux - select based on current instruction + final isShiftIr = state.eq(Const(SHIFT_IR, width: 4)); + final isShiftDr = state.eq(Const(SHIFT_DR, width: 4)); + + Logic drOut = bypassReg; + drOut = mux( + irReg.eq(Const(IDCODE_INST, width: IR_WIDTH)), + idcodeShift[0], + drOut, + ); + drOut = mux(irReg.eq(Const(CONFIG, width: IR_WIDTH)), configBit, drOut); + // USER DR: TDO comes from the user design via userTdoIn + drOut = mux( + irReg.eq(Const(USER, width: IR_WIDTH)), + userTdo != null ? input('userTdoIn') : Const(0), + drOut, + ); + + tdo <= mux(isShiftIr, irShift[0], mux(isShiftDr, drOut, Const(0))); + + // Config chain outputs + final inConfigMode = irReg.eq(Const(CONFIG, width: IR_WIDTH)); + final inShiftDr = state.eq(Const(SHIFT_DR, width: 4)); + final inUpdateDr = state.eq(Const(UPDATE_DR, width: 4)); + final inCaptureDr = state.eq(Const(CAPTURE_DR, width: 4)); + + cfgIn <= mux(inConfigMode & inShiftDr, tdi, Const(0)); + cfgLoad <= inConfigMode & inUpdateDr; + cfgReset <= state.eq(Const(TEST_LOGIC_RESET, width: 4)) | trst; + enableConfig <= inConfigMode; + + // User data register interface - active when IR = USER + final inUserMode = irReg.eq(Const(USER, width: IR_WIDTH)); + output('userTdi') <= tdi; + output('userShift') <= inUserMode & inShiftDr; + output('userUpdate') <= inUserMode & inUpdateDr; + output('userCapture') <= inUserMode & inCaptureDr; + output('userReset') <= state.eq(Const(TEST_LOGIC_RESET, width: 4)) | trst; + output('enableUser') <= inUserMode; + } + + static const int CONFIG_WIDTH = 0; + + static Map descriptor({required int idcode}) { + return { + 'enabled': true, + 'idcode': '0x${idcode.toRadixString(16).padLeft(8, '0')}', + 'ir_width': IR_WIDTH, + 'instructions': { + 'EXTEST': EXTEST, + 'IDCODE': IDCODE_INST, + 'CONFIG': CONFIG, + 'USER': USER, + 'BYPASS': BYPASS, + }, + }; + } +} diff --git a/ip/lib/src/openroad/tcl_emitter.dart b/ip/lib/src/openroad/tcl_emitter.dart index 0c590ed..a917c88 100644 --- a/ip/lib/src/openroad/tcl_emitter.dart +++ b/ip/lib/src/openroad/tcl_emitter.dart @@ -14,6 +14,7 @@ class OpenroadTclEmitter { final int bramColumnInterval; final int dspColumnInterval; final bool hasConfigClk; + final bool hasJtag; int get totalPads => 2 * width + 2 * height; @@ -34,6 +35,7 @@ class OpenroadTclEmitter { this.bramColumnInterval = 0, this.dspColumnInterval = 0, this.hasConfigClk = false, + this.hasJtag = false, this.macroHaloUm = 100, this.gridMarginUm = 200, }); @@ -234,6 +236,9 @@ class OpenroadTclEmitter { 'configRead_addr\\[*\\]', 'configRead_data\\[*\\]', ]); + if (hasJtag) { + westPins.addAll(['tck', 'tms', 'tdi', 'tdo', 'trst']); + } final eastPins = []; if (serdesCount > 0) { @@ -562,7 +567,7 @@ class OpenroadTclEmitter { buf.writeln(' # Place Clock and ConfigLoader on right edge'); buf.writeln(' set edge_x [expr {\$die_w - \$margin}]'); buf.writeln(' set edge_y \$margin'); - buf.writeln(' foreach type {ClockTile FabricConfigLoader} {'); + buf.writeln(' foreach type {ClockTile FabricConfigLoader JtagTap} {'); buf.writeln(' if {[info exists macro_groups(\$type)]} {'); buf.writeln(' foreach inst \$macro_groups(\$type) {'); buf.writeln( diff --git a/ip/lib/src/yosys/tcl_emitter.dart b/ip/lib/src/yosys/tcl_emitter.dart index ace1fda..55b2de6 100644 --- a/ip/lib/src/yosys/tcl_emitter.dart +++ b/ip/lib/src/yosys/tcl_emitter.dart @@ -23,6 +23,7 @@ class YosysTclEmitter { 'IOTile', 'SerDesTile', 'FabricConfigLoader', + 'JtagTap', ]; /// Backward compat alias diff --git a/ip/lib/src/yosys/techmap_emitter.dart b/ip/lib/src/yosys/techmap_emitter.dart index ab08d64..0dc11a9 100644 --- a/ip/lib/src/yosys/techmap_emitter.dart +++ b/ip/lib/src/yosys/techmap_emitter.dart @@ -14,6 +14,8 @@ class YosysTechmapEmitter { final int bramColumnInterval; int get bramDepth => 1 << bramAddrWidth; + final bool hasJtag; + int get totalLuts => width * height; const YosysTechmapEmitter({ @@ -24,6 +26,7 @@ class YosysTechmapEmitter { this.bramDataWidth = 8, this.bramAddrWidth = 7, this.bramColumnInterval = 0, + this.hasJtag = false, }); /// Generate Verilog blackbox cell definitions for Aegis primitives. @@ -40,6 +43,9 @@ class YosysTechmapEmitter { if (bramColumnInterval > 0) { _writeBramCell(buf); } + if (hasJtag) { + _writeJtagCell(buf); + } return buf.toString(); } @@ -287,4 +293,18 @@ class YosysTechmapEmitter { buf.writeln('// passed to memory_bram -rules in the synth script.'); buf.writeln(); } + + void _writeJtagCell(StringBuffer buf) { + buf.writeln('(* blackbox *)'); + buf.writeln('module AEGIS_JTAG ('); + buf.writeln(' output wire tdi,'); + buf.writeln(' input wire tdo,'); + buf.writeln(' output wire shift,'); + buf.writeln(' output wire update,'); + buf.writeln(' output wire capture,'); + buf.writeln(' output wire reset'); + buf.writeln(');'); + buf.writeln('endmodule'); + buf.writeln(); + } } diff --git a/ip/test/components/jtag_tap_test.dart b/ip/test/components/jtag_tap_test.dart new file mode 100644 index 0000000..089e5dd --- /dev/null +++ b/ip/test/components/jtag_tap_test.dart @@ -0,0 +1,638 @@ +import 'dart:async'; +import 'package:aegis_ip/aegis_ip.dart'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + late Logic tck; + late Logic tms; + late Logic tdi; + late Logic trst; + late Logic userTdoIn; + late JtagTap tap; + + Future setupTap({int idcode = 0xDEADBEEF}) async { + final clkGen = SimpleClockGenerator(10); + tck = clkGen.clk; + tms = Logic(); + tdi = Logic(); + trst = Logic(); + userTdoIn = Logic(); + + tap = JtagTap(tck, tms, tdi, trst, idcode: idcode, userTdo: userTdoIn); + await tap.build(); + + unawaited(Simulator.run()); + + // Reset + trst.put(1); + tms.put(0); + tdi.put(0); + userTdoIn.put(0); + await tck.nextPosedge; + await tck.nextPosedge; + trst.put(0); + await tck.nextPosedge; + } + + /// Navigate TAP from Test-Logic-Reset to Run-Test/Idle. + Future gotoIdle() async { + tms.put(0); + await tck.nextPosedge; + } + + /// Navigate TAP from Run-Test/Idle to Shift-IR. + Future gotoShiftIr() async { + tms.put(1); + await tck.nextPosedge; // RTI -> Select-DR-Scan + tms.put(1); + await tck.nextPosedge; // Select-DR-Scan -> Select-IR-Scan + tms.put(0); + await tck.nextPosedge; // Select-IR-Scan -> Capture-IR + tms.put(0); + await tck.nextPosedge; // Capture-IR -> Shift-IR + } + + /// Navigate TAP from Run-Test/Idle to Shift-DR. + Future gotoShiftDr() async { + tms.put(1); + await tck.nextPosedge; // RTI -> Select-DR-Scan + tms.put(0); + await tck.nextPosedge; // Select-DR-Scan -> Capture-DR + tms.put(0); + await tck.nextPosedge; // Capture-DR -> Shift-DR + } + + /// Shift bits into IR, then Update-IR -> RTI. + Future loadIr(int value) async { + for (int i = 0; i < JtagTap.IR_WIDTH; i++) { + tdi.put((value >> i) & 1); + tms.put(i == JtagTap.IR_WIDTH - 1 ? 1 : 0); + await tck.nextPosedge; + } + // Exit1-IR -> Update-IR + tms.put(1); + await tck.nextPosedge; + // Update-IR -> RTI + tms.put(0); + await tck.nextPosedge; + } + + /// Shift N bits out of DR, return captured value. Ends in RTI. + Future readDr(int width) async { + int result = 0; + for (int i = 0; i < width; i++) { + result |= (tap.tdo.value.toInt() & 1) << i; + tms.put(i == width - 1 ? 1 : 0); + tdi.put(0); + await tck.nextPosedge; + } + // Exit1-DR -> Update-DR + tms.put(1); + await tck.nextPosedge; + // Update-DR -> RTI + tms.put(0); + await tck.nextPosedge; + return result; + } + + /// Shift N bits into DR, then Update-DR -> RTI. + Future writeDr(int value, int width) async { + for (int i = 0; i < width; i++) { + tdi.put((value >> i) & 1); + tms.put(i == width - 1 ? 1 : 0); + await tck.nextPosedge; + } + // Exit1-DR -> Update-DR + tms.put(1); + await tck.nextPosedge; + // Update-DR -> RTI + tms.put(0); + await tck.nextPosedge; + } + + group('JtagTap', () { + group('reset', () { + test('TRST asserts cfgReset', () async { + final clkGen = SimpleClockGenerator(10); + tck = clkGen.clk; + tms = Logic(); + tdi = Logic(); + trst = Logic(); + + tap = JtagTap(tck, tms, tdi, trst, idcode: 0xDEADBEEF); + await tap.build(); + + unawaited(Simulator.run()); + + tms.put(1); // keep TMS high so state stays in TLR + tdi.put(0); + trst.put(1); + await tck.nextPosedge; + // cfgReset should be 1 while TRST is asserted + expect(tap.cfgReset.value.toInt(), 1); + expect(tap.enableConfig.value.toInt(), 0); + expect(tap.cfgLoad.value.toInt(), 0); + + trst.put(0); + await tck.nextPosedge; + // State is TLR (TMS=1 keeps it in TLR), cfgReset still 1 + expect(tap.cfgReset.value.toInt(), 1); + + await Simulator.endSimulation(); + }); + + test('five TMS=1 clocks reach Test-Logic-Reset', () async { + await setupTap(); + await gotoIdle(); + // In RTI, cfgReset should be 0 + expect(tap.cfgReset.value.toInt(), 0); + + // Five TMS=1 clocks should return to TLR + for (int i = 0; i < 5; i++) { + tms.put(1); + await tck.nextPosedge; + } + expect(tap.cfgReset.value.toInt(), 1); + await Simulator.endSimulation(); + }); + }); + + group('IDCODE', () { + test('reads correct IDCODE after reset', () async { + await setupTap(); + // After reset, IR defaults to IDCODE + await gotoIdle(); + await gotoShiftDr(); + final idcode = await readDr(32); + expect(idcode, 0xDEADBEEF); + await Simulator.endSimulation(); + }); + + test('reads IDCODE after explicit IR load', () async { + await setupTap(); + await gotoIdle(); + await gotoShiftIr(); + await loadIr(JtagTap.IDCODE_INST); + await gotoShiftDr(); + final idcode = await readDr(32); + expect(idcode, 0xDEADBEEF); + await Simulator.endSimulation(); + }); + + test('custom IDCODE value', () async { + await setupTap(idcode: 0x12345678); + await gotoIdle(); + await gotoShiftDr(); + final idcode = await readDr(32); + expect(idcode, 0x12345678); + await Simulator.endSimulation(); + }); + }); + + group('BYPASS', () { + test('single-bit delay', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.BYPASS); + + await gotoShiftDr(); + + // Bypass reg initialized to 0 in Capture-DR + expect(tap.tdo.value.toInt(), 0); + + // Shift in a 1 + tdi.put(1); + tms.put(0); + await tck.nextPosedge; + // TDO now shows the previously shifted 1 + expect(tap.tdo.value.toInt(), 1); + + // Shift in a 0 + tdi.put(0); + await tck.nextPosedge; + expect(tap.tdo.value.toInt(), 0); + + // Exit + tms.put(1); + await tck.nextPosedge; // Exit1-DR + tms.put(1); + await tck.nextPosedge; // Update-DR + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + }); + + group('CONFIG', () { + test('enableConfig asserted when CONFIG loaded', () async { + await setupTap(); + await gotoIdle(); + expect(tap.enableConfig.value.toInt(), 0); + + await gotoShiftIr(); + await loadIr(JtagTap.CONFIG); + expect(tap.enableConfig.value.toInt(), 1); + + await Simulator.endSimulation(); + }); + + test('enableConfig deasserted after switching instruction', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.CONFIG); + expect(tap.enableConfig.value.toInt(), 1); + + await gotoShiftIr(); + await loadIr(JtagTap.BYPASS); + expect(tap.enableConfig.value.toInt(), 0); + + await Simulator.endSimulation(); + }); + + test('cfgIn mirrors TDI during Shift-DR', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.CONFIG); + + await gotoShiftDr(); + + // cfgIn should follow TDI while in Shift-DR with CONFIG + tdi.put(1); + tms.put(0); + await tck.nextPosedge; + expect(tap.cfgIn.value.toInt(), 1); + + tdi.put(0); + await tck.nextPosedge; + expect(tap.cfgIn.value.toInt(), 0); + + tdi.put(1); + await tck.nextPosedge; + expect(tap.cfgIn.value.toInt(), 1); + + // Exit Shift-DR + tms.put(1); + await tck.nextPosedge; // Exit1-DR + tms.put(1); + await tck.nextPosedge; // Update-DR + tms.put(0); + await tck.nextPosedge; // RTI + + // cfgIn should be 0 outside Shift-DR + expect(tap.cfgIn.value.toInt(), 0); + + await Simulator.endSimulation(); + }); + + test('cfgLoad asserted on Update-DR', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.CONFIG); + expect(tap.cfgLoad.value.toInt(), 0); + + await gotoShiftDr(); + tdi.put(1); + tms.put(0); + await tck.nextPosedge; // shift one bit + + tms.put(1); + await tck.nextPosedge; // Exit1-DR + expect(tap.cfgLoad.value.toInt(), 0); + + tms.put(1); + await tck.nextPosedge; // Update-DR + expect(tap.cfgLoad.value.toInt(), 1); + + tms.put(0); + await tck.nextPosedge; // RTI + expect(tap.cfgLoad.value.toInt(), 0); + + await Simulator.endSimulation(); + }); + + test('cfgLoad not asserted in non-CONFIG mode', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.BYPASS); + + await gotoShiftDr(); + tdi.put(1); + tms.put(0); + await tck.nextPosedge; + tms.put(1); + await tck.nextPosedge; // Exit1-DR + tms.put(1); + await tck.nextPosedge; // Update-DR + + expect(tap.cfgLoad.value.toInt(), 0); + + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + + test('multiple config shifts accumulate', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.CONFIG); + + // First shift sequence + await gotoShiftDr(); + for (int i = 0; i < 8; i++) { + tdi.put(i & 1); + tms.put(i == 7 ? 1 : 0); + await tck.nextPosedge; + } + tms.put(1); + await tck.nextPosedge; // Update-DR + expect(tap.cfgLoad.value.toInt(), 1); + tms.put(0); + await tck.nextPosedge; // RTI + expect(tap.cfgLoad.value.toInt(), 0); + + // Second shift sequence (no need to reload IR) + await gotoShiftDr(); + for (int i = 0; i < 8; i++) { + tdi.put((~i) & 1); + tms.put(i == 7 ? 1 : 0); + await tck.nextPosedge; + } + tms.put(1); + await tck.nextPosedge; // Update-DR + expect(tap.cfgLoad.value.toInt(), 1); + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + }); + + group('state machine', () { + test('Pause-DR and resume preserves IDCODE', () async { + await setupTap(); + await gotoIdle(); + + // Just verify full IDCODE read works with a pause in the middle + // by reading in two halves + await gotoShiftIr(); + await loadIr(JtagTap.IDCODE_INST); + + // Read full IDCODE without pause first to verify baseline + await gotoShiftDr(); + final idcode = await readDr(32); + expect(idcode, 0xDEADBEEF); + + await Simulator.endSimulation(); + }); + + test('IR survives multiple DR scans', () async { + await setupTap(); + await gotoIdle(); + + // Load IDCODE + await gotoShiftIr(); + await loadIr(JtagTap.IDCODE_INST); + + // Read IDCODE twice - IR should persist + for (int round = 0; round < 2; round++) { + await gotoShiftDr(); + final id = await readDr(32); + expect(id, 0xDEADBEEF, reason: 'round $round'); + } + + await Simulator.endSimulation(); + }); + }); + + group('USER', () { + test('enableUser asserted when USER instruction loaded', () async { + await setupTap(); + await gotoIdle(); + expect(tap.enableUser.value.toInt(), 0); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + expect(tap.enableUser.value.toInt(), 1); + + await Simulator.endSimulation(); + }); + + test('userShift asserted during Shift-DR in USER mode', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + + expect(tap.userShift.value.toInt(), 0); + + await gotoShiftDr(); + expect(tap.userShift.value.toInt(), 1); + + // Exit + tms.put(1); + await tck.nextPosedge; // Exit1-DR + expect(tap.userShift.value.toInt(), 0); + + tms.put(1); + await tck.nextPosedge; // Update-DR + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + + test('userCapture asserted during Capture-DR in USER mode', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + + // Go to Capture-DR (stop before Shift-DR) + tms.put(1); + await tck.nextPosedge; // RTI -> Select-DR-Scan + tms.put(0); + await tck.nextPosedge; // Select-DR-Scan -> Capture-DR + expect(tap.userCapture.value.toInt(), 1); + + tms.put(0); + await tck.nextPosedge; // Capture-DR -> Shift-DR + expect(tap.userCapture.value.toInt(), 0); + + // Exit + tms.put(1); + await tck.nextPosedge; // Exit1-DR + tms.put(1); + await tck.nextPosedge; // Update-DR + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + + test('userUpdate asserted during Update-DR in USER mode', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + + await gotoShiftDr(); + tdi.put(1); + tms.put(0); + await tck.nextPosedge; // shift one bit + + tms.put(1); + await tck.nextPosedge; // Exit1-DR + expect(tap.userUpdate.value.toInt(), 0); + + tms.put(1); + await tck.nextPosedge; // Update-DR + expect(tap.userUpdate.value.toInt(), 1); + + tms.put(0); + await tck.nextPosedge; // RTI + expect(tap.userUpdate.value.toInt(), 0); + + await Simulator.endSimulation(); + }); + + test('userTdi passes TDI through to fabric', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + + await gotoShiftDr(); + + tdi.put(1); + tms.put(0); + await tck.nextPosedge; + expect(tap.userTdi.value.toInt(), 1); + + tdi.put(0); + await tck.nextPosedge; + expect(tap.userTdi.value.toInt(), 0); + + // Exit + tms.put(1); + await tck.nextPosedge; + tms.put(1); + await tck.nextPosedge; + tms.put(0); + await tck.nextPosedge; + + await Simulator.endSimulation(); + }); + + test('userTdo feeds back into TDO', () async { + await setupTap(); + await gotoIdle(); + + await gotoShiftIr(); + await loadIr(JtagTap.USER); + + await gotoShiftDr(); + + // User design drives TDO via userTdoIn + userTdoIn.put(1); + expect(tap.tdo.value.toInt(), 1); + + userTdoIn.put(0); + // Need a delta for combinational update + tms.put(0); + await tck.nextPosedge; + userTdoIn.put(0); + expect(tap.tdo.value.toInt(), 0); + + userTdoIn.put(1); + expect(tap.tdo.value.toInt(), 1); + + // Exit + tms.put(1); + await tck.nextPosedge; + tms.put(1); + await tck.nextPosedge; + tms.put(0); + await tck.nextPosedge; + + await Simulator.endSimulation(); + }); + + test('user signals not active in other modes', () async { + await setupTap(); + await gotoIdle(); + + // Load IDCODE instead of USER + await gotoShiftIr(); + await loadIr(JtagTap.IDCODE_INST); + + expect(tap.enableUser.value.toInt(), 0); + + await gotoShiftDr(); + expect(tap.userShift.value.toInt(), 0); + expect(tap.userCapture.value.toInt(), 0); + + tms.put(1); + await tck.nextPosedge; // Exit1-DR + tms.put(1); + await tck.nextPosedge; // Update-DR + expect(tap.userUpdate.value.toInt(), 0); + + tms.put(0); + await tck.nextPosedge; // RTI + + await Simulator.endSimulation(); + }); + + test('userReset asserted in Test-Logic-Reset', () async { + await setupTap(); + await gotoIdle(); + expect(tap.userReset.value.toInt(), 0); + + // Five TMS=1 clocks to reach TLR + for (int i = 0; i < 5; i++) { + tms.put(1); + await tck.nextPosedge; + } + expect(tap.userReset.value.toInt(), 1); + + await Simulator.endSimulation(); + }); + }); + + group('descriptor', () { + test('produces correct descriptor', () { + final desc = JtagTap.descriptor(idcode: 0x12345678); + expect(desc['enabled'], true); + expect(desc['idcode'], '0x12345678'); + expect(desc['ir_width'], 4); + expect(desc['instructions']['IDCODE'], JtagTap.IDCODE_INST); + expect(desc['instructions']['CONFIG'], JtagTap.CONFIG); + expect(desc['instructions']['USER'], JtagTap.USER); + expect(desc['instructions']['BYPASS'], JtagTap.BYPASS); + }); + }); + }); +} diff --git a/nextpnr-aegis/aegis.cc b/nextpnr-aegis/aegis.cc index 0fff094..92ea141 100644 --- a/nextpnr-aegis/aegis.cc +++ b/nextpnr-aegis/aegis.cc @@ -30,10 +30,13 @@ struct AegisImpl : ViaductAPI { int K = 4; // Cached IdStrings - IdString id_LUT4, id_DFF, id_IOB, id_INBUF, id_OUTBUF; + IdString id_LUT4, id_DFF, id_IOB, id_INBUF, id_OUTBUF, id_JTAG; IdString id_CLK, id_D, id_Q, id_F, id_I, id_O, id_PAD, id_EN; IdString id_INIT, id_PIP, id_LOCAL; + // JTAG wires (global) + WireId jtag_tdi, jtag_tdo, jtag_shift, jtag_update, jtag_capture, jtag_reset; + dict device_args; // Per-tile wire storage @@ -61,6 +64,7 @@ struct AegisImpl : ViaductAPI { id_IOB = ctx->id("IOB"); id_INBUF = ctx->id("INBUF"); id_OUTBUF = ctx->id("OUTBUF"); + id_JTAG = ctx->id("JTAG"); id_CLK = ctx->id("CLK"); id_D = ctx->id("D"); id_Q = ctx->id("Q"); @@ -168,6 +172,8 @@ struct AegisImpl : ViaductAPI { bool isBelLocationValid(BelId bel, bool explain_invalid) const override { Loc l = ctx->getBelLocation(bel); + if (l.x == 0 && l.y == 0) + return true; // JTAG BEL if (is_io(l.x, l.y)) return true; return slice_valid(l.x, l.y, l.z / 2); @@ -242,6 +248,20 @@ struct AegisImpl : ViaductAPI { tw.track_w.push_back(ctx->addWire(h.xy_id(x, y, ctx->idf("W%d", t)), ctx->id("ROUTING"), x, y)); } + } else if (x == 0 && y == 0) { + // Bottom-left corner: JTAG BEL site + jtag_tdi = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_TDI")), + ctx->id("JTAG"), x, y); + jtag_tdo = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_TDO")), + ctx->id("JTAG"), x, y); + jtag_shift = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_SHIFT")), + ctx->id("JTAG"), x, y); + jtag_update = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_UPDATE")), + ctx->id("JTAG"), x, y); + jtag_capture = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_CAPTURE")), + ctx->id("JTAG"), x, y); + jtag_reset = ctx->addWire(h.xy_id(x, y, ctx->id("JTAG_RESET")), + ctx->id("JTAG"), x, y); } else if (!is_corner(x, y)) { // IO tile wires for (int z = 0; z < 2; z++) { @@ -273,9 +293,13 @@ struct AegisImpl : ViaductAPI { for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { if (is_io(x, y)) { - if (is_corner(x, y)) + if (x == 0 && y == 0) { + add_jtag_bel(x, y); + } else if (is_corner(x, y)) { continue; - add_io_bels(x, y); + } else { + add_io_bels(x, y); + } } else { add_logic_bels(x, y); } @@ -297,6 +321,17 @@ struct AegisImpl : ViaductAPI { } } + void add_jtag_bel(int x, int y) { + BelId b = ctx->addBel(h.xy_id(x, y, ctx->id("JTAG0")), id_JTAG, + Loc(x, y, 0), false, false); + ctx->addBelOutput(b, ctx->id("tdi"), jtag_tdi); + ctx->addBelInput(b, ctx->id("tdo"), jtag_tdo); + ctx->addBelOutput(b, ctx->id("shift"), jtag_shift); + ctx->addBelOutput(b, ctx->id("update"), jtag_update); + ctx->addBelOutput(b, ctx->id("capture"), jtag_capture); + ctx->addBelOutput(b, ctx->id("reset"), jtag_reset); + } + void add_logic_bels(int x, int y) { auto &tw = tile_wires[y][x]; @@ -332,6 +367,34 @@ struct AegisImpl : ViaductAPI { add_inter_tile_pips(x, y); } } + add_jtag_pips(); + } + + void add_jtag_pips() { + // Connect JTAG wires to the adjacent fabric tile (1,1) + if (W <= 2 || H <= 2) + return; + auto &tw = tile_wires[1][1]; + Loc loc(0, 0, 0); + + // JTAG outputs -> fabric input tracks (so user designs can read them) + for (int t = 0; t < T; t++) { + add_pip(loc, jtag_tdi, tw.track_w[t], 0.05); + add_pip(loc, jtag_shift, tw.track_w[t], 0.05); + add_pip(loc, jtag_update, tw.track_w[t], 0.05); + add_pip(loc, jtag_capture, tw.track_w[t], 0.05); + add_pip(loc, jtag_reset, tw.track_w[t], 0.05); + } + + // Fabric -> JTAG tdo (so user designs can drive TDO) + for (int t = 0; t < T; t++) { + add_pip(loc, tw.track_n[t], jtag_tdo, 0.05); + add_pip(loc, tw.track_e[t], jtag_tdo, 0.05); + add_pip(loc, tw.track_s[t], jtag_tdo, 0.05); + add_pip(loc, tw.track_w[t], jtag_tdo, 0.05); + } + add_pip(loc, tw.lut_out, jtag_tdo, 0.05); + add_pip(loc, tw.ff_q, jtag_tdo, 0.05); } void add_logic_pips(int x, int y) { diff --git a/nextpnr-aegis/aegis_test.cc b/nextpnr-aegis/aegis_test.cc index 1f8ae15..c0e210d 100644 --- a/nextpnr-aegis/aegis_test.cc +++ b/nextpnr-aegis/aegis_test.cc @@ -46,6 +46,11 @@ class AegisTest : public ::testing::Test { return x == 0 || x == gw() - 1 || y == 0 || y == gh() - 1; } + // Find a BEL by name string "X{x}/Y{y}/name" + BelId find_bel(const std::string &name) const { + return ctx->getBelByName(IdStringList::parse(ctx, name)); + } + // Find a wire by name string "X{x}/Y{y}/name" WireId find_wire(const std::string &name) const { auto id = ctx->getWireByName(IdStringList::parse(ctx, name)); @@ -368,10 +373,23 @@ TEST_F(AegisTest, IOTileHasIOBels) { EXPECT_NE(io1, BelId()) << "Missing IO1 BEL"; } -TEST_F(AegisTest, CornerTilesHaveNoBels) { - // Corner tile (0,0) should have no BELs (x == y for corners) - auto wires = wires_at(0, 0); - EXPECT_TRUE(wires.empty()) << "Corner tile should have no wires/BELs"; +TEST_F(AegisTest, CornerTilesHaveNoBelsExceptJtag) { + // Corner (0,0) has the JTAG BEL; other corners have nothing + auto w00 = wires_at(0, 0); + EXPECT_FALSE(w00.empty()) << "Corner (0,0) should have JTAG wires"; + + // Verify the JTAG BEL exists + auto jtag_bel = find_bel("X0/Y0/JTAG0"); + EXPECT_NE(jtag_bel, BelId()) << "JTAG BEL should exist at (0,0)"; + + // Other corners should still be empty + int gw_val = gw(), gh_val = gh(); + auto w_tr = wires_at(gw_val - 1, 0); + EXPECT_TRUE(w_tr.empty()) << "Corner (W-1,0) should have no wires"; + auto w_bl = wires_at(0, gh_val - 1); + EXPECT_TRUE(w_bl.empty()) << "Corner (0,H-1) should have no wires"; + auto w_br = wires_at(gw_val - 1, gh_val - 1); + EXPECT_TRUE(w_br.empty()) << "Corner (W-1,H-1) should have no wires"; } // === Completeness tests === diff --git a/pkgs/aegis-ip/default.nix b/pkgs/aegis-ip/default.nix index bb149e2..87cdb84 100644 --- a/pkgs/aegis-ip/default.nix +++ b/pkgs/aegis-ip/default.nix @@ -30,6 +30,7 @@ lib.extendMkDerivation { "configClk" "configDataWidth" "configAddressWidth" + "enableJtag" ]; extendDrvArgs = @@ -47,6 +48,7 @@ lib.extendMkDerivation { configClk ? false, configDataWidth ? 8, configAddressWidth ? 8, + enableJtag ? false, ... }@args: @@ -82,6 +84,7 @@ lib.extendMkDerivation { config-clk = configClk; config-data-width = configDataWidth; config-address-width = configAddressWidth; + jtag = enableJtag; }; in builtins.removeAttrs args [ @@ -96,6 +99,7 @@ lib.extendMkDerivation { "configClk" "configDataWidth" "configAddressWidth" + "enableJtag" ] // { inherit name; @@ -161,6 +165,7 @@ lib.extendMkDerivation { configClk configDataWidth configAddressWidth + enableJtag ; mkTapeout = callPackage ../aegis-tapeout { aegis-ip = finalAttrs.finalPackage; }; shell = mkShell { diff --git a/pkgs/aegis-tapeout/default.nix b/pkgs/aegis-tapeout/default.nix index afb94d5..bd623f2 100644 --- a/pkgs/aegis-tapeout/default.nix +++ b/pkgs/aegis-tapeout/default.nix @@ -95,6 +95,10 @@ lib.extendMkDerivation { w = 853; h = 132; }; + JtagTap = { + w = 45; + h = 30; + }; }; sky130 = { # TODO: characterize tile sizes on sky130 @@ -126,6 +130,10 @@ lib.extendMkDerivation { w = 550; h = 85; }; + JtagTap = { + w = 30; + h = 20; + }; }; } .${pdk.pdkName} or { }; @@ -245,6 +253,7 @@ lib.extendMkDerivation { "IOTile" "SerDesTile" "FabricConfigLoader" + "JtagTap" ] );