برمجة لعبة لجهاز مضمن على ESP32

الجزء 0: الدافع


المقدمة


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

يذهب Odroid


كان لدي Odroid Go مستلقيا ، وهو أمر مثير للاهتمام للعب معه. جوهرها هو ESP32 - متحكم شائع جدًا مع وظائف MK القياسية (SPI ، I2C ، GPIO ، المؤقتات ، وما إلى ذلك) ، ولكن أيضًا مع WiFi و Bluetooth ، مما يجعلها جذابة لإنشاء أجهزة إنترنت الأشياء.

تكمل Odroid Go ESP32 مع مجموعة من الأجهزة الطرفية ، وتحويلها إلى آلة ألعاب محمولة تشبه Gameboy Color: شاشة LCD ، ومكبر صوت ، وصليب تحكم ، وزرين أساسيين وأربعة أزرار مساعدة ، وبطارية وقارئ بطاقة SD.

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


محددات


الدقة 320 × 240

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

ألوان 16 بت

تدعم الشاشة ألوان 16 بت لكل بكسل: 5 بت للون الأحمر و 6 بت للأخضر و 5 للون الأزرق. لأسباب واضحة ، تسمى هذه الدائرة عادة RGB565. حصل اللون الأخضر على اللون الأحمر والأزرق أكثر قليلاً ، لأن العين البشرية تميز بشكل أفضل بين تدرجات اللون الأخضر من اللون الأزرق أو الأحمر.

لون 16 بت يعني أنه لا يمكننا الوصول إلا إلى 65 ألف لون فقط. قارن هذا مع اللون القياسي 24 بت (8 بت لكل لون) ، مما يوفر 16 مليون لون.

عدم وجود GPU

بدون GPU ، لا يمكننا استخدام API مثل OpenGL. اليوم ، عادة ما يتم استخدام وحدات معالجة الرسومات نفسها لتقديم ألعاب ثنائية الأبعاد كما هو الحال مع الألعاب ثلاثية الأبعاد. فقط بدلاً من الكائنات ، يتم رسم الزوايا الرباعية ، حيث يتم تركيب مواد البتات عليها. بدون GPU ، يتعين علينا تنقيط كل بكسل باستخدام وحدة معالجة مركزية ، وهي أبطأ ولكن أبسط.

مع دقة شاشة 320 × 240 ولون 16 بت ، يبلغ إجمالي حجم المخزن المؤقت للإطار 153،600 بايت. هذا يعني أنه على الأقل ثلاثين مرة في الثانية ، سنحتاج إلى إرسال 153،600 بايت إلى الشاشة. يمكن أن يتسبب هذا في نهاية المطاف في حدوث مشاكل ، لذلك نحتاج إلى أن نكون أكثر ذكاءً عند عرض الشاشة. على سبيل المثال ، يمكنك تحويل لون مفهرس إلى لوحة بحيث تحتاج لكل بكسل إلى تخزين بايت واحد ، والذي سيتم استخدامه كمؤشر لوحة ألوان 256.

4 ميجا بايت

ESP32 يحتوي على 520 كيلو بايت من ذاكرة الوصول العشوائي الداخلية ، بينما يضيف Odroid Go 4 ميجا بايت أخرى من ذاكرة الوصول العشوائي الخارجية. ولكن ليست كل هذه الذاكرة متاحة لنا ، لأن الجزء يستخدم من قبل ESP32 SDK (المزيد عن هذا لاحقًا). بعد تعطيل جميع الوظائف الدخيلة المحتملة وإدخال وظيفتي الرئيسية ، يبلغ ESP32 أنه يمكننا استخدام 4،494،848 بايت. إذا احتجنا في المستقبل إلى مزيد من الذاكرة ، فيمكننا فيما بعد العودة إلى تقليم الوظائف غير الضرورية.

معالج 80-240 ميجا هرتز

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

تصحيح سيئ

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

لماذا كل المشاكل؟


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

القيود تحفز الإبداع ،

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

التنمية منخفضة المستوى أمر ممتع

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

إلى أي مدى سيكون التطوير منخفضًا؟


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

لذا ، سوف نستخدم ESP-IDF ، وهو في الأساس SDK لـ ESP32. يمكننا أن نفترض أنه يوفر لنا بعض الأدوات المساعدة التي يوفرها نظام التشغيل عادةً ، ولكن نظام التشغيل لا يعمل في ESP32 . بالمعنى الدقيق للكلمة ، يستخدم هذا MK FreeRTOS ، وهو نظام تشغيل في الوقت الفعليلكن هذا ليس نظام تشغيل حقيقي. هذا مجرد مخطط. على الأرجح ، لن نتفاعل معه ، ولكن في جوهره ESP-IDF يستخدمه.

يوفر لنا ESP-IDF واجهة برمجة تطبيقات للأجهزة الطرفية ESP32 مثل SPI و I2C و UART ، بالإضافة إلى مكتبة وقت التشغيل C ، لذلك عندما نسمي شيئًا مثل printf ، فإنه في الواقع ينقل وحدات البايت عبر UART ليتم عرضها على شاشة الواجهة التسلسلية. كما أنه يعالج جميع رموز بدء التشغيل اللازمة لإعداد الجهاز قبل أن يستدعي نقطة بدء لعبتنا.

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

الجزء الأول: بناء النظام


المقدمة


قبل أن نبدأ في كتابة كود لـ Odroid Go ، نحتاج إلى تكوين ESP32 SDK. يحتوي على الرمز الذي يبدأ ESP32 ويستدعي وظيفتنا الرئيسية ، بالإضافة إلى الرمز المحيطي (على سبيل المثال ، SPI) الذي سنحتاجه عند كتابة برنامج تشغيل LCD.

تقوم Espressif باستدعاء ESP-IDF SDK ؛ نستخدم أحدث إصدار ثابت v4.0 .

يمكننا إما استنساخ المستودع وفقًا لتعليماتهم (مع علامة العودية ) ، أو ببساطة تنزيل ملف zip من صفحة الإصدارات.

