Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/MCU

[MCU] SysTick 타이머

sm_amoled 2025. 11. 4. 21:00

SysTick 타이머란?

ARMv7-M Architecture Reference Manual 에서 SysTick의 정체에 대해서 찾을 수 있다.

 

SysTick은

  • 메인 프로세서의 CLK을 사용하는 빠른 속도의 타이머
  • SysTick Routine을 매 주기마다 실행하는 RTOS 타이머
  • 연결된 CLK과 Counter 값에 따라 측정 시간을 변경할 수 있는 가변 타이머

등으로 활용할 수 있다.

코어에 있기 때문에, HCLK과 동기화되어 CLK을 받는다고 보면 됨.

SysTick은 왜 프로세서 내부에 있냐?

다른 타이머들(TIM1, TIM2 같은 녀석들)은 다 프로세서 외부에 페리퍼럴로서 존재하는데, 왜 SysTick은 내부로 들고왔는지가 궁금했다.

임베디드 시스템에서 OS의 스케쥴링이나 RTOS 등에서 시간 관리를 위해서는 주기적인 타이머 인터럽트가 필요하다.

만약 시스템 외부에 타이머가 있고, 이 타이머의 인터럽트를 이용한다면 다음의 과정을 거쳐야한다.

  1. 설정에 따라서 타이머 인터럽트가 발생!
  2. APB 버스를 통해서 인터럽트 신호를 전달
  3. APB → AHB 브릿지를 통과 (프로세서에게 가기위해)
  4. 프로세서 내 NVIC 에 도달
  5. CPU가 인터럽트를 처리

이 과정에서 여러가지 이슈가 발생해서 인터럽트가 처리되는 시점이 불규칙해질 수 있다.

  • APB 버스 / AHB 버스를 이미 누가 쓰고있어서 기다려야 한다면?
  • APB - AHB의 CLK 속도 차이 → CLK 변환을 위해 지연 발생 가능
  • 기타 버스 문제로 인터럽트의 도달 시간이 불안정할 수 있다.
  • 1ms 마다 뭔가 처리해야 하는데 1.05ms → 1.2ms → 1.1ms → 0.95ms 이렇게 타이머 간격이 들어오면 제대로 신호를 처리하기 어려움.

만약 이 대신에 주변장치로서가 아닌, 프로세서 내부에 타이머를 넣는다면 버스로 인해 발생하는 문제 없이 바로 처리가 가능하다.

  1. 타이머 인터럽트 발생!
  2. NVIC에 이를 알림
  3. CPU가 인터럽트를 처리

그렇다면 우선순위가 높은 Peripheral 타이머를 만들면 되지 않았나?

라는 생각이 들었다. 주변장치로 CPU에 빠르게 신호를 줄 수 있는 타이머를 만드는게 프로세서의 구조를 바꿔가면서 뭔가 처리하는 것보다 프로세서의 공간도 덜 차지하고 extension으로서 더 좋을 것 같은데..? 라는 생각을 했다.

그런데 이걸 하려면 AHB 같은 버스에다가 System Timer를 위한 우선순위 판단 로직을 설계 + ST 같은 칩 제조사에서 타이머를 구현(제조사마다 구현 방식 달라짐) + 버스를 타고 전달되는 타이머에 대한 검증 복잡 등의 이유로 구현 복잡도가 크게 올라간다. 이걸 사용하는 사람도 칩의 제조사마다 다르게 System 로직을 작성해야함!

그런데, 만약 타이머를 프로세서 내부에 넣는다고 하면 단순히 SysTick 타이머를 위한 레지스터 몇 개랑 카운터, 타이머 인터럽트 시 NVIC으로 신호를 전달하는 선만 연결해주면 된다. 모든 ARM 에서 동일하게 작동하는 코드가 되고 검증도 타이머 모듈에 대해서만 수행하면 되며 시스템 입장에서는 더욱 믿고 쓸 수 있는 타이머가 되기 때문에 오히려 이렇게 하는 편이 더 효율적이라고 판단한 듯 하다.

의외로 STM32F420에는 AHB와 연결된 타이머가 없다. SysTick이 있기 때문일까? 🤔🤔

SysTick의 목적

Cortex-M4 에서는 SysTick 타이머를 RTOS를 위해 사용하는 타이머라고 되어있다. 그치만 일부 상황에서는 카운터로도 활용할 수 있다고 되어있다. 타이머 기능이 필요하다면 굳이굳이 SysTick 타이머를 불러서 사용하지말고, 일반 TIM 들을 사용하자.

