타이머 인터럽트 0.5ms 오차, 누구 탓일까

타이머 인터럽트 0.5ms 오차, 누구 탓일까

타이머 인터럽트 0.5ms 오차, 누구 탓일까

또 0.5ms

오실로스코프 파형을 본다. 타이머 인터럽트가 10ms마다 떨어져야 한다. 근데 10.5ms, 9.8ms, 10.3ms… 춤을 춘다.

클록은 정확하다. 72MHz 크리스털, 오차 ±20ppm. 이론상 0.0002% 오차다. 근데 실제로는 0.5ms씩 흔들린다. 5% 오차다.

양산팀에서 전화 왔다. “타이밍 안 맞아서 통신 끊긴대요.” 그래.

HW팀한테 물어봤다. “클록 확인해보셨어요?” 답: “크리스털 스펙 보내드릴게요.” 본다. 문제없다.

그럼 뭐가 문제야.

클록은 죄가 없다

크리스털은 정확하다. 72MHz HSE, PLL 써서 시스템 클록 만든다. 코드 확인한다.

RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;

문제없다. 레지스터 값도 확인한다. RCC->CR, RCC->CFGR. 전부 정상이다.

타이머 설정도 본다.

TIM2->PSC = 71;  // 72MHz / 72 = 1MHz
TIM2->ARR = 9999;  // 10ms

계산한다. 1MHz 타이머 클록, 10000 카운트면 정확히 10ms다. 근데 오차가 난다.

오실로스코프에 클록 핀 붙여본다. 72MHz 맞다. 주파수 카운터로 재본다. 72.000142MHz. 오차 0.0002%. 문제없다.

그럼 뭐지.

인터럽트 지연

타이머는 정확하게 카운트한다. 근데 ISR이 늦게 실행된다.

타이머 인터럽트 발생 → NVIC 처리 → ISR 진입. 이 사이에 시간이 걸린다.

최악의 경우: 다른 인터럽트가 실행 중이면 기다린다.

우리 프로젝트는 인터럽트가 많다. UART RX, SPI, I2C, ADC, DMA… 우선순위는 대충 정했다.

HAL_NVIC_SetPriority(TIM2_IRQn, 5, 0);
HAL_NVIC_SetPriority(USART1_IRQn, 3, 0);
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 2, 0);

UART가 우선순위 높다. DMA는 더 높다. 타이머는 5번이다.

UART 통신 많으면 타이머 인터럽트가 밀린다. UART ISR이 끝날 때까지 기다린다.

UART ISR 측정해본다. 길면 150us 걸린다. 데이터 파싱 때문이다. 최적화 안 돼 있다.

DMA ISR은 짧다. 20us 정도. 근데 자주 발생한다. ADC 샘플링 1kHz라서.

계산해본다. DMA 1ms당 한 번, 20us씩. UART 불규칙하게, 최대 150us. 겹치면 최대 170us 지연이다.

근데 0.5ms 오차는 설명 안 된다.

메모리가 느리다

Flash에서 코드 실행한다. STM32는 Flash 속도가 느리다. 72MHz에서 2 wait state 필요하다.

명령어 하나 읽는데 3 클록 걸린다. 평균적으로.

ISR 코드 본다.

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR = ~TIM_SR_UIF;
        
        // 여기서 뭔가 한다
        GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        
        counter++;
        if (counter >= 100) {
            flag = 1;
            counter = 0;
        }
    }
}

Flash에서 실행한다. 레지스터 읽기, 쓰기, GPIO 토글, 변수 증가… 전부 Flash에서 명령어 가져온다.

측정해본다. ISR 진입부터 종료까지 8us 걸린다. 명령어 약 200개 정도.

근데 Flash wait state 때문에 실제로는 더 걸린다. 캐시 미스 나면 더 길어진다.

I-Cache 켜져 있다. 근데 ISR은 자주 실행 안 되면 캐시에서 빠진다. 10ms마다 실행이면 캐시 유지 안 될 수도 있다.

다른 태스크들이 Flash 접근하면 캐시 경쟁한다. FreeRTOS 태스크 5개 돌아간다. 각자 코드 실행한다.

결과: ISR 실행 시간이 일정하지 않다. 8us~25us 사이로 흔들린다.

Bus contention

여기서 끝이 아니다.

DMA가 메모리 접근한다. ADC 데이터를 RAM에 쓴다. SPI 수신 데이터도 DMA로 RAM에 쓴다.

DMA와 CPU가 버스를 공유한다. AHB 버스다. 동시 접근하면 누군가 기다린다.

STM32 Reference Manual 본다. DMA는 2 클록 사이클마다 버스 사용 권한 있다. Round-robin 방식이다.

근데 DMA 채널 3개 동시에 돌면? 버스 점유율 높아진다. CPU는 기다린다.

측정해본다. Logic analyzer로 DMA 활동 본다. ADC DMA는 1ms마다 active. SPI DMA는 통신 있을 때만.

겹치는 순간이 있다. ADC + SPI 동시 전송. 이때 CPU가 Flash 접근 못 한다. 지연 발생한다.

계산해본다. DMA 버스 점유 최대 30%. CPU는 70% 시간만 버스 쓴다. 평균적으로.

ISR 실행 중에 DMA active면? ISR 실행 시간 늘어난다. 8us가 12us 된다.

불규칙하다. DMA 타이밍 예측 안 된다. 외부 통신 타이밍 때문이다.

결과: ISR 실행 시간이 8us~25us로 흔들린다.

Clock jitter

