You are on page 1of 180

목 차

1장 아두이노 개요
1.1 아두이노란? ············································································································2
1.2 아두이노의 종류 ·····································································································5
1.3 아두이노 우노 R3 ································································································10
1.4 개발 환경 ·············································································································12

2장 디지털 입출력
2.1 디지털 포트의 개념 ·····························································································20
2.2 LED 깜박이기 ······································································································23
2.3 부저를 이용한 실험 ·····························································································29
2.4 터치센서 ···············································································································33
2.5 택스위치 ···············································································································35
2.6 인체감지센서 ········································································································38

3장 시리얼 통신
3.1 시리얼통신 개요 ···································································································42
3.2 시리얼 통신으로 초음파 센서값 받기 ·······························································48

4장 인터럽트
4.1 인터럽트(interrupt)의 개념 ················································································54
4.2 터치센서를 이용한 인터럽트 실험 ·····································································55
4.3 택스위치와 디바운싱 ···························································································56

5장 아날로그핀
5.1 아날로그 투 디지털 변환기 ················································································62
5.2 온도센서를 이용한 아날로그 입력 실험 ····························································65
5.3 광량센서를 이용한 아날로그 입력 실험 ····························································67
6장 아날로그 출력
6.1 아날로그 출력 개요 ·····························································································72
6.2 아두이노의 아날로그 핀 ······················································································73
6.3 고휘도 LED ··········································································································74
6.4 (초)소형 DC모터 실험 ·························································································76
6.5 중형 DC모터 실험 ·······························································································80
6.6 PWM 주파수 변경 ·······························································································82

7장 아두이노 라이브러리
7.1 Servo 라이브러리 ································································································89
7.2 LiquidCrystal 라이브러리 ···················································································91
7.3 저항 하나로 터치센서 구현 ················································································97
7.4 I2C (two wire) 통신 ·························································································101

8장 사용자 라이브러리
8.1 다운로드 받은 라이브러리 설치법 ···································································113
8.2 사용자 라이브러리 작성법 ················································································114

9장 바퀴 로봇 제어
9.1 makeblock사의 로봇 키트 ················································································122
9.2 조향 제어 ···········································································································125

10장 다른 아두이노들
10.1 아두이노 프로 미니 ·························································································134
10.2 아두이노 두에(due) ·························································································137
10.3 갈릴레오 보드 ··································································································140

부록 A. C++ 의 클래스(class) 문법 ················································································147


부록 B. 아두이노 표준함수 레퍼런스 ···············································································169
PART I
아두이노 기초
Chapter

01
아두이노 개요
2 - 아두이노 개요

1.1 아두이노란?
아두이노는 AVR이라는 마이크로 컨트롤러를 기반으로 한 오픈소스 원보드 마이컴이라고 할
수 있다. 예전에는 이런 시스템을 설계하고 다루기 위해서는 전기/전자와 관련된 전문적인 기술
이나 지식이 필요했다. 이 보드의 특징은 그러한 어려움을 극복하고자 기술 숙련도가 낮은 디자
이너 혹은 예술가들이 쉽게 사용할 수 있도록 설계된 것이다. 따라서 비전공자들도 손쉽게 익히
고 사용할 수 있다는 큰 장점을 가지며 이러한 장점으로 인해 널리 사용되고 있다.

[그림 1.1.1] 가장 기본적이고 널리 사용되는 아두이노 우노 R3 보드의 외형과 크기

그러면 이러한 아두이노 보드를 이용하여 어떤 일들을 할 수 있을까. 간단한 몇 가지 예를 들


어보면 다음과 같다.

∙ 사람이 접근하면 자동으로 조명이 켜지는 장치.


∙ 애완 동물에게 정해진 시간에 먹이를 공급하는 장치.
∙ 화분의 습도가 낮아지면 자동으로 물을 주라는 트윗을 보내는 장치.
∙ 여러개의 LED를 이용하여 귀걸이나 목걸이의 장식으로 이용.
∙ 좀 더 전문적인 응용으로 로봇의 제어 장치.

위의 예는 단순한 몇 개의 예를 든 것이고 인터넷으로 검색을 해보면 정말 많은 응용 제품들이


만들어지고 있으며 일반인 뿐만 아니라 기업에서도 이것을 이용하여 상용 제품을 출시하며 비즈
니스 모델로 이용하고 있는 등 널리 사용되고 있다.
아두이노 개요 - 3

[그림 1.1.2] 다양한 아두이노 응용 작품들

대부분의 아두이노 보드들은 AVR 이라는 8비트 마이크로콘트롤러를 기반으로 하고 있으나


최근에는 Cortex-M3를 이용한 제품(Arduino Due)을 비롯한 여러 하이엔드 제품군이 개발되고
부가 기능을 더한 제품들도 수없이 많다. 또한 소프트웨어 개발을 위한 스케치(sketch)라는 통
합 개발 환경(IDE)이 제공되며 이것 또한 오픈소스로 공개되어 있다. C언어 (기본적으로는 C++
임) 로 개발 가능하나 관련 라이브러리가 잘 갖추어져 있어서 난이도는 낮은 편이다.
4 - 아두이노 개요

[그림 1.1.3] 다양한 아두이노 보드들

앞에서도 언급했듯이 아두이노의 가장 큰 장점은 전기/전자의 깊은 지식이 없이도 마이크로컨


트롤러를 쉽게 동작시킬 수 있다는 점이다. 일반적으로 AVR 프로그래밍이 WinAVR로 컴파일하
여, ISP장치를 통해 업로드를 하는 등 일반인들에게는 어렵고 번거로운 과정을 거치는데 비해
서, 아두이노는 전용 IDE를 이용하여 컴파일된 펌웨어를 USB를 통해 업로드를 쉽게 할 수 있다.
또한 다른 모듈에 비해 비교적 저렴하고, 윈도를 비롯해 맥 OS X, 리눅스와 같은 여러 OS를 모
두 지원한다. 아두이노 보드의 회로도가 CCL에 따라 공개되어 있으므로, 누구나 직접 보드를 직
접 만들고 수정할 수 있으며 실제로 수많은 유사 (호환)제품이 존재한다. 특히 요즘에는 저가형
중국산 제품들이 쏟아지고 있다.

아두이노가 인기를 끌면서 이를 비즈니스에 활용하는 기업들도 늘어나고 있다. 장난감 회사


레고는 자사의 로봇 장난감과 아두이노를 활용한 로봇 교육 프로그램을 학생과 성인을 대상으로
북미 지역에서 운영하고 있다. 자동차회사 포드는 아두이노를 이용해 차량용 하드웨어와 소프트
웨어를 만들어 차량과 상호작용을 할 수 있는 오픈XC라는 프로그램을 선보이기도 했다
아두이노 개요 - 5

1.2 아두이노의 종류
아두이노(arduino)보드는 하드웨어와 소프트웨어 구조가 모두 개방된 오픈소스 플랫폼이기
때문에 정품뿐만 아니라 수많은 변종 보드들이 존재한다. 여기에서는 동일한 Arduino IDE를 사
용해서 개발할 수 있고 기본적으로 많이 사용되는 보드들 위주로 설명하도록 하겠다.

1.2.1 아두이노 우노(uno), 나노(nano) / 프로(pro), 미니(mini),


프로미니(pro mini)

이 다섯 가지 보드는 모두 atmega328p 라는 프로세서를 기반으로 한 보드들이므로 코어


부분은 모두 동일하다. 다만 크기를 작게 만들거나 또는 양산을 위해서 USB통신부가 제거
되었거나 하는 부분이 다를 뿐이다.

(a) 아두이노 우노와 나노

아두이노 우노는 가장 널리 사용되는 기본적인 아두이노 보드이다. 이 보드의 핀 배열은


아두이노 계열의 보드와 쉴드에서 거의 표준과 같이 사용된다. 2014년 현재 세 번째 버전인
우노 R3 가 유통되고 있다.

[그림 1.2.1] 아두이노 우노 R3

아두이노 나노(nano)는 우노와 거의 동일한 구성을 가지고 있다.


빵판에서 실험할 수 있도록 작은 크기와 핀배열을 가진다.
6 - 아두이노 개요

[그림 1.2.2] 아두이노 나노

(b) 아두이노 프로와 프로 미니

다량의 완성품에 장착하기 용이하도록 소형화시키고 usb시리얼 변환 칩을 제거한 제품이


다. 따라서 프로그래밍을 위한 별도의 usb시리얼 변환기가 필요하다. Atmega328(혹은
Atmega168) 기반으로서 아두이노 우노와 거의 동일한 스펙을 가진다. 가장 크기가 작은 아
두이노 프로 미니에 대한 상세설명은 이 포스트를 참조.

[그림 1.2.3] 아두이노 미니 - 프로미니와 핀배열이 거의 동일함

1.2.2 아두이노 레오나르도 (arduino leonardo)와 마이크로(micro)

우노 보드는 usb 통신을 위해서 메인 프로세서와 별도의 칩을 사용하는데 반해서 레오나


르도 보드는 USB기능이 내장된 atmega32u4 를 메인 프로세서로 사용한다. 따라서 프로그
램 다운로드와 시리얼통신 포트가 독립적으로 동작된다. 단가가 우노보다 낮을 수 밖에 없
지만 실제작으로 우노나 레오나르도나 최저가로 검색하면 비슷한 가격대가 형성되어 있다.
우노에 비교한 장점은 키보드/마우스/조이스틱과 같은 주변기기로 인식시킬 수 있어서 활
용도가 높다는 것이다.
아두이노 개요 - 7

[그림 1.2.4] 아두이노 레오나르도

아두이노 마이크로는 레오나르도 보드와 동일한 프로세서를 사용한 소형 보드이다. 빵판


이나 기타 완제품을 제작하는데 용이하도록 같은 기능에 크기를 줄여놓은 것이다.

[그림 1.2.5] 아두이노 마이크로 (레오나르도 보드를 작게 만든 것)

1.2.3 아두이노 메가 2560/ADK (arduino mega 2560/adk)

앞서 소개한 보드들의 부족한 핀수와 입출력 기능을 보강한 것이 아두이노 메가 계열의


보드들이며 프로세서로 ATmgea2560을 사용한다.

[그림 1.2.6] 아두이노 메가 ADK


8 - 아두이노 개요

1.2.4 아두이노 두에 (arduino due)

다른 아두이노 제품들이 8-bit 마이크로콘트롤러인 AVR 기반인 것과 달리 아두이노 두


에는 32-bit ARM core 프로세서를 사용한다. 성능이 AVR기반의 아두이노들보다는 높고
핀 수가 매우 많아서 보다 전문적인 제품 개발과 연구 목적으로 사용할 수 있다.

[그림 1.2.7] 아두이노 두에(Due) : ARM 프로세서 기반의 아두이노

1.2.5 아두이노 융 (arduino yun)

레오나르도 기판과 HTTP 통신과 같은 온갖 텍스트 기반의 작업을 처리하는 Linino(리


눅스 변형 OS)를 구동하는 WI-FI 리눅스 기판을 내장하고 있다. 사물인터넷을 구현하기 위
한 플랫폼으로서 USB통신뿐만 아니라 와이파이를 통해서도 프로그램이 가능하다.

[그림 1.2.8] 아두이노 융(YUN)


아두이노 개요 - 9

1.2.6 갈릴레오 보드 (galilero board)

인텔의 쿼크(Quark) 프로세서 X1000 를 이용한 고성능의 아두이노 호환 보드이다. 최대


400MHz의 클럭속도를 가지는 초전력 x86 기반의 프로세서이다. 기존 아두이노 우노의 핀
배열을 가지며 유사한 개발 환경을 이용할 수 있다. 별도의 갈릴레오보드 전용 IDE가 제공
되지만 외형과 사용법은 아두이노 IDE 와 동일하다.

[그림 1.2.9] 인텔 갈릴레오 보드

1.2.7 기타 변종 아두이노들

'아두이노'는 등록된 상표이므로 정품 보드만 이 이름을 사용할 수 있다. 이런 이유로 보


통 파생 제품들은 두이노(duino)라는 접미어로 새로운 이름을 만들어서 사용한다. 개중에는
공식 아두이노와 완벽하게 호환되면서 가격은 더 저렴한 중국 제품들이 많이 있고 인터넷으
로 쉽게 구입할 수 있으며 이것을 이용해도 같은 개발도구와 라이브러리로 작업을 할 수 있
다. 몇몇 예를 들어보면 다음과 같다.

Freeduino
Funduino
Seeeduino
Korduino
Meduino (make block)
10 - 아두이노 개요

Paperduino
Boraduino
Roboduino
Femtoduino (가장 작은 아두이노 호환 복제품)
Ruggeduino (I/O 보호 기능이 내장된 아두이노 보드)
pcDuino (갈릴레오보드와 비슷한 개념)
Teensyduino

등등 이 외에도 수많은 변종 보드들이 개발되고 있으며 아두이노 코어를 사용하여 편의성


을 증대시킨 제품들도 속속 개발되고 있다.

1.3 아두이노 우노 R3
아두이노 우노 R3는 ATmega328P 라는 AVR 8-bit 마이크로콘트롤러를 사용한다. 우노는 이
탈리아어로 하나(one)의 의미이며 이름에서 알 수 있듯이 아두이노 보드들 중 가장 널리 사용되
는 표준 보드라고 할 수 있다. (이탈리아어로 uno, due, tre .. 는 일, 이, 삼 ... 이다.)
이 보드는 PC와 USB로 연결할 수 있으며 이것만으로도 전원 공급과 프로그램 다운로드 그리
고 PC와의 시리얼 통신을 수행한다.

[그림 1.3.1] 아두이노 우노 R3의 각 부분의 명칭 및 기능


아두이노 개요 - 11

1.3.1 전원 연결

아두이노 우노보드는 내부적으로 5V로 동작한다. 전원은 다음과 같이 두 가지 방법 중 한


가지로 인가하면 된다.

∙ USB로부터 5V 전원을 공급받아서 동작할 수 있다. 따라서 이 보드를 PC와 USB로 연


결하면 일단 기본적인 하드웨어 세팅은 끝난 것이다.
∙ 외부 전원을 연결하는 단자가 있는데 이것으로 7~12V 사이의 전원을 인가하면 되며
9V가 권장 전압이다. 일반적인 AA 혹은 AAA 사이즈의 1.5V건전지를 6개를 직렬 연결
하거나 1.2V 충전지를 사용해도 된다. 물론 AC어댑터도 전압 범위가 맞으면 사용 가능
하다.

만약 USB와 전원 소켓에 둘 다 연결되어 있다면 소켓에서 공급되는 전원을 자동으로 사용


하게 된다.

1.3.2 디지털 입출력 핀 14개 (0번~13번 핀)

디지털(digital) 입출력 핀들을 이용해서 외부의 이진 신호를 읽어들어나 또는 이진 신호


를 내보낼 수 있다. 이진 신호란 on/off 와 같이 상태값이 두 가지만을 가지는 신호라는 의
미이다. 이 디지털 핀을 이용해서 LED를 켜고/끄거나 외부의 스위치가 눌려져 있는지 아닌
지 등을 검출할 수 있다. 구체적으로 0V 와 5V 두 전압중 하나의 값을 가지며 이것은 프로
그램으로 제어할 수 있다.
디지털 입출력으로 사용되면서 또한 부가적인 기능을 가지는 핀들은 다음과 같다.

∙ 0번과 1번 핀은 시리얼 통신에 사용된다. USB로 PC와 통신을 할 수 있다.


∙ 2번과 3번 핀은 인터럽트 기능을 갖는다.
∙ 3, 5, 6, 9, 10, 11번 핀은 PWM 기능을 가지며 아날로그 출력을 흉내낼 수 있다.

1.3.3 아날로그 입력 핀 6개 (A0~A5)

아날로그 입력 핀이란 외부의 아날로그 입력값을 읽어들이는 핀으로서 주로 센서


(sensor)와 연결하여 사용된다. 아날로그(analog)신호는 디지털 신호와는 달리 연속값을 의
미하면 예를 들어서 온도, 빛의 세기 등이 있다. 이러한 물리량을 센서가 전기 신호로 변환
하며 이것을 이 아날로그 핀으로 읽어들일 수 있다. 센서를 통해 읽은 전압값은 0에서 1023
사이의 숫자로 변환된다. 기준 전압은 5V 이지만 1.1V의 내부 전압이 사용될 수 있으며
AREF핀으로 기준 전압을 직접 인가할 수도 있다.
12 - 아두이노 개요

그리고 아날로그 핀은 디지털 입/출력 핀으로도 사용할 수 있다.

1.3.4 아날로그 출력핀 6개 (3,5,6,9,10,11번핀)

디지털 출력핀이 0V/5V 두 가지 값만을 가질수 있는데 비해서 아날로그 출력핀은


0V~5V사이의 전압 값(256단계)을 가질 수 있다. 엄밀히 얘기하면 PWM방식으로 동작하므
로 순수 아날로그 방식은 아니다.

1.3.5 인터럽트 (2, 3번 핀)

2번 핀과 3번 핀은 인터럽트(interrupt) 기능을 가진다. 인터럽트 처리(interrupt


handling)라는 것은 이벤트를 처리하는 데 사용되는 기능으로서 특정한 신호가 발생했을
때 정해진 동작을 수행하여야 하는 경우 사용되는 방식이다. 예를 들어서 버튼이 눌려진 시
점에서 (또는 떼어진 시점에서) 어떤 작업을 수행해야 하는 경우다.

1.3.6 그 외의 기능들

아두이노 우노 보드는 3.3V의 전압도 공급할 수 있다. 이는 USB만 연결한 경우도 마찬가
지이다. AREF 핀은 아날로그 핀의 기준 전압을 설정하는 용도로 사용된다.
다른 기기와의 통신 기능은 다음과 같다.

∙ 시리얼 통신 : 0번, 1번 핀
∙ SPI 통신 : ICSP 헤더핀
∙ TWI (I2C) 통신 : A0, A1 핀

시리얼 통신 방식 외에는 일반적으로 사용 빈도가 낮지만 기기간 통신이나 일대다 통신을


하는 경우 SPI, TWI 통신이 널리 사용된다.

1.4 개발 환경
아두이노를 프로그래밍 하는데 C/C++언어가 사용된다. 만약 사용자가 C언어나 C++언어에 익
숙하다면 물론 좋겠지만 그렇지 않더라도 라이브러리가 사용하기 편하게 잘 갖추어져 있으므로
익히는 시간이 그리 많이 걸리진 않는다. 사실 사용하는 언어는 C++ 이지만 (백그라운드에서는
이것을 C코드로 변환하여 컴파일한다.) 잘 모른다고 미리 겁먹을 필요는 없다. 어차피 아두이노
라는 플랫폼 자체가 비전공자(디자이너, 예술가 등)들이 깊은 전공 지식 없이 개발을 할 수 있도
록 설계가 된 것이기 때문이다.
아두이노 개요 - 13

1.4.1 아두이노 IDE

프로그램을 작성/편집 하고 컴파일/디버깅 할 수 있는 통합 환경을 IDE(Integrated


development envirionment)라고 한다. 아두이노는 processing 이라는 오픈소스 언어 환경
(이것도 굉징히 흥미로운 프로젝트이다. processing.org 홈페이지에 자세한 내용이 있다.)을
이용하여 개발된 전용 IDE를 무료로 제공하고 있어서 편리하게 사용할 수 있다. 아래의 공
식 다운로드 링크에서 OS에 맞는 프로그램을 다운받아서 설치한다.

[그림 1.4.1] 아두이노 공식 홈페이지 www.arduino.cc

아두이노 IDE를 실행하기 전에 아두이노를 USB로 PC와 연결하는 것이 좋다. 아두이노 장


치는 가상 시리얼기기로 인식이 되기 때문에 연결을 한 후에 시리얼포트 번호를 확인해야
한다. 윈도우즈의 경우 제어판>시스템 및 보안>시스템>장치관리자 로 찾아들어간 후 포트
(COM&LPT) 항목을 보면 확인할 수 있다.
14 - 아두이노 개요

[그림 1.4.2] 아두이노 기기의 포트 번호 확인

이제 아두이노IDE를 실행시키면 <그림 1.4.3>와 같은 조금은 단순해 보이는 화면이 뜰 것이


다. 이 프로그램을 이용하여 아두이노 프로그램을 입력하고 컴파일한 후 (USB로 아두이노
보드와 연결되어 있다면) 다운로드까지 수행할 수 있다. 이 IDE는 사실 Processing 이라는
오픈소스 프로젝트에 기반한 것이다. 아두이노에서 취득한 데이터를 시각화하는데 많이 사
용되는 플랫폼이니 관심이 있다면 홈페이지(processing.org)를 방문해서 관련 정보를 취득
해 보는 것도 좋을 것이다.
아두이노 개요 - 15

[그림 1.4.3] 아두이노 IDE를 처음 실행한 모습

프로그램을 입력하기 앞서서 먼저 해야 할 것은 보드와 포트를 설정하는 것이다. 어떤 아두이노


보드를 사용하는지 그리고 연결된 포트의 번호는 무엇인지를 정확히 설정해야 정상적으로 프로
그램을 다운로드하거나 시리얼통신을 수행할 수 있다.

(1) 도구>보드 항목 : 연결된 보드의 종류를 선택


(2) 도구>포트 항목 : 가상 시리얼 포트의 번호를 선택

위의 두 항목만 올바르게 선택했다면 일단 개발 환경은 다 갖춘 셈이다. 만약 아두이노가 PC에


연결되어 있다면 포트는 자동으로 설정되어 있을 것이다.
16 - 아두이노 개요

[그림 1.4.4] 포트 번호 설정하는 메뉴

아두이노 IDE는 1.0.x 버전과 1.5.x버전이 존재한다. 1.5.x 버전의 차이점은 ARM 기반의
아두이노도 지원한다는 점이다.

1.4.2 아두이노 하프

전절에서 소개한 아두이노 IDE는 개발에 필요한 거의 최소한의 기능을 제공한다. 아두이
노 프로그래밍을 하다 보면 이러한 기능상의 제약이 무척 아쉽다. 이에 다양한 개발자들이
개발 편의성을 증대시켜주는 아두이노용 개발 환경을 만들어서 배포하고 있다. AVR을 개발
한 경험이 있다면 Atmel Studio 같은 전용 개발툴로도 아두이노 프로그래밍을 할 수 있으
므로 검토해 보는 것도 좋다.
필자도 새로운 윈도우용 아두이노 IDE를 교육용으로 개발하였고 이것을 아두이노 하프
(harp)라고 이름을 붙였다. 다음 그림은 아두이노 하프의 외형이다.
아두이노 개요 - 17

[그림 1.4.5] 아두이노 하프의 외형

이것을 개발하면서 기본적인 아두이노 IDE의 단순한 인터페이스는 그대로 유지하면서 개발


의 편의성을 높이는 몇몇 기능들을 추가하였다. 아두이노 하프의 중요한 특징은 다음과 같
다.

❶ 연결된 아두이노의 자동 인식.


❷ 아두이노 전용 함수/변수의 자동 완성.
❸ 아두이노 라이브러리 멤버 함수의 자동 완성.
❹ 시리얼 데이터를 각종 그래픽으로 도시할 수 있는 터미널.
❺ 유용한 코드 블록의 자동 삽입.

특히 자동 완성 기능은 아두이노 프로그래밍을 하면서 편의성을 크게 높이는데 일일이 아두


이노 함수나 상수의 철자를 완벽하게 타이핑해야 하는 수고를 크게 덜어주기 때문이다. 또
한 자주 쓰이는 함수 블록 예를 들면 초음파 센서의 거리 측정 함수라든지 디바운싱하는 루
틴들도 마련되어 있어서 메뉴를 선택하는 것으로만 입력이 가능하게 하였다. 각각에 대해서
는 차차로 설명하도록 하겠다.
Chapter

02
디지털 입출력
(Digital Input/Output)
20 - 디지털 입출력

2.1 디지털 포트의 개념


디지털 입출력 포트(digital I/O port, 혹은 그냥 포트)는 디지털 신호를 출력하거나 입력받을
수 있는 통로(물리적으로는 핀)이다. 디지털 신호는 0과 1 두 가지 상태만을 표현하므로 포트를
통해서 0 또는 1신호를 내보내거나 입력받을 수 있다. 아두이노 우노에는 13개의 디지털핀이 있
는데 이 핀들이 포트에 해당된다.

[그림 2.1.1] 아두이노우노의 디지털 핀들

앞으로의 설명을 위해서 약간의 전기회로 지식이 필요하다. 먼저 전압(voltage)과 전류


(current)의 개념을 설명하면 다음과 같다. 시중에서 흔히 구할 수 있는 AA나 AAA사이즈 건전
지 하나의 '전압'은 1.5V (V는 Volt 볼트, 전압의 단위) 라는 것은 알고 있을 것이다. 이 의미는
음극과 양극의 '전위차'가 1.5V라는 의미이며 일단 '전압은 전류를 흘릴 수 있는 힘' 정도로 이해
하면 된다. 이 전위차가 있는 두 부분을 도선으로 연결하면 전자가 도선을 따라서 흐르게 되는
데 이 전자의 흐름이 전류이다. 전자는 음극에서 양극으로 흐르며 반대로 양전하는 양극에서 음
극으로 흐른다. 전압(또는 전위차)이 높을수록 전자가 더 많이 흐르고 전류값도 높아진다. 전자
의 흐름인 전류의 단위는 암페어(Ampere, A로 표시함)이다. 전류는 '양전하의 흐름'이다.

∙ 전류(단위는 암페어, A) : 양전하의 흐름


∙ 전압(단위는 볼트, V) : 전류를 흘릴 수 있는 힘
디저털 입출력 - 21

[그림 2.1.2] 전압, 전류, 그리고 저항

위 그림을 보면 건전지의 +극과 -극을 저항 R로 연결하였다. (왼쪽은 건전지와 저항의 모양


을 그대로 그렸고 오른쪽은 이것을 기호로 표시한 것이다.) 이 경우 ‘전류’가 도선을 따라 화살표
방향으로 흐르게 되는데 전지의 전압을 v, 저항을 R이라고 하면 전류의 크기는 v/R로 계산된
다. 이것을 오옴의 법칙이라고 한다. 저항의 단위는 오옴(ohm)이다. '저항은 전류의 흐름을 제한
하는 역할을 하는 소자'이다.

∙ 저항(단위는 오옴 Ω) : 전류의 흐름을 제한하는 역할을 하는 소자


∙ 오옴의 법칙 :    

디지털 시스템에서 신호(signal)는 바로 이 '전압'으로 표현된다. 신호 0(LOW)은 0V (ground,


줄여서 GND라고 보통 표시하며 그라운드라고 읽는다)가 사용되고 1신호는 주로 5V, 3.3V 혹은
1.8V이다. 아두이노 우노의 경우 동작 전압이 5V이므로 신호 1 (HIGH) 은 전압으로 5V가 되는
것이다.

2.1.1 포트를 이용한 출력 내보내기

포트를 출력으로 사용하는 경우는 스위치를 생각하면 간단히 이해할 수 있다. 건전지와
연결된 전구사이에 스위치가 있는 간단한 실험 장치를 생각해 보면 된다. 다음 그림의 스위
치를 손가락으로 눌러서 연결(on 되었다고 한다)되면 전구에 전류가 흐로게 되므로 불이 켜
질 것이고 손가락을 떼면 (off되었다고 한다.) 전구가 꺼지게 된다.
22 - 디지털 입출력

[그림 2.1.3] 스위치 회로

포트는 이와 같이 핀에 연결된 회로에 전압을 인가하거나(1, HIGH 신호) 인가하지 않을 (0,


LOW 신호) 수 있는 스위치의 역할을 하는데 아두이노는 이 스위치를 프로그램을 통해서
on시키거나 off시킬 수 있다. 물론 아두이노 내부의 스위치는 이 그림처럼 기계적으로 눌러
야만 하는 스위치가 아니고 스위치 역할을 하는 트랜지스터라는 전자 소자이다. 이러한 스
위치 역할을 하는 전자소자를 프로그램으로 제어할 수 있으며 사용자가 원하는 타이밍, 주
기, 속도를 가지고 스위치를 켰다 끌 수 있다. 따라서 손으로 기계적인 스위치를 조작하는
것과는 비교할 수 없는 속도와 시간 정밀도로 개폐를 제어할 수 있다.

2.1.2 포트를 이용한 입력신호 받기

입력의 경우에는 출력과 반대로 포트핀과 연결된 부분의 전압이 0V(LOW)이냐 혹은 5V


이냐(HIGH)를 읽어들이는 기능을 한다. 보통 디지털 입력 실험을 할 때 처음으로 접하는
부품이 택스위치 회로인데 택스위치가 눌려졌는지 혹은 떼어졌는지를 포트의 입력 기능으
로 판별할 수 있다.
[그림 2.1.4] (a)에서 화살표 표시된 곳의 전압은 5V인데 저항에 전류가 흐르지 않아 저
항 양단에 전위차가 발생하지 않기 때문이다. 반면에 (b)를 보면 화살표 된 곳의 전압은 0V
인데 GND와 직결되어 있기 때문이다. 이제 (c) 그림을 보면 스위치를 안 눌렸을 때 (a)와
같고 스위치를 누르면 (b)그림과 같다는 것을 알 수 있다. 따라서 스위치를 눌렀을 때와 안
눌렸을 때의 전압값이 달라지므로 포트에서 이 전압값을 읽어 들여서 스위치의 상태를 검출
할 수 있다. 여기에선 사용된 저항 R을 '풀업(pull-up) 저항'이라고 한다.
디저털 입출력 - 23

[그림 2.1.4] 디지털 입력의 개념 회로도

2.2 LED 깜박이기


전자 소자인 LED(light-emitting diode)는 전류가 흐르면 그것을 빛에너지로 바꾸어 빛을 내
는 소자이다. 우리 주변에서 흔히 볼 수 있는데 차량이나 가정의 조명등, 신호등 같은 데서 널리
사용되고 있으며 전력 효율이 좋기 때문에 기존의 형광등을 빠르게 대체하고 있다.

[그림 2.2.1] 여러가지 모양과 색상의 LED들

2.2.1 내장 LED 깜박이기

