LED PWM 제어 - LED PWM jeeo

  오늘은 아두이노 출력 함수 중 마지막인 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을 이용하여 모터를 구동해보는 시간을 갖도록 하겠습니다. 

강의: youtu.be/tVER6nWyjXM

Toplist

최신 우편물

태그