ESP32 पर एक एम्बेडेड डिवाइस के लिए एक गेम प्रोग्रामिंग

भाग ०: प्रेरणा


परिचय


मुझे एक शौक परियोजना की तलाश थी जो कि मैं दुनिया में स्थिति से बचने के लिए अपने मुख्य कार्यों के बाहर काम कर सकता था। मैं ज्यादातर खेल प्रोग्रामिंग में दिलचस्पी रखता हूं, लेकिन मुझे एम्बेडेड सिस्टम भी पसंद है। अब मैं एक गेमिंग कंपनी में काम करता हूं, लेकिन इससे पहले मैं मुख्य रूप से माइक्रोकंट्रोलर्स में लगा हुआ था। हालांकि अंत में मैंने अपना रास्ता बदलने और खेल उद्योग में जाने का फैसला किया, फिर भी मैं उनके साथ प्रयोग करना पसंद करता हूं। तो दोनों शौक को क्यों नहीं मिलाते?

ओदराय जाना


मेरे पास Odroid Go पड़ा हुआ था , जिसके साथ खेलना दिलचस्प होगा। इसका मूल ESP32 है - मानक एमके कार्यक्षमता (SPI, I2C, GPIO, टाइमर, आदि) के साथ एक बहुत लोकप्रिय माइक्रोकंट्रोलर, लेकिन वाईफाई और ब्लूटूथ के साथ भी, जो IoT डिवाइस बनाने के लिए आकर्षक बनाता है।

Odroid Go, ESP32 को परिधीयों के एक झुंड के साथ पूरक करता है, इसे एक पोर्टेबल गेमिंग मशीन में बदलकर गेमबॉय कलर की याद दिलाता है: एक एलसीडी डिस्प्ले, एक स्पीकर, एक कंट्रोल क्रॉस, दो प्राथमिक और चार सहायक बटन, एक बैटरी और एक एसडी कार्ड रीडर।

ज्यादातर लोग पुराने 8-बिट सिस्टम के एमुलेटर चलाने के लिए Odroid Go खरीदते हैं। यदि यह चीज़ पुराने खेलों का अनुकरण करने में सक्षम है, तो यह विशेष रूप से इसके लिए डिज़ाइन किए गए देशी गेम के लॉन्च के साथ सामना करेगा।


सीमाएं


रिज़ॉल्यूशन 320x240

डिस्प्ले का आकार केवल 320x240 है, इसलिए हम एक ही समय में स्क्रीन पर प्रदर्शित जानकारी की मात्रा में बहुत सीमित हैं। हमें ध्यान से विचार करने की आवश्यकता है कि हम किस खेल को बनाएंगे और किन संसाधनों का उपयोग करेंगे।

16-बिट रंग

प्रदर्शन पिक्सेल प्रति 16-बिट रंग का समर्थन करता है: लाल रंग के लिए 5 बिट्स, हरे रंग के लिए 6 बिट्स, और नीले रंग के लिए 5 बिट्स। स्पष्ट कारणों के लिए, ऐसे सर्किट को आमतौर पर RGB565 कहा जाता है। ग्रीन को एक और लाल और नीला रंग मिला, क्योंकि मानव की आंखें नीले या लाल की तुलना में हरे रंग के उन्नयन के बीच भिन्न होती हैं।

16-बिट रंग का मतलब है कि हमारे पास केवल 65 हजार रंगों तक पहुंच है। मानक 24-बिट रंग (प्रति रंग 8 बिट) के साथ इसकी तुलना करें, 16 मिलियन रंग प्रदान करते हैं।

GPU का अभाव

GPU के बिना, हम OpenGL जैसी API का उपयोग नहीं कर सकते हैं। आज, एक ही GPU का उपयोग आमतौर पर 3D गेम्स के लिए 2D गेम्स को प्रस्तुत करने के लिए किया जाता है। केवल वस्तुओं के बजाय, चतुष्कोण खींचे जाते हैं, जिन पर बिट बनावट अतिव्याप्त होती है। एक GPU के बिना, हमें प्रत्येक पिक्सेल को सीपीयू के साथ रेखापुंज करना पड़ता है, जो धीमा लेकिन सरल है।

320x240 और 16-बिट रंग के स्क्रीन रिज़ॉल्यूशन के साथ, कुल फ्रेम बफर आकार 153,600 बाइट्स है। इसका मतलब है कि प्रति सेकंड कम से कम तीस बार हमें प्रदर्शन के लिए 153,600 बाइट्स प्रेषित करने की आवश्यकता होगी। यह अंततः समस्याएँ पैदा कर सकता है, इसलिए स्क्रीन को प्रस्तुत करते समय हमें चालाक होने की आवश्यकता है। उदाहरण के लिए, आप एक अनुक्रमित रंग को पैलेट में बदल सकते हैं ताकि प्रत्येक पिक्सेल के लिए आपको एक बाइट स्टोर करने की आवश्यकता हो, जिसे 256-रंग पैलेट के सूचकांक के रूप में उपयोग किया जाएगा।

4 MB

ESP32 में 520 KB की इंटरनल रैम है, जबकि Odroid Go में एक और 4 एमबी एक्सटर्नल रैम है। लेकिन यह सभी मेमोरी हमारे लिए उपलब्ध नहीं है, क्योंकि ईएसपी 32 एसडीके (इस पर बाद में) द्वारा भाग का उपयोग किया जाता है। सभी संभावित बाहरी कार्यों को अक्षम करने और मेरे मुख्य फ़ंक्शन में प्रवेश करने के बाद, ESP32 रिपोर्ट करता है कि हम 4,494,848 बाइट्स का उपयोग कर सकते हैं। यदि भविष्य में हमें अधिक मेमोरी की आवश्यकता होती है, तो बाद में हम अनावश्यक कार्यों को रौंद कर वापस आ सकते हैं।

