آموزش VHDL برای FPGA

در این آموزش قصد داریم شما را با زبان طراحی سخت‌افزار VHDL آشنا کنیم. هدف اصلی ما کمک به شما در طراحی مدارهای دیجیتال خواهد بود. بنابراین مطالبی که آماده کرده‌ایم شامل دو بخش است، در ابتدا معرفی و آشنایی سریعی با زبان VHDL و پس از آن مرور دقیق تمام جزئیاتی را خواهیم داشت که در یادگیری این زبان به آنها احتیاج دارید. با این حال اگر پس از پایان مطالعه‌ی این آموزش، سوال یا ابهامی داشتید که ضمن مطالعه‌ی این مطلب پاسخ آن را دریافت نکرده‌ بودید، لیستی از منابع مفید در این زمینه را نیز برای شما در قسمت مراجع آماده کرده‌ایم که می‌توانید به آن‌ها مراجعه کنید.

1. معرفی زبان VHDL

VHDL برگرفته از عبارت VHSIC Hardware Description Language است که VHSIC خود کوتاه‌ شده‌ی عبارت Very High Speed Integrated Circuits به معنای آیسی‌های فوق سریع است. بنابراین VHDL را می‌توان زبان توصیف سخت‌افزاری برای آیسی‌های فوق سریع معنا کرد.

در اواسط دهه 1980 وزارت دفاع آمریکا با همکاری IEEE، این زبان توصیف سخت‌افزاری را با هدف طراحی چیپ‌های الکترونیکی با سرعت بسیار بالا توسعه دادند و امروزه این زبان به یک زبان استاندارد برای طراحی و توسعه سیستم‌های دیجیتال تبدیل شده است که نه تنها در کاربردهای نظامی، بلکه در صنایع تجاری نیز بسیار متداول است. در کنار VHDL، زبان توصیف سخت‌افزاری دیگری به نام Verilog نیز وجود دارد که آن نیز بسیار پرکاربرد است.

هم وریلاگ و هم VHDL هر دو زبان‌های قدرتمندی هستند که به شما این امکان را می‌دهند که مدارها و سیستم‌های دیجیتالی پیچیده را به راحتی طراحی و سیموله کنید.

بد نیست همین‌جا این را هم اضافه کنیم که در کنار این دو، یک زبان سومی هم وجود دارد به نام ABEL که به طور خاص برای طراحی PLDها ایجاد شده است و البته نسبت به Verilog و VHDL قدرت کمتری دارد و در صنعت نیز استفاده از آن چندان متداول نیست.

ما در این آموزش به دو زبان ABEL و Verilog کاری نداریم و تنها به بررسی VHDL، مطابق با استاندارد 1076-1993 IEEE می‌پردازیم.

زبان‌های توصیف سخت‌افزاری در ظاهر ممکن است شباهت‌های زیادی به زبان‌های برنامه‌نویسی معمولی داشته باشند، اما در واقع تفاوت‌های بسیار مهمی میان آنها وجود دارد. مثلا اینکه این زبان‌ها ماهیتاً موازی (parallel) هستند. به این معنا که به محض رسیدن یک ورودی جدید، دستورات آنها که در واقع توصیف‌‌کننده‌ی گیت‌های منطقی هستند، به صورت موازی و همزمان اجرا (کامپایل) می‌شوند.

برای فهم دقیق‌تر، می‌توان گفت که یک برنامه‌ی توصیف سخت‌افزار (HDL Program)، تلاشی برای توصیف رفتار و عملکرد یک سیستم فیزیکی (سخت‌افزاری) و معمولا دیجیتال است. به این ترتیب که اجزای مختلف یک مدار فیزیکی به وسیله‌ی توصیف رفتار آن سیستم و اتصالات مختلف موجود میان اجزا، ساخته می‌شوند و در نهایت آن سیستم را تولید می‌کنند. از طرفی حتی می‌توان به کمک این زبان‌ها، ملاحظات زمانی (timing) مدارها را نیز اعمال و محاسبه نمود.

2. سطوح مختلف توصیف و نمایش یک مدار

یک سیستم دیجیتال را می‌توان درسطوح مختلفی از انتزاع نمایش داد. به این ترتیب طراحی و توصیف سیستم‌های پیچیده را می‌توان به خوبی مدیریت کرد. در تصویر زیر سطوح مختلف انتزاع را می‌بینیم.

آموزش VHDL Primer
سطوح مختلف انتزاع یک مدار: فیزیکی، ساختاری و رفتاری

بالاترین سطح انتزاع سطح رفتاری (behavioral) است که در آن مدار را در قالب جملات و عباراتی که عملکرد آن را توضیح می‌دهند، توصیف می‌کنیم. بنابراین در اینجا دیگر پای رسم اجزای مدار و اتصالات میان آنها در میان نیست، بلکه تنها رفتار مدار را توصیف می‌کنیم. این توصیف رفتار، در واقع توصیف و توضیح چگونگی رابطه‌ی میان ورودی‌ها و خروجی‌هاست. این کار را می‌توانیم با استفاده از عبارت‌های بولین، قوانین الگوریتم‌ها و توصیفاتی در سطح RTL انجام دهیم.

به عنوان مثال، بیایید یک سیستم ساده را در نظر بگیریم. سیستم اعلام هشدار یک خودرو در زمان باز بودن درها و یا بسته نبودن کمربند ایمنی مسافران، به محض قرار گرفتن سوییچ در محفظه‌ی احتراق. رفتار این سیستم را می‌توانیم به این صورت توصیف کنیم.

Warning = Ignition_on AND ( Door_open  OR Seatbelt_off)

یعنی هر زمان که «خودرو روشن شد» و «درها باز بود » یا «کمربند بسته نشده بود»، هشدار اعلام شود.

از سوی دیگر، انتزاع یک مدار در سطح ساختاری شامل توصیف آن سیستم به صورت مجموعه‌ای از گیت‌ها و اجزای منطقی است که با اتصالاتی مشخص در کنار هم قرار گرفته‌اند تا هدف مشخصی را برآورده سازند. بنابراین توصیف ساختاری را می‌توان چیزی شبیه شماتیکی مداری از گیت‌های منطقی در نظر گرفت. این نوع نمایش در واقع نزدیک‌ترین حالت به چیزی است که در نهایت و در عمل به صورت فیزیکی از یک سیستم بر روی سخت‌افزار پیاده می‌شود. مثلا در مورد مثال قبلی، توصیف در سطح ساختاری را می‌توانیم مانند تصویر زیر داشته باشیم.

آموزش VHDL Primer
توصیف ساختاری مدار اعلام هشدار خودرو

با استفاده از زبان VHDL می‎‌توان سیستم‌های دیجیتال را هم در سطح رفتاری و هم در سطح ساختاری توصیف کرد. خود توصیف رفتاری می‌تواند به دو گونه انجام شود، توصیف به صورت جریان داده یا توصیف الگوریتمی.

در روش توصیف به کمک جریان داده، همانطور که از نام آن می‌توان حدس زد، رفتار مدار را مبتنی بر جریانی که داده از ورودی تا خروجی می‌پیماید توصیف می‌کنیم. از این روش معمولا در توصیف جریان داده بین رجیسترها (RTL level) استفاده می‌شود. عبارت‌هایی که در توصیف این مسیر نوشته می‌شوند، به محض رسیدن یک ورودی جدید، همگی به صورت همزمان و موازی اجرا می‌شوند. البته این حرف به این معنا نیست که در زبان VHDL نمی‌توان فرآیند‌های ترتیبی را توصیف کرد. اگر چنانچه فرآیندی ماهیتی ترتیبی داشته باشد، عبارات داخل آن بخش به صورت ترتیبی اجرا خواهند شد اما کلیت آن بلوک نیز مانند سایر بلوک‌ها به صورت موازی کامپایل می‌شود. اگر کمی گیج شده‌اید نگران نباشید، در ادامه مثال‌هایی از این حالت‌ها را با هم بررسی خواهیم کرد.

3. ساختار ساده‌ی یک فایل VHDL

زمانی که یک سیستم دیجیتال را در زبان VHDL توصیف می‌کنیم، این طراحی احتمالا شامل یک entity یا ماژول است که خود این ماژول شامل ماژول‌های کوچکتر است. به آن entity اولیه که سایر entityها را در خود جای می‌دهد، top-level entity می‌گوییم. ساختار هر entity به این صورت است که در بخش ابتدایی آن یک عبارت شفاف‌سازی و معرفی (Entity Declaration) وجود دارد به این منظور که مشخص شود این تکه کد، توصیف کننده‌ی چه چیزی است و پس از آن دارای بخشی است که در آن معماری عملکرد آن entity توصیف می‌شود (Architecture body). بخش اول را می‌توان همانند اینترفیس این ماژول با سایر ماژول‌‌ها در نظر گرفت چرا که سیگنال‌های ورودی و خروجی را در آن قسمت مشخص می‌کنیم. اما در بخشی که مربوط به معماری است و بدنه‌ی آن ماژول محسوب می‌شود، توصیف اجزا، عملکرد، اتصالات و فرآیند‌هایی که قرار است در هر ماژول رخ بدهد را داریم. این دو بخش به صورت شماتیکی در تصویر زیر نشان داده شده‌اند. در طراحی یک سیستم دیجیتال معمولا تعداد بسیار زیادی از این entity‌ها یا ماژول‌ها وجود دارند که به هم متصل می‌شوند و در مجموع و در کنار هم عملکردی که برای آن سیستم مدنظر بوده است را تولید می‌کنند.

آموزش VHDL Primer
یک VHDL entity شامل دو بخش بدنه (توصیف معماری) و اینترفیس است.

VHDL نیز مانند هر زبان برنامه‌نویسی دیگری، دارای کلمات کلیدی از پیش تعیین شده‌ای است که ما نمی‌توانیم از آنها به عنوان نام سیگنال‌ها یا … استفاده‌ کنیم. نسبت به بزرگ یا کوچک نوشتن حروف نیز چه در کلمات کلیدی و چه در نام‌گذاری‌های خود کاربر حساس نیست. اگر بخواهیم جایی توضیح (command) بنویسیم، کافیست در ابتدای آن جمله دو تا علامت خط تیره‌ی پشت سر هم قرار دهیم (- -) به این ترتیب کامپایلر متوجه می‌شود که این قسمت جزء کد اصلی نیست. ضمنا رفتن به خط بعدی یا فاصله گذاشتن بین خطوط نیز در زبان VHDL مورد توجه قرار نمی‌گیرد و به عنوان آخرین نکته‌ی این قسمت نیز این که VHDL یک زبان به شدت حساس به نوع (type) است و شما باید برای هر object که تعریف می‌کنید و دارای مقدار است، نوع آن را نیز تعیین کنید؛ مثلا، signal، constant و … .

در ادامه، هر کدام از این دو بخشی که در قسمت بالا گفتیم که در ساختار یک entity وجود دارند را به طور کامل توضیح می‌دهیم.

 Entity Declaration .a

در این قسمت نام آن entity و لیست ورودی/ خروجی‌های آن را مشخص می‌کنیم. فرمول کلی به این صورت است.

entity NAME_OF_ENTITY is generic generic_declarations);]
     port (signal_namesmode type;
            signal_namesmode type;
                :
            signal_namesmode type);
end [NAME_OF_ENTITY] ;

براساس ساختار فوق، هر entity همیشه در ابتدا با ذکر کردن کلمه‌ی کلیدی entity آغاز می‌شود. پس از آن بلافاصله نام آن entity و سپس کلمه‌ی کلیدی is قرار داده می‌شود. پس از آن نوبت به معرفی پورت‌های این entity است. برای این کار با کلمه‌ی کلیدی port آغاز می‌کنیم و تک تک پورت‌های ورودی و خروجی را ذکر می‌کنیم. در پایان همیشه از کلمه‌ی کلیدی end استفاده می‌کنیم و می‌توانیم به صورت اختیاری دوباره نام انتخاب شده برای آن entity را نیز در داخل کروشه بیاوریم.

  • عبارت NAME_OF_ENTITY نامی است که شما برای ماژول خود انتخاب می‌کنید.
  • نام سیگنال‌ها هم توسط خود کاربر انتخاب می‌شوند و مشخص کننده‌ی اینترفیس‌های خارجی این ماژول هستند.
  • کلمه mod هم یکی از کلمات کلیدی از پیش رزرو شده‌ی خود VHDL است که جهت هر سیگنال را با استفاده از آن تعیین می‌کنیم.
    • In: مشخص کننده‌ی سیگنال ورودی.
    • Out: مشخص کننده‌ی سیگنال خروجی یک ماژول که مقدار آن تنها می‌تواند توسط ماژول‌های دیگری که از آن استفاده می‌کنند؛ خوانده شود. (نمی‌توانند مقدار آن را تغییر دهند)
    • Buffer: نشان دهنده‌ی سیگنال خروجی که مقدار آن می‌تواند در قسمت بدنه‌ی معماری یک entity خوانده شده و مورد استفاده قرار گیرد.
    • Inout: سیگنالی که هم می‌تواند به عنوان خروجی و هم به عنوان ورودی مورد استفاده قرار گیرد.
  • Type: تعیین کننده‌ی نوع هر سیگنال که یا توسط خود VHDL شناخته شده و معلوم است و یا توسط کاربر تعیین می‌شود.

مثال‌هایی از انواع سیگنال‌ها می‌تواند نوع bit ،bit_vector، Boolean ،character ،std_logic ،std_ulogic و … باشد.

    • نوع داده‌ی bit: سیگنالی که می‌تواند مقادیر ۰ و ۱ را بپذیرد.
    • نوع داده‌ی bit_vector: این سیگنال برداری از بیت‌ها است. مثلا یک بردار ۷ بیتی.
    • نوع داده‌های std_logic ،std_ulogic ،std_logic_vector ،std_ulogic_vector: این نوع سیگنال‌ها می‌توانند ۹ مقدار بپذیرند. دو نوع std_ulogic و std_logic به انواع bit و bit_vector معمولا ترجیح داده می‌شوند.
    • نوع داده‌ی Boolean: سیگنالی که می‌تواند مقادیر true و false داشته باشد.
    • نوع داده‌ی integer: سیگنالی که مقادیر مورد پذیرش آن اعداد صحیح هستند.
    • نوع داده‌ی real: سیگنالی که مقادیر مورد پذیرش آن اعداد حقیقی هستند.
    • نوع داده‌ی character: سیگنالی برای تعیین هر نوع کاراکتر قابل چاپ.
    • نوع داده‌ی time: سیگنالی که زمان را مشخص می‌کند.
  • generic: این بخش اختیاری محسوب می‌شود و می‌توان در صورتی که به آن نیاز نداشته باشیم آن را پاک کنیم. کار آن تعیین و تعریف مقادیر ثابتی است که به صورت local و برای timing و sizing (عرض باند باس‌ها) به کار می‌روند.

یک generic می‌تواند دارای مقادیر از پیش تعیین شده باشد. سینتکس به کار بردن آنها به صورت زیر است.

generic (
constant_name: type [:=value] ;
constant_name: type [:=value] ;
:
constant_name: type [:=value] );

به عنوان مثال برای شماتیک ساختاری عکس دوم، بخش entity declaration چیزی شبیه به این خواهد بود.

-- comments: example of the buzzer circuit of fig. 2
entity BUZZER is
     port (DOOR, IGNITION, SBELT: in std_logic;
       WARNING: out std_logic);
end BUZZER;

بسیار خب، در کد بالا می‌بینیم که entity یا همان ماژول ما با نام BUZZER (به معنای هشداردهنده)، دارای سه پورت (سیگنال) ورودی با نام های DOOR ،IGNITION و SBELT و همینطور یک سیگنال خروجی با نام WARNING است.

( به نحوه‌ی استفاده و محل‌های قرار دادن ; دقت کنید.)

نام BUZZER معرف ماژول ماست. پورت‌ها هم با کلمات کلیدی in و out مشخص شده‌اند. Type یا نوع هر پورت هم مشخص شده است که در اینجا می‌بینیم از نوع std_logic استفاده شده است. معمولا این نوع از سیگنال را به بقیه‌ی انواع سیگنال‌های دیجیتال ترجیح می‌دهند. به همان علتی که در بخش قبلی به آن اشاره کردیم؛ اینکه نوع bit تنها می‌تواند مقادیر ۰ و ۱ را بپذیرد اما انواعی مانند std_logic و std_ulogic می‌توانند ۹ مقدار مجزا را بپذیرند پس طبیعتا قدرتمندتر هستند. این قدرت ما را در توصیف دقیق‌تر عملکرد یک سیستم دیجیتال کمک خواهد کرد چرا که می‌توانیم حالت‌های بیشتری از یک سیگنال را مدل کنیم؛ مثلا علاوه بر مقادیر ۰ و ۱، حالت unknown یا همان X، حالت dontcare یا همان – ، حالت high impedance یا همان Z و … را نیز می‌توانیم داشته باشیم.

نوع داده‌ی std_logic در پکیج std_logic_1164 از کتابخانه‌ی IEEE موجود است. پیش از این اشاره کردیم که اهمیت مشخص کردن تایپ سیگنال‌ها اولا در این است که تعیین می‌کنیم این سیگنال چه مقادیری را می‌تواند داشته باشد، و در ثانی همین کار باعث خواهد شد که توصیف‌های دقیق‌تر و کم‌خطا تری از مدارها داشته باشیم. به این ترتیب که فرض کنید اگر در جایی از مدار اشکالی وجود داشته باشد که موجب شود به سیگنالی مقداری نسبت داده شود که برای آن type تعریف شده مجاز نیست، کامپایلر اعلام خطا کرده و باعث می‌شود شما از وجود آن اشکال در روند طراحی مطلع شوید.

