পাঠ ১৬ · ৫৭-এর মধ্যে · মডিউল ৩
Home / Courses / Computer Architecture & Digital Logic / কাউন্টার

কাউন্টার — বাইনারি ও রিং কাউন্টার

Counters — binary & ring
৮ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • বাইনারি কাউন্টারের ripple ডিজাইন এবং কেন এটা L09-এর ripple-carry adder-এর মতোই "প্রোপাগেট" করে
  • modulo wraparound — একটা N-বিট কাউন্টার কীভাবে $2^N$ ছাড়িয়ে গেলে 0-তে ফিরে আসে
  • রিং কাউন্টারের ফিডব্যাক গঠন এবং কেন এটা $2^N$ নয়, N states নিয়ে চক্র সম্পন্ন করে
  • Python-এ উভয় কাউন্টার বাস্তবায়ন করে ম্যাক্সিমাম ভ্যালুর বেশি টিক করিয়ে wraparound ও পূর্ণ চক্র সরাসরি ট্রেস করে যাচাই

১ · কাউন্টার কী

কাউন্টারCounterএকটা সিকোয়েনশিয়াল সার্কিট যা প্রতিটা ক্লক টিকে একটা নির্দিষ্ট, সসীম state sequence-এর মধ্য দিয়ে চক্রাকারে এগিয়ে চলে। হলো এমন একটা সিকোয়েনশিয়াল সার্কিট যা প্রতিটা ক্লক টিকে একটা নির্দিষ্ট state sequence-এর মধ্য দিয়ে চক্রাকারে এগিয়ে চলে — সবচেয়ে পরিচিত রূপ হলো বাইনারিতে গণনা বাড়ানো (বা কমানো)। এই পাঠে দুটো ভিন্ন "গণনা করার ধরন" দেখব — একটা সংখ্যাগত গণনার জন্য, আরেকটা পালাক্রমিক (round-robin) সিকোয়েন্সিং-এর জন্য।

২ · বাইনারি কাউন্টার — T ফ্লিপ-ফ্লপ চেইন

একটা N-বিট বাইনারি আপ-কাউন্টার প্রতি ক্লক টিকে তার সঞ্চিত বাইনারি মান ১ বাড়ায়, এবং সর্বোচ্চ মান ($2^N - 1$) পার হলে আবার 0-তে wrap করে। ক্লাসিক "ripple counter" ডিজাইনে প্রতিটা T ফ্লিপ-ফ্লপের ক্লক ইনপুট আসে আগের ফ্লিপ-ফ্লপের আউটপুট থেকে — একটা বিট শুধু তখনই টগল করে যখন তার ঠিক আগের বিট 1 থেকে 0-এ রোল-ওভার করে, ঠিক যেভাবে হাতে বাইনারি যোগ করার সময় carry এক ঘর থেকে পরের ঘরে ছড়িয়ে যায়। এই propagation প্যাটার্নটা L09-এর ripple-carry adder-এর carry ছড়িয়ে যাওয়ার সাথে সরাসরি সাদৃশ্যপূর্ণ।

Modulo wraparound

N-বিট বাইনারি কাউন্টার আসলে mod $2^N$ গণনা করে — অর্থাৎ মান সবসময় $(value + 1) \bmod 2^N$ সূত্র মেনে চলে। এই কারণেই একটা 3-বিট কাউন্টার 7 (সর্বোচ্চ মান) পার হয়ে আবার 0-তে ফিরে আসে, ঠিক যেমন একটা ঘড়ির কাঁটা 12-এর পর আবার 1-এ ফিরে আসে।

৩ · রিং কাউন্টার — ফিডব্যাক-সহ শিফট রেজিস্টার

