في
المقالة الأولى من السلسلة ، روجت بنشاط لفكرة أن تطوير الكود الخاص بـ Redd هو ثانوي ، والمشروع الرئيسي أساسي. Redd أداة مساعدة ، لذا فإن قضاء الكثير من الوقت عليها يعد أمرًا خاطئًا. وهذا هو ، التنمية لذلك يجب أن تذهب بسرعة. ولكن هذا لا يعني على الإطلاق أن البرامج الناتجة لا ينبغي أن تكون الأمثل. في الواقع ، إذا لم يتم تحسينها على الإطلاق ، فلن تكون قوة الجهاز كافية لتنفيذ نظام الاختبار المطلوب. لذلك ، يجب أن تكون العملية ، كما قلت ، سريعة وسهلة ، ولكن يجب على المطور دائمًا مراعاة بعض مبادئ التحسين.

تم نشر كتب سميكة حول التحسين. بعض هذه الكتب مفيدة ، وبعضها قديم بالفعل ، لأن المبادئ الموضحة فيها قد انتقلت لفترة طويلة إلى مرحلة التحسين التلقائي عند إنشاء الكود ... لكن هناك بعض الأشياء التي لا قيمة لها عند تطوير برامج عادية للمعالجات العادية ، لذلك لا تصف الكتب النموذجية عادة . سنبدأ الآن في النظر فيها.
مقدمة
حتى الآن ، كتبت عن مبدأ "مشكلة واحدة - مقال واحد". وتم الحصول على المقالات في شكل محاضرات ، والتي تؤثر على العديد من المواضيع في وقت واحد ، متحدًا بمشكلة مشتركة. لكن بعض القراء قالوا إن مثل هذه المقالات لا يمكن قراءتها دفعة واحدة. لذلك ، سنحاول الآن التحدث عن موضوع واحد فقط في مقال واحد. كما أنه من الأسهل بالنسبة لي أن أكتب هكذا. دعونا نرى ، سيكون فجأة أكثر ملاءمة للجميع.
أيضا ، فرحة ناقلات غامضة. إذا تم نشر مقال في الصباح ، فإن أول ناقص له يصل بعد فترة من الزمن يستحيل خلالها قراءة النص بأكمله. شخص ما يفعل هذا من حيث المبدأ ، مع تجنيب مواضيع فقط عن UDB و balalaika. إذا كان المنشور لم يكن في الصباح ، ولكن في فترة ما بعد الظهر ، ثم يلقي ناقص مع تأخير. يصل الطرح الثاني خلال اليوم (وهذا الصديق ، بالمناسبة ، يدخر أيضًا موضوعات حول UDB و balalaika). سيكون هناك المزيد من المقالات بالتنسيق الجديد ، مما يعني أنه ستكون هناك لحظات ممتعة أكثر لهذا الزوجين (رغم أنني شخصياً ، كمؤلف ، يصبح حزينًا ومهينًا من أفعالهم).
المقالات السابقة في السلسلة:
- تطوير أبسط "البرامج الثابتة" FPGAs المثبتة في Redd ، وتصحيح الأخطاء باستخدام اختبار الذاكرة كمثال.
- تطوير أبسط "البرامج الثابتة" لأجهزة FPGA المثبتة في Redd. الجزء 2. رمز البرنامج.
- تطوير جوهرها الخاص لتضمينها في نظام المعالجات المستندة إلى FPGA.
- تطوير برامج للمعالج المركزي Redd على سبيل المثال من الوصول إلى FPGA.
- أول تجارب باستخدام بروتوكول الدفق على مثال اتصال وحدة المعالجة المركزية والمعالج في FPGA لمجمع Redd.
- ميلاد سعيد Quartusel ، أو كيف أن المعالج قد حان لهذه الحياة.
سلوك غامض من نظام نموذجي
لنقم بأبسط نظام للمعالجات من خلال تضمين ساعة ومعالج Nios II / f ووحدة تحكم SDRAM ومنفذ إخراج. هكذا يبدو Spartan هذا النظام في Platform Designer