در ادامه چند مثال مختصر دیگر را از entity declarations با هم می‌بینیم.

برای یک مالتی‌پلکسر ۴ به ۱، با ورودی‌های ۸ بیتی:

entity mux4_to_1 is
     port (I0,I1,I2,I3: in std_logic_vector(7 downto 0);
SEL: in std_logic_vector (1 downto 0);
           OUT1: out std_logic­_vector(7 downto 0));
     end mux4_to_1;

و برای یک D-FlipFlop که دارای ورودی‌های set و reset می‌باشد:

entity dff_sr is
     port (D,CLK,S,R: in std_logic;
           Q,Qnot: out std_logic­);
     end dff_sr;
b.      Architecture body

در این قسمت از کد چگونگی پیاده‌سازی و رفتار مدار را توصیف می‌کنیم. در قسمت‌های قبل گفتیم که یک مدار را می‌توان در سطح رفتار، ساختاری و … و یا حتی ترکیبی از سطوح مختلف، توصیف کرد. فرم کلی این قسمت از کد، معمولا به این صورت است.

architecture architecture_name of NAME_OF_ENTITY is
-- Declarations
       -- components declarations
       -- signal declarations
       -- constant declarations
       -- function declarations
       -- procedure declarations
       -- type declarations
           :
begin
       -- Statements          
           :
end architecture_name;

توصیف مدار با استفاده از مدل رفتاری

خب، معماری همان سیستم مثال قبلی، یعنی سیستم هشدار دهنده‌ی خودرو را اگر بخواهیم با روش رفتاری (Behavioral model) توصیف کنیم، داریم:

architecture behavioral of BUZZER is
begin
      WARNING <= (not DOOR and IGNITION) or (not SBELT and IGNITION);
end behavioral;

در اولین خط این کد، مشخص شده است که اولا ما یک توصیف رفتاری داریم، در ثانی مدار مورد توصیف ما دارای نام BUZZER است. این نام هر چیز مجازی می‌تواند باشد. (غیر از کلمات کلیدی از پیش رزرو شده توسط VHDL) در قسمت اصلی کد، توصیف مدار با استفاده از کلمه‌ی کلیدی begin شروع می‌شود و پس از آن یک عبارت بولین می‌بینیم که عملکرد مدار را توصیف می‌کند. البته در ادامه‌ی آموزش می‌بینیم که می‌توان به جز عبارت‌های بولین از روش‌های دیگری نیز برای توصیف استفاده کرد.

نماد => که در این عبارت استفاده شده است، عملگر assign است و مقداری که در سمت راست عبارت قرار گرفته است را به متغیر سمت چپ assign می‌کند.

پس از اتمام توصیف مدار، با کلمه‌ی کلیدی end به این بخش از کد خاتمه می‌دهیم.

در ادامه دو مثال دیگر را هم از توصیف مدارها با روش توصیف رفتاری می‌بینیم.

توصیف رفتاری یک گیت AND دو ورودی:

entity AND2 is
     port (in1, in2: in std_logic;
       out1: out std_logic);
end AND2;
     
architecture behavioral_2 of AND2 is
begin
          out1 <= in1 and in2;
end behavioral_2;

و یک گیت XNOR دو ورودی:

entity XNOR2 is
     port (A, B: in std_logic;
           Z: out std_logic);
end XNOR2;

architecture behavioral_xnor of XNOR2 is
     -- signal declaration (of internal signals X, Y)
     signal X, Y: std_logic;
begin
       X <= A and B;
       Y <= (not A) and (not B);
       Z <= X or Y;
End behavioral_xnor;

می‌بینیم که در کدهای فوق، عبارت‌هایی که در قسمت توصیف معماری استفاده می‌شوند، همگی از عملگرهای منطقی استفاده می‌کنند. از میان این عملگرهای منطقی، آنهایی که در زبان VHDL شناخته شده هستند و استفاده از آنها به همین صورت مجاز است عبارتند از and ،or ،nand ،nor ،xor ،xnor و not. علاوه ‌بر عملگرهای منطقی، عملگرهای شیفت، مقایسه‌ای و ریاضیاتی نیز مجاز هستند. در قسمت‌های بعدی بیشتر درباره‌ی این موارد صحبت خواهیم کرد.

مفهوم Concurrency یا همزمانی در VHDL

بد نیست که در اینجا یادآوری کنیم که در تمام مثال‌های فوق، signal assignmentها همگی به صورت همزمان رخ می‌دهند، یعنی به محض اینکه یک یا تعداد بیشتری از سیگنال‌های سمت راست هر عبارت تغییر کند، آن تغییر سریع به سمت چپ عبارت نیز assign می‌شود و بر روی مقدار آن تاثیر می‌گذارد. در مثال قبلی، اگر سیگنال ورودی A تغییر کند، سیگنال‌های داخلی X و Y نیز تغییر می‌کنند که تغییر آنها نیز به نوبه‌ی خود موجب تغییر و آپدیت شدن مقدار سیگنال Z می‌شود.

البته در این فرآیندها ممکن است یک تاخیر انتشار (propagation delay) وجود داشته باشد، اما باید بدانیم که اساس سیستم‌های دیجیتال مبتنی برداده است به این معنی که هر رخداد یا تغییری که در یکی از سیگنال‌های آن سیستم رخ بدهد، موجب رخداد یا سلسه رخدادهایی بر روی سیگنال‌های دیگر خواهد شد. بنابراین ترتیب اجرا شدن assignmentها نه به ترتیب از بالا به پایین، بلکه تابع تغییرات روی جریان داده است و به همین دلیل، ترتیب نوشتن عبارات اصلا اهمیتی ندارد. (در همان مثال قبلی، اگر سومین عبارت یعنی عبارت مربوط به assign شدن سیگنال Z را به قبل از عبارت‌های مربوط به X و Y منتقل کنیم هم خروجی مدار تغییری نخواهد کرد)

این حالت، دقیقا برعکس چیزی است که در زبان‌های برنامه‌نویسی نرم‌افزاری داشتیم و کدها به صورت ترتیبی از بالا به ترتیب تفسیر و اجرا می‌شدند.

روش توصیف ساختاری

همان مثال مدار هشدار دهنده‌ی خودرو را به خاطر بیاورید. گفتیم که می‌توان آن را به روش ساختاری نیز توصیف کرد و حتی تصویری از توصیف ساختاری آن را نیز با هم دیدیم. در توصیف ساختاری مشخص می‌کنیم که چه گیت‌هایی در این مدار وجود دارند و چگونه به هم متصل هستند، البته نه به صورت تصویری، بلکه به زبان سخت‌افزار. کدی که در ادامه می‌آید منظور ما را روشن‌تر خواهد کرد.

architecture structural of BUZZER is
         -- Declarations
         component AND2
                port (in1, in2: in std_logic;
                      out1: out std_logic);
         end component;
         component OR2
                port (in1, in2: in std_logic;
                      out1: out std_logic);
         end component;
         component NOT1
                port (in1: in std_logic;
                      out1: out std_logic);
         end component;
         -- declaration of signals used to interconnect gates
         signal DOOR_NOT, SBELT_NOT, B1, B2: std_logic;
begin
           -- Component instantiations statements
           U0: NOT1 port map (DOOR, DOOR_NOT);
           U1: NOT1 port map (SBELT, SBELT_NOT);
           U2: AND2 port map (IGNITION, DOOR_NOT, B1);
           U3: AND2 port map (IGNITION, SBELT_NOT, B2);
           U4: OR2  port map (B1, B2, WARNING);
end structural;

پس از خط اول که عنوان کد است، می‌بینیم که تک تک گیت‌هایی که در این مدار قرار است مورد استفاده قرار گیرند، معرفی می‌شوند. در مثالی که ما داریم، همانطور که می‌بینید یک گیت AND دو ورودی داریم، یک گیت OR دو ورودی و یک اینورتر (معکوس‌کننده). اما پیش از این خود این گیت‌ها باید به عنوان entity معرفی شده باشند (با روشی که در مراحل قبل گفتیم)

می‌توان این کار را در فایل جداگانه‌ای انجام داد و آن را ذخیره نمود و در این کد، آن فایل را در قسمت کتابخانه‌ها اضافه نمود. (این را در قسمت کتابخانه‌ها و پکیج‌ها بیشتر توضیح می‌دهیم)

پورت‌های ورودی و خروجی هر گیتی که قرار است استفاده شود را نیز در همین کد مشخص می‌کنیم.

در قدم بعدی، سیگنال‌های داخلی مدار را هم باید تعریف کنیم. در مثال ما این سیگنال‌ها شامل DOOR_NOT ،SBELT_NOT ،B1 ،B2 هستند. همانطور که می‌بینید type آنها را نیز مشخص می‌کنیم. (در اینجا std_logic).

در ادامه، عبارت‌هایی که پس از کلمه‌ی کلیدی begin قرار می‌گیرند، اتصالات بین گیت‌ها را مشخص می‌کنند. در هر کدام از خطوط این بخش می‌بینیم که یکی از گیت‌های تعریف شده در قسمت اول را صدا زده‌ایم و برای آن یک نام هم انتخاب کرده‌ایم (U0، U1 و …). پس از آن کلمه‌ی کلیدی port map را داریم. زمانی از این کلمه‌ی کلیدی استفاده می‌کنیم که بخواهیم توضیح دهیم گیت‌های موجود چگونه به هم متصل هستند. مثلا در مثال فوق می‌بینیم این کار چگونه انجام شده است.

سیگنال DOOR ورودی گیت NOT1 و سیگنال DOOR_NOT خروجی آن است. از طرفی در گیت AND1 می‌بینیم که ورودی‌ها به صورت IGNITION و DOOR_NOT تعیین شده‌اند. این یعنی که خروجی گیت NOT1، به یکی از ورودی‌های گیت AND1 متصل شده است و بقیه نیز به همین ترتیب. این روش اتصال یک روش هوشمندانه و ضمنی محسوب می‌شود. در مقابل، یک روش دیگر نیز وجود دارد که به صورت خارجی و واضح اتصالات را مشخص می‌کنیم.

label: component-name port map (port1=>signal1, port2=> signal2,… port3=>signaln);
U0: NOT1 port map (in1 => DOOR, out1 => DOOR_NOT);
U1: NOT1 port map (in1 => SBELT, out1 => SBELT_NOT);
U2: AND2 port map (in1 => IGNITION, in2 => DOOR_NOT, out1 => B1);
U3: AND2 port map (in1 => IGNITION, in2 => SBELT_NOT, B2);
U4: OR2  port map (in1 => B1, in2 => B2, out1 => WARNING);

توجه کنید که ترتیب نوشتن عبارات هیچ تاثیری بر نتیجه‌ی خروجی نخواهد داشت چون تمام این عبارات به صورت همزمان و موازی اجرا می‌شوند. به عبارت دیگر؛ شماتیک سخت‌افزاری حاصل شده از اجرای این عبارات توصیفی، مستقل از ترتیب نوشتن آنهاست.

از همین روش توصیف ساختاری، می‌توان الگو برداری کرد و طراحی سلسله مراتبی انجام داد. به این ترتیب که واحد‌های تشکیل دهنده‌ی یک ماژول را که قرار است به صورت مکرر در آن استفاده شوند، طراحی و مشخص کرد و سپس به عنوان یک بلوک از قبل معلوم شده، در آن ماژول مرتبه‌ی بالاتر استفاده کرد. این روش پیچیدگی طراحی‌های بزرگ را به طرز چشم‌گیری کاهش می‌دهد. می‌توان گفت که عموما روش طراحی سلسله مراتبی همواره به روش طراحی یکدست (flat) ارجح است. در تصویری که در ادامه می‌بینید، ما از همین روش سلسله مراتبی برای طراحی یک ماژول جمع‌کننده‌ی ۴ بیتی استفاده کرده‌ایم. هر full adder را می‌توان با تعیین کردن عبارت‌های بولین مربوط به سیگنال‌های خروجی آن یعنی sum و carry out توصیف کرد.

 sum =  (A Å B) Å C
carry = AB + C(A Å B)
آموزش VHDL Primer
شماتیک یک مدار جمع‌کننده‌ی ۴ بیتی که از ماژول‌های full adder تشکیل شده است.

کد VHDL این مدار را نیز در ادامه آورده‌ایم. اگر به آن دقت کنید، می‌بینید که ابتدا full adder را به عنوان یک ماژول زیرمجموعه معرفی و تعیین کرده‌ایم و سپس به صورت مکرر نمونه‌هایی (instance) از آن را برای ساختن ماژول سطح بالاتر یعنی جمع‌کننده‌ی ۴ بیتی، استفاده کرده‌ایم. کتابخانه‌های مورد نیاز را هم در کد اضافه کرده‌ایم.

مدار جمع‌کننده‌ی ۴ بیتی – مدل‌سازی سلسله مراتبی با استفاده از VHDL

-- Example of a four bit adder
library  ieee;
use  ieee.std_logic_1164.all;
-- definition of a full adder
entity FULLADDER is
     port (a, b, c: in std_logic;
          sum, carry: out std_logic);
end FULLADDER;
architecture fulladder_behav of FULLADDER is
begin
          sum <= (a xor b) xor c ;
          carry <= (a and b) or (c and (a xor b));
 end fulladder_behav;
-- 4-bit adder
library  ieee;
use  ieee.std_logic_1164.all;
entity FOURBITADD is
     port (a, b: in std_logic_vector(3 downto 0);
           Cin : in std_logic;
           sum: out std_logic_vector (3 downto 0);
           Cout, V: out std_logic);
end FOURBITADD;
architecture fouradder_structure of FOURBITADD is
     signal c: std_logic_vector (4 downto 0);
      component FULLADDER
           port(a, b, c: in std_logic;
              sum, carry: out std_logic);
      end component;
begin
           FA0: FULLADDER
               port map (a(0), b(0), Cin, sum(0), c(1));
           FA1: FULLADDER
                port map (a(1), b(1), C(1), sum(1), c(2));
           FA2: FULLADDER
                port map (a(2), b(2), C(2), sum(2), c(3));
           FA3: FULLADDER
                port map (a(3), b(3), C(3), sum(3), c(4));
           V <= c(3) xor c(4);
           Cout <= c(4);
end fouradder_structure;

نکته‌ی دیگری که بد نیست در مورد کد اضافه کنیم این است که اگر دقت کنید، هم نام پورت‌های ورودی برای full adder و هم نام پورت‌های ورودی ماژول اصلی یعنی جمع‌کننده‌ی ۴ بیتی را a و b گذاشته‌ایم. به نظر می‌رسد که شاید چنین کاری موجب بروز خطا شود اما اینطور نیست. در واقع در زبان VHDL، این قدرت وجود دارد که تشخیص داده شود هر کدام از این ماژول‌ها در یک سطح خاص قرار دارند، یکی در سطح پایین‌تر و دیگری در سطح بالاتر قرار دارند، پس اشکالی به وجود نخواهد آمد. اما با این حال توصیه می‌شود که برای خواناتر بودن کد از لحاظ انسانی، بهتر است سعی کنیم نام‌های متفاوتی برای این دو انتخاب کنیم.

همانطور که می‌بینید یک سیگنال داخلی هم تعریف کرده‌ایم به نام c (4:0) که وظیفه‌ی آن انتقال carry خروجی از یک ماژول به عنوان carry ورودی به ماژول بعدی است. واضح است که carry ورودی برای اولین ماژول full adder نیز همان سیگنال Cin ورودی به مدار است.

اما شاید با همین منطق فکر کنید که بسیار خب، پس carry خروجی از آخرین ماژول هم باید همان سیگنال Cout خروجی از مدار باشد. اما اینطور نیست. ما مجبوریم برای carry خروجی آخرین ماژول هم یک سیگنال داخلی (همان c(4)) را در نظر بگیریم چون قرار است آن را به ورودی یک XOR ببریم. زبان VHDL به ما این اجازه را نمی‌دهد که با خروجی مدار مانند یک سیگنال داخلی برخورد کنیم و آن را وارد یک ماژول XOR کنیم. پس حتما باید یک سیگنال داخلی تعریف کنیم و با این کار هم می‌توانیم آن را در ماژول یا گیت دیگری استفاده کنیم و هم آن را به Cout هم assign کنیم.

c. کتابخانه‌ها و پکیج‌ها: استفاده از کلمات کلیدی

کتابخانه را می‌توان جایی در نظر گرفت که کامپایلر اطلاعات لازم برای کامپایل شدن یک طراحی را در آن نگه می‌دارد.

یک پکیج را هم در زبان VHDL می‌توان به فایل یا ماژولی اطلاق کرد که حاوی اطلاعاتی در مورد objectها، data typeها، component declarationsها، سیگنال‌ها و توابعی است که در طراحی‌های بسیاری ممکن است پرکاربرد و متداول باشند.

مطلب پیشنهادی:  آموزش FPGA و Verilog برای تازه کارها – DDR SDRAM

