{"id":3678,"date":"2026-07-15T11:33:44","date_gmt":"2026-07-15T03:33:44","guid":{"rendered":"http:\/\/manufacturing.wiki\/?p=3678"},"modified":"2026-07-15T11:33:44","modified_gmt":"2026-07-15T03:33:44","slug":"on-site-programmable-gate-array-script-automation-development-method","status":"publish","type":"post","link":"http:\/\/manufacturing.wiki\/index.php\/2026\/07\/15\/on-site-programmable-gate-array-script-automation-development-method\/","title":{"rendered":"On-site programmable gate array script automation development method"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">FPGA Script Automation: How to Stop Doing Manual Work and Start Building Flows<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Every FPGA engineer has done it. Open the synthesis tool, click through the GUI, set a few options, wait twenty minutes, then repeat the same steps for the next build. Click. Wait. Click. Wait. Do this fifty times a day and you are not engineering. You are clicking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Scripting changes everything. A good automation flow turns a four-hour build process into a four-minute command. It eliminates human error. It makes your build reproducible. And it frees you to actually think about the design instead of babysitting the tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers the scripting methods that actually work in real FPGA projects. No fluff. No theory. Just patterns you can drop into your workflow today.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Manual FPGA Workflows Are a Trap<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most FPGA teams start with GUI-based tools. That is fine for the first prototype. But once you move past a single module, the GUI becomes a bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every time you change a parameter, you have to re-run synthesis, implementation, bitstream generation, and export. In a GUI, that means opening multiple windows, clicking through tabs, and waiting. In a script, that means one command.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The real cost is not time. It is errors. A missed constraint. A wrong clock assignment. A forgotten IO standard. These things happen when you click through the same steps a hundred times. A script does not forget. A script does not get tired. A script does not accidentally select the wrong device family.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Automation also makes your project portable. When a new engineer joins the team, they run one script and the entire project builds. No &#8220;it works on my machine&#8221; excuses. No &#8220;I forgot to set that option&#8221; surprises.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">TCL Scripting: The Backbone of FPGA Automation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">TCL is the language that most FPGA toolchains speak natively. If you are not scripting in TCL, you are leaving productivity on the table.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Writing a Basic Build Script<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal TCL build script for an FPGA project looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1# Project setup\n2set proj_name \"my_fpga_project\"\n3set part_name \"xc7a35tcsg324-1\"\n4\n5# Create project\n6create_project $proj_name .\/build -part $part_name -force\n7\n8# Add source files\n9add_files -norecurse .\/rtl\/*.v\n10add_files -norecurse .\/constraints\/*.xdc\n11\n12# Set top module\n13set_property top top_module &#91;current_fileset]\n14\n15# Run synthesis\n16launch_runs synth_1 -jobs 4\n17wait_on_run synth_1\n18\n19# Run implementation\n20launch_runs impl_1 -to_step write_bitstream -jobs 4\n21wait_on_run impl_1\n22\n23# Generate bitstream\n24write_bitstream -force .\/build\/${proj_name}.bit\n25\n26puts \"Build complete.\"\n27<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That script does in thirty seconds what takes twenty minutes in the GUI. And it does it the same way every single time. No variation. No mistakes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The&nbsp;<code>wait_on_run<\/code>&nbsp;command is critical. Without it, the script launches the run and exits immediately. The tool is still working in the background while your script thinks it is done. Always wait for the run to finish before moving to the next step.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Automating Multiple Builds With Variables<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Real projects have multiple configurations. Debug build. Release build. Different target devices. Different clock speeds. Instead of maintaining five separate GUI projects, you maintain one script with variables.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1# Configuration\n2set CONFIG \"release\"\n3set CLOCK_FREQ \"100\"\n4\n5# Conditional compilation based on config\n6if {$CONFIG == \"debug\"} {\n7    add_files -norecurse .\/rtl\/debug_module.v\n8    set_property STEPS.SYNTH_DESIGN.ARGS.FLATTEN_HIERARCHY none &#91;get_runs synth_1]\n9} else {\n10    add_files -norecurse .\/rtl\/optimized_module.v\n11}\n12\n13# Override clock constraint based on frequency\n14set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} &#91;get_ports clk]\n15create_clock -period &#91;expr {1000.0 \/ $CLOCK_FREQ}] -name sys_clk &#91;get_ports clk]\n16<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Change the&nbsp;<code>CONFIG<\/code>&nbsp;variable at the top of the script and the entire build adapts. No opening the GUI. No editing constraints manually. One variable controls everything.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error Handling in TCL Scripts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Scripts fail. Tools crash. Files go missing. If your script has no error handling, it will silently produce a broken bitstream and you will not know until you program the board.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wrap every critical step in a catch block:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1if {&#91;catch {launch_runs impl_1 -jobs 4} err]} {\n2    puts \"ERROR: Implementation failed.\"\n3    puts $err\n4    exit 1\n5}\n6wait_on_run impl_1\n7\n8if {&#91;catch {write_bitstream -force .\/build\/${proj_name}.bit} err]} {\n9    puts \"ERROR: Bitstream generation failed.\"\n10    puts $err\n11    exit 1\n12}\n13<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The&nbsp;<code>catch<\/code>&nbsp;command traps any error, prints it, and exits with a non-zero status. This is how you know something went wrong. Without it, the script prints &#8220;Build complete&#8221; even when the bitstream is empty.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Python Automation: When TCL Is Not Enough<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">TCL is great for driving the toolchain. But it is terrible at everything else. File manipulation. Report parsing. Regression testing. Email notifications. For those tasks, you need Python.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Driving TCL From Python<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most common pattern is a Python wrapper that calls TCL scripts. Python handles the orchestration. TCL handles the toolchain.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python1import subprocess\n2import sys\n3import os\n4\n5def run_build(config=\"release\", clock_mhz=100):\n6    tcl_script = f\"\"\"\n7    set CONFIG {config}\n8    set CLOCK_FREQ {clock_mhz}\n9    source .\/scripts\/build.tcl\n10    \"\"\"\n11    \n12    with open(\".\/temp_config.tcl\", \"w\") as f:\n13        f.write(tcl_script)\n14    \n15    result = subprocess.run(\n16        &#91;\"vivado\", \"-mode\", \"batch\", \"-source\", \".\/temp_config.tcl\"],\n17        capture_output=True,\n18        text=True\n19    )\n20    \n21    if result.returncode != 0:\n22        print(\"BUILD FAILED\")\n23        print(result.stderr)\n24        sys.exit(1)\n25    \n26    print(\"BUILD SUCCEEDED\")\n27    os.remove(\".\/temp_config.tcl\")\n28\n29if __name__ == \"__main__\":\n30    run_build(config=sys.argv&#91;1], clock_mhz=int(sys.argv&#91;2]))\n31<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now you can call&nbsp;<code>python build.py release 100<\/code>&nbsp;from the command line or from a CI pipeline. The Python script sets up the TCL configuration, runs the tool, checks the result, and cleans up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Parsing Synthesis Reports With Python<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After every build, you need to know if the design met timing. The synthesis tool generates a report file. Reading that report manually is painful. Parsing it with Python takes five minutes and saves you hours.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python1import re\n2\n3def check_timing(report_path):\n4    with open(report_path, \"r\") as f:\n5        content = f.read()\n6    \n7    # Look for worst negative slack\n8    match = re.search(r\"Worst Negative Slack\\s+(&#91;-\\d.]+)\", content)\n9    if match:\n10        wns = float(match.group(1))\n11        if wns &lt; 0:\n12            print(f\"TIMING FAIL: WNS = {wns} ns\")\n13            return False\n14    \n15    # Look for total logic utilization\n16    match = re.search(r\"Slice LUTs\\s+(\\d+)\\s+\\\/\\s+(\\d+)\", content)\n17    if match:\n18        used = int(match.group(1))\n19        total = int(match.group(2))\n20        print(f\"LUT Utilization: {used}\/{total} ({100*used\/total:.1f}%)\")\n21    \n22    return True\n23<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run this after every build. If timing fails, the script exits with an error. If utilization is too high, it warns you. You never have to open the report file yourself.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">CI\/CD Integration for FPGA Projects<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Continuous integration is not just for software. FPGA projects benefit enormously from automated builds that run on every commit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set up a simple pipeline:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Commit pushed. Trigger build script. Run synthesis. Check timing. Generate bitstream. Archive artifacts. If any step fails, send a notification.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python1# CI pipeline pseudo-code\n2def ci_pipeline():\n3    if not run_synthesis():\n4        notify_team(\"Synthesis failed\")\n5        return False\n6    \n7    if not check_timing(\".\/reports\/timing_summary.rpt\"):\n8        notify_team(\"Timing failed\")\n9        return False\n10    \n11    if not generate_bitstream():\n12        notify_team(\"Bitstream generation failed\")\n13        return False\n14    \n15    archive_artifacts(\".\/build\/\")\n16    notify_team(\"Build passed\")\n17    return True\n18<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This catches broken builds before they reach hardware. No more &#8220;it worked yesterday&#8221; surprises. No more debugging a board that was programmed with a bad bitstream.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Makefile Patterns for FPGA Projects<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Not everyone wants Python. Some teams prefer Makefiles. They work just as well and require zero dependencies beyond what is already on the system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal Makefile for an FPGA build:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>makefile1PROJECT = my_fpga_project\n2PART = xc7a35tcsg324-1\n3TCL_SCRIPT = scripts\/build.tcl\n4\n5all: build\n6\n7build:\n8\tvivado -mode batch -source $(TCL_SCRIPT) -tclargs $(PART)\n9\n10clean:\n11\trm -rf build\/\n12\trm -rf *.jou *.log *.str\n13\n14regen: clean build\n15\n16.PHONY: all build clean regen\n17<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run&nbsp;<code>make<\/code>&nbsp;to build. Run&nbsp;<code>make clean<\/code>&nbsp;to wipe everything. Run&nbsp;<code>make regen<\/code>&nbsp;to start fresh. Simple. No Python. No TCL wrapper. Just one command that does everything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Add variables for different configurations:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>makefile1ifeq ($(CONFIG),debug)\n2    TCL_ARGS = -tclargs $(PART) debug\n3else\n4    TCL_ARGS = -tclargs $(PART) release\n5endif\n6\n7build:\n8\tvivado -mode batch -source $(TCL_SCRIPT) $(TCL_ARGS)\n9<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now&nbsp;<code>make CONFIG=debug<\/code>&nbsp;builds the debug version.&nbsp;<code>make CONFIG=release<\/code>&nbsp;builds the release version. Same Makefile. Two configurations. Zero duplication.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common Scripting Mistakes That Bite You Later<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardcoding file paths everywhere. When you move the project to a different directory, every script breaks. Use relative paths from the project root. Always.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Not versioning your scripts. The TCL script that builds your project is part of the project. It belongs in version control. If you lose it, you lose your entire build flow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Skipping the wait command. This is the most common mistake. The script launches synthesis and immediately tries to read the output file. The tool has not finished yet. The file does not exist. The script crashes. Always use&nbsp;<code>wait_on_run<\/code>&nbsp;or equivalent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Building on the wrong machine. Your script assumes a specific tool version. When you run it on a different machine with a different version, things break. Pin your tool version in the script and check it at the start.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1if {&#91;version -short] ne \"2024.1\"} {\n2    puts \"ERROR: Wrong tool version. Expected 2024.1.\"\n3    exit 1\n4}\n5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This saves you from the &#8220;it works on my machine&#8221; problem that plagues every FPGA team at some point.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Building a Script Library You Actually Reuse<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Do not write the same script twice. Every time you copy a TCL file and tweak it slightly, you create a maintenance burden. Instead, build a library of reusable functions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a file called&nbsp;<code>utils.tcl<\/code>&nbsp;with common operations:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1proc run_synth {proj_name} {\n2    launch_runs synth_1 -jobs 4\n3    wait_on_run synth_1\n4    return &#91;get_property STATUS &#91;get_runs synth_1]]\n5}\n6\n7proc check_timing {rpt_file} {\n8    set content &#91;read_file $rpt_file]\n9    regexp {Worst Negative Slack\\s+(&#91;-\\d.]+)} $content -&gt; wns\n10    return &#91;expr {$wns &lt; 0}]\n11}\n12\n13proc send_notification {msg} {\n14    puts \"NOTIFICATION: $msg\"\n15    # Add email or Slack hook here\n16}\n17<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then your build script becomes a sequence of function calls:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcl1source .\/scripts\/utils.tcl\n2\n3if {&#91;run_synth $proj_name] ne \"synth_design\"} {\n4    send_notification \"Synthesis failed\"\n5    exit 1\n6}\n7\n8if {&#91;check_timing \".\/reports\/timing.rpt\"]} {\n9    send_notification \"Timing failed\"\n10    exit 1\n11}\n12<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Clean. Readable. Reusable. When you need to change how notifications work, you edit one file. When you need to change how synthesis runs, you edit one function. Not fifty scattered scripts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your future self will thank you for this. The time you spend building a script library pays for itself ten times over in the first month.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ChipApex is a global distributor of electronic components: ICs, semiconductors, passives &amp; interconnects. Source active &amp; obsolete parts with wholesale pricing, fast RFQ response, and worldwide delivery.Official website address:chipapex.com<\/p>\n","protected":false},"excerpt":{"rendered":"<p>FPGA Script Automation: How to Stop Doing Manual Work a &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-3678","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/posts\/3678","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/comments?post=3678"}],"version-history":[{"count":1,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/posts\/3678\/revisions"}],"predecessor-version":[{"id":3679,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/posts\/3678\/revisions\/3679"}],"wp:attachment":[{"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/media?parent=3678"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/categories?post=3678"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/manufacturing.wiki\/index.php\/wp-json\/wp\/v2\/tags?post=3678"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}