80-240 मेगाहर्ट्ज प्रोसेसर

CPU को तीन संभावित गति: 80 MHz, 160 MHz और 240 MHz पर कॉन्फ़िगर किया गया है। यहां तक ​​कि अधिकतम 240 मेगाहर्ट्ज आधुनिक कंप्यूटरों के तीन से अधिक गीगाहर्ट्ज़ की शक्ति से दूर है, जिसके साथ हम काम करने के आदी हैं। हम 80 मेगाहर्ट्ज से शुरू करेंगे और देखेंगे कि हम कितनी दूर जा सकते हैं। अगर हम चाहते हैं कि खेल बैटरी की शक्ति पर काम करे, तो बिजली की खपत कम होनी चाहिए। ऐसा करने के लिए, आवृत्ति कम करना अच्छा होगा।

खराब डिबगिंग

एम्बेडेड उपकरणों (JTAG) के साथ डिबगर्स का उपयोग करने के तरीके हैं, लेकिन, दुर्भाग्य से, ओड्रोइड गो हमें आवश्यक संपर्क प्रदान नहीं करता है, इसलिए हम डिबगर में कोड के माध्यम से कदम नहीं उठा सकते हैं, जैसा कि आमतौर पर मामला है। इसका मतलब यह है कि डिबगिंग एक कठिन प्रक्रिया हो सकती है, और हमें सक्रिय रूप से ऑन-स्क्रीन डिबगिंग (रंगों और पाठ का उपयोग करके) का उपयोग करना होगा, और डिबगिंग कंसोल को जानकारी भी आउटपुट करना होगा (जो सौभाग्य से, USB UART के माध्यम से आसानी से सुलभ है)।

सारी परेशानी क्यों?


यहां तक ​​कि ऊपर सूचीबद्ध सभी सीमाओं के साथ इस कमजोर डिवाइस के लिए एक गेम बनाने का प्रयास क्यों करें, और सिर्फ डेस्कटॉप पीसी के लिए कुछ भी नहीं लिखें? इसके दो कारण हैं:

सीमाएँ रचनात्मकता को उत्तेजित करती हैं।

जब आप एक ऐसी प्रणाली के साथ काम करते हैं जिसमें उपकरणों का एक निश्चित समूह होता है, जिनमें से प्रत्येक की अपनी सीमाएँ होती हैं, तो यह आपको यह सोचने पर मजबूर कर देता है कि इन सीमाओं के लाभों का सर्वोत्तम उपयोग कैसे किया जाए। इसलिए हम पुराने सिस्टम के गेम डेवलपर्स के करीब पहुंचते हैं, उदाहरण के लिए, सुपर निंटेंडो (लेकिन यह उनके लिए अभी भी हमारे लिए बहुत आसान है)।

निम्न-स्तरीय विकास मजेदार है

एक नियमित डेस्कटॉप सिस्टम के लिए स्क्रैच से गेम लिखने के लिए, हमें मानक निम्न-स्तरीय इंजन अवधारणाओं के साथ काम करना होगा: प्रतिपादन, भौतिकी, टक्कर मान्यता। लेकिन एक एम्बेडेड डिवाइस पर यह सब लागू करते समय, हमें निम्न-स्तरीय कंप्यूटर अवधारणाओं से भी निपटना होगा, उदाहरण के लिए, एक एलसीडी ड्राइवर लिखना।

विकास कितना कम होगा?


जब यह निम्न स्तर पर आता है और अपना कोड बनाता है, तो आपको कहीं सीमा खींचनी होगी। यदि हम डेस्कटॉप के लिए पुस्तकालयों के बिना एक खेल लिखने की कोशिश कर रहे हैं, तो सीमा एक ऑपरेटिंग सिस्टम या एसडीएल जैसे क्रॉस-प्लेटफॉर्म एपीआई होने की संभावना है। अपने प्रोजेक्ट में, मैं SPI ड्राइवरों और बूटलोडर्स जैसी चीजों को लिखने पर एक रेखा खींचूंगा। उनके साथ मस्ती से कहीं ज्यादा तड़प।

इसलिए, हम ईएसपी-आईडीएफ का उपयोग करेंगे, जो अनिवार्य रूप से ईएसपी 32 के लिए एसडीके है। हम मान सकते हैं कि यह हमें कुछ उपयोगिताओं के साथ प्रदान करता है जो ऑपरेटिंग सिस्टम आमतौर पर प्रदान करता है, लेकिन ऑपरेटिंग सिस्टम ESP32 में काम नहीं करता है । कड़ाई से बोलते हुए, यह एमके फ्रीआरटीओएस का उपयोग करता है, जो एक वास्तविक समय ऑपरेटिंग सिस्टम हैलेकिन यह एक वास्तविक ओएस नहीं है। यह सिर्फ एक योजनाकार है। सबसे अधिक संभावना है, हम इसके साथ बातचीत नहीं करेंगे, लेकिन इसके मूल ईएसपी-आईडीएफ में इसका उपयोग करते हैं।

