01signal.com

FPGA에서의 비동기 리셋: 많은 사람이 믿는 것처럼 결코 쉽지 않다

이 페이지는 FPGA의 리셋에 관한 시리즈 중 첫 번째 글입니다. 많은 사람이 비동기 리셋(asynchronous reset)이 실제로는 자신이 기대하는 대로 동작하지 않는다는 것을 모른 채 사용하기 때문에, 이 첫 페이지에서는 이 주제가 보기보다 쉽지 않다는 점을 설명합니다.

여러분의 리셋은 정말 동작하고 있나요?

상태 머신(state machine)을 리셋하는 다음 예제를 살펴보겠습니다. 이 예제는 잘못된 예제입니다.

   always @(posedge clk or negedge resetn)
     if (!resetn)
       state <= ST_START;
     else
       case (state)
	 ST_START:
	   begin
	      state <= ST_NEXT;
	      [  ... do some stuff maybe? ... ]
	   end

	 ST_NEXT:
	   begin
	      [ ... do something ... ]
	   end
       endcase

그럼 여기서 리셋의 무엇이 잘못된 것인지 궁금할 것입니다. 이건 교과서에 나오는 방식과 정확히 같지 않은가요? 액티브 로우(active low) 비동기 리셋(asynchronous reset)으로, @state를 구현하는 레지스터를 초기 상태로 만드는 것입니다. 무엇이 잘못될 수 있을까요?

논의를 위해 @resetn 신호 자체는 문제가 없다고 가정하겠습니다. 다시 말해, 이 신호는 푸시버튼 같은 것에 직접 연결되어 있지 않습니다. 어쩌면 리셋 신호를 생성하도록 설계된 칩을 사용했을 수도 있고, 리셋이 FPGA 내부에서 생성되었을 수도 있습니다. 어찌 되었든, 리셋이 안정적으로 활성화되고, 충분히 오랫동안 활성 상태를 유지한 다음, 비활성화된다고 가정하겠습니다. 그래도 여전히 잘못되었습니다.

제가 ‘잘못되었다’고 말하는 것은, FPGA가 아무런 이유도 없이 이따금 이상하게 동작하게 만드는 그런 종류의 잘못을 의미합니다.

그렇다면 문제가 무엇일까요? 리셋이 충분히 오랫동안 활성 상태이므로, @state는 분명히 초기 상태로 들어갈 것입니다. 그런데 리셋이 비활성화될 때(즉, 위 예제에서 다시 ‘1’로 돌아갈 때)는 어떤 일이 벌어질까요? 바로 그때 관련 플립플롭들이 @clk의 상승 에지에 응답하기 시작해야 합니다.

하지만 플립플롭이 리셋 신호에서 회복되어 클록 상승 에지에서 데이터 입력을 샘플링(sampling)하기 시작하려면 약간의 시간이 필요합니다. 그리고 리셋은 정의상 비동기(asynchronous)이므로, @clk에 대해 어떤 시점에든 비활성화될 수 있습니다.

만약 @clk의 첫 번째 상승 에지가 리셋 비활성화 직후 너무 일찍 도달한다면, 플립플롭은 이 상승 에지를 무시합니다. @clk에 연결된 모든 플립플롭이 그렇게 동작한다면 아무 문제가 없습니다. 하지만 모든 플립플롭이 완전히 똑같지는 않습니다. 어떤 플립플롭은 클록 에지를 다른 플립플롭보다 조금 일찍 받기도 하고, 어떤 플립플롭은 리셋 비활성화를 다른 플립플롭보다 늦게 받기도 합니다.

솔직히 말하면, 이 설명은 다소 단순한 면이 있습니다. 문제를 더 정확하게 이해하려면 타이밍의 기초를 설명하는 페이지에서 Recovery와 Removal에 관한 부분을 참조하시기 바랍니다.

결국 핵심은 이렇게 요약됩니다. 운이 나쁘면 리셋이 클록 상승 에지에 충분히 가까운 시점에 비활성화되어, 일부 플립플롭은 첫 번째 상승 에지에 반응하고 다른 플립플롭은 그 에지를 무시할 수 있습니다. 사실 어떤 플립플롭은 무엇을 해야 할지 결정하는 데 추가 시간이 걸리기도 합니다. 같은 칩 안의 플립플롭 간 차이를 탓하거나 클록 스큐(clock skew)와 리셋 스큐를 탓할 수 있습니다. 핵심은 일부 플립플롭이 다른 플립플롭보다 한 클록 사이클만큼 앞서게 된다는 것입니다.

