[Doom sur STM32] Compilation et debug - Episode #1

Où l'on parle de la compilation du projet.

Après quelques manips sur l'USB, je cale devant la gestion des HUBs. Ce n'est pas demain la veille que j'arriverai à brancher clé USB + clavier + souris sur cette carte. Je vais donc laisser ce sujet décanter, et creuser une autre approche : gérer les contrôles en direct.

Récapitulons : stm32doom propose comme périphérique de contrôle l'écran tactile et le bouton user pour tirer. C'est bien entendu totalement injouable, et c'était clairement un moyen d'avoir un truc fonctionnel rapidement. Je ne saurais me contenter de ce pis-aller, et je souhaite implémenter de vrais contrôles. Voici les différentes options que j'ai identifié:

  • Gérer les hubs USB et connecter clavier et souris sur le port USB OTG. Comme dit précédemment, j'ai creusé ça et pour le moment je suis au point mort sur un problème insoluble qu'il faut que je prenne du recul (et puis ça m'a un peu gonflé aussi).
  • Faire des contrôles custom, connectés en GPIO, ADC, SPI, I²C, UART, whatever.
  • Utiliser d'autres cartes qui feront la conversion USB vers UART / SPI / I²C / whatever. Chaque carte implémente un host USB "standard" et transmet les datas sur un bus plus simple, comme ça la carte principale n'a que la clé à gérer sur l'USB et on peut garder le driver actuel.
  • Pour la souris, utiliser une souris PS/2, étant donné que ce protocole est assez proche d'une SPI classique (il y a un signal de clock et un signal de data, la souris est master).
  • Pour la souris, en bricoler une et récupérer à l'intérieur un signal intermédiaire entre le capteur et le contrôleur USB (si ce signal existe ...).

Je pense faire un système hybride : contrôleurs custom pour le clavier (c'est très facile à faire), avec un joystick en analogique (avec des potars, comme ça je le gère avec des ADCs) et une souris PS/2 ou bricolée.

Mais pour avancer sur cela, il va falloir que j'injecte des modifs dans le code de stm32doom. Et donc que j'arrive à compiler la version existante, et surtout debugger, sinon ça va juste être ingérable.

Au passage, ne pas hésiter à lire l'incroyable Black Book de Fabien Sanglard sur Wolfenstein 3D et, surtout, pour ce qui nous occupe, Doom.

  1. Débugger un projet non-CubeIDE dans CubeIDE

Le projet fourni avec stm32doom est prévu pour être compilé en ligne de commande. Je ne vous raconte pas l'enfer que c'est pour moi vu que je déteste GCC et la compilation en ligne de commande d'une façon générale. J'ai donc importé le projet dans CubeIDE, et après quelques manips j'ai réussi à le configurer correctement pour qu'il compile. Par contre, je n'arrive pas à lancer le debug, je n'arrive pas à configurer le projet pour qu'il arrive à lancer une session de debug. On va voir donc ici comment on peut s'y prendre.

Quand on veut ouvrir un projet avec CubeIDE, on peut:

  • L'importer tel quel s'il a des fichiers de config / makefiles qu'Eclipse est capable de comprendre,
  • Créer un projet dans Eclipse et copypasta les fichiers source et laisser Eclipse refaire les liens.

Dans le premier cas, il y a plein de configs qui peuvent ne pas s'importer correctement, typiquement chez moi avec stm32doom la config de la cible pour le debug. Or, Eclipse est très relou, sur un projet importé les menus de propriétés du projet n'ont pas les meêmes onglets qu'un projet Cube, et je n'arrive pas à trouver où on précise la cible et la config de debug. Donc, il me dit qu'il ne trouve aps la config de debug, mais je ne trouve pas où il faut mettre la config de debug. Ca se mord la queue quand-même. Bon, c'est aussi parce que je ne connais pas bien Eclipse.

Autre approche : créer un projet Cube "vierge" et importer les sources dedans. Dans ce cas on peut créer un projet "standard" avec une config déjà présente et du code généré par CubeMX, ou bien faire un projet vide. Je vais commencer par tester la deuxième option, qui devrait poser moins de problèmes d'intégration.

Projet vierge et import des sources

Lorsqu'on met l'option "Project : Empty" sur un nouveau projet CubeIDE, cela créé tout de même quelques fichiers : un main.c avec une fonction main() vide, syscalls.c et sysmem.c et des fichiers de config / startup. Et lorsqu'on le build, ça build quelque chose, et surtout le debug est configurer correctement, ce qui est un bon début. Maintenant, intégrons les fichiers stm32doom dedans.

En-dehors de /src, il y a un répertoire /lib avec les libs STM32, USB et fatfs, et un répertoire /img avec les images à afficher pour l'USB. On copie tout ça tel-quel. Dans /src on a le répertoire /chocdoom que l'on copie tel-quel, ainsi que le reste des fichiers. Et là il faut faire attention car stm32doom a aussi des fichiers syscalls.c et main.c. L'on va donc éviter de les écraser et regarder dans le détail ce qu'ils contiennent.

Le main.c est assez straightforward, vu que celui généré par Cube est quasiment vide, on va juste recopier le contenu de l'autre, en gardant peut-être le #if !defined(__SOFT_FP__) && defined(__ARM_FP) même si je doute de l'utilisé de ce bout de code. Il n'y a pas de main.h généré pas Cube, donc on le copie tel quel.

Syscalls.c est plus compliqué. Y sont définies une série de fonctions d'appels système, ainsi que la HEAP, et il n'y a rien en commun entre celles générées par Cube et celles de stm32doom. L'on va donc évaluer le niveau d'importance de ces fonctions en regardant si elles sont effectivement appelées.

Dans syscalls.c est définit le tableau static char heap[HEAP_SIZE]; qui sert à attribuer en mémoire la heap. Pour rappel, heap et stack sont deux espaces mémoires en RAM qui sont nécessaires pour le fonctionnement des programmes. La stack est l'espace où sont stockées les variables globales (qui ne sont pas définies explicitement dans le fichier de mapping mémoire comme les variables globales, qui elles ont une adresse fixe définie à la compilation). La heap est un espace mis à disposition de l'utilisateur pour faire de l'allocation dynamique de mémoire, typiquement des malloc / calloc.

Un peu d'info ici : https://www.guru99.com/stack-vs-heap.html

Ici, est créé un pointeur vers ce tableau : \*curbrk. Également, l'on va tester l'occupation de la heap dans la fonction _sbrk_r. Rien de folichon. SI l'on recherche _cur_brk pareil : on ne le trouve appelé que dans syscalls.c, et visiblement principalement pour tester si la heap est full. Je suis très fortement tenté de merger les deux fichiers, à tout hasard.

Compilation : il ne trouve pas les fichiers .h, ce qui est normal, car arrive la partie chiante de l'import d'un projet : la définition des chemins d'include. Toujours pénible, mais bon ça se fait. il faut aller dans les options du projet, C/C++ General/Paths and symbols/Includes et ajouter tous les répertoires qui contiennent des fichiers .h. C'est ultra-chiant, mais bon, ça se fait. Par souci de "propreté", je vais prendre tous les fichiers .h dans /src et les mettre dans /inc, puisque c'est l'archi proposée par défaut. J'ajoute donc:

  • lib\fatfs
  • lib\stm32
  • lib\usb
  • Src\chocdoom

Compilons à nouveau : il y a un #include "SDL.h" dans i_videohr.c. Grmpf, ça sent l'oubli, étant donné que le code de Chocolate Doom a dû être inclus sans trop de délicatesse, il doit rester des scorie, comme ici l'inclusion de la lib SDL. Librairie que l'on ne va bien entendu pas utiliser ici, puisqu'il s'agit d'une librairie pour faire le lien avec l'OS. Je suis pourtant assez surpris, car je n'ai pas cette erreur en compilant stm32doom en direct ...

Cela s'explique assez bien : dans le makefile de stm32doom, ce fichier n'est pas présent, il n'est pas compilé, et donc ignoré. Ce qui ne me surprend pas, c'est le module qui gère les fonctions d'affichage, qui sont ici remplacées par les fonctions bas-niveau pour dessiner sur le LCD. L'on va donc check ce qui est fait des fonctions qu'il contient, pas acquis de conscience : aucune des fonctions de ce module ne sont appelées ailleurs dans le projet, ce sont donc des fonctions morte. L'on peut donc sans risque exclure ce fichier de la compilation.

Ensuite on a des messages d'erreurs qui apparaissent dans les drivers bas-niveau. Exemple dans stm32f4xx.h, il me dit qu'il faut sélectionner le target ("Please select first the target STM32F4xx device used in your application (in stm32f4xx.h file)"), ce qui ici se traduit concrètement par la définition d'une variable d'environnement. Or, sans surprise, entre GCC en makefile et CubeIDE, les variables d'environnement ne sont pas définies avec les mêmes noms. Sinon, ça serait trop facile. On y trouve "STM32", "STM32F4", mais pas "STM32F4XX" par exemple ... L'on va donc regarder le makefile de stm32doom et regarder ce qui manque comme switches:

DEFINES = -DDEBUG_ -DSTM32F4XX -DUSE_STDPERIPH_DRIVER -DSTM32F429_439xx

L'on définit ici les switches de compilation, et l'on retrouve STM32F4XX et STM32F429_439 qui sont utilisés dans stm32f4xx.h pour activer les bouts de code correspondant au target. Allons donc ajouter manuellement ces switches dans C/C++ General/Paths and symbols/Symbols (attention : bien y ajouter dans "GNU C", pas dans "Assembly").

Passé ça, j'ai des problèmes de mapping ("section `.sdram' will not fit in region `RAM'") et de fonctions qu'il n'arrive pas à linker. Je suspecte que le link lui fait jeter des fonctions qu'il ne trouve pas par la suite, donc on va tâcher de régler le problème de mapping avant de dépiler le reste.

L'on va comparer le fichier .ld de stm32doom avec celui généré par CubeIDE. Dans stm32doom il n'y a qu'un seul fichier memory.ld, alors que Cube en génère deux, une pour la RAM et un pour la flash. Ca ne change pas grand chose, le contenu des deux est pris en compte et mergé.

stm32doom définit les trois zones de mémoires principales : ROM (la flash), RAM (la RAM interne du MCU) et SDRAM (la RAM externe):

MEMORY
{
    rom (rx)	: ORIGIN = 0x08000000, LENGTH = 2048K
    ram (rwx)   : ORIGIN = 0x20000000, LENGTH = 256K
    sdram (rwx) : ORIGIN = 0xD004B000, LENGTH = 7892K /* first 300K is used as LCD frame buffer */
}

L'on retrouve les 2MB de flash et les 256kB de RAM, ainsi que les 8MB (64Mb) de la RAM externe. Pour rappel, dans les fichiers .ld les tailles sont données en octets, pas en bits.

Si on regarde dans les fichiers .ld de Cube, on voit qu'il définit un espace mémoire différent:

MEMORY
{
 RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 192K
 FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 2048K
}

On a bien les 2MB de flash, mais il ne définit que 192kB sur la RAM, et l'on remarque aussi qu'il définit la taille de la heap et de la stack de façon explicite:

_Min_Heap_Size = 0x200;	/* required amount of heap  */
_Min_Stack_Size = 0x400;	/* required amount of stack */

Ce qui fait 512 octets pour la heap, et 1024 octets pour la stack. Dans stm32doom, il se content de définir qu'il y a une section "heap", mais sans ne préciser la taille. D'ailleurs, dans syscalls.c il était précisé la section de mémoire ainsi que la taille:

#define HEAP_SIZE		0x00700000 // 7 MB
__attribute__ )
static char heap[HEAP_SIZE];

Ici la heap est donc assignée dans la SDRAM externe, et fixée à 7MB. Ceci n'est pas surprenant : Doom va faire des allocations mémoires pour charger / décharger des données, donc il faut lui allouer de la place pour le faire.

D'ailleurs, cela me fait réaliser que je me suis visiblement fourvoyé sur le fonctionnement de cette implémentation : stm32doom ne va sans doute pas charger l'intégralité du WAD en RAM, mais charger dynamiquement dans la heap ... à moins qu'il ne charge dynamiquement l'intégralité du WAD dans la heap au démarrage ? Bon, ça ne change pas grand chose, de toutes façons avec 7MB on ne va pas loin. Et si je change la RAM externe, c'est dans ces environs qu'il faudra que je modifie des choses pour adapter à la nouvelle taille disponible.

Bon, j'ai une solution simple pour avancer : supprimer les ficheirs générés par Cube et utiliser à la place le memory.ld de stm32doom. Essayons ça.

Toutes les erreurs précédentes ont disparu :) Ça plante maintenant car le linker ne trouve pas i_video.o, ce qui sous-entend que i_video.c n'a pas été compilé correctement, ce qui n'est peut-être pas étonnant vu que je viens d'exclure _\videohr. Check dans le makefile : il est présent dans la compilation. Ah ... Donc ce n'est pas ça. Tiens, en lmisant le message dans le détail, je remarque qu'il cherche "bin/chocdoom/i_video.o" ... Or, le répertoire /bin n'existe pas, les objets sont créés dans /Debug. Pourquoi va-t-il chercher ce fichier ici ?

La réponse est dans le memory.ld : il y a des objets qui sont affectés de force à des sections mémoire, et c'est fait en donnant les chemins d'accès des fichiers objet en dur:

/* Section Definitions */ 
SECTIONS 
{	
/* external SDRAM */
.sdram (NOLOAD) :
{
. = ALIGN(4);
*(.sdram .sdram.*)
bin/chocdoom/i_video.o(COMMON)
bin/chocdoom/r_bsp.o(COMMON)
bin/chocdoom/w_wad.o(COMMON)
bin/chocdoom/r_main.o(COMMON)
bin/chocdoom/r_plane.o(COMMON)
} > sdram

Il faut donc que je modifie les chemins d'accès pour que ça corresponde à l'archi de Cube :

/* Section Definitions */ 
SECTIONS 
{	
/* external SDRAM */
.sdram (NOLOAD) :
{
. = ALIGN(4);
*(.sdram .sdram.*)
Debug/Core/chocdoom/i_video.o(COMMON)
Debug/Core/chocdoom/r_bsp.o(COMMON)
Debug/Core/chocdoom/w_wad.o(COMMON)
Debug/Core/chocdoom/r_main.o(COMMON)
Debug/chocdoom/r_plane.o(COMMON)
} > sdram

Je serais curieux de savoir s'il n'y a pas une façon plus élégante de faire ça, mais bon, on verra plus tard.

Nouveau tombereau d'erreurs, plein de références qu'il n'arrive pas à résoudre. Entre autres dans un fichier de startup. Mais ce fichier n'est pas censé être utile pour stm32doom, vu que c'est un fichier généré par Cube. Excluons-le du projet : la compil aboutit :) :) :)

Essayons le debug ... Ca lance la session, mais quand je fais run il perd la comm :( "Failed to read target status Debugger connection lost. Shutting down..." Hum ... Il n'a pas l'air d'avoir chrgé de code. Et le binaire fait 66ko ... Et à la sortie de la compilation il dit avoir rempli 8 octets dans les sections ... WTF ? Ya un truc qui foire. J'ai comme l'impression qu'il génère les objets, qu'il fait le lnk, mais qu'il ne remplit pas le binaire. Dans le .map toutes les adresses sont à 0. Ça sent le problème de mapping. Je pense que je vais changer mon fusil d'épaule, reprendre les .ld de Cude et juste ajouter la section sdram.

En reprenant juste les fichiers fournis par Cube : à peine mieux. En fait j'ai l'impression que c'est le fichier de startup qui permet d'accrocher les sections, parce qu'il y a des .text dedans, et donc l'avoir enlevé fait qu'il n'ajoute plus les sections ... Grmpf ...

Comment faire alors ? Et surtout comment s'en sort le projet d'origine ?

Ok ok ok, je vais reprendre les .ld de Cube, ajouter la section SDRAM, et remettre le fichier de startup, et voir ce qu'on arrive à faire. Compilation : "`.sdram' will not fit in region `RAM'" grmpf ...

Bon, re-checkons les messages d'erreur. "ld.exe: warning: cannot find entry symbol Reset_Handler; defaulting to 0000000000800000" Ah ...

Toujours dans le .map, on peut voir où il essaye de mettre les fonctions. Or, si on regarde un fichier généré sur un projet vide pas touché, on trouve le vecteur d'interruption (.isr_vector), qui est le premier artefact qui va être mis au début de la flash, à l'adresse 0x0000000008000000. Ce qui colle avec l'adresse donnée dans le .ld.

Si on regarde côté fichier de startup, de quoi parle-t-on ? En fait il s'agit d'un fichier en assembleur qui va faire l'init très très bas-niveau du MCU. Entre autres, il va faire les initialisations des variables, l'initialisation du bas-niveau encore plus bas que les drivers, et lancer le main. Dans ce fichier on dit explicitement quelles instructions vont dans quelles sections de la mémoire. Il y a des labels, un des premiers est Reset_Handler, et on voit les initialisations ultra-basiques, genre la stack, lancer l'init des horloges, copie des inits des variables.

Reset_Handler:
 ldr   r0, =_estack
 mov   sp, r0          /* set stack pointer */
/* Call the clock system intitialization function.*/
 bl  SystemInit

/* Copy the data segment initializers from flash to SRAM */
 ldr r0, =_sdata
 ldr r1, =_edata
 ldr r2, =_sidata
 movs r3, #0
 b LoopCopyDataInit

Etc.

Et ça appelle des fonctions qui sont dans d'autres modules. Et le problème que j'ai, c'est que visiblement il n'arrive pas à trouver les fonctions alors qu'elles existent et que la compilation de ces fonctions se passe bien par ailleurs.

Or, dans stm32doom, il n'y a pas de fichier d'init en assembleur, tout est lancé dans le main.c. Et donc c'est sans doute pour ça que ça coince, il y a conflit entre l'init du main et l'init du startup.

Et ce que je remarque aussi, c'est que le main.c appelle aussi ces fonctions qu'il n'arrive pas à trouver. Et donc je suspecte qu'en fait c'est pare qu'il n'arrive pas à mapper les fonctions qu'il ne remplit pas le binaire. Ce qui est très étrange parce que je ne comprend pas pourquoi ça ne provoque pas d'erreur à la compilation ... Visiblement en cherchant un peu sur le net il semble bien que ça soit lié au fait qu'Eclipse n'arrive pas à trouver le point d'entrée. Je vais me focaliser sur la fonction SystemInit() qui semble un bon indicateur.

Au passage,je ne comprend pas où est-ce qu'il gère le fichier de mapping de la RAM. Parce qu'il n'y a qu'un seul champ dans les options du linker où l'on peut mettre un lien vers un fichier .ld, et il est pris par celui de la flash.

Tout cela est très confus, et très frustrant ...

HAN ! En creusant encore sur google, j'ai fini par trouver ça : https://stackoverflow.com/questions/50709778/stm32-gnu-arm-linker-undefined-reference-to-function-with-eclipse

Parmi les nombreuses possibilités envisagées, ils évoquent le fait que les répertoires peuvent être marqués "exclude from build". Et ... c'était le cas du répertoire /stm32 ! Je ne m'y étais pas attendu parce que je pensais que cette propriété ne pouvait être ajoutée que manuellement, mais visiblement non. Donc j'ai check tous les répertoires, et visiblement tous ceux qui ne sont pas dans /Src ou /Inc sont marqués "exclude from build par défaut", et il faut décocher manuellement. Bizarre ... Je clean / refresh / build ... Et ça me gnère toujours un exécutable vide. Par contre je vois la fonction SystemInit qui n'apparaissait pas dans le .map avant, on avance, mais qui est indiquée à l'adresse 0. Par contre le fichier .lst est encore plus vide que d'habitude. Et si je remet le fichier assembleur d'init, il me met un autre message d'erreur : undefined reference to `_init'. Ça se complique salement ...

Ça me gonfle, je laisse de côté, je change mon fusil d'épaule (encore).

A l'aveugle ?

J'ai aussi la possibilité de ne pas debug en direct, mettre au point les fonctionnalités sur un projet spécifique, puis importer le tout dans le projet stm32doom et tester sans debug. Ça serait tellement casse-burnes ...

Import d'un projet makefile

En farfouinant sur google, j'ai trouvé un cas semblable au mien : https://community.st.com/s/question/0D70X0000075KOD/detail

Et un "tutorial" assez détaillé ici : https://github.com/ethanhuanginst/STM32CubeIDE-Workshop-2019/tree/master/hands-on/02_stm32_hp141_lcd

Il parle d'ajouter une "nature" de projet; Et il ne comprend pas plus que moi ce que ça veut dire ... Et il faut ajouter le target à la main dans le fichier .cproject.

... Mais ça ne marche pas, le bouton Debug ne se dé-grise pas.

Ça me gonfle à un point ...

https://community.st.com/s/question/0D50X0000AyBp0K/is-it-possible-to-debug-a-makefile-project-into-stm32cubeide

Ok, on essaye openOCD, pas mieux.

Je commence à me demander si je n'ai pas tout simplement un problème d'hygiène de mon workspace. Ce sont des petites choses fragiles, et à force de bricoler 46 projets dessus, il y a peut-être des chances qu'il commence à se mélanger les pinceaux.

Là aussi je sens que je perds mon temps sur des problèmes sans intérêt.

Bricolage dégueulasse

Une troisième solution est d'envoyer se faire foutre tout ce petit monde, cube et le dev de stm32doom alike, et faire l'intégration au forceps, en envoyant valdinguer l'architecture. Au programme:

  • Base d'un projet cube managé avec CubeMX et tout le merdier, pour être sûrs que ça compile et que ça debug,
  • Nique sa race les middleware allemand chelou, je prend les couches immondes de ST, peste et choléra.

La rage commence à se faire sentir.

Donc, il va falloir "extraire" la conf bas-niveau pour la reproduire. Pour ça on regarde ce qu'il se passe à l'init du main:

 int main(void)
{
uint32_t start;
usb_msc_host_status_t usb_status;

SystemInit ();
hw_init ();
sdram_init ();
debug_init ();

printf ("\n\033[1;31m\r\nSTM32Doom\033[0m\n");

Visiblement SystemInit() va juste configurer horloges et reset. Il faut les valeurs des PLLs. Visiblement ils prennent les valeurs "par défaut", appelées dans les fonctions d'iit standard, et ils n'écrivent pas dans les registres manuellement. Au début du fichier system_stm32f4xx.c il y a les confs par défaut par famille de processeur. Pour le notre on rgarde celle de STM32F42xxx : AHB = 1, APB1 = 4, APB2 = 2, PLL M = 8, PLL N = 336, PLL P = 2, PLL Q = 7. Avec le quartz à 8MHz en entrée ça donne SYSCLK = 168MHz, PCLK1 = 42MHz, PCLK2 = 84MHz, APB1 timers = 84MHz, APB2 timers = 168MHz, et on a bien 48MHz pour l'USB. Pour la config de la clock du LCD c'est moins évident, il n'y a pas de commentaire explicite, mais en farfouinant dans stm32f4xx_rcc.c on trouve un RCC->PLLSAICFGR = 0x24003000; ce qui donne : N = 192, Q = 4, R = 2, ce qui donne 48MHz pour l'horloge du LCD, ce qui semble cohérent.

Ensuite hw_init() configure le tick système:

static void hw_init (void)
{
/* configure SysTick for 1 ms interrupt */
if (SysTick_Config (168000000 / 1000))
{
/* capture error */
while (1);
}

/* configure the SysTick handler priority */
NVIC_SetPriority (SysTick_IRQn, 0x0);
}

Il est mis à 1ms via une fonction "core" de core_cm4, no sweat, on recopie tel quel. Pour rappel, il s'agit d'une interruption de timer qui tombe régulièrement et incrémente un compteur qui permet d'avoir une sorte de "temps système" qui évolue en permanence, ce qui sert à faire du scheduling (et ça fait un compteur généraliste qui est à disposition ).

sdram_init () fait l'initialisation de la SDRAM externe, avec config des IOs et du module FMC. Dans les projets Cube c'est fait dans les fonctions MspInit dans stm32f4xx_hal_msp.c, ici HAL_LTDC_MspInit(), et dans le main avec la fonction MX_FMC_Init(). Je vais faire confiance à Cube pour avoir configuré ça correctement, on checkera plus tard si ça donne l'impression de foirer.

debuginit () va configurer un UART pour du debug, sur les pins PA9 et PA10, ce qui correspond à USART1, qui est configuré par défaut par Cube, heureuse coïncidence, et visiblement avec les mêmes paramètres (115200, 8b ...), donc je laisse MXUSART1UARTInit() tel quel. Le module debug qu'il a ajouté n'apporte pas grand chose, juste un init et une fonction debug_chr, qui est utilisée dans les fonctions système "customisées" de syscalls.c. Il fait ensuite un printf, qui va sans doute écrire sur ce port, il faut donc vérifier que le printf est bien overload correctement ... Grmpf, j'aime pas ça, trop implicite, amis bon, ça me forcera à mieux comprendre comment ça marche.

Suite du main:

spi_init ();
i2c_init ();
lcd_init ();
gfx_init ();

spi_init() va faire l'init ... de la SPI. Sur cette carte, en SPI et I²C on a:

  • Une liaison SPI vers l'accéléromètre L3GD20
  • Une I²C vers le contrôleur du touchscreen STMPE811QTR
  • Une liaison SPI vers le LCD

Vu les connections (PF7, PF8, PF9) ça va vers le LCD, clairement. La SPI5 est configurée dans le programme d'exemple de Cube, on ne touche à rien.

i2c_init() pour l'I²C, vers le contrôleur touchscreen. Je vais l'activer pour ocmmencer, mais je le virerai à terme. L'I2C3 est déjà configuré dans l'exemple, je ne touche à rien.

Après avoir initialisé la SPI du LCD et du touchscreen, on pase à lcd_init(), qui comprend plusieurs sous-fonctions:

lcd_init_io ();
lcd_init_controller ();
lcd_init_ltcd ();
lcd_layer_fullscreen ();

lcd_init_io() va configurer les pins CS et WRX en C2 et D13, qui sont des GPIO, déjà configurées par le bas niveau. Puis lcd_init_controller() va envoyer des commandes SPI pour initialiser le LCD. Puis lcd_init_ltcd() initialiser le contrôlleur LCD du MCU (LTDC, j'ai cru à une faute de frappe au début, mais c'est vraiment le nom du module). Puis on configure un fonctionnement "full screen" avec lcd_layer_fullscreen (). Je vais devoir récupérer ces fonctions pour que l'initialisation du LCD se passe bien.

Puis gfx_init () va juste appliquer un écran blanc:

gfx_clear_screen (RGB565_WHITE);

Puis on affiche l'image "Loading ...":

show_image (img_loading);

show_image() est une fonction statique du main qui permet d'envoyer un fichier PNG vers les couches gfx poiur y afficher sur l'écran.

Suite du main:

fatfs_init ();
usb_msc_host_init ();
led_init ();
touch_init ();
button_init ();

fatfs_init () va initialiser le système de fichiers. Elle fait:

SDCard_Init ();
USBDisk_Init ();

Je ne comprend pas trop l'intérêt de l'init du SDCard, mais je suppose que c'est une procédure générique pour identifier s'il y a un contrôleur SDcard, et s'il n'en trouve pas il se tourne vers l'USB ... Mais USBDisk_init est vide ... Bizarre ...

Ensuite on initialise l'USB host avec usb_msc_host_init (). Il faut que je checke comment remplacer cette fonction par les fonctions ST.

led_init () configure PG13, qui est connecté à LED3. Le module LED permet de faire ON/OFF sur cette LED. Enfin un module qui n'est pas trop compliqué pour moi !

touch_init () va initialiser le contrôleur touchscreen, entre autre en configurant les entrée pour l'interruption externe (PA15) et en envoyant de la com pour configurer via I²C. le module touchscreen gère la détection de la position du (des ?) doigts sur le touchscreen et fait tout le leg-work pour le bas niveau. Je vais tâcher de le garder à court-terme, mais c'est le premier module que je vais remplacer.

button_init () configure PA0 qui est le "blue push button" et qui est utilisé pour tirer.

Suite du main:

start = systime;
usb_status = USB_MSC_DEV_CONNECTED;

while (systime - start < 20000)
{
 usb_status = usb_msc_host_main ();
	
 if (usb_status == USB_MSC_DEV_CONNECTED)
	{
 break;
	}
}

Ici on va détecter si une clé USB est connectée, de façon assez ... brutale :p On fait un while, avec une condition déchappement au bout de 20000 ticks système (ce qui va faire 20 vu que le systick est à 1ms). Dans la boucle on appelle usb_msc_host_main (), qui va lancer la fonction de tâche de fond de l'USB MSC:

usb_msc_host_status_t usb_msc_host_main (void)
{
if (usb_msc_host_status != USB_MSC_DEV_NOT_SUPPORTED)
{
USBH_Process (&USB_OTG_Core, &USB_Host);
}

return usb_msc_host_status;
}

L'on remarque que malgré le fait qu'on appelle la fonction générique USBH_Process() on est dans un module "MSC", donc on s'attend à ne gérer que du stockage de masse (clé USB). Or donc, on appelle cette fonction en boucle, jusqu'à avoir un statut "connected" ou que 20 secondes aient passé (pour timeout). On teste le statut:

if (usb_status != USB_MSC_DEV_CONNECTED)
{
 show_image (img_no_usb);
 fatal_error ("USB not connected\\n");
}

Si on a pas connecté un device (et donc qu'on a passé le timeout de 20s), on affiche l'image "no USB disk found ..." et on déclenche le process erreur fatale. Comment dire ... On pourrait faire plus intelligent, mais bon, c'est un POC, hein, on ne va pas se plaindre de tout non plus. Suite :

if (fatfs_mount (DISK_USB))
{
printf ("USB mounted\n");
}
else
{
show_image (img_no_usb);
fatal_error ("USB mount failed\n");
}

On essaye de monter le système de fichier sur la clé, si ça échoue : "no USB found" et erreur fatale, sinon on passe à la suite:

ledset (LED_GREEN, LED_STATEON);

D_DoomMain ();

while (1)
{
}

On allume la LED verte et on lance Doom !

Comment intégrer ça ?

Deux approches possibles, deux niveaux de greffe sauvage:

  • Garder le bas-niveau de stm32doom en gardant le strict minimum du code généré par Cube
  • Remplacer tous les drivers de stm32doom par ceux de Cube.

La première solution peut être facile à faire, "yaka" comenter toutes les fonctions d'init de cube et tout remplacer par le code du main de stm32doom. Je pense qu'il n'y a que l'init système que je ne peux pas reprendre "tel quel" pour ne pas que ça foute la merde. Bon, en vrai c'est difficile de faire le tri entre ce qui est nécessaire et de gérer les conflits. Donc je vais plutôt partir sur la deuxième : je dégage tous les drivers et les libs de stm32doom et je mets celles de cube. Jusqu'à ce que ça marche.

Et bien allons-y !

button.c : tout ce qui est structures et fonctions bas-niveau ont des noms différents. Exemple, ce bout de code:

/* enable GPIO clock */
RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOA, ENABLE);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init (GPIOA, &GPIO_InitStructure);

En allant chercher dans stm32f4xx_hal_gpio.h on retrouve les éléments équivalents, et le bloc devient:

__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Pin = GPIO_PIN_0;
HAL_GPIO_Init (GPIOA, &GPIO_InitStructure);

On remplace la fonction d'activation de la clock par la macro HAL kivabien, et on va retrouver les éléments équivalents du type GPIO_InitTypeDef. A noter que l'élément GPIO_OType est regroupé avec GPIO_Mode, et les deux sont remplacés par l'élément Mode. De même, dans la fonction qui va lire la pin:

return GPIO_ReadInputDataBit (GPIOA, GPIO_PIN_0);

La fonction GPIO_ReadInputDataBit() renvoie un booléen qu'on va directement donner en retour de la fonction. Dans le HAL la fonction équivalente est HAL_GPIO_ReadPin(), qui renvoie un GPIO_PinState, qui est une énumération, donc on rajoute juste un test pour obtenir un booléen:

return (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET);

debug.c : tous les "USART3 deviennent "UART".

USART_InitTypeDef USART_InitStructure;

Devient:

UART_InitTypeDef UART_InitStructure;

Les RCC_xxxx\ClkCmd() deviennent des __HAL_RCC_xxxx_CLK_ENABLE(), l'activation de la clock UART se fait via __HAL_RCC_USART1_CLK_ENABLE(), etc. Globalement on retrouve les routines dans le fichier stm32f4xx_hal_msp.c.

D'une façon générale, étant donné que les initiailisations sont déjà faites par les fonctions d'init de Cube, je vais vider les init des fonctions stm32doom pour ne garder que ce qui est spécifique, comme les configurations des interruptions par exemple. Les fonctions pour gérer la config des interuptions est dans stm32f4xx_hal_cortex.h. Dans stm32doom, l'interruption UART est gérée snas buffer explicite : on active l'interruption, et dans le handler de l'interruption on vient faire un read et mettre le caractère reçu dans le buffer. Les équivalences se font avec les macros __HAL_UART_ENABLE_IT() et autres. Ici j'ai bien envie d'écrire directement dans le buffer, avec la fonction HAL_UART_Receive_IT(), je modifierai plus tard, pour l'instant je vais garder le mécanisme actuel.

Aparté : Mapper printf()

J'ai regardé comment régler le problème de l'UART avec printf. L'idée, c'est de mapper printf sur l'UART de façon à avoir un moyen de debug en envoyant des chaînes de caractères. D'ailleurs, stm32doom fait fortement usage de printf(). Je dois avouer que ce n'est pas d'une évidence limpide.

L'idée principale, c'est que printf() est une fonction "standard" de la libc, et que ls compilateurs vont plus ou moins automatiquemetn reconnaitre son existence, moyennant un #include <stdio.h>. Derrière printf(), il y a d'autres fonctions bas-niveau, définies en weak, qui vont aller pointer vers les périphériques d'affichage du système. Sur un code compilé pour desktop, ça va sortir sur le "standard output" qui est géré par l'OS, sur un système embarqué c'est le canal de debug défini pour le système. Ici c'est USART1, de façon à pouvoir débugger par le port série via un VCOM. Pour mapper printf(), il faut donc définir ces fonctions de bas-niveau pour les override.

J'ai trouvé plusieurs sources, mais celle-là à l'avantage de fournir un code qui a marché : https://shawnhymel.com/1873/how-to-use-printf-on-stm32/

Et ici, en prime, explication sur comment utiliser la console série dans cubeIDE : https://github.com/ethanhuanginst/STM32CubeIDE-Workshop-2019/tree/master/hands-on/03_printf

Ce n'est pas du tout trivial, il m'a fallu plusieurs essais avant d'y arriver. Je recommande d'ailleurs d'utiliser Tera Term ou la console série de cubeIDE, mais pas Putty qui a l'air de galérer avec le VCOM ST. Or donc, on va exclure syscalls.c, et ajouter un module "retarget" qui va juste mapper les fonctions de bas-niveau__io_putchar() / _write() / _read() ... partant de là, #include <stdio.h> dans le main et on peut alors utiliser printf() pour envoyer du texte sur l'UART. Hautement pratique !

Retour aux moutons

i2c.c : là aussi tout l'init va faire la même chose que la fonction MspInit, donc je commente tout.

il y a plein de routines pour faire des actios bas-niveau, qui n'existent pas dans le HAL. A chaque fois, je prends le nom de la routine, je fais une recherche dans le projet stm32doom d'origine pour trouver la définition de al fonction, je regarde quelle est l'action faite (généralement juste une modificatino d'un registre), et je vais chercher u bout de code dans le HAl qui fait la même chose, et je le copie-colle tel quel.

Exemple : I2C_GenerateSTART()

 /* Check the parameters */
 assert_param(IS_I2C_ALL_PERIPH(I2Cx));
 assert_param(IS_FUNCTIONAL_STATE(NewState));
 if (NewState != DISABLE)
 {
   /* Generate a START condition */
   I2Cx->CR1 |= I2C_CR1_START;
 }
 else
 {
   /* Disable the START condition generation */
   I2Cx->CR1 &= (uint16_t)~((uint16_t)I2C_CR1_START);
 }

Bon, je saute les assert par commodité (mais je devrais les implémenter aussi, mais là j'ai pas envie), et je vois que l'action principale est I2Cx->CR1 |= I2C_CR1_START;. Je fais une recherche sur I2C_CR1_START dans le HAL et je trouve une ligne de code équivalente, genre :

/* Generate Start */
   SET_BIT(hi2c->Instance->CR1, I2C_CR1_START);

Je rapatrie le handle de l'I²C du main, je déplace la déclaration dans i2c.c et je le déclare en externe dans i2c.h.

lcd.c est un peu plus compliqué de prime abord ... Mais en fait aps tant que ça. Toutes les initialisations sont quasi-identiques à ce qui est fait dans le main, donc je vide ces fonctions, et je mets juste à jour dans le main la fonction MX_LTDC_Init avec les éventuelles valeurs différentes. Dans l'init Msp la priorité de l'interuption est mise à 5 au lieu de 6 ... Bon, je laisse, je doute que ça soit grave.

  • LTDC_HorizontalStart ->WindowX0
  • LTDC_HorizontalStop -> WindowX1
  • LTDC_VerticalStart -> WindowY0
  • LTDC_VerticalStop -> WindowY1
  • LTDC_ConstantAlpha -> Alpha
  • LTDC_DefaultColorAlpha -> Alpha0
  • LTDC_CFBLineLength -> ImageWidth
  • LTDC_CFBLineNumber -> ImageHeight
  • LTDC_CFBPitch -> ???

Sur ce dernier élément, en regardant son usage, on se rend compte qu'il est juste mis dans une variable locale dans le LayerInit de stm32f4xx_ltdc.c dans stm32doom, mais cette variable n'est jamais lue, rien ne pointe dessus ... ça ressemble à une variable morte, donc je vais juste l'ignorer.

  • LTDC_ReloadConfig() -> HAL_LTDC_Reload();
  • LTDC_LayerCmd() -> __HAL_LTDC_LAYER_ENABLE() ou __HAL_LTDC_LAYER_DISABLE()
  • LTDC_DitherCmd (DISABLE) -> HAL_LTDC_DisableDither()
  • LTDC_Cmd (ENABLE) -> __HAL_LTDC_ENABLE()
  • LTDC_ITConfig() -> __HAL_LTDC_ENABLE_IT() ou __HAL_LTDC_DISABLE_IT()
  • LTDC_LayerAlpha() ->pas d'équivalent strict, donc LTDC_LAYER(&hltdc, LTDC_Layer1)->CACR &= ~(LTDC_LxCACR_CONSTA) et LTDC_LAYER(&hltdc, LTDC_Layer1)->CACR = transparency
  • LTDC_ClearITPendingBit() -> __HAL_LTDC_CLEAR_FLAG()

gfx.c : Là, c'est une autre histoire, les couches sont très différentes. Visiblement la lib utilisée par stm32doom permet beaucoup plus de choses, et ça se voit avec le nombre et la taille des structures dans l'équivalent du dma2d. Là où les couches ST proposent une structure DMA2D_InitTypeDef avec trois éléments Mode, ColourMode et OutputOffset, et une structure DMA2D_LayerCfgTypeDef avec 4 éléments InputOffset, InputColorMode, AlphaMode, InputAlpha, la lib dans stm32doom propose 10 éléments dans DMA2D_InitTypeDef, et deux structures DMA2D_FG_InitTypeDef et DMA2D_BG_InitTypeDef similaires, chacune dédiée au premier plan (FG, foreground) et au fond (BG, background) et contenant 12 éléments. L'équivalence n'est pas évidente.

Je propose de passer toutes les fonctions en revue et voir ce qu'on peut faire avec:

  • void gfx_init (void) :
  • bool gfx_add_object (gfx_obj_t* obj) fait juste un gfx_clear_screen (RGB565_WHITE);
  • bool gfx_delete_object (gfx_obj_t* obj) ajoute obj dans le tableau gfx_objects
  • void gfx_delete_objects (void) cherche dans gfx_objects où est l'objet obj et met NULL à la place
  • void gfx_draw_objects (void) cycle dans gfx_objects, et pour chaque élément qui n'est pas NULL fait un gfx_draw_img() dessus
  • void gfx_refresh (void) lance lcd_refresh()
  • void gfx_clear_screen (uint16_t color) vide l'écran et remplace par une couleur unique, là ya un peu plus de code, il faudra entrer dans le détail
  • void gfx_draw_img (gfx_image_t* img, gfx_coord_t* coord) en fonction du "pixel format" dans img, va lancer gfx_draw_img_rgb565() ou gfx_draw_img_argb8888()
  • bool gfx_load_img (const char* file_name, gfx_image_t* img) va charger dans img une image récupérée dans le système de fichiers, avec décompactage au passage c'est un JPEG
  • void gfxunloadimg (gfximaget* img) libère le contenu de img, avec un free() et met NULL dans img->pixel_data
  • uint8_t gfx_draw_character (const char chr, gfx_image_t* img, uint16_t x0, uint16_t y0, const gfx_font_t* font, uint32_t color) écrit un caractère, aussi un gfx_map_char() et un gfx_draw_pixel(), aussi un gfx_blend_pixel() et un gfx_draw_pixel(), le reste c'est des trucs standards.
  • void gfx_draw_character_centered (const char chr, gfx_image_t* img, const gfx_font_t* font, gfx_coord_t* coords, uint32_t color) fait un gfx_get_char_dimensions pour déterminer le centre du caractère, et un gfx_draw_character au centre de l'écran
  • void gfx_draw_string (const char* str, const gfx_font_t* font, gfx_image_t* img, uint16_t x, uint16_t y, uint16_t max_width, uint32_t color) fait une série de gfx_draw_character(), après avoir calculé la longueur "physique" de la chaîne de caractères pour savoir ou la positionner
  • void gfx_draw_string_wrapped (const char* str, const gfx_font_t* font, gfx_image_t* img, uint16_t x, uint16_t y, uint16_t max_width, uint32_t color) idem la précédente, ça change le positionnement de la nouvelle chaîne
  • void gfx_draw_string_centered (const char* str, const gfx_font_t* font, gfx_image_t* img, uint16_t y, uint32_t color) idem centré
  • void gfx_rotate (gfx_image_t* source, gfx_image_t* dest, float radians, uint32_t back_color) fait de la rotation graphique, donc pas de fonctions spécifique, c'est que des maths
  • void gfx_flip90 (gfx_obj_t* obj) fait des rotations de 90°, il y a une fonction dédiée pour éviter de passer par l'algo de gfx_rotate() qui va être beaucoup trop lourd pour une simple rotation de 90° qui se résume à des décalages de bits et des échanges de coordonnées
  • void gfx_copy_image (gfx_image_t* source, gfx_image_t* dest) fait du memcpy
  • void gfx_set_pixel (gfx_image_t* img, uint16_t x, uint16_t y, uint32_t color) fait un gfx_draw_pixel
  • uint32_t gfx_get_pixel (gfx_image_t* img, uint16_t x, uint16_t y) va lire une couleur dans un pixel dans img
  • void gfx_draw_line (gfx_image_t* img, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint32_t color) dessine des lignes, si horizontale ou verticale fait un gfx_fill_rect(), sinon du gfx_draw_pixel pour construire la ligne pixel par pixel
  • void gfx_fill_rect (gfx_image_t* img, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint32_t color) utilise les fonctions DMA2D pour générer un rectangle, appelle gfx_bytes_per_pixel() et fait du fait un init du DMA2D.
  • void gfx_draw_img_on_img (gfx_image_t* img_src, gfx_image_t* img_dst, gfx_coord_t* coord) fait aussi du gfx_bytes_per_pixel et du DMA2D.
  • void gfx_fill_img (gfx_image_t* img, uint32_t color) fait un gfx_fill_rect()
  • void gfx_init_img (gfx_image_t* img, uint16_t width, uint16_t height, gfx_pixel_format_t pixel_format, uint32_t color) fait un malloc et un gfx_fill_img
  • void gfxinitimg_obj (gfx_obj_t* obj, gfx_image_t* img) va juste mettre des valeurs dans des éléments de obj
  • void gfxinitimg_coord (gfx_coord_t* coord, gfx_image_t* img) va juste mettre des valeurs dans des éléments de coord

Donc, au final, les seules fonctions à porter sont les appels à DMA2D à metre à jour avec la lib ST, pour le reste ya pas grnad chose de spécifique, c'est beaucoup de fonctions qui s'appellent les unes les autres.

DMA2D est le "Chrom-Art Accelerator controller" du MCU, il s'agit d'un accélérateur graphique qui permet de faire en hardware certaines opérations simples, qui boufferaient trop de temps CPU s'il fallait le faire en code:

  • Remplir une partie ou l'intégralité d'une image de destination avec une couleur unique,
  • Copier tout ou partie d'une image dans une autre,
  • Copier tout ou partie d'une image dans une autre en faisant une conversion de format de pixel au passage,
  • Mélanger tout ou partie de deudx images en faisant des conversions de format de pixel au passage.

Ce n'est pas grand-chose, mais en vrai ça permet d'éviter de devoir faire des boucles for imbriquées pour des opérations simples comme de la copie d'image ou de la création de rectangles, ce qui permet de décharger le CPU de ces tâches, sur lesquelles il n'a aucune valeur ajoutée. L'idée c'est qu'on donne des paramètres de remplissage / copie, et le DMA2D va faire les remplissage de tous les pixels en HW. Et il faut un périphérique dédié car s'agissant d'images l'info est "en 2D", et donc un DMA "standard", qui ne fonctionne qu'en une seule dimension, n'est pas adapté. C'est donc bien, comme son nom l'indique, un "DMA en 2D" :)

De ce que j'ai compris, le principe du HAL de ST, c'est de configurer d'un côté le DMA2D, de l'autre deux couches ("Layers") qui seront utilisée comme les deux opérandes des opérations : soit uniquement la destination pour les opérations de remplissage de couleur unique, soit la source et la destination en cas de copie de l'une vers l'autre.

Pour lancer une opération, on va donc configurer le DMA2D puis appeler HAL_DMA2D_Init(), puis cofigurer les couches et appeler HAL_DMA2D_ConfigLayer() pour chaque couche, puis appeler une fonction start pour lancer l'opération.

  • DMA2D_FG_InitStruct.DMA2D_FGCM -> hdma2d.LayerCfg[1].InputColorMode
  • DMA2D_FG_InitStruct.DMA2D_FGPFC_ALPHA_MODE -> hdma2d.LayerCfg[1].AlphaMode
  • DMA2D_FG_InitStruct.DMA2D_FGPFC_ALPHA_VALUE -> hdma2d.LayerCfg[1].InputAlpha
  • DMA2D_FG_InitStruct.DMA2D_FGO -> hdma2d.LayerCfg[1].InputOffset
  • DMA2D_StartTransfer() -> HAL_DMA2D_Start() auquel on passe les paramètres qui sont définis dans DMA2D_InitStruct mais qui n'ont pas d'équivalent dans hdma2 : pData -> DMA2D_FGMA -> source_address, DstAddress -> DMA2D_OutputMemoryAdd -> dest_address, Width -> DMA2D_PixelPerLine -> coord->source_w, Height -> DMA2D_NumberOfLine -> coord->source_h.
  • DMA2D_GetFlagStatus() -> __HAL_DMA2D_GET_FLAG()

Petite note sur les structures utilisées ici : il y a donc une structure DMA2D_HandleTypeDef qui "contient" le DMA2D, et lui-même contient un tableau LayerCfg[MAX_DMA2D_LAYER] avec MAX_DMA2D_LAYER = 2, ce qui veut dire qu'il gère deux couches, ce qui correspond dans les drivers de stm32doom au premier plan (FG, foreground) et à l'arrière-plan (BG, background). Donc dans stm32doom il y a une structure dédiée pour FG et BG, dans les couches ST c'est un tableau dans DMA2D_HandleTypeDef. On retouve la définition de qui est le FG et qui est el BG ici:

#define DMA2D_BACKGROUND_LAYER             0x00000000U   /*!< DMA2D Background Layer (layer 0) */
#define DMA2D_FOREGROUND_LAYER             0x00000001U   /*!< DMA2D Foreground Layer (layer 1) */

DMA2D_DeInit() n'est pas le DeInit du Msp, mais une fonction qui fait un reset du périphérique, mais l'on remarque une chose intéressante : si on cherche un équivalent de RCC_AHB1PeriphResetCmd(RCC_AHB1Periph_DMA2D, ENABLE) on trouve une liste de macro dans stm32f4xx_rcc.h qui permet de forcer le reset des périphériques câblés sur le AHB1, et on se rend compte que la macro n'est pas écrite pour le DMA2D, alors que tous les #define existent et sont dispo pour l'écrire. Très bizarre ... Donc je l'ajoute dans gfx.h:

  • #define __HAL_RCC_DMA2D_FORCE_RESET() (RCC->AHB1RSTR |= (RCC_AHB1RSTR_DMA2DRST))
  • #define __HAL_RCC_DMA2D_RELEASE_RESET() (RCC->AHB1RSTR &= ~(RCC_AHB1RSTR_DMA2DRST))

Dans gfx_clear_screen() on précise en plus la couleur à afficher. Dans la structure DMA2D_Init il y a des éléments DMA2D_OutputRed, DMA2D_OutputGreen et DMA2D_OutputBlue, qui sont passés à la fonction d'init, qui va en fonction du color mode va y passer dans le registre OCOLR du DMA2D. Dans les couches ST, ce registre n'est écrit qu'à un seul endroit : dans DMA2D_SetConfig(), qui est une fonction qui fait ce que fait la fonction d'init du DMA2D dans les couches de stm32doom. Donc il faut appeler cette fonction avec chaque utilisation de HAL_DMA2D_Init(), mais en fait c'est fait dans les fonctions Start du DMA2D, donc il faut juste voir comment lui passer les paramètres. Cette fonction prend le handle du DMA2D, la taille de data à afficher (là on met tout l'écran), le buffer de destination (ici le frame buffer. Le pointeur pData, c'est soit un bitmap à aficher, soit on va récupérer les infos RGB+alpha dedans via des masques DMA2D_COLOR_xxx. On va donc devoir construire une variable qui va contenir les trois infos RGB pour que cette fonction puisse y dépiler correctement. Si on regarde ces fameux masques:

#define DMA2D_OCOLR_BLUE_1         0x000000FFU                                 /*!< BLUE Value */
#define DMA2D_OCOLR_GREEN_1        0x0000FF00U                                 /*!< GREEN Value  */
#define DMA2D_OCOLR_RED_1          0x00FF0000U                                 /*!< Red Value */
#define DMA2D_OCOLR_ALPHA_1        0xFF000000U                                 /*!< Alpha Channel Value */

On voit que chaque composante se voit assigner un octet sur 32 bits. Ensuite la fonction DMA2D_SetConfig() va check quel est le mode de gestion de couleurs pour voir comment "mélanger" ces 4 paramètres par rapport au mode de rendu choisi. Donc il faut juste metre les 4 infos RGB+A dans un 32 bits, donc je créée une variable locale pour ça, et donc juste décaler les bits. Pour éviter d'utiliser des magic numbers je vais utiliser les constantes x_Pos fournies dans stm32f429xx.h:

/********************  Bit definition for DMA2D_FGCOLR register  **************/

#define DMA2D_FGCOLR_BLUE_Pos      (0U)                                        
#define DMA2D_FGCOLR_BLUE_Msk      (0xFFUL << DMA2D_FGCOLR_BLUE_Pos)            /*!< 0x000000FF */
#define DMA2D_FGCOLR_BLUE          DMA2D_FGCOLR_BLUE_Msk                       /*!< Blue Value */
#define DMA2D_FGCOLR_GREEN_Pos     (8U)                                        
#define DMA2D_FGCOLR_GREEN_Msk     (0xFFUL << DMA2D_FGCOLR_GREEN_Pos)           /*!< 0x0000FF00 */
#define DMA2D_FGCOLR_GREEN         DMA2D_FGCOLR_GREEN_Msk                      /*!< Green Value */
#define DMA2D_FGCOLR_RED_Pos       (16U)                                       
#define DMA2D_FGCOLR_RED_Msk       (0xFFUL << DMA2D_FGCOLR_RED_Pos)             /*!< 0x00FF0000 */
#define DMA2D_FGCOLR_RED           DMA2D_FGCOLR_RED_Msk                        /*!< Red Value */

Je n'ai pas trouvé de macros qui feraient la conversion dans les couches ST. Par contre, dans gfx.h il y a une macro GFX_ARGB8888 qui le fait, donc je vais l'utiliser. Ca fait un peu des conversion inutiles (vu qu'on va déjà extraire les infos de couleurs en RGB565, pour ensuite les reconvertir en ARGB8888 ...) mais bon.

gfx_fill_rect() va desiner un rectangle dans une image en RAM, en utilisant le DMA2D comme accélérateur hardware, mais pas afficher. Donc il n'y a pas de source, vu que l'on construit l'objet à desiner de toutes pièces, et la destination est le bitmap de la structure fournie en paramètre, et ça sera une autre fonction qui déclenchera laffichage de l'image sur l'écran. Et donc là aussi la donnée qu'on donne en "input" est en fait un uint32 qui continet le RGB+A. En paramètre on a la position x et y du rectangle, qui est utilisée pour calculer offset, qui sert à savoir où on va taper dans le bitmap.

gfx_draw_img_on_img va aussi utiliser le DMA2D comme accélérater graphique pour recopier une image dans une autre, sans l'afficher. On donne en paramètre une image source et une image destination, et la "somme" des deux sera mise dnas l'image définie comme destination, un peu comme les opérations en assembleur (le résultat est mis dans le deuxième opérande, il n'est pas stocké "à part"). Donc on définit source et destination chacune dans une des deux couches (FG et BG), et on va metre le mélange es deux dans l'image destination, qui est donc écrasée.

Il faudra que je joue avec toutes ces fonctions, elles ont l'air intéressantes pour faire de l'IHM :)

Tout cela fait, il me reste seulement un warning du fait de la fonction f_readn qui ne pointe nulle part. Il faudra que je gère ça ...

led.c : GPIO_Pin_xx devient GPIO_PIN_xx tout en majuscules, et GPIO_SetBits() devient HAL_GPIO_WritePin().

spi.c : SPI_I2S_SendData() devient HAL_SPI_Transmit(), SPI_I2S_ReceiveData() devient HAL_SPI_Receive(), pas d'interuption, pas de DMA.

sdram.c : il y a une série de #define qui vont aller chercher des constantes dans stm32f4xx_fmc.h, dans les couches ST il n'y a pas de HAL, c'est du LL : stm32f4xxllfmc.h.

#define SDRAM_MEMORY_WIDTH	FMC_SDMemory_Width_16b -> FMC_SDRAM_MEM_BUS_WIDTH_16
#define SDRAM_CLOCK_PERIOD	FMC_SDClockPeriod2 -> FMC_SDRAM_CLOCK_PERIOD_2
#define SDRAM_CAS_LATENCY		FMC_CAS_Latency_3 -> FMC_SDRAM_CAS_LATENCY_3
#define SDRAM_READBURST		FMC_Read_Burst_Disable -> FMC_SDRAM_RBURST_DISABLE
#define SDRAM_SIZE				0x800000 -> ça fait 8Mo, c'est effectivement la taille de la RAM, si je la change il faudra que je double ici

Mais bon, encore une fois, ces paramètres sont utilisés pour l'init du contrôleur, qui est déjà fait avec les bonnes valeurs dans le main, donc je vais juste commenter le code de l'init. Je garde juste la mise à zéro de la RAM.

Une fois initialisée, la RAM est gérée "automatiquement" avec son propre espace mémoire. C'est le FMC (Flexible Memory Controler) qui gère ça, et son but est de faire une "translation automatique", dans laquelle la mémoire du device externe est accessible "en direct" de façon transparente, c'est à dire qu'on va aller taper directement dans un espace mémoire come si on allait dans la RAM du MCU, mais en fait le FMC va faire la translation automatique et aller chercher les datas sur le device externe. Dans le user manual page 1607 figure 457 on a le mapping "virtuel". On voit qu'il y a une plage par device qu'on peut adresser, et pour la SDRAM on peut connecter deux device, chacun aura une banque, et ces banques sont aux adresses 0xC0000000 et 0xD0000000. On sélectionne la banque dans HADDR, chacune ayant son propre jeu de registres de config, ici on active la banque qui est en 0xD0000000. Et une fois ça initialisé ... Ben ya rien à faire, le FMC gère tout. Il faut juste mapper explicitement les variables que l'on veut mettre en RAM dans la bonne section.

touch.c : a priori la pin d'interruption PA15 est configurée comme tel, mais la callback n'est pas créée automatiquement par CubeMX, donc il va falloir l'ajouter. On va donc chercher comme cette pin est mappée vers les interruptions, page 382 du user manual, et l'on voit que PA15 est sur EXTI15. L'on repasse dans l'interface de cubeMX, et on va dans System Core / GPIO / GPIO, et sur la config de PA15 on sélectionne le GPIO mode "External Interrupt Mode with Falling edge trigger detection", ce qui va faire apparaître l'onglet NVIC, et on va pouvoir activer l'interruption pour EXTI15 : on coche Enabled et dans System Core / NVIC on va attraper l'interruption EXTI15 et on met preemption priority à 15 comme dans le code d'origine. On fait gaffe de faire un commit avant de re-générer pour récupérer les éventuelles modifications écrasées par CubeMX. Ca va ajouter une config différente pour la pin, et ajouter la fonction EXTI15_10_IRQHandler() dans stm32f4xx_it.c. C'est la fonction qui sera appelée quand l'interruption se déclenchera, il faut l'activer, et mapper les callbacks. La fonction EXTI15_10_IRQHandler() est définie en WEAK dans stm32f4xx_exti, ce qui signifie qu'il y a une définition par défaut, amis que toute autre définition sans WEAK la remplacera. Dans touch.c elle est réécrite justement, donc soit je garde cette définition et je vire celle de stm32f4xx_it.c, soit je change son nom et je l'appelle dans stm32f4xx_it.c. Si on décoche la création de la fonction dans CubeMX ça ne l'ajoute pas dans stm32f4xx_it.c, je vais plutôt faire ça, donc retour dans CubeMX : dans System Core / NVIC / Code generation on va décocher "Generate IRQ handler" et on re-génère le tout (idem pout LTDC d'ailleurs). Et je commente le contenu de la fonction d'init.

Il y a une fonction sleep_ms qui est définie dans main.c. Grrr ... Je déteste ça, la hiérarchie entre les modules est completment cassée, je préfère faire un module "utility". Mais comme ça utilise systime qui est défini dans main.c ... Il faudrait que je fasse un driver spécifique à la gestion du temps, bon, je ne vais pas le faire ici, je vais garder tel quel, à bas la hiérarchie !

Dans la callback de l'interruption, il y a EXTI_GetITStatus() et EXTI_ClearITPendingBit(), dont les équivalents sont HAL_EXTI_GetPending() et HAL_EXTI_ClearPending(), auquel il faut passer un EXTI_HandleTypeDef, donc j'en créé un pour touch.c, vu qu'il n'est pas créé dans main.c. Il n'y a que deux éléments dans cette structure : la ligne d'IT et un pointeur vers une callback. A priori les seules fonctions pour lesquelles j'utilise cette structure ne vont regarder que le numéro de ligne, donc je le met à EXTI_LINE_15 dans la fonction d'init.

J'exclue vectors.c du build (remplacé par stm32f4xx_it.c) et la compilatio aboutit ! Houra :) Et le binaire n'est pas vide, ouf !

Par contre, aucune chance que ça démarre : à part le frame buffer il n'y a rien qui est mappé dans la SRAM, et je n'ai rien check sur l'USB et le fatfs. On va commencer par ça.

Init de l'USB

Pour l'USB, il faut check qu'une clé USB est branchée. Dans stm32doom on a un while avec un timeout qui lance la tâche de fond host jusqu'à avoir un status "device connected". Dans les couches ST, l'init de l'USB balance une erreur si ça se passe mal, et après c'est la tâche de fond qui va check les connections, par contre elle ne retourne pas d'état, il faut donc aller lire l'état du handle USB pour identifier un état équivalent. De plus, dans l'archi standard la tâche de fond n'est appelée que dans le while(1) du main, donc après l'init. Il faudrait que j'ajoute une fonction dans le module host USB qui permet de lancer la tâche de fond de l'USB jusqu'à avoir un device connecté. L'autre problème c'est que le handle dans le module USB host n'est pas partagé (extern), donc on ne peut pas y accéder depuis un autre module. Je pense que je vais faire une fonction qui retourne un booléen qui passera à true quand l'état du handle sera "device connected", et qu'on lancera dans un while dans l'init du main.

L'élément à check c'est hUsbHostHS.gState, et l'tat à atteindre c'est HOST_CLASS. Je vais juste ajouter dan sun premier temps un while dans la fin de l'init du USB host (ya un espace pour mettre du code user) qui va lancer USBH_Process() en boucle et tester gState jusqu'à avoir HOST_CLASS et faire un break. Un coup de débug pour voir comment ça se passe : ok, il y arrive. Bon, plus tard il faudra que j'ajoute un check que c'est la bonne classe, et gérer l'absence de device (là pour l'instant le main n'a aucun moyen de savoir si un device a été connecté).

Maintenant, regardons le FATFS pour voir comment ça se passe.

Init du FATFS

Le but du driver FATFS est de monter un système de fichier dans la clé USB, de façon à pouvoir faire des opérations de lecture / écritre en utilisant des fonctions "génériques" (read_f, write_f ...). C'est une couche d'abstraction pour accéder à des données classées dans un système de fichier (ce qui est le cas dans une clé USB formattée par un OS de bureau).

Dans stm32doom, la lib utilisée est une lib développée par le même bonhomme qui a dveloppé tous les drivers du MCU. ST propose une lib FATFS, je vais utiliser celle-là. Dans stm32doom, il y a deux opérations qui sont effectuées avec le fatfs : fatfs_init() au startup, et fatfs_mount() une fois la clé USB connectée au host. Il va donc falloir trouver des équivalents.

void fatfs_init (void)
{
SDCard_Init ();
USBDisk_Init ();
}

Déjà, USBDisk_Init () est vide. Comme ça c'est fait. Quand à SDCard_Init (), il va initialiser les interruptions pour le contrôleur de carte SD, et faire l'initialisation des GPIO et du contrôleur SDCard. Donc, en vrai, ça ne sert à rien.

bool fatfs_mount (diskio_media_t dev)
{
DWORD free_clusters;
FATFS* fs;
char* volume;

volume = fatfs_get_vol (dev);

if 
{
return true;
}

return false;
}

Là il se passe des choses. fatfs_get_vol (dev) va juste check quelle est la valeur de dev, c'est un enum qui dit si le device à monter est une carte SD ou une clé USB. Puis f_mount() va monter le volume, et f_getfree() va obtenir le nombre de clusters libre sur le volume. A priori tous les résultats de ces opérations seront enregistré dans des structures du module fatfs, et dans le code de chocolate Doom, les fonctions du module fatfs remplaceront avantageusement celles utilisée par défaut avec un OS de bureau.

Bien bien, donc, 1) il faut trouver l'implémentation équivalente dans le FatFs fourni par ST, et retrouver les fonctions à remplacer dans le code de chocolate Doom. A priori, il n'y a que dans w_file_stdc.c, p_saveg.c et .h, et m_misc.c et .h qu'il est fait appel au système de fichier.

Allons regarder la documentation du FatFs fourni par ST. Section 4.5 il y a la liste des fonctions f_xxx qui sont utilisées pour manipuler le sustème de fichiers (on les retrouve dans ff.h), et section 4.6 les fonctions de bas-niveau, dans lesquelles on va trouver lest fonctions d'initialisation, mais la doc dit explicitement qu'il ne faut pas les utiliser, et laisser les fonctions de haut-niveau (f_mount, f_read et f_write) gérer le bas-niveau. Dans le fichier stm32f4xx_hal_conf.h on va retrouver les #define qui permettent d'activer les différentes "options" des drivers. J'ai bien #define HAL_HCD_MODULE_ENABLED qui est décommenté, donc j'ai bien coché les bonnes cases dans CubeMX.

Globalement les deux drivers ontl'air de faire la même chose, et mêmes les interfaces / API sont les mêmes. Donc je suis très tenté de remplacer tel quel et voir se qu'il se passe.

MX_FATFS_Init() est appelé par défaut juste après MX_USB_HOST_Init(), et va appeler FATFS_LinkDriver(), qui va essayer d'activer le disque. Il n'y a pas d'aquivalent de fatfs_mount(), je vais l'ajouter dans la partie user de FatFs.c.

Lancement : ça plante vers l'init de SDRAM. Hum ... Visiblement l'initiailisation à 0 de la RAM le fait partir en vrac, quand il essaye d'écrire sur 0xD0000040 le débugger se meule ... J'ai vérifié toutes les valeurs d'init, il n'y en a qu'une seule de différente () et en la mettant à la "bonne valeur" ça plante toujours. Regardons un exemple fourni par ST. Il y a une fonction SDRAMInitializationSequence(), je vais la copier et l'appeler juste après l'init du FMC. Visiblement ça résout le problème, maintenant ça initialise la RAM en entier snas planter. Tant mieux ...

Bon, printf ne marche pas, mais il faut que je regarde en détail, pas maintenant. Maintenant c'est l'init du LCD qui plante, une réponse à une trame SPI qui ne vient jamais, dans le premier lcd_cmd(), RXNE ne passe jamais à 1. Cherchons un exemple, et checkons toutes les valeurs d'init, le CRC est configuré différemment, ok, je corrige. Mais je doute que ça règle le problème. En allant en pas à pas dans la fonction send du SPI, je vois le RXNE passer à 1 juste après l'écriture du DR, puis passer à 0. Pas clair. J'ai abandonné les fonctions HAL et je suis allé taper directement dans les routines et dans le registre DR, ça marche mieux et ça passe. Bon, aucune idée si ça marche vraiment par contre, je n'ai aps déoscillo pour check.

gfx_draw_img_rgb565 plante, erreur à la première configuration de layer DMA2D. __HAL_LOCK(hdma2d) me jette direct, visiblement il est lock dans gfx_init(), par le HAL_DMA2D_Start(). Argh, ça me rappelle pourquoi j'ai maudi les couches HAL dans un projet au taf. Ok, donc il semble que la "bonne façon" de faire, c'est qu'après avoir fait un start le process est lock, et pour le dé-lock il faut utiliser une fonction ad-hoc, et il semble que ça soit HAL_DMA2D_PollForTransfer(), qui va check le flag TC, et unlock une fois qu'il passe à 1.

Grmpf ... C'est écrit dans l'en-tête du driver ...

*** Polling mode IO operation ***
=================================
[..]
(#) Configure pdata parameter (explained hereafter), destination and data length
         and enable the transfer using HALDMA2DStart().
      (#) Wait for end of transfer using HALDMA2DPollForTransfer(), at this stage
          user can specify the value of timeout according to his end application.

RTFM ! Bon, donc je met du HAL_DMA2D_PollForTransfer(). Ca me débloque.

sleep_ms plante, visiblement parce que le timer système ne tourne pas. En effet, il n'est écrit nulle part, ça ne risque pas de marcher. J'ajoute systime++ dans le SysTick_Handler() dans stm32f4xx_it.c, ce qui débloque la situation. Je passe la fonction show_image() ... Mais aucune image ne s'affiche sur le LCD :( Damned ...

Bon bon bon, finissons l'init, et après on ira faire un tour dans les exemples ST.

L'init du touchscreen plante, erreur fatale, à la lecture de l'ID I²C la valeur n'est pas bonne. Regardons un exemple, qui n'est pas pratique vu qu'il n'utilise par le HAL mais le BSP ... qui est un jeu de couches basses, dédiées aux cartes d'éval. Et le numéro attendu correspond bien à celui de stm32doom, et ce n'est pas ce que je relis. Grmpf ...

... J'ai bien envie d'y intégrer dans le projet et de les utiliser ces couches "toutes faites". On est plus à un Frankenstein près ...

En prenant l'init BSP fourni par ST, j'arrive à faire un clear et la couleur est appliquée, donc l'init est bon. Par contre, show_image() ne marche pas. Visiblement les éléments dans gfx_objects ont un flag "enabled" et tout est à false. Bizarre ... Surtout que ce flag n'est mis à true que dans la fonction gfx_init_img_obj() qui n'est appelée nulle part ... Essai en forçant à true -> l'image ne change toujours pas.

Bon, j'ai pris les fonctions BSP tel quel, ça affiche les images de l'exemple, mais pas l'image de stm32doom, même ça met la fonction BSP_LCD_DrawBitmap() en deadlock ... Clairement le format des images n'est pas adapté pour cette fonction.

Si on regarde comment marche cette fonction BSP_LCD_DrawBitmap(), on voit qu'elle va chercher les metadata dans l'en-tête, puis qu'elle saute l'en-tête et qu'elle va afficher ligne par ligne en écrivant dans le frame buffer via ConvertLineToARGB8888(), qui va faire des DMA2D_Init() et DMA2D_Start(). Et là, quand on lui met l'image de stm32doom en entrée, les infos qu'elle récupère dans l'en-tête sont completement faux.

En fait, visiblement dans stm32doom, l'image ne contient pas de metadata, en tous cas la fonction show_image() ne va pas parser l'en-tête de l'image, et il faut donner ces données par ailleurs. Dans show_image() elles sont données en dur.

Ya vraiment un problème dans show_image, c'est comme si le pointeur vers l'image n'était jamais écrit, ce qui fait que le buffer qu'on envoie à afficher est rempli de merde. Où est-ce que img est pointé dans obj_title ? Ca passe dans jpeg_decode, mais ça ressort vide, pas d'erreur remontée par le convertisseur de jpeg.

static void show_image (const uint8_t* img)
{
gfx_image_t title;
gfx_obj_t obj_title;

title.width = 240;
title.height = 320;
title.pixel_format = GFX_PIXEL_FORMAT_RGB565;
title.pixel_data = malloc (title.width * title.height * 2);

obj_title.obj_type = GFX_OBJ_IMG;
obj_title.coords.dest_x = 0;
obj_title.coords.dest_y = 0;
obj_title.coords.source_x = 0;
obj_title.coords.source_y = 0;
obj_title.coords.source_w = title.width;
obj_title.coords.source_h = title.height;
obj_title.data = &title;

jpeg_decode ((uint8_t*)img, title.pixel_data, 0, 0);

Je vois bien l'initialisation de title.pixel_data, mais une fois passé dans jpeg_decode(), il est vide. Je ne sais pas dire si c'est parce que la fonction de conversion sort des 0 ou bien si le pointeur n'a pas été écrit. Le pointeur d'entrée de la fonction contient bien les données correctes. Le truc c'est que l'image de base est pleine de noir, donc ça pourrait expliquer qu'il y ait beaucoup de 0 une fois décodé. Et visiblement le débugeur galère à lire dans la mémoire sur des tableaux très gros, donc pour aaller chercher le centre de l'image où il y a des variations ... Pas évident. Essai en mettant un piège dans la fonction StoreBuffer() de jpeg_decode (je cherche les couleurs différentes de 0) -> ok, ça ne sort pas que des 0, donc ce n'est pas le décodage qui créé une image vide, et je retrouve bien les données "non-zéro" dans la mémoire là où est title.pixel_data. Donc c'est l'affichage ne lui-même qui déconne, pas le décodage de l'image, a priori.

Ce qui est vraiment chiant, c'est que la fonction BSP_LCD_DrawBitmap fournie par ST va chercher les meta-data de l'image dans le buffer de l'image, alors que ce qui sort du décodage du jpeg ne contient que les pixels, et il faut trouver un moyen de fournir les meta-data par ailleurs. Bon, je vais esayer de gruger en ajoutant une fonctions BSP_LCD_DrawJpeg() qui va prendre ne argument les meta-data. Et ... WOPUTAIN CA MARCHE !!! Par contre j'ai l'image en miroir ... Dans BSP_LCD_DrawJpeg() je change le mode comptage de l'index du pointeur pBmp pour qu'il commence au début et s'incrémente (et pas qu'il commence à la fin et se décrémente, comme pour les images fournies par ST), et ça revient dans le bon sens. Je tiens le bon bout :)

Dans un premier temps je ne vais pas faire dans la finesse, et tâcher d'utiliser autant que possible les fonctions BSP "telles que'elles". On fera de la finesse plus tard, pour optimiser. Regardons ce qu'on peut remplacer par des fonctions du module gfx du BSP, normalement plutôt les fonctions qui vont directement écrire sur le LCD, sachant que la plupart des fonctions sont plutôt de la manipulation d'image sans affichage.

gfx_draw_img_rgb565() écrit dans le frame buffer. C'est un peu le bazar entre les specs de la source et de la destination. L'idée, c'est que l'on a une image en mémoire (img) et que l'on veut en afficher une partie dnas le frame buffer. On a donc:

  • Le buffer de l'image d'origine img, défini par sa largeur (img->width), sa hauteur (img->height), son format (img->pixel_format) et un pointeur vers l'image (img-> pixel_data),
  • Les coordonnées de la portion d'image que l'on va prendre dans ce buffer : coord->source_x et coord->source_y,
  • Les dimensions que l'on va prendre dans ce buffer : coord->source_w et coord->source_h,
  • La position où on veut "poser" cette portion d'image dans le frame buffer destination : coord->dest_x et coord->dest_y,
  • La taille dans le buffer destination sera de la taille de la portion prise dans l'image d'origine, donc coord->source_w et coord->source_h,
  • Il y a un test qui vérifie que 1) on ne va pas prendre une portion qui dépasse de l'image d'origine et 2) on ne dépasse pas du cadre du frame buffer de destination.

Par contre ce queje n'arrive pas trop à comprendre ce sont les adresses du frame buffer, qui sont très différentes entre le code de stm32doom et le BSP. A court-terme, les valeurs du BSP fonctionnent, donc je les garde, mais il faudra que j'y nettoie.

Bon, maintenant l'USB.

Montage de la clé USB

Comme dit plus haut, on fait un while avec un timout en nombre de tours de while, et on fait tourner MX_USB_HOST_Process(). On teste si l'état du handle du host passe à HOST_CLASS, signe qu'un device a été connecté. Ensuite, il faut monter le système de fichiers.

Erreur à la détection du file system : la classe MSC n'est pas dans le bon mode, elle devrait être en MSC_IDLE, et en fait elle est en MSC_READ_INQUIRY. Quelle est cette sorcellerie ? En fait, en plus de l'initialisation du host, il faut aussi initialiser le device et la classe associée, pour qu'il soit détecté correctement, et pas seulement juste énuméré. J'ai donc ajouté un autre while, dans lequel je lance toujours MX_USB_HOST_Process(), et ne condition de break je vais check l'état de la machine d'état de la classe. Tant qu'il n'est pas à MSC_INIT c'est qu'il n'est pas initialisé correctement, et quand il est à MSC_INIT je vais monter le système de fichiers. Et là ça marche :)

Bon. Il est temps de lancer Doom :P

Mapping mémoire

Je vais donc appeler D_DoomMain(), et ça ne compile pas, car il manque plusieurs fonctions dans le module ff. Visiblement, les fonctions f_read et f_write sont dédoublées pour avoir une version "no limit". Je vais donc les recopier. Après, il y a les sections de RAM ...

Je vais donc re-chercher le script de stm32doom:

/* Internal Memory Map*/
MEMORY
{
rom (rx)	: ORIGIN = 0x08000000, LENGTH = 2048K
ram (rwx)   : ORIGIN = 0x20000000, LENGTH = 256K
sdram (rwx) : ORIGIN = 0xD004B000, LENGTH = 7892K /* first 300K is used as LCD frame buffer */
}

/* Section Definitions */ 
SECTIONS 
{	
/* external SDRAM */
.sdram (NOLOAD) :
{
. = ALIGN(4);
*(.sdram .sdram.*)
bin/chocdoom/i_video.o(COMMON)
bin/chocdoom/r_bsp.o(COMMON)
bin/chocdoom/w_wad.o(COMMON)
bin/chocdoom/r_main.o(COMMON)
bin/chocdoom/r_plane.o(COMMON)
} > sdram

Donc, dans la partie MEMORY on définit la SDRAM avec l'addresse qui vont bien, défalqué de 300ko, et dans la partie SECTIONS on définit .sdram, et dedans on indique quels fichiers objets devront être placés en SDRAM. Par contre il faut enlever bin/chocdoom qui est nécessaire quand on utilise ld en ligne de commande, et il faut aller dans les options du linker pour ajouter le répertoire Debug dans les "Libraries path search".

Maintenant il me dit que les fichiers en question sont dupliqués. Je fatigue.

Conclusion

J'arrête l'article ici pour souffler un peu, et c'est déjà bien assez long comme ça. Je vais changer mon fusil d'épaule, donc autant faire une coupure.

Episode suivant

Ajouter un rétrolien

URL de rétrolien : http://blog.randagodron.eu/index.php?trackback/90

Haut de page