[아두이노 고급] 1. 아두이노 프로그램과 구조와 부트로더

우리가 아두이노를 사용하다 보면 이런 말을 들은 적이 있을 것이다.

아두이노 스케치는 C++을 기반으로 만들어져 있다는 말을 말이다.

그리고 아마 C언어를 사용해 본 사람들은 알겠지만 C언어가 동작하기 위해서는 main 함수가 필요하다.

그런데 스케치에서는 main 함수가 존재하지 않는다.

대신 setup과 loop가 존재하고 이 두 가지를 통해 아두이노 보드가 동작하게 된다.

이때 setup은 스케치가 시작될 때 동작하는 함수로 변수의 초기화, 핀 모드 설정, 라이브러리 초기화 등의 작업을 수행하고 전원이 켜지거나 리셋이 되었을 때 가장 처음에 한 번만 동작한다.

loop는 스케치가 동작하는 동안 지속적으로 반복 동작하여 프로그램의 메인 동작을 하게 된다.

그리고 이 두 가지가 정의되지 않으면, 즉 둘 중 하나가 존재하지 않거나 하나보다 많이 정의되면 오류가 발생하게 된다.

이렇게 동작을 하고 있는데 그럼 main 함수는 어디에 존재할까.

그 답은 간단하다.

사실 main 함수는 이미 정의되어 있고 우리는 main 함수 안에서 setup과 loop 두개의 함수를 정의하고 있는 것이다.

그럼 이 큰 틀을 어디서 볼 수 있을가.

아두이노 프로그램이 설치된 경로에서 hardware/arduino/avr/
cores/arduino 로 가면 main.cpp가 있다.

main.cpp
#include <Arduino.h>

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}

이 코드를 살펴보면 가장 위에는 Arduino.h를 포함하고 있는데 이 파일은 아두이노에서 사용되는 기본 함수, 자료형, 상수 등의 선언되어 있는 헤더 파일이다.

그리고 다음에 4번째 줄에 정의된 것은 C언어에서 프로그램의 종료 처리와 관련된 atexit()를 Arduino 환경에 맞게 구현해 놓았다.

마이크로컨트롤러에선 일반 PC의 프로그램과 달리 프로그램이 종료되는 구조가 아니기 때문에 실질적인 작업은 하지 않고 0만 반환한다.

참고로 4번째 줄 위로 주석이 있는데 이 주석에 의하면 atexit()가 weak로 선언되어 있어 필요한 경우 다른 곳에서 같은 함수를 다시 정의해서 대체할 수 있다고 되어 있다.

여기서 weak는 기본적으로 사용할 함수를 정의해 두되, 동일한 이름의 일반적인 함수가 별도로 정의되면 그 함수를 우선하도록하는 GCC의 속성이다.

그리고 마찬가지로 8~12번째 줄 코드에서도 마찬가지로 초기화 작업을 하는 코드인데 weak로 선언되어 있다.

거기다 지금은 함수 안이 비어있어 대부분의 경우 아무것도 하지 않는다.

Variant의 경우 보통 보드별 하드웨어 구성을 뜻하는 곳인데 여기는 비어어서 아무것도 하지 않는다.

다만 weak로 선언되어 있기 때문에 특정 아두이노 보드만의 추가 설정이 필요한 경우 다시 정의할 수 있도록 되어 있다.

그 아래로 있는 setupUSB도 USB 관련 초기화를 위한 기본 함수를 마련해 놓은 것이다.

다음으론 대망의 main 함수다.

main 함수 내부를 보면 먼저 init( )를 통해 기본적인 하드웨어 초기화를 수행한다.

이때 보통 타이머 같은 기능들을 아두이노에서 사용할 수 있도록 초기화 하게된다.(millis, micros, delay 같은 기능)

다음으론 initVariant가 있는데 보드별 추가 초기화를 수행한다.

앞서 이야기한 것처럼 함수가 비어 있어서 별다른 동작을 하지 않는다.