이 문제가 얼마나 심각한지 이해하려면 위의 예제를 다시 보십시오. 합성 도구(synthesizer)가 이것을 상태 머신(state machine)으로 인식한다면, 상태 변수를 원-핫 인코딩(one-hot encoding)으로 구현할 가능성이 높습니다. 다시 말해, 각 상태에 대해 단일 비트 레지스터를 할당합니다. 상태 머신이 해당 상태에 있을 때 각 레지스터가 활성화됩니다. 합성 도구가 ST_START라는 상태를 위해 hot_state_0이라는 레지스터를, ST_NEXT를 위해 hot_state_1이라는 레지스터를 할당했다고 가정해 보겠습니다. 분명히 리셋은 hot_state_0을 활성화하고 hot_state_1을 비활성화합니다.

이제 상태 머신이 ST_START에서 ST_NEXT로 무조건 이동한다는 점에 주목하십시오. 따라서 리셋이 해제된 후 첫 번째 클록에서 hot_state_0은 비활성화되고 hot_state_1은 활성화됩니다.

그런데 리셋이 운 나쁜 타이밍에 비활성화되어, 일부 플립플롭은 첫 번째 클록 에지를 놓치고 다른 플립플롭은 놓치지 않는다면 어떻게 될까요? 한 가지 가능성은 hot_state_0이 첫 번째 클록을 놓치지만 hot_state_1은 그 클록에 반응하는 것입니다. 그 결과 둘 다 활성화되며, 이것은 원-핫 인코딩에서 허용되지 않는 조건입니다. 반대의 경우에는 두 레지스터가 모두 비활성화되어, 사실상 상태 머신의 모든 원-핫 레지스터가 비활성화됩니다. 어느 쪽이든 상태 머신은 정상 상태로 회복하지 못할 수 있습니다.

실제로는 어떤 모습으로 나타날까요? 물론 응용 분야에 따라 다르겠지만, FPGA에 다시 리셋을 인가할 때까지 어떤 것이 제대로 동작하지 않을 가능성이 높습니다. 이 문제는 무작위로, 그리고 그다지 자주 발생하지 않을 수도 있기 때문에 원인을 찾기가 극도로 어려울 수 있습니다. 또한 FPGA 설계를 컴파일할 때마다 다르게 동작할 가능성이 있고, 보드가 다르면 또 다르게 나타날 수도 있습니다. 한마디로, 사람을 미치게 만드는 종류의 버그입니다. 여기서는 문제의 원인이 바로 논의 주제이기 때문에 그렇게 심각하게 들리지 않을 수도 있습니다. 하지만 실제 현장에서 이런 불안정성이 발생하면 증상은 무엇이든 될 수 있으며, FPGA가 귀신에 씐 것처럼 느껴지는 경우도 자주 있습니다.

그런데 저는 항상 이렇게 하는데, 잘 동작합니다!

맞습니다. 대부분의 경우, 일부 플립플롭이 리셋 후 첫 번째 클록을 놓친다 해도 그렇게 큰 문제가 되지는 않습니다.

위의 상태 머신(state machine) 예제가 실패할 수 있는 주된 이유는 첫 번째 클록 사이클에서 초기 상태를 벗어나기 때문입니다. 실제 설계에서 대부분의 상태 머신은 초기 상태에서 벗어나는 규칙을 갖고 있어서, 처음 몇 클록 사이클 동안은 항상 초기 상태에 머무릅니다. 그래서 이런 실수를 저질러도 별 탈이 없는 것입니다.

하지만 또 다른 예제가 있습니다. 간단한 카운터입니다.

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter + 1;

이 경우 @counter는 16개의 플립플롭으로 구성됩니다. 각 플립플롭은 데이터 입력에서 다음 클록 사이클의 카운터 값을 받고, 비동기 리셋(asynchronous reset) 입력에서는 @resetn을 받습니다.

@resetn이 활성화되어 있으면 @counter는 0 값을 얻고, 다음 클록 사이클의 카운터 값은 1이 됩니다. 따라서 counter[0]을 제외한 모든 플립플롭은 첫 번째 클록 에지를 놓치든 놓치지 않든 0으로 유지됩니다. 어느 쪽이든 카운터는 올바르게 카운팅을 시작합니다. 이런 코드를 작성하는 대부분의 경우, 카운터가 첫 번째 클록 사이클을 놓쳤는지 여부는 중요하지 않습니다.

