آیا تا به حال هنگام طراحی و پیادهسازی مدارات دیجیتال با FPGA با سوالات زیر رو به رو شدهاید؟
برای تمامی این موارد میتوانید از شمارندهها استفاده کنید.
در این مقاله، به بررسی کاربردهای شمارندهها و نحوهی پیادهسازی آنها به کمک کد VHDL میپردازم. همچنین، یک پالس مربعی را به کمک شمارنده پیادهسازی میکنم.
More...
شمارنده یا counter، یکی از پرکاربردترین بلوکها در مدارات دیجیتال است. این بلوک به قدری در طراحی دیجیتال مهم است که در کلاسهای "طراحی دیجیتال با FPGA" و در مبحث شمارندهها آقای ثقفی همواره توصیهی زیر را مطرح میکنند:

اگر برای طراحی یک ماجول دیجیتال ایدهای به ذهنتان نمیرسد، به عنوان اولین راهحل به شمارنده فکر کنید؛ در بسیاری از موارد شمارنده میتواند مشکل شما را حل کند.
احمد ثقفی
اما شمارنده چه کاربردهایی دارد؟
کاربردهای شمارنده
به طور کلی، شمارندهها دو کاربرد اصلی دارند:
۱. زمانسنجی
اولین کاربرد شمارنده، زمانسنجی یا timing است. شما به کمک شمارنده میتوانید زمان را در یک مدار دیجیتال بسنجید و محاسبه کنید.
برای مثال، فرض کنید میخواهید در مدارتان، دو ثانیه بعد از رسیدن یک رجیستر خاص به مقدار ۱۰۰، عملیات دیگری انجام شود؛ مثلاً، موتوری حرکت کند یا یک چراغ روشن شود.
چگونه باید این دو ثانیه را در مدار دیجیتال محاسبه کنید؟ این کار را به کمک شمارنده انجام میدهیم.
و یا فرض کنید میخواهید پروتکل RS232 را پیادهسازی کنید.
پروتکل RS232، بیتها را به صورت سریال در خروجی ارسال میکند؛ هر کدام از این بیتها، عرض بیتی دارد که با توجه به baud rate یا سرعت پروتکل سریال مشخص میشود.
مثلاً فرض کنید هر بیت باید به مدت ۱۰۰ میکروثانیه در خروجی باشد. برای ایجاد این عرض بیتهای ۱۰۰ میکروثانیهای، نیاز به محاسبهی زمان دارید؛ این ۱۰۰ میکروثانیه را چگونه میتوانید حساب کنید؟ این کار هم به کمک شمارنده انجام میشود.
یکی از مهمترین کاربردهای شمارندهها زمانسنجی است که برای ایجاد تایمینگهای پروتکلهای مختلف، ایجاد تاخیر و بسیاری موارد دیگر به کار میرود.
۲. شمارش رخدادها
کاربرد دیگر شمارنده، شمارش رخدادها است.
برای مثال، فرض کنید نیاز است که در صورت تکرار یک رویداد به تعداد مشخص (مثلاً ۱۰ بار)، عملیات دیگری انجام شود؛ مثلاً، چراغی روشن شود.
شما چگونه میتوانید تعداد رخ دادن آن رویداد را بشمارید؟ بله، در این مورد هم میتوانید از یک شمارنده استفاده کنید.
اما روند به کارگیری شمارنده در این مثال به چه صورت است؟
ابتدا یک شمارنده تعریف میکنید. سپس شرطی تعریف میکنید که اگر آن رویداد اتفاق افتاد، یک واحد به شمارندهی شما اضافه شود.
آنگاه، در محل دیگری از مدار، یک شرط دیگر تعریف میکنید که اگر شمارنده برابر با ۱۰ شد، عملیات مورد نظر شما انجام شود.
دو کاربرد اصلی شمارندهها در مدارات دیجیتال:
در ادامه، ابتدا انواع شمارندهها را بررسی و پیادهسازی میکنم و سپس به کمک یک شمارنده، یک پالس مربعی تولید میکنم.
پیادهسازی شمارنده به زبان VHDL
در این بخش از مقاله، دو نوع شمارنده را پیادهسازی میکنیم:
پیادهسازی شمارندهی Free Running
سادهترین نوع شمارنده، اصطلاحاً free running counter نامیده میشود.
این نوع شمارنده، یک Clock و یک خروجی دارد که عمل شمارش، در خروجی آن انجام میشود.
شکل زیر، شمای کلی یک شمارنده free running را نشان میدهد:

