이 페이지는 타이밍(timing)에 관한 연재 글 모음의 일부입니다. 지금까지의 페이지에서는 타이밍 계산의 이론과 클록 주기 타이밍 제약 조건(timing constraints) 같은 몇 가지 기본 주제를 다뤘습니다. 또한 타이밍 리포트 몇 개를 보여 주고 설명했습니다. 이제 이 지식의 중요한 목적 중 하나인 타이밍 문제 해결에 대해 이야기할 차례입니다.
서론
FPGA 툴이 겪는 가장 큰 어려움은 타이밍 제약 조건의 요구사항을 충족시키는 일입니다. 보통은 성공하지만 때로는 실패합니다. 실패했을 때, 왜 실패했는지 찾아내고 고쳐야 할 책임은 우리 인간에게 있습니다. 이 작업에는 이름이 붙어 있습니다. 바로 타이밍 클로저(Timing Closure)입니다. 그리고 결코 쉬운 작업이 아닙니다.
타이밍 클로저가 왜 어려울까요? 툴에는 FPGA의 자원을 최적으로 활용하려는 배치 및 배선(place and route) 알고리듬이 있습니다. 대개 이 알고리듬은 처음에 별다른 노력 없이 로직 요소들을 FPGA에 배치하는 것에서 시작합니다. 그다음 반복 과정이 시작됩니다. 툴은 모든 경로(path)를 훑으면서 타이밍 제약 조건을 충족하지 못하는 경로를 찾아냅니다. 이 실패들을 바로잡기 위해 해당 경로들에 보정 조치를 취합니다. 가장 대표적인 조치는 로직 요소를 FPGA의 다른 위치로 옮기고, 배선을 조정하는 것입니다. 더 고급 보정 조치에 대해서는 FPGA 툴마다 저마다의 방법이 있습니다.
모든 경로가 타이밍 제약 조건을 충족하면 구현(implementation)이 끝난 것으로 간주합니다. 하지만 툴이 이 목표를 달성하지 못하고 결국 시도를 포기하면서 구현이 끝나는 경우도 있습니다. 이런 상황에서는 툴이 노력을 멈춘 시점까지 달성한 결과물을 받게 됩니다. 그 결과물은 반드시 최적이지는 않습니다. 개선할 수 있었던 경로들이 남아 있을 수 있는데, 툴은 다른 문제를 고치느라 바빴고, 그 문제 해결이 실패하자 다른 것들을 시도하지도 않고 포기했을 수 있습니다. 마치 툴이 이렇게 말하는 것 같습니다. "어차피 실패할 구현에 시간을 낭비할 가치가 없다."
FPGA 설계자로서 우리의 임무는 이 차선의 결과를 살펴보고, 왜 타이밍 제약 조건의 목표를 달성하지 못했는지 그 이유를 찾는 것입니다.
알고리듬은 시간이 지나면서 더 좋아집니다. 타이밍 제약 조건 달성 실패의 흔한 원인이 발견되면 다음 버전의 소프트웨어에는 그 상황에 대한 특화된 해결책이 포함됩니다. 그러므로 툴이 실패할 때는 대개 그럴 만한 이유가 있습니다.
그러므로 우리는 툴이 달성한 결과를 보고 스스로에게 물어봐야 합니다. 도대체 왜 실패했을까? 우리가 불가능한 것을 요구한 것은 아닐까? 더 중요한 것은 불필요한 것을 요구하지는 않았을까? 툴을 실패하게 만든 장애물이 애초에 필요조차 없는 것일 수도 있습니다. 아니면 최적화 알고리듬이 제대로 작동하지 않은 것일까요? 때로는 단순히 운이 나빴던 것입니다. 초기 로직 배치가 너무 엉망이어서 이후의 성능 개선 시도가 처음부터 실패할 수밖에 없는 경우도 있습니다.
문제가 무엇이든 실패 원인을 찾는 일은 범죄 현장을 조사하는 형사의 일과 비슷합니다. 사실은 눈앞에 있는데 원인은 흔히 숨겨져 있습니다. 이러한 사실 대부분은 타이밍 리포트에서 찾을 수 있지만, 단서가 쉽게 드러나지는 않습니다. 항상 스스로에게 물어야 할 질문은 타이밍 리포트에서 무엇이 잘못되었거나, 평소와 다르거나, 비정상적인가입니다. 범인을 찾으려는 형사처럼, 문제로 이어지는 세부 사항을 찾는 것이 목표입니다.
그런데 비정상적인 것을 찾으려면 정상적인 것이 무엇인지 알아야 합니다. 예를 들어 팬아웃(fan-out)이 특정 값일 때 네트(net)의 정상적인 지연은 얼마인가? 특정 로직 함수를 구현할 때 로직 레벨 몇 개가 정상인가? 이런 질문에 대한 대답은 FPGA마다 다릅니다. 따라서 모든 것이 정상일 때도 타이밍 리포트를 읽고 이해하면서 경험을 쌓는 것이 필요합니다. 타이밍 리포트가 문제를 가리키는 부분을 찾으려면, 모든 것이 정상일 때의 리포트가 어떤 모습인지 알고 있어야 합니다. 이전 페이지들에서 세부 사항까지 깊게 다룬 이유가 궁금하다면, 그것이 바로 이유 중 하나입니다.
임계 경로
툴이 타이밍 제약 조건을 달성하지 못하면, 슬랙(slack)이 음수인 경로가 적어도 하나 있다는 뜻입니다. 슬랙이 가장 많이 음수인 경로를 임계 경로(Critical Path)라고 부릅니다. 이 이름은 타이밍 클로저의 일반적인 전략을 반영합니다. 임계 경로에 집중하는 것이 흔히 타이밍 문제를 해결하는 방법입니다. 하지만 아래에서 설명하겠지만, 이 전략이 시간 낭비가 될 수도 있습니다.
타이밍 제약 조건이 달성된 경우, 임계 경로는 슬랙이 가장 작은 경로입니다. 이 경로는 대개 그다지 흥미롭지 않습니다. 툴은 슬랙이 양수인 경로를 개선하려 하지 않기 때문입니다. 그래서 최악인 경로의 슬랙이 양수라면, 그 경로가 최악으로 꼽힌 것은 우연일 수 있습니다.
그러나 슬랙이 양수이지만 거의 0에 가깝다면(예: 0.2ns 미만), 그 경로가 타이밍 제약 조건을 달성하기 어려웠다는 신호일 수 있습니다. 이런 임계 경로는 미래에 문제를 일으킬 수 있다는 경고로 볼 수 있습니다. 특히 FPGA에 더 많은 로직이 채워져 툴의 노력이 다른 경로로 분산되면 그럴 수 있습니다.
타이밍 리포트에는 보통 클록마다 제한된 수의 임계 경로가 포함됩니다. 대부분의 FPGA 툴은 슬랙이 양수일 때(즉 타이밍 제약 조건이 달성되었을 때)에도 몇 개의 임계 경로를 표시하는 것이 기본 설정입니다. 이것이 권장되는 설정입니다.
임계 경로의 예
임계 경로 분석의 예부터 시작하겠습니다. 이 예에 사용된 Verilog 코드는 다음과 같습니다.
reg [24:0] calc, result;
reg [11:0] x, y, z;
always @(posedge clk)
begin
calc <= x * y + z;
result <= calc;
end
이 예에서 @clk의 주파수는 250MHz이고, 이 클록을 만들기 위해 PLL은 사용되지 않았습니다. 또한 @x, @y, @z는 @clk에 동기되는 레지스터라고 가정하겠습니다. 이 레지스터들에 값을 할당하는 Verilog 코드는 관련이 없으므로 표시하지 않았습니다.
이 코드를 Vivado에서 구현하려고 했을 때 타이밍 제약 조건이 달성되지 않았습니다. 타이밍 리포트에서 이것이 임계 경로로 나타났습니다.
Slack (VIOLATED) : -0.239ns (required time - arrival time) Source: x_reg[1]__0_replica_2/C (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg[23]/D (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 4.180ns (logic 1.642ns (39.282%) route 2.538ns (60.718%)) Logic Levels: 7 (CARRY8=4 LUT3=1 LUT4=1 LUT6=1) Clock Path Skew: -0.087ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.176ns = ( 7.176 - 4.000 ) Source Clock Delay (SCD): 3.864ns Clock Pessimism Removal (CPR): 0.601ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.032ns (routing 0.396ns, distribution 1.636ns) Clock Net Delay (Destination): 1.748ns (routing 0.365ns, distribution 1.383ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y0 (CLOCK_ROOT) net (fo=106, routed) 2.032 3.864 clk_IBUF_BUFG SLICE_X54Y54 FDRE r x_reg[1]__0_replica_2/C ------------------------------------------------------------------- ------------------- SLICE_X54Y54 FDRE (Prop_HFF2_SLICEL_C_Q) 0.137 4.001 r x_reg[1]__0_replica_2/Q net (fo=21, routed) 0.371 4.372 x[1]_repN_2 SLICE_X56Y53 LUT6 (Prop_E6LUT_SLICEL_I1_O) 0.219 4.591 r calc[23]_i_101/O net (fo=2, routed) 0.550 5.141 calc[23]_i_101_n_0 SLICE_X54Y57 CARRY8 (Prop_CARRY8_SLICEL_DI[5]_CO[7]) 0.228 5.369 r calc_reg[23]_i_30/CO[7] net (fo=1, routed) 0.030 5.399 calc_reg[23]_i_30_n_0 SLICE_X54Y58 CARRY8 (Prop_CARRY8_SLICEL_CI_O[1]) 0.163 5.562 r calc_reg[23]_i_22/O[1] net (fo=3, routed) 0.351 5.913 calc_reg[23]_i_22_n_14 SLICE_X56Y57 LUT3 (Prop_C6LUT_SLICEL_I1_O) 0.146 6.059 r calc[23]_i_26/O net (fo=3, routed) 0.240 6.299 calc[23]_i_26_n_0 SLICE_X55Y58 LUT4 (Prop_A6LUT_SLICEM_I0_O) 0.089 6.388 r calc[23]_i_7/O net (fo=1, routed) 0.407 6.795 calc[23]_i_7_n_0 SLICE_X53Y57 CARRY8 (Prop_CARRY8_SLICEM_DI[2]_O[4]) 0.308 7.103 r calc_reg[23]_i_2/O[4] net (fo=1, routed) 0.538 7.641 P[20] SLICE_X54Y56 CARRY8 (Prop_CARRY8_SLICEL_S[4]_O[7]) 0.352 7.993 r calc_reg[23]_i_1/O[7] net (fo=1, routed) 0.051 8.044 P0_out[23] SLICE_X54Y56 FDRE r calc_reg[23]/D ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y0 (CLOCK_ROOT) net (fo=106, routed) 1.748 7.176 clk_IBUF_BUFG SLICE_X54Y56 FDRE r calc_reg[23]/C clock pessimism 0.601 7.777 clock uncertainty -0.035 7.741 SLICE_X54Y56 FDRE (Setup_HFF_SLICEL_C_D) 0.063 7.804 calc_reg[23] ------------------------------------------------------------------- required time 7.804 arrival time -8.044 ------------------------------------------------------------------- slack -0.239
이 경로의 슬랙은 –0.239ns이므로 타이밍 제약 조건을 살짝 충족하지 못했습니다. 가장 먼저 살펴볼 것은 이 경로의 시작과 끝입니다. 리포트 헤더의 "Source"와 "Destination"을 보면 x_reg와 calc_reg가 나옵니다. 문제의 원인은 분명히 다음 부분입니다.
calc <= x * y + z;
Verilog 코드에서 유일한 의미 있는 연산이므로 놀랄 일은 아닙니다. 실제 시나리오에서는 어떤 로직 부분이 문제를 일으켰는지가 이렇게 명확하지 않습니다.
타이밍 리포트에는 로직 레벨이 7개나 된다는 사실도 분명히 드러납니다. 조합 경로(combinatorial path)가 너무 깁니다. 다시 말해 @clk의 두 클록 엣지 사이에 처리해야 할 일이 너무 많습니다.
그렇다면 실패의 실제 원인은 무엇일까요? 지연의 61%를 라우팅이 차지한다는 사실 때문일까요? 라우팅 지연이 보통 전체 지연의 40% 정도 된다는 경험 법칙이 있다는 점을 기억하세요. 그렇다면 FPGA 툴이 더 잘 작동하도록 조치를 취해볼까요? 하지만 그것은 성공 가능성이 낮은 해결책입니다. 툴은 보통 경로의 타이밍 제약 조건 달성을 포기하기 전에 이미 열심히 노력했기 때문입니다.
@x와 @calc 사이의 로직 함수를 바꾸려는 시도도 마찬가지로 소용없습니다. 곱셈은 필수적이므로 더 간단한 다른 것으로 대체할 방법이 없습니다.
이와 같은 문제를 해결하는 다른 가능한 접근법들은 다음 페이지에서 소개하겠습니다. 하지만 이번 경우에는 기법 목록을 훑어보는 것이 도움이 되지 않습니다. 이 단순한 예는 때로는 형사처럼 생각해야 한다는 것을 보여 줍니다.
머리를 쓰는 것에 대안은 없다
타이밍 리포트를 읽을 때 가장 먼저 물어야 할 질문은 그 리포트에서 무엇이 비정상적인가입니다. 이 예에서 그 답은 조합 경로가 슬라이스(slice)로만 이루어져 있다는 것입니다. 사실상 모든 FPGA에는 곱셈이 요구될 때 사용하도록 지정된 전용 산술 유닛(DSP, ALU 등 이름은 다양함)이 있습니다. 실제로 곱셈과 덧셈은 그러한 전용 로직에서 가장 흔한 기능입니다. 따라서 대부분의 경우 가장 간단한 해결책은 툴이 전용 산술 유닛을 사용하도록 하는 것입니다. 이 해결책에 대한 타이밍 리포트는 이 페이지 맨 아래에 있습니다.
하지만 우리가 실제로 물어야 할 질문은 왜 전용 산술 유닛 대신 슬라이스가 사용되었는가입니다. 가장 흔한 이유는 FPGA에서 사용 가능한 산술 유닛이 모두 설계의 다른 부분에서 이미 사용 중이기 때문입니다. 이 경우 필요한 변경은 임계 경로와 전혀 관련이 없을 수도 있습니다. 설계에서 일부 로직을 제거하여 산술 유닛 몇 개를 확보해야 할 수도 있습니다. 또 다른 가능성은 툴에게 설계의 여러 부분 간에 산술 유닛을 다르게 배분하도록 지시하는 것입니다.
이 예에서 슬라이스가 산술 유닛 대신 사용된 이유는 제가 그렇게 되도록 의도했기 때문입니다. Vivado의 합성기(synthesizer) 매개변수 중 하나로 산술 유닛 사용을 의도적으로 꺼두었습니다(max_dsp를 0으로 설정). 하지만 그 때문에 이 예가 인위적이라고 할 수는 없습니다. FPGA 툴에 잘못된 매개변수를 사용하여 정확히 이런 상황이 생기는 일이 때때로 있습니다. 사실 산술 유닛이 설계의 다른 곳에서 더 필요하기 때문에 의도적으로 사용하지 않는 것이 올바른 경우도 있습니다.
따라서 쉬운 해결책은 전용 산술 유닛을 사용하는 것이었습니다. 하지만 어쩔 수 없이 슬라이스를 써야 한다면 어떨까요? 이번에도 해결책은 간접적입니다. 문제가 되었던 부분을 다시 보세요.
calc <= x * y + z;
그런데 바로 그다음에 이 코드가 온다는 점에 주목하세요.
result <= calc;
@calc가 이 행에서만 사용되고 다른 곳에서는 사용되지 않는다면, 계산을 파이프라이닝(pipelining)으로 두 단계로 나눌 수 있습니다. 이 기법은 흔히 파이프라이닝이라고 합니다. 그러면 Verilog 코드는 다음과 같이 바뀝니다.
reg [24:0] calc, result;
reg [11:0] x, y, z, z_d;
always @(posedge clk)
begin
z_d <= z;
calc <= x * y;
result <= calc + z_d;
end
이 해결책에서 @calc에는 곱셈 결과만 저장됩니다. @z의 값은 다음 단계에서만 @calc에 더해집니다. 더 정확히 말하면, 덧셈 연산은 한 클록 사이클 뒤에 일어나므로 @calc와 @z_d 사이에서 수행됩니다. 따라서 @result의 값은 이전과 정확히 같습니다.
원래 Verilog 코드에서 @result가 단순히 @calc의 지연된 복사본이었기 때문에 이렇게 쉽게 문제를 해결할 수 있었습니다. 실무에서는 대개 이렇게 운이 좋지 않습니다.
임계 경로에는 @calc와 @x만 관련되어 있었고, @z는 경로에 아예 언급되지 않았다는 점에 주목하세요. 따라서 이 변경의 목적은 산술 연산의 부담을 줄이는 것입니다. 더 정확히 말하면 로직 레벨(logic levels)의 수를 줄이는 것입니다.
임계 경로는 최적화 알고리듬을 실행한 후의 최악의 경로임을 기억하세요. 이 알고리듬은 문제의 원인을 묻지 않습니다. 그보다는 슬랙이 음수인 경로를 개선하려고 시도합니다. 따라서 문제의 해결책이 @z를 변경하는 것임에도, @z와 관련된 경로는 임계 경로가 아니었습니다. 이것은 단지 우연이지만, 이런 일은 자주 일어납니다.
이 해결책의 임계 경로에 대한 타이밍 리포트도 이 페이지 맨 아래에 있습니다. 로직 레벨 수가 7개에서 6개로 줄었음을 보여 줍니다. 그 결과 데이터 경로 지연이 0.715ns 감소하여 타이밍 제약 조건을 충족하기에 충분했습니다.
이 예에서 배울 수 있는 교훈은 임계 경로가 항상 문제의 직접적인 원인은 아니라는 점입니다. 이 경로가 왜 실패했는지 묻는 것은 여전히 옳지만, 해결책은 다른 곳에 있을 수 있습니다. 각 FPGA 툴에는 문제의 근본 원인을 찾는 데 도움이 되는 정보를 제공하는 유틸리티가 따로 있다는 점을 기억하세요. 이러한 유틸리티를 살펴보고 문서를 읽는 데 시간을 투자할 가치가 있습니다.
나중에 해결하기보다 처음부터 문제를 피하라
로직 설계를 처음부터 올바르게 한다면 타이밍 클로저 작업의 상당 부분을 피할 수 있습니다. 그러려면 로직 설계가 소프트웨어가 아니라는 사실을 항상 인식하고 있어야 합니다. Verilog 코드의 목적은 시뮬레이션 중에 올바른 결과를 만들어 내는 것이 아닙니다. 정말로 중요한 것은 합성기(synthesizer)가 Verilog 코드로부터 만들어 내는 출력입니다.
좋은 로직 설계는 로직이 그 목적을 가장 잘 달성하려면 어떻게 해야 할지 먼저 곰곰이 생각하는 것에서 시작합니다. 여기에는 타이밍과 관련된 잠재적 장애물을 파악하는 일도 포함됩니다.
경험이 적은 FPGA 설계자들은 종종 시행착오를 통해 Verilog 코드를 개발합니다. 시뮬레이션으로 로직이 예상대로 동작하는지 확인하고, 시뮬레이션 출력이 올바를 때까지 점진적으로 수정합니다. 그 결과는 하드웨어에서 사용할 수 없는 로직일 수 있습니다. 타이밍 제약 조건을 달성하려면 Verilog 코드를 완전히 다시 작성해야 할 수도 있습니다.
Verilog 코드가 만들어 내는 조합 경로를 미리 생각해 보는 것이 중요합니다. 각 레지스터를 살펴보고 조합 경로를 끝까지 따라가 보는 것입니다. 조합 경로는 항상 레지스터에서 시작하여 레지스터에서 끝난다는 점을 기억하세요.
다음 예를 살펴보겠습니다.
reg [15:0] a, b;
wire [16:0] x, y;
reg [33:0] z;
assign x = a + 2;
assign y = b + 3;
always @(posedge clk)
z <= x * y;
@a를 봅시다. 이 레지스터가 바뀌면 조합 경로는 첫 단계로서 @x에 도달합니다. 그런데 @x는 레지스터가 아닙니다. @x는 연속 할당(continuous assignment)으로 갱신됩니다. 따라서 경로는 @z까지 계속 이어집니다. 그러므로 이 로직은 이 조합 경로 안에서 덧셈과 곱셈이라는 두 가지 의미 있는 연산을 수행합니다. 이것은 너무 과한가요? 파이프라이닝을 통해 이를 두 클록 사이클로 나눌 필요가 있을까요? 그 답은 클록 주파수와 사용하는 FPGA에 따라 달라집니다.
또 하나 중요한 요소는 조합 경로를 짧게 만드는 것이 얼마나 어려운가입니다. 때로는 긴 조합 경로가 불가피합니다. 하지만 경로의 타이밍을 개선하기 쉬운 상황이라면, 설계에 훨씬 더 나쁜 경로들이 있더라도 그렇게 하세요. 설계에 문제가 있는 경로가 몇 개뿐이라면, 툴은 그 경로들에 노력을 집중하여 타이밍 제약 조건을 달성할 수 있는 경우가 많습니다. 그리고 다른 경로들의 타이밍 요구사항을 충족시키기 쉬우면 큰 도움이 됩니다.
따라서 타이밍 제약 조건을 달성하는 설계를 얻기 위해 무엇이 허용되고 금지되는지에 대한 규칙은 존재하지 않습니다. 이 분야에서 올바른 결정을 내리려면 FPGA 설계 경험이 필요합니다. 특정 FPGA 툴에 대한 경험도 중요합니다. 언제나 맞는 유일한 규칙은 이것입니다. Verilog 코드를 조금만 바꾸면 타이밍을 개선할 수 있다면 그렇게 하세요. 게으르게 굴지 말고 처음부터 그렇게 만드세요. 항상 타이밍을 염두에 두세요.
빠른 로직 작성하기
이미 말했듯이 목표는 레지스터 사이의 조합 경로를 짧게 만드는 것입니다. 레지스터의 다음 값을 계산하는 로직 함수는 단순해야 합니다. 다시 말해, 이러한 로직 함수를 구현하는 데 필요한 로직 레벨 수가 적어야 합니다.
FPGA 설계자로서 우리의 임무는 Verilog 코드를 보고 그 로직 함수가 얼마나 복잡해질지 평가하는 것입니다. 그러려면 합성기(synthesizer)가 Verilog를 LUT 및 기타 로직 프리미티브(primitive)와 같은 로직 요소로 어떻게 변환하는지에 대한 지식이 필요합니다. 이러한 지식은 경험을 통해 얻으며, 그중 일부는 타이밍 리포트를 분석하면서 쌓입니다. 더 어렵게도, 이 변환 과정은 FPGA마다 다릅니다. 따라서 빠른 로직을 만드는 Verilog 코드를 작성하는 것은 쉬운 일이 아닙니다.
FPGA 초보자라면 학습을 위해 구현 결과를 살펴보는 시간을 갖는 것이 좋습니다. 타이밍 리포트는 로직 설계가 단순한 로직 요소들로 어떻게 분해되는지 보여 주는 예입니다. FPGA 툴은 저수준 로직 요소를 볼 수 있는 다른 유틸리티도 제공합니다.
게다가 도움이 되는 몇 가지 간단한 규칙이 있습니다.
- 파이프라이닝(pipelining): 레지스터를 아끼지 마세요. 가능하면 로직의 작업을 작은 조각으로 나누고 각 단계 뒤에 레지스터를 삽입하세요. FPGA에는 플립플롭이 아주 많습니다(보통 각 LUT 옆에 플립플롭이 하나씩 있습니다). 따라서 레지스터를 삽입해도 FPGA의 사용률은 늘어나지 않습니다. 파이프라이닝을 피해야 할 유일한 이유는 설계가 너무 복잡해질 때입니다.
- if-then-else를 사용할 때는 "else" 절을 여러 개 길게 연결하지 마세요. 대신 "case" 문을 쓸 수 있다면 보통 그편이 더 좋습니다. "else" 절은 흔히 그 절 앞에 있는 모든 조건이 거짓임을 보장하는 로직 함수를 필요로 합니다. 따라서 "else" 절이 여러 개이면 로직 레벨이 여러 개 필요할 수 있습니다.
- 불필요한 리셋은 피하세요. 특히 동기 리셋(synchronous reset)은 로직 함수에 약간의 복잡성을 더합니다. 두 종류의 리셋 모두 많은 로직 요소에 도달해야 하는 신호이기 때문에 라우팅을 더 어렵게 만듭니다. 이 주제에 대해서는 별도 연재가 있습니다.
- 거대한 상태 머신(state machine)을 만들지 마세요. 상태 수가 얼마까지 괜찮은지에 대한 엄격한 한계는 없습니다. 하지만 상태 수가 20개를 넘어간다면 설계 구조를 다시 고려해 보아야 합니다. 또한 합성기(synthesizer)가 큰 상태 머신에 대해 one-hot 인코딩을 사용하는지 확인하세요(대부분의 합성기는 기본적으로 그렇게 합니다). 그러면 빠른 로직을 만드는 데 도움이 됩니다.
- RAM 뒤에 레지스터를 하나 더 두는 것이 보통 더 좋습니다. 묵시적으로 만들어진 RAM의 예를 살펴보겠습니다.
reg [7:0] array[0:127]; reg [7:0] val; reg [6:0] addr; always @(posedge clk) val <= array[addr];이 Verilog 코드는 올바릅니다. 하지만 @val은 RAM의 동기 출력이라는 점에 유의하세요. 상승 클록 엣지가 오면 RAM의 동작이 시작되고, 배열에서 값을 가져온 뒤에야 @val이 갱신됩니다. 따라서 @val은 (플립플롭과 비교하여) clock-to-output 지연이 상대적으로 큽니다. 그 결과 @val에서 시작하는 경로는 본질적으로 불리합니다. 레지스터를 하나 더 추가하면 이를 해결할 수 있습니다.
reg [7:0] array[0:127]; reg [7:0] val_d, mem_out; reg [6:0] addr; always @(posedge clk) begin mem_out <= array[addr]; val_d <= mem_out; end참고로 이것은 기능적으로 동등하지 않습니다. 여기서 @mem_out이 RAM의 출력입니다. 이 출력은 한 클록 뒤에야 @val_d로 복사되므로 @val을 정확히 대체하지는 못합니다. 그러나 @val_d는 진짜 레지스터이며 clock-to-output 지연이 작습니다. 많은 FPGA에서 이 추가 레지스터는 블록 RAM(block RAM)의 일부이므로 플립플롭이 낭비되지 않습니다. 플립플롭 낭비를 걱정할 필요가 없다고 말한 적이 있나요?
안타깝게도 이러한 레지스터를 추가하면 설계가 상당히 복잡해지는 경우가 많습니다. 그런 경우라면 레지스터를 추가하지 않는 편이 낫고, 대신 @val에서 시작하는 조합 경로를 짧게 유지하려고 노력하는 것이 좋습니다.
타이밍 리포트 두 개 더 보기
위의 "머리를 쓰는 것에 대안은 없다" 절에서 타이밍 리포트 두 개를 보여 주겠다고 약속했습니다. 리포트가 길고 완전히 관련된 내용은 아니어서 언급된 자리가 아니라 여기에 모아 두었습니다.
이 두 타이밍 리포트는 각각 해당 시나리오의 임계 경로입니다. 따라서 위에 표시된 경로와 시작하고 끝나는 레지스터가 같지는 않습니다.
첫 번째 타이밍 리포트는 첫 번째 Verilog 코드 예와 관련된 것입니다. 위의 타이밍 리포트와 달리, 툴이 전용 산술 유닛을 사용하도록 허용되었습니다. 그 결과 타이밍 제약 조건은 쉽게 달성되었습니다.
이 타이밍 리포트는 Kintex Ultrascale FPGA용으로 생성되었습니다. 이 FPGA 제품군에서 전용 산술 유닛은 DSP48E2라고 불립니다. 경로가 같은 DSP48E2 유닛에서 시작하고 끝난다는 점에 주목하세요. 따라서 로직 지연이 100%입니다.
Slack (MET) : 1.406ns (required time - arrival time) Source: calc_reg/DSP_A_B_DATA_INST/CLK (rising edge-triggered cell DSP_A_B_DATA clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg/DSP_OUTPUT_INST/ALU_OUT[10] (rising edge-triggered cell DSP_OUTPUT clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 2.445ns (logic 2.445ns (100.000%) route 0.000ns (0.000%)) Logic Levels: 4 (DSP_ALU=1 DSP_M_DATA=1 DSP_MULTIPLIER=1 DSP_PREADD_DATA=1) Clock Path Skew: -0.010ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.392ns = ( 7.392 - 4.000 ) Source Clock Delay (SCD): 4.096ns Clock Pessimism Removal (CPR): 0.694ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.264ns (routing 0.756ns, distribution 1.508ns) Clock Net Delay (Destination): 1.964ns (routing 0.696ns, distribution 1.268ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=80, routed) 2.264 4.096 calc_reg/CLK DSP48E2_X11Y34 DSP_A_B_DATA r calc_reg/DSP_A_B_DATA_INST/CLK ------------------------------------------------------------------- ------------------- DSP48E2_X11Y34 DSP_A_B_DATA (Prop_DSP_A_B_DATA_DSP48E2_CLK_A2_DATA[9]) 0.302 4.398 r calc_reg/DSP_A_B_DATA_INST/A2_DATA[9] net (fo=1, routed) 0.000 4.398 calc_reg/DSP_A_B_DATA.A2_DATA<9> DSP48E2_X11Y34 DSP_PREADD_DATA (Prop_DSP_PREADD_DATA_DSP48E2_A2_DATA[9]_A2A1[9]) 0.182 4.580 r calc_reg/DSP_PREADD_DATA_INST/A2A1[9] net (fo=1, routed) 0.000 4.580 calc_reg/DSP_PREADD_DATA.A2A1<9> DSP48E2_X11Y34 DSP_MULTIPLIER (Prop_DSP_MULTIPLIER_DSP48E2_A2A1[9]_U[10]) 0.994 5.574 f calc_reg/DSP_MULTIPLIER_INST/U[10] net (fo=1, routed) 0.000 5.574 calc_reg/DSP_MULTIPLIER.U<10> DSP48E2_X11Y34 DSP_M_DATA (Prop_DSP_M_DATA_DSP48E2_U[10]_U_DATA[10]) 0.164 5.738 r calc_reg/DSP_M_DATA_INST/U_DATA[10] net (fo=1, routed) 0.000 5.738 calc_reg/DSP_M_DATA.U_DATA<10> DSP48E2_X11Y34 DSP_ALU (Prop_DSP_ALU_DSP48E2_U_DATA[10]_ALU_OUT[10]) 0.803 6.541 r calc_reg/DSP_ALU_INST/ALU_OUT[10] net (fo=1, routed) 0.000 6.541 calc_reg/DSP_ALU.ALU_OUT<10> DSP48E2_X11Y34 DSP_OUTPUT r calc_reg/DSP_OUTPUT_INST/ALU_OUT[10] ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=80, routed) 1.964 7.392 calc_reg/CLK DSP48E2_X11Y34 DSP_OUTPUT r calc_reg/DSP_OUTPUT_INST/CLK clock pessimism 0.694 8.086 clock uncertainty -0.035 8.050 DSP48E2_X11Y34 DSP_OUTPUT (Setup_DSP_OUTPUT_DSP48E2_CLK_ALU_OUT[10]) -0.104 7.946 calc_reg/DSP_OUTPUT_INST ------------------------------------------------------------------- required time 7.946 arrival time -6.541 ------------------------------------------------------------------- slack 1.406
두 번째 타이밍 리포트는 두 번째 Verilog 코드 예와 관련된 것입니다. 이 예는 파이프라이닝(pipelining)을 통해 상황이 개선된 경우입니다.
Slack (MET) : 0.433ns (required time - arrival time) Source: y_reg[1]__0/C (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg[23]/D (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 3.465ns (logic 1.653ns (47.706%) route 1.812ns (52.294%)) Logic Levels: 6 (CARRY8=4 LUT4=2) Clock Path Skew: -0.129ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.373ns = ( 7.373 - 4.000 ) Source Clock Delay (SCD): 4.040ns Clock Pessimism Removal (CPR): 0.538ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.208ns (routing 0.756ns, distribution 1.452ns) Clock Net Delay (Destination): 1.945ns (routing 0.696ns, distribution 1.249ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=121, routed) 2.208 4.040 clk_IBUF_BUFG SLICE_X54Y88 FDRE r y_reg[1]__0/C ------------------------------------------------------------------- ------------------- SLICE_X54Y88 FDRE (Prop_EFF2_SLICEL_C_Q) 0.138 4.178 r y_reg[1]__0/Q net (fo=25, routed) 0.505 4.683 y[1] SLICE_X53Y91 LUT4 (Prop_B6LUT_SLICEM_I0_O) 0.150 4.833 r calc[7]_i_28/O net (fo=1, routed) 0.344 5.177 calc[7]_i_28_n_0 SLICE_X53Y89 CARRY8 (Prop_CARRY8_SLICEM_DI[2]_CO[7]) 0.424 5.601 r calc_reg[7]_i_9/CO[7] net (fo=1, routed) 0.043 5.644 calc_reg[7]_i_9_n_0 SLICE_X53Y90 CARRY8 (Prop_CARRY8_SLICEM_CI_O[0]) 0.122 5.766 r calc_reg[23]_i_30/O[0] net (fo=3, routed) 0.402 6.168 calc_reg[23]_i_30_n_15 SLICE_X51Y88 LUT4 (Prop_C5LUT_SLICEL_I0_O) 0.169 6.337 r calc[15]_i_8/O net (fo=1, routed) 0.437 6.774 calc[15]_i_8_n_0 SLICE_X51Y92 CARRY8 (Prop_CARRY8_SLICEL_DI[1]_CO[7]) 0.422 7.196 r calc_reg[15]_i_1/CO[7] net (fo=1, routed) 0.030 7.226 calc_reg[15]_i_1_n_0 SLICE_X51Y93 CARRY8 (Prop_CARRY8_SLICEL_CI_O[7]) 0.228 7.454 r calc_reg[23]_i_1/O[7] net (fo=1, routed) 0.051 7.505 calc_reg[23]_i_1_n_8 SLICE_X51Y93 FDRE r calc_reg[23]/D ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=121, routed) 1.945 7.373 clk_IBUF_BUFG SLICE_X51Y93 FDRE r calc_reg[23]/C clock pessimism 0.538 7.910 clock uncertainty -0.035 7.875 SLICE_X51Y93 FDRE (Setup_HFF_SLICEL_C_D) 0.063 7.938 calc_reg[23] ------------------------------------------------------------------- required time 7.938 arrival time -7.505 ------------------------------------------------------------------- slack 0.433
차이는 전용 산술 유닛을 사용했을 때만큼 극적이지는 않습니다. 그렇지만 타이밍 제약 조건을 달성할 만큼 충분합니다.
이상으로 타이밍 클로저에 대한 일반적인 논의를 마칩니다. 다음 페이지에서는 이 주제에 대한 몇 가지 실용적인 전략을 제안합니다.