Memorie Seriali I2C/SPI: Come Funzionano e Come Usarle con Arduino | EEPROM Tutorial Pratico

Le memorie seriali spiegate in modo pratico! In questo episodio del corso di elettronica digitale vediamo cosa sono le memorie seriali come EEPROM e Flash e come integrarle nei tuoi progetti di elettronica digitale. Scopri nel dettaglio la differenza tra memorie parallele e seriali, perché le i protocolli I2C e SPI sono essenziali per Arduino, ESP32 e Raspberry Pi, e come funzionano a livello hardware e software.

Analizziamo il funzionamento delle memorie 24C08B (I2C) e CAT25640 (SPI) per capire le differenze tra i due protocolli/bus.

Approfondiamo poi il bus I2C e vediamo l’effettiva connessione fisica, le necessarie resistenze di pull-up e la gestione delle linee open drain/open collector sul bus. Utilizziamo gli sketch Arduino per dimostrare come leggere e scrivere byte e pagine, come gestire la selezione degli indirizzi e come affrontare le differenze di documentazione tra i produttori. Il video è pensato per chi sta sviluppando sistemi embedded, chi lavora con progetti retrocomputing, o chi vuole capire a fondo i dettagli dei protocolli I2C/SPI/Microwire direttamente sulla breadboard.

Codice mem_24c08_byte_rw.ino
/************************************************************
Lettura e scrittura di una memoria seriale 24C08

Codice mostrato nel video YouTube:
https://youtu.be/glFaGDb5_NE

Alexa Academy
Paolo Godino
@ 2025
*************************************************************/
#include "Arduino.h"

#define MEM_IO1  22
#define MEM_IO2  24
#define MEM_IO3  26
#define MEM_IO4  28
#define MEM_IO5  30
#define MEM_IO6  32
#define MEM_IO7  34
#define MEM_IO8  36

#define MEM_A0  23
#define MEM_A1  25
#define MEM_A2  27
#define MEM_A3  29
#define MEM_A4  31
#define MEM_A5  33
#define MEM_A6  35
#define MEM_A7  37
#define MEM_A8  39
#define MEM_A9  41
#define MEM_A10  43
#define MEM_A11  45
#define MEM_A12  47
#define MEM_A13  49
#define MEM_A14  51

#define MEM_WE  50
#define MEM_OE  46
#define MEM_CS  48

#define CMD_READ  "r"
#define CMD_WRITE "w"


char buffer[1000]= {0};  // Buffer utilizzato per sprintf per visualizzare su seriale il contenuto della memoria
char buffer_ascii[1000]= {0}; // Buffer utilizzato per sprintf per visualizzare su seriale il contenuto della memoria nella parte ASCII
char tmp_buf[100] = {0};

#define READ_CHUNK_SIZE 300  // Dimensione pacchetto per l'invio del contenuto della memoria nella lettura binaria
char message[READ_CHUNK_SIZE];

bool isWriting = false;

unsigned int current_add = 0;
unsigned int final_add = 0;

byte data_read;

#define COMMANDSIZE 32
char cmdbuf[COMMANDSIZE];

void setDatabusOut(bool isOut) {
  if (isOut) {
    pinMode(MEM_IO1, OUTPUT);
    pinMode(MEM_IO2, OUTPUT);
    pinMode(MEM_IO3, OUTPUT);
    pinMode(MEM_IO4, OUTPUT);
    pinMode(MEM_IO5, OUTPUT);
    pinMode(MEM_IO6, OUTPUT);
    pinMode(MEM_IO7, OUTPUT);
    pinMode(MEM_IO8, OUTPUT);
  } else {
    pinMode(MEM_IO1, INPUT);
    pinMode(MEM_IO2, INPUT);
    pinMode(MEM_IO3, INPUT);
    pinMode(MEM_IO4, INPUT);
    pinMode(MEM_IO5, INPUT);
    pinMode(MEM_IO6, INPUT);
    pinMode(MEM_IO7, INPUT);
    pinMode(MEM_IO8, INPUT);
  }
}