রিং কাউন্টারRing Counterএকটা শিফট রেজিস্টার যার আউটপুট প্রান্ত আবার নিজের ইনপুট প্রান্তে ফিরিয়ে দেওয়া হয়, একটামাত্র 1 বিট নিয়ে শুরু করে — প্রতি টিকে সেই একক 1 বিট এক ঘর করে ঘুরতে থাকে। বাইনারি কাউন্টারের থেকে সম্পূর্ণ ভিন্ন কৌশলে গণনা করে — এটা L15-এর শিফট রেজিস্টারকেই নেয়, কিন্তু তার আউটপুট প্রান্তকে আবার নিজের ইনপুট প্রান্তে ফিরিয়ে দেয় (ফিডব্যাক), এবং শুরু করা হয় ঠিক একটামাত্র 1 বিট দিয়ে (বাকি সব 0)। প্রতিটা ক্লক টিকে সেই একক 1 বিট রিং-এর মধ্যে এক ঘর করে সরে যায়। এভাবে একটা N-বিট রিং কাউন্টার $2^N$ নয়, বরং ঠিক N states-এর মধ্যে চক্র সম্পন্ন করে — প্রতিটা ফ্লিপ-ফ্লপ পজিশনের জন্য একটা করে state। এটা সংখ্যাগত গণনার জন্য নয়, বরং সরল পালাক্রমিক সিকোয়েন্সিং-এর জন্য দরকারি — যেমন N-টা আউটপুট লাইনকে একটা একটা করে পালাক্রমে সক্রিয় করা।

বাইনারি কাউন্টার: প্রতিটা T ফ্লিপ-ফ্লপ পরের বিটে সাড়া দেয় শুধু আগের বিট 1→0 হলে -- $2^N$ states রিং কাউন্টার: একক 1 বিট শিফট রেজিস্টারে ফিডব্যাক-সহ ঘোরে -- ঠিক N states দুটোই FSM-এর নির্দিষ্ট উদাহরণ -- states = সম্ভাব্য কাউন্ট মান (L17-এ সাধারণীকৃত)
দুটো ভিন্ন "গণনা" কৌশল — একটা সংখ্যাগতভাবে বাড়ে, আরেকটা একটামাত্র বিট ঘুরিয়ে পালাক্রমে চলে।

৪ · কোড সেলে যাচাই — genuine wraparound ও পূর্ণ চক্র

নিচের কোডে BinaryCounter-কে তার সর্বোচ্চ মানের বেশি টিক করিয়ে wraparound সরাসরি দেখানো হচ্ছে, আর RingCounter-কে ঠিক n_bits বার টিক করিয়ে যাচাই করা হচ্ছে এটা সত্যিই তার শুরুর অবস্থায় ফিরে আসে কি না।

Python
class BinaryCounter:
    """T ফ্লিপ-ফ্লপ চেইন দিয়ে তৈরি N-বিট আপ-কাউন্টার -- প্রতি tick()-এ মান ১ বাড়ে, 2**n_bits-এ wrap করে"""
    def __init__(self, n_bits):
        self.n_bits = n_bits
        self.modulus = 2 ** n_bits
        self.value = 0

    def tick(self):
        self.value = (self.value + 1) % self.modulus
        return self.value

    def bits(self):
        return [int(b) for b in format(self.value, f"0{self.n_bits}b")]


class RingCounter:
    """শিফট রেজিস্টারে একটামাত্র 1 বিট ফিডব্যাক-সহ ঘোরানো হয় -- n_bits বার টিকে ঠিক একবার পূর্ণ চক্র শেষ হয়"""
    def __init__(self, n_bits):
        self.n_bits = n_bits
        self.bits = [0] * n_bits
        self.bits[0] = 1   # শুরুর একক 1

    def tick(self):
        dropped = self.bits[-1]
        self.bits = [dropped] + self.bits[:-1]   # শিফট, পড়ে যাওয়া বিটই ফিডব্যাক হয়ে সামনে ঢোকে
        return list(self.bits)


print("BinaryCounter -- 3-বিট কাউন্টার, সর্বোচ্চ মান (7) পার হয়ে wraparound দেখানো হচ্ছে")
print("-" * 62)
bc = BinaryCounter(3)
print(f"শুরুর মান: {bc.value}  বিট: {bc.bits()}")
for tick in range(1, 11):
    v = bc.tick()
    note = "  <- wrap! (7 এর পর আবার 0)" if v == 0 else ""
    print(f"টিক {tick:2d}: মান={v}  বিট={bc.bits()}{note}")

assert bc.value == 10 % 8, "BinaryCounter-এর wraparound গণনা ভুল!"
print(f"যাচাই: ১০ বার tick()-এর পর মান হওয়া উচিত 10 % 8 = {10 % 8}, পাওয়া গেছে {bc.value} -- মিলেছে")

