아두이노 프로그래밍

아두이노(Arduino)는 마이크로컨트롤러이므로 원하는 기능을 먼저 설정하고 이에 맞는 프로그래밍을 통해 기능을 구현한다. 사용 언어는 C/C++을 사용하고 컴파일러 및 라이브러리는 AVR-GCC을 사용한다.

아두이노는 공식적으로 오픈소스로 이러한 통합개발환경(IDE)인 아두이노 소프트웨어를 제작하여 무료로 배포하고있다.

avr-gcc(AVR Libs) 함수에 존재하는 표준함수의 라이브러리와 아두이노에서 제공하는 함수 등 다양한 함수를 적용 시킬 수 있다. gcc 컴파일러를 기본으로 하지만 일반 범용의 컴퓨터와 다른 부분을 수정하여 만들어진 것이므로 이를 고려해야 한다. 예를 들어 입출력 장치가 아두이노와 범용의 개인용 컴퓨터와 다르다. 따라서 몇몇 함수는 특수한 처리를 해야 하고 어떤 것은 사용이 불가능하다.

아두이노 설치 시, 이미 존재하는 함수도 있지만, 그 외의 많은 기능들은 함수가 없는 경우도 있다. 그러나 아두이노의 활발한 사용으로 많은 오픈 소스 함수 들이 존재하므로 검색을 통해 찾아, 파일을 받아 라이브러리 함수를 등록해서 사용할 수 있다.

내장된 함수 사용 시, 마이크로컨트롤러의 모델에 따라 해당 함수나 클래스가 지원되지 않는 경우가 있으므로 우선 사용하는 모델 파악이 필요하다.
아두이노 우노는 시리얼 포트가 아두이노 메가에 비해 적다. 따라서 우노에서는 Serial1,2,3 등이 존재하지 않으므로, 이것을 사용하면 안된다.

함수와 연산자 
 아두이노는 C/C++언어 기반으로 작성하고,  표준 C와 일부 C ++의 모든 기능을 지원한다.

기본 문법 
 구분 기호 :  {} , ;
 주석 : //, /* */
 define : #define, #include
 산술 연산자 : +, -, *, /, %
 할당 연산자 : =
 비교 연산자 : ==, !=, <, >, <=, >=
 부울 연산자 : &&, ||, !
 포인터 연산자 : *, &
 비트와 쉬프트 연산자 : &, |, ^, , <<, >>
 증감 연산자 : ++, --
 결합 산술 연산자 : +=, -=, *=, /=, &=, |=

조건 및 반복 
 조건 : if, if...else, switch case
 반복 : for, while, do... while
 분기 및 점프 : break, continue, return, goto

변수 
C++을 사용하므로 이 언어의 변수 사용과 같다.

상수 
 HIGH / LOW : 입출력 핀의 상태를 말하며, 디지털 회로의 논리 표현과 같다. LOW(논리 0, 0 V 근처), HIGH(논리 1, 높은 전압, 5 V 또는 3.3 V 논리 게이트 VCC 전압)
 INPUT / OUTPUT: 입출력의 방향으로 입력과 출력. INPUT (입력), OUTPUT(출력)
 true / false  : 논리의 부울

변수 데이터 유형 및 C++ 클래스 
 void, boolean, char, unsigned char, byte, int, unsigned int, word, long, unsigned long, float, double
 string, array

형 변환 함수 

다음 형 변환 함수는 인수로 받은 형의 데이터를 형 변환하여 반환한다.

 char(), byte(), int(), word(), long(), float()

변수의 특성 정의 예약어 
 static, volatile, const

유틸리티 
 sizeof() - C/C++에서 함수가 아니라 변수나 형의 크기를 측정하는 도구이다. 따라서 변수의 크기를 컴파일러가 알고 있기 때문에 바이트 단위로 변환한다. C/C++의 모든 정적 변수는 선언 시, 크기가 정해지기 때문이다.

활용 예:
const int pinSens = A0;  // 아두이노 우노의 A0 포트 사용 예

int gdata[] = { 1, 2, 3, 4, 5, 3};
const int SZARR_DATA = sizeof(gdata)/sizeof(gdata[0]); // 배열 크기 6개를 얻는 경우
#define  SZ_IARRDATA  (sizeof(gdata)/sizeof(gdata[0]))
const int sz_int = sizeof(int);  // 8비트 CPU에서는 주로 2바이트

void loop() {
    char cnt;

    for (cnt = 0;cnt < SZARR_DATA;cnt++) {
         gdata[cnt ] = analogRead(pinSens);
         deley(100);
    }
}
sizeof가 함수가 아니므로 전역변수를 선언 할 때도 문제가 없이 숫자화 한다.

기초 함수 들 

다음의 기본함수는 아두이노 전용의 함수들이다. 일부는 표준함수에 존재하는 함수도 있다.

