Table des matières

Retourner au sommaire

Le Virtual Net 96

Basé sur les articles publiés dans Quasar CPC numéro 16, numéro 17 et numéro 18, Assembleur : Coding, par Zik.

Hage et Steve ? 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 ?!

Le principe

Caractéristiques

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 :

N'avançons pas en aveugle ! 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
Jusque là, ça reste très simple. Mais que se passe-t-il si deux CPC veulent imposer des états différents ? Eh bien c'est le zéro qui est prioritaire. Donc là ligne ne sera à 1 que si tous les CPCs ont envoyé un 1 sur la ligne. Dès qu'au moins un seul CPC veut imposer un 0, la ligne passe effectivement à 0.

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.

Communiquons

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.

Timings Virtual Net 96

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.

Lors d'un envoi de bloc de données, les deux premiers octets envoyés indiquent la longueur du bloc en octets (avec la convention poids faible puis poids fort).

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 !

Structures des programmes

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.

Le maître à parlé, à vous de jouer ! 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é.

En pratique

Plouf ! 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…

Plouf !

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.

Mettons-nous d'accord !

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”).

Quoi dire ?!

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 :

Dernières recommandations

Grat ! Grat ! 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.

Listing : squelette de gestion réseau pour un jeu

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

Listing : bibliothèque VN96 de Wizcat

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

Simulation d'un autre protocole

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 !

Du nouveau

Le CPCiste moyen en train de jouer à LAP 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.

Première idée

Voici la première méthode sur ce principe qui vient à l'esprit :

La première méthode qui vient à l'esprit

  • écriture = envoi successif des n bits du message (un bit à chaque interruption)

 

  • libération = mise à l'état neutre de la ligne (écriture d'un 1)

 

  • lecture = réception des n bits du message (un bit à chaque interruption)
Comme dit plus haut, on ne réalise qu'une seule opération à chaque interruption. Comme d'habitude, ce protocole suppose que l'on connait le nombre de joueurs et que chaque ordinateur sait quand c'est à lui de parler (ce qui correspond à l'étape d'écriture). Il faut évidemment que chaque CPC démarre dans ce graphe par l'opération adéquate, déterminée par son numéro de joueur (dans ce cas soit la lecture, soit l'écriture).

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.

Listing : Simulation du protocole erroné

Voici un programme de simulation en BASIC qui met en évidence le problème.

Télécharger le listing BASIC.

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

Deuxième idée

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 :

Protocole sécurisé

Attention, la “légende” est différente de précédemment :

  • écriture = envoi d'un bit du message (mise à 1 ou à 0 de la ligne)

 

  • libération = mise à l'état neutre de la ligne (écrire un 1)

 

  • lecture = réception d'un bit du message

 

  • Ø = ne rien faire à cette interruption là !

 

Concernant les boucles :

  • boucle de lecture : il y a en fait à ce niveau deux boucles imbriquées, l'une pour le nombre de bits et l'autre pour le nombre de CPC.

 

  • boucle d'écriture : une seule boucle ici, pour le nombre de bits à envoyer (le même qu'en lecture bien entendu !)

Voyons voir ça de plus prêt...

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).

Cycle d'échange développé

Quel temps fait-il ?!

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.

En vrac

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.

Listing : Simulation du protocole fonctionnel

Voici un programme en BASIC qui simule le protocole, il est mal commenté et un peu brouillon mais il peut vous intéresser.

Télécharger le listing BASIC.

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
1) les concepteurs du Virtual Net 96