본문 바로가기
임베디드/STM32

20. [STM32] Timer - Output Compare

by fuhehe 2025. 1. 6.

타이머의 Output Compare 기능은 카운터 레지스터(TIMx_CNT)와 Capture/Comare 레지스터(TIMx_CCRx)의 값이 동일할 때 인터럽트를 발생시키거나 지정된 GPIO핀을 통해 출력을 내보낼 수 있도록 한다.

동작방식은 앞서 타이머 기초에서 설명한 TIMx_CNT와 ARR의 비교를 통한 타이머 제어와 유사하다. 다만 Output Compare는 기본적으로 타이머에 설정한 Prescaler값을 기반으로 타이머의 ARR값과는 별개의 주기를 만들어 낼 수 있고 GPIO핀으로 신호를 내보낼 수 있다는 점이 다르다.

이번장에서는 이 Output Compare기능에 대해 알아보자.

 

 

  • 본 블로그는 STM32를 소프트웨어 엔지니어 관점에서 바라본 블로그입니다. 따라서 회로등의 전자공학 관련 내용은 사실과 다를 수 있습니다.
  • 본 블로그에서 사용된 MCU 및 개발보드는 다음과 같습니다.
    1. NUCLEO-F429ZI (STM32F429ZIT LQFP144)
    2. NUCLEO-F439ZI (STM32F439ZIT LQFP144)

 

 

 

1. Output Compare의 동작방식

Output Compare를 사용하든 안하든 타이머는 APB1, APB2에 인가되는 클럭을 기반으로 Prescaler, ARR에 지정된 값에 따른 주기를 가진다.

앞장에서 설명했다시피 APB1/APB2의 클럭을 Prescaler에 지정한 값으로 분주한 매 클럭마다 TIMx_CNT가 증가(Upcounting모드라고 가정)하고 TIMx_CNT값이 ARR에 지정한 값과 동일할 경우 다음 클럭에서 UEV를 발생시킨 뒤 TIMx_CNT는 0으로 초기화 된다.

 

이때 타이머의 한 채널을 Output Compare로 지정하면 이 Output Compare는 해당 타이머의 주기를 그대로 사용하면서 별도의 ARR값을 사용하는 것과 유사하다.

 

말로 설명하는것보다 예제를 하나 들어서 설명하는게 더 나을 수 있을거 같아 예제부터 보기로 하겠다.

 

Basic Timer에서는 Output Compare기능을 지원하지 않으므로 GP타이머중 TIM3을 사용하자.

TIM3은 STM32F4xx에서 APB1에 연결되어 있고 NUCLEO보드의 APB1의 클럭은 디폴트로 84MHz이다.

따라서 아래 그림과 같이 설정한다면 타이머는 10kHz의 클럭속도(84000000/8400)로 TIMx_CNT값이 증가하고, TIMx_CNT가 10000(-1)이 되는 순간 UEV가 발생한다. 즉 타이머는 1s속도(1 / (84000000/8400) * 10000 ) 로 동작하게 된다.

 

그리고 Channel1을 Output Compare CH1로 설정하고 Pulse를 1000(-1)으로 설정하자.

이 설정은 현재 TIMx_CNT값이 10kHz속도로 증가하고 이 레지스터 값이 1000이 되는 시점에 Output Compare신호를 발생하겠다는 의미이다. 따라서 Pulse가 1000이라는 말은 100ms가 되는 시점에 Output Compare신호가 발생한다는 의미이다.(TIMx_CNT값은 1/10kHz=1/10000=0.0001초단위로 증가하므로 0.0001*1000=0.1초)

 

이 설정대로라면 타이머는 1초간격으로 돌아가고, Output Compare는 0.1초가 되면 신호를 발생시킨다.

 

그리고 타이머 자체의 인터럽트를 사용할 것이므로 TIM3 global interrupt에 체크하자. 

Channel1에 Output Compare CH1을 선택했으므로 TIM3의 CH1에 할당된 PA6 GPIO핀이 자동설정된 것을 볼 수 있다. 따라서 이 핀을 통해 Output Compare의 신호를 받을 수 있다.(Output Compare CH1대신 Output Compare No Output을 선택하면 핀으로의 출력없이 내부 인터럽트만 사용할 수도 있다. 그리고 TIM3의 CH1에 할당된 PA6핀은 데이터시트 상에 정의된 내용이므로 임의로 다른 핀으로 변경할 수는 없다.)

또한 Output Compare의 Mode를 "Toggle on match"로 설정해서 신호가 발생할 때 마다 Low/High를 토글하도록 설정했다.(따라서 0.1초 단위로 Low/High상태가 변경된다.)

 

마지막으로 TIM3이 1초간격으로 인터럽트를 발생시킬때 PG3핀을 토글시키도록 해서 외부에서 확인이 가능하도록 하였다.(라벨을 TIM3_SIG로 정의)

