모든 디지털 시스템은 크게 데이터를 처리하는 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-Always: 상태 전이와 출력을 한 블록에 다 때려 넣음. (가독성 최악, 비추천)
- 2-Always: 상태 레지스터(Seq) + 다음 상태 로직(Comb). (표준적인 방식)
- 3-Always: 상태 레지스터(Seq) + 다음 상태 로직(Comb) + 출력 로직(Seq/Comb).
추천: 3-Always (또는 분리된 Output Logic)
가장 추천하는 방식은 상태 전이(Next State Logic)와 출력 결정(Output Logic)을 명확히 분리하는 것입니다.
// 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
endTip: 출력 로직을
always @(*)로 짜면 코드는 짧아지지만 글리치(Glitch)가 발생할 수 있습니다. 위 코드처럼 Next State를 미리 보고(Look-ahead) 레지스터로 출력하면 타이밍이 훨씬 깔끔해집니다.
3. 상태 인코딩: Binary vs One-hot
상태(State)를 비트로 표현하는 방법(Encoding)에 따라 칩의 성능이 달라집니다.
| 방식 | 설명 (4 states 예시) | 장점 | 단점 | 추천 환경 |
| Binary | 00, 01, 10, 11 | 레지스터 사용량 최소화 (N개 상태 -> log2(N) 비트) | 디코딩 로직이 복잡해짐 (속도 저하) | ASIC (면적 중시) |
| One-hot | 0001, 0010, 0100 | 디코딩이 매우 빠름 (비트 하나만 보면 됨) | 레지스터를 많이 씀 (N개 상태 -> N 비트) | FPGA (레지스터 풍부, 속도 중시) |
| Gray | 00, 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 설계 수칙
- Default Case 필수:
case문 마지막엔 반드시default: next_state = IDLE;을 넣어 정의되지 않은 상태에서도 초기화되도록 해야 합니다. - 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