개인 공부/회로

[digital] 구조적 모델링

KimuGamJa 2024. 12. 19. 22:30

 

 

 

 

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 키워드 (동적 생성)
  • 코드가 컴파일 될 때,
  1. 동일한 하드웨어 블록반복적으로 생성하거나
  2. 조건에 따라 선택적으로 하드웨어 블록을 생성해주는 키워드
  • 설계의 유연성과 간결성을 크게 향상시킬 수 있다.

 

 

특징

  • 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를 작성해주어야 한다!