Clavier, son et souris
Parmi les possibilités offertes pour les fonctionnalités du matériel IBM PC du QuickC, nous aborderons trois sujets : le clavier, le générateur de sons du compatible IBM PC et la souris. En commençant par le clavier, nous verrons comment nous pouvons utiliser à la fois les routines du BIOS en ROM et les fonctions intégrées fournies par QuickC pour lire les codes de caractères. Les outils de clavier que nous construisons nous permettront de prendre le contrôle du clavier afin que nous puissions construire y compris les touches de fonction et les touches de déplacement du curseur. Ou bien sûr, nous devrons être capables de lire les touches étendues afin de pouvoir développer les outils de l'interface utilisateur - fenêtres contextuelles et menus par exemple.
En plus de fournir des routines pour contrôler le clavier, le QuickC permet la générateur de sons et écrirons des routines pour produire des sons et la souris. Bien que la souris ne soit pas réellement intégrée au PC, il s'agit d'une fonctionnalité complémentaire importante pouvant grandement améliorer les interfaces utilisateur. Le QuickC permet de faire fonctionner la souris en utilisant un appel d'interruption.
Travailler avec le clavier
Nous allons commencer par étudier comment nous pouvons contrôler le clavier du IBM PC à l'aide de l'interruption ROM du BIOS 16h. Bien que QuickC fournisse une fonction nommée _bios_keybrd pour appeler les routines du clavier BIOS, il est possible d'utiliser des fonctions du BIOS pour lire les touches ASCII standard et il est possible d'utiliser _bios_keybrd pour construire un ensemble d'outils d'entrée/sortie. Le BIOS en ROM fournit trois fonctions pour lire les données du clavier. Les tâches effectuées par ces fonctions sont :
- Déterminer si une touche a été enfoncée;
- Lire le code de numérisation d'un caractère et le code ASCII;
- Déterminer l'état du clavier.
Explorons la première tâche
Lorsque vous saisissez une touche, les données représentant cette touche sont entreposées dans une mémoire tampon du clavier. Une fois qu'une touche est entreposée dans le tampon, elle y reste jusqu'à ce qu'elle soit lue avec un appel de fonction ROM du BIOS. Le tampon du clavier est maintenu par DOS; ainsi, nous n'avons pas à nous soucier de la façon dont les touches sont entreposées dans le tampon. Le BIOS en ROM fournit cependant une fonction pour déterminer s'il y a des touches dans le tampon. La description de cette fonction est :
Interruption | 16h |
---|---|
Description | Détermine si une touche a été enfoncée |
Entrées | AH=1 |
Retours | Le bit de drapeau zéro dans le registre de drapeau 8088. Si le bit de drapeau est défini sur 0, cela indique qu'une touche a été enfoncée. Si une touche a été
enfoncée. Si une touche a été frappée, cette fonction renvoie également : AH = Le code de position du caractère AL = Le code ASCII du caractère |
Notez que si le tampon du clavier est vide - une indication qu'une touche n'a pas été enfoncée depuis que la dernière touche a été extraite du tampon - le bit de drapeau zéro est défini sur 1 et la valeur dans les registres AH/AL est indéfinie. Mais maintenant, vous vous demandez peut-être quel est le bit de drapeau zéro et quel est le registre de drapeau ? La famille de microprocesseurs 8088 fournit un registre pour suivre les effets secondaires des opérations arthmétiques. Ce registre, appelé registre des drapeaux, contient 16 bits, dont 9 sont utilisés pour représenter différentes conditions telles qu'un débordement ou une retenue. L'organisation de ce registre est construit de la façon suivante :
Bit | Nom | Description |
---|---|---|
0 | CF | Ce bit permet d'indiquer le drapeau de retenue |
1 | - | Ce bit vaut 1 |
2 | PF | Ce bit permet d'indiquer le drapeau de parité |
3 | - | Ce bit vaut 0 |
4 | AF | Ce bit permet d'indiquer le drapeau d'ajustement. |
5 | - | Ce bit vaut 0. |
6 | ZF | Ce bit permet d'indiquer le drapeau du zéro. |
7 | SF | Ce bit permet d'indiquer le drapeau de signe. |
8 | TF | Ce bit permet d'indiquer le drapeau de trappe. |
9 | IF | Ce bit permet d'indiquer le drapeau d'interruption. |
10 | DF | Ce bit permet d'indiquer le drapeau de direction. |
11 | OF | Ce bit permet d'indiquer le drapeau de débordement. |
12 à 13 | IOPL | Ces bits permettent d'indiquer le champ de niveau de privilège d'entrée/sortie. |
14 | NT | Ce bit permet d'indiquer le drapeau de tâche chainée. |
15 | - | Ce bit vaut 0. |
Bien sûr, le bit nous préoccupant le plus maintenant est le bit de drapeau zéro (ZF). Lorsque l'interruption 16h (code de fonction 1) est exécutée, le bit de drapeau est défini pour indiquer l'état du tampon du clavier.
Heureusement, le QuickC fournit une variable spéciale pour représenter le registre des drapeaux ; ainsi nous pouvons tester le sixième bit de cette variable pour déterminer s'il y a une touche dans le tampon du clavier. La variable appelée cflag est incluse dans l'union REGS étant définie dans dos.h. Ainsi pour accéder au registre de drapeaux, on utilise la syntaxe :
regs.x.cflag |
A illustré, il s'agit de la même technique utilisée pour accéder à l'un des autres registres de mots. Maintenant que nous avons discuté du tampon du clavier et du registre des drapeaux, écrivons une fonction pour déterminer si une touche a été enfoncée. Cette fonction, checkingKey, renvoie une valeur pour indiquer l'état du tampon du clavier. Si une touche est disponible, checkingKey renvoie également le code de position et l'ASCII de la touche :
- int checkingKey(int *k) {
- union REGS regs;
- regs.h.ah = 1;
- int86(0x16,®s,®s);
- *k = regs.x.ax;
- if(regs.x.flags & 0x0040) return(0);
- else return(1);
- }
Notez que l'instruction (regs.x.flags & 0x0040) est utilisée pour tester le bit de drapeau zéro. L'opérateur C ET au niveau du bit & effectue une opération ET avec le bit 9 et la valeur 1.
Gardez à l'esprit que si une touche est disponible, cet appel de fonction ne supprime pas la touche du tampon du clavier. Afin d'effacer la touche, nous devons utiliser l'interruption 16h (code de fonction 0). Cette interruption est définie comme :
Interruption | 16h |
---|---|
Description | Lecture d'une touche |
Entrées | AH=0 |
Retours | AH = Le code de position du caractère AL = Le code ASCII du caractère |
Lorsque cette interruption est appelée, la première touche enfoncée est lue dans la mémoire tampon du clavier. Si une touche n'est pas disponible, cette routine du BIOS attend qu'une touche soit enfoncée. La valeur de la touche est retournée dans le registre AX. Mais sachez que chaque touche est représentée par une valeur de deux octets. Le premier octet entreposé dans le registre AL représente le code ASCII du caractère. Les codes ASCII sont normalisés et vous pouvez les trouver dans la plupart des ouvrages de référence. L'autre octet renvoyé est le code de position du caractère.
À ce stade, vous vous demandez peut-être pourquoi nous devons nous préoccuper des codes de position. Après tout, lorsqu'une touche est enfoncée, nous recevons toujours son code ASCII associé. Malheureusement, de nombreuses touches du clavier du PC n'ont pas de codes ASCII prédéfinis ; ainsi, nous devons utiliser le code de position pour déterminer quelle touche a été enfoncée.
Les touches entrant dans cette catégorie sont les touches de fonction, les touches de position du curseur, les touches Home, End, Page Down, les touches Insert et Delete, les combinaisons de touches Alt et les combinaisons de touches Ctrl. Chaque fois qu'une de ces touches est sélectionnée, le code ASCII est défini sur la valeur 0. Par conséquent, si nous appelons l'interruption du BIOS en ROM pour lire une touche et que la valeur renvoyée dans le registre AL est 0, nous savons que l'un des touches spéciales du PC ont été sélectionnées.
Les codes de position sont représentés à l'aide de nombres décimaux. Nous pouvons utiliser ces valeurs pour définir des variables pour représenter les différentes touches. Par exemple, les instructions de définition suivantes peuvent être utilisées pour représenter les touches de fonction F1 à F10 :
- #define F1_KEY 0x3b00
- #define F2_KEY 0x3c00
- #define F3_KEY 0x3d00
- #define F4_KEY 0x3e00
- #define F5_KEY 0x3f00
- #define F6_KEY 0x4000
- #define F7_KEY 0x4100
- #define F8_KEY 0x4200
- #define F9_KEY 0x4300
- #define F10_KEY 0x4400
Dans ce cas, chaque définition consiste en une valeur hexadécimale à quatre chiffres. Les deux premiers chiffres représentent le code de position en hexadécimal et les deux derniers chiffres représentent le code ASCII. Nous pouvons également représenter n'importe laquelle des touches standard en utilisant cette technique. Par exemple, voici la définition de la touche d'échappement (Esc) :
- #define ESC_KEY 0x11b
Ici, le code de position est 01h et le code ASCII est 1Bh étant déterminé en recherchant la touche d'échappement dans une table ASCII. Maintenant que nous avons expliqué comment les touches sont représentés, écrivons une fonction pour appeler l'interruption du BIOS en ROM pour lire une touche. Cette fonction, readKey, renvoie le code de numérisation complet pour une touche enfoncée ainsi que le code de code ASCII pour une référence rapide :
Notez également que cette fonction teste la touche lue pour déterminer si la touche est la combinaison Ctrl+C. Si c'est le cas, la fonction se terminera en appelant la fonction exit. Le test de la combinaison Ctrl+C est réalisé en utilisant la macro lo étant définie comme ceci :
- #define lo(f) ((f) & 0xff )
cette macro teste l'octet de poids faible de la clé lue. Le test de la combinaison Ctrl+C est une fonctionnalité utile car elle permet à l'utilisateur de terminer rapidement un programme. Vous pouvez également modifier cette fonction afin que si la combinaison Ctrl+C est lue, readKey demandera d'abord à l'utilisateur s'il souhaite abandonner. Par exemple, cette tâche peut être accomplie avec le code suivant :
La routine du clavier QuickC
Nous pouvons également créer des routines pour contrôler le clavier en utilisant la fonction _bios_keybrd fournie par QuickC. Cette routine est déclarée dans bios.h comme suit :
unsigned _bios_keybrd(unsigned service); |
Le paramètre service définit quel appel de fonction de l'interruption 16h est utilisé lorsque _bios_keybrd est appelé. Le QuickC propose trois options présentées dans le tableau suivant :
Option | Description |
---|---|
_KEYBRD_READ | Appelle la fonction 0 pour lire une touche dans le tampon. Si aucune touche n'a été saisie, la fonction en attendra une. |
_KEYBRD_READY | Appelle la fonction 1 pour voir si une touche est dans le tampon. |
_KEYBRD_SHIFT_STATUS | Appelle la fonction 2 pour obtenir l'état actuel de la touche Shift. |
Nous pouvons utilisez les deux premiers paramètres constants pour réécrire nos fonctions de clavier checkKey et readKey. Notez que nous pouvez aussi ajouté une nouvelle fonction readKeyExt étant une variante de readKey. Cette fonction n'accepte que les touches d'entrée n'étant pas des touches étendues :
- char readKeyExt(void) {
- char c;
- int k;
- for(; ( (c = readKey(&k)) == 0););
- return c;
- }
Il s'agit d'une fonction utile si vous souhaitez limiter les types de touches de saisie que l'utilisateur peut sélectionner.
Construire des outils sonores
Nous pouvons contrôler le générateur de sons intégré d'un compatible IBM PC en lisant et en écrivant sur les ports d'entrée/sortie du compatible IBM PC. Pour cette tâche, nous utiliserons les fonctions QuickC intégrées inp et outp étant prototypées dans le fichier d'entête conio.h comme ceci :
int inp(unsigned port); int outp(unsigned port, int databyte); |
La fonction inp lit un octet à partir d'un port d'entrée spécifié et outp écrit un octet sur un port de sortie spécifié. Afin de générer des sons, nous devons effectuer les quatre étapes suivantes :
- Réglez la fréquence du générateur de sons ;
- Allumez le haut-parleur ;
- Définir un délai ;
- Éteignez le haut-parleur.
La fonction de haut niveau que nous utiliserons pour effectuer ces actions s'appelle sound, et elle est codée comme suit :
Ici sound prend deux paramètres, la fréquence du son que l'on souhaite générer en Hertz et la durée du retard en millisecondes. Cette fonction effectue les tâches nécessaires en appelant les quatre fonctions setspeakerfreq, makenoise, delay et silence. Le paramètre freq définit la fréquence du générateur de son. Cette valeur est spécifiée en Hertz. Par exemple, l'appel :
- sound(100,1000);
allumerait le haut-parleur à une fréquence de 100 Hertz. Bien sûr, une fois l'enceinte allumée, elle restera allumée pendant la durée du délai que nous spécifions. Si le délai n'est pas assez long, vous n'entendrez pas le son. En revanche, si le délai est trop long, vous serez constamment rappelé. Étant donné que les compatibles IBM PC fonctionnent à de nombreuses vitesses différentes, vous devrez expérimenter avec la valeur de retard pour obtenir l'effet souhaité. Vous pouvez utilisez la macro suivante pour émettre un bip :
- #define beep sound(1250,200);
Cette macro peut être utilisée conjointement avec nos routines d'entrée. Chaque fois qu'une touche invalide est enfoncée, nous pouvons appeler la fonction bip pour avertir l'utilisateur.
Contrôler le haut-parleur
La première étape pour produire un son consiste à régler la fréquence du haut-parleur. Cette tâche est accomplie en envoyant des données à l'un des ports de sortie d'un compatible IBM PC. La compatible IBM PC fournit en fait un certain nombre de ports de sortie pour contrôler les interruptions, régler la puce de minuterie 8253 et contrôler d'autres périphériques et matériels tels que le clavier et la RAM. Pour définir la fréquence du haut-parleur, nous n'avons qu'à écrire sur le port 43h accédant au registre de commande de la minuterie, comme illustré dans le tableau suivant :
Bits 7 à 6 | Bits 5 à 4 | Bits 3 à 1 | Bits 0 |
---|---|---|---|
00 - canal 0 01 - canal 1 10 - canal 2 |
00 - compteur de verrou 01 - MSB seulement 10 - LSB seulement 11 - LSB, MSB |
000 - Mode 0 001 - Mode 1 010 - Mode 2 011 - Mode 3 100 - Mode 4 101 - Mode 5 |
0 - Binaire 1 - BCD |
Les quatre premiers bits du registre définissent le mode de la puce de minuterie 8253 et les quatre premiers bits sont utilisés pour sélectionner le canal de minuterie et le mode de lecture/écriture.
Essentiellement, nous devons charger la minuterie avec la valeur de fréquence du haut-parleur, puis cette valeur est utilisée par la minuterie pour contrôler la façon dont le son est généré. La première étape consiste à indiquer au chronomètre quel canal et quel mode doivent être utilisés. Nous utiliserons le canal 2 et le mode 4 ; ainsi, nous devons envoyer la valeur B6h au registre de commande du temporisateur (port 43h). Cette tâche est effectuée par le code de fonction suivant :
- outp(0x43,0xb6);
Une fois le registre de commande initialisé, nous pouvons entreposer la fréquence en écrivant sur le port 42h comme indiqué :
Notez qu'il s'agit d'un processus en deux étapes. Le premier appel à outp charge l'octet de poids faible de la valeur de fréquence et le second appel charge l'octet de poids fort. Le port 42h accède au canal 2 de la puce de temporisation 8252. Étant donné que le canal est représenté sous la forme de deux registres de verrouillage à 8 bits, toutes les opérations de lecture et d'écriture doivent être effectuées en accédant d'abord à l'octet de poids faible, puis à l'octet de poids fort. La valeur de fréquence étant entreposée dans ces deux registres fonctionne comme un compteur pour contrôler le haut-parleur. Une fois la fréquence réglée, on peut allumer le haut-parleur pour générer un son. Cette tâche est effectuée par la fonction suivante :
Dans ce cas, les deux premiers bits du port 61h sont utilisés pour allumer le haut-parleur. Le premier bit sélectionne la minuterie et le second bit sélectionne les données du haut-parleur. Par conséquent, notez que nous lisons d'abord le contenu du port 61h, puis définissons les deux premiers bits en associant par un OU la valeur entreposée dans le port avec l'expression i | 3.
Une fois le haut-parleur allumé, il restera allumé jusqu'à ce que nous l'éteignons. Cela se fait en définissant les deux premiers bits du port 61h sur 0. Par conséquent, pour contrôler le haut-parleur, nous avons également une fonction pour définir un délai avant d'éteindre le haut-parleur. Cette tâche est effectuée par la routine delay étant déclarée comme suit :
- int delay(int del);
Le paramètre del spécifie le délai en millisecondes. Lorsque delay est appelé, le programme en cours est suspendu pendant la durée du délai indiqué. Une fois cette fonction terminée, la dernière étape consiste à éteindre le haut-parleur en appelant silence, comme indiqué ci-dessous :
Notez que cette fonction est similaire à makenoise sauf qu'ici nous mettons les deux premiers bits du port 61h à 0 en utilisant l'expression i & 0xfc. Cela nous permet d'éteindre le haut-parleur sans affecter aucun des autres périphériques contrôlés par le port 61h.
Travailler avec la souris
La souris est une composante important de l'interface utilisateur pouvant grandement améliorer les programmes que nous écrivons. Étant donné que le compatible IBM PC est essentiellement piloté par un clavier, il est nécessaire de prendre en charge la souris de manière à ce que le clavier puisse également être utilisé.
L'interruption de la souris
Lorsqu'une souris compatible Microsoft est installée dans un compatible IBM PC, un ensemble de fonctions de souris est affecté à l'interruption 33h. Ces routines sont fournies avec le pilote de la souris et sont accessibles à l'aide de la fonction int86 de QuickC. Les fonctions de la souris vont de tâches simples telles que l'initialisation de la souris à des tâches complexes telles que le chargement de routines d'interruption.
Lorsque l'interruption 33h est appelée, les registres AX, BX, CX et DX sont utilisés pour passer des paramètres. La valeur dans le registre AX détermine quelle fonction de la souris est appelée. Le tableau suivant liste la plupart des fonctions principales :
Code de fonction (AX) | Description |
---|---|
0 | Demande l'état de la souris et initialiser la souris |
1 | Afficher le curseur de la souris |
2 | Masquer le curseur de la souris |
3 | Demande l'état du bouton et la position de la souris |
4 | Déplacez le curseur de la souris sur la ligne, la position de la colonne |
5 | Demande des informations sur les boutons |
6 | Demande des informations sur le relâchement des boutons |
7 | Définir l'intervalle de coordonnées horizontales du curseur |
8 | Définir l'intervalle de coordonnées verticales du curseur |
9 | Définir le style du curseur graphique |
10 | Sélectionnez le curseur de texte matériel ou logiciel |
11 | Charger la routine d'interruption |
12 | Charger la route d'interruption |
13 | Activer l'émulation du crayon lumineux |
14 | Désactiver l'émulation du crayon lumineux |
15 | Définir le rapport mickey/pixel de la souris |
La technique d'appel d'une routine de souris est la même que celle que nous avons utilisée pour appeler n'importe laquelle des routines vidéo du BIOS, sauf que nous utilisons maintenant l'interruption 33h. Voici le format d'un appel de fonction :
- int86(0x33,&inregs,&outregs);
Pour accéder à l'une des routines de la souris, nous utiliserons la fonction suivante :
- void mouse(int *m1, int *m2, int *m3, int *m4) {
- union REGS inregs, outregs;
- inregs.x.ax = *m1;
- inregs.x.bx = *m2;
- inregs.x.cx = *m3;
- inregs.x.dx = *m4;
- int86(0x33), &inregs, &outregs);
- *m1 = outregs.x.ax;
- *m2 = outregs.x.bx;
- *m3 = outregs.x.cx;
- *m4 = outregs.x.dx;
- }
Notez que l'interruption de la souris prend 4 paramètres et renvoie 4 paramètres. Les paramètres sont passés dans les paramètres m1, m2, m3 et m4. Voyons maintenant comment nous pouvons utiliser cette fonction pour contrôler la souris.
Initialisation de la souris
Pour utiliser la souris, nous devons l'initialiser. Il s'agit d'un processus en deux étapes. Nous devons d'abord tester le vecteur d'interruption 33h pour nous assurer qu'un pilote de souris est installé. Cette tâche est accomplie avec la fonction suivante :
- int checkMouseDriver(int needMouse) {
- union REGS inregs, outregs;
- struct SREGS segregs;
- unsigned char firstByte;
- inregs.x.ax = 0x3533;
- intdosx(&inregs,&outregs,&segregs);
- address = (((long) segregs.es) < 16) + (long) outregs.x.bx;
- firstByte = * (long far *) address;
- if((address == 0) || (firstByte == 0xcf)) {
- if(needMouse) {
- printf("Pilote de souris non installé");
- exit(1);
- } else {
- return 0;
- }
- }
- return 1;
- }
L'astuce ici consiste à lire le vecteur d'interruption à 33h et à tester le vecteur pour voir si un pilote est installé. Si un pilote n'est pas installé. Si aucun pilote n'est installé, cette fonction se termine en appelant exit. Pour tester le vecteur d'interruption, l'interruption DOS 21h est appelée avec le code de fonction 35h. Notez la description de cet appel d'interruption :
Interruption | 21h |
---|---|
Description | Demande l'adresse de l'interruption |
Entrées | AH = 35h AL = Numéro d'interruption |
Retours | ES:BX = Pointeur vers la routine d'interruption |
Pour appeler cette interruption, nous utilisons la fonction intdosx comme indiqué la syntaxe suivante :
int intdosx(union REGS *InRegs,union REGS *OutRegs,struct SREGS *SegRegs); |
Cette routine est fournie par QuickC pour effectuer des appels d'l'interruption 21h du MS-DOS. Pour tester le pilote de la souris, nous avons défini le registre AL sur 33h et le registre AH sur 35h (le code de la fonction d'interruption). La fonction 35h obtient l'adresse de l'interruption spécifiée à partir de la table d'interruption interne. La fonction renvoie l'adresse de segment du gestionnaire d'interruption dans le registre ES et l'adresse de déplacement dans le registre BX. Par conséquent, après l'appel de intdosx, nous devons vérifier ces registres pour voir si un vecteur d'interruption est réellement disponible pour la souris. Le test du vecteur d'interruption implique d'ajouter le segment et l'adresse de déplacement pour produire une valeur de 32 bits comme indiqué dans la formule suivante :
- address = (((long) segregs.es) << 16) + (long) outregs.x.bx;
Ensuite, le premier octet est obtenu avec l'instruction suivante :
- firstByte=*(long far *) address;
Cette valeur peut ensuite être testée pour voir si un vecteur d'interruption est stocké pour le pilote de la souris, comme indiqué :
- if((address == 0) || (firstByte == 0xcf))
Une fois que nous savons qu'un pilote de souris est installé, l'étape suivante consiste à réinitialiser la souris en appelant l'interruption 33h, fonction 0, étant représentée par :
Interruption | 33h |
---|---|
Description | Réinitialise la souris et renvoie l'état de la souris |
Entrées | m1 = 0 |
Retours | m1 = État de la souris m2 = Nombre de boutons |
Si le matériel et le logiciel de la souris sont correctement installés, m1 est défini sur -1 ; sinon il contient la valeur 0. Pour effectuer cette initialisation, nous allons utiliser la fonction suivante :
- #define M_RESET 0
-
- int mouseReset(void) {
- int m1, m2, m3, m4;
- m1 = M_RESET;
- mouse(&m1,&m2,&m3,&m4);
- return m1;
- }
La fonction mouseReset est en fait appelée par une fonction d'initialisation de souris de niveau supérieur. Le nom de cette fonction d'initialisation est mouseInit et elle est déclarée comme suit :
Essentiellement, cette routine vérifie si le pilote de la souris est installé, puis initialise la souris en appelant mouseReset et en définissant la variable globale mouseInitialized sur 1 pour indiquer que la souris est initialisée.