هدفنا الأول هو تطبيق بسيط على غرار Hello World مثبت على Odroid Go يثبت الإعداد الصحيح لبيئة البناء.

C أو C ++


يستخدم ESP-IDF C99 ، لذلك سنختارها أيضًا. إذا رغبت في ذلك ، يمكننا استخدام C ++ (في سلسلة أدوات ESP32 يوجد مترجم C ++) ، ولكن في الوقت الحالي سنلتزم بـ C. في

الواقع ، أحب C وبساطته. بغض النظر عن مدى كتابة الرمز في C ++ ، لم أتمكن أبدًا من الوصول إلى لحظة الاستمتاع به.

يلخص هذا الشخص أفكاري بشكل جيد.

بالإضافة إلى ذلك ، إذا لزم الأمر ، يمكننا التبديل إلى C ++ في أي وقت.

مشروع الحد الأدنى


يستخدم الجيش الإسرائيلي CMake لإدارة نظام البناء. كما أنها تدعم Makefile ، ولكن تم إيقافها في الإصدار 4.0 ، لذلك سنستخدم CMake فقط.

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

يحتاج CMake إلى الإشارة إلى متغيرات البيئة التي تخبره بمكان البحث عن IDF وسلسلة الأدوات. لقد انزعجت من وجوب إعادة تثبيتها في كل مرة بدأت فيها جلسة عمل طرفية جديدة ، لذا كتبت البرنامج النصي export.sh . يقوم بتعيين IDF_PATH و IDF_TOOLS_PATH، وهو أيضًا مصدر تصدير لجيش الدفاع الإسرائيلي يحدد متغيرات البيئة الأخرى.

يكفي أن يقوم مستخدم النص البرمجي بتعيين المتغيرين IDF_PATH و IDF_TOOLS_PATH .

IDF_PATH=
IDF_TOOLS_PATH=


if [ -z "$IDF_PATH" ]
then
	echo "IDF_PATH not set"
	return
fi

if [ -z "$IDF_TOOLS_PATH" ]
then
	echo "IDF_TOOLS_PATH not set"
	return
fi


export IDF_PATH
export IDF_TOOLS_PATH

source $IDF_PATH/export.sh

CMakeLists.txt في الجذر:

cmake_minimum_required(VERSION 3.5)

set(COMPONENTS "esptool_py main")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

project(game)

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

CMakeLists.txt داخل الرئيسي :

idf_component_register(
	SRCS "main.c"
    INCLUDE_DIRS "")

كل ما يفعله - بشكل لا نهائي مرة واحدة في الثانية تعرض على الشاشة الواجهة التسلسلية "Hello World". يستخدم VTaskDelay FreeRTOS للتأخير .

ملف main.c بسيط للغاية:

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>


void app_main(void)
{
	for (;;)
	{
		printf("Hello World!\n");
		vTaskDelay(1000 / portTICK_PERIOD_MS);
	}

	// Should never get here
	esp_restart();
}

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

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

يمكننا استخدام كلا المركزين ، ولكن في الوقت الحالي ، فلنقتصر على أحدهما.

مكونات


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

app_trace app_update bootloader bootloader_support cxx driver efuse esp32 esp_common esp_eth esp_event esp_ringbuf
esp_rom esp_wifi espcoredump esptool_py freertos heap log lwip main mbedtls newlib nvs_flash partition_table pthread
soc spi_flash tcpip_adapter vfs wpa_supplicant xtensa

العديد منها منطقي تمامًا ( bootloader ، esp32 ، freertos ) ، ولكن يتبعها مكونات غير ضرورية لأننا لا نستخدم وظائف الشبكة: esp_eth ، esp_wifi ، lwip ، mbedtls ، tcpip_adapter ، wpa_supplicant . لسوء الحظ ، ما زلنا مجبرين على تجميع هذه المكونات.

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

Total sizes:
 DRAM .data size:    8476 bytes
 DRAM .bss  size:    4144 bytes
Used static DRAM:   12620 bytes ( 168116 available, 7.0% used)
Used static IRAM:   56345 bytes (  74727 available, 43.0% used)
      Flash code:   95710 bytes
    Flash rodata:   40732 bytes
Total image size:~ 201263 bytes (.bin may be padded larger)
Per-archive contributions to ELF file:
            Archive File DRAM .data & .bss   IRAM Flash code & rodata   Total
                  libc.a        364      8   5975      63037     3833   73217
              libesp32.a       2110    151  15236      15415    21485   54397
           libfreertos.a       4148    776  14269          0     1972   21165
                libsoc.a        184      4   7909        875     4144   13116
          libspi_flash.a        714    294   5069       1320     1386    8783
                libvfs.a        308     48      0       5860      973    7189
         libesp_common.a         16   2240    521       1199     3060    7036
             libdriver.a         87     32      0       4335     2200    6654
               libheap.a        317      8   3150       1218      748    5441
             libnewlib.a        152    272    869        908       99    2300
        libesp_ringbuf.a          0      0    906          0      163    1069
                liblog.a          8    268    488         98        0     862
         libapp_update.a          0      4    127        159      486     776
 libbootloader_support.a          0      0      0        634        0     634
                libhal.a          0      0    519          0       32     551
            libpthread.a          8     12      0        288        0     308
             libxtensa.a          0      0    220          0        0     220
                libgcc.a          0      0      0          0      160     160
               libmain.a          0      0      0         22       13      35
                libcxx.a          0      0      0         11        0      11
                   (exe)          0      0      0          0        0       0
              libefuse.a          0      0      0          0        0       0
         libmbedcrypto.a          0      0      0          0        0       0
     libwpa_supplicant.a          0      0      0          0        0       0

الأهم من ذلك كله ، يؤثر libc على حجم الثنائي ، ولا بأس بذلك.

تكوين المشروع