ईएसपी-आईडीएफ हमें एसपीआई, आई 2 सी, और यूएआरटी के साथ-साथ सी रनटाइम लाइब्रेरी के लिए ईएसपी 32 बाह्य उपकरणों के लिए एक एपीआई प्रदान करता है, इसलिए जब हम प्रिंटफ जैसे कुछ कहते हैं, तो यह वास्तव में सीरियल इंटरफ़ेस मॉनीटर पर प्रदर्शित होने के लिए यूएआरटी के माध्यम से बाइट्स को स्थानांतरित करता है। यह मशीन को तैयार करने के लिए आवश्यक सभी स्टार्टअप कोड को संसाधित करता है, इससे पहले कि यह हमारे गेम के लॉन्च बिंदु को आमंत्रित करता है।

इस पोस्ट में मैं एक विकास पत्रिका रखूंगा जिसमें मैं उन दिलचस्प बिंदुओं के बारे में बात करूंगा जो मुझे प्रतीत होते हैं और सबसे कठिन पहलुओं की व्याख्या करते हैं। मेरे पास कोई योजना नहीं है और सबसे अधिक संभावना है कि मैं कई गलतियां करूंगा। यह सब मैं रुचि से बाहर बनाता हूं।

भाग 1: निर्माण प्रणाली


परिचय


इससे पहले कि हम Odroid Go के लिए कोड लिखना शुरू कर सकें, हमें ESP32 SDK को कॉन्फ़िगर करना होगा। इसमें वह कोड होता है जो ESP32 शुरू करता है और हमारे मुख्य फ़ंक्शन को कॉल करता है, साथ ही परिधीय कोड (उदाहरण के लिए, एसपीआई) जिसे हमें एलसीडी ड्राइवर लिखने पर आवश्यकता होगी।

एस्प्रेसिफ ने अपने ईएसपी-आईडीएफ एसडीके को कॉल किया ; हम नवीनतम स्थिर संस्करण v4.0 का उपयोग करते हैं

हम या तो उनके निर्देशों के अनुसार रिपॉजिटरी को क्लोन कर सकते हैं ( पुनरावर्ती ध्वज के साथ ), या बस रिलीज पृष्ठ से ज़िप डाउनलोड कर सकते हैं।

हमारा पहला लक्ष्य Odroid Go पर स्थापित एक न्यूनतम हैलो वर्ल्ड-स्टाइल एप्लिकेशन है जो बिल्ड वातावरण के सही सेटअप को साबित करता है।

C या C ++


ESP-IDF C99 का उपयोग करता है, इसलिए हम इसे भी चुनेंगे। यदि वांछित है, तो हम सी ++ का उपयोग कर सकते हैं (ईएसपी 32 टूलचैन में सी ++ कंपाइलर है), लेकिन अब के लिए, हम सी के साथ छड़ी करेंगे।

वास्तव में, मुझे सी और इसकी सादगी पसंद है। कोई फर्क नहीं पड़ता कि मैं C ++ में कितना कोड लिखता हूं, मैं कभी भी इसका आनंद लेने के क्षण तक पहुंचने में कामयाब नहीं हुआ।

इस व्यक्ति ने मेरे विचारों को बहुत अच्छी तरह से गाया है।

इसके अलावा, यदि आवश्यक हो, तो हम किसी भी समय C ++ पर स्विच कर सकते हैं।

न्यूनतम परियोजना


IDF बिल्ड सिस्टम को प्रबंधित करने के लिए CMake का उपयोग करता है। यह मेकफाइल का भी समर्थन करता है, लेकिन वे v4.0 में पदावनत हैं, इसलिए हम केवल CMake का उपयोग करेंगे।

कम से कम, हमें अपनी परियोजना के विवरण के साथ CMakeLists.txt फ़ाइल की आवश्यकता है , खेल में प्रवेश बिंदु के स्रोत फ़ाइल के साथ एक मुख्य फ़ोल्डर , और मुख्य के अंदर एक और CMakeLists.txt फ़ाइल है , जो स्रोत फ़ाइलों को सूचीबद्ध करती है। सीएमके को पर्यावरण चर का संदर्भ देने की जरूरत है जो यह बताता है कि आईडीएफ और टूलचैन को कहां देखना है। मुझे इस बात पर गुस्सा आ रहा था कि मुझे हर बार नए टर्मिनल सत्र को शुरू करने के लिए उन्हें फिर से स्थापित करना होगा, इसलिए मैंने 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 / घटकों के अंदर हर संभव घटक का निर्माण करेगा , जिसके परिणामस्वरूप अधिक संकलन समय होगा। हम अपने मुख्य फ़ंक्शन को कॉल करने के लिए घटकों का एक न्यूनतम सेट संकलित करना चाहते हैं, और यदि आवश्यक हो तो बाद में अतिरिक्त घटकों को कनेक्ट करें। यह वह है जो घटक चर के लिए है

CMakeLists.txt मुख्य के अंदर :

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

वह सब कुछ जो वह करता है - धारावाहिक इंटरफ़ेस "हैलो वर्ल्ड" पर मॉनिटर पर एक बार दूसरा प्रदर्शन करता है। 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 कहा जाता है , मुख्य नहीं मुख्य फ़ंक्शन का उपयोग आवश्यक तैयारी के लिए IDF द्वारा किया जाता है, और फिर यह हमारे app_main फ़ंक्शन के साथ एक प्रवेश बिंदु के रूप में एक कार्य बनाता है एक कार्य सिर्फ एक निष्पादन योग्य ब्लॉक है जिसे FreeRTOS प्रबंधित कर सकता है। जबकि हमें इस बारे में चिंता नहीं करनी चाहिए (या शायद बिल्कुल नहीं), यहां यह ध्यान रखना महत्वपूर्ण है कि हमारा खेल एक कोर में चलता है (ESP32 में दो कोर हैं), और लूप के लिए प्रत्येक पुनरावृत्ति के साथ, कार्य एक सेकंड के लिए निष्पादन में देरी करता है। इस देरी के दौरान, फ्रीआरटीओएस अनुसूचक अन्य कोड को निष्पादित कर सकता है जो निष्पादन के लिए इंतजार कर रहा है (यदि कोई हो)।



