Verilog FSM에서 상태는 어떻게 저장되는가 — 플립플롭과 클럭 동기화

⚙️ Verilog FSM에서 상태는 어떻게 저장되는가 — 플립플롭과 클럭 동기화

디지털 논리 설계 · 유한 상태 기계(FSM) · RTL 코딩 표준

Verilog로 작성하는 유한 상태 기계(FSM)는 디지털 논리 설계의 가장 기본적인 골격입니다. 그 동작은 명확합니다 — 다음 상태는 순수 조합 논리가 현재 상태와 입력으로부터 즉시 계산하고, 엣지 트리거 플립플롭이 클럭 엣지에서 단 한 번 포착해 새로운 현재 상태로 고정합니다. 이 글에서는 표준 2-Always 구조를 따라 상태가 실제로 어디에, 어떻게 저장되는지를 래치와 플립플롭의 타이밍 차이를 파형으로 드러내며 정리합니다. 흔히 떠올리는 ‘래치에 잠깐 담겼다가 다음 클럭에 넘어간다’는 그림이 왜 물리 계층에서만 부분적으로 맞는지도 함께 짚습니다.

💡 한 줄 핵심: 정석 FSM에서 다음 상태는 순수 조합 논리가 즉시 계산하고, 엣지 트리거 플립플롭이 클럭 엣지에서 단 한 번 포착해 고정합니다. 별도의 래치에 임시 저장되는 단계는 없으며, ‘래치’ 직관은 플립플롭 내부 물리 구조(master-slave)에 한해서만 부분적으로 맞습니다.

1. 핵심 교정 — 상태는 ‘래치’가 아니라 ‘플립플롭’에 저장된다

정석적인 FSM에서 다음 상태(next_state)는 래치를 거쳐 임시 저장되지 않습니다. 그 값은 ① 순수 조합 논리가 현재 상태와 입력으로부터 즉시 계산하고, ② 엣지 트리거 D 플립플롭이 클럭 엣지 순간에 단 한 번 포착(capture)해 새로운 현재 상태로 고정합니다. 합성 과정에서 의도치 않게 생긴 투명 래치(transparent latch)는 오히려 정적 타이밍 분석(STA)을 어렵게 만들고 글리치를 유발하는 설계 결함으로 간주됩니다.

그렇다면 “래치”라는 직관은 어디서 온 오해일까요? 물리 계층을 보면 일부는 맞습니다. 엣지 트리거 D 플립플롭 자체가 내부적으로는 두 개의 레벨 민감 래치(master-slave)를 직렬로 묶어 만든 구조입니다. 마스터 래치가 클럭 한 위상에서 D를 받아들이고, 슬레이브 래치가 반대 위상에서 그 값을 내보내면서, 결과적으로 “엣지에서만 한 번 통과”하는 동작이 만들어집니다. 즉 “값이 한 단계 잡혔다가 다음에 고정된다”는 감각은 트랜지스터·게이트 수준의 master-slave 동작을 가리킨 것으로 볼 수 있습니다.

다만 RTL/Verilog 코드 수준에서는 이를 단일 엣지 트리거 레지스터 하나로 모델링하며, 조합 블록 안에서 독립적인 투명 래치가 추론(infer)되는 것은 피해야 합니다. 정리하면 — 정석 FSM에 독립 래치는 없고, 사용자가 떠올린 ‘래치’는 FF 내부 물리 구조에 한해서만 부분적으로 유효합니다. 이 두 가지를 분리해서 이해하면 직관과 표준이 화해합니다.

2. 타이밍으로 보는 래치 vs 플립플롭의 결정적 차이

두 소자의 차이는 파형으로 보면 한눈에 들어옵니다. 아래는 동일한 데이터 입력 D를 레벨 민감 래치와 상승 엣지 트리거 FF에 각각 넣었을 때의 출력입니다(클럭 High = 래치가 ‘투명’해지는 구간).

wavedrom diagram

📊 다이어그램 요약: 클럭이 High인 동안 래치(Q_latch)는 D를 그대로 따라가, D가 0으로 떨어지면 즉시 0이 됩니다. 반면 플립플롭(Q_ff)은 상승 엣지에서 잡은 1을 그대로 유지합니다 — 래치는 글리치에 흔들리고, FF는 엣지 외 변화에 면역입니다.

파형을 단계별로 읽으면:

① 첫 상승 엣지 — D=1. 래치와 FF 모두 출력이 1로 올라갑니다.

② clk High 중 D가 0으로 하강 — 래치는 투명 상태라 입력을 따라가 Q_latch가 즉시 0으로 떨어집니다. 반면 FF는 엣지 순간의 값(1)을 이미 잡았으므로 Q_ff는 1을 유지합니다. ← 두 소자의 결정적 차이.

③ clk Low 중 D가 잠깐 튐(글리치성 펄스) — 래치는 Low일 때 불투명(hold)이라 무시하고, FF도 엣지가 아니므로 무시합니다. 둘 다 흔들리지 않습니다.

