STM32 #7

홍태준·2026년 1월 22일

STM32

목록 보기
7/15
post-thumbnail

Week 2 Day 2: 기본 타이머 인터럽트

학습 목표

  • 타이머 인터럽트의 동작 원리를 이해한다
  • 정확한 주기의 인터럽트를 생성하는 방법을 익힌다
  • 1ms, 10ms, 100ms 등 다양한 주기의 타이머를 설정한다
  • 타이머 인터럽트를 활용한 실용적인 응용 프로그램을 작성한다
  • 여러 타이머를 동시에 사용하는 방법을 습득한다

1. 타이머 인터럽트 기초

타이머 인터럽트는 정확한 주기로 함수를 실행할 수 있게 해주는 핵심 기능입니다. HAL_Delay()와 달리 CPU를 차단하지 않으며, 백그라운드에서 정확한 시간 간격으로 작업을 수행할 수 있습니다.

1.1 타이머 인터럽트란?

타이머 인터럽트의 동작

Timer Counter: 0 → 1 → 2 → ... → ARR
                                    ↓
                              Update Event 발생
                                    ↓
                            인터럽트 요청 (IRQ)
                                    ↓
                              NVIC가 처리
                                    ↓
                          ISR(Interrupt Service Routine) 실행
                                    ↓
                         HAL_TIM_PeriodElapsedCallback() 호출

HAL_Delay()와의 차이

// HAL_Delay() - 블로킹 방식
HAL_Delay(1000);  // 1초 동안 CPU 정지, 다른 작업 불가

// 타이머 인터럽트 - 논블로킹 방식
HAL_TIM_Base_Start_IT(&htim6);  // 타이머 시작
// CPU는 계속 다른 작업 수행 가능
// 1초마다 자동으로 콜백 함수 호출됨

1.2 타이머 인터럽트 사용 시나리오

타이머 인터럽트는 정확한 주기가 필요하거나 여러 작업을 동시에 수행해야 할 때 필수적입니다.

시나리오주기용도
시스템 틱1ms시간 측정, 타임아웃
LED 깜박임100ms ~ 1s상태 표시
센서 읽기10ms ~ 1s주기적 샘플링
디스플레이 갱신16ms (60Hz)화면 업데이트
모터 제어1ms ~ 10msPID 제어 루프
통신 타임아웃가변통신 오류 감지

2. 타이머 인터럽트 설정

STM32CubeMX에서 타이머 인터럽트를 설정하는 과정은 간단하지만, Prescaler와 Period 값을 정확히 계산하는 것이 중요합니다.

2.1 STM32CubeMX 설정

Step 1: 타이머 선택

Timers → TIM6 선택
Mode: Activated (체크)

참고: TIM6, TIM7은 Basic Timer로 인터럽트 전용

Step 2: 파라미터 설정 (1ms 주기 예제)

Configuration → TIM6

Parameter Settings:
  Prescaler (PSC):        83
  Counter Mode:           Up
  Counter Period (ARR):   999
  Auto-reload preload:    Enable

계산:
  Timer Clock = 84MHz (TIM6는 APB1)
  Counter Clock = 84MHz / (83 + 1) = 1MHz
  Update Freq = 1MHz / (999 + 1) = 1kHz
  Period = 1 / 1kHz = 1ms ✓

Step 3: NVIC 설정

NVIC Settings:
  TIM6 global interrupt: Enabled
  Preemption Priority: 0 (높음)
  Sub Priority: 0

Step 4: 코드 생성

Project Manager → Generate Code

2.2 다양한 주기 설정

원하는 주기에 따라 Prescaler와 ARR을 조절합니다. 일반적으로 Prescaler로 기본 속도를 맞추고, ARR로 정밀한 주기를 설정합니다.

1ms 주기 (1kHz)

Timer Clock: 84MHz
PSC: 83  → Counter Clock: 1MHz
ARR: 999 → Update: 1kHz (1ms)

10ms 주기 (100Hz)

방법 1: PSC = 83, ARR = 9999
  → 84MHz / 84 / 10000 = 100Hz

방법 2: PSC = 839, ARR = 999
  → 84MHz / 840 / 1000 = 100Hz (권장)