아두이노 우노보드에는 실습용 LED가 실장되어 있는데 이것은 13번 핀과 연결되어 있다.
24 - 디지털 입출력

[그림 2.2.2] 아두이노 우노 보드에 실장된 LED. 13번 핀과 연


결되어 있다.

이것을 이용하여 깜박이는 예제는 아두이노 IDE에 미리 탑재되어서 제공되는데 다음과 같


이 예제 파일을 선택하여 열 수 있다.

[그림 2.2.3] 아두이노 IDE 에서 Blink 예제를 연다.

이 예제는 매우 간단하게 작성되어 있으며 소스 코드는 다음과 같다. (주석문은 삭제하였


다.) 여기에서 이 소스코드의 구조를 살펴볼 필요가 있다.
╒══════════════════════════════════
// Pin 13 has an LED connected on most Arduino boards.
디저털 입출력 - 25

// give it a name:
int led = 13;
// the setup routine runs once when you press reset:
void setup() {
// initialize the digital pin as an output.
pinMode(led, OUTPUT);
}
// the loop routine runs over and over again forever:
void loop() {
// turn the LED on (HIGH is the voltage level)
digitalWrite(led, HIGH);
delay(1000); // wait for a second
// turn the LED off by making the voltage LOW
digitalWrite(led, LOW);
delay(1000); // wait for a second
}
╘══════════════════════════════════
이 예제의 구조를 살펴보면 먼저 눈에 띄는 것이 있는데 setup() 함수와 loop() 함수이다.
이 두 함수는 아두이노 프로그램의 전체적인 구조를 잡아주는 역할을 한다.

❶ setup() 함수
• 아두이노 프로그램이 실행될 때 맨 처음에 단 한 번 호출되어 수행된다.
• 여기에는 각종 장치를 초기화하거나 초기값을 설정하는 코드가 오게 된다.

❷ loop() 함수
• setup()함수 실행 후 수행되면 계속 반복 수행된다.
• 아두이노에 연결된 장치들을 구동시키는 코드가 위치한다.

setup()함수 내에서 pinMode()함수를 사용하는데 이 함수는 핀을 입력(INPUT) 혹은 출력


(OUTPUT)으로 사용할 지를 설정하는 함수이다. 첫 번째 인자로 핀의 번호가 두 번째 인자
로 INPUT, 혹은 OUTPUT이라는 상수를 넘겨주면 된다. pinMode(led, OUTPUT) 은 led(13
번)을 출력(OUTPUT)으로 사용하겠다고 설정하는 것이다.

• pinMode(pinNumber, INPUT/OUTPUT) 함수
• pinNumber 핀을 입력(INPUT) 혹은 출력(OUTPUT)으로 사용할 지를 지정한다.
• pinNumber는 우노의 경우 0,1,2, … 13, A0, A1, ...A6 중 하나이다.

loop()함수에서는 digitalWrite()함수를 사용했는데 이 함수는 핀으로 출력값을 내보내는 작


업을 수행한다. 첫 번째 인자로 핀 번호를 받고 두 번째 인자로 HIGH 혹은 LOW 상수를 받
는다. digitalWrite(led, HIGH) 명령은 led 핀에 HIGH 신호를 내보내므로 LED가 켜지게 된
다. 반대로 digitalWrite(led, LOW) 명령은 led 핀에 LOW 신호를 내보내므로 LED가 꺼지
26 - 디지털 입출력

게 된다.

• digitalWrite(pinNumber, HIGH/LOW) 함수
• pinNumber 핀이 출력일 경우에 사용한다.
• 1값을 내보낼지 (HIGH) 0값을 내보낼지(LOW)를 지정한다.

delay()함수는 입력된 시간만큼 아무 일도 안하고 멈춰있는 동작을 수행한다. 입력받는 숫자


는 ms 단위이다.

• delay(time) 함수
• time ms 만큼 지연 (아무런 일도 안 하고 멈춰 있음)

입력이 밀리세컨드 단위라는 것에 유의해야 한다. 예로 delay(1000)은 1000ms 동안 지연시


키는 것이고 delay(10)은 10ms (100분의 1초) 동안 지연시키는 것이다.

2.2.2 신호등 실험

전 절에서는 내장 LED를 이용하여 실험을 진행했지만 사용자가 원하는 디지털 핀에 LED


를 연결할 수 있다. 예를 들어서 7번 핀과 LED를 연결하려면 다음 그림과 같이 결선하면
된다. 주의할 점은 LED는 극성이 있는 소자이므로 긴 다리가 포트핀에 연결이 되어야 하며
짧은 쪽인 GND쪽에 연결되어야 한다. 그리고 전류를 제한하기 위해서 반드시 저항을 직결
해야 하는데 330Ω~1kΩ 사이의 저항을 연결하는 것이 좋다. 저항은 극성이 없으므로 연결
할 때 방향을 고려하지 않아도 되지만 LED는 극성이 있으므로 잘못 연결하면 불이 켜지지
않는다.

[그림 2.2.4] 아두이노의 7번 핀에 LED를 연결


한 회로도.
디저털 입출력 - 27

이제 실험을 위하여 다음과 같이 세 개의 LED가 연결되어 있다고 하자.

• 12번 핀 : 빨간색 LED


• 11번 핀 : 노란색 LED
• 10번 핀 : 초록색 LED

회로를 좀 더 편리하게 구성하기 위해서 빵판이 포함된 프로토타잎 쉴드를 사용한다. 여러


종류의 프로토타잎 쉴드가 있으나 본서에서는 [그림 2.2.5]에 도시된 DFROBOT사의 제품
을 이용하였다. 이것은 아두이노 우노의 헤더핀에 끼워서 사용할 수 있으며 리셋 버튼 외에
별도의 버튼과 ICSP 헤더핀 및 다수의 GND, 5V헤더핀이 마련되어 있어서 결선할 때 무척
편리하다.

[그림 2.2.5] DFROBOT사의 프로토타잎 쉴드

첫 번째 실험으로 세 개의 LED가 동시에 0.5초 간격으로 깜박이는 프로그램을 작성해 보


자. 다음 프로그램에서 문자상수를 정의하기 위해서 #define 명령문을 이용하였다. 이렇게
문자상수로 정의해 두면 추후에 회로가 변경되어 핀 번호가 바뀐다 하더라도 손쉽게 프로그
램을 수정할 수 있게 되므로 중요한 상수는 가급적 문자상수로 정의하는 습관을 들이는 것
이 좋다.
28 - 디지털 입출력

[그림 2.2.6] 프로토타잎 쉴드에 LED 세 개와 저항을 연결한 예시

╒══════════════════════════════════
#define RED 12
#define YELLOW 11
#define GREEN 10

void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}

void loop() {
digitalWrite(RED, HIGH);
digitalWrite(YELLOW, HIGH);
digitalWrite(GREEN, HIGH);
delay(500);
digitalWrite(RED, LOW);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, LOW);
delay(500);
}
╘══════════════════════════════════

두 번째로 신호등처럼 다음과 같은 순서로 차례대로 점멸되는 프로그램을 작성해 보자.

빨간색만 ➜ 빨간색과 노란색 ➜ 초록색 ➜ 다시 반복

각 신호의 간격은 1초로 정한다. 여러 가지 방법이 있겠지만 이것을 수행하기 위해서 예를


들면 다음과 같이 알고리듬을 작성할 수 있을 것이다.
디저털 입출력 - 29

❶ 처음에 빨간색만 켜고 나머지 두 개는 끄고 1초 지연한다.


❷ 노란색을 켜고 1초를 지연한다.
❸ 빨간색과 노란색을 끄고 초록색을 켠후 1초를 지연한다.
❹ 다시 ❶부터 반복한다.

또한 앞서 언급했듯이 이 시간도 상수로 미리 정의해 두면 나중에 프로그램을 수정하기가


용이하다. 프로그램은 다음과 같다.

╒══════════════════════════════════
#define RED 12
#define YELLOW 11
#define GREEN 10
#define TIME 1000 // 1 sec

void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}

void loop() {
digitalWrite(RED, HIGH);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, LOW);
delay(TIME);

digitalWrite(YELLOW, HIGH);
delay(TIME);

digitalWrite(RED, LOW);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, HIGH);
delay(TIME);
}
╘══════════════════════════════════

2.3 부저를 이용한 실험

본 절에서는 부저(buzzer)를 이용한 실험을 해 보겠다. 부저는 소리를 내는 부품으로서 소리


파형을 만들어서 인가하여야 하는 것과 그냥 정해진 전압을 걸면 소리가 나는 두 가지 종류가
있으나 실험의 편의상 후자를 택해서 진행한다.
30 - 디지털 입출력

[그림 2.3.1] 부저의 외형

주의할 점은 다이오드와 마찬가지로 부저는 극성이 있으며 다리가 긴쪽이 (+)이다. 여기에 전압
을 걸면 삐~~ 하는 전자음이 발생한다.
실험을 위해서 다음 그림과 같이 아두이노의 7번 핀에 부저의 (+)를, GND핀에 (-)를 연결했
다.

[그림 2.3.2] 부저를 11번핀에 연결 (a) 회로도 (b) 실제 장착 모습

이렇게 연결했다면 부저를 울리는 것은 LED를 동작시키는 것과 똑같다.

2.3.1 첫 번째 실험

단순히 1초 간격으로 반복해서 부저가 울리는 프로그램은 다음과 같이 하면 된다.

╒══════════════════════════════════
#define BUZ 7
#define TIME 50
void setup() {
pinMode(BUZ, OUTPUT);
}
void loop() {
digitalWrite(BUZ, HIGH); // 부저가 울리기 시작한다.
디저털 입출력 - 31

delay(TIME);
digitalWrite(BUZ, LOW); // 부저가 멈춘다
delay(1000-TIME);
}
╘══════════════════════════════════

이 예제에서는 부저가 울리는 시간을 50ms로 설정했으므로 짧은 시간 동안 ‘삑’하는 소리가


날 것이다. loop()함수 안에 있으므로 이것이 1초 간격으로 반복된다.

2.3.2 두 번째 실험

두 번째 예제로 13번 핀에 연결된 내장 LED와 조합해서 동작하도록 해보자. LED는 0.5


초마다 한 번씩 켜지고 부저는 1초 마다 한 번씩 울리게 해서 마치 타이머처럼 동작하도록
하는 예제이다.

╒══════════════════════════════════
#define BUZ 7
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(BUZ, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(BUZ, HIGH);
delay(50);
digitalWrite(BUZ, LOW);
delay(200);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
}
╘══════════════════════════════════

2.3.3 세 번째 실험

본 부절에서는 앞 절의 신호등 실험에 부저를 추가하여 새로운 실험을 하도록 하겠다. 동


작 순서는 다음과 같다.

❶ 빨간색만 켜진다. (6초)


❷ 빨간색과 노란색이 같이 켜진다. (3초)
❸(a) 초록색이 켜진다. (3초)
32 - 디지털 입출력

(b) 초록색이 1초 간격으로 깜박이면서 부저도 울린다. (3초)

즉, 파란불의 신호가 절반이 지나가면 깜박이면서 부저도 울려서 경고음을 발생시키는 것이


다.

╒══════════════════════════════════
#define RED 12
#define YELLOW 11
#define GREEN 10
#define BUZZER 7

void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
pinMode(BUZZER, OUTPUT);
}

void loop() {
leds(HIGH, LOW, LOW, 6000);
leds(HIGH, HIGH, LOW, 3000);
leds(LOW, LOW, HIGH, 3000);

leds(LOW, LOW, LOW, 490);


beep(10);
leds(LOW, LOW, HIGH, 500);
leds(LOW, LOW, LOW, 490);
beep(10);
leds(LOW, LOW, HIGH, 500);
leds(LOW, LOW, LOW, 490);
beep(10);
leds(LOW, LOW, HIGH, 500);
}

void leds(bool r, bool y, bool g, int time) {


digitalWrite(RED, r);
digitalWrite(YELLOW, y);
digitalWrite(GREEN, g);
delay(time);
}

void beep(int time) {


digitalWrite(BUZZER, HIGH);
delay(time);
digitalWrite(BUZZER, LOW);
}
╘══════════════════════════════════

이 예제에서는 반복적으로 사용되는 부분은 함수로 정의하였음을 유의하자. 함수의 정의부


분은 호출되는 곳의 위에 와도 되고 아래에 위치해도 상관없다. 보통은 setup() 함수와
디저털 입출력 - 33

loop()함수를 맨 위에 위치하고 다은 함수 정의는 그 밑에 놓아서 가독성을 높이는 것이 바


람직하다.
1

2.4 터치센서
이번 절에서는 터치센서를 이용하여 디지털 입력 값을 받는 실험을 진행해보도록 하겠다. 시
중에서 구할 수 있는 터치센서 모듈의 외형은 다음 그림과 같다. 금속판을 도전체로 (인체도 가
능함) 터치할 때 출력 신호가 바뀐다. 즉, 터치시에 HIGH를 아닐 때는 LOW 신호를 내보낸다.

[그림 2.4.1] 터치센서 모듈

이 모듈은 선이 세 가닥이 있다. 두 가닥은 전원선이고 나머지 하나는 신호선이다. 이 신호선을


아두이노의 디지털 핀에 연결하면 되는데 예를 들어서 3번 핀에 연결한다면 다음과 같다.

[그림 2.4.2] 터치센서 연결도

이 모듈의 동작은 간단하다. 터치가 안 되어 있다면 신호선으로 LOW 신호가 나오고 터치가 되
었다면 HIGH 신호가 나온다. 따라서 이 디지털 신호를 읽으면 터치 패널에 현재 터치가 되었는
34 - 디지털 입출력

지 아닌 지를 판별할 수 있다.

2.4.1 첫 번째 예제

단순하게 터치가 된 상태라면 LED가 켜진 상태를 유지하고 아니라면 LED도 꺼진 상태를 유


지하는 매우 간단한 프로그램을 작성해 보도록 하겠다.

╒══════════════════════════════════
#define TS 3 // 터치센서에 연결된 핀 번호
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(TS, INPUT);
}
void loop() {
int iTouched = digitalRead(TS);
digitalWrite(LED_BUILTIN, iTouched);
}
╘══════════════════════════════════

[그림 2.4.3] 터치센서 실험 영상

이것을 더 간단하게 줄이면 다음과 같이 iTouched 변수를 생략할 수 있다.

╒══════════════════════════════════
#define TS 3 // 터치센서에 연결된 핀 번호
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(TS, INPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, digitalRead(TS));
}
╘══════════════════════════════════
디저털 입출력 - 35

2.5 택스위치
택스위치(tact switch)는 기계적인 접점을 가지는 스위치로서 누르면 접점이 닫히고 누르지
않은 상태에서는 접점이 열려있는 소자이다.

[그림 2.5.1] 여러 가지 택스위치들

택스위치는 다리가 2개인 것과 4개인 것이 있다. 왜 궂이 다리가 4개가 필요할까 의아하겠지만


스위치 배열 회로를 꾸밀 때 유용하다.

아두이노의 회로를 제시하기 전에 풀업저항(pull-up resister)에 대한 개념이 필요하다. 먼저


다음 그림(a)를 살펴보면 포트핀에 아무 것도 연결되어 있지 않다. 이런 상태에서 이 핀의 입력
값을 읽어들이면 어떻게 될까? Vcc(보통은 5V) 인가 GND(0V)인가? 당연히 0V로 짐작하기 쉽
지만 이 경우는 0V도 5V도 아닌 붕 뜬 (floating) 상태이다.
디지털 포트가 어떤 값을 가지려면 확실하게 그림 (b)와 같이 Vcc 나 그림 (c)와 같이 0V에
확실하게 연결이 되어 있어야 한다. 그림 (b)에서 보면 붕 뜬 상태의 포트핀의 전위를 저항을 통
해서 Vcc(5V) 로 끌어 올려 (pull up) 주고 있다. 따라서 이 저항을 풀업저항이라고 하며 보통
5kΩ~10kΩ 정도의 저항을 사용한다. 포트핀을

(a) 붕 뜬 상태(floating) (b) HIGH (c) LOW

[그림 2.5.2] 버튼의 상태


36 - 디지털 입출력

이제 다음 그림을 보면 스위치가 GND에 연결되어 있다. 스위치가 오픈되어 있다면(즉, 안 눌


려졌다면) 이 회르는 그림 (b)와 같다. 만약 눌려졌다면 이 회로는 그림 (c)와 같다. 따라서 스위
치가 눌려졌느냐 안 눌려졌느냐에 따라서 포트핀의 디지털 신호는 HIGH가 되거나 LOW가 되는
것이다. 이것으로 스위치가 눌려졌는지 안 눌려졌는지를 감지할 수 있게 된다.

[그림 2.5.3] 풀업저항 회로

그런데 아두이노에서는 그림과 같이 풀업저항을 외부에서 달아줄 필요가 없는데 아두이노의


프로세서인 AVR 칩의 내부에 모든 포트핀에 이 풀업저항이 아예 내장되어 있기 때문이다. 이것
의 용량은 약 10kΩ 정도이며 이를 선택적으로 (프로그램적으로) 연결할 수 있고 연결하지 않을
수도 있다. 이것을 내부(internal) 풀업저항이라 한다.

이제 다음 그림과 같이 아두이노의 디지털핀과 연결하여 접점의 상태를 읽을 수 있다. 일단 2


번 핀에 스위치의 한 쪽 다리를 연결하고 GND에 다른 쪽 다리를 연결해야 한다.
디저털 입출력 - 37

그림 2.5.4 택스위치 결선도

<그림 2.4.2>와 같이 회로가 구성되었다면 pinMode() 함수를 이용하여 스위치가 연결된 핀을 입


력으로 설정해야 하는데 다음과 같이 두 가지 방법이 있다.

❶ pinMode(2, INPUT ) ; // 2번 핀을 입력으로 설정


❷ pinMode(2, INPUT_PULLUP ); // 2번 핀을 입력으로 설정 후 내부 풀업 저항 연결

❶은 단순히 2번 핀을 입력으로 설정하는 것이다. ❷는 2번 핀을 입력으로 설정한 후 내부 풀


업(pull-up)저항까지 연결하도록 하는 것이다.

기계적인 스위치를 핀에 연결할 때는 보통 내부 풀업저항을 연결해서 사용한다. 내부 풀업저


항이 연결되었다면 <그림 2.4.2>의 회로 동작은 간단하다. 내부에 풀업 저항이 연결되어 있으므
로 안 눌렸다면 2번 핀으로 HIGH신호가 나오고, 눌렸다면 LOW 신호가 나온다. 따라서 이 디지
털 신호를 읽으면 택스위치가 눌렸는지 아닌지를 판별할 수 있다.

∙ 스위치가 눌렸다면 LOW 신호


∙ 스위치가 안 눌렸다면 HIGH 신호

이 핀값을 읽을 때는 digitalRead()함수를 사용하면 된다.

∙ digitalRead(2); // 2번 핀의 값을 읽는다

예를 들어서 이 값을 변수에 저장할 수도 있다.


38 - 디지털 입출력

byte bySwitchIn = digitalRead(2);

2.4.1 스위치 입력 예제

첫 예제로 버튼을 누르고 있으면 내장된 LED가 켜지는 간단한 기능을 구현해 보자. 누르
고 있는 동안에만 켜져 있고 누르지 않고 있다면 꺼지도록 하는 것이다.
╒══════════════════════════════════
#define SW 2
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(SW, INPUT_PULLUP); // 내부 풀업 저항을 연결한다.
}
void loop() {
digitalWrite(LED_BUILTIN, !digitalRead(SW));
}
╘══════════════════════════════════
1

2.6 인체감지센서
인체감지센서 모듈은 적외선을 감지하는 센서를 장착하고 있으며 약 6미터 이내의 인간이나
동물의 움직임을 감지할 수 있다. 인체나 동물의 몸과 같이 열을 발산하는 곳에서 발생하는 적
외선을 감지하는 원리이다.

[그림 2.6.1] 인체 감지 센서 모듈

이 센서 모듈은 인체나 동물의 몸체에서 발산되는 열적외선을 감지한다. 둥근 캡은 적외


선을 증폭하는 효과가 있다. 약 6m 이내의 인체의 움직임을 감지할 수 있다.
전원과 신호를 전달하는 세 핀을 가지고 있고 5V 전원을 받아서 인체가 감지되면 신호선
은 약 3.3V 전압을 띠게된다. 따라서 신호선을 아두이노의 디지털 핀에 연결하면 쉽게 인체
감지 여부를 알 수 있게 된다.
또한 인체가 감지되었을 때 high 신호를 유지하는 시간을 설정하는 조절기가 내장되어
디저털 입출력 - 39

있어서 이 시간을 0,3초~18초 사이로 조절할 수 있다. 이 조절 기능을 이용하면 인체가 감


지되었을 때 점등되는 시스템을 아주 간단하게 구현할 수 있다. 모델에 따라서 열적외선 센
서의 감도를 조절하는 조절기가 내장된 경우도 있다.

2.6.1 첫 번째 예제

인체 감지 센서 모듈의 신호선이 4번 핀에 연결되어 있다고 가정하자.


╒══════════════════════════════════
#define HBS 4
void setup() {
pinMode(HBS, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite( LED_BUILTIN, digitalRead(HBS) );
}
╘══════════════════════════════════

이 예제를 실행해 보면서 인체가 감지되는 거리 범위와 LED가 켜지는 시간을 파악하고 조
절하여 보자.
40 - 디지털 입출력

연습문제

1. 아두이노에 적색, 황색, 녹색 LED와 부저 그리고 택스위치 하나, 터치센서 하나가 연결되어
있다고 가정하고 다음과 같은 동작을 하는 프로그램을 작성하라.

❶ 스위치도 눌려져 있지 않고 터치센서도 터치되어 있지 않다면 LED는 모두 꺼지고 부저


도 울리지 않는다.
❷ 택스위치만 눌려져 있을 때 녹색LED만 켜진다.
❸ 터치센서만 터치되었을 때는 황색LED만 켜진다.
❹ 택스위치가 눌려지고 터치센서가 터치된 상태에서는 적색LED가 켜지고 동시에 부저가
울린다.
Chapter

03
시리얼 통신
USART
42 - 시리얼 통신

3.1 시리얼통신 개요
시리얼 통신(serial communication)은 기기들 사이에 데이터를 주고받는 방법 중 하나인데 병
렬 통신 (parallel communication) 방식에 비해서 통신선의 개수가 적다는 장점이 있다. 아두이
노는 UART (universal asynchronous receiver and transmitter), SPI, I2C 방식의 시리얼 통신
을 지원하는데 이중 UART는 주로 아두이노와 PC간의 통신에 사용된다. 아두이노는 하나 이상
의 UART 통신기가 내장되어 있는데 라이브러리와 IDE에 내장된 터미널(terminal, 주고 받는
데이터를 확인할 수 있는 프로그램)을 이용하면 손쉽게 PC와의 통신을 수행할 수 있다.
아두이노 우노의 경우 0번과 1번핀이 시리얼 통신에 사용된다. 이 핀들은 내부적으로 USB통
신을 담당하는 칩과 연결되어서 USB신호로 변환된 후 PC에 전달된다. 반대로 PC에서 보내지는
USB신호는 이 칩에서 시리얼 통신 신호로 변환되어 아두이노의 AVR에 전달된다. 따라서 만약
아두이노가 PC와의 통신을 수행하고 있다면 이 핀들을 다른 용도로 사용하면 안된다. 그리고 통
신을 수행할 때에는 TX, RX라고 마킹된 LED가 깜빡인다. 일단 사용자는 아두이노와 PC간에
USB케이블로 연결하면 통신 실습을 할 준비가 끝나게 된다.

[그림 3.1.1] 시리얼 통신에 사용되는 두 개의 핀(빨간색)과 usb변환 칩(하늘색).


내부적으로 USART 신호는 USB신호로 변환되어 PC에 전송된다.

3.1.1 첫 번째 예제

첫 번째 예제로 아두이노에서 PC로 간단한 데이터를 전송하는 예를 해보도록 하겠다. 단


순히 아두이노에게 전원을 인가하면 “I’m ready.” 라는 메세지를 PC에 전송하는 예이다.
╒══════════════════════════════════
void setup() {
Serial.begin(9600);
Serial.print("I'm ready.");
}
void loop() {
}
╘══════════════════════════════════
시리얼 통신 - 43

이 예제를 다운로드 한 후 터미널을 켜고 리셋버튼을 누르면 터미널에 “I’m ready.”라는 문


자열이 찍히는 것을 확인할 수 있다. 터미널 실행 버튼을 누르면 아두이노에 자동으로 리셋
신호가 걸려서 프로그램이 처음부터 수행된다. 그리고 프로그램 다운로드 중에서는 터미널
을 실행시키지 못 한다는 것도 알아두자.

[그림 3.1.2] 첫 번째 시리얼 통신 예제 실행 결과

USART와 관련된 아두이노의 라이브러리는 Serial클래스에 다 모여 있다. 일단 이 예제에


서 보면 setup()함수내에서 두 개의 함수가 호출되었다.

■ void Serial.begin(long baud_rate) 함수


• USART 통신을 초기화 시킨다.
• 통신 속도(baud rate)를 입력으로 받는다.
• 9600, 19200, 57600, 115200 등 여러 baud rate를 지원한다.
• 반드시 터미널의 통신속도는 설정한 속도와 같게 맞추어 주어야 한다.

■ long Serial.print(val) 함수
• 입력값을 ASCII값으로 변환하여 PC쪽으로 출력한다.
• 전송된 데이터의 바이트 수를 리턴한다. (잘 사용되지 않음)
• 비동기 통신 방식이므로 데이터가 전송되기 전에 리턴된다.
• 입력 변수 val은 어떤 데이터 타입도 가능하다. 예를 들면

Serial.print(78); //gives "78"


Serial.print(1.23456); //gives "1.23"
Serial.print('N'); //gives "N"
Serial.print("Hello world."); //gives "Hello world."
44 - 시리얼 통신

• 두 번째 인수로 출력 형식을 지정할 수 있다. 예를 들면

Serial.print(78, BIN); //gives "1001110"


Serial.print(78, OCT); // gives "116"
Serial.print(78, DEC); // gives "78"
Serial.print(78, HEX); // gives "4E"
Serial.print(1.23456, 0); // gives "1"
Serial.print(1.23456, 2); // gives "1.23"
Serial.print(1.23456, 4); // gives "1.2346"

첫 번째 예제는 setup() 함수 내에서 UART를 초기화 하고 문자열 하나를 보내는 아주 간단


한 예제이다.
한 가지 주의할 것은 만약 Serial.setup( ) 함수로 설정한 속도로 터미널의 통신 속도로
같게 맞추어 주어야만 정상적으로 통신이 수행된다는 것이다. 터미널의 하단에 속도를 설정
할 수 있는 콤보박스가 있으며 그것을 이용하여 통신 속도를 지정해 줄 수 있다.

[그림 3.1.3] 터미널의 통신 속도 지정

✱ 보레이트(baud rate) 란?

시리얼 통신의 속도를 나타내는 숫자로서 단위는 초당 전송되는 비트 수이다.


따라서 9600이라면 초당 9600비트를 전송할 수 있다. 1바이트는 8비트이므로
이는 초당 1200바이트, 즉 1.2kbyte 를 전송할 수 있는 속도이다. 다양한 속도
를 지정해 줄 수 있으며 시리얼 통신으로 지정해 줄 수 있는 최대 속도는 약 1
메가 bps 정도이다.
시리얼 통신 - 45

3.1.2 두 번째 예제
두 번째 예제로 아두이노 디지털핀에 연결된 버튼을 누르면 메세지를 PC쪽에 보내는 예
제를 작성해 보자. 택스위치가 2번 핀에 연결되어 있다고 가정하고 그 버튼이 눌려졌다면
“pressed!”라는 문자열이 터미널에 표시되도록 해보자. Serial.print() 함수를 이용하면 매우
간단하게 작성할 수 있다.
╒══════════════════════════════════
#define SW 2
void setup() {
Serial.begin(9600);
pinMode(SW,INPUT_PULLUP);
}
void loop() {
if (digitalRead(SW)==LOW)
Serial.print("pressed!");
}
╘══════════════════════════════════
이 예제를 실행시키면 버튼을 누르고 있드면 “pressed!”라는 문자열이 연속적으로 PC로 전
송되게 된다.

3.1.3 세 번째 예제
세 번째 예제로 이번에는 PC에서 문자 하나를 받아서 그것이 ‘0’이면 LED를 끄고 ‘1’이면
LED를 켜는 프로그램을 작성해 보자. 이 경우 아두이노는 데이터가 사용자로부터 들어올
때 까지 대기 상태로 있다가 데이터가 읽히면 거기에 맞는 동작을 수행해야 한다.
╒══════════════════════════════════
#define LED 13
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
if ( Serial.available() ) {
char command = Serial.read();
if (command == '0') {
digitalWrite(LED, LOW);
Serial.println("LED off.");
}
else if (command == '1') {
digitalWrite(LED, HIGH);
Serial.println("LED on.");
}
else {
Serial.print("Wrong command :");
Serial.println(command);
}
}
}
╘══════════════════════════════════
46 - 시리얼 통신

이 예제에서는 다음과 같은 함수들이 사용되었다.

■ int Serial.available() 함수
• 전송되어 내부버퍼(64바이트)에 저장된 데이터의 개수를 반환한다.
• 입력인수는 없다.

■ int Serial.read() 함수
• 전송되어 내부 버퍼(64바이트)에 저장된 데이터 중 가장 첫 번째 데이터(ASCII코드)
를 읽어서 반환한다.
• 이 함수가 수행되면 내부 버퍼의 크기는 하나가 줄어든다.
• 내부 버퍼가 비었다면 -1을 반환하다.

■ int Serial.println() 함수
• 출력 문자열의 끝에 줄바꿈 기호 ‘\r\n’ 가 자동으로 붙는다는 점 외에는
Serial.print()함수와 동일한 동작을 수행한다.

이 예제에서는 USRT 통신기의 내부 버퍼(buffer)의 개념을 이해할 필요가 있다. 내부 버


