본문 바로가기
전자공학/아두이노

아두이노 PWM 이론2: analogWrite() 함수 분석

by 블랜드 2022. 2. 1.
반응형

 <선수지식>

 (1) 아두이노 analogWrite() 함수에 대해 알아보기 전에 원활한 이해를 이해 먼저 아래 글을 읽어오면 본문을 이해하기 수월하다. 

 

아두이노 PWM 이론1: PWM, 펄스파, 듀티 사이클

<아두이노 PWM 사용 이유>  아두이노는 디지털 장치이므로 아날로그 신호를 바로 출력해내지 못 한다. 보통 디지털 신호를 아날로그로 변환하기 위해서는 디지털 아날로그 변환기(DAC, Digital analog

recall.tistory.com

(2) AVR과 같은 마이크로컨트롤러의 타이머/카운터에 대한 지식이 있으면 좋다. 물론 없어도 이해하는 데 지장은 없다.


PWM을 지원하는 핀

아두이노 우노 R3 보드의 PWM을 지원하는 디지털 핀

 아두이노는 PWM 구현하기 위해 일일이 코드를 따로 작성하여 구현할 필요가 없다. PWM 기능을 지원하는 아두이노 보드의 전용 디지털 핀을 이용하면 쉽게 PWM을 사용할 수 있다.

 위 그림과 같이, 아두이노 우노에서 PWM을 지원하는 핀으로 디지털 3, 5, 6, 7, 9, 11번 총 6개가 있다. 이들 핀 번호 앞에는 물결 기호(~)가 표시되어 있어 쉽게 구별할 수 있다. 이 핀들만이 analogWrite() 함수를 사용할 수 있다.


analogWrite() 함수

 analogWrite(Pin 번호, 아날로그 값(0~255))

 analogWrite() 함수는 아두이노에서 아날로그 출력을 위해 제공되는 함수이다. 첫 번째 매개변수는 PWM을 출력할 Pin 번호이고, 두 번째 매개변수는 해당 핀에 쓰는 아날로그 값이다. digitalWrite() 함수가 HIGH(1)와 LOW(0)만 출력할 수 있는 점과 다르게, analogWrite() 함수는 0~255까지의 아날로그 값을 표현할 수 있다. 아두이노 보드는 8bit 규격의 PWM을 지원하므로 2^8=256개의 수, 즉 0~255까지의 수를 이용해 아날로그 값을 나타낼 수 있다. 따라서 두 번째 매개변수에 들어올 수 있는 값의 범위가 0~255까지인 것이다.

analogWrite()의 역할

 analogWrite(핀 번호, 아날로그 값)는 아날로그 값 0~255에 해당하는 듀티 사이클(Dury cycle)로 PWM(Pulse Width Modulation)파를 출력한다.


analogWrite() 작동 원리

타이머/카운터(timer/counter)

 analogWrite() 함수의 작동 원리를 이해하기 위해서는 먼저 '타이머/카운터(timer/counter)'와 '클럭(clock)'이 무엇인지 알아야 한다. 아두이노에 내장된 CPU는 일정한 시간 간격으로 펄스파를 내보내는데, 이 펄스파들을 '클럭(clock)'이라고 한다. 위 그림은 펄스파로 이루어진 신호인 클럭 신호(clock signal)를 나타낸 것이다. 

 

 이때 이러한 클럭이 발생한 개수를 세어주는 장치가 '타이머/카운터'이다. 타이머/카운터가 셀 수 있는 클럭 개수는 비트(bit) 수에 따라 달라진다. 8bit 타이머/카운터는 2^8(=256)개의 클럭을 세줄 수 있고, 16bit 타이머/ 카운터는  2^16(=65536) 개를 세줄 수 있다. 타이머/카운터가 세 줄 있는 범위를 넘어설 경우에는 오버플로우가 발생하여 타이머/카운터 값이 0으로 초기화된다. 타이머/카운터의 특징은 다음과 같다.

 

타이머/카운터의 특징

  • 시간 경과를 계산할 수 있다
  • 장치 제어를 위한 주기적인 펄스 출력이 가능하다
  • 외부에서 입력되는 펄스의 발생시각을 알 수 있다
동작 설명
 타이머로 동작 (1) 마이크로 컨트롤러 내부의 시스템 클럭을 카운트
(2) 내부 시스템 클럭은 일정한 주기를 가지기 때문에 타이머 동작은 결과적으로 시간을 카운트할 수 있다 -> 1Hz 펄스를 5개 카운트하면 5초
카운터로 동작 (1) 마이크로컨트롤러 외부에서 입력되는 클럭을 카운트
(2) 외부 입력 클럭은 주기가 불규칙할 수 있기 때문에 클럭의 개수만을 카운트하게 된다

 

 ATmega328P에는 PWM파 출력을 위한 3개의 타이머/카운터(Timer/Counter0, Timer/Counter1, Timer/Counter3)가 있다. 각각의 타이머/카운터에는 2개의 비교기가 있으며 6개의 핀은 PWM 파형을 출력할 수 있다. ATmega328P를 장착한 Arduino Uno도 마찬가지로 6개의 핀에서 PWM 파형을 출력할 수 있다.

 

 3개의 타이머/카운터 중 2개(Timer/Counter0, Timer/Counter2)는 8bit이고 나머지 하나(Timer/Count3)는 16bit이다. PWM파의 출력 주파수는 init() 함수에서 설정된다. analogWrite(핀 번호, 아날로그 값)는 아날로그 값(0~255)을 통해 듀티 사이클을 설정하고 PWM파를 출력한다. analogWrite()는 ATmega328P의 타이머/카운터 관련 레지스터를 사용하여 구현된다.

 

이전 글에서 보여준 digitalWrite()와 delay()를 이용한 PWM 직접 구현 방법은 PWM 신호를 출력하기 위해 CPU의 거의 대부분의 클럭을 사용하므로 다른 작업을 동시에 진행하기 어렵다.

 

 반면에 analogWrite() 함수는 아두이노 보드에 내장되어 있는 PWM 전용 하드웨어인 '타이머/카운터'를 이용하여 PWM을 구현하므로 CPU 클럭을 소비하지 않고도 PWM 신호를 출력할 수 있다. 타이머/카운터를 이용한 PWM 생성 원리는 매우 단순하다. 타이머/카운터에서 비교 일치가 발생한 경우 파형 생성기를 통해 PWM 신호가 출력된다. 즉, 타이머의 값이 비교값과 일치하면 출력되는 디지털 신호가 ON 또는 OFF가 되어 PWM 신호가 생성된다. 

 

Fast PWM

Fast PWM
톱니파: 타이머/카운터가 센 클럭 개수

  위 모양과 같이 톱니파처럼 생긴 신호를 활용하여 PWM 신호를 만들어 내는 모드를 Fast PWM이라고 한다. 이때 톱니파는 타이머/카운터가 세준 클럭 개수이다. 0~255(16진수: FF)까지 세주고 255을 넘어가면 0에서 다시 세주기 때문에 그래프로 그렸을 때 저렇게 보이는 것이다. 

 analogWrite()는 위와 같이 비교값과 일치하기 전에는 ON이고 비교값과 일치하면 OFF가 되어 PWM 신호가 만들어지게 된다. 이때 비교값은 analogWrite(핀 번호, 아날로그 값)에서의 아날로그 값에 해당한다. analogWrite()는 타이머/카운터로 8bit로 제한하여 쓰므로 비교값의 범위는 0~255로 한정되어 있다. 이 비교값 조절을 통해 ON/OFF 주기를 조절함으로써 Duty cycle을 원하는 데로 맞출 수 있는 것이다.

 

Phase Correct PWM

Phase Correct PWM
삼각파: 타이머/카운터가 센 클럭 개수

 위 모양과 같이 삼각파처럼 생긴 신호를 활용하여 PWM 신호를 만들어 내는 모드를 Phase Correct PWM이라고 한다. 이 삼각파도 마찬가지로 타이머/카운터가 세준 클럭 개수이다. 0~255(16진수: FF)까지 업카운트(값을 증가시키며 세줌)했다가 255에서 0까지 다운카운트(값을 감소시키며 세줌)했기 때문에 삼각파와 같은 형태로 그래프가 그려지는 것이다. 

 업카운트할 때 비교값과 일치하면 OFF하고, 다운카운트할 때 비교값과 일치하면 ON하여 PWM 신호를 발생시킨다. 이 모드에서도 analogWrite()의 비교값의 범위는 0~255이다.

분주비(prescaler)

 PWM에 대한 더 상세한 이해를 위해서는 타이머/카운터에서 쓰이는 분주비에 대해서 알아야 한다.

분주비란 타이머/카운터가 클럭을 얼마로 나누어서 셀 것인지를 의미한다. 즉, 분주비가 64라면 클럭을 64단위마다 1로 센다. 아두이노 우노에 쓰이는 마이크로프로세서인 ATmega328P의 경우 기본 시스템 클럭은 16MHz(1클럭당 주기=1/16MHz=0.063us)이다.

 이때 ATmega328의 프리스케일러를 8로 설정하면 시스템 클럭은 8분주한 클럭을 카운트하게 된다. , 클럭 8개마다 한 개씩 카운트하므로 기존보다 8배 느린 클럭을 카운트 하게 된다. 수식으로 나타내면 다음과 같다. 주기는 주파수의 역수이므로 아래와 같이 계산된 것이다.

분주비=8인 경우의 주파수 및 주기 계산

  좀 더 일반적인 수식을 정리하면 다음과 같다. 

이 식은 시스템 클럭이 16MHz인 ATmega328의 주파수 f와 주기 T의 계산식이다. n은 분주비(prescaler)이다. 기본적으로 아두이노 우노의 PWM 출력 함수인 analogWrite()의 분주비는 64로 설정되어 있다.

 

※분주: 주파수를 1/n 하는 . n 정수이며이를 분주비()라고 한다.


타이머/카운터 레지스터

아두이노 우노의 analogWrite()에 쓰인 타이머/카운터 레지스터의 기본 설정을 정리하면 다음과 같다.

아두이노 타이머/카운터 레지스터 공통 설정
파형 모드 클럭 주파수 분주비 출력
Fast PWM 16MHZ 64 비교 일치가 발생하면 출력은 OFF,
BOTTOM(0)에서 출력은 ON
Phase Correct PWM 업카운팅 시 비교 일치에서 출력을 OFF
다운카운팅 시 비교 일치에서 출력을 ON

 

타이머/카운터 파형 모드 디지털 핀 PWM 주파수 PWM 주기
0 Fast PWM 5(OC0B), 6(OC0A) 976.5625Hz 256 클럭
1 Phase Correct PWM
, 8bit
9(OC1A), 10(OC1B) 490.1961Hz 510 클럭
2 Phase Correct PWM 3(OC2B), 11(OC2A) 510 클럭

 

디지털 5, 6번  핀(타이머/카운터 0)

 타이머/카운터0은 8비트 카운터다. TCCR0A, TCCR0B, TCNT0, OCR0A, OCR0B, TIMSK0, TIFR0 레지스터에 의해 제어된다. 타이머/카운터 0은 디지털 핀 6(OC0A 관련) 및 5(OC0B 관련)의 PWM 출력을 제어한다.

비트: 8비트
디지털 핀: 5(OC0B), 6(OC0A)
파형 모드: Fast PWM
-Fast PWM에서 카운터는 0에서 255로 증가했다가 256이 되면 다시 0부터 시작한다.
Fast PWM의 주파수 계산: f = 클럭 주파수 / (분주비 x 256)
클럭 주파수는 16MHz이고 분주비는 64이며 디지털 핀 5와 6에 출력되는 PWM의 주파수는 16000000 / (64 * 256) = 976.5625Hz이다.

디지털 9, 10번  핀(타이머/카운터 1)

 타이머/카운터1은 16비트 카운터입니다. TCCR1A, TCCR1B, TCCR1C, TCNT1H/TCNT1L, OCR1AH/ORC1AL, OCR1BH/OCR1BL, ICR1H/ICR1L, TIMSK1, TIFR1 레지스터에 의해 제어된다. 타이머/카운터 1은 디지털 핀 9(OC1A)과 10(OC1B)의 PWM 출력을 제어한다.

비트: 16비트
디지털 핀: 9(OC1A), 10(OC1B)
파형 모드: Phase Correct PWM, 8bit
-Phase Correct PWM에서 카운터는 0에서 255로 증가하고 타이머는 255에서 0으로 감소한다.
업카운팅할 때 비교 일치에서 출력 OC1A/OC1B를 OFF한다. 다운카운팅 시 비교 일치에서 OC1A/OC1B를 ON한다.
Phase Correct PWM 주파수 계산:  f=클럭 주파수/(분주비 x TOP x 2)
Phase Correct PWM 8비트를 사용할 때 TOP의 값은 255이다. 클럭 주파수가 16MHz이고 분주비가 64이므로 디지털 핀 9와 핀 10으로 출력되는PWM 주파수는 16000000 / (64 * 255 * 2) = 490.1961Hz이다.

 

디지털 3, 11번  핀(타이머/카운터 2)

 타이머/카운터 2는 8비트 카운터다. TCCR2A, TCCR2B, TCNT2, OCR2A, OCR2B, TIMSK2, TIFR2 ASSR, GTCCR 레지스터에 의해 제어된다. 타이머/카운터 2는 디지털 핀 3(OC2B)과 핀 11(OC2A)의 PWM 출력을 제어한다.

비트: 8비트
디지털 핀: 3(OC2B), 11(OC2A)
파형 모드: Phase Correct PWM
-Phase Correct PWM에서 카운터는 0에서 255로 증가하고 타이머는 255에서 0으로 감소한다.
업카운팅 시 비교 일치에서 OC2A를 OFF한다. 다운 카운팅 시 비교 일치에서 OC2A를 ON한다.
업카운팅 시 비교 일치에서 OC2B를 OFF한다. 다운 카운팅 시 비교 일치에서 OC2B를 ON한다.
Phase Correct PWM 주파수 계산:  f=클럭 주파수/(분주비 x 255 x 2)
클럭 주파수가 16MHz이고 분주비가 64이므로 디지털 핀 11과 핀 3에 출력되는 PWM 주파수는 16000000 / (64 * 255 * 2) = 490.1961Hz이다.


analogWrite() 소스코드 

analogWrite()는 파일 경로 C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino에 정의되어 있다.

// Right now, PWM output only works on the pins with
// hardware support.  These are defined in the appropriate
// pins_*.c file.  For the rest of the pins, we default
// to digital output.
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))
		{
			case TIMER0A:
				// connect pwm to pin on timer 0, channel A
				sbi(TCCR0A, COM0A1);
				OCR0A = val; // set pwm duty
				break;

			case TIMER0B:
				// connect pwm to pin on timer 0, channel B
				sbi(TCCR0A, COM0B1);
				OCR0B = val; // set pwm duty
				break;

			case TIMER1A:
				// connect pwm to pin on timer 1, channel A
				sbi(TCCR1A, COM1A1);
				OCR1A = val; // set pwm duty
				break;

			case TIMER1B:
				// connect pwm to pin on timer 1, channel B
				sbi(TCCR1A, COM1B1);
				OCR1B = val; // set pwm duty
				break;

			case TIMER2A:
				// connect pwm to pin on timer 2, channel A
				sbi(TCCR2A, COM2A1);
				OCR2A = val; // set pwm duty
				break;

			case TIMER2B:
				// connect pwm to pin on timer 2, channel B
				sbi(TCCR2A, COM2B1);
				OCR2B = val; // set pwm duty
				break;

			case NOT_ON_TIMER:
			default:
				if (val < 128) {
					digitalWrite(pin, LOW);
				} else {
					digitalWrite(pin, HIGH);
				}
		}
	}
}

 


