이 페이지는 타이밍(timing)에 관한 연재 글 모음의 일부입니다. 지금까지의 페이지에서는 타이밍 계산의 이론, 클록 주기 제약 조건, 타이밍 클로저(Timing Closure)의 원리를 다뤘고, Tcl 환경을 살펴보기 시작했습니다. 이 페이지에서는 타이밍 제약 조건(timing constraints)을 구체적으로 만들 수 있게 해 주는 명령들을 설명합니다.
개요
타이밍 제약 조건의 목적은 설계의 모든 경로(path)에 대한 타이밍 요구사항이 충족되도록 보장하는 것임을 기억하세요. 경로마다 요구사항이 다를 수 있으므로, 각 타이밍 제약 조건은 관련된 경로 그룹에만 적용되고 다른 것은 포함하지 않아야 합니다.
경로를 정의하는 유일한 방법은 그 경로들과 관련된 로직 요소를 가리키는 것입니다. 따라서 올바른 로직 요소 그룹을 선택하는 정확한 표현을 작성하는 능력이 중요합니다. 다시 말해 get_clocks, get_ports, get_cells 같은 명령들을 올바르게 사용하여 그 결과가 정확히 필요한 것과 일치하도록 해야 합니다.
이 페이지에서는 로직 요소 그룹을 기술하는 기본적인 방법을 살펴봅니다. 여기의 거의 모든 내용은 SDC 문법에 특화된 것입니다. 또한 Tcl 명령줄 인터페이스를 사용할 수 있다고 가정하겠습니다. Vivado와 Quartus뿐만 아니라 최근 대부분의 FPGA 툴에서도 그렇습니다.
일부 FPGA 제조사는 Synopsys의 소프트웨어가 자기 툴에 통합되어 있다고 공개적으로 밝히며, 따라서 당연히 이 툴들은 같은 Tcl 명령을 사용합니다. 반면 Vivado는 Synopsys에서 파생된 것으로 간주되지 않습니다. 그런데도 둘 사이에는 특히 Tcl 인터페이스에서 놀랍도록 비슷한 점이 몇 가지 있습니다. Quartus는 독자적으로 개발된 것으로 보이며, 따라서 그 Tcl 인터페이스는 조금 다릅니다.
언뜻 보면 이 페이지가 Tcl 스크립팅의 세부 사항에 너무 깊이 들어가는 것처럼 보일 수 있습니다. 사실은 그 반대입니다. 정확성은 매우 중요하며, 정확성은 명령이 어떻게 해석되는지 정확히 이해해야만 얻을 수 있습니다. 실제로 이 페이지는 기본 원리만 설명합니다. 문서를 읽는 것에 대안은 없습니다.
FPGA 툴에서 도움 받기
타이밍 제약 조건 작성을 시작하는 것은 종종 어렵습니다. 챙겨야 할 세부 사항이 너무 많기 때문입니다. FPGA 툴이 이 과정을 도와줄 수 있습니다.
"help" 명령은 Tcl 콘솔에서 Tcl 명령에 대한 문서를 보는 데 사용할 수 있습니다. 이것은 대개 공식 문서(예: pdf 형식)의 내용과 정확히 같습니다.
대부분의 FPGA 툴에는 타이밍 제약 조건을 자동으로 만들어 주는 GUI 인터페이스가 있습니다. 이 방법은 필요한 타이밍 제약 조건의 유형을 선택할 때 흔히 쓰입니다. 다음 단계는 목록에서 관련 로직 요소를 선택하는 것입니다. 그 결과 SDC 파일에 추가되는 하나 또는 여러 개의 타이밍 제약 조건이 만들어집니다.
이런 종류의 마법사(wizard)를 사용하면 Tcl 문법에 대한 힌트를 얻는 데 도움이 됩니다. 가끔은 자동으로 만들어진 제약 조건을 그대로 사용할 수도 있습니다. 그러나 대부분의 경우 마법사의 출력을 그대로 쓰고 싶은 유혹을 참고, 대신 타이밍 제약 조건의 목적을 가장 잘 달성하는 방법을 신중하게 생각해 보는 것이 좋습니다. 또한 프로젝트가 앞으로 어떻게 발전할지 고려하고, 타이밍 제약 조건이 시간이 지나도 계속 올바르게 유지되도록 하는 것도 중요합니다. GUI 마법사로 짧게 작업한다고 해서 이런 것들이 달성되지는 않을 것입니다.
일부 FPGA 툴에는 설계에서 로직 요소를 찾는 GUI 인터페이스도 있습니다. 이 인터페이스를 사용하면 요청한 검색에 해당하는 Tcl 명령이 화면에 표시되는 경우가 많습니다. 이것은 특정 로직 요소를 찾는 Tcl 표현식을 얻는 편리한 방법입니다. 다시 말하지만, 이 표현식은 추가 작업을 위한 출발점으로 취급해야 합니다.
대부분의 FPGA 툴이 제공하는 세 번째 기능은 Tcl 명령줄 콘솔입니다. 여기에서 명령을 직접 입력하거나(또는 복사-붙여넣기 하거나) 화면에서 결과를 볼 수 있습니다. 이를 통해 명령과 검색 패턴을 시험하고 어떤 객체가 검색되는지 확인할 수 있습니다. 이것은 검색 패턴이 올바른지 검증하는 데 도움이 됩니다.
결론적으로, FPGA 툴은 Tcl 명령을 만드는 데 도움을 줄 수 있습니다. 하지만 앞서 말했듯이 툴이 만든 Tcl 명령은 시간이 지나도 올바르게 동작함이 보장되는 정확한 검색 패턴을 작성하기 위한 기초에 불과해야 합니다.
이제 타이밍 제약 조건을 만드는 쉬운 방법을 알았으니, 제대로 하는 방법을 배울 때가 되었습니다.
네트리스트: 간단한 복습
Tcl 환경에 대해 더 구체적으로 다루기 전에 네트리스트(netlist)에 대해 간단히 복습하고 싶습니다.
네트리스트의 가장 일반적인 파일 형식은 EDIF이지만, 많은 FPGA 툴은 고유한 형식도 있습니다. 대개 합성기(synthesizer)가 네트리스트를 만들며, 이후 단계에서 툴이 이를 수정하기도 합니다. 이 파일은 로직 설계를 기본 구성 요소와 구성 요소 사이의 연결로 표현합니다. 마치 배선도와 같지만 그래픽 이미지 대신 텍스트로 표현된 것입니다.
네트리스트의 구성 요소를 셀(cell)이라고 부릅니다. 대부분의 셀은 LUT, 또 다른 작은 조합 로직 요소, 또는 플립플롭 같은 것입니다. 또한 Verilog 코드에서 블랙박스로 인스턴스화(instantiation)한 것들(예: IP 코어)도 네트리스트에서 셀로 표현됩니다. 그 밖의 셀로는 PLL, 블록 RAM, 큰 로직 요소("하드 IP") 등이 있습니다. PCIe 블록, MGT 트랜시버, 프로세서 등이 그 예입니다.
각 셀에는 여러 핀(pin)이 있습니다. 이 핀들은 물리적인 전자 부품의 외부 연결점과 같습니다. 하지만 이것을 FPGA의 외부 I/O와 혼동하지 마세요. 셀과 핀 모두 FPGA 내부에 존재합니다.
네트리스트의 상호 연결은 네트(net)로 구성됩니다. 이것은 물리적인 전선과 같습니다. 예를 들어 Verilog에서 "wire"로 신호를 정의하면 그것은 네트가 됩니다. 네트는 두 개 이상의 핀을 연결하며, 그렇게 함으로써 이 핀들이 항상 같은 로직 레벨을 갖도록 보장합니다.
로직 요소의 객체 표현
FPGA 툴이 프로젝트의 구현을 실행할 때 실제로 일어나는 일은 Tcl 스크립트가 실행되는 것입니다. 이것은 Vivado와 Quartus 및 다른 여러 FPGA 툴에 해당하는 말입니다. 다르게 동작하는 소프트웨어에서도 이렇게 가정하는 것이 여전히 타당합니다. 제약 조건 및 기타 스크립트 파일을 위한 API가 모든 것이 하나의 거대한 Tcl 스크립트라는 환상을 만들어 내기 때문입니다.
이 Tcl 스크립트의 환경(실재든 가상이든)에서 모든 로직 요소는 서로 다른 클래스에서 만들어진 객체로 표현됩니다. 이 객체들은 SDC 파일(또는 Xilinx의 XDC 파일)에 있는 타이밍 제약 조건이 접근할 수 있습니다. 마찬가지로 Tcl 명령줄 콘솔과 Tcl 스크립트의 Tcl 명령도 이 객체들에 접근할 수 있습니다.
SDC 문법으로 동작하는 모든 FPGA 툴이 지원하는 Tcl 명령 다섯 가지가 있습니다. 이 명령들은 서로 다른 유형(즉 서로 다른 클래스)의 객체를 찾는 데 사용됩니다. 앞서 타이밍 제약 조건의 예들에서 이미 이것들을 사용했습니다. 사실 의미 있는 타이밍 제약 조건을 이 명령들 없이 작성하는 것은 거의 불가능합니다.
인자 없이 사용하면 이 명령들은 해당 유형의 모든 객체를 찾습니다. 나중에 검색을 좁히는 방법을 살펴보겠습니다.
- get_cells: 이 명령은 셀(네트리스트의 구성 요소)을 나타내는 객체를 찾습니다.
- get_pins: 이 명령은 핀(셀의 연결점)을 나타내는 객체를 찾습니다.
- get_nets: 이 명령은 네트(네트리스트의 "전선")를 나타내는 객체를 찾습니다.
- get_ports: 이 명령은 FPGA 설계의 외부 I/O 포트를 나타내는 객체를 찾습니다. 이것들은 최상위 모듈의 포트입니다.
- get_clocks: 이 명령은 클록 객체를 찾습니다. 여기서 언급한 객체들 중에서 클록 객체만이 설계의 로직 요소에 대응하지 않는다는 점에 유의하세요. 오히려 클록 객체는 앞서 설명했듯이 클록에 대한 정보를 담는 데 사용됩니다.
이 다섯 명령 외에도 각 FPGA 툴은 고유한 추가 명령과 추가 객체를 가지고 있습니다. 예를 들어 Vivado에는 all_ffs, all_registers, all_inputs, all_outputs, all_rams 등과 같은 추가 명령이 있습니다. Quartus는 이들 중 일부를 지원하며, get_registers, get_keepers, get_nodes, get_fanins, get_fanouts 등도 있습니다.
이 객체들이 중요한 이유는 타이밍 제약 조건에서 FPGA 설계의 로직 요소를 가리키는 데 사용되기 때문입니다. 또한 Tcl 스크립트에서 정보(예: 클록 주파수)를 얻는 데도 사용할 수 있습니다. 각 FPGA 툴은 Tcl 스크립트에서 이러한 객체에 접근하는 고유한 API를 가지고 있습니다. 예를 들어 Vivado에서는 다음 Tcl 명령으로 클록 주기를 얻을 수 있습니다.
> get_property PERIOD [get_clocks clk] 4.000
Quartus에서도 같은 작업은 다음과 같습니다.
> get_clock_info -period [get_clocks clk] 4.000
이러한 차이에도 불구하고 대부분의 FPGA 툴은 SDC 형식 타이밍 제약 조건의 문법과 의미에 대해서는 서로 일치합니다. 각 툴이 지원하는 API에 대한 정보는 대개 Tcl 스크립팅 또는 타이밍 클로저와 관련된 제목의 사용자 가이드에서 찾을 수 있습니다.
특별히 다른 언급이 없으면 이 페이지의 예는 Vivado를 기준으로 합니다.
Tcl에 대한 몇 가지 참고 사항
Tcl은 오래된 언어이지만, 로직 설계 분야에서 확고하게 자리 잡았기 때문에 이 언어가 곧 사라질 것 같지는 않습니다. 다행히도 Tcl을 잘 알지 못해도 유용한 일을 할 수 있습니다.
첫 번째는 대괄호("["와 "]")입니다. 앞서 간단히 언급했습니다. Tcl에서 이것은 대괄호 안의 명령을 실행하고 그 결과를 대괄호 자리에 놓으라는 뜻입니다. 셸 스크립트나 Perl에 익숙한 사람들에게는 백틱(backtick)과 같습니다. 예를 들어, 다음 명령에서 get_port는 "clk"라는 이름을 가진 포트 객체로 대체됩니다.
create_clock -period 4.000 -name clk [get_ports clk]
마찬가지로, 이것은 다음과 같이 쓸 수도 있습니다.
set the_clk_port [get_ports clk] create_clock -period 4.000 -name clk $the_clk_port
두 번째 예에서 볼 수 있듯이 변수는 "set" 명령으로 정의하고 값을 할당합니다. 변수의 값을 접근할 때는 달러 기호($)를 사용합니다. 이것도 셸 스크립트와 Perl과 비슷합니다.
중괄호("{"와 "}")에 관해서는 사정이 다릅니다. 다른 여러 언어와 마찬가지로 중괄호의 의미는 문맥에 따라 크게 달라집니다. Tcl에서 중괄호의 덜 예상되는 의미 중 하나는 중괄호 안의 문자열을 그대로 두라는 뜻입니다. 다시 말해 치환(substitution)을 하지 말고, 공백도 일반 문자처럼 취급하라는 것입니다. 예를 들어 같은 타이밍 제약 조건을 다음과 같이 쓸 수도 있습니다.
create_clock -period {4.000} -name {clk} [get_ports {clk}]
이 예에서 중괄호는 전혀 필요하지 않으며, 이 명령은 이전과 정확히 같은 의미입니다. 불필요한 중괄호는 안타깝게도 흔하며, 이 예처럼 아무 의미가 없는 경우가 많습니다.
Tcl 콘솔 사용을 위한 팁
검색 결과로 찾아지는 객체의 수가 많아서 검색 명령의 출력을 읽기 어려운 경우가 자주 있습니다. 이것은 간단한 Tcl 명령으로 해결할 수 있습니다. 정확한 방법은 사용하는 툴에 따라 다릅니다. Vivado에서는 다음 명령으로 설계의 모든 셀을 출력합니다. 각 셀이 줄바꿈으로 구분되어 출력되므로 셀이 많아도 출력을 읽을 수 있습니다.
> join [get_cells -hierarchical] \n
GND
VCC
bar__0_i_1
bar_reg_OBUF_inst
bar_reg__0
[ ... ]
"join" 명령은 get_cells가 만들어 내는 배열의 각 요소 사이에 줄바꿈을 넣습니다.
Quartus에서는 다음으로 같은 결과를 얻습니다.
join [query_collection -all [get_cells -hierarchical] ] \n
또는 query_collection을 더 현명하게 사용합니다.
query_collection -all -report_format [get_cells -hierarchical]
특정 요소 찾기
긴 서론이 끝났으니, 이제 정말 흥미로운 이야기를 할 때입니다. 아래 예들을 위해 다음 Verilog 코드를 참조하세요. 이 코드는 뒤에서도 사용됩니다.
module top(
input clk,
input foo,
output reg bar_reg,
output reg baz
);
reg foo_reg;
reg bar;
reg baz_metaguard;
wire pll_clk_8, pll_clk_6;
clk_wiz_1 pll_i
(.clk_in1(clk),
.clk_out1(pll_clk_8),
.clk_out2(pll_clk_6));
always @(posedge pll_clk_8)
foo_reg <= foo;
always @(posedge pll_clk_6)
begin
bar <= !foo_reg;
bar_reg <= bar;
end
always @(posedge clk)
begin
baz_metaguard <= bar;
baz <= baz_metaguard;
end
이 페이지의 맨 위에서 말했듯이, 타이밍 제약 조건의 정확성은 올바른 로직 요소 그룹을 선택하는 것에 달려 있습니다. 따라서 위에서 언급한 다섯 개의 get_* 명령의 검색 결과를 좁힐 수 있고 또 좁혀야 합니다. 이를 수행하는 방법은 여러 가지가 있지만, 가장 일반적인 방법은 객체의 이름을 기반으로 하는 것입니다. 가장 단순한 패턴은 우리가 찾는 정확한 이름을 가진 단일 객체를 찾는 것입니다. 예를 들어 Vivado의 Tcl 콘솔에서:
> get_ports clk clk
마찬가지로 특정 패턴과 일치하는 이름을 가진 모든 객체를 찾을 수도 있습니다.
> get_pins pll_i/* pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
이 두 예의 출력은 모두 객체라는 점에 유의하세요. Vivado의 Tcl 콘솔에서는 편의를 위해 이 객체들의 이름이 출력됩니다.
더 중요한 것은, FPGA 툴마다 이러한 검색 패턴의 동작이 조금씩 다르다는 점입니다. 여기 예는 Vivado를 사용한 것입니다. 다른 툴에도 같은 원리가 적용됩니다.
별표("*")는 와일드카드 문자이며 임의의 개수의 문자와 일치합니다. 물음표("?")는 문자 하나와 일치합니다. 이것은 파일 이름에서 와일드카드가 동작하는 방식과 같습니다.
파일 이름과 마찬가지로 와일드카드는 계층 구분자(예: Vivado의 "/", Quartus의 "|")와는 일치하지 않는다는 점에 유의하세요.
계층적 경로와 파일의 디렉터리 사이에는 또 다른 유사점이 있습니다. 검색은 파일 시스템의 루트 디렉터리와 비슷한, 최상위 계층을 기준으로 수행됩니다. 예를 들면:
> get_pins */clk_* pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2 > get_pins pll_i/clk_out? pll_i/clk_out1 pll_i/clk_out2
각 로직 요소의 계층에서 정확한 위치를 지정해야 한다는 것은 종종 상당한 단점이 됩니다. 찾으려는 로직 요소들이 서로 다른 위치에 있는 경우가 많기 때문입니다. 이것은 "-hierarchical"로 해결합니다. 이 플래그가 있으면 패턴 검색이 계층의 모든 위치에서 수행됩니다.
일반적으로 와일드카드를 사용하여 로직 요소를 찾는 것은 권장되지 않습니다. 유일한 예외는 검색 패턴이 아주 단순하거나 다른 선택지가 없을 때입니다. 와일드카드와 "-hierarchical"을 사용하는 방법과 그 한계를 설명하는 별도 페이지가 있습니다.
-filter 사용하기
와일드카드 기반 검색 패턴을 사용하는 데는 몇 가지 단점이 있습니다. 가장 큰 단점은 계층이 정확히 정의되거나 전혀 정의되지 않아야 한다는 것입니다. 한 가지 문제는 검색을 특정 하위 계층에 속한 객체로 제한하고 싶은 경우가 많다는 것입니다. 그런데 와일드카드로는 이것이 불가능하며, "-hierarchical" 옵션도 이 문제를 해결하지 못합니다.
이러한 이유와 다른 이유 때문에, 검색 패턴을 정의할 때 선호되는 방법은 -filter 옵션을 사용하는 것입니다. 이 옵션에는 불리언 표현식이 따라옵니다. 이 옵션을 사용하면 그 표현식이 참인 검색 결과만 남습니다.
예를 들어,
> get_pins */clk_*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
> get_pins */clk_* -filter {name =~ *2*}
pll_i/clk_out2
이 예에서는 와일드카드로 찾은 각 객체의 "name"이라는 속성을 검사했습니다. 그 속성이 "*2*"와 일치하는 객체만 검색 결과에 남았습니다. 다시 말해 객체의 이름에 "2"가 들어 있는 경우에만 남습니다.
이 예는 실용적인 관점에서 흥미롭지 않습니다. "-hierarchical"을 사용하면 훨씬 더 흥미로워집니다. 검색 패턴 없이 모든 객체가 검색됩니다. 다시 말해 다음 명령은 모든 계층의 모든 핀을 찾습니다.
get_pins -hierarchical
여기에서 -filter로 검색 결과를 좁힐 수 있습니다.
> get_pins -hierarchical -filter {name =~ pll_i/*/*out1 }
pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ pll_i/*out1 }
pll_i/clk_out1 pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ *out1 }
pll_i/clk_out1 pll_i/inst/clk_out1
중괄호("{"와 "}")의 의미는 단지 그 안에 있는 부분을 Tcl 인터프리터가 수정하지 말라는 것임을 유의하세요.
또한 검색 패턴이 -filter 옵션에 속하므로 다르게 동작한다는 점에 유의하세요. -filter는 "name"을 객체의 속성으로 취급합니다. 따라서 모든 문자가 동등하게 취급됩니다. "/"도 특별한 의미가 없습니다. "/"가 계층 구분자라는 것은 중요하지 않습니다. "/"를 포함한 모든 문자가 와일드카드("*")와 일치할 수 있습니다. 마찬가지로 "/"를 포함한 모든 문자가 검색 패턴에 사용될 수 있습니다. -filter가 없을 때는 물론 그렇지 않습니다.
어떤 문자든 "*"와 일치할 수 있다는 사실은 -filter를 더 강력한 도구로 만들어 주지만, 이 장점은 실수의 가능성이기도 합니다. 위의 예에서 볼 수 있듯이 무해해 보이는 "*" 하나가 "pll_i/clk_"와 "pll_i/inst/clk_" 양쪽 모두와 우연히 일치할 수 있다는 점을 잊기 쉽습니다.
이 기능의 올바른 용도 중 하나는 계층 어딘가에 있는 이름을 아는 로직 요소를 찾는 것입니다.
> get_pins -hierarchical -filter {name =~ */clkout2_buf/O }
pll_i/inst/clkout2_buf/O
설계에 "clkout2_buf"라는 이름의 셀이 정확히 하나만 있다고 확신한다면 이것은 올바릅니다. 이 명령을 사용하면 이 로직 요소를 포함하는 모듈이 프로젝트 계층의 다른 위치로 옮겨지더라도 이 셀의 출력 핀이 항상 검색됩니다. 실제 시나리오에서는 "clkout2_buf"보다 더 고유한 이름을 고르는 것이 좋습니다.
같은 방법이 get_cells와 get_nets에도 적용됩니다. 예를 들어:
> get_cells -hierarchical -filter {name =~ */clk*_buf}
pll_i/inst/clkf_buf pll_i/inst/clkout1_buf pll_i/inst/clkout2_buf
하지만 -filter는 이름뿐만 아니라 모든 속성에 대해 동작합니다. 따라서 다음 명령은 이름에 "bar"가 들어 있는 모든 레지스터를 찾습니다.
get_cells -hier -filter {primitive_type =~ register.*.* && name =~ *bar*}
"&&" 연산자에 유의하세요. 이것은 논리 AND를 뜻합니다(Verilog와 C에서처럼).
이 명령에서 "primitive_type"과 "name"은 속성의 이름입니다. "=~" 연산자는 비교를 수행하며 와일드카드를 허용합니다.
객체의 속성은 "report_property" 명령(Vivado에서)으로 나열할 수 있음을 기억하세요.
이처럼 -filter는 유연하게 사용할 수 있습니다. 안타깝게도 모든 FPGA 툴에서 사용할 수 있는 것은 아닙니다.
Vivado에는 filter라는 명령이 있으며, 이것은 -filter와 같은 동작을 수행합니다. 따라서 다음 두 명령은 동등합니다.
set result [get_cells -hierarchical -filter {name =~ *_reg}] set result [filter [get_cells -hierarchical] {name =~ *_reg}]
두 번째 형식은 객체 목록이 변수에 저장되어 있을 때 유용합니다. 따라서 위의 두 명령은 다음 명령과도 동등합니다.
set all_cells [get_cells -hierarchical]
set result [filter $all_cells {name =~ *_reg}]
-regex 사용하기
정규 표현식(regular expression)에 익숙한 사람이라면 이 옵션을 사용하고 싶을 수 있습니다. 보통 이것은 좋은 생각이 아닙니다. 주된 이유는 타이밍 제약 조건을 다른 사람들이 이해하기 더 어렵게 만들기 때문입니다. 정규 표현식을 지원하는 FPGA 툴은 대개 -filter 옵션도 지원하므로, 거의 항상 -filter를 사용하는 편이 낫습니다.
일반적인 검색 패턴의 주요 문제는 계층 구분자가 와일드카드와 일치하지 않는다는 점임을 기억하세요.
-regexp는 다음과 같이 이 문제를 해결할 수 있습니다.
> get_pins -hierarchical -regexp {.+/clk_out[123]}
pll_i/clk_out1 pll_i/clk_out2 pll_i/inst/clk_out1 pll_i/inst/clk_out2
> get_pins {pll_i/[^/]+/clk[^/]+} -hierarchical -regexp
pll_i/inst/clk_in1 pll_i/inst/clk_out1 pll_i/inst/clk_out2
첫 번째 명령은 "."이 임의의 문자와 일치한다는 것을 보여 줍니다. 여기에는 "/"(계층 구분자)도 포함됩니다.
두 번째 명령은 "[^/]+"이 계층 구분자를 제외한 모든 것과 일치하는 데 사용되는 방법을 보여 줍니다. 따라서 검색 결과의 계층 깊이를 정확히 제어할 수 있습니다. 또한 이 명령은 검색 패턴이 -regexp의 인자가 아니라, -regexp가 검색 패턴의 문법을 바꾼다는 것을 보여 줍니다.
정규 표현식은 객체 이름 전체와 일치해야 한다는 점에 유의하세요. 다시 말해 툴이 정규 표현식의 시작에 "^"를, 끝에 "$"를 암시적으로 추가합니다.
하지만 같은 결과를 얻을 다른 방법이 있다면 -regex를 사용하지 마세요. 대부분의 다른 FPGA 설계자들은 그 검색 패턴을 이해하지 못할 것입니다.
-of_objects 사용하기
이름에 따라 객체를 찾는 대신, -of_objects는 다른 객체와의 관계에 따라 객체를 찾을 수 있게 해 줍니다. 대부분의 경우 "-of_objects"는 대략 "연결되어 있는"을 뜻합니다.
예를 들어, 어떤 네트에 연결된 모든 핀을 찾으려면:
> get_pins -of_objects [get_nets bar] bar_reg_reg/D baz_metaguard_reg/D bar_reg__0/Q
또는 어떤 셀의 모든 핀:
> get_pins -of_objects [get_cells bar_reg_reg] bar_reg_reg/Q bar_reg_reg/C bar_reg_reg/CE bar_reg_reg/D bar_reg_reg/R
또는 클록의 원점이 되는 핀:
> get_pins -of_objects [get_clocks clk_out1_clk_wiz_1] pll_i/inst/mmcme3_adv_inst/CLKOUT0
같은 방법으로 네트를 찾을 수 있습니다. 예를 들어 특정 핀에 연결된 네트는 무엇일까요?
> get_nets -of_objects [get_pins bar_reg_reg/C] pll_clk_6
또는 관련 셀에 연결된 네트는 무엇일까요?
> get_nets -of_objects [get_cells bar_reg_reg] bar_reg_OBUF pll_clk_6 <const1> bar <const0>
마찬가지로 이 방법으로 셀을 검색할 수 있습니다. 예를 들어 @pll_clk_6에 연결된 셀은 무엇일까요?
> get_cells -of_objects [get_nets pll_clk_6]
bar_reg__0 bar_reg_reg pll_i
"pll_i"는 Clock Wizard IP의 인스턴스 이름이라는 점에 유의하세요. 이것은 아마 검색의 원하는 결과가 아닐 것입니다. 그렇다면 검색 결과를 플립플롭만으로 좁혀 볼까요?
> get_cells -of_objects [get_nets pll_clk_6] -filter {primitive_type =~ register.*.*}
bar_reg__0 bar_reg_reg
지금까지 -of_objects가 있는 예들은 핀, 네트, 셀이 서로를 어떻게 참조할 수 있는지 보여 주었습니다. 하지만 이 옵션으로 클록 객체를 찾는 것도 가능합니다.
> get_clocks -of_objects [get_nets pll_clk_6] clk_out2_clk_wiz_1 > get_clocks -of_objects [get_cells bar_reg_reg] clk_out2_clk_wiz_1 > get_clocks -of_objects [get_pins bar_reg_reg/C] clk_out2_clk_wiz_1
이 세 명령은 클록에 연결된 로직 요소에 따라 클록을 찾는 방법을 보여 줍니다. 이 로직 요소가 셀인 경우 결과가 둘 이상일 수 있다는 점에 유의하세요. 예를 들어:
> get_clocks -of_objects [get_cells pll_i] clk clk_out1_clk_wiz_1 clk_out2_clk_wiz_1
"pll_i"는 IP이므로 그 핀들이 이 모듈의 포트라는 점을 기억하세요.
> get_pins -of_objects [get_cells pll_i] pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
따라서 특정 클록을 찾으려면 get_pins가 더 안전합니다.
> get_clocks -of_objects [get_pins pll_i/clk_out2] clk_out2_clk_wiz_1
물론 외부 I/O 포트의 클록 객체는 다음으로 찾는 것이 가장 좋습니다.
> get_clocks -of_objects [get_ports clk] clk
또는 동등한 더 짧은 명령으로:
> get_clocks [get_ports clk] clk
get_ports에 관해서 말하자면, get_ports도 -of_objects와 함께 사용할 수 있습니다. get_ports에 특화된 가능성에 대해서는 문서를 참조하세요. 다른 명령들과 비슷한 용법은 대개 무의미합니다. 예를 들어:
> get_ports -of_objects [get_nets clk] clk
요약하면, -of_objects는 특정 로직 요소를 선택하는 훌륭한 방법입니다. 특히 객체의 이름으로 검색할 때의 어려움 때문에 더욱 그렇습니다. 이름 검색은 아래에서 다음 주제로 다룹니다.
안타깝게도 많은 FPGA 툴은 -of_objects를 지원하지 않아서 덜 신뢰할 수 있는 방법에 의존할 수밖에 없습니다.
이름으로 검색할 때의 문제
앞서 이미 논의했듯이 타이밍 제약 조건의 정확성은 로직 요소를 찾는 정확성에 달려 있습니다. 이러한 검색 결과 중 적어도 일부는 객체의 이름에 의존합니다. 이것이 어떻게 문제를 일으킬 수 있는지 살펴보겠습니다.
예를 들어 "get_ports clk"는 클록 객체와 특정 I/O 핀에 존재하는 신호 사이의 연결을 만드는 데 사용됩니다. 이 I/O 핀은 "clk"라는 이름을 가진 포트 객체로 표현됩니다.
create_clock -period 4.000 -name clk [get_ports clk]
그런데 이 포트 객체는 왜 "clk"라는 이름을 갖게 되었을까요? 답은 합성기가 네트리스트를 만드는 방식과 관련이 있습니다. Verilog 코드의 최상위 포트 이름이 네트리스트의 최상위 포트 이름으로도 선택되었습니다. 이것은 당연한 선택이므로, 제대로 된 합성기라면 모두 그렇게 할 것입니다.
그렇다면 셀의 이름은 어떨까요? 위 Verilog 코드에서 "foo_reg"라는 이름의 레지스터를 보겠습니다. 이 레지스터의 플립플롭을 나타내는 셀 객체의 이름은 무엇일까요? Vivado의 합성기는 이 객체의 이름으로 "foo_reg_reg"를 선택했습니다. 따라서 이 합성기는 Verilog 코드의 이름에 "_reg" 접미사를 붙이는 경향이 있다는 것이 분명합니다. 그럴듯해 보이는 규칙입니다. 하지만 다른 합성기는 아마 다르게 할 것입니다.
그럼 "bar"라는 이름의 레지스터는 어떨까요? 관련 셀 객체의 이름은 "bar_reg"여야 했지만, 합성기는 다른 선택을 했습니다. "bar_reg__0"입니다. Verilog 코드에 "bar_reg"라는 레지스터가 있기 때문입니다. 이름 충돌을 피하기 위해 합성기는 조금 다른 이름을 골랐습니다. "_reg" 대신 "_reg__0"을 붙인 것입니다. 이 단순한 예는 객체의 이름에 의존할 때의 문제를 잘 보여 줍니다.
설상가상으로, "bar_reg"라는 레지스터가 Verilog 코드에 추가되기 전에 타이밍 제약 조건이 작성되었다고 가정해 보겠습니다. 이 경우 @bar에 해당하는 셀 객체는 평소처럼 "bar_reg"라는 이름을 얻습니다. 따라서 타이밍 제약 조건은 이 객체에 대해 이 이름을 사용했을 것입니다. 이후 어느 시점에 "bar_reg"라는 레지스터가 설계에 추가됩니다. 그 결과 요청된 셀 객체의 이름이 "bar_reg"에서 "bar_reg__0"으로 바뀝니다. "bar_reg"라는 이름에 의존하는 타이밍 제약 조건은 갑자기 올바르지 않게 됩니다. 운이 좋으면 툴이 이에 대해 경고를 보낼 것입니다.
객체 이름을 사용할 때 잘못될 수 있는 다른 이유도 있습니다. 예를 들어 툴은 레지스터의 팬아웃(fan-out)이 한도를 초과하면 그 레지스터를 자동으로 복제할 수 있습니다. 이런 일이 생기면 추가된 레지스터는 타이밍 제약 조건에 포함되지 않을 수 있습니다. 새 레지스터의 이름이 검색 패턴과 일치하지 않기 때문입니다.
더 심각한 문제는 로직 요소가 우연히 타이밍 제약 조건에 포함되는 경우입니다. 예를 들어 IP 블록에 속한 로직 요소에서 이런 일이 생길 수 있습니다. 이러한 로직 요소들의 이름을 우리가 제어할 수 없으므로, 그 이름들이 타이밍 제약 조건의 패턴과 우연히 일치할 가능성이 있습니다.
로직 요소가 타이밍 제약 조건에 우연히 포함되는 것은 게으름의 결과일 수도 있습니다. 타이밍 제약 조건은 대개 로직 설계에 새 기능을 추가하면서 함께 작성됩니다. 검색 패턴을 시행착오로 작성한다면, 미래에 추가될 로직 요소들의 이름은 고려되지 않을 수 있습니다. 따라서 새 로직이 추가되면 그중 일부 로직 요소의 이름이 기존 타이밍 제약 조건과 의도치 않게 일치할 수 있습니다.
타이밍 제약 조건 실수를 피하는 방법
첫 번째이자 가장 중요한 규칙은 타이밍 제약 조건의 검색 패턴과 일치하는 로직 요소를 테스트해 보는 것만으로는 충분하지 않다는 것입니다. 그 로직 요소들의 전체 목록을 만들고 주의 깊게 검토한다고 해도, 나중에 추가될 로직에 대해서는 아무것도 보장할 수 없습니다. 합성기나 구현의 다른 단계에서 객체의 이름이 바뀌어도 타이밍 제약 조건이 계속 예상대로 동작할 것이라는 보장도 그러한 검토로는 얻을 수 없습니다.
따라서 검색 패턴을 수학적 표현처럼 취급하는 것이 중요합니다. 검색 패턴이 지금 당장 예상대로 동작하는 것만으로는 충분하지 않습니다. 그리고 예상대로 동작하지 않을 때, 동작하도록 조금 고치는 것만으로도 충분하지 않습니다. 오히려 검색 패턴이 왜 올바른지, 그리고 시간이 지나도 왜 올바르게 유지될 가능성이 높은지에 대한 논리적 설명이 있어야 합니다.
또한 검색 패턴이 바뀔 가능성이 낮은 것들에 의존하는 것도 중요합니다. 예를 들어 툴은 Verilog 코드에 있는 인스턴스화(instantiation)의 이름을 바꾸지 않습니다. 이것은 모듈, IP, 프리미티브(primitive)의 인스턴스화 모두에 해당합니다. 따라서 계층적 경로가 Verilog 코드에 작성된 인스턴스화 이름만으로 이루어져 있다면 그것에 의존하는 것은 안전합니다. IP 블록 내부에서 만들어진 이름을 사용하는 것은 그만큼 안전하지 않습니다. 이를 설명하기 위해 다음 명령을 살펴보겠습니다.
get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]
이것은 클록을 분배하는 글로벌 클록 버퍼의 출력 핀을 참조하여 클록 객체를 얻는 방법입니다. 문제는 이것이 오랫동안 동작할 것이라고 확신할 수 있느냐입니다.
이 방법의 문제는 "inst"와 "clkout1_buf"가 Clocking Wizard IP 내부에서 만들어진 인스턴스화 이름이라는 점입니다. 이 이름들이 바뀔 가능성은 낮지만, 바뀌지 않으리라는 보장은 없습니다.
한 가지 가능한 해결책은 IP를 구현하는 Verilog 코드를 찾아서 이 Verilog를 프로젝트에 직접 포함시키는 것입니다. 그러면 미래에 아무것도 바뀌지 않음을 보장할 수 있습니다.
또 다른 방법은 Verilog에서 IP의 인스턴스화를 보는 것입니다. 그것이 다음과 같았음을 기억하세요.
clk_wiz_1 pll_i
(.clk_in1(clk),
.clk_out1(pll_clk_8),
.clk_out2(pll_clk_6));
이것은 블랙박스의 인스턴스화이므로 각 포트는 네트리스트에 핀이 있습니다. IP의 포트를 식별하는 방식이므로 이름은 바뀔 수 없습니다. 따라서 "pll_i/clk_out1"이라는 이름의 핀이 @pll_clk_8과 연결을 만든다는 것은 보장됩니다. 그러므로 클록 객체를 얻는 안전한 방법은 다음과 같습니다.
get_clocks -of_object [get_pins pll_i/clk_out1]
참고로 clk_wiz_1이 프로젝트의 또 다른 Verilog 모듈일 뿐이라면 이것은 아마 동작하지 않을 것입니다. 이 경우 이 모듈의 인스턴스화를 대신하는 핀이 생성되지 않기 때문입니다(합성기는 대개 포트의 연결을 네트 병합으로 구현합니다). 가능한 해결책은 계층의 더 낮은 수준에 나타나는 이름을 사용하는 것입니다.
의존할 수 있는 이름은 몇 가지 종류가 있습니다.
- IP가 블랙박스로 프로젝트에 포함될 때, 그 인스턴스화 이름과 IP의 핀 이름은 안전합니다.
- 프리미티브의 인스턴스화도 마찬가지입니다. 인스턴스화 이름과 핀 이름이 안전합니다. 이것은 특히 글로벌 클록 버퍼를 참조할 때 유용합니다. 또 다른 시나리오는 FPGA의 관련 프리미티브를 통해 플립플롭을 명시적으로 인스턴스화하는 것입니다. 이렇게 하면 툴이 어떤 조작도 할 수 없습니다.
- FPGA의 I/O 포트 이름도 신뢰할 수 있습니다. 이것들은 최상위 모듈의 포트 이름입니다.
- 레지스터에 길고 특이한 이름을 사용합니다. 예를 들어 모든 메타스태빌리티 보호(metastability guard) 레지스터의 이름이 "_metastability_do_not_protect"로 끝난다면, 다음과 같은 false path 제약 조건을 작성하는 것은 상당히 안전합니다.
set_false_path -to [get_cells -hier *_metastability_do_not_protect*]
패턴 끝에 있는 와일드카드에 유의하세요.
이 제약 조건이 부적절한 대상에 적용될 가능성은 낮습니다. 또한 레지스터를 놓칠 정도로 이름이 크게 바뀔 가능성도 낮습니다. 그래도 이 전략은 안전성이 조금 떨어집니다.
크게 실패하는 편이 낫다
가장 나쁜 상황은 타이밍 제약 조건이 거의 맞는 경우입니다. 포함되지 않은 경로가 몇 개뿐이거나, 실수로 포함된 경로가 몇 개뿐인 경우입니다. 이런 종류의 오류가 가장 찾기 어렵습니다.
이것이 타이밍 제약 조건이 짧고 간결하며 수학적인 스타일을 가질 때 더 좋은 주된 이유입니다. 아주 작은 로직 요소 그룹마다 타이밍 제약 조건이 하나씩 있다면, 이 긴 규칙 목록에 실수가 숨어들기 쉽습니다.
이 아이디어를 설명하기 위해 위에서 클록 객체를 찾는 예로 보여 주었던 다음 식으로 돌아가 보겠습니다.
get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]
앞서 말했듯이 이 표현의 문제는 pll_i의 인스턴스화가 계층의 다른 위치로 옮겨지면 클록 객체가 검색되지 않는다는 것입니다. 그럼 그것이 얼마나 나쁠까요?
이 클록 객체를 다음과 같이 Tcl 변수에 저장한다고 가정해 보겠습니다.
set my_clock [get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]]
그런 다음 $my_clock이 여러 타이밍 제약 조건에서 사용되므로 많은 경로가 관련됩니다. 이 경우 버그로 인해 $my_clock에 갑자기 클록 객체가 들어 있지 않게 되어도 사실 큰 문제는 아닙니다. 많은 것들이 잘못되므로 이 실수가 곧 발견될 가능성이 높기 때문입니다.
하지만 $my_clock이 단 하나의 타이밍 제약 조건에서만 사용되고, 그 제약 조건이 좀처럼 영향을 미치지 않는 작은 문제를 해결하려는 것이라면, 이것은 나쁜 상황입니다. 그 실수는 아마 눈에 띄지 않을 것입니다.
결론: 잘 작성된 타이밍 제약 조건은 완벽하게 동작하거나 아예 동작하지 않아야 합니다.
이 페이지에서는 로직 요소를 정확하게 선택하는 방법을 살펴보았습니다. 다음 페이지에서는 이 지식을 활용하여 선택적인 타이밍 제약 조건을 정의합니다.