Register ও Register File — একাধিক Bit-এর স্মৃতি
Registers and Register Files
N-টা D flip-flop এক clock-এ বাঁধা হলে register হয়; write-enable আর reset control যোগ হলে সেটা programmable memory element; আর register-এর array-কে address দিয়ে access করলেই সেটা CPU-র general-purpose register file।
আগে এটা বুঝি
গত লেসনে আমরা D flip-flop বানিয়েছি — একটা 1-bit স্মৃতি। Clock edge-এ যা input-এ ছিল, তাই ধরে রাখে, পরের edge পর্যন্ত।
কিন্তু বাস্তব ডেটা 1 bit-এ থামে না। একটা integer 32 bit। একটা pointer 64 bit। CPU-র প্রতিটা register 32 বা 64 bit চওড়া। প্রশ্নটা তাই সহজ:
১ bit-এর memory element থেকে কীভাবে 32-bit, 64-bit memory element বানাব?
উত্তরটা প্রায় বিরক্তিকরভাবে সহজ — একগাদা D flip-flop পাশাপাশি রাখুন, সবাইকে একই clock দিন। এটাই register। কিন্তু শুধু এইটুকু দিয়ে কাজ চলে না — বাস্তব register-এর দুইটা বাড়তি ক্ষমতা লাগে যেগুলো আজকের মূল বিষয়:
১. Write-enable — register-কে বলা “এই cycle-এ নতুন মান নাও” বনাম “যা আছে তাই ধরে রাখো”। এটা ছাড়া একটা register প্রতি clock edge-এ বাধ্যতামূলকভাবে নতুন মান নিয়ে নেবে — কিন্তু বাস্তবে আমরা প্রায়ই চাই একটা register কয়েকশ cycle স্থির থাকুক, শুধু নির্দিষ্ট মুহূর্তে বদলাক।
২. Reset/clear — সিস্টেম চালু হওয়ার মুহূর্তে register-এর ভেতর কী আছে? Power-on-এ flip-flop-এর অবস্থা অনির্দিষ্ট (আগের লেসনের SR latch-এর মতোই — কোনো জোর করা প্রাথমিক অবস্থা নেই)। Reset signal সেই অনিশ্চয়তা দূর করে।
আর তারপর একটা বড় লাফ — যদি অনেকগুলো register-কে একটা index দিয়ে access করা যায়, সেটাই CPU-র register file। এই লেসনের শেষে আপনি জানবেন x86, ARM, RISC-V-এর “general-purpose register”-গুলো আসলে ঠিক কী — কোনো রহস্য নেই, শুধু flip-flop-এর একটা সুবিন্যস্ত সজ্জা।
মূল ধারণা
Register — N-টা D flip-flop, এক clock
একটা 4-bit register কল্পনা করুন। চারটা D flip-flop, প্রতিটার input আলাদা bit, কিন্তু CLK pin সবগুলোর একসাথে তারযুক্ত — একই wire, একই signal, একই মুহূর্তে পৌঁছায় (আদর্শভাবে — লেসন ১১-এ দেখব বাস্তবে এই “একই মুহূর্তে” পুরোপুরি সত্য না)।
D3 ──┤D Q├── Q3 D2 ──┤D Q├── Q2
│ FF3 │ │ FF2 │
└──┬──┘ └──┬──┘
│ │
D1 ──┤D │Q├── Q1 D0 ──┤D │Q├── Q0
│ FF1 │ │ FF0 │
└──┬──┘ └──┬──┘
│ │
└────────┬───────────────┘
│
CLK (একটাই signal, চারটা flip-flop-এই যায়)গুরুত্বপূর্ণ পর্যবেক্ষণ: প্রতিটা bit সম্পূর্ণ স্বাধীন circuit —
D3 শুধু Q3-কে প্রভাবিত করে, D2 শুধু Q2-কে।
একমাত্র জিনিস যা তাদের সংযুক্ত করে সেটা হলো সময় —
সবাই একই clock edge-এ update হয়।
এই কারণেই register-এর সংজ্ঞা এত সরল: N-টা independent D flip-flop, একটা shared clock, একসাথে N-bit মান হিসেবে ব্যাখ্যা করা।
n-bit register বানাতে n-টা flip-flop লাগে —
এটা linear, আর প্রতিটা bit-এর জন্য আলাদা gate/wiring খরচ,
ঠিক যেমন গত module-এ (computer-representation) আমরা দেখেছি n-bit-এ 2ⁿ টা মান represent হয়,
কিন্তু hardware খরচ n-এর সমানুপাতিক (exponential না)।
Write-enable — “নাও” বনাম “ধরে রাখো”
Plain D flip-flop-এ কোনো choice নেই — প্রতি clock edge-এ যা input pin-এ আছে তাই নিয়ে নেয়। কিন্তু ভাবুন একটা accumulator register — এটা শুধু নির্দিষ্ট cycle-এ নতুন যোগফল নেবে, বাকি cycle-এ আগের মানই ধরে রাখবে। কীভাবে?
ভুল উপায় — clock নিজেই gate করা:
স্বজ্ঞাগতভাবে মনে হতে পারে: “যখন enable না,
তখন clock signal-টাই flip-flop পর্যন্ত পৌঁছাতে দেব না” (একটা AND gate দিয়ে CLK AND EN)।
এটা কাজ করে, কিন্তু বিপজ্জনক — নিচে “hood” অংশে দেখব ঠিক কেন।
সঠিক, নিরাপদ উপায় — feedback MUX:
Clock-কে অক্ষত রাখুন — প্রতিটা flip-flop প্রতি cycle-এ latch করতেই থাকুক। বরং D input-এ একটা 2:1 MUX বসান, যেটা বেছে নেয় নতুন ডেটা নাকি flip-flop-এর নিজের বর্তমান output — যা আবার নিজেরই input হিসেবে ফিরে আসে।
┌────────┐
D_new ────►│0 │
│ MUX ├──► D ──┤D Q├──► Q ──┐
Q ──┬──►│1 │ │ FF │ │
│ └───┬────┘ └──┬──┘ │
│ │ │ │
│ EN CLK │
└─────────────────────────────────────┘
(Q ফিরে আসছে নিজের input হিসেবে)EN = 1→ MUX select input-0 (D_new) →D = D_new→ পরের edge-এ নতুন মান load হয়EN = 0→ MUX select input-1 (Q) →D = Q→ পরের edge-এ “নতুন” মান = “পুরনো” মান — কার্যত hold, যদিও flip-flop প্রযুক্তিগতভাবে প্রতি cycle-এ latch-ই করছে
এইটাই register-এর একটা genuinely গুরুত্বপূর্ণ সূক্ষ্মতা: hold করা মানে flip-flop “কাজ বন্ধ” করেনি — সে প্রতি cycle-এ latch করছে, শুধু যা latch করছে সেটা বারবার নিজের আগের মানই। Power খরচের হিসেবে এটা গুরুত্বপূর্ণ (নিচে দেখুন)।
n-bit register-এ প্রতিটা bit-এর নিজস্ব MUX লাগে,
কিন্তু EN signal-টা সবার জন্য একই (একটা shared control wire,
N-গুণ ডেটা wire নয়) —
এটাই “control vs data” বিভাজনের প্রথম concrete উদাহরণ,
যেটা CPU control unit-এ (এই module-এরই পরের অংশে) বারবার ফিরে আসবে।
Reset / Clear — সিস্টেম শুরু হওয়ার মুহূর্তটা
Power-on মুহূর্তে flip-flop-এর ভেতরের cross-coupled গঠন (গত লেসনের SR latch মনে করুন) কোনো নির্দিষ্ট অবস্থায় “জন্মায় না” — physically এটা যেকোনো দিকে settle করতে পারে, transistor-level manufacturing variation-এর উপর নির্ভর করে। তাই প্রতিটা ব্যবহারযোগ্য register-এ একটা reset (বা clear) input থাকে যেটা জোর করে একটা পরিচিত মান (সাধারণত সব-শূন্য) বসিয়ে দেয়।
দুইটা fundamentally আলাদা ডিজাইন আছে:
Synchronous reset
Reset input-টাকে শুধু আরেকটা MUX select হিসেবে treat করুন —
write-enable-এর মতোই।
Reset assert হলে, D input-এ জোর করে 0 পাঠানো হয়,
কিন্তু শুধু পরের clock edge-এ কার্যকর হয়।
RESET ─┐
▼
┌────────┐
0 ►│0 │
│ MUX ├──► D ──┤D Q├──► Q
D_in►│1 │ │ FF │
└───┬────┘ └──┬──┘
RESET' │
CLKAsynchronous reset
সরাসরি flip-flop-এর ভেতরের গঠনে একটা অতিরিক্ত input যোগ করা হয় (extra transistor,
cross-coupled গঠনের একটা node-এ সরাসরি জোর করে টেনে নামানো) —
যেটা clock নির্বিশেষে, যেকোনো মুহূর্তে কাজ করে।
Reset assert হওয়ামাত্র output সাথে সাথে 0-এ চলে যায়,
clock edge-এর জন্য অপেক্ষা না করেই।
| Synchronous reset | Asynchronous reset | |
|---|---|---|
| কখন কাজ করে | শুধু active clock edge-এ | যেকোনো মুহূর্তে, clock নির্বিশেষে |
| Extra hardware | একটা MUX (বিদ্যমান write-logic-এর অংশ) | Flip-flop-এর ভেতরে সরাসরি extra transistor |
| Glitch-এর প্রতি সংবেদনশীলতা | কম — শুধু clock edge-এ সমস্যা করতে পারে | বেশি — যেকোনো মুহূর্তে assert হতে পারে, নিজেই একটা asynchronous signal |
| Power-up-এ ব্যবহারযোগ্য? | পুরোপুরি না — clock না চললে কখনো reset হবে না | হ্যাঁ — এই কারণেই বেশিরভাগ chip-এর power-on-reset async |
| Timing closure-এ প্রভাব | সহজ — reset পথও normal data path-এর মতোই বিশ্লেষণ করা যায় | কঠিন — reset-এর de-assertion (ছেড়ে দেওয়া) যদি clock edge-এর কাছাকাছি ঘটে, সেটা নিজেই একটা setup/hold-এর মতো (“recovery/removal time”) সমস্যা তৈরি করতে পারে |
ব্যবহারিক নিয়ম: Power-on-reset (chip চালু হওয়ার মুহূর্তে) প্রায় সবসময় async — clock তখনো স্থিতিশীল নাও হতে পারে (PLL lock হতে সময় লাগে)। কিন্তু normal operation-এর মধ্যে কোনো state machine বা counter reset করতে হলে (যেমন লেসন ১৩-এ mod-N counter), sync reset-ই বেশি পছন্দনীয় — কারণ এটা predictable, আর existing timing analysis tool-এ (লেসন ১১) স্বাভাবিক data path-এর মতোই বিশ্লেষণ করা যায়।
Register File — একটা array of register, address দিয়ে access
একটামাত্র register দরকারী, কিন্তু বাস্তব প্রোগ্রামে একসাথে অনেকগুলো মান লাগে (variable-গুলো)। যদি ৩২টা 32-bit register থাকে, প্রতিটাকে আলাদা নাম না দিয়ে বরং একটা index (address) দিয়ে বেছে নিলে কী হয়?
এটাই register file — আর এটাই ঠিক CPU-র “general-purpose register”।
┌─────────────────────────────────┐
WriteAddr(5) ───►│ │
WriteData(32) ──►│ 32 × 32-bit রেজিস্টার │
WriteEnable ────►│ (প্রতিটা রেজিস্টার = লেসন │
│ এই লেসনের উপরের অংশের │
ReadAddr1(5) ───►│ write-enable register) │──► ReadData1(32)
ReadAddr2(5) ───►│ │──► ReadData2(32)
└─────────────────────────────────┘
▲
CLK (শুধু write synchronous;
read সাধারণত combinational)তিনটা মূল অংশ:
১. Write port — WriteAddr (কোন register), WriteData (কী মান), WriteEnable (আদৌ লিখব কি না)। ভেতরে একটা decoder থাকে যেটা 5-bit address-কে 32-লাইনের one-hot signal-এ রূপান্তর করে (গত module-এর decoder লেসন মনে করুন), আর সেই one-hot signal-এর প্রতিটা লাইনকে WriteEnable-এর সাথে AND করে প্রতিটা individual register-এর নিজস্ব enable pin-এ পাঠানো হয়। শুধু একটা register-এর enable সেই cycle-এ high হয় — বাকি ৩১টা hold করে।
২. Read port — ReadAddr দিয়ে একটা বড় MUX (৩২-টা 32-bit input থেকে একটা বেছে নেওয়া, log₂32 = 5-bit select) — এটা সম্পূর্ণ combinational, কোনো clock লাগে না। Address বদলালেই output সাথে সাথে বদলে যায় (propagation delay সাপেক্ষে — লেসন ১১)।
৩. Multiple read port — একই register file-এ একাধিক independent read port থাকতে পারে, শুধু আরেকটা address input + আরেকটা বিশাল MUX যোগ করে। Read port যোগ করা তুলনামূলক সস্তা (শুধু আরও wiring আর MUX, প্রতিটা register-এর নিজস্ব copy লাগে না), কিন্তু write port যোগ করা ব্যয়বহুল — দুইটা write port একই সময়ে ভিন্ন register-এ লিখলে ঠিক আছে, কিন্তু একই register-এ দুইটা ভিন্ন মান লিখতে চাইলে conflict হয় (কোনটা জিতবে, hardware-এ define করতে হয়)।
Register width বনাম data width — sign আর zero extension
একটা register file-এর প্রতিটা register একটা নির্দিষ্ট,
স্থির width (ধরুন 32-bit)।
কিন্তু প্রোগ্রামের ডেটা সবসময় সেই width-এর হয় না —
একটা byte (8-bit) বা short (16-bit) মান একটা 32-bit register-এ রাখতে হলে,
বাকি bit-গুলো দিয়ে কী করা হবে?
এখানেই computer-representation module-এর two’s complement লেসন সরাসরি ফিরে আসে।
দুইটা উপায়:
Zero extension — বাকি উপরের bit-গুলো সবসময় 0 দিয়ে ভরা হয়।
ব্যবহৃত হয় unsigned মানের জন্য।
Sign extension — বাকি উপরের bit-গুলো মূল সংখ্যার sign bit (সবচেয়ে বামের bit) দিয়ে ভরা হয়। ব্যবহৃত হয় signed (two’s complement) মানের জন্য — কারণ এটাই একমাত্র উপায় যাতে মানটার সাংখ্যিক মান অপরিবর্তিত থাকে।
মূল 8-bit মান: 1111 1111 (unsigned হলে 255, signed হলে −1)
Zero-extend (32-bit): 0000 0000 0000 0000 0000 0000 1111 1111
= 255 (unsigned অর্থেই সঠিক)
Sign-extend (32-bit): 1111 1111 1111 1111 1111 1111 1111 1111
= −1 (signed two's complement অর্থে সঠিক)লক্ষ্য করুন —
একই bit pattern, একই register-প্রস্থে সম্প্রসারিত,
কিন্তু দুই সম্পূর্ণ ভিন্ন 32-bit মান তৈরি করে।
কোনটা “সঠিক” তা bit pattern নিজে বলে না —
এটা computer-representation module-এর কেন্দ্রীয় থিসিসেরই আরেকটা রূপ: bit pattern কিছু বলে না, ব্যাখ্যা (interpretation) বলে।
বাস্তব ISA-তে এই পার্থক্যটা প্রায়ই সরাসরি instruction-এর নামে প্রকাশ পায় —
RISC-V-এ LBU (Load Byte Unsigned,
zero-extend করে) বনাম LB (Load Byte,
sign-extend করে) দুইটা আলাদা instruction,
কারণ hardware-কে স্পষ্টভাবে বলে দিতে হয় কোন সম্প্রসারণ ব্যবহার করবে —
register file বা load logic নিজে থেকে অনুমান করতে পারে না ডেটাটা signed নাকি unsigned ছিল।
বাস্তব লেআউট — bit-sliced ডিজাইন
VLSI (chip layout) ডিজাইনে register file একটা বিশেষভাবে নিয়মিত (regular) গঠন হিসেবে বানানো হয় —
একটা কৌশল যাকে বলে bit-slicing।
পুরো register file-কে W-টা (register-এর width) অভিন্ন “column”-এ ভাগ করা হয়,
প্রতিটা column-এ সব n-টা register-এর একটাই bit position থাকে (bit 0-এর column,
bit 1-এর column, …)।
বিট ৩১ বিট ৩০ ... বিট ০
রেজিস্টার ০: [FF + MUX] [FF + MUX] ... [FF + MUX]
রেজিস্টার ১: [FF + MUX] [FF + MUX] ... [FF + MUX]
...
রেজিস্টার ৩১: [FF + MUX] [FF + MUX] ... [FF + MUX]
│ │ │
(একই column-এর (control সিগন্যাল —
সব cell physically enable, address — সব
identical, শুধু column-এই একই ভাবে
পাশাপাশি বসানো) horizontally ছড়ায়)প্রতিটা column হুবহু একই circuit-এর কপি (শুধু কোন bit position ধরে আছে সেটাই আলাদা) —
তাই chip design tool-এ একটা column ডিজাইন করেই সেটা W-বার কপি-পেস্ট করা যায়,
প্রতিটা bit আলাদাভাবে ডিজাইন করতে হয় না।
Control সিগন্যাল (address,
enable) সবগুলো column জুড়ে অনুভূমিকভাবে (horizontally) ছড়িয়ে যায়।
এই নিয়মিততাই কেন register file (আর সাধারণভাবে memory array) মিলিয়ন-transistor chip-এও তুলনামূলক দ্রুত আর নির্ভরযোগ্যভাবে ডিজাইন করা সম্ভব —
এলোমেলো logic (যেমন control unit,
পরের অংশে) ডিজাইন করা এর চেয়ে অনেক বেশি শ্রমসাধ্য,
ঠিক কারণ সেখানে এই ধরনের পুনরাবৃত্তিমূলক নিয়মিততা নেই।
Forward reference — Level 3-এ ঠিক এই register file-ই CPU-র “general-purpose registers”। x86-64-এ ১৬টা 64-bit register (RAX,
RBX, …), ARM AArch64-এ ৩১টা, RISC-V-এ ৩২টা (x0–x31)।
Program Counter (PC) আর Stack Pointer (SP) —
এগুলোও প্রায়ই এই একই ধরনের register হার্ডওয়্যার,
শুধু বিশেষ ব্যবহারিক সম্মেলন সহ (RISC-V-এ x2 conventionally SP,
x1 return address; PC প্রায়ই সম্পূর্ণ আলাদা, dedicated counter —
লেসন ১৩-এ counter শেখার পর এটা আরও স্পষ্ট হবে)।
| ISA | GPR সংখ্যা | চওড়া | Read port (typical) | Write port |
|---|---|---|---|---|
| RISC-V (RV64) | 32 (x0–x31) | 64 bit | 2 | 1 |
| ARM AArch64 | 31 (x0–x30) + SP, XZR | 64 bit | 2–3 (microarch-নির্ভর) | 1–2 |
| MIPS | 32 | 32/64 bit | 2 | 1 |
| x86-64 (ISA-level visible) | 16 (RAX–R15) | 64 bit | microarchitecture-নির্ভর | microarchitecture-নির্ভর |
x86-64-এর সারিটা ইচ্ছাকৃতভাবে অস্পষ্ট — কারণ আধুনিক x86 CPU-তে ISA-তে দেখা যাওয়া ১৬টা register আসলে একটা অনেক বড়, physical register file-এর উপর dynamic map করা (register renaming, Level 11-এ বিস্তারিত)। ISA যা “দেখায়” আর hardware আসলে যা রাখে — এই দুইটা এখানেই প্রথমবার আলাদা হয়ে যাচ্ছে, আর এই ফাঁকটাই আধুনিক out-of-order CPU ডিজাইনের একটা কেন্দ্রীয় কৌশল।
MUX গাছ বনাম Tri-state bus — read port বাস্তবায়নের দুই পথ
উপরের block diagram-এ read port-কে একটা “বড় MUX” বলা হয়েছিল, কিন্তু বাস্তবে সেটা দুইভাবে বানানো যায় — আর দুইটাই বাস্তব chip-এ ব্যবহৃত হয়।
পদ্ধতি ১ — MUX গাছ। ৩২-টা register-এর মধ্যে থেকে একটা বেছে নিতে ৫-স্তরের 2:1 MUX-এর একটা গাছ লাগে (log₂32 = 5)।
প্রতিটা 2:1 MUX নিজেই একটা ছোট Boolean function —
গত module-এর AND/OR/NOT দিয়ে সরাসরি বানানো যায়:
এই একটা expression-এই একটা NOT, দুইটা AND, একটা OR — গত module-এর Boolean algebra লেসনের সরাসরি প্রয়োগ। CMOS-এ বাস্তবে এটা প্রায়ই আরও কম transistor-এ বানানো হয় — একটা transmission-gate MUX (দুইটা transmission gate + select-এর জন্য একটা inverter, মোট ~৬–৮ transistor প্রতি bit) gate-দিয়ে বানানো mux-এর (~১৮–২০ transistor, প্রতিটা AND/OR/NOT আলাদা গেট হিসেবে ধরলে) চেয়ে সস্তা — ঠিক যেমন লজিক গেট-এর glossary entry-তে দেখেছেন NAND/NOR AND/OR-এর চেয়ে সস্তা, কারণ CMOS স্বাভাবিকভাবে inverting।
MUX গাছের সমস্যা: প্রতিটা স্তর নিজের propagation delay যোগ করে (লেসন ১১-এ এটা directly critical path গণনায় ঢুকবে) —
32-থেকে-1 MUX মানে 5-স্তরের delay,
64-থেকে-1 মানে 6-স্তরের।
পদ্ধতি ২ — Tri-state bus। প্রতিটা register-এর output একটা tri-state buffer দিয়ে একটা shared bus-এ যুক্ত থাকে।
Tri-state buffer-এর output তিন অবস্থায় থাকতে পারে —
0, 1, বা high-impedance (Z),
অর্থাৎ “আমি bus স্পর্শই করছি না, যেন সংযুক্তই নই”।
Decoder ঠিক একটা buffer enable করে, বাকি সব Z-তে থাকে —
তাই bus-এ ঠিক একটা register-এর মানই “দেখা” যায়, MUX গাছ ছাড়াই।
Reg[0].Q ──►[tri-state buf, EN=dec[0]]──┐
Reg[1].Q ──►[tri-state buf, EN=dec[1]]──┤
Reg[2].Q ──►[tri-state buf, EN=dec[2]]──┼──► shared bus ──► ReadData
... │ (ঠিক একটা driver active,
Reg[31].Q─►[tri-state buf, EN=dec[31]]──┘ বাকি সব high-Z)এর সুবিধা — bus-এর delay register সংখ্যার সাথে MUX-tree-র মতো log-depth স্তর বাড়িয়ে বাড়ে না (আদর্শভাবে প্রায় constant, বাস্তবে বড় bus-এ capacitance-জনিত delay থাকে)। অসুবিধা — এটা genuinely বিপজ্জনক যদি ভুলবশত একাধিক buffer একসাথে enable হয়ে যায় (নিচে “misconception” অংশে বিস্তারিত — এটা কোনো তাত্ত্বিক শঙ্কা না, বাস্তব hardware-এ ঘটে যাওয়া একটা পরিচিত bug ক্লাস)। আধুনিক ASIC design-এ predictability-র কারণে MUX-tree বেশি জনপ্রিয়, কিন্তু CPU-র internal data bus আর memory bus (Level 3, Level 4) আজও tri-state ধরনের bus-শেয়ারিং নীতিতে কাজ করে — তাই ধারণাটা এখানেই প্রথম দেখা, পরে বারবার ফিরে আসবে।
ভেতরে কী ঘটছে
ভেতরে কী ঘটছে — decoder, AND gate, আর clock gating-এর আসল বিপদ
Write port ভেতর থেকে
উপরের block diagram-এ “decoder” বলে যা দেখানো হয়েছে,
সেটা গত module-এর n-to-2ⁿ decoder-এরই সরাসরি প্রয়োগ।
5-bit WriteAddr ভেতরে যায়, ৩২-লাইনের output বেরোয়,
যার মধ্যে ঠিক একটা লাইন 1 (address-টা যেটা represent করে)।
WriteAddr(5) ──► [ 5-to-32 DECODER ] ──► line[0]..line[31], ঠিক একটা = 1
প্রতিটা register i-এর জন্য:
EN_i = line[i] AND WriteEnableএখানেই লক্ষ্য করুন: WriteEnable global signal-টা AND হয়ে প্রতিটা লাইনে গুণ হচ্ছে —
তাই WriteEnable = 0 হলে কোনো register-ই লিখবে না,
address যাই হোক না কেন।
এটাই নিশ্চিত করে “ভুল করে address set করা কিন্তু write না চাওয়া” অবস্থায় কিছু ভাঙে না।
কেন সরাসরি clock gate করা বিপজ্জনক
উপরে বলা হয়েছিল feedback-MUX-ই “নিরাপদ” পদ্ধতি।
সরাসরি CLK AND EN করে clock signal-টাকেই বন্ধ করলে কী সমস্যা?
সমস্যা হলো EN নিজেই একটা সাধারণ combinational signal —
এটা glitch করতে পারে (একাধিক input বদলানোর সময় ক্ষণিকের জন্য ভুল মান দেখাতে পারে,
gate delay-জনিত hazard —
লেসন ১১-এ বিস্তারিত)।
যদি EN clock-এর সাথে সরাসরি AND হয়,
আর EN-এ একটা glitch ঠিক তখনই ঘটে যখন CLK high,
তাহলে flip-flop একটা ভুয়া extra edge দেখতে পারে —
অথবা উল্টোটা, EN দেরিতে high হলে একটা আসল edge miss করতে পারে।
এই দুটোই catastrophic bug —
data নীরবে হারিয়ে যায় বা ভুল সময়ে capture হয়।
CLK ‾‾\__/‾‾\__/‾‾\__/‾‾\__
EN ______/‾\_/‾‾‾‾‾‾‾‾____ ← EN নিজেই glitch করছে (combinational hazard)
CLK∧EN ______/‾\__/‾‾\__/‾‾____
↑
একটা ভুয়া extra pulse — flip-flop এই মুহূর্তে
একটা অনিচ্ছাকৃত edge দেখতে পারেএই কারণেই বাস্তব chip-এ যখন সত্যিকারের clock gating করা হয় (শুধু power বাঁচানোর জন্য,
নিচে দেখুন), সেটা কখনো একটা সাধারণ AND gate দিয়ে হয় না —
বরং একটা বিশেষ Integrated Clock Gating (ICG) cell ব্যবহার হয়,
যার ভেতরে একটা level-sensitive latch থাকে যেটা EN-কে শুধু clock low থাকা অবস্থায় sample করে (যখন EN-এর glitch করলেও কোনো ক্ষতি নেই,
কারণ gated output তখনো low-ই থাকবে)।
Feedback-MUX পদ্ধতি (এই লেসনে শেখানো) এই সমস্যা সম্পূর্ণ এড়িয়ে যায় —
কারণ clock কখনো touch হয় না, শুধু data path বদলায়,
আর data path-এর glitch flip-flop-এর জন্য কোনো সমস্যাই না (flip-flop শুধু clock edge-এর মুহূর্তে D pin দেখে —
তার বাইরে যেকোনো glitch অগ্রাহ্য হয়)।
Same-cycle write-then-read — একটা forward reference
একটা প্রশ্ন যেটা register file নিয়ে কাজ করলে অবধারিতভাবে আসে: যদি একই cycle-এ একটা register-এ লেখা হয় আর ঠিক একই register পড়া হয়, তাহলে read output-এ কোনটা দেখা যাবে — পুরনো মান, নাকি নতুন মান?
যেহেতু read সম্পূর্ণ combinational (উপরে দেখানো হয়েছে) আর write শুধু clock edge-এ কার্যকর হয়, স্বাভাবিক আচরণ হলো read সবসময় পুরনো মান দেখাবে (write তখনো register-এর ভেতরে “in flight”, clock edge পার হয়নি)। কিছু design ইচ্ছাকৃতভাবে একটা “write-through” bypass path যোগ করে যাতে read সাথে সাথে নতুন লেখা মানই দেখায় — এই সিদ্ধান্তটা Level 3-এ CPU pipeline hazard আলোচনায় (data forwarding) সরাসরি ফিরে আসবে। আপাতত শুধু মনে রাখুন — এটা একটা design choice, কোনো “স্বাভাবিক সত্য” না।
উদাহরণ
একটা সম্পূর্ণ trace — accumulator register, cycle বাই cycle
ধরুন একটা 8-bit accumulator register আছে, write-enable সহ,
আর সেটা একটা adder-এর output ধরে রাখছে (Q_new = Q_old + input,
কিন্তু শুধু EN=1 cycle-এ)।
| Cycle | CLK edge | EN | D (adder output = Q_old + input) | Q (edge-এর আগে) | Q (edge-এর পরে) |
|---|---|---|---|---|---|
| 1 | ↑ | 1 | 0 + 5 = 5 | 0 | 5 |
| 2 | ↑ | 0 | 5 + 3 = 8 (গণনা হচ্ছে, কিন্তু…) | 5 | 5 (hold — ৮ ignored) |
| 3 | ↑ | 1 | 5 + 3 = 8 | 5 | 8 |
| 4 | ↑ | 1 | 8 + 10 = 18 | 8 | 18 |
| 5 | ↑ | 0 | 18 + 7 = 25 (গণনা হচ্ছে, কিন্তু…) | 18 | 18 (hold) |
লক্ষ্য করুন —
cycle ২ আর ৫-এ combinational adder ঠিকই নতুন যোগফল গণনা করে ফেলছে (D input-এ সেটাই বসে আছে),
কিন্তু EN=0 থাকায় feedback-MUX সেটা বাদ দিয়ে Q-ই আবার D-তে পাঠায়,
তাই register পরিবর্তিত হয় না।
এটাই write-enable-এর পুরো কাজ —
combinational logic সবসময় চলতে থাকে, কিন্তু register কখন সেই ফলাফল “গ্রহণ” করবে সেটা EN নিয়ন্ত্রণ করে।
Register file-এ একটা instruction trace
RISC-V-এর মতো একটা instruction ভাবুন: add x3, x1, x2 (মানে: x3 = x1 + x2)।
| ধাপ | Signal | মান |
|---|---|---|
| ১. Read | ReadAddr1 | 1 (x1) |
ReadAddr2 | 2 (x2) | |
ReadData1, ReadData2 (combinational, সাথে সাথে) | x1, x2-এর বর্তমান মান | |
| ২. Compute | ALU (register file-এর বাইরে, পরের লেসনগুলোয়) | ReadData1 + ReadData2 |
| ৩. Write | WriteAddr | 3 (x3) |
WriteData | ALU-র ফলাফল | |
WriteEnable | 1 | |
| পরের clock edge-এ | x3-এর register-এ ফলাফল লেখা হয় |
এই একই cycle-এ read (combinational, সাথে সাথে) আর write (clock edge-এ) দুইটাই ঘটছে — এটাই ঠিক Level 3-এ single-cycle CPU datapath-এ যা দেখবেন, শুধু এখানে ALU নেই (আগের লেসনগুলোয় বানানো হয়েছে)।
কেন কিছু register file আসলে flip-flop দিয়ে বানানোই হয় না
এই পুরো লেসনে ধরে নেওয়া হয়েছে register file মানে flip-flop-এর array — ছোট (১৬–৩২ entry) CPU register file-এর জন্য এটাই বাস্তবসম্মত, কারণ speed সবচেয়ে গুরুত্বপূর্ণ, আর flip-flop দ্রুততম memory element।
কিন্তু GPU-র মতো জায়গায় (উপরে “realworld”-এ উল্লেখ) যেখানে register file হাজার হাজার entry — সেখানে প্রতি bit ~২০+ transistor-এর flip-flop দিয়ে বানালে চিপের বিশাল এলাকা শুধু register file-ই খেয়ে ফেলবে। তাই বড় register file প্রায়ই SRAM cell-ভিত্তিক ডিজাইন ব্যবহার করে (এই module-এরই পরের লেসনে SRAM-এর গঠন দেখবেন — মাত্র ৬ transistor প্রতি bit, কিন্তু flip-flop-এর চেয়ে ধীর আর জটিল read/write timing)। এটাই এই module-এর একটা বড় থিম প্রথমবার দেখা যাচ্ছে — “সঠিক” ডিজাইন বলে কিছু নেই, শুধু trade-off আছে: flip-flop = দ্রুত কিন্তু বড়, SRAM cell = ছোট কিন্তু জটিল আর তুলনামূলক ধীর।
Read port fan-in — একটা লুকানো critical-path সমস্যা
MUX গাছ (উপরে) বা tri-state bus — দুইটার কোনোটাই বিনামূল্যে না।
Register সংখ্যা n বাড়লে:
- MUX গাছে depth বাড়ে
log₂n-এর হারে — প্রতিটা বাড়তি স্তর নতুন propagation delay যোগ করে - Tri-state bus-এ প্রতিটা buffer bus-এ কিছু capacitance যোগ করে (এমনকি
Zঅবস্থায়ও) —nবড় হলে bus নিজেই ধীর হয়ে যায়, কারণ charge/discharge করার জন্য বেশি capacitance
তার মানে register file-এর read port প্রায়ই পুরো CPU-র critical path-এর একটা বড় অংশ —
বিশেষ করে যদি port সংখ্যা বেশি হয় (প্রশ্ন ৩-এ দেখা O(n·(r+w²)) স্কেলিং মনে করুন)।
এই কারণেই বড় register file বা বেশি port লাগলে architect-রা ছোট করে ভাগ করে নেন (register banking/clustering —
একটা বড় ৬৪-entry register file-এর বদলে দুইটা ৩২-entry ব্যাংক,
প্রতিটার আলাদা, ছোট read-MUX)।
এটা এই লেসনের একটা মূল সিদ্ধান্তমূলক পাঠ: “correctness” পাওয়া (একটা কাজ করা register file বানানো) সহজ; “fast enough at the target frequency” পাওয়া —
সেটাই পরের লেসনের বিষয়।
নিজে চালিয়ে দেখুন
Logisim/Digital-এ write-enable register বানানো
১. একটা 4-bit D flip-flop bank বানান (৪টা পৃথক D flip-flop, common clock)।
২. প্রতিটা flip-flop-এর D input-এর আগে একটা 2:1 MUX বসান — select line-টা একটাই EN signal, সব MUX-এ একসাথে যাবে।
৩. MUX-এর input-1-এ flip-flop-এর নিজের Q ফিরিয়ে দিন (feedback), input-0-এ বাইরের D_new[3:0] দিন।
৪. একটা manual clock (button) আর EN toggle switch যোগ করুন।
৫. পরীক্ষা করুন:
D_new = 1010,EN = 1, clock করুন →Q=1010হওয়া উচিত- এখন
D_new = 0101-এ বদলান, কিন্তুEN = 0রাখুন, clock করুন →Qএখনো1010থাকা উচিত (hold কাজ করছে প্রমাণ) EN = 1করে আবার clock করুন →Qএখন0101হয়ে যাবে
৬. একটা asynchronous reset pin যোগ করুন (Logisim-এ flip-flop-এর properties-এ সরাসরি option থাকে) — যেকোনো মুহূর্তে (clock নির্বিশেষে) assert করে দেখুন Q সাথে সাথে 0 হয়ে যায়, পরের clock edge-এর জন্য অপেক্ষা না করেই। এটাই async বনাম sync-এর মূল পার্থক্য প্রত্যক্ষভাবে দেখা।
Feedback-MUX দিয়ে বানানো enable যুক্তিসঙ্গতভাবে কাজ করে — EN=0 অবস্থায় D input বদলালেও Q অপরিবর্তিত থাকে, EN=1-এ পরের edge-এ ঠিকই নতুন মান লোড হয়।
একটা 4×4 register file বানানো — decoder + MUX দিয়ে
১. চারটা 4-bit register বানান (উপরের experiment-এর মতো enable-সহ)।
২. একটা 2-to-4 decoder ব্যবহার করে WriteAddr[1:0]-কে ৪টা one-hot লাইনে রূপান্তর করুন।
৩. প্রতিটা লাইনকে WriteEnable-এর সাথে AND করে সংশ্লিষ্ট register-এর EN pin-এ পাঠান।
৪. একটা 4-to-1 MUX বানান ReadAddr[1:0] দিয়ে চারটা register-এর output থেকে একটা বেছে নেওয়ার জন্য।
৫. পরীক্ষা: WriteAddr=01, WriteData=1100, WriteEnable=1, clock করুন। তারপর ReadAddr=01 সেট করুন — ReadData সাথে সাথে 1100 দেখানো উচিত, clock ছাড়াই (কারণ read combinational)। ReadAddr=10-এ বদলান — সাথে সাথে সেই (এখনো না-লেখা, সম্ভবত 0000) register-এর মান দেখাবে।
৬. চ্যালেঞ্জ: একটা দ্বিতীয় independent read port যোগ করুন (আরেকটা ReadAddr2 + আরেকটা 4-to-1 MUX, একই চারটা register-এর output থেকেই)। এটাই dual-read-port register file — যাচাই করুন দুইটা read port একই সময়ে দুইটা ভিন্ন register-এর মান দেখাতে পারছে।
Write port-এ decoder + AND দিয়ে ঠিক একটা register-ই আপডেট হয়, আর read port-এ MUX দিয়ে যেকোনো address-এর মান সাথে সাথে (combinational) বেরিয়ে আসে — এটাই CPU register file-এর মূল প্যাটার্ন, শুধু ছোট আকারে।
Tri-state bus দিয়ে read port — আর ইচ্ছাকৃতভাবে bus contention ঘটানো
১. আগের experiment-এর ৪টা register রাখুন, কিন্তু read port-টা এবার MUX দিয়ে না বানিয়ে প্রতিটা register-এর output-এ একটা tri-state buffer (Logisim/Digital-এ আলাদা component হিসেবে পাওয়া যায়) যোগ করুন।
২. চারটা buffer-এর output একটাই common wire-এ (bus) জুড়ে দিন।
৩. Decoder-এর output লাইনগুলো (আগের experiment-এর write-decoder-এর মতোই, কিন্তু এবার আলাদা ReadAddr-এর জন্য) প্রতিটা buffer-এর enable pin-এ পাঠান।
৪. পরীক্ষা করুন — ReadAddr=10 সেট করলে bus-এ ঠিক register ২-এর মান দেখা উচিত, বাকি তিনটা buffer high-Z।
৫. ইচ্ছাকৃতভাবে ভাঙুন: decoder সরিয়ে সাময়িকভাবে দুইটা buffer-এর enable pin একসাথে সরাসরি 1-এ বেঁধে দিন (দুইটা ভিন্ন register-এর buffer)। যদি দুই register-এর মান আলাদা হয় (একটায় 1, আরেকটায় 0 — একই bit position-এ), সিমুলেটর সেই bit-এ একটা conflict/error state (প্রায়ই কমলা রঙে বা X/error চিহ্নে) দেখাবে — এটাই bus contention, নিচে “misconception” অংশে যার বাস্তব বিপদ আলোচিত।
Tri-state buffer দিয়ে বানানো read port ঠিক MUX-এর মতোই সঠিক ফলাফল দেয় যতক্ষণ ঠিক একটা buffer active থাকে — কিন্তু দুইটা buffer ভুল করে একসাথে active হলে সিমুলেটর নিজেই একটা error/conflict দেখাবে, বাস্তব hardware-এ যা শর্ট-সার্কিটের সমতুল্য।
নিজে বানান
Register File Simulator — Python-এ cycle-accurate model
- একটা RegisterFile ক্লাস লিখুন — N-টা register, প্রতিটা W-bit চওড়া
- read(addr) মেথড লিখুন — combinational, সাথে সাথে বর্তমান মান ফেরত দেয়
- write(addr, data, enable) মেথড লিখুন — কিন্তু আসল পরিবর্তন pending রাখুন
- একটা আলাদা tick() মেথড লিখুন — এটাই "clock edge", যেখানে pending write আসলে কার্যকর হয়
- একটা ছোট instruction sequence simulate করুন — verify করুন hold, write-enable, dual-read সব ঠিকঠাক কাজ করছে
মূল বিষয় —
সফটওয়্যারে register file মডেল করার সময় সবচেয়ে গুরুত্বপূর্ণ শৃঙ্খলা হলো write() আর সেই write কার্যকর হওয়া আলাদা রাখা,
ঠিক যেমন hardware-এ combinational logic আর clocked update আলাদা।
class RegisterFile:
"""N registers, প্রতিটা W-bit — hardware-এর মতোই read combinational,
write শুধু tick()-এ কার্যকর হয়।"""
def __init__(self, n_registers=32, width=32):
self.n = n_registers
self.mask = (1 << width) - 1
self.regs = [0] * n_registers
self._pending = None # (addr, data) অথবা None
def read(self, addr):
"""Combinational — সাথে সাথে বর্তমান মান, tick() ছাড়াই।"""
assert 0 <= addr \< self.n
return self.regs[addr]
def write(self, addr, data, enable=True):
"""শুধু 'ইচ্ছা' রেকর্ড করে — আসল write এখনো হয়নি।
হার্ডওয়্যারে এটাই D input সেট করার সমতুল্য, clock edge-এর আগে।"""
if enable:
self._pending = (addr, data & self.mask)
else:
self._pending = None # EN=0 → hold, কিছুই লেখা হবে না
def tick(self):
"""এটাই clock edge — pending write, যদি থাকে, এখনই কার্যকর হয়।"""
if self._pending is not None:
addr, data = self._pending
self.regs[addr] = data
self._pending = None
def __repr__(self):
return " ".join(f"x{i}={v}" for i, v in enumerate(self.regs) if v != 0) or "(সব শূন্য)"
# ── ব্যবহার — একটা ছোট instruction sequence ──────────────
rf = RegisterFile(n_registers=8, width=8)
# add x3, x1, x2 — কিন্তু আগে x1, x2-এ মান বসাতে হবে
rf.write(1, 5, enable=True)
rf.tick() # x1 = 5
rf.write(2, 3, enable=True)
rf.tick() # x2 = 3
# এখন একই cycle-এ read + write (add x3, x1, x2)
a = rf.read(1) # combinational read, tick()-এর আগে
b = rf.read(2)
rf.write(3, a + b, enable=True) # শুধু pending — এখনো x3-এ যায়নি
print("tick()-এর ঠিক আগে:", rf) # x3 এখনো 0 — write এখনো "in flight"
rf.tick()
print("tick()-এর পরে :", rf) # x3 = 8
# write-enable=False দিয়ে hold পরীক্ষা
before = rf.read(3)
rf.write(3, 99, enable=False) # EN=0 — hold করার কথা
rf.tick()
after = rf.read(3)
assert before == after == 8, "EN=False থাকা সত্ত্বেও x3 বদলে গেছে — bug!"
print("hold verified: x3 এখনো", after)প্রত্যাশিত output:
tick()-এর ঠিক আগে: x1=5 x2=3
tick()-এর পরে : x1=5 x2=3 x3=8
hold verified: x3 এখনো 8লক্ষ্য করুন write() কল করার পরপরই rf.regs[3] পরীক্ষা করলে এখনো পুরনো মান দেখাবে —
ঠিক hardware-এ যেমন write পরের edge পর্যন্ত কার্যকর হয় না।
এই _pending/tick() বিভাজনটাই সবচেয়ে গুরুত্বপূর্ণ শিক্ষা —
অনেক শিক্ষার্থী প্রথমবার register file model করতে গিয়ে write()-কে সাথে সাথে effective ধরে ফেলেন,
যা বাস্তব hardware semantics ভুলভাবে উপস্থাপন করে।
নিজে বাড়ান:
- দুইটা read port (
read1,read2) আলাদাভাবে যোগ করুন, একসাথে দুইটা ভিন্ন address পড়ে দেখান - একটা synchronous reset যোগ করুন —
reset()মেথড যেটা_pending = (all_addrs, 0)-এর মতো কাজ করে, শুধু পরেরtick()-এ কার্যকর হয় - এখন একটা
reset_async()মেথড যোগ করুন যা সাথে সাথে সব register শূন্য করে দেয়,tick()-এর অপেক্ষা ছাড়াই — sync বনাম async-এর পার্থক্যটা কোডে স্পষ্টভাবে দেখান - একটা “write-through” mode যোগ করুন যেখানে একই cycle-এ write হওয়া register পড়লে নতুন মানই ফেরত আসে (bypass) — আর ছাড়া mode-এ তুলনা করুন
x0(RISC-V কনভেনশন) সবসময়0—write(0, ...)কে no-op বানিয়ে এই hardware কনভেনশন বাস্তবায়ন করুন
একটা সমাধান দেখে নেওয়া যাক — dual read port আর x0-hardwired-zero:
class RiscVStyleRegisterFile(RegisterFile):
"""x0 সবসময় 0, দুইটা independent (combinational) read port।"""
def read1(self, addr):
return 0 if addr == 0 else self.read(addr)
def read2(self, addr):
return 0 if addr == 0 else self.read(addr)
def write(self, addr, data, enable=True):
if addr == 0:
return # x0-এ লেখার চেষ্টা silently ignore — হার্ডওয়্যার কনভেনশন
super().write(addr, data, enable)
rf2 = RiscVStyleRegisterFile(n_registers=32, width=32)
rf2.write(1, 100, enable=True); rf2.tick()
rf2.write(2, 200, enable=True); rf2.tick()
# add x3, x1, x2
a, b = rf2.read1(1), rf2.read2(2)
rf2.write(3, a + b, enable=True)
rf2.tick()
print("x3 =", rf2.read(3)) # 300
# addi x5, x0, 5 — x0 আসলে 0, তাই এটাই "constant load"
rf2.write(5, rf2.read1(0) + 5, enable=True)
rf2.tick()
print("x5 =", rf2.read(5)) # 5
# x0-এ লেখার চেষ্টা — কার্যকর হওয়া উচিত না
rf2.write(0, 999, enable=True)
rf2.tick()
print("x0 =", rf2.read(0)) # এখনো 0 — hardwiredপ্রত্যাশিত output:
x3 = 300
x5 = 5
x0 = 0লক্ষ্য করুন read1/read2 সম্পূর্ণ independent মেথড —
ঠিক দুইটা physically আলাদা read port-এর মতো,
দুইটা একই cycle-এ (এখানে একই Python statement-এর মধ্যেই) দুইটা ভিন্ন address পড়তে পারছে।
আর write()-এর প্রথম লাইনেই x0 guard —
hardware-এ এটাই সবচেয়ে সহজ implementation: MUX/decoder-এর সাথে একটা extra AND gate,
WriteAddr == 0 হলে সব EN_i জোর করে 0।
বাস্তব সিস্টেমে
Register যেখানে বাস্তবে বাস করে
x86-64 GPR। ১৬টা 64-bit general-purpose register (RAX–R15),
প্রতিটাই এই লেসনের register —
flip-flop bank, write-enable,
address দিয়ে (instruction encoding-এর register-field দিয়ে) access।
RISC-V-এর x0। x0 register হার্ডওয়্যারে hardwired zero —
এতে কোনো flip-flop নেই, শুধু ground/০-এর তার।
লেখার চেষ্টা silently ignore হয়, পড়লে সবসময় 0।
এটা একটা elegant trick —
addi x5, x0, 5 দিয়ে “constant 5 load করা” যায়,
আলাদা কোনো “load immediate” instruction লাগে না।
Program Counter। সাধারণত register file-এর বাইরে একটা dedicated register, কারণ এর আচরণ বিশেষ — প্রতি instruction-এ স্বয়ংক্রিয়ভাবে বাড়ে (এটাই পরের লেসনের counter), branch/jump-এ নতুন মান load হয়। লেসন ১৩-এ এই “register + counter” সংযোগ স্পষ্ট হবে।
Stack Pointer। কিছু ISA-তে (RISC-V) এটা শুধু register file-এর একটা register (x2) একটা সম্মেলন অনুযায়ী —
hardware নিজে জানে না x2 “special”; কম্পাইলার আর calling convention সেটা নির্ধারণ করে।
অন্য ISA-তে (x86) এটা সত্যিই একটা dedicated hardware register (RSP),
push/pop instruction সরাসরি এর সাথে যুক্ত।
GPU register file। একটা modern GPU-র প্রতিটা Streaming Multiprocessor-এ register file CPU-র চেয়ে বিশাল — হাজার হাজার entry, কারণ শত শত thread একসাথে সক্রিয় থাকে, প্রতিটার নিজস্ব register set লাগে (context switch খরচ এড়াতে)। এটা register file-এর আকার আর ব্যবহার সম্পূর্ণ ভিন্ন স্কেলে নিয়ে যায়, কিন্তু মূলনীতি একই — address দিয়ে access করা flip-flop array।
Pipeline register। CPU pipeline-এর প্রতিটা স্তরের (fetch,
decode, execute…) মাঝে একটা register বসানো হয় —
যা এই লেসনেরই plain N-bit register,
শুধু write-enable প্রায়ই সবসময় 1 (প্রতি cycle-এ pipeline এগোয়),
যদি না একটা “stall” সংকেত hold করতে বলে।
Level 3/11-এ এটাই pipeline hazard-এর কেন্দ্রীয় hardware উপাদান।
Memory-mapped I/O control register। শুধু CPU register file-ই “register” না —
একটা peripheral-এর control register-ও (যেমন STM32 microcontroller-এর GPIO ODR register) hardware-এর দিক থেকে ঠিক একই জিনিস —
write-enable-যুক্ত flip-flop bank,
শুধু address CPU-র memory bus-এ ম্যাপ করা,
register file-এর decoder-এর বদলে memory address decoder ব্যবহার করে।
Out-of-order CPU-র physical register file। আধুনিক high-performance CPU-তে (Level 11) architectural register (যেমন x0–x31) আসলে ৮–১০ গুণ বড় একটা “physical” register file-এর উপর map করা হয় (register renaming) —
একই মূলনীতি (address দিয়ে access করা flip-flop array),
শুধু address mapping dynamic ও রানটাইমে বদলায়।
যে ভুলগুলো সবাই করে
“Register মানেই ছোট RAM — শুধু address space ছোট।”
সম্পূর্ণ ভুল স্কেল আর গঠন দুই দিক থেকেই।
গঠন: RAM (এই module-এর পরের অংশে দেখব) সাধারণত একটা dense grid — একটা transistor বা কয়েকটা transistor প্রতি bit, একটা shared bit-line/word-line গঠনে, বিশাল array-তে scale করার জন্য optimize করা। Register file-এর প্রতিটা bit সম্পূর্ণ একটা D flip-flop — অনেক বেশি transistor প্রতি bit (~২০+ বনাম RAM-এর ১–৬), কিন্তু অনেক দ্রুত এবং সরাসরি ALU-র সাথে তারযুক্ত।
গতি: Register access সাধারণত একই cycle-এ হয়, কোনো বাড়তি latency ছাড়াই। L1 cache access কয়েক cycle লাগে। Main RAM কয়েকশ cycle। এটাই memory hierarchy-র (Level 3/11) সবচেয়ে উপরের স্তর — register সবচেয়ে দ্রুত, কিন্তু সবচেয়ে ছোট (মাত্র কয়েক ডজন entry, বনাম RAM-এর বিলিয়ন byte)।
আকার: একটা register file-এ সাধারণত ১৬–৩২টা entry থাকে। RAM-এ বিলিয়ন byte। এটা কোনো “ছোট সংস্করণ” না — সম্পূর্ণ ভিন্ন প্রযুক্তি, ভিন্ন উদ্দেশ্যে অপ্টিমাইজড।
“Write-enable বন্ধ থাকলে flip-flop 'কাজ করা বন্ধ' করে দেয়, power বাঁচায়।”
Feedback-MUX পদ্ধতিতে (এই লেসনে শেখানো) না —
flip-flop প্রতি clock edge-এই সক্রিয়ভাবে latch করে,
শুধু বারবার একই মান (নিজের Q) latch করে।
Internal transistor সুইচ করে, dynamic power খরচ হয় — hold অবস্থাতেও।
সত্যিকারের power-saving clock gating (উপরে “hood” অংশে আলোচিত ICG cell দিয়ে) আসলে clock signal-টাকেই flip-flop পর্যন্ত পৌঁছাতে দেয় না — তখন flip-flop সত্যিই টোগল করে না, power বাঁচে। কিন্তু এটা একটা আলাদা, সাবধানে ডিজাইন করা কৌশল — সাধারণ feedback-MUX enable এই সুবিধা দেয় না।
“Asynchronous reset সবসময় ভালো, কারণ এটা 'সাথে সাথে' কাজ করে।”
“সাথে সাথে কাজ করা” শুনতে ভালো মনে হলেও এর একটা লুকানো দাম আছে — reset de-assert (ছেড়ে দেওয়া) যদি clock edge-এর কাছাকাছি সময়ে ঘটে, flip-flop-এর জন্য এটা ঠিক setup/hold ভায়োলেশনের মতোই বিপজ্জনক (recovery/removal time — এই লেসনের “hood” অংশ দেখুন), ফলে metastability-র (পরের লেসন) ঝুঁকি তৈরি হতে পারে।
সেই কারণে বাস্তব chip design-এ প্রায়ই একটা hybrid পদ্ধতি ব্যবহার হয় — reset assert async (নিরাপদ, দ্রুত), কিন্তু de-assert একটা synchronizer দিয়ে clock edge-এর সাথে align করে ছাড়া হয়। “Async সবসময় better” একটা অতি-সরলীকরণ।
“একাধিক register একসাথে পড়া অসম্ভব, কারণ register file-এর আউটপুট একটাই তার।”
Read port সংখ্যা একটা design choice, hardware limitation না। প্রতিটা read port নিজের address input + নিজের বড় MUX নিয়ে সম্পূর্ণ independent — একই register বন্টন-এর ভেতরকার flip-flop-গুলোর output সব read port-এই একসাথে wire করা যায় (একটা output pin অন্য output pin-কে block করে না, কারণ এগুলো আলাদা তার, আলাদা MUX)।
সীমা আছে বাস্তবে — প্রতিটা বাড়তি read port মানে বাড়তি wiring আর বড় MUX (আরও gate delay, লেসন ১১), তাই খুব বেশি port ব্যবহারিকভাবে ব্যয়বহুল হয়ে যায়। কিন্তু “একাধিক read সম্ভবই না” — এটা ভুল; ২ read port (RISC design-এ প্রমিত) থেকে শুরু করে ৮+ read port (বড় superscalar CPU-তে) পর্যন্ত বাস্তবে দেখা যায়।
“Tri-state bus-এ দুইটা driver একসাথে active হলে সিমুলেটর একটা 'error' দেখায় বলে বাস্তবে সেটাও নিরাপদ, শুধু একটা logical bug।”
না — এটা বাস্তব hardware-এ physically বিপজ্জনক। যখন দুইটা tri-state buffer একই bus wire-এ একসাথে active থাকে আর ভিন্ন মান ড্রাইভ করার চেষ্টা করে (একটা 1/উচ্চ voltage-এর দিকে ঠেলছে,
আরেকটা 0/ground-এর দিকে টানছে),
এটা কার্যত সরাসরি power আর ground-এর মধ্যে একটা কম-প্রতিরোধের পথ তৈরি করে —
এটাকে বলে bus contention,
আর এটা একটা সত্যিকারের short circuit।
ফলাফল: হঠাৎ বড় পরিমাণ current প্রবাহ, তাপ উৎপন্ন হওয়া, আর দীর্ঘ সময় ধরে (বা বারবার) ঘটলে সত্যিকারের transistor পুড়ে যাওয়া/chip নষ্ট হওয়া সম্ভব। পুরনো TTL-ভিত্তিক সিস্টেমে (আগেকার computer bus design) ভুল বাসের arbitration-এর কারণে এই সমস্যা একটা পরিচিত hardware failure mode ছিল — এই কারণেই আধুনিক bus protocol design-এ (Level 7-এর networking bus বা Level 4-এর memory bus) arbitration logic এত সাবধানে ডিজাইন করা হয়, যাতে দুইটা driver কখনো একসাথে সক্রিয় না হয়। Logisim/Digital-এর “error” indicator একটা সৌজন্যমূলক সতর্কতা — বাস্তব সিলিকনে কোনো সৌজন্যমূলক সতর্কতা নেই।
বুঝেছেন কি না দেখুন
1একটা 8-bit register-এ write-enable নেই,
শুধু plain D flip-flop bank।
এই register দিয়ে কি কোনো দরকারী কাজ করা সম্ভব? সমস্যাটা ঠিক কোথায়?
যুক্তি
সম্ভব, কিন্তু মারাত্মকভাবে সীমিত।
Write-enable ছাড়া register প্রতি clock edge-এ বাধ্যতামূলকভাবে নতুন input নিয়ে নেয় — কোনো “hold” বিকল্প নেই। এর মানে:
- যদি upstream combinational logic (যেমন একটা adder) প্রতি cycle-এ ভিন্ন মান আউটপুট দেয়, register সেটাই প্রতি cycle-এ গিলে নেবে — কোনোভাবে একটা নির্দিষ্ট মান কয়েক cycle ধরে রাখা যাবে না।
- এটা শুধু তখনই কাজে লাগে যদি circuit-টা এমনভাবে ডিজাইন করা হয় যে “সবসময় নতুন মান আসছে, সবসময় গ্রহণযোগ্য” — যেমন একটা pure pipeline stage যেখানে প্রতি cycle-এ নতুন ডেটা আসা প্রত্যাশিত (কোনো stall নেই)।
বাস্তবে প্রায় কোনো দরকারী register-ই write-enable ছাড়া চলে না — একটা accumulator, একটা counter (যেটা মাঝে মাঝে reset বা hold দরকার), একটা register file (যেখানে বেশিরভাগ cycle-এ কোনো write হয় না) — সবারই “এই cycle-এ বদলাবে না” বলার ক্ষমতা লাগে।
একটা ব্যতিক্রম: যদি সার্কিটটা এমন হয় যেখানে প্রতিটা register সবসময় প্রতি cycle-এ একটা নতুন, গ্রহণযোগ্য মান পায় (যেমন একটা fully-pipelined, stall-free datapath-এর মাঝের register), তখন write-enable আসলেই অপ্রয়োজনীয় — এটাই সেই বিশেষ ক্ষেত্র যেখানে “সবসময় লোড করো” সঠিক আচরণ।
2একটা register file-এ WriteEnable = 1, WriteAddr = 5,
কিন্তু ঠিক সেই cycle-এ ReadAddr1 = 5-ও সেট করা আছে (একই register পড়া আর লেখা হচ্ছে)।
এই লেসনের ডিজাইন অনুযায়ী ReadData1-এ কী দেখা যাবে —
নতুন লেখা মান, নাকি পুরনো মান? কেন?
প্রয়োগ
WriteEnable = 1, WriteAddr = 5,
কিন্তু ঠিক সেই cycle-এ ReadAddr1 = 5-ও সেট করা আছে (একই register পড়া আর লেখা হচ্ছে)।
এই লেসনের ডিজাইন অনুযায়ী ReadData1-এ কী দেখা যাবে —
নতুন লেখা মান, নাকি পুরনো মান? কেন?পুরনো মান — এই লেসনে দেখানো standard ডিজাইনে (bypass/forwarding ছাড়া)।
কারণ:
- Read port সম্পূর্ণ combinational — এটা সরাসরি register-এর বর্তমান
Qoutput পড়ে, কোনো clock এর জন্য অপেক্ষা করে না। - Write শুধু পরের clock edge-এ কার্যকর হয় — সেই মুহূর্ত পর্যন্ত register-এর
Qঅপরিবর্তিতই থাকে,WriteDataকেবলDinput-এ “অপেক্ষমাণ”। - তাই একই cycle-এর মধ্যে যেকোনো সময়ে read করলে, clock edge না আসা পর্যন্ত পুরনো মানই দেখা যাবে।
এটাই Build It অংশের Python simulator-এ _pending/tick() বিভাজন দিয়ে ঠিক এই আচরণ মডেল করা হয়েছিল —
write() কল করার পরপরই read() করলে পুরনো মান দেখা যায়,
tick()-এর পরেই নতুন মান দেখা যায়।
গুরুত্বপূর্ণ নোট: কিছু বাস্তব CPU design ইচ্ছাকৃতভাবে একটা “write-through” বা “forwarding” পথ যোগ করে ঠিক এই পরিস্থিতিতে নতুন মান সরাসরি read port-এ bypass করার জন্য — কিন্তু এটা একটা অতিরিক্ত hardware feature, ডিফল্ট আচরণ না। এই সিদ্ধান্তটা Level 3-এ pipeline hazard আর data forwarding আলোচনায় বিস্তারিত ফিরে আসবে।
3একটা 64×64-bit register file-এ ৪টা read port আর ২টা write port দরকার (একটা modern superscalar CPU-র মতো)।
২টা read port,
১টা write port থেকে port সংখ্যা বাড়ালে hardware খরচ ঠিক কীভাবে বাড়ে —
linearly, নাকি দ্রুততর?
যুক্তি
Read port যোগ করা তুলনামূলক সস্তা এবং প্রায় linear —
প্রতিটা নতুন read port মানে শুধু আরেকটা independent address input + আরেকটা n-to-1 MUX (৬৪টা register-এর মধ্যে থেকে একটা বেছে নেওয়ার জন্য)।
বিদ্যমান register-গুলোর output-এ শুধু আরেকটা তার (wire) জুড়ে দেওয়া লাগে —
register-গুলো নিজেরা বদলায় না।
Write port যোগ করা করা অনেক বেশি ব্যয়বহুল এবং সুপার-লিনিয়ার, কারণ:
১. প্রতিটা register-এর নিজস্ব একাধিক write-enable path দরকার হয় (প্রতিটা write port-এর জন্য আলাদা feedback-MUX ইনপুট), তাই প্রতিটা bit-এর D-input MUX আরও বড় হয়ে যায় (২ write port হলে ৩:১ MUX — একটা “hold” ইনপুট + দুইটা সম্ভাব্য নতুন ডেটা)।
২. Conflict resolution দরকার — যদি ২টা write port একই cycle-এ একই register-এ ভিন্ন মান লিখতে চায়, hardware-কে সিদ্ধান্ত নিতে হয় কোনটা জিতবে (সাধারণত একটা priority/arbitration logic, যা নিজেই আরেকটা combinational block, নিজস্ব propagation delay যোগ করে)।
৩. প্রতিটা decoder-ও প্রতিটা write port-এর জন্য আলাদা লাগে (একটা 6-to-64 decoder প্রতি write port)।
সব মিলিয়ে,
w-টা write port আর r-টা read port-এর register file-এ hardware খরচ মোটামুটি O(n·(r + w²))-এর কাছাকাছি স্কেল করে (n = register সংখ্যা) —
w² টার্মটাই ব্যাখ্যা করে কেন CPU design-এ read port অনেক বেশি (৪, ৬,
এমনকি ৮+) দেখা গেলেও write port সাধারণত ২–৩-এর বেশি খুব কমই যায়।
এই কারণেই Level 11-এর superscalar CPU design-এ register file port সংখ্যা একটা real bottleneck — instruction-level parallelism বাড়াতে চাইলে আরও port লাগে, কিন্তু port বাড়ানোর খরচ দ্রুত prohibitively বেশি হয়ে যায়, তাই architect-রা বিকল্প কৌশল (register banking, clustered register file) খোঁজেন।
4আপনাকে একটা 16-bit “status register” ডিজাইন করতে বলা হয়েছে যেখানে বেশিরভাগ bit স্বাধীনভাবে set/clear হয় (যেমন CPU-র flag register —
carry flag, zero flag, ইত্যাদি,
প্রতিটা ভিন্ন instruction দ্বারা ভিন্নভাবে আপডেট হয়)।
এই লেসনের plain N-bit register ডিজাইন (একটা single write-enable,
সব bit একসাথে) কি এখানে সরাসরি খাটবে? কী বদলাতে হবে?
ডিজাইন
সরাসরি খাটবে না — একটা গুরুত্বপূর্ণ পরিবর্তন লাগবে: per-bit write-enable, একটাই global enable না।
এই লেসনের plain register-এ একটাই EN signal সব bit-এর MUX-এ যায় —
হয় সব bit আপডেট হয়, নয়তো কোনোটাই না।
কিন্তু status register-এ আলাদা আলাদা bit ভিন্ন সময়ে,
ভিন্ন কারণে বদলায় (একটা ADD instruction হয়তো শুধু carry আর zero flag আপডেট করবে,
বাকি flag বদলাবে না)।
সমাধান: প্রতিটা bit-এর জন্য আলাদা EN_i সিগন্যাল রাখুন —
মানে প্রতিটা bit-এর নিজস্ব feedback-MUX, নিজস্ব enable input।
এটা মূলত এই লেসনের “N-টা independent D flip-flop” ধারণাতেই ফিরে যাওয়া —
শুধু এবার প্রতিটার independent enable ব্যবহার করা হচ্ছে,
যেখানে plain register-এ সেগুলো একসাথে বাঁধা ছিল।
Bit i-এর জন্য:
D_i ──► [MUX: EN_i নির্বাচন করে D_new_i নাকি Q_i] ──► FF_i ──► Q_iএখন control logic-এর কাজ হয়ে যায় “কোন bit-এর EN_i কোন instruction-এ high হবে” সেটা ঠিক করা —
এটাই CPU-র control unit-এর কাজের একটা ছোট নমুনা (এই module-এরই পরের অংশ)।
Status/flag register বাস্তবে প্রায় সবসময় এভাবেই ডিজাইন করা হয় —
প্রতিটা flag-এর নিজস্ব update logic,
একটা shared register-এ bit হিসেবে বসানো,
কিন্তু update decision প্রতি bit-এ আলাদা।
5Synchronous reset আর asynchronous reset —
কোনটা power-on মুহূর্তে ব্যবহার করা হয়,
আর কেন অন্যটা সেই কাজে উপযুক্ত না?
স্মরণ
Asynchronous reset power-on-এ ব্যবহার হয়।
কারণ synchronous reset কাজ করার জন্যই একটা চলমান clock দরকার — এটা তো শুধু আরেকটা MUX input, যা কার্যকর হয় শুধুমাত্র clock edge-এ। কিন্তু chip চালু হওয়ার একদম প্রথম মুহূর্তে clock signal নিজেই হয়তো এখনো স্থিতিশীল না — PLL (clock উৎপাদনকারী circuit) lock হতে (স্থির frequency-তে পৌঁছাতে) কিছুটা সময় লাগে। যদি reset sync হয় আর clock তখনো ঠিকভাবে চলছে না, register কখনোই একটা নির্দিষ্ট, পরিচিত অবস্থায় পৌঁছাবে না।
Asynchronous reset clock নির্বিশেষে কাজ করে — power সাপ্লাই স্থিতিশীল হওয়ার সাথে সাথেই (একটা power-on-reset circuit দ্বারা trigger হয়ে) সব flip-flop জোর করে একটা পরিচিত অবস্থায় (সাধারণত সব-শূন্য) নিয়ে যায়, clock শুরু হওয়ার আগেই। এরপর clock স্থিতিশীল হলে, reset de-assert হয় (সাবধানে synchronized ভাবে, এই লেসনের “hood” অংশে আলোচিত recovery/removal সমস্যা এড়াতে), আর স্বাভাবিক synchronous operation শুরু হয়।
6একটা 32-entry register file-এর read port MUX-tree দিয়ে বানানো হয়েছে (log₂32 = 5 স্তর)।
যদি একই register file-কে ৬৪-entry-তে বাড়ানো হয়,
MUX-tree-র depth কত বাড়বে? আর এই একই পরিবর্তন tri-state bus-ভিত্তিক ডিজাইনে কী প্রভাব ফেলবে?
ডিজাইন
log₂32 = 5 স্তর)।
যদি একই register file-কে ৬৪-entry-তে বাড়ানো হয়,
MUX-tree-র depth কত বাড়বে? আর এই একই পরিবর্তন tri-state bus-ভিত্তিক ডিজাইনে কী প্রভাব ফেলবে?MUX-tree: log₂64 = 6 স্তর —
মাত্র একটা বাড়তি স্তর,
কারণ MUX-tree-র depth register সংখ্যার logarithm-এর সমানুপাতিক।
৩২ থেকে ৬৪-তে register দ্বিগুণ হলেও depth মাত্র ১ বাড়ে (৫ থেকে ৬) —
এটাই logarithmic scaling-এর ব্যবহারিক সুবিধা (গণিত module-এর asymptotic notation লেসন মনে করুন)।
Tri-state bus: এখানে “স্তর” ধারণাটা প্রযোজ্য না —
বাস্তবে bus-এর delay register সংখ্যার সাথে সরাসরি log-scale করে না,
বরং bus wire-এ যুক্ত মোট capacitance-এর সাথে সম্পর্কিত,
যা প্রতিটা বাড়তি tri-state buffer-এর সাথে (এমনকি সেটা inactive/Z অবস্থায় থাকলেও) কিছুটা বাড়ে।
৩২ থেকে ৬৪-এ register দ্বিগুণ হলে bus-এর capacitance-ও মোটামুটি দ্বিগুণ হয়,
যা delay-কে (RC time constant অনুযায়ী) প্রায় linearly বাড়ায় —
MUX-tree-র logarithmic scaling-এর চেয়ে খারাপ।
ব্যবহারিক সিদ্ধান্ত: এই কারণেই খুব বড় register file (যেমন GPU-র হাজার-entry register file) সাধারণত tri-state bus এড়িয়ে MUX-tree বা banked/segmented ডিজাইন ব্যবহার করে —
logarithmic scaling বড় n-এ সবসময় জেতে,
ঠিক যেমন asymptotic notation লেসনে O(log n) বনাম O(n)-এর পার্থক্য ছোট n-এ অদৃশ্য কিন্তু বড় n-এ নির্ণায়ক হয়ে ওঠে।
এরপর কী
এরপর কী
আমরা এখন জানি কীভাবে N-bit মান store করতে হয়, কীভাবে নির্বাচিতভাবে আপডেট করতে হয় (write-enable), আর কীভাবে অনেকগুলো register-কে address দিয়ে সংগঠিত করতে হয় (register file)।
কিন্তু পুরো সময় আমরা একটা সরলীকরণে বিশ্বাস রেখেছি — “clock edge” একটা পরিষ্কার, instantaneous মুহূর্ত, যেখানে সব flip-flop একসাথে, নিখুঁতভাবে update হয়। বাস্তবে এই সরলীকরণটা মিথ্যা — সিগন্যাল ভ্রমণ করতে সময় নেয়, gate output স্থির হতে সময় নেয়, আর “একসাথে” বলে সত্যিকারের কিছু নেই একটা physical circuit-এ।
পরের লেসনে (clock ও timing) আমরা এই সরলীকরণটা ভাঙব — propagation delay, setup time, hold time, metastability, আর একটা circuit ঠিক কত দ্রুত clock করা যায় তার গাণিতিক সীমা। এটাই এই module-এর সবচেয়ে “physical” লেসন — এখানেই ডিজিটাল লজিকের নিখুঁত, বিমূর্ত জগৎ সরাসরি electron আর wire-এর বাস্তবতার মুখোমুখি হয়।
আরও পড়ুন
- Computer Organization and Design: RISC-V Edition — David A. Patterson, John L. Hennessy · Register file-কে CPU datapath-এর অংশ হিসেবে দেখানো প্রামাণ্য উৎস
- Digital Design and Computer Architecture, RISC-V Edition — Sarah L. Harris, David Money Harris · Enable, reset, আর register file-এর gate-level বাস্তবায়ন বিস্তারিতভাবে আলোচিত