سيحتوي رمز البرنامج الخاص به على وظيفة واحدة فقط ، يبدو نصها غريبًا إلى حد ما ، نظرًا لأنه يحتوي على العديد من الأسطر المتكررة ، ولكن هذا سيكون مفيدًا لنا.
الرمز مخفي لأنه ضيق جدًا.extern "C" { #include "sys/alt_stdio.h" #include <system.h> #include <io.h> } void MagicFunction() { while (1) { IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); } } int main() { MagicFunction(); /* Event loop never exits. */ while (1); return 0; }
ضع نقطة توقف على آخر الأسطر:
IOWR (PIO_0_BASE,0,0);
في
MagicFunction وتشغيل البرنامج. ما الذي حصلنا عليه عند خروج الميناء؟ نبضات خشنة للغاية:

الرعب حسنا نعم. ومع ذلك ، انقر فوق "الإطلاق" مرة أخرى لإكمال التكرار آخر من الحلقة. والآن عند الخروج نرى تعرجًا ناعمًا جميلًا:

التكرار آخر. وأكثر واحد ... مستقر التعرج. نزيل نقطة الإيقاف ونشاهد العمل في ديناميكيات - لم يعد هناك مثل هذه الاستراحات. هناك رشقات نارية لا نهاية لها من البقول.
لماذا مزقنا نبضات في أول تمريرة؟ حادث؟ لا. نتوقف عن التصحيح وبدء تشغيله مرة أخرى. ومرة أخرى نحصل على نبضات ممزقة. تنشأ الفجوات دائمًا عند مدخل البرنامج.
فكرة تكمن في ذاكرة التخزين المؤقت
في الواقع ، يكمن الحل لهذا السلوك في ذاكرة التخزين المؤقت. يتم تخزين برنامجنا في SDRAM. جلب رمز من SDRAM ليست سريعة. من الضروري إعطاء أمر قراءة ، ومن الضروري إعطاء عنوان ، ويتكون العنوان من جزأين. عليك أن تنتظر قليلا. عندها فقط سوف تعطي microcircuit البيانات. من أجل تجنب مثل هذه التأخيرات في كل مرة ، يمكن للدائرة الصغيرة إصدار واحدة ، ولكن عدة كلمات متتالية. لن نفكر في مخططات الوقت اليوم ، وسنؤجلها للمقالات التالية.
حسنًا ، على الجانب الأساسي للمعالج ، تم إنشاء ذاكرة تخزين مؤقت افتراضيًا. فيما يلي إعداداته:

في الواقع ، تحدث حالات تأخير في الوقت الذي يتم فيه تحميل مجموعة من الإرشادات من SDRAM إلى ذاكرة التخزين المؤقت. في التكرار التالي ، الرمز موجود بالفعل في ذاكرة التخزين المؤقت ، لذلك لم يعد التحميل مطلوبًا.
يظهر رسم الذبذبات متوسط 8 إدخالات لكل منفذ (وحدة واحدة مكتوبة 4 مرات والصفر مكتوب 4 مرات) لكل عملية تحميل. سجل واحد - أمر تجميع واحد ، والذي يمكن العثور عليه عن طريق اختيار عنصر القائمة نافذة-> إظهار عرض-> أخرى:

ثم Debug-> التفكيك:

فيما يلي سلاسلنا ورمز التجميع المقابل:

8 فرق من 4 بايت لكل منهما. نحصل على 32 بايت لكل سطر مخبأ ... ننظر إلى ملف المساعدة المفضل لدينا C: \ Work \ CachePlay \ software \ CachePlay_bsp \ system.h ونرى:
#define ALT_CPU_ICACHE_LINE_SIZE 32 #define ALT_CPU_ICACHE_LINE_SIZE_LOG2 5
تزامنت البيانات المحسوبة عمليا مع النظرية. علاوة على ذلك ، من الوثائق التي تتبع أنه لا يمكن تغيير حجم السلسلة. انها دائما تساوي اثنين وثلاثين بايت.
تجربة أكثر تعقيدا قليلا
دعونا نحاول استفزاز ذاكرة التخزين المؤقت لإعادة التشغيل أثناء العمل الثابت. دعنا نغير برنامج الاختبار قليلا. نجعل وظيفتين ونستدعيهما من الوظيفة
الرئيسية () ، ونضع حلقة فيه. لن أضع نقطة توقف. بالمناسبة ، إذا قمت بجعل الوظائف متطابقة تمامًا ، فسوف يلاحظ المُحسِّن هذا ويزيل أحدها ، لذلك سطر واحد على الأقل ، لكن يجب أن يختلفوا ... هذا هو ما كتبته في البداية: المحسّنون أذكياء للغاية الآن.
تعديل رمز برنامج الاختبار. extern "C" { #include "sys/alt_stdio.h" #include <system.h> #include <io.h> } void MagicFunction1() { IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); } void MagicFunction2() { IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); } int main() { while (1) { MagicFunction1(); MagicFunction2(); } /* Event loop never exits. */ while (1); return 0; }
نحصل على نتيجة جميلة جدًا ، تم التقاطها بالفعل في الوضع الثابت للبرنامج.

والآن سنضع بعض الوظائف الجديدة بين هذا الزوج من الوظائف ، ولن نسميها ، سيتم وضعها فقط في الذاكرة. الآن سأحاول جعلها تشغل مساحة أكبر ... حجم ذاكرة التخزين المؤقت هو 4 كيلو بايت ، لذلك سنجعلها تساوي أربعة كيلو بايت ... فقط أدخل 1024 NOPs ، كل منها بحجم 4 بايت. سأعرض نهاية أول وظيفة ، الوظيفة الجديدة وبداية الثانية ، بحيث يكون من الواضح كيف يتغير البرنامج:
... IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); } #define Nops4 __asm__ volatile ("nop");__asm__ volatile ("nop");__asm__ volatile ("nop");__asm__ volatile ("nop"); #define Nops16 Nops4 Nops4 Nops4 Nops4 #define Nops64 Nops16 Nops16 Nops16 Nops16 #define Nops256 Nops64 Nops64 Nops64 Nops64 #define Nops1024 Nops256 Nops256 Nops256 Nops256 volatile void FuncBetween() { Nops1024 } void MagicFunction2() { IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); ...
لم يتغير منطق البرنامج ، ولكن عندما يتم تشغيله الآن ، سنحصل على نبضات ممزقة

