نخلق لعبة منصة محمولة على متحكم Cortex M0 +


مقدمة


(يتم توفير روابط إلى شفرة المصدر ومشروع KiCAD في نهاية المقال.)

على الرغم من أننا ولدنا في عصر 8 بت ، إلا أن جهاز الكمبيوتر الخاص بنا كان Amiga 500. إنه جهاز رائع 16 بت مع رسومات مذهلة وصوت رائع ، مما يجعله رائعًا للألعاب. أصبحت Platforming نوع لعبة شائع جدًا على هذا الكمبيوتر. وكان الكثير منهم الملونة للغاية وكان التمرير المنظر سلس جدا. أصبح هذا ممكنًا بفضل المبرمجين الموهوبين الذين استخدموا Amiga coprocessors ببراعة لزيادة عدد ألوان الشاشة. نلقي نظرة على LionHeart على سبيل المثال!


قلب الأسد على أميغا. هذه الصورة الساكنة لا تنقل جمال الرسومات.

منذ التسعينيات ، تغير الإلكترونيات كثيرًا ، وهناك الآن العديد من ميكروكنترولر الصغيرة التي تسمح لك بإنشاء أشياء مذهلة.

لقد أحببنا دائمًا ألعاب المنصات ، واليوم ، مقابل بضعة دولارات فقط ، يمكنك شراء Raspberry Zero ، وتثبيت Linux ، وكتابة لعبة منصة ملونة بسهولة.

لكن هذه المهمة ليست لنا - نحن لا نريد أن نطلق النار على العصافير من مدفع!

نريد استخدام متحكم دقيق ذو ذاكرة محدودة ، وليس نظامًا قويًا على شريحة مزودة بوحدة معالجة الرسومات المدمجة! بمعنى آخر ، نريد صعوبات!

بالمناسبة ، حول إمكانيات الفيديو: يتمكن بعض الأشخاص من الضغط على جميع العصائر من متحكم AVR في مشاريعهم (على سبيل المثال ، في مشروع Uzebox أو Craft لمطور lft). ومع ذلك ، لتحقيق ذلك ، تجبرنا ميكروكنترولر AVR على الكتابة في مجمع ، وعلى الرغم من أن بعض الألعاب جيدة للغاية ، فسوف تواجه قيودًا خطيرة لا تسمح لك بإنشاء لعبة بأسلوب 16 بت.

لذلك ، قررنا استخدام متحكم / لوحة أكثر توازناً ، مما يسمح لنا بكتابة التعليمات البرمجية بالكامل في C.

إنه ليس بنفس قوة Arduino Due ، ولكنه ليس ضعيفًا مثل Arduino Uno. ومن المثير للاهتمام ، أن كلمة "Due" تعني "two" ، و "Uno" تعني "one". لقد علمتنا Microsoft أن نحسب بشكل صحيح (1 ، 2 ، 3 ، 95 ، 98 ، ME ، 2000 ، XP ، Vista ، 7 ، 8 ، 10) ، كما ذهب Arduino بهذه الطريقة! سوف نستخدم Arduino Zero ، والذي يقع في الوسط بين 1 و 2!

نعم ، وفقا لأردوينو ، 1 <0 <2.

على وجه الخصوص ، نحن لسنا مهتمين باللوحة نفسها ، ولكن في سلسلة المعالجات الخاصة بها. يحتوي Arduino Zero على متحكم سلسلة ATSAMD21 مع Cortex M0 + (48 ميجاهرتز) وذاكرة فلاش 256 كيلو بايت و 32 كيلوبايت RAM.

على الرغم من أن Cortex M0 + بسرعة 48 ميجاهرتز يتفوق بشكل كبير على MC68000 القديم البالغ 7 ميجاهرتز في الأداء ، فإن جهاز Amiga 500 يحتوي على 512 كيلوبايت من ذاكرة الوصول العشوائي ، وأنواع الأجهزة ، ولوحة الألعاب المزدوجة المتكاملة ، Blitter (محرك نقل كتلة الصور المستندة إلى DMA مع نظام التعرف على التصادم المدمج في البكسل) والشفافية) والنحاس (المعالج الثانوي النقطي الذي يسمح لك بإجراء عمليات مع السجلات بناءً على موضع الاجتياح لإنشاء العديد من التأثيرات الجميلة جداً). لا يحتوي SAMD21 على كل هذه الأجهزة (باستثناء جهاز بسيط إلى حد ما مقارنةً بـ Blitter DMA) ، لذلك سيتم تقديم الكثير برمجيًا.

نريد تحقيق المعلمات التالية:

  • الدقة 160 × 128 بكسل على شاشة SPI بحجم 1.8 بوصة.
  • الرسومات مع 16 بت لكل بكسل.
  • أعلى معدل الإطار. 25 إطارًا في الثانية على الأقل بسرعة 12 ميجاهرتز أو 40 إطارًا في الثانية بسرعة 24 ميجاهرتز ؛
  • ميدان اللعب المزدوج مع التمرير المنظر.
  • كل شيء مكتوب في C. لا رمز المجمع.
  • دقة بكسل الاعتراف الاصطدامات.
  • تراكب الشاشة.

يبدو أن تحقيق هذه الأهداف صعب للغاية. إنه ، خاصة إذا رفضنا الكود على أسم!

على سبيل المثال ، مع لون 16 بت ، سيتطلب حجم الشاشة 160 × 128 بكسل 40 كيلوبايت للمخزن المؤقت للشاشة ، ولكن لدينا فقط 32 كيلو بايت من ذاكرة الوصول العشوائي! وما زلنا بحاجة إلى التمرير المنظر على ملعب مزدوج وأكثر من ذلك بكثير ، مع تردد لا يقل عن 25/40 إطارًا في الثانية!

لكن لا شيء مستحيل بالنسبة لنا ، أليس كذلك؟

