본문으로 건너뛰기
code0.HDL

인스턴스·계층·parameter

모듈을 다른 모듈 안에 인스턴스로 놓고, 포트를 이름으로 연결하고, parameter를 인스턴스마다 재정의하고, 인스턴스 사이를 내부 net으로 잇고, 4비트 리플 캐리 덧셈기를 계층으로 읽고, 연결 목록이 숨기는 elaboration 오류와 조용한 오배선을 알아봅니다.

이 장에서 배울 내용

  • 모듈의 인스턴스를 놓고 포트를 이름으로 어느 순서로든 연결할 수 있습니다
  • 기본값이 있는 parameter를 선언하고 인스턴스 하나에서 재정의할 수 있습니다
  • 인스턴스를 내부 net으로 잇고 자리올림이 전가산기 네 단을 지나는 과정을 추적할 수 있습니다
  • 없는 포트 이름의 elaboration 오류를 연결하지 않은 입력, 암시적 net, 포트 폭 경고와 구분할 수 있습니다
이 장에서 배우는 개념
  • 인스턴스·이름 포트 연결
먼저 알아야 할 개념

1. 전가산기가 기본 단위입니다

전가산기는 세 비트 a, b, 자리올림 입력 cin을 더해 합 비트 하나와 자리올림 출력 하나를 만듭니다. 두 출력을 합치면 입력 가운데 1의 개수를 두 비트로 쓴 값입니다. 어느 행이든 두 출력을 예측할 수 있을 때까지 회로를 조작해 보세요.

개념 실험 · full-adder

컴파일한 설계가 아니라 브라우저 안의 회로 모델입니다. 실제 도구 체인은 예제에서 실행됩니다.

입력

출력

sum
0
cout
0
진리표
abcinsumcout현재 입력
00000◂ 현재 입력
00110
01010
01101
10010
10101
11001
11111

확인 문제

a, b, cin이 모두 1입니다. sum과 cout은 무엇입니까?

답

FullAdder.v는 같은 회로를 assign 두 개로 씁니다. 입력은 a, b, cin이고 출력은 sum, cout입니다. 이것 네 개를 자리올림으로 이어 붙이면 4비트 덧셈기가 되고, 모듈을 이어 붙이는 방법이 이 장의 내용입니다.

FullAdder.v
// 연속 대입으로 직접 작성한 1비트 전가산기.
module full_adder (
  input  a,
  input  b,
  input  cin,
  output sum,
  output cout
);
  assign sum  = a ^ b ^ cin;
  assign cout = (a & b) | (cin & (a ^ b));
endmodule

2. 인스턴스와 포트 연결

모듈 계층은 elaboration 단계에서 확정됩니다. iverilog는 모든 모듈 정의를 읽고 최상위 모듈부터 인스턴스 트리를 만들며, 이때 각 parameter 값을 정하고 모든 포트 연결을 검사합니다. 시뮬레이션은 그 뒤에 vvp가 실행합니다. 따라서 포트 이름 오타는 시뮬레이션 시간이 흐르기 전에 오류로 검출됩니다.

2.1 인스턴스 만들기

모듈 정의는 회로의 틀입니다. 다른 모듈 안에 그 틀의 이름을 쓰고 새 이름과 연결 목록을 붙이면 인스턴스가 생깁니다. 인스턴스는 고유한 이름, 내부 신호, 연결을 가지며, 같은 정의에서 만든 두 인스턴스는 상태를 공유하지 않습니다. 지금까지의 모든 테스트벤치에서 dut라는 이름으로 하나씩 보았습니다.

형식

모듈_이름 인스턴스_이름 (연결 목록);
wire s1, c1, s2, c2;
half_adder ha0 (.a(x), .b(y),  .sum(s1), .carry(c1));
half_adder ha1 (.a(s1), .b(z), .sum(s2), .carry(c2));
부분의미
half_adder인스턴스로 만들 모듈 정의의 이름입니다
ha0, ha1인스턴스 이름입니다. 한 모듈 안에서 겹치면 안 됩니다
.a(x)하위 모듈의 포트 a를 상위 모듈의 신호 x에 연결합니다
s1ha0의 출력이 구동하고 ha1의 입력이 읽는 내부 net입니다

2.2 이름 포트 연결

.포트(신호) 형식은 포트 이름으로 연결 대상을 정합니다. 목록 안에서 쓰는 순서는 결과에 영향을 주지 않습니다.

