Il protocollo seriale I2C con PIC 16F84

Nota del 2013: ho scritto quest'articolo nel 2004 e da allora alcune cose sono un po' cambiate. Prima di tutto, Philips non vende più semiconduttori e la divisione che si occupava di queste cose ora si chiama NXP. Poi, il PIC16F84 era già obsoleto allora, oggi è diventato poco più di una curiosità. Ciononostante certe idee non sono cambiate e la lettura può esser utile ancora oggi.


Nota del 2016: Sembrerebbe che questa paginetta sia ancora letta da qualcuno! Ringrazio Tom Lillevig per avermi segnalato un errore nella funzione "i2cwaitack".


Nota del 2020: Ebbene sì, questa pagina è ancora letta oggi! Sentiti ringraziamenti a István Sándor per avermi indicato una piccola correzione di stile che era opportuna da fare nel codice.



Introduzione
Cosa si può fare
A livello hardware
A livello software
Il protocollo
Se qualcosa va storto...
In conclusione

Introduzione

Nell'elettronica attuale, i circuiti digitali tendono a diventare progressivamente più complessi e divengono disponibili circuiti integrati in grado di svolgere i compiti più vari. D'altro canto, la disponibilità di microcontrollori di basso costo alla portata dei comuni mortali (i PIC, la serie ST6 e 7, il Motorola 68HC11, in una lista non esaustiva) consente di ideare circuiti relativamente piccoli in grado di gestire funzioni abbastanza articolate.
Uno dei fattori che più incidono nel costo di un microcontrollore o una logica programmabile (Altera, Lattice, Maxim, etc...) è il numero complessivo di piedini di ingresso e di uscita. In altre parole, a parità di prestazioni e diffusione sul mercato, un microcontrollore in grado di gestire 24 ingressi/uscite costa generalmente di più di un altro che ne ha solamente 13.
Il bus I2C è un sistema messo a punto dalla Philips nella metà degli anni ottanta che consente di pilotare una famiglia molto vasta di circuiti integrati utilizzando solamente due linee I/O più la massa.
Si tratta dunque di un economico protocollo di comunicazione seriale a bassa o media velocità (100kbit/s, 400kbit/s o più recentemente 3,4Mbit/s) il quale consente tuttavia di indirizzare un numero molto grande di dispositivi sullo stesso bus, grazie ad un codice d'indirizzo proprio a ciascun dispositivo.
In questo documento, sono presentate delle routine per PIC16F84 (penso utilizzabili con poche modifiche anche su altri microcontrollori della medesima famiglia non dotati di USART hardware) capaci di alleggerire il compito del programmatore per la gestione a basso livello dello standard.

Cosa si può fare

Molti dei dispositivi che adottano il bus I2C sono costruiti dalla Philips, ma anche aziende indipendenti adottano quello che è ormai diventato uno standard molto diffuso. Fra i modelli forniti dalla Philips, troviamo diversi orologi/calendari (PCF8573, PCF8583), memorie RAM statiche (PCF8570), memorie EEPROM (PCF8582, 24C01), convertitori analogico/digitali (PCF8591) e molto altro.
Con un integrato di tipo PCF8574, è possibile aggiungere 8 porte bidirezionali in un sol colpo al microcontrollore. Se si tiene conto che di integrati di questo tipo se ne possono utilizzare fino ad 8, otteniamo un totale di 64 piedini di ingresso/uscita controllati con solo due linee, ovviamente ad una velocità non elevatissima. Se avete un plastico ferroviario per esempio, potete pensare di controllare localmente il sistema di scambi o l'illuminazione delle case facendo scorrere solo due fili di controllo.
Con il  PCF8575, le porte a disposizione diventano 16...
Il sito della Philips contiene una vasta sezione dedicata allo standard I2C che consiglio di visitare per avere un'idea delle possibilità offerte da questa soluzione semplice ed efficace.
In questo articolo, vedremo come si può pilotare in maniera semplice il bus I2C utilizzando un PIC tipo 16F84. La velocità di comunicazione è quella più bassa (100kbit/s) e ci troveremo in una situazione semplice in cui il PIC controlla da solo il clock della trasmissione. Non si vuole fare una rivista completa delle numerose caratteristiche offerte dallo standard (il meglio da fare è quello di scaricarsi il il documento I2C bus specification sul sito della Philips).

