HDLBits Modules편

강정윤·2024년 9월 22일

안녕하세요 오늘은 HDLBits의 Modules편을 다루어볼까 합니다.
이전의 내용을 충분히 학습하시고 문제를 풀어보셨다면, 이번 편은 금방 따라오실거라 생각합니다.

그럼 시작하겠습니다.

Module

자 module이 뭐냐?
입력, 출력 포트로 이루어진 회로를 말합니다.
이 module은 다른 module이 포함할 수 있습니다.
이 때 module의 instance를 생성한다라고 말하는데, 이 표현이 조금 낯선 분들도 있을겁니다.(저도 그랬습니다.)

혹시 HDL을 시작하시기 전에 간단한 프로그래밍을 하셨다면 조금 쉬울 수 있습니다.
혹시 object oriented programming을 하실 때, 클래스라는 것을 배우셨을 겁니다.
클래스 코드를 작성하고 실제로 사용하기 위해서는 클래스의 인스턴스를 생성해야하는데, 이 말은 module에서도 거의 동일하게 적용됩니다.

sub module을 작성하고 다른 module에서 이 module을 사용하기 위해서는,
이 module을 생성해야합니다. 또한 module을 여러개 생성할 수 도 있습니다.
마치 클래스의 인스턴스를 여러개 생성하는 것이 가능한 것처럼요.

자 이게 무슨 말인지 모르겠다...흑흑 하시는 분은
그냥 이렇게 아시면 좋을거 같습니다.

module은 회로도이다!
인스턴스가 회로이다!

네 이렇게 생각하시면 편할 거 같습니다.

사실 그렇게 어려운 부분이 없기때문에, 하나하나 sub module을 작성하고, 그걸 통합하는 top module을 작성하다보면 자연스럽게 알게되실 겁니다.

자 이번 문제 시작해보겠습니다.

Big Endian 구조 이미지 그림 1: module 문제
module top_module ( input a, input b, output out );
    //mod_a instance1(a,b,out);
    
    mod_a instance2(
        .in1(a),
        .in2(b),
        .out(out)
    );
    
endmodule

이렇게 작성하면 됩니다.
여기서 module의 인스턴스를 생성하는 방법이 2가지입니다.

제가 주석처리한 부분과 아닌 부분 2가지인데, 선호하시는 방법으로 사용하시면 됩니다.
하지만 저는 두번째 방법을 추천합니다.
나중에 module을 설계하다보면 in/out 포트가 정말로 많은 경우도 있는데,
헷갈립니다. 그리고 첫번째 방법으로 인스턴스를 생성하면 그 위치를 맞추어 줘야합니다.
그 위치와 순서를 기억하고 코드를 작성하기엔... 좀 귀찮습니다.
(포트가 20개 넘어가면 그 위치를 기억하기엔...뇌용량 issue)

Connecting ports by position

자 문제를 비슷한걸 하나 더 풀어봅시다.
이번 문제는 위의 문제에서 1번째로 작성한 것처럼 포트 위치를 고려해서 인스턴스를 생성해보라고 합니다.

자 바로 들어가봅시다

그림2 : Connecting port by position
module top_module ( 
    input a, 
    input b, 
    input c,
    input d,
    output out1,
    output out2
);
    mod_a inst(out1,out2,a,b,c,d);
endmodule

어때요 아주 easy하지요?

Connecting ports by name

이번 문제는 name으로 포트를 연결하는 문제입니다.
이것도 문제로 바로 들어가보도록 하지요.

그림3 : Connecting ports by name
module top_module ( 
    input a, 
    input b, 
    input c,
    input d,
    output out1,
    output out2
);
    mod_a inst(
        .in4(d),
        .in3(c),
        .in1(a),
        .in2(b),
        .out2(out2),
        .out1(out1)
    );
endmodule

이해를 위해 그 순서를 뒤죽박죽으로 작성했습니다.
하지만 위치가 엉망이어도 이름으로 port들을 mapping해주어서 가능한 풀이입니다.

어때요? 2가지 방법 중 어떤게 좋은가요?
(아까 말했듯 저는 이 방법이 best)

Three modules

