Modules داخل Modules
تصميم Verilog هو شجرة من modules. الـ module على المستوى الأعلى (إما testbench لديك أو غلاف الشريحة العلوي) يأخذ نسخًا من modules أدنى، التي تأخذ بدورها نسخًا من modules أدنى، حتى تصل إلى عناصر بوابات يقدمها المصنّع. Instantiation هي صياغة هذا التداخل.
شاهدت الشكل بالفعل - استخدمناه في أول module:
and_gate dut(.a(a), .b(b), .y(y));
هذا السطر يُنشئ نسخة واحدة من and_gate، يسميها dut، ويربط منافذها بإشارات محلية. لنفكك كل قطعة.
شكل الـ Instantiation
module_name instance_name (port_connections);
module_nameيجب أن يطابق اسمًا من إعلانmoduleفي مكان ما في مشروعك. Verilog حسّاس لحالة الأحرف.instance_nameهي لافتة تختارها - عادةً وصفية للدور الذي تؤديه هذه النسخة. ستستخدمها في المسارات الهرمية وعروض الموجات.port_connectionsتصل منافذ النسخة بالإشارات المحلية. هناك طريقتان لكتابتها.
وصلات المنافذ المُسماة (استخدم هذه)
يبدو الأسلوب المُسمى كالتالي:
my_module instance_name(
.clk (clk),
.reset (reset_n),
.data_in(in_bus),
.data_out(out_bus),
.valid (out_valid)
);
كل زوج .port(signal) يقول "صِل منفذ هذه النسخة المسمى port بالإشارة المحلية المسماة signal". الترتيب لا يهم. إن أضفت منفذًا جديدًا إلى إعلان module، فإن الـ instantiations القائمة لا تنكسر طالما أعطيت المنفذ الجديد قيمة افتراضية أو حدّثت كل موقع.
ملاحظتان عمليتان:
- اسم المنفذ (يسار القوسين) يجب أن يطابق إعلان module بالضبط.
- اسم الإشارة (داخل القوسين) محلي لحيث تعيش الـ instantiation - عادةً module الأب.
إن كان المنفذ غير موصول، اترك الداخل فارغًا: .optional_port(). تطفو الإشارة (z) داخل النسخة. بعض أدوات التركيب تحذر؛ معظمها يقبل.
وصلات المنافذ الموضعية (تجنّبها)
الأسلوب الأقصر يُدرج الإشارات بترتيب قائمة المنافذ:
my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);
أقصر لكنه هش. أعد ترتيب قائمة منافذ module (إعادة هيكلة حقيقية تحدث) وكل instantiation موضعي يُعاد توصيله بهدوء بشكل خاطئ. لا تلجأ إلى الموضعي إلا إذا كانت قائمة المنافذ تحوي عضوًا أو عضوين فقط ومن غير المرجّح أن تتغيّر يومًا.
حيث يبقى مقبولًا: modules أداة صغيرة يكون فيها ترتيب المنافذ جزءًا من API. بوابة بدخلين لا بأس بها موضعيًا. متحكم ذاكرة بـ 30 منفذًا يطلب المتاعب.
مثال هرمي كامل
هذه هرمية ثلاثية المستويات فعلًا: test → full_adder → نسختان من half_adder. كل نسخة لها نسختها الخاصة من البوابات داخل half_adder؛ ستُصدر أداة التركيب دارة واحدة لكل instantiation.
نسخ متعددة من نفس module
عندما تأخذ نسخًا متعددة من نفس module، فإن كل نسخة عتاد مستقل. لا تتشارك الحالة. لا تتشارك البوابات. تخيّل كل نسخة كنسخة جديدة مطبوعة من التصميم.
adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));
هذه أربعة جامعات منفصلة تعمل بالتوازي. لو كان adder يحوي register، لكان لكل نسخة نسختها الخاصة من ذلك register بحالتها الخاصة.
حلقات generate: طباعة عتاد متكرر
كتابة أربع نسخ يدويًا لا بأس بها. كتابة 64 مُملّة. كتلة generate تدع المُعالج يقوم بالكتابة عنك:
ثلاث قطع جديدة من الصياغة:
genvar iيُعلن عن متغير حلقة قابل للاستخدام فيgenerate. ليس إشارة وقت تشغيل - يوجد فقط في وقت elaboration.generate ... endgenerateيلفّ الحلقة. بعض الأدوات تقبل حلقات generate دون الكلمةgenerateصراحةً، لكن كتابتها تُوضّح القصد.begin : invert_loopيُسمّي نطاق generate. يصبح الاسم جزءًا من الاسم الهرمي لكل نسخة مُولَّدة (dut.invert_loop[0].u_inv،dut.invert_loop[1].u_inv، إلخ).
تنشر أداة التركيب الحلقة وتُنتج WIDTH نسخة من bit_inverter. كل نسخة عتاد مستقل.
تجاوز Parameters عند الـ Instantiation
إن كان لـ module parameters، يمكنك تجاوزها بـ #(.PARAM(value)) بين اسم module واسم النسخة:
counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));
كلا النسختين تستخدم مصدر counter نفسه لكن بعرضين مختلفين. غطّينا الصياغة في Parameters؛ تنسلّ بنظافة في الـ instantiation.
الأسماء الهرمية
بمجرد وجود هرمية، كل إشارة لها مسار هرمي:
test.dut.ha0.sum
يُقرأ ذلك: في module test، داخل النسخة dut، داخل النسخة ha0، الإشارة المسماة sum. سترى هذه المسارات في عارضات الموجات ورسائل الأخطاء واستدعاء $display العرضي الذي يدخل عميقًا إلى sub-module من testbench:
$display("internal carry1 = %b", dut.carry1);
الإشارات الهرمية كهذه هي فقط لـ testbenches والتنقيح - RTL القابل للتركيب لا يصل إلى داخل modules أخرى.
أخطاء شائعة
عدم تطابق اسم المنفذ. .clk_in(clk) يصل clk المحلي بمنفذ اسمه clk_in. إن كان منفذ module في الحقيقة clk، فسيخبرك المُحلّل (بعض الأدوات أوضح من غيرها).
عدم تطابق العرض على منفذ. وصل إشارة 4 بت بمنفذ 8 بت يُمدّد بالأصفار بصمت؛ والعكس يُقصّر بصمت. معظم الأدوات تحذر؛ إن لم ترَ تحذيرات، فابحث بجدية أكثر.
نسيان # للـ parameter. counter (.WIDTH(8)) c(.clk(clk)) يبدو كتجاوز لكنه ليس كذلك - يحاول المُحلّل التعامل مع (.WIDTH(8)) كوصلة منفذ ويفشل. الصحيح: counter #(.WIDTH(8)) c(.clk(clk)).
إعادة استخدام اسم نسخة. لا يمكن لنسختين أن تحملا نفس الاسم في نفس النطاق. رسالة الخطأ عادةً واضحة؛ غواية النسخ واللصق هي ما يوقعك.
ماذا بعد
تستطيع الآن وصل modules ببعضها في هرمية حقيقية. المستند التالي يُكمل الجانب الهيكلي من Verilog - Continuous Assignment - ويتعمق في جملة assign التي استخدمناها بحرية منذ الفصل الأول.
الأسئلة الشائعة
كيف تأخذ نسخة من module في Verilog؟
اكتب اسم module ثم اسم النسخة ثم قائمة وصلات المنافذ بين قوسين: my_module instance_name(.port(signal), ...);. الأسلوب الأكثر شيوعًا يستخدم الوصلات المُسماة (.port(signal))، التي تطابق بالاسم بغض النظر عن الترتيب. الأسلوب الموضعي الأقصر (my_module instance(signal1, signal2)) يعتمد على ترتيب قائمة المنافذ ومن الخطر صيانته.
ما الفرق بين وصلات المنافذ المسماة والموضعية؟
الوصلات الموضعية تُدرج الإشارات بنفس ترتيب قائمة منافذ module - الإشارة الأولى تتصل بالمنفذ الأول، والثانية بالثاني، وهكذا. الوصلات المُسماة تستخدم .port_name(signal_name)، فتطابق بالاسم. المُسماة مُسهبة لكنها محصّنة ضد إعادة ترتيب المنافذ وتوثّق نفسها عند موقع الاستدعاء. استخدم المُسماة لأي شيء يتجاوز منفذين أو ثلاثة.
هل يمكن أخذ عدة نسخ من نفس Verilog module؟
نعم - هذا هو الغرض كله. كل نسخة هي عتاد مستقل بحالته الخاصة. إن كان لديك module adder، فيمكنك أخذ 64 نسخة منه في وحدة SIMD، كل واحدة بمدخلات مختلفة. حلقة generate هي الصياغة المعتمدة عندما تكون النسخ متشابهة ومُفهرسة.
ما هي كتلة generate في Verilog؟
generate ... endgenerate هو بناء يُنفَّذ في وقت الترجمة (compile-time) يُكرّر العتاد. حلقة for داخل generate تُنشئ N نسخة من جسمها. generate يعمل في وقت elaboration، قبل بدء المحاكاة - فهو ليس حلقة وقت تشغيل بل مولّد شيفرة لأداة التركيب.