[digital] 구조적 모델링
Verilog HDL을 이용한 여러 모델링과 문법
두번째로 살펴볼 것은 바로 "구조적 모델링"이다.
구조적 모델링
- Verilog HDL에서 하드웨어를 설게하는 기본 단위는 모듈이다.
- 구조적 모델링에서, 하나의 모듈은, 다른 모듈들을 이용하여 계층적으로 설계된다.
⇒ 상위 수준의 모듈은 하위 수준의 모듈을 인스턴스하고, 입력/출력/양방향 포트들을 통해 모듈들 간 신호를 전달해준다. - EX) PCB보드는 여러 IC칩들을 인스턴스하여 구성된다.
또한, 각 IC 칩들은 플립플롭, MUX, ALU등과 같은 하위 모듈들을 인스터스하여 구성되고
그러한 모듈들 또한 여러 하위 인스턴스들을 통해 계층적으로 구성된다.
모듈
⇒ 머리부, 선언부, 몸체로 구성된다.
module and_gate #(
parameter N = 4
)(
input [N-1:0] a, // 입력 A
input [N-1:0] b, // 입력 B
input cin, // 입력 Cin
output [N-1:0] sum, // 출력 sum
output cout // 출력 Cout
);
reg [N-1:0] a, b;
reg cin;
wire [N-1:0] sum;
wire cout;
// 논리 회로
wire [N-1:0] axb_cin;
wire [N-1:0] axb;
wire [N-1:0] ab;
and (axb_cin, a, b);
and (axb, a, b);
or (cout, axb, axb_cin);
assign sum = a ^ b ^ cin;
endmodule
위 코드를 머리부/선언부/몸체부로 구분하여 살펴보자!
머리부
: 모듈명, 파라미터 및 포트 목록
module and_gate #(
parameter N = 4
)(
input [N-1:0] a, // 입력 A
input [N-1:0] b, // 입력 B
input cin, // 입력 Cin
output [N-1:0] sum, // 출력 sum
output cout // 출력 Cout
);
...
- module 키워드
- 모듈 이름 (and_gate)
- parameter 목록 (N)
- 포트 목록 (port_list) 또는 포트 선언 ⇒ 위 예시는 선언이 사용됨
선언부
: port, parameter, net, var 선언
reg [N-1:0] a, b;
reg cin;
wire [N-1:0] sum;
wire cout;
wire [N-1:0] axb_cin;
wire [N-1:0] axb;
wire [N-1:0] ab;
- 머리부에 포트가 선언된 경우에는 재선언하지 ❌
- 포트들의 선언 순서는 인스턴스화 될 때 포트 매핑에 영향을 미친다!
몸체부
: 회로의 동작, 기능, 구조를 모델링
and (axb_cin, a, b);
and (axb, a, b);
or (cout, axb, axb_cin);
assign sum = a ^ b ^ cin;
인스턴스
- 설계된 모듈을 다른 모듈 내에서 재사용하는 것
- 모듈을 인스턴스화하면, 그 모듈이 내부에서 정의한 신호, 입력, 출력을 외부 설계와 연결할 수 있다.
인스턴스 사용
- 하위 모듈을 인스턴스로 사용할 때는 각각의 인스턴스 이름을 반드시 지정해주어야 한다.
- 인스턴스 이름은 해당 모듈의 복사본을 구별하는 데 사용된다.
(📌단, 게이트 프리미티브의 인스턴스 이름은 생략하고 그냥 인스턴스종류만 작성해주어도 된다.)
EX) full_adder 모듈을 인스턴스로 사용하여 4bit adder 구현
module ripple_carry_adder (
input [3:0] a, // 4비트 입력 A
input [3:0] b, // 4비트 입력 B
input cin, // 초기 입력 캐리
output [3:0] sum, // 4비트 출력 합계
output cout // 최종 출력 캐리
);
wire c1, c2, c3; // 내부 캐리 신호
// Full Adder 인스턴스 (포트 순서: a, b, cin, sum, cout)
full_adder fa0(a[0], b[0], cin, sum[0], c1); // 인스턴스 이름 : fa0
full_adder fa1(a[1], b[1], c1, sum[1], c2); // 인스턴스 이름 : fa1
full_adder fa2(a[2], b[2], c2, sum[2], c3);
full_adder fa3(a[3], b[3], c3, sum[3], cout);
endmodule
⇒ 여기서, full_adder 인스턴스를 fa0, fa1, fa2, fa3 과 같이 각각 이름을 지정하여 사용한 것을 알 수 있다.
인스턴스 포트 매핑
- 하위 모듈을 인스턴스로 사용할 때는 각각의 인스턴스 이름을 반드시 지정해주어야 한다.
- 인스턴스 이름은 해당 모듈의 복사본을 구별하는 데 사용된다.
EX)
module full_adder (
input a, // 입력 A (1비트)
input b, // 입력 B (1비트)
input cin, // 입력 캐리
output sum, // 출력 합계
output cout // 출력 캐리
);
assign sum = a ^ b ^ cin; // XOR 연산으로 합계 계산
assign cout = (a & b) | (cin & (a ^ b)); // 캐리 계산
endmodule
위와 같이 구현된 모듈을 인스턴스로 사용할 때,
포트 매핑은 다음과 같이 이루어질 수 있다.
1. 순서에 따른 매핑
: 인스턴스 되는 모듈에 작성된 포트 목록 순서에 맞춰 포트매칭이 이루어 진다.
full_adder fa0(sum[0], c1, a[0], b[0], cin);
// full_adder 모듈에서 정의한 순서대로 a,b, cin, sum, cout으로 매핑된다.
2. 이름에 따른 매핑
: 인스턴스 되는 모듈에 작성한 이름을 명시적으로 지정함으로써 포트매칭이 이루어 진다.\
full_adder fa0 (
.a(a[2]), .sum(sum[2]), .cin(c2), .cout(c3), .b(b[2]),
);
포트의 선언
- 한 번 포트선언에 사용된 이름은 다른 포트 선언/자료형 선언에서 다시 선언될 수 없다.
- 명시적인 선언이 없는 net은 unsigned로 취급된다.
EX)
input wire signed [7:0] a, // signed 8비트 입력 신호 a
input wire [3:0] b, // unsigned 4비트 입력 신호 b
output reg signed [15:0] result, // signed 16비트 출력 레지스터 result
inout wire [7:0] data_bus, // unsigned 8비트 양방향 신호 data_bus
output wire valid, // unsigned 단일 비트 출력 신호 valid
input tri0 [3:0] control // tri0 기본값을 가지는 4비트 3상 네트워크
parameter
- parameter는 모듈 내부에서 상수처럼 사용되는 자료형이다.
- 상수를 정의하거나, 모듈의 동작 및 크기(구조)를 매개변수화하는 데 사용된다.
- 모듈 정의 시 초기값을 설정해야 하며, 이후에는 변경 불가능하다. (상수)
- 📌 단, 모듈을 인스턴스화 하여 사용할 때는 명시적 재정의 문법 #(.PARAM_NAME(VALUE)) 을 통해 재정의 가능하다
- 해당 모듈내에서만 사용하는 상수는 localparam 자료형으로 선언되며, 이는 내/외부에서 재정의 불가능하다.
parameter 활용 예시
예를 들어, 다음과 같이 하나의 adder 모듈을 만들었다고 가정하자.
module adder #(
parameter WIDTH = 8 // 기본 비트 폭 8
)(
input [WIDTH-1:0] a, // 입력 A
input [WIDTH-1:0] b, // 입력 B
output [WIDTH-1:0] sum // 출력 합계
);
assign sum = a + b;
endmodule
한 모듈에서는 4bit adder가 필요하고, 다른 모듈에서는 16bit adder가 필요하다면,
adder_4, adder_8, adder_16를 각각 다르게 작성해주어야 할까?
⇒ NO!!
기본 비트폭을 parameter로 설정하고,
인스턴스로 사용 될 때 원하는 bit대로 parameter를 재정의 해주면 된다.
다음은 위와 같이 구현된 모듈을 16bit adder로 사용하는 코드이다.
module top;
wire [15:0] sum;
adder #(.WIDTH(16)) u1 ( // WIDTH를 16으로 재설정
.a(16'hA5A5),
.b(16'h5A5A),
.sum(sum)
);
endmodule
⇒ adder 인스턴스 선언시, WIDTH 파라미터를 16으로 재설정 한 것을 볼 수 있다!
[보충] generate-endgenerate 키워드 (동적 생성)
- 코드가 컴파일 될 때,
- 동일한 하드웨어 블록을 반복적으로 생성하거나
- 조건에 따라 선택적으로 하드웨어 블록을 생성해주는 키워드
- 설계의 유연성과 간결성을 크게 향상시킬 수 있다.
특징
- generate 블록은 컴파일 시간에 처리된다.
- 반복 생성
- genvar 변수를 이용해 generate 블록 내에서 반복을 제어할 수 있다.
- 조건부 생성
- if-else나 case를 사용하여 특정 조건에 따라 하드웨어를 생성할 수 있다.
활용 예시
예를 들어, 다음과 같이 크기가 명확하고 반복적인 조건이 없는 경우,
컴파일러가 모든 할당을 정적으로 확인할 수 있으므로 generate를 사용하지 않아도 된다.
EX) 고정된 4bit_and
module and_gate (
input [3:0] a,
input [3:0] b,
output [3:0] y
);
assign y[0] = a[0] & b[0];
assign y[1] = a[1] & b[1];
assign y[2] = a[2] & b[2];
assign y[3] = a[3] & b[3];
endmodule
하지만, 다음과 같이 크기나 동작이 변경 가능할 경우, 반복이나 조건부 생성을 위해 generate가 필요하다.
EX) 변경가능한 비트수를 가지는 Nbit and_gate
module and_gate #(
parameter N = 8 // 비트 폭
)(
input [N-1:0] a,
input [N-1:0] b,
output [N-1:0] y
);
generate
genvar i;
for (i = 0; i < N; i = i + 1) begin
assign y[i] = a[i] & b[i]; // 각 비트마다 AND 연산
end
endgenerate
endmodule
그렇다면, for, while 과 같은 반복문이 있을 때는 무조건 generate 를 작성해주어야 할까?
⇒ NO!!
generate 블록은 컴파일 시간에, 하드웨어 블록을 반복적/선택적으로 생성할 때 사용된다는 것에 주의해야 한다.
📌하드웨어 블록?
: Verilog 코드가 매핑되는 실제 논리 회로 구성 요소. (논리 게이트, 플립플롭, 레지스터 등등…)
예를 들어, 다음과 같은 코드를 생각해보자
odule decoder_while (
input [2:0] in, // 3비트 입력
output reg [7:0] out // 8비트 출력
);
integer i; // 반복 변수
always @(*) begin
out = 8'b0; // 출력 초기화
i = 0;
while (i < 8) begin
if (i == in) // 입력 값에 해당하는 출력 비트 활성화
out[i] = 1;
i = i + 1;
end
end
endmodule
⇒ 주어진 코드에 대해 요구되는 하드웨어 블록은 위와 같이 고정적이다.
즉, 반복문은 있으나, 고정된 3to8 decoder 하드웨어 구성 요소를 가지고 수행되므로 generate를 작성하지 않는다.
하지만, 다음과 같이 코드를 작성한다면
module decoder_generate #(
parameter N = 3 // 입력 비트 수
)(
input [N-1:0] in, // N비트 입력
output [2**N-1:0] out // 2^N 비트 출력
);
generate
genvar i;
for (i = 0; i < (1 << N); i = i + 1) begin
assign out[i] = (in == i); // 입력과 일치하는 출력 비트 활성화
end
endgenerate
endmodule
⇒ N 값 따라 입력 비트 수와 출력 비트수가 동적으로 결정되므로 요구되는 하드웨어 블록이 각각 다르다
⇒ generate를 작성해주어야 한다!