형식

모듈_이름 인스턴스_이름 (.포트_이름(신호), .포트_이름(신호));
half_adder u (.carry(c), .sum(s), .b(y), .a(x));   // 순서가 달라도 같은 연결

정의의 포트 순서가 나중에 바뀌어도 이름 포트 연결로 만든 인스턴스는 그대로 올바릅니다. 포트 이름이 정의에 없으면 elaboration 오류가 발생합니다. 오류 메시지는 5절에서 확인합니다.

2.3 위치 연결

포트 이름 없이 신호만 나열하면 정의에 선언된 포트 순서대로 연결됩니다.

형식

모듈_이름 인스턴스_이름 (신호, 신호, 신호);
// half_adder의 포트 순서는 a, b, sum, carry
half_adder u (x, y, s, c);

주의. 위치 연결은 순서를 잘못 써도 폭만 맞으면 오류도 경고도 없습니다. 포트가 셋 이상이면 이름 포트 연결을 쓰세요. 이 과정은 언제나 이름으로 연결합니다.

2.4 연결하지 않은 포트

연결 목록에서 포트를 빼거나 .포트()처럼 괄호를 비우면 그 포트는 연결되지 않습니다. 연결하지 않은 입력은 아무도 구동하지 않으므로 z 상태가 됩니다.

full_adder u0 (.a(a), .b(b), .cin(), .sum(s), .cout(co));   // cin은 z

z는 산술과 논리 연산에서 값을 정할 수 없는 입력이므로, cin에 의존하는 sum은 x가 됩니다. 출력 포트를 비워 두는 것은 그 출력을 쓰지 않겠다는 뜻이며 문제가 없습니다.

3. parameter

3.1 parameter 선언

parameter는 모듈 정의 안에서 폭이나 상수를 이름으로 나타내는 값입니다. 인스턴스마다 다른 값으로 재정의할 수 있으므로, 정의 하나로 폭이 다른 회로를 여러 개 만들 수 있습니다.

형식

module 모듈_이름 #(parameter 이름 = 기본값) (포트 목록);
module pass #(parameter W = 8) (
  input  [W-1:0] d,
  output [W-1:0] q
);
  localparam TOP = W - 1;   // 정의 안에서만 쓰는 상수
  assign q = d;
endmodule
부분의미
#(parameter W = 8)기본값이 8인 parameter W를 선언합니다
[W-1:0]포트 폭을 W로 표현합니다. W가 4이면 [3:0]이 됩니다
localparam TOP다른 값에서 계산한 상수입니다. 인스턴스에서 재정의할 수 없습니다

3.2 인스턴스에서 재정의하기

모듈 이름과 인스턴스 이름 사이에 #(...)를 써서 parameter 값을 바꿉니다. 재정의는 그 인스턴스에만 적용됩니다.

형식

모듈_이름 #(.이름(값)) 인스턴스_이름 (연결 목록);
모듈_이름 #(값)       인스턴스_이름 (연결 목록);
pass #(.W(4)) p4 (.d(nibble), .q(q4));   // 이름으로 재정의: 4비트
pass #(16)    p16 (.d(word),  .q(q16));  // 위치로 재정의: 16비트
pass          p8 (.d(data8),  .q(q8));   // 기본값 8비트

parameter 값은 elaboration에서 확정됩니다. 재정의 값마다 서로 다른 크기의 회로가 생성됩니다. 정의 하나를 average #(.W(8)) wide와 average #(.W(4)) narrow로 두 번 인스턴스화하는 Average.v는 리터럴·signed·식의 폭 장에서 실행합니다. 그 장에서는 두 폭이 자리올림의 보존 여부를 정합니다.

주의. localparam을 #(.TOP(...))처럼 재정의하려고 하면 Icarus는 오류 없이 parameter TOP not found in ... 경고만 출력하고 값을 바꾸지 않습니다. 경고를 놓치면 재정의가 적용된 줄 알고 넘어가기 쉽습니다.

4. 하위 인스턴스로 계층 구성하기

4.1 내부 net

상위 모듈은 내부 net으로 하위 인스턴스를 서로 잇습니다. 내부 net은 상위 모듈 안에 선언한 wire이며, 상위 모듈의 어느 포트에도 쓰이지 않고, 한 인스턴스의 출력이 구동하고 다른 인스턴스의 입력이 읽습니다.

형식

