Corso su ARM: le comunicazioni su I2C

Alcuni dei sensori che costituiscono la dotazione della FRDM-KL46Z comunicano tramite il bus I2C. Comodo per alcuni, eccessivamente "semplice" per altri, comunque lo vogliate vedere si tratta di un'interfaccia di comunicazione utile e che può essere impiegata con grande successo. Oggi vediamo il dettaglio del suo utilizzo in funzione dei fenomeni di stallo cui va spesso in contro la MCU. Siete pronti?

Il nostro corso sta andando avanti e si addentra sempre di più nelle applicazioni con lo scopo di diventare sempre più specialistico e di farvi vedere casi implementativi reali di grande interesse. L'argomento di oggi riguarda l'utilizzo del bus di comunicazione I2C, uno dei più gettonati quando si lavora con sensori "semplici" e sicuramente tutti gli amanti di Arduino lo conosceranno molto bene.

Su I2C è possibile che si verifichi un caso in cui avvenga il blocco delle comunicazioni ovvero quel fenomeno per cui il microcontrollore si blocca e resta in attesa fino all'avvenuto trasferimento dei byte dal registro dei dati. All'interno del codice questo viene generalmente implementato come controllo del bit di stato all'interno di un ciclo "while". Ciò nondimeno questo tipo di approccio potrebbe non essere utile all'interno di applicazioni in cui gli intervalli temporali di esecuzione dei comandi, oppure il tempo, più in generale, siano un parametro critico, soprattutto applicazioni real-time. Se assumiamo che la velocità di trasferimento dei dati sia di 100 kHz, allora per trasferire 1 byte il microcontrollore, o la CPU più in generale, rimarrà ferma per 9 microsecondi. Il tempo totale all'interno del quale il processore resta in attesa della trasmissione dei dati oppure della loro ricezione diventa molto più grande per effetto del fatto che i messaggi su questo bus sono costituiti da 3 byte.
Vedremo descritti i dettagli di un algoritmo che permette di inviare l'intero pacchetto senza che il processore sia “incastrato” all'interno di loop che rischiano di far perdere tempo inutilmente.
Prima di andare avanti, però, per chiarezza, è fondamentale spiegare tre distinti acronimi che verranno utilizzati all'interno del testo:
  • ACK -> Segnale di acknowledge, ovvero il segnale generato dal dispositivo che riceve i dati;
  • ISR -> Interrupt Service Routine, ovvero una porzione del codice che viene eseguita non appena ci sia un interrupt;
  • NACK -> in qualche modo, il duale del primo. È una condizione in cui sono sorti dei problemi di interpretazione o di ricezione.

Il bus

Dell'I2C ci siamo già occupati in passato, in diverse occasioni; l'Inter-Integrated Circuit è un'interfaccia di comunicazione a due fili bidirezionale. SDA ed SCL, ovvero Serial Data e Serial Clock, che trasportano l'informazione tra dispositivi connessi tramite il bus. Tutti i sensori in questione verranno poi alimentati con la relativa tensione di funzionamento. Ciascun dispositivo viene riconosciuto ed identificato all'interno dell'intera rete di connessione grazie all'utilizzo di un indirizzo univoco specifico. Un dispositivo, che funge da master, si occupa di inizializzare il trasferimento dei dati generando il segnale di clock che sincronizza il trasferimento stesso. Al tempo della trasmissione, ovvero non appena comincia, ciascun dispositivo viene considerato come slave nel momento stesso in cui venga indirizzato.

Il formato in cui vengono trasferiti i dati, i diagrammi temporali delle due linee ed anche la definizione dei segnali di Start, Stop ed altri possono essere comunemente trovati all'interno di diversi manuali. Seguendo le richieste di alcuni di voi su alcuni casi particolari di funzionamento, specie in operazioni di boost, parleremo più specificatamente di questo tipo di applicazione nel mese di dicembre in un articolo dedicato proprio a questo bus. Restate sintonizzati con noi per i dettagli; arriveranno molto presto.
Nel frattempo, però, è importante ricordare alcuni concetti tra cui i formati dei messaggi sia per le operazioni di scrittura sia per quelle di lettura. Vediamoli, quindi, meglio, qui di seguito.
Il formato dei messaggi per la trasmissione dei dati verso un dispositivo slave è costituito di cinque sezioni differenti:
  • segnale di start;
  • indirizzo del dispositivo, seguito dal "write" bit;
  • trasmissione dell'indirizzo del registro all'interno del quale il dato verrà scritto;
  • trasmissione dei dati;
  • segnale di stop.