SysTick의 제어

  • CTRL 레지스터
    • SysTick의 여러 제어를 위한 Bit 들을 가지고 있다.
    • CLK Source 결정 (AHB or AHB/8)
    • 인터럽트 사용 여부
    • SysTick 활성화
    • 타이머가 0이 되었는지 Flag. 지난번에 이 Flag를 읽었던 시점 이후에 타이머가 0이 되었다면 Set 된다.
  • Reload Value 레지스터
    • 타이머 카운터가 0이 되었을 때 카운터 레지스터에 들어가는 값
    • 24Bit의 값을 담을 수 있다 (0x00000001 ~ 0x00FFFFFF)
  • Current Value 레지스터
    • 현재 카운터 레지스터의 값을 보여준다. (Current Value 레지스터에서 직접 값이 줄어드는게 아니다)
  • Calibration 레지스터
    • 읽기 전용 레지스터
    • 일부 Calibration을 지원하는 프로세서에서, 10ms 를 하려면 몇 CLK을 Reload Value 레지스터에 넣어야하는지 알려준다.

만약 Calibration 레지스터에서 SKEW 값이 1이라면 “알아서 계산해서 만들어라” 라는 의미이다.
이럴때는 TEMMS 비트를 가져가 사용하면 안되고, 차라리 SystemCoreClock 같는 CMSIS 변수에서 값을 가져와 계산해주는게 좋다.

유의할 점

CTRL 레지스터에서 CLK Source를 선택할 때, AHB 또는 AHB/8을 선택할 수 있다. 이때 만약 타이밍의 정확도가 중요하다면 8배나 더 정밀한 AHB를 클럭으로 선택하는게 맞다. 다만 저전력으로 구동하는게 훨씬 더 중요하다거나 최대로 측정할 수 있는 시간이 더 길어야하는 상황이라면 AHB/8를 선택해주면 된다.

안전한 제어를 위해서 SysTick을 사용하는 방법까지 친절하게 안내가 되어있다. 사실 내가 생각하기에 Control 레지스터에서 SysTick Enable bit만 가장 나중에 하고 나머지 설정하는 것은 순서가 상관이 없을 것 같긴한데, 위 순서를 지켜서 작성해주어야 한다.

  1. Reload Value 레지스터 값을 작성할 것
  2. Current Value 레지스터를 초기화할 것
  3. Control 레지스터의 값을 작성할 것

순서 바꿔서 하면 진짜 안되나 팩트체크 해보기

→ 순서 바꿔서 해봤는데, 됨!

아마도 첫 사이클에서는 문제가 발생할 수도 있을 것 같은데, 타이머 자체가 너무 빨라서 문제가 있는 것처럼 보이지 않는 것으로 생각된다.

혹시나 몰라서 레지스터값도 까봤다. 확실한건, 디버그를 찍어서 메모리 값을 확인해보니 처음에 VAL 값을 0으로 초기화해주지 않으면 쓰레기 값을 담고 시작한다.

Document가 분명 Reset Value는 0x00000000 이라고 했을텐데 말이지,,, 이제는 ARM 문서도 믿을 수 없는 증거가 하나 추가되었다. 뭐든지 끝없이 의심해야한다.

 

Reload 레지스터에 넣어주는 값은 원하는 값보다 1 작은 값을 넣어주어야 한다. N CLK을 카운트하고싶다면 N-1 값을 Reload 제리스터에 넣어주어야 한다. 문서에도 그렇게 작성되어있음!

 

SysTick과 RTOS

그래서 이 SysTick이 RTOS와 연관이 있다는데, 어떤 연관이 있는지 살펴보고 넘어가야겠다. 모든 타이머를 주변장치로두고 이 SysTick만 내부로 끌고들어온게 뭔가 이유가 있지 않겠어?

RTOS의 핵심은 Task의 전환이다(라고 나는 생각한다.) 현재 들고있는 Task들을 동시에 관리하면서 제한시간 내에 각 Task를 완료하기. 이게 RTOS의 정수가 아닐까.

이를 위해서는 Task 전환이 필요하다. CPU에게 Task를 전환할 타이밍을 알려주는게 SysTick의 역할이다. RTOS에서는 가장 작은 시간의 단위로 Tick을 쓴다. 그리고 이 한 틱 한 틱을 측정하기 위해 사용하는 것이 바로 SysTick이다. 비유적으로 보자면 RTOS의 CLK 같은 존재가 아닐까.

SysTick에 대한 설명을 다시 한 번 읽어보면, RTOS의 Tick 타이머라는 말과 함께 매 주기마다 SysTick 루틴(SysTick_Handler)을 호출한다고 되어있다.

과연 이름부터 SysTimer가 아니라 SysTick 이였던 이유가 있었군. 나중에 RTOS할 때 SysTick 다시 만나면 반가울 것 같다 ㅋㅋㅋ

 

