[ACC Simulation]
📌 Testbench
module tb_acc();
reg clk, reset_p, acc_high_reset_p;
reg fill_value; // 안 쓰는 비트를 0으로 채우거나 1로 채움
reg rd_en, acc_in_select;
reg [1:0] acc_high_select, acc_low_select; // Half_ACC가 두 개이므로 구분해줘야 한다.
reg [3:0] bus_data, alu_data; // BUS와 ALU로부터 받는 데이터
wire [3:0] acc_high_data2bus, acc_high_register_data;
wire [3:0] acc_low_data2bus, acc_low_register_data;
acc DUT(clk, reset_p, acc_high_reset_p, fill_value, rd_en, acc_in_select, acc_high_select, acc_low_select, bus_data, alu_data,
acc_high_data2bus, acc_high_register_data, acc_low_data2bus, acc_low_register_data);
// 변수 초기화
initial begin
clk = 0;
reset_p = 1;
acc_high_reset_p = 0;
fill_value = 0;
rd_en = 1;
acc_in_select = 0;
acc_high_select = 0; // 0주면 현재 값 유지
acc_low_select = 0; // 0주면 현재 값 유지
bus_data = 4'b0101; // 5
alu_data = 4'b0010; // 2
end
always #4 clk = ~clk;
initial begin
#8
reset_p = 0; #8; // 초기화 끝
acc_high_select = 2'b11; #8;
acc_high_select = 2'b00; #8;
acc_in_select = 1; acc_high_select = 2'b11; #8;
acc_high_select = 2'b00; #8;
acc_low_select = 2'b11; #8;
acc_low_select = 2'b00; #8;
$stop;
end
endmodule
📌 Result
1. 전체 변수 초기화
2. 한 번 리셋해주고
3. acc_in_select = 0 이니까 alu_data를 받는다.
4. acc_high에서 alu_data를 받은 뒤 유지 -> acc_high는 alu_data인 2를 출력한다.
5. acc_in_select = 1 로 변경하고 bus_data를 받는다.
6. acc_high에서 bus_data를 받은 뒤 유지 -> acc_high는 bus_data 인 5를 출력한다.
7. acc_low_select를 2'b11 -> 2'b00 -> acc_high의 데이터를 받은 후 유지
-> acc_low는 acc_high 인 5를 출력한다.
acc_high_select = 2'b01; acc_low_select = 2'b01; #8; // 우시프트
acc_high_select = 2'b00; acc_low_select = 2'b00; #8;
acc_high_select = 2'b10; acc_low_select = 2'b10; #8; // 좌시프트
acc_high_select = 2'b00; acc_low_select = 2'b00; #8;
acc_high와 acc_low를 우시프트(2'b01) 한 번, 좌시프트(2'b10) 한 번 해주자.
우시프트를 하면,
acc_high는 msb에는 fillvalue가 채워지고, 나머지 3bit는 이전 값의 상위 3bit가 채워진다.
▶ 0101 → 0010
acc_low는 msb에는 acc_high_register_data[0] 가 채워지고, 나머지 3bit는 이전 값의 상위 3bit가 채워진다.
▶ 0101 → 1010
좌시프트를 하면,
acc_high는 lsb에는 acc_low_register_data[3]가 채워지고, 나머지 3bit는 이전 값의 하위 3bit가 채워진다.
▶ 0010 → 0101
acc_low는 lsb에는 fill_value가 채워지고, 나머지 3bit는 이전 값의 하위 3bit가 채워진다.
▶ 1010 → 0100
acc_high_select = 2'b10; acc_low_select = 2'b10; #8; // 좌시프트
acc_high_select = 2'b00; acc_low_select = 2'b00; #8;
acc_high_reset_p = 1; #8; // 상위 4bit만 리셋
좌시프트 한 번 더 하고 상위 4bit만 리셋
[Block_acc_alu]
ALU는 혼자서 동작하지 못 한다.
따라서 ACC와 합친 모듈을 하나 만들어주자.
📌 Code
module block_alu_acc(
input clk, reset_p, acc_high_reset_p,
input rd_en, acc_in_select,
input [1:0] acc_high_select, acc_low_select, // acc의 mode를 선택
input op_add, op_sub, op_mul, op_div, op_and,
input [3:0] bus_data, bus_reg_data,
output sign_flag, zero_flag,
output [7:0] acc_data // 최종적인 연산의 결과 / 얘는 BUS로만 간다.(상위 4bit, 하위 4bit)
);
// 모듈 내부에서 주고 받을 신호들은 wire 처리
wire [3:0] alu_data;
wire [3:0] acc_high_data2bus, acc_low_data2bus;
wire [3:0] acc_high_register_data, acc_low_register_data;
wire carry_flag, cout;
wire [1:0] acc_high_select_mul_div;
// acc_low_register_data의 최하위비트가 0이면 안 더하고 1이면 더함
// assign acc_high_select_mul_div[0] = op_mul ? acc_low_register_data[0] : acc_high_select[0];
// assign acc_high_select_mul_div[1] = op_mul ? acc_low_register_data[0] : acc_high_select[1];
assign acc_high_select_mul_div[0] = (op_mul | op_div) ? (op_mul & acc_low_register_data[0]) | (op_div & cout) : acc_high_select[0];
assign acc_high_select_mul_div[1] = (op_mul | op_div) ? (op_mul & acc_low_register_data[0]) | (op_div & cout): acc_high_select[1];
// op_div = 1이면서 cout이 1이 된다면, 뺴기가 가능하다는 뜻이므로 acc_high_select_mul_div = 2'b11이 되어 뺸 값을 로드한다.
acc U_acc(.clk(clk), .reset_p(reset_p), .acc_high_reset_p(acc_high_reset_p),
.fill_value(carry_flag), .rd_en(rd_en), .acc_in_select(acc_in_select),
.acc_high_select(acc_high_select_mul_div), .acc_low_select(acc_low_select),
.bus_data(bus_data), .alu_data(alu_data),
.acc_high_data2bus(acc_high_data2bus), .acc_high_register_data(acc_high_register_data),
.acc_low_data2bus(acc_low_data2bus), .acc_low_register_data(acc_low_register_data)
);
assign acc_data = {acc_high_data2bus, acc_low_data2bus}; // BUS로 출력되는 acc_data(조건부 출력)
alu U_alu(.clk(clk), .reset_p(reset_p),
.op_add(op_add), .op_sub(op_sub), .op_mul(op_mul), .op_div(op_div), .op_and(op_and), // 연산에 사용할 명령어
.alu_lsb(acc_high_register_data[0]), // 최하위 비트 입력 하나 있어야 됨
.acc_high_data(acc_high_register_data), // BUS를 통해 입력된 첫 번쨰 Data 4bit는 ACC에 저장 -> ALU의 상위 4bit에 저장
.bus_reg_data(bus_reg_data), // BUS를 통해 입력된 다음 Data 4bit는 BREG에 저장 -> ALU의 하위 4bit에 저장
.alu_out(alu_data), // 연산 결과 -> ACC로 전송
.zero_flag(zero_flag), // 연산의 결과가 0이면, zero flag = 1
.sign_flag(sign_flag), // 계산 결과, 음수가 발생하면 sign_flag = 0, 부호(음수/양수)에 따라 sign flag값이 정해진다.
.carry_flag(carry_flag), // carry가 발생하면, carry flag = 1
.cout(cout) // carry_out
);
// 더하기와 뺴기할 때는 op_add 또는 op_sub를 주고 high & low_select에 2'b11를 준 뒤 출력하면 된다.
// acc_data는 상위와 하위비트는 assign으로 합쳐줬기 때문에 더하기 효과를 나타낸다.
endmodule
📌 Testbench
module tb_block_alu_acc();
reg clk, reset_p, acc_high_reset_p;
reg rd_en, acc_in_select;
reg [1:0] acc_high_select, acc_low_select; // acc의 mode를 선택
reg op_add, op_sub, op_mul, op_div, op_and;
reg [3:0] bus_data, bus_reg_data;
wire sign_flag, zero_flag;
wire [7:0] acc_data; // 최종적인 연산의 결과 / 얘는 BUS로만 간다.(상위 4bit, 하위 4bit)
block_alu_acc DUT(
clk, reset_p, acc_high_reset_p,
rd_en, acc_in_select,
acc_high_select, acc_low_select, // acc의 mode를 선택
op_add, op_sub, op_mul, op_div, op_and,
bus_data, bus_reg_data,
sign_flag, zero_flag,
acc_data // 최종적인 연산의 결과 / 얘는 BUS로만 간다.(상위 4bit, 하위 4bit)
);
initial begin
clk = 0;
reset_p = 1;
rd_en = 1;
acc_high_reset_p = 0;
acc_in_select = 0;
bus_data = 4'b0111; bus_reg_data = 4'b0010; // 2에 7을 곱하자.
acc_high_select = 0; acc_low_select = 0;
op_add = 0; op_sub = 0; op_mul = 0; op_div = 0; op_and = 0;
end
always #4 clk = ~clk;
initial begin
#8;
reset_p = 0; #8;
// BUS의 data를 high에 넣고 low에서 로드한 뒤, high 클리어
acc_in_select = 1; // BREG 값 로드
acc_high_select = 2'b11; #8; // high에 로드.
acc_in_select = 0; acc_high_select = 2'b00; // 값 받았으면, 꺼주고
acc_low_select = 2'b11; #8; // high 값을 low로 보내주고
acc_low_select = 2'b00; // 값 받았으면 꺼준다.
acc_high_reset_p = 1; #8; // 상위 4bit만 리셋하고 한 클락 보냄
acc_high_reset_p = 0;
// 곱셉 시작(곱셈은 더하기 + 우시프트의 연속)
op_mul = 1; #8; // 더하기
// op_mul = 1이면, acc_high_select_mul_div에 2'b11이 들어간다.
// ALU에서 더한 결과가 ACC로 전달된다.
op_mul = 0; acc_low_select = 2'b01; acc_high_select = 2'b01; #8; // 우시프트
op_mul = 1; acc_low_select = 2'b00; #8; // 더하기 / 또 밀리면 안 되니까, 데이터 유지 모드
op_mul = 0; acc_low_select = 2'b01; acc_high_select = 2'b01; #8; // 우시프트
op_mul = 1; acc_low_select = 2'b00; #8; // 더하기 / 또 밀리면 안 되니까, 데이터 유지 모드
op_mul = 0; acc_low_select = 2'b01; acc_high_select = 2'b01; #8; // 우시프트
op_mul = 1; acc_low_select = 2'b00; #8; // 더하기 / 또 밀리면 안 되니까, 데이터 유지 모드
op_mul = 0; acc_low_select = 2'b01; acc_high_select = 2'b01; #8; // 우시프트
acc_low_select = 2'b00; acc_high_select = 2'b00; #8; // 다 0줘서 데이터 유지
$stop;
end
endmodule
4bit 곱셈하는 데 총 8clk가 필요하다.
✅ 곱셈(더하기 + 우시프트의 연속)
i) op_mul = 1로 설정을 하면,
acc_high_select_mul_div[0] = acc_low_register_data[0]
acc_high_select_mul_div[1] = acc_low_register_data[0] 이 되고,
이때 acc_low의 lsb가 1일 때는 더하기된 값을 로드하고(2'b11)
0일 때는 현재 데이터를 유지한다.
ii) 이후 op_mul을 0으로 바꿔준 뒤,
acc_high와 acc_low를 둘 다 우시프트해준다.
iii) i) ~ ii) 과정을 BUS를 통해 입력받은 값의 msb까지 반복
cf) carry가 발생할 경우, acc_high의 msb로 설정되니 참고할 것.
0111 x 0010 을 계산해보자.
0 0 1 0 → BREG
x 0 1 1 1 → ACC(상위 4bit)
------------------------------------
0 0 1 0 ①
0 0 1 0 ②
0 0 1 0 ③
+ 0 0 0 0 ④
-------------------------------------
0 0 0 1 1 1 0 = 14
초기 상태 : ACC 0000 0000 BREG 0010
① - i) 0010 0111 + ( + 0010)
ii) 0001 0011 shift 우시프트
② - i) 0011 0011 + ( + 0010)
ii) 0001 1001 shift 우시프트
③ - i) 0011 1001 + ( + 0010)
ii) 0001 1100 shift 우시프트
④ - i) 0001 1100 + ( + 0000)
ii) 0000 1110 shift 우시프트
∴ 0000 1110
module tb_block_alu_acc();
reg clk, reset_p, acc_high_reset_p;
reg rd_en, acc_in_select;
reg [1:0] acc_high_select, acc_low_select; // acc의 mode를 선택
reg op_add, op_sub, op_mul, op_div, op_and;
reg [3:0] bus_data, bus_reg_data;
wire sign_flag, zero_flag;
wire [7:0] acc_data; // 최종적인 연산의 결과 / 얘는 BUS로만 간다.(상위 4bit, 하위 4bit)
block_alu_acc DUT(
clk, reset_p, acc_high_reset_p,
rd_en, acc_in_select,
acc_high_select, acc_low_select, // acc의 mode를 선택
op_add, op_sub, op_mul, op_div, op_and,
bus_data, bus_reg_data,
sign_flag, zero_flag,
acc_data // 최종적인 연산의 결과 / 얘는 BUS로만 간다.(상위 4bit, 하위 4bit)
);
initial begin
clk = 0;
reset_p = 1;
rd_en = 1;
acc_high_reset_p = 0;
acc_in_select = 0;
bus_data = 4'b0111; bus_reg_data = 4'b0010;
acc_high_select = 0; acc_low_select = 0;
op_add = 0; op_sub = 0; op_mul = 0; op_div = 0; op_and = 0;
end
always #4 clk = ~clk;
// 나눗셈
initial begin
#8;
reset_p = 0; #8;
// BUS의 data를 high에 넣고 low에서 로드한 뒤, high 클리어
acc_in_select = 1; // BREG 값 로드
acc_high_select = 2'b11; #8; // high에 로드.
acc_in_select = 0; acc_high_select = 2'b00; // 값 받았으면, 꺼주고
acc_low_select = 2'b11; #8; // high 값을 low로 보내주고
acc_low_select = 2'b00; // 값 받았으면 꺼준다.
acc_high_reset_p = 1; #8; // 상위 4bit만 리셋하고 한 클락 보냄
acc_high_reset_p = 0;
// 여기까지 ACC : 0000 0111, BREG : 0010
// 나눗셈 시작
// 빼기 + 좌시프트의 연속
// 빼기는 acc_high_data - bus_reg_data 이 것의 뺄셈이 가능한 지 불가능한 지 여부에 따라 결정한다.
// 뺀 결과가 음수가 나온다는 것이 빼기가 불가능하다는 의미.
acc_low_select = 2'b10; acc_high_select = 2'b10; #8;// 좌시프트 먼저 하고
// 좌시프트를 먼저 하는 이유 : 첫 자리는 어짜피 빼기해도 음수가 나오므로 데이터가 유지된다. 따라서 생략 가능
// 여기까지 ACC : 0000 1110, BREG : 0010
acc_low_select = 2'b00; // 값 받았으니 꺼주고
op_div = 1; #8; // 0000 - 0010 하면, 뺼셈이 불가능하므로 cout = 0이고, 데이터 유지
// 뺼 수 있으면, carry_flag = cout = 1 / 뺼 수 없으면, carry가 발생하고, cout = 0
// 여기까지 ACC : 0000 1110, BREG : 0010
op_div = 0; // 시프트하기전에 op_div 꺼주고
acc_low_select = 2'b10; acc_high_select = 2'b10; #8;// 좌시프트 먼저 하고
// 여기까지 ACC : 0001 1100, BREG : 0010
acc_low_select = 2'b00; // 값 받았으니 꺼주고
op_div = 1; #8; // 0001 - 0010 하면, 뺼셈이 불가능하므로 cout = 0이고, 데이터 유지
// 여기까지 ACC : 0001 1100, BREG : 0010
op_div = 0; // 시프트하기전에 op_div 꺼주고
acc_low_select = 2'b10; acc_high_select = 2'b10; #8;// 좌시프트 먼저 하고
// 여기까지 ACC : 0011 1000, BREG : 0010
acc_low_select = 2'b00; // 값 받았으니 꺼주고
op_div = 1; #8; // 0011 - 0010 하면, 뺼셈이 가능하므로 cout = 1이므로 빼고 그 값을 로드
// 여기까지 ACC : 0001 1000, BREG : 0010
op_div = 0; // 시프트하기전에 op_div 꺼주고
acc_low_select = 2'b10; acc_high_select = 2'b10; #8;// 좌시프트 먼저 하고
// 여기까지 ACC : 0011 0001, BREG : 0010
acc_low_select = 2'b00; // 값 받았으니 꺼주고
op_div = 1; #8; // 0011 - 0010 하면, 뺼셈이 가능하므로 cout = 1이므로 빼고 그 값을 로드
// 여기까지 ACC : 0001 0001, BREG : 0010
op_div = 0; // 시프트하기전에 op_div 꺼주고
acc_low_select = 2'b10; acc_high_select = 2'b00; #8; // acc_low만 좌시프트 한 번 해준다.
// 여기까지 ACC : 0001 0011, BREG : 0010
acc_low_select = 2'b00; #8;
$stop;
end
endmodule
✅ 나눗셈(좌시프트 + 빼기의 연속)
뺴기할 때 carry flag가 나온다
뺄셈은 ACC_high - BREG(코드로 보면, acc_high_data - bus_reg_data)
뺀 결과가 양수 또는 0일 때, carry flag = 1 이다 → 뺄 수 있다는 뜻
따라서 뺼 수 있는지 없는지는 cout을 보면 된다.(cout = 1일 때 뺄 수 있음)
뺼 수 있으면 cout = 1을 써주고 뺴주면 된다.
시프트한 뒤, 로드하던가 말던가 정하고.
나눗셈은 나누어지는 수의 bit 크기만큼 빼고 시프트하는 것을 반복하면 된다.
ex) 4bit짜리를 나눈다면, 그 과정은 다음과 같다.
좌시프트 → ( 빼기 → 좌시프트) → (뺴기 → 좌시프트_ → (빼기 → 좌시프트) → (빼기 → ACC_Low만 좌시프트)
결과 : ACC_Hogh = 나머지, ACC_Low = 몫
▶ 원래는 빼기를 처음에 한 번 하고 시작해서 총 5번 해야 하는데,
어짜피 첫 빼기의 결과는 음수가 나오므로, 데이터 유지가 된다. 따라서 생략 가능
[Program Counter]
📌 Program Counter란?
PC는 Program Counter로 현재 실행 중인 프로그램의 다음 명령어 위치를 가리키는 레지스터이다.
CPU는 PC가 가리키는 명령어를 실행하고, 그 후에 PC를 증가시켜 다음 명령어의 위치를 가리키며
이러한 PC로 인해, CPU는 명령어를 순차적으로 실행할 수 있다.
이전 값을 기반으로 다음 값이 카운트되므로, Loadable Counter로 설계되어야 한다.
📌 구상도
8개의 Half_adder를 연속적으로 이어 붙여 count를 할 것이고,
그 값을 REG에 저장하고 불러오는 기능을 추가하여 Load 기능까지 포함된
Loadable Counter를 구현할 것이다.
위 구상도처럼 구조적 모델링으로만 가능하느냐?
그 것은 아니다.
동작적 모델링으로도 충분히 구현 가능하지만,
generate문을 학습하기 위해 구조적 모델링으로 for문을 활용하여 구현하도록 하자.
참고로 FPGA에 대한 확실한 이해와 LUT, DFF의 조합을 완벽하게 숙지하지 않는 한,
구조적 모델링은 지양하는 것이 좋다.
Latch가 만들어질 수 있기 때문.
FPGA에는 logic gate들이 없다. LUT과 DFF를 조합하여 gate를 만들어내는 것이므로
완벽하게 설계해주지 않는다면, 문제가 발생할 소지가 크다.
따라서 웬만하면 구조적 모델링은 지양하고, 동작적 모델링을 지향하도록 하자.
📌 4 bit Half_adder
module half_adder_dataflow( // 데이터플로우 모델링
//assign 문을 활용하여 수식으로 출력을 표현
input A,
input B,
output sum,
output carry
);
assign sum = A^B; // xor
assign carry = A&B; // and
endmodule // 앞으로 half addr는 이 것을 자주 사용할 것.
module half_adder_4bit(
// load_data 에 1만 더해주는 기능을 한다.
// 하프 에더이기 때문에 0 또는 1씩만 더해줄 수 있다.
// 1씩 증가하는 카운터를 이 걸로 만들어줄 수 있음.
input inc,
input [3:0] load_data,
output [3:0] sum
);
// half adder 3개로 만들어보자.
// wire [3:0] carry_out;
// half_adder_dataflow ha0(.A(inc), .B(load_data[0]), .sum(sum[0]), .carry(carry_out[0]));
// half_adder_dataflow ha1(.A(carry_out[0]), .B(load_data[1]), .sum(sum[1]), .carry(carry_out[1]));
// half_adder_dataflow ha2(.A(carry_out[1]), .B(load_data[2]), .sum(sum[2]), .carry(carry_out[2]));
// half_adder_dataflow ha3(.A(carry_out[2]), .B(load_data[3]), .sum(sum[3]), .carry(carry_out[3]));
// 위 코드를 for문으로 만들어보자.
wire [3:0] carry_out;
half_adder_dataflow ha0(.A(inc), .B(load_data[0]), .sum(sum[0]), .carry(carry_out[0]));
// for문으로 구조적 모델링할 떄
genvar i; // 얘는 회로로 안 만들어진다. 단순히 for문에서만 쓸 변수
generate
for(i = 1 ; i < 4 ; i = i + 1) begin
half_adder_dataflow ha(.A(carry_out[i-1]), .B(load_data[i]), .sum(sum[i]), .carry(carry_out[i]));
end
endgenerate
endmodule
generate
for(i = 1 ; i < N ; i = i + 1) begin : ha
half_adder_dataflow ha(.A(carry_out[i-1]), .B(load_data[i]), .sum(sum[i]), .carry(carry_out[i]));
end
endgenerate
위 코드처럼, for문의 begin 뒤에 : [쓰고 싶은 이름]
을 넣어주면, 내가 원하는 이름을 붙여줄 수 있다.
✅ generate문
generate문은 조건부 또는 루프와 같은 제어 구조를 사용하여,
모듈과 다른 Verilog 코드 블록을 동적으로 생성하는 데 사용하는 기능이다.
1. 조건부 생성
: 조건에 따라 모듈 또는 코드블록을 생성하거나 생성하지 않을 수 있다.
예를 들어 특정 조건이 충족될 때만 특정 모듈을 생성하고,
다른 조건에서는 생성하지 않을 수 있다.
generate
if (ENABLE_FEATURE)
// 특정 모듈 생성
some_module #(parameter_value) instance_name();
else
// 다른 동작 수행
endgenerate
2. 반복 생성
: 반복적으로 모듈 또는 코드 블록을 생성할 수 있으며,
이 것은 유사한 하드웨어 블록을 여러 번 생성할 때 유용하다.
generate
genvar i;
for (i = 0; i < NUM_INSTANCES; i = i + 1) begin
// 모듈을 NUM_INSTANCES 만큼 반복 생성
some_module #(parameter_value) instance_name[i]();
end
endgenerate
📌 N bit Half_adder
parameter를 활용하여 N bit half adder를 만들어주자.
module half_adder_N_bit #(parameter N = 8)(
// load_data 에 1만 더해주는 기능을 한다.
// 하프 에더이기 때문에 0 또는 1씩만 더해줄 수 있다.
// 1씩 증가하는 카운터를 이 걸로 만들어줄 수 있음.
input inc,
input [N-1:0] load_data,
output [N-1:0] sum
);
wire [N-1:0] carry_out;
half_adder_dataflow ha0(.A(inc), .B(load_data[0]), .sum(sum[0]), .carry(carry_out[0]));
// for문으로 구조적 모델링할 떄
genvar i; // 얘는 회로로 안 만들어진다. 단순히 for문에서만 쓸 변수
generate
for(i = 1 ; i < N ; i = i + 1) begin
half_adder_dataflow ha(.A(carry_out[i-1]), .B(load_data[i]), .sum(sum[i]), .carry(carry_out[i]));
end
endgenerate
endmodule