हम दोनों कोर का उपयोग कर सकते हैं, लेकिन अभी के लिए, अपने आप को एक तक सीमित रखें।

अवयव


भले ही हम हैलो वर्ल्ड एप्लिकेशन (जो 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

उनमें से कई काफी तार्किक ( बूटलोडर , esp32 , freertos ) हैं, लेकिन उनका पालन अनावश्यक घटकों द्वारा किया जाता है क्योंकि हम नेटवर्क फ़ंक्शन का उपयोग नहीं करते हैं: esp_eth, esp_wifi, lwip, mbedtls, tcp4_adcape, 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 बाइनरी के आकार को प्रभावित करता है, और यह ठीक है।

प्रोजेक्ट कॉन्फ़िगरेशन


आईडीएफ आपको विभिन्न कार्यों को सक्षम या अक्षम करने के लिए संकलन-समय कॉन्फ़िगरेशन मापदंडों को निर्दिष्ट करने की अनुमति देता है जो इसे विधानसभा के दौरान उपयोग करता है। हमें ऐसे पैरामीटर सेट करने की आवश्यकता है जो हमें ओडायराइड गो के अतिरिक्त पहलुओं का लाभ उठाने की अनुमति देंगे।

सबसे पहले, आपको निर्यात की स्रोत स्क्रिप्ट को चलाने की आवश्यकता है। ताकि सीमेक के पास आवश्यक पर्यावरण चर तक पहुंच हो। इसके अलावा, सभी CMake परियोजनाओं के लिए, हमें एक असेंबली फ़ोल्डर बनाने और उसमें से CMake को कॉल करने की आवश्यकता है।

source export.sh
mkdir build
cd build
cmake ..

यदि आप मेनुकोनफिग बनाते हैं , तो एक विंडो खुलती है जहां आप प्रोजेक्ट सेटिंग्स कॉन्फ़िगर कर सकते हैं।

16 एमबी तक फ्लैश मेमोरी का विस्तार


Odroid Go मानक फ्लैश ड्राइव की क्षमता को 16 एमबी तक बढ़ाता है। आप इस सुविधा को Serial flasher config -> Flash size -> 16MB पर जाकर सक्षम कर सकते हैं

बाहरी SPI RAM चालू करें


हमारे पास एसपीआई के माध्यम से जुड़े अतिरिक्त 4 एमबी बाहरी रैम तक पहुंच भी है। आप इसे कंपोनेंट कॉन्फिगर -> ESP32-specific -> सपोर्ट फॉर एक्सटर्नल, SPI- कनेक्टेड RAM और स्पेस बार को इनेबल करने के लिए इसे दबाकर इनेबल कर सकते हैं। हम SPI RAM से मेमोरी को स्पष्ट रूप से आवंटित करने में सक्षम होना चाहते हैं; इसे SPI RAM कॉन्फिगरेशन -> SPI RAM ऐक्सेस मेथड में जाकर इनेबल किया जा सकता है -> heap_caps_malloc का उपयोग करके RAM को आबंटित करें

आवृत्ति कम करें


ESP32 160 मेगाहर्ट्ज की आवृत्ति के साथ डिफ़ॉल्ट रूप से काम करता है, लेकिन चलो इसे कम करके 80 मेगाहर्ट्ज तक देखते हैं कि आप सबसे कम घड़ी आवृत्ति के साथ कितनी दूर जा सकते हैं। हम चाहते हैं कि खेल बैटरी की शक्ति पर काम करे, और आवृत्ति कम करने से बिजली की बचत होगी। आप इसे घटक कॉन्फ़िगरेशन -> ESP32- विशिष्ट -> सीपीयू आवृत्ति -> 80MHz पर जाकर बदल सकते हैं

यदि आप सहेजें का चयन करते हैं , तो sdkconfig फ़ाइल प्रोजेक्ट फ़ोल्डर की जड़ में सहेजी जाएगी । हम इस फ़ाइल को गिट में लिख सकते हैं, लेकिन इसमें बहुत सारे पैरामीटर हैं जो हमारे लिए महत्वपूर्ण नहीं हैं। अब तक, हम मानक मापदंडों से संतुष्ट हैं, सिवाय इसके कि हम सिर्फ बदल गए।

आप इसके बजाय sdkconfig.defaults फ़ाइल बना सकते हैंजिसमें ऊपर दिए गए मान शामिल होंगे। बाकी सब कुछ डिफ़ॉल्ट रूप से कॉन्फ़िगर किया जाएगा। निर्माण के दौरान, आईडीएफ 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.3V या 0V) में से एक है, लेकिन जब आईडीएफ फ़ंक्शन का उपयोग करके उन्हें पढ़ते हैं, तो वे पूर्णांक मानों में बदल जाते हैं।

प्रारंभ


आरेख में SW के रूप में चिह्नित तत्व स्वयं भौतिक बटन हैं। जब दबाया नहीं जाता है, तो ESP32 संपर्क ( IO13 , IO0 , आदि) 3.3 V से जुड़े होते हैं; यानी 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 के मामले में , हमें इन संपर्कों के पुल-अप प्रतिरोधों को चालू करने के लिए IDF से पूछना होगा। के लिए IO0 और IO39 हम क्योंकि वे शारीरिक प्रतिरोधों है ऐसा करने की जरूरत नहीं है, लेकिन हम किसी तरह इसे क्या करेंगे विन्यास सुंदर बनाने के लिए।

