Showing Posts From
인터럽트
- 23 Dec, 2025
타이머 인터럽트 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 잡았다. 뿌듯하다.타이밍 오차는 범인이 여럿이다. 하나씩 잡아야 한다.
- 11 Dec, 2025
인터럽트 우선순위 때문에 3시간을 고민했다
인터럽트 우선순위 때문에 3시간을 고민했다 아침엔 몰랐다 출근했다. 어제 올린 코드 테스트 결과 확인했다. 타이머 값이 이상했다. 100ms마다 찍히는 로그가 98ms, 102ms, 97ms... 들쭉날쭉이었다. '뭐지?' 처음엔 타이머 설정 문제인 줄 알았다. 클럭 소스 확인했다. LSE 32.768kHz. 프리스케일러 확인했다. 맞다. ARR 값도 맞다. 그럼 왜? 커피 마시고 다시 봤다. 여전히 이상했다.인터럽트가 두 개였다 우리 제품은 인터럽트가 두 개 돌아간다.UART 인터럽트 - 센서 데이터 받기 (1ms마다) 타이머 인터럽트 - 메인 로직 (100ms마다)UART는 우선순위 1로 설정했다. 타이머는 2. 숫자 낮을수록 높은 우선순위다. 센서 데이터 놓치면 안 되니까 UART를 높게 뒀다. 문제는 여기서 시작이었다. UART 인터럽트가 타이머 인터럽트를 블락한다. 우선순위가 높으니까. 타이머 인터럽트가 발생해도 UART 처리 끝날 때까지 기다린다. 그래서 타이밍이 밀린 거였다. 오실로스코프 꺼냈다 확인이 필요했다. 프로브 두 개 연결했다.CH1: UART RX 핀 CH2: 디버그용 GPIO (타이머 인터럽트 들어갈 때 HIGH)트리거 걸고 돌렸다. 화면에 보였다. UART 신호 들어올 때 GPIO가 안 올라간다. UART 끝나고 나서야 올라간다. 지연이 0.5ms에서 2ms까지 다양했다. '이거구나.' 센서에서 1ms마다 데이터가 온다. UART 인터럽트 처리에 평균 0.3ms 걸린다. 그런데 타이머 인터럽트 시점이랑 겹치면? 타이머는 기다린다. UART가 끝날 때까지. 100ms 주기인데 매번 랜덤하게 밀리니까 로그가 들쭉날쭉한 거였다.문제는 ADC였다 우리 메인 로직은 타이머 인터럽트에서 ADC 읽는다. 배터리 전압 체크한다. 100ms마다 읽어서 평균내서 상태 판단한다. 그런데 ADC 읽는 타이밍이 밀리면? ADC는 타이밍 크리티컬하다. 샘플링 시점이 중요하다. 특히 우리 회로는 RC 필터 달려있어서 settling time이 필요하다. 정확히 100ms 간격으로 읽어야 한다. 근데 98ms, 102ms 이러니까 값이 튄다. 배터리 90%인데 85%로 보였다가 92%로 보였다가. 양산 들어가면 고객 불만 들어온다. '배터리 표시가 왔다갔다해요.' 리콜이다. 3시간 동안 고민했다 점심 먹고 돌아와서 계속 생각했다. 해결 방법은 세 가지였다. 1. 우선순위를 바꾼다 타이머를 1로, UART를 2로. 그러면 타이머가 먼저 처리된다. ADC 타이밍 문제 해결. 근데 UART 데이터 놓친다. 센서 통신 프로토콜이 타임아웃 100ms다. 데이터 놓치면 센서가 에러 뱉는다. 이것도 문제. 2. 타이머 인터럽트 처리 시간을 줄인다 ADC 읽는 걸 인터럽트에서 빼고 메인 루프로. 인터럽트에선 플래그만 세운다. 근데 메인 루프는 다른 작업도 많다. RTOS 안 쓰는 베어메탈이라 루프 한 바퀴 도는 시간이 일정하지 않다. 이것도 타이밍 보장 안 된다. 3. 설계를 다시 한다 ADC를 DMA로 읽는다. 인터럽트 안 쓰고. 하드웨어 타이머로 트리거 걸어서 자동으로 100ms마다 읽히게. 이게 답이었다. 근데 DMA 설정 다시 해야 한다. 타이머 트리거 설정 해야 한다. 버퍼 관리 다시 짜야 한다. 오후 3시였다. 팀장한테 보고했다. "설계 수정 필요합니다." "얼마나 걸려?" "하루면 됩니다." "내일까지."DMA 설정했다 레퍼런스 매뉴얼 펼쳤다. STM32 RM0090. 1700페이지 중에 DMA 챕터는 10장. 80페이지. 읽었다. 다 읽었다. // ADC를 타이머 트리거로 DMA 읽기 void adc_dma_init(void) { // DMA 클럭 활성화 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // ADC 클럭 활성화 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // 타이머 클럭 활성화 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // DMA 스트림 설정 DMA2_Stream0->CR = 0; while(DMA2_Stream0->CR & DMA_SxCR_EN); // 스트림 비활성화 대기 DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; // 소스: ADC 데이터 레지스터 DMA2_Stream0->M0AR = (uint32_t)adc_buffer; // 목적지: 버퍼 DMA2_Stream0->NDTR = ADC_BUFFER_SIZE; // 전송 개수 // 설정: peripheral to memory, circular mode, 16bit DMA2_Stream0->CR = DMA_SxCR_CHSEL_0 | // 채널 0 DMA_SxCR_MSIZE_0 | // 메모리 16bit DMA_SxCR_PSIZE_0 | // peripheral 16bit DMA_SxCR_MINC | // 메모리 주소 증가 DMA_SxCR_CIRC | // circular mode DMA_SxCR_EN; // 활성화 // ADC 설정 ADC1->CR1 = 0; ADC1->CR2 = ADC_CR2_ADON | // ADC 켜기 ADC_CR2_DMA | // DMA 활성화 ADC_CR2_DDS | // DMA 연속 요청 ADC_CR2_EXTEN_0 | // 외부 트리거 rising edge ADC_CR2_EXTSEL; // TIM2 TRGO // 타이머 설정 (100ms 주기) TIM2->PSC = 8399; // 84MHz / 8400 = 10kHz TIM2->ARR = 999; // 10kHz / 1000 = 10Hz = 100ms TIM2->CR2 = TIM_CR2_MMS_1; // TRGO on update TIM2->CR1 = TIM_CR1_CEN; // 타이머 시작 }컴파일했다. 에러 없다. 올렸다. 안 됐다. 레지스터 순서가 중요했다 문제는 초기화 순서였다. ADC_CR2에 ADON 비트 쓰고 나서 설정을 더 써야 한다. 근데 나는 한 번에 다 썼다. 데이터시트 읽어보니 ADON을 1로 만들고 나서 다른 설정 해야 한다고 나와 있었다. 수정했다. // 순서 중요 ADC1->CR2 = ADC_CR2_ADON; // 먼저 켜고 for(volatile int i=0; i<100; i++); // 안정화 대기 ADC1->CR2 |= ADC_CR2_DMA | // 그 다음 설정 ADC_CR2_DDS | ADC_CR2_EXTEN_0 | ADC_CR2_EXTSEL;다시 올렸다. 됐다. 테스트했다 오실로스코프로 확인했다.CH1: ADC 입력 (배터리 전압) CH2: DMA Transfer Complete 인터럽트 GPIO100ms마다 정확히 찍힌다. 오차 없다. UART 인터럽트 들어와도 상관없다. DMA가 하드웨어로 처리하니까. 로그 찍어봤다. 100.0ms, 100.0ms, 100.0ms... 완벽하다. 배터리 전압도 안정적이다. 90%면 90%, 89%면 89%. 튀지 않는다. 오후 6시였다. 팀장한테 보고했다 "됐습니다." "테스트는?" "오실로스코프로 확인했습니다. 100ms 정확합니다." "좋아. 내일 통합 테스트 넣어." "네." 커피 한 잔 더 마셨다. 네 번째였다. 배운 것 인터럽트 우선순위는 신중하게 정해야 한다. 높은 우선순위가 낮은 우선순위를 블락한다. 당연한 건데 실제로 부딪히니까 다르다. 타이밍 크리티컬한 작업은 인터럽트에 의존하면 안 된다. 특히 다른 인터럯트가 많으면. DMA를 쓰면 CPU 부하도 줄고 타이밍도 보장된다. 하드웨어 트리거 쓰면 더 좋다. 레퍼런스 매뉴얼은 끝까지 읽어야 한다. "Note:" 부분이 중요하다. 거기에 초기화 순서 같은 게 나온다. 3시간 걸렸다. 길다. 하지만 양산 나가서 리콜되는 것보단 낫다. 퇴근 저녁 8시에 나왔다. 일찍 나온 거다. 편의점에서 맥주 샀다. 집 와서 샤워하고 마셨다. 내일은 통합 테스트다. HW팀이랑 같이 실제 배터리 연결해서 돌려본다. 또 문제 나올 수도 있다. 근데 오늘은 해결했다. 그걸로 됐다. 맥주 한 모금 더 마셨다. 시원했다.인터럽트 우선순위, 생각보다 복잡하다.