Un convertitore USB per AVR

Tutto quello che serve per realizzare un convertitore utilizzando l’interfaccia seriale USB verso una porta parallela è un AVR e una manciata di componenti. Ciò che se ne ricava è un progetto estremamente semplice in grado di sfruttare ogni caratteristica di un processore di questo tipo.

L’idea è semplice: realizzare un convertitore da un’interfaccia USB lowspeed protocol utilizzando un processore della famiglia Atmel sfruttando USBtiny, un’implementazione software in grado di rispondere a questa nostra particolare esigenza. La soluzione che ci apprestiamo a realizzare è anche in grado di lavorare con la serie ATmega sfruttando un AVR con una frequenza di lavoro di 12 MHz. La forma d’onda richiesta per un’implementazione di questo tipo è realizzata attraverso il software. Il driver USB scelto per implementare questa funzionalità ha la necessità di utilizzare da 1250 a 1350 byte di memoria flash. Grosso modo, la dimensione di memoria richiesta per soddisfare questo requisito risulta comunque fortemente dipendente dalla configurazione scelta e dal sistema di sviluppo. Sono anche necessari circa 46 byte di ram: da questo computo è esclusa la quantità di memoria utilizzata dallo stack. Il driver fornisce diverse funzioni per gestire correttamente la forma d’onda richiesta.

L’interfaccia USB richiede due segnali differenziali, D+ e D-, che risultano normalmente complementari. I dati non sono trasmessi direttamente sul bus USB, ma sono codificati secondo il protocollo NRZI, a questo proposito il listato 2 mostra la funzione che si occupa della trasmissione del pacchetto. La connessione USB dispone di quattro fili, due per l’alimentazione (Vbus e Ground) e due per i dati. Tra i due fili di alimentazione è presente una tensione nominale di 5 Volt. La seconda coppia di fili (D+ e D-) serve alla trasmissione dei dati, con segnali di ampiezza massima di 3,3 Volt. Nello standard USB, o meglio nella versione 1.1, la trasmissione avviene su linea differenziale (cioè il bit dipende dalla tensione su entrambi i fili), sui fili D+ e D-. Con due fili sono possibili 4 combinazioni come mostrato nella tabella 3: la trasmissione di uno “0” avviene portando il potenziale del terminale D+ del cavo USB a livello basso e quello del terminale Da livello alto, e viceversa per trasmettere un “1”. I bit sono inviati partendo dal LSB. I dati vengono codificati e decodificati usando il metodo NRZI (Non Return to Zero Inverted – rappresenta i cambiamenti di stato logico con “1” e la permanenza di uno stato logico con “0”). Per la sincronizzazione viene adottata la tecnica del bit stuffing (per gruppi di sei bit alti), mentre l’inizio del pacchetto è caratterizzato dalla sequenza 01010100. La fine è marcata dallo stato Se0 per la durata di due bit, seguiti da un bit alto e dal passaggio in IDLE del trasmettitore. Secondo il protocollo USB sono possibili diversi tipo di segnali. Ad esempio con un reset signaling l’host può resettare la periferica inviando un SE0 (single ended zero) cioè portando entrambe i potenziali D+ e D- a livello basso per più di 2,5 s o il suspend signaling dove l’host può forzare la periferica in suspend mode, in cui il dispositivo non risponderà al traffico USB. Si attiva il suspend mode quando il bus rimane inattivo per più di 3ms e per un tempo non superiore ai 10ms. Tutti i trasferimenti sono iniziati dall’host, con metodo polling-selecting, inviando un pacchetto che, come un biglietto, autorizza i terminali a comunicare. In seguito, se la periferica è pronta, questa risponde all’invito e da ciò deriva l’instaurazione della trasmissione, tramite una procedura di handshaking. Ricordiamo che gli indirizzi delle periferiche sono assegnati dall’host in maniera dinamica, con un processo che viene continuamente ripetuto. Quando un device USB si collega al bus, l’host che lo governa interroga il dispositivo utilizzando l’indirizzo di default 0. Successivamente l’host assegna al device un indirizzo USB univoco nel bus, oltre a richiedere altre informazioni. La comunicazione tra Host e device si definisce transfer. Ogni transfer si suddivide in una o più transaction, una per ogni coppia host-device. Ogni transaction è suddivisa in packet, ed è composta da un token packet che definisce il destinatario e il tipo di transaction, uno o più data packet e un handshake packet per il controllo dell’errore. Il gestore dell’interrupt del driver, sincronizzato con il sync byte, si preoccupa di rimuovere la codifica NRZI e i bit di stuffing e, solo successivamente, inserisce il frame in arrivo in memoria ram. I frame sono inseriti in due buffer di ram al fine di ottimizzarne la gestione. Il driver offerto fornisce la funzione usb_poll() che deve essere invocata periodicamente per controllare qualsiasi pacchetto da trattare. Il driver supporta solo un endpoint. Per utilizzare correttamente il driver USB è necessario configurare le diverse macro presenti nel file usbtiny.h, oltre a fornire una funzione (usb_setup) per gestire il setup del frame. È anche possibile fornire le funzioni usb_in() e usb_out() per gestire tutte le transazioni di ingresso e uscita. Il software dell’utilizzatore deve poi invocare la usb_init() allo startup per configurare correttamente il drive. In seguito, durante il running mode, è necessario chiamare la funzione usb_poll() a intervalli regolari. Nel driver distribuito è possibile reperire le diverse implementazioni delle funzioni usb_Setup(), usb_in() e usb_out() nel file main.c.

