오늘은 아두이노 출력 함수 중 마지막인 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

 

+ Recent posts