Journal du projet Manette Dreamcast à USB [en cours]

Objectif

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:

Dreamcast Programming: http://mc.pp.se/dc/
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

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.

#circuitCouleur FonctionSignal 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

Requête

Zoom sur requête

Zoom sur requête

Requête et réponse

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

Connecteur ISP

MultiusePCB 2

MultiusePCB 2

Vue d'ensemble

Vue d'ensemble

DB9

DB9



Le DB9-F côté circuit est câblé ainsi:

Côté circuit
BrocheSignal
1+5v
2GND
3PC0
4PC1
Côté Extension
BrocheCouleur
1Brun
2Blanc
3Rouge
4Jaune

Fils pour les tests

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

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

Début de requête

Fin de requête

Fin de requête

Requête complète

Requête complète

Décodage

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

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…

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

Crystal 12Mhz à changer

Changement

Changement

Crystal 16 Mhz

Crystal 16 Mhz

Ça fonctionne!

Ça fonctionne!

Ç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

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

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.

Prochaine étape: Page pour ce projet dans la section électronique avec les schémas, fichiers .hex et code source.


Support de la souris

La souris pour Dreamcast

La souris pour Dreamcast

27 Octobre 2013

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.

Le détail des informations d'état retournées par la souris sont disponibles ici:
http://mc.pp.se/dc/mouse.html.

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

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 normale

Réponse tardive

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-4000

HKT-7600

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!

Comparez vous-même: 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.

Image au démarrage

Image au démarrage

Image pour firmware 1.2

Image pour firmware 1.2