가장 기본이라 할 수 있는 특정 핀의 시그널을 컨트롤하는 방법에 대해 기술한다.
흔히 GPIO 라 불리는 인터페이스는 하드웨어 통신의 가장 기본적인 방법이다. 이를 응용한 것이 I2C 나 UART 등이다.

무작정 따라하기

핀에 흘리는 전류를 High(1) 이나 Low(0) 로 조절하여 이를 LED 로 확인해보자. 먼저아래의 코드를 작성하여 컴파일하여 아두이노에 올려보자.

const int LED = 13;          ---- 1
 
void setup()                   ---- 2
{
  pinMode(LED, OUTPUT);     ---- 3
}
 
void loop()                      ---- 4
{
  digitalWrite(LED, HIGH);               ---- 5
  delay(1000);                              ---- 6
  digitalWrite(LED, LOW);              ---- 7
  delay(1000);
}

특정 LED 하나가 깜박이는 걸 볼 수 있다.

이제 코드를 보자. 아두이노는 H/W 를 접해보지 않은 사람도 알 수 있도록 아주 직관적이다.

  1. LED 라는 상수 변수가 미리 정의되어 있다. 이것은 GPIO 핀을 가리키며, 13 을 입력한 것은 13번째 GPIO 핀을 지정했다. 위치는 아두이노 PCB 기판에 나와 있기 때문에 이해가 쉽다(회로도 참고할 것)
  2. setup 함수는 아두이노 프로그래밍 코드라면 반드시 선언되어야 한다. 이는 말그대로 초기화 함수 인데, loop 함수가 실행되기 전에 호출된다.
  3. GPIO 핀의 성격을 정의한다. 앞서 지정한 13번 핀을 출력 핀으로 지정한다.
  4. C 프로그램의 main() 함수 역할로서, 반드시 지정되어야 한다. 무한 루프로 실행된다.
  5. 앞서 지정한 핀의 출력을 High(1) 로 내보낸다.
  6. 1000 ms(= 1초) 지연시킨다.
  7. 핀의 출력을 Low(0) 로 내보낸다.

스위치를 누르고 있는 동안 LED 켜기

아래 그림처럼 회로를 꾸민다. 저항은 10k 옴을 사용한다.

그리고 코드를 작성한다.

const int LED = 13;
const int BUTTON = 7;
 
int val = 0;
 
void setup()
{
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);       ---- 1
}
 
void loop()
{
  val = digitalRead(BUTTON);    ---- 2
 
  if(val == HIGH)
  {
    digitalWrite(LED, HIGH);
  }
  else
  {
    digitalWrite(LED, LOW);
  }
}

코드를 보자.

  1. 7번 핀을 입력 받도록 설정했다.
  2. 7번 핀으로 입력값을 읽어들인다. High 라면 1, Low 라면 0 이 반환된다.

스위치를 누를 때마다 LED 켜고 끄고 하기

한번 누르면 켜지고, 다시 한번 누르면 꺼지도록 코드를 수정해보자.

const int LED = 13;
const int BUTTON = 7;
 
int val = 0;
int old_val = 0;        ---- 1
int state = 0;           ---- 2
 
void setup()
{
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);      
}
 
void loop()
{
  val = digitalRead(BUTTON);   
 
  if((val == HIGH) && (old_val == LOW))    ---- 3
  {
    state = 1 - state;
  }
 
  old_val = val;            ---- 4
 
  if(state == HIGH)
  {
    digitalWrite(LED, HIGH);
  }
  else
  {
    digitalWrite(LED, LOW);
  }
}

전보다 상태값을 저장하는 변수와 루틴이 추가되었다. 스위치를 누를 때(입력 신호가 LOW → HIGH 로 바뀌는)를 인식하기 위해서이다.

  1. 현재 상태보다 이전 상태을 저장하는 변수 선언
  2. 현재 LED 의 켜고꺼짐 상태를 저장하는 변수 선언,
  3. 스위치를 눌렀을 때의 상태를 인식하기 위한 조건문
  4. 현재 상태값을 예전 상태값에 저장한다