byte readData() {
  byte d7 = digitalRead(MEM_IO8);
  byte d6 = digitalRead(MEM_IO7);
  byte d5 = digitalRead(MEM_IO6);
  byte d4 = digitalRead(MEM_IO5);
  byte d3 = digitalRead(MEM_IO4);
  byte d2 = digitalRead(MEM_IO3);
  byte d1 = digitalRead(MEM_IO2);
  byte d0 = digitalRead(MEM_IO1);

  byte data_bus = d7<<7 | d6<<6 | d5<<5 | d4<<4 | d3<<3 | d2<<2 | d1<<1 | d0;

  return data_bus;
}

void writeData(byte b) {
  digitalWrite(MEM_IO1, bitRead(b, 0));
  digitalWrite(MEM_IO2, bitRead(b, 1));
  digitalWrite(MEM_IO3, bitRead(b, 2));
  digitalWrite(MEM_IO4, bitRead(b, 3));
  digitalWrite(MEM_IO5, bitRead(b, 4));
  digitalWrite(MEM_IO6, bitRead(b, 5));
  digitalWrite(MEM_IO7, bitRead(b, 6));
  digitalWrite(MEM_IO8, bitRead(b, 7));
}

void writeAddress(word w) {
  digitalWrite(MEM_A0, bitRead(w, 0));
  digitalWrite(MEM_A1, bitRead(w, 1));
  digitalWrite(MEM_A2, bitRead(w, 2));
  digitalWrite(MEM_A3, bitRead(w, 3));
  digitalWrite(MEM_A4, bitRead(w, 4));
  digitalWrite(MEM_A5, bitRead(w, 5));
  digitalWrite(MEM_A6, bitRead(w, 6));
  digitalWrite(MEM_A7, bitRead(w, 7));
  digitalWrite(MEM_A8, bitRead(w, 8));
  digitalWrite(MEM_A9, bitRead(w, 9));
  digitalWrite(MEM_A10, bitRead(w, 10));
  digitalWrite(MEM_A11, bitRead(w, 11));
  digitalWrite(MEM_A12, bitRead(w, 12));
  digitalWrite(MEM_A13, bitRead(w, 13));
  digitalWrite(MEM_A14, bitRead(w, 14));
}

// Utilizziamo il Write Cycle 1
void writeCycle(byte data, word address) {
  writeAddress(address);
  delayMicroseconds(3);
  digitalWrite(MEM_OE, 1);
  delayMicroseconds(3);
  digitalWrite(MEM_CS, 0);
  delayMicroseconds(3);
  setDatabusOut(true);
  writeData(data);
  delayMicroseconds(3);
  digitalWrite(MEM_WE, 0);
  delayMicroseconds(1);
  digitalWrite(MEM_WE, 1);
  delayMicroseconds(3);
  setDatabusOut(false);
  delayMicroseconds(3);
  digitalWrite(MEM_CS, 1);
  delay(6);
}

// Utilizziamo il Read Cycle 1
byte readCycle(word address) {
  writeAddress(address);
  digitalWrite(MEM_CS, 0);
  delayMicroseconds(3);
  digitalWrite(MEM_OE, 0);
  delayMicroseconds(3);
  byte data = readData();
  digitalWrite(MEM_OE, 1);
  delayMicroseconds(3);
  digitalWrite(MEM_CS, 1);
  delayMicroseconds(3);

  return data;
}

// Utilizziamo il Read Cycle 2
void continuousReadCycle(word address, byte* data, unsigned int size) {
  writeAddress(address);
  delayMicroseconds(1);

  digitalWrite(MEM_CS, 0);
  digitalWrite(MEM_OE, 0);

  for (int idx = 0; idx < size; ++idx) {
    writeAddress(address + idx);
    delayMicroseconds(1);
    data[idx] = readData();
  }

  digitalWrite(MEM_OE, 1);
  digitalWrite(MEM_CS, 1);
  delayMicroseconds(1);
}