قبل‌تر اشاره کردیم که مثال std_logic که یکی از کلمات کلیدی پر استفاده در تعیین نوع سیگنال‌هاست، در پکیج ieee.std_logic_1164 از کتابخانه‌ی ieee موجود است. هر زمان بخواهیم در کدی از این کلمه‌ی کلیدی استفاده کنیم، باید حتما در ابتدای کد، این پکیج و کتابخانه را اضافه کرده باشیم. این کار را مطابق الگوی زیر انجام می‌دهیم.

library  ieee;
use  ieee.std_logic_1164.all;

پسوند all. برای تاکید بر این نکته‌ است که تمام محتوای این پکیج را به کد اضافه کن.

کمپانی زایلینکس پکیج‌های مختلفی را برای FPGAهای خود در نظر گرفته است.

مثلا از کتابخانه‌ی IEEE:

  • std_logic_1164 package: مشخص کننده‌ی نوع داده‌ی استاندارد.
  • std_logic_arith package: شامل توابع محاسباتی، تبدیلی و مقایسه‌ای برای داده‌های علامت‌دار، بدون‌علامت، صحیح (integer)، std_logic ،std_ulogic و std_logic_vector.
  • std_logic_unsigned
  • std_logic_misc package: در برگیرنده‌ی برخی توابع دیگر برای پیکج std_logic_1164 package، برخی ثابت‌ها و چند نوع‌داده‌ی مکمل.

برای استفاده از هر کدام از این پکیج‌ها ابتدا باید کتابخانه و سپس عبارت مربوط به پکیج را به کد اضافه کنیم.

library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all;
use ieee.std_logic_unsigned.all;

یا مثلا کتابخانه‌ی دیگری وجود دارد به نام synopsis که آن نیز پکیج‌های مخصوص خود را دارد.

library SYNOPSYS;
use SYNOPSYS.attributes.all;

و بسیاری کتابخانه‌ها و پکیج‌های دیگری که می‌توان در زبان VHDL از آنها استفاده کرد. سینتکس کلی تعریف یک پکیج به صورت زیر است.

 -- Package declaration
package name_of_package is
       package declarations
end package name_of_package;
-- Package body declarations
package body name_of_package is
       package body declarations
end package body name_of_package;

اگر بخواهیم از توابع ساده‌ای مانند AND2 ،OR2 ،NAND2 ،NOR2 ،XOR2 و … استفاده کنیم باید ابتدا آنها را تعریف کنیم. اما می‌توان به جای این کار، یک بار و برای همیشه آنها را در یک پکیج تعریف کرد و پس از آن هربار که با آنها کار داشته باشیم کافیست آن پکیج را هم به کد اضافه کنیم. این پکیج را می‌توانیم به صورت زیر داشته باشیم.

-- Package declaration
library ieee;
use ieee.std_logic_1164.all;
package basic_func is
     -- AND2 declaration
     component AND2
           generic (DELAY: time :=5ns);
           port (in1, in2: in std_logic; out1: out std_logic);
     end component;
     -- OR2 declaration
     component OR2
           generic (DELAY: time :=5ns);
           port (in1, in2: in std_logic; out1: out std_logic);
     end component;
end package basic_func;
-- Package body declarations
library ieee;
use ieee.std_logic_1164.all;
package body basic_func is
     -- 2 input AND gate
     entity AND2 is
           generic (DELAY: time);
           port (in1, in2: in std_logic; out1: out std_logic);
     end AND2;
     architecture model_conc of AND2 is
           begin
                out1 <= in1 and in2 after DELAY;
     end model_conc;
-- 2 input OR gate
entity OR2 is
           generic (DELAY: time);
           port (in1, in2: in std_logic; out1: out std_logic);
     end OR2;
     architecture model_conc2 of AND2 is
           begin
               out1 <= in1 or in2 after DELAY;
     end model_conc2;
end package body basic_func;

دقت کنید که در کد یک تاخیر (delay) 5 ns را لحاظ کرده‌ایم اما این تاخیر در هنگام سنتز شدن کد نادیده گرفته می‌شود. هم‌چنین از آنجا که در این کد از نوع داده‌ی std_logic استفاده کرده‌ایم، کتابخانه و پکیج مربوط به آن را نیز اضافه کرده‌ایم.

خب حالا فرض کنید که ما این پکیج را نوشته و در کتابخانه‌ای با نام my_func قرار داده‌ایم. اگر بخواهیم در کد دیگری از این پکیج استفاده کنیم؛ آن را به این صورت به آن کد اضافه می‌کنیم.

library  ieee, my_func;
use  ieee.std_logic_1164.all, my_func.basic_func.all;

همانطور که در عبارت بالا می‌بینید، می‌توان پکیج‌های مختلف را با فاصله از هم در مقابل عبارت use قرار داد و تمام آنها را به کد اضافه کرد. نکته‌ی مهم این است که در صورتی که این کار را انجام دهیم در خط اول نیز باید کتابخانه‌های هر کدام از این پکیج‌ها را به همان ترتیب ذکر کنیم. در ضمن اگر کدی شامل چند entity باشد مثلا مانند طراحی سلسله مراتبی جمع‌کننده‌ی ۴ بیتی که در قسمت‌های قبلی داشتیم، برای هر کدام از این entity‌ها که در طول کد تعریف می‌شوند، باید کتابخانه و پکیج‌ها را جداگانه مشخص کنیم و در ابتدای بخش مربوط به خودشان قرار دهیم.

4. ساختارهای زبانی در VHDL

a. شناسه‌ها

شناسه‌ها یا Identifiers کلماتی هستند که توسط کاربران، یعنی هر یک از ما که برنامه‌ای را می‌نویسیم، به عنوان نام برای ماژول‌ها انتخاب می‌شوند. البته شناسه‌ها را برای پورت‌های ورودی و خروجی نیز انتخاب می‌کنیم و تنها منحصر به ماژول‌های طراحی معماری آنها نیست.

اما در انتخاب این نام‌ها شاید آن‌قدرها هم آزادی مطلق وجود ندارد و یک سری قوانین کوچک توسط زبان VHDL طراحی شده است که در انتخاب شناسه‌ها باید آنها را رعایت کنیم.

  • شناسه‌ها تنها می‌توانند شامل حروف الفبای انگلیسی (بزرگ یا کوچک) و اعداد از ۱ تا ۹ و نیز کاراکتر آندرلاین ( _ ) باشند.
  • هر شناسه همیشه باید با یکی از حروف الفبا آغاز شود و هرگز نمی‌تواند با کاراکتر ( _ ) خاتمه یابد.
  • یک شناسه نمی‌تواند شامل دو کاراکتر آندرلاین به صورت متوالی باشد. در صورت نیاز به استفاده از دو ( _ )، حتما باید بین آن‌ها با کاراکترهای دیگر (حروف یا اعداد) فاصله وجود داشته باشد.
  • شناسه‌ها به بزرگی یا کوچکی حروف حساس نیستند و بزرگ یا کوچک کردن حروف، موجب بروز تفاوت میان دو شناسه نمی‌شود. مثلا And2 ،AND2 و and2 هر سه به عنوان یک چیز تلقی می‌شوند.
  • طول شناسه دلخواه است و می‌تواند بلند یا کوتاه باشد.

به عنوان چند مثال از شناسه‌های معتبر می‌توان به مواردی مثل X10 ،x_10 ،My_gate1 و… اشاره کرد

به عنوان چند مثال از شناسه‌های غیرمعتبر هم می‌توان به مواردی مانند X10 ،my_gate@input ،gate-input_ و… اشاره کرد.

شناسه‌هایی که مطابق با قوانین فوق ساخته می‌شوند را شناسه‌های پایه یا basic identifierها می‌گویند. اما اگر بخواهیم سیگنال‌ها را هم با همین شناسه‌ها نام‌گذاری کنیم، به نظر می‌رسد که این قوانین زیادی سخت‌گیرانه باشند. مثلا فرض کنید که یک سیگنال active low داشته باشیم؛ مثلا یک active low RESET. براساس قوانین فوق نمی‌توانیم آن را اینطور نام‌گذاری کنیم، RESET/.

بنابراین برای غلبه بر این محدودیت‌های وضع شده، دسته‌ی دیگری از شناسه‌ها نیز ایجاد شدند که به آنها شناسه‌های توسعه‌یافته یا extended identifiers گفته می‌شود. در این دسته از شناسه‌ها که محدودیت‌های بسیار کمتری دارند، می‌توان با هر ترتیبی از کاراکترها یک شناسه ساخت و از آن برای نام سیگنال‌ها استفاده نمود.

  • هر شناسه ما بین دو علامت \ (backslash) قرار می‌گیرد.
  • بزرگی و کوچکی حروف استفاده شده موجب ایجاد تمایز می‌شود.
  • این شناسه‌ها با کلمات کلیدی از پیش رزرو شده توسط خود VHDL و نیز با شناسه‌های پایه‌ای، متفاوت هستند.
  • چیزی که مابین هر دو \ قرار می‌گیرد، می‌تواند هرچیزی با هر ترتیبی باشد. فقط اینکه اگر قرار باشد خود \ هم به عنوان یکی از کاراکترهای استفاده شده باشد، باید حتما مشخص شود. مثلا اگر بخواهیم نام سیگنالی را BUS:\data بگذاریم، باید به این شکل باشد، \BUS:\data\
  • یک نکته‌ی بسیار مهم این است که شناسه‌های دسته‌ی دوم یعنی توسعه‌یافته‌ها را در نسخه‌ی VHDL-93 می‌توان استفاده کرد ولی در ورژن VHDL-87 قابل استفاده نیستند.

چند مثال از شناسه‌های معتبر در این دسته.

Input ،\Input\ ،\input#1\ ،\Rst\\as\

b. کلمات کلیدی (از پیش رزرو شده)

در زبان VHDL نیز مانند بسیاری از زبان‌های دیگر، برخی از کلمات به صورت از پیش تعیین شده برای کارهای مشخص خود زبان و کامپایلر رزرو شده‌اند و معنا و مفهوم مشخصی دارند. بنابراین مجاز نیستیم که از این کلمات به عنوان شناسه‌ی ماژول‌ها یا سیگنال‌ها استفاده کنیم. به عنوان مثال از این کلمات کلیدی مواردی مثل in ،out ،or ،and ،port ،map ،end و … هستند که از قبل هم آنها و استفاده‌هایشان را در کدها دیده‌ایم. معمولا در کدها یا آموزش‌ها این کلمات کلیدی را برای متمایز شدن با حروف بولد تایپ می‌کنند که ما هم در برخی قسمت‌های این آموزش این کار را انجام داده‌ایم. اگر دوست داشتید لیست این کلمات کلیدی در زبان VHDL را به صورت یکجا داشته باشید، می‌توانید از اینجا دریافت کنید.

درشناسه‌های توسعه یافته، می‌توان از کلمات کلیدی نیز استفاده کرد چرا که در آنجا کلمات را بین دو \ قرار می‌دهیم و این کار باعث می‌شود کامپایلر آن را با کلمه کلیدی اشتباه نگیرد. مثلا شناسه‌ی \end\ هرچند که end یک کلمه‌ی کلیدی است، اما شناسه‌ی معتبری محسوب می‌شود.

c. اعداد

حالت پیش‌فرض برای نمایش اعداد در VHDL نمایش دسیمال است. نمایش‌ عددها به صورت صحیح (integer) و حقیقی (real)  قابل قبول هستند. منظور از نمایش integer شامل تمام اعدادی است که دارای ممیز اعشاری نیستند و منظور از نمایش حقیقی اعداد دارای ممیز هستند.

نوتیشن‌های توان رسانی نیز با استفاده از نماد E یا e امکان پذیر هستند. ضمن آنکه می‌دانیم برای اعداد صحیح، عدد موجود در توان همواره باید یک عدد مثبت باشد.

مثال برای اعداد integer:

12    10    256E3   12e+6

و مثال برای اعداد حقیقی:

1.2   256.24  3.14E-2

یا مثلا عددی مانند 12- شامل یک علامت منفی است که با یک عدد در نمایش صحیح ترکیب می‌شود.

اگر بخواهیم عددی را در مبنایی غیر از مبنای 10 نمایش دهیم، باید با فرمت زیر آن را بیان کنیم.

Base#number#

یعنی ابتدا قید می‌کنیم که عدد ما در چه مبنایی است و پس از از علامت # استفاده می‌کنیم. سپس عدد مدنظر را در همان مبنا نوشته و در انتها نیز مجددا علامت # را قرار می‌دهیم.

مثال: عدد 18 (در مبنای 10) که در مبناهای دیگر نشان داده شده است.

Base 2:   2#10010#
Base 16: 16#12#
Base 8:   8#22#

و نمایش عدد 29 (در مبنای 10) در مبناهای دیگر.

Base 2:   2#11101#
Base 16: 16#1D#
Base 8:   8#35#

اگر نمایش عددی طولانی شود، برای خواناتر بودن آن می‌توان مابین رقم‌ها را با علامت ( _ ) از هم تفکیک کرد فقط به این شرط که در اول و آخر اعداد استفاده نشود. مثلا :

2#1001_1101_1100_0010#
215_123

d. کاراکترها، رشته‌ها و رشته بیت‌ها

اگر بخواهیم یک کاراکتر را در VHDL نشان دهیم، آن را بین دو ‘ ’  قرار می‌دهیم.

مثلا : ‘B’ یا ‘b’ یا حتی ‘,’  و … .

و اگر بخواهیم یک رشته از کاراکترها یا به عبارت دیگر یک string را مشخص کنیم، آن را بین دو علامت “ ” قرار می‌دهیم. مثلا:

“This is a string.”

اگر خواستید در درون یک string از علامت “ ” استفاده کنید، آن را دو بار پشت سر هم تکرار کنید. به این شکل:

“This is a “”String””.”

هر کاراکتری که بخواهیم print شود را می‌توانیم در داخل علامت stirng قرار دهیم.

اما رشته بیت چیست؟ مسلما همانطور که از ظاهر آن می‌توان حدس زد، زنجیره‌ای از بیت‌ها که با آنها مانند یک رشته رفتار می‌کنیم. اما برای آنکه مشخص کنیم که این نوع رشته با رشته‌ی کاراکتری متفاوت است و این تفاوت را به کامپایلر نیز بفهمانیم، در ابتدای این رشته علامت B یا b را قرار می‌دهیم. به این شکل، B”1001”.

می‌توان همین روش را برای نمایش hexagonal یا octal هم انجام داد. فقط کافیست به جای B، این بار به ترتیب X یا O را قبل از string قرار دهیم. به مثال‌های زیر دقت کنید.

Binary:  B”1100_1001”, b”1001011”
Hexagonal: X”C9”, X”4b”
Octal: O”311”, o”113”

دقت داشته باشید که در سیستم هگزادسیمال، هر رقم دقیقا ۴ بیت را نشان می‌دهد. بنابراین دو عدد ”b”1001011 و ”X”4b را نباید معادل هم گرفت چرا که اولی ۷ بیت دارد ولی دومی نشان‌دهنده‌ی یک رشته بیت دقیقا ۸ بیتی است. با دلیلی مشابه، دو رشته‌ی ”O”113 (که نشان‌دهنده‌ی ۹ بیت است ) و ”X”4b (که نشان‌دهنده‌ی ۸ بیت است) نیز معادل هم نیستند.

5. داده‌ها: سیگنال‌ها، متغیرها و ثابت‌ها

داده‌ها یا data objectها به اشیائی گفته می‌شود که می‌توان به آنها نوع و مقدار نسبت داد. این نوع می‌تواند سیگنال، متغیر، ثابت یا فایل باشد. تا این لحظه با سیگنال‌ها آشنا شده‌ایم و گفتیم که می‌توان از آنها به عنوان ورودی و خروجی و یا اتصالات داخلی استفاده کرد. در شماتیک یک مدار می‌توان آنها را به عنوان سیم‌‌هایی در نظر گرفت که مقدار آنها در هر لحظه تابع عبارت‌های توصیفی‌ای است که برای تخصیص مقدار به آنها نوشته‌ایم.

حالا با دو نوع داده‌ی دیگر یعنی متغیر و ثابت آشنا می‌شویم. این دو نوع داده‌ها برای مدل کردن رفتار مدارها به کار می‌روند و در ضمن پروسه‌ها و اجرای توابع استفاده می‌شوند. درست مانند زبان‌های برنامه‌نویسی دیگر. در ادامه، درباره‌ی هر کدام از این انواع به طور جداگانه نیز توضیح مختصری آورده‌ایم.

ثابت‌ها یا constants

یک داده از نوع ثابت، داده‌ای است که تنها دارای یک مقدار مشخص و از پیش تعیین شده است و در طول سیمولیشن مقدار آن تغییر نخواهد کرد و همواره همان یک مقدار را دارد. برای مشخص و معرفی کردن چنین داده‌ای به این ترتیب عمل می‌کنیم.

constant list_of_name_of_constant: type [ := initial value] ;

مقدار اولیه یا همان initial value، مقدار دلخواهی است که شما برای آن داده درنظر می‌گیرید. این ثابت‌ها را در ابتدای یک معماری تعریف می‌کنیم و بعد در هر قسمتی از آن معماری که به آن نیاز داشته باشیم، می‌توانیم از آن استفاده کنیم. یک حالت دیگر هم وجود دارد که یک مقدار ثابت را در درون یک پروسه (process) تعریف کنیم. منتها در این حالت دیگر تنها در درون همان پروسه می‌توانیم از آن استفاده کنیم و خارج از آن معتبر نیست.