يسمح لك IDF بتحديد معلمات تكوين وقت الترجمة التي يستخدمها أثناء التجميع لتمكين الوظائف المختلفة أو تعطيلها. نحتاج إلى تعيين معلمات تسمح لنا بالاستفادة من الجوانب الإضافية لـ Odroid Go.

أولاً ، تحتاج إلى تشغيل البرنامج النصي المصدر لـ export.sh حتى يتمكن CMake من الوصول إلى متغيرات البيئة الضرورية. علاوة على ذلك ، بالنسبة لجميع مشاريع CMake ، نحتاج إلى إنشاء مجلد تجميع واستدعاء CMake منه.

source export.sh
mkdir build
cd build
cmake ..

إذا قمت بتشغيل make menuconfig ، تفتح نافذة حيث يمكنك تكوين إعدادات المشروع.

توسيع ذاكرة الفلاش حتى 16 ميجا بايت


يقوم Odroid Go بتوسيع سعة محرك الأقراص المحمول القياسي إلى 16 ميجابايت. يمكنك تمكين هذه الميزة بالانتقال إلى Serial flasher config -> حجم الفلاش -> 16 ميجا بايت .

قم بتشغيل SPI RAM الخارجي


لدينا أيضًا إمكانية الوصول إلى 4 ميغابايت إضافية من ذاكرة الوصول العشوائي الخارجية المتصلة عبر SPI. يمكنك تمكينه بالانتقال إلى مكون التكوين -> خاص بـ ESP32 -> دعم ذاكرة الوصول العشوائي الخارجية المتصلة بـ SPI والضغط على شريط المسافة لتمكينه. نريد أيضًا أن نكون قادرين على تخصيص الذاكرة بشكل صريح من SPI RAM ؛ يمكن تمكين ذلك بالانتقال إلى SPI RAM config -> SPI RAM access method -> جعل RAM قابلة للتخصيص باستخدام heap_caps_malloc .

خفض التردد


يعمل ESP32 بشكل افتراضي بتردد 160 ميجاهرتز ، لكن دعنا نخفضه إلى 80 ميجاهرتز لنرى إلى أي مدى يمكنك الذهاب بأقل تردد ساعة. نريد أن تعمل اللعبة على طاقة البطارية ، وخفض التردد سيوفر الطاقة. يمكنك تغييره بالانتقال إلى مكون التكوين -> خاص بـ ESP32 -> تردد وحدة المعالجة المركزية -> 80 ميجا هرتز .

إذا قمت بتحديد حفظ ، سيتم حفظ ملف sdkconfig في جذر مجلد المشروع . يمكننا كتابة هذا الملف في git ، ولكن يحتوي على الكثير من المعلمات التي ليست مهمة بالنسبة لنا. حتى الآن ، نحن راضون عن المعلمات القياسية ، باستثناء تلك التي قمنا بتغييرها للتو.

يمكنك إنشاء ملف sdkconfig.defaults بدلاً من ذلكوالتي ستحتوي على القيم التي تم تغييرها أعلاه. سيتم تكوين كل شيء آخر بشكل افتراضي. أثناء البناء ، سيقرأ IDF sdkconfig.defaults ، وسيتجاوز القيم التي قمنا بتعيينها ، ويستخدم المعيار لجميع المعلمات الأخرى.

الآن يبدو sdkconfig.defaults مثل هذا:

# Set flash size to 16MB
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

# Set CPU frequency to 80MHz
CONFIG_ESP32_DEFAULT_CPU_FREQ_80=y

# Enable SPI RAM and allocate with heap_caps_malloc()
CONFIG_ESP32_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_USE_CAPS_ALLOC=y

بشكل عام ، يبدو الهيكل الأصلي للعبة كما يلي:

game
├── CMakeLists.txt
├── export.sh
├── main
│   ├── CMakeLists.txt
│   └── main.c
└── sdkconfig.defaults

بناء وميض


عملية التجميع والبرامج الثابتة نفسها بسيطة للغاية.