퍼란 전송된 데이터가 일시적으로 저장되는 내부 메모리를 말하며 데이터가 전송된 순서대
로 저장된다. 아두이노의 시리얼 통신부의 내부 버퍼의 크기는 64바이트이다. 전송되어 저
장된 데이터를 사용하려면 내부 버퍼에서 이 데이터를 읽어내야 하는데 이때 사용되는 함수
가 Serial.read()함수이다. 가장 먼저 전송된 데이터 하나를 읽어낸 후 그 데이터는 버퍼에서
삭제되며, 만약 버퍼가 비었다면 -1을 반환한다. 따라서 버퍼에 읽어낼 데이터가 있는지 없
는지를 먼저 검사하는 것이 일반적이고 이때 사용되는 함수가 Serial.available()이다. 이 함
수는 버퍼에 저장된 데이터의 개수를 반환하며 만약 버퍼가 비어있다면 0을 반환한다.

if ( Serial.available() ) { 명령어들 }

또한 if() 문은 조건을 검사하는 명령어다. 만약 조건문이 참이라면 {명령어들}이 수행되는


데 C/C++에서 조건식은 0값만을 거짓으로 그 외의 값들은 참으로 판단한다. 따라서 0이 아
니라면 명령어들이 수행되고 이로 부터 버퍼에 데이터가 하나라도 있다면 명령어들이 수행
된다는 것을 알 수 있다.
이런 사항들을 이해하였다면 이 예제를 이해하는데 어려움이 없을 것이다. 데이터가 전송
되어 오면 그것을 읽어들여서 ‘1’이면 LED를 켜고, ‘0’이면 끈다. 그 이외의 데이터에 대해서
는 잘못된 명령이라는 문자열을 출력한다.

3.1.4 네 번째 예제

네 번째로 11번 핀에 부저가 연결되었다고 가정하고 명령어를 하나 받아서 다음과 같은


시리얼 통신 - 47

동작을 수행하는 예제를 작성해 보도록 하겠다.

∙ ‘0’ : LED를 끈다.


∙ ‘1’ : LED를 켠다.
∙ ‘2’ : 부저를 짧게 한 번 울린다. (삑)
∙ ‘3’ : 부저를 짧게 두 번 울린다.(삐삑)
∙ 그 외의 명령어들은 잘못된 것이라는 메세지를 출력한다.

이 예제의 경우는 처리해야 될 경우의 수가 많기 때문에 if-else 명령보다는 switch-case


명령이 더 효율적이다.
╒══════════════════════════════════
#define LED 13
#define BUZ 11
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUZ, OUTPUT);
Serial.begin(9600);
}
void loop() {
if ( Serial.available() ) {
char command = Serial.read();
switch(command) {
case '0':
digitalWrite(LED, LOW);
Serial.println("LED off.");
break;
case '1' :
digitalWrite(LED, HIGH);
Serial.println("LED on.");
break;
case '2' :
digitalWrite(BUZ, HIGH);
delay(50);
digitalWrite(BUZ, LOW);
break;
case '3' :
digitalWrite(BUZ, HIGH);
delay(50);
digitalWrite(BUZ, LOW);
delay(50);
digitalWrite(BUZ, HIGH);
delay(50);
digitalWrite(BUZ, LOW);
break;
default:
Serial.print("Wrong command :");
Serial.println(command);
}
}
}
╘══════════════════════════════════
48 - 시리얼 통신

3.2 시리얼 통신으로 초음파 센서값 받기


초음파 센서 모듈은 초음파를 발생시켜서 물체에 반사되어 돌아오는 시간을 측정해서 물체까
리의 거리를 구할 수 있는 장치이다. 다음 그림은 parallax.com 사의 초음파 센서 모듈이다.

전원핀,
GND핀,
출력핀, 세 개로 동작한다.
2cm 부터 3m 범위를 측정할 수 있다.

[그림 3.2.1] parallax.com 사의 초음파센서모듈

이 모듈의 사용법은 굉장히 간단하다. 일단 5V, GND 핀을 연결한 후 신호핀은 아두이노의 디지


털 핀에 연결한다. 그리고 디지털핀으로 시작 신호(펄스)를 보낸 후 같은 핀에서 되돌아오는 펄
스의 폭(시간)을 잰다.

- 시작 펄스는 5micro-sec 동안 유지해야 한다.


- 응답 펄스는 초음파가 발생한 후 되돌아온 시
간 동안 HIGH를 유지한다.

[그림 3.2.2] 초음파 모듈의 동작 순서

이 응답 펄스의 폭은 장애물과의 거리에 비례하며 초음파가 발생된 후 되돌아올 때 까지의 시간


동안 HIGH로 유지된다. 오직 하나의 디지털 핀만을 사용하므로 시작 신호를 보낸 후 바로 핀의
모드를 입력으로 전환해야 한다. 시작 펄스는 5micro-sec 동안 유지해야 한다. 각 단계는 다음
과 같다.

❶ 핀을 출력으로 설정한다.
❷ LOW신호를 2micros 동안 유지한다.
❸ HIGH 신호를 5micros동안 유지한다.
❹ LOW로 신호를 내보냄과 동시에 핀을 입력으로 설정한다.
❺ pulseIn() 함수를 이용하여 펄스가 HIGH인 구간 시간을 micros 단위로 받는다.
시리얼 통신 - 49

초음파 모듈 시간은 초음파를 송신함과 동시에 핀을 HIGH로 올리고 물체에 반사된 초음파를
수신하면 LOW로 내린다. 따라서 핀이 HIGH로 유지되는 경과 시간은 초음파가 나가서 물체에
반사되어 되돌아온 시간인 것이다.

이 시간을 이용한 계산으로 거리 값을 도출할 수 있는데 다음과 같은 식을 이용한다. 소리는


초당 343m를 이동하므로 1cm를 이동하는데 29.155micro-sec 이 걸린다. 따라서 측정된 시간
(마이크로 초 단위)를 58.31 로 나누어야 한다. 2를 곱해주는 것은 측정된 시간이 왕복 시간이기
때문이다.

3.2.1 첫 번째 프로그램

이제 전술한 설명을 바탕으로 장애물까지의 거리를 반환해주는 함수를 작성해 보자. 함수의
이름은 measureDist() 이고 출력은 float형 값이다.

――――――――――――――――――――――――――――――――――――――――――
#define MS_PER_CM 58.31f
#define ULTRA_SONIC_SENSOR 2

void setup() {
Serial.begin(9600);
}

void loop() {
float fDist = measureDist();
if (fDist < 0) {
Serial.println("No obstable or no sensor.");
} else {
Serial.print("Distance: ");
Serial.print(fDist);
Serial.println(" cm");
}
delay(100);
}

float measureDist() {
pinMode(ULTRA_SONIC_SENSOR, OUTPUT); //(1)단계
digitalWrite(ULTRA_SONIC_SENSOR, LOW); //(2)단계
delayMicroseconds(2);
digitalWrite(ULTRA_SONIC_SENSOR, HIGH); //(3)단계
delayMicroseconds(5);
digitalWrite(ULTRA_SONIC_SENSOR, LOW); //(4)단계
pinMode(ULTRA_SONIC_SENSOR, INPUT);

// (5) 단계
unsigned long ulPulseTime = pulseIn(ULTRA_SONIC_SENSOR,
50 - 시리얼 통신

HIGH);
if (ulPulseTime == 0 )
return -1.0f;
else
return ulPulseTime / MS_PER_CM;
}
――――――――――――――――――――――――――――――――――――――――――

위 코드에서 measureDist() 함수를 실행하면 위에서 설명한 각 단계를 거쳐서 거리까지 계산한
float 값을 반환한다. 만약 pulseIn()함수의 반환값이 0이라면 -1.0f 를 반환하여 장애물이 감지
되지 않은 것임을 표시한다.

3.2.2 두 번째 예제

초음파 센서는 자동차의 후방 장애물을 감지하는데 사용된다. 이것과 유사하게 부저를 이


용하여 만약 장애물이 40cm 바깥에 있다면 부저가 울리지 않고, 40cm~20cm 의 범위에 있
다면 0.5초 주기로 삑삑거리고 20cm 이내에 있다면 삐~하고 연속적인 신호음을 내는 프로
그램을 작성해 보자. 이것의 순서를 다시 기술해 보면 다음과 같다.

❶ 장애물이 30cm 밖에 있다면 부저는 울리지 않는다.


❷ 30cm 이내 10cm 바깥에 있다면 0,5초 주기로 삑삑거린다.
❸ 10cm보다 안쪽에 있다면 연속적인 삐~ 신호를 울린다.

이 예제를 작성하는데 전 절에서 만든 함수를 이용하도록 하겠다.

╒══════════════════════════════════
#define ULTRA_SONIC_SENSOR 7
#define BUZ 11
void setup() {
Serial.begin(9600);
pinMode(BUZ, OUTPUT);
}

void loop() {
float fDist = measureDist();
int iTmBuzOn = 0;
int iTmBuzOff = 0;

if (fDist > 30) {


iTmBuzOn = -1;
} else if (fDist > 10) {
iTmBuzOn = 50;
iTmBuzOff = 450;
} else {
iTmBuzOn = 0;
시리얼 통신 - 51

if (iTmBuzOn == 0) {
digitalWrite(BUZ, HIGH);
} else if (iTmBuzOn > 0) {
digitalWrite(BUZ, HIGH);
delay(iTmBuzOn);
digitalWrite(BUZ, LOW);
delay(iTmBuzOff);
} else {
digitalWrite(BUZ, LOW);
}

Serial.print(fDist);
Serial.println(" cm");
}

// 거리측정 함수 부분 (이전 예제와 같다.)


#define MS_PER_CM 58.31f

float measureDist() {
pinMode(ULTRA_SONIC_SENSOR, OUTPUT); //(1)단계
digitalWrite(ULTRA_SONIC_SENSOR, LOW); //(2)단계
delayMicroseconds(2);
digitalWrite(ULTRA_SONIC_SENSOR, HIGH); //(3)단계
delayMicroseconds(5);
digitalWrite(ULTRA_SONIC_SENSOR, LOW); //(4)단계
pinMode(ULTRA_SONIC_SENSOR, INPUT);

// (5) 단계
unsigned long ulPulseTime = pulseIn(ULTRA_SONIC_SENSOR,
HIGH);
if (ulPulseTime == 0 )
return -1.0f;
else
return ulPulseTime / MS_PER_CM;
}
╘══════════════════════════════════
프로그램을 살펴보면 부저를 울리는데 iTmBuzOn 과 iTmBuzOff 라는 변수를 이용하였
다는 것을 알 수 있다. iTmBuzOn 변수가 –1 이라면 부저를 울리지 않고 0이라면 연속적
으로 울리게끔 작성되어 있다. 만약 0도 아니고 –1도 아니라면 저장된 시간 만큼 부저를
울리고 나머지 시간동안 끄도록 되어 있다.
Chapter

04
인터럽트
( Interrupt )
54 - 인터럽트

4.1 인터럽트(interrupt)의 개념

보통 프로세서가 사용되는 디지털 시스템(아두이노 등)에서는 만약 계통에 특정한 이벤트


가 발생했을 경우에는 수행 중인 작업을 중단하고 발생한 이벤트를 처리한 다음, 다시 원래
수행하던 작업으로 되돌아가 계속 처리하게 된다. 이러한 ‘즉시 처리해야 하는 특수한 이벤
트의 발생’을 인터럽트라고 한다. 인터럽트가 발생하면 아두이노는 그 이벤트를 처리할 작
업들이 수록된 함수를 호출하는데 이 함수를 인터럽트 서비스 루틴 (interrupt service
routine, 이하 ISR)이라고 한다.

∙ 인터럽트 : 즉시 처리해야 하는 특수한 이벤트의 발생


∙ ISR : 인터럽트 서비스 루틴, 인터럽트가 발생했을 때 호출되는 함수

이 인터럽트는 마이크로프로세서 시스템에서 굉장히 중요한 비중을 차지한다. 비유를 들어


서 설명하자면 어떤 전화기가 있는데 전화가 걸려왔는데도 벨소리도 안 나고 진동도 안 된
다고 가정해보자. 이 전화기로 꼭 받아야만 하는 중요한 전화가 올 예정이라면 그 사람은
다른 일은 못 하고 몇 초에 한 번씩 전화가 왔나 안 왔나 들어서 확인해 봐야 할 것이다.
하지만 전화가 걸려왔을 때 벨소리가 나거나 진동하는 전화라면 편하게 다른 일을 하고 있
다가 전화가 오면 벨소리를 듣고(인터럽트 요구) 전화를 받아 용무를 처리한 후 하던 일을
계속할 것이다.

터치 버튼을 가지고 부연 설명을 하면 ‘터치되었다’라는 특수한 이벤트를 감지하려면 기


본적으로 아두이노가 loop()함수 내에서 항상 이 버튼의 상태를 검사해야 한다. 하지만 이것
을 인터럽트로 처리하면 소프트웨어적으로 이 버튼의 상태를 항상 검사할 필요가 없이 다른
일을 할 수가 있는 것이다. 다른 작업을 하고 있다가 ‘버튼이 눌려지면’ 그 즉시 (하드웨어적
으로) 인터럽트를 발생하여 하던 일을 멈추고 이를 처리할 ISR을 호출한 뒤 ISR이 끝나면
하던 일을 다시 하면 되는 것이다. 이와 같이 (외장 혹은 내장) 주변기기와 아두이노가 통신
을 수행하는데 있어서 아두이노가 주변기기의 상태를 loop()함수에서 상시 검사하지 않고
인터럽트로 처리하면 전체적인 시스템의 운용 효율이 매우 높아지게 된다.

아두이노 우노의 경우 인터럽트를 처리할 수 있는 핀은 2번과 3번 핀인데 각각 INT0,


INT1이라고 부른다. (인터럽트 0번, 인터럽트 1번 이다.)

INT0 (인터럽트 0번) INT1 (인터럽트 1번)


핀 번호 2번 핀 3번 핀
인터럽트 - 55

좀 더 구체적으로 설명하면 인터럽트라는 것은 이 핀들의 특정한 신호 상태를 의미한다.


예를 들면 신호가 0에서 1로 바뀌는 순간 (또는 그 반대의 경우도) 에 하던 일을 멈추고 연
결된 특정한 함수(ISR)를 수행할 수 있다. 신호가 0에서 1로 변하는 순간을 ‘rising edge’ 라
고 하고 1에서 0으로 변하는 순간을 ‘falling edge’라고 한다.

ISR을 등록하기 위한 아두이노 함수로 attachInterrup()라는 함수가 있다.

- attachInterrupt( pin, ISR, mode) 함수


- 인터럽트핀에 처리 함수를 붙이는 일을 수행한다.
- pin 값은 우노의 경우 0(또는 INT0 ), 1 (또는 INT1) 둘 중 하나이다.
(2번 핀의 경우 0을, 3번 핀의 경우 1을 입력해야 한다는 것에 주의해야
한다. 혼동을 피하기 위해서 INT0, INT1 상수를 이용할 것을 권한다.)
- ISR은 인터럽트가 걸렸을 때 호출할 함수의 이름이다.
- mode 는 RISING / FALLING / CHANGE / LOW 중 하나이다. (아
래 표 참조)

RISING FALLING CHANGE LOW

[그림 4.1.1] 아두이노에서 사용되는 인터럽트의 종류

ISR은 입력 인수를 받을 수 없고 출력도 void형이어야 한다. 이 attatchInterrupt() 함수


는 통상 setup() 함수 안에서 사용되어 초기에 인터럽트를 설정한다.

4.2 터치센서를 이용한 인터럽트 실험


이번 절에서는 터치센서를 이용하여 인터럽트 실험을 진행해보도록 하겠다. 터치센서는 2.4절
에서 자세히 설명한 바 있으니 참조하기 바란다. 이전 예제와는 다르게 이제는 터치센서를 한
번 터치하면 LED가 켜지고 다시 터치하면 LED가 꺼지도록 동작하는 프로그램을 작성해보도록
하겠다. 여러가지 방법이 있겠지만 인터럽트(interrupt)를 사용하는 것이 정석이다.
56 - 인터럽트

4.2.1 첫 번째 실험

다음은 우노의 11번 핀에 부저가 달렸고 3번 핀에 터치센서가 달렸다고 가정하고 작성한


예제이다.
╒══════════════════════════════════
#define TS 3
#define BUZ 11
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(BUZ, OUTPUT);
pinMode(TS, INPUT);
attachInterrupt(INT1, toggleLed, RISING);
}
void loop() {
digitalWrite(BUZ, HIGH);
delay(50);
digitalWrite(BUZ, LOW);
delay(450);
}
void toggleLed()
{
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN) );
}
╘══════════════════════════════════
setup()함수에서 attachInterrupt(INT1, toggleLed, RISING) 를 수행하여 INT1 핀(3번 핀)
에 toggleLed()함수를 붙였다. 이 ISR 함수는 rising edge 에서 호출된다.
그리고 loop() 함수에서는 부저를 0.5초마다 한 번씩 울리는 일 외에는 하지 않는다. 터치
센서의 값을 읽는 코드는 어디에도 없다는 것에 주목해야 한다. 터치 센서의 출력값이 0에
서 1로 변하는 순간 함수 toggleLed() 가 자동으로 호출된다.

4.3 택스위치와 디바운싱

이전의 터치센서를 이용한 실험처럼 이번에는 인터럽트를 이용하여서 스위치를 누를 때마다


LED가 토글(toggle)되는 프로그램을 작성하도록 하겠다. 즉, 스위치 신호의 폴링 에지 (falling
edge)에서 LED의 출력 신호가 반전되도록 하는 프로그램을 작성하도록 하겠다.

터치센서와 다르게 기계적인 접점을 갖는 스위치는 점점이 붙거나 떨어지는 순간에 바운싱
(bouncing) 현상이 있다. 이것은 접점이 붙거나 떨어지는 그 짧은 순간에 접점이 고속으로 여러
번 on/off 되는 현상을 말하며 기계적인 스위치라면 반드시 발생하는 현상이다. 이것은 짧은 순
간(약 100ms 이내임)에 여러 번 접점이 붙었다가 떨어지는 것을 반복하므로 의도하지 않게 인
인터럽트 - 57

터럽트가 여러 번 발생할 수 있으므로 이러한 바운싱 현상을 제거해야 올바르게 동작하는데 이


것을 디바운싱(debouncing)이라고 한다.
다음 그림에서 하단의 그림이 바운싱을 도시한 것이고 이것을 제거한 깨끗한 신호가 상단에
도시되어 있다.

<- 디바운스된 신호

<- 기계적 접점의 바운싱 현상

[그림 4.3.1] (a) 디바운싱 (b) 바운싱 현상

디바운싱하는데 여러 가지 방법들이 알려져 있는데 크게 두 가지로 분류할 수 있으며 하드웨


어적인 방법과 소프트웨어적인 것이다. 하드웨어적인 방법은 RC 저역 필터를 슈미트트리거나
디지털 인버터에 직결한 후 이 신호를 포트 핀에 연결하는 방법인데 아래의 회로도가 대표적인
것이다. RC 필터와 슈미트트리거가 직결되어 있다. 이 회로도를 사용하면 아두이노의 내부 풀
업 저항은 사용하지 못한다.

[그림 4.3.2] 대표적인 디바운싱 회로도

하지만 아두이노와 같은 프로세서가 사용된다면 부가적인 디바운싱 회로 없이 소프트웨어적으


로 이것을 구현하여 불필요한 인터럽트를 억제할 수 있다. 여기에서는 그 방법에 대해서 설명하
도록 하겠다. 기본적인 아이디어는 ISR 함수 안에서 바운싱이 없어질 때 까지 기다렸다가 스위
치 신호를 다시 읽어서 정상 호출된 것인가 아닌가를 판별하는 것이다.
58 - 인터럽트

4.3.1 falling edge 의 경우 디바운싱

만약 <그림 2>와 같이 회로를 꾸미고 폴링에지에 인터럽트가 걸리도록 설정되었다고 가


정하자.

attachInterrupt(INT0, toggleLed, FALLING);

ISR 함수 처음에 바운싱이 안정될 때 까지 일정시간(80ms 정도가 적당하다.) 지연시킨 후


다음과 같이 스위치 신호를 읽은 다음 그것으로 올바른 인터럽트 호출인가를 판단한다.

void isr() {
_delay_ms(80);
if (digitalRead(SW)==HIGH) return;
<실제 인터럽트 처리 루틴>
,,,
}

이 코드를 보면 80ms 지연시킨 이후에 스위치 값을 읽어 HIGH라면 잘못 호출된 것으로 판


단하고 아무 일도 않고 리턴한다. 즉, 이 경우는 rising edge의 바운싱 구간에서 호출된 것
으로 판단하는 것이다. 반대로 80ms 지연시킨 이후에 스위치 값이 LOW라면 falling edge
에서 정상 호출된 것으로 간주하고 이후에 있는 인터럽트 처리 루틴을 실행시키는 것이다.
이 방법은 손으로 버튼을 조작하는 경우 정밀한 제어가 필요치 않으므로 필자의 경험상
너무 빠르게 버튼을 동작시키지 않는 한 대부분 정상 작동한다.

이 설명에서 처음 소개하는 함수가 있는데 바로 _delay_ms() 함수이다. 이름을 보면 지연


시키는 함수라는 것을 알 수 있을 텐데 왜 아두이노의 표준 함수인 delay() 함수 대신 이
함수를 사용했는지 의문이 들 것이다. ISR함수 안에서 아두이노의 시간관련 함수인 다음 함
수들은 정상 동작하지 않는다.(주의!)

∙ delay()
∙ delayMicrosecond()
∙ millis()
∙ micros()

이 함수들은 내부적으로 타이머 인터럽트를 사용하므로 ISR안에서는 작동할 수 없다. 이유


는 ISR이 호출되는 순간 인터럽트가 자동으로 막히고 ISR의 끝나야 인터럽트가 다시 가능
하게 되기 때문이다. (AVR에 대한 지식이 있다면 이해가 쉽게 가겠지만 일단은 위 함수들
은 ISR 안에서 정상 동작하지 않는다고만 알아도 된다.) 이것은 필자가 구글링을 해 보고
인터럽트 - 59

직접 실험으로 확인한 사실이다.

반면에 _delay_ms()함수는 단순히 소트프웨어적으로 지연을 시켜주는 함수라서 ISR안에


서도 정상 작동한다. 단, 이 함수를 사용하기 위해서는 다음과 같이 헤더 파일을 인클루드
시켜야 한다. (avr-gcc에서 사용되는 유틸리티 라이브러리이다.)

#include<util/delay.h>

전체 프로그램은 다음과 같다.

#include<util/delay.h>
#define SW 2
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(SW, INPUT_PULLUP);
attachInterrupt(INT0, toggleLed, FALLING);
}
void loop() {
}
void toggleLed() {
_delay_ms(80);
if (digitalRead(SW)==HIGH) return;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

스위치의 디바운싱은 고전적이만 해결하기 꽤 까다로운 문제이다. 하지만 여기서 소개한


방법을 사용하면 대부분의 경우 잘 동작한다.

4.3.2 rising edge 의 경우 디바운싱

만약 <그림 2>와 같이 회로를 꾸미고 라이징 에지에 인터럽트가 걸리도록 설정되었다


고 하자.

attachInterrupt(INT0, toggleLed, RISING);

앞의 경우와 반대로 ISR의 함수 처음에 일정시간 지연시킨 후 스위치 신호가 LOW일 때 그


냥 리턴시키는데 그 이유는 falling edge에서 발생한 것이 때문이다.

void isr() {
_delay_ms(80);
if (digitalRead(SW)==LOW) return;

<실제 인터럽트 처리 루틴>
}
60 - 인터럽트

4.3.3 changing edge 의 경우 디바운싱

만약 <그림 2>와 같이 회로를 꾸미고 체인징 에지, 즉 falling과 rising edge 모두에서
인터럽트가 걸리도록 설정되었다고 하자.

attachInterrupt(INT0, toggleLed, CHANGE);

이 경우는 하나의 에지에서 두 개 이상의 인터럽트가 걸리는 것을 막기 위해서 단순히 지


연시키는 것으로 충분하다.

void isr() {
_delay_ms(80);
<실제 인터럽트 처리 루틴>
….
}

4.3.4 low 인터럽트 의 경우 디바운싱

만약 <그림 2>와 같이 회로를 꾸미고 low 신호에서 인터럽트가 걸리도록 설정되었다


고 하자. 이 인터럽트는 핀 신호가 low 이면 몇 번이고 반복해서 인터럽트가 걸리도록 하려
는 의도로 설정되는 것이다.

attachInterrupt(INT0, toggleLed, LOW);

따라서 이 경우는 별도의 디바운싱이 필요치 않다.


Chapter

05
아날로그핀
62 - 아날로그핀

5.1 아날로그 투 디지털 변환기

온도, 광량, 습도 등등과 같은 자연계의 물리량을 전기적인 신호로 바꿔주는 소자를 센서


(sensor)라고 한다. 보통 센서는 물리량에 비례하여 저항값이 변한다거나 혹은 물리량에 비례하
는 전위값을 갖는다거나 하는데 이러한 전압값을 이진값으로 변환하는 장치를 ADC(analog to
digital converter)라고 한다. 아두이노 우노에 채용된 ATmega328은 내부에 ADC가 하나 내장
되어 있으며 채널은 6개가 있다. 이것들이 A0부터 A5핀까지 6개의 핀에 연결 되어있어서 이 핀
들의 전위값을 이진값으로 변환할 수 있다.

5.1.1 아두이노의 아날로그 핀

전술한 바와 같이 아두이노 우노의 A0부터 A5 까지 6개의 핀을 이용하여 아날로그 입력


을 받을 수 있는데 디지털 핀이 0과 1 두 값만을 입출력으로 가지는 것과는 달리 아날로그
핀은 그 전압을 0~1023 의 정수값으로 변환하여 읽는다. 다음 그림 5.1.1에 보면 아날로그
핀과 AREF핀이 표시가 되어있는데 AREF핀은 ADC의 기준 전압을 설정한다. 기준 전압에
대해서는 이후에 자세히 설명할 것이다.

[그림 5.1.1] A0~A5 핀과 AREF핀

아날로그 핀은 0~5V 사이의 전압 값을 0~1023 사이의 정수값으로 변환시킨다. 이 과정


을 A/D변환(analog-to-digital converting)이라고 한다. 따라서 분해능은 0.0049V (4.9mV
= 5V/1024)이며 이 때 사용되는 함수가 analogRead()함수이다.

ananlogRead(pin)
아날로그핀 - 63

입력 인수로 0(또는 A0), 1(또는 A1), … 5(또는 A5)를 주고 리턴값은 int형으로서 앞에서
언급한 대로 0~1023 값이다. 변환 시간은 100micro-sec 으로서 이론상으로는 1초에 10000
번 정도 변환이 가능하다. 아날로그 핀은 디지털 핀과 달리 기본적으로 입력으로 설정되어
있으므로 별도로 입력을 설정하는 과정 없이 바로 위의 함수를 이용할 수 있다.

한 가지 알아두어야 할 점은 아두이노 우노의 경우 다음 세 가지 방법은 같은 동작을 수


행한다.

방법1 방법2 방법3 비고


analogRead(0) analogRead(A0) analogRead(14) A0==14
analogRead(1) analogRead(A1) analogRead(15) A1==15
analogRead(2) analogRead(A2) analogRead(16) A2==16
analogRead(3) analogRead(A3) analogRead(17) A3==17
analogRead(4) analogRead(A4) analogRead(18) A4==18
analogRead(5) analogRead(A5) analogRead(19) A5==19

즉, analogRead(0)과 analogRead(A0) 그리고 analogRead(14) 는 내부적으로 같은 동작을


수행한다. 왜냐면 상수 A0, A1, …, A5 는 내부적으로 다음과 같이 14,15,...,19로 정의되어
있기 때문이다.

#define A0 14
#define A1 15

#define A5 19

필자는 처음에 상수 A0 는 내부적으로 0으로 정의되었을 것으로 짐작을 했었는데 그게 아


니었다. 독자들도 혼동하기 쉬우리라 생각된다. 보통은 <방법1> 혹은 <방법2> 를 사용하고
<방법3>은 사용하지 않지만 내부적으로 A0상수가 14값을 갖는 다는 것은 알아두는 것이 좋
은데 그 이유는 아날로그 핀을 디지털 핀으로 사용할 때 이 상수가 사용되기 때문이다. 디
지털 핀이 13번 까지 있으므로 그 다음 숫자부터 아날로그 핀에 할당되었다는 것을 알 수
있다.

5.1.2 아날로그 핀을 디지털 핀으로 사용하기

아날로그핀도 디지털핀과 똑같이 사용할 수 있다. 만약 A0핀을 디지털 출력핀으로 사용


하고 싶다면 다음과 같이 A0 상수를 이용한다.

pinMode(A0, OUTPUT);
digitalWrite(A0, HIGH); 혹은 digitalWrite(A0, LOW);
64 - 아날로그핀

다른 예로 A1핀을 디지털 입력핀으로 사용하고 싶다면 A1상수를 이용하면 된다. 입력으로


설정할 때 내부 풀업 저항도 연결할 수 있다.

pinMode(A1, INPUT); 혹은 pinMode(A1, INPUT_PULLUP);


int iA = digitalRead(A1);

만약 디지털 핀이 부족하다면 이와 같은 방법으로 아날로그 핀도 디지털 핀과 똑같이 사용


할 수 있다.

5.1.3 ADC 기준 전압 바꾸기

아날로그 입력에 관련된 함수가 하나 더 있는데 바로 analogReference()함수이다.

ananlogReference(type)

이것은 아날로그 입력값이 1023로 읽히는 실제 최대 전압 값을 설정해 주는 함수로서 우노


의 경우 따로 설정해 주지 않으면 5V이지만 이 함수를 이용하여 다른 값을 설정해 줄 수
있다. type의 종류는 다음과 같다.

∙ DEFAULT : 아두이노의 동작 전압(우노는 5V 이고 보드에 따라서 3.3V일 수 있다.)


∙ INTERNAL : 내장 전압 (우노는 1.1V)
∙ EXTERNAL : AREF핀에 인가된 전압 (0~ 5V 사이어야 됨)
∙ INTERNAL1V1 : 내장된 1.1V (Arduino Mega에서만 사용된다.)
∙ INTERNAL2V56 : 내장 2.56V (Arduino Mega 에서만 사용됨)

아두이노 우노와 관련된 옵션은 DAFAULT, INTERNAL, EXTERNAL 세 개 이며 별도로 설


정하지 않는다면 기본적으로 DEFAULT가 사용된다.
만약 INTERNAL로 설정하면 기준 전압이 1.1V이므로 기본 모드보다 더 높은 분해능
(0.0011V)을 얻을 수 있으며 약 4.5배(=5/1.1)의 증폭효과가 있다.
만약 3.3V를 기준 전압으로 사용하고 싶다면 우노 보드상의 3.3V핀과 AREF 핀을 결선한
후 EXTERNAL 옵션을 설정하면 된다. (다음 그림 참조) 이 경우 분해능은 0.0032V 이고
5V를 기준 전압으로 사용하는 경우 대비 약 1.5배의 증폭 효과가 있다. 아두이노 우노의
3.3V 핀은 외부 전원(7V~12V)을 연결한 경우뿐만 아니라 USB만 연결한 경우에도 정확히
3.3V 전압을 출력하므로 편리하게 사용할 수 있다.
아날로그핀 - 65

∙ 만약 아날로그 핀의 기준 전압을 3.3V


를 사용하고 싶다면 그림과 같이 연결
한 후 setup()함수 내에서

analogReference(EXTERNAL
);

로 설정한다. 5V 기준 전압에 비해서


약 1.5배의 증폭 효과가 있다.
∙ 아두이노 우노의 3.3V 핀은 외부 전원
(7V~12V)을 연결한 경우뿐만 아니라 USB
만 연결한 경우에도 정확히 3.3V 전압을 갖
는다.

[그림 5.1.2] 아두이노의 3.3V 핀과 AREF 핀을 연결한 회로도

5.2 온도센서를 이용한 아날로그 입력 실험

이번 절에서는 온도 센서 LM35DZ를 이용하여 아날로그 입력 실험을 수행하도록 하겠다. 이


소자의 외형과 핀의 기능은 다음 그림과 같다. 세 개의 다리가 있는데 각각 5V, 출력, GND 이고
가운데 출력핀을 아두이노의 아날로그 핀에 연결해야 한다.

[그림 5.2.1] 온도센서 LM35-DZ

데이터시트에 따르면 이 소자는 섭씨 +2도~+150도 의 범위를 측정할 수 있으며 섭씨 1도는


약 10mV 의 전위차를 갖는다. 예를 들어서 센서핀의 전압이 0.21V라면 210mV 이므로 온도는
21도씨라는 것이다. 따라서 아날로그 값을 읽어들여서 약간의 계산(전압값에 100을 곱한다)을
거쳐서 섭씨 온도가 얻어진다.
66 - 아날로그핀

만약 A0 핀에 센서가 연결되어 있다고 하면 이를 수행하는 코드는 다음과 같다.

short sVal = analogRead(A0); // 아날로그값: 0~1023의 값을 갖는다.


float fVoltage = sVal*5.0/1024; // 실제 전압값을 변환 (5.0을 곱함에 유
의한다.)
float fTemp = fVoltage * 100; // 전압값을 섭씨 온도로 변환한다.

