AT24C256 Serial EEPROM 2015 Adrian Krag Objective In this session we will learn how to us the AT24C256 Serial EEPROM. The Arduino has very little non-volatile (doesn't forget when you turn the power off) memory. Only enough for 512 integers, or 1000 characters. That's not very much. The AT24C256 is a low cost device that can remember up to 128,000 integers or 256,000 characters. When our projects need to gather data and store it, this device can be very useful. To use this device we will be looking at several of the "wire" library commands and how to use the I2C interface. This information is very useful when using many different types of devices. Wire.h The wire library comes with the basic Arduino installation. It interfaces the Arduino with the Wire class and allows you to communicate using I2C. I2C requires 4 wires - Power (5Volts) Ground (GND), SCL (Serial Clock - A5) and SDA (Serial Data - A4) On the red Robo Arduinos, these 4 pins are together in the lower left hand corner of the board - 4 male pins in a blue base. Here we see the 4 pins connected to the Red (GND), Blue (VCC), Yellow (SDA) and Green (SCL) wires. Connect these pins directly to the wires on the device - AT24C256 or other I2C device. Add the line #include <wire.h> to your Arduino program We are ready to begin using the I2C interface to talk to the device. We will be working with the EEPROM device, but the information is useful for most any I2C device. These are the functions that are available in the TwoWire class. In this case the constructor does not require any parameters and is inserted automatically as part of the include file and is names "Wire". The last line in the include file and acts as the constructor for the class extern TwoWire Wire; It constructs a copy of the TwoWire Class and calls it Wire. Throughout our program we will precede all functions with "Wire." This will tell the program that we are accessing one of the functions listed below and that the code can be found in the Wire.cpp program from the library. Wire Functions These are the functions that the TwoWire class brings to our program. begin() requestFrom() beginTransmission() endTransmission() write() available() read() The first function to consider is Wire.begin(); The EEPROM uses the internal I2C block in the processor. To use this in communication, we must initialize it, set registers and dedicate the A4 and A5 pins to the communication functions. Wire.begin(); does this. If you have multiple I2C devices connected the Arduino, such as an LCD, real time clock, etc, each will have a begin() function. Some will require parameters. I have not evaluated all possible combinations, but in many cases, only one begin() is required. In this program, I have connected the AT24C256 and an LCD on a single Arduino. As I am using the lcd.begin(chars, lines); function, the Wire.begin() is optional since the lcd.begin() initializes the I2C port. Writing to the EEPROM To write to the EEPROM, we use the beginTransmission(Address); function. Here we must specify the address of AT24C256 device. All I2C devices have a 7 bit address. The first 4 bits are set by the type of device - the LCD first 4 bits are 0100. For the EEPROM, the first 4 are 1010. The last 3 bits of the address is a number from 0 to 7 which is defined by the connections to pins A0, A1, and A2. These can be changed by changing the shorting jumpers. The default, like the picture shows is 000. We add a leading 0 to these 7 bits to create a byte. For the LCD this address becomes 00100111 or in hex 0x27 since A0, A1, and A2 on the chip are all soldered to the VCC net. For the EEPROM 01010000 or in hex the address is 0x50. Moving the jumpers will change the last 3 digits of the address. Suppose the jumpers for A0 and A2 were switched from 0 Volts to 5 Volts. What would the address of the AT24C256 be? _____________________ The EEPROM is a memory device. To be useful we write to the memory where it is stored, then read it back at a later time when we need it. There are 2 kinds of memory used in computers and microprocessors - Volatile (memory that only saves information as long as there is power on the circuit) and non-volatile (memory that remembers even after the power is turned off). EEPROM is non-volatile memory. When we write something to the EEPROM and then unplug the Arduino, it stays in memory and will be there when we plug it back in. To write a series of bytes to the memory, we first must send a 16 bit address for the start location. We can start writing to any one of 32,768 locations. Each location can hold 8 bits, one byte of information. We need 2 bytes (16 bits) to specify the location where we want to start writing to or reading from. I2C only sends and receives 8 bits at a time, so it will require 2 writes to send the address which is a 16 bit (2 byte) number. For example if you wish to start writing at location 0x0827 We would first send a 0x08, followed with a second write of 0x27. What is the address of the last byte in the memory? ______________________________ What is the address of the first byte of memory? ______________________________ Wire.write (0x08); // Send the upper 8 bits. Wire.write (0x27); // Send the lower 8 bits. After we write the above to the EEPROM, the device knows we are going to send bytes. After we set the start address, we can write one byte at a time to the device. Each time we write a byte to the memory, the address is incremented. The first byte will be stored in memory location 0x0827. The next byte will be stored in 0x0828 and so on. Let's look at an example: In this program we #include the TwoWire class of functions with the first line of the program. #include <Wire.h> Next the program creates a global array of characters called msg[] and it is initialized with a few letters. Wire.begin(); tells the processor we will be using the I2C port and to set aside A4 and A5 for serial communications. Next we are going to send a message We start with Wire.beginTransmission(0x50); This tells the processor what address to use to get the attention of the EEPROM. Next we must set the address of the first memory we want to talk to. This can be any number between 0, and 32767 or in hex 0 to 0x7FFF. In the above example I picked a memory location at random. Finally we are ready to send the characters in the msg[] array. We send the bytes 1 at a time starting with msg[0] 'H' stored in 0x148, then msg[1] 'e' stored in 0x149, and msg[2] 'l' stored in 0x014A and so on. Note the sizeof(msg) function. This returns the number of bytes in the "msg" array. It will cause the loop to stop when the end of the message is reached. This only returns the number of bytes in the array. Each character takes one byte. If this had been an array of integers, this would have had to be 2*sizeof(array), since each integer has 2 bytes. How may bytes are in the msg[] array - Hint don't forget to count the spaces __________________________ When we start writing, and give the start address, the EEPROM keeps storing each byte that comes in the next location, until we specify that we are finished. Wire.endTransmission(); tells that we are finished. The EEPROM now stops expecting bytes to store, and is now expecting another start command Wire.beginTransmission(0x50), followed by another 2 byte address. Reading from the EEPROM To read data that has been stored in the EEPROM, we begin by sending the address where we want to read the first byte of information. We have to send the start address (2 bytes) just as we did with the write operation, followed by Wire.endTransmission(). We need to remember where we put the string if we are going to get it back. Now that I have set the start address, the program requests bytes from the memory locations using the Wire.requestFrom(address, how many); instruction requires 2 parameters. The I2C address and the number of bytes the EEPROM is supposed to send. Again, the program uses the sizeof(msg) to tell the program how many bytes to read. This instruction causes the EEPROM to send all the bytes (characters) in the string to the Arduino. The Arduino has a buffer that collects and stores the data. This is similar to the USART buffer used by the serial monitor. Notice in the for loop, the program uses Wire.available(). This function returns the number of bytes that have been received and are waiting in the buffer. Just as with Serial.available(), every time the program reads a byte from the buffer, there is one less character available and Wire.available() eventually becomes 0 ending the loop and read operations. Saving and Retrieving Numbers In the above, we worked with characters, each are one byte long. However, sometimes we want to store and retrieve numbers that are more than one byte - integers are 2 bytes, long and float variables are 4. We need to be able to break these numbers into bytes and send them one byte at a time. When retrieving them we need to get all the bytes and reassemble them. Integers and long variables are relatively easy. We can use shift function and a mask to separate the pieces into bytes for storage, then we would recombine them using the OR functions. Let's look at an example. In this program we start with a random long number 1254962917 - a big number, too big for an integer and way bigger than a byte. If we want to store this in the AT24C256 chip we will need to break it up and store it as 4 separate bytes. To do this the program begins by creating a byte array btst[4]. The program will break up the long integer into 4 bytes and store them in this array so they can be written to the memory in sequence. To break the long up into bytes we use a device called "mask" and the "&" AND function to eliminate all but part of the number. This is a bit by bit operation. Remember, the AND function carried out by "&" part of the instruction follows these rules for each bit. 1 & 1 = 1, 1 & 0 = 0, 0 & 0 = 0, 0 & 1 = 0. So, where ever the mask is 0, the "&" will make the result 0. Wherever the mask is 1, the result will be the same as the original. Try to work some problems: 1101 & 0011 = __ __ __ __ 1111 & 0101 = __ __ __ __ How did you come out? Did you get 0001 and 0101 for answers? The important thing to understand is that since FF is 11111111, then where ever there is an FF in the mask, that part of the number will not be changed. Wherever the mask is 00 the number will be set to 0s. Consider this problem using hex numbers and a hex mask: Number 0xA5463E2B & mask 0x000000FF = 0x_____________ 0xA5463E2B & 0x0000FF00 = 0x_____________ 0xA5463E2B & 0x00FF0000 = 0x_____________ Do you see how the mask will separate out the parts of the number? We also need to discuss the ">>" and the "<<" operators. These things shift the bits to the right and the left respectively. Suppose I have a hex number 0x00340000 and I shift it 8 bits to the right - Remember each hex digit is 4 bits and the computer will fill in blank 0s on the left. This number becomes 0x00003400. Can you figure out what each of these operations will do? 0x00003600 >> 8 = _______________ 0x00003600 << 16 = _______________ 0x00003600 << 8 = _______________ 0x00460000 >> 16 = _______________ 0x54000000 >> 24 = _______________ This section of the code will separate the long into 4 byte size pieces that we can written to the memory. Remember tst = 0xA5463E2B. These things are best done with numbers expressed in HEX. btst[0] = byte(tst & 0x000000FF); btst[1] = byte((tst & 0x0000FF00) >> 8); btst[2] = byte((tst & 0x00FF0000) >> 16); btst[3] = byte((tst & 0xFF000000) >> 24); Each of these bytes could be written to the memory with simple write commands. Here we are going to look at a program for storing long integers. It starts out just as we did above with the #include statement. The program defines a couple 4 byte arrays and the long integer that we have been working with. It is written in hex, but we could convert it to the decimal equivalent of 1254962917. First the program breaks the long integer into bytes as we did above. Then Wire.beginTransmission(I2C_Address); a the program sends the memory address we want to use 0x0148 - This is selected at random. Normally we would start at 0x0000 and go from there, but here we are using a different place in memory. Now the AT24C256 is waiting for us to send bytes and it will receive them and store them sequentially until the Wire.endTransmission() command. After we have stored the code in the EEPROM, we will eventually want to retrieve it an put it back together. Just as we use the mask, the "&" and ">>" to separate the number into bytes, the program uses "<<" Shift and the "|" OR function to put it back together. This code separates the big long 4 byte number in 4 - one byte pieces. Then we again send the start address and read the 4 bytes back into the Rbld[] (rebuild) array. Load the code an run it. You should see the result on the serial monitor that shows the program recovering the number correctly. Floating Point Numbers. This method works great for integers and long variables, but we cannot manipulate float numbers with masks and shifts. To break up float numbers, it is appropriate to use a "union" function. Consider the following code: First we define a float variable "tst" and initialize it to the number 345.687. Float variables are stored in a complicated scientific notation that we will not discuss here. The program also creates another float variable "ReBld" (Rebuild) which we will use to verify that the reconstruction worked. The next line is probably new. It creates a "union" and names it FtoB. The union function says that the byte array b[4] and the float f start at the same location. It specifies that they are the two different ways to access the same processor memory. Finally the "union" is given a name "Un" In setup() the program initializes the serial monitor and writes the floating point variable "tst" to the screen so we can see it. Next, the program specifies that "tst" is copied into the float part of the union with the instruction Un.f = tst; // This not only puts the float variable test into the union variable Un.f, but also puts it into the union variable Un.b[]. Remember "union" makes both Un.f and Un.b[] start at the same location in processor memory. Now the program writes the 4 bytes Un.b[] to the serial monitor with the for statement. The serial monitor displays the initial float number, and the 4 bytes that make it up. Don't be overly concerned if these 4 bytes seem to have little in common with the original number. float numbers are stored in a complex scientific notation. What's important is that we can put these number back together using the same union that took them apart. In the next line we can set ReBld = Un.f; remember Un.b[] is the same memory location as the floating point Un.f so the bytes fall into place. Let's write them to the EEPROM and read them back - Look at this program. Again, we include the Wire.h include file and it's library functions. The program defines a "tst" float variable, and assigns a random number to it. Also, another float variable that we will read from memory. In this case the program uses memory locations 0x0000 - the first address in the device as the start of the location. The program uses the "union" to allow us to access the individual bytes of the float number. Un.f = tst; copies the tst variable into the memory defined byte the float variable Un.f and since Un.b[] has the same address. we can access it as bytes. The for statement in the program writes all 4 of these bytes into EEPROM address 0, 1, 2, and 3. After all 4 are written, we Wire.endTransmission(); To read it back we must begin by sending the address. We can only do this after a Wire.beginTransmission(); this means we must end transmission before reading. An interesting thing about EEPROM memory. When we write to it, it takes a few milliseconds to actually get it written. If you try to read it to fast after writing it, you will find that it's not written correctly. The delay(100); is there to insure that the write is complete before we again try to read it back. Now we're ready to read the bytes back. The program reads them directly into the Un.b[] array. This has the effect of reading them into the Un.f float variable as well since they are in the same memory locations. Conclusions: This has been a very informative discussion. The programs have sent data via the I2C interface to the serial EEPROM memory available on the AT24C256 device. We have stored and retrieved data. The examples and questions, cover the use of ">>" and "<<" the shift functions. The "&" and "|" AND and OR functions and how they are used to break ups and reassemble integers and long variables. Also the lesson introduce the "union" functions and we saw a first look at how to do direct memory manipulations. I hope you found this informative and useful. As always, if you have any questions, please feel free to contact me Adrian Krag 707 321 6985 ak@thecije.org