print()
print("RingCounter -- 4-বিট রিং, n_bits=4 বার টিকের পর ঠিক শুরুর অবস্থায় ফিরে আসা উচিত")
print("-" * 62)
rc = RingCounter(4)
start_state = list(rc.bits)
print(f"শুরুর অবস্থা: {start_state}")
for tick in range(1, 5):
    state = rc.tick()
    print(f"টিক {tick}: {state}")

assert rc.bits == start_state, "৪ টিকের পর RingCounter শুরুর অবস্থায় ফেরত আসেনি!"
print(f"যাচাই: {rc.n_bits} বার tick()-এর পর অবস্থা = {rc.bits}, শুরুর অবস্থা = {start_state} -- ঠিক মিলেছে (পূর্ণ চক্র শেষ)")

    
দুটো assert লাইনই নিছক দাবি নয় — প্রতিবার কোড সত্যিই চালিয়ে ফলাফল যাচাই করছে। BinaryCounter-এ ১০ বার tick()-এর পর মান আসলেই $10 \bmod 8 = 2$-এর সাথে মিলছে (৭-এর পর একবার wrap ঘটেছে)। RingCounter-এ ঠিক ৪ বার tick()-এর পর (n_bits=4) বিট প্যাটার্ন সত্যিই শুরুর [1, 0, 0, 0]-এ ফিরে এসেছে — একটাও বেশি বা কম টিক লাগেনি।
মূল কথা · Key takeaway

বাইনারি কাউন্টার আর রিং কাউন্টার দুটোই "চক্রাকারে state বদলানো" এই একই মূলনীতি থেকে এসেছে, কিন্তু একদম ভিন্ন উদ্দেশ্যে — একটা সংখ্যাগত গণনার জন্য ($2^N$ states), আরেকটা পালাক্রমিক সিকোয়েন্সিং-এর জন্য (ঠিক N states)। পরের পাঠে (L17) দেখবেন এই দুটোই আসলে ফাইনাইট স্টেট মেশিন-এর নির্দিষ্ট উদাহরণ মাত্র — এবং M3-এর সবকিছু (ল্যাচ, ফ্লিপ-ফ্লপ, রেজিস্টার, কাউন্টার) এই একই সাধারণ কাঠামোর নিচে একত্রিত হবে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ বাইনারি কাউন্টারের ripple ডিজাইনে প্রতিটা ফ্লিপ-ফ্লপ আগেরটার আউটপুট থেকে ক্লক পায় — এতে কোনো সমস্যা হতে পারে কি?

হ্যাঁ, একটা গুরুত্বপূর্ণ বাস্তব সীমাবদ্ধতা আছে — যেহেতু প্রতিটা বিট তার আগের বিটের টগল হওয়ার জন্য অপেক্ষা করে, একটা বড় N-বিট কাউন্টারে সবচেয়ে বাম বিট পর্যন্ত পরিবর্তন "পৌঁছাতে" ছোট ছোট বিলম্ব পরপর যোগ হয়ে একটা লক্ষণীয় মোট বিলম্ব তৈরি করে — ঠিক যেমন L09-এর ripple-carry adder-এ carry প্রোপাগেশন বিলম্ব তৈরি করে। এই কারণেই বাস্তব উচ্চ-গতির সিস্টেমে "synchronous counter" নামের একটা বিকল্প ডিজাইন ব্যবহৃত হয়, যেখানে সব ফ্লিপ-ফ্লপ একই ক্লক সরাসরি শেয়ার করে, শুধু অতিরিক্ত কম্বিনেশনাল লজিক দিয়ে ঠিক করা হয় কোনটা টগল করবে — এই কোর্সের সিলেবাসের বাইরে হলেও নীতিটা জানা ভালো।

প্র ০২ রিং কাউন্টার যদি শুধু N states-ই দিতে পারে, তাহলে $2^N$ states দেওয়া বাইনারি কাউন্টারের চেয়ে এটা কম কার্যকর নয় কি?

