Quartus® II Tcl Example: Arbitrary Paths Timing Reporting

author-image

By

The list_path and report_timing Tcl commands are very powerful, but they have some limitations. Path endpoints must be clocks, pins, or registers. Also, those commands do not report every combinational path between the endpoints. This advanced script example supports reporting timing on arbitrary paths in your design (including combinational endpoints), and reports all combinational paths between endpoints. The script uses an iterative search algorithm to find paths. The algorithm stops at pins and registers to prevent excessive run times.

You can specify node names, wildcards, or timegroup names for the source and destination. Timegroup exclusions are not supported by this script; a warning is displayed if you specify a timegroup that contains exclusions for the endpoints, and the exclusions are ignored.

You can direct the output of the script to a Comma Separated Value (.csv) file. The default file name is p2p_timing.csv. In addition, you can direct the output of the script to a panel in your project's timing report. The default panel name is Point-to-point Timing.

quartus_tan -t p2p_timing.tcl -project <project name> -from <node name|wildcard|timegroup name> -to <node name|wildcard|timegroup name> [-write_file] [-file <output file name>] [-write_panel] [-panel <report panel name>]

If you want to direct the output to a file different than the default file name, you must specify both the -write_file and -file <output file name> options. If you want to direct the output to a report panel different than the default report panel name, you must specify both the -write_panel and -panel <report panel name> options.

Copy the following Tcl commands to a file and name it p2p_timing.tcl.

package require cmdline 
load_package advanced_timing 
load_package report 

global quartus 
variable ::argv0 $::quartus(args) 

set options { \ 
   { "from.arg" "" "Source node name" } \ 
   { "to.arg" "" "Destination node name" } \ 
   { "project.arg" "" "Project name" } \ 
   { "file.arg" "p2p_timing.csv" "Output csv file name" } \ 
   { "write_file" "" "Write the output to a file" } \ 
   { "panel.arg" "Point-to-point Timing" "Report panel name" } \ 
   { "write_panel" "" "Write the output to a report panel" } \ 
} 

array set opts [::cmdline::getoptions ::argv0 $options "Bad option"] 
############################################################## 
# 
# Returns a list that of node names and corresponding node IDs 
# for design names that match the pattern argument. 
# Any pattern that doesn't match names in the design has an 
# empty list returned 
# Example: Pass in "reset" and get { reset 3 } back 
# 
############################################################## 
proc get_node_ids { pattern } { 
   array set name_to_node [list] 

   if { [string equal "" $pattern] } { 
      return [list] 
   }

   # Is the pattern actually the name of a timegroup? 
   # If it is, then the script recursively gets the members of 
   # the timegroup. 
   set members [get_all_global_assignments -name TIMEGROUP_MEMBER -section_id $pattern] 

   # If there are any members of the collection, 
   # the pattern is a timegroup 
   if { 0 < [get_collection_size $members]} { 
      # Warn if there are exclusions, because the script 
      # skips exclusions 
      if {0 < [get_collection_size [get_all_global_assignments -name TIMEGROUP_EXCLUSION -section_id $pattern]] } { 
         post_message -type warning "Skipping exclusions in timegroup $pattern" 
      } 

      # Go through each item in the timegroup. 
      foreach_in_collection assignment $members { 
         # Each item in the collection is a list like this: 
         # {$pattern} {TIMEGROUP_MEMBER} {node/real pattern} 
         array set sub_collection_names [get_node_ids [lindex $assignment 2]] 

         foreach node_name [array names sub_collection_names] { 
            set name_to_node($node_name) $sub_collection_names($node_name) 
         } 
      } 
   } else { 
      # It is not a timegroup 
      # Iterate through all timing nodes in the design, 
      # checking whether the name matches the pattern specified 
      foreach_in_collection node_id [get_timing_nodes -type all] { 
         set node_name [get_timing_node_info -info name $node_id] 
         if { [string match [escape_brackets $pattern] $node_name] } { 
            set name_to_node($node_name) $node_id 
         } 
      } 
   } 
   return [array get name_to_node]
} 