디지털 입출력 함수 
 pinMode(pin, mode)    -  핀의 입출력을 정의
 digitalWrite(pin, value) - 디지털 출력 핀에 출력하는 함수
 int digitalRead(pin)        - 디지털 핀의 상태를 읽는 함수

아날로그 입력 및 PWM 출력 

ATmega는 ADC을 내장하고 있다. 따라서 아날로그 신호를 디지털화할 수 있다. 이 ADC 모듈을 사용하는 함수이다.
 analogReference(유형)
 int analogRead(pin)  - ADC 보통 10비트 정밀도를 갖는 모듈이 많다.
ADC는 보통 10비트를 많이 사용하므로 최대 숫자가 10비트이다. 따라서 8비트 정수형 char 변수를 사용하여 데이터를 저장하면 위 부분 MSB의 값이 버려지므로 int 형의 정수 변수가 필요하다. 아두이노 보드에 따라 ADC의 비트수는 주로 10비트를 많이 사용하지만, 고사양의 아두이노를 사용한다면 비트수가 달라질 수 있으므로 미리 확인하는 것도 좋은 방법이다.  ADC의 응용에서 10, 12 그리고 16비트 등을 사용하지만 이 이상의 분해능은 거의 사용하지 않으므로 int 형이면 거의 처리된다.

PWM 출력 

아두이노에 사용하는 ATmega는 디지털 값을 아날로그 전압으로 바꾸어 주는 DAC가 거의 드물다. 따라서 다음 함수는 순수한 아날로그 출력이 아니고 아날로그를 모사하는 PWM(Pulse Width Modulation)방식으로 출력해 사용하는 함수이다. 만약 사용하는 마이크로컨트롤러가 DAC가 존재한다면 이에 맞는 함수를 찾아 확인하고 사용하여야 한다. 이러한 AREF핀으로 디지털-아날로그 컨버터기능을 강화할 수 있다.

 analogWrite(pin, value) - PWM는 타이머 모듈을 사용하므로 OCxA, OCxB의 기능을 사용한다. 별도의 설정없이 이 함수를 사용하면 된다.
 핀번호 (pin) : 타이머와 연결된 OCxA, OCxB 핀을 사용하므로 숫자가 정해지면 타이머 모듈이 자동 결정된다.
 듀티값 (value) : 펄스파의 HIGH와 LOW의 시간 비이다. 최댓값이 8비트 타이머는 비교레지스터도 8비트를 사용하므로 0 ~255이다. 0 은 항상 LOW가 출력되고 이 값의 비에 따라 펄스폭이 결정된다.

PWM 출력은 아두이노 핀이 제한되어 있다. 보통 핀 숫자 앞에 ~,-,# 등의 문자가 있는 핀이 PWM 가능한 핀이다.  각각의 타이머 마다 특성이 틀리므로 주파수는 변경은 관련 레지스터를 별도로 제어 해야 한다. ATmega에서 사용하는 타이머는 8비트(타이머 0, 2)와 16비트 타이머(1,3,...)을 사용하므로 프리스케일러와 제어 비트 설정으로 원하는 주파수를 바꿀 수 있다. 그러나 8비트 타이머의 주파수 변환은 프로스케일러에 의해 제한 되므로, 이 함수를 사용하면 세밀한 조정을 힘들다. 이 함수를 사용하지 않고, 마이크로컨트롤러의 레지스터를 직접 제어해서 다양한 방식의 PWM을 구현할 수 있다.

PWM 발생은 디지털 회로인 타이머 모듈에서 이루어지게 때문에 레지스터 설정값에 따라 각 요소값이 변화한다. 이 때 소프트웨어는 레지스터 설정만 하면 되므로 신호를 만드는 과정상 직접적으로 관여하지 않는다. 따라서 효과적으로 자원을 사용할 수 있다.

만약 8비트 타이머를 사용한다면:

    analogWrite(6, 127);  // 보통 뒤의 값이 듀티값이므로 8비트 255 까지이다. 따라서 127은 듀티가 거의 50%이다.

아두이노 우노의 핀 6번은 OC0A 이므로, ATmega328P의 타이머 0을 사용한다. 따라서 이 함수 호출로 8비트 타이머0로 자동 할당된다.  따라서 듀티값은 최댓값이 255까지이다.

함수의 한번 호출로 타이머의 레지스터가 설정되기 때문에 다른 설정이 있기 전까지는 계속 PWM이 작동한다.

쉬프트 및 펄스 함수 
 shiftOut(dataPin, clockPin, bitOrder, value)
 unsigned long pulseIn(pin, value)

시간 함수 
시간 읽기:
 unsigned long millis()
 unsigned long micros()

지연 함수:
 delay(ms)
 delayMicroseconds(microseconds)

