This content is translated with AI. Please refer to the original Traditional Chinese version (zh-TW) for accuracy.
First of all, I don't know.
This is inherently a difficult topic. Vivado was not originally designed for automation (at least it doesn't seem like it). Its GUI and integrated Vivado project storage environment certainly provide a lot of functionality. However, in practice, it is strongly discouraged to include the entire Vivado generated project files into version control, as even slight changes can disrupt the version control system, making Vivado projects a pain point in automation and version management.
This issue troubles many people, and a quick search will yield a lot of discussions, such as How do you manage your Vivado projects in git? . Some have proposed solutions, like the project vivado-git , which I haven't tried yet so I'm not sure of its effectiveness.
After some attempts, I am sharing how I currently manage projects at my company, dividing the article into two main parts: First: Using AXI Lite and the Adder that appears as an example, we aim to package the IP with almost one button and output it to a specified location. Second: Use the IP produced in the previous step, integrate it into the block diagram, and generate a project ready for bitstream generation.
The following vivado script is somewhat related to paths, so first, let's explain the working paths:
origin_dir is the main directory of the entire project, named because Vivado's script calls it that way.
src: Folder for hdl codescript: Script files and resources for vivado generationip_repo: The working directory for IPs not yet scriptedproj: The vivado project where the entire IP is packaged onto the FPGAtmp_pkg: Temporary project for vivado
ip_repo, proj, tmp_pkg are the locations for vivado projects, and these folders can be added to .gitignore.
Packaging IP
Generate AXI Files
Initially, you only have files to be packaged, and you still need files generated by vivado to handle the AXI interface. Firstly, open vivado to create a new project, which will be deleted later, so call it tmp_pkg. No source code is needed.

Click Tool -> Create and Package New IP -> Create a new AXI4 Peripheral. The module information mainly requires setting a name, which will correspond to the file name and the desired AXI Lite such as the number of registers, etc.

Using Adder as an example for the IP name, set the location within origin_dir. The last step is not to select Edit IP but to select Add IP to the repository. After pressing Finish, close this project.
Go to the origin_dir/ip_repo to find two files generated by vivado:
- Adder_v1_0.v: Top-level of the IP
- Adder_v1_0_S00_AXI.v: AXI Lite slave handler
Copy these two files together into the src folder, then you can delete both the tmp_pkg and ip_repo folders.
Package IP
Having modified the AXI files in advance, start from scratch, open Vivado and create a tmp_pkg project. Add Source to add your project's source code along with the two newly added AXI related files into this project.

Click Tool -> Create and Package New IP -> Package your current project
Choose IP location in origin_dir/ip_repo. Since many extra files will be generated during editing, temporarily store them here. Set the desired module information, and complete the packaging by clicking package IP.

Once completed, we need to automate the above process. Choose File -> Project -> Write TCL to let vivado write what was just done into package_ip.tcl.
When writing package_ip.tcl, write it to origin_dir first, then move it to the script folder. If stored directly into the script folder, many ../ will appear that need handling

Modify package_ip.tcl
The tcl script written by vivado is usable but contains too many unrelated things. My three-file IP had a total of 550 lines, requiring cleanup to remove unnecessary parts. Nowadays, a lot of cleaning can be done with AI assistance.
- In ip_repo, find component.xml and move it to the script folder; then modify package_ip.tcl to change the path of component.xml to script.
- Find the line creating_project and add
-force, so even if tmp_pkg already exists, it can be overridden the second time. - Find the definition of
proc checkRequiredFilesand its call and remove the entire section. Keeping it is acceptable, but I find deleting it refreshing. If the source code changes, only one place needs modification. (But if your source code changes, there's a high chance you'll need to repackage.) - Find
proc print_helpand delete the section from its definition toif { $::argc > 0 }underneath it.
There are also some snippets like:
if { [info exists ::origin_dir_loc] } {
set origin_dir $::origin_dir_loc
}
Allowing you to change origin_dir using environment variables. Due to its brevity, it can be deleted or not.
From the second half of the tcl, it will sequentially:
- Create sources_1 and add the source code (most content belongs here).
- Create constrs_1 and add constraint files.
- Create sim_1 to save simulation files.
- Create synth_1 to save synthesis results.
- Create impl_1 to save implementation (P&R) results.
- You may even see impl_2 in your script.
Since we're packaging IP, everything after sim_1 isn't needed and can be removed; constrs_1 can also be considered deleted if empty.
Note that when Vivado performs package operations, it automatically determines what interface you're using. For instance, if everything starts with s00_axi_ and has awready, wready, bready etc., Vivado will autonomously infer it as AXI Lite and map ports to corresponding interfaces.
Since we're using AXI Lite .v files generated by Vivado, this process is likely error-free.
If you wrote it yourself, then during the packaging, you must complete the relevant settings at the Port and Interface step so that Vivado captures this mapping action.
Some info might be stored in component.xml, but I'm not sure.
Modify ip_repo Location
During this study, I found it best to have a common ip_repo folder on the computer, storing all hardware IP repositories generated in that folder.
Here I choose ${HOME}/ip_repo, and add the following in package_ip.tcl:
set home_dir $::env(HOME)
set ip_repo_dir [file normalize [file join $home_dir "ip_repo"]]
Vivado allows multiple repository settings, but maintaining a long list becomes hard to manage, and centralized storage is more efficient.
As for whether this ip_repo folder can be connected to the network or whether various hardware IPs can be automatically compiled, CI/CD uploads during updates, or whether version control is necessary, this goes beyond the content of this article and should be modified according to the company/organization's plans.
package script
Finally, if Vivado hasn't written the packaging part, add the following:
set core_name "adder"
set core_version "1.0"
set core_vendor "user.org"
set core_library "User"
set display_name "adder_v1_0"
set description "Wrap Adder into AXI Lite"
set ip_root_dir [file normalize \
[file join $ip_repo_dir "${core_name}_${core_version}"]]
set proj_dir [file normalize "./${_xil_proj_name_}"]
file mkdir $ip_repo_dir
if { [file exists $ip_root_dir] } {
file delete -force $ip_root_dir
}
file mkdir $ip_root_dir
update_compile_order -fileset sources_1
ipx::package_project -root_dir $ip_root_dir -vendor $core_vendor \
-library $core_library -taxonomy {/UserIP} -import_files
set core [ipx::current_core]
set_property name $core_name $core
set_property version $core_version $core
set_property display_name $display_name $core
set_property description {$description} $core
ipx::merge_project_changes files $core
ipx::save_core $core
ipx::unload_core $core
set fileset_obj [get_filesets sources_1]
if { $fileset_obj != {} } {
set_property "ip_repo_paths" $ip_repo_dir $fileset_obj
update_ip_catalog -rebuild
}
close_project
file delete -force $proj_dir
puts "INFO: Packaged IP into $ip_root_dir"
puts "INFO: Removed temporary project directory $proj_dir"
The display name and description can be modified to match the IP being packaged.
Note this script is destructive as it deletes and remakes the package folder under ${HOME}/ip_repo; it also closes and deletes tmp_pkg upon completion.
Packaging Block Diagram
The second script, generally called script/xxx_proj.tcl, is responsible for connecting the block diagram, hereafter referred to as proj.tcl.
Creating this is simpler:
- Set the vivado IP search paths, pointing to the folder above, select
Tools->Settings, configure under IP/Repository as shown below:

- Create a new block diagram, connect components like processor, IP, AXI Interconnect, etc.
- Create HDL Wrapper
- Write TCL to generate proj.tcl.
When writing TCL, with the option Recreate Block Designs using Tcl, you can:
- Check Recreate Block Designs using Tcl
- Or don't select it and add the .bd file from xxx_proj into your src, modifying the tcl pointing to the .bd file path.
The first choice results in a lengthy .tcl file containing instructions to construct the entire block diagram from scratch, whereas the second choice with .bd file won’t make the .tcl any less lengthy, and is harder to read and manage with version control. I always opt for the first to let the .tcl redraw the block diagram.
Modify proj.tcl
After writing proj.tcl, modifications are similar to those in package_ip.tcl (since both are written by Vivado)
- Find the create_project line and similarly add
-force. - Find the definition and call of
proc checkRequiredFilesand delete the entire section. - Find
proc print_helpstarting from its definition up toif { $::argc > 0 }below and remove the section. - In the second script, synth_1 may not necessarily be removed as it's after all needed to run implementation for projects containing block diagrams. Keeping it is harmless, but if the file seems too long, removing is fine.
Add setting of the ip_repo location just like in package_ip.tcl:
set home_dir $::env(HOME)
set ip_repo_dir [file normalize [file join $home_dir "ip_repo"]]
Then search for the keyword ip_repo_paths and modify the original relative path to ip_repo_dir so vivado can find the packaged IP.
if { $obj != {} } {
set_property "ip_repo_paths" "[file normalize "$ip_repo_dir"]" $obj
}
After preparing both files, I use them as follows:
vivado -mode batch -source script/package_ip.tcl
vivado -source script/proj.tcl
The first line completes IP packaging, and the second line opens Vivado, prepares the block diagram, and waits for you to press synthesis, implementation, generate bitstream.
If you are confident in your design, during the proj.tcl creation, first press synthesis, implementation, generate bitstream. Then write the .tcl file, so upon execution, it will run up to bitstream creation.
What's Next?
I’m currently content with these two scripts, although I noticed a few areas for improvement:
Firstly, clean the tcl further for better clarity, separate the src section into a file, and let tcl read the file. Doing so allows a single tcl script to be copied directly for other projects without repeating all the steps for each new project.
Secondly, the script for IP packaging should obviously do more (yet not too much), at least verify there's no major issue with the IP.
Vivado has two actions I'm aware of that suit it, while the rest are excessive:
- Run Linter to check for errors
- Run Synthesis to check for errors
Regarding Vivado version issues, don't even think about it; this side of Vivado can be described as tragic
Vivado on the other hand, not so much. You want to pick ONE version and use it for your project. And everyone working on it will need to use the same version.
The translation here is that you're forced to stick with one version.
We once had a partner using 2022.2, and we internally accidentally upgraded to 2023.2, which caused issues since our scripts wouldn't work for them, and the solution was for us to reinstall 2022.2 specifically for generating scripts for them.
We've also encountered projects for review with scripts written in version 2025.1, which naturally we couldn't run.
With this script tactic, any Vivado updates mean it's not salvageable; be prepared to redo it.
Another big issue is that tcl scripts tend to be highly binding with specific FPGAs, boards, SoCs, etc., making it difficult for one script to read different configurations to generate bitstream files for different FPGAs. Currently, there's no good solution for this either.
Conclusion
Above is an implementation of a Vivado automation scheme, far from perfect but somewhat usable.
I hope it helps everyone and also serves as a starting point for discussions on improvement. If anyone has secret tips on using Vivado, feel free to share them below, helping us find the best practices for Vivado automation.