100ms 주기 (10Hz)

PSC = 8399, ARR = 999
→ 84MHz / 8400 / 1000 = 10Hz

1초 주기 (1Hz)

PSC = 8399, ARR = 9999
→ 84MHz / 8400 / 10000 = 1Hz

계산 공식

Update Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Period (초) = 1 / Update_Frequency

역산:
  (PSC + 1) × (ARR + 1) = Timer_Clock / Update_Frequency

2.3 생성된 초기화 코드 확인

CubeMX가 생성한 코드를 이해하면 수동으로 타이머를 설정할 때도 도움이 됩니다.

/* TIM6 init function */
void MX_TIM6_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  htim6.Instance = TIM6;
  htim6.Init.Prescaler = 83;
  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim6.Init.Period = 999;
  htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  
  if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
  {
    Error_Handler();
  }
  
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  
  if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

인터럽트 핸들러 (stm32f4xx_it.c)

void TIM6_DAC_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim6);
}

3. 타이머 인터럽트 사용

타이머를 시작하고 콜백 함수에서 주기적인 작업을 처리합니다.

3.1 타이머 시작

/* USER CODE BEGIN 2 */
// 타이머 인터럽트 시작
if (HAL_TIM_Base_Start_IT(&htim6) != HAL_OK)
{
  Error_Handler();
}

printf("Timer started: 1ms period\r\n");
/* USER CODE END 2 */

3.2 콜백 함수 구현

콜백 함수는 인터럽트 컨텍스트에서 실행되므로 가능한 짧고 빠르게 작성해야 합니다.

기본 예제: LED 토글

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 1ms마다 호출됨
    static uint16_t counter = 0;
    counter++;
    
    // 500ms마다 LED 토글 (500 × 1ms)
    if (counter >= 500)
    {
      counter = 0;
      HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    }
  }
}
/* USER CODE END 0 */

1초 카운터

/* USER CODE BEGIN 0 */
volatile uint32_t seconds_counter = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 1ms마다 호출
    static uint16_t ms_counter = 0;
    ms_counter++;
    
    if (ms_counter >= 1000)
    {
      ms_counter = 0;
      seconds_counter++;
      
      printf("Uptime: %lu seconds\r\n", seconds_counter);
    }
  }
}
/* USER CODE END 0 */

3.3 타이머 정지 및 재시작

// 타이머 정지
HAL_TIM_Base_Stop_IT(&htim6);

// 타이머 재시작
HAL_TIM_Base_Start_IT(&htim6);

// 카운터 리셋
__HAL_TIM_SET_COUNTER(&htim6, 0);

4. 실용 예제

타이머 인터럽트를 활용한 실제 응용 사례들입니다.

4.1 시스템 틱 (System Tick)

1ms 타이머로 시스템 시간을 관리하고, 타임아웃 기능을 구현합니다.

/* USER CODE BEGIN 0 */
volatile uint32_t system_tick_ms = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    system_tick_ms++;
  }
}

// 타임아웃 함수
uint8_t wait_with_timeout(uint32_t timeout_ms)
{
  uint32_t start_tick = system_tick_ms;
  
  while ((system_tick_ms - start_tick) < timeout_ms)
  {
    // 조건 확인
    if (condition_met())
    {
      return 1;  // 성공
    }
  }
  
  return 0;  // 타임아웃
}

// 경과 시간 측정
uint32_t measure_execution_time(void)
{
  uint32_t start = system_tick_ms;
  
  // 작업 수행
  some_function();
  
  uint32_t elapsed = system_tick_ms - start;
  printf("Execution time: %lu ms\r\n", elapsed);
  
  return elapsed;
}
/* USER CODE END 0 */

4.2 주기적인 센서 읽기

일정 주기로 센서 값을 읽어 처리합니다.

/* USER CODE BEGIN 0 */
#define SAMPLE_INTERVAL_MS 100  // 100ms마다 샘플링