TOOL

L’intero ambiente di lavoro è stato realizzato sfruttando una piattaforma Linux. La suite di compilazione è la classica applicazione del software GNU o, come direbbero altri, like GNU. Per questa ragione sono presenti make per gestire le differenti versioni software, gcc per compilare e binutils e glibc per costruire correttamente la nostra applicazione. Di conseguenza è necessario adattare i package gcc-avr, binutils-avr e avr-libc per le nostre esigenze. Inoltre, per inserire il codice sul target si utilizza avrdude su una linea parallela. È anche possibile utilizzare la  versione 3.4.3 di GCC con l’opzione –Os poiché genera codice abbastanza compatto. Occorre prestare però particolare attenzione sulla versione del compilatore. Infatti, ad esempio la versione 4.1.0 genera codice non particolarmente ottimizzato. Si è constato che la versione 4.1.0 genera una quantità di codice di almeno del 10% in più e questo comporta  una  richiesta di maggiore spazio di memoria non disponibile sul target. La figura 1 mostra lo schematico che realizza l’interfaccia di conversione.

Figura 1: USB converter.

Figura 1: USB converter.

Il progetto è gestito via host con un PC attraverso una utility Python. A questo proposito il modulo software usbtiny.py definisce una classe USB che è, a sua volta, utilizzata per comunicare con il firmware residente su scheda.

PROGETTO

ATtiny2313, tabella 4, è un microcontrollore RISC che racchiude straordinaria potenza in un piccolo package a 20 pin, con frequenza di clock fino a 20MHz per applicazioni avanzate. In grado di eseguire un’istruzione in un singolo ciclo di clock, consente di ottimizzare il consumo di potenza nei confronti della velocità di esecuzione, anche sfruttando le modalità di basso consumo. Ha un flash da 2K bytes che con la funzionalità ISP (In-System - Programming) può essere programmata con interfaccia SPI dopo che il microcontrollore è già stato montato sulla scheda che lo deve ospitare. La figura 1 pone in evidenza una possibile realizzazione della nostra interfaccia. Dalla figura vediamo che i segnali del connettore USB possono essere collegati verso il blocco SPI (Serial Programming Interface) interno. I segnali sono poi pilotati verso una porta parallela, connettore di tipo DB-25. La figura mostra che il segnale di ACK è collegato alla porta INT1. La figura mostra come collegare qualsiasi dispositivo dotato di porta parallela. Dalla figura notiamo i ruoli di protezione delle resistenze. La funzione usb_poll() monitora costantemente l’interfaccia, listato 1, mentre il listato 2 mostra l’implementazione della trasmissione su USB.

extern void usb_poll ( void )
{
 byte_t i;

// check for incoming USB packets
if ( usb_rx_len != 0 )
{
 usb_receive( usb_rx_buf + USB_BUFSIZE - usb_rx_off + 1, usb_rx_len - 3 );
 usb_tx_len = 0; // abort pending transmission
 usb_rx_len = 0; // accept next packet
}
// refill an empty transmit buffer, when the transmitter is active
if ( usb_tx_len == 0 && usb_tx_state != TX_STATE_IDLE )
{
 usb_transmit();
}
// check for USB bus reset
for ( i = 10; i > 0 && ! (USB_IN & USB_MASK_DMINUS); i— )
{
}
if ( i == 0 )
{ // SE0 for more than 2.5uS is a reset
 usb_new_address = 0;
 usb_address = 0;
#ifdef USBTINY_USB_OK_LED
 CLR(USBTINY_USB_OK_LED); // LED off
#endif
 }
}
</APPNOTES>
Listato 1 – USB_POLL
static                        voidusb_transmit ( void )
{
byte_t                        len;
byte_t*                       src;
byte_t*                       dst;
byte_t                        i;
byte_t                        b;
usb_tx_buf[0] ^= (USB_PID_DATA0 ^ USB_PID_DATA1);
len = usb_tx_total;
if                           ( len > 8 )
{
                             len = 8;
}
dst = usb_tx_buf + 1;
if                           ( len > 0 )
{
#if                          USBTINY_CALLBACK_IN
                             if ( usb_tx_state == TX_STATE_CALLBACK )
                             {
                                len = usb_in( dst, len );
                             }
                             else
#endif
                            {
                               src = usb_tx_data;
                               if ( usb_tx_state == TX_STATE_RAM )
                               {
                                   for ( i = 0; i < len; i++ )
                                   {
                                       *dst++ = *src++;
                                   }
                                }
                                else // usb_tx_state == TX_STATE_ROM
                                {
                                     for ( i = 0; i < len; i++ )
                                     {
                                          b = pgm_read_byte( src );
                                          src++;
                                          *dst++ = b;
                                      }
                                 }
                                 usb_tx_data = src;
                             }
                             usb_tx_total -= len;
 }
 crc( usb_tx_buf + 1, len );
 usb_tx_len = len + 3;
 if                        ( len < 8 )
 {                         // this is the last packet
                           usb_tx_state = TX_STATE_IDLE;
 }
}
Listato 2 – Trasmissione su USB