wire 내부_net;
모듈_A 인스턴스1 (.출력_포트(내부_net), ...);
모듈_B 인스턴스2 (.입력_포트(내부_net), ...);

리플 캐리 덧셈기가 대표적인 예입니다. 1비트 전가산기마다 자리올림을 만들고, 그 자리올림이 다음 단의 자리올림 입력이 됩니다.

4.2 예제: 4비트 리플 캐리 덧셈기

Ripple4.v는 c1, c2, c3을 선언하고 bit0부터 bit3까지 전가산기 인스턴스 네 개를 배치합니다. 읽을 때 다음 연결을 따라가 보세요. 벡터·비트 선택·연산자 장의 비트 선택이 4비트 포트에서 단마다 한 비트를 골라냅니다.

인스턴스a, b 입력cin 입력cout 출력
bit0a[0], b[0]모듈 입력 cinc1
bit1a[1], b[1]c1c2
bit2a[2], b[2]c2c3
bit3a[3], b[3]c3모듈 출력 cout
Ripple4.v
// 자리올림으로 이어진 전가산기 네 개.
module ripple4 (
  input  [3:0] a,
  input  [3:0] b,
  input        cin,
  output [3:0] sum,
  output       cout
);
  // 내부 net이 각 단의 자리올림을 다음 단으로 전달한다.
  wire c1, c2, c3;

  full_adder bit0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c1));
  full_adder bit1 (.a(a[1]), .b(b[1]), .cin(c1),  .sum(sum[1]), .cout(c2));
  full_adder bit2 (.a(a[2]), .b(b[2]), .cin(c2),  .sum(sum[2]), .cout(c3));
  full_adder bit3 (.a(a[3]), .b(b[3]), .cin(c3),  .sum(sum[3]), .cout(cout));
endmodule

확인 문제

bit2의 cin 포트에는 어느 net이 연결되어 있습니까?

답

확인 문제

bit2의 목록에서 .cin(c2)를 지우고 a[2]와 b[2]가 서로 다르면 bit2의 cin과 출력은 어떻게 됩니까?

답

Tb.v는 지정한 입력 두 개의 결과를 출력한 뒤, 4비트 입력 두 개와 자리올림 입력의 조합 512개를 모두 Verilog + 결과와 비교합니다. 불일치는 줄마다 출력하지 않고 개수만 셉니다.

Tb.v
module tb;
  reg  [3:0] a, b;
  reg        cin;
  wire [3:0] sum;
  wire       cout;
  integer x, y, errors;

  ripple4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

  initial begin
    a = 4'd9; b = 4'd5; cin = 1'b0;
    #1 $display("RIPPLE 9+5+0 -> cout=%b sum=%0d", cout, sum);
    a = 4'd15; b = 4'd0; cin = 1'b1;
    #1 $display("RIPPLE 15+0+1 -> cout=%b sum=%0d", cout, sum);
    // 입력 조합 512개를 모두 + 연산 결과와 비교한다.
    errors = 0;
    for (x = 0; x < 32; x = x + 1)
      for (y = 0; y < 16; y = y + 1) begin
        {cin, a} = x; b = y; #1;
        if ({cout, sum} !== a + b + cin) errors = errors + 1;
      end
    $display("EXHAUSTIVE errors=%0d", errors);
    $finish;
  end
endmodule

이 개념 실습하기

다음 단계로 짧은 Verilog 문제에 이 개념을 적용해 보세요.

실행하기 전에 예측해 보세요.

  1. 9 + 5 + 0의 cout과 sum은 무엇일까요? 네 단을 따라가 보세요. bit0은 1과 1을, bit1은 0과 0과 자리올림을 더합니다.
  2. 15 + 0 + 1을 계산할 때 자리올림은 몇 단을 통과할까요?
  3. 전수 비교의 오류 수는 몇 개일까요?

확인 문제

RIPPLE 9+5+0 줄은 무엇을 보입니까?

답

결과 확인. 실행을 누르세요.

출력 줄확인할 내용
RIPPLE 9+5+014는 4비트에 들어가므로 cout=0, sum=14입니다
RIPPLE 15+0+1bit0이 1, 0, 1을 더해 합 0과 자리올림 1을 만들고, 나머지 세 단도 같으므로 sum=0, cout=1입니다. {cout, sum}은 16입니다
EXHAUSTIVE오류 수가 0입니다. 512개 조합에서 모두 +와 같은 결과를 냅니다