알고보니 ioc 파일에서 CLK Configuration 쪽에 가보면 Cortex System Timer에 적용해줄 수 있는 Prescaler가 따로 있다. 테스트를 해봤는데, 내가 이후에 코드로 SysTick의 CTRL 레지스터에 Source를 지정하면 해당 세팅으로 변경되는 것을 확인할 수 있었다. 요 Prescaler 값을 지정해주면 SysTick CTRL 레지스터로 바로 꽂힌다!

 

SysTick 써보기 실습!

UART를 활용해서 0.5초마다 . 을 터미널에 출력하는 예제. UART는 USART3을 활용해서 Init 시켜주었다.

Polling 방식

void main() 
{
    ...

  // AHB CLK 16MHz 기준
  // Core 의 클럭 (AHB)를 가져오기 + 이걸 8로 나눈 값을 LOAD에서 사용
  uint32_t scaled_CLK = SystemCoreClock >> 3;

  // LOAD 초기화
  SysTick->LOAD &= ~(0xFFFFFF);
  // scaled_CLK을 그대로 사용하면 1초
  // scaled_CLK을 2로 나눠서 0.5초 간격으로 타이머가 찍히도록 LOAD 레지스터 값 설정
  SysTick->LOAD |= scaled_CLK >> 1;

  // VAL 초기화
  SysTick->VAL &= ~(0xFFFFFF);

  // CTRL 초기화
  SysTick->CTRL &= ~(0x7);

  // Interrupt 사용 안함
  // SysTick->CTRL |= 0x2;

  // SysTick 활성화
  SysTick->CTRL |= 0x1;

  while (1)
  {
        // 폴링 방식으로 구현
        if(SysTick->CTRL >> 16 & 0x1) {

            while(((USART3->SR >> 7) & 0x1) == 0);
            USART3->DR = '.';
        }
  }
}

Interrupt 방식

void main() 
{
    ...

  // AHB CLK 16MHz 기준
  // Core 의 클럭 (AHB)를 가져오기 + 이걸 8로 나눈 값을 LOAD에서 사용
  uint32_t scaled_CLK = SystemCoreClock >> 3;

  // LOAD 초기화
  SysTick->LOAD &= ~(0xFFFFFF);
  // scaled_CLK을 그대로 사용하면 1초
  // scaled_CLK을 2로 나눠서 0.5초 간격으로 타이머가 찍히도록 LOAD 레지스터 값 설정
  SysTick->LOAD |= scaled_CLK >> 1;

  // VAL 초기화
  SysTick->VAL &= ~(0xFFFFFF);

  // CTRL 초기화
  SysTick->CTRL &= ~(0x7);

  // Interrupt 사용
  SysTick->CTRL |= 0x2;

  // SysTick 활성화
  SysTick->CTRL |= 0x1;

  while (1)
  {

  }
}
// stm32f4xx_it.c
// SysTick 카운터 값이 0이 되어서 인터럽트가 발생할 때 호출되는 함수
void SysTick_Handler(void)
{
        while(((USART3->SR >> 7) & 0x1) == 0);
        USART3->DR = '.';
}

그래서 Handler랑 Polling이랑 둘 중에 누가 더 빠르게 반응하냐?

에 대해서 궁금해서 한 번 파형을 측정해봤다. 당연히 Handler가 빠를것이라고 생각했는데, 과연 그게 맞을지 어디 한 번 보자구.

코드는 다음과 같이 구성했다.

1) 모든 레지스터 SET
2) 500ms 정도로 SysTick 타이머를 구동 + 인터럽트 허용
3-1) 인터럽트가 호출하면 PD6을 Toggle (갈색 Channel 1)
3-2) while 문에서 Polling으로 인터럽트 감지하면 PD7을 Toggle (빨간색 Channel 2)

실행 결과는 아래그림과 같이 나왔다.

  • 검은색 Channel 0은 비교군
  • 황토색 Channel 1은 폴링 방식
  • 빨간색 Channel 2는 인터럽트 방식

처음에 봤을 때는 “동시에 동작하는구만” 이라고 생각했는데, 완전 자세히 당겨보니깐 인터럽트를 사용한 방식이 폴링 방식보다 약 3~4마이크로초 정도 (== 0.003밀리초 == 0.000003초) 정도 빠른 것을 확인할 수 있었다. 아마도 while 문에 작업중인 Task가 더 많다거나 여러 인터럽트가 혼잡하게 날아다니는 비상상황을 꾸려봤다면 인터럽트쪽이 훨씬 더 빨랐을지도 모르겠다.

사실 인터럽트가 훨씬 더 빠르기를 기대했는데, 4마이크로초가 튀어나와서 ‘에개’ 라는 생각이 절로 들었다. 4마이크로초요??

320x100