এই পর্বে যা শিখবেন
- প্রতিটি control signal 0 আর 1 হলে datapath-এ ঠিক কী ঘটে
- R-type, lw, sw, beq আর addi-এর main control টেবিল নিজে লেখা, আর প্রতিটি X কেন নিরাপদ তা বোঝানো
- ALUOp, funct7[30] আর funct3 থেকে ALU control-এর ৪ বিট বের করা
- Opcode-এর বিট থেকে K-map দিয়ে control-এর সমীকরণ আর PLA বানানো
- bne, jal, lui যোগ করতে কী বদলাতে হয়, আর একটি control তার আটকে গেলে কোন instruction ভাঙে
১ · Control unit-এর কাজ — কোন সুইচ কখন চালু
পর্ব ৩২-এ single-cycle datapath জোড়া লাগিয়েছেন: PC, instruction memory, register file, immediate generator, ALU, data memory আর কয়েকটি multiplexer. সব তার সবসময় জোড়া থাকে, কিন্তু প্রতিটি instruction এই পথের আলাদা অংশ ব্যবহার করে। add দুটি register যোগ করে register-এ লেখে। lw ঠিকানা হিসাব করে memory থেকে পড়ে। sw memory-তে লেখে, কিন্তু কোনো register-এ নয়। beq কিছুই লেখে না, শুধু PC বদলাতে পারে।
কোন পথ কখন চালু থাকবে, তা বলে দেয় কয়েকটি তার — control signal. আর যে অংশ instruction দেখে এই তারগুলোয় 0 বা 1 বসায়, তার নাম control unit. এই পর্ব পুরোটাই সেই অংশ নিয়ে।
একটি রেস্তোরাঁর রান্নাঘর ভাবুন। ওয়েটার একটি অর্ডার-স্লিপ দেন, তাতে শুধু একটি কোড লেখা। Head chef নিজে কিছু রাঁধেন না। তিনি কোডটি পড়েন আর প্রতিটি স্টেশনে হাঁক দেন: “তন্দুর চালু”, “ভাজার কড়াই বন্ধ”, “এটি পার্সেল, প্লেটে নয়”। স্লিপ হলো instruction, chef হলো control unit, আর স্টেশনগুলো হলো datapath-এর ALU, memory আর register file. Chef ডেটা — অর্থাৎ খাবার — ছোঁন না; তিনি শুধু সুইচ ঠিক করেন।
Control signal তিন রকম। (১) Mux-এর select: কোন ইনপুট সামনে যাবে — ALUSrc, MemtoReg, আর PC-র mux-এর PCSrc. (২) Write enable ও read enable: কোনো state (register বা memory) বদলাবে কি না — RegWrite, MemWrite, MemRead. (৩) ALU-র কাজের কোড: ALUOp, যা পরের স্তরে ALU control-কে যায়। নিচের টেবিলে প্রতিটি signal-এর অর্থ। নামগুলো Patterson–Hennessy-এর Computer Organization and Design-এর RISC-V সংস্করণের, এই কোর্সে যা অনুসরণ করা হচ্ছে।
| signal | 0 হলে | 1 হলে |
|---|---|---|
| RegWrite | কোনো register লেখা হয় না | clock edge-এ rd-তে write data লেখা হয় |
| ALUSrc | ALU-র দ্বিতীয় ইনপুট = rs2-এর মান | ALU-র দ্বিতীয় ইনপুট = sign-extend করা immediate |
| MemtoReg | register-এ লেখার মান = ALU-র ফল | register-এ লেখার মান = data memory থেকে পড়া মান |
| MemRead | memory পড়া হয় না | ALU-র ফলকে ঠিকানা ধরে memory পড়া হয় |
| MemWrite | memory লেখা হয় না | ALU-র ফলের ঠিকানায় rs2-এর মান লেখা হয় |
| Branch | branch নয়, PC ← PC + 4 | branch instruction; ALU-র Zero = 1 হলে PC ← PC + imm |
| ALUOp[1:0] | 00 = যোগ, 01 = বিয়োগ, 10 = funct field দেখে ঠিক হবে (অংশ ২) | |
অন্য বইয়ে নাম একটু আলাদা হতে পারে — যেমন Harris & Harris-এর RISC-V বইয়ে MemtoReg-এর জায়গায় ResultSrc, আর immediate-এর format বাছতে আলাদা ImmSrc signal. ধারণা একই। পুরোনো MIPS সংস্করণে আরও একটি signal ছিল, RegDst; পর্ব ২৭-এ দেখেছেন RISC-V-এ rd সবসময় একই জায়গায় থাকে বলে সেটি লাগে না।
Single-cycle CPU-তে control unit একটি combinational circuit: ইনপুট instruction-এর কয়েকটি বিট, আউটপুট control signal, ভেতরে কোনো স্মৃতি নেই। একই instruction সবসময় একই signal দেয়। Multi-cycle CPU-তে এটি একটি FSM হয়ে যায় (পর্ব ১৭-এর ধারণা, বিস্তারিত পর্ব ৩৪-এ)।
২ · দুই স্তরের decoding — main control আর ALU control
Control unit-কে কী কী দেখতে হয়? opcode (৭ বিট), funct3 (৩ বিট), আর add/sub আলাদা করতে funct7 (৭ বিট)। মোট ১৭ বিট। সব একসাথে একটি টেবিলে দিলে সেই টেবিলে ২১৭ = ১৩১০৭২টি সারি — ROM বানালে বিশাল, gate বানালে জটিল। তাই কাজটি দুই স্তরে ভাগ করা হয়:
- Main control: ইনপুট শুধু opcode. আউটপুট ছয়টি signal আর ২-বিটের ALUOp. ALUOp মোটা দাগে বলে ALU-কে কোন শ্রেণির কাজ করতে হবে।
- ALU control: ইনপুট ALUOp, inst[30] (funct7-এর একমাত্র দরকারি বিট) আর funct3 — মোট ৬ বিট, ২৬ = ৬৪টি সমন্বয়। আউটপুট ৪-বিটের ALU operation, যা পর্ব ১৩-এর ALU সরাসরি বোঝে।
| ALUOp | কোন instruction | ALU যা করবে |
|---|---|---|
00 | lw, sw (আর আমাদের addi) | যোগ — ঠিকানা = rs1 + imm |
01 | beq | বিয়োগ — rs1 − rs2; ফল ০ হলে দুটি সমান |
10 | R-type | funct7[30] আর funct3 দেখে ঠিক হবে |
11 | ব্যবহার হয় না | X — পরে I-type ALU-র জন্য কাজে লাগানো যায় (অংশ ৭) |
লাভ দুটি। প্রথমত, ছোট: main control-কে funct field দেখতে হয় না, ALU control-কে opcode দেখতে হয় না; দুটি ছোট টেবিলের মোট gate একটি বড় টেবিলের চেয়ে অনেক কম। দ্বিতীয়ত, দ্রুত হতে পারে: ছোট circuit-এ gate-এর স্তর কম, আর ALU control-কে ALU-র ঠিক পাশে বসানো যায়, যেখানে funct-এর বিট সরাসরি instruction থেকে আসে। Patterson–Hennessy-ও এই দুই যুক্তিই দেন — একাধিক ছোট control unit main control-এর আকার কমায়, আর গতিও বাড়াতে পারে। রান্নাঘরের ভাষায়: head chef শুধু বলেন “এটি গ্রিলের আইটেম”; মাঝারি না কড়া করে সেঁকা হবে, সেই খুঁটিনাটি গ্রিলের রাঁধুনি নিজেই স্লিপ থেকে পড়ে নেন।
৩ · Main control-এর পূর্ণ truth table — আর প্রতিটি X কেন নিরাপদ
এটিই সেই বিখ্যাত টেবিল, যা বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় বারবার আসে। কলামের ক্রম বইয়ের মতো রাখা হলো। X মানে don't care — 0 বা 1 যেকোনোটি দিলেই instruction ঠিক চলবে।
| instruction | opcode | ALUSrc | MemtoReg | RegWrite | MemRead | MemWrite | Branch | ALUOp1 | ALUOp0 |
|---|---|---|---|---|---|---|---|---|---|
| R-type | 0110011 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
| lw | 0000011 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
| sw | 0100011 | 1 | X | 0 | 0 | 1 | 0 | 0 | 0 |
| beq | 1100011 | 0 | X | 0 | 0 | 0 | 1 | 0 | 1 |
| addi | 0010011 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
প্রথম চার সারি বইয়ের টেবিলের মতো; addi আমরা যোগ করেছি। একটি ছোট পার্থক্য খেয়াল রাখুন: বইয়ের RISC-V সংস্করণ ৬৪-বিটের RV64 ধরে, তাই সেখানে ld/sd লেখা। এখানে RV32I-এর lw/sw. Opcode একই (0000011, 0100011), শুধু funct3 আলাদা, তাই control-এর সারিও হুবহু এক।
প্রতিটি সারি কেন এমন
- R-type: দুটি register-এর মান লাগে, তাই ALUSrc = 0. ফল ALU থেকে register-এ যায়: MemtoReg = 0, RegWrite = 1. Memory-র কাজ নেই। ALUOp = 10, কারণ কোন কাজ — যোগ, বিয়োগ, AND, OR — তা funct বলবে।
- lw: ঠিকানা = rs1 + imm, তাই ALUSrc = 1 আর ALUOp = 00 (যোগ)। Memory পড়া হয় (MemRead = 1), আর পড়া মান register-এ যায়: MemtoReg = 1, RegWrite = 1. পাঁচটি ইউনিটই কাজে লাগে বলে lw-ই single-cycle-এর সবচেয়ে লম্বা পথ, তাই clock-এর মাপ সাধারণত lw-এর পথ দিয়েই ঠিক হয়।
- sw: ঠিকানার হিসাব lw-এর মতোই (ALUSrc = 1, ALUOp = 00). MemWrite = 1, কিন্তু RegWrite = 0 — store কোনো register-এ কিছু ফেরত দেয় না।
- beq: দুটি register তুলনা করতে হবে, তাই ALUSrc = 0, ALUOp = 01 (বিয়োগ)। ফল ০ হলে ALU-র Zero = 1. Branch = 1; কিছু লেখা হয় না।
- addi: rs1 + imm, তাই ALUSrc = 1, ALUOp = 00. ফল ALU থেকে register-এ: MemtoReg = 0, RegWrite = 1.
X কেন নিরাপদ, আর কোথায় X চলবে না
sw আর beq-এ MemtoReg = X. কারণ সরল: MemtoReg শুধু ঠিক করে register-এ কোন মান লেখা হবে। কিন্তু এই দুই instruction-এ RegWrite = 0 — কিছুই লেখা হবে না। Mux যে মানই বাছুক, তা register file-এর দরজায় গিয়ে থেমে যায়। একে বলা যায় “বন্ধ দরজার সামনের পার্সেল”: দরজা না খুললে পার্সেলে কী আছে, তাতে কিছু আসে যায় না।
উল্টো দিকে, RegWrite, MemWrite আর Branch কখনো X হতে পারে না। এরা state বদলায়। sw-এ RegWrite = X লিখলে নকশাকার 1 বেছে নিতে পারেন, তখন sw-এর rd-field (যা আসলে immediate-এর নিচের ৫ বিট) নম্বরের register-এ আবর্জনা লেখা হবে। MemRead-ও সাধারণত X রাখা হয় না: অকারণে memory পড়লে শক্তি খরচ হয়, আর যে ঠিকানা আসলে কোনো I/O device-এর register (memory-mapped I/O), সেটি পড়লেই device-এর অবস্থা বদলে যেতে পারে। নিয়মটি এক লাইনে: যে signal শুধু পথ বাছে, তার ফল যদি কোথাও জমা না হয়, তবেই সেটি X হতে পারে।
নিচের ডায়াগ্রামে একটি আসল instruction word থেকে শুরু করে প্রতিটি signal কীভাবে আসে, ধাপে ধাপে দেখুন। সমীকরণগুলো অংশ ৫-এ K-map থেকে বের করা হবে; এখানে শুধু ব্যবহার।
0x0040A283
= 0000 0000 0100 0000 1010 0010 1000 0011
imm[11:0] = 000000000100 (= 4)
rs1 = 00001 (x1) funct3 = 010 rd = 00101 (x5)
opcode = 0000011 (LOAD)
opcode = op6 op5 op4 op3 op2 op1 op0
= 0 0 0 0 0 1 1
আমাদের পাঁচ instruction-এ শুধু op6, op5, op4 বদলায়
→ main control দেখে op6 op5 op4 = 000
RegWrite = op4 + op5′ = 0 + 1 = 1 ALUSrc = op5′ + op6′·op4′ = 1 + 1 = 1 MemtoReg = op4′ = 1 MemRead = op5′·op4′ = 1 MemWrite = op6′·op5·op4′ = 1·0·1 = 0 Branch = op6 = 0 ALUOp = (op5·op4, op6) = 00
ALUOp = 00 → funct দেখার দরকার নেই
ALU operation = 0010 (add)
সাবধান: funct3 = 010 R-type-এ হলে slt হতো!
ALUOp = 00 সেটিকে উপেক্ষা করায়
ALU : x1 + 4 = 8 + 4 = 12 (ALUSrc = 1 → imm)
Memory : MemRead = 1 → Mem[12] = 13
Write mux: MemtoReg = 1 → 13
Register : RegWrite = 1 → x5 ← 13
PC : Branch = 0 → PCSrc = 0 → PC ← PC + 4
- Field আলাদা করা।
0x0040A283-এর সর্বনিম্ন ৭ বিট0000011— LOAD শ্রেণি। rs1 = x1, rd = x5, immediate = ৪, funct3 =010(word). Main control শুধু opcode দেখবে। - যে বিটগুলো আসলে কাজে লাগে। R, lw, sw, beq, addi — পাঁচটির opcode-এই op1 op0 = 11 আর op3 = op2 = 0. পার্থক্য শুধু op6, op5, op4-এ। lw-এর জন্য সেটি
000. - সমীকরণ। প্রতিটি signal opcode-বিটের একটি ছোট SOP. lw-এ op5′ = 1 আর op4′ = 1, তাই RegWrite, ALUSrc, MemtoReg, MemRead — চারটিই 1; op6 = 0 বলে Branch = 0; MemWrite-এর জন্য op5 = 1 লাগত, তাই 0. ALUOp = 00. ফল টেবিলের lw সারির সাথে হুবহু মেলে।
- দ্বিতীয় স্তর। ALUOp = 00 মানেই যোগ, funct3 যা-ই হোক। এটি জরুরি: lw-এর funct3 =
010, অথচ R-type-এ একই funct3 মানেslt. ALUOp না থাকলে ALU ঠিকানা না বানিয়ে তুলনা করে বসত। - Datapath-এ প্রভাব। ধরুন x1 = ৮ আর ঠিকানা ১২-তে ১৩ রাখা আছে। ALU দেয় ১২, memory দেয় ১৩, MemtoReg = 1 বলে mux ১৩ বাছে, RegWrite = 1 বলে clock edge-এ x5 = ১৩. Branch = 0, তাই পরের instruction PC + ৪-এ।
৪ · ALU control-এর truth table — ALUOp থেকে চার বিট
পর্ব ১৩-এর ALU চার বিটের একটি কোড বোঝে: Ainvert, Bnegate আর ২-বিটের Operation (00 AND, 01 OR, 10 adder, 11 slt). তাই 0010 = যোগ, 0110 = Bnegate চালু রেখে যোগ, অর্থাৎ বিয়োগ, 0000 = AND, 0001 = OR. এই কোড ISA-র অংশ নয় — নকশাকার নিজে ঠিক করেন। ALU control এই কোড বানায়:
| instruction | ALUOp | funct7 | funct3 | কাজ | ALU operation |
|---|---|---|---|---|---|
| lw | 00 | XXXXXXX | XXX | add | 0010 |
| sw | 00 | XXXXXXX | XXX | add | 0010 |
| beq | 01 | XXXXXXX | XXX | subtract | 0110 |
| add | 10 | 0000000 | 000 | add | 0010 |
| sub | 10 | 0100000 | 000 | subtract | 0110 |
| and | 10 | 0000000 | 111 | AND | 0000 |
| or | 10 | 0000000 | 110 | OR | 0001 |
এবার শুধু দরকারি ইনপুট রেখে টেবিলটি ছোট করা যাক। funct7-এর সাতটি বিটের মধ্যে এই চার R-type-এ শুধু inst[30] বদলায়; funct3 হলো inst[14:12]. আর ALUOp = 11 কখনো আসে না, তাই সেটি don't care:
ALUOp1 ALUOp0 │ I30 I14 I13 I12 │ Operation
0 0 │ X X X X │ 0010 lw, sw, addi
X 1 │ X X X X │ 0110 beq
1 X │ 0 0 0 0 │ 0010 add
1 X │ 1 0 0 0 │ 0110 sub
1 X │ 0 1 1 1 │ 0000 and
1 X │ 0 1 1 0 │ 0001 or
দ্বিতীয় সারিতে “X 1” কেন? ALUOp = 11 ঘটে না, তাই ALUOp0 = 1 দেখলেই বোঝা যায় এটি 01 — beq. তেমনি ALUOp1 = 1 মানেই 10. একই minimiser (অংশ ৫-এ যে কৌশল দেখবেন) দিয়ে এই টেবিল থেকে পাওয়া সবচেয়ে ছোট সমীকরণ:
Operation3 = 0
Operation2 = ALUOp0 + ALUOp1 · I30 (Bnegate: beq বা sub)
Operation1 = ALUOp1′ + I14′ (adder বাছা: AND/OR ছাড়া সব)
Operation0 = ALUOp1 · I14 · I12′ (শুধু or: funct3 = 110)
প্রতিটির অর্থ সহজ। Bnegate দরকার বিয়োগে — beq-এ (ALUOp0 = 1) অথবা R-type-এ বিট ৩০ = 1 হলে। Adder (Operation1 = 1) দরকার ALUOp যখন 10 নয়, অথবা funct3-এর বিট ২ = 0 (add/sub-এর 000). আর OR আলাদা হয় AND (111) থেকে শুধু বিট ০-তে, add (000) থেকে বিট ২-তে — তাই I14 · I12′.
addi-কে ALUOp = 10 দেবেন না। মনে হতে পারে, addi তো ALU-র কাজ, funct3 = 000 — R-type-এর মতো funct দেখলেই হয়। কিন্তু I-type-এ বিট ৩১..২০ পুরোটাই immediate; inst[30] সেখানে immediate-এর বিট ১০. addi x5, x6, -1024-এর immediate 1100 0000 0000, তাই inst[30] = 1 — ALU control সেটিকে sub ভেবে বসবে। এজন্যই টেবিলে addi-এর ALUOp = 00. পুরো OP-IMM (andi, ori, slli…) চাইলে ALUOp = 11-কে “I-type ALU” বানাতে হয়, যেখানে funct3 দেখা হয় কিন্তু inst[30] শুধু funct3 = 101 (srli/srai)-তে।
এবার sub-এর জন্য চারটি বিট হাতে বের করে দেখুন:
0x40208233
opcode = 0110011 → op6 op5 op4 = 011 (R-type)
ALUOp1 = op5·op4 = 1·1 = 1
ALUOp0 = op6 = 0
→ ALUOp = 10 ("funct দেখে ঠিক হবে")
0100 0000 0010 0000 1000 0010 0011 0011 ↑ I30 = 1 ↑ I14 I13 I12 = 000 funct7 = 0100000, funct3 = 000
Operation3 = 0
Operation2 = ALUOp0 + ALUOp1·I30
= 0 + 1·1 = 1 (Bnegate চালু)
Operation1 = ALUOp1′ + I14′ = 0 + 1 = 1 (adder) Operation0 = ALUOp1·I14·I12′ = 1·0·1 = 0
sub : 0 1 1 0 = 0110 → A + B′ + 1 = 8 − 5 = 3
add : I30 = 0 → Operation2 = 0 → 0010
and : I14 = 1 → Operation1 = 0, I12 = 1 → 0000
শুধু একটি বিট (I30) add আর sub আলাদা করে
- প্রথম স্তর। opcode
0110011-এ op5 = op4 = 1, op6 = 0. তাই ALUOp1 = 1, ALUOp0 = 0 — ALUOp =10. Main control নিজে জানে না এটি add না sub; সেই সিদ্ধান্ত পরের স্তরের। - দরকারি বিট তোলা। word-এর বিট ৩০ = 1 (hex-এর প্রথম অঙ্ক 4) আর বিট ১৪..১২ =
000. ALU control-এর কাছে আসে এই চারটি বিট আর ALUOp — মোট ছয়টি তার। - Bnegate. Operation2 = ALUOp0 + ALUOp1·I30. ALUOp0 = 0, কিন্তু ALUOp1 = 1 আর I30 = 1, তাই 1 — দ্বিতীয় operand উল্টে carry-in 1 দেওয়া হবে।
- কোন ইউনিট। Operation1 = ALUOp1′ + I14′. ALUOp1′ = 0, কিন্তু I14 = 0, তাই I14′ = 1 — adder বাছা হলো। Operation0 = ALUOp1·I14·I12′-এ I14 = 0, তাই 0.
- ফল।
0110— বিয়োগ। x1 = ৮, x2 = ৫ হলে x4 = ৩. add-এ শুধু I30 = 0, তাই0010; and-এ funct3 =111, তাই0000. পর্ব ২৭-এর “একটি বিটের গল্প” এখানে hardware-এ পৌঁছাল।
এখন দুই স্তর একসাথে চালিয়ে দেখুন। নিচের সিমুলেটর আটটি RV32I word নেয়, opcode, inst[30] আর funct3 বের করে, main control-এর টেবিল থেকে signal আর ALUOp ছাপে, ALU control-এর টেবিল থেকে ৪ বিট বের করে, আর সেই ফলকে উপরের gate-সমীকরণের সাথে মিলিয়ে দেখে। শেষ লাইনে addi-এর ফাঁদটিও যাচাই হয়। Word-গুলো একটি আলাদা Python encoder দিয়ে বানানো:
// Two-level control for a single-cycle RV32I subset: main control (opcode) -> ALUOp -> ALU control.
const MAIN = { // opcode: [type, ALUSrc, MemtoReg, RegWrite, MemRead, MemWrite, Branch, ALUOp]; X = don't care
0x33: ["R-type", 0, 0, 1, 0, 0, 0, "10"], 0x13: ["addi", 1, 0, 1, 0, 0, 0, "00"],
0x03: ["lw", 1, 1, 1, 1, 0, 0, "00"], 0x23: ["sw", 1, "X", 0, 0, 1, 0, "00"],
0x63: ["beq", 0, "X", 0, 0, 0, 1, "01"] };
function aluControl(aluOp, i30, f3) { // second level, straight from the ALU control table
if (aluOp === "00") return "0010"; // lw, sw, addi: address / immediate add
if (aluOp === "01") return "0110"; // beq: subtract, then look at Zero
return { "0000": "0010", "1000": "0110", "0111": "0000", "0110": "0001" }[i30 + f3] ?? "????";
}
function aluGates(aluOp, i30, f3) { // the same table as minimised gates
const a1 = +aluOp[0], a0 = +aluOp[1], i14 = +f3[0], i12 = +f3[2];
return "0" + (a0 | (a1 & i30)) + ((1 - a1) | (1 - i14)) + (a1 & i14 & (1 - i12));
}
const OP = { "0010": "add", "0110": "sub", "0000": "and", "0001": "or" };
const words = [0x002081B3, 0x40208233, 0x0020F433, 0x0020E3B3, 0x00800093, 0x0040A283, 0x0030A223, 0x00518463];
console.log("word opcode i30 f3 | type AS M2R RW MR MW Br ALUOp | ALUctl op gates");
for (const w of words) {
const op = w & 0x7F, i30 = (w >>> 30) & 1, f3 = ((w >>> 12) & 7).toString(2).padStart(3, "0");
const [type, ...s] = MAIN[op], aluOp = s[6], ctl = aluControl(aluOp, i30, f3), g = aluGates(aluOp, i30, f3);
console.log(`${w.toString(16).toUpperCase().padStart(8, "0")} ${op.toString(2).padStart(7, "0")} ${i30} ${f3} | ` +
`${type.padEnd(6)} ${s.slice(0, 6).map((v, k) => String(v).padEnd(k === 1 ? 4 : 3)).join("")}${aluOp} | ` +
`${ctl} ${OP[ctl].padEnd(4)} ${g === ctl ? "same" : "DIFF"}`);
}
const w = 0xC0030293; // addi x5, x6, -1024: inst[30] = 1 (an immediate bit!)
const f3 = ((w >>> 12) & 7).toString(2).padStart(3, "0"), i30 = (w >>> 30) & 1;
console.log(`addi x5,x6,-1024 (i30=${i30}): ALUOp=00 -> ${OP[aluControl("00", i30, f3)]},` +
` but if ALUOp were 10 -> ${OP[aluControl("10", i30, f3)]}`);আউটপুট (নির্ধারিত — প্রতিবার একই; কলামের ক্রম এই কোর্সের টেবিলের মতো: AS = ALUSrc, M2R = MemtoReg, RW = RegWrite, MR = MemRead, MW = MemWrite, Br = Branch):
word opcode i30 f3 | type AS M2R RW MR MW Br ALUOp | ALUctl op gates
002081B3 0110011 0 000 | R-type 0 0 1 0 0 0 10 | 0010 add same
40208233 0110011 1 000 | R-type 0 0 1 0 0 0 10 | 0110 sub same
0020F433 0110011 0 111 | R-type 0 0 1 0 0 0 10 | 0000 and same
0020E3B3 0110011 0 110 | R-type 0 0 1 0 0 0 10 | 0001 or same
00800093 0010011 0 000 | addi 1 0 1 0 0 0 00 | 0010 add same
0040A283 0000011 0 010 | lw 1 1 1 1 0 0 00 | 0010 add same
0030A223 0100011 0 010 | sw 1 X 0 0 1 0 00 | 0010 add same
00518463 1100011 0 000 | beq 0 X 0 0 0 1 01 | 0110 sub same
addi x5,x6,-1024 (i30=1): ALUOp=00 -> add, but if ALUOp were 10 -> sub
আটটি সারিই main control-এর টেবিলের সাথে মেলে, আর প্রতিটির শেষে same — টেবিল থেকে পাওয়া ALU operation আর gate-সমীকরণের ফল এক। চারটি R-type-এর প্রথম সাতটি কলাম হুবহু এক (0 0 1 0 0 0 10); পার্থক্য আসে শুধু দ্বিতীয় স্তরে, i30 আর f3 থেকে: 0010, 0110, 0000, 0001. lw আর sw-এর f3 = 010, তবুও ALUOp = 00 বলে ফল add. sw আর beq-এর M2R কলামে X — টেবিলের don't care. শেষ লাইন দেখায় addi x5,x6,-1024-এর i30 = 1; ALUOp = 00 থাকায় add হয়, কিন্তু ALUOp = 10 দিলে ALU control একে sub বানাত।
৫ · Opcode-এর বিট থেকে সমীকরণ — K-map দিয়ে সরলীকরণ
টেবিল তো হলো; এখন এটিকে gate বানাতে হবে। প্রথমে পাঁচটি opcode বিট ধরে পাশাপাশি লিখুন:
instruction op6 op5 op4 op3 op2 op1 op0
R-type 0 1 1 0 0 1 1
lw 0 0 0 0 0 1 1
sw 0 1 0 0 0 1 1
beq 1 1 0 0 0 1 1
addi 0 0 1 0 0 1 1
op3 থেকে op0 পাঁচটিতেই এক (0011), তাই আমাদের ছোট CPU-র জন্য এরা কোনো তথ্য দেয় না। বাকি থাকে তিনটি চলক — op6, op5, op4 — অর্থাৎ ৩-চলকের K-map (পর্ব ০৯)। আটটি ঘরের পাঁচটিতে আমাদের instruction; বাকি তিনটি (100, 101, 111) আমাদের CPU-তে কখনো আসবে না বলে ধরে নিই, তাই X. সারি op6, কলাম op5 op4 (Gray ক্রমে 00, 01, 11, 10):
op5op4: 00 01 11 10
op6 = 0 lw addi R sw
op6 = 1 (X) (X) (X) beq
এবার প্রতিটি signal-এর জন্য ঘরগুলোয় তার মান বসিয়ে দল বানান। এখানে চারটি পুরো করে দেখানো হলো (নোটেশন: A′ মানে A-এর উল্টো; কোডে ~A বা 1 - A).
RegWrite 00 01 11 10 দল ১: কলাম 00,01 (1 1 X X) → op5′
op6 = 0 1 1 1 0 দল ২: কলাম 01,11 (1 1 X X) → op4
op6 = 1 X X X 0 RegWrite = op5′ + op4
ALUSrc 00 01 11 10 দল ১: কলাম 00,01 (1 1 X X) → op5′
op6 = 0 1 1 0 1 দল ২: সারি 0-এর 10 আর 00 (ঘুরে পাশাপাশি) → op6′·op4′
op6 = 1 X X X 0 ALUSrc = op5′ + op6′·op4′
MemtoReg 00 01 11 10 sw, beq-এর ঘরও X (অংশ ৩)
op6 = 0 1 0 0 X দল: কলাম 00 আর 10, দুই সারি (1 X X X) → op4′
op6 = 1 X X X X MemtoReg = op4′
Branch 00 01 11 10 দল: পুরো দ্বিতীয় সারি (X X X 1) → op6
op6 = 0 0 0 0 0 Branch = op6
op6 = 1 X X X 1
লক্ষ করুন, X না থাকলে প্রতিটি সমীকরণ অনেক বড় হতো। যেমন Branch-এর জন্য তখন beq-এর পুরো minterm op6·op5·op4′ লাগত; X-এর কারণে মাত্র একটি literal, op6. MemtoReg-এ sw আর beq-এর X-ও কাজে লেগেছে: op4′ দলটি সেই দুই ঘরকে 1 ধরে নেয় — নিরাপদ, কারণ তখন RegWrite = 0. বাকি চারটি signal একইভাবে:
| signal | R, lw, sw, beq (বইয়ের চারটি) | addi সহ পাঁচটি |
|---|---|---|
| RegWrite | op4 + op5′ | op4 + op5′ |
| ALUSrc | op6′·op4′ | op5′ + op6′·op4′ |
| MemtoReg | op5′ | op4′ |
| MemRead | op5′ | op5′·op4′ |
| MemWrite | op6′·op5·op4′ | op6′·op5·op4′ |
| Branch | op6 | op6 |
| ALUOp1 | op4 | op5·op4 |
| ALUOp0 | op6 | op6 |
দুই কলামের তুলনাই এই অংশের বড় শিক্ষা। addi আসার আগে 001 ঘরটি X ছিল; addi এলে সেটি একটি নির্দিষ্ট মান পায়, আর যে সমীকরণ সেই X-এর ওপর নির্ভর করছিল, সেটি ভাঙে। যেমন ALUOp1 = op4 থাকলে addi-তেও ALUOp1 = 1 হতো — ঠিক উপরের সতর্কবার্তার ফাঁদ। তাই নতুন instruction যোগ করা মানে শুধু একটি সারি যোগ নয়; পুরোনো সব সমীকরণ আবার যাচাই করা।
বাকি তিনটি ঘর সত্যিই X কি না, তা নির্ভর করে CPU কোন instruction সমর্থন করে তার ওপর। RISC-V-এ 1000011 (op6..op4 = 100) হলো F extension-এর MADD, 1010011 (101) হলো OP-FP, আর 1110011 (111) হলো SYSTEM, যেখানে ecall থাকে। আমাদের Branch = op6 সমীকরণে একটি ecall এলে Branch = 1 হয়ে যাবে! শেখার CPU-তে এটি মেনে নেওয়া যায়, কিন্তু বাস্তব CPU-কে পুরো opcode দেখে অচেনা বা অসমর্থিত instruction-এ illegal-instruction exception তুলতে হয় (পর্ব ৩৮)।
হাতে K-map ছোট টেবিলে চলে, কিন্তু চলক বাড়লে যন্ত্র লাগে। নিচের minimiser পুরো ৭টি opcode বিট নেয়, টেবিলে নেই এমন সব opcode-কে don't care ধরে, আর প্রতিটি signal-এর জন্য সবচেয়ে ছোট SOP খোঁজে — প্রথমে সবচেয়ে কম product term, তারপর সবচেয়ে কম literal. কৌশলটি নিখুঁত খোঁজ: যে instruction-গুলোতে signal = 1, তাদের সম্ভাব্য সব দলে ভাগ করা; প্রতিটি দলের জন্য সেই বিটগুলোর সবচেয়ে ছোট উপসেট বাছা, যা 0-সারির সবগুলোকে বাদ দেয়। তারপর সমীকরণটিকে প্রতিটি instruction-এ চালিয়ে টেবিলের সাথে মেলায়:
// Minimal SOP for every main-control signal from opcode bits op6..op0; unused opcodes = don't care.
const INS = { R: "0110011", lw: "0000011", sw: "0100011", beq: "1100011", addi: "0010011" };
const SIG = { RegWrite: "11001", ALUSrc: "01101", MemtoReg: "01XX0", MemRead: "01000", // R lw sw beq addi
MemWrite: "00100", Branch: "00010", ALUOp1: "10000", ALUOp0: "00010" }; // X = don't care
const names = Object.keys(INS), lit = (i, v) => "op" + (6 - i) + (v === "1" ? "" : "'");
function bestCube(block, off) { // fewest literals among the bits on which the whole block agrees
let best = null, fixed = [...Array(7).keys()].filter(i => block.every(m => m[i] === block[0][i]));
for (let mask = 0; mask < 1 << fixed.length; mask++) {
const keep = fixed.filter((_, j) => (mask >> j) & 1); // the cube must exclude every 0-row
if (off.every(o => keep.some(i => o[i] !== block[0][i])) && (!best || keep.length < best.length)) best = keep;
}
return best && best.map(i => lit(i, block[0][i]));
}
function* parts(a) { // every way to split the 1-rows into groups (one product term each)
if (!a.length) return yield [];
for (const p of parts(a.slice(1))) {
for (let i = 0; i < p.length; i++) yield p.map((b, j) => (j === i ? [a[0], ...b] : b));
yield [[a[0]], ...p]; } }
function minimise(on, off) { let best = null, cost = t => t.length * 100 + t.flat().length; // terms, then literals
for (const p of parts(on)) { const t = p.map(b => bestCube(b, off));
if (!t.includes(null) && (!best || cost(t) < cost(best))) best = t; }
return best;
}
const val = (t, m) => +t.some(c => c.every(l => (m[6 - +l[2]] === "1") === !l.endsWith("'")));
for (const set of [names.slice(0, 4), names]) {
console.log(`-- ${set.join(", ")} (columns: value given by the equation)`);
for (const [sig, col] of Object.entries(SIG)) {
const want = n => col[names.indexOf(n)], pick = v => set.filter(n => want(n) === v).map(n => INS[n]);
const eq = minimise(pick("1"), pick("0")), got = set.map(n => val(eq, INS[n]));
const ok = set.every((n, k) => want(n) === "X" || +want(n) === got[k]);
console.log(`${sig.padEnd(8)} = ${eq.map(c => c.join("·")).join(" + ").padEnd(18)} ` +
`${set.map((n, k) => n + ":" + (want(n) === "X" ? "X→" : "") + got[k]).join(" ").padEnd(36)} ${ok ? "ok" : "FAIL"}`);
}
}আউটপুট (নির্ধারিত — প্রতিবার একই):
-- R, lw, sw, beq (columns: value given by the equation)
RegWrite = op4 + op5' R:1 lw:1 sw:0 beq:0 ok
ALUSrc = op6'·op4' R:0 lw:1 sw:1 beq:0 ok
MemtoReg = op5' R:0 lw:1 sw:X→0 beq:X→0 ok
MemRead = op5' R:0 lw:1 sw:0 beq:0 ok
MemWrite = op6'·op5·op4' R:0 lw:0 sw:1 beq:0 ok
Branch = op6 R:0 lw:0 sw:0 beq:1 ok
ALUOp1 = op4 R:1 lw:0 sw:0 beq:0 ok
ALUOp0 = op6 R:0 lw:0 sw:0 beq:1 ok
-- R, lw, sw, beq, addi (columns: value given by the equation)
RegWrite = op4 + op5' R:1 lw:1 sw:0 beq:0 addi:1 ok
ALUSrc = op6'·op4' + op5' R:0 lw:1 sw:1 beq:0 addi:1 ok
MemtoReg = op4' R:0 lw:1 sw:X→1 beq:X→1 addi:0 ok
MemRead = op5'·op4' R:0 lw:1 sw:0 beq:0 addi:0 ok
MemWrite = op6'·op5·op4' R:0 lw:0 sw:1 beq:0 addi:0 ok
Branch = op6 R:0 lw:0 sw:0 beq:1 addi:0 ok
ALUOp1 = op5·op4 R:1 lw:0 sw:0 beq:0 addi:0 ok
ALUOp0 = op6 R:0 lw:0 sw:0 beq:1 addi:0 ok
দুই সেটেই সব সারি ok, আর সমীকরণগুলো উপরের হাতে করা K-map আর টেবিলের সাথে হুবহু এক — যদিও প্রোগ্রামটি ৭টি বিটই দেখেছে। কারণ op3..op0 পাঁচটি instruction-এ একই, তাই কোনো 0-সারিকে বাদ দিতে এরা কাজে লাগে না। X-এর কলামগুলো সবচেয়ে মজার: চার instruction-এর সেটে MemtoReg = op5′, তাই sw আর beq-এ X→0; addi যোগ হলে MemtoReg = op4′, তখন একই দুই ঘর X→1. দুটিই বৈধ, কারণ সেখানে RegWrite = 0. অর্থাৎ X-এর “আসল” মান নকশাকার ঠিক করেন না — minimisation যেটি বেছে নেয়, chip-এ সেটিই থাকে। বড় আকারে এই কাজটি করে Quine-McCluskey বা Espresso-র মতো পদ্ধতি (পর্ব ১০)।
৬ · Hardwired control-এর circuit — PLA বা দুই স্তরের AND–OR
সমীকরণগুলো সবই SOP, তাই সরাসরি দুই স্তরের logic-এ বসে: প্রথমে AND, তারপর OR. এমন নকশাকে বলে hardwired control — নিয়ন্ত্রণের নিয়ম তারের সংযোগেই গাঁথা। একে নিয়মিত আকারে বানানোর পুরোনো আর পরিচিত উপায় PLA: একটি AND-তল আর একটি OR-তল। আমাদের আটটি signal-এর জন্য আলাদা product term লাগে মাত্র আটটি:
ছবিতে তিনটি জিনিস খেয়াল করুন। প্রথমত, product term ভাগাভাগি হয়: P2 (op5′) RegWrite আর ALUSrc দুজনেই নেয়, P7 (op6) যায় Branch আর ALUOp0-তে। PLA-র সুবিধাই এটি — একটি AND-পদ একবার বানিয়ে যত output-এ খুশি জোড়া যায়। দ্বিতীয়ত, এক-literal-এর পদ (P1, P2, P4, P7) আসলে কোনো AND gate চায় না; বাস্তব নকশায় তারা সোজা তার, আর একটিমাত্র পদের output (M2R, MR, MW, Br, Op1, Op0)-এর OR gate-ও লাগে না। তৃতীয়ত, delay: inverter → AND → OR, মোটামুটি তিন gate-এর পথ। Single-cycle CPU-তে এই সময় register file পড়ার সাথে একসাথে চলে, তাই সাধারণত এটি critical path-এর বড় অংশ নয়; ALU control-এর দুই স্তর যোগ হলেও তা ALU-র নিজের delay-এর তুলনায় ছোট।
Verilog-এ (পর্ব ১৪) একই নকশা কয়েকটি assign-এ লেখা যায়। নিচেরটি উদাহরণ (এই পাতায় চালানো যায় না), synthesis-যোগ্য ধাঁচে লেখা:
// উদাহরণ: hardwired main control — RV32I subset (R-type, lw, sw, beq, addi)
module main_control (
input wire [6:0] opcode,
output wire RegWrite, ALUSrc, MemtoReg, MemRead, MemWrite, Branch,
output wire [1:0] ALUOp
);
wire o6 = opcode[6], o5 = opcode[5], o4 = opcode[4];
assign RegWrite = o4 | ~o5;
assign ALUSrc = ~o5 | (~o6 & ~o4);
assign MemtoReg = ~o4;
assign MemRead = ~o5 & ~o4;
assign MemWrite = ~o6 & o5 & ~o4;
assign Branch = o6;
assign ALUOp = {o5 & o4, o6};
endmodule
শিল্পে সাধারণত উল্টো পথে লেখা হয়: পুরো ৭-বিট opcode-এর ওপর একটি case, প্রতিটি instruction-এর জন্য signal-এর সারি, আর default-এ সব write enable 0 (সাথে একটি illegal-instruction signal). তারপর synthesis tool নিজেই minimisation করে। এটি আসলে পর্ব ১২-এর decoder-এর ধারণা: প্রতিটি instruction-এর জন্য ৭ বিটের পূর্ণ AND (one-hot লাইন), তারপর OR. Gate একটু বেশি লাগে, কিন্তু অচেনা opcode কখনো ভুল করে কোনো signal চালু করে না। পাঠ্যবইয়ের পুরোনো MIPS সংস্করণের একটি পরিশিষ্টেও এভাবে প্রতিটি instruction-এর জন্য একটি AND-পদ রেখে PLA দেখানো হয়েছিল।
৭ · নতুন instruction যোগ করা — bne, jal, lui আর PCSrc
PCSrc = Branch · Zero
PC-র সামনের mux দুটি ঠিকানার একটি বাছে: PC + ৪, নাকি PC + imm (branch target). এর select, PCSrc, main control সরাসরি দেয় না — কারণ branch নেওয়া হবে কি না, তা জানা যায় ALU তুলনা শেষ করার পর। তাই main control শুধু বলে “এটি branch” (Branch = 1), আর একটি AND gate সেটিকে ALU-র Zero-র সাথে মেলায়:
PCSrc = Branch · Zero
beq x3, x5, 8 : ALU = x3 − x5; সমান হলে Zero = 1 → PCSrc = 1 → PC ← PC + 8
bne — নতুন opcode নয়, তাই main control-এর নতুন সারিও নয়
পরীক্ষায় খুব চেনা প্রশ্ন: “datapath-এ bne যোগ করতে কী বদলাবেন?” প্রথম পর্যবেক্ষণ — bne-এর opcode beq-এর মতোই 1100011; পার্থক্য শুধু funct3-এ (beq 000, bne 001). Main control শুধু opcode দেখে, তাই সে দুটিকে আলাদা করতেই পারে না: দুটির জন্যই ALUSrc = 0, ALUOp = 01, Branch = 1. পরিবর্তন লাগে branch-এর শর্তে। দুটি সমান বৈধ উপায়:
উপায় ১ (funct3 দিয়ে উল্টানো):
PCSrc = Branch · (Zero ⊕ funct3[0])
beq: funct3[0] = 0 → Zero bne: funct3[0] = 1 → Zero′
উপায় ২ (আলাদা signal):
decoder funct3 দেখে Branch আর BranchNE আলাদা করে
PCSrc = Branch · Zero + BranchNE · Zero′
প্রথমটিতে একটি XOR gate যোগ হলো, main control একেবারে অপরিবর্তিত। blt, bge-এর জন্য Zero যথেষ্ট নয় — ফলের চিহ্ন আর overflow লাগে (পর্ব ১৩-এর slt-এর যুক্তি), তাই সেখানে ALU থেকে আরও একটি flag বা একটি আলাদা comparator লাগে।
jal আর lui — নতুন পথ, নতুন signal
jal rd, offset দুটি কাজ করে: PC ← PC + imm (সবসময়, কোনো শর্ত নেই), আর rd ← PC + ৪. Datapath-এ branch-এর adder আগে থেকেই PC + imm বানায়, তবে immediate generator-কে J-format চিনতে হবে। নতুন যা লাগে: (১) একটি Jump signal, আর PCSrc = Branch · Zero + Jump; (২) register-এ লেখার mux-এ তৃতীয় ইনপুট PC + ৪, তাই MemtoReg দুই বিটের হয় (00 = ALU, 01 = memory, 10 = PC + ৪)। jalr-এর লক্ষ্য rs1 + imm, যা ALU-র ফল — তাই PC-র mux-এ আরও একটি ইনপুট লাগে।
lui rd, imm rd-তে লেখে imm << ১২. এর বিট ১৯..১৫ immediate-এর অংশ, তাই “rs1 = x0 ধরে যোগ” ভরসা করা যায় না — register file সেখানে যে register-ই পড়ুক, তা আসল x0 না-ও হতে পারে। একটি সহজ সমাধান: ALU-র প্রথম ইনপুটে একটি নতুন mux (ALUSrcA: 0 = rs1, 1 = শূন্য), তারপর ALUSrc = 1, ALUOp = 00 — ফল ০ + imm. (একই mux-এ PC ইনপুট যোগ করলে auipc-ও হয়ে যায়।) সব মিলিয়ে বাড়ানো টেবিল:
| instr | opcode | ALUSrc | ALUSrcA | MemtoReg | RegWrite | MemRead | MemWrite | Branch | Jump | ALUOp |
|---|---|---|---|---|---|---|---|---|---|---|
| R-type | 0110011 | 0 | 0 | 00 | 1 | 0 | 0 | 0 | 0 | 10 |
| lw | 0000011 | 1 | 0 | 01 | 1 | 1 | 0 | 0 | 0 | 00 |
| sw | 0100011 | 1 | 0 | XX | 0 | 0 | 1 | 0 | 0 | 00 |
| beq / bne | 1100011 | 0 | 0 | XX | 0 | 0 | 0 | 1 | 0 | 01 |
| addi | 0010011 | 1 | 0 | 00 | 1 | 0 | 0 | 0 | 0 | 00 |
| jal | 1101111 | X | X | 10 | 1 | 0 | 0 | 0 | 1 | XX |
| lui | 0110111 | 1 | 1 | 00 | 1 | 0 | 0 | 0 | 0 | 00 |
jal-এ ALUSrc, ALUSrcA আর ALUOp X, কারণ ALU-র ফল কোথাও যায় না: লেখা হয় PC + ৪, লাফ দেয় branch adder. এবার সমীকরণে কী ভাঙে, দেখুন। jal-এর op6..op4 = 110 — beq-এর সাথে হুবহু এক! পুরোনো Branch = op6 তাই jal-এও Branch = 1 দিত। পার্থক্য op3-এ (beq 0, jal 1), তাই Branch = op6·op3′, আর Jump = op3. তেমনি lui-এর op6..op4 = 011, R-type-এর সমান; পার্থক্য op2-এ। তাই ALUOp1 = op5·op4 থেকে হয় op5·op4·op2′, আর ALUSrcA = op2. একই minimiser দিয়ে সাত instruction-এর পূর্ণ সমাধান যাচাই করা হয়েছে: RegWrite = op4 + op5′ + op2, ALUSrc = op6′·op4′ + op5′ + op2, MemtoReg[1] = op6, MemtoReg[0] = op6′·op4′, Branch = op6·op3′, Jump = op3. যে বিট আগে “অকেজো” ছিল, নতুন instruction-এ সেটিই সিদ্ধান্ত নেয়।
৮ · Stuck-at fault — একটি control তার আটকে গেলে
কারখানায় chip বানানোর সময় কোনো তার মাটির সাথে লেগে যেতে পারে (সবসময় 0), বা সরবরাহের সাথে (সবসময় 1). পরীক্ষার জন্য এই ত্রুটিকে সরল একটি মডেলে ভাবা হয়: stuck-at fault — তারটি “stuck-at-0” বা “stuck-at-1”. Control-এর তারে এমন ত্রুটি হলে কোন instruction ভুল করবে, তা টেবিল দেখেই বলা যায়: যেসব instruction-এ ওই signal-এর দরকারি মান আটকে থাকা মানের উল্টো, শুধু তারাই ভাঙে; যেখানে signal-টি X, সেখানে কিছু হয় না।
| ত্রুটি | কোন instruction ভাঙে | কেন |
|---|---|---|
| RegWrite stuck-at-0 | R-type, lw, addi | ফল কখনো register-এ লেখা হয় না; sw, beq-এ এমনিতেই 0, তাই ঠিক |
| RegWrite stuck-at-1 | sw, beq | rd-field-এর নম্বরে (sw-এ imm[4:0], beq-এ imm[4:1] আর imm[11]) আবর্জনা লেখা হয় |
| ALUSrc stuck-at-1 | R-type, beq | rs2-এর বদলে immediate generator-এর মান; R-type-এ তা inst[31:20] = funct7 আর rs2-field-এর বিট |
| ALUSrc stuck-at-0 | lw, sw, addi | immediate-এর বদলে rs2-field নম্বরের register; lw/addi-তে সেটি আসলে imm[4:0] |
| MemtoReg stuck-at-0 | শুধু lw | memory-র মানের বদলে ঠিকানা (ALU-র ফল) লেখা হয় |
| MemtoReg stuck-at-1 | R-type, addi | ALU-র ফলের বদলে memory-র output লেখা হয়, অথচ MemRead = 0 |
| Branch stuck-at-0 | beq (শুধু যখন নেওয়ার কথা) | শর্ত সত্য হলেও PC + ৪ |
| Branch stuck-at-1 | ALU-র ফল ০ এমন যেকোনো instruction | Zero = 1 হলেই PC ← PC + imm |
শেষ সারিটি সূক্ষ্ম: Branch stuck-at-1 সব instruction ভাঙে না, শুধু যার ALU-র ফল কাকতালীয়ভাবে ০. পরীক্ষার প্রশ্নে এই “শর্তসাপেক্ষ” উত্তরই সবচেয়ে বেশি নম্বর পায়। এবার MemtoReg stuck-at-0 একটি ছোট প্রোগ্রামে ধাপে ধাপে দেখুন:
addi x1, x0, 8 → x1 = 8
addi x2, x0, 5 → x2 = 5
add x3, x1, x2 → x3 = 13
sw x3, 4(x1) → Mem[12] = 13
এবার চলবে: lw x5, 4(x1)
AS M2R RW MR MW Br ALUOp
হওয়ার কথা : 1 1 1 1 0 0 00
ত্রুটিসহ : 1 0 1 1 0 0 00
শুধু MemtoReg বদলেছে — তার মাটিতে লেগে আছে
ALU : x1 + 4 = 12 Memory : MemRead = 1 → Mem[12] = 13 পড়া হলো এখন পর্যন্ত কোনো ভুল নেই
mux-এর ইনপুট: 0 → ALU-র ফল = 12
1 → memory = 13
MemtoReg = 0 → বাছা হলো 12
x5 ← 12 (ঠিক উত্তর 13)
beq x3, x5, 8 : 13 − 12 ≠ 0 → Zero = 0 → নেওয়া হয় না
addi x6, x0, 1 : চলে যায় → x6 = 1 (হওয়ার কথা 0)
শেষে: x5 = 12, x6 = 1; বাকি সব ঠিক
MemtoReg = 1 লাগে শুধু lw-এ
R-type, addi: দরকার 0 → ঠিক
sw, beq : X → ঠিক
→ ভাঙে শুধু lw
টেস্ট: lw দিয়ে এমন মান আনুন যা ঠিকানার সমান নয়
- প্রস্তুতি। প্রথম চারটি instruction-এ MemtoReg = 0 দরকার বা X, তাই ত্রুটি থাকলেও তারা ঠিক চলে: x1 = ৮, x2 = ৫, x3 = ১৩, আর ঠিকানা ১২-তে ১৩ লেখা হয়।
- একটি তারের পার্থক্য। lw-এর জন্য main control ঠিক signal-ই বানায়, কিন্তু MemtoReg-এর তার 0-তে আটকে থাকায় mux পায় 0. বাকি ছয়টি signal স্বাভাবিক।
- পথের প্রথম অংশ ঠিক। ALUSrc = 1 বলে ALU হিসাব করে x1 + ৪ = ১২, আর MemRead = 1 বলে memory ঠিকঠাক ১৩ পড়ে। ভুলটি এখনো ঘটেনি।
- ভুলের মুহূর্ত। Write-back mux-এর দুই ইনপুট: ALU-র ফল ১২ আর memory-র ১৩. MemtoReg = 0 বলে mux বাছে ১২ — ঠিকানাটিই, ডেটা নয়। RegWrite = 1, তাই x5 = ১২ লেখা হয়ে যায়।
- ভুল ছড়ানো। পরের
beq x3, x5, 8তুলনা করে ১৩ আর ১২ — সমান নয়, তাই branch নেওয়া হয় না, আর যেaddi x6, x0, 1লাফিয়ে পার হওয়ার কথা, সেটি চলে x6 = ১ লেখে। একটি তারের ত্রুটি দুটি register নষ্ট করল। - সাধারণ নিয়ম। টেবিলে MemtoReg = 1 শুধু lw-এর সারিতে, তাই শুধু lw ভাঙে। আর টেস্টের শিক্ষা: lw যদি ঠিকানা ১২ থেকে ঠিক ১২ মানটিই পড়ত, ত্রুটিটি ধরা পড়ত না। ভালো টেস্টে ঠিকানা আর ডেটা আলাদা রাখতে হয়।
নিচের প্রোগ্রাম এই বিশ্লেষণ যন্ত্রে করে। এটি একটি সরল single-cycle মডেল: control আসে অংশ ৫-এর সমীকরণ থেকে (তাই X-এর জায়গায় chip-এ যে মান থাকত, সেটিই), immediate generator opcode দেখে format বাছে, আর memory পড়া বন্ধ থাকলে output ধরা হয় ০ (আসল hardware-এ সেটি অনির্ধারিত)। প্রথমে ত্রুটিহীন অবস্থায় দশ instruction-এর প্রোগ্রামটি চলে; তারপর প্রতিটি ত্রুটির জন্য দুটি জিনিস ছাপা হয় — (১) ঠিক একই আগের অবস্থা থেকে কোন কোন instruction-এর ফল (register, memory বা পরের PC) আলাদা হয়, (২) ত্রুটি নিয়ে পুরো প্রোগ্রাম চালালে শেষ অবস্থা কী:
// Single-cycle RV32I subset with one control line stuck at 0 or 1: which instructions go wrong?
const PROG = [0x00800093, 0x00500113, 0x002081B3, 0x40208233, 0x0030A223,
0x0040A283, 0x00518463, 0x00100313, 0x0020E3B3, 0x0020F433];
const NAME = ["addi", "addi", "add", "sub", "sw", "lw", "beq", "addi", "or", "and"];
const bit = (w, n) => (w >>> n) & 1, nt = x => 1 - x;
function control(op) { // hardwired equations from the K-maps (op6, op5, op4)
const o6 = bit(op, 6), o5 = bit(op, 5), o4 = bit(op, 4);
return { RegWrite: o4 | nt(o5), ALUSrc: nt(o5) | (nt(o6) & nt(o4)), MemtoReg: nt(o4),
MemRead: nt(o5) & nt(o4), MemWrite: nt(o6) & o5 & nt(o4), Branch: o6, ALUOp: (o5 & o4) * 2 + o6 };
}
const immGen = w => (w & 0x7F) === 0x23 ? ((w >> 25) << 5) | ((w >>> 7) & 31) // S
: (w & 0x7F) === 0x63 ? ((w >> 31) << 12) | (bit(w, 7) << 11) | (((w >>> 25) & 63) << 5)
| (((w >>> 8) & 15) << 1) : w >> 20; // B : I
const aluCtl = (op, w) => ((op & 1) | ((op >> 1) & bit(w, 30))) << 2 | (nt(op >> 1) | nt(bit(w, 14))) << 1
| ((op >> 1) & bit(w, 14) & nt(bit(w, 12)));
const ALU = { 2: (a, b) => a + b, 6: (a, b) => a - b, 0: (a, b) => a & b, 1: (a, b) => a | b };
function step([pc, reg, mem], fault) { // one clock cycle of the single-cycle datapath
const w = PROG[pc / 4], c = { ...control(w & 0x7F), ...fault }, r = [...reg], m = { ...mem };
const rd = (w >>> 7) & 31, a = reg[(w >>> 15) & 31], b2 = reg[(w >>> 20) & 31];
const res = ALU[aluCtl(c.ALUOp, w)](a, c.ALUSrc ? immGen(w) : b2) | 0;
const data = c.MemRead ? (mem[res] ?? 0) : 0; // model: memory outputs 0 when MemRead = 0
if (c.MemWrite) m[res] = b2;
if (c.RegWrite && rd) r[rd] = c.MemtoReg ? data : res;
return [c.Branch && res === 0 ? pc + immGen(w) : pc + 4, r, m];
}
function run(f) { let s = [0, Array(32).fill(0), {}]; const t = [s];
while (s[0] % 4 === 0 && s[0] / 4 < PROG.length && t.length < 40) t.push(s = step(s, f)); return t; }
const same = (x, y) => JSON.stringify(x) === JSON.stringify(y), gold = run({});
const show = s => "x1..x8=" + s[1].slice(1, 9).join(",") + " mem=" + JSON.stringify(s[2]) + " pc=" + s[0];
console.log("no fault : " + show(gold.at(-1)));
for (const f of [{ RegWrite: 0 }, { RegWrite: 1 }, { ALUSrc: 1 }, { ALUSrc: 0 }, { MemtoReg: 0 },
{ MemtoReg: 1 }, { Branch: 0 }, { Branch: 1 }, { MemWrite: 1 }]) {
const bad = gold.slice(0, -1).filter(s => !same(step(s, f), step(s, {}))).map(s => NAME[s[0] / 4] + "@" + s[0]);
console.log(`${Object.entries(f)[0].join(" stuck ").padEnd(17)}: wrong = ${bad.join(" ")}\n${"".padEnd(19)}${show(run(f).at(-1))}`);
}আউটপুট (নির্ধারিত — প্রতিবার একই; sub@12 মানে PC = ১২-এর sub):
no fault : x1..x8=8,5,13,3,13,0,13,0 mem={"12":13} pc=40
RegWrite stuck 0 : wrong = addi@0 addi@4 add@8 sub@12 lw@20 or@32
x1..x8=0,0,0,0,0,0,0,0 mem={"4":0} pc=40
RegWrite stuck 1 : wrong = sw@16
x1..x8=8,5,13,0,13,0,13,0 mem={"12":13} pc=40
ALUSrc stuck 1 : wrong = add@8 sub@12 beq@24 or@32
x1..x8=8,5,10,-1018,10,1,10,0 mem={"12":10} pc=40
ALUSrc stuck 0 : wrong = addi@0 addi@4 sw@16 lw@20
x1..x8=0,0,0,0,0,0,0,0 mem={"0":0} pc=40
MemtoReg stuck 0 : wrong = lw@20
x1..x8=8,5,13,3,12,1,13,0 mem={"12":13} pc=40
MemtoReg stuck 1 : wrong = addi@0 addi@4 add@8 sub@12 or@32
x1..x8=0,0,0,0,0,0,0,0 mem={"4":0} pc=40
Branch stuck 0 : wrong = beq@24
x1..x8=8,5,13,3,13,1,13,0 mem={"12":13} pc=40
Branch stuck 1 : wrong = and@36
x1..x8=8,5,13,3,13,0,13,0 mem={"12":13} pc=38
MemWrite stuck 1 : wrong = addi@0 addi@4 add@8 sub@12 lw@20 beq@24 or@32 and@36
x1..x8=8,5,13,3,13,0,13,0 mem={"0":5,"3":5,"5":0,"8":0,"12":3,"13":5} pc=40
উপরের টেবিলের সাথে মেলান। RegWrite stuck 0-এ ভাঙে দুটি addi, add, sub, lw আর or — যারা লেখে; সব register ০ থেকে যায়। ALUSrc stuck 1-এ add, sub, beq, or ভাঙে: add পায় x1 + ২ = ১০ (R-type-এর inst[31:20] = 0x002, rs2-field-এর বিট), sub পায় ৮ − ১০২৬ = −১০১৮. MemtoReg stuck 0-এ শুধু lw, আর শেষ অবস্থা স্টেপারের মতোই: x5 = ১২, x6 = ১. Branch stuck 0-এ beq নেওয়া হয় না, তাই x6 = ১.
সবচেয়ে শিক্ষণীয় হলো যেখানে ত্রুটি লুকিয়ে যায়। ALUSrc stuck 1-এ and-ও ভুল operand নেয় (x1 AND ২), কিন্তু ৮ AND ২ = ০, আসল ৮ AND ৫-ও ০ — তাই তালিকায় নেই। RegWrite stuck 1-এ beq-ও x8-এ লেখে, কিন্তু লেখে ০, আর x8 তখন ০ — তাই শুধু sw ধরা পড়ে (x4 = ০)। Branch stuck 1-এ শুধু শেষের and (ফল ০) লাফ দেয় PC = ৩৮-এ, আর শেষ register-গুলো একদম ঠিক — শুধু pc=38 ত্রুটির চিহ্ন। MemWrite stuck 1-এ register সব ঠিক, কিন্তু memory-তে ছয়টি ঠিকানায় আবর্জনা, এমনকি ঠিকানা ১২-এর ১৩ মুছে ৩ হয়েছে। অর্থাৎ শুধু শেষের register দেখে টেস্ট পাস ধরা বিপজ্জনক — memory আর PC-ও মেলাতে হয়, আর টেস্ট প্রোগ্রামে এমন মান রাখতে হয় যাতে ভুল ফল ঠিক ফলের সাথে কাকতালীয়ভাবে না মেলে।
৯ · Hardwired বনাম microprogrammed control — আর বাস্তব প্রসেসর
আমাদের control একটি gate-এর জাল। অন্য পথ হলো microprogrammed control: প্রতিটি instruction-এর জন্য control signal-এর একটি ক্রম একটি ছোট ROM-এ (microcode) লেখা থাকে, আর একটি ছোট sequencer প্রতি cycle-এ ROM থেকে পরের সারি পড়ে। ধারণাটি Maurice Wilkes ১৯৫১ সালে প্রস্তাব করেন। Multi-cycle datapath-এ এর পুরো রূপ দেখবেন পর্ব ৩৪-এ; এখানে শুধু তুলনা:
| দিক | Hardwired | Microprogrammed |
|---|---|---|
| গতি | দ্রুত — শুধু কয়েক স্তরের gate | ধীর — প্রতি ধাপে ROM পড়া আর পরের ঠিকানা বাছা |
| বদলানো | নতুন নকশা, নতুন chip | ROM-এর বিষয়বস্তু বদলালেই; কিছু CPU-তে পরে patch-ও করা যায় |
| জটিল ISA | শত শত জটিল instruction হলে নকশা আর যাচাই কঠিন | নিয়মিত পদ্ধতি — প্রতিটি instruction একটি ছোট প্রোগ্রাম |
| কোথায় মানায় | RISC core: RISC-V, ARM; x86-এরও সাধারণ instruction | পুরোনো CISC — IBM System/360-এর বেশিরভাগ মডেল, VAX; আজকের x86-এর বিরল ও জটিল instruction |
১৯৬০-৭০-এর দশকে memory ছিল ধীর, আর CPU-র ভেতরের ROM তুলনায় দ্রুত; তাই microcode দিয়ে শক্তিশালী, জটিল instruction বানানো লাভজনক ছিল। ১৯৭০-এর দশকের শেষ থেকে ১৯৮০-এর দশকে RISC আন্দোলন (IBM 801, Berkeley RISC, Stanford MIPS) যুক্তি দেয়: compiler জটিল instruction খুব কমই ব্যবহার করে, সরল instruction-কে hardwired control-এ এক cycle-এ চালানোই ভালো, আর তাতে pipelining (পর্ব ৩৫) সহজ হয়।
আজকের x86 CPU দুটিকেই মেশায় — বিস্তারিত সংস্থা প্রতিটি প্রজন্মে বদলায়, তাই এখানে মোটা দাগে বলা হলো। Decoder সাধারণ instruction-কে সরাসরি এক বা কয়েকটি micro-op-এ ভাঙে (hardwired ধাঁচে); কয়েকটি micro-op-এর বেশি লাগে এমন জটিল বা বিরল instruction (যেমন string-এর কিছু কাজ, কিছু system instruction) একটি microcode ROM থেকে micro-op-এর ক্রম আনে। Intel আর AMD BIOS/UEFI বা operating system-এর মাধ্যমে microcode update দেয়, যা দিয়ে কিছু bug আর নিরাপত্তা-সমস্যার প্রশমন পাঠানো হয়েছে। Micro-op কীভাবে superscalar আর out-of-order core-এ চলে, তা পর্ব ৩৯ আর পর্ব ৪০-এ।
আরও গভীরে: Pipelined CPU-তে (পর্ব ৩৫) এই পর্বের control unit প্রায় হুবহু থাকে — decode ধাপে একবার signal তৈরি হয়, তারপর signal-গুলো pipeline register-এ instruction-এর সাথে সাথে এগোয়, আর প্রতিটি stage নিজের অংশ ব্যবহার করে। বড় out-of-order core-এ decoder-এর কাজ এত বড় যে একাধিক decoder সমান্তরালে চলে, আর আগে decode করা micro-op একটি cache-এ রাখা হয়। উল্টো দিকে ছোট RISC-V microcontroller core-গুলোর decoder মাত্র কয়েকশো gate-এর হতে পারে — সংখ্যা core ভেদে অনেক বদলায়।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- sw-এ MemtoReg কেন X? — RegWrite = 0, তাই mux-এর আউটপুট কোথাও লেখা হয় না।
- ALUOp কেন লাগে, সরাসরি opcode থেকে ALU control কেন নয়? — দুই স্তরে ভাগ করলে main control ছোট হয়, funct দেখার ঝামেলা ALU-র পাশের ছোট circuit-এ যায়, আর lw-এর funct3-কে উপেক্ষা করা যায়।
- ALUSrc stuck-at-1 হলে কোন instruction তবুও চলে? — lw, sw, addi (যাদের ALUSrc এমনিতেই 1).
- bne যোগ করতে main control-এ নতুন সারি লাগবে? — না, opcode beq-এর মতো; PCSrc = Branch · (Zero ⊕ funct3[0]).
- Hardwired আর microprogrammed-এর মূল পার্থক্য? — gate-এর জাল বনাম ROM-এ রাখা signal-এর ক্রম; প্রথমটি দ্রুত, দ্বিতীয়টি নমনীয়।
১০ · AI-এর যুগে — control টেবিলের দাবি যাচাই
Control টেবিল দেখতে সরল, তাই AI-এর উত্তর প্রায়ই আত্মবিশ্বাসী আর সুন্দর — কিন্তু একটি ঘর ভুল হলেই CPU ভুল চলে। বিশেষ করে MIPS আর RISC-V-এর বই দুটি এত কাছাকাছি যে AI প্রায়ই দুটি মিশিয়ে ফেলে। সুখবর: এখানে সবকিছু যন্ত্রে যাচাই করা যায়।
| AI প্রায়ই যা করে | সত্যিটা | যাচাইয়ের উপায় |
|---|---|---|
| RISC-V টেবিলে RegDst কলাম রাখে, বা MIPS-এর opcode (lw = 100011) বসায় | RISC-V-এ RegDst নেই; lw-এর opcode 0000011 | RISC-V manual-এর opcode map আর পর্ব ২৭ |
| sw-এ RegWrite = X লেখে | write enable কখনো X নয়; sw-এ 0 | “X হলে 1 ধরলে কী লেখা হতো?” — প্রশ্নটি নিজে করুন |
| addi-কে ALUOp = 10 দেয় | inst[30] immediate-এর বিট; ঋণাত্মক imm-এ sub হয়ে যাবে | সিমুলেটরে addi x5,x6,-1024 চালান |
| bne-এর জন্য main control-এ নতুন সারি বানায় | opcode একই; পরিবর্তন PCSrc-এর logic-এ | দুটি instruction encode করে opcode মেলান |
| K-map-এ X-কে 0 ধরে বড় সমীকরণ দেয়, বা দলে 0-এর ঘর ঢুকিয়ে ফেলে | X দল বড় করতে কাজে লাগে; 0-এর ঘর কখনো নয় | প্রতিটি instruction-এ সমীকরণ চালিয়ে টেবিলের সাথে মেলান |
| নির্দিষ্ট কোনো chip-এর microcode বা decoder-এর বিস্তারিত বানিয়ে বলে | এসব প্রায়ই প্রকাশিত নয়, বা প্রজন্মভেদে বদলায় | নির্মাতার optimization manual বা প্রকাশিত গবেষণা |
AI-কে control নিয়ে প্রশ্ন করার সময় ISA, বই আর সংস্করণ স্পষ্ট বলুন, আর যাচাইয়ের উপায় চেয়ে নিন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Control-কে দুই স্তরে ভাগ না করে একটি বড় টেবিলে রাখলে আসলে কী ক্ষতি হতো? আর তিন স্তর কেন নয়?
প্রথমে আকারের হিসাব। একটি স্তরে control unit-কে opcode-এর ৭ বিট, funct3-এর ৩ বিট আর funct7-এর ৭ বিট — মোট ১৭টি ইনপুট দেখতে হবে। ROM হিসেবে বানালে ২১৭ = ১৩১০৭২টি সারি, প্রতিটিতে ১০-১২ বিট — single-cycle CPU-র বাকি সব অংশের চেয়ে বড়। Gate দিয়ে বানালে অবশ্য minimisation অনেক কমাবে, কারণ বেশিরভাগ সমন্বয় don't care; কিন্তু তখনো প্রতিটি output-এর সমীকরণে opcode আর funct-এর literal মিশে যাবে, আর নকশাকারকে একসাথে সব ভাবতে হবে।
দুই স্তরে ভাগ করলে দায়িত্ব আলাদা হয়। Main control-এর ৭টি ইনপুট, ALU control-এর ৬টি। প্রতিটি ছোট, আলাদাভাবে যাচাই করা যায়, আর ALU control-কে ALU-র পাশে বসানো যায়। এটি software-এর ভাগ করে জয়ের নীতির মতো — একটি বড় function-এর বদলে দুটি ছোট, পরিষ্কার interface-যুক্ত function. ALUOp হলো সেই interface: মাত্র দুই বিটে main control বলে দেয় “ঠিকানা বানানো”, “তুলনা” বা “funct দেখা”।
গতির প্রশ্নটি সূক্ষ্ম। দুই স্তর মানে পথে gate একটু বেশি — main control-এর পরে ALU control. কিন্তু single-cycle-এ ALU-কে এমনিতেই register file থেকে operand আসার অপেক্ষা করতে হয়, আর register পড়ার সময়েই দুই স্তরের control শেষ হয়ে যায়। তাই বাস্তবে এই বাড়তি delay সাধারণত critical path-এ পড়ে না, আর ছোট circuit প্রায়ই প্রতিটি স্তরে দ্রুত।
তিন স্তর কেন নয়? প্রয়োজন নেই বলে। আরও ভাগ করলে প্রতিটি সীমানায় নতুন interface signal আর delay আসে, অথচ লাভ সামান্য। তবে বড় CPU-তে আসলে অনেক স্তরই থাকে: memory unit-এর নিজস্ব control, branch unit-এর নিজস্ব শর্ত-logic, floating-point unit-এর আলাদা decoder. RISC-V-এর বিভিন্ন extension আলাদা আলাদা opcode পায় বলে প্রতিটি extension-এর decoder প্রায় স্বাধীনভাবে বানানো যায় — এটিও একই নীতির বড় রূপ।
মূল উপলব্ধি: দুই স্তরের decoding hardware-এ modularity — ছোট টুকরো, পরিষ্কার interface (ALUOp), আলাদা যাচাই; গতির ক্ষতি নগণ্য, কারণ control register পড়ার সাথে সমান্তরালে চলে।
প্র ০২Microprogramming একসময় প্রায় সব বড় কম্পিউটারে ছিল, তারপর RISC এসে hardwired control ফিরিয়ে আনল। এই উত্থান-পতনের পেছনে আসল কারণ কী?
১৯৫১ সালে কেমব্রিজের Maurice Wilkes লক্ষ করেন যে কম্পিউটারের control অংশই সবচেয়ে এলোমেলো আর ভুলপ্রবণ — প্রতিটি নকশা নতুন করে হাতে গাঁথা তারের জাল। তাঁর প্রস্তাব: control signal-এর ক্রম একটি নিয়মিত স্মৃতিতে প্রোগ্রামের মতো লেখা হোক। ধারণাটি জনপ্রিয় হয় ১৯৬৪ সালে IBM System/360-এর হাত ধরে। IBM চেয়েছিল ছোট-বড় নানা দামের মডেল একই ISA চালাক। বেশিরভাগ মডেলে আলাদা datapath, কিন্তু microcode দিয়ে একই instruction set — সস্তা মডেলে ধীরে, দামি মডেলে দ্রুত। ISA আর hardware-কে আলাদা করার এই কৌশল ছাড়া “একই সফটওয়্যার সব মডেলে” সম্ভব হতো না।
১৯৭০-এর দশকে আরও একটি কারণ যোগ হয়: main memory ধীর আর দামি, কিন্তু CPU-র ভেতরের ROM তুলনায় দ্রুত। তাই একটি instruction-কে যত শক্তিশালী করা যায় — একটি instruction-এ লুপ, string-কপি, polynomial-এর হিসাব — memory থেকে তত কম instruction আনতে হয়। DEC VAX এর চূড়ান্ত উদাহরণ, শত শত instruction আর বহু addressing mode, সবই microcode-এ।
তারপর হিসাব বদলায়। Cache এসে instruction আনার খরচ কমায়; compiler-এর গবেষণা দেখায় যে জটিল instruction খুব কমই ব্যবহার হয়; আর IBM-এর John Cocke-এর দল (801), Berkeley-র David Patterson আর Stanford-এর John Hennessy যুক্তি দেন যে সরল instruction-কে hardwired control-এ প্রতি cycle-এ একটি করে চালানো, আর pipeline করা, সামগ্রিকভাবে দ্রুত। Microcode-এর প্রতিটি ধাপে ROM পড়ার সময় এই নতুন জগতে বাড়তি বোঝা হয়ে দাঁড়ায়।
কিন্তু microcode হারিয়ে যায়নি। x86 backward compatibility-র কারণে পুরোনো জটিল instruction রাখতে বাধ্য; আধুনিক x86 সাধারণ instruction সরাসরি decode করে, আর বিরল জটিলগুলোকে microcode ROM-এ পাঠায়। তার ওপর microcode update হয়ে উঠেছে একটি জরুরি নিরাপত্তা-হাতিয়ার: chip বাজারে যাওয়ার পরেও কিছু bug-এর প্রশমন পাঠানো যায়।
মূল উপলব্ধি: কোন control ভালো, তা নির্ভর করে প্রযুক্তির দামের অনুপাতের ওপর — memory, ROM, transistor আর compiler-এর ক্ষমতা বদলালে সেরা উত্তরও বদলায়; আজ দুটি পাশাপাশি থাকে।
প্র ০৩Don't care দিয়ে সমীকরণ ছোট হয়, কিন্তু chip-এ X বলে কিছু নেই — প্রতিটি X শেষে 0 বা 1. এই “লুকোনো আচরণ” কি কখনো বিপদ ডেকেছে?
হ্যাঁ, আর ইতিহাসে এর মজার উদাহরণও আছে। ১৯৭৫ সালের MOS 6502 — Apple II-এর CPU, আর এর রূপভেদ ছিল Commodore 64 ও NES-এর মতো যন্ত্রে — তার instruction decode করত একটি PLA-ধাঁচের decode ROM দিয়ে। নকশাকাররা সংজ্ঞায়িত opcode-গুলোর জন্য সংযোগ বানিয়েছিলেন, বাকি opcode-গুলোর কী হবে, তা নিয়ে আলাদা মাথা ঘামাননি — সেগুলো ছিল কার্যত don't care. কিন্তু chip-এ সেই opcode এলে PLA-র কিছু লাইন একসাথে চালু হতো, আর অদ্ভুত কিন্তু নির্ধারিত কাজ ঘটত — যেমন একসাথে দুটি register-এ load. প্রোগ্রামাররা এগুলো খুঁজে বের করে “illegal opcode” নামে ব্যবহার শুরু করেন, কিছু প্রোগ্রামে আর কয়েকটি গেমেও। পরে CMOS-এ বানানো 65C02 সংস্করণে এসব opcode-কে NOP-এর মতো নিরীহ করা হয়, আর পুরোনো সেই ফাঁকফোকরে ভর করা কিছু প্রোগ্রাম নতুন chip-এ ঠিক চলেনি।
এই পর্বের সংখ্যাতেও একই ঘটনা দেখেছেন। চার instruction-এ MemtoReg = op5′, তাই sw-এ X → 0; addi যোগ করার পর MemtoReg = op4′, তখন X → 1. আর আমাদের Branch = op6 সমীকরণে একটি ecall (SYSTEM, op6..op4 = 111) এলে Branch = 1 হয়ে যায় — একটি system call হঠাৎ branch হয়ে লাফ দিতে পারে, যদি ALU-র ফল কাকতালীয়ভাবে ০ হয়।
এজন্য আধুনিক ISA অচেনা encoding-এর আচরণ স্পষ্ট করে দেয়। RISC-V-এ সংরক্ষিত বা অসমর্থিত encoding-এ সাধারণত illegal-instruction exception হয়, যাতে operating system সেটি ধরতে পারে বা software দিয়ে অনুকরণ করতে পারে। নিরাপত্তার দিক থেকেও এটি জরুরি: undocumented আচরণ মানে এমন কিছু যা কেউ যাচাই করেনি। Fuzzing দিয়ে x86 প্রসেসরে লুকোনো, অপ্রকাশিত instruction খোঁজার গবেষণাও প্রকাশিত হয়েছে।
তাই শিল্পের রীতি: X ব্যবহার করুন শুধু সেই signal-এ যা কোনো state বদলায় না (mux-এর select), আর সেখানেও কেবল সংজ্ঞায়িত instruction-এর মধ্যে; অচেনা opcode-এ সব write enable জোর করে 0 আর একটি exception.
মূল উপলব্ধি: X নকশার স্বাধীনতা, কিন্তু silicon-এ তা একটি নির্দিষ্ট আচরণ হয়ে যায়; সংজ্ঞার বাইরের ইনপুটে কী হবে, তা ঠিক করে না দিলে কেউ একদিন সেটি খুঁজে বের করবে।
প্র ০৪Stuck-at fault একটি খুব সরল মডেল — একটি তার সবসময় 0 বা 1. বাস্তবের chip-টেস্টে এত সরল মডেল কেন এত গুরুত্বপূর্ণ?
Chip তৈরির কারখানায় প্রতিটি wafer-এ কিছু chip ত্রুটিযুক্ত হয় — ধুলোর কণা, ধাতুর পাতলা স্তরে ফাটল, দুটি তারের অনিচ্ছাকৃত সংযোগ। প্রতিটি chip বাজারে যাওয়ার আগে পরীক্ষা করতে হয়, আর পরীক্ষার সময় টাকা। প্রশ্ন হলো: কোন ইনপুট দিলে ত্রুটি ধরা পড়বে? সম্ভাব্য সব ভৌত ত্রুটি মডেল করা অসম্ভব, তাই নকশাকাররা একটি সরল বিমূর্ত রূপ নেন: প্রতিটি তার stuck-at-0 বা stuck-at-1 হতে পারে। মজার ব্যাপার, এই সরল মডেলের সব ত্রুটি ধরতে পারে এমন টেস্ট অভিজ্ঞতায় অনেক বাস্তব ত্রুটিও ধরে ফেলে।
এই মডেলের ওপর গড়ে উঠেছে automatic test pattern generation (ATPG): software প্রতিটি সম্ভাব্য stuck-at ত্রুটির জন্য এমন ইনপুট খোঁজে, যাতে ত্রুটিযুক্ত আর ভালো circuit-এর output আলাদা হয়। মোট ত্রুটির কত শতাংশ টেস্টে ধরা পড়ে, তাকে বলে fault coverage; শিল্পে stuck-at coverage সাধারণত খুব উঁচু রাখার লক্ষ্য থাকে। ভেতরের flip-flop-গুলোকে বাইরে থেকে সেট ও পড়ার জন্য chip-এ scan chain বসানো হয় — flip-flop-গুলোকে একটি লম্বা shift register বানিয়ে।
এই পর্বের সিমুলেটরে সেই নীতির ছোট রূপ দেখেছেন। একটি ত্রুটি ধরতে হলে দুটি শর্ত লাগে: ত্রুটিটিকে “সক্রিয়” করতে হবে (যে instruction-এ signal-টির ঠিক মান উল্টো, সেটি চালাতে হবে), আর ফলটিকে “দৃশ্যমান” করতে হবে (ভুল মান এমন কোথাও পৌঁছাতে হবে যা আমরা দেখি)। ALUSrc stuck-at-1-এ and সক্রিয় হয়েছিল, কিন্তু ৮ AND ২ = ৮ AND ৫ = ০, তাই দৃশ্যমান হয়নি — fault masking. RegWrite stuck-at-1-এ beq ভুল করে x8-এ ০ লিখল, যা আগেও ০ ছিল।
আরেকটি দিক নির্ভরযোগ্যতা। গাড়ি, বিমান বা মহাকাশযানে chip চলার সময়ও ত্রুটি হতে পারে — বয়সজনিত ক্ষয়, বা মহাজাগতিক কণার আঘাতে ক্ষণিকের bit-flip. তাই নিরাপত্তা-সংবেদনশীল microcontroller-এ প্রায়ই দুটি core একই কাজ একই তালে চালায় আর প্রতি cycle-এ ফল মেলায় (lockstep), মিল না হলে সতর্কসংকেত দেয়।
মূল উপলব্ধি: সরল মডেল শক্তিশালী কারণ তা যন্ত্রে হিসাবযোগ্য; আর একটি ত্রুটি ধরতে তাকে সক্রিয় করা ও দৃশ্যমান করা — দুটিই লাগে।
প্র ০৫একটি নতুন CPU-র control unit-এ কোনো ভুল নেই — নকশাকাররা কীভাবে এ ব্যাপারে নিশ্চিত হন?
পুরোপুরি নিশ্চিত হওয়া কঠিন, আর ইতিহাস তা মনে করিয়ে দেয়। ১৯৯৪ সালে Intel Pentium-এর floating-point ভাগের একটি lookup টেবিলের কয়েকটি ঘর ফাঁকা থাকায় বিরল কিছু ভাগে ভুল উত্তর আসত (FDIV bug). এটি কারখানার ত্রুটি নয় — নকশার ত্রুটি, তাই প্রতিটি chip-এ একই। Intel-কে বিপুল সংখ্যক processor বদলে দিতে হয়েছিল, আর এর পর শিল্পে formal verification-এ বিনিয়োগ উল্লেখযোগ্যভাবে বাড়ে। Control unit-ও মূলত একটি বড় টেবিল; একটি ঘর ভুল হলে একই রকম বিপদ।
যাচাইয়ের কয়েকটি স্তর আছে। প্রথম, golden model-এর সাথে তুলনা: ISA-র একটি সহজ simulator (যেমন RISC-V-এর Spike) আর RTL নকশা একই প্রোগ্রাম চালায়, প্রতি instruction-এর পর register মেলানো হয়। এই পর্বের stuck-at সিমুলেটর ঠিক এই কৌশল — ত্রুটিহীন রানকে মানদণ্ড ধরা। দ্বিতীয়, নির্দেশিত আর random টেস্ট: RISC-V-এর জন্য আন্তর্জাতিকভাবে শেয়ার করা architectural test suite আছে, আর random instruction generator লক্ষ লক্ষ অদ্ভুত সমন্বয় বানায়, যেমন একটি instruction-এর পরেই তার ফল ব্যবহার করা।
তৃতীয়, formal verification: টেস্টের বদলে গাণিতিক প্রমাণ — “যেকোনো বৈধ instruction-এ RTL-এর আচরণ ISA-র সংজ্ঞার সমান”। Control unit-এর মতো combinational circuit-এ এটি বেশ কার্যকর, কারণ SAT solver সব সম্ভাব্য ইনপুট একসাথে বিবেচনা করতে পারে। RISC-V-এর জন্য খোলা উৎসের formal যাচাইয়ের কাঠামোও আছে। চতুর্থ, coverage: টেস্ট টেবিলের প্রতিটি সারি, প্রতিটি signal-এর 0 আর 1 দুটি মানই অন্তত একবার ঘটিয়েছে কি না, তা মাপা।
এই পর্বের ছোট উদাহরণেও একই শিক্ষা। Minimiser-এর ok কলাম একটি ক্ষুদ্র formal যাচাই: প্রতিটি সমীকরণ প্রতিটি instruction-এ টেবিলের সাথে মেলানো। আর addi যোগ করার পর সমীকরণ বদলানো দেখায় যে প্রতিটি পরিবর্তনের পর পুরো যাচাই আবার চালাতে হয় — একে বলে regression testing.
মূল উপলব্ধি: ভুলহীনতা আসে বহুস্তর যাচাই থেকে — golden model, বিপুল টেস্ট, coverage আর গাণিতিক প্রমাণ; আর প্রতিটি পরিবর্তনের পর সব আবার।
অনুশীলন
খাতা-কলম নিন; প্রথমে নিজে করুন, তারপর উপরের সিমুলেটর দিয়ে মিলিয়ে নিন।
-
Control টেবিল পূরণ করুন: নিচের তিনটি word-এর জন্য opcode, main control-এর সাতটি মান (ALUSrc, MemtoReg, RegWrite, MemRead, MemWrite, Branch, ALUOp), ALU operation-এর ৪ বিট আর datapath-এর কাজ লিখুন: (ক)
0x0020E3B3(খ)0x00712623(গ)0xFE028CE3.(ক)
0000 0000 0010 0000 1110 0011 1011 0011: opcode0110011(R-type), funct3 =110, inst[30] = 0, rs1 = x1, rs2 = x2, rd = x7 →or x7, x1, x2. Control: ALUSrc 0, MemtoReg 0, RegWrite 1, MemRead 0, MemWrite 0, Branch 0, ALUOp 10. ALU control: Operation2 = 0 + 1·0 = 0, Operation1 = 0 + 0 = 0, Operation0 = 1·1·1 = 1 →0001(OR). x7 ← x1 OR x2.(খ) opcode
0100011(STORE), funct3 =010, rs1 = x2, rs2 = x7, imm = ১২ →sw x7, 12(x2). Control: 1, X, 0, 0, 1, 0, 00. ALU control0010(add): ঠিকানা = x2 + ১২, Mem[ঠিকানা] ← x7. MemtoReg X, কারণ RegWrite = 0.(গ) opcode
1100011(BRANCH), funct3 =000→ beq; rs1 = x5, rs2 = x0; immediate: inst[31] = 1, inst[7] = 1, inst[30:25] =111111, inst[11:8] =1100→1 1111 1111 1000= −৮. অর্থাৎbeq x5, x0, -8. Control: 0, X, 0, 0, 0, 1, 01. ALU control0110(sub): x5 − ০; x5 = ০ হলে Zero = 1, PCSrc = 1, PC ← PC − ৮. -
সমীকরণ বের করুন: (ক) addi ছাড়া আর addi সহ — দুই ক্ষেত্রে MemRead আর ALUOp1-এর K-map এঁকে সবচেয়ে ছোট SOP দিন। (খ) addi-সহ সেটে
lui(0110111) যোগ করলে কোন সমীকরণ ভাঙে, আর নতুন সমীকরণ কী? (lui-এর জন্য ALUOp = 00 ধরুন।)(ক) ঘর (op6, op5op4): lw = (0,00), addi = (0,01), R = (0,11), sw = (0,10), beq = (1,10); (1,00), (1,01), (1,11) X. addi ছাড়া (0,01)-ও X. MemRead = 1 শুধু lw-এ; কলাম 00 আর 01 নিয়ে চারটি ঘর (1, X, X, X) → MemRead = op5′. ALUOp1 = 1 শুধু R-এ; কলাম 01 আর 11 (X, 1, X, X) → ALUOp1 = op4. addi সহ (0,01) = 0 হয়ে যায়: MemRead-এর দল ছোট হয়ে কলাম 00, দুই সারি → op5′·op4′; ALUOp1-এর দল কলাম 11, দুই সারি → op5·op4.
(খ) lui-এর op6..op4 =
011, R-type-এর সমান; পার্থক্য op2-এ (lui 1, R 0). ALUOp1 = op5·op4 lui-তে 1 দেবে, অথচ চাই 0 — ভাঙে। নতুন: ALUOp1 = op5·op4·op2′. RegWrite = op4 + op5′ lui-তে 1 দেয় — ঠিকই আছে। ALUSrc = op5′ + op6′·op4′ lui-তে 0 দেয়, অথচ চাই 1 — ভাঙে; একটি পদ যোগ করে ALUSrc = op5′ + op6′·op4′ + op2. MemtoReg = op4′ lui-তে 0 — ঠিক। (minimiser দিয়ে যাচাই করা: ALUOp1 = op5·op4·op2′, ALUSrc = op6′·op4′ + op5′ + op2.) -
Stuck-at বিশ্লেষণ আর ফল অনুমান: সব register প্রথমে ০. প্রোগ্রাম:
addi x1,x0,8,addi x2,x0,5,add x3,x1,x2. (ক) ALUSrc stuck-at-0 হলে কোন কোন instruction ভাঙবে (R-type, lw, sw, beq, addi-এর মধ্যে)? এই তিন লাইনের পর x1, x2, x3 কত? (খ) Branch stuck-at-1 হলেand x8, x1, x2(x1 = ৮, x2 = ৫, word0x0020F433, PC = ৩৬) চালানোর পর PC কত হবে?(ক) যাদের ALUSrc = 1 দরকার — lw, sw, addi — তারা ভাঙবে; R-type আর beq ঠিক। addi-তে ALU-র দ্বিতীয় ইনপুট immediate-এর বদলে rs2-field নম্বরের register. I-type-এ rs2-field = imm[4:0]:
addi x1,x0,8-এ সেটি ৮, তাই x1 ← x0 + x8 = ০ + ০ = ০.addi x2,x0,5: x2 ← x0 + x5 = ০.add x3,x1,x2নিজে ঠিক চলে, কিন্তু ০ + ০ = ০. ফল x1 = x2 = x3 = ০ — সিমুলেটরের “ALUSrc stuck 0” সারির সাথে মেলে।(খ) ৮ AND ৫ =
1000AND0101= ০, তাই Zero = 1. Branch আটকে 1, তাই PCSrc = 1, PC ← PC + imm. R-type-এ immediate generator যা দেয় (আমাদের মডেলে I-format ধরে inst[31:20]) তা0x002= ২, তাই PC = ৩৬ + ২ = ৩৮ — ৪-এর গুণিতকও নয়। সিমুলেটরের শেষ অবস্থাpc=38. বাস্তব datapath-এ R-type-এর “immediate” কোন বিট থেকে আসবে তা immediate generator-এর নকশার ওপর নির্ভর করে, তাই সংখ্যাটি বদলাতে পারে; কিন্তু ভুল লাফ হবেই। -
bne আর jal যোগ করুন (circuit লিখুন): (ক) bne-এর জন্য main control-এর সারি আর PCSrc-এর gate-সমীকরণ লিখুন। (খ) jal-এর জন্য কোন নতুন signal আর mux-ইনপুট লাগে, টেবিলের সারি দিন, আর দেখান পুরোনো Branch = op6 কেন আর চলে না।
(ক) bne-এর opcode
1100011— beq-এর সমান, তাই main control-এর সারিও এক: ALUSrc 0, MemtoReg X, RegWrite 0, MemRead 0, MemWrite 0, Branch 1, ALUOp 01. পরিবর্তন শুধু শর্তে: PCSrc = Branch · (Zero ⊕ funct3[0]). একটি XOR আর একটি AND gate: beq-এ (funct3[0] = 0) Zero, bne-এ (funct3[0] = 1) Zero′. বিকল্প: আলাদা BranchNE signal, PCSrc = Branch·Zero + BranchNE·Zero′.(খ) নতুন: Jump signal (PCSrc = Branch·Zero + Jump), immediate generator-এ J-format, আর write-back mux-এ তৃতীয় ইনপুট PC + ৪ (MemtoReg ২ বিট: 00 ALU, 01 memory, 10 PC + ৪)। সারি: ALUSrc X, MemtoReg 10, RegWrite 1, MemRead 0, MemWrite 0, Branch 0, Jump 1, ALUOp XX. jal-এর opcode
1101111-এ op6 = 1, তাই Branch = op6 jal-এও Branch = 1 দিত; op6..op4 =110, beq-এর সমান। পার্থক্য op3-এ (beq 0, jal 1). তাই Branch = op6·op3′, Jump = op3, MemtoReg[1] = op6 (beq-এ X বলে চলে)।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমSingle-cycle RISC-V-এ একটি R-type instruction-এর ALU operation ঠিক হওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Instruction memory থেকে ৩২-বিটের word আসে
- Main control opcode দেখে ALUOp = 10 আর বাকি signal দেয়
- ALU control ALUOp, inst[30] আর funct3 নেয়
- ALU control ৪-বিটের operation (যেমন 0110) বানায়
- ALU কাজ করে, আর RegWrite = 1 বলে clock edge-এ ফল rd-তে লেখা হয়
Patterson–Hennessy-ধাঁচের single-cycle RISC-V datapath-এ sw instruction-এর জন্য কোন control signal don't care (X)?
- ALUSrc
- MemtoReg
- RegWrite
- ALUOp
ALU control-এর টেবিলে (0010 add, 0110 sub, 0000 AND, 0001 OR) ALUOp = 01 এলে ALU operation কত হয়?
- 0010
- 0000
- 0110
- funct3 দেখে ঠিক হয়
Single-cycle RISC-V CPU-তে RegWrite signal stuck-at-0 হলে R-type, lw, sw, beq আর addi-এর মধ্যে কোনগুলো তবুও ঠিক চলবে?
- শুধু R-type
- lw আর addi
- কোনোটিই নয়
- sw আর beq
RISC-V-এ beq আর bne-এর opcode একই (1100011), funct3 আলাদা (000, 001). শুধু beq সমর্থন করে এমন single-cycle datapath-এ (PCSrc = Branch · Zero) bne যোগ করার সবচেয়ে সরল উপায় কোনটি?
- PCSrc = Branch · (Zero ⊕ funct3[0])
- Main control-এ bne-এর জন্য নতুন সারি
- bne-এর ALUOp = 10 করা
- MemtoReg দিয়ে PC বাছা
R-type (0110011), lw (0000011), sw (0100011), beq (1100011) আর addi (0010011) সমর্থন করে এমন control-এ Branch = op6. এখন jal (1101111) যোগ করলে Branch-এর নতুন সমীকরণ কী হওয়া উচিত?
- Branch = op6 (বদলানোর দরকার নেই)
- Branch = op6 · op5′
- Branch = op6 · op3′
- Branch = op3
Control-কে main control আর ALU control — দুই স্তরে ভাগ করার প্রধান কারণ কী?
- ISA-তে ALUOp field রাখতে হয় বলে
- ছোট logic, আর সম্ভাব্য দ্রুততর circuit
- প্রতিটি instruction-কে দুই cycle দিতে
- Microprogramming ব্যবহার করতে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কাজ: control unit opcode দেখে mux-এর select (ALUSrc, MemtoReg), write enable (RegWrite, MemWrite), MemRead, Branch আর ALUOp দেয়; PCSrc = Branch · Zero.
- টেবিল: R 0 0 1 0 0 0 10 · lw 1 1 1 1 0 0 00 · sw 1 X 0 0 1 0 00 · beq 0 X 0 0 0 1 01 · addi 1 0 1 0 0 0 00 (ক্রম ALUSrc, MemtoReg, RegWrite, MemRead, MemWrite, Branch, ALUOp).
- দুই স্তর: ALUOp 00 = add, 01 = sub, 10 = funct দেখে ঠিক হয়; ALU control: 0010 add, 0110 sub, 0000 AND, 0001 OR; addi-কে ALUOp 10 নয়।
- সমীকরণ: RegWrite = op4 + op5′, ALUSrc = op5′ + op6′·op4′, MemtoReg = op4′, Branch = op6; X শুধু state-না-বদলানো signal-এ, আর নতুন instruction এলে সব সমীকরণ আবার যাচাই।
- ত্রুটি আর বিকল্প: stuck-at fault ভাঙে শুধু সেই instruction যাদের দরকারি মান উল্টো — fault masking-ও হয়; hardwired দ্রুত, microprogrammed নমনীয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৪ · Multi-cycle datapath আর microprogrammed controlপরবর্তী পর্বcontrol যখন FSM, আর FSM যখন ROM-এর প্রোগ্রাম।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বtransistor থেকে multicore পর্যন্ত পুরো রোডম্যাপ।
- পর্ব ৩২ · Single-cycle datapathআগের ধাপএই পর্বের signal-গুলো যে mux আর ইউনিট চালায়।
- পর্ব ০৯ · K-map দিয়ে সরলীকরণভিত্তিdon't care দিয়ে দল বড় করার কৌশল।
- অপারেটিং সিস্টেম · হার্ডওয়্যারের প্রাথমিক ধারণাপাশের ট্র্যাকCPU, memory আর I/O — OS-এর চোখে।