1. 모듈은 포트가 있는 상자입니다
Verilog는 디지털 회로를 글로 기술하는 하드웨어 기술 언어입니다. 겉모습은 C와 비슷하지만, 코드 한 줄은 명령이 아니라 회로의 연결을 뜻합니다. 대부분의 줄은 어떤 선과 게이트가 어떻게 이어지는지를 선언합니다.
Verilog 설계의 단위는 module입니다. 모듈은 이름, 바깥과 통하는 포트, 안쪽 동작으로
이루어집니다. 모듈 하나는 회로 블록 하나에 해당하고, 포트는 다른 블록과 연결되는 단자입니다.
포트는 방향이 있습니다. input은 밖에서 들어오는 신호, output은 밖으로 나가는 신호입니다.
모듈 안의 동작은 포트 이름으로만 바깥을 봅니다.
형식
module 모듈_이름 (포트 목록);
// 신호 선언
// 회로 기술
endmodule다음은 입력 두 개의 AND를 출력하는 가장 작은 모듈입니다.
module and_gate (input a, input b, output y);
assign y = a & b;
endmodule| 코드 | 의미 |
|---|---|
module and_gate (...) | and_gate라는 모듈을 시작하고 포트를 선언합니다 |
input a, input b | 1비트 입력 포트 두 개입니다 |
output y | 1비트 출력 포트입니다 |
assign y = a & b; | y가 언제나 a와 b의 AND라는 연결을 선언합니다 |
endmodule | 모듈을 닫습니다 |
주의. 모듈 본문은 위에서 아래로 한 번 실행되는 프로그램이 아닙니다. assign y = a & b;는
"지금 y에 값을 넣어라"가 아니라 "y는 항상 a & b와 같다"는 관계입니다. 시뮬레이터는
a나 b가 바뀔 때마다 y를 다시 계산합니다.
포트 하나는 기본적으로 비트 하나입니다. 여러 비트를 묶은 포트가 벡터이며, 벡터·비트 선택·연산자 장에서 다룹니다.
1.1 어휘 규칙
| 항목 | 규칙 | 예 |
|---|---|---|
| 대소문자 | 구분합니다 | Data와 data는 다른 이름입니다 |
| 한 줄 주석 | //부터 줄 끝까지 | // 출력 포트 |
| 여러 줄 주석 | /*부터 */까지 | /* 설명 */ |
| 문장 끝 | 단순 문장은 ;로, begin/end 블록은 end로 끝냅니다 | assign y = a; |
| 문장 묶기 | 문장 하나가 올 자리에 여러 문장을 넣을 때 begin과 end로 묶습니다 | initial begin ... end |
2. 포트의 방향·종류·폭
2.1 포트 목록 안에서 포트 선언하기
포트의 방향, 종류, 폭을 포트 목록 안에서 한 번에 선언하는 형식을 ANSI 형식이라고 합니다. 이 과정의 예제는 모두 이 형식을 씁니다.
형식
module 모듈_이름 (
방향 [종류] [폭] 포트_이름,
방향 [종류] [폭] 포트_이름
);
// 모듈 본문
endmodulemodule counter (
input clock,
input [7:0] limit,
output reg [7:0] count
);| 부분 | 의미 |
|---|---|
input, output | 포트 방향입니다 |
reg | 포트 종류입니다. 생략하면 wire입니다 |
[7:0] | 포트 폭입니다. 생략하면 1비트입니다 |
clock, limit, count | 포트 이름입니다. 모듈 본문에서 이 이름으로 신호를 읽고 씁니다 |
마지막 포트 뒤에는 쉼표를 쓰지 않습니다. 포트 목록을 닫는 ); 뒤에 본문이 이어집니다.
2.2 포트 방향과 종류
| 방향 | 모듈 안에서 | 상위 모듈에서 연결할 대상 |
|---|---|---|
input | 모듈이 읽기만 하는 net입니다 | 임의의 식 |
output | 기본은 net이고, output reg로 선언하면 변수입니다 | net |
inout | 양방향 net입니다 | net |
output을 assign으로 구동하면 wire로 두고, always나 initial 안에서 대입하면
output reg로 선언합니다. 이 구분은 net·변수·조합
always 장의 net과 변수 규칙입니다. 이 장의 출력은 모두 assign으로 구동하므로 wire로
둡니다.
참고. 오래된 코드에서는 포트 목록에 이름만 쓰고 방향과 폭을 본문에서 다시 선언하는 형식도 볼 수 있습니다. 두 형식은 같은 모듈을 만듭니다.
module and_old (a, b, y);
input a, b;
output y;
assign y = a & b;
endmodule2.3 최상위 모듈
다른 어떤 모듈도 인스턴스로 만들지 않는 모듈이 계층의 최상위 모듈입니다. 이 과정의 예제에서
최상위는 언제나 포트 없는 테스트벤치 tb입니다. Icarus는 아무도 사용하지 않는 모듈을 찾아
자동으로 최상위로 정합니다.
3. assign은 배선입니다
assign 왼쪽 = 오른쪽;은 오른쪽 식의 값을 왼쪽 신호에 항상 연결합니다. 오른쪽 입력이 바뀌면
왼쪽도 바로 따라 바뀝니다. 소프트웨어의 대입문처럼 한 번 실행되고 끝나는 문장이 아니라,
회로에 영구히 놓인 선입니다. 식 안의 ^는 XOR, &는 AND입니다.
3.1 AND 게이트 하나에서 반가산기로
1절의 and_gate는 assign 한 줄짜리 모듈입니다. 반가산기는 여기에 한 줄만 더하면 됩니다.
1비트 수 두 개를 더하면 결과는 0, 1, 2 가운데 하나이고, 2를 나타내려면 비트가 두 개
필요합니다. 2의 자리가 carry, 1의 자리가 sum입니다. 아래 표를 옮겨 적고, 다음 내용을 읽기
전에 마지막 두 열을 직접 채워 보세요.
a | b | a + b | carry | sum |
|---|---|---|---|---|
| 0 | 0 | 0 | ? | ? |
| 0 | 1 | 1 | ? | ? |
| 1 | 0 | 1 | ? | ? |
| 1 | 1 | 2 | ? | ? |
채운 열을 하나씩 게이트의 진리표로 읽어 봅니다.
확인 문제
직접 채운 carry 열과 진리표가 똑같은 2입력 게이트는 무엇입니까?
확인 문제
직접 채운 sum 열과 진리표가 똑같은 2입력 게이트는 무엇입니까?
결국 반가산기는 출력 이름을 carry로 바꾼 and_gate에 sum을 만드는 XOR 한 줄을 더한
회로입니다.
HalfAdder.v는 이 반가산기를 Verilog로 쓴 것입니다. 1비트 입력 두 개와
1비트 출력 두 개를 가지며, sum은 두 입력의 XOR이고 carry는 AND입니다. 포트 넷 개와
assign 두 줄이 설계 전부입니다. 포트 선언 순서가 a, b, sum, carry라는 점을 기억해
두세요.
// 두 비트의 합과 자리올림. 저장하는 상태는 없다.
module half_adder (
input a,
input b,
output sum,
output carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule4. 테스트벤치가 입력을 넣고 출력을 읽습니다
설계는 스스로 동작하지 않습니다. 두 번째 파일 Tb.v는 테스트벤치입니다. 테스트벤치는 포트가
없는 모듈로, 설계에 입력을 넣고 출력을 확인합니다. 반가산기를 dut라는 이름으로 인스턴스로
만들고, .a(a)처럼 포트 이름으로 자기 신호를 연결합니다. 그 다음 a, b에 네 조합을 차례로
넣고, 조합마다 sum과 carry를 한 줄씩 출력합니다. 시뮬레이션에서 값을 출력하거나 시간
순서대로 일을 시키려면 절차 블록을 씁니다.
4.1 initial 블록과 $display
initial 블록은 시뮬레이션 시작 시점(시간 0)에 한 번 실행되며, 안의 문장을 위에서 아래로
차례로 실행합니다.
형식
initial begin
문장;
문장;
endmodule hello;
initial begin
$display("Hello, Verilog");
$finish;
end
endmodule| 구문 | 역할 |
|---|---|
initial begin ... end | 시간 0에 시작해 안의 문장을 한 번 차례로 실행합니다 |
$display(...) | 문자열과 값을 한 줄로 출력합니다. C의 printf와 비슷합니다 |
$finish; | 시뮬레이션을 끝냅니다 |
$로 시작하는 이름은 시스템 태스크나 시스템 함수를 가리킵니다. $display 같은 태스크는
동작을 수행하고, $clog2 같은 함수는 값을 반환합니다. 이런 기능은 대부분 하드웨어
의미가 없어 합성 도구가 무시하거나 거부합니다. $clog2, $readmemh처럼 합성 도구가
받아들이는 소수의 예외가 있습니다. 한 번이 아니라 반복 실행되는 always 블록은
테스트벤치 기법 장에서 다루고, 클록 에지에서
실행되는 형태는 레지스터와 nonblocking 대입
장에서 다룹니다.
예제는 모두 Icarus Verilog로 실행합니다. iverilog가 소스 파일을 컴파일해 sim.vvp 파일을
만들고, vvp가 그 파일을 시뮬레이션합니다. 실행을 누르면 이 두 단계가 차례로 실행됩니다.
4.2 Tb.v 읽기
Tb.v를 쓰는 데 모든 줄을 알 필요는 없습니다. 이 장에서 볼 줄은 세 줄입니다.
Tb.v의 줄 | 하는 일 |
|---|---|
half_adder dut (.a(a), .b(b), .carry(carry), .sum(sum)); | 반가산기를 배치하고, 각 포트를 같은 이름의 테스트벤치 신호에 연결합니다 |
{a, b} = i; | 입력을 표의 다음 행 00, 01, 10, 11로 바꿉니다. i의 하위 두 비트가 왼쪽부터 a와 b에 들어갑니다 |
#1 $display("HA a=%b b=%b -> carry=%b sum=%b", ...); | assign 출력이 새 입력을 따라 바뀌도록 1 시간 단위를 기다린 뒤 한 행을 출력합니다 |
나머지인 reg·wire 선언, integer i 반복문, #1 지연은 설계를 시간에 따라 움직이게 하는
장치입니다. 지금은 직접 쓰지 않고 그대로 사용하면 되며, 하나하나의 뜻은
테스트벤치 기법 장에서 설명합니다. 하나 더 보면,
연결 목록은 선언 순서와 달리 .carry를 .sum보다 먼저 씁니다.
module tb;
reg a, b;
wire sum, carry;
integer i;
// 이름 포트 연결 .port(signal)은 목록 순서와 무관하다.
half_adder dut (.a(a), .b(b), .carry(carry), .sum(sum));
initial begin
for (i = 0; i < 4; i = i + 1) begin
// 반복 횟수를 두 입력 비트로 나누어 넣는다.
{a, b} = i;
#1 $display("HA a=%b b=%b -> carry=%b sum=%b", a, b, carry, sum);
end
$finish;
end
endmodule어디를 볼까요. 출력 한 줄이 표의 한 행입니다. a=와 b=는 테스트벤치가 넣은 입력이므로
지금 어느 행인지만 알려 줍니다. 확인할 열은 carry=와 sum=이며,
AND 게이트 하나에서 반가산기로에서
직접 채운 마지막 두 열과 비교합니다.
실행하기 전에 예측해 보세요. 출력은 네 줄입니다. a, b가 0 0, 0 1, 1 0, 1 1일
때 carry와 sum이 각각 무엇인지 공통 기초의 반가산기 진리표로 적어 보세요.
- 네 줄 가운데
carry=1인 줄은 몇 개이고, 그 줄의sum은 무엇일까요? .carry를 먼저 쓴 연결 목록이 출력 열의 값을 바꿀까요?
확인 문제
a = 1, b = 1인 줄에서 carry와 sum은 각각 무엇입니까?
실행을 누르면 실행 패널이 열리고 두 파일을 컴파일해 시뮬레이션합니다. 출력 네 줄을 예측과
한 줄씩 비교하세요. 네 줄이 진리표의 네 행과 같은 순서입니다.
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
a=0 b=0 | 두 출력이 모두 0입니다 |
a=0 b=1, a=1 b=0 | sum만 1입니다. 입력 하나가 1이면 XOR이 1이 됩니다 |
a=1 b=1 | carry=1, sum=0입니다. 1 + 1 = 2를 2비트로 쓴 10과 같습니다 |
네 줄이 반가산기 진리표와 일치합니다. .carry와 .sum의 순서는 결과에 영향을 주지
않았습니다. 이름 포트 연결은 이름으로 대응하기 때문입니다.
주의. 출력이 나왔다는 사실은 시뮬레이션이 실행되었다는 뜻이지, 설계가 올바르다는 뜻은
아닙니다. 출력값을 진리표와 비교해 동작을 확인하세요. 이 회로가 실제
칩으로 합성 가능한지는 다른 질문이고, assign으로만 쓴 조합 회로는 합성 도구가 받아들이는
전형적인 형태입니다. 테스트벤치의 $display와 #1은 시뮬레이션에만 있는 기능입니다.
5. 배선 하나를 바꿔 보세요
실행 패널의 편집기에서 HalfAdder.v의 assign sum = a ^ b;를 assign sum = a | b;로 바꾸고
다시 실행하세요. 바꾸기 전에 어느 줄이 달라질지 먼저 적어 둡니다.
확인 문제
sum을 a | b로 바꾸면 네 줄 가운데 어느 줄의 sum이 달라집니까?
이번에는 다른 출력을 바꿔 봅니다. sum의 연산자를 다시 ^로 돌리고,
assign carry = a & b;를 assign carry = a | b;로 바꾼 뒤 실행하기 전에 다시 예측해 보세요.
확인 문제
carry = a | b, sum = a ^ b일 때 carry=1이 나오는 줄은 어느 것입니까?
바꾼 코드는 패널에 남습니다. 원래대로 돌리려면 |를 다시 &로 고치거나 페이지의 실행을
눌러 예제를 새로 불러옵니다.
두 번째 변경은 설계가 아니라 테스트벤치를 건드립니다. 연결 목록을
half_adder dut (a, b, carry, sum);으로 바꿔 위치 연결로 만들어 보세요. 위치 연결은 정의 순서
a, b, sum, carry를 따르므로 테스트벤치의 carry net이 하위 모듈의 sum에, sum net이
carry에 연결됩니다. 컴파일은 그대로 성공하지만 두 열이 맞바뀌어, 가운데 두 줄은 carry=1,
마지막 줄은 sum=1이 됩니다. 위치 연결의 규칙과 이 과정이 이름으로 연결하는 이유는
인스턴스·계층·parameter 장에 있습니다.
6. 정리
| 요소 | 뜻 |
|---|---|
module … endmodule | 설계 단위. 이름과 포트와 동작. 본문은 차례로 실행되는 프로그램이 아니라 연결의 모음 |
방향 [종류] [폭] 이름 | 포트 선언. 종류를 생략하면 wire, 폭을 생략하면 1비트 |
input, output, inout | 포트의 방향 |
assign y = 식; | 식을 y에 항상 연결하는 배선 |
initial begin … end, $display, $finish | 시간 0에 한 번 실행되는 절차 블록과 시뮬레이션용 시스템 태스크 |
인스턴스 이름 dut (.포트(신호)); | 모듈을 다른 모듈 안에 놓고 포트 이름으로 연결 |
7. 확인 문제
-
반가산기 인스턴스를 위치 연결
(a, b, carry, sum)으로 바꾸었는데도 컴파일된 이유는 무엇일까요?정답 보기
위치 연결은 정의 순서
a, b, sum, carry에 맞춰 신호를 연결할 뿐 이름을 비교하지 않습니다.carry와sum은 둘 다 1비트 net이므로 서로 바뀌어 연결되어도 폭이 맞고, 컴파일 오류가 생길 이유가 없습니다. -
모듈 안에
assign y = a & b;가 한 번 적혀 있습니다. 시뮬레이터는 이 식을 몇 번 계산할까요?정답 보기
a나b가 바뀌는 횟수만큼 계산합니다. 연속 대입은 한 번 실행되는 문장이 아니라 연결이므로, 시뮬레이션이 진행되는 동안 식의 입력이 바뀔 때마다y가 다시 계산됩니다.
이 장에는 실습 문제가 없고, 위의 실행이 점검 과제입니다. 포트 선언의 다른 형식과 위치 연결은 모듈을 다른 모듈 안에 배치할 때까지 필요하지 않습니다.