④ 두 번째 상승 엣지 — D=0이 잡혀 Q_ff가 0으로 내려갑니다.

핵심은 이것입니다. 래치는 “창문이 열린 동안(High) 들어오는 모든 변화를 통과”시키므로 입력 글리치에 취약하고, FF는 “엣지라는 한 순간”에만 샘플링하므로 그 외 구간 노이즈에 면역입니다. FSM의 상태 레지스터로 FF를 쓰는 이유가 바로 여기에 있습니다 — 입력이 클럭 주기 도중에 어떻게 요동치든, 상태는 클럭 엣지마다 단 한 번, 깔끔하게 갱신됩니다.

구분 레벨 민감 래치 엣지 트리거 플립플롭
동작 트리거 클럭 레벨 (High 구간 내내) 클럭 엣지 (상승 순간 1회)
투명 구간 High 동안 D를 즉시 통과 없음 (엣지만)
글리치 민감도 취약 (High 중 모든 변화 통과) 면역 (엣지 외 무시)
FSM 상태 저장 부적합 (생기면 결함) 표준

3. 표준 2-Always FSM의 구조

업계 표준은 상태 저장(순차 논리)다음 상태 계산(조합 논리)을 서로 다른 always 블록으로 분리하는 2-Always 구조입니다(출력까지 떼면 3-Always). 데이터가 어떻게 흐르는지 도식으로 보겠습니다.

d2 diagram

🔗 다이어그램 요약: 입력과 현재 상태로 조합 논리(Next-State Logic)가 next_state를 즉시 계산하고, 엣지 트리거 FF(State Register)가 클럭 엣지에 그 값을 담아 current_state로 되먹입니다. 출력은 별도 조합 논리에서 만들어 상태 전이와 동시에 나옵니다.

next_state를 만드는 조합 논리는 클럭을 기다리지 않고 입력·현재 상태에 즉각 반응하며, 상태 레지스터(FF)는 오직 클럭 엣지에 그 값을 담는 역할만 합니다. current_state는 조합 논리로 되먹임(feedback)되어 다음 사이클 계산의 입력이 됩니다.

4. 표준 2-Always FSM 예시 코드

아래는 가장 전형적인 2-Always FSM 골격입니다. 순차 블록과 조합 블록이 어떻게 역할을 나누는지 주목하세요.

Verilog


module fsm_2always_example (
    input  wire clk,
    input  wire rst_n,
    input  wire in_sig,
    output reg  out_sig
);

    // 상태 정의 (파라미터화)
    localparam S_IDLE = 2'b00;
    localparam S_WORK = 2'b01;

    reg [1:0] current_state, next_state;

    // ------------------------------------------------------------
    // [블록 1] 순차 논리 — 클럭 엣지에 current_state를 갱신하는 FF
    // ------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            current_state <= S_IDLE;
        else
            current_state <= next_state;   // Non-blocking(<=) 할당
    end

    // ------------------------------------------------------------
    // [블록 2] 조합 논리 — next_state / out_sig 연산
    // ------------------------------------------------------------
    always @(*) begin
        // 의도치 않은 래치 추론을 막는 디폴트 할당
        next_state = current_state;
        out_sig    = 1'b0;

        case (current_state)
            S_IDLE: begin
                if (in_sig) next_state = S_WORK;
            end
            S_WORK: begin
                out_sig = 1'b1;            // 현재 상태 기반 출력(Moore)
                if (!in_sig) next_state = S_IDLE;
            end
            default: next_state = S_IDLE;
        endcase
    end
endmodule

▶ 블록 1 (순차)posedge clk에서 Non-blocking 할당으로 FF만 기술합니다. Non-blocking을 쓰는 이유는 한 클럭 안에서 여러 레지스터가 동시에 이전 값을 읽고 새 값을 잡도록 보장하기 위함입니다.

▶ 블록 2 (조합)always @(*)로 순수 논리만 수행합니다. 블록 최상단의 디폴트 할당(next_state = current_state; out_sig = 0)이 핵심입니다. case/if에서 어떤 분기가 누락되더라도 “값을 유지하려고 메모리(래치)를 만드는” 합성 동작을 원천 차단합니다. 이것이 1절에서 말한 “의도치 않은 래치”를 막는 표준 관용구입니다.

5. FSM 상태 전이의 타이밍

위 코드가 동작할 때 next_state(조합)와 current_state(FF)가 시간축에서 어떻게 어긋나는지 보겠습니다.

wavedrom diagram

🔁 다이어그램 요약: 입력 in_sig가 바뀌면 조합 논리 next_state는 클럭과 무관하게 즉시 WORK로 계산되지만, 실제 current_state는 그다음 클럭 엣지에서야 WORK로 커밋됩니다 — 조합은 즉시 준비, FF는 정확히 한 엣지 늦게 확정.

