[Verilog] FSM(유한 상태 머신) RTL 설계의 정석

모든 디지털 시스템은 크게 데이터를 처리하는 Data Path와 이를 지휘하는 Control Logic으로 나뉩니다. 이 Control Logic의 핵심이 바로 FSM (Finite State Machine)입니다.

“FSM이야 case 문 써서 상태 옮기면 되는 거 아닌가?”라고 생각할 수 있습니다. 하지만 clock 주파수가 500MHz를 넘어가고, 상태가 50개를 넘어가는 복잡한 IP RTL 설계를 하다 보면, 잘못 짠 FSM은 타이밍 위반(Timing Violation)과 데드락(Deadlock)의 주범이 됩니다.

이번 글에서는 주니어 엔지니어들이 흔히 범하는 실수를 짚어보고, 실무에서 통용되는 안전하고 효율적인 FSM 설계 패턴을 다룹니다.

1. 구조의 선택: Moore vs Mealy, 그리고 Registered Output

교과서에서는 Moore와 Mealy 머신을 다음과 같이 구분합니다.

  • Moore Machine: 출력(Output)이 현재 상태(Current State)에만 의존함. (안정적)
  • Mealy Machine: 출력(Output)이 현재 상태 + 입력(Input)에 의존함. (반응 빠름)

실무의 딜레마와 해결책

Mealy 머신은 입력 변화에 즉각 반응하지만, 입력의 노이즈(Glitch)가 출력으로 바로 전달되거나 조합 회로 경로(Combinational Path)가 너무 길어지는 문제가 있습니다. 반면 Moore 머신은 한 클럭 늦게 반응합니다.

시니어의 선택: Registered Output Mealy (Class 3 FSM) 실무에서는 이 둘의 장점을 합치기 위해, 상태 결정 로직은 Mealy처럼 짜되, 최종 출력단에 반드시 레지스터(Flip-flop)를 두는 방식을 선호합니다.

  • 장점: 출력 신호가 클럭에 깨끗하게 동기화되어 나가므로(Glitch Free), 다음 블록의 타이밍 계산(Setup Time)이 매우 유리해집니다.

2. RTL 코딩 스타일: 2-Always vs 3-Always

Verilog로 FSM을 구현하는 방법은 크게 세 가지가 있습니다.

  1. 1-Always: 상태 전이와 출력을 한 블록에 다 때려 넣음. (가독성 최악, 비추천)
  2. 2-Always: 상태 레지스터(Seq) + 다음 상태 로직(Comb). (표준적인 방식)
  3. 3-Always: 상태 레지스터(Seq) + 다음 상태 로직(Comb) + 출력 로직(Seq/Comb).

추천: 3-Always (또는 분리된 Output Logic)

가장 추천하는 방식은 상태 전이(Next State Logic)와 출력 결정(Output Logic)을 명확히 분리하는 것입니다.

FSM 예시
FSM 예시
// 1. State Register (Sequential)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) current_state <= IDLE;
    else        current_state <= next_state;
end

// 2. Next State Logic (Combinational) - 타이밍의 핵심
always @(*) begin
    case (current_state)
        IDLE:    if (start) next_state = READ;
                 else       next_state = IDLE;
        READ:    if (done)  next_state = WRITE;
                 else       next_state = READ;
        // ...
        default: next_state = IDLE; // 필수!
    endcase
end

// 3. Output Logic (Sequential recommended for timing)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cmd_out <= 0;
        en_out  <= 0;
    end else begin
        // Next State를 미리 보고 출력을 결정 (Look-ahead)
        case (next_state) 
            IDLE:    {cmd_out, en_out} <= 2'b00;
            READ:    {cmd_out, en_out} <= 2'b01;
            WRITE:   {cmd_out, en_out} <= 2'b10;
            default: {cmd_out, en_out} <= 2'b00;
        endcase
    end
end

Tip: 출력 로직을 always @(*)로 짜면 코드는 짧아지지만 글리치(Glitch)가 발생할 수 있습니다. 위 코드처럼 Next State를 미리 보고(Look-ahead) 레지스터로 출력하면 타이밍이 훨씬 깔끔해집니다.

3. 상태 인코딩: Binary vs One-hot

상태(State)를 비트로 표현하는 방법(Encoding)에 따라 칩의 성능이 달라집니다.

방식설명
(4 states 예시)
장점단점추천 환경
Binary00, 01, 10, 11레지스터 사용량 최소화 (N개 상태 -> log2(N) 비트)디코딩 로직이 복잡해짐 (속도 저하)ASIC (면적 중시)
One-hot0001, 0010, 0100디코딩이 매우 빠름 (비트 하나만 보면 됨)레지스터를 많이 씀 (N개 상태 -> N 비트)FPGA (레지스터 풍부, 속도 중시)
Gray00, 01, 11, 10한 번에 1비트만 변함글리치 최소화, 저전력고속/저전력 설계
  • FPGA 설계자라면: 합성 툴이 알아서 최적화해주지만, 명시적으로 (* fsm_encoding = "one_hot" *) 속성을 주어 타이밍을 확보하기도 합니다.
  • ASIC 설계자라면: 보통 Binary나 Gray 코드를 사용하여 면적을 아낍니다.

4. 안전성(Safety): 데드락과 복구

우주선(Radiation)이나 전원 노이즈로 인해 FSM의 상태 레지스터 비트가 0에서 1로 뒤집히는 SEU(Single Event Upset)가 발생할 수 있습니다. 만약 정의되지 않은 상태(Invalid State)로 점프했는데, 거기서 빠져나오는 길이 없다면? 칩은 먹통이 됩니다(Deadlock).

Safe FSM 설계 수칙

  1. Default Case 필수: case 문 마지막엔 반드시 default: next_state = IDLE;을 넣어 정의되지 않은 상태에서도 초기화되도록 해야 합니다.
  2. Explicit State Assignment: parameter IDLE = 3'b000; 처럼 상태 값을 명시적으로 할당하고, 남는 상태(Unused State)들이 어디로 갈지 설계자가 인지해야 합니다.

5. 가독성을 높이는 팁 (Code Quality)

  • Parameter 사용: state <= 3'b001 대신 state <= READ_Header 처럼 의미 있는 이름을 사용하세요.
  • FSM 다이어그램 그리기: 코드를 짜기 전에 버블 다이어그램(Bubble Diagram)을 먼저 그리세요. 로직의 구멍을 코딩 전에 찾을 수 있습니다.
  • 출력 로직 분리: 상태 전이 로직 안에 출력 할당(cmd_out = 1)을 섞어 쓰면 스파게티 코드가 됩니다. 분리하세요.

6. 결론: FSM은 ‘타이밍’이다

잘 만든 FSM은 단순히 순서대로 동작하는 것을 넘어, 다음 블록에게 깨끗한 신호를, 정확한 박자(Timing)에 전달합니다.

  • 출력단에 레지스터를 두어 타이밍 경로(Timing Path)를 끊어주는 습관.
  • Default case를 챙기는 꼼꼼함.
  • 타겟(FPGA/ASIC)에 맞는 인코딩 선택.

참고: DigiKey

Similar Posts