유한 상태 기계
엘리베이터는 지금 몇 층에 있고 어느 방향으로 가던 중인지 기억한다. 자판기는 지금까지 들어온 동전이 얼마인지, 신호등은 지금이 어느 방향 녹색인지 기억한다. 이처럼 "지금까지의 사연"을 몇 비트로 요약해 들고 다니며, 새 입력이 올 때마다 그 요약을 고쳐 쓰는 회로가 유한 상태 기계(FSM, Finite State Machine)다. 8장에서 만든 플립플롭이 기억을 맡고, 앞 장들의 조합 논리가 판단을 맡는다. 이 장에서는 둘을 엮는 단 하나의 틀을 익힌 다음, 그 틀로 카운터와 LFSR, 시퀀스 검출기, 자판기와 신호등을 직접 만들고 클럭을 한 번씩 넣어 가며 움직여 본다.
- 모든 동기 순차 회로를 상태 레지스터 + 다음 상태 논리 + 출력 논리의 일반형으로 읽는다.
- 이진 카운터·시프트 레지스터·LFSR의 동작과, 리플 카운터가 잠깐 틀린 값을 내는 이유를 설명한다.
- 상태도 → 상태표 → 인코딩 → 다음 상태 식 → 게이트의 절차로 FSM을 설계하고, 무어와 밀리의 타이밍 차이를 안다.
- 이진·원핫·그레이 인코딩의 비용을 비교하고, 상태 최소화와 리셋·불법 상태 복구를 설계에 반영한다.
동기 순차 회로의 일반형
조합 회로의 출력은 지금의 입력만으로 정해진다. 순차 회로의 출력은 지금까지 들어온 입력 전체에 달려 있다. 그러나 과거 전체를 다 기억할 수는 없으니, 앞으로의 동작에 필요한 만큼만 요약해 둔다. 이 요약이 상태(State)다. 상태를 \(k\)비트 플립플롭에 담으면 가능한 상태는 많아야 \(2^k\)개, 즉 유한하다. 이름의 "유한"이 바로 이 뜻이다.
클럭 하나로 모든 플립플롭을 함께 움직이는 동기 순차 회로(Synchronous sequential circuit)는 예외 없이 아래 세 덩어리로 나눌 수 있다. 카운터도, 시프트 레지스터도, CPU의 제어 장치도 마찬가지다.
이 틀의 핵심은 되먹임 고리가 반드시 플립플롭을 지난다는 것이다. 조합 논리만으로 고리를 만들면 8장의 래치처럼 값이 언제 자리 잡을지 알 수 없지만, 고리 중간에 에지 트리거 플립플롭을 두면 "한 클럭에 한 걸음"이라는 규칙이 생긴다. 그 덕에 우리는 지연이 몇 피코초인지 잊고, 상태가 클럭마다 하나씩 바뀌는 이산 시간 기계로 회로를 생각할 수 있다. 대가는 하나다. 다음 상태 논리가 한 클럭 안에 계산을 마쳐야 한다(10장).
아래 그림에서 2비트 업/다운 카운터를 이 틀로 돌려 보자. 다음 상태 논리는 "UP이면 +1, 아니면 −1"을 계산하고, 출력 논리는 무어 모드에서 "상태가 3인가", 밀리 모드에서 "이번 클럭에 한 바퀴를 넘어가는가(자리올림·자리내림)"를 계산한다.
카운터: 가장 단순한 상태 기계
입력이 없거나(클럭만 받는다) 허용 신호 하나뿐인 FSM의 대표가 카운터(Counter)다. \(n\)비트 이진 카운터의 상태는 \(2^n\)개이고, 다음 상태 함수는 \(\delta(s) = s+1 \bmod 2^n\)이다. 덧셈기를 붙여 구현할 수도 있지만, 비트 단위로 보면 훨씬 단순한 규칙이 보인다. 비트 \(i\)는 그보다 아래 비트가 모두 1일 때만 뒤집힌다.
4비트 카운터가 0111(7)에서 1000(8)으로 넘어갈 때 뒤집히는 비트 수는? 모든 비트가 정확히 같은 순간에 뒤집히지 않고 아래 비트부터 차례로 뒤집힌다면, 중간에 어떤 값들이 잠깐 보일까?
답 보기
네 비트가 모두 뒤집힌다. 아래 비트부터 차례로 바뀌면 0111 → 0110 → 0100 → 0000 → 1000을 거친다. 6, 4, 0이라는 엉뚱한 값이 잠깐 나타나는 것이다. 이것이 바로 다음에 볼 리플 카운터의 모습이다.
토글 조건을 만드는 방법은 둘이다. 동기 카운터(Synchronous counter)는 모든 플립플롭이 같은 클럭을 받고, AND 사슬로 \(T_i\)를 만들어 XOR로 다음 값을 계산한다. 모든 비트가 같은 에지에서 \(t_{cq}\)만큼 뒤에 함께 바뀐다. 리플 카운터(Ripple counter, 비동기 카운터)는 더 교묘하다. 비트 \(i\)가 1→0으로 떨어지는 순간이 곧 비트 \(i+1\)이 뒤집혀야 하는 순간이므로, 각 비트의 출력을 다음 비트의 클럭으로 쓴다. 게이트가 하나도 없지만, 바뀜이 물결처럼 한 비트씩 번져 가므로 비트마다 \(t_{cq}\)가 쌓인다.
리플 카운터 vs 동기 카운터: 게이트 수준 시뮬레이션
시뮬레이터에서 리플 카운터를 \(n=6\)으로 늘려 보자. 011111 → 100000에서 여섯 비트가 \(t_{cq}\) 간격으로 차례로 넘어지며 31 → 30 → 28 → 24 → 16 → 0 → 32를 지나간다. 동기 카운터는 같은 순간에 모든 비트가 바뀌므로(빨간 띠 없음) 값을 읽는 쪽이 안심할 수 있다. 대신 클럭 주기를 줄이다 보면 동기 카운터는 AND 사슬이 끝나기 전에 다음 에지가 와서 세는 값 자체가 틀리기 시작한다. 리플 카운터는 첫 비트만 \(t_{cq}\) 안에 뒤집히면 되므로 아주 빠른 클럭도 셀 수는 있지만, 그 값을 다른 동기 회로가 읽을 수는 없다.
| 비트 수 n | 리플: 정착 \(n\,t_{cq}\) | 동기(AND 사슬) 최소 주기 | 동기(AND 트리) 최소 주기 | 동기(트리) 최대 클럭 |
|---|---|---|---|---|
| 4 | 100 ps | 67 ps | 67 ps | 약 15 GHz |
| 8 | 200 ps | 107 ps | 77 ps | 약 13 GHz |
| 16 | 400 ps | 187 ps | 87 ps | 약 11 GHz |
| 32 | 800 ps | 347 ps | 97 ps | 약 10 GHz |
DG-7 기준 공정의 교육용 추정값: \(t_{cq}\)=25 ps(약 3.3 FO4), \(t_{\text{setup}}\)=10 ps, AND2 10 ps, XOR 12 ps. 실제 칩의 카운터는 배선과 클럭 스큐 여유까지 더해 이보다 느리게 잡는다.
리플 카운터는 게이트가 없어 전력이 적고 작다. 그래서 저속 분주기나 실시간 시계처럼 값을 "가끔만" 읽는 곳에 아직 쓰인다. 그러나 각 비트가 서로 다른 클럭 도메인이 되므로 정적 타이밍 분석이 어렵고, 출력을 조합 논리로 해독하면 위의 잘못된 값이 글리치로 새어 나간다. 동기 설계 규칙은 단순하다. 플립플롭의 클럭 핀에는 클럭만, 나머지는 모두 D 입력(또는 허용 신호)으로.
시프트 레지스터와 LFSR
플립플롭을 줄지어 놓고 각 출력을 다음 플립플롭의 입력에 이으면 시프트 레지스터(Shift register)가 된다. 클럭마다 모든 비트가 한 칸씩 옆으로 옮겨 간다. 직렬로 들어온 비트를 병렬로 펼치거나(SIPO, 직렬→병렬), 반대로 병렬 데이터를 한 줄로 내보내는(PISO) 직렬 통신의 기본 부품이고, 신호를 정확히 \(k\)클럭 늦추는 지연선이기도 하다. 끝을 처음으로 다시 이으면 1 하나가 빙빙 도는 링 카운터가 되는데, 이것이 곧 뒤에 나올 원핫 상태 레지스터다.
되먹임에 XOR을 넣으면 놀라운 일이 생긴다. 몇 개의 비트 위치(탭(Tap))를 골라 XOR한 값을 맨 앞에 넣는 선형 되먹임 시프트 레지스터(LFSR, Linear Feedback Shift Register)는, 탭을 잘 고르면 0을 뺀 모든 \(2^n-1\)개 상태를 한 번씩 다 거친 다음에야 처음으로 돌아온다. 이것을 최대 길이 수열(m-sequence)이라 한다.
| n | 탭 (XOR 피보나치형) | 되먹임 다항식 | 주기 \(2^n-1\) | 1 GHz로 한 바퀴 |
|---|---|---|---|---|
| 4 | 4, 3 | \(x^4+x^3+1\) | 15 | 15 ns |
| 8 | 8, 6, 5, 4 | \(x^8+x^6+x^5+x^4+1\) | 255 | 255 ns |
| 16 | 16, 15, 13, 4 | \(x^{16}+x^{15}+x^{13}+x^4+1\) | 65 535 | 약 66 µs |
| 32 | 32, 22, 2, 1 | \(x^{32}+x^{22}+x^2+x+1\) | 약 4.3×10⁹ | 약 4.3 s |
| 64 | 64, 63, 61, 60 | \(x^{64}+x^{63}+x^{61}+x^{60}+1\) | 약 1.8×10¹⁹ | 약 585년 |
LFSR이 사랑받는 이유는 값싸기 때문이다. \(n\)비트에 플립플롭 \(n\)개와 XOR 몇 개면 끝나고, 되먹임 경로에 XOR 한두 단만 있으므로 같은 비트 수의 이진 카운터보다 훨씬 빠르다. 쓰임새는 넓다.
- 의사 난수(Pseudo-random): 출력 비트열은 0과 1의 개수가 거의 같고(\(2^{n-1}\)개 대 \(2^{n-1}-1\)개) 자기상관이 작다. 통신의 스크램블러, 확산 스펙트럼 코드, 디더링에 쓴다. 단, 암호용으로는 안전하지 않다.
- CRC(Cyclic Redundancy Check): 데이터 비트를 XOR 되먹임에 섞어 넣으면 LFSR이 다항식 나눗셈의 나머지를 계산한다. 이더넷의 CRC-32가 바로 32비트 LFSR이다.
- 내장 자체 테스트(BIST): 칩 안에서 LFSR로 테스트 패턴을 만들고, 다른 LFSR(MISR)로 응답을 압축해 서명 하나와 비교한다(16장).
- 값싼 카운터: 정확한 이진값이 필요 없고 "\(N\)클럭이 지났는가"만 알면 되는 타이머는 LFSR로 만들고 끝 상태 하나만 비교한다.
위 그림처럼 탭들을 모아 XOR한 뒤 맨 앞에 넣는 구조를 피보나치형, 반대로 맨 끝 비트를 여러 탭 위치에 XOR로 뿌려 넣는 구조를 갈루아형이라 한다. 같은 다항식이면 같은 주기를 내지만, 갈루아형은 되먹임 경로에 XOR이 딱 한 단이라 탭이 많아도 빠르다. CRC 하드웨어는 보통 갈루아형이다. 한편 XNOR로 되먹이면 금지 상태가 "모두 0" 대신 "모두 1"이 되어, 리셋 직후 0인 레지스터에서 바로 출발할 수 있다.
상태도에서 회로로: "101" 검출기
카운터는 다음 상태 함수가 뻔했다. 일반적인 FSM은 요구 사항에서 출발해 다섯 단계를 거친다. 예제로 입력 비트열에서 "101"이 나타날 때마다 1을 내는 시퀀스 검출기(Sequence detector)를 만들자. "10101"처럼 겹치는 경우도 두 번 세는 겹침 허용 방식이다.
① 상태도. 상태는 "지금까지 본 꼬리가 목표의 앞부분과 얼마나 맞는가"로 정한다. S0: 아무것도 못 맞춤, S1: "1"까지 맞춤, S2: "10"까지 맞춤, S3: "101" 완성(출력 1). S3에서 1이 오면 "1"부터 다시(S1), 0이 오면 "10"까지 맞은 셈이므로 S2로 간다. 이 겹침 처리가 검출기 설계의 핵심이다.
② 상태표와 ③ 인코딩. 상태도를 표로 옮기고, 각 상태에 2비트 코드를 준다. 여기서는 가장 단순한 이진 인코딩 S0=00, S1=01, S2=10, S3=11을 쓴다.
| 현재 상태 | \(q_1 q_0\) | x | 다음 상태 | \(q_1^{+} q_0^{+}\) | 출력 y |
|---|---|---|---|---|---|
| S0 | 00 | 0 | S0 | 00 | 0 |
| S0 | 00 | 1 | S1 | 01 | |
| S1 | 01 | 0 | S2 | 10 | 0 |
| S1 | 01 | 1 | S1 | 01 | |
| S2 | 10 | 0 | S0 | 00 | 0 |
| S2 | 10 | 1 | S3 | 11 | |
| S3 | 11 | 0 | S2 | 10 | 1 |
| S3 | 11 | 1 | S1 | 01 |
④ 다음 상태 식. 표의 \(q_0^{+}\) 열을 보면 x 열과 똑같다. \(q_1^{+}\)은 세 행에서 1이고, 카르노 맵(2장)으로 묶으면 두 항이 된다. 출력은 상태가 S3일 때만 1이다.
⑤ 게이트. 식을 그대로 게이트로 옮기면 아래 회로가 된다. 선을 일일이 그리는 대신 같은 이름의 핀은 연결된 것으로 보고, 각 핀 이름의 색으로 지금 그 선의 논리값(1 0)을 표시했다.
시퀀스 검출기의 상태는 "목표 문자열의 접두사 중 지금의 꼬리와 맞는 가장 긴 것"이다. 길이 \(m\)인 목표면 무어는 상태 \(m+1\)개, 밀리는 \(m\)개면 충분하다. 문자열 검색 알고리즘 KMP의 실패 함수와 같은 생각이다. 상태 이름을 의미 있게 붙여 두면(S_none, S_1, S_10, S_101) 상태도와 Verilog 코드를 함께 읽기가 훨씬 쉽다.
무어 기계와 밀리 기계
출력을 상태만으로 정하면 무어 기계(Moore machine), 상태와 현재 입력으로 정하면 밀리 기계(Mealy machine)다. 같은 "101" 검출기를 밀리로 만들면 S3가 필요 없다. S2("10"까지 맞춤)에 있는데 입력이 1이면 그 자리에서 출력 1을 내면 되기 때문이다. 상태도에서 무어는 출력을 상태 동그라미 안에, 밀리는 화살표에 "입력/출력"으로 적는다.
"…1 0 1"의 마지막 1이 들어온 클럭 주기를 생각하자. 무어 검출기와 밀리 검출기 중 어느 쪽이 그 주기 안에 출력을 1로 올릴까? 다른 쪽은 언제 올릴까?
답 보기
밀리는 마지막 1이 입력에 나타나는 즉시(같은 주기 안에서, 조합 논리 지연만큼 뒤에) 출력이 1이 된다. 무어는 그 1을 클럭 에지에서 받아 S3로 넘어간 다음 주기에 출력이 1이 된다. 즉 밀리가 한 클럭 빠르다. 대신 밀리의 출력은 입력이 흔들리면 같이 흔들린다. 아래 그림에서 직접 보자.
| 무어 기계 | 밀리 기계 | |
|---|---|---|
| 출력 함수 | \(y=\lambda(s)\) | \(y=\lambda(s,x)\) |
| "101" 검출기 상태 수 | 4개 (플립플롭 2개) | 3개 (플립플롭 2개) |
| 입력 → 출력 반응 | 다음 클럭 (1주기 지연) | 같은 주기 (조합 지연만) |
| 출력의 깨끗함 | 클럭 직후 한 번만 바뀜 | 입력 글리치가 출력으로 샘 |
| 타이밍 경로 | 입력과 출력이 레지스터로 끊김 | 입력 → 출력 조합 경로가 생겨 앞뒤 블록과 이어짐 |
밀리 기계 둘을 이어 붙이면 앞 기계의 입력 → 출력 조합 경로가 뒤 기계의 입력 → 출력 경로와 이어져, 한 클럭 주기 안에 두 기계를 모두 통과해야 한다. 서로의 출력을 입력으로 받으면 아예 조합 루프가 생길 수도 있다. 그래서 큰 설계에서는 블록 경계의 출력을 레지스터로 끊는(무어 또는 "등록된 밀리") 규칙을 많이 쓴다. 반응이 한 클럭 늦어지는 대신 타이밍 분석이 블록 단위로 끝난다.
FSM 놀이터
이제 여러 FSM을 직접 돌려 보자. 프리셋을 고른 뒤 입력 버튼으로 이번 주기의 입력을 정하고 "클럭 ↑"을 누르면 상태가 한 칸 움직인다. 지금 상태는 진하게, 방금 지난 전이는 굵은 화살표로 보인다. 아래쪽 파형에는 지난 클럭들의 입력·상태·레지스터 비트·출력이 쌓인다. 편집 줄에서 전이 하나를 바꿔 나만의 기계를 만들어 볼 수도 있다.
FSM 놀이터: 상태도 · 상태 레지스터 · 파형
프리셋마다 생각해 볼 거리가 있다. 자판기는 무어 기계라 "배출" 상태에 한 클럭 머문 뒤 다음 동전을 받는다. 밀리로 만들면 상태가 줄지만 출력이 동전 입력 신호의 잡음에 노출된다. 신호등은 입력(차량 감지)을 보는 상태와 무조건 넘어가는 상태(황색)가 섞여 있다. 실제 신호등이라면 황색 상태에 머무는 시간을 세는 타이머 카운터가 하나 더 붙는다. 큰 FSM은 이렇게 작은 FSM(카운터)과 짝을 이루는 경우가 많다. 홀짝 검사기는 상태 2개, 플립플롭 하나, XOR 하나짜리 가장 작은 쓸모 있는 FSM이다.
상태 인코딩: 이진 · 원핫 · 그레이
상태도에는 이름만 있고 비트가 없다. 상태 \(N\)개에 어떤 코드를 줄지가 상태 인코딩(State encoding / assignment)이고, 이 선택이 플립플롭 수와 다음 상태 논리의 크기, 속도, 전력을 함께 바꾼다.
- 이진(Binary): 상태 번호를 그대로 2진수로. 플립플롭이 \(\lceil\log_2 N\rceil\)개로 가장 적다. 대신 다음 상태 식이 모든 상태 비트를 봐야 해서 식이 크고 깊어지기 쉽다.
- 원핫(One-hot): 상태마다 플립플롭 하나, 늘 정확히 하나만 1. 플립플롭이 \(N\)개로 많지만, "지금 S2인가?"가 플립플롭 하나를 보면 끝나므로 다음 상태 식이 "들어오는 화살표의 OR"로 짧고 얕다.
- 그레이(Gray): 이웃한 번호끼리 한 비트만 다른 코드. 상태가 주로 순서대로 흘러가는 기계(카운터, 순차 제어)에서 전이마다 한 비트만 바뀌어 스위칭 전력이 줄고, 비동기로 읽어도 중간값이 튀지 않는다. 클럭 도메인을 건너는 FIFO 포인터가 대표적인 예다(10장).
인코딩별 비용 비교: 플립플롭 · 리터럴 · LUT
| 인코딩 | 플립플롭 | 다음 상태 리터럴 | 출력 리터럴 | 최대 팬인 | LUT4 추정 |
|---|
결과는 생각보다 미묘하다. 상태가 2~4개인 작은 기계에서는 이진(또는 그레이)이 플립플롭도 리터럴도 적다. 원핫은 식의 개수가 상태 수만큼 늘어 리터럴 합계가 커진다. 그런데 원핫 식 하나의 크기는 그 상태로 들어오는 화살표 수에만 달려 있고 상태 수 \(N\)과 무관하다. 모듈로-8 카운터를 보면 이진 식은 상태 비트 셋과 입력을 모두 보지만(팬인 4), 원핫 식은 "이전 상태 비트 · en + 지금 상태 비트 · en′" 꼴로 늘 변수 셋이다. 상태가 수십 개로 늘면 이진의 다음 상태 식은 \(\log_2 N\)개 상태 비트와 입력을 모두 엮은 깊은 논리가 되고, 원핫은 여전히 얕다.
이것이 FPGA에서 원핫을 선호하는 이유다. FPGA의 논리 칸은 "4~6입력 LUT 하나 + 플립플롭 하나"로 짝지어져 있어 플립플롭은 사실상 공짜이고, 비용은 LUT 개수보다 LUT를 몇 단 거치느냐(속도)로 정해지는 경우가 많다. 팬인이 LUT 입력 수 안에 들면 식 하나가 LUT 한 단으로 끝난다. 덤으로 출력 해독도 "q_S3 하나 보기"로 끝난다. 반대로 ASIC에서는 플립플롭이 NAND2 대여섯 개 면적이고 클럭 전력도 먹으므로, 상태가 많으면 이진(또는 그 변형)이 유리한 경우가 많다.
FPGA·ASIC 합성 도구는 Verilog 코드에서 FSM 패턴(상태 레지스터 + case 문)을 알아보고 인코딩을 다시 고르기도 한다. FPGA 도구는 상태가 많지 않으면 기본으로 원핫을 고르는 경우가 많고, 속성(예: fsm_encoding)으로 직접 지정할 수 있다. 그러니 코드에서는 상태를 localparam 이름으로 쓰고 숫자에 의미를 싣지 않는 습관이 좋다. 반대로 상태 비트를 칩 밖으로 내보내거나 다른 블록이 비트 패턴을 직접 해석한다면, 도구가 바꾸지 못하게 인코딩을 고정해야 한다.
상태 최소화와 불법 상태 복구
요구 사항을 따라 상태도를 그리다 보면 하는 일이 똑같은 상태가 둘 생기기 쉽다. 두 상태가 모든 입력 열에 대해 같은 출력 열을 낸다면 바깥에서는 구별할 수 없으므로 하나로 합쳐도 된다. 이런 상태를 동치 상태(Equivalent states)라 한다. 찾는 방법은 분할 정제(Partition refinement)다. 처음에는 출력이 같은 상태끼리 한 묶음으로 두고, 같은 묶음 안에서 어떤 입력에 대해 "다음 상태가 다른 묶음으로 가는" 상태를 떼어 낸다. 더는 쪼개지지 않을 때 남은 묶음 하나하나가 최소 기계의 상태가 된다.
상태를 합치면 플립플롭이 줄 수도 있고(6개 → 3개면 3비트 → 2비트), 줄지 않더라도 다음 상태 논리가 단순해진다. 다만 상태 수가 \(\lceil\log_2 N\rceil\)의 경계를 넘지 않으면 플립플롭 수는 그대로라는 점도 기억하자.
상태를 \(k\)비트로 인코딩하면 \(2^k\)개 코드 중 \(N\)개만 쓰이고 나머지는 불법 상태(Illegal / unreachable state)다. 정상 동작에서는 절대 들어가지 않지만, 전원 투입 직후의 무작위 초기값, 우주선·알파 입자가 일으키는 소프트 에러(16장), setup 위반으로 생긴 준안정이 레지스터를 불법 코드에 떨어뜨릴 수 있다. 그때 기계가 스스로 빠져나오지 못하면 시스템이 멈춘다.
- 리셋은 반드시: 모든 상태 레지스터에 리셋을 달아 알려진 상태(보통 IDLE)에서 출발한다. 비동기 리셋은 클럭 없이도 즉시 걸리지만, 풀릴 때는 클럭에 동기화해서 풀어야 플립플롭마다 다른 클럭에서 출발하는 사고를 막는다(리셋 동기화기).
- default 가지: Verilog의
case에default: next = IDLE;를 두면 쓰지 않는 코드에서 IDLE로 돌아오는 논리가 생긴다. 단, 합성 도구가 "도달 불가능"을 이유로 이 논리를 지울 수 있으므로 안전이 중요하면 "safe FSM" 옵션을 켠다. - 원핫의 함정: 원핫은 불법 코드가 \(2^N - N\)개로 압도적으로 많다. 위 그림처럼 복구 항을 넣거나, "1의 개수가 1이 아니면 리셋"하는 검사기를 단다. 우주·차량용 칩은 상태 레지스터를 세 벌 두고 다수결로 고치는 TMR까지 쓴다.
Verilog로 쓰는 FSM 맛보기
실제 설계에서는 상태도를 손으로 게이트까지 옮기지 않고 하드웨어 기술 언어로 적는다. 합성 도구가 ③~⑤단계(인코딩, 식 최소화, 게이트 매핑)를 대신한다. 코드 구조는 그림 9-1의 세 덩어리를 그대로 따르는 것이 정석이다. 상태 레지스터는 always @(posedge clk) 블록 하나, 다음 상태 논리는 always @(*) 조합 블록 하나, 출력 논리는 assign이나 또 하나의 조합 블록이다. 자세한 코딩 패턴과 합성 과정은 17장에서 다룬다.
module det101 ( input wire clk, rst_n, x, output wire y, // 무어 출력 output wire z // 밀리 출력 (비교용) ); // 상태 이름. 숫자에 의미를 싣지 않는다 → 도구가 인코딩을 바꿔도 안전 localparam [1:0] S0 = 2'b00, S1 = 2'b01, S2 = 2'b10, S3 = 2'b11; reg [1:0] state, next; // ① 상태 레지스터: 비동기 리셋(풀림은 바깥의 리셋 동기화기가 맞춘다) always @(posedge clk or negedge rst_n) if (!rst_n) state <= S0; else state <= next; // ② 다음 상태 논리: 순수 조합 회로 always @(*) begin next = S0; // 기본값: 래치 추론 방지 case (state) S0: next = x ? S1 : S0; S1: next = x ? S1 : S2; S2: next = x ? S3 : S0; S3: next = x ? S1 : S2; // 겹침 허용: "101" 뒤의 0은 "10" default: next = S0; // 불법 상태 → 복구 endcase end // ③ 출력 논리 assign y = (state == S3); // 무어: 상태만 본다 assign z = (state == S2) & x; // 밀리: 입력도 본다 (S3 없이도 검출) endmodule
몇 가지 습관이 버그를 크게 줄인다. 상태 레지스터 블록에서만 논블로킹 대입(<=)을 쓰고, 조합 블록은 블로킹 대입(=)과 맨 앞의 기본값으로 시작한다. 기본값을 빼먹고 어떤 가지에서 next를 대입하지 않으면, 합성 도구는 "이전 값을 기억하라"는 뜻으로 읽어 원치 않는 래치를 만든다. 출력도 조합 블록에서 만든다면 같은 규칙을 따른다.
위처럼 세 덩어리를 나누어 쓰는 방식을 흔히 "3-프로세스 스타일"이라 부른다. 다음 상태와 출력을 한 조합 블록에 합친 2-프로세스, 모두를 클럭 블록 하나에 넣은 1-프로세스도 쓰인다. 1-프로세스에서는 출력도 레지스터를 거치므로 글리치가 없지만, 출력이 한 클럭 늦어진다는 것을 잊고 쓰면 무어·밀리 타이밍이 꼬인다. 어느 스타일이든 결국 합성된 회로는 그림 9-1의 일반형이다.
핵심 정리
- 모든 동기 순차 회로는 상태 레지스터 + 다음 상태 논리 \(\delta(s,x)\) + 출력 논리로 나뉜다. 되먹임은 반드시 플립플롭을 지나고, 상태는 클럭마다 한 걸음씩 바뀐다.
- 이진 카운터에서 비트 \(i\)는 아래 비트가 모두 1일 때 뒤집힌다. 리플 카운터는 게이트 없이 비트마다 \(t_{cq}\)가 쌓여 \(n\,t_{cq}\) 동안 잘못된 중간값을 내고, 동기 카운터는 모든 비트가 함께 바뀌지만 토글 조건 논리가 클럭 주기를 정한다.
- LFSR은 시프트 레지스터에 XOR 되먹임을 단 것으로, 원시 다항식의 탭이면 \(2^n-1\)개 상태를 모두 도는 최대 길이 수열을 낸다. 의사 난수, CRC, BIST에 쓴다.
- FSM 설계 절차는 상태도 → 상태표 → 인코딩 → 다음 상태·출력 식 → 게이트다. 시퀀스 검출기의 상태는 "목표의 접두사 중 지금 맞은 가장 긴 것"이다.
- 무어는 출력이 상태만 보고 한 클럭 늦지만 깨끗하다. 밀리는 상태가 적고 같은 주기에 반응하지만 입력 글리치와 긴 조합 경로를 출력으로 넘긴다.
- 이진 인코딩은 플립플롭이 \(\lceil\log_2N\rceil\)개로 적고, 원핫은 \(N\)개지만 식이 얕아 FPGA에 맞으며, 그레이는 순차적 전이에서 한 비트만 바꾼다.
- 동치 상태는 분할 정제로 합친다. 리셋,
default복구, 원핫 검사로 불법 상태에서 빠져나오는 길을 반드시 만든다.
확인 퀴즈
Q1. 상태가 11개인 FSM을 이진 인코딩과 원핫 인코딩으로 만들 때 필요한 플립플롭 수는 각각?
Q2. 6비트 리플 카운터의 \(t_{cq}\)가 25 ps일 때, 011111 → 100000 전이에서 최종 값이 자리 잡기까지 걸리는 시간에 가장 가까운 것은?
Q3. 무어 기계와 밀리 기계에 대한 설명으로 옳은 것은?
Q4. 4비트 XOR 되먹임 LFSR이 최대 길이 탭으로 동작한다. 리셋 직후 레지스터가 0000이라면 어떻게 될까?
Q5. FPGA에서 원핫 인코딩이 자주 선호되는 가장 큰 이유는?
Q6. Verilog의 다음 상태 조합 블록 always @(*)에서 case의 일부 가지가 next에 값을 대입하지 않고, 블록 앞에 기본값도 없다. 합성 결과로 생기기 쉬운 문제는?
next = S0;(또는 next = state;) 같은 기본값을 두어 막는다.