void setup() {
  Serial.begin(19200);

  setDatabusOut(false);

  pinMode(MEM_A0, OUTPUT);
  pinMode(MEM_A1, OUTPUT);
  pinMode(MEM_A2, OUTPUT);
  pinMode(MEM_A3, OUTPUT);
  pinMode(MEM_A4, OUTPUT);
  pinMode(MEM_A5, OUTPUT);
  pinMode(MEM_A6, OUTPUT);
  pinMode(MEM_A7, OUTPUT);
  pinMode(MEM_A8, OUTPUT);
  pinMode(MEM_A9, OUTPUT);
  pinMode(MEM_A10, OUTPUT);
  pinMode(MEM_A11, OUTPUT);
  pinMode(MEM_A12, OUTPUT);
  pinMode(MEM_A13, OUTPUT);
  pinMode(MEM_A14, OUTPUT);
  
  pinMode(MEM_WE, OUTPUT);
  pinMode(MEM_OE, OUTPUT);
  pinMode(MEM_CS, OUTPUT);

  digitalWrite(MEM_WE, 1);
  digitalWrite(MEM_OE, 1);
  digitalWrite(MEM_CS, 1);
}

void read_mem(unsigned int start, unsigned int len) {
  unsigned int cur_add = start;

  do {
    sprintf(buffer, "%04X: ", cur_add);
    sprintf(buffer_ascii, "");

    byte data[16];
    continuousReadCycle(cur_add, data, 16);

    for (int i=0; i<16; ++i) {      
      if (cur_add < start+len) {
        data_read = data[i];
        cur_add++;
        
        sprintf(tmp_buf, " %02X", data_read);
      } else {
        sprintf(tmp_buf, "   ");
      }
      strcat(buffer, tmp_buf);

      if (i == 7) {
        strcat(buffer, " ");
      }

      if(isprint(data_read)) {
        sprintf(tmp_buf, "%c", data_read);
        strcat(buffer_ascii, tmp_buf);
      } else {
        strcat(buffer_ascii, ".");
      }
    }

    strcat(buffer, "  ");
    strcat(buffer, buffer_ascii);

    Serial.println(buffer);
  } while (cur_add < start+len);
}

// Legge il comando da seriale
void readCommand() {
  // Cancella il buffer
  for(int i=0; i< COMMANDSIZE;i++) cmdbuf[i] = 0;

  char c = ' ';
  int idx = 0;

  // Legge dalla seriale fino a che si trova un fine riga oppure il buffer è pieno
  do {
    if(Serial.available()) {
      c = Serial.read();
      cmdbuf[idx++] = c;
    }
  } 
  while (c != '\n' && c != '\r' && idx < (COMMANDSIZE)); //save the last '\0' for string end
  
  // Cambia l'ultimo newline in '\0' per ottenere una stringa terminata con NULL
  cmdbuf[idx - 1] = 0;

  Serial.println("");
}

void loop() {
  if (isWriting) {
    readCommand();

    // Fine scrittura
    if (strcmp(cmdbuf, "") == 0) {
      isWriting = false;

      Serial.println("OK");
    } else {
      byte data_wr = strtol(cmdbuf, NULL, 16);
      Serial.println(data_wr, HEX);
      writeCycle(data_wr, current_add++);
    }
  } else {
    readCommand();

    byte index = 0;
    char *strings[6];
    char *ptr = NULL;
    ptr = strtok(cmdbuf, " ");
    while (ptr != NULL) {
      strings[index++] = ptr;
      ptr = strtok(NULL, " ");
    }

    if (index > 0) {
      if (strcmp(strings[0], CMD_READ) == 0) {
        unsigned int startAdd = 0;
        unsigned int len = 16;
        if (index > 1) startAdd = strtol(strings[1], NULL, 16);
        if (index > 2) len = strtol(strings[2], NULL, 10);

        read_mem(startAdd, len);
      } else if (strcmp(strings[0], CMD_WRITE) == 0) {
        unsigned int startAdd = 0;
        if (index > 1) startAdd = strtol(strings[1], NULL, 16);
        current_add = startAdd;

        isWriting = true;
      } else {
        Serial.println("WRONG COMMAND");
      }
    } else {
      Serial.println("WRONG COMMAND");
    }
  }
}
Codice mem_24c08_read_32.ino
/************************************************************
Lettura di 32 byte di una memoria seriale 24C08

Codice mostrato nel video YouTube:
https://youtu.be/glFaGDb5_NE

Alexa Academy
Paolo Godino
@ 2025
*************************************************************/