위의 코드가 완벽하지 않을 수 있는데, 스위치 기계 구조에서 오는 문제점 때문이다. 푸시버튼은 아주 간단한 장치로 두 금속조각이 스프링에 의해 떨어진 상태다. 버튼을 누를 때, 두 조각이 서로 연결되어 전기가 흐를 수 있다. 버튼을 완전히 누르지 않았을 때는 바운싱 이라는 가짜신호가 나타난다.
푸시버튼이 바운싱할 때 아두이노는 켜지고 꺼지는 신호가 아주 빠르게 바뀌는 것처럼 인식한다. 바운싱을 제거하는 기술적인 방법이 많이 있지만, 코드에서는 대개 변경을 감지한 후에 10~50 밀리초 정도 대기하는 명령을 추가하는 정도면 충분하다.

...
 
  if((val == HIGH) && (old_val == LOW))    
  {
    state = 1 - state;       
    delay(10);             ---- 1
  }  
...

위와 같이 지연하는 코드를 추가했다.

LED 밝기 조절하기

아래와 같이 회로를 꾸며보자. 저항은 270 옴을 사용한다.

먼저 밝기의 의미에 대해서 알아보자. 밝기는 H/W 적으로 어떻게 조절할까? 전기 신호는 High 와 Low 뿐인데, 이걸 가지고 어떻게?
단순하게 High 와 Low 의 중간 레벨의 신호를 흘려보내면 밝기가 어중간하지 않을까?

결론부터 말하면, 틀렸다. 모든 전자회로는 High 와 Low 에서만 정확하게 동작한다. 정말 심플하지 않은가?
밝기조절은 LED 를 껐다가 켜는 것으로 가능하다. 시간당 빠르게 또는 느리게 이를 반복하는 것으로 우리의 눈은 마치 밝기가 조절되는 것으로 보이기 때문이다.

디지털 신호와 달리 아날로그 신호는 High → Low → High 를 반복하는 파형을 그린다. 바로 아날로그 출력을 사용해서 밝기 조절을 할 수 있다.
이제 코드를 보자.

const int LED = 9;     ---- 1
int i = 0;
 
void setup()
{
  pinMode(LED, OUTPUT);
}
 
void loop()
{
  for(i=0; i<255; i++)
  {
    analogWrite(LED, i);     ---- 2
 
    delay(10);
  }
 
  for(i=255; i>0; i--)
  {
    analogWrite(LED, i);
    delay(10);
  }
}

새로운 함수가 나왔다. 앞서 나온 digitalWrite 함수와 짝을 이루는 아날로그 신호를 출력하는 함수다.

  1. 아날로그 신호를 출력할 수 있는 PWM(펄스 폭 변조) 기능을 사용할 수 있도록 설계된 핀이 9,10,11 번이다.
  2. 두번째 인자의 값은 0 ~ 255 숫자를 사용할 수 있다. 높을 수록 많은 변조 가해 출력한다. 0은 완전히 꺼진 상태이고, 255는 완전히 켜진 상태다.

스위치를 누를 때, 밝기 조절하기

앞서 만들었던 두개의 회로를 모두 연결해보자.

const int LED = 9;
const int BUTTON = 7;
 
int val = 0;
 
int old_val = 0;
int state = 0;
 
int brightness = 128;
unsigned long starttime = 0;     ---- 1
 
void setup()
{
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);
}
 
void loop()
{
  val = digitalRead(BUTTON);
 
  if((val == HIGH) && (old_val == LOW))        
  {
    state = 1 - state;
 
    starttime = millis();                 ---- 2
 
    delay(10);
  }
 
  if((val == HIGH) && (old_val == HIGH))
  {
    if(state == 1 && (millis() - starttime) > 500)         ---- 3
    {
      brightness++;
      delay(10);
 
      if(brightness > 255)                     ---- 4
      {
        brightness = 0;
      }
    }
  }
 
  old_val = val;
 
  if(state == 1)
  {  
    analogWrite(LED, brightness);
  }else
  {
    analogWrite(LED, 0);
  }
}

