본문 바로가기
ISR은 어떻게 구현해 - 선점형과 비선점형

ARM에서의 ISR은 어떻게 구현하는가 하는 질문과 의구심은 어떻게 해결할 건가요. ISR은 Interrupt가 걸려 IRQ Exceptin이 발생하여, IRQ Handler로 Branch하여, Interrupt를 처리 하는 거겠죠. 머.
 
일단은 마르고 닳도록 Exception Vector를 다시 한번 보시죠 머.
 
        AREA    INIT_VECTOR, CODE, READONLY   → INIT_VECTOR라는 부분 기억해 두세요.
        CODE32                         ; 32 bit ARM instruction set.

        ENTRY
        B       Reset_Handler                       ; 0x0
        B       Undefined_Handler                 ; 0x4
        B       SWI_Handler                         ; 0x8
        B       Prefetch_Handler                   ; 0xC
        B       Abort_Handler                       ; 0x10
        B       Nothing_Handler                    ; 0x14
        B       IRQ_Handler                          ; 0x18
        B       FIQ_Handler                          ; 0x1C
 
5번째의 0x18번지의 IRQ_Handler로 branch하도록 되어 있지요? 그러면, IRQ_Handler를 구현하면 되겠지요? IRQ_Handler를 구현할 때 주의 할 점은 Pipe line 때문에 돌아갈 때 lr값에서 -4를 해줘서 돌아가야 한다는 사실을 잊지 않았겠지요? 그리고, IRQ mode로 들어서는 순간 SP는 IRQ mode의 SP를 가리키고 있답니다. 잘 기억 안 나면, Microprocessor 아뜰리에를 다시 한번 봐 주셔야겠고요.
 
그러면, IRQ mode로 들어서면서 제일 처음 할 일은
ⓐ lr을 lr=lr-4로 보정해야 해주고요,
ⓑ 이전 mode에서 사용하던 Register들을 backup 해야겠지요.
Software가 해줘야 할 일들이 많아서, 피곤하지요.그러면, 한번 구현해 보지요. 뭐.
 
IRQ_Handler
ⓐ    SUB     lr, lr, #4   
ⓑ    STMFD   sp!, {r0-r12, r14}
 
오홋, 이제 Interrupt후에 어디로 돌아가야 할지, "Context"를 모두 저장한 셈이 되는 거지요. r13을 저장하지 않은 이유는 SPSR을 복구하면 이전 mode의 SP로 자동 복구 되니까, 하지 않은 거에요. 이제 ISR routine으로 Branch하면 됩니다. BL로 다녀오면 되겠고요. 만일 Thumb mode로 ISR routine을 만들었으면 BLX로, ARM mode로 ISR routine을 만들었으면 BL로 가면 되겠네요. 다 구찮으니까, 뭐든지 자동으로 갈 수 있는 BLX로 branch한다고 치면,
 
    LDR r3, =ISR_ROUTINE
    LDR r3, [r3]
    BLX r3
 
요런 식으로 branch해서 다녀오면 되겠사옵니다. 우리가 다루던 식당의 System을 빌려오면, 손님이 왔을 때, Interrupt가 뜰테니까,
 
void customer_isr (void)
{
    send_signal (waiter_task, SRV_SIGNAL)
}
 
요거를 isr로 쓰고 싶으면.
 
   LDR r3, customer_isr
   LDR r3, [r3]
   BLX r3
 
요런 식이 되겠네요. 자, 이제 ISR을 통해서 Interrupt 처리가 끝나고 나면, 돌아 가야 하겠죠.
 
돌아 가는 방법은 아까 저장했던 Context를 다시 복구하면 되는 겝니다.
 
ⓐ LDMFD sp!, {r0-r2, pc}^ ; SPSR을 다시 CPSR로 복구해 주고, lr을 pc에 넣어주는 거죠.
 
전체적인 그림은,
IRQ_Handler
    SUB     lr, lr, #4   
    STMFD   sp!, {r0-r12, r14}
    LDR r3, =customer_isr
    LDR r3, [r3]
    BLX r3
    LDMFD sp!, {r0-r2, pc}^ ; SPSR을 다시 CPSR로 복구해 주고, lr을 pc에 넣어주는거죠.
 
 
이런 식으로 IRQ_Handler를 구성할 수 있는거죠. 그런데, 여기에서__irq라고 지시자를 보신적이 있을텐데 붙이면 뭐가 달라지느냐. 하면
IRQ_Handler를 C로 만들 수 있는 멋쟁이 편리한 지시자인 거에요. C로 IRQ Handler를 직접 작성할 수 있는 지시자이죠. (ARMCC로 Compile해줘야 해요, TCC로 Compile하면 곤란해요. 왜냐하면 아시죠. Exception 걸렸을 때의 ARM의 mode는 ARM mode라는 거 ARM mode의 Assembly를 만들어 내려면 ARMCC로 compile 해줘야 합니다~!)
예를 들어,
 