ESP_ERROR_CHECK IDF का एक सहायक मैक्रो है जो स्वतः ही उन सभी कार्यों के परिणाम की जाँच करता है जो एस्पिरेटर वापस करते हैं(अधिकांश IDF) और यह दावा करता है कि परिणाम 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);

क्रॉसपीस (डी-पैड)



एडीसी


क्रॉस कनेक्ट करना बटन कनेक्ट करने से अलग है। अप और डाउन बटन एक एनालॉग-टू-डिजिटल कनवर्टर (एनालॉग-टू-डिजिटल कनवर्टर, एडीसी) के एक पिन से जुड़े होते हैं, और बाएं और दाएं बटन दूसरे एडीसी पिन से जुड़े होते हैं।

GPIO डिजिटल संपर्कों के विपरीत, जिसमें से हम दो राज्यों (उच्च या निम्न) में से एक को पढ़ सकते हैं, ADC एक सतत एनालॉग वोल्टेज (जैसे, 0 V से 3.3 V) को असतत संख्यात्मक मान (जैसे, 0 से 4095 तक) में परिवर्तित करता है )

मुझे लगता है कि Odroid Go डिजाइनरों ने GPIO पिंस को बचाने के लिए ऐसा किया था (आपको चार डिजिटल पिंस के बजाय केवल दो एनालॉग पिंस की आवश्यकता है)। जैसा कि हो सकता है, यह इन संपर्कों से कॉन्फ़िगरेशन और पढ़ने को थोड़ा जटिल करता है।

विन्यास


संपर्क IO35 है की Y अक्ष से जुड़ा मकड़ी , और संपर्क IO34 है जुड़ा के एक्स अक्ष के लिए मकड़ी । हम देखते हैं कि क्रॉस के जोड़ संख्या बटन की तुलना में थोड़ा अधिक जटिल हैं। प्रत्येक अक्ष के दो स्विच ( Y अक्ष के लिए SW1 और SW2 , X अक्ष के लिए SW3 और SW4 ) हैं, जिनमें से प्रत्येक प्रतिरोधों ( R2 , R3 , R4 , R5 ) के एक सेट से जुड़ा है

यदि न तो "ऊपर" और न ही "नीचे" दबाया जाता है, तो I335 पिन को R3 के माध्यम से जमीन पर खींच लिया जाता है , और हम मान लेते हैं 0 V। यदि न तो "बाएं" और न ही "दाएं" दबाया जाता है, तो IO34 से संपर्क करेंR5 के माध्यम से जमीन पर नीचे की ओर खींचता है , और हम 0 वी के मान की गणना करते हैं।

यदि SW1 दबाया जाता है ("ऊपर") , तो IO35 के साथ हम 3.3 V की गणना करते हैं। यदि SW2 दबाया जाता है ("नीचे") , तो I35 के साथ हम 1 के बारे में गिनते हैं। 65 V, क्योंकि आधा वोल्टेज रोकनेवाला R2 पर छोड़ देगा

यदि SW3 दबाया जाता है ("बाएं") , तो IO34 के साथ हम 3.3 V की गणना करते हैं। यदि SW4 दबाया जाता है ("सही") , तो IO34 के साथ हम लगभग 1.65 V को भी गिनते हैं, क्योंकि प्रतिरोध वोल्टेज 4 पर आधा वोल्टेज गिर जाएगा

दोनों मामले वोल्टेज डिवाइडर के उदाहरण हैं जब वोल्टेज विभक्त में दो प्रतिरोधों का एक ही प्रतिरोध (हमारे मामले में - 100K) होता है, तो वोल्टेज ड्रॉप आधे इनपुट वोल्टेज होगा।

यह जानकर, हम क्रॉसपीस को कॉन्फ़िगर कर सकते हैं:

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));

हमने एडीसी को 12 बिट्स चौड़ा किया ताकि 0 V को 0, और 3.3 V को 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();
}

प्रिंटफ़ फ़ंक्शन एक नया जोड़ने के बजाय पिछली पंक्ति को अधिलेखित करने के लिए \ r का उपयोग करता है एक पंक्ति को प्रदर्शित करने के लिए fflush की आवश्यकता होती है, क्योंकि सामान्य स्थिति में इसे newline character \ n द्वारा रीसेट किया जाता है


संदर्भ



भाग 3: प्रदर्शन


परिचय


हमें Odroid Go LCD पर पिक्सल रेंडर करने में सक्षम होना चाहिए।

स्क्रीन पर रंगों को प्रदर्शित करना इनपुट स्थिति को पढ़ने से अधिक कठिन होगा क्योंकि एलसीडी में दिमाग होता है। स्क्रीन को ILI9341 द्वारा नियंत्रित किया जाता है - एक एकल चिप पर एक बहुत लोकप्रिय TFT एलसीडी ड्राइवर।

दूसरे शब्दों में, हम ILI9341 से बात कर रहे हैं, जो एलसीडी पर पिक्सल को नियंत्रित करके हमारी आज्ञाओं का जवाब देता है। जब मैं इस भाग में "स्क्रीन" या "प्रदर्शन" कहता हूं, तो मेरा वास्तव में ILI9341 होगा। हम ILI9341 के साथ काम कर रहे हैं। यह एलसीडी को नियंत्रित करता है।

एसपीआई