############################################################## 
# 
# This procedure finds combinational paths between a source 
# node and a list of destination nodes. It returns a list of 
# paths between the nodes. Each path is made up of a triplet 
# of node ID, and interconnect delay and cell delay from the 
# previous node. 
# The procedure stops traversing the netlist at a register 
# or pin, so it does not find paths that go through registers. 
#
############################################################## 
proc find_combinational_paths_between {queue dest_nodes} { 
   set num_iterations 0 
   set paths [list] 
   
   while {0 < [llength $queue]} { 
      # Report on the progress of the loop every thousand 
      # iterations incr num_iterations 
      if { 1000 == $num_iterations } { 
         set num_iterations 0 
         post_message "Checking [llength $queue] paths." 
      } 
      
      # Pop the first path from the queue. 
      # The first time the procedure is called, the queue 
      # is just one number, the source node. 
      set path [lindex $queue 0] 
      set queue [lrange $queue 1 end] 
      
      # Get the last node in the path, then in the foreach loop 
      # get fanout from that node 
      set last_triplet_in_path [lindex $path end] 
      set last_node_in_path [lindex $last_triplet_in_path 0] 
 
      # Extract just the node IDs in the current path. 
      # This is used later to ensure loops are not traversed. 
      set nodes_in_path [collapse_triplets_to_node_list $path] 

      # Get all fanouts of the last node in this path and make 
      # new paths with them to push on the queue. 
      foreach n [get_timing_node_fanout $last_node_in_path] { 
         foreach { node_id ic_delay cell_delay } $n { 
            break 
         }
 
         if { -1 != [lsearch $dest_nodes $node_id] } { 
            # If this node in the path is in the list of 
            # destination nodes, there is a path. 
            # Add it to the list of paths between the nodes 
            set new_path $path lappend 
            new_path $n 
            lappend paths $new_path 
         } 

         if { -1 == [lsearch $nodes_in_path $node_id] } { 
            # If this node in the path is not in the path 
            # already, this is not a loop. Push it on the 
            # queue if it is a combinational or clock node. 
            # The path is not pushed on if this node is a 
            # register or pin. 
            # Pushing a new path on the queue like this, 
            # even though this node in the path might match 
            # an end node, ensures the longest possible 
            # paths are found. 
            set node_type [get_timing_node_info -info type $node_id] 
            switch -exact -- $node_type { 
               comb - 
               clk { 
                  set next_path $path 
                  lappend next_path $n 
                  lappend queue $next_path 
               } 
               default { 
               } 
            } 
         } 
      }
   }
   return $paths 
} 

############################################################## 
# 
# Adds two delay numbers and returns the result. 
# Delay numbers are in the form "value units" where units 
# may be nanoseconds (ns) or picoseconds (ps), and value may 
# be x{1,3} if the units are picoseconds, or x+.y{1,3} if the 
# units are nanoseconds. This procedure normalizes delays to 
# nanoseconds and adds the values. 
# Example: add_delays "1.234 ns" "56 ps" # 
############################################################## 
proc add_delays { a b } { 
   if { ![regexp {^([\d\.]+)\s+([np]s)$} $a match a_value a_unit] } { 
      post_message -type error "Couldn't determine parts of time: $a" 
   } 

   if { ![regexp {^([\d\.]+)\s+([np]s)$} $b match b_value b_unit] } { 
      post_message -type error "Couldn't determine parts of time: $b" 
   } 
  
   # Convert everything to nanoseconds if required 
   if { [string equal -nocase ps $a_unit] } { 
      set a_value_ps [format "%.3f" $a_value] 
      set a_value [format "%.3f" [expr { $a_value_ps / 1000 }]] 
   } 

   if { [string equal -nocase ps $b_unit] } { 
      set b_value_ps [format "%.3f" $b_value] 
      set b_value [format "%.3f" [expr { $b_value_ps / 1000 }]] 
   } 

   # Now the units are equal, and nanoseconds. 
   # Just add the numbers together. 
   set sum_value [format "%.3f" [expr { $a_value + $b_value }]] 
  
   return "$sum_value ns" 
} 

