[ La porta parallela SPP: indice ] [ Pagina precedente ] [ Pagina successiva ]
I PC hanno normalmente una sola porta parallela, indicata come LPT1 oppure PRN. Il BIOS permette però di estenderne il numero fino a tre (su alcune macchine, soprattutto vecchie, fino a quattro). Le porte aggiuntive sono indicate come LPT2 e LPT3. Se è indispensabile e a condizione di trovare schede adatte, il numero potrebbe essere ulteriormente elevato.
Ciascuna porta parallela è vista dal processore come una serie di indirizzi nello spazio di I/O (Input/Output), occupando un minimo di quattro indirizzi consecutivi; il primo di questi indirizzi è chiamato indirizzo di base.
In genere LPT1 ha indirizzo di base 0x378 (usando la sintassi C, con 0x378 si intende il numero esadecimale 378, indicato con 378h oppure $378 se si usano altri linguaggi; in decimale l'indirizzo corrispondente è 888), LPT2 ha indirizzo 0x278 ed LPT3 0x3BC.
Gli indirizzi di tali porte non sono però rigorosamente fissi ma dipendono dall'hardware installato e da come il BIOS (o il sistema operativo) effettua la ricerca, causando spesso problemi nell'interpretazione corretta delle informazioni. Per esempio la porta con indirizzo 0x3BC è indicata sulle vecchie macchine come LPT1.
Due sono i modi per conoscere gli indirizzi effettivi:
Indirizzo di memoria | Contenuto (2 byte) |
0000:0408 | Indirizzo di base di LPT1 |
0000:040A | Indirizzo di base di LPT2 |
0000:040C | Indirizzo di base di LPT3 |
0000:040E | Indirizzo di base di LPT4 |
Su macchine moderne, in linea di massima successive agli IBM PS2 prima serie, l' indirizzo corrispondente alla LPT4 è usato per altri scopi e quindi ne sconsiglio l'uso e raccomando una particolare attenzione nell'interpretazione dei dati letti.
Di seguito un frammento di codice che legge l'indirizzo di LPT1 in Turbo C in ambiente DOS.
unsigned int far *pAddr; // Puntatore ai vettori di indirizzo
unsigned int IOaddr; // Indirizzo di LPT1
{
pAddr = (unsigned int far *) 0x408;
IOaddr = *pAddr;
printf("LPT1 base address is 0x%X\n", IOaddr);
}
In TurboPascal l'indirizzo di LPT1 si ricava con il seguente codice.
Var addr:word; {Indirizzo di LPT1}
Begin
addr := MemW[$0000:$0408];
writeln("LPT1 base address is", addr);
End
Per le altre porte il codice è lo stesso, cambiando ovviamente l'area di memoria da cui leggere.
Si noti che spesso le porte parallele su bus PCI hanno indirizzi maggiori di 0x400 e non sono riconosciute dal BIOS: per conoscerne l'indirizzo è utile verificare le informazioni fornite dal proprio sistema operativo (se ovviamente riconosce la porta)
Il sistema operativo Linux (c) utilizza convenzioni diverse, assegnando il nome di lpt1, lpt2 ed lpt3 rispettivamente alle porte che individua agli indirizzi 0x378, 0x278 e 0x3BC.
Ciascuna porta parallela configurata come SPP è controllabile attraverso tre registri consecutivi nello spazio di I/O del processore, chiamati data register, status register e control register, ciascuno di 8 bit.
Per scrivere un byte in questi registri è necessario eseguire una istruzione di output verso l'indirizzo interessato. Per leggere un byte è necessario effettuare una istruzione di input. Non è possibile leggere o scrivere un solo bit alla volta, indipendentemente dal linguaggio adottato; inoltre non è in genere possibile utilizzare le istruzioni corrispondenti alle SET dell'assembler. Infine non è possibile leggere un dato da un registro di sola scrittura o scrivere in una porta a sola lettura (l'operazione non genera nessun errore ma il dato letto o scritto è inconsistente).
Una precisazione: quanto detto vale solo in ambiente MS-DOS o derivati.
Usando il BorlandC o il TurboC la sintassi è la seguente (tale sintassi verrà adottata nel seguito del tutorial):
unsigned char dato;
unsigned int addr;
outportb (addr, dato);
dato = inportb (addr);
dove dato è il byte da leggere o da scrivere (una variabile di otto bit, senza segno: normalmente di tipo unsigned char) e addr è l'indirizzo del registro (una variabile o una costante a 16 bit, senza segno).
Altri compilatori C usano una sintassi simile ma non è prevista nell'ANSI C la scrittura diretta su dispositivi hardware: occorre quindi verificare sul manuale del proprio compilatore.
Usando la sintassi TurboPascal le stesse istruzioni diventano:
Port[addr] := dato;
dato := Port[addr];
Dove Port è una matrice predefinita di 65k elementi il cui l'indice corrisponde all'indirizzo della porta da utilizzare.
Usando la sintassi GW-Basic o altri basic per DOS:
OUT addr,dato
dato = INP (addr)
Usando la sintassi assembler:
MOV DX,ADDR
MOV AL,DATO
OUT DX,AL
MOV DX,ADDR
IN AL,DX
MOV DATO,AL
Usando sistemi operativi diversi dal DOS le cose cambiano in quanto ai programmi utente è impedito l'accesso diretto alle risorse hardware. La descrizione approfondita va oltre lo scopo di questo tutorial e mi limiterò quindi ad un breve accenno.
Questo sistema operativo implementa due modalità operative: il ring0 ed il ring3. Le istruzioni di I/O sono possibili senza limitazioni solo nel ring 0, quello del kernel del sistema operativo. Nel ring3 (il livello delle normali applicazioni utente) sono in teoria possibili istruzioni di ingresso e uscita solo su particolari indirizzi attraverso la manipolazione della cosiddetta I/O permission table, modificabile solo all'interno del ring0.
Eseguendo una applicazione DOS viene automaticamente generata da WindowsNT una macchina virtuale (la NTVDM: NT virtual dos machine) che emula le periferiche standard ed in particolare la parallela permettendo in qualche caso il corretto funzionamento dei programmi anche se con notevolissimi rallentamenti.
Nei compilatori nativi in ambiente Windows normalmente le istruzioni di IN e OUT non sono neppure implementate: ciò è vero in particolare per VisualBasic, BorlandC e Delphi.
La soluzione a questi problemi è quella di scrivere un device driver che, essendo eseguito nel ring0, permette la gestione diretta dell'hardware; purtroppo l'uso della DDK (driver development kit) di Microsoft è tutt'altro che semplice... Forse potrebbe esservi utile WinDriver disponibile in versione demo su http://www.jungo.com
In alternativa è possibile usare driver generici che mettono a disposizione apposite funzioni attraverso DLL o VBX. Spesso questi prodotti sono disponibili gratuitamente su internet: posso citare DriverLINX, per Win9x e WinNT e PortTalk. Alcune funzioni sono disponibile all'interno del tool VVIO, del quale rendo disponibile sia il codice sorgente sia l'eseguibile.
Analoghe considerazioni valgono anche in Win95/98/Me, anche se questi OS sono molto più laschi nella protezione dell'hardware.
Anche in ambiente Linux l'I/O diretto a livello di applicazione utente non è possibile. Chi volesse fare esperimenti con questo sistema operativo deve utilizzare la funzione C
ioperm(from, num, on_or_off)
Essa permette di rendere accessibile in lettura/scrittura una serie di registri ad una applicazione qualunque. Per usare un programma che invoca questa funzione occorre però essere l'utente root.
In alternativa occorre scrivere applicazioni in kernel mode; per questo potrebbe essere utile per esempio il pacchetto GPL short.
Il data register è un registro di sola scrittura direttamente connesso agli otto pin di dato presenti sul connettore esterno della parallela. L'indirizzo è quello di base della porta.
Per impostare un pin alto o basso basta scrivere nel bit corrispondente di questo registro rispettivamente 1 oppure 0. Il bit meno significativo del registro corrisponde al pin Data0.
Se per esempio si vogliono impostare i pin in modo tale che i pin 2 (bit 0) e 7 (bit 5) siano alti ed i pin 3, 4, 5, 6, 8 e 9 bassi, occorre eseguire la seguente riga di codice (la costante DATA indica l'indirizzo del registro dei dati della porta interessata, per esempio 0x278):
#define DATA 0x278
outportb (DATA, 0x21); // 0x21 = 00100001 in binario
Occorre infine notare che una volta scritto il byte nel registro, i pin di uscita non cambiano più il loro valore fino alla scrittura successiva in quanto il circuito di uscita della porta è formato da otto flip-flop.
Questo registro è di sola lettura ed ha indirizzo BASE + 1.
Quando il processore effettua la lettura di questo registro vengono riportati i valori dei cinque pin in ingresso, secondo la tabella di seguito riportata.
Indirizzo | Bit | Nome | Pin (DB25) | Invertito |
BASE +1 | Status 7 | Busy | 11 | Si |
Status 6 | Ack | 10 | No | |
Status 5 | Paper Out | 12 | No | |
Status 4 | Select In | 13 | No | |
Status 3 | Error | 15 | No | |
Status 2 | IRQ | - | ||
Status 1 | Riservato | - | ||
Status 0 | Riservato | - |
L'ultima colonna indica se è presente una porta not all'ingresso della porta parallela: nel caso ci sia un SI, vuol dire che un livello logico alto viene letto come 0 logico. Se invece si trova scritto NO, un livello logico alto viene letto come 1 logico.
Se voglio conoscere il valore dei pin 11 e 12 devo eseguire il seguente codice
#define STATUS (DATA+1)
unsigned char data, busy, po // Tre variabili ad 8 bit, senza segno
data = inportb (STATUS); // Leggo gli 8 bit di stato
data = data ^ 0x80; // Inverto il bit più significativo con uno xor
busy = (data & 0x80) >> 7; // Estraggo il valore di busy (bit 7)
po = (data & 0x20) >> 5; // Estraggo il valore di Paper Out (bit 5)
Il bit IRQ viene resettato dall'hardware in caso di interrupt attivato dalla linea Ack. Vale invece 1 qualora non si sia attivata nessuna interrupt.
Questo registro è primariamente un registro di scrittura anche se, in alcuni casi, possibile l'utilizzo in lettura. L'indirizzo è BASE + 2.
Il processore può scrivere in questo registro per impostare il valore di quattro pin di uscita, secondo la seguente tabella. Altri due bit sono per usi interni.
Indirizzo |
Bit |
Nome |
Pin (DB25) |
Invertito |
BASE + 2 |
Control 7 |
Riservato |
- |
- |
|
Control 6 |
Riservato |
- |
- |
|
Control 5 |
Bidirezionale |
- |
- |
|
Control 4 |
Abilitazione interrupt |
- |
- |
|
Control 3 |
Select-in |
17 |
SI |
|
Control 2 |
Inizialize |
16 |
NO |
|
Control 1 |
Linefeed |
14 |
SI |
|
Control 0 |
Strobe |
1 |
SI |
Nel caso dei tre bit invertiti, la scrittura di un 1 causa sull'uscita una tensione di 0V.
Per esempio per portare tutti i quattro pin della LPT2 ad un livello logico alto devo eseguire la seguente istruzione
#define CONTROL (DATA+2)
outportb (CONTROL, 0x04);
// 0x04 = 00000100, cioè ricordando che 3 bit sono invertiti, 00001111
Ho accennato al fatto che i quattro segnali connessi al registro di controllo possono essere in alcuni casi utilizzati anche per l'input.
Per fare ciò è necessario porre prima tutti i bit di uscita a livello logico alto e quindi leggere il byte:
outportb (CONTROL, 0x04); // tutti i pin alti
data = inportb (CONTROL) & 0x0F;
Questa possibilità è subordinata al fatto che queste uscite siano a collettore aperto (open collector), possibilità prevista nell'originaria porta del PC XT ma non in tutte le porte parallele moderne, soprattutto se configurate come EPP o ECP. Per verificare se la porta funziona secondo questa modalità provate a collegare una resistenza da 1 kohm tra uno dei pin associati al registro di controllo e massa: se viene letto uno 0 le uscite sono open-collector e quindi possono essere usate come ingressi.
Personalmente sconsiglio l'uso di tale modalità anche se funzionante in quanto potrebbe portare alla distruzione della porta nel caso di porte senza uscita a collettore aperto, cioè praticamente per tutte le porte dei PC moderni correttamente configurati.
Il bit 5 del registro di controllo permette di attivare la modalità bidirezionale delle porte che ne sono provviste: ponendo a 1 questo bit è possibile leggere i dati presenti sugli otto pin 2...9, attraverso la lettura del registro dei dati, come di seguito mostrato
outportb (CONTROL, 0x20); // 0x20 = 0010 000 setto in ingresso
data = inportb (DATA);
Se il bit vale 0, è possibile scrivere nel registro dati, come precedentemente descritto.
Non tutte le porte hanno questa possibilità ed alcune funzionano con modalità diverse. Dalla mia esperienza posso dire che le tutte le porte configurate come EPP hanno questo funzionamento. Non l'hanno invece le porte del PC XT originale ed in genere le vecchie macchine. Per effettuare il test che verifica se la propria porta parallela è bidirezionale o meno è possibile usare la seguente procedura:
Se viene letto il numero binario 11111110 (0xFE) la porta supporta la modalità bidirezionale. Se viene letto il dato preventivamente scritto, la porta non supporta la modalità bidirezionale, perlomeno secondo lo schema che ho descritto: molte schede parallele hanno infatti capacità bidirezionali di tipo proprietario e quindi incompatibili con la procedura descritta.
Il bit 4 permette di abilitare le interrupt alla ricezione di un fronte sul pin Ack. Se l'interrupt è abilitato e la porta stampante connessa ad una linea del controllore di interrupt (p.e. attraverso l'apposito ponticello presente su molte schede,), una transizione sul pin Ack causa un interrupt. Il fronte attivo può cambiare da scheda a scheda. L'uso di tale possibilità richiede la conoscenza delle problematiche correlate alla gestione delle interruzioni, argomento che va oltre lo scopo di questo tutorial.
La porta parallela - Versione 2.2a - Luglio 2006
Copyright © 1999-2001-2006, Vincenzo Villa (https://www.vincenzov.net)
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".