스위치를 누르고 있는 동안, 밝기가 변해야 하기 때문에 시간을 재는 함수가 추가되었다.

  1. 스위치를 누른 시간을 저장하는 변수 선언
  2. 현재 시간 반환하는 함수(millis) 를 호출하여 저장한다.
  3. 500 ms 이상 스위치를 눌렀는지 조건을 확인한다.
  4. 오랫동안 눌렀을 경우, 다시 0으로 초기화 한다.

가변저항을 사용하여 밝기 조절하기

말그대로 저항값이 바뀌는 저항이다. 마치 라디오 볼륨처럼 다이얼을 돌리면 저항값이 바뀐다.
이번에는 다이얼을 돌리는 것에 따라 LED 밝기를 조절해보겠다.

회로를 만들 때, 가변 저항은 아래 그림처럼 브레드보드에 꼽는다.

const int se = 0;
 
const int led = 9;
 
void setup()
{
  Serial.begin(9600);
  pinMode(led, OUTPUT);
}
 
void loop()
{
  int val = analogRead(se);
 
  Serial.println(val);
 
  int inten = map(val, 0, 1023, 0, 255);  // 가변 저항기의 값을 기초로 LED 의 밝기를 구한다. 0~1023 의 입력을 0~255 범위로 스케일링
 
  analogWrite(led, inten);
 
  delay(100);
}

아두이노 보드는 아날로그 입력을 0 에서 1023 까지 1024 단계로 표현한다. 이에 비해 analogWrite 는 0 에서 255 까지 256 단계가 있기 때문에 map 이라는 메서드를 사용해 범위를 맞추고 있다.

7 세그먼트 사용하기

0 부터 9 까지의 숫자를 나타낼 수 있는 7 Segment 를 제어해보자. 세븐 세그먼트는 크게 2가지로 나뉜다. Anode(에노드) / Cathode(캐노드) 형에 따라 회로가 바뀐다.
3번과 8번 핀을 각각 에노드는 전원(Vcc)에 연결하고 캐노드는 그라운드(Gnd)에 연결한다. 내가 가진 것은 캐노드 형으로 그라운드에 연결했다.



Arduino Pin 7 Segment Pin Connection
2 7 (A)
3 6 (B)
4 4 (C)
5 2 (D)
6 1 (E)
7 9 (F)
8 10 (G)
9 5 (DP)
byte seven_seg_digits[10][7] = { { 1,1,1,1,1,1,0 },  // = 0
                                                           { 0,1,1,0,0,0,0 },  // = 1
                                                           { 1,1,0,1,1,0,1 },  // = 2
                                                           { 1,1,1,1,0,0,1 },  // = 3
                                                           { 0,1,1,0,0,1,1 },  // = 4
                                                           { 1,0,1,1,0,1,1 },  // = 5
                                                           { 1,0,1,1,1,1,1 },  // = 6
                                                           { 1,1,1,0,0,0,0 },  // = 7
                                                           { 1,1,1,1,1,1,1 },  // = 8
                                                           { 1,1,1,0,0,1,1 }   // = 9
                                                           };
 
void setup() {               
  pinMode(2, OUTPUT);  
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  writeDot(0);  // start with the "dot" off   .(점) 을 끈다
}
 
void writeDot(byte dot) {
  digitalWrite(9, dot);
}
 
void sevenSegWrite(byte digit) {
  byte pin = 2;
  for (byte segCount = 0; segCount < 7; ++segCount) {
    digitalWrite(pin, seven_seg_digits[digit][segCount]);
    ++pin;
  }
}
 
void loop() {
  for (byte count = 10; count > 0; --count) {
   delay(1000);
   sevenSegWrite(count - 1);
  }
  delay(4000);
}

숫자가 작아지면서 9 ~ 0 순으로 출력된다.

도트 매트릭스 사용하기