نحن نستخدم الحيل والوظائف المدمجة في ATSAMD21! ك "الأجهزة" نأخذ uChip ، والتي يمكن شراؤها في متجر Itaca .


uChip: قلب مشروعنا!

له نفس خصائص Arduino Zero ، ولكنه أصغر بكثير ، وأرخص أيضًا من Arduino Zero الأصلي (نعم ، يمكنك شراء Arduino Zero وهمية مقابل 10 دولارات على AliExpress ... لكننا نريد أن نبني على الأصل). سيسمح لنا ذلك بإنشاء وحدة تحكم محمولة صغيرة. يمكنك تكييف هذا المشروع مع Arduino Zero دون عناء تقريبًا ، وستكون النتيجة فقط مرهقة جدًا في الحجم.

كما أنشأنا لوحة اختبار صغيرة تنفذ وحدة تحكم محمولة للفقراء. التفاصيل أدناه!


لن نستخدم إطار اردوينو. ليست مناسبة تمامًا عندما يتعلق الأمر بتحسين المعدات وإدارتها. (ودعونا لا نتحدث عن IDE!)

في هذه المقالة ، سنصف كيف وصلنا إلى الإصدار النهائي من اللعبة ، ونصف جميع التحسينات والمعايير المستخدمة. اللعبة نفسها لم تكتمل بعد ، فهي تفتقر إلى الصوت ، والمستويات ، إلخ. ومع ذلك ، يمكن استخدامه كنقطة انطلاق للعديد من أنواع الألعاب المختلفة!

بالإضافة إلى ذلك ، هناك العديد من خيارات التحسين ، حتى بدون التجميع!

لذلك ، دعونا نبدأ رحلتنا!

تعقيد


في الواقع ، للمشروع جانبان معقدان: التوقيت والذاكرة (ذاكرة الوصول العشوائي والتخزين).

الذاكرة


لنبدأ بالذاكرة. أولاً ، بدلاً من تخزين صورة كبيرة المستوى ، نستخدم البلاط. في الواقع ، إذا قمت بتحليل معظم المنصات بعناية ، ستلاحظ أنها قد تم إنشاؤها من عدد صغير من العناصر الرسومية (التجانبات) التي تتكرر عدة مرات.


توريكان 2 في أميغا. واحدة من أفضل الألعاب منصة في كل العصور. يمكنك أن ترى بسهولة البلاط في ذلك!

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

الحيلة الثانية التي نستخدمها ممكنة بسبب القدرة الحسابية الكبيرة لجامعة كاليفورنيا ووجود DMA! بدلاً من تخزين جميع بيانات الإطار في ذاكرة الوصول العشوائي (ولماذا تكون هناك حاجة لذلك؟) سنقوم بإنشاء مشهد في كل إطار من البداية. على وجه الخصوص ، سنستمر في استخدام المخازن المؤقتة ، لكن بحيث تناسبها في كتلة أفقية واحدة من رسومات البيانات بارتفاع 16 بكسل.

توقيت - وحدة المعالجة المركزية


عندما يحتاج المهندس إلى إنشاء شيء ما ، فإنه يتحقق أولاً لمعرفة ما إذا كان هذا ممكنًا. بالطبع ، في البداية ، أجرينا هذا الاختبار!

لذلك ، نحن بحاجة إلى 25 إطارًا في الثانية على الأقل على شاشة بحجم 160 × 128 بكسل. هذا هو 512000 بكسل / ثانية. نظرًا لأن المتحكم الدقيق يعمل بتردد 48 ميجاهرتز ، فلدينا 93 دورة على الأقل لكل ساعة بكسل. تنخفض هذه القيمة إلى 58 دورة إذا كنا نهدف إلى 40 إطارًا في الثانية.

في الواقع ، فإن متحكمنا قادر على معالجة ما يصل إلى 2 بكسل في كل مرة ، لأن كل بكسل يستغرق 16 بت ، ولديه ATSAMD21 ناقل داخلي 32 بت ، أي أن الأداء سيكون أفضل!

تبلغنا قيمة 93 دورة على مدار الساعة أن المهمة قابلة للتنفيذ تمامًا! في الواقع ، يمكننا أن نستنتج أن وحدة المعالجة المركزية وحدها يمكنها معالجة جميع مهام التقديم دون DMA. على الأرجح ، هذا صحيح ، خاصة عند العمل مع المجمّع. ومع ذلك ، سيكون من الصعب للغاية التعامل مع الكود. وفي C يجب أن يكون الأمثل للغاية! في الواقع ، ليست Cortex M0 + سهلة الاستخدام لـ C مثل Cortex M3 ، وتفتقر إلى الكثير من التعليمات (لا يتم تحميلها / حفظها مع زيادة / تناقص لاحق / أولي!) ، والتي يجب تنفيذها بتعليمين أو أكثر.

دعونا نرى ما يتعين علينا القيام به لرسم ملعبين (على افتراض أننا نعرف بالفعل إحداثيات x و y ، إلخ).

  • حساب موقع بكسل المقدمة في ذاكرة فلاش.
  • الحصول على قيمة بكسل.
  • إذا كان شفافًا ، فقم بحساب موضع بكسل الخلفية في الفلاش.
  • الحصول على قيمة بكسل.
  • حساب الموقع المستهدف.
  • حفظ بكسل إلى المخزن المؤقت.

علاوة على ذلك ، يجب أن يتم تنفيذ العمليات التالية لكل العفاريت التي يمكنها الوصول إلى المخزن المؤقت:

  • حساب موضع بكسل العفريت في ذاكرة فلاش.
  • الحصول على قيمة بكسل.
  • إذا لم تكن شفافة ، فقم بحساب موقع المخزن المؤقت الوجهة.
  • حفظ بكسل في المخزن المؤقت.

لا يتم تنفيذ جميع هذه العمليات فقط كتعليمات ASM واحدة ، ولكن كل تعليمات ASM تتطلب دورتين عند الوصول إلى ذاكرة الوصول العشوائي / ذاكرة الفلاش.

