1. 전가산기가 기본 단위입니다
전가산기는 세 비트 a, b, 자리올림 입력 cin을 더해 합 비트 하나와 자리올림 출력 하나를
만듭니다. 두 출력을 합치면 입력 가운데 1의 개수를 두 비트로 쓴 값입니다. 어느 행이든 두
출력을 예측할 수 있을 때까지 회로를 조작해 보세요.
개념 실험 · full-adder
컴파일한 설계가 아니라 브라우저 안의 회로 모델입니다. 실제 도구 체인은 예제에서 실행됩니다.
입력
출력
- sum
- 0
- cout
- 0
| a | b | cin | sum | cout | 현재 입력 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | ◂ 현재 입력 |
| 0 | 0 | 1 | 1 | 0 | |
| 0 | 1 | 0 | 1 | 0 | |
| 0 | 1 | 1 | 0 | 1 | |
| 1 | 0 | 0 | 1 | 0 | |
| 1 | 0 | 1 | 0 | 1 | |
| 1 | 1 | 0 | 0 | 1 | |
| 1 | 1 | 1 | 1 | 1 |
확인 문제
a, b, cin이 모두 1입니다. sum과 cout은 무엇입니까?
FullAdder.v는 같은 회로를 assign 두 개로 씁니다. 입력은 a, b, cin이고 출력은
sum, cout입니다. 이것 네 개를 자리올림으로 이어 붙이면 4비트 덧셈기가 되고, 모듈을 이어
붙이는 방법이 이 장의 내용입니다.
// 연속 대입으로 직접 작성한 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));
endmodule2. 인스턴스와 포트 연결
모듈 계층은 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에 연결합니다 |
s1 | ha0의 출력이 구동하고 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은 zz는 산술과 논리 연산에서 값을 정할 수 없는 입력이므로, 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 출력 |
|---|---|---|---|
bit0 | a[0], b[0] | 모듈 입력 cin | c1 |
bit1 | a[1], b[1] | c1 | c2 |
bit2 | a[2], b[2] | c2 | c3 |
bit3 | a[3], b[3] | c3 | 모듈 출력 cout |
// 자리올림으로 이어진 전가산기 네 개.
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 + 결과와 비교합니다. 불일치는 줄마다 출력하지 않고 개수만 셉니다.
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 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요.
- 9 + 5 + 0의
cout과sum은 무엇일까요? 네 단을 따라가 보세요.bit0은 1과 1을,bit1은 0과 0과 자리올림을 더합니다. - 15 + 0 + 1을 계산할 때 자리올림은 몇 단을 통과할까요?
- 전수 비교의 오류 수는 몇 개일까요?
확인 문제
RIPPLE 9+5+0 줄은 무엇을 보입니까?
결과 확인. 실행을 누르세요.
| 출력 줄 | 확인할 내용 |
|---|---|
RIPPLE 9+5+0 | 14는 4비트에 들어가므로 cout=0, sum=14입니다 |
RIPPLE 15+0+1 | bit0이 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)를 연결합니다.
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. 확인 문제
-
포트 순서가
a, b, sum, carry인half_adder의 인스턴스를 위치 연결(a, b, carry, sum)으로 바꾸었는데도 컴파일된 이유는 무엇일까요?정답 보기
위치 연결은 정의 순서
a, b, sum, carry에 맞춰 신호를 연결할 뿐 이름을 비교하지 않습니다.carry와sum은 둘 다 1비트 net이므로 서로 바뀌어 연결되어도 폭이 맞고, 컴파일 오류가 생길 이유가 없습니다. -
ripple4의 내부 net은 무엇이며, 각 net을 구동하는 인스턴스는 무엇일까요?정답 보기
내부 net은
c1,c2,c3입니다.c1은bit0,c2는bit1,c3은bit2의cout이 구동합니다.bit3의cout은 내부 net이 아니라 모듈 출력cout을 구동합니다. -
.cn(c)를.cin(c)로 고치지 않고 지우면 무엇이 관찰될까요?정답 보기
컴파일은 메시지 없이 성공합니다. 연결되지 않은
cin이z가 되어,sum은 항상x가 되고cout도cin에 의존하는 입력에서x가 됩니다.