본문 바로가기

개발자/Arduino

아두이노에서 멀티태스킹 구현하기 3 - Multi-tasking the arduino : Blink without delay




아두이노에서 멀티태스킹 구현하기 3 - Multi-tasking the arduino


여기서는 앞서 배운 기술을 토대로 몇 가지 유형의 Arduino 인터럽트를 살펴보고, Arduino 인터럽트를 사용하여 코드를 간단하고 신속하게 처리하면서 Arduino의 더 많은 작업을 실행하는 방법을 배운다.


타이머 인터럽트를 활용하여 모든 일이 시계처럼 작동하도록하는 방법을 배운다. 외부 이벤트에 대한 알림을 제공하기 위해 외부 인터럽트를 사용하는 방법 또한 배운다. 여기서 나오는 용어들 인터럽트, 타이머 인터럽트, 외부 인터럽트들에 대한 간단한 설명을 하면 다음과 같다.


타이머를 알기 위해서는 먼저 인터럽트라는 개념을 조금 알아둘 필요가 있다. 인터럽트는 중요한 일이 발생하면 하던 일을 잠시 멈추고 중요한 일을 먼저하는 것을 말한다. 인터럽트가 발생했다는 말은 지금 하던 일보다 아주 중요한 일이 발생해서 하던 일을 잠시 중단하고 그 일(인터럽트를 건 일)을 해야만 하게 되었다는 뜻이다.


마이크로프로세서에서 인터럽트(interrupt, 문화어: 중단, 새치기)란 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 또는 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다.


타이머 인터럽트는 마이크로 프로세서 내부에서 일정한 주기로 발생되는 시간에 인터럽트를 발생시켜 중요한 일들을 처리하는 인터럽트를 말한다. 


외부 인터럽트란 외부의 입 출력 장치나 외부 디바이스에서 발생된 중요하고 급한 신호를 감지하여 처리해 주는 방법의 일종이다.


설명에 사용된 이미지와 소스코드의 출처 https://learn.adafruit.com/multi-tasking-the-arduino-part-2/


여기에서 나오는 모든 내용은 아래 회로 연결도를 가지고 설명한다.





인턴럽트 개념을 다시 한번 설명하자.


인터럽트란 무엇인가?


인터럽트는 프로세서가 수행중인 작업을 즉시 중단하고 우선 순위가 높은 일부 처리를 처리하도록 알리는 신호다. 우선 순위가 높은 우선순위를 먼처 처리하는 것을 인터럽트 처리기라고 한다.

인터럽트 처리기는 다른 void 함수와 비슷하다. 우선 인터럽드 하나를 사용하기로 하고 할당을 하게 되면 인터럽트 신호가 트리거 될 때마다 할당된 함수가 호출된다. 인터럽트 처리를 마치고 처리기에서 돌아오면 이전에 실행중이던 작업을 계속 진행한다.


인터럽트들은 어디서 발생이 되나?


여러 소스에서 인터럽트를 생성 할 수 있다. 타이머는 Arduino 에 있는 여러 타이머 중 하나에서 인터럽트를 발생시킨다. 외부 인터럽트 핀 중 하나의 상태 변화로 인한 인터럽트가 발생할 수 있다. 여러 핀 그룹 중 하나의 상태 변화로 인한 핀 변경 인터럽트도 발생한다. 


인터럽트를 사용함으로 얻는 장점은 무엇인가?


인터럽트를 사용하면 우선 순위가 높은 인터럽트 조건을 계속 확인하기 위해 루프 코드를 작성할 필요가 없다. 응답이 느리거나 장시간 CPU 를 소유하며 실행되는 프로그램 코드들 때문에 급하고 중요하게 처리될 일을 잊는 일은 없다. 즉, 프로세서는 인터럽트가 발생할 때 자동으로 중지하고 인터럽트 처리기를 호출한다. 따라서 우리는 인터럽트가 일어날 때 실행되는 인터럽트에 응답하는 코드를 작성하기만 하면 된다.


타이머 인터럽드


인터럽트를 사용한다는 의미는 바로 "전화해서 확인하지 말고 전화오면 바로 확인한다."


이 연재의 1 부에서는 타이밍을 위해 millis ()를 사용하는 방법을 배웠다. 하지만 그 일을하기 위해서 매번 루프를 통해 millis ()를 호출하여 뭔가 할 시간인지 확인했다.  millis ()를 1 밀리 초 이상 호출하면 시간이 변하지 않았음을 알게되는 것은 낭비이다. 그렇다고 1밀리 세컨드 당 한 번만 확인하면 좋을것인가? 차라리  계속 확인하는 코드를 사용하지 않는 것이  좋을 것이다.

  

