오늘은 아두이노 출력 함수 중 마지막인 analogWrite 함수에 대해서 알아보도록 하겠습니다. analogWrite 함수는 이름 그대로 Analog 값을 출력할 수 있게 해 줍니다. 아두이노는 Digital 시스템인데 어떻게 Analog 출력을 낼 수 있는지 궁금하실 겁니다. 사실상 정확하게 말해서는 Analog 값을 선형적으로 매끄럽게 출력하는 것이 아닌 PWM 제어를 통해서 Analog 출력을 모사한다고 볼 수 있습니다. 그럼 본론으로 들어가기 전에 PWM 제어란 무엇인지 알아보도록 하겠습니다.
1. PWM 제어
PWM이란 Pulse-width modulation의 약자로 파형(Pulse)의 폭(Width)을 변조 혹은 조정(Modulation)을 한다는 의미입니다. 말 그대로 Digital로 출력되는 파형의 폭을 변조하면 0(GND)과 1(5V)이 아닌 Analog 값(0 ~ 5V)을 출력할 수 있습니다. 이렇게 말씀드리면 이해가 어려울 거 같으니 그림 보면서 더 자세하게 설명해드리도록 하겠습니다.
위 그림을 보시면 duty에 따라서 출력이 달라지는 것을 볼 수 있습니다. 여기서 duty란 일정 주기(Period) 내에서 HIGH로 올라와있는 비율, 혹은 시간을 의미합니다. 첫 번째 예시로 duty가 5%인 경우에는 1주기 내에서 5퍼센트의 시간만큼만 HIGH로 올라와 있다는 뜻입니다. 좀 더 디테일하게 보면, 1주기가 100 msec라고 가정했을 때, HIGH값을 유지하는 시간은 100 * 0.05로 5 msec만큼만 HIGH로 올라오겠죠? 이렇게 주기적으로 일정한 duty비만큼 HIGH로 인가하게 되었을 때, 5V(HIGH)의 값에 duty비만큼 곱해준 것과 동일한 출력을 볼 수 있습니다. 각 duty비 별로 설명드리면,
Case 1. 5V 시스템 / Duty 5% / Period 100msec
5V 시스템의 경우 HIGH의 값은 5V로 출력됩니다. 여기서 한 주기 100 msec동안 Duty비인 5%만큼만 HIGH로 출력을 하면, 이전에 계산했던 것처럼 5 msec동안만 HIGH를 출력하게 됩니다. 그러면 100 msec동안 평균 출력 전압을 계산해보면 (5V*5msec)/100msec = 0.25 V입니다. 즉, 이 동작이 계속 주기적으로 반복하게 되면 0.25V를 출력하는것과 동일하게 보인다는 이야기입니다.
Case 2. 5V 시스템 / Duty 30% / Period 100msec
위와 조건은 동일하고 Duty비가 30% 일 경우에 대해 생각해봅시다. 30 msec만큼 5V의 전압이 인가되었으니, 위와 동일한 방식으로 평균 출력 전압을 계산해보면 (5V*30 msec)/100 msec = 1.5V입니다. 그렇단 이야기는 Duty비를 30%로 두고 PWM을 출력하게 되면 1.5V를 모사할 수 있단 이야기겠네요.
위 두 Case를 통해 식 하나를 도출해낼 수 있습니다.
[ Microcontroller 출력 전압값 ] X [ Duty 비 ] = [ PWM 출력 전압 ]
이를 통해서 우리는 PWM의 Duty비만 조정을 한다면, Microcontroller의
PWM을 구현하기 위해서는 Timer/Counter라는 모듈을 이용해야 하는데, 우리는 아두이노 내장 함수를 활용할 것이기 때문에 이에 대한 설명은 다음으로 미루도록 하겠습니다.
다시 본론으로 돌아와서 위에서 설명한 PWM 제어를 아두이노에서 어떻게 사용하고 활용할 수 있을지 다뤄봅시다.
2. analogWrite()
우리는 아두이노 스케치를 이용하면 복잡하게 구현해야 했던 PWM을 손쉽게 사용할 수 있습니다. 먼저 analogWrite로 출력을 내보낼 수 있는 Pin이 무엇인지 알아보아야 하는데, 그 답은 보드에 적혀있습니다.
위의 아두이노 우노 사진을 보면 Digital Pin 숫자 옆에 물결 표시가 있는 것을 볼 수 있습니다. 빨간색 동그라미로 표시되어 있는 것처럼 PWM으로 사용 가능한 Pin은 ~표시를 해두었다입니다. '~'표시가 되어있는 핀들 아무거나 골라서 이용하시면 됩니다.
- analogWrite(Pin number, Value): PWM 출력을 내보내고자 하는 Pin number를 입력 (PWM 출력 가능 여부를 확인) / Value는 0 ~ 255의 값이 들어가는데, 만약 50% Duty비를 가진 PWM을 출력하고자 할 때, 255 * 0.5 = 127.5 (127 or 128)을 입력
analogWrite 함수에 대해 좀 더 설명을 추가하면, value값에는 0 ~ 255 사이의 값을 넣을 수 있는데 그 이유는 해당 PWM을 만들어내는 Timer/Counter가 8-bit기 때문이다. 물론 아두이노 메가에는 16-bit Timer/Counter도 존재하지만, 해당 Timer/Counter도 8-bit에 맞춰서 함수를 사용하도록 설계된 것 같다. 해당 내용에 대해 자세히 알고 싶을 경우 "~\Arduino\hardware\arduino\avr\cores\arduino\wiring_analog.c"를 확인하시면 됩니다.
void analogWrite(uint8_t pin, int val)
{
// We need to make sure the PWM output is enabled for those pins
// that support it, as we turn it off when digitally reading or
// writing with them. Also, make sure the pin is in output mode
// for consistenty with Wiring, which doesn't require a pinMode
// call for the analog output pins.
pinMode(pin, OUTPUT);
if (val == 0)
{
digitalWrite(pin, LOW);
}
else if (val == 255)
{
digitalWrite(pin, HIGH);
}
else
{
switch(digitalPinToTimer(pin))
{
// XXX fix needed for atmega8
#if defined(TCCR0) && defined(COM00) && !defined(__AVR_ATmega8__)
case TIMER0A:
// connect pwm to pin on timer 0
sbi(TCCR0, COM00);
OCR0 = val; // set pwm duty
break;
#endif
#if defined(TCCR0A) && defined(COM0A1)
case TIMER0A:
// connect pwm to pin on timer 0, channel A
sbi(TCCR0A, COM0A1);
OCR0A = val; // set pwm duty
break;
#endif
#if defined(TCCR0A) && defined(COM0B1)
case TIMER0B:
// connect pwm to pin on timer 0, channel B
sbi(TCCR0A, COM0B1);
OCR0B = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1A1)
case TIMER1A:
// connect pwm to pin on timer 1, channel A
sbi(TCCR1A, COM1A1);
OCR1A = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1B1)
case TIMER1B:
// connect pwm to pin on timer 1, channel B
sbi(TCCR1A, COM1B1);
OCR1B = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1C1)
case TIMER1C:
// connect pwm to pin on timer 1, channel B
sbi(TCCR1A, COM1C1);
OCR1C = val; // set pwm duty
break;
#endif
#if defined(TCCR2) && defined(COM21)
case TIMER2:
// connect pwm to pin on timer 2
sbi(TCCR2, COM21);
OCR2 = val; // set pwm duty
break;
#endif
#if defined(TCCR2A) && defined(COM2A1)
case TIMER2A:
// connect pwm to pin on timer 2, channel A
sbi(TCCR2A, COM2A1);
OCR2A = val; // set pwm duty
break;
#endif
#if defined(TCCR2A) && defined(COM2B1)
case TIMER2B:
// connect pwm to pin on timer 2, channel B
sbi(TCCR2A, COM2B1);
OCR2B = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3A1)
case TIMER3A:
// connect pwm to pin on timer 3, channel A
sbi(TCCR3A, COM3A1);
OCR3A = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3B1)
case TIMER3B:
// connect pwm to pin on timer 3, channel B
sbi(TCCR3A, COM3B1);
OCR3B = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3C1)
case TIMER3C:
// connect pwm to pin on timer 3, channel C
sbi(TCCR3A, COM3C1);
OCR3C = val; // set pwm duty
break;
#endif
#if defined(TCCR4A)
case TIMER4A:
//connect pwm to pin on timer 4, channel A
sbi(TCCR4A, COM4A1);
#if defined(COM4A0) // only used on 32U4
cbi(TCCR4A, COM4A0);
#endif
OCR4A = val; // set pwm duty
break;
#endif
#if defined(TCCR4A) && defined(COM4B1)
case TIMER4B:
// connect pwm to pin on timer 4, channel B
sbi(TCCR4A, COM4B1);
OCR4B = val; // set pwm duty
break;
#endif
#if defined(TCCR4A) && defined(COM4C1)
case TIMER4C:
// connect pwm to pin on timer 4, channel C
sbi(TCCR4A, COM4C1);
OCR4C = val; // set pwm duty
break;
#endif
#if defined(TCCR4C) && defined(COM4D1)
case TIMER4D:
// connect pwm to pin on timer 4, channel D
sbi(TCCR4C, COM4D1);
#if defined(COM4D0) // only used on 32U4
cbi(TCCR4C, COM4D0);
#endif
OCR4D = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5A1)
case TIMER5A:
// connect pwm to pin on timer 5, channel A
sbi(TCCR5A, COM5A1);
OCR5A = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5B1)
case TIMER5B:
// connect pwm to pin on timer 5, channel B
sbi(TCCR5A, COM5B1);
OCR5B = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5C1)
case TIMER5C:
// connect pwm to pin on timer 5, channel C
sbi(TCCR5A, COM5C1);
OCR5C = val; // set pwm duty
break;
#endif
case NOT_ON_TIMER:
default:
if (val < 128) {
digitalWrite(pin, LOW);
} else {
digitalWrite(pin, HIGH);
}
}
}
}
위의 함수에서 보이듯 입력받는 value는 정수형으로 전달이 되어야 하기 때문에 정확한 50% Duty비를 구현하지 못합니다. 그에 근사한 127이나 128을 입력하여 출력할 수 있습니다.
3. Example
PWM 제어는 대부분 모터를 제어하는데 많이 쓰입니다. 하지만 모터 제어에 대해서는 뒤에서 좀 더 자세하게 다루고 오늘은 LED를 이용해서 PWM 제어 실습을 해보도록 하겠습니다. LED에 PWM을 이용하여 Duty비를 조정했을 때, 출력 전압이 달라지기 때문에 밝기가 변하는 것을 볼 수 있습니다. 우선 코드 먼저 확인하시죠.
#define LEDPin 10
int value = 0;
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
for (value = 0; value < 255; value += 25) {
analogWrite(LEDPin, value); // value 값 만큼 PWM 출력
delay(1000); // 1초 지연
}
for (value = 255; value > 0; value -= 25) {
analogWrite(LEDPin, value); // value 값 만큼 PWM 출력
delay(1000); // 1초 지연
}
}
위의 코드의 구조는 익숙한 구조처럼 보일 것입니다. 그 이유는 이전 실습 때 Servo Motor를 제어할 때 비슷한 방식으로 구현을 했죠. 방식은 동일합니다. 모터 출력을 25 값 씩 증가시켜나갔다가 25씩 감소시키는 코드입니다. 여기서 25의 값이란 Timer/Counter에서의 25 값으로 duty비를 의미하는 것이 아닙니다. 여기서 각 값이 의미하는 duty비를 구하려면, Timer/Counter의 Max값인 255만큼 나누고 100을 곱해주면 어렵지 않게 구할 수 있습니다. 우리가 증가시켜주는 25의 값은 duty비로 환산했을 때, (25 / 255) * 100 = 9.8039 % 입니다. 즉, 25씩 증가시킨다는 것은 duty비를 약 9.8%씩 증가시키는 것으로 5V 시스템이라 가정하였을 때, 5 * 0.098039 = 0.490195 V 씩 증가합니다. 각 반복문이 돌면서 LED에 PWM으로 인가되는 전압이 달라지므로 밝기가 밝아졌다 어두워졌다 변화를 볼 수 있습니다.
회로는 위와 같이 구성해주시면 됩니다.
다음 시간에는 모터의 구동 원리를 알아보고 PWM을 이용하여 모터를 구동해보는 시간을 갖도록 하겠습니다.
'임베디드 프로그래밍 > ATMEL 계열' 카테고리의 다른 글
[임베디드 설계] PWM 제어 (DC 모터 제어) (0) | 2021.04.04 |
---|---|
[임베디드 설계] 시리얼 통신 - 과제 정답 (0) | 2020.06.03 |
[임베디드 설계] 시리얼 통신 (0) | 2020.05.31 |
[임베디드 설계] 라이브러리 추가 및 서보 모터 제어 (0) | 2020.05.07 |