도트 매트릭스 역시 여러가지 종류가 있다. 여기서는 5(가로) * 7(세로) 크기의 LED 를 사용한다. 아래 데이터시트를 참고한다.

아두이노와 연결하기 위해서는 총 12개의 핀이 필요한데, D2 ~ D13 을 사용할 것이다. 핀 매핑은 다음과 같다.

Arduino Pin Dot Matrix Pin Col/Row
D2 12 Row 1
D3 1 Col 1
D4 11 Row 2
D5 3 Col 2
D6 2 Row 3
D7 10 Col 3
D8 9 Row 4
D9 7 Col 4
D10 4 Row 5
D11 8 Col 5
D12 5 Row 6
D13 6 Row 7

도트 매스릭스 제어를 위해서는 영화처럼 1초당 30회 이상 화면을 출력하여야 한다. 이런 작업을 위해서 타이머를 이용해야 하는데, MsTimer2 라이브러리가 필요하다.

#include <MsTimer2.h>     // 타이머2 를 이용한 잔상효과 전광판을 만들기 위해 타이머2 라이브러리를 추가한다
 
 
// 도트 메트릭스에 출력할 폰트를 설계한다. 1 은 LED ON, 0 은 LED OFF 로 표시
#define SPACE { \
{0,0,0,0,0,0,0}, \
{0,0,0,0,0,0,0}, \
{0,0,0,0,0,0,0}, \
{0,0,0,0,0,0,0}, \
{0,0,0,0,0,0,0} \
}
 
#define I { \
{0,0,1,1,1,0,0}, \
{0,0,0,1,0,0,0}, \
{0,0,0,1,0,0,0}, \
{0,0,0,1,0,0,0}, \
{0,0,1,1,1,0,0} \
}
 
#define Heart { \
{0,0,1,0,1,0,0}, \
{0,1,1,1,1,1,0}, \
{0,0,1,1,1,0,0}, \
{0,0,1,1,1,0,0}, \
{0,0,0,1,0,0,0} \
}
 
#define Y { \
{0,0,1,0,1,0,0}, \
{0,0,1,0,1,0,0}, \
{0,0,0,1,0,0,0}, \
{0,0,0,1,0,0,0}, \
{0,0,0,1,0,0,0} \
}
 
#define O { \
{0,0,0,1,0,0,0}, \
{0,0,1,0,1,0,0}, \
{0,0,1,0,1,0,0}, \
{0,0,1,0,1,0,0}, \
{0,0,0,1,0,0,0} \
}
 
#define U { \
{0,1,0,0,0,1,0}, \
{0,1,0,0,0,1,0}, \
{0,1,0,0,0,1,0}, \
{0,0,1,0,1,0,0}, \
{0,0,0,1,0,0,0} \
}
 
//폰트를 출력할 변수 선언
byte col = 0;
int row = 0;
byte leds[5][7];   // 5 * 7 LED, 5 * 7 폰트 데이터를 저장할 2차 배열 변수를 선언
int font = 0;
int i,j,k;
 
int pins[13] = {-1,2,3,4,5,6,7,8,9,10,11,12,13};  // 메트릭스에 연결될 아두이노의 핀을 어레이 변수에 저장, -1 은 dummy 데이터 pin[0] 은 -1 이며, pin[1] 은 2 이다
 
int cols[5] = {pins[2], pins[4], pins[6], pins[8], pins[10]};  // 매트릭스 세로 줄의 핀번호를 정의,
 
int rows[7] = {pins[1], pins[3], pins[5], pins[7], pins[9], pins[11], pins[13]};    // 매트릭스 가로 줄의 핀번호를 정의
 
//출력 폰트 수
const int numFonts = 6;                 // 출력할 폰트의 수만큼 3차원 배열 폰트를 정의, 출력할 문자는 'I 하트 YOU' 와 빈칸을 포함해서 6 문자이다
byte fonts[numFonts][5][7] = {
  I, Heart, Y, O, U, SPACE
};
 
