MicroPython Driver for MB85RC256V FRAM
Contents
Introduction
This a MicroPython driver written specifically for the BBC micro:bit that will work with the MB85RC256V FRAM 32KB memory chip. The driver implements most functions described in the product datasheet.
The MB85RC256V FRAM chip is discussed in some detail here.
The MB85RC256V FRAM chip is readily available on inexpensive breakout boards.
Fig 1 - MB85RC256V FRAM breakout board: [L to R] front side and rear side
Connecting the MB85RC256V FRAM Board
This sensor communicates via I2C. It must be connected to the micro:bit as follows:
micro:bit | MB85RC256V |
---|---|
3.3V | VCC |
GND | GND |
Pin 19 | SCL |
Pin 20 | SDA |
This utilises the standard I2C pins of the micro:bit.
WP pinThe WP pin on the breakout board provides a memory write protect function. It can optionally be connected to any available digital pin on the micro:bit. This driver does cater for this pin with a method but it is entirely optional and probably won't be used by most people.
I2C Address using A0, A1, A2 pinsThe A0, A1 and A2 pins can be connected to either VCC or GND to give a selection of eight possible I2C addresses. Details on changing the I2C address can be found here.
The breakout board has all three pins connected to GND giving a default address of 0x50.
Driver Overview
The driver code can be:
- Copied from this webpage onto the clipboard then pasted into the MicroPython editor e.g. the Mu Editor. It should be saved as fc_mb85rc256v.py - OR -
- Download as a zip file using the link. Unzip the file and save it as fc_mb85rc256v.py into the default directory where the MicroPython editor e.g. Mu Editor saves python code files.
After saving the fc_mb85rc256v.py file to the computer it should be copied to the small filesystem on the micro:bit. The examples on this page will not work if this step is omitted. Any MicroPython editor that recognises the micro:bit will have an option to do this.
The Mu Editor is recommended for its simplicity. It provides a Files button on the toolbar for just this purpose.
Class constructorThe driver is implemented as a class. This is useful since up to eight of these chips can be used together - each with a different I2C address configured through the A0, A1 and A2 pins. Each MB85RC256V chip will be a separate instance of the class so can be controlled independently of the other chips.
The first thing to do is call the constructor. This provides an instance of the class.
Syntax:
instance = MB85RC256V(I2C_Addr=0x50, WP=None)
Where:
I2C_Addr : The I2C address.
By default this is set to 0x50.
WP : The micro:bit pin that is connected to
the WP pin of the MB85RC256V board. The
default value of None means the
write-protect function will not be used.
Example
from fc_mb85rc256v import *
# Use the defaults
# Default I2C address (0x50).
# Write-protect won't be used.
fram = MB85RC256V()
# Change the default values:
# Pin A0 is connected to VCC.
# Pin WP is connected to micro:bit pin8.
fram = MB85RC256V(0x51, pin8)
This assumes that the file fc_mb85rc256v.py has been successfully copied to the micro:bit's filesystem as described above.
Class methodsThe MB85RC256V() class provides methods that:
- Read & writes blobs, strings, integers and float values to the memory in a controlled manner.
- Erases the entire contents of the memory.
- Maintains a pointer to the memory address to be written to or read from next.
- Returns the status of the last read or write to memory.
- Sets or releases memory write-protect if enabled in the call to the constructor.
- Returns manufacturer and device IDs.
Following is a full list of methods that are user callable. Each method is described and example use given in the sections below.
WriteBlob() | ReadBlob() | WriteStr() | ReadStr() |
WriteInt() | ReadInt() | WriteFloat() | ReadFloat() |
Clear() | SetMemPtr() | GetMemPtr() | GetStatus() |
GetBytesWritten() | WriteProtect() | ReadDeviceID() |
Memory Address Pointer
The MB85RC256V's main purpose is to supply 32KB of non-volatile memory that can be read and written to by the user. With one notable exception this is very easy to do.
This memory chip has a single flat memory addressing system accessed at the byte level. Each memory location is identified by a 16-bit address. These 16-bit memory addresses are in the range of 0x00 through to 0x7FFF (0 to 32767).
Note: If the end of the memory addressing space is reached and further read or writes occur then the memory address pointer will roll back to the start of the memory i.e. 0x00 and continue upwards from there. This may overwrite data previously written.The GetMemPtr() and SetMemPtr() methods allow the user to set or get the memory location where the next read or write action will start from.
Syntax: GetMemPtr() Returns: The memory address of the location where the next read or write action will start from. Syntax: SetMemPtr(MemPtr) Where: MemPtr : This address will be the memory location where the next read or write action will start from. It is a value in the range 0x00 to 0x7FFF In decimal this is the range 0 to 32767. Example: from fc_mb85rc256v import * fram = MB85RC256V() # Read the memory address pointer. print('Current memory location:', hex(fram.GetMemPtr())) # Change the memory address pointer. # This is the memory address where the next # read or write action will start from. fram.SetMemPtr(0x2F41) # Read the memory address pointer. print('New memory location:', hex(fram.GetMemPtr())) Output: Current memory location: 0x0 New memory location: 0x2f41
The value of these two methods will become obvious when the read and write methods are being discussed.
Reading the Status
The status consists of two important pieces of information:
- Was the last read or write operation successful?
- How many bytes were written or read?
The GetStatus() method returns a boolean value indicating whether the last read or write action was successful.
The GetByteCount() returns the number of bytes that were either written or read.
Reading and Writing to Memory
Clearing the Memory
The Clear() method writes a given byte to all 32,768 memory addresses on the MB85RC256V.
The method writes a 256-byte buffer filled with the 'clearing' byte to the FRAM in 128 separate write calls to fully cover the entire memory i.e 256 x 128 = 32768 bytes (32KB). This is much more efficient than making 32,768 write calls with a single byte at a time.
This takes approximately 3.4 seconds to complete with the I2C clock at 100KHz.
Syntax: Clear(Ch=0) Where: Ch : The byte that's written to all memory words on the FRAM chip. The value must be an integer in the range of 0 to 255. The default is 0. When the entire memory has been overwritten the memory address pointer is reset to point to the first memory byte (0x0000). Examples: from fc_mb85rc256v import * fram = MB85RC256V() # Write 0 to entire memory range. fram.Clear() # Write 0x25 to entire memory range. fram.Clear(0x25) # Get the status values. print('Successful clear? :', fram.GetStatus()) print('Number of bytes written:', fram.GetByteCount()) print('Memory address pointer:', hex(fram.GetMemPtr())) Output: Successful clear? : True Number of bytes written: 32768 Memory address pointer: 0x0
Reading and Writing Blobs
A blob is simply a stream of bytes i.e. a good way to represent binary data. The stream can be of any length; only limited by the capacity of the chip.
This driver provides the ReadBlob() and WriteBlob() methods to read and write binary data to the FRAM chip.
Syntax: ReadBlob(N, MemAddr=None) Where: N : The number of bytes to read. An integer value between 1 and 32768. MemAddr : The address of the memory to start reading from. If set to None the reading will start from the memory address stored in the memory address pointer. The default is None. Syntax: WriteBlob(Buffer, MemAddr=None) Where: Buffer : The binary data to write. It must be a bytes object or bytearray object and can contain any (practical) number of bytes. MemAddr : The address of the memory to start writing to. If set to None the write will start from the memory address stored in the memory address pointer. The default is None. Example: from fc_mb85rc256v import * from random import getrandbits fram = MB85RC256V() # Generate a buffer of 1,000 random bytes. # Set the starting memory address then write # the buffer of bytes to memory. # A checksum will also be calculated. memstart = 0x00FF fram.SetMemPtr(memstart) outBuffer = bytearray(1000) outsum = 0 for i in range(1000): byte = getrandbits(8) outBuffer[i] = byte outsum += byte fram.WriteBlob(outBuffer) # Get the status of the memory write. print('Successful blob write? :', fram.GetStatus()) print('Number of bytes written:', fram.GetByteCount()) print('Checksum :', outsum) # Read back the 1,000 bytes. # Calculate a checksum on the bytes read. # Check that checksums match. insum = 0 inBuffer = fram.ReadBlob(1000, memstart) for e in inBuffer: insum += e # Get the status of the memory read. print('\nSuccessful blob read? :', fram.GetStatus()) print('Number of bytes read:', fram.GetByteCount()) print('Checksum :', insum) print('Do checksums match? :', outsum == insum) Output: Successful blob write? : True Number of bytes written: 1000 Checksum : 126587 Successful blob read? : True Number of bytes read: 1000 Checksum : 126587 Do checksums match? : True
Reading and Writing Strings
This driver makes reading and writing strings with a length up to 255 characters very easy. Most of the underlying detail is hidden from the user.
The user doesn't need to keep track of string lengths as this is handled automatically. The string length is stored with the string in the memory. Thus the length of the string doesn't need to specified when reading it back.
If the string has a length greater than 255 characters it could be either (1) broken down into smaller strings with the slice operator or (2) handled as a blob.
The ReadStr() and WriteStr() methods are provided to read and write strings to the FRAM chip.
Syntax: ReadStr(MemAddr = None) Where: MemAddr : The address of the memory to start reading from. If set to None the reading will start from the memory address stored in the memory address pointer. The default is None. Syntax: WriteStr(String, MemAddr=None) Where: String : The string to be written to FRAM memory. The string length cannot be greater than 255 otherwise it will be truncated. MemAddr : The address of the memory to start writing to. If set to None the write will start from the memory address stored in the memory address pointer. The default is None. Example: # This program writes the first verse # of 'Mary had a little lamb' nursery # rhyme to FRAM memory. # It is then read back from the memory # and displayed in the REPL. from fc_mb85rc256v import * fram = MB85RC256V() # Build the nursery rhyme strings # storing each line as an element # in a list. poem = list([None]) * 6 poem[0] = 'Mary had a little lamb,' poem[1] = 'Its fleece was white as snow.' poem[2] = 'It followed her to school one day,' poem[3] = 'Which was against the rules;' poem[4] = 'It made the children laugh and play,' poem[5] = 'To see a lamb at school.' # Set the memory address to start writing to. fram.SetMemPtr(0x00) # Write the strings to memory count = 0 for s in poem: fram.WriteStr(s) count += 1 # Reset the memory address ready for reading. fram.SetMemPtr(0x00) # Read the strings from FRAM memory # and display to the REPL. for i in range(count): # Must use decode() method to convert # bytes object to a string. print(fram.ReadStr().decode()) Output: Mary had a little lamb, Its fleece was white as snow. It followed her to school one day, Which was against the rules; It made the children laugh and play, To see a lamb at school.
Reading and Writing Integers
This driver makes reading and writing integers very easy. Most of the underlying detail is hidden from the user.
Integers are efficiently packed and unpacked using methods from the ustruct module. Integers are written as 4-byte signed integers.
This means that integers must be in the range -2,147,483,648 to 2,147,483,647. An attempt to write larger or smaller integers won't throw an exception but will lead to unexpected values being written to FRAM.
The ReadInt() and WriteInt() methods are provided to read and write integers to the FRAM chip.
Syntax: ReadInt(MemAddr=None) Where: MemAddr : The address of the memory to start reading from. If set to None the reading will start from the memory address stored in the memory address pointer. The default is None. Syntax: WriteInt(Integer, MemAddr=None) Where: Integer : The integer value written to FRAM memory. MemAddr : The address of the memory to start writing to. If set to None the write will start from the memory address stored in the memory address pointer. The default is None. Example: # This program writes 7,000 random integers # to FRAM. A checksum is calculated. # The 7,000 random integers are read back with # a checksum also being calculated. # The two checksums are compared to determine that # what was written to FRAM was correctly read back. from fc_mb85rc256v import * from random import randrange fram = MB85RC256V() # Set the starting memory address then write # 7,000 random integers (0 to 200,000) to # FRAM memory. A checksum will also be calculated. outsum = 0 # Set the first memory address to be written. fram.SetMemPtr(0x00) # Write the integers to FRAM. for i in range(7000): num = randrange(200000) fram.WriteInt(num) outsum += num # Read back the 7,000 integers from FRAM # and calculate a checksum. # Set the first memory address to be read. fram.SetMemPtr(0x00) insum = 0 for i in range(7000): num = fram.ReadInt() insum += num # Do both checksums match? print('Do the checksums match? :', outsum == insum) Output: Do the checksums match? : True
Reading and Writing Floats
Reading and writing floats to the FRAM memory is easy. As occurs for string and integer read/writes this driver hides most of the underlying technical details.
Floats are efficiently packed and unpacked using methods of the ustruct module. The micro:bit's MicroPython flavour doesn't support double precision. This means that all float values are packed into 4-bytes.
The ReadFloat() and WriteFloat() methods are provided to read and write floats to the FRAM chip.
Syntax: ReadFloat(MemAddr=None) Where: MemAddr : The address of the memory to start reading from. If set to None the reading will start from the memory address stored in the memory address pointer. The default is None. Syntax: WriteFloat(Float, MemAddr=None) Where: Float : The float value written to FRAM memory. MemAddr : The address of the memory to start writing to. If set to None the write will start from the memory address stored in the memory address pointer. The default is None. Example: # This program writes 7,000 random floats # to FRAM. A checksum is calculated. # The 7,000 random floats are read back with # a checksum also being calculated. # The two checksums are compared to determine that # what was written to FRAM was correctly read back. from fc_mb85rc256v import * from random import random fram = MB85RC256V() # Set the starting memory address then write # 7,000 random floats (0.0 to 1.0) to FRAM # memory. A checksum will also be calculated. outsum = 0 # Set the first memory address to be written. fram.SetMemPtr(0x00) # Write the floats to FRAM. for i in range(7000): num = random() fram.WriteFloat(num) outsum += (int(num * 10000)) # Read back the 7,000 floats from FRAM # and calculate a checksum. # Set the first memory address to be read. fram.SetMemPtr(0x00) insum = 0 for i in range(7000): num = fram.ReadFloat() insum += (int(num * 10000)) # Do both checksums match? print('Do the checksums match? :', outsum == insum) Output: Do the checksums match? : True
Write Protecting the Memory
The WP pin on the MB85RC256V is held LOW with an internal pull-down resistor. In this default state the memory can be freely written i.e. it is not write-protected.
The entire memory becomes write-protected if the WP pin is pulled HIGH and held in this state. This means that an error will occur if any attempts to write to memory are made. The memory address pointer can still be written as all read operations to memory are still allowed.
The WriteProtect() method is used to set and reset memory write protection. Write-protection is only enabled if the WP pin is physically connected to a digital pin on the micro:bit.
Syntax: WriteProtect(On =False) Where: On : Boolean value that indicates whether write-protection should be turned on or off. A value of True causes write-protection to be enabled while False disables it. The default is to turn off write-protection. Example: from fc_mb85rc256v import * # WP pin is connected to micro:bit's pin8. fram = MB85RC256V(WP=pin8) # Turn on write-protection. fram.WriteProtect(True) # Write to memory address pointer. # This will not cause an error. fram.SetMemPtr(0x00) print('Memory address pointer:', hex(fram.GetMemPtr())) # Attempt to write to memory. # This will cause an error. fram.WriteStr('Hello World') Output: Memory address pointer: 0x0 Traceback (most recent call last): File "main.py", line 13, in <module> File "fc_mb85rc256v.py", line 125, in WriteStr File "fc_mb85rc256v.py", line 100, in WriteBlob OSError: [Errno 19] ENODEV
Attempting to write to write-protected memory throws an OSError MicroPython exception as the above example shows. The driver does not handle this exception; that is left up to the discretion of the user.
Reading the Device ID
Each MB85RC256V chip has a manufacturer's ID and Product ID 'burnt' into it during manufacture. The ReadDeviceID() returns the manufacturer's ID and the Product ID in a tuple.
The genuine Fujitsu MB85RC256V chip returns:
- Manufacturer's ID : 0xA
- Product ID : 0x5
Syntax: ReadDeviceID() This method returns a tuple with two elements. The first element contains the manufacturer's ID. The second element contains the chip ID. Example: from fc_mb85rc256v import * fram = MB85RC256V() id = fram.ReadDeviceID() print("Manufacturer's ID:", id[0]) print('Product ID:', id[1]) Output: Manufacturer's ID: 0xa Product ID: 0x5
Example
This program will use a Sensirion SHT40 relative humidity and temperature sensor chip mounted on a breakout board to take 3,000 temperature readings in °C. Each temperature value will be stored as a float in the memory of the MB85RC256V.
Mean (average), minimum, maximum, standard deviation and a 95% confidence interval will be calculated across the 3,000 readings and reported to the REPL.
The calculation of the standard deviation by necessity involves reading the 3,000 temperature measurements back from the MB85RC256V's memory.
DriversThe following driver module files need to be copied to the micro:bit's filesystem:
- MB85RC256V : fc_mb85rc256v.py
- SHT40 : fc_sht4x.py
The MB85RC256V and SHT40 are I2C devices so the connections are the same for both of them.
- Connect VCC to micro:bit's 3.3V.
- Connect GND to micro:bit's GND.
- Connect SCL to micro:bit's pin19.
- Connect SDA to micro:bit's pin20.
Copy this code to the MicroPython editor (e.g. the Mu editor) then flash it to the micro:bit.
# This program takes 3,000 temperature
# readings from an SHT40 sensor and
# stores them on a MB85RC256V FRAM
# memory chip.
# Some basic statistics are calculated
# on the temperature dataset and reported.
from fc_mb85rc256v import *
from fc_sht4x import *
from microbit import sleep
from math import sqrt
fram = MB85RC256V()
sht40 = SHT4X()
samples = 3000
Z = 1.96
# Set FRAM memory location for
# first write.
fram.SetMemPtr(0x00)
# Take 3,000 temperature readings
# with a 100ms interval between
# readings. Store them to the FRAM.
sum = 0
for i in range(samples):
# Read() method returns a tuple
# with temperature and humidity
# readings. The first element
# of the tuple is the temperature.
value = sht40.Read()[0]
fram.WriteFloat(value)
sum += value
sleep(100)
# Calculate the mean.
mean = sum/samples
# Set FRAM memory location back
# for the first read.
fram.SetMemPtr(0x00)
# Calculate standard deviation,
# minimum, maximum and
# 95% Confidence Interval values.
# Calculating standard deviation involves
# reading back all values from the FRAM.
sumdif = 0
minimum = 100
maximum = -100
for i in range(samples):
value = fram.ReadFloat()
if value < minimum:
minimum = value
if value > maximum:
maximum = value
sumdif += ((mean-value)**2)
# Standard deviation calculation.
SD = sqrt(sumdif / samples)
CI = Z * (SD / sqrt(samples))
# Report the statistics.
print('N :', samples)
print('Minimum :', minimum)
print('Maximum :', maximum)
print('Mean :', mean)
print('Standard Deviation :', SD)
print('Confidence Interval (95%) :',
round(mean, 4), '+/-',
round(CI, 4))
Typical Output:
N : 3000 Minimum : 20.97849 Maximum : 21.21881 Mean : 21.10506 Standard Deviation : 0.06079444 Confidence Interval (95%) : 21.1051 +/- 0.0022
It took just less than six minutes for the sensor to take the 3,000 temperature readings. The measurements were taken in a room that's well insulated where temperature usually only changes slowly.
Enjoy!
MB85RC256V FRAM Driver Code for micro:bit
Download as zip file
'''
MB85RC256V
32KB FRAM Memory
MicroPython driver for micro:bit
AUTHOR: fredscave.com
DATE : 2025/01
VERSION : 1.00
'''
from microbit import *
from ustruct import pack, unpack
I2C_ADDRESS = 0x50
I2C_DEVICE_ID = 0x7C
MEM_END = 0x7FFF
class MB85RC256V():
def __init__(self, I2C_Addr = I2C_ADDRESS, WP = None):
self.I2C_Addr = I2C_Addr
self.WP = WP
self.WriteProtect(False)
self.MemPtr = 0
self.Bytes = 0
self.Status = False
def _MSB(self, Addr):
return (Addr >> 8) & 0xFF
def _LSB(self, Addr):
return Addr & 0xFF
def _UpdateInternals(self, Status, Bytes, Mem):
self.Status = Status
self.Bytes = Bytes
Mem += self.Bytes
if Mem > MEM_END:
self.MemPtr = Mem - MEM_END - 1
else:
self.MemPtr = Mem
def GetMemPtr(self):
return self.MemPtr
def SetMemPtr(self, MemPtr):
if (MemPtr >= 0) and (MemPtr <= MEM_END):
i2c.write(self.I2C_Addr,
bytes([self._MSB(MemPtr),
self._LSB(MemPtr)]))
self._UpdateInternals(True, 0, MemPtr)
def GetStatus(self):
return self.Status
def GetByteCount(self):
return self.Bytes
def WriteProtect(self, On =False):
if self.WP != None:
if On == True:
self.WP.write_digital(1)
else:
self.WP.write_digital(0)
def ReadDeviceID(self):
i2c.write(I2C_DEVICE_ID,
bytes([I2C_ADDRESS << 1]),
True)
buf = i2c.read(I2C_DEVICE_ID, 3)
manufacturer = (buf[0]*256 + buf[1]) >> 4
product = buf[1] & 0b1111
return (hex(manufacturer), hex(product))
def Clear(self, Ch = 0):
buf = bytearray([Ch])
for x in range(257):
buf.append(Ch)
self.SetMemPtr(0)
for x in range(128):
addr = x * 256
buf[0] = self._MSB(addr)
buf[1] = self._LSB(addr)
i2c.write(self.I2C_Addr, buf)
self._UpdateInternals(True, 0x8000, 0)
def WriteBlob(self, Buffer, MemAddr=None):
self._UpdateInternals(False, 0, self.MemPtr)
if MemAddr == None:
mem = self.MemPtr
elif MemAddr in range(MEM_END + 1):
mem = MemAddr
else:
return
if not isinstance(Buffer, (bytes, bytearray)):
return
MSB = bytes([self._MSB(mem)])
LSB = bytes([self._LSB(mem)])
buf = MSB + LSB + Buffer
i2c.write(self.I2C_Addr, buf)
self._UpdateInternals(True, len(Buffer), mem)
def ReadBlob(self, N, MemAddr=None):
self._UpdateInternals(False, 0, self.MemPtr)
if MemAddr == None:
mem = self.MemPtr
elif MemAddr in range(MEM_END + 1):
mem = MemAddr
else:
return bytes([0])
i2c.write(self.I2C_Addr,
bytes([self._MSB(mem),
self._LSB(mem)]))
buf = i2c.read(self.I2C_Addr, N)
self._UpdateInternals(True, N, mem)
return buf
def WriteStr(self, String, MemAddr=None):
if isinstance(String, str):
String = String[0:255]
Buffer = bytearray(0)
for e in String:
Buffer = Buffer + bytes([ord(e)])
Buffer = bytes([len(String)]) + Buffer
self.WriteBlob(Buffer, MemAddr)
else:
self._UpdateInternals(False, 0, self.MemPtr)
def ReadStr(self, MemAddr = None):
String=''
buf = self.ReadBlob(1, MemAddr)
if buf[0] > 0:
String = self.ReadBlob(buf[0], self.GetMemPtr())
return String
def WriteInt(self, Integer, MemAddr=None):
if isinstance(Integer, int):
Buffer = pack('l', Integer)
self.WriteBlob(Buffer, MemAddr)
else:
self._UpdateInternals(False, 0, self.MemPtr)
def ReadInt(self, MemAddr=None):
Buffer = self.ReadBlob(4, MemAddr)
return unpack('l', Buffer)[0]
def WriteFloat(self, Float, MemAddr=None):
if isinstance(Float, (int, float)):
Buffer = pack('f', Float)
self.WriteBlob(Buffer, MemAddr)
else:
self._UpdateInternals(False, 0, self.MemPtr)
def ReadFloat(self, MemAddr=None):
Buffer = self.ReadBlob(4, MemAddr)
return unpack('f', Buffer)[0]