[Output Compare CH1설정 1]

 

[GPIO PG3 설정]

 

설정이 끝났으면 아래와 같이 코딩하자.

/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim3;

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM3_Init(void);



int __io_putchar(int ch)
{
 // Write character to ITM ch.0
 ITM_SendChar(ch);
 return(ch);
}

/*
 * TIM3
 * CLK : 84MHz (APB1)
 * ARR : 10000-1
 * Prescaler : 8400-1
 * 시간 : 1s
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	  if(htim->Instance == TIM3)
	  {
		  printf("TIM3 Update (Tick=%lu)\r\n", HAL_GetTick());
		  HAL_GPIO_TogglePin(TIM3_SIG_GPIO_Port, TIM3_SIG_Pin);
	  }
}

/*
 * TIM3 - Output Compare
 * Mode : Toggle on match
 * Pulse : 1000-1
 * 시간 : 100ms
 */
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
	  if(htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
	  {
		  printf("\tTIM3 OC (Tick=%lu), TIM3_CNT=%lu, TIM3_CCR1=%lu\r\n", HAL_GetTick(), htim->Instance->CNT, htim->Instance->CCR1);

		  /*
		   * Output Compare(이하 OC) 펄스를 1000(-1)으로 설정했으므로 타이머 인터럽트가 발생한 뒤 1000클럭 이후(0.1s) OC인터럽트가 발생한다.
		   * 이 상태로는 OC역시 타이머 인터럽트에서 0.1초 늦은 인터럽트 1회만 발생한다.
		   * 목적이 OC는 0.1초 간격으로 계속해서 발생시키는 것이므로 TIM3_CNT가 1000(-1)이 되는 순간 TIM3_CCR1에 1000(-1)을 더한 값으로 다시 갱신해야 한다.
		   * 그렇지 않으면 1000(-1)이 되는 순간 한번 신호가 발생한 뒤 TIM3_CNT는 10000(-1)이 될 동안 계속 값이 증가하기 때문에 이 구간동안은 OC가 발생하지 않는다.
		   * 그리고 TIM3_CNT가 다시 0으로 리셋되어 증가하다 1000(-1)이 되는 순간 다시 OC가 발생하므로 실제적으로는 OC가 TIM3의 인터럽트와 1000클럭 지연된 신호로 1초간격
		   *  으로 발생한다.
		   *
		   * 단, 여기서 TIM3의 ARR은 10000(-1)이므로 TIM3_CNT 레지스터의 값은 10000(-1)이 되는 순간 다시 0으로 리셋된다.
		   * 이렇게 되면 TIM3_CCR1의 값을 1000(-1)씩 누적해서 1000(-1)*10이 넘어가는 순간 인터럽트가 발생하지 않다가 65535이상되어 오버플로가 발생해야
		   * 다시 정상적으로 인터럽트가 발생한다.(즉 ARR값이 10000(-1)이므로 TIM3_CNT의 값은 절대 10000이상 카운트 되지 않는다.)
		   * 따라서 TIM3_CCR1의 값이 ARR값을 초과하는 경우 다시 초기값으로 설정해야 정상동작 한다.
		   */
		  if(htim->Instance->CCR1 > htim->Instance->ARR)
			  htim->Instance->CCR1 = 1000-1;
		  else
			  htim->Instance->CCR1 += 1000-1;
	  }
}



/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();  
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */
  HAL_Delay(100);
  printf("Start (%lu)\r\n", HAL_RCC_GetPCLK1Freq());

  HAL_TIM_Base_Start_IT(&htim3);				// TIM3을 인터럽트 발생하도록 초기화
  HAL_TIM_OC_Start_IT(&htim3, TIM_CHANNEL_1);	// TIM3-CH1을 Output Compare로 인터럽트 발생가능하게 초기화


  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

 

코딩은 main함수에서 HAL_TIM_Base_Start_IT()로 TIM3시작, HAL_TIM_OC_Start_IT()로 TIM3의 Channel1을 OC로 시작시키는 함수를 Interrupt버전으로 호출했고, TIM3과 OC 각각의 Callback함수를 작성한게 전부다.

OC의 동작방식은 OC의 Callback함수에 주석으로 설명을 해놨으므로 참조하라.

 

오실로스코프가 있다면 PG3핀과 PA6핀을 통해 각각 TIM3과 TIM3-CH1의 OC 신호를 확인할 수 있고, 오실로스코프가 없다면 printf로 SWV ITM Data Console창에 메지시를 찍고 있으므로 이 창을 통해 확인하면 된다.

 

다음은 오실로스코프와 SWT Console창에 찍힌 결과이다.

[출력 결과]

 

