반응형

아두이노는 스케치에 저장된 변수와, 문자 열등의 데이터를 아두이노 초기화 시 Flash memory에서 읽어  SRAM에 저장하여 코드 실행 시 빠르게 처리될 수 있도록 하고 있다. 하지만 아두이노 우노의 경우 SRAM의 크기가 2048 bytes로써 그렇게 크다고 할 수 없다. 통신용 라이브러리의 경우 보통 300 ~ 500 bytes 정도의 통신용 버퍼가 필요하고 그 버퍼로 SRAM을 사용한다고 한다. 비단 통신용 라이브러리뿐만 아니라 각 라이브러리들에는 눈에 보이지 않는 라이브러리 내부 변수들이 있고 이러한 변수들 역시 SRAM의 공간을 차지하게 된다. 사용하는 라이브러리가 많아질수록 SRAM의 여유 공간은 줄어들고 코드 실행에 필요한 충분한 메모리 여유 공간을 확보하지 못한다면 메모리 에러(stack error) 또는 무한 리부팅을 경험하게 된다. 이러한 오류 발생을 방지하기 위해서는 스케치를 작성할 때 최대한 메모리를 확보하도록 코드를 짜주어야 하는데 여러 방법 중에 PROGMEM과 F() 매크로를 사용하여 상수(const, 고정된 값)로써 읽기만 하는 데이터는 Flash memory에서 바로 읽도록 하는 게 가장 손쉬운 방법이라 할 수 있다. 

대표적인 상수로 문자열(또는 스트링)을 꼽을 수 있고 이 문자열을 PROGMEM과 F() 매크로를 사용하여 Flash memory에서 바로 읽도록 할 때 SRAM의 공간 확보에 있어 그 효과가 가장 좋다고 말할 수 있다.  