एलसीडी एसपीआई (सीरियल पेरिफेरल इंटरफेस) के माध्यम से ईएसपी 32 से जुड़ा है

एसपीआई एक मानक प्रोटोकॉल है जिसका उपयोग मुद्रित सर्किट बोर्ड पर उपकरणों के बीच डेटा का आदान-प्रदान करने के लिए किया जाता है। इसके चार संकेत हैं: MOSI (मास्टर आउट स्लेव इन) , MISO (मास्टर इन स्लेव आउट) , SCK (क्लॉक) और CS (चिप सेलेक्ट)

बस में एक एकल मास्टर डिवाइस SCK और CS को नियंत्रित करके डेटा स्थानांतरण का समन्वय करता है। एक बस में कई उपकरण हो सकते हैं, जिनमें से प्रत्येक के अपने सीएस सिग्नल होंगे। जब इस उपकरण का CS सिग्नल सक्रिय होता है, तो यह डेटा संचारित और प्राप्त कर सकता है।

ईएसपी 32 एसपीआई मास्टर (मास्टर) होगा, और एलसीडी दास एसपीआई दास होगा। हमें आवश्यक मापदंडों के साथ एसपीआई बस को कॉन्फ़िगर करना होगा और संबंधित संपर्कों को कॉन्फ़िगर करके बस में एक एलसीडी डिस्प्ले जोड़ना होगा।



VSPI.XXXX नाम आरेख में संपर्कों के लिए केवल लेबल हैं, लेकिन हम एलसीडी और ईएसपी 32 आरेखों के हिस्सों को देखकर स्वयं संपर्कों के माध्यम से जा सकते हैं।

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

हमारे पास IO14 भी है , जो GPIO पिन है जिसका उपयोग बैकलाइट को चालू करने के लिए किया जाता है, और IO21 भी , जो LCD के DC पिन से जुड़ा होता है । यह संपर्क उस सूचना के प्रकार को नियंत्रित करता है जिसे हम डिस्प्ले में प्रसारित करते हैं।

सबसे पहले, 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 का उपयोग करके बस को कॉन्फ़िगर करते हैं हमारे द्वारा उपयोग किए जाने वाले संपर्कों और एक डेटा ट्रांसफर के अधिकतम आकार को संवाद करना आवश्यक है।

अभी के लिए, हम सभी फ्रेम बफ़र डेटा के लिए एक एसपीआई ट्रांसमिशन का प्रदर्शन करेंगे, जो एलसीडी की चौड़ाई (पिक्सेल में) गुणा इसकी ऊँचाई (पिक्सेल में) गुणा प्रति पिक्सेल बाइट्स की संख्या के बराबर है।

चौड़ाई 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));

बस को शुरू करने के बाद, हमें बस में एक एलसीडी डिवाइस जोड़ने की जरूरत है ताकि हम उससे बात करना शुरू कर सकें।

  • clock_speed_hz — - , SPI 40 , . 80 , .
  • spics_io_num — CS, IDF CS, ( SD- SPI).
  • queue_size — 1, ( ).
  • झंडे - आईडीएफ एसपीआई ड्राइवर आमतौर पर एसपीआई डिवाइस से पढ़ने के दौरान समय की समस्याओं से बचने के लिए ट्रांसमिशन में खाली बिट्स सम्मिलित करता है, लेकिन हम एकतरफा ट्रांसमिशन करते हैं (हम डिस्प्ले से नहीं पढ़ेंगे)। SPI_DEVICE_NO_DUMMY रिपोर्ट करती है कि हम इस एकतरफा प्रसारण की पुष्टि करते हैं और हमें खाली बिट्स डालने की आवश्यकता नहीं है।


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

हमें GPIO पिन के रूप में DC और बैकलाइट पिन भी सेट करने की आवश्यकता है डीसी स्विच करने के बाद , बैकलाइट लगातार चालू रहेगा।

टीमें


एलसीडी के साथ संचार कमांड के रूप में होता है। सबसे पहले, हम एक बाइट पास करते हैं जो कमांड हम भेजना चाहते हैं, और फिर हम कमांड पैरामीटर (यदि कोई हो) पास करते हैं। डिस्प्ले समझता है कि डीसी सिग्नल कम होने पर बाइट एक कमांड है। यदि डीसी सिग्नल अधिक है, तो प्राप्त डेटा को पहले से प्रसारित कमांड के मापदंडों को माना जाएगा।

सामान्य तौर पर, धारा इस तरह दिखती है:

  1. हम डीसी को कम संकेत देते हैं
  2. हम कमांड का एक बाइट भेजते हैं
  3. हम डीसी को उच्च संकेत देते हैं
  4. कमांड की आवश्यकताओं के आधार पर शून्य या अधिक बाइट भेजें
  5. चरण 1-4 दोहराएं

यहां हमारा सबसे अच्छा दोस्त ILI9341 विनिर्देश हैयह सभी संभावित आदेशों, उनके मापदंडों और उनका उपयोग करने के तरीके को सूचीबद्ध करता है।


मापदंडों के बिना कमांड का एक उदाहरण डिस्प्ले ऑन हैकमांड बाइट 0x29 है , लेकिन इसके लिए कोई पैरामीटर निर्दिष्ट नहीं किया गया है।


मापदंडों के साथ एक कमांड का उदाहरण कॉलम एड्रेस सेट हैकमांड बाइट 0x2A है , लेकिन इसके लिए चार आवश्यक पैरामीटर निर्दिष्ट हैं। आदेश का उपयोग करने के लिए, आप एक भेजने की जरूरत कम संकेत करने के लिए डीसी , भेज 0x2A , एक भेजने के उच्च संकेत करने के लिए डीसी , और फिर चार मापदंडों के बाइट्स हस्तांतरण।

