[중급강좌] 아두이노 내부 동작 구조 – Part1
아두이노가 구동되는 방식을 세밀하게 파헤친 자료가 있어서 번역해서 소개합니다. 아래 해외 포스트의 번역 자료입니다.
A TOUR OF THE ARDUINO INTERNALS: HOW DOES HELLO WORLD ACTUALLY WORK?
피지컬 컴퓨팅의 Hello World, Blink
아두이노를 처음 접할 때 가장 먼저 해보게 되는 소스가 Blink 입니다. 아두이노에 내장된 LED가 13번 핀에 연결되어 있기 때문에 13번 핀을 OUTPUT 모드로 설정해서 0.5초 간격으로 on, off 시키는 예제입니다.
void setup(){ pinMode(13, OUTPUT); } void loop(){ digitalWrite(13, HIGH); delay(500); digitalWrite(13, LOW); delay(500); }
굉장히 간단한 국민예제이지만 이 소스코드의 내부까지 세밀히 추적하면 아두이노가 어떻게 핀들을 제어하고 동작하는지 알 수가 있습니다. 그래서 몇 번의 시리즈에 걸쳐 Blink 예제의 내부를 추적하고 분석해보도록 하겠습니다. 번역을 하는 본인 스스로드 많은 것을 알게 해준 내용이네요.
AVR Architecture의 기본: Register, PORT, PIN, DDR
AVR microcontroller는 일련의 레지스터(register)로 구동됩니다. 레지스터는 데이터를 읽거나 기록할 수 있는 작은 데이터 공간입니다. 그리고 아두이노가 가진 핀들은 3개의 레지스터에 연결되어 있습니다. 각 핀을 제어하는데 3개의 레지스터들이 사용됩니다.
- Data Direction Register (DDR)
- PORT register
- PIN register
DDR 레지스터는 핀을 input 혹은 output 상태로 설정하는 레지스터입니다. pinMode 함수 기능이 이 레지스터와 연동됩니다. 핀이 output 상태로 설정되면 AVR은 출력 값을 전달하기 위해 PORT 레지스터를 사용합니다. digitalWrite 함수의 기능을 떠올리시면 됩니다. 핀이 input 상태로 설정되면 AVR은 입력 값을 읽기 위해 PIN 레지스터를 사용합니다. digitalRead 함수의 기능입니다.
AVR이 사용하는 레지스터는 8bit(=1byte) 크기이기 때문에 0과 1로 표현되는 8개의 값(슬롯, slot)을 표현할 수 있습니다. 1비트로 표현되는 각각의 값(슬롯)이 아두이노가 가진 핀의 상태를 표현합니다. 물론 아두이노의 핀들을 8개의 슬롯으로 모두 표한할 수 없기 때문에 아두이노는 ‘A’~’D’까지 4개의 라벨(Label)을 사용해서 확장해 뒀습니다. 즉 DDR, PORT, PIN 레지스터는 다음과 같이 4개의 세트로 존재합니다.
DDRA, PORTA, PINA, DDRB, PORTB, PINB… DDRD, PORTD, PIND
‘Hello World’ 스케치에서는 13번 핀을 제어해서 LED를 깜빡이는데, 13번 핀은 B set 레지스터에 있습니다. 그리고 LED를 on/off 하기 위해 output 모드로 사용하므로 DDRB와 PORTB 두 개의 레지스터만 사용합니다. 실제 13번 LED를 제어하는 동작은 아래와 같이 이루어집니다.
- pinMode(13, OUTPUT) = 13번 핀 OUTPUT 모드로 설정 = DDRB 레지스터의 13번 핀에 해당하는 bit를 on
- digitalWrite(13, HIGH) = 13번 핀 on = PORTB 레지스터의 13번 핀에 해당하는 bit를 on
레지스터의 슬롯에 값을 기록하는 것은 논리적인 작업이지만 아두이노의 물리적인 회로는 정확히 레지스터의 값에 따라 동작하도록 설계되어 있습니다. 그래서 우리가 작업한 소스대로 하드웨어가 동작하는 것입니다.
핀 모드 설정 : pinMode()
우리가 아두이노에 특정 핀을 제어하는 명령을 내릴 때는 pinMode(), digitalWrite() 등의 함수를 사용합니다. 그럼 아두이노 내부에서는 이 함수들이 어떻게 동작하는지 살펴보겠습니다. 먼저 pinMode() 함수를 보도록 하겠습니다. 아두이노 설치폴더에서 [arudino-0015/hardware/cores/arduino/wiring_digital.c] 파일을 찾아보면 pinMode() 함수가 아래와 같이 정의되어 있습니다.
void pinMode(uint8_t pin, uint8_t mode) { uint8_t bit = digitalPinToBitMask(pin); uint8_t port = digitalPinToPort(pin); volatile uint8_t *reg; if (port == NOT_A_PIN) return; // JWS: can I let the optimizer do this? reg = portModeRegister(port); if (mode == INPUT) *reg &= ~bit; else *reg |= bit; }
pinMode() 함수의 첫 번째 인자로는 정수형으로 된 핀 넘버를 입력하고 두 번째 인자로는 INPUT, OUTPUT을 사용합니다. INPUT, OUTPUT은 [arduino-0015/hardware/cores/arduino/wiring.h] 파일에 정수형 1, 0으로 정의되어 있습니다.
pinMode() 함수를 이용해 해야 할 작업이 뭔지는 이미 알고 있습니다. DDRB 레지스터에서 해당 슬롯의 값을 바꿔줘야 합니다. 이 작업을 하기 위해서는 핀 넘버에 해당하는 DDRB 레지스터에서 슬롯의 위치를 찾아야하고, 해당 슬롯의 값을 모드에 맞게 바꿔줘야 합니다. 이 과정을 따라가 보도록 하겠습니다.
위 코드에서 pinMode() 함수는 가장 먼저 digitalPinToBitMask() 를 호출합니다. [arduino-0015/hardware/cores/arduino/pins_arduino.h] 파일을 찾아보면 이 매크로에 대한 정의가 나와 있습니다.
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
마치 함수처럼 동작하는 매크로입니다. 매크로는 아두이노가 동작할 때 함수처럼 실행되는게 아니라 컴파일 할 때 미리 수행할 작업이 지정됩니다. 이해하기는 좀 어렵지만 아두이노에서는 광범위하게 매크로가 사용되고, 성능 향상에 도움을 줍니다.
이 매크로가 하는 역할은 DDRB의 레지스터 슬롯 8비트 중 7개를 0으로 세팅하고 우리가 원하는 핀을 나타내는 슬롯 하나만 1로 설정한 byte(=8bit)를 리턴해 주는겁니다. digitalPinToBitMask() 매크로가 리턴하는 byte(=8 bit)를 “bit mask”라고 합니다. 왜냐면 이것을 레지스터 byte에 OR 연산을 했을 때 다른 슬롯의 값을 변경하지 않고 우리가 원하는 슬롯만 1로 바꿔주기 때문입니다. 따라서 digitalPinToBitMask(pin) 를 호출해서 얻은 uint8_t bit 비트 마스크를 이용해서 DDRB 레지스터의 원하는 슬롯만 바꿀 수 있습니다.
위 매크로 정의 구문에서 digital_pin_to_bit_mask_PGM 은 [arduino-0015/hardware/cores/arduino/pins_arduino.c] 파일에 정의된 배열입니다. digital_pin_to_bit_mask_PGM + (P) 구문은 덧셈 연산처럼 보이지만 실은 배열의 특정 원소를 찾는 indexing 연산입니다. 우리가 찾는 것이 13번 핀이라고 하면 13이 더해지므로 배열의 14번째 원소의 값을 가져옵니다. 배열은 아래와 같이 값들이 이미 정의되어 있습니다.
const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = { // PIN IN PORT // ------------------------------------------- // [...] _BV( 4 ) , // PB 4 ** 10 ** PWM10 _BV( 5 ) , // PB 5 ** 11 ** PWM11 _BV( 6 ) , // PB 6 ** 12 ** PWM12 _BV( 7 ) , // PB 7 ** 13 ** PWM13 _BV( 1 ) , // PJ 1 ** 14 ** USART3_TX //[...] }
배열의 index는 0부터 시작하므로 14번째 원소가 13번 핀에 해당합니다. 그 값은 _BV( 7 )으로 정의되어 있습니다.
여기서 _BV(7) 의 의미를 알아야겠네요. 이것도 역시 매크로이고 [arduino-0015/hardware/tools/avr/avr/include/avr/sfr_defs.h] 파일에 정의가 되어 있습니다.
#define _BV(bit) (1 << (bit))
비트 연산을 하는 매크로인데 1(이진수 표현=B0000 0001) 을 지정된 bit 값 만큼 왼쪽으로 이동(shift) 시켜줍니다. 따라서 _BV(7)은 1(=B0000 0001)을 왼쪽으로 7번 shift 시켜주므로 아래와 같은 값으로 바뀌게 됩니다.
B1000 0000
주의하실 점은 _BV() 매크로를 이용한 bit shift 과정이 아두이노 동작하면서 pinMode() 함수를 호출할 때 이루어지는 것이 아니라는겁니다. 컴파일 과정에서 이미 수행되기 때문에 digital_pin_to_bit_mask_PGM 배열에는 bit mask 들이 이미 기록된 상태가 됩니다.
그럼 digital_pin_to_bit_mask_PGM + (P) 구문이 의미하는 바를 알 수 있겠죠. 아두이노가 digital_pin_to_bit_mask_PGM 배열로 미리 저장해 둔 bit mask를 가져오는 역할을 합니다.
이제 pgm_read_byte가 의미하는 것을 살펴보면 되겠습니다. 이 매크로는 [arduino-0015/hardware/tools/avr/avr/include/avr/pgmspace.h]에 정의되어 있습니다. 이 매크로는 메모리 사용과 관련되어 있는데, 결론부터 얘기하자면 아두이노 RAM(2KB) 소비를 줄이기 위해 Flash 메모리(32KB)에 저장된 값을 직접 읽어서 사용하도록 하는 매크로입니다.
이제 좀 복잡하고도 중요한 Program memory에 대해 얘기할 차례입니다.
(나머지 내용은 다음 Part에서 계속됩니다.)
참고자료 :