BBC micro:bit
Adafruit I2C FRAM

Introduction

The Adafruit I2C FRAM breakout board provides 32 kilobytes of non-volatile storage. The data are stored in frames, each one a byte. The frame addresses are 16 bit values. The board is quite small and looks like this,

micro:bit circuit

The breakout plays nicely at 3V3 and the read/write operations are blissfully easy to program. Although you have nearly the same amount of persistent storage on the micro:bit, it gets wiped every time you flash a program. You don't get that problem with this chip, making it easier to develop projects that depend on large reference tables, fonts or other data.

Circuit

You will need to connect 4 of the pins on the breakout.

  • Connect VCC to 3V on the microbit.
  • Connect GND on the breakout to GND.
  • Connect SDA to pin 20.
  • Connect SCL to pin 19.

Programming

This breakout is about as easy to work with as you get. To write a value to a frame, you send the frame address split into two bytes, leftmost 8 bits first, then you send the value. To read a value from a frame, you write the frame address like before, then read a byte to get the value. This is a basic class,

from microbit import *

class FRAM:
    def __init__(self, address = 0x50):
        self.ADDR = address
    
    # write a byte to a 16 bit frame address
    def write_frame(self,frameAddr,value):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF,value]), repeat=False)
    
    # read a byte from a 16 bit frame address
    def read_frame(self,frameAddr):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF]), repeat=False)
        data = i2c.read(self.ADDR,1,repeat=False)
        return data[0]

Adafruit have a test program in their Arduino library. It uses the first frame to store the number of times the program has been executed. This version dumpes the first 32 frames instead of all of them. You need to run this with access to the REPL. Mu is the easiest way to do that,

from microbit import *

class FRAM:
    def __init__(self, address = 0x50):
        self.ADDR = address
    
    # write a byte to a 16 bit frame address
    def write_frame(self,frameAddr,value):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF,value]), repeat=False)
    
    # read a byte from a 16 bit frame address
    def read_frame(self,frameAddr):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF]), repeat=False)
        data = i2c.read(self.ADDR,1,repeat=False)
        return data[0]
        
ferro = FRAM()
# test read
test = ferro.read_frame(0x0)
print("The micro:bit has been restarted", test, "time(s).")
# test write
ferro.write_frame(0x0,test+1)
# dump first 32 frames
out = []
for i in range(32):
    out = out + [ferro.read_frame(i)]
print ('{}'.format(' '.join('{:02X}'.format(x) for x in out)),sep=': ')        

micro:bit circuit

Notice that the first frame reads 19. That is the hexadecimal for 25. The program adds 1 to the value after it has shown the message. This is the value that will be read the next time that the program is started.

Storing Strings

Assume that you want to store character data on the breakout. Just write the ASCII value to the frame and make sure that you do the conversion each time you want to read or write a value. This sample program writes and reads a string.

from microbit import *

class FRAM:
    def __init__(self, address = 0x50):
        self.ADDR = address
    
    def wipe(self):
        for i in range(32768):
            self.write_frame(i,0)
    
    # write a byte to a 16 bit frame address
    def write_frame(self,frameAddr,value):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF,value]), repeat=False)
    
    # read a byte from a 16 bit frame address
    def read_frame(self,frameAddr):
        i2c.write(self.ADDR, bytes([frameAddr>>8,frameAddr & 0xFF]), repeat=False)
        data = i2c.read(self.ADDR,1,repeat=False)
        return data[0]
    
    # write the ASCII values of a string s in consecutive frames starting with f
    def write_string(self,s,f):
        for i, c in enumerate(s):
            self.write_frame(f+i,ord(c))
    
    # read a byte from a 16 bit frame address and convert to character
    def read_char(self,frameAddr):
        return chr(self.read(frameAddr))
        
ferro = FRAM()
test = "FRAM Test Write/Read"
print("Writing the string '",test,"' to memory.", sep=" ")
ferro.write_string(test,0)
st = ""
nu = ""
for i,c in enumerate(test):
    tmp = ferro.read_frame(i)
    st = st + chr(tmp)
    nu = nu + '{:02X}'.format(tmp) + " "
print("Hexadecimal byte values")
print(nu)
print("#String rebuilt")
print(st)

This is the output you get,

micro:bit circuit

In this example, the length of the string is known. If you wanted to store strings of indeterminate length, you make sure that you write a zero to the frame just after the end of the string. You keep reading and displaying/processing a character at a time until you reach a blank. The wipe method might come in useful when working with character data and relying on finding empty frames.

Images

In order to store the string data, all we had to do was to split it up into bytes. For character data, there is an easy way to do it. For other types of information, you have to consider the most appropriate way to represent that using bytes. You can use more than one byte for each thing you want to store.

Let's say you want to store images for the LED matrix. You have several choices. You could take the string representation of an image and write all of that character data to the memory. When you read it back, recreate the string and generate your image from a string. This would be the easiest way to do things and you'd get a truckload of images stored. It is pretty wasteful of the space and maybe you might come up with something better.

Negative Integers

If you wanted to store whole numbers in the range -128 to 127, you can do that with a byte, using two's complement form. A brief explanation can be found here, about half way down the page. The following program doesn't involve the FRAM, but it does show how you could encode your values quite easily.

from microbit import *

def make_twos_comp(value, nBits):
    return (-value) & (2**nBits - 1)

def read_twos_comp(value, nBits):
    return value - int((value << 1) & 2**nBits)
    
testvalues = [-127,-10,-1,0,1,127]
print("Two's Complement Test")
for i in testvalues:
    if i<0:
       toos = make_twos_comp(abs(i),8)
    else:
       toos = i
    dec = read_twos_comp(toos,8)
    print("Value:",i, "\tTwo's: ",'{:08b}'.format(toos),"\tDecoded: ", dec)

Here is the output in the REPL window,

micro:bit circuit

The binary pattern is the one that you would write to the FRAM frame (the variable 'toos'). The last column displays the value decoded.

Challenges

  1. Start simply by making sure that you can write and read to specific locations in a program.
  2. Use the memory to log readings from an analog sensor.
  3. Work out how to store and read images. Make a couple of animations.
  4. Use the breakout to store the details of the most gigantic maze ever to be explored on a micro:bit. It's difficult to orientate yourself when navigating such a small fragment of a maze. You don't need to make your maze too large before it'll be quite a challenge to navigate or escape.
  5. Attach an RTC as well as a sensor. Instead of storing the readings taken at specific intervals, store only those that matter, along with a timestamp. If you use a PIR, you could store the times that you got a hit. This means storing several bytes for each event, with each byte corresponding to a piece of information like the hours of the time. You can just write the raw bytes that you read from the RTC and decode them when you read the FRAM back.