A livello hardware

Il bus I2C è composto, come si è detto, da due sole linee bidirezionali più la massa. La prima linea, denominata SCK è il clock della trasmissione e la seconda, denominata SDA è la linea su cui transitano i dati al ritmo scandito da SCK. Il protocollo in questo modo è sincrono (a differenza, per esempio del protocollo RS232 che è asincrono e più complesso da gestire).
Data la possibilità di avere più dispositivi presenti sulle linee, normalmente esse sono gestite con una logica a drain aperto e richiedono una resistenza di pull-up collegata con il positivo di alimentazione, come mostrato in Fig.1. Questo vuol dire che ogni dispositivo può imporre un livello logico 0 sulla linea cortocircuitandola con la massa (presumibilmente con un MOSFET, da cui il nome drain aperto), oppure un livello logico 1 semplicemente senza fare nulla. In questo modo, un dispositivo il quale voglia rimanere assolutamente inerte sulla linea senza perturbare altre comunicazioni in corso non deve fare altro che lasciare scollegate da massa le due linee.

I2C lines

Fig. 1: connessione di diversi dispositivi con un OR cablato, dal datasheet Philips.

Ma noi immagineremo di trovarci in una situazione semplice in cui vi sia un solo trasmettitore ed un solo ricevitore sul bus I2C.
Si può distinguere tra dispositivo master e dispositivo slave a seconda di chi genera il clock, in altre parole a seconda di chi impone la cadenza con cui i dati vengono inviati sulla linea, sia in un senso che nell'altro. In questo modo, il dispositivo master potrà essere sia un trasmettitore o un ricevitore, in modo complementare rispetto al dispositivo slave. Come regola generale, ad un istante prefissato, sul bus I2C vi può essere un solo master ed un numero anche rilevante di slave.

Ritorniamo a noi. Nel nostro caso abbiamo un solo dispositivo da gestire con un microcontrollore. Nella stragrande maggioranza dei casi, il microcontrollore funge da master ed il dispositivo da gestire da slave. In altre parole, il clock SCK sarà sempre gestito dal microcontrollore mentre la linea SDA è generalmente bidirezionale.

Ecco le definizioni che useremo nel seguito per migliorare la leggibilità:

; ****************************************************************
; I2C routines developed by Davide Bucci, version 1.0, August 2004
; ****************************************************************

; Control lines of the I2C interface
SCL     equ     00
SDA     equ     01
I2CPORT equ     PORTB
I2CTRIS equ     TRISB

; Variables, substitute adresses of free RAM bytes
TMP     equ     0C      ; Dummy variable
COM     equ     10      ; I2C Communication Register

In questo caso, le linee SCL e SDA sono da collegare con i bit 0 e 1 della porta B, con una resistenza di pull-up verso il positivo di alimentazione (per il valore c'è ampia scelta; valori più elevati penalizzano l'immunità ai disturbi ma riducono il consumo. Una scelta di valori intorno ai 10 kΩ è valida nella maggior parte dei casi).

A livello software

A livello software, le cose si complicano in quanto bisogna gestire il livello logico delle linee in modo da pilotare il dispositivo desiderato sul bus I2C. Durante la trasmissione, i bit sono inviati in maniera sequenziale incominciando da quello più significativo e la linea SDA può essere cambiata di stato solamente quando il segnale SCK è alto. Vi sono due importanti eccezioni a questa regola. Nei periodi di inattività, entrambe le linee sono mantenute a livello logico alto tramite le resistenze di pull-up; il microcontrollore agente come master segnala l'inizio di una trasmissione proprio abbassando la linea SDA mentre SCK è a livello 1. Ecco il codice necessario:

i2cstart                        ; Send a start on the I2C bus
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA    ; SDA as output
        bcf     I2CTRIS, SCL    ; SCL as output
        BANKSEL I2CPORT

        bsf     I2CPORT, SDA      ; The start condition on the I2C bus
        bsf     I2CPORT, SCL      ; An high to low transition when SCL is high
        call    shortdelay
        bcf     I2CPORT, SDA
        call    shortdelay
        bcf     I2CPORT, SCL
        call    shortdelay      ; Leave SDA and SCL low
        return

Nel codice, si nota una chiamata ad una funzione di nome shortdelay, la quale si occupa di gestire correttamente la temporizzazione in modo da rispettare le specifiche per il bus. Nel caso di un quarzo a 4MHz, una versione adeguata (anche un po' più lenta del necessario) è la seguente:

shortdelay                      ; A short delay ;-)
        nop
        nop
        nop
        return

The start condition and the aknowledgement on the I2C bus

Fig. 2: la condizione di start, dai datasheet Philips.

Una condizione simmetrica è lo stop, che segnala la fine della trasmissione: una transizione da livello logico basso a livello logico alto sulla linea SDA mentre SCK è alta.

i2cstop                          ; Send a stop on the I2C bus
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA     ; SDA as output
        BANKSEL I2CPORT
        bcf    I2CPORT, SCL
        bcf     I2CPORT, SDA     ; The stop condition on the bus I2C
        call    shortdelay
        bsf     I2CPORT, SCL     ; A low to high transition when SCL is high
        call    shortdelay
        bsf     I2CPORT, SDA
        call    shortdelay       ; SCL and SDA lines are left high
        return

A questo punto, si entra nel vivo del discorso ed avviene la trasmissione vera e propria che avviene byte per byte partendo da quello maggiormente significativo (MSB). Vengono inviati otto bit dal trasmettitore (che qui supporremo essere il microcontrollore), dopodiché la linea verra lasciata alta di modo che il ricevitore possa dire se tutto è andato bene o no. Questo segnale si chiama Acknowledgment e permette di indicare al trasmettitore la buona riuscita della trasmissione. In questo caso, la linea SDA è lasciata in ricezione e il microcontrollore deve verificare che il ricevitore la ponga a livello basso. Il codice è semplice da usare e la routine va richiamata con nel registro w il byte da inviare:

i2csend                         ; Send a byte over the I2C interface,
        movwf   COM             ; return 0x00 if ACK
        movlw   0x08
        movwf   TMP             ; TMP is used as a counter
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA    ; SDA as output
        BANKSEL I2CPORT
icloops
        bcf     I2CPORT, SCL    ; Clock low: change of SDA allowed
        rlf     COM,f
        bcf     I2CPORT, SDA
        btfsc   STATUS, C       ; Test the carry bit
        bsf     I2CPORT, SDA
        call    shortdelay
        bsf     I2CPORT, SCL    ; Clock high
        call    shortdelay
        decfsz  TMP,f
        goto    icloops         ; i2cwaitack follows directly
i2cwaitack
        bsf     I2CPORT, SDA
        BANKSEL I2CTRIS
        bsf     I2CTRIS, SDA    ; SDA as input
        BANKSEL I2CPORT
        bcf     I2CPORT, SCL    ; Clock low
        call    shortdelay
        bsf     I2CPORT, SCL    ; Clock high
        call    shortdelay
        movlw   0x00            ; Ox00 in w means ack
        btfsc   I2CPORT, SDA    ; SDA low means ack
        movlw   0xFF            ; 0xFF in w means no ack
        BANKSEL I2CPORT         ; Clock is left low
        bcf     I2CPORT, SCL
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA    ; SDA as output
        BANKSEL I2CPORT
        call    shortdelay
        return