कमांड कोड स्वयं गणना में निर्दिष्ट हैं।

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 संपर्कों के साथ किया था, लेकिन एनम के लिए धन्यवाद, पहली नज़र में हम समझ सकते हैं कि किसी फ़ंक्शन या संरचना के सदस्य को क्या डेटा दिया जाता है: वे प्रकार कमांडकोड के हैंअन्यथा, यह कच्चा uint8_t हो सकता है जो कोड को पढ़ने वाले प्रोग्रामर को कुछ नहीं बताता है

प्रक्षेपण


आरंभीकरण के दौरान, हम कुछ आकर्षित करने में सक्षम होने के लिए विभिन्न आदेश पारित कर सकते हैं। प्रत्येक कमांड में एक कमांड बाइट होती है, जिसे हम कमांड कोड कहेंगे

हम लॉन्च कमांड को संग्रहीत करने के लिए एक संरचना को परिभाषित करेंगे ताकि आप उनकी सरणी निर्दिष्ट कर सकें।

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

  • कोड कमांड कोड है।
  • पैरामीटर , कमांड पैरामीटर्स का एक सरणी है (यदि कोई हो)। यह आकार 15 का एक स्थिर सरणी है, क्योंकि हमें अधिकतम पैरामीटर की आवश्यकता है। सरणी की स्थिर प्रकृति के कारण, हमें हर बार प्रत्येक कमांड के लिए एक गतिशील सरणी आवंटित करने के बारे में चिंता करने की ज़रूरत नहीं है।
  • लंबाई पैरामीटर सरणी में मापदंडों की संख्या है

इस संरचना का उपयोग करके, हम लॉन्च कमांड की एक सूची निर्दिष्ट कर सकते हैं।

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

  • लैंडस्केप मोड: डिफ़ॉल्ट रूप से, डिस्प्ले पोर्ट्रेट ओरिएंटेशन (240x320) का उपयोग करता है, लेकिन हम लैंडस्केप (320x240) का उपयोग करना चाहते हैं।
  • Top-Left Origin: (0,0) , ( ) .
  • BGR Panel: , BGR. , , , , .

PIXEL_FORMAT_SET

  • 16 bits per pixel: 16- .

कई अन्य कमांड हैं जिन्हें स्टार्टअप पर विभिन्न पहलुओं, जैसे गामा, को नियंत्रित करने के लिए भेजा जा सकता है। आवश्यक मापदंडों को एलसीडी के विनिर्देश में वर्णित किया गया है (और ILI9341 नियंत्रक नहीं), जिसके लिए हमारे पास पहुंच नहीं है। यदि हम इन आदेशों को प्रसारित नहीं करते हैं, तो डिफ़ॉल्ट डिस्प्ले सेटिंग्स का उपयोग किया जाता है, जो हमें पूरी तरह से सूट करता है।

लॉन्च कमांड की एक सरणी तैयार करने के बाद, हम उन्हें प्रदर्शन में स्थानांतरित करना शुरू कर सकते हैं।

सबसे पहले, हमें एक फ़ंक्शन की आवश्यकता होती है जो प्रदर्शन के लिए एक बाइट आदेश भेजता है। यह मत भूलो कि कमांड भेजना मापदंडों को भेजने से अलग है, क्योंकि हमें डीसी को कम संकेत भेजने की आवश्यकता है

#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);
}

IDF में एक spi_transaction_t संरचना होती है , जिसे हम उस समय आबाद करते हैं जब हम SPI बस के माध्यम से कुछ स्थानांतरित करना चाहते हैं। हम जानते हैं कि पेलोड कितने बिट्स हैं और लोड को खुद ट्रांसफर करते हैं।

हम या तो पेलोड को पॉइंटर पास कर सकते हैं, या आंतरिक संरचना tx_data संरचना का उपयोग कर सकते हैं , जो आकार में केवल चार बाइट्स है, लेकिन ड्राइवर को बाहरी मेमोरी तक पहुंचने से बचाता है। यदि हम tx_data का उपयोग करते हैं , तो हमें ध्वज SPI_TRANS_USE_TXDATA सेट करना होगा

डेटा प्रसारित करने से पहले, हम डीसी को एक कम संकेत भेजते हैं , यह दर्शाता है कि यह एक कमांड कोड है।

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);
}

पासिंग पैरामीटर एक कमांड भेजने के समान है, केवल इस बार हम अपने स्वयं के बफर ( डेटा ) का उपयोग करते हैं और डीसी को एक उच्च सिग्नल भेजते हैं ताकि डिस्प्ले को यह बताया जा सके कि पैरामीटर ट्रांसमिट हो रहे हैं। इसके अलावा, हम 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 लेनदेन के समन्वय के लिए जिम्मेदार है।
  • आप एक फ्रेम को एक में नहीं, बल्कि कई लेनदेन में स्थानांतरित कर सकते हैं।
  • आप गैर-अवरुद्ध संचरण का उपयोग कर सकते हैं, जिसमें हम भेजने की शुरुआत करते हैं, और फिर अन्य संचालन करना जारी रखते हैं।
  • आप उपरोक्त विधियों के किसी भी संयोजन का उपयोग कर सकते हैं।