Il formato dei messaggi per la trasmissione dei dati dallo slave nel caso di formato combinato viene definito da nove distinti contributi:
  • segnale di start;
  • indirizzo dello slave seguito dal "read" bit;
  • trasmissione dell'indirizzo del registro da cui viene eletto il dato;
  • nuovo segnale di start;
  • trasmissione dell'indirizzo dello slave seguito dal "write" bit;
  • lettura dei primi dati dal data register;
  • trasmissione dei dati (multipla);
  • nessun segnale di acknowledge;
  • segnale di stop.

Ogni volta che il master ha ricevuto 1 byte, esso genera il segnale di acknowledge per segnalare allo slave che l'operazione è stata completata con successo. È bene sapere che le specifiche per la trasmissione su I2C permettono anche altri formati; il dispositivo secondario (di turno) può supportare formati di messaggi combinati sia per operazioni di lettura sia per operazioni di scrittura.

Non-Blocking

Vediamo di capire meglio, in questo paragrafo, che cosa significa avere a che fare con messaggi I2C multi-byte. Dal momento che lo scopo di una comunicazione non-blocking è quello di fare in modo che il processore non aspetti fino al trasferimento di ciascun byte ma consente l'esecuzione continua, dopo che il byte sarà stato trasferito ed il segnale ACK ricevuto, avverrà l'interrupt. In questo caso lo stato della macchina che guida le comunicazioni inizierà il trasferimento del nuovo byte del messaggio.
Parlando del driver della comunicazione, della struttura dei tipi di dati, dei flag e dei vari fault che possono avvenire, è necessario specificare che il messaggio di scrittura e lettura è costituito da diverse porzioni e, al fine di identificare in quale fase della comunicazione il messaggio si trova in un dato momento, la struttura di controllo contiene dei flag che servono allo scopo
typedef enum
{
I2C_TRM_STAGE_NONE = 0,
I2C_TRM_STAGE_WRITE_DATA,
I2C_TRM_STAGE_WRITE_DEV_ADDRESS_W,
I2C_TRM_STAGE_WRITE_DEV_ADDRESS_R,
I2C_TRM_STAGE_WRITE_REG_ADDRESS,
I2C_TRM_STAGE_READ_DUMMY_DATA,
I2C_TRM_STAGE_READ_DATA,
I2C_TRM_STAGE_NACK,
} tI2C_trm_stage;
l'enumerazione viene utilizzata, invece, per identificare il formato del messaggio, se di lettura oppure di scrittura
typedef enum
{
I2C_MODE_READ = 0,
I2C_MODE_WRITE,
} tI2C_mode;
e la trasmissione, infine, viene segnalata tramite l'utilizzo di flag opportuni, come vediamo qui di seguito
typedef enum
{
I2C_FLAG_NONE = 0,
I2C_FLAG_TRANSMISSION_PROGRESS,
} tI2C_flag;
Per controllare che tutto sia andato a buon fine vengono utilizzati dei check
typedef enum
{
I2C_NO_FAULT = 0,
I2C_BUS_BUSY,
I2C_TIMEOUT,
I2C_PERMANENT_BUS_FAULT,
} tI2C_fault;
Nel caso intervenga "busy" vuol dire che si è verificato un tentativo di invio di un nuovo messaggio sul bus mentre lo stesso è ancora occupato (naturalmente il nome non è casuale). Questo vuol dire, sostanzialmente, che il trasferimento del messaggio precedente non è completato e pertanto è stato rilevato uno start signal ma non il successivo di stop.
Questa attesa, o qualunque altra attesa, non può e non deve in alcun modo diventare infinita per cui ci si aspetta che venga implementato un qualche meccanismo che controlli il tempo di esecuzione e a questo scopo esiste il "timeout". Qual è l'intervallo giusto, allora, da attendere prima di considerare di aver aspettato troppo? Nel nostro caso, vengono considerati 150 microsecondi che, dal momento che si tratta di un protocollo abbastanza veloce, è accettabile.
Tra l'altro questo intervallo, di solito, supera abbondantemente i "rest times" dei dispositivi più performanti, per esempio gli accelerometri. Alcuni di essi, infatti, non richiedono più di 40 microsecondi di rest time prima di poter effettuare una nuova lettura.
Evidentemente, però, il timeout ha delle cause che potrebbero anche essere dovute alla presenza di rumore. In questo caso, a maggior ragione, è utile che la comunicazione si consideri interrotta dal momento che le misure, ovvero i segnali, potrebbero addirittura arrivare completamente sbagliate.
Se c'è un errore di timeout il software disabilita il modulo I2C ed invia un segnale convenzionale costituito da nove impulsi di clock seguiti da un Nack e successivamente da uno stop signal. È utile saperlo perché questo genere di segnali convenzionali possono essere controllati grazie all'utilizzo di un oscilloscopio digitale direttamente impiegato sul bus e quindi, per fare debug, può risultare particolarmente utile.
In questa eventualità, lo slave è costretto a rilasciare l'SDA in maniera tale che il modulo possa essere inizializzato nuovamente. Successivamente tenterà di inviare il messaggio un'altra volta e, se dovesse fallire questo nuovo tentativo, si passerà nello stato di "permanent fault", codificato come abbiamo visto. Da qui si esce soltanto con un reset del bus.

