diff --git a/README.md b/README.md index 5bce461031f5bdb24003009de6093da6fb889d84..4d2a6802ad92db30511892a0aa2f6c8585a0834e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ cores are (re)created using Vivado Tcl scripts. After cloning this repo, please run the following to generate all IP cores on Linux, or use the corresponding .bat file on Windows. +### On Linux The simplest way is to simply call the IP core creation script from inside the cloned repository: @@ -18,13 +19,13 @@ pushd tclink popd ``` -For the impatient, the above can be sped up (albeit at the cost of -some loss of efficiency) when GNU parallel is available: +### On Windows + +Open a ```cmd``` window in the ```tclink``` repository +directory, and then enter the following command. ``` -pushd tclink -parallel ./scripts/vivado_create_ips.sh ::: $(find . -iname '*vivado_create_ip_*.tcl' | sed -s 's/.*vivado_create_ip_\(.*\)\.tcl/\1/g') -popd +call <path-to-vivado-installation>\bin\vivado.bat -notrace -mode batch -source .\scripts\vivado_create_ips.tcl ``` ## TClink concept diff --git a/scripts/pkgIndex.tcl b/scripts/pkgIndex.tcl index 0848997aa2c75a1c4dea0e868a4334cc45046115..2874e76530675b2a46a9699fe3b62c70cc0cfb29 100644 --- a/scripts/pkgIndex.tcl +++ b/scripts/pkgIndex.tcl @@ -8,4 +8,6 @@ # script is sourced, the variable $dir must contain the # full path name of this file's directory. +package ifneeded repo_utils 0.1 [list source [file join $dir repo_utils.tcl]] +package ifneeded tcl_utils 0.1 [list source [file join $dir tcl_utils.tcl]] package ifneeded vivado_utils 0.1 [list source [file join $dir vivado_utils.tcl]] diff --git a/scripts/tcl_utils.tcl b/scripts/tcl_utils.tcl new file mode 100644 index 0000000000000000000000000000000000000000..abe87438e06e624debdb64580e09dfd026474af6 --- /dev/null +++ b/scripts/tcl_utils.tcl @@ -0,0 +1,49 @@ +######################################################################## + +package require Tcl 8.5 + +namespace eval tcl_utils { + set version 0.1 +} + +package provide tcl_utils $tcl_utils::version + +######################################################################## + +proc tcl_utils::map {lambda list} { + variable result {} + foreach item $list { + lappend result [apply $lambda $item] + } + return $result +} + +######################################################################## + +# Crude, but seems to be sufficient. +proc tcl_utils::get_os {} { + variable os [lindex $::tcl_platform(platform) 0] + return $os +} + +######################################################################## + +proc tcl_utils::os_is_windows {} { + set is_win [expr {[get_os] == "windows"}] + return $is_win +} + +######################################################################## + +proc tcl_utils::source_with_args {file_name {source_args {}} {cmd_line_args {}}} { + variable argv $::argv + variable argc $::argc + set ::argv $cmd_line_args + set ::argc [llength $cmd_line_args] + variable code [catch {uplevel [list source $source_args $file_name]} return] + set ::argv $argv + set ::argc $argc + return -code $code $return +} + +######################################################################## diff --git a/scripts/vivado_create_ips.sh b/scripts/vivado_create_ips.sh index c55046d98ede799088b903033cc0966f8c1f244a..dad61c214804a8a1019d3d84154f3461be129496 100755 --- a/scripts/vivado_create_ips.sh +++ b/scripts/vivado_create_ips.sh @@ -1,7 +1,7 @@ #!/bin/bash # Disable wildcard expansion because of the possibility of a '.*' as -# ip-name-pattern. +# IP include/exclude patterns. set -o noglob args=() @@ -24,12 +24,25 @@ done TCL_ARGS="" +if [ ! -z "${args[0]}" ] && [ ! -z "${ip_include_pattern}" ]; then + echo "Cannot specify both -include-ips and a free IP selection argument" + exit 1 +fi + if [ ! -z "${args[0]}" ]; then - TCL_ARGS="-ip-name-pattern ${args[0]}" + TCL_ARGS="-include-ips ${args[0]}" +fi + +if [ ! -z "${include_ips}" ]; then + TCL_ARGS="$TCL_ARGS -include-ips ${include_ips}" +fi + +if [ ! -z "${exclude_ips}" ]; then + TCL_ARGS="$TCL_ARGS -exclude-ips ${exclude_ips}" fi -if [ ! -z "${args[1]}" ]; then - TCL_ARGS="$TCL_ARGS -target-part ${args[1]}" +if [ ! -z "${target_part}" ]; then + TCL_ARGS="$TCL_ARGS -target-part ${target_part}" fi if [ ! -z "${user_ip_repo}" ]; then diff --git a/scripts/vivado_create_ips.tcl b/scripts/vivado_create_ips.tcl index dfdcc7c873f70a0df389901777dc66778c72e5bc..190d4da0322f335b1bdc598ddf659747d6a391b3 100644 --- a/scripts/vivado_create_ips.tcl +++ b/scripts/vivado_create_ips.tcl @@ -16,21 +16,21 @@ # vivado -mode batch -notrace -nolog -nojou -quiet -source scripts/vivado_create_ips.tcl -tclargs -target-part xcku15p-ffva1760-2-e ######################################################################## -package require cmdline +package require Tcl 8.5 -set script_dir [file dirname [file normalize [info script]]] +variable script_dir [file dirname [file normalize [info script]]] set env(TCLLIBPATH) [list $script_dir] lappend ::auto_path $script_dir +package require cmdline +package require tcl_utils + ######################################################################## # The IP core generation scripts are are found by name using the # following pattern. set script_name_pattern_base "vivado_create_ip_" -# This is how we want to call Vivado. -set vivado_cmd {vivado -mode batch -notrace -nolog -nojou -quiet -source} - set sep_line [string repeat "-" 60] ######################################################################## @@ -38,8 +38,11 @@ set sep_line [string repeat "-" 60] proc glob_recursive {{dir .} {filespec *} {types {b c f l p s}}} { set files [glob -nocomplain -types $types -dir $dir -- $filespec] foreach x [glob -nocomplain -types {d} -dir $dir -- *] { - set files [concat $files \ - [glob_recursive [file join [pwd] $x] $filespec $types]] + # We don't follow links for directories. + if {[file type [file normalize $x]] != "link"} { + set files [concat $files \ + [glob_recursive [file join [pwd] $x] $filespec $types]] + } } set filelist {} foreach x $files { @@ -50,154 +53,245 @@ proc glob_recursive {{dir .} {filespec *} {types {b c f l p s}}} { ######################################################################## -set parameters { - {target-part.arg "" "The FPGA to target"} - {target-board.arg "" "The evaluation board to target"} - {ip-name-pattern.arg ".*" "Regular expression describing which IP core(s) to generate"} - {user-ip-repo.arg "" "Path to an optional user-IP repository to include"} +# This would work in TCL 8.6. Vivado 2022.2 seems to provide 8.5. +# proc tempdir {template} { +# close [file tempfile path $template] +# file delete $path +# file mkdir $path +# return $path +# } + +proc tempdir {{template ""}} { + set tmp [pwd] + if {[file exists /tmp]} { + set tmp /tmp + } + catch {set tmp $::env(TRASH_FOLDER)} + catch {set tmp $::env(TMP)} + catch {set tmp $::env(TEMP)} + set suffix [pid] + if [string length $template] { + set suffix ${template}_[pid] + } + set dir_name [file join $tmp $suffix] } -set usage "- A script to (re)generate Xilinx IP cores from stored parameters" +######################################################################## + +namespace eval ns { + set parameters { + {target-part.arg "" "The FPGA to target"} + {target-board.arg "" "The evaluation board to target"} + {include-ips.arg ".*" "Regular expression describing which IP core(s) to generate"} + {exclude-ips.arg "" "Regular expression describing which IP core(s) not to generate"} + {user-ip-repo.arg "" "Path to an optional user-IP repository to include"} + } -if { [catch {array set options [cmdline::getoptions ::argv $parameters $usage]}] } { - puts [cmdline::usage $parameters $usage] - exit 1 -} + set usage "- A script to (re)generate Xilinx IP cores from stored parameters" -# Find all the Vivado IP core creation scripts. -set ip_name_pattern $options(ip-name-pattern) -set script_name_pattern "${script_name_pattern_base}*\.tcl" -set ip_scripts_tmp [glob_recursive . $script_name_pattern] -# NOTE: There is a bit of fiddling here with the IP name pattern. We -# want to make sure to apply the filtering only to the IP name part of -# the file names (while still filtering a list of file names). This is -# not super efficient. -set ip_scripts {} -foreach file_name $ip_scripts_tmp { - regexp "${script_name_pattern_base}(.*)\.tcl$" $file_name dummy ip_name - if {[regexp $ip_name_pattern $ip_name]} { - lappend ip_scripts $file_name + if {[catch {array set options [cmdline::getoptions ::argv $parameters $usage]}]} { + puts [cmdline::usage $parameters $usage] + exit 1 } -} -# Sort and remove duplicates (which may arise from symlinks). -set ip_scripts [lsort -unique $ip_scripts] + # Find all the Vivado IP core creation scripts. + set ip_include_pattern $options(include-ips) + set ip_exclude_pattern $options(exclude-ips) + + set script_name_pattern "${script_name_pattern_base}*\.tcl" + set ip_scripts_tmp [glob_recursive . $script_name_pattern] + # NOTE: There is a bit of fiddling here with the IP name + # patterns. We want to make sure to apply the filtering only to + # the IP name part of the file names (while still filtering a list + # of file names). This is not super efficient. + set ip_scripts {} + set need_exclude_filter [expr {$ip_exclude_pattern eq ""}] + foreach file_name $ip_scripts_tmp { + regexp "${script_name_pattern_base}(.*)\.tcl$" $file_name dummy ip_name + if {[regexp $ip_include_pattern $ip_name]} { + if {$need_exclude_filter || ![regexp $ip_exclude_pattern $ip_name]} { + lappend ip_scripts $file_name + } + } + } -# Get the values of all options. -set target_part $options(target-part) -set target_board $options(target-board) -set user_ip_repo [file normalize $options(user-ip-repo)] + # Sort and remove duplicates (which may arise from symlinks). + set ip_scripts [lsort -unique $ip_scripts] -# Perform some basic checks on the user-IP repo path. -if { $user_ip_repo ne "" } { - if { ! [file exists $user_ip_repo] } { - error "Path '$user_ip_repo' does not exist" + # Get the values of all options. + set target_part $options(target-part) + set target_board $options(target-board) + set user_ip_repo [file normalize $options(user-ip-repo)] + + # Perform some basic checks on the user-IP repo path. + if {$user_ip_repo ne ""} { + if {! [file exists $user_ip_repo]} { + error "Path '$user_ip_repo' does not exist" + } + if {! [file isdirectory $user_ip_repo]} { + error "Path '$user_ip_repo' is not a directory" + } } - if { ! [file isdirectory $user_ip_repo] } { - error "Path '$user_ip_repo' is not a directory" + + puts "$sep_line" + if {$user_ip_repo ne ""} { + puts "Including user-IP repository '$user_ip_repo'" } -} + set s_or_not "" + set tmp [llength $ip_scripts] + if {$tmp > 1 || $tmp == 0} { + set s_or_not "s" + } + puts "Found [llength $ip_scripts] matching IP core creation script$s_or_not" + # foreach item $ip_scripts { + # puts $item + # } + # puts "$sep_line" -puts "$sep_line" -if { $user_ip_repo ne "" } { - puts "Including user-IP repository '$user_ip_repo'" -} -set s_or_not "" -set tmp [llength $ip_scripts] -if { $tmp > 1 || $tmp == 0 } { - set s_or_not "s" -} -puts "Found [llength $ip_scripts] matching IP core creation script$s_or_not" -# foreach item $ip_scripts { -# puts $item -# } -puts "$sep_line" + # Suppress some of the cluttering Vivado output. + set_msg_config -string "Refreshing IP repositories" -suppress + set_msg_config -string "Loaded user IP repository" -suppress + set_msg_config -string "Loaded Vivado IP repository" -suppress + set_msg_config -string "Using compiled simulation libraries for IPs" -suppress + set_msg_config -string "Exporting simulation files" -suppress + set_msg_config -string "Script generated" -suppress + set_msg_config -string "Pre-compiled simulation library path" -suppress + set_msg_config -string "Using boost library" -suppress + set_msg_config -regexp -string {.*Generating.*target.*} -suppress + if {![tcl_utils::os_is_windows]} { + set_msg_config -string "The Windows operating system has path length limitations" -suppress + } -# Now process all found scripts. -set status 0 -foreach {script_name} $ip_scripts { + # Now process all found scripts. + set status 0 + foreach {script_name} $ip_scripts { - # Derive the Vivado project name from the script name. (NOTE: This - # relies on a naming convention.) - set vivado_project_name [file rootname [file tail $script_name]] + # Derive the Vivado project name from the script name. (NOTE: This + # relies on a naming convention.) + set vivado_project_name [file rootname [file tail $script_name]] - # Derive the IP core name from the vivado project name. - set ip_name [regsub ***=$script_name_pattern_base $vivado_project_name ""] + # Derive the IP core name from the vivado project name. + set ip_name [regsub ***=$script_name_pattern_base $vivado_project_name ""] - # Derive a descriptive 'parent' name. (Useful in case there are - # multiple IP creation scripts with the same (IP) name. - set grandparent_name [file tail [file dirname [file dirname [file dirname [file dirname $script_name]]]]] - set parent_name [file tail [file dirname [file dirname [file dirname $script_name]]]] - puts "Processing \"$grandparent_name:$parent_name:$ip_name\"" - puts "$sep_line" + # Derive a more-or-less descriptive name for progress + # reporting. (Useful in case there are multiple IP creation + # scripts with the same (IP) name.) + regsub ***=[pwd] $script_name "" tmp + set chunks [file split [file dirname $tmp]] + # NOTE: The following is a bit ad hoc. It removes path + # separators. + set unwanteds [list \\ /] + set chunks_clean $chunks + foreach unwanted $unwanteds { + set chunks_clean [lsearch -all -inline -not -exact $chunks_clean $unwanted] + } + puts "$sep_line" + puts "Processing \"[join $chunks_clean :]:$ip_name\"" - # Derive the expected Vivado project directory name from the - # project name. - set dir_name $vivado_project_name + # Derive the expected Vivado project (temporary) directory name + # from the project name. + set tmp_dir_name [tempdir] + set dir_name [file join $tmp_dir_name $vivado_project_name] - # Remove any possible left-over Vivado project directory. - file delete -force -- $dir_name + # Remove any possible left-over Vivado project directory. + file delete -force -- $dir_name + # And create a clean directory. + file mkdir $dir_name - # Have Vivado run the script and generate the IP core. - set args "" - if { [string length $target_part] != 0 } { - set args [concat $args "-target-part \"$target_part\""] - } - if { [string length $target_board] != 0 } { - set args [concat $args "-target-board \"$target_board\""] - } - if { [string length $user_ip_repo] != 0 } { - set args [concat $args "-user-ip-repo \"$user_ip_repo\""] - } - set full_args "" - if { [string length $args] != 0 } { - set full_args "-tclargs $args" - } - set full_cmd [concat $vivado_cmd $script_name $full_args] - set status [catch {exec {*}$full_cmd} err] - if { $status != 0 } { - puts "A problem occurred:" - set tmp {} - foreach i [split $err "\n"] { - append tmp " !!! $i\n" + # This default case will work on Linux. + set dir_name_short $dir_name + # For OSs that suffer from path length limitations, however, we + # need a trick. + if {[tcl_utils::os_is_windows]} { + set win_virtual_drive Z: + set dir_name_short ${win_virtual_drive}/ + if {[catch {exec subst $win_virtual_drive $dir_name}]} { + puts "Virtual drive $win_virtual_drive is not available.\ + Please unmap that drive\ + ('subst $win_virtual_drive /d')\ + for this script to work." + break + } } - puts -nonewline $tmp - break - } else { - set target_base_name [file dirname $script_name] - set target_sub_name $ip_name - set target_dir_name [file join $target_base_name $target_sub_name] - file delete -force $target_dir_name - file mkdir $target_dir_name - - # Find the produced IP core name and move the produced IP core - # to where it should go. (I.e. to where the original script - # lives.) - set ip_file_name "$ip_name.xci" - set created_ip_file [lindex [glob_recursive $dir_name $ip_file_name] 0] - file rename $created_ip_file $target_dir_name - - # Find the produced example design files (or at least the ones - # under 'imports') and copy these as well. - set ex_file_name "imports" - set created_ex_dir [glob_recursive $dir_name $ex_file_name d] - if { [llength $created_ex_dir] > 0 } { - file rename $created_ex_dir [file join $target_dir_name "example_imports"] + # Have Vivado run the script and generate the IP core. + set args "-work-dir \"$dir_name_short\"" + if {[string length $target_part] != 0} { + set args [concat $args "-target-part \"$target_part\""] + } + if {[string length $target_board] != 0} { + set args [concat $args "-target-board \"$target_board\""] + } + if {[string length $user_ip_repo] != 0} { + set args [concat $args "-user-ip-repo \"$user_ip_repo\""] + } + # NOTE: Calling Vivado here as a separate process is not very + # efficient. So we source the script in a separate namespace + # instead. This should prevent the script from affecting our + # local variables. This is a bit more involved, but it + # definitely speeds things up. + variable err + variable status [catch { + namespace eval sub_ns { + # NOTE: The '-notrace' flag to the 'source' command is + # Vivado-specific. + set ::ns::status [tcl_utils::source_with_args $::ns::script_name {-notrace} $::ns::args] + } + } err] + if {$status != 0} { + puts "A problem occurred:" + set tmp {} + foreach i [split $err "\n"] { + append tmp " !!! $i\n" + } + puts -nonewline $tmp + } else { + set target_base_name [file dirname $script_name] + set target_sub_name $ip_name + set target_dir_name [file join $target_base_name $target_sub_name] + file delete -force $target_dir_name + file mkdir $target_dir_name + + # Find the produced IP core name and move the produced IP core + # to where it should go. (I.e. to where the original script + # lives.) + set ip_file_name "$ip_name.xci" + set created_ip_file [lindex [glob_recursive $dir_name_short $ip_file_name] 0] + file rename $created_ip_file $target_dir_name + + # Find the produced example design files (or at least the ones + # under 'imports') and copy these as well. + set ex_file_name "imports" + set created_ex_dir [glob_recursive $dir_name_short $ex_file_name d] + if {[llength $created_ex_dir] > 0} { + file rename $created_ex_dir [file join $target_dir_name "example_imports"] + } + } + + # Remove the Vivado project directory. + file delete -force -- $dir_name_short + + # Unmap the virtual drive, if we mapped it. + if {[tcl_utils::os_is_windows]} { + exec subst $win_virtual_drive /d + } + + # If a problem occurred, there is no need to continue with the + # other IPs. + if {$status != 0} { + break } } - # Remove the Vivado project directory. - file delete -force -- $dir_name -} + puts "$sep_line" + if {$status == 0} { + puts "Done" + } else { + puts "Failed" + } + puts "$sep_line" -if { $status == 0 } { - puts "Done" -} else { - puts "Failed" + exit $status } -puts "$sep_line" - -exit $status ######################################################################## diff --git a/scripts/vivado_utils.tcl b/scripts/vivado_utils.tcl index 230d8b09868f01c34194873afb4cb9a685dd6304..53228801b7c4f04c8f707ac6b0662cf62e0b3e75 100644 --- a/scripts/vivado_utils.tcl +++ b/scripts/vivado_utils.tcl @@ -1,5 +1,7 @@ ######################################################################## +package require Tcl 8.5 + namespace eval vivado_utils { set version 0.1 } @@ -20,32 +22,38 @@ proc vivado_utils::run_vivado_create_ip {ip_name \ default_part \ default_board \ argv} { - set parameters [list \ - [list target-part.arg $default_part "The FPGA to target"] \ - [list target-board.arg $default_board "The development board to target"] \ - [list user-ip-repo.arg "" "Path to an optional user-IP repository"] - ] - - if { [catch {array set options [cmdline::getoptions argv $parameters]}] } { + variable parameters [list \ + [list target-part.arg $default_part "The FPGA to target"] \ + [list target-board.arg $default_board "The development board to target"] \ + [list user-ip-repo.arg "" "Path to an optional user-IP repository"] \ + [list work-dir.arg "" "Path to the (temporary) working directory to use"] + ] + + variable options + if {[catch {array set options [cmdline::getoptions argv $parameters]}]} { puts [cmdline::usage $parameters] exit 1 } # The part to target. - set part $options(target-part) + variable part $options(target-part) # The board to target. - set board $options(target-board) + variable board $options(target-board) # An optional user IP repository. - set user_ip_repo_path $options(user-ip-repo) + variable user_ip_repo_path $options(user-ip-repo) + + # Working directory path. + variable work_dir $options(work-dir) vivado_utils::vivado_create_ip \ $ip_name $ip_vendor $ip_library $ip_version \ $module_name $module_properties \ $include_example_design \ $part $board \ - $user_ip_repo_path + $user_ip_repo_path \ + $work_dir } ######################################################################## @@ -59,20 +67,26 @@ proc vivado_utils::vivado_create_ip {ip_name \ {include_example_design false} \ {part ""} \ {board ""} \ - {user_ip_repo_path ""}} { - set dir_name_base vivado_create_ip_$module_name - set dir_name [file join $dir_name_base ${module_name}_ip] - set project_name ${module_name}_ip + {user_ip_repo_path ""} \ + {work_dir ""}} { + variable dir_name_base + if {[string length $work_dir]} { + set dir_name_base $work_dir + } else { + set dir_name_base [pwd] + } + variable dir_name [file join $dir_name_base ${module_name}_ip] + variable project_name ${module_name}_ip create_project -force $project_name $dir_name - if { $part ne "" } { + if {$part ne ""} { set_property PART $part [current_project] } - if { $board ne "" } { + if {$board ne ""} { set_property BOARD_PART $board [current_project] } - if { $user_ip_repo_path ne "" } { - set ip_repo_path_ori [get_property ip_repo_paths [current_fileset]] + if {$user_ip_repo_path ne ""} { + variable ip_repo_path_ori [get_property ip_repo_paths [current_fileset]] set_property ip_repo_paths "$ip_repo_path_ori $user_ip_repo_path" [current_fileset] update_ip_catalog -rebuild } @@ -87,11 +101,11 @@ proc vivado_utils::vivado_create_ip {ip_name \ -module_name $module_name # Apply the IP settings. - if { [dict size $module_properties] != 0 } { + if {[dict size $module_properties] != 0} { set_property -dict $module_properties [get_ips $module_name] } - if { $include_example_design == true } { + if {$include_example_design} { # Create the corresponding example design/project. open_example_project -force -dir $dir_name_base -in_process [get_ips $module_name] }