Start (42000000)
	TIM3 OC (Tick=1201), TIM3_CNT=999, TIM3_CCR1=999
	TIM3 OC (Tick=1301), TIM3_CNT=1998, TIM3_CCR1=1998
	TIM3 OC (Tick=1401), TIM3_CNT=2997, TIM3_CCR1=2997
	TIM3 OC (Tick=1501), TIM3_CNT=3996, TIM3_CCR1=3996
	TIM3 OC (Tick=1601), TIM3_CNT=4995, TIM3_CCR1=4995
	TIM3 OC (Tick=1701), TIM3_CNT=5994, TIM3_CCR1=5994
	TIM3 OC (Tick=1801), TIM3_CNT=6993, TIM3_CCR1=6993
	TIM3 OC (Tick=1901), TIM3_CNT=7992, TIM3_CCR1=7992
	TIM3 OC (Tick=2001), TIM3_CNT=8991, TIM3_CCR1=8991
	TIM3 OC (Tick=2101), TIM3_CNT=9990, TIM3_CCR1=9990
	TIM3 OC (Tick=2102), TIM3_CNT=0, TIM3_CCR1=10989
TIM3 Update (Tick=2102)
	TIM3 OC (Tick=2201), TIM3_CNT=999, TIM3_CCR1=999
	TIM3 OC (Tick=2301), TIM3_CNT=1998, TIM3_CCR1=1998
	TIM3 OC (Tick=2401), TIM3_CNT=2997, TIM3_CCR1=2997
	TIM3 OC (Tick=2501), TIM3_CNT=3996, TIM3_CCR1=3996
	TIM3 OC (Tick=2601), TIM3_CNT=4995, TIM3_CCR1=4995
	TIM3 OC (Tick=2701), TIM3_CNT=5994, TIM3_CCR1=5994
	TIM3 OC (Tick=2801), TIM3_CNT=6993, TIM3_CCR1=6993
	TIM3 OC (Tick=2901), TIM3_CNT=7992, TIM3_CCR1=7992
	TIM3 OC (Tick=3001), TIM3_CNT=8991, TIM3_CCR1=8991
	TIM3 OC (Tick=3101), TIM3_CNT=9990, TIM3_CCR1=9990
	TIM3 OC (Tick=3102), TIM3_CNT=0, TIM3_CCR1=10989
TIM3 Update (Tick=3102)
	TIM3 OC (Tick=3201), TIM3_CNT=999, TIM3_CCR1=999
	TIM3 OC (Tick=3301), TIM3_CNT=1998, TIM3_CCR1=1998
	TIM3 OC (Tick=3401), TIM3_CNT=2997, TIM3_CCR1=2997
	TIM3 OC (Tick=3501), TIM3_CNT=3996, TIM3_CCR1=3996
	TIM3 OC (Tick=3601), TIM3_CNT=4995, TIM3_CCR1=4995
	TIM3 OC (Tick=3701), TIM3_CNT=5994, TIM3_CCR1=5994

...

 

만일 위의 예제에서 TIM3자체의 인터럽트를 사용할 필요가 없고 OC만 사용한다면 TIM3의 ARR값을 65535로 설정하고 HAL_TIM_OC_DelayElapsedCallback()에서 if구문없이 단순히 1000(-1)값만 누적시키면 된다.

void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
	  if(htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
	  {
		  printf("\tTIM3 OC (Tick=%lu), TIM3_CNT=%lu, TIM3_CCR1=%lu\r\n", HAL_GetTick(), htim->Instance->CNT, htim->Instance->CCR1);

		  htim->Instance->CCR1 += 1000-1;
	  }
}

 

 

 

2. Output Compare의 몇가지 Mode

Output Compare에는 몇가지  모드가 존재한다. 위의 예제에서 사용한 Mode는 Toggle on match였다. 

이 모드 설정에 따라 OC는 동작방식이 달라지므로 이번에는 OC의 모드들중 몇가지를 간단하게 살펴보자.

 

A. Frozen

TIMx_CNT와 TIMx_CCRn의 비교와 상관없이 해당 Channel의 출력을 고정시키는 모드이다. 내부적으로 CCR레지스터의 값과 비교는 수행하지만 외부 출력은 하나로 고정시켜야 할 경우 사용한다. 이때 고정되는 출력은 타이머 시작시 설정한 CH Polarity에 의해 출력이 결정된다.

 

단, 이 모드는 Channel설정시 "Output Compare No Output"으로 설정하는것과는 약간의 의미차이가 있다.

Channel자체를 "Output Compare No Output"로 설정하는 것은 외부출력을 사용하지 않겠다는 명시적인 선언이다. 따라서 CCR 비교는 수행하지만 어떠한 출력도 하지 않는다는 의미이다.

