openlang

TMR3 ISR에서 플래그가 w1c이고 클리어하는데도 캡처 이벤트가 빠져요

리플로우 오븐 컨트롤러예요. Cortex-M0 클론, arm-none-eabi-gcc -O2, Fedora 환경이고요. TMR3는 1 MHz로 돌아요. Update 이벤트가 1 ms tick이고, CH1은 교류 전원 zero-cross를 캡처하고, CH2 compare가 triac을 발화해요.

문제: 50 Hz에서 60초가 지나면 zc_count가 6000 정도여야 해요. 그런데 5900에서 5990 사이로 나오고, 실행할 때마다 달라요. zero-cross를 놓치면 그 반파에는 게이트 펄스가 없어서 히터 출력이 깜빡여요. UART 로그 속도를 올리면 더 심해져요.

확인한 것들:

  • 스코프로 보면 zero-cross 펄스는 깨끗하고 간격도 10 ms로 일정해요. 그러니까 옵토커플러나 노이즈 문제는 아니에요.
  • 카운터는 volatile이에요.
  • 캡처 블록을 tick 블록 위로 옮겼어요. 손실이 덜 나기는 하는데 없어지지는 않아요.
  • TMR3 인터럽트 우선순위를 높여도 달라지는 게 없어요.

이해가 안 돼요. 데이터시트에는 SR 플래그가 write-1-to-clear라고 되어 있고, 저는 각 플래그를 처리하자마자 바로 클리어해요. 플래그가 세트되어 있으면 NVIC가 핸들러에 다시 진입해야 하잖아요. 그런데 플래그가 세트된 이벤트를 어떻게 놓칠 수 있죠?

void TMR3_IRQHandler(void)
{
    uint32_t sr = TMR3_SR;

    if (sr & TMR_SR_UIF) {
        TMR3_SR |= TMR_SR_UIF;
        tick_ms++;
        if ((tick_ms % 500u) == 0)
            log_due = 1;
    }
    if (sr & TMR_SR_CC1IF) {
        TMR3_SR |= TMR_SR_CC1IF;
        zc_count++;
        zc_stamp = TMR3_CCR1;
        TMR3_CCR2 = (zc_stamp + fire_delay_us) & 0xFFFFu;
    }
    if (sr & TMR_SR_CC2IF) {
        TMR3_SR |= TMR_SR_CC2IF;
        TRIAC_ON();
    }
}

SR은 regs.h에서 base + 0x10에 있는 평범한 volatile uint32_t로 정의돼 있어요. 어쩌면 이 클론에 실리콘 quirk가 있을 수도 있는데, 타깃 밖에서는 테스트할 수 없어요. UART 카운터랑 스코프밖에 없어요.

영어에서 번역원문 보기

이어지는 대화 5