analogWrite() 소스코드 분석

void analogWrite(uint8_t pin, int val)
{

 입력은 pin과 val이다. 자료형은 각각 uint8_t 및 int이다. 이 val이 비교값이다. 출력 자료형은 void이므로 함수는 값을 반환하지 않는다.

 

	pinMode(pin, OUTPUT);

 analogWrite() 함수 내부에 PinMode가 출력으로 설정되어 있는 것을 확인할 수 있다. 이 덕분에 analogWrite()를 쓸 때는 굳이 PinMode(pin, OUTPUT)를 선언하지 않아도 된다.

 

	if (val == 0)
	{
		digitalWrite(pin, LOW);
	}
	else if (val == 255)
	{
		digitalWrite(pin, HIGH);
	}

 val이 0이면 digitalWrite() 사용하여 출력을 LOW로, 255이면 digitalWrite() 사용하여 출력을 HIGH로 설정한다.

 

	else
	{
		switch(digitalPinToTimer(pin))
		{

 val이 0도 255도 아니면 pin에 대응하는 타이머의 이름을 반환하는 digitalPinToTimer() 함수의 결과에 따라 작업이 switch문을 통해 분기된다.

 

			case TIMER0A:
				// connect pwm to pin on timer 0, channel A
				sbi(TCCR0A, COM0A1);
				OCR0A = val; // set pwm duty
				break;

			case TIMER0B:
				// connect pwm to pin on timer 0, channel B
				sbi(TCCR0A, COM0B1);
				OCR0B = val; // set pwm duty
				break;

			case TIMER1A:
				// connect pwm to pin on timer 1, channel A
				sbi(TCCR1A, COM1A1);
				OCR1A = val; // set pwm duty
				break;

			case TIMER1B:
				// connect pwm to pin on timer 1, channel B
				sbi(TCCR1A, COM1B1);
				OCR1B = val; // set pwm duty
				break;

			case TIMER2A:
				// connect pwm to pin on timer 2, channel A
				sbi(TCCR2A, COM2A1);
				OCR2A = val; // set pwm duty
				break;

			case TIMER2B:
				// connect pwm to pin on timer 2, channel B
				sbi(TCCR2A, COM2B1);
				OCR2B = val; // set pwm duty
				break;

 

 digitalPinToTimer()의 결과에 따라 PWM 출력 제어 레지스터인 TCCRnx와 비교값인 OCRnx를 설정한다. 만약 digitalPinToTimer(pin)의 반환 결과가 TIMER0A이면 함수는 sbi() 를 사용하여 TCCR0A 레지스터의 COM0A1 비트를 1로 설정한다. sbi()는 주소(첫 번째 매개변수)의 비트(두 번째 매개변수)를 1로 설정하는 함수다. 그런 다음 함수는 val을 비교값인 OCR0A에 대입하여 Duty cycle을 설정한다. 

 

			case NOT_ON_TIMER:
			default:
				if (val < 128) {
					digitalWrite(pin, LOW);
				} else {
					digitalWrite(pin, HIGH);
				}
		}
	}
}

 해당 핀이 PWM을 출력할 수 없는 핀인 경우 val이 128보다 작으면  LOW를 출력하고, 128 이상이면 HIGH를 출력한다.