이제 이것을 함수로 구현하여 1초마다 한 번씩 온도를 감지하여 PC로 시리얼 통신을 통해서 표
시해주는 예제는 다음과 같다.

void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println(getTemp());
delay(1000);
}
float getTemp()
{
short sVal = analogRead(A0);
float voltage = sVal*5.0/1024; // 실제 전압값을 구한다.
return voltage*100;
}

[그림 5.2.2] 1초마다 온도를 감지하여 표시하는 결과 화면


아날로그핀 - 67

만약 기준 전압으로 내부 전압인 1.1V를 사용하면 분해능이 약 5배 정도 높아지므로


OP-AMP와 같은 외부 회로 없이 간접적으로 5배 증폭의 효과를 볼 수 있다. 1.1V라도 섭씨
110도에 해당되므로 실온을 측정하는 경우라면 크게 문제가 없을 것이다. 다음과 같이 앞의 예
제를 조금만 수정하면 된다.

#define SUPPLY_VOLTAGE 1.1


void setup() {
Serial.begin(9600);
analogReference(INTERNAL);
}
void loop() {
Serial.println(getTemp());
delay(1000);
}
float getTemp()
{
short sVal = analogRead(A0);
float voltage = sVal*SUPPLY_VOLTAGE/1024; // 1.1을 곱한 뒤
1024로 나눈다.
return voltage*100;
}

한 가지 더 부연할 것은 실제 온도와 센싱된 온도와는 아무래도 차이가 나게 마련이다. 따라


서 정밀하게 온도를 잴 수 있는 온도계 값과 센싱된 값을 비교하여서 그 차이만큼 보정을 해주
면 좀 더 정확하게 온도를 잴 수 있을 것이다.

5.3 광량센서를 이용한 아날로그 입력 실험

CdS 광전도 셀(CdS photoconductive cell) 또는 CdS셀은 황화카드뮴을 주성분으로 하는 광전


도 소자로, 빛의 양에 따라 저항값이 변하는 일종의 가변 저항으로 생각할 수 있다. 빛의 양에
따라 내부 저항값이 변하는 특성이 있으므로 광 가변 저항기라고도 불리는데, 조사되는 빛의 양
이 클수록 저항값이 낮아지지만 입사광이 거의 없으면 거의 절연체에 가까워질 정도로 저항값이
커지게 된다. 따라서 광량에 의해서 개폐되는 전기적인 스위치로도 생각할 수 있으며 회로도 스
위치의 그것과 동일하게 제작되었다.
68 - 아날로그핀

[그림 5.3.1] Cds 관전도 셀의 외형

회로 구성은 택스위치와 동일하게 CdS셀의 한 핀에 아날로그 핀을 연결하고 다른 핀은 GND와


직결하면 된다. 단, 이 경우 아날로그 핀의 내부 풀업저항을 이용해야 하므로 다음과 같이 설정
해야 한다. (만약 A0에 센서가 연결되어 있다면)

pinMode(A0, INPUT_PULLUP);

센서 주변에 조사광이 많다면 (주위가 밝다면) CdS셀의 저항이 0에 가까워지므로 아날로그 핀


은 GND에 연결이 되므로 0에 가까운 값이 읽혀질 것이다. 반대로 센서 주위가 어둡다면 저항이
커지므로 내부 풀업 저항에 의해서 1023에 가까운 값이 읽혀질 것이다. 즉, 밝을수록 더 작은 값
이 읽혀지게 된다.

5.3.1 첫 번째 예제

첫 번째 예제로 Cds센서의 한 쪽 다리를 A0에 연결하고 다른 핀은 GND에 연결한 후 내


부 풀업 저항을 연결한다. 그런 후 아날로그 값을 읽어서 시리얼 터미널에 그 값을 출력하
는 프로그램을 작성해보도록 하겠다.

void setup() {
pinMode(A0, INPUT_PULLUP);
Serial.begin(115200);
}

void loop() {
Serial.println(analogRead(A0));
delay(100);//1초에 약 10번 정도 출력한다.
}
아날로그핀 - 69

5.3.2 두 번째 예제

이번에는 주변이 밝으면 내장 LED가 꺼지고 어느 정도 어두워지면 LED가 켜지는 프로그


램을 작성해 보자.

void setup() {
pinMode(A0, INPUT_PULLUP);
pinMode(13, OUTPUT);
}

void loop() {
if ( analogRead(A0) > 100 ) {
digitalWrite(13, HIGH);
} else {
digitalWrite(13, LOW);
}
}
Chapter

06
아날로그 출력
(PWM)
72 - 아날로그 출력

6.1 아날로그 출력 개요

아두이노의 디지털핀은 오직 HIGH(5V) 아니면 LOW(0V) 두 가지 신호 외에는 출력할 수 없


으며 전압의 관점에서 보면 5V와 0V만 가질 수 있다. 하지만 PWM (pulse width modulation,
펄스 폭 변조) 기능을 이용하면 마치 아날로그 전압처럼 0V와 5V 사이의 전압으로 (예를 들면
2V, 3.5V 등) 출력을 낼 수 있다. 따라서 LED의 밝기를 제어한다든가 모터의 회전 속도를 제어
하는데 사용할 수 있다.

PWM은 진정한 의미의 아날로그 출력은 아니고 흉내를 내는 것인데 그 원리는 다음 그림과 같
다. (출처 : arduino.cc)

(a) 0V

(b) 1.25V

(c) 2.5V

(d) 3.75V

(e) 5V

[그림 6.1.1] PWM의 원리

이 그림에서 보면 주기적인 구형파를 발생하고 이 구형파의 폭을 조절하여 그 듀티비 (HIGH 구


간 대비 LOW 구간의 비율)로 아날로그 전압값을 가지도록 한다. 이 구형파의 주기를 매우 빠르
게 (아두이노 우노의 경우 490Hz 혹은 980Hz이다.) 하면 상대적으로 반응 속도가 느린 기계 장
치(모터 등)는 이것을 아날로그 전압으로 인식하게 된다. 예를 들어서 그림 2(b)의 경우 주기의
1/4동안만 on 이 되므로 평균 출력 전압도 5V * ¼ 인 1.25V 정도가 된다. 만약 LED가 연결되
었다면 LED는 정확하게 on구간에서만 켜지고 off구간에서는 꺼진다. 하지만 눈으로 보기에는
이것이 인식하지 못 할만큼 고속으로 동작하므로 밝기가 다르게 보이는 것이다.
아날로그 출력 - 73

6.2 아두이노의 아날로그 핀

아두이노 우노의 모든 핀이 PWM 출력을 낼 수 있는 것은 아니고 아두이노 우노의 경우 3, 5,


6, 9, 10, 11번 핀이 PWM출력을 낼 수 있으며 이것은 정품 보드의 경우 다음 그림과 같이 ~로
표시되어 있다.

[그림 6.2.1] 아두이노 우노의 PWM 핀

PWM의 동작 주파수는 다음과 같다.

∙ 3, 9, 10, 11번 핀 - 490Hz


∙ 5, 6번 핀 – 980Hz (5,6번 핀이 좀 더 고속으로 동작)

PWM기능을 사용하기 위해서는 다음과 같은 analogWrite()함수를 이용한다.

∙ analogWrite(pin, value)
pin : 3, 5, 6, 9, 10, 11 중 하나 (아두이노 우노의 경우)
value : 0에서 255 사이의 정수.

그리고 PWM 기능을 사용하기 위해서는 해당 핀을 출력으로 설정하여야 한다.


74 - 아날로그 출력

6.3 고휘도 LED

고휘도 LED는 이전 실험에서 사용된 일반 LED보다 밝기가 대폭 개선된 LED이다.

[그림 6.3.1] 고휘도 LED

일반적인 LED와 외형은 큰 차이가 없지만 밝기가 획기적으로 개선된 것이고 전력 효율이 높아
서 가정용 조명이나 신호등, 차량의 전조등 등으로 폭 넓게 사용된다.
구동 회로는 일반 LED와 다르지 않게 저항을 직결하여 전원이 연결하면 되는데 문제는 저항값
으로 어떤 값을 사용하는가이다. 데이터쉬트를 살펴보면 최대 허용 전류는 20mA 이고 (전류가
클수록 더 밝다.) 이 전류가 흐를 때 다이오드 양단의 전압 강하는 3.0~3.6V 이다. 따라서 만약
5V 전원을 사용하고 최대 전류를 흘릴 때 전압강하가 3.0V라고 가정하면 저항값은 오옴의 법칙
에 의해서 다음과 같이 간단히 계산할 수 있다.

  
    


이 저항값이 허용되는 가장 최소 저항이므로 이것보다 큰 용량의 저항을 선택하면 구동하는데


무리가 없을 것이다. 실습에는 100 Ω의 저항을 선택했다.

6.3.1 첫 번째 실험

첫 번째 예제로 서서히 밝아졌다가 다시 서서히 어두워지는 동작을 하는 프로그램을 작성


해 보자.
╒══════════════════════════════════
#define HLED 5
void setup() {
pinMode(HLED,OUTPUT);
}
아날로그 출력 - 75

void loop() {
for (int k=0; k<256; k++) {
analogWrite(HLED,k);
delay(15);
}
for (int k=255; k>=0; k--) {
analogWrite(HLED, k);
delay(15);
}
}
╘══════════════════════════════════
위 프로그램에서 delay(15) 함수를 이용하여 서서히 밝아지거나 서서히 어두워지는 효과를
내었다.

6.3.2 두 번째 실험

두 번째로 스위치를 누르면 고휘도 LED가 서서히 켜지는 프로그램을 작성해 보자. 완전
히 꺼진 상태에서 최고 밝기로 켜지는 시간은 4초로 설정한다. 버튼을 떼면 그 즉시로 고휘
도 LED가 꺼져야 한다.

╒══════════════════════════════════
#define HLED 6
#define SW 3
void setup() {
pinMode(HLED,OUTPUT);
pinMode(SW,INPUT_PULLUP);
}

int iL=0;
void loop() {
if (digitalRead(SW)==LOW) {
analogWrite(HLED, iL++);
if (iL>255) iL = 255;//❶
delay(15);
} else {
digitalWrite(HLED,LOW);
iL = 0;
}
}
╘══════════════════════════════════
전술한 바와 같이 analogWrite()함수의 두 번째 인수의 범위는 0~255이다. 따라서 ❶에서
이 범위를 넘으면 (즉 256이 되면) 255값을 계속 가지도록 if 문으로 처리했음을 유의해서
보자.

6.3.3 세 번째 실험

이전 실험에서는 스위치를 떼면 그 즉시 꺼졌지만 이번 예제에서는 서서히 꺼지는 부분을


76 - 아날로그 출력

추가해 보자. 꺼지는 속도는 완전히 켜졌을 때에서 완전히 꺼질 때까지의 시간이 4초 정도
되게 설정한다.
╒══════════════════════════════════
#define HLED 6
#define SW 3
void setup() {
pinMode(HLED,OUTPUT);
pinMode(SW,INPUT_PULLUP);
}
int iL=0;
void loop() {
if (digitalRead(SW)==LOW) {
analogWrite(HLED, iL++);
if (iL>255) iL = 255; //❶
delay(15);
} else {
analogWrite(HLED, iL--); //❷
if (iL<0) iL = 0;
delay(15);
}
}
╘══════════════════════════════════
이 프로그램의 동작은 스위치를 누르고 있으면 서서히 켜지고 떼면 다시 서서히 꺼지게 된
다. analogWrite()함수의 두 번째 인수의 범위는 0~255이다. 따라서 ❶과 ❷에서 이 범위를
넘으면 그 한계값을 계속 가지도록 if 문으로 처리했음을 유의해서 보자.

6.3.4 네 번째 실험

이번에는 광량 센서와 조합하여 주변이 어두워지면 LED를 켜고 밝아지면 끄는 동작을 하


는 프로그램을 작성해 보겠다.

6.4 (초)소형 DC모터 실험

DC모터는 영구자석의 자기장과 그 자기장 속에 놓여 있는 도체에 흐르는 전류에 의해 발생한


전자력 간의 반발력으로 회전하는 장치이며 DC전원을 사용한다. 기동 토크가 크며 인가되는 전
압에 대해 회전 특성이 선형적으로 비례하며 가격이 저렴한 장점이 있는 반면에 구조상 브러시
(brush)와 정류자(commutator)에 의한 기계적 점점이 있다는 단점이 있다. 이 접점으로 인해
회전 시 전기적인 불꽃이나 잡음이 발생하며 이는 uC로 제어할 때는 고려하여 제거하여야 한다.
DC모터는 인가 전압의 극성을 바꾸어 인가하면 방향을 바꾸어 회전한다.
아날로그 출력 - 77

[그림 6.4.1] 다양한 DC 모터들

만약 아두이노의 전원이 PC의 USB에서만 공급된다면 DC모터를 직접 구동하는 것은 무리가


있다. DC모터는 일반적으로 대전류가 필요하기 때문에 최대 공급 전류가 최대 500mA 정도인
(노트북인 경우 공급 전류량은 더 작아진다.) USB로 직접 구동할 수는 없다. 그러나 구동 전류
가 작은 초소형 모터의 경우 USB전원으로도 충분해 제어할 수 있으며 이 경우 높은 회전 속도
나 큰 토크(회전력)을 기대할 수는 없다. 중형 DC모터를 아두이노로 제어하려면 전용 모터 제어
쉴드를 사용하는 것이 유리하다.

6.4.1 소형 DC모터의 제어

실험에서는 다음 <그림 1>과 같은 3V로 구동되는 소형 DC모터를 선택하였으며 드라이버


IC로는 소형 DC모터의 구동에 많이 쓰이는 BA6208을 사용하였다. 소전류로도 구동이 가능
한 초소형 모터이므로 USB전원만으로도 제어하는데 충분하다.

[그림 6.4.2] 실험에 사용된 초소형 DC모터

BA6208은 소형 DC모터 구동 IC이다. SIP패키지의 BA6208 외형은 그림 6.4.3과 같다. 그


림에서 보면 홈이 파인 쪽이 1번 핀이다. 논리부와 출력부로 구성되는데 논리부는 모터의
회전 방향을 제어하며, 출력부는 논리 제어에 따라 100mA까지 출력 전류를 공급할 수 있
다. 공급 전압의 최대 값은 18V이다
78 - 아날로그 출력

[그림 6.4.3] BA6208의 외형과 핀 기능

다음 표는 BA6208의 논리 값에 따른 동작을 정리한 것이다.

[표 6.4.1] BA6208의 입력 신호에 따른 모터 동작 표

아두이노 우노의 5번과 6번 핀을 BA6208로 연결하여 모터를 제어하도록 하겠다. 결선도는


다음 그림과 같다. VCC는 아두이노 우노의 3.3V핀과 연결한다.

[그림 6.4.4] 아두이노와 BA6208 그리고 모터의 결선도


아날로그 출력 - 79

한 가지 유의할 것은 DC모터가 회전하면서 발생하는 전기적인 잡음 때문에 전원이 불안


정해질 가능성이 있으며 이것은 전체 시스템이 불안정하게 만드는 요인이 되기도 한다. 이
를 억제하기 위해서 BA6208단의 VCC와 GND 사이에 커패시터(10uF)을 연결하여 전원을
안정시키는 것이 좋으나 아두이노 우노 보드를 사용하는 경우 전원을 안정화 시키는 회로가
포함되어 있으므로 별도로 커패시터를 달아 줄 필요는 없다.

다음 예는 1초 간격으로 모터의 회전 속도를 바꾸는 예제이다.

#define A_IN 5
#define B_IN 6
void setup() {
pinMode(A_IN, OUTPUT);
pinMode(B_IN, OUTPUT);
}
void loop() {
digitalWrite(A_IN, HIGH); // 정회전
digitalWrite(B_IN, LOW);
delay(1000);
digitalWrite(A_IN, LOW); // 역회전
digitalWrite(B_IN, HIGH);
delay(1000);
}

6.4.2 PWM을 이용한 속도제어

이제 analogWrite() 함수를 이용하여 속도를 제어해보도록 하겠다. 다음 예제는 시리얼


통신으로 PC에서 -255~255 사이의 정수를 입력하면 그 값을 analogWrite()함수로 내보내
어 모터의 속도를 조절하는 예제이다. 언급한 바와 같이 아두이노의 PWM 주파수는 490Hz,
980Hz인데 소형 모터를 제어하기에는 무리가 없다.
80 - 아날로그 출력

#define A_IN 5
#define B_IN 6
void setup() {
Serial.begin(9600);
Serial.setTimeout(100);
}
void loop() {
if (Serial.available()) { // 만약 시리얼 버퍼에 데이터가
있다면
// 정수로 해석하여 읽어들인다.
short motorSpeed = Serial.parseInt();
if (motorSpeed >= 0){
analogWrite(A_IN, motorSpeed);
analogWrite(B_IN, 0);
} else {
analogWrite(A_IN, 0);
analogWrite(B_IN, -motorSpeed);
}
}
}

6.5 중형 DC모터 실험
아두이노 쉴드 중에 DC모터를 제어할 수 있는 것이 있는데 2014년 현재 R3버전이 시판중이
다.

[그림 6.5.1] 아두이노 DC모터 쉴드 R3

일단 기본적인 특성은 다음과 같다.

∙ 동작전압 : 5V to 12V
∙ 모터제어IC: L298P (두 개의 DC모터 혹은 1개의 스테핑모터 제어 가능)
∙ 최대 전류 :채널당 2A 혹은 4A(두 채널을 병렬로 연결시)
∙ 전류 센싱 : 1.65V/A
∙ 강제 정지(brake) 기능
아날로그 출력 - 81

모터의 전원은 반드시 별도로 연결해 주어야 하는데 아두이노의 2.1파이 DC잭에 연결해도 되
고 쉴드의 스크류단자에 전선으로 연결해도 된다. 그런데 사용 설명에는 모터 전압이 9V 이상
이면 아두이노와 쉴드의 전원(Vin)을 분리하는 것이 바람직하며 '쉴드 뒷면의 "Vin Connect" 점
퍼를 절단하면 된다'고 설명되어 있다. 이 경우 쉴드에 직접 인가할 수 있는 전압의 최대값은
18V이다.
사용되는 핀은 다음과 같다.

[표 6.5.1] 모터쉴드 R3의 핀 매핑

기능 채널A 채널B 기능
방향 D12 D13 회전 방향 제어
PWM D3 D11 속도 제어
브레이크 D9 D8 HIGH일 때 강제 정지
전류 감지 A0 A1 전류 감지

모터 회전 시에 PWM을 0 으로 인가하면 전압이 0으로 떨어지고 모터 축은 관성에 따라 천천히


멈추게 되지만 이에 반해서 Brake는 강제 정지를 할 때 사용되는 핀으로 HIGH가 되는 순간 모
터가 곧바로 멈추게 된다.

[그림 6.5.2] 모터쉴드의 핀 기능

이것을 아두이노Due 와 연결해서 동작시킬때 Due의 동작 전압이 3.3V이므로 맞지 않을 것이라


고 짐작할 수 있으나 다음 페이지에 설명된 대로 직접 연결해도 아무런 문제가 없다.
82 - 아날로그 출력

Arduino Due + Motor Shiled : DC motor

단 모터 전원이 9V이상일 때는 Vin은 분리하는 것이 좋을 것 같다.


이 쉴드에 대해서 한 가지 아쉬운 점은 PWM(A) 핀으로 3번 핀을 사용하고 있어서 인터럽트
하나를 쓰지 못하게 한다는 것이다.

6.6 PWM 주파수 변경

6.6.1 아두이노의 PWM 주파수

아두이노 우노의 경우 PWM주파수가 980Hz (5,6번 핀)와 490Hz(그 외의 핀)로 고정되어


있다. 보통의 경우(LED의 밝기를 제어한다든가 소형 모터를 돌릴 때)에는 이 주파수를 사용
하는 것에 별 문제는 없다.

[표 6.6.1] PWM 주파수

핀 번호 PWM 주파수
5, 6 980Hz
3, 9, 10,11 490Hz

하지만 소형 보다는 더 큰 중소형 이상의 DC모터를 구동하는 경우에는 보통 10KHz 이상의


PWM 주파수를 사용하므로 아두이노에서 제공하는 기본 주파수로는 DC모터를 구동하기에
적절하지 않다. 이 주파수가 중요한 이유는 만약 PWM주파수가 너무 낮다면 모터의 속도를
정밀하게 제어할 수 없으며 모터에서 소음이 발생하는 경우도 있기 때문이다. 아두이노의
표준 API에는 아쉽게도 PWM의 주파수를 조절할 수 있는 함수를 제공하지 않지만 사용자
가 만들어 놓은 라이브러리가 있다.

6.6.2 pwm.h 라이브러리

아래의 페이지에 자세하게 설명되어 있다.

pwm.h 라이브러리
pwm.h 라이브러리 다운로드 링크

다운로드 받은 파일의 압축을 풀면 세 개의 폴더가 있는데 이 중 PWM 폴더를 아래의 폴더


아날로그 출력 - 83

에 복사하여 붙여넣는다.

<아두이노 IDE 설치 폴더>\Arduino\libraries

윈도우즈 시스템에서는 보통 아래의 폴더이다.

C:\Program Files\Arduino\libraries

그러면 아두이노 IDE에 다음 [그림 6.6.1]과 같은 항목이 생성된다. 이 항목을 선택하면 텍


스트 에디터에 다음과 같이 인클루드문이 추가된다.

#include <PWM.h>

이것으로 이 라이브러리를 사용할 준비가 된 것이다.

[그림 6.6.1] PWM.h 를 메뉴에서 인클루드하는


방법

이 라이브러리에서는 다음과 같이 다섯 개의 전역 함수를 제공한다.


84 - 아날로그 출력

[표 6.6.2] pwm.h 라이브러리에서 제공하는 전역 함수들

함수명 기능

Initializes all timers. Needs to be called before changing


InitTimers()
the timers frequency or setting the duty on a pin

InitTimersSafe( Same as InitTimers() except timer 0 is not initialized in


) order to preserve time keeping functions

pwmWrite(uint8_ Same as 'analogWrite()', but it only works with


t pin, uint8_t initialized timers. Continue to use analogWrite() on
val) uninitialized timers
SetPinFrequency Sets the pin's frequency (in Hz) and returns a bool for
(int8_t pin, success. 주파수 범위는 31Hz~2MHz 사이이다.
i n t 3 2 _ t Sets the pin's frequency (in Hz) and returns a bool for
frequency) success. 주파수 범위는 31Hz~2MHz 사이이다.
SetPinFrequency
Safe(int8_t Same as SetPinFrequency except it does not affect
pin, int32_t timer 0. 주파수 범위는 31Hz~2MHz 사이이다.
frequency)

여기서 보면 InitTimers() 와 SetPinFrequency() 함수는 함께 사용되는데 이 함수들은 내부


적으로 timer0번을 초기화시킨다. 따라서 시간 관련 함수인 millis(), micros(), delay(),
delayMicroseconds() 함수들이 정상 동작하지 않는다. 그리고 3, 5, 9, 10번 핀을 PWM 핀
으로 사용할 수 있다.

반면 InitTimersSafe()와 SetPinFrequency() 함수는 역시 쌍으로 사용되는데 timer0를


초기화시키지 않으므로 시간 관련 함수들이 정상적으로 동작한다. 그리고 3,9,10번 핀을
PWM으로 사용할 수 있다. 두 경우 모두 PWM값을 쓰기 위해서는 analogWrite()함수 대신
pwmWrite()함수를 사용한다.

[표 6.6.3] pwm.h의 두 함수 그룹 비교

함수 그룹 특징 장단점 적용 핀

InitTimers() 3,
millis(), micros(), delay(),
SetPinFrequenc timer0 5,
delayMicroseconds() 함수들이 정상
y() 초기화 9,
동작하지 않는다.
10

InitTimersSafe
timer0 millis(), micros(), delay(), 3,
()
초기화 delayMicroseconds() 함수들이 정상 9,
SetPinFrequenc
안함 동작한다. 10
ySafe()

시간 관련 함수들이 정상적으로 동작 하지 않는 것은 큰 문제이므로 InitTimersSafe() 과


SetPinFrequencySafe() 함수를 사용하는 것이 좋을 것이다.
PART Ⅱ
아두이노 응용
Chapter

07
아두이노 라이브러리
88 - 아두이노 라이브러리

이전 장에서 설명했던 아두이노의 기본적인 기능에 더해서 주변 기기들을 간편하게 제에할 수


있는 다양한 라이브러리가 존재한다. arduino.cc 에서는 이들 라이브러리를 표준 라이브러리와
그외의 것들로 구분하는데 표준 라이브러리는 아두이노IDE에 포함된 것으로 다음과 같은 것들
이 있다.

표 7.1 아두이노 표준 라이브러리


라이브러리명 기능 비고
EEPROM EEPROM에 읽고 쓰는 기능을 하는 함수들.
Ethernet Arduino Ethernet Shield를 이용한 인터넷 접속.
Firmata 시리얼 프로토콜을 이용하여 PC와 연결하여 데이터를 주고 받기.
GSM GSM shield를 이용하여 GSM/GRPS 네트워크에 접속하기.
LiquidCrystal LCD 제어.
SD SD카드에 읽고 쓰기.
Servo 서보모터 제어.
SPI SPI(Serial Peripheral Interface) 통신.
SoftwareSerial 시리얼통신을 소프트웨어적으로 구현한 것.
Stepper 스테핑모터 구동.
TFT Arduino TFT screen에 문자, 이미지, 도형 표시하기.
WiFi Arduino WiFi shield를 이용하여 인터넷 접속하기.
Wire TWI (I2C) 통신.

표 7.2 특수 라이브러리
라이브러리명 기능 비고
Audio SD카드에 저장된 오디오파일 재싱.
Scheduler 다중 non-blocking 태스크 관리. Due
USBHost 마우스와 키보드 같은 USB주변장치와의 통신.
Esplora Esplora 보드의 센서와 작동기를 쉽게 접근하도록 함. Esplora
Keyboard 연결된 컴퓨터에 키보드 입력 전송. Leonardo,
Mouse 연결된 컴퓨터의 마우스 커서 제어. Micro,Due,Esplora

표 7.3 기타 라이브러리 (사용하기 위해서는 설치 과정이 필요함)


라이브러리명 기능 비고
Messenger PC로부터의 텍스트기반 메시지 처리.
NewSoftwareSerial SoftwareSerial 라이브러리의 개선판
OneWire 1-wire protocol을 사용하는 장치 제어(Dallas Semiconductor)
PS2Keyboard PS2 키보드로부터 문자 읽어들이기.
Simple Message System 아두이노와 PC간 메시지 보내기.
통신
SSerial2Mobile 휴대폰을 이용하여 문자메세지나 이메일 보내기.
Webduino 확장가능한 웹서버 라이브러리(이더넷쉴드)
X10 AC전원선으로 x10신호 보내기.
XBee XBee와의 통신.
SerialControl 시리얼 통신으로 다른 아두이노 제어하기
아두이노 라이브러리 - 89

Capacitive Sensing 두 개 혹은 그 이상의 핀을 정전식 터치센서로 사용.


센싱
Debounce (버튼으로부터) 바운싱을 제거하기.

GFX 기반 클래스(표준 그래픽 루틴)


