June 1, 2019

Writing An Arduino Library For The 25LC256 EEPROM


This is going to be less of a tutorial and more of a general overview of the process. This project requires multiple skillsets, and I can't assume your profficiency in all of them. You may be a master of electronics, but new to C++. Likewise, you may be a master programmer, but have never engaged in an electronics project beyond the complexity of blinking an LED. Therefore, I'm just going to walk through this process, explaining as I go. Depending on your skill-level, this article may raise more questions than answers, and I'm okay with that. It is my hope that this article will serve as a main artery connecting many different areas that you can branch off of and explore on your own as your curiosity suits you.

The 25LC256 EEPROM

The 25LC256 EEPROM (Electrically Erasable Programmable Read-Only Memory), is a nonvolatile (retains information even after it loses power) memory storage chip. It stores 32 kilobytes. To put that in perspective, thats enough space to hold the Super Mario Brothers NES cartridge, but not enough to hold this webpage (even without images). I chose this chip because I had a bunch lying around. I had totally forgotten that I had learned how to use this chip from another tutorial that was included in the book Make: AVR Programming by Elliot Williams, which is an excellent book. Fortunately, that book is geared toward programming the ATMEGA chip that sits atop the Arduino, and not the Arduino itself, so this tutorial isn't completely redundant. If you ever seek to go beyond Arduino into the lower chips themselves, I highly recommend that book, and if you're reading this tutorial, you are already on that path.

Interfacing With Modules

For this project, I'm going to be writing a library to interface with the 25LC256 EEPROM with its glorious 32kB of nonvolatile memory. In general, modules are interfaced with using the following 3 concepts: commands, registers, and data.

Commands tell the module whether we want to send data, receive data, set register information, or retrieve register information.

Registers have 2 functions. They allow you to customize the settings of the module, and they tell you the current status of the module.

Data is simply information sent or received from the module to fulfill its purpose.

Communication Protocols

The modules we use with microcontrollers use communication protocols that typically come in one of three flavors: SPI, TWI, or UART.

SPI - Serial Peripheral Interface. SPI typically requires a minimum of 4 wires connected to a module. There is a line for sending, recieving, a clock signal, and a chip-selector. Each additional module can share the data and clock lines, but will need its own dedicated line for its chip-selector. The chip selector is just a way for the Arduino to tell which module is being spoken to over the shared data lines.

TWI - Two-Wire Interface (Also called I2C). TWI reduces the number of wires used by having a single data line used for both transmitting and recieving. It also eliminates the need for a chip-selector wire as individual modules are selected by sending their address over the data line. So the only two wires used are a data line and a clock signal. This protocol is beneficial when several modules are used and speed is not a priority.

UART - Universal Asychronous Reciever-Transmitter. UART is ashychronous as it does not require a clock signal like the other two. Instead, it listens for a start signal and samples the data line at a predetermined time scale (baud rate). You've already used this protocol if you've ever called the Serial.println() function to write to the Arduino serial console. This is the slowest protocol and is not able to share data lines with other modules.

Datasheets - Understanding Your Module

If this is your first time writing a library, you have hitherto had the luxery of libraries being provided for you that seem to just magically work (most of the time). Now you're headed off into the rough wilderness with nothing but a map known as the datasheet. Admittedly, datasheets are not the most interesting reading material. They are often overly complex while somehow still having insufficient explainations. They do, however, get easier to navigate with practice and understanding them is an absolutely necessary skill for someone wanting to get into advanced Arduino development, or digital electronics in general.

You do not need to read the entire datasheet from front to back, but you do need to be able to recognize the important parts. As a library writer, you need to know which communication protocol is used, the procedure and timing of issuing commands, how high and low signals on the pins affect behavior, what register settings are available, how those settings affect the module, and how to query status registers to understand the state of the module. There also may be additional pertinent information, for example, how long it takes a module to power-up before its safe to use. You also want to be aware of limitations, such as, the maximum voltage a certain pin tolerates. You can download the datasheet for the 25LC256 here.

SPI - More In Depth