ندير جعل لترجمة (لبناء مواز، إضافة -j4 أو -j8جعل فلاش لإرسال الصورة إلى Odroid العودة، و جعل شاشة لعرض الإخراج من printf البيانات .

make
make flash
make monitor

يمكننا أيضًا تنفيذها في سطر واحد.

make flash monitor

والنتيجة ليست مثيرة للإعجاب بشكل خاص ، لكنها ستصبح الأساس لبقية المشروع.


المراجع



الجزء 2: المدخلات


المقدمة


يجب أن نكون قادرين على قراءة الأزرار التي ضغط عليها اللاعب والصليب على Odroid Go.

أزرار



GPIO


يحتوي Odroid Go على ستة أزرار: A و B و Select و Start و Menu و Volume .

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

تحتاج أولاً إلى تكوين جهات الاتصال كمدخلات ، وبعد ذلك يمكننا قراءة حالتها. تحتوي جهات الاتصال الداخلية على واحدة من جهدين (3.3 فولت أو 0 فولت) ، ولكن عند قراءتها باستخدام وظيفة IDF ، يتم تحويلها إلى قيم صحيحة.

التهيئة


العناصر المميزة بعلامة SW في الرسم البياني هي الأزرار المادية نفسها. عند عدم الضغط ، يتم توصيل جهات اتصال ESP32 ( IO13 ، IO0 ، إلخ) بـ 3.3 فولت ؛ أي 3.3 V يعني أن الزر غير مضغوط . المنطق هنا هو عكس ما هو متوقع.

يحتوي IO0 و IO39 على مقاومات مادية على اللوحة. إذا لم يتم الضغط على الزر ، فإن المقاوم يسحب جهات الاتصال إلى جهد عالي. إذا تم الضغط على الزر، ثم التيار

المتدفق من خلال اتصالات يذهب إلى أرض الواقع بدلا من ذلك، وبالتالي فإن الجهد 0 سيتم قراءة من الأسماء. IO13 ، IO27 ، IO32 و IO33لا تحتوي على مقاومات ، لأن جهة الاتصال الموجودة على ESP32 تحتوي على مقاومات داخلية ، قمنا بتكوينها لوضع السحب.

بمعرفة ذلك ، يمكننا تكوين ستة أزرار باستخدام GPIO API.

const gpio_num_t BUTTON_PIN_A = GPIO_NUM_32;
const gpio_num_t BUTTON_PIN_B = GPIO_NUM_33;
const gpio_num_t BUTTON_PIN_START = GPIO_NUM_39;
const gpio_num_t BUTTON_PIN_SELECT = GPIO_NUM_27;
const gpio_num_t BUTTON_PIN_VOLUME = GPIO_NUM_0;
const gpio_num_t BUTTON_PIN_MENU = GPIO_NUM_13;

gpio_config_t gpioConfig = {};

gpioConfig.mode = GPIO_MODE_INPUT;
gpioConfig.pull_up_en = GPIO_PULLUP_ENABLE;
gpioConfig.pin_bit_mask =
	  (1ULL << BUTTON_PIN_A)
	| (1ULL << BUTTON_PIN_B)
	| (1ULL << BUTTON_PIN_START)
	| (1ULL << BUTTON_PIN_SELECT)
	| (1ULL << BUTTON_PIN_VOLUME)
	| (1ULL << BUTTON_PIN_MENU);

ESP_ERROR_CHECK(gpio_config(&gpioConfig));

تتطابق الثوابت المحددة في بداية الكود مع كل من جهات اتصال الدائرة. نستخدم بنية gpio_config_t لتكوين كل من الأزرار الستة كمدخل سحب. في حالة IO13 و IO27 و IO32 و IO33 ، نحتاج أن نطلب من جيش الدفاع الإسرائيلي تشغيل مقاومات السحب هذه. بالنسبة إلى IO0 و IO39 ، لا نحتاج إلى القيام بذلك لأنهما يمتلكان مقاومات مادية ، ولكننا سنفعل ذلك على أي حال لجعل التكوين جميلًا.

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

أزرار القراءة


لذا ، قمنا بتكوين جميع جهات الاتصال ، ويمكننا في النهاية قراءة القيم.

تتم قراءة أزرار الأرقام بواسطة وظيفة gpio_get_level ، لكننا نحتاج إلى عكس القيم المستلمة ، لأن جهات الاتصال يتم سحبها لأعلى ، أي أن الإشارة العالية تعني بالفعل "لم يتم الضغط" ، بينما تعني الإشارة المنخفضة "تم الضغط". يحافظ عكس المنطق المعتاد: 1 يعني "مضغوط" ، 0 - "غير مضغوط".

int a = !gpio_get_level(BUTTON_PIN_A);
int b = !gpio_get_level(BUTTON_PIN_B);
int select = !gpio_get_level(BUTTON_PIN_SELECT);
int start = !gpio_get_level(BUTTON_PIN_START);
int menu = !gpio_get_level(BUTTON_PIN_MENU);
int volume = !gpio_get_level(BUTTON_PIN_VOLUME);

Crosspiece (D-pad)



ADC


توصيل الصليب يختلف عن توصيل الأزرار. يتم توصيل الزرين لأعلى ولأسفل بمسمار واحد من محول تناظري إلى رقمي (ADC) ، ويتم توصيل الأزرار اليسرى واليمنى بدبوس ADC آخر.

على عكس جهات الاتصال الرقمية GPIO ، التي يمكننا من خلالها قراءة إحدى حالتين (عالية أو منخفضة) ، فإن ADC يحول الجهد التناظري المستمر (على سبيل المثال ، من 0 فولت إلى 3.3 فولت) إلى قيمة رقمية منفصلة (على سبيل المثال ، من 0 إلى 4095 )

أفترض أن مصممي Odroid Go فعلوا ذلك لتوفير دبابيس GPIO (تحتاج فقط إلى دبابيس تناظرية بدلاً من أربعة دبابيس رقمية). ومع ذلك ، فإن هذا يعقد بشكل طفيف التكوين والقراءة من جهات الاتصال هذه.

ترتيب


يتم توصيل جهة الاتصال IO35 بالمحور Y للعنكبوت ، ويتم توصيل جهة الاتصال IO34 بالمحور X للعنكبوت . نرى أن مفاصل الصليب أكثر تعقيدًا من أزرار الأرقام. يحتوي كل محور على مفتاحين ( SW1 و SW2 للمحور Y ، SW3 و SW4 للمحور X) ، يرتبط كل منهما بمجموعة من المقاومات ( R2 ، R3 ، R4 ، R5 ).

إذا لم يتم الضغط على "لأعلى" أو "لأسفل" ، يتم سحب دبوس IO35 إلى الأرض عبر R3 ، ونحن نعتبر القيمة 0 V. إذا لم يتم الضغط على "اليسار" أو "الأيمن" ، فاتصل بـ IO34يسحب إلى الأرض من خلال R5 ، ونحسب القيمة إلى 0 V.

إذا تم الضغط على SW1 ("لأعلى") ، فإننا مع IO35 نحسب 3.3 V. إذا تم الضغط على SW2 ("لأسفل") ، فإننا مع IO35 نحسب حوالي 1 ، 65 فولت ، لأن نصف الجهد سينخفض ​​على المقاوم R2 .

إذا تم الضغط على SW3 ("يسار") ، فمع IO34 نحسب 3.3 فولت. إذا تم الضغط على SW4 ("يمين") ، فإننا مع IO34 نحسب أيضًا حوالي 1.65 فولت ، لأن نصف الجهد سينخفض ​​على المقاوم R4 .

كلتا الحالتين أمثلة على فواصل الجهد.. عندما يكون للمقاومين في مقسم الجهد نفس المقاومة (في حالتنا - 100 كلفن) ، فإن انخفاض الجهد سيكون نصف جهد الدخل.

بمعرفة هذا ، يمكننا تكوين المقطع العرضي:

const adc1_channel_t DPAD_PIN_X_AXIS = ADC1_GPIO34_CHANNEL;
const adc1_channel_t DPAD_PIN_Y_AXIS = ADC1_GPIO35_CHANNEL;

ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_12));
ESP_ERROR_CHECK(adc1_config_channel_atten(DPAD_PIN_X_AXIS,ADC_ATTEN_DB_11));
ESP_ERROR_CHECK(adc1_config_channel_atten(DPAD_PIN_Y_AXIS,ADC_ATTEN_DB_11));