سأطرح سؤالًا ساذجًا: لقد خرجنا من ذاكرة التخزين المؤقت ، والآن ، مع اتساع الفجوة ، هل سيكون هناك دائمًا تحميل؟ لا على الاطلاق! قم بتغيير حجم الوظيفة "السيئة" ، مما يجعلها مساوية ، على سبيل المثال ، لخمسة كيلو بايت. خمسة أكثر من أربعة ، هل ما زلنا نطير؟ أم لا؟ يستعاض عن إدراج مع هذا:
volatile void FuncBetween() { Nops1024 Nops256 }
ومرة أخرى نحصل على الجمال:

فما الذي يحدد الحاجة إلى تحميل الكود في ذاكرة التخزين المؤقت؟ هل يمكننا التنبؤ بشيء ما ، أو في كل مرة نحتاج فيها إلى إلقاء نظرة على الحقيقة؟ دعنا
نتعمق في النظرية التي
يساعدنا عليها الدليل المرجعي لمعالج Nios II .
قليلا من الناحية النظرية
هذه هي الطريقة التي ينقسم بها حقل العنوان في المعالج:

كما ترون ، ينقسم العنوان إلى ثلاثة أجزاء. العلامة ، الخط والإزاحة. البعد لحقل الإزاحة ثابت لمعالج Nios II وهو دائمًا خمسة بتات ، أي أنه يمكنه معالجة 32 بايت. يعتمد بُعد حقل "الخط" على حجم ذاكرة التخزين المؤقت المحددة عند تكوين المعالج. في الشكل أعلاه ، إنه كبير جدًا. لا أعرف لماذا يحتوي المستند على مثل هذا البعد الضخم. لدينا حجم ذاكرة التخزين المؤقت من 4 كيلو بايت ، مما يعني أن عمق البت الكلي والإزاحة هي 12 بت. 5 بت تأخذ إزاحة ، لخط يبقى 12-5 = 7 بت.
نحصل على جدول معين يتكون من 128 صفًا ، يبلغ طول كل منها 32 بايت. سأقدم ، على سبيل المثال ، الخطوط الستة الأولى:
وهكذا تحولنا إلى العنوان 0x123
004 . إذا تجاهلت الجزء "غير المهم" ، فسيكون زوج "line + offset" هو 0x004. هذا هو نطاق الصف صفر. سيتم تحميل البيانات في هذا الخط. والمزيد من العمل مع البيانات من النطاق 0x123
000 إلى 0x123
01F ستعمل من خلال ذاكرة التخزين المؤقت. تحت أي ظروف سيتم تحميل السلسلة بشكل زائد؟ عند الوصول إلى أي عنوان آخر ينتهي في النطاق من 0x000 إلى 0x01F. حسنًا ، إذا انتقلنا إلى العنوان 0xABC
204 ، فسيظل كل شيء في مكانه ، لأن نطاق العناوين الأدنى لا يتداخل مع عنواننا. و 0xABC
804 لن تدمر أي شيء. ولكن عند تنفيذ التعليمات البرمجية من العنوان 0xABC
004 ، سينتج عن ذلك تحميل محتويات جديدة في سطر ذاكرة التخزين المؤقت. وبالفعل فإن الانتقال إلى العنوان 0x123
004 سيؤدي مرة أخرى إلى زيادة التحميل. إذا كنت تقفز باستمرار بين 0xABC
004 و 0x123
004 ، فستحدث زيادة التحميل بشكل مستمر.
دعنا نحاول تصوير هذا في شكل صورة. افترض أن لدينا 8 خطوط فقط في ذاكرة التخزين المؤقت ، وهو أكثر ملاءمة لتلوينها بألوان مختلفة. سأجعل حجم الخط 0x10 أكثر ملاءمة لرسم العناوين في الصورة (تذكر أنه في Nios II الحقيقي يكون حجم الخط دائمًا 0x20 بايت). تتفوق الذاكرة على الصفحات الشرطية بنفس حجم خطوط ذاكرة التخزين المؤقت. ستنتقل الصفحة الحمراء للذاكرة دائمًا إلى الخط الأحمر لذاكرة التخزين المؤقت والبرتقالي إلى البرتقالي وما إلى ذلك. وفقا لذلك ، سيتم تفريغ المحتويات القديمة.