uint16_t sensor_buffer[10];
uint8_t buffer_index = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    static uint16_t ms_counter = 0;
    ms_counter++;
    
    // 100ms마다 센서 읽기
    if (ms_counter >= SAMPLE_INTERVAL_MS)
    {
      ms_counter = 0;
      
      // ADC 읽기 (예시)
      HAL_ADC_Start(&hadc1);
      HAL_ADC_PollForConversion(&hadc1, 10);
      uint16_t adc_value = HAL_ADC_GetValue(&hadc1);
      
      // 버퍼에 저장
      sensor_buffer[buffer_index] = adc_value;
      buffer_index = (buffer_index + 1) % 10;
      
      // 평균 계산
      uint32_t sum = 0;
      for (int i = 0; i < 10; i++)
      {
        sum += sensor_buffer[i];
      }
      uint16_t average = sum / 10;
      
      // 결과 처리
      process_sensor_data(average);
    }
  }
}
/* USER CODE END 0 */

4.3 디지털 시계

1초 타이머로 시, 분, 초를 카운트하는 디지털 시계를 구현합니다.

/* USER CODE BEGIN 0 */
typedef struct {
  uint8_t hours;
  uint8_t minutes;
  uint8_t seconds;
} Clock_t;

Clock_t clock = {0, 0, 0};

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 1초마다 호출 (PSC=8399, ARR=9999)
    clock.seconds++;
    
    if (clock.seconds >= 60)
    {
      clock.seconds = 0;
      clock.minutes++;
      
      if (clock.minutes >= 60)
      {
        clock.minutes = 0;
        clock.hours++;
        
        if (clock.hours >= 24)
        {
          clock.hours = 0;
        }
      }
    }
  }
}

void print_clock(void)
{
  printf("%02d:%02d:%02d\r\n", 
         clock.hours, clock.minutes, clock.seconds);
}

void set_time(uint8_t h, uint8_t m, uint8_t s)
{
  clock.hours = h;
  clock.minutes = m;
  clock.seconds = s;
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// TIM6 설정: 1초 주기 (PSC=8399, ARR=9999)
set_time(12, 0, 0);  // 12:00:00으로 설정
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */

/* USER CODE BEGIN 3 */
while (1)
{
  print_clock();
  HAL_Delay(1000);
}
/* USER CODE END 3 */

4.4 소프트웨어 타이머 (멀티 태스크)

하나의 하드웨어 타이머로 여러 개의 소프트웨어 타이머를 구현합니다.

/* USER CODE BEGIN 0 */
#define MAX_SW_TIMERS 5

typedef struct {
  uint32_t interval_ms;
  uint32_t last_tick;
  void (*callback)(void);
  uint8_t enabled;
} SoftwareTimer_t;

SoftwareTimer_t sw_timers[MAX_SW_TIMERS];
volatile uint32_t system_tick = 0;

void sw_timer_init(uint8_t id, uint32_t interval_ms, void (*callback)(void))
{
  if (id < MAX_SW_TIMERS)
  {
    sw_timers[id].interval_ms = interval_ms;
    sw_timers[id].last_tick = 0;
    sw_timers[id].callback = callback;
    sw_timers[id].enabled = 1;
  }
}

void sw_timer_enable(uint8_t id)
{
  if (id < MAX_SW_TIMERS)
  {
    sw_timers[id].enabled = 1;
    sw_timers[id].last_tick = system_tick;
  }
}

void sw_timer_disable(uint8_t id)
{
  if (id < MAX_SW_TIMERS)
  {
    sw_timers[id].enabled = 0;
  }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    system_tick++;
    
    // 모든 소프트웨어 타이머 확인
    for (uint8_t i = 0; i < MAX_SW_TIMERS; i++)
    {
      if (sw_timers[i].enabled && sw_timers[i].callback != NULL)
      {
        if ((system_tick - sw_timers[i].last_tick) >= sw_timers[i].interval_ms)
        {
          sw_timers[i].last_tick = system_tick;
          sw_timers[i].callback();
        }
      }
    }
  }
}