in_sig가 High로 바뀌는 순간, 조합 논리 next_state는 클럭과 무관하게 즉시 WORK로 계산됩니다(게이트 지연만 거침). 래치처럼 “기다렸다 저장”하는 단계가 없습니다.

current_state는 그 다음 클럭 상승 엣지에서 비로소 next_state를 포착해 WORK가 됩니다 — 즉 조합 출력보다 정확히 한 엣지 늦게 커밋됩니다.

in_sig가 다시 Low가 되면 next_state는 즉시 IDLE로 돌아가고, current_state는 다음 엣지에 따라갑니다.

이 “조합은 즉시 준비하고, FF는 엣지에 커밋한다”는 분업이 FSM 동기 설계의 본질입니다.

6. 왜 굳이 always를 나누는가 — 단일 블록을 안 쓰는 이유

기능적으로는 모든 로직을 하나의 always @(posedge clk)에 욱여넣는 1-Always 구조도 작성 자체는 가능합니다. 그럼에도 2-Always가 표준이 된 데에는 명확한 이유가 네 가지 있습니다.

가. 출력 1-클럭 지연(latency) 문제

1-Always 구조에서는 출력도 클럭 엣지에 레지스터링됩니다. 따라서 상태가 바뀌면서 동시에 내보내야 할 출력이 있어도, 그 출력은 상태가 바뀐 다음 클럭 엣지가 되어서야 나타나는 1-클럭 지연이 생깁니다. 조합 논리부에서 출력을 만들면(2-Always) 상태 전이와 동시에, 지연 없이 출력이 반영됩니다.

나. Mealy 머신 구현의 한계

출력이 ‘현재 상태’와 ‘입력’에 동시에 의존하는 Mealy FSM에서는 입력이 변하면 클럭과 무관하게 출력이 즉시 변해야 합니다. 모든 로직이 클럭 엣지에 종속되는 1-Always 구조로는 이 비동기적 즉시 반응을 서술하는 것 자체가 불가능합니다. 조합 논리를 분리해야만 입력을 출력에 직접 연결할 수 있습니다.

다. 관심사의 분리(Separation of Concerns)

FSM이 복잡해질수록 전이 조건식과 상태별 출력 로직은 길어집니다. 레지스터 할당과 조건 분기가 한 블록에 뒤섞이면 추적·디버깅이 급격히 어려워집니다. “메모리를 만드는 영역(순차)”과 “논리를 연산하는 영역(조합)”을 분리하면 가독성이 올라가고 휴먼 에러가 줄어듭니다.

라. 합성기(synthesizer) 인식

Design Compiler 같은 합성 툴은 ‘순차 + 조합’으로 또렷이 나뉜 2-Always 패턴을 전형적 FSM 템플릿으로 인식합니다. 툴이 구조를 정확히 파악해야 상태 인코딩 최적화(One-hot, Gray)나 무효 상태 제거 같은 고급 최적화를 올바로 적용할 수 있습니다.

7. 정리 — 직관과 표준의 화해

상태와 컨트롤 로직을 서로 다른 always로 나누는 것은 단순한 코딩 취향이 아니라, 순차 논리와 조합 논리라는 두 물리적 동작 원리를 코드에 가장 정확히 대응시키는 방법론입니다.

조합 블록은 클럭을 기다리지 않고 현재 상태·입력으로 next_state를 실시간 계산합니다 — 정석 설계에 독립 래치는 없으며, 그 자리에 래치가 생기면 결함입니다.

순차 블록은 오직 클럭 엣지에 그 값을 FF에 안전하게 담는 일에만 집중합니다. FF가 엣지에서만 샘플링하기에 상태는 주기 도중 입력 글리치에 흔들리지 않습니다.

✅ “래치 → 다음 클럭에 FF 고정”이라는 직관은 RTL 수준에선 부정확하지만, 엣지 트리거 FF가 내부적으로 master-slave 래치 쌍이라는 물리 구조를 가리킨 것으로 이해하면 직관과 표준이 화해합니다.

이 분업은 불필요한 출력 지연을 없애고, 설계 의도를 합성 툴에 명확히 전달하며, 복잡한 SoC에서 타이밍 위반이나 예기치 못한 래치 생성을 막는 가장 견고한 엔지니어링 방식입니다. FSM을 처음 배울 때 “래치 vs 플립플롭”의 타이밍 차이 한 가지만 정확히 잡아두면, 이후 복잡한 동기 설계 전체가 훨씬 또렷하게 보이기 시작합니다.

G
GraceMoon
여러 출처로 확인한 리서치

웹 검색과 원문 확인을 거쳐 사실관계를 교차 점검한 뒤 정리합니다. 인용한 출처를 본문에 적습니다.

본 글은 공개된 데이터와 출처를 바탕으로 작성했습니다. 최종 업데이트: 2026-06-21

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