이번 포스트도 이전 포스트들과 마찬가지로 학생들의 질문에서 시작된다.
어떤 학생이 한 센서의 예제 코드를 찾다가 예제 코드에서 이상한 코드가 있다고 이야기를 했었다.
Serial.println(F(“Error, max timeout reached”));
위의 코드였는데 여기서 다른건 다 알겠는데 F가 뭔지에 대해 물어온 것이다.
보통 Serial.println이라고 한다면 뒤의 괄호에서 ” “가 나온다면 안의 문구를 시리얼 모니터에 출력하는 것이고 2+3과 같이 작성하면 계산된 값이 시리얼 모니터로 출력되는 함수다.
그런데 그 안에 F( )라고 되어 있는 함수 같은 것이 사용된 것이다.
이게 무엇을가.
이건 사실 F 함수라고 부르기도 하는데 실제론 F 매크로란 말이 더 맞지 않을까 싶다.
이 F가 하는 것은 문자열을 저장하는 장소를 바꾸는 역할을 한다.
일반적으로 ATmega를 기준으로 하는 아두이노는 프로그램을 저장하는 Flash 메모리와 프로그램이 동작하는 동안 데이터를 임시로 저장하는 SRAM이 존재한다.
이 두 메모리들은 ATmega328을 기반으로 보면 Flash 메모리가 32KB, SRAM이 2KB가 존재한다.
그리고 이 저장공간들은 전체 용량의 80% 이상 사용되면 문제가 발생할 확률이 높아진다.
특히 SRAM의 경우엔 용량을 철저히 관리해야 한다.
그럼 다시 본론으로 돌아와서 일반적으로 Serial.println(“Hello World”)라고 작성하면 이 내용은 어떻게 동작하게 될까.
먼저 이렇게 작성한 코드는 프로그램의 일부이기 때문에 Flash 메모리에 저장되어 있다.
하지만 아두이노가 동작하게 되면 즉, 켜지는 순간 해당 문자열을 다루기 위해 “Hello World”라는 문구를 SRAM에 미리 복사하고 필요한 순간 사용하게 된다.
이렇게 되면 가뜩이나 작은 SRAM의 용량이 부족해질 수도 있는 것이다.
특히 처음에 나타냈던 예시처럼 디버깅용 로그 같은 출력할 문자열이 많다면 더 그럴 것이다.
F 매크로는 이때 SRAM에 문자열이 복사되는 것을 막고 Flash 메모리에서 직접 읽어 출력할 수 있도록 하는 역할을 하게 된다.
간단히 말해 SRAM에 할당되는 용량을 줄일 수 있다는 말이다.
이 부분은 실제로 코드를 작성해서 컴파일을 해보면 비교할 수 있다.

그리고 이 F매크로는 실제로 아두이노 내부 소스코드에 #define F(string_literal)의 형태로 정의되어 있다.
이때 #define이 나오는데 이것도 예제 코드들을 통해 많이 접한 형식일 것이다.
일반적으로 #define을 사용하는 것을 보면 변수를 지정하는 것 대신 사용하는 경우가 많다.
예를 들어 이렇게 사용된다.
int LED = 2;
와 동일하게 사용하기 위해
#define LED 2
이런 식으로 말이다.
우리는 보통 이렇게 지정하고 LED라는 이름을 pinMode나 digitalWrite에서 사용하게 된다.
동작도 큰 차이가 없을 것이다.
하지만 두 형식에는 큰 차이가 존재한다.
먼저 간단한 차이를 말하자면 int로 선언한 것은 정수형 변수를 나타낸 것이기 때문에 다른 코드에서 값을 변경할 수 있다.
하지만 #define으로 지정한 것은 LED라는 이름을 숫자 2로 치환한 것이기 때문에 다른 코드에서 변경이 불가능하다.
이것을 위에서 말한 것과 같이 저장 공간에 빗대어 보면 변수로 선언된 것은 SRAM에 저장되어 그 값이 변경이 가능하지만 #define의 경우 별도의 저장공간을 만들어 값을 보관하지 않기 때문에 공간을 차지 하지 않는다.
그래서 #define은 자료형이라고 하지 않고 전처리기 지시문이라고 부른다.
간단히 말하자면 #define을 사용하면 변수처럼 저장하지 않고 코드가 동작하기 전에 코드 안에 있는 LED라는 이름을 숫자 2로 치환하고 시작하게 된다.
그럼 실제로 간단하게 코드를 작성해서 비교해보자.

