• Non ci sono risultati.

Realizzazione del firmware

N/A
N/A
Protected

Academic year: 2021

Condividi "Realizzazione del firmware"

Copied!
25
0
0

Testo completo

(1)

________________________________Capitolo 5

Realizzazione del firmware

5.1 Introduzione

Questo capitolo prende in esame il firmware che è stato realizzato per fare svolgere al microcontrollore tutte le operazioni necessarie al funzionamento dell’intero sistema.

Per la realizzazione del firmware sono stati utilizzati due software completamente freeware: AVR Studio e WinAVR.

AVRStudio è un potente programma che viene messo a disposizione gratuitamente da Atmel per la programmazione e il debugging di applicazioni rivolte a microcontrollori Atmel AVR 8 bit in ambiente Microsoft Windows®. Esso supporta le diverse fasi a cui si và incontro quando viene creato un prodotto basato su un microcontrollore Atmel AVR; in particolare con esso è possibile: editare il codice sorgente e compilarlo in linguaggio assembler (o di terze parti), simulare il dispositivo utilizzato ed eseguire il debug dell’applicazione realizzata, programmare il microcontrollore con il firmware creato, e infine emulare il dispositivo grazie all’interfaccia JTAG. Inoltre supporta varie schede di sviluppo, tra cui STK500, AVRISP, JTAGICE e per finire può interagire (mediante l’utilizzo di plug-ins) con software di terze parti. Nel nostro caso in particolare, è stato utilizzato il plug-in relativo al compilatore per il linguaggio C incluso nel pacchetto WinAVR.

WinAVR costituisce un insieme di programmi open source, volti alla realizzazione di applicazioni per microcontrollori Atmel AVR. Tra i vari programmi di cui è costituito, è presente il compilatore per linguaggio C/C++ GCC (Gnu Compiler Collection), pienamente supportato da AVR Studio, e la libreria avr-libc, ricca di funzioni di vario

(2)

genere molto utili alla realizzazione di codice sorgente e dedicate esclusivamente ai microcontrollori Atmel AVR.

L’intero firmware realizzato per il prototipo è stato scritto utilizzando il linguaggio C ed il compilatore GCC, e inoltre è stato fatto largo uso della libreria avr-libc.

5.2 Organizzazione del firmware

Il firmware del microcontrollore è stato organizzato secondo una struttura modulare, al fine di poter essere letto con maggiore chiarezza e di poter essere eventualmente modificato senza dover necessariamente agire sull’intero codice. Per questo è stato suddiviso in tre file: “main.c”, “display.h” e “functions.c”.

Il primo file è quello principale che contiene la funzione main, e la dichiarazione delle variabili globali del sistema.

Il file “display.h” è un header file, ovvero contiene tutte le direttive “define” per il preprocessore, la dichiarazione delle variabili globali esterne contenute in “main.c”, le definizioni di tipi struttura e la dichiarazione di tutte le funzioni utilizzate in “main.c”.

Per quanto riguarda invece il file “function.h”, in esso sono presenti tutte le definizioni delle funzioni che vengono utilizzate nel file “main.c”.

5.3 Principio di funzionamento

Il funzionamento generale del firmware è simile a quello relativo all’emulatore con LabVIEW™. Infatti anche nel caso del microcontrollore devono essere svolte le stesse operazioni, cioè:

• Aggiornamento del display led.

• Salvataggio permanente di fonts e bitmaps.

• Ricezione di dati dalla porta RS-232 e loro salvataggio in un buffer. • Esecuzione dei vari comandi ricevuti attraverso la porta seriale. • Rimozione dei dati utilizzati dal buffer.

(3)

Tuttavia queste operazioni vengono effettuate in un ambiente totalmente diverso rispetto a quello di LabVIEW™ quindi si è reso necessario l’utilizzo di un approccio differente.

Vediamo di descrivere in linea di principio il funzionamento del firmware.

Dopo il reset del dispositivo viene effettuata un’inizializzazione del sistema, che consiste nel settaggio di alcune variabili globali, nell’abilitazione e configurazione di tutte le periferiche del microcontrollore utilizzate (UART, SPI, TIMER, etc.), e nel settaggio opportuno delle linee di I/O del microcontrollore. Questa fase viene effettuata attraverso la chiamata di funzioni predisposte a tale scopo.

Dopodiché viene attivato il bit I del registro SREG, ovvero viene abilitato l’uso globale delle richieste di interruzione. Le richieste di interruzione sono risultate molto utili nello sviluppo di questo firmware. In particolare ne sono state utilizzate due: la prima è relativa ad un timer del microcontrollore, ed è stata sfruttata allo scopo di eseguire il refresh del display led ad una determinata frequenza, mentre l’altra si riferisce all’uso della UART, per la ricezione di dati attraverso l’interfaccia seriale.

Quando perviene un dato alla porta RS-232, la routine di servizio si occupa di salvare questo dato all’interno di un buffer circolare per una sua successiva elaborazione. Il programma principale, quando non viene interrotto dalle routine di interruzione, si occupa di verificare che vi siano dati da elaborare nel buffer circolare. Quando ciò accade vengono interpretati tali dati ed eseguite le operazioni necessarie. Dopo lo svolgimento di queste operazioni, vengono aggiornate le condizione del buffer circolare e si torna di nuovo ad attendere l’arrivo di nuovi dati.