قمنا بتعيين ADC إلى 12 بتًا بحيث تتم قراءة 0 فولت على شكل 0 ، و 3.3 فولت على 4095 (2 ^ 12). تشير تقارير التوهين إلى أننا لا نحتاج إلى توهين الإشارة حتى نحصل على نطاق الجهد الكامل من 0 فولت إلى 3.3 فولت.

عند 12 بت ، يمكننا أن نتوقع أنه إذا لم يتم الضغط على أي شيء ، فسيتم قراءة 0 ، عند الضغط لأعلى وإلى اليسار - 4096 ، و سيتم قراءة حوالي 2048 تقريبًا عند الضغط لأسفل وإلى اليمين (لأن المقاومات تقلل الجهد إلى النصف).

قراءة متقاطعة


قراءة الصليب أكثر صعوبة من الأزرار ، لأننا بحاجة إلى قراءة القيم الأولية (من 0 إلى 4095) وتفسيرها.

const uint32_t ADC_POSITIVE_LEVEL = 3072;
const uint32_t ADC_NEGATIVE_LEVEL = 1024;

uint32_t dpadX = adc1_get_raw(DPAD_PIN_X_AXIS);

if (dpadX > ADC_POSITIVE_LEVEL)
{
	// Left pressed
}
else if (dpadX > ADC_NEGATIVE_LEVEL)
{
	// Right pressed
}


uint32_t dpadY = adc1_get_raw(DPAD_PIN_Y_AXIS);

if (dpadY > ADC_POSITIVE_LEVEL)
{
	// Up pressed
}
else if (dpadY > ADC_NEGATIVE_LEVEL)
{
	// Down pressed
}

ADC_POSITIVE_LEVEL و ADC_NEGATIVE_LEVEL هي قيم ذات هامش ، مما يضمن أننا نقرأ القيم الصحيحة دائمًا.

تصويت


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

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

typedef struct
{
	uint16_t a : 1;
	uint16_t b : 1;
	uint16_t volume : 1;
	uint16_t menu : 1;
	uint16_t select : 1;
	uint16_t start : 1;
	uint16_t left : 1;
	uint16_t right : 1;
	uint16_t up : 1;
	uint16_t down : 1;
} Odroid_Input;

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

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

يتيح لنا حقل بت بسيط الاستفادة من كلا النهجين: وحدتي بايت من البيانات وحقول مسماة.

تجريبي


الآن يمكننا استطلاع حالة المدخلات داخل الحلقة الرئيسية وعرض النتيجة.

void app_main(void)
{
	Odroid_InitializeInput();

	for (;;)
	{
		Odroid_Input input = Odroid_PollInput();

		printf(
			"\ra: %d  b: %d  start: %d  select: %d  vol: %d  menu: %d  up: %d  down: %d  left: %d  right: %d",
			input.a, input.b, input.start, input.select, input.volume, input.menu,
			input.up, input.down, input.left, input.right);

		fflush(stdout);

		vTaskDelay(250 / portTICK_PERIOD_MS);
	}

	// Should never get here
	esp_restart();
}

تستخدم وظيفة printf \ r للكتابة فوق السطر السابق بدلاً من إضافة سطر جديد. مطلوب fflush لعرض خط ، لأنه في الحالة العادية يتم إعادة تعيينه بواسطة حرف السطر الجديد \ n .


المراجع



الجزء 3: العرض


المقدمة


يجب أن نكون قادرين على عرض وحدات البكسل على Odroid Go LCD.

سيكون عرض الألوان على الشاشة أكثر صعوبة من قراءة حالة الإدخال لأن شاشة LCD بها أدمغة. يتم التحكم في الشاشة بواسطة ILI9341 - برنامج تشغيل TFT LCD شائع جدًا على شريحة واحدة.

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

SPI


يتم توصيل شاشة LCD بـ ESP32 عبر SPI (الواجهة الطرفية التسلسلية) .

SPI هو بروتوكول قياسي يستخدم لتبادل البيانات بين الأجهزة الموجودة على لوحة الدوائر المطبوعة. يحتوي على أربع إشارات: MOSI (Master Out Slave In) ، MISO (Master In Slave Out) ، SCK (Clock) و CS (Chip Select) .

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

سيكون ESP32 سيد SPI (سيد) ، وستكون LCD هي عبد SPI الرقيق. نحن بحاجة إلى تكوين ناقل SPI بالمعلمات المطلوبة وإضافة شاشة LCD إلى الناقل عن طريق تكوين جهات الاتصال المقابلة.



إن الأسماء VSPI.XXXX هي مجرد تسميات لجهات الاتصال في الرسم التخطيطي ، ولكن يمكننا استعراض جهات الاتصال نفسها من خلال النظر إلى أجزاء مخططات LCD و ESP32.

  • MOSI -> VSPI.MOSI -> IO23
  • MISO -> VSPI.MISO -> IO19
  • SCK -> VSPI.SCK -> IO18
  • CS0 -> VSPI.CS0 -> IO5

لدينا أيضًا IO14 ، وهو دبوس GPIO المستخدم لتشغيل الإضاءة الخلفية ، وأيضًا IO21 ، المتصل بدبوس DC لشاشة LCD. تتحكم جهة الاتصال هذه في نوع المعلومات التي نرسلها إلى الشاشة.

أولاً ، قم بتكوين ناقل SPI.