타이머와 타이머 인터럽트를 통해 우리는 이런 방식을 정확하게 처리 할 수 ​​있다. 밀리 세컨드 당 한 번 우리에게 알려주는 타이머를 설정할 수 있다. 타이머가 실제로 시계를 확인할 때가 되었음을 알려주도록 우리에게 전화 할 것이다.


Arduino 타이머


Arduino Uno에는 마이크로 컨트롤러 내부에 Timer0, Timer1 및 Timer2의 3 개의 타이머가 있다. Timer0은 이미 millis ()에 의해보고 된 밀리 초 카운터를 업데이트하기 위해 밀리 초 인터럽트를 생성하도록 설정된다.  Timer 0 는 인터럽트를 생성하도록 할 것이다.


빈도 및 개수


타이머는 16MHz 시스템 클록에서 파생 된 일부 주파수에서 계산되는 간단한 카운터이다. 주파수 및 다양한 계수 모드를 변경하기 위해 클럭 발생 수를 구성 할 수 있다. 타이머가 특정 카운트에 도달하면 인터럽트를 생성하도록 구성 할 수 있다는 말이다.

Timer0은 0에서 255까지 카운트하고 255가 넘어갈 때마다 인터럽트를 생성하는 8 비트 카운터가 된다. 그것은 우리에게 976.5625 Hz의 인터럽트 속도를 제공하기 위해 기본적으로 64의 클록 제수를 사용한다 (우리의 목적을 위해 1KHz에 충분히 가깝다). 우리는 millis () 를 정확히 지켜야 하기 때문에 Timer0의 freqency를 일정한 수준으로 지켜야 한다. 


비교 레지스터


Arduino 타이머에는 여러 구성 레지스터가 있습니다. 이것들은 Arduino IDE에 정의 된 특수 기호를 사용하여 읽거나 쓸 수 있습니다. 이 모든 레지스터와 기능에 대한 자세한 설명은 아래의 "추가 정보"링크를 참조하십시오.


타이머 0에 대한 비교 레지스터 (이 레지스터는 OCR0A라고 함)를 설정하여 그 카운트 중간에 다른 인터럽트를 생성합니다. 모든 틱에서 타이머 카운터는 비교 레지스터와 비교되며, 같으면 인터럽트가 생성됩니다.

아래의 코드는 카운터 값이 0xAF를 통과 할 때마다 'TIMER0_COMPA'인터럽트를 생성합니다.



1
2
3
4
  // Timer0 is already used for millis() - we'll just interrupt somewhere
  // in the middle and call the "Compare A" function below
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
cs


그런 다음 "TIMER0_COMPA_vect"로 알려진 타이머 인터럽트 벡터에 대한 인터럽트 처리기를 정의합니다. 이 인터럽트 처리기에서는 루프에서 수행했던 모든 작업을 수행합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Interrupt is called once a millisecond, 
SIGNAL(TIMER0_COMPA_vect) 
{
  unsigned long currentMillis = millis();
  sweeper1.Update(currentMillis);
  
  //if(digitalRead(2) == HIGH)
  {
     sweeper2.Update(currentMillis);
     led1.Update(currentMillis);
  }
  
  led2.Update(currentMillis);
  led3.Update(currentMillis);
}
cs


그러면 우리에게 완전히 빈 루프가 생깁니다.


1
2
3
void loop()
{
}
cs



이제 루프에서 원하는 모든 작업을 수행 할 수 있습니다. 구식일 수 도 있고 delay()을 사용할 수도 있다. 자동 점멸 장치와 청소부는 신경 쓰지 않을 것입니다. 그들은 여전히 밀리 세컨드에 관계없이 한번 불려질 것이다.

추가 읽기 :


이것은 타이머가 수행 할 수있는 간단한 예제 일뿐입니다. 여러 유형의 타이머 및 구성 방법에 대한 자세한 내용은 "라이브러리 및 링크"페이지 http://fishpoint.tistory.com/2160 를 참조하십시오.


다음은 flashers 및 스위퍼를 포함한 전체 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <Servo.h> 
 
class Flasher
{
    // Class Member Variables
    // These are initialized at startup
    int ledPin;      // the number of the LED pin
    long OnTime;     // milliseconds of on-time
    long OffTime;    // milliseconds of off-time
 
    // These maintain the current state
    int ledState;                     // ledState used to set the LED
    unsigned long previousMillis;      // will store last time LED was updated
 
  // Constructor - creates a Flasher 
  // and initializes the member variables and state
  public:
  Flasher(int pin, long on, long off)
  {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);     
      