//폰트 준비 사용자 함수
void setFont(int font)  // 출력할 폰트를 준비하는 사용자 함수, leds[i][j] = fonts[font][i][j]; 에서 7 개의 폰트 중에서 출력할 폰트를 led[i][j] 배열 변수에 저장한다. 이 변수는 dotDisplay() 사용자 함수에서 도트 매트릭스 LED 에 출력된다
{
  for(int i = 0; i < 5; i++)
  {
    for(int j = 0; j < 7; j++)
    {
      leds[i][j] = fonts[font][i][j];
    }
  }
}
 
//흐르는 폰트를 만드는 사용자 함수
void slideFont(int font, int del)   // 현재의 1에서 6까지 Row 데이터를 leds[j][i] = leds[j][i+1]; 하여 1칸씩 앞으로 당기고, 7번째 Row 데이터는 leds[j][6] = fonts[font][j][0 + k]; 와 같이 지정한 폰트의 데이터를 leds 변수에 저장하여 
{                                           // 지연시간 delay(del) 시간 동안 매트릭스의 LED 에 출력시킨다. 7개의 Row 데이터를 차례대로 앞으로 흐르도록 한다.
  for(k = 0; k < 7; k++)
  {
    for(i = 0; i < 6; i++)
    {
      for(j = 0; j < 5; j++)
      {
        leds[j][i] = leds[j][i+1];
      }
    }
 
    for(j = 0; j < 5; j++)
    {
      leds[j][6] = fonts[font][j][0 + k];
    }
    delay(del);
  }
}
 
//인터럽트 서비스 루틴
void dotDisplay()     // leds[][] 2차원 어레이 변수에 저장되어 있는 폰트를 도트매트릭스 LED 로 출력하는 사용자 함수이다. 먼저 7개의 Row 데이터를 출력하고 한 column 을 표시한다. 
{                           // 이와 같은 동작을 5회하여 한 화면을 출력한다. 이 사용자 함수는 MsTimer2::set(2, dotDisplay); 에 의해 2ms 마다 호출되어 초당 30 프레임 이상의 화면이 출력된다.
 
  // 1줄(column) 데이터 출력을 위한 준비
  digitalWrite(cols[col], LOW);   // 이전 col OFF
  col++;
 
  if(col == 5)
  {
    col = 0;
  }
 
  // 7개의 row 데이터를 준비시킴
  for(row = 0; row < 7; row++)
  {
    if(leds[col][6-row] == 1)
    {
      digitalWrite(rows[row], LOW);
    }
    else
    {
      digitalWrite(rows[row], HIGH);
    }
  }
  // 매트릭스 1줄(column) 출력
  digitalWrite(cols[col], HIGH);
}
 
void setup()
{   // 사용하는 모든 핀을 출력으로 설정
  for(int i = 1; i <= 12; i++)
  {
    pinMode(pins[i], OUTPUT);
  }
 
  MsTimer2::set(2, dotDisplay);   // 타이머 인터럽트 핸들러 등록, 인터럽트가 2ms 마다 발생하면 영화와 같은 잔상효과를 연출할 수 있다. 그리고 타이머2를 작동(시작)시킨다
  MsTimer2::start();                   // 화면이 껌뻑이면 2ms 보다 작은 값으로 변경하여 사용한다
 
  setFont(font);
}
 
void loop()
{
  // 출력할 폰트 계산
  font = ++font % numFonts;     // font++ 한 값을 numFonts 로 나눈 나머지를 font 값으로 계산한다. 값은 0~6 이 된다
 
  // 다음 폰트를 준비 시킨다. 60ms 마다
  slideFont(font, 60);         // 흘러가는 폰트 만드는 함수를 호출한다. 출력할 폰트와 흐르는 시간 간격(ms)을 전달인수로 사용한다. 60보다 큰 수를 사용하면 문자의 흐르는 속도가 느려진다.
}
  • computer/embedded/아두이노_그대로_따라하기_-_2.led_켜고_끄기.txt
  • Last modified: 3 years ago
  • by likewind