8-bit ALU
8-bit ALU
একটা ৮-বিট ALU — ADD, SUB, AND, OR, XOR, শিফট, আর zero/carry/overflow/negative ফ্ল্যাগ সহ — একবার Logisim-এ গেট দিয়ে, একবার Verilog-এ, তারপর একই test vector টেবিল দিয়ে দুইটাকেই hardware-verification-এর মতো যাচাই করা।
কেন এই প্রজেক্ট
ALU Design লেসনে আমরা দেখেছি কীভাবে adder, logic gate, আর multiplexer জোড়া লাগিয়ে একটা op-select সিগন্যাল দিয়ে একগুচ্ছ অপারেশনের মধ্যে বেছে নেওয়া যায়। এই প্রজেক্টে সেটা দুইবার বানাব — একবার Logisim/Digital-এ, তার-দিয়ে-জোড়া-লাগানো গেট হিসেবে (চোখে দেখা যায় বিদ্যুৎ কোন পথে যাচ্ছে), আর একবার Verilog-এ, টেক্সট দিয়ে বর্ণনা করে (একটা synthesis tool যেটাকে গেটে রূপান্তর করবে)।
দুইবার বানানোর আসল কারণ হলো — এটাই প্রথমবার আপনি দেখবেন একই ডিজাইন দুইটা সম্পূর্ণ ভিন্ন abstraction level-এ কীভাবে প্রকাশ পায়। Logisim-এ আপনি প্রতিটা তার নিজে টানছেন। Verilog-এ আপনি বলছেন “op যদি SUB হয়, ফলাফল A বিয়োগ B” — আর টুলচেইন বাকিটা বের করে নেয়। Level 2-এর বাকি অংশে (আর Level 3-এ পুরোপুরি) আমরা ধীরে ধীরে দ্বিতীয় পদ্ধতিতে সরে যাব, কিন্তু প্রথমটা না বুঝলে দ্বিতীয়টা জাদুবিদ্যা মনে হবে।
শেষে যে ধাপটা সবচেয়ে গুরুত্বপূর্ণ — verification। “দেখতে ঠিক লাগছে” আর “স্পেসিফিকেশন মেনে চলছে” এক জিনিস না। বাস্তব হার্ডওয়্যার ডিজাইনাররা একটা test vector টেবিল বানান (নির্দিষ্ট ইনপুট → প্রত্যাশিত আউটপুট) আর তার বিপরীতে ডিজাইন যাচাই করেন। এখানে আমরা ঠিক সেটাই করব — দুইটা আলাদা implementation-কে একই স্পেসিফিকেশনের বিপরীতে পরীক্ষা করে দেখব তারা একমত কি না।
ALU স্পেসিফিকেশন
৩-বিট op-select কোড (৭টা ব্যবহৃত, একটা reserved):
| op | অপারেশন | ফলাফল | carry | overflow |
|---|---|---|---|---|
000 | ADD | A + B | carry-out | signed overflow |
001 | SUB | A − B | NOT borrow | signed overflow |
010 | AND | A & B | 0 | 0 |
011 | OR | A | B | 0 | 0 |
100 | XOR | A ^ B | 0 | 0 |
101 | SHL | A-কে ১ বিট বামে শিফট (LSB-তে 0 ঢোকে) | shift-out বিট | 0 |
110 | SHR | A-কে ১ বিট ডানে শিফট (logical, MSB-তে 0 ঢোকে) | shift-out বিট | 0 |
111 | — | সংরক্ষিত (ভবিষ্যতের জন্য ফাঁকা রাখা) | — | — |
সব ক্ষেত্রে zero = (result == 0) আর negative = result[7] (ফলাফলকে two’s complement signed ধরলে তার sign bit)।
ধাপে ধাপে — Logisim / Digital
১. Adder/Subtractor — একই সার্কিট দুই কাজে
একটা ৮-বিট adder/subtractor বানানো যায় মাত্র একটা ট্রিক দিয়ে — B-এর প্রতিটা বিট একটা XOR গেট দিয়ে একটা SUB কন্ট্রোল লাইনের সাথে XOR করুন, আর সেই একই SUB লাইনটা প্রথম full adder-এর carry-in-এ দিন:
B'_i = B_i XOR SUB (SUB=0 হলে B'_i = B_i; SUB=1 হলে B'_i = NOT(B_i))
Cin_0 = SUB
FullAdder_i(A_i, B'_i, Cin_i) → Sum_i, Cout_i
Cin_(i+1) = Cout_i (ripple carry — একটার carry-out পরেরটার carry-in)
কেন কাজ করে: two’s complement-এ A − B = A + (NOT B) + 1। SUB=1 হলে B ইনভার্ট হয়ে যায় (NOT B) আর +1 আসে carry-in থেকে — একই adder চেইন তখন বিয়োগ করছে, কোনো আলাদা সার্কিট ছাড়াই। SUB=0 হলে B'=B, Cin_0=0 — সাধারণ যোগ।
আটটা full adder চেইন করে R[7:0] আর চূড়ান্ত Cout_7 পাবেন।
২. Overflow — carry-based সূত্র
Logisim-এ ripple-carry adder থেকে সরাসরি পাওয়া যায় এমন একটা সূত্র ব্যবহার করা সবচেয়ে সহজ:
overflow = Cout_6 XOR Cout_7
অর্থাৎ, sign bit (বিট ৭)-এ ঢোকা carry আর সেখান থেকে বেরোনো carry যদি আলাদা হয়, তাহলে overflow। এটা মাত্র একটা XOR গেট — অতিরিক্ত কোনো sign-comparison লজিক লাগে না, কারণ Cout_6 আর Cout_7 দুইটাই আগে থেকেই adder চেইনে বিদ্যমান।
৩. AND/OR/XOR — bitwise গেট অ্যারে
আটটা 2-input AND গেট পাশাপাশি (A_i AND B_i), আটটা OR, আটটা XOR — প্রতিটা bit-position-এ স্বাধীনভাবে। কোনো carry propagate করার দরকার নেই, তাই এগুলো adder-এর চেয়ে অনেক সহজ আর দ্রুত (কোনো ripple delay নেই)।
৪. শিফটার — এক-বিট, শুধু ওয়্যারিং
একটা fixed ১-বিট শিফট আসলে কোনো গেট লাগে না — শুধু ওয়্যার পুনর্বিন্যাস:
- SHL:
R[7:1] = A[6:0],R[0] = 0, shift-out বিট (carry) =A[7] - SHR:
R[6:0] = A[7:1],R[7] = 0, shift-out বিট (carry) =A[0]
৫. আউটপুট মাল্টিপ্লেক্সার
৭টা (adder/subtractor একটাই সার্কিট, তাই কার্যত ৬টা ভিন্ন ফলাফল উৎস) ৮-বিট ফলাফল একটা ৮-টু-১ মাল্টিপ্লেক্সারে ঢুকিয়ে op-এর ৩ বিট দিয়ে সিলেক্ট করুন। carry আর overflow আউটপুটও একইভাবে op অনুযায়ী মাল্টিপ্লেক্স করতে হবে (AND/OR/XOR/শিফটে carry=overflow=0 হার্ডওয়্যার্ড)। zero আর negative চূড়ান্ত R[7:0] থেকে সরাসরি বের করা যায় — ওগুলোর জন্য আলাদা মাক্সের দরকার নেই।
ধাপে ধাপে — Verilog
৬. কম্বিনেশনাল ALU মডিউল
module alu8 (
input [7:0] a,
input [7:0] b,
input [2:0] op,
output reg [7:0] result,
output zero,
output reg carry,
output reg overflow,
output negative
);
wire [8:0] add_ext = {1'b0, a} + {1'b0, b};
wire [8:0] sub_ext = {1'b0, a} + {1'b0, ~b} + 9'b1;
always @(*) begin
carry = 1'b0;
overflow = 1'b0;
case (op)
3'b000: begin // ADD
result = add_ext[7:0];
carry = add_ext[8];
overflow = (a[7] == b[7]) && (result[7] != a[7]);
end
3'b001: begin // SUB (a - b)
result = sub_ext[7:0];
carry = sub_ext[8]; // 1 মানে borrow লাগেনি (a >= b, unsigned)
overflow = (a[7] != b[7]) && (result[7] != a[7]);
end
3'b010: result = a & b; // AND
3'b011: result = a | b; // OR
3'b100: result = a ^ b; // XOR
3'b101: begin // SHL
result = {a[6:0], 1'b0};
carry = a[7];
end
3'b110: begin // SHR (logical)
result = {1'b0, a[7:1]};
carry = a[0];
end
default: result = 8'b0; // 111 — সংরক্ষিত
endcase
end
assign zero = (result == 8'b0);
assign negative = result[7];
endmodule
লক্ষ্য করুন add_ext/sub_ext দুইটাই ৯-বিট — a, b-কে একটা অতিরিক্ত শূন্য বিট দিয়ে zero-extend করে যোগ করা হয়েছে, যাতে ৯ম বিট ([8]) সরাসরি carry-out হয়ে যায়। sub_ext = a + ~b + 1 হলো two’s complement বিয়োগের ঠিক সেই সূত্র যা Logisim-এর adder/subtractor ট্রিক-এ হার্ডওয়্যারে হয় — এখানে টেক্সটে লেখা।
Overflow-এর জন্য এখানে sign-comparison সূত্র ব্যবহার করলাম (a[7]==b[7] হয়ে result[7] আলাদা হলে overflow) — Logisim-এর carry-based সূত্রের (Cout_6 XOR Cout_7) সমতুল্য, কিন্তু বিহেভিয়ারাল কোডে বেশি readable। দুইটা সূত্রই সবসময় একই উত্তর দেয় — সেটা নিজে যাচাই করাও একটা ভালো অনুশীলন।
যাচাই — ১০টা টেস্ট ভেক্টর
| A | B | op | R (প্রত্যাশিত) | Z | C | V | N | মন্তব্য |
|---|---|---|---|---|---|---|---|---|
0x05 | 0x03 | ADD | 0x08 | 0 | 0 | 0 | 0 | সাধারণ যোগ |
0xFF | 0x01 | ADD | 0x00 | 1 | 1 | 0 | 0 | unsigned carry হলো, কিন্তু signed overflow নয় (−1 + 1 = 0, সঠিক) |
0x50 | 0x50 | ADD | 0xA0 | 0 | 0 | 1 | 1 | দুইটা positive যোগ হয়ে negative — signed overflow |
0x03 | 0x05 | SUB | 0xFE | 0 | 0 | 0 | 1 | 3−5 = −2, borrow হয়েছে তাই C=0 |
0x80 | 0x01 | SUB | 0x7F | 0 | 1 | 1 | 0 | −128−1 wrap করে +127; unsigned-এ borrow লাগেনি (C=1) তবু signed overflow (V=1) |
0xF0 | 0x0F | AND | 0x00 | 1 | 0 | 0 | 0 | কোনো বিট মেলেনি |
0xF0 | 0x0F | OR | 0xFF | 0 | 0 | 0 | 1 | সব বিট সেট |
0xAA | 0x55 | XOR | 0xFF | 0 | 0 | 0 | 1 | পরিপূরক প্যাটার্ন |
0x81 | — | SHL | 0x02 | 0 | 1 | 0 | 0 | MSB (1) শিফট-আউট হয়ে carry=1 |
0x01 | — | SHR | 0x00 | 1 | 1 | 0 | 0 | LSB (1) শিফট-আউট হয়ে carry=1, ফলাফল শূন্য |
চতুর্থ ও পঞ্চম সারি বিশেষভাবে গুরুত্বপূর্ণ — এরা দেখায় carry আর overflow সম্পূর্ণ আলাদা জিনিস বলছে। 0x80 − 0x01-এ unsigned দৃষ্টিতে কোনো borrow লাগেনি (128 ≥ 1) অথচ signed দৃষ্টিতে ফলাফল রেঞ্জের বাইরে চলে গেছে। একটা মাত্র ফ্ল্যাগ দিয়ে এই দুইটা প্রশ্নের উত্তর একসাথে দেওয়া অসম্ভব — এই জন্যই ALU-তে দুইটা আলাদা ফ্ল্যাগ রাখা হয়।
Logisim-এ প্রতিটা ভেক্টর ম্যানুয়ালি ইনপুট দিয়ে আউটপুট মিলিয়ে দেখুন। Verilog-এ একই কাজ একটা testbench দিয়ে স্বয়ংক্রিয় করা যায়:
`timescale 1ns/1ps
module alu8_tb;
reg [7:0] a, b;
reg [2:0] op;
wire [7:0] result;
wire zero, carry, overflow, negative;
integer errors = 0;
alu8 dut (.a(a), .b(b), .op(op), .result(result),
.zero(zero), .carry(carry), .overflow(overflow), .negative(negative));
task check(input [7:0] ta, input [7:0] tb, input [2:0] top,
input [7:0] exp_r, input exp_z, input exp_c, input exp_v, input exp_n,
input [127:0] label);
begin
a = ta; b = tb; op = top;
#1;
if (result !== exp_r || zero !== exp_z || carry !== exp_c ||
overflow !== exp_v || negative !== exp_n) begin
errors = errors + 1;
$display("FAIL %0s : R=%h(exp %h) Z=%b(exp %b) C=%b(exp %b) V=%b(exp %b) N=%b(exp %b)",
label, result, exp_r, zero, exp_z, carry, exp_c, overflow, exp_v, negative, exp_n);
end else begin
$display("PASS %0s", label);
end
end
endtask
initial begin
check(8'h05, 8'h03, 3'b000, 8'h08, 0,0,0,0, "ADD normal");
check(8'hFF, 8'h01, 3'b000, 8'h00, 1,1,0,0, "ADD carry, no signed overflow");
check(8'h50, 8'h50, 3'b000, 8'hA0, 0,0,1,1, "ADD signed overflow");
check(8'h03, 8'h05, 3'b001, 8'hFE, 0,0,0,1, "SUB borrow");
check(8'h80, 8'h01, 3'b001, 8'h7F, 0,1,1,0, "SUB signed overflow");
check(8'hF0, 8'h0F, 3'b010, 8'h00, 1,0,0,0, "AND");
check(8'hF0, 8'h0F, 3'b011, 8'hFF, 0,0,0,1, "OR");
check(8'hAA, 8'h55, 3'b100, 8'hFF, 0,0,0,1, "XOR");
check(8'h81, 8'h00, 3'b101, 8'h02, 0,1,0,0, "SHL boundary");
check(8'h01, 8'h00, 3'b110, 8'h00, 1,1,0,0, "SHR boundary");
if (errors == 0) $display("সব ১০টা টেস্ট PASS");
else $display("%0d টা টেস্ট FAIL", errors);
$finish;
end
endmodule
iverilog -o alu_sim alu8.v alu8_tb.v && vvp alu_sim চালিয়ে দেখুন — সবগুলো লাইন PASS দেখানো উচিত। কোনোটা FAIL দিলে সেই একটা লাইনই বলে দেবে ঠিক কোন ফ্ল্যাগ বা কোন বিট মিলছে না, পুরো ডিজাইন নতুন করে দেখতে হবে না।
নিজেকে চ্যালেঞ্জ করুন
- Variable-amount শিফট —
op=101/110-এ B-এর নিচের ৩ বিটকে শিফট-অ্যামাউন্ট হিসেবে ব্যবহার করে barrel shifter বানান (log-shifter — ৩টা ধাপে ১/২/৪ বিট শিফটের মাক্স চেইন) - Arithmetic shift right — logical SHR-এর পাশে একটা arithmetic SHR (sign bit repeat) যোগ করুন, op=111 ব্যবহার করে
- Multiply — ৮×৮ = ১৬-বিট গুণ, shift-and-add পদ্ধতিতে (এখনো কম্বিনেশনাল না রেখে multi-cycle করলে পরের প্রজেক্টের FSM ধারণার সাথে মিলবে)
- Fuzz testing — হাজার হাজার র্যান্ডম (A, B, op) জেনারেট করে Verilog সিমুলেশনের ফলাফল Python-এ লেখা একটা reference model-এর সাথে মিলিয়ে দেখুন
- Gate count তুলনা — Logisim ডিজাইনে মোট কয়টা গেট লাগলো গুনুন, তারপর একই ALU শুধু NAND দিয়ে বানালে (Gate Universality-এর নিয়মে) কয়টা লাগত আন্দাজ করুন
এটা যেখানে গিয়ে মিশবে
| এখানে যা শিখলেন | পরে কোথায় লাগবে |
|---|---|
| Adder/subtractor + MUX-ভিত্তিক ALU গঠন | Level 3 — CPU Architecture-এর execute stage, ALU integration |
| Zero/Carry/Overflow/Negative ফ্ল্যাগ | Level 3 — condition codes, branch instruction-এর শর্ত |
| Logisim বনাম Verilog-এ একই ডিজাইন | Level 11 — RTL design workflow, synthesis |
| Test vector দিয়ে যাচাই | এই মডিউলেরই Single-cycle CPU প্রজেক্ট, ও Level 3-এর CPU Emulator প্রজেক্ট |
| Shift/logical অপারেশন | Level 3 (Assembly module) — shift/rotate instruction |