GLCD KS0108 칩 기반의 그래픽 LCD 라이브러리
Improved LCD library LCD라이브러리의 오류 수정 버전
LedControl LED행렬/7세그먼트 제어 (MAX7221/MAX7219)
LedControl 여러개의 LED를 Maxim칩으로 제어하는 다른 라이브러리 디스플
레이/L
LedDisplay HCMS-20xx 스크롤링 LED디스틀레이 제어. ED
Matrix 기본적인 LED매트릭스를 제어하는 라이브러리.
PCD8544 Nokia55100 LCD 제어기(Adafruit ind.)
Sprite LED매트릭스의 애니메이션 사용을 위한 기반 클래스
ST7735 1.8“ TFT 128x160 스크린 제어 라이브러리(adafruit)

FFT 오디오 혹은 다른 아날로그 신호의 주파수 해석.


audio
Tone 오디오 주파수의 구형파 생성.

TLC5940 16채널 12비트 PWM 제어기 PWM

DateTime 현재 날짜와 시간을 추적.


Metro 정해진 시간 간격으로 수행. 타이밍
MsTimer2 타이머2를 써서 매 N밀리초마다 정해진 일을 수행

PString 버퍼에 프린팅을 하기위한 가벼운 클래스


유틸
Streaming 프린트문을 간략히 하기위한 메쏘드

본 장에서는 이러한 라이브러리 중에서 많이 사용하는 것들에 대해서 알아보도록 하겠다.

7.1 Servo 라이브러리


Servo라이브러리는 서보모터의 제어를 위한 객체를 정의한 것이다. 서보모터는 R/C카의 조향
장치나 마이크로로봇의 구동장치로 쓰이는 모터로서 신호선 한 가닥으로 0도~180도의 각도를
조절할 수 있는 것이다.

[그림 7.1.1] 서보모터의 외형

그림에서 보듯이 전원선인 Vcc/GND와 신호선 세 가닥이 인터페이스의 전부이다. 모터축의 각


90 - 아두이노 라이브러리

도는 신호선으로 인가되는 펄스폭으로 조정된다.

[그림 7.1.2] 서보모터의 신호선으로 인가되는 펄스

[그림 7.1.2]에 서보모터의 신호선으로 인가되는 펄스를 도시하였다. 주기 B는 3ms~20ms 의


범위를 가지며 주기가 짧을 수록 모터축의 회전 속도가 빨라진다. 펄스의 폭 A는 1.5ms 일때 중
심각도인 0도를 가리키게 되고 1ms(최소값 0.7ms)일때 –90도, 2ms(최대값 2.3ms)일 때 +90도
를 갖는다. 즉, 이 펄스폭을 조절하여 –90도에서 90도 사이의 원하는 각도를 가지도록 제어할
수 있는 것이다.
아두이노의 Servo라이브러리는 이러한 펄스를 발생시키는 기능을 구현하고 있으며 사용자는
세부 동작 원리를 알지 못하더라도 라이브러리 함수를 사용하는 방법만 알면 서보모터를 제어할
수 있다. 생성자는 입력인수가 없는 것 하나이며 멤버함수로 다음과 같은 것들이 있다.

void attach(pin) : 서보모터의 신호선이 연결된 핀을 지정한다.


void attach(pin, min_us, max_us) : 펄스폭 A의 최소, 최대값까지 지정한
다.
void write(angle) : 각도를 지정한다. (angle: 0~180)
void writeMicroseconds(us) :펄스폭 A를 us단위로 지정한다.
int read() : 현재 각도를 읽는다.반환값은 [0, 180]의 정수.
boolean attached() : 서보모터 핀이 지정되어 있는지 검사한다.
void detach() : 서보모터의 지정된 핀을 내부적으로 제거한다.

실험에 사용된 서보모터는 SG-90이라는 초소형 모터이다.

[그림 7.1.3] 실험에 사용된 서보모터


아두이노 라이브러리 - 91

7.1.1 첫 번째 예제

첫 번째로 서보모터를 설정하고 중심각도를 유지하는 간단한 예제를 작성해 보자.

╒══════════════════════════════════
#include <Servo.h>
Servo sm;
void setup() {
sm.attach(9); //9번 핀에 서보모터의 신호선을 연결
sm.write(90); //90도(중심각) 각도로 회전
}

void loop() {
}
╘══════════════════════════════════

7.1.2 두 번째 예제

두 번째로 서보모터 축이 0도에서 180도로 계속 왕복 운동을 하도록 프로그램을 작성해


보자.
╒══════════════════════════════════
#include <Servo.h>
Servo sm;
void setup() {
sm.attach(9); //9번 핀에 서보모터의 신호선을 연결
}

void loop() {
sm.write(0); //0도로 회전
delay(2000);
sm.write(180); //180도로 회전
delay(2000);
}
╘══════════════════════════════════

7.2 LiquidCrystal 라이브러리


가전제품이나 전자 장치에서 간단한 정보를 표시하는데 많이 사용되는 것이 바로 액정 표시기
(liquid crystal display, LCD)이다. 크게 문자만을 표시할 수 있는 문자형과 선이나 도형 등 그
래픽을 그릴 수 있는 그래픽형으로 나뉜다.
92 - 아두이노 라이브러리

[그림 7.2.1] LCD 장치의 예

아두이노의 LiquidCrystal 라이브러리는 텍스트 액정(liquid crystal) 표시장치를 구동하기 위


한 라이브러리이다. 이 라이브러리를 이용하기 위해서는 다음과 같이 프로그램 서두에 해당 헤
더파일을 인클루드시켜야 한다.

#include <LiquidCrystal.h>

이 헤더파일에는 LiquidCrystal 이라는 클래스가 정의되어 있으며 다음과 같은 공용(public) 멤버함수들


이 정의되어 있어서 쉽게 사용할 수 있다.

- 생성자 : LCD 모듈과 아두이노의 연결 방식에 따라서 생성자를 사용하면 된다. 다음과 같은
네 가지 함수가 오버로드되어 있다.

❶ LiquidCrystal(rs, enable, d4, d5, d6, d7)


❷ LiquidCrystal(rs, rw, enable, d4, d5, d6, d7)
❸ LiquidCrystal(rs, enable, d0, d1, d2, d3, d4, d5, d6, d7)
❹ LiquidCrystal(rs, rw, enable, d0, d1, d2, d3, d4, d5, d6, d7)

보통은 핀수를 가장 적게 차지하는 ❶번 방식이 많이 사용된다.

- 공용 멤버함수

void begin(row, col) : LCD를 초기화 한다.


void clear() : 화면을 지운 후 커서를 처음 자리로 옮긴다.
void home() : 화면은 그대로 나둔채로 처서만 (0,0)자리로 옮긴다.
setCursor(col, row) : 커서를 (col, row)자리로 옮긴다.
byte write(data) : 한 문자를 출력한다.
print(data, BASE) : 문자열이나 변수 값을 출력한다.
createChar() : 사용자 (그림)문자를 생성한다. (8개까지 가능함)
void cursor() : (밑줄)커서를 표시한다.
void noCursor() : 커서를 표시하지 않는다.
void blink() : 깜박이는 커서를 표시한다.
void noBlink() : 커서를 표시하지 않는다.
void display() : 내용을 표시한다.
void noDisplay() : 내용을 표시하지 않는다.(메모리에 내용은 남아 있음)
scrollDisplayLeft() : 표시된 내용을 왼쪽으로 한 칸 이동한다.(회전)
scrollDisplayRight() : 표시된 내용을 오른쪽으로 한 칸 이동한다.(회전)
아두이노 라이브러리 - 93

void autoscroll() : 기존 내용을 좌로 이동시킨 후 현재 내용을 표시.


noAutoscroll()
void leftToRight() : 문자 표시 방향을 왼쪽에서 오른쪽으로 설정한다.
void rightToLeft() : 문자 표시 방향을 오른쪽에서 왼쪽으로 설정한다.

여기서 write()함수는 입력을 아스키(ASCII)값으로 받으며 print()함수는 문자열이나 상수


(변수)값을 받는 것에 유의하자.

다음 예제들은 DFRobot사의 LCD쉴드를 이용하여 실험을 진행하였다.

[그림 7.2.2] (a)실험에 사용된 LCD쉴드 (b)회로도

회로도를 보면 RS가 8번 핀, RW가 9번핀, 그리고 D4~D7번 핀이 4번에서 7번핀까지로 결


선되어 있음을 알 수 있다. 따라서 LiquidCrystal 클래스의 인스턴스는 첫 번째 생성자를 이
용하여 다음과 같이 생성하면 된다.

LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

이제 예제들을 작성해 보도록 하겠다.

7.2.1 첫 번째 예제

첫 번째 예제로 간단한 문자열을 출력하는 프로그램이다.


╒══════════════════════════════════
// 헤더파일을 인클루드 시킨다.
#include <LiquidCrystal.h>
// 회로도를 참조하여 LCD 객체를 생성/초기화시킨다.
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
void setup() {
// LED의 행수와 열수를 주어 표시를 시작한다.
lcd.begin(16, 2);
// LDC에 문자열을 표시한다.
lcd.print("hello, world!");
94 - 아두이노 라이브러리

}
void loop() {
// 커서의 위치를 0열, 1행(두 번재 행)으로 설정한다.
lcd.setCursor(0, 1);
// 초를 표시한다.
lcd.print(millis()/1000);
}
╘══════════════════════════════════

7.2.2 두 번째 예제

LiquidCrystal 라이브러리에는 blink() 와 noBlink() 함수가 있는데 깜박이는 직사각형의


커서를 표시할 것인지 아닌지를 설정하는 함수이다. 다음 예제를 이것을 이용한 것이다.
╒══════════════════════════════════
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
void setup() {
lcd.begin(16, 2);
lcd.print("hello, world!");
}

void loop() {
lcd.noBlink();
delay(3000);
lcd.blink();
delay(3000);
}
╘══════════════════════════════════

유사한 함수로 cursor() 함수와 noCursor()함수가 있는데 이것은 밑줄 모양의 커서를 표시


할 것인가 말 것인가를 설정하는 것이다. 이 경우 밑줄 커서는 깜박이지 않는다.

7.2.3 세 번째 예제

LiquidCrystal 라이브러리의 공용 멤버함수 중에는 display()함수와 noDisplay()함수가 있


다. 함수 명에서 알 수 있듯이 화면에 표시를 할 것인지 말 것인지를 설정하는 것이다.
╒══════════════════════════════════
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
void setup() {
lcd.begin(16, 2);
lcd.print("hello, world!");
}
void loop() {
lcd.display();
delay(3000);
lcd.noDisplay();
아두이노 라이브러리 - 95

delay(3000);
}
╘══════════════════════════════════
위 예제어서 noDisplay()함수가 실행되면 화면에 표시된 문자열이 사라지지만 그 내용은 내
부 메모리에 여전히 남아있다. 따라서 display()함수가 호출되면 되살아나는 것이다. 또한
이 함수들을 이용하면 화면 전체를 깜박이는 효과를 줄 수도 있다.

7.2.4 네 번째 예제

문자열이 표시되는 방향을 바꾸는 함수로 rightToLeft()함수와 leftToRight()함수가 있다.


다음 예제는 이것을 이용한 것이다.
╒══════════════════════════════════
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

int thisChar = 'a';

void setup() {
lcd.begin(16, 2);
lcd.cursor();
}

void loop() {
// ‘m’ 이 표시되면 방향을 바꾼다.
if (thisChar == 'm') {
lcd.rightToLeft();
}

// 's'가 표시되면 방향을 다시 바꾼다.


if (thisChar == 's') {
lcd.leftToRight();
}

// 'z'가 표시된 후 처음 조건으로 다시 리셋한다.

if (thisChar > 'z') {


lcd.clear(); // 화면을 지운 후 커서를 처음 자리로
thisChar = 'a';
}

lcd.write(thisChar);
delay(1000);
thisChar++;
}
╘══════════════════════════════════

7.2.5 다섯 번째 예제

이번에는 터미널에 입력한 문자를 LCD에 표시하는 프로그램을 작성해 보자.


96 - 아두이노 라이브러리

╒══════════════════════════════════
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup() {
lcd.begin(16, 2);
Serial.begin(9600);
}

void loop() {
// 문자가 전송되면
if (Serial.available()) {
// 전체 문자열이 들어올 때까지 잠시 기다린다.
delay(100);
lcd.clear();
// 전송된 모든 문자를 표시한다.
while (Serial.available() > 0) {
lcd.write(Serial.read());
}
}
}
╘══════════════════════════════════

7.2.6 여섯 번째 예제

화면의 내용을 스크롤(scroll)하는 예제이다.


╒══════════════════════════════════
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup() {
lcd.begin(16, 2);
lcd.print("hello, world!");
delay(1000);
}

void loop() {
// 왼쪽으로 스크롤
for (int cnt = 0; cnt < 13; cnt++) {
lcd.scrollDisplayLeft();
delay(150);
}
// 오른쪽으로 스크롤
for (int cnt = 0; cnt < 29; cnt++) {
lcd.scrollDisplayRight();
delay(150);
}
// 다시 왼쪽으로 스크롤하여 제자리로
for (int cnt = 0; cnt < 16; cnt++) {
lcd.scrollDisplayLeft();
delay(150);
}
delay(1000);
}
╘══════════════════════════════════
아두이노 라이브러리 - 97

이 예제에서는 scrollDisplayLeft() 함수와 scrollDisplayRight() 함수를 이용하여 수동으


로 이동시켰다. 반면 autoScroll()함수를 사용하면 문자가 표시되기 전에 왼쪽으로 자동으로
이동이 된다. (실습해 보길 바란다.)

7.3 저항 하나로 터치센서 구현


아두이노의 디지털핀 두 개와 저항 하나로 터치센서를 구현할 수 있는데 관련 객체를 제공하
는 것이 바로 Capacitance Sensing Library 이다.

7.3.1 기본 개념과 구성 회로

정전용량식 터치 센서의 개념도는 아래 그림과 같다.

[그림 7.3.1] 정전용량식 터치 센서의 개념도

이 개념도를 보면 두 개의 핀을 사용하는데 하나는 발신핀(send pin) 이고 다른 하나는 수신


핀(receive pin)이다. 이 두 핀을 저항으로 연결하고 수신핀단에 터치할 도체(foil)를 연결한다.
만약 이 도체를 터치를 하면 수신핀 단의 정전용량이 틀려지고 발신핀에서 신호를 보낼 때(즉
발신핀이 on되었을 때) 수신핀이 기립 시간(rising time, 발신핀이 on 된 후 수신핀이 on이 되
는데 걸리는 시간 간격)이 변하게 된다. 이 변화를 감지하여 터지가 되었는지 안 되었는지를 판
단하는 것이다. 아래에 데모 동영상이 있다.
98 - 아두이노 라이브러리

[그림 7.3.2] 실제 응용 예

7.3.2 해당 라이브리리 설치와 사용법

이 기능을 아두이노에서 사용하려면 먼저 라이브러리를 다운로드 받아서 설치해야 한다. 아


래와 같은 절차를 따른다.

1. 소스를 다운로드 받는다.( CapacitiveSensor04.zip )


2. 압축을 푼 후 그 안의 CapacitiveSensor 폴더를 복사해서 Arduino/libraries/
폴더에 붙인다.
3. 아두이노 IDE에서 Sketch>Import Library>CapacitiveSensor 메뉴를 선택한다.

위와 같이 했다면 IDE에 다음과 같이 헤더파일이 인클루드 된다.

#include <CapacitiveSensor.h>

이 라이브러리에서는 CapacitiveSensor라는 클래스가 있는데 이것의 인스턴스는 다음과 같이


초기화 할 수 있다.

CapacitiveSensor CapacitiveSensor(byte sendPin, byte receivePin);

여기서 sendPin과 receivePin 은 저항이 연결된 두 개의 디지털 핀을 지정해주면 된다. 이제


이 인스턴스의 메소드(함수)로 다음과 같은 것들이 있다.

long capacitiveSensorRaw(byte samples)


아두이노 라이브러리 - 99

이 함수는 감지된 정전용량 값을 반환이며 반환값은 무단위이다. 입력으로 주어지는


samples 는 (byte형이므로 0-255 사이의 값) 샘플링 횟수를 지정해 주는 것이다. 이 값이 커지
면 리턴값의 분해능이 커지는 대신 속도가 느려지게 되므로 적당한 값을 선택해야 한다. 리턴값
은 각각의 샘플링에서 얻어진 값을 모두 더한 값이며 평균값이 아니다.

long capacitiveSensor(byte samples)

이 함수 역시 감지된 정전용량 값을 반환이며 반환값은 무단위이다. 앞의 함수와 다른 점은


터치가 안 되었을 때의 값(기본값)을 추적하여 현재 감지된 값에서 그 값을 뺀 다음 반환한다.
따라서 터지가 안 된 상태라면 0에 가까운 작은 값을 반환하고 터치가 되었다면 그것 보다는 큰
숫자를 반환한다.

기본값(터치가 안 되었을 때의 정전용량 값)은 CS_Autocal_Millis 라는 상수에 저장된 시간


마다 자동으로 조정된다. 초기값은 20000 밀리초 (20초)이다. 이 갱신 시간은
set_CS_Autocal_Millis() 함수를 이용해서 변경할 수 있다

void set_CS_Autocal_Millis(unsigned long autoCal_millis);

자동 갱신 기능을 끄려면 매우 큰 값(0xFFFFFFFF)으로 지정하면 된다.

만약 즉시 기본값을 조정하려면 다음 함수를 호출하면 된다.

void reset_CS_AutoCal()

그리고 사용 빈도는 낮지만 다음 함수는 capacitiveSensor()함수나 capacitiveSensorRaw()함


수의 타임아웃 시간을 지정해주는 함수도 있다.

void set_CS_Timeout_Millis(unsigned long timeout_millis);

이 함수는 CS_Timeout_Millis 내부 변수를 변경하는 함수인데 초기값은 2000 밀리초(2초)이


다. 타임아웃시간이란 발신핀이 on 되었는데도 수신핀이 on이 되지 않을 경우 언제까지 기다려
야 하는가를 정하는 것이다. 타임아웃 시간이 지나도 수신핀이 on이 되지 않으면 -2를 반환한
다.
100 - 아두이노 라이브러리

7.3.3 저항값의 선택

저항값은 다음과 같은 기준으로 선택한다.

- 1 MΩ (혹은 이것보다 다소 작은 용량) : 완전히 터치되었을 때에만 반응시키고자 할 때


- 10 MΩ : 4~6인치 정도 떨어진 곳에서도 반응시킬 때
- 40 MΩ : 12~24인치 정도 떨어진 곳에서도 반응시킬 때 (도체의 크기에 따라 가변적임)
- 즉, 저항값이 커지면 감도도 높이지고 반응 속도는 느려진다.
- 또한 터치부가 노출된 금속판이라면 발신부에서 서 on 신호를 발생시키지 못 할 가능성도
있다.
- 수신핀을 작은 용량의 커패시터 (100 pF ~ 0,01uF) 를 통해서 접지시키면 센서의 안정도를
개선시킬 수 있다.

7.3.4 실제 예제 프로그램

회로도는 다음과 같다. 아두이노 우노와 1MΩ 저항 하나로 구성한다.

[그림 7.3.3] 회로도

다음 프로그램을 실행하면 D3핀 쪽의 리드선을 터치할 때 LED가 켜진다.

#include <CapacitiveSensor.h>
#define LED 13
아두이노 라이브러리 - 101

CapacitiveSensor cs23 = CapacitiveSensor(2,3);

void setup() {
pinMode(LED, OUTPUT);
cs23.set_CS_AutocaL_Millis(0xFFFFFFFF);
Serial.begin(9600);
}

void loop() {
long start = millis();
long total1 = cs23.capacitiveSensor(30);
(total1>10) ? digitalWrite(LED, HIGH):digitalWrite(LED, LOW);
}

7.4 I2C (two wire) 통신


아두이노로 TWI (또는 I2C) 통신을 하기 위해서는 Wire 라는 라이브러리를 이용해야 한다.
시리얼 통신이 1:1 통신 규약인 반면, TWI는 1:n 통신 규약으로서 하나의 마스터 기기가 여러
개의 슬레이브 디바이스들과 통신을 할 수 있다는 장점이 있다.

#include <Wire.h>

그리고 통신을 하는 기기들 간에 SDA, SCL, 그리고 GND 끼리 연결하고 전압 레벨이 맞는지 반
드시 확인해야 한다. 즉, 5V 기기는 5V 끼리, 3.3V기기는 3,3V 기기 상호간에 연결해야 하면
3.3V와 5V를 연결하려면 레벨 컨버터를 통해야 한다.

연결된 기기는 하나의 master와 다수의 slave로 구분하고 slave들은 7bit 혹은 8bit 아이디(숫
자)로 서로를 구분한다. 모든 통신 주도권은 master가 가진다. slave에서 데이터를 읽어올 때도
master에서 요구하고 slave로 데이터를 보낼 때도 마찬가지로 master에서 보낸다. 마스터에서
는 다음과 같이 초기화 함수를 호출한다.

Wire.begin(); // 마스터의 TWI 초기화

슬레이브에서는 다음과 같이 초기화 함수를 아이디를 가지고 호출한다.

Wire.begin(n); //슬레이브의 TWI 초기화


102 - 아두이노 라이브러리

예를 들어서 2번 슬레이브는 Wire.begin(2); 라고 하면 된다. 이 아이디를 가지고 마스터에서 기


기간 구별을 하게 된다.

(1) 마스터에서 슬레이브로 데이터 보내기

마스터에서 슬레이브로 데이터를 보낼 때는 다음과 같은 순서를 따른다.

Wire.beginTransmission(id); //❶ 아이디가 id인 슬레이브와 TWI 통신을 시작한다.


Wire.write(by); //❷ byte형 데이터를 보낸다.
Wire.endTransmission(); //❸ 통신을 끝낸다.

Wire.write()함수는 입력 변수의 자료형에 따라 다음과 같이 세 가지가 오버로드 되어 있다.

Wire.write(byte by); // byte형 데이터를 하나를 전송


Wire.write(string str); // 문자열 str을 전송
Wire.write(const byte *ptrByte, int n); // byte형 포인터에 저장된 byte 배열에서
n개의 데이터를 전송

슬레이브 쪽에서는 마스터에서 데이터를 보냈을 때 호출되는 이벤트 함수를 등록해 두면 데이터
가 보내졌을 때 이 함수가 호출되어 알맞게 처리를 해주면 된다.

Wire.onReceive(receiveEvent); // 데이터가 보내졌을 때 호출될 receiveEvent() 라는 함수를


등록한다.

...

void receiveEvent(int iNum) // 이 이벤트 핸들러는 넘겨받은 바이트 수를 입력으로 건네준다.


{
byte by[iNum];
for(int n=0; n<iNum; n++)
by[n] = Wire.read();
}

(2) 마스터에서 슬레이브로 부터 데이터 읽기

마스터가 슬레이브에서 데이터를 받을 때도 마스터에서 먼저 요구하며 그 요구에 슬레이


브가 반응하도록 되어 있다. 마스터에서 슬레이브에 데이터를 요구하고 읽어오는 과정은 다
음과 같다.

byte by[n], m=0;


아두이노 라이브러리 - 103

Wire.requestFrom(id, n); // id를 가지는 슬레이브에 n 바이트를 보내기를 요구한다.


while( Wire.available() ) // 읽어올 데이터가 버퍼에 남아있다면
{
by[m++]=Wire.read(); // 데이터를 읽는다.
}

슬레이브 측에서는 마스터에서 요구가 올 때 처리할 이벤터 핸들러를 등록해 두면 된다.

Wire.onRequest( requestEvent ); // 마스터의 전송 요구시 호출된 requestEvent()라는 함수


를 등록
...
void requestEvent()
{
Wire.write(100); // (예를 들어) 100이라는 숫자를 전송한다.
}

7.4.1 멀티바이트를 전송하기

아두이노의 TWI로 한 바이트나 문자열을 주고 받는 것은 Wire.read(), Wire.write() 함


수를 사용하면 쉽게 할 수 있으므로 전혀 문제가 없는데 문제는 멀티 바이트로 구성된
short, long, float 등의 데이터를 주고 받는 것이다. 예를 들어서 signed short형 (아두이노
에서는 2bytes임) 데이터를 전송하려고 하면 바이트로 쪼개서 보낸 다음, 받는 곳에서 다시
이를 조합해서 원래의 데이터로 복구시켜야 하는데 무척 번거롭다.
이런 경우에 union 이라는 자료형을 사용하면 문제를 쉽게 해결할 수 있다. 예를 들어서
다음과 같이 (signed) short 형 데이터를 저장하기 위한 union을 정의한다. (volatile 은 인
터럽트 루틴 안에서 사용되기 때문에 붙인 것이다.)

union UShort{
volatile short sVal;
volatile byte byArr[2];
};

union자료형은 포함된 변수의 메모리를 공유하기 때문에 여기서 보면 byArr[0]은 short형


sVal 변수의 하위바이트를, byArr[1]은 상위 바이트를 가진다.

이제 이것을 이용해서 변수를 생성한다. (volatile 을 붙인 이유는 이전과 같다.)


104 - 아두이노 라이브러리

volatile UShort uMtr; // pwm value to motor


volatile UShort usv; // encoder value

그러면 usv.sVal 변수를 보통의 short형 변수처럼 사용하다가 TWI로 전송할 때는


usv.byArr[0]과 usv.byArr[1] 두 바이트를 차례로 보내주면 된다. 예를 들어서 슬레이브의
TWI 전송함수는 다음과 같다.

void requestEvent()
{
Wire.write( (const byte *)usv.byArr, 2);
usv.sVal = 0;
}

주의할 것은 usv.byArr 을 반드시 (const byte *) 형으로 캐스트해줘야 한다는 점이다. 그


렇지 않으면 컴파일 시에 에라가 발생한다.

수신단에서도 동일한 union을 정의해 놓고 받은 데이터를 바이트 배열 byArr 에 차례로


넣어주면 곧바로 sVal 변수를 통해서 short형 변수값을 쓸 수 있다.

void receiveEvent(int howMany)


{
uMtr.byArr[0] = Wire.read(); // 하위바이트 받음
uMtr.byArr[1] = Wire.read(); // 상위 바이트 받음
MotorInput( uMtr.sVal ); // 받은 short형 데이터를 바로 사용한다.
}

이 함수에서 보면 uMtr.byArr[0] 과 uMtr.byArr[1] 을 차례로 전송받은 후 바로


uMtr.sVal 변수값을 사용하였다. 이렇게 별도로 원래 데이터를 쪼개서 보내고, 받은 후에
(비트 연산자 같은 것을 사용해서)복구하는 과정이 전혀 필요가 없는 것이다.

다음 사진에서 보면 좌측의 모터부가 DC모터-모토쉴드R3-아두이노우노 이렇게 연결되


어 있고, 우측의 아두이노프로미니와 TWI로 연결되어 있다. 프로미니에서 모터의 PWM값
을 전송하면 그것을 우노가 받아서 모터를 회전시키고 발생하는 엔코더 신호를 우노에서 프
로미니로 전송하는데 이 값들이 모두 signed short 형이다.
아두이노 라이브러리 - 105

다음은 아두이노 우노의 전체 소스코드이다. _DEBUG 상수값을 정의시키면 디버그를 위


한 시리얼 통신부가 컴파일 되고, 디비깅이 끝났다면 이것을 commnet-out 시켜 시리얼 통
신부를 컴파일에서 제외시키면 된다.
--------------------------------------------------
//#define _DEBUG

#include <PWM.h>
#include <Wire.h>

union UShort{
volatile short sVal;
volatile byte byArr[2]; //uint8_t
};

#define ID_TWI 2

// motor shield constants


#define PWM_A 3
#define DIR_A 12
#define BRAKE_A 9
#define SNS_A A0 // current sening pin

// encoder constants
#define phaseA 0 // phase-A of rotary encoder to *INT0*
#define phaseB 7 // phase-B of rotary encoder to D7

volatile UShort usv; // encoder value


volatile UShort uMtr; // pwm value to motor

int32_t frequency = 120000; // PWN frequency in [Hz], maximum:


2,000,000

void setup() {
//(pwm.h)initialize all timers except for 0, to save time keeping
106 - 아두이노 라이브러리

functions
InitTimersSafe();
//(pwm.h)sets the frequency for the specified pin
bool success = SetPinFrequencySafe(PWM_A, frequency);

// Configure the motorshield A output


pinMode(BRAKE_A, OUTPUT); // Brake pin on channel A
pinMode(DIR_A, OUTPUT); // Direction pin on channel A

// set ISR for encoder


attachInterrupt(phaseA, EncoderCount, FALLING);
pinMode(phaseB, INPUT);

usv.sVal = 0;
uMtr.sVal = 0;

// TWI setup
Wire.begin(ID_TWI); // join i2c bus as SLAVE with
address #4
Wire.onRequest(requestEvent); // register event to send sensor
value
Wire.onReceive(receiveEvent); // register event to receive motor
pwm value

MotorForcedStop();

#ifdef _DEBUG
//////////////////////////////////////////////////////
Serial.begin(115200); // working!!! others: 115200, 250000,
460800
# e n d i f
//////////////////////////////////////////////////////////////
}

#ifdef _DEBUG
void ReadEncoder()
{
Serial.print("en:");
Serial.println(usv.sVal);
usv.sVal = 0;
}
#endif

void loop() {
#ifdef _DEBUG /////////////////////////////////////////////////
short sTmp = 0;
if (Serial.available())
{
sTmp = Serial.parseInt();
while(Serial.available()) Serial.read(); // empty buffer
Serial.print("Received: ");
Serial.println(sTmp);
MotorInput(sTmp);
}
delay(50);
ReadEncoder();
아두이노 라이브러리 - 107

#endif /////////////////////////////////////////////////////////
}

void EncoderCount() {
usv.sVal += (1 - digitalRead(phaseB)*2); // LOW: +1, HIGH: -1
}

void MotorInput(short sIn)