A quick glance at the datasheet tells us this module uses SPI, so now is a good time to go a little more in depth. SPI uses a minimum of 4 wires.

MOSI - Master-out-slave-in. This is information sent from the microcontroller to the module.

MISO - Master-in-slave-out. This is information sent from the module to the microcontroller.

SCK - Serial-clock. This is the drum beat that provides the timing of when the HIGH and LOW signals that represent 1's and 0's are to be shifted across the data lines.

CS (or SS) - Chip-Select (or Slave-Select). Signals a module that it should listen for incoming information. The chip-select can be used very differently depending on the module.

It would be highly beneficial to read up on how SPI works at the lowest level. For this article, you can get by knowing that SPI transmits and recieves at the same time, everytime. Whatever area of memory holds the bytes you are sending out will be overwritten by incoming bytes. Not all bytes sent or received are meaningful. For example, I may be sending a 1-byte command to the module. As a side-effect, I receive 1-byte from the module, but this byte is meaningless. Next, I intend to receive a byte of data from the module. I send a byte, which is meaningless, in order to receive a byte back from the module. If this seems confusing, spend a little more time researching SPI.

25LC256 Pinout

We refer back to the datasheet to find out what each of the 8 pins on the 25LC256 does.

1. CS - Chip-Select-Not. We append the "Not" to the name when there is a bar above the name. This indicates that the pin uses inverse logic. This means that when we want to select the chip, we send a LOW signal. When we want the chip to be idle, we send a HIGH signal.

2. SO - Serial Out. SO is shortened from MISO. I'm not sure why the datasheet omitted the MI. The important thing to know is that this is where we connect the MISO line.

3. WP - Write-Protection-Not. The 25LC256 has 2 safety measures to ensure data isn't accidentally written. One is this pin, and the other is register based. According to the datasheet, only one of the safety mechanisms need to be disabled to allow for writing. Because this pin uses inverse logic, a LOW signal on this pin engages the write-protection.

4. Vss - Ground. I'm not an electrical engineer so I don't know the reasons why there are different acronymns such as VSS and VDD depending on the chip. Just know this is ground.

5. SI - Serial In. This is the MOSI line.

6. SCK - Serial Clock.

7. HOLD - Hold-not. This pin is used to suspend operations on the 25LC256, which resume as soon as the pin returns HIGH (remember, inverse logic).

8. Vcc - Source. According to the datasheet, the 25LC256 will operate on voltages from 2.5 to 5.5V.

Arduino SPI Library

Now we need to figure out how to use SPI on the Arduino side. The Arduino SPI library can be found here. I'm using an Arduino Uno to test my library, so I included this relevant excerpt from the table. This tells each SPI-related pin so we can link them to their appropriate counterpart on the 25LC256.

Uno 11 or ICSP-4 12 or ICSP-1 13 or ICSP-3 5V

The SPI page also gives the descriptions of the functions to use SPI. We will cover the essential ones here.

begin() Initializes the SPI bus by setting SCK, MOSI, and SS to outputs, pulling SCK and MOSI low, and SS high.

beginTransaction(settings) Initializes the SPI bus using the given settings (provided by the next function).

SPISettings(speedMaximum, dataOrder, dataMode) This function creates the settings object that is passed to the former function. speedMaximum is the maximum speed rated in Hz. dataOrder is the byte-order. Look up big-endian vs. little endian for an explanation on byte-order.. dataMode is which one of the 4 SPI modes are being used. Up until writing this article, I didn't even know SPI had different modes, so unless the datasheet says otherwise, I would guess its okay to use SPI_MODE0.

endTransaction() Stops using the SPI bus. Normally this is called after de-asserting the chip select, to allow other libraries to use the SPI bus.

transfer(byte) or transfer(buffer, size) is based on a simultaneous send and receive: the received data is returned. In case of buffer transfers the received data is stored in the buffer in-place (the old data is replaced with the data received).

transfer16(uint16) or transfer16(buffer, size) is the same as above, but using 16-bit numbers instead of a single byte.

