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 : 코딩은 무조건 따라해 봐야 늡니다. 따라해 보시고 잘 안되는 부분 댓글로 질문 주시면 답변 달아 드리겠습니다.^^
'실전! Verilog HDL RTL Design' 카테고리의 다른 글
[Verilog HDL] 13. Register File (0) | 2022.09.14 |
---|---|
[Verilog HDL] 12. IP, Bus, SoC (0) | 2022.08.01 |
[Verilog HDL] 10. task를 이용한 shifter 설계 (0) | 2022.05.16 |
[Verilog HDL] 9. Function 을 사용한 뺄샘기(Subtractor) 설계 (0) | 2022.05.11 |
[Verilog HDL] 8. 순차논리 (Sequential Logic) Adder 설계 (0) | 2022.05.04 |