حسنًا ، في الواقع ، أصبح سلوك البرنامج أثناء التجربة واضحًا الآن. عندما تم فصل الوظائف بدقة عن طريق 4 كيلو بايت ، فإنها تضرب صفحات ذات ألوان متشابهة. لذلك الرمز
while (1) { MagicFunction1(); MagicFunction2(); }
أدى إلى تحميل ذاكرة التخزين المؤقت من أجل واحد ، ثم من أجل وظيفة أخرى. وعندما لا يكون التباعد 4 ، ولكن 5 كيلو بايت ، كانت المسافات متباعدة في كتل بألوان مختلفة. لم يكن هناك صراع ، كل شيء يعمل دون تأخير.
النتائج
عندما قرأت منذ سنوات عديدة أن هناك خطوطًا من النوى Cortex A و Cortex R و Cortex M مصممة للأشياء المنتجة وللعمل في الوقت الفعلي وللعمل في أنظمة رخيصة ، على التوالي ، في البداية ، لم أفهم ، ولكن ما الفرق ، في الواقع ، هو الفرق . لا ، الأنظمة الرخيصة مفهومة ، لكن الأولين هما ما الاختلافات؟ ومع ذلك ، بعد لعب كورتيكس A9 المتوفر في Cyclone V SoC FPGA ، شعرت بكل عيوب ذاكرة التخزين المؤقت عند العمل بالحديد. هناك العديد من ذاكرات التخزين المؤقت في جوهر Cortex A ... وإمكانية التنبؤ بسلوك النظام تكاد تكون صفرية. ولكن ذاكرة التخزين المؤقت تحسين الأداء. في بعض الأحيان يكون من الأفضل إذا كان كل شيء يعمل بطريقة غير دقيقة على نحو متوقع بالنسبة للفوز ، ولكن بسرعة من بطئ متوقع. ينطبق هذا بشكل خاص على الحوسبة أو ، على سبيل المثال ، عرض الرسومات.
لكن المشكلة الرئيسية ليست أن الأشياء الموضحة في المقالة تنشأ ، بل أن سلوك النظام سوف يتغير من التجميع إلى التجميع ، حيث لا أحد يعرف عناوين هذه الوظيفة التي ستنخفض بعد إضافة أو إزالة الكود. قبل 15 عامًا ، في مشروع محاكي وحدة ألعاب Sega لوحدة فك تشفير تلفزيون الكبل ، كان علينا أن نجعل مُعالجًا أوليًا ، بعد كل عملية تحرير ، نقل الوظائف التي كانت تحاكي أوامر مجمّع موتورولا على SPARC-8 الأساسية حتى يكون وقت تنفيذها دائمًا هو نفسه (هناك بسبب ذاكرة التخزين المؤقت ، وإلا كل شيء سبح كثيرا).
ولكن متى نحتاج إلى القدرة على التنبؤ؟ بالطبع ، أثناء تكوين المخططات الزمنية برمجيًا (تذكر أنه بشكل عام في FPGAs من الممكن أن يعهد بذلك إلى الجهاز أيضًا ، ولكن هناك بعض التفاصيل مع التطور السريع). ولكن عند العمل مع الخوارزميات الحسابية ، فإنه ليس مهمًا جدًا. ما لم تكن الخوارزمية معقدة ، فأنت بحاجة إلى التأكد من أن الأقسام الهامة لا تسبب حملًا زائدًا في ذاكرة التخزين المؤقت. في معظم الحالات ، لا تخلق ذاكرة التخزين المؤقت مشاكل ، وزيادة الإنتاجية.
في المقالة التالية ، سنبحث في كيفية التنبؤ بالوظائف الحرجة في ذاكرة غير قابلة للتخزين المؤقت ، والتي تعمل دائمًا بأقصى سرعة ، ومناقشة المزايا الضمنية لـ FPGAs على الأنظمة القياسية الناشئة عن التقنيات المستخدمة في هذه العملية.
لأكثر يقظة
قد يتساءل القارئ المتآكل: "لماذا لم يتم مزق التذبذب بشكل كافٍ عند إدخال أربعة كيلو بايت من الشفرة؟" كل شيء بسيط. إذا قمت بإدخال 4 كيلو بايت بالضبط ، فسوف نحصل على العناوين التالية لوضع وظائف في الذاكرة:
MagicFunction1(): 0200006c: movhi r2,1024 02000070: movi r4,1 02000074: addi r2,r2,4096 02000078: stwio r4,0(r2) 92 IOWR (PIO_0_BASE,0,0); 0200007c: mov r3,zero 02000080: stwio r3,0(r2) 93 IOWR (PIO_0_BASE,0,1); ... 120 IOWR (PIO_0_BASE,0,0); 020000f0: stwio r3,0(r2) 020000f4: ret 131 Nops1024 FuncBetween(): 020000f8: nop 020000fc: nop 02000100: nop 02000104: nop ... 020010ec: nop 020010f0: nop 020010f4: nop 020010f8: ret 135 IOWR (PIO_0_BASE,0,0); MagicFunction2(): 020010fc: movhi r2,1024 02001100: mov r4,zero 02001104: addi r2,r2,4096
للحصول على شكل موجي سيء تمامًا ، تحتاج إلى إدراج NOPs بحيث يكون حجم وحدة التخزين 4 كيلوبايت مع طول
دالة MagicFunction1 () . لا يهم ما تذهب لصورة جميلة! تغيير الإدراج إلى هذا:
volatile void FuncBetween() { Nops256 Nops256 Nops256 Nops64 Nops64 Nops64 Nops16 Nops16 }
مرارًا وتكرارًا ، ألاحظ أن الملحق لا يتلقى التحكم. إنه ببساطة يغير موضع الوظائف في الذاكرة بالنسبة لبعضها البعض. مع هذا الإدراج ، نحصل على الرعب الرهيب المطلوب:

يبدو لي أن مثل هذه التفاصيل التي أدرجت في النص الرئيسي ستصرف انتباه الجميع عن التفاصيل الرئيسية ، لذلك وضعت لهم في حاشية.