하지만 "Output Compare CHn"으로 설정하고 모드를 Frozen으로 설정하면 외부출력이 고정되기는 하지만 실제 출력은 하나로 설정된다.

따라서 외부출력이 전혀 필요없다면 Channel설정에서 No output으로 설정하면 되고, 타이머가 동작중일 때 등을 외부에서 체크(초기 CH Polarity값)해야 하는 상황에서는 Frozen모드로 설정해서 사용할 수 있다.

 

B. Active Level on match

이 모드는 CNT가 CCR값과 동일할 때 출력핀으로 특정 출력을 발생시키도록 한다. 예를들어 모드를 Active level on match로 설정하고  Ch Polarity를 High로 설정하면 타이머가 시작할 때 출력은 Low상태였다가 CNT와 CCR이 매치되는 순간 Ch Polarity에 설정한 High로 출력이 변환된다.

 

C. Inactive level on match

Active Level on match와 다르게 타이머 시작시에 CH Polarity에 설정된 값으로 출력이 초기화 된다. 이후 CNT와 CRR이 매치되는 순간 출력은 비활성화 된다.

** 테스트 결과 이 모드에서는 초기에 CH Polarity와 반대신호가 출력되고 CNT-CRR매치시에도 아무런 변화가 없다. 왜 그런지는 모르겠음.

 

D. Toggle on match

예제에서 설정한 모드로 CNT와 CCR이 매치되는 순간 출력이 변환되는건 동일하지만 Active level on match와 다르게 이전 출력값을 반전시킨다.

 

E. Forced Active/Inactive

이 모드는 CNT/CCR매치와 상관없이 출력을 S/W에서 제어하는 모드이다. 내부적으로는 CNT/CCR매치를 체크하지만 외부 출력은 이와 무관하게 S/W에서 제어해야 할 경우 사용한다.

Forced Active모드는 출력을 강제로 High로 설정, Inactive모드는 Low로 설정한다.

출력값은 TIMx_CCMRn 레지스터의 OCxM비트 조작으로 변경할 수 있다.(모드 자체를 Forced Active, Forced Inactive모드로 변경한다.)

 

F. PWM 1/2

Channel설정을 PWM으로 설정했을때 사용할 수 있는 모드이다.

PWM출력이 필요한 경우 설정한다. 이후 별도의 PWM 글에서 설명하겠다.

 

 

3. Active level on match 예제

이번에는 Active level on match모드로 간단한 예제를 하나 만들어보자.

TIM3을 사용하고 Channel1은 Active, Channel2는 Inactive모드로 설정한다. 아래 그림과 같이 세팅하면 Channel1은 2초후 High상태로, Channel2는 3초후에 Low상태로 전환된다.

타이머 자체의 ARR값은 이 예제에서 큰 의미가 없으므로 각 Channel의 Pulse값보다 크면 되는데 그냥 최대값으로 지정했다.

그리고 Channel1은 PA6, Channel2는 PC7핀으로 출력을 내보내므로 이 핀에 LED를 달고 확인해보면 된다.

[OC Active/Inactive Level on match 설정]

 

코딩은 별로 할게 없다.

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_OC_Start(&htim3, TIM_CHANNEL_1);
  HAL_TIM_OC_Start(&htim3, TIM_CHANNEL_2);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
}

 

main함수에서 TIM3의 1,2번 Channel별로 HAL_TIM_OC_Start()함수를 호출해 주기만 하면 된다.

단, 이때 이전 예제와는 달리 별도 인터럽트를 사용하지 않기 때문에 HAL_TIM_OC_Start_IT()대신 HAL_TIM_OC_Start()를 사용하였다.

 

 

4. 마무리

이번 시간에는 타이머의 카운트를 체크해서 특정값이 될 때 외부로 신호를 출력하는 Output Compare에 대해 살펴보았다.

외부장치가 정확한 타이밍에 동작을 수행할 수 있도록 신호를 발생시켜야 한다면 Output Compare를 활용해 쉽게 구현할 수 있다.

 

STM32의 타이머는 사용자가 상황에 맞게 설정해서 사용할 수 있도록 다양한 옵션을 제공한다. 하지만 이게 다른 의미로는 옵션 하나하나의 의미를 정확하게 알고 있어야 타이머를 제대로 사용할 수 있다는 말이다.

소개된 내용말고도 옵션을 바꿔가며 하나하나 확인하면서 공부하는 자세가 필요할 듯 하다.

 

 

'임베디드 > STM32' 카테고리의 다른 글

21. [STM32] Timer - PWM  (0) 2025.01.10
19. [STM32] Timer의 기초  (0) 2024.11.19
18. [STM32] Clock  (0) 2024.11.14
17. [STM32] Power - PVD  (0) 2024.11.08
16. [STM32] Power - Backup Domain Access  (0) 2024.11.07