본문 바로가기

실전! Verilog HDL RTL Design

[Verilog HDL] 11. ALU (Arithmetic Logic Unit) 설계

반응형

ALU는 CPU에서 덧셈, 뺄셈 등과 같은 산술 논리 연산을 담당하는 하드웨어 블록이다. 지금 까지 배운 덧셈기, 뺄셈기, 시프터를 이용하여 ALU설계를 해보자.

일반적인 ALU의 개념은 아래 링크를 참조한다.

https://ko.wikipedia.org/wiki/%EC%82%B0%EC%88%A0_%EB%85%BC%EB%A6%AC_%EC%9E%A5%EC%B9%98

 

산술 논리 장치 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

산술논리장치의 일반적인 입출력은 다음과 같이 구성할 수 있다.

1) 입력

  • operand A : 산술 연산의 대상이 되는 입력 값
  • operand B : 산술 연산의 대상이 되는 입력 값
  • opcode  OP : 산술 연산의 종류를 결정하는 코드 (예를 들어 '0'은 덧셈, '1'은 뺄셈 등과 같이 미리 약속한다.)

2) 출력

  • Y : 산술 연산 결과 값

그리고 출력을 레지스터에 저장한다면 시퀀셜 로직으로 설계하기 위하여 clock과 reset 입력이 추가로 필요하다.

이 장에서는 시퀀셜 로직으로 ALU를 설계하기로 한다.

이에 필요한 입/출력을 표로 정리하면 아래와 같다.

이름 bit width 입출력 설명
clock 1 input clock 입력
resetn 1 input active low reset 
opA 8 input operand 
opB 8 input operand
opcode 2 input opcode :
'00' : 덧셈
'01' : 뺄셈
'10' : 오른쪽 시프트
'11' : 왼쪽 시프트
shift_num 3 input shift bit number
이 숫자 만큼 bit를 옮긴다.
y 8 output 연산 결과 값
opOut 2 output opcode를 1 클럭 지연시켜 출력하는 값.
연산 결과 y가 어떤 연산인지 확인하는 용도로 사용한다.

코딩하기에 앞서 각 sub module과 입/출력을 어떻게 연결할지 생각해 보고 그림으로 그려서 확인해 보는 것이 좋다. 이렇게 각 설계 블럭끼리 연결하는 그림을 블럭 다이어그램이라고 한다. 우리가 원하는 기능을 하기 위한 블록 다이어그램을 그려 보면 아래와 같다.

위의 그림에서 각 sub module 즉, adder, subtractor, shifter는 입력 신호들을 공유하는 것을 알 수 있다. 그리고 shifter는 예외적으로 필요한 shift_num이라는 입력을 직접 받고 있고 left_right 신호는 opcode를 해석하여 만들고 있다. 구름 표시의 그림은 조합회로임을 나타낸다. 위의 입/출력 테이블에서 opcode가 '10'이면 right shift, '11'이면 left shift를 하라는 것이므로 left_right신호는 opcode의 0번 bit 신호를 기준으로 만들 수 있다.

shifter에서 left_shift가 '0'이면 right shift, '1'이면 left shift이므로 left_right 신호는 opcode의 0번 bit와 동일하다고 생각해도 무방하다. 이를 assign left_shift = opcode[0];으로 코딩할 수 있다.

주의할 것은 각 sub module이 입력을 공유하므로 각 모듈이 동시에 동작하고 동시에 출력을 하게 된다. 따라서 이들 중 적절한 것을 하나 골라 출력으로 내보내야 하는데 이 기능을 하는 것이 mux module이다. mux module은 같은 alu.v file에서 코딩한다. mux는 여러개의 입력과 입력 중 하나를 선택하는 selection 신호를 입력으로 갖는다. 그리고 selection신호에 따라 입력 중 하나를 출력으로 내보낸다. 그러면 구체적으로 어떻게 코딩하는지 아래의 전체 코드를 보며 설명하겠다.

 

