— Basé sur les articles publiés dans Quasar CPC numéro 16, numéro 17 et numéro 18, Assembleur : Coding, par Zik.
Derrière ce doux nom se cache le réseau CPC. Il a été conçu en 1996 par Hage et Steve du groupe allemand Wizcat dont vous trouverez le site web dédié au réseau ici. Sur ce site sont disponibles les jeux et utilitaires développés par Wizcat.
Cet article a pour but principal d'encourager les CPCistes à créer des programmes l'exploitant. En effet, le réseau est un moyen formidable de redonner un coup de jeune à quantité de vieux jeux qui passent aujourd'hui pour des dinosaures. Pourquoi ne pas en créer des versions en réseau ?!
Vous trouverez le schéma électrique du réseau ici ; il est fort simple et est implémenté sur la Soundplayer+. La ligne du réseau comporte un fil de masse et un fil de données binaires. Donc la ligne peut prendre deux états (0 ou 1) et cet état est évidemment le même pour tous les CPCs connectés. Il peut être consulté via le bit 6 du port PPI &F500
(signal Busy
de l'imprimante). Par ailleurs, chaque CPC a la possibilité de tenter d'imposer un état à la ligne par l'intermédiaire du bit 7 du port &EF00
(signal Strobe
de l'imprimante). Voilà quelques exemples :
Consulter l'état de la ligne :
En BASIC IF INP(&F500) AND 64=1 THEN PRINT"1" ELSE PRONT"0" |
En assembleur ld b,&f5 in a,(c) bit 6,a jr nz,Un jr Zero |
Imposer un 1 :
En BASIC OUT &EF00,128 |
En assembleur ld bc,&ef80 out (c),c |
Imposer un 0 :
En BASIC OUT &EF00,0 |
En assembleur ld bc,&ef00 out (c),c |
Donc, l'état de repos de la ligne est 1. Tout programme qui utilise le réseau doit mettre la ligne à 1 le plus tôt possible, d'autant plus qu'au reset le CPC place la ligne à 0, ce qui empêche toute transmission sur le réseau pour tous les ordinateurs. L'interrupteur présent sur l'interface permet d'isoler l'ordinateur du réseau. Pour se joindre à un programme en cours, il faut fermer l'interrupteur après avoir lancé le programme ; pour quitter une application, il faut ouvrir l'interrupteur avant de faire un reset.
Je vous propose maintenant de voir comment peut s'établir le dialogue sur ce type de réseau. En fait, nous allons regarder comment procèdent les routines déjà écrites par Steve et Hage. Ces routines sont regroupées dans un pack, elles permettent une transmission à 60000 Baud nous dit-on.
Le réseau comporte une seule ligne bidirectionnelle sur laquelle nous voulons envoyer les bits les uns après les autres. On a choisi de commencer par envoyer le bit de poids faible. Il faut par ailleurs qu'un seul CPC à la fois s'exprime sur la ligne. D'où la nécessité de vérifier avant chaque envoi si la ligne est libre, c'est-à-dire qu'elle est à 1.
Les schémas ci-contre vous montrent comment se déroulent l'envoi et la réception d'un octet. Lorsqu'un CPC veut envoyer, après s'être assuré que la ligne est libre (ce test n'est pas inclu dans la routine vnsnd), il place la ligne à 0 pendant un temps suffisant pour que les autres CPC aient le temps de se préparer à une réception. Il passe ensuite la ligne à 1 pendant un temps fixé à 20µs. C'est sur ce front montant que se synchronisent les autres CPCs (en réception). Vient ensuite la succession des bits dont l'état est maintenu 12µs (10µs pour le bit 7 !). La ligne est ensuite remise à 1. |
Les routines du pack n'effectuent pas de contrôle sur les données, il n'y a ni parité ni checksum systématiques. C'est à vous de les gérer si vous en ressentez la nécessité mais je peux vous assurer qu'à priori il y a très peu d'erreurs introduites par la ligne. Pensez à interdire les interruptions pour tout transfert !
Vous êtes maintenant motivés pour programmer votre propre application réseau ! Très bien, mais il faut d'abord voir comment organiser le dialogue. Il existe deux techniques qui sont les communications asynchrones ou synchrones.
Je vais passer rapidement sur le fonctionnement en asynchrone, non pas que ce mode soit moins intéressant, mais il est moins exploitable et moins courant avec le réseau dont nous disposons. En effet, dans ce mode, une transmission a lieu dès qu'un événement défini se produit. Par conséquent, si rien ne se passe, rien n'est envoyé. L'exemple type d'une communication asynchrone est un programme de dialogue en temps réel (dialog, IRC, talk, etc.). Chaque fois que l'on appuie sur une touche, son code est envoyé sur le réseau. Donc, tous les ordinateurs doivent continuellement vérifier l'état de la ligne pour savoir s'il y a quelque chose à recevoir. C'est possible assez faiclement pour un programme de dialogue, mais pour un jeu c'est plutôt inadapté.
D'un point de vu plus concrêt, la ligne est au niveau 1 quand rien ne se passe. Lorsqu'un CPC veut émettre il passe la ligne à 0 pendant suffisamment longtemps pour que tous les ses collègues l'aient remarqué. Ce temps doit couvrir le cas ou un des CPC serait en train de faire le traitement le plus long en durée qui existe dans le programme entre deux consultations de la ligne. Puis le CPC émetteur peut envoyer ses données en étant sûr que tout le monde l'écoute.
Voyons le mode synchrone. C'est du genre : on envoie chacun notre paquet de données puis on se retrouve plus tard pour remettre ça. Comment peut-on faire pour synchroniser tous les CPCs puisqu'ils ne mettront pas tous le même temps pour traiter les données ? Cette fois la ligne est à 0 pendant le traitement. Quand un CPC a fini de triturer ses données et qu'il est prêt à communiquer, il envoie 1 sur la ligne et attend que la ligne passe effectivement à 1. En effet, cela n'aura lieu que quand tous les CPCs en seront à ce point. Commence alors une phase de transmission de données.
Chacun doit parler à ton tour, dans un ordre déterminé par le numéro d'ordinateur (ex : joueur 1, 2, …). Ce numéro aura été attribué en début de programme. La méthode aura été attribuée en début de programme. La méthode proposée (pour un jeu) par Steve est la suivante. Chaque CPC tire aléatoirement un nombre. La ligne est à 1 et rien ne se passe jusqu'à ce qu'un des joueurs démarre une partie. Alors, chaque CPC décompte son nombre en surveillant l'état de la ligne. Quand un CPC a terminé son décompte, il envoie ses informations (nom, points, etc.). Tous les CPCs envoient donc leurs données les uns après les autres, et à chaque fois un numéro de joueur est attribué. cette phase d'initialisation est quittée lorsque le temps maximum est écoulé.
Alors que dans d'autres rubriques OffseT met souvent l'accent sur l'optimisation en vitesse (comment donc “en vitesse”, serait-ce une tendance à bacler ses articles qui se dévoilent ?), je vais pour ma part continuer sur le réseau CPC. Et là, même si on essaie d'aller le plus vite possible dans les transferts, il faut aussi savoir s'arrêter (souvent un bon moment) pour attendre les copains avec qui on tente de communiquer. Car le maître mot est ici la synchronisation ! Croyez-moi, vous allez en entendre parler dans cet article.
Sur ce, jetons-nous à l'eau…
Je vous propose un source assembleur commenté qui constitue le squelette convenable pour bon nombre de jeux exploitant le réseau. Il contient tout le protocole de base pour un jeu à deux joueurs ou plus, et il utilise le pack de vecteurs dont je vous ai parlé dans la section précedente.
L'organisation du programme qui suit est la même que celle du désormais célèbre Shoulder Dash qui nous a déjà prouvé son efficacité et sa fiabilité assez lointaine de celle de Last Action Point.
Je vous rappelle les principes de base : la ligne passe à 0 si au moins un CPC la force à 0, elle ne passe à 1 que si tous les CPC appliquent un 1. Donc, si on veut synchroniser tous les CPC après un travail, on place la ligne à 0 pendant celui-ci puis, en fin de tâche, chaque CPC écrit 1 et attend ensuite que la ligne soit à 1. L'attente peut se faire grâce aux routines “vnOne” ou “vnOnei”. La seconde scrute sans timeout contrairement à la première, c'est-à-dire qu'elle attend indéfiniment d'avoir un 1 alors que l'autre sort de la boucle au bout d'un certain temps (65536 tentatives dans le cas présent) en signalant une erreur (un timeout) par la Carry à 1. Il faut se méfier des routines sans timeout qui peuvent bloquer l'ordinateur dans une boucle infinie si la condition de sortie n'arrive jamais.
Donc, quand les CPC calculent ou affichent, etc…. la ligne est à 0. Au contraire, pendant l'attente d'un début de partie, la ligne est à 1 ; ainsi un CPC seul peut donner le départ en inscrivant 0. C'est l'unique occasion où la ligne reste à 1 pendant un temps relativement long. En effet, lors d'une transmission de données, la ligne fluctue. donc, quand il y a eu un problème et que l'on veut attendre la partie suivante, on cherche en fait un 1 stable. C'est ce que réalise la routine “PartieEnCours” ; elle attend jusqu'à obtenir un timeout lors d'une recherche de 0 (“vnZero”).
Une fois synchronisés, on peut échanger les données du jeu. Dans le programme d'exemple on communique des blocs de données. Je n'ai pas utilisé pour cela les routines de Wizcat mais des versions modifiées. Les routines originales envoient en tête la longueur du bloc alors que dans celles que je propose, le récepteur connait d'avance le nombre d'octets qu'on va lui envoyer. Ceci parce que toutes les longueurs des blocs sont fixées dans l'exemple. Il me paraît plus sûr de faire avec ces routines là quand c'est possible car envoyer la taille peut-être “dangereux” s'il y a une erreur de transmission.
Dans le programme proposé, on envoie chronologiquement un bloc qui donner les infos sur la partie qui va débuter (le nom de la carte par exemple, pour faire le parallèle avec Shoulder Dash), puis chaque CPC transmet un bloc d'informations sur son joueur (nom, scores, etc.) ; on entre ensuite dans la boucle du jeu où tous les joueurs donnent régulièrement des informations sur leur état (mort ou vivant, état du clavier, position sur la carte, etc.). Oui, on envoie directement l'état du clavier car dans cette organisation de programme, chaque CPC fait les calculs pour tous les joueurs. Le programme qui suit échange effectivement toutes ces données, mais seules les infos sur l'état des joueurs sont affichées, ceci pour prouver que ça marche !
Tant que je parle des blocs, voici des éclaircissements sur certains labels du programme :
Je dois vous faire remarquer que lorsqu'un joueur est mort, il faut que le CPC qui correspond continue à envoyer son état (qui peut dire “je suis mort, les octets qui suivent n'ont aucun intérêt !”). Sinon, les autres CPC vont attendre qu'il parle son tour venu et faire un timeout. C'est d'ailleurs ce qui arrive dans le programme d'exemple quand on appuie sur ESPACE ou RETURN, justement parce que ce n'est qu'un exemple !
Le programme contient beaucoup de temporisations avec des valeurs empiriques (ce sont les mêmes que Shoulder Dash). La plupart dépendent en fait du temps machine de la boucle qui les précède. Vous pourrez être amenés à les modifier dans votre jeu !
La routine de choix de nombre pseudo-aléatoires de l'exemple est très basique, une plus élaborée est disponible dans Shoulder Dash.
Les notes suivantes détaillent certains points de fonctionnement crutiaux du programme :
Télécharger le listing au format Maxam 1.5.
; Structure de programme de jeu pour ; le réseau Virtual Net de Wizcat ; Par Zik pour Quasar CPC 17 ; Automne 1999 ; Org &8000 Nolist LgInfJ Equ 4 LgEtatJ Equ 4 LgPartieInf Equ 4 call One ; <di, mise en place int, ei> call alea ; <Saisie nom> ld hl,mes1 call mesg ; Vérifie que le 1 est stable ; (= pas de partie en cours) Verif call vnZero jp nc,PartieEnCours ; Timeout ld hl,mes2 call mesg ; Attente du début d'une nouvelle ; partie déclenchée par l'appui sur ; une touche d'un des CPC du réseau AttendDebPartie call tstline jr z,Cpasmoi ; Attend une touche halt call &bb09 jr nc,attenddebpartie ; touche appuyée jr Cmoi ; Une partie est déjà en cours, ; il faut attendre qu'elle finisse PartieEnCours ld hl,mes4 call mesg ; attend d'avoir un 1 stable Attend0 call vnZero jr nc,attend0 jr verif ; ** On fait démarrer une partie ** Cmoi call tstline ; cf. note (1) jr z,PartieEnCours ; force la ligne à 0 pour indiquer ; qu'on commence une partie call Zero ld hl,mes5 call mesg ; <prépare les infos sur la partie> ; Attend que tout le monde soit pret ld hl,3000 call vnWait di call One ; pour synchroniser ld hl,30 call vnWait ; attente ; Envoi des caractéristiques ; de la partie à venir ld hl,partieinf ld de,lgpartieinf call Sndb jp commun ; ** On participe à une partie ** ; ** lancée par un autre ** Cpasmoi ld hl,mes5 call mesg di call vnOne ; synchronisation jp c,erreur ; cf. note (2) ; Reçoit le bloc d'info sur la ; partie à venir ld hl,partieinf ld de,lgpartieinf call Recb jp c,erreur ; si timeout Commun call Zero ; Si les données ne conviennent pas ; call One ; ei ; jp attenddebpartie ; Les données nous conviennent... ; <Prépare la carte du niveau> ; <choisit une position de départ> ld hl,mes6 call mesg di call One call vnOne jp c,erreur ; si timeout ; Il faut maintenant échanger les ; données des joueurs (nom, position ; de départ, ...) et en déduire le ; nombre de joueurs et le numéro de ; notre joueur ld de,(ID) ld hl,infos ld (ptr),hl xor a ld (NbrJ),a ld hl,&5f3f Dial call tstline jr z,Ecoute dec hl ld a,h or l jp z,FinDial dec de ld a,d or e jr nz,Dial ; À nous de parler (DE est à 0) Parle push hl call Zero ld hl,50 call vnWait call One ld hl,30 call vnWait ; attente ; Envoi des caractéristiques du ; joueur (nom, pos de départ, ...) ld hl,infosJ ld de,lginfJ call Sndb call Zero ; fini de parler ; copie des infos en mémoire ld hl,infosJ ld de,(ptr) ld bc,lginfJ ldir ; mise à jour du pointeur ex de,hl ld de,lgetatJ add hl,de ld (ptr),hl ld a,(nbrJ) ld (noJ),a inc a ld (nbrJ),a call One ; libère la ligne call vnOne pop hl jp c,erreur ; si timeout ld de,&ffff jp dial ; Un autre joueur envoie ses données Ecoute push hl push de call vnOne jp c,erreur ; si timeout ; Reçoit le bloc d'info du joueur ld hl,(ptr) ld de,lginfJ call Recb jp c,erreur ; si timeout call Zero ; Incrémente le pointeur ld hl,(ptr) ld de,lginfJ+lgetatJ add hl,de ld (ptr),hl ld hl,nbrj inc (hl) call One call vnOne pop de pop hl jp c,erreur jp dial ; Tous les joueurs ont parlé FinDial ld hl,&200 call vnWait call Zero ei ld hl,mes7 call mesg ld a,(nbrj) call affnbr ld a,(noj) call affnbr ; <Calculs initiaux> ; <affichage du décors> di call One call vnOnei ; sans timeout call zero ei ; *** Boucle du jeu *** Boucle ld hl,mes8 call mesg di call One ; synchronisation call vnOne jp c,erreur ; si timeout ; tout le monde est au meme point ; Echange d'infos sur les joueurs ; (état clavier/joystick, position ; dans le niveau,...) ld hl,infos+lginfJ ld (ptr),hl ld a,(nbrJ) ld b,a ld c,0 ; no du joueur Jsuiv push bc ld a,(noJ) cp c jr nz,ecouteJ ParleJ ld hl,(ptr) ld de,lgetatJ call Sndb jr nextJ EcouteJ ld hl,(ptr) ld de,lgetatJ call Recb jp c,erreur_ ; si timeout NextJ ld hl,(ptr) ld bc,lginfJ+lgetatJ add hl,bc ld (ptr),hl pop bc inc c djnz Jsuiv ; Tous les joueurs ont parlé ld hl,10 call vnWait call Zero ei ; <traiter les données> ; <affichages> ; <test clavier> ; si partie terminée -> jp finpartie ; sinon -> jp boucle ; -- la suite est pour les tests -- ; affiche les états des joueurs ld a,(nbrJ) ld b,a ld hl,infos ld c,0 Aff_B push bc ld de,lginfJ add hl,de ld de,mes_etat4 ld bc,lgetatJ ldir pop bc ld a,c add a,&30 ld (mes_etat3+7),a push bc push hl ld hl,mes_etat3 call mesg pop hl pop bc inc c djnz aff_b ld hl,10000 call vnWait ; longue attente ; place des infos aléatoires dans ; l'état de notre joueur ld a,(noJ) call calcadr ld de,lginfJ add hl,de ld de,mes_etat2 ld b,lgetatJ pokeJ ld a,r xor b ; pour brasser un peu and &3f add a,&30 ld (hl),a ld (de),a inc hl inc de djnz pokeJ ; affiche l'état choisi ld hl,mes_etat1 call mesg ; test clavier halt call &bb09 jp nc,boucle cp &20 jp z,finpartie cp 13 jp z,finprg jp boucle ; Quand une partie se termine pour ; notre joueur il faut attendre la ; fin de la partie en cours Finpartie call One ; attend d'avoir un 1 stable Attend1 call vnZero jr nc,attend1 ; <afficher le nom du gagnant, ...> ld hl,mes9 call mesg jp verif Finprg call One ret ; Data Error ! Erreur_ pop hl Erreur call One call alea ; cf. note (3) ld hl,mes3 call mesg jp partieencours ; Choisit aléatoirement un nombre ; 16 bits de 4000 à 20383 ; identifiant le joueur Alea ld a,r and &3f ld h,a ld a,r ld l,a ld bc,4000 add hl,bc ld (ID),hl ret ; Teste l'état de la ligne ; Out ; Z si ligne à 0 ; ; NZ si ligne à 1 TstLine ld b,&f5 in b,(c) bit 6,b ret ; Passe la ligne à 1 One ld bc,&ef80 out (c),c ret ; Force la ligne à 0 (prioritaire) Zero ld bc,&ef00 out (c),c ret Ptr dw 0 ; variable temporaire ID dw 0 ; nb identifiant le joueur NbrJ db 0 ; nombre de joueurs NoJ db 0 ; notre numéro de joueur ; Routine; 'sndb', transmit a block ; ---------------------------------- ; In ; HL=Pointer to the data block ; DE=Length of the data block ; Out ; A, BC, DE, HL destroyed ; Sndb push de ld a,(hl) inc hl call vnsnd pop de dec de ld a,d or e jr nz,sndb ret ; Routine; 'recb', receive a block ; ---------------------------------- ; In ; HL=Pointer to a buffer ; DE=Length of the data block ; Out ; CY=0 --> Ok ; CY=1 --> Timeout error ; A, BC, DE, HL destroyed ; Recb push de call vnrec jr c,rec1 ld (hl),a inc hl pop de dec de ld a,d or e jr nz,recb or a ret Rec1 pop de ret ; Renvoie dans HL l'adresse des ; infos du joueur numéro A CalcAdr ld de,taille call mult ex de,hl ld hl,infos add hl,de ret ; HL=DE*A Mult ld b,8 ld hl,0 Mulbouc rra jr nc,mulsaut add hl,de Mulsaut sla e rl d djnz mulbouc ret ; Affiche un message à l'ecran ; avec retour à la ligne Mesg ld a,(hl) cp &ff jp z,fintxt cp 32 jr c,CtrlCh push hl call &bb5a pop hl CtrlCh inc hl jr mesg Fintxt ld a,13 call &bb5a ld a,10 call &bb5a ret ; Affiche un chiffre de 0 à 9 ; avec retour à la ligne Affnbr add a,&30 call &bb5a jp fintxt Mes1 db "Saisie du nom",255 Mes2 db "Appuyez sur une touche pour demarrer une partie",255 Mes3 db "Data error !",255 Mes4 db "Jeu deja en cours !",255 Mes5 db "Preparation du jeu",255 Mes6 db "Echange des donnees des joueurs",255 Mes7 db "Debut de la partie",255 Mes8 db "Echange des etats des joueurs",255 Mes9 db "Fin de la partie",255 Mes_etat1 db "Mon nouvel etat : " Mes_etat2 ds lgetatJ:db 255 Mes_etat3 db "Joueur x " Mes_etat4 ds lgetatJ:db 255 read"vn96rout.asm" ; Pour Maxam PartieInf ds lgpartieinf InfosJ ds lginfJ Taille Equ lginfJ+lgetatJ NbmaxJ Equ 4 ; non traite ds le prg Infos ds nbmaxJ*taille
Télécharger le listing de la bibliothèque VN96 au format Maxam 1.5.
; ; Virtual Net 96 Routinenpack, 1996 by Hage & Steve of Wizcat ;_____________________________________________________________ ; IMPORTANT; All routines do not change the interrupt state. But if a routine ; is executed the interrupts MUST be disabled (DI). ; vnsnd Transmit a byte ; vnrec Receive a byte ; vnsndb Transmit a data block ; vnrecb Receive a data block ; vnzero Wait for zero ; vnone Wait for one ; vnzeri Wait for zero without timeout ; vnonei Wait for one without timeout ; vnwait Wait a determined time ; vnwais Wait for a request routine ; Routine; 'vnsndb', transmit a data block ; ---------------------------------------------- ; Jump in; HL=Pointer to the data block ; DE=Length of the data block in bytes ; Jump out; A, BC, DE, HL destroyed ; ; Comments; Data block is immediately transmitted without checking the ; wire. ; -------------------------------------------------------------------------- vnsndb: push de ; Laenge senden ld a,e call vnsnd pop de push de ld a,d call vnsnd pop de ; vnsnd1: push de ; Block senden ld a,(hl) inc hl call vnsnd pop de dec de ld a,d or e jr nz,vnsnd1 ret ; Routine; 'vnrecb', receive a data block ; --------------------------------------------- ; Jump in; HL=Pointer to a buffer ; Jump out; CY=0 --> Ok, (HL)=data block ; DE=length ot the data block in bytes ; CY=1 --> Timeout error ; A, BC, DE, HL destroyed ; ; Comments; If there's no data block received within ca. 1s the routine ; returns with CY=1. ; -------------------------------------------------------------------------- vnrecb: call vnrec ; Laenge empfangen ret c ld (vnrec1+1),a call vnrec ret c ld (vnrec1+2),a ; vnrec1: ld de,0 ; Block empfangen vnrec2: push de call vnrec jr c,vnrec3 ld (hl),a inc hl pop de dec de ld a,d or e jr nz,vnrec2 ld de,(vnrec1+1) or a ret vnrec3: pop de ret ; Routine; 'vnsnd', transmit a byte ; --------------------------------------- ; Jump in; A=Byte to transmit ; Jump out; A, BC destroyed ; ; Comments; Byte is immediately transmitted without checking the wire. ; -------------------------------------------------------------------------- vnsnd: ld c,a ; Startbit senden ld b,&EF xor a out (c),a ds 18 ld a,128 out (c),a ; rr c:rra ; Byte senden ds 13 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a rr c:rra ds 5 out (c),a ; ld a,128 ds 4 out (c),a ret ; Routine; 'vnrec', receive a byte ; -------------------------------------- ; Jump in; - ; Jump out; CY=0 --> Ok, A=received byte ; CY=1 --> Timeout error ; A, BC, DE destroyed ; ; Comments; If there's no byte received within ca. 1s the routine ; returns with CY=1. ; -------------------------------------------------------------------------- vnrec: ld b,&F5 ; Startbit empfangen ld de,0 vnemp1: in a,(c) cp 64 jr c,vnemp2 dec de ld a,d or e jr nz,vnemp1 scf ret ; vnemp2: in a,(c) cp 64 jr c,vnemp2 ; ds 12 ; Byte empfangen in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ds 4 in a,(c) rla:rla:rr c ; ld a,c or a ret ; Routine; 'vnwait', wait a determined time ; ----------------------------------------------- ; Jump in; HL=Wait time in 7.63us steps ; Jump out; A, HL destroyed ; ; Comments; Waits HL*7.63 micro seconds. ; Rule of thumb; 9 HL cycles are one raster line. ; -------------------------------------------------------------------------- vnwait: dec hl ld a,h or l jr nz,vnwait ret ; Routine; 'vnwais', wait for a request routine ; --------------------------------------------------- ; Jump in; - ; Jump out; - ; ; Comments; Waits the time the routines vnzero, vnone, vnzeri and ; vnonei need to notice a change of the data state. ; -------------------------------------------------------------------------- vnwais: ds 60 ret ; Routine; 'vnzero', wait for zero state ; -------------------------------------------- ; Jump in; - ; Jump out; CY=0 --> Data wire is zero ; CY=1 --> Timeout error ; A, B, DE destroyed ; ; Comments; If the data wire doesn't switch to zero state within ca. 1s ; the routine returns with CY=1. ; -------------------------------------------------------------------------- vnzero: ld b,&F5 ld de,0 vnzer1: in a,(c) cp 64 jr c,vnzer2 dec de ld a,d or e jr nz,vnzer1 scf ret vnzer2: or a ret ; Routine; 'vnone', wait for one state ; ------------------------------------------ ; Jump in; - ; Jump out; CY=0 --> Data wire is one ; CY=1 --> Timeout error ; A, B, DE destroyed ; ; Comments; If the data wire doesn't switch to one state within ca. 1s ; the routine returns with CY=1. ; -------------------------------------------------------------------------- vnone: ld b,&F5 ld de,0 vnone1: in a,(c) cp 64 ret nc dec de ld a,d or e jr nz,vnone1 scf ret ; Routine; 'vnzeri', wait for zero state without timeout ; ------------------------------------------------------------ ; Jump in; - ; Jump out; Data wire is zero ; A, B, DE destroyed ; ; Comments; This routine returns only if the data wire is in zero ; state. ; -------------------------------------------------------------------------- vnzeri: ld b,&F5 vnzei1: in a,(c) cp 64 jr nc,vnzei1 ret ; Routine; 'vnonei', wait for one state without timeout ; ----------------------------------------------------------- ; Jump in; - ; Jump out; Data wire is one ; A, B, DE destroyed ; ; Comments; This routine returns only if the data wire is in one state. ; -------------------------------------------------------------------------- vnonei: ld b,&F5 vnoni1: in a,(c) cp 64 jr c,vnoni1 ret
Après avoir étudié le protocole par défaut proposé par Wizcat1), je vous en propose un d'un tout autre type. Celui-ci n'a pas été testé faute de temps et de matériel, mais sa simulation sous BASIC fonctionne parfaitement !
Pourquoi donc chercher un autre genre de protocole alors que celui déjà présenté ci-dessus ravit déjà un grand nombre de CPCistes lors des meetings ? Tout simplement parce-que celui-ci a au moins un défaut : il monopolise totalement le Z80 lors des longues périodes de synchronisation. Ceci empêche notamment l'usage de techniques sympathiques comme les rasters ou les changements de mode, et gêne l'émission de sons. Bref, on est coincé dès lors que l'on veut exécuter une routine à une fréquence régulière.
L'idée est donc de garder les interruptions en permanence… et la communication se fera entièrement sous interruption. Un inconvénient de cette façon de procéder est l'augmentation non négligeable du temps de communication étant donné la fréquence faible des interruptions du CPC (300Hz). De plus, ce temps grandit avec le nombre de CPC qui échangent des données ; c'est-à-dire avec le nombre de joueurs.
Voici la première méthode sur ce principe qui vient à l'esprit :
|
Ceci dit, la séquence telle qu'elle ne marche pas ! De temps en temps, on observe une erreur de transmission. Elle est due à une lecture prématurée de la ligne alors que l'écriture n'est pas terminée ou à une libération de la ligne alors que la lecture n'est pas finie.
Voici un programme de simulation en BASIC qui met en évidence le problème.
10 ' Simulation du premier protocole 20 ' pour le réseau Virtual Net 30 ' présenté dans la rubrique coding 40 ' de Quasar CPC 18 50 ' 60 ' On observe des erreurs de transmission 70 ' 80 DEFINT a-z 90 MODE 2 100 ' FNl est l'{tat de la ligne 110 ' o(1), o(2) et o(3) sont les états des 3 cartes réseau 120 DEF FNl=o(1) AND o(2) AND o(3) 130 ' 140 ' Passage de la ligne à son état de repos 150 FOR n=1 TO 3:o(n)=1:NEXT 160 ' 170 code=&D6 :' code échangé (le même pour les trois ordinateurs) 180 ' Initialisations 190 'n? numéro de l'émetteur en cours (1 à 3) 200 'b? numéro du prochain bit à envoyer (0 à 7) 210 n1=1:n2=1:n3=1 ' c'est l'ordinateur 1 qui va émettre en premier 220 b1=0:b2=0:b3=0 ' on commence par le bit 0 230 lib1=0:lib2=0:lib3=0 240 ' 250 EVERY 30,3 GOSUB 300 260 FOR x=1 TO 400:NEXT:EVERY 30,2 GOSUB 480 270 FOR x=1 TO 400:NEXT:EVERY 30,1 GOSUB 660 280 GOTO 280 290 ' 300 ' ** ordinateur 1 ** 310 PRINT "n1=";n1 ' Affichage du numéro de l'ordinateur émetteur 320 IF n1=1 THEN GOTO 400 330 ' lecture 340 IF lib1=1 THEN lib1=0:o(1)=1:i1=0:RETURN ' liberation 350 ' Réception 360 i1=i1+FNl*2^b1 370 b1=(b1+1) AND 7 380 IF b1=0 THEN n1=(n1 MOD 3)+1:PRINT "reçu par 1 :";HEX$(i1):lib1=1:RETURN ' fin de réception 390 RETURN 400 ' Émission 410 o(1)=-((code AND 2^b1)<>0) 420 b1=(b1+1) AND 7 430 IF b1<>0 THEN RETURN 440 n1=(n1 MOD 3)+1 450 lib1=1 ' il faudra faire la libération au prochain coup 460 RETURN 470 ' 480 ** ordinateur 2 ** 490 PRINT"2" 500 IF n2=2 THEN GOTO 580 510 ' lecture 520 IF lib2=1 THEN lib2=0:o(2)=1:i2=0:RETURN ' libération 530 ' Réception 540 i2=i2+FNl*2^b2 550 b2=(b2+1) AND 7 560 IF b2=0 THEN n2=(n2 MOD 3)+1:PRINT "reçu par 2 :";HEX$(i2):lib2=1:RETURN ' fin de réception 570 RETURN 580 ' Émission 590 o(2)=-((code AND 2^b2)<>0) 600 b2=(b2+1) AND 7 610 IF b2<>0 THEN RETURN 620 n2=(n2 MOD 3)+1 630 lib2=1 ' il faudra faire la libération au prochain coup 640 RETURN 650 ' 660 ' ** ordinateur 3 ** 670 PRINT"3" 680 IF n3=3 THEN GOTO 760 690 ' lecture 700 IF lib3=1 THEN lib3=0:o(3)=1:i3=0:RETURN ' libération 710 ' Réception 720 i3=i3+FNl*2^b3 730 b3=(b3+1) AND 7 740 IF b3=0 THEN n3=(n3 MOD 3)+1:PRINT "reçu par 3 :";HEX$(i3):lib3=1:RETURN ' fin de réception 750 RETURN 760 ' Émission 770 o(3)=-((code AND 2^b3)<>0) 780 b3=(b3+1) AND 7 790 IF b3<>0 THEN RETURN 800 n3=(n3 MOD 3)+1 810 lib3=1 ' il faudra faire la libération au prochain coup 820 RETURN
Pour éviter tout problème sans trop compliquer le protocole, il vient l'idée d'ajouter des étapes intermédiaires d'inactivité pour assurer que la lecture a toujours lieu quand la ligne est dans un état stable. Voici ce que ça donne :
Attention, la “légende” est différente de précédemment :
Concernant les boucles :
|
Bien sûr, l'insertion d'états supplémentaires augmente la durée d'un cycle d'échange d'information mais elle rend celui-ci fiable.
J'ai développé ci-dessous un cycle d'échange d'informations codées sur 2 bits entre trois ordinateurs. Les étapes sont représentées au même niveau pour plus de clarté mais en réalité les interruptions n'ont pas lieu au même instant sur tous les CPC (ce serait trop facile).
On voit d'un échange de mots de 2 bits à trois ordinateurs donne un cycle qui dure 17 interruptions, ce qui représente à quelque chose près la durée de trois rafraichissements d'écran. Donc, à priori, avec ce principe c'est raté pour les jeux où l'affichage des actions des joueurs est à 50Hz. Avec toujours trois ordinateurs qui échangent 8 bits (ce qui est beaucoup, je pense que 4 bits suffisent pour un bon nombre de jeux), il faut compter 71 interruptions par cycle, soit environ un quart de seconde.
Je pars du principe qu'on utilise la seule interruption disponible sur CPC, à 300Hz, mais le schéma est sensé fonctionner quelle que soit la fréquence. D'ailleurs, vu la fréquence faible de variation de l'état de la ligne avec ce protocole, on peut espérer le faire fonctionner sur CPC+ en y apportant éventuellement quelques modifications.
Une subtilité que je n'ai pas encoré évoquée peut faire capoter l'ensemble. Les CPC n'ont pas exactement la même horloge (encore plus entre CPC et CPC+). Donc, il faut prendre le soin de recaler les CPC de temps en temps sur l'étape adéquate. D'après des calculs fumeux, un recalage toutes les 5 secondes environ suffit dans tous les cas de figure.
Voici un programme en BASIC qui simule le protocole, il est mal commenté et un peu brouillon mais il peut vous intéresser.
10 ' Simulation du second protocole 20 ' pour le réseau Virtual Net 30 ' présenté dans la rubrique coding 40 ' de Quasar CPC 18 50 ' 60 DEFINT a-z 70 MODE 2 80 ' FNl est l'état de la ligne 90 ' o(1), o(2) et o(3) sont les états des 3 cartes réseau 100 DEF FNl=o(1) AND o(2) AND o(3) 110 ' 120 ' Passage de la ligne à son état de repos 130 FOR n=1 TO 3:o(n)=1:NEXT 140 ' 150 code=&D6 ' code échanger (8 bits) 160 ' 170 ' Initialisations 180 'n? numéro de l'émetteur en cours (1 à 3) 190 'b? numéro du prochain bit à envoyer (0 à 7) 200 n1=1:n2=1:n3=1 ' l'ordinateur 1 sera le premier à émettre 210 b1=0:b2=0:b3=0 ' la transmission commence par le bit 0 220 lib1=0:lib2=1:lib3=1 230 wait1=0:wait2=0:wait3=0 240 ' 250 ' Mise en place des routines d'interruption 260 ' les boucles FOR/NEXT servent à décaler 270 ' leur lancement pour avoir un fonctionnement 280 ' plus proche de la réalité 290 EVERY 12,3 GOSUB 340 300 FOR x=1 TO 100:NEXT:EVERY 12,2 GOSUB 570 310 FOR x=1 TO 100:NEXT:EVERY 12,1 GOSUB 780 320 GOTO 320 330 ' 340 ' ** ordinateur 1 ** 350 PRINT "n1=";n1 ' affichage du numéro de l'émetteur courant 360 IF n1=1 THEN GOTO 460 370 ' lecture 380 IF lib1<>0 THEN lib1=lib1-1:o(1)=1:PRINT"p":RETURN ' libération 390 ' Réception 400 PRINT"Lect" 410 i1=i1+FNl*2^b1 420 b1=(b1+1) AND 7 430 lib1=2 440 IF b1=0 THEN n1=(n1 MOD 3)+1:PRINT "reçu par 1 :";HEX$(i1):wait1=0:i1=0:RETURN ' fin de réception 450 RETURN 460 ' Émission 470 IF wait1<>0 THEN wait1=wait1-1:PRINT"p":RETURN ' p comme pause 480 PRINT"Ecr" 490 o(1)=-((code AND 2^b1)<>0) 500 b1=(b1+1) AND 7 510 wait1=2 520 IF b1<>0 THEN RETURN 530 n1=(n1 MOD 3)+1 540 lib1=3 ' il faudra faire la libération au prochain coup 550 RETURN 560 ' 570 ' ** ordinateur 2 ** 580 IF n2=2 THEN GOTO 680 590 ' lecture 600 IF lib2<>0 THEN lib2=lib2-1:o(2)=1:RETURN ' libération 610 ' Réception 620 PRINT"Lect2" 630 i2=i2+FNl*2^b2 640 b2=(b2+1) AND 7 650 lib2=2 660 IF b2=0 THEN n2=(n2 MOD 3)+1:PRINT "reçu par 2 :";HEX$(i2):wait2=0:i2=0:RETURN ' fin de réception 670 RETURN 680 ' Émission 690 IF wait2<>0 THEN wait2=wait2-1:RETURN 700 o(2)=-((code AND 2^b2)<>0) 710 b2=(b2+1) AND 7 720 wait2=2 730 IF b2<>0 THEN RETURN 740 n2=(n2 MOD 3)+1 750 lib2=3 ' il faudra faire la libération au prochain coup 760 RETURN 770 ' 780 ' ** ordinateur 3 ** 790 IF n3=3 THEN GOTO 890 800 ' lecture 810 IF lib3<>0 THEN lib3=lib3-1:o(3)=1:RETURN ' libération 820 ' Réception 830 PRINT"Lect3" 840 i3=i3+FNl*2^b3 850 b3=(b3+1) AND 7 860 lib3=2 870 IF b3=0 THEN n3=(n3 MOD 3)+1:PRINT "reçu par 3 :";HEX$(i3):wait3=0:i3=0:RETURN ' fin de réception 880 RETURN 890 ' Émission 900 IF wait3<>0 THEN wait3=wait3-1:RETURN 910 o(3)=-((code AND 2^b3)<>0) 920 b3=(b3+1) AND 7 930 wait3=2 940 IF b3<>0 THEN RETURN 950 n3=(n3 MOD 3)+1 960 lib3=3 ' il faudra faire la libération au prochain coup 970 RETURN