// 태스크 함수들
void task_led1(void)
{
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void task_led2(void)
{
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}

void task_sensor_read(void)
{
  printf("Reading sensor...\r\n");
  // 센서 읽기 코드
}

void task_display_update(void)
{
  printf("Updating display...\r\n");
  // 디스플레이 업데이트
}

void task_heartbeat(void)
{
  static uint32_t count = 0;
  count++;
  printf("Heartbeat: %lu\r\n", count);
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// 소프트웨어 타이머 초기화
sw_timer_init(0, 500, task_led1);           // 500ms마다 LED1 토글
sw_timer_init(1, 1000, task_led2);          // 1초마다 LED2 토글
sw_timer_init(2, 100, task_sensor_read);    // 100ms마다 센서 읽기
sw_timer_init(3, 50, task_display_update);  // 50ms마다 디스플레이 업데이트
sw_timer_init(4, 2000, task_heartbeat);     // 2초마다 하트비트

// 하드웨어 타이머 시작 (1ms)
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */

5. 여러 타이머 동시 사용

서로 다른 주기의 작업을 위해 여러 타이머를 동시에 사용할 수 있습니다.

5.1 다중 타이머 설정

TIM6: 1ms 타이머 (고속 작업)

PSC: 83
ARR: 999
Priority: 0 (높음)

TIM7: 100ms 타이머 (중속 작업)

PSC: 8399
ARR: 999
Priority: 1 (중간)

5.2 다중 타이머 콜백

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 1ms 타이머: 정밀한 시간 측정
    system_tick_ms++;
    
    // 빠른 응답이 필요한 작업
    check_critical_sensors();
  }
  else if (htim->Instance == TIM7)
  {
    // 100ms 타이머: 일반 주기 작업
    static uint8_t count_100ms = 0;
    count_100ms++;
    
    // 센서 읽기
    read_sensors();
    
    // 1초마다 (100ms × 10)
    if (count_100ms >= 10)
    {
      count_100ms = 0;
      update_display();
      send_data_to_pc();
    }
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// 두 타이머 모두 시작
HAL_TIM_Base_Start_IT(&htim6);  // 1ms
HAL_TIM_Base_Start_IT(&htim7);  // 100ms
/* USER CODE END 2 */

5.3 타이머 우선순위 관리

인터럽트 우선순위를 적절히 설정하여 중요한 작업이 먼저 처리되도록 합니다.

/* USER CODE BEGIN 2 */
// TIM6: 최고 우선순위 (긴급)
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);

// TIM7: 중간 우선순위 (일반)
HAL_NVIC_SetPriority(TIM7_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(TIM7_IRQn);

// SysTick: 낮은 우선순위 (HAL_Delay 등)
HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);
/* USER CODE END 2 */

우선순위 시나리오

TIM7 ISR 실행 중...
  ↓
TIM6 인터럽트 발생! (우선순위 높음)
  ↓
TIM7 ISR 일시 정지
  ↓
TIM6 ISR 실행
  ↓
TIM6 ISR 완료
  ↓
TIM7 ISR 재개
  ↓
TIM7 ISR 완료

6. 성능 최적화

인터럽트 핸들러의 실행 시간을 최소화하여 시스템 성능을 향상시킵니다.

6.1 ISR 실행 시간 측정

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // GPIO를 HIGH로 (측정 시작)
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);
    
    // 실제 작업
    perform_task();
    
    // GPIO를 LOW로 (측정 종료)
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_RESET);
  }
}
/* USER CODE END 0 */

오실로스코프로 PD15를 측정하면 ISR 실행 시간을 정확히 알 수 있습니다.

6.2 플래그 방식 (권장)

ISR에서는 플래그만 설정하고, 실제 작업은 메인 루프에서 처리합니다.

/* USER CODE BEGIN 0 */
// 나쁜 예: ISR에서 직접 처리
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 느린 작업 (위험!)
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    uint16_t value = HAL_ADC_GetValue(&hadc1);
    
    // 복잡한 계산
    float result = complex_calculation(value);
    
    // UART 출력 (매우 느림!)
    printf("Value: %.2f\r\n", result);
  }
}

// 좋은 예: 플래그만 설정
volatile uint8_t sensor_read_flag = 0;
volatile uint16_t sensor_value = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 빠른 읽기
    HAL_ADC_Start(&hadc1);
    if (HAL_ADC_PollForConversion(&hadc1, 1) == HAL_OK)
    {
      sensor_value = HAL_ADC_GetValue(&hadc1);
      sensor_read_flag = 1;  // 플래그만 설정
    }
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 3 */
while (1)
{
  if (sensor_read_flag)
  {
    sensor_read_flag = 0;
    
    // 메인 루프에서 처리 (ISR 차단 안 됨)
    float result = complex_calculation(sensor_value);
    printf("Sensor: %.2f\r\n", result);
  }
  
  // 다른 작업도 수행 가능
  other_tasks();
}
/* USER CODE END 3 */