{
if (digitalRead(BRAKE_A) == HIGH)
digitalWrite(BRAKE_A, LOW); // setting brake LOW disable motor
brake

if (sIn >= 0)
{
digitalWrite(DIR_A, HIGH);
pwmWrite(PWM_A, sIn); // pwm.h
}
else if (sIn < 0)
{
digitalWrite(DIR_A, LOW);
pwmWrite(PWM_A, -sIn); // pwm.h
}
}

void MotorForcedStop()
{
digitalWrite(BRAKE_A, HIGH); // setting brake LOW disable
motor brake
// pwmWrite(PWM_A, 0); // pwm.h <- this HINDERS motor from
forcing stop
}

void requestEvent()
{
Wire.write((const byte *)usv.byArr, 2);
#ifdef _DEBUG
////////////////////////////////////////////////////
Serial.print("enc : ");
Serial.println(usv.sVal);
# e n d i f
///////////////////////////////////////////////////////////
usv.sVal = 0;
}

void receiveEvent(int howMany)


{
uMtr.byArr[0] = Wire.read();
uMtr.byArr[1] = Wire.read();
#ifdef _DEBUG ///////////////////////////////////////////////////
Serial.print("TWI reads: ");
Serial.println(uMtr.sVal);
#endif //////////////////////////////////////////////////////////
MotorInput(uMtr.sVal);
}
------------------------------------------------
108 - 아두이노 라이브러리

7.4.2 TWI 통신의 주파수 높이기

TWI 통신 (I2C라고도 한다)은 두 가닥의 선(SDA, SCL)만으로 여러 개의 디바이스와 통


신을 할 수 있다는 장점을 가지며 아두이노에서도 이를 지원한다. 이것의 통신 속도는 마스
터 기기에서 발생시키는 클럭(SCL)신호를 기준으로 정해지며 보통 많이 사용되는 표준 주
파수는 100kHz 와 400kHz 이다. 단순하게 이론적으로 계산하면 100kHz 의 주파수라면 초
당 100k 비트(바이트 아님)를 전송할 수 있으며 초당 약 12,5k 바이트를 전송할 수 있다.
아두이노의 I2C 통신에 사용되는 클럭 주파수는 100kHz 로 맞추어져 있다. 아두이노에
서 쓰이는 AVR은 400kHz 의 주파수도 지원을 하며 대부분의 I2C 통신 기기들이 이 주파
수를 지원한다. 그런데 아두이노 API에서는 이 클럭 주파수를 조절하는 함수나 메쏘드가 없
다. 이것을 400kHz로 상향시키기 위해서는 다음과 같이 약간 번거로운 과정을 거쳐야 한다.
아두이노 IDE 1.5.5 와 윈도즈를 기준으로 설명하도록 하겠다. 먼저 다음 파일을 연다
C:\Program Files \ Arduino \ hardware \ arduino \ avr \ libraries
\ Wire \ utility \ twi.h

이 코드의 윗부분에 보면 다음과 같은 상수가 있다.

#ifndef TWI_FREQ
#define TWI_FREQ 100000L
#endif

이름에서 알 수 있듯이 TWI_FREQ 상수가 I2C 통신의 클럭 주파수를 정의한 상수이다. 이


상수를 400000L 로 바꾸면 된다.

#ifndef TWI_FREQ
#define TWI_FREQ 400000L
#endif

이렇게 변경하고 저장한 후 한 가지 과정을 더 거쳐야 한다. 현재 프로젝트의 (과거에


100kHz 상수 값으로 생성되었던)오브젝트 파일들인 wire.cpp.o , twi.c.o 파일들을 제거해야
하는데 이것을 제거하지 않으면 과거에 컴파일된 오브젝트파일을 가지고 링크를 하기 때문
에 변경 사항이 적용되지 않는다.
다음의 폴더를 열어보자. (윈도7의 경우임)

C:\ Users \ [user id] \ AppData \ Local \ Temp


아두이노 라이브러리 - 109

이 폴더 하위에 많은 build***********.tmp 폴더는 아두이노 프로젝트가 컴파일되면서


생성되는 임시파일들을 저장하는 폴더이다. 이것들을 모두 삭제한 다음 다시 컴파일하면 변
경된 속도가 적용된다.
위와 같이 오브젝트 (임시)파일을 삭제하는 절차가 번거로우면 아예 프로젝트를 새로 생
성해서 코드를 붙여넣은 후 컴파일하면 된다.
Chapter

08
사용자 라이브러리
112 - 사용자 라이브러리

이전 장에서 아두이노에서 정한 표준 라이브러리에 대해서 설명하였다. 본 장에서는 인터넷으


로 내려 받은 사용자 라이브러리를 설치하는 방법과 직접 라이브러리를 작성하는 방법에 대해서
좀 더 자세히 설명하도록 하겠다.

아두이노 IDE를 설치하면 {내문서}\Arduino\libraris 폴더가 생성되고 여기에 다운로드 받


은 라이브러리를 복사하거나 사용자가 직접 작성한 라이브러리를 저장한다. 컴파일을 수행할 때
이 폴더는 자동으로 인클루드되어서 여기에 있는 라이브러리들이 링크된다.

라이브러리는 기본적으로 폴더단위로 저장되어야 한다. 예를 들어서 MyLib 라는 이름으로


(보통 라이브러리명은 대문자로 시작한다.) 라이브러리를 작성한다면 {내문
서}\Arduino\libraris 폴더 밑에 MyLib라는 폴더가 있어야 하고 이 폴더 밑에 MyLib.h 헤더
파일과 MyLib.cpp 파일이 있어야 한다. 만약 예제 파일을 제공하고 싶다면 example 폴더 밑에
예제 파일의 이름과 같은 폴더를 두고 그 밑에 .ino 파일을 위치해 두어야 한다. 이를 도시하면
다음과 같다.

{내문서}\Arduino (폴더)
 libraries (폴더) : 여기에 사용자 라이브러리를 저장한다.
 MyLib (폴더)
 examples (폴더)
  example1 (폴더)
   example1.ino
  example2 (폴더)
  example2.ino
 MyLib.h
 MyLib.cpp
[그림 8.0.1] 아두이노 라이브러리 폴더의 구조

보통은 라이브러리는 클래스나 변수 등을 선언한 헤더 파일(.h)과 클래스 멤버함수의 정의부가


있는 .cpp 파일로 나뉜다. 그리고 헤더 파일에는 MyLib 클래스가 선언되어 있어야 한다. 보통은
헤더 파일과 본체 파일(.cpp 파일)로 나뉘지만 모든 선언과 정의를 헤더 파일에 둘 수도 있다.
하지만 헤더 파일에는 클래스의 선언부를 작성하고 그것을 구현한 cpp파일을 별도로 두는 것이
더 일반적이고 바람직하다. 이렇게 구성하면 이 라이브러리를 사용하는 스케치파일에서는 헤더
파일만 인클루드해서 사용할 수 있고, 여러 곳에서 중복해서 사용할 경우에도 헤더 파일만 인클
루드 하면 되기 때문이다.
사용자 라이브러리 - 113

8.1 다운로드 받은 라이브러리 설치법


다음 홈페이지에서 다양한 아두이노의 라이브러리에 대해서 설명하고 있다. 사용자는 여기에
서 필요한 라이브러리를 다운로드 받아서 설치할 수 있다.
http://arduino.cc/en/Reference/Libraries

예를 들어서 OneWire 라는 라이브러리를 다운로드 받았다고 하자. 그러면 다운로드 폴더에


OneWire.zip 파일이 다운로드 되어 있을 것이다. 이 압축파일에는 OneWire.h, OneWire.cpp 파
일이 들어 있다. 이 경우 이 압축파일을 이용하여 바로 사용자 라이브러리 폴더에 복사할 수 있
는데 다음과 같이 Sketch>Add library from Zip file 메뉴를 이용하여 압축파일을 선택해 주면
된다.

이 메뉴를 선택한 후 OneWire.zip 파일을 선택하면 자동으로 사용자 라이브러리 폴더에 압축이
해제되서 복사해 들어가게 된다.

OneWire.zip 파일은 다운로드 받은 압축파일에 필요한 라이브러리 파일이 직접 들어가 있는


경우이지만 경우에 따라서 인터넷에서 받은 파일이 여러 버전의 라이브러리를 가지고 있는 것도
있다. 예를 들어서 우노용과 두에용이 분리되어서 있는 경우이다. 이런 경우에는 사용자가 직접
압축을 해제한 후에 필요한 폴더만 사용자 라이브러리 폴더에 수동으로 복사해 주어야 한다.
114 - 사용자 라이브러리

8.2 사용자 라이브러리 작성법


사용자 라이브러리를 작성하는 방법을 설명하도록 하겠다. 유용한 라이브러리를 어떤 사용자
가 작성하면 그것을 여러 다른 사용자와 공유할 수 있으므로 아두이노라는 오픈된 플랫폼의 발
전에 작게나마 기여할 수 있다.
이제 다음과 같이 간단한 LED로 모르스 부호를 표시하는 스케치 프로그램으로 시작하도록 하
겠다.
int pin = 13;

void setup()
{
pinMode(pin, OUTPUT);
}

void loop()
{
dot(); dot(); dot();
dash(); dash(); dash();
dot(); dot(); dot();
delay(3000);
}

void dot()
{
digitalWrite(pin, HIGH);
delay(250);
digitalWrite(pin, LOW);
delay(250);
}

void dash()
{
digitalWrite(pin, HIGH);
delay(1000);
digitalWrite(pin, LOW);
delay(250);
}

이것을 업로드하여 실행하면 내장 LED로 SOS신호를 3초 간격으로 반복하여 생성한다.

이 프로그램에서 dot() 함수와 dash()를 라이브러리로 빼내서 만들어보도록 하겠다. 여기서는


내장 LED를 사용하였지만 라이브러리에는 LED가 연결한 핀을 생성자에서 지정할 수 있도록 할
것이다. 생성자 안에서는 pinMode()함수를 호출하여 지정된 핀을 출력으로 지정하도록 한다. 이
제 변환을 시작해 보자.

앞에서도 설명한 바와 같이 아두이노 라이브러리는 보통 두 개의 파일이 필요하다. 헤더 파일


(.h 파일)에는 라이브러리에서 제공하는 클래스와 변수 등의 선언이 위치한다. 클래스 멤버함수
사용자 라이브러리 - 115

등의 실제 코드는 .cpp 파일에 작성하면 된다. 이렇게 작성해 놓으면 만약 다른 사용자가 이 라


이브러리를 참고할 때에는 헤더 파일만 보면 될 것이다. 다른 사용자가 궂이 보지 않아도 되는
실제 코드 부분은 다른 파일로 분리시켜서 가독성을 향상시키는 것이다.

헤더파일의 핵심은 다음과 같은 클래스 선언 부분이다.

class Morse
{
public:
Morse(int pin);
void dot();
void dash();
private:
int _pin;
};

클래스에는 사용되는 함수와 변수의 선언이 있으며 외부에서 호출해야 하는 함수는 public 으
로, 외부에 궂이 노출시킬 필요가 없는 함수/변수는 private 으로 선언한다. 클래스에는 생성자
(constructor)라는 것이 있는데 이것은 클래스의 인스턴스가 생성되는 시점에서 호출되는 함수
이다. C++의 문법 상 생성자는 클래스와 이름이 같으며 출력형식은 지정해주지 않는다.

헤더파일을 작성할 때 추가로 고려해야 되는 사항이 있는데 먼저 아두이노의 표준 함수들을


사용하기 위해서 다음과 같이 Arduino.h 헤더파일을 인클루드시켜야 하는 것이다. 이 헤더는 스
케치파일의 경우 자동으로 인클루드되므로 별도로 작성할 필요는 없지만 라이브러리 파일의 경
우에는 명시적으로 다음과 같이 포함시켜야 한다.

#include "Arduino.h“

또한, 전체 헤더파일을 다음과 같은 전처리문으로 둘러싸주는 것이 필요하다.

#ifndef Morse_h
#define Morse_h

// the #include statment and code go here...

#endif

이것은 C/C++에서 특정 헤더파일은 중복해서 인클루드되는 것을 방지하는 일반적인 방법이므


로 이해가 가지 않아도 상관없지만 관련 부분을 검색에서 한 번 숙지하기를 권한다.

마지막으로 주석문을 이용하여 라이브러리에 대한 간단한 설명을 헤더 파일 머리에 작성해주


116 - 사용자 라이브러리

는 것이 좋다. 여기에는 보통 보통 작성자, 날짜, 버전, 라이센스에 대한 정보를 기록한다. 이러


한 고려사항들을 모두 적용하면 헤더 파일은 다음과 같은 모양을 가질 것이다.

/*
Morse.h - Library for flashing Morse code.
Created by David A. Mellis, November 2, 2007.
Released into the public domain.
*/
#ifndef Morse_h
#define Morse_h

#include "Arduino.h"

class Morse
{
public:
Morse(int pin);
void dot();
void dash();
private:
int _pin;
};

#endif

이제 실제 코드가 위치하는 .cpp 파일의 작성법에 대해서 알아보자. 이 파일은 Arduino.h 와


방금 작성한 Morse.h 파일을 처음에 인클루드해야 한다.

#include "Arduino.h"
#include "Morse.h"

그 다음 생성자를 작성한다. 이 생성자는 클래스 인스턴스를 생성할 때 호출되므로 여기에서


pinMode()함수를 호출하여 사용자가 지정한 핀을 출력으로 설정하는 것이 좋다.

Morse::Morse(int pin)
{
pinMode(pin, OUTPUT);
_pin = pin;
}

함수명 앞의 Morse:: 는 이 함수가 Morse라는 클래스의 멤버함수라는 것을 지정한다. 이 클래


스의 다른 함수들을 정의할 때에도 같은 문법이 적용된다. 그리고 private 변수 _pin에서 맨 앞
의 _은 이 변수가 private 변수라는 것을 관례적으로 표시하는 방법이며 또한 함수의 입력 인수
pin과도 구별시켜주는 역할을 한다.
사용자 라이브러리 - 117

그 다음으로 원래의 스케치 파일에서 가져온 dot()함수와 dash()함수를 정의하면 된다. 이 함


수의 내부에서는 스케치 파일과 다르게 클래스의 멤버 변수 _pin을 사용한다.

void Morse::dot()
{
digitalWrite(_pin, HIGH);
delay(250);
digitalWrite(_pin, LOW);
delay(250);
}

void Morse::dash()
{
digitalWrite(_pin, HIGH);
delay(1000);
digitalWrite(_pin, LOW);
delay(250);
}

또한 간단한 설명을 담고 있는 주석문을 파일 서두에 포함시키는 것이 좋다. 전체 .cpp 파일


은 다음과 같다.

/*
Morse.cpp - Library for flashing Morse code.
Created by David A. Mellis, November 2, 2007.
Released into the public domain.
*/

#include "Arduino.h"
#include "Morse.h"

Morse::Morse(int pin)
{
pinMode(pin, OUTPUT);
_pin = pin;
}

void Morse::dot()
{
digitalWrite(_pin, HIGH);
delay(250);
digitalWrite(_pin, LOW);
delay(250);
}

void Morse::dash()
{
digitalWrite(_pin, HIGH);
delay(1000);
digitalWrite(_pin, LOW);
delay(250);
}
118 - 사용자 라이브러리

이것으로 라이브러리의 작성은 끝났으며 이 파일들은 {내문서}\Arduino\libraries\MyLib 폴


더에 위치하여야 아두이노 하프가 인클루드시킬 수 있다. 먼저 {내문서}\Arduino\libraries 밑
에 Morse 폴더를 생성한 후 이 두 파일을 그 안에 복사해서 넣으면 된다. 폴도의 구성도를 그려
보면 다음과 같다.

{내문서}\Arduino (폴더)
 libraries (폴더) : 여기에 사용자 라이브러리를 저장한다.
 Morse(폴더)
 Morse.h
 Morse.cpp

이제 아두이노 하프를 다시 실행시키면 Sketch>Import library 메뉴의 하위에 Morse 항목이 새


로 생긴 것을 확인할 수 있을 것이다.

주의할 점은 헤더 파일과 본체 파일 모두 이름이 속한 폴더와 같아야 한다는 것이다. 그리고 확


장자는 정확하게.h와 .cpp 이어야 한다.

이제 스케치에서 이 라이브러리를 사용하는 방법을 알아보자.

#include <Morse.h>
Morse morse(13);
void setup()
사용자 라이브러리 - 119

{
}

void loop()
{
morse.dot(); morse.dot(); morse.dot();
morse.dash(); morse.dash(); morse.dash();
morse.dot(); morse.dot(); morse.dot();
delay(3000);
}

이 파일은 원래의 스케치파일과 비교하여 몇 가지 다른 점이 있다. 먼저 라이브러리를 인클루


드하기 위해서 #include <Morse.h>를 서두에 추가한 것이다. 이것으로 라이브러리에서 정의된
클래스를 이용할 수 있다. 두 번째로 클래스의 인스턴스를 다음과 같이 생성한다는 것이다.

Morse morse(13);

이것이 실행되면 Morse 클래스의 생성자가 실행되며 13이라는 핀번호를 넘겨주게 된다. 이 생
성자 안에서 pinMode()함수를 실행했으므로 setup() 함수에서는 그럴 필요가 없다. 그리고
dot(), dash() 함수를 호출하기 위해서는 이 클래스 인스턴스를 매개로 해야 한다. 예를 들어서
13번 핀과 12번 핀에 LED가 연결되어 있다면 다음과 같이 하면 된다.

Morse morse(13);
Morse morse2(12);

8.2.1 멤버 함수 하이라이트 기능

만약 아두이노 IDE를 사용한다면 추가로 keyword.txt 파일을 만들어서 하이라이트될 함


수명을 지정해 줄 수 있다.

Morse KEYWORD1
dash KEYWORD2
dot KEYWORD2

각각의 줄은 함수명과 탭구분자 그리고 KEYWORD1/KEYWORD2 라는 지정어가 와야 한


다. KEYWORD1은 클래스명을 지정하면 KEYWORD2 는 멤버함수임을 지정한다.

8.2.2 예제 파일 제공

만약 다른 사용자들의 편의를 제공하기 위해서 예제를 제공하고 싶다면 Morse 폴더 밑에


examples 폴더를 생성한 후 그 안에 각각의 예제를 위한 폴더들을 만들고 그 안에 예제파
일을 두면 된다. (그림 8.0.1 참조.)
120 - 사용자 라이브러리

8.2.3 #ifndef ~ #endif 전처리문의 이해

동일한 헤더 파일(.h 파일)을 중복해서 선언하면 문법적으로 오류를 발생하게 되어 있다.


예를 들어서 MyLib.cpp 파일에서

#include “MyLib.h”
#include “MyLib.h”
....

와 같이 중복선언을 한다면 컴파일러 오류가 발생할 것이다. 사용자가 이런 식으로 작성하


는 경우는 거의 없겠지만 개발자의 의도와 관계없이 이런 경우가 발생할 수도 있다. 예를
들어서 a.h 에서 b.h 를 인클루드 했는데 a.cpp 파일에서 이를 간과하고 a.h 와 b.h 둘 다
인클루드 했다면 결과적으로 b.h 는 두 번 포함되는 것이다.
이 문제는 #ifndef _상수 ~ #endif 전처리문으로 회피할 수 있다. 전처리문은 컴파일이
수행되기 이전에 처리되며 이것의 동작은 이것으로 묶인 부분이 컴파일에 포함되려면 ‘_상
수’ 가 정의되지 않은 상태여야 한다.(if not defined 가 조합된 명령어가 #ifndef 이다.)
해결책은 전 절에서 언급한 바와 같이 헤더 파일 전체를 다음과 같이 이 전처리문으로 묶
는 것이다.

#ifndef MYLIB_H
#define MYLIB_H

class MyLib {
...
};

#endif

이제는 MyLib.h 헤더파일을 아무리 중복해서 인클루드해도 오류가 발생하지 않는다. 그


원리는 맨 처음 인클루드 명령에서는 MYLIB_H라는 상수가 정의되지 않았기 때문에
#ifndef ~ #endif 안의 프로그램이 수행되는데 그 안에서 MYLIB_H가 정의된다. 즉 맨 처
음 이 구문이 포함될 때 상수 MYLIB_H가 정의되므로 중복되서 인클루드되는 경우 두 번째
부터는 #ifndef ~ #endif 안의 프로그램이 포함이 되지 않는 것이다.

결과적으로 맨 처음의 #include “MyLib.h” 문만 처리되며 그 이후의 중복되는 #inclde


명령은 처리되지 않게 되어 MyLib 클래스의 선언부가 한 번만 확장된다.
Chapter

09
바퀴 로봇 제어
122 - 바퀴 로봇 제어

9.1 makeblock 사의 로봇 키트
makeblock 의 robot kit v2 를 구해서 조립해 보고 구성품을 좀 살펴보았다. 레고의 치수에
맞추어진 깔끔한 알루미늄 기구부가 일단 인상적이다. 표면 처리도 잘 되어있고 무엇보다 원하
는 형태로 자유자재로 결합되도록 설계가 되어있다. 이것으로는 ❶무환궤도 방식과 ❷)바퀴 방
식의 로봇을 만들 수 있었는데 그중 무한궤도 방식을 선택했다.

일단 제어 보드로 아두이노를 사용하고 적외선 센서와 리모콘 세트 그리고 초음파 센서와 구동


부로 구성되어 있다. 아두이노 코어와 모터 드라이버가 일체가 된 전용 제어보드와 아두이노 라
이브러리가 제공된다.
그런데 이 제어보드의 인터페이스가 특이해서 뭔가를 더 붙여서 확장하는 것이 불가능해 보인
다. 한 가지 더 마음에 안 드는 것은 제공되는 전용 라이브러리이다. 제공되는 기능 외에 더 추
가한다든가 하는 것이 힘들다.
바퀴 로봇 제어 - 123

그래서 일단 일반적인 아두이노 우노와 모터쉴드(R3)를 사용하여 모터를 구동해 보기로 하였


다. 이 로봇에 포함된 모터가 6V/2A 의 모터라 BA6208이나 LM1630 같은 IC등은 구동 전류를
충분히 내지 못해서 모터의 토크가 약해진다. 모터 한 개당 BA6208 두 개를 병렬로 연결하여
제어해 보았으나 크게 성능이 향상되지는 않았다. 모터쉴드에 대한 설명은 6.5절을 참조하면 된
다.
다음 사진은 모터 쉴드에 DC모터 전원선을 연결한 것이다. 밑에 아두이노 우노가 있고 모터
쉴드를 얹어서 모터에 연결하였고 전원은 1.5V AA건전지 6개(9V)를 사용한다.

프로그램 소스는 다음과 같다. 실험할 때 한 가지 주의할 점은 반드시 외부전원(건전지나 변압


기)를 보드에 연결하여야 한다는 것이다 USB만으로 모터를 구동시키려다가 PC보드가 손상될
수도 있다.

───────────────────────────────
#define PWM_RIGHT 3
#define DIR_RIGHT 12
#define BRAKE_RIGHT 9

#define PWM_LEFT 11
#define DIR_LEFT 13
#define BRAKE_LEFT 8

void setup() {
124 - 바퀴 로봇 제어

pinMode(PWM_LEFT, OUTPUT);
pinMode(DIR_LEFT, OUTPUT);
pinMode(BRAKE_LEFT, OUTPUT);

pinMode(PWM_RIGHT, OUTPUT);
pinMode(DIR_RIGHT, OUTPUT);
pinMode(BRAKE_RIGHT, OUTPUT);

analogWrite(PWM_LEFT,0);
analogWrite(PWM_RIGHT,0);
}

void loop() {
digitalWrite(DIR_LEFT, LOW);
digitalWrite(DIR_RIGHT,LOW);
analogWrite(PWM_LEFT, 255);
analogWrite(PWM_RIGHT, 255);
delay(2000);

digitalWrite(DIR_LEFT, HIGH);
digitalWrite(DIR_RIGHT, HIGH);
analogWrite(PWM_LEFT, 255);
analogWrite(PWM_RIGHT, 255);
delay(2000);
}
───────────────────────────────
바퀴 로봇 제어 - 125

[그림 9.1.1] 실험 장면

9.2 조향 제어
먼저 하나의 모터(왼바퀴에 연결된 모터)를 제어하는 함수를 다음과 같이 작성한다.

void leftMotorControl(int iSpd) {


int spd = abs(iSpd);
spd = (spd>255) ? 255:spd;
if (iSpd > 0){
digitalWrite(BRAKE_LEFT, LOW);
digitalWrite(DIR_LEFT, LOW);
analogWrite(PWM_LEFT, spd);
}
else if (iSpd < 0) {
digitalWrite(BRAKE_LEFT, LOW);
digitalWrite(DIR_LEFT, HIGH);
analogWrite(PWM_LEFT, spd);
}
else // if (spd == 0)
digitalWrite(BRAKE_LEFT, HIGH);

}
126 - 바퀴 로봇 제어

이 함수의 입력은 속도이다. +값이면 전진방향, -값이면 후진방향으로 모터가 돌아가도록 회로


를 설정한다. 만약 모터가 반대방향으로 돈다면 모터의 결선을 바꾸어 올바른 방향으로 회전하
도록 교정한다. 255값이면 최대 속도로 돌아가고 절대값이 작으면 더 천천히 회전한다. 만약 0
을 입력으로 주면 브레이크가 걸린다. 또한 255가 넘는 수라면 자동으로 255값으로 내부 변수
spd 가 설정되도록 작성되었다.

오른쪽 모터에 대해서도 똑같은 함수를 작성한 후 모터의 정지와 전후진을 담당하는 함수를 다
음과 같이 작성하였다.

void stopAll() {
leftMotorControl(0);
rightMotorControl(0);
}

void runForward(int iSpd, int iTime) {


leftMotorControl(iSpd);
rightMotorControl(iSpd);
if (iTime<0) return; // 시간이 음수라면 그냥 리턴
delay(iTime); // 아니면 delay 후 멈춤
stopAll();
}

여기에서 stopAll() 함수는 호출되는 즉시 양 쪽 모터들이 모두 멈추는 동작을 한다.


그리고 runForward()는 양쪽 모터를 똑같은 속도로 돌린다. 이 함수의 입력 인수가 두 개인데
하나는 속도이고 두 번째는 지속 시간이다. 속도값이 양수이면 전진, 음수이면 후진을 하며 지속
시간이 음수이면 계속 유지하도록 프로그램 되어 있다.
바퀴 로봇 제어 - 127

(a) 전진 (b) 후진

좌회전과 우회전은 양쪽의 모터를 서로 다른 속도로 돌리면 된다. 한 예로 두 모터를 속도는


같은데 서로 다른 방향으로 회전시킨다면 회전 중심이 로봇의 정 중앙이 되게 된다.

(a) 좌회전 (b) 우회전

이러한 동작을 수행하는 함수를 다음과 같이 작성한다.

void turnLeft(int iSpd, int iTime) {


leftMotorControl(-iSpd);
rightMotorControl(iSpd);
if (iTime<0) return;
delay(iTime);
stopAll();
}
128 - 바퀴 로봇 제어

void turnRight(int iSpd, int iTime) {


leftMotorControl(iSpd);
rightMotorControl(-iSpd);
if (iTime<0) return;
delay(iTime);
stopAll();
}

이 함수들도 회전 속도와 지속 시간을 인수로 주는데 시간이 음수값이면 계속 회전하는 상태로 놔두게
된다.

상수 정의와 초기 설정을 하는 부분은 다음과 같다.

#define PWM_RIGHT 3
#define DIR_RIGHT 12
#define BRAKE_RIGHT 9

#define PWM_LEFT 11
#define DIR_LEFT 13
#define BRAKE_LEFT 8

void setup() {
pinMode(PWM_LEFT, OUTPUT);
pinMode(DIR_LEFT, OUTPUT);
pinMode(BRAKE_LEFT, OUTPUT);

pinMode(PWM_RIGHT, OUTPUT);
pinMode(DIR_RIGHT, OUTPUT);
pinMode(BRAKE_RIGHT, OUTPUT);

analogWrite(PWM_LEFT,0);
analogWrite(PWM_RIGHT,0);
}

setup()함수에서는 모터제어 핀을 모두 출력으로 설정한 후 모터출력을 0V로 설정한다.


바퀴 로봇 제어 - 129

이제 직진->좌회전->직진->좌회전-> ... 과 같이 반복 동작하여 결과적으로 사각형을 그리면서 로봇이


움직이도록 프로그램을 작성해 보자. 앞에서 소개한 함수들을 이용하면 loop()함수는 다음과 간단하게 구
현할 수 있다.

void loop() {
runForward(255, 1000);
delay(1000);
turnLeft(255, 600);
delay(1000);
}

전체 프로그램 소스는 다음과 같다.


#define PWM_RIGHT 3
#define DIR_RIGHT 12
#define BRAKE_RIGHT 9

#define PWM_LEFT 11
#define DIR_LEFT 13
#define BRAKE_LEFT 8

void setup() {
pinMode(PWM_LEFT, OUTPUT);
pinMode(DIR_LEFT, OUTPUT);
pinMode(BRAKE_LEFT, OUTPUT);
130 - 바퀴 로봇 제어

pinMode(PWM_RIGHT, OUTPUT);
pinMode(DIR_RIGHT, OUTPUT);
pinMode(BRAKE_RIGHT, OUTPUT);

analogWrite(PWM_LEFT,0);
analogWrite(PWM_RIGHT,0);
}

void loop() {
runForward(255, 1000);
delay(1000);
turnLeft(255, 600);
delay(1000);
}

void stopAll() {
leftMotorControl(0);
rightMotorControl(0);
}

void runForward(int iSpd, int iTime) {


leftMotorControl(iSpd);
rightMotorControl(iSpd);

if (iTime < 0) return;

delay(iTime);
stopAll();
}

void turnLeft(int iSpd, int iTime) {


leftMotorControl(-iSpd);
rightMotorControl(iSpd);
if (iTime<0) return;
delay(iTime);
stopAll();
}
바퀴 로봇 제어 - 131

void turnRight(int iSpd, int iTime) {


leftMotorControl(iSpd);
rightMotorControl(-iSpd);
if (iTime<0) return;
delay(iTime);
stopAll();
}