자 이번에는 module shift라는 것을 해볼겁니다
우선 문제에서는 친절하게도? D_FF을 미리 만들어두고, 이걸 3개를 연결해서 shift register를 만들라고 하는군요.

easy합니다.
자 그럼 바로 가보시죠.

module top_module ( input clk, input d, output q );
    wire a,b;
    
    my_dff inst1(.clk(clk),.d(d),.q(a));
    my_dff inst2(.clk(clk),.d(a),.q(b));
    my_dff inst3(.clk(clk),.d(b),.q(q));
endmodule

모듈간 연결을 위해 중간에 1bit wire 2개를 사용했습니다.

네 정말로 쉽지요? 이건 3bit짜리 shift register를 만들어 본 것입니다.

Modules and vectors

이번 문제는 이전 문제의 확장판입니다.
여기서는 이전 문제와 달리 입력이 8bit인 경우입니다.
굳이 얘기하자만 24bit register인데, shift 단위가 8bit인 shift register입니다.

자 vector를 다룰 때 중요한 것은 bit수를 잘 맞추어 줘야한다 입니다.
제대로 맞춰주지 않으면 vector의 bit가 잘리거나 0으로 채워지는 불상사가 생깁니다.
어디까지나 잘 알고 이걸 사용한다면, 문제가 안되겠지만 이 부분에 대해 주의하면서 한번 해봅시다.

그림4 : Module shift8
module top_module ( 
    input clk, 
    input [7:0] d, 
    input [1:0] sel, 
    output [7:0] q 
);
    
    wire [7:0] a,b,c;
    
    my_dff8 inst1(
        .clk(clk),
        .d(d),
        .q(a)
    );
    
    my_dff8 inst2(
        .clk(clk),
        .d(a),
        .q(b)
    );
    
    my_dff8 inst3(
        .clk(clk),
        .d(b),
        .q(c)
    );
    
    always@(*)begin
        case(sel)
            2'b00 : 
                q = d;
            2'b01 : 
                q = a;
            2'b10 : 
                q = b;
            2'b11 : 
           		q = c;
       endcase
        
    end

endmodule

네 mux는 always블럭을 사용해서 작성했습니다.
나중에 따로 always블럭을 사용해서 combination logic을 작성할 때, 주의할 점에 대해 심도있게 다룰 예정입니다.

combination logic은 입력과 출력을 명확하게 기술해줘야 합니다.
그렇게 하지 않으면 Design tool이 Latch를 생성하는 상황이 벌어지는데,
latch는 clock의 edge에 작성하는 것이 아니라 signal의 특정 level인 경우에 data가 쓰여집니다.
이 말은 우리가 예상하기 힘든 동작을 한다는 것인데, 나중에 여기에 대해 다루면서
어떻게 작성하면 latch를 안 만들고 combination logic을 정확하게 기술할 수 있는지에 다루겠습니다.

Adder1

이번 문제는 adder에 관한 것입니다.
우리가 자주사용하는 PC에 들어가는 cpu에서 중요한 역할을 하는 친구입니다.
이 부분도 나중에 조금 심도 있게 다루어 보겠습니다.
일단 문제를 한번 보도록 합시다.

그림5 adder1

자 그래도 이번 문제에서는 adder를 만들라고 한 것이 아닌 만들어진 adder module을 연결해봐라 뭐 그런 문제입니다.

module top_module(
    input [31:0] a,
    input [31:0] b,
    output [31:0] sum
);
    
    wire[15:0] sum1,sum2;
    wire carry1,carry2;
    
    add16 inst1(
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(0),
        .sum(sum1),
        .cout(carry1)
    );
    
    add16 inst2(
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(carry1),
        .sum(sum2),
        .cout(carry2)
    );
    
    assign sum = {sum2,sum1};
endmodule

이제 슬슬 힘들어지시나요? 아니면 아직 할만한가요? 아니면 너무 easy한가요?

그래도 module간의 연결을 작성하는 부분이다 보니, 조금 쉬울거라? 생각합니다.

Adder2

이번 문제는 그림이 좀 복잡해 보이지만, 별거 없습니다.
adder1에서 한 것처럼 16bit adder module은 제공한다고 합니다.
그래서 topmodule은 addder1과 동일하게 작성하시면 됩니다.
하지만 여기서 16bit adder내부에 있어야 하는 1bit adder module을 한번 작성해보라 이것이 이번 문제에서 요구하는 것입니다.

