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 :
- un pour usage d'une
mémoire 24C02 (256 octets)
- un pour usage d'une mémoire
24C04 (512 octets)
- un pour usage d'une mémoire
24C16 (2048 octets)
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.
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).
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.
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é.
- Schéma avec 24C02 : on lit l'état logique
des huit broches du port
B (RB0 à RB7), ce qui donne une valeur codée sur huit bits, soit un octet. On
stocke cette valeur dans une zone mémoire de l'EEProm puis on
relie la valeur de l'octet présent au même emplacement mémoire quelques
ms plus tard. Enfin on utilise cette valeur lue pour modifier l'état
logiques des lignes du Port A du PIC. En réalité, on ne met en évidence
que quatre bits sur les huit de l'octet enregistré dans l'EEProm, mais
le principe est bien là. Il va de soit qu'un tel usage n'a pas de
correspondance pratique dans la vie de tous les jours, et que vous
n'utiliserez pas une seule case mémoire du composant mais
plus.
- Schéma avec 24C04
: même chose que pour le schéma avec 24C02, mais pour valider
l'écriture et la lecture dans des espaces mémoire différents, on
utilise la valeur lue sur le port B de façon directe ou inversée. La
première valeur numérique stockée en mémoire correspond à l'état
des quatre lignes RB0 à RB3 avec état logique d'origine et la seconde
valeur numérique stockée en mémoire correspond à l'état des quatre
lignes RB0 à RB3 avec état logique inversé.
- Schéma avec 24C16
: même pricipe de validation, mais cette fois avec lecture réelle des
huit bits du port B. On écrit quatre valeurs différentes dans quatre
zones mémoires et on relit ces zones mémoire pour les afficher sur les
quatre LED. La première valeur numérique stockée en mémoire
correspond à l'état des quatre lignes RB0 à RB3 avec état logique
d'origine. La seconde valeur numérique stockée en mémoire
correspond à l'état des quatre lignes RB0 à RB3 avec état logique
inversé. La troisième valeur numérique stockée en mémoire
correspond à l'état des quatre lignes RB4 à RB7 avec état logique
d'origine. La quatrième valeur numérique stockée en mémoire
correspond à l'état des quatre lignes RB4 à RB7 avec état logique
inversé.
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 !