If this seems a little complicated, worry not. It will make more sense in the code. In fact, it is not absolutely necessary to use all of these functions. beginTransaction(), endTransaction(), and SPISettings() are newer additions to the library. My guess is that their purpose is to allow multiple SPI devices to play well with each other without having to get into low-level register settings. If your Arduino IDE doesn't recognize these functions (because its older), you can either update your IDE or use the SPI library in the old style. As of this writing, there are still many (most) examples in the old style.

25LC256 Commands and Procedures

A command, also called an instruction, is simply a number that is sent to the module to inform it to take some kind of action. Commands are usually described in the datasheet as either binary or hexadecimal numbers. Here, we are using binary (recall that C prefixes binary numbers with 0b). I also prefixed every command macro with M25LC256 so that there is no way there is a naming conflict if a future user is importing multiple libraries (READ is a common instruction in SPI modules).

/* Commands */
#define M25LC256_READ    0b00000011 /* Read command */
#define M25LC256_WRITE   0b00000010 /* Write command */
#define M25LC256_WRDI    0b00000100 /* Reset write-enable latch */
#define M25LC256_WREN    0b00000110 /* Set write-enable latch */
#define M25LC256_RDSR    0b00000101 /* Read STATUS register */
#define M25LC256_WRSR    0b00000001 /* Write STATUS register */

Understanding The Status Register

It is often that settings need to be configured in modules before they are ready to use. This table describing the 25LC256 Status register is taken directly from the datasheet. If you're not used to reading datasheets, this can seem very confusing.

Status Register
W/R = writable/rewritable. R = read-only.

The status register consists of 1 byte. Each choice in the register is a bit, which acts like an on/off switch. There are 8 bits in a byte so their are 8 options (labeled 0-7). We will go through and break this table down by row. Row 1, the top row, is 0-7, numbering the bits. Row 2 is showing whether each bit is writable (W/R), or read only (R). Row 3 is labeling each bit by its function. Which I'll describe in detail.

Bit 7 is WPEN. This stands for "Write-Protection Enabled". This part can get a little confusing as the 25LC256 has two write-protection mechanisms: a physical pin and a register setting. According to the datasheet, if either mechanism is disabled, the entire write-protection system is disabled. Therefore if bit 7 is set to 0, write protection is disabled.

Bits 4-6 are unused.

Bit 2 and 3 are used to specify write-protection for specific blocks (segments of memory and organized into blocks). I won't go into much detail concerning this as I simply disable these machanisms for simplicity. Consult the datasheet if you wish to know more.

Bit 1 is the Write-Enable Latch (Have you noticed a lot of write-protection yet?). Before performing a write operation, this latch must be set. The latch is reset automatically after a write operation has ended.

Bit 0 is the Write-In-Progress bit. We can use this to check if the 25LC256 is busy before we attempt to send further commands (which may get ignored if the 25LC256 is still busy).

Read Status Register

We start by selecting the 25LC256 by writing LOW to the CS pin. Next we start the SPI session. The RDSR (Read Status Register) command is then sent to the 25LC256. After this, the byte from the status register is ready to be shifted out to us. Remember though, with SPI we must send data to receive data. So, in order to receive the status byte, we send out a dummy byte. It doesn't matter what the dummy byte is as the 25LC256 will simply discard it. In this case, we just send 0. Following that, we end the SPI session and signal the 25LC256 that we are done with this transaction by writing HIGH to the CS pin.

uint8_t M25LC256::readStatus() {
  uint8_t status = 0;
  digitalWrite(M25LC256_NOT_CS, LOW);   /* Chip select */
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); /* No more than 10MHz, MSB first, SPI 0 */

  SPI.transfer(M25LC256_RDSR);          /* Read status register command */
  status = SPI.transfer(0);             /* Send dummy byte to receive status */

  digitalWrite(M25LC256_NOT_CS, HIGH);  /* Deselect chip */

  return status;

