Logiciels > Programmation PIC > Bases > MP > Lecture / écriture sur bus I2C > 24C02 à 24C16

Dernière mise à jour : 24/07/2011

Présentation

Les exemples décrits ici montrent comment écrire et lire des données dans une mémoire EEPROM externe de type 24C02 (256 octets), 24C04 (512 octets) ou 24C16 (2048 octets) à travers une liaison I2C et avec un microcontrôleur de type 18F2420. 

Electronique mise en oeuvre

Les exemples de code logiciel qui suivent montrent comment lire ou écrire une valeur tenant dans un octet (valeur comprise entre 0 et 255), depuis ou vers la mémoire 24C02, 24C04 ou 24C16 grâce à une liaison bifilaire I2C. Trois schémas quasiment identiques sont proposés :

Schéma avec mémoire 24C02 (256 octets)
La mémoire 24C02 dispose de 3 broches d'adressage physique qui permettent de câbler plusieurs composants sur le même bus I2C. Ici nous n'en tenons pas compte et l'adresse physique sera fixée à $00.

pic_tuto_base_i2c_24c02_001a

Schéma avec mémoire 24C04 (512 octets)
Il est possible de connecter plusieurs mémoires 24C04 sur un même bus I2C et de les différencier par leur adresse physique, qui est définie par l'état logique donné aux broches A1 et A2 du composant. Ici il n'est fait usage que d'une seule mémoire sur le bus I2C et l'adresse physique est spécifiée à $00 (broches A1 et A2 à la masse).

pic_tuto_base_i2c_24c04_001a

Schéma avec mémoire 24C16 (2048 octets)
La mémoire 24C16 utilisée ici ne dispose pas de broches d'adressage physique permettant d'en mettre plusieurs en parallèle sur un même bus I2C. L'adresse physique de base est donc de fait $00. Si la mémoire que vous utilisez possède des broches d'adresse A0 à A2 (24C164 par exemple), elles devront être mises à la masse pour rester avec une adresse de base de valeur $00 et permettre un fonctionnement correct avec le code logiciel de test proposé plus loin.

pic_tuto_base_i2c_24c16_001a

Tronc commun aux différents schémas
Rien de particulier à ajouter à ces schémas presque trop simples pour être vrais, si ce n'est les deux résistances de pullup R1 et R2 qui maintiennent les lignes SCL et SDA à l'état logique haut quand les composants actifs se reposent. Le principe de fonctionnement de ces circuits de test est simple et varie un petit poil selon le type de mémoire utilisé.

Lecture et écriture de données avec la 24C02

Le code exemple qui suit comporte deux routines :
- routine d'écriture vers la mémoire 24C02;
- routine de lecture depuis la mémoire 24C02.

Logiciel
Le code ci-dessous correspond au programme complet.

program electronique_pic_tuto_base_i2c_24c02_001a;

const
c24C02_W = $A2;
c24C02_R = $A3;

var
Values: byte;

procedure Init_Main;
begin
CMCON := $07; // turn off comparators
ADCON1 := ADCON1 or $0F; // turn off analog inputs
INTCON2.NOT_RBPU := 0;
TRISA := $00;
TRISB := $FF;
PORTA := $00;
PORTB := $00;
I2C1_Init(100000); // initialisation comm I2C
end;

