본문으로 건너뛰기
code0.HDL

모듈·포트·assign

반가산기를 포트가 있는 상자로 읽고, 모듈과 포트 선언 형식을 익히고, 테스트벤치가 무엇을 출력할지 예측한 뒤 실행하고, 배선 하나를 바꿔 출력이 달라지는 것을 확인합니다.

이 장에서 배울 내용

  • ANSI 형식으로 포트를 선언한 모듈을 쓰고, 각 포트의 방향·종류·폭을 말할 수 있습니다
  • 진리표에서 반가산기를 이끌어 내 sum은 XOR, carry는 AND임을 보이고, 이를 확인할 출력 열을 짚을 수 있습니다
  • assign이 한 번 실행되는 문장이 아니라 영구적인 연결인 이유를 설명할 수 있습니다
  • initial 블록, 이름 포트 연결, $display로 이루어진 테스트벤치를 읽고 실행 전에 출력을 예측할 수 있습니다
  • 실행이 거치는 Icarus의 두 단계와, 출력 한 줄이 증명하는 것과 증명하지 않는 것을 말할 수 있습니다
이 장에서 배우는 개념
  • 모듈·포트·assign
먼저 알아야 할 개념

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 b1비트 입력 포트 두 개입니다
output y1비트 출력 포트입니다
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 모듈_이름 (
  방향 [종류] [폭] 포트_이름,
  방향 [종류] [폭] 포트_이름
);
  // 모듈 본문
endmodule
module 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;
endmodule

2.3 최상위 모듈

다른 어떤 모듈도 인스턴스로 만들지 않는 모듈이 계층의 최상위 모듈입니다. 이 과정의 예제에서 최상위는 언제나 포트 없는 테스트벤치 tb입니다. Icarus는 아무도 사용하지 않는 모듈을 찾아 자동으로 최상위로 정합니다.

3. assign은 배선입니다

assign 왼쪽 = 오른쪽;은 오른쪽 식의 값을 왼쪽 신호에 항상 연결합니다. 오른쪽 입력이 바뀌면 왼쪽도 바로 따라 바뀝니다. 소프트웨어의 대입문처럼 한 번 실행되고 끝나는 문장이 아니라, 회로에 영구히 놓인 선입니다. 식 안의 ^는 XOR, &는 AND입니다.

3.1 AND 게이트 하나에서 반가산기로

1절의 and_gate는 assign 한 줄짜리 모듈입니다. 반가산기는 여기에 한 줄만 더하면 됩니다. 1비트 수 두 개를 더하면 결과는 0, 1, 2 가운데 하나이고, 2를 나타내려면 비트가 두 개 필요합니다. 2의 자리가 carry, 1의 자리가 sum입니다. 아래 표를 옮겨 적고, 다음 내용을 읽기 전에 마지막 두 열을 직접 채워 보세요.

aba + bcarrysum
000??
011??
101??
112??

채운 열을 하나씩 게이트의 진리표로 읽어 봅니다.

확인 문제

직접 채운 carry 열과 진리표가 똑같은 2입력 게이트는 무엇입니까?

답

확인 문제

직접 채운 sum 열과 진리표가 똑같은 2입력 게이트는 무엇입니까?

답

결국 반가산기는 출력 이름을 carry로 바꾼 and_gate에 sum을 만드는 XOR 한 줄을 더한 회로입니다.

HalfAdder.v는 이 반가산기를 Verilog로 쓴 것입니다. 1비트 입력 두 개와 1비트 출력 두 개를 가지며, sum은 두 입력의 XOR이고 carry는 AND입니다. 포트 넷 개와 assign 두 줄이 설계 전부입니다. 포트 선언 순서가 a, b, sum, carry라는 점을 기억해 두세요.

HalfAdder.v
// 두 비트의 합과 자리올림. 저장하는 상태는 없다.
module half_adder (
  input  a,
  input  b,
  output sum,
  output carry
);
  assign sum   = a ^ b;
  assign carry = a & b;
endmodule

4. 테스트벤치가 입력을 넣고 출력을 읽습니다

설계는 스스로 동작하지 않습니다. 두 번째 파일 Tb.v는 테스트벤치입니다. 테스트벤치는 포트가 없는 모듈로, 설계에 입력을 넣고 출력을 확인합니다. 반가산기를 dut라는 이름으로 인스턴스로 만들고, .a(a)처럼 포트 이름으로 자기 신호를 연결합니다. 그 다음 a, b에 네 조합을 차례로 넣고, 조합마다 sum과 carry를 한 줄씩 출력합니다. 시뮬레이션에서 값을 출력하거나 시간 순서대로 일을 시키려면 절차 블록을 씁니다.

4.1 initial 블록과 $display

initial 블록은 시뮬레이션 시작 시점(시간 0)에 한 번 실행되며, 안의 문장을 위에서 아래로 차례로 실행합니다.

형식

initial begin
  문장;
  문장;
end
module 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보다 먼저 씁니다.

Tb.v
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이 각각 무엇인지 공통 기초의 반가산기 진리표로 적어 보세요.

  1. 네 줄 가운데 carry=1인 줄은 몇 개이고, 그 줄의 sum은 무엇일까요?
  2. .carry를 먼저 쓴 연결 목록이 출력 열의 값을 바꿀까요?

확인 문제

a = 1, b = 1인 줄에서 carry와 sum은 각각 무엇입니까?

답

실행을 누르면 실행 패널이 열리고 두 파일을 컴파일해 시뮬레이션합니다. 출력 네 줄을 예측과 한 줄씩 비교하세요. 네 줄이 진리표의 네 행과 같은 순서입니다.

결과 확인.

출력 줄확인할 내용
a=0 b=0두 출력이 모두 0입니다
a=0 b=1, a=1 b=0sum만 1입니다. 입력 하나가 1이면 XOR이 1이 됩니다
a=1 b=1carry=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. 확인 문제

  1. 반가산기 인스턴스를 위치 연결 (a, b, carry, sum)으로 바꾸었는데도 컴파일된 이유는 무엇일까요?

    정답 보기

    위치 연결은 정의 순서 a, b, sum, carry에 맞춰 신호를 연결할 뿐 이름을 비교하지 않습니다. carry와 sum은 둘 다 1비트 net이므로 서로 바뀌어 연결되어도 폭이 맞고, 컴파일 오류가 생길 이유가 없습니다.

  2. 모듈 안에 assign y = a & b;가 한 번 적혀 있습니다. 시뮬레이터는 이 식을 몇 번 계산할까요?

    정답 보기

    a나 b가 바뀌는 횟수만큼 계산합니다. 연속 대입은 한 번 실행되는 문장이 아니라 연결이므로, 시뮬레이션이 진행되는 동안 식의 입력이 바뀔 때마다 y가 다시 계산됩니다.

이 장에는 실습 문제가 없고, 위의 실행이 점검 과제입니다. 포트 선언의 다른 형식과 위치 연결은 모듈을 다른 모듈 안에 배치할 때까지 필요하지 않습니다.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지