const gpio_num_t LCD_PIN_MISO = GPIO_NUM_19;
const gpio_num_t LCD_PIN_MOSI = GPIO_NUM_23;
const gpio_num_t LCD_PIN_SCLK = GPIO_NUM_18;
const gpio_num_t LCD_PIN_CS = GPIO_NUM_5;
const gpio_num_t LCD_PIN_DC = GPIO_NUM_21;
const gpio_num_t LCD_PIN_BACKLIGHT = GPIO_NUM_14;
const int LCD_WIDTH = 320;
const int LCD_HEIGHT = 240;
const int LCD_DEPTH = 2;


spi_bus_config_t spiBusConfig = {};
spiBusConfig.miso_io_num = LCD_PIN_MISO;
spiBusConfig.mosi_io_num = LCD_PIN_MOSI;
spiBusConfig.sclk_io_num = LCD_PIN_SCLK;
spiBusConfig.quadwp_io_num = -1; // Unused
spiBusConfig.quadhd_io_num = -1; // Unused
spiBusConfig.max_transfer_sz = LCD_WIDTH * LCD_HEIGHT * LCD_DEPTH;

ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &spiBusConfig, 1));

نقوم بتكوين الحافلة باستخدام spi_bus_config_t . من الضروري توصيل جهات الاتصال التي نستخدمها والحد الأقصى لحجم نقل البيانات.

في الوقت الحالي ، سنقوم بإجراء إرسال SPI واحد لجميع بيانات المخزن المؤقت للإطارات ، وهو ما يساوي عرض LCD (بالبكسل) مضروبًا في ارتفاعه (بالبكسل) مضروبًا في عدد البايتات لكل بكسل.

يبلغ العرض 320 ، والارتفاع 240 ، وعمق اللون 2 بايت (تتوقع الشاشة أن يكون عمق ألوان البكسل 16 بت).

spi_handle_t gSpiHandle;

spi_device_interface_config_t spiDeviceConfig = {};
spiDeviceConfig.clock_speed_hz = SPI_MASTER_FREQ_40M;
spiDeviceConfig.spics_io_num = LCD_PIN_CS;
spiDeviceConfig.queue_size = 1;
spiDeviceConfig.flags = SPI_DEVICE_NO_DUMMY;

ESP_ERROR_CHECK(spi_bus_add_device(VSPI_HOST, &spiDeviceConfig, &gSpiHandle));

بعد تهيئة الناقل ، نحتاج إلى إضافة جهاز LCD إلى الناقل حتى نتمكن من بدء التحدث إليه.

  • clock_speed_hz — - , SPI 40 , . 80 , .
  • spics_io_num — CS, IDF CS, ( SD- SPI).
  • queue_size — 1, ( ).
  • الإشارات - عادةً ما يقوم برنامج تشغيل IDF SPI بإدخال وحدات بت فارغة في الإرسال لتجنب مشاكل التوقيت أثناء القراءة من جهاز SPI ، لكننا نقوم بإجراء نقل باتجاه واحد (لن نقرأ من الشاشة). تفيد تقارير SPI_DEVICE_NO_DUMMY أننا نؤكد هذا الإرسال أحادي الاتجاه ولا نحتاج إلى إدراج وحدات بت فارغة.


gpio_set_direction(LCD_PIN_DC, GPIO_MODE_OUTPUT);
gpio_set_direction(LCD_PIN_BACKLIGHT, GPIO_MODE_OUTPUT);

نحتاج أيضًا إلى تعيين دبابيس DC والإضاءة الخلفية كدبابيس GPIO. بعد تبديل التيار المستمر ، سيتم تشغيل الإضاءة الخلفية باستمرار.

فرق


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

بشكل عام ، يبدو الدفق كما يلي:

  1. نعطي إشارة منخفضة إلى العاصمة
  2. نرسل بايت واحد من الأمر
  3. نعطي إشارة عالية ل DC
  4. إرسال صفر بايت أو أكثر ، حسب متطلبات الأمر
  5. كرر الخطوات من 1-4

هنا أفضل صديق لنا هو مواصفات ILI9341 . يسرد جميع الأوامر الممكنة ، ومعلماتها وكيفية استخدامها.


مثال على أمر بدون معلمات هو عرض ON . بايت الأمر هو 0x29 ، ولكن لم يتم تحديد معلمات له.


مثال لأمر مع معلمات هو مجموعة عناوين الأعمدة . بايت الأمر هو 0x2A ، ولكن تم تحديد أربع معلمات مطلوبة له. لاستخدام الأمر ، تحتاج إلى إرسال إشارة منخفضة إلى DC ، وإرسال 0x2A ، وإرسال إشارة عالية إلى DC ، ثم نقل وحدات البايت من أربعة معلمات.

يتم تحديد رموز الأوامر نفسها في التعداد.

typedef enum
{
	SOFTWARE_RESET = 0x01u,
	SLEEP_OUT = 0x11u,
	DISPLAY_ON = 0x29u,
	COLUMN_ADDRESS_SET = 0x2Au,
	PAGE_ADDRESS_SET = 0x2Bu,
	MEMORY_WRITE = 0x2Cu,
	MEMORY_ACCESS_CONTROL = 0x36u,
	PIXEL_FORMAT_SET = 0x3Au,
} CommandCode;

