زمن البرمجيات مقابل زمن العتاد
في برنامج، يتقدم الزمن تعليمة واحدة في كل مرة. تُنهي CPU السطر 1 ثم تشغّل السطر 2. إذا أردت أن يحدث شيئان في وقت واحد، فأنت تلجأ إلى threads أو async أو جهاز ثانٍ.
في العتاد، كل شيء يحدث دفعة واحدة. الدارة لا تتناوب. الجامع يجمع دائمًا، وmultiplexer يختار دائمًا، وflip-flop يراقب الساعة دائمًا. لا يوجد عدّاد برنامج. لا يوجد "سطر حالي".
على Verilog أن يصف هذا العالم نصيًا. والطريقة التي يفعل بها ذلك هي مصدر كل التشويش في هذا الفصل.
طبقتان من التزامن
عندما تقرأ module من Verilog، فأنت تنظر إلى أمرين مختلفين في نفس الوقت:
- الوصف الساكن للدارة. أسلاك، registers، نسخ من البوابات (gate instances)، نسخ من sub-modules، continuous assignments. كلها موجودة في وقت واحد. ترتيبها في الملف لا يهم.
- الكتل الإجرائية -
initialوalways- التي تبدو كبرامج صغيرة يخطو المحاكي خلالها. داخل إحدى هذه الكتل، الجمل تُنفَّذ بنوع من الترتيب. لكن عدة كتلalwaysيمكن أن تكون نشطة في وقت واحد، كل منها في خيطها الصغير من الزمن المُحاكَى.
module example(input wire a, input wire b, output wire y, output wire z);
assign y = a & b; // موجودة في كل الأوقات
assign z = a | b; // موجودة أيضًا في كل الأوقات، بالتوازي
always @(posedge a) begin
// مفاعل "دائم التشغيل" منفصل يستيقظ عند كل
// حافة صاعدة لـ `a`
end
endmodule
سطرا assign ليسا تسلسلًا. إنهما يصفان قطعتين من المنطق التوافقي تستطيع أداة التركيب وضعهما جنبًا إلى جنب. وكتلة always شيء ثالث يحدث بالتوازي.
ماذا تعني "إشارة" (Signal)
في البرمجيات، يحمل المتغير قيمة إلى أن تُغيّرها. في Verilog، الإشارة تحمل قيمة باستمرار - وفي كل لحظة تعتمد على ما يقودها.
wire يُقاد من الخارج (عبر assign، أو خرج sub-module، أو وصلة inout). reg يُقاد من الداخل من كتلة إجرائية. كلاهما له قيمة طوال الوقت. لا يوجد شيء اسمه "غير مُهيَّأ" بالمعنى الذي تقصده C - الإشارات إما مَقُودة إلى قيمة مُعرَّفة، أو إلى المجهول الخاص x، أو إلى المعاوقة العالية z. سنغطّي الأخيرين في قيم X وZ.
الساعات تُغيّر كل شيء
بمجرد أن تُدخِل ساعة، يبدأ الزمن في الأهمية. flip-flop هو قطعة صغيرة من العتاد تلتقط قيمة دخلها عند الحافة الصاعدة (أو الهابطة) للساعة وتحتفظ بها حتى الحافة التالية. الشيء الذي يتيح لك بناء عدّادات وآلات حالة وpipelines - أي شيء له ذاكرة - هو الساعة.
لاحظ q <= d بدلًا من q = d. هذه non-blocking assignment - عماد المنطق المتزامن مع clock. تقول: "عند حافة الساعة التالية، جدوِل تحديث q ليصبح ما يساوي d الآن". سنتعمق في القواعد في Blocking مقابل Non-blocking؛ في الوقت الحالي، يكفي أن تدرك أن الجملة ليست تتظاهر بأنها جملة برمجية.
RTL: النموذج الذهني لنقل السجلات
معظم Verilog القابل للتركيب يُكتب بأسلوب يُسمى Register Transfer Level، أو RTL. الفكرة بسيطة:
- حدّد ما تحتاجه دارتك من حالة (الـ registers).
- لكل register، صِف شيئين: ما الذي يُعيد ضبطه (reset)، وما المنطق التوافقي الذي يحسب قيمته التالية.
- وصّل خرج المنطق التوافقي بدخل register فتحصل على دارة عاملة.
always @(posedge clk) begin
if (reset) state <= IDLE;
else state <= next_state;
end
always @(*) begin
case (state)
IDLE: next_state = start ? RUNNING : IDLE;
RUNNING: next_state = done ? IDLE : RUNNING;
default: next_state = IDLE;
endcase
end
هذه آلة حالة بحالتين. كتلة always الأولى متزامنة مع الساعة - فهي flip-flop. والثانية توافقية بحتة - مجرد معادلة. كل آلة حالة وعدّاد وpipeline ستكتبه تقريبًا يتبع هذا الشكل.
العادات التي توقعك
إن كنت قادمًا من البرمجيات، فهذه قائمة قصيرة بالعادات التي عليك التخلي عنها:
- "المتغيرات تتحدث عندما أُسنِد إليها". ليس عند حافة الساعة - non-blocking assignment تُجدول التحديث ليُنفَّذ في نهاية الخطوة الزمنية.
- "الجمل تُنفَّذ من أعلى لأسفل". خارج الكتل الإجرائية، لا. داخل كتلة متزامنة مع الساعة، نوعًا ما - لكن blocking مقابل non-blocking يُغيّر معنى "بالترتيب".
- "سأخصص ذلك عند الحاجة". العتاد لا يُخصص شيئًا. كل register وكل بوابة يجب أن توجد في وقت التركيب. حجم كل vector ثابت.
- "هذه الحلقة سريعة - مجرد عملية واحدة". حلقة
forفي Verilog القابل للتركيب يتم فردها (unrolled) إلى عتاد متوازٍ. حلقة من 64 تكرارًا تصبح 64 نسخة من الجسم، وليست تعليمة CPU تُنفَّذ 64 مرة.
أنت لا تتعلم كتابة برنامج. أنت تتعلم وصف دارة. الغريزة بقراءة الشيفرة من أعلى لأسفل هي بالضبط الغريزة الخاطئة، وإعادة التدريب تستغرق وقتًا.
ماذا بعد
تمشي المستندات التالية عبر الحصول على toolchain محلي (اختياري - محرر المتصفح كافٍ)، ثم كتابة أول module من الصفر. سنعود إلى تباين العتاد مقابل البرمجيات في كل مرة يبدو فيها شيء غريبًا، لأن معظم الأجزاء "الغريبة" لها السبب الجذري نفسه: هذا ليس برمجيات.
الأسئلة الشائعة
ما الفرق بين العتاد والبرمجيات بمصطلحات Verilog؟
البرمجيات هي تسلسل من التعليمات تنفّذها CPU، واحدة تلو الأخرى. أما العتاد - وهو ما يصفه Verilog - فهو شبكة من البوابات والأسلاك تحمل إشارات في الوقت نفسه. ملف Verilog يصف تلك الشبكة. المحاكي يحاكي السلوك المتوازي؛ وأداة التركيب تحوّله إلى سيليكون فعلي.
هل تعمل شيفرة Verilog من أعلى لأسفل كما في C؟
لا - والتعامل معها بهذا الشكل هو الخطأ الأكثر شيوعًا بين المبتدئين. الجمل على المستوى العلوي (assigns، module instances، always blocks) كلها 'موجودة' في الوقت ذاته. فقط داخل الكتل الإجرائية - initial وalways - يحدث شيء أشبه بالتنفيذ التتابعي، وحتى هناك، تكسر assignments من نوع non-blocking هذا الوهم.
ماذا يعني 'concurrent' في Verilog؟
يعني أن جملًا متعددة تصف أجزاء من دارة تعمل كلها في الوقت نفسه. جملتا assign في نفس الـ module ليستا 'السطر 1 ثم السطر 2' - بل قطعتان من العتاد تعملان بالتوازي، وكلتاهما تستجيب لمدخلاتها باستمرار.
ما هو تصميم RTL؟
RTL اختصار لـ Register Transfer Level. وهو أسلوب لكتابة Verilog تصف فيه الدارة بوصفها registers (flip-flops) والمنطق التوافقي الذي يحسب قيمها التالية. معظم Verilog القابل للتركيب هو RTL. المستوى الأعلى منه هو behavioral؛ والمستوى الأدنى هو gate-level.