Ristampato col permesso del Pluto Journal
In questo articolo vedremo come si può utilizzare l'intefaccia parallela in Linux. L'interfaccia parallela riveste un certo interesse per chi si diverte a giocare con il saldatore a stagno in quanto è un'interfaccia molto semplice da usare e si può trovarla in molti computer: la discussione seguente vale per PC e Alpha, ma non per Sparc; per le altre piattaforme supportate non so dire...
L'interfaccia parallela del calcolatore, nella sua forma più utilizzata, è la periferica più semplice che si possa immaginare. Non mi riferisco qui ai nuovi standard, quelli con nomi tipo EPP, ma al protocollo originale della parallela, che è supportato da tutti i PC, dall'8088 in poi.
In pratica, sul connettore a 25 piedini si trovano direttamente i segnali corrispondenti ad alcuni bit di I/O, in logica TTL (con segnali a 0V e 5V). Con "direttamente" intendo dire che i bit che vengono scritti dal processore sulle porte di output appaiono sui piedini del connettore, e i bit della porta di ingresso vengono letti dal segnale di tensione sul connettore.
Il connettore parallelo porta 12 bit di output e 5 bit di input, più 8 segnali di terra. L'interfaccia software si compone quindi di due porte di output ed una porta di input. Per rendere le cose un pò più complicate, alcuni bit subiscono una inversione tra quello che appare sul connettore e quello che viene visto dal processore.
Prima che qualcuno collegi lampadine da 20W ai segnali di output della parallela è forse il caso di dire due parole sui segnali elletrici utilizzati: TTL (Transistor-Transistor Logic) è una famiglia logica molto utilizzata, almeno in passato, che presenta specifiche di ingresso/uscita non simmetriche: mentre un'uscita bassa (0V) può assorbire una corrente significativa, un'uscita alta (5V nominali) non è in grado di erogare più di un paio di mA di corrente, e risulta spesso a tensione molto più bassa di 5V (il minimo assicurato è 2.7 V). Allo stesso modo, per abbassare un ingresso occorre assorbire tanta corrente, mentre per alzarlo basta una resistenza di 5k verso 5V.
Le porte parallele più recenti forniscono prestazioni elettriche migliori, ma la cosa non è affatto garantita; lampade da 20W non possono comunque essere collegate direttamente, e l'uso di fotoaccoppiatori è comunque consigliabile per proteggere il calcolatore (ma io personalmente non l'ho mai fatto).
Ogni porta parallela del sistema viene vista tramite tre indirizzi di
I/O consecutivi: "base", "base+1", "base+2". Ad ogni porta parallela
è anche associata un'interruzione, che vedremo a breve. I valori di
"base" più utilizzati ed i relativi numeri di interruzione sono
0x378
(IRQ 7), 0x278
(IRQ 2) e
0x3bc
(IRQ 5).
Il registro "base+1" è un registro di input, e viene usato per leggere i 5 segnali di ingresso del connettore, "base" e "base+2", invece, sono registri di output, e quando vengono letti restituiscono l'ultimo valore scritto (cioè lo stato attuale dei segnali al connettore).
L'effettiva mappatura tra i bit nei registri e i piedini del connettore è spiegata meglio con una figura che a parole.
Figura 1: Mappatura tra i bit nei registri e i piedini del connettore L'immagine è anche disponibile in postscript come parportcolor.ps.
Poichè non interessa qui spiegare come si possa controllare una stampante, non resta nient'altro da dire sulla parallela. Chi non è interessato a questioni hardware può tranquillizzarsi, in quanto non parlerò più di segnali elettrici fino agli esempi finali.
La discussione dell'uso delle porte di input/output ripete parzialmente quanto detto nel primo articolo sui device drivers, ma un minimo di introduzione mi sembra dovuta.
In Linux sono definite le seguenti funzioni per accedere alle porte:
inb(port)
legge la porta port
e ne
ritorna il valore (8 bit).
outb(value, port)
scrive sulla porta port
il
valore value
.
outw(value, port)
scrive un valore a 16 bit su di una
porta a 16 bit. L'equivalente inw(port)
esiste
per l'input.
outl(value, port)
scrive un valore a 32 bit. Per
l'input si usa inl(port)
.
outsb(port, address, count)
effettua un ciclo di
istruzioni outb()
per scrivere count
bytes sulla porta port
a partire dall'indirizzo
address
. Le equivalenti funzioni w
ed l
sono definite, come puter le tre funzioni di
input.
Le funzioni precedenti sono definite sia per Intel che per Alpha, nonostante il processore Alpha non supporti uno "spazio di I/O". Alpha supporta solo l'I/O mappato in memoria, e i segnali di I/O per il bus vengono generati da circuiti che rimappano alcune locazioni di memoria sullo spazio di I/O (questo permette di usare periferiche ISA e PCI in calcolatori basati su Alpha, con l'ulteriore ausilio di un emulatore 8086 per eseguire l'inizializzatione delle schede stesse che si trova sulla loro stessa ROM in forma di codice intel).
L'architettura Sparc è differente da questo punto di vista, e non esiste nessuna delle funzioni precedenti quando Linux gira su Sparc. L'I/O per Sparc è mappato in memoria come per Alpha, e le stesse periferiche non hanno alcuna nozione di indirizzi di input/output. Poichè le nuove Sparc supportano PCI è probabile che verrà introdotto un modo per accedere al bus compatibilmente con le funzioni introdotte sopra e circuiteria simile a quella per Alpha.
E' interessante notare come le funzioni di "I/O di stringa" sono
singole istruzioni sulla piattaforma Intel mentre vengono implementate
da cicli software per Alpha. La versione 2.0 del kernel non esporta
outsl()
e insl()
per Alpha, mentre
linux-2.1.3 e seguenti le esportano. Questo non è comunque un
problema finchè ci limitiamo ad usare porte a 8 bit.
Per alcune architetture sono anche definite delle istruzioni "con
pausa" per operare su periferiche lente, ma non è questo il luogo per
parlarne. Il lettore curioso può sempre guardare in
<asm/io.h>
Il modo più semplice per leggere e scrivere sulle locazioni di I/O è farlo nello spazio utente. Questo però comporta alcune limitazioni:
root
.
ioperm(from, num, on_or_off)
per abilitare
l'uso delle porte nel processo corrente.
Avendo il vizio di attaccare semplici circuiti al mio calcolatore, ho
scritto due stupidi programmi per leggere e scrivere le porte dallo
spazio utente. Si chiamano inp
e outp
, ed
utilizzano solo le porte ad 8 bit. I sorgenti si chiamano, ovviamente,
inp.c
e
outp.c
.
inp
e outp
possono essere fatti
"set-userid" root in modo da non dover diventare esplicitamente root
per usarli. Io difficilmente lavoro come root sulla mia macchina, per
cui ho usato questa tecnica; bisogna però fare molta attenzione
perchè lasciare outp
libero a tutti gli utenti è un
serio rischio per la sicurezza della macchina -- io non saprei
esattamente cosa farci, ma un utente smaliziato e abbastanza
competente può tirar fuori di tutto da un buchetto del genere.
L'unica attenzione da ricordare quanto si compilano programmi che
usano le funzioni di I/O è di specificare -O
tra le
opzioni del compilatore. Questo perchè le funzioni di I/O sono
dichiarate come "extern inline
", e le funzioni
inline
non vengono espanse da gcc
se
l'ottimizzazione non è abilitata. La problematica e' spiegata in
maniera chiara nella pagine del manuale di gcc
.
Il codice per accedere alle porte di I/O da parte del kernel è uguale
a quello usato nello spazio utente, tranne che non occorre usare
ioperm()
, in quanto il codice del kernel (che gira in "modo
supervisore" sul processore), ha sempre accesso a tutte le risorse
hardware.
Quello che occorre per utilizzare la porta parallela è un
device driver che permetta di leggere o scrivere le porte in
questione. Il modulo "short
" (Simple Hardware Operations
and Raw Tests) permette di leggere e/o scrivere quattro porte a 8 bit
consecutive: i nodi in /dev
creati da short
sono direttamente mappati sulle porte di I/O: /dev/short0
serve per leggere/scrivere la porta base
,
/dev/short1
accede a base+1
eccetera. Il
valore di default di base
è 0x378
e
permette quindi di usare la porta parallela; una linea di comando come
"insmod short.o short_base=0x278
" si può usare per
accedere a porte diverse.
Inoltre, per permettere di vedere il differente comportamento di
outb()
, outsb()
e outb_p()
(la versione con
pausa), il modulo crea diversi nodi in /dev
a questo fine:
/dev/short0
usa le normali chiamate inb()
/outb()
.
/dev/short0p
usa le chiamate con pausa.
/dev/short0s
usa outsb()
/insb()
.
Il seguente esempio mostra il tempo che il dispositivo impiega a scrivere 1MB sulla porta usando i tre diversi nodi:
morgana.root# time dd bs=1k count=1000 if=/dev/zero of=/dev/short0
0.020u 2.040s 0:02.06 100.0% 0+0k 0+0io 64pf+0w
morgana.root# time dd bs=1k count=1000 if=/dev/zero of=/dev/short0p
0.020u 3.510s 0:03.57 98.8% 0+0k 0+0io 64pf+0w
morgana.root# time dd bs=1k count=1000 if=/dev/zero of=/dev/short0s
0.020u 1.640s 0:01.66 100.0% 0+0k 0+0io 64pf+0w
Come si vede, le chiamate con pausa aggiungono un attesa di circa un microsecondo dopo ogni operazione, mentre le chiamate "stringa" sono circa il 25% più veloci dei cicli software.
Bisogna però dire che scrivere tanti dati consecutivamente sulla porta parallela è difficilmente di interesse, e il controllo di semplici periferiche esterne viene di solito effettuato scrivendo o leggendo un byte alla volta.
La porta parallela è anche in grado di generare interruzioni quando
il piedino numero 10 passa da una tensione bassa ad una alta. Le
interruzioni, però, a differenza delle porte, non si possono gestire
nello spazio utente, e bisogna appoggiarsi a codice nel
kernel. Perchè la porta parallera interrompa il processore occorre
scrivere un 1 nel bit 4 della porta base+2
.
Il modulo short
è in grado di gestire le interruzioni: quando
il modulo viene caricato abilita l'interfaccia a riportare interruzioni.
Il device /dev/shortint
riporta nello spazio utente gli
istanti di tempo (in secondi e microsecondi) in cui il processore
viene interrotto dall'interfaccia. La scrittura di
/dev/shortint
fa si che vengano scritti alternativamente
0x00
e 0xff
sulla porta dat
(base+0
). Si possono perciò generare delle
interruzioni collegando insieme il piedino 9 e il piedino 10 dell
connettore della parallela. Questo è quello che succede sulla
mia macchina mettendo il ponticello:
morgana% echo 1122334455 ">" /dev/shortint ; cat /dev/shortint
50588804.876653
50588804.876693
50588804.876720
50588804.876747
50588804.876774
Una trattazione approfondita della gestione delle interruzioni è fuori argomento in questa sede, ed
short
Il driver short
è distribuito in formato sorgente
secondo la GPL ed è composto dal sorgente, un header per
risolvere alcune dipendenze dalla versione del kernel e due script per
caricare e scaricare
il modulo. Più, ovviamente, il Makefile
.
La cosa migliore per chi intende provarlo è comunque scaricare il
tar completo: short.tar.gz
.
Vediamo ora tre esempi di uso della porta parallela per il collegamento di circuiti personali.
Il primo è veramente banale, e consiste nell'applicazione di un LED (o più di uno) per la visualizzazione dello stato del (dei) bit della porta. In figura è rappresentato il monitoraggio del bit 0 della porta dati. Il ponticello tra il piedino 9 (bit 7 della porta dati) e il piedino 10 permette di giocare con le interruzioni.
Figura 2: Rappresentazione del monitoraggio del bit 0 della porta dati
L'immagine è anche disponibile in postscript come parsample1.ps
Il secondo esempio riguarda il controllo di un relay ("relé"). tramite logica TTL. Per l'implementazione di questo circuito occorre un'alimentazione esterna a 5V e l'alimentazione per il relay.
In questo esempio un '244 (buffer a 8 bit) isola il segnale della
parallela dalla circuiteria esterna, dove un '138 (multiplexer 3-to-8)
abbassa una delle otto linee di output in base ai tre bit di indirizzo
A, B, C. I tre segnali di enable del '138 sono collegati in modo da
rispondere ai dati compresi tra 0x20
e 0x27
.
Il relay mostrato in figura viene attivato scrivendo 0x20
e disattivato scrivendo 0x21
.
Collegare direttamente i bit della porta al relay non permette di preservare lo stato dell'interruttore quando si spegne o riaccende la macchina. L'uso dei codici 0x20-27 permette invece una persistenza dello stato comandato dal calcolatore (se non si toglie l'alimentazione esterna). Ho usato personalmente questo circuito per collegare 8 relé al mio calcolatore, tramite due '138 con i fili di enable collegati diversamente.
Figura 3: Rappresentazione del circuito per collegare 8 releé al calcolatore
L'immagine è anche disponibile in postscript come parsample2.ps
Il terzo esempio è una segreteria telefonica che fa uso di una scheda audio per la ripetizione del proprio messaggio e la registrazione della telefonata.
Per implementare la segreteria basta riconoscere quando il telefono squilla e quando invece il telefono viene messo giù. La porta parallela può essere usata per generare delle interruzioni quando la tensione sulla linea telefonica oscilla bruscamente, e il riconoscimento dello squillo può poi avvenire via software.
La trasmissione e la ricezione del segnale audio possono avvenire tramite accoppiamento capacitivo, mentre la cornetta può essere "sollevata" tramite un relé di segnale. Questo permette di mantenere i due circuiti elettricamente separati.
A differenza dei due esempi precedenti, non ho avuto modo di provare questo circuito in pratica, ed è quindi possibile che contenga delle sviste macroscopiche da parte mia. Prima di toccare i fili del telefono, comunque, conviene reperire le informazioni sui livelli di tensione usati ed accertarsi che i valori dei condensatori e degli zener usati siano corretti per accoppiare la scheda audio, poichè i valori mostrati sono spannometrici, in quanto non conosco i livelli di tensione della scheda audio.
Figura 4: Circuito non verificato per collegamento tramite parallela
di dispositivi audio e telefono
L'immagine è anche disponibile in postscript come parsample3.ps
La copia lettarale di questo articolo e la sua distribuzione con qualunque mezzo sono permesse, a condizione che questa nota sia conservata.