수학 관련 함수 
 min(x, e), max(x, e), abs(x), constrain(x, a, b), pow(base, exponent), sqrt(x)

 map(value, fromLow, fromHigh, toLow, toHigh) - 특정 범위의 위치 값을 다른 범위의 위치 값으로 매핑하여 변환한다.

이 map 함수는 특정 범위의 값을 다른 범위의 값으로 변환하는 것이다. 수학적 개념은 일차함수 계산과 같은 의미이다.

함수 내에서 실수가 아니라 정수형의 계산을 통해 맵핑을 하고 정수형 숫자를 반환한다.  여기서 사용된 변수는 long이며, 32 비트(4 바이트)를 사용하므로 -2,147,483,648부터 2,147,483,647 범위를 갖는다. 따라서 계산 중에 또는 계산 결과가 32비트 이상이 되면 문제를 일으키므로,  숫자가 크면 예상되는 값을 예측해야 한다.

    int val = analogRead(A0); // 아두이노 우노나 메가2560은 10비트 ADC을 사용하므로 10비트 값이 읽힌다.
    val = map(val, 0, 1023, 0, 255);
이 경우 아날로그 변환값이 500이라면:

가 된다.

위의 예에서처럼 비트수가 딱 떨어질 때는 오히려 비트 쉬프트 연산이 효과적이다. 결국 10비트 정수값을 8비트 정수 값으로 변환하고자 하는 것과 같은 의미이다. 다음 경우처럼 간단히 처리하면 기계어 코드의 실시간 실행에서 효율적일 수 있다.

10비트를 8비트화하는 것이므로 다음과 같이 쉬프트 연산을 사용할 수 있다:
    int val = analogRead(A0);
    val >>=2;
    unsigned char u8val = (unsigned char) val;

삼각함수 
 sin(rad), cos(rad), tan(rad)

랜덤 함수 
 randomSeed(seed), long random(max), long random(min, max)

비트 바이트 함수 
 lowByte(), highByte(), bitRead(), bitWrite(), bitSet(), bitClear(), bit()

외부 인터럽트 함수 

 attachInterrupt(interrupt, function, mode) : 외부 인터럽트가 발생했을 때, 처리를 하기 위해 설정하는 함수이다. 여기서 function은 인터럽트가 발생했을 때 기능을 처리하기 위한 인터럽트 핸들러이다.

인수 :
 interrupt : 인터럽트 구분용 숫자이다. 아두이노 우노의 경우 0(핀 2번)과 1(핀 3번) 2개의 외부 인터럽트가 있다. 마이크로컨트롤러에 따라 개수가 정해져 있다.
 function : 인터럽트 핸들러로  void 타입의 함수로 정의해야 한다. 인터럽트가 걸리면 이 함수가 자동 호출된다.
 mode : 인터럽트 접수 방법을 지정한다. 디지털 입력이기 때문에 각각의 상황을 정의한다.
 LOW : 핀이 LOW일 때,  인터럽트가 걸린다.
 CHANGE : 핀의 입력 상태가 변하면  인터럽트가 걸린다.
 RISING : 상승 엣지(LOW -> HIGH)일 때,  인터럽트가 걸린다.
 FALLING : 하강 엣지(HIGH -> LOW)일 때,  인터럽트가 걸린다.

 detachInterrupt(interrupt) - 인터럽트 실행을 중지한다.

인터럽트 함수 

 interrupts(), noInterrupts()

시리얼 통신 함수 

시리얼 포트의 통신을 위해 데이터를 보내기 전에 우선 설정 함수를 사용하여 통신 채널을 설정한다. 설정 완료 후, 관련 입출력 함수를 사용한다.

 begin() - 통신 설정
 available() - 통신 수신 여부에 따라 함수 자동 호출됨.
 read(), flush(), print(), println(), write() - 데이터 읽거나 쓰기

포트 조작 관련  

마이크로컨트롤러의 포트를 조작할 때, 하나의 핀을 사용할 경우 입출력 모드 함수로 설정하고 입출력 함수를 데이터를 입출력 할 수 있다. 그러나 포트를 동시에 한 묶음으로 조작 하거나 분산된 입출력 이라면 마이크로컨트롤러의 레지스터를 직접 조작하여 사용할 수 있다.

다음과 같은 레지스터 조작을 통해 포트를 사용할 수 있다:
 DDR[B/C/D] : 데이터 방향 레지스터로 각 포트의 방향을 지정한다. 주로 각 비트의 값이 1은 출력, 0은 입력이다.
 PORT[B/C/D] : 디지털 출력 레지스터로 여기에 써지면 바로 핀으로 논리 상태가 출력 된다.
 PIN[B/C/D] : 핀으로부터 디지털 입력 레지스터로 디지털 상태를 읽는다.

포트 조작 예 