constant  RISE_FALL_TME: time := 2 ns;
constant  DELAY1: time := 4 ns;
constant  RISE_TIME, FALL_TIME: time:= 1 ns;
constant  DATA_BUS: integer:= 16;

متغیرها یا variables

متغیرها هم درست مانند ثابت‌ها دارای یک مقدار هستند. با این تفاوت که این یک مقدار می‌تواند ثابت نباشد و در هر لحظه از زمان و بر حسب نیاز و دستورالعملی که ما مشخص می‌کنیم، تغییر کند. این دستورالعمل تغییر را به کمک عبارت‌های تخصیصی (assignment statement) توصیف می‌کنیم. به محض اینکه این دستورالعمل اجرا شود، بدون هیچ تاخیری مقدار متغیر هم به روز خواهد شد. متغیرهای هر فرآیند را باید در داخل خود آن تعریف کنیم و به اصطلاح برای آن فرآیند local محسوب می‌شوند. این کار را به فرم زیر انجام می‌دهیم.

variable list_of_variable_names: type [ := initial value] ;

چند مثال را هم با هم ببینیم.

variable CNTR_BIT: bit :=0;
variable VAR1: boolean :=FALSE;
variable SUM: integer range 0 to 256 :=16;
variable STS_BIT: bit_vector (7 downto 0);

برای نمونه، متغیر SUM را در مثال‌های بالا ببینید. یک متغیر صحیح که در بازه‌ی ۰ تا ۲۵۶ مقدار می‌پذیرد و در ابتدای سیمولیشن مقدار اولیه‌ی آن روی ۱۶ قرار داده شده است. یا مثلا آخرین مورد را در نظر بگیرید، یک متغیر که برداری ۸ تایی از بیت‌هاست. یعنی المان‌های آن STS_BIT(7), STS_BIT(6),… STS_BIT(0) هستند.

عبارت تخصیصی که مقدار یک متغیر را در هر لحظه به روزرسانی می‌کند، دارای چنین فرمتی است.

Variable_name := expression;

به محض اینکه عبارت (expression) اجرا شود، مقدار جدید متغیر به آن اختصاص داده می‌شود، بدون هیچ تاخیری.

سیگنال‌ها یا signals

سیگنال‌ها را در خارج از فرآیندها و با کمک عبارت‌هایی مانند عبارت‌های زیر تعریف می‌کنیم.

signal list_of_signal_names: type [ := initial value] ;
     signal SUM, CARRY: std_logic;
     signal CLOCK: bit;
     signal TRIGGER: integer :=0;
     signal DATA_BUS: bit_vector (0 to 7);
     signal VALUE: integer range 0 to 100;

مقدار سیگنال‌ها پس از آنکه عبارت تخصیص دهنده‌ی آنها تغییر کند، با اندکی تاخیر  مانند دستور زیر به روز رسانی می‌شود.

SUM <= (A xor B) after 2 ns;

اگر خودمان مقدار تاخیر را تعیین نکنیم، به صورت پیش‌فرض یک مقدار نامعلوم دلتا برای تاخیر در نظر گرفته می‌شود. حتی می‌توان به صورت زیر با تعیین چند مقدار و تاخیر مختلف، یک شکل موج را مقداردهی نمود.

signal wavefrm : std_logic;
wavefrm <= ‘0’, ‘1’ after 5ns, ‘0’ after 10ns, ‘1’ after 20 ns;

توجه داشته باشید که فهم تفاوت میان سیگنال‌ها و متغیرها بسیار مهم است. مخصوصا در این مورد که الگوی تغییر مقادیر در هر کدام از این داده‌ها به چه شکل است. گفتیم که در متغیرها، به محض اینکه عبارت تخصیص مقدار تغییر کند، مقدار جدید بلافاصله و بدون تاخیر به متغیر تخصیص داده می‌شود. اما در سیگنال‌ها همین فرآیند با اندکی تاخیر اتفاق می‌افتد. یعنی میان محاسبه‌ی تغییر در عبارت و تغییر کردن مقدار خود سیگنال، مقدار کمی تاخیر وجود دارد. می‌توانیم خودمان این تاخیر را مشخص کنیم. اما اگر مشخص نکنیم هم یک مقدار نامشخص دلتا به این کار اختصاص داده می‌شود.

وجود همین تاخیر کوچک، در به روزشدن داده‌های از نوع سیگنال و متغیر تفاوت مهمی را ایجاد می‌کند. برای فهم این تفاوت اجازه دهید یک برنامه را که در دو حالت مختلف اجرا شده است با هم ببینیم. یکی نتیجه را با داده‌ی سیگنال و دیگری آن را با داده‌ی از نوع متغیر محاسبه می‌کنند.

اجرای برنامه با داده متغیر

architecture VAR of EXAMPLE is
     signal TRIGGER, RESULT: integer := 0;
begin
     process
           variable variable1: integer :=1;
           variable variable2: integer :=2;
           variable variable3: integer :=3;
     begin
           wait on TRIGGER;
           variable1 := variable2;
           variable2 := variable1 + variable3;
           variable3 := variable2;
           RESULT <= variable1 + variable2 + variable3;
     end process;
end VAR

اجرای برنامه با داده سیگنال

architecture SIGN of EXAMPLE is
     signal TRIGGER, RESULT: integer := 0;
signal signal1: integer :=1;
     signal signal2: integer :=2;
     signal signal3: integer :=3;
begin
     process   
     begin
           wait on TRIGGER;
           signal1 <= signal2;
           signal2 <= signal1 + signal3;
           signal3 <= signal2;

           RESULT  <= signal1 + signal2 + signal3;
     end process;
end SIGN;

در کد اول، متغیرهای variable2 ،variable1 و variable3 به ترتیب و به محض اینکه سیگنال TRIGGER از راه برسد مقادیرشان به‌ روز می‌شود. پس از آن، خروجی RESULT که به صورت سیگنال تعریف شده است، در زمان دلتا بعد از رخ دادن TRIGGER و به ‌روز شدن متغیرها، مقدارش به‌ روز می‌شود. پس، به محض رخ دادن یک TRIGGER، نتایج ما به این صورت خواهد شد.

variable1 = 2, variable2 = 5 (=2+3), variable3= 5

و خروجی RESULT هم که به صورت سیگنال تعریف شده است، در زمان TRIGGER + Delta و با مقادیر جدید متغیرها محاسبه می‌شود و مقدار به روز شده‌ی آن RESULT=12 خواهد بود.

اما در کد دوم به چه صورت است؟ با رخ دادن TRIGGER، محاسبات سیگنال‌ها آغاز می‌شود و تمام سیگنال‌ها به صورت همزمان و با استفاده از مقادیر قدیمی (به‌روز نشد‌ه‌ی) سیگنال‌های 1، 2 و 3 مقادیرشان محاسبه می‌شود. با تاخیر دلتا پس از رسیدن TRIGGER، مقادیر جدید محاسبه شده روی آنها پدیدار می‌شود. بنابراین مقادیر به این ترتیب خواهد بود.

Signal1 = 2   ,   signal2 = 4 (1+3)    , signal3 = 2    , RESULT =  6

6. انواع داده‌ها

هر شئ یا آبجکت دارای یک نوع (type) است. تایپ یک داده مشخص می‌کند که آن داده چه مقادیری را می‌تواند بپذیرد و چه عملیات‌هایی می‌تواند بر روی آن انجام شود. این موضوع یعنی تعیین نوع هر داده‌ای، در زبان VHDL بسیار حائز اهمیت است تا جایی که گفته می‌شود VHDL یک زبان type محور است؛ یعنی بسته به نوعی که داده‌ها به آن تعلق دارند، با آنها کار می‌کند و به طور کلی، اجازه نداریم به داده‌ای از یک تایپ، مقادیری از تایپ دیگر را اختصاص بدهیم (مثلا اینکه یک مقدار صحیح را به داده‌ای که از نوع بیت است نسبت بدهیم).

انواع داده‌ها را در زبان VHDL می‌توانیم در چهار کلاس کلی دسته‌بندی کنیم. scalar ،composite ،access و file.

داده‌های گروه اسکالر، داده‌هایی هستند که می‌توان به آنها یک تک‌ مقدار نسبت داد و بر روی آنها عملیات‌های مقایسه‌ای انجام داد. داده‌هایی مانند integer ،real، شمارشی (enumerated)، boolean (بولین) و character (کاراکتر). در ادامه مثال‌های بیشتری از آنها را ذکر خواهیم کرد.

a. تایپ‌هایی که در پکیج استاندارد تعریف شده‌اند

زبان VHDL، تایپ‌های مختلفی را به صورت از پیش تعریف شده در پکیج استاندارد خود دارد. جدول زیر این انواع را نشان می‌دهد. اگر بخواهیم از هر کدام از آنها استفاده کنیم، باید به وسیله‌ی عبارت زیر، این پکیج را به ابتدای کدمان اضافه کنیم.

library std, work;
    use std.standard.all;

آموزش VHDL Primer

User-defined type .b

یکی از ویژگی‌های جالب VHDL این است که به غیر تایپ‌های از پیش تعیین‌شده‌ی خودش، این امکان را به کاربر نیز می‌دهد که مطابق با نیاز و خواست خودش، تایپ‌های جدیدی را نیز تعریف کند. برای این کار کافیست نام آن تایپ و محدوده‌ی مقادیر قابل پذیرش آن را با سینتکس زیر بنویسیم.

 type identifier is type_definition;

چند مثال برای تعریف type با هم ببینیم.

از نوع integer

type small_int is range  to 1024;
type my_word_length is range 31 downto ;
 subtype data_word is my_word_length range 7 downto 0;

یک زیرتایپ یا subtype هم به زیرمجموعه‌ای از یک تایپ که قبلا تعریف شده است، گفته می‌شود. آخرین مورد از مثال‌های بالا یک مورد از تعریف subtype را می‌بینیم که نوعی از داده به نام data_word را تعریف می‌کند زیرمجموعه‌ای از نوع داده‌ی my_word_length است که در خط دوم تعریف شده است. می‌بینیم که محدوده‌ی تایپ اصلی از ۳۱ تا ۰ و محدوده‌ی تایپ زیرمجموعه از ۷ تا ۰ است. یک مثال دیگر برای subtype.

subtype int_small is  integer range -1024 to +1024;

از نوع Floating-point

type cmos_level is range 0.0 to 3.3;
type pmos_level is range -5.0 to 0.0;
type probability is range 0.0 to 1.0;
subtype cmos_low_V is cmos_level range 0.0 to +1.8;

یک نکته‌ی مهمی که باید بدانید این است که floating point type توسط ابزارهای سنتز شرکت زایلینکس پشتیبانی نمی‌شوند پس استفاده از آنها همواره مجاز نیست.

از نوع physical

تعریف این نوع از داده، شامل تعریف زیرمجموعه‌های یک یکا یا واحد فیزیکی است. مثلا:

type conductance is range 0 to 2E-9
       units
          mho;
          mmho = 1E-3 mho;
          umho = 1E-6 mho;
          nmho = 1E-9 mho;
          pmho = 1E-12 mho;
       end units conductance;

و اگر بخواهیم آبجکت‌هایی را تعریف کنیم و نوع آنها را از همین تایپ‌هایی قرار دهیم که خودمان تعریف کرده‌ایم، مانند مثال‌های زیر رفتار می‌کنیم.

variable BUS_WIDTH: small_int :=24;
signal DATA_BUS: my_word_length;
variable VAR1: cmos_level range 0.0 to 2.5;
constant LINE_COND: conductance:= 125 umho;

(به فاصله‌ای که قبل از ذکر کردن نام یکا وجود دارد دقت کنید)

نوع داده‌ی فیزیکال هم توسط ابزار سنتز Xilinx Foundation Express پشتیبانی نمی‌شود.

اگر بخواهیم از تایپ‌هایی که خودمان تعریف کرده‌ایم استفاده کنیم، یا باید حتما تعریف آن تایپ را در قسمت بدنه‌ی معماری کد بیاوریم و یا اینکه آن را در یک پکیج قرار دهیم و آن پکیج را به کدمان اضافه کنیم. در ادامه کدی را می‌بینیم که روش دوم را در مورد پکیجی به نام my_types انجام داده است.

package my_types is
     type small_int is range  to 1024;
     type my_word_length is range 31 downto ;
     subtype data_word is my_word_length is range 7 downto 0;
     type cmos_level is range 0.0 to 3.3;
type conductance is range 0 to 2E-9
           units
                mho;
                mmho = 1E-3 mho;
                umho = 1E-6 mho;
                nmho = 1E-9 mho;
                pmho = 1E-12 mho;
           end units conductance;
end package my_types;

Enumerated Type .c

این نوع داده از لیستی از کاراکترها یا شناسه‌ها تشکیل می‌شود و برای مدل‌سازی یک طراحی در سطح abstract داده‌ی بسیار پرکاربردی است. سینتکس استفاده از آن به این صورت است.

type type_name is (identifier list or character literal) ;

چند مثال را برای نمونه ببینیم.

type my_3values is (‘0’, ‘1’, ‘Z’);
type PC_OPER  is (load, store, add, sub, div, mult, shiftl, shiftr);
type hex_digit  is (‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, 8’, ‘9’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’);
type state_type is (S0, S1, S2, S3);

 و چند مثال برای objectهایی که از نوع داده‌های فوق استفاده کنند.

signal SIG1: my_3values;
variable ALU_OP: pc_oper;
variable first_digit: hex_digit :=’0’;
signal STATE: state_type :=S2;

اگر به صورت دستی یک سیگنال را مقداردهی اولیه نکنیم، به صورت پیش‌فرض اولین داده‌ای که در سمت چپ لیست قرار دارد به عنوان مقدار اولیه‌ی آن قرار داده می‌شود.

داده‌های شمارشی هم یا باید در بدنه‌ی معماری تعریف شوند و یا اینکه به صورت پکیج به کد اضافه شوند. درست مانند روالی که در قسمت قبل توضیح دادیم.

به عنوان یک مثال از داده‌های شمارشی که در پکیج std_logic_1164 تعریف شده است، داده‌های از نوع std_ulogic هستند که تعریف آنها به این شکل است.

 type STD_ULOGIC is (
     ‘U’,                  -- uninitialized
     ‘X’,                  -- forcing unknown
     ‘0’,                   -- forcing 0
     ‘1’,                   -- forcing 1
     ‘Z’,                  -- high impedance
     ‘W’,                 -- weak unknown
     ‘L’,                  --  weak 0
     ‘H’.                  -- weak 1
     ‘-‘);                  -- don’t care

برای استفاده از این تایپ باید عبارت زیر را در کد اضافه کنیم.

 library ieee; use ieee.std_logic_1164.all;

بسیار ممکن است این حالت پیش بیاید که درایورهای مختلفی یک سیگنال را درایو کنند. در چنین حالتی ممکن است میان این درایورهای مختلف و حالتی که برای سیگنال ایجاب می‌کنند تعارض وجود داشته باشد به نحوی که مقدار سیگنال غیرقابل تعیین شود. مثلا؛ فرض کنید که یک سیگنال خروجی به نام OUT1 داشته باشیم که درایورهای آن همزمان هم خروجی یک گیت AND و هم خروجی یک گیت NOT باشد (یعنی هر دوی این گیت‌ها امکان مقدار گذاشتن روی این سیگنال خروجی را دارند)

خب، در چنین حالتی چطور باید مقدار این سیگنال را تعیین کرد؟ می‌توان از توابعی موسوم به resolution کمک گرفت. توابعی که معمولا باید توسط خود کاربر نوشته شوند و وظیفه‌ی آنها تعیین تکلیف برای چنین خروجی‌هایی است.

مخصوصا اگر سیگنال بلاتکلیف از نوع std_ulogic باشد، برای حل مشکل آن حتما باید از چنین توابعی استفاده کنیم. در این حالت می‌توانیم پکیج std_logic_1164 را به کد اضافه کنیم. این پکیج یکی از همین توابع را به صورت آماده در خود دارد و نام آن RESOLVED است. پس از آن برای سیگنال OUT1 به این شکل کد می‌نویسیم.

signal OUT1: resolved: std_ulogic;

هر کجا تعارضی بین خروجی درایورهای مختلف به وجود بیاید، RESOLVED وارد عمل شده و تصمیم می‌گیرد که خروجی سیگنال OUT1 بالاخره کدام یک از آنها باشد و البته لازم به ذکر است که نه تنها در حالت std_ulogic، بلکه اگر از subtype آن یعنی std_logic هم باشد باز هم می‌توان مشکل را برطرف کرد چون std_logic هم در پکیج std_logic_1164 تعریف شده است.

signal OUT1: std_logic;

Composite Type .dها یا داده‌های از نوع مرکب: آرایه‌ها و رکوردها

این نوع داده‌ها، آبجکت‌هایی هستند که از مجموعه‌ای از داده‌های مرتبط با هم در قالب یک آرایه (array) یا رکورد (record) تشکیل شده‌اند. پیش از آنکه بخواهیم از چنین نوع داده‌ای استفاده کنیم، ابتدا باید مشخص کنیم که از کدام حالت است، آرایه یا رکورد؟

Array type

داده‌های از نوع آرایه به فرم زیر تعریف می‌شوند.