하지만 다음은 상황이 다릅니다.

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter - 1;

작은 차이지만 매우 중요한 차이입니다. 카운터가 0에서 시작하여 감소 방향으로 카운팅한다면, 다음 클록 사이클의 카운터 값은 0xffff가 됩니다. 다시 말해, 모든 플립플롭이 리셋 후 첫 번째 클록에서 값을 변경해야 합니다. 따라서 일부 플립플롭이 리셋 후 첫 번째 클록 에지에 반응하고 다른 일부가 반응하지 않는다면, 카운터는 사실상 임의의 값에서 시작할 수 있습니다.

하지만 누가 카운터를 0으로 리셋한 다음에 감소 방향으로 셀까요?

그렇다면 좀 더 현실적인 예를 들어 보겠습니다. 클록 인에이블(clock enable) 신호를 사용하여, 로직이 마치 클록 주파수가 절반으로 줄어든 것처럼 동작하게 만드는 경우입니다(따라서 필요하다면 멀티사이클 경로(multi-cycle path)를 허용하게 됩니다).

   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= !en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

여기서는 아무 문제가 없을 거라고 생각할 수 있습니다. 클록 인에이블(clock enable) 신호인 @en은 단일 레지스터이므로, 언제 토글되기 시작하는지는 별로 중요하지 않지 않을까요? 문제는 클록 인에이블 신호는 팬아웃(fan-out)이 높은 경향이 있다는 점입니다. 그래서 합성 도구(synthesizer)가 팬아웃 한도를 초과하지 않도록 이 신호를 복제할 수도 있습니다.

제가 Vivado 합성 도구로 비공식적으로 수행한 실험에서는, @en을 구현하기 위해 복제된 각 레지스터가 자신의 출력 신호에 의존했습니다. 다시 말해, 모든 플립플롭이 다음 출력을 결정하는 데 사용하는 단일 신호가 존재하지 않았습니다. 대신 클록의 상승 에지에서 항상 값을 변경하는 독립적인 플립플롭이 여럿 있었습니다. 따라서 이 플립플롭들이 같은 클록 사이클에 토글을 시작하지 않으면, 그 출력들은 무한정 서로 다른 값을 유지합니다.

이런 사고가 발생하면 로직은 완전히 오작동할 가능성이 높습니다. 그러므로 굳이 비동기 리셋(asynchronous reset)을 고집하려면, 적어도 모든 클록 인에이블(clock enable)이 하나의 소스에 의존하도록 만드십시오. 예를 들어 다음과 같이 말입니다.

   reg pre_en; // Apply some don't-touch synthesis directive on this
   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       pre_en <= 0;
     else
       pre_en <= !pre_en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= pre_en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

비결은 @pre_en이 다음 값을 결정하는 레지스터라는 점입니다. 이 플립플롭은 팬아웃(fan-out)이 낮고, 합성 도구(synthesizer)가 건드리지 못하게 하는 어떤 속성(attribute)이 있을 수도 있습니다. 이렇게 하면 확실히 단일 레지스터가 됩니다. @en을 구현하는 모든 플립플롭은 @pre_en에 의존하므로, 다음 클록에서 @en의 값에 대해 모두 일치합니다. 리셋 후 첫 번째 클록의 경우에는 일부 플립플롭이 그 클록을 놓치더라도 상관없습니다. 어차피 첫 번째 클록 사이클에서 @en의 값은 0이기 때문입니다.

결론적으로, 비동기 리셋(asynchronous reset)을 잘못 사용해도 대개는 아무 문제가 없습니다. 주된 이유는 로직이 클록에 대한 리셋 비활성화 시점의 불확실성에 관대한 경우가 많기 때문입니다. 그렇다고 비동기 리셋을 부주의하게 적용하면, 해결하기 매우 어려운 간헐적 오동작이 발생할 수 있습니다.

리셋과 클록 사이에 타이밍 제약 사용하기

리셋 비활성화와 클록 상승 에지 사이의 불확실한 타이밍 관계를 피하는, 겉보기에 당연한 방법은 리셋 신호에 타이밍 제약(timing constraint)을 적용하는 것입니다. 그러나 그렇게 하면 리셋 신호는 동기(synchronous) 신호가 됩니다.

