이 페이지는 타이밍(timing)에 관한 연재 글 모음의 일부입니다. 지금까지의 페이지에서는 타이밍 계산의 이론을 설명하고, 몇 가지 타이밍 제약 조건(timing constraints)을 작성하는 방법을 보여 주었으며, 타이밍 클로저(Timing Closure)의 원리도 논의했습니다. 이 페이지에서는 멀티사이클 경로(multicycle path)를 위한 타이밍 제약 조건을 다룹니다.
서론
멀티사이클 경로에 대해 가장 먼저 알아야 할 것은, 그것을 사용하는 것이 대개 좋은 생각이 아니라는 점입니다. 이 페이지에서 멀티사이클 경로 제약 조건을 사용하는 방법을 설명하지만, 결론은 이 기법을 아예 피하는 것이 좋다는 것입니다. ASIC 세계에서는 이 기법을 사용할 이유가 있지만, FPGA 설계에서는 대개 클록을 하나 더 추가하는 편이 더 낫습니다.
그렇긴 해도, 이런 종류의 타이밍 예외(timing exception)가 언제 관련이 있는지 살펴보겠습니다.
다음 Verilog 코드 예를 보겠습니다.
reg foo, bar;
reg en, pre_en;
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
if (en)
begin
foo <= !foo;
bar <= foo;
end
end
이 예는 불완전하다는 점에 유의하세요. 아마 @pre_en과 @en에 합성 속성(synthesis attributes)을 추가해야 할 것입니다. 그렇지 않으면 합성기(synthesizer)의 최적화 때문에 예상치 못한 일이 벌어질 수 있습니다. 이에 대해서는 아래에서 더 설명합니다.
@pre_en은 매 클록 사이클마다 '0'과 '1' 사이를 오갑니다. @en도 약간의 지연을 두고 똑같이 동작합니다.
"if (en)" 부분은 @en이 "begin"과 "end" 사이 모든 것에 대해 클록 인에이블(clock enable) 역할을 한다는 뜻입니다. @en이 낮으면 이 Verilog 코드 부분에서는 아무 일도 일어나지 않습니다. 다시 말해 @en이 낮을 때는 @foo와 @bar가 마치 클록 엣지가 없는 것처럼 동작합니다.
이 예에서 @en은 클록 사이클 두 번마다 한 번씩 높아집니다. 따라서 @foo와 @bar는 마치 클록 주파수가 실제의 절반인 것처럼 동작합니다. 따라서 타이밍 요구사항도 그에 맞게 완화될 수 있습니다. tsetup 계산을 클록 주기가 두 배인 것처럼 수행할 수 있습니다.
thold에 관해서는 변화가 없습니다. 이 타이밍 요구사항의 계산은 같은 클록 엣지가 두 플립플롭에 도달한다고 가정합니다. 따라서 thold 분석의 예에서 이미 논의했듯이 클록 주기는 의미가 없습니다. 그 결과 클록이 느려진 듯한 착각은 thold에 관해서는 아무런 차이를 만들지 않습니다.
클록 인에이블을 사용해야 하는 경우
클록 인에이블을 사용할 만한 좋은 이유는 단 두 가지뿐입니다.
- 이미 잘 동작하는 로직 설계가 클록 인에이블을 기반으로 하고 있을 때.
- 클록 자원이 부족할 때. FPGA에는 이 용도에 필요한 것보다 보통 더 많은 자원이 있으므로 흔한 상황은 아닙니다.
클록 인에이블이 FPGA에서 전력 소비를 개선하는지 여부는 분명하지 않습니다. 클록을 하나 더 쓰면 전력이 낭비된다고 볼 수도 있습니다. 하지만 클록 인에이블도 팬아웃(fan-out)이 큰 신호입니다. 평균적으로 클록 인에이블은 그것이 대체하는 클록만큼 자주 값을 바꿉니다. 따라서 (전력 소비의 주요 요인인) 로직 상태 변화의 측면에서는 차이가 없습니다.
멀티사이클 경로를 꼭 써야 하는 특별한 이유가 없다면 이 페이지를 건너뛰어도 좋습니다. 설계에 기술적으로 적합한 클록 인에이블이 있더라도 관련 타이밍 예외를 사용하지 않는 편이 나을 수 있습니다. 특히 이 타이밍 예외 없이도 타이밍 제약 조건을 쉽게 달성할 수 있다면, 실수할 위험을 감수할 가치가 없습니다.
타이밍 예외
위 Verilog 코드와 관련하여, Vivado용 멀티사이클 경로 제약 조건은 다음과 같습니다.
set en_regs [all_fanout -endpoints_only -only_cells -flat [get_nets en]] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Quartus에서도 같은 작업은 다음과 같습니다.
set en_regs [get_fanouts en] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Vivado와 Quartus의 차이는 첫 행뿐입니다. Vivado에서는 "all_fanout" 명령을, Quartus에서는 "get_fanouts"를 사용합니다. SDC로 동작하는 다른 툴에서도 제약 조건은 비슷합니다.
첫 행은 @en에서 시작하는 모든 경로의 끝점에 해당하는 모든 순차 요소(셀)를 찾습니다. 이 셀 객체 목록은 $en_regs에 저장됩니다. 다음 두 행은 이 목록에 있는 셀에서 시작하고 끝나는 경로들의 타이밍 요구사항을 변경합니다.
여기에는 설명할 것이 많습니다. 왜 $en_regs의 정의를 그런 방식으로 작성했을까요? 왜 set_multicycle_path 명령이 두 개일까요? thold 타이밍 요구사항은 멀티사이클 경로의 영향을 받지 않는다고 말했는데, 왜 두 번째 명령에 "-hold"라고 써 있을까요?
설명하기 쉬운 부분인 set_multicycle_path 명령부터 시작하겠습니다.
set_multicycle_path 명령
위에서 보았듯이 명령은 다음과 같습니다.
set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
위 예를 어느 정도 일반화하기 위해, 이런 경우도 생각해 보겠습니다. 클록 인에이블이 클록 사이클 여덟 번마다 한 번씩 활성화된다면, Verilog 코드는 다음과 같았을 것입니다.
reg en;
reg [2:0] pre_en;
always @(posedge clk)
begin
pre_en <= pre_en + 1;
en <= (pre_en == 0);
end
@en은 스트로브(strobe)처럼 동작하며, 매번 한 클록 사이클 동안만 활성화된다는 점에 유의하세요. 클록 분주기의 MSB가 아닙니다.
이 경우에 대한 멀티사이클 제약 조건은 다음과 같을 것입니다.
set_multicycle_path -setup -from $en_regs -to $en_regs 8 set_multicycle_path -hold -from $en_regs -to $en_regs 7
이 두 예를 보면, 첫 번째 set_multicycle_path 명령의 숫자는 단순히 클록 인에이블의 분주비라는 것이 분명합니다.
두 번째 명령의 숫자는 같은 분주비에서 1을 뺀 값입니다. 그러니 항상 N과 N-1입니다.
첫 번째 명령에 대해서는 설명할 것이 많지 않습니다. 클록 인에이블이 N개의 클록 사이클 중 한 사이클에서 활성화된다면 허용되는 지연은 N배가 됩니다. 이것은 tsetup 타이밍 요구사항과 관련이 있습니다.
그런데 왜 두 번째 명령이 필요할까요? thold에 대해 무엇인가를 말할 필요가 왜 있을까요? 답은 첫 번째 명령이 최소 지연 요구사항도 바꿔 버리기 때문입니다. 다시 말해 "-setup" 옵션이 있는 명령이 thold 계산에도 영향을 줍니다. 왜 그럴까요? 아마 그럴듯한 설명은 없을 것입니다.
두 번째 명령이 이것을 바로잡습니다. 최소 지연 요구사항을 원래 값으로 되돌립니다. 따라서 두 번째 명령 이후에는 thold 계산이 이전처럼 수행됩니다.
두 번째 명령에서 왜 N-1을 쓰는지에 대한 긴 설명이 문서에 있습니다. 솔직히 말하면, 그 추가 정보에는 흥미로운 것이 없습니다. thold에 관한 set_multicycle_path의 동작은 이상하며, 숫자가 왜 N-1이어야 하는지 이해한다고 해서 덜 이상해지지는 않습니다.
하지만 set_multicycle_path가 쉬운 부분이었습니다. 이제 진짜 어려운 부분이 나옵니다. $en_regs로 사용할 올바른 셀 객체 목록을 만드는 것입니다.
레지스터 선택하기
$en_regs에 어떤 레지스터들을 나열할지 선택하려면, 어떤 경로가 멀티사이클 경로로 적합한지 이해해야 합니다. 규칙은 이것입니다. 클록 인에이블이 양쪽을 모두 제어하는 경우에만 경로의 타이밍 요구사항을 완화하는 것이 허용됩니다. 이것은 클록 인에이블이 비활성일 때, 두 순차 요소 중 어느 것도 클록 엣지 후에 값을 바꾸지 않음이 보장된다는 뜻입니다.
클록 도메인(clock domain)의 관점에서 생각해 보세요. 클록 인에이블이 제어하는 모든 순차 요소는 가상의 클록 도메인에 속합니다. 이 가상의 클록 도메인 안의 클록은 주파수가 더 낮으므로, 이 클록 도메인 안의 타이밍 요구사항을 조정할 수 있습니다.
그러나 경로의 한쪽이 이 가상의 클록 도메인에 속하지 않는다면, 이것은 두 관련 클록(related clocks) 사이의 가상의 클록 도메인 크로싱입니다. 그러한 경로에 대해 특별히 할 일은 없습니다. 기존 타이밍 제약 조건이 이미 처리하기 때문입니다. 그러나 이런 종류의 경로에 멀티사이클 예외를 적용하는 것은 올바르지 않습니다.
위에 제시된 타이밍 제약 조건은 이 아이디어를 반영합니다. @en이 제어하는 모든 순차 요소가 $en_regs에 셀 객체로 나열됩니다. 그런 다음 두 set_multicycle_path 명령이 이 목록에 속한 순차 요소에서 시작하고 끝나는 경로에 적용됩니다.
멀티사이클 경로에서 가장 어려운 부분은 이 순차 요소 목록이 올바른지 확인하는 것입니다. 이 목록에는 클록 인에이블이 제어하는 모든 순차 요소가 들어 있어야 합니다. 그러나 그 외의 순차 요소는 이 목록에 없어야 합니다.
이 목록에서 순차 요소가 빠지면, 관련 경로에 대한 타이밍 적용이 필요 이상으로 엄격해집니다. 이것은 재앙이 아니지만, 타이밍 예외의 효율을 떨어뜨립니다.
그러나 목록에 없어야 할 순차 요소가 실수로 추가되면 심각한 결과가 있을 수 있습니다. 타이밍 요구사항이 충분히 엄격하지 않은 경로가 생기기 때문입니다. 다시 말해 툴이 그 순차 요소의 요구사항을 제대로 동작하도록 보장하지 않습니다. 그리고 타이밍 요구사항이 충족되지 않으면 이상한 일이 일어날 수 있습니다.
저는 이 순차 요소 목록을 만들기 위해 "all_fanout" 명령(또는 "get_fanouts")을 사용하기로 했습니다. 이것이 항상 올바르게 동작한다는 보장은 없으며, 그것이 다음에 논의할 내용입니다. 그다음에는 이 목록을 만드는 다른 옵션들에 대해 논의하겠습니다. 이러한 다른 옵션들은 FPGA 툴이 "all_fanout"이나 그와 비슷한 명령을 지원하지 않을 때 특히 관련이 있습니다.
all_fanout과 get_fanouts의 잠재적 문제
멀티사이클 제약 조건에서 가장 흔한 실수는 클록 인에이블 자체(즉 @en)가 목록(즉 $en_regs)에 포함되는 것입니다. 이렇게 되면 멀티사이클 예외가 @en 자체에서 그것이 제어하는 순차 요소로 가는 모든 경로에 적용됩니다. 이것은 사실상 이 경로들의 타이밍 요구사항이 보장되지 않는다는 뜻입니다. 클록 인에이블은 팬아웃(fan-out)이 큰 신호인 경우가 많으므로 이것은 눈에 띄는 영향을 줄 수 있습니다.
위의 예에서는 @pre_en 덕분에 이것을 피했습니다. @en을 다음과 같이 정의하지 않은 이유가 궁금했을 수도 있습니다.
always @(posedge clk)
en <= !en; // Wrong!
@en이 이렇게 정의되었다면 @en에서 @en 자신으로 가는 경로가 생겼을 것입니다. 그 결과 @en이 $en_regs에 포함되었을 것입니다.
그래서 @pre_en이 이 문제를 해결합니다. 그러나 합성기가 최적화를 위해 이 레지스터를 없애지 않도록 하는 것이 중요합니다. 예를 들어 Quartus의 합성기는 (이 페이지 맨 위의 Verilog 코드에서) @pre_en의 유일한 용도가 @en에 값을 주는 것임을 감지합니다. 따라서 합성기는 @pre_en을 제거하고 마치 "en <= !en"이라고 쓰인 것처럼 계속 진행합니다. 그 결과 @en이 $en_regs에 포함됩니다. 가능한 해결책은 @pre_en을 다음과 같이 선언하는 것입니다.
reg pre_en /* synthesis preserve */;
이 단순한 예는 합성기의 예상치 못한 최적화가 어떻게 재앙적인 결과를 초래할 수 있는지 보여 줍니다. 해결책은 단순하지만, 이 최적화를 막아야 할 필요성을 간과하기 쉽습니다.
클록 인에이블에서 발생할 수 있는 또 다른 문제는 이 신호의 팬아웃(fan-out)이 흔히 크다는 사실과 관련이 있습니다. 툴은 레지스터를 자동으로 복제하여 각 복제본의 팬아웃이 특정 한도 아래가 되도록 할 수 있습니다. 그런데 이것이 $en_regs에 어떤 영향을 미칠까요? 이 목록에 포함되는 기준은 특정 네트(net)를 기반으로 했습니다. 따라서 @en의 복제본들이 제어하는 순차 요소들은 목록에 포함되지 않습니다.
이런 상황의 결과는 재앙적이지 않습니다. 앞서 말했듯이 이것은 단지 일부 경로에 대한 타이밍 적용이 필요 이상으로 엄격해진다는 뜻일 뿐입니다. 설계의 신뢰성에는 영향을 미치지 않습니다.
레지스터 복제는 높은 팬아웃의 맥락에서 이미 논의했습니다. 거기서 말했듯이 합성기가 복제하도록 기다리는 것보다 @en을 수동으로 복제하는 편이 더 낫습니다. 예상치 못한 일을 피하려면 이 레지스터의 복제를 금지하는 합성 속성을 항상 추가하세요. 나중에 높은 팬아웃이 타이밍 클로저 문제를 일으키면 수동 복제로 해결하세요. 그렇게 하면 문제의 원인을 이해하기가 더 쉬울 것입니다. 프로젝트가 커졌다고 합성기가 갑자기 @en을 복제한다면, 왜 타이밍 제약 조건이 달성되지 않았는지 깨닫기가 그렇게 쉽지 않을 것입니다.
어느 쪽이든, $en_regs를 정의하는 명령은 @en의 복제본들이 포함되도록 갱신되어야 합니다.
그런데 말이 나왔으니, $en_regs의 정의가 네트의 이름에 의존한다는 점을 유의하세요. 앞서 논의했듯이 이것은 합성기가 네트의 이름을 "en"과 다른 무엇인가로 바꾸기만 해도 $en_regs가 빈 목록이 된다는 뜻입니다. 결과적으로 멀티사이클 경로 제약 조건은 완전히 쓸모없어집니다. 이 가능성도 재앙은 아닙니다. 설계는 신뢰성을 유지하지만, 타이밍 제약 조건을 달성하기는 더 어려워집니다.
또 다른 잠재적 문제는 $en_regs가 정의되는 방식 때문에 @en을 클록 인에이블 이외의 용도로 사용해서는 안 된다는 것입니다. 예를 들어 다음 Verilog 코드를 보겠습니다.
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
이 예에서 @en은 분명히 클록 인에이블로 사용됩니다. 따라서 @counter와 관련된 모든 경로가 멀티사이클 경로인 것은 문제없습니다. 그런데 이것은 어떨까요?
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
else
counter <= counter - 1;
여기서 @en은 평범한 레지스터처럼 사용됩니다. @counter의 값은 매 클록 사이클마다 바뀝니다. 따라서 @counter는 절대로 멀티사이클 경로의 대상이 아니어야 합니다. 그런데도 @counter의 모든 플립플롭이 $en_regs에 포함됩니다. @en에서 이 모든 플립플롭으로 가는 경로가 있기 때문입니다.
이것은 @en의 복제본을 만들어 비교적 쉽게 해결할 수 있습니다.
reg [7:0] counter;
reg non_ce_en;
always @(posedge clk)
non_ce_en <= pre_en;
always @(posedge clk)
if (non_ce_en)
counter <= counter + 1;
else
counter <= counter + 2;
참고로, 합성기가 @en과 @non_ce_en을 하나의 레지스터로 합치지 못하도록 합성 속성이 필요합니다.
결론적으로, @en에서 시작하는 모든 경로를 기준으로 하는 $en_regs의 정의는 단순하고 간결합니다. 그러나 이 정의는 또한 지뢰밭이기도 합니다. 그럼 몇 가지 대안을 살펴보겠습니다.
$en_regs를 만드는 다른 방법
멀티사이클 경로 타이밍 예외를 위한 순차 요소 목록을 만드는 가장 안전한 방법은 설계 계층 구조에 의존하는 것입니다. 클록 인에이블이 제어하는 모든 로직은 별도의 모듈(그리고 가능하면 하위 모듈들)에 있어야 합니다. 그러면 셀 객체의 전체 이름을 기준으로 셀을 찾아 $en_regs를 만들 수 있습니다. 예를 들어 Vivado에서는:
set all_sync [all_fanout -endpoints_only -only_cells -flat \ [get_nets -of_objects [get_clocks clk]]] set en_regs [filter $all_sync {name =~ module_ins/multicycle_ins/* }]
첫 번째 명령은 @clk에 연결된 모든 로직 요소를 찾습니다(클록 버퍼 자체는 제외. 이 클록은 "clk"라는 이름의 클록 객체로 표현됩니다). 그 결과는 $all_sync에 저장됩니다. 이것은 관련될 수 있는 모든 순차 요소를 포함하는 목록을 만드는 한 가지 가능한 방법입니다. 두 번째 명령은 $all_sync에서 그 별도 모듈 안에 있는 모든 로직 요소의 목록을 만듭니다.
이 방법은 클록 객체의 이름과 인스턴스화(instantiation)의 이름에 의존한다는 점에 유의하세요. 이 이름들은 바뀌지 않을 것으로 예상됩니다. 이 방법을 쓰면 클록 인에이블이 복제되거나 합성기가 그 이름을 바꾸어도 문제가 없습니다.
별도 모듈을 사용할 때의 또 다른 장점은 Verilog 코드로 작업하기가 더 쉽다는 것입니다. 클록 인에이블이 제어하는 순차 요소와 그렇지 않은 순차 요소 사이에 혼동이 생길 가능성이 더 작습니다.
하지만 클록 인에이블에 의존하는 로직을 분리해서 별도 모듈에 넣는 것이 항상 자연스러운 것은 아닙니다. 게다가 Verilog 코드가 이미 작성되었고 올바르게 동작하는 것으로 알려져 있다면, 그것을 변경하는 것은 좋은 생각이 아닐 수 있습니다.
또 하나의 대안도 언급하겠습니다. 어떤 상황에서는 적합할 수 있습니다. 모든 레지스터에 대한 명명 규칙을 사용하는 것입니다. 예를 들어 클록 인에이블이 제어하는 모든 레지스터의 이름이 "MC_"로 시작하게 할 수 있습니다. 이 선택은 $en_regs를 만드는 명령을 단순하게 만듭니다. 이름에 따라 셀 객체를 검색하기만 하면 됩니다. 다른 로직 요소(예: 블록 RAM)도 인스턴스 이름을 그에 맞게 지으면 포함시킬 수 있습니다. 어떤 사람들은 이 방법이 Verilog 코드를 보기 흉하게 만든다고 하고, 어떤 사람들은 작업하기 더 쉽게 만든다고 합니다. 완벽한 방법은 없습니다.
왜 -of_objects를 사용하지 않는가
$en_regs를 단순한 기준으로 정의하는 것이 매력적으로 보일 수 있습니다. "en"이라는 이름의 네트를 찾고, 이 네트에 연결된 모든 레지스터를 추가하는 것입니다. 예를 들어 Vivado에서는 다음과 같이 쓸 수 있습니다.
set en_regs [get_cells -of_objects [get_nets en]]
이것이 틀린 데는 여러 이유가 있습니다. 첫 번째 이유는 @en 자체가 포함된다는 것입니다. 따라서 멀티사이클 제약 조건이 클록 인에이블 자체에서 그것이 제어하는 순차 요소로 가는 모든 경로에 적용됩니다. 앞서 말했듯이 이것은 심각한 실수입니다.
두 번째 이유는 일부 순차 요소가 빠질 수 있다는 것입니다. 위 명령에 따르면 목록에 포함되는 기준은 셀이 특정 네트(@en)에 연결되어야 한다는 것입니다. 이것은 그 네트가 플립플롭의 CE 입력에 직접 연결될 때만 동작합니다. 그런데 합성기는 종종 @en을 기반으로 하는 조합 함수를 사용하기로 선택합니다.
예를 들어 합성기는 @foo를 마치 Verilog 코드가 다음과 같았던 것처럼 구현하기로 선택할 수 있습니다.
foo <= foo ^ en;
이것은 원래 표현과 기능적으로 동등합니다.
if (en)
foo <= !foo;
이런 종류의 최적화는 합법적이며 예상해야 합니다. NOT 게이트를 구현하려면 합성기는 어차피 LUT를 사용해야 합니다. 그렇다면 이 LUT를 사용하여 플립플롭의 다음 값을 직접 얻는 것이 어떨까요? 플립플롭의 CE 입력에 또 다른 배선을 추가할 이유가 무엇일까요?
이 최적화의 부작용은 플립플롭 자체가 @en에 직접 연결되지 않는다는 것입니다. 따라서 $en_regs에 포함되지 않습니다. 이런 상황의 영향은 이미 논의했습니다.
합성기에게 @en을 순차 요소의 클록 인에이블 입력으로만 사용하라고 지시하는 것이 종종 가능합니다. 예를 들어 일부 합성기는 "direct_enable" 또는 그와 비슷한 이름의 합성 속성을 지원합니다. 이 기능을 사용하면 합성기가 로직을 최적화할 자유가 줄어든다는 점에 유의하세요. 따라서 툴의 기술적 문제를 해결하려다가 설계 성능이 부정적인 영향을 받을 수 있습니다.
게다가 @en이 복제되거나 이름이 바뀌면 앞서 언급한 것과 같은 문제들이 발생합니다.
따라서所有这些 이유로, 네트에 직접 연결되는 것을 기준으로 삼는 것은 나쁜 선택입니다.
리셋과의 상호작용
위의 Verilog 예에 동기 리셋(synchronous reset)을 추가한다고 가정해 보겠습니다.
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
end
always @(posedge clk)
if (reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
멀티사이클 타이밍 예외의 기본 아이디어는 모든 순차 요소가 마치 가상의 클록 도메인의 일부인 것처럼 동작한다는 것이었음을 기억하세요. 이 클록 도메인 안의 가상 클록은 @clk의 절반 주파수를 가집니다. 따라서 모든 레지스터는 @en이 낮을 때 @clk를 무시해야 합니다. 이것은 이 마지막 Verilog 코드 예에서는 사실이 아닙니다. @reset은 @en과 무관하게 효과를 가집니다.
예를 들어 @reset이 다음과 같이 정의되면 어떤 일이 일어나는지 생각해 보세요.
assign reset = foo;
이것은 합법적인 동기 리셋이지만, 아마 실용적인 용도는 없을 것입니다. 그러나 이 정의는 멀티사이클 경로 예외의 문제를 잘 보여 줍니다. @en이 높으면 그다음 클록 사이클에 @foo가 높아집니다. 그런데 그것은 @reset도 높게 만듭니다. 따라서 다음 클록 사이클에 @foo는 다시 낮아집니다. @foo는 매 클록 사이클마다 값을 바꿉니다. 그러므로 @foo에서 @foo 자신으로 가는 멀티사이클 경로는 이 경로에 대해 충분히 엄격하지 않은 타이밍 요구사항을 만듭니다.
이것은 @en이 동기 리셋도 제어하도록 만들면 쉽게 해결됩니다.
always @(posedge clk)
if (en && reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
이것은 올바릅니다. 다만 @reset은 @en과 함께 활성화되어야 합니다. 간단한 해결책은 @reset을 여러 클록 사이클 동안 높게 유지하는 것입니다.
비동기 리셋(asynchronous reset)에도 같은 원리가 적용됩니다. 비동기 리셋에 관한 페이지에 쓰인 모든 것이 여기에도 관련이 있지만, 멀티사이클 경로 제약 조건과 함께라면 훨씬 더 복잡해집니다. 아마 가장 쉬운 해결책은 다른 페이지에서 제안한 대로 동기화기를 사용하는 것입니다.
@en과 비동기 리셋
@pre_en의 장점 중 하나는 @en의 모든 복제본이 항상 같은 로직 레벨을 가지도록 보장한다는 것입니다. 이것은 당연해 보일 수 있지만, 비동기 리셋을 잘못 사용하면 보장되지 않습니다. 예를 들어 원래 Verilog 코드가 다음과 같았다고 가정해 보세요.
reg en;
always @(posedge clk or posedge reset)
if (reset)
en <= 0;
else
en <= !en; // This is not recommended!
@en이 복제되면 그 결과는 다음 코드와 동등해질 수 있습니다.
reg en, en_1, en_2;
always @(posedge clk or posedge reset)
if (reset)
begin
en <= 0;
en_1 <= 0;
en_2 <= 0;
end
else
begin
en <= !en;
en_1 <= !en_1;
en_2 <= !en_2;
end
@en_1의 다음 값은 @en의 값이 아니라 자신의 값에 의존한다는 점에 유의하세요. 이것은 레지스터 복제의 현실적인 결과입니다.
비동기 리셋이 안전하지 않은 방식으로 비활성화되면 어떤 일이 일어날까요? @en의 일부 복제본은 리셋 후 첫 번째 클록 엣지에 반응하고, 다른 복제본은 이 클록 엣지를 무시할 수 있습니다. 그 결과 복제본들의 로직 상태는 (다음 리셋 때까지) 결코 같아지지 않을 것입니다.
@pre_en은 이 문제를 해결합니다. 모든 복제본이 같은 소스에서 다음 값을 복사하기 때문입니다. 이것은 시작이 거칠더라도 장기적으로는 올바른 동작을 보장합니다.
요약
클록 인에이블을 사용하는 것은 쉽습니다. set_multicycle_path를 타이밍 예외 명령으로 사용하는 것도 쉽습니다. 그러나 이것을 안정적으로 동작하게 만드는 것은 결코 쉽지 않습니다. 잘못될 수 있는 일이 많으며, 때로는 그 이유가 성장하는 로직 설계에 응답하여 합성기가 동작을 바꾸기 때문입니다.
이런 예상치 못한 문제들의 결과는 멀티사이클 경로가 그 목적을 다하지 못하는 것일 수 있습니다. 완화된 타이밍 요구사항이 일부 경로에 적용되지 않으면 이 방법의 이점이 의문시됩니다. 더 나쁘게는, 영향을 받아서는 안 되는 경로에 멀티사이클 경로 예외가 적용되면 신뢰할 수 없는 로직 설계로 이어질 수 있습니다.
따라서 예상치 못한 일이 없었는지 확인하기 위해 관련 경로의 타이밍 리포트를 읽는 것이 매우 중요합니다. 안타깝게도 이것이 미래의 놀라움을 막아 주지는 않습니다. 설계가 발전함에 따라 합성기의 동작을 예측하기는 어렵기 때문입니다.
그러므로 가능하다면 클록 인에이블을 사용하는 대신 같은 PLL에서 추가 클록을 생성하는 것이 훨씬 낫습니다. 이 새 클록과의 클록 도메인 크로싱은 두 클록이 관련 클록(related clocks)이므로 안정적입니다. 이 새 클록에 대한 타이밍 제약 조건은 툴이 자동으로 생성합니다. 이 방법에는 어떤 놀라움의 위험도 없습니다.
그러니 클록 인에이블과 멀티사이클 예외를 설계에 추가하려고 이 글을 읽고 있다면, 이 페이지가 생각할 거리를 주었기를 바랍니다.
이상으로 FPGA 내부 경로에 대한 타이밍 제약 조건 부분을 마칩니다. 그렇다면 I/O는 어떨까요? 그것이 다음 페이지에서 다루기 시작할 내용입니다.