void leftMotorControl(int iSpd) {


int spd = abs(iSpd);
spd = (spd>255) ? 255:spd;

if (iSpd > 0){


digitalWrite(BRAKE_LEFT, LOW);
digitalWrite(DIR_LEFT, LOW);
analogWrite(PWM_LEFT, spd);
}
else if (iSpd < 0) {
digitalWrite(BRAKE_LEFT, LOW);
digitalWrite(DIR_LEFT, HIGH);
analogWrite(PWM_LEFT, spd);
}
else // if (spd == 0)
digitalWrite(BRAKE_LEFT, HIGH);
}

void rightMotorControl(int iSpd) {


int spd = abs(iSpd);
spd = (spd>255) ? 255:spd;

if (iSpd > 0) {
digitalWrite(BRAKE_RIGHT, LOW);
digitalWrite(DIR_RIGHT, LOW);
analogWrite(PWM_RIGHT, spd);
}
else if (iSpd < 0) {
digitalWrite(BRAKE_RIGHT, LOW);
digitalWrite(DIR_RIGHT, HIGH);
132 - 바퀴 로봇 제어

analogWrite(PWM_RIGHT, spd);
}
else // if (spd == 0)
digitalWrite(BRAKE_RIGHT, HIGH);
}
Chapter

10
다른 아두이노들
134 - 부 록

10.1 아두이노 프로 미니
아두이노 우노는 크기가 상대적으로 크고 빵판에 꼽아서 실험하기 불편하기 때문에 이런 단점
을 보완한 소형 제품들이 몇 가지 마련되어 있다. 아두이노 정품 중에서 가장 작은 크기를 가지
는 보드로 프로 미니(pro mini)가 있다. 동작 전압에 따라 두 가지 모델이 분리되어 있다. (5V
동작 모델/ 3.3V동작 모델)

그림 10.1.1 아두이노 프로 미니의 외형

스펙은 다음과 같으며 단순 스펙 상으로는 아두이노 우노보다 핀수가 조금 더 많다.

∙ 메인 프로세서 : ATmega328 (초기 버전에서는 ATmega168)


∙ 디지털핀 16개 (6개의 PWM포함)
∙ 아날로그핀 8개
∙ 크기 : 1.3" x 0.7" (약 33 mm x 18 mm)
∙ 클럭 : 16MHz (5V 모델) / 8MHz (3.3V 모델)
∙ 가격 : US $9,95 (정품)
∙ 전원 : 3.35 -12 V (3.3V model) or 5 - 12 V (5V model)

전원을 인가할 때 한 가지 주의할 점은 정전압 (즉, 정류된 정확한 3.3V/5V 전압) 은 VCC핀에
연결해야 한다는 것이다. 정류되지 않은 전압(즉 정확히 3.3V/5V 가 아니라면)은 RAW핀에 연
결해야 하는데 내장된 레귤레이터가 필요한 전압으로 바꿔준다. 그리고 VCC 와 RAW 핀 두 개
에 동시에 전원을 인가하면 안된다. 보드가 상할 수도 있다고 한다.
핀아웃 다이어그램은 그림[7.1.2]와 같다. 아두이노 우노와의 차이점은 핀 배치가 틀리므로
표준 쉴드(shield)를 사용할 수 없다는 것과, 크기를 줄이기 위해서 USB인터페이스부가 생략되
어 있다는 점이다. 따라서 프로그램을 다운로드 하려면 별도의 UART to USB 변환기를 사용해
야 한다. ([그림 7.1.3] 참조) 따라서 이 제품은 다량의 완성품을 만들어야 하는 경우에 하나의
변환기로 여러 개의 보드를 프로그램할 수 있으므로 단가를 낮출 수 있어 유리하다.
부 록 - 135

[그림 10.1.2] 아두이노 프로미니의 핀아웃 다이어그램

[그림 10.1.3] 아두이노 프로 미니의 프로그램 다운로딩

외국의 쇼핑몰, 예를 들어서 sparkfun.com 에서 정품이 개당 9.95$ 이고 ebay.com 같은 곳에서


는 3.00$ 짜리 중국산 저가 복제품도 쉽게 찾을 수 있다. 또한 크기가 작으므로 소형 제품을 설
계할 때에도 유리하게 작용한다. 그리고 3.3V 모델이 별도로 마련되어 있어서 라즈베리 파이와
같은 3.3V 전원을 사용하는 마이컴과의 인터페이스도 용이하다는 장점도 있다.
136 - 부 록

이것과 유사한 소형 보드로는 아두이노 나노(nano)와 아두이노 미니(mini) 가 있는데 모두


Atmega328 기반의 제품들이다. 나노는 usb시리얼 변환 칩과 usb단자가 있다는 점이 프로미니
와 다르다. 미니와 프로미니는 핀배열이 조금 다르고 입력 전압을 인가하는 방식이 약간 틀린
것 같다. 즉 다음과 같은 모델들이 ATmega328 기반의 유사한 스펙이라고 보면 될 것 같다.

ㆍ아두이노 우노 (uno)
ㆍ아두이노 나노 (nano)
ㆍ아두이노 미니 (mini)
ㆍ아두이노 프로미니 (promini)

또 다른 소형 보드로 아두이노 마이크로(micro)라는 제품도 있는데 이것은 레오나르도를 소형화


시켜놓은 것이라고 보면 된다. (개인적인 경험으로 레오나르도 보드를 가지고 개발하는 것은 별
로 권하고 싶지 않은데, 프로그램을 다운로드 하고 리셋이 걸릴 때마나 가상 시리얼 포트를 매
번 잡아주는 것이 영 번거로웠기 때문이다. 우노를 가지고 어느 정도 아두이노에 익숙해 지면
도전해 보자.)

이것을 프로그램하려면 아두이노IDE에서 [도구]>[보드]>[Arduino Pro or Pro Mini] 메뉴를


선택한다. (아래 그림 참조) 그러면 [도구] 메뉴에 [프로세서] 라는 새로운 메뉴가 생성된다. 여
부 록 - 137

기서 프로세서(328/168)와 전압(5V/3.3V)을 선택하면 된다. 최근의 모델은 모두 ATmega328을


사용한다. 한 번 올바르게 설정한 이후에는 일반적인 아두이노를 프로그램하는 방식과 동일하게
다룰 수 있다.

10.2 아두이노 두에(due)

이탈리아어로 due 는 두에 라고 읽고 '둘'이라는 뜻이다. 숫자 1, 2, 3 이 uno, due, tre ... 이


다. 다른 것들은 모두 AVR계열의 프로세서를 사용해서 최대 클럭 속도(16MHz)나 배정도
(double) 실수 사용에 제약이 있으나 이것은 ARM Cortex-M3 프로세서를 사용하여 훨씬 높은
성능을 가지고 있다.

[그림 10.2.1]아두이노 두에 보드의 외형

일단 클럭주파수가 84MHz이고 내부적으로 3.3V를 사용 (보통 다른 아두이노 보드는 5V로 동작


한다)하므로 라즈베리파이의 GPIO와 핀끼리 바로 연결할 수 있다는 장점도 있다. 또한 AVR기
반의 아두이노 보드와 달리 DAC도 내장하고 있으며 프로그램에서 64bit double형도 다룰 수
있다고 한다. 성능이 높은 만큼 가격은 다소 비싼 편이다. 동작 클럭이 84MHz이므로 단순히 비
교하면 우노보드보다 5배 이상 성능이 높으며 디지털 핀수가 굉장히 많아졌고 PWM 개수도 우
노보드보다 더 많으며 해상도도 12bit까지 지정할 수 있다. 그리고 모든 디지털핀에 인터럽트를
설정해 줄 수 있다는 것도 큰 장점이다. 우노의 경우 두 개의 외부 인터럽트만 사용할 수 있다.
자세한 스펙은 다음 표에 수록하였다.
138 - 부 록

Microcontroller AT91SAM3X8E
Operating Voltage 3.3V
Input Voltage (recommended) 7-12V
Input Voltage (limits) 6-16V
Digital I/O Pins 54 (of which 12 provide PWM output)
Analog Input Pins 12
Analog Outputs Pins 2 (DAC)
Total DC Output Current on all
130 mA
I/O lines
DC Current for 3.3V Pin 800 mA
DC Current for 5V Pin 800 mA
Flash Memory 512 KB all available for the user applications
SRAM 96 KB (two banks: 64KB and 32KB)
Clock Speed 84 MHz

아두이노 두에(이하 두에)보드에는 USB연결단자가 두 개가 있는데 기본적으로 DC잭에 가까


운 마이크로USB포트와 PC를 연결하면 전원이 공급되며 프로그램을 업로드할 수 있는 환경이
된다. 이 경우 별도로 DC잭으로 전원을 공급할 필요는 없으나 필요할 경우 7V~12V를 연결해야
한다.

[그림 10.2.2] 아두이노 두에의 프로그램용 USB포트

한 가지 주의할 점은 기존 아두이노보드들은 5V로 구동되는데 비해서 이것은 구동 전압이


3.3V라는 것이다. 입출력 핀에 5V신호를 인가하면 보드에 손상이 올 수도 있다고 하니 주의해야
한다.
PC와 USB를 연결하고 스케치를 실행한 후 도구>보드>아두이노Due(Programming port)를 선
택하고 도구>포트>COM? 에서 올바른 포트를 선택하면 일단 환경설정이 끝난 것이다. 예제의
Blink 를 열어서 업로드하면 보드상의 LED 가 깜빡이는 것을 볼 수 있다.
핀의 구조를 도시한 다이어그램은 다음과 같다.
부 록 - 139

[그림 10.2.3] 아두이노 두에의 핀 기능 다이어그램

한 가지 주의할 점은 클럭이 84MHz 이니까 16MHz 를 사용하는 우노보다 6배 이상 성능이


높을 것이라고 기대하면 안된다는 것이다. 성능 테스트를 위해서 간단한 프로그램으로 테스트를
해 보았다. 프로그램은 다음과 같이 2번 핀을 연속적으로 on/off 시키는 것이다. 그리고 이 핀의
주파수가 어떻게 나오는가를 스코프로 찍어보는 것이다.