Vediamo che il controllo dell’interfaccia è espletato su un PC attraverso una utility Python. Per permettere la programmazione l’Atmel potrebbe essere alimentato attraverso il pin 14 del connettore DB25. Per questa funzionalità può essere richiesto un adattatore dove la tabella 2 mostra le connessioni richieste. La figura 3 mostra la nostra schedina a lavoro ultimato. In realtà la gestione dei segnali SPI via USB si rileva abbastanza problematica per via delle tempistiche lente e non apprezzabili. Per questo motivo si è deciso di utilizzare l’algoritmo SPI direttamente in AVR con l’invio di un comando a 32 bit SPI in un singolo pacchetto USB. Inoltre, è anche possibile leggere o scrivere fino a 255 byte da o/e per flash o EEPROM in un trasferimento unico. Il nostro componente deve essere connesso ad un clock esterno di 12 MHz. In realtà la nostra applicazione si presta a diversi usi: da un canale USB a qualsiasi interfaccia esterna su diversi protocolli. Per esigenze di protezione è possibile ridurre la tensione sui dati USB aggiungendo un diodo Zener tra il segnale e la massa.

Tabella 1: corrispondenza tra DB25 e ATmel.

Tabella 1: corrispondenza tra DB25 e ATmel.

 

Tabella 2: corrispondenza tra SPI e DB25.

Tabella 2: corrispondenza tra SPI e DB25.

 

Tabella 3: USB Low Speed.

Tabella 3: USB Low Speed.

 

Tabella 4: specifica tecnica Atmel ATtiny.

Tabella 4: specifica tecnica Atmel ATtiny.

UNA NUOVA APPLICAZIONE

Come possiamo utilizzare la nostra interfaccia di conversione? È pensabile utilizzare il progetto USBtiny per realizzare un ricevitore per un controllore remoto ad infrarossi. A questo proposito il controllo del nostro dispositivo può essere realizzato attraverso un modulo Python (ir.py, listato 3).

import sys, os.path
sys.path[0] = os.path.join(sys.path[0], ‘../util’)
import usbtiny
vendor = 0x03eb
product = 0x0002
IGORPLUG_CLEAR = 1 # clear IR data
IGORPLUG_READ = 2 # read IR data (wValue: offset)
LCD_INSTR = 20# write instructions to LCD (via OUT)
LCD_DATA = 21# write data to LCD (via OUT)
usage = “”“Available commands:
t - perform USB echo test
c - clear IR data
r - read current IR data
i <byte> ... - send instruction bytes to LCD
d <byte> ... - send data bytes to LCD
s <string> ... - send strings to LCD”“”
dev = usbtiny.USBtiny(vendor, product)
cmd = ‘?’
if len(sys.argv) > 1:
cmd = sys.argv[1]
arg = sys.argv[2:]
if cmd == ‘r’:
data = dev.control_in(IGORPLUG_READ, 0, 0, 3 + 36)
usbtiny.dump(0, data)
elif cmd == ‘c’:
dev.control_in(IGORPLUG_CLEAR, 0, 0, 0)
elif cmd == ‘t’:
dev.echo_test()
elif cmd == ‘i’:
s = ‘’.join([chr(int(x,16)) for x in arg])
dev.control_out(LCD_INSTR, 0, 0, s)
elif cmd == ‘d’:
s = ‘’.join([chr(int(x,16)) for x in arg])
dev.control_out(LCD_DATA, 0, 0, s)
elif cmd == ‘s’:
dev.control_out(LCD_DATA, 0, 0, ‘ ‘.join(arg))
else:
print >> sys.stderr, usage
sys.exit(1)
Listato 3 – Ir.py

Come misura visuale si utilizza un LED per verificare quando un segnale è ricevuto. È anche possibile connettere un display LCD 2x16 alla porta B. Il programmatore può controllare il display attraverso il bus USB. In figura 2 mostriamo lo schematico del circuito. È possibile disabilitare/abilitare la porzione LCD intervenendo sulla macro LCD_PRESENT contenuta nel file main.c. In questa realizzazione si utilizza un TSOP17 al fine di permettere la ricezione dei segnali per un controllore remote a infrarossi. Il componente dispone di tre pin (GND, Vs e OUT).

Figura 2: controller remoto con USB

Figura 2: controller remoto con USB

 

Figura 3: convertitore a lavoro ultimato.

Figura 3: convertitore a lavoro ultimato.

Scrivi un commento

Send this to a friend