############################################################## 
# 
# Formats and prints the node names in the path with the  
# delays between the nodes. 
# 
############################################################## 
proc print_path_delays { path {iteration first}} { 
   set source_triplet [lindex $path 0] 
   set source_node [lindex $source_triplet 0] 
   set source_node_name [get_timing_node_info -info name $source_node] 
   set source_node_loc [get_timing_node_info -info location $source_node] 
   
   # Print the delays first 
   if { [string equal "first" $iteration] } { 
      accumulate_data [list "IC(0.000 ns)" "CELL(0.000 ns)"] 
   } else { 
      set ic_delay [lindex $source_triplet 1] 
      set cell_delay [lindex $source_triplet 2] 
      accumulate_data [list "IC($ic_delay)" "CELL($cell_delay)"] 
   } 
   accumulate_data [list $source_node_loc $source_node_name] 
   print_accumulated_data 

   # Recurse on the rest of the path 
   if { 1 < [llength $path] } { 
      print_path_delays [lrange $path 1 end] other 
   } 
} 

############################################################## 
# 
# Sums the IC and cell delays on the specified path and 
# returns a list with total interconnect delay and total cell 
# delay. 
# 
############################################################## 
proc end_to_end_delay { path } { 
   set ic_total "0.000 ns" 
   set cell_total "0.000 ns" 
   
   # This goes through nodes 1 to end in the path because the 
   # first node in the path is the source and each node in the 
   # path contains the delays from the node preceding it. The 
   # source has no node preceding it, so it has no delays. 
   foreach n [lrange $path 1 end] { 
      foreach { node_id ic_delay cell_delay } $n { 
         break 
      } 
      set ic_total [add_delays $ic_total $ic_delay] 
      set cell_total [add_delays $cell_total $cell_delay] 
   } 

   return [list $ic_total $cell_total] 
} 

##############################################################
# 
# Ensures the specified source and destinations exist in the 
# design, finds the combinational paths between them, and 
# prints the paths. 
# 
############################################################## 
proc find_paths_and_display { source dest } { 
   array set sources [get_node_ids $source] 
   array set dests [get_node_ids $dest] 

   set nodes_exist 1 

   # Ensure the named nodes exist 
   if { 0 == [llength [array get sources]] } { 
      set nodes_exist 0 
      post_message -type error "No nodes matching $source were found in your design." 
   } 
   if { 0 == [llength [array get dests]] } { 
      set nodes_exist 0 
      post_message -type error "No nodes matching $dest were found in your design." 
   } 

   # If they do, find paths. 
   if { $nodes_exist } { 
      # Get the list of destination node ids 
      set dest_node_ids [list] 
      foreach d [array names dests] { 
         lappend dest_node_ids $dests($d) 
      } 

      # Walk through all the from nodes 
      foreach s [array names sources] { 
         set paths [find_combinational_paths_between $sources($s) $dest_node_ids] 
         if { 0 == [llength $paths] } {  
            post_message "No combinational path exists from $s to $dest" 
         } else { 
            foreach path $paths { 
               # Print out the path 
               print_path_delays $path 

               # Sum the interconnect and cell delays and 
               # print them out under the path. 
               foreach {total_ic_delay total_cell_delay } [end_to_end_delay $path] { 
                  break 
               } 
               accumulate_data [list $total_ic_delay $total_cell_delay] 
               accumulate_data [list [add_delays $total_ic_delay $total_cell_delay]] 

               # There are two calls to print_accumulated_data 
               # here, one to print the sums of the interconnect 
               # and cell delays, and one to generate a blank 
               # line in the output. 
               print_accumulated_data print_accumulated_data 
            } 
         } 
      }
   } 
} 