; Questa versione di i2cwaitack pu  essere un po' sbrigativa per certi
; dispositivi i2c come certe EEPROM.
; Pu  essere conveniente introdurre un meccanismo di timeout ed incrementare
; l'attesa per l'acknowledge fino ad un tempo limite prefissato.
; Si faccia riferimento al datasheet del dispositivo per maggiori dettagli.

All'interno del registro w, una volta ritornati alla funzione chiamante sarà contenuto il valore 0x00 se tutto è andato bene ed abbiamo ricevuto il segnale di risposta dal dispositivo, oppure 0xFF se tale segnale non è stato inviato.
Abbiamo visto come inviare un byte, adesso vediamo come riceverlo:

i2creceive
        clrf    COM            ; Receive a byte over the I2C interface
        movlw   0x08
        movwf   TMP            ; TMP is used as a counter
        BANKSEL I2CTRIS
        bsf     I2CTRIS, SDA   ; SDA as input
        BANKSEL I2CPORT
icloopr
        bcf     I2CPORT, SCL   ; Clock low: change of SDA allowed
        call    shortdelay
        bsf     I2CPORT, SCL   ; Clock high
        call    shortdelay
        bcf     STATUS, C      ; Clear the carry
        rlf     COM,f
        btfsc   I2CPORT, SDA   ; Test the bit being received
        bsf     COM,0          ; Stock the bit read in COM and rotate
        decfsz  TMP,
        goto    icloopr
        movf    COM,w
        bcf     I2CPORT, SCL   ; Clock is left low
        call    shortdelay
        return

Una volta ricevuto il byte dalle linee I2C, l'esecuzione passa al programma chiamante con li byte ricevuto nel registro w.
A questo punto, dato che chi riceve è il microcontrollore (come master receiver, dato che genera il clock), si tratta se scegliere se inviare il segnale di Acknowledgement o no:

i2csendack
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA   ; SDA as output
        BANKSEL I2CPORT
        bcf     I2CPORT, SCL   ; Clock low: change of SDA allowed
        call    shortdelay
        bcf     I2CPORT, SDA   ; SDA low means ack
        call    shortdelay
        bsf     I2CPORT, SCL   ; Clock high
        call    shortdelay
        bcf     I2CPORT, SCL   ; Clock is left low
        return



i2cnoack
        BANKSEL I2CTRIS
        bcf     I2CTRIS, SDA   ; SDA as output
        BANKSEL I2CPORT
        bcf     I2CPORT, SCL   ; Clock low: change of SDA allowed
        call    shortdelay
        bsf     I2CPORT, SDA   ; SDA high means no ack
        call    shortdelay
        bsf     I2CPORT, SCL   ; Clock high
        call    shortdelay
        bcf     I2CPORT, SCL
        return                 ; Clock is left low

Normalmente, il ricevitore segnala Ack se ha correttamente ricevuto il byte per passare al successivo, oppure NoAck se si sono verificati problemi o per segnalare la fine della trasmissione.

Il protocollo

Abbiamo qui visto quali sono i mattoni di base che vengono utilizzati nella comunicazione. Si tratta adesso di analizzare come essi vengano utilizzati in sequenza in modo da mettere in piedi la comunicazione vera e propria. Attenzione! In questo caso, molte cose dipendono dal dispositivo che è stato scelto e la cosa migliore da fare è far riferimento alla documentazione ufficiale ed al datasheet. Ad ogni modo, certe caratteristiche sono più o meno costanti. Si parte con uno start che segnala l'inizio della trasmissione, il primo byte è sempre l'indirizzo su 7 bit del dispositivo slave da indirizzare sulla linea. Per il PCF8573, avremo qualcosa di simile alla quanto mostrato in Fig. 3.

The address byte on the I2C bus

Fig. 3: il primo byte di comunicazione, dai datasheet Philips.