비교식 {cout, sum} !== a + b + cin의 왼쪽은 5비트이므로 오른쪽 덧셈도 5비트로 계산되어 자리올림을 보존합니다. 리터럴·signed·식의 폭 장의 폭 규칙이 이 검사를 올바르게 만듭니다.

4.3 하나만 바꿔 보세요: 잘못 연결한 net

Ripple4.v에서 bit2의 연결을 .cin(c2)에서 .cin(c1)으로 바꾸고 다시 실행하세요. 설계는 여전히 컴파일됩니다. c1은 폭이 맞는 정당한 net이고, c2는 bit1이 구동하지만 아무도 읽지 않는 net이 됩니다. 결과를 보기 전에 9 + 5가 무엇이 될지 정하세요.

확인 문제

bit2가 c2 대신 c1을 읽으면 RIPPLE 9+5+0 줄은 무엇을 보입니까?

답

잘못 연결한 내부 net은 어떤 컴파일러도 보고하지 않는 종류의 결함입니다. 컴파일은 깨끗하게 성공하고, 정확히 정해진 일부 입력에서만 틀립니다. 전수 비교는 한 번의 실행으로 찾아내지만, 지정한 경우 몇 개로는 놓칠 수 있습니다.

5. 존재하지 않는 포트 연결하기

이름 포트 연결은 elaboration에서 검사합니다. 하위 모듈 정의에 없는 포트 이름을 연결하면 빌드가 중단됩니다.

다음 예제는 의도적으로 elaboration 오류를 발생시킵니다. carry_chain은 입력 이름이 cin인 full_adder에 .cn(c)를 연결합니다.

CarryChain.v
module full_adder (
  input  a,
  input  b,
  input  cin,
  output sum,
  output cout
);
  assign sum  = a ^ b ^ cin;
  assign cout = (a & b) | (cin & (a ^ b));
endmodule

module carry_chain (
  input  a,
  input  b,
  input  c,
  output s,
  output co
);
  // 포트 이름은 cn이 아니라 cin이다.
  full_adder u0 (.a(a), .b(b), .cn(c), .sum(s), .cout(co));
endmodule

실행하기 전에 예측해 보세요. 오류 메시지에는 잘못된 포트 이름과 함께 무엇이 나타날까요? 모듈 이름 full_adder일까요, 인스턴스 이름 u0일까요?

확인 문제

인스턴스 u0의 연결 목록이 full_adder에 없는 포트 cn을 씁니다. 오류 메시지는 어느 이름을 알려 줍니까?

답

결과 확인.

확인 항목내용
잘못된 코드u0 연결 목록의 .cn(c)
실패 단계iverilog의 elaboration입니다. 시뮬레이션은 실행되지 않습니다
오류 메시지port ``cn'' is not a port of u0.
고치는 방법정의에 있는 포트 이름을 써서 .cin(c)로 연결합니다

오류 메시지는 모듈 이름이 아니라 인스턴스 이름 u0을 알려 줍니다. 같은 모듈의 인스턴스가 여러 개여도 어느 연결 목록이 틀렸는지 바로 찾을 수 있습니다.

주의. .cn(c)를 고치지 않고 지우면 아무 메시지 없이 컴파일됩니다. 연결되지 않은 cin은 z가 되고, sum은 항상 x, cout은 a와 b가 서로 다를 때 x가 됩니다. 이름 포트 연결은 오타를 검출하지만 모든 포트가 연결되었는지는 검사하지 않습니다.

6. 자주 하는 실수

6.1 위치 연결의 순서 착오

// 잘못된 코드: 정의 순서는 a, b, sum, carry
half_adder u (a, b, carry, sum);

// 바른 코드: 이름으로 연결
half_adder u (.a(a), .b(b), .sum(sum), .carry(carry));

위치 연결은 정의의 포트 순서를 따릅니다. 순서가 틀려도 폭이 같으면 오류도 경고도 없고 두 출력의 값만 맞바뀝니다.

6.2 입력 포트를 연결하지 않음

// 잘못된 코드: cin이 빠져 z가 됩니다
full_adder u0 (.a(a), .b(b), .sum(s), .cout(co));

// 바른 코드
full_adder u0 (.a(a), .b(b), .cin(c), .sum(s), .cout(co));

