Raspberry Pi Pico

Pico I2C EEPROM: Write 0xDEADBEEF, Read It Back, Prove It

Raspberry Pi Pico W with red write and green read indicator LEDs for a virtual I2C EEPROM test in a circuit simulator

RAM forgets. Every variable your Pico is happily juggling right now evaporates the moment the USB cable comes out, which is fine until you need a calibration value, a user setting, or a boot count that actually counts. The answer on the I2C bus is an EEPROM — a few kilobytes of “still there in the morning” for twenty cents.

This is the same write-read-verify exercise we ran on the Arduino Uno, ported to the Pico: a virtual EEPROM at address 0x50, eight famous bytes in, eight bytes back out, every one checked. The protocol is identical; the Pico bits around the edges are what you’re here for.

The circuit

  • GP12 → red LED anode; cathode → GND — labeled “write” indicator
  • GP10 → green LED anode; cathode → GND — labeled “read” indicator
  • The EEPROM itself is virtual: the simulator hangs it on the I2C bus (SDA=GP4, SCL=GP5) at 0x50

Raspberry Pi Pico W with red write and green read indicator LEDs for a virtual I2C EEPROM test in a circuit simulator

On real hardware this is a four-wire AT24Cxx module — SDA, SCL, 3.3V, GND — and the code below wouldn’t change. Mind the voltage: the Pico has no 5V tolerance, so a 5V-powered EEPROM module needs level shifting on the bus lines.

The sketch

// Raspberry Pi Pico — I2C EEPROM write/read/verify
// Virtual EEPROM at 0x50. Test runs once, in setup().

#include <Wire.h>

#define EEPROM_ADDR 0x50

void eepromWrite(byte memAddr, byte data) {
  Wire.beginTransmission(EEPROM_ADDR);
  Wire.write(memAddr);   // which cell
  Wire.write(data);      // what to store
  Wire.endTransmission();
  delay(5);              // real chips need a few ms to commit the write
}

byte eepromRead(byte memAddr) {
  // dummy write: set the address pointer, send no data
  Wire.beginTransmission(EEPROM_ADDR);
  Wire.write(memAddr);
  Wire.endTransmission();

  Wire.requestFrom(EEPROM_ADDR, 1);
  return Wire.available() ? Wire.read() : 0xFF;  // 0xFF = bus went silent
}

void setup() {
  Serial.begin(115200);
  delay(500);
  Wire.begin();
  Serial.println("=== Pico I2C EEPROM Test ===");
  Serial.println();

  // the two most famous magic numbers in computing, split into bytes
  byte testData[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE};

  Serial.println("Writing 8 bytes...");
  for (int i = 0; i < 8; i++) {
    eepromWrite(i, testData[i]);
    Serial.print("  ["); Serial.print(i);
    Serial.print("] = 0x");
    if (testData[i] < 16) Serial.print('0');
    Serial.println(testData[i], HEX);
  }
  Serial.println();

  Serial.println("Reading back...");
  int pass = 0;
  for (int i = 0; i < 8; i++) {
    byte val = eepromRead(i);
    Serial.print("  ["); Serial.print(i);
    Serial.print("] = 0x");
    if (val < 16) Serial.print('0');
    Serial.print(val, HEX);
    if (val == testData[i]) {
      Serial.println(" OK");
      pass++;
    } else {
      Serial.print(" FAIL (expected 0x");
      Serial.print(testData[i], HEX);
      Serial.println(")");
    }
  }

  Serial.println();
  Serial.print("Result: ");
  Serial.print(pass);
  Serial.println("/8 passed");
}

void loop() {
  delay(10000);   // test already ran — keep the output on screen
}

Run it and you get sixteen lines of hex and a verdict: 8/8 passed. The test data is a small piece of computing culture — 0xDEADBEEF and 0xCAFEBABE are the two most famous magic numbers in the business (the second one literally starts Java class files). There’s a practical reason too: a memorable pattern means that when a read comes back wrong, you notice which bytes went wrong, instead of squinting at random noise.

The three moves that matter

  • Write needs recovery time. EEPROM cells take a few milliseconds to physically commit after endTransmission(). The delay(5) is the datasheet’s number for the AT24C series, not superstition. The simulator’s virtual chip is forgiving; real silicon silently drops writes that arrive too fast.
  • Read starts with a fake write. You “write” the cell number first (that’s setting the chip’s internal address pointer), then requestFrom() pulls the byte back out. Same register-pointer dance as the RTC project, one byte instead of seven.
  • The test lives in setup() on purpose. A storage smoke test should run once per reset and then get out of the way. The empty loop() isn’t laziness — it’s correct design for a self-test. Every storage chip you bring up in your career deserves this exact ceremony before you trust it with data that matters.

Honest notes

  • The indicator LEDs strike out again. Third Pico example in a row: the circuit wires “write” and “read” LEDs, the prose describes them blinking, and the sketch contains not one pinMode or digitalWrite for either. They’re decoration. If you want them real: pin 12 HIGH around the write call, pin 10 HIGH around the read, LOW after — and at that point you’ve reinvented the logic analyzer, poorly, which is still a fine exercise.
  • 0xFF is still a lying sentinel. A silent bus returns 0xFF — which is also the natural state of an erased EEPROM cell. “Read failed” and “cell was never written” are indistinguishable here. For a demo, acceptable; for firmware that matters, return a status and a value separately.
  • The chip wears out, slowly but surely. ~100,000 write cycles per cell on real AT24C silicon. Writing a counter once a second burns that in about a day. This sketch writes eight cells once — the correct relationship with EEPROM. High-frequency state wants FRAM or flash with wear leveling.
  • Byte-at-a-time is the slow lane. Real chips take page writes (up to 32 bytes per transaction). Eight bytes doesn’t justify the complexity; a data logger writing hundreds of bytes per minute absolutely does.
  • The address isn’t guaranteed to be 0x50. A0–A2 pins on the physical chip shift the address — up to eight EEPROMs per bus, or one nasty collision if left floating. When a real chip won’t answer, those three pins are suspect number one. (The Pico scanner from the earlier project is how you check.)

When it fails

  • All 8 reads come back 0xFF, all FAIL → nothing on the bus at 0x50. In the sim: reload the example so the virtual EEPROM attaches. On hardware: address jumpers, wiring, pull-ups — and this is what the I2C scanner exists for.
  • Intermittent FAILs on real hardware → write delay too short for your chip. Try 10 ms. Cheap clones sometimes need longer than the datasheet admits.
  • First read fails, rest pass → marginal timing: the chip was still committing the last write when the first read arrived. Same fix.
  • Output stops after the banner → compile succeeded but the write loop crashed? No — more likely the sim wasn’t actually running. It happens to everyone weekly.

Where to go from here

The upgrade that turns this from demo to feature: store a struct. Serialize a settings object — brightness, volume, last mode — into bytes 0–15 with a checksum at the end; on boot, read back and verify the checksum before trusting anything. Bytes + offset + checksum is the skeleton of every “remember my settings” feature in existence. Strings are the same trick with a length byte in front.

Open the simulator and load the Pico EEPROM example. Watch 0xDEADBEEF and 0xCAFEBABE go in and come back verified — then change one expected value in the compare loop to something wrong and run again. A test that can fail is the only kind worth having, and watching it catch your own sabotage is the lesson that sticks.

Leave a Reply

Your email address will not be published. Required fields are marked *