بالإضافة إلى ذلك ، لا نزال لا نملك منطق اللعب (والذي ، لحسن الحظ ، يستغرق وقتًا صغيرًا ، لأنه يتم حسابه مرة واحدة لكل إطار) ، والتعرف على التصادم ، ومعالجة المخزن المؤقت ، والتعليمات الضرورية لإرسال البيانات عبر SPI.

على سبيل المثال ، فيما يلي الكود الزائف لما يتعين علينا القيام به (في الوقت الحالي ، نفترض أن اللعبة لا تحتوي على التمرير ، ولعب الملعب خلفية ملونة ثابتة!) فقط للمقدمة.

اجعل الكاميرا و cameraX هما إحداثيات الزاوية العليا اليسرى من الشاشة في عالم الألعاب.

اجعل xTilepos و yTilepos هما موضع التجانب الحالي على الخريطة.

xTilepos = cameraX / 16; // this is a rightward shift of 4 bits. yTilepos = cameraY / 16; destBufferAddress = &buffer[0][0]; for tile = 0...9 nTile = gameMap[yTilepos][xTilepos]; tileDataAddress = &tileData[nTile]; xTilepos = xTilepos + 1; for y = 0…15 for x = 0…15 pixel = *tileDataAddress; tileDataAddress = tileDataAddress + 1; *destBufferAddress = pixel; destBufferAddress = destBufferAddress + 1; next destBufferAddress = destBufferAddress + 144; // point to next row next destBufferAddress = destBufferAddress – ( 160 * 16 - 16); // now point to the position where the next tile will be saved. next 

يبلغ عدد التعليمات الخاصة بـ 2560 بكسل (160 × 16) حوالي 16 كيلو بايت ، أي 6 لكل بكسل. في الواقع ، يمكنك رسم بكسلين في وقت واحد. يؤدي هذا إلى تقليل عدد الإرشادات الفعلية لكل بكسل إلى النصف ، أي أن عدد الإرشادات عالية المستوى لكل بكسل هو 3 تقريبًا. ومع ذلك ، سيتم تقسيم بعض هذه الإرشادات عالية المستوى إلى إثنين من إرشادات المجمّع أو أكثر ، أو تتطلب دورتين على الأقل لإكمالهما عند الوصول إلى الذاكرة. أيضًا ، لم نفكر في إعادة تعيين خط أنابيب وحدة المعالجة المركزية بسبب حالات القفز والانتظار لذاكرة فلاش. نعم ، نحن لا نزال بعيدين عن 58-93 دورات تحت تصرفنا ، لكننا ما زلنا بحاجة إلى أن نأخذ في الاعتبار خلفية الملعب والعفاريت.

على الرغم من أننا نرى أنه يمكن حل المشكلة على وحدة المعالجة المركزية واحدة ، إلا أن DMA ستكون أسرع بكثير. يتيح الوصول المباشر للذاكرة مساحة أكبر لشفرات الشاشة أو تأثيرات رسومية أفضل (على سبيل المثال ، يمكننا تنفيذ مزج ألفا).

سنرى أنه لتكوين DMA لكل تجانب ، نحتاج إلى أقل من 100 تعليمات C ، أي أقل من 0.5 لكل بكسل! بالطبع ، لا يزال يتعين على DMA تنفيذ نفس عدد عمليات النقل في الذاكرة ، ولكن يتم تنفيذ زيادة العنوان ونقله دون تدخل وحدة المعالجة المركزية ، والتي يمكنها القيام بشيء آخر (على سبيل المثال ، حساب وعرض العفاريت).

باستخدام مؤقت SysTick ، ​​اكتشفنا أن الوقت اللازم لإعداد DMA للكتلة بأكملها ، ثم لإكمال DMA ، يساوي تقريبًا 12 ساعة من دورات الساعة. ملاحظة: دورات على مدار الساعة! ليست تعليمات عالية المستوى! عدد الدورات مرتفع جدًا بالنسبة إلى 2560 بكسل فقط ، أي 1280 كلمة 32 بت. في الواقع ، نحصل على حوالي 10 دورات لكل كلمة 32 بت. ومع ذلك ، تحتاج إلى مراعاة الوقت اللازم لإعداد DMA ، بالإضافة إلى الوقت الذي يستغرقه DMA لتحميل واصفات النقل من ذاكرة الوصول العشوائي (والتي تحتوي أساسًا على مؤشرات وعدد البايتات المنقولة). بالإضافة إلى ذلك ، هناك دائمًا نوع من تغيير ناقل الذاكرة (بحيث لا تقف وحدة المعالجة المركزية خاملاً بدون بيانات) ، وتتطلب ذاكرة الفلاش حالة انتظار واحدة على الأقل.

توقيت - SPI


عنق الزجاجة آخر هو SPI. هل 12 ميجاهرتز كافية لمدة 25 إطارًا في الثانية؟ الجواب هو نعم: 12 ميغاهيرتز يتوافق مع حوالي 36 لقطة في الثانية الواحدة. إذا استخدمنا 24 ميغاهيرتز ، فإن الحد سيتضاعف!

بالمناسبة ، تشير مواصفات الشاشة وجهاز التحكم الدقيق إلى أن الحد الأقصى لسرعة SPI هو 15 و 12 MHz على التوالي. لقد اختبرنا وتأكدنا من أنه يمكن زيادتها إلى 24 ميجاهرتز دون مشاكل ، على الأقل في "الاتجاه" الذي نحتاج إليه (يكتب متحكم الشاشة).