############################################################## 
# 
# A path is made up of triplets of information - node id, 
# interconnect delay, and cell delay. This procedure extracts 
# the node id from each triplet in order and returns a list 
# of the node ids 
# 
############################################################## 
proc collapse_triplets_to_node_list { l } { 
   set to_return [list] 
   foreach triplet $l { 
      lappend to_return [lindex $triplet 0] 
   } 
   return $to_return 
} 

############################################################## 
# 
# Concatenates information to a global variable in preparation 
# for it being printed out. 
# 
############################################################## 
proc accumulate_data { data } { 
   global accum set accum [concat $accum $data] 
}
 
############################################################## 
# 
# Print out the accumulated data. 
# It is printed to standard out and optionally to a file in 
# CSV format if the file handle exists, and optionally to a 
# report panel if the report panel exists (not a value of -1) 
# 
############################################################## 
proc print_accumulated_data {} { 
   global accum fh panel_id 
   puts [join $accum ","] 

   # Write it out to a file? 
   if { [info exists fh] } { 
      puts $fh [join $accum ","] 
   } 

   # Add it to the report panel? 
   if { -1 != $panel_id } { 
      # If the report panel row doesn't have 4 items 
      # in it, pad it to 4. 
      while { 4 > [llength $accum] } { 
         lappend accum [list] 
      } 
      add_row_to_table -id $panel_id $accum 
   } 
   # Clear the information in the global variable. 
   set accum [list] 
}

############################################################## 
############################################################## 
# 
# End of procedures, beginning of script 
# 
############################################################## 
##############################################################
# 
# Global variables to hold data for printing, and the panel 
# ID for an optional report panel 

set accum [list] 
set panel_id -1 

if { [string equal "" $opts(project)] } { 
   # Print usage options if the script is called without 
   # arguments 
   puts [::cmdline::usage $options] 
} elseif { [string equal "" $opts(project)] } { 
   post_message -type error "Specify a project with the -project option." 
} elseif { ! [project_exists $opts(project)] } { 
   post_message -type error "The project $opts(project) does not exist in this directory." 
} elseif { [string equal "" $opts(from)] } { 
   post_message -type error "Specify a name or wildcard pattern with the -from option." 
} elseif { [string equal "" $opts(to)] } { 
   post_message -type error "Specify a name or wildcard pattern with the -to option." 
} else { 
   set cur_revision [get_current_revision $opts(project)] 
   project_open $opts(project) -revision $cur_revision 

   # Try to create the timing netlist. This command would fail 
   # if quartus_fit has not yet been run, for example. 
   if { [catch { create_timing_netlist } msg ] } { 
      post_message -type error $msg 
   } else { 

      # Prepare to write the output to a file if required 
      if { 1 == $opts(write_file) } { 
         if { [catch {open $opts(file) w} fh] } { 
            post_message -type error "Couldn't open $opts(write_file): $fh" unset fh 
         } else { 
            post_message "Writing output to $opts(file)" 
            # Add some introductory information to the output file 
            puts $fh "Report of paths from $opts(from) to $opts(to)" 
            puts $fh "Generated on [clock format [clock seconds]]" 
            puts $fh "" puts $fh "IC delay,Cell delay,Node location,Node name" 
         } 
      } 

      # Prepare to write the output to a report panel if required 
      if { 1 == $opts(write_panel) } { 
         # Load the report, delete the panel if it exists already, 
         # create a new panel, and add the heading row. 
         load_report 
         set panel_id [get_report_panel_id "Timing Analyzer||$opts(panel)"] 
         if { -1 != $panel_id } { 
            delete_report_panel -id $panel_id 
         } 
        set panel_id [create_report_panel -table "Timing Analyzer||$opts(panel)"] 
        add_row_to_table -id $panel_id [list "IC delay" "Cell delay" "Node location" "Node name"] 
      } 
      find_paths_and_display $opts(from) $opts(to) 

      # close the output file if necessary 
      if { [info exists fh] } { close $fh } 

      # Save the report panel if necessary 
      if { -1 != $panel_id } { 
         save_report_database 
         unload_report 
      } 
   } 
   project_close 
}