La stampa di una stringa di caratteri consiste nell'invio uno dopo l'altro di tutti i codici ASCII dei caratteri da visualizzare a video. I PIC sono piuttosto limitati per quanto riguarda la memorizzazione di stringhe nella memoria programma, e bisogna ricorrere anche questa volta alle tabelle di istruzioni RETLW.
STR1 ADDLW PCL,F RETLW 'H' RETLW 'E' RETLW 'L' RETLW 'L' RETLW 'O' RETLW ' ' RETLW 'W' RETLW 'O' RETLW 'R' RETLW 'L' RETLW 'D'
Per inviare questi codici, cioè la frase HELLO WORLD
occorre
una piccola subroutine che li prelevi uno ad uno dalla tabella (leggendola con
un indice progressivo) e li passi alla subroutine di trasmissione vera e propria.
Per la lunghezza della stringa si possono usare due sistemi, o si conosce
già a priori la sua lunghezza e si imposta un registro contatore per
inviare un numero ben preciso di caratteri, oppure si inserisce una RETLW aggiuntiva
in fondo alla stringa che ritorna il valore 0. In questo caso la subroutine
continuerà ad inviare caratteri finchè non incontra uno 0.
Una stringa di questo tipo in altri linguaggi di programmazione si chiama
"null terminated string".
Entrambi i sistemi hanno vantaggi e svantaggi, nel primo caso si risparmia
una riga nella tabella ma occorre usare un registro contatore,
nel secondo caso è esattamente l'opposto, si risparmia un registro
contatore ma occorre usare una RETLW in più
;-----------Caso1 STRINGA MOVLW 11 MOVWF CONT CLRF INDX STRINGA2 MOVF INDX,W CALL STR1 MOVWF DL CALL TX00 INCF INDX,F DECFSZ CONT,F GOTO STRINGA2 RETURN ;-----------Caso2 STRINGA CLRF INDX STRINGA2 MOVF INDX,W CALL STR1 XORLW 0 BTFSC STATUS,Z RETURN MOVWF DL CALL TX00 INCF INDX,F GOTO STRINGA2
Ecco quindi il programma completo per scrivere la prima frase, che usa il metodo della null terminated string e la USART interna:
PROCESSOR 16F628 RADIX DEC INCLUDE "P16F628.INC" __CONFIG 11110100010000B DL EQU 32 INDX EQU 33 ORG 0 BSF STATUS,RP0 ;banco 1 MOVLW 25 MOVWF SPBRG BSF TXSTA,BRGH ;9600 BAUD BSF TXSTA,TXEN ;ABILITA TX BCF STATUS,RP0 ;banco 0 BSF RCSTA,SPEN ;ABILITA SERIALE CALL STRINGA ;Invia stringa SLEEP ;Stop programma ;----------------------------------------------------- STRINGA CLRF INDX STRINGA2 MOVF INDX,W CALL STR1 XORLW 0 BTFSC STATUS,Z RETURN MOVWF DL CALL TX00 INCF INDX,F GOTO STRINGA2 STR1 ADDLW PCL,F RETLW 'H' RETLW 'E' RETLW 'L' RETLW 'L' RETLW 'O' RETLW ' ' RETLW 'W' RETLW 'O' RETLW 'R' RETLW 'L' RETLW 'D' RETLW 0 ;----------------------------------------------------- TX00 BTFSS PIR1,TXIF ;Attende trasmettitore libero GOTO $-1 MOVF DL,W MOVWF TXREG ;Invia dato RETURN ;----------------------------------------------------- END
E'utile ricordare che il set dei caratteri ASCII prevede simboli stampabili a partire dal codice 32 (spazio) fino al 127. I codici dallo 0 al 31 sono invece definiti "caratteri di controllo", perché servivano a dare comandi ai vecchi terminali e stampanti. I due codici forse più comuni sono il ritorno carrello (CR) e avanzamento riga (LF), i cui codici sono rispettivamente 13 e 10 (0D e 0A in esadecimale).
Per andare a caporiga e scrivere una nuova stringa occorre inviare questi due valori, che tra l'altro sono ancora oggi presenti (anche se normalmente non visibili) alla fine di ogni riga di un qualsiasi file di testo. La stringa seguente verrà scritta su due righe differenti:
STR1 ADDLW PCL,F RETLW 'H' RETLW 'E' RETLW 'L' RETLW 'L' RETLW 'O' RETLW ' ' RETLW 'W' RETLW 'O' RETLW 'R' RETLW 'L' RETLW 'D' RETLW 13 RETLW 10 RETLW 'P' RETLW 'I' RETLW 'C' RETLW '1' RETLW '6' RETLW 'F' RETLW '6' RETLW '2' RETLW '8' RETLW ' ' RETLW 'H' RETLW 'E' RETLW 'R' RETLW 'E' RETLW '.' RETLW 0
E' naturalmente possibile, e utile, prevedere una piccola subroutine apposta per andare a caporiga, magari chiamata proprio CRLF:
CRLF MOVLW 13 MOVWF DL CALL TX00 MOVLW 10 MOVWF DL CALL TX00 RETURN
Per scrivere un numero binario si devono inviare 8 caratteri 0
o 1
a seconda che il bit corrispondente sia 0 o 1. La subroutine seguente va chiamata
caricando in EL il valore da visualizzare. Per 8 volte si prepara in W il codice
dello 0
(48), si controlla il valore del bit più significativo di EL
e eventualmente si aggiunge 1 a W se risulta settato. Il codice, 48 o 49,
corrispondente ai caratteri 0
e 1
, viene quindi passato
alla subroutine di trasmissione TX00 attraverso il solito registro DL.
IL registro CH ha la funzione di contatore dei bit/caratteri.
Alla routine si può passare un valore a 8 bit compreso tra 0 e 255, che verrà visualizzato in forma binaria da 00000000 a 11111111.
WRBIN MOVLW 8 MOVWF CH ;conteggio bit/caratteri = 8 WRBIN2 MOVLW '0' ;W=codice dello "0" BTFSC EL,7 ;controlla bit piu' significativo ADDLW 1 ;se settato somma 1 al codice MOVWF DL CALL TX00 ;trasmetti carattere RLF EL,F ;ruota a sinistra EL DECFSZ CH,F ;decrementa conteggio bit/caratteri GOTO WRBIN2 ;se non zero torna a WRBIN2 RETURN ;fine subroutine
La scrittura di un numero in formato esadecimale si compone di due fasi, nella
prima vanno considerati i 4 bit più significativi del valore per ricavare il
primo digit, poi i 4 meno significativi. La subroutine seguente va chiamata caricando
in EL il valore da visualizzare. I due nibbles (gruppi di 4 bit) del valore vengono
scambiati (SWAP) e i 4 bit più significativi vengono azzerati con l'AND 0FH.
Si chiama WRHDIG che somma il valore 0..15 del nibble al codice dello 0
.
Si controlla che il valore ottenuto non superi il 57 (che è il codice del
carattere 9
), nel qual caso si aggiunge 7 per arrivare ai codici delle lettere
maiuscole, che nella tabella ASCII iniziano
7 valori dopo la fine dei codici dei numeri.
Una volta scritto il primo digit vengono considerati i 4 bit meno significativi del valore originario e si applica lo stesso procedimento. Alla routine si può passare un valore a 8 bit compreso tra 0 e 255, che verrà visualizzato in forma esadecimale da 00 a FF.
WRHEX SWAPF EL,W ;scambia i nibbles ANDLW 0FH ;azzera i 4 bit superiori CALL WRHDIG ;scrivi il primo digit MOVF EL,W ;riprendi valore EL ANDLW 0FH ;azzera 4 bit superiori WRHDIG MOVWF DH ;salva in DH MOVLW '0' ;prepara in W il codice dello "0" ADDWF DH,F ;sommalo a DH MOVF DH,W SUBLW 57 ;controlla se risultato>57 MOVLW 7 BTFSS STATUS,C ;se no skip ADDWF DH,F ;altrimenti somma 7 MOVF DH,W ;metti il codice in W MOVWF DL CALL TX00 ;trasmetti carattere RETURN ;fine subroutine
Seguendo attentamente le istruzioni si può notare un uso un po'strano
della CALL, infatti la subroutine ad un certo punto richiama come ulteriore
subroutine un pezzo di se stessa. Questo è un piccolo trucco usabile
in assembly per ridurre di qualche istruzione la lunghezza del programma,
nei particolari casi come questo in cui esistono delle istruzioni da eseguire
più volte e che l'ultima volta coincidono con una semplice
continuazione lineare del programma. La RETURN finale in questo caso rimanda la
prima volta alla quinta istruzione della subroutine WRHEX, e la seconda volta
ritorna al chiamante della WRHEX. La forma classica estesa
sarebbe stata
la seguente di identico funzionamento:
WRHEX SWAPF EL,W ;scambia i nibbles ANDLW 0FH ;azzera i 4 bit superiori CALL WRHDIG ;scrivi il primo digit MOVF EL,W ;riprendi valore EL ANDLW 0FH ;azzera 4 bit superiori CALL WRHDIG ;scrivi il secondo digit RETURN WRHDIG MOVWF DH ;salva in DH MOVLW '0' ;prepara in W il codice dello "0" ADDWF DH,F ;sommalo a DH MOVF DH,W SUBLW 57 ;controlla se risultato>57 MOVLW 7 BTFSS STATUS,C ;se no skip ADDWF DH,F ;altrimenti somma 7 MOVF DH,W ;metti il codice in W MOVWF DL CALL TX00 ;trasmetti carattere RETURN ;fine subroutine
Se si guardano le ultime due subroutines e gli esempi con la trasmissione
seriale software (senza usare la USART interna), si può notare che per
ogni subroutine occorre definire un certo numero di registri di lavoro.
Per esempio quella per scrivere i numeri binari richiede un registro EL in
cui riceve il valore, un CH come contatore dei caratteri, e usa DL per passare
i codici ASCII alla subroutine di trasmissione.
Quest'ultima riceve il valore in DL, e usa BL e CL rispettivamente come
contatore dei bit e contatore per i cicli di ritardo.
La subroutine per
scrivere in esadecimale usa invece un registro di lavoro chiamato DH. Se li
contiamo siamo già a 6 diversi registri, la cosa non è grave
visto che possiamo definirne tanti di nome diverso quante sono le celle
della RAM disponibili (Nel PIC16F628 sono 96 solo nel banco 0), ma se si
usano molte subroutines che si chiamano l'un l'altra è facile perdere
di vista quali registri vengono usati in una e quali nell'altra.
Per esempio può venire comodo chiamare sempre con il nome CL un registro
che serve per fare un conteggio, o DL quello in cui impostare un valore da
elaborare. Il problema è che se non si fa attenzione una subroutine
chiamata potrebbe alterare il valore di un registro usato anche nella routine
chiamante producendo risultati imprevedibili.
Le possibilità sono soltanto due, o si fa estrema attenzione a dare un
nome univoco ad ogni registro usato in ciascuna subroutine, o ogni subroutine
deve incaricarsi di salvare da qualche parte i valori dei registri comuni
e ripristinarne il valore alla fine.
Nei microprocessori più evoluti lo stack può essere usato
proprio per questo scopo. Solitamente dispongono di pochi registri da usare
per tutto il programma, e pertanto viene comodo salvarne il contenuto
(con operazioni di PUSH) e recuperarlo successivamente (con operazioni di POP).
Questo modo di procedere permette di evitare molte interferenze
tra
le diverse parti del programma, e se non ci sono vincoli strettissimi di
velocità da rispettare è un buon sistema di programmare.
Sfortunatamente sui PIC lo stack può contenere solo gli indirizzi di ritorno delle subroutine, è gestito in hardware ed è molto piccolo. Esiste però la possibilità di simulare uno stack dati usando una porzione della memoria RAM a cui accedere tramite il registro puntatore FSR.
Nel registro FSR si può scrivere l'indirizzo di una cella della RAM, a cui si può accedere (per leggerla o scriverci) tramite il registro INDF. Ad esempio:
MOVLW 32 ;indirizzo inizio area dati MOVWF FSR ;lo scrive nel puntatore MOVF INDF,W ;legge il contenuto della cella ;puntata da FSR e lo mette in W MOVLW 32 ;indirizzo inizio area dati MOVWF FSR ;lo scrive nel puntatore CLRF INDF ;azzera la cella puntata da FSR
Per creare uno stack dati simulato a software si possono riservare ad esempio i primi 16 byte della RAM per questo scopo, e puntare al primo di essi tramite FSR.
ORG 32 ;indirizzo inizio area dati S_STACK RES 16 ;16 byte usati come stack simulato ORG 0 ;indirizzo inizio programma MOVLW S_STACK ;W=indirizzo di partenza stack MOVWF FSR ;FSR punta all'inizio dello stack
Supponiamo di voler salvare il valore di un generico registro chiamato DL, richiamare una subroutine, e successivamente recuperare il valore di DL originale:
MOVF DL,W ;W = valore registro DL MOVWF INDF ;Lo scrive nella cella puntata da FSR INCF FSR,F ;Incrementa indirizzo puntato da FSR CALL ........ ;chiama la subroutine che può ;tranquillamente modificare il valore di DL DECF FSR,F ;Decrementa indirizzo puntato da FSR MOVF INDF,W ;Legge valore della cella puntata da FSR MOVWF DL ;Lo riscrive in DL
Certo si sarebbe anche potuto salvare DL in un altro registro con un altro nome, ma
una struttura come lo stack permette di riutilizzare per compiti diversi sempre
le stesse celle di RAM e soprattutto senza bisogno di definire nuovi nomi e
e senza assegnare loro un compito che viene svolto magari una sola volta in
tutto il programma. Semplicemente con quelle prime 3 istruzioni salviamo
il nostro registro lasciando il compito al puntatore FSR di tenere traccia di
dove viene salvato, e quando ci riserve quel valore usiamo le ultime 3
istruzioni che lo recuperano.
E'ovviamente possibile effettuare più di un salvataggio uno dietro
l'altro (in questo caso fino a riempire tutti i 16 byte dello stack),
ricordando però che il recupero dei dati avviene sempre in ordine inverso.
Lo stack infatti è una struttura LIFO
(last in first out), come una
catasta di piatti, l'ultimo piatto che si appoggia sulla cima della catasta è
il primo che si riprende. Quindi se si salvano in sequenza i registri AL BL CL DL,
si recupera per primo il valore di DL per finire con AL.
Da questo si può anche capire che alla fine il numero dei recuperi deve essere uguale a quello dei salvataggi, altrimenti si rischia di lasciare qualche dato nello stack, o far sconfinare l'indice dello stack al di fuori dello spazio previsto ottenendo i soliti risultati imprevedibili.
Usando poi la direttiva MACRO è anche possibile racchiudere i gruppi di istruzioni di salvataggio/recupero in due comodi nomi facili da ricordare, per esempio PUSH e POP come negli assembly di micro più evoluti:
PUSH MACRO registro MOVF registro,W MOVWF INDF INCF FSR,F ENDM POP MACRO registro DECF FSR,F MOVF INDF,W MOVWF registro ENDM
Con queste due macro è possibile salvare un registro semplicemente scrivendo: PUSH nomeregistro, e recuperarne il valore con POP nomeregistro. Bisogna comunque ricordare che le PUSH e POP scritte nel programma sono delle macro, e quindi al momento della compilazione verranno automaticamente sostituite con le istruzioni corrispondenti. Ciascuna PUSH e POP occupa perciò lo spazio di 3 istruzioni, ed è solo una forma mnemonica abbreviata per identificarle. Inoltre ricordare sempre che ad ogni PUSH e POP viene alterato il valore dell'accumulatore W e del flag Z!
Per tornare alla visualizzazione su terminale supponiamo allora di voler usare la
subroutine di trasmissione software e voler usare meno nomi possibili per
i registri di lavoro salvandoli sullo stack quando necessario.
Vogliamo scrivere in binario e in esadecimale il valore 207 separati da due spazi.
Il seguente programma completo svolge questa funzione e usa lo stack simulato
per salvare il valore di alcuni registri prima di riutilizzarli all'interno delle
subroutines.
PROCESSOR 16F628 RADIX DEC INCLUDE "P16F628.INC" __CONFIG 11110100010000B ORG 32 S_STACK RES 16 ;16 byte usati come stack simulato BL RES 1 CL RES 1 DL RES 1 #DEFINE TXOUT PORTB,2 ;Uscita seriale ;----------------------------------------------------- PUSH MACRO registro MOVF registro,W MOVWF INDF INCF FSR,F ENDM POP MACRO registro DECF FSR,F MOVF INDF,W MOVWF registro ENDM ;----------------------------------------------------- ORG 0 BSF STATUS,RP0 ;Attiva banco 1 BCF TRISB,2 ;PORTB=uscita BCF STATUS,RP0 ;Ritorna al banco 0 MOVLW 7 MOVWF CMCON ;PORTA=I/O digitali MOVLW S_STACK ;W=indirizzo di partenza stack MOVWF FSR ;FSR punta all'inizio dello stack MOVLW 207 MOVWF DL CALL WRBIN ;scrive 207 in binario MOVLW ' ' MOVWF DL CALL TX00 ;scrive uno spazio CALL TX00 ;un altro spazio MOVLW 207 MOVWF DL CALL WRHEX ;scrive 207 in esadecimale SLEEP ;Stop programma ;----------------------------------------------------- ; Scrive numero in binario 8 bit (8 digit) ; DL=valore da scrivere. BL=copia di DL. ; CL=contatore cicli. Non altera i registri. ;----------------------------------------------------- WRBIN PUSH BL PUSH CL PUSH DL MOVF DL,W MOVWF BL ;copia DL in BL MOVLW 8 MOVWF CL ;conteggio bit/caratteri = 8 WRBIN2 MOVLW '0' ;W=codice dello "0" MOVWF DL BTFSC BL,7 ;controlla bit piu' significativo INCF DL,F ;se settato somma 1 al codice CALL TX00 ;trasmetti carattere RLF BL,F ;ruota a sinistra BL DECFSZ CL,F ;decrementa conteggio bit/caratteri GOTO WRBIN2 ;se non zero torna a WRBIN2 POP DL POP CL POP BL RETURN ;fine subroutine ;----------------------------------------------------- ; Scrive numero in esadecimale 8 bit (2 digit) ; DL=valore da scrivere. Non altera i registri. ;----------------------------------------------------- WRHEX PUSH DL ;salva 2 copie di DL PUSH DL SWAPF DL,W ;scambia i nibbles ANDLW 0FH ;azzera i 4 bit superiori MOVWF DL CALL WRHDIG ;scrivi il primo digit POP DL ;riprendi copia di DL MOVLW 0FH ANDWF DL,F ;azzera 4 bit superiori CALL WRHDIG ;scrivi il secondo digit POP DL RETURN WRHDIG MOVLW '0' ;prepara in W il codice dello "0" ADDWF DL,F ;sommalo a DL MOVF DL,W SUBLW 57 ;controlla se risultato>57 MOVLW 7 BTFSS STATUS,C ;se no skip ADDWF DL,F ;altrimenti somma 7 CALL TX00 ;trasmetti carattere RETURN ;----------------------------------------------------- ; Trasmissione seriale di un byte in formato ; 9600 8-N-1 dal pin "TXOUT" (per clock 4MHz) ; DL=valore da trasmettere, BL=contatore dei bit, ; CL=contatore cicli ritardo. Non altera i registri. ;----------------------------------------------------- TX00 PUSH BL PUSH CL PUSH DL MOVLW 10 MOVWF BL ;Contatore dei bit=10 BCF STATUS,C ;Azzera flag C TX01 BTFSS STATUS,C ;Se flag C=1 skip GOTO TX02 ;altrimenti salta a TX02 NOP BSF TXOUT ;Manda a 1 il pin TXOUT GOTO TX03 ;e salta a TX03 TX02 BCF TXOUT ;Manda a 0 il pin TXOUT NOP NOP TX03 MOVLW 30 MOVWF CL ;CL=30, ritardo durata bit DECFSZ CL,F ;Decrementa CL, skip se zero GOTO $-1 ;Altrimenti nuovo decremento BSF STATUS,C ;Setta flag C RRF DL,F ;Ruota a destra DL DECFSZ BL,F ;Decrem.contat.bit, skip se 0 GOTO TX01 ;Altrimenti torna a TX01 POP DL POP CL POP BL RETURN ;----------------------------------------------------- END
In questo programma si possono notare alcune cose. Intanto il salvataggio dei registri all'inizio di ogni subroutine e il ripristino alla fine, questo permette ad esempio di impostare il codice in DL e, senza rischiare che il suo valore cambi, richiamare due o piu' volte di fila la TX00 (cosa che viene fatta nella sezione principale del programma quando si scrivono i due spazi di fila). Poi si può notare che il recupero dei dati con le POP avviene in ordine inverso a quello della PUSH. Ed infine nella subroutine WRHEX si nota che DL viene salvato due volte. Questo è perfettamente lecito, e in questo caso permette di evitare l'uso di un registro di copia temporanea.
E' pure possibile usare lo stack per scambiare il valore tra due registri, anche se è una pessima scelta come velocità di esecuzione e occupazione di memoria programma (12 istruzioni effettive quando uno scambio si può fare con 4), ma serve ad illustrare le possibilità che si hanno a disposizione con uno stack. Alla fine CL e DL si sono scambiati i valori:
PUSH CL PUSH DL POP CL POP DL