Now we have that status information is stored in the variable named status. But how do we extract the bits to view the individual settings? The answer is to use bitwise operations. By either luck or design, we won't need to manipulate individual bits for this project. Since were not using any of the advanced write-protection functionality, those bits will be set to zero. The only bit that could potentially be one is the Write-In-Progress bit, which just happens to be the only bit we care about checking. So there are only two possiblities. Either the whole byte is zero (write not in progress), or the byte is nonzero (write in progress). However, it is still crucial to understand bitwise operations if you're going to be interfacing with other other modules so I would highly recommend researching more into it.

Write Status Register

We select the 25LC256 by writing LOW to the CS pin. Then we start the SPI session and send the WRSR (Write Status Register) command. Immediately following the command, we send a single byte that represents the individual bits settings for the status register (remember the bit settings are done using bitwise operations). Following that, we end the SPI session and signal the 25LC256 that we are done with this transaction by writing HIGH to the CS pin.

void M25LC256::writeStatus(uint8_t status) {
  digitalWrite(M25LC256_NOT_CS, LOW);   /* Chip select */
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); /* No more than 10MHz, MSB first, SPI 0 */

  SPI.transfer(M25LC256_WRSR);  /* write status register command */
  SPI.transfer(status);         /* Send status byte */  

  digitalWrite(M25LC256_NOT_CS, HIGH);  /* Deselect chip */   

Read Sequence

