Chapter 17

HDL에서 게이트까지

지금까지 우리는 트랜지스터를 손으로 이어 인버터를 만들고, 게이트를 골라 덧셈기와 SRAM을 지었다. 그런데 오늘날 칩 하나에는 트랜지스터가 수백억 개 들어간다. 이것을 사람이 회로도로 그린다면 수만 년이 걸린다. 실제 설계자는 회로를 그리지 않고 글로 쓴다. Verilog 같은 하드웨어 기술 언어로 "무엇을 해야 하는지"를 적으면, 합성 도구가 그 글을 게이트와 플립플롭의 그물로 바꾼다. 이 장에서는 코드 한 줄이 어떤 하드웨어가 되는지 짚어 보고, 아주 작은 합성기를 직접 돌려 식이 게이트가 되는 과정을 들여다본 뒤, 만든 회로가 정말 맞는지 확인하는 검증까지 따라간다.

회로도를 손으로 그리지 않는 이유

1971년의 첫 마이크로프로세서 인텔 4004는 트랜지스터가 약 2,300개였다. 설계자들은 커다란 모눈종이에 트랜지스터를 하나하나 그리고 손으로 마스크를 잘랐다. 50여 년이 지난 지금 노트북용 칩은 트랜지스터가 약 1,000억 개다. 개수가 4천만 배 늘었는데 설계 팀은 기껏해야 수백 배 커졌을 뿐이다. 그 차이를 메운 것이 추상화(Abstraction)와 자동화다.

이 책의 앞 장들은 사다리를 한 칸씩 올라왔다. 트랜지스터를 묶어 게이트를 만들고(5장), 게이트를 묶어 플립플롭과 FSM을(8장, 9장), 다시 덧셈기와 ALU를(11장, 12장) 만들었다. 칸을 하나 오를 때마다 아래 칸의 세부는 "이미 검증된 부품"으로 접어 둔다. 그 꼭대기에서 설계자가 손에 쥐는 도구가 하드웨어 기술 언어(HDL, Hardware Description Language)다. HDL로 쓰는 추상화 수준을 RTL(Register-Transfer Level)이라 부른다. "클럭마다 레지스터의 값이 어떤 조합 논리를 거쳐 다음 레지스터로 옮겨 가는가"만 적는 수준이다.

게이트 등가 수(약 ÷4)—
회로도 쪽수 (쪽당 100게이트)—
한 사람이 손으로 그리면—
그림 17-1. 해 보기슬라이더로 트랜지스터 수를 바꿔 보자. 가로축은 로그 눈금이다. 게이트 하나를 그리고 잇는 데 30초, 하루 8시간·한 해 250일 일한다고 가정했다. 4004는 몇 주 거리지만, 오늘날의 칩은 인류 역사보다 긴 시간이 든다. 점은 대표 칩의 대략적인 트랜지스터 수(연도)다.

손으로 그리지 않는 이유가 시간만은 아니다. 회로도는 구조를 적은 것이고, RTL은 의도를 적은 것이다. "4비트 카운터"를 RTL로 쓰면 한 줄이지만, 회로도에는 그 의도가 어디에도 쓰여 있지 않다. 의도가 글로 남아 있으면 도구가 같은 의도를 더 빠른 회로, 더 작은 회로, 다른 공정의 회로로 몇 번이고 다시 만들어 낼 수 있다. 공정을 7 nm에서 5 nm로 옮길 때 RTL은 거의 그대로 두고 합성만 다시 한다.

언어는 두 가지 일을 한다