서로의 근거를 따라
  1. 고유 링크

    저도 비슷한 일을 겪었어요. 몇 주 동안 쓴 dev board로 만든 실험실 데이터 로거였는데, 이벤트 카운트가 몇 퍼센트 모자라게 나왔어요. 핸들러에 SR |= flag가 있었고요. 이건 현재 세트된 플래그를 전부 읽어서 전부 다시 써요. 그래서 읽기와 쓰기 사이에 올라온 플래그가 처리되지 못한 채 클리어돼요. 제 경우엔 그냥 SR = flag로 바꾸니까 해결됐어요.

    영어에서 번역원문 보기
  2. 고유 링크

    bkt님 말씀에 하나 덧붙이면, 올려주신 핸들러에서는 sr을 맨 위에서 딱 한 번만 읽는데도 손실이 생겨요. TMR3_SR |= TMR_SR_UIF는 새로 읽기를 하니까, 그 첫 읽기 이후에 CC1IF가 올라오면 write-back이 그걸 처리하지 못한 채 클리어해요. 핸들러에 있는 |=는 전부 똑같아요.

    한 가지 주의할 점은, 클론의 레퍼런스 매뉴얼에서 SR이 정말 write-1-to-clear인지 확인해 보시면 좋겠다는 거예요. 어떤 타이머는 write-0-to-clear를 쓰는데, 그런 경우 올바른 형태는 SR = ~flag예요.

    영어에서 번역원문 보기
  3. 고유 링크

    네, 그러면 손실이 설명돼요. 첫 읽기와 |= 쓰기 사이에 window가 있고, UART 부하가 커질수록 그게 넓어진다고 보면 5900에서 5990이라는 값도 맞아요.

    매뉴얼에는 w1c라고 되어 있고, 이건 확인했어요. 그러니까 TMR3_SR = TMR_SR_CC1IF는 그 한 비트만 건드려요. 좋아요.

    질문이 있어요. volatile에 쓰는 |=는 -O2에서 항상 ldr, orr, str로 컴파일되나요? 아니면 GCC가 합쳐 버릴 수도 있나요? GCC volatile 페이지를 읽어 봤는데, 복합 대입에 대해서는 말이 거의 없어요. 핸들러를 바꾸기 전에 objdump로 디스어셈블리를 확인해 볼게요.

    영어에서 번역원문 보기
  4. 고유 링크

    제가 알기로는 GCC가 그걸 없애 버리지는 않아요. volatile 객체에서는 x |= y도 여전히 x를 읽고 x에 써야 하고, 각 접근이 side effect로 취급돼요. Cortex-M0에는 메모리 피연산자를 쓰는 OR 명령이 없으니까 ldr, orrs, str이 보일 거예요(마스크 상수를 위한 ldr이 하나 더 있을 수도 있어요). x86 같은 다른 타깃에서는 컴파일러가 메모리에 대해 or 명령 하나를 쓸 수 있어요. 그래도 read-modify-write인 건 똑같아서 문제는 같아요.

    GCC 페이지가 애매한 건, C 표준이 "무엇이 접근인가"를 구현에 맡겨 두기 때문이에요. 그래서 디스어셈블리를 확인하는 게 맞는 방법이라는 데 동의해요. 오브젝트 파일에 arm-none-eabi-objdump -d를 돌려서 핸들러를 보면 돼요.

    TMR3_SR = TMR_SR_CC1IF;로 바꾼 다음에는 컴파일러가 플래그마다 str을 하나씩 내보내는지도 보세요. 단순 store에는 읽기가 없으니까 윈도우가 남지 않아요.

    그래도 그 뒤에 카운트가 모자라게 나오면, 말씀하신 실리콘 quirk가 다음 용의자가 돼요.

    영어에서 번역원문 보기
  5. 고유 링크

    고마워요. x86에서 메모리에 대한 or도 여전히 read-modify-write라는 건 생각 못 했어요. 거기도 같은 윈도우가 있네요.

    세 군데 전부 단순 store로 바꾸고, 디스어셈블리를 확인한 다음 60초 테스트를 돌릴게요. 그래도 모자라면 다음은 quirk예요.

    영어에서 번역원문 보기

같은 질문의 다른 갈래

  1. D-cache를 켜면 STM32H7 UART TX DMA가 예전 바이트를 보내요

    STM32H7, arm-none-eabi-gcc예요. 64바이트 프레임을 DMA TX로 보내요. 캐시를 끄면 괜찮아요. -DUSE_DCACHE를 켜면 분석기에 잘못된 프레임이 보여요. 리셋 후 첫 프레임은 맞아요. 그다음부터…

    답글 8최근 활동 c
  2. C 콜백에서는 설정해 둔 아레나가 사라진다

    C 라이브러리가 내 Odin 코드로 콜백을 부르는데, 콜백은 등록 전에 설정해 둔 context를 보지 못한다. proc "c"에는 암묵적 context가 없어서 직접 만들어야 하고, runtime.default_contex…

    답글 5최근 활동 odinc
  3. C 포인터에 .?를 쓰는 것은 단언이다

    C에서 돌아온 포인터에 쓰는 .?는 검사가 아니라 단언입니다. 여기서 널은 불가능하다고 말하는 것입니다. 안전 검사 빌드에서 널이면 패닉이 나고, ReleaseFast나 ReleaseSmall에서는 정의되지 않은 동작입니다.…

    답글 3최근 활동 zig