Serial.print("문자열')에 사용하는 문자열도 SRAM에 저장되었다가 시리얼 모니터에 메시지를 출력할 때 사용되게 된다. 이렇게 반복적이지 않게 어느 한 코드에서만 실행되는 문자열의 경우에는 F() 매크로를 사용하여 Serial.print(F("문자열"))로 처리를 해주면 간단하게 SRAM의 공간을 절약할 수 있게 된다.  

Serial.print(F("Write something on the Serial Monitor that is stored in FLASH")); 
Serial.print((const PROGMEM char *)("Write something on the Serial Monitor that is stored in FLASH"));

* 본래의 pgmspace.h 라이브러리에는 "#define PSTR(s) ((const PROGMEM char *)(string))"가 정의되어 있으나 아두이노 우노에서는 컴파일되지 않는다.(ESP8266 / ESP32 모듈에서는 사용 가능) 라이브러리에 통합하면서 F() 매크로와 중복된 기능이므로 생략됐을 수 있다. F() 매크로를 사용하는 경우와 매크로를 사용하지 않고 PROGMEM 지시어 "(const PROGMEM char *)(string)"을 사용하는 것은 같다고 볼 수 있다. 

 

서로 다른 코드에서 반복적으로 사용되는 문자열(전역 변수 스트링)의 경우 PROGMEM을 사용하여 Flash memory에서 바로 읽도록 하는 게 효과적이다. 또한 PROGMEM의 경우 문자열뿐만 아니라 다양한 자료형에 사용할 수 있다.  

PROGMEM을 사용하기 위해서는 pgmspace.h 라이브러리가 필요했었지만 아두이노 IDE 버전 1.0부터 자체 라이브러리에 포함되어 있어 따로 라이브러리를 불러올 필요 없이 스케치에서 Flash memory에서 바로 읽을 변수에 변수 수식어 "PROGMEM"을 사용하면 된다.  

아래 페이지에서 pgmspace.h 라이브러리에 정의된 매크로와 함수들을 살펴볼 수 있다.  
https://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html 

변수에 PROGMEM 수식어 적용하기 

아래 형식 중 하나를 사용하면 된다.

const 자료형 변수이름[] PROGMEM = {}; 
const PROGMEM 자료형 변수이름[] = {}; 
const 자료형 PROGMEM 변수이름[] = {};

RPOGMEM을 전역 변수로 사용할 경우에는 상기의 형식을 바로 사용하면 되지만 로컬 변수로 사용할 경우에는 static 키워드로 정의를 해주어야 한다. 


전역 변수로 사용 
const char str[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n"; 
const PROGMEM char str[] = "Hi, I would like to tell you a bit about myself.\n"; 
const char PROGMEM str[] = "Hi, I would like to tell you a bit about myself.\n"; 

로컬 변수로 사용 - 문자열의 경우 F() 매크로를 사용하면 간단하다.
const static char str[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n" ;
const PROGMEM static char value[] = { 36, -5, 0, -26, 99 };
const static char PROGMEM value[] = { 36, -5, 0, -26, 99 }; 

 

 

PROGMEM 지시어로 저장된 문자열 출력하기  

아두이노에서 PROGMEM 지시어로 저장된 문자열을 Serial.print("문자열") 함수를 통해 직접 출력하게 되면 읽을 수 없는 이상한 문자('⸮')가 출력되거나 아무것도 출력되지 않게 된다.

정상적으로 출력하기 위해서는 자료형 __FlashStringHelper를 이용해야 하고 이 자료형을 이용하여 아래처럼 매크로로 정의하여 문자열을 출력하고자 하는 코드에 사용할 수 있다.   

#define FV(s) ((const __FlashStringHelper*)(s))

 

아래 예제를 업로드하고 확인해 보자.

progmem_string_print_test.ino
0.00MB
#define FV(s) ((const __FlashStringHelper*)(s))

const char hello[] PROGMEM = "My Little Friend";
const char xyz[]  PROGMEM = {  // 헥사표시 문자열
  0x53, 0x61, 0x79, 0x20, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 
  0x74, 0x6f, 0x20, 0x4d, 0x79, 0x20, 0x4c, 0x69, 0x74, 0x74, 
  0x6c, 0x65, 0x20, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x00 
}; 
const byte xyz1[] PROGMEM = {  // 헥사표시 문자열
  0x53, 0x61, 0x79, 0x20, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 
  0x74, 0x6f, 0x20, 0x4d, 0x79, 0x20, 0x4c, 0x69, 0x74, 0x74, 
  0x6c, 0x65, 0x20, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x00 
}; 

void setup() {
  Serial.begin(9600);
  Serial.println(F("Hello World!"));  // F() 매크로 사용
  Serial.println(hello);              // PROGMEM 변수 직접 읽기 - 오류
  Serial.println(FV(hello));          // 정의된 FV(s) 매크로 이용 자료형 __FlashStringHelper로 변환
  Serial.println((const __FlashStringHelper *)hello); //자료형 __FlashStringHelper 직접사용
  const __FlashStringHelper *str = (const __FlashStringHelper *)hello; // __FlashStringHelper 포인터 이용 저장
  Serial.println(str);
  String str1 = (const __FlashStringHelper *)hello; // 스트링 변수에 저장
  Serial.println(str1);
  Serial.println((const __FlashStringHelper *)xyz); // 자료형 char 문자 배열 출력
  for (int i = 0; i < sizeof(xyz1); i++) {          // 자료형 byte 문자 배열 출력
    Serial.write(pgm_read_byte(xyz1 + i));
  }
}

void loop() {
}

문자열이 아닌 자료형에 따른 매크로의 사용방법 

avr/pgmspace.h 라이브러리에 정의된 매크로를 사용하여 코드를 작성해야 한다. 

 
const uint8_t value[] PROGMEM = { 28, 0, 255, 10, 117 };                     // 1 byte 자료형

const char value[] PROGMEM = { 36, -5, 0, -26, 99 };                           // 1 byte 자료형
const uint16_t value[] PROGMEM = { 65000, 32796, 16843, 10, 11234 };  // 2 byte 자료형

const float value[] PROGMEM = { 19.0, 2.5, 22.56, -8.9, 3.5 };                 // float 자료형(4 byte)

 

자료형에 따른 매크로 

pgm_read_byte(address_short)       // 포인터 주소의 1 byte 값을 읽는 매크로
pgm_read_word(address_short)     // 포인터 주소의 2 byte 값을 읽는 매크로
pgm_read_dword(address_short)   //  포인터 주소의 4 byte 값을 읽는 매크로
pgm_read_float(address_short)      //  포인터 주소의 float 값(4 byte)을 읽는 매크로
pgm_read_ptr(address_short)         // 16비트 포인터 주소를 읽는 매크로, uint16_t

 

예) pgm_read_byte(value + 2)          //  (배열 이름(배열 주소) + 배열 인덱스) 

 

상기 매크로들은 PROGMEM 수식어가 정의된 pgmspace.h 라이브러리에 정의된 매크로들이고 사용 예제에서는 상기 매크로들을 이용하여 PROGMEM 수식어로 정의된 배열에 접근하고 값을 읽게 된다. 이때 매크로에 사용되는 매개변수는 Flash memory에 저장된 변수의 주소 값이 들어가게 된다. 배열의 인덱스를 활용하여 값을 가져오기 위해서는 상기와 같이 주소 값에 인덱스 값을 더 해주면 된다. arry[배열 인덱스] 형식은 사용할 수 없다. 

 

아래 코드를 업로드하고 테스트해보자.

progmem_print_test.ino
0.00MB
const uint8_t value1[] PROGMEM = { 28, 0, 255, 10, 117 };              // 1 byte 자료형
const int8_t value2[] PROGMEM = { 36, -5, 0, -26, 99 };                  // 1 byte 자료형
const uint16_t value3[] PROGMEM = { 65000, 32796, 16843, 10, 11234 };  // 2 byte 자료형
const float value4[] PROGMEM = { 19.0, 2.5, 22.56, -8.9, 3.5 };        // float 자료형(4 byte)

void setup() { 
  Serial.begin(9600);  
  Serial.println(pgm_read_byte(value1 + 2)); // 1 byte 자료형 읽기, value1 인덱스 2번 값 출력
  Serial.println((int8_t)pgm_read_byte(value2 + 1)); // 1 byte 자료형 읽기, value2 값을 읽어와 signed로 변환
  Serial.println(pgm_read_word(value3 + 1)); // 2 byte 자료형 읽기
  for (int i = 0; i < sizeof(value4)/sizeof(value4[0]); i++) { // float는 4바이트 자료형
    float float_val = pgm_read_float(value4 + i); 
    Serial.print(float_val);
    Serial.print(",");
  }
}

void loop() {
}

시리얼 모니터 출력 값

255 
-5 
32796 
19.00,2.50,22.56,-8.90,3.50,

 

아두이노에서 PROGMEM 지시어에 의해 저장된 배열을 PROGMEM 지시어를 사용하지 않은 일반 배열로 취급하여 "Serial.println(value[0]);"처럼 코드를 작성할 경우 정상적으로 값을 읽어올 수도 있고 값을 읽지 못하거나 엉뚱한 값을 읽어 올 수도 있다. 

 

아래 코드를 아두이노에 업로드하고 시리얼 모니터를 확인해보면 "Serial.println(value [i]);" 코드에 의한 출력 값이 정상적으로 표시되지 않는 것을 확인할 수 있다.

progmem_array_test_error.ino
0.00MB
const float value[] PROGMEM = { 19.0, 2.5, 22.56, -8.9, 3.5 }; 

void setup() {
  Serial.begin(9600);
//  Serial.println(value[0]); // 에러 테스트용 주석처리
  for (int i = 0; i < sizeof(value)/sizeof(value[0]); i++) { // float는 4바이트 자료형 
    float float_val = pgm_read_float(value + i);  
    Serial.println(float_val); // output the buffer.  
  } 
  for (int i = 0; i < sizeof(value)/sizeof(value[0]); i++) { // float는 4바이트 자료형 
    Serial.println(value[i]); 
  }
}

void loop() {
}

다시 Serial.println(value[0]); 코드의 주석을 풀고 업로드해보면 정상적으로 값이 출력됨을 확인할 수 있다. 

 

관련 글

[arduino] - 아두이노 - 와이파이 모듈 ESP01, WiFiEsp.h 라이브러리 이용 웹페이지에서 디지털핀 원격제어

[arduino] - 아두이노 - 문자열의 이해와 표현 방법

[arduino] - 아두이노 - 와이파이 매니저, ESP01 soft AP를 통해 공유기 연결용 아이디와 비밀번호 설정하기

 

+ Recent posts