아두이노 우노는 디지털 출력 핀 0~7까지 PORTD에 연결되어 있다. 만약 디지털 출력 핀 1부터 7까지 출력으로 설정하고, 데이터를 출력 한다면 다음과 같은 예를 생각할 수 있다.
void setup()  {
    char cnt;
    for (cnt =1;cnt <=7;cnt++)
         pinMode(cnt, OUTPUT);
}
void loop() {
    char cnt;
    char data;
    data = 0xFE;

    for (cnt =1;cnt <=7;cnt++) {
         data >>= 1;
         if (data & 0x01)
             digitalWrite(cnt, HIGH);
         else
             digitalWrite(cnt, LOW);
     }
}

그러나 마이크로컨트롤러의 포트 구조상 8비트 CPU는 포트 묶음이 8비트로 처리되고, 조작도 8비트 단위로 할 수 있다.

따라서 위의 코드의 기능을 다음과 같은 방법으로 구현할 수 있다 :
void setup()  {
     DDRD = B11111110;
}
void loop() {
     PORTD = 0xFE;
}
위의 2가지 코딩 방법을 비교할 때, 묶음으로 처리하는 것이 실행 시간 측면이나 기계어 코드 크기 측면에서 효율적일 수 있다.

특정 핀 만을 재 설정하는 예:

  DDRD = DDRD | B11111100;  // 포트 D의 핀 2부터 7까지 출력 설정,  핀0과 1은 시리얼 통신의 RX와 TX으로 사용한다.
  PORTD = B10101000;             // 핀  7,5,3을 HIGH 그리고 핀 6,4,2는 LOW로 출력, 위의 DDRD 설정에 따라 PORTD0-1은 무시 된다.

주어진 함수 만 사용할 수 있는 것이 아니고 마이크로컨트롤러의 레지스터를 직접 제어할 수 있다. 이 레지스터 명과 정의는 마이크로컨트롤러 마다의 헤더파일에 이미 지정되어 있다.
이렇게 포트를 비트 별로 설정을 하려면, 우선 아두이노의 회로를 확인하여 어떤 포트가 어느 커넥터에 연결되었는지 인지하고 입출력 회로의 기능이 동작하도록 포트를 조작할 수 있다.

AVR Libc 

AVR-gcc는 gcc를 Atmel AVR적용하기 위해 만들어졌다. 일반 컴퓨터와 마이크로컨트롤러의 개발 특성이 다른 부분이 있기 때문에, AVR 특성에 맞는 몇가지가 추가되거나 변형 되었다. 각 마이크로컨트롤러의 레지스터, 사용하는 마이크로프로세서 체계 등에 적합하도록 변형 및 추가 되었다. 예를 들어 사용하는 AVR의 SFR 레지스터가 정의되어 있다.

gcc도구로부터 변형 되었으므로 틀의 유사성이 있으며, 다음 3부분으로 구성 된다:

 avr-binutils
 avr-gcc
 avr-libc

AVR을 기반으로 하는 아두이노 프로그래밍은 AVR-gcc를 아두이노 통합 개발 환경에 결합하여, 개발 환경을 설치하면 별도의 추가 설치없이 사용할 수 있다.

인터럽트 

마이크로컨트롤러의 인터럽트 처리를 허용할 것인지를 설정한다.

전체 인터럽트 요청 신호를 무시:
  cli();  //  CPU가 인터럽트 요청을 무시

전체 인터럽트 요청 신호를 허용 :
  sei(); //  CPU는 인터럽트 요청을 허용하여 인터럽트 핸들러를 실행한다.

delayMicroseconds()나 시리얼 통신은 인터럽트의 cli()에 의한 비활성화에 영향을 받는다.

시간 지연 
delayMicroseconds()는 가장 작은 시간 지연이 2μs 정도이다. 이것보다 짧은 시간을 지연할 때는 어셈블리어의 'NOP' 명령을 사용할 수 있다.
'NOP' 명령 실행에 걸리는 시간은 16 Mhz을 사용할 경우 62.5ns 정도의 지연 시간을 얻을 수 있다.

이를 위해 다음과 같이 코딩한다:
  __asm__("nop\n\t");

포트 조작 
이미 언급했듯이, AVR 포트 제어 시, 레지스터를 직접 제어하여 빠른 포트 제어를 실현 할 수 있다.

특수 레지스터의 조작 (SET/RESET) 

AVR의 SFR(특수 기능 레지스터)의 비트 단위 조작이 가능하다.

다음 헤더에 정의된 정의 구조를 사용할 수 있다:

# ifndef cbi
# define cbi(sfr, bit) (_SFR_BYTE(sfr) &= _BV(bit))
# endif
# ifndef sbi
# define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
# endif

같이 보기 
 아두이노
 아두이노 보드
 아두이노 라이브러리
 마이크로컨트롤러

각주 

[참고] 아두이노 레퍼런스

아두이노
마이크로컨트롤러
 
전자공학