그림을 보면 알겠지만 ‘확인’ 버튼을 눌러보면 용량이 서로 동일하다.
앞서 이야기한 것에 따르면 용량이 달라야하는데 같은 것이다.
이것은 사실 우리가 누른 ‘확인’ 버튼 때문이다.
이 ‘확인’ 버튼은 우리가 흔히 코드를 제대로 작성했는지 확인하는 용도로 사용한다라고 알고 있는데 실제로는 조금 다르다.
‘확인’ 버튼을 누르면 IDE의 아래쪽에 컴파일 중이라고 나오고 컴파일 완료라는 문구가 나타나게 된다.
이 ‘컴파일(Compile)’이라는 문구에 주목을 해야 한다.
컴파일은 사실 단순히 코드를 검증하는 것이 아닌 우리가 작성한 코드를 아두이노 같은 마이크로컨트롤러가 받아들일 수 있는 기계어로 변환하는 과정을 말한다.
그래서 ‘확인’ 버튼을 누르면 이 ‘컴파일’ 동작이 실행되고 이때 이 컴파일 동작을 실행하는 프로그램을 ‘컴파일러(Compiler)’라고 한다.
컴파일러에는 다양한 종류가 있지만 아두이노에서는 별도의 컴파일러를 우리가 선택하거나 설치하지 않기 때문에 넘어가자.
(실제로는 AVR-GCC 컴파일러를 사용한다.)
아무튼 이 컴파일은 작성한 코드를 기계어로 변환하게 되는데 이때 단순히 번역만 하진 않는다.
프로그램의 동작에 영향을 주지 않는다고 판단되는 변수나 계산을 제거하거나 더 효율적인 코드로 변형하는 최적화(Optimization)과정을 함께 수행하는 것이다.
그래서 위의 그림과 같이 코드를 작성하면 int LED = 2라고 작성되어 있는데 LED가 정수형 변수긴 하지만 아래쪽에서 변수의 값을 변형하는 코드가 따로 없기 때문에 이 최적화 과정에서 LED라는 이름을 단순히 2로 치환해서 사용하게 되는 것이다.
이러면 결과적으로 int 형으로 하나 #define으로 하나 용량이 같게 된다.
그럼 어떻게 하면 될까
당연히 컴파일러에게 우리가 명령을 내리면 된다.
‘이 변수의 값은 현재 코드만으로는 알 수 없지만, 언제든지 바뀔 수 있으니, 값을 임의로 상수로 바꾸거나 제거하지 말라’ 라는 명령을 말이다.
이 명령을 간단하게 저달하기 위해 사용하는 것이 ‘volatile’이다.
int LED = 2; 라고 되어 있는 변수에 int 앞에 volatile을 붙여서 volatile int LED = 2; 라고 한다면 LED는 밑에서 값이 변형되지 않아도 컴파일러를 거쳤을 때 변수로써 SRAM에 할당되게 되는 것이다.

여기서는 간단하게 메모리 사용량을 확인하기 위해 사용했지만 실제로 ‘volatile’은 마이크로컨트롤러를 코딩할 때 자주 사용되는 표현이니 꼭 알아두자.
특히 인터럽트(interrupt)를 사용할 때 인터럽트 함수와 일반 코드가 같은 변수를 사용할 때 자주 등장하게 되므로 아두이노나 마이크로컨트롤러 코딩을 좀 더 깊게 다루게 되면 자주 보게 될 것이다.
자, 다시 본론으로 돌아와서 int형과 #define에 대해 다시 살펴보자.
지금까지 이야기한 것으로는 int형의 경우의 공간을 차지 하니 #define을 사용한 것이 더 나아보이긴 한다.
다만, #define의 경우 실제 변수를 만드는 기능이 아니라 컴파일 전에 (위의 예시로 보자면) LED라는 문자를 숫자 2로 바꾸는 기능이기 때문에 자료형이라는 개념이 없다.
그래서 일반적인 범위 규칙을 가지지 않아서 의도치 않은 오류가 발생할 수 있다.