다음 코드는 USB 초기화에 대한 내용인데 ‘USBCON’이 정의된 보드는 USBDevice.attach 함수를 사용한다는 것인데 이것은 USB 기능을 직접적으로 사용하는 아두이노 보드들에 대한 내용이다.

예를 들자면 아두이노 우노 보드는 통신에 ATmega16U2를 사용하기 때문에 해당되지 않고 아두이노 레오나르도 보드는 ATmega32U4 칩이 직접 통신을 하기 때문에 해당된다.

그리고 이 과정을 다 거치면 이제 setup이 호출된다.

여기서 setup은 우리가 아두이노 스케치에서 작성한 그 setup에 해당한다.

이렇게 setup을 호출해서 사용하기 때문에 아두이노 스케치에서 setup이 없거나 두 개이상 작성되면 오류가 발생하게 된다.

참고로 우리가 보통 아두이노를 배울때 전원이 들어가면 setup이 가장 먼저 동작한다고 하는데 사실 지금 동작 구조를 보면 아니라는 것을 알 수 있다.

실제 순서는 main 실행 – 아두이노 기본 초기화 – 보드별 초기화 – 필요한 USB 연결 처리까지 거치고 setup이 동작하는 것이다.

아두이노 고급 과정이라고 한다면 이정도는 알아두자.

그리고 다음에 나오는 것이 loop의 정체다.

26번째 줄을 보면 알겠지만 for( ; ; ) 이렇게 되어 있다.

우리가 아는 for문은 보통 내가 원하는 만큼 반복시키는 반복문이지만 지금과 같이 사용되면 이것은 조건 없이 무한이 반복하는 무한 반복문이 된다.

그리고 이 무한 반복문 안에서 loop를 호출하는 것이다.

이 loop 역시 아두이노 스케치에서 작성한 그 loop문을 말한다.

그래서 loop에 값을 적으면 무한히 반복한다고 하는데 그 이유가 바로 지금과 같이 for문이 무한 반복으로 설정되어 있고 그 안에서 loop를 호출하기 때문이다.

또 setup과 마찬가지로 이때 loop를 호출하기 때문에 loop가 없거나 두 개 이상 존재하면 오류가 발생한다.
(참고로 조금 더 추가하잠면 Arduino.h에 void setup(void), void loop(void)로 선언되어 있고 여기서 해당 함수들을 호출하기 때문에 반드시 정의해야 한다.)

그리고 loop의 동작이 끝나면 serialEnventRun이 동작하는데 이것은 하드웨어의 시리얼 포트에 수신된 데이터가 있는지 검사하고 있으면 해당 serialEnventRun함수를 실행시키게 하는 아두이노 내부 함수에 해당한다.

실제로는 HardwareSerial.cpp에 정의되어 있고 보드에 따라 다르게 정의되어 있다.

그래서 지금 코드에 연결된 Arduino.h에 가보면 이 함수 역시 weak로 선언되어 있는 것을 볼 수 있다.

마지막으로 return 0이 있는데 사실 위에서 무한 반복되기 때문에 여기까지 코드가 오지는 않는다.

마이크로컨트롤러 프로그램은 일반 PC 프로그램과 달리 끝이 없기 때문에 그렇다.

이를 참고해서 코드를 다시 나타내면 이렇다.

C++
#include <Arduino.h>

int main(void)
{
	init();

	setup();
    
	for (;;) {
		loop();
	}

}

weak로 선언된 코드를 제거하면 이렇게만 남게 된다.

그리고 여기서 우리는 setup과 loop의 코드를 작성해서 동작하게 된다.

이때 이 main.cpp와 앞에서 이야기한 아두이노의 각종 함수, 매크로 등이 선언된 Arduino.h 파일 그리고 지금 우리가 경로를 따라 열어둔 폴더에 있는 파일들을 전부 합쳐 Arduino AVR Core라고 부르기도 한다.

그럼 이렇게 작성된 코드를 어떻게 마이크로컨트롤러로 전달할까.