그런데 리셋 신호는 어느 클록과 동기(synchronous)일까요? 전체 로직 설계에 전역 비동기 리셋(asynchronous reset) 하나가 있으면 편리한 경우가 많습니다. 이 리셋은 특정 클록과 동기화된 로직으로 생성됩니다. 만약 이 리셋을 다른 클록과 동기화된 로직에 사용한다면, 클록 도메인 크로싱(clock domain crossing)이 됩니다. 이것은 그 자체로 하나의 주제이지만, 가장 중요한 점은 도구들이 관련 경로(path)에 대한 타이밍을 무시할 가능성이 있다는 것입니다.

따라서 비동기 리셋에 타이밍 제약(timing constraint)을 사용하기 위한 출발점은, 클록마다 별도의 비동기 리셋(asynchronous reset)이 있어야 한다는 것입니다. 또는 굳이 말하자면, 연관 클록(related clocks) 그룹마다 별도의 비동기 리셋이 있어야 합니다. 그렇지 않으면 타이밍 제약은 의미가 없습니다. 이것이 이상하게 들린다면, 그 이유는 그 비동기 리셋이 더 이상 비동기가 아니기 때문입니다.

Verilog 코드가 비동기 리셋(asynchronous reset) 패턴을 사용한다는 사실은 아무런 차이를 만들지 않습니다. 플립플롭의 비동기 리셋 입력이 사용되는지, 또는 플립플롭이 자신의 리셋 입력을 비동기(asynchronous)로 간주하도록 구성되었는지도 중요하지 않습니다. 리셋이 어떤 클록과 동기(synchronous)이고 타이밍 제약(timing constraint)이 사용된다면, 그 리셋은 실질적으로 동기(synchronous)입니다. 이런 경우에는 Verilog 패턴을 직접 사용하는 방법을 고려해 볼 수 있습니다.

   always @(posedge clk)
     if (!resetn)
       state <= ST_START;
[ ... ]

그렇긴 하지만, Intel의 타이밍 클로저(timing closure) YouTube 동영상은 클록에 동기화된 비동기 리셋(asynchronous reset)을 사용하고, 타이밍 제약(timing constraint)도 함께 적용할 것을 권장합니다. 비동기 리셋을 선택하는 이유는 FPGA의 전역 라우팅(global routing)을 위한 전용 리소스를 활용하기 위해서입니다. 저는 이것이 상당히 이상하다고 생각합니다. 전역 라우팅조차도 큰 지연을 가질 수 있고, 특히 대형 FPGA에서는 더욱 그렇습니다. 하지만 분명히 이런 방식이 타당한 시나리오도 있을 것입니다.

사실, 비동기 리셋이 사실상 동기(synchronous)인 경우에도 비동기 리셋(asynchronous reset)을 계속 사용해야 하는 또 다른 이유가 있습니다. 이 내용은 다음 페이지에서 다룹니다. 바로 리셋 신호의 활성화를 비동기적으로 전파할 수 있게 해주기 때문이며, 이는 시뮬레이션뿐만 아니라 ASIC 테스트에서도 유용합니다.

동기화된 비동기 리셋(asynchronous reset)을 사용하는 방법을 쓴다면, 타이밍 제약(timing constraint)이 플립플롭의 비동기 리셋 입력으로 들어가는 모든 신호 경로(path)에 적용되도록 하는 것이 중요합니다. 리셋이 목적지 플립플롭과 동일한 클록에 동기화된 플립플롭에서 생성된다는 사실만으로는, 그 사이의 경로(path)가 타이밍 분석 대상이 된다는 것이 보장되지 않습니다. 일부 타이밍 도구는 기본적으로 비동기 입력으로 끝나는 모든 경로를 무시하므로, 이런 강제 적용을 명시적으로 활성화해야 할 수 있습니다. 타이밍 리포트(timing report)에서 이 경로들이 실제로 타이밍 제약(timing constraint)에 포함되는지 꼭 확인하십시오. 여기서 실수하기 쉽습니다.

이것으로 리셋에 관한 이 시리즈의 첫 페이지를 마칩니다. 다음 페이지에서는 리셋의 다양한 선택지와 FPGA 초기화에 대해 다룹니다.

이 페이지는 영어 원문을 기계 번역한 것입니다. 의문이 드는 부분이 있으면 원문을 참조하시기 바랍니다.
Copyright © 2021-2026. All rights reserved. (dcc38493)