Nel corso dei paragrafi successivi verrà illustrato con maggior dettaglio il funzionamento del sistema, concentrandosi sulle parti più importanti.

(4)

5.3.1 Operazione di refresh del display

Il rinfresco del display led è un’operazione fondamentale per il funzionamento dell’intero sistema. Per effettuare il refresh del display è stato sfruttato uno dei timer presenti all’interno dell’ATmega128, in particolare il Timer/Counter1. Esso è un timer/counter a 16 bit che può ricevere due sorgenti di clock: una interna, e una esterna (proveniente da uno dei pin preposti a questo scopo). La sorgente di clock dipende dallo stato dei bit CS12:0, presenti nel registro TCCR1B (Timer Counter Control Register B): per mezzo di essi è infatti possibile disattivare il timer o stabilire se la sorgente di clock è interna (con prescaler o senza) o esterna (e in tal caso è possibile considerare attivo il fronte in salita o quello in discesa del segnale in ingresso). Il valore del Timer/Counter1 (contenuto nel registro TCNT1) viene comparato in ogni momento con il contenuto dei 3 registri OCR1A/B/C: quando viene rilevato un compare match con uno di questi registri (cioè se accade che TCNT1 e, ad esempio, OCR1A contengono lo stesso valore) è possibile poter generare una richiesta di interruzione. Inoltre è anche possibile fare in modo che ogniqualvolta venga raggiunta la condizione di compare match venga azzerato il valore di TCNT1 (ovvero viene eseguito il CTC, Clear Timer on compare match). Questo significa che, impostando come sorgente di clock quella proveniente dall’interno (eventualmente con l’uso del prescaler) e caricando in uno dei registri OCR1x un valore opportuno, è possibile generare una richiesta di interruzione con una cadenza prefissata.

Questo è quello che viene fatto in pratica nel firmware del pannello a led; in fase di inizializzazione del timer (che avviene grazie alla funzione timer1_init), si abilita quest’ultimo a generare una richiesta di interruzione in seguito ad un compare match rilevato nel registro OCR1A e si attiva la modalità CTC. Viene impostato il prescaler ad un valore fissato e caricato in OCR1A un valore opportuno. La frequenza con la quale viene generata la richiesta di interruzione è determinabile con la seguente formula:

)

1

1