अभी के लिए, हम सबसे सरल तरीके का उपयोग करेंगे: एकमात्र अवरुद्ध लेनदेन। जब ड्राफ्रेम कहा जाता है, तो प्रदर्शन को हस्तांतरण शुरू किया जाता है और हमारा काम तब तक रोक दिया जाता है जब तक कि हस्तांतरण पूरा नहीं हो जाता। यदि बाद में हमें पता चलता है कि हम इस पद्धति के साथ एक अच्छी फ्रेम दर प्राप्त नहीं कर सकते हैं, तो हम इस समस्या पर लौटेंगे।

RGB565 और बाइट ऑर्डर


एक विशिष्ट प्रदर्शन (उदाहरण के लिए, आपके कंप्यूटर की निगरानी) में 24 बिट्स (1.6 मिलियन रंग) की थोड़ी गहराई है: 8 बिट्स प्रति लाल, हरा और नीला। पिक्सेल को RRRRRRRGGGGGGGGBBBBBBBB के रूप में मेमोरी में लिखा जाता है

ओड्रोइड एलसीडी में 16 बिट्स (65 हजार रंग) की थोड़ी गहराई है: 5 बिट्स ऑफ रेड, 6 बिट्स ऑफ ग्रीन और 5 बिट्स ऑफ ब्लू। पिक्सेल को RRRRRGGGGGGBBBB के रूप में मेमोरी में लिखा जाता है । इस प्रारूप को 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 लिटिल एंडियन ऑर्डर में डेटा संग्रहीत करता है , यानी कम से कम महत्वपूर्ण बाइट निचले मेमोरी पते में संग्रहीत किया जाता है।

उदाहरण के लिए, 32-बिट मान [0xDE 0xAD 0xBE 0xEF] को मेमोरी में [0xEF 0xBE 0xAD 0xDE] के रूप में संग्रहीत किया जाएगा । डेटा को डिस्प्ले में ट्रांसफर करते समय, यह एक समस्या बन जाती है क्योंकि कम से कम महत्वपूर्ण बाइट पहले भेजा जाएगा, और एलसीडी को सबसे महत्वपूर्ण बाइट पहले प्राप्त होने की उम्मीद है।

स्थूल SWAP_ENDIAN_16 सेट करेंबाइट्स को स्वैप करने के लिए और RGB565 मैक्रो में इसका उपयोग करें

यहां बताया गया है कि आरजीबी 565 में तीन प्राथमिक रंगों में से किसका वर्णन किया गया है और यदि आप बाइट क्रम को नहीं बदलते हैं, तो उन्हें ईएसपी 32 मेमोरी में कैसे संग्रहीत किया जाता है।

रेड

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

ग्रीन

00000; 111111 | 00000; -> 00000111 11100000 -> 11100000 00000111

Blue

00000; 000000; 11111; -> 00000000 00011111 -> 00011111 00000000

डेमो


हम एलसीडी को कार्रवाई में देखने के लिए एक सरल डेमो बना सकते हैं। फ्रेम की शुरुआत में, यह फ्रेम बफर को काला करने के लिए फ्लश करता है और 50x50 वर्ग खींचता है। हम एक क्रॉस के साथ वर्ग को स्थानांतरित कर सकते हैं और इसके रंग को बटन , बी और स्टार्ट के साथ बदल सकते हैं

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 x 240, प्रति पिक्सेल दो बाइट्स (16-बिट रंग)। हम heap_caps_malloc का उपयोग करते हैं ताकि इसे मेमोरी में आवंटित किया जाए, जिसका उपयोग डायरेक्ट मेमोरी एक्सेस (डीएमए) के साथ एसपीआई लेनदेन के लिए किया जा सकता है डीएमए एसपीआई बाह्य उपकरणों को सीपीयू भागीदारी की आवश्यकता के बिना फ्रेम बफर का उपयोग करने की अनुमति देता है। डीएमए के बिना, एसपीआई लेनदेन में अधिक समय लगता है।

हम यह सुनिश्चित करने के लिए जांच नहीं करते हैं कि रेंडरिंग स्क्रीन की सीमाओं के बाहर नहीं होती है।


मजबूत फाड़ ध्यान देने योग्य है। डेस्कटॉप अनुप्रयोगों में, फाड़ को खत्म करने का मानक तरीका कई बफ़र्स का उपयोग करना है। उदाहरण के लिए, जब डबल बफरिंग होती है, तो दो बफ़र होते हैं: फ्रंट और रियर बफ़र। जबकि सामने बफ़र प्रदर्शित किया गया है, रिकॉर्डिंग
रियर में की जाती है। फिर वे स्थान बदलते हैं और प्रक्रिया दोहराती है।

ESP32 में दो फ्रेम बफ़र्स (4 एमबी बाहरी एसपीआई रैम को स्टोर करने के लिए डीएमए क्षमताओं के साथ पर्याप्त रैम नहीं है, दुर्भाग्य से, डीएमए क्षमताएं नहीं हैं), इसलिए यह विकल्प उपयुक्त नहीं है।

ILI9341 में एक सिग्नल ( TE ) है जो आपको बताता है कि VBLANK कब होता है ताकि हम डिस्प्ले को तब तक लिख सकें जब तक इसे खींचा न जाए। लेकिन Odroid (या डिस्प्ले मॉड्यूल) के साथ यह सिग्नल कनेक्ट नहीं है, इसलिए हम इसे एक्सेस नहीं कर सकते।

शायद हमें एक अच्छा मूल्य मिल सकता है, लेकिन अब हम ऐसा नहीं करेंगे, क्योंकि अब हमारा काम केवल स्क्रीन पर पिक्सल प्रदर्शित करना है।

स्रोत


सभी स्रोत कोड यहां देखे जा सकते हैं

संदर्भ



All Articles