9/24/2025, 5:24:32 PM
Fujitsu MB85RC256V 32KB FRAM Module

June 13, 2025

Ferroelectric RAM (FRAM) Driver

Source code: github.com/NabeelAhmed1721/MB85RC-driver and the datasheet: Fujitsu Semiconductor MB85RC256V

I recently bought a Ferroelectric RAM module for a project I'm working on. I could've bought a EEPROM, but upon looking at the listing for the MB85RC, I was immediately struck by its speed and durability. It's so fast that the datasheet reminds the user to not wait for writes because it performs "high speed write operations, so any waiting time for an ACK polling* does not occur."

It seems too good to be true, right? Why don't use we FRAM modules for everything? As with most things, there are trade-offs, and with FRAM it is the price to storage ratio. FRAM is expensive, which is why you can typically only find modules with at most 32 or 16 KB.

Anyway, the chip interfaces with I2C, but I wanted a interface that abstracted away the details of the protocol so I could use the chip in a idiomatic way.

Driver Design

I had two options when writing the driver:

  1. Implement I2C manually (via I2C registers and interrupts or software bit-banging)
  2. Use Arduino's Wire library, which is an implementation of the hardware support for the I2C bus on AVR microcontrollers

From an engineering perspective, the second option is the better place to start. The Wire library is mature, tested, and well-documented thanks to its open-source nature. That's not to say you should always rely on a library, but when starting a project it's important to eliminate unnecessary complexity. Begin with what works, and only move to a more custom solution if the need arises.

I2C Transactions

I bought the 32KB MB85RC, which features 32,768 addressable words. This means the maximum address is 0x7FFF, so the chip requires a two-byte memory address before any read or write to the FRAM array. Transactions follow the I2C standard format:

I2C Header Transaction

I2C Header Transaction

Reading a Byte

Reading (and writing) a byte from/to the FRAM chip is a two-step process. First, we perform a "dummy write" to set the memory address pointer to the address we want to read from. This is done by sending a two-byte address to the device. The device acknowledges the address but doesn't receive any data. The FRAM chip's internal register is set to the address. This is powerful because, for longer reads or writes, we only need to send the address once and the chip will automatically increment the pointer for us. The second step is to send a read request. This is when the chip sends the byte of data stored at the address we specified. The following diagram illustrates this process:

I2C Traffic Diagram

I2C Traffic Diagram

This transaction happens very quickly, so it's more intutive to see it as blocks of data present on the bus:

I2C Bus Traffic

I2C Bus Traffic

Writing a Byte

Writing a byte is more straightforward. We send the two-byte memory address followed by the single byte of data we want to write. Since the write is instantaneous, the transaction is complete once the data is received.

Code

The core of the driver is a C++ class called MB85RC. This class encapsulates the I2C communication and provides an idiomatic interface for reading and writing data. It's designed to be a singleton, ensuring only one instance is created to manage the global I2C Wire interface.

This is how the header is defined:

cpp
/** * MB85RC FRAM driver * NOTE: Only one instance of this class should be created * as it manages the global I2C Wire interface */ class MB85RC { private: static constexpr uint8_t BASE_ADDRESS = 0x50; uint8_t device_address = MB85RC::BASE_ADDRESS; static bool initialized; // can fail if address is not in range bool writeAddress(uint16_t address, bool end = true); public: MB85RC(uint8_t address_extension = 0); void close(); // single byte operations uint8_t readByte(uint16_t address); bool writeByte(uint16_t address, uint8_t value); // multi-byte operations bool read(uint16_t address, uint8_t* buffer, size_t length); bool write(uint16_t address, const uint8_t* buffer, size_t length); // utility functions uint8_t getAddress() const; };

Example Usage

I added an example file in the project repository to demostrate its use. The examples/main.cpp has a hexdump-like utility to visualize the FRAM's memory contents. The code first writes a text string to a specific address, then reads a larger chunk of memory to show the written data in context:

cpp
#include <Arduino.h> #include "MB85RC.h" void hexdump(uint8_t* buffer, size_t length); MB85RC fram = MB85RC(); void setup() { Serial.begin(9600); // write text buffer at address 0x0100 const char* text = "Nabeel was here!"; fram.write(0x0100, (uint8_t*)text, strlen(text)); // read 1024 bytes from address 0x0000 const size_t SIZE = 1024; uint8_t buffer[SIZE]; fram.read(0x0000, buffer, SIZE); hexdump(buffer, SIZE); // close the driver fram.close(); } void loop() {} void hexdump(const uint8_t* buffer, size_t length) { const size_t bytesPerLine = 16; char line[80]; for (size_t i = 0; i < length; i += bytesPerLine) { char* p = line; sprintf(p, "%04X: ", (unsigned int)i); p += strlen(p); for (size_t j = 0; j < bytesPerLine; j++) { if (i + j < length) { sprintf(p, "%02X ", buffer[i + j]); } else { sprintf(p, " "); } p += 3; } *p++ = ' '; for (size_t j = 0; j < bytesPerLine; j++) { if (i + j < length) { char c = buffer[i + j]; *p++ = (c >= 32 && c <= 126) ? c : '.'; } } // null-terminate *p = '\0'; Serial.println(line); } }

Output:

bash
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ... 0100: 4e 61 62 65 65 6c 20 77 61 73 20 68 65 72 65 21 Nabeel was here! 0110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ...