العاملان
داخل كتل always وinitial، يوفّر Verilog عاملي إسناد:
=هو إسناد blocking. حدّث LHS الآن، قبل الانتقال إلى الجملة التالية.<=هو إسناد non-blocking. قيّم RHS الآن، جدوِل LHS ليُحدَّث في نهاية الخطوة الزمنية الحالية.
خارج الكتل الإجرائية (في assign) يوجد فقط = - assign a <= b خطأ صياغي. داخل الكتل الإجرائية، كلاهما شرعي، واختيار الصحيح هو القرار الأهم في Verilog للمبتدئين.
ماذا يفعل blocking
Blocking هو ما تتوقعه من لغة برمجيات: سطرًا تلو الآخر، من الأعلى للأسفل. الجملة N تكتمل قبل بدء الجملة N+1:
هذا تسلسلي بحت. كل جملة ترى آثار التي فوقها. هذا يطابق حدس البرمجيات.
ماذا يفعل non-blocking
Non-blocking مُشكَّل على هيئة العتاد. كل الأطراف اليمنى تُقَيَّم باستخدام القيم في بداية الخطوة الزمنية. كل الأطراف اليسرى تُحدَّث في نهاية الخطوة الزمنية. ترتيب الجمل في المصدر لا يُغيّر التبعيات بين الإشارات:
تلك الشيفرة تُنفّذ تدوير 3 طرق لـ a → b → c → a في خطوة واحدة. مع blocking، ستحتاج متغيرًا مؤقتًا لتجنب إفساد أحدها. مع non-blocking، يحدث التبادل ذريًا لأن كل RHS يقرأ قيم ما قبل الخطوة.
هذا بالضبط كيف تتصرف ثلاث flip-flops عند حافة clock: كلها تلتقط مدخلاتها في نفس اللحظة، بصرف النظر عن التبعيات بينها.
القاعدة التي تنقذك
استخدم <= في الكتل المتزامنة مع clock. استخدم = في الكتل التوافقية.
هذا كل شيء. احفظه. اكتبه بدون تفكير. معظم race conditions، وعدم تطابق المحاكاة/التركيب، وأخطاء "تعمل عندي لكن ليس في التركيب" تأتي من انتهاك هذه القاعدة.
للقاعدة سبب عتادي: الكتل المتزامنة مع clock تنمذج flip-flops تأخذ عينات من مدخلاتها في وقت واحد. الكتل التوافقية تنمذج منطقًا ينتشر بأسرع ما يمكن. دلالات عامل الإسناد تطابق تلك السلوكيات.
كل <= يقرأ القيمة الحالية لـ register المصدر ويُجدول register الوجهة ليأخذ تلك القيمة في نهاية الخطوة الزمنية. التأثير الصافي هو بالضبط ما يفعله shift register عتادي: كل flip-flop يلتقط قيمة جاره، كلها في نفس حافة clock، بلا سباق.
تأمل ما كان سيحدث مع blocking assignment:
// WRONG - this is not a shift register!
always @(posedge clk) begin
out[3] = out[2]; // out[3] becomes out[2]
out[2] = out[1]; // out[2] becomes out[1], which we just set above
out[1] = out[0];
out[0] = in;
end
كل جملة تُفسد المصدر قبل أن تقرأه الجملة التالية. في دورة clock واحدة، سينتشر in كل الطريق إلى out[3]، لأن كل سطر يرى القيمة المكتوبة حديثًا للسطر فوقه. السلوك على العتاد الحقيقي (الذي يستخدم دلالات non-blocking) سيكون مختلفًا تمامًا عما أظهر المحاكي.
الكتل التوافقية: blocking صحيح
لـ always @(*)، blocking assignment صحيح. لا flip-flops، لا قاعدة التقاط متزامن لفرضها، والمتغيرات الوسيطة مفيدة:
sum يُحسب أولًا، ثم result يستخدم القيمة المحسوبة لتوّها. المنطق التوافقي يُسطَّح إلى قطعة واحدة من العتاد: result = ~(a + b). لا تظهر flip-flops لأن لا يوجد clock.
إن استخدمت <= هنا، سيُحدّث المحاكي sum قبل تقييم result ضده (لأن كلا التحديثين يحدثان عند نهاية الخطوة)، لكن الترتيب سيكون مختلفًا بدقة وكثير من أدوات التركيب تشكو. لا تخلط؛ اختر العامل الذي يطابق نوع الكتلة.
الخطأ الأكثر إيلامًا
ها هو: كتلة متزامنة مع clock بـ blocking assignment.
// BUG: race condition waiting to happen
always @(posedge clk) begin
a = b;
b = c;
c = a;
end
في المحاكاة، قد يعطي المحاكي a أولًا، ثم b، ثم c، فينتج مجموعة قيم. العتاد سيُنتج مجموعة أخرى، لأن flip-flops الحقيقية تلتقط في وقت واحد. الاثنان يتباعدان بصمت وستضيع يومًا في إيجاد الخطأ. استخدم <= في الكتل المتزامنة مع clock.
لماذا يوجد عاملان أصلًا
كان بإمكان مصممي Verilog اختيار دلالة إسناد واحدة والالتزام بها. لم يفعلوا، لأن اللغة عليها نمذجة سلوكين عتاديين متمايزين:
- المنطق التوافقي: الإشارات تنتشر باستمرار، التبعيات تهم، "ماذا تحسب هذه البوابة" منطقي.
- المنطق التسلسلي: حافة clock تحدث، كل flip-flop يلتقط في وقت واحد، التبعيات بين مدخلات flip-flop ومخارجها مفكوكة.
Blocking للأول. Non-blocking للثاني. العامل يختار الدلالة؛ المحاكي يفعل الباقي.
ماذا بعد
لديك الآن القواعد لكتابة أي كتلة إجرائية بشكل صحيح. الفصل التالي يصعد من الكتل الفردية إلى بناءات التحكم في التدفق التي تذهب داخلها: if/else وcase وحلقات for. قواعد blocking مقابل non-blocking ما زالت تنطبق داخلها كلها.
الأسئلة الشائعة
ما الفرق بين blocking وnon-blocking assignment في Verilog؟
Blocking (=) يُحدّث الهدف فورًا، قبل تشغيل الجملة التالية. ينمذج التنفيذ التسلسلي. Non-blocking (<=) يُجدول التحديث لنهاية الخطوة الزمنية الحالية - كل RHS non-blocking يُقَيَّم باستخدام القيم القديمة لكل الإشارات، ثم كل LHS يُحدَّث في خطوة منسقة واحدة. Non-blocking هو كيف يتصرف flip-flop فعلًا.
متى أستخدم = مقابل <= في Verilog؟
القاعدة: استخدم <= في كتل always @(posedge clk) المتزامنة مع clock، واستخدم = في كتل always @(*) التوافقية. تلك القاعدة الواحدة تمنع كامل صنف race conditions التي تُنتجها الإسنادات المختلطة. داخل كتل initial لـ testbench، = هو الخيار الطبيعي؛ <= يظهر نادرًا هناك.
لماذا non-blocking assignment ضرورية في Verilog؟
لأن flip-flops العتاد جميعها تلتقط مدخلاتها في وقت واحد عند حافة clock. إن استخدمت = (blocking) في شيفرة متزامنة مع clock، فإن ترتيب الجمل في ملفك سيُغير أي إشارة ترى القيمة الجديدة لأي إشارة أخرى - race condition بين المحاكاة والعتاد الحقيقي. <= يطابق سلوك العتاد بتقييم كل RHS أولًا، ثم القيام بكل التحديثات.
ماذا يحدث إن خلطت = و<= في نفس كتلة Verilog always؟
تحصل على race condition. الخلط يُنتج عتادًا يعتمد سلوكه على داخليات المحاكي (ترتيب جدولة الأحداث)، والأسوأ، أن المحاكاة قد لا تطابق ما يُنتجه التركيب. معظم أدوات lint تُعلّم هذا كخطأ. الحل هو الالتزام بأحدهما حسب دور الكتلة - non-blocking للمتزامن مع clock، blocking للتوافقي.