ASM PIC per novizi
From UIC
PIC Asm for Beginners
Contents |
| Infos | |
|---|---|
| Author: | phobos |
| Email: | phobos333 .at. email .dot. it |
| Website: | Here |
| Date: | 01/05/2003 (dd/mm/yyyy) |
| Level: |
|
| Language: | Italian |
| Comments: | |
Introduction
L'elettronica è un campo che mi ha sempre affascinato... fin da quando ero più piccolo, ma un campo in cui, come molte delle cose che mi riprometto sempre di fare, era nella mia "to do list" mentale da tempo memorabile, senza essere stato mai affrontato seriamente... devo ringraziare tre persone in particolare per avermi aperto la strada in questo meraviglioso campo: MrCode, CaptZero e quell'omone di TiN_MaN :D
Ho deciso di scrivere questo corso per newbie, perché a mio parere, i PIC micro, sono una sorta di sintesi tra elettronica e programmazione... un compromesso che abbraccia due delle cose che reputo tra le più affascinanti.
Cercherò con questo corso, di mettere nelle condizioni di appassionarsi a questo mondo, tutti coloro che, come me condividono questa passione ma non hanno mai trovato nessuno che desse loro le giuste "direttive" per partire...
P.S. Poiche' queste MCU sono utilizzate in campi poco legali (sat hack in primis) voglio premettere che su questo sito, non insegnerò assolutamente, ne fornirò supporto di alcun tipo a questo utilizzo dei PIC micro, quindi, se leggete questi doc con la speranza di trovare la "patch che rimetta in sesto la vostra wafer card", beh vi dico fin d'ora che siete finiti sul sito sbagliato...
Bando alle ciance e cominciamo...
Tools
Editor testi di vostro gradimento. Microchip MPLab
Essay
Fondamenti
Iniziamo con una serie di nozioni che possiamo considerare propedeutiche al resto del corso di programmazione.
Prima di tutto, vediamo come è strutturato in genere un elaboratore: Un elaboratore è schematizzabile mediante una serie di sottoparti specializzate:
Un processore Una memoria Una o più unità che permettono l'output dei dati elaborati Una o più unità che permettono l'input dei dati da elaborare.
Le unità di input/output sono diverse: tastiera, mouse, scanner, monitor, stampante, i led del modem, ecc.
La memoria permette la memorizzazione delle informazioni in corso di elaborazione, o gia elaborate, o che comunque attendono di essere comunicate all'esterno. La memoria, solitamente è organizzata in registri.
Senza addentrarci troppo sull'implementazione hardware (dobbiamo imparare a programmarli, non a costruirli ;)), possiamo fare un breve excursus su come è strutturato in genere un microprocessore al suo interno.
All'interno di ogni processore sono presenti alcune unita':
Unità ALU (Logica matematica) Unità di Interpretazione dei comandi (i così detti opcode) Una serie di registri con funzioni specializzate.
ALU: Si occupa di tutti i calcoli e delle operazioni matematiche che devono essere compiute sui dati da elaborare, al suo interno ha dei registri che servono a memorizzare i dati in transito e in corso di elaborazione.
Registri: sono delle zone di memoria che permettono la conservazione di un dato: immaginate di stabilire un codice con quattro lampadine: se accendiamo la prima, abbiamo un dato, la prima e la seconda, un altro e così via... Abbiamo realizzato un registro ;) Nei processori i registri sono fatti esattamente cosi'... sequenze di bit, che possono assumere valori disparati, in base alle loro condizioni di 1 o 0 (acceso o spento).
Unità di elaborazione istruzioni: come ben sappiamo ogni processore ha una serie di comandi (set di istruzioni) che permettono a questo di svolgere tutti i calcoli che servono per l'elaborazione dei dati.
La bellezza di un pic sta proprio in questo: in quel piccolo ragnetto nero, sono racchiuse, una memoria, un processore e delle porte dati che permettono di comunicare con il mondo esterno... in pratica un computer di due centimetri cubici...
Fantastico no?!? :)
Ma torniamo ai nostri processori... Un processore esegue un programma, il programma non è altro che una sequenza di operazioni da compiere per ottenere un risultato da dei dati di partenza; (lo facciamo anche noi, quando ad esempio svolgiamo una operazione di somma con carta e penna). Questa unità si occupa di interpretare i dati che arrivano sotto forma di opcode.
Cosa sono gli opcode?
Facciamo alcune premesse:
Noi siamo abituati al sistema decimale, vuoi perché lo usiamo in maniera inconscia da tanto di quel tempo, vuoi perché forse siamo geneticamente predisposti al suo uso (abbiamo dieci dita sulle mani e dieci sui piedi), quindi per molti di noi la sola idea di iniziare a contare in un sistema diverso, causa paura... In realtà è solo una questione di abitudine, se madre natura ci avesse fatti evolvere con otto dita per mano, non avremmo avuto nessuna difficoltà a "pensare" in esadecimale. Ma veniamo a noi, un sistema numerico è definito dalla sua base (base dieci, base otto, base due o base sedici) e la base indica quelle che sono le cifre che costituiscono i numeri del sistema stesso. Ad esempio, nel caso del sistema decimale (base dieci) le cifre sono appunto dieci: 1, 2, 3, 4, 5, 6, 7, 8, 9 e 0. Tutti i numeri sono costituiti da una sequenza di queste dieci cifre. Si parla anche di sistema posizionale, vediamo perché... il numero 10 è più piccolo del numero 100... perché? Entrambi sono costituiti dalle stesse cifre (zero e uno)... sì ma il numero cento ha uno zero in piu'... ma lo zero di per se' non significa nulla, è la sua posizione quella che importa (infatti se scrivessi 1,10 o 01,10 direi la stessa cosa, l'aggiunta dello zero daventi all'uno, non significa nulla e non cambia il numero). Il sistema posizionale, non fa altro che 'attribuire un "peso" alle cifre in base a quella che è la loro posizione in una sequenza di numeri, mediante l'uso delle potenze della base di numerazione. ('zzo dici pho'???? hehehe calma... faccio un esempio) Immaginiamo di dover rappresentare graficamente la scomposizione di un numero: 129 voi cosa direste? Direste, come vi hanno insegnato dalle elementari: Un centinaio, Due decine, e nove unita'. Giusto. Ma che vuol dire in termini di utilizzo delle potenze della base?
Semplice.
Cento a quanto è uguale se lo voglio scomporre per dieci? 100 = 1 * 10 * 10 = 1 * 10^2
(^ indica l'elevamento alla potenza, * il prodotto)
Capito ora? Torniamo al nostro esempio: 129
129 = 100 + 20 + 9 cioe'
1 * 10 * 10 + 2 * 10 + 9 <---????? e il nove?
hehehe... vi hanno mai detto che per definizione qualunque numero elevato alla potenza zero fa sempre uno? Si'? Quindi...
129 = 1 * 10^2 + 2 * 10^1 + 9 * 10^0 = 1*100 + 2*10 + 9*1 = 129
(se avete notato, la prima potenza del dieci è il due, mentre il numero 129 ha tre cifre; praticamente si fa sempre cosi': si contano le cifre che compongono il numero, si sottrae uno al risultato, poi si numerano gli esponenti in modo decrescente a partire dal risultato ottenuto, fino ad arrivare allo zero). Facile no? No?... pensateci meglio... è un concetto che utilizzate sempre per fare i conti... quindi se sapete fare una addizione ed una moltiplicazione, potete anche capire questo ragionamento (ahem... lo fanno i ragazzetti alle scuole elementari... :b)
Quindi, ricapitolando, un sistema di numerazione, è costituito da un numero di simboli pari a quelli della sua base, i numeri che esso rappresenta vengono "sviluppati" come somma delle potenze della base moltiplicate per ogni cifra del numero, prese in ordine decrescente a seconda della posizione che esse occupano nel numero stesso. Tutto qui'... Ora non importa che siano 2, 8, 10 o 16 cifre, l'importante è che sia acquisito questo concetto... :)
Per quanto concerne l'aritmetica binaria, il sistema di numerazione è lo stesso, con sole due cifre pero', lo zero e l'uno (voi vi chiederete perché si utilizza proprio questo nei computer?... hehehe perché è l'unico sistema numerico rappresentabile con due soli stati: acceso o spento, corrente si', corrente no, uno o zero
Un numero binario non è altro che una sequenza di zero e uno:
1011011101
Piuttosto criptico... ma vediamo come è possibile convertirlo in decimale. Come abbiamo già detto, anche in questo caso si tratta di un sistema di numerazione posizionale, solo che in questo caso la base è il due, quindi il numero sarà rappresentato dalla somma delle potenza crescenti di due, moltiplicate per le cifre del numero:
= 1 * 512 + 0 * 256 + 1 * 128 + 1 * 64 + 0 * 32 + 1 * 16 + 1 * 8 + 1 * 4 + 0 * 2 + 1 * 1 =
= 512 + 0 + 128 + 64 + 0 + 16 + 8 + 4 + 0 + 1 = 733(d)
Convertire un numero decimale in binario è leggermente più difficoltoso. Bisogna trovare quelle potenze di due che, quando sommate tra loro, danno il valore del numero decimale. Il metodo più semplice consiste nel lavorare con la potenza di due più grande a decrescere fino alla potenza 'zero'.
Consideriamo ad esempio il valore decimale 1359:
· 2^10 = 1024, 2^11 = 2048, quindi 1024 è la potenza di due minore di 1359. Si sottrae 1024 da 1359 e si inizia il numero binario con un '1'. Binario ="1", decimale: 1359 - 1024 = "355".
· La successiva potenza di due, è 2^9 = 512, ed è maggiore di 355, così si aggiunge Uno zero al numero binario e si prosegue. Binario = "10", decimale 355
· La potenza successiva è 2^8 = 256 (minore di 355), si aggiunge un uno al numero Binario, si sottrae 256 a 355 (risultato 79) e si prosegue. Binario = "101", decimale 79.
· 128 (2^7) è maggiore di 79, così si aggiunge uno zero alla stringa binaria binario = "1010", decimale rimane 79.
· La successiva potenza di due (2^6 = 64) è minore di 79, così si inserisce un uno nel numero binario e si sottrae 64 a 79. Binario = "10101", resto decimale 15.
· 15 è più piccolo di 32 (2^5) così si inserisce uno zero e si prosegue. Binario = "101010", decimale ancora 15.
· 16 (2^4) è ancora maggiore di 15, quindi, si inserisce uno zero nella stringa e si prosegue. Binario = "1010100", decimale 15
· 2^3 (otto) è minore di 15, quindi, si inserisce un uno nella stringa e si sottrae 8 a 15. Binario = "10101001", resto decimale 7.
· 2^2 è minore di sette, quindi si sottrae a sette e si aggiunge un uno alla stringa. Binario = "101010011", resto decimale 3.
· 2^1 è minore di tre, quindi, si inserisce un uno nella stringa binaria e si sottrae due da tre. Binario = "1010100111", decimale, ora 1.
· In fine il risultato decimale è uno (2^0), quindi si inserisce un uno alla fine della stringa binaria. Il risultato binario finale è: "10101001111".
Semplice... (oddio... mi sa che l'ho detta grossa :b), almeno teoricamente... poco pratico da utilizzare... ma questo passa la tecnologia, al momento... ;)
(nota: l'esempio di conversione binario - decimale, l'ho spudoratamente scopiazzato dal libro "The Art of Assembly Language", però non vi lamentate visto che ve l'ho pure tradotto... ;))
Per quanto riguarda le operazioni effettuabili sui binari, sono definite tutte le stesse operazioni che si possono fare sugli altri sistemi di numerazione (decimale in primis). Faremo un esempio sulle addizioni, ma non andremo oltre, vabbe' la cultura, ma fare una trattazione completa sui sistemi di numerazione mi pare un po eccessivo.
Le addizioni in binario sono identiche a quelle in decimale, solo che il "riporto" scatta quando già si somma 1 + 1. Es.
1 1 0 1 0 + (Da dx a sx: 0+1 fa 1; 1+1 fa
10: 0 e riporto di 1; 1+1 fa 10: 0
e porto 1;
1 1 1 1 = 1+1 e 1 di riporto fa 11: 1 e porto 1;
------------- 1+1 fa 10)
1 0 1 0 0 1
Per quanto concerne la sottrazione, le regole sono identiche, tranne che al "prestito della decina" si sostituisce il prestito dell'uno, e il numero che presta diventa zero (viene sottratto di uno, tutto qui').
Idem per divisioni, moltiplicazioni, e operazioni derivate da queste (elevamento a potenza, radici ennesime, ecc.).
Penso che non sia necessario descrivere anche la conversione decimale - binario, visto che non credo che tra voi ci sia qualche folle che abbia intenzione di mettersi a convertire numeri da una base all'altra 'a mano'. ;)
Cenni di aritmetica esadecimale
L'aritmetica esadecimale (hex, con un abuso di linguaggio) si fonda sugli stessi principi che abbiamo visto con il sistema di numerazione binaria, la sola differenza è data dalla presenza di sedici cifre. Noi abbiamo solo dieci simboli numerici, e allora come facciamo? Facile, utilizziamo come "cifre" le prime sei lettere dell'alfabeto, otteniamo quindi che il sistema esadecimale è composto dalle cifre:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f.
Come sempre, la base è sedici, e i numeri si rappresentano come somma delle potenza decrescenti della base, moltiplicate per le cifre che costituiscono il numero.
13af(h) = 1*16^3 + 3*16^2 + a*16^1 + f*16^0
Con l'accortezza di tenere presente che: a = 10, b = 11, ... , f = 15.
Nel nostro caso, quindi: 1*4096 + 3*256 + 10*16 + 15*1 =
= 4096 + 768 + 160 + 15 = 5039(d)
Il "contare" in questa numerazione, segue queste regole:
1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f, 10, 11, ... 1a, 1b, ... 1f, 20, ... 1ff, 200, ... 1fff, 2000, ...
e così via, in parole povere la "esa-decina" scatta dopo cinque numeri in più rispetto al sistema decimale.
Le operazioni, sono tutte definite, e seguono le stesse regole che abbiamo già visto.
Per la addizione, il riporto scatta al raggiungimento del numero 'f':
11
ad12 + (2+1 fa 3; 1+1 fa 2; d+d fa 1a, scrivo 'a'
e riporto 1; f+e fa 1c e 1 di riporto, 1d)
fe10 =
-------
1da23
Stesso procedimento si applica per le altre operazioni, sempre con l'accortezza di tenere presente che, non si "arriva" a dieci ma a sedici. Sono sicuro che nessuno di voi si metterà mai a fare operazioni di questo tipo 'a mano', visto e considerato che esistono calcolatrici che permettono di fare questi conti molto velocemente, pero', una trattazione dei metodi di numerazione diverse da quelle che siamo abituati ad utilizzare, mi sembrava doverosa, per completezza del testo.
Conversioni binario - esadecimale
Passiamo ora ad una parte più semplice: le conversioni di base. Anche in questo caso, non vi servirà effettivamente a molto, visto che le macchinette calcolatrici (anche quella del windoze) lo fanno per voi, ma sapere come funziona una determinata cosa, è molto più bello di farla 'a pappagallo'... o no?! ;)
Tratteremo solo la conversione binaria esadecimale e viceversa, visto che come passare al sistema decimale, da questi ultimi, lo abbiamo già visto nei paragrafi precedenti.
Vi inserisco la seguente tabella:
| BINARIO | ESADECIMALE |
| 0001 | 01 |
| 0010 | 02 |
| 0011 | 03 |
| 0100 | 04 |
| 0101 | 05 |
| 0110 | 06 |
| 0111 | 07 |
| 1000 | 08 |
| 1001 | 09 |
| 1010 | 0A |
| 1011 | 0B |
| 1100 | 0C |
| 1101 | 0D |
| 1110 | 0E |
| 1111 | 0F |
|_________|_____________|
tab. 1.1
La conversione binario - esadecimale e viceversa, è molto semplice, basta prendere il numero binario, suddividerlo a gruppi di quattro bit da destra verso sinistra, colmare l'ultimo gruppetto a sinistra con il necessario numero di zeri, e poi convertire ogni gruppetto di bit con la tabella in alto.
Es.
^
Zero immesso
= 0101 -> 5
1010 -> A
1010 -> A
1101 -> D = 5AADh (*)
Semplice non trovate? In modo del tutto analogo, si passa dall'esadecimale al binario, vi risparmio l'esempio, tanto se non avete capito, hehehe... l'asm non fa per voi ;)
Andiamo avanti con alcune precisazioni, scrivendo i numeri binari, decimali, esadecimali, avrete sicuramente notato, che lo ho fatti seguire da un suffisso, quest'ultimo serve per identificare velocemente il tipo di numerazione in cui il numero è rappresentato. Chiaramente: d = decimale, b = binario, h = esadecimale, o = ottale e così via.
Nota: (*) Attualmente, far seguire un valore esadecimale con una "h" è una convenzione utilizzata dalla Intel, non una convenzione generale. L'assembler del 68000 e del 65c816 usati nei Machintosh e negli Apple II denota i numeri esadecimali facendoli precedere dal simbolo '$', mentre nei PIC micro si utilizza la notazione 0x. Vengono utilizzate anche altre convenzioni, quali ad esempio quella di indicare gli esadecimali facendoli precedere dal simbolo 0x (ma questa è più una convenzione utilizzata dai programmatori) Per quanto ci riguarda, non useremo prefissi e suffissi se il contesto di utilizzo sarà univoco, mentre in caso di equivoci, utilizzaremo i suffissi descritti sopra.
Ma torniamo ai nostri opcode... Abbiamo visto che l'unica maniera che un circuito ha di operare è mediante l'utilizzo di due stati: acceso o spento, On o Off, corrente si', corrente no. Da ciò nasce il bit (acronimo per binary digit: cifra binaria). Il processore, la memoria e tutto il resto, comunicano, elaborano e lavorano solo ed esclusivamente, scambiando segnali elettrici. Un segnale elettrico è un bit, la sua presenza (o il suo voltaggio) indica lo stato 1, la sua assenza (o un voltaggio più basso) indica lo stato 0. Abbiamo visto che per scrivere un programma, dovremmo comunicare al processore una sequenza immonda di uno e zero, opportunamente combinati. In questi casi la facilità di conversione dal sistema binario a quello esadecimale, viene ampiamente sfruttata per consentire una agevole memorizzazione dei dati. Nascono quindi gli opcode, che altro non sono che l'equivalente esadecimale di una istruzione in linguaggio assembler. Ad esempio:
BCF PORTB,4 12 06 01001000000110
Potete comprendere come sia più semplice variare un byte rispetto a una serie di bit, o come sia più semplice per uno sviluppatore, implementare un compilatore, che trasformi le istruzioni in opcode esadecimali invece di essere costretto a fare il passaggio diretto asm -> binario.
Come conseguenza logica di tutto questo discorso, nasce il linguaggio assembler (per gli amici asm) che permette di avere un "rapporto uno a uno" con quelle che sono le istruzioni specifiche di un dato processore.
Avrete sicuramente notato che esistono altri tipi di linguaggi, detti di "alto livello" (C, C++, Basic, Cobol, Fortran, ecc. ecc.) questi linguaggi, sono stati inventati per facilitare ulteriormente il compito dei programmatori, cioe' per permettere loro di scrivere un programma senza curarsi di quello che sia effettivamente il set di istruzioni del processore su cui il programma dovre' "girare" ma lasciando il compito della sua trasformazione in asm al compilatore (il programma che si usa per trasformare un sorgente di programma in un programma funzionante, per intenderci). I linguaggi di alto livello, non hanno una corrispondenza "uno a uno" con il set di istruzioni del processore, ma molto più elevata (un banale print "ciao mondo" in asm richiederebbe molti più passaggi di quell'unica linea di codice che viene utilizzata in un linguaggio di alto livello)
Noi in questo corso impareremo (o almeno ci proveremo ;)) a conoscere il linguaggio assembler dei microchip PIC, anche se per questi esistono dei compilatori che permettono di utilizzare per la loro programmazione, linguaggi di alto livello (tipo il C), ma scrivere codice ottimizzato (che richieda il minor numero di passaggi e di memoria, per ottenere una data operazione) è un'arte che i compilatori non sono in grado di svolgere come la mano di un bravo programmatore, insomma ci siamo capiti.
Fondamenti di architettura
Iniziamo questo capitolo della saga "programmazione PIC for Dummies" con un paio di nozioni di architettura
Partiamo da alcune definizioni:
RAM: Acronimo di Random Access Memory (memoria ad accesso casuale) è una memoria, suddivisa in registri, ognuno dei quali è direttamente accessibile, al contrario di una memoria ad accesso sequenziale (tipo un nastro magnetico) o semicasuale (un CD audio) ogni registro di questo tipo di memoria può essere raggiunto direttamente, senza "passare" per altri registri.
ROM: Read Only Memory (memoria a sola lettura) tutti i tipi di memoria che una volta scritte, non possono più essere modificate. Ne esistono di svariate tipologie (EEPROM, EPROM, FlashROM, ecc.) un esempio classico è la ROM dei commodore (i più vetusti tra noi sanno di cosa sto parlando) che conteneva il linguaggio basic del sistema.
EEPROM: Acronimo di Electrically Erasable Programmable ROM (memoria ROM cancellabile/programmabile elettricamente) un tipo di memoria ROM che può essere modificata (alla sola condizione di cancellarla e riscriverla tutta) è il tipo di memoria che ci interesserà più da vicino in questo corso.
Vediamo ora come è strutturato un PIC.
Abbiamo visto che i PIC non sono altro che dei microcomputer, che hanno la possilità di essere programmati e di essere utilizzati nelle applicazioni più disparate. Al loro interno è presente una eeprom, è questa che noi andiamo a programmare con le interfacce hardware e software che (si presume) tutti conosciamo. Mediante questi pic, utilizzando un opportuno programma possiamo pilotare un qualsiasi circuito (da una serie di led, a un display a cristalli liquidi, da un telecomando per cancelli a una chiave a trasponder)
I pic hanno parti dedicate all'ingresso dei dati (linee di input) parti dedicate all'uscita dei dati (linee di output) un processore che si occupa di interpretare le istruzioni del programma, una memoria ram che permette lo stoccaggio temporaneo dei dati da trattare o comunicare all'esterno.
Tutte queste informazioni sono reperibili, leggendo i cosiddetti "datasheet" (letteralmente: fogli dati) disponibili per la maggior parte dei pic esistenti.
Vediamo un esempio, considerando il più usato/abusato PIC: il Microchip 16F84.
Il 16F84 è un chip strutturato come un "ragnetto" nero con una serie di 9 piedini per lato, grande all'incirca 2x0.5 cm. Ogni piedino svolge una sua specifica funzione, per individuarli, i piedini sono numerati sequenzialmente, in senso rotatorio, a partire da una tacca posta nella scatoletta in plastica che contiene il chip vero e proprio: guardando il PIC dall'alto, con i piedini rivolti verso il basso, e con la tacchetta in alto, il pin numero uno è quello in alto a sinistra, scendendo verso il basso, sempre a sinistra troviamo i pin da 2 a 9, alla destra del pin 9 (dall'altro lato) troviamo il pin 10 e salendo verso l'alto, arriviamo al pin 18 (in corrispondenza del pin 1), per quanto riguarda la numerazione dei piedini, questa è una regola che possiamo considerare come generale: imparatela!! :D
Come dicevo prima, ogni piedino svolge una sua funzione specifica, nel nostro esempio: Pin 14: Alimentazione (positivo) Pin 5: Alimentazione (negativo) Pin 1,2,3,6,7,8,9,10,11,12,13,17,18: Linee di Input/Output Pin 4: Reset (come il tastino rosso sul case del PC :D) Pin 15 e 16: Linee di Input/output per il circuito di clock esterno.
Come vediamo, l'alto numero di porte di Input/Output (d'ora in avanti: I/O) nasce dalla necessità di rendere questi microcontroller il più flessibili possibile. Solitamente il protocollo di comunicazione tra PIC e mondo esterno è di tipo seriale (protocollo che necessita di due sole linee di dati: I/O, su cui i bit costituenti le informazioni scambiate, sono inviati uno di seguito all'altro, cioe' in serie) diversamente da microcontroller più potenti, o da vere e proprie CPU, nelle quali è utilizzato il protocollo parallelo: i bit di ogni dato, suddiviso in byte, word (due bytes), doubleword (due word: 4 bytes) vengono trasmessi tutti assieme, ognuno su una sua linea dati, ottenendo, chiaramente una maggiore velocità nella trasmissione dati.
All'interno dei pic, è presente una memoria eeprom che contiene il programma che viene eseguito, una ram che serve per stoccare i dati "volatili". La memoria eeprom è strutturata in una sequenza di records delle dimensioni di 14 bit (ontrariamente ai processori "più grandi" a cui di solito siamo abituati, che lavorano a gruppi di 16, 32 o 64 bit) la capacità totale della eeprom interna (la zona destinata al programma) dei PIC, varia a seconda delle versioni: il 16F84 ha 1Kword di eeprom(*), il 16F628 ne ha 2, il 16F876 otto.
Oltre alla eeprom, è presente come abbiamo gia detto, anche una RAM, anch'essa di dimensioni variabili a seconda della tipologia del PIC, sempre nel nostro caso (16F84) la ram è di 68 bytes e una zona di memoria detta DATA EEPROM (eeprom destinata a memorizzare dati non volatili, ad esempio le costanti di un programma, una tabella di dati e così via...) delle dimensioni di 62 bytes.
(*) NOTA: Non si parla di bytes nelle dimensioni di memoria ma di word, e comunque parliamo di una word "fuori standard" nel senso che la word dovrebbe essere una sequenza di due bytes, per un totale di 16 bit, mentre nei pic micro abbiamo visto che il record ha 14 bit e non i canonici 8, se volessimo quantizzare correttamente la dimensione in bytes della eeprom di un pic dovremmo fare questo calcolo: [(1024 word * 14 bit) / 8] * 1024 Che fornirebbe nel nostro caso, un quantitativo di memoria di 1.75 Kbytes.
In sintesi, quando definiamo il quantitativo di memoria di un PIC (Programmable Integrated Circuit, circuito integrato programmabile, maschile, quindi "un", e non come scrivono molti LA PIC... ;)) e parliamo di "Kappa" stiamo dicendo Kword ognuna di 14 bit e non Kbytes.
La PROGRAM EEPROM (zona in cui risiede il programma che "gira" nel pic) è organizzata secondo il seguente schema:
|.........VETTORE DI RESET........| ADDRESS 0x0000
|_________________________________|
|..VETTORE INTERRUPT PERIFERICHE..| ADDRESS 0x0004
|_________________________________|
|.................................|
|.................................|
. .
. .
. .
|.....ISTRUZIONI DEL PROGRAMMA....|
. .
. .
. .
|_________________________________| ADDRESS 0x03FF
Questo discorso, vale sempre per la nostra "cavia" il 16F84, per i suoi fratelli maggiori, si entra nel merito di divisione della memoria in pagine ognuna delle dimensioni di 2 Kwords, visto che per indirizzare ogni locazione di memoria, è necessario un numero maggiore di bit rispetto a quello del program counter che l'architettura microchip ha a disposizione.
'azzo sto dicendo?
Ok... mi spiego meglio...
Immaginate di dover individuare in una tabella fatta di quadratini, ogni quadratino, indicandolo con un numero in base sedici:
|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|
------------------------------
Per rappresentare i numeri in base sedici, abbiamo visto nella scorsa lezione, è possibile utilizzare il sistema binario. Quanti bit ci servono per poter indicare ogni valore contenuto nelle 16 caselle sopra? Ce ne servono almeno 4... (1 = 0001, ... ,F = 1111) Immaginate ora di dover lavorare con due tabelle uguali, non potrete con 4 bit individuare univocamente ogni casellina, vi servirà un ulteriore bit per individuare se stiamo numerando la prima o la seconda riga...
Con un numero maggiore di caselline da "numerare", ovviamente vi servirà un numero maggiore di bits... Nei pic c'è un record di 13 bits che fa questo (oltretutto, non tutti bit del registro svolgono la stessa funzione, quindi parliamo di meno di 13 bit)... Il program counter di un processore, non è altro che questo: un record di bits che indica al processore a quale casellina della memoria che contiene il programma siamo arrivati.
Questa tecnica viene definita "indirizzamento della memoria", nel campo informatico è presente in tutto: memoria ram, processori, HardDisk (la famosissima FAT: File Allocation Table, tabella di allocazione dei file)
In ogni locazione della memoria programma, possiamo trovare una istruzione (quello che il processore deve fare) e i suoi dati (una somma tra due valori, un prodotto e così via...).
Oltre alla program eeprom, nei pic abbiamo la DATA EEPROM, una zona di memoria che contiene le variabili interne atte al funzionamento del pic e le variabili o costanti, definite dal programmatore che ha sviluppato il programma che il pic deve eseguire.
La sua struttura è questa:
|..IA...|...IA..| LOC 0x00|0x80 Indirect Address: registri per indirizzamento indiretto
|-------|-------|
|.TMR0..|.OPTION| LOC 0x01|0x81
|-------|-------|
|..PCL..|..PCL..| LOC 0x02|0x82
|-------|-------|
|STATUS.|.STATUS| LOC 0x03|0x83
|-------|-------|
|..FSR..|..FSR..| LOC 0x04|0x84
|-------|-------|
|.PORTA.|.TRISA.| LOC 0x05|0x85
|-------|-------|
|.PORTB.|.TRISB.| LOC 0x06|0x86
|-------|-------|
|..---..|..---..| LOC 0x07|0x87
|-------|-------|
|.EEDATA|EECON1.| LOC 0x08|0x88
|-------|-------|
|.EEADR.|EECON2.| LOC 0x09|0x89
|-------|-------|
|.PCLATH|PCLATH.| LOC 0x0A|0x8A
|-------|-------|
|INITCON|INITCON| LOC 0x0B|0x8B
|-------|-------|
|.......|.......| LOC 0x0C|0x8C
|.......|.......|
|.......|.......|
|..68...|.......| 68 General Pourpose Registers
|..GPR..|.......| registri di uso generico
|..REG..|.......| mappati nel banco 0 (primo banco di memoria del pic)
|.......|.......|
|.......|.......|
|_______|_______|LOC 0x4F|0xCF
La zona di memoria da 0x00 a 0x0B e da 0x80 a 0x0C contiene i registri di "sistema" cioe' tutti i registri che vengono utilizzati internamente dal pic e che vengono gestiti mediante il suo set di istruzioni. I registri GPR, possono essere definiti dall'utente per contenere dati e/o viariabili e sono largamente utilizzati nei programmi di emulazione.
MCU Registers
Come promesso, iniziamo immediatamente questo capitolo affrontando un argomento delicato ma di importanza fondamentale nellla programmazione dei pic.
I REGISTRI
Abbiamo visto sopra che ogni cpu è dotata di registri che assolvono a funzioni specifiche. Come tutte le cpu, anche i pic (che non sono propriamente delle cpu, ma ci si avvicinano molto) sono dotati di una serie di registri con funzioni specifiche.
Lo schema della memoria dei pic destinata a contenere i registri e':
|..IA...|...IA..| LOC 0x00|0x80 Indirect Address: registri per indirizzamento indiretto
|-------|-------|
|.TMR0..|.OPTION| LOC 0x01|0x81
|-------|-------|
|..PCL..|..PCL..| LOC 0x02|0x82
|-------|-------|
|STATUS.|.STATUS| LOC 0x03|0x83
|-------|-------|
|..FSR..|..FSR..| LOC 0x04|0x84
|-------|-------|
|.PORTA.|.TRISA.| LOC 0x05|0x85
|-------|-------|
|.PORTB.|.TRISB.| LOC 0x06|0x86
|-------|-------|
|..---..|..---..| LOC 0x07|0x87
|-------|-------|
|.EEDATA|EECON1.| LOC 0x08|0x88
|-------|-------|
|.EEADR.|EECON2.| LOC 0x09|0x89
|-------|-------|
|.PCLATH|PCLATH.| LOC 0x0A|0x8A
|-------|-------|
|INITCON|INITCON| LOC 0x0B|0x8B
|-------|-------|
|.......|.......| LOC 0x0C|0x8C
|.......|.......|
|.......|.......|
|..68...|.......| 68 General Pourpose Registers
|..GPR..|.......| registri di uso generico
|..REG..|.......| mappati nel banco 0 (primo banco di memoria del pic)
|.......|.......|
|.......|.......|
|_______|_______| LOC 0x4F|0xCF
Ora cercheremo di analizzarne i più comuni o utilizzati.
Lo STATUS register:
È il registro che individua lo stato del pic, in particolare contiene lo stato dei risultati della ALU (Aritmetical Logical Unit: unità logico aritmetica) lo stato seguente al reset e il bit che identifica le pagine della data memory.
È strutturato in questo modo:
|IRP|RP1|RP0|TO |PD | Z |DC | C |
-------------------------------
7 6 5 4 3 2 1 0 numero bit
La particolarità dell set di istruzioni dei pic micro è che è possibile accedere ai singoli bit di un registro indicandone il numero progressivo.
Vediamo cosa rappresentano i bit di questo registro (peraltro molto utilizzato nei programmi)
- IRP0 bit di pagina della data eeprom (0 = pagina 0, 1 = pagina 1)
- TO Bit di time out utilizzato nei conteggi durante le fasi di sleep
- PD Bit di power down utilizzato con funzioni simile al TO
- Z Zero Flag bit di controllo per le operazioni logico/matematiche
- DC Digital Carry (riporto) utilizzanto nelle operazioni dell'ALU
- C Carry bit come sopra ma con funzioni leggermente diverse.
Necessitiamo a questo punto di una breve digressione sul significato e sulla funzione dei bit Z e C. Ogni operazione che viene effettuata dal pic influenza lo stato di uno (o entrambi) di questi bit, che vengono utilizzati come bit di controllo per effettuare delle "scelte" per poter decidere se far fare una serie di operazioni al programma piuttosto che un'altra. Ad esempio, il bit Z si setta (assume valore 1) ogni qualvolta una operazione logica o matematica, dia come risultato zero, diversamente rimane resettato (valore 0) se le operazioni logiche o matematiche non ritornano zero. Il discorso del bit C è analogo, se una somma tra due valori eccede la dimensione dei registri il bit C viene settato, in caso di sottrazione con risultato negativo, il bit viene settato parimenti. Lo STATUS register viene utilizzato quindi per controllare lo stato del pic e regolarsi di conseguenza.
Il registro PCLATH
Questo è un altro dei registri fondamentali per il funzionamento del programma che gira nel pic, infatti si tratta del registro program counter (il registro che si occupa, come abbiamo gia visto, di indicare al processore qual è la successiva istruzione da eseguire) Questo registro è suddiviso in due parti:
|PCH|PCL|
-------
Il PCL è la parte bassa del registro (bits da 0 a 7) e viene utilizzata direttamente per l'indirizzamento della memoria del programma, è inoltre direttamente accessibile. Il PCH è la parte alta del registro (bits da 8 a 12) e non è direttamente accessibile mediante le istruzioni del programma, mediante la combinazione dei bit di questo registro è possibile indirizzare tutta la program memory del pic.
A questo punto è necessario introdurre un ulteriore concetto:
Lo STACK
Lo stack (dall'inglese, letteralmente "pila") è una struttura in memoria organizzata come una pila di oggetti (immaginate una pila di monetine) gli oggetti vengono inseriti dall'altro (in gergo: push) e ritirati sempre dall'alto (in gergo: pop) è una struttura di tipo LIFO (Last In First Out: l'ultimo ad entrare è il primo ad uscire) [curiosita': il contrario di una struttura a coda, dove il primo ad arrivare è il primo ad essere "servito"; FIFO: First In, First Out (Es. i dati in transito su un collegamento seriale sono del tipo FIFO)]
A cosa serve questa struttura dati?
È presto detto...
Immaginate di avere un programma che debba fare alcune cose sequenzialmente:
PROGRAMMA - Richiama la procedura di ricezione dati -- Richiama la procedura di lettura caratteri dalla tastiera - Ritorno alla procedura di ricezione dati - Richiama altre routine (tipo elaborazione testo o altro) ... FINE PROGRAMMA
Questa sequenza immaginaria di codice può essere implementata mediante l'utilizzo delle subroutines quindi nella memoria destinata al programma ci dovrà essere una sequenza del tipo:
call input_data
call other_routines
...
end main
...
input_data
call char_read
istruzioni
...
return
...
other_subroutines
istruzioni
...
return
char_read
istruzioni
...
return
Cosa succede al nostro programma?
Il codice viene eseguito sequenzialmente, e ogni volta che viene incontrata una call, il controllo viene mandato alla zona di memoria che contiene il sottoprogramma che viene richiamato, alla fine del sottoprogramma abbiamo l'istruzione "return" che indica al processore che il sottoprogramma è finito e si può tornare all'istruzione dopo la call. Abbiamo visto che ogni istruzione del programma, occupa una(piu') locazione(i) di memoria, indicate univocamente da un loro indirizzo, tali istruzioni sono indicate al processore dal registro program counter, al momento del salto alla subroutine, nel registro PC (program counter, d'ora in avanti PC) viene scritto l'indirizzo della prima locazione di memoria della sequenza di quelle che costituiscono il sottoprogramma che deve essere eseguito... Ora voi, miei piccoli allievi volenterosi di apprendere, vi chiederete: "phobbino, ma al return cosa succede?? Come si ritorna alla istruzione immediatamente successiva a quella che ha richiamato la sub??" Domanda più che lecita, per poter effettuare il ritorno nel punto esatto dopo il salto alla sub, nel registro PC deve essere riscritto l'indirizzo successivo a quello che c'era nel momento in cui è stata chiamata la subroutine... dove cavolo lo andiamo a riprendere??? L'abbiamo sovrascritto con quello della sub!!!! Azz!!! E adesso?!?!?!? Calma... è qui che subentra lo stack. Ogni qualvolta viene richiamata una sub, il contenuto del PC automaticamente viene incrementato di uno e memorizzato nella prima locazione libera che si incontra nello stack. Nel PC viene quindi scritto l'indirizzo da cui continuare l'esecuzione del programma e alla fine di questa sub, nel momento del ritorno, viene prelevato l'elemento che era memorizzato nello stack e rimesso nel PC così che l'istruzione successiva alla call possa venire eseguita regolarmente. Alla luce di questo esempio, capiamo anche perché viene utilizzata una struttura dati di tipo LIFO... Riguadiamo l'esempio di sopra immettendo dei numeri di riga, e visualizziamo contemporaneamente il contenuto dello stack (ipotizzando di averne uno con 4 locazioni) e del PC (curiosita': è quello che avviene quando si fa il debug di una applicazione, il debugger visualizza programma, memoria, registri e tutto quello che succede ad ogni istruzione che facciamo eseguire al processore)
Sequenza in memoria delle istruzioni del programma:
0001 call input_data
0002 call other_routines
0003 ...
000n end main
...
1000 input_data
1001 call char_read
1002 istruzioni
100k ...
100n return
...
2000 other_subroutines
2001 istruzioni
2002 ...
200n return
...
3000 char_read
3001 istruzioni
3002 ...
300n return
Sequenza di esecuzione effettiva:
0001 call input_data PC = 0001 - STACK = [0000|0000|0000|0000]
------- CALL ----->
1000 input_data PC = 1000 - STACK = [0000|0000|0000|0002]
1001 call char_read PC = 1001 - STACK = [0000|0000|0000|0002]
------- CALL ----->
3000 char_read PC = 3000 - STACK = [0000|0000|1002|0002]
3001 ... PC = 3001 - STACK = [0000|0000|1002|0002]
3002 istruzioni PC = 3002 - STACK = [0000|0000|1002|0002]
300n return PC = 300n - STACK = [0000|0000|1002|0002]
------- RETURN --->
1002 istruzioni PC = 1002 - STACK = [0000|0000|0000|0002]
100k ... PC = 100k - STACK = [0000|0000|0000|0002]
100n return PC = 100n - STACK = [0000|0000|0000|0002]
------- RETURN --->
0002 call other_routines PC = 0002 - STACK = [0000|0000|0000|0000]
------- CALL ----->
2000 other_subroutines PC = 2000 - STACK = [0000|0000|0000|0003]
2001 istruzioni PC = 2001 - STACK = [0000|0000|0000|0003]
2002 ... PC = 2002 - STACK = [0000|0000|0000|0003]
200n return PC = 200n - STACK = [0000|0000|0000|0003]
------- RETURN --->
0003 ... PC = 0003 - STACK = [0000|0000|0000|0000]
000n end main PC = 000n - STACK = [0000|0000|0000|0000]
Visto a cosa serve la struttura LIFO dello stack? Immaginate se fosse di tipo FIFO, ritorneremmo dalla call a char_read sull'indirizzo 0002 e al return della input_data ritorneremmo a 1002, con chiaro imputtanamento della struttura del codice :)
Senza complicarci ulteriormente la vita, con il fatto che lo stack oltre agli indirizzi del PC può contenere anche eventuali dati che devono essere passati alla subroutine, possiamo dire di aver concluso questa breve (rotfl) parentesi.
Torniamo ai nostri registri.
Un'altra serie di registri molto importanti nei pic sono i registri che gestiscono la data eeprom del pic; sono quattro e più precisamente:
- I tre bit 7, 6 e 5 non sono utilizzati. - Il bit EEIF (EEprom Interrupt Flag) per controllare le operazioni di scrittura sulla data eeprom, se settato (1) la operazione di scrittura è terminata, se resettato (0) non è terminata o non è ancora iniziata. - WRERR (WRite ERRor flag) controlla eventuali errori di scrittura: settato = errore resettato = scrittura completata con successo. - WREN (WRite ENabled) Abilita i cicli di scrittura eeprom (i dati nella eeprom non vengono scritti tutti assieme, in parallelo, ma in maniera seriale, quindi, per immettere una sequenza di dati nella eeprom, è necessario utilizzare dei cicli) bit settato = inizio sequenza ciclo di scrittura, bit resettato = fine sequenza. - WR (WRite control flag) Abilita la scrittura in eeprom. Il bit viene settato all'inizio del processo di scrittura, e resettato alla fine del processo. - RD (ReaD control flag) Flag di controllo per le operazioni di lettura eeprom. Viene settato all'inizio dell'operazione di lettura e resettato alla fine.
- Secondo registro di controllo (fisicamente non presente, implementato in software in memoria)
- Registro contenente i dati prelevati dalla eeprom, direttamente accessible mediante l'uso del puntatore FSR.
- Registro contenente gli indirizzi dei dati da leggere (o scrivere) in data eeprom, utilizzato per le tecniche di indirizzamento a memoria.
Tralasciamo per il momento la descrizione e il funzionamento degli altri registri presenti in queste MCU (MicroController Unit) e concludiamo questo capitolo con alcuni cenni delle tecniche di indirizzamento. Esistono due tecniche di indirizzamento: indirizzamento diretto ed indirizzamento indiretto. L'indirizzamento diretto, consiste nel fornire direttamente alla cpu l'indirizzo di memoria su cui operare. La tecnica dell'indirizzamento indiretto, è leggermente più complicata, si basa infatti sul concetto di puntatore (POINTER). In pratica, immaginate di avere in una locazione di memoria un dato, in un registro l'indirizzo della locazione contente il dato, i registro in questione prende il nome di puntatore. La cpu può accedere direttamente alla locazione di memoria in cui è contenuto il dato (indirizzamento diretto) oppure utilizzare il puntatore della locazione di memoria e accedere a questa prelevando l'indirizzo dal puntatore (indirizzamento indiretto).
Facendo un esempio pratico, nei nostri PIC, sono presenti due registri che si occupano dell'indirizzamento: il registro FSR (fisicamente implementato nel chip) e il registro INDF (che non è fisicamente implementato) il registro FSR ha la funzione di puntatore (contiene l'indirizzo della locazione di memoria che contiene il dato) il registro INDF contiene il dato. In parole più povere, FSR punta alla locazione di memoria che contiene il dato, INDF legge il contenuto di FSR e memorizza direttamente il dato contenuto nella locazione puntata da FSR (complicato?... ok, vi copio un esempio preso "paro paro" dal datasheet ;))
Immaginiamo di avere in memoria questa situazione:
INDIRIZZO LOCAZIONE: 0005 [10] <- valore contenuto INDIRIZZO LOCAZIONE: 0006 [0A] <- valore contenuto
Se carichiamo in FSR il valore 0005, il registro INDF conterrà il valore 10 Se incrementiamo FSR di 1 (valore 0006), INDF conterrà il valore 0A.
Più semplice cosi'? :)
Instruction Set
L'assembler di queste mcu è costituito da sole 35 istruzioni, ciò se da un lato rende molto facile il suo apprendimento, dall'altro ci costringe ad utilizzare costrutti particolari per ottenere operazioni che in processori con un set di istruzioni più "avanzato" sono svolte da singoli opcode. Comunque, tutto alla fine si risolve con un po' di pazienza ed esperienza, e magari questa limitazione rende anche la programmazione più divertente (tocca far funzionare di più il cervello... e se ciò vi diverte... beh... allora diventa un bell'hobby)
Bando alle ciance e cominciamo.
Necessaria è una premessa:
una istruzione di programma, normalmente ci permette di effettuare una operazione su uno o più dati (un'assegnazione, una somma, una differenza, un controllo, un cambio di registro, ecc.). Questa è una regola generale che vale per qualsiasi linguaggio di programmazione stiamo utilizzando. Nel linguaggio assembler, abbiamo solitamente istruzioni costituite da una doppietta o da una tripletta di dati:
opcode dato
opcode dato1 dato2
L'opcode (OPeration CODE) è l'istruzione in senso assoluto (l'operazione che deve essere effettuata dal processore). Le istruzioni del primo tipo sono dette "istruzioni ad un operando", quelle del secondo tipo "istruzioni a due operandi".
Queste, solitamente sono le due tipologie di istruzioni più diffuse, che vengono implementate in assembler (qualsiasi CPU stiamo utilizzando).
Utilizzeremo alcuni descrittori, per generalizzare il tipo di operandi utilizzati nell'assembler di questi microcontroller.
Descrittori dei registri:
f : file register (registro file) uno dei registri a otto bit mappati nella zona della data eeprom (indirizzi data eeprom da 0x00 a 0x4F (1)) tra cui ad esempio, TMR0, STATUS, FSR e così via.
W : Working register (accumulatore) il registro della CPU con funzioni generiche (utilizzabile per qualsiasi costrutto si voglia implementare)
b : numero del bit in un registro di otto bit (la posizione occupata dal bit che può variare da 0 a 7)
k : literal field (valore letterale) può essere un dato immediato, una costante o l'indirizzo di una variabile
d : destination selector (bit selettore di destinazione) permette di decidere dove immettere il risultato di una operazione tra due registri.
x : bit che non sono gestibili via assembler e che comunque sono gestiti dal compilatore e messi a 0 in automatico.
(1) NOTA: chiaramente il numero di registri GPR (general pourpose) varia a seconda del PIC utilizzato (il 16F84 ne ha 68, i suoi fratelli maggiori ne hanno di piu')
Tipi di istruzioni:
Il set di istruzioni di questi PIC si può suddividere in tre categorie:
Byte-Oriented operations: operazioni orientate ai bytes
Bit-Oriented operations: operazioni orientate ai singoli bit
Literal and control operations: operazioni orientate a dati di tipo "literal" e di controllo.
La struttura generale dei registri di questi tipi di istruzioni assume questa forma:
Byte Oriented:
______________________________
| OP CODE | d | f REGISTER |
f : file register (8 bit)
Bit Oriented
______________________________
| OP CODE | b BIT#| f REGISTER |
Literal e control
______________________________
| OP CODE | k literal |
k : valore immediato (valore diretto a otto bit)
Call e Goto
_____________________________
|OP CODE| k literal |
Set di istruzioni
- ADDLW (ADD Literal and W)
[label] ADDLW k -> W = W + k
Somma il valore contenuto in W con il valore definito da k, dove k può essere una costante, un valore immediato o un indirizzo di memoria, con la limitazione che k sia compreso tra 0 e 255. Questa istruzione modifica lo stato dei bit C, Z e DC del registro STATUS.
- ADDWF (ADD W and F) (2)
[label] ADDWF f, d -> destinazione = W + (f)
Somma il valore contenuto in W con il valore definito da f (f è un registro) e a seconda dello stato di d, il risultato dell'operazione di somma viene posto in W (bit d = 0) o in f stesso (se d = 1). Questa istruzione modifica lo stato dei bit Z, C e DC del registro STATUS.
- ANDLW (AND Literal with W) (2)
[label] ANDLW k -> W = W and k
Viene effettuata l'operazione logica di AND tra W e il valore definito da k, che può essere una costante, una etichetta di dato (label) o comunque un valore compreso tra 0 e 255 (dimensione di 8 bit). L'operazione modifica lo stato dei bit Z del registro STATUS.
(2) NOTA: Per chi non la conoscesse l'and è una operazione logica che viene effettuata tra due valori, "confrontando" i bit che li costituiscono (confronto bit a bit), seguendo quella che viene definita "tabella di verita'"
-----------
0 | 0 | 0
-----------
1 | 0 | 1
Il risultato di questa operazione soddisfa le regole della tabella vista sopra, l'and viene effettuato tra i bit dei due valori che occupano la stessa posizione, otteniamo ad esempio: 11001100 and 11110000 = 11000000 (in esadecimale: CC and F0 = C0).
- ANDWF (AND W with F)
[label] ANDWF f,d -> destinazione = W and (f) (3)
Viene effettuato un and tra il valore contenuto in W e quello contenuto in f, a seconda dello stato di d, il risultato viene messo in W o f stesso (stessa procedura seguita nell'istruzione ADDWF). Viene modificato lo stato del bit Z del registro STATUS.
(3) NOTA: Facciamo un esempio:
addwf EEADR2,w ;somma w a EEADR2 e mette il risultato in EEADR2
movwf EEADR2 ;muove EEADR2 in W
La struttura dei registri della program eeprom nei pic e', come abbiamo visto, costituita da 14 bit, i quali sono divisi in campi per gli opcode, per i dati e così via. Nelle istruzioni del tipo addWF, subWF, movWF il registro destinazione è identificato dal bit d.
La domanda che sorge a questo punto e': come facciamo a settare o meno questo bit? Vediamolo visivamente:
_____ _____ _______________
|x|x|x| |1|1|1| |d|f|f|f|f|f|f|f|
I bit da 13 a 10 non sono utilizzati, i bit da 10 a 8 rappresentano l'opcode (ADDWF) poi abbiamo i bit da 6 a 0 che sono il nostro "file register", il bit 7 è il famoso bit "d" se questo bit è settato, il risultato dell'operazione viene messo nel file register stesso, diversamente viene messo in W. Il bit "d" viene settato dal compilatore nel momento in cui nell'istruzione in assembler viene immesso il secondo operando (nel nostro esempio: addwf EEADR2, w.Semplice no?!? ;)
- BCF (Bit Clear Flag)
[label] BCF f,b -> bit "b" del registro (f) = 0
Il bit numero "b" (ricordiamo la convenzione che la numerazione dei bit parte dal bit 0 in poi) del registro "f" viene messo a zero (cleared). Questa operazione non influenza nessun flag dello STATUS.
- BSF (Bit Set Flag)
[label] BSF f,b -> bit "b" del registro (f) = 1
Come sopra, solo che il bit "b" viene messo a 1 (set).
- BTFSS (Bit Test Flag Skip if Set) (4)
[label] BTFSS f,b -> controlla lo stato del bit "b" del registro (f)
Viene controllato il bit "b" del registro f, se il bit è settato (1) la istruzione successiva nel flusso del programma non viene eseguita (skip: eseguita come NOP = No Operation) se il bit è a zero, viene eseguita l'istruzione successiva. Questa istruzione non influenza nessun bit del registro STATUS
- BTFSC (Bit Test Flag Skip if Clear) (4)
[label] BTFSC f,b -> controlla lo stato del bit "b" del registro (f)
Viene controllato il bit "b" del registro f, se il bit è a zero l'istruzione successiva viene saltata (eseguita come NOP) se il bit è settato la stessa viene eseguita. Questa istruzione non influenza nessun bit del registro STATUS
(4) NOTA: Entrambe le operazioni richiedono due cicli macchina (cicli di clock)
- CALL (Chiamata a subroutine)
[label] CALL k -> chiama la subroutine identificata dalla label k
Viene chiamata la sub indicata dalla label k (valore a 11 bit) k può variare tra 0 e 2047 (la famosa limitazione di due kbyte di memoria direttamente indirizzabile dal registro PC), il valore contenuto in PC (come abbiamo gia visto) viene addizionato di 1 e memorizzato nella prima posizione libera dello stack, il valore k a 11 bit, viene memorizzato in PC, i bit 4 e 3 del registro PCLATH vengono spostati nelle posizioni 11 e 12 del registro PC. Questa istruzione richiede due cicli macchina e non modifica nessun bit del registro STATUS.
- CLRF (Clear f)
[label] CLRF f -> (f) = 0
Il contenuto del registro f viene azzerato. Viene setttato il bit Z dello STATUS.
- CLRW (Clear W)
[label] CLRW -> W = 0
Istruzione senza operandi, permette di azzerare il contenuto del registro W. Viene settato il bit Z dello STATUS.
- CLRWDT (Clear Watchdog Timer)
[label] CLRWDT -> WDT = 0
Il registro wattchdog timer, viene resettato (messo a zero) Vengono modificati i bit TO (Time Out) e PD (Power Down) del registro STATUS. (alcuni dettagli più avanti).
- COMF (Complement f)
[label] COMF f,d -> destinazione = complemento su (f)
Il valore contenuto in f viene complementato (ogni bit viene invertito: es. 10010111 -> 0110100) il risultato dell'operazione viene memorizzato in f stesso o in W a seconda se d è settato o meno. L'operazione alza (setta) il bit Z dello STATUS.
- DECF (Decrement f)
[label] DECF f,d -> destinazione = (f) - 1
Il valore contenuto in f viene decrementato (diminuito di una unita') e il risultato viene posto in f o W sempre a seconda dello stato di d. Viene modificato lo stato del bit Z dello STATUS.
- DECFSZ (Decrement f, Skip if Zero)
[label] DECFSZ f,d -> destinazione = (f) - 1, skip se il risultato = 0
Il contenuto del registro f viene decrementato di una unita', il risultato dell'operazione viene salvato in W o in f stesso, a seconda dello stato del bit d, se il risultato è zero, viene saltata l'istruzione successiva (eseguita come NOP). Nessun bit dello STATUS viene influenzato. Anche questa operazione richiede due cicli macchina.
- GOTO (Salto incondizionato)
[label] GOTO k -> L'esecuzione salta alla label indicata da k
Questa istruzione memorizza nel registro PC il valore a 11 bit definito da k, i due bit 3,4 del PCLATH vengono memorizzati nelle posizioni 11 e 12 del registro PC. Nessun bit dello STATUS viene modificato. L'istruzione richiede due cicli macchina.
- INCF (Increment f)
[label] INCF f,d -> destinazione = (f) + 1
Il contenuto del registro f viene incrementato di una unita', il risultato viene memorizzato in W o f sempre a seconda dello stato del bit d. Viene modificato lo stato del bit Z dello STATUS.
- INCFSZ (Increment f, Skip if Zero)
[label] INCFSZ f,d -> destinazione = (f) + 1, skip istruzione successiva se f = 0
Il contenuto del registro f viene incrementato di una unita', il risultato viene memorizzato in f o W sempre a seconda dello stato del bit d. Se il risultato è Zero, l'istruzione successiva viene saltata (eseguita come NOP). Non viene influenzato il registro STATUS, anche questa rientra nelle operazioni a due cicli macchina.
- IORLW (Inclusive OR Literal with W) (5)
[label] IORLW k -> W = W or k
Il contenuto del registro W viene "OR-ato" (non il maschio del pesce che si cucina "all'acquapazza", ma l'operazione logica OR :b) con il valore literal k, il risultato viene memorizzato in W. Viene influenzato lo stato del bit Z del registro STATUS.
- IORWF (Inclusive OR W with f) (5)
[label] IORWF f,d -> destinazione = W or (f)
Effettua l'or inclusivo tra il contenuto di W e il contenuto di (f), il risultato viene memorizzato in W o in (f) stesso, sempre a seconda dello stato di d. Viene influenzato il bit Z dello STATUS.
(5) NOTA: L'or è una operazione logica che viene effettuata tra due valori, "confrontando" i bit che li costituiscono seguendo la tabella di verita':
-----------
0 | 0 | 1
-----------
1 | 1 | 1
Il risultato di questa operazione soddisfa le regole della tabella vista sopra, l'or viene effettuato anche in questo caso, tra i bit dei due valori che occupano la stessa posizione, otteniamo ad esempio: 11001100 and 11110000 = 11111100 (in esadecimale: CC or F0 = FC).
- MOVF (Move f)
[label] MOVF f,d -> destinazione = (f)
Il contenuto del registro f viene spostato nella nuova destinazione seguendo la regola dello stato del bit d. L'istruzione influenza lo stato del flag Z.
- MOVLW (Move Literal to W)
[label] MOVLW k -> W = k
Il valore di tipo literal (valore a 8 bit) viene caricato nel registro W, nessun bit dello STATUS viene alterato da questa istruzione.
- MOVWF (Move W to (f))
[label] MOVWF f -> (f) = W
Il valore contenuto in W viene caricato nel file register (f). Anche questa istruzione non altera nessun bit dello STATUS.
- NOP (No Operation)
NOP -> non fa nulla
A cosa serve? Serve, serve :) viene eseguita come un ciclo a vuoto della cpu, viene usata sugli skip dopo controlli e in tutti i casi che abbiamo visto sopra.
- RETFIE (Return From Interrupt)
[label] RETFIE -> ritorna da interrupt
Ritorna da un "interrupt", il valore memorizzato in testa allo stack viene caricato nel PC e il registro GIE (Global Interrupt Enable bit: settato, abilita tutti gli interrupt) viene impostato a 1. (6)
(6) NOTA: Cosa diavolo è un interrupt? Senza entrare nei meriti dell'implementazione o troppo nei dettagli, possiamo definire l'iterrupt, come una linea preferenziale che una periferica utilizza per "attirare l'attenzione" (i più smaliziati programmatori, mi consentano l'esempio banale) del processore. Ad esempio, una linea dati in ingresso al PIC, se deve passare al processore un dato appena arrivato su di essa, effettua una chiamata alla cpu utilizzando un interrupt. Spero che questo concetto sia chiaro... Nei pic sono implementati quattro tipi di interrupt: un interrupt per una richiesta di input sulla PORT B (una delle linee dati dei pic), un interrupt per comunicare alla cpu la fine di un ciclo di scrittura eeprom, un interrupt generabile esternamente al pic (utilizzando una variazione di tensione su un particolare pin del chip) e un interrupt generato da un overflow sul registro TMR0 (un registro timer, che viene utilizzato nelle operazioni temporizzate, o nei cicli a conteggio e così via). Gli interrupt vengono gestiti (in gergo "masked") mediante opportuni controlli su particolari flags presenti in alcuni registri di queste mcu; un esempio è il flag GIE visto sopra che è contenuto nel registro INITCON (INITial CONdition: condizione iniziale) un registro che memorizza lo stato del chip subito dopo l'accensione o un reset.
- RETLW (Return With Literal)
[label] RETLW k -> return W = k
Ritorna da una subroutine, con caricato in W il valore literal "k". La procedura è sempre la solita: il PC viene caricato con il primo elemento in testa allo stack, il registro W viene caricato con k e l'esecuzione del programma riprende dall'istruzione successiva alla call che ha chiamato la sub da cui stiamo uscendo. Nessun flag dello STATUS viene modificato. L'istruzione richiede due cicli di clock per essere eseguita.
- RETURN (return :D)
[label] RETURN -> vedi sotto
Ritorna da una sub, con le stesse modalità viste nella istruzione precedente per il discorso PC e stack, ma senza alterare il registro W o alcun flag dello STATUS. L'istruzione richiede due cicli clock.
- RLF (Rotate Left f through carry)
[label] RLF f,d -> vedi sotto
Il contenuto del registro f viene ruotato a sinistra, passando per il bit dello STATUS "C" (Carry), sempre secondo la solita regola, il risultato dell'operazione viene immesso in W o in f stesso, a seconda dello stato di d. Viene modificato lo stato del flag C del registro STATUS.
Es.
___ ____________
<-| C |<-| registro f |<-,
\_______________________/
carry f register
___ _________________
| 1 | | 0 1 1 0 0 1 0 0 |
Dopo l'operazione diventa:
carry f register
___ _________________
| 0 | | 1 1 0 0 1 0 0 1 |
- RRF (Rotate Right f through carry)
[label] RRF f,d -> vedi sotto
Stesso tipo di operazione, solo che la rotazione viene fatta a destra. Chiaramente, viene modificato il flag C dello STATUS.
Es.
___ _________________
| 0 | | 1 1 1 0 0 1 0 1 |
Dopo l'operazione diventa:
carry f register
___ _________________
| 1 | | 0 1 1 0 0 1 0 0 |
- SLEEP (vai a nanna ;))
[label] SLEEP
Il processore viene messo in modalità "sleep", modo a basso consumo di corrente, con il clock fermo e con settati il bit TO (Time Out), resettato il bit PD (Power Down) e il WTD (WaTch Dog timer) viene impostato a 0x00.
- SUBLW (Subtract W from Literal)
[label] SUBLW k -> (W) = (W) - k
Al valore k, viene sottratto il contenuto del registro W. Il risultato viene memorizzato in W. I bit C, DC e Z dello status vengono modificati.
- SUBWF (Subtract W from f)
[label] SUBWF f,d -> destinazione = (f) - W
Il contenuto del registro W viene sottratto al contenuto del registro f, il risultato messo in W o f a seconda dello stato del bit b. Vengono modificati i flag C, DC e Z dello STATUS.
- SWAPF (Swap nibbles in f)
[label] SWAPF f,d -> vedi sotto
Premettiamo il concetto di nibble: un nibble equivale a mezzo byte (ovvero 4 bit degli 8 che formano un byte) in un byte ci sono due nibbles. Ciò premesso, questa operazione non fa altro che scambiare i due nibbles del registro f, memorizzando il risultato in W o in f stesso, sempre a seconda dello stato del bit d. Nessun flag dello STATUS viene modificato.
- XORLW (Exclusive Or Literal with W)
[label] XORLW k -> W = W xor k
Il valore contenuto in W viene "xorato" con il valore k, e il risultato dell'operazione memorizzato in W. Viene alterato lo stato del flag Z. (7)
(7) NOTA: Anche lo xor è una operazione logica effettuata tra due operandi, confrontando tra loro i bit che occupano la stessa posizione. Segue questa tabella della verita':
-----------
0 | 0 | 1
-----------
1 | 1 | 0
Sempre rifacendoci all'esempio fatto per le altre operazioni logiche: 11001100 and 11110000 = 00111100 (in esadecimale: CC xor F0 = 3C).
- XORWF (Exclusive Or W with f)
[label] XORWF f,d -> destinazione = W xor (f)
Il contenuto del registro W viene xorato con il contenuto del registro f, il risultato memorizzato in W o in f a seconda dello stato del bit d. Viene modificato il flag Z dello STATUS.
Note Finali
Per questa puntata è tutto... Abbiamo affrontato tutto il set delle istruzioni necessarie per programmare queste mcu. Nella prossima lezione inizieremo a vedere come si realizzano i costrutti più utilizzati nel PIC programming, e commenteremo alcune porzioni di codice. Vi premetto subito che la lezione successiva richiederà un po' più di pazienza, visto che sta per iniziare un periodo che mi vedrà piuttosto impegnato. Nel frattempo, i più volenterosi di voi, possono gia iniziare a dare qualche occhiata ai sorgenti commentati dei programmi per pic (emulatori e non) che sono facilmente reperibili in rete. Se avete domande, critiche o quand'altro, rimango a vostra completa disposizione.
Salutoni a tutti.
Disclaimer
I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.
Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevoli e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.
Questo tute è scritto per soli fini didattici, l'autore non vuole in alcun modo incoraggiare attività illegali quali hacking, cracking o phreacking. Non mi assumo inoltre nessuna responsabilità sull'uso che il lettore possa fare delle informazioni acquisite dalla lettura di questo documento. Se avete suggerimenti, critiche costruttive o domande, contattatemi. Se invece volete reclamare diritti sui marchi o prodotti citati all'interno del presente, andate pure al diavolo (io non ci guadagno una lira, andate a cercare chi ci specula sopra).
Tutti i nomi citati sono marchi registrati o copyright dei rispettivi produttori.
Categories: Electronics | PIC | Phobos | 2003