그림6 : adder2
module top_module (
    input [31:0] a,
    input [31:0] b,
    output [31:0] sum
);//
 	wire[15:0] sum1,sum2;
    wire carry1,carry2;
    
    add16 inst1(
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(0),
        .sum(sum1),
        .cout(carry1)
    );
    
    add16 inst2(
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(carry1),
        .sum(sum2),
        .cout(carry2)
    );
    
    assign sum = {sum2,sum1};
endmodule


module add1 ( input a, input b, input cin,   output sum, output cout );

// Full adder module here
    assign {cout,sum} = a+b+cin;
  
endmodule

top module아래에 add1이라는 1bit full adder를 작성하면 끝납니다.
여기서 adder의 동작은 1줄이면 끝납니다.
하지만 이걸 combination logic으로 and,or,xor gate를 사용해서 작성하면 조금 길어질 수는 있겠지요.

하지만 full adder는 동작할 때 bit수가 늘어날수록 logic gate를 통과하는 path가 길어져서 나중에 고속 동작 회로를 만들때는 timing 문제가 생길 수 있습니다만,

나중에 adder에 대해 다룰 때, 다시 한번 언급하겠습니다.

Carry-select adder

자 이번에는 Carry-select adder라는 문제입니다.
Ripple Carry Adder(RCA)이 단점은 최하위 bit에서 carry가 발생해서 최종 carry out까지 나오는 path가 너무 길어진다는 것입니다.

그래서 이런 문제를 해결하기 위한 방법이 몇 가지 있는데, 그 중 하나가 Carry select adder입니다.

16bit adder를 cascading한 32bit adder에서 상위 16bit adder에서는 2개의 adder를 사용하고, 하위 16bit adder에서 carry가 발생한 경우, 발생하지 않은 경우 둘 다 계산합니다.
이렇게 하면 carry전파로 인한 delay를 어느정도 해결할 수 있겠지요.
(물론 adder를 총 3개 사용한다는 점에서 circuit의 area가 증가할 겁니다.
이런 trade-off는 좀 따져봐야겠지요.)

자 그럼 이번 문제를 풀어보도록 합시다.

module top_module(
    input [31:0] a,
    input [31:0] b,
    output [31:0] sum
);
    wire[15:0] sum_low, sum_high0, sum_high1;
    wire cout_low, cout_high0, cout_high1;
    
    add16 inst_low(
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(0),
        .cout(cout_low),
        .sum(sum_low)
    );
   
    add16 inst_high1(
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(1),
        .cout(cout_high1),
        .sum(sum_high1)
    );    
    
    add16 inst_high0(
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(0),
        .cout(cout_high0),
        .sum(sum_high0)
    );
    
    assign sum = cout_low ? {sum_high1,sum_low}:{sum_high0,sum_low};
endmodule

마지막 assign을 통해서 bit concatenation을 해주는데, 삼항 연산자인 ?을 사용해서 mux도 함께 기술해주었습니다.

어떤가요? 뭔가 단순히 코드만 작성하는 것을 넘어서서, 회로의 performance를 위해 adder를 하나 더 배치해보고, 이에 따른 area가 증가할 것임을 생각해보면 좋겠지요?

결국 우리가 HW를 만들 때는 항상 경쟁력이 있어야 합니다.

FPGA같은 경우 logic을 수정하는 것이 가능하지만
ASIC으로 대량의 chip을 생산하는 경우 chip을 생산하고 나서는 수정이 안됩니다.

어떤 경우든 간에 PPA라고 하죠, Power, Performance, Area 부분에서 내가 작성한 코드가 괜찮은지 생각해보면, Verilog를 이용한 Digital circuit 설계를 잘 할 수 있을거라 생각합니다.

Adder-subtractor

이번에는 Adder와 Subtractor가 함께 있는 계산기를 작성해보는 파트입니다.
Adder는 잘 알지요 여러분?
하지만 Subtractor라고 하는 감산기에 대해 혹시 아시나요?
이 부분도 나중에 다루어 보겠습니다.
하지만 지금은 2의 보수만 제대로 알고 있다면 따라오는데 문제 없습니다.