L'algoritmo

Siccome l'argomento di oggi è uno specifico algoritmo, va preso in considerazione l'inizio della trasmissione, quando la struttura di controllo deve essere inizializzata; essa comincia con una chiamata alle funzioni di lettura e scrittura che inviano il primo byte dei dati, come vediamo di seguito
Il flow chart che abbiamo appena visto riporta l'utilizzo del flag di controllo "transmission progress" che viene impostato alla fine delle operazioni di lettura o di scrittura e quindi delle relative funzioni. L'applicazione principale non inizia il trasferimento di un nuovo messaggio soltanto se il suo valore non è tornato "none".
Quando il primo byte del messaggio viene inviato con successo, l'applicazione può continuare con la sua esecuzione, che naturalmente può prevedere anche altri task. Durante il tentativo di invio del primo byte, invece, il bus è nello stato "occupato" e pertanto se si cerca di accedere al bus il risultato sarà un "fault". Naturalmente questo non vuol dire che ci sia una condizione prettamente associata ad un fault e quindi ad un problema ma anche, per l'appunto, che il trasferimento del byte è ancora in corso. Qui interviene proprio quella condizione di timeout di cui avevamo parlato in precedenza: viene impostato, infatti, il valore iniziale del contatore che deve essere impostato in maniera tale da tenere conto anche della dimensione del più grande dei messaggi che è possibile inviare (come sempre il dimensionamento va fatto nel caso "peggiore") ma soprattutto tenendo conto dell'appropriato intervallo di tempo che il microcontrollore deve aspettare.
Se il trasferimento del byte è completato, ed avviene la conferma di trasferimento, allora il modulo I2C su microprocessore genera una richiesta di interrupt. In alcuni casi, quando non si verifica la conferma, avviene il caso cui avevamo accennato in precedenza ovvero il Nack signal.
Qui i casi sono diversi perché si va dall'assenza fisica del dispositivo ricevitore per l'indirizzo specificato al fatto che il ricevitore sia impossibilitato a ricevere o trasmettere perché impegnato ad eseguire funzioni real-time e pertanto non è pronto per le comunicazioni con il master. Altre possibilità riguardano il trasferimento per cui i dati non vengono ben interpretati oppure non c'è spazio nella memoria del ricevitore per accoglierne di ulteriori.
Una quinta possibilità riguarda il fatto che un master che riceve deve segnalare la fine della trasmissione allo slave. Questa, nello specifico, non rappresenta una condizione di fallimento della trasmissione che viene utilizzata alla fine del messaggio di lettura. La generazione del segnale Nack dallo slave non viene rilevata sul bus e questo potrebbe dare origine a problemi di timeout.
 
Nella routine "ISR Callback" accade che l'algoritmo continua ad inviare i byte del messaggio e si comporta in maniera differente tra ricezione e lettura; in particolare la struttura della ricezione risulta essere più complicata e questo dipende dal fatto che l'ISR inizia a valutare lo stato corrente del messaggio utilizzando switch cases che controllano singolarmente la componente "indirizzo" del registro piuttosto che quello del dispositivo per poi passare alla lettura dei dati. Ciascuno di questi casi segue poi un percorso diverso in cui, se il dato c'è ed è corretto, si può andare avanti nella realizzazione della lettura del dato. Alla fine dell'ultimo caso, quando cioè tutti i controlli sono andati a buon fine, viene impostato il flag transmission per segnalare all'applicazione principale che il trasferimento dell'intero messaggio è terminato e il bus è pronto per un nuovo trasferimento.
 