Questi 7 bit sono composti da due parti. La prima è fissa e determinata dal dispositivo che si sta utilizzando. La seconda, composta da due, tre o quattro bit solitamente, è decisa dal livello logico di altrettanti piedini appositamente predisposti dall'integrato. Questo vuol dire che è possibile indirizzare sulla medesima linea più dispositivi uguali aventi indirizzi diversi.
Un esempio è rappresentato dal caso dell'orologio/datario Philips PCF8573, la cui parte fissa è formata dai bit 1101; ad essi segue un bit sempre a zero e poi due bit il cui livello logico dipende da come sono collegati i due piedini A0 ed A1 sull'integrato. In questo modo, senza complicazioni aggiuntive, è possibile utilizzare separatamente fino a 4 orologi PCF8573 sulla medesima linea.
L'ultimo bit è quello di direzione. Nel caso in cui si desideri trasmettere dei dati (il microcontrollore funziona quindi da master transmitter generando il clock), lo si deve lasciare a 1. Se invece si vogliono ricevere dei dati (come master receiver, quindi occupandosi sempre del clock), va messo a 0.
La condizione di start può esser ripetuta all'interno della trasmissione per più volte, solitamente per separare parti logicamente diverse della comunicazione, come il passaggio da lettura a scrittura o viceversa. In questo caso (il codice è identico), basta richiamare la routine i2cstart.

Se qualcosa va storto...

Anche dopo aver studiato con attenzione i datasheet, è molto difficile che un programma funzioni perfettamente al primo colpo... Per effettuare un controllo diagnostico sul bus I2C, esistono dei tester che si collegano a massa ed alle due linee SCK e SDA e mostrano su uno schermo cosa viene inviato sulle linee.
Una soluzione più economica che fa uso di un oscilloscopio a doppia traccia è quella di osservare sui due canali le linee SCK e SDA utilizzando una terza linea del microcontrollore per controllare il trigger dell'oscilloscopio (che quindi dovrà essere impostato su esterno). Il programma dovrà essere modificato per ripetere a loop la sequenza che pone problemi, di modo da poterla disegnare in maniera continua sullo schermo dell'oscilloscopio.
Una soluzione alternativa per chi non avesse a disposizione un oscilloscopio è quella di utilizzare due sonde logiche o, in mancanza di esse, due semplici LED pilotati da un transistor a mo' di buffer che segnalino lo stato logico sulle linee SCK e SDA. Dato che lo standard I2C non pone limiti inferiori alla velocità di trasmissione, si può rallentare di qualche ordine di grandezza la frequenza della linea SCK semplicemente modificando la routine shortdelay di modo che la sua chiamata duri circa un secondo. In questo modo, armati di penna e molta pazienza, è possibile seguire bit per bit quello che succede sulle linee ed individuare eventuali errori.

In conclusione

In questo intervento, ho voluto fare un'introduzione ad uno standard che è molto utilizzato in tantissime applicazioni. Probabilmente, il vostro videoregistratore, il vostro televisore ed il vostro calcolatore contengono fra gli altri degli integrati che adottano tale sistema di comunicazione. La mia esposizione non ha alcuna pretesa di essere completa e neppure le routine presentate sono a prova di bomba (per ora le ho usate su un PCF8573); esse sono presentate così come sono, senza nessuna garanzia di buon funzionamento, sotto licenza GNU versione 2. Vi invito a provarle ed a farmi avere le vostre impressioni in merito, di modo da migliorarle se risulta opportuno.

Log:

October 30, 2020 - Corrected a minor discrepancy (reference to PORTB instead of I2CPORT in i2cstart).

January 17, 2016 - Corrected the end of i2cwaitack and some typos in the Italian version of the page.

January 10, 2016 - Added Tom Lillevig's correction in the code.

May 26, 2013 - Review and English translation.

August 2004 - First version of the page.

License

 License:
--------

Copyright (C) 2004-2020 Davide Bucci davbucci at tiscali dot it

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.