module alu (
    input clock,
    input resetn,
    input [7:0] opA,
    input [7:0] opB,
    input [1:0] opcode,
    input [2:0] shift_num,
    output [7:0] y,
    output reg [1:0] opOut
);

wire left_right;
wire [7:0] sum;
wire [7:0] sub;
wire [7:0] shift;

assign left_right = opcode[0];

adder u_adder (
    .clock   (clock),
    .resetn  (resetn),
    .a       (opA),
    .b       (opB),
    .y       (sum)
);

subtractor u_sub (
    .clock   (clock),
    .resetn  (resetn),
    .a       (opA),
    .b       (opB),
    .en      (1'b1),
    .y       (sub)
);

barrel_shifter u_shift (
    .clock   (clock),
    .resetn  (resetn),
    .a       (opA),
    .num     (shift_num),
    .left_right (left_right),
    .y       (shift)
);

mux u_mux (
    .in0(sum),
    .in1(sub),
    .in2(shift),
    .in3(shift),
    .sel(opOut),
    .out(y)
);

always @(posedge clock or negedge resetn)
begin
    if (!resetn)
        opOut <= 0;
    else
        opOut <= opcode;
end

endmodule

module mux (
    input [7:0] in0,
    input [7:0] in1,
    input [7:0] in2,
    input [7:0] in3,
    input [1:0] sel,
    output reg [7:0] out
);

always @(*)
begin
    case(sel)
        2'b00:
            out = in0;
        2'b01:
            out = in1;
        2'b10:
            out = in2;
        2'b11:
            out = in3;
        default:
            out = 8'b0;
    endcase
end

endmodule

1 ~ 10 line :

module이름과 입출력을 정의한다. module 이름은 alu로 하였다. 그동안 설계한 모듈들이 clock을 입력으로 받는 시퀀셜 로직이므로 clock과 reset입력을 정의한다. 나머지 입출력은 위의 테이블에 정의한대로 코딩한다.

12 ~ 15 line :

모듈간의 연결과 필요한 내부신호를 선언한다. 연결에 필요한 신호는 모두 wire로 선언한다. 내부 신호는 reg나 wire 중 임의의 것으로 선언하면 되나 타입에 맞게 할당문을 써야 한다. 즉 reg type의 경우 반드시 always문의 begin/end block안에서 사용하여야 한다. wire type의 경우 assign 문을 사용하여 할당하도록 한다. left_right신호는 shifter의 opcode를 해석하여 왼쪽 쉬프트인지 오른 쪽 쉬프트인지 결정해 주는 신호이다.

 

17 line :

위에서 설명한 대로 left_right 신호를 만들어 준다. wire type이므로 assign 문을 사용한다.

 

19 ~ 52 line : 

adder, subtractor, shifter, mux module의 인스턴스를 만들고 wire 신호로 연결해 준다. 이 때 각 포트의 이름 앞에 '.'을 붙이는 named 방식으로 연결한다. 포트의 순서대로 연결하는 방법도 있지만 이렇게 명시적으로 이름을 사용하여 연결하는 것이 에러를 없애는 확실한 방법이다. 각 인스턴스의 이름앞에 'u_'를 붙인 것을 볼 수 있는데 이는 예전 부터 관행적으로 사용해오던 관습 같은 것이라 생각하면 되겠다. 이렇게 코딩할 때 변수나 인스턴스의 네이밍 룰을 정해두면 가독성이 높아진다. 특히 팀 단위로 여러 사람이 일을 할 경우 반드시 코딩 가이드를 정해서 코딩을 하는 것이 필요하다.

 

54 ~ 60 line :

각 모듈은 시퀀셜 로직으로써 입력이 클럭에 동기 되어 들어 오면 다음 클럭에 그 연산 결과 값을 출력 하게 된다. 따라서 이 출력이 어떤 operation command인지 알기 위하여 같은 타이밍에 opcode를 출력하기 위한 시퀀셜 로직을 만든다. opOut은 그런 용도로 opcode입력을 한 클럭 지연시켜 출력 시키는 레지스터이다.

 

64 ~ 71 line :

위에서 사용한 mux module을 같은 파일 안에서 코딩한다. 모듈 이름은 mux라고 정하고 8bit 입력을 4개 받아 하나의 8bit 출력으로 내보내는 mux를 정의한다. 4개 중에 하나를 고르는 것이므로 4가지 경우를 표현 할 수 있게 selection신호는 2bit의 'sel'이라는 이름으로 정의한다. 출력 변수 'out'은 always문 안에서 사용하므로 reg 타입으로 선언한다.

 

73 ~ 87 line :

입력 4개중 하나를 선택하는 조합 회로를 always 문을 사용하여 기술한다. 감응 리스트 @(*)는 always 문 안에 사용되는 모든 입력 신호의 변화에 반응하여 동작한다는 의미이다.  여기서 사용한 case문은 괄호안에 있는 selection신호 값에 따라 분기되는 문장으로 C언어에서 switch문과 비슷하다. 단, 각 case별로 해당하는 문장을 실행하고 다음 조건 문으로 가는게 아니라 바로 case문을 빠져나가는 것이 C언어와 다르다. C언어에서는 각 조건문마다 break;를 사용해야 했다.

그리고 주의해야 할 것은 반드시 default 문을 기술하여 빠지는 조건이 없도록 해야 한다. 만약 default문이 없어 해당하는 조건문이 없을 때 그 코드를 합성하면 latch가 생성되게 되는데 이는 시퀀셜 로직의 하나로써 원하는 조합회로가 아니라 시퀀셜 회로가 합성되는 오류가 발생하게 된다. 따라서 반드시 default문을 기술해 주어야 한다.

 

이로써 간단한 alu 설계가 끝났다. 이번에는 이 alu를 테스트 하기 위한 테스트벤치를 코딩해야 한다. 아래는 이러한 테스트 벤치의 예이다. (test.v)

 

`timescale 1ns/1ns
 
module test;

// delare variables
reg clock;
reg resetn;
reg [7:0] a;
reg [7:0] b;
reg [2:0] shift_num;
reg [1:0] opcode;
 
wire [7:0] y;
wire [1:0] opOut;
 
// clock generation
always #10 clock = ~clock;
 
alu u_alu (
    .clock      (clock),
    .resetn     (resetn),
    .opA        (a),
    .opB        (b),
    .opcode     (opcode),
    .shift_num  (shift_num),
    .y          (y),
    .opOut      (opOut)
);
 
 
// create wave dump file
initial
begin
    $dumpfile("alu.vcd");
    $dumpvars(0, test);
end
 
parameter OP_ADD = 2'b00;
parameter OP_SUB = 2'b01;
parameter OP_SHIFT_R = 2'b10;
parameter OP_SHIFT_L = 2'b11;

// input stimulus
initial
begin
    clock <= 0;
    resetn <= 0;
    #100;
    resetn <= 1;
    @(posedge clock);
    opcode <= OP_ADD;
    a <= 1;
    b <= 2;
    @(posedge clock);
    opcode <= OP_SUB;
    a <= 3;
    b <= 1;
    @(posedge clock);
    a <= 3;
    opcode <= OP_SHIFT_L;
    shift_num <= 2;
    @(posedge clock);
    a <= 4;
    opcode <= OP_SHIFT_R;
    shift_num <= 1;
    @(posedge clock);
    #100;
    $finish();
end
 
endmodule

1 line : timescale을 정의한다. simulator에서 timescale을 정의하는 모듈이 제일 처음에 오지 않으면 에러를 내보낸다. 따라서 나중에 실행 명령을 만들 때 이 timescale 정의가 들어 있는 test.v가 제일 앞에 오도록 한다.

 

6 ~ 14 line :

필요한 신호들을 정의한다. 앞서 말한대로 변수 타입은 begin/end block문 안에 기술 되느냐 아니냐로 판단하면 된다.

주의할 것은 always/inital/task등에서 한줄만 문장이 들어갈 경우에는 begin/end를 생략할 수 있다. 하지만 그렇다라도 그 문장에서는 reg type의 변수만 할당할 수 있다.

 

17 line : 클럭 생성문이다. 이는 시퀀셜 로직에서 설명하였으니 참조하기 바란다.

 

32 ~ 36 line :

initial 문을 이용하여 시뮬레이션 초기화 시점에서 dump file을 생성하도록 한다. 하나의 initial 문 안에서 문장들은 순차적으로 실행되지만, 여러개의 initial 문은 동시에 수행된다는 것에 유의 한다.

 

38 ~ 41 line :

verilog에서 상수로 사용되는 값은 parameter로 정의할 수 있다. 상수를 직접 쓰는 것 보다 이처럼 parameter로 정의하여 의미있는 이름으로 대체하여 사용하는 것이 가독성에서 유리하다. parameter를 사용하는 다른 경우는 하나의 모듈에서 각 인스턴스 마다 바꾸어 사용하고 싶은 상수 값이 있는 경우다. 예를 들어 port width를 parameter 값으로 정의하는 경우 각 인스턴스 마다 parameter 값을 달리 적용하여 사용할 수 있는데 이러한 설계 기법을 parameterized design이라고 한다. 이런 설계 방법은 차후에 자세히 설명하기로 한다. 여기서는 사용하는 opcode의 의미로 naming을 하여 코드 값을 정의하였다.

 

44 ~ 69 line :

변수를 초기화 하고 clock에 동기하여 입력 값들을 할당하고 있다. 이에 대한 출력은 wave viwer를 통해 확인할 수 있다.

 

테스트벤치까지 만들었느니 시뮬레이션을 돌릴 차례이다. 이전 까지는 파일 개수가 하나이므로 그냥 코맨드 라인에서 명령어를 직접 입력 하였으나 파일 개수가 늘어나면 일일히 그 때 그 때마다 명령을 입력하는 것은 여간 귀찮은 일이 아니다. 따라서 간단한 bash script를 작성하여 사용한다. 아래와 같은 파일을 만들고 'run'이라는 이름으로 저장한다. 각 파일이 있는 위치는 사용자마다 다를 수 있으므로 자신이 코딩한 파일의 위치를 적어주면 된다. 앞서 설명한 대로 timescale을 적용하기 위해 timescale이 정의되어 있는 test.v file이 파일 리스트의 맨 앞에 위치해 있다.

run file 내용

iverilog test.v alu.v ../adder/adder.v ../subtractor/sub.v ../shifter/shifter.v

이렇게 저장한 뒤 코맨드 창에서 'chmod +x run'을 수행하여 'run' file을 실행 파일로 만든다. 

>>chmod +x run

그리고 코맨드 창에서 아래와 같이 실행하면 verilog compile이 실행되고 a.out file이 만들어 진다. a.out은 실행 파일로 속성이 지정되어 있으므로 코맨드 창에서 ./a.out을 치면 시뮬레이션이 진행되고 dump file이 만들어 진다.

>>./run
>>./a.out

 

 'gtkwave alu.vcd&'를 실행하여 wave form을 확인하면 아래와 같다. (참고로 마지막에 &기호는 리눅스에서 프로그램을 백그라운드에서 실행하라는 의미이다. 즉, 이렇게 하면 리눅스가 프로그램을 끝내지 않고 바로 명령 대기 상태에 있게 된다.)

>>gtkwave alu.vcd&

p.s : 코딩은 무조건 따라해 봐야 늡니다. 따라해 보시고 잘 안되는 부분 댓글로 질문 주시면 답변 달아 드리겠습니다.^^

 

 

반응형