    OnTime = on;
    OffTime = off;
    
    ledState = LOW; 
    previousMillis = 0;
  }
 
  void Update(unsigned long currentMillis)
  {
    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
    {
        ledState = LOW;  // Turn it off
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
    {
      ledState = HIGH;  // turn it on
      previousMillis = currentMillis;   // Remember the time
      digitalWrite(ledPin, ledState);      // Update the actual LED
    }
  }
};
 
class Sweeper
{
  Servo servo;              // the servo
  int pos;              // current servo position 
  int increment;        // increment to move for each interval
  int  updateInterval;      // interval between updates
  unsigned long lastUpdate; // last update of position
 
public
  Sweeper(int interval)
  {
    updateInterval = interval;
    increment = 1;
  }
  
  void Attach(int pin)
  {
    servo.attach(pin);
  }
  
  void Detach()
  {
    servo.detach();
  }
  
  void Update(unsigned long currentMillis)
  {
    if((currentMillis - lastUpdate) > updateInterval)  // time to update
    {
      lastUpdate = millis();
      pos += increment;
      servo.write(pos);
      if ((pos >= 180|| (pos <= 0)) // end of sweep
      {
        // reverse direction
        increment = -increment;
      }
    }
  }
};
 
 
Flasher led1(11123400);
Flasher led2(12350350);
Flasher led3(13200222);
 
Sweeper sweeper1(25);
Sweeper sweeper2(35);
 
void setup() 
  sweeper1.Attach(9);
  sweeper2.Attach(10);
  