위의 그림과 같이 코드를 작성했을 때 setup에 #define으로 LED를 설정했는데도 loop에서도 동작하는 것을 볼 수 있다.
이처럼 #define은 해당 함수 안에서만 사용되는 것이 아니라, 정의된 위치 이후의 코드 전체에 영향을 주게 된다.
이로 인해 예상치 못한 충돌이나 잘못된 치환이 발생할 수 있다.
그래서 보통 아두이노에서는 #define 보다 ‘const’를 더 권장하고 있다.
‘const’는 단독으로 변수를 만드는 명령어는 아니고 자료형과 함께 사용하는 한정자다.
여기서 한정자(Qualifier)는 간단하게 변수의 범위나 코드의 성격을 제한하고 구체화하는 개념을 뜻한다.
이 말대로 자료형 앞에 const를 붙이면 해당 변수는 ‘읽기 전용’이 되어 버린다.
그럼, 한 번 사용해보자.

그림에서 볼 수 있는 것처럼 const로 선언하고 아래에서 변수의 값을 변형하려고 하면 ‘read-only’라는 문구가 나온다.
뭐 용량으로 따지면 실제 #define과 큰 차이는 없겠지만 변수의 범위를 유지하고 코드의 안정성과 가독성 측면에선 const가 더 유리하다.
이 부분은 아두이노 공식 홈페이지에서도 설명하고 있다.
( https://docs.arduino.cc/language-reference/en/structure/further-syntax/define/ )
그리고 const라고 하면 다양한 예제에서 사용하고 있는데 특히 이러한 형태로 많이 사용하고 있다.
const char *str = “hello”;
처음보는 사람이라면 문자형이면 char을 사용해야 하는데 char *은 뭐지라는 생각과 const가 왜 char 앞에 있지라고 생각하게 될 것이다.
여기서 const는 당연히 데이터를 변경할 수 없게 앞에 넣은 제한자이고 뒤에 나온 *은 포인터를 뜻한다.
포인터라고 하는 것은 C언어에서 많이 사용되는 것으로 일반적인 변수는 값을 저장하지만 포인터는 값이 아닌 값이 저장된 메모리의 주소를 저장하는 변수다.
그래서 위와 같이 작성하면 “hello”라는 데이터가 메모리 어딘가에 저장이 되고 str은 이 문자열의 첫 번째 문자인 ‘H’가 저장된 주소를 가리킨다.
이걸 전체로 본다면 str은 수정할 수 없는 char 데이터를 가리키는 포인터라고 할 수 있다.
이 경우에는 지정된 주소는 변경 가능한데 문자열의 값으 변경할 수 없다.
그래서 str = “Arduino” 이렇게는 사용하면 “Arduino”가 어딘가 메모리에 저장되고 그 중 ‘A’가 저장된 주소가 str에 저장 된다.
하지만 str[0] =’A’와 같이 문자열을 변경하려고 하면 변경이 불가능 하다.

그리고 만약 코드가 이렇게 되어 있다면,
char *const str = “hello”;
문자열의 값은 변경이 가능한데 str에 저장된 주소 값의 변경이 불가능하다.

그리고 만약 전부 변경이 불가능하게 만들고 싶다면 이렇게 하면 된다.
const char *const str = “hello”;

여기까지가 인터넷에 나오는 다양한 예제들에 있던 문법들에 대한 내용이었다.
사실 좀 극단적인 예로 표현한 것들이라 문법적으론 엄밀히 말하면 맞지 않지만 어떤 내용인지는 확실히 이해할 수 있을 것이다.
const char *str
데이터 변경 불가, 주소 변경 가능
char *const str
데이터 변경 가능, 주소 변경 불가
const char *const str
데이터와 주소 모두 변경 불가
요약해서 이정도만 기억하고 있어도 인터넷에 있는 대부분의 예제나 라이브러리 코드를 이해하는데 도움이 될 것이다.
그리고 이런 것들이 사실 특별한 문법이라기보단 메모리 관리나 코드의 안정성을 위해 사용되는 기능들로 프로젝트의 규모가 커진다면 자주 접하게 되고 신경써야 하는 부분이니 익혀두는 것이 좋다.
