Conversione da UART a SPI con FPGA

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

Figura 1: apertura del progetto Libero 8.5.

Figura 1: apertura del progetto Libero 8.5.

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.

Figura 2: schema a blocchi del progetto.

Figura 2: schema a blocchi del progetto.

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

Figura 3: gestione dei due protocolli per la scrittura e la lettura

Figura 3: gestione dei due protocolli per la scrittura e la lettura

Per la gestione della scrittura e lettura di memoria si veda il flow chart di figura 4 e la relativa tabella 1 dei comandi.

Figura 4: procedure di lettura e scrittura.

Figura 4: procedure di lettura e scrittura.

 

Tabella1: definizione dei comandi.

Tabella1: definizione 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).

Figura 5: il simulatore per l’analisi dei parametri e delle tempistiche

Figura 5: il simulatore per l’analisi dei parametri e delle tempistiche

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.

Scrivi un commento