سوف نستخدم شاشة SPI الشهيرة مقاس 1.8 بوصة. لقد تأكدنا من أن كلاً من ILI9163 و ST7735 يعملان بشكل طبيعي بتردد 12 ميجاهرتز (على الأقل مع 12 ميجاهرتز. تم التحقق من أن ST7735 يعمل بتردد يصل إلى 24 ميجاهرتز). إذا كنت ترغب في استخدام نفس الشاشة كما في البرنامج التعليمي "كيفية تشغيل مقاطع الفيديو على Arduino Uno" ، فإننا نوصي بتعديلها في حالة الرغبة في إضافة دعم SD في المستقبل. نحن نستخدم إصدار بطاقة SD بحيث نوفر مساحة كبيرة لعناصر أخرى ، مثل الصوت أو مستويات إضافية.

الرسومات


كما ذكرنا سابقًا ، تستخدم اللعبة البلاط. سيتألف كل مستوى من البلاط المكرر وفقًا للجدول ، والذي أطلقنا عليه اسم "gameMap". كم سيكون حجم كل بلاطة؟ يؤثر حجم كل مربع بشكل كبير على استهلاك الذاكرة والتفاصيل والمرونة (كما سنرى لاحقًا ، السرعة أيضًا). ستتطلب المربعات الكبيرة جدًا إنشاء بلاطة جديدة لكل تباين صغير نحتاجه. سيستغرق هذا مساحة كبيرة على محرك الأقراص.


اثنين من البلاط 32 × 32 بكسل في الحجم (يسار ووسط) ، والتي تختلف في جزء صغير (الجزء العلوي الأيمن من بكسل هو 16 × 16). لذلك ، نحن بحاجة إلى تخزين اثنين من البلاط مختلفة بحجم 32 × 32 بكسل. إذا استخدمنا بلاط 16 × 16 بكسل (على اليمين) ، فسنحتاج إلى تخزين اثنين فقط من البلاط 16 × 16 (بلاط أبيض تمامًا وبلاط على اليمين). ومع ذلك ، عند استخدام مربعات 16 × 16 ، نحصل على 4 عناصر خريطة.

ومع ذلك ، هناك حاجة إلى عدد أقل من البلاط لكل شاشة ، مما يزيد من السرعة (انظر أدناه) ويقلل من حجم الخريطة (أي عدد الصفوف والأعمدة في الجدول) من كل مستوى. البلاط الصغيرة جدًا تخلق المشكلة المعاكسة. جداول الخرائط تزداد وسرعة أبطأ. بالطبع ، لن نتخذ قرارات غبية. على سبيل المثال ، حدد مربعات بحجم 17 × 31 بكسل. صديقنا المخلص - درجتان! الحجم 16 × 16 هو "القاعدة الذهبية" تقريبًا ، ويتم استخدامه في العديد من الألعاب ، وسنختار ذلك!

حجم شاشتنا هو 160 × 128. بمعنى آخر ، نحتاج إلى 10 × 8 بلاطات لكل شاشة ، أي 80 إدخالات في الجدول. بالنسبة لمستوى كبير من 10 × 10 شاشات (أو 100 × 1 شاشات) ، ستكون هناك حاجة إلى 8000 سجل فقط (16 كيلوبايت إذا استخدمنا 16 بت للتسجيل. في وقت لاحق سوف نوضح لماذا قررنا اختيار 16 بت للتسجيل).

قارن هذا مع مقدار الذاكرة التي من المحتمل أن تشغلها صورة كبيرة على الشاشة بأكملها: 40 كيلو بايت * 100 = 4 ميجابايت! هذا جنون!

دعونا نتحدث عن نظام التقديم.

يجب أن يحتوي كل إطار (بترتيب الرسم):

  • رسومات الخلفية (خلفية الملعب)
  • الرسم البياني المستوى نفسه (المقدمة).
  • العفاريت
  • النص / تراكب العلوي.

على وجه الخصوص ، سوف نقوم بالعمليات التالية بالتتابع:

  1. رسم الخلفية + المقدمة (البلاط)
  2. رسم البلاط الشفاف + العفاريت + الغطاء العلوي
  3. إرسال البيانات عن طريق SPI.

سيتم رسم الخلفية والبلاط كامد بالكامل بواسطة DMA. البلاط غير شفاف بالكامل عبارة عن بلاط لا توجد فيه وحدات بكسل شفافة.


بلاط شفاف جزئيًا (يسار) ومبهّم تمامًا (يمين). في تجانب شفاف جزئيًا ، تكون بعض البكسلات (في أسفل اليسار) شفافة ، وبالتالي تكون الخلفية مرئية من خلال هذه المنطقة.

لا يمكن تقديم البلاط والشفرات والتراكبات الشفافة جزئيًا بشكل فعال بواسطة DMA. في الواقع ، يقوم نظام DMA للرقاقة ATSAMD21 ببساطة بنسخ البيانات ، وعلى عكس Blitter للكمبيوتر في Amiga ، فإنه لا يتحقق من الشفافية (تم تحديدها بواسطة قيمة اللون). يتم رسم كافة العناصر الشفافة جزئيًا بواسطة وحدة المعالجة المركزية.


ثم يتم نقل البيانات إلى الشاشة باستخدام DMA.

إنشاء خط أنابيب


كما ترون ، إذا أجرينا هذه العمليات بالتتابع في مخزن مؤقت واحد ، فسوف يستغرق الأمر الكثير من الوقت. في الواقع ، أثناء تشغيل DMA ، لن تكون وحدة المعالجة المركزية مشغولة باستثناء انتظار اكتمال DMA! هذه طريقة سيئة لتنفيذ محرك رسومات. علاوة على ذلك ، عندما ترسل DMA البيانات إلى جهاز SPI ، فإنها لا تستخدم النطاق الترددي الكامل. في الواقع ، حتى عندما تعمل SPI بتردد 24 MHz ، يتم إرسال البيانات فقط بتردد 3 MHz ، وهو صغير جدًا. بمعنى آخر ، لا يتم استخدام DMA إلى أقصى إمكاناته: يمكن DMA أداء مهام أخرى دون فقدان الأداء حقًا.

