Usare un coprocessore matematico con i PIC

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à.

Figura 1. Schema a blocchi e piedinatura di uM-FPU V3.1

Figura 1. Schema a blocchi e piedinatura di uM-FPU V3.1

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.
Tabella 1. Istruzioni della uM-FPU V.3.1

Tabella 1. Istruzioni della uM-FPU V.3.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.

Figura 2. Schema elettrico di utilizzo di uM-FPU V3 con PIC

Figura 2. Schema elettrico di utilizzo di uM-FPU V3 con PIC

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
Figura 3. uM-FPU V3 IDE

Figura 3. uM-FPU V3 IDE

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.

Figura 4. Esecuzione dell’applicazione per uM-FPU con Hyperterminal

Figura 4. Esecuzione dell’applicazione per uM-FPU con Hyperterminal

 

 

Scrivi un commento

Send this to a friend