"কার্যকারিতা" এখানে উদ্দেশ্যের উপর নির্ভর করে, রাশির উপর নয়। যদি লক্ষ্য হয় একটা সংখ্যা গণনা করা (যেমন কতবার একটা ইভেন্ট ঘটেছে), তাহলে বাইনারি কাউন্টারই দরকার — বেশি states মানেই বেশি তথ্য এনকোড করা যায়। কিন্তু যদি লক্ষ্য হয় শুধু N-টা জিনিসের মধ্যে ঘুরে ঘুরে একটাকে "সক্রিয়" চিহ্নিত করা (যেমন ৪টা LED-কে পালাক্রমে জ্বালানো, বা ৪টা ডিভাইসকে round-robin ভিত্তিতে সার্ভিস দেওয়া), তাহলে রিং কাউন্টার সরাসরি এবং decoder ছাড়াই কাজটা করে দেয় — প্রতিটা বিট position সরাসরি একটা আউটপুট লাইনের সাথে যুক্ত থাকতে পারে, কোনো অতিরিক্ত ডিকোডিং লজিক (L11) লাগে না।

প্র ০৩ কোড সেলে RingCounter-এর tick() মেথড dropped = self.bits[-1] নিয়ে সেটাই আবার সামনে বসিয়ে দেয় — এই "ফিডব্যাক" জিনিসটা ঠিক কোথায় ঘটছে?

ফিডব্যাকটা ঠিক এই লাইনেই ঘটছে — সাধারণ একটা শিফট রেজিস্টারে (L15) সবচেয়ে শেষ বিট "পড়ে যায়" এবং চিরতরে হারিয়ে যায় (নতুন বিট বাইরে থেকে ঢোকে)। কিন্তু এখানে dropped-কে বাইরে থেকে নতুন কোনো মান না নিয়ে, ঠিক সেই পড়ে-যাওয়া বিটটাকেই আবার তালিকার শুরুতে বসানো হচ্ছে ([dropped] + self.bits[:-1]) — এটাই "আউটপুট প্রান্ত আবার ইনপুট প্রান্তে ফিরিয়ে দেওয়া" এই সংজ্ঞাটার সরাসরি কোড-বাস্তবায়ন। এই একটা পরিবর্তনই একটা সাধারণ শিফট রেজিস্টারকে রিং কাউন্টারে রূপান্তরিত করে দেয়।

অনুশীলন

  1. হাতে ট্রেস করুন: একটা 2-বিট বাইনারি কাউন্টার Value=0 দিয়ে শুরু হয়। ৬ বার tick()-এর পর মান কত হবে হাতে বের করুন (মডুলাস কত হবে সেটাও লিখুন), এবং কোন কোন টিকে wraparound ঘটবে তা চিহ্নিত করুন।

    2-বিট কাউন্টারের মডুলাস $2^2 = 4$। মানের ক্রম: টিক ১→1, টিক ২→2, টিক ৩→3, টিক ৪→(3+1) মড 4 = 0 (এখানে প্রথম wraparound), টিক ৫→1, টিক ৬→2। তাই ৬ বার tick()-এর পর মান 2। এটা সরাসরি $6 \bmod 4 = 2$-এর সাথে মেলে, যেমন কোড সেলের 3-বিট উদাহরণেও একই সূত্র ব্যবহৃত হয়েছিল।

  2. চিন্তা করুন: যদি একটা RingCounter শুরুর অবস্থায় দুটো 1 বিট দিয়ে শুরু হতো (যেমন [1, 1, 0, 0], একটা নয়) — তাহলে এটা কি এখনো "রিং কাউন্টার" হিসেবে সঠিকভাবে কাজ করবে?

    টেকনিক্যালি শিফট আর ফিডব্যাক মেকানিজম একই থাকবে এবং এখনো n_bits টিকেই একটা পূর্ণ চক্র সম্পন্ন হবে, কিন্তু এটা তখন প্রচলিত সংজ্ঞার "রিং কাউন্টার" (একটা মাত্র সক্রিয় বিট) থাকবে না — বরং এটাকে বলা হয় "Johnson counter"-এর একটা ভ্যারিয়েশন বা দুই-বিট প্যাটার্নের রিং। রিং কাউন্টারের আসল ব্যবহারিক সুবিধা (ঠিক একটা আউটপুট লাইন সবসময় সক্রিয়, তাই সরাসরি "কোনটা এখন সক্রিয়" জানার জন্য ডিকোডিং লাগে না) দুটো 1 বিট দিয়ে শুরু করলে নষ্ট হয়ে যায় — তাই ব্যবহারিক ডিজাইনে ঠিক একটা 1 দিয়েই শুরু করা হয়, যেমন কোড সেলে দেখানো হয়েছে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
রেজিস্টার ও শিফট রেজিস্টার