Il Serial Device di Amiga di Antonello Biancalana Antonello Biancalana lavora per la ProMIND, una software house di Perugia che sviluppa software grafico e musicale per Amiga, ed è sviluppatore Amiga registrato nella categoria "commercial". Di recente, Antonello Biancalana, ha progettato e sviluppato MSPL (Music Synthesis Programming Language), un particolare linguaggio di programmazione rivolto alla sintesi sonora e musicale. Amiga possiede una porta seriale compatibile RS-232C che gli permette di dialogare con il mondo esterno, e quindi può essere facilmente usata in tutte quelle applicazioni che richiedono l'invio e la ricezione di flussi di dati (modems, MIDI, collegamenti fra computers, ecc.). L'accesso alla porta seriale può essere fatto in due modi diversi, a secondo dell'uso che intendiamo fare del dispositivo. Infatti, la porta seriale può essere gestita sia attraverso l'uso dei registri hardware a lei assegnati, oppure, nella modalità standard prevista nell'ambiente Amiga, cioè facendo uso dei "devices". Il primo metodo viene usato raramente, cioè viene usato quando si ha bisogno intensivamente del dispositivo seriale lavorando con alte velocità di comunicazione, e ovviamente, questo tipo di implementazione deve essere fatta in linguaggio assembler. Il secondo metodo, è quello che viene usato principalmente nelle applicazioni più "comuni", in quanto la gestione dei devices fornita da exec risolve in maniera ottimale l'uso della porta seriale. Questo articolo si occupa esclusivamente del secondo metodo, facendo quindi riferimento al cosidetto "serial.device". Gli esempi di questo articolo, per motivi di chiarezza, saranno riportati in linguaggio "C", comunque, chi volesse accedere il "serial.device" in assembler, non dovrebbe avere difficoltà a gestire correttamente il dispositivo secondo le informazioni riportate. Come abbiamo già detto in precedenza, il "serial.device" di Amiga viene gestito mediante exec, e come tale, dovrà fare riferimento a tutte le convenzioni previste per la gestione Input/Output dei devices. La gestione exec di questo dispositivo è molto flessibile e può essere utilizzato in modalità di accesso esclusiva oppure condivisa da più tasks. La modalità di accesso esclusiva, permette di usare la porta seriale solamente dal task che ha aperto il dispositivo, mentre la modalità di accesso "condivisa", permette l'uso della porta seriale da parte di più tasks. Come per tutti i dispositivi di Amiga, la prima cosa da fare è quella di provvedere alla sua apertura. A differenza dei dispositivi standard di exec, il "serial.device" richiede l'uso di una struttura dati di tipo "IOExtSer" invece dell'usuale struttura "IOStdReq". Avremo quindi bisogno di una struttura di questo tipo propriamente inizializzata prima di poter aprire il "serial.device". Per fare questo, utilizzeremo la funzione "CreateExtIO". Questa funzione è definita all'interno di "Amiga.lib" che deve essere inclusa all'object code generato dal compilatore durante la fase di "Linking". La funzione "CreateExtIO" necessita di due parametri, ovvero l'indirizzo della porta da usare nella gestione del dispositivo e la dimensione della struttura dati che intendiamo creare. La porta di cui abbiamo bisogno possiamo ottenerla mediante la funzione "CreatePort", ovvero: struct MsgPort *MyPort; MyPort = CreatePort("MySerialPort",0); La funzione "CreatePort" necessita di due parametri: il primo rappresenta il nome da associare alla porta che intendiamo aprire mentre il secondo è relativo alla priorità della porta. Il nome della porta non è obbligatorio, ma è necessario nel caso in cui intendiamo ricercare la stessa da altri task mediante la funzione "FindPort". Nell'esempio precedente, manca una cosa importantissima che non ci interessa in questo esempio, ma che dovrà essere comunque presente: il controllo dell'effettiva creazione della porta, cioè è necessario controllare se exec ha creato la porta secondo i nostri requisiti oppure no. Questo è molto importante perchè se successivamente faremo riferimento ad una porta inesistente (o qualsiasi altra cosa), il nostro Amiga ci saluterà con un magnifico messaggio "Guru meditation", cosa che è facilmente evitabile in ogni programma (Non ci credete? Vi assicuro che è più semplice di quanto possiate immaginare). Una volta ottenuta la porta, possiamo creare la struttura "IOExtSer" mediante la funzione "CreateExtIO". struct IOExtSer *SerialData; SerialData = (struct IOExtSer *)CreateExtIO(MyPort,sizeof(struct IOExtSer)); Questa funzione allocherà la memoria ed i dati necessari alla nostra struttura, e da questo momento potremo iniziare tutte le operazioni sul nostro dispositivo seriale. La funzione "CreateExtIO" ritorna un puntatore ad una struttura di tipo "IORequest" che è ovviamente incompatibile con la nostra struttura "IOExtSer". L'operatore di "casting" (struct IOExtSer *) posto prima della funzione "CreateExtIO" viene utilizzato per convertire correttamente il puntatore ritornato dalla funzione con quello specificato, ovvero con un puntatore alla struttura "IOExtSer". Ricordiamo che il "casting" è un'operazione fornita dal linguaggio "C" che permette di convertire il tipo di dato in una certa fase del programma. Il dato viene solamente convertito e restituito a chi ne fa richiesta, ed è ovvio che il dato su cui si è operato il "cast" non cambia categoria ! Supponiamo di avere la variabile "dec" definita come "float" e la variabile "num" definita come "int". E` chiaro che non possiamo eseguire direttamente operazioni tra le due variabili in quanto appartengono a tipi di dati diversi. Nel caso in cui dovremo memorizzare la parte intera della variabile "dec" all'interno della variabile "num", possiamo fare ricorso ad un "cast". num = (int)dec; Il contenuto della variabile "dec" verrà convertito in un numero intero mediante l'operatore di cast "(int)" e quindi assegnato alla variabile "num". In ogni caso (e non è possibile fare diversamente), la variabile "dec" rimarrà per tutta l'esecuzione del programma una variabile di tipo "float". In Amiga l'uso dell'operatore cast è comunque molto frequente e permette di utilizzare la stessa funzione (nei limiti del possibile) con diversi tipi di dati. Chiusa questa breve parentesi sull'operatore di cast, torniamo al nostro esempio. Nella funzione "CreateExtIO" abbiamo specificato come porta "MyPort", cioè la porta che abbiamo creato precedentemente per questo scopo. Ora che abbiamo sia la porta che la struttura dati su cui lavorare (controllate sempre che questo sia vero con delle istruzioni "if"), possiamo finalmente aprire il dispositivo seriale. OpenDevice("serial.device",0,SerialData,0); La funzione di exec "OpenDevice" viene utilizzata per aprire un dispositivo. Questa funzione necessita di quattro parametri, ovvero del nome del dispositivo da aprire, il numero di unità, la struttura di dati da usare ed eventuali flags. Il nome, ovviamente sarà "serial.device", il numero di unità sarà zero, la struttuta dati da usare sarà "SerialData", cioè la struttura che abbiamo creato ed inizializzato precedentemente. Il valore relativo ai flags viene lasciato a zero in quanto non viene usato. Oltre ad aprire il dispositivo, la funzione "OpenDevice" provvede anche a inizializzare correttamente la nostra struttura dati "SerialData" secondo le specifiche e la configurazione della porta seriale in uso. Quando il "serial.device" viene aperto, esso verrà utilizzato secondo le specifiche indicate nel file "system-configuration" ovvero, secondo le specifiche impostate mediante "preferences". E` ovvio che questi parametri non soddisferanno sempre le nostre esigenze, quindi possiamo programmare il dispositivo seriale in modo che rispetti la configurazione a noi necessaria (velocità di trasmissione, numero dei bit di dati, ecc.). Il dispositivo seriale può essere utilizzato e comandato mediante l'uso di comandi speciali, alcuni di essi sono standard per tutti i dispositivi, mentre alcuni sono utilizzati esclusivamente per questo dispositivo. I comandi devono essere specificati nel campo IOExtSer->IOSer.io_Command. Nel nostro caso, è ovvio che "IOExtSer" dovrà essere sostituito da "SerialData". I comandi standard più utilizzati nella gestione del dispositivo seriale sono: CMD_WRITE, che scrive sul dispositivo i dati richiesti e CMD_READ, che legge i dati provenienti dalla porta seriale. Tutti i comandi specifici del dispositivo seriale iniziano con il suffisso "SD", e sono: SDCMD_QUERY, SDCMD_BREAK e SDCMD_SETPARAMS. SDCMD_QUERY viene utilizzato per conoscere il numero di bytes che sono stati letti dalla porta seriale e che sono attualmente contenuti nel buffer. SDCMD_BREAK viene utilizzato per inviare un segnale di interruzione alla porta seriale. SDCMD_SETPARAMS permette di impostare il dispositivo seriale. Una nota importantissima sull'uso dei comandi di dispositivi: essi devono essere sempre maiuscoli per poter lavorare correttamente! Se i valori di default previsti per il dispositivo seriale non sono di nostro gradimento, dovremo cambiare la configurazione dello stesso in modo da farlo lavorare correttamente. L'impostazione delle specifiche del dispositivo seriale viene fatta mediante il comando "SDCMD_SETPARAMS". L'esempio che segue è tratto dal programma "Seriale.c" (che troverete allegato nel dischetto di questa rivista) e che fa uso del comando "SDCMD_SETPARAMS". SerialData->IOSer.io_Command=SDCMD_SETPARAMS; SerialData->io_RBufLen=2048L; SerialData->io_Baud=2400; SerialData->io_TermArray.TermArray0=0x0d0a0d0a; SerialData->io_TermArray.TermArray1=0x0d0a0d0a; SerialData->io_ReadLen=8; SerialData->io_WriteLen=8; SerialData->io_StopBits=1; SerialData->io_SerFlags=SERF_EOFMODE|SERF_XDISABLED; Come detto in precedenza, il campo "IOSer.io_Command", viene utilizzato per specificare il comando da trasmettere al dispositivo; nel nostro caso intendiamo impostare dei nuovi parametri per la porta seriale. E` possibile anche riservare una zona di memoria utilizzata per contenere i dati letti dalla seriale attraverso il campo "io_RBufLen". Questo campo viene infatti utilizzato per specificare la dimensione del buffer di lettura. Il dispositivo seriale di Amiga è molto versatile e potente; esso infatti ci permette di impostare qualsiasi configurazione di trasmissione e soprattutto di velocità. Il dispositivo seriale accetta velocità di trasmissione che vanno da 110 baud a 1.000.000 baud, anche se per velocità maggiori a 19200 baud si potrebbero avere alcuni problemi durante la lettura dei dati ed in questi casi è preferibile lavorare in linguaggio assembler. Comunque non siamo assolutamente vincolati nell'uso di velocità standard (2400, 4800, 9600, ecc.), ed avremo la piena libertà di specificare velocità di trasmissioni non convenzionali. Nel nostro caso la velocità di trasmissione sarà di 2400 baud, come specificato nel campo "io_Baud". I due campi successivi ("io_TermArray.TermArray0" e "io_TermArray.TermArray1") vengono utilizzati per specificare i caratteri che indicano il termine della ricezione dati. Il numero massimo di caratteri di terminazione che possiamo specificare è otto, e nel caso in cui non avessimo bisogno di tutti questi caratteri, siamo costretti in ogni caso di specificarne otto. Questa è la ragione perchè nel nostro esempio abbiamo ripetuto sempre la stessa combinazione di caratteri (0x0d e 0x0a). Se questi campi non vengono totalmente riempiti, i caratteri lasciati liberi saranno uguali a zero. Questo è da evitare, perchè istruisce il dispositivo seriale ad accettare come carattere di terminazione il carattere nullo (cosa che è indesiderata nella maggior parte dei casi), e quindi è preferibile ripetere la serie di caratteri (o parte di essi), fino al completamento dei campi. L'uso di questi due campi non è comunque obbligatorio, ed ha solamente effetto se viene specificato "SERF_EOFMODE" nel campo dei flags. Il programma riportato nel dischetto necessita di questa tecnica, in quanto attende che venga spedito dal computer collegato ad Amiga il carattere di ritorno a capo. I campi successivi indicano rispettivamente il numero dei bits che compongono i dati in lettura e scrittura nonchè il numero dei bits di stop. Il campo "io_SerFlags" viene utilizzato per specificare tutte le caratteristiche di connessione e di lavoro come per esempio "l'handshaking", la parità ed altro ancora. Nel nostro caso, i flags utilizzati sono "SERF_EOFMODE" e "SERF_XDISABLED". SERF_EOFMODE abilita l'uso dei caratteri di terminazione utilizzati nei campi "io_TermArray", ed è quindi necessario per il nostro esempio. SERF_XDISABLED viene utilizzato per disabilitare la procedura di XON/XOFF. Dopo aver inizializzato i campi della struttura dati, si dovrà inviare la stessa al dispositivo seriale in modo da rendere effettivi i parametri desiderati. DoIO((struct IORequest *)SerialData); La funzione exec "DoIO" viene utilizzata per trasmettere i comandi al dispositivo in uso (nel nostro caso alla porta seriale) ed attende sempre il completamento del comando utilizzato prima di ritornare il controllo al programma. Ponete la vostra attenzione sull'operatore di cast applicato alla funzione "DoIO": esso è necessario in quanto "SerialData" è un puntatore ad una struttura "IOExtSer", mentre "DoIO" richiede un puntatore ad una struttura di tipo "IORequest". L'operatore di cast (come abbiamo già detto in precedenza) sistemerà il tutto e permetterà alla funzione di lavorare correttamente. Ora che abbiamo aperto ed inizializzato correttamente il dispositivo seriale, è giunto il momento di cominciare a "parlare" con il dispositivo collegato alla nostra porta seriale. L'esempio riportato sul dischetto, legge semplicemente dei dati dalla porta seriale, e risponde con dei messaggi in accordo a quanto ricevuto. Il programma continuerà questo lavoro fino a quando non verrà ricevuto il messaggio "fine". La prima cosa che faremo all'interno del ciclo è quella relativa ad una richiesta di lettura dati dalla porta seriale, e questo, come per qualsiasi altro genere di richiesta, dovrà essere fatto mediante la struttura "SerialData".  SerialData->IOSer.io_Command=CMD_READ; SerialData->IOSer.io_Length=BUFLEN; SerialData->IOSer.io_Data=(APTR)buffer; Questa volta, nel campo "IOSer.io_Command" troviamo il valore "CMD_READ" perchè si tratta appunto di una richiesta di lettura. Il campo "IOSer.io_Length" contiene la dimensione massima del buffer dati in cui intendiamo memorizzare i valori letti. Il campo "IOSer.io_Data" dovrà contenere l'indirizzo relativo alla zona di memoria da cui inizia il buffer dati. Dopo aver indicato correttamente i valori necessari per la lettura, non ci resta altro che inviare la richiesta al dispositivo mediante la funzione "SendIO". SendIO((struct IORequest *)SerialData); In realtà, si potrebbe utilizzare sia la funzione "DoIO" o "SendIO", in quanto entrambe permettono di trasmettere i nostri comandi al dispositivo, ma esiste una differenza fondamentale tra le due che vale la pena di esaminare. La funzione "SendIO", a differenza di "DoIO", non attende che il comando inviato sia terminato, ma semplicemente accoda la richiesta al dispositivo e restituisce il controllo al nostro programma. Questo risulta comodo in molti casi, soprattutto quando abbiamo diverse cose da svolgere contemporaneamente nel nostro programma. Nel nostro caso, accoderemo la richiesta al dispositivo ed attenderemo il suo termine mediante la funzione "Wait". Nel nostro esempio avremmo potuto utilizzare tranquillamente la funzione "DoIO" al posto di "SendIO", ma ho preferito, per scopi didattici, di utilizzare quest'ultima in modo da introdurre ed utilizzare la porta assegnata al dispositivo seriale. Dopo aver accodato la nostra richiesta, dovremo attendere che qualche messaggio arrivi alla nostra porta seriale. Quando questo accade, verremo segnalati per mezzo della porta che abbiamo riservato al dispositivo seriale, quindi, non ci resta che aspettare. Wait(1L<mp_SigBit); La funzione "Wait" risolve in maniera egregia questo problema, e soprattutto, non consuma prezioso tempo macchina che potrebbe essere utile per altri task (Amiga è un computer che opera in multitasking !). Porre particolare attenzione all'argomento passato alla funzione: il contenuto di "MyPort->mp_SigBit" rappresenta il numero del bit che viene utilizzato dalla porta, mentre la funzione "Wait" necessita della maschera relativa a quel determinato bit. L'operatore di shift "<<" risolve appunto questo problema, trasformando il bit di segnalazione nella relativa maschera. Ricordate comunque che il dispositivo seriale è stato impostato per leggere tutti i dati fino a quando non incontra uno dei caratteri specificati nel campo "io_TermArray", oppure quando i dati ricevuti avranno superato la dimensione del buffer. La funzione "Wait" attenderà fino a che uno di questi due eventi non sia soddisfatto. Non ci resta ora che analizzare la parte di codice che si occupa della scrittura sulla porta seriale. SerialData->IOSer.io_Command=CMD_WRITE; SerialData->IOSer.io_Length=-1; SerialData->IOSer.io_Data=(APTR)string; La struttura dati assume ora un'aspetto leggermente diverso. Il campo "IOSer.io_Command" verrà impostato con il valore "CMD_WRITE" in quanto desideriamo scrivere sul dispositivo. Il campo "IOSer.io_Length" contiene ora il numero di bytes da trasmettere alla porta seriale. Può sembrare del tutto strano il valore "-1" che abbiamo specificato, ma in questo caso esso assume un significato ben preciso. Quando in questo campo viene specificato -1, significa che dovranno essere trasmessi tutti i byte a partire dall'indirizzo fornito fino a quando non si incontra un carattere nullo. Il campo "IOSer.io_Data" contiene l'indirizzo di memoria in cui risiedono i dati da trasmettere. Siccome nel nostro esempio utilizzeremo un'array di caratteri (cioè una stringa), ecco che si spiega il valore "-1" sul campo precedente. Per rendere effettiva questa richiesta, non ci resta altro che spedire al dispositivo la nostra struttura dati mediante la funzione "DoIO" o "SendIO". Quando avremo terminato l'uso del dispositivo seriale, sarà necessario (anzi, obbligatorio!) deallocare e rilasciare al sistema tutte le risorse che abbiamo utilizzato compresa la chiusura del "serial.device". CloseDevice((struct IORequest *)SerialData); DeleteExtIO((struct IORequest *)SerialData); DeletePort(MyPort); Le funzioni "DeletePort", "DeleteExtIO" e "CloseDevice" provvedono appunto a quest'ultimo lavoro. Il programma incluso nel dischetto di questa rivista contiene un completo esempio di quanto esposto in questo articolo. Per poterlo far funzionare correttamente, è necessario collegare il vostro Amiga ad un'altro computer fornito di porta seriale RS-232C unitamente ad un programma di comunicazione. Dopo averlo eseguito, sarà possibile inviare dal computer collegato ad Amiga, alcuni "comandi" ai quali Amiga risponderà con appropriati messaggi. I "comandi" riconosciuti dal programma sono: host, baud, data, stop, buffer, fine e magic.