Programmation d'un jeu pour un appareil embarqué sur ESP32

Partie 0: motivation


introduction


Je cherchais un projet de loisir sur lequel je pourrais travailler en dehors de mes tâches principales afin d'échapper à la situation dans le monde. Je suis principalement intéressé par la programmation de jeux, mais j'aime aussi les systèmes embarqués. Maintenant, je travaille dans une entreprise de jeux, mais avant j'étais principalement engagé dans les microcontrôleurs. Bien qu'à la fin j'ai décidé de changer de voie et de me lancer dans l'industrie du jeu, j'aime toujours les expérimenter. Alors pourquoi ne pas combiner les deux hobbies?

Odroid go


J'avais Odroid Go qui traînait , ce qui serait intéressant de jouer avec. Son cœur est ESP32 - un microcontrôleur très populaire avec des fonctionnalités MK standard (SPI, I2C, GPIO, minuteries, etc.), mais aussi avec WiFi et Bluetooth, ce qui le rend attrayant pour la création d'appareils IoT.

Odroid Go complète l'ESP32 avec un tas de périphériques, le transformant en une machine de jeu portable rappelant Gameboy Color: un écran LCD, un haut-parleur, une croix de contrôle, deux boutons principaux et quatre auxiliaires, une batterie et un lecteur de carte SD.

La plupart des gens achètent Odroid Go pour exécuter des émulateurs d' anciens systèmes 8 bits. Si cette chose est capable d'émuler d'anciens jeux, elle fera également face au lancement d'un jeu natif spécialement conçu pour elle.


Limites


Résolution 320x240 L'

écran n'a qu'une taille de 320x240, nous sommes donc très limités dans la quantité d'informations affichées à l'écran en même temps. Nous devons examiner attentivement quel jeu nous allons créer et quelles ressources utiliser.

Couleur 16 bits L'

écran prend en charge la couleur 16 bits par pixel: 5 bits pour le rouge, 6 bits pour le vert et 5 pour le bleu. Pour des raisons évidentes, un tel circuit est généralement appelé RGB565. Le vert est devenu un peu plus rouge et bleu, car l'œil humain distingue mieux les dégradés de vert que de bleu ou de rouge.

La couleur 16 bits signifie que nous n'avons accès qu'à 65 000 couleurs. Comparez cela avec la couleur standard 24 bits (8 bits par couleur), fournissant 16 millions de couleurs.

Manque de GPU

Sans GPU, nous ne pouvons pas utiliser une API comme OpenGL. Aujourd'hui, les mêmes GPU sont généralement utilisés pour le rendu des jeux 2D que pour les jeux 3D. Juste au lieu d'objets, des quadrangles sont dessinés, sur lesquels des textures de bits sont superposées. Sans GPU, nous devons pixelliser chaque pixel avec un processeur, ce qui est plus lent mais plus simple.

Avec une résolution d'écran de 320x240 et des couleurs 16 bits, la taille totale du tampon de trame est de 153 600 octets. Cela signifie qu'au moins trente fois par seconde, nous devrons transmettre 153 600 octets à l'écran. Cela peut finalement causer des problèmes, nous devons donc être plus intelligents lors du rendu de l'écran. Par exemple, vous pouvez convertir une couleur indexée en une palette de sorte que pour chaque pixel, vous devez stocker un octet, qui sera utilisé comme index d'une palette de 256 couleurs.

4 Mo

ESP32 a 520 Ko de RAM interne, tandis que Odroid Go ajoute 4 Mo de RAM externe supplémentaires. Mais toute cette mémoire n'est pas disponible pour nous, car une partie est utilisée par le SDK ESP32 (plus à ce sujet plus tard). Après avoir désactivé toutes les fonctions étrangères possibles et entré dans ma fonction principale, ESP32 signale que nous pouvons utiliser 4 494 848 octets. Si, à l'avenir, nous avons besoin de plus de mémoire, nous pourrons ensuite revenir à la suppression des fonctions inutiles.

Processeur 80-240 MHz

Le CPU est configuré à trois vitesses possibles: 80 MHz, 160 MHz et 240 MHz. Même un maximum de 240 MHz est loin de la puissance de plus de trois gigahertz d'ordinateurs modernes avec lesquels nous avons l'habitude de travailler. Nous allons commencer à 80 MHz et voir jusqu'où nous pouvons aller. Si nous voulons que le jeu fonctionne sur batterie, la consommation électrique doit être faible. Pour ce faire, il serait bon de baisser la fréquence.

Mauvais débogage

Il existe des moyens d'utiliser des débogueurs avec des appareils intégrés (JTAG), mais, malheureusement, Odroid Go ne nous fournit pas les contacts nécessaires, nous ne pouvons donc pas parcourir le code dans le débogueur, comme c'est généralement le cas. Cela signifie que le débogage peut être un processus difficile, et nous devrons utiliser activement le débogage à l'écran (en utilisant des couleurs et du texte), ainsi que des informations de sortie sur la console de débogage (qui, heureusement, est facilement accessible via USB UART).