type array_name is array (indexing scheme) of element_type;
type MY_WORD is array (15 downto 0) of std_logic;
type YOUR_WORD is array (0 to 15) of std_logic;
type VAR is array (0 to 7) of integer;
type STD_LOGIC_1D is array (std_ulogic) of std_logic;

در دو مثال اول از مثال‌های فوق، ما آرایه‌های یک بعدی از متغیرهای std_logic تعریف کرده‌ایم که به ترتیب از ۱۵ تا ۰ و از ۰ تا ۱۵ هستند. اما به مثال آخر دقت کنید. در اینجا هم یک آرایه‌ی یک بعدی از متغیرهای std_logic داریم اما ایندکس آنها از نوع std_ulogic است. یعنی این آرایه محتوی مقادیری است که ایندکس‌های آنها به این شکل نام‌گذاری شده‌اند.

Index: ‘U’  ‘X’  ‘0’  ‘1’  ‘Z’  ‘W’  ‘L’  ‘H’  ‘-‘

Element:

مطلب پیشنهادی:  آموزش FPGA و Verilog برای تازه کارها – سیستم‌‌های نهفته

بسیار خب، پس از آنکه تایپ‌ها را تعریف کردیم، حالا می‌توانیم آبجکت‌هایی را با این نوع داده تعریف کنیم.

signal MEM_ADDR: MY_WORD;
signal DATA_WORD: YOUR_WORD :=  B“1101100101010110”;
constant SETTING: VAR := (2,4,6,8,10,12,14,16);

در اولین مورد، سیگنال MEM_ADDR یک آرایه‌ی ۱۶ بیتی است که با مقداراولیه‌ی ۰ مقداردهی شده است. برای آنکه به هر کدام از المان‌های یک آرایه دسترسی داشته باشیم، باید به ایندکس آنها اشاره کنیم. مثلا MEM_ACCR(15) به آخرین بیت این آرایه (اولی از سمت چپ) اشاره دارد. اگر بخواهیم همزمان به تعدادی از المان‌ها دسترسی داشته باشیم، به این صورت اشاره می‌کنیم، MEM_ADDR(15 downto 8) یا DATA_WORD(0 to 7).

آرایه‌های فوق همگی یک‌بعدی بودند. اما ما می‌توانیم آرایه‌ها را به صورت چند بعدی نیز تعریف کنیم. مثلا برای تعریف آرایه‌های دو بعدی داریم:

type MY_MATRIX3X2 is array (1 to 3, 1 to 2) of natural;
type YOUR_MATRIX4X2 is array (1 to 4, 1 to 2) of integer;
type STD_LOGIC_2D is array (std_ulogic, std_ulogic) of std_logic;
variable DATA_ARR: MY_MATRIX :=((0,2), (1,3), (4,6), (5,7));

به این ترتیب متغیری که در خط آخری تعریف کرده‌ایم به این شکل مقداردهی اولیه می‌شود.

0  2 
1  3 
4  6 
5  7

حالا مثلا فرض کنید که بخواهیم به داده‌ی سطر سوم، ستون اول دسترسی داشته باشیم؛ یعنی عدد ۴. آن را به این صورت فراخوانی می‌کنیم، DATA_ARR (3,1).

در خط سوم از مثال‌های بالا، دیتا تایپی را معرفی کرده‌ایم که یک آرایه‌ی ۹×۹ با ایندکس‌هایی از جنس std_ulogic است.

گاهی اوقات، در زمان‌هایی که نوع داده‌ی ایندکس را قید می‌کنیم، بهتر است که دیگر ابعاد آرایه را ذکر نکنیم. به این روش unconstrained array type گفته می‌شود. به طور کلی سینتکس تعریف آرایه به این صورت است.

type array_name is array (type range <>) of element_type;

چند مثال را با هم می‌بینیم.

type MATRIX is array (integer range <>) of integer;
type VECTOR_INT is array (natural range <>) of integer;
type VECTOR2 is array (natural range <>, natural range <>) of std_logic;

زمانی که object را تعریف می‌کنیم محدوده‌ی آرایه مشخص خواهد شد.

variable MATRIX8: MATRIX (2 downto -8) := (3, 5, 1, 4, 7, 9, 12, 14, 20, 18);
variable ARRAY3x2: VECTOR2 (1 to 4, 1 to 3)) := ((‘1’,’0’), (‘0’,’-‘), (1, ‘Z’));

Record Type

یکی دیگر از نوع داده‌های مرکب recordها هستند. یک رکورد از تعدادی المان تشکیل می‌شود که ممکن است از یک نوع هم نباشند. سینتکس یک داده‌ی رکورد به این شکل است.

type name is
     record
            identifier :subtype_indication;
                       :
            identifier :subtype_indication;
     end record;

و به عنوان یک مثال:

type MY_MODULE  is
     record
            RISE_TIME     :time;
            FALL_TIME   : time;
            SIZE                : integer range 0 to 200;
            DATA              : bit_vector (15 downto 0);
     end record;
signal A, B: MY_MODULE;

برای دسترسی داشتن به المان‌های یک رکورد یا تخصیص دادن مقدار به آنها می‌توان از یکی از روش‌های زیر استفاده کرد.

A.RISE_TIME <= 5ns;
A.SIZE <= 120;
B <= A;

e. تبدیل تایپ‌های مختلف به یکدیگر

همانطور که گفتیم VHDL، زبانی بسیار تایپ محور است و به همین دلیل شما اصلا مجاز نیستید که داده‌ای از یک نوع را، به سیگنالی که از نوع دیگری تعریف شده است، نسبت بدهید. نکته‌ی دیگری هم که در این رابطه وجود دارد، این است که به طور کلی بهتر است که سعی کنید در یک طراحی، تا جایی که ممکن است از تایپ یکسانی برای سیگنال‌ها استفاده کنید (مثلا تا جایی که ممکن است، اگر از std_logic استفاده کنیم، از اینکه مخلوطی از std_logic و bit را با هم داشته باشیم بهتر است) به این ترتیب اتصال دادن آنها به یکدیگر نیز راحت‌تر خواهد بود. اما گاهی هر قدر هم تلاش کنیم ممکن است نتوانیم تمام سیگنال‌ها را با تایپ یکسانی استفاده کنیم. در این حالت برای آنکه بتوانیم داده‌های یک سیگنال را به سیگنال دیگری انتقال دهیم، باید ابتدا تبدیل تایپ انجام دهیم. در بسیاری از پکیج‌های ieee، توابعی وجود دارند که این تبدیل را برای ما انجام می‌دهند. مثلا دو پکیج std_logic_1164 و std_logic_arith. توابعی که در جدول زیر می‌بینید، در پکیج std_logic_1164 وجود دارند و قابل استفاده هستند.

آموزش VHDL Primer

پکیج‌های دیگری هم مانند IEEE std_logic_unsigned و IEEE std_logic_arith هستند که توابع تبدیل بیشتری را هم ارائه می‌کنند. مثلا تبدیل از نوع integer به نوع std_logic_vector و برعکس آن.

در ادامه یک مثال را با هم می‌بینیم.

entity QUAD_NAND2 is
     port (A, B: in bit_vector(3 downto 0);
        out4: out std_logic_vector (3 downto 0));      
end QUAD_NAND2;
architecture behavioral_2 of QUAD_NAND2 is
begin
          out4 <= to_StdLogicVector(A and B);
end behavioral_2;

عبارت «A and B» که از نوع bit_vector است برای آنکه بتواند به سیگنال خروجی out 4 محول شود، باید با آن هم نوع شود. پس باید به std_logic_vector تبدیل شود. سینتکس کلی یک تبدیل تایپ به این شکل است.

type_name (expression);

برای آنکه این تبدیل، تبدیل معتبری باشد، عبارتی که می‌خواهیم نوع آن را تبدیل کنیم باید خود دارای نوعی باشد که در آن کتابخانه و پکیج استفاده شده، قابل تبدیل به نوع جدید (type_name) باشد. بنابراین تمام شرایطی که برای یک تبدیل درست باید وجود داشته باشد از این قرار است.

  • تبدیل تایپ بین انواع مختلف داده‌های integer یا آرایه‌های از یک نوع، امکان پذیر است.
  • برای تبدیل آرایه‌ها به هم باید:
  1. دارای طول یکسان باشند.
  2. المان‌های داخل آنها از یک نوع باشند.
  3. المان‌ها دارای نوع تبدیل‌پذیری باشند.
  • داده‌های از نوع شمارشی (enumerated) قابل تبدیل نیستند.

f. دریافت مشخصات از آبجکت‌ها

در زبان VHDL، برای دریافت مشخصات از پنج روش پشتیبانی می‌شود.

آنهایی که از پیش تعریف شده‌اند قابل اعمال به نام سیگنال‌ها یا متغیرها هستند اما در کل از روش attribute برای دریافت اطلاعات و مشخصاتی در مورد سیگنال‌ها و متغیرها و تایپ داده‌ها و … استفاده می‌شود. برای این کار ابتدا یک علامت ‘  و سپس نام آن مشخصه‌ای که لازم داریم را قید می‌کنیم.

Signal attributs

مشخصه‌هایی که از یک سیگنال می‌توان به دست آورد را در جدول زیر می‌بینیم.

آموزش VHDL Primer

یک مثال از کاربرد آنها را هم ببینیم.

if (CLOCK’event and CLOCK=’1’) then …

عبارت فوق در واقع فرا رسیدن لبه‌ی مثبت (بالارونده) کلاک را رصد می‌کند، یا مثلا اگر بخواهیم ببینیم که از زمان آخرین لبه‌ی کلاک چقدر گذشته است از دستور زیر استفاده می‌کنیم.

CLOCK’last_event
Scalar attributes

بسیاری از ویژگی‌های عددی و اسکالر داده‌های از نوع اسکالر را می‌توان مطابق جدول زیر بدست آورد.

آموزش VHDL Primer

چند مثال را هم با هم ببینیم.

type conductance is range 1E-6 to 1E3
            units mho;
            end units conductance;
type my_index is range 3 to 15;
type my_levels is (low, high, dontcare, highZ);
conductance’right          returns: 1E3
conductance’high           1E3
conductance’low            1E-6
my_index’left              3
my_index’value(5)          “5”
my_levels’left             low
my_levels’low              low
my_levels’high             highZ
my_levels’value(dontcare)      “dontcare”

Array attributes

با استفاده از این دستورات، می‌توان ویژگی‌ها و مقادیر مربوط به ایندکس‌های مختلف یک آرایه را بدست آورد.

در جدول زیر می‌بینیم که چه attributeهایی برای آرایه‌ها مجاز هستند.

آموزش VHDL Primer

عدد N که در پرانتزها وجود دارد، مشخص کننده‌ی ابعاد آرایه است. اگر N = 1 باشد می‌توان آن را ننوشت (مانند مثال زیر) به این مثال خوب دقت کنید تا کاربرد array attribute را متوجه شوید.

type MYARR8x4 is array (8 downto 1, 0 to 3) of boolean;
type MYARR1 is array (-2 to 4)  of integer;
MYARR1’left       returns:      -2
MYARR1’right                     4
MYARR1’high                      4
MYARR1’reverse_range             4 downto to -2
MYARR8x4’left(1)                 8
MYARR8x4’left(2)                 0
MYARR8x4’right(2)                3
MYARR8x4’high(1)                 8
MYARR8x4’low(1)                  1
MYARR8x4’ascending(1)            False

7. عملگرها (اپراتورها)

در زبان VHDL کلاس‌های مختلفی از عملگرها تعریف شده‌اند که می‌توان آنها را بر روی سیگنال‌ها، متغیرها و یا ثابت‌ها اعمال کرد. در تصویر زیر این کلاس‌بندی را به طور خلاصه می‌بینیم.