6.3 인터럽트 중첩 주의사항

우선순위가 낮은 ISR에서 HAL_Delay()를 사용하면 SysTick이 차단되어 시스템이 멈출 수 있습니다.

// 위험한 예
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM7)  // Priority: 5
  {
    HAL_Delay(100);  // 위험! SysTick(Priority: 15)이 차단됨
  }
}

// 안전한 예
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM7)
  {
    // 카운터 사용
    static uint8_t delay_counter = 0;
    delay_counter++;
    
    if (delay_counter >= 10)  // 100ms × 10 = 1초
    {
      delay_counter = 0;
      // 작업 수행
    }
  }
}

7. 실전 프로젝트: 간단한 RTOS 스케줄러

타이머 인터럽트로 간단한 협력형 스케줄러를 구현합니다.

7.1 태스크 구조 정의

/* USER CODE BEGIN 0 */
#define MAX_TASKS 8

typedef enum {
  TASK_STATE_READY,
  TASK_STATE_RUNNING,
  TASK_STATE_BLOCKED,
  TASK_STATE_SUSPENDED
} TaskState_t;

typedef struct {
  void (*function)(void);
  uint32_t period_ms;
  uint32_t last_run;
  TaskState_t state;
  const char* name;
} Task_t;

Task_t task_list[MAX_TASKS];
uint8_t task_count = 0;
volatile uint32_t scheduler_tick = 0;
/* USER CODE END 0 */

7.2 스케줄러 API

/* USER CODE BEGIN 0 */
void scheduler_add_task(void (*function)(void), 
                        uint32_t period_ms, 
                        const char* name)
{
  if (task_count < MAX_TASKS)
  {
    task_list[task_count].function = function;
    task_list[task_count].period_ms = period_ms;
    task_list[task_count].last_run = 0;
    task_list[task_count].state = TASK_STATE_READY;
    task_list[task_count].name = name;
    task_count++;
    
    printf("Task added: %s (Period: %lu ms)\r\n", name, period_ms);
  }
}

void scheduler_suspend_task(uint8_t task_id)
{
  if (task_id < task_count)
  {
    task_list[task_id].state = TASK_STATE_SUSPENDED;
  }
}

void scheduler_resume_task(uint8_t task_id)
{
  if (task_id < task_count)
  {
    task_list[task_id].state = TASK_STATE_READY;
    task_list[task_id].last_run = scheduler_tick;
  }
}
/* USER CODE END 0 */

7.3 스케줄러 실행

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    scheduler_tick++;
    
    // 모든 태스크 확인
    for (uint8_t i = 0; i < task_count; i++)
    {
      if (task_list[i].state == TASK_STATE_READY)
      {
        if ((scheduler_tick - task_list[i].last_run) >= task_list[i].period_ms)
        {
          task_list[i].last_run = scheduler_tick;
          
          // 태스크 실행
          if (task_list[i].function != NULL)
          {
            task_list[i].state = TASK_STATE_RUNNING;
            task_list[i].function();
            task_list[i].state = TASK_STATE_READY;
          }
        }
      }
    }
  }
}
/* USER CODE END 0 */

7.4 태스크 정의 및 등록