우리가 뺄셈을 연산할 때는 그 숫자의 2의 보수를 구해서 더하기 연산을 합니다.
그래서 뺄셈을 하는 연산에서는 처음에 2의 보수를 만들어주는 logic만 추가해주면 옵션에 따라 adder로 동작하기도하고, subtractor로 동작하는 계산기를 만들 수 있습니다.

그림7 : adder & subtractor

자 회로도에 보시면 sub라는 controll signal이 추가 되었습니다.
다들 예상하듯이, sub신호가 1이 되면 subtractor으로 동작하고, 0이면 adder로 동작합니다.

자 그리고 XOR gate가 하나 달려있는데요, XOR gate를 잘 이용하면 단순 buffer로 만들 수 있고, 아니면 inverter(NOT gate)로 만들 수 있습니다.

이 내용만 한번 집고 넘어가도록 합시다.

0^0 = 0
0^1 = 1
1^0 = 1
1^1 = 1

위의 XOR 계산표에서 왼쪽 피연산자는 입력이고 오른쪽은 sub signal이라 생각해봅시다.

오른쪽이 0일 때는 왼쪽이 그대로 출력으로 나가지요?
반면에 오른쪽이 1일 때는 왼쪽이 반전되어서 출력으로 나갑니다.

이걸 이용하면 XOR gate의 한쪽을 전부 1로 한다면, 입력 값을 전부 반전시킬 수 있겠지요?
(우리의 경우 32'b1)
반대로 XOR gate의 한쪽을 전부 0으로 한다면, 입력 값을 그대로 adder로 전달할 수 있겠지요.
(우리의 경우 32'b0)

그래서 XOR gate를 사용한 겁니다.
또한 2의 보수를 만들기 위해서는 피연산자를 반전하는 것으로 끝나지 않지요.
+1을 해야겠지요? 그걸 하위 16bit adder의 carry에 sub signal을 물려준다면

a-b ===> a+~b+1

이렇게 만들 수 있겠지요?

네 정말로 좋은 방법이라고 생각합니다.

따로 subtractor를 만들지 않고, 기존의 adder를 잘 활용하면 둘 다 가능하답니다.

자 그럼 이제 문제로 돌아가봅시다.

module top_module(
    input [31:0] a,
    input [31:0] b,
    input sub,
    output [31:0] sum
);
    
    wire [15:0] sum_low, sum_high;
    wire cout_low, cout_high;
    wire [31:0] b_option;
    
    assign b_option = b^{32{sub}};
    
    add16 inst_low(
        .a(a[15:0]),
        .b(b_option[15:0]),
        .cin(sub),
        .cout(cout_low),
        .sum(sum_low)
    );
    
    add16 inst_high(
        .a(a[31:16]),
        .b(b_option[31:16]),
        .cin(cout_low),
        .cout(cout_high),
        .sum(sum_high)
    );
    
    
    assign sum = {sum_high, sum_low};
endmodule

자 처음에 b_option이라는 wire를 하나 더 선언하고, 여기에 sub와 input b를 XOR한 결과를 assign했지요?
여기서 저는 bit 연산에서 잘못되는 경우를 없도록 하고 싶어서, sub를 32bit으로 맞추어 주었습니다.
이렇게 하면 명확하게 기술할 수 있겠지요?

왜냐하면 sub는 1bit signal이고 32bit vector와 bitwise연산을 할 때, zero-padding됩니다...
그렇게 되면 b를 반전해야하는 경우 제대로 반전시킬 수 없겠지요
그러면 HDLBits의 결과에서도 볼 수 있듯이 Incorrect가 나옵니다.
그래서 {}를 사용하면 이런 상황을 피할 수 있습니다.

그리고 concatenation에 대해 다룰 때, sign extend해주는 circuit을 작성해보았지요? 그 내용을 확실하게 알고 있다면, 실수를 하지 않을 수 있을 겁니다.

네 여기까지가 modules편이고요, 다음에는 Procedures편으로 찾아오겠습니다.

좋은 하루 보내세요~

profile
muscle brain

1개의 댓글

comment-user-thumbnail
2024년 9월 22일

멋지시네요

답글 달기