Vediamo, ora, alcune linee di codice utili per fare l'inizializzazione e la comunicazione con alcuni sensori di temperatura fittizi in modo tale da verificare che la gestione degli stessi sia fatta esattamente come l'abbiamo proposta fino a questo momento.
#define SENSOR_I2C_ADDRESS 0x5A
#define IICWRITE(iicaddress) ((iicaddress<<1) & 0xFE)
#define IICREAD(iicaddress) ((iicaddress<<1) | 0x01)
#define SENSOR_I2C_ADDRESS_W IICWRITE( SENSOR_I2C_ADDRESS ) //0xB4
#define SENSOR_I2C_ADDRESS_R IICREAD( SENSOR_I2C_ADDRESS ) //0xB5

typedef enum
{
SENSOR_NO_FAULT = 0,
SENSOR_INIT_FAILED,
SENSOR_READ_FAILED,
} tSensor_fault;

typedef enum
{
SENSOR_NO_TRANSFER = 0,
SENSOR_TRANSFER_PROGRESS,
SENSOR_TRANSFER_FINISHED,
} tSensor_data_transfer;

typedef struct
{
tSensor_fault eSensor_fault;
tSensor_data_transfer eSensor_data_transfer;
unsigned char measured_data;
} tSensor_com_ctr;

tSensor_com_ctr sTMPR_com_ctr;

unsigned char iic_data[10];
Leggendo il codice, provate a pensare alla struttura che vi avevamo indicato.
Una volta dichiarati i sensori e definiti per tipo ed indirizzo, sono, naturalmente, necessarie delle funzioni che permettano la lettura ed anche, nel caso in cui servano, la rilevazione dei problemi.
void TemperatureSensingInit (void)
{
sI2C_com_ctr.device_address_w = SENSOR_I2C_ADDRESS_W;
sI2C_com_ctr.device_address_r = SENSOR_I2C_ADDRESS_R;

sI2C_com_ctr.eI2C_flag = I2C_FLAG_NONE;
sI2C_com_ctr.eI2C_fault = I2C_NO_FAULT;
if (Sensor_Init() == SENSOR_INIT_FAILED)
{
sTMPR_com_ctr.eSensor_fault = SENSOR_INIT_FAILED;
return;
}
ENABLE_TIMER_INTERRUPT;
}
tSensor_fault Sensor_Init (void)
Abbiamo parlato di timer e, infatti, ed eccone una realizzazione
void Timer1_Isr(void)
{
// initialization of I2C control structure
sI2C_com_ctr.register_address = 0x04;
sI2C_com_ctr.data_size = 1;
sI2C_com_ctr.eI2C_flag = I2C_FLAG_NONE;
sI2C_com_ctr.eI2C_trm_stage = I2C_TRM_STAGE_WRITE_DEV_ADDRESS_W;
if (I2C_read_data(&sI2C_com_ctr, iic_data) != I2C_NO_FAULT)
sTMPR_com_ctr.eSensor_fault SENSOR_READ_FAILED;
else
{
sTMPR_com_ctr.eSensor_data_transfer = SENSOR_TRANSFER_PROGRESS;
sTMPR_com_ctr.eSensor_fault = SENSOR_NO_FAULT; }
CLEAR_TIMER_INTERRUPT_FLAG;
}
Questo genere di applicazione prevede un algoritmo che può essere utilizzato per un generico microcontrollore che comunichi con un generico sensore perché, naturalmente, la qualità ed il tipo dei dati non è influente. La porzione del codice relativo ai messaggi in scrittura può essere utilizzata per la configurazione del sensore all'avvio dell'applicazione mentre tutto ciò che riguarda la lettura del software driver può essere utilizzato per effettuare le richieste e per ricevere i dati misurati.
 