/* USER CODE BEGIN 0 */
// 태스크 함수들
void task_blink_led(void)
{
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void task_read_sensor(void)
{
  static uint16_t sample_count = 0;
  sample_count++;
  
  // 센서 읽기
  printf("Sensor reading #%d\r\n", sample_count);
}

void task_update_display(void)
{
  // 디스플레이 업데이트
  printf("Display updated at %lu ms\r\n", scheduler_tick);
}

void task_send_data(void)
{
  // 데이터 전송
  printf("Data sent at %lu ms\r\n", scheduler_tick);
}

void task_watchdog(void)
{
  // Watchdog kick
  static uint32_t wdt_count = 0;
  wdt_count++;
  printf("WDT kicked: %lu\r\n", wdt_count);
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// 태스크 등록
scheduler_add_task(task_blink_led, 500, "LED Blink");
scheduler_add_task(task_read_sensor, 100, "Sensor Read");
scheduler_add_task(task_update_display, 200, "Display Update");
scheduler_add_task(task_send_data, 1000, "Data Send");
scheduler_add_task(task_watchdog, 5000, "Watchdog");

// 스케줄러 시작 (1ms 타이머)
HAL_TIM_Base_Start_IT(&htim6);

printf("\r\nScheduler started with %d tasks\r\n\n", task_count);
/* USER CODE END 2 */

/* USER CODE BEGIN 3 */
while (1)
{
  // 메인 루프는 idle 상태
  // 또는 우선순위 낮은 작업 수행
  __WFI();  // Wait For Interrupt (저전력)
}
/* USER CODE END 3 */

8. 실습 과제

과제 1: 스톱워치 구현

1ms 타이머를 사용하여 정밀한 스톱워치를 구현하세요.

요구사항

  • 1ms 단위 시간 측정
  • 버튼으로 시작/정지/리셋 제어
  • UART로 시간 출력 (형식: MM:SS.mmm)
  • 랩타임 기록 (최대 10개)

힌트

typedef struct {
  uint16_t minutes;
  uint8_t seconds;
  uint16_t milliseconds;
  uint8_t running;
} Stopwatch_t;

Stopwatch_t stopwatch = {0};
uint32_t lap_times[10];
uint8_t lap_count = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6 && stopwatch.running)
  {
    stopwatch.milliseconds++;
    
    if (stopwatch.milliseconds >= 1000)
    {
      stopwatch.milliseconds = 0;
      stopwatch.seconds++;
      
      if (stopwatch.seconds >= 60)
      {
        stopwatch.seconds = 0;
        stopwatch.minutes++;
      }
    }
  }
}

과제 2: 주기적 데이터 로깅

100ms마다 센서 값을 읽어 배열에 저장하고, 1초마다 평균값을 출력하세요.

요구사항

  • 100ms 타이머로 ADC 읽기
  • 10개 샘플의 이동 평균 계산
  • 1초마다 평균값과 최대/최소값 출력
  • 버퍼 오버플로우 처리

힌트

#define BUFFER_SIZE 10

uint16_t adc_buffer[BUFFER_SIZE];
uint8_t buffer_index = 0;
uint8_t buffer_full = 0;

void calculate_statistics(void)
{
  uint32_t sum = 0;
  uint16_t max = 0;
  uint16_t min = 4095;
  
  uint8_t count = buffer_full ? BUFFER_SIZE : buffer_index;
  
  for (uint8_t i = 0; i < count; i++)
  {
    sum += adc_buffer[i];
    if (adc_buffer[i] > max) max = adc_buffer[i];
    if (adc_buffer[i] < min) min = adc_buffer[i];
  }
  
  uint16_t avg = sum / count;
  
  printf("Avg: %d, Min: %d, Max: %d\r\n", avg, min, max);
}

과제 3: 다중 LED 패턴 제어

여러 타이머를 사용하여 4개의 LED가 각각 다른 패턴으로 동작하도록 구현하세요.

요구사항

  • LED1: 500ms 깜박임
  • LED2: 1초 깜박임
  • LED3: 2초 깜박임
  • LED4: 페이드 인/아웃 (PWM, 다음 시간 내용 사용)
  • 소프트웨어 타이머 또는 다중 하드웨어 타이머 사용

9. 트러블슈팅

문제 1: 인터럽트가 발생하지 않음

타이머 인터럽트가 전혀 발생하지 않는 경우입니다.

증상

콜백 함수가 호출되지 않음

원인 및 해결

// 1. 타이머 시작 확인
HAL_TIM_Base_Start_IT(&htim6);  // 호출했는지 확인

// 2. NVIC 활성화 확인 (CubeMX)
// NVIC Settings → TIM6 global interrupt: Enabled

// 3. 인터럽트 핸들러 존재 확인
// stm32f4xx_it.c에 TIM6_DAC_IRQHandler() 있는지 확인

