아두이노에는 전원을 끄더라도 저장된 데이터를 유지하도록 플래시 메모리가 장착되어 있다. 흔히 아두이노 IDE 로 프로그래밍을 하고, 바이너리 이미지를 아두이노 쪽으로 업로드 시키면, 자동으로 플래시 메모리에 Write 하게 된다.
여기서는 IDE 에 의해서가 아닌 프로그래밍을 통해 플래시 메모리에 데이터를 저장하는 방법을 알아본다. 기술된 내용은 '스케치로 시작하는 아두이노 프로그래밍' 이라는 책에서 발췌한 것이다.
PROGMEM 지시문
데이터를 플래시메모리에 저장하려면 PROGMEM 라이브러리를 포함시켜야 한다.
#include <avr/pgmspace.h>
위 라이브러리를 추가하면, PROGMEM 키워드와 pgm_read_word 함수를 사용할 수 있다. 아두이노 소프트웨어에 포함되어 있는 이 라이브러리는 공식적으로 지원되는 아두이노 라이브러리이다.
PROGMEM 을 사용할 때는 특별한 PROGMEM 스타일의 데이터 유형을 사용해야 한다. 하지만 아쉽게도 char 배열의 배열은 이 데이터 유형에 해당되지 않는다.
PROGMEM 을 사용하려면 다음과 같이 PROGMEM 문자열 유형을 사용하여 각 문자열에 대한 변수를 정의한 후 모든 변수를 PROGMEM 배열 유형에 저장해야 한다.
PROGMEM prog_char sA[] = ".-"; PROGMEM prog_char sB[] = "-..."; // 이후 모든 문자에 적용 PROGMEM const char* letters[] = { sA, sB, sC, sD, sE };
스케치를 로드해서 실행해보면 RAM 기반 버전과 같은 방식으로 작동한다는 것을 확인할 수 있다. 데이터를 생성하는 방법도 특별하지만 데이터를 읽을 때도 특별한 방법을 사용해야 한다.
배열에서 모스부호 문자열을 가져오는 코드의 경우 다음과 같이 수정해야 한다.
strcpy_P(buffer, (char*)pgm_read_word(&(letters[ch - 'a'])));
이 구문은 PROGMEM 문자열을 buffer 변수에 복사하여 넣는다. 따라서 이 변수는 일반적인 char 배열로 사용할 수 있으며, 다음과 같이 전역변수로 정의해야 한다.
char buffer[6];
이 방법은 데이터가 상수인 경우에만 사용할 수 있다. 다시 말해서, 스케치를 실행하는 동안 데이터를 변경하지 않아야 한다.
EEPROM
아두이노 우노 보드의 심장에 해당하는 ATMega328 에는 1KB 의 EEPROM 이 있다. EEPROM 은 컨텐츠를 여러 해 동안 기억할 수 있도록 설계된 메모리이다.
EEPROM 을 읽거나 쓰기 위한 아두이노 명령들도 PROGMEM 과 관련된 명령들과 마찬가지로 조금 까다롭다. EEPROM 은 한 번에 1바이트씩 읽고 써야 한다.
아래 예제는 시리얼 모니터를 통해 한 자리 문자 코드를 입력받은 다음 이 문자코드를 기억하고서 시리얼 모니터에 반복해서 표시한다.
#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); }
위 예제를 테스트하기 위해 시리얼 모니터를 열고 새 문자를 입력한다. 그런 다음 아두이노 보드의 전원을 끊었다가 다시 연결한 후 시리얼 모니터를 열면 앞에서 입력했던 문자가 다시 표시될 것이다. 이는 문자가 메모리에서 사리지지 않고 남아 있었다는 것을 의미한다.
EEPROM.write 함수의 인자는 두 가지이다. 첫 번째 인수는 EEPROM 내의 위치를 나타내는 0 부터 1023 사이의 주소이고, 두 번째 인수는 해당 위치에 기록할 데이터이다. 이 데이터는 1 바이트여야 한다. 문자 한 개는 8 비트로 표현되기 때문에 괜찮지만 16 비트인 int 형 정수는 바로 저장할 수 없다.
EEPROM 에 int 형 정수 저장하기
2바이트인 int 형 정수를 EEPROM 의 위치 0과 1에 저장하는 코드는 다음과 같다.
int x = 1234; EEPROM.write(0, highByte(x)); EEPROM.write(1, lowByte(x));
이 코드와 같이 highByte 함수와 lowByte 함수를 사용하면 int 형 정수를 2바이트로 쉽게 분리할 수 있다. 아래 그림에서는 int 형 정수 1234 가 EEPROM 에 실제로 저장되는 모습을 보여준다.
EEPROM 에 저장되어 있는 int 형 정수를 읽어오려면 다음과 같이 EEPROM 에서 두 개의 바이트 정보를 읽어와서 하나의 int 형 정수로 결합해야 한다.
byte high = EEPROM.read(0); byte low = EEPROM.read(1); int x = (high << 8) + low;
여기에서는 비트 시프트 연산자인 « 연산자를 사용하여 상위 8비트를 int 형 정수의 앞쪽으로 이동시킨 후 나머지 하위 8비트를 추가하여 하나의 온전한 int 형 정수를 만든다.
EEPROM 에 부동 소수점 저장하기(공용체 사용)
EEPROM 에 부동 소수점을 저장하는 작업은 약간 더 복잡하다. 이 작업을 수행하려면 C 언어의 공용체(union)를 사용할 수 있어야 한다. 공용체는 동일한 메모리 영역에서 두 개 이상의 변수에 액세스할 수 있는 흥미로운 데이터 구조이다. 게다가 변수의 크기만 같으면(바이트 단위) 변수의 데이터 유형이 서로 달라도 문제가 되지 않는다.
다음은 float 형 변수와 int 형 변수가 동일한 2 바이트의 메모리를 참조하는 union 정의이다.
union data{ float f; int i; }convert;
공용체를 정의한 후에는 다음과 같은 방법으로 float 형 변수를 union 에 저장할 수 있다.
float f = 1.23; convert.f = f;
그런 다음에는 EEPROM 에 저장하기 위해 다음과 같이 하나의 정수를 두 개의 바이트로 분할할 수 있다.
EEPROM.write(0, highByte(convert.i)); EEPROM.write(1, highByte(convert.i));
이렇게 저장된 값을 float 값으로 다시 읽으려면 지금까지의 작업을 역순으로 수행하면 된다. 먼저 두 개의 바이트 데이터를 하나의 int 형 정수로 결합한 후 이 정수를 union 에 저장한다. 그런 다음 float 값으로 다시 읽어오면 된다.
byte high = EEPROM.read(0); byte low = EEPROM.read(1); convert.i = (high << 8) + low; float f = convert.f;
EEPROM 에 문자열 저장하기
EEPROM 에 문자열을 쓰거나 읽는 작업은 상당히 간단하다. 우선 문자열을 쓸 때는 다음과 같이 한 번에 한 문자씩 써야 한다.
char *test = "Hello"; int i = 0; while(test[i] != '\0') { EEPROM.write(i, test[i]); i++; } EEPROM.write(i, '\0');
EEPROM 에 있는 문자열을 문자 배열로 읽어올 경우에는 다음과 같은 방법을 사용할 수 있다.
char test[10]; int i = 0; char ch; ch = EEPROM.read(i); while(ch != '\0' && i < 10) { test[i] = ch; ch = EEPROM.read(i); i++; }
EEPROM 의 내용 지우기
EEPROM 에 데이터를 쓸 때는 새 스케치를 업로드하더라도 EEPROM 이 지워지지 않기 때문에 이전 프로젝트에서 사용하던 값들이 그대로 남아 있을 수 있다는 점에 유의해야 한다. 다음은 EEPROM 의 모든 내용을 0 으로 재설정한다.
#include <EEPROM.h> void setup() { Serial.begin(9600); Serial.println("Clearing EEPROM"); for(int i = 0; i <= 1023; i++) { EEPROM.write(i, 0); } Serial.println("EEPROM Cleared"); } void loop() { }
그리고 EEPROM 은 약 10만 번 정도의 쓰기 작업 이후에는 신뢰성이 떨어지기 때문에 반드시 필요한 경우에만 EEPROM 에 값을 기록해야 한다. 또한 EEPROM 은 1 바이트를 기록하는데 약 3 밀리초가 걸릴 정도로 속도가 매우 느리다.
압축
EEPROM 에 데이터를 저장하거나 PROGMEM 을 사용할 때 메모리 용량이 부족한 경우가 발생하기도 한다. 이런 경우에는 데이터를 가장 효과적으로 사용할 수 있는 방법을 찾아야 한다.
범위 압축
일반적으로 많이 사용되는 int 형과 float 형의 데이터는 각각 16비트와 32비트를 사용한다. 예를 들어, 섭씨온도는 20.25 처럼 float 형 값을 사용해서 표현할 수 있다. 그리고 float 형 값을 저장하려면 2 바이트가 필요하다. 하지만 이 값을 1바이트로 변환해서 저장할 수 있다면 메모리 공간을 2배로 활용할 수 있을 것이다.
이제 데이터의 용량을 줄여서 메모리에 저장하는 방법 중 하나를 살펴보자. 1바이트로 저장할 수 있는 양의 정수의 범위는 0부터 255까지이다. 그리고 온도의 정밀도를 약간만 희생해서 근사치를 사용한다면 float 형을 int 형으로 변환하여 소수점 이하 부분을 버릴 수 있다. 이 작업을 수행하는 코드는 다음과 같다.
int tempInt = (int)tempFloat;
tempFloat 변수에는 부동 소수점 값이 들어있다. (int)명령은 변수의 유형을 호환가능한 다른 유형으로 변환할 때 사용되는 유형변환(type cast)명령이다. 이 경우에는 float 형 값인 20.25 가 int 형 값인 20 으로 변환된다.
살펴볼 최고 온도가 섭씨 60 도이고 최저 온도가 섭씨 0 도라면 각 온도에 4를 곱한 값을 바이트로 변환한 후 저장하는 방법을 사용할 수 있다. 그런 다음 EEPROM 에서 데이터를 읽어올 때는 4로 나누어서 정밀도가 0.25 도인 값을 얻을 수 있다.
지금까지의 설명을 실제로 보여주기 위해 아래 예제에서는 특정 섭씨 온도(20.75도)를 EEPROM 에 저장한 후 이 온도를 다시 읽어와서 시리얼 모니터에 표시한다.
#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() { }
이외에도 여러가지 데이터 압축 방법이 있다. 예를 들어, 앞에서 살펴본 온도처럼 값이 느리게 변경되는 데이터라면 최초 온도를 높은 정밀도로 기록하고 나서 이후 판독되는 값부터는 변경된 온도 차이만 기록하는 방법을 사용할 수도 있다. 이런 경우에는 일반적으로 값의 차이가 크지 않기 때문에 메모리 용량도 절약할 수 있다.