هذا هو السبب في أننا قمنا بتنفيذ خط الأنابيب ، وهو تطوير فكرة التخزين المؤقت المزدوج (نستخدم ثلاثة مخازن مؤقتة!). بالطبع ، في النهاية ، تتم العمليات دائمًا بالتتابع. لكن وحدة المعالجة المركزية و DMA في وقت واحد أداء مهام مختلفة ، دون (وخاصة) تؤثر على بعضها البعض.

إليك ما يحدث في نفس الوقت:

  • يتم استخدام المخزن المؤقت لرسم بيانات الخلفية باستخدام قناة DMA 1 ؛
  • في وحدة تخزين مؤقتة أخرى (كانت مليئة مسبقًا ببيانات الخلفية) ، تقوم وحدة المعالجة المركزية برسم العفاريت والبلاطات الشفافة جزئيًا ؛
  • ثم يتم استخدام مخزن مؤقت آخر (يحتوي على كتلة بيانات أفقية كاملة) لإرسال البيانات إلى الشاشة عبر SPI باستخدام قناة DMA 0. بالطبع ، كان المخزن المؤقت المستخدم لإرسال البيانات عبر SPI ممتلئًا مسبقًا باستخدام sprites بينما قام SPI بإرسال الكتلة السابقة وأثناء وجود مخزن مؤقت آخر مليئة البلاط.



DMA


لا يشبه نظام DMA للرقاقة ATSAMD21 نظام Blitter ، لكن مع ذلك يتمتع بميزاته المفيدة. بفضل DMA ، يمكننا توفير معدل تحديث مرتفع للغاية ، على الرغم من وجود ملعب مزدوج.

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

يمكن أن تعمل DMA مع بيانات ذات أطوال مختلفة: البايتات والصفارات النصفية (16 بت) والكلمات (32 بت). في المواصفات ، يسمى هذا الطول "حجم الخفقان". بالنسبة إلى SPI ، نحن مضطرون إلى استخدام نقل البايت (على الرغم من أن مواصفات REVD الحالية تنص على أن شرائح ATSAMD21 SERCOM لها FIFO ، والتي ، وفقًا لـ Microchip ، يمكنها قبول بيانات 32 بت ، في الواقع ، يبدو أنها لا تملك FIFO. سجل SERCOM CTRLC ، الغائب في كل من ملفات الرأس وفي قسم وصف السجل. لحسن الحظ ، على عكس AVR ، يحتوي ATSAMD21 على الأقل على سجل بيانات نقل مخزّن ، لذلك لن يكون هناك أي توقف مؤقت في الإرسال!). لرسم البلاط ، نحن ، بالطبع ، نستخدم 32 بت. هذا يتيح لك نسخ بكسل اثنين لكل فوز. تتيح رقاقة ATSAMD21 DMA أيضًا لكل مصدر فوز زيادة عنوان المصدر أو الوجهة بعدد ثابت من أحجام الإيقاع.

هذان الجانبان مهمان للغاية ويحددان طريقة رسم البلاط.

أولاً ، إذا قمنا بتقديم بكسل واحد لكل إيقاع (16 بت) ، فسنخفض إنتاجية نظامنا إلى النصف. لا يمكننا رفض عرض النطاق الترددي الكامل!

ومع ذلك ، إذا قمنا برسم بكسلين لكل إيقاع ، فسيكون حقل اللعبة قادرًا على التمرير فقط لعدد متساوٍ من البكسل ، مما سيتسبب في حركة سلسة. للتعامل مع هذا ، يمكنك استخدام مخزن مؤقت أكبر من بكسلين أو أكثر. عند إرسال البيانات إلى الشاشة ، سنستخدم الإزاحة الصحيحة (0 أو 1 بكسل) ، اعتمادًا على ما إذا كنا بحاجة إلى نقل "الكاميرا" بعدد متساوٍ أو فردي من البكسل.

ومع ذلك ، من أجل البساطة ، فإننا نحتفظ بمساحة 11 تجانبًا كاملًا (160 + 16 بكسل) ، وليس لـ 160 + 2 بكسل. هذا النهج له ميزة واحدة كبيرة: لا يتعين علينا حساب وتحديث عنوان المستلم لكل واصف DMA (قد يتطلب هذا العديد من التعليمات ، مما قد ينتج عنه الكثير من العمليات الحسابية لكل تجانب). بالطبع ، لن نرسم سوى أقل عدد ممكن من وحدات البكسل ، أي ما لا يزيد عن 162. نعم ، في النهاية ، سوف ننفق القليل من الذاكرة الإضافية (مع الأخذ في الاعتبار ثلاثة مخازن مؤقتة ، أي حوالي 1500 بايت) للسرعة والبساطة. يمكنك أيضًا إجراء تحسينات إضافية.


جميع المخازن المؤقتة كتلة 16 سطر (بدون واصفات) مرئية في هذه الرسوم المتحركة GIF. على اليمين هو ما يتم عرضه في الواقع. يتم عرض أول 32 إطارًا في GIF ، حيث نقوم بنقل بكسل واحد إلى اليمين في كل إطار. المنطقة السوداء للمخزن المؤقت هي الجزء الذي لم يتم تحديثه ، ومحتوياته تبقى ببساطة من العمليات السابقة. عندما تقوم الشاشة بالتمرير بعدد فردي من الإطارات ، يتم رسم مساحة 162 بكسل في المخزن المؤقت. ومع ذلك ، يتم تجاهل العمود الأول والأخير منها (والتي يتم تمييزها في الرسوم المتحركة). عندما تكون قيمة التمرير متعددة من 16 بكسل ، تبدأ عمليات السحب في المخزن المؤقت من العمود الأول (x = 0).

ماذا عن التمرير العمودي؟

سنتعامل معها بعد أن نعرض طريقة لتخزين البلاط في ذاكرة الفلاش.

كيفية تخزين البلاط