// 4. 클럭 활성화 확인
__HAL_RCC_TIM6_CLK_ENABLE();

문제 2: 인터럽트 주기가 부정확함

설정한 주기와 실제 주기가 다른 경우입니다.

증상

1ms로 설정했는데 실제로는 1.2ms마다 발생

원인 및 해결

// 1. 타이머 클럭 확인
// TIM6는 APB1 Timer Clock 사용
// CubeMX에서 Clock Configuration 확인
// APB1 Timer clocks = 84MHz인지 확인

// 2. Prescaler 및 ARR 재계산
uint32_t timer_clock = 84000000;  // 84MHz
uint32_t target_freq = 1000;      // 1kHz (1ms)

uint32_t total_division = timer_clock / target_freq;  // 84000
// PSC = 83, ARR = 999
// 확인: (83+1) × (999+1) = 84000 ✓

// 3. Auto-reload preload 활성화
// CubeMX: Auto-reload preload: Enable

문제 3: 시스템이 느려지거나 멈춤

인터럽트로 인해 시스템 전체가 느려지는 경우입니다.

증상

메인 루프가 거의 실행되지 않음
UART 출력이 느림

원인

ISR 실행 시간이 너무 김
ISR 주기가 너무 짧음

해결

// 1. ISR 실행 시간 줄이기
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    // 나쁜 예: 느린 작업
    // printf("Timer\r\n");  // 제거!
    // HAL_Delay(10);        // 절대 안 됨!
    
    // 좋은 예: 빠른 작업
    flag = 1;
    counter++;
  }
}

// 2. 주기 늘리기
// 1ms → 10ms로 변경
// PSC: 839, ARR: 999

// 3. 우선순위 낮추기
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 10, 0);  // 우선순위 낮춤

문제 4: 여러 타이머 사용 시 충돌

여러 타이머를 사용할 때 예상치 못한 동작이 발생합니다.

증상

한 타이머의 콜백만 호출됨

원인 및 해결

// 콜백에서 타이머 구분 확인
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  // 잘못된 예
  if (htim == &htim6)  // 포인터 비교 (위험)
  {
    // ...
  }
  
  // 올바른 예
  if (htim->Instance == TIM6)  // 인스턴스 비교
  {
    // TIM6 처리
  }
  else if (htim->Instance == TIM7)
  {
    // TIM7 처리
  }
}

// 두 타이머 모두 시작했는지 확인
HAL_TIM_Base_Start_IT(&htim6);
HAL_TIM_Base_Start_IT(&htim7);

10. 학습 정리

오늘 배운 내용

  • 타이머 인터럽트의 동작 원리와 설정 방법
  • Prescaler와 ARR을 이용한 정확한 주기 계산
  • 다양한 주기의 타이머 설정 (1ms, 10ms, 100ms, 1s)
  • 타이머 인터럽트를 활용한 실용적 예제
  • 여러 타이머 동시 사용 및 우선순위 관리
  • ISR 최적화 기법 (플래그 방식)
  • 간단한 스케줄러 구현

핵심 개념

1. 타이머 주기 계산

Update Frequency = Timer_Clock / ((PSC + 1) × (ARR + 1))
Period = 1 / Update_Frequency

예: 1ms 주기
  84MHz / (84 × 1000) = 1kHz = 1ms

2. 콜백 함수 작성 원칙

✓ 짧고 빠르게
✓ volatile 변수 사용
✓ 플래그 설정 후 메인 루프에서 처리
✗ HAL_Delay() 사용 금지
✗ printf() 최소화
✗ 복잡한 계산 금지

3. 타이머 선택 가이드
| 타이머 | 용도 | 특징 |
|--------|------|------|
| TIM6, TIM7 | 인터럽트 전용 | Basic Timer, 간단 |
| TIM2, TIM5 | 긴 시간 측정 | 32비트 카운터 |
| TIM3, TIM4 | 범용 | 16비트, PWM 가능 |

주기별 설정 요약

주기PSCARR주파수용도
1ms839991kHz시스템 틱
10ms839999100Hz센서 읽기
100ms839999910Hz디스플레이
1s839999991Hz시계

profile
당신의 코딩 메이트

0개의 댓글