HDL ও Verilog পরিচিতি — Schematic যখন কোড হয়ে যায়
Introduction to HDL and Verilog
Billion-transistor চিপ হাতে schematic এঁকে ডিজাইন করা অসম্ভব — তাই hardware বর্ণনা করা হয় কোডে, একটা Hardware Description Language-এ। আজ আমরা দেখব structural বনাম behavioral Verilog, wire বনাম reg-এর আসল অর্থ, sequential circuit ও FSM কোড করা, আর কীভাবে একই কোড simulate (verify) এবং synthesize (বাস্তব gate-এ রূপান্তর) — দুইটা সম্পূর্ণ ভিন্ন কাজে ব্যবহৃত হয়।
আগে এটা বুঝি
গত লেসনের শেষ বাক্যটা আবার পড়ুন: বাস্তব CPU-তে কোটি কোটি transistor থাকে, আর schematic হাতে আঁকা সেখানে অসম্ভব।
সংখ্যাটা একটু থামিয়ে দেখা যাক, কারণ “কোটি কোটি” শব্দটা বলা সহজ, বোঝা কঠিন। Apple M2 Ultra চিপে আনুমানিক ১৩৪ বিলিয়ন transistor আছে। NVIDIA-র H100 GPU-তে প্রায় ৮০ বিলিয়ন। এই মডিউলের প্রথম লেসনে আমরা যে ৮-বিট toy ALU হাতে বানিয়েছিলাম, সেটাতে হয়তো কয়েকশো transistor লেগেছিল। ১৩৪ বিলিয়নকে কয়েকশো দিয়ে ভাগ করুন — পার্থক্যটা এতই বিশাল যে এটা আর “বেশি কাজ” না, এটা একটা সম্পূর্ণ ভিন্ন সমস্যা।
এই বৃদ্ধিটা কয়েক দশকে ঘটেছে, ধাপে ধাপে — লেসন ১-এর Moore’s Law আলোচনার সংখ্যাগুলো এখন সরাসরি দেখা যাক। প্রথম বাণিজ্যিক মাইক্রোপ্রসেসর, Intel 4004 (১৯৭১), ছিল মাত্র ২,৩০০ transistor — একজন প্রকৌশলী চাইলে হাতে schematic এঁকেই সেটা ডিজাইন করতে পারতেন, আর বাস্তবে অনেকটা তা-ই করা হয়েছিল। Intel Pentium (১৯৯৩) পৌঁছাল ৩১ লক্ষ transistor-এ — হাতে আঁকা তখনই অব্যবহারিক হয়ে উঠছিল। আজকের ১৩৪ বিলিয়ন সেই ১৯৭১-এর ২,৩০০-এর তুলনায় প্রায় ৫ কোটি ৮০ লক্ষ গুণ বেশি। এই পুরো বৃদ্ধিটাই সম্ভব হয়েছে শুধু transistor ছোট হওয়ার কারণে না — ডিজাইন করার পদ্ধতি বদলানোর কারণেও। HDL ছাড়া এই স্কেল-আপ কল্পনাই করা যেত না।
গত লেসনের ৫-state, ৫-signal control unit-টা আমরা হাতে schematic এঁকে ডিজাইন করেছিলাম — সম্ভব হয়েছিল, কারণ সেটা ছোট ছিল। কিন্তু একটা modern CPU-তে হাজার হাজার instruction, out-of-order execution, multiple pipeline stage, branch predictor, cache controller — প্রতিটার নিজস্ব state machine, নিজস্ব datapath। কেউ এসব হাতে তার টেনে টেনে আঁকে না। তাহলে ডিজাইন হয় কীভাবে?
উত্তরটা এই মডিউলের বাকি প্রতিটা লেসনের মতোই একটা পরিচিত প্যাটার্ন অনুসরণ করে: abstraction-এর আরেকটা স্তর যোগ করা। ঠিক যেমন Boolean algebra গেট-লেভেল বিস্তারিত থেকে চিন্তাকে মুক্ত করেছিল (লেসন ৪), ঠিক যেমন register-transfer level চিন্তা করলে প্রতিটা flip-flop আলাদা করে ভাবতে হয় না (লেসন ১০) — একইভাবে engineer এখন circuit-টা বর্ণনা করেন একটা ভাষায়, hardware-এর প্রতিটা তার নিজে টানার বদলে। সেই ভাষার নাম Hardware Description Language, সংক্ষেপে HDL — আর আজকের ইন্ডাস্ট্রি-স্ট্যান্ডার্ড HDL-এর নাম Verilog (আর তার আধুনিক superset, SystemVerilog)।
এখানে একটা তুলনা সাহায্য করবে, কারণ এটা আপনার আগে থেকেই পরিচিত একটা জগতের সমান্তরাল। Assembly-তে প্রতিটা instruction হাতে লেখা — ঠিক কোন register-এ কী থাকবে, ঠিক কোন cycle-এ কী ঘটবে, প্রোগ্রামার নিজে ঠিক করে দেন। একটা high-level ভাষায় (যেমন Python বা C) প্রোগ্রামার শুধু কী চাই সেটা বলেন — total = a + b — আর compiler ঠিক করে কোন register, কোন instruction। HDL-এও ঠিক এই একই বিভাজন আছে: structural style-এ আপনি বলেন ঠিক কোন gate, কীভাবে তারে জোড়া — এটা “hardware-এর assembly”। Behavioral style-এ আপনি বলেন আচরণ কী হওয়া উচিত — sum = a ^ b ^ cin — আর একটা synthesis tool ঠিক করে কোন গেট, কীভাবে তারে জোড়া। এটা “hardware-এর high-level ভাষা”।
আজকের লেসনে আমরা দেখব ঠিক কীভাবে গত লেসনের datapath আর control table প্রায় যান্ত্রিকভাবে Verilog কোডে রূপান্তরিত হয়ে যায় — আর কীভাবে একটা tool সেই কোডকে হাতে-আঁকা schematic-এর সমতুল্য একটা গেট-লেভেল circuit-এ পরিণত করে, কোনো নতুন “যাদু” ছাড়াই। এটাই এই মডিউলের শেষ ধাপ — আর এখান থেকেই Level 3-এর দরজা খুলবে।
মূল ধারণা
Verilog-এর সবচেয়ে ছোট সম্পূর্ণ কাঠামো
একটা Verilog module হলো একটা hardware block-এর সংজ্ঞা — ইনপুট, আউটপুট, আর ভেতরের logic। এটা অনেকটা একটা function-এর মতো, কিন্তু এটা কোনো একটা মুহূর্তে “চলে” না — এটা একটা সার্কিট-এর বর্ণনা, যেটা সবসময় বিদ্যমান, সবসময় সক্রিয়।
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodulemodule and_gate ( ... ); ... endmodule— একটা নামযুক্ত hardware block, ঠিক গত লেসনের প্রতিটা schematic box-এর মতো।input/output— port, schematic-এ box-এর গায়ে বের হওয়া তারের সমতুল্য।wire— একটা তার। কোনো মান “সংরক্ষণ” করে না, শুধু একটা signal বহন করে, ক্রমাগত।assign y = a & b;— একটা continuous assignment। এর মানে “যখনইaবাbবদলাবে,yতাৎক্ষণিকভাবে পুনর্গণনা হবে” — ঠিক physical তারের মতো, একটা লজিক gate-এর input বদলালে output সাথে সাথে বদলায় (propagation delay বাদে, যেটা এই abstraction level-এ উপেক্ষা করা হয়)।
এটাই — একটা সম্পূর্ণ, বৈধ, synthesizable Verilog module। লেসন ২-এর CMOS AND gate-টা এখন এই ৬ লাইনে বর্ণিত।
wire বনাম reg — এই লেসনের সবচেয়ে গুরুত্বপূর্ণ পার্থক্য
Verilog-এ দুই ধরনের সিগন্যাল আছে, আর নামগুলো নিজেই একটা ফাঁদ — একটু পরেই দেখব কেন।
wire — একটা তার। শুধুমাত্র একটা continuous assignment (assign) দিয়ে, বা একটা gate/module instantiation-এর output হিসেবে driven হতে পারে। কখনো কোনো মান “মনে রাখে না” — এটা সবসময় তার driver-এর বর্তমান মান প্রতিফলিত করে, ঠিক এই মুহূর্তে।
reg — একটা variable, যেটার মান একটা procedural block (always বা initial)-এর ভেতরে assign করা হয়। এই assignment-টাই মূল কথা — reg টাইপের কোনো সিগন্যালে সরাসরি assign দিয়ে মান বসানো যায় না, শুধু একটা always/initial ব্লকের ভেতরেই।
সহজ নিয়ম মনে রাখার জন্য:
wire | reg | |
|---|---|---|
| কোথায় assign হয় | assign স্টেটমেন্ট, বা gate/module output | always/initial ব্লকের ভেতর |
| আচরণ | সবসময় driver-এর মান প্রতিফলিত করে | procedural logic যা assign করে তাই ধরে রাখে |
| Hardware অর্থ | সবসময় শুধু একটা তার/net | context-নির্ভর — নিচে দেখুন |
Bit-vector, concatenation, replication — কয়েকটা ছোট কিন্তু জরুরি সিনট্যাক্স
এই লেসনের বাকি অংশে বারবার আসবে এমন তিনটা ছোট syntax এখনই পরিচয় করিয়ে দেওয়া ভালো, কারণ পরের প্রতিটা কোড উদাহরণেই এগুলো লাগবে।
Bit-vector — একাধিক bit-এর একটা port/wire ঘোষণা করতে [MSB:LSB] নোটেশন:
input wire [15:0] data; // ১৬-বিট, bit 15 (সবচেয়ে বাম) থেকে bit 0 (সবচেয়ে ডান)
wire hi_byte = data[15:8]; // উপরের ৮ বিট — slicing
wire lsb = data[0]; // একটামাত্র বিটলেসন ১-এর “একটা তার একটা bit বহন করে” ধারণাটার সরাসরি সম্প্রসারণ — [15:0] মানে আসলে ১৬টা আলাদা তারের একটা bundle, একসাথে নাম দেওয়া।
Concatenation — curly-brace দিয়ে একাধিক signal পাশাপাশি জোড়া দিয়ে একটা বড় vector বানানো:
wire [7:0] byte_val = {hi_nibble, lo_nibble}; // দুইটা 4-বিট জোড়া → ৮-বিটএই লেসনের testbench-গুলোতে ঠিক এই একই সিনট্যাক্স উল্টো দিকে ব্যবহার করা হয়েছে — একটা ৩-বিট মানকে ভেঙে তিনটা আলাদা ১-বিট সিগন্যালে (উদাহরণস্বরূপ a, b, cin-এ) বসিয়ে দেওয়া, একই curly-brace নোটেশন দিয়ে।
Replication — একই signal N বার পাশাপাশি বসানো, নেস্টেড curly-brace দিয়ে (N বার, প্রতিটা কপি ভেতরের braces-এ) — register module-এর reset লাইনে যেমন দেখেছি: WIDTH বার 1'b0 পাশাপাশি বসিয়ে একটা পূর্ণ WIDTH-বিট শূন্য।
এই তিনটাই ভাষার অংশ, কোনো গেট বা memory না — কম্পাইল-টাইমে (বা synthesis-টাইমে) সমাধান হয়ে যায়, ঠিক যেমন Python-এ list[2:5] slicing কোনো “হার্ডওয়্যার” তৈরি করে না, শুধু ডেটার একটা অংশ নির্দেশ করে।
একটু ইতিহাস — Verilog কোথা থেকে এলো
Verilog কোনো কমিটির প্রথম থেকে ডিজাইন করা ভাষা না — এটা শুরু হয়েছিল একটা কোম্পানির অভ্যন্তরীণ, proprietary simulator ভাষা হিসেবে। ১৯৮৪ সালে Phil Moorby আর Prabhu Goel, Gateway Design Automation-এ, একটা দ্রুত gate-level simulator বানানোর জন্য এই ভাষাটা তৈরি করেন — লক্ষ্য ছিল শুধু simulation, কোনো synthesis ধারণা তখনও ছিল না। ১৯৮৯-৯০ সালে Cadence Design Systems Gateway অধিগ্রহণ করে, আর ১৯৯০ সালে Cadence সিদ্ধান্ত নেয় Verilog-এর ভাষাগত সংজ্ঞা পাবলিক ডোমেইনে ছেড়ে দেওয়ার — একটা অস্বাভাবিক, কিন্তু ভাষাটাকে ইন্ডাস্ট্রি-স্ট্যান্ডার্ড বানানোর পথ খুলে দেওয়া সিদ্ধান্ত। ১৯৯৫ সালে IEEE 1364 হিসেবে আনুষ্ঠানিকভাবে স্ট্যান্ডার্ডাইজড হয়।
সমান্তরালে আরেকটা HDL ছিল — VHDL (VHSIC Hardware Description Language), যেটা ১৯৮০-র দশকে US Department of Defense-এর অর্থায়নে তৈরি, মূলত hardware documentation-এর জন্য (পরে simulation/synthesis সক্ষমতা যোগ হয়), IEEE 1076 হিসেবে ১৯৮৭-তে স্ট্যান্ডার্ডাইজড।
| Verilog | VHDL | |
|---|---|---|
| উৎপত্তি | ১৯৮৪, কোম্পানি (Gateway), simulator-কেন্দ্রিক | ১৯৮০-র দশক, US DoD, documentation-কেন্দ্রিক |
| সিনট্যাক্স স্টাইল | C-এর মতো — সংক্ষিপ্ত, কম কীওয়ার্ড | Ada-এর মতো — verbose, strongly-typed |
| Typing | দুর্বল (loosely typed) — এই লেসনের সবকিছু সহজে মিশ্রিত করা যায় | কড়া (strongly typed) — টাইপ মিসম্যাচ কম্পাইল-টাইমেই ধরা পড়ে |
| আঞ্চলিক প্রচলন | উত্তর আমেরিকা/এশিয়ার commercial ASIC, FPGA শিল্পে বেশি | ইউরোপীয় defense/aerospace, কিছু academic জগতে বেশি |
| এই কোর্সে কেন Verilog | সংক্ষিপ্ত সিনট্যাক্স, সবচেয়ে বেশি free tooling (Icarus, Yosys), সবচেয়ে বড় community/সম্পদ |
দুটোই আজও সমান্তরালে ব্যবহৃত হয় — কোনোটাই “মৃত” না। কিন্তু আধুনিক commercial ASIC/FPGA ডিজাইনের সংখ্যাগরিষ্ঠ অংশ এখন Verilog-এর উত্তরসূরি SystemVerilog-এ লেখা (“realworld” অংশে আরও বিস্তারিত)।
Structural Verilog — schematic-টাই কোড
Structural style-এ আপনি ঠিক বলেন কোন primitive gate, কীভাবে তারে জোড়া — schematic-এর প্রতিটা box আর তার literally একটা লাইনে অনুবাদ হয়। Verilog-এ কিছু গেট primitive (built-in, gate-level) হিসেবে ভাষাতেই আছে: and, or, not, xor, nand, nor, xnor।
লেসন ৫-এর full adder-এর কথা মনে করুন — Sum = A ⊕ B ⊕ Cin, Cout = AB + Cin·(A⊕B) (majority function), মোট ৫টা গেট: ২টা XOR, ২টা AND, ১টা OR। ঠিক এই schematic-টাই এবার কোডে:
module full_adder_structural (
input wire a, b, cin,
output wire sum, cout
);
wire p; // p = a XOR b — লেসন ৫-এর "propagate" সিগন্যাল
wire g1, g2;
xor (p, a, b); // p = a ^ b
xor (sum, p, cin); // sum = p ^ cin = a ^ b ^ cin
and (g1, a, b); // g1 = a AND b
and (g2, p, cin); // g2 = p AND cin
or (cout, g1, g2); // cout = g1 OR g2
endmoduleলক্ষ্য করুন প্রতিটা লাইন — xor (p, a, b); — লেসন ৫-এর schematic-এর একটা gate-এর একটা সরাসরি অনুবাদ। প্রথম argument সবসময় output, বাকিগুলো input। p, g1, g2 — এগুলো internal wire, schematic-এর ভেতরের তারের সমতুল্য, যেগুলো একটা গেট-এর output আরেকটা গেটের input-এ পৌঁছে দেয়।
এখানে কোনো reg নেই — সবই wire, কারণ প্রতিটা signal সরাসরি কোনো না কোনো gate primitive-এর output, কোনো procedural block নেই।
Behavioral Verilog — কী চাই, কীভাবে না
Behavioral style-এ আপনি Boolean expression-টাই সরাসরি লেখেন — কোন গেট ব্যবহার হবে, কীভাবে তারে জোড়া হবে, সেটা synthesis tool-এর সিদ্ধান্তে ছেড়ে দেওয়া হয়।
module full_adder_behavioral (
input wire a, b, cin,
output wire sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
endmoduleদুই লাইন। কোনো internal wire নেই, কোনো gate primitive নেই — শুধু Boolean algebra, ঠিক লেসন ৪-এর নোটেশনে যেভাবে আমরা এই সমীকরণ দুটো লিখতাম।
কখন কোনটা — একটা বাস্তব trade-off
| Structural | Behavioral | |
|---|---|---|
| আপনি কী লেখেন | কোন gate, কীভাবে তারে জোড়া | কী আচরণ চাই (Boolean expr, RTL) |
| Synthesis-এর কাজ | প্রায় কিছুই না — netlist প্রায় ready-made | expression থেকে গেট derive করা |
| Readability বড় ডিজাইনে | কমে যায় দ্রুত — হাজার গেট হাতে লেখা কঠিন | ভালো থাকে — উচ্চস্তরের intent স্পষ্ট |
| Full control | সর্বোচ্চ — প্রতিটা গেট আপনার সিদ্ধান্তে | কম — tool-এর optimization-এর উপর নির্ভরশীল |
| আধুনিক ব্যবহার | primitive gate-instantiation বিরল; module-level structural (বড় ব্লক জোড়া) সাধারণ | RTL design-এর মূল স্টাইল, ৯৯% আধুনিক কোড |
বাস্তবে আধুনিক Verilog কোড প্রায় সবসময় উভয়ের মিশ্রণ: বড় ব্লক (ALU, register file, control unit) behavioral style-এ লেখা হয় ভেতরে, কিন্তু সেই ব্লকগুলোকে একে অপরের সাথে জোড়া হয় structural style-এ — একটা top-level module যেখানে alu u1 (...), register_file u2 (...) — এই instantiation লাইনগুলো ঠিক schematic-এর box-connecting-wire ধারণাটাই ব্যবহার করে, শুধু primitive gate না, বড় sub-module দিয়ে। আমরা “build” অংশে এই মিশ্র স্টাইল ব্যবহার করব।
এই অংশের সারসংক্ষেপ, এক নজরে
| ধারণা | মূল কথা |
|---|---|
module/endmodule | একটা hardware block-এর সীমানা |
wire | তার — শুধু assign বা gate-output দিয়ে driven, কোনো memory না |
reg | procedural block-এ assign করা variable — hardware অর্থ context-নির্ভর |
| Structural | কোন gate, কীভাবে তারে জোড়া — schematic-এর সরাসরি অনুবাদ |
| Behavioral | কী আচরণ চাই — synthesis tool গেট বেছে নেয় |
assign | continuous — driver বদলালেই তাৎক্ষণিক পুনর্গণনা |
এই ছয়টা লাইনই এই সম্পূর্ণ “concept” অংশের নির্যাস — বাকি সব উদাহরণ, টেবিল, আর Callout শুধু এই ছয়টা ধারণাকে বিভিন্ন কোণ থেকে দেখানোর চেষ্টা।
ভেতরে কী ঘটছে
Simulation বনাম Synthesis — একই ফাইল, দুইটা সম্পূর্ণ ভিন্ন মেশিন পড়ে
এই বিভাজনটা না বুঝলে বাকি সবকিছু বিভ্রান্তিকর মনে হবে, তাই প্রথমেই স্পষ্ট করা যাক।
একটা Verilog ফাইল লেখার পর সেটা দুইটা সম্পূর্ণ ভিন্ন কাজে ব্যবহৃত হতে পারে:
Simulation — একটা software প্রোগ্রাম (simulator, যেমন Icarus Verilog) আপনার Verilog module-কে একটা event-driven সফটওয়্যার model হিসেবে চালায়। এটা আপনার সাধারণ কম্পিউটারে চলে, কোনো real hardware লাগে না। Input বদলালে, simulator গণনা করে কোন wire/reg-এর মান কীভাবে বদলাবে, আর সময় (simulated সময়, ন্যানোসেকেন্ডে) advance করে। এটা ঠিক একটা unit test চালানোর মতো — logic সঠিক কি না যাচাই করা, কোনো physical hardware না বানিয়েই।
Synthesis — একটা সম্পূর্ণ ভিন্ন tool (যেমন Yosys, বা commercial tool Synopsys Design Compiler) আপনার Verilog কোড পড়ে, বোঝে, আর একটা gate-level netlist তৈরি করে — একটা ফাইল যেখানে শুধু primitive gate/flip-flop instance আর তাদের সংযোগ আছে, কোনো always ব্লক নেই, কোনো উচ্চস্তরের abstraction নেই। এই netlist-টাই একটা FPGA-তে বসানো যায়, বা একটা ASIC-এর জন্য physical layout-এ রূপান্তরিত হয় (place & route)। এটা “compile” করার সমতুল্য — কিন্তু আউটপুট একটা executable না, একটা circuit।
- always @(posedge clk) q <= d;Verilog source — একটা আচরণ বর্ণনা করছে, এখনও hardware না
- Synthesis tool (Yosys / Vivado / ...)কোড parse করে, RTL semantics বোঝে, একটা technology-independent internal graph বানায়
- Technology mappingসেই graph-কে standard cell library-র সাথে মেলায় — যেমন DFF_X1, AND2_X1, INV_X1
- Gate-level netlistআরেকটা .v ফাইল — কিন্তু এবার শুধু primitive gate/flip-flop instance, কোনো always block নেই
- Place & routeপ্রতিটা cell-কে chip-এর একটা নির্দিষ্ট (x, y) অবস্থানে বসানো, তারপর wire route করা
- Standard cell = CMOS gate groupলেসন ২-৩ — প্রতিটা DFF cell আসলে কয়েক ডজন transistor দিয়ে বানানো, ঠিক এই মডিউলের শুরুর মতো
- সিলিকনে খোদাইFabrication — মাস্ক, লিথোগ্রাফি — এই মডিউলের সম্পূর্ণ বাইরে, কিন্তু এখানেই আসল "হার্ডওয়্যার" জন্ম নেয়
Verilog কোডের প্রতিটা লাইন এই দুইটা মেশিনের যেকোনো একটা (বা দুটোই) পড়তে পারে — কিন্তু দুটো মেশিনের নিয়ম এক না। এটাই পরের কয়েকটা উপবিভাগের মূল বিষয়।
Testbench — hardware-এর unit test
আপনার লেখা module-টা (এখন থেকে বলা হবে DUT — Device Under Test) নিজে থেকে চলে না, নিজে থেকে input দেয় না। এটাকে test করতে লাগে আরেকটা Verilog module, যেটার কাজ শুধু: DUT-কে instantiate করা, তাতে test input feed করা, আর output সঠিক কি না check করা। একে বলে testbench।
module tb_and_gate;
reg a, b; // testbench নিজেই input drive করে — তাই এখানে reg
wire y;
and_gate dut (.a(a), .b(b), .y(y)); // DUT instantiate
initial begin
a = 0; b = 0; #10;
a = 0; b = 1; #10;
a = 1; b = 0; #10;
a = 1; b = 1; #10;
end
endmoduleলক্ষ্য করুন — testbench-এ a, b কে reg ঘোষণা করা হয়েছে, যদিও এগুলো কোনো flip-flop না, বরং আমরা তাদের মান ম্যানুয়ালি বসাচ্ছি একটা initial ব্লকে (initial মানে “simulation শুরুতে একবার চালাও” — একটা always ব্লকের মতোই procedural, তাই ভেতরের সিগন্যাল reg হতে হয়)। এটাই সেই “reg নাম বিভ্রান্তিকর” সমস্যার আরেকটা প্রকাশ — এই reg-দুটো কখনোই কোনো flip-flop হবে না, কারণ testbench কখনো synthesize করা হয় না।
#10 — একটা delay statement: “১০ (simulation) সময় একক অপেক্ষা করো”। এটা একটা বিশুদ্ধ simulation-only construct — বাস্তব hardware-এ “১০ ন্যানোসেকেন্ড অপেক্ষা করো” বলে কোনো gate নেই, সময় নিজে থেকেই চলে। তাই #10 synthesize করা যায় না, আর করার দরকারও নেই — testbench কখনোই চিপে বসে না, এটা শুধু simulation-এর সময়ে PC-তে চলে, verify করার জন্য।
Delta cycle — কেন testbench-এ ইনপুট বদলানোর পর একটু অপেক্ষা করা নিরাপদ
এখানে একটা সূক্ষ্ম কিন্তু বাস্তবিক সমস্যা আছে, যেটা “check” অংশের একটা প্রশ্নে ফিরে আসবে। ধরুন একটা testbench-এ input বদলানো হলো, আর সাথে সাথেই (কোনো # delay ছাড়া) সেই input-নির্ভর output প্রিন্ট করা হলো। সমস্যা হলো — simulator নিজে input assign করা আর তার উপর নির্ভরশীল সব assign/gate পুনর্গণনা করা, এই দুইটা কাজ একসাথে, একই মুহূর্তে করে না, বরং একটা internal queue-এর মাধ্যমে ধাপে ধাপে করে। একই simulation-time-এর মধ্যেই এরকম বহু “ধাপ” ঘটতে পারে, যাদের বলে delta cycle — বাস্তব সময় শূন্য এগোয়, কিন্তু simulator-এর ভেতরের event-queue processing-এ একাধিক “রাউন্ড” লাগে যতক্ষণ না সবকিছু স্থির (settle) হয়।
আপনি যদি input assign করার ঠিক পরের মুহূর্তেই (একই delta-তে) output পড়েন, সেই output হয়তো এখনও পুরনো (stale) মান বহন করছে — সব দিক থেকে সেটেল হওয়ার সুযোগ পায়নি। এই কারণেই testbench-এ ইনপুট বদলানোর পর একটা ছোট delay (#1 বা তার বেশি) রাখা একটা সাধারণ, নিরাপদ অভ্যাস — এটা simulator-কে সব delta cycle শেষ করে সত্যিকারের স্থির মানে পৌঁছানোর সময় দেয়, তারপরই output পড়া/প্রিন্ট করা হয়। লক্ষ্য করুন এটা বাস্তব gate propagation delay-র সাথে সম্পর্কিত না — সেই পার্থক্যটা “misconception” অংশে আরও বিস্তারিত আসবে — এটা শুধু simulator-এর নিজের বইখাতা গোছানোর একটা কৌশল।
Synthesizable subset — Verilog-এর মধ্যে আরেকটা ছোট ভাষা
এখান থেকেই একটা গুরুত্বপূর্ণ সত্য বেরিয়ে আসে: Verilog ভাষার প্রতিটা feature synthesize করা যায় না। #10 (delay), $display (simulation console-এ print), $finish (simulation থামানো), initial ব্লক দিয়ে test stimulus তৈরি করা — এগুলো সবই simulation-only। যে subset synthesize করা যায় (এবং করা উচিত), তাকে বলে synthesizable subset — মূলত module/assign/always @(posedge ...)/always @(*)/case/if -এর একটা সুশৃঙ্খল ব্যবহার।
Blocking (=) বনাম Non-blocking (<=) — একটা ছোট চিহ্ন, বিশাল পার্থক্য
Verilog-এ দুই ধরনের assignment operator আছে procedural block-এর ভেতরে — আর দেখতে প্রায় একই রকম হলেও, semantics সম্পূর্ণ ভিন্ন।
Blocking (=) — স্টেটমেন্টগুলো ঠিক লেখা ক্রম অনুযায়ী, একটার পর একটা, সাথে সাথে কার্যকর হয় (একটা conventional programming language-এর মতোই আচরণ করে) — পরের স্টেটমেন্ট শুরুর আগে আগেরটার effect ইতিমধ্যেই ধরা পড়ে।
Non-blocking (<=) — সবগুলো right-hand-side expression একসাথে বর্তমান মান দিয়ে গণনা হয়, কিন্তু সেই মানগুলো left-hand-side-এ একসাথে, block-এর শেষে বসে — একটা সময়ের “snapshot” নিয়ে সবাইকে একসাথে আপডেট করা।
নিয়ম, ব্যতিক্রমহীনভাবে:
| Always block ধরন | কোন operator | কারণ |
|---|---|---|
Combinational (always @(*)) | Blocking (=) | মান সাথে সাথে propagate হওয়া উচিত, ঠিক real combinational gate-এর মতো |
Sequential (always @(posedge clk)) | Non-blocking (<=) | সব flip-flop-কে একই clock edge-এ, পুরনো মান থেকে, একসাথে update হতে হবে |
কেন সিকোয়েন্সিয়াল ব্লকে non-blocking জরুরি সেটা “check” অংশের একটা প্রশ্নে আমরা একটা কংক্রিট bug দিয়ে দেখাব — একটা দুই-ধাপের shift-এ যদি ভুলে blocking ব্যবহার হয়, মান এক clock-এই দুই ধাপ শিফট হয়ে যায়, যেটা বাস্তব hardware-এ কখনো ঘটতে পারে না।
Latch — যখন ভুলে একটা অনাকাঙ্ক্ষিত memory তৈরি হয়ে যায়
একটা combinational always @(*) ব্লকের নিয়ম: প্রতিটা সম্ভাব্য input combination-এর জন্য output-কে একটা মান assign করতেই হবে, নাহলে tool ধরে নেয় “বাকি ক্ষেত্রে আগের মান ধরে রাখো” — আর সেই “ধরে রাখা” বাস্তবায়ন করতে হলে একটা memory element লাগবেই, নাম latch (একটা level-sensitive memory, flip-flop-এর মতো edge-triggered না)।
// ❌ ভুল — else শাখা নেই
always @(*) begin
if (sel)
y = a;
// sel == 0 হলে y-এর কী হবে? কোনো assignment নেই!
endsel == 0 হলে এই কোড অনুযায়ী y-এর “আগের মান” ধরে রাখতে হবে — আর সেটা করার জন্য synthesis tool একটা latch বসিয়ে দেয়, যদিও আপনি হয়তো কখনো কোনো memory চাননি। এটা একটা real, খুবই সাধারণ bug ক্লাস — কোড দেখতে সরল combinational লজিকের মতো, কিন্তু synthesize হয় sequential (memory-সহ) circuit-এ।
// ✓ ঠিক — সব শাখায় assignment
always @(*) begin
if (sel)
y = a;
else
y = b;
endএখন y প্রতিটা সম্ভাব্য sel-এর জন্য explicit ভাবে assign হচ্ছে — কোনো “আগের মান ধরে রাখা” দরকার নেই, তাই pure combinational logic (একটা MUX, লেসন ৭) synthesize হবে, কোনো latch না।
সংক্ষিপ্ত চেকলিস্ট — সুশৃঙ্খল, synthesizable RTL
এই “hood” অংশের চারটা বিষয় (simulation/synthesis, testbench, blocking/non-blocking, latch inference) একসাথে করলে একটা ব্যবহারিক checklist দাঁড়ায়, যেটা প্রতিটা ইন্ডাস্ট্রি RTL কোডিং গাইডলাইনেই কোনো না কোনো রূপে থাকে:
| নিয়ম | কেন |
|---|---|
Combinational (always @(*))-এ সবসময় blocking (=) | মান সাথে সাথে propagate হওয়া উচিত, real gate-এর মতো |
Sequential (always @(posedge clk))-এ সবসময় non-blocking (<=) | সব flip-flop পুরনো মান থেকে একসাথে update হওয়া উচিত |
| Combinational ব্লকে প্রতিটা branch-এ, প্রতিটা output assign করা | নাহলে latch infer হবে — অনাকাঙ্ক্ষিত memory |
case-এ সবসময় একটা default শাখা রাখা | অসম্পূর্ণ case-ও latch inference-এর একটা সাধারণ উৎস |
একই সিগন্যাল দুইটা ভিন্ন always ব্লকে assign না করা | কে “জিতবে” তা অনির্ধারিত (multiple driver error) |
Delay (#), $display, initial শুধু testbench-এ, কখনো DUT-এ না | DUT সবসময় synthesizable subset-এর মধ্যে থাকা উচিত |
এই ছয়টা নিয়ম মেনে চললে simulation আর synthesis-এর ফলাফল প্রায় সবসময় মিলে যায় — আর যেখানে মেলে না, সেটাই সবচেয়ে ব্যয়বহুল ধরনের bug, কারণ সেটা শুধু আসল hardware-এ ধরা পড়ে, ততক্ষণে অনেক দেরি হয়ে গেছে।
উদাহরণ
D flip-flop — লেসন ৯-এর circuit, এক লাইনে
লেসন ৯-এ আমরা cross-coupled feedback দিয়ে হাতে-কলমে D flip-flop বানিয়েছিলাম — কয়েকটা গেট, একটা master-slave latch pair। এখন সেই আচরণটাই:
module d_flip_flop (
input wire clk,
input wire d,
output reg q
);
always @(posedge clk)
q <= d;
endmodulealways @(posedge clk) — “শুধুমাত্র clk-এর rising edge-এ (0→1 transition) কিছু ঘটে, বাকি সময় কিছুই না” — এটাই লেসন ৯-এর edge-triggered আচরণের সরাসরি বর্ণনা। q এখানে reg — কারণ এটা একটা procedural block-এর ভেতরে assign হচ্ছে — আর যেহেতু সেই procedural block একটা clocked (posedge clk) block, এই reg-টা সত্যিই একটা flip-flop synthesize করবে। এটাই সেই দুই শর্তের প্রথম উদাহরণ যেটা misconception অংশে সম্পূর্ণভাবে ব্যাখ্যা হবে।
N-bit register — লেসন ১০-এর “sneak peek” এখন সম্পূর্ণ প্রসঙ্গে
Register-এর লেসনে একটা ঝলক দেখানো হয়েছিল এই ঠিক কোডটার — এখন সম্পূর্ণ প্রসঙ্গে সেটা আবার দেখা যাক, কারণ এটা এই লেসনের প্রতিটা ধারণা এক জায়গায় দেখায়:
module register #(parameter WIDTH = 32) (
input wire clk,
input wire rst, // synchronous reset
input wire en, // write-enable
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk) begin
if (rst)
q <= {WIDTH{1'b0}}; // sync reset — সব বিট শূন্য
else if (en)
q <= d; // load — নতুন মান নাও
// else: কোনো assignment নেই → hold, ঠিক feedback-MUX-এর মতো
end
endmoduleলক্ষ্য করুন কয়েকটা জিনিস যা এখন আমরা পুরোপুরি ব্যাখ্যা করতে পারি:
#(parameter WIDTH = 32)— একটা parameter, compile-time-এ configurable। এই একই moduleWIDTH=16দিয়ে instantiate করলে ১৬-বিট register,WIDTH=64দিয়ে ৬৪-বিট — লেসন ১০-এর “N-টা D flip-flop, একটা shared clock” ধারণাটা এখানেNএকটা variable, হাতে ৩২ বা ৬৪ বার না লিখেই।qএকটাreg,always @(posedge clk)-এর ভেতরে assign — তাই এটাWIDTH-টা আসল flip-flop synthesize করবে, প্রতিটা bit-এর জন্য একটা।- উপরের কোডের reset লাইনে ব্যবহৃত curly-brace নোটেশনটা Verilog-এর replication সিনট্যাক্স —
1'b0(single-bit শূন্য)-কেWIDTHবার পাশাপাশি বসিয়ে একটা পূর্ণWIDTH-বিট শূন্য বানানো। এটা লেসন ১০-এর “সব বিট রিসেট” আচরণের সরাসরি প্রকাশ। - শেষ মন্তব্যটাই সবচেয়ে গুরুত্বপূর্ণ পয়েন্ট আবার — যখন
rstআরenদুটোই0, কোনো assignment হয় না, তাইqতার আগের মান ধরে রাখে। কিন্তু এবার এটা bug না — কারণ এই ব্লকটাalways @(posedge clk)(sequential), তাই “assignment না থাকলে hold করা” মানেই flip-flop feedback, ঠিক hardware-এর প্রকৃত আচরণ। “Hood” অংশের latch সমস্যা শুধু combinational (always @(*)) ব্লকে প্রযোজ্য — sequential ব্লকে এই “hold” আচরণটাই কাঙ্ক্ষিত এবং সঠিক।
FSM কোড করা — লেসন ১২-এর state table যখন case স্টেটমেন্ট হয়
এবার সবচেয়ে গুরুত্বপূর্ণ প্রদর্শনী: লেসন ১২-তে হাতে-আঁকা “101” sequence detector-এর state transition table এখন Verilog-এ। টেবিলটা মনে করুন:
| বর্তমান state | Input=0 | Input=1 | Output |
|---|---|---|---|
| S0 | S0 | S1 | 0 |
| S1 | S2 | S1 | 0 |
| S2 | S0 | S3 | 0 |
| S3 | S2 | S1 | 1 |
State encoding ছিল S0=00, S1=01, S2=10, S3=11। এই টেবিলটা এখন কোডে:
module seq_detector_101 (
input wire clk,
input wire rst,
input wire x, // serial bit input
output wire y // '101' match হলে 1
);
localparam S0 = 2'b00, S1 = 2'b01, S2 = 2'b10, S3 = 2'b11;
reg [1:0] state, next_state;
// ── state register — এখানে সত্যিকারের ২টা flip-flop ──
always @(posedge clk) begin
if (rst) state <= S0;
else state <= next_state;
end
// ── next-state logic — pure combinational, টেবিল থেকে সরাসরি ──
always @(*) begin
case (state)
S0: next_state = x ? S1 : S0;
S1: next_state = x ? S1 : S2;
S2: next_state = x ? S3 : S0;
S3: next_state = x ? S1 : S2;
default: next_state = S0;
endcase
end
// ── output logic — Moore machine, শুধু state-এর ফাংশন ──
assign y = (state == S3);
endmoduleপ্রতিটা case শাখা টেবিলের ঠিক একটা row। S0: next_state = x ? S1 : S0; মানে “state S0-তে, input=1 হলে S1-এ যাও, input=0 হলে S0-এই থাকো” — টেবিলের প্রথম row হুবহু। এটাই লেসন ১৫-এর শেষ বাক্যের প্রতিশ্রুতি — রূপান্তরটা প্রায় যান্ত্রিক।
লেসন ১২-এর নিজের observation-টাও এখানে খাটে — output logic-টা (assign y = (state == S3);) একটা সরল ২-input AND-এর সমতুল্য (Q1 AND Q0, যখন state == 2'b11), ঠিক যেমন লেসন ১২ হাতে derive করেছিল।
একটা বোনাস — counter, register-এর সবচেয়ে সাধারণ ব্যবহার
Register (একটা মান ধরে রাখা) আর FSM (state অনুযায়ী পরবর্তী state ঠিক করা) — এই দুইটা ধারণা একসাথে করলেই একটা counter বেরিয়ে আসে: এমন একটা register যার “next state logic” শুধু +1। আগের লেসনে counter schematic-এ বানানো হয়েছিল; এখানে সেই একই আচরণ কোডে:
module up_counter #(parameter WIDTH = 8) (
input wire clk,
input wire rst,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk) begin
if (rst)
count <= {WIDTH{1'b0}};
else if (en)
count <= count + 1; // next-state logic = "বর্তমান + ১"
// else: assignment নেই → hold, ঠিক আগের register module-এর প্যাটার্ন
end
endmoduleলক্ষ্য করুন এই module-টা structurally আগের register module-এর প্রায় হুবহু — শুধু q <= d; (বাইরে থেকে আসা মান নাও)-এর বদলে count <= count + 1; (নিজের বর্তমান মানের উপর ভিত্তি করে পরের মান গণনা করো)। এটাই counter-এর সংজ্ঞাগত বৈশিষ্ট্য: এর “input” আংশিকভাবে নিজের output — একটা feedback loop, ঠিক এই মডিউলের একদম শুরুতে (লেসন ৯) flip-flop তৈরির মূল কৌশলের মতোই, শুধু এবার একটা +1 combinational logic সেই feedback-এর মাঝে বসানো।
নিজে চালিয়ে দেখুন
Icarus Verilog দিয়ে full adder simulate করুন — দুই স্টাইল, একই সত্য
Icarus Verilog একটা free, open-source Verilog simulator — apt install iverilog (Debian/Ubuntu), brew install icarus-verilog (macOS), বা Windows-এ WSL দিয়ে ইনস্টল করা যায়। ইনস্টল করতে না চাইলে EDA Playground (edaplayground.com)-এ ব্রাউজারেই একই কাজ করা যায়, কোনো সেটআপ ছাড়া।
আগের section-এর দুইটা module (full_adder_structural, full_adder_behavioral) একটা ফাইলে (full_adder.v) রাখুন, তারপর একটা testbench লিখুন যেটা দুইটাকেই একসাথে test করে:
`timescale 1ns/1ps
module tb_full_adder;
reg a, b, cin;
wire sum_s, cout_s; // structural output
wire sum_b, cout_b; // behavioral output
full_adder_structural dut_s (.a(a), .b(b), .cin(cin), .sum(sum_s), .cout(cout_s));
full_adder_behavioral dut_b (.a(a), .b(b), .cin(cin), .sum(sum_b), .cout(cout_b));
integer i;
initial begin
$display(" a b cin | struct(sum,cout) | behav(sum,cout) | match?");
for (i = 0; i \< 8; i = i + 1) begin
{a, b, cin} = i[2:0];
#10;
$display(" %b %b %b | %b %b | %b %b | %s",
a, b, cin, sum_s, cout_s, sum_b, cout_b,
(sum_s == sum_b && cout_s == cout_b) ? "YES" : "NO");
end
$finish;
end
endmoduleiverilog -o fa_tb full_adder.v tb_full_adder.v
vvp fa_tbপ্রত্যাশিত output (৮টা row-ই, a,b,cin = ০ থেকে ৭ পর্যন্ত সব সংমিশ্রণ):
a b cin | struct(sum,cout) | behav(sum,cout) | match?
0 0 0 | 0 0 | 0 0 | YES
0 0 1 | 1 0 | 1 0 | YES
0 1 0 | 1 0 | 1 0 | YES
0 1 1 | 0 1 | 0 1 | YES
1 0 0 | 1 0 | 1 0 | YES
1 0 1 | 0 1 | 0 1 | YES
1 1 0 | 0 1 | 0 1 | YES
1 1 1 | 1 1 | 1 1 | YESসব আটটা row-এ YES — লেসন ৫-এর হাতে-গোনা truth table-এর সাথেও হুবহু মেলে (যাচাই করুন)। দুইটা সম্পূর্ণ ভিন্ন Verilog লিখনশৈলী, একই আচরণ।
নিজে বাড়ান: full_adder_structural-এ ইচ্ছাকৃত একটা bug ঢোকান (যেমন and (g2, p, cin);-কে and (g2, a, cin); বানান) — টেবিলে কোন row-গুলো NO দেখায় সেটা দেখুন, আর হাতে ব্যাখ্যা করুন কেন ঠিক সেই row-গুলোতেই পার্থক্য দেখা যাচ্ছে।
| System task | কাজ |
|---|---|
$display(...) | একবার, সাথে সাথে console-এ প্রিন্ট — printf-এর মতো |
$monitor(...) | যখনই কোনো argument বদলায়, স্বয়ংক্রিয়ভাবে আবার প্রিন্ট — continuous logging |
$finish | simulation থামিয়ে দেওয়া |
$dumpfile("wave.vcd") | একটা waveform ফাইল (VCD ফরম্যাট) তৈরি শুরু করা |
$dumpvars | কোন কোন সিগন্যাল সেই VCD ফাইলে রেকর্ড হবে তা নির্দিষ্ট করা |
$dumpfile/$dumpvars একসাথে ব্যবহার করলে একটা .vcd ফাইল তৈরি হয়, যেটা GTKWave বা EDA Playground-এর EPWave দিয়ে খুলে সিগন্যালগুলোর টাইমিং ডায়াগ্রাম চোখে দেখা যায় — একটা চমৎকার debugging অভ্যাস, বিশেষত যখন $display-এর টেক্সট আউটপুট পড়ে বোঝা কঠিন হয়ে যায় (যেমন এই সম্পূর্ণ toy CPU-র মতো জটিল ডিজাইনে, “build” অংশে)।
structural আর behavioral — দুই সম্পূর্ণ ভিন্নভাবে লেখা Verilog module — সিমুলেশনে হুবহু একই truth table তৈরি করে। এটাই প্রমাণ করে দুইটা style একই hardware-এর দুইটা বিবরণ মাত্র, দুইটা আলাদা hardware না।
FSM টেস্টবেঞ্চ — overlapping '101' সত্যিই ধরা পড়ছে কি না যাচাই করুন
লেসন ১২-এর নিজের experiment স্মরণ করুন: bit stream 10101-এ 101 দুইবার ম্যাচ করা উচিত (overlapping — position ১-৩ আর position ৩-৫)। এখন সেটা কোড দিয়ে যাচাই করি।
module tb_seq_detector;
reg clk, rst, x;
wire y;
seq_detector_101 dut (.clk(clk), .rst(rst), .x(x), .y(y));
// ── clock generator — প্রতি ৫ সময়-একক-এ toggle, তাই period = 10 ──
always #5 clk = ~clk;
reg [7:0] stream = 8'b1_0_1_0_1_0_0_1; // MSB আগে পাঠানো হবে, নিচে দেখুন
integer i;
initial begin
clk = 0; rst = 1; x = 0;
@(posedge clk); rst = 0;
$display("bit state y");
for (i = 7; i \>= 0; i = i - 1) begin
x = stream[i];
@(posedge clk);
$display(" %b %0d %b", x, dut.state, y);
end
$finish;
end
endmodule8'b1_0_1_0_1_0_0_1 — আন্ডারস্কোর শুধু পঠনযোগ্যতার জন্য, Verilog উপেক্ষা করে। এই stream-টাই লেসন ১২-এর 10101 (আরও কিছু বিট সহ, প্যাটার্নটা আরেকবার confirm করতে) — MSB (stream[7]) প্রথমে পাঠানো হচ্ছে।
iverilog -o fsm_tb seq_detector.v tb_seq_detector.v
vvp fsm_tbপ্রত্যাশিত ফলাফল — y=1 ঠিক তখনই যখন state S3-এ পৌঁছায়, অর্থাৎ bit 1,0,1 দেখার ঠিক পরের clock edge-এ। Stream 1,0,1,0,1,0,0,1-এ প্রথম 101 position ১-৩-এ শেষ হয় (প্রথম তিন বিট), দ্বিতীয় 101 overlap করে position ৩-৫-এ (তৃতীয় বিট 1 টাই দুইবার ব্যবহৃত হচ্ছে) — তাই y=1 দুইবার আসা উচিত, একেবারে কাছাকাছি দুই cycle-এ, ঠিক লেসন ১২-এর ব্যাখ্যা অনুযায়ী।
case statement-ভিত্তিক এই FSM কোড, লেসন ১২-এর হাতে-আঁকা state table অনুযায়ী, সত্যিই overlapping pattern সঠিকভাবে ধরে — কোনো physical circuit না বানিয়েই, শুধু সিমুলেশনে verify করা যায়।
নিজে বানান
লেসন ১৫-এর সম্পূর্ণ toy CPU — এবার Verilog-এ
- register_file module লিখুন — 8×16, ২টা read port + ১টা write port, লেসন ১০-এর register module-এর সম্প্রসারণ
- alu_add module লিখুন — এই ISA-তে শুধু ADD দরকার (চাইলে লেসন ৮-এর op-select ALU থেকে SUB/AND/OR যোগ করে সম্প্রসারণ করুন)
- data_memory module লিখুন — 2048×16, লেসন ১৪-এর SRAM মডেলের সরাসরি প্রয়োগ
- control_unit module লিখুন — লেসন ১৫-এর ৫-state, ৫-signal control table হুবহু একটা case statement-এ রূপান্তর করে
- একটা top-level toy_cpu module-এ সবগুলো instantiate ও wire করুন, লেসন ১৫-এর ব্লক ডায়াগ্রাম অনুসরণ করে
- একটা testbench লিখুন যা লেসন ১৫-এর ৪-instruction প্রোগ্রাম memory-তে প্রি-লোড করে, clock চালায়, আর শেষে R1=7, R2=35, R3=42, Memory[108]=42 assert করে
এটাই এই সম্পূর্ণ মডিউলের payoff project, কোড আকারে — লেসন ১৫-এ যা schematic-এ বানানো হয়েছিল, এখন সেই একই ডিজাইন Verilog-এ।
Register file
লেসন ১৫-এর instruction format-এ ফিরে তাকান: ADD Rd,Rs,Rt-এ তিনটা স্বাধীন ৩-বিট register field আছে (Rd at [13:11], Rs at [10:8], Rt at [7:5]) — যেমন “উদাহরণ” section-এর trace-এ ADD R3,R1,R2-এ R3, R1, R2 তিনটাই আলাদা register। আর STORE Rs,addr-এ যে register store হবে সেটার field position Rd-এর মতোই [13:11] (যেহেতু STORE-এর কোনো destination register লাগে না, সেই bit-জায়গাটা STORE-এর নিজের “কোন register store করব” সংখ্যার জন্য পুনর্ব্যবহার হয়)। তার মানে register file-কে তিনটা read port দিতে হবে — Rs, Rt-এর জন্য দুটো (ADD-এর ALU operand), আর Rd-এর field-position পড়ার জন্য একটা তৃতীয়টা (STORE-এর data source, যেহেতু সেই field ADD-এর Rs/Rt-এর থেকে ভিন্ন bit-এ থাকে)।
module register_file (
input wire clk,
input wire RegWrite,
input wire [2:0] rs_addr, rt_addr, rd_addr,
input wire [15:0] wdata,
output wire [15:0] rs_data, rt_data, rd_data
);
reg [15:0] regs [0:7]; // ৮টা ১৬-বিট register — লেসন ১০-এর register file
assign rs_data = regs[rs_addr]; // read — combinational, সবসময় বর্তমান মান
assign rt_data = regs[rt_addr];
assign rd_data = regs[rd_addr]; // STORE-এর data source — Rd-field position-এর register পড়া
always @(posedge clk)
if (RegWrite) regs[rd_addr] <= wdata; // write — শুধু clock edge-এ
endmoduleলক্ষ্য করুন regs একটা memory array (reg [15:0] regs [0:7]) — reg-এর একটা array, প্রতিটা element ১৬-বিট। তিনটা read port (rs_data, rt_data, rd_data) assign দিয়ে — সবসময় বর্তমান address-এর মান, কোনো clock লাগে না (combinational read, ঠিক লেসন ১০-এর register file-এর মতো)। Write port একটাই, always @(posedge clk)-এর ভেতরে — শুধু clock edge-এ, শুধু RegWrite=1 হলে। লক্ষ্য করুন rd_data-কে একই ঠিকানায় (rd_addr) পড়া আর লেখা একসাথে ঘটতে পারে কোনো conflict ছাড়াই — read combinational (তাৎক্ষণিক), write শুধু clock edge-এ, তাই একই cycle-এ “পুরনো মান পড়া” আর “নতুন মান লেখা” একসাথে সংজ্ঞায়িত ও নিরাপদ।
ALU
module alu_add (
input wire [15:0] a, b,
output wire [15:0] result
);
assign result = a + b; // এই টয় ISA-তে ALU-র একমাত্র কাজ
endmoduleData memory
module data_memory (
input wire clk,
input wire MemRead, MemWrite,
input wire [10:0] addr,
input wire [15:0] wdata,
output reg [15:0] rdata
);
reg [15:0] mem [0:2047]; // 2048 × 16 — লেসন ১৪-এর ঠিক সেই মডেল
always @(posedge clk) begin
if (MemWrite) mem[addr] <= wdata;
if (MemRead) rdata <= mem[addr];
end
endmoduleControl unit — লেসন ১৫-এর টেবিল, প্রায় অক্ষরে-অক্ষরে
module control_unit (
input wire clk,
input wire rst,
input wire [1:0] opcode, // IR[15:14] — শুধু S_DECODE-এর পর বৈধ
output reg IRWrite,
output reg RegWrite,
output reg MemRead,
output reg MemWrite,
output reg MemToReg
);
localparam ADD = 2'b00, LOAD = 2'b01, STORE = 2'b10;
localparam S_DECODE = 3'd0,
S_ADD_EXEC = 3'd1,
S_MEM_READ = 3'd2,
S_MEM_WRITE = 3'd3,
S_WB = 3'd4;
reg [2:0] state, next_state;
// ── state register — এই মডিউলের একমাত্র প্রকৃত flip-flop-গুলো ──
always @(posedge clk) begin
if (rst) state <= S_DECODE;
else state <= next_state;
end
// ── next-state logic — লেসন ১৫-এর "S_DECODE-এর next-state যুক্তি" টেবিল ──
always @(*) begin
case (state)
S_DECODE: case (opcode)
ADD: next_state = S_ADD_EXEC;
LOAD: next_state = S_MEM_READ;
STORE: next_state = S_MEM_WRITE;
default: next_state = S_DECODE;
endcase
S_ADD_EXEC: next_state = S_WB;
S_MEM_READ: next_state = S_WB;
S_MEM_WRITE: next_state = S_DECODE;
S_WB: next_state = S_DECODE;
default: next_state = S_DECODE;
endcase
end
// ── output logic — লেসন ১৫-এর সম্পূর্ণ control signal truth table, এক জায়গায় ──
always @(*) begin
IRWrite = (state == S_DECODE);
RegWrite = (state == S_WB);
MemRead = (state == S_MEM_READ);
MemWrite = (state == S_MEM_WRITE);
MemToReg = (state == S_WB) && (opcode == LOAD);
end
endmoduleলক্ষ্য করুন MemToReg = (state == S_WB) && (opcode == LOAD); লাইনটা — এটা ঠিক লেসন ১৫-এর সেই সতর্কতামূলক Callout-এর কোডে রূপ: “control signal শুধু state-এর ফাংশন না, state এবং সংরক্ষিত ডেটা (opcode) দুটোর ফাংশন।” সেখানে এটা prose-এ বলা হয়েছিল; এখানে সেটাই সরাসরি একটা Boolean expression।
Top-level — schematic-টাই wiring
module toy_cpu (
input wire clk,
input wire rst,
input wire [15:0] instr_in // "already fetched" — লেসন ১৫-এর স্কোপ অনুযায়ী
);
reg [15:0] IR;
wire [1:0] opcode = IR[15:14];
wire [2:0] rd_field = IR[13:11]; // ADD/LOAD-এ Rd, STORE-এ "কোন register store হবে"
wire [2:0] rs_field = IR[10:8]; // শুধু ADD-এ — প্রথম source operand
wire [2:0] rt_field = IR[7:5]; // শুধু ADD-এ — দ্বিতীয় source operand
wire [10:0] addr_field = IR[10:0]; // শুধু LOAD/STORE-এ
wire IRWrite, RegWrite, MemRead, MemWrite, MemToReg;
// ── IR — লেসন ১৫-এর স্কোপ অনুযায়ী, "already fetched" ধরে নিয়ে ──
always @(posedge clk)
if (IRWrite) IR <= instr_in;
// ── control unit ──
control_unit ctrl (
.clk(clk), .rst(rst), .opcode(opcode),
.IRWrite(IRWrite), .RegWrite(RegWrite),
.MemRead(MemRead), .MemWrite(MemWrite), .MemToReg(MemToReg)
);
// ── register file — তিনটা read address, একটা write address ──
wire [15:0] rs_data, rt_data, rd_data, wb_data;
register_file rf (
.clk(clk), .RegWrite(RegWrite),
.rs_addr(rs_field), .rt_addr(rt_field), .rd_addr(rd_field),
.wdata(wb_data),
.rs_data(rs_data), .rt_data(rt_data), .rd_data(rd_data)
);
// ── ALU + ALUOut ── (ADD-এ Rs+Rt; LOAD/STORE-এ ফলাফল অব্যবহৃত থাকে)
wire [15:0] alu_result;
reg [15:0] ALUOut;
alu_add alu (.a(rs_data), .b(rt_data), .result(alu_result));
always @(posedge clk) ALUOut <= alu_result;
// ── data memory + MDR ── (STORE-এর data আসে rd_data থেকে — Rd-field position-এর register)
wire [15:0] mem_rdata;
reg [15:0] MDR;
data_memory dmem (
.clk(clk), .MemRead(MemRead), .MemWrite(MemWrite),
.addr(addr_field), .wdata(rd_data), .rdata(mem_rdata)
);
always @(posedge clk) if (MemRead) MDR <= mem_rdata;
// ── MemToReg MUX — লেসন ১৫-এর সেই একমাত্র MUX ──
assign wb_data = MemToReg ? MDR : ALUOut;
endmoduleএই wiring লেসন ১৫-এর ব্লক ডায়াগ্রামের প্রতিটা তার, প্রতিটা MUX-এর একটা সরাসরি অনুবাদ — assign wb_data = MemToReg ? MDR : ALUOut; লাইনটাই সেই “MemToReg MUX (0=ALUOut, 1=MDR)” বাক্সের সম্পূর্ণ বর্ণনা। একমাত্র সংযোজন হলো তৃতীয় read port (rd_data) — কারণ এই toy ISA-তে ADD-এর Rs/Rt field আর STORE-এর “কোন register” field আলাদা bit-অবস্থানে থাকে (উপরের register file-এর ব্যাখ্যা দ্রষ্টব্য), তাই একটা physical read port দিয়ে দুটোই সামলানো যায় না।
Testbench — লেসন ১৫-এর ৪-instruction প্রোগ্রাম
module tb_toy_cpu;
reg clk = 0, rst = 1;
reg [15:0] instr_in;
always #5 clk = ~clk;
toy_cpu cpu (.clk(clk), .rst(rst), .instr_in(instr_in));
// instruction encoding, লেসন ১৫-এর ফরম্যাট অনুযায়ী:
// ADD Rd,Rs,Rt : opcode=00, Rd[13:11], Rs[10:8], Rt[7:5] — তিনটাই স্বাধীন field
// LOAD Rd,addr : opcode=01, Rd[13:11], addr[10:0]
// STORE Rs,addr : opcode=10, Rs[13:11] (= rd_field), addr[10:0]
initial begin
rst = 1; instr_in = 16'h0000;
@(posedge clk); rst = 0;
// Memory[100]=7, Memory[104]=35 প্রি-লোড করা হয়েছে ধরে নিচ্ছি
// (বাস্তব টেস্টে cpu.dmem.mem[100] = 7; cpu.dmem.mem[104] = 35; ব্যবহার করুন)
instr_in = {2'b01, 3'd1, 11'd100}; // LOAD R1, 100
repeat (3) @(posedge clk);
instr_in = {2'b01, 3'd2, 11'd104}; // LOAD R2, 104
repeat (3) @(posedge clk);
instr_in = {2'b00, 3'd3, 3'd1, 3'd2, 5'b0}; // ADD R3, R1, R2
repeat (3) @(posedge clk);
instr_in = {2'b10, 3'd3, 11'd108}; // STORE R3, 108
repeat (2) @(posedge clk);
$display("R1=%0d R2=%0d R3=%0d Memory[108]=%0d",
cpu.rf.regs[1], cpu.rf.regs[2], cpu.rf.regs[3], cpu.dmem.mem[108]);
$finish;
end
endmoduleপ্রত্যাশিত চূড়ান্ত অবস্থা: R1=7 R2=35 R3=42 Memory[108]=42 — ঠিক লেসন ১৫-এর ১১-cycle trace টেবিলের চূড়ান্ত ফলাফল। যদি এই সংখ্যাগুলো মিলে যায়, আপনি লেসন ১৫-এর হাতে-আঁকা schematic-টাই এখন কোডে, সিমুলেশনে verify করা অবস্থায় দাঁড় করিয়েছেন।
নিজে বাড়ান:
cpu.dmem.mem[100] = 7; cpu.dmem.mem[104] = 35;লাইন দুটো testbench-এর শুরুতে (reset-এর পর, প্রথম clock-এর আগে) যোগ করে test-টা সম্পূর্ণ চালান।- লেসন ১৫-এর BuildIt-এর challenge অনুযায়ী
SUBinstruction যোগ করুন —alu_add-কে একটাALUOp-সহ op-select ALU-তে বদলান (লেসন ৮), আরcontrol_unit-এ opcode11-এর জন্য নতুন case শাখা যোগ করুন। - Yosys ইনস্টল থাকলে (
yosys -p "synth; stat" toy_cpu.v ...) চালিয়ে দেখুন — কতগুলো gate, কতগুলো flip-flop synthesize হলো। তুলনা করুন: এই ডিজাইনে ঠিক কয়টা flip-flop আসলে থাকা উচিত (IR ১৬ + state ৩ + ALUOut ১৬ + MDR ১৬ + register file ৮×১৬ = কত?) — সিমুলেশনের আগে হিসাব করে, তারপর tool-এর রিপোর্টের সাথে মিলিয়ে দেখুন।
বাস্তব সিস্টেমে
HDL ও Verilog বাস্তব ইন্ডাস্ট্রিতে
Icarus Verilog — এই লেসনের experiment-এ ব্যবহৃত simulator। সম্পূর্ণ free ও open-source, ছোট প্রজেক্ট, শিক্ষা, আর অনেক hobbyist FPGA প্রজেক্টে ব্যাপকভাবে ব্যবহৃত হয়। কমান্ড-লাইন থেকে চলে, কোনো বিশাল IDE লাগে না — ঠিক এই লেসনে যেভাবে ব্যবহার করা হলো।
Yosys + OpenROAD/OpenLane — সম্পূর্ণ open-source RTL-থেকে-GDSII (আসল চিপ-লেআউট ফাইল) flow। SkyWater-এর ওপেন ১৩০ন্যানোমিটার PDK-এর সাথে মিলিয়ে (“চিপইগনাইট” প্রোগ্রাম) ছাত্র, hobbyist, এমনকি এই কোর্সের কোনো শিক্ষার্থীও নিজের লেখা Verilog থেকে একটা সত্যিকারের সিলিকন চিপ তৈরি (tape-out) করাতে পারেন, প্রায় বিনামূল্যে — এমন কিছু যা এক দশক আগেও শুধু বড় কোম্পানির পক্ষেই সম্ভব ছিল।
Xilinx Vivado ও Intel/Altera Quartus — FPGA-র জন্য শিল্প-মান commercial toolchain। এগুলো Verilog/VHDL কোড নিয়ে synthesis + place-and-route করে একটা bitstream তৈরি করে, যেটা একটা FPGA চিপে লোড করলে সেই চিপ-টা আক্ষরিকভাবে আপনার বর্ণনা করা circuit-এ পরিণত হয় — কোনো fabrication ছাড়াই, কয়েক মিনিটেই।
Synopsys Design Compiler ও Cadence Genus — ASIC ডিজাইনের জন্য শিল্প-মান commercial synthesis tool। Apple, NVIDIA, AMD, Intel-এর মতো কোম্পানি এই ধরনের tool ব্যবহার করে তাদের বিলিয়ন-transistor চিপের RTL (Verilog/SystemVerilog) থেকে gate-level netlist তৈরি করে — এই লেসনের “synthesis” ধারণাটাই, শুধু বহুগুণ বড় স্কেলে।
SystemVerilog (IEEE 1800) — Verilog-এর আনুষ্ঠানিক উত্তরসূরি, একটা superset। সবচেয়ে গুরুত্বপূর্ণ সংযোজনগুলোর একটা হলো logic টাইপ — যেটা wire/reg-এর জায়গায় বেশিরভাগ ক্ষেত্রে ব্যবহার করা যায়, আর নামের কারণে “reg = register” এই ভুল ধারণাটা এড়ায় (যদিও নিয়মটা একই থেকে যায়: hardware অর্থ context-নির্ভর, শুধু syntax সরল হয়েছে)। আধুনিক শিল্পের প্রায় সব নতুন কোড SystemVerilog-এ লেখা হয়, যদিও ভিত্তি ধারণাগুলো — module, always block, wire, testbench — Verilog থেকেই এসেছে, যা আমরা এই লেসনে শিখলাম।
Open-source RISC-V core — PicoRV32, SERV — গত লেসনে উল্লেখিত এই দুটো core সম্পূর্ণভাবে Verilog-এ লেখা, publicly downloadable, আর synthesizable। PicoRV32 হাজার হাজার FPGA প্রজেক্টে ব্যবহৃত হয়েছে; SERV-এর bit-serial ডিজাইন Yosys দিয়েই synthesize করা যায় কোনো commercial লাইসেন্স ছাড়াই — এই লেসনে শেখা প্রতিটা concept (module, always block, FSM, testbench) সেই কোডে হুবহু প্রয়োগ হয়েছে, শুধু বিশাল স্কেলে।
Hardware verification শিল্প ও UVM — একটা চমকপ্রদ পরিসংখ্যান: আধুনিক বড় চিপ ডিজাইন টিমে design engineer-এর চেয়ে verification engineer সংখ্যায় প্রায়ই বেশি — কারণ একটা bug চিপ fabrication-এর পরে ধরা পড়লে সংশোধনের খরচ লক্ষ লক্ষ ডলার। UVM (Universal Verification Methodology), SystemVerilog-এর উপর বানানো একটা framework, testbench লেখার একটা standardized, বিশাল-স্কেল পদ্ধতি — এই লেসনের ছোট্ট tb_full_adder module-টার শিল্প-মানের, বহু-স্তরের উত্তরসূরি।
Google TPU-র মতো বিশাল চিপ — লক্ষ লক্ষ গেট, বিলিয়ন transistor-এর একটা AI accelerator চিপ — এর সবকিছুর মূলে এখনও এই লেসনের ঠিক এই ধারণাগুলো: module, always, assign, structural instantiation। স্কেল আলাদা, ভাষা আর পদ্ধতি একই — এটাই এই লেসনের কেন্দ্রীয় দাবির চূড়ান্ত প্রমাণ: একটা AND gate থেকে একটা AI চিপ পর্যন্ত, একই কয়েকটা primitive নিয়মে সবকিছু বানানো।
Formal verification ও SymbiYosys-এর মতো টুল — testbench দিয়ে যাচাই করার একটা সীমাবদ্ধতা আছে: এটা শুধু আপনি যেসব input পাঠিয়েছেন সেগুলোর জন্যই সঠিকতা প্রমাণ করে, বাকি সব সম্ভাব্য input অপরীক্ষিত থেকে যায়। Formal verification এর বদলে গাণিতিকভাবে সব সম্ভাব্য input-এর জন্য একটা property (যেমন “এই FSM কখনো অবৈধ state-এ যাবে না”) প্রমাণ করার চেষ্টা করে — SAT/SMT solver ব্যবহার করে, testbench না চালিয়েই। SymbiYosys (Yosys-ভিত্তিক, open-source) ছোট মডিউলে এটা ব্যবহারিকভাবে সম্ভব করে তোলে। এটা এই কোর্সের Level 13 (গবেষণা-স্তরের CS, formal methods)-এর একটা সরাসরি প্রয়োগ — testbench “স্যাম্পল টেস্টিং”, formal verification “গাণিতিক প্রমাণ”, ঠিক যেমন Level 0-তে আমরা example-checking বনাম প্রকৃত proof-এর পার্থক্য শিখেছিলাম।
যে ভুলগুলো সবাই করে
“`reg` কীওয়ার্ড মানেই hardware-এ একটা flip-flop/register তৈরি হবে।”
এটাই Verilog-এর সবচেয়ে সাধারণ, সবচেয়ে persistent ভুল ধারণা — নামটাই এই ভুলের জন্ম দেয়।
সত্য: reg ভাষাগতভাবে শুধু এইটুকু বলে — “এই সিগন্যালের মান একটা procedural block (always বা initial)-এর ভেতরে assign করা হয়, কোনো assign স্টেটমেন্ট দিয়ে না।” এর বেশি কিছু না। এটা flip-flop হবে কি না, সম্পূর্ণ নির্ভর করে কোন ধরনের procedural block-এ, কীভাবে সেটা assign হচ্ছে তার উপর:
// এই reg টা ✓ ফ্লিপ-ফ্লপ synthesize করে —
// clocked (posedge) always block-এর ভেতরে assign
reg q_ff;
always @(posedge clk)
q_ff <= d;
// এই reg টা ✗ ফ্লিপ-ফ্লপ synthesize করে না —
// শুধু combinational logic, কারণ always @(*), কোনো clock এজ নেই
reg q_comb;
always @(*)
q_comb = a & b;দুইটাই reg কীওয়ার্ড দিয়ে ঘোষিত। প্রথমটা always @(posedge clk)-এর ভেতরে — synthesis tool এটা পড়ে বোঝে “এই সিগন্যাল শুধু clock edge-এ বদলায়, বাকি সময় আগের মান ধরে থাকে” — এই আচরণ শুধু একটা physical memory element (flip-flop) দিয়েই সম্ভব, তাই tool একটা flip-flop বসায়। দ্বিতীয়টা always @(*)-এর ভেতরে (কোনো edge-sensitivity নেই, শুধু input বদলালেই re-evaluate) — এই আচরণ pure combinational logic দিয়েই সম্ভব (input বদলালে output তাৎক্ষণিক বদলায়, কোনো “মনে রাখা” নেই), তাই tool কোনো flip-flop বসায় না, শুধু কয়েকটা গেট।
এই লেসনের FSM উদাহরণেই এই দুইটা পাশাপাশি দেখা গেছে — state (clocked always block) সত্যিকারের flip-flop, next_state (combinational always block) নিছক তার/গেট, যদিও দুটোই reg কীওয়ার্ড দিয়ে ঘোষিত।
| এই লেসনের উদাহরণ | ঘোষণা | assign হয় কোথায় | সিন্থেসাইজড hardware |
|---|---|---|---|
q (D flip-flop) | output reg q | always @(posedge clk) | আসল flip-flop |
q (register module) | output reg [WIDTH-1:0] q | always @(posedge clk) | আসল flip-flop, WIDTH-টা |
state (FSM) | reg [1:0] state | always @(posedge clk) | আসল flip-flop, ২-টা |
next_state (FSM) | reg [1:0] next_state | always @(*) | শুধু গেট, কোনো flip-flop না |
count (counter) | output reg [WIDTH-1:0] count | always @(posedge clk) | আসল flip-flop, WIDTH-টা |
পাঁচটা reg-ই ঘোষণায় প্রায় অভিন্ন প্যাটার্ন দেখায়, কিন্তু শুধু next_state ব্যতিক্রম — কারণ এটার assignment একটা combinational block-এ, বাকি সবগুলোর মতো clocked block-এ না।
নিয়ম মনে রাখার সহজ উপায়: reg/wire টাইপের নাম দেখে হার্ডওয়্যার অনুমান করবেন না — কোন ধরনের always block-এ, কীভাবে assign হচ্ছে সেটা দেখুন। Clocked edge (posedge/negedge) → flip-flop। Combinational (@(*)), সব branch-এ assignment সহ → pure logic, কোনো memory না। ঠিক এই বিভ্রান্তির কারণেই SystemVerilog logic কীওয়ার্ড চালু করেছে — কিন্তু মূল নিয়মটা (hardware অর্থ context-নির্ভর) তাতেও অপরিবর্তিত থাকে।
“Simulation-এ সঠিক ফলাফল এলে, synthesize করা hardware-ও ঠিক একইভাবে আচরণ করবে।”
প্রায়ই সত্য, কিন্তু গ্যারান্টি না — আর যেখানে ভাঙে, সেখানে বিপজ্জনকভাবে ভাঙে।
Simulation একটা software model — এটা Verilog-এর সম্পূর্ণ ভাষা বোঝে, #10 delay, $display, এমনকি অস্পষ্টভাবে লেখা কোডও একটা নির্দিষ্ট (simulator-নির্ধারিত) নিয়মে চালিয়ে দেয়। Synthesis শুধু একটা সীমিত subset বোঝে, আর অস্পষ্ট কোডের জন্য তার নিজস্ব সিদ্ধান্ত নেয় — যেটা simulator-এর “সিদ্ধান্ত”-এর সাথে না-ও মিলতে পারে।
এই লেসনের “hood” অংশের latch উদাহরণটাই এর সবচেয়ে স্পষ্ট প্রমাণ: একটা combinational always @(*) ব্লকে একটা branch-এ assignment মিসিং থাকলে —
- Simulation-এ:
yvariable-টা তার আগের মেমরি-মান ধরে রাখে, কোনো error দেখায় না, testbench হয়তো এমনকি “pass” করে যদি টেস্ট কেসগুলো ঠিক সেই সমস্যাযুক্ত ইনপুট কম্বিনেশন কভার না করে। - Synthesis-এ: সেই “আগের মান ধরে রাখা” বাস্তবায়ন করতে tool একটা আসল latch বসায় — একটা memory element, যেটা আপনি কখনো চাননি, আর যেটা timing hazard/glitch তৈরি করতে পারে বাস্তব hardware-এ।
আরেকটা ক্লাসিক উদাহরণ: blocking (=) বনাম non-blocking (<=) ভুল ব্যবহার একটা sequential block-এ — কিছু simulator একরকম আচরণ দেখাতে পারে, আসল synthesize করা hardware আরেকরকম (কারণ hardware-এ সব flip-flop সত্যিই একসাথে আপডেট হয়, কিন্তু ভুল blocking assignment simulation-এ একটা মিথ্যা “sequential-এর মতো” আচরণ তৈরি করতে পারে যা synthesized সার্কিটে থাকে না)।
নিয়ম: simulation দিয়ে logic সঠিকতা verify করুন, কিন্তু কোড লেখার সময়ই synthesizable, সুশৃঙ্খল প্যাটার্ন মেনে চলুন (সব branch-এ assignment, clocked block-এ non-blocking, combinational block-এ blocking) — শুধু “simulation pass করছে” দেখেই নিশ্চিন্ত হবেন না। প্রকৃত ইন্ডাস্ট্রিতে synthesis tool-এর warning (যেমন “inferred latch”) কখনো উপেক্ষা করা হয় না।
“Verilog একটা প্রোগ্রামিং ভাষা — পুরো ফাইল উপর থেকে নিচে, statement-বাই-statement, ঠিক C-এর মতো execute হয়।”
মৌলিকভাবে ভুল, আর এই ভুল ধারণা নিয়ে Verilog শিখলে বেশিরভাগ কোড বিভ্রান্তিকর মনে হবে।
Verilog hardware বর্ণনা করে, আর বাস্তব hardware-এ সব গেট, সব flip-flop একসাথে, সমান্তরালে কাজ করে — কোনো একটা গেট “তার পালার” জন্য অপেক্ষা করে না। এই লেসনের full_adder_structural module-টাই এর প্রমাণ:
xor (p, a, b);
xor (sum, p, cin);
and (g1, a, b);
and (g2, p, cin);
or (cout, g1, g2);এই পাঁচটা লাইন উপর থেকে নিচে “execute” হয় না — এগুলো পাঁচটা সমান্তরাল, সবসময়-সক্রিয় গেট-এর বর্ণনা, ঠিক physical hardware-এর মতো। and (g2, p, cin); লাইনটা xor (p, a, b);-এর “পরে চলে” না — বরং g2-এর মান p বদলালেই, তা যে মুহূর্তেই হোক, তৎক্ষণাৎ পুনর্গণনা হয়, ঠিক একটা physical AND gate-এর মতো। একইভাবে দুইটা আলাদা always ব্লক, বা একটা always ব্লক আর একটা assign — এগুলো সবাই concurrent (সমান্তরাল), একটা প্রোগ্রামের ভিন্ন ভিন্ন লাইনের মতো ক্রমান্বয়ে না, বরং physical circuit-এর ভিন্ন ভিন্ন অংশের মতো একই সাথে সক্রিয়।
একটামাত্র always/initial ব্লকের ভেতরে স্টেটমেন্টগুলো লেখা ক্রম অনুযায়ী scheduled হয় (simulator সেই ক্রম মেনে চলে) — কিন্তু এটা “প্রোগ্রাম execute হওয়া” না, এটা simulator-এর একটা মডেলিং কনভেনশন, যা বাস্তব hardware-এর সমান্তরাল আচরণকেই সঠিকভাবে প্রতিফলিত করার জন্য ডিজাইন করা।
সঠিক মানসিক মডেল: একটা Verilog ফাইল একটা circuit diagram-এর টেক্সট রূপ — প্রতিটা module, প্রতিটা always ব্লক, প্রতিটা assign একটা physical hardware ব্লক, যেগুলো সব সবসময়, সমান্তরালভাবে বিদ্যমান আর সক্রিয়, ঠিক লেসন ১৫-এর schematic-এর মতো — একটা প্রোগ্রামের instruction sequence না।
“testbench-এ #10 লেখা মানে আসল hardware ১০ ন্যানোসেকেন্ডে সাড়া দেবে — simulation-এর সময়ই বাস্তব gate delay।”
না — এই দুইটা “সময়” সম্পূর্ণ ভিন্ন জিনিস, আর গুলিয়ে ফেলা সহজ কারণ দুটোই একক হিসেবে ন্যানোসেকেন্ড ব্যবহার করে।
`timescale 1ns/1ps আর #10 — এগুলো একটা simulation-এর বই-খাতার হিসাব, শুধুই একটা abstraction যাতে simulator জানে event-গুলো কোন আপেক্ষিক ক্রমে ঘটবে। এটা RTL কোডের একটা annotation, বাস্তব সিলিকনের কোনো পরিমাপ না। যখন আমরা #10 লিখি একটা testbench-এ, সেটা শুধু বলছে “পরবর্তী স্টেটমেন্ট চালানোর আগে সিমুলেটরের ঘড়িতে ১০ একক এগিয়ে যাও” — DUT-এর ভেতরের কোনো gate/wire-এর সাথে এই সংখ্যার প্রত্যক্ষ সম্পর্ক নেই, যদি না আলাদাভাবে gate-delay annotate করা হয় (যা সাধারণ RTL কোডে করা হয় না)।
বাস্তব propagation delay আসে সম্পূর্ণ ভিন্ন জায়গা থেকে — লেসন ১১ (clock ও timing)-এ যা শেখা হয়েছে: transistor switching speed, wire capacitance, standard cell library-র characterization ডেটা। এই সংখ্যাগুলো RTL simulation-এ থাকেই না — সেগুলো আসে synthesis-পরবর্তী static timing analysis (STA) থেকে, যেখানে প্রতিটা gate/wire-এর প্রকৃত delay (place & route-এর পরে, physical layout থেকে বের করা) ব্যবহার করে গণনা করা হয় circuit সর্বোচ্চ কত দ্রুত clock-এ চলতে পারবে।
তাই একটা RTL simulation “কাজ করছে” মানে শুধু logic সঠিক — সিমুলেশনে ব্যবহৃত #10, #5 সংখ্যাগুলো বদলে #1000000 করলেও logic-এর ফলাফল অপরিবর্তিত থাকবে, শুধু simulation চলতে সময় বেশি লাগবে। বাস্তব চিপ কত দ্রুত চলতে পারবে (মেগাহার্জ/গিগাহার্জে), সেই প্রশ্নের উত্তর RTL simulation দেয় না — সেটা আসে synthesis, place & route, আর STA-এর পরে, একটা সম্পূর্ণ ভিন্ন ধাপে।
বুঝেছেন কি না দেখুন
1wire আর reg-এর মধ্যে ভাষাগত পার্থক্য ঠিক কী? আর একটা reg কখন সত্যিকারের flip-flop synthesize করে, কখন করে না?
যুক্তি
wire আর reg-এর মধ্যে ভাষাগত পার্থক্য ঠিক কী? আর একটা reg কখন সত্যিকারের flip-flop synthesize করে, কখন করে না?ভাষাগত পার্থক্য (assignment-এর স্থান):
wire— শুধু একটাassignস্টেটমেন্ট দিয়ে, বা কোনো gate/module instantiation-এর output হিসেবে driven হতে পারে। সবসময় তার driver-এর বর্তমান মান প্রতিফলিত করে, কোনো “মনে রাখা” নেই।reg— শুধু একটা procedural block (alwaysবাinitial)-এর ভেতরে assign করা যায়। কোনোassignদিয়ে সরাসরি না।
Hardware অর্থ — নির্ভর করে কীভাবে assign হচ্ছে তার উপর, নাম না:
reg কোথায় assign হচ্ছে | সিন্থেসাইজড hardware |
|---|---|
always @(posedge clk) — clocked, edge-sensitive | সত্যিকারের flip-flop — মান শুধু clock edge-এ বদলায়, বাকি সময় ধরে রাখে (physically memory প্রয়োজন) |
always @(*) — combinational, সব branch-এ assign করা | শুধু গেট, কোনো flip-flop না — মান input বদলালেই তাৎক্ষণিক বদলায়, কোনো memory প্রয়োজন নেই |
always @(*) — combinational, কোনো branch-এ assignment বাদ পড়েছে | অনাকাঙ্ক্ষিত latch — hood অংশের bug, “hold করা” আচরণ implement করতে tool একটা memory element বসায় |
এই লেসনের FSM উদাহরণেই দুটো reg পাশাপাশি ছিল: state (clocked block-এ assign) সত্যিকারের ২টা flip-flop; next_state (combinational block-এ, সব branch কভার করা) নিছক কয়েকটা গেট, কোনো memory না। দুটোই reg কীওয়ার্ড, কিন্তু সম্পূর্ণ ভিন্ন hardware।
সংক্ষেপে: reg মানে “procedural assignment”, hardware অর্থ context থেকেই আসে — clocked block = memory, combinational block (সম্পূর্ণ কভার করা) = pure logic।
2নিচের combinational always block-টা দেখুন। এটাতে কী সমস্যা আছে, synthesis-এ কী hardware তৈরি হবে, আর কীভাবে ঠিক করবেন?
always @(*) begin
case (sel)
2'b00: y = in0;
2'b01: y = in1;
2'b10: y = in2;
endcase
end
প্রয়োগ
always @(*) begin
case (sel)
2'b00: y = in0;
2'b01: y = in1;
2'b10: y = in2;
endcase
endসমস্যা: sel একটা ২-বিট সিগন্যাল, তাই এর ৪টা সম্ভাব্য মান আছে (00, 01, 10, 11), কিন্তু case স্টেটমেন্টে শুধু তিনটা কভার করা হয়েছে — sel = 2'b11 হলে y-এর কোনো assignment নেই।
Synthesis-এ কী হয়: sel = 2'b11-এর ক্ষেত্রে “y-এর আগের মান ধরে রাখো” — এই আচরণ বাস্তবায়ন করতে synthesis tool একটা অনাকাঙ্ক্ষিত latch বসিয়ে দেয়। ফলাফল একটা 4-to-1 MUX-এর বদলে একটা MUX + latch-এর মিশ্রণ — যেটা শুধু বাড়তি hardware না, বরং একটা real timing hazard (latch-এর transparent/opaque আচরণ race condition তৈরি করতে পারে)।
Simulation-এ কী দেখাবে: সম্ভবত কোনো visible সমস্যা না — y তার আগের মান রেখে দেবে, testbench হয়তো sel = 2'b11 টেস্ট না করলে এই bug ধরাই পড়বে না। এটাই আগের misconception অংশের সরাসরি একটা উদাহরণ — simulation “pass” করলেও hardware ভুল।
সমাধান — দুইটা উপায়:
// উপায় ১ — default শাখা যোগ করা
always @(*) begin
case (sel)
2'b00: y = in0;
2'b01: y = in1;
2'b10: y = in2;
default: y = in0; // অথবা যেকোনো defined মান
endcase
end// উপায় ২ — সব সম্ভাব্য মান explicit ভাবে কভার করা
always @(*) begin
case (sel)
2'b00: y = in0;
2'b01: y = in1;
2'b10: y = in2;
2'b11: y = in2; // অথবা এই ইনপুটের জন্য যা যুক্তিসঙ্গত
endcase
endদুইটাতেই এখন প্রতিটা সম্ভাব্য sel মানের জন্য y explicit ভাবে assign হচ্ছে — কোনো “আগের মান ধরে রাখা” দরকার নেই, তাই pure combinational logic (একটা সাধারণ 4-to-1 MUX, লেসন ৭) synthesize হবে, কোনো latch না। একটা ভালো synthesis tool এই ধরনের ভুল case statement দেখলে “incomplete case, latch inferred” ধরনের warning দেয় — সেটা কখনো উপেক্ষা করা উচিত না।
3লেসন ১৫-এর BuildIt challenge ছিল একটা SUB Rd, Rs, Rt instruction যোগ করা (opcode 11)। এই লেসনের control_unit module-এ ঠিক কোন কোন লাইন বদলাতে/যোগ করতে হবে?
প্রয়োগ
SUB Rd, Rs, Rt instruction যোগ করা (opcode 11)। এই লেসনের control_unit module-এ ঠিক কোন কোন লাইন বদলাতে/যোগ করতে হবে?লেসন ১৫-এর নিজের উত্তর ছিল: opcode encoding-এ নতুন কোড, ALU-তে একটা ALUOp signal, control table-এ opcode অনুযায়ী পার্থক্য করা — datapath-এ নতুন bus/MUX লাগবে না। এই লেসনের control_unit কোডে এই পরিবর্তনগুলো ঠিক এভাবে দেখাবে:
১. একটা নতুন opcode constant:
localparam ADD = 2'b00, LOAD = 2'b01, STORE = 2'b10, SUB = 2'b11;২. S_DECODE-এর ভেতরের case-এ একটা নতুন শাখা:
S_DECODE: case (opcode)
ADD: next_state = S_ADD_EXEC;
SUB: next_state = S_ADD_EXEC; // ADD-এর সাথে state শেয়ার!
LOAD: next_state = S_MEM_READ;
STORE: next_state = S_MEM_WRITE;
default: next_state = S_DECODE;
endcaseলক্ষ্য করুন SUB আর ADD একই state (S_ADD_EXEC)-এ যায় — কারণ দুটোই একই ধাপে (register read → ALU → ALUOut) একই hardware path ব্যবহার করে, শুধু ALU-র ভেতরের operation ভিন্ন। এটা ঠিক লেসন ১৫-এর S_WB state শেয়ারিং-এর একই নীতি — “একই hardware path হলে state শেয়ার করা যায়।”
৩. একটা নতুন ALUOp output signal, আর সেটা opcode থেকে derive করা:
output reg ALUOp; // 0 = ADD, 1 = SUB — নতুন signal
// output logic-এ যোগ:
always @(*) begin
// ... আগের সব signal অপরিবর্তিত ...
ALUOp = (opcode == SUB);
end৪. ALU module-টা op-select-সহ বদলাতে হবে (লেসন ৮-এর ALU সরাসরি প্রয়োগ):
module alu (
input wire [15:0] a, b,
input wire op, // 0=ADD, 1=SUB
output wire [15:0] result
);
assign result = op ? (a - b) : (a + b);
endmoduleসংক্ষেপে বদলানো লাগল: control_unit-এ একটা নতুন opcode case, একটা নতুন ALUOp output; ALU module-এ একটা নতুন input পোর্ট আর একটা conditional expression। কোনো নতুন state, কোনো নতুন datapath wire/MUX লাগেনি — ঠিক যেমন লেসন ১৫ আগেই ভবিষ্যদ্বাণী করেছিল।
4একটা testbench-এ #10 delay আর $display স্টেটমেন্ট থাকে, কিন্তু সেই testbench কখনো synthesize করা হয় না, করা যায়ও না। এটা কি একটা সমস্যা — মানে, testbench-এর “hardware” কোথায় যায়?
যুক্তি
#10 delay আর $display স্টেটমেন্ট থাকে, কিন্তু সেই testbench কখনো synthesize করা হয় না, করা যায়ও না। এটা কি একটা সমস্যা — মানে, testbench-এর “hardware” কোথায় যায়?এটা সমস্যা না — এটাই testbench-এর সংজ্ঞাগত ভূমিকা।
Testbench-এর উদ্দেশ্য hardware তৈরি করা না — উদ্দেশ্য হলো DUT (Device Under Test)-কে simulation-এ যাচাই করা। Testbench কখনোই একটা চিপে বসে না, কখনোই একটা FPGA-তে load হয় না — এটা শুধু আপনার সাধারণ কম্পিউটারে, simulator-এর ভেতরে, একটা সফটওয়্যার প্রোগ্রাম হিসেবে চলে, ঠিক একটা unit test যেভাবে চলে।
এই কারণেই testbench-এ #10 (delay), $display (console print), $finish (simulation থামানো), initial ব্লক দিয়ে manual stimulus তৈরি করা — এই সব simulation-only construct ব্যবহার করা সম্পূর্ণ স্বাভাবিক আর প্রত্যাশিত। এগুলো “synthesize করা যায় না” বলাটা কোনো সীমাবদ্ধতা না, বরং সঠিক আচরণ — কারণ testbench কখনো hardware হওয়ারই কথা না।
তুলনা করুন সফটওয়্যার টেস্টিং-এর সাথে: একটা Python unittest ফাইলে assert, mock, টেস্ট ডেটা তৈরির লজিক থাকে — এই কোডটা কখনো production-এ deploy হয় না, শুধু development-এ চলে। DUT (আপনার আসল ফাংশন/মডিউল) production-এ যায়; testbench (টেস্ট) যায় না। Verilog-এও ঠিক এই একই বিভাজন — DUT synthesizable হতে হবে (এটাই আসল hardware), testbench শুধু simulation-এ verify করার জন্য (এটা কখনো hardware হয় না, হওয়ার দরকারও নেই)।
একটা বাস্তব প্রজেক্টে তাই দুই ধরনের ফাইল স্পষ্টভাবে আলাদা রাখা হয়: full_adder.v (synthesizable, চিপে যাবে) আর tb_full_adder.v (simulation-only, শুধু verify করার জন্য, চিপে কখনো যাবে না)।
5একটা ২-ধাপের shift register কল্পনা করুন — প্রতিটা clock cycle-এ q1 ← d, q2 ← q1 (পুরনো q1-এর মান)। যদি কেউ ভুল করে একটা always @(posedge clk) ব্লকে blocking assignment (=) ব্যবহার করে, ঠিক কী ভুল হবে? একটা concrete উদাহরণ দিয়ে দেখান।
যুক্তি
q1 ← d, q2 ← q1 (পুরনো q1-এর মান)। যদি কেউ ভুল করে একটা always @(posedge clk) ব্লকে blocking assignment (=) ব্যবহার করে, ঠিক কী ভুল হবে? একটা concrete উদাহরণ দিয়ে দেখান।সঠিক কোড (non-blocking):
always @(posedge clk) begin
q1 <= d;
q2 <= q1;
endNon-blocking semantics: right-hand-side-এর সব মান (d, q1) clock edge-এর ঠিক আগের, পুরনো মান দিয়ে একসাথে গণনা হয়, তারপর left-hand-side-এ একসাথে বসে। তাই q2 <= q1;-এ যে q1 ব্যবহৃত হচ্ছে, সেটা এই clock edge-এর আগের q1 — q1 <= d; এই একই ব্লকে থাকলেও তার নতুন মান q2-এর গণনায় ঢুকে পড়ে না। ফলাফল: প্রতি cycle-এ মান ঠিক এক ধাপ shift করে — d-এর মান এক cycle পর q1-এ, দুই cycle পর q2-এ পৌঁছায়। এটাই বাস্তব দুইটা ফ্লিপ-ফ্লপের সঠিক আচরণ।
ভুল কোড (blocking):
always @(posedge clk) begin
q1 = d;
q2 = q1; // ⚠ এই q1 কোনটা — পুরনো, না নতুন?
endBlocking semantics: স্টেটমেন্ট লেখা ক্রম অনুযায়ী, একটার পর একটা, সাথে সাথে কার্যকর হয়। তাই q1 = d; কার্যকর হওয়ার সাথে সাথে q1-এর মান বদলে যায় — আর তারপরের লাইন q2 = q1; সেই নতুন (এইমাত্র বদলানো) q1-এর মান পড়ে, পুরনোটা না।
ফলাফল: একটা মান d-এ বসালে, এক cycle-এই q1 আর q2 দুটোতেই সেই মান পৌঁছে যায় — যেন d-এর মান একবারে দুই ধাপ shift হয়ে গেল। বাস্তব hardware-এ এটা কখনো সম্ভব না — দুইটা physical ফ্লিপ-ফ্লপ কখনো একই clock edge-এ একে অপরের নতুন মান “দেখতে” পারে না, প্রতিটা ফ্লিপ-ফ্লপ শুধু তার input-এর ওই edge-এর ঠিক আগের মান capture করে। তাই এই কোড simulation-এ হয়তো “চলে” (কোনো syntax error নেই), কিন্তু একটা সম্পূর্ণ ভুল shift-register বর্ণনা করছে — বাস্তব ২-ফ্লিপ-ফ্লপ hardware-এর আচরণের সাথে এর কোনো মিল নেই।
এই কারণেই নিয়মটা exception ছাড়া মেনে চলতে হয়: clocked (sequential) always ব্লকে সবসময় non-blocking (<=), যাতে প্রতিটা flip-flop-এর নতুন মান গণনা হয় সবার পুরনো মান থেকে, একসাথে — ঠিক যেভাবে physical hardware-এ সব ফ্লিপ-ফ্লপ একই clock edge-এ সমান্তরালে কাজ করে।
6এই লেসনের full-adder testbench-এ প্রতিটা input combination-এর পর #10 অপেক্ষা করা হয়েছে। যদি এই #10-কে বদলে #1 করা হয়, truth table output-এ কি কোনো পরিবর্তন আসবে? আর যদি পুরোপুরি বাদ দেওয়া হয় (কোনো delay ছাড়াই সব input একসাথে বদলানো হয়), কী সমস্যা হবে?
যুক্তি
#10 অপেক্ষা করা হয়েছে। যদি এই #10-কে বদলে #1 করা হয়, truth table output-এ কি কোনো পরিবর্তন আসবে? আর যদি পুরোপুরি বাদ দেওয়া হয় (কোনো delay ছাড়াই সব input একসাথে বদলানো হয়), কী সমস্যা হবে?#10 থেকে #1-এ বদলালে: কোনো পার্থক্য হবে না — truth table output অবিকল একই থাকবে।
কারণ #10/#1 শুধু simulation-এর “বই-খাতার সময়” — DUT-এর ভেতরের xor/and/or গেটগুলোর কোনো actual propagation delay এই RTL কোডে annotate করা নেই, তাই simulator সেই গেটগুলোকে zero-delay ধরে নেয় (input বদলালেই output তাৎক্ষণিক আপডেট, একই simulation-time-এর মধ্যে)। #10 বা #1 শুধু testbench-কে বলছে “পরবর্তী input বসানোর আগে এতটুকু সময় অপেক্ষা করো, যাতে আগের input-এর output স্থির হওয়ার (settle করার) সুযোগ পায়” — আর যেহেতু এখানে সেই output zero-delay-তেই স্থির হয়ে যায়, #1 বা #10 দুটোই যথেষ্ট। এটাই আগের misconception-এর মূল দাবি — simulation-এর #N বাস্তব gate delay না, এটা শুধু event-ordering-এর একটা সুবিধাজনক abstraction।
যদি delay পুরোপুরি বাদ দেওয়া হয় — input বসানোর পরেই, কোনো #10 ছাড়াই, সাথে সাথে $display করা হয়:
{a, b, cin} = i[2:0];
$display(...); // ⚠ কোনো delay নেই এর আগেএখানে সমস্যাটা delay-র “বাস্তবতা” নিয়ে না, বরং simulator-এর event scheduling-এর একটা সূক্ষ্মতা নিয়ে — Verilog-এর simulation model অনুযায়ী, a, b, cin-এর নতুন মান assign হওয়ার পর তার উপর নির্ভরশীল সব assign/gate-output আপডেট হতে একটা ন্যূনতম “delta cycle” লাগতে পারে (simulator-এর ভেতরের event queue processing, বাস্তব সময় শূন্য হলেও)। যদি $display সেই আপডেট সম্পূর্ণ হওয়ার আগেই চলে, এটা পুরনো (stale) sum/cout মান প্রিন্ট করতে পারে — একটা race condition, ঠিক লেসন এই লেসনের blocking/non-blocking আলোচনার আরেকটা রূপ। তাই বাস্তবে testbench-এ ইনপুট বদলানোর পর অন্তত একটা ছোট delay (#1 বা তার বেশি) রাখাই নিরাপদ অভ্যাস, এমনকি যখন DUT নিজে zero-delay হলেও।
সাধারণ নীতি: simulation-এর সময় একক (#N) দুইটা ভিন্ন উদ্দেশ্যে কাজ করে — (১) simulation-এর event-গুলো সঠিক আপেক্ষিক ক্রমে ঘটানো (এইটুকু আসলেই জরুরি), আর (২) মানুষের কাছে একটা “সময়ের মতো” মনে হওয়া সংখ্যা দেখানো (যেটা বাস্তব চিপের speed-এর সাথে সরাসরি সম্পর্কহীন)। প্রথমটা আসল প্রয়োজন, দ্বিতীয়টা নিছক পাঠযোগ্যতা।
এরপর কী
ষোলো লেসন পর — Digital Logic শেষ
পিছনে তাকান। ষোলোটা লেসনে আমরা একটা on/off সুইচ থেকে একটা কার্যকরী, প্রোগ্রামযোগ্য CPU পর্যন্ত পুরো পথটা হেঁটেছি — একটাও ধাপ বাদ না দিয়ে।
একটা transistor একটা voltage-নিয়ন্ত্রিত সুইচ। কয়েকটা transistor মিলে CMOS-এ NOT, NAND, NOR — আর NAND universality-র প্রমাণ দেখাল একটা মাত্র গেট-টাইপ থেকেই যেকোনো Boolean function বানানো সম্ভব। Boolean simplification আর K-map সেই গেট-সংখ্যা কমাল। Adder যোগ করতে শিখল, subtractor আর comparator সেই একই adder-কে পুনর্ব্যবহার করে বিয়োগ আর তুলনা করল। Multiplexer আর decoder পথ বাছতে শিখল, আর সব মিলে একটা ALU-তে একত্র হলো — একটা মাত্র circuit যেটা op-select দেখে ADD/SUB/AND/OR/শিফট যেকোনোটা করতে পারে।
তারপর সময় ঢুকল ছবিতে। Feedback-এর মাধ্যমে latch আর flip-flop-এ প্রথমবার memory জন্ম নিল — একটা bit “মনে রাখা” সম্ভব হলো। Register সেই স্মৃতিকে N-বিটে বাড়াল, clock আর timing সবাইকে একই ছন্দে বাঁধল। FSM state আর transition দিয়ে আচরণ বর্ণনা করতে শেখাল, counter আর shift register সেই FSM-এর সবচেয়ে ব্যবহারিক রূপ দেখাল। Memory (SRAM/DRAM) লক্ষ লক্ষ bit সংরক্ষণের বাস্তবতা দেখাল — আর গত লেসনে, datapath আর control unit — এই সবগুলো টুকরো একটা bus system আর একটা FSM দিয়ে জুড়ে, প্রথমবারের মতো একটা সৎ CPU দাঁড় করাল।
আর আজ, শেষ ধাপ: HDL। আমরা শিখলাম hardware বর্ণনা করা যায় structural (গেট-বাই-গেট, তারের ডায়াগ্রামের অনুবাদ) বা behavioral (কী চাই বলা, কীভাবে করব tool-এর হাতে ছেড়ে দেওয়া) স্টাইলে। শিখলাম wire আর reg-এর আসল অর্থ নামের উপর না, ব্যবহারের উপর নির্ভর করে। শিখলাম কীভাবে একটা always @(posedge clk) একটা flip-flop হয়ে ওঠে, আর একটা state table কীভাবে যান্ত্রিকভাবে একটা case statement-এ রূপান্তরিত হয়। আর শিখলাম simulation (verify করা, hardware না বানিয়ে) আর synthesis (কোড থেকে সত্যিকারের গেট বানানো) — দুইটা সম্পূর্ণ ভিন্ন যন্ত্র, একই কোড পড়ে।
একটা মাত্র বাক্যে পুরো মডিউল: একটা CPU আসলে শুধু গেট — লক্ষ লক্ষ, বিলিয়ন, কোটি কোটি গেট — একটা state machine দিয়ে সুনির্দিষ্টভাবে wire করা, আর আজকের ইন্ডাস্ট্রি সেই wiring-টা হাতে না এঁকে বর্ণনা করে কোডে, যাতে industrial scale-এ কাজ করা যায়।
এই মডিউলে যা শিখলেন তার প্রতিটা টুকরোই — গেট, ALU, register, FSM, control unit, HDL — আসলে একটা প্রশ্নের প্রস্তুতি ছিল, যেটা আমরা এখনও সরাসরি জিজ্ঞেস করিনি: এই সব hardware ব্যবহার করে একটা প্রোগ্রাম কীভাবে চলে?
এতক্ষণ যা শিখলাম সেটা hardware কীভাবে বানানো হয় তা নিয়ে — গেট থেকে CPU পর্যন্ত। এখন থেকে প্রশ্নটা বদলে যায়: সেই hardware ব্যবহার করে একটা প্রোগ্রাম আসলে কীভাবে চলে?
পরের module — CPU Architecture — শুরু হবে এই প্রশ্ন দিয়ে:
a = b + cলিখলে সিলিকনের ভেতর ঠিক কী কী ঘটে?
সেখানে আমরা দেখব ISA — hardware আর software-এর মধ্যেকার contract — ঠিক কী। RISC বনাম CISC, আর কেন x86-64, ARM64, RISC-V তিনটা ভিন্ন দর্শন। এই মডিউলের register file-টাই সেখানে হয়ে উঠবে CPU-র general-purpose register, প্লাস একটা বিশেষ register — Program Counter (PC) — যেটা সবসময় “পরের instruction কোথায়” মনে রাখে। Instruction encoding ও decoding দেখাবে কীভাবে একটা বিট-প্যাটার্ন একটা নির্দিষ্ট অপারেশনে রূপান্তরিত হয় — ঠিক এই লেসনের toy ISA-র মতোই, শুধু বাস্তব স্কেলে। আর fetch-decode-execute cycle — যেটা গত লেসনে আমরা ইচ্ছাকৃতভাবে “বাইরে থেকে already fetched” বলে এড়িয়ে গিয়েছিলাম — সেটাই হবে Level 3-এর প্রথম পূর্ণাঙ্গ বিষয়, PC আর instruction memory-সহ সম্পূর্ণ।
এই মডিউলে বানানো control unit-টা যেভাবে datapath-কে চালিয়েছিল, ঠিক সেভাবেই একটা বাস্তব CPU-র control logic (হোক hardwired বা microcoded, লেসন ১৫-এর সেই পার্থক্য মনে করুন) fetch-decode-execute-এর প্রতিটা ধাপ চালায় — শুধু এবার আমাদের toy ৩-instruction ISA-র বদলে শত শত instruction, আর একটা মাত্র প্রোগ্রামের বদলে যেকোনো প্রোগ্রাম।
প্রশ্নটা এবার gate কীভাবে জোড়া লাগে তা নিয়ে না — সেই জোড়া-লাগা gate দিয়ে একটা প্রোগ্রাম কীভাবে “চলে”, সেটা নিয়ে।
আরও পড়ুন
- Verilog HDL: A Guide to Digital Design and Synthesis — Samir Palnitkar · Verilog-এর সবচেয়ে প্রচলিত পাঠ্যপুস্তক — synthesis-aware coding style-এর উপর জোর দেয়, এই লেসনের spirit-এর কাছাকাছি
- Icarus Verilog · এই লেসনের experiment-এ ব্যবহৃত free, open-source Verilog simulator
- EDA Playground · কোনো ইনস্টল ছাড়াই ব্রাউজারে Verilog লেখা, simulate, আর share করার টুল
- Yosys Open SYnthesis Suite · Open-source Verilog synthesis tool — RTL code থেকে gate-level netlist, এই লেসনের 'synthesis' অংশের হাতেকলমে প্রমাণ