تتمثل الطريقة الساذجة (التي تناسبنا إذا كنا نقدم فقط من خلال وحدة المعالجة المركزية) في تخزين البلاط في ذاكرة فلاش كتسلسل من ألوان البكسل. بكسل الأول من السطر الأول ، والثاني ، وهلم جرا ، حتى السادس عشر. بعد ذلك ، نحفظ البيكسل الأول من الصف الثاني والثاني وما إلى ذلك.

لماذا هذا القرار ساذج؟ لأنه في هذه الحالة ، يمكن فقط عرض DMA 16 بكسل لكل واصف DMA! لذلك ، نحتاج إلى 16 واصفًا ، يحتاج كل منها إلى 4 + 4 عمليات وصول إلى الذاكرة (أي لنقل 32 بايت - 8 عمليات قراءة للذاكرة + 8 عمليات كتابة للذاكرة - يجب على DMA إجراء 4 قراءات أخرى + 4 كتب). هذا غير فعال جدا!

في الواقع ، بالنسبة لكل واصف ، يمكن DMA فقط زيادة عناوين المصدر والوجهة بعدد ثابت من الكلمات. بعد نسخ السطر الأول من التجانب إلى المخزن المؤقت ، لا ينبغي زيادة عنوان المستلم بكلمة واحدة ، ولكن بهذه القيمة يشير إلى السطر التالي من المخزن المؤقت. هذا غير ممكن لأن كل واصف نقل يشير فقط إلى زيادة انتقال الإرسال ، والتي لا يمكن تغييرها.

سيكون من الأذكى إرسال أول بكسلين من كل سطر من التجانب بالتسلسل ، أي بكسل 0 و 1 من السطر 0 ، بكسل 0 و 1 من السطر 1 ، وما إلى ذلك ، ما يصل إلى بكسل 0 و 1 من السطر 15. ثم نرسل بكسلات 2 و 3 من السطر 0 ، وهلم جرا.


كيف يتم تخزين البلاط؟

في الشكل أعلاه ، يشير كل رقم إلى الترتيب الذي يتم به تخزين بكسل 16 بت في صفيف التجانب.

يمكن القيام بذلك باستخدام واصف ، لكننا نحتاج إلى شيئين:

  • يجب تخزين البلاط بحيث نشير دائمًا إلى مواضع البكسل الصحيحة عند زيادة المصدر بكلمة واحدة. بمعنى آخر ، إذا كانت (r ، c) بكسل في الصف r والعمود c ، فإننا بحاجة إلى حفظ البكسل (0،0) (0،1) (1،0) (1،1) (2،0) بالتتابع (2.1) ... (15.0) (15.1) (0.2) (0.3) (1.2) (1.3) ...
  • يجب أن يكون المخزن المؤقت 256 بكسل (لا 160)

الهدف الأول سهل للغاية: فقط قم بتغيير ترتيب البيانات ، يمكنك القيام بذلك عند تصدير الرسومات إلى ملف c (انظر الصورة أعلاه).

يمكن حل المشكلة الثانية لأن DMA يسمح لك بزيادة عنوان المستلم بعد كل فوز بـ 512 بايت. هذا له نتيجتان:

  • لا يمكننا إرسال البيانات باستخدام واصف واحد عبر كتلة SPI. هذه ليست مشكلة خطيرة للغاية ، لأننا في النهاية نقرأ واصفًا واحدًا من خلال 160 بكسل. سيكون تأثير الأداء ضئيلاً.
  • يجب أن يكون حجم الكتلة 256 * 2 * 16 بايت = 8 كيلوبايت ، وسيكون هناك الكثير من "المساحة غير المستخدمة" فيه.

ومع ذلك ، لا يزال من الممكن استخدام هذه المساحة ، على سبيل المثال ، للواصفات.

في الواقع ، حجم كل واصف 16 بايت. نحن بحاجة إلى ما لا يقل عن 10 * 8 (وفي الواقع 11 * 8!) واصفات للبلاط و 16 واصفات لل SPI.

لهذا السبب كلما زاد عدد البلاط ، زادت السرعة. في الواقع ، إذا استخدمنا ، على سبيل المثال ، بلاط 32 × 32 ، فسنحتاج إلى عدد أقل من الواصفات لكل شاشة (320 بدلاً من 640). هذا من شأنه أن يقلل من هدر الموارد.

عرض كتلة البيانات


يتم تخزين المخزن المؤقت كتلة ، واصفات ، وغيرها من البيانات في نوع هيكل ، والتي أطلقنا عليها displayBlock_t.

displayBlock عبارة عن صفيف مكون من 16 عنصر displayLineData_t. تحتوي بيانات DisplayLine على 176 بكسل بالإضافة إلى 80 كلمة. في هذه الكلمات الثمانين ، نقوم بتخزين واصفات العرض أو غيرها من بيانات العرض المفيدة (باستخدام الاتحاد).



نظرًا لأن لدينا 16 سطرًا ، يستخدم كل بلاطة في الموضع X أول 8 واصفات DMA (0 إلى 7) من خطوط X. نظرًا لأن لدينا 11 بلاطة كحد أقصى (خط العرض بعرض 176 بكسل) ، فإن البلاط لا يستخدم إلا واصفات DMA الأولى 11 صفوف من البيانات. واصفات 8-9 من جميع الأسطر واصفات 0-9 من الخطوط 11-15 أحرار.

من هذه ، سيتم استخدام واصفات 8 و 9 من الأسطر 0..7 لـ SPI.

سيتم استخدام 0-.9 الأسطر 11-15 (حتى 50 واصف ، على الرغم من أننا سوف نستخدم 48 منها فقط) لملعب الخلفية.

يوضح الشكل أدناه هيكلها.


ملعب الخلفية


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

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