(

*

int

+

=

A

OCR

presc

f

f

clk

(5)

dove fint è la frequenza con cui viene eseguita la routine di servizio, presc è la

divisione del prescaler (1, 8, 64, 256, 1024), fclk è la frequenza di clock del sistema e

OCR1A è il valore da caricare nell’omonimo registro.

Nel firmware realizzato è stato scelto di effettuare il refresh dell’intero display ad una frequenza di circa 55 Hz. Questo implica di dover commutare le righe ad una frequenza 28 volte superiore (ove 28 è il numero di righe del display), cioè a circa 1540 Hz. Utilizzando un fattore di prescaler pari a 256, e una frequenza di clock di sistema pari a 16 MHz, il valore da caricare in OCR1A per ottenere una frequenza intorno a 1540 Hz è uguale a 39. Con questo valore si ottiene una frequenza di interruzione pari a 1562.5 Hz, quindi l’intero rinfresco viene eseguito ad una frequenza pari a 55.80 Hz. Variando semplicemente il valore da caricare in OCR1A e/o cambiando il fattore di prescaler, è possibile variare la frequenza di aggiornamento.

Ogni volta che viene eseguita la routine per il refresh del display, è necessario reperire in qualche modo lo stato che dovranno assumere i pixel della riga correntemente selezionata. A tale scopo è stato previsto l’utilizzo dell’array SRAM_DATA[]: esso è un array di 448 elementi di tipo unsigned int che ha la funzione di memoria video. L’array SRAM_DATA[] viene allocato in una porzione di memoria SRAM.

Andiamo ad analizzare in dettaglio il comportamento della routine di interruzione. In figura 1 ne viene riportato il diagramma a blocchi.

Per tenere conto della riga del display che deve essere accesa, è stata adottata la variabile globale row; questa variabile viene incrementata prima del termine della routine stessa e consente di stabilire la porzione dell’array SRAM_DATA[] che contiene i dati da leggere.

La prima operazione che viene effettuata dalla routine di interruzione è la chiamata alla funzione spi_transmit_to_display: ad essa vengono passati come parametri il numero di riga corrente (row) ed il numero di bytes (costante e pari a 16) da leggere nella memoria video; questa funzione calcola l’indice dell’elemento della variabile SRAM_DATA[] da cui far partire la lettura dei dati della memoria video ed invia i dati letti ai drivers di colonna per mezzo dell’interfaccia SPI.

(6)

Figura 1 – Diagramma a blocchi relativo alla routine di servizio per il rinfresco del display.

Successivamente vengono disattivate le uscite dei drivers di riga e di colonna e si fornisce un impulso di clock ai segnali latch enable dei drivers per salvare i nuovi dati.

Vengono a questo punto riattivate le uscite dei drivers: la nuova riga si porta ad un potenziale prossimo a 5 V e vengono accesi i led della riga stessa sulla base dei dati caricati nei drivers di colonna.

L’ultima operazione che viene svolta è l’aggiornamento della variabile row e la preparazione dei driver alla riga successiva. Ricordando che gli shift register dei due STP16C595 di riga sono connessi ad anello, per passare da una riga alla successiva è sufficiente fornire un impulso di clock al pin SDI di uno dei due driver. Quando tuttavia si raggiunge l’ultima riga del display (cioè row = 27) è necessario fornire 5 cicli di

Inizio

Spegnimento della riga corrente e delle uscite

dei drivers di colonna

Latch dei vari drivers per il salvataggio della nuova

riga e dei dati di colonna

Attivazione delle uscite dei vari drivers

Uscita dalla routine di

servizio

Lettura da SRAM_DATA[] ed invio dati letti ai drivers

di colonna

Aggiornamento della variabile row per la riga

(7)

clock per riposizionare l’unico livello logico 1 presente nell’anello in corrispondenza della prima riga; questo è dovuto al fatto che lo shift register è costituito da 32 flip flop, mentre le righe sono solo 28.

Nel prossimo paragrafo viene descritta la routine di interruzione per la ricezione di dati dalla porta RS-232.

5.3.2 Ricezione dati attraverso la USART

L’Atmega128 dispone al suo interno di due USART (Universal Synchronous and Asynchronous serial Receiver and Transmitter), ognuna delle quali è indipendente grazie all’adozione di registri differenti. Le caratteristiche principali delle due USART sono le seguenti:

• Permette di operare in full duplex in quanto ha registri di ricezione e trasmissione indipendenti.

• Può utilizzare una modalità sincrona o asincrona.

• Supporta diversi formati di trame di dati; in particolare è possibile configurare il numero di bit di dati ad un valore di 5, 6, 7, 8, ed anche 9 bit. Inoltre è possibile impostare 1 o 2 bit di stop.

• Sono disponibili 3 tipi di eventi capaci di generare una richiesta di interruzione: a trasmissione completata (Tx Complete), a ricezione completata (Rx Complete) e a buffer di trasmissione vuoto (Tx Data Register Empty).

In figura 2 si riporta il diagramma a blocchi, tratto dal datasheet dell’Atmega128, di una delle due USART presenti al suo interno.

Come è possibile notare dalla figura, si possono distinguere tre blocchi principali separati: il generatore di clock (Baud generator), il trasmettitore (Transmitter) ed il ricevitore (Receiver); inoltre sono presenti dei registri per il controllo dell’unità.

(8)

Il generatore di clock può utilizzare una sorgente di clock interna o esterna: nel nostro progetto è stata utilizzata la USART in modalità asincrona, quindi il clock viene generato internamente per mezzo del Baud Rate Generator. In modalità sincrona è invece possibile utilizzare una sorgente esterna connessa al pin XCK del microcontrollore.

Figura 2 – Diagramma a blocchi di una delle due USART presenti nell’ ATmega128 (tratta dal datasheet dello stesso).

Attraverso il registro UBRR è possibile stabilire il baud rate utilizzato dalla USART per la trasmissione e la ricezione. Nel firmware realizzato è stata utilizzata la USART in

(9)

modalità asincrona in “Normal mode”. In questo modo di funzionamento è possibile determinare il BAUD rate secondo la relazione riportata di seguito:

È comunque possibile, nel caso di funzionamento asincrono, agire sul bit U2X del registro UCSRA per ridurre il divisore di frequenza da 16 a 8, raddoppiando quindi la velocità di trasferimento: questo tuttavia comporta una riduzione dei campioni utilizzati in fase di ricezione da 16 a 8 e di conseguenza un maggior rischio di errore.

Per impostare il formato della trama, si può agire sui bit UCSZ2:0, UPM1:0 e USBS: i primi stabiliscono il numero di bit di dati (5, 6, 7, 8, 9), i bit UPM1:0 definiscono la parità (nessuna, pari o dispari), mentre il bit USBS consente di impostare il numero di bit di stop (1 o 2).

Dopo il reset del sistema, ad occuparsi di inizializzare la USART1 è la funzione uart_init: prima di tutto imposta il formato della trama (8 bit di dati, 1 bit di stop, nessuna parità), dopodiché carica nel registro UBRR1 il valore 51 (che corrisponde, con una frequenza di clock di sistema pari a 16 MHz, ad impostare il baud rate ad un valore pari a 19200 bps), ed infine abilita il trasmettitore, il ricevitore e la modalità di interruzione “Rx Complete Interrupt”.

Questa modalità di interruzione consente, ogni volta che è stata completata la ricezione di un dato, di eseguire una routine di servizio. Questa proprietà viene sfruttata nel prototipo per non dover impegnare inutilmente le risorse del microcontrollore per verificare periodicamente se sono pervenuti dati nel buffer di ingresso della USART.

Vediamo in dettaglio quali sono le operazioni che vengono svolte dalla routine di servizio per la ricezione dei dati dalla USART.

)

1

