Il coprocessore matematico uM-FPU di Micromega è un microcontrollore in grado di effettuare operazioni floating point a 32 bit secondo lo standard IEEE 754, concepito per facilitare l’uso di operazioni anche sofisticate con micro dalle risorse ridotte o per i quali non siano disponibili librerie matematiche adatte.
Tra le funzioni che rende disponibili, oltre a quelle matematiche di base, vi sono quelle trascendenti ed operazioni tipiche degli algoritmi di DSP, come la MAC e la FFT. Può essere collegato a qualsiasi microcontrollore dotato di un’interfaccia SPI o I2C. Nell’articolo vedremo quali strumenti sono disponibili per la stesura del codice e come utilizzare uM-FPU con un PIC16.
CARATTERISTICHE DEL COPROCESSORE uM-FPU
Le versioni che analizzeremo di uM-FPU sono la V2 e la V3.1: queste differiscono principalmente per la potenza di calcolo ed il set di operazioni implementate. uM-FPU V2 è un micro ad 8 bit con moltiplicatore hardware ed una potenza di calcolo di 4 MIPS: gode di una certa popolarità tra gli utilizzatori di sistemi, basati su microcontrollori, di potenza medio-bassa come Basic Stamp. Appare comunque poco adeguato per essere usato insieme a micro delle famiglie PIC16 o PIC18 programmati in assembler o C. La uM-FPU V3.1, commercializzata a partire dalla fine del 2006, è basata invece su un micro a 16 bit con moltiplicatore hardware e potenza di calcolo di 30 MIPS: ha una velocità di esecuzione delle operazioni da 10 a 70 volte superiore rispetto alla V2, oltre a disporre di un set di istruzioni esteso. I mnemonici e la sintassi di molte istruzioni sono stati modificati, per cui generalmente un programma scritto per la V2 non è compatibile con la V3.1. Per avere un’idea della velocità della FPU, l’operazione di calcolo di sin(x) impiega un massimo di 100 ms per essere eseguita, mentre un PIC16 a 20 MHz con libreria assembler ottimizzata ne impiega circa 1200. Nel seguito ci occuperemo nello specifico di uMFPU V3.1. In figura 1 sono rappresentati lo schema a blocchi e la piedinatura dell’unità.
uM-FPU V3.1 dispone di 2304 Byte di memoria Flash utilizzabili per memorizzare funzioni definite dall’utente (basate sulle funzioni matematiche incorporate), tabelle di lookup e polinomi di ordine n. Altri 1024 Byte di EEPROM possono essere usati per memorizzare le funzioni definite dall’utente ed i dati a run-time. La FPU si collega al micro tramite un’interfaccia seriale sincrona SPI o I2C: l’interfaccia SPI supporta una frequenza di clock massima di 15MHz, mentre la I2C è compatibile con lo standard a 400 KHz. È presente un’interfaccia UART che può essere usata con un Debug monitor incorporato per effettuare il debug del codice della FPU. Viene anche usata per memorizzare le funzioni definite dall’utente ed i dati nella memoria Flash della FPU. uM-FPU V3.1 possiede anche un convertitore A/D da 12 bit a due canali, triggerabile da software oppure tramite un timer interno a 32 bit o un impulso esterno. Il convertitore può effettuare conversioni con un rate massimo di 10 Ksamples/s. Il clock dell’unità è generato internamente senza bisogno di componenti esterni, se è necessario avere una sorgente di clock più precisa e stabile, è possibile collegare quarzo ai pin OSC1 ed OSC2.
Il modello di programmazione della uM-FPU
uM-FPU V3.1 contiene 128 registri a 32 bit ad uso generale, più 8 registri temporanei a 32 bit non accessibili direttamente. Ogni registro può memorizzare sia float che long integer a 32 bit. I registri ad uso generale sono numerati da 0 a 127, il registro 0 viene utilizzato solita mente per memorizzare valori temporanei in quanto viene modificato automaticamente da molte istruzioni. I registri T0-T7 sono usati internamente dalla FPU per memorizzare valori temporanei nelle operazioni che fanno uso di parentesi. Normalmente il processo di calcolo consiste nell’inviare istruzioni e dati dal microcontrollore alla FPU, quindi attendere che la FPU termini le operazioni di calcolo e leggere il risultato: poichè l’instruction buffer della FPU ha una dimensione di 256 Byte, quest’ultima può ricevere più istruzioni ed eseguire i calcoli mentre magari il microcontrollore è impiegato in altri compiti. Nelle istruzioni che svolgono un’operazione matematica, vengono coinvolti generalmente due registri: un registro, indicato genericamente dalla lettera A, funge da sorgente di un operando e da accumulatore per il risultato dell’operazione, mentre, nelle operazioni che coinvolgono due operandi, il secondo è prelevato da un registro indicato con la lettera B. Spesso il registro B corrisponde al registro 0. Il registro A viene selezionato tramite l’istruzione SELECTA,nn dove nn indica un numero compreso tra 0 e 127. Il registro B viene indicato aggiungendolo al codice dell’operazione da eseguire, ad esempio:
FDIV,2
divide il valore contenuto in A per il valore nel registro B (il registro 2), quindi pone il risultato in A. Un numero floating point o long integer viene caricato in un registro del micro con istruzioni tipo
<istruzione>[,nn],b1,
b2,b3,b4
dove nn è il registro in cui caricare il valore e b1..b4 sono i 4 Byte della codifica IEEE754 del numero (MSB first). La versione 3.1 di uM-FPU con sente anche di utilizzare un registro X con autoincremento tramite l’istruzione
SELECTX,nn
così tutte le istruzioni successive che operano su un registro X incrementano di uno l’indirizzo del registro, ad esempio:
SELECTX,3
seleziona il registro 3 come registro X
FWRITEX,3F,80,00,00
carica il numero F.P. 1.0 nel registro 3, incrementa X
FWRITEX,41,20,00,00
carica il numero F.P. 10.0 nel registro 4, incrementa X e così via.
Il set di istruzioni della uM-FPU V3.1
Le istruzioni della uM-FPU sono circa 200 e risultano suddivise in quattro grandi categorie:
- Floating Point, operano su numeri floating point IEEE754
- Long Integer, operano su interi con o senza segno a 32 bit
- General Purpose
- Special Purpose
Vediamo più in dettaglio le istruzioni principali delle categorie suddette.
Istruzioni Floating Point
Sono divise a loro volta nelle seguenti sottocategorie:
- Istruzioni di base: operazioni elementari sui numeri in virgola mobile (addizione, moltiplicazione, divisione, comparazione, ecc.). Per ognuna di queste istruzioni esistono tre varianti a seconda della provenienza del secondo operando. Ad esempio l’istruzione di addizione FADD ha le seguenti varianti:
FADD,nn
carica il 2° operando dal registro B=nn;
FADD0
carica il 2° operando dal registro B=0;
FADDI,bb
carica un valore immediato di 1 Byte (signed byte) tra
-128 e 127. - Istruzioni trascendenti ed addizionali: permettono di effettuare le operazioni di sin, cos, tan , ln, log10 e inverse. Altre istruzioni di conversione, arrotondamento, comparazione. Istruzioni MAC (Multiply and ACCumulate) ed MSC (Multiply and Subtract): queste richiedono tre operandi contenuti nei registri A, nn ed mm, ad esempio FMAC,nn,mm esegue [A]=[A]+([nn]*[mm]). Istruzioni di caricamento di numeri floating point: caricano un numero f.p. dal micro in un registro della uM-FPU. L’istruzione ATOF permette di caricare e convertire in f.p. un numero reale espresso come stringa ASCII. Istruzioni di lettura di numeri floating point: leggono un numero f.p. dalla uM-FPU. L’istruzione FTOA permette di leggere un numero f.p. come stringa ASCII formattata.
- Istruzioni che operano su matrici: oltre ad istruzioni di manipolazione degli elementi di una matrice, l’istruzione MOP permette di effettuare numerose operazioni su matrici tra cui addizione, divisione, moltiplicazione e divisione sia scalare che per elementi, prodotto tra matrici, calcolo di deteminante e inversa (solo matrici 2x2 o 3x3).
- FFT (Fast Fourier Transform): opera come istruzione singola per blocchi di max 64 data point o in più passi per blocchi di dati più grandi.
- Istruzioni di conversione: da floating point a long integer e viceversa.
Istruzioni Long Integer
Sono distinte nelle seguenti sottocategorie:
- Istruzioni di base: operazioni elementari sui numeri long integer (addizione, moltiplicazione, divisione, comparazione, ecc.). Per ognuna di queste istruzioni esistono tre varianti a seconda della provenienza del secondo operando, esattamente come visto per i numeri floating point.
- Istruzioni addizionali: AND, OR, XOR, incremento, decremento ed altre.
- Istruzioni di caricamento di numeri long integer dal micro verso la uM-FPU, analoghe a quelle viste per i numeri floating point.
- Istruzioni di lettura di numeri long integer dalla uM-FPU, analoghe a quelle viste per i numeri floating point.
Istruzioni General Purpose
Sono istruzioni di vario tipo, ad esempio usate per il controllo della FPU, come l’istruzione RESET, o per cancellare o copiare il contenuto dei registri. Un’istruzione di particolare importanza è la READSTR, usata per ritornare una stringa risultante dalla conversione in ASCII di un numero floating point o long integer. Ancora, le istruzioni LEFT e RIGHT per mettono di usare le parentesi nelle espressioni matematiche: è permesso un massimo di 8 livelli di annidamento.
Istruzioni Special Purpose
Sono distinte tra:
- Manipolazione di stringhe: ricerca di campi e sottostringhe, conversione di sottostringhe in floating point/long integer e viceversa, ed altre. Queste istruzioni sono utilizzabili per effettuare il parsing delle stringhe NMEA (dette sentences) dei GPS.
- Stored functions: le istruzioni FCALL ed EECALL servono per richiamare le funzioni user-defined memorizzate nella Flash o nella EEPROM. Altre istruzioni permettono di effettuare le operazioni di table lookup e di reverse table lookup. Ancora, l’istruzione NPOLY calcola il polinomio di ordine n per il valore del registro A. Tutte queste istruzioni sono valide solo se usate all’interno di una funzione user-defined.
- EEPROM: L’istruzione EEWRITE viene usata per memorizzare una funzione user-defined a run time, mentre altre istruzioni servono per scrivere/leggere valori in EEPROM.
- Debugging: come già accennato, l’interfaccia UART può essere usata per interfacciarsi con un programma monitor debug presente nella FPU per poter effettuare il debug ed il test del codice. L’istruzione BREAK, in particolare, permette di arrestare l’esecuzione delle operazioni ed entrare in modalità monitor. Le istruzioni TRACE gestiscono il trace delle istruzioni eseguite dalla FPU. Se la modalità di debugging non è attiva (si veda il paragrafo dedicato ai collegamenti della FPU) queste istruzioni vengono ignorate.
- Serial I/O: se la modalità di debugging non è attiva, l’interfaccia UART può essere usata tramite queste istruzioni per I/O verso dispositivi esterni, ad esempio un ricevitore GPS.
- A/D conversion: gestiscono il convertitore A/D incorporato.
- Timer: settano ed utilizzano il timer a 32 bit interno.
- External input: utilizzano il contatore di impulsi esterni. Nella tabella 1 sono riassunte leistruzioni della uM-FPU V3.1.
Usare la uM-FPU con un PIC
La uM-FPU può essere connessa al microcontrollore tramite un’interfaccia I2C (Inter-Integrated Circuit) o SPI (Serial Peripheral Interconnect) a 2 o 3 fili. Nel seguito ci riferiremo all’interfaccia SPI a 3 fili in quanto semplice da utilizzare e molto veloce. La velocità di clock massima è di 15 MHz (5 MHz per flussi dati continuativi). L’interfaccia SPI permette opzionalmente di utilizzare più uM-FPU se viene abilitata la modalità bus, programmando gli interface mode parameter nella Flash della FPU tramite il debug monitor.
L’SPI della FPU è configurato in modalità slave
(SPI Mode 0): i pin dell’interfaccia sono i seguenti:
- SCLK: Clock, si collega al pin SCLK dell’unità master, nel nostro caso il microcontrollore collegato alla FPU. Notiamo che il segnale di clock, generato dal master, è presente solo quando è necessario un trasferimento di dati tra i dispositivi.
- SIN: ingresso dati, va collegato al pin SOUT del micro.
- SOUT: uscita dati, si collega al pin SIN del micro.
Nello schema in figura 2 vediamo un possibile utilizzo di uM-FPU V3 con un PIC16F876. Il MAX232 può essere usato opzionalmente per collegare la seriale del PIC ad un porta RS232 del PC.
Prima di iniziare l’esecuzione di un programma è necessario resettare l’FPU inviando una sequenza di 10 byte 0xFF mentre SIN è tenuto al livello alto, e porre successivamente SIN basso per almeno 10 ms. Per selezionare l’interfaccia SPI è necessario porre a livello basso il pin CS durante la fase di reset e mantenerlo basso per tutta l’esecuzione delle operazioni. La modalità di debugging viene attivata se il pin SERIN è al livello alto durante la fase di reset. Le istruzioni che inviano dati alla FPU consistono in un byte di opcode seguito da 0 o più byte di dati: il microcontrollore emette il segnale di clock per trasmettere un singolo byte alla volta, mentre è necessario rispettare un ritardo minimo tra l’inizio di un byte ed il successivo (minimum data period) di 1.6ms. Per le istruzioni che ricevono dati in uscita dalla FPU invece, è necessario attendere che SOUT, che funge ora da segnale di Busy/Ready, diventi alto segnalando così che l’instruction buffer è vuoto. Successivamente il micro invia il byte di opcode, quindi attende un tempo minimo pari a 15ms (read setup delay) prima di poter emettere il segnale di clock che permetterà di ricevere il primo byte in uscita dalla FPU. I byte successivi invece saranno distanziati fra loro di un tempo minimo pari a 1ms (read byte delay).
Librerie di utilità per PIC
Micromega mette a disposizione degli utilizzatori delle librerie assembler che semplificano l’uso di uM-FPU V2 con i microcontrollori PIC tramite l’interfaccia SPI. Sono disponibili versioni specifiche per alcuni PIC16 con interfaccia SPI hardware o software (bit-banged). Al momento della stesura di questo articolo, Micromega non ha rilasciato una versione specifica per la uM-FPU V3.1, ho quindi provveduto a modificare le librerie esistenti per adattarle. Le funzioni fornite dalle librerie si suddividono in due gruppi:
- funzioni per la comunicazione con la FPU tramite l’interfaccia SPI;
- funzioni per stampare i dati emessi dalla FPU tramite la UART del micro.
Le funzioni per la comunicazione SPI sono le seguenti:
- fpu_reset: effettua il reset della FPU all’inizio del programma;
- fpu_wait: attende che la FPU segnali lo stato di Ready;
- fpu_write: invia il valore del registro W del PIC come un byte di opcode o dato alla FPU, tenendo conto del minimum data period;
- fpu_read: riceve in W un byte di dati in uscita dalla FPU, tenendo conto del read byte delay;
- fpu_readDelay: attende il read setup delay tra l’invio dell’opcode ed il primo byte da ricevere dalla FPU.
Le funzioni di comunicazione tramite UART che useremo nel programma d’esempio sono:
- print_setup: inizializza l’UART del PIC
- print_floatFormat: invia il valore del registro A della FPU come stringa formattata. Accetta il registro W del PIC come parametro per indicare il numero totale di caratteri da usare (le decine) ed il numero di decimali (le unità).Ad esempioA=123.456 se W=73, viene stampato come123.456A=123.456 se W=62, viene stampato come123.46A=123.456 se W=42, viene stampato come*.**
- print_hex: invia il valore contenuto nel registro W come due cifre hex, può essere usata per visualizzare il risultato floating point in formato hex
- print_string: invia una stringa letta tramite una table read della Flash del PIC. W deve contenere la parte bassa dell’indirizzo della stringa.
Vedremo un’applicazione di queste funzioni nel programma d’esempio, nel paragrafo dedicato all’IDE di uM-FPU.
uM-FPU V3 IDE
uM-FPU V3 IDE è un’applicazione Windows fornita da Micromega allo scopo di facilitare ulteriormente la scrittura di codice per uM-FPU V3.1. Inoltre l’IDE può essere utilizzata per effettuare il debug del codice con l’ausilio del debug monitor, collegandosi alla FPU tramite l’interfaccia seriale. Ancora, tramite l’IDE è possibile memorizzare nella Flash della FPU le funzioni user-defined. In figura 3 possiamo vedere l’IDE con il codice dell’esempio. Le operazioni che la FPU deve svolgere vengono descritte mediante un file di testo contenente le espressioni in forma matematica oltre alle definizioni delle variabili del micro e dei registri della FPU utilizzati. I registri della FPU 0..127 vengono indicati come F0..F127, mentre per le variabili del micro sono definite i tipi seguenti:
- BYTE: 8-bit signed integer
- UBYTE: 8-bit unsigned integer
- WORD: 16-bit signed integer
- UWORD: 16-bit unsigned integer
- LONG: 32-bit signed integer
- ULONG: 32-bit unsigned integer
- FLOAT: 32-bit floating point
per associare una variabile ad un registro della FPU scriveremo
<nome variabile> EQU Fnn
mentre per le variabili del PIC si usa la forma seguente
<nome variabile> VAR <tipo>
Vediamo con un esempio come generare il codice assembler per un PIC. Vogliamo calcolare il valore del seno per 10 valori dell’angolo (in radianti) tra 0 e 2π. Inseriamo il codice seguente nell’area di editing presente in corrispondenza della scheda untitled dell’IDE
; variabili lette dal PIC Step VAR BYTE
; variabili ritornate al PIC Result VAR FLOAT
; registri FPU TotalStep EQU F1
PI2 EQU F2
; espressioni matematiche
TotalStep = 10
PI2 = 2 * PI
Result = SIN((PI2 * Step) / TotalStep)
vediamo che i registri della FPU vengono usati per
memorizzare le costanti TotalStep e PI2. La variabile Step viene fornita dal PIC come intero a 8 bit, mentre il risultato dell’operazione viene restituito al PIC sotto forma di numero floating point, ad esempio per poterlo memorizzare in una tabella. Selezioniamo PICmicro dalla Listbox e premiamo Compile per generare il codice per il PIC: nella scheda Output possiamo vedere il codice generato. Vediamo come vengono utilizzate alcune delle funzioni di libreria per i PIC di cui abbiamo parlato in precedenza. Le istruzioni che inviano dati alla FPU vengono trasmesse un byte alla volta utilizzando la funzione fpu_write, ad esempio la porzione seguente del codice:
movlw SELECTA ; SELECTA, TotalStep call fpu_write movlw TotalStep call fpu_write movlw FSETI ; FSETI, .10 call fpu_write movlw .10 call fpu_write
seleziona il registro F1 come registro A, quindi pone il valore del registro pari al valore immediato 10 e lo converte in floating point. Le istruzioni che ricevono dati dalla FPU usano codice del tipo seguente:
call fpu_wait ;attesa stato Ready movlw FREAD0 ;FREAD0, Result call fpu_write call fpu_readDelay call fpu_read movwf Result call fpu_read movwf Result+1 call fpu_read movwf Result+2 call fpu_read movwf Result+3
notiamo la chiamata di fpu_wait prima di inviare l’opcode della FREAD0, per attendere che la FPU sia Ready. A causa di un bug nella versione V3 dell’IDE questa chiamata non viene inserita ed è quindi necessario aggiungerla manualmente. La chiamata a fpu_wait viene invece inserita automaticamente dall’IDE ogni 256 B di dati inviati alla FPU, per evitare l’overflow dell’instruction buffer. Il risultato viene ritornato nella variabile Result del PIC un byte alla volta (MSB first).
L’esempio d’uso completo
Per completare il codice dell’applicazione PIC non ci resta che aggiungere una chiamata alla funzione fpu_reset per resettare la FPU, nonché inserire il codice che genera i valori di Angle da 0 a 10. Possiamo sfruttare il collegamento seriale del PIC per poter leggere tramite Hyperterminal i valori del seno. Creiamo una nuova connessione settando Hyperterminal a 19200 baud, 8 bit dati, 1 bit di stop, nessun controllo di flusso. Per leggere il valore floating point nella codifica hex IEEE754 usiamo il codice seguente:
movfw Result call print_hex movfw Result +1 call print_hex movfw Result +2 call print_hex movfw Result +3 call print_hex
mentre per visualizzarlo come stringa formattata useremo la seguente porzione di codice:
movlw .96 ; in W il formato da usare call print_floatFormat
il formato 9.6 indica di utilizzare 9 caratteri in totale e 6 cifre decimali; il valore stampato è quello contenuto nel registro A, nel nostro caso il registro 0 che contiene la variabile Result. In figura 4 possiamo vedere la schermata di Hyperterminal.