Da i2c.h:
typedef enum
{
I2C_TRM_STAGE_NONE = 0,
I2C_TRM_STAGE_WRITE_DATA,
I2C_TRM_STAGE_WRITE_DEV_ADDRESS_W,
I2C_TRM_STAGE_WRITE_DEV_ADDRESS_R,
I2C_TRM_STAGE_WRITE_REG_ADDRESS,
I2C_TRM_STAGE_READ_DUMMY_DATA,
I2C_TRM_STAGE_READ_DATA,
I2C_TRM_STAGE_NAK,
} tI2C_trm_stage; // transmission stages
typedef enum
{
I2C_MODE_READ = 0,
I2C_MODE_WRITE,
}tI2C_mode;
typedef enum
{
I2C_FLAG_NONE = 0,
I2C_FLAG_TRANSMISSION_PROGRESS,
} tI2C_flag;
typedef enum
{
I2C_NO_FAULT = 0,
I2C_BUS_BUSY,
I2C_TIMEOUT,
I2C_PERMANENT_BUS_FAULT,
}tI2C_fault;
typedef struct
{
tI2C_trm_stage eI2C_trm_stage;
tI2C_flag eI2C_flag;
tI2C_mode eI2C_mode;
tI2C_fault eI2C_fault;
unsigned char device_address_w;
unsigned char device_address_r;
unsigned char register_address;
unsigned char data_size;
unsigned char data_index;
} tI2C_com_ctr;

#if defined (TOWER)
#define I2C_MASTER_SDA_PIN_1 GPIOF_DR |= GPIOF_DR_D_3
#define I2C_MASTER_SDA_PIN_0 GPIOF_DR &= ~GPIOF_DR_D_3
#define I2C_MASTER_SCL_PIN_1 GPIOF_DR |= GPIOF_DR_D_2
#define I2C_MASTER_SCL_PIN_0 GPIOF_DR &= ~GPIOF_DR_D_2
#define I2C_MASTER_SDA_PIN_AS_IN GPIOF_DDR &= ~GPIOF_DDR_DD_3
#define I2C_MASTER_SDA_PIN_AS_OUT GPIOF_DDR |= GPIOF_DDR_DD_3
#define I2C_MASTER_SCL_PIN_AS_IN GPIOF_DDR &= ~GPIOF_DDR_DD_2
#define I2C_MASTER_SCL_PIN_AS_OUT GPIOF_DDR |= GPIOF_DDR_DD_2
#define I2C_MASTER_SDA_PIN_AS_GPIO GPIOF_PER &= ~GPIOF_PER_PE_2
#define I2C_MASTER_SCL_PIN_AS_GPIO GPIOF_PER &= ~GPIOF_PER_PE_3
#define I2C_MASTER_SDA_PIN_AS_I2C GPIOF_PER |= GPIOF_PER_PE_2
#define I2C_MASTER_SCL_PIN_AS_I2C GPIOF_PER |= GPIOF_PER_PE_3
#endif

#define I2C_START_SIGNAL (I2C_C1 |= I2C_C1_MST)
#define I2C_STOP_SIGNAL (I2C_C1 &= ~I2C_C1_MST)
#define I2C_REPEAT_START_SIGNAL (I2C_C1 |= I2C_C1_RSTA)
#define I2C_WRITE_BYTE(data) (I2C_D = data)
#define I2C_READ_BYTE (unsigned char)I2C_D
#define I2C_GET_IRQ_FLAG (I2C_S & I2C_S_IICIF)
#define I2C_CLEAR_IRQ_FLAG (I2C_S |= I2C_S_IICIF)
#define I2C_SET_RX_MODE (I2C_C1 &= ~I2C_C1_TX)
#define I2C_SET_TX_MODE (I2C_C1 |= I2C_C1_TX)
#define I2C_SET_NACK_MODE (I2C_C1 |= I2C_C1_TXAK)
#define I2C_CLEAR_NACK_MODE (I2C_C1 &= ~I2C_C1_TXAK)
extern void I2C_Isr(void);
extern tI2C_fault I2C_write_data(tI2C_com_ctr *psI2C_tr_ctrl, unsigned char *data);
extern tI2C_fault I2C_read_data(tI2C_com_ctr *psI2C_tr_ctrl, unsigned char *data);
void I2C_Init();
void I2C_DeInit();
tI2C_fault I2C_Restore();
void I2C_delay(void);
tI2C_fault I2C_isr_Callback (tI2C_com_ctr *psI2C_tr_ctrl,unsigned char *data);
#endif
Da i2c.c:
#include "i2c.h"
unsigned char iic_data[0x80];
tI2C_com_ctr sI2C_com_ctr; 