بدلاً من ذلك ، يمكننا استخدام ماكرو ( #define SOFTWARE_RESET (0x01u) ) ، لكن ليس لديهم رموز في مصحح الأخطاء وليس لديهم نطاق. سيكون من الممكن أيضًا استخدام الثوابت الثابتة الصحيحة ، كما فعلنا مع جهات اتصال GPIO ، ولكن بفضل التعداد ، يمكننا في لمحة أن نفهم البيانات التي يتم تمريرها إلى وظيفة أو عضو في الهيكل: فهي من نوع CommandCode . خلاف ذلك ، يمكن أن يكون uint8_t الخام الذي لا يخبر المبرمج قراءة أي شيء.

إطلاق


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

سنحدد بنية تخزين أمر التشغيل بحيث يمكنك تحديد صفيفها.

typedef struct
{
	CommandCode code;
	uint8_t parameters[15];
	uint8_t length;
} StartupCommand;

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

باستخدام هذه البنية ، يمكننا تحديد قائمة بأوامر التشغيل.

StartupCommand gStartupCommands[] =
{
	// Reset to defaults
	{
		SOFTWARE_RESET,
		{},
		0
	},

	// Landscape Mode
	// Top-Left Origin
	// BGR Panel
	{
		MEMORY_ACCESS_CONTROL,
		{0x20 | 0xC0 | 0x08},
		1
	},

	// 16 bits per pixel
	{
		PIXEL_FORMAT_SET,
		{0x55},
		1
	},

	// Exit sleep mode
	{
		SLEEP_OUT,
		{},
		0
	},

	// Turn on the display
	{
		DISPLAY_ON,
		{},
		0
	},
};

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

الغرض من معظم الفرق واضح من الاسم ، باستثناء فريقين.

MEMORY_ACCESS_CONTROL

  • الوضع الأفقي : بشكل افتراضي ، تستخدم الشاشة الاتجاه الرأسي (240 × 320) ، لكننا نريد استخدام الوضع الأفقي (320 × 240).
  • Top-Left Origin: (0,0) , ( ) .
  • BGR Panel: , BGR. , , , , .

PIXEL_FORMAT_SET

  • 16 bits per pixel: 16- .

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

بعد إعداد مجموعة من أوامر التشغيل ، يمكننا البدء في نقلها إلى الشاشة.

أولاً ، نحتاج إلى وظيفة ترسل بايت واحد من الأوامر إلى الشاشة. لا تنس أن أوامر الإرسال تختلف عن إرسال المعلمات ، لأننا نحتاج إلى إرسال إشارة منخفضة إلى DC .

#define BYTES_TO_BITS(value) ( (value) * 8 )

void SendCommandCode(CommandCode code)
{
	spi_transaction_t transaction = {};

	transaction.length = BYTES_TO_BITS(1);
	transaction.tx_data[0] = (uint8_t)code;
	transaction.flags = SPI_TRANS_USE_TXDATA;

	gpio_set_level(LCD_PIN_DC, 0);
	spi_device_transmit(gSpiHandle, &transaction);
}

يمتلك جيش الدفاع الإسرائيلي بنية spi_transaction_t ، والتي نملأها عندما نريد نقل شيء ما عبر ناقل SPI. نحن نعرف عدد البتات للحمولة وننقل الحمولة نفسها.

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

قبل إرسال البيانات ، نرسل إشارة منخفضة إلى DC ، تشير إلى أن هذا هو رمز الأمر.

void SendCommandParameters(uint8_t* data, int length)
{
	spi_transaction_t transaction = {};

	transaction.length = BYTES_TO_BITS(length);
	transaction.tx_buffer = data;
	transaction.flags = 0;

	gpio_set_level(LCD_PIN_DC, 1);
	spi_device_transmit(SPIHANDLE, &transaction);
}

يشبه تمرير المعلمات إرسال أمر ، هذه المرة فقط نستخدم المخزن المؤقت ( البيانات ) الخاص بنا ونرسل إشارة عالية إلى DC لإخبار الشاشة بأن المعلمات يتم إرسالها. بالإضافة إلى ذلك ، لم نقم بتعيين علامة SPI_TRANS_USE_TXDATA لأننا نمر المخزن المؤقت الخاص بنا.

ثم يمكنك إرسال جميع أوامر التشغيل.

#define ARRAY_COUNT(value) ( sizeof(value) / sizeof(value[0]) )

int commandCount = ARRAY_COUNT(gStartupCommands);

for (int commandIndex = 0; commandIndex < commandCount; ++commandIndex)
{
	StartupCommand* command = &gStartupCommands[commandIndex];

	SendCommandCode(command->code);

	if (command->length > 0)
	{
		SendCommandData(command->parameters, command->length);
	}
}

إننا نجتاز بشكل متكرر صفيف أوامر التشغيل ، ونمرر رمز الأمر أولاً ، ثم المعلمات (إن وجدت).

رسم الإطار


بعد تهيئة الشاشة ، يمكنك البدء في الرسم عليها.

#define UPPER_BYTE_16(value) ( (value) >> 8u )
#define LOWER_BYTE_16(value) ( (value) & 0xFFu )

void Odroid_DrawFrame(uint8_t* buffer)
{
	// Set drawing window width to (0, LCD_WIDTH)
    uint8_t drawWidth[] = { 0, 0, UPPER_BYTE_16(LCD_WIDTH), LOWER_BYTE_16(LCD_WIDTH) };
	SendCommandCode(COLUMN_ADDRESS_SET);
	SendCommandParameters(drawWidth, ARRAY_COUNT(drawWidth));

	// Set drawing window height to (0, LCD_HEIGHT)
    uint8_t drawHeight[] = { 0, 0, UPPER_BYTE_16(LCD_HEIGHT), LOWER_BYTE_16(LCD_HEIGHT) };
	SendCommandCode(PAGE_ADDRESS_SET);
	SendCommandParameters(drawHeight, ARRAY_COUNT(drawHeight));

	// Send the buffer to the display
	SendCommandCode(MEMORY_WRITE);
	SendCommandParameters(buffer, LCD_WIDTH * LCD_HEIGHT * LCD_DEPTH);
}

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

لتقديم إطار ، يتطلب إعداد نافذة تجسيد. للقيام بذلك ، أرسل الأمر COLUMN_ADDRESS_SET مع عرض النافذة والأمر PAGE_ADDRESS_SET مع ارتفاع النافذة. يأخذ كل أمر أربعة بايت من المعلمة التي تصف النافذة التي سنقوم بتنفيذ التقديم فيها.

UPPER_BYTE_16 و LOWER_BYTE_16- هذه وحدات ماكرو مساعدة لاستخراج وحدات البايت العالية والمنخفضة من قيمة 16 بت. تتطلب معلمات هذه الأوامر تقسيم قيمة 16 بت إلى قيمتين 8 بت ، ولهذا السبب نقوم بذلك.

يبدأ التقديم بواسطة الأمر MEMORY_WRITE ويرسل إلى الشاشة كل 153،600 بايت من ذاكرة التخزين المؤقت للإطار في كل مرة.

هناك طرق أخرى لنقل المخزن المؤقت للإطار إلى الشاشة:

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

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

RGB565 وترتيب البايت


يحتوي العرض النموذجي (على سبيل المثال ، شاشة الكمبيوتر) على عمق بت 24 بت (1.6 مليون لون): 8 بت لكل أحمر وأخضر وأزرق. البكسل مكتوب على الذاكرة كـ RRRRRRRRGGGGGGGGGBBBBBBBBB .

يبلغ عمق البت في شاشة Odroid LCD 16 بت (65 ألف لون): 5 بت من اللون الأحمر و 6 بت من اللون الأخضر و 5 بت من اللون الأزرق. البكسل مكتوب على الذاكرة كـ RRRRRGGGGGGGBBBBB . يسمى هذا التنسيق RGB565 .

#define SWAP_ENDIAN_16(value) ( (((value) & 0xFFu) << 8u) | ((value) >> 8u)  )
#define RGB565(red, green, blue) ( SWAP_ENDIAN_16( ((red) << 11u) | ((green) << 5u) | (blue) ) )

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

ومع ذلك ، يقوم ESP32 بتخزين البيانات بترتيب Little Endian ، مما يعني أنه يتم تخزين البايت الأقل أهمية في عنوان الذاكرة السفلي.

على سبيل المثال ، سيتم تخزين قيمة 32 بت [0xDE 0xAD 0xBE 0xEF] في الذاكرة مثل [0xEF 0xBE 0xAD 0xDE] . عند نقل البيانات إلى الشاشة ، تصبح هذه مشكلة نظرًا لأن البايت الأقل أهمية سيتم إرساله أولاً ، وتتوقع شاشة LCD استقبال البايت الأكثر أهمية أولاً. تعيين

الماكرو SWAP_ENDIAN_16لتبديل وحدات البايت واستخدامها في ماكرو RGB565 .

إليك كيفية وصف كل من الألوان الأساسية الثلاثة في RGB565 وكيف يتم تخزينها في ذاكرة ESP32 إذا لم تقم بتغيير ترتيب البايت.

أحمر

11111 | 000000 | 00000؟ -> 11111000 00000000 -> 00000000 11111000

أخضر

00000 | 111111 | 00000؟ -> 00000111 11100000 -> 11100000 00000111

أزرق

00000 | 000000 | 11111؟ -> 00000000 00011111 -> 00011111 00000000

تجريبي


يمكننا إنشاء عرض توضيحي بسيط لمشاهدة شاشة LCD وهي تعمل. في بداية الإطار ، يقوم بمسح المخزن المؤقت للإطار إلى الأسود ويرسم مربع 50x50. يمكننا تحريك المربع بعلامة وتغيير لونه باستخدام الأزرار A و B و Start .

void app_main(void)
{
	Odroid_InitializeInput();
	Odroid_InitializeDisplay();

	ESP_LOGI(LOG_TAG, "Odroid initialization complete - entering main loop");

	uint16_t* framebuffer = (uint16_t*)heap_caps_malloc(320 * 240 * 2, MALLOC_CAP_DMA);
	assert(framebuffer);

	int x = 0;
	int y = 0;

	uint16_t color = 0xffff;

	for (;;)
	{
		memset(framebuffer, 0, 320 * 240 * 2);

		Odroid_Input input = Odroid_PollInput();

		if (input.left) { x -= 10; }
		else if (input.right) { x += 10; }

		if (input.up) { y -= 10; }
		else if (input.down) { y += 10; }

		if (input.a) { color = RGB565(0xff, 0, 0); }
		else if (input.b) { color = RGB565(0, 0xff, 0); }
		else if (input.start) { color = RGB565(0, 0, 0xff); }

		for (int row = y; row < y + 50; ++row)
		{
			for (int col = x; col < x + 50; ++col)
			{
				framebuffer[320 * row + col] = color;
			}
		}

		Odroid_DrawFrame(framebuffer);
	}

	// Should never get here
	esp_restart();
}

نقوم بتخصيص المخزن المؤقت للإطار وفقًا للحجم الكامل للشاشة: 320 × 240 ، وحدتي بايت لكل بكسل (لون 16 بت). نستخدم heap_caps_malloc بحيث يتم تخصيصها في الذاكرة ، والتي يمكن استخدامها لمعاملات SPI مع الوصول المباشر للذاكرة (DMA) . تسمح DMA للأجهزة الطرفية لـ SPI بالوصول إلى المخزن المؤقت للإطار دون الحاجة إلى مشاركة وحدة المعالجة المركزية. بدون DMA ، تستغرق معاملات SPI وقتًا أطول.

نحن لا نجري فحوصات للتأكد من أن العرض لا يحدث خارج حدود الشاشة.


تمزق قوي ملحوظ. في تطبيقات سطح المكتب ، الطريقة القياسية لإزالة التمزق هي استخدام مخازن مؤقتة متعددة. على سبيل المثال ، عند التخزين المؤقت المزدوج ، هناك نوعان من المخازن المؤقتة: المخازن المؤقتة الأمامية والخلفية. أثناء عرض المخزن المؤقت الأمامي ، يتم التسجيل
في الخلف. ثم يغيرون الأماكن وتتكرر العملية.

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

يحتوي ILI9341 على إشارة ( TE ) تخبرك عند حدوث VBLANK حتى نتمكن من الكتابة على الشاشة حتى يتم رسمها. ولكن مع Odroid (أو وحدة العرض) هذه الإشارة غير متصلة ، لذا لا يمكننا الوصول إليها.

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

مصدر


يمكن العثور على جميع التعليمات البرمجية المصدر هنا .

المراجع



All Articles