#include "Wire.h"

#define EEPROM_I2C_ADDRESS  0x50

// Hex+ASCII dump
void printHexAscii(const uint8_t start_add, const uint8_t *b, int len) {
  for (int off=0; off<len; off+=16) {
    Serial.print(F("0x"));
    uint8_t add = start_add + off;
    if(add<0x1000) Serial.print('0');
    if(add<0x100)  Serial.print('0');
    if(add<0x10)   Serial.print('0');
    Serial.print(add,HEX);
    Serial.print(F(": "));
    for (int i=0;i<16;i++){
      if (off+i<len){
        if (b[off+i]<16) Serial.print('0');
        Serial.print(b[off+i],HEX);
        Serial.print(' ');
      } else {
        Serial.print(F("   "));
      }
    }
    Serial.print(F(" | "));
    for(int i=0;i<16 && off+i<len;i++) {
      char c = (b[off+i]>=32 && b[off+i]<127) ? char(b[off+i]) : '.';
      Serial.print(c);
    }
    Serial.println();
  }
}


void read_mem_block(uint8_t* data, uint8_t add, int block) {
  Wire.beginTransmission(EEPROM_I2C_ADDRESS+block);   // Viene iniziata una scrittura "finta" per impostare l'indirizzo
  Wire.write(add);
  Wire.endTransmission();
  Wire.requestFrom(EEPROM_I2C_ADDRESS+block, 32);
  int i=0;
  while (Wire.available() && i < 256) {
    uint8_t value = Wire.read();
    data[i++] = value;
  }

}

void setup() {
  Serial.begin(9600);
  while (!Serial) {
      // wait for Serial to become active
  }
  Wire.begin();
  delay(5);
  
  uint8_t buffer[32];
  read_mem_block(buffer, 0x0, 0);
  printHexAscii(0x0, buffer, 32);
}

void loop() {
  

}
Codice mem_24c08_write_8.ino
/************************************************************
Scrittura multibyte di una memoria seriale 24C08

Codice mostrato nel video YouTube:
https://youtu.be/glFaGDb5_NE

Alexa Academy
Paolo Godino
@ 2025
*************************************************************/

#include "Wire.h"

#define EEPROM_I2C_ADDRESS  0x50

void write_8byte(uint8_t* data, uint8_t add, int block) {
  Wire.beginTransmission(EEPROM_I2C_ADDRESS+block);
  Wire.write(add); // indirizzo interno iniziale
  for (byte i = 0; i < 8; i++) {
    Wire.write(data[i]); // scrive 8 byte consecutivi
  }
  Wire.endTransmission();
  delay(10); // attesa per tWR (tempo di scrittura interno)
}

void setup() {
  Serial.begin(9600);
  while (!Serial) {
      // wait for Serial to become active
  }
  Wire.begin();
  delay(5);

  uint8_t buffer[8] = {0x2a, 0x02, 0xaa, 0xba, 0x07, 0xfe, 0x12, 0xaa};
  write_8byte(buffer, 0x00, 0);

  Serial.println("Scrittura terminata");
}

void loop() {
  

}
Avatar Paolo Godino