16–17. SysTick 타이머와 정확한 지연 · 범용 타이머·PWM 출력
빈 루프 delay()는 시간이 부정확합니다. Cortex-M 코어에 내장된 SysTick 타이머로 해결합니다 — 모든 Cortex-M에 똑같이 들어 있는 24비트 카운터라 칩이 바뀌어도 같은 코드가 돕니다. 핵심 패턴은 "1ms마다 인터럽트 → 전역 밀리초 카운터 증가 → 그 카운터로 시간 측정"입니다. 이 한 패턴으로 정확한 지연·주기 실행·타임아웃을 모두 구현합니다. 부호 없는 뺄셈으로 래핑에 안전한 delay_ms도 익힙니다. 범용 타이머(TIM)는 채널·PWM·입력 캡처를 제공합니다. 가장 많이 쓰는 PWM은 켜진 시간의 비율(듀티)을 빠르게 조절해 LED 밝기나 모터 속도를 "아날로그처럼" 제어합니다. TIM2 채널1(PA0)로 1kHz PWM을 만들고 듀티를 조절합니다. 세 핵심 레지스터 PSC(분주)·ARR(주기)·CCR(듀티)의 관계와, APB1 타이머 클럭이 PCLK1의 2배가 되는 함정을 익힙니다.
이 강의에서 배우는 것
- 1SysTick의 구조(24비트 다운 카운터)와 동작을 이해한다
- 2SysTick_Config()로 1ms 주기 인터럽트를 설정한다
- 3SysTick_Handler에서 밀리초 카운터를 증가시킨다
- 4부호 없는 뺄셈으로 래핑에 안전한 delay_ms()를 만든다
- 5SysTick의 24비트 한계와 reload 계산을 안다
- 6범용 타이머의 시간축(PSC·ARR)과 카운터 클럭을 계산한다
- 7PWM 모드1의 동작(CNT vs CCR 비교)을 이해한다
- 8TIM2 채널1로 PWM을 출력하도록 레지스터를 설정한다
- 9CCR로 듀티를, ARR로 주파수를 조절한다
- 10APB1 타이머 클럭이 PCLK1의 2배가 되는 규칙을 안다
16. SysTick 타이머와 정확한 지연
빈 루프 delay()는 시간이 부정확합니다. Cortex-M 코어에 내장된 SysTick 타이머로 해결합니다 — 모든 Cortex-M에 똑같이 들어 있는 24비트 카운터라 칩이 바뀌어도 같은 코드가 돕니다. 핵심 패턴은 "1ms마다 인터럽트 → 전역 밀리초 카운터 증가 → 그 카운터로 시간 측정"입니다. 이 한 패턴으로 정확한 지연·주기 실행·타임아웃을 모두 구현합니다. 부호 없는 뺄셈으로 래핑에 안전한 delay_ms도 익힙니다.
소개
SysTick은 reload 값에서 0까지 내려가고 0에 닿으면 다시 채워지며 인터럽트(SysTick exception)를 냅니다. 코어에 내장돼 어떤 Cortex-M에서도 같은 방식으로 씁니다.
핵심 개념
1) SysTick_Config 와 핸들러
SysTick_Config(SystemCoreClock / 1000u); /* 1ms 마다 인터럽트 (72MHz→72000) */
static volatile uint32_t g_ms = 0;
void SysTick_Handler(void) { g_ms++; } /* 핸들러 이름은 CMSIS 약속 */g_ms 에 volatile 필수. 인터럽트와 메인이 공유하므로 캐싱을 막아야 합니다. 핸들러 이름은 startup 벡터 테이블과 정확히 일치해야 합니다(SysTick_Handler).
2) 래핑에 안전한 delay_ms
static void delay_ms(uint32_t ms) {
uint32_t start = g_ms;
while ((g_ms - start) < ms) { }
}
/* 부호 없는 뺄셈 → g_ms 가 32비트 한계를 넘어 0으로 돌아가도 올바른 경과를 준다 */3) 24비트 한계
SysTick은 24비트라 reload 최댓값 0xFFFFFF. 72MHz에서 한 주기 최대 ~233ms. 그보다 긴 주기는 1ms 틱을 세는 방식으로 만듭니다.
핵심 예제
#include "stm32f10x.h"
#define LED 13u
static volatile uint32_t g_ms = 0;
void SysTick_Handler(void){ g_ms++; }
static void delay_ms(uint32_t ms){ uint32_t s=g_ms; while((g_ms-s)<ms){} }
int main(void){
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH &= ~(0xFu << ((LED-8u)*4u));
GPIOC->CRH |= (0x2u << ((LED-8u)*4u));
SysTick_Config(SystemCoreClock / 1000u); /* 1ms 틱 */
while (1) { GPIOC->ODR ^= (1u << LED); delay_ms(500u); } /* 정확히 500ms */
}래핑 검산: start=0xFFFFFFFF, now=4 → elapsed=5. start=0xFFFFFF00, now=0x100 → elapsed=512. 카운터가 한 바퀴 돌아도 부호 없는 뺄셈이 올바른 경과를 줍니다.
자주 하는 실수
Q. g_ms 가 안 변하거나 최적화로 사라져요.
A. volatile 을 빠뜨렸습니다. 인터럽트 핸들러와 메인이 공유하는 변수는 반드시 volatile 로 선언하세요.
Q. SysTick_Handler 이름을 다르게 지었더니 인터럽트가 안 걸려요.
A. 핸들러 이름은 startup 벡터 테이블과 정확히 일치해야 합니다. CMSIS 표준 이름은 SysTick_Handler 입니다.
Q. 1초 주기를 SysTick 하나로 만들려는데 안 돼요.
A. 24비트 한계로 72MHz에서 한 주기 최대 ~233ms입니다. 긴 시간은 1ms 틱을 세는 방식(g_ms)으로 만드세요.
정리
- SysTick은 모든 Cortex-M에 내장된 24비트 다운 카운터로 이식성이 좋다
- SysTick_Config(SystemCoreClock/1000)으로 1ms 주기 인터럽트를 만든다
- 핸들러에서 volatile 밀리초 카운터를 증가시킨다
- delay_ms는 부호 없는 뺄셈으로 래핑에도 안전하다
- 24비트 한계(72MHz에서 ~233ms)는 틱 카운팅으로 넘는다
과제
- delay_ms 없이 g_ms 만으로 "마지막 토글 후 200ms 경과 시 토글"하는 논블로킹 점멸 작성
- g_ms 로 1초마다 카운트를 증가시키는 주기 실행 만들기
- elapsed(now, start) 가 래핑 케이스에서 올바른지 pc_test 로 검산
17. 범용 타이머·PWM 출력
범용 타이머(TIM)는 채널·PWM·입력 캡처를 제공합니다. 가장 많이 쓰는 PWM은 켜진 시간의 비율(듀티)을 빠르게 조절해 LED 밝기나 모터 속도를 "아날로그처럼" 제어합니다. TIM2 채널1(PA0)로 1kHz PWM을 만들고 듀티를 조절합니다. 세 핵심 레지스터 PSC(분주)·ARR(주기)·CCR(듀티)의 관계와, APB1 타이머 클럭이 PCLK1의 2배가 되는 함정을 익힙니다.
소개
디지털 핀은 0/1만 내지만, 켜진 시간 비율을 빠르게 바꾸면 평균 전압이 달라져 아날로그처럼 동작합니다. 타이머 출력은 GPIO의 대체 기능(AF)이라 PA0를 AF 푸시풀(0xB)로 둬야 합니다.
핵심 개념
1) 시간축 — PSC·ARR·CCR
counter_clk = TIMxCLK / (PSC + 1)
pwm_freq = counter_clk / (ARR + 1)
duty(%) = CCR * 100 / (ARR + 1)
/* PSC=71 → 72MHz/72 = 1MHz, ARR=999 → 1kHz, CCR=250 → 25% */타이머 클럭 함정: TIM2~4는 APB1 버스인데, APB1 프리스케일러가 1이 아니면 타이머 클럭은 PCLK1의 2배가 됩니다. 표준 72MHz 설정에서 PCLK1=36MHz지만 TIM2 클럭은 72MHz입니다.
2) PWM 모드1과 설정 순서
PWM 모드1: CNT < CCR 동안 출력 High. 설정 순서는 클럭 → 핀(AF) → 시간축(PSC/ARR) → 듀티(CCR) → 모드(CCMR1 OC1M=110+OC1PE) → 출력(CCER CC1E) → UG(강제 로드) → CEN(시작).
TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1; /* PWM1(110) */
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;핵심 예제
#include "stm32f10x.h"
int main(void){
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
GPIOA->CRL &= ~0xFu; GPIOA->CRL |= 0xBu; /* PA0 = AF 푸시풀 */
TIM2->PSC = 71u; TIM2->ARR = 999u; TIM2->CCR1 = 250u; /* 1kHz, 25% */
TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
TIM2->CCER |= TIM_CCER_CC1E;
TIM2->CR1 |= TIM_CR1_ARPE; TIM2->EGR |= TIM_EGR_UG; TIM2->CR1 |= TIM_CR1_CEN;
while (1) { /* CCR1 을 바꾸면 듀티가 즉시 바뀐다 */ }
}계산 검산: PSC=71,ARR=999,CCR=250 → 1000Hz·25%. CCR=500 → 50%. ARR=499 → 2000Hz. Logic Analyzer에 PORTA.0을 추가하면 듀티 비율을 확인할 수 있습니다.
자주 하는 실수
Q. 핀에서 PWM이 안 나와요.
A. ① PA0를 AF 푸시풀(0xB)로 설정, ② CCER의 CC1E로 출력 켜기, ③ CR1의 CEN으로 카운터 시작 — 세 가지를 확인하세요.
Q. 주파수가 계산의 절반/2배로 나와요.
A. 타이머 클럭을 잘못 잡았습니다. APB1 프리스케일러가 1이 아니면 TIM2 클럭은 PCLK1의 2배(72MHz)입니다. 36MHz로 계산하면 2배 어긋납니다.
Q. 설정을 바꿨는데 첫 주기에 반영이 안 돼요.
A. EGR의 UG 비트로 업데이트 이벤트를 강제하면 PSC/ARR/CCR이 즉시 로드됩니다. 빠뜨리면 다음 업데이트까지 옛 값으로 돕니다.
정리
- 타이머: PSC로 카운터 클럭, ARR로 주기, CCR로 듀티를 정한다
- PWM 모드1은 CNT < CCR 동안 High → 듀티 = CCR/(ARR+1)
- 타이머 출력은 AF 핀(PA0=0xB)으로 내보낸다
- 설정 순서: 클럭 → 핀 → 시간축 → 모드/출력 → UG → CEN
- APB1 타이머 클럭은 PCLK1의 2배(72MHz)임에 주의
과제
- 듀티를 0→100%로 서서히 올렸다 내리는 LED 호흡(breathing) 효과 구현
- PWM 주파수를 500Hz로 바꾸려면 PSC/ARR을 어떻게 할지 계산
- CCR ≥ ARR+1 이면 출력이 항상 High(100%)임을 확인
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침