When you give values to variables, the Arduino board will remember those values only as long as the power is on. The moment that you turn the power off or reset the board, all that data is lost.
In this chapter, we look at some ways to hang on to that data.
If the data that you want to store does not change, then you can just set the data up each time that the Arduino starts. An example of this approach is the case in the letters array in your Morse code translator of Chapter 5 (sketch 5-05).
You used the following code to define a variable of the correct size and fill it with the data that you needed:
char* letters[] = {
".-", "-...", "-.-.", "-..", ".",
"..-.", "--.", "....", "..", // A-I
".---", "-.-", ".-..", "--", "-.",
"---", ".--.", "--.-", ".-.", // J-R
"...", "-", "..-", "...-", ".--",
"-..-", "-.--", "--.." // S-Z
};
You may remember that you did the calculation and decided that you had plenty of your meager 2K to spare. However, if memory was a bit tight, it would be far better to be able to store this data in the 32K of flash memory used to store programs, rather than the 2K of RAM. There is a means of doing this. It is a directive called PROGMEM; it lives in a library and is a bit awkward to use.
To store your data in flash memory, you have to include the PROGMEM library as follows:
#include <avr/pgmspace.h>
The purpose of this command is to tell the compiler to use the pgmspace library for this sketch. In this case, a library is a set of functions that someone else has written and that you can use in your sketches without having to understand all the details of how those functions work.
Because you are using this library, the PROGMEM keyword and the pgm_read_word function are available. You will use both in the sketches that follow.
This library is included as part of the Arduino software and is an officially supported Arduino library. A good collection of such official libraries is available, and many unofficial libraries, developed by people like you and made for others to use, are also available on the Internet. Such unofficial libraries must be installed into your Arduino environment. You will learn more about these libraries, as well as how to write your own libraries, in Chapter 11 .
When using PROGMEM , you have to make sure that you use special PROGMEM -friendly data types. Unfortunately, that does not include an array of variable length char arrays. However, it does include access to an array of char arrays if those char arrays are of fixed size. The full program is very similar to that of sketch 5-05 in chapter 5 . You may like to open sketch 8-01 in the IDE while I highlight the differences.
There is a new constant called maxLen that contains the maximum length of a single character’s dots and dashes plus one for the null character on the end.
The structure to contain the letters now looks like this:
PROGMEM const char letters[26][maxLen] = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", // A-I
".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", // J-R
"...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.." // S-Z
};
The PROGMEM keyword indicates that the data structure is to be stored in flash. You can only store constants like this; once in the flash, the data structure cannot be changed, hence the use of const . The size of the array also has to be fully specified as 26 letters by maxLen (minus 1) dots and dashes.
The loop function is also slightly different from sketch 5-05.
void loop()
{
char ch;
char sequence[maxLen];
if (Serial.available() > 0)
{
ch = Serial.read();
if (ch >= 'a' && ch <= 'z')
{
memcpy_P(&sequence, letters[ch - 'a'], maxLen);
flashSequence(sequence);
}
else if (ch >= 'A' && ch <= 'Z')
{
memcpy_P(&sequence, letters[ch - 'A'], maxLen);
flashSequence(sequence);
}
else if (ch >= '0' && ch <= '9')
{
memcpy_P(&sequence, numbers[ch - '0'], maxLen);
flashSequence(sequence);
}
else if (ch == ' ')
{
delay(dotDelay * 4); // gap between words
}
}
}
The data may look like an array of strings, but actually internally it is stored in flash in a way that can only be accessed by the special function memcp_P , which copies the flash data into a char array called sequence that is initialized to maxSize characters in length.
The & character before sequence allows memcpy_P to modify the data inside the sequence character array.
I have not listed sketch 8-01 here, as it is a little lengthy, but you may wish to load it and verify that it works the same way as the RAM-based version.
In addition to creating the data in a special way, you also have to read the data back a special way. Your code to get the code string for a Morse letter from the array has to be modified to look like this:
strcpy_P(buffer, (char*)pgm_read_word(&(letters[ch - 'a'])));
This uses a buffer variable into which the PROGMEM string is copied, so that it can be used as a regular char array. This needs to be defined as a global variable as follows:
char buffer[6];
This approach works only if the data is constant—that is, you are not going to change it while the sketch is running. In the next section, you will learn about using the EEPROM memory that is intended for storing persistent data that can be changed.
If you have individual strings that are perhaps formatted for messages to be displayed on the serial monitor, then Arduino C provides a handy shortcut. You can just enclose the string in F() as shown in the example below:
Serial.println(F("Hello World"));
The string will then be stored in flash memory, rather than use up RAM.
The ATMega328 at the heart of an Arduino Uno has a kilobyte of electrically erasable read-only memory (EEPROM). EEPROM is designed to remember its contents for many years. Despite its name, it is not really read-only. You can write to it.
The official Arduino commands for reading and writing to EEPROM are just as awkward to use as the ones for using PROGMEM . You have to read and write to and from EEPROM one byte at a time.
The example of sketch 8-02 allows you to enter a single-digit letter code from the Serial Monitor. The sketch then remembers the digit and repeatedly writes it out on the Serial Monitor.
// sketch 8-02
#include <EEPROM.h>
int addr = 0;
char ch;
void setup()
{
Serial.begin(9600);
ch = EEPROM.read(addr);
}
void loop()
{
if (Serial.available() > 0)
{
ch = Serial.read();
EEPROM.write(0, ch);
Serial.println(ch);
}
Serial.println(ch);
delay(1000);
}
To try this sketch, open the Serial Monitor and enter a new character. Then unplug the Arduino and plug it back in. When you reopen the Serial Monitor, you will see that the letter has been remembered.
The function EEPROM.write takes two arguments. The first is the address, which is the memory location in EEPROM and should be between 0 and 1023. The second argument is the data to write at that location. This must be a single byte. A character is represented as eight bits, so this is fine, but you cannot directly store a 16-bit int .
To store a two-byte int in locations 0 and 1 of the EEPROM, you could do this:
int x = 1234;
EEPROM.write(0, highByte(x));
EEPROM.write(1, lowByte(x));
The functions highByte and lowByte are useful for separating an int into two bytes. Figure 8-1 shows how this int is actually stored in the EEPROM.
To read the int back out of EEPROM, you need to read the two bytes from the EEPROM and reconstruct the int , as follows:
byte high = EEPROM.read(0);
byte low = EEPROM.read(1);
int x = (high << 8) + low;
The << operator is a bit shift operator that moves the eight high bytes to the top of the int and then adds in the low byte.
The official Arduino way of using EEPROM is just fine if you are only using single bytes. However, as you saw with ints , this becomes more complicated for larger data types. It’s even worse for floats (4 bytes). Fortunately, there is an alternative method that uses one of the libraries that Arduino itself uses called the AVR EEPROM library. This allows you to read and write as much data as will fit into your EEPROM with single commands.
The example of sketch 8-03 uses this library to write and then read an int .
// sketch 08-03
#include <avr/eeprom.h>
void setup()
{
Serial.begin(9600);
int i1 = 123;
eeprom_write_block(&i1, 0, 2);
int i2 = 0;
eeprom_read_block(&i2, 0, 2);
Serial.println(i2);
}
void loop()
{
}
The library is actually included in the Arduino IDE, so you do not need to install anything, just include the library. The function that writes to EEPROM is called eeprom_write_block and, as the name suggests, it writes a block of memory into EEPROM. Its first parameter is a reference to the variable. In this case, this is to i1 that has been given a value of 123. There is an & in front of i1 as the function expects the parameter to be a reference to the variable’s address in memory rather than the variable’s value. The second parameter is the starting byte in EEPROM where the block should be written and the final parameter is the number of bytes to write (2 for an int ).
Reading the value from EEPROM back into a RAM variable (i2 ) is a mirror of the writing process with the same parameters.
Storing a float in EEPROM using the AVR EEPROM library is very similar to storing an int as sketch 8-04 illustrates.
// sketch 08-04
#include <avr/eeprom.h>
void setup()
{
Serial.begin(9600);
float f1 = 1.23;
eeprom_write_block(&f1, 0, 4);
float f2 = 0;
eeprom_read_block(&f2, 0, 4);
Serial.println(f2);
}
void loop()
{
}
The main difference is that this time the final parameter to eeprom_write_block and eeprom_read_block is 4 (4 bytes) rather than 2.
Writing and reading character strings into the EEPROM is also best accomplished using the AVR EEPROM library. Sketch 8-05 illustrates this with an example that reads and writes passwords from EEPROM. The sketch first displays this password read from EEPROM and then prompts you to enter a new password (Figure 8-2 ). Having set the password, you can unplug the Arduino to power it down and when you plug it back in again and open the Serial Monitor, the old password will still be there.
// sketch 08-05
#include <avr/eeprom.h>
const int maxPasswordSize = 20;
char password[maxPasswordSize];
void setup()
{
eeprom_read_block(&password, 0, maxPasswordSize);
Serial.begin(9600);
}
void loop()
{
Serial.print("Your password is:");
Serial.println(password);
Serial.println("Enter a NEW password");
while (!Serial.available()) {};
int n = Serial.readBytesUntil('\n', password,
maxPasswordSize);
password[n] = '\0';
eeprom_write_block(password, 0, maxPasswordSize);
Serial.print("Saved Password: ");
Serial.println(password);
}
The character array password has a fixed size of 20 characters that must also include the ‘\0’ end marker. In the startup function the contents of EEPROM starting at location 0 are read into password .
The loop function displays the necessary messages and then the while loop does nothing until serial communication arrives, indicated by Serial.available returning more than 0. The readBytesUntil function will then keep reading characters until the end of line character ‘\n’ is encountered. The bytes being read will be put straight into the password char array.
Because you don’t know how long a password will be entered, the result of reading the bytes is stored in n and then element n of the password is set to ‘\0’ to mark the end of the string. Finally, the new password is printed to the Serial Monitor to confirm the change in password.
When writing to EEPROM, remember that even uploading a new sketch will not clear the EEPROM, so you may have leftover values in there from a previous project. Sketch 8-06 resets all the contents of EEPROM to zeros:
// sketch 8-06
#include <EEPROM.h>
void setup()
{
Serial.begin(9600);
Serial.println("Clearing EEPROM")a;
for (int i = 0; i < 1024; i++)
{
EEPROM.write(i, 0);
}
Serial.println("EEPROM Cleared");
}
void loop()
{
}
Also be aware that you can write to an EEPROM location only about 100,000 times before it will become unreliable. So only write a value back to EEPROM when you really need to. EEPROM is also quite slow, taking about 3 milliseconds to write a byte.
When saving data to EEPROM or when using PROGMEM , you will sometimes find that you have more to save than you have room to save it. When this happens, it is worth finding the most efficient way of representing the data.
You may have a value for which on the face of it you need an int or a float that is 16-bit. For example, to represent a temperature in degrees Celsius, you might use a float value such as 20.25. When you are storing that into EEPROM, life would be so much easier if you could fit it into a single byte, and you could store twice as much as if you used a float .
One way that you can do this is to change the data before you store it. Remember that a byte will allow you to store a positive number between 0 and 255. So if you only cared about the temperature to the nearest degree Celsius, then you could simply convert the float to an int and discard the part after the decimal point. The following example shows how to do this:
int tempInt = (int)tempFloat;
The variable tempFloat contains the floating point value. The (int) command is called a type cast and is used to convert a variable from one type to another compatible type. In this case, the type cast converts the float of (for example) 20.25 to an int that will simply truncate the number to 20.
If you know that the highest temperature that you care about is 60 degrees Celsius and that the lowest is 0 degrees Celsius, then you could multiply every temperature by 4 before converting it to a byte and saving it. Then when you read the data back from EEPROM, you can divide by 4 to get a value that has a precision of 0.25 of a degree.
The following code example (sketch 8-07) saves such a temperature into EEPROM, then reads it back and displays it in the Serial Monitor as proof:
//sketch 8-07
#include <EEPROM.h>
void setup()
{
float tempFloat = 20.75;
byte tempByte = (int)(tempFloat * 4);
EEPROM.write(0, tempByte);
byte tempByte2 = EEPROM.read(0);
float temp2 = (float)(tempByte2) / 4;
Serial.begin(9600);
Serial.println("\n\n\n");
Serial.println(temp2);
}
void loop(){}
There are other means of compressing data. For instance, if you are taking readings that change slowly—again, changes in temperature are a good example of this—then you can record the first temperature at full resolution and then just record the changes in temperature from the previous reading. This change will generally be small and occupy fewer bytes.
You now know a little about how to make your data hang around after the power has gone off. In the next chapter, you will look at displays.