شمای کلی یک شمارنده free running
برای مثال، اگر همانند شکل بالا، یک شمارنده چهار بیتی داشته باشیم، با هر کلاک ورودی، یک واحد به خروجی چهار بیتی آن اضافه میشود.
به عبارت دیگر، اگر فرض کنید که شمارش از صفر شروع شده باشد، در کلاک بعدی مقدار خروجی برابر با یک میشود. در کلاک بعد از آن، خروجی برابر با دو خواهد بود و به همین ترتیب شمارش ادامه دارد تا به مقدار ۱۵ برسد.
بیشترین مقداری که میتوان با چهار بیت نمایش داد، مقدار ۱۵ است. پس از رسیدن شمارنده به مقدار ۱۵، اگر به ورودی کلاک یک لبهی بالاروندهی دیگر اعمال شود، مقدار خروجی شمارنده به مقدار صفر باز میگردد و به همین ترتیب کار شمارش ادامه پیدا میکند.
به چنین شمارندهای، free running counter میگویند؛ یعنی شمارندهای که کنترلی بر آن نیست و با اعمال هر لبهی کلاک، یک واحد به آن اضافه میشود.
شما به راحتی میتوانید این شمارنده را به زبان VHDL و با یک خط کدنویسی ساده پیادهسازی کنید.
فرض کنید نام رجیستری که قرار است شمارنده را برای ما پیادهسازی کند، A است. برای پیادهسازی شمارنده چهاربیتی، کافی است که درون پراسس و درون شرط لبهی بالا رونده Clock، کد زیر را بنویسید:
A <= A + 1;
این کد، توصیفکنندهی یک شمارندهی free running است.
البته دربارهی رجیستری که در آن عمل شمارش را انجام میدهیم، جزئیاتی وجود دارد که در مثال بعدی به آن میپردازم.
اما ممکن است بخواهیم یک شمارندهی کاملتر، یعنی شمارندهای با امکان اعمال کنترلهایی روی آن، پیادهسازی کنیم.
در ادامهی مقاله، یک شمارنده با امکانات کنترلی را به کمک زبان VHDL پیادهسازی میکنم.
پیادهسازی شمارنده با امکانات کنترلی
شمارندهای که در شکل زیر مشاهده میکنید، نسبت به free running counter، دارای امکانات بیشتری است:

شمای کلی یک شمارندهی با امکانات کنترلی
قابلیتهای یک شمارنده با امکانات کنترلی:
اولین قابلیتی که در این شمارنده وجود دارد، جهت شمارش است.
میدانید که میتوانیم دو نوع شمارندهی افزایشی (up counter) و کاهشی (down counter) داشته باشیم. در این شمارنده، یک ورودی کنترلی تکبیتی وجود دارد که میتوان جهت شمارش را با تغییر مقدار آن تغییر داد.
قابلیت دیگری که این شمارنده میتواند داشته باشد، ورودی ریست است. شما به کمک ورودی ریست میتوانید در هر لحظه، شمارش را از ابتدا (مقدار صفر) شروع کنید.
اما چگونه میتوانیم یک شمارنده با امکانات کنترلی را در یک کد VHDL پیادهسازی کنیم؟
در کد زیر، کد VHDL پیادهساز یک شمارنده با امکانات کنترلی را میبینید:
library IEEE; use IEEE.STD_LOGIC_1164.ALL; use ieee.numeric_std.all; entity Up_Down_Counter is port ( Clock : in std_logic; Reset : in std_logic; Up_Down : in std_logic; Counter_Out : out unsigned (3 downto 0) ); end Up_Down_Counter; architecture Behavioral of Up_Down_Counter is signal Counter_Out_Int : unsigned(3 downto 0) := (others=>'0'); begin Counter_Out <= Counter_Out_Int; process(Clock) begin if rising_edge(Clock) then -- Up_Down = 0 => Up counter, Up_Down = 1 => Down counter. Counter_Out_Int <= Counter_Out_Int - 1; if (Up_Down = '0') then Counter_Out_Int <= Counter_Out_Int + 1; end if; if (Reset = '1') then Counter_Out_Int <= (others=>'0'); end if; end if; end process; end Behavioral;
در بخش Entity این کد، ورودیهای Clock، Reset و ورودی کنترلی Up_Down وجود دارد. تنها خروجی این مدار، پورت Counter_Out است.
من در این کد، شمارنده را به صورت چهاربیتی در نظر گرفتهام؛ بنابراین، خروجی را به صورت زیر تعریف کردهام:
Counter_Out : out unsigned (3 downto 0);
توجه داشته باشید که اصولاً شمارندهها را به صورت بدون علامت تعریف میکنیم؛ زیرا معمولاً در شمارش به دنبال اعداد منفی نیستیم.
در خط ۱۸ از کد، سیگنالی به نام Counter_Out_Int تعریف کردهام تا شمارش را روی این سیگنال انجام دهم.
در تعریف این سیگنال نکتهای وجود دارد:
من در این کد، بهجای انجام عمل شمارش روی پورت خروجی (یعنی پورت Counter_Out)، یک سیگنال داخلی به نام Counter_Out_Int تعریف کردهام.
این سیگنال از نوع بدون علامت و چهار بیتی است و همانطور که در کد مشاهده میکنید، عمل شمارش را روی این سیگنال داخلی انجام دادهام؛ در بخش کانکارنت و در خط ۲۲ از کد نیز، این سیگنال داخلی را به پورت خروجی مدار ارجاع دادهام.
اما چرا به این ترتیب عمل کردم؟
زیرا ماهیت پورت خروجی به گونهای است که فقط میتوانید در آن بنویسید و امکان خواندن از این پورت وجود ندارد.
به کد زیر که کد توصیفکنندهی یک شمارنده free running است توجه کنید:
A <= A + 1;
در این کد، سیگنال A، هم در سمت چپ و هم در سمت راست ارجاع وجود دارد؛ به عبارت دیگر، ما هم روی این سیگنال مینویسیم و هم از آن میخوانیم. اما برای یک پورت خروجی، به دلیل ماهیتش نمیتوانیم این کار را انجام دهیم.
بنابراین، برای تعریف یک شمارنده، باید یک سیگنال داخلی تعریف کنیم.
این سیگنال داخلی را به عنوان شمارنده پیادهسازی میکنیم و مانند خط ۲۲ از کد، به راحتی آن را در محیط کانکارنت به پورت خروجی مدار ارجاع میدهیم.
اکنون به بررسی کد درون پراسس میپردازم؛ در ابتدای پراسس و در خط ۲۹ از کد، یک کامنت با دو علامت منها (خط تیره) نوشتهام.
به طور کلی، دلیل نوشتن کامنت در کدها این است که هر زمان به این کد مراجعه کردم و یا کسی غیر از من به این کد مراجعه کرد، نحوهی عملکرد مدار قابل درک باشد.
مثلاً در این کامنت، من به این نکته اشاره کردهام که صفر بودن ورودی کنترلی Up_Down باعث شمارش به صورت افزایشی میشود و اگر ورودی Up_Down برابر با یک باشد، ماجول به صورت کاهشی میشمارد.
این کامنت به شفافیت کد کمک میکند و خوانندهی کد میتواند به راحتی آن را تفسیر کند.
پیشنهاد میکنم، از ابتدای کار کدنویسی، خود را به کامنتگذاری در کدها عادت دهید؛ این کار ممکن است در ابتدا وقتگیر به نظر برسد، اما در دراز مدت و به خصوص در مدارهای بزرگی که پیادهسازی خواهید کرد، به خوانایی و شفافیت کد شما کمک میکند.
به این ترتیب، در صورت نیاز به استفاده از بخشی از کد در مداری دیگر و یا در صورت نیاز به ایجاد تغییری در همان کد، به سرعت متوجه جزئیات کدی که قبلاً نوشتهاید میشوید و به راحتی میتوانید این کار را انجام دهید. به این ترتیب، در وقت شما نیز صرفهجویی خواهد شد.
کد توصیفکنندهی شمارندهی کاهشی
به عبارت زیر از خط ۳۰اُم کد و اولین سطر پراسس توجه کنید:
Counter_Out_Int <= Counter_Out_Int - 1;
در واقع در این خط کد، یک شمارندهی کاهشی تعریف کردهام؛ یعنی با هر کلاک، قرار است یک واحد از شمارنده کم شود.
به عبارت دیگر، فرض کردهام که ورودی کنترلی Up_Down برابر با یک است.
کد توصیفکنندهی شمارندهی افزایشی
در خطهای بعدی از کد، یعنی خطوط ۳۱ تا ۳۳، آمده است که اگر Up_Down برابر صفر باشد (یعنی اگر قرار است این شمارنده یک شمارندهی افزایشی باشد)، ارجاع به سیگنال Counter_Out_Int به صورت زیر انجام شود:
Counter_Out_Int <= Counter_Out_Int + 1;
بنابراین، به طور خلاصه، در پراسس کد پیادهساز یک شمارنده با امکانات کنترلی چنین اتفاقاتی رخ میدهد؛ در خط ۳۰، بدون هیچ شرطی فرض کردهام که شمارنده کاهشی است؛ در ادامه، شرط شمارندهی افزایشی را نوشتهام و با توجه به اینکه واقعاً Up_Down چه باشد، یکی از دو ارجاع کدهای شمارندهی کاهشی یا افزایشی، به عنوان آخرین ارجاع به سیگنال Counter_Out_Int اتفاق میافتد.
اگر Up_Down برابر با یک باشد، پس شرط برقرار نیست و آخرین ارجاع همان ارجاع شمارندهی کاهشی (خط ۳۰) خواهد بود.
اما اگر Up_Down برابر با صفر باشد، آخرین ارجاع به سیگنال Counter_Out_Int، ارجاع شمارندهی افزایشی (خط ۳۲) است و شمارش به صورت افزایشی انجام میشود.
پیادهسازی Reset برای شمارنده در کد VHDL
عبارات انتهای کد (خطوط ۳۵ تا ۳۷)، به صورت زیر است:
if (Reset = '1') then
Counter_Out_Int <= (others=>'0');
end if;
این عبارات به این معنی هستند که اگر Reset برابر با یک بود، سیگنال Counter_Out_Int برابر با صفر شود.
پس، اگر Reset برابر با یک باشد، فارغ از اینکه مقدار Up_Down چند است، سیگنال Counter_Out_Int صفر میشود؛ زیرا آخرین ارجاع به این سیگنال، ارجاع موجود در خط ۳۶ است که مقدار صفر را به آن ارجاع میدهد.
در ادامه، یک مثال مهم از کاربرد شمارندهها را بررسی خواهم کرد.
تولید یک پالس مربعی به کمک شمارنده
قصد دارم به کمک شمارنده، یک پالس مربعی پیادهسازی کنم. فرض میکنیم فرکانس کلاک اعمالی به مدار ۱۰۰ مگاهرتز باشد؛ بنابراین بر اساس رابطهی زیر، دورهی تناوب هر پالس کلاک، ۱۰ نانوثانیه خواهد بود.
T = 1 / f = 10 nsec
فرض کنید میخواهیم پالسی با دورهی تناوب ۲۰۰ نانوثانیه و duty_cycle برابر ۵۰ درصد داشته باشیم. منظور از duty_cycle، نسبت مدت زمان یک بودن پالس به پریود آن است.
کد زیر، پیاده کنندهی چنین پالسی است که در ادامهی مطلب آن را بررسی میکنم:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Pulse is
Generic (
Pulse_Period : integer := 200;--nSec --f_Clock=100MHZ
Pulse_Width : integer := 100 --nSec
);
Port (
Clock : in std_logic;
Pulse : out std_logic
);
end Pulse;
architecture Behavioral of Pulse is
signal Pulse_Int : std_logic := '0';
signal Counter : unsigned (4 downto 0) := (others=>'0');
begin
Pulse <= Pulse_Int;
process(Clock)
begin
if rising_edge(Clock) then
Pulse_Int <= '1';
Counter <= Counter + 1;
if ( Counter > ( to_unsigned(Pulse_Width/10 - 1,5) ) ) then
Pulse_Int <= '0';
end if;
if ( Counter = to_unsigned( Pulse_Period/10-1,5 ) ) then
Counter <= (others=>'0');
end if;
end if;
end process;
end Behavioral;
در بخش entity کد، دو بخش Generic و Port را داریم؛ در بخش Generic، دو مقدار دورهی تناوب پالس (Pulse_Period) و مدت زمان یک بودن آن (Pulse_Width) را بر حسب نانوثانیه مشخص کردهایم. در بخش پورتها، پورت ورودی کلاک (Clock) و پورت خروجی پالس مربعی (Pulse) را داریم.
حال به بررسی بخش Architecture میپردازیم؛ در قسمت معرفی (Declaration)، دو سیگنال داخلی تعریف کردهایم. سیگنال داخلی Pulse_Int، برای رعایت الگوی استاندارد کدنویسی و رجیستر کردن خروجی تعریف شده است و سیگنال داخلی Counter یک شمارنده است که میخواهیم به کمک آن پالس مربعی را پیادهسازی کنیم.
برای اطلاعات بیشتر در مورد الگوی استاندارد کدنویسی برای FPGA، این برنامه را مشاهده کنید...
برای رجیستر کردن خروجی، در قسمت کانکارنت، همانطور که در خط ۲۵ از کد میبینید، سیگنال داخلی Pulse_Int را به پورت خروجی Pulse ارجاع میدهیم. از این پس، در مدار اصلی هر گاه به پورت خروجی نیاز داشتیم از سیگنال داخلی Pulse_Int استفاده میکنیم.
با توجه به اینکه دورهی تناوب سیگنال کلاک ۱۰ نانوثانیه است، باید مدت زمان یک بودن پالس مربعی (۱۰۰ نانوثانیه) را بر ۱۰ نانوثانیه تقسیم کنیم تا ببینیم به اندازهی چند سیگنال کلاک باید مقدار یک در خروجی ظاهر شود.
همچنین، با تقسیم دورهی تناوب پالس مربعی (۲۰۰ نانوثانیه) بر ۱۰ نانوثانیه متوجه میشویم که کل پالس مربعی به اندازهی چند سیگنال کلاک طول خواهد کشید.
بنابراین، با توجه به مقادیر این مثال، مقدار خروجی باید به مدت ۱۰ پالس کلاک برابر با یک باشد (از مقدار صفر سیگنال Counter تا مقدار نُه) و به مدت ۱۰ پالس کلاک دیگر (از مقدار ۱۰ سیگنال Counter تا مقدار ۱۹) سیگنال خروجی باید صفر باشد.
طبق الگوی استاندارد کدنویسی، مدارمان را کاملاً سنکرون پیادهسازی میکنیم؛ به این ترتیب که کل مدار را در شرط لبهی بالاروندهی کلاک قرار میدهیم.
ابتدا در خط ۳۲ از کد، مقدار یک را به سیگنال داخلی Pulse_Int ارجاع میدهیم، سپس در خط ۳۴، یک واحد به مقدار Counter اضافه میکنیم. آنگاه، در شرط if در خطهای ۳۶ تا ۳۸ قید میکنیم که در صورتی که شمارنده بزرگتر از مقدار (Pulse_Width/10 – 1) بود (در این مثال این مقدار برابر با نُه است)، مقدار خروجی برابر با صفر شود.
به این ترتیب، اگر شرط موجود در خط ۳۶ از کد برقرار نباشد، آخرین ارجاع به سیگنال Pulse_Int، ارجاع مقدار یک به این سیگنال در خط ۳۲ از کد خواهد بود؛ پس، همین مقدار به آن اعمال میشود. اما اگر شرط خط ۳۶ از کد برقرار باشد، آخرین ارجاع به سیگنال Pulse_Int، ارجاع خط ۳۷ با مقدار صفر خواهد بود؛ بنابراین، مقدار صفر به پالس Pulse_Int ارجاع میشود.
در شرط if در خطهای ۴۰ تا ۴۲، قید شده است که اگر مقدار شمارنده برابر با مقدار (Pulse_Period/10-1) بود (که در این مثال این مقدار برابر با ۱۹ است)، شمارنده ریست شود تا از ابتدا بشمارد.
به این ترتیب، در این مثال، به مدت ۱۰ پالس کلاک که برابر با ۱۰۰ نانوثانیه میشود، مقدار خروجی یک است و به مدت ۱۰ پالس کلاک بعدی، مقدار خروجی صفر خواهد بود و پالس مربعی به درستی تولید میشود.
گرچه مثالهای این مقاله، مثالهای کوچکی هستند، اما کدهای بزرگی هم که به زودی خواهید نوشت، از همین بخشهای کوچک تشکیل شدهاند.
درک صحیح نحوهی پیادهسازی بخشهای کوچک یک سیستم باعث میشود بتوانید کدهای پیچیدهتر و بزرگتر را به راحتی و به صورت بهینه پیادهسازی کنید. پس، این مثالهای کوچک را دست کم نگیرید و سعی کنید آنها را مرور کنید و برای خودتان بازنویسی و تفسیر کنید تا تسلط لازم را برای پیادهسازی کدهای بزرگتر به دست بیاورید.
امیدوارم از خواندن این مقاله هم لذت برده باشید و بتوانید از نکات موجود در آن، در انجام پروژههایتان استفاده کنید.
این مقاله، برگرفته از دوره طراحی دیجیتال با FPGA بود.
برای اطلاع از جزئیات این دوره، روی دکمه زیر کلیک کنید:
لطفا نظرتان را در مورد این برنامه در پایین همین پست با دیگران به اشتراک بگذارید. همچنین با کلیک روی هر کدام از دکمههای اشتراک گذاری ابتدای این مطلب و به اشتراکگذاری آن در شبکههای اجتماعی میتوانید افراد بیشتری را در یادگیری این مطالب سهیم کنید.

