Technologie : Un Avertissement Majeur !
Cet article présente un projet en phase de pré-lancement. Je mettrai à jour le contenu et le projet, ce qui signifie que ce document n’est pas figé. Si vous souhaitez suivre les modifications, c’est à vous de le faire. Je m’engage simplement à fournir une liste des changements en bas de l’article.
Architecture de PalmOS : Un Peu d’Histoire
Historique
Avant la version 5.4, PalmOS stockait toutes les données en RAM sous forme de bases de données. Ces bases de données se divisaient en deux catégories : les bases de données d’enregistrement (comme on pourrait l’imaginer) et les bases de données de ressources (similaires aux ressources de MacOS classique). Chaque base de données possédait un type et un identifiant de créateur, chacun étant un entier de 32 bits, généralement constitué de caractères ASCII. En règle générale, chaque application créait des bases de données avec son propre identifiant de créateur. Certains types avaient également une signification particulière, par exemple, « appl » désignait une application.
Modules, Bibliothèques, DALs et Pilotes
L’architecture de PalmOS repose sur divers composants, notamment des modules, des bibliothèques, des DALs (Data Abstraction Layers) et des pilotes. Ces éléments interagissent pour permettre aux applications de fonctionner de manière fluide sur le système d’exploitation.
Vers le Premier Port Non Autorisé de PalmOS
Qu’est-ce qui rend cela si difficile ?
Le portage de PalmOS sur d’autres plateformes présente de nombreux défis. Les formats de ROM sont complexes, et il est nécessaire de développer un DAL pour faciliter l’interaction avec le matériel. Cela peut sembler simple, mais la réalité est bien plus compliquée.
Les Formats de ROM
Les formats de ROM sont un véritable casse-tête. Chaque appareil a ses propres spécificités, ce qui complique le processus de portage. Il est essentiel de comprendre quel type de ROM est utilisé pour chaque appareil afin de garantir la compatibilité.
Écriture d’un DAL Minimal
Créer un DAL minimal est une étape cruciale. Cela permet de simplifier les interactions entre le système d’exploitation et le matériel, mais cela nécessite une compréhension approfondie des deux.
La Difficulté du Dessin
Le dessin sur des appareils PalmOS peut s’avérer complexe. Les limitations matérielles et les spécificités du système d’exploitation rendent cette tâche ardue.
La Controverse de l’Imitation
L’imitation méticuleuse peut être perçue comme une forme de flatterie, mais elle soulève également des questions éthiques. La frontière entre l’inspiration et le plagiat est souvent floue dans le domaine de la technologie.
La Carte SD Virtuelle
L’utilisation d’une carte SD virtuelle est une solution innovante pour contourner certaines limitations des appareils PalmOS. Cela permet d’étendre les capacités de stockage sans nécessiter de matériel supplémentaire.
Quel ROM Utilisez-vous ?
La question de la ROM utilisée est essentielle. Chaque appareil a ses propres caractéristiques, et il est crucial de s’assurer que le portage fonctionne correctement sur le matériel cible.
Alors, C’est Fini ? Ça Fonctionne ?
Une fois toutes ces étapes franchies, il est temps de tester le portage. Cela peut sembler simple, mais de nombreux problèmes peuvent survenir à ce stade.
Vers le Premier Appareil PalmOS Pirate
Un Aperçu des Appareils PalmOS 5.x
Les appareils PalmOS 5.x, avec leurs processeurs ARM, ont marqué une avancée significative dans l’évolution de la technologie mobile. Ces appareils ont ouvert la voie à de nouvelles possibilités, mais ils présentent également des défis uniques.
ARMv7M
L’architecture ARMv7M est un élément clé dans le développement de nouveaux appareils. Sa puissance et son efficacité en font un choix privilégié pour les développeurs.
Et Si Nous Essayions ?
Face aux défis, il est toujours possible d’explorer de nouvelles avenues. L’innovation est souvent le résultat de l’expérimentation.
Le Développement sur Matériel : Un Défi
CortexEmu à la Rescousse
CortexEmu est un émulateur qui facilite le développement sur du matériel PalmOS. Il permet aux développeurs de tester leurs applications sans avoir besoin d’un appareil physique.
Mais Vous Aviez Promis du Matériel Réel
Le passage au développement sur du matériel réel peut être frustrant. Les bugs matériels peuvent entraver le processus de développement et nécessiter des ajustements constants.
La Nécessité d’un Noyau
Pourquoi Pas Linux ?
L’utilisation de Linux comme noyau pour le développement de PalmOS est une option intéressante. Cela permet de bénéficier d’une base solide tout en explorant de nouvelles fonctionnalités.
Les Défis du Code ARM
Le Code ARM : Un Problème Persistant
Le code ARM reste un défi majeur lors du portage. Les différences d’architecture peuvent entraîner des complications inattendues.
Écrire un Émulateur en C : Est-ce Lent ?
La question de la vitesse est cruciale. Écrire un émulateur en C peut sembler lent, mais des optimisations peuvent améliorer les performances.
Est-ce Suffisamment Rapide Maintenant ?
Après de nombreuses itérations, il est essentiel de tester la rapidité de l’émulateur. Les performances doivent être à la hauteur des attentes des utilisateurs.
Le Travail d’un JIT n’est Jamais Terminé
LDM et STM : Un Cauchemar à Jamais !
Les instructions LDM et STM peuvent poser de nombreux problèmes lors de l’émulation. Leur complexité nécessite une attention particulière.
Instructions Conditionnelles et Sauts
Les instructions conditionnelles et les sauts sont des éléments clés à prendre en compte lors de l’émulation. Leur gestion correcte est essentielle pour garantir le bon fonctionnement des applications.
La Rapidité de PACE
Les Sauts Indirects
Les sauts indirects peuvent ralentir le processus d’émulation. Il est crucial de trouver des solutions adaptées à ce problème.
Une Solution Spéciale pour un Problème Particulier
Trouver des solutions spécifiques à des problèmes particuliers est une compétence essentielle pour les développeurs.
Histoires de Reverse Engineering de PalmOS
Support de la Carte SD
Le support des cartes SD est un aspect important du développement de PalmOS. Cela permet d’étendre les capacités de stockage des appareils.
Support du Port Série
Le port série est un autre élément clé. Il permet la communication entre les appareils et peut être testé par les développeurs.
Support de la Connectivité
Le développement de la connectivité est en cours. Les défis sont nombreux, mais les avancées sont prometteuses.
Support Audio
Le support audio est un domaine complexe. La gestion de l’audio sur PalmOS nécessite des compétences techniques avancées.
Matériel Réel : reSpring
L’Accessoire Ultime pour Springboard
reSpring est un accessoire innovant qui améliore les fonctionnalités des appareils PalmOS. Son développement a nécessité une attention particulière aux détails.
Interfaçage avec le Visor
L’interfaçage avec le Visor est une étape cruciale pour garantir la compatibilité avec les appareils existants.
Conclusion : Où en Sommes-Nous ?
Le développement autour de PalmOS continue d’évoluer. Les défis sont nombreux, mais les opportunités d’innovation sont également présentes. Les développeurs doivent rester attentifs aux évolutions technologiques pour tirer le meilleur parti de cette plateforme.
Le système d’exploitation PalmOS a été initialement conçu pour fonctionner sur des processeurs Motorola 68k, et ce, depuis ses débuts jusqu’à la version 4.x. Avec l’arrivée de la version 5, Palm Inc a décidé de migrer vers des processeurs ARM, offrant ainsi des performances nettement supérieures. Cependant, cette transition posait un défi majeur : que faire des applications déjà existantes ? De nombreuses applications PalmOS avaient été développées pour la version 4.x et compilées pour le processeur m68k. Pour résoudre ce problème, Palm Inc a introduit le PACE (Palm Application Compatibility Extension). Ce système interceptait les appels système, tels que SysAppLaunch, et émulait le processeur m68k, permettant ainsi aux anciennes applications de fonctionner sur la nouvelle architecture. Lorsqu’une application m68k effectuait un appel système, PACE traduisait les paramètres et appelait l’API native ARM, garantissant ainsi une exécution rapide des applications, même si leur logique était émulée. En effet, les appareils PalmOS 4.x fonctionnaient généralement à 33 MHz, tandis que les appareils PalmOS 5.x atteignaient des vitesses de plusieurs centaines de MHz, ce qui minimisait les ralentissements. Cela était suffisant pour Palm Inc, car de nombreuses applications intégrées, comme le calendrier et les contacts, restaient des applications m68k.
Il est à noter que Palm Inc n’a jamais fourni de documentation sur la création d’applications ARM natives complètes pour PalmOS 5.x. Bien que cela fût possible, les meilleures pratiques n’étaient pas documentées. La méthode officielle pour tirer parti de la vitesse des nouveaux processeurs ARM consistait à utiliser l’appel système PceNativeCall pour accéder à un petit morceau de code ARM natif, que Palm Inc appelait « ARMlet » puis « PNOlet ». Palm recommandait de n’utiliser cette méthode que pour les parties de code les plus critiques, et il était relativement difficile d’effectuer des appels système depuis ce code natif, nécessitant un retour vers PACE pour gérer les paramètres.
PalmOS 5.x a conservé de nombreux aspects de la conception de PalmOS 4.x, tels que le tas partagé, l’absence de mémoire protégée et le manque de documentation sur le multithreading. Une nouveauté de cette version était le support des modules chargés dynamiquement. En fait, chaque application ou bibliothèque ARM native dans PalmOS 5.x est un module, chacun ayant un identifiant unique dans la plage de 0 à 1023. Cela explique probablement pourquoi Palm Inc n’a jamais documenté la création d’applications natives complètes, car cela aurait limité le nombre d’applications à 1024.
Modules, bibliothèques et pilotes
Le noyau du système d’exploitation, la gestion de la mémoire, la plupart des pilotes et la gestion bas niveau du processeur sont assurés par le DAL (Device Abstraction Layer). Le DAL (Module ID 0) exporte environ 200 appels système, selon la version de PalmOS. Ces appels concernent des fonctions de bas niveau, telles que l’état de la batterie, l’accès brut aux primitives de dessin à l’écran, la gestion des modules, etc. Au-dessus du DAL se trouve le module Boot (Module ID 1), qui fournit de nombreux appels système orientés utilisateur. Ce module inclut des gestionnaires pour les données, la mémoire, les alarmes, les échanges, les bitmaps et les fenêtres. Enfin, le module UI (Module ID 2) offre toutes les primitives d’interface utilisateur, telles que les contrôles, les formulaires, les menus et les tableaux. Ensemble, ces trois modules constituent le cœur de PalmOS, et il serait même possible de démarrer un ROM contenant uniquement ces fichiers.
Ces trois premiers modules sont spéciaux, car ils sont toujours chargés et leurs fonctions exportées sont accessibles via un raccourci particulier. Pour les modules 0, 1 et 2, il est possible d’appeler une fonction exportée en exécutant deux instructions spécifiques. Ce raccourci facilite les appels aux fonctions système par les modules natifs, mais ne s’applique pas aux autres modules. Il est également possible de modifier ces tables de pointeurs de fonction, ce qui a souvent été fait par ce que l’on appelait des « hacks », et cela est également utilisé par le système d’exploitation lui-même.
PalmOS ne dispose d’aucune protection mémoire, ce qui signifie que tout code utilisateur peut accéder au matériel. Cela est exploité par des pilotes, comme ceux des cartes SD, qui sont généralement des modules séparés et non intégrés au DAL. Le module Boot charge toutes les bases de données de ressources PalmOS de certains types au démarrage, permettant leur initialisation. Parmi ces types, on trouve les pilotes de slot, les pilotes de système de fichiers, les pilotes de port série, et d’autres extensions. Cette séparation est pratique, car elle permet de remplacer ou de retirer facilement ces modules, bien qu’il existe des cas particuliers que les développeurs de PalmOS n’avaient pas anticipés.
Technologie vers le premier port non autorisé de PalmOS
Quelles sont les difficultés rencontrées ?
Comme mentionné précédemment, aucune documentation sur l’API native de PalmOS 5.x n’a jamais été publiée. Un petit nombre de personnes a réussi à comprendre certaines parties, mais personne n’a réussi à tout saisir. Cela est en partie dû au fait que de nombreuses fonctionnalités ne sont pas pertinentes pour un développeur d’applications, ce qui a limité l’intérêt. Cela pose un problème pour ceux qui souhaitent créer un nouvel appareil. J’ai donc dû effectuer beaucoup de rétro-ingénierie pour ce projet, en explorant des API ennuyeuses que je devais tout de même mettre en œuvre. De plus, il me fallait un noyau et du matériel fonctionnel.
Les formats de ROM sont complexes
Pour commencer, j’ai développé un outil permettant de décomposer et de reconstruire des images de ROM PalmOS. Le format est assez complexe et a évolué entre les versions, mais après de nombreux efforts, l’outil « splitrom » peut désormais décomposer avec succès une ROM PalmOS allant des appareils pré-version 1.0 jusqu’aux ROMs PalmOS 6.0 Cobalt. L’outil « mkrom » peut également produire des images valides de PalmOS 5.x, bien que je n’aie pas cherché à créer d’autres versions, car cela ne m’était pas nécessaire. À ce stade, j’ai pris un détour dans le projet pour collecter des ROM PalmOS.
Exploration des Défis de Développement d’un DAL
Après avoir acquis une expérience significative avec divers appareils et prototypes, j’ai décidé de partager mes découvertes. Mon processus a commencé par l’analyse d’un ROM T|T3, où j’ai remplacé certains fichiers, réassemblé le tout et reflashé mon T|T3. À ma grande surprise, il a démarré ! C’était prometteur.
Écrire un DAL : Une Étape Cruciale
Ne disposant d’aucun matériel pour effectuer des tests, ni d’un noyau à utiliser, j’ai dû agir rapidement. La solution la plus efficace que j’ai trouvée était d’utiliser un processeur ARM réel avec un noyau existant, en l’occurrence Linux. Comme mon ordinateur de bureau est basé sur un processeur x86, j’ai eu recours à QEMU. J’ai développé un DAL rudimentaire qui enregistrait chaque fonction appelée avant de provoquer un crash intentionnel. Au démarrage, il se comportait comme le DAL de PalmOS : il chargeait Boot et appelait PalmOSMain dans un nouveau thread. J’ai ensuite créé une application simple qui utilisait mmap() pour mapper une zone de mémoire à un emplacement spécifique, soutenue par « rom.bin » et « ram.bin », et j’ai tenté de la démarrer. J’ai obtenu quelques messages de journalisation et un crash, comme prévu. Cela semblait prometteur, mais j’ai réalisé que la plupart des fonctions étaient nécessaires pour le démarrage. Une journée décevante…
Construction d’un DAL Minimal
Après plusieurs mois de travail, j’ai réussi à implémenter la majorité du DAL, qui fonctionnait dans mon « runner » sous QEMU. C’était une configuration complexe. Étant donné que tout cela se déroulait dans un espace utilisateur sous Linux, je devais faire appel au « runner » pour des tâches comme la création de threads. C’était un véritable casse-tête. Le code actuel de rePalm prend encore en charge ce mode, mais je ne prévois pas de l’utiliser fréquemment pour diverses raisons. Par exemple, le noyau Linux ne dispose pas de certaines API essentielles à PalmOS, comme la possibilité de désactiver et de réactiver le changement de tâche. PalmOS demande parfois que la préemption soit désactivée, ce que Linux ne permet pas. De plus, PalmOS nécessite la capacité de mettre en pause et de reprendre un thread à distance, sans le consentement de celui-ci. La bibliothèque pthreads ne permet pas non plus cela. J’ai bricolé quelques solutions avec ptrace, mais c’était chaotique. Anecdote amusante : comme ma machine est multi-cœur et que je n’ai jamais défini d’affinités, c’était la première fois que PalmOS fonctionnait sur un appareil multi-cœur. Je ne m’en suis rendu compte que bien plus tard, mais c’était plutôt cool, non ?
Les Défis du Dessin
Un problème majeur est apparu. Pour une raison quelconque, des opérations telles que dessiner des lignes, des rectangles, des cercles et des bitmaps faisaient toutes partie du DAL. Bien que dessiner une ligne ne soit pas compliqué, des instructions comme « dessiner un rectangle arrondi avec une couleur de premier plan X et une couleur de fond Y, en utilisant le mode de dessin ‘masque’ sur ce canevas » ou « afficher cette image compressée en 16 bits couleur pleine à 144 ppi sur ce canevas de 4 bits par pixel à 108 ppi avec dithering, en respectant les couleurs de transparence, et en utilisant le mode ‘inverser' » deviennent rapidement complexes. Et oui, le DAL était censé gérer tout cela. Bien sûr, rien de tout cela n’était documenté ! C’était un véritable cauchemar. Au début, j’ai traité toutes les fonctions de dessin comme des NOP et j’ai simplement enregistré le texte dessiné pour suivre l’avancement de mon démarrage. Cela m’a permis de mettre en œuvre de nombreuses autres OsCalls que le DAL devait fournir, mais j’ai finalement dû me confronter à la nécessité de dessiner. Ma première approche a été de mettre en œuvre les choses moi-même, en me basant sur les noms de fonction et un peu d’ingénierie inverse. Cette méthode a échoué rapidement – la matrice des possibilités était tout simplement trop vaste. Il y a 8 modes de dessin, 3 densités supportées, 4 formats de compression d’image, 5 profondeurs de couleur supportées et deux formats de police. Il était impossible de tout envisager, surtout sans moyen de s’assurer que j’avais raison. Je ne suis pas sûr que certains de ces modes aient jamais été utilisés par un logiciel existant, mais peu importe - il fallait que ce soit pixel par pixel exact ! Que faire ?
Une Inspiration Contestable
J’ai décidé d’adopter une solution temporaire. J’ai désassemblé le DAL du Zire72. J’ai copié chacune des fonctions nécessaires, ainsi que toutes les fonctions qu’elles appelaient, et ainsi de suite. J’ai ensuite nettoyé leurs références directes aux variables globales du Zire DAL et entre elles, et j’ai regroupé le tout dans un énorme fichier « drawing.S ». Ce fichier contenait plus de 30 000 lignes, et je n’avais en grande partie aucune idée de son fonctionnement. Ou de son efficacité…
Et pourtant, cela a fonctionné ! Pas immédiatement, bien sûr, mais cela a fonctionné. Les couleurs étaient déformées, des artefacts apparaissaient partout, mais j’ai réussi à voir l’écran de calibration de l’écran tactile après le démarrage ! Succès, n’est-ce pas ? Eh bien, pas du tout. En fait, il s’est avéré qu’en raison d’optimisations, le code de dessin de PalmOS interagissait directement avec les variables globales du pilote d’affichage. À ce stade, mon « pilote » d’affichage n’était qu’une zone de mémoire soutenue par une surface SDL. Cela a nécessité beaucoup de travail (un travail inutile – le pire genre) pour comprendre ce qu’il recherchait et lui fournir ce dont il avait besoin. Mais après quelques semaines supplémentaires, le code de dessin du DAL du Zire72 fonctionnait sous rePalm, et j’ai pu voir les éléments dessinés correctement. Après avoir mis en place un support rudimentaire pour l’écran tactile, j’ai même pu interagir avec le dispositif virtuel et voir l’écran d’accueil. Super, mais tout cela était vain. Je ne possède pas ce code et je ne peux pas le distribuer. Je ne peux pas non plus l’améliorer, l’étendre, le corriger ou même prétendre le comprendre entièrement. Ce n’était pas une voie à suivre.
Une Réécriture Méticuleuse
Il était temps de passer à l’action. J’ai réécrit le code de dessin. Fonction par fonction. Ligne par ligne. Instruction par instruction. J’ai testé chaque fonction après l’avoir remplacée du mieux que je pouvais. En cours de route, j’ai acquis une compréhension de la manière dont PalmOS dessine, des raccourcis pour les cas courants, etc. Cet effort a duré deux mois, et à la fin, 30 000 lignes d’assemblage non commentées se sont transformées en 8 000 lignes de C. rePalm était enfin redevenu entièrement mon propre code ! Au passage, j’ai optimisé quelques éléments et ajouté le support pour une densité d’un et demi, une fonctionnalité que le DAL du Zire72 ne supportait pas. Parmi toutes les parties de ce projet, c’était la plus difficile à réaliser, car à la fin de chaque fonction décodée, comprise et réécrite, il n’y avait pas de progrès visible – l’objectif était simplement de ne rien casser, et il y avait toujours des dizaines de milliers de lignes de code à désassembler, comprendre et réécrire en C.
Création d’une Carte SD Virtuelle
Pour faciliter les tests, il serait pratique de pouvoir charger des programmes plus facilement dans l’appareil plutôt que de les intégrer dans le ROM. J’ai développé un pilote de slot personnalisé qui ne faisait rien d’autre que de permettre l’utilisation de mon système de fichiers personnalisé. Ce système de fichiers utilisait des hyper-appels pour accéder au code dans le « runner » afin d’effectuer des opérations de système de fichiers sur l’hôte. En gros, cela a créé un dossier partagé entre mon PC et rePalm. J’ai utilisé cela pour vérifier que la plupart des logiciels et des jeux fonctionnaient comme prévu.
Compatibilité des ROMs de Dispositifs
Je pouvais tester n’importe quelle ROM ! J’ai essayé l’image pré-production de Tungsten T, l’image de LifeDrive, et même la ROM de Sony TH55 a démarré ! Bien qu’il y ait eu des ajustements spécifiques à chaque appareil et à chaque version de l’OS, j’ai réussi à les appliquer automatiquement au moment de l’exécution. Par exemple, déterminer quelle version de l’OS était en cours d’exécution se faisait facilement en examinant le nombre de points d’entrée exportés de Boot. De plus, identifier si la ROM était un appareil Sony était simple en recherchant le module SonyDAL. Dans ce cas, nous refusons de la charger et exportons nous-mêmes des fonctions équivalentes. Pourquoi le DAL a-t-il besoin de connaître la version de l’OS ? Certains points d’entrée du DAL ont changé entre PalmOS 5.0 et PalmOS 5.2, et PalmOS 5.4 ou ultérieur attend des comportements supplémentaires de certaines fonctions existantes que nous devons prendre en charge.
Et maintenant, c’est fini, n’est-ce pas ?
Vers la création du premier appareil PalmOS pirate
Comprendre le fonctionnement des appareils PalmOS 5.x
Pour saisir les défis rencontrés, il est essentiel d’explorer le fonctionnement des appareils PalmOS 5.x. Ces dispositifs étaient conçus pour des processeurs ARMv4T ou ARMv5, avec une capacité de mémoire flash ou ROM variant de 4 à 32 Mo, et une RAM allant de 8 à 128 Mo pour les allocations en temps réel et le stockage de données. La version 5.4 de PalmOS a introduit le NVFS, que nous allons ignorer pour l’instant, comme beaucoup d’entre nous l’ont souhaité à l’époque de sa sortie. Les processeurs ARMv4T et ARMv5 utilisaient deux ensembles d’instructions distincts : ARM et Thumb. Les instructions ARM mesurent chacune exactement 4 octets et constituent l’ensemble d’instructions original pour les processeurs ARM. En revanche, Thumb, introduit avec la version v4T, visait à améliorer la densité du code en utilisant des instructions de 2 octets, permettant ainsi de réduire la taille du code. Cependant, cette optimisation avait un coût : les instructions Thumb nécessitaient souvent plus d’étapes pour accomplir les mêmes tâches, et elles avaient accès à moins de registres que leurs homologues ARM, ce qui pouvait entraîner un code moins optimal. Malgré cela, de larges portions de PalmOS étaient compilées en mode Thumb, car la vitesse n’était pas toujours un facteur déterminant. À l’inverse, Sony avait choisi de compiler l’ensemble de son système d’exploitation en mode ARM, profitant de puces flash plus grandes.
Les défis de la commutation entre ARM et Thumb
La commutation entre les modes ARM et Thumb dans les processeurs ARMv5 se faisait par certaines instructions qui modifiaient le flux de contrôle. Étant donné que toutes les instructions ARM mesurent 4 octets et sont alignées sur une frontière de 4 octets, toute adresse d’instruction ARM valide a ses deux derniers bits à zéro. Les instructions Thumb, quant à elles, mesurent 2 octets et ont donc leur dernier bit à zéro. Cela a permis d’utiliser cette caractéristique pour la commutation de mode. L’instruction BX vérifiait le bit inférieur du registre de destination : s’il était à 1, la destination était considérée comme Thumb, sinon comme ARM. D’autres instructions, comme POP et LDR, fonctionnaient de la même manière. Cependant, l’architecture ARMv5 est désormais obsolète, l’architecture ARM ayant évolué jusqu’à la version v8.x, qui propose des registres de 64 bits et un ensemble d’instructions totalement différent. Bien que les cartes de développement ARMv7 soient abondantes et peu coûteuses, leur documentation est souvent insuffisante, rendant leur utilisation complexe sans un noyau Linux.
Exploration des microcontrôleurs ARM
Une alternative intéressante était d’explorer les microcontrôleurs, qui sont largement disponibles, bien documentés et économiques. ARM propose une gamme de cœurs sous la marque Cortex, tels que Cortex-M0, M3 et M4, qui utilisent un ensemble d’instructions Thumb/Thumb2. Ces cœurs sont adaptés pour des tâches complexes, prenant en charge des opérations de multiplication étendues et des flux de contrôle sophistiqués. Cependant, un inconvénient majeur est que ces microcontrôleurs ne prennent en charge que les instructions Thumb, ce qui limite l’exécution de nombreuses fonctionnalités de PalmOS, car chaque bibliothèque doit être entrée en mode ARM. Cela posait un problème significatif pour l’exécution d’un système d’exploitation non modifié.
Une solution innovante pour l’intégration de PalmOS
Face à ces limitations, j’ai décidé d’étendre le format de module de PalmOS pour permettre une entrée directe en mode Thumb. J’ai également modifié le chargeur de modules pour qu’il reconnaisse les points d’entrée des bibliothèques pointant vers un simple thunk ARM vers Thumb, ce qui a permis un démarrage presque complet sans nécessiter de mode ARM. Cependant, cette solution n’était pas suffisante, car de nombreuses parties du système d’exploitation restaient en mode ARM, notamment des fonctions essentielles comme MemMove et MemCmp. L’objectif de faire fonctionner un PalmOS non modifié restait donc un défi de taille.
Défis de l’architecture ARM
Dans le domaine des systèmes d’exploitation et des applications, la possibilité d’éditer tout et n’importe où était limitée. Certaines fonctionnalités pouvaient être ajustées via SysPatchEntry. J’ai ainsi optimisé les fonctions MemMove et MemCmp pour améliorer la vitesse, en fournissant des implémentations optimales pour Thumb2. Cependant, d’autres éléments, comme la division entière (qui n’est pas prise en charge par ARMv5), étaient disséminés dans presque toutes les bibliothèques et ne pouvaient pas être modifiés car ils n’étaient pas exportés. Nous avions réellement besoin d’un environnement capable d’exécuter des instructions ARM.
Exploration des possibilités
Que se passerait-il si nous tentions de passer un microcontrôleur ARMv7M en mode ARM ? Le manuel est très explicite à ce sujet. Il CHANGERA le mode, effacera le bit d’état indiquant que nous sommes en mode Thumb, et lorsqu’il tentera d’exécuter l’instruction suivante, il déclenchera une UsageFault car il ne peut pas fonctionner dans ce mode. L’instruction Thumb BLX qui change toujours de mode est indéfinie dans ARMv7M, et si elle est exécutée, le processeur générera également une UsageFault, signalant une instruction invalide. Bien que cela semble décourageant, c’est en réalité une excellente nouvelle ! Nous pouvons intercepter une UsageFault… Si vous comprenez où je veux en venir et que cela vous horrifie, merci de votre attention ! Nous reviendrons sur cette intrigue plus tard pour permettre à tout le monde de se mettre à jour.
Technologie : Le besoin de matériel
CortexEmu : La solution
J’avais l’intention de faire fonctionner tout cela sur une puce de classe Cortex-M, mais je ne voulais pas développer directement sur celle-ci – c’était trop lent et frustrant. De plus, je n’ai trouvé aucun bon émulateur pour les puces de classe Cortex-M. À ce stade, j’ai pris une pause de deux semaines pour créer CortexEmu. Cet émulateur fonctionnel pour Cortex-M0/M3/M23 reproduit fidèlement le matériel Cortex. Il dispose d’un stub GDB pour le débogage, d’une émulation matérielle rudimentaire pour afficher un écran, et prend en charge un RTC, une console et un écran tactile. Il gère les modes privilégiés et non privilégiés, et émule également l’unité de protection de la mémoire (MPU). CortexEmu reste le meilleur moyen de développer rePalm.
Attendez ! Vous avez promis du matériel réel
Oui, oui, nous y arriverons, mais cela prendra encore plusieurs mois dans l’histoire, alors soyez patients !
Technologie : La nécessité d’un noyau
Pourquoi ne pas utiliser Linux ?
PalmOS nécessite un noyau avec un ensemble particulier de primitives. Nous avons déjà abordé certaines (mais pas toutes) des raisons pour lesquelles Linux est un choix peu judicieux. De plus, la version Linux compatible avec Cortex-M3 est lente ET volumineuse, ce qui en fait une option inacceptable. Alors, quelle alternative avons-nous ?
J’ai finalement décidé d’écrire mon propre noyau. Il est simple et fonctionne bien. Il peut s’exécuter sur n’importe quel processeur de classe Cortex-M, prend en charge le multithreading avec priorités, des minuteries précises, des mutex, des sémaphores, des groupes d’événements, des boîtes aux lettres, et toutes les primitives que PalmOS exige, comme la possibilité de suspendre des threads et de désactiver le changement de tâche. Il utilise également le MPU pour ajouter des mesures de sécurité de base, comme des gardes de pile. De plus, il offre un excellent support pour le stockage local des threads, ce qui sera utile par la suite. Pourquoi créer mon propre noyau alors qu’il en existe déjà tant ? Aucun des noyaux disponibles ne possédait vraiment les primitives dont j’avais besoin, et les ajouter aurait pris tout autant de temps.
Technologie : Les défis du code ARM
Le code ARM reste problématique
PalmOS ne parvenait toujours pas à démarrer complètement jusqu’à l’interface utilisateur à cause du code ARM. Mais, comme je l’ai mentionné précédemment, nous pouvons intercepter les tentatives d’accès au mode ARM. J’ai donc écrit un gestionnaire de UsageFault pour cela, puis… je l’ai émulé.
Vous ne voulez pas dire… ?
Oh, mais si. J’ai développé un émulateur ARM qui lit chaque instruction et l’exécute, jusqu’à ce que le code sorte du mode ARM, moment auquel je quitte l’émulation pour reprendre l’exécution native. Les détails de ce fonctionnement sont fascinants, car l’émulateur nécessite sa propre pile et ne peut pas s’exécuter sur la pile du code émulé. Il doit également y avoir un espace pour stocker les registres émulés, car nous ne pouvons pas simplement les garder dans les registres réels (il n’y a pas assez de registres pour les deux). Quitter l’émulation est également un défi, car il faut charger tous les registres et le registre d’état en une seule opération atomique. Ce n’est pas trivial sur Cortex-M. Quoi qu’il en soit, « emu.c » et « emuC.c » contiennent le code – n’hésitez pas à explorer.
Écrire un émulateur en C n’est pas lent ?
Vous n’avez aucune idée ! L’émulateur était effectivement lent. J’ai instrumenté CortexEmu pour compter les cycles et j’ai constaté qu’il fallait en moyenne 170 cycles du processeur hôte pour émuler une seule instruction ARM. Ce n’était pas suffisant. Il est bien connu que les émulateurs écrits en C sont lents. Les compilateurs C ne sont pas très efficaces pour optimiser le code des émulateurs. Que faire alors ? J’ai donc réécrit le cœur de l’émulateur en assembleur. En fait, je l’ai fait deux fois : une fois pour ARMv7M (cible Cortex-M3) et une fois pour ARMv6M (cible Cortex-M0). La vitesse s’est considérablement améliorée. Pour le cœur M3, j’atteignais en moyenne 14 cycles par instruction, et pour le M0, 19. Une performance d’émulateur très respectable, si je puis me permettre de le dire.
Est-ce assez rapide maintenant ?
Comme mentionné précédemment, sur les appareils PalmOS d’origine, le code ARM était généralement plus rapide que le code Thumb, donc la plupart des routines les plus critiques étaient écrites en ARM. Pour nous, le code ARM est 14 fois plus lent que le code Thumb. Ainsi, le code censé être le plus rapide est devenu lent. Mais examinons ce code de plus près. Les routines de division en font partie. ARMv7M gère la division en matériel, mais ARMv5 ne le faisait pas (et ARMv6M non plus). Ces routines prennent environ une centaine de cycles en mode ARM. Les fonctions MemMove, MemMSet et MemCmp dont nous avons déjà parlé ne nous préoccupent pas car nous les avons remplacées, mais de nombreuses bibliothèques avaient leurs propres copies internes que nous ne pouvons pas modifier. Je suppose que le compilateur préfère en général intégrer ses propres « memset » et « memcpy ». Cela représentait une grande partie de l’utilisation du code ARM lors du processus de démarrage. Heureusement, toutes ces fonctions sont identiques partout…
Pouvons-nous donc identifier certaines de ces fonctions dans le code de l’émulateur et exécuter des routines natives plus rapides ? J’ai fait cela et le temps de démarrage a effectivement diminué. Le coût moyen par instruction a augmenté en raison de l’identification, mais le temps de démarrage a été réduit. Super. Mais que se passe-t-il après le démarrage ? Après le démarrage, nous rencontrons le véritable défi… l’émulateur m68k de PACE est écrit en ARM. 60 kilooctets de code clairement écrit à la main avec de nombreuses astuces astucieuses. Ces astuces deviennent problématiques lorsque vous devez les émuler… Cela signifie que chaque application m68k (qui est la plupart d’entre elles) fonctionne désormais sous double émulation. Pas idéal… Et aussi : lent. Il fallait agir. J’ai envisagé de réécrire PACE, mais cela n’est pas une solution viable – il existe de nombreuses bibliothèques ARM et je ne peux pas toutes les réécrire. De plus, comment puis-je prétendre exécuter un système d’exploitation non modifié si je remplace chaque élément ?
Il existe une autre méthode pour rendre le code non natif plus rapide…
Technologie : Une solution innovante
Traduction à la volée
PACE contient beaucoup de code statique très sollicité. Sur les appareils réels, il réside dans la ROM et ne change pas. La plupart des bibliothèques sont identiques. Que pouvons-nous faire pour améliorer la vitesse d’exécution ? Les traduire en un format que nous pouvons exécuter nativement, bien sûr. La plupart des gens hésiteraient à entreprendre seuls la tâche d’écrire un traducteur à la volée. Mais cela ne fait que parce qu’ils sont…
Introduction aux JIT : Comment démarrer ?
Pour initier un JIT, nous procédons de manière similaire à celle utilisée pour un émulateur. Nous mettons en place un cache de traduction par thread (CT) qui stockera nos traductions. Pourquoi par thread ? Cela permet d’éviter que l’un d’eux ne purge le cache pendant qu’un autre est en cours d’exécution, ce qui pourrait entraîner des problèmes. Le CT contiendra des unités de traduction (UT), chacune représentant un code traduit. Chaque UT inclut l’adresse ARM d’origine et le code Thumb2 valide. De plus, une table de hachage sera utilisée pour associer les adresses »ARM » d’origine à un seau où la première UT correspondant à cette valeur de hachage est stockée. Chaque seau est une liste chaînée, et nous utilisons 4096 seaux, un chiffre configurable. Un hachage rapide et simple est appliqué, et les tests sur un échantillon représentatif d’adresses ont montré une bonne distribution. Ainsi, chaque fois qu’un UsageFault se produit, indiquant une tentative d’entrée en mode ARM, nous consultons la table de hachage pour l’adresse souhaitée. En cas de succès, nous remplaçons simplement le PC dans le cadre d’exception par le pointeur « code » de l’UT correspondante et continuons l’exécution du code natif rapidement. Parfait ! Que se passe-t-il si nous n’obtenons pas de correspondance ? Nous sauvegardons alors l’état et remplaçons le PC dans le cadre d’exception par l’adresse du code de traduction (nous ne voulons pas traduire en mode noyau).
Comprendre les instructions ARM
Le front-end d’un JIT doit essentiellement ingérer les instructions ARM et les interpréter. Nous allons intercepter celles que nous ne comprenons pas et tenter de traduire toutes celles que nous maîtrisons. Cependant, nous rencontrons un premier obstacle. Certains jeux utilisent des instructions non valides. Par exemple, le jeu « Bejeweled » contient du code ARM qui tente d’exécuter LDMDB R11, {R0-R12, SP, PC}^. Ignorons le fait que R0-R2 et R12 n’ont pas besoin d’être sauvegardés, ce qui est inefficace, et notons que cette instruction n’est pas valide en mode utilisateur. Le petit symbole caret à la fin signifie « transférer également SPSR vers CPSR« . Cette demande est invalide en mode utilisateur, et le manuel de référence de l’architecture ARM précise clairement que son exécution en mode utilisateur aura des effets indéfinis. Cela explique pourquoi Bejeweled ne fonctionnait pas sous rePalm avec QEMU, qui a correctement refusé d’exécuter cette instruction. J’ai donc sorti un appareil Palm d’un tiroir pour tester ce qui se passe réellement lors de son exécution. Il s’avère que cela est simplement ignoré. Mon JIT fera donc de même. Mes cœurs d’émulateur n’ont eu aucun problème avec cette instruction, car étant indéfinie, la traiter comme si elle n’avait pas de caret était sûr, et ils n’ont donc même pas vérifié le bit indiquant cela.
Heureusement, ARM ne possède que quelques formats d’instructions. Malheureusement, ils sont tous assez complexes. La bonne nouvelle, c’est que le décodage est relativement simple. Presque chaque instruction ARM est conditionnelle, et les 4 bits supérieurs déterminent si elle s’exécute ou non. Les opérations de traitement de données utilisent toujours trois opérandes : registre de destination, registre source et « opérande », qui est le mode d’adressage 1 d’ARM. Cela peut être une valeur immédiate de certaines formes, un registre, un registre décalé par une valeur immédiate, ou un registre décalé par un autre registre. Par exemple, on peut exécuter ADD R0, R1, R2, ROR R3. Cela peut sembler intimidant ! La définition des indicateurs est optionnelle. Le chargement et le stockage de bytes ou de mots utilisent le mode d’adressage 2, qui permet d’utiliser un registre plus ou moins une valeur immédiate, ou un registre plus ou moins un autre registre, ou un registre plus ou moins un registre décalé par une valeur immédiate. Tous ces modes peuvent être indexés, post-indexés ou indexés avec écriture, ce qui permet des instructions complexes comme LDR R0, [R1], R2, LSL #12. Le chargement et le stockage de demi-mots ou de données signées utilisent le mode d’adressage 3, qui est similaire au mode 2, mais sans décalage de registre. Ce mode est également utilisé pour les instructions LDRD et STRD que certains cœurs ARMv5 implémentent (partie de l’extension DSP optionnelle). Le mode d’adressage 4 est utilisé pour les instructions LDM et STM, qui sont redoutables en raison de leur complexité et de leurs nombreux cas particuliers. Elles peuvent charger ou stocker n’importe quel sous-ensemble de registres à une adresse de base donnée avec incrémentation ou décrémentation avant ou après, et écriture optionnelle. Elles sont souvent utilisées pour les opérations de pile. Enfin, les branches sont toutes encodées de manière simple et se décodent facilement. Ouf !
Les défis de la traduction des instructions Thumb2
Au départ, on pensait que la traduction ne serait pas si difficile. Les instructions semblent similaires, et cela ne devrait pas poser trop de problèmes. Puis la réalité a frappé. Thumb2 impose de nombreuses restrictions sur les opérandes. Par exemple, SP ne peut pas être traité comme un registre général, et LR et PC ne peuvent jamais être chargés ensemble. De plus, il n’offre pas la même capacité que le mode d’adressage 1 pour décaler un registre par un autre comme troisième opérande dans une opération ALU. Il ne permet pas de décaler un troisième registre de plus de 3, contrairement au mode 2 d’ARM. Je ne vais même pas aborder les instructions LDM et STM ! De plus, il y a le problème de ne pas permettre au code traduit de savoir qu’il est en cours de traduction. Cela signifie qu’il doit toujours croire qu’il s’exécute à partir de son emplacement d’origine, et s’il se lit lui-même, il doit voir des instructions ARM. Cela implique que nous ne pouvons jamais divulguer la valeur réelle de PC dans un état exécutable. En pratique, cela signifie que nous ne pouvons jamais émettre une instruction BL, et chaque fois que PC est lu, nous devons produire une valeur immédiate équivalente à ce que PC aurait été si le code ARM avait été exécuté à son emplacement réel en mémoire. Pas très amusant…
Les instructions LDM/STM de Thumb2 manquent en fait de la moitié des modes disponibles dans ARM (modes ID et DA), ce qui nous oblige à étendre ces instructions à beaucoup plus de code. De plus, Thumb impose des limites sur l’écriture qui ne correspondent pas à celles d’ARM (plus strictes), et il est impossible d’utiliser SP dans l’ensemble des registres, ni de stocker PC de cette manière dans Thumb2. À ce stade, il devient évident que la tâche de traduire instruction par instruction ne sera pas simple. Nous aurons besoin d’endroits pour stocker des valeurs immédiates temporaires, nous devrons réécrire de nombreuses instructions, et tout cela sans provoquer d’effets secondaires. Et cela doit également être rapide !
Les complexités des opérations LDM et STM
Fonctionnement des LDM et STM en ARM
ARM dispose de deux opérations à plusieurs registres : LDM et STM. Chacune d’elles propose plusieurs modes d’adressage. Tout d’abord, il y a l’ordre : croissant ou décroissant dans les adresses (c’est-à-dire, le registre de base indique-t-il où stocker le registre le moins numéroté ou le plus élevé). Ensuite, il s’agit de savoir si le registre de base doit être utilisé tel quel ou s’il doit être incrémenté ou décrémenté au préalable. Cela nous donne les quatre modes de base : IA (« incrément après »), IB (« incrément avant »), DA (« décrément après »), DB (« décrément avant »). De plus, il est optionnel de mettre à jour l’adresse de base dans le registre de base. Bien sûr, il existe des cas particuliers, comme la valeur qui est stockée si le registre de base avec mise à jour est utilisé, ou quelle valeur le registre de base aura s’il est chargé alors que la mise à jour est également spécifiée. La spécification ARM définit explicitement certains de ces cas comme ayant des conséquences imprévisibles.
Pour la pile, ARM utilise une pile descendante complète. Cela signifie qu’à tout moment, le registre SP pointe vers la dernière position de pile déjà utilisée. Ainsi, pour dépiler une valeur, vous la chargez depuis [SP], puis vous incrémentez SP de 4. Cela se fait en utilisant une instruction LDM.
Comprendre le fonctionnement des instructions LDM/STM dans Thumb2
Pour introduire le sujet, il est essentiel de comprendre comment les valeurs sont gérées dans la pile. Pour ajouter une valeur à la pile, il faut d’abord diminuer le registre de pile (SP) de 4, puis stocker la valeur souhaitée à l’adresse indiquée par SP. Cela correspond à une instruction STM utilisant un mode d’adressage DB. Les modes IB et DA ne sont généralement pas utilisés pour la gestion de la pile dans le code ARM classique.
Les limitations de Thumb2
Pourquoi est-ce important ? Lors de la conception de l’ensemble d’instructions Thumb2, ARM a fait des choix sur les fonctionnalités à inclure. Cela a conduit à l’exclusion de certaines options moins courantes. En effet, Thumb2 ne prend pas en charge les modes IB et DA. De plus, il est interdit d’utiliser les registres PC ou SP dans la liste des registres à stocker lors d’une instruction STM. Il est également impossible de charger SP avec LDM, et si une instruction LDM charge PC, elle ne peut pas charger LR en même temps, et vice versa. En outre, PC ne peut pas être utilisé comme registre de base, et la liste des registres doit contenir au moins deux éléments. Cela constitue une liste partielle des limitations de Thumb2 par rapport à ARM.
Complexité de la traduction des instructions
Les instructions qui se traduisent bien d’ARM à Thumb2 ne sont pas toujours simples à convertir. Par exemple, le stockage de PC est complexe, car il faut un registre temporaire pour conserver la valeur attendue de PC avant de l’ajouter à la pile. Les registres sont empilés dans un ordre précis, donc le choix du registre temporaire peut affecter la relative position des autres registres, nécessitant parfois de diviser le stockage en plusieurs opérations. Si le stockage implique SP, il faut également ajuster en conséquence. Dans le cas d’une instruction STMDB SP (c’est-à-dire PUSH), il devient difficile de préempiler un registre temporaire.
Les défis des instructions atomiques
Une autre complication réside dans le fait que LDM/STM est censé agir comme une instruction atomique pour l’espace utilisateur. Cela signifie qu’elle doit être soit annulée, soit reprise au niveau système. Cependant, dans les puces Cortex-M utilisant Thumb2, SP a une importance particulière, car le cadre d’exception y est stocké. Cela implique que SP doit toujours être valide, et toute donnée stockée en dessous de SP n’est pas garantie de persister, car une interruption peut survenir à tout moment. Heureusement, sur ARM, il était également déconseillé de stocker des données en dessous de SP, ce qui était rare. Un exemple notoire est un morceau de code PalmOS qui le fait, mais pour d’autres raisons, ce code a été remplacé. Dans tous les autres cas, le JIT émettra un avertissement si une tentative de chargement ou de stockage en dessous de SP est effectuée.
La complexité de la traduction LDM/STM
- Vérifier si l’instruction entraîne un comportement indéfini ou n’est pas définie selon le Manuel de Référence de l’Architecture ARM. Si c’est le cas, enregistrer une erreur et abandonner.
- Déterminer si elle peut être émise comme une instruction Thumb2 LDM/STM, c’est-à-dire si elle respecte toutes les restrictions imposées par Thumb2, et si PC n’est pas stocké, émettre une instruction Thumb2 LDM/STM.
- Vérifier si elle peut être émise comme LDR/STR/LDRD/STRD tout en respectant les limites de Thumb2. Si c’est le cas, émettre cette instruction.
- Traiter quelques cas spéciaux pour des traductions rapides d’instructions courantes non couvertes par les étapes précédentes.
- Pour les modes non pris en charge IB et DA, s’ils ne nécessitent pas de retour d’écriture, ils peuvent être réécrits en termes de modes pris en charge.
- Si l’instruction charge SP, il est impossible d’émettre une traduction valide en raison de l’utilisation de SP dans ARMv7-M. Pour ce cas particulier, le JIT émet une instruction indéfinie et nous l’imitons. Heureusement, aucun code courant ne l’utilise !
- Enfin, suivre le chemin générique lent :
- Générer une liste de registres à charger/stocker et à quelles adresses.
- Calculer le retour d’écriture si nécessaire.
- Si besoin, allouer un ou deux registres temporaires (deux si PC et SP sont stockés) et sauvegarder leur contenu sur la pile.
- Pour tous les registres restants à charger/stocker, déterminer combien peuvent être chargés/stokés simultanément et procéder. Cela implique d’émettre une série d’instructions : LDR/STR/LDRD/STRD/LDM/STM jusqu’à ce que tout soit fait.
- Si des registres temporaires ont été alloués, les restaurer.
Instructions conditionnelles et leur complexité
Les modes d’adressage complexes, notamment le mode 1, posent également des défis. Grâce aux modes de rotation par registre, un registre temporaire est nécessaire pour calculer la valeur avant de l’utiliser. Si le registre de destination n’est pas utilisé, il peut servir de stockage temporaire, à condition qu’il ne soit pas également l’un des autres opérandes sources, ou SP, ou PC. Cela complique encore la situation. Si PC est également un opérande, un registre temporaire est requis pour charger la valeur « factice » de PC avant de pouvoir effectuer des opérations. Cela peut rapidement devenir un casse-tête. Pour plus de détails, consultez « emuJit.c ». nous faisons de notre mieux pour éviter de déverser des données sur la pile, mais parfois, cela s’avère inévitable.
Cette complexité s’applique également à certains modes d’adressage complexes. Thumb2 a optimisé ses instructions pour les cas courants, rendant les cas moins fréquents très difficiles à traduire. Il est encore plus difficile de trouver des registres temporaires, car si nous empilons quoi que ce soit, nous devons en tenir compte si notre registre de base est SP. les traductions efficaces sont réservées aux cas courants, tandis que les cas rares sont souvent négligés. Un cas particulier concerne les chargements basés sur PC, utilisés pour charger des données constantes. Dans la plupart des cas, nous intégrons les données constantes dans les traductions produites pour des raisons de rapidité.
Instructions Conditionnelles dans Thumb2
Le système Thumb2 offre des mécanismes pour établir des instructions conditionnelles grâce à l’instruction IT, qui permet de rendre les 1 à 4 instructions suivantes conditionnelles. J’ai choisi de ne pas l’utiliser, car cela modifie également la manière dont les indicateurs sont définis par les instructions Thumb de 2 octets, et je ne souhaitais pas créer des cas particuliers. De plus, il arrive que 4 instructions ne suffisent pas pour une traduction complète. Par exemple, certaines instructions STMDA peuvent s’étendre sur environ 28 instructions. Dans ces cas, j’émet un saut de polarité opposée (condition) sur la traduction. Cela fonctionne, car ces sauts ne font également que 2 octets pour toutes les longueurs de traduction possibles.
Sauts et Appels
Ce domaine est particulièrement fascinant. En gros, il existe deux types de sauts/appels : ceux dont les destinations sont connues au moment de la traduction et ceux dont elles ne le sont pas. Les sauts avec des adresses connues sont relativement simples à gérer. Nous recherchons l’adresse de destination dans notre TC. Si elle est trouvée, nous émettons un saut direct vers cette unité de traduction (TU). Cela rend les boucles fréquentes très rapides, car aucune sortie du code traduit n’est nécessaire. Les sauts indirects ou calculés sont moins courants, ce qui pourrait laisser penser qu’ils ne sont pas très importants. Cependant, c’est une erreur, car un type de saut indirect se produit fréquemment : le retour de fonction. Au moment de la traduction, nous ne savons pas où le retour va se diriger. Comment gérons-nous cela ? Si le code charge directement PC, tout fonctionne comme prévu. Soit il s’agit d’une adresse ARM et notre gestionnaire UsageFault s’en occupera, soit il s’agit d’une adresse Thumb et notre CPU y sautera directement. Une optimisation existe pour le cas où une instruction BX LR est rencontrée. Nous émettons alors un saut direct vers une fonction qui recherche LR dans le hachage, ce qui nous fait gagner le temps nécessaire pour gérer une exception et y revenir (environ 60 cycles). D’autres optimisations sont possibles et seront ajoutées, mais pour l’instant, c’est ainsi que cela fonctionne. Que faire pour un saut dont la destination est connue mais qui n’a pas encore été traduite ? Nous laissons un marqueur, c’est-à-dire une instruction que nous savons être indéfinie, suivie de l’adresse cible. Ainsi, si le saut est effectivement pris (ce qui n’est pas toujours le cas), nous prendrons l’exception, traduirons, puis remplacerons cette instruction indéfinie et le mot qui la suit par un saut réel. La prochaine fois, ce saut sera rapide, sans provoquer d’exceptions.
Processus de Traduction d’une TU
Le processus est simple : nous traduisons les instructions jusqu’à atteindre une instruction que nous considérons comme terminale. Qu’est-ce qui est terminal ? Un saut inconditionnel est terminal. Un appel l’est aussi (qu’il soit conditionnel ou non). Pourquoi ? Parce qu’il se peut que quelqu’un revienne de cet appel, et nous préférons que le code de retour soit dans une nouvelle TU afin de pouvoir le retrouver lors du retour. Une écriture inconditionnelle dans PC de quelque sorte que ce soit est également terminale. Il y a une astuce pour les sauts vers des endroits proches. En traduisant une TU, nous gardons une trace des dernières instructions traduites et de leurs traductions. Ainsi, si nous voyons un saut court en arrière, nous pouvons littéralement intégrer un saut vers cette traduction, créant ainsi une traduction très rapide de cette petite boucle. Mais qu’en est-il des sauts courts en avant ? Nous les mémorisons également, et si avant d’atteindre notre instruction terminale, nous traduisons une adresse que nous avons mémorisée d’un saut précédent dans cette même TU, nous retournerons et remplacerons ce saut par un saut court vers ici.
Que faire si le TC est plein ?
Vous aurez peut-être remarqué que j’ai mentionné que nous émettons des sauts entre les TUs. « Cela ne signifie-t-il pas, » pourriez-vous demander, « que vous ne pouvez pas simplement supprimer une TU ? » C’est exact. Il s’avère que suivre quelles TUs sont beaucoup utilisées et lesquelles ne le sont pas est trop complexe, et les avantages des sauts inter-TU sont trop importants pour être ignorés. Que faisons-nous donc lorsque le TC est plein ? Nous le vidons – nous le jetons littéralement. Cela aide également à s’assurer que les anciennes traductions qui ne sont plus nécessaires sont finalement éliminées. Le TC de chaque thread croît jusqu’à une taille maximale. Certains threads n’exécutent jamais beaucoup d’ARM et finissent avec des TCs petits. Le TC du thread principal de l’interface utilisateur atteindra pratiquement toujours la taille maximale (actuellement 32 Ko).
Améliorations du JIT
Après avoir mis en place le JIT, j’ai entrepris de le réécrire. La version initiale contenait de nombreuses valeurs magiques et des lacunes (des cas qui pouvaient se produire dans un code légitime mais qui seraient mal traduits). Elle émettait parfois des opcodes invalides que le Cortex-M4 exécutait malgré les documents indiquant qu’ils n’étaient pas autorisés. Le JIT a été divisé en deux parties. La première était le frontend qui ingérait les instructions ARM, maintenait le TC et suivait divers autres états. La seconde était le backend. Le backend avait une fonction pour chaque mode d’adressage ARMv5 ou format d’instruction possible, et pour toute instruction ARMv5 valide, il pouvait produire une séquence d’instructions ARMv7M pour accomplir la même tâche. Pour les cas courants, la séquence était bien optimisée, tandis que pour les cas moins fréquents, elle ne l’était pas. Cependant, le backend gère TOUTE demande ARMv5 valide possible, même des instructions étranges comme RSBS PC, SP, PC, ROR SP. Personne de sensé ne produirait jamais cette instruction, mais le backend la traduira correctement. J’ai écrit des tests et les ai exécutés automatiquement pour vérifier que toutes les entrées possibles sont traitées correctement. J’ai également optimisé le chemin le plus fréquent dans tout le système – l’émulation de l’instruction BLX en mode Thumb. Cela a permis un gain de 50 cycles, ce qui a eu un impact notable sur les performances. En outre, j’ai remarqué que souvent, le code Thumb utilisait un BLX simplement pour sauter à un OsCall (qui, en raison de l’utilisation de R12 et R9, ne peut pas être écrit en mode Thumb). Le nouveau gestionnaire BLX détecte cela et évite l’émulation en appelant directement l’OsCall requis.
J’ai ensuite développé un sous-backend pour l’extension EDSP (instructions ARMv5E) car certaines applications Sony les utilisent. La raison d’un sous-backend séparé est que l’ARMv7E (Cortex-M4) dispose d’instructions que nous pouvons utiliser pour traduire les instructions EDSP de manière très efficace, tandis que l’ARMv7 (Cortex-M3) ne le fait pas et nécessite des séquences d’instructions plus longues pour accomplir le même travail. rePalm prend en charge les deux.
Plus tard, j’ai revisité le code et, malgré la complexité, j’ai trouvé un moyen d’utiliser l’instruction IT sur Cortex-M3+. Cela a entraîné une refonte massive du code – en gros, en poussant le « code conditionnel » dans chaque fonction backend et en s’attendant à ce qu’elle se conditionne comme elle le souhaite. Cela a produit un changement avec un diff de plus de 4000 lignes, mais cela fonctionne très bien et a entraîné une augmentation de la vitesse !
Le Backend Cortex-M0
Pourquoi c’est un défi
C’était un véritable défi, mais je voulais voir si je pouvais créer un backend fonctionnel pour Cortex-M0 dans mon JIT. Le Cortex-M0 exécute l’ensemble d’instructions ARMv6-m. Cela correspond essentiellement à Thumb-1, avec quelques ajouts mineurs. Pourquoi cela est-il préoccupant ? Dans Thumb-1, la plupart des instructions n’ont accès qu’à la moitié des registres (r0..r7). Seules trois instructions ont accès aux registres supérieurs : CMP, MOV et ADD. Presque toutes les instructions Thumb-1 définissent toujours des indicateurs. De plus, il n’existe pas d’instructions de multiplication longue dans Thumb-1. Et il n’y a pas de mode de rotation RRX du tout. La confluence de tous ces problèmes rend la tentative d’une traduction instruction par instruction de l’ARM vers Thumb-1 pratiquement impossible.
Les Fondamentaux
Les registres r0 à r3 sont des registres de travail temporaires pour nous. Le registre r4 est l’endroit où nous stockons notre contexte (l’endroit où nous conserverons l’état supplémentaire). De plus, pour des raisons de rapidité, nous aurons besoin d’un registre pour stocker le registre d’état virtuel. Pourquoi en avons-nous besoin ? Parce que presque toutes nos instructions Thumb-1 modifient les indicateurs, tandis que le code ARM que nous traduisons s’attend à ce que les indicateurs restent valides pendant de longues séquences d’instructions. Ainsi, notre total est de 6. Nous avons besoin de 6 registres. Ils doivent être des registres bas, car, comme nous l’avons discuté, les registres supérieurs sont pratiquement inutiles dans Thumb-1.
Technologie : La rapidité de PACE est-elle suffisante ?
Les sauts indirects…
Malheureusement, l’émulation implique presque toujours de nombreux sauts indirects. C’est en effet la méthode utilisée pour le décodage des instructions. L’architecture 68k, étant une architecture CISC avec des instructions de longueur variable, rend cette étape de décodage complexe. L’émulateur de PACE est clairement écrit à la main en assembleur, avec quelques astuces. Il s’agit d’un code entièrement ARM, et il est identique instruction par instruction de PalmOS 5.0 à PalmOS 5.4. Bien que le code environnant ait changé, le cœur de l’émulateur est resté inchangé. C’est une bonne nouvelle, car cela signifie qu’il était déjà performant. Mon JIT gère correctement la traduction de PACE, comme en témoigne le fait que rePalm fonctionne sur ARMv7-M. Le principal problème réside dans le fait que chaque instruction émulée nécessite au moins un saut indirect (pour les instructions courantes), deux pour celles de fréquence moyenne, et jusqu’à trois pour certaines rares. En raison du fonctionnement de mon JIT, chaque saut indirect qui n’est pas un retour de fonction nécessite une exception (14 cycles à l’entrée, 12 à la sortie), un code d’assemblage supplémentaire (~30 cycles) et une recherche de hachage (~20 cycles). Ainsi, même si le code cible a été traduit, cela ajoute environ 70 cycles à chaque saut indirect. Cela limite l’efficacité de l’émulateur 68k à un rapport de 1/70 de la vitesse. Ce n’est pas idéal. En général, PACE fonctionne à environ 1/15 de la vitesse du code natif, ce qui représente un ralentissement significatif. J’ai envisagé d’écrire une meilleure traduction spécifiquement pour PACE, mais cela s’avère assez complexe. En effet, il n’existe pas de méthode simple et rapide pour traduire des instructions comme LDR R0, [R11, R1, LSL #2]; ADD PC, R11, R0. Il est impossible de savoir où ce saut ira, ou même si R11 pointe vers un emplacement immuable. Malheureusement, c’est exactement ce à quoi ressemble le dispatching de haut niveau de PACE.
Une solution spéciale pour un problème particulier
J’avais déjà atteint mon objectif de faire fonctionner PalmOS sans modifications – PACE fonctionne avec mon JIT, et le système d’exploitation est utilisable et réactif. Cependant, je souhaitais une solution améliorée, car je considérais que PACE représentait un problème suffisamment unique pour justifier des efforts supplémentaires. L’émulateur de code dans PACE a un point d’entrée unique et n’appelle d’autres codes que dans dix cas précis : Line1010 (instructions commençant par 0xA), Line1111 (instructions commençant par 0xF), TRAP0, TRAP8, TRAPF (OsCall), Division par zéro, Instruction illégale, Instruction non implémentée, Bit de trace activé, et atteinte d’une valeur PC de 0xFFFFFFF0. Que faire alors ? J’ai développé un outil appelé « patchpace » qui prend un fichier PACE.prc de n’importe quel appareil PalmOS, l’analyse pour localiser ces gestionnaires dans le binaire, et identifie le cœur principal de l’émulateur. Il remplace ensuite ce cœur (sur place si l’espace le permet, sinon en l’ajoutant au binaire) par le code que vous fournissez. Les adresses des gestionnaires seront insérées dans votre code aux offsets fournis par l’en-tête, et un saut vers votre code sera placé à l’endroit où se trouvait l’ancien cœur de l’émulateur. L’en-tête est très simple (voir « patchpace.c ») et inclut juste des offsets de demi-mots depuis le début du binaire jusqu’à l’entrée, et vers les sauts vers chacun des gestionnaires mentionnés sous forme d’instructions BL ou BLX. Le seul paramètre pour l’émulateur est l’état. Il est structuré comme suit : le premier mot est libre pour l’utilisation de l’émulateur, suivi de 8 registres D, puis de 8 registres A, ensuite le PC, et enfin le SR. Aucun autre donnée n’est autorisée (PACE utilise des données après cela). Ce même état doit être transmis à tous les gestionnaires. Le gestionnaire TRAPF a également besoin que le mot suivant lui soit transmis (numéro OsCall). Oui, vous avez bien compris, cela vous permet d’apporter votre propre émulateur 68k à la fête. N’importe quel émulateur 68k fera l’affaire, il n’a pas besoin de connaître quoi que ce soit sur PalmOS. Plutôt intéressant, non ?
N’importe quel émulateur 68k…
Alors, où peut-on se procurer un émulateur 68k ? Eh bien, n’importe où ! J’ai écrit un émulateur simple en C pour tester cette idée, et cela a bien fonctionné, mais pour ce genre de projet, vous souhaitez utiliser de l’assembleur. J’ai pris l’émulateur de PACE comme guide de style et j’ai réalisé un travail considérable pour produire un émulateur 68k en thumb2. Il est beaucoup plus efficace que ce que PACE a jamais été. Cela est inclus dans le dossier « mkrom » sous le nom « PACE.0003.patch ». Comme mentionné précédemment, cela est entièrement optionnel et non requis. Cependant, cela améliore la vitesse brute du 68k d’environ 8,4 fois dans les cas typiques.
Technologie : Mais vous aviez promis du matériel…
Le matériel a ses défauts
J’avais besoin d’une carte de développement pour expérimenter. La carte de découverte STM32F429 semblait être un bon choix. Elle dispose de 8 Mo de RAM, ce qui est suffisant, de 2 Mo de flash, ce qui est bon, et d’un écran avec un écran tactile. En théorie, c’est parfait. Oh, si seulement j’avais su à quel point la réalité était imparfaite. En consultant le manuel de référence du STM32F429, il semble que ce soit la puce idéale pour ce projet. Cependant, ST ne fait pas vraiment d’efforts pour vous indiquer où se trouvent les problèmes. La feuille d’errata est accablante. En gros, si vous faites fonctionner le CPU à partir de la mémoire externe, placez la pile dans la mémoire externe, et que le FIFO SDRAM est activé, les exceptions feront planter la puce (lecture incorrecte de l’adresse de vecteur). D’accord, je peux contourner cela – il suffit de désactiver le FIFO. Problème suivant : même histoire, mais si le FIFO est désactivé, parfois les écritures seront…
Défis et Solutions dans l’Ingénierie Inverse de PalmOS
Problèmes de RAM et de Gestion des Interruptions
Il est frustrant de constater que, malgré mes efforts pour transférer mes données vers la RAM interne, des plantages continuent de se produire. Après avoir retiré rePalm et mis en place un scénario de reproduction de 20 lignes, j’ai découvert un problème non documenté dans les notes de version de ST. Lorsque le registre PC pointe vers la RAM externe et que l’instruction WFI est exécutée pour attendre des interruptions en mode basse consommation, si une interruption survient après plus de 60 ms, le processeur sélectionne un vecteur d’interruption aléatoire au lieu du bon. Cela a nécessité de nombreuses nuits blanches à déchiffrer des plantages aléatoires dans les gestionnaires d’interruptions, qui ne devraient pas être actifs à ce moment-là. J’ai contourné ce problème en évitant d’utiliser WFI, ce qui entraîne un gaspillage d’énergie, mais cela est acceptable pour le développement jusqu’à ce que je conçoive une carte avec un chip fonctionnel.
Problèmes d’Adresse de RAM
Le STM32F429 prend en charge deux banques de RAM, 0 et 1, avec la banque 0 commençant à 0xC0000000 et la banque 1 à 0xD0000000. Cela pose un problème, car PalmOS exige que la RAM et la mémoire flash soient en dessous de 0x80000000. Heureusement, la banque 0 peut être remappée à 0x00000000. Cependant, la conception de la carte laisse à désirer, car elle ne dispose que d’une seule puce RAM, qui est logiquement la banque 0. En réalité, c’est la banque 1 qui est utilisée, et celle-ci ne peut pas être remappée. Cela rend la carte inutilisable pour démarrer PalmOS, car la limite de 0x80000000 est rigide.
Pourquoi la Limite de 0x80000000 ?
PalmOS gère deux types de blocs de mémoire : les blocs mobiles et non mobiles. Cette distinction est essentielle pour un système d’exploitation sans unité de gestion de mémoire (MMU) afin d’éviter une fragmentation excessive. Lorsqu’un bloc mobile n’est pas verrouillé, l’OS peut le déplacer, et il est référencé par un « handle ». On peut ensuite le verrouiller pour obtenir un pointeur, l’utiliser, puis le déverrouiller. La limite de 0x80000000 est cruciale, car PalmOS utilise le bit supérieur d’un pointeur pour indiquer s’il s’agit d’un handle ou d’un pointeur réel. Si le bit supérieur est activé, cela signifie un handle ; s’il est désactivé, c’est un pointeur. Ainsi, il est impossible d’avoir à la fois la RAM et la ROM au-dessus de 0x80000000.
Des Solutions Innovantes aux Problèmes de Conception
Étant donné que cette carte est destinée à un développement temporaire, pourquoi ne pas aller plus loin ? La distinction entre handle et pointeur n’est vérifiée qu’à quelques endroits. J’ai donc décidé de modifier ces vérifications pour inverser la condition, du moins temporairement. J’ai désassemblé et corrigé manuellement 58 emplacements, principalement dans Boot, où se trouve le MemoryManager, et quelques-uns dans UI, car le code des champs de texte doit déterminer si un pointeur passé est un pointeur (non modifiable) ou un handle (modifiable). J’ai également effectué des modifications dans PACE, qui avait un SysTrap pour déterminer le type de pointeur. Bien que cela ne soit plus « PalmOS non modifié », cela reste temporaire. Cependant, cela soulève une question : si nous inversions la condition, cela nécessiterait que la RAM et la ROM soient toutes deux au-dessus de 0x80000000. Mais la mémoire flash est à 0x08000000… Oups. Cela signifie que nous ne pouvons plus utiliser la mémoire flash. J’ai donc modifié la disposition de la RAM, réservant 2 Mo à 0xD0600000 pour simuler la « ROM » et j’y copie la flash au démarrage. Cela fonctionne !
Support de la Carte SD
Heureusement, j’avais déjà développé un pilote de slot pour PalmOS, donc la création d’un pilote pour carte SD n’a pas été difficile. J’ai même réutilisé une partie du code source de PowerSDHC ! Le support des cartes SD est désormais opérationnel sur la carte de développement STM32F469. Sur la carte STM32F429, bien qu’elles soient également prises en charge, il faut les câbler soi-même, car la carte ne dispose pas de slot (CLK -> C12, CMD -> D2, DAT_0 -> C8). En raison de la configuration de la carte, seul un bus d’un bit fonctionne, ce qui limite la vitesse à environ 4 Mbit/s sur la STM32F429, tandis que sur la STM32F469, la vitesse atteint 37 Mbit/s. Des vitesses plus élevées pourraient être atteintes avec DMA, mais cela est suffisant pour le moment. En développant le support de la carte SD pour les puces STM32F4, j’ai découvert un bug matériel difficile à déboguer. le bus SD permet à l’hôte d’arrêter l’horloge à tout moment, mais cela pose des problèmes lorsque la carte est occupée, car elle attend un signal d’horloge pour changer l’état de la ligne DAT_0. J’ai donc désactivé la fonction d’arrêt automatique de l’horloge après une semaine de débogage, ce qui a nécessité une simple correction d’une ligne. Le pilote de slot est disponible dans le répertoire « slot_driver_stm32 ».
Support du Port Série
Palm Inc a documenté la création d’un pilote de port série pour PalmOS 4, avec deux types : les pilotes virtuels et les pilotes série. Les premiers étaient destinés aux ports non câblés (comme le port Bluetooth ou infrarouge), tandis que les seconds étaient pour les ports câblés (comme le port série du socle). PalmOS 5 a fusionné ces deux types en un type « virtuel » unifié, mais cela n’a pas été documenté. J’ai dû effectuer une ingénierie inverse prolongée pour comprendre son fonctionnement. J’ai produit une idée fonctionnelle de ce système sur PalmOS 5, que vous pouvez consulter dans le fichier d’inclusion « vdrvV5.h ». Ces informations suffisent pour créer un pilote fonctionnel pour un port série, un port IrDA SIR et USB pour les synchronisations.
La mise en œuvre du port série sur le matériel STM32F4 a été un défi. Le matériel ne dispose que d’un seul tampon d’un octet, ce qui signifie qu’il faut utiliser le contrôle de flux matériel ou faire en sorte que l’interruption du port série soit de la plus haute priorité pour éviter de perdre des données à des débits élevés. Cela n’était pas acceptable pour moi, j’ai donc opté pour le DMA. Cela m’a permis d’écrire ma première bibliothèque PalmOS 5 pouvant être utilisée par d’autres bibliothèques. J’ai développé une bibliothèque DMA pour les puces de la série STM32F4, disponible dans le répertoire « dma_driver_stm32 ». Cependant, le DMA nécessite de savoir combien d’octets vous attendez de recevoir, ce qui pose problème pour les données UART génériques. J’ai donc utilisé une approche astucieuse : le DMA peut nous interrompre lorsque la moitié d’un transfert est terminée, puis à nouveau lorsque tout est terminé. Le DMA peut être circulaire, ce qui nous permet de redémarrer au début une fois le transfert terminé. Cela nous rapproche de la solution idéale tant que les données continuent d’arriver.
Gestion des Interruptions et Réception de Données
Lorsqu’il s’agit de gérer les interruptions, il est essentiel de s’assurer que notre système peut traiter les données efficacement. Dans notre gestionnaire d’interruptions, il est crucial de vérifier la position actuelle dans le tampon et de signaler les octets reçus depuis notre dernière vérification comme de nouvelles données. Cependant, un problème se pose si nous ne recevons qu’un seul octet. Cela représente moins de la moitié d’un transfert, ce qui signifie qu’aucune interruption ne sera générée, et nous ne pourrons pas signaler cette donnée aux clients. Cette situation est inacceptable. Heureusement, le mode « détection IDLE » du UART STM32F4 résout ce problème. Ce mode déclenche une interruption si, après la réception d’un octet, quatre temps de bits s’écoulent sans qu’un nouveau caractère ne commence. En intégrant cette interruption à notre code de gestion du tampon circulaire, nous pouvons recevoir des données à la vitesse à laquelle elles arrivent, peu importe leur taille. Mon pilote série, que vous pouvez trouver dans le répertoire « uart_driver_stm32 », fonctionne parfaitement et a même permis une synchronisation à chaud. Le support IrDA est également inclus, et tout fonctionne de manière fluide. Consultez l’album photo pour une démonstration vidéo !
Essayez-le vous-même !
Pour ceux qui souhaitent expérimenter sur la carte de découverte STM32F429, le trou non peuplé de 0,1 pouce étiqueté « RX » est en réalité la sortie de transmission du STM32 (une étiquette quelque peu déroutante pour une broche de transmission). La broche B7 est celle de réception. En connectant un adaptateur USB-série, vous pourrez effectuer une synchronisation à chaud via le port série. Si vous optez pour un émetteur-récepteur IrDA SIR, vous obtiendrez également un fonctionnement IR. J’ai utilisé le transceiver MiniSIR2 de Novalog, Inc., qui est le même que celui utilisé par la plupart des appareils Palm.
Support de Vibration et de LED
Le support de la vibration et des LED n’a jamais été documenté, car ce sont des fonctionnalités matérielles gérées par les fournisseurs. Heureusement, j’avais déjà effectué une rétro-ingénierie de ces fonctionnalités lorsque j’ai ajouté le support de vibration au T|X. Il s’avère que j’avais presque tout compris à l’époque. Un peu plus de rétro-ingénierie a permis d’obtenir un résultat complet concernant l’API appropriée. Le fonctionnement des LED suit le même principe que celui des vibreurs : une fonction « GetAttributes » et une fonction « SetAttributes ». Les paramètres réglables incluent le motif, la vitesse, le délai entre les répétitions et le nombre de répétitions. Le système d’exploitation les utilise selon les besoins et ajoute automatiquement les paramètres »Vibrer » et « LED » au panneau de préférences « Sons et Alertes » si le matériel est pris en charge. Et rePalm prend désormais en charge les deux ! Le code est disponible dans « halVibAndLed.c », n’hésitez pas à l’explorer à votre convenance.
Support Réseau (En Cours de Développement)
Débuts Difficiles
Mon objectif était d’ajouter un support réseau à rePalm. Plusieurs méthodes me venaient à l’esprit pour garantir la compatibilité avec les applications existantes. Une option serait de remplacer Net.lib par une version ayant une interface similaire, mais sous mon contrôle. Cela me permettrait de le connecter à n’importe quelle interface de mon choix, rendant tout cela magique. Cependant, cette approche présente des inconvénients. Bien que de nombreuses parties de Net.lib soient documentées, d’autres ne le sont pas, rendant leur compréhension difficile. De plus, il est essentiel de faire fonctionner un PalmOS non modifié, et remplacer des bibliothèques aléatoires compromet cette capacité. Cette méthode ne serait donc pas viable. Une autre possibilité serait de créer une interface série fictive et de demander à PalmOS de s’y connecter via SLIP ou PPP à une machine distante fictive. L’autre extrémité de ce port série pourrait être reliée à un thread communiquant avec notre véritable interface réseau. Bien que cela soit réalisable, cela impliquerait des surcharges de codage et de décodage des trames PPP/SLIP, et l’interface utilisateur serait confuse. Trouver des moyens de configurer l’interface serait également un défi. Mais peut-être existe-t-il une meilleure solution ?
Une Voie Difficile mais Prometteuse
Sur le plan conceptuel, une meilleure approche existe. Le Net.lib de PalmOS prend en charge des interfaces réseau modulables (que j’appelle un pilote NetIF). On peut en trouver plusieurs sur tous les appareils PalmOS : PPP, SLIP, Boucle de retour. Certains autres modèles disposent également d’une interface pour le WiFi ou le réseau cellulaire. Il me suffit donc de produire un pilote NetIF. Cela semble simple, n’est-ce pas ? En réalité, la réponse est un « non » retentissant ! Écrire des pilotes NetIF n’a jamais été documenté, et une interface réseau est beaucoup plus complexe qu’un pilote de port série (qui était l’interface de pilote précédente de PalmOS que j’avais rétro-ingénierie). La rétro-ingénierie de cela s’avère être un défi considérable.
Apprendre de l’Histoire
J’ai commencé par examiner certains appareils PalmOS 4.x et les pilotes NetIF SLIP/PPP/Boucle de retour. Pourquoi ? Comme je l’ai mentionné précédemment, dans l’architecture 68k, le compilateur a tendance à laisser les noms de fonction dans le binaire, sauf si cela est désactivé. Cela facilite grandement la rétro-ingénierie. Cependant, ne vous laissez pas tromper, les noms de fonction ne sont pas si utiles en soi. Il faut encore deviner les formats de structure, les paramètres, etc. Ainsi, bien que Net.lib et l’interface des pilotes NetIF aient changé entre PalmOS 4.x et 5.x, comprendre comment fonctionnaient les pilotes NetIF dans PalmOS 4.x fournirait des bases solides. Après quelques semaines d’analyse, je pensais avoir acquis cette connaissance. Je me suis alors demandé : « Y avait-il un appareil PalmOS 4.x avec WiFi ? » En effet, il y en avait un. L’Alphasmart Dana Wireless était équipé de WiFi. Maintenant que je pensais avoir compris les bases du fonctionnement de ces pilotes NetIF, il était temps d’examiner un modèle plus complexe, car PPP, SLIP et Boucle de retour sont très simples. Malheureusement, les développeurs d’Alphasmart savaient comment désactiver l’insertion des noms de fonction dans le binaire. Leur pilote WiFi était utile, mais il a fallu des semaines pour en tirer un sens. C’est à ce moment-là que j’ai réalisé que Net.lib avait de nombreuses versions et que je devais examiner d’autres variantes. J’ai donc désassemblé chaque version de Net.lib existante pour observer l’évolution de l’interface des pilotes NetIF et de Net.lib lui-même. J’ai ainsi étudié les versions de Palm V, Palm Vx, Palm m505 et Dana. Les changements les plus intéressants se sont produits avec la version 9, où le support d’ARP et de DHCP a été intégré à Net.lib, alors qu’auparavant, chaque pilote NetIF nécessitant ces fonctionnalités intégrait sa propre logique.
Vers le Net.lib de PalmOS 5
Toutes ces découvertes étaient intéressantes, mais mon objectif n’était pas de comprendre comment fonctionnaient les pilotes NetIF dans PalmOS 4.x. Il était temps de passer à la rétro-ingénierie de la manière dont PalmOS 5.x procédait. J’ai récupéré une copie de Net.lib du T|T3 et commencé à tracer ses fonctions, en les associant à leurs équivalents de PalmOS 4.x. Après quelques semaines supplémentaires, j’avais plus ou moins compris le fonctionnement de Net.lib dans PalmOS 5.x.
Une Découverte Importante
Au cours de mes recherches, j’ai découvert un véritable bug : un problème de « use-after-free » dans arp_close().
NETLIB_T3:0001F580 CMP R4, #0 ; La liste chaînée est-elle vide ?
NETLIB_T3:0001F584 BEQ loc_1F5A4 ; Si oui, il suffit de sauter cette partie
NETLIB_T3:0001F588 B loc_1F590 ; Sinon, libérer un par un
NETLIB_T3:0001F58C
NETLIB_T3:0001F58C loc_1F58C:
NETLIB_T3:0001F58C BEQ loc_1F598
Exploration des pilotes NetIF de PalmOS
Introduction à l’ingénierie inverse
J’ai commencé à examiner les pilotes NetIF SLIP/PPP/Loopback de PalmOS 5.x pour comprendre les modifications apportées par rapport à PalmOS 4.x. Je pensais que la logique de base n’avait pas beaucoup évolué, donc les différences que je pourrais observer pourraient indiquer des changements dans la structure de Net.lib et NetIF entre les deux versions. En réalité, peu de choses avaient changé. Les structures avaient été réalignées et quelques valeurs d’attributs modifiées, mais dans l’ensemble, la similarité était frappante. À ce stade, je me suis félicité et j’ai décidé de créer mon propre pilote NetIF pour tester mes connaissances.
Une réalité plus complexe
Cette satisfaction n’a pas duré longtemps. En consultant mes notes, j’avais marqué certains éléments que je pensais insignifiants comme « à revoir plus tard ». Il s’est avéré qu’ils n’étaient pas du tout négligeables. Par exemple, le retour d’appel du DHCP vers le pilote NetIF pour l’informer de l’état du DHCP n’était pas simplement informatif, comme je l’avais supposé. En fait, une quantité importante de logique devait être intégrée à ce processus. Cette logique interagissait avec la structure DhcpState, dont une partie m’était restée obscure, car je pensais qu’elle était opaque pour le pilote NetIF. Je me suis donc replongé dans IDA pour approfondir mes recherches. J’ai réalisé qu’il me fallait une compréhension bien plus approfondie du DHCP et de l’ARP. Après avoir consacré plusieurs heures à lire les RFC sur le DHCP et l’ARP, je suis retourné au code désassemblé, et tout a commencé à prendre sens. Pour résumer, il m’a fallu encore trois semaines pour documenter chaque structure et fonction utilisée par le code ARP et DHCP.
Approfondissement de l’ingénierie inverse
Il restait une dernière étape. Lorsque le pilote NetIF se lance, il doit afficher une interface utilisateur et interagir avec Net.lib à différents moments. Les divers pilotes NetIF que j’ai analysés le faisaient de manières très variées, ce qui m’a laissé dans l’incertitude quant à la méthode appropriée. J’ai donc consulté mon archive de ROM PalmOS et développé un outil pour identifier tous les fichiers de type neti (les pilotes NetIF portent ce type), en excluant ceux qui étaient PPP, SLIP ou Loopback, et en copiant le reste dans un dossier après avoir éliminé les doublons. J’ai ensuite désassemblé tous ces fichiers, produisant des diagrammes et des notes sur la manière dont chacun se lançait et se fermait, où l’interface utilisateur était affichée ou masquée, et à quel moment chaque étape était réalisée. En procédant ainsi, j’ai découvert quelques journaux dans certains de ces pilotes, ce qui m’a permis de renommer mes propres valeurs et structures avec des noms plus appropriés, inspirés des déclarations de journalisation des développeurs de ces pilotes. J’ai ainsi désassemblé : le « CFEtherDriver » de Sony pour l’UX50, le pilote WiFi de Hagiwara « HNTMSW_neti », le « WLAN NetIF » de Janam pour l’XP30, le « CFEtherDriver » de Sony pour le TH55, le « PxaWiFi » de PalmOne pour le Tungsten C, le « WiFiLib » de PalmOne pour le TX, et le « WiFiLib » de PalmOne pour leur carte SD WiFi. Cela a été un travail considérable ! l’interface NetIF que j’ai rétro-ingénierie est documentée dans « netIfaceV5.h », et je pense qu’il est désormais possible d’écrire un pilote NetIF fonctionnel en utilisant cette documentation.
Vous pourriez vous demander : « As-tu déjà testé cela ? ». Non, je suis encore en train de rédiger mon pilote NetIF, alors restez à l’écoute…
Support de densité 1.5
Les bases de la densité
Depuis la version 4.2, PalmOS prend en charge plusieurs densités d’écran. Cela signifie qu’un appareil peut avoir un écran de même taille, mais avec plus de pixels, permettant ainsi de voir les éléments affichés à la même taille, mais avec plus de détails. Bien que Sony ait proposé des écrans haute résolution avant Palm, et HandEra avant les deux, la solution de Palm a été la première à être intégrée à l’échelle du système d’exploitation, ce qui a été adopté par PalmOS 5. Le principe est simple : chaque Bitmap/Fenêtre/Police, etc., possède un système de coordonnées associé, et toutes les opérations s’appuient sur cela pour déterminer comment mettre à l’échelle les éléments. Les écrans 160×160 étaient qualifiés de 72ppi (sans rapport avec les points ou les pouces réels), tandis que les nouveaux écrans 320×320 étaient à 144ppi (double densité). Cela simplifiait la vie : lorsque l’image ou la police de la bonne densité était manquante, il était possible de doubler les pixels de la version basse résolution. L’inverse était également vrai. Les coordonnées du stylet devaient également être ajustées, car le développeur pouvait désormais demander à travailler dans un système de coordonnées particulier, ce qui nécessitait une adaptation de l’ensemble de l’API système.
Comment cela a-t-il été mis en œuvre ? Plusieurs systèmes de coordonnées sont toujours en jeu : natif (ce que l’affichage est), standard (utilisé pour la mise en page de l’interface utilisateur) et actif (défini par l’utilisateur via WinSetCordinateSystem). Ainsi, à tout moment, il existe six facteurs d’échelle pour convertir d’un système à un autre. PalmOS 5.0 n’en utilisait qu’un, ce qui était désordonné et ne sera pas approfondi ici. Disons simplement que cette solution n’a pas perduré. PalmOS 5.2 et les versions ultérieures utilisent quatre facteurs d’échelle, représentant des transformations bidirectionnelles entre actif et natif, et natif et standard. Pourquoi pas la troisième paire ? Elle est utilisée de manière suffisamment rare pour que deux transformations soient acceptables. Étant donné que les calculs en virgule flottante sont lents sur ARMv5, des nombres à virgule fixe sont utilisés. Ici, il existe une différence entre PalmOS 5.2 et PalmOS 5.4. La première utilise des nombres à virgule fixe de 16 bits au format 10.6, tandis que la seconde utilise des nombres de 32 bits au format 16.16. Je vous laisse le soin de vous renseigner sur les nombres à virgule fixe, mais l’essentiel est que le nombre de bits de fraction limite la précision du nombre lui-même et les calculs que l’on peut effectuer avec. Pour des puissances de deux précises, il n’est pas nécessaire d’avoir autant de bits, donc tant qu’il n’y avait que des écrans 72ppi et 144ppi, le format 10.6 était suffisant, avec des facteurs d’échelle toujours à 0x20 (x0.5), 0x40 (x1.0) et 0x80 (x2.0). PalmOS 5.4 a ajouté le support de la densité un et demi en raison de l’abondance d’écrans 320×240 bon marché à l’époque. Cette nouvelle résolution était spécifiée à 108ppi, soit précisément 1,5 fois la résolution standard. Techniquement, tout ce qui était en PalmOS 5.2 fonctionnera tel quel, et si vous donnez à PalmOS 5.2 un écran de cette résolution, cela fonctionnera également.
Comprendre les Défis de PalmOS et les Solutions Innovantes
Lorsqu’on examine l’affichage sur un écran, il est évident que cela fonctionne, bien que de manière peu esthétique. À droite, vous pouvez voir un exemple de ce à quoi cela ressemble. Bien que cela ne soit pas attrayant, le système ne plante pas et les fonctionnalités de base fonctionnent comme prévu. Mais pourquoi cet affichage est-il si dégradé ? La réponse réside dans les facteurs d’échelle. Analysons les facteurs d’échelle nécessaires.
Les Limitations de PalmOS
Tout d’abord, il est important de noter que PalmOS ne peut pas effectuer de mise à l’échelle entre 108 et 144 ppi pour les bitmaps ou les polices. Par conséquent, ces facteurs d’échelle ne sont pas requis. Cependant, rePalm, dans un cas particulier, peut dessiner des bitmaps de 144 ppi sur un écran de 108 ppi, mais uniquement si aucun bitmap de 72 ppi ou 108 ppi n’est disponible. Les nouveaux facteurs d’échelle introduits se situent entre la densité standard et 1,5. Pour passer de la densité standard à 108 ppi, le facteur d’échelle est de 1,5, ce qui peut être représenté sous la forme 0x60 dans le format à virgule fixe 10.6. Cela fonctionne parfaitement, mais le passage de 108 ppi à 72 ppi pose problème, car le facteur d’échelle est de 2/3, ce qui n’est pas représentable de manière exacte en binaire, peu importe la précision des bits.
La règle simple avec les mathématiques à virgule fixe est que lorsque vos nombres ne sont pas représentables de manière exacte, les erreurs d’arrondi s’accumulent. Pour le format 10.6, la plus petite unité (LSB) est de 1/64, donc dès que nous travaillons avec des nombres supérieurs à 64, les erreurs d’arrondi dépassent une unité. Cela pose un problème, car PalmOS utilise fréquemment des nombres supérieurs à 64 dans l’interface utilisateur. Par exemple, la largeur standard de l’écran est de 160 pixels. Ces erreurs d’arrondi accumulées sont visibles dans les captures d’écran. La prise en charge officielle de la densité 108 ppi a été introduite dans PalmOS 5.4. Que s’est-il passé pour corriger cela ? Ils ont opté pour le format 16.16, où la LSB est de 1/65536, permettant ainsi un arrondi correct pour les nombres jusqu’à 65536, ce qui est suffisant puisque toute l’interface utilisateur de PalmOS utilise des nombres de 16 bits pour les coordonnées.
Les Problèmes de PalmOS 5.4
Pourquoi est-ce important ? PalmOS 5.4 présente d’autres caractéristiques qui le rendent peu attrayant pour rePalm, notamment l’utilisation obligatoire de NVFS. Mon objectif était de faire fonctionner PalmOS 5.2 tout en intégrant la prise en charge de la densité 1,5, car les écrans 320×240 sont encore très abordables, comme celui de ma carte de développement STM32F427. Il n’est pas possible de simplement transférer Boot.prc de PalmOS 5.4, car cela impliquerait également NVFS. Que faire alors ? J’ai décidé de dresser un inventaire de chaque partie du système d’exploitation utilisant ces valeurs d’échelle, qui sont dissimulées dans la structure « Window », principalement dans Boot. Cependant, d’autres problèmes peuvent survenir. Par exemple, dans certaines parties de l’interface utilisateur, des séquences comme BmpGetDensity(WinGetBitmap(WinGetDisplayWindow())) peuvent être observées. Cela peut poser des problèmes, surtout si des calculs sont effectués avec ces valeurs.
La Solution : Écrire une Extension OEM
Corriger autant d’endroits est faisable, mais que se passe-t-il si je décide d’utiliser le Boot d’un autre appareil à l’avenir ? Ce n’était pas une solution viable. J’ai donc choisi d’écrire une extension OEM, un module que le système d’exploitation chargera au démarrage, pour résoudre ce problème. Mais comment procéder ? Étant donné que la ROM est en lecture seule et qu’il n’y a pas d’UMM pour mapper une page sur les zones à corriger, comment faire ? Chaque problème se trouve logiquement dans une fonction, et chaque fonction est parfois appelée, que ce soit par un minuteur, une notification, un thread ou une action de l’utilisateur. Heureusement, PalmOS n’attend que des travaux d’interface utilisateur du thread d’interface, donc toutes ces fonctions étaient appelées uniquement par des fonctions orientées utilisateur. Malheureusement, certaines étaient profondément enfouies. J’ai commencé à écrire des fonctions de remplacement, en m’inspirant de ce que faisait le Boot de PalmOS 5.4. Pour la plupart des fonctions, j’ai écrit des correctifs complets, remplaçant entièrement la fonction originale dans la table de dispatch, sans jamais rappeler l’original. J’ai ainsi développé 73 correctifs, incluant des fonctions telles que FntBaseLine, FntCharHeight, FntLineHeight, et bien d’autres.
Améliorations de l’Interface Utilisateur dans PalmOS
Dans le cadre de l’optimisation de l’interface utilisateur de PalmOS, certaines fonctions se sont révélées trop complexes pour être remplacées intégralement. Prenons l’exemple de PrvDrawControl, une fonction essentielle de CtlDrawControl, qui est également utilisée dans divers contextes, notamment la gestion des événements pour les contrôles. Que faire alors ? Bien que je puisse remplacer tous les appelants, tels que FrmHandleEvent et CtlDrawControl, cela ne résout pas le problème, car PrvDrawControl présente des défauts et est d’une complexité énorme. Après une analyse minutieuse, j’ai constaté qu’elle ne se préoccupait réellement de la densité que dans un cas particulier, lors du dessin d’un cadre de type 0x4004. Dans ce cas, elle modifie le système de coordonnées pour le rendre natif, dessine le cadre manuellement, puis réinitialise le système de coordonnées. J’ai donc mis en place un indicateur global avant de l’appeler si le type de cadre demandé était celui-là, et la fonction de dessin de cadre, que j’avais déjà réécrite (WinDrawRectangleFrame), détecte ce drapeau et exécute alors cette opération spéciale. La même approche a été appliquée pour l’effacement du cadre de type 0x4004. Les résultats ? Cela a fonctionné !
Gestion des Titres de Fenêtres
Un autre cas complexe restait à traiter : le dessin d’un titre de fenêtre. Cela était profondément intégré dans FrmDrawForm, car un titre est techniquement un type d’objet cadre. Pour intercepter cela sans réécrire la fonction entière, j’ai converti un objet titre en un type spécial d’objet liste et sauvegardé l’objet original dans mes variables globales. Pourquoi une liste ? FrmDrawForm appelle LstDrawList sur un objet liste sans examiner son contenu. J’ai donc intercepté LstDrawList, vérifié notre pointeur magique, et si c’était le cas, dessiné le titre, sinon laissé la fonction originale LstDrawList s’exécuter. À la sortie de FrmDrawForm, tout cela est annulé. Pour les fonctions de réglage de titre de formulaire, je les ai remplacées, car elles redessinent le titre manuellement, et j’avais déjà écrit une fonction de dessin de titre. Il restait une petite chose à régler : l’icône (i) sur les formulaires associés à l’aide. Elle était mal rendue lorsqu’on appuyait dessus. Ma fonction de dessin de titre la rendait parfaitement, mais la réponse au tap était gérée par FrmHandleEvent, une autre fonction que je ne souhaitais pas remplacer. En examinant cela, j’ai remarqué que la gestion des taps sur l’icône d’aide (i) se faisait assez tôt. J’ai donc dupliqué cette logique (et certaines qui la précédaient) dans mon correctif pour FrmHandleEvent et n’ai pas laissé la fonction originale traiter cet événement. Cela a parfaitement fonctionné ! Ainsi, nous avons quatre correctifs partiels supplémentaires : LstDrawList, FrmDrawForm, FrmHandleEvent, et CtlDrawControl.
Optimisation de la Densité d’Affichage
Il restait encore une tâche à accomplir : assurer un support adéquat pour la fonctionnalité de densité 1.5, telle que définie par le SDK. J’ai donc modifié le DAL pour me permettre de corriger des fonctions qui n’existent pas du tout dans la version actuelle du système d’exploitation, car certaines nouvelles avaient été ajoutées après la version 5.2 pour faire fonctionner cette fonctionnalité : WinGetScalingMode et WinSetScalingMode. Ensuite, j’ai adapté le gestionnaire de dispatch 68k de PACE pour sysTrapHighDensityDispatch afin de gérer les nouveaux sélecteurs de trap 68K HDSelectorWinSetScalingMode et HDSelectorWinGetScalingMode, laissant le reste des anciens sélecteurs être gérés par PACE comme auparavant. J’ai également acquis des polices 108ppi et écrit du code pour remplacer les polices système par celles-ci, ainsi que des images système 108ppi (comme les icônes d’alerte) et fait en sorte que mon extension les place aux bons endroits.
Le résultat ? Le système a fière allure ! Il reste encore des éléments à corriger, techniquement, et « main.c » dans le dossier « Fix1.5DD » contient un commentaire les listant, mais ce sont tous des détails mineurs et le système est déjà très esthétique. L’extension « Fix1.5DD » fait partie du code source que je publie avec rePalm, et vous pouvez voir la capture d’écran de comparaison « après » juste au-dessus à droite. Cela représente environ 4000 lignes de code, réparties sur 77 correctifs et un peu de logique d’installation.
Support des Services de Zone d’Entrée Dynamique et de Gestion des Entrées au Stylet
Les Fondamentaux de DIA/PINS
Initialement, PalmOS ne prenait en charge que les écrans carrés. Quelques fabricants (Handera, Sony) ont produit des écrans non carrés, mais cela n’était pas standardisé. Sony a fait des progrès significatifs avec ses appareils Sony Clie 320×480. Cependant, leur API était exclusive à Sony et n’a pas été adoptée par d’autres. Lorsque PalmOS 5.2 a ajouté le support des écrans non carrés, Palm a créé une API appelée PINS (ou DIA ou AIA). Bien qu’elle ne soit pas aussi performante que celle de Sony, elle était officielle, ce qui a conduit tout le monde à y migrer. Les appareils Sony ultérieurs ont également dû la prendre en charge. Pourquoi était-elle moins efficace ? L’API de Sony était simple : réduire la zone d’entrée dynamique ou la rétablir. Activer ou désactiver le bouton correspondant. Facile. L’API de Palm, quant à elle, essaie d’être intelligente, avec des politiques par formulaire et une multitude de complexités. Elle propose également des fonctionnalités simples : faire descendre ou remonter la zone, ou activer ou désactiver le bouton. Cependant, tous ces paramètres sont aléatoirement modifiés ou effacés chaque fois qu’un nouveau formulaire apparaît à l’écran, ce qui rend l’utilisation très pénible ! Quoi qu’il en soit, c’est l’API publique. Comment cela fonctionne-t-il ? Dans PalmOS 5.4, tout cela fait partie intégrante du système d’exploitation, intégré dans Boot.
Fonctionnement avant Garnet
Comme je l’ai mentionné, je visais PalmOS 5.2. À cette époque, ce n’était pas intégré au système d’exploitation, mais représentait une extension. Le DAL présente au système un écran brut de la résolution réelle (généralement 320×480) et l’extension masque la zone inférieure des applications tout en dessinant la zone d’entrée dynamique dessus. Cela nécessite d’intercepter certains appels du système d’exploitation, tels que FrmDrawForm (pour appliquer la nouvelle politique), FrmSetActiveForm (pour appliquer la politique aux formulaires déjà dessinés), SysHandleEvent (pour gérer les événements dans la zone d’entrée dynamique), et UIReset (pour réinitialiser les paramètres par défaut lors du changement d’application). Nous souhaitons également être informés de certains événements, comme le changement de profondeur de couleur de l’écran. Lorsque cela se produit, nous devons peut-être redessiner la zone d’entrée. Voilà l’essentiel. Cependant, il existe de nombreux détails petits mais significatifs.
Les Complexités de l’Écriture d’une Implémentation DIA
Avant de me lancer dans l’écriture de ma propre implémentation DIA, j’ai testé toutes les existantes pour voir si elles supportaient des résolutions autres que 320×480. Je ne voulais pas écrire du code inutile, après tout. Aucune d’entre elles ne fonctionnait correctement. Même des résolutions simples comme 160×240 (un simple redimensionnement 2x) étaient défaillantes. Les écrans avec des rapports d’aspect différents, comme les courants 240×320 et 160×220, étaient encore plus problématiques. Pourquoi ? Je suppose que personne n’écrit de code générique. Il est plus simple de bricoler des solutions « pour l’instant » sans plan pour « plus tard ». J’ai donc décidé d’écrire une implémentation DIA capable de supporter presque toutes les résolutions.
Lorsque la zone d’entrée dynamique est réduite, une barre d’état s’affiche. Elle montre de petites icônes comme le bouton d’accueil et le bouton de menu, ainsi que le bouton pour révéler la zone d’entrée. J’ai essayé de rendre tout aussi générique que possible. Pour chaque résolution d’écran, il est possible de créer un skin. Un skin est un ensemble de graphiques représentant la zone d’entrée dynamique, ainsi que quelques entiers décrivant les zones et leur fonctionnement (quels codes de touches ils envoient, ce qu’ils font). Les spécificités sont décrites dans le code, les commentaires et les exemples (3 skins conçus pour ressembler aux interfaces de Sony). Ils définissent également une zone de « notification ». Toute application peut y ajouter des icônes. Même les applications 68k normales peuvent le faire ! J’inclus également un exemple à ce sujet. L’horloge que vous voyez dans la barre d’état est en fait une application 68k appelée « NotifGeneral », et son code source est fourni dans le cadre de rePalm.
Introduction au support audio sur PalmOS
Depuis la version 1.0 de PalmOS, un support audio basique a été intégré, permettant la production de sons simples via un haut-parleur piézoélectrique. Cela se traduit par des bips élémentaires. L’API officielle permet de jouer un fichier MIDI (un canal, uniquement des ondes carrées), de générer un ton à un volume et une amplitude spécifiés (en arrière-plan ou au premier plan), et d’arrêter ce ton. Avec PalmOS 5.0, l’API de bas niveau qui soutient cette API audio simple est presque identique à l’API officielle de haut niveau. La fonction HALSoundPlay est utilisée pour démarrer un ton pendant une durée déterminée. Ce ton fonctionne en arrière-plan, et la fonction retourne immédiatement. Si un autre ton était déjà en cours, il est remplacé par le nouveau. Une valeur de durée négative signifie que le ton ne s’arrêtera jamais automatiquement. La fonction HALSoundOff permet d’arrêter un ton en cours de lecture, s’il y en a un. La fonction HALPlaySmf joue une mélodie MIDI, mais son utilisation est optionnelle. Si le DAL renvoie une erreur, Boot interprétera le fichier MIDI et effectuera une série d’appels à HALSoundPlay. Cela signifie que, sauf si vous disposez d’un matériel capable de jouer des MIDI de manière plus efficace que des ondes carrées à un canal, il n’est pas judicieux d’implémenter HALPlaySmf dans votre DAL.
Support audio échantillonné sur PalmOS
À l’époque de la sortie de PalmOS 5.0, une API pour le son échantillonné a été introduite. Bien qu’elle ne nécessite pas obligatoirement PalmOS 5.0, il n’existe pas de dispositifs Palm OS 4 qui l’implémentent. Des API audio spécifiques aux fournisseurs avaient été présentes dans les versions antérieures de PalmOS, mais elles étaient non standard et dépendaient généralement de puces d’accélération matérielle personnalisées, car le processeur 68k n’est pas assez rapide pour décoder des formats audio complexes. L’API de son échantillonné est plus complexe que l’API de son simple, mais elle peut être expliquée par le concept de flux. On peut créer un flux d’entrée ou de sortie, définir le volume et le panoramique, et recevoir une notification lorsque des données sont disponibles (entrée) ou nécessaires (sortie). Pour les flux de sortie, le système est censé les mixer ensemble, permettant ainsi à plusieurs flux audio de jouer simultanément.
Les défis de l’audio et la solution de PalmOS
Le mélange audio est un processus complexe. Assurer une bonne qualité est encore plus difficile, et le faire rapidement l’est encore davantage. Pourquoi cela ? Le matériel audio ne peut sortir qu’un seul flux, ce qui nécessite de mixer plusieurs flux en un seul. Ce mélange peut impliquer une conversion de format, par exemple si le matériel nécessite des échantillons signés de 16 bits en format little-endian et qu’un des flux est en format flottant. De plus, le mélange peut nécessiter un rééchantillonnage. Si, par exemple, le matériel fonctionne à 48 000 échantillons par seconde et qu’un client demande à jouer un flux à 44 100 échantillons par seconde, il faut générer plus d’échantillons que ceux fournis. Cela peut sembler simple avec de grands tampons, mais cela entraîne une latence importante. En audio, il est crucial de travailler avec des tampons relativement petits, car les utilisateurs remarqueront si les échantillons audio sont livrés en retard.
Comment rePalm gère le mélange audio
Mon objectif avec rePalm était de produire une qualité audio optimale tout en prenant en charge tous les formats que l’API PalmOS prétendait supporter. En réalité, j’ai même pris en charge davantage de formats : entiers signés et non signés de 8, 16 et 32 bits, ainsi que des échantillons en virgule flottante simple précision dans n’importe quel ordre d’octets. Pour les taux d’échantillonnage, le mélangeur de rePalm prend en charge : 8 000, 11 025, 16 000, 22 050, 24 000, 32 000, 44 100 et 48 000 échantillons par seconde. Le format utilisé par le matériel de sortie est déterminé par le pilote matériel à l’exécution dans rePalm. Le matériel mono et stéréo est pris en charge, ainsi que tous les taux d’échantillonnage et formats d’échantillons pour la sortie matérielle native.
Gestion des tampons audio
La méthode initiale consistait à attribuer à chaque canal un tampon circulaire unique que le client remplirait et que le mélangeur lirait. Cependant, cela s’est avéré trop complexe à gérer en langage d’assemblage. La solution finale que j’ai adoptée était en réalité plus simple à gérer. Chaque flux dispose de plusieurs tampons (la profondeur du tampon est actuellement définie à quatre), et une fois qu’un tampon est complètement rempli, il est envoyé au mélangeur. Si aucun tampon n’est disponible, le client est bloqué, comme l’exige PalmOS. Si le mélangeur n’a pas de tampons pour un flux, ce dernier ne sera pas joué, conformément à l’API de PalmOS. Ce système est facile à gérer des deux côtés, car le mélangeur n’a jamais à traiter des tampons partiellement remplis.
Il est important de noter que le support de l’API de son échantillonné ne dispense pas de prendre en charge les fonctions de son simple. rePalm crée un flux sonore pour le support du son simple et l’utilise pour jouer les tons requis, générés à partir d’une onde sinusoïdale interpolée au moment de la demande. Pour faciliter cela sans avoir à gérer des rappels, le mélangeur prend en charge des canaux « bouclés ». Cela signifie qu’une fois le tampon de données rempli, il est joué en boucle jusqu’à ce qu’il soit arrêté.
Introduction à la gestion audio de rePalm
rePalm a choisi de ne pas reproduire de tonalités en dessous de 20Hz, ce qui me semble tout à fait acceptable.
Assemblage et mixage audio
La question du rééchantillonnage, du mixage et de la conversion de format était un défi majeur. L’approche simpliste consistant à prélever un échantillon de chaque flux, à le mélanger dans le flux de sortie, puis à répéter le processus pour le flux suivant s’est révélée trop lente en raison des constantes « commutations » nécessaires selon les types et les taux d’échantillonnage. Le rééchantillonnage devient également complexe s’il doit être effectué avec une qualité acceptable. Que fait donc le DAL de rePalm ? Pour le rééchantillonnage, un grand nombre de tables sont utilisées. Pour l’upsampling, une table indique comment interpoler linéairement entre les échantillons d’entrée pour produire des échantillons de sortie. Une table soigneusement ajustée existe pour chaque paire de fréquences. Pour le downsampling, une table précise combien d’échantillons doivent être moyennés et avec quel poids. Une table est également créée pour chaque paire de fréquences. Ces méthodes surpassent largement ce que propose PalmOS. Cependant, si le mixage était déjà complexe, cela l’est devenu encore plus. Décortiquons cela en étapes plus simples. Tout d’abord, nous avons besoin d’un format intermédiaire – un format avec lequel nous pouvons travailler de manière efficace et rapide, sans perte de données significative. J’ai opté pour un format à virgule fixe de 32 bits avec 8 bits pour la partie entière et 24 bits pour la partie fractionnaire. Étant donné qu’aucun appareil PalmOS n’a jamais produit d’audio à plus de 24 bits de résolution, cela est acceptable. Le flux est conceptuellement simple : d’abord, on remplit un tampon intermédiaire de zéros. Ensuite, pour chaque flux dont nous avons des tampons de données, nous mélangeons ces tampons dans le tampon intermédiaire, avec rééchantillonnage si nécessaire. Ensuite, nous coupons les échantillons du tampon intermédiaire, car le mélange de deux flux forts peut produire des valeurs dépassant le maximum autorisé. Enfin, nous convertissons le tampon intermédiaire dans le format pris en charge par le matériel et le transmettons à celui-ci. rePalm ne s’embarrasse pas d’un tampon intermédiaire stéréo si le matériel audio est uniquement mono. Le tampon intermédiaire n’est en stéréo que si le matériel le permet ! Comment obtenons-nous cette flexibilité ? Grâce à la manière dont nous mélangeons les éléments.
La seule partie difficile de ce processus est l’étape « mélanger les tampons dans le tampon intermédiaire avec rééchantillonnage ». En effet, non seulement nous devons rééchantillonner, mais nous devons également appliquer le volume, le panoramique, et éventuellement convertir d’un format mono à stéréo ou vice versa. L’approche la plus efficace consiste à écrire une fonction de mixage personnalisée et bien ajustée pour chaque combinaison possible d’entrées et de sorties. Le nombre de combinaisons est vertigineux. L’entrée a 8 taux possibles, 2 configurations de canaux possibles et 12 types d’échantillons possibles. La sortie a 8 taux possibles et 2 configurations de canaux. Cela signifie qu’il y a un total de plus de 3 000 combinaisons (8 * 2 * 12 * 8 * 2). Je n’allais pas écrire 3072 fonctions à la main. En fait, même les générer automatiquement au moment de la construction (si j’avais pu le faire) aurait gonflé la taille du code de rePalm à des mégaoctets. Non, une autre approche était nécessaire.
J’ai décidé de réutiliser certaines connaissances acquises lors de l’écriture du JIT, ainsi que de réutiliser une partie de son code. En effet, lorsque vous créez un flux, une fonction de mixage personnalisée est générée spécifiquement pour la configuration de ce flux et pour la configuration de sortie de votre matériel. Ce code d’assemblage personnalisé utilise tous les registres de manière optimale et, en fait, il parvient à ne pas utiliser de pile du tout ! Le bénéfice est évident ! Le code de mixage est toujours optimal puisqu’il est personnalisé pour votre configuration. Par exemple, si le matériel ne prend en charge que la sortie mono, le code de mixage effectuera un downmix avant l’upsampling (pour traiter moins d’échantillons), mais ne fera un downmix qu’après le downsampling (encore une fois, pour réduire les calculs nécessaires). Étant donné qu’il existe trois cas principaux : upsampling, downsampling et pas de rééchantillonnage, il y a trois chemins dans la génération de code pour produire des fonctions de mixage. Chaque fonction de mixage correspond à un prototype très simple : int32_t* (*MixInF)(int32_t* dst, const void** srcP, uint32_t maxOutSamples, void* resampleStateP, uint32_t volumeL, uint32_t volumeR, uint32_t numInSamples). Elle renvoie le pointeur vers le premier échantillon du tampon intermédiaire qui n’a PAS été écrit. srcP est mis à jour pour pointer vers le premier échantillon audio d’entrée non consommé, maxOutSamples limite le nombre d’échantillons audio pouvant être produits, numInSamples limite le nombre d’échantillons audio pouvant être consommés. Les fonctions de mixage retournent lorsque l’une ou l’autre limite est atteinte. La logique de rééchantillonnage peut avoir un état à long terme, donc cela est stocké dans une structure de données par flux (5 mots) et passé en tant que resampleStateP. Le pointeur de la table de rééchantillonnage est encodé dans la fonction elle-même (pour des raisons de rapidité), car il ne changera jamais. Pourquoi ? Parce que le taux d’échantillonnage du flux est constant, et le matériel ne pourra pas magiquement acquérir la capacité de jouer à un autre taux d’échantillonnage plus tard. En revanche, le volume et le panoramique du flux peuvent être modifiés à tout moment, donc ils ne sont pas codés en dur dans le corps de la fonction. Ils sont fournis en tant que paramètres au moment du mixage. J’ai même envisagé de les coder en dur et de régénérer la fonction de mixage chaque fois que le volume ou le panoramique changeait, mais le gain aurait été trop faible pour être significatif, donc j’ai décidé de ne pas le faire. Au lieu de cela, nous pré-calculons simplement le « volume gauche » et le « volume droit » à partir des réglages de l’utilisateur concernant le volume et le panoramique, et nous les passons à la fonction de mixage.
Avoir une fonction de mixage aussi efficace facilite le reste du mélangeur. Il suffit d’appeler la fonction de mixage pour chaque flux non mis en pause tant qu’il y a des tampons à consommer et que le tampon de sortie n’est pas plein. Si nous consommons entièrement un tampon, nous le libérons à l’utilisateur. Sinon, nous nous souvenons simplement du nombre d’échantillons qui y restent pour plus tard. C’est tout ! Alors, toute cette machinerie complexe fonctionne-t-elle ? Oui, elle fonctionne ! Le mélangeur audio compte environ 1 500 lignes de code, MAIS il peut rééchantillonner et mélanger des flux en temps réel à moins de 3 millions de cycles par flux par seconde, ce qui est bien meilleur que ce que faisait PalmOS, et avec une qualité supérieure ! Le code se trouve dans « audio.c ».
Architecture du pilote audio de rePalm
La couche matérielle audio de rePalm est très simple. Pour un support audio basique, il suffit de fournir les fonctions nécessaires et la couche sonore les appelle directement. Pour l’audio échantillonné, la fonction d’initialisation audio informe le mélangeur audio du nombre de canaux natifs et du taux d’échantillonnage. Qu’en est-il du format d’échantillon natif ? Le code fournit une fonction inline pour convertir un échantillon du format intermédiaire du mélangeur (entier signé 8.24) vers le format requis par le matériel. Ainsi, le format d’échantillon natif du matériel est défini par cette fonction inline. Au moment de l’initialisation, la couche matérielle fournit au mélangeur toutes ces informations, ainsi que la taille du tampon audio matériel. Ce tampon est nécessaire car les interruptions ont une latence et nous avons besoin que le matériel audio ait toujours de l’audio à jouer.
Sur la carte STM32F429, la sortie audio se fait sur la broche A5. L’audio est généré à l’aide d’un canal PWM, fonctionnant à 48 000 échantillons par seconde, en mode mono. Étant donné que l’horloge PWM fonctionne à 192 MHz, si nous voulons sortir 48 000 échantillons par seconde, l’unité PWM ne pourra compter que jusqu’à 4000. En effet, pour cette carte, qui ne dispose d’aucun matériel audio réel, nous sommes limités à une précision d’environ 12 bits. Cela est suffisant pour des tests et ne sonne pas si mal. La sortie en mode single-ended directement depuis la broche du microcontrôleur ne peut pas fournir beaucoup de puissance, mais avec un petit haut-parleur, le son est clair et agréable ! Je publierai bientôt une image avec le support audio.
Sur reSpring, l’horloge du CPU (et donc l’horloge PWM) est à 196,6 MHz. Pourquoi cette fréquence étrange ? Parce qu’elle est précisément 48 000 x 4096. Cela nous permet d’éviter de devoir mettre à l’échelle l’audio de manière complexe, comme nous le faisons sur la carte STM32F429. Il suffit de le saturer à 12 bits. De plus, sur reSpring, deux broches sont utilisées pour la sortie audio, en polarité opposée, ce qui nous donne le double de l’amplitude de tension, produisant des sons plus forts.
Microphone
Je n’ai pas implémenté de mélangeur/rééchantillonneur pour le microphone – PalmOS n’a jamais pris en charge plus d’un utilisateur d’un microphone à la fois, donc pourquoi s’en soucier ? – aucune application ne le fera. Au lieu de cela, quel que soit le taux d’échantillonnage demandé, je le transmets au pilote matériel et le fais fonctionner à ce taux d’échantillonnage. En ce qui concerne le type d’échantillon, c’est le même que pour la sortie audio, une fonction personnalisée est générée pour convertir le format d’échantillon de l’entrée (mono 16 bits little-endian) vers le format requis. Le code généré est assez compact et fonctionne bien !
Support Zodiac
Introduction au Tapwave Zodiac
Le Tapwave Zodiac, lancé en 2003, était un appareil PalmOS atypique, principalement conçu pour le jeu vidéo. Il se distinguait par un écran en mode paysage, un joystick analogique, une puce Yamaha Midi, ainsi qu’un accélérateur graphique ATI Imageon W4200 doté de sa propre mémoire graphique. Plusieurs titres exclusifs à Tapwave ont été développés, exploitant efficacement ce nouveau matériel, y compris des jeux 3D impressionnants. Bien entendu, ce matériel innovant nécessitait un support au niveau du système d’exploitation. Tapwave a introduit plusieurs nouvelles API, qui ont été bien documentées, facilitant ainsi leur utilisation. La conception de cette nouvelle API était soignée et intuitive. La documentation était presque parfaite. Bravo à Tapwave ! J’avais bien sûr l’intention de prendre en charge les jeux Tapwave dans rePalm.
Ingénierie inverse
Les API personnalisées de Tapwave étaient accessibles via une vaste table de pointeurs de fonction, fournie à toutes les applications ciblant Tapwave, après vérification de leur signature (Tapwave exigeait des approbations et une signature des applications). Cependant, il était évident qu’elles devaient interagir avec une bibliothèque ou du matériel. En explorant, j’ai découvert que la plupart d’entre elles faisaient appel à la Tapwave Application Layer (TAL). Ce module est particulier, car sur le Zodiac, à l’instar de DAL, Boot et UI, le TAL peut être accédé directement via R9 avec l’instruction LDR R12, [R9, #-16]; LDR PC, [R12, #4 * tal_func_no]. Après avoir passé beaucoup de temps dans le TAL, j’ai réalisé qu’il ne s’agissait que d’un wrapper. Les autres bibliothèques, comme la Tapwave Midi Library et la Tapwave Multiplayer Library, étaient également des wrappers. La véritable richesse fonctionnelle se trouvait dans le DAL, qui offrait un nombre impressionnant de points d’entrée. Alors que les DALs PalmOS classiques en comptent environ 230, celui de Tapwave en possède 373 !
Après de nombreuses heures de traçage dans le TAL et d’exploration des documents techniques du processeur, j’ai pu identifier les noms et paramètres de la plupart des fonctions exportées supplémentaires du DAL. J’ai réussi à déduire le fonctionnement de presque toutes, sauf 14 fonctions dont je n’ai trouvé aucune utilisation dans le logiciel de l’appareil. Les implémentations réelles sont moins importantes puisque je les réimplémente. Mes principales préoccupations concernaient, bien sûr, les API d’accélération graphique. Étonnamment, cette partie s’est révélée être la plus simple à mettre en œuvre !
L’accélérateur graphique
L’accélérateur graphique du Zodiac était assez avancé pour un appareil portable de l’époque, bien qu’il soit relativement basique. Il disposait de 8 Mo de mémoire intégrée et ne gérait que les opérations 2D. il pouvait : copier des rectangles de données d’image, mélanger des rectangles entre couches avec un alpha blending constant ou paramétrique, effectuer un redimensionnement bilinéaire basique, et dessiner des lignes, des rectangles et des points. Son fonctionnement se limitait aux couches RGB565LE en 16 bits. La mise en œuvre de ces fonctionnalités s’est avérée relativement simple. Bien que le traitement logiciel ne soit pas rapide, cela suffisait pour mon prototype. Après quelques jours de travail, cela fonctionnait ! Plusieurs jeux ont pu être lancés.
La prochaine étape, toujours en cours, consiste à utiliser l’unité DMA2D du STM32 pour accélérer la plupart des opérations que l’accélérateur ATI pouvait réaliser. À l’exception du redimensionnement d’image, cela peut être effectué en un ou deux passages ! Pour couronner le tout, il peut également fonctionner en arrière-plan, comme le faisait la puce ATI pour le CPU du Zodiac. Mais cela, c’est pour plus tard…
Autres API de Tapwave
Le sous-système d’entrée du Zodiac était assez particulier et nécessitait un certain travail. Au lieu des méthodes habituelles de PalmOS pour lire les touches, le tactile, etc., ils ont introduit un nouveau mécanisme de « file d’attente d’entrée » qui permettait de centraliser tous ces événements. J’ai dû réimplémenter cela à partir de rien, en me basant uniquement sur l’API de haut niveau documentée et la désassemblage. Cela a fonctionné : rePalm dispose désormais d’une implémentation fonctionnelle de TwInput, qui peut servir de référence à quiconque souhaitant également l’implémenter.
TwMidi a été principalement reverse-engineered en une semaine. Cependant, je n’ai pas encore développé de séquenceur MIDI. Je pourrais le faire et je le ferai, mais pas tout de suite. L’API est connue et c’est tout ce dont j’avais besoin pour renvoyer des codes d’erreur appropriés, permettant ainsi au reste du système de continuer à fonctionner.
Technologie matérielle réelle : reSpring
L’accessoire ultime pour Springboard
Lorsque Handspring a lancé le Visor, son emplacement d’extension Springboard était l’une des fonctionnalités les plus révolutionnaires. Il permettait d’ajouter divers dispositifs d’extension, tels que des téléphones cellulaires, des récepteurs GPS, des lecteurs de codes-barres, des lecteurs de cartes d’extension et des caméras. L’emplacement Springboard est intéressant car il constitue une connexion directe au bus de données et d’adresses du CPU, offrant ainsi de nombreuses possibilités d’expansion. J’ai décidé que la première application de rePalm serait un accessoire Springboard qui, une fois branché, mettrait à niveau un Visor vers PalmOS 5. L’idée est que reSpring exécutera rePalm sur son propre CPU, tandis que le Visor servira d’écran, de tactile et de boutons. J’ai collaboré avec George Rudolf Mezzomo sur reSpring, moi définissant les spécifications, lui réalisant les schémas et la mise en page, et moi développant le logiciel et les pilotes.
Interface avec le Visor
Pour le Visor, le module Springboard apparaît comme deux zones de mémoire (deux lignes de sélection de puce), chacune ayant une taille maximale de quelques mégaoctets. La première doit contenir une image ROM valide que le Visor peut détecter, structurée comme une mémoire ROM PalmOS, avec un seul tas. En général, ce tas contient une seule application - le pilote de ce module. Pour reSpring, j’ai décidé d’adopter une approche différente. Plusieurs raisons motivent ce choix. La principale était qu’une mémoire flash NOR pour stocker la ROM occuperait de l’espace sur la carte, mais je ne voulais pas non plus gérer de nombreux composants flashables différents sur la carte. Il y avait une troisième raison, mais nous y reviendrons plus tard.
Le Visor s’attend à interagir avec le Springboard en effectuant des accès mémoire (lectures et écritures), et le module doit se comporter comme un dispositif mémoire synchrone. Cela signifie qu’il n’y a pas de ligne « je suis prêt à répondre », mais plutôt un nombre fixe de cycles pour répondre à toute demande. Lorsqu’un module est inséré, le Visor configure ce nombre à six, mais il peut ensuite être réduit par l’application pilote du module. Essayer de répondre à des demandes avec un délai fixe (et très court) serait une charge CPU énorme pour notre processeur ARM. J’ai donc décidé que la solution la plus simple serait d’intégrer une RAM, permettant ainsi au Visor d’y accéder. Mais comment y accéder si le Visor peut le faire à tout moment ? Eh bien, il existe des types spéciaux de RAM qui permettent cela.
Oui, la fameuse (et coûteuse) RAM à double port. J’ai décidé que reSpring utiliserait une petite quantité de RAM à double port comme boîte aux lettres entre le Visor et le CPU de rePalm. De cette manière, le Visor pouvait y accéder à tout moment, tout comme rePalm. L’emplacement Springboard dispose également de deux lignes de demande d’interruption, une pour le Visor et une pour le module. Celles-ci peuvent être utilisées pour signaler lorsqu’un message est dans la boîte aux lettres. Cependant, deux problèmes se posent. Le premier est que les RAM à double port sont généralement volumineuses, principalement en raison du grand nombre de broches nécessaires. Étant donné que le Visor nécessite une mémoire de 16 bits dans l’emplacement Springboard, notre RAM à double port hypothétique devrait également être de 16 bits. Ensuite, nous avons besoin de lignes d’adresse, de lignes de contrôle, de lignes de sélection de byte, et de lignes de sélection de puce. Si nous devions utiliser une mémoire de 4 Ko, par exemple, nous aurions besoin de 11 lignes d’adresse, 16 lignes de données, 2 lignes de sélection de byte, une ligne de sélection de puce, une ligne d’activation de sortie, et une ligne d’activation d’écriture, PAR PORT ! Ajoutez au moins deux broches d’alimentation, et notre puce hypothétique devient un monstre de 66 broches. Étant donné que les packages de 66 broches n’existent pas, nous serions contraints d’utiliser une pièce de 100 broches. Et 4 Ko, ce n’est même pas beaucoup. Idéalement, nous aimerions y intégrer notre framebuffer entier pour éviter des transferts complexes. Malheureusement, comme l’a dit le grand philosophe Jagger, « On ne peut pas toujours avoir ce que l’on veut. » Les RAM à double port sont très coûteuses. Il n’y a que deux entreprises qui les fabriquent, et elles demandent beaucoup. J’ai donc opté pour 4
KB part uniquement basé sur le coût. Même à cette taille modeste de 4 Ko, cette RAM est de loin le composant le plus coûteux de la carte, à 25 $. Étant donné que les coûts d’intégration d’une pièce de 64 Ko (ma taille préférée) dépassaient mes attentes (et les capacités de mon portefeuille), j’ai décidé d’inventer un protocole de messagerie complexe et de le faire fonctionner avec une RAM de 4 Ko utilisée comme une boîte aux lettres bidirectionnelle.
Revenons à notre besoin d’une ROM pour contenir notre programme de pilote. Nulle part dans les spécifications de Sprinboard il n’y a réellement une exigence pour une ROM, juste une mémoire. Que signifie cela ? Nous pouvons éviter ce chip supplémentaire en intégrant l’image ROM dans le CPU reSpring, et en l’écrivant rapidement dans la RAM à double port au démarrage. Éliminer une puce à acheter et à placer sur la carte est fantastique !
Version 1
Je l’admets : il y a eu un peu de dérive fonctionnelle, mais le design final du matériel pour la version 1 s’est avéré être : 8 Mo de RAM, 128 Mo de flash NAND, un CPU à 192 MHz avec 2 Mo de flash pour le système d’exploitation, un emplacement pour carte microSD, un haut-parleur pour la sortie audio, et un amplificateur pour utiliser le microphone intégré du Visor pour l’entrée audio. La sortie audio sera réalisée de la même manière que sur la carte STM32F429, tandis que l’entrée audio se fera via le véritable ADC. La RAM principale fonctionne sur un bus de 32 bits à 96 MHz (bande passante de 384 Mo/s). Le flash NAND est sur un bus QSPI à 96 MHz (bande passante de 48 Mo/s). Le système d’exploitation sera stocké dans la mémoire flash interne du CPU STM32F469. Le NAND embarqué est juste une exploration que j’aimerais réaliser. Ce sera soit une carte SD interne, soit un stockage pour quelque chose comme NVFS (mais pas aussi instable), lorsque j’aurai le temps de l’écrire.
Alors, quand cela se produira-t-il ? Cinq cartes de version 1 m’ont été livrées fin novembre 2019 !
Mise en route de la version 1
Avoir le matériel en main est formidable. C’est encore mieux quand tout fonctionne correctement dès le premier essai. Fantastique comme des licornes, et tout aussi probable. Non… rien n’a fonctionné tout de suite. Les cartes ne voulaient pas communiquer avec le débogueur du tout, et après des semaines de torture, j’ai réalisé qu’il manquait des résistances de tirage sur les cartes. Ce n’était pas un problème sur les cartes de développement de STM, car elles incluent ces résistances. Une fois que le CPU a commencé à communiquer avec moi, il est devenu évident très rapidement qu’il était très instable. Il est spécifié pour fonctionner à 180 MHz (oui, cela signifie que normalement nous l’overclockons de 9,2 % à 196,6 MHz). Sur les cartes reSpring, le CPU ne fonctionnait pas avec une stabilité supérieure à 140 MHz. J’ai vérifié l’alimentation et les condensateurs de découplage. Tout semblait en place, jusqu’à ce que… VCAP1 et VCAP2 soient absents. Le cœur du CPU fonctionne à une tension inférieure à 3,3 V, donc le CPU a un régulateur interne. Ce régulateur a besoin de condensateurs pour stabiliser sa sortie face à une consommation variable par le CPU. C’est à cela que servent les broches VCAP1 et VCAP2. Eh bien, la carte n’avait pas de condensateurs sur VCAP1 et VCAP2. La sortie du régulateur interne oscillait énormément (+/- 600 mV sur une alimentation de 1,8 V, c’est beaucoup de fluctuation !). En fait, il est incroyable que le CPU ait fonctionné avec une alimentation aussi instable ! Après une autre reconfiguration sous le microscope avec l’ajout de deux condensateurs, la carte était stable. Passons au problème suivant…
Le problème suivant concernait la SDRAM. C’est l’endroit principal où le code s’exécute et où les données sont stockées. L’interface semblait complètement défaillante. Chaque mot écrit, le 15ème bit se lisait toujours comme 1, et les 0ème et 1er bits se lisaient toujours comme zéro. Inutile de dire que cela n’est pas acceptable pour une RAM dont j’espérais exécuter le code. Cela a été un véritable casse-tête à déboguer, mais il s’est avéré qu’il y avait une faute de frappe dans la configuration GPIO qui ne mappait pas les deux bits inférieurs à SDRAM DQ0 et DQ1. Cela laissait seulement le bit 15 bloqué à l’état haut à résoudre. Ce problème ne s’est pas reproduit sur d’autres cartes, donc c’était un problème local à une seule carte. Un examen minutieux au microscope a révélé une boule de soudure sous la broche laissée par le PCBA, qui court-circuitait une broche voisine qui était haute. Soulever la broche, enlever la soudure et reconnecter la broche au PCB a résolu ce problème. La SDRAM fonctionnait maintenant. Étant donné que cette SDRAM était assez différente de celle de la carte de découverte STM32F429, j’ai dû rechercher les configurations à utiliser pour elle, et traduire entre les temps utilisés par STM et ceux du datasheet de la RAM pour obtenir les réglages appropriés. Le résultat était une SDRAM assez rapide qui semble stable. Génial !
Bien sûr, ce n’était pas du tout la fin. Je ne pouvais pas accéder à la SRAM à double port. Une vérification rapide du schéma de la carte a révélé que sa broche de sélection de puce n’était pas du tout connectée au STM. J’ai sorti le microscope et le fer à souder, et j’ai ajouté un fil. Et voilà, la SRAM était accessible. Plus de lecture de datasheet a suivi pour la configurer correctement. En faisant cela, j’ai remarqué que sa consommation d’énergie est indiquée comme « faible », seulement 380 mW ! Donc, non seulement c’est la puce la plus coûteuse de la carte, mais c’est aussi la plus énergivore ! Il faut vraiment que cela change !
Je pourrais vous parler de nombreuses autres reconfigurations qui ont suivi après quelques tests dans le Visor, juste pour garder toute l’histoire de reconfiguration ensemble. Il s’est avéré que la ligne pour interrompre le Visor n’était jamais connectée nulle part, donc je l’ai câblée à PA4, afin que reSpring puisse envoyer une IRQ au Visor. Il s’est également avéré que la SRAM a beaucoup de « modes » et qu’elle était configurée pour le mauvais. Trois broches distinctes ont dû être reconfigurées pour passer du mode « maître » au mode « esclave ». Ces modes configurent comment plusieurs SRAM peuvent être utilisés ensemble. Comme reSpring n’en a qu’une, logiquement, elle était configurée comme maître. Cela s’est avéré être une erreur. Oups.
Intégration dans un Visor ?
Reconnaissance
Alors simple, n’est-ce pas ? Il suffit de l’insérer dans le Visor et de terminer ? Lire et relire le Guide de développement Springboard de Handspring fournissait presque toutes les informations nécessaires, en théorie. La pratique était différente. Pour une raison quelconque, peu importe comment je formattais la fausse ROM dans la SRAM partagée, le Visor ne la reconnaissait pas. j’ai abandonné cette approche et écrit une application de test pour simplement afficher ce que le Visor voit à l’écran, dans une série de boîtes de message. La ROM Springboard est toujours mappée à 0x28000000. J’ai rapidement réalisé les problèmes. Tout d’abord, le Visor effectue un échange de bytes sur tous les accès. Cela est dû au fait que la plupart du monde est en little-endian, tandis que le CPU 68k est en big-endian. Pour permettre aux concepteurs de périphériques de ne pas s’inquiéter, Handspring échange les bytes sur le bus. « Mais, » pourriez-vous dire, « qu’en est-il des accès non-mots ? » Il n’y a pas de tels accès. Le Visor accède toujours à 16 bits à la fois. Il n’y a pas de lignes de sélection de bytes. Pour nous, c’est en fait plutôt cool. Tant que nous communiquons en utilisant uniquement des quantités de 16 bits, aucun échange de bytes dans le logiciel n’est nécessaire. Il y avait un autre problème : le Visor voyait chaque autre mot que reSpring écrivait. Cela a nécessité quelques investigations, mais le résultat était à la fois hilarant et triste en même temps. Malgré tous les accès à Springboard étant de 16 bits de large, la ligne d’adresse 0 est câblée au connecteur Springboard. Pourquoi ? Qui sait ? Mais elle est toujours basse. Sur la carte reSpring, le connecteur Springboard A0 était câblé à RAM A0. Mais comme il est toujours à 0, cela signifie que le Visor ne peut accéder qu’à chaque autre mot de RAM – les adresses paires. …soupir… Donc, nous n’avons pas 4 Ko de RAM partagée. Nous avons 2 Ko… Mais maintenant que nous savons tout cela, pouvons-nous faire reconnaître reSpring comme un module Springboard ? OUI ! L’image à droite a été prise la première fois que le module reSpring a été reconnu par le Visor.
Économie d’espace précieux
Bien sûr, cela a été…
Défis Initiaux
Les difficultés ne font que commencer. Les applications s’exécutent directement à partir de la mémoire ROM du module, ce qui présente des avantages et des inconvénients. Pour nous, cela s’avère principalement problématique. Que cela signifie-t-il ? L’image ROM que nous plaçons dans la SRAM doit y rester indéfiniment. Par conséquent, il est crucial de la rendre aussi petite que possible. J’ai consacré beaucoup d’efforts à réduire sa taille, atteignant environ 684 octets. La plupart de mes tentatives pour superposer des structures afin d’économiser de l’espace n’ont pas abouti, car le code Visor qui valide la ROM sur le module Springboard est impitoyable. L’application elle-même est minuscule. Elle met en œuvre le protocole de messagerie le plus simple possible (un mot à la fois) pour communiquer avec le STM. Elle ne prend pas en charge les graphiques ni le stylet. Alors, que fait-elle réellement ? Elle télécharge un morceau de code plus volumineux, mot par mot, depuis le STM. Ce code est stocké dans la RAM du Visor et peut y être exécuté. Ensuite, il suffit de sauter à ce code. Pourquoi ? Cela nous permet d’économiser de l’espace précieux dans la SRAM. Au final, nous disposons de 2 Ko – 684 octets = 1,3 Ko de RAM pour l’échange de données. Ce n’est pas beaucoup, mais cela pourrait suffire.
Protocoles de Communication
Avec 1,3 Ko de RAM partagée et une interruption dans chaque direction, comment communiquons-nous ? J’ai conçu deux protocoles de communication : un simple et un complexe. Le protocole simple est utilisé uniquement pour charger le code plus volumineux dans la RAM du Visor. Il envoie un message de 16 bits et reçoit une réponse de 16 bits. Les messages sont assez basiques : une demande de réponse pour vérifier la communication, quelques requêtes pour obtenir des informations sur les emplacements des grandes boîtes aux lettres pour le protocole complexe, une demande pour connaître la taille du code téléchargé, et le message pour télécharger le mot suivant du code. Une fois le code téléchargé et conscient des emplacements et tailles des boîtes aux lettres, il utilise le protocole complexe. En quoi cela diffère-t-il ? Un gros morceau de données est placé dans la boîte aux lettres, puis le protocole simple est utilisé pour indiquer une demande et obtenir une réponse. Les boîtes aux lettres sont unidirectionnelles et de tailles très différentes. La boîte aux lettres du STM vers le Visor occupe environ 85 % de l’espace, tandis que celle dans l’autre direction est minuscule. La raison est évidente : les données d’écran sont volumineuses.
Toutes les demandes proviennent toujours du Visor et reçoivent une réponse du module reSpring. Si le module a quelque chose à communiquer au Visor, il déclenche une IRQ, et le Visor envoie une demande pour les données. Si le Visor n’a rien à envoyer, il envoie simplement un message NOP vide. Comment le Visor envoie-t-il une demande ? D’abord, les données sont écrites dans la boîte aux lettres, puis le type de message est écrit à un emplacement spécial de la SRAM, et enfin, un marqueur spécial indiquant que le message est terminé est écrit à un autre emplacement de la SRAM. Une IRQ est ensuite déclenchée vers le module. Le gestionnaire d’IRQ dans le STM recherche ce marqueur « message valide », et s’il est trouvé, le message est lu et une réponse est donnée : d’abord, les données sont écrites dans la boîte aux lettres, puis le type de message est écrit à l’emplacement SRAM partagé pour le type de message, et enfin, le marqueur « c’est une réponse » est écrit à l’emplacement de marqueur SRAM. Pendant ce temps, le Visor lit en boucle l’emplacement de marqueur SRAM en attendant qu’il change. Cette attente active pose-t-elle un problème ? Non. Le STM est si rapide, et le code pour gérer l’IRQ effectue si peu de traitement que les réponses arrivent souvent en quelques microsecondes.
Support Précoce du Visor
À ce stade, certaines fonctionnalités de base pouvaient être testées, mais elles échouaient toutes sur le Visor Deluxe et le Visor Solo. En fait, tout plantait peu après l’insertion du module. Pourquoi ? La raison est en réalité évidente : ils fonctionnent sous PalmOS 3.1, tandis que tous les autres Visors fonctionnent sous PalmOS 3.5. Un nombre surprenant d’API sur lesquelles on compte dans la programmation PalmOS ne sont tout simplement pas disponibles sur PalmOS 3.1. Des fonctions simples comme ErrAlertCustom()
, BmpGetBits()
, WinPalette()
, et WinGetBitmap()
n’existent tout simplement pas. J’ai dû écrire du code pour éviter de les utiliser dans PalmOS 3.1. Mais certaines d’entre elles sont nécessaires. Par exemple, comment copier directement des bits dans le framebuffer d’affichage si je ne peux pas obtenir un pointeur vers le framebuffer via BmpGetBits( WinGetBitmap( WinGetDisplayWindow ()))
? J’ai tenté de plonger dans les structures des fenêtres et des bitmaps moi-même, mais il s’avère que le bitmap d’affichage n’est pas un bitmap valide dans PalmOS 3.1. j’ai réalisé que PalmOS 3.1 ne supportait que les processeurs MC68EZ328 et MC68328, et que les deux configurent l’adresse de base du contrôleur d’affichage dans le même registre, donc je l’ai simplement lu directement. Quant à la configuration de la palette, elle n’est pas nécessaire puisque PalmOS 3.1 ne prend pas en charge les couleurs ou les palettes. C’est assez simple.
Optimisation des Performances
Données Initiales
Certaines données sont nécessaires pour que rePalm puisse démarrer correctement : la résolution de l’écran et les profondeurs supportées, les indicateurs matériels (par exemple : si l’écran a un réglage de luminosité ou de contraste), et si l’appareil dispose d’une LED d’alerte (oui, vous avez bien lu, plus d’informations à ce sujet plus tard). Ainsi, rePalm ne démarre pas tant qu’il n’a pas reçu un message « continuer le démarrage » envoyé par le code sur le Visor une fois toutes ces informations collectées.
Transmission des Données d’Affichage
Les données à plus haut débit que nous devons transférer entre le Visor et le module reSpring sont les données d’affichage. Par exemple, pour un écran de 160×160 pixels à 16 bits par pixel à 60 images par seconde, nous devrions transférer 160x160x16x60=23,44 Mbps. Ce n’est pas un faible débit à tenter sur un CPU 68k à 33 MHz. En fait, je ne pense pas que cela soit même possible. Pour un niveau de gris à 4 bits par pixel, les chiffres semblent un peu meilleurs : 160x160x4x60=5,86 Mbps. Mais il y a un second problème. Chaque message nécessite un aller-retour complet. Nous sommes limités par la latence d’interruption du Visor et notre latence générale d’aller-retour. Malheureusement, cette latence peut atteindre 2 à 4 ms. Nous devons donc minimiser le nombre de paquets envoyés. Nous y reviendrons plus tard. Au départ, j’envoyais les données morceau par morceau et les affichais à l’écran. Cela a-t-il fonctionné du premier coup ? En fait, presque. L’image à droite montre les résultats. Il a suffi d’un simple échange de bytes pour que cela fonctionne parfaitement !
Cependant, c’était assez lent, environ 2 images par seconde. En y regardant de plus près, j’ai réalisé que l’appel à MemMove était l’une des raisons. J’ai écrit une routine optimisée pour déplacer de gros blocs de données, étant donné qu’ils n’étaient pas superposés et toujours alignés. Cela a amélioré le taux de rafraîchissement à environ 8 images par seconde sur les appareils en niveaux de gris. D’autres améliorations étaient nécessaires. Le principal problème était le temps d’aller-retour pour copier les données, attendre, les copier à nouveau, et ainsi de suite. Comment minimiser le nombre d’aller-retours ? Oui, en compressant les données. J’ai écrit un compresseur d’images sans perte très rapide sur le STM. Il fonctionne un peu comme LZ, avec une table de hachage pour trouver les occurrences précédentes d’un motif de données. Les taux de compression étaient très bons, et les taux de rafraîchissement ont atteint 30 à 40 images par seconde sur les appareils en niveaux de gris. Le Bejeweled en couleur est devenu jouable même !
Obtenir les données d’affichage était également assez intéressant. PalmOS 5 s’attend à ce que l’affichage soit simplement un framebuffer qui peut être écrit librement. Bien qu’il existe des API pour dessiner, on peut également écrire directement dans le framebuffer. Cela signifie qu’il n’y a pas vraiment de moyen d’être notifié lorsque l’image à l’écran change. Nous pourrions envoyer les données d’écran en continu. En fait, c’est ce que j’ai fait au départ. Cela épuise la batterie du Visor d’environ deux pour cent par minute.
Optimisation de l’Utilisation de la Mémoire et de l’Énergie
Lorsqu’un processeur est constamment sollicité, il peut entraîner une consommation excessive de ressources. Pour éviter cela, il est essentiel de trouver un moyen efficace de recevoir des notifications lors de l’exécution de certaines tâches. Une solution innovante consiste à utiliser l’Unité de Gestion de la Mémoire (MPU). En protégeant le tampon d’affichage contre les écritures, nous permettons uniquement les lectures. En cas d’écriture, une exception est déclenchée. Nous gérons cette exception en programmant un minuteur pour 1/60 de seconde, après quoi nous autorisons les écritures et reprenons le processus. Le code qui était en cours d’exécution continue sans interruption. Lorsque le minuteur se déclenche, nous verrouillons à nouveau le tampon d’affichage et demandons le transfert d’une image complète à Visor. Cela nous permet d’éviter d’envoyer les mêmes données à plusieurs reprises. De plus, certaines écritures à l’écran peuvent ne rien changer, ce qui m’a conduit à ajouter une couche supplémentaire : chaque fois que nous envoyons une image, nous en conservons une copie. Lors de la prochaine demande, nous comparons les images et n’effectuons aucune action si elles sont identiques. Grâce à la compression et à ces deux techniques, nous parvenons à une utilisation raisonnable de l’énergie et à un taux de rafraîchissement d’écran satisfaisant.
Gestion des Boutons, Stylet, Luminosité et Informations sur la Batterie
Le Visor a la capacité d’envoyer des données au module reSpring à tout moment, ce qui facilite l’envoi d’informations sur les boutons et le stylet. Pour le transfert de données dans l’autre sens, le design est tout aussi simple. Si le module demande une interruption (IRQ), le Visor envoie un message NOP, et en réponse, le module transmet sa demande. Les requêtes peuvent concerner la palette d’affichage, la luminosité, le contraste ou les informations sur la batterie. Le Visor exécute l’action demandée et peut éventuellement répondre, par exemple, pour les informations sur la batterie.
Support du Microphone
Sur les premières versions des cartes, l’amplificateur audio était mal câblé, mais après quelques modifications complexes, il a été possible de tester la fonctionnalité d’enregistrement audio de base. Cela a fonctionné ! Bien que la qualité ne soit pas optimale, j’ai pu reconnaître ma voix en disant « 1 2 3 4 5 6 7 » à l’application de mémos vocaux. Cependant, amplifier le microphone du Visor s’est avéré compliqué, nécessitant un gain de 40 dB pour obtenir des résultats exploitables. Les composants analogiques nécessaires pour réaliser cela de manière efficace et sans bruit sont trop coûteux et nombreux. Ainsi, pour la version 2, il a été décidé d’intégrer un microphone numérique sur la carte, ce qui s’est avéré moins cher. De plus, éviter l’analogique est souvent la meilleure option pour une carte !
Améliorations Techniques
Transmission Série/IrDA
J’ai intégré la possibilité de transférer le port série du Visor vers reSpring. À quoi cela sert-il ? Principalement pour le HotSync (qui fonctionne) et le transfert IR (qui fonctionne en grande partie). Cela représente un défi technique. Pour prendre en charge PalmOS 3.1, il est nécessaire d’utiliser l’API de l’Ancien Gestionnaire Série. N’ayant jamais utilisé cette API, j’ai dû m’adapter. Les API sont similaires, mais peu conviviales pour nos besoins. Nous devons être informés de l’arrivée de données sans avoir à les vérifier constamment, ce qui consomme de la batterie. J’ai finalement découvert qu’en utilisant la « fenêtre de réception » et le « gestionnaire de réveil », je pouvais obtenir un rappel lorsque des données arrivaient. J’ai également trouvé un moyen d’augmenter la taille du tampon de réception, ce qui permet d’éviter de perdre des données reçues même si nous prenons quelques millisecondes pour les traiter. J’ai réussi à connecter le port série du Visor à un pilote dans reSpring. Malheureusement, le transfert IR nécessite une réponse rapide, ce qui est difficile à atteindre avec notre latence. Le transfert fonctionne, mais pas toujours. Le HotSync fonctionne également, même via USB.
LED d’Alerte
Étant donné que rePalm prend en charge les LED d’alerte et que certains modèles de Visor en sont équipés (Pro, Prism et Edge), j’ai voulu les relier. Cependant, il n’existe pas d’API publique pour accéder aux LED sur les appareils Handspring. Après quelques recherches, j’ai découvert que le HAL de Handspring dispose d’une fonction pour régler l’état de la LED : HalLEDCommand(). Cette fonction fait exactement ce que je veux, mais les versions antérieures du HAL de Handspring ne la prennent pas en charge, ce qui peut entraîner un plantage. En réalité, seuls trois appareils possèdent une LED, et je peux les détecter. J’ai donc opté pour un accès direct au matériel. Sur le Visor Edge, la LED est sur GPIO K4, sur le Pro, c’est K3, et sur le Prism, C7. Nous pouvons écrire directement sur ces GPIO, et cela fonctionne comme prévu.
Mise à Jour Logicielle
Je souhaitais que reSpring puisse se mettre à jour automatiquement à partir d’une carte SD. Comment y parvenir ? Le flash dans le STM32 peut être écrit par du code exécuté sur le STM32, donc cela ne devrait pas être trop compliqué. Cependant, quelques complications se présentent : l’ensemble de PalmOS fonctionne à partir du flash, y compris les pilotes pour divers matériels. Notre couche de communication pour interagir avec le Visor est également intégrée. Pour effectuer la mise à jour, nous devons arrêter l’ensemble du système d’exploitation et désactiver toutes les interruptions et pilotes. Cela semble simple, mais parmi ces pilotes se trouvent ceux de la carte SD, où se trouve notre mise à jour. La solution consiste à copier la mise à jour en RAM avant de commencer, car la RAM ne nécessite pas de pilotes. Mais comment afficher la progression à l’utilisateur ? Notre tampon d’affichage n’est pas réel, et faire afficher cela par le Visor nécessite beaucoup de code et des interruptions fonctionnelles. Il était donc peu probable que cela fonctionne normalement.
J’ai décidé que la meilleure façon de procéder était de laisser le Visor dessiner l’interface utilisateur de mise à jour lui-même, en utilisant un emplacement SRAM unique pour afficher la progression. Écrire dans un emplacement SRAM est quelque chose que notre processus de mise à jour peut faire sans problème, car la SRAM est simplement mappée en mémoire. Le reste était simple : un programme pour charger la mise à jour en RAM, envoyer le message « mettre à jour maintenant », puis flasher la ROM, tout en écrivant dans l’emplacement SRAM approprié le « pourcentage complété ». Cela a nécessité l’exportation de l’API « envoyer un message » du DAL de rePalm pour que les applications puissent l’utiliser. J’ai réalisé cela.
Stockage NAND Intégré
Un Défi avec le NAND
La carte reSpring est équipée de 256 Mo de mémoire flash NAND sur un bus QSPI. Pourquoi ? À l’époque de sa conception, je pensais que ce serait intéressant et c’était assez économique. Le NAND est la technologie de stockage sous-jacente à la plupart des dispositifs modernes - vos cartes SD, vos SSD et le stockage de votre téléphone. Cependant, le NAND présente des défis : il nécessite des corrections d’erreurs, car il peut parfois inverser un ou deux bits. De plus, des inversions de bits peuvent s’accumuler avec le temps, rendant la correction d’erreurs insuffisante, ce qui nécessite de déplacer les données lorsque cela devient nécessaire. L’unité adressable la plus petite du NAND est une page, qui est la taille pouvant être lue ou programmée. La programmation ne fait que changer des bits en zéro, et pour récupérer des bits, une opération d’effacement est nécessaire, mais cela se fait par bloc, ce qui complique la gestion des données.
Gestion des Disques NAND : Défis et Solutions
Les dispositifs de stockage NAND présentent des défis uniques en raison de leur structure. En raison de la nécessité d’utiliser des codes de correction d’erreurs, et du fait que les bits ne peuvent être inversés que de un à zéro, la réécriture des données devient complexe. De plus, il existe des limites quant au nombre de fois qu’une page peut être programmée avant d’être effacée. Les pages d’un bloc doivent souvent être programmées dans un ordre spécifique, et il est également possible que des blocs deviennent défectueux, ne parvenant pas à s’effacer ou à se programmer correctement. Il n’est pas rare qu’un appareil NAND soit livré avec des blocs défectueux dès sa sortie d’usine. Cela remet en question l’idée que l’on se fait du stockage par blocs. La gestion des NAND nécessite une attention particulière pour garantir une utilisation efficace de l’espace de stockage. Étant donné que les blocs s’usent à cause des effacements, il est crucial de répartir uniformément l’usure sur l’ensemble du dispositif, ce qui peut nécessiter le déplacement de données. De plus, des coupures de courant peuvent survenir pendant ces opérations, rendant nécessaire une gestion prudente des effacements et des écritures. Maintenir une vision cohérente de ce qui est stocké où est un véritable défi, et c’est là qu’intervient la couche de traduction flash (FTL).
Création d’une FTL
Ayant déjà développé une FTL il y a plus d’une décennie, j’avais une idée générale du processus. Cependant, cette fois-ci, j’avais des objectifs clairs. La priorité absolue était de garantir qu’aucune donnée ne soit perdue en cas de coupure de courant aléatoire, car le module peut être retiré à tout moment. La FTL que j’ai conçue est conçue pour ne jamais perdre de données, peu importe le moment où l’alimentation est coupée. Un autre objectif secondaire était de minimiser l’utilisation de la RAM, car reSpring ne dispose que de 8 Mo de mémoire vive.
Les pages de la NAND sur reSpring mesurent 2176 octets. Parmi ceux-ci, 4 sont réservés pour le « marqueur de bloc défectueux », 28 sont libres d’utilisation sans protection par correction d’erreurs, et le reste est divisé en quatre parties égales de 536 octets, qui peuvent être corrigées par le chip (en utilisant les 16 derniers octets pour le code ECC). Cela signifie qu’à chaque page, nous avons 2080 octets corrigés par erreur et 28 octets non corrigés. Chaque bloc contient 64 pages, et l’appareil dispose de 2048 blocs, dont au moins 2008 sont garantis comme étant fonctionnels dès la sortie d’usine. L’utilisation de l’ECC par le chip est avantageuse, car il dispose d’une unité matérielle spécialisée capable d’effectuer cette tâche beaucoup plus rapidement que notre CPU ne pourrait le faire en logiciel. Il nous informe également du nombre de bits corrigés à chaque lecture, ce qui est essentiel pour évaluer la santé de chaque page et décider quand relocaliser les données avant qu’elles ne deviennent illisibles.
Présentation de la FTL comme un Dispositif de Bloc
J’ai décidé que ma FTL se présenterait comme un dispositif de bloc avec des blocs de 4 Ko. C’est la taille de cluster que FAT16 devrait idéalement utiliser sur notre appareil, et des blocs plus grands permettent de réduire la taille de la table de mappage (la correspondance entre le « numéro de secteur » virtuel et le « numéro de page » réel). Ainsi, nous traiterions toujours deux pages comme une seule. Cela signifie que chacune de nos pages virtuelles contiendrait 4160 octets de données corrigées par erreur et 56 octets de données non corrigées. Étant donné que notre flash permet d’écrire la même page deux fois, nous utiliserons la zone non corrigée pour stocker des données persistantes, telles que le nombre d’effacements de ce bloc, ainsi que pour les blocs précédents et suivants, et un compteur de génération pour déterminer l’ancienneté des informations. L’ECC fait maison était simple : un code de Hamming pour corriger jusqu’à un bit d’erreur, et ensuite répliquer l’information et le code de Hamming trois fois. Cela devrait fournir une protection suffisante. Comme cela n’utilisait que la partie non corrigée des pages, nous pouvons facilement écrire des données corrigées par erreur par-dessus sans problème. Chaque fois que nous effaçons une page, nous écrivons immédiatement ces données. Si nous sommes interrompus, les pages environnantes contiennent les informations nécessaires pour reprendre l’écriture après le retour de l’alimentation.
Gestion des Données et Points de Contrôle
Les données corrigées par erreur contiennent les données utilisateur (4096 octets) ainsi que nos données de service, telles que le secteur virtuel associé, le compteur de génération, des informations sur ce bloc et quelques blocs voisins, ainsi que d’autres informations. Ces données nous permettent de reconstruire la table de mappage après un cycle d’alimentation. Cependant, lire l’intégralité de l’appareil à chaque mise sous tension est lent et peu pratique. Nous avons donc mis en place des points de contrôle. Chaque fois que l’appareil est éteint ou que la FTL est démontée, nous écrivons un point de contrôle. Celui-ci contient les données de mappage et d’autres informations qui nous permettent de reprendre rapidement l’opération sans avoir à scanner l’intégralité de l’appareil. Bien sûr, en cas de coupure de courant inattendue, un scan est nécessaire. Pour ces cas, une optimisation a été mise en place : un répertoire à la fin de chaque bloc indique son contenu, ce qui permet de lire seulement 1/32 de l’appareil au lieu de 100 %, offrant ainsi un gain de vitesse de 32 fois !
Intégration avec PalmOS
Les requêtes de lecture et d’écriture de PalmOS se traduisent directement par des opérations de lecture et d’écriture de la couche FTL. Cependant, un problème se pose : PalmOS ne prend en charge que des dispositifs de bloc avec des tailles de secteur de 512 octets. J’ai donc développé une couche de traduction simple qui effectue des opérations de lecture-modification-écriture selon les besoins pour mapper mes secteurs de 4 Ko aux secteurs de 512 octets de PalmOS, si la demande de PalmOS ne s’aligne pas parfaitement avec les secteurs de 4 Ko de la FTL. Cela n’est pas aussi complexe ou lent que l’on pourrait le penser, car PalmOS utilise FAT16 pour formater l’appareil. Lorsqu’il le fait, il interroge l’appareil sur sa taille de bloc préférée. Nous répondons avec 4 Ko, et à partir de ce moment, le pilote FAT de PalmOS n’écrit que des clusters de 4 Ko complets, qui s’alignent parfaitement avec nos secteurs de 4 Ko de la FTL. L’utilisation de mémoire à l’exécution de la FTL n’est que de 128 Ko, ce qui est plutôt raisonnable, si je puis me permettre de le dire ! J’ai conçu une série de tests rigoureux pour la FTL et les ai exécutés sur mon ordinateur pendant plusieurs nuits. Les tests simulaient des défaillances de données, des coupures de courant aléatoires, etc. La FTL a réussi. Il y a en réalité beaucoup plus de détails concernant cette FTL, et vous êtes libre d’explorer le code source pour en savoir plus.
Un Dernier Détail Surprenant
Malgré tout ce travail, rePalm fonctionnait plutôt bien dans l’ensemble. Cependant, il arrivait parfois qu’un message soit perdu entre le Visor et le module, ou vice-versa. Après de nombreuses heures de débogage, j’ai fait une découverte surprenante. La SRAM à double port ne prend pas en charge l’accès simultané à la même adresse par les deux ports. Cela est documenté dans sa fiche technique comme une « fonctionnalité utile », mais ce n’est en réalité pas le cas. Il pourrait être raisonnable d’interdire deux écritures simultanées sur le même mot, mais deux lectures devraient fonctionner, tout comme une lecture et une écriture (avec une lecture retournant soit l’ancienne, soit la nouvelle donnée, ou même un mélange des deux). Cette SRAM signale plutôt « occupé » à un côté. Étant donné qu’elle ne devrait jamais être occupée, et que le slot Springboard n’a même pas de broche BUSY, ces signaux n’étaient reliés à rien. C’est dans une note de bas de page du manuel que j’ai trouvé cette information. Il était indiqué que passer la puce en mode SLAVE et élever les broches BUSY (qui sont désormais des entrées) à un niveau élevé permettrait un accès simultané. Cela fonctionne en partie. Il n’y a plus de signalisation d’occupation, mais parfois une écriture sera PERDUE si elle est exécutée en même temps qu’une lecture. De plus, une lecture renverra parfois ZÉRO si elle est exécutée en même temps qu’une autre lecture ou écriture, même si les anciennes et nouvelles données n’étaient pas toutes deux nulles. Il semble qu’il n’y ait pas de solution à ce problème. Une autre entreprise proposant de la SRAM à double port avait la même limitation, ce qui me fait penser qu’aucun fabricant dans l’industrie ne produit de véritables SRAM à double port. Cette SRAM dispose de ce que l’on appelle des « sémaphores », qui peuvent être utilisés pour mettre en œuvre de véritables sémaphores partagés par les deux dispositifs, mais autrement, ce n’est pas une véritable RAM à double port. Dommage !
Utiliser ces sémaphores nécessiterait un câblage significatif : il faudrait une nouvelle ligne de sélection de puce pour cette puce et inventer un nouveau moyen d’interrompre le STM, car la seconde ligne de sélection de puce serait désormais utilisée pour accéder aux sémaphores. Cela dépassait mes capacités de reconfiguration, donc j’ai simplement renforcé le protocole pour éviter ces problèmes. Désormais, le STM écrira chaque mot de données qui pourrait être lu simultanément 64 fois, puis le relira pour vérifier qu’il a été écrit. Le protocole de communication a également été modifié pour ne jamais utiliser de zéros, de sorte que si un zéro est détecté, il est clair qu’une nouvelle lecture est nécessaire. Grâce à ces ajustements, la communication est stable, mais lors de la prochaine révision de la carte, je pense que des améliorations supplémentaires seront nécessaires.
Technologie : Un matériel plus authentique
rePalm-MSIO
Après avoir analysé le protocole Sony MemoryStick, une idée a émergé : pourquoi ne pas créer une version rePalm sur un MemoryStick ? En théorie, il serait possible de faire fonctionner un microcontrôleur comme un appareil MemoryStick, de charger un programme sur l’appareil hôte PalmOS de Sony, puis de le contrôler, à l’instar de ce que faisait reSpring. C’était l’objectif, bien sûr. L’espace est restreint et les exigences de synchronisation sont extrêmes. Le fait que le protocole MemoryStick soit si différent des bus normaux signifie qu’il n’y aura pas de solutions simples. Cependant, j’étais déterminé à faire fonctionner ce projet.
Choix du microcontrôleur
Utiliser un STM32F429 avec une puce SDRAM prendrait trop de place pour s’insérer dans un slot MemoryStick. J’ai donc opté pour un chip STM32H7 à 64 broches. Ce dernier dispose de 1,25 Mo de RAM interne, ce qui est un peu juste pour PalmOS. Heureusement, il prend en charge une interface QSPI en lecture/écriture, ce qui est parfait pour interagir avec des puces PSRAM QSPI comme l’APS6404L d’APMemory ! Cela permet d’avoir 8 Mo de RAM sans occuper beaucoup d’espace sur la carte ni nécessiter un grand nombre de broches. Le STM32H7 est également un Cortex-M7, offrant une amélioration significative par rapport au cœur Cortex-M4 du STM32F429. Le M7 est plus rapide par cycle et dispose d’un cache ! Le fait que le STM32F429 n’ait pas de cache était un handicap sérieux lors de l’exécution de code depuis la RAM, car celle-ci était limitée à la moitié de la fréquence d’horloge du cœur. Avec un ensemble de travail suffisamment petit, le M7 peut fonctionner à pleine vitesse depuis le cache ! De plus, il y a le TCM, une mémoire proche du cœur qui fonctionne toujours à pleine vitesse sans délai ni états d’attente !
J’ai conçu la carte pour qu’elle s’insère dans le slot MemoryStick. Il s’agit d’une carte à 4 couches (qui est apparemment très abordable maintenant). Cela facilite le routage et améliore l’intégrité du signal. Avec l’épaisseur de carte appropriée, il y a juste assez d’espace pour que les puces s’adaptent. Tout fonctionne, s’insère, clique, tout ! C’est assez incroyable, en fait. Bien sûr, il y a eu des erreurs, mais lors de la deuxième révision de la carte, il n’a fallu qu’un seul fil de contournement, comme vous pouvez le voir sur la photo. La carte est exactement de la taille d’un MemoryStick. Il y a un surplus qui dépasse, ce sont les en-têtes de débogage et ils sont détachables. J’en ai un que j’ai détaché et il est incroyable de voir à quel point il s’adapte bien à l’intérieur.
Les problèmes rencontrés…
Évidemment, étant donné qu’il s’agit d’une puce STM, des bugs sont apparus. La puce se bloquait parfois complètement lors de l’exécution depuis la RAM QSPI. Lors d’une consultation, ST a suggéré de modifier les paramètres de l’MPU pour rendre la RAM QSPI non mise en cache. C’est une suggestion absurde, car même si cela fonctionnait (spoiler : ça ne fonctionne pas), cela rendrait cette RAM tellement lente qu’elle deviendrait inutile. Quoi qu’il en soit, lorsque j’ai essayé cela, la RAM s’est corrompue. J’ai vérifié avec des traces de bus et l’ai présenté à STM. ils ont admis que toute écriture sur l’interface QSPI qui n’est pas séquentielle et de taille mot entraînerait une corruption. Cela me dit clairement quel était le seul test qu’ils avaient jamais effectué sur ce périphérique. Quel désespoir…
Heureusement, avec le cache activé, l’éviction de la ligne de cache sale écrira toujours un nombre entier de mots de manière séquentielle, donc il y a de l’espoir. Malheureusement, la puce fonctionnait un moment, puis se bloquait. Ce blocage était très étrange, mon débogueur ne pouvait pas se connecter au cœur dans cet état, mais il pouvait accéder au port d’accès de débogage lui-même. Cela m’a amené à croire que ce n’était pas le cœur qui se bloquait, mais le tissu interne AHB. J’ai pu le confirmer en me connectant à un autre port d’accès de débogage (celui sur AHB3), où je pouvais explorer mais n’avais pas accès aux bus AHB principaux. STM n’avait pas d’idées.
En tenant compte de ce que je savais sur le fonctionnement des bus AHB, j’ai formulé des hypothèses sur la manière dont ST avait probablement conçu les arbitres et comment ils avaient câblé leur unité QSPI. J’ai deviné le problème et trouvé un contournement qui pourrait fonctionner. Après quelques prototypes, je peux confirmer que cela fonctionne. Le coût en performance est d’environ 20 % (par rapport à l’absence de contournement), mais au moins, il n’y a plus de blocages. Pourquoi suis-je si prudent sur ce contournement ? Eh bien, tout en niant l’existence du problème, STM a demandé les détails précis de mon contournement une fois qu’ils ont appris que j’en avais trouvé un. Apparemment, un client réellement important a également rencontré ce problème. Je refuse actuellement de divulguer le contournement tant qu’ils n’acceptent pas d’admettre le problème. Jusqu’à présent, c’est une impasse, ce qui est acceptable – je ne perds aucune vente à cause de cela. Et eux… ?
Niveau bas de MSIO
Le signal principal qui contrôle les phases du protocole est le BS, qui précède toujours la transition d’état réelle d’un cycle, ce qui le rend très difficile à utiliser. Si seulement il n’était pas en avance d’un cycle, je pourrais l’utiliser (et son inverse) comme sélecteurs de puce et essayer d’utiliser les unités de bus SPI matérielles d’une manière ou d’une autre. Après quelques réflexions, une solution est devenue évidente. Deux bascules feront l’affaire. En faisant passer le signal BS à travers elles, je peux le retarder d’un cycle. Trouver une bascule à double déclenchement sur front négatif s’est avéré impossible, donc un inverseur a été ajouté, ce qui m’a permis d’utiliser un SN74LVC74A facilement disponible.
Avec le signal BS retardé, il pouvait être utilisé comme sélection de puce pour certaines unités SPI. Pour que cela fonctionne, j’ai câblé TROIS unités SPI ensemble. Le premier front du BS déclenche un canal DMA qui active les trois unités SPI : l’une reçoit le TPC, et les deux autres sont prêtes à recevoir les données qui suivent. Nous n’aurons pas le temps de valider le TPC entre-temps, donc nous préparons l’unité SPI pour le recevoir quoi qu’il arrive. Cela ne pose aucun problème. Ce premier front du BS déclenche également une interruption logicielle. En supposant qu’il n’y ait pas trop de délais, nous arriverons dans l’IRQ après que le TPC ait déjà été reçu et, si la transaction est une écriture, les données sont déjà en route. Si nous avons moins de chance, les données pourraient même avoir déjà été entièrement reçues. À ce stade, nous pouvons valider le TPC et vérifier sa direction. Si c’est une LECTURE, nous devons immédiatement envoyer le motif de handshake, donc nous utilisons l’une des unités SPI pour le faire maintenant. Pendant ce temps, nous trouvons les données et les mettons en file d’attente pour transmission, en indiquant à l’unité SPI d’envoyer également le CRC après. Si c’était une ÉCRITURE, nous avions deux unités SPI recevant les données. L’une copiait les données dans la RAM, l’autre dans l’unité CRC (le STM32H7 ne peut pas calculer le CRC des données entrantes si nous ne connaissons pas à l’avance la longueur). Nous vérifions rapidement le CRC et configurons l’une des unités SPI pour envoyer le motif de handshake afin d’accuser réception des données.
« Maintenant, tout cela semble très fragile », dirait un observateur avisé. Oui ! Très. Cela signifie également que nous ne pouvons jamais désactiver les interruptions trop longtemps, car il n’y a que quelques cycles de marge entre l’envoi des données et la nécessité d’une réponse pour éviter que l’hôte ne dépasse le délai. J’ai dû réarchitecturer un peu la gestion des interruptions du noyau rePalm pour permettre à certaines interruptions de ne JAMAIS être désactivées, en échange de concessions de la part de ces gestionnaires d’interruptions : ils ne font pas d’appels système ni ne modifient un état partagé avec d’autres morceaux de code. Alors, comment interagir avec eux ? Lorsqu’une transaction MSIO se termine, les données sont placées dans un tampon partagé, et une interruption logicielle est déclenchée, qui est gérée normalement par du code normal avec des contraintes normales. Cela peut être désactivé, priorisé, etc., car ce n’est plus critique en termes de temps. Bien sûr, tout le code critique en temps doit être exécuté depuis l’ITCM (la mémoire d’instructions étroitement couplée) pour respecter les délais.
Lorsque le STM32H7 fonctionne à 320 MHz, cela fonctionne la plupart du temps avec les nouveaux appareils Palm, car ils exécutent l’interface MSIO à 16 MHz, ce qui me laisse un peu de marge. Les appareils plus anciens comme le S500C sont plus difficiles. Ils fonctionnent avec le bus MSIO à 20 MHz, et le…
Défis et Solutions de l’Intégration MSIO
Les délais d’exécution sont très serrés. Bien que le système fonctionne correctement, si le cœur du dispositif attend une instruction de récupération depuis le QSPI, il ne pourra pas accéder au gestionnaire d’interruptions tant que cette opération n’est pas terminée, ce qui entraîne une latence accrue. Cela peut parfois retarder le gestionnaire d’interruptions MSIO, le faisant manquer la fenêtre appropriée pour accuser réception d’une transaction. Mon pilote côté hôte effectue des tentatives de reprise pour compenser ce problème. La véritable solution serait d’utiliser un petit FPGA pour décharger cette tâche du microcontrôleur principal. Je suis en train d’explorer cette option.
Vue d’ensemble de MSIO
Étant donné qu’il n’existe pas de pilotes MSIO pour rePalm, j’ai dû les développer moi-même. Mais comment un utilisateur pourrait-il les transférer sur l’appareil ? En théorie, d’après mes recherches en rétro-ingénierie, une MemoryStick peut avoir plusieurs fonctions, potentiellement de la mémoire et une ou plusieurs fonctions d’E/S. Aucune telle clé n’ayant été observée, j’ai décidé de créer la première. Pourquoi pas ? La logique de fonctionnement est assez simple : la fonction 0xFF devrait être dédiée à la mémoire, et tout autre numéro de fonction inutilisé pourrait être attribué à l’E/S de rePalm. J’ai choisi le numéro de fonction 0x64. Pourquoi simuler une mémoire ? Pour fournir le pilote à l’utilisateur, bien sûr !
Mon code simule un MemoryStick en lecture seule avec 4 Mo de stockage. Étant donné que les MemorySticks sont des dispositifs NAND bruts, mon code se comporte comme un modèle parfait - sans blocs défectueux, sans besoin de correction d’erreurs. Le support est « formaté » avec FAT12 et contient un système de fichiers plutôt curieux. Pour prendre en charge TOUTES les appareils Sony, le pilote est nécessaire à plusieurs endroits. Tout appareil avec PalmOS 4.0 ou version ultérieure affichera les fichiers dans /PALM/LAUNCHER pour l’utilisateur et lancera automatiquement /PALM/START.prc lors de l’insertion. Les appareils avec des versions antérieures de PalmOS ne permettront à l’utilisateur de naviguer que dans /PALM/PROGRAMS/MSFILES. Tous les appareils Sony, à l’exception des premiers modèles, avaient également un autre moyen de lancer automatiquement un exécutable lors de l’insertion de la clé – un utilitaire Sony appelé « MS AutoRun ». Ce dernier lit un fichier de configuration à /DEFAULT.ARN et charge le programme spécifié en RAM lors de l’insertion. L’auto-exécution n’est jamais déclenchée si la MemoryStick était déjà insérée au démarrage de l’appareil, ce qui rend cette méthode peu fiable. C’est pourquoi nous avons besoin que le fichier soit visible et accessible à l’utilisateur pour un lancement manuel. Comptons alors combien de copies de l’application pilote notre MemoryStick nécessite. Une dans /PALM/LAUNCHER, une dans /PALM/PROGRAMS/MSFILES, et une comme /PALM/START.prc. Trois copies. Cela ne suffira pas ! Si seulement FAT12 supportait les liens durs…
Mais attendez, si le système de fichiers est en lecture seule, il PEUT supporter les liens durs ! Plusieurs entrées de répertoire peuvent référencer la même chaîne de clusters. Cela ne pose problème que lorsque le fichier est supprimé, ce qui ne se produit pas sur un système de fichiers en lecture seule. Le système de fichiers contient donc un répertoire PALM à la racine, qui contient le fichier DEFAULT.ARN, pointant vers un cluster avec son contenu, un répertoire PROGRAMS, un répertoire LAUNCHER, et une entrée de répertoire nommée START.PRC pointant vers le premier cluster de notre pilote. Le répertoire PROGRAMS contient un répertoire MSFILES, qui lui-même contient une autre entrée de répertoire pointant vers le pilote, nommée DRIVER.PRC. Le répertoire /PALM/LAUNCHER contient la troisième entrée de répertoire pointant vers le pilote, également nommé DRIVER.PRC. PalmOS ne réalise pas de vérification du système de fichiers sur les supports en lecture seule, donc aucun problème ne se pose – tout fonctionne.
Performance de MSIO
Certains appareils Sony disposent d’une API MSIO exportée dans leurs pilotes MemoryStick, que j’ai pu rétroconcevoir (et publier). D’autres n’en avaient pas, mais Sony a publié des mises à jour incluant cette API. Ces mises à jour étaient généralement accompagnées de périphériques MSIO comme l’adaptateur Bluetooth MemoryStick ou la caméra MemoryStick. Certains appareils n’ont jamais eu de support MSIO officiel. Je souhaitais les prendre tous en charge, et comme j’avais déjà rétroconçu le fonctionnement de la puce hôte MemoryStick (MB86189), j’ai pu écrire mes propres pilotes, communiquant directement avec elle. Cela a fonctionné pour certains appareils. D’autres n’ont pas d’accès direct à la puce, car le DSP en prend le contrôle. Le DSP de Sony n’est pas documenté, le firmware est crypté, et la clé n’est pas connue. J’ai été bloqué un moment. j’ai réussi à comprendre juste assez pour pouvoir envoyer et recevoir des TPC bruts via le DSP. Cela a bien fonctionné sur presque tous les appareils, sauf sur la série N7xx. Leur firmware DSP était le plus ancien de tous (d’après mes informations) et la meilleure bande passante que j’ai pu en tirer était de 176 Kbit/s. Inutile de dire que ce n’est pas suffisant pour la vidéo en direct (ce que rePalm tente de réaliser). Cela fonctionne, mais la qualité n’est pas optimale.
Étant donné que MSIO permet des transferts de pas plus de 512 octets par transfert, le transfert des données d’image d’écran est complexe. La même compression que celle utilisée dans reSpring est appliquée ici. Même ainsi, la performance varie en fonction de l’appareil et de la configuration de l’écran. Sur les appareils à faible résolution, tout est rapide. Sur les appareils à haute résolution (sauf N7xx), 35 FPS est atteignable en mode 16 bits par pixel. C’est encore plus rapide sur les appareils en niveaux de gris. Le seul appareil PalmOS 4 HiRes+ (NR70V) est à la traîne avec environ 20 FPS. Cela est dû à la quantité de données à transférer à chaque image - 300 Ko.
Autres points à considérer
Il est intéressant de noter qu’Asus a licencié la technologie MemoryStick de Sony, donc les appareils PalmOS d’Asus (familles s10 et s60) utilisent également MemoryStick. J’ai ajouté le support pour eux. Pour chaque appareil, j’ai connecté autant que possible à rePalm. Les appareils avec une LED ont été reliés au gestionnaire d’attention, et ceux avec un moteur de vibration ont également été intégrés. La gestion du son est un peu plus complexe. Certains de ces appareils avaient un DSP pour le décodage MP3, mais la capacité de jouer des sons échantillonnés bruts est limitée, car le 68K n’était probablement pas capable de le faire assez rapidement. Il existe une API Sony pour jouer de l’ADPCM à 8 kHz et 4 bits par échantillon. J’ai envisagé de la connecter à la sortie audio de rePalm, mais je n’ai pas eu le temps de le faire. Il est probable que cela ne vaille pas la peine, car la qualité serait médiocre. J’ai également envisagé une alternative : faire encoder la sortie de rePalm en MP3 et trouver un moyen de l’envoyer au DSP, mais j’ai rencontré des obstacles. Dans la plupart des appareils, le firmware DSP lit le fichier MP3 directement depuis la MemoryStick, contournant complètement le système d’exploitation, ce qui me fait penser que je ne trouverai peut-être pas de moyen d’injecter des données MP3 même si j’y parvenais.
Au départ, j’ai développé sur le STM32H7B0RB. Cette variante ne dispose que de 128 Ko de mémoire flash, ce qui n’est évidemment pas suffisant pour contenir PalmOS. J’ai utilisé une partie de la RAM pour contenir une image ROM, que je chargeais via SWD à chaque fois. Cela fonctionnait assez bien, mais ce n’était pas vraiment pratique car cela ne pouvait pas être utilisé loin d’un ordinateur. Heureusement, j’ai pu (avec beaucoup d’aide d’une source anonyme) obtenir certains des puces STM32H7 avec 2 Mo de mémoire flash interne. Cela EST suffisant pour contenir PalmOS, donc maintenant j’ai des variantes qui démarrent directement à l’insertion. Les dernières cartes disposent également d’une mémoire NAND intégrée qui sert de dispositif de stockage pour l’utilisateur utilisant mon FTL, mentionné précédemment. L’album photo (lien ci-dessus) contient plus de photos et de vidéos ! Voici une d’elles. Profitez-en !
AximX3
Ce projet a été un défi amusant, réalisé pour le plaisir. Étant donné que cet appareil utilise un processeur ARMv5T, j’ai dû adapter mon noyau à cette architecture. Ce n’était pas très compliqué et cela fonctionne maintenant. Fait intéressant, cet appareil présente des similitudes internes avec le Palm Tungsten T3, ce qui signifie que cette même version de rePalm peut fonctionner avec peu de modifications sur le T|T3 également.
J’ai investi beaucoup d’efforts dans ce dispositif. Heureusement, une grande partie de l’analyse initiale du matériel avait déjà été effectuée dans le cadre de mon projet uARM-Palm. Presque tout fonctionne : l’audio entrant et sortant, la carte SD, l’infrarouge, l’écran tactile et les boutons, ainsi que le rapport de batterie. Seuls l’USB et le mode veille/réveil manquent. Pour l’USB, je ne vois pas d’intérêt, et le mode veille est compliqué par le bootloader intégré. Les premières versions utilisaient un chargeur WinCE que j’avais écrit pour charger le ROM dans la RAM et exécuter à partir de là. Une analyse plus approfondie du ROM de l’appareil m’a révélé qu’il existe un bootloader assez complet capable de flasher le ROM de l’appareil à partir de la carte SD. J’ai décidé d’exploiter cela, et avec quelques modifications, rePalm peut maintenant être flashé directement dans le ROM de l’appareil. Oui !
Comment cela fonctionne-t-il ? Le bootloader d’origine a un mode pour cela. Si un fichier image est placé sur la carte SD sous le nom /P16R_K0.NB0, en insérant la carte, en maintenant le sélecteur de la molette et le deuxième bouton d’application, puis en réinitialisant l’appareil, il flashera l’image dans la mémoire flash, juste après le bootloader. Cela peut être utilisé pour flasher rePalm ou pour reflasher l’image d’origine. Selon la version de l’AximX3 (il y en a trois), la quantité de mémoire flash et de RAM varie. rePalm détecte la RAM disponible et l’utilise entièrement !
Carte de Découverte STM32F469
Ceci a été un petit hack rapide pour voir PalmOS en action sur un écran à densité 3x. Aucun appareil de ce type n’a jamais été commercialisé. La carte STM32F469DISCOVERY possède un écran de 480×800, dont 480×720 est utilisé comme un écran à densité 3x avec une zone d’entrée dynamique. Cette carte est équipée d’un écran tactile capacitif, ce qui la rend peu adaptée à PalmOS. Les écrans tactiles capacitifs ne sont pas idéaux pour des frappes précises sur de petits éléments, car votre doigt obstrue généralement ce que vous essayez de toucher. Bien que cet écran soit relativement grand, cela n’aide pas beaucoup. J’ai réussi à faire fonctionner cette carte suffisamment pour voir ce que cela donne, mais j’ai peu travaillé dessus par la suite. Seuls l’écran, le tactile et la carte SD sont pris en charge. De plus, tout comme le STM32F429, le STM32F469 ne dispose d’aucune mémoire cache, ce qui le rend plutôt lent lorsqu’il fonctionne à partir de la SDRAM.
RP2040
C’est possible !
Quelle est la véritable exigence en RAM/CPU pour PalmOS 5 ? Étant donné que rePalm avait un support (du moins en théorie) pour Cortex-M0, j’ai voulu essayer sur du matériel réel, car le support avait été testé auparavant uniquement sur CortexEmu. Il se trouve qu’il existe une puce Cortex-M0 avec suffisamment de RAM : le RP2040, la puce du Raspberry Pi Pico à 4 $. J’ai ensuite cherché un écran avec un écran tactile facilement disponible. Il n’y avait pas beaucoup d’options, mais celle-ci semblait convenir. Après quelques investigations, il s’est avéré que le faire fonctionner correctement et rapidement ne serait pas facile. La solution spéciale du RP2040 – le PIO – est venue à la rescousse ! J’ai trouvé un moyen de le faire. J’ai changé les résistances sur la carte de l’écran de « SPI » à « SDIO » pour activer la carte SD, et j’ai câblé la LED pour qu’elle serve de LED d’alarme pour PalmOS. Ce sont les choses faciles.
Étant donné que ce projet dépend de certains comportements non documentés dans les puces Cortex-M, il était toujours incertain de ce qui se passerait dans certains cas. Par exemple, le Cortex-M3 provoque un UsageFault lorsque vous sautez à une adresse sans le bit inférieur défini, indiquant un passage en mode ARM. Que ferait le Cortex-M0 ? Il s’avère qu’il provoque simplement un HardFault. m0FaultDispatch à la rescousse ! Il est capable de catégoriser toutes les causes d’un HardFault et de les diriger vers le bon endroit. J’ai trouvé une différence avec le Cortex-M3. Lorsque le Cortex-M3 exécute une instruction BX PC, il sautera à l’adresse actuelle plus 4, en mode ARM. Cela diffère de ce que font les puces ARMv5 lorsque vous exécutez cette même instruction en mode Thumb. Elles sautent à l’adresse actuelle plus 4, arrondie à la baisse au multiple de 4 le plus proche, en mode ARM. Cette différence a déjà été gérée par mon code JIT et mon émulateur. Mais le Cortex-M0 fait encore une troisième chose dans ce cas. Il semble en fait traiter l’instruction comme invalide. Le PC n’est pas modifié, le mode n’est pas changé, et un HardFault est pris directement sur l’instruction elle-même. Curieusement, cela ne se produit pas si un autre registre non-PC avec le bit inférieur effacé est utilisé. Quoi qu’il en soit, j’ai ajusté le code JIT et l’émulateur pour gérer cela. J’ai également modifié CortexEmu pour émuler cela correctement.
Souvenirs
Le RP2040 ne dispose d’aucune mémoire flash, il utilise une mémoire flash externe Q/D/SPI pour le stockage de code et de données. Cela est pratique lorsque vous avez beaucoup de données. Pour rePalm, cela signifie que nous pouvons avoir un ROM aussi grand que la plus grande puce flash que nous pouvons acheter. Le Pi Pico est livré avec une puce de 2 Mo, donc je l’ai ciblée. La situation de la RAM est beaucoup plus serrée. Il n’y a que 264 Ko de RAM. Ce n’est pas beaucoup. Le dernier appareil PalmOS à avoir eu si peu de RAM fonctionnait sous PalmOS 1.0. Mais cela vaut la peine d’essayer. L’une des plus grandes dépenses en RAM concerne les graphiques. La principale est le framebuffer. PalmOS suppose que l’affichage a un framebuffer directement accessible par le CPU. Cela signifie que si je voulais utiliser l’ensemble de l’écran 320×240 en mode vrai couleur, le framebuffer occuperait 150 Ko. Ouf ! Eh bien, combien est acceptable ?
Après quelques expérimentations, pour démarrer avec succès et lancer le lanceur, l’application de préférences et le panneau de calibration du numériseur, environ 128 Ko de RAM dynamique sont nécessaires. Les diverses bases de données par défaut ainsi que les bases de données temporaires PACE dans le tas de stockage nécessitent un tas de stockage d’au moins 50 Ko. Une taille minimale de tas de stockage de 64 Ko est vraiment préférée, afin de ne pas manquer d’espace immédiatement au démarrage. Et le DAL de rePalm nécessite au moins 15 Ko de mémoire pour ses structures de données et environ 24 Ko pour le tas du noyau où les piles et diverses autres structures de données sont allouées. Additionnons tout cela. La somme est de 231 Ko. Cela laisse au maximum 33 Ko pour le framebuffer. Il y a quelques options. Nous pouvons utiliser tout l’écran à 2 bits par pixel (4 niveaux de gris). Cela nécessitera un framebuffer de 18,75 Ko. Nous pouvons utiliser un écran carré de 240×240 à 4 bits par pixel, pour un framebuffer de 28,125 Ko. Nous pouvons également utiliser la résolution standard à faible densité de 160×160 à un impressionnant 8 bits par pixel (la seule option non niveaux de gris).
On peut remarquer que les zones de mémoire ci-dessus n’incluaient pas un cache de traduction JIT. C’est exact. Bien que mon JIT prenne effectivement en charge le ciblage du Cortex-M0, il n’y a tout simplement pas assez d’espace pour que cela en vaille la peine. J’ai plutôt activé le cœur d’émulateur ARM asmM0 car il n’a besoin d’aucun espace supplémentaire. Ce n’est pas idéal, mais tant pis. Nous savions depuis le début que des compromis seraient nécessaires ! Tant que je montre, faisons en sorte d’avoir une expérience plein écran, avec une zone d’entrée dynamique et tout ! 320×240, c’est parti ! Le deuxième cœur du RP2040 n’est pas encore utilisé.
PACE à nouveau
Mon PACE patché ciblant le Cortex-M3 mentionné précédemment n’est d’aucune utilité sur un Cortex-M0. Combiné au fait que je ne peux pas utiliser le JIT signifie que tout le code 68K sera…
Fonctionnant sous une double émulation (68K émulé par ARM, ARM lui-même émulé en mode Thumb), il était temps de développer un tout nouvel émulateur 68k, bien sûr en langage d’assemblage Thumb-1. Je vous présente PACE.m0. Cet émulateur se révèle assez rapide, rivalisant efficacement avec le PACE ARM de Palm en termes de performance, comme l’a prouvé mes tests sur mon Tungsten T3. Cela a grandement contribué à rendre la version RP2040 utilisable, atteignant désormais une vitesse comparable à celle d’un Tungsten T.
État des lieux technologique
Il reste encore beaucoup à accomplir : intégrer le Bluetooth, le WiFi, l’USB, peaufiner le débogage de NVFS, et probablement bien d’autres tâches. Néanmoins, je mets à disposition quelques images préliminaires à tester, si vous possédez une carte de découverte STM32F429, un AximX3 ou un Raspberry Pi Pico avec l’écran adéquat. Notez qu’il n’y a pas de support pour l’USB. Si vous souhaitez expérimenter, voici le lien : LINK. Je poursuis également mes travaux sur les options matérielles reSpring/MSIO, et il se pourrait que vous puissiez bientôt mettre la main sur l’un d’eux :) Si vous avez déjà un module reSpring (vous savez qui vous êtes), l’archive mentionnée ci-dessus contient une mise à jour vers la version 1.3.0.0 pour vous également.
Code source technologique
Introduction au code source
Le téléchargement de la version 0000 du code source est disponible ici. Il s’agit d’une version très précoce du code source, destinée à permettre aux utilisateurs d’explorer cette base de code et de comprendre sa structure. Le fichier README explique l’organisation des répertoires, et un document de licence se trouve dans chaque répertoire. Pour construire ce projet, il est nécessaire de disposer d’une version moderne (c’est-à-dire la mienne) de PilRC (inclus) ainsi que d’une chaîne d’outils ARM cross-gcc. Certaines constructions nécessitent également une chaîne d’outils 68k spécifique à PalmOS, que vous pouvez trouver ici, par exemple.
Principes de construction
Créer une image fonctionnelle est un processus en plusieurs étapes. Tout d’abord, il faut construire le DAL. Cela se fait en exécutant make dans le répertoire myrom/dal. Certains paramètres doivent y être passés. Par exemple, pour construire pour rPI-Pico avec l’écran Waveshare, la commande make BUILD=RP2040_Waveshare suffira. Dans certains cas, il sera nécessaire de modifier le makefile. Pour la construction mentionnée ci-dessus, par exemple, nous ne souhaitons pas utiliser le JIT, préférant l’émulateur à la place. Pour ce faire, il faudra commenter la ligne ENABLE_JIT =yes et décommenter celle qui indique EMU_CORE =asmM0. Cela générera le fichier DAL.prc. L’étape suivante consiste à créer une image ROM complète. Cela se fait depuis le répertoire myrom. Encore une fois, make est utilisé. Les paramètres à spécifier sont le type de construction (qui détermine les paramètres de l’image ROM) et le répertoire des fichiers à inclure dans la ROM. Pour la construction RP2040_Waveshare, la commande appropriée est make RP2040_Waveshare FILESDIR=files_RP2040_Waveshare. Le répertoire de fichiers donné contient déjà d’autres éléments de rePalm, tels que PACE et le panneau de préférences d’informations rePalm.
Construction de PACE
Le patch PACE est un patch binaire appliqué à PACE. Sa construction se fait en plusieurs étapes. Tout d’abord, le patch lui-même est assemblé en exécutant make dans le répertoire myrom/paceM0. Cela produira le patch sous forme de fichier « .bin ». Ensuite, en utilisant l’outil patchpace (que vous devez également construire), vous pouvez appliquer ce patch à un fichier PACE.prc non modifié (une copie de celui-ci peut être trouvée, par exemple, dans le répertoire AximX3). Ce PACE patché peut maintenant remplacer la version d’origine dans le répertoire des fichiers de destination.
Historique des mises à jour de l’article technologique
- La version de l’image ci-dessus a été mise à jour vers v00001 : le JIT est désormais activé (beaucoup plus rapide), l’RTC fonctionne (temps), un bloc-notes a été ajouté, et la réponse tactile a été améliorée.
- La version de l’image ci-dessus a été mise à jour vers v00002 : la zone de graffiti est maintenant dessinée, le graffiti fonctionne, et plus d’applications ont été ajoutées (Bejeweled a été retiré pour des raisons d’espace).
- La version de l’image ci-dessus a été mise à jour vers v00003 : la ROM est maintenant compressée pour permettre l’inclusion de plus de contenus. Cela est acceptable puisque nous la décompressons en RAM de toute façon. Des travaux ont été réalisés sur le support de la carte SD.
- Explication de la traduction des instructions LDM/STM.
- Rédaction d’une section sur le support de la carte SD.
- Rédaction d’une section sur le support du port série.
- Rédaction d’une section sur le support de la vibration et des LED.
- Rédaction de la première partie sur les pilotes NetIF.
- La version de l’image ci-dessus a été mise à jour vers v00004 : certains problèmes de dessin ont été corrigés (soulignement sous le champ de texte du bloc-notes), la LED d’alerte fonctionne maintenant, et la carte SD fonctionne (si vous la connectez à la carte).
- La version de l’image ci-dessus a été mise à jour vers v00005 : un certain support pour les écrans de densité 1.5 fonctionne, donc l’image utilise désormais tout l’écran.
- Rédaction de la section documentaire sur le support des écrans de densité 1.5.
- Rédaction de la section documentaire sur le support DIA et téléchargement de l’image v000006 avec cela.
- Rédaction d’une section sur PACE, téléchargement de l’image v000007 avec une exécution 68k beaucoup plus rapide et quelques corrections DIA.
- Téléchargement de l’image v000008 avec support IrDA.
- Rédaction sur le support audio.
- Rédaction sur reSpring.
- Téléchargement de l’image v000009 avec un support audio préliminaire.
- Téléchargement de l’image v000010 avec un nouveau backend JIT et plusieurs corrections JIT.
- Téléchargement de l’image v000011 avec un backend JIT amélioré et plus de corrections JIT, ainsi qu’un mise à jour basée sur carte SD. Rédaction sur le backend Cortex-M0.
- Rédaction d’une section détaillée sur le matériel reSpring v1 et son état actuel.
- Téléchargement de l’image de découverte STM32F429 v000012 avec des améliorations de vitesse significatives et quelques corrections (graffiti, bloc-notes) ! (cela correspond à rePalm v 1.1.1.8).
- Téléchargement des images STM32F429 et, pour la première fois, des images reSpring pour v 1.3.0.0 avec de nombreuses améliorations de vitesse, rédaction sur le support microphone et le support Zodiac.
- 15 avril 2023 : PACE pour M0, mise à jour du matériel rePalm : MSIO, AximX3, RP2040, nouveaux téléchargements.
- 3 septembre 2023 : Code source publié pour la première fois.