임베디드 학습 노트 목차

타이머와 PWM — 시간을 만드는 하드웨어

delay_ms(500) 은 CPU 를 0.5초 동안 묶어 둔다. 그동안 아무것도 못 한다. 타이머는 CPU 를 쓰지 않고 시간을 세는 하드웨어라, 정확한 주기를 만들면서도 CPU 는 다른 일을 한다.

그리고 PWM 은 타이머의 응용이다 — 디지털 핀으로 아날로그처럼 밝기와 속도를 만든다.


세 레지스터로 주기를 정한다

타이머는 클럭을 세는 장치이고, 프리스케일러가 그 속도를 먼저 나눈다. 계수 속도와 되돌아갈 값이 정해지면 주기가 나오고, 비교값이 듀티를 만든다.

클럭 ──> [PSC 분주] ──> [CNT 증가] ──> ARR 에 닿으면 0 으로 되돌리고 인터럽트
                            │
                            └─ CCR 과 비교해 핀을 켜고 끈다  (PWM)
레지스터역할
PSC (Prescaler)입력 클럭을 나눈다
ARR (Auto-Reload)여기까지 세면 0 으로 되돌린다 → 주기
CNT (Counter)현재 값
CCR (Capture/Compare)이 값에서 핀 상태를 바꾼다 → 듀티
타이머 주파수 = 클럭 / (PSC + 1)
주기          = (ARR + 1) / 타이머 주파수

+1 이 붙는 이유가 자주 헷갈린다. PSC 가 0 이면 나누지 않는다(1로 나눈다)는 뜻이고, ARR 이 0 이면 한 클럭마다 되돌린다는 뜻이다. 0 부터 세기 때문이다.

1ms 주기를 만들어 본다

// 클럭 84MHz 에서 1ms (1kHz) 주기
// 84,000,000 / (8399+1) = 10,000Hz  →  (9+1)/10,000 = 1ms
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

TIM2->PSC = 8399;                 // 84MHz → 10kHz
TIM2->ARR = 9;                    // 10 카운트 = 1ms
TIM2->DIER |= TIM_DIER_UIE;       // 업데이트 인터럽트 허용
TIM2->CR1 |= TIM_CR1_CEN;         // 시작

NVIC_EnableIRQ(TIM2_IRQn);

void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;  // 플래그를 지운다
        (void)TIM2->SR;           // 쓰기가 반영되기를 기다린다
        tick_1ms++;
    }
}

PSC 와 ARR 을 어떻게 나눌지에 요령이 있다. PSC 를 키우면 분해능이 떨어지고, ARR 을 키우면 세밀하지만 16비트(0~65535) 한계에 걸린다. 원칙은 원하는 분해능이 나오는 선에서 ARR 을 크게 잡는 것이다.

왜 ARR 을 크게 잡아야 하나 — 수로 보면 분명하다

같은 1kHz 를 만드는 두 조합을 비교하면 차이가 한눈에 보인다 (84MHz 기준).

조합타이머 클럭실제 주파수duty 단계한 단계
PSC=83, ARR=9991,000,000 Hz1,000.000 Hz1000 단계0.10%
PSC=8399, ARR=910,000 Hz1,000.000 Hz10 단계10.00%

주파수는 똑같이 정확한데, 아래 조합은 duty 를 10% 단위로밖에 못 준다. 모터 속도나 LED 밝기를 0.1% 단위로 조절해야 한다면 위 조합이어야 한다. ARR + 1 이 곧 duty 해상도다.

실무에서 PSC·ARR 을 고르는 순서

  1. 먼저 "딱 떨어지는" 타이머 클럭을 만든다. 84MHz → PSC = 83 이면 정확히 1MHz(1µs tick). 이 습관 하나로 오차가 0 이 된다.
  2. 그 tick 으로 ARR 을 계산한다. ARR = tick / 목표주파수 − 1.
  3. ARR 이 65535 를 넘으면 PSC 를 키워 다시 계산한다(16비트 타이머 기준).
목표PSCARR타이머 클럭실제오차
1 kHz PWM839991 MHz1,000.000 Hz0.0000%
50 Hz 서보8319,9991 MHz50.000 Hz0.0000%
20 kHz 모터(가청 주파수 회피)04,19984 MHz20,000.000 Hz0.0000%
1 Hz LED8,3999,99910 kHz1.000 Hz0.0000%