قارن بين الخلفية والملاعب الأمامية:

  • في الخلفية ، يتم استخدام القطاعات ، والتي يتم تخزين بلاطات طويلة بطريقة "ساذجة".
  • الخلفية لها خريطتها الخاصة ، لكنها أفقية تتكرر. بفضل هذا ، يتم استخدام ذاكرة أقل.
  • خلفية لديه المنظر لكل قطاع.

الملعب الأمامي


كما قيل ، في كل كتلة لدينا ما يصل إلى 11 بلاطة (10 بلاطات كاملة ، أو 9 بلاطات كاملة وملفان جزئيان). كل من هذه المربعات ، إذا لم يتم تمييزها على أنها شفافة ، يتم رسم DMA. إذا لم يكن كامدًا تمامًا ، فسيتم إضافته إلى القائمة التي سيتم تحليلها لاحقًا عند تقديم العفاريت.

نحن نربط معا اثنين من الملاعب


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

العفاريت والبلاط مع الشفافية


تتم معالجة البلاط مع الشفافية والعفاريت نفسه تقريبا.يتم تنفيذ بلاط / العفريت بكسل التحليل. إذا كان لونه أسود ، فسيكون شفافًا ، وبالتالي لا يتغير بلاط الخلفية. إذا لم يكن أسودًا ، فسيتم استبدال بكسل الخلفية في صورة بكسل / تجانب.

التمرير العمودي


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

عند العمل باستخدام التمرير العمودي ، نحتاج إلى حساب كل من سجل المتلقي وحجم الإرسال. يجب ضبطها عدة مرات لكل إطار. لتجنب هذه الضجة ، يمكننا ببساطة رسم ما يصل إلى 9 كتل كاملة لكل إطار (8 إذا كان التمرير هو مضاعفات 16).

معدات


كما قلنا ، فإن قلب النظام هو uChip. ماذا عن البقية؟

هنا هو مخطط! بعض جوانب ذلك تستحق الذكر.


مفاتيح


لتحسين استخدام I / O ، نستخدم خدعة صغيرة. سيكون لدينا 4 حافلات استشعار L1-L4 ، وسلك مشترك واحد LC. يتم تطبيق 1 و 0 بالتناوب على السلك الشائع ، وبناءً على ذلك ، سيتم سحب حافلات المستشعر بالتناوب لأسفل أو لأعلى بمساعدة مقاومات سحب داخلية. يتم توصيل مفتاحين بين كل من الحافلات الرئيسية والحافلة المشتركة. يتم إدخال الصمام الثنائي في سلسلة مع هذين المفتاحين. يتم تبديل كل من هذه الثنائيات في الاتجاه المعاكس ، بحيث في كل مرة يتم فيها قراءة مفتاح واحد فقط.

نظرًا لعدم وجود وحدة تحكم مدمجة في لوحة المفاتيح (ولا توجد وحدة تحكم مدمجة في لوحة المفاتيح تستخدم هذه الطريقة المثيرة للاهتمام) ، يتم استقصاء ثمانية مفاتيح بسرعة في بداية كل إطار. نظرًا لأنه يجب سحب المدخلات لأعلى ولأسفل ، لا يمكننا (ولا نريد) استخدام مقاومات خارجية ، لذلك نحن بحاجة إلى استخدام مدخلات متكاملة ، والتي يمكن أن تتمتع بمقاومة عالية إلى حد ما (60 كيلو أوم). هذا يعني أنه عندما تتغير حالة الناقل العام ، وتغير ناقل البيانات حالة السحب لأعلى / لأسفل ، فإنك تحتاج إلى إدخال بعض التأخير حتى يغير المقاوم المنسدل لأعلى / لأسفل العقد ويضبط السعة الشاردة على المستوى المطلوب. لكننا لا نريد الانتظار! لذلك ، نضع الناقل المشترك في حالة مقاومة عالية (بحيث لا يوجد خلاف) ، ونقوم أولاً بتغيير ناقل أجهزة الاستشعار إلى القيم المنطقية 1 أو 0 ،تكوين مؤقت لهم كما الإخراج. في وقت لاحق يتم تكوينها كمدخلات عن طريق السحب لأعلى أو لأسفل. نظرًا لأن مقاومة الخرج في حدود عشرات أوم ، فإن الحالة تتغير في عدد قليل من النانوثانية ، أي عندما تعود حافلة المستشعر إلى المدخلات ، ستكون في الحالة المطلوبة بالفعل. بعد ذلك ، تنتقل الحافلة العامة إلى الإخراج مع قطبية معاكسة.

هذا يحسن سرعة المسح بشكل كبير ويزيل الحاجة إلى تأخير / تعليمات nop.

اتصال SPI


قمنا بتوصيل SD والشاشة بحيث يتواصلوا مع بعضهم البعض دون نقل البيانات إلى ATSAMD21. قد يكون ذلك مفيدًا إذا كنت تريد تشغيل الفيديو.

يجب أن تكون المقاومات التي تربط MISO و MOSI منخفضة. إذا كانت كبيرة جدًا ، فلن تعمل SPI ، لأن الإشارة ستكون ضعيفة جدًا.

التحسين والمزيد من التطوير


واحدة من أكبر المشاكل هي استخدام ذاكرة الوصول العشوائي. تشغل ثلاث كتل 8 كيلوبايت لكل منها ، تاركة فقط 8 كيلوبايت لكل رصة ومتغيرات أخرى. في الوقت الحالي ، لدينا 1.3 كيلوبايت فقط من ذاكرة الوصول العشوائي المجانية و 4 كيلوبايت من المكدس (4 كيلوبايت لكل مكدس - وهذا كثير ، وربما سنقوم بتقليله).