  // Timer0 is already used for millis() - we'll just interrupt somewhere
  // in the middle and call the "Compare A" function below
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
 
// Interrupt is called once a millisecond, looks for any new GPS data, and stores it
SIGNAL(TIMER0_COMPA_vect) 
{
  unsigned long currentMillis = millis();
  sweeper1.Update(currentMillis);
  
  if(digitalRead(2== HIGH)
  {
     sweeper2.Update(currentMillis);
     led1.Update(currentMillis);
  }
  
  led2.Update(currentMillis);
  led3.Update(currentMillis);
 
void loop()
{
}
cs



외부 인터럽트


루프에서 벗어나는 것이 더 나을 때


타이머 인터럽트와 달리 외부 인터럽트는 외부 이벤트에 의해 트리거됩니다. 예를 들어, 버튼을 누르거나 로터리 엔코더에서 펄스를 수신 한 경우. 그러나 타이머 인터럽트와 마찬가지로 변경을 위해 GPIO 핀을 폴링하지 않아도됩니다.

Arduino UNO에는 2 개의 외부 인터럽트 핀이 있습니다. 이 예에서는 푸시 버튼 중 하나에 푸시 버튼을 부착하여 스위퍼를 재설정합니다. 먼저 스위퍼 클래스에 "reset ()"함수를 추가하십시오. reset () 함수는 위치를 0으로 설정하고 즉시 서보를 위치시킵니다.


1
2
3
4
5
6
  void reset()
  {
    pos = 0;
    servo.write(pos);
    increment = abs(increment);
  }
cs


그런 다음 AttachInterrupt ()에 대한 호출을 추가하여 외부 인터럽트를 핸들러 코드와 연결합니다.

UNO에서 인터럽트 0은 디지털 핀 2와 연관되어 있습니다.이 핀에서 신호의 "FALLING"에지를 찾도록 지시합니다. 버튼을 누르면 신호가 HIGH에서 LOW로 떨어지며 "Reset"인터럽트 핸들러가 호출됩니다.


1
2
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(0, Reset, FALLING);
cs


그리고 여기에 "재설정"인터럽트 핸들러가 있습니다. 스위퍼 재설정 기능을 호출합니다.


1
2
3
4
5
void Reset()
{
  sweeper1.reset();
  sweeper2.reset();
}
cs


이제 버튼을 누를 때마다 서보가하는 일을 멈추고 바로 0 위치로 이동합니다.


소스 코드 :


다음은 타이머와 외부 인터럽트를 사용한 전체 스케치입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <Servo.h> 
 
class Flasher
{
    // Class Member Variables
    // These are initialized at startup
    int ledPin;      // the number of the LED pin
    long OnTime;     // milliseconds of on-time
    long OffTime;    // milliseconds of off-time
 
    // These maintain the current state
    volatile int ledState;                     // ledState used to set the LED
    volatile unsigned long previousMillis;      // will store last time LED was updated
 
  // Constructor - creates a Flasher 
  // and initializes the member variables and state
  public:
  Flasher(int pin, long on, long off)
  {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);     
      
    OnTime = on;
    OffTime = off;
    
    ledState = LOW; 
    previousMillis = 0;
  }
 
  void Update(unsigned long currentMillis)
  {
    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
    {
        ledState = LOW;  // Turn it off
      previousMillis = currentMillis;  // Remember the time
      digitalWrite(ledPin, ledState);  // Update the actual LED
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
    {
      ledState = HIGH;  // turn it on
      previousMillis = currentMillis;   // Remember the time
      digitalWrite(ledPin, ledState);      // Update the actual LED
    }
  }
};
 
class Sweeper
{
  Servo servo;              // the servo
  int  updateInterval;      // interval between updates
  
  volatile int pos;                  // current servo position 
  volatile unsigned long lastUpdate; // last update of position
  volatile int increment;            // increment to move for each interval
 
public
  Sweeper(int interval)
  {
    updateInterval = interval;
    increment = 1;
  }
  
  void Attach(int pin)
  {
    servo.attach(pin);
  }
  
  void Detach()
  {
    servo.detach();
  }
  
  void reset()
  {
    pos = 0;
    servo.write(pos);
    increment = abs(increment);
  }
  
  void Update(unsigned long currentMillis)
  {
    if((currentMillis - lastUpdate) > updateInterval)  // time to update
    {
      lastUpdate = currentMillis;
      pos += increment;
      servo.write(pos);
      if ((pos >= 180|| (pos <= 0)) // end of sweep
      {
        // reverse direction
        increment = -increment;
      }
    }
  }
};
 
 
Flasher led1(11123400);
Flasher led2(12350350);
Flasher led3(13200222);
 
Sweeper sweeper1(25);
Sweeper sweeper2(35);
 
void setup() 
  sweeper1.Attach(9);
  sweeper2.Attach(10);
  
  // Timer0 is already used for millis() - we'll just interrupt somewhere
  // in the middle and call the "Compare A" function below
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
  
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(0, Reset, FALLING);
 
void Reset()
{
  sweeper1.reset();
  sweeper2.reset();
}
 
// Interrupt is called once a millisecond, 
SIGNAL(TIMER0_COMPA_vect) 
{
  unsigned long currentMillis = millis();
  sweeper1.Update(currentMillis);
  
  //if(digitalRead(2) == HIGH)
  {
     sweeper2.Update(currentMillis);
     led1.Update(currentMillis);
  }
  
  led2.Update(currentMillis);
  led3.Update(currentMillis);
 
void loop()
{
}
cs



Library 와 유용한 Link들 - 따로 정리함 http://fishpoint.tistory.com/2160


타이머에 대한 추가 정보


타이머는 다양한 주파수에서 작동하고 다른 모드에서 작동하도록 구성 할 수 있습니다. 인터럽트 생성과 더불어 PWM 핀을 제어하는데도 사용됩니다. 아래의 링크는 타이머를 구성하고 사용하는 방법을 이해하는 데 도움이되는 훌륭한 자료입니다.


Arduino 101: Timers and Interrupts http://robotshop.com/letsmakerobots/arduino-101-timers-and-interrupts



Secrets of Arduino PWM   https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM



Adjusting PWM Frequencies    http://playground.arduino.cc/Main/TimerPWMCheatsheet



타이머 라이브러리


웹에는 수많은 Arduino 'timer'라이브러리가 있습니다. 많은 사람들이 millis ()를 모니터링하고이 시리즈의 1 부에서했던 것처럼 계속 폴링을 요구합니다. 그러나 실제로 인터럽트를 생성하도록 타이머를 구성 할 수있는 몇 가지가 있습니다.

Paul Stoffregan의 탁월한 TimerOne 및 TimerThree 라이브러리는 타이머 인터럽트 구성에 대한 많은 세부 사항을 처리합니다. (TimerThree는 UNO에는 적용되지 않으며 Leonardo, Mega 및 일부 Teensy 보드와 함께 사용할 수 있습니다)



TimerOne & TimerThree Libraries   https://www.pjrc.com/teensy/td_libs_TimerOne.html



핀 변경 인터럽트


2개의 외부 인터럽트가 충분하지 않은 경우


Arduino UNO에는 2 개의 외부 인터럽트 핀만 있습니다. 하지만 2 개 이상의 인터럽트가 필요한 경우에는 어떻게해야합니까? 다행히 Arduino UNO는 모든 핀에서 "핀 변경"인터럽트를 지원합니다.

핀 변경 인터럽트는 외부 인터럽트와 유사합니다. 차이점은 8 개의 연관된 핀 중 하나에서 상태가 변경되면 하나의 인터럽트가 생성된다는 것입니다. 인터럽트를 유발 한 8 개의 핀 중 어느 것이 었는지 파악하기 위해 모든 8 개의 핀 중 마지막으로 알려진 상태를 추적해야하기 때문에 좀 더 복잡합니다.

Arduino Playground의 PinChangeInt 라이브러리는 핀 교환 인터럽트를위한 편리한 인터페이스를 구현합니다 : http://playground.arduino.cc/Main/PinChangeInt


PinChangeInt Library  http://playground.arduino.cc/Main/PinChangeInt



타이머 및 인터럽트 에티켓


인터럽트는 슈퍼마켓의 급행 차선과 같습니다. 사려깊게 사용하고, 10개 이하로 유지해야 모든 것이 원활하게 진행될 것입니다. 모든 것이 높은 우선 순위라면 높은 우선 순위는 의미가 없습니다.


인터럽트 핸들러는 우선 순위가 높고 시간에 민감한 이벤트만 처리하는 데 사용해야합니다. 인터럽트 핸들러를 사용하는 동안 다른 인터럽트는 사용할 수 없음을 기억하십시오. 인터럽트 수준에서 너무 많이 수행하려고하면 다른 인터럽트에 대한 응답이 저하됩니다. 가능한 한 인터럽트 처리 함수에서는 아주 작은 일만 처리하는게 좋습니다. 


한 번에 하나의 인터럽트.


ISR에서 인터럽트가 비활성화됩니다. 여기에는 두 가지 중요한 의미가 있습니다.

ISR에서 수행 된 작업은 인터럽트를 놓치지 않도록 짧게 유지해야합니다.

ISR의 코드는 인터럽트를 활성화해야하는 항목 (예 : delay () 또는 i2c 버스를 사용하는 항목)을 호출해서는 안됩니다. 이로 인해 프로그램이 중단됩니다.


긴 처리를 루프에 지연시킵니다.


당신이 인터럽트에 응답하여 광범위한 처리를해야 할 경우, 추가 처리가 필요하다는 것을 나타 내기 위해 (아래 참조) 휘발성 상태 변수를 설정, 필수적인 것만 수행 할 인터럽트 핸들러를 사용합니다. 루프에서 업데이트 함수를 호출 할 때 후속 처리가 필요한지 여부를 확인하기 위해 상태 변수를 확인하십시오.


타이머를 다시 구성하기 전에 확인하십시오.


타이머는 제한된 리소스입니다. UNO에는 3 개 밖에 없으며 많은 것들을 위해 사용됩니다. 타이머 설정을 엉망으로 만들면 다른 것들은 더 이상 작동하지 않을 수 있습니다. 

예를 들어, Arduino UNO에서 :

Timer0 - 핀 5 및 6의 millis (), micros (), delay () 및 PWM에 사용됩니다.

Timer1 - Servos, WaveHC 라이브러리 및 핀 9 및 10의 PWM에 사용됩니다.

Timer2 - 11 번과 13 번 핀의 톤과 PWM에 사용됩니다.

 

안전한 데이터 공유


인터럽트는 프로세서가 인터럽트를 처리하기 위해 수행하는 작업을 일시 중단하기 때문에 우리는 인터럽트 핸들러와 루프의 코드간에 데이터를 공유하는 데주의해야합니다.


휘발성 변수


때로는 컴파일러가 속도를 위해 코드를 최적화하려고 시도합니다. 때때로 이러한 최적화는 일반적으로 사용되는 변수를 레지스터에 복사하여 빠른 액세스를 유지합니다. 문제는 그 변수들 중 하나가 인터럽트 핸들러와 루프 코드 사이에서 공유된다면 그것들 중 하나가 실재 대신 오래된 사본을 보게 될지도 모른다는 것입니다. 변수를 휘발성으로 표시하면 컴파일러에서 잠재적으로 위험한 최적화 애들을하지 않게됩니다.


더 큰 변수 보호하기


가변 변수 휘발성을 표시하는 Evan은 변수가 정수 (예 : 문자열, 배열, 구조 등)보다 큰 경우 충분하지 않습니다. 큰 변수는 여러 명령어 사이클을 업데이트해야하며, 업데이트 중간에 인터럽트가 발생하면 데이터가 손상 될 수 있습니다. 인터럽트 핸들러와 공유되는 더 큰 변수 나 구조체를 가지고 있다면 루프에서 인터럽트를 업데이트 할 때 인터럽트를 비활성화해야합니다. (인터럽트는 이미 기본적으로 인터럽트 처리기에서 비활성화되어 있습니다.)





더욱 좋은 정보를 제공하겠습니다.~ ^^

이 글 공유하기

facebook twitter kakaoTalk kakaostory naver band