بالاترین اولویت اجرا مربوط به اپراتورهای کلاس ۷ است. همینطور به ترتیب اولویت بعدی مربوط به کلاس ۶ و آخرین اولویت مربوط به عملگرهای کلاس ۱ است. بنابراین اگر در یک عبارت از پرانتز استفاده نشده باشد، اولویت اجرای عملگرهای موجود در آن، به همین ترتیبی است که گفتیم. ضمن آنکه اپراتورهایی که در یک کلاس قرار دارند هم از اولویت اجرای یکسان برخوردارند و چنانچه همزمان با هم در عبارتی وجود داشته باشند، ترتیب اجرای آنها از چپ به راست خواهد بود. به عنوان مثال سه بردار (’X (=’010’), Y(=’10’), and Z (‘10101 که از نوع std_ulogic_vector هستند و عبارت not X & Y xor Z rol 1 را در نظر بگیرید.

از نظر اولویت اجرای عملگرها این عبارت در واقع چنین خواهد بود.

((not X) & Y) xor (Z rol 1)

پس اگر مطابق عبارت فوق مقدار متغیرها را جاگذاری کنیم و اپراتورها را روی آنها اعمال کنیم داریم:

((101) & 10) xor (01011)  =(10110) xor (01011) = 11101

(xor به صورت بیت به بیت اجرا می‌شود)

a. عملگرهای منطقی

عملگرهای منطقی شامل and ،or ،nand ،nor ،xor و xnor هستند که بر روی تایپ‌های bit ،Boolean ،std_logic ،std_ulogic و بردارهای آنها قابل اعمال‌اند. از این عملگرها برای توصیف عبارت‌های منطقی بولین و اجرای عملیات‌های مبتنی بر بیت بر روی داده‌های گفته شده و یا وکتورهایی از آنها استفاده می‌شود، نتیجه‌ی حاصل شده نیز چیزی از همان جنس خواهد بود.

این عملگرها بر روی متغیرها، سیگنال‌ها و ثابت‌ها قابل اعمال هستند.

نکته‌ی دیگری که باید مورد توجه قرار گیرد این است که عمگلرهای nand و nor شرکت‌پذیر نیستند و برای پیشگیری از به وجود آمدن خطای سینتکسی، در زمان استفاده از آنها باید حتما از پرانتز استفاده کرد. یعنی مثلا نوشتن عبارت X nand Y nand Z با سینتکس ارور مواجه خواهد شد و به جای آن باید چنین نوشت، (X nand Y) nand Z.

b. عملگرهای مقایسه‌‌ای

این عملگرها یک رابطه‌ی مقایسه‌ای یا relational بین دو مقدار از نوع اسکالر را انجام داده و نتیجه را به صورت یک داده‌ی بولین صحیح یا غلط (true / false) اعلام می‌کنند.

آموزش VHDL Primer

اگر دقت کرده باشید می‌بینید که نماد یکی از این عملگرها یعنی عملگر مقایسه‌ای کوچکتر یا مساوی به صورت => است که دقیقا همان نماد عملگر assign است که با کمک آن یک مقدار را به یک سیگنال یا متغیر نسبت می‌دادیم. بنابراین در موارد کاربرد این نماد باید دقت کنیم که کدام یک از این دو مد نظر است. مثلا در مثالی که در ادامه می‌آید اولین => به کار رفته عملگر assign است. مثال‌ها را ببینید.

variable STS               : Boolean;
constant A                 : integer :=24;
constant B_COUNT           : integer :=32;
constant C                 : integer :=14;
STS <= (A < B_COUNT) ;  -- will assign the value “TRUE” to STS
STS <=  ((A >= B_COUNT) or (A > C));    -- will result in “TRUE”
STS <=  (std_logic (‘1’, ‘0’, ‘1’) < std_logic(‘0’, ‘1’,’1’));--makes STS “FALSE”

برای آرایه‌های گسسته، عمل مقایسه به صورت مقایسه‌ی درایه به درایه انجام می‌شود و از درایه‌ی سمت چپ شروع شده و به ترتیب به سمت راست‌ترین (آخرین) درایه می‌رسد. دو مثال آخر از مثال‌های بالا از همین نوع هستند.

c. عملگرهای انتقال

این عملگرها یک عملیات shift یا rotate (چرخش) را به صورت bit-wise بر روی آرایه‌های یک بعدی از نوع بیت یا بولین یا std_logic انجام می‌دهند.

آموزش VHDL Primer

متغیری که قرار است عملیات جابه‌جایی یا چرخش روی آن انجام شود در سمت چپ عملگر و تعداد شیفت‌ها یا rotateها نیز در سمت راست عملگر قرار می‌گیرند. به مثال های زیر دقت کنید.

variable NUM1          :bit_vector := “10010110”;
NUM1 srl 2;

نتیجه‌ی عملیات فوق «۰۰۱۰۰۱۰۱» خواهد بود.

اگر یک integer منفی داده شده باشد، عملگرهای انتقال به صورت برعکس روی متغیر عمل خواهند کرد. یعنی مثلا عملگر شیفت به راست به صورت شیفت به چپ بر روی آن اجرا می‌شود. مثلا عبارت NUM1 srl –2 معادل NUM1 sll 2 عمل خواهد کرد و نتیجه «۰۱۰۱۱۰۰۰» می‌شود.

مثال دیگر برای عملیات‎‌های جابه‌جایی، انجام آنها بر روی رشته بیت‌ها (بردارها) است. مثلا بردار ”A = “101001 را در نظر بگیرید، جدول زیر حالت‌های مختلف انتقال را برای آن بررسی می‌کند.

آموزش VHDL Primer

d. عملگرهای گروه جمع

این عملگرها برای انجام عملیات‌های ریاضی (جمع و تفریق) استفاده می‌شوند و بر روی داده‌هایی از هر تایپ قابل اعمال هستند.

عملگر اتصال (concatenation) با نماد & نیز برای کنار هم قرار دادن و پیوند زدن دو بردار و تبدیل آنها به یک بردار بزرگتر استفاده می‌شود. برای استفاده از این عملگرها علاوه بر پکیج ieee.std_logic_1164، باید پکیج‌های ieee.std_logic_unsigned.all یا std_logic_arith را نیز به کد اضافه کرد.

آموزش VHDL Primer

یکی از کاربردهای رایج برای عملگر & زمانی است که می‌خواهیم چند سیگنال را در کنار هم قرار داده و یک bus بسازیم.

signal MYBUS                :std_logic_vector (15 downto 0);
signal STATUS               :std_logic_vector (2 downto 0);
signal RW, CS1, CS2         :std_logic;
signal MDATA                :std_logic_vector ( 0 to 9);
MYBUS <= STATUS & RW & CS1 & SC2 & MDATA;

و مثال‌هایی دیگر.

MYARRAY (15 downto 0) <= “1111_1111” & MDATA (2 to 9);
NEWWORD <= “VHDL” & “93”;

در مثال اول چه اتفاقی می‌افتد؟ ۸ بیت ابتدایی MYARRAY از سمت چپ با عدد ۱ پر می‌شوند و بقیه‌ی آن با ۸ بیت انتهایی MDATA از سمت راست. مثال آخر هم به آرایه‌ای از کاراکترها به صورت «VHDL93» منجر خواهد شد.

e. عملگرهای یگانی

عملگرهایی به صورت «+» و «-» برای تعیین علامت داده‌هایی از نوع عددی (numeric).

آموزش VHDL Primer

f. عملگرهای گروه ضرب

از این عملگرها برای انجام عملیات‌‌ها و توابع ضرب و تقسیم بر روی داده‌های عددی (integer و floating point) استفاده می‌شوند.

آموزش VHDL Primer

عملگرهای گروه ضربی هم‌چنین در مواردی که یکی از عملوند‌ها از نوع physical type و دیگری از نوع integer یا real باشند نیز قابل تعریف و استفاده هستند.

عملگرهای محاسبه‌ی باقی‌مانده (rem) و محاسبه تقسیم پیمانه‌ای (mod) به صورت زیر تعریف می‌شوند.

A rem B = A –(A/B)*B

(A/B از نوع integer است)

A mod B = A – B * N

(N از نوع integer است)

نتیجه‌ی عملیات rem همیشه دارای همان علامتی است که عملوند اول داشته باشد اما نتیجه‌ی عملیات mod دارای علامت عملوند دوم است.

چند مثال از کاربرد این عملگرها را با هم می‌بینیم.

11 rem 4         results in 3
(-11) rem 4      results in -3
9 mod 4          results in 1
7 mod (-4)       results in –1  (7 – 4*2 = -1)

g. اپراتورهای متفرقه

اپراتورهای توان‌رسانی و محاسبه‌ی قدر مطلق که بر روی داده‌های عددی قابل اعمال هستند. عملگر not هم عملگر معکوس کردن منطقی است و به داده‌ای از همان نوع با علامت برعکس منجر می‌شود.

آموزش VHDL Primer

8. مدل‌سازی رفتاری: عبارت‌های ترتیبی

در قسمت‌های قبل هم توضیح دادیم که زبان VHDL زبانی است که می‌توان به کمک آن مدارات دیجیتال را در سطوح مختلفی نمایش داد و توصیف کرد. از جمله‌ روش‌های توصیف و مدل‌سازی نیز روش‌های توصیف ساختاری و مدل‌سازی رفتاری هستند. در این قسمت می‌خواهیم قواعد توصیف رفتاری مدارها را با تکیه بر عبارات ترتیبی (sequential) توضیح دهیم. مبنای مدل‌سازی sequential، براساس ساخت فرآیندهاست. در ادامه با هم خواهیم دید که چگونه با این روش می‎توان پیچیده‌ترین مدارهای دیجیتال را نیز مدل کرد.

a. فرآیند

عبارت‌هایی که برای توصیف فرآیند‌های یک مدار استفاده می‌کنیم، پایه‌های روش مدل‌سازی رفتاری مدارها هستند و به ما کمک می‌کنند که بتوانیم رفتار مدار را در طول زمان به صورت ترتیبی بیان کنیم. سینتکس کلی یک عبارت توصیف فرآیند در زیر آمده است.

[process_label:] process [ (sensitivity_list) ] [is]
     [ process_declarations]
begin
      list of sequential statements such as:
           signal assignments
           variable assignments 
           case statement
           exit statement
           if statement
           loop statement
           next statement           
           null statement
           procedure call
           wait statement
end process [process_label];

بسیارخب، حالا بیایید یک مدار ساده را با همین روش توصیف کنیم. مثلا یک D-flip flop حساس به لبه‌ی بالارونده‌ی ساعت، با سیگنال ورودی clear که آسنکرون است.

library ieee;
use ieee.std_logic_1164.all;
entity DFF_CLEAR is
     port (CLK, CLEAR, D : in std_logic;
           Q : out std_logic);
end DFF_CLEAR;
architecture BEHAV_DFF of DFF_CLEAR is
begin
DFF_PROCESS: process (CLK, CLEAR)
     begin
           if (CLEAR = ‘1’) then
                Q <= ‘0’;
           elsif (CLK’event and CLK = ‘1’) then
                Q <= D;
           end if;
     end process;
end BEHAV_DFF;

عبارت‌های توصیف یک فرآیند را در بخش بدنه‌ی توصیف معماری می‌نویسیم و به عنوان یک واحد، اجرای آن همزمان با سایر واحدهای موجود در آن بدنه است اما در داخل خود این فرآیند، اجرای جملات به صورت ترتیبی است.

این واحد مجزا که در درون بدنه‌ی معماری قرار می‌گیرد، مانند سایر بخش‌هایی که وجود دارند و اجرای همزمان دارند، برای ارتباط برقرار کردن با قسمت‌های دیگر معماری، می‌تواند بر روی سیگنال‌ها و پورت‌های ورودی و خروجی داده بنویسد یا از آنها داده‌ای بخواند. بنابراین می‌توان به سیگنال‌های وارد شونده یا خارج‌شونده از این واحدها عبارتی assign کرد. مثلا به خروجی فیلپ فلاپ مثال بالا یعنی Q.

ضمنا عبارت ’CLK’event and CLK = ‘1 که در کد فوق به کار رفته است، به معنای بررسی رخ دادن یا ندادن رویداد لبه‌ی بالارونده‌ی ساعت است. (اینکه یک رویداد روی سیگنال کلاک رخ بدهد «و» این رویداد لبه‌ی بالارونده باشد)

لیست حساسیت یا همان sensitivity list، مجموعه‌ سیگنال‌هایی هستند که می‌خواهیم فرآیند ما به رخداد آنها واکنش نشان بدهد. یعنی هر تغییری که در هرکدام از این سیگنال‌ها رخ بدهد، در صورتی که مطابق عبارت‌های قید شده در لیست حساسیت فرآیند ما باشد، موجب می‌شود که فرآیند اجرا شود. اما ممکن است بپرسید اگر فرآیندی داشته باشیم که بخواهیم همواره اجرا شود و نه فقط در زمان رخداد تغییراتی خاص در سیگنال‌هایی خاص، یعنی در واقع لیست حساسیتی وجود نداشته باشد. در آن صورت چه باید کرد؟ پاسخ این است که مشکلی نیست و قسمت لیست حساسیت یک فرآیند می‌تواند خالی باشد. در این صورت فرآیند همواره اجرا می‌شود اما برای آنکه مطمئن باشیم اجرای مداوم و پشت سر هم آن موجب بروز اختلالی در سیگنال‌ها یا مقادیر مدار نمی‌شود، باید از تابع wait استفاده کنیم. به این ترتیب به اجراهای مختلف فرآیند وقفه‌ای وجود خواهد داشت. مقدار این تاخیر را خودمان می‌توانیم تعیین کنیم.

نکته‌ی دیگر این است که این امکان نیز وجود دارد که همزمان هم لیست حساسیت تعریف کنیم و هم wait داشته باشیم.

تمام متغیرها و ثابت‌هایی که در یک فرآیند قرار است استفاده شوند، قبل از شروع توصیف چگونگی فرآیند یعنی قبل از کلمه‌ی begin باید در بخش process_declarations کاملا معرفی شوند. begin یک کلمه‌ی کلیدی است که به معنای سیگنالی برای آغاز قسمت محاسباتی و اجرایی فرآیند تفسیر می‌شود. عبارت‌های درون این قسمت درست مانند یک برنامه‌ی نرم‌افزاری به صورت ترتیبی شروع به اجرا شدن می‌کنند.

باید دقت داشت که تخصیص مقدار به متغیرها (variable assignments) به صورت در لحظه و بدون تاخیر انجام می‌شود و نماد عملگر آن «=:» است. درست برعکس تخصیص مقدار به سیگنال‌ها (signal assignments) که عملگر آن «=>» است و اجرای آن اندکی تاخیر دارد.

به این ترتیب هرگونه تغییری که در متغیرها رخ بدهد، به صورت آنی در بقیه‌ی قسمت‌های فرآیند که از آن متغیر استفاده می‌کنند هم اجرا می‌شود. تفاوت بین سیگنال‌ها و متغیرها را در قسمت ۵ دقیق‌تر توضیح دادیم، اگر نیاز به یادآوری داشتید یک بار دیگر آن بخش را مرور کنید.

در مثال قبلی که یک D-flip flop بود به خوبی دیدیم که چگونه می‌توان یک مدار ترتیبی را با کمک ساختار توصیف فرآیند، توصیف کرد. اما جالب است بدانید که علیرغم اینکه ساختارهای توصیف فرآیند به طور عمده به منظور توصیف مدارهای ترتیبی ایجاد شده‌اند اما می‌توان از آنها برای توصیف مدارهای ترکیبی (combinational) نیز استفاده نمود.

در مثالی که در ادامه می‌آید می‌بینیم که چگونه این کار امکان‌پذیر است. یک full adder داریم که می‌خواهیم آن را از دو واحد half adder بسازیم. هم‌چنین در ضمن این مثال یاد می‌گیریم که چگونه سیگنال‌های تولیدی توسط یک فرآیند، می‌توانند سیگنال‌های ورودی برای یک فرآیند دیگر باشند. توصیف بولین برای Full adder و Half adder را به صورت زیر داریم.

Half Adder :
     S_ha = (AÅB)   and C_ha = AB
Full Adder:
     Sum = (AÅB)ÅCin = S_ha ÅCin
     Cout = (AÅB)Cin + AB = S_ha.Cin + C_ha

و در تصویر بعدی می‌بینیم که ساختار تمام جمع‌کننده چگونه مدل‌سازی می‌شود.

آموزش VHDL Primer
یک Full Adder که با استفاده از دو Half Adder ساخته شده و با دو فرآیند P1 و P2 مدل‌ شده است.
library ieee;
use ieee.std_logic_1164.all;
entity FULL_ADDER is
     port (A, B, Cin : in std_logic;
           Sum, Cout : out std_logic);
end FULL_ADDER;
architecture BEHAV_FA of FULL_ADDER is
signal int1, int2, int3: std_logic;
begin
-- Process P1 that defines the first half adder
P1: process (A, B)
     begin
           int1<= A xor B;
           int2<= A and B;
     end process;
-- Process P2 that defines the second half adder and the OR -- gate
P2: process (int1, int2, Cin)
     begin
          Sum <= int1 xor Cin;
           int3 <= int1 and Cin;
           Cout <= int2 or int3;
     end process;
end BEHAV_FA;

البته لازم به ذکر است که این توصیف رفتاری از مدار Full adder می‌توانست ساده‌تر و تنها در قالب یک فرآیند هم انجام شود.

b. استفاده از ساختار if

در ساختار شرطی، تعدادی عبارت ترتیبی وجود دارند (مانند حالت عادی) که اجرای آنها منوط به برقراری شرایطی است که در عبارت if تعیین می‌کنیم (تفاوت با حالت عادی)

سینتکس کلی این ساختار به صورت زیر است.

if condition then
           sequential statements
        [elsif condition then
           sequential statements ]
        [els
           sequential statements ]
end if;

هر کدام از شرط‌ها یک عبارت بولین است. یک ساختار if به این ترتیب اجرا می‌شود که شرط‌ها دانه به دانه و به همان ترتیبی که نوشته شده‌اند بررسی می‌شوند تا زمانی که به یکی از آنها برسیم که برقرار باشد، به این نقطه که برسیم، عبارت تحت آن شرط اجرا خواهد شد.

استفاده از ifهای تودرتو نیز در ساختار شرطی مجاز است یعنی می‌توان در داخل یکی از شرط‌های یک ساختار شرطی، یک ساختار شرطی دیگر هم قرار داد.

یکی از مثال‌هایی که تا پیش از این برای ساختار if داشتیم، در D-flip flop بود. می‌توانید برگردید و کد آن ‌را یک بار دیگر مرور کنید.

با کمک ساختار If حتی می‌توان مدارهای ترکیبی را هم توصیف کرد. مثلا یک مالتی‌پلکسر ۴ به ۱ با ورودی‌های A ،B ،C و D و سیگنال‌های انتخاب S0 و S1 را در نظر بگیرید. یک مدار ترکیبی است که به نظر می‌رسد باید با ساختار فرآیند آن را توصیف کرد. اما با هم می‌بینیم که ساختارهای دیگری از جمله Conditional Signal Assignmentها مانند (When-else) یا (Select)  چقدر کار را راحت‌تر خواهند کرد.

entity MUX_4_1a is
   port (S1, S0, A, B, C, D: in std_logic;
           Z: out std_logic);
   end MUX_4_1a;
architecture behav_MUX41a of MUX_4_1a is
begin
   P1: process (S1, S0, A, B, C, D)
   begin
     if (( not S1 and not S0 )=’1’) then
           Z <= A;
     elsif (( not S1 and S0) = ‘1’) then
           Z<=B;
     elsif ((S1 and not S0) =’1’) then
           Z <=C;
     else
           Z<=D;
end if;
   end process P1;
end behav_MUX41a;

و حالا با یک پیاده‌سازی قدری متفاوت‌تر از همان مالتی‌پلکسر:

if S1=’0’ and  S0=’0’ then
     Z <= A;
       elsif S1=’0’ and  S0=’1’ then
          Z <= B;
       elsif S1=’1’ and  S0=’0’ then
          Z <= C;
       elsif S1=’1’ and  S0=’1’ then
          Z <= D;
end if;

از ساختار If معمولی در پیاده‌سازی نمودار حالت استفاده می‌کنند. مثلا در mealy machineها که در ادامه بیشتر با آنها آشنا می‌شویم.

c. استفاده از ساختار case

در این قسمت تعداد زیادی حالت (case) وجود دارد که بسته به اینکه مقدار سیگنال مورد نظر کدام یک از این حالت‌ها باشد، تنها یکی از آنها اجرا خواهد شد. سینتکس آن به این صورت است.

case expression is
      when choices =>
         sequential statements
      when choices =>
         sequential statements
         -- branches are allowed
      when others => sequential statements ]
end case;

 عبارتی که در مقابل case نوشته می‌شود، باید قابل ارزیابی به صورت یک عدد یا یک آرایه‌ی یک ‌بعدی شمارشی (مثلا یک bit_vector) باشد. کار case این است که مقدار این عبارت را در هر لحظه ارزیابی کند و آن را با حالات مختلفی که ذکر شده‌اند (یعنی choiceها) مقایسه کند. هر کدام از آنها که با مقدار عبارت برابر بود، عبارت ترتیبی پس از آن اجرا خواهند شد. چند قانون هم وجود دارد که باید به آنها پایبند بود.

  • نمی‌توان حالات مختلف را طوری چید که یک حالت دوبار تکرار شده باشد یا حالت‌های مختلفی با هم هم‌پوشانی داشته باشند. این کار مانند این است که برای یک وضعیت یکسان دو دستور مختلف تعریف کرده‌ باشیم که قطعا با خطا مواجه خواهیم شد.
  • تنها در صورتی می‌توانیم حالت «when others» را اضافه نکنیم که تمام وضیت‌های ممکن برای آن expression را پوشش داده‌ باشیم و هیچ حالتی بدون دستور نمانده باشد.

در ادامه برای ساختار case یک مثال می‌بینیم که در آن برای متغیری از جنس شمارشی حالات مختلف را تعیین کرده‌ایم. می‌خواهیم وضعیت‌های مختلف را براساس سیگنال GRADES تعریف کنیم و تصمیم بگیریم که در هر لحظه براساس مقدار این سیگنال چه دستوری باید اجرا شود. می‌خواهیم اگر مقدار آن در محدوده‌ی ۵۱ تا ۶۰ بود، سیگنال خروجی D=1 شود. اگر در محدوده‌ی ۶۱ تا ۷۰ بود سیگنال خروجی C=1 شود و زمانی که هر مقدار دیگری داشت در وضعیت others قرار بگیرید و خروجی F=1 شود.

library ieee;
use ieee.std_logic_1164.all;
entity GRD_201 is
     port(VALUE: in integer range 0 to 100;
           A, B, C, D: out bit);
end GRD_201;
architecture behav_grd of GRD_201 is
begin
     process (VALUE)
       A <= ’0’;
       B <= ’0’;
       C <= ’0’;
       D <= ’0’;
       F <= ’0’;
       begin
           case VALUE is
            when 51 to 60 =>
                D <= ’1’;
            when 61 to 70 | 71 to 75 =>
                C <= ’1’;
            when 76 to 85 =>
                B <= ’1’;
             when 86 to 100 =>
                A <= ’1’;
             when others  =>
                F <= ‘1’;
           end case;
     end process;
end behav_grd;

در کد فوق ما از نماد خط عمودی (|) که معادل عملگر OR است استفاده کردیم تا بتوانیم بازه‌های مختلف متصور برای یک سیگنال را در کنار هم نمایش دهیم. استفاده از این نماد مخصوصا برای زمان‌هایی که این بازه‌ها در مجاورت هم قرار ندارند، بسیار کاربردی است. (یعنی مثلا بازه‌ی 0 to 4 | 6 to 10 )

یک مثال دیگر برای کاربرد ساختار case مالتی‌پلکسر ۴ به ۱ است.

entity MUX_4_1 is
   port ( SEL: in std_logic_vector(2 downto 1);
           A, B, C, D: in std_logic;
           Z: out std_logic);
   end MUX_4_1;
architecture behav_MUX41 of MUX_4_1 is
begin
   PR_MUX: process (SEL, A, B, C, D)
   begin
     case SEL is
           when “00” => Z <= A; 
           when “01” => Z <= B;      
           when “10” => Z <= C;                  
           when “11” => Z <= D;
           when others => Z <= ‘X’;
     end case;
   end process PR_MUX;
end behav_MUX41;

حالت when others زمانی رخ می‌دهد که سیگنال SEL مقداری غیر از آن مقادیر مشخص شده داشته باشد یعنی:

SEL=”0X”, “0Z”, “XZ”, “UX”,…

دقت داشته باشید که مدار mux را که یک مدار ترکیبی است می‌توان با ساختارهای دیگری نیز پیاده‌سازی کرد.

مطلب پیشنهادی:  دانلود کتاب آموزش مدار منطقی

نکته‌ی دیگر آنکه از آنجا که ساختار case هم یک ساختار ترتیبی است می‌توان آن را به صورت تو‌در‌تو نیز استفاده کرد. یعنی در دل یک ساختار case، می‌توان یک ساختار case دیگر قرار داد و همینطور ادامه داد.

d. ساختار حلقه

از ساختار حلقه برای این استفاده می‌کنیم که بخواهیم مجموعه‌ای از عبارات ترتیبی را چندین و چند بار تکرار کنیم. سینتکس کلی برای استفاده از این ساختار را مشاهده می‌کنید.

[ loop_label :]iteration_scheme loop
           sequential statements
           [next  [label] [when condition];
           [exit  [label] [when condition];
end loop [loop_label];

 استفاده از labelها اختیاری است اما برای زمانی که بخواهیم از حلقه‌های تودرتو استفاده کنیم بسیار مفید است.

عبارت‌های next و exit هم عبارت‌هایی ترتیبی هستند که تنها در داخل حلقه‌ی for می‌توان از آنها استفاده نمود. کاری که next می‌کند این است که اجرای حلقه‌ای که در حال اجرا است را در همان‌ مکان متوقف نموده و دور بعدی حلقه را آغاز می‌کند. Exit هم مانند next اجرای فعلی را در همان نقطه متوقف کرده و دستورات بعدی را نادیده می‌گیرد اما تفاوت آن با next در این است که در حالت exit اجرای حلقه کلا متوقف می‌شود و دیگر دور بعدی آن از سر گرفته نمی‌شود بلکه به دستوراتی که بعد از حلقه قرار دارند می‌رویم.

به طور کلی در حلقه‌ها سه مدل تکرار دور را می‌توانیم داشته باشیم.

  • حلقه‌های ساده (basic loops)
  • حلقه‌های while
  • حلقه‌های for

ساختار حلقه‌‌های ساده

تکرارهای متوالی این حلقه با برنامه و طرح به خصوصی نیست و همینطور پشت سر هم اجرا می‌شود تا زمانی که به یک عبارت next یا exit برسد. در ساختار آنها درست مانند حلقه‌های while (که در ادامه آن‌ را بررسی می‌کنیم) باید حداقل یک عبارت wait وجود داشته باشد. مثلا یک شمارنده‌ی ۵ بیتی که قرار است از ۰ تا ۳۱ بشمارد را در نظر بگیرید. زمانی که به عدد ۳۱ برسد دوباره از ۰ شروع خواهد کرد. اما با استفاده از دستور wait کاری کرده‌ایم که این کار را (از سر گرفتن شمارش از ۰) تنها زمانی انجام دهد که کلاک از ۰ به ۱ تغییر می‌کند.

entity COUNT31 is
   port ( CLK: in std_logic;
           COUNT: out integer);
   end COUNT31;
architecture behav_COUNT of COUNT31 is
begin
   P_COUNT: process
     variable intern_value: integer :=0;
   begin
     COUNT <= intern_value;
     loop
        wait until CLK=’1’;
   intern_value:=(intern_value + 1) mod 32;
        COUNT <= intern_value;
     end loop;
end process P_COUNT;
end behav_COUNT;

متغیر داخلی intern_value را برای آن تعریف کردیم که همانطور که قبلا هم اشاره کردیم از خروجی‌های یک فرآیند نمی‌تواند در دل خود آن فرآیند استفاده نمود.

ساختار حلقه‌های while

ساختار while … loop برای تکرار حلقه با استفاده از یک عبارت بولین شرط تعیین می‌کند. یعنی تا زمانی که ارزیابی شرط آن را True اعلام کند، حلقه به تکرار خود ادامه می‌دهد. به محض اینکه به نقطه‌ای برسیم که آن عبارت بولین دیگر true نباشد، تکرار حلقه متوقف می‌شود. سینتکس چنین حلقه‌هایی به این شکل است.

loop_label :] while condition loop
           sequential statements
           [next  [label] [when condition];
           [exit  [label] [when condition];
end looploop_label ];

قبل از هر بار اجرای حلقه condition چک می‌شود، حتی در اولین اجرا. در صورتی که برقرار نباشد حلقه اجرا نخواهد شد.

ساختار حلقه‌های for

در این حالت با استفاده از اعداد صحیح، خودمان تعداد دفعات تکرار حلقه را مشخص می‌کنیم. سینتکس این ساختار به این شکل است.

loop_label :] for identifier in range loop
           sequential statements
           [next  [label] [when condition];
           [exit  [label] [when condition];
end looploop_label ];
  • شناسه یا همان index که برای مشخص کردن تعداد دفعات تکرار حلقه می‌خواهیم از آن استفاده کنیم نیازی نیست از قبل به صورت جداگانه تعریف کرده باشیم، در داخل همین ساختار حلقه می‌توان آن را معرفی کرد. مقدار آن نیز تنها در داخل همین حلقه معتبر است و دسترسی به آن یا خواندن آن از خارج حلقه میسر نیست. بنابراین مقدار آن در حین اجرا و از خارج قابل تغییر نیست. برخلاف حلقه‌های while که شرط تکرار آنها وابسته به متغیر‌هایی بود که می‌توانستیم مقادیر آنها را توسط قسمت‌هایی دیگر خارج از ساختار حلقه دستکاری کنیم.
  • بازه‌ای که برای تکرار دفعات حلقه در نظر گرفته می‌شود باید حتما صحیح و قابل شمارش باشد و به یکی از دو فرم زیر اعلام شود.
  1. integer_expression to integer_expression (از یک مقدار صحیح تا یک مقدار صحیح (صعودی))
  2. integer_expression downto integer_expression ( از یک مقدار صحیح تا یک مقدار صحیح ( نزولی))

e. کلمه‌های کلیدی next و exit

همانطور که گفتیم، زمانی که به دستو next برسیم، اجرای فعلی حلقه در هر مرحله‌ای از آن که باشد متوقف شده و دور جدیدی از آن از سر گرفته می‌شود. سینتکس استفاده از این دستور به این شکل است.

next [label] [when  condition];

در عبارت فوق یک کلمه‌ی کلیدی دیگر هم وجود دارد و آن when است. استفاده از آن در دستور next اختیاری است و می‌توان آن را حذف کرد اما اگر وجود داشته باشد به این معناست که می‌خواهیم زمانی دستور next اجرا شود که شرط قید شده از نظر بولین true  شده باشد.

در مورد exit هم قبلا توضیح دادیم. کار آن این است که اجرای حلقه را کاملا متوقف کرده، از آن خارج شده و به دستورات پس از آن می‌پردازد. سینتکس استفاده از آن به صورت زیر است.

exit [label] [when  condition];

در اینجا نیز در مورد when همان توضیح قبلی برقرار است یعنی آنکه استفاده کردن یا نکردن از آن اختیاری است و در صورتی که بخواهیم دستور exit تحت شرایط خاصی اجرا شود از آن استفاده می‌کنیم.

باز هم تاکید می‌کنیم که تفاوت بین next و exit را همواره در نظر داشته باشید، exit اجرای حلقه‌ را کاملا قطع می‌کند و از آن خارج می‌شود، next فقط اجرای فعلی را متوقف کرده و دور بعدی آن را از سر می‌گیرد.

f. دستور wait

دستور wait اجرای یک فرآیند را تا زمان رخداد به خصوصی متوقف می‌کند. سینتکس استفاده از آن می‌تواند فرم‌های مختلفی داشته باشد.

wait until condition;
            wait for time expression;
            wait on signal;
            wait;

شرکت زایلینکس تنها فرم اول را در FPGAهای خود پیاده‌سازی کرده است.

wait until signal = value;
wait until signal’event and signal = value;
wait until not signal’stable and signal = value;

برای اینکه اجرای فرآیند ادامه یابد و جلو برود، شرطی که در مقابل wait until قرار داده می‌شود باید true شود. چند مثال را ببینیم.

wait until CLK=’1’;
wait until CLK=’0’;
wait until CLK’event and CLK=’1’;
wait until not CLK’stable and CLK=’1’;

مثلا در اولین مورد، تا زمانی که یک سطح مثبت از کلاک اتفاق نیفتد، پروسه متوقف خواهد ماند. برعکس در مثال دومی، این صبر کردن تا زمانی وجود دارد که یک سطح منفی از کلاک رخ بدهد. دو مورد آخر هم دقیقا همان شرط اولی را دارند. یعنی از نظر پیاده‌سازی سخت‌افزاری هر سه‌ مورد به یک شکل خواهند بود.

توجه داشته باشید که اگر در فرآیندی بخواهیم از دستور wait استفاده کنیم، آن فرآیند نمی‌تواند لیست حساسیت داشته باشد.

و اگر قرار باشد در فرآیندی از تعداد بیشتری دستور wait استفاده کنیم، ابزار سنتزی مانند Foundation Express با آنها به صورت ترتیبی برخورد خواهد کرد. یعنی نتیجه محاسبات در یک فیلپ فلاپ ذخیره می‌شوند.

g. کلمه کلیدی null

این دستور نشان‌دهنده‌ی این است که هیچ کاری انجام نشود. سینتکس استفاده از آن چنین است.

null;

اما ممکن است بپرسید کاربرد چنین دستوری چه می‌تواند باشد؟! مثلا ساختار case را که بالاتر توضیح دادیم در نظر بگیرید. از طرفی مجبوریم تمام حالت‌ها را تعیین وضعیت کنیم، از طرف دیگر ممکن است مداری داشته باشیم که برخی از حالت‌های آن واقعا برای ما مهم نیستند و نمی‌خواهیم در آنها کار به خصوصی انجام شود. در اینجا دستور null به معنای «هیچ کاری نکن» به ما کمک می‌کند. مثلا یک ساختار case داریم که سیگنال کنترلی آن از ۰ تا ۳۱ حالت مختلف می‌تواند داشته باشد. از طرفی مدار ما این است که در صورت ۳ یا ۱۵ بودن سیگنال کنترلی، سیگنال‌های A و B با هم XOR شوند. در این حالت می‌توانیم بگوییم در غیر این صورت (otherwise) دستور null است.

entity EX_WAIT is
   port ( CNTL: in integer range 0 to 31;
           A, B: in std_logic_vector(7 downto 0);
           Z: out std_logic_vector(7 downto 0) );
   end EX_WAIT;
architecture arch_wait of EX_WAIT is
begin
   P_WAIT: process (CNTL)
   begin
     Z <=A;
     case CNTL is
           when 3 | 15 =>
              Z <= A xor B;
        when others =>
              null;
     end case;
     end process P_WAIT;
end arch_wait;

h. مثالی از نمودار حالت Mealy machine

نموداری که در ادامه می‌بینید، نمودار حالت یک مدار تشخیص دنباله (sequence detector) است. دنباله‌ی مد نظر “X: “1011  است. ماشین به گونه‌ای طراحی شده است که تا زمانی که دنباله‌ی گفته شده را پیدا نکند، به جستجوی خود ادامه می‌دهد و هنگامی که پیدا کرد reset نمی‌شود. در طراحی mealy machine همانطور که در تصویر زیر می‌بینید، مقدار سیگنال خروجی نیز در هر بار تغییر ورودی، نشان داده می‌شود.

آموزش VHDL Primer
یک مدار sequence detector که با ساختار mealy machine طراحی شده است.

در ادامه کد VHDL آن را نیز می‌بینیم.

library ieee;
use ieee.std_logic_1164.all;
entity myvhdl is
    port (CLK, RST, X: in STD_LOGIC;
             Z: out STD_LOGIC);
end;
architecture myvhdl_arch of myvhdl is
-- SYMBOLIC ENCODED state machine: Sreg0
type Sreg0_type is (S1, S2, S3, S4);
signal Sreg0: Sreg0_type;
begin
--concurrent signal assignments
Sreg0_machine: process (CLK)
begin
if CLK'event and CLK = '1' then
    if RST='1' then
        Sreg0 <= S1;
    else
    case Sreg0 is
        when S1 =>
            if X='0' then
                Sreg0 <= S1;
            elsif X='1' then
                Sreg0 <= S2;
            end if;
        when S2 =>
            if X='1' then
                Sreg0 <= S2;
            elsif X='0' then
                Sreg0 <= S3;
            end if;
        when S3 =>
            if X='1' then
                Sreg0 <= S4;
            elsif X='0' then
                Sreg0 <= S1;
            end if;
        when S4 =>
            if X='0' then
                Sreg0 <= S3;
            elsif X='1' then
                Sreg0 <= S2;
            end if;
        when others =>
            null;
    end case;
    end if;
end if;
end process;
-- signal assignment statements for combinatorial outputs
Z_assignment:
Z <= '0' when (Sreg0 = S1 and X='0') else
        '0' when (Sreg0 = S1 and X='1') else
        '0' when (Sreg0 = S2 and X='1') else
        '0' when (Sreg0 = S2 and X='0') else
        '0' when (Sreg0 = S3 and X='1') else
        '0' when (Sreg0 = S3 and X='0') else
        '0' when (Sreg0 = S4 and X='0') else
        '1' when (Sreg0 = S4 and X='1') else
        '1';
end myvhdl_arch;

9. مدل‌سازی با روش جریان داده – عبارت‌های همزمان

در بخش قبل گفتیم که مدل‌سازی رفتاری را می‌توان با استفاده از عبارت‌های ترتیبی و در قالب ساختار توصیف فرآیند‌ها و یا با استفاده از عبارت‌های همزمان انجام داد. روش اول یعنی استفاده از ساختار توصیف فرآیند و عبارت‌های ترتیبی را نیز در همان بخش توضیح دادیم و گفتیم که برای طراحی مدارهای پیچیده‌ی دیجیتال، روش بسیار کارآمدی محسوب می‌شود. حالا در این بخش می‌خواهیم ببینیم با استفاده از عبارت‌های همزمان چگونه می‌توان مدارها را مدل‌سازی کرد. به این روش مدل‌سازی براساس جریان داده یا Dataflow Modeling گفته می‌شود و به طور خلاصه شامل توصیف توابع یک مدار و نیز مسیر جریان یافتن داده در آن است. باید دقت داشت که آن را با روش مدل‌سازی ساختاری که در آن المان‌های تشکیل دهنده‌ی یک مدار و شیوه‌ی اتصالات موجود بین آنها را توصیف می‌کردیم، اشتباه نشود.

در روش همزمان، تخصیص مقدار به سیگنال‌ها وابسته به رویدادهای مدار است و به محض رخداد یک رویداد جدید، سیگنال‌های متناظر همزمان مقداردهی می‌شوند. در ادامه‌ی این بخش تعدادی ساختار همزمان را برای استفاده در روش مدل‌سازی براساس جریان داده معرفی می‌کنیم.

Simple Concurrent signal assignments .a

در بخش‌های قبلی مثال‌های متعددی از این نوع تخصیص مقدار به سیگنال‌ها را دیده‌ایم. در این بخش مرور و مقایسه‌ای بر انواع مختلف این روش‌ها را با هم خواهیم داشت.

یک مثال ساده از این روش را در زیر می‌توانیم ببینیم.

Sum <= (A xor B) xor Cin;
Carry <= (A and B);
Z <= (not X) or after 2 ns;

حتما خودتان هم می‌توانید حدس بزنید که سینتکس استفاده از آن به این صورت است.

Target_signal <= expression;

واضح است که منظور از تخصیص انتقال مقدار عبارت expressin به target signal است. به محض اینکه روی یکی از سیگنال‌های مدار رخدادی روی بدهد، عبارت expression ارزیابی می‌شود تا مشخص شود که آیا تحت تاثیر آن رویداد مقدار آن تغییری داشته است یا خیر. در صورت مثبت بودن جواب، مقدار جدید به سیگنال هدف منتقل می‌شود و در صورتی که مقدار عبارت از آن رویداد متاثر نشده باشد، مقدار سیگنال هدف همانی که بود خواهد ماند. نکته‌ی مهمی که وجود دارد این است که تایپ سیگنال هدف باید حتما با تایپ مقدار محاسبه شده در عبارت یکسان باشد تا بتوان مقدار را به آن تخصیص داد.

یک مثال خب دیگر، مدار جمع‌کننده‌ی ۴ بیتی است. دقت کنید که پکیج IEEE.std_logic_unsigned را به این دلیل اضافه کرده‌ایم که بتوانیم از عملگر «+» استفاده کنیم.

library ieee;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
entity ADD4 is
    port (
        A: in STD_LOGIC_VECTOR (3 downto 0);
        B: in STD_LOGIC_VECTOR (3 downto 0);
        CIN: in STD_LOGIC;
        SUM: out STD_LOGIC_VECTOR (3 downto 0);
        COUT: out STD_LOGIC
    );
end ADD4;
architecture ADD4_concurnt of ADD4 is
-- define internal SUM signal including the carry
signal SUMINT: STD_LOGIC_VECTOR(4 downto 0);
begin
-- <<enter your statements here>>
  SUMINT <=  ('0' & A) + ('0' & B) + ("0000" & CIN);
  COUT <= SUMINT(4);
  SUM <= SUMINT(3 downto 0);
end ADD4_concurnt;

Conditional Signal assignments .b

مرور سینتکس این روش که به  شکل زیر است احتمالا به اندازه‌ی کافی برای فهمیدن آن واضح است.

Target_signal <= expression when Boolean_condition else
 expression when Boolean_condition else
     :
expression;

خب، حتما خودتان حالا بهتر از ما می‌توانید آن را توضیح دهید. پس با هم مرور می‌کنیم.

در صورتی که شرط موجود در اولین خط به صورت منطقی true باشد، مقدار عبارت متناظر با آن محاسبه شده و به سیگنال هدف منتقل می‌شود. در غیر این صورت (true نبودن شرط اول) شرط عبارت خط دوم بررسی می‌شود، در صورت true بودن مقدار عبارت آن به سیگنال منتقل شده و درغیر این صورت شرط عبارت بعدی بررسی می‌شود. این روال همینطور ادامه می‌یابد تا زمانی که به اولین شرط true برسیم. اگر تا انتها رفتیم و هیچ کدام از شرط‌ها true نبود، آخرین عبارت محاسبه شده و مقدار آن به سیگنال منتقل می‌شود. اگر هم بیش از یکی از شرط‌ها true باشند، آن که بالاتر قرار دارد اجرا خواهد شد.

یک مثال خوب برای این روش، مالتی‌پلکسر ۴ به ۱ است.

entity MUX_4_1_Conc is
   port (S1, S0, A, B, C, D: in std_logic;
           Z: out std_logic);
   end MUX_4_1_Conc;
architecture concurr_MUX41 of MUX_4_1_Conc is
begin
       Z <= A when S1=’0’ and S0=’0’ else
          B when S1=’0’ and S0=’1’ else
          C when S1=’1’ and S0=’0’ else
          D;
end concurr_MUX41;

به محض تغییر هر کدام از سیگنال‌ها در شرط‌ها یا عبارات، همه‌ چیز دوباره ارزیابی شده و تخصیص‌های لازم اتفاق می‌افتند.

ضمنا همانطور که احتمالا از قبل می‌دانید، ساختار when-else برای پیاده‌سازی مدارهای دیجیتالی که دارای truth table هستند بسیار مناسب است. همان مالتی‌پلسکر بالا را این بار با یک کد کوتاه‌تر ببینید.

entity MUX_4_1_funcTab is
   port (A, B, C, D: in std_logic;
         SEL: in std_logic_vector (1 downto  0);
           Z: out std_logic);
   end MUX_4_1_ funcTab;
architecture concurr_MUX41 of MUX_4_1_ funcTab is
begin
        Z <= A when SEL = ”00” else
            B when SEL = ”01” else
            C when SEL = “10” else
            D;
end concurr_MUX41;

می‌بینید که این ساختار از ساختار If-then-else در فرآیند‌ها یا caseها بسیار خلاصه‌تر است. (روش دیگر توصیف مالتی‌پلکسر با استفاده از ساختار case و در قالب یک فرآیند بود که در بخش‌های قبلی گفته شد)

Selected Signal assignments .c

این روش تا حدودی شبیه همین روش conditional است که در قسمت b توضیح دادیم. ابتدا سینتکس آن را ببینیم.

with choice_expression select
target_name <= expression when choices,
target_name <= expression when choices,
                 :
target_name <= expression when choices;

در قست choice_expression سیگنال کنترل کننده قرار می‌گیرد و تمام حالت‌های مختلف آن که قرار است روی هر کدام از آنها یک دستورالعمل متفاوت اجرا شود، در قسمت‌های choices موجود در خطوط لیست می‌شوند. دستورالعمل مربوط به هر حالت هم در قسمت expression مربوط به آن خط. Target signal هم که مانند قبل همان سیگنال هدفی است که قرار است مقداردهی شود. حالت‌هایی که در قسمت choiceها قرار می‌دهیم، می‌توانند هم یک مقدار ثابت باشند، مثلا اینکه اگر سیگنال کنترلی برابر ۵ باشد، یا اینکه به صورت بازه‌ای تعریف شوند، مثلا اینکه سیگنال کنترلی متعلق به بازه‌ی ۴ تا ۹ باشد. در اینجا هم دو قانون وجود دارد که در انتخاب حالت‌ها باید به آنها پایبند باشیم.

  • هیچ دو بازه‌ای نباید هم‌پوشانی داشته باشند.
  • یا باید تمام حالت‌هایی که ممکن است برای آن سیگنال کنترلی وجود داشته ‌باشند را لیست کنیم و دستورالعمل مربوط به هر حالت را بنویسیم، و یا اینکه اگر بخواهیم تعدادی از آنها را در نظر نگیریم، یک حالت را به others و دستورالعمل مخصوص آن اختصاص دهیم.

مالتی‌پلکسر ۴ به ۱ را این بار با این ساختار ببینیم.

entity MUX_4_1_Conc2 is
   port (A, B, C, D: in std_logic;
           SEL: in std_logic_vector(1 downto 0);
           Z: out std_logic);
   end MUX_4_1_Conc2;
architecture concurr_MUX41b of MUX_4_1_Conc2 is
begin
     with SEL select
        Z <= A when “00”,
              B when “01”,
              C when “10”,
              D when “11”;
 end concurr_MUX41b;

اگر می‌خواستیم به صورت یک فرآیند این ساختار را توصیف کنیم، احتمالا باید ازcase استفاده می‌کردیم. این ساختار نیز مشابه ساختار when-else برای پیاده کردن truth tableها مناسب است.

مانند مثال‌ زیر، choiceها می‌توانند دارای یک تک‌ مقدار باشند، یا به صورت بازه‌ای یا چند مقداری.

target <= value1 when “000”,
        value2 when “001” | “011”  | “101” ,
        value3 when others;

در مثال فوق اولا تمام حالت‌های ممکن برای سیگنال کنترلی لحاظ شده‌اند و ثانیا هیچ حالتی دو بار ذکر نشده است. پس شروط لازم برآورده شده‌اند. یادآوری این نکته نیز خالی از لطف نیست که همیشه حالت others را باید در انتهای لیست قید کنیم.

نکته‌ی دیگری که بد نیست بدانید این است که Xilinx Foundation Express به شما اجازه نمی‌دهد که در این ساختار از سیگنال کنترلی‌ای استفاده کنید که برداری باشد. مثلا مجاز نیستید که چنین کنترلی داشته باشید، std_logic_vector’(A,B,C).

برای مثال، یک full_adder را بررسی کنیم با ورودی‌های A ،B و C و خروجی‌های sum و cout.

entity FullAdd_Conc is
   port (A, B, C: in std_logic;
           sum, cout: out std_logic);
   end FullAdd_Conc;
architecture FullAdd_Conc of FullAdd_Conc is
     --define internal signal: vector INS of the input signals
     signal INS: std_logic_vector (2 downto 0);
begin
        --define the components of vector INS of the input signals
        INS(2) <= A;
        INS(1) <= B;
        INS(0) <= C;
        with INS select
               (sum, cout) <=  std_logic_vector’(“00”) when “000”,
                               std_logic_vector’(“10”) when “001”, 
                               std_logic_vector’(“10”) when “010”, 
                               std_logic_vector’(“01”) when “011”, 
                               std_logic_vector’(“10”) when “100”, 
                               std_logic_vector’(“01”) when “101”, 
                               std_logic_vector’(“01”) when “110”, 
                               std_logic_vector’(“11”) when “111”,
                               std_logic_vector’(“11”) when others;
end FullAdd_Conc; ]

نکته: به دلیل همان محدودیتی که زایلینکس در عدم پذیرش سیگنال کنترلی به صورت بردار دارد، در کد فوق ما یک بردار داخلی تعریف کرده‌ایم به نام INS(A,B,C) و از آن برای استفاده به عنوان بخشی از ساختار with-select-when statement کمک گرفته‌ایم.

10. مدل‌سازی ساختاری

در بخش سوم، روش مدل‌سازی ساختاری را به طور خیلی خلاصه توضیح دادیم. مدل‌سازی ساختاری مدار را در قالب اجزا و واحد‌های تشکیل دهنده‌ی آن و اتصالات بین این واحد‌ها توصیف می‌کند. مسلما پیش فرض این است که هر کدام از این واحد‌ها قبلا به صورت مجزا تعریف و توصیف شده باشند (با هر روشی ساختاری، رفتاری یا جریان داده) و حالا در قالب یک پکیج در دسترس و قابل ارجاع باشند. اگر بخواهیم به صورت سلسله مراتبی در نظر بگیریم، در پایین‌ترین سطح، هر کدام از این واحدها باید با توصیف رفتاری و با استفاده از عملگرهای منطقی موجود در VHDL توصیف شوند. پس از آن هر چه به سطوح بالاتر بیاییم ممکن است از مدل‌های دیگر هم برای توصیف استفاده کنیم. بنابراین می‌توان اینطور گفت که به طور کلی روش مدل‌سازی ساختاری به عنوان یکی از روش‌ها برای توصیف مدارات پیچیده، در سطوح بالا بسیار کارآمد است.

یکی از بهترین روش‌های شناخت مدل‌سازی ساختاری، مقایسه‌ی آن با شماتیک‌های بلوک دیاگرامی است که در آنجا نیز شمای کلی اجزاء یک مدار و اتصالات بین آنها را داریم. بنابراین VHDL برای استفاده از مدل‌سازی ساختاری نیز الگوریتم تقریبا مشابهی تعریف کرده است.

  • ابتدا لیستی از واحد‌ها و اجزائی که وجود دارند را تهیه کنید.
  • سیگنال‌هایی که قرار است به عنوان اتصالات بین واحد‌ها باشند را مشخص کنید.
  • اگر قرار است از یک واحد چندین و چند بار در قسمت‌های مختلف مدار استفاده کنید، بهتر است برای هرکدام از نمونه‌های آن که استفاده می‌کنید یک label بگذارید تا با هم اشتباه نشوند.

تعریف سیگنال‌ها و اجزاء در بدنه‌ی معماری کد:

architecture architecture_name of NAME_OF_ENTITY is
     -- Declarations
           component declarations
           signal declarations
     begin
     -- Statements
           component instantiation and connections   
:
     end architecture_name;

a. تعریف واحد‌ها

گفتیم که بلوک‌هایی که در توصیف ساختاری یک مدار استفاده می‌شوند، باید از قبل تعریف شده باشند. این تعریف یا در همان کد و در بخش architecture declaration انجام می‌شود و یا در یک فایل مجزا که در اینجا به صورت یک پکیج به کد اضافه می‌شود. در تعریف هر کدام از واحد‌ها باید نام آن واحد و اینترفیس‌های (پورت‌) آن را بگوییم. سینتکس آن چیزی شبیه فرم زیر است.

component component_name [is]
     [port (port_signal_names: mode type;
     port_signal_names: mode type;
                :
      port_signal_names: mode type);]
end component [component_name];

نام هر واحد یا می‌تواند نام یک entity باشد که قبلا در کتابخانه تعریف شده است و یا یک entity که به صورت خارجی در فایل VHDL فعلی تعریف می‌شود. (مثال جمع‌کننده‌ی ۴ بیتی را ببینید)

لیست اینترفیس‌های هر واحد هم شامل نام هر پورت، مود و تایپ آنهاست. دقیقا مشابه تعریف پورت‌ها در entity.

چند مثال از تعریف واحد‌ها را ببینیم.

component OR2
           port (in1, in2: in std_logic;
                 out1: out std_logic);
end component;
component PROC
           port (CLK, RST, RW, STP: in std_logic;
                 ADDRBUS: out std_logic_vector (31 downto 0);
                 DATA: inout integer range 0 to 1024);
component FULLADDER
           port(a, b, c: in std_logic;
                sum, carry: out std_logic);
end component;

گفتیم که تعریف واحد‌ها یا می‌تواند به صورت یک فایل مجزا صورت گیرد و بعدا به کد اصلی مانند یک پکیج اضافه شود و یا اینکه در خود آن کد تمام تعریف‌ها انجام شوند. اگر حالت اول را انتخاب کنیم، دیگری نیازی نیست که مجددا آنها را در کد اصلی نیز معرفی کنیم. بلکه کافیست آن پکیج و کتابخانه‌ی مربوطه را اضافه کنیم.

b. نمونه گرفتن از واحدها و توصیف اتصالات میان آنها

زمانی که از یک جزء مدار نمونه گیری می‌کنیم، در حقیقت به جزئی ارجاع می‌دهیم که:

  • یا از قبل در همین کد آن را معرفی نموده‌ایم.
  • یا آن را در کتابخانه و پکیجی که به کد اضافه کرده‌ایم، معرفی کرده‌ایم.

سینتکس چنین کار به این صورت است.

instance_name : component name 
port map (port1=>signal1, port2=> signal2,… port3=>signaln);

label یا برچسب یا همان نامی که برای هر نمونه انتخاب می‌کنیم می‌تواند هر نامی که در محدوده‌ی قوانین VHDL قرار دارد باشد و از این به بعد نام اختصاصی آن بخش از مدار می‌شود. نامی که برای هر واحد وجود دارد هم نامی است که از قبل در همان جایی که آن بلوک را به عنوان مرجع معرفی کرده‌ایم، بر روی آن گذاشته‌ایم. اینکه هر پورت این بلوک در این قسمت از مدار و این نمونه‌ی خاص قرار است به چه سیگنالی متصل شود را هم در داخل پرانتز ذکر می‌کنیم. این روش یعنی port map یکی از روش‌های تعیین اتصالات بلوک‌هاست که اگر نقشه اتصلات تمام بلوک‌ها را به همین روش بنویسیم و در کنار هم به آ‌ن‌ها نگاه کنیم، خواهیم دید که کل اتصالات میان قسمت‌های مختلف مدار پوشش داده می‌شود. اما روش دیگری نیز برای تعیین اتصالات وجود دارد که به این صورت است.

port map (signal1,  signal2,…signaln);

در این حالت، اولین پورتی که در تعریف آن جزء مدار ذکر شده باشد، به اولین سیگنال، دومین پورت به دومین سیگنال و … متصل می‌شوند. بنابراین در این روش ترتیب تعریف پورت‌ها و توجه به این ترتیب حائز اهمیت خواهد بود.

البته می‌توان در یک مدار هر دوی این روش‌ها را برای تعیین اتصالات به صورت همزمان به کار گرفت. به عنوان مثال کد زیر را در نظر بگیرید.

component NAND2
                port (in1, in2: in std_logic;
                      out1: out std_logic);
end component;
signal int1, int2, int3: std_logic;
architecture struct of EXAMPLE is
                U1: NAND2 port map (A,B,int1);
                U2: NAND2 port map (in2=>C, in2=>D, out1=>int2);
                U3: NAND3 port map (in1=>int1, int2, Z);

مثال دیگر می‌تواند مدار هشداردهنده‌ی خودرو باشد که در قسمت‌ سوم در مورد آن صحبت کردیم.

11. منابع

آموزش VHDL Primer

منبع: ترجمه از سایت seas.upenn.edu

دوستان عزیز برای ترجمه، ویرایش و بارگذاری این نوشته‌ کلی نفر ساعت زحمت کشیدیم امیدواریم برای شما مفید واقع شده باشد. دیگر آموزش‌های FPGA را هم مطالعه کنید.

اگر این نوشته‌ برایتان مفید بود لطفا کامنت بنویسید و حمایت مالی کنید برای تولید محتوی‌ بیشتر. همچنین دوست داشتین اپلیکیشن اندویدی ما را هم نصب کنید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد.