Quartus® II Tcl 示例:任意路径时序报告

author-image

作者

list_pathreport_timing Tcl 命令非常强大,但都存在一定的限制。路径端点必须是时钟、引脚或寄存器。此外,这些命令无法报告端点之间的每个组合路径。此高级脚本示例可报告设计中任意路径(包括组合端点)的时序,并可报告端点之间的所有组合路径。此脚本使用迭代搜索算法查找路径。算法在引脚和寄存器上停止,防止运行时间过长。

可以为源节点和目标节点指定节点名称、通配符或时序组名称。此脚本不支持时序组异常;如果指定的时序组包含端点异常并且忽略了异常,将会显示警告。

可以将脚本的输出定向到逗号分隔值 (.csv) 文件。默认文件名为 p2p_timing.csv。此外,可以将脚本的输出定向到项目时序报告中的面板。默认面板名称为“点到点时序”

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>]

如果要将输出定向到与默认文件名不同的文件,必须同时指定 -write_file 和-file <output file name> 选项。如果要将输出定向到与默认报告面板名称不同的报告面板,必须同时指定 -write_panel 和 -panel <report panel name> 选项。

将以下 Tcl 命令复制到一个文件,并将其命名为 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"] 
############################################################## 
# 
# 对与模式参数匹配的设计名称返回
# 节点名称和相应节点 ID 的列表。 
# 对于与设计中的名称不匹配的任何模式,返回
# 一个空列表
# 示例:传入 "reset" 并再次获取 { reset 3 }
# 
############################################################## 
proc get_node_ids { pattern } { 
   array set name_to_node [list] 

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

   # 模式实际上是否为时序组的名称? 
   # 如果是,脚本将以递归方式获取
   # 时序组的成员。 
   set members [get_all_global_assignments -name TIMEGROUP_MEMBER -section_id $pattern] 

   # 如果集合中有任何成员,
   # 表明模式是时序组
   if { 0 < [get_collection_size $members]} { 
      # 如果有异常,将发出警告,因为脚本
      # 会跳过异常
      if {0 < [get_collection_size [get_all_global_assignments -name TIMEGROUP_EXCLUSION -section_id $pattern]] } { 
         post_message -type warning "Skipping exclusions in timegroup $pattern" 
      } 

      # 遍历时序组中的每个项。 
      foreach_in_collection assignment $members { 
         # 集合中的每个项如以下列表所示:
         # {$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 { 
      # 它不是时序组
      # 对设计中的所有时序节点进行迭代,
      # 检查名称是否与指定模式匹配
      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]
} 

############################################################## 
# 
# 此程序查找一个源节点与
# 一组目标节点之间的组合路径。它返回节点之间的
# 路径列表。每个路径都由
# 节点 ID、上一个节点的互连延迟和单元延迟
# 组成的三元组构成。 
# 程序在寄存器或引脚上停止遍历网表,
# 因此找不到通过寄存器的路径。 
#
############################################################## 
proc find_combinational_paths_between {queue dest_nodes} { 
   set num_iterations 0 
   set paths [list] 
   
   while {0 < [llength $queue]} { 
      # 每进行一千次迭代,报告一次循环进度
      # incr num_iterations
      if { 1000 == $num_iterations } { 
         set num_iterations 0 
         post_message "Checking [llength $queue] paths." 
      } 
      
      # 弹出队列中的第一个路径。 
      # 第一次调用程序时,队列中
      # 仅有一项,即源节点。 
      set path [lindex $queue 0] 
      set queue [lrange $queue 1 end] 
      
      # 获取路径中的最后一个节点,然后在 foreach 循环中
      # 获取此节点的扇出
      set last_triplet_in_path [lindex $path end] 
      set last_node_in_path [lindex $last_triplet_in_path 0] 
 
      # 仅提取当前路径中的节点 ID。 
      # 稍后使用这些节点 ID 确保没有对循环进行遍历。 
      set nodes_in_path [collapse_triplets_to_node_list $path] 

      # 获取此路径中最后一个节点的所有扇出,
      # 并使用这些扇出生成新的路径,从而将队列向前推进。 
      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] } { 
            # 如果路径中的此节点包含在
            # 目标节点列表中,表明存在路径。 
            # 将其添加到节点间路径的列表中
            set new_path $path lappend 
            new_path $n 
            lappend paths $new_path 
         } 

         if { -1 == [lsearch $nodes_in_path $node_id] } { 
            # 如果路径中的此节点不再处于此路径中,
            # 表明它不是循环。如果它是组合节点
            # 或时钟节点,将它添加到队列中,将队列向前推进。 
            # 如果此节点是寄存器或引脚,则不添加
            # 路径。 
            以这种方式在队列中添加新路径,
            # 即使路径中的此节点可能与
            # 一个端点节点匹配,也能确保找到
            # 最长的路径。 
            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 
} 

############################################################## 
# 
# 添加两个延迟数,然后返回结果。 
# 延迟数采用“值单位”的形式,其中单位
# 可为纳秒 (ns) 或皮秒 (ps),值可为
# x{1,3}(如果单位是皮秒)或 x+.y{1,3}(如果
# 单位是纳秒)。此程序将延迟归一化为
# 纳秒,并添加值。 
# 示例: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" 
   } 
  
   # 必要时将所有延迟转换为纳秒
   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 }]] 
   } 

   # 现在单位均为纳秒。 
   # 将数字相加。 
   set sum_value [format "%.3f" [expr { $a_value + $b_value }]] 
  
   return "$sum_value ns" 
} 

