타이머와 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=999 | 1,000,000 Hz | 1,000.000 Hz | 1000 단계 | 0.10% |
PSC=8399, ARR=9 | 10,000 Hz | 1,000.000 Hz | 10 단계 | 10.00% |
주파수는 똑같이 정확한데, 아래 조합은 duty 를 10% 단위로밖에 못 준다. 모터 속도나 LED 밝기를 0.1% 단위로 조절해야 한다면 위 조합이어야 한다. ARR + 1 이 곧 duty 해상도다.
실무에서 PSC·ARR 을 고르는 순서
- 먼저 "딱 떨어지는" 타이머 클럭을 만든다. 84MHz →
PSC = 83이면 정확히 1MHz(1µs tick). 이 습관 하나로 오차가 0 이 된다. - 그 tick 으로 ARR 을 계산한다.
ARR = tick / 목표주파수 − 1. - ARR 이 65535 를 넘으면 PSC 를 키워 다시 계산한다(16비트 타이머 기준).
| 목표 | PSC | ARR | 타이머 클럭 | 실제 | 오차 |
|---|---|---|---|---|---|
| 1 kHz PWM | 83 | 999 | 1 MHz | 1,000.000 Hz | 0.0000% |
| 50 Hz 서보 | 83 | 19,999 | 1 MHz | 50.000 Hz | 0.0000% |
| 20 kHz 모터(가청 주파수 회피) | 0 | 4,199 | 84 MHz | 20,000.000 Hz | 0.0000% |
| 1 Hz LED | 8,399 | 9,999 | 10 kHz | 1.000 Hz | 0.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 이 자주 난다.
| CCR | duty | 실제 파형 |
|---|---|---|
| 0 | 0.0% | 항상 LOW — 비교가 한 번도 성립하지 않는다 |
| 1 | 0.1% | 1 카운트만 HIGH (최소 펄스) |
| 500 | 50.0% | 절반 |
| 999 | 99.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비트) 표