Molto spesso è necessario poter utilizzare direttamente componenti che abbiano un’interfaccia di tipo SPI come sistema di comunicazione. L’articolo evidenzia come sia possibile una comunicazione diretta su questi componenti, utilizzando la classica seriale RS232 senza uso di microprocessori e relativi FW, ma integrando il tutto in una piccola ed economica FPGA.
L’articolo fornisce i dettagli per l’integrazione di un sistema in grado di dialogare all’esterno con un PC che utilizza una comunicazione UART (per esempio la sempre presente RS232), pilotando a sua volta una periferica esterna con interfaccia di comunicazione di tipo SPI. Le porte di comunicazione SPI sono ampiamente utilizzate in tutti i sistemi embedded, grazie alla loro semplicità di interfaccia HW e alla possibilità di comunicazione sincrona e full duplex. In questo modo con pochi I/O è possibile comunicare in modo sincrono e con velocità anche di alcune decine di Mbit con periferiche di tutti i tipi (dalle memorie ai convertitori A/D e D/A). Genericamente il protocollo di tipo SPI standard è estremamente flessibile e permette di definire sia la fase che la polarità (rispetto al clock di base) con cui i dati verranno memorizzati dalla periferica pilotata oppure letti dal microprocessore. Nato per gestire periferiche veloci sullo stesso PCB, il protocollo SPI mal si adatta a trasmissione su cavo, a causa della presenza del clock che deve sempre accompagnare il flusso dati da master verso slave. La disponibilità sul mercato di un grande numero di periferiche HW compatibili con questo tipo di comunicazione può far nascere l’esigenza di avere un semplice convertitore da UART a SPI, da inserire in un piccola FPGA, senza per forza dover utilizzare un microcontrollore, che, seppur economico, richiede la scrittura di un FW che gestisca le due periferiche.
Struttura del progetto: schema a blocchi
Con l’ambiente Libero 8.5 regolarmente installato, se avete scaricato il nostro progetto, è sufficiente, dopo aver lanciato Libero 8.5, selezionare menu Project > Open project e con un doppio click su file di progetto PRJ aprirlo (figura 1).
Si noti che, essendo già compilato correttamente, le icone di sintesi e Place and Route sono colorate in verde. Il progetto come schema a blocchi è presentato in figura 2 ed è composto fondamentalmente da tre blocchi: l’interfaccia UART, il blocco UART to SPI e l’interfaccia SPI MASTER.
Il clock di base deve essere fornito dall’esterno, mentre il Reset HW previsto, sempre dall’esterno, opera a livello attivo basso. L’interfaccia UART è ottenuta usando la macro CoreUart, fornita gratuitamente da Actel/Microsemi nel sistema di sviluppo in formato netlist (quindi senza sorgenti), ma documentata con relativo handbook. Il blocco UART to SPI interface è a tutti gli effetti il top level di progetto, dove i vari moduli vengono connessi sia fra loro che verso il mondo esterno; il passaggio e il controllo del flusso fra il modulo UART e il modulo SPI MASTER viene sviluppato dal blocco UART_SPI_CNTRL. Il modulo SPI MASTER, infine, gestisce il controllo della comunicazione con le periferiche SPI collegate, tutte di tipo slave. Il processo generale per il funzionamento del progetto prevede, subito dopo il reset, l’invio di un singolo byte utile alla programmazione della SPI (dettagli nella sezione successiva). Inviato il byte di programmazione è possibile, a seconda del valore che avrà il segnale di ingresso SPI_OR_MEM, avere due protocolli di comunicazione differenti per poter gestire letture e scritture anche a memorie di tipo SPI. Nel caso che il segnale SPI_OR_MEM sia a 1, il protocollo di comunicazione si limita all’invio del comando di Read (0x01) o di Write (0x01), seguito dalla relativa lettura o scrittura (flow chart figura 3).
Per la gestione della scrittura e lettura di memoria si veda il flow chart di figura 4 e la relativa tabella 1 dei comandi.
SERIALE SPI: spi_master.VHD
Il blocco SPI in questione ha il pregio di essere una seriale SPI di tipo master, fornita a livello di sorgente e non come macro gratuita ma offuscata, come nel caso della UART. Pertanto, essendo il codice disponibile, sarà possibile modificare il suo funzionamento. Come vedremo nella fase di implementazione, alla partenza è possibile programmare il modulo SPI. In particolare il registro di programmazione e controllo a otto bit prevede la selezione di uno degli otto slave collegati alla SPI master (bit 7 - 5), la scelta della polarità operativa del clock CPOL (bit 4), oltre alla selezione della fase per poter maneggiare i dati sul fronte di salita o di discesa del clock (bit 3). Infine i bit 2-0 vengono utilizzati per il prescaler previsto nella macro, per cui dalla frequenza base che viene fornita (nel nostro caso 20 MHz) è possibile dividerla da un minimo di quattro a un massimo di cinquecentododici, con passi multipli di tipo binario. Pertanto, una volta programmato il registro di controllo è possibile scrivere o leggere dal blocco SPI, utilizzando due fili di indirizzo (listato 1).
— Reading, writing SPI registers — process(pro_clk, nreset, CS, addr, status, data_in, WR, RD ) begin if ( nreset = ‘0’ )then control <= (others => ‘0’) ; data_s <= (others => ‘0’) ; txdata <= (others => ‘0’) ; elsif rising_edge (pro_clk) then if (CS = ‘1’) then case (addr) is when “00” => if (WR = ‘1’) then control <= data_in; end if; when “01” => if (RD = ‘1’) then data_s <= status; end if; when “10” => if (WR = ‘1’) then txdata <= data_in; end if; when “11” => if (RD = ‘1’) then data_s <= shift_register; end if; when others => data_s <= x”00”; txdata <= x”00”; control <= x”00”; end case; end if; end if; end process;
Listato 1 |
Dalla figura notiamo il process, che permette le operazioni di lettura e scrittura. In particolare in presenza del segnale CS attivo alto (in arrivo dal blocco SPI uart_spi_cntrl.vhd), sotto la cadenza del clock di sistema a 20 MHz, questa sezione decodifica i due fili di indirizzo e in condizione di valore 0x0 carica il registro di controllo sopra descritto con quanto presente sul bus dati in ingresso (anche questo generato e controllato dal blocco uart_spi_cntrl.vhd); con indirizzo 0x01 e segnale di lettura RD attivo viene caricato sul bus dati di uscita il valore del registro di status della SPI. La presenza dell’indirizzo 0x02 e del segnale di scrittura WR attivo produce il caricamento del registro dati in trasmissione, mentre l’indirizzo 0x03 in unione con il segnale RD attivo permette la lettura del dato in ricezione. Del codice VHDL, che compone il modulo, osserviamo nel listato 2 come viene effettuata la selezione della frequenza generata dal prescaler. Il codice evidenzia l’uso di un semplice contatore libero a otto bit definito clk_divide; utilizzando i tre bit caricati nel registro di controllo come puntatori, si seleziona una delle otto uscite del contatore come sorgente di clock, da utilizzare per la sezione di ricezione e trasmissione specifica per la SPI.
process(pro_clk, nreset) begin if nreset = ‘0’ then clk_divide <= (others => ‘0’) ; elsif falling_edge (pro_clk) then clk_divide <= clk_divide + 1 ; end if; end process; spi_clk_gen <= clk_divide(conv_integer(divide_factor)) ; sclk_cpha_cpol_sig <= sclk_sig xor (CPHA xor CPOL) ;
Listato 2 |
Si noti anche la conversione da binario a intero, necessaria per la selezione dell’uscita del contatore, utilizzando la funzione conv_integer, a cui viene passato un valore binario e restituisce un intero. Per ultimo il segnale sclk_cpha_cpol_sig, diretta conseguenza di CPOL e PH, permetterà la lettura e scrittura dei bit su SPI con la fase e la polarità del clock desiderato.
Blocco da UART a SPI: uart_spi_cntrl.vhd
Il blocco uart_spi_cntrl è il cuore del progetto, in quanto consente il controllo della periferica SPI, utilizzando quanto ricevuto dalla UART. Per permettere questa gestione l’esempio fa uso di una macchina a stati divisa in tre sezioni ben distinte (listato 3).
type uart_read is (cntrl_st1, cntrl_st2, idle_st,delay_st, oen_st, data_latch, spi_wr_st1, spi_wr_st2, wr_st1, wr_st2, wrsr_st1, wrsr_st2, rdsr_st1, rdsr_st2, write_st, wr_st, pp_st, tx_st, read_st, pr_st, rx_st, dummy_st1, dummy_st2, rd_pg_st, rd_pg_st2, rd_en_st, rd_en_st1, wr_uart_st, read_done_st,write_done_st); signal pr_state, nxt_state : uart_read; process (nreset, clock_20m) begin if (nreset = ‘0’) then pr_state <= cntrl_st1 ; elsif (clock_20m’event and clock_20m = ‘1’) then pr_state <= nxt_state ; end if; end process ; process (pr_state, rxrdy, txrdy, tx_rx_done, pwdata_m_sig, paddr_m_sig, counter_3, fast_read_sig, se_sig) begin case (pr_state) is when cntrl_st1 => if (rxrdy = ‘1’) then nxt_state <= cntrl_st2 ; else nxt_state <= cntrl_st1 ; end if; when cntrl_st2 => nxt_state <= idle_st ; when idle_st => if (rxrdy = ‘1’) then nxt_state <= delay_st ; else nxt_state <= idle_st ; end if; when delay_st => nxt_state <= oen_st ; process (pr_state, rxrdy, counter_3, pwdata_m_sig) begin case (pr_state) is when cntrl_st1 => paddr_m_sig <= “00” ; pwrite_m_sig <= ‘0’ ; cs_m <= ‘0’ ; user_cs_sig <= ‘1’ ; pread_m_sig <= ‘0’ ; wen_s <= ‘1’ ; cnt_rd_en <= ‘0’ ; idle_en <= ‘0’ ; fast_rd_en <= ‘0’ ; se_flag <= ‘0’ ; write_done <= ‘0’; read_done <= ‘0’; if (rxrdy = ‘1’) then oen_s <= ‘0’ ; else oen_s <= ‘1’ ; end if; when cntrl_st2 => oen_s <= ‘1’ ; paddr_m_sig <= “00” ; pwrite_m_sig <= ‘1’ ; cs_m <= ‘1’ ; user_cs_sig <= ‘1’ ; pread_m_sig <= ‘0’ ; wen_s <= ‘1’ ; cnt_rd_en <= ‘0’ ; idle_en <= ‘0’ ; fast_rd_en <= ‘0’ ; se_flag <= ‘0’ ; write_done <= ‘0’; read_done <= ‘0’; when idle_st => oen_s <= ‘1’ ; paddr_m_sig <= “00” ; pwrite_m_sig <= ‘0’ ; cs_m <= ‘0’ ; user_cs_sig <= ‘1’ ; pread_m_sig <= ‘0’ ; wen_s <= ‘1’ ; cnt_rd_en <= ‘0’ ; idle_en <= ‘0’ ; fast_rd_en <= ‘0’ ; se_flag <= ‘0’ ; write_done <= ‘0’; read_done <= ‘0’;
Listato 3 |
Nel process in figura, al reset viene definito come valore di partenza lo stato cntrl_st1 e di seguito, sotto la cadenza del clock a 20 MHz, viene aggiornato lo stato pr_state con il valore next_state. Per coloro che non avessero esperienza con il linguaggio VHDL, ricordiamo che queste sono variabili di tipo enumerativo. Vengono definite con la dichiarazione Type e possono essere composte da un insieme ordinato di caratteri o identificatori. In questo modo è possibile creare un array di variabili di tipo enumerativo, utili in una macchina a stati, per capire dove ci si trova e che azione intraprendere per lo stato successivo. Sempre nel listato 3 abbiamo inserito la prima parte dei due case statement: il primo aggior na lo stato a seconda della condizione in cui ci si trova, mentre il secondo case definisce il valore delle variabili e dei segnali che piloteranno fisicamente i vari blocchi. Si vede dalla figura che dalla condizione di reset, che assegna il valore cntrl_st1, ci si sposta alla condizione cntrl_st2 solo in presenza del segnale rxready con valore alto e tale condizione si presenta solo ed esclusivamente quando il blocco UART ha ricevuto completamente un carattere. Nel secondo case si noti che allo stato cntrl_st1 (che è lo stato di partenza) i segnali pwrite_m_sig e cs_m sono a zero, mentre nello stato cntrl_st2 sono portati a 1 per permettere la scrittura del valore ricevuto dalla UART nel blocco SPI. Questi segnali sono la copia del CS della SPI e il comando di WR della medesima (riferimento alla schema a blocchi in figura 2). Con questa tecnica è possibile definire e controllare completamente il passaggio dei dati dalla UART alla SPI e viceversa. Questa struttura può essere presa come utile esempio non solo per questa specifica applicazione, ma per qualsiasi altro progetto, come controllo sincrono di un flusso dati.
Seriale UART: uart_cmp.VHD
Il blocco core UART è la seconda macro che utilizziamo, sfruttando sempre la libreria gratuita, che Actel/Microsemi mette a disposizione nella suite tools. Analizzando il datasheet di questa IP, vediamo che possiamo decidere di avere o meno delle FIFO, sia in ricezione che in trasmissione. Per l’abilitazione delle FIFO è sufficiente passare alla macro il parametro generico TX FIFO e RX FIFO, definendolo a 1 logico. In caso contrario le FIFO rimarranno disabilitate. Per la gestione del baud rate di ricetrasmissione, la macro ha predisposto un ingresso fisico come registro a 8 bit. Il calcolo del baud rate può essere effettuato usando la seguente formula: Baudrate = CLK / (baud_reg+1) * 16, dove CLK sarà il clock di sistema per il nostro progetto. Per il nostro esempio con un CLK di 20 MHz, volendo avere un baud rate di 9600 bit/sec, dovremo caricare nel registro baud_reg il valore 129 decimale. La macro mette inoltre a disposizione con uscita a pin le segnalazioni di base per la sua gestione: Txredy, Rxready per il controllo del flusso dati in trasmissione e ricezione, Parity error e Overflow per la verifica degli errori. Guardando lo schema a blocchi di figura 2 è possibile notare il pin BIT8 posto a uno per permettere la selezione del carattere di TX/RX a 8 bit (mentre a zero avremo il carattere a 7 bit) e il pin di PARITY ENAB, che se posto a zero disabilita la gestione del bit di parità in TX/RX.
Implementazione pratica
Il progetto è stato provato a livello di simulazione e allegato con esso troverete anche il file dedicato al simulatore Modelsim. In questa sezione, usando il file VHD di simulazione uart_to_spi_interface_tbench.vhd, abbiamo simulato la programmazione della SPI, in modalità master e conseguentemente una trasmissione di caratteri. Lanciando il simulatore sarà possibile analizzare i parametri e le tempistiche della sezione SPI (figura 5).
Per la simulazione va ricordato che è possibile avere più file di stimolo disponibili ed è possibile la scelta di questi ultimi cliccando con il tasto destro del mouse nel corner in alto a sinistra della casella stimulus (figura 1) e selezionando la voce organize stimulus. Nell’esempio presentato abbiamo simulato l’invio del carattere a 9600 baud del primo carattere di programmazione della SPI selezionando lo slave 0, con il clock master da 20 MHz diviso per 512 e polarità del clock di SPI in condizione di riposo a uno. Infine, abbiamo inviato il carattere 0x02 come comando di trasmissione seguito dal valore 0x55. La figura mette in evidenza l’ultima parte della simulazione per evidenziare la trasmissione su MOSI del carattere 0x55 con un clock di trasmissione di 39,062 KHz. Per arricchire ulteriormente la simulazione, sarà sufficiente lanciare il generatore di stimoli WaveFormer cliccando sulla casella omonima presente nel project flow. Una volta aperto troverete la sezione già costruita (di default carica per la simulazione della SPI il file uart_to_spi_interface_tbench.btim) e potrete proseguire disegnando ulteriori cicli. Ricordate sempre che, conclusa la creazione dei nuovi cicli, dovrete effettuare il salvataggio non solo della parte grafica disegnata (estensione *.btim), ma anche della generazione del file di simulazione vero e proprio. Per tale creazione è sufficiente selezionare dal menù file la voce Save as… il formato Top level VHDL test bench. Per l’implementazione in HW usando lo starter KIT AGL-NANO potrete utilizzare una porta USB del PC sfruttando il convertitore USB to UART montato sul kit. Le eventuali periferiche SPI (memorie o altro) potranno essere montate su una basetta millefori e collegate utilizzando i connettori a passo 2,54 mm previsti sul kit stesso
Conclusioni
Per verificare il numero di gate consumati per questa applicazione è sufficiente selezionare nel Sw di Place e Route Designer il menu Tools > Reports > Status. Dall’analisi di questo report possiamo verificare che il numero delle macrocelle utilizzate è di 672 celle base (o Tile) e per il package QN48 18 I/O utilizzati su 34 disponibili. Sempre nel report status possiamo verificare l’utilizzo dell’A3PN030 all’87% delle sue risorse. L’esperienza evidenzia come sia possibile con un componente economico, come una piccola CPLD, sfruttare periferiche previste per comunicazioni di tipo SPI, senza dover fare forzatamente uso di microprocessori e relativi FW di gestione, rimanendo nel medesimo target di costo.