Category: Assembleur

  • Les sons préprogrammés de l’Oric (fin)

    3 – ZAP et OUPS

    Introduction

    Ces deux derniers sons ont une structure différente de celle des 5 sons préprogrammés précédents. Ils n’utilisent pas d’enveloppe, mais font appel à des boucles de temporisation pour contrôler l’évolution du son.

    LA COMMANDE ZAP

    Voici la routine copiée et adaptée de la Rom :

    Voici ce que révèle le bloc des 14 paramètres :

    -Contrôle des canaux en service : R7 = #3E, soit 0011 1110. Seul le canal 1 est activé.

    -Période du canal 1 : R0/R1 = #0000. Soit une fréquence infinie que le PSG 9812 ne peut générer !

    -Aucun bruit blanc n’est généré car R6 = #00.

    -Volume du son du canal 1 : R8 = #0F (le maximum). Aucune enveloppe n’est mise en jeu.

    Au premier abord, les paramètres de ZAP sont surprenants ! Le PSG est initialement mis en position de générer des ultra-sons, ce qu’il ne sait pas faire. Mais dans le code qui suit le chargement des 14 paramètres, une boucle est générée, comprenant une temporisation de 1,28 ms et une incrémentation de la période du 1er canal. Cette boucle tourne jusqu’à ce que la période atteigne #0070 ce qui correspond à 554 Hz : Le PSG génère donc un son de plus en plus grave. Le processus se termine alors on envoyant #00 (volume sonore nul) dans le registre R8.

    Si on augmente la temporisation par un facteur 10, afin de mieux percevoir ce qui sort du PSG au début de ZAP, on se rend compte que des criailleries atroces sont générées, comme si on était en train d’assassiner votre Oric ! Heureusement, avec la temporisation normale de ZAP, elles sont trop brèves pour être perçues. Mais il aurait été plus propre d’initialiser la période de départ du canal 1 avec une valeur un peu plus haute que  #0000 !

    Variantes

    La plus évidente est de réduire le volume sonore qui est actuellement au maximum. Un POKE # 982E suivit d’une valeur de #00 à #0F permet de l’ajuster finement.

    Ceci mis à part, les autres possibilités de modifications sont infinies grâce au concept de temporisation permettant d’introduire une modification des paramètres du PSG9812. Rien n’empêche de compliquer le profil du son produit. ZAP est de loin le son préprogrammé le plus intéressant de l’Oric.

    Pour l’heure, nous serons plus modestes et fixerons une période de départ autre que #0000 et une période d’arrivée autre que #0070. Nous pourrons aussi modifier la temporisation, c’est-à-dire la durée de chaque fréquence générée, et par suite la durée totale du son produit.

    Comme précédemment, la procédure consistera à charger ZAPLM en Ram et à POKEr les valeurs à modifier. Cette routine est localisée de #9800 à #9833 et les adresses à POKEr sont les suivantes :

    En #982E : Paramètre R8. Le volume du son du canal 1.

    En #9808 : Octet de poids faible de la période de départ (limitée à #FF, car un seul octet est pris en compte).

    En #981C : Octet de poids faible de la période finale (idem un seul octet pris en compte).

    En #9813 : Le nombre de tours de la boucle de temporisation. Dans l’état actuel de la routine (une boucle de 256 réitérations), il est seulement possible de réduire cette durée. Pour l’augmenter, il faudra mettre en place une deuxième boucle.

    Le programme ZAP1.BAS offre quelques échantillons tout prêts tandis que ZAP2.BAS permet d’expérimenter soi-même de nouveaux sons, en ajustant le volume sonore, la temporisation, les périodes de départ et de fin.

    Quelques remarques :

    -La durée des sons produits est très variable compte tenu de la gamme des fréquences à parcourir.

    -Les sons produits vont de l’aigu vers le grave. Or les sons aigus semblent moins audibles que les graves (à moins que ce ne soit un problème personnel). Il s’en suit que toutes les variantes se ressemblent car elles finissent par des sons graves mieux perçus.

    LA COMMANDE OUPS

    Voici le code d’OUPS transposé et adapté pour l’Atmos à partir du code original d’OUPS pour Telestrat :

    Voici ce que révèle le bloc des 14 paramètres :
    -Contrôle des canaux en service : R7 = #3E, soit 0011 1110. Seul le canal 1 est activé.
    -Période du canal 1 : R0/R1 = #0046 (environ un LA de l’octave 4).
    -Aucun bruit blanc n’est généré car R6 = #00.
    -Volume du son du canal 1 : R8 = #0F (le maximum). Aucune enveloppe n’est mise en jeu.
    Le PSG 9812 génère un LA de l’octave 4 avec un volume sonore maximum suivit d’une temporisation de 0,12 s avant que finalement le canal 1 soit inactivé. Simple non ?

    Variantes

    OUPS repose sur 3 paramètres : Période et volume du son, durée de la temporisation. La période du canal 1 (R0/R1) peut être comprise entre #0000 et #0FFF. Le volume sonore (R8) est ajustable de #00 à #0F. La durée du son peut varier de 1,28 ms (avec Y=1) et 0,33 s (avec Y=#FF). On peut évidemment générer une infinité de sons en combinant ces 3 paramètres.
    Après avoir chargé OUPSLM en Ram, il suffit de poker les valeurs à modifier. Compte tenu de l’adresse d’implantation d’OUPS en Ram (de #9800 à #9827), les adresses à poker sont :
    En # 981A-#981B : Période du canal 1 (R0/R1).
    En #9809 : Nombre de tours de la boucle externe Y.
    En #9822 : Volume sonore du canal 1 (R8)
    Comme pour les autres sons, je vous propose deux petits programmes Basic: Le premier OUPS1.BAS propose divers échantillons de variantes d’OUPS et le second OUPS2.BAS permet d’expérimenter soi-même toutes les combinaisons possibles des 3 paramètres d’OUPS.

    Le programme OUPS1.BAS

    Le menu propose 20 périodes correspondant à la fréquence des notes de DO de l’octave 1 au DO de l’octave 6, puis du LA de l’octave 1 au LA de l’octave 6 et enfin toute la gamme de l’octave 4. Les deux autres paramètres (durée et volume du son) n’ont pas été modifiés et sont ceux du OUPS d’origine.

    Le programme OUPS2.BAS

    Le menu vous propose de tester vous-même tous les sons de votre choix. On peut fixer indépendamment période, volume et durée et écouter ce que cela donne.

    LA COMMANDE ZAAP

    Voici une nouvelle famille de sons, les “ZAAP”, basés sur la routine ZAP, mais dont la durée serait augmentée grâce à une boucle supplémentaire. Non seulement cela permettra d’obtenir des sons plus longs, mais aussi de “normaliser” leur durée, qui varie selon le nombre de périodes balayées. Au final, avec cette “normalisation”, des sons de durée similaire pourront plus facilement être comparés.

    Listing modifié :

    Le nouveau listing ZAAPLM.ASM peut être obtenu à partir du listing ZAPLM.ASM en remplaçant la section située entre les lignes “;Début de la temporisation” et “;Fin de la temporisation” par la même section prise dans OUPSLM.ASM. Cette boucle supplémentaire permet des temporisations de 1,23 ms (pour Y=#01) à 315 ms (pour Y=#00, c’est-à-dire #100 en réalité).
    La temporisation comporte une partie fixe (boucle interne de 1,28 ms environ) et une partie ajustable, la 2e boucle ou boucle externe, qui joue un rôle multiplicateur. La durée totale théorique en ms d’un ZAAP est égale à 1,28 x(période finale – période initiale) x(nombre de tours de 2e boucle). La brièveté des sons ne permet pas de vérifier les valeurs réelles obtenues.

    Le programme ZAAP1.BAS

    Il montre ce que ça donne avec les octaves 3 à 7 lorsque la temporisation a été ajustée pour obtenir des sons de durée totale de 160, 320, 640 et 1300 ms. Voici  les périodes de départ et de fin utilisées pour chaque octave :

    Résultats On observe une nette progressivité dans les résultats obtenus avec les octaves 7 à 3 et pour chacune avec les durées croissantes. C’est l’octave 7 (la plus aiguë)  qui est la moins satisfaisante : La durée 640 ms et surtout la durée 1300 ms révèlent des distorsions peu agréables. Mais dans l’ensemble, cela représente une belle bibliothèque de sons cohérents dérivés de ZAP.

    Le programme ZAAP2.BAS

    Il vous permettra de procéder à vos propres tests en modifiant, les périodes de départ et de fin, ainsi que le nombre de boucles Y et le volume sonore. Le tableau ci-dessus vous sera probablement de quelque aide.

    Vous trouverez tous les programmes produits au cours de ce travail dans le fichier SonsPreprogr3.zip qui accompagne cet article.

  • Les sons préprogrammés de l’Oric

    2e partie : PING, SHOOT, EXPLODE, TOUCON et TOUNOR

    Introduction

    Dans la première partie, nous avons vu comment modifier en Ram les sons préprogrammés dans la Rom, afin de pouvoir les adapter à nos besoins. Dans  cette 2e partie, nous verrons PING, SHOOT, EXPLODE, TOUCON (son des touches contrôles) et TOUNOR (son des touches normales). Ces cinq commandes ont toutes le même code : LDX LL, LDY HH et JSR #FA86. Seule diffère l’adresse LLHH ciblant le bloc des 14 paramètres que la routine #FA86 doit envoyer au PSG 8912.

    Pendant que je suis dans les généralités, remarquons que ces 5 sons utilisent une seule et même enveloppe, la n°0 \, mais avec une durée différente pour chacun d’eux.

    Ils se répartissent ensuite en 2 groupes : D’une part PING, TOUCON et TOUNOR qui utilisent un seul canal, le n°1 et pas de bruit blanc et d’autre part SHOOT et EXPLODE qui n’utilisent QUE du bruit blanc, mais mixé aux canaux 1,2 et 3.

    Pour le groupe PING, TOUCON et TOUNOR, c’est la période de ce canal 1 (paramètres R0/R1) qui fait la différence.  Pour le groupe SHOOT et EXPLODE, c’est la période du bruit blanc (paramètre R6) qui fait la différence. Vous savez déjà quasiment tout ! Les 2 commandes restantes, ZAP et OUPS, ont une structure différente et seront traitées dans la troisième et dernière partie de cette étude.

    La commande PING

    Contenu du bloc des 14 paramètres (voir le fichier source PINGLM.ASM) :

    #18, #00, #00, #00, #00, #00, #00               pour R0 à R6

    #3E, #10, #00, #00, #00, #0F, #00               pour R7 à R13.

    Prêtons attention aux valeurs différentes de zéro (sauf pour R7). Que fait PING ?

    -Contrôle des canaux en service : R7 = #3E = 0011 1110. Seul le canal 1 est actif.

    -Période du canal 1 : R0/R1 = #0018. Le son produit est proche du MI de l’octave 6.

    -Volume du son pour le canal 1 : R8 = #10. Le volume est contrôlé par une enveloppe.

    -Durée de cette enveloppe : R11/R12 : #0F00. On a #0F00x256=983040 µs soit environ 1s.

    -Numéro de l’enveloppe : R13 = #00. Il s’agit de la rampe descendante de forme \.

    PING envoie sur le canal 1 un son proche du MI de l’octave 6. Ce son attaque directement au volume maximal puis décroit jusqu’au niveau zéro, ce qui prend  environ 1s. La production de bruit blanc est inactivée. Les variables que nous pouvons ajuster sont la période et la durée du son.

    Variantes

    Je vous propose deux programmes, PING1.BAS qui offre quelques exemples de variantes et PING2.BAS qui permet d’expérimenter soi-même des sons basés sur PING en ajustant à volonté la période et la durée de l’enveloppe.

    Ces 2 programmes chargent PINGLM en Ram et de DOKEnt les valeurs à modifier selon les choix offerts par un menu. Compte tenu de l’adresse d’implantation de PINGLM en Ram (de #9800 à #9814), les adresses à DOKEr sont :

    En #9807- #9808 : La période du canal 1.

    En #9812- #9813 : La durée de la rampe. PING1.BAS propose 20 variantes de PING qui illustrent les possibilités (figure de gauche page précédente), que je vous laisse approfondir avec PING2.BAS en jouant simultanément sur la période et sur la durée.

    Les commandes TOUCON et TOUNOR

    Voici ce que révèle le bloc des 14 paramètres :

    Pour TOUCON :

    #2F, #00, #00, #00, #00, #00, #00               pour R0 à R6

    #3E, #10, #00, #00, #1F, #00, #00               pour R7 à R13.

    Pour TOUNOR :

    #1F, #00, #00, #00, #00, #00, #00               pour R0 à R6

    #3E, #10, #00, #00, #1F, #00, #00               pour R7 à R13.

    Ces deux commandes ne diffèrent que par la période du canal 1.

    -Contrôle des canaux en service : R7 = #3E = 0011 1110. Seul le canal 1 est actif.

    -La période du canal 1 (R0/R1) vaut :

     #002F pour TOUCON (MI de l’octave 5) et

     #001F pour TOUNOR (SI de l’octave 5).

    -Volume du son pour le canal 1 : R8 = #10. Le volume est contrôlé par une enveloppe.

    -La durée de l’enveloppe (R11/R12) est de #001F (environ 8 ms). C’est la différence majeure avec PING : Si PING est bref, TOUCON et TOUNOR sont ultra-brefs !

    -Numéro de l’enveloppe : R13 = #00. Il s’agit de la rampe descendante de forme \.

    Comme pour PING, les variables que nous pouvons ajuster sont la période et la durée du son. En fait, le fonctionnement des commandes TOUCON et TOUNOR est tout à fait semblable à celui de PING.

    Variantes

    Comme pour PING, je vous propose deux programmes :

    -TOUCON1.BAS propose 18 variantes de TOUCON/TOUNOR qui illustrent les possibilités (figure de droite page précédente) et que je vous laisse approfondir avec

    -TOUCON2.BAS en jouant simultanément sur la période et sur la durée.

    Ces 2 programmes s’appliquent aussi bien à TOUCON qu’à TOUNOR. Les adresse d’implantation de TOUCONLM en Ram et les adresses à DOKEr sont les mêmes que pour PING.

    Les commandes SHOOT et EXPLODE

    Voici ce que révèle le bloc des 14 paramètres :

    Pour SHOOT :

    #00, #00, #00, #00, #00, #00, #0F               pour R0 à R6

    #07, #10, #10, #10, #00, #08, #00               R7 à R13.

    Pour EXPLODE :

    #00, #00, #00, #00, #00, #00, #1F               pour R0 à R6

    #07, #10, #10, #10, #00, #18, #00               R7 à R13.

    On peut observer les paramètres :

    -Contrôle des canaux en service : R7 = #07 = 0000 0111. Les canaux 1, 2 et 3 ne délivrent que du bruit blanc.

    -Ce bruit blanc a pour période R6 = #0F pour SHOOT et R6 = #1F pour EXPLODE.

    -Pour les 2 commandes, le volume du son sur les 3 canaux est contrôlé par une enveloppe : R8 à R10 valent #10.

    -La durée de cette enveloppe (R11/R12) est #0800  (0,5 s environ) pour SHOOT et  #1800 (1,6 s environ) pour EXPLODE.

    -Numéro de l’enveloppe : R13 = #00. Il s’agit de la rampe descendante de forme \. Ces deux commandes sont très proches l’une de l’autre. Elles ne diffèrent que par la durée de l’enveloppe et la période du bruit blanc. La particularité de ces 2 sons est de reposer uniquement sur la production de bruit blanc ! Par définition, un bruit blanc est un signal aléatoire (voir https://fr.wikipedia.org/wiki/Bruit_blanc) et la notion de “période du bruit blanc” est surprenante, mais bon, voyons ce que ça donne en pratique…

    Variantes

    Ici encore les possibilités de modulation sont évidentes : Soit on modifie la période du bruit blanc, soit on joue sur la durée de l’enveloppe.

    Comme précédemment, la procédure consistera à charger SHOOTLM ou EXPLODELM en Ram et à POKEr les valeurs à modifier. Ces 2 routines ont la même adresse d’implantation en Ram (de #9800 à #9814) et les mêmes adresses à POKEr :

    En #980D :                         La période du bruit blanc.

    En #9812- #9813 :            La durée de la rampe.

    Les programmes SHOOT1.BAS (figure de gauche page précédente) et EXPLODE1.BAS (figure de droite page précédente) offrent quelques échantillons à écouter. Dans SHOOT1.BAS, la période du bruit blanc et la durée de l’enveloppe varient séparément, tandis que dans EXPLODE1.BAS les variations de ces 2 paramètres sont combinées. Les programmes SHOOT2.BAS et EXPLOD2.BAS vous permettrons d’expérimenter vos propres idées, en ajustant à volonté la période du bruit blanc et la durée de l’enveloppe. En fait ces 2 programmes s’appliquent aussi bien à SHOOT qu’à EXPLODE. Avec un peu de chance vous aurez la surprise de retrouver EXPLODE avec SHOOT2.BAS et SHOOT avec EXPLODE2.BAS !

    Petite douceur

    Après tout ce travail, il est temps de s’offrir une petite gâterie. Tous les sons examinés jusqu’ici étaient basés sur l’enveloppe n°0 \. Juste pour voir, j’ai fait quelques essais avec les enveloppes /, ////, / ͞   et /\/\/ , ceci pour les 5 sons PING, SHOOT, EXPLODE, TOUCON et TOUNOR. Pour chacun de ces sons, un seul paramètre diffère des paramètres d’origine : R13, qui est respectivement mis à #04 (/), #0C (////), #0D (/ ͞  ) et #0E (/\/\/) (figure ci-dessus).

    Comme on pouvait s’y attendre, les sons produits avec les enveloppes ////, / ͞   et /\/\ continuent à l’infini  (mais j’ai coupé !).

    Pour PING, SHOOT, EXPLODE, TOUCON et TOUNOR avec enveloppe /, les résultats sont assez surprenants, car ces 5 sons fonctionnent à l’envers de ce qu’on a habituellement dans l’oreille ! Les sons TOUCON et TOUTNOR inversés sont particulièrement intéressants.

    Avec l’enveloppe //// (c’est à dire les sons précédents mais répétés à l’infini), on découvre des bruits surprenants de machines ou de sonneries.

    L’enveloppe / ͞   est moins intéressante car le volume du son est maintenu au niveau haut à l’infini.

    Enfin l’enveloppe /\/\/, couplée à PING, SHOOT et EXPLODE nous emmène au bord de la mer (avec beaucoup d’imagination). A nouveau TOUCON et TOUTNOR donnent des résultats plutôt rigolos que je vous laisse les découvrir.

    Vous trouverez tous les fichiers de ces expérimentations et une mine d’idées pour agrémenter le bruitage de vos programmes dans le fichier SonsPreprogr2.zip qui accompagne cet article.

    A suivre…

  • Les sons préprogrammés de l’Oric

    1e partie : Récupération et adaptation du code source

    par André C.

    Les 3 articles précédents “Musique, Note, Octave, Fréquence et Période“, “Les enveloppes du PSG 8912” et “Temporisation en langage machine” vous ont fourni quelques éléments de compréhension des sons de l’Oric. Nous allons voir comment exploiter au mieux les sons préprogrammés grâce à une nouvelle série de 3 articles. Afin d’éviter les redites, j’ai zappé les bases déjà décrites et vous invite à vous reporter aux articles précédents en cas de besoin.

    Liste des sons préprogrammés

    Aux commandes usuelles PING, SHOOT, EXPLODE et ZAP, il faut ajouter TOUCON (bruit clavier des touches contrôles), TOUNOR (idem touches normales) et OUPS, le petit dernier du Telestrat.

    Tous les sons préprogrammés ont le même inconvénient : il n’est pas possible d’en moduler le volume sonore (d’où divers petits gadgets hardware apparus pour atténuer le son pendant la nuit !). La raison en est simple : Tous ces sons sont figés en Rom et on peut difficilement intervenir.

    Il est quand même possible de s’inspirer de leur code pour le mettre en Ram et le modifier. Pour OUPS par exemple, le volume sonore est figé plein pot, mais il est facile de rendre ce paramètre ajustable dans une version en Ram. Malheureusement, pour les sons préprogrammés utilisant une enveloppe, on est bien obligé de garder celle-ci, sous peine de faire autre chose. Il est toutefois possible de modifier la hauteur de la note, sa durée etc.

    Structure des sons préprogrammés

    Tous ces sons sont basés sur 2 routines en Rom :

    • La routine #F590 envoie un seul des 14 paramètres au PSG (les registres A et Y du 6502 contenant respectivement le paramètre à envoyer et le numéro du registre cible).
    • La routine #FA86 envoie 14 paramètres au PSG (les registres X et Y du 6502 contenant l’adresse où se trouvent les 14 paramètres).

    En outre ZAP et OUPS font appel à une temporisation à boucle pour gérer la durée du son produit.

    Récupération des sources en langage machine

    Le plus simple est donc de recopier le code présent en Rom dans un fichier source en langage machine. Il faut aussi modifier les adresses des blocs de paramètres pour les diriger vers la Ram. Enfin, dans le cas d’OUPS, il faut remplacer les adresses Telestrat par les adresses Atmos pour les 2 routines citées ci-dessus.

    C’est ainsi que j’ai obtenus les 8 fichiers source suivants : PINGLM.ASM, SHOOTLM.ASM, EXPLODLM.ASM, TOUCONLM.ASM, TOUNORLM.ASM, ZAPLM.ASM, OUPSLM.ASM et même PREDEFLM.ASM qui regroupe le code des 7 sons préprogrammés (voir le fichier SonsPreprogr1.zip qui accompagne cet article).

    Mise en œuvre des sons préprogrammés

    Après compilation, j’ai obtenu 8 fichiers exécutables .TAP qui sans surprise reproduisent fidèlement les 7 sons d’origine. Il ne reste plus qu’à étudier le mécanisme de chaque son et éventuellement à modifier certains paramètres.

    Les commandes PING, SHOOT, EXPLODE, TOUCON et TOUNOR utilisent une enveloppe pour contrôler le volume sonore alors que ZAP et OUPS fixent celui-ci au maximum. Le code de tous ces sons envoie un bloc de 14 paramètres au PSG 8912. Mais en outre, le code de ZAP et OUPS met en place des boucles de temporisation pour contrôler le son produit.

    L’analyse du bloc de 14 paramètres est facile car tous les paramètres sont à zéro par défaut (sauf R7 qui contrôle les canaux en service et qui est mis à #3F pour tout inhiber). Voir éventuellement “Retour sur la mise en œuvre du PSG AY3-8912” dans le CEO-mag de Janvier 2020.  Les paramètres spécifiques d’un son donné sont donc immédiatement apparents ce qui permet de comprendre ce que fait ce son. Attention toutefois au paramètre R13 dont la valeur #00 peut désigner l’enveloppe de n°0 lorsque le contrôle du volume sonore fait appel à une enveloppe.

    A suivre…

  • Musique, Note, Octave, Fréquence et Période

    Rappels et précisions

    par André C.

    CE QU’IL Y A DERRIÈRE LE HAUT-PARLEUR

    Le PSG 8912, circuit intégré complexe, génère des sons sur la base de 14 paramètres qu’il faut lui fournir. Ce sont surtout les 6 premiers paramètres (en fait 3 paramètres sur 2 octets chacun) qui nous intéressent aujourd’hui. Ils définissent la période des sons qui seront respectivement générés par les canaux 1, 2 et 3.
    Lorsqu’on veut programmer des sons en langage machine, il faut envoyer ces 14 paramètres aux 14 registres du PSG 8912 à l’aide de la routine #FA86 (Atmos), après avoir renseigné X et Y pour qu’ils pointent sur l’adresse où se trouve le bloc des 14 paramètres. Heureusement ces 14 registres sont latchés (verrouillés) et gardent leur valeur tant qu’on ne la remplace pas par une autre. La plupart du temps il y a donc peu de changements et il est possible de modifier un seul registre à l’aide de la routine #F590 (Atmos), après avoir renseigné A avec le n° de registre (de 0 à 13) et X avec la valeur à y placer (A, X et Y sont les registres du microprocesseur 6502).

    VOUS PRÉFÉREZ LE BASIC ?

    Il n’empêche que le PSG 8912, ce n’est pas de la tarte, avec ses 14 paramètres dont certains sont complexes ! Mais les commandes PLAY, MUSIC et SOUND simplifient la vie de ceux qui sont effrayés par le langage machine. Le bel effort, qui a été fait pour programmer les commandes PLAY, MUSIC et SOUND, est handicapé par la pauvreté des manuels. A ce propos, je vous renvoie à l’excellent article “PLAY, MUSIC, SOUND”, paru dans le CEO-mag d’avril 2000, n°119-120, pages 35 à 39.
    Toujours est-il que ces 3 commandes Basic permettent de mettre en œuvre assez facilement les paramètres du PSG 8912.

    MUSIC

    Dans le cas de MUSIC, par exemple, il suffit d’indiquer le canal à activer (1 à 3), l’octave (0 à 7), la note (1 à 12) et le volume (1 à 15 pour le volume sonore ou 0 pour passer la main au contrôle par une enveloppe). Quoi de plus simple ? Soit dit en passant, la syntaxe de ce dernier paramètre est regrettable. Pourquoi ne pas avoir repris les valeurs qu’accepte directement le PSG 8912, à savoir 0 à 15 pour le volume et 16 (#10) pour le passage de contrôle à une enveloppe ?

    SOUND

    Mais MUSIC a les inconvénients (les limites) de son avantage (la simplicité) : Cette commande ne sait produire aucun son en dehors des notes standards. Pour ce genre de sons, il existe la commande SOUND, pour laquelle il faut indiquer le canal (1 à 6 avec 1 à 3 pour le canal et 4 à 6 pour ajouter du bruit blanc aux canaux 1 à 3), la période (de 0 à 65535) et le volume (idem MUSIC).
    Pas top la syntaxe du 3e paramètre (période). En français la hauteur d’une note est définie par sa fréquence. On ne parle jamais de la période d’une note. L’utilisation de la période était probablement plus simple d’un point de vue programmation, mais elle est contre culturelle et pas du tout intuitive (plus une note est haute plus sa période diminue).
    Toute valeur entre #0000 et #FFFF (0 et 65535) est acceptée et la valeur que l’on indique est directement envoyée au PSG 8912. Mais celui-ci ne prend pas en compte les 4 bits supérieurs de l’octet de poids fort. Les valeurs réellement injectées dans le PSG sont ra menées à l’espace #0000 à #0FFF (0 à 4095 en décimal). L’intervalle de 0 à 65535 est donc illusoire (mais les capacités du PSG 8912 et celles de l’oreille humaine sont quand même largement couvertes).

    PERIODE

    La période (en secondes) est l’inverse de la fréquence (en Hertz). En pratique, pour avoir des valeurs manipulables, on exprime la période en µs. Pour ce faire, il faut diviser 1 000 000 par la fréquence (il y a un million de µs dans une seconde). Par exemple pour le LA de l’octave 3 (le diapason qui sert de référence en musique), la fréquence officielle est 440 Hz. La période correspondante est de 1 000 000 / 440 = 2 272,73 µs, une valeur qui n’est jamais mentionnée, ni utilisée.

    LE TRUC INVISIBLE

    Le truc qui n’est pas explicité dans les divers documents disponibles (et il y en a un paquet !) est que dans le cas du PSG 8912, la période n’est pas exprimée en µs, mais en unités de 16 µs (ça sent le système binaire là-dessous). Il faut donc appliquer un facteur de conversion de 62 480 (car 1 000 000 / 62 480 = 16,00512164). Dans le cas du LA de l’octave 3 (440 Hz) la période exprimée en unité de 16 µs est de 62480 / 440 = 142 (soit #008E en hexadécimal). Les amateurs de langage machine pourront vérifier que #008E est bien la valeur indiquée dans la Rom par les tables de conversion en #FC5E (octet de poids fort) et #FC6B (octet de poids faible). Et c’est bien la valeur 142 qu’il faut indiquer à la commande SOUND pour obtenir un LA de l’octave 3.

    POUR EN REVENIR AU PSG 8912

    C’est bien sûr #008E qu’il faut aussi indiquer au PSG pour obtenir ce même LA de l’octave 3. Par exemple, pour obtenir ce son sur le canal 1, il faudra placer #8E dans le premier registre et #00 dans le second. D’une manière générale, pour produire un son de fréquence F, il faudra placer dans les registres du PSG une valeur de période P = 62480 / F. Ce n’est pas si compliqué, encore fallait-il le dire clairement. En complément pour ceux qui seraient intéressé, une table de conversion “Note-Frequence-periode.pdf” accompagne cet article.

  • Les enveloppes du PSG 8912

    par André C.

    Cet article fait suite à “PLAY, MUSIC, SOUND”, paru en avril 2000, dans le CEO-mag n°119-120 page 35 à 39. Mais il est plus particulièrement consacré aux enveloppes du PSG 8912.

    L’Oric permet de générer deux sortes de sons : Soit des sons continus dont on peut fixer (entre autre) le volume sonore, soit des sons complexes dont le volume évolue avec le temps selon un profil appelé “enveloppe”. Par exemple, l’enveloppe du fameux PING est une rampe descendante allant du volume maximum à zéro : \

    Problèmes de n° d’enveloppe

    Pour le registre “Numéro de l’enveloppe”, le PSG 8912 accepte des valeurs de #00 à #0F soit 16 possibilités (voir “L’Oric à nu” de Fabrice Broche, page 23). Mais certaines valeurs donnent des résultats identiques : d’une part #00, #01, #02, #03, #9 et d’autre part #04, #05, #06, #07, #0F. Cela ramène le nombre réel d’enveloppes à 8 (voir tableau page suivante).
    Mais 7 enveloppes seulement sont décrites dans le manuel de l’Oric-1 (page 111) et dans celui de l’Atmos (page 124). En fait les concepteurs de la commande PLAY ont fait un mauvais choix : La table pour de conversion des valeurs de 0 à 7 de la commande PLAY en valeurs de #00 à #FF à envoyer au PSG (soit : #00, #00, #04, #08, #0A, 0B, #0C et #0D) fait l’impasse sur l’enveloppe #0E, tandis que l’enveloppe #00 y figure (inutilement) deux fois ! (cf. “L’Oric à nu”, page 380).

    Problèmes de profils

    La forme de ces 16 enveloppes est un peu délicate à comprendre. Comme déjà indiqué, leur numéro va de #00 à #0F, soit en binaire, de 0000 0000 à 0000 1111. Ce numéro pilote en fait 4 paramètres correspondants aux bits b0 à b3 (actif lorsqu’ils sont mis à un) :
    ● Bit0 (HOLD) : Si 0 = répète indéfiniment le profil initial, si 1 = maintient (HOLD) indéfiniment le dernier son.
    ● Bit1 (ALT) : Si 0 = garde profil initial, si 1 = ALTerne front montant et front descendant.
    ● Bit2 (ATT) : Si 0 = ATTaque avec un front initial descendant, si 1 = ATTaque avec un front initial montant.
    ● Bit3 (CONT) : Si 0 = son limité dans la durée, si 1 = son CONTinu.
    En fait il n’y a que deux profils de base : Rampe descendante (ATT = 0, soit \) et rampe montante (ATT = 1, soit /). Le profil peut alors en rester là (CONT = 0) ou le son être prolongé (CONT = 1). Il y a deux manières de le prolonger : soit le dernier son est maintenu (HOLD = 1), soit le profil est répété (HOLD = 0). Enfin, si le profil est répété, il peut soit garder sa forme initiale (ALT = 0, la forme est répétée telle quelle et on a alors un résultat en dents de scie (//// ou \\\\), soit la forme est alternée (ALT = 1) et on a alors un résultat en \/\/\/ ou en /\/\/\ selon que l’on commence par un front descendant ou un front montant.
    Certaines enveloppes me laissent songeur (par exemple l’enveloppe #0B : rampe descendante qui se termine par un niveau sonore nul mais dont le niveau maintenu est haut ou à l’inverse l’enveloppe #0F : la rampe monte jusqu’au volume maximum, suivi par un niveau maintenu bas ! En fait ce niveau bas est inaudible et on est ramené à l’enveloppe #04 Le fichier ENVELOP.TAP qui accompagne cet article vous permettra d’étudier vous-même ce que cela donne. Dans cette démo, le PSG 8912 acceptant des valeurs de #00 à #0F j’ai retenu ces valeurs pour tester les 16 enveloppes. C’est le moment de vous faire l’oreille…
    Pour ma part, je suis arrivé à la conclusion suivante :

    n° pour
    le PSG
    n° pour PLAYDescription et profil
    #00 *0 & 1Rampe descendante puis arrêt \
    #04 *2Rampe montante puis arrêt /
    #083Répétition de la rampe descendante \\\\
    #0A4Rampe descendante, puis rampes alternées \/\/\
    #0B5Rampe descendante, puis plateau haut continu\ ͞
    #0C6Répétition de la rampe montante ////
    #0D7Rampe montante, puis plateau haut continu / ͞
    #0E **.Rampe montante, puis rampes alternées /\/\/
    Les différentes enveloppes gérée par le PSG 8912

    Note : Dans ce tableau, les numéros d’enveloppe à envoyer au PSG figurent en hexadécimal, tandis que les numéros destinés à la commande Basic PLAY figurent en décimal. Il s’agit seulement d’un artifice de présentation.
    Note *: Pour le PSG, les numéros d’enveloppe #00, #01, #02, #03 et #9 sont identiques. Idem pour les numéros #04, #05, #06, #07 et #15. Les enveloppes #09 \ _ et #0F /_ se terminent par un niveau bas continu donc inaudible et on est ramené aux enveloppes #00 et #04 respectivement.
    Note **: Cette enveloppe n’est pas accessible avec PLAY.

    Durée de l’enveloppe

    Dernière ombre au sujet des enveloppes : Le registre “Période de l’enveloppe” du PSG 8912. Ce terme de “période” entraîne une confusion avec le même terme utilisé pour la commande SOUND pour désigner la hauteur du son à générer.
    En fait, ce paramètre fixe la durée des enveloppes. Mais son usage n’est pas évident car sur les 8 enveloppes existantes, seules 2 ont une durée déterminée, les 6 autres durent sans fin (jusqu’à ce qu’on envoie de nouveaux paramètres au PSG).
    J’ai écrit plus haut “il n’y a que deux profils de base : Rampe descendante \ et rampe montante /” et justement le paramètre “durée” fixe la durée de ces rampes. Dans le cas où les rampes sont répétées sans fin, leur durée individuelle est celle indiquée par le paramètre “durée”.
    Dans “L’oric à nu” page 23, Fabrice Broche indique que ce paramètre “va de #0000 à #FFFF et qu’il faut multiplier par 16 pour avoir la période en µs, soit de 0 à 1 seconde environ”. En réalité, c’est par 256 qu’il faut multiplier. Comme tout un chacun peut le vérifier, la durée des enveloppes va de 0 à 16 secondes environ. Avec la durée maximum #FFFF, on a 65535×256 = 16776960 µs, soit un peu plus de 16 secondes. Et c’est bien cela qu’on obtient en pratique lorsqu’on envoie #FFFF au PSG 8912.
    C’est encore différent avec la commande Basic PLAY, qui permet (entre autres) de fixer la durée l’enveloppe à utiliser. PLAY accepte des valeurs de 0 à #7FFF (32767), ce qui génère des sons durant environ 16 s. Pour toute valeur supérieure à #7FFF, le PSG ne prend pas en compte le bit de poids le plus fort. Ainsi #8000 est ramené à #0000 et #FFFF est ramené à #7FFF, c’est-à-dire environ 16 s.

    Conclusion

    J’espère que cet article vous donne toutes les clefs pour utiliser des enveloppes dans la production de sons avec votre Oric. Il n’en reste pas moins que l’usage de ces enveloppes n’est pas évident et il faut tâtonner avant d’obtenir ce que l’on désire. Mais quel plaisir de combiner plusieurs enveloppes à la suite les unes des autres pour produire un son complexe, comme par exemple le lancement d’un missile, le sifflement de sa trajectoire et son explosion au contact de la cible ! Le PSG 8912 est un beau jouet, pour peu que l’on soit patient…

  • Temporisation en langage machine

    par André C. avec l’aide de Christian L.

    Cet article a été corrigé grâce aux indications précieuses de Christian L. “Assinie” concernant l’interférence des interruptions dans la routine TEMPOB. Qu’il en soit cordialement remercié.

    La commande Basic WAIT est une petite merveille de simplicité d’utilisation. Il suffit d’indiquer une valeur de temporisation en centièmes de secondes et pourvu que les IRQ ne soient pas inhibées, l’exécution du programme se fige pendant le délai indiqué. La valeur du délai indiqué doit être comprise entre 0 et 65535 (en pratique entre 1 à 65535), ce qui permet des temporisations de 0,1 à 655 secondes (presque 11 min donc). C’est plus qu’il n’est utile.
    La précision de WAIT est étonnamment bonne. Mais il n’est pas possible d’obtenir des délais très brefs, car la valeur minimale est de 100 µs, ce qui est beaucoup comparé aux 2 µs qu’il est possible d’obtenir en langage machine, en insérant un NOP dans un programme. En effet NOP (No OPeration) ne fait rien, mais ça prend 2 cycles de microprocesseur, soit 2 µs dans le cas de l’Oric. Moi quand je ne fais rien, ça prend quand même plus de temps, heureusement ça n’arrive pas souvent !

    Temporisation à boucle

    Dans la plupart des programmes LM ainsi que dans les routines de la Rom, les temporisations sont basées sur des boucles. Mais vous verrez qu’il y a peut-être mieux à faire.
    Voici le principe d’une temporisation à boucle :

    C’est simple non ? Mais si on veut calculer le délai correspondant à cette valeur 5, ce n’est pas si facile. En effet, il faut tenir compte du nombre de cycles de microprocesseur que prend chaque instruction.
    De plus, très souvent ce nombre de cycle n’est pas une valeur fixe, mais dépend du contexte. Ainsi si LDX et DEX sont des cas simples car nécessitant toujours 2 cycles chacun, c’est plus compliqué avec BNE où trois cas peuvent se présenter :
    ● 2 cycles si le branchement n’a pas lieu.
    ● 3 cycles si le branchement a lieu (cas normal pendant la boucle).
    ● 4 cycles si le branchement a lieu avec un changement de page mémoire.
    Si on fait un bilan pour l’exemple ci-dessus, on a 2 cycles pour LDX, plus 4 fois 2+3 cycles (2 pour DEY + 3 pour BNE avec branchement) plus 1 fois 2+2 cycles (2 pour DEY + 2 pour BNE sans branchement) on arrive à un total de 26 cycles, soit 26 µs pour l’Oric, qui tourne à 1 MHz. Le calcul précédent peut se formuler ainsi : Le délai vaut D = 5Y -1 + 2 ( 1 pour le dernier BNE sans branchement et +2 pour le LDX initial).
    Le délai maximum pour une simple boucle est obtenu avec Y=#FF et se limite à environ 1,3 ms. (D = 256×5 -1 + 2 = 1281 µs). C’est exactement ce code que l’on trouve dans la Rom en #FAE1 pour la durée de ZAP.

    Temporisation à 2 boucles

    Si on veut obtenir un délai plus long, il faut imbriquer deux boucles. C’est ce qu’a fait Fabrice Broche pour OUPS, le nouveau son préprogrammé du Telestrat :

    Essayons de calculer le délai produit par l’exécution de cette routine :
    ● Pour les LDY et LDX d’initialisation : 4 cycles.
    ● Pour la 1e boucle : D = 256×5 – 1 = 1279 cycles (-1 pour le dernier BNE de la 1e boucle). Notez que cette 1e boucle se termine avec X=0 donc X est régénéré à sa valeur initiale pour la suite.
    ● Pour la 2e boucle : D= 96(1279+5) -1 = 123263 cycles (avec 1279 pour l’exécution de la 1e boucle et -1 pour le dernier BNE sans branchement de la 2e boucle). Notez que le temps d’exécution d’un tour de la 2e boucle inclus celui de l’exécution totale de la 1e boucle.
    Au total on a donc 123267 cycles, soit 123267 µs pour l’Oric. La durée théorique finale de OUPS est donc de 0,123 s (sauf si je me suis planté). On voit aussi que l’on peut simplifier ce calcul car il y a quelques cycles négligeables, par exemple les LDY et LDX initiaux et la réduction de 1 cycle pour BNE sans branchement. On arrive alors à un délai D = 96(1280+5) = 123360 µs, soit une approximation de +0,08%.
    Le délai maximum que l’on puisse obtenir avec 2 boucles avec X=#FF et Y=#FF est d’environ 0,33 ms.
    Le calcul simplifié donne D = 256(1280+5)=328960 µs. Ça risque de n’être pas assez dans certains cas. On pourrait ajouter quelques NOPs (2 cycles à chaque fois) dans la 1e boucle qui est exécutée 256×256=65536 fois. L’exécution s’allongerait alors de 65536×2=131072 cycles par NOP ajouté, c’est-à-dire 0,13 s, ce qui n’est pas négligeable. Mais, par exemple, pour atteindre un délai total de 1 s, il faudrait 5 NOP (131072 x 5 = 655360 cycles supplémentaires plus 328960 cycles initiaux = 984320 cycles, soit environ 0,98 s). Mais il ne serait pas raisonnable d’aller plus loin en accumulant les NOP.

    Temporisation à 3 boucles

    Si 1 s ne vous suffit pas, il faut ajouter une 3e boucle en utilisant le 3e registre du 6502 : L’accumulateur A (il n’existe pas de DEA pour décrémenter A, mais c’est facilement contournable). La temporisation maximale possible est tellement grande que l’on peut ajuster finement les valeurs respectives de A, Y et X pour obtenir un délai en chiffre rond. En voici un exemple pour obtenir un délai de 40 s. J’ai choisi cette durée afin de chronométrer la durée obtenue.

    Cette structure des boucles est nécessitée par la réinitialisation des compteurs au début de chaque boucle. A l’issue de la 1e boucle, X est automatiquement réinitialisé à zéro. Toutefois, cette remise à zéro est perdue dans le cas de l’exécution de la 3e boucle et doit être repositionnée. D’autre part, l’exécution de la 2e boucle se termine avec Y = zéro. Si l’on veut utiliser une valeur initiale de Y différente de zéro, il faut également repositionner Y. C’est pourquoi la 3e boucle ne se termine pas par un BNE BOUCL1, mais avec un BNE BOUCL3.
    En faisant abstraction de la durée réduite des BNE de fin de boucle et de 6 cycles pour les LDA, LDY et LDX initiaux, on peut faire le calcul simplifié suivant :
    ● L’exécution de la boule X dure D = 256×5 = 1280 µs.
    ● L’exécution de la boucle Y dure D = 156(1280+5) = 200460 µs soit environ 0,20 s
    ● L’exécution de la boucle A dure D = 200(200460+9) = 40093800 µs soit environ 40 s.
    En ajustant les valeurs de Y et A il est possible d’obtenir la durée dont on a besoin. Il suffit de faire quelques calculs avec les formules ci-dessus.
    Durée maximale possible avec une temporisation à 3 boucles (hors ajout de NOPs) : 84 s
    ● L’exécution de la boule X dure D = 256×5 = 1280 µs.
    ● L’exécution de la boucle Y dure D = 256(1280+5) = 328960 µs soit environ 0,32 s
    ● L’exécution de la boucle A dure D = 256(328960+9) = 84216064 µs soit environ 84 s.

    Temporisation avec le Timer 2

    Vous vous rappelez la simplicité de mise en œuvre de la commande Basic WAIT ? Si on consulte “L’Oric à nu” page 186, on peut examiner ce que fait cette routine. Le paramètre qui suit WAIT (nombre de centièmes de secondes de délai) est analysé, puis placé en page zéro aux adresses #33 (LL de a valeur) et #34 (HH de la valeur). Le registre A est initialisé avec #02 (n° du timer à utiliser), puis le registre Y reçoit le contenu de la mémoire #33 et X celui de #34. La routine #EEAB écrit la valeur YX dans le timer 2, lequel est décrémenté tous les centièmes de secondes. On lit l’état du timer 2 avec la routine #EE9D et on sort quand il tombe à zéro.
    Voici un exemple qui permet d’obtenir un délai de 40 s :

    Quelques mesures

    Pour vérifier les exemples, j’ai comparé ce que donnent WAIT 4000, TEMPOB et TEMPOT avec un Atmos réel, un Atmos sous Euphoric+DosBox et un Atmos sous Oricutron 1.2. Voici les résultats :

    Atmos réelEuphoricOricutron
    WAIT 400040s42s41s
    TEMPOB40s42s41s
    TEMPOT40s42s41s
    Temporisations mesurées

    L’Atmos donne les valeurs escomptées pour WAIT 4000, TEMPOB et TEMPOT. Par contre Oricutron donne des valeurs légèrement plus élevées, mais qui restent dans la limite de la précision de mon chronométrage. Pour Euphoric, c’est encore plus marqué, mais peut-être est-ce dû à DosBox. J’ai déjà remarqué ce défaut de réactivité avec DosBox dans d’autres situations.

    Conclusion

    La temporisation avec le timer 2 est plus facile à mettre en œuvre que les temporisations à boucles et devrait être utilisée préférentiellement pour les délais de 0,1 à 655 s (comme WAIT). Il n’en reste pas moins que pour les petits délais une simple boucle permet de temporiser jusqu’à 1281 µs (et éventuellement deux boucles pour aller jusqu’à 0,33 s). Il n’y donc que l’embarras du choix quant à la méthode à utiliser !

  • Fonctionnement de la pile matérielle du 6502 (4/8)

    PARAMÈTRES ET SOUS-ROUTINES

    L’entrée et la sortie de données nécessaires au fonctionnement d’une sous-routine, peut se dérouler de quatre façons :

    1. En passant par les registres (A, X et / ou Y). C’est la meilleure méthode et la plus performante lorsqu’il s’agit de données constituées d’un ou deux octets
    2. En passant par des variables en RAM. Cette méthode est parfaite pour des données qui doivent rester en RAM et qui doivent servir à d’autres routines. Elle est souvent utilisée de manière conjointe à la première méthode.
    3. Les besoins en entrées de la sous-routine peuvent être intégrés au programme lui-même, juste après l’instruction JSR (comme par exemple une chaîne alphanumérique à afficher). Dans ce cas, la sous-routine utilise l’adresse de retour stockée sur la pile en guise d’adresse de début des données. Elle peut aussi utiliser un octet qui détermine le nombre de données ou un délimiteur afin de savoir de combien d’octets avancer l’adresse de retour avant d’exécuter l’instruction RTS (et d’éviter que le processeur ne traite les données comme une instruction).
    4. En passant par la pile matériel.
      • Pile en page 1 : le programme appelant empile la (ou les) données grâce à PHA, puis récupère le (ou les) résultats grâce à PLA, au retour de la sous-routine.
      • Pile virtuelle en page 0 : Idem précédemment, avec une pile de données différentes de la pile matérielle, ce qui peut faciliter certaines opérations sur la pile et l’adressage indirect. Par exemple, la pile de données peut être utilisée dans un processus en plusieurs étapes pour calculer une adresse, adresse qui à son tour peut être utilisée pour un accès indirect à un octet dans un tableau pour faire un calcul ou une opération logique.
      • Pile en RAM : Idem pile virtuelle en page 0, mais pouvant typiquement servir pour des nombres en virgule flottante autres que des adresses nécessitant les modes d’adressage disponibles en page 0.

    Examinons un exemple dans lequel il s’agit d’utiliser la pile matériel pour une donnée d’entrée. Dans cet exemple, notre sous-routine transforme un demi-octet hexadécimal en octet ASCII, avec un octet en entrée et un octet en sortie, le tout stocké sur la pile matériel.

    <do_stuff>      ; Get the nybble into A.  Allowable value is 0-F.
    PHA             ; Other code can be put between the PHA and the PLA, as long
    JSR  NYB2ASCII ; as it doesn't care if NYB2ASCII overwrites A and X. The
    PLA             ; subroutine's input and output are protected on the stack though.
    <do_stuff>      ; Do something with the ASCII output, like add it to a string, display it, etc..

    La sous-routine NYB2ASCII pourrait ressembler à ceci.

    NYB2ASCII:

    TSX

    LDA  $103,X     ; 103 reaches past the return address, to the input parameter.

    CMP  #$0A       ; Anything below $0A will end up in the $30’s.

    CLC             ; CLC before the BMI so we only have to do it once.

    BMI  n2a1       ; For 9 or less, skip the next instruction.

    ADC  #7         ; $0A becomes $11, $0B becomes $12, etc..  C is still clear.

     n2a1:  ADC  #$30       ; Whether the 7 got added above or not, this gives the ASCII.

    STA  $103,X     ; Put it on the stack, overwriting the input value.  Note

    RTS             ; that we read and overwrote the byte just behind the

    ; return address, leaving the return address undisturbed.

    Voici un autre exemple dans lequel deux nombres de 16 bits non signés en entrée sur la pile, sont multipliés pour donner un nombre de 32 bits non signé, en sortie sur la pile.

    On empile tout d’abord les deux nombres à transformer, chacun avec le poids fort en premier, de sorte que le poids fort prenne l’adresse la plus haute comme dans le fonctionnement habituel du 6502.

    <do_stuff>     ; Get high byte of first input,

    PHA            ; and push it.

    <do_stuff>     ; Get low  byte of first input,

    PHA            ; and push it.

    <do_stuff>     ; Get high byte of second input,

    PHA            ; and push it.

    <do_stuff>     ; Get low byte of second input,

    PHA            ; and push it.

    JSR  UM_STAR   ; Now you can call the subroutine below that does the multiplying.

                   ; If you pull the product off the stack now, the byte order will be:

    PLA            ; 2nd-highest byte

    <do_stuff>

    PLA            ; high byte

    <do_stuff>

    PLA            ; low byte

    <do_stuff>

    PLA            ; 2nd lowest byte

    <do_stuff>

    Selon les opérations que vous souhaitez réaliser ensuite, vous pouvez très bien laisser le premier résultat sur la pile.

    Si l’opération après la multiplication des deux nombres est de calculer la racine carrée pour en obtenir la raison géométrique, alors il convient de ne pas toucher à la pile tant que cette seconde opération n’est pas effectuée (la raison géométrique équivaut à une moyenne logarithmique. La raison géométrique de 1 et 100 est de 10, et non de 50,5).

    Pour accéder aux données stockées sur la pile sans les dépiler et dans l’ordre de votre préférence, vous pouvez continuer d’utiliser X comme index de pile (ainsi que décrit dans l’exemple UM_STAR ci-dessous). En cas de TSX, vous devrez ajuster l’index en tenant compte du fait que l’adresse de retour n’est plus sur la pile, et comme décrit ci-après, qu’un PLA a déplacé les résultats sur la pile de $101,X à $104,X. Par contre, sans TSX l’index est toujours valide et pointe bien sur les bonnes données.

    UM_STAR: LDA #0                 ; Unsigned, mixed-precision (16-bit by 16-bit input, 32-bit output)

             PHA                    ; multiply.  Add a variable byte to the stack, initializing it as 0.

             TSX                    ; Now 101,X holds that new variable, $102,X and $103,X hold the return address

             LSR $107,X             ;and $104,X to $107,X holds the inputs and later the outputs.

             ROR $106,X

             FOR_Y  16, DOWN_TO, 0  ; Loop 16x.  The DEY, BNE in NEXT_Y below will drop through on 0.

                 IF_CARRY_SET

                     CLC

                     PHA            ; Note that the PHA (and PLA below) doesn’t affect the indexing.

                        LDA $101,X

                        ADC $104,X

                        STA $101,X

                     PLA

                     ADC $105,X

                 END_IF

                 ROR

                 ROR $101,X

                 ROR $107,X

                 ROR $106,X

             NEXT_Y

             STA $105,X

             PLA                    ; Retrieve the variable byte we added at the top, cleaning up the stack.

             STA $104,X             ; Again note that the PLA changed S but not X, so the $104 is still $104.

             RTS

    Note : Dans de nombreux cas, il serait plus judicieux de nommer les données présentes sur la pile, en utilisant la macro EQU, plutôt que de se contenter de $101,X ou équivalent.

    Supposons maintenant que vous ayez besoin d’une sous-routine avec quatre octets en entrée et six octets en sortie.

    Afin de servir de la pile matérielle pour deux données supplémentaires en sortie, il faut donc résoudre le problème de l’adresse de retour qui ne sera pas en haut de la pile lors du RTS.

    La meilleure méthode consiste à faire de la place sur la pile avant d’appeler la sous-routine :

    PHA              ; Push two dummy bytes onto the stack to hold the

    PHA              ; positions open for outputs of the subroutine called below.

    <do_stuff>       ; Prepare the subroutine input bytes to be passed.

    JSR  subroutine  ; Without changing the stack pointer, the subroutine

    ; can now give you two more bytes of output than input.

    Autre cas pratique, celui où vous auriez besoin de plus de données en entrées qu’en sortie.

    Dans ce cas, il suffit de dépiler les données inutiles après être sorti de la sous-routine.

    Ainsi, pour restaurer la hauteur de pile, faites :

    <do_stuff>        ; Set up inputs, reserve output byte places in stack, etc..

    TSX               ; Mark the current stack position.

    <do_more_stuff>

    JSR  <subroutine>

    <do_stuff>        ; Handle outputs, etc..

    TXS               ; Restore stack to the marker set earlier, possibly also

    ; to put certain outputs at the top.

    Évidemment le registre X ne devra pas être modifié par la sous-routine, ou alors il faudra le sauvegarder à son tour sur la pile.

    Comme décrit précédemment, la pile matériel n’est pas la seule solution en tant que pile de stockage. Une pile en page zéro (ZP) présente par exemple l’avantage de disposer de modes d’adressages supplémentaires.

    Utiliser une autre pile que la pile matériel permet aussi de résoudre le problème suivant : admettons que la pile matériel en page 1 serve pour passer des paramètres à une sous-routine qui elle-même doit fournir des paramètres à autre sous-routine. Lors de l’entrée dans la seconde sous-routine, l’adresse de retour se retrouve donc désormais au sommet de la pile, tandis que les données d’entrée sont décalées vers le bas, ce qui donnera à coup sûr des résultats erronés.

    Mettons en œuvre cet exemple avec la sous-routine GEOMEAN.

    GEOMEAN: JSR  UM_STAR  ; Multiply two 16-bit unsigned inputs on the stack.  Get 32-bit product.

             JSR  SQRT     ; Take a 32-bit input on the stack and get a 16-bit output, leaving two

             RTS           ; dummy bytes on stack (problem: GEOMEAN’s return addr is still on top!)

    Supposons que vous commenciez par empiler $1234 sur la pile matériel (poids fort en premier, sur le dessus de la pile), puis que vous empiliez $5678 avant d’appeler la sous-routine GEOMEAN.

    Lorsque le pointeur de programme attend la première instruction après PHA et TSX dans le programme UM_STAR, UM_STAR part à la recherche du premier nombre à multiplier ($1234 dans notre exemple), censé se trouver aux adresses $106,X et $107,X, puis du second nombre ($5678 dans l’exemple) qui se trouve aux adresses $104,X et $105,X. L’adresse de retour se trouve quant à elle indexée par $102,X et $103,X.

    Il y a cependant maintenant deux adresses de retour au sommet de la pile : celle qui a fait appel à UM_STAR et celle qui a fait appel à GEOMEAN. UM_STAR va donc ignorer $1234 et multiplier $5678 avec l’adresse de retour de GEOMEAN, entraînant ainsi un résultat erroné et l’écrasement de l’adresse de retour de GEOMEAN. C’est le plantage assuré !

    Vous vous apercevez également que le fait d’utiliser JMP au lieu de la paire d’instruction JSR / RTS peut entraîner des problèmes d’incompatibilité liés à une mauvaise gestion des données empilées sur la pile matériel.

    La pile en page zéro permet de contourner ce genre de problème, tout simplement parce que l’adresse de retour n’est pas empilée sur cette dernière.

    En plus de l’adressage de la page zéro qui est plus performant que celui de la pile matériel, signalons aussi les bénéfices suivants :

    • Inutile de replacer les paramètres d’entrée sur la pile sans arrêt, avec pour résultat immédiat une exécution plus rapide des étages de la pile ;
    • Réduction des effets liés à la récursivité (ce sujet sera abordé dans un prochain article) ;
    • Possibilité de placer plusieurs type (et quantité) de valeurs ;
    • Code plus compact qui permet un meilleur fonctionnement car plus besoin de devoir empiler/dépiler des paramètres lors de l’appel de sous-routines.

    Revenons maintenant à notre sous-routine qui transforme un demi-octet hexadécimal en octet ASCII et voyons comment réaliser cette transformation en utilisant une pile ZP.

    X servira de pointeur de pile, initialisé en début de programme pour indexer de la manière suivante $00,X.

    Pour empiler en page zéro, il faut donc faire un DEX puis stocker en $0,X.

    Voici ce que donne la routine modifiée pour la pile ZP :

    <do_stuff>      ; Get the nybble into A.  Allowable value is 0-F.

    DEX

    STA  0,X        ; Other code can be put between the STA and the LDA, and

    JSR  NYB2ASCII  ; NYB2ASCII’s input and output are protected on the stack.

    LDA  0,X        ; X must be preserved as the data stack pointer of course.

    INX

    <do_stuff>      ; Do something with the ASCII output, like add it to a string, display it, etc..

    Note : Si vous avez besoin du registre X dans le reste de votre programme, il vous faudra le sauver sur la pile matériel.

    La sous-routine NYB2ASCII devient :

    NYB2ASCII:              ; (The initial TSX is no longer necessary.)

            LDA  0,X        ; Get the input parameter from the data stack.

            CMP  #$0A       ; Anything below $0A will end up in the $30’s.

            CLC             ; CLC before the BMI so we only have to do it once.

            BMI  n2a1       ; For 9 or less, skip the next instruction.

            ADC  #7         ; $0A becomes $11, $0B becomes $12, etc..  C is still clear.

     n2a1:  ADC  #$30       ; Whether the 7 got added above or not, this gives the ASCII.

            STA  0,X        ; Put it on the data stack, overwriting the input value.  Note

            RTS             ; that the return address is not on this stack to worry about.

    Procédons selon la même logique pour la sous-routine UM_STAR.

    UM_STAR: DEX                    ; Add a stack byte to use as a temporary variable.

             LDA  #0

             STA  0,X               ; 0,X addresses the temporary variable.  These are in ZP.

             LSR  4,X

             ROR  3,X

             FOR_Y  16, DOWN_TO, 0

                 IF_CARRY_SET       ; The 1st time thru the loop, A needs 0; so don’t use STZ above.

                     CLC

                     PHA

                        LDA  0,X

                        ADC  1,X

                        STA  0,X

                     PLA

                     ADC  2,X

                 END_IF

                 ROR

                 ROR  0,X

                 ROR  4,X

                 ROR  3,X

             NEXT_Y

             STA  2,X

             LDA  0,X

             STA  1,X

             INX                    ; Take back the stack byte we used as a temporary variable.

             RTS

     ;——————

    Ici, un octet est empilé sur la pile ZP grâce à l’instruction DEX (puis dépilé grâce à INX), de la même manière que pour PHA (puis PLA). La pile ZP permet donc d’éviter d’avoir à sauver/restaurer l’index X.

    Il est en outre possible de se passer des instructions DEX/INX en adressant le nouvel octet grâce à $FF,X qui permet de rester en page zéro et qui économise au passage 2 octets et 4 cycles d’horloge.

    Assurez-vous cependant de n’avoir aucune sous-routine ou interruption susceptible d’écraser cet octet, car X semble indiquer que cet emplacement est libre.

    La sous-routine GEOMEAN quant à elle, peut désormais s’écrire :

    GEOMEAN: JSR  UM_STAR  ; Multiply two 16-bit unsigned inputs on the stack and get a 32-bit product.

             JMP  SQRT     ; (JSR, RTS)  Take a 32-bit input on the stack and get a 16-bit output.

     ;——————

    Voici dans le détail les avantages à travailler avec des piles séparées :

    • Le nombre d’octets entrés est indépendant du nombre d’octets sortis. En d’autres termes, nul besoin de nettoyer les octets de sortie factices ni de placer des octets factices sur la pile ;
    • En disposant de données séparées, les adresses de retour ne sont pas traitées comme des données. Le paramétrage devient implicite ;
    • Une sous-routine peut faire appel à une autre sous-routine sans avoir à faire le ménage sur la pile matériel (qui devient désormais la pile de retour) ;
    • La pile matériel est maintenant disponible pour d’autres usages tels que la gestion des limites et des index de boucles (y compris des boucles imbriquées) ;
    • L’ajout d’une nouvelle pile permet d’augmenter l’espace de stockage et d’éviter un crash sur la pile matériel ;
    • Grâce aux modes d’adressages du 6502 en page zéro (ZP,X), nous pouvons adresser la pile ZP très facilement.

    Voyons justement comment mettre à profit les modes d’adressage ZP,X dans l’exemple de routine de déplacement suivante.

    Nous appellerons cette routine CMOVE, pour “character move”.

    Avant de faire tourner cette routine, nous plaçons six octets sur la pile ZP, sous la forme de 3 nombres de 16 bits (poids fort sur l’adresse la plus haute dans chaque cas) :

    • Adresse de départ
    • Adresse de destination
    • Taille de mémoire à déplacer (pouvant aller jusqu’à quelques dizaines de milliers d’octets)

    CMOVE:  LDA  0,X      ; “See-move” Character (memory) move  ( from to len — )

            ORA  1,X

            BEQ  POP3     ; If remaining length is 0, branch to POP3 which is just 6 INX’s, then RTS.

            LDA  (4,X)    ; Get a byte and

            STA  (2,X)    ; transfer it.

            INC  4,X

            BNE  cm1$

            INC  5,X      ; Increment the source addr

     cm1$:  INC  2,X

            BNE  cm2$

            INC  3,X      ; and the destination addr,

     cm2$:  DEC  0,X      ; decrement the count left,

            LDA  0,X

            CMP  #$FF

            BNE  CMOVE

            DEC  1,X

                          ; and go back up for another loop.  If we’re done,

            BRA  CMOVE    ; that fact will be caught in the first three lines (BRA is for 65c02 only and stands for Branch Always).

    Quand faut-il éviter l’emploi d’une pile pour l’usage de paramètres d’entrées / sorties dans des sous-routines ?

    Déplacer des données dans une pile requiert du temps processeur.

    Aussi, si vous devez manipuler une grande quantité de données, comme une chaîne alphanumérique, un tableau ou même une matrice, placez là ailleurs en mémoire et indiquez son adresse de stockage sur la pile.

    RÉCURSIVITÉ

    Une sous-routine peut présenter la particularité d’être récursive, à savoir qu’elle peut s’appeler elle-même à de nombreuses reprises.

    Afin d’éviter que ce genre de sous-routine ne reboucle indéfiniment, entraînant une surcharge de la pile matériel, il faut prévoir une condition qui permette de quitter la sous-routine au bout de x boucles récursives.

    Lorsque cette (ou ces) condition(s) est respectée, on quitte alors la sous-routine par une série de RTS.

    Chaque sous-routine nidifiée (imbriquée) peut disposer de ses propres variables ayant  pourtant des noms semblables.

    Prenons l’exemple de la fonction de Fibonacci qui est utilisée en calculs financiers, lors de la génération de nombres pseudo aléatoires, dans des algorithmes de tri et de recherche ou encore pour la compression de fichiers audio.

    fiboIO:    SETL $103    ; For clarity, give names to the locals.

    fiboTemp:  SETL $102    ; (Record of X will be at $101,X.)

    FIBO:  CMP  #2          ; Test the input.

           BCC  end         ; If it’s 0 or 1, so is the output, so just end.

                            ; This prevents endless nesting, too.  Otherwise,

           PHA              ; create local variable fiboIO, and store A there.

           PHA              ; Create another local variable byte, fiboTemp.

                            ; (Its initial value doesn’t matter.)

           PHX              ; Push X too, to protect the calling routines.

           TSX              ; Prepare X to use for stack-relative addressing.

           DEA              ; Get the n-1 term

           JSR  FIBO        ; and calculate the number for it (recursing here)

           STA  fiboTemp,X  ; and store its result in local variable fiboTemp.

           LDA  fiboIO,X

           DEA

           DEA              ; Now get the n-2 term

           JSR  FIBO        ; and calculate the number for it (recursing here).

           CLC

           ADC  fiboTemp,X  ; Add those two together, and

           STA  fiboIO,X    ; store the answer.  Don’t forget the “,X”!

                            ; Note that there is no looping.

           PLX              ; Restore X for the calling routines.

           PLA              ; Pull fiboTemp off the stack and discard it.

           PLA              ; Pull the answer (fiboIO) off the stack into A.

    end:   RTS

    Cette sous-routine tient sur 34 octets et n’utilise pas la RAM pour le stockage, hormis bien sûr la pile matériel.

    La donnée d’entrée doit rester inférieure à 14 car la donnée de sortie est sur 8 bits et le nombre de Fibonacci pour 13 est 233.

    Avec une réponse sur deux octets ou plus, nous pourrions avoir une donnée d’entrée plus grande sans manquer de place sur la pile, même si ce genre de sous-routine en consomme beaucoup.

    La place disponible sur la pile est largement suffisante pour des sous-routines simples, mais rappelons que les routines récursives ne sont pas des routines normales.

    À suivre.

  • Fonctionnement de la pile matérielle du 6502 (1/8)

    EN GUISE D’INTRODUCTION

    Fasciné par certaines utilisations détournées de la pile de matériel du 6502, telles que les programmes de protections, j’ai voulu en apprendre davantage sur son fonctionnement.
    Je vous propose donc ici la traduction partielle d’un sujet dont la version originale se trouve à l’adresse suivante : http://wilsonminesco.com/stacks/

    QUELQUES DEFINITIONS

    Le type de pile dont il est question ici est une partie de la mémoire vive utilisée pour le stockage temporaire de données, qu’il s’agisse de variables ou d’adresses, avec comme mode d’accès, dernier entré, premier sorti (Last In First Out).
    On compare généralement ce type de pile à une pile d’assiettes dans un meuble à ressorts que l’on peut trouver à côté d’un buffet restaurant.

    La quantité de compression ou d’étirement des ressorts est proportionnelle au poids des assiettes que le meuble contient, de sorte que tant que le meuble n’est pas rempli complètement, un employé peut ajouter n’importe quel nombre d’assiettes récemment lavées et les assiettes seront toujours dans la même position au sommet, prêtes pour le prochain client.

    D’habitude, ni l’employé ni le client n’ont besoin de savoir combien d’assiettes chaudes sont stockées en dessous.

    On peut aussi prendre l’exemple d’une tige métallique avec un tas de papiers embrochés.  Vous pouvez voir les documents qui sont sous le sommet, et vous pourriez même lire ou écrire sur eux dans une certaine mesure sans retirer ceux qui sont au-dessus, mais le retrait ou l’ajout réel de documents a toujours lieu par le sommet.

    Le contraire d’une pile (ou mémoire LIFO) serait une mémoire de type premier entré, premier sorti (FIFO) ou file d’attente, qui peut dans ce cas être comparée au convoyeur d’une machine à rayons X dans un aéroport. Sur cette machine, les affaires passent d’un côté à un autre des rayons X, dans le même ordre qu’elles sont entrées.

    Contrairement à la pile d’assiettes, une pile de nombres dans la mémoire de l’ordinateur ne se déplace pas de haut en bas quand quelque chose est ajouté ou enlevé de la pile. Il serait en effet contre-productif de déplacer toutes ces données vers le haut ou vers le bas comme pour les assiettes, alors qu’il est plus simple d’utiliser un pointeur réglable pour savoir où se trouve le haut ou, en fait, dans le cas de la pile matériel du 6502, quelle est l’adresse RAM qui est la prochaine disponible pour mettre quelque chose sur la pile.

    Ce pointeur est le registre S (Stack pointer) du 6502.

    Dès lors, si l’analogie du distributeur d’assiettes ne tient plus, pourquoi l’évoquer ?

    En fait, ce qu’il convient surtout de retenir ici, c’est qu’habituellement, on ne s’occupe que de ce qui se trouve au-dessus de la pile, sans se soucier de la profondeur de celle-ci, comme un navire en pleine mer qui flotte de la même manière que la profondeur de l’eau soit de 100 mètres ou de 1000 mètres. Tant qu’une action ou une routine dispose des éléments de la pile dont elle a besoin, peu importe combien d’autres éléments sont plus bas dans la pile, en attendant d’être utilisés par d’autres routines ou parties d’une routine.

    La pile du processeur 6502 permet de stocker des adresses pour des sous-routines et des interruptions (moins fréquentes que pour des sous-routines) ainsi que des variables.

    C’est dans la première page du 6502 ($0100-$01FF) que réside la pile de matériel. Les tests montrent d’ailleurs que contrairement à une légende urbaine, cette zone mémoire est beaucoup plus grande que ce dont le programmeur a réellement besoin.

    Le pointeur de la pile de matériel est le registre S qui n’a que 8 bits de largeur; le 6502 ajustant automatiquement le numéro de page à $01 pour tous les accès à la pile. La pile de matériel en page 1 se développe vers le bas, et non vers le haut. Toute routine de réinitialisation doit donc initialiser le pointeur de pile au sommet de l’espace disponible. Il s’agit habituellement de l’adresse $01FF. Cette routine s’écrira simplement :

    LDX  #$FF ; Initialise X au sommet de la pile
    TXS ; Transfère dans le pointeur de pile

    Puisque la pile se développe vers le bas, le pointeur de pile se décrémente après chaque octet poussé sur la pile, et s’incrémente avant que chaque octet ne soit tiré hors de la pile.

    ADRESSES DE SOUS-ROUTINE ET NIDIFICATION

    Une sous-routine est une partie de votre programme dont vous avez besoin à de multiples reprises et à différents endroits. Au lieu d’avoir une copie de cette routine à chaque endroit où vous en avez besoin, vous l’écrivez une fois pour toutes et vous l’appelez depuis la portion de programme requise. Le compteur de programme se rend à cette sous-routine puis doit pointer de nouveau sur la routine d’appel pour reprendre l’exécution. Pour savoir où revenir, le compteur de programme (donc l’adresse de retour) a au préalable été sauvée sur la pile.

    C’est ce que réalise l’instruction JSR : « Jump, Saving Return address » (JSR est habituellement traduit par « Jump to SubRoutine », ce qui fonctionne tout aussi bien mais n’est pas tout à fait exact.) Supposons maintenant que cette routine doive en appeler d’autres. Comme chaque JSR met l’adresse de retour sur la pile, les adresses de retour nécessaires en premier lieu seront les dernières stockées sur la pile.

    La nidification est ce qui convient de mieux pour ce genre de situation. On appelle nidification (ou imbrication), le fait qu’une sous-routine en appelle une autre, qui à son tour en appelle une autre, et ainsi de suite. Si vous n’aviez pas besoin de la nidification, vous pourriez inscrire l’adresse de retour dans une variable ou un registre; mais vous vous apercevez que dès la première nidification, la variable serait écrasée et la première adresse de retour serait perdue.

    C’est là que la pile entre en jeu.

    Considérons à présent la nidification des sous-routines ci-dessous.

    Dans ce scénario, la pile contiendra les nombres hexadécimaux suivants aux points  (a), (b) et (c) ; (sans compter ce qui aurait déjà pu être sur la pile avant (a).

      (a)(b)(c)
    Sommet pile (*) $1915 $2632 $2F43
        $1915 $2632
          $1915

    (*) Top Of Stack (TOS)

    Ce tableau semble comporter des erreurs. En effet, au point (a)  l’adresse de la prochaine instruction est censée être $1916, et non $1915.

    En fait, l’instruction RTS (Return From subroutine) incrémentera l’adresse de retour et l’exécution du programme reprendra bien à $1916 (ce qui n’est pas le cas pour RTI, ReTurn-from-Interrupt, et fera l’objet d’un autre article).

    Puisque le bus de données et la mémoire du 6502 sont adressés sur 8 bits, chaque adresse de retour évoquée ci-dessus nécessite deux emplacements dans la pile. Lorsque JSR pousse l’adresse de retour (moins 1) sur la pile, il pousse d’abord l’octet haut, puis l’octet bas. N’oubliez pas que la pile grandit, et que le pointeur de la pile se décrémente lorsque les octets sont poussés sur la pile, de sorte que l’ordre d’octet résultant est l’octet de poids faible dans l’adresse mémoire la plus basse, comme le sont les autres adresses dans le 6502.

    Supposons par exemple que la valeur du pointeur de pile (registre S) était $EF avant que le code n’entre dans le diagramme de pile ci-dessus. Cela donnerait une occupation de l’espace mémoire de la pile selon le tableau suivant (ne sachant pas ce qui figurait de $EF à $FF, ces octets ne sont pas cartographiés).

    Lorsque vous quittez les sous routines et que vous revenez à la routine principale, voici à quoi ressemble la mémoire de la pile.

    Les nombres entre parenthèses sont toujours là, mais cette partie de la mémoire de la pile est libérée. Retirer quelque chose de la pile signifie simplement que le processeur en lit le contenu et incrémente ensuite le pointeur de la pile. Pouvez-vous utiliser ces chiffres, puisqu’ils sont toujours là ? Si vous avez des interruptions, la réponse est non, car une interruption pourrait survenir à tout moment et les écraser. Cela violerait le protocole LIFO et le processus des interruptions nécessite que ce protocole soit respecté.

    À suivre.