크리스털은 정확하다. 근데 완벽하지 않다.

크리스털 오실레이터는 물리적 공진이다. 온도 변하면 주파수 미세하게 변한다. ±20ppm 스펙이다.

측정해본다. 오실로스코프 persistence mode로 클록 본다. 수백 사이클 겹쳐서 본다.

엣지가 완벽하게 겹치지 않는다. ±100ps 정도 흔들린다. 이게 jitter다.

72MHz 클록에서 ±100ps jitter는 ±0.0072 클록 오차다. 무시할 수준이다.

근데 PLL 통과하면? PLL은 jitter를 증폭한다. 이론적으로 10배 정도.

측정 못 한다. 장비 없다. 회사에 있는 오실로스코프는 200MHz 대역폭이다. Jitter 측정 기능 없다.

추정한다. PLL jitter 1ns 정도? 72MHz에서 0.072 클록 오차다.

10ms 타이머면 720000 클록이다. 0.072 클록 오차는 0.1ns다. 무시할 수준이다.

Clock jitter는 원인 아니다.

범인은 복합적

결론: 한 가지 원인 아니다.

타이머는 정확하다. 근데 ISR 실행이 지연된다.

원인 1: 다른 인터럽트. UART ISR 150us, 우선순위 높음.

원인 2: Flash wait state. I-Cache 미스 나면 더 느림.

원인 3: DMA bus contention. CPU가 기다림.

원인 4: ISR 실행 시간 불규칙. 8us~25us 흔들림.

전부 합치면 0.5ms 오차 설명된다.

최악의 시나리오:

  • 타이머 인터럽트 발생
  • UART ISR 실행 중 (150us 대기)
  • ISR 진입
  • Flash cache miss (15us 추가)
  • DMA bus 점유 중 (10us 추가)
  • ISR 실행 느림 (25us)

합계: 200us 지연. 2%다.

근데 이게 누적된다. 10ms마다 최악의 경우 만나면? 확률 낮지만 발생한다.

통계적으로 평균 50us 지연이면? 0.5% 오차다. 10ms → 10.05ms.

측정 결과랑 맞다.

해결 방법

방법 1: ISR 우선순위 높인다.

HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);  // 최우선

근데 UART 통신 놓칠 수 있다. 리스크 있다.

방법 2: UART ISR 최적화한다.

데이터 파싱을 ISR에서 하지 말고 태스크에서 한다. ISR은 버퍼에만 쓴다.

void USART1_IRQHandler(void)
{
    rx_buffer[rx_idx++] = USART1->DR;  // 끝
}

150us → 5us로 줄어든다. 효과 크다.

방법 3: ISR 코드를 RAM에 둔다.

__attribute__((section(".RamFunc")))
void TIM2_IRQHandler(void)
{
    // ...
}

Linker script 수정 필요하다. RAM 공간 확인해야 한다. STM32F103은 RAM 20KB밖에 없다. 아깝다.

근데 효과 있다. Flash wait state 없어진다. ISR 실행 시간 8us → 3us.

방법 4: DMA 우선순위 조정한다.

필요 없는 DMA는 끈다. 우선순위 낮춘다.

// ADC DMA 우선순위 낮춤
DMA1_Channel1->CCR &= ~DMA_CCR_PL;
DMA1_Channel1->CCR |= DMA_PRIORITY_LOW;

버스 경쟁 줄어든다.

방법 5: Hardware timer output 쓴다.

ISR 안 쓴다. 타이머 출력 핀을 직접 토글한다.

TIM2->CCMR1 = TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0;  // Toggle mode

소프트웨어 지연 없다. 하드웨어가 직접 한다. 정확도 높다.

근데 제한적이다. 핀 출력만 된다. 복잡한 로직 안 된다.

적용 결과

UART ISR 최적화했다. 5us로 줄었다.

타이머 ISR을 RAM에 뒀다. 3us로 줄었다. RAM 256바이트 썼다.

DMA 우선순위 조정했다. SPI DMA만 MEDIUM, 나머지 LOW.

측정한다. 오실로스코프로 500번 확인한다.

결과: 10.00ms ± 0.05ms. 오차 0.5%.

양산팀 테스트 통과했다. 통신 안정적이다.

근데 아직 완벽하지 않다. 최악의 경우 0.1ms 오차 있다. 99.9%는 괜찮다.

Trade-off다. 더 정확하려면 RTOS tick 줄이고 인터럽트 더 줄여야 한다. 근데 기능 제약 생긴다.

적당히 타협했다.

교훈

타이밍 오차는 한 가지 원인 아니다. 복합적이다.

크리스털 정확해도 소프트웨어에서 지연 생긴다. 인터럽트, 메모리, 버스… 전부 영향 준다.

측정이 중요하다. 추측하지 말고 오실로스코프로 본다. Logic analyzer 쓴다.

최적화는 단계적으로 한다. 한 번에 여러 개 바꾸면 뭐가 효과 있는지 모른다.

문서 읽는다. Reference Manual, Datasheet. Flash wait state, DMA arbitration… 몰랐으면 못 찾았다.

HW팀이랑 협업한다. 크리스털 스펙, PCB 레이아웃, 전원 노이즈… 하드웨어도 영향 준다.

펌웨어는 하드웨어랑 싸운다. 정확한 타이밍 필요하면 소프트웨어만으로 안 된다. 하드웨어 기능 활용해야 한다.

그래도 0.5ms 잡았다. 뿌듯하다.


타이밍 오차는 범인이 여럿이다. 하나씩 잡아야 한다.