Verilog는 1984년 시뮬레이션 언어로 태어났다. 그래서 문법의 상당 부분(지연 #10, initial, 파일 입출력, $display)은 하드웨어가 될 수 없고 시뮬레이션에서만 의미가 있다. 합성 도구가 하드웨어로 바꿀 수 있는 부분을 합성 가능한 부분 집합(Synthesizable subset)이라 한다. 2005년에 나온 SystemVerilog는 Verilog에 logic, always_comb/always_ff 같은 설계 의도 표시와 강력한 검증 기능(클래스, 어서션, 커버리지)을 더했다. VHDL은 같은 역할을 하는 또 다른 언어로, 유럽과 국방·항공 분야에서 많이 쓴다.

Verilog의 기본: 코드 한 줄이 하드웨어 한 조각

소프트웨어 언어처럼 생겼지만 HDL은 "차례로 실행되는 명령"이 아니라 동시에 존재하는 부품과 배선을 적는다. 이 감각이 가장 중요하다. 아래 모듈은 4비트 입력 두 개 중 하나를 고르는 MUX, 그 결과를 클럭마다 붙잡는 레지스터, 레지스터 값의 패리티를 내는 XOR 트리로 이루어져 있다.


          
코드 줄 이동
그림 17-2. 해 보기코드의 줄을 누르거나 버튼으로 한 줄씩 내려가 보자. 오른쪽 회로도에서 그 줄이 만들어 내는 하드웨어가 밝게 강조된다. 회로도의 부품을 눌러도 해당 줄을 찾아 준다. 굵은 선과 사선 위의 숫자 4는 4비트 버스다.

눈여겨볼 점은 순서다. assign par = ^q;가 always_ff보다 위에 있어도 아무 문제가 없다. 하드웨어는 모두 동시에 존재하기 때문이다. 코드의 줄 순서가 의미를 갖는 곳은 always 블록 안쪽뿐이고, 그 안에서도 규칙이 미묘하다(4절).

reg는 레지스터가 아니다

Verilog의 가장 유명한 오해다. reg는 "always 블록에서 값을 받는 변수"라는 문법상의 구분일 뿐, 그것이 플립플롭이 될지 그냥 선이 될지는 어떤 블록에서 어떻게 대입하느냐가 정한다. always @(*) 안에서 대입한 reg는 조합 논리의 선이 된다. 이 혼란 때문에 SystemVerilog는 logic 하나로 통일하고, 의도는 always_comb/always_ff/always_latch로 표시하게 했다. 의도와 다른 하드웨어가 추론되면 도구가 경고를 낸다.

비트 벡터와 연산자

Verilog 연산자는 C와 비슷해 보이지만 하드웨어 비용은 제각각이다. 비트 연산 & | ^ ~는 비트마다 게이트 하나씩, 서로 독립이다. 상수만큼의 시프트 << 1과 연결 {a, b}는 게이트가 하나도 없다. 선을 옆으로 옮겨 꽂을 뿐이다. 반면 +는 캐리가 비트를 타고 흐르는 덧셈기(11장)가 되고, ==는 XNOR 다발과 AND 트리가 된다. 단항 &a, ^a 같은 축약 연산자(Reduction operator)는 벡터의 모든 비트를 한 게이트 트리로 모은다.

연산자
값 바꾸기
결과—
추론되는 하드웨어—
게이트 수(4비트)—
그림 17-3. 해 보기a와 b의 비트 칸을 눌러 0과 1을 바꾸고 연산자를 골라 보자. 선이 결과 비트의 어느 입력 비트에서 오는지 보여 준다. 비트 연산은 자기 자리끼리만 이어지고, 덧셈은 캐리(주황 화살표)가 아래 자리에서 위 자리로 흐른다. 시프트와 연결은 선만 바뀌고 게이트는 0개다.

조합 논리 코딩과 래치 추론의 함정

always_comb 블록은 "입력이 바뀌면 출력을 다시 계산하라"는 뜻이다. 그런데 어떤 조건에서 출력에 아무것도 대입하지 않으면 어떻게 될까? Verilog의 의미로는 그 변수가 이전 값을 그대로 유지한다. 값을 유지하려면 기억 소자가 필요하다. 합성 도구는 어쩔 수 없이 래치(Latch)를 넣는다. 이것이 악명 높은 래치 추론(Latch inference)이다.

래치 자체가 나쁜 소자는 아니다(8장). 문제는 의도하지 않은 래치다. 타이밍 분석이 까다로워지고, 글리치가 래치를 통과해 엉뚱한 값이 붙잡히며, 무엇보다 설계자가 "조합 회로"라고 믿는 곳에 상태가 숨어 버린다.


          
코드 모양
입력 토글
합성 결과—
합성 도구 메시지—
그림 17-4. 해 보기코드 모양을 바꾸면 합성 결과 회로가 MUX와 래치 사이를 오간다. en·d·b 버튼으로 입력을 바꾸며 아래 파형의 y를 보자. "else 없음"에서는 en이 0일 때 y가 b를 따르지 않고 옛값을 붙든다. 처음 en이 한 번도 1이 된 적 없을 때 y는 알 수 없는 값 X(빨간 빗금)다.
case문에서 default를 빼먹으면?

2비트 sel로 네 경우 중 세 경우(2'b00, 2'b01, 2'b10)만 case에 적고 default를 쓰지 않았다. always_comb 안이다. 어떤 하드웨어가 나올까?

답 보기

래치가 나온다. sel = 2'b11일 때 출력에 대입하는 문장이 없으므로 그 경우에는 옛값을 유지해야 하기 때문이다. 해결책은 위 그림의 세 번째 방식처럼 블록 맨 앞에서 기본값을 먼저 대입하거나, default 가지를 반드시 두는 것이다. "이 경우는 절대 오지 않는다"는 것을 알면 default: y = 'x;로 적어 도구가 마음대로 최적화하게 할 수도 있다(단, 시뮬레이션에서 X가 퍼지는지 꼭 확인).

안전한 조합 논리 코딩 규칙을 정리하면 이렇다. ① 모든 조건 가지에서 모든 출력에 대입하거나, 블록 맨 앞에서 기본값을 대입한다. ② case에는 default를 둔다. ③ 감지 목록을 손으로 적지 말고 always_comb(또는 always @(*))를 쓴다. 손으로 적다 신호 하나를 빠뜨리면 시뮬레이션은 래치처럼 동작하는데 합성 결과는 조합 회로가 되어, 시뮬레이션과 실제 칩이 다르게 동작한다.

블로킹(=)과 논블로킹(<=) 대입

always 블록 안의 대입에는 두 종류가 있다. 블로킹 대입(Blocking assignment, =)은 소프트웨어처럼 그 줄에서 곧바로 값을 바꾸고 다음 줄은 바뀐 값을 본다. 논블로킹 대입(Nonblocking assignment, <=)은 오른쪽 값을 지금 계산해 두기만 하고, 실제 갱신은 그 시각의 모든 문장이 끝난 뒤 한꺼번에 한다. 플립플롭 여러 개가 같은 클럭 에지에서 "동시에" 옛값을 보고 새 값을 받는 실제 하드웨어와 맞는 쪽은 논블로킹이다.


          
대입 방식
시뮬레이션
그림 17-5. 해 보기3단 시프트 레지스터를 두 방식으로 써 보았다. "한 문장 실행"으로 클럭 에지 한 번 동안 시뮬레이터가 문장을 하나씩 처리하는 모습을 보자. 논블로킹에서는 오른쪽 값이 예약 칸에 쌓였다가 마지막에 한꺼번에 들어간다. 블로킹에서는 d가 한 에지 만에 q3까지 뚫고 지나간다. 아래 그림은 각 코드가 합성되는 하드웨어다.
블로킹인데 순서만 뒤집으면?

블로킹으로 쓰되 줄 순서를 q3 = q2; q2 = q1; q1 = d;로 뒤집었다. 이 회로는 시프트 레지스터일까?

답 보기

그렇다. 뒤에서부터 대입하면 각 줄이 아직 바뀌지 않은 옛값을 읽으므로 우연히 시프트 레지스터가 된다. 하지만 이것은 문장 순서에 기대는 깨지기 쉬운 코드다. 누군가 줄 순서를 정리하는 순간 하드웨어가 바뀐다. 그래서 업계 규칙은 단순하다. 순차 논리(always_ff)에는 <=, 조합 논리(always_comb)에는 =. 한 블록 안에서 둘을 섞지 않는다.

왜 시뮬레이터는 이런 이상한 두 단계 규칙을 갖게 되었을까? 시뮬레이터는 한 번에 한 문장만 처리하는 순차 프로그램이지만, 흉내 내야 할 하드웨어는 수백만 개의 플립플롭이 정확히 같은 순간에 움직인다. 논블로킹은 "지금 시각의 모든 읽기를 끝낸 다음 쓰기"를 보장해, 블록이 어떤 순서로 실행되든 같은 결과를 낸다. 서로 다른 always 블록 사이의 실행 순서는 언어 표준이 정하지 않으므로, 블로킹으로 쓴 플립플롭은 시뮬레이터마다 다른 결과(경쟁 상태, Race condition)를 낼 수도 있다.

미니 합성기: 식이 게이트가 되기까지

논리 합성(Logic synthesis) 도구는 RTL을 받아 목표 공정의 표준 셀로 이루어진 게이트 수준 넷리스트(Gate-level netlist)를 내놓는다. 실제 도구는 수백만 줄짜리 프로그램이지만, 뼈대는 네 단계로 정리된다.

  1. 파싱과 정교화(Parsing, Elaboration): 글을 읽어 식 트리(구문 트리)로 만들고, 모듈 계층과 매개변수를 펼친다.
  2. 직역(Translation): 연산자마다 일반 게이트 하나를 대응시킨 기술 독립 넷리스트를 만든다.
  3. 논리 최적화(Logic optimization): 상수 전파, 중복 제거, 불 대수 재구성으로 같은 기능의 더 작은 회로를 찾는다.
  4. 기술 매핑(Technology mapping): 목표 공정의 셀 라이브러리에 있는 셀들로 회로를 덮는다.

아래 시뮬레이터는 Verilog의 아주 작은 부분 집합만 이해하는 장난감 합성기다. 1비트 신호의 assign 이름 = 식; 문장을 여러 개 쓸 수 있고, 연산자는 ~ & | ^ ~^ ?:와 괄호, 상수 1'b0 1'b1을 쓴다. 대입되지 않은 이름은 모두 입력으로 본다(최대 6개). 앞에서 대입한 이름을 뒤 식에서 쓰면 내부 선(wire)이 된다.

SIMULATOR

미니 합성기: 파싱 → 넷리스트 → 최적화 → 기술 매핑

—
예제
합성 단계
최적화 패스
셀 라이브러리
입력 / 출력—
게이트 수—
트랜지스터 수—
논리 깊이—
등가 검사—
진리표 — 행을 누르면 그 입력값으로 회로도의 선 색이 바뀐다(1·0).
교육용 모델: 트랜지스터 수는 정적 CMOS 기준 INV 2, NAND2·NOR2 4, AND2·OR2 6, XOR2·XNOR2 12, MUX2 12개. 논리 깊이는 입력에서 출력까지 거치는 게이트의 최대 개수. "등가 검사"는 현재 단계의 넷리스트를 식 트리와 모든 입력 조합에서 비교한다. 실제 도구는 AIG(AND-인버터 그래프)와 컷 기반 매핑, 타이밍 구동 최적화를 쓴다.

먼저 다수결 예제를 단계별로 넘겨 보자. ① 식 트리는 괄호와 우선순위대로 연산자를 묶은 것이다. Verilog의 우선순위는 ~ > & > ^ > | > ?: 순이다. ② 직역은 트리의 노드마다 게이트 하나를 둔다. ④ 표준 셀로 매핑해도 여기서는 줄 것이 별로 없지만, 라이브러리를 NAND2+INV로 바꾸면 AND-OR 구조가 NAND-NAND로 깔끔하게 바뀌어 트랜지스터가 오히려 준다. 2장에서 본 "NAND만으로 모든 회로를 만든다"가 실제 매핑에서 쓰이는 모습이다.

상수 전파 예제는 쓸데없는 논리가 얼마나 빨리 사라지는지 보여 준다. \(x \,\&\, 1 = x\), \(x \,\&\, 0 = 0\), \(x \oplus x = 0\) 같은 규칙을 반복 적용하면 긴 식이 선 하나로 줄어든다. 실제 설계에서 이런 일은 흔하다. 매개변수로 기능을 끈 블록, 0으로 묶어 둔 테스트 입력, 쓰지 않는 출력이 모두 상수 전파와 죽은 논리 제거(Dead-logic removal)로 사라진다. 중복 식 예제에서는 a & b와 b & a가 같은 게이트로 합쳐지고(교환 법칙을 아는 구조적 해싱), ~~c가 선으로 바뀐다. 전가산기에서는 합과 캐리가 a ^ b를 함께 쓰도록 공유된다.

XOR는 NAND로 바꾸면 비싸질까, 싸질까?

패리티 예제(XOR 세 개)를 표준 셀로 매핑했을 때와 NAND2+INV로 매핑했을 때 트랜지스터 수는 어떻게 다를까? 먼저 예측한 뒤 라이브러리를 바꿔 보자.

답 보기

XOR2 셀 하나(12개)가 NAND2 네 개(16개)로 바뀌므로 NAND만의 라이브러리에서는 더 비싸진다(36 → 48개). 반대로 다수결처럼 AND-OR 구조는 NAND-NAND가 더 싸다. 그래서 실제 라이브러리에는 수백 가지 셀이 있고, 매핑 도구는 회로의 각 부분을 가장 싼(또는 가장 빠른) 셀로 덮는다. 다음 절의 주제다.

합성할 수 없는 코드, 시뮬레이션과 다른 코드

#5 y = a; 같은 지연, initial 블록, $random, 실행 횟수가 변수로 정해지는 루프는 하드웨어가 되지 못한다. 도구는 지연을 조용히 무시하거나 오류를 낸다. 더 위험한 것은 합성은 되지만 시뮬레이션과 다르게 동작하는 코드다. 불완전한 감지 목록, 블로킹/논블로킹 혼용, x를 비교하는 casex가 대표적이다. 그래서 합성 후에는 넷리스트로 다시 시뮬레이션하거나 RTL과 넷리스트의 등가성 검사(Equivalence checking)를 수학적으로 돌린다. 위 시뮬레이터의 "등가 검사"가 그 축소판이다. 실제 도구는 모든 입력을 다 넣어 보는 대신 SAT 풀이기나 BDD로 증명한다.

기술 매핑과 표준 셀 라이브러리

합성의 마지막 단계는 넷리스트를 실제로 만들 수 있는 부품, 즉 표준 셀(Standard cell)로 덮는 것이다. 표준 셀은 높이가 모두 같은 미리 설계·검증된 레이아웃 조각으로, 같은 높이의 행에 벽돌처럼 나란히 놓인다. 공정마다 파운드리나 IP 회사가 셀 라이브러리를 만들어 주며, 보통 기능 100~200가지 × 크기 여러 가지 × 문턱 전압 여러 가지로 수천 개의 셀이 들어 있다.

같은 기능이라도 구동 세기(Drive strength)가 다른 셀이 여러 개 있다. 이름 끝의 X1, X2, X4는 트랜지스터 폭의 배수다. 폭이 넓으면 저항이 작아 큰 부하를 빨리 끌지만, 입력 커패시턴스와 면적, 누설 전류가 그만큼 커진다. 셀의 지연은 6장의 논리적 노력 모델로 가늠할 수 있다.

$$d = \tau\left(p + g\,h\right) = \tau\left(p + \frac{C_{out}}{X\cdot C_{inv}}\right),\qquad h = \frac{C_{out}}{C_{in}},\quad C_{in} = g\,X\,C_{inv}$$
\(\tau\): 기준 지연(DG-7에서 약 1.5 ps), \(p\): 기생 지연(INV 1, NAND2 2, AOI22 4), \(g\): 논리적 노력, \(h\): 전기적 노력(팬아웃), \(X\): 구동 세기 배수, \(C_{inv}\): 단위 인버터 입력 커패시턴스(약 0.2 fF).

식을 보면 같은 부하에서 X를 키우면 \(C_{out}/X\) 항이 줄어 빨라진다. 그러나 이 셀의 입력 커패시턴스 \(C_{in}\)도 X배가 되어 앞 단의 부하가 커진다. 셀 하나만 보면 크게 쓸수록 이득이지만, 경로 전체로 보면 알맞은 크기가 따로 있다.

NAND2 크기
지연 계산 범위
지연—
입력 커패시턴스—
면적 (약)—
이 부하에서 가장 빠른 크기—
그림 17-6. 해 보기부하를 늘리며 네 크기의 지연 곡선을 비교해 보자. "이 셀만"에서는 언제나 큰 셀이 빠르다. "앞 단 INV_X1 포함"으로 바꾸면 큰 셀의 무거운 입력이 앞 단을 느리게 해서, 부하가 작을 때는 X1·X2가 이긴다. 합성 도구는 이런 계산을 경로의 모든 셀에 대해 반복하며 크기를 고른다(Gate sizing).
셀기능트랜지스터면적 (µm², 약)입력 C (fF)p지연 @1 fF (ps)누설 (상대)
INV_X1\(\overline{A}\)20.0410.2019.01
INV_X2\(\overline{A}\)2 (폭 2배)0.0550.4015.32
INV_X4\(\overline{A}\)2 (폭 4배)0.0820.8013.44
NAND2_X1\(\overline{AB}\)40.0550.27210.51.3
NAND2_X4\(\overline{AB}\)4 (폭 4배)0.1371.0724.95
NOR2_X1\(\overline{A+B}\)40.0550.33210.51.6
AND2_X1\(AB\)60.0680.273+1≈ 142
AOI22_X1\(\overline{AB+CD}\)80.0820.40413.52
XOR2_X1\(A\oplus B\)120.1100.80413.53
MUX2_X1\(S\,?\,B:A\)120.1230.404≈ 143
DFF_X1상승 에지 D 플립플롭240.270.20—clk→Q ≈ 208

표 17-1. DG-7 교육용 셀 라이브러리(셀 높이 약 0.24 µm, 게이트 간격 약 57 nm 가정). 지연은 \(\tau(p + 5/X)\), 즉 1 fF 부하를 \(C_{inv}=0.2\) fF 기준으로 계산했다. 실제 7 nm급 라이브러리의 NAND2_X1 면적은 약 0.04~0.06 µm²다.

기술 매핑은 결국 덮기(Covering) 문제다. 넷리스트를 NAND2와 INV 같은 기본 조각으로 잘게 쪼갠 "주제 그래프"(Subject graph)를 만들고, 라이브러리의 각 셀을 "무늬"(Pattern)로 삼아 그래프를 빈틈없이 덮는다. 어떤 무늬로 덮느냐에 따라 면적과 지연이 달라진다. 트리 모양의 회로라면 동적 계획법으로 최적 덮기를 빠르게 찾을 수 있다(Keutzer의 DAGON, 1987).

덮는 방법
면적 (약)—
트랜지스터—
경로 지연—
이 부하의 승자—
그림 17-7. 해 보기같은 함수 \(y = ab + cd\)를 네 가지 셀 조합으로 덮어 보았다. 점선 상자가 셀 하나다. 면적은 AOI22+INV가 가장 작고, 부하가 작을 때는 NAND-NAND가 가장 빠르며, 부하가 크면 마지막 셀을 X4로 키운 쪽이 이긴다. 부하 슬라이더를 끌며 승자가 바뀌는 지점을 찾아보자. 단계마다 적힌 숫자는 τ 단위 지연이다.
합성 도구는 무엇을 최소화하나

실제 합성은 "타이밍 제약을 만족하는 가장 작은 회로"를 찾는다. 설계자는 SDC(Synopsys Design Constraints) 파일에 클럭 주기, 입출력 지연, 부하를 적는다. 도구는 먼저 면적 위주로 매핑한 뒤 정적 타이밍 분석(10장)으로 위반 경로를 찾아, 그 경로의 셀만 키우거나 빠른 구조(예: 리플 캐리 → 캐리 예측)로 바꾸고, 여유가 남는 경로는 작고 누설이 적은 고문턱(HVT) 셀로 되돌린다. 클럭 게이팅 셀 삽입(15장)과 스캔 체인 삽입(16장)도 이 단계에서 자동으로 이루어진다.

시뮬레이션과 검증

코드가 합성된다고 해서 맞는 것은 아니다. 칩은 한 번 만들면 고칠 수 없고, 마스크 한 벌에 수백억 원이 든다. 그래서 현대 칩 프로젝트에서 시간과 인력이 가장 많이 드는 일은 설계가 아니라 검증(Verification)이다. 기본 도구는 테스트벤치(Testbench)다. 검증 대상 설계(DUT, Design Under Test)를 감싸고 입력 자극을 넣으며, 출력을 기대값과 비교하는 하드웨어가 아닌 코드다.

module tb;
  logic clk = 0, rst, en;  logic [3:0] q, q_ref;
  bcd_counter dut (.clk, .rst, .en, .q);          // 검증 대상
  always #5 clk = ~clk;                           // 클럭 생성 (시뮬레이션 전용)
  always_ff @(posedge clk)                         // 기준 모델: 쉽게 쓴 '정답'
    q_ref <= rst ? 0 : !en ? q_ref : (q_ref == 9) ? 0 : q_ref + 1;
  always @(negedge clk)                            // 스코어보드: 비교
    if (q !== q_ref) $error("q=%0d, expected %0d", q, q_ref);
  // 어서션: q는 언제나 0~9 범위에 있어야 한다
  a_range: assert property (@(posedge clk) disable iff (rst) q <= 9);
  initial begin
    rst = 1; en = 0; @(posedge clk); @(posedge clk); rst = 0;
    repeat (20) begin en = $urandom_range(0,4) != 0; @(posedge clk); end
    $finish;
  end
endmodule

테스트벤치의 핵심 부품은 네 가지다. 자극 생성기는 입력을 만든다(위에서는 무작위 en). 기준 모델(Reference model)은 같은 기능을 하드웨어 구조와 무관하게 가장 쉽게 쓴 "정답"이다. 스코어보드는 둘의 출력을 매 사이클 비교한다. 어서션(Assertion)은 "언제나 성립해야 하는 성질"을 시간 위의 논리식으로 적은 것이다. 기대값을 몰라도 위반만은 즉시 잡는다. 아래 시뮬레이터는 BCD 카운터(0~9 반복)를 이 책의 게이트 수준 시뮬레이터 DG.Circuit로 실제 게이트와 플립플롭으로 만들고, 위 테스트벤치를 그대로 돌린다.

SIMULATOR

테스트벤치: BCD 카운터 검증과 버그 주입

버그 주입
자극
무작위 시드
시드 1
스코어보드 불일치—
첫 불일치—
어서션 q ≤ 9 위반—
상태 커버리지 (0~9)—
토글 커버리지—
—
교육용 모델: DUT는 XOR·AND·NOT·BUF 게이트 15개와 D 플립플롭 4개(지연 0.5, clk→Q 1, 클럭 주기 10 시간 단위). 다음 상태 = 리셋이면 0, en이면 q==9에서 0으로 돌아가고 아니면 +1. 플립플롭 초기값은 X(알 수 없음)이고 X는 게이트를 거치며 퍼진다. 커버리지는 "얼마나 시험했는가"의 척도로, 상태 커버리지는 방문한 카운터 값, 토글 커버리지는 0↔1을 모두 겪은 내부 신호의 비율이다.

버그를 하나씩 주입해 보자. 캐리 AND→OR은 오타 한 글자다. 스코어보드가 첫 불일치 사이클을 정확히 짚는다. 9 비교 실수는 9 대신 10(1010)에서 0으로 돌아가게 만든 버그로, 기대값을 몰라도 어서션이 "q=10"을 잡아낸다. 리셋 빠짐은 플립플롭이 X에서 벗어나지 못해 파형 전체가 빨간 빗금이 된다. 실제 칩이었다면 전원을 켤 때마다 임의의 값에서 시작했을 것이다. 4값 논리(0, 1, X, Z)를 쓰는 HDL 시뮬레이터가 이런 초기화 버그를 잡는 데 강한 이유다.

흥미로운 것은 en 무시 버그다. 자극을 "en 항상 1"로 바꾸면 이 버그는 전혀 드러나지 않는다. 테스트가 en=0인 경우를 한 번도 시험하지 않았기 때문이다. 버그를 찾으려면 자극이 설계의 구석구석을 건드려야 하고, 그 정도를 재는 것이 커버리지(Coverage)다. "en 드묾"으로 바꾸면 카운터가 9까지 가지 못해 상태 커버리지가 떨어지는 것도 보인다. 현대 검증은 무작위 자극을 대량으로 넣고 커버리지가 목표에 닿을 때까지 제약을 조정하는 커버리지 주도 제약 무작위 검증(Constrained-random, coverage-driven verification), 그리고 모든 입력을 수학적으로 따지는 형식 검증(Formal verification)을 함께 쓴다. 이를 체계화한 방법론이 UVM이다.

명세에서 테이프아웃까지

지금까지 본 단계를 한 줄로 이으면 디지털 칩 설계의 흐름이 된다. 이 책이 다루는 범위는 RTL과 합성까지이고, 넷리스트를 실리콘 위에 놓고 잇는 물리 설계는 시리즈의 DesignBook이 자세히 다룬다. 칩의 블록 구조와 아키텍처 결정은 SoCBook에 있다.

단계
그림 17-8. 해 보기상자를 누르거나 버튼으로 단계를 옮겨 보자. 파란 상자는 이 장에서 다룬 단계, 주황 상자는 DesignBook이 다루는 물리 설계 단계다. 아래쪽 화살표는 되돌아가는 고리다. 타이밍이 맞지 않으면 합성이나 RTL로 돌아가고, 검증에서 버그가 나오면 RTL을 고친다. 실제 프로젝트는 이 고리를 수백 번 돈다.

같은 RTL이 반드시 주문형 칩(ASIC, Application-Specific Integrated Circuit)이 되는 것은 아니다. FPGA(Field-Programmable Gate Array)는 이미 만들어진 칩 안에 작은 진리표 메모리인 LUT(Look-Up Table)와 플립플롭, 프로그래밍 가능한 배선을 수십만~수백만 개 깔아 둔 칩이다. 합성 도구는 회로를 표준 셀 대신 LUT로 덮고, 배선 스위치의 설정 비트를 만든다. \(k\)입력 LUT는 \(2^k\)비트의 SRAM에 진리표를 그대로 저장하므로, 입력이 \(k\)개인 함수라면 무엇이든 하나로 구현한다.

$$\#\{\,f:\{0,1\}^k\to\{0,1\}\,\} = 2^{2^k},\qquad k=4 \;\Rightarrow\; 2^{16} = 65{,}536\ \text{가지}$$
\(k\)입력 함수의 진리표는 \(2^k\)칸, 칸마다 0 또는 1이므로 가능한 함수의 수는 \(2^{2^k}\)다. 4입력 LUT 하나가 그 전부를 구현한다.
LUT에 넣을 함수
입력 토글
출력 y—
선택된 칸—
FPGA: LUT 트랜지스터(약)—
ASIC: 같은 함수 셀(약)—
그림 17-9. 해 보기왼쪽 16칸은 LUT 안의 설정 메모리다. 칸을 눌러 0/1을 바꾸면 어떤 4입력 함수든 만들 수 있다. 입력 a~d는 MUX 트리의 선택 신호가 되어 16칸 중 하나를 골라 출력으로 내보낸다(강조된 길). 같은 함수를 ASIC 셀로 만들면 트랜지스터가 몇 배에서 수십 배 적다. FPGA가 같은 공정의 ASIC보다 면적·전력은 수~수십 배 크고 속도는 몇 배 느린 대신, 마스크 없이 몇 분 만에 다시 프로그래밍된다.

그래서 선택은 물량과 시간의 문제다. 한두 해 안에 수백만 개 이상 팔릴 칩은 마스크 비용을 감수하고 ASIC으로 만든다. 소량 장비, 표준이 아직 바뀌는 통신 장비, 그리고 ASIC을 만들기 전 RTL을 실제 속도로 돌려 보는 프로토타이핑에는 FPGA를 쓴다. 대형 SoC 팀은 RTL을 FPGA 수십 개로 나눠 올리거나 전용 에뮬레이터(Emulator)로 돌려, 테이프아웃 전에 운영체제를 부팅해 본다.

실제 칩 설계의 규모

마지막으로 오늘날의 칩 설계가 얼마나 큰 일인지 수치로 보자. 아래 값은 공개 자료와 업계 조사의 대략값이다.

항목대략값비고
인텔 4004 트랜지스터 수약 2,300개1971년, 손으로 설계
스마트폰 SoC 트랜지스터 수약 150억~200억 개2023~2024년, 3 nm급
데이터센터 GPU 트랜지스터 수약 800억 개(단일 다이) ~ 2,000억 개(2다이)2022~2024년
대형 SoC의 표준 셀 인스턴스수억~10억 개 이상나머지 상당 부분은 SRAM
대형 SoC의 RTL 규모수백만~수천만 줄상당 부분은 재사용 IP
프로젝트 시간 중 검증 비중약 50~60%업계 조사(Wilson Research, 2022~2024년)
설계 엔지니어 1명당 검증 엔지니어약 1~1.5명ASIC 프로젝트 평균
첫 실리콘에 그대로 양산 가능한 비율약 15~25%나머지는 수정 후 재제작(리스핀)
블록 하나 합성 시간수 시간 ~ 하루수백만 셀 규모, 서버 여러 대
첨단 공정 마스크 한 벌 비용약 1,000만~3,000만 달러 이상3~5 nm급, 수십 장의 마스크

첫 실리콘이 그대로 성공하는 비율이 생각보다 낮다는 점에 주목하자. 실패 원인 1위는 꾸준히 논리·기능 버그다. 시간의 절반 이상을 검증에 쓰고도 그렇다. 이 장에서 본 래치 추론, 블로킹 대입, 리셋 누락 같은 실수는 하나하나는 사소하지만, 수천만 줄의 코드 어딘가에 숨으면 수백억 원짜리 리스핀이 된다. 좋은 코딩 규칙, 꼼꼼한 테스트벤치, 형식 검증과 등가성 검사가 디지털 설계 실력의 큰 부분인 이유다.

더 가 보려면

이 장의 미니 합성기를 실제 도구로 바꿔 보고 싶다면 오픈 소스 합성기 Yosys와 논리 최적화 엔진 ABC, 시뮬레이터 Verilator·Icarus Verilog를 써 보자. 오픈 PDK(SkyWater 130 nm 등)와 OpenROAD 흐름을 쓰면 RTL에서 GDS까지 개인 노트북으로 끝까지 가 볼 수 있다. 그 뒤의 배치·배선·STA·DRC/LVS 이야기는 DesignBook에서 이어진다. 게이트를 직접 놓고 이어 보고 싶다면 다음 장 디지털 회로 실험실로 가자.

핵심 정리

  1. 수십억 트랜지스터를 손으로 그릴 수는 없다. 설계자는 HDL로 RTL 수준의 의도를 쓰고, 합성 도구가 그것을 게이트와 플립플롭의 구조로 바꾼다.
  2. HDL 코드는 차례로 실행되는 명령이 아니라 동시에 존재하는 하드웨어다. assign·always_comb는 조합 논리, always_ff는 플립플롭, 상수 시프트와 연결은 배선만 된다.
  3. always_comb에서 어떤 가지가 출력에 대입하지 않으면 래치가 추론된다. 기본값을 먼저 대입하거나 else/default를 빠짐없이 둔다.
  4. 순차 논리에는 논블로킹 <=, 조합 논리에는 블로킹 =를 쓴다. 논블로킹은 모든 플립플롭이 같은 에지에서 옛값을 읽는 하드웨어와 맞는다.
  5. 합성은 파싱 → 직역 → 최적화(상수 전파, 중복 제거) → 기술 매핑으로 진행되며, 매핑은 셀 라이브러리로 회로를 덮는 문제다. 덮는 방법과 구동 세기에 따라 면적·지연이 맞바뀐다.
  6. 검증은 프로젝트 시간의 절반 이상을 차지한다. 테스트벤치는 자극·기준 모델·스코어보드·어서션으로 이루어지고, 커버리지가 "충분히 시험했는가"를 잰다.
  7. 흐름은 명세 → RTL → 검증 → 합성 → 물리 설계 → 서명 → 테이프아웃이다. FPGA는 같은 RTL을 LUT로 구현해 재프로그래밍성을 얻는 대신 면적·속도·전력을 내준다.

확인 퀴즈

Q1. always_comb begin if (sel) y = a; end 만 쓰고 else를 쓰지 않았다. 합성 결과로 가장 알맞은 것은?

sel=0일 때 y에 대입하는 문장이 없으므로 y는 옛값을 유지해야 한다. 기억 소자가 필요하므로 sel이 1일 때 투명한 D 래치가 추론된다. 대부분의 도구는 경고만 내고 합성은 진행한다.

Q2. always_ff @(posedge clk) begin q1 <= d; q2 <= q1; q3 <= q2; end에서 처음 q1=q2=q3=0, d=1로 고정하고 클럭 에지를 두 번 주었다. (q1, q2, q3)는?

논블로킹이므로 에지마다 한 칸씩 밀린다. 첫 에지 후 (1,0,0), 두 번째 에지 후 (1,1,0)이다. 블로킹 =로 썼다면 첫 에지에서 바로 (1,1,1)이 된다.

Q3. assign y = (a & 1'b0) | (b ^ 1'b1);을 상수 전파로 최적화하면?

\(a\,\&\,0 = 0\), \(b \oplus 1 = \overline{b}\), \(0 \,|\, \overline{b} = \overline{b}\). AND, OR, XOR 세 게이트가 인버터 하나로 줄어든다.

Q4. 같은 NAND2를 X1 대신 X4로 바꾸었다. 이 교체의 결과로 틀린 것은?

구동 세기는 트랜지스터 폭만 바꾸고 논리 기능은 같다. 그래서 합성 후 크기 조정은 등가성 검사를 그대로 통과한다. 대신 지연·면적·입력 부하·누설이 맞바뀐다.

Q5. 테스트벤치에서 자극을 "en 항상 1"로만 주었더니 en을 무시하는 버그를 놓쳤다. 이 상황을 미리 알려 줄 수 있었던 지표는?

커버리지는 자극이 설계의 어떤 경우를 건드렸는지 잰다. "en=0이 적어도 한 번 나왔는가"를 기능 커버리지 항목으로 두었다면 빈 칸이 드러나 테스트를 보강했을 것이다. 버그가 드러나지 않았다고 버그가 없는 것은 아니다.

Q6. 4입력 LUT 하나로 구현할 수 있는 서로 다른 불 함수의 수는?

진리표가 \(2^4 = 16\)칸이고 칸마다 0/1을 고를 수 있으므로 \(2^{16} = 65{,}536\)가지다. LUT는 진리표를 SRAM에 그대로 저장하므로 그 전부를 구현한다.