함정 — "ARR 을 최대한 크게" 만 기계적으로 적용하면 나누어떨어지지 않는 조합이 나온다. 1Hz 를 만들려고 PSC=1281(tick 65,522.6Hz)처럼 어중간한 클럭을 만들면 ARR 을 어떻게 잡아도 정확히 1Hz 가 안 된다. 먼저 딱 떨어지는 tick, 그다음 ARR 순서가 맞다.

PWM — 켜진 비율로 아날로그를 흉내 낸다

ARR = 999 (주기 1000)

CCR = 250        CCR = 500        CCR = 900
 ___              _____            _________
|   |____        |     |___       |         |_
 25% 듀티         50% 듀티          90% 듀티
 (어둡다)         (중간)            (밝다)

LED 는 사람 눈이 따라가지 못할 속도로 켜고 끄면 평균 밝기로 보인다. 모터는 관성 때문에 평균 전압처럼 반응한다. 진짜 아날로그 전압을 만드는 것이 아니라 빠르게 껐다 켜는 것PWM 이다.

// PA0 을 TIM2_CH1 PWM 으로
GPIOA->MODER  |= (0b10 << (0*2));      // 대체 기능
GPIOA->AFR[0] |= (0x1  << (0*4));      // AF1 = TIM2

TIM2->PSC = 83;                         // 84MHz → 1MHz
TIM2->ARR = 999;                        // 1MHz / 1000 = 1kHz PWM
TIM2->CCR1 = 500;                       // 듀티 50%

TIM2->CCMR1 |= (0b110 << 4);            // PWM 모드 1
TIM2->CCMR1 |= TIM_CCMR1_OC1PE;         // 프리로드 — 다음 주기에 반영
TIM2->CCER  |= TIM_CCER_CC1E;           // 채널 출력 허용
TIM2->CR1   |= TIM_CR1_ARPE;
TIM2->EGR    = TIM_EGR_UG;              // 설정을 즉시 로드
TIM2->CR1   |= TIM_CR1_CEN;

// 실행 중 밝기 변경 — 이 한 줄이면 된다
TIM2->CCR1 = 750;                       // 75%

프리로드가 왜 필요한가

CCR 을 주기 중간에 바꾸면 그 주기의 파형이 깨진다. 이미 핀이 꺼진 뒤에 더 큰 값을 넣으면 그 주기는 계속 꺼진 상태로 끝난다 — 순간적인 깜빡임이나 모터 떨림으로 나타난다.

OC1PE(프리로드)를 켜면 새 값이 그림자 레지스터에 대기하다 다음 주기 시작에 반영된다. 파형이 항상 온전하다.

모터·LED 밝기를 부드럽게 바꾸려면 반드시 켠다.

duty 0% 와 100% — 경계에서 한 칸이 어긋난다

ARR = 999 면 카운터는 0~999 로 1000 단계를 센다. duty 를 계산할 때 분모가 ARR 이 아니라 ARR + 1 이라는 점에서 off-by-one 이 자주 난다.

CCRduty실제 파형
00.0%항상 LOW — 비교가 한 번도 성립하지 않는다
10.1%1 카운트만 HIGH (최소 펄스)
50050.0%절반
99999.9%1 카운트만 LOW — 완전한 100% 가 아니다
1000 (= ARR+1)100.0%항상 HIGH — 비교가 항상 성립
// duty(0~100) → CCR 변환. ARR+1 을 곱해야 100% 에서 진짜 항상 HIGH 가 된다
static inline uint32_t duty_to_ccr(uint32_t arr, uint32_t duty_pct) {
    return (arr + 1) * duty_pct / 100;      // duty 100 → arr+1 → 항상 HIGH ✓
}
// ✗ (arr * duty / 100) 로 쓰면 100% 에서 CCR=999 라 1 카운트가 LOW 로 남는다

함정 — 이 1 카운트가 왜 문제가 되나. 모터 드라이버·LED 드라이버 중에는 "완전한 100%" 를 요구하는 것이 있다(부트스트랩 커패시터가 없는 하이사이드 게이트 드라이버 등). 99.9% 에서 남는 짧은 LOW 펄스가 스위칭 노이즈로 나타나거나, 반대로 부트스트랩 충전이 필요한 회로에서는 100%(항상 HIGH)가 오히려 게이트를 못 여는 일도 있다. 데이터시트가 요구하는 쪽으로 맞춰야 한다.

주파수를 어떻게 고르나

용도주파수이유
LED 밝기1kHz 이상낮으면 눈에 깜빡임이 보인다(플리커)
DC 모터15~20kHz가청 주파수 위 — 아니면 모터가 "삐~" 하고 운다
서보 모터50Hz 고정규격이다. 1~2ms 펄스 폭으로 각도를 표현
스위칭 전원100kHz~부품을 작게 하려고