아두이노를 사용해봤다면 당연히 USB 포트를 이용해서 업로드하면 되지 않나라고 생각할 것이다.

하지만 일반적으로 이런 코드를 전송할 때는 AVR 기반의 마이크로컨트롤러는 ISP 장치 또는 ISP 프로그래머라고 불리는 장치를 이용한다.

이때 ISP는 In System Programming의 약자로 마이크로컨트롤러에 직접 프로그래밍 하는 방식을 말한다.

( http://content.alexandria.atmel.com/webhelp/GUID-EEA42155-A873-46B4-9500-D88617041B0B-en-US-1/index.html )

그리고 이 방식을 동작하게 하는 것이 ISP 프로그래머라고 하는 장치다.

보통은 위의 그림과 같이 되어 있고 MOSI, MISO, SCK, RESET 등의 핀을 사용해서 통신을 하게 된다.

다만 이때 사용하는 핀이 SPI 통신과 유사해서 SPI 통신을 사용하나 싶은데 실제로는 좀 다르다.

핀 구성도 SS핀이 없고, RESET 핀이 존재한다.

일반적으로 AVR 마이크로컨트롤러는 Reset 핀이 평상시에는 HIGH 상태에 있고 이걸 LOW로 변경하면 마이크로컨트롤러가 ‘Reset 상태’로 변경된다.

그리고 이 상태에선 기존에 업로드된 프로그램들이 동작하지 않는다.

이후 Reset핀이 HIGH로 변경되면 마이크로컨트롤러는 Reset 되면서 동작을 처음부터 진행하게 된다.

ISP 프로그래머는 이걸 이용하게 된다.

실제 코드를 전송하는 과정을 보면 Reset을 활성화한 상태에서 프로그래머가 코드를 전송하게 된다.

물론 이렇게 생각할 수 있다.

마이크로컨트롤러가 Reset 상태인데 동작을 하나?라는 의문이다.

Reset이 LOW 상태면 우리가 이전에 넣어둔 프로그램은 실행되지 않는다.

뭐 당연한 말이다.

다만, 이때 마이크로컨트롤러 내부에는 ISP 프로그래밍 명령을 처리하는 기능을 동작할 수 있게 된다.

Reset 상태에서 특정 명령어(Programming Enable)를 전송하면 마이크로컨트롤러 내부의 ISP 제어로직이 활성화 되는 것이다.

제어로직이 활성화되면 우리가 만든 코드를 기록하기 위해 동작하는데 이때 동작은 보통 ‘메모리 초기화 – 데이터 전송 및 Flash 메모리 기록 – 무결성 검증 – 퓨즈 비트 및 락 비트 설정’ 순으로 동작하게 된다.

요약하자면 기존 프로그램의 실행을 Reset으로 막고, 마이크로컨트롤러 내부의 프로그래밍 기능을 통해 MOSI/MISO/SCK로 직접 읽고 쓰는 것이다.

그리고 프로그램 업로드가 끝나면 Reset 핀이 HIGH로 돌아가면서 마이크로컨트롤러가 방금 들어온 프로그램을 기반으로 동작 하게 된다.

이게 일반적인 AVR 기반의 마이크로컨트롤러에 코드를 업로드하는 방식이다.

하지만 우리가 사용하는 아두이노 보드에는 이런 장치를 이용해서 업로드 하지 않는다.

그럼 어떻게 된 것일까.

지금은 간단히 설명했지만 실제로는 좀 더 복잡한 과정을 거치게 되는데 그 방식이 번거롭고 어렵다 보니 아두이노에서는 좀 다른 방식을 사용하게 된다.

그것이 부트로더(Bootloader)다.

부트로더는 마이크로컨트롤러에 미리 심어놓은 프로그램으로 이 프로그램을 통해 ISP 프로그래머가 아닌 다른 방식으로 코드를 받을 수 있게 해준다.

단적인 예로 아두이노 우노 보드를 생각해보자.

아두이노 우노 보드는 USB 포트를 통해 연결되서 데이터를 받게 되는데 이때 구조가 이렇게 된다.

아두이노 스케치 코딩 – USB – ATmega16U2 – ATmega328 이렇게 받게 된다.

이때 ATmega16U2는 USB를 통해 들어온 데이터를 Serial통신(UART)으로 변경해서 ATmega328에 전송을 해준다.

그리고 ATmega328은 UART 통신을 통해 데이터를 업로드 받게 된다.

이런식으로 ISP 방식이 아닌 다른 방식으로도 코드를 업로드 할 수 있게 해주는 것이 바로 부트로더다.

그래서 코드가 업로드 되는 과정을 보면 이렇다.

부트로더는 앞에서 말했듯이 이미 마이크로컨트롤러에 들어있는 프로그램이다.

그래서 ISP 방식과 동일하게 Reset 핀을 LOW 두고 사용할 순 없다.

다만 Reset 핀을 사용하긴 하는데 부트로더의 경우 마이크로컨트롤러가 켜질때 가장 먼저 실행되는 프로그램이기 때문에 코드를 업로드할 때는 보드의 동작을 다시 처음부터 동작하게 해야 한다.

이말인 즉 Reset 핀을 잠깐 LOW 상태로 뒀다가 HIGH로 변경해야 한다는 것이다.

그래서 보통 부트로더를 사용하는 마이크로컨트롤러는 Reset, RX, TX 핀을 사용하게 된다.

보드가 리셋 되면 그때 RX, TX 핀을 이용해서 통신을 하게 되는 것이다.

하지만 여기서도 문제가 있다.

보드를 Reset 시키려면 앞서 말한 것과 같이 LOW와 HIGH로 값을 변경해야 하는데 이것을 할 방법이 소프트웨어 적으론 조금 애매한 것이다.

그래서 선택한 것이 회로적인 방법이다.

실제로 PC와 아두이노 간의 시리얼 통신을 시작하면 Reset 핀으로 보내는 신호가 HIGH에서 LOW로 변경된다.

이 상태라면 보드는 계속 Reset 상태에 있어서 부트로더가 동작하지 못할 것이다.

이걸 해결하기 위해 Reset 핀에 커패시터를 직렬로 연결하고 5V로 풀업저항과 같이 연결한 방식을 사용했다.

그럼 PC에서 나오는 신호가 HIGH에서 LOW 떨어지는 순간 전류가 흘러 마이크로컨트롤러의 Reset 핀을 순간적으로 LOW로 툭 떨어뜨리고 그 직후 커패시터가 충전되면서 Reset 핀은 풀업 저항에 의해 다시 HIGH로 복귀하게 된다.

이 짧은 순간 보드는 처음부터 다시 동작하게 되고 부트로더가 동작하면 RX와 TX 핀을 이용해서 코드가 업로드 되는 것이다.

이걸 보통 오토 리셋(Auto Reset)이라 부르고 이때 연결되는 핀을 DTR(Data Terminal Ready)이라고 부른다.
(참고로 DTR은 통신규격의 이름이다)

그래서 보통 업로드할 때 사용되는 Serial 통신 핀이다라고 하면 DTR, RX, TX 핀을 말하는 것이다.

아두이노는 보드마다 이 부트로더가 미리 탑재되어 출시되기 때문에 PC의 USB 포트를 통해 코드를 바로 업로드할 수 있게 된다.

다만, 이러한 부트로더도 처음 마이크로컨트롤러에 넣을 때는 당연히 ISP 프로그래머가 필요하다.

우리가 사용하는 아두이노도 당연히 ISP 프로그래머로 부트로더를 넣었을 것이고 그리고 제조사에서 이 부트로더가 망가졌을 때를 위해 부트로더를 다시 올릴 수 있는 장소도 마련해두었다.

그것이 그림에 표시된 부분으로 아마 평상시에는 이런게 있구나로 넘겼던 부분일 것이다.

그래서 다음 포스트에선 부트로더를 아두이노 보드에 올리는 과정에 대해 이야기해보자.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
error: Content is protected !!