We select the 25LC256 by writing LOW to its CS pin. Then we start the SPI session and the read command is then sent to the 25LC256. Next, the 16-bit address is sent, MSB first (Most-significant bit. Again, if this isn't familiar, lookup Little-endian vs Big-endian). After this transfer, data will be shifted out. We can shift this data out by sending a dummy byte. Remember, with SPI, we have to give bytes to get bytes back. It doesn't matter what we send because the 25LC256 will simply discard it. If we want to keep reading bytes from that address onward, we can continue sending dummy bytes. The 25LC256 automatically increments its address pointer. Finally the read operation is ended by writing HIGH to the CS pin.

void M25LC256::read(uint16_t address, uint16_t nBytes, uint8_t *buffer) {
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); /* No more than 10MHz, MSB first, SPI 0 */
  digitalWrite(M25LC256_NOT_CS, LOW);   /* Chip select */
  SPI.transfer(M25LC256_READ);          /* READ command */
  SPI.transfer16(address);              /* Address */
  SPI.transfer(buffer, nBytes);         /* Read in data */

  digitalWrite(M25LC256_NOT_CS, HIGH);  /* Deselect chip */

Write Sequence

Life was so easy until this part of the tutorial. The write sequence requires some extra steps to disable the write-protection machanism. On top of that, there are restrictions on where and how many bytes are written. The memory is divided up into 64 byte sections called pages. A write sequence cannot cross page boundaries. If you need to write more than 64 bytes, or if you need to write across a page boundary, you must stop the write sequence and start a new one to continue writing. This actually isn't that hard to deal with if you are just writing a simple write() function and leaving it up to the user to deal with the restrictions, but we're not doing that. That would not make a good user-friendly Arduino library. The burden is ours.

Lets start with the function that writes within page boundaries and we'll work out how to jump boundaries later with a broader function. We'll call this function writeInPage. It takes a 16-bit address, the number of bytes to transfer, and the buffer location from which to transfer.

We start by selecting the 25LC256 by setting CS to LOW. Then we start the SPI session and send the WREN (Write-enable) command. Now, before we can actually write, we have to disable the write-protection by toggling the CS to HIGH then back to LOW. Then we send the WRITE command, followed by the 16-bit address. After that, we send the data. Finally, we close the SPI session and deselect the chip by setting CS to HIGH.

void M25LC256::sendInPage(uint16_t address, uint16_t nBytes, uint8_t *buffer) {
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); /* No more than 10MHz, MSB first, SPI 0 */
  digitalWrite(M25LC256_NOT_CS, LOW);   /* Chip select */

  SPI.transfer(M25LC256_WREN);          /* WRITE ENABLE command */
  digitalWrite(M25LC256_NOT_CS, HIGH);  /* Toggle chip select */
  digitalWrite(M25LC256_NOT_CS, LOW);
  SPI.transfer(M25LC256_WRITE);         /* WRITE command */
  SPI.transfer16(address);              /* Address */
  SPI.transfer(buffer, nBytes); /* Write data */

  digitalWrite(M25LC256_NOT_CS, HIGH);  /* Deselect chip */

  while (readStatus() & M25LC256_WIP); /* Don't return until write operation complete */

We end this function with a while loop that ensures that this function won't end until the write operation is done. This is a crude way of ensuring that any of the functions can be called back-to-back without the user having to worry about checking the status register. This works by reading the byte from the status register and using a bitwise & (AND) operation to compare it to the M25LC256_WIP macro, which evalutates to TRUE if the WIP bit is set to 1 (meaning there is a write in progress).

The next function takes the same arguments as writeInPage. In fact, it acts as a wrapper for writeInPage, handling jumping across the page boundaries.

We start by initializing bytesLeft to the total bytes to be sent, nBytes.

pageBytesLeft is the number of bytes before hitting a page boundary, starting from the address. We calculate is by using the modulo operation address % M25LC256_PAGE_SIZE. The macro, M25LC256_PAGE_SIZE, is 64 as dicussed earlier. So the modulo operation returns how many bytes the address is above a page boundary. Then we subtract that from M25LC256_PAGE_SIZE, giving us the number of bytes from the address until the next boundary.

The while loop continues as long as the total number of bytes remaining is larger than a page size. Any remaining bytes less than a page size are sent after the loop. The loop works by first calling sendInPage() to write however many bytes it takes to get the next boundary (assuming there is enough bytes overstep a boundary). After this initial iteration, pageBytesLeft is set to equal the page size, 64 bytes, and all subsequent calls to sendInPage() send the remaining data in 64 byte sections. If there is not 64 bytes remaining, the loop is exited and any remaining bytes are sent in the sendInPage() call immediately following the loop.

void M25LC256::write(uint16_t address, uint16_t nBytes, uint8_t *buffer) {
  int bytesLeft = nBytes; /* Total bytes left to send */
  uint16_t pageBytesLeft = M25LC256_PAGE_SIZE - address % M25LC256_PAGE_SIZE; /* Remaining bytes before hitting page boundary */

  /* This loop constitutes a write cycle which ensures no more than 
     *  64 bytes is written in a single cycle and to ensure data
     *  isn't trying to be written over page boundaries.
  while (bytesLeft > pageBytesLeft) {
    sendInPage(address + nBytes - bytesLeft, pageBytesLeft, buffer + nBytes - bytesLeft);
    bytesLeft -= pageBytesLeft;
    /* We may start in the middle of a page which is why we had to calculate the pageBytesLeft
     *  After the first page is finished, every subsequent page will simply be a full 64 bytes.
    pageBytesLeft = M25LC256_PAGE_SIZE;
  sendInPage(address + nBytes - bytesLeft, bytesLeft, buffer + nBytes - bytesLeft); /* Send remaining bytes that don't fill up complete page */

Wrapping It Up

We've covered the core functionality of how the library works. You can download the full library here to see how all the pieces fit together. It is important to note the structure of downloadable library as this is what the Arduino environment requires of libraries. The library itself must be composed of a .cpp and .h file, with any example sketches (which there should be) in a subfolder named examples. If you're new to Arduino, you may never have written code outside of the Arduino environment so a .cpp and .h file may be foreign to you. This is a standard C++ convention. You will need a code editor to edit/view .cpp and .h files. There are several excellent ones out there free to download. I usually use notepad++ on the rare occasions that I'm editing code on Windows.

If you're having trouble with this part, there are several great tutorials out there and how to create and structure an Arduino library. I was going to include that with this tutorial, but I'm worried this tutorial is already a bit too ambitious on the scope of content its attempting to cover.

This tutorial was written somewhat haphazardly over the course of a few months, so it is bound to have errors or at least sections that require further clarification. Please contact me with any questions or corrections. I hope you found it helpful and good luck on your projects!