Pourquoi tous ces ennuis?


Pourquoi même essayer de créer un jeu pour cet appareil faible avec toutes les limitations énumérées ci-dessus, et ne rien écrire pour un ordinateur de bureau? Il y a deux raisons à cela: Les

limitations stimulent la créativité.

Lorsque vous travaillez avec un système qui a un certain ensemble d'équipements, chacun ayant ses propres limitations, cela vous fait réfléchir sur la meilleure façon d'utiliser les avantages de ces limitations. Nous nous rapprochons donc des développeurs de jeux d'anciens systèmes, par exemple Super Nintendo (mais c'est toujours beaucoup plus facile pour nous que pour eux).

Le développement de bas niveau est amusant

Pour écrire un jeu à partir de zéro pour un système de bureau classique, nous devons travailler avec des concepts de moteur de bas niveau standard: rendu, physique, reconnaissance des collisions. Mais lors de la mise en œuvre de tout cela sur un périphérique intégré, nous devons également faire face à des concepts informatiques de bas niveau, par exemple, l'écriture d'un pilote LCD.

À quel point le développement sera-t-il faible?


En ce qui concerne le bas niveau et la création de votre propre code, vous devez tracer une frontière quelque part. Si nous essayons d'écrire un jeu sans bibliothèques pour le bureau, la frontière est probablement un système d'exploitation ou une API multiplateforme comme SDL. Dans mon projet, je vais tracer une ligne sur l'écriture de choses comme les pilotes SPI et les chargeurs de démarrage. Avec eux beaucoup plus de tourments que de plaisir.

Nous allons donc utiliser l'ESP-IDF, qui est essentiellement un SDK pour ESP32. Nous pouvons supposer qu'il nous fournit certains utilitaires que le système d'exploitation fournit habituellement, mais le système d'exploitation ne fonctionne pas dans ESP32 . À strictement parler, ce MK utilise FreeRTOS, qui est un système d'exploitation en temps réelmais ce n'est pas un vrai OS. Ce n'est qu'un planificateur. Très probablement, nous n'interagirons pas avec lui, mais dans son noyau ESP-IDF l'utilise.

ESP-IDF nous fournit une API pour les périphériques ESP32 tels que SPI, I2C et UART, ainsi qu'une bibliothèque d'exécution C, donc lorsque nous appelons quelque chose comme printf, il transfère en fait des octets via UART pour être affiché sur le moniteur de l'interface série. Il traite également tout le code de démarrage nécessaire pour préparer la machine avant d'invoquer le point de lancement de notre jeu.

Dans ce billet je garderai un magazine de développement dans lequel je parlerai des points intéressants qui m'ont paru et expliquerai les aspects les plus difficiles. Je n'ai pas de plan et je ferai très probablement de nombreuses erreurs. Je crée tout cela par intérêt.

Partie 1: système de construction


introduction


Avant de commencer à écrire du code pour Odroid Go, nous devons configurer le SDK ESP32. Il contient le code qui démarre ESP32 et appelle notre fonction principale, ainsi que le code périphérique (par exemple, SPI) dont nous aurons besoin lors de l'écriture du pilote LCD.

Espressif appelle son SDK ESP-IDF ; nous utilisons la dernière version stable v4.0 .