############################################################## 
# 
# 如果路径中的节点之间有延迟,对这些节点的名称进行
# 格式化和打印。 
# 
############################################################## 
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] 
   
   # 先打印延迟
   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 

   # 对路径的其余部分进行递归
   if { 1 < [llength $path] } { 
      print_path_delays [lrange $path 1 end] other 
   } 
} 

############################################################## 
# 
# 将指定路径上的互连延迟和单元延迟相加,然后
# 返回总互连延迟和总单元延迟的
# 列表。 
# 
############################################################## 
proc end_to_end_delay { path } { 
   set ic_total "0.000 ns" 
   set cell_total "0.000 ns" 
   
   # 由于路径中的第一个节点是源节点,路径中的
   # 每个节点都包含上一个节点的延迟,因此需要
   # 从节点 1 进行到路径中的最后一个节点。源
   # 节点前面没有任何节点,因此不包含延迟。 
   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] 
} 

##############################################################
# 
# 确保设计中有指定源节点和目标
# 节点,找到它们之间的组合路径,然后
# 打印路径。 
# 
############################################################## 
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 

   # 确保存在指定节点
   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 { $nodes_exist } { 
      # 获取目标节点 ID 列表
      set dest_node_ids [list] 
      foreach d [array names dests] { 
         lappend dest_node_ids $dests($d) 
      } 

      # 遍历所有节点
      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_path_delays $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]] 

               # 此处调用了两次 print_accumulated_data
               # 一次是为了打印互连延迟与
               # 单元延迟的和,另一次是为了在输出中
               # 生成空行。 
               print_accumulated_data print_accumulated_data 
            } 
         } 
      }
   } 
} 

############################################################## 
# 
# 路径由信息三元组构成 - 节点 ID、
# 互连延迟和单元延迟。此程序按顺序
# 提取每个三元组中的节点 ID,然后返回
# 节点 ID 列表
# 
############################################################## 
proc collapse_triplets_to_node_list { l } { 
   set to_return [list] 
   foreach triplet $l { 
      lappend to_return [lindex $triplet 0] 
   } 
   return $to_return 
} 

############################################################## 
# 
# 将信息连接到准备中的全局变量,
# 以供打印。 
# 
############################################################## 
proc accumulate_data { data } { 
   global accum set accum [concat $accum $data] 
}
 
############################################################## 
# 
# 打印累加数据。 
# 将其打印到标准输出,如果已存在文件句柄,
# 可以 CSV 格式打印到文件,如果已存在
# 报告面板(值不是 -1),可以打印到报告面板
# 
############################################################## 
proc print_accumulated_data {} { 
   global accum fh panel_id 
   puts [join $accum ","] 

   # 将其写入文件? 
   if { [info exists fh] } { 
      puts $fh [join $accum ","] 
   } 

   # 将其添加到报告面板? 
   if { -1 != $panel_id } { 
      # 如果报告面板行中的项数不足 4 个,
      # 填充成 4 项。 
      while { 4 > [llength $accum] } { 
         lappend accum [list] 
      } 
      add_row_to_table -id $panel_id $accum 
   } 
   # 清除全局变量中的信息。 
   set accum [list] 
}

############################################################## 
############################################################## 
# 
# 程序结束,脚本开始
# 
############################################################## 
##############################################################
# 
# 用于容纳待打印数据的全局变量,以及
# 可选报告面板的面板 ID

set accum [list] 
set panel_id -1 

if { [string equal "" $opts(project)] } { 
   # 如果在未使用参数的情况下调用了脚本,打印 usage
   # 选项
   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 

   # 尝试创建时序网表。在尚未运行 quartus_fit 等
   # 情况下,此命令将会失败。 
   if { [catch { create_timing_netlist } msg ] } { 
      post_message -type error $msg 
   } else { 

      # 必要时,准备将输出写入文件
      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)" 
            # 在输出文件中添加一些介绍信息
            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" 
         } 
      } 

      # 必要时,准备将输出写入报告面板
      if { 1 == $opts(write_panel) } { 
         # 加载报告,删除已有面板,
         # 创建一个新面板,并添加标题行。 
         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) 

      # 必要时关闭输出文件
      if { [info exists fh] } { close $fh } 

      # 必要时保存报告面板
      if { -1 != $panel_id } { 
         save_report_database 
         unload_report 
      } 
   } 
   project_close 
}