이 페이지는 FPGA의 리셋에 관한 시리즈 중 두 번째 글입니다. 비동기 리셋(asynchronous reset)이 많은 사람이 생각하는 것과 다르게 동작하는 이유를 이전 페이지에서 설명한 데 이어, 이 페이지에서는 리셋의 다양한 선택지와 FPGA 초기화에 대해 다룹니다.
우선 가장 먼저: 리셋이란 무엇인가?
아니, 우리 모두 리셋이 뭔지 알지 않나요? 컴퓨터에 있는 버튼처럼 누르면 모든 것이 처음부터 다시 시작되는 그런 것입니다. 어떤 일이 있었든 관계없이 지금부터는 모든 것이 제대로 동작하도록 보장해 주는, 칩으로 들어가는 신호입니다. 어떤 사람들은 리셋을 시스템을 알려진 상태로 만드는 신호라고 부르기도 합니다.
FPGA 설계자에게 리셋은 흔히 모든 모듈에 추가하고 반복적인 코드 패턴으로 사용하는 그저 하나의 여분 입력에 불과합니다. 반복되는 패턴이니까 그냥 사용할 뿐, 이 리셋 신호가 실제로 무엇을 보장하는지, 보장한다면 정말 그런지, 아니면 아예 생략할 수 있는지에 대해서는 별로 신경 쓰지 않습니다.
가장 흔한 오해는 리셋 신호가 시스템을 알려진 상태로 만든다는 점에만 주목하는 것입니다. 물론 그것은 사실이지만, 더 중요한 부분은 리셋이 비활성화된 뒤에 일어나는 일입니다. 로직이 예측 가능한 방식으로 동작을 시작하도록 보장되어야 합니다. 적어도 제대로 동작한다고 확신할 수 있을 만큼은 예측 가능해야 합니다. 리셋을 비활성화한 후의 동작을 확신할 수 없다면 리셋을 하는 것은 무의미합니다.
이 주제를 특히 어렵게 만드는 것은 운이 작용한다는 점입니다. 일반적으로 리셋이 활성화될 당시 시스템의 상태는 알 수 없고 무작위이며, 리셋의 비활성화 시점도 마찬가지입니다. 따라서 리셋 신호를 잘못 처리하면 무작위한 순간에 나타나는 드문 오작동이 발생할 수 있는데, 이는 전혀 다른 종류의 문제처럼 보일 수도 있습니다. 마찬가지로 이 문제를 무시해도 눈에 띄는 결과가 없을 수 있지만, 가끔 발생하는 문제는 대개 주술로 다루곤 합니다.
적절한 타이밍 제약(timing constraints), 클록과 클록 도메인(clock domain)의 올바른 처리와 마찬가지로, FPGA의 웨이크업과 리셋을 제대로 처리하는 것도 FPGA가 안정적으로 동작하도록 보장하는 데 필수적입니다. 이러한 주제들에 공통된 점은, 무시하고 넘어가도 그럭저럭 넘어갈 수 있고 실제로 많은 엔지니어가 그렇게 한다는 것입니다. 다만 그 대가로 FPGA가 이따금씩 귀신에 씐 것처럼 동작한다는 것이 문제입니다.
시뮬레이션과 하드웨어
이 페이지는 리셋에 대한 결정이 FPGA에 로딩된 설계에 어떤 영향을 미치는지에 초점을 맞춥니다. 하지만 당연하게도 이러한 결정은 로직의 시뮬레이션에도 영향을 줍니다.
이 두 상황을 구분하는 것이 중요합니다. 시뮬레이터는 특히 동작 수준 시뮬레이션(behavioral simulation)에서 모든 레지스터에 X(unknown, 미지) 초기 값을 할당합니다. 그러면 이 X 값은 자기 로직 함수에서 X 값에 의존하는 모든 레지스터로 전파됩니다. 그 결과 레지스터 하나만 X 값을 가져도 설계 전체가 X로 가득 차서 시뮬레이션이 쓸모없게 되는 일이 발생합니다.
이 문제에 대한 흔한 잘못된 해결책은 모든 X를 없애기 위해 설계의 모든 레지스터에 비동기 리셋(asynchronous reset)을 넣는 것입니다. 보통 시뮬레이션 초반에 리셋을 잠깐 활성화하는 방식으로 이루어집니다. 그러면 모든 레지스터가 알려진 값을 얻게 되어 모든 것이 완벽해 보입니다. 안타깝게도 이러한 잘못된 해결책은 이전 페이지에서 논의한 문제들을 숨김으로써 깨끗하게 시작하는 듯한 착각을 불러일으키는 경우가 많습니다.
리셋을 올바르게 사용하더라도, 시뮬레이션에서 X 값의 출처를 추적하는 수고를 피하려고 모든 레지스터를 리셋하는 것은 여전히 게으른 선택입니다. 이는 단지 리소스를 낭비하고 타이밍 제약(timing constraints) 달성을 어렵게 만들 뿐만 아니라, 불필요한 레지스터 리셋은 버그를 숨길 수도 있습니다. X 값의 홍수는 레지스터 사이의 의도하지 않은 의존성에서 비롯될 수 있기 때문입니다. 따라서 X 값이 넘쳐나는 것은 설계에 무언가 문제가 있다는 경고 신호가 될 수 있습니다.
시뮬레이션과 비교하면 하드웨어는 리셋을 사용하지 않는 것에 대해 훨씬 관대합니다. 그러나 리셋을 잘못 사용하거나, 필요한데 전혀 사용하지 않으면 하드웨어는 상당히 예상 밖으로 동작할 수 있습니다.
결론적으로, 리셋은 하드웨어를 염두에 두고 사용해야 합니다. 시뮬레이션 중에 성가신 X를 없애기 위한 용도가 아니라는 뜻입니다. 초점은 하드웨어에 맞춰져야 하므로, 시뮬레이션에 관해 제가 할 말은 이것이 전부입니다.
리셋 적용 전략
레지스터에 리셋을 적용할지, 어떻게 적용할지를 결정할 때는 각 레지스터의 상황을 개별적으로 고려해야 합니다. 이렇게 하면 리셋 신호의 불필요하게 높은 팬아웃(fan-out)을 피할 수 있을 뿐 아니라, 이전 상태가 어떠했든 리셋 이후 로직이 올바르게 시작하도록 보장할 수 있는지 점검하는 좋은 기회가 됩니다.
원칙적으로 네 가지 선택지가 있습니다:
- 레지스터의 초기 값이 미지(unknown)라는 사실을 받아들인다.
- FPGA 동기 소자의 초기 값 설정에 의존한다. 즉, 명시적인 리셋이 없다.
- 비동기 리셋(asynchronous reset)을 사용한다. 예를 들어:
always @(posedge clk or negedge resetn) if (!resetn) counter <= 0; else counter <= counter + 1; - 동기 리셋(synchronous reset)을 사용한다. 예를 들어:
always @(posedge clk) if (!resetn) counter <= 0; else counter <= counter + 1;
저의 견해
각 선택지에 대해서는 아래에서 다룹니다. 먼저 제가 올바르다고 생각하는 방식을 제시하고, 그다음 자세히 설명하겠습니다.
- 설계 안의 각 레지스터(및 리셋 입력이 있는 다른 로직 요소)를 개별적으로 검토하여, 처음 두 가지 선택지가 타당한지 확인하십시오. 다시 말해, 리셋을 피할 수 있는지 검토하라는 것입니다.
- 그렇지 않다면 동기 리셋(synchronous reset)을 우선적으로 사용하십시오.
- 비동기 리셋(asynchronous reset)은 명시적으로 이를 요구하는 로직 요소(특히 복잡한 FPGA 프리미티브(primitive)와 IP 코어(IP core), 예를 들어 트랜시버나 버스 컨트롤러)에만 사용하고, 그런 경우에도 가능하면 동기 리셋(synchronous reset) 신호를 사용하도록 하십시오.
- 상태 머신(state machine)은 항상 제대로 리셋하십시오. 동작상의 정의가 언젠가는 알려진 상태에 도달하게 만든다고 해도, 명시적이고 안전한 리셋이 상태 머신을 알려진 상태로 만든다는 것을 확인해야 합니다. 합성 도구(synthesizer)가 최적화(특히 원-핫(one-hot) 인코딩) 때문에 상태 변수를 예상과 다른 방식으로 구현할 수 있기 때문입니다. 이렇게 구현된 상태 변수 표현은 상태 머신이 명시적으로 리셋되지 않으면 정상 상태로 수렴하지 못할 수 있습니다.
- 리셋 입력이 있는 IP 코어(IP core), 설계 블록, 프리미티브(primitive)는 문서가 요구한다면 항상 리셋하십시오(그리고 어떤 경우든 리셋하는 것이 좋습니다). 리셋 없이도 잘 동작할 수 있지만, 그것은 순전히 운일 수 있습니다. 리셋이 사용하지 않는 기능과 관련되어 있거나, 단지 설계에 필요 없는 몇몇 출력을 0으로 만드는 것처럼 보여도 규칙은 간단합니다. 리셋에 관해서는 문서를 주의 깊게 따르십시오. 그것이 필요한 이유가 명확해 보이지 않더라도 말입니다.
이러한 제안들은 요즘 FPGA 벤더들이 권장하는 방식과 대체로 일치합니다.
매우 일반적인 지침 하나를 더 추가하겠습니다. 제어 로직은 항상 명시적으로 리셋하고, 데이터 경로(data path)는 초기의 쓰레기 데이터가 자연스럽게 흘러가도록 두십시오.
이제 각 선택지에 대해 자세히 이야기하겠습니다.
선택지 #1: 미지의 초기 값
어떤 레지스터는 리셋도 초기 값도 필요하지 않습니다. 특히 시프트 레지스터(shift register)나 그와 유사한 로직 요소가 여기에 해당합니다. 일반적으로 데이터 경로(data path)는 이 선택지에 잘 맞습니다.
이 코드 조각을 살펴보겠습니다:
reg [31:0] d0, d1, d2, d3, d4;
always @(posedge clk)
begin
d4 <= d3;
d3 <= d2;
d2 <= d1;
d1 <= d0;
d0 <= orig_data;
end
이것은 분명히 32비트짜리 지연 레지스터 다섯 개입니다. 일부 FPGA(특히 Xilinx)에서는 마지막 값(@d4)만 사용되고 이 레지스터들에 리셋이 없으면, 합성 도구(synthesizer)가 이것을 시프트 레지스터(shift register)로 인식합니다. 이렇게 하면 로직 소모를 상당히 줄일 수 있습니다.
이 지연 레지스터들의 출력에 연결된 로직은 초기 상태의 임의의 데이터를 어느 정도 견딜 수 있어야 합니다. 문제가 되지 않는 대표적인 경우는, 다른 어떤 레지스터나 상태 머신(state machine)이 제대로 리셋되어 초기화되지 않은 값들이 도착해도 무시하도록 만들어 주는 상황입니다. 예를 들어 이 지연 라인들이 파이프라인(pipeline)과 관련되어 있다면, 파이프라인의 제어 로직이 무효한 데이터를 자연스럽게 무시할 것입니다.
좀 더 일반적으로 말하면, 유효성을 나타내는 플래그나 상태가 함께 따라다니는 경우에는 레지스터에 리셋도 초기화도 하지 않아도 될 기회를 쉽게 알아볼 수 있습니다. 또는 레지스터에 값을 먼저 할당한 다음에 그 값을 소비하는 명확한 순서가 있는 경우도 마찬가지입니다. 요컨대, 레지스터에 올바른 값이 할당되기 전까지는 그 값이 무시된다는 것을 쉽게 알 수 있을 때입니다.
미지의 초기 값과 관련된 또 다른 유형은 합성 도구(synthesizer)에 따라 달라질 수 있는 경우입니다. 예를 들어:
reg val;
always @(posedge clk)
val <= 1;
합성 도구는 @val을 상수 1을 갖는 와이어(wire)로 결정할 수도 있습니다. 또 다른 가능성으로는 초기 값을 0으로 갖는 레지스터를 할당해서 첫 번째 클록에서 1로 바뀌게 하는 것입니다. 실제로 어떤 일이 벌어질지는 합성 도구에 달려 있습니다. 따라서 @val의 값은 첫 번째 클록 사이클만 제외하면 항상 알려져 있지만, 초기 값은 미지(unknown)로 간주해야 합니다.
선택지 #2: FPGA의 초기 값
FPGA의 기본 동기 소자(일반적으로 플립플롭, 시프트 레지스터(shift register), 메모리)의 초기 값은 설정용 비트스트림(bitstream)에 포함됩니다. 이 기능의 잘 알려진 용도는 블록 RAM에 초기 값을 넣고 쓰기를 전혀 하지 않음으로써 ROM을 만드는 것입니다.
이 기능의 또 다른 잘 알려진 측면은 FPGA가 설정(configuration) 후 깨어날 때 일반적으로 모든 레지스터가 0 값을 갖는 것처럼 보인다는 것입니다. 합성 도구(synthesizer)가 보통 모든 레지스터의 초기 값을 0으로 지정하기 때문입니다. 하지만 초기 값을 명시적으로 설정하지 않으면 예상 밖의 일이 생길 수 있습니다.
일부 동기 소자, 특히 시프트 레지스터와 전용 RAM 블록은 Verilog나 VHDL로 동작을 기술(즉, 추론(inference))하여 만들 수 있습니다. 합성 도구는 코드가 지연 라인처럼 보이면 보통 시프트 레지스터를 만들고, 배열을 보면 RAM 로직 요소를 만듭니다. 하지만 이러한 레지스터에 리셋(동기 리셋(synchronous reset)이든 비동기 리셋(asynchronous reset)이든)을 사용하면, 합성 도구는 이런 방식으로 로직 리소스를 활용하지 못합니다. 시프트 레지스터나 RAM 모두 내부 메모리의 값을 설정하는 리셋 입력이 없기 때문입니다.
그렇다면 초기 값은 어떻게 설정될까요? 설정(configuration) 과정 중, FPGA가 막 활성 상태가 되기 직전, 즉 동기 소자가 클록과 비동기 리셋(asynchronous reset) 입력에 반응하기 시작하기 직전에 모든 동기 소자에는 초기 값이 주어집니다. Xilinx 디바이스에서는 GSR(Global Set Reset) 신호를 통해 이 작업이 수행되는데, GSR이 모든 동기 소자를 초기 상태로 만듭니다. 그 후 GWE(Global Write Enable)가 활성화되어 동기 소자가 정상적으로 동작하게 됩니다.
설정(configuration) 과정은 FPGA의 애플리케이션 로직이 사용하는 어떤 클록과도 무관하게 수행되므로, FPGA가 동작 상태로 전환되는 시점은 이 클록들 중 어느 것과도 비동기(asynchronous)입니다. 따라서 동기 소자들은 마치 어떤 클록과도 무관하게 비동기 리셋(asynchronous reset)이 비활성화된 것과 똑같이 동작합니다. 다시 말해, 일부 동기 소자는 동작 상태 전환 후 도착하는 첫 번째 클록 에지에 응답하고, 다른 동기 소자는 타이밍 위반으로 인해 이 클록 에지에 응답하지 않을 수 있습니다. 이 시리즈의 첫 페이지에서 논의했듯이, 이로 인해 골치 아픈 버그가 발생할 수 있습니다.
FPGA가 깨어날 때 클록이 반드시 안정적이라는 보장은 없다는 점을 유의해야 합니다. 클록이 FPGA 자체의 PLL에서 생성된다면 타이밍 제약(timing constraints)을 크게 위반할 수 있습니다. 위에서 논의한 것처럼, 클록이 안정될 때까지 무시되거나(예를 들어 클록 인에이블(clock enable)을 통해) FPGA가 깨어날 때 이미 안정적이라고 알려져 있다면 문제가 되지 않습니다. 또한 설계상 클록이 안정될 때까지 어떤 동기 소자도 값을 변경할 이유가 없다면 괜찮습니다. 그렇지 않다면 초기 값을 설정해도 그다지 많은 것을 보장하지 못합니다.
초기 값 설정에는 한계가 있지만, 많은 시나리오에서는 충분하며 명시적인 리셋이 필요하지 않습니다. 그리고 어떤 경우에는 리셋 신호를 쓸 수 없기 때문에 선택의 여지가 아예 없기도 합니다. 예를 들어 FPGA가 깨어난 직후 리셋 신호를 생성하는 로직이 그렇습니다. 이러한 로직의 예는 이 시리즈의 세 번째 페이지에 나와 있습니다.
대부분의 합성 도구에서 레지스터의 초기 값을 설정하는 것은 매우 간단합니다. Verilog의 'initial'을 사용하면 됩니다.
reg [15:0] counter;
initial counter = 1000;
합성용 Verilog 코드에 'initial'을 사용할 수 있다는 사실은 의외일 수 있지만, 실제로 이 용법은 폭넓게 지원됩니다. 따라서 합성 도구가 이것을 지원한다면(즉, 문서에 이 'initial' 용법이 명시적으로 언급되어 있다면) 이 키워드가 분명히 선호되는 방법입니다. 게다가 대안적인 방법은 벤더 전용인 경우가 많고, 때로는 특정 FPGA 패밀리 전용이기도 합니다. 따라서 'initial'이 항상 이식성이 뛰어난 것은 아니지만, 아마도 여전히 가장 이식성이 좋은 선택일 것입니다.
초기 값을 설정하는 대안적인 방법은 사용하는 FPGA에 따라 다릅니다. 이 방법은 보통 동기 소자를 프리미티브(primitive)로 인스턴스화(instantiation)하고 초기 값을 인스턴스화 매개변수로 할당하는 방식으로 이루어집니다. 예를 들어 Xilinx의 플립플롭은 다음과 같습니다:
FDCE myflipflop (
.C(clk),
.D(in),
.Q(out),
.CLR(1'b0),
.CE(1'b1)
);
defparam myflipflop.INIT = 1;
'initial'을 사용하는 것이 훨씬 낫지 않나요?
선택지 #3: 비동기 리셋
비동기 리셋(asynchronous reset)이 흔히 잘못 사용되는 이유를 다룬 페이지를 아직 읽지 않았다면, 먼저 읽어 보시기를 권합니다. 어차피 이런 종류의 리셋을 사용할 생각이 없다면 굳이 그러지 않아도 됩니다.
왜 그런지는 모르지만, 많은 사람이 비동기 리셋(asynchronous reset)을 모든 용도에 맞는 올바른 해법이라고 생각합니다. 어쩌면 코드 예제에 자주 등장하기 때문일 수도 있고, 모든 동기 소자에 도달하는 전역 리셋을 간단히 만드는 것 같은 착각 때문일 수도 있습니다. 옛날 ASIC 세계에서는 비동기 리셋이 제조 공정 중 칩 테스트에 유용했기 때문일 수도 있습니다. 칩 전체를 리셋하고 테스트 벡터(test vector)를 적용하기 시작할 수 있게 해주기 때문입니다.
그럼 현실로 돌아가 보겠습니다. 비동기 리셋(asynchronous reset)을 제대로 그리고 깔끔하게 사용하는 방법은 클록을 끈 상태에서 리셋하는 것입니다. 그것이 바로 '비동기'라는 말이 진짜 의미하는 바입니다. 실제 설계에서는 다음 단계로 이루어집니다:
- 모든 클록을 비활성화합니다(이 절차를 제어하는 로직이 사용하는 클록은 제외). 이 비활성화는 흔히 전역 클록 버퍼의 클록 인에이블(clock enable) 입력을 끄는 방식으로 수행됩니다(클록 게이팅(clock gating)).
- 비동기 리셋(asynchronous reset) 신호를 활성화했다가 비활성화합니다. 모든 동기 소자를 리셋할 수 있을 만큼 펄스가 충분히 길어야 합니다.
- 모든 동기 소자가 클록을 받을 준비가 될 때까지 충분한 시간을 기다립니다.
- 클록을 다시 활성화합니다.
이 절차는 구현하기 어렵지 않지만, 각 클록의 첫 번째 에지가 글리치(glitch) 없이 제대로 형성되도록 보장하는 것은 더 어려울 수 있습니다. 클록 버퍼에서 흔한 문제는 클록 버퍼의 출력 인에이블 활성화와 클록 버퍼를 통과하는 첫 번째 클록 에지 사이의 타이밍 요구 사항입니다. 이 타이밍 요구 사항을 위반하면 클록 버퍼가 글리치(짧은 펄스로, FPGA의 클록 요구 사항을 위반하는 신호)를 출력할 수 있습니다. 이로 인해 이 클록에 의존하는 모든 동기 소자가 예측할 수 없게 동작할 수 있습니다.
안타깝게도 FPGA 벤더가 제공하는 문서가 이 타이밍 요구 사항을 어떻게 보장하는지 항상 설명해 주지는 않습니다. 그 결과, 리셋 후 첫 번째 클록 에지가 올바르게 동작하도록 보장하지 못할 수도 있습니다. 첫 번째 클록 에지에 대한 보장이 없다면 리셋은 무의미합니다.
이 방법을 사용한다면 비동기 리셋(asynchronous reset)과 관련된 경로(path)에 타이밍 제약(timing constraints)이 적용되지 않도록 하십시오. 이런 경우에는 그러한 강제 적용이 불필요하며, 기본적으로 켜져 있을 수도 있습니다.
비동기 리셋(asynchronous reset)을 안정적으로 적용하는 또 다른 방법이 흔히 제안됩니다. 이 방법은 클록 게이팅(clock gating)을 사용하지 않으므로 클록 버퍼에 의존하지 않습니다. 아이디어는 플립플롭 몇 개를 추가하여 비동기 리셋 신호의 활성화는 직접 통과시키되, 리셋 비활성화는 동기적으로 일어나게 하는 것입니다. 즉, 동기화된 비동기 리셋(synchronized asynchronous reset)입니다.
예를 들어, 원래의 비동기 리셋으로 사용할 신호가 @external_resetn이라면, 다음과 같은 방식으로 리셋을 생성합니다:
reg pre_rstn1, pre_rstn2;
reg resetn;
always @(posedge clk or negedge external_resetn)
if (!external_resetn)
begin
resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
@clk는 @resetn에 의해 리셋되는 동기 소자들이 사용하는 클록이라는 점에 유의하십시오.
@external_resetn이 활성화되면(즉, 로우(low)) 세 레지스터 모두 비동기적으로 활성화됩니다(즉, 0). 하지만 @external_resetn이 비활성화되면, 다음 클록 에지에서 @pre_rstn1만 비활성화되고, 이것이 이후 클록에서 @pre_rstn2와 @resetn으로 전파됩니다.
추가된 두 레지스터의 목적은 메타스테이빌리티(metastability)를 방지하여 @resetn이 안전한 방식으로 비활성화되도록 하는 것입니다. @external_resetn이 @clk에 대해 좋지 않은 타이밍에 비활성화되면 @pre_rstn1이 메타스테이블 상태에 빠질 수 있으므로, 이 보호가 필요합니다(이 페이지에서 메타스테이빌리티에 대해 설명합니다).
이 동기화기(synchronizer)의 장점은 @external_resetn을 비동기 리셋(asynchronous reset)으로 사용할 수 있다는 것입니다. 클록이 전혀 활성화되어 있지 않아도 동작하기 때문입니다. 그럼에도 동기 소자들이 받는 리셋 신호는 동기적으로 비활성화되므로 타이밍을 보장할 수 있습니다.
말할 필요도 없이, 각 클록에는 고유한 동기화된 비동기 리셋(synchronized asynchronous reset)이 필요합니다.
위와 같이 @resetn을 생성하는 것만으로는 충분하지 않다는 점을 유의하십시오. @resetn에서 동기 소자로 가는 경로(path)에는 @clk에 대한 타이밍 제약(timing constraints)을 적용해야 합니다. 일부 FPGA 도구는 동기 소자의 비동기 리셋(asynchronous reset) 입력으로 끝나는 경로의 타이밍을 기본적으로 무시합니다. 그러려면 도구 설정을 변경해야 할 수도 있습니다.
따라서 @resetn을 일반적인 비동기 리셋으로 사용한다면, 예를 들어:
always @(posedge clk or negedge resetn)
if (!resetn) // Are you sure this path is timed?
the_register <= 0;
else
[ ... ]
위에 제시된 동기화기만으로는 리셋으로부터의 안정적인 회복을 보장하기에 충분하지 않습니다. @resetn에서 시작하여 플립플롭들의 비동기 리셋(asynchronous reset) 입력으로 끝나는 경로(path)가 실제로 타이밍 분석 대상인지 확인하는 것은 여러분의 책임입니다.
또한 이 동기화기는 @external_resetn의 글리치(glitch)에 대해서는 도움이 되지 않는다는 점도 중요합니다. @external_resetn의 활성 펄스 길이가 FPGA 플립플롭의 사양보다 짧으면 어떤 일이든 벌어질 수 있습니다. 따라서 @external_resetn은 긴 펄스를 보장하는 어떤 로직이나 외부 전자 회로로 생성해야 합니다. 이것이 불가능하다면, 유일한 해결책은 @sync_resetn에 대한 다음 예제처럼 리셋을 완전히 동기화하는 것입니다:
reg pre_rstn1, pre_rstn2;
reg sync_resetn;
always @(posedge clk)
if (!external_resetn)
begin
sync_resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
sync_resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
하지만 이 동기화기는 @clk가 비활성 상태이면 @external_resetn을 무시합니다. 만약 로직이 @external_resetn을 비동기 리셋(asynchronous reset)처럼 취급해야 한다면 문제가 됩니다. 다시 말해, 클록이 활성화되어 있지 않아도 동작해야 한다는 뜻입니다.
첫 번째 동기화기로 돌아가 보겠습니다. @resetn을 일반적인 동기 리셋(synchronous reset)으로 사용하면 어떨까요? 예를 들어 다음과 같이 말입니다:
always @(posedge clk) // @resetn not in sensitivity list!
if (!resetn)
the_register <= 0;
else
[ ... ]
이것은 대체로 괜찮습니다. @resetn의 비활성화는 타이밍 제약(timing constraints) 덕분에 분명히 타이밍 분석 대상이 되기 때문입니다. 그러나 @resetn의 비동기적인 활성화는 타이밍 분석 대상이 아니므로, 리셋이 적용되기 직전에 관련 동기 소자들이 임의로 동작할 수 있습니다. 이런 용도로는 완전히 동기화된 리셋, 예를 들어 위에서 정의한 @sync_resetn을 사용하는 것이 더 좋습니다.
이 주제를 마무리하며 한 가지 일반적인 언급을 하겠습니다. 위 예제들에서 액티브 로우(active-low) 리셋을 선택한 이유는, 리셋 신호를 저항을 통해 전원 전압에 연결된 커패시터로 생성하던 시절의 전통 때문입니다. 커패시터에 처음에는 전압이 없으므로 리셋 입력이 '0'이었습니다. 이 커패시터는 곧 충분한 전하를 축적하게 되고, 그 결과 리셋 입력은 '1'로 바뀌었습니다. 이 고대적인 파워업 리셋(power-up reset) 방식이 오늘날까지도 많은 리셋이 액티브 로우인 이유입니다.
결론적으로, 비동기 리셋(asynchronous reset)을 안정적으로 사용하는 것은 가능하지만, 이를 구현하는 것은 많은 사람이 믿는 것처럼 결코 간단하지 않습니다. 안정적인 비동기 리셋을 보장하는 두 가지 방법을 제시했습니다. 타이밍 문제를 피하기 위해 클록을 잠시 꺼 두는 방법, 또는 타이밍을 보장하기 위해 동기화기를 사용하는 방법입니다. FPGA에서는 늘 그렇듯, 타이밍이 가장 핵심입니다.
실제 세상에서 비동기 리셋(asynchronous reset)에 의존하는 대부분의 설계는 이 중 어떤 방법도 사용하지 않습니다. 그 결과 FPGA 설계의 신뢰성은 순전히 운에 달려 있습니다.
선택지 #4: 동기 리셋
동기 리셋(synchronous reset)은 다음 코드 패턴으로 가장 잘 알려져 있습니다:
always @(posedge clk)
if (reset)
the_register <= 0;
else
[ ... ]
더 나은 코드 패턴은 나중에 제안하겠습니다. 지금은 일단 이 패턴을 기준으로 설명하겠습니다. 어쨌든 여기서는 액티브 하이(active-high) 리셋을 선택했다는 점에 유의하십시오. 동기 리셋(synchronous reset)에서는 액티브 하이가 더 일반적인 선택이기 때문입니다. 적어도 제 인상으로는 그렇습니다.
동기 리셋(synchronous reset)은 다음 몇 가지를 제외하면 거의 모든 측면에서 비동기 리셋(asynchronous reset)보다 낫습니다:
- 동기 리셋(synchronous reset) 신호는 엄청난 팬아웃(fan-out)에 이를 수 있고, 모든 경로(path)에 타이밍 제약(timing constraints)이 적용됩니다. 따라서 이런 종류의 리셋은 타이밍 제약을 달성하는 것을 어렵게 만들 수 있습니다.
- 클록이 활성화되어 있지 않으면 동기 리셋(synchronous reset)을 사용할 수 없습니다.
- 일부 FPGA에는 전역 라우팅 전용 리소스가 있는데, 이것은 비동기 리셋(asynchronous reset)에서만 사용할 수 있습니다(이것이 실제로 사실인지 확실하지는 않지만, Intel FPGA에 대해 언급된 것을 본 적이 있어서 이 주석을 추가합니다).
FPGA는 클록이 전혀 활성화되지 않은 채로 사용되는 경우가 드물고(전통적으로 테스트를 위해 이런 상황이 필요했던 ASIC과 달리) 라우팅 전용 리소스와 관련된 문제가 실제로 존재하는지도 명확하지 않으므로, 저는 핵심 주제인 팬아웃(fan-out)에 초점을 맞추겠습니다. 다행히 이것은 해결하기 쉽습니다.
또한 위에서 제안한 동기화기를 사용한다면 동일한 팬아웃(fan-out) 문제가 비동기 리셋(asynchronous reset)에도 똑같이 영향을 미친다는 점을 언급할 가치가 있습니다. 따라서 팬아웃이 동기 리셋(synchronous reset)의 단점이라고 말할 수 있는 사람은 사실상 클록을 끈 상태(즉, 게이팅한 상태)에서 비동기 리셋을 사용하는 사람들뿐입니다.
팬아웃(fan-out) 문제를 해결하기 위해 가장 먼저 떠오르는 방법은 합성 도구의 제약이나 속성(attribute)으로 팬아웃 한도를 설정하는 것입니다. 하지만 이것은 선호되지 않는 방법입니다. 한도에 도달하면 합성 도구(synthesizer)가 플립플롭을 복제할 뿐이기 때문입니다. 그 결과 복제된 플립플롭의 출력이 완전히 다른 용도의 모듈들로 연결되는 경우가 자주 발생하며, 따라서 이 출력들의 목적지가 FPGA 전체에 흩어질 수 있습니다. 이는 긴 라우팅과 상당한 전파 지연(propagation delay)을 초래합니다.
간단하면서도 효율적인 해결책은 로직의 주요 부분마다 로컬 리셋을 만드는 것입니다. 예를 들어 다음과 같습니다:
module medium_sized_module (
input clk,
input reset,
input [15:0] in_data,
output [15:0] out_data
);
(* dont_touch = "true" *) reg local_reset;
reg the_register;
always @(posedge clk)
local_reset <= reset;
always @(posedge clk)
if (local_reset)
the_register <= 0;
else
[ ... ]
아이디어는 @local_reset이 @reset의 로컬 사본(한 클록만큼 지연된)이라는 것입니다. 이 모듈부터 하위 계층까지 @reset 대신 @local_reset을 사용하면 팬아웃(fan-out)을 합리적인 수준으로 유지할 수 있습니다. 이 로컬 리셋을 사용하는 소비자들은 어차피 서로 긴밀하게 연결될 것으로 예상되므로, FPGA의 특정 영역에 배치될 가능성이 높습니다. 따라서 로컬 리셋이 로직 패브릭 위를 멀리 이동할 필요가 없습니다.
로직 최적화를 위해 합성 도구(synthesizer)가 로컬 리셋 레지스터를 제거하지 못하게 하는 것이 중요합니다. 합성 도구는 동일한 동작을 하는 레지스터가 있으면, 그것들이 서로 다른 모듈에 속해 있더라도 보통 이런 제거를 수행합니다. 위 예제에는 Vivado의 합성 속성, 즉 'dont_touch'가 표시되어 있습니다. 각 합성 도구마다 이를 처리하는 고유한 방법이 있습니다(Quartus의 경우 'dont_merge' 합성 속성으로 동일한 효과를 얻습니다).
합성 도구가 실제로 모든 레지스터를 유지했는지 확인하려면, 이 레지스터들에 모두 동일한 이름(예를 들어 위에서 제안한 local_reset)을 부여한 다음, 구현된 설계에서 이 이름을 가진 레지스터를 검색해 보는 것이 유용합니다.
물론 모든 모듈에 로컬 리셋을 만들 필요는 없습니다. 대략적인 수치로 말하면, 팬아웃(fan-out)이 50~100 정도면 로컬 리셋에 합리적입니다. 특히 FPGA의 작은 물리적 영역 안에 있는 로직 요소들에 연결될 때 그렇습니다.
팬아웃(fan-out)을 줄이는 주제는 타이밍 클로저(timing closure) 맥락에서도 다루어집니다.
동기 리셋에 대해 더 알아보기
동기 리셋(synchronous reset)에 대한 흔한 오해는, 합성 도구(synthesizer)가 동기 리셋에 해당하는 Verilog 코드 패턴을 만나면 리셋 신호를 플립플롭의 동기 리셋 입력에 연결할 것이라는 것입니다. 그럴 수도 있지만, 대개는 그렇지 않습니다.
이것은 비동기 리셋(asynchronous reset)의 경우와 다릅니다. 비동기 리셋은 플립플롭의 비동기 리셋 입력에 연결되어야 합니다. 그렇지 않으면 클록이 없을 때 리셋이 동작하지 않습니다.
합성 도구(synthesizer)는 코드 패턴에 특별한 의미를 부여하지 않는 경향이 있습니다. 대신 Verilog 코드에서 도출되는 논리 방정식을 계산할 뿐입니다. 다음 예를 살펴보겠습니다:
always @(posedge clk)
if (reset)
the_register <= 0;
else if (some_condition)
the_register <= !the_register;
else if (some_other_condition)
the_register <= 0;
이 코드를 읽는 한 가지 방법은, always 문이 동기 리셋(synchronous reset)을 요청하는 표준 코드 패턴으로 시작하고, 그 다음에 레지스터 동작에 대한 구체적인 정의가 오는 것으로 보는 것입니다. 따라서 @reset이 해당 플립플롭의 동기 리셋 입력에 연결되고, 논리 함수(LUT로 구현되는)의 출력이 플립플롭의 데이터 입력으로 갈 것이라고 기대할 수 있습니다.
실제로 합성 도구(synthesizer)는 보통 @the_register의 다음 클록에서의 값을 가능한 한 간결하게 구현합니다. 예를 들어, 플립플롭의 리셋 입력이 (reset || (some_other_condition && !some_condition))이라는 식을 구현하는 논리 함수(즉, LUT)에 연결될 수도 있습니다.
하지만 더 흥미로운 가능성도 있습니다. 플립플롭의 리셋 입력을 아예 사용하지 않을 수도 있습니다. 대신 데이터 입력만 사용하고, 논리 함수가 @reset 신호를 입력 중 하나로 사용하는 것입니다. @reset이 하이(high)이면 논리 함수의 출력은 0이 됩니다. 이렇게 하면 @reset이 실제로 @the_register를 0으로 만들기는 하지만, 다른 신호와 다르게 취급되지는 않습니다.
다시 강조하자면, 플립플롭에 동기 리셋(synchronous reset) 입력이 있다 하더라도 합성 도구(synthesizer)는 동기 리셋 코드 패턴을 특별히 취급하지 않는 경향이 있으며, 리셋 신호를 다른 신호와 다르게 취급하지도 않습니다. 플립플롭의 리셋 입력은 Verilog 코드가 요구하는 동작을 구현하는 데 가장 좋은 방식으로 사용됩니다. 때로는 리셋 입력을 리셋 신호에 직접 연결하고, 때로는 리셋 신호가 포함된 어떤 논리 함수에 연결하고, 때로는 리셋 입력을 전혀 사용하지 않을 수도 있습니다. 합성 도구는 성능 목표를 더 잘 달성하는 데 도움이 되는 방식이면 무엇이든 할 뿐, 그 이상도 이하도 아닙니다.
Xilinx Vivado 사용자는 DIRECT_RESET과 EXTRACT_RESET이라는 두 가지 합성 속성(attribute)을 통해 이 문제를 더 잘 제어할 수 있습니다.
마지막으로 비동기 리셋(asynchronous reset)의 또 다른 단점을 언급하겠습니다. 대부분의 FPGA에서 플립플롭에는 리셋/셋 입력이 하나만 있습니다. 이 입력은 동기적으로도 비동기적으로도 동작할 수 있습니다. 리셋이 동기(synchronous)라면, 합성 도구(synthesizer)는 요청된 동작을 구현하는 과정에서 LUT를 덜 사용하기 위해 이 입력을 활용하는 묘안을 찾아낼 수 있습니다. 하지만 비동기 리셋이 있으면 이런 지름길은 불가능합니다. 따라서 비동기 리셋은 합성 도구의 손을 묶어 더 많은 로직 리소스를 낭비하도록 강요합니다.
레지스터가 의도치 않게 고정되는 문제 피하기
리셋을 구현할 때 흔히 사용하는 코드 패턴에는 다음과 같은 함정이 있습니다:
always @(posedge clk or negedge resetn)
if (!resetn)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
주석이 암시하듯, @reg3은 @resetn이 활성 상태일 때의 begin-end 블록에 나타나지 않습니다. 그 결과, 위 Verilog 코드는 @resetn이 활성화되어 있는 동안 @reg3의 값이 변하지 않을 것을 요구합니다. 이것은 @reg3을 다음과 같이 정의한 것과 같습니다:
always @(posedge clk)
if (resetn)
reg3 <= [ ... ];
다시 말해, @resetn이 @reg3의 클록 인에이블(clock enable)로 동작하는 셈입니다. 즉, @resetn이 하이일 때만 클록이 유효합니다.
동기 리셋(synchronous reset)에서도 정확히 같은 일이 발생합니다:
always @(posedge clk)
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
이 경우는 사실 이해하기 더 쉽습니다. 이것은 단지 두 개의 begin-end 블록일 뿐이며, 두 번째 블록은 @reset이 비활성 상태일 때만 적용되기 때문입니다. 따라서 이 예제에서 @reg3의 정의는 명확하게 다음과 같습니다:
always @(posedge clk)
if (!reset)
reg3 <= [ ... ];
그렇다면 뻔한(그다지 영리하지는 않은) 결론은 리셋을 위한 begin-end 블록에서 어떤 레지스터도 빠뜨리지 말라는 것입니다. 실제로 많은 FPGA 설계자는 이것이 유일한 방법이라고 믿기 때문에, 필요하든 필요하지 않든 모든 레지스터를 리셋합니다. 아니면 각 레지스터마다 고유한 'always' 문을 두는 코딩 스타일을 채택하기도 합니다.
그런데 일부 레지스터만 리셋하고 나머지는 리셋하지 않으려면 어떻게 해야 할까요?
비동기 리셋(asynchronous reset)의 경우에는 리셋할 레지스터들을 별도의 'always' 문에 넣는 방법밖에 없습니다. 하지만 동기 리셋(synchronous reset)에서는 이를 해결할 간단한 방법이 있습니다:
always @(posedge clk)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// I don't want to reset reg3, and that's fine!
end
end
'if (reset)' 문을 앞에 두고 나머지 핵심 동작을 'else' 문 아래에 두는 대신, 'if (reset)'을 맨 마지막에 배치하면 리셋을 위한 할당들이 그 앞의 모든 것을 덮어씁니다(override).
이것은 위의 어떤 예제와도 동등하지 않음에 유의하십시오. @reg1과 @reg2는 @reset이 활성일 때 리셋되지만, @reg3는 리셋의 영향을 전혀 받지 않습니다.
동기 리셋(synchronous reset)을 적용하는 이 대안적인 방식이 불편하게 느껴진다면 이해할 수 있습니다. 그럴 만한 이유가 몇 가지 있습니다. 우선, FPGA 설계에서는 일반적으로 널리 쓰이는 코드 패턴을 따르는 것이 좋은 관행입니다. 그렇지 않으면 합성 도구(synthesizer)가 이색적인 버그를 드러낼 수도 있는데, 잘 정립된 코드 패턴에서는 그런 일이 발생할 가능성이 훨씬 낮습니다(Golden Rule #4 참조). 따라서 Verilog 표준이 이 방법이 동작한다고 명시적으로 요구한다고 해도, 이 기능에 의존하는 것이 반드시 좋은 생각은 아니라고 주장할 수 있습니다.
그것은 타당한 논쟁입니다. 하지만 참고로 말하자면, 저는 다양한 합성 도구와 함께 이런 코드 패턴을 10년 넘게 많이 사용해 왔습니다. 특히 제 코드에서 동기 리셋(synchronous reset)을 정의할 때 바로 이 방식을 사용합니다. 이 방식으로 단 한 번도 문제를 겪은 적이 없습니다.
이 방법을 선호하지 않는 또 다른 가능한 이유는, 일반적인 코드 패턴이 아니기 때문에 합성 도구(synthesizer)가 동기 리셋(synchronous reset)을 원한다는 힌트를 놓칠지도 모른다는 생각입니다. 하지만 위에서 이미 언급했듯이 대부분의 합성 도구는 어차피 그런 힌트를 사용하지 않으며, 동기 리셋을 단지 로직의 요구 동작에 대한 또 하나의 정의로 취급합니다. 따라서 이 이유는 근거가 없습니다.
제 말을 믿고 이것이 안전하다고 받아들인다면, 골칫거리를 하나 덜 수 있을 것입니다.
동일한 방식을 비동기 리셋(asynchronous reset)에도 적용할 수 있을까요? 예를 들어, 다음 코드는 어떻게 동작할까요?
always @(posedge clk or negedge resetn)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
if (!resetn)
reg1 <= 0;
end
이것은 물론 비동기 리셋(asynchronous reset)의 일반적인 코드 패턴에서 벗어난 것입니다. Vivado 합성 도구(synthesizer)로 비공식적으로 테스트해 본 결과, 합성 도구는 힌트를 알아차리고 @reg1에 비동기 리셋을 할당했습니다.
하지만 @reg2에 요구되는 동작은 FPGA에서 구현할 수 없습니다. 위에 작성된 대로라면, @clk와 @resetn이 둘 다 클록이며, @reg2는 각각의 상승 에지와 하강 에지에서 새 값을 샘플링(sampling)한다는 뜻이기 때문입니다. 제가 아는 어떤 FPGA에도 클록 입력이 두 개인 플립플롭은 없으므로, @reg2의 정의를 합성하는 것은 불가능합니다.
Vivado 합성 도구는 'negedge resetn' 부분을 무시하고 @clk만을 클록으로 사용하는 플립플롭을 만들어 이 상황에 대응했습니다. 합성 결과에는 이런 이상한 점의 흔적이 전혀 없었고, 합성 도구가 어떤 경고나 불만도 내보내지 않았습니다. 합성 도구가 Verilog 코드에 정의된 대로 동작하지 않는 로직을 만들었다는 사실에도 불구하고 말입니다.
따라서 특히 Vivado 합성 도구에서는 동일한 코드 패턴이 비동기 리셋(asynchronous reset)에서도 실제로 동작합니다. 하지만 이것에 의존해서는 안 됩니다. Verilog 코드는 로직이 하길 원하는 바를 명확히 말해야 합니다. 그렇지 않으면 합성 도구는 자기 마음대로 코드를 잘못 해석할 권리가 충분히 있습니다.
이상으로 리셋에 관한 이 시리즈의 두 번째 페이지를 마칩니다. 다음 페이지에서는 전원 투입과 외부 리셋 이후 FPGA가 기동하는 다양한 측면을 다룹니다.