function EEProm24C02_Read(iAdd: byte): byte;
begin
I2C1_Start; // signal START
I2C1_Wr(c24C02_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Repeated_Start; // issue I2C signal repeated start
I2C1_Wr(c24C02_R); // send byte (device address + R)
Result := I2C1_Rd(0); // lecture valeur à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

procedure EEProm24C02_Write(iAdd, iVal: byte);
begin
I2C1_Start; // signal START
I2C1_Wr(c24C02_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Wr(iVal); // valeur à ecrire à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

begin
Init_Main;
delay_ms(100);
while true do
begin
Values := PORTB; // lecture valeurs sur PORTB
EEProm24C02_Write($02, Values); // ecriture valeur PORTA dans mémoire
Delay_ms(100);
Values := EEProm24C02_Read($02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers PORTA
Delay_ms(100);
end;
end.


Descriptif du logiciel

Procédure Init
Cette routine d'initialisation générale, qui est appelée une seule fois au moment de la mise sous tension du PIC, permet de définir l'orientation des broches des ports A et B, grâce aux lignes TRISA et TRISB. La ligne CMCON = 7 permet de désactiver le fonctionnement des comparateurs du PIC. La ligne ADCON1 permet de désactiver les fonctions liées au convertisseur Analogique / Numérique, toutes les lignes sont en effet utilisées ici de façon logique. La ligne I2C1_Init permet d'initialiser le module MSSP utilisé pour les fonctions de lecture / écriture sur bus I2C.

Ecriture de données vers la mémoire 24C02
Cette fonction permet de stocker un octet à un emplacement donné de la mémoire et est assurée par la procédure EEProm24C02_Write, qui attend deux arguments : la valeur à stocker et l'adresse de la mémoire où la stocker.

Lecture de données contenues dans la mémoire 24C02
Cette fonction permet de relire une valeur préalablement stockée et est assurée par la fonction EEProm24C02_Read, qui attend comme seul argument l'adresse de la mémoire où lire la donnée. La valeur lue est directement passée en retour de la fonction.

Programme principal
Dans le programme principal se déroule des actions qui se répètent indéfiniment, grace à l'emploi d'une boucle infinie (qui ne s'arrête jamais ca aucune condition n'impose son arrêt). Voici précisement ce qui est fait de façon répétée :
- on lit l'état logique des entrées du port B du PIC, et on stocke la valeur lue (sous forme d'un octet) dans la mémoire 24C02.
- on attend 100 ms sans rien faire
- on relit l'octet précédement enregistré dans la mémoire et on le dupliques sur le port A du PIC.
- on attend 100 ms sans rien faire
- etc...

Lecture et écriture de données avec la 24C04

La mémoire 24C02 comporte une zone de stockage des données de 256 octets. Cet espace peut être insuffisant pour certains types de projet et il faut dans ce cas s'orienter vers des mémoires de taille supérieure, par exemple 24C04 (512 octets), 24C08 (1024 octets) ou 24C16 (2048 octets). Nous allons maintenant voir comment utiliser une 24C04.

Partage de la mémoire totale en plusieurs banques
La mémoire 24C04 comporte deux paquets de 256 octets, chaque paquet s'appelant une banque (bank en anglais). Pour la mémoire 24C02, le numéro de banque est toujours 0 car il n'y a pas besoin de plusieurs banques pour faire tenir 256 octets de données. En revanche pour la mémoire 24C04 qui comporte 512 octets adressables, le "découpage" se fait en deux banques de 256 octets chacune. Pour l'adressage d'une case mémoire d'un circuit 24C02, il n'y a pas lieu de se préoccuper d'un quelconque numéro de banque, mais il faut bien y passer dès qu'on attaque la taille au-dessus. Car si on ne spécifie pas la banque dans laquelle travailler, ce sont toujours les 256 premiers octets qui seront adressés en lecture ou en écriture (adresse commençant à $00 et finissant à $FF).

Adressage par banque
Bien beau tout ça, mais comment faire pour spécifier un numéro de banque, sachant qu'on ne dispose que d'un seul octet d'adressage de zone mémoire ? Et bien grâce à un bit côté poids faible de l'octet spécifiant l'adresse. Pour la mémoire 24C04, on ne dispose que de deux banques et un seul bit suffit. C'est le bit N°1 (le deuxième en partant du plus faible donc, puisqu'on démarre toujours par le bit N°0). Si l'adresse de base est $A0 (10100000 en binaire), alors l'accès aux deux banques de 256 octets se fait en spécifiant les adresses suivantes :
- Accès banque 0 en mode écriture (write) : adresse $A0
- Accès banque 0 en mode lecture (read) : adresse $A1
- Accès banque 1 en mode écriture (write) : adresse $A2
- Accès banque 1 en mode lecture (read) : adresse $A3
A noter que les bits 2 et 3 doivent refléter l'état logique donné aux bornes physiques d'adresse du circuit intégré 24C04 (A1 sur broche 2 et A2 sur broche 3). Ici les deux bits d'adresse sont positionnés à la valeur 0 car les entrées physiques d'adresse du circuit intégré sont toutes deux reliées à la masse (on n'est évidement pas obligé mais c'est le choix arbitraire fait ici). 

Les lignes de code qui suivent montrent comment le code précédent pour mémoire 24C02 a été adapté pour tenir compte de cette notion de banques.

program tuto_24c04_18F2420_Test_001;

const
c24C04_W = $A0;
c24C04_R = $A1;

var
c24C04B_W: byte;
c24C04B_R: byte;
Values: byte;

procedure Init_Main;
begin
CMCON := $07; // turn off comparators
ADCON1 := ADCON1 or $0F; // turn off analog inputs
INTCON2.NOT_RBPU := 0;
TRISA := $00;
TRISB := $FF;
PORTA := $00;
PORTB := $00;
I2C1_Init(100000); // initialisation comm I2C
end;

function EEProm24C04_Read(iBank, iAdd: byte): byte;
begin
c24C04B_W := c24C04_W;
c24C04B_W.1 := iBank; // iBank must be 0 or 1 !
c24C04B_R := c24C04_R;
c24C04B_R.1 := iBank; // iBank must be 0 or 1 !
I2C1_Start; // signal START
I2C1_Wr(c24C04B_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Repeated_Start; // issue I2C signal repeated start
I2C1_Wr(c24C04B_R); // send byte (device address + R)
Result := I2C1_Rd(0); // lecture valeur à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

procedure EEProm24C04_Write(iBank, iAdd, iVal: byte);
begin
c24C04B_W := c24C04_W;
c24C04B_W.1 := iBank; // iBank must be 0 or 1 !
I2C1_Start; // signal START
I2C1_Wr(c24C04B_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Wr(iVal); // valeur à ecrire à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

begin
Init_Main;
delay_ms(100);
portA := $0F;
delay_ms(200);
PortA := $00;
while true do
begin
Values := PORTB; // lecture valeurs sur PORTB
EEProm24C04_Write(0, $02, Values); // ecriture valeur PORTB dans mémoire
Delay_ms(100);
Values := EEProm24C04_Read(0, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers PORTA
Delay_ms(500);
Values := PORTB xor $FF; // lecture valeurs inverses sur PORTB
EEProm24C04_Write(1, $02, Values); // ecriture valeur PORTB dans mémoire
Delay_ms(100);
Values := EEProm24C04_Read(1, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers PORTA
Delay_ms(500);
end;
end.


La routine principale lit comme avant l'état des lignes RB0 à RB3 et stocke la valeur dans la première banque de la mémoire 24C04, à l'adresse $00. Puis quelques centaines de millisecondes plus tard, la valeur lue est "inversée" est est stocké dans la seconde banque de la mémoire. Entre ces deux opérations, lecture du même emplacement mémoire de la même banque pour visualisation sur les LED reliées au lignes du PortA du PIC. Résultat visuel, les LED doivent tantôt refléter l'état logique des entrées RB0 à RB3, tantôt refléter leur état inverse.

Lecture et écriture de données avec la 24C16

Nous nous appuyons sur la même méthode que celle utilisée pour travailler avec la mémoire 24C04. Seulement là on va accéder à quatre banques différentes sur les huit disponibles. Le code logiciel qui suit est complet.

program tuto_24c16_18F2420_Test_001;

const
// write address depend on bank to store data :
// $A0 for bank#0, $A2 for bank#1, $A4 for bank#2, $A6 for bank#3,
// $A8 for bank#4, $AA for bank#5, $AC for bank#6, $AE for bank#7,
// read address depend on bank from read data :
// $A1 for bank#0, $A3 for bank#1, $A5 for bank#2, $A7 for bank#3,
// $A9 for bank#4, $AB for bank#5, $AD for bank#6, $AF for bank#7,
// following constant are "base" addresses that can be latter incremented
c24C16_W = $A0;
c24C16_R = $A1;

var
i24C16B_W: byte; // can take any value between $A0 and $AE
i24C16B_R: byte; // can take any value between $A1 and $AF
Values: byte;

procedure Init_Main;
begin
CMCON := $07; // turn off comparators
ADCON1 := ADCON1 or $0F; // turn off analog inputs
INTCON2.NOT_RBPU := 0;
TRISA := $00;
TRISB := $FF;
PORTA := $00;
PORTB := $00;
I2C1_Init(100000); // initialisation comm I2C
end;

function EEProm24C16_Read(iBank, iAdd: byte): byte;
begin
i24C16B_W := c24C16_W; // "base" addr for write operation
i24C16B_R := c24C16_R; // "base" addr for read operation
// iBank must be comprised from 0 to 7 !
i24C16B_W.1 := iBank.0;
i24C16B_W.2 := iBank.1;
i24C16B_W.3 := iBank.2;
i24C16B_R.1 := iBank.0;
i24C16B_R.2 := iBank.1;
i24C16B_R.3 := iBank.2;
I2C1_Start; // signal START
I2C1_Wr(i24C16B_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Repeated_Start; // issue I2C signal repeated start
I2C1_Wr(i24C16B_R); // send byte (device address + R)
Result := I2C1_Rd(0); // lecture valeur à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

procedure EEProm24C16_Write(iBank, iAdd, iVal: byte);
begin
i24C16B_W := c24C16_W; // "base" addr for write operation
// iBank must be comprised from 0 to 7 !
i24C16B_W.1 := iBank.0;
i24C16B_W.2 := iBank.1;
i24C16B_W.3 := iBank.2;
{
c24C16B_R := c24C16_R;
c24C16B_R.1 := iBank.0;
c24C16B_R.2 := iBank.1;
c24C16B_R.3 := iBank.2;
}
I2C1_Start; // signal START
I2C1_Wr(i24C16B_W); // adresse composant I2C
I2C1_Wr(iAdd); // adresse mémoire
I2C1_Wr(iVal); // valeur à ecrire à l'adresse spécifiée
I2C1_Stop; // signal STOP
end;

begin
Init_Main;
delay_ms(100);
portA := $0F;
delay_ms(200);
PortA := $00;
while true do
begin

// step #1 - write and read in bank #1
Values := PORTB and $0F; // lecture valeurs directes sur RB0..RB3
EEProm24C16_Write(0, $02, Values); // ecriture valeur RB0..RB3 dans mémoire
Delay_ms(100);
Values := EEProm24C16_Read(0, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers RA0..RA3
Delay_ms(500);

// step #2 - write and read in bank #2
Values := PORTB xor $FF; // lecture valeurs inverses sur RB0..RB3
EEProm24C16_Write(1, $02, Values); // ecriture valeur RB0..RB3 dans mémoire
Delay_ms(100);
Values := EEProm24C16_Read(1, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers RA0..RA3
Delay_ms(500);

// step #3 - write and read in bank #3
Values := PORTB SHR 4; // lecture valeurs directes sur RB4..RB7
EEProm24C16_Write(2, $02, Values); // ecriture valeur RB4..RB7 dans mémoire
Delay_ms(100);
Values := EEProm24C16_Read(2, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers RA0..RA3
Delay_ms(500);

// step #4 - write and read in bank #4
Values := (PORTB SHR 4) xor $0F; // lecture valeurs inverses sur RB4..RB7
EEProm24C16_Write(3, $02, Values); // ecriture valeur RB4..RB7 dans mémoire
Delay_ms(100);
Values := EEProm24C16_Read(3, $02); // lecture valeur dans mémoire
PortA := Values; // affectation valeur lue vers RA0..RA3
Delay_ms(500);
Delay_ms(500); // allow simpler visual verification with LED

end;
end.


Rien de bien compliqué en somme...

Mémoire de plus grande taille ?

Je comprend aisement que l'usage de mémoires de type 24C02, 24C04 ou même 24C16 vous fasse sourire, vu la quantité d'espace adressable dans les mémoires les plus récentes. Viendra bien un jour où je parlerai plus précisement de ces mémoires plus dignes de vos réalisations... Tiens, pourquoi pas une FRAM (Ferroelectric Random Access Memory) MB85RS256A de 256K ? Il paraît que les temps d'accès d'une telle mémoire sont beaucoup plus courts que ceux d'une Flash ou EEPROM. Pour l'instant je continue de me documenter, et espère bien pouvoir profiter de routines toutes faites mises à dispo par de talentueux informaticiens... Et oui, moi aussi aime me simplifier la vie !