-----------------------------------------------
void setup() {
// put your setup code here, to run once:
pinMode(2, OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
while(1) {
140 - 부 록

digitalWrite(2, HIGH);
digitalWrite(2, LOW);
}
}
-------------------------------------------

2번 핀을 스코프로 찍어서 본 결과는 다음과 같다. 화질이 안 좋지만 세로 줄 한 칸이 1us이므


로 구형파의 한 주기가 5us 이다. 따라서 주파수는 200kHz라는 것을 알 수 있다. 가로줄은 한
칸이 1V이므로 on 상태에서 3.3V 의 값을 갖는것을 알 수 있다.

[그림 10.2.4] 오실로스코프로 확인한 실험 결과

똑같은 프로그램을 아두이노 우노에 집어 넣고 실행 시킨 결과 구형파의 한 주기가 8us 가 나왔


다. 주파수로 따지면 125kHz이다.
이것만으로 단순하게 성능비교를 하기에는 무리지만 두에가 우노보다 약 1.6배의 성능을 보여
준다. 우노의 클럭주파수가 16MHz 이고 두에가 84MHz이다. 내부적인 연산 속도는 5배 이상(실
수 연산의 경우에는 더 높은 성능을 기대할 수 있을 것이다.)의 성능을 가질 것으로 짐작할 수
있으나 디지털 핀과 같은 하드웨어를 제어하는 경우에는 우노에 비해서 5배 이상의 성능을 기대
한다는 것이 무리라는 결론이 나온다.

10.3 갈릴레오 보드

인텔의 갈릴레오 보드는 아두이노와 똑 같은 개발환경을 이용할 수 있는 고성능의 임베디드


보드이다.
부 록 - 141

[그림 10.3.1] 갈릴레오 보드 패키지

잠깐 사용해 본 간단한 느낌은 일단 업로드 속도가 일반 아두이노보다 매우 빠르다는 것과 아두


이노의 개발환경과 거의 동일하다는 것이다. 하지만 리눅스 OS 위에서 동작이 되기 때문에 실제
저수준의 성능이 매우 높다고 단정할 수는 없을 것 같다.

이 사진은 아두니오 프로미니 (좌상단)과 갈릴레오 보드를 TWI로 연결하여 통신 실험을 하는


것이다. 매우 잘 동작한다.
142 - 부 록

약간의 구글링 결과 제조사 홈페이지에서 확인한 바에 의하면 I2C 통신의 경우 갈릴레오 보드는
마스터로만 사용할 수 있고 100kHz의 속도만을 지원한다. 또한 GPIO핀의 신호를 변화시키는데
2ms 가 소요되고 실제 오버헤드를 고려한다면 230 Hz 정도가 최대이다. 생각보다 성능이 높지
는 않은 것 같다.
전 절에서 두에에서 했던 것과 똑같은 실험을 수행해 보았다. 즉 다음과 같은 코드를 실행시킨
다음 2번 핀의 주파수를 측정해 보는 것이다.

void setup() {
// put your setup code here, to run once:
pinMode(2, OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
while(1) {
digitalWrite(2, HIGH);
digitalWrite(2, LOW);
}
}

이 프로그램은 2번 핀을 단순히 on/off 시키는 것이다. 결과는 아래 그림과 같은데 좀 의외였다.


[그림 ]에서 스코프의 세로줄 한 칸은 1ms 이다. 따라서 구형파의 한 주기가 약 8.5ms이고 이
것을 주파수로 환산하면 약 118 Hz 라는 어이 없는 결과가 나온다. 400MHz 짜리 갈릴레오가
16MHz 짜리 우노 보다 하드웨어 제어 성능이 한참 아래라는 결론이다. 아무리 갈릴레오가 리눅
스 위에서 돌아가고 물리적인 핀들이 내부적으로는 I2C로 제어된다고는 하지만 성능이 너무 낮
은 것 아닌가 하는 생각이 든다.

[표 10.3.1] 구형파 신호의 주파수 실측 결과 요약

구형파의 한
보드 프로세서 동작 클럭 주파수
주기 시간
아두이노 우노 (uno) ATmega328 16 MHz 5 us 200,000 Hz (200 kHz)
아두이노 두에 (due) AT91SAM3X8E 87 MHz 8 us 125,000 Hz (125KHz)
인텔 갈릴레오 intel Quark 400 MHz 8.5 ms 118 Hz

물론 갈릴레오는 원보드 마이컴이라서 일반적인 아두이노와 종류와 그 타겟 분야가 다르기는


하다. 하지만 단순히 굉장히 빠른 아두이노라는 오해를 가지고 실제 프로젝트에 적용시키려면
부 록 - 143

충분한 검토가 이루어져야 할 것 같다.

[그림 10.3.2] 구형파의 주파수 실측 결과


PART Ⅲ
부 록
부 록 - 147

부록 A. C++ 의 클래스(class) 문법
아두이노의 프로그래밍 언어는 C++이다. 물론 C언어의 기초적인 문법만 이해하고 있어도 스
케치 작성에 무리가 없을 정도로 표준 함수나 라이브러리가 잘 만들어져 있기는 하지만 C++의
기본적인 문법을 알고 있다면 이해의 폭이 더 넓어질 것은 자명하다.
부록 A에서는 C언어와 확연히 구별되는 C++의 특징들을 위주로 기술하도록 하겠다. 먼저 함
수 중복, 디폴트 매개변수에 대해서 설명하고 C++의 핵심인 클래스(class)의 개념과 기본적인
문법들에 대해서 설명한다. 그리고 중간 중간에 아두이노에 적용할 수 있는 예제를 들어서 이해
를 돕도록 하겠다.

A.1 함수의 중복과 디폴트 매개변수

A.1.1 함수의 중복

C언어 프로그램은 같은 이름을 갖는 함수를 정의할 수 없으나 C++에서는 가능하다. 이렇


게 같은 이름을 갖는 함수들을 작성하는 것을 함수 중복(overloading)이라고 하며 전역 함
수와 (뒤에 나올) 멤버 함수 모두에 가능하다. 예를 들면 다음과 같다.

int print(int iA);


int print(strint strA);
int print(int iA, byte byMode);

이 예제들의 세 개의 print()함수는 모두 같은 이름을 갖지만 입력인수가 서로 다르므로 모


두 다른 함수들이다. 이와 같이 함수의 중복이 가능하려면 각각을 구별해야 하므로 다음의
조건들을 만족해야 한다.

❶ 중복되는 함수들의 입력 인수의 개수가 다르거나 타입이 달라야 한다.


❷ 반환형이 다르고 입력 인수가 같은 중복은 허용되지 않는다.

따라서 다음의 두 함수는 함수 중복의 요건이 안 되기 때문에 오류를 발생한다.

void print(string strA);


int print(string strA);

함수 중복을 이용하면 같은 종류의 일을 하는 서로 다른 입력 인수를 갖는 함수들을 같은


148 - 부 록

이름으로 일관성 있게 관리할 수 있으므로 무척 효율적이다. 또한 컴파일러는 함수가 호출


될 때 대응되는 함수를 프로그램이 실행될 때가 아니라 컴파일할 때 결정하므로 함수 중복
으로 인해서 실행 시간이 지연되는 경우는 없다.

A.1.2 디폴트 입력 인수

어떤 함수가 호출될 때 입력 인수에 값이 넘어오지 않는다면 미리 정해진 값을 함수 내


부에서 가지도록 선언된 입력 인수를 디폴트 입력 인수(default parameter)라고 한다. 이것
은 함수의 구현에서 다음과 같이 구현된다.

void func(int iA=0); // iA의 기본 값 10

변수 iA는 호출될 때 값이 넘어오지 않으면 자동으로 5로 넘어오게 선언되었다. 즉, 이 함


수를 호출할 때 입력인수를 주어도 되고 안 해도 된다.

func(); // 변수 iA는 0값을 자동으로 갖는다.


func(10); // 변수 iA에 10값을 넘겨준다.

만약 디폴트 입력 인수기능이 없이 함수 중복으로만 이를 구현한다면 다음 그림의 좌측과


같이 두 개의 함수를 별도로 작성해야 것이다.

void func() {
int iA=0;
/// 함수 본체
....
}
void func(int iA = 0);

void func(iA) {
/// 함수 본체
....
}

함수 중복으로 구현한 경우 디폴트 입력 인수 함수로 간략화

이것을 살펴보면 디폴트 입력 인수 기능이 프로그램을 상당히 간결히 해주는 유용한 기능임
을 알 수 있다.

다른 예를 들어보면 다음과 같다.

void send(int iA, string str=“Hello”);


부 록 - 149

이 함수는 다음과 같이 두 가지로 호출이 가능하다.

send(10);
send(10, “Hi”);

디폴트 입력 인수가 설정된 변수만 생략 가능하다는 점에 유의해야 한다. 따라서 다음과 같


은 호출은 오류를 발생시킬 것이다.

send();
send(“Hi”);

만약 다음과 같이 디폴트 입력 인수를 두 개 가진 함수라면

void sum(double dA, double dB=10.0, double dC=20,0);

다음과 같이 세 가지로 호출할 수 있다.

sum(1.0); // dA는 1.0, dB는 10.0, dC는 20.0 값을 갖는다.


sum(1.0, 2.0); // dA는 1.0, dB는 2.0, dC는 20.0 값을 갖는다.
sum(1.0, 2.0, 3.0); // dA는 1.0, dB는 2.0, dC는 3.0 값을 갖는다.

디폴트 입력 인수를 가진 함수를 선언할 때 주의할 점은 이것들이 모두 끝 쪽에 몰려있어


야 하며 디폴트 입력 인수를 가지는 변수 다음에 일반 변수는 못 온다는 것이다. 다음 예는
모두 잘못된 것이다.

void sum(int i1, int i2=5, int i3, int i4=10);// 에러 발생


void sum(int i1=10, int i2, int i3);// 에러 발생

그리고 다음과 같은 함수 중복도 오류를 발생한다.

void print(string strA);


void print(string strA = “Hello”);

그 이유는 두 함수가 입력 인수의 개수와 형이 같기 때문이다. 그리고 다음과 같은 함수 중


복도 오류를 발생한다.

void func(int iA, string strA=“Hi”);


void func(int iA);
150 - 부 록

두 함수는 분명히 입력 인수의 개수가 다르지만 만약 func(10) 이라고 호출했을 때 두 개


중 어느 것을 호출해도 문제가 없기 때문에 컴파일러는 오류를 발생시킨다.

A.2 클래스 만들기

클래스를 만들기 위해서는 class라는 키워드를 이용한다. C++클래스는 C언어의 구조체와 유


사하게 개발자가 정의하는 새로운 데이터 타입이다. 구조체가 변수들만을 멤버(member)로 가질
수 있는 반면 클래스는 변수와 함수 모두를 멤버로 가질 수 있다. 또한 멤버들의 접근 영역을
public 이나 private 키워드를 이용하여 지정해 줄 수 있다. 멤버를 접근할 때 점(.)을 이용한다
는 점은 C언어의 구조체와 동일하다.

클래스는 선언부(declaration)과 구현부(implementation)로 나뉜다. 다음은 클래스를 선언하


고 구현하는 기본적인 예이다.

// 클래스 선언부
class Rect {
public:
int width, height;
int getArea();
};

// 클래스 구현부
double Rect::getArea() {
return width*height;
}

멤버 함수 구현을 다음과 같이 클래스 선언부에 둘 수도 있다.

class Rect {
public:
int width, height;
int getArea(){
return width*height;
}
};

일견 후자의 경우가 더 간결해 보이고 실제로 함수 본체의 길이가 짧다면 별 문제가 없다. 하지
만 멤버 함수의 구현이 길어지고 이러한 멤버 함수가 여러 개라면 클래스의 전체적인 구조를 한
눈에 파악하기가 어려워지고 가독성이 떨어지게 된다. 따라서 전자와 같이 클래스의 선언부와
멤버함수의 구현부를 분리하는 것이 더 일반적이다.

A.2.1 클래스 선언부


부 록 - 151

클래스는 다음과 같이 class 키워드와 이름으로 선언한다.

class Rect {// Rect 라는 클래스 선언


...
}; // 반드시 세미콜론(;)으로 종료

클래스 이름은 일반적인 식별자 구성 규칙에 따라 개발자가 원하는 대로 붙이며 관습상


대문자로 시작한다. 클래스 선언은 클래스의 모양을 정의하는 것으로서 멤버 함수와 멤버
변수의 원형을 선언한다. 멤버들은 “{”와 “}” 으로 감싸고 마지막에 반드시 세미콜론(;)을 붙
여야 한다.

A.2.2 클래스 멤버

클래스의 멤버는 변수와 함수로 구성된다. 멤버 변수는 클래스 선언부에서 초기화될 수


없다. 따라서 다음과 같은 경우 오류가 발생한다.

class Rect {
public:
int width = 10, height = 20;
int getArea();
};

멤버 함수는 원형 형태로 선언되며 C언어의 함수 선언 규칙과 동일하고 반환 타입, 입력 인


수의 리스트 등이 모두 선언되어야 한다.

int getArea();

이 경우는 반환형은 int 이고 입력 인수는 없는 함수의 원형이다.

A.2.3 접근 지정자

접근 지정자(access specifier)는 클래스 외부에서 그 멤버들을 접근할 수 있는 범위를 지


정하는 것이다. C++의 접근 지정자는 public, private, protected 세 가지가 있으며 같은 접
근범위를 가지는 멤버를 모아놓고 그 앞에 public: 또는 private: 라고 써주면 된다. 여기서
는 일단 앞의 두 개에 대해서만 설명하겠다.

키워드 public 은 클래스 외부에서 자유롭게 접근할 수 있음을 지정하며 변수의 경우 외


152 - 부 록

부에서 읽고 쓰는 것이 허용되며 함수의 경우 외부 호출이 가능하게 된다. 예로 든 Rect 클


래스에서 멤버 변수 width, height 는 public 으로 선언되었으므로 외부에서 자유롭게 변경
이 가능하고 getArea()함수도 외부에서 호출이 가능하다.

반면, private 지정자는 클래스 외부에서는 전혀 접근할 수 없고 오직 클래스 내부에서만


접근 가능함을 지정하는 것이다. 만약 접근 지정자가 생략되어 있다면 default로 이 private
이 적용이 된다.

A.2.4 클래스 구현부

클래수 구현부에서는 클래스 선언부에 선언된 멤버 함수의 실제 코드를 작성한다. 구현부


에서 작성되는 멤버함수의 소속을 지정하기 위해서 함수명 바로 앞에 className ::을 앞에
붙여야 하며 이 함수가 className 의 멤버함수라는 것을 나타낸다. 앞의 예에서

double Rect::getArea() {
return width*height;
}

여기서 :: 는 ‘범위 지정 연산자’라고 하며 함수가 어떤 클래스의 멤버인지를 지정할 때 사


용되는 연산자이다.

A.3 객체 생성 및 활용

A.3.1 객체 생성

클래스가 정의되었다면 이제는 그것을 이용하여 실제 사용할 객체(object)를 생성할 수


있다.

Rect rect1;

이것을 실행하면 rect1 이라는 Rect 클래스를 구현한 객체가 생성되며 이것을 클래스의 인
스턴스(instance)라고 칭한다. 클래스의 인스턴스의 이름은 일반적인 변수 규칙에 따른다.
인스턴스가 생성되었다면 public 으로 지정한 멤버들은 닷(.) 접근자로 접근할 수 있다.
인스턴스의 이름 바로 뒤에 닷을 찍고 멤버의 이름을 써주면 된다. 예를 들면 다음고 같다.

rect1.width = 10;
rect1.height = 15;
부 록 - 153

int area1 = rect1.getArea();

첫 번째 줄과 두 번째 줄은 멤버 변수 width 와 height 의 값을 10과 15로 설정하는 부분


이고 세 번째 줄에서 getArea() 멤버 함수를 호출하여 지정한 값으로부터 면적을 계산하는
것이다. 결과값으로 area1 은 150 값을 가지게 될 것이다.

클래스 Rect를 이용하여 얼마든지 객체를 생성할 수 있다.

Rect rect2, rect3, rect4;


rect2.width = rect2.height = 12;
rect3.width = rect3.height = 13;
rect4.width = rect4.height = 14;

이렇게 선언하고 값을 지정한 경우 rect2.getArea() 함수값과 rect3.getArea(),


rect4.getArea() 함수값은 각각 어떤 값을 반환할 것인지 생각해 보자.

A.3.2 객체의 멤버 접근

전절에서 언급한 대로 인스턴스를 이용하여 멤버를 접근할 때는 닷(.)을 사용한다.

Rect myRect, yourRect;


myRect.width = 10;
myRect.height = 20;
yourRect.width = 30;
yourRect.height = 40;

당연한 이야기지만 이때 myRect 와 yourRect 의 width 변수는 각각 완전히 별개의 변수이


다. height변수도 마찬가지로 서로 다른 변수이다. 클래스의 인스턴스가 생성되면 별도의 메
모리에 멤버변수가 저장될 곳이 할당된다. 그리고 myRect.getArea() 함수가 내부적으로 접
근하는 변수는 myRect에 할당된 width 와 height 이다. yourRect.getArea()도 yourRect
에 내부적으로 할당된 변수를 이용한다.

myRect yourRect
int weight : 10 int weight : 30
int height : 20 int height : 40
int getArea() int getArea()
154 - 부 록

A.4 생성자

A.4.1 생성자란?

생성자(constuctor)란 클래스의 인스턴스가 생성되는 시점에서 자동으로 호출되는 특수한


멤버 함수이다. 생성자 내에서 일반적으로 멤버 변수들을 초기화 시키거나 기타 객체 인스
턴스를 생성하는데 필요한 작업들을 수행하게 된다. 생성자는 여러 개를 정의할 수 있으나
이중 하나만 실행된다. 아래는 생성자가 두 개인 경우의 예이다.

class Rect {
Rect(); // 생성자1 선언
Rect(int w, int h); //생성자2 선언
....
};

// 생성자 구현
Rect::Rect() { // 입력 인수 없는 생성자
width = 0;
height = 0;
}
Rect::Rect(int w, int h) { // 입력 인수가 두 개인 생성자
width = w;
height = h;
}

생성자는 반환값이 없다는 사실에 주의해야 한다. 반환 값이 없으니 반환형도 지정해 주지


않는다.(void 라고도 지정하지 않는다는 점에 유의하라.) 또한 생성자의 이름은 클래스의 이
름과 같다는 점도 입력할 때 주의해야 한다.
생성자는 반환값이 없기 때문에 어떤 값도 return해서는 안되지만 중간에 실행을 멈추기
위해서 반환값이 없는 단순 return문은 사용할 수 있다. 만약 다음과 같이 생성자를 입력한
다면 오류를 발생시킬 것이다.

Rect::Rect() { // 입력 인수 없는 생성자
width = 0;
height = 0;
return 0;
}

그리고 생성자는 여러 개를 중복하여 선언하고 구현할 수 있으나 각각의 입력 인수의 개


수나 타입이 서로 달라서 구별되어야 한다. 즉, 이 경우 C++의 일반적인 함수 중복의 규칙
에 따른다. 따라서 다음과 같이 여러 개의 생성자를 선언할 수 있다.

Rect();
부 록 - 155

Rect(int x);
Rect(double y);
Rect(int x, int y);

위에서 든 네 개의 생성자는 서로 입력 인수가 다르므로 다른 생성자이다. 단, 인스턴스 생


성시에는 이 중 단 하나만 실행된다.

A.4.2 인스턴스의 생성과 생성자의 자동 실행

이제 생성자를 실행시키는 방법을 알아보자. 생성자에게 입력 인수를 넘기려면 인스턴스


바로 뒤에 괄호로 묶어서 넘기면 된다.

Rect rect1; // 입력 인수가 없는 생성자 호출


Rect rect2(10,20); // 입력 인수가 두 개인 생성자 호출

마치 함수를 호출하는 듯하게 인스턴스를 생성하면 입력 인수가 매치가 되는 생성자가 호출


되게 된다. 즉, rect1이 생성될 때 Rect() 생성자가 호출되고 rect2가 생성될 경우에는
Rect(int w, int h) 생성자가 호출된다.

따라서 rect1의 내부 변수들은 모두 0으로 초기화 되어 있게 되고 rect2의 내부변수는 각


각 10과 20으로 초기화가 되었다. 따라서

rect1.getArea();
rect2.getArea();

는 각각 0과 200을 반환하게 된다.

A.4.3 기본 생성자

C++에서 클래스에는 반드시 생성자가 있어야 하며 인스턴스를 생성할 때 반드시 하나의


생성자가 호출되도록 정해져 있다. 사용자가 생성자를 하나도 선언/구현하지 않은 경우에도
컴파일러가 기본 생성자(default constructor)를 만들어서 인스턴스 생성시에 이 기본 생성
자를 호출하게 된다. 기본 생성자는 매개변수가 없는 생성자이다.

class Rect{
Rect(); // 기본 생성자
.....
};
156 - 부 록

사용자가 생성자를 하나도 선언/정의하지 않았을 경우 C++ 컴파일러는 백그라운드에서


기본 생성자를 삽입하여 인스턴스 생성 시에 호출한다.
한 가지 주의할 점은 생성자가 하나라도 선언된 클래스는 기본 생성자가 자동으로 생기지
않는다는 점이다. 이 말은 매개변수가 있는 생성자들만 있는 경우 인스턴스 선언에서 입력
인수가 없이 선언한 경우는 오류를 발생하게 된다는 의미이다. 기본 생성자가 없기 때문이
다. 예를 들어서

class Rect {
Rect(int w, int h);
....
};

Rect::Rect(int w, int h) { // 입력 인수가 두 개인 생성자


width = w;
height = h;
}

와 같이 클래스가 정의된 경우에

Rect rect;

라고 인스턴스를 생성하려하면 에러를 발생시킨다. 왜나면 이 경우 기본 생성자를 호출해야


하는데 클래스 정의에서 입력 인수가 두 개인 생성자가 정의되어 있으므로 컴파일러는 자동
으로 기본 생성자를 만들지 않기 때문이다. 따라서 없는 기본 생성자를 호출한 격이 되므로
에러를 발생한다.

A.5 소멸자

소멸자(destructor)는 생성자와는 반대로 인스턴스가 소멸될 때 반드시 호출되는 멤버 함수이


다. 소멸자는 객체가 삭제되는 시점에서 필요한 마무리 작업을 처리하기 위해서 있는 것이다. 예
를 들어서 동적으로 할당받은 메모리를 반환하거나 열어 놓은 파일을 닫거나 시리얼 등의 통신
연결을 끊거나 하는 마무리 작업을 수행한다.
소멸자의 이름은 클래스이름 앞에 틸다(~)를 붙인다. 예를 들어서 Rect 클래스의 소멸자 이름
은 ~Rect() 이다.
class Rect {
public:
Rect(); // 생성자
~Rect(); // 소멸자
.....
};
// 소멸자 구현
부 록 - 157

Rect::~Rect() {
.....
}

생성자와 마찬가지로 소멸자도 반환 값이 없으며 함수 내부에서 어떤 값을 반환해서도 안된


다. 또한 생성자가 여러 개가 있을 수 있는 반면에 소멸자는 입력 인수가 없는 기본형 하나밖에
없다.
생성자와 마찬가지로 사용자가 소멸자를 선언하지 않은 경우에는 컴파일러가 자동으로 기본
소멸자를 삽입한다. 이 기본 소멸자는 아무 일도 하지 않고 단순히 리턴하도록 만들어진다.

A.6 생성자/소멸자의 실행순서

변수와 마찬가지로 함수 내에서 생성된 인스턴스는 지역 인스터스, 함수 바깥에 선언된 인스


턴스는 전역 인스턴스이다. 다음 예에서 rect1 은 전역 인스턴스이고 rect2, rect3 는 지역 인스
턴스이다.

class Rect {
.....
};

Rect rect1;

void func() {
Rect rect2;
Rect rect3;
.....
}

int main() {
.....
func();
.....
}

전역 인스턴스는 프로그램이 실행될 때 생성되고 프로그램이 종료될 때 소멸된다. 그리고 지역


인스턴스는 함수가 실행될 때 생성되고 함수가 종료될 때 소멸된다.
위 예제와 같이 main()함수에서 func() 함수가 호출될 때 rect2, rect3 의 순으로 인스턴스가
생성되며 생성자도 같은 순서로 호출된다. 하지만 func() 함수가 종료될 때는 그 역순으로 소멸
되며 소멸자도 역순으로 호출된다.
전체적인 생성자와 소멸자의 호출순서는 다음과 같다.

프로그램 시작
158 - 부 록

rect1 객체 생성 (생성자 호출)


main함수 실행
.....
func()함수 실행
rect2 객체 생성 (생성자 호출)
rect3 객체 생성 (생성자 호출)
.....
rect3 객체 소멸 (소멸자 호출)
rect2 객체 소멸 (소멸자 호출)
func()함수 종료
.....
main함수 종료
rect1 객체 소멸 (소멸자 호출)
프로그램 종료

A.7 접근 지정자와 사용 예

객체 지향 언어에서 객체의 내부는 최대한 외부로 노출시키지 않도록 작성되어야 하고 꼭 필


요한 것만 접근을 허용시켜야 한다. C++에서 접근지정자는 다음과 같이 세 가지가 있다.

❶ public : 외부에 공개시킨다.


❷ protected : 상속 클래스에만 공개하고 그 외에는 비공개이다. 즉, 클래스 자신과 자
식 클래스의 멤버 함수에서만 접근가능하다.
❸ private : 외부에 비공개시킨다. 클래스의 멤버 함수에서만 접근가능하다.

접근 지정자는 여러 번 사용할 수 있으며, 한 번 지정되면 다른 지정자가 선언될 때까지 모든


멤버에 적용된다.

public: int iA;


public: float fA;
private: long lA;
short sA;
protected:double dA;

접근 지정을 하지 않은 경우 기본 접근 지정은 private 이다. 전에도 언급했듯이 클래스의 멤


버는 가능한 숨기는 것이 원칙이기 때문이다.

A.7.1 LED 클래스 예제

Led라는 간단한 아두이노 클래스를 예로 들어보자. 아두이노의 디지털핀에는 LED를 연


부 록 - 159

결할 수 있으므로 Led 클래스는 연결된 핀 번호를 갖는 멤버가 있어야 한다. 따라서 다음과
같이 작성할 수 있다.

class Led {
public: byte pin;
};

그리고 생성할 때 이 핀번호를 받도록 하려면 다음과 같이 생성자를 작성하면 될 것이다.

class Led {
public:
byte pin;
Led(int);
};
Led::Led(int dp) {
pin = dp;
pinMode(pin, OUTPUT);
}

이렇게 작성하면 멤버 변수 pin을 외부에서 접근할 수 있다. 예를 들면

Led ledRed(12);
ledRed.pin = 11;

와 같이 인스턴스를 생성한 이후에 핀번호를 바꾸는 것이 가능하다. 그렇지만 핀번호는 한


번 초기화되면 바꿀 필요가 없으므로 궂이 외부에 노출시킬 필요가 없다. 따라서 다음과 같
이 private 멤버로 지정하는 것이 바람직하다. 또한 내부 멤버임을 표시하기 위하여 첫 문자
를 underscore(_)로 했다.

class Led {
public:
Led(int);
private:
byte _pin;
};
Led::Led(int pin) {
_pin = pin;
pinMode(_pin, OUTPUT);
}

또는 기본 지정자가 private 이므로 아래와 같이 작성해도 된다.

class Led {
byte _pin;
public:
160 - 부 록

Led(int);
};
Led::Led(int pin) {
_pin = pin;
pinMode(_pin, OUTPUT);
}

하지만 명시적으로 private 키워드를 이용하여 지정해 주는 것이 가독성 측면에서 더 바람


직하므로 보통은 이렇게 하지 않는다.

이렇게 핀 번호를 private로 지정해 놓으면 외부에서 접근할 수 없으므로 LED를 켜고 끄


는 것도 외부에서 수행할 수 없다. 하지만 멤버 함수는 내부 변수를 접근할 수 있으므로
LED를 켜고 끄는 public 멤버 함수를 다음과 같이 지정할 수 있겠다. 전체적인 클래스의 모
양은 다음과 같을 것이다.

class Led {
public:
Led(int);
void turnOn();
void turnOff();
private:
byte _pin;
};

Led::Led(int pin) {
_pin = pin;
pinMode(_pin, OUTPUT);
}

Led::void turnOn() {
digitalWrite(_pin, HIGH); // 멤버 함수는 _pin을 사용할 수 있다.
}

Led::void turnOff() {
digitalWrite(_pin, LOW); // 멤버 함수는 _pin을 사용할 수 있다.
}

멤버 변수나 함수는 관례적으로 그 이름이 소문자로 시작하며 클래스는 대문자로 시작한다


는 점을 알아두자. 이렇게 하는 이유는 식별자(이름)만 보고 타잎을 유추하기 쉽게 하기 위
한 프로그래밍 관례이다.

만약 12번 핀에 빨간색 LED가 연결되어 있다면 다음과 같이 Led 객체를 사용할 수 있다.

Led ledRed(12);
....
부 록 - 161

ledRed.turnOn();
delay(500);
ledRed.turnOff();
delay(500);
....

이와 같이 인스턴스의 이름도 그것으로 실제 의미를 유추할 수 있도록 작성하는 것이 프로


그램의 가독성 측면에서 유리하다. 만약 11번핀에 노란색, 10번핀에 파란색 LED가 추가로
달려있다면 아래와 같이 작성할 수 있을 것이다.

Led ledRed(12), ledYellow(11), ledBlue(10);

이후에 만약 파란색 LED를 켜고 싶다면 다음과 같이 멤버함수를 호출한다.

ledBlue.turnOn();

이런 식으로 클래스를 이용하여 LED를 객체화 시키면 직관적으로 프로그램을 작성할 수 있


다는 장점이 있다.

A.7.2 Button 클래스 예제

본 절에서는 입력장치로 많이 사용되는 푸시버튼을 객체화 시키는 예제를 작성해 보도록


하겠다. 푸시버튼도 디지털핀에 연결할 수 있으며 외부에서 풀업을 시켜주느냐 아니면 내부
풀업을 시켜주느냐를 지정할 수 있도록 하겠다. Led 클래스와 마찬가지로 연결된 핀 번호는
private 변수에 저장한다. 그리고 내부에 풀업이 되었는지의 여부를 저장하는 변수도
private변수에 저장한다.
클래스의 선언을 다음과 같다.

class Button {
public:
Button(byte, boolean); // 생성자
boolean isPushed(); // 버튼이 눌렸다면 true를 반환하는 멤버 함

private:
byte _pin; // 연결된 핀
boolean _internalPullup; //내부에 풀업이 되었는가
};

생성자를 보면 입력 인수의 타입만 지정해 주었음을 알 수 있다. 생성자와 멤버 함수의


구현부는 다음과 같다.
162 - 부 록

// 생성자의 구현
Button::Button(byte pin, boolean internalPullUp = true) {
_pin = pin;
_internalPullup = internalPullUp;
if (_internalPullup)
pinMode(_pin, INPUT_PULLUP);
else
pinMode(_pin, INPUT);
}

// 멤버 함수의 구현
boolean Button::isPushed() {
return (digitalRead(_pin) == LOW)? true:false;
}

생성자의 구현을 보면 다음과 같이 디폴트 파라메터가 사용되었다.

Button::Button(byte pin, boolean internalPullUp = true) {...}

이렇게 구현해 놓으면 생성자는 파라메터를 하나를 받을 수도 있고 두 개를 받을 수도 있는


데 하나만 있다면 두 번째는 자동으로 true로 지정된다. 즉, 다음과 같이 두 가지로 생성하
는 것이 가능하다.

Button btn1(12); // 내부 풀업 저항을 연결하는 경우


Button btn2(11, false); // 외부에서 풀업 된 경우

두 번째 파라메터의 값에 따라서 pinMode()함수를 적절하게 호출하였음을 알 수 있다.


풀업 저항이 연결되었을 경우 푸시버튼을 누른 상태에서 digitalRead()함수의 반환값은
LOW가 된다. 따라서 이에 맞게 멤버 함수 isPushed() 가 작성되었음을 알 수 있다.

만약 클래스 선언이 Button.h에, 구현부가 Button.cpp 에 저장되어 라이브러리를 구성했


다면, 이것을 이용하여 버튼이 눌리면 내장 LED가 켜지고 그렇지 않으면 꺼지는 프로그램
을 다음과 같이 작성할 수 있다.

#include <Button.h> // 라이브러리 인클루드

Button btn1(12); //12번 핀에 내부 풀업 연결

void setup() {
pinMode(13, OUTPUT);
}

void loop() {
if (btn1.isPushed()) // 만약 버튼이 눌렸다면
부 록 - 163

digitalWrite(13, HIGH);
else // 그렇지 않다면 (눌리지 않았다면)
digitalWrite(13, LOW);
}

이와 같이 class를 이용하여 객체화를 시도하면 프로그램을 좀 더 직관적으로 작성할 수 있


다.

A.8 인라인 함수

덩치가 큰 프로그램을 작성할 때는 그것을 작은 단위로 나누어서 그 단위를 함수로 작성하는


것이 구조적 프로그램의 핵심이다. A라는 작업을 수행하기 위해서 하나의 함수에 전체를 구현하
는 방법도 있겠지만 A1, A2, 등등으로 나누어서 각각을 함수로 구현하는 것이 일반적이다. 하지
만 A를 수백 번, 수천 번 반복해야 할 경우에는 전자보다 후자가 처리 시간이나 메모리 효율면
에서 더 불리하다. 즉, 함수를 호출하는데 있어서는 그만큼의 댓가가 따르는 것이다. PC에서 수
행되는 프로그램을 작성할 때에는 이러한 오버헤드는 무시할 수 있을 것이다. 하지만 아두이노
와 같은 메모리나 성능이 매우 제약된 프로세서를 프로그래밍할 경우에는 무시 못할 성능 저하
요인디 될 수도 있다.
C++의 인라인(inline)함수는 함수의 길이가 매우 짧을 경우에 효율을 높이기 위해서 도입된
방법으로서 함수의 선언부 앞에 inline 이라는 키워드로 구현된다. 전절의 코드로 예를 들면 다
음과 같다.

inline boolean Button::isPushed() {


return (digitalRead(_pin) == LOW)? true:false;
}

컴파일러는 인라인 함수를 호출하는 곳에 인라인 함수의 코드를 그대로 삽입하여 함수의 호출이
일어나지 않도록 한다. 이렇게 되면 함수를 호출할 때 드는 비용이 없어지기 때문에 속도가 더
올라간다. 반대급부로 프로그램의 길이는 더 길어지게 된다.

다른 객체지향 언어(JAVA, C# 등등)도 마찬가지지만 C++도 비교적 작은 크기의 함수를 많이


사용한다. 대표적인 것이 멤버 변수를 접근하는 getter/setter 함수인데 이러한 사용 빈도가 높
고 크기가 작은 함수들을 inline으로 정의해 놓으면 더 높은 실행 효율을 얻을 수 있다.
하지만 inline으로 설정했다고 모두 코드를 치환하는 것이 아니고 컴파일러에 따라서 동작 방
식이 다르다. 자기 자신을 호출하는 재귀함수나 반복문, switch문 등이 포함된 함수는 inline으
로 지정했다고 하더라도 컴파일러에 의해서 인라인 선언이 무시될 수도 있다.
164 - 부 록

또한 생성자를 포함하여 모든 멤버 함수가 인라인으로 선언될 수 있다. 전 절의 Button클래스


의 멤버 함수를 인라인 함수로 지정하면 다음과 같다.

class Button {
public:
Button(byte, boolean); // 생성자
boolean isPushed(); // 버튼이 눌렸다면 true를 반환하는 멤버 함

private:
byte _pin; // 연결된 핀
boolean _internalPullup; //내부에 풀업이 되었는가
};

// 생성자의 구현
inline Button::Button(byte pin, boolean internalPullUp = true) {
_pin = pin;
_internalPullup = internalPullUp;
if (_internalPullup)
pinMode(_pin, INPUT_PULLUP);
else
pinMode(_pin, INPUT);
}

// 멤버 함수의 구현
inline boolean Button::isPushed() {
return (digitalRead(_pin) == LOW)? true:false;
}

멤버 함수의 크기가 작을 경우 클래스의 선언부에 바로 함수를 구현해도 무방하다. 즉, 위 예의


isPushed() 함수를 다음과 같이 클래스의 선언부에 바로 구현해도 된다.

class Button {
public:
Button(byte, boolean); // 생성자
boolean isPushed();{
return (digitalRead(_pin) == LOW)? true:false;
}
boolean isPushed(); // 버튼이 눌렸다면 true를 반환하는 멤버 함

private:
byte _pin; // 연결된 핀
boolean _internalPullup; //내부에 풀업이 되었는가
};

// 생성자의 구현
inline Button::Button(byte pin, boolean internalPullUp = true) {
_pin = pin;
_internalPullup = internalPullUp;
if (_internalPullup)
pinMode(_pin, INPUT_PULLUP);
부 록 - 165

else
pinMode(_pin, INPUT);
}

단 한 가지 주의할 점은 컴파일러는 클래스 선언부에 바로 구현된 멤버 함수들에 대해서는


inline선언이 없어도 인라인 함수로 자동으로 처리한다. 따라서 이 예에서 isPushed()함수는 인
라인 함수로 자동으로 간주된다.

A.9 C++의 구조체

C++은 기본적으로 C와 호환성을 갖게끔 설계되었으며 이것은 C로 작성된 프로그램은 수정


없이 C++컴파일러가 컴파일을 할 수 있다는 의미이다. C에서 struct(구조체)가 있으므로 C++도
이것이 있는데 표준 C구조체의 기능을 확장하여 C++의 클래스와 동일한 기능을 갖는다는 점에
유의해야 한다. 따라서 구조체도 멤버 변수뿐만 아니라 멤버 함수도 가질 수 있으며 접근 지정
자도 사용 가능하다. 예를 들어서

struct Name {
private:
int iA;
void func1();
public:
double dA;
void func2();
protected:
short sA;
};

C++의 구조체의 인스턴스 생성은 클래스와 동일한데 C언어의 경우에는 struct 키워드를 붙여
야 하지만 C++에서는 그렇지 않다.

Name ins1;

구조체와 클래스의 유일한 차이점은 구조체의 기본 지정자가 public 이라는 점이다. 즉, 접근


지정자가 명시적으로 지정되지 않으면 구조체는 public으로 지정된다.

struct Name {
int iA;
double dA;
};
166 - 부 록

이 예에서 멤버 변수 iA와 dA는 모두 public이므로 외부에서 자유롭게 접근 가능하다. 이렇게


정해진 이유는 역시 C언어와의 호환성 때문인데 C언어의 구조체 멤버는 기본적으로 외부에서
모두 접근이 가능하기 때문이다. 따라서 위의 구조체는 다음의 클래스와 기능적으로 동일하다.

class Name {
public:
int iA;
double dA;
};

구조체를 사용하느냐 클래스를 사용하느냐는 전적으로 사용자의 몫이겠지만 C++ 프로그램을


작성한다면 궂이 C언와의 호환성 때문에 남겨 놓은 구조체를 이용하는 것 보다는 클래스를 사
용하는 것이 더 바람직하다.

A.10 정적 멤버

클래스의 모든 멤버 변수와 함수는 정적(static) 멤버로 지정될 수 있다. 정적 멤버 변수는 인


스턴스가 생성될 때마다 독립적으로 생기는 멤버 변수와 달리 해당 클래스에 하나만 생성되고
모든 인스턴스에서 공동으로 접근할 수 있는 변수이다. 마찬가지로 정적 멤버 함수도 클래스당
하나만 생기며 모든 인스턴스에서 공동으로 호출할 수 있다.
멤버를 정적 멤버로 선언하려면 선언문 앞에 static 이라는 지정자를 붙이면 된다. 모든 멤버
들이 static으로 선언될 수 있으며 정적 멤버들도 접근 지정자 (public, protected, private)을 붙
일 수 있다.

A.10.1 정적 멤버 변수

앞에서 들었던 LED 클래스의 예를 가지고 정적 멤버 변수를 추가시켜 보면 다음과 같다.

class Led {
public:
Led();
void turnOn();
void turnOff();
static int iCount; // 정적 멤버 변수
private:
byte _pin;
};

이 예제에서 iCount는 정적 멤버 변수로 선언되었다. 정적 멤버 변수를 초기화하기 위해서


부 록 - 167

는 클래스 내부에서는 불가능하다. 다음과 같이 초기화 하려고 하면 에러를 발생한다.

class Led {
public:
Led();
void turnOn();
void turnOff();
static int iCount = 0; // 오류 발생
private:
byte _pin;
};

따라서 클래스 외부에서 전역 변수처럼 초기화시켜야 한다.

int Led::iCount = 0;

정적 변수를 접근하는 방법은 두 가지가 있는데 먼저 클래스 이름으로 접근하는 것이다.


이때 범위 지정 연산자인 ::을 이용한다.

Led::iCount ++; // iCount값을 하나 증가시킨다.

또는 인스턴스를 이용해서도 접근할 수 있다.

Led led1(12), led2(11);


led1.iCount = 10;
led2.iCount = 30;

비록 인스턴스 멤버 변수를 접근하는 문법과 똑같지만 서로 다른 인스턴스를 통해서 정적


변수를 참조했다고 할지라도 결국 같은 하나의 정적 변수 iCount를 접근하는 것이다.

정적 변수의 첫 번째 활용 목적은 인스턴스들 사이에서 공유할 변수를 만들고자 하는 것이


다. 예를 들어서 인스턴스가 몇 개나 생성되었는지를 기록하고자 할 때 앞의 예에서 iCount
를 사용하면 될 것이다. 이 경우 생성자 내에서 iCount 값을 증가시키면 새로운 인스턴스가
생성될 때마다 이 변수값이 증가되므로 이것으로 개수를 알 수 있다.

int Led::iCount = 0; // 전역에서 정적 멤버 변수 초기화

Led::Led(int pin) { //생성자


_pin = pin;
pinMode(_pin, OUTPUT);
iCount++; // 인스턴스 생성 시 하나 증가
}
168 - 부 록

Led::~Led(int pin) { //소멸자


iCount--; // 인스턴스 소멸 시 하나 감소
}

또한 전역 변수를 쓰는 대신 관련된 클래스 내부에 정적 멤버 변수를 집어넣어서 C언어


의 전역 변수처럼 사용할 수도 있다.

A.10.2 정적 멤버 함수

멤버 함수 앞에 static 이라는 키워드를 붙이면 정적 멤버 함수가 된다. 정적 변수와 마찬


가지로 정적 멤버 함수는 인스턴스에 속하는 멤버 함수가 아니라 클래스에 속하는 하나의
함수이다. 정적 함수 내부에서는 오직 정적 변수만을 사용할 수 있으며 함수도 정적 함수만
을 호출할 수 있다. 반대로 일반 멤버 함수에서는 정적 멤버를 접근하는데 전혀 제약이 없
다. 다음 예를 보자.

class Util {
public:
int iA;
static double dA;
void getA() {
iA += (int)dA; // 정적과 비정적 변수를 모두 사용가능
return iA;
}
static void getB() {
return dA; //정적 변수만 사용 가능
}
};

비정적 함수 getA() 내부에서는 변수 iA와 정적 변수 dA를 사용하는데 아무런 제약이 없으


나 정적함수 getB() 에서는 정적 변수 dA만 사용할 수 있다. 마찬가지로 정적 함수 내에서
는 정적인 함수만 호출할 수 있으며 일반적인 멤버 함수는 호출할 수 없다.
부 록 - 169

부록 B. 아두이노 표준함수 레퍼런스

B.1 아두이노 프로그램의 구조

1. setup() 함수

• 아두이노 프로그램이 실행될 때 맨 처음에 단 한 번 호출되어 수행된다.


• 여기에는 각종 장치를 초기화하거나 초기값을 설정하는 코드가 오게 된다.

2. loop() 함수

• setup()함수 실행 후 수행되면 계속 반복 수행된다.


• 아두이노에 연결된 장치들을 구동시키는 코드가 위치한다.

B.2 디지털 입출력

1. pinMode(pin, INPUT/OUTPUT) 함수

• pin을 입력(INPUT) 혹은 출력(OUTPUT)으로 사용할 지를 지정한다.


• pin은 우노의 경우 0,1,2, … 13, A0, A1, ...A6 중 하나이다.

2. digitalWrite(pin, HIGH/LOW) 함수

• pin핀이 출력으로 설정된 경우에 사용한다.


• 1값을 내보낼지 (HIGH) 0값을 내보낼지(LOW)를 지정한다.
• pin은 우노의 경우 0,1,2, … 13, A0, A1, ...A6 중 하나이다.

3. digitalRead(pin) 함수

• pin핀이 입력으로 설정된 경우에 사용한다.


• pin 의 디지털 값을 읽어들인다.
• pin은 우노의 경우 0,1,2, … 13, A0, A1, ...A6 중 하나이다.

B.3 아날로그 입출력

1. analogWrite(pin, value)
170 - 부 록

• PWM 출력을 내보낸다.


• pin : 3, 5, 6, 9, 10, 11 중 하나 (아두이노 우노의 경우)
• value : 0에서 255 사이의 정수.

2. analogRead(pin)

• 센서의 입력을 받는 함수.


• pin : A0, A1, A2, A3, A4, A5 (우노의 경우)
• 출력값은 0~1023 사이의 값이다.

3. ananlogReference(type)

• 아날로그 입력값이 1023로 읽히는 실제 최대 전압 값을 설정해 주는 함수.


• 우노의 경우 따로 설정해 주지 않으면 5V이지만 이 함수를 이용하여 다른 값을 설정해
줄 수 있다.
• type의 종류 :
∙ DEFAULT : 아두이노의 동작 전압(우노는 5V 이고 보드에 따라서 3.3V일 수
있다.)
∙ INTERNAL : 내장 전압 (우노는 1.1V)
∙ EXTERNAL : AREF핀에 인가된 전압 (0~ 5V 사이어야 됨)
∙ INTERNAL1V1 : 내장된 1.1V (Arduino Mega에서만 사용된다.)
∙ INTERNAL2V56 : 내장 2.56V (Arduino Mega 에서만 사용됨)
• 아두이노 우노와 관련된 옵션은 DAFAULT, INTERNAL, EXTERNAL 세 개 이며 별도
로 설정하지 않는다면 기본적으로 DEFAULT가 사용된다.
• 만약 INTERNAL로 설정하면 기준 전압이 1.1V이므로 기본 모드보다 더 높은 분해능
(0.0011V)을 얻을 수 있으며 약 4.5배(=5/1.1)의 증폭효과가 있다.
• 만약 3.3V를 기준 전압으로 사용하고 싶다면 우노 보드상의 3.3V핀과 AREF 핀을 결선
한 후 EXTERNAL 옵션을 설정하면 된다.

B.4 고급 입출력 함수들

1. tone(pin, freq [, duriation] )

• 듀티비가 50%인 구형파를 발생.


• pin : 구형파를 발생시킬 핀.
• freq : 구형파의 주파수 (Hz) - unsinged int
부 록 - 171

• duration (옵션) : 구형파의 주파수 (Hz) - unsinged int. 이 값이 주어지지 않으면 구


형파를 noTone()함수가 호출될 때까지 연속적으로 생성함.

2. noTone(pin)

• pin 에 발생시키던 구형파를 멈춤.

3. shiftOut(dataPin, clockPin, bitOrder, value)

• 바이트데이터 하나씩을 핀으로 내보내는 함수.


• clockPin의 폴링 에지가 dataPin에 데이터가 올라왔다는 신호임.
• MSB 부터인지 LSB부터인지를 지정할 수 있음(bitOrder = MSBFIRST / LSBFIRST)
• dataPin : 신호를 내보낼 핀.
• clockPin : dataPin에 한 비트가 정확히 세트될 때마다 토글되는 핀.
• value : 내보낼 바이트 데이터.

4. byte in = shiftIn(dataPin, clockPin, bitOrder)

• 바이트데이터 하나씩을 핀으로 읽어들이는 함수.


• clockPin를 HIGH로 올리고 데이터를 읽은 다음 LOW로 내린다.
• MSB 부터인지 LSB부터인지를 지정할 수 있음(bitOrder = MSBFIRST / LSBFIRST)
• dataPin : 신호를 읽어들일 핀.
• clockPin : dataPin에 한 비트가 정확히 세트될 때마다 토글되는 핀.

5. pulseIn(pin, value [,timeout] )

• 펄스하나를 읽어서 그 시간(마이크로세컨드 단위)을 반환한다.


• pin : 펄스가 입력되는 핀
• value : 펄스의 타입(HIGH/LOW)
• timeout: 타임 아웃 지정(마이크로세컨드) 디폴트값은 1초

B.5 시간 관련 함수

1. millis()

• 프로그램이 수행되기 시작한 시점부터의 시간을 반환함.


• 반환값은 밀리세컨드 단위임.
• 약 50일 이후에 오버플로(다시 0값으로 리셋되는 것) 발생.
172 - 부 록

2. micros

• 프로그램이 수행되기 시작한 시점부터의 시간을 반환함.


• 반환값은 마이크로세컨드 단위임.
• 약 70분 이후에 오버플로(다시 0값으로 리셋되는 것) 발생.

3. delay(time)

• 지연 수행 (아무런 일도 안 하고 멈춰 있음)
• time 은 밀리세컨드 단위로 입력 (예: 500은 0.5초)

4. delayMicroseconds(time)

• 지연 수행 (아무런 일도 안 하고 멈춰 있음)
• time 은 마이크로 세컨드 단위로 입력 (예: 500은 0.0005초)
• 입력의 최대값은 16383임.

B.6 난수

1. randomSeed(seed)

• 난수 발생기를 초기화함.
• 아두이노에서는 아날로그 입력값을 이용하여 초기화.
• 전형적인 사용 예: randomSeed(analogRead(A0))

2. random(max), random(min,max)

• (min, max-1) 사이의 임의의 숫자를 반환함.


• min값이 지정되지 않은 경우 0부터 max-1 사이의 난수 발생.
아두이노를 이용한 디지털 시스템 프로그래밍 실습

발행인 박 장 현
발행일 2015년 2월 27일
발행처 국립목포대학교
전라남도 무안군 청계면 영산로 1666
인 쇄 중앙기획(Tel. 061-276-0276)

본 출판물의 저작권은 국립목포대학교에 있습니다.


무단 전재와 무단 복제를 금합니다.

You might also like