analogWrite() 듀티 사이클 계산법

※ x = 비교값 = Fast PWM에서의 Ton 클럭수 (범위: 0~255)

파형 모드 디지털 핀 PWM 주기 Duty Cycle
Fast PWM 5, 6 256 클럭 x / 256
Phase Correct PWM 3, 9, 10, 11 510 클럭 x / 510 * 2 = x / 255

 위 표에 정리해 놓은 것을 보면 두 파형 모드에 따라 PWM 주기와 Duty Cycle이 다른 것을 볼 수 있다.

 

 (1) Fast PWM의 경우 PWM의 한 주기가 256 클럭이기 때문에 Duty Cycle이 x/256로 계산되는 것이다. 여기서 x가 Ton의 클럭수인데 비교값이 될 수 있는 이유는 비교값과 Ton의 클럭수가 같기 때문이다. 이를테면, Fast PWM에서 비교값이 100이면 Ton의 클럭수도 100이 된다.

 

 (2) Phase Correct PWM의 경우 PWM의 한 주기가 510 클럭이므로 Duty Cycle이 x / 510 * 2 = x / 255로 계산된다. 이때 x / 510에 2를 곱해주는 이유는 비교값인 x의 2배만큼 Ton 클럭수가 발생하기 때문이다. 즉, Fast PWM은 직각삼각형 모양인 반면에 Phase Correct PWM은 삼각파 모양이 되어 PWM 주기가 2배이고, 동일한 비교값에 따른 Ton 클럭수도 2배가 되기 때문이다.

 