void I2C_Init(void)
{
I2C_F = 0x27; // clock divider 56, I2C frequency: 80 kHz
I2C_C1 = I2C_C1_IICIE | I2C_C1_IICEN;
// enable interrupt in INTC module
INTC_IPR6 |= INTC_IPR6_IIC0_0 | INTC_IPR6_IIC0_1;
}

void I2C_DeInit(void)
{
I2C_C1 = 0;
}

void I2C_delay(void) // delay of 200 us @50MHz CPU clock (creates period of 5 kHz)
{
unsigned int cnt;
I2C Non-blocking Communication, Rev 0, 10/2013
18 Freescale Semiconductor, Inc.
for ( cnt = 0;cnt < 10000; cnt++)
{ asm(nop); };
}

tI2C_fault I2C_Restore(void)
{
unsigned char tmp = 0;
I2C_STOP_SIGNAL;
I2C_DeInit();
I2C_MASTER_SDA_PIN_AS_GPIO;
I2C_MASTER_SDA_PIN_GPIO_HIGH_DRIVE;
I2C_MASTER_SCL_PIN_AS_GPIO;
I2C_MASTER_SDA_PIN_AS_OUT;
I2C_MASTER_SDA_PIN_0;
I2C_MASTER_SCL_PIN_AS_OUT;
for(tmp = 0; tmp <9; tmp ++) // nine clock for data
{
I2C_MASTER_SCL_PIN_0;
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
}
I2C_MASTER_SCL_PIN_0;
I2C_MASTER_SDA_PIN_AS_OUT; //SDA pin set to output
I2C_MASTER_SDA_PIN_1; //negative acknowledge
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
I2C_MASTER_SCL_PIN_0;
I2C_delay();
I2C_MASTER_SDA_PIN_0; //stop
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
I2C_MASTER_SDA_PIN_AS_IN;
tmp = 0;
if (!((GPIOC_DR>>14) & 1))
{
while (!((GPIOC_DR>>14) & 1) && (tmp < 30))
{
I2C_MASTER_SCL_PIN_0;
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
tmp++;
};
if (tmp == 30)
return I2C_PERMANENT_BUS_FAULT; // giving up, permanent
I2C Non-blocking Communication, Rev 0, 10/2013
Freescale Semiconductor, Inc. 19
// error, reset required
I2C_MASTER_SCL_PIN_0;
I2C_MASTER_SDA_PIN_AS_OUT; //SDA pin set to output
I2C_MASTER_SDA_PIN_1; //negative acknowledge
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
I2C_MASTER_SCL_PIN_0;
I2C_delay();
I2C_MASTER_SDA_PIN_0; //stop
I2C_delay();
I2C_MASTER_SCL_PIN_1;
I2C_delay();
}
I2C_MASTER_SDA_PIN_AS_I2C;
I2C_MASTER_SCL_PIN_AS_I2C;
I2C_Init();
return I2C_NO_FAULT;
}

Conclusioni

L'argomento di oggi era, in effetti, davvero specifico e specialistico. Abbiamo cercato di scendere nel dettaglio e di andare a vedere gli aspetti fondamentali sia della programmazione sia del funzionamento del bus I2C per comprenderne al meglio problematiche ed aspetti salienti. Quello che abbiamo trovato è un metodo universale e versatile grazie al quale potremo rendere sempre più utile un bus di comunicazione il cui utilizzo è ormai giornaliero. La prossima volta, nella prossima puntata, ci occuperemo di operazioni matematiche e di applicazioni di misura mentre nell'ultima puntata di questo corso torneremo a mettere le mani sulla scheda, e sulla programmazione più specificatamente, per completare alcune nozioni utili a restituirvi un panorama completo di tutto ciò che dovete sapere. Come sempre, lo ricordiamo, i commenti a questi articoli sono il mezzo attraverso cui potete esprimere non soltanto apprezzamenti o critiche rispetto a quello che state leggendo ma anche fare proposte per quello che volete che venga trattato. Se avete argomenti, suggerimenti o qualunque altra idea da proporre, siamo qui per questo. Alla prossima.

 

Credits: l'immagine principale è una libera rielaborazione di questa.

 

 

                    

Per ulteriori informazioni potete scrivere a questa email: [email protected]

Scarica subito una copia gratis

4 Commenti

  1. Avatar photo Giorgio B. 4 Dicembre 2013
  2. Avatar photo Marven 4 Dicembre 2013
  3. Avatar photo Piero Boccadoro 4 Dicembre 2013
  4. Avatar photo Mario Toma 13 Aprile 2016

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend