| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- CC 로직
- CAN FD
- 임베디드 오디오
- 리눅스 CAN
- i.MX8MP
- 하드웨어 디버깅
- fsl-sai
- 리눅스 커널
- 오디오 클럭
- EASRC
- can_filter
- can-utils
- restart-ms
- EILSEQ
- assigned-clocks
- CAN 필터
- 임베디드 리눅스
- bus-off
- 커스텀 보드
- 타임베이스
- TIM17
- xHCI
- 44.1kHz
- HAL_Delay
- vcan
- Azure RTOS
- PTN5150A
- SocketCAN
- 임베디드
- error-passive
- Today
- Total
임베디드 일기장
STM32 ThreadX에서 HAL 타임베이스를 SysTick과 분리해야 하는 이유 — CubeMX TIM17 설정의 의미 본문
STM32 ThreadX에서 HAL 타임베이스를 SysTick과 분리해야 하는 이유 — CubeMX TIM17 설정의 의미
jaeuuu 2026. 7. 11. 13:47CubeMX에서 ThreadX(Azure RTOS)를 활성화하면 System Core > SYS의 Timebase Source가 SysTick에서 TIM6이나 TIM17 같은 일반 타이머로 바뀌어 있는 걸 보게 된다. 이게 뭘 하는 설정이고 왜 바꿔야 하는지, 그리고 안 바꾸면 어떤 증상이 나는지를 STM32H563 기준으로 정리한다. 결론은 한 문장이다: SysTick이라는 하나의 인터럽트에 주인이 둘(HAL과 ThreadX)이 붙으면 안 되기 때문이다.
환경
- MCU: STM32H563ZIT6
- RTOS: ThreadX
- 툴: CubeMX, HAL 라이브러리
1. SYS는 페리페럴이 아니다
먼저 헷갈리기 쉬운 것부터. CubeMX의 "SYS"는 칩 안에 존재하는 페리페럴이 아니라 CubeMX가 만든 가상 그룹이다. 디버그 인터페이스(SWD/JTAG), Timebase Source 등 시스템 설정을 묶어놓은 이름일 뿐이다. 그중 Timebase Source가 오늘의 주인공이다.
2. HAL은 자기만의 시계가 필요하다
HAL 라이브러리는 HAL_Delay(), HAL_GetTick(), 그리고 수많은 HAL 함수 내부의 타임아웃 판정을 위해 1ms마다 증가하는 tick 카운터가 필요하다. 이 카운터를 올려줄 인터럽트 소스가 Timebase Source이고, 베어메탈에서는 기본값이 SysTick이다.
/* 베어메탈 기본 구성 */
void SysTick_Handler(void)
{
HAL_IncTick(); /* HAL tick 증가 */
}
3. ThreadX도 SysTick을 원한다
문제는 RTOS다. ThreadX의 시간 의존 동작 전부 — 컨텍스트 스위치의 타임슬라이싱, tx_thread_sleep(), 이벤트 플래그/큐/세마포어의 타임아웃 — 는 주기 인터럽트에서 호출되는 _tx_timer_interrupt()가 구동한다. 그리고 Cortex-M 포트에서 그 주기 인터럽트는 관례적으로 SysTick이다. tick 주파수는 TX_TIMER_TICKS_PER_SECOND 매크로(1000이면 1ms tick)로 정의되는데, 이 상수는 하드웨어를 제어하는 값이 아니라 "실제 SysTick 주기를 반영해서 맞춰 써야 하는" 값이라는 점도 함께 기억해 두자. 둘이 불일치하면 sleep 시간이 통째로 틀어진다.
여기서 충돌이 보인다. SysTick 핸들러 하나를 놓고 HAL은 HAL_IncTick()을, ThreadX는 _tx_timer_interrupt()를 각자 부르고 싶어 한다. 한 핸들러에 둘 다 넣는 것도 이론상 가능하지만, ThreadX가 컨텍스트 스위칭을 위해 SysTick 우선순위와 동작을 자기 기준으로 관리하기 때문에 HAL이 얹혀 있으면 미묘한 타이밍 간섭의 온상이 된다. 그래서 CubeMX는 RTOS 활성화 시 HAL을 다른 타이머로 이사시킨다.
4. 분업 구조
CubeMX가 Timebase Source를 TIM17로 바꾸면 생성 코드가 이렇게 갈라진다.
SysTick → ThreadX 전용 (_tx_timer_interrupt: 스케줄러 tick, sleep, 타임아웃)
TIM17 → HAL 전용 (HAL_IncTick: HAL_GetTick, HAL_Delay, HAL 타임아웃)
/* stm32h5xx_hal_timebase_tim.c (CubeMX 생성) */
void TIM17_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim17); /* → HAL_IncTick() */
}
SysTick과 TIM17은 본질적으로 같은 일(1ms 주기 인터럽트)을 한다. 차이는 출처다. SysTick은 Cortex-M 코어 내장 24비트 타이머로 NVIC를 거치지 않는 전용 exception(#15)이고, TIM17은 STM32 페리페럴 타이머로 NVIC를 통한 일반 인터럽트다. 기능이 같으니 분리하는 이유는 오직 소유권 충돌 회피다.
5. 이 구조에서 조심할 것: HAL_Delay는 인터럽트가 돌아야 풀린다
분리 구조를 이해하면 자연스럽게 따라오는 함정이 있다. HAL_Delay()는 TIM17 인터럽트가 tick을 올려줘야 탈출하는 busy-wait이다. 따라서:
- TIM17보다 높거나 같은 우선순위의 ISR 안에서 HAL_Delay를 부르면 영원히 못 빠져나온다. TIM17 인터럽트가 선점을 못 하니 tick이 멈추기 때문이다.
- 크리티컬 섹션(인터럽트 비활성 구간)에서도 마찬가지다.
- RTOS 환경에서는 스레드 컨텍스트에서 지연이 필요하면
HAL_Delay()가 아니라tx_thread_sleep()을 쓰는 게 맞다. HAL_Delay는 CPU를 붙잡고 도는 busy-wait이라 다른 스레드에게 양보하지 않는다.
6. 보너스: 두 개의 시계가 주는 진단 포인트
이 이중 구조는 디버깅 때 공짜 진단 도구가 된다. 시스템이 이상하게 멈췄을 때:
tx_thread_sleep(1)이 정상적으로 풀린다 → SysTick이 살아 있다 (ThreadX 스케줄러 정상)HAL_Delay(1)이 풀린다 → TIM17이 살아 있다 (HAL 타임베이스 정상)
둘 중 하나만 죽는 경우가 실제로 있고(예: 특정 ISR이 TIM17 우선순위를 막는 경우), 어느 쪽 시계가 멈췄는지로 문제 영역을 절반으로 줄일 수 있다.
정리
- HAL과 ThreadX는 각각 1ms tick 인터럽트가 필요하고, 기본 후보가 둘 다 SysTick이다
- 그래서 CubeMX는 RTOS 사용 시 HAL 타임베이스를 TIM6/TIM17 등으로 옮긴다 — 이 설정이 SYS > Timebase Source다
TX_TIMER_TICKS_PER_SECOND는 실제 tick 주기와 일치시켜야 하는 선언값이다- HAL_Delay는 TIM17 인터럽트 의존이므로 ISR/크리티컬 섹션에서 금지, 스레드에서는 tx_thread_sleep 사용
- 두 개의 독립된 시계는 락업 디버깅 때 유용한 진단 포인트가 된다
STM32H563ZIT6 + ThreadX 프로젝트에서 정리한 내용입니다.