ومع ذلك ، يمكنك استخدام كتل بارتفاع لا 16 ، ولكن 8 بكسل. سيؤدي ذلك إلى زيادة هدر الموارد على واصفات DMA ، ولكن تقريبًا يقلل مقدار الذاكرة التي يشغلها المخزن المؤقت للكتل إلى النصف (لاحظ أن عدد الواصفات لن يتغير إذا واصلنا استخدام المربعات 16 × 16 ، لذا سيتعين علينا تغيير بنية الكتلة). يمكن أن يوفر هذا حوالي 7.5 كيلوبايت من ذاكرة الوصول العشوائي ، والتي ستكون مفيدة للغاية لتنفيذ وظائف مثل بطاقة قابلة للتعديل مع أسرار أو إضافة صوت (على الرغم من أنه يمكن إضافة الصوت حتى مع 1 كيلو بايت من ذاكرة الوصول العشوائي).

هناك مشكلة أخرى هي العفريت ، لكن هذا التعديل أبسط من ذي قبل ، وتحتاج فقط إلى الدالة createNextFrameScene () لذلك. في الواقع ، نحن في ذاكرة الوصول العشوائي مجموعة ضخمة مع حالة جميع العفاريت. بعد ذلك ، لكل عفريت ، نحسب ما إذا كان موضعها داخل منطقة الشاشة ، ثم ننشطه ونضيفه إلى قائمة التجسيد.

بدلاً من ذلك ، يمكنك إجراء التحسين. على سبيل المثال ، في gameMap ، يمكنك تخزين ليس فقط قيمة التجانب ، ولكن أيضًا علامة تشير إلى شفافية التجانب ، المعينة في المحرر. سيتيح لنا ذلك التحقق بسرعة مما إذا كان يجب تقديم التجانب: DMA أو CPU. لهذا السبب استخدمنا سجلات 16 بت لبطاقة التجانب. إذا افترضنا أن لدينا مجموعة من 256 بلاطة (في الوقت الحالي لدينا أقل من 128 بلاطة ، ولكن هناك مساحة كافية على ذاكرة الفلاش لإضافة أخرى جديدة) ، فهناك 7 بتات مجانية يمكن استخدامها لأغراض أخرى. يمكن استخدام ثلاثة من هذه البتات السبعة للإشارة إلى ما إذا كان يتم تخزين sprite / كائن. على سبيل المثال:

0b000 =
0b001 =
0b010 =
0b011 =
0b100 =
0b101 =
0b110 =
0b111 = , , .


بعد ذلك ، يمكنك إنشاء جدول بت في ذاكرة الوصول العشوائي يعني فيه كل بت ما إذا كان (على سبيل المثال ، عدوًا) قد تم اكتشافه / ما إذا كان (على سبيل المثال ، مكافأة) يتم التقاطها / ما إذا كان قد تم تنشيط كائن معين (التبديل). على مستوى 10 × 10 شاشات ، سيتطلب ذلك 8000 بت ، أي 1 كيلوبايت من ذاكرة الوصول العشوائي. تتم إعادة تعيين البت عندما يتم اكتشاف عدو أو يتم الحصول على المكافأة.

في createNextFrameScene () ، يجب علينا التحقق من البتات المقابلة للبلاط في المنطقة المرئية الحالية. إذا كانت لديهم قيمة 1:

  • إذا كانت هذه مكافأة ، فقم فقط بإضافتها إلى قائمة العفاريت لتقديمها.
  • إذا كان هذا عدوًا ، فقم بإنشاء شبح ديناميكي وإعادة تعيين العلم. في الإطار التالي ، سيحتوي المشهد على شبح ديناميكي حتى يغادر العدو الشاشة أو يقتل.

هذا النهج له عيوب.

  1. -, ( ). .
  2. -, 80 , , . , 32 . , «/» ( «», .. 0!). «», «» ( ).
  3. -, . ( ), . , .
  4. -, , , , . , , . , , , , !
  5. , (, Unreal Tournament , ).

ومع ذلك ، وبهذه الطريقة يمكننا تخزين ومعالجة العفاريت على مستوى أكثر كفاءة بكثير.

ومع ذلك ، تعتبر هذه التقنية أكثر صلة بـ "منطق اللعبة" أكثر من محرك رسومات اللعبة.

ربما في المستقبل سوف ننفذ هذه الوظيفة.

لتلخيص


نأمل أن تستمتعوا بهذا المقال التمهيدي. نحتاج إلى شرح العديد من الجوانب التي ستكون موضوعات المقالات المستقبلية.

في غضون ذلك ، يمكنك تنزيل شفرة المصدر الكاملة للعبة! إذا كنت ترغب في ذلك ، فيمكنك دعم الفنان ansimuz ماليًا ، الذي رسم كل الرسومات وقدمها للعالم مجانًا. نحن نقبل أيضا التبرعات .

لم تنته اللعبة بعد. نريد إضافة صوت ، والعديد من المستويات ، والأشياء التي يمكنك التفاعل وما شابه ذلك. يمكنك إنشاء التعديلات الخاصة بك! نأمل أن نرى ألعاب جديدة مع رسومات ومستويات جديدة!

سنصدر قريباً محرر خرائط ، لكن في الوقت الحالي ، من البدائي جدًا عرضه على المجتمع!

فيديو


(ملاحظة: نظرًا لضعف الإضاءة ، تم تسجيل الفيديو بمعدل إطار أقل كثيرًا! سنقوم قريبًا بتحديث الفيديو حتى تتمكن من تقدير السرعة القصوى عند 40 إطارًا في الثانية!)


شكر


يتم الحصول على رسومات اللعبة (والبلاط الموضح في بعض الصور) من الأصل المجاني "Sunny Land" الذي أنشأه ansimuz .

المواد القابلة للتحميل


شفرة المصدر للمشروع هي في المجال العام ، وهذا هو ، يتم توفيره مجانا. إننا نشاركه على أمل أن يكون مفيدًا لشخص ما. نحن لا نضمن أنه بسبب أي خطأ / خطأ في الكود لن تكون هناك مشاكل!

مخطط تخطيطي مشروع

KiCad

Atmel Studio 7 (المصدر)

Source: https://habr.com/ru/post/ar466323/


All Articles