Duty Cycle과 analogWrite()의 관계

 아래 표는 위 그림과 같이 Duty Cycle이 계산되는지 알아보기 위해 analogWrite()의 val 값 64, 127, 191에 따른 Duty Cycle을 계산하여 정리한 것이다.

analogWrite(val) Duty Cycle(Fast PWM) Duty Cycle(Phase Correct PWM)
64 64/256 * 100 = 25% 64/255 * 100 = 25.1%
127 127/256 * 100 = 49.6% 127/255 * 100 = 49.8%
191 191/256 * 100 = 74.6% 191/255 * 100 = 74.9%

 

 Duty Cycle 공식에 대한 검증은 다음편에서 아두이노 시뮬레이터인 서킷을 통해 알아보도록 하겠다.

 

※ x = 비교값 = Fast PWM에서의 Ton 클럭수 (범위: 0~255)
(1) Fast PWM
주기: 256 클럭: 5, 6 핀
Duty Cycle = x /256
(2) Phase Correct PWM: 3, 11, 9, 10 핀
주기: 510 클럭
Duty Cycle = x / 510 * 2 = x / 255


<참고문헌>

(1)http://www.righto.com/2009/07/secrets-of-arduino-pwm.html

(2)https://garretlab.web.fc2.com/en/arduino/inside/hardware/arduino/avr/cores/arduino/wiring_analog.c/analogWrite.html

반응형

댓글