void __irq IRQ_Handler (void)
{
     customer_isr();
}
 
요런 함수를 만들어 놓으면,
 
IRQ_Handler
    STMFD sp!,{r0-r4,r12,lr}
    BL customer_isr
    LDMFD sp!,{r0-r4,r12,lr}
    SUBS pc,lr,#4
 
요렇게 IRQ mode에 들어왔을 때 해야 할 일들을 앞뒤로 자동으로 만들어 주는 거지요. 이렇게 자동으로 만들어 주는 IRQ Handler는 pc를 보정해 주는 일을 나중에 하는군요. 그리고 register도 최소한의 scratch register들만 stack에 저장해 놓네요. 하지만, 이런 지시자를 썼을 경우에는 SPSR을  CPSR로 보정해 주는 코드는 안 들어 가게 되네요. __irq 지시자는 SPSR을 보정해 주는 코드가 안 들어가 있기 때문에, Interrupt가 걸렸을 당시의 CPSR로는 회기가 안되니까, __irq 지시자를 이용해서, Interrupt Handler를 작성할 때는, 개발하면서 Interrupt가 잘 처리 되는지 Test해 보고 싶을 때, Test Code로는 아주 만점인 지시자에요. 잘 써먹었으면 하는 작은 바램이 있네요. 여기에서 잠깐, 보통은 System에 Interrupt가 한가지만 있지는 않겠죠. 여러 가지 Interrupt가 있을 텐데, SoC에 Interrupt에 관련한 Interrupt Controller가 붙어 있어요. Interrupt Controller가 하는 일은, Interrupt가 들어 왔을 때, ARM에게 IRQ Exception이 들어 왔다는 사실을 알려주고, Interrupt Controller내부 Register안에 어떤 Interrupt가 걸렸는지를 담고 있어요. 그러니까, IRQ mode에 진입한 후에는, ARM SoC (여기서는 MCU)의 Interrupt Controller의 Register를 읽어서 어떤 Interrupt가 걸렸는지 확인 가능한 거죠. 0x8000000 번지에 이런 Interrupt Controller의 Register가 있다는 가정아래, Test용 IRQ_Handler를 만들어 본다면,
 
 
void __irq IRQ_Handler (void)
{
uint32 IRQ_NUM;
utin32 *which_irq = 0x8000000;
IRQ_NUM = *which_irq;    /* 0x8000000를 읽어서 어떤 Interrupt가 걸렸는 지 확인 후,
ISRVector [IRQ_NUM]();  /* 가져 온 IRQ_NUM을 이용해서 해당 ISR을 호출 하는 거지요.
}
요런 식이 되겠죠. ISRVector[] 는 함수를 내용으로 갖는 Array이고요. 각 Interrupt 번호에 맞춘 해당 ISR이 들어 있으면 그 내용을 실행하는 거지요. 이제 완전한 IRQ_Handler가 되었다고 생각하시면 또 한번 오산~! 이에요 으흐. IRQ_Handler구현 시에 조금 더 신경 써 주셔야 하는 부분이 있답니다. 뭐냐하면, mode에요. 아까 언급했던 Handler를 보시면, customer_isr을 ISR_Handler로 이름 바꿔 주고 아래와 같이 Handler를 구현해 주면, 조금 더 완벽하겠죠.
IRQ_Handler
    SUB     lr, lr, #4   
    STMFD   sp!, {r0-r12, r14}
    LDR r3, = ISR_Handler
    LDR r3, [r3]
    BLX r3
    LDMFD sp!, {r0-r2, pc}^ ; SPSR을 다시 CPSR로 복구해 주고, lr을 pc에 넣어주는 거죠.
 
void ISR_Handler (void)
{
uint32 IRQ_NUM;
volatile uint32 *which_irq = (volatile uint32 *)0x8000000;
IRQ_NUM = *which_irq;    /* 0x8000000를 읽어서 어떤 Interrupt가 걸렸는 지 확인 후,
ISRVector [IRQ_NUM]();  /* 가져온 IRQ_NUM을 이용해서 해당 ISR을 호출 하는 거지요.
}
냐하하.  대충의 IRQ_Handler는 이런 식으로 만든다구요. ISRVector는 void pointer 함수 type으로 주르르륵 선언해서 쓰는 거죠. 이런걸 Vector type ISR이라고 불러요.
 

IRQ_Handler의 내용을 너무 길게 짜면 안 된다는 통설이 있어요. IRQ_Handler의 내용을 너무 길게 짜면 다른 Task가 일을 해야 하는 순간에, IRQ_Handler가 CPU를 독점하고 있기 때문에, System이 제때 해야 하는 일들을 못하고 문제를 야기 시킬 수 있어요. 나중에 얘기 하겠지만, 다른 Task들에게 순서가 돌아오지 않아서, Priority가 낮은 Task는 Starvation (배고파서 나가 떨어짐)을 맞을 수가 있어요. 이런 일을 방지 하기 위하여, 보통 Watch dog이라는 Timer를 사용해서 System이 이상해 지는 걸 막는 답니다. 이 Watch dog이라는 녀석은 뒤에 나올 Clock_tick_isr을 짚고 넘어간 후에 다시 한번 만나요~ 그러면 할 일이 많은 IRQ는 어떻게 구현 할까요. 그것 때문에 DPC라는 둥, APC라는 둥, Bottom Half라는 둥 하는 용어가 난무하는 건데요, IRQ자체는 좀 짧게 짜고요, 좀 나중에 해도 되는 일들은 Task Level로 일을 줘서 나중에 처리 하게 하는 거지요. 이것도 볼 꺼니까 걱정마세요. 
 

 중첩이 가능한 ISR을 구현하는 것도 아주 중요한 얘기에요. ISR에서 Nesting이라는 개념이 나오는데, 중첩 = Nesting이라고 보시면 되어요. 결국엔 Interrupt가 걸린 상태에서 Interrupt가 또 걸릴 수 있는 System을 Interrupt가 중첩 가능한 System이라고 부르는데요, 이런 Interrupt 중첩을 위해서 ISR에 진입하면서 Interrupt Enable하면 자연스럽게~ ISR이 동작 중에도 IRQ mode로 진입을 다시 해서 또 다른 Interrupt를 실행 가능하게 해 주는 거죠. 다만, ISR을 구현할 때는 방금 ISR에 진입하게 한 Interrupt를 Clear해주고 들어와야지 안 그러면 무한히 같은 ISR이 계속 걸린다는 말이죠. 이런 Nesting Interrupt를 구현할 때 우선 순위를 이용한 Nesting을 많이 사용하는데요, 왜냐하면 ISR에 순위를 주지 않으면, System이 Interrupt만 하루 종일 처리하다가 꽥 하는 수도 생기니까, ISR에 우선 순위를 주어서 중요한 ISR처리 하는 거지요 ISR에 어떻게 우선순위를 주느냐 하면 Vector Type ISR을 사용하면 가능해 져요. 위의 예는 아주 간단한 경우의 ISRVector[]()이구요. 보통, ISRVector[]()에 높은 순위의 ISR부터 차례로 등록 해 놓고요, ISRVecotr[]()와 짝으로 IRQ_NUM[]을 선언해서 IRQ_NUM[]에는 ISR을 등록해 놓는 거죠. 그러면 Interrupt가 걸리면 IRQ_NUM[]을 loop를 돌면서 Interrupt의 원인을 찾아 내서요 거기에 맞는 ISRVector를 실행하는 거에요. 실행하기 직전에, 방금 걸린 Interrupt를 clear해주고, 그것보다 낮은 순위의 것들을 Disable할 수 있도록 Interrupt Controller에 명령을 주는 거죠. 보통은 이렇게 Interrupt 하나하나에 대해서 Control이 가능하기도 하지만 보통 Level을 주어서 IRQ_NUM 번호에 대해서 Level을 줘서 어느 Level 보다 낮은 순위는 Interrupt가 안 걸리도록 Interrupt Controller에 명령을 줄 수 있는 경우도 있어요. 여하튼 Disable을 한 후에, 해당 ISR을 처리하게 되면 해당 Interrupt보다 순위가 높은 녀석들은 그 위에 또 걸릴 수가 있겠죠. 이게 바로 Nesting되는 거지요. Nesting되었을 경우에 Nesting이 된 ISR을 처리한 후에, 다시 원래 ISR을 처리 하기 위해서는 Nesting되었었다는 근거가 있어야 되는데, 이런 경우에 Counter를 두어서 ISR을 처리 하기 시작할 때 Counter를 증가시켰다가 ISR처리가 끝나면 Counter를 감소시키면 Counter가 0이 되지 않는 한 모든 ISR을 다 처리하지 않았다는 사실을 아니까, Counter가 0이 된 경우에만 IRQ mode에서 빠져나오면서 Scheduling을 시도하면 되요. 보통은 무한히 Nesting시키면 또 System이 Interrupt만 처리하다가 꽥 될 가능성이 있으니까, Nesting도 몇 번 이상 안되도록 Counter를 조절하는 System도 많이 있답니다.

친절한 임베디드 시스템 개발자 되기 강좌 글 전체 리스트 (링크) -



댓글





친절한 임베디드 개발자 되기 강좌 글 전체 리스트 (링크) -