(

16

+

=

UBRR

f

BAUD

OSC

(10)

Figura 3 – Diagramma a blocchi del funzionamento della routine di interruzione per la ricezione di dati dalla USART dell’ATmega128.

I dati che pervengono dalla USART, vengono inseriti all’interno dell’array UART_BUFFER[] utilizzato come buffer circolare. Per mantenere memoria della posizione corrente di inserimento nel buffer è stata prevista la variabile unsigned int write_pos, mentre la variabile free_bytes specifica il numero di posizioni ancora

Inizio

UART_BUFFER[write pos] = UDR1 free_bytes--free_bytes > 0 write_pos = BUFFER_DIM - 1 ? write_pos = 0 write_pos++ FLOW_CONTROL = ON?

num bytes buffer <= EMPTY_BYTES ? uart_transmit(0xFF) Fine SI SI NO SI NO NO SI

num bytes buffer >= FULL_BYTES ? uart_transmit(0xFE) SI NO NO

(11)

disponibili all’interno del buffer. In figura 3 viene fornito il diagramma a blocchi relativo alla routine di interruzione per la ricezione dei dati dalla USART.

Quando viene ricevuto un dato dalla USART, si valuta inizialmente se c’è abbastanza spazio nel buffer UART_BUFFER[] per ricevere tale dato. Se lo spazio è sufficiente si preleva dal registro UDR1 il dato ricevuto e si decrementa il numero dei bytes disponibili nel buffer; altrimenti viene valutato lo stato della variabile globale FLOW_CONTROL.

Se vi è spazio sufficiente si procede ad aggiornare l’indice per la posizione corrente write_pos e si valuta lo stato della variabile globale FLOW_CONTROL. Se questo risulta attivo si confronta il numero di dati presenti nel buffer con le variabili globali EMPTY_BYTES e FULL_BYTES: esse contengono rispettivamente i livelli di soglia per i messaggi “Almost Empty” e “Almost Full”. Se il numero di bytes presente nel buffer è minore o uguale alla variabile EMPTY_BYTES, viene trasmesso al controller il carattere 0xFF (Buffer Almost Empty), mentre se è maggiore o uguale a FULL_BYTES si trasmette il carattere 0xFE (Buffer Almost Full). A questo punto la routine giunge al termine.

5.3.3 Utilizzo dell’interfaccia SPI

L’ATmega128 dispone di un’interfaccia SPI (Serial Peripheral Interface) che permette di trasferire dati ad elevata velocità in modo sincrono, ovvero attraverso l’utilizzo di un segnale di clock.

Le caratteristiche principali di questa interfaccia sono riportate di seguito: • Trasferimento dati seriale a 3 fili.

• Modalità master o slave.

• Possibilità di trasmettere per primo il bit più significativo o quello meno significativo.

• 7 bit rates impostabili attraverso appositi registri.

• Double speed Master SPI mode (abilitando questa modalità si può trasferire dati alla frequenza di clock massima di 8 MHz).

(12)

Figura 4 – Struttura a blocchi dell’interfaccia SPI dell’ATmega128 (tratta dal relativo datasheet).

In figura 4 viene proposto il diagramma a blocchi relativo all’interfaccia SPI tratto dal datasheet dell’ATmega128.

Nel prototipo realizzato l’unico dispositivo SPI capace di generare il clock è solamente il microcontrollore: esso viene quindi configurato come master; tutti gli altri dispositivi che utilizzano tale interfaccia (STP16C596 e memoria eeprom) sono slave.

Descriviamo brevemente come viene realizzato il trasferimento di dati tra un dispositivo master e uno slave utilizzando l’interfaccia SPI.

Le linee che vengono utilizzate per un trasferimento sono tipicamente 4: MOSI (Master Out Slave In), MISO (Master In Slave Out), CLK (CLocK) e /SS (Slave Select).

Il dispositivo master inizia la comunicazione con lo slave desiderato portando a livello logico basso la linea /SS ad esso relativa. Master e slave preparano i dati da trasferire

(13)

nei loro rispettivi shift registers; il master genera il segnale di clock e lo trasmette attraverso la linea CLK. I dati sono sempre trasferiti dal master allo slave nella linea MOSI e dallo slave al master attraverso la linea MISO. Terminato il trasferimento, il dispositivo master riporta a livello alto la linea /SS. Quando l’ATmega128 è configurato come master, esso non dispone del controllo automatico della linea /SS: questo deve essere eseguito da parte dell’utente via software. Il microcontrollore inizia a generare il clock non appena viene caricato un byte nel registro SPDR (SPi Data Register) e da quel momento in poi vengono trasmessi serialmente gli 8 bit di dati. Contemporaneamente alla trasmissione verso lo slave, il master è in grado di ricevere un byte da quest’ultimo. Al termine del trasferimento del dato viene settato il flag SPIF (SPi Interrupt Flag): questa caratteristica viene utilizzata per rilevare il completamento del dato trasferito.

Nel firmware del prototipo l’inizializzazione dell’interfaccia SPI viene svolta dalla funzione spi_init: viene innanzitutto stabilita la direzione delle linee di I/O che saranno utilizzate per la comunicazione (ovvero vengono impostate come uscite SCK, MOSI e /SS, mentre come ingresso la linea MISO), successivamente viene abilitato il bit SPI2X (Double SPI speed bit) in modo da settare una frequenza del clock SPI pari a fOSC/2

(ovvero pari a 8 MHz), ed infine si abilita l’utilizzo dell’interfaccia SPI in modalità master (con la trasmissione del bit meno significativo per primo).

La funzione base che viene utilizzata per il trasferimento di un byte è spi_transfer: essa ha per argomento il byte da trasmettere e restituisce al termine della sua esecuzione l’eventuale byte ricevuto dal dispositivo slave (l’unico dispositivo slave in grado di poter trasmettere un dato al microcontrollore è la memoria eeprom). Le operazioni che vengono svolte da tale funzione sono le seguenti:

• Vengono disattivate le richieste di interruzione: questo viene fatto al fine di evitare che possa essere eseguita la routine di interruzione per il refresh del display mentre è in corso un trasferimento da parte di spi_transfer; in tal modo viene evitato un possibile conflitto nell’utilizza della risorsa.

• Si inserisce il dato da trasmettere nel registro SPDR.

(14)

Figura 5 - Diagramma a blocchi della funzione spi_transfer.

• Vengono riattivate le richieste di interruzione.

• Si restituisce l’eventuale dato ricevuto dal dispositivo slave.

Nella figura 5 viene riportato il diagramma a blocchi relativo alla funzione spi_transfer.

Anche la funzione spi_transmit_to_display funziona in modo simile a spi_transfer: le uniche differenze sono relative al fatto che le interruzioni non vengono mai disabilitate (questa funzione viene eseguita solo nella routine di servizio per il refresh del display) e inoltre vengono trasmessi i 16 bytes letti dalla memoria video invece di un singolo byte.

5.3.4 Il file “main.c”

Il file “main.c” è il file principale che contiene la funzione main.

Dopo l’inclusione dei file di libreria e dei file “display.h” e “functions.c”, si passa alla dichiarazione di tutte le costanti e le variabili globali a cui si è fatto ricorso per il

cli() (disattivazione delle interruzioni) SPDR = data SPIF = 1 ? sei() (riattivazione delle interruzioni) return SPDR SI NO

(15)

funzionamento del firmware. Esse possono essere suddivise in gruppi sulla base del contesto nel quale sono inserite. Questi gruppi sono:

• Variabili e costanti relative al display led: ad esse fanno parte le variabili row (che tiene conto del numero di riga corrente per il rinfresco del display) e pos (che contiene l’indice nell’array SRAM_DATA[] dal quale cominciare la lettura dei dati), l’array SRAM_DATA[] (utilizzato come memoria video), NUM_BYTE e TOT_BYTE (che stabiliscono il numero di bytes che contengono i dati relativi ad una riga e a tutto il display).

• Variabili relative al buffer di ricezione dati dalla USART: fanno parte di questo gruppo l’array UART_BUFFER[] (utilizzato come buffer circolare per i dati provenienti dalla USART), le variabili write_pos e read_pos (che puntano alle posizioni correnti di scrittura dati e lettura dati nel buffer), EMPTY_BYTES e FULL_BYTES (che stabiliscono le soglie per l’invio dei messaggi “Almost Empty” e “Almost Full” attraverso l’interfaccia seriale), FLOW_CONTROL (che stabilisce se il controllo di flusso è attivo o disattivo).

• Variabili relative ai comandi grafici: fanno parte di tale categoria la variabile COLOR (che stabilisce se il comando grafico seguente dovrà accendere o spegnere i pixel che lo coinvolgono), le variabili x_last e y_last (che contengono le coordinate del secondo vertice dell’ultima retta tracciata per mezzo dei comandi “Draw line” o “Continue line”), l’array BAR_GRAPH[] (costituito da elementi di tipo struttura BG_struct: esso contiene i dati relativi ai grafici a barre eventualmente utilizzati), l’array BITMAP[] (contenuto nella eeprom interna al microcontrollore: è costituito da elementi di tipo EE_struct e tiene conto di alcune informazioni relative alle bitmaps eventualmente presenti nella eeprom esterna).

• Variabili relative ai FONTS: sono variabili che riguardano la metrica correntemente in uso e informazioni circa la posizione del cursore per la visualizzazione dei caratteri. Tra queste ricordiamo: x_position e y_position (coordinate relative al carattere da visualizzare utilizzando la metrica in uso), x_pixel_value e y_pixel_value (coordinate relative al carattere da visualizzare utilizzando il sistema di riferimento in pixel), FONT_LOAD (che stabilisce se è stato caricato o meno in memoria il font corrente), HEIGHT e WIDTH (altezza e larghezza

(16)

nominale del font correntemente in uso), l’array FONT_DATA[] (costituito da elementi di tipo EE_Struct analogamente all’array BITMAP[]), e tutti i parametri sulla metrica (LEFT MARGIN, TOP MARGIN, etc.).

Dopo la dichiarazione delle costanti e delle variabili, nel file “main.c” è presente il codice relativo alle due routine di interruzione per le quali è stata già stata fornita la descrizione.

Infine è presente la funzione main. Come è possibile vedere dall’appendice, il programma principale è molto contenuto, in quanto la parte onerosa di tutto il lavoro viene eseguita dalle funzioni execute_cmd e char_on_display. Le operazioni compiute dalla funzione main sono le seguenti:

Vengono eseguite tutte le routine per l’inizializzazione del sistema. In particolare esse sono: display_init, spi_init, ext_eeprom_init, timer1_init, uart_init e infine glob_var_init. Le funzioni che finora non sono ancora state descritte sono ext_eeprom_init e glob_var_init. La prima si occupa di inizializzare la memoria eeprom esterna: in particolare vengono impostate le linee ad essa relative e si verifica che non via nessuna zona di memoria protetta in scrittura (ad esempio potrebbe essere stata impostata una zona protetta a causa di un errore). La funzione glob_var_init, invece, si occupa di inizializzare alcune variabili ad un valore di default.

Dopo l’inizializzazione del sistema si provvede ad attivare le richieste di interruzione per mezzo della funzione sei, disponibile nella libreria “avr/interrupt.h”.

Dopo di essa viene eseguita la funziona main: viene avviato un loop infinito nel quale si controlla in continuazione la presenza di dati all’interno del buffer predisposto per ricezione dalla USART; se sono presenti dei dati si procede alla loro elaborazione, altrimenti non viene eseguito nulla. Qualora siano presenti dei dati nel buffer, si va ad effettuarne la lettura a partire dalla posizione puntata dalla variabile read_pos: se il dato letto risulta essere il carattere 254 (cioè il carattere che precede ogni comando), è necessario attendere che sia pervenuto nel buffer anche il tipo del comando stesso. Ad occuparsi di questo è la funzione wait_requested_bytes. Essa controlla il buffer UART_BUFFER[] finchè non rileva la presenza del numero di dati che le è stato passato come parametro. Una volta che sono pervenuti il numero di bytes richiesti termina la sua esecuzione. A questo punto a svolgere tutte le funzioni di elaborazione ed

(17)

esecuzione è la funzione execute_cmd alla quale viene passato il tipo di comando letto dal buffer.

Qualora non sia stato riconosciuto il carattere 254, significa che il dato pervenuto è un carattere da visualizzare. In tal caso non è necessaria nessuna attesa e viene eseguita la funzione char_on_display, a cui è affidato il compito di elaborare il carattere pervenuto e di aggiornare SRAM_DATA[] affinché venga visualizzato. Nel diagramma a blocchi della figura seguente (figura 6) viene illustrato il funzionamento di tutto il programma principale.

Nei prossimi paragrafi verrano descritti anche i file “display.h” e “functions.c”.

Figura 6 – Diagramma a blocchi del programma principale (contenuto nel file “main.c”). Inizializzazione

del sistema

Si attende che vi sia anche il tipo di

comando Sono presenti dei

dati nell'array UART_BUFFER[] ? Il dato è un comando ? Visualizzazione carattere SI NO SI NO

Lettura del tipo di comando

Esecuzione del comando

(18)

5.3.5 I file “display.h” e “functions.c”

Come è stato accennato in precedenza, il file “display.h” è un file header, ovvero contiene tutte le direttive di tipo “define” e le definizioni dei tipi di alcune variabili. Inoltre è presente la dichiarazione di tutte le funzioni che sono state definite nel file “function.c”.

L’utilizzo della direttiva “define” ha permesso di rendere più leggibile alcune parti del firmware, in quanto è stato sufficiente l’utilizzo di simboli per sostituire delle porzioni di codice.

Nel file display.h sono state inoltre dichiarati due tipi struttura:

• BG_Struct: viene utilizzata per definire il tipo del singolo elemento dell’array BAR_GRAPH[]. La struttura è costituita da 5 campi unsigned int che identificano il tipo di grafico a barre e l’area da esso occupata.

• EE_Struct: viene utilizzata per definire il singolo elemento degli array FONT[] e BITMAP[]; un elemento di tipo EE_Struct è costituito dai campi start, end e filled. L’elemento i-esimo dell’array FONT[] definisce informazioni circa la memorizzazione in eeprom del font identificato dal riferimento i: in particolare il campo filled stabilisce se esiste o meno un font con quel riferimento, mentre i campi start ed end definiscono le locazioni di memoria di inizio e fine dei dati di quel font. Stessa cosa vale per l’array BITMAP[], ma relativo alle bitmaps.

Per quanto riguarda invece il file “functions.c”, esso contiene tutte le dichiarazioni delle variabili definite nel file “main.c” ed inoltre la definizione di tutte le funzioni che sono state realizzate per il firmware (ad eccezione naturalmente della funzione main).

5.3.6 La funzione execute_cmd

Parlando a proposito della funzione main è stato detto che qualora debba essere elaborato un comando, ad occuparsi di tutte le operazioni necessarie è la funzione execute_cmd.

Al momento della sua chiamata, la funzione main le passa il codice del comando da elaborare (letto dall’array UART_BUFFER[]). La funzione execute_cmd è costituita

(19)

da un’istruzione switch con un numero di casi pari al numero dei comandi presenti nel display Matrix Orbital. Una volta che è stato individuato il caso a cui appartiene il comando da elaborare, viene eseguita una porzione di codice. Nella maggioranza dei casi questo codice effettua una chiamata ad una funzione preposta all’esecuzione del comando stesso. Tuttavia, quando la funzione da svolgere è molto semplice (come ad esempio cambiare il contenuto di una o più variabili globali), la fase di elaborazione viene direttamente eseguita nel costrutto case senza effettuare alcuna chiamata di funzione.

Una volta che il comando è stato eseguito, si passa ad effettuare l’eliminazione dei dati utilizzati dal’array UART_BUFFER[]: questo avviene grazie all’utilizzo della funzione delete_from_buffer. L’eliminazione dei dati dal buffer consiste nell’aggiornamento del puntatore di lettura e del numero di bytes liberi disponibili all’interno del buffer.

A questo punto termina l’esecuzione della funzione execute_cmd, e nella funzione main si torna all’elaborazione degli altri eventuali dati presenti nel buffer.

5.3.7 La funzione char_on_display

È la funzione che ha lo scopo di consentire la visualizzazione di caratteri sul display. Viene eseguita dalla funzione main qualora il dato letto dal buffer di ricezione dati non rappresenti un comando. Il principio di funzionamento è del tutto simile all’omonima funzione presente nell’emulatore LabVIEW™. Tuttavia l’approccio che è stato seguito è differente. Le operazioni che vengono eseguite in questa funzione sono le seguenti:

• Si valuta se il font correntemente selezionato dalla variabile CURRENT_FONT è effettivamente presente in memoria. Se ciò non accade non viene eseguita alcuna operazione

• Dopo aver accertato la presenza del font in memoria eeprom, si verifica se le informazioni di base relative al font stesso sono state già ricavate in precedenza (ovvero nel caso in cui sia già stato visualizzato un carattere con lo stesso font): in caso affermativo non è necessario ricavarle di nuovo in quanto sono già state salvate in opportune variabili globali (WIDTH, HEIGHT, FIRST_CHAR, LAST_CHAR).

(20)

Se tali informazioni non sono disponibili è necessario effettuare la lettura dalla memoria eeprom esterna di questi dati. Si và a reperire dall’elemento FONT[CURRENT_FONT] (presente nella memoria eeprom interna al microcontrollore) la locazione della memoria esterna da cui ha inizio e in cui finisce il font corrente. Si effettua tale lettura per mezzo della funzione spi_transfer e si salvano tali dati nelle variabili globali preposte a tale scopo.

• A questo punto viene fatta la verifica relativa al codice ASCII del carattere pervenuto: se tale carattere è compreso tra quelli definiti nel font allora è possibile procedere alla visualizzazione, altrimenti si esegue solamente l’aggiornamento del buffer e la funzione giunge al termine.

• Se il carattere ha le credenziali per poter essere visualizzato, viene effettuata la lettura dalla eeprom della larghezza specifica del carattere, e si esegue la funzione display_char: a questa vengono passati come parametri l’indirizzo della locazione di memoria da cui inizia il font, il codice ASCII del carattere, il codice ASCII del primo e dell’ultimo carattere definiti, l’altezza del font e infine la larghezza specifica del carattere.

• Terminata la funzione display_char, vengono aggiornate le condizioni del buffer UART_BUFFER[].

(21)

Figura 7 – Diagramma a blocchi relativo alla funzione char_on_display. Inizio Esiste in eeprom il font selezionato ? FONT_LOAD = FALSE ? Vengono reperiti i dati relativi al font

dalla memoria

Il carattere è stato definito

nel font?

Si legge dalla memoria la larghezza specifica del carattere Viene eseguita la funzione display_char Aggiornamento di UART_BUFFER[] Fine NO SI SI NO SI NO

(22)

5.3.8 Cenni al salvataggio di fonts e bitmaps

Grazie alla dotazione di una memoria eeprom esterna al microcontrollore è possibile il salvataggio permanente di fonts e bitmaps anche dopo il reset o l’assenza di alimentazione. L’utilizzo della parte di memoria flash del microcontrollore non utilizzata per il firmware, in aggiunta a quella esterna, non è stato previsto allo stato attuale del firmware: può tuttavia costituire un possibile sviluppo futuro.

Il salvataggio di fonts e bitmaps avviene rispettivamente mediante l’utilizzo dei comandi “Upload Font” e “Uplaod Bitmap” inviati al sistema da parte del controller esterno. Il protocollo di comunicazione tra il controller esterno ed il prototipo non cambia rispetto a quello descritto nell’emulatore LabVIEW™, e per tale motivo non viene riproposto. Ci sono tuttavia altri aspetti che presentano delle differenze e che verranno descritti nel seguito. Prendiamo in considerazione ad esempio il salvataggio di un font (il caso della bitmap segue lo stesso principio).

Dopo che il controller esterno ha comunicato al microcontrollore le dimensioni del font, è necessario valutare se c’è spazio sufficiente per contenerlo. Nel seguito indicheremo con ref l’identificatore con cui il font si appresta ad essere salvato in memoria.

Dalla memoria eeprom del microcontrollore viene letto lo stato del campo filled relativo a FONT_DATA[ref], e il valore delle variabili write_pos e free_space. (si ricorda che write_pos punta la locazione di memoria esterna a partire dalla quale verrà memorizzato il font, mentre free_space indica il numero di bytes di memoria disponibili).

Dopodichè si verifica se un font con lo stesso riferimento è già presente in memoria: se così fosse è necessario aggiungere alla memoria disponibile anche lo spazio occupato dal vecchio font.

Fatta questa considerazione è possibile valutare se lo spazio disponibile in memoria è sufficiente a contenere il font. Se c’è spazio sufficiente viene inviato al controller il carattere esadecimale 0x01, altrimenti si invia il carattere 0x08.

Qualora vi sia spazio disponibile e un font con lo stesso riferimento è già presente, è necessario liberare la memoria occupata dal vecchio font. Questa operazione viene

(23)

compiuta dalla funzione erase_file, che si occupa di traslare il contenuto della memoria e di aggiornare i campi di inizio e di fine di fonts e/o bitmap contenuti nella porzione di memoria traslata.

Da questo momento in poi inizia il salvataggio di tutti i dati inviati al microcontrollore. Se durante la fase di trasferimento viene rilevato un echo errato da parte del controller esterno o del microcontrollore, non è più possibile ripristinare un eventuale font presente in precedenza con lo stesso riferimento, in quanto cancellato. In tal caso si pone il campo filled di FONT[ref] al valore FALSE, ad indicare che la posizione individuata da ref è disponibile al salvataggio di un font.

5.4 Breve descrizione dei comandi grafici

In appendice è presente la lista di tutti i comandi che sono stati previsti per il prototipo; in questo paragrafo sarà fornita una breve descrizione dei soli comandi di tipo grafico. I comandi grafici in tutto sono 9:

• Set drawing color: permette di stabilire il “colore” dei pixel che saranno coinvolti dai successivi comandi grafici (ovvero stabilisce se i pixel dovranno essere accesi o spenti). Il comando viene eseguito all’interno della funzione execute_cmd senza l’ausilio di alcuna funzione dedicata: infatti l’operazione che viene fatta consiste semplicemente nella scrittura della variabile globale COLOR.

• Draw line: questo comando è accompagnato dalle coordinate (x1,y1) e (x2,y2) dei due estremi del segmento di retta da visualizzare. Ad eseguirlo è la funzione draw_line. Essa calcola le coordinate dei pixel da accendere o spegnere (in funzione del contenuto della variabile COLOR) e aggiorna il contenuto della memoria video (SRAM_DATA[]). Il suo funzionamento è il seguente: vengono calcolate le quantità ∆x = |(x2-x1)| e ∆y = |(y2-y1)|; se accade che ∆x è maggiore o uguale a ∆y, allora per il calcolo delle coordinate dei punti costituenti la retta viene utilizzata la relazione y = f(x), altrimenti si utilizza la relazione x = f(y). Questo consente di tracciare la retta utilizzando il numero maggiore di punti. Per fare un esempio, supponiamo che i due estremi siano: (x1,y1) = (0,10) e (x2,y2) = (80,11); se per il calcolo dei pixel da accendere (o spegnere) si utilizza la relazione y = f(x) i pixel coinvolti sarebbero 81

(24)

in quanto viene calcolato il valore della coordinata y per 81 valori di x; invece nel caso in cui si utilizzasse la relazione x = f(y) se ne accenderebbero (o spegnerebbero) soltanto 3. Una volta che la funzione ha determinato i pixel da accendere o spegnere, si procede all’aggiornamento di SRAM_DATA[].

• Continue line: traccia una linea sul display utilizzando per un estremo le coordinate (x2,y2) dell’ultima retta tracciata e come secondo estremo le coordinate (x,y) che riceve come parametri. Le coordinate x2 e y2 (che tale funzione non riceve insieme al comando) vengono lette grazie all’ausilio di due variabili globali (x_last e y_last) che vengono aggiornate ad ogni esecuzione dei comandi Draw line e Continue line. Per l’esecuzione del comando Continue line viene usata la funzione draw line alla quale vengono passate le coordinate dei due estremi come parametri. • Put pixel: è la funzione grafica più elementare, che permette di accendere o spegnere un solo pixel del display (a seconda del contenuto dalla variabile COLOR). I parametri trasmessi dopo il comando sono le coordinate (x,y) del pixel da accendere o spegnere. L’esecuzione di questo comando è affidata alla funzione put_pixel: essa, sulla base delle coordinate ricevute, calcola il byte a cui è associato il pixel coinvolto nel comando e setta o resetta il bit corrispondente nell’array SRAM_DATA[].

• Draw outline rectangle: questo comando consente di tracciare sullo schermo i lati di un rettangolo del quale sono state fornite le coordinate di due vertici opposti. Per la sua esecuzione utilizza la funzione elementare put_pixel. Il “colore” da utilizzare viene fornito tra i parametri del comando.

• Draw solid rectangle: attraverso questo comando è possibile tracciare un rettangolo “pieno” sul display. Come per il comando precedente viene trasmesso insieme ai parametri il “colore” da utilizzare.

• Initialize bar graph: a tale comando sono associati ben 6 parametri: il numero identificativo del grafico a barre che si sta inizializzando, il tipo di grafico e le coordinate di due vertici opposti relativi all’area occupata dal grafico stesso. È possibile inizializzare fino a 16 grafici a barre. Le informazioni relative a ciascuno dei 16 grafici a barre sono contenute in un array di elementi di tipo BG_struct. Ogni

(25)

elemento è costituito da 6 campi che ne definiscono le proprietà. L’esecuzione di questo comando viene compiuta dalla funzione initialize_bar_graph.

• Write to bar graph: tale comando è accompagnato dal numero identificativo del grafico a barre coinvolto e dal valore da assegnare a quest’ultimo. Questo comando viene eseguito dalla funzione write_to_bar_graph.

• Display saved bitmap: questo comando è seguito da 3 parametri, ovvero il numero di riferimento della bitmap salvata e le coordinate del display a partire dalle quali essa deve essere visualizzata. Ad occuparsi dell’esecuzione di questo comando è la funzione display_saved_bitmap. Essa controlla innanzitutto che la bitmap da visualizzare sia presente in memoria. Ne vengono determinate le dimensioni e si procede alla lettura dalla memoria eeprom. Terminato il processo di lettura si procede all’aggiornamento della variabile SRAM_DATA[].

Figura

Figura 1 – Diagramma a blocchi relativo alla routine di servizio per il rinfresco del display
Figura 2 – Diagramma a blocchi di una delle due USART presenti nell’ ATmega128 (tratta dal datasheet  dello stesso)
Figura 3 – Diagramma a blocchi del funzionamento della routine di interruzione per la ricezione di dati  dalla USART dell’ATmega128
Figura 4 – Struttura a blocchi dell’interfaccia SPI dell’ATmega128 (tratta dal relativo datasheet)
+4

Riferimenti

Documenti correlati