On me l'a demandé plusieurs fois et bien que j'avais hâte de m'y mettre en raison du défi, il m'a fallu du temps pour commencer car
j'avais plusieurs autres projets, tous aussi intéressants. Cette fois, suite à une suggestion et contrairement à mon habitude de
simplement mettre en ligne le projet terminé avec le code et les schémas, j'ai décidé de faire une page dès le début que je
mettrais à jour lorsqu'il y aura du progrès.
Voici les fonctionnalitées que je vise:
Requis: Support d'une manette Dreamcast standard.
Requis: Apparaître en tant que joystick HID standard du côté USB.
Si possible: Supporter d'autres périphériques.
Si possible: Possibilité de communiquer avec un VMU.
Étape 1 : Documents, câblage et vérifications
28 Septembre 2013
Je connaissais depuis longtemps un site de documentation (autant logiciel que matériel) sur la Dreamcast qui
est un bon point de départ:
Ce même site contient même les sources d'un adaptateur USB basé sur le MCU Cypress EZ-USB FX2. Mais
comme je ne veux pas être privé du plaisir de réaliser ce projet, cela ne m'intéresse pas. Et de toute façon, je vais utiliser un AVR.
Alors pour commencer, je voulais avoir une trace de la communication entre ma dreamcast et une manette. Même si le timing est expliqué sur http://mc.pp.se/dc/.
Extension coupée pour accéder aux signaux
J'ai coupé une rallonge en deux pour pouvoir accéder aux signaux facilement. Ensuite, j'ai déterminé le code de couleur utilisé par le câble en me basant sur le
tableau de http://mc.pp.se/dc/controller.html.
#circuit
Couleur
Fonction
Signal AVR choisi
1
Rouge
Data
PC0
2
Brun
+5v
3
Blanc
GND
4
Bleu
SENSE
5
Jaune
Data
PC1
MÉTAL
Noir
Le code de couleur utilisé n'est pas celui de SEGA. Pas surprenant car les rallonges
sont fabriquées par un tier et je ne crois pas qu'il s'agisse d'un produit officiel. Puisque
le code de couleur peut varier, le tableau ici ne devrait jamais être utilisé sans vérification.
Il me restait à installer la Dreamcast et l'oscilloscope, ce que je fit.
Une première chose à confirmer était le voltage. Est-ce vraiment 5 volts? J'ai souvent vu des pages se contredire, alors je fais attention.
Mais l'alimentation est bien sous 5 volt. Toutefois, la communication se fait à 3.3v! Sur ce point, cette page est inexacte. (Citation: «At the beginning of phase 1, pin 1 is always high (+5V), and pin 5 is always low (0V).»)
Requête
Zoom sur requête
Requête et réponse
Afin de respecter les voltages d'alimentation et de communication, je vais utiliser mon circuit multiuse PCB2, en configuration 12 Mhz, MCU alimenté à 3.3v. L'alimentation 5 volt provenant d'USB ira directement à la manette.
J'ai modifié l'emplacement des résistances zéro ohm à l'endos d'un multiuse PCB2, j'ai câblé un connecteur ISP pour la programmation, un câble USB et quelques fils
pour amener les signaux et alimentation pour la manette Dreamcast vers un DB9.
J'ai câblé un DB9 correspondant à ma rallonge Dreamcast de manière à ce qu'elle puisse encore fonctionner comme rallonge, car je vais surement devoir regarder les signaux de près à nouveau. Mon montage sera ainsi facile à isoler.
Connecteur ISP
MultiusePCB 2
Vue d'ensemble
DB9
Le DB9-F côté circuit est câblé ainsi:
Côté circuit
Broche
Signal
1
+5v
2
GND
3
PC0
4
PC1
Côté Extension
Broche
Couleur
1
Brun
2
Blanc
3
Rouge
4
Jaune
Fils pour les tests
J'ai aussi soudé des fils pour pouvoir facilement installer les sondes de l'oscilloscope
sur les signaux de Data de la manette ainsi qu'un fil sur le GND de référence.
J'ai tout branché et la manette semble être alimenté correctement car
l'écran noir d'initialisation du VMU s'affiche et un beep d'une seconde est émit, ce qui
indique je crois que la pile est morte. Exactement comme lorsque j'allume la Dreamcast.
Maintenant, le vrai travail peut commencer!
Prêt pour la programmation
Étape 2 : Transmission de trames
6 Octobre 2013
J'ai commencé à programmer. Première étape, implémenter une routine de transmission en assembleur dont
le chronomètrage est identique ou très près de ce qui est présenté sur mc.pp.se/dc/, mais aussi très près
de ce que j'observe sur le bus physique de ma Dreamcast avec l'oscilloscope.
Le MCU tourne à 12 Mhz, ce qui veut dire que 12 cycles s'exécutent en 1us. Le temps d'exécution de chaque
phase de transmission (voir référence) est de 500ns,
ce qui nous laisse seulement 6 cycles.
Une implémentation naïve ne fonctionne pas. Trop de temps est perdu à «brancher» pour changer l'état
des signaux conditionnellement aux donnés. En plus, il faut surveiller la fin de chaque octet pour
charger une nouvelle valeur du tableau de transmission, ce qui cause un délais supplémentaire
après chaque transmission de 8 bits. Et il faut aussi compter les octets.
Il serait possible de «dérouler» le code pour économiser le temps autrement perdu à faire des vérifications
pour les boucles. Pour tout de même supporter un nombre variable d'octets, il suffirait de dérouler la
boucle pour très grand nombre d'octets. Pour transmettre une quantité donnée, il suffirait alors de faire
un saut à un distance calculée de la fin. Une technique classique.
Mais dérouler des boucles plusieurs dizaines de fois en assembleur est pénible à faire manuellement,
c'est pourquoi je ferais plutôt un script qui s'en occuperait. Mais avant tout, j'aimerais le plus
rapidement possible réussir de la communication avec une manette. Je perdrai du temps sur une
implémentation plus efficace peut-être un autre jour.
J'ai donc fait une implémentation qui prépare les donnés à l'avance. D'une manière gourmande
utilisant 1 octet mémoire pour chaque bit, mais qui réduit la routine de transmission en assembleur
à une simple boucle. À cause de la gestion du compteur, un cycle de trop est utilisé. 83 ns sont donc
perdues à chaque deux bits transmits. Idéalement il faudrait en faire une version déroulée, mais
heureusement la manette pardonne ce léger écart. D'ailleurs, n'est-ce pas le but d'avoir
une protocole synchrone?
// Pas montré
Initialisation du compteur r19
Initialisation des constantes r20 et r21
ld r16, z+ // 2 load phase 1 data
next_byte:
out %0, r20 // 1 initial phase 1 state
out %0, r16 // 1 data
cbi %0, 0 // 1 falling edge on pin 1
ld r16, z+ // 2 load phase 2 data
dec r19 // 1 Decrement counter for brne below
out %0, r21 // 1 initial phase 2 state
out %0, r16 // 1 data
cbi %0, 1 // 1 falling edge on pin 5
ld r16, z+ // 2
brne next_byte // 2
Une fois assez certain de mon «timing», j'ai construit un frame en me fiant à la documentation. La manette
ne répondait pas, alors j'ai réexaminé les signaux à l'oscilloscope, ce qui n'a pas beaucoup aidé.
Début de requête
Fin de requête
Requête complète
Décodage
Mais en relisant la documentation, j'ai fini par comprendre l'ordre des octets. Chaque groupe de 4 doit être inversé.
Donc [ 1 2 3 4 ] [ 5 6 7 8 ] devient [ 4 3 2 1 ] [7 6 5 4 ]. Comme le matériel de la Dreamcast s'occupe de faire les
permutations automatiquement, la documentation est écrite dans l'ordre normal. J'avais mal compris ce détail.
La manette répondais enfin, mais je ne voyais pas les bits bouger lorsque je déplacais les axes. C'est qu'en fait,
je n'envoyait pas la bonne commande. Le code de commande 1 (Request device information) ne retourne pas l'état
de la manette. Selon la documentation, il me faut plutôt la commande 9 (Get condition).
J'ai donc modifié mon programme de test qui transmettait la commande 1 en boucle pour qu'il transmette la commande 9.
Pas de réponse. Je remarque que je dois passer un argument avec la requête pour spécifier que je m'attends
à parler avec une manette. Je change le code. Toujours pas de réponse…
1 bit changeant selon un bouton
J'ai essayé plusieurs choses avant de comprendre que la manette désire être interrogée par la commande 1 (Request device information)
au moins une fois avant d'accepter la commande 9. Lorsque j'ai modifié le code pour envoyer la commande 1 au démarrage pour ensuite
envoyer la commande 9 en boucle, j'ai été heureux de constater dans l'écran de l'oscilloscope que les bits changaient
au gré des mouvements du joystick et selon l'état des boutons.
Maintenant que tout fonctionnait bien, j'en ai profité pour vérifier à quel point la manette tolérerait une transmission
plus lente. J'ai transmit environ 4 fois plus lentement, et ça fonctionnait toujours bien. Il serait donc probablement possible
de transmettre en C ou avec du code assembleur simple! Mais je pense que pour être certain d'avoir une bonne compatiblité avec
éventuellement des manettes n'ayant pas été fabriquées par Sega, il est mieux d'imiter le mieux possible la vitesse
d'une Dreamcast.
En résumé: Succès pour la transmission. Mais comme regarder la réponse dans un écran d'oscilloscope n'est évidemment pas très utile, la prochaine étape sera d'implémenter une routine de réception. J'ai hâte!
Étape 3 : Réception de trames et USB
13 Octobre 2013
Il n'y a pas de port série sur mon circuit multiuse-pcb2,
alors pour afficher les données reçues de la manette, il faut passer par USB. J'ai donc téléchargé la dernière version de
V-USB et mis en place l'environnement habituel pour agir en tant que manette HID standard. Rien de nouveau
pour moi ici, alors après peu de temps, le circuit était détecté par linux:
USB étant réglé, j'ai ensuite mis en place la mécanique de base pour interroger la manette avec «request device information[1]» avant de
me mettre à émettre des «get condition[9]» répétés. Mon premier objectif était alors de retransmettre l'état
d'un bouton au PC. J'étais donc prêt à écrire le code de réception.
D'après les diagrammes de timing de mc.pp.se, les donnés sont
stables pour un minimum de 250ns. À 12MHz, cela représente 3 cycles. 3 cycles est exactement ce qu'il faut pour
lire le port (instruction «in» de 1 cycle) et stocker les donnés (instruction «st», 2 cycles). C'est très serré.
Il est donc hors de question d'essayer de détecter les transitions en temps réel et de compter le nombre de bits
reçus. J'ai donc généré à l'aide d'un script shell une longue séquence assembleur qui lit le port et stocke les donnés
immédiatement dans un tableau de longueur fixe. Ensuite, il suffit d'analyser, en prenant le temps qu'il faut,
les donnés reçues pour détecter le début du paquet, détecter les transition haut->bas d'horloge pour recevoir les
bits, reconstruire les octets, puis détecter la fin du paquet. Voici un extrait du code de réception:
; Z pointe vers le tableau de réception
; PINC est le registre d'entrée
;
in r16, PINC ; 1 cycle
st z+, r16 ; 2 cycles
in r16, PINC ; 1 cyclse
st z+, r16 ; 2 cycles
....
Répété 640 fois.
Malheureusement, cela demande beaucoup de mémoire vive. 640 octets sur seulement 1024. J'ai bien peur qu'il ne sera pas
possible de supporter les cartes mémoires.
Donnés stables pendant seulement 240ns…
J'ai réussi à faire fonctionner la réception, mais il y avait beaucoup d'erreurs. La valeur des axes changait constamment,
les boutons ne conservait pas bien leur état. La raison est que l'échantillonage s'effectuait souvent au mauvais moment.
Par exemple, lorsqu'un échantillonage avait lieu juste avant le front descendant et qu'un niveau haut était enregistré,
ce n'est qu'à l'échantillon suivant, 250ns plus tard, que le front était alors détecté. Mais c'est trop tard, car après 250ns,
le fil représentant notre bit avait souvent commencé à changé. D'ailleur, j'ai remarqué qu'en fait les donnés ne sont stables que
240ns. Démontré dans l'image à droite.
J'ai mis en place la vérification de chaque paquet grace au LRC (un «ou exclusif» de tout les octets) qui
fait partie du protocole du bus maple afin de détecter les erreurs et rejeter les paquets invalides.
Mais comme la majorité des paquets (je dirais 80%) contenaient des erreurs, le joystick et les boutons répondaient
très lentement. Totalement inacceptable pour un jeu.
J'ai donc du me résoudre à augmenter l'horloge à 16 MHz. Chaque cycle ne predrait donc que 62.5ns, plutôt que 83.3ns.
Le temps entre chaque échantillonage n'étant donc maintenant que de 187.5ns, une capture de donnés à un moment
stable était assuré, et effectivement, les erreurs ont cessé et la manette était utilisable!
Crystal 12Mhz à changer
Changement
Crystal 16 Mhz
Ça fonctionne!
Ça fonctionne!
Maintenant, un MCU comme le Atmega168 étant alimenté à 3.3volt ne devrait pas utiliser une horloge de 16 MHz
car cela dépasse la limite documentée comme fiable. Oui, ça fonctionne peut-être, mais comme je n'aime trop pas de faire
ce genre de design, j'ai modifié le circuit pour que le MCU soit alimenté à 5 volt. Pour communiquer à 3.3volt,
Avec résistance 1.5k
l'idée était de contrôler la direction de la broche, alternant entre «Sortie à zéro» et «entrée». Pour transmettre
un niveau élevé quand la broche est en entrée, j'ai installé une résistance de 1.5k vers l'alimentation 3.3v. Le
bus I2C fonctionne ainsi, et j'ai déjà utilisé cette technique pour communiquer avec les manettes de N64 et Gamecube.
Malheureusement, nous n'avons pas un simple bus I2C à 100kHz dont les signaux ne parcourent qu'une faible distance.
La communication avec la manette de Dreamcast est environ 10 fois plus rapide et c'est un problème. Le voltage monte
beaucoup trop lentement. En fait, il n'a même pas fini de monter (~1.8v) que le signal se fait déjà tirer à zéro.
C'est sans doute à cause de la capacitance du câble et probablement aussi à cause du circuit dans la manette (filtres
probables). Nous avons donc avec la résistance de 1.5k un circuit RC. Diminiuer la résistance accélère les montées,
mais je n'ai pas réussi à obtenir une rapidité satisfaisante. J'ai atteint une limite où c'est le micro-contrôleur
qui n'arrive plus à tirer la ligne à zéro (car évidemment le courant augmente lorsqu'on diminue la résistance). L'image
suivante démontre ce problème:
Résistance trop petite
J'ai également tenté de diminuer la cadence de transmission, mais la manette ne semblait pas répondre. L'interrogation
était peut-être rendue trop lente. Et même si cela avait fonctionné, il me semble qu'en tant que solution, c'est
remplacer un risque (l'«overclocking») par une autre (incompatiblité avec certaines manettes?).
En résumé, l'adaptateur est maintenant fonctionnel. Toutefois, le MCU est légèrement surcadencé. À première vue,
il n'y a pas d'effets néfastes mais des tests s'imposent. Alors contactez-moi si vous
souhaiteriez m'aider à tester. Mais cela va toujours m'inquiéter un peu, alors si la
demande le permet, je ferai un nouveau circuit muni d'un convertisseur de niveaux, par exemple le SN74AUC2G07.
Ce week-end j'en ai profité pour tenter de faire fonctionner la souris pour Dremcast que j'ai reçue en cours de
semaine. Électriquement c'est la même chose qu'une manette alors il n'y avait que le logiciel à mettre au point.
La souris est interrogée par la commande « Request device information [0x01] » et retourne une structure qui
permet d'identifier qu'il s'agit d'une souris (Le premier mot 32 bit contient la valeur 0x200). Ensuite,
la commande « Get condition [0x09] » retourne l'état des boutons et le mouvement depuis la dernière interrogation.
J'ignore ce que Sega prévoyait, mais la structure de donnés supporte 32 boutons et 8 axes. Et chaque axe est sur 16 bits.
Cela fait beaucoup d'information. Trop d'information pour pouvoir en recevoir la totalité avec mon approche
de réception qui utilise beaucoup de mémoire. De plus, après un certain nombre d'octets, il y une pause
dans la communication qui empire la situtation:
Dans l'image de gauche, la trace du haut représente un des fils de donnés du bus. On aperçoit d'abord la requête
d'information (« Get condition [0x09] ») provenant de l'adaptateur. Arrive ensuite la réponse de la souris,
en deux blocs.
La trace du bas représente l'état d'une broche libre que j'utilise pour déboguer. Une première impulsion marque
le début de l'exécution du code de réception et une deuxième sa fin. On constate que la réception prends
fin beaucoup trop tôt.
J'ai voulu allonger le code de réception, mais ce micro-contrôleur ne dispose pas d'assez de mémoire pour
emmagasiner les donnés supplémentaire que cela engendrerait. Mais j'ai quand même réussi à faire fonctionner
la souris. Mais ce n'est pas parfait: La roulette ne fonctionne pas car les donnés sur ses mouvements
est transmise dans le deuxième bloc. Aussi, il n'est pas possible de vérifier le LRC du paquet car
celui-ci arrive à la toute fin. Si une erreur de communication survient, des donnés erronées
seront utilisés.
Conclusion: La souris ne sera supportée que partiellement par l'adaptateur tant qu'il sera
basé sur mon circuit multiuse-pcb2.
Support d'une manette non-sega
Performance P-20-007
2 Novembre 2013
J'ai retrouvé une manette pour Dreamcast fabriquée par Performance. Je tout de suite essayée mais elle ne fonctionnait pas... J'ai donc
immédiatement regardé les signaux avec l'oscilloscope. La manette répondait, mais beaucoup plus lentement qu'une manette Sega. 56 μS vs 159 μS:
Réponse normale
Réponse tardive
Mon code de réception n'attendait tout simplement pas assez longtemps pour la réponse. Il n'a suffit que d'une légère modification
pour rendre la manette fonctionelle.
Cette amélioration est dans la version 1.1.1 du logiciel.
Support du clavier
HKT-4000
HKT-7600
3 Novembre 2013
Supporter le clavier n'a vraiment pas été difficile. L'infrastructure de détection via la commande
« Request device information [0x01] » était déjà en place depuis que la souris était supportée.
Je pensais devoir faire une table de conversion pour traduire les codes de touches du clavier
Dreamcast pour des codes standard pour PC, mais non, les codes utilisés sont les mêmes!
Les claviers testés sont les modèles HKT-4000 et HKT-7600 fabriqués par Sega. Le support pour clavier est dans la version 1.2.
Les claviers que j'ai pu tester sont des claviers Japonais alors il faut
configurer le clavier comme tel. Autrement, plusieurs touches, notamment
les symbols près du retour de chariot ne correspondront pas. Voici quelques
«screenshots» sous win7:
Support du LCD
Je trouvais triste que l'écran du VMU reste vierge alors j'ai fait en sorte
qu'au moins une image s'affiche.
Permettez-moi d'insister sur le fait que cette fonctionnalité ne fait qu'afficher
une image faisant partie du «firmware». L'image ne peut pas être changée,
sans recompiler le projet. L'affichage n'est pas contrôlé par USB. En d'autres
mots, un émulateur ne pourriat pas contrôler le contenu du LCD.
Il faut transmettre 192 octets d'un seul coup alors utiliser ma routine de
transmission rapide (mais gourmande en mémoire) n'était pas possible. Mais
comme il s'agit de communication synchrone, j'ai implémenté une routine en C.
Elle est évidemment plus lente, mais ce n'est pas grave. (Notez que je continue
d'utiliser la routine assembleur partout ailleurs).
L'image affichée par défaut sera changée à chaque version.