연결하지 않은 cin은 오류 없이 z가 됩니다. 이 회로에서 sum은 항상 x이지만, cout은 a와 b가 다를 때만 x입니다. 두 데이터 비트가 같으면 cin 없이도 자리올림을 정할 수 있습니다. 입력을 설정했는데 출력이 x이면 연결 목록에서 빠진 .포트(...)를 찾으세요.

6.3 선언하지 않은 이름으로 연결

reg  [7:0] din;
// 잘못된 코드: dinn은 선언하지 않은 이름입니다
pass8 u (.data(dinn), .q(q));

// 바른 코드
pass8 u (.data(din), .q(q));

연결 목록에 선언하지 않은 이름을 쓰면 Verilog는 그 이름으로 1비트 wire를 자동으로 만듭니다. 오류 대신 Port 1 (data) of pass8 expects 8 bits, got 1. 경고만 출력되고, q는 상위 7비트가 0이고 최하위 비트가 z인 값이 됩니다. 파일 맨 앞에 `default_nettype none을 두면 자동 생성이 꺼져 같은 오타가 Unable to bind wire/reg/memory 오류로 검출됩니다.

6.4 포트와 폭이 다른 신호 연결

// pass8의 포트 data는 8비트
reg [3:0] nib;
reg [11:0] wide12;
// 잘못된 코드: 폭이 맞지 않아도 경고만 출력됩니다
pass8 u1 (.data(nib),    .q(q1));   // 상위 4비트를 0으로 채웁니다
pass8 u2 (.data(wide12), .q(q2));   // 상위 4비트를 버립니다

// 바른 코드: 포트 폭과 같은 폭의 신호를 연결합니다
reg [7:0] byte_in;
pass8 u3 (.data(byte_in), .q(q3));

포트 폭이 다른 연결은 컴파일 오류가 아닙니다. Icarus는 Port 1 (data) of pass8 expects 8 bits, got 4. 같은 경고와 함께 Padding 또는 Pruning 줄을 출력하고, 대입과 같은 규칙으로 확장(부호 없는 값은 0, signed 값은 부호 비트)이나 절단을 적용합니다. 컴파일 경고에 포트 폭 메시지가 있으면 연결한 신호의 선언부터 확인하세요.

7. 실습: 반가산기 두 개로 전가산기 만들기

점검 과제는 실습 문제 full-adder-wiring입니다. 올바른 half_adder 인스턴스 두 개로 만든 전가산기인데 표의 여러 행이 틀립니다. 두 인스턴스를 상자로 그리고 각 .포트(net)을 해당 핀에 적은 뒤, 틀린 행 하나를 그림을 따라 추적하세요. 입력 하나가 둘째 반가산기에 도달하지 않습니다. 연결 하나를 바꾸고 여덟 행 모두를 입력 가운데 1의 개수와 비교하세요.

8. 정리

요소뜻
이름 inst (.포트(신호), …);포트 이름으로 연결한 인스턴스. 인스턴스끼리 상태를 공유하지 않음
이름 연결이름으로 대응하며 순서 무관. 없는 포트 이름은 elaboration 오류
위치 연결정의 순서를 따름. 순서 착오는 검출되지 않음
wire c1, c2, c3;한 인스턴스의 출력을 다른 인스턴스의 입력으로 전달하는 내부 net
연결하지 않은 입력z가 됨. 다른 입력만으로 값을 정할 수 없는 출력은 x
parameter#(parameter W = 8)로 선언하고 #(.W(4))로 인스턴스마다 재정의. localparam은 재정의 불가
{cout, sum} !== a + b + cin자리올림을 유지하는 5비트 비교

9. 확인 문제

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

    정답 보기

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

  2. ripple4의 내부 net은 무엇이며, 각 net을 구동하는 인스턴스는 무엇일까요?

    정답 보기

    내부 net은 c1, c2, c3입니다. c1은 bit0, c2는 bit1, c3은 bit2의 cout이 구동합니다. bit3의 cout은 내부 net이 아니라 모듈 출력 cout을 구동합니다.

  3. .cn(c)를 .cin(c)로 고치지 않고 지우면 무엇이 관찰될까요?

    정답 보기

    컴파일은 메시지 없이 성공합니다. 연결되지 않은 cin이 z가 되어, sum은 항상 x가 되고 cout도 cin에 의존하는 입력에서 x가 됩니다.

확인 지점

실습 문제를 제출해 채점을 받습니다

채점은 서버의 비공개 테스트로 진행되며, 결과는 이 브라우저에만 기록됩니다.

참고 자료

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