DC 모터에서 1kHz 를 쓰면 소리가 난다. 가청 대역(20Hz~20kHz)에 들어가기 때문이다. 20kHz 이상으로 올리면 조용해지지만 스위칭 손실이 늘어난다.

// 서보 — 50Hz 주기에 1~2ms 펄스
TIM2->PSC = 83;        // 1MHz (1µs 단위)
TIM2->ARR = 19999;     // 20ms = 50Hz
TIM2->CCR1 = 1500;     // 1.5ms = 중앙 (1000=0도, 2000=180도)

1µs 단위로 셀 수 있게 PSC 를 잡은 것이 요점이다. 서보는 펄스 폭이 곧 각도라 분해능이 중요하다.

입력 캡처 — 들어오는 신호의 시간을 잰다

// 초음파 센서의 에코 폭 재기
TIM3->CCMR1 |= (0b01 << 0);            // CC1 을 입력으로, TI1 에 연결
TIM3->CCER  |= TIM_CCER_CC1E;          // 상승 엣지 캡처
TIM3->DIER  |= TIM_DIER_CC1IE;

void TIM3_IRQHandler(void) {
    if (TIM3->SR & TIM_SR_CC1IF) {
        uint32_t now = TIM3->CCR1;      // ← 엣지가 온 '순간의' CNT 값
        if (rising) { start = now; TIM3->CCER |= TIM_CCER_CC1P; }   // 다음은 하강
        else        { width = now - start; TIM3->CCER &= ~TIM_CCER_CC1P; }
        rising = !rising;
    }
}

하드웨어가 엣지 순간의 카운터 값을 붙잡는다는 것이 핵심이다. 소프트웨어로 폴링하면 ISR 진입 지연(12사이클+)만큼 오차가 생기는데, 캡처는 하드웨어가 정확한 순간을 기록하므로 오차가 없다.

now - start 로 계산하면 카운터가 한 바퀴 돌아도(오버플로) 결과가 맞다 — 부호 없는 정수의 순환 성질 덕분이다. 이것이 타이머 값을 다룰 때의 표준 관용구다.

// ✗ 이렇게 비교하면 오버플로에서 깨진다
if (now > start + width) { }

// ✓ 차이를 계산해 비교한다
if ((uint32_t)(now - start) > width) { }

정확한 지연 — delay 를 다시 만든다

// ✗ 클럭·최적화에 따라 시간이 달라진다
void bad_delay(volatile uint32_t n) { while (n--) { } }

// ✓ 타이머 값을 읽어 기다린다 — CPU 는 묶이지만 시간은 정확하다
void delay_us(uint32_t us) {
    uint32_t start = TIM5->CNT;                    // 1µs 단위 타이머
    while ((uint32_t)(TIM5->CNT - start) < us) { }
}

// ✓✓ 아예 안 기다린다 — 슈퍼루프에서 가장 좋은 방법
static uint32_t next_run;
if ((int32_t)(tick_1ms - next_run) >= 0) {
    next_run = tick_1ms + 100;                     // 100ms 마다
    do_work();
}

세 번째가 임베디드다운 형태다. "기다린다" 를 "시간이 됐나 확인한다" 로 바꾸면 루프가 멈추지 않는다. int32_t 로 캐스팅해 비교하는 것이 오버플로에 안전한 관용구다.

SysTick — Cortex-M 이 기본으로 주는 타이머

SysTick_Config(SystemCoreClock / 1000);            // 1ms 마다

volatile uint32_t ms_tick;
void SysTick_Handler(void) { ms_tick++; }

벤더 타이머와 달리 모든 Cortex-M 에 있다. 24비트 감소 카운터라 이식성 있는 시간 기준으로 쓰기 좋고, RTOS 가 이것을 스케줄러 틱으로 쓴다.

RTOS 를 쓴다면 SysTick 을 건드리지 않는다. 이미 커널이 쓰고 있으므로, 별도 시간이 필요하면 벤더 타이머를 쓴다.

워치독 — 멈추면 리셋한다

// 독립 워치독 — 별도 클럭으로 돌아 메인 클럭이 죽어도 동작한다
IWDG->KR  = 0x5555;              // 쓰기 허용
IWDG->PR  = 4;                   // 분주
IWDG->RLR = 1250;                // 약 1초
IWDG->KR  = 0xCCCC;              // 시작