Nous pouvons soit cloner le référentiel selon leurs instructions (avec l'indicateur récursif ), soit simplement télécharger le zip depuis la page des versions.

Notre premier objectif est une application minimale de style Hello World installée sur Odroid Go qui prouve la configuration correcte de l'environnement de génération.

C ou C ++


ESP-IDF utilise C99, nous allons donc le choisir aussi. Si vous le souhaitez, nous pourrions utiliser C ++ (il y a un compilateur C ++ dans la chaîne d'outils ESP32), mais pour l'instant, nous nous en tiendrons à C.

En fait, j'aime C et sa simplicité. Peu importe combien j'écris du code en C ++, je n'ai jamais réussi à en profiter.

Cette personne résume assez bien mes pensées.

De plus, si nécessaire, nous pouvons à tout moment passer au C ++.

Projet minimal


IDF utilise CMake pour gérer le système de génération. Il prend également en charge Makefile, mais ils sont obsolètes dans la version 4.0, nous allons donc simplement utiliser CMake.

Au minimum, nous avons besoin d'un fichier CMakeLists.txt avec une description de notre projet, un dossier principal avec le fichier source du point d'entrée dans le jeu et un autre fichier CMakeLists.txt à l' intérieur de main , qui répertorie les fichiers source.

CMake doit référencer les variables d'environnement qui lui indiquent où chercher IDF et la chaîne d'outils. J'étais ennuyé de devoir les réinstaller à chaque fois que je commençais une nouvelle session de terminal, j'ai donc écrit le script export.sh . Il définit IDF_PATH et IDF_TOOLS_PATHet est également une source d'exportation IDF qui définit d'autres variables d'environnement.

Il suffit à l'utilisateur du script de définir les variables IDF_PATH et 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 à la racine:

cmake_minimum_required(VERSION 3.5)

set(COMPONENTS "esptool_py main")

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

project(game)

Par défaut, le système de construction construira tous les composants possibles dans $ ESP_IDF / components , ce qui entraînera plus de temps de compilation. Nous voulons compiler un ensemble minimal de composants pour appeler notre fonction principale et connecter des composants supplémentaires plus tard si nécessaire. C'est à cela que sert la variable COMPOSANTS .

CMakeLists.txt à l' intérieur de main :

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

Tout ce qu'il fait - infiniment une fois par seconde affiche sur le moniteur l'interface série "Hello World". VTaskDelay utilise FreeRTOS pour retarder .

Le fichier main.c est très simple:

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

Notez que notre fonction s'appelle app_main , pas principale . La fonction principale est utilisée par l'IDF pour la préparation nécessaire, puis elle crée une tâche avec notre fonction app_main comme point d'entrée.

Une tâche n'est qu'un bloc exécutable que FreeRTOS peut gérer. Bien que nous ne devrions pas nous en préoccuper (ou peut-être pas du tout), il est important de noter ici que notre jeu s'exécute dans un cœur (ESP32 a deux cœurs), et à chaque itération de la boucle for, la tâche retarde l'exécution d'une seconde. Pendant ce délai, le planificateur FreeRTOS peut exécuter un autre code en attente d'exécution (le cas échéant).

Nous pouvons utiliser les deux cœurs, mais pour l'instant, limitons-nous à un seul.

Composants


Même si nous réduisons la liste des composants au minimum nécessaire pour l'application Hello World (qui sont esptool_py et main ), en raison de la configuration de la chaîne de dépendances, elle collecte toujours d'autres composants dont nous n'avons pas besoin. Il rassemble tous ces composants:

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

Beaucoup d'entre eux sont assez logiques ( bootloader , esp32 , freertos ), mais ils sont suivis de composants inutiles car nous n'utilisons pas de fonctions réseau: esp_eth, esp_wifi, lwip, mbedtls, tcpip_adapter, wpa_supplicant . Malheureusement, nous sommes toujours obligés d'assembler ces composants.

Heureusement, l'éditeur de liens est assez intelligent et ne place pas les composants inutilisés dans un fichier binaire prêt à l'emploi du jeu. Nous pouvons le vérifier avec make size-components .

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

Surtout, libc affecte la taille du binaire, et c'est très bien.

Configuration du projet


IDF vous permet de spécifier les paramètres de configuration au moment de la compilation qu'il utilise lors de l'assemblage pour activer ou désactiver diverses fonctions. Nous devons définir des paramètres qui nous permettront de tirer parti des aspects supplémentaires d'Odroid Go.

Tout d'abord, vous devez exécuter le script source d' export.sh pour que CMake ait accès aux variables d'environnement nécessaires. De plus, comme pour tous les projets CMake, nous devons créer un dossier d'assemblage et appeler CMake à partir de celui-ci.

source export.sh
mkdir build
cd build
cmake ..

Si vous exécutez make menuconfig , une fenêtre s'ouvre dans laquelle vous pouvez configurer les paramètres du projet.

Extension de la mémoire flash jusqu'à 16 Mo


Odroid Go étend la capacité standard du lecteur flash à 16 Mo. Vous pouvez activer cette fonctionnalité en allant config flasher série -> Taille flash -> 16Mo .

Activer la RAM SPI externe


Nous avons également accès à 4 Mo supplémentaires de RAM externe connectés via SPI. Vous pouvez l'activer en allant dans Configuration des composants -> Spécifique à ESP32 -> Prise en charge de la RAM externe connectée SPI et en appuyant sur la barre d'espace pour l'activer. Nous voulons également pouvoir allouer explicitement de la mémoire à partir de la RAM SPI; ceci peut être activé en allant dans SPI RAM config -> SPI RAM access method -> Make RAM allocatable using heap_caps_malloc .

Baisser la fréquence


L'ESP32 fonctionne par défaut avec une fréquence de 160 MHz, mais abaissons-le à 80 MHz pour voir jusqu'où vous pouvez aller avec la fréquence d'horloge la plus basse. Nous voulons que le jeu fonctionne sur batterie, et abaisser la fréquence permettra d'économiser de l'énergie. Vous pouvez le changer en allant dans Configuration des composants -> Spécifique à ESP32 -> Fréquence du processeur -> 80 MHz .

Si vous sélectionnez Enregistrer , le fichier sdkconfig sera enregistré à la racine du dossier du projet . Nous pouvons écrire ce fichier dans git, mais il a beaucoup de paramètres qui ne sont pas importants pour nous. Jusqu'à présent, nous sommes satisfaits des paramètres standard, à l'exception de ceux que nous venons de changer.

Vous pouvez créer le fichier sdkconfig.defaults à la placequi contiendra les valeurs modifiées ci-dessus. Tout le reste sera configuré par défaut. Pendant la construction, l'IDF lira sdkconfig.defaults , remplacera les valeurs que nous avons définies et utilisera la norme pour tous les autres paramètres.

Maintenant, sdkconfig.defaults ressemble à ceci:

# 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

En général, la structure originale du jeu ressemble à ceci:

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

Construire et flasher


Le processus d'assemblage et de firmware lui-même est assez simple.

Nous lançons make to compile (pour la construction parallèle, ajoutons -j4 ou -j8 ), faisons flash pour écrire l'image sur Odroid Go, et faisons moniteur pour voir la sortie des instructions printf .

make
make flash
make monitor

Nous pouvons également les exécuter sur une seule ligne.

make flash monitor

Le résultat n'est pas particulièrement impressionnant, mais il deviendra la base du reste du projet.


Références



Partie 2: entrée


introduction


Nous devons être en mesure de lire les boutons enfoncés par le joueur et la croix sur Odroid Go.

Boutons



GPIO


Odroid Go a six boutons: A , B , Select , Start , Menu et Volume .

Chacun des boutons est connecté à une broche GPO (General Purpose IO) distincte . Les broches GPIO peuvent être utilisées comme entrées (pour la lecture) ou comme sorties (nous leur écrivons). Dans le cas des boutons, nous avons besoin d'une lecture.

Vous devez d'abord configurer les contacts comme entrées, après quoi nous pouvons lire leur état. Les contacts à l'intérieur ont l'une des deux tensions (3,3 V ou 0 V), mais lors de leur lecture à l'aide de la fonction IDF, ils sont convertis en valeurs entières.

Initialisation


Les éléments marqués SW dans le diagramme sont les boutons physiques eux-mêmes. Lorsqu'ils ne sont pas enfoncés, les contacts ESP32 ( IO13 , IO0 , etc.) sont connectés à 3,3 V; soit 3,3 V signifie que le bouton n'est pas enfoncé . La logique ici est à l'opposé de ce qui est attendu.

IO0 et IO39 ont des résistances physiques sur la carte. Si le bouton n'est pas enfoncé, la résistance tire les contacts à une haute tension. Si le bouton est enfoncé, le courant traversant les contacts passe à la terre à la place, de sorte que la tension 0 sera lue à partir des contacts.

IO13 , IO27 , IO32 et IO33n'ont pas de résistances, car le contact sur l'ESP32 a des résistances internes, que nous avons configurées pour le mode pull-up.

Sachant cela, nous pouvons configurer six boutons à l'aide de l'API GPIO.

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

Les constantes spécifiées au début du code correspondent à chacun des contacts du circuit. Nous utilisons la structure gpio_config_t pour configurer chacun des six boutons comme entrée pull-up. Dans le cas de IO13 , IO27 , IO32 et IO33, nous devons demander à IDF d'activer les résistances de rappel de ces contacts. Pour IO0 et IO39, nous n'avons pas besoin de le faire car ils ont des résistances physiques, mais nous le ferons quand même pour rendre la configuration magnifique.

ESP_ERROR_CHECK est une macro d'aide de l'IDF qui vérifie automatiquement le résultat de toutes les fonctions qui renvoient esp_err_t(la plupart des IDF) et affirment que le résultat n'est pas égal à ESP_OK . Cette macro est pratique à utiliser pour une fonction si son erreur est critique et après cela n'a aucun sens pour continuer l'exécution. Dans ce jeu, un jeu sans entrée n'est pas un jeu, donc cette affirmation est vraie. Nous utiliserons souvent cette macro.

Boutons de lecture


Nous avons donc configuré tous les contacts et pouvons enfin lire les valeurs.

Les boutons numériques sont lus par la fonction gpio_get_level , mais nous devons inverser les valeurs reçues, car les contacts sont relevés, c'est-à-dire qu'un signal haut signifie en fait «non enfoncé», et un faible signifie «enfoncé». L'inversion conserve la logique habituelle: 1 signifie «pressé», 0 - «non pressé».

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

Traverse (D-pad)



ADC


Connecter la croix est différent de connecter les boutons. Les boutons haut et bas sont connectés à une broche d'un convertisseur analogique-numérique (ADC) , et les boutons gauche et droit sont connectés à une autre broche ADC.

Contrairement aux contacts numériques GPIO, à partir desquels nous pouvons lire l'un des deux états (haut ou bas), l'ADC convertit une tension analogique continue (par exemple, de 0 V à 3,3 V) en une valeur numérique discrète (par exemple, de 0 à 4095 )

Je suppose que les concepteurs d'Odroid Go l'ont fait pour économiser sur les broches GPIO (vous n'avez besoin que de deux broches analogiques au lieu de quatre broches numériques). Quoi qu'il en soit, cela complique légèrement la configuration et la lecture de ces contacts.

Configuration


Le contact IO35 est connecté à l'axe Y de l' araignée et le contact IO34 est connecté à l'axe X de l' araignée . On voit que les articulations de la croix sont un peu plus compliquées que les touches numériques. Chaque axe possède deux commutateurs ( SW1 et SW2 pour l'axe Y, SW3 et SW4 pour l'axe X), chacun étant connecté à un ensemble de résistances ( R2 , R3 , R4 , R5 ).

Si ni «haut» ni «bas» n'est enfoncé, la broche IO35 est tirée vers le bas via R3 , et nous considérons la valeur 0 V. Si ni «gauche» ni «droite» n'est enfoncé, contactez IO34tire vers le sol par R5 , et nous comptons la valeur à 0 V.

Si SW1 est pressé ("haut") , puis avec IO35 nous comptons 3,3 V. Si SW2 est pressé ("bas") , puis avec IO35 nous comptons environ 1, 65 V, car la moitié de la tension chutera sur la résistance R2 .

Si SW3 est pressé ("à gauche") , alors avec IO34 nous comptons 3,3 V. Si SW4 est pressé ("à droite") , alors avec IO34 nous comptons également environ 1,65 V, car la moitié de la tension chutera sur la résistance R4 .

Les deux cas sont des exemples de diviseurs de tension.. Lorsque deux résistances dans le diviseur de tension ont la même résistance (dans notre cas - 100K), la chute de tension sera alors la moitié de la tension d'entrée.

Sachant cela, nous pouvons configurer la traverse:

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

Nous avons défini l'ADC sur 12 bits de large afin que 0 V soit lu comme 0 et 3,3 V comme 4095 (2 ^ 12). L'atténuation signale que nous n'avons pas besoin d'atténuer le signal pour obtenir la plage de tension complète de 0 V à 3,3 V.

À 12 bits, nous pouvons nous attendre à ce que si rien n'est pressé, alors 0 sera lu, lorsqu'il est pressé vers le haut et à gauche - 4096, environ 2048 seront lus en appuyant vers le bas et vers la droite (car les résistances réduisent la tension de moitié).

Lecture croisée


La lecture de la croix est plus difficile que les boutons, car il faut lire les valeurs brutes (de 0 à 4095) et les interpréter.

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 et ADC_NEGATIVE_LEVEL sont des valeurs avec une marge, garantissant que nous lisons toujours les valeurs correctes.

Sondage


Il existe deux options pour obtenir les valeurs des boutons: interrogation ou interruptions. Nous pouvons créer des fonctions de traitement d'entrée et demander à IDF d'appeler ces fonctions lorsque les boutons sont enfoncés, ou d'interroger manuellement l'état des boutons lorsque nous en avons besoin. Les comportements provoqués par les interruptions rendent les choses plus compliquées et difficiles à comprendre. De plus, je m'efforce toujours de rendre tout aussi simple que possible. Si nécessaire, nous pouvons ajouter des interruptions ultérieurement.

Nous allons créer une structure qui stockera l'état de six boutons et quatre directions de la croix. Nous pouvons créer une structure avec 10 booléens, ou 10 int, ou 10 int non signé. Cependant, à la place, nous allons créer la structure en utilisant des champs de bits .

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;

Lors de la programmation de systèmes de bureau, les champs de bits sont généralement évités car ils sont mal portés sur différentes machines, mais nous programmons pour une machine spécifique et nous n'avons pas à nous en préoccuper.

Au lieu de champs, une structure de 10 valeurs booléennes d'une taille totale de 10 octets peut être utilisée. Une autre option est une uint16_t avec des macros de décalage et de masquage de bits qui peuvent définir, effacer et vérifier des bits individuels. Cela fonctionnera, mais ce ne sera pas très beau.

Un simple champ de bits nous permet de tirer parti des deux approches: deux octets de données et des champs nommés.

Démo


Nous pouvons maintenant interroger l'état des entrées à l'intérieur de la boucle principale et afficher le résultat.

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

La fonction printf utilise \ r pour remplacer la ligne précédente au lieu d'en ajouter une nouvelle. fflush est nécessaire pour afficher une ligne, car à l'état normal, elle est réinitialisée par le caractère de nouvelle ligne \ n .


Références



Partie 3: affichage


introduction


Nous devons être capables de rendre les pixels sur l'écran LCD Odroid Go.

L'affichage des couleurs à l'écran sera plus difficile que la lecture de l'état d'entrée car l'écran LCD a du cerveau. L'écran est contrôlé par ILI9341 - un pilote LCD TFT très populaire sur une seule puce.

En d'autres termes, nous parlons à ILI9341, qui répond à nos commandes en contrôlant les pixels sur l'écran LCD. Quand je dis «écran» ou «affichage» dans cette partie, je veux dire en fait ILI9341. Nous avons affaire à ILI9341. Il contrôle l'écran LCD.

SPI


L'écran LCD est connecté à l'ESP32 via SPI (Serial Peripheral Interface) .

SPI est un protocole standard utilisé pour échanger des données entre des périphériques sur une carte de circuit imprimé. Il a quatre signaux: MOSI (Master Out Slave In) , MISO (Master In Slave Out) , SCK (Clock) et CS (Chip Select) .

Un seul appareil maître sur le bus coordonne le transfert de données en contrôlant SCK et CS. Il peut y avoir plusieurs appareils sur un bus, chacun ayant ses propres signaux CS. Lorsque le signal CS de cet appareil est activé, il peut transmettre et recevoir des données.

L'ESP32 sera le maître SPI (maître) et l'écran LCD sera l'esclave SPI esclave. Nous devons configurer le bus SPI avec les paramètres requis et ajouter un écran LCD au bus en configurant les contacts correspondants.



Les noms VSPI.XXXX ne sont que des étiquettes pour les contacts dans le diagramme, mais nous pouvons parcourir les contacts eux-mêmes en regardant les parties des diagrammes LCD et ESP32.

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

Nous avons également IO14 , qui est la broche GPIO utilisée pour allumer le rétro-éclairage, et également IO21 , qui est connecté à la broche DC de l' écran LCD. Ce contact contrôle le type d'informations que nous transmettons à l'écran.

Tout d'abord, configurez le bus 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));

Nous configurons le bus à l'aide de spi_bus_config_t . Il est nécessaire de communiquer les contacts que nous utilisons et la taille maximale d'un transfert de données.

Pour l'instant, nous effectuerons une transmission SPI pour toutes les données du tampon de trame, qui est égale à la largeur de l'écran LCD (en pixels) multipliée par sa hauteur (en pixels) multipliée par le nombre d'octets par pixel.

La largeur est de 320, la hauteur est de 240 et la profondeur de couleur est de 2 octets (l'écran s'attend à ce que les couleurs des pixels aient une profondeur de 16 bits).

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

Après avoir initialisé le bus, nous devons ajouter un périphérique LCD au bus afin que nous puissions commencer à lui parler.

  • clock_speed_hz — - , SPI 40 , . 80 , .
  • spics_io_num — CS, IDF CS, ( SD- SPI).
  • queue_size — 1, ( ).
  • drapeaux - le pilote IDF SPI insère généralement des bits vides dans la transmission pour éviter les problèmes de synchronisation lors de la lecture à partir du périphérique SPI, mais nous effectuons une transmission unidirectionnelle (nous ne lirons pas à partir de l'écran). SPI_DEVICE_NO_DUMMY signale que nous confirmons cette transmission unidirectionnelle et que nous n'avons pas besoin d'insérer des bits vides.


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

Nous devons également définir les broches CC et de rétroéclairage en tant que broches GPIO. Après avoir commuté DC, le rétro - éclairage sera constamment allumé.

Équipes


La communication avec l'écran LCD se fait sous forme de commandes. Tout d'abord, nous transmettons un octet indiquant la commande que nous voulons envoyer, puis nous transmettons les paramètres de commande (le cas échéant). L'écran comprend que l'octet est une commande si le signal DC est faible. Si le signal DC est élevé, les données reçues seront considérées comme les paramètres de la commande précédemment transmise.

En général, le flux ressemble à ceci:

  1. Nous donnons un signal faible à DC
  2. Nous envoyons un octet de la commande
  3. Nous donnons un signal élevé à DC
  4. Envoyer zéro ou plusieurs octets, selon les exigences de la commande
  5. Répétez les étapes 1 à 4

Ici, notre meilleur ami est la spécification ILI9341 . Il répertorie toutes les commandes possibles, leurs paramètres et comment les utiliser.


Un exemple de commande sans paramètres est Display ON . L'octet de commande est 0x29 , mais aucun paramètre n'est spécifié pour lui.


Un exemple de commande avec des paramètres est l' ensemble d'adresses de colonne . L'octet de commande est 0x2A , mais quatre paramètres obligatoires lui sont spécifiés. Pour utiliser la commande, vous devez envoyer un signal bas à DC , envoyer 0x2A , envoyer un signal haut à DC , puis transférer les octets de quatre paramètres.

Les codes de commande eux-mêmes sont spécifiés dans l'énumération.

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;

Au lieu de cela, nous pourrions utiliser une macro ( #define SOFTWARE_RESET (0x01u) ), mais ils n'ont pas de symboles dans le débogueur et ils n'ont aucune portée. Il serait également possible d'utiliser des constantes statiques entières, comme nous l'avons fait avec les contacts GPIO, mais grâce à l'énumération, nous pouvons en un coup d'œil comprendre quelles données sont passées à une fonction ou à un membre de la structure: elles sont de type CommandCode . Sinon, il pourrait s'agir de uint8_t brut qui ne dit rien au programmeur qui lit le code.

lancement


Lors de l'initialisation, nous pouvons passer différentes commandes pour pouvoir dessiner quelque chose. Chaque commande a un octet de commande, que nous appellerons Code de commande .

Nous allons définir une structure pour stocker la commande de lancement afin que vous puissiez spécifier leur tableau.

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

  • code est le code de commande.
  • parameters est un tableau de paramètres de commande (le cas échéant). Il s'agit d'un tableau statique de taille 15, car il s'agit du nombre maximal de paramètres dont nous avons besoin. En raison de la nature statique du tableau, nous n'avons pas à nous soucier d'allouer un tableau dynamique pour chaque commande à chaque fois.
  • longueur est le nombre de paramètres dans le tableau des paramètres .

En utilisant cette structure, nous pouvons spécifier une liste de commandes de lancement.

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

Les commandes sans paramètres, par exemple SOFTWARE_RESET , définissent la liste d'initialisation sur les paramètres comme vides (c'est-à-dire avec un zéro) et la longueur définie sur 0. Les commandes avec paramètres remplissent les paramètres et spécifient la longueur. Ce serait formidable si nous pouvions définir la longueur automatiquement et ne pas écrire de nombres (au cas où nous ferions une erreur ou si les paramètres changent), mais je ne pense pas que cela en vaille la peine.

Le but de la plupart des équipes est clair d'après le nom, à l'exception de deux.

MEMORY_ACCESS_CONTROL

  • Mode paysage: par défaut, l'affichage utilise l'orientation portrait (240x320), mais nous voulons utiliser le paysage (320x240).
  • Top-Left Origin: (0,0) , ( ) .
  • BGR Panel: , BGR. , , , , .

PIXEL_FORMAT_SET

  • 16 bits per pixel: 16- .

Il existe de nombreuses autres commandes qui peuvent être envoyées au démarrage pour contrôler divers aspects, tels que le gamma. Les paramètres nécessaires sont décrits dans la spécification de l'écran LCD lui-même (et non du contrôleur ILI9341), auquel nous n'avons pas accès. Si nous ne transmettons pas ces commandes, les paramètres d'affichage par défaut sont utilisés, ce qui nous convient parfaitement.

Après avoir préparé un tableau de commandes de lancement, nous pouvons commencer à les transférer sur l'écran.

Tout d'abord, nous avons besoin d'une fonction qui envoie un octet de commande à l'écran. N'oubliez pas que l'envoi de commandes est différent de l'envoi de paramètres, car nous devons envoyer un signal faible à 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);
}

L'IDF a une structure spi_transaction_t , que nous remplissons lorsque nous voulons transférer quelque chose via le bus SPI. Nous savons combien de bits représente la charge utile et transférons la charge elle-même.

Nous pouvons soit passer un pointeur vers la charge utile, soit utiliser la structure interne struct tx_data , qui ne fait que quatre octets, mais évite au pilote d'avoir à accéder à la mémoire externe. Si nous utilisons tx_data , nous devons définir l'indicateur SPI_TRANS_USE_TXDATA .

Avant de transmettre des données, nous envoyons un signal bas au DC , indiquant qu'il s'agit d'un code de commande.

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

La transmission de paramètres est similaire à l'envoi d'une commande, mais cette fois-ci, nous utilisons notre propre tampon ( données ) et envoyons un signal élevé à DC pour indiquer à l'écran que les paramètres sont transmis. De plus, nous ne définissons pas l'indicateur SPI_TRANS_USE_TXDATA car nous transmettons notre propre tampon.

Ensuite, vous pouvez envoyer toutes les commandes de lancement.

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

Nous parcourons de manière itérative le tableau des commandes de lancement, en passant d'abord le code de commande, puis les paramètres (le cas échéant).

Dessin de cadre


Après avoir initialisé l'affichage, vous pouvez commencer à dessiner dessus.

#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 a la possibilité de redessiner des parties individuelles de l'écran. Cela peut être utile à l'avenir si nous remarquons une baisse de la fréquence d'images. Dans ce cas, il sera possible de mettre à jour uniquement les parties modifiées de l'écran, mais pour l'instant nous allons simplement redessiner tout l'écran à nouveau.

Pour rendre un cadre, il faut définir une fenêtre de rendu. Pour ce faire, envoyez la commande COLUMN_ADDRESS_SET avec la largeur de la fenêtre et la commande PAGE_ADDRESS_SET avec la hauteur de la fenêtre. Chacune des commandes prend quatre octets du paramètre qui décrivent la fenêtre dans laquelle nous allons effectuer le rendu.

UPPER_BYTE_16 et LOWER_BYTE_16- Ce sont des macros auxiliaires pour extraire les octets haut et bas d'une valeur 16 bits. Les paramètres de ces commandes nous obligent à diviser la valeur 16 bits en deux valeurs 8 bits, c'est pourquoi nous le faisons.

Le rendu est initié par la commande MEMORY_WRITE et envoie à l'affichage tous les 153 600 octets du tampon de trame à la fois.

Il existe d'autres façons de transférer le tampon de trame vers l'affichage:

  • Nous pouvons créer une autre tâche FreeRTOS (tâche), qui est responsable de la coordination des transactions SPI.
  • Vous pouvez transférer un cadre non pas en un, mais en plusieurs transactions.
  • Vous pouvez utiliser une transmission non bloquante, dans laquelle nous initions l'envoi, puis continuons à effectuer d'autres opérations.
  • Vous pouvez utiliser n'importe quelle combinaison des méthodes ci-dessus.

Pour l'instant, nous allons utiliser le moyen le plus simple: la seule transaction de blocage. Lorsque DrawFrame est appelé, le transfert vers l'affichage est lancé et notre tâche est suspendue jusqu'à ce que le transfert soit terminé. Si plus tard, nous découvrons que nous ne pouvons pas atteindre une bonne fréquence d'images avec cette méthode, nous reviendrons sur ce problème.

RGB565 et ordre des octets


Un affichage typique (par exemple, le moniteur de votre ordinateur) a une profondeur de 24 bits (1,6 million de couleurs): 8 bits par rouge, vert et bleu. Le pixel est écrit en mémoire sous la forme RRRRRRRRGGGGGGGGGBBBBBBBBB .

L'écran LCD Odroid a une profondeur de 16 bits (65 000 couleurs): 5 bits de rouge, 6 bits de vert et 5 bits de bleu. Le pixel est écrit en mémoire sous la forme RRRRRGGGGGGGBBBBB . Ce format est appelé RGB565 .

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

Définissez une macro qui crée une couleur au format RGB565. Nous lui passerons un octet de rouge, un octet de vert et un octet de bleu. Il prendra les cinq bits de rouge les plus significatifs, les six bits de vert les plus significatifs et les cinq bits de bleu les plus significatifs. Nous avons choisi des bits hauts car ils contiennent plus d'informations que des bits bas.

Cependant, l'ESP32 stocke les données dans l'ordre Little Endian , c'est-à-dire que l'octet le moins significatif est stocké dans l'adresse mémoire inférieure.

Par exemple, la valeur 32 bits [0xDE 0xAD 0xBE 0xEF] sera stockée en mémoire en tant que [0xEF 0xBE 0xAD 0xDE] . Lors du transfert de données vers l'affichage, cela devient un problème car l'octet le moins significatif sera envoyé en premier et l'écran LCD s'attend à recevoir l'octet le plus significatif en premier. Définir la

macro SWAP_ENDIAN_16pour échanger des octets et l'utiliser dans la macro RGB565 .

Voici comment chacune des trois couleurs primaires est décrite dans RGB565 et comment elles sont stockées dans la mémoire ESP32 si vous ne changez pas l'ordre des octets.

Rouge

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

Vert

00000 | 111111 | 00000? -> 00000111 11100000 -> 11100000 00000111

Bleu

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

Démo


Nous pouvons créer une démo simple pour regarder l'écran LCD en action. Au début du cadre, il vide le tampon de cadre au noir et dessine un carré de 50x50. Nous pouvons déplacer le carré avec une croix et changer sa couleur avec les boutons A , B et Démarrer .

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

Nous allouons le tampon de trame en fonction de la taille totale de l'affichage: 320 x 240, deux octets par pixel (couleur 16 bits). Nous utilisons heap_caps_malloc afin qu'il soit alloué en mémoire, ce qui peut être utilisé pour les transactions SPI avec accès direct à la mémoire (DMA) . Le DMA permet aux périphériques SPI d'accéder au tampon de trame sans nécessiter l'intervention du processeur. Sans DMA, les transactions SPI prennent beaucoup plus de temps.

Nous n'effectuons pas de vérifications pour nous assurer que le rendu ne se produit pas en dehors des bordures de l'écran.


Un fort déchirement est perceptible. Dans les applications de bureau, la méthode standard pour éliminer les déchirures consiste à utiliser plusieurs tampons. Par exemple, lors d'une double mise en mémoire tampon, il y a deux tampons: les tampons avant et arrière. Pendant que le tampon avant est affiché, l'enregistrement est effectué
à l'arrière. Ensuite, ils changent de place et le processus se répète.

L'ESP32 n'a pas assez de RAM avec des capacités DMA pour stocker deux tampons de trame (4 Mo de RAM SPI externe, malheureusement, n'a pas de capacités DMA), donc cette option ne convient pas.

ILI9341 a un signal ( TE ) qui vous indique quand VBLANK se produit afin que nous puissions écrire sur l'écran jusqu'à ce qu'il soit dessiné. Mais avec Odroid (ou le module d'affichage), ce signal n'est pas connecté, nous ne pouvons donc pas y accéder.

Peut-être pourrions-nous trouver une valeur décente, mais pour l'instant nous ne le ferons pas, car maintenant notre tâche consiste simplement à afficher les pixels à l'écran.

La source


Tout le code source peut être trouvé ici .

Références



All Articles