FPGA时序约束步骤(vivado)
情况常常是100MHz以下的简单工程不需要做很多约束,裕量绰绰有余,但是涉及到100MHz以上的工程,如DDR4的300MHz,时序约束就显得尤为重要了
常规流程
建立工程
先新建一个工程,实现一个单BIT的FIFO,代码如下
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity fifo_2bit is
Port (
-- Write domain
wr_clk : in STD_LOGIC;
wr_rst : in STD_LOGIC;
wr_en : in STD_LOGIC;
wr_data : in STD_LOGIC_VECTOR(1 downto 0);
-- Read domain
rd_clk : in STD_LOGIC;
rd_rst : in STD_LOGIC;
rd_en : in STD_LOGIC;
rd_data : out STD_LOGIC_VECTOR(1 downto 0);
-- Status signals
full : out STD_LOGIC;
empty : out STD_LOGIC
);
end fifo_2bit;
architecture Behavioral of fifo_2bit is
signal fifo_mem : STD_LOGIC_VECTOR(1 downto 0);
signal wr_ptr, rd_ptr : STD_LOGIC_VECTOR(0 downto 0);
signal wr_sync, rd_sync : STD_LOGIC_VECTOR(0 downto 0);
signal wr_full, rd_empty : STD_LOGIC;
signal wr_ptr_gray, rd_ptr_gray : STD_LOGIC_VECTOR(0 downto 0);
signal wr_ptr_gray_sync, rd_ptr_gray_sync : STD_LOGIC_VECTOR(0 downto 0);
-- Function to convert binary to Gray code
function bin2gray(bin : STD_LOGIC_VECTOR) return STD_LOGIC_VECTOR is
variable gray : STD_LOGIC_VECTOR(bin'range);
begin
gray(bin'length-1) := bin(bin'length-1);
for i in bin'length-2 downto 0 loop
gray(i) := bin(i+1) xor bin(i);
end loop;
return gray;
end function;
-- Function to convert Gray code to binary
function gray2bin(gray : STD_LOGIC_VECTOR) return STD_LOGIC_VECTOR is
variable bin : STD_LOGIC_VECTOR(gray'range);
begin
bin(bin'length-1) := gray(gray'length-1);
for i in bin'length-2 downto 0 loop
bin(i) := bin(i+1) xor gray(i);
end loop;
return bin;
end function;
begin
-- Write domain logic
process(wr_clk, wr_rst)
begin
if wr_rst = '1' then
wr_ptr <= "0";
wr_ptr_gray <= "0";
wr_full <= '0';
elsif rising_edge(wr_clk) then
if wr_en = '1' and wr_full = '0' then
fifo_mem <= wr_data;
wr_ptr <= wr_ptr + 1;
wr_ptr_gray <= bin2gray(wr_ptr + 1);
if bin2gray(wr_ptr + 1) = rd_ptr_gray_sync then
wr_full <= '1';
else
wr_full <= '0';
end if;
end if;
end if;
end process;
-- Synchronize read pointer gray code to write domain
process(wr_clk, wr_rst)
begin
if wr_rst = '1' then
rd_ptr_gray_sync <= "0";
elsif rising_edge(wr_clk) then
rd_ptr_gray_sync <= rd_ptr_gray;
end if;
end process;
-- Read domain logic
process(rd_clk, rd_rst)
begin
if rd_rst = '1' then
rd_ptr <= "0";
rd_ptr_gray <= "0";
rd_empty <= '1';
elsif rising_edge(rd_clk) then
if rd_en = '1' and rd_empty = '0' then
rd_data <= fifo_mem;
rd_ptr <= rd_ptr + 1;
rd_ptr_gray <= bin2gray(rd_ptr + 1);
if bin2gray(rd_ptr + 1) = wr_ptr_gray_sync then
rd_empty <= '1';
else
rd_empty <= '0';
end if;
end if;
end if;
end process;
-- Synchronize write pointer gray code to read domain
process(rd_clk, rd_rst)
begin
if rd_rst = '1' then
wr_ptr_gray_sync <= "0";
elsif rising_edge(rd_clk) then
wr_ptr_gray_sync <= wr_ptr_gray;
end if;
end process;
full <= wr_full;
empty <= rd_empty;
end Behavioral;
由于没有约束,可以看到Timing部分报告为NA
创建时钟
点击set
添加完之后可以在下方看到xdc文件的预览
重新布线以后,可以看到裕量十分充足
时序分析报告
打开最坏情况
双击path1可以查看具体路径
设定衍生时钟以及异步时钟
对于100MHz以上的双口RAM或者FIFO,异步时钟组一般来说是必要的,因为vivado会默认所有时钟之间都是有关联的,并由此尝试做时序分析,所以需要澄清异步时钟组之间的关系
声明这两个时钟是异步的,逻辑互斥指的是多路选择器选择的,物理互斥意思是从不同的pin脚输入
设置输入输出延时
输入延迟约束用于指定外部信号到达FPGA输入端口的时间。定义了从外部设备(AD、DSP等)发送信号到FPGA捕获该信号之间的延迟。
# 时钟周期为10ns,输入延迟为3ns
set_input_delay -clock [get_clocks clk] 3 [get_ports input_signal]
输出延迟约束用于指定FPGA输出信号到达外部设备的时间。
# 设时钟周期为10ns,输出延迟为2ns
set_output_delay -clock [get_clocks clk] 2 [get_ports output_signal]
当信号在不同的时钟域之间传递时,跨时钟域路径往往不会有严格的时序要求,因为信号会通过同步器或者其他跨时钟域处理机制。这些路径可以设置为set_false_path。
# 设置跨时钟域路径为false path
set_false_path -from [get_clocks clkA] -to [get_clocks clkB]
# 设置复位路径为false path
set_false_path -through [get_ports reset]
# 设置测试逻辑路径为false path
set_false_path -through [get_cells *test*]
有些路径可能需要多个时钟周期来完成信号传递。如果这些路径的时序要求超过一个时钟周期,可以使用set_multicycle_path约束来指定多周期路径,而不是set_false_path。有时候也可以选择将其设置为false path。
# 设路径需要两个时钟周期
set_multicycle_path 2 -from [get_cells start_cell] -to [get_cells end_cell]
set_multicycle_path <number_of_cycles> -setup -from <source> -to <destination>
set_multicycle_path <number_of_cycles> -hold -from <source> -to <destination>
<number_of_cycles>:指定路径的周期数。
-setup:指定与建立时间相关的多周期路径。
-hold:指定与保持时间相关的多周期路径。
-from <source>:指定路径的源端。
-to <destination>:指定路径的目的端。
- Setup 时间:指数据必须在时钟上升沿之前稳定的时间。通常,setup 检查的是当前时钟周期的上升沿与下一个时钟周期的上升沿之间的数据传输。
- Hold 时间:指数据在时钟上升沿之后必须保持稳定的时间。通常,hold 检查的是当前时钟周期的上升沿与下一个时钟周期的上升沿之间的数据传输。
实际工程中的应用
1、同频同相
此时的multicycle默认设置如下(单周期路径默认关系):
set_multicycle_path 1 -setup -from CLK1 -to CLK2
set_multicycle_path 0 -hold -from CLK1 -to CLK2
即默认情况下:setup检查是从launch_clk的一个上升沿到capture_clk的下一个上升沿,hold检查是从launch_clk的一个上升沿到capture_clk的捕获沿的前一个沿。
现进行设置:set_multicycle_path 2 -setup -from CLK1 -to CLK2 ,对应的时序检查变为(capture_clk右移(2-1)个周期):
这样hold检查向后(左)移动(延迟)1个period,由于-hold默认移动launch_clk,也就是launch_clk向前(向右)移动了1个时钟周期(也可看做capture_clk向左移动了1个时钟周期),如下图(这种情景设置只适用于多周期采样,例如存在图中的使能信号Clock Enable):
2、同频异相
3、慢到快
4、快到慢
具体详见多周期路径及set_multicycle_path详解_set multicycle path
最大延迟约束用于指定信号路径的最大允许延迟,最小延迟约束用于指定信号路径的最小允许延迟,用于控制信号传输的时间范围。
# 设路径的最大延迟为5ns
set_max_delay 5 -from [get_ports src] -to [get_ports dest]
# 假设路径的最小延迟为1ns
set_min_delay 1 -from [get_ports src] -to [get_ports dest]
推荐阅读 FPGA开发全攻略——时序约束
改编自8FPGA时序约束实战篇之主时钟约束_check timing no clock
wave_gen工程
以Vivado自带的wave_gen工程为例,该工程的各个模块功能较为明确,如下图所示。使用打开示例工程,搜索wavegen
为了引入异步时钟域,我们在此程序上由增加了另一个时钟–clkin2,该时钟产生脉冲信号pulse,samp_gen中在pulse为高时才产生信号。
建立工程后直接点击布线,可以在Project Summary中查看Timing
下面我们来一步一步进行时序约束。
梳理时钟树
我们首先要做的就是梳理时钟树,就是工程中用到了哪些时钟,各个时钟之间的关系又是什么样的,如果自己都没有把时钟关系理清楚,不要指望综合工具会把所有问题暴露出来。
在我们这个工程中,有两个主时钟,四个衍生时钟,如下图所示。
确定了主时钟和衍生时钟后,再看各个时钟是否有交互,即clka产生的数据是否在clkb的时钟域中被使用。
这个工程比较简单,只有两组时钟之间有交互,即:
- clk_rx与clk_tx
- clk_samp与clk2
其中,clk_rx和clk_tx都是从同一个MMCM输出的,两个频率虽然不同,但他们却是同步的时钟,因此他们都是从同一个时钟分频得到(可以在Clock Wizard的Port Renaming中看到VCO Freq的大小),因此它们之间需要用set_false_path来约束;而clk_samp和clk2是两个异步时钟,需要用asynchronous来约束。
完成以上两步,就可以进行具体的时钟约束操作了。
约束时钟
我们先把wave_gen工程的wave_gen_timing.xdc中的内容都删掉,即先看下在没有任何时序约束的情况下会综合出什么结果?
对工程综合并Implementation后,Open Implemented Design,会看到下图所示内容。
可以看到,时序并未收敛。可能到这里有的同学就会有疑问,我们都已经把时序约束的内容都删了,为什么还会报错呢,这是因为在该工程中,用了一个MMCM,并在里面设置了输入信号频率,因此这个时钟软件会自动加上约束。
所以其实可以理解为,假设全都使用外部晶振不经过MMCM或者PLL,直接作为系统时钟,由于vivado不知道时钟周期以及占空比等信息,也就无从分析时序违例。具体表现为timing报告中会显示NA,如下图所示
此时点击时钟时钟约束
也可以看到这些锁定的与MMCM相关的时钟,这是软件自动添加的约束
双击创建时钟工具可以添加时钟,对于不经过时钟管理单元的时钟,这一步是必须的
展开check timing工具
可以看到警告信息
添加以下约束,可以看到报错信息已经变更
create_clock -period 6.000 -name virtual_clock
#指定 virtual_clock 时钟信号,周期为 6.000 ns。用于同步其他逻辑元件。
set_input_delay -clock [get_clocks -of_objects [get_ports clk_pin_p]] 0.000 [get_ports rxd_pin]
#设置输入延迟。当接收到 rxd_pin 的信号时,应该考虑时钟信号 clk_pin_p 的 0.000 单位延迟。
set_input_delay -clock [get_clocks -of_objects [get_ports clk_pin_p]] -min -0.500 [get_ports rxd_pin]
#设置最小延迟。 rxd_pin 的信号必须至少在 clk_pin_p 之前 0.500 单位到达。
set_input_delay -clock virtual_clock -max 0.0 [get_ports lb_sel_pin]
#设置 lb_sel_pin 的最大延迟,相对于 virtual_clock。最大延迟为 0.0,意味着信号应该立即到达。
set_input_delay -clock virtual_clock -min -0.5 [get_ports lb_sel_pin]
#设置最小延迟。这意味着 lb_sel_pin 的信号必须至少在 virtual_clock 之前 0.5 单位到达。
set_false_path -from [get_ports rst_pin]
#指定了一个“假路径”。不需要考虑 rst_pin 信号的时序路径,它不会影响设计的正确性。
继续添加约束来解决outputdelay问题
set_output_delay -clock virtual_clock -max 0.0 [get_ports {txd_pin led_pins[*]}]
#设置输出延迟。当发送到 txd_pin 和 led_pins 的信号时,应该立即发送,不需要额外的延迟。
create_generated_clock -name spi_clk -source [get_pins dac_spi_i0/out_ddr_flop_spi_clk_i0/ODDR_inst/C] -divide_by 1 -invert [get_ports spi_clk_pin]
#定义了 spi_clk 生成时钟,来源是 dac_spi_i0/out_ddr_flop_spi_clk_i0/ODDR_inst/C,并且被除以1(即不分频)。用于同步其他逻辑元件。
set_output_delay -clock spi_clk -max 1.000 [get_ports {spi_mosi_pin dac_cs_n_pin dac_clr_n_pin}]
#设置了输出延迟。当发送到 spi_mosi_pin、dac_cs_n_pin 和 dac_clr_n_pin 的信号时,应该在 spi_clk 之前最多延迟 1.000 单位。
set_output_delay -clock spi_clk -min -1.000 [get_ports {spi_mosi_pin dac_cs_n_pin dac_clr_n_pin}]
#设置了最小延迟。信号应该至少在 spi_clk 之前 1.000 单位到达。
set_multicycle_path -from [get_cells {cmd_parse_i0/send_resp_data_reg[*]} -include_replicated_objects] -to [get_cells {resp_gen_i0/to_bcd_i0/bcd_out_reg[*]}] 2
#这行代码设置了多周期路径。它指定了从 cmd_parse_i0/send_resp_data_reg[*] 到 resp_gen_i0/to_bcd_i0/bcd_out_reg[*] 的路径,允许 2 个时钟周期的传输。
#其他行类似,设置了不同的路径约束,包括最大延迟、最小延迟、保持时间等。
set_multicycle_path -hold -from [get_cells {cmd_parse_i0/send_resp_data_reg[*]} -include_replicated_objects] -to [get_cells {resp_gen_i0/to_bcd_i0/bcd_out_reg[*]}] 1
set_multicycle_path -from [get_cells uart_rx_i0/uart_rx_ctl_i0/* -filter IS_SEQUENTIAL] -to [get_cells uart_rx_i0/uart_rx_ctl_i0/* -filter IS_SEQUENTIAL] 108
set_multicycle_path -hold -from [get_cells uart_rx_i0/uart_rx_ctl_i0/* -filter IS_SEQUENTIAL] -to [get_cells uart_rx_i0/uart_rx_ctl_i0/* -filter IS_SEQUENTIAL] 107
# For 193.75 MHz CLOCK_RATE_TX
#set_multicycle_path -from [get_cells "uart_tx_i0/uart_tx_ctl_i0/*" -filter {IS_SEQUENTIAL}] -to [get_cells "uart_tx_i0/uart_tx_ctl_i0/*" -filter {IS_SEQUENTIAL}] 105
#set_multicycle_path -from [get_cells "uart_tx_i0/uart_tx_ctl_i0/*" -filter {IS_SEQUENTIAL}] -to [get_cells "uart_tx_i0/uart_tx_ctl_i0/*" -filter {IS_SEQUENTIAL}] -hold 104
# For 166.667 MHz CLOCK_RATE_TX
set_multicycle_path -from [get_cells uart_tx_i0/uart_tx_ctl_i0/* -filter IS_SEQUENTIAL] -to [get_cells uart_tx_i0/uart_tx_ctl_i0/* -filter IS_SEQUENTIAL] 90
set_multicycle_path -hold -from [get_cells uart_tx_i0/uart_tx_ctl_i0/* -filter IS_SEQUENTIAL] -to [get_cells uart_tx_i0/uart_tx_ctl_i0/* -filter IS_SEQUENTIAL] 89
create_generated_clock -name clk_samp -source [get_pins clk_gen_i0/clk_core_i0/clk_tx] -divide_by 32 [get_pins clk_gen_i0/BUFHCE_clk_samp_i0/O]
# To keep the synchronizer registers near each other
set_max_delay -from [get_cells clkx_nsamp_i0/meta_harden_bus_new_i0/signal_meta_reg] -to [get_cells clkx_nsamp_i0/meta_harden_bus_new_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells clkx_pre_i0/meta_harden_bus_new_i0/signal_meta_reg] -to [get_cells clkx_pre_i0/meta_harden_bus_new_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells clkx_spd_i0/meta_harden_bus_new_i0/signal_meta_reg] -to [get_cells clkx_spd_i0/meta_harden_bus_new_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells lb_ctl_i0/debouncer_i0/meta_harden_signal_in_i0/signal_meta_reg] -to [get_cells lb_ctl_i0/debouncer_i0/meta_harden_signal_in_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells samp_gen_i0/meta_harden_samp_gen_go_i0/signal_meta_reg] -to [get_cells samp_gen_i0/meta_harden_samp_gen_go_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells uart_rx_i0/meta_harden_rxd_i0/signal_meta_reg] -to [get_cells uart_rx_i0/meta_harden_rxd_i0/signal_dst_reg] 2.000
set_max_delay -from [get_cells rst_gen_i0/reset_bridge_clk_rx_i0/rst_meta_reg] -to [get_cells rst_gen_i0/reset_bridge_clk_rx_i0/rst_dst_reg] 2.000
set_max_delay -from [get_cells rst_gen_i0/reset_bridge_clk_tx_i0/rst_meta_reg] -to [get_cells rst_gen_i0/reset_bridge_clk_tx_i0/rst_dst_reg] 2.000
set_max_delay -from [get_cells rst_gen_i0/reset_bridge_clk_samp_i0/rst_meta_reg] -to [get_cells rst_gen_i0/reset_bridge_clk_samp_i0/rst_dst_reg] 2.000
此外,由于对工程加入了修改,还有一个clk_in2被遗忘了,接下来,我们在tcl命令行中输入report_clock_networks -name main
,显示如下:
可以看出,Vivado会自动设别出两个主时钟,其中clk_pin_p是200MHz,这个是直接输入到了MMCM中,因此会自动约束;另一个输入时钟clk_in2没有约束,需要我们手动进行约束。
或者可以使用check_timing -override_defaults no_clock
指令,这个指令我们之前的内容讲过,这里不再重复讲了。
在tcl中输入create_clock -name clk2 -period 25 [get_ports clk_in2]
注:在Vivado中,可以直接通过tcl直接运行时序约束脚本,运行后Vivado会自动把这些约束加入到xdc文件中。
再执行report_clock_networks -name main
,显示如下:
此时重新运行检测,时序约束已经被满足