// 메인 루프에서 주기적으로 "살아 있다"
while (1) {
    do_work();
    IWDG->KR = 0xAAAA;           // 리로드 — 이걸 안 하면 1초 뒤 리셋
}

워치독을 어디서 리로드하느냐가 설계다.

// ✗ 타이머 ISR 에서 리로드 — 메인 루프가 멈춰도 워치독은 계속 먹는다
void TIM2_IRQHandler(void) { IWDG->KR = 0xAAAA; }

// ✓ 모든 태스크가 살아 있음을 확인한 뒤에만 리로드
if (task_a_alive && task_b_alive && task_c_alive) {
    task_a_alive = task_b_alive = task_c_alive = false;
    IWDG->KR = 0xAAAA;
}

첫 번째가 흔한 실수다. 워치독이 "시스템이 일을 하고 있다" 가 아니라 "인터럽트가 돌고 있다" 만 확인하게 되어, 메인 루프가 멈춘 채로 영원히 리셋되지 않는다.

실무에서 데는 곳

// ✗ 16비트 타이머에 65536 이상을 넣으면 잘린다
TIM3->ARR = 100000;              // 실제로는 100000 & 0xFFFF = 34464

// ✓ 32비트 타이머를 쓰거나 PSC 로 나눈다
TIM2->PSC = 1;  TIM2->ARR = 49999;

STM32F4 에서 TIM2·TIM5 만 32비트이고 나머지는 16비트다. 데이터시트로 확인해야 한다.

// 설정을 바꿨는데 반영이 안 된다
TIM2->PSC = 100;                 // 다음 업데이트 이벤트까지 적용되지 않는다
TIM2->EGR = TIM_EGR_UG;          // ✓ 강제로 업데이트 — 즉시 반영

PSC 는 그림자 레지스터를 거치므로 즉시 반영되지 않는다. 초기화 마지막에 UG 를 세우는 것이 관용이다.


한눈에 정리

  • PSC·ARR·CCR 세 개로 주기와 듀티가 정해진다. +1 은 0부터 세기 때문
  • PWM 은 진짜 아날로그가 아니라 빠르게 껐다 켜는 것 — 눈과 모터가 평균으로 받아들인다
  • 프리로드(OC1PE)를 켜야 주기 중간의 듀티 변경이 파형을 깨지 않는다
  • DC 모터는 15~20kHz — 가청 대역이면 모터가 소리를 낸다. 서보는 50Hz 고정
  • 입력 캡처는 하드웨어가 엣지 순간을 붙잡아 ISR 지연 오차가 없다
  • 타이머 값 비교는 항상 차이로(uint32_t)(now - start) > n 이 오버플로에 안전하다
  • delay 대신 "시간이 됐나 확인"슈퍼루프가 멈추지 않는다
  • SysTick 은 모든 Cortex-M 에 있다. RTOS 를 쓰면 건드리지 않는다
  • 워치독은 "일을 하고 있음" 을 확인한 뒤 리로드 — ISR 에서 무조건 먹이면 멈춘 시스템을 살려 둔다
  • 16비트 타이머에 65536 이상은 잘린다. STM32F4 는 TIM2·TIM5 만 32비트
  • PSC 변경은 즉시 반영되지 않는다EGR = UG 로 강제한다

꼬리질문 대비

  • "PSC 와 ARR 을 고르는 순서는?" → 먼저 딱 떨어지는 tick(예 1MHz)을 만들고, 그 tick 으로 ARR 을 계산한다
  • "같은 1kHz 인데 duty 해상도가 달라지는 이유는?" → 해상도는 ARR+1 이다. PSC 를 키우고 ARR 을 줄이면 단계 수가 줄어든다
  • "duty 100% 를 만들려면 CCR 에 무엇을 넣나?" → ARR+1(또는 그 이상). ARR 을 넣으면 1카운트가 LOW 로 남는다
  • "주기 중간에 CCR 을 바꾸면?" → 그 주기 파형이 깨진다. 프리로드(OC1PE)를 켜면 다음 주기부터 반영된다

출처 — STMicroelectronics, RM0090 STM32F4 Reference Manual §17~19 타이머(PSC·ARR·CCR·CCMR·EGR)·§21 IWDG · Arm, Cortex-M4 Devices Generic User Guide(DUI 0553) §4.4 SysTick · STMicroelectronics, DS8626 STM32F407 Datasheet — 타이머 폭(16/32비트) 표

인터럽트와 NVIC — 하던 일을 멈추고 달려가기ADC — 아날로그를 숫자로