====== VPOS 분석 - 3.인터럽트 ====== VPOS 에서 인터럽트 발생시, 어떻게 처리하는지에 대한 내용을 다루고 있다. ARM 의 경우, 플래시 영역에 인터럽트 벡터 테이블을 만들어 놓고, 인터럽트가 발생하면 자동으로 그 쪽으로 점프하게끔 되어 있다. ====== 인터럽트 벡터 테이블 생성 ====== 부트로더 역할을 하는 vpos_bootloader.S 파일을 보자!! vh_VPOS_Bootloader_Startup: (0x0) b vh_reset // 0x20 (0x4) b vh_undefined_instruction // 0xC4 (0x8) b vh_software_interrupt // 0xC8 (0xC) b vh_prefetch_abort // 0xCC (0x10) b vh_data_abort // 0xD0 (0x14) b vh_not_used // 0xD4 (0x18) b vh_irq // 0xD8 (0x1C) b vh_fiq // 0xDC ... 가장 먼저 vh_reset 이 실행이 되고, 이후 루틴들은 해당 인터럽트가 발생할 때에만 수행된다. 또한 이 루틴들은 프로그램 상에서 호출하는 것이 아니고, CPU 에서 H/W 적으로 자동적으로 해당 인터럽트 주소로 점프한다. 아래의 표를 참고하기 바란다. | 인터럽트 종류 | 점프하는 주소 | | reset | 0x0 | | undefined_instruction | 0x4 | | software_interrupt | 0x8 | | prefetch_abort | 0xC | | data_abort | 0x10 | | not_used | 0x14 | | irq | 0x18 | | fiq | 0x1C | CPU 는 오로지 해당 인터럽트에 정해진 주소(Flash 영역)으로 점프한다. 이 때 만일 각 영역에 제대로된 인터럽트 서비스 루틴이 없을 경우, 시스템은 죽어버린다. 예를 들어 설명하겠다. 앞에서 보인 vpos_bootloader.S 파일을 그대로 Flash 에 write 하면, CPU 가 알고 있는 주소와 일치하기 때문에 우리가 원하는 대로 제대로 인터럽트 서비스 루틴이 수행될 것이다. 하지만, 만일 vpos_bootloader.S 파일을 아래와 같이 vh_VPOS_Bootloader_Startup: b vh_reset b vh_software_interrupt // 순서가 바뀜 b vh_undefined_instruction // 순서가 바뀜 b vh_prefetch_abort b vh_data_abort b vh_not_used b vh_irq b vh_fiq ... 인터럽트 벡터 테이블의 위치가 바뀌어져 있다면, 시스템 수행도중, 'undefined_instruction' 이 발생했을 때, CPU 는 자동적으로 'b vh_software_interrupt' 이 수행되어, 엉뚱하게도 소프트웨어 인터럽트 서비스 루틴이 수행된다. 이 점을 항상 염두해두기 바란다. ====== 인터럽트 서비스 루틴 ====== 여기서는 'undefined_instruction' 인터럽트가 발생했을 때, VPOS 에서 어떤 인터럽트 서비스 루틴이 수행되는지 설명하도록 하겠다. 'undefined_instruction' 인터럽트가 발생하면 자동으로 '0x4' 로 점프한다. (0x4) b vh_undefined_instruction // 0xC4 ---- ① vpos_bootloader.S ... ... ... (0xC4) vh_undefined_instruction: ldr pc, vh_remapped_undefined_instruction // 0x158 ---- ② vpos_bootloader.S ... ... ... (0x30000004) b vk_undef_handler // 0x30001F0C ---- ③ HAL_arch_startup.S ... ... ... (0x30001F0C) void vk_undef_handler(void) ---- ④ Exception_handler.c { printk("Undefined instruction exception.\n"); while(1); } 1 ~ 4 의 순서가 인터럽트가 처리되는 서비스 루틴이다. 여기서는 'undefined_instruction' 인터럽트를 예로 들었지만, 다른 인터럽트도 마찬가지 방식으로 수행된다. ====== 스케줄링 순서 ====== VPOS_start() 에 의해서 가장 처음 스케줄러가 호출되지만, 그 이후로는 타이머 인터럽트에 의해서 스케줄러가 동작하게 된다. 각각의 쓰레드들을 우선순위에 맞는 ready_queue 에 저장하고 나면, 스케줄러는 우선순위가 높은 순서대로 ready_queue 를 검색한다. 가장 높은 우선순위를 가지면서도, head_list 에 있는 쓰레드를 current_thread 로 저장한다. 가장 처음 스케줄러가 호출된 것이라면, 현재의 쓰레드를 저장하고(save_thread) 하고, 계속 수행한다. 곧 이어, 타이머 인터럽트에 의해서 스케줄러가 동작하면, 다시 ready_queue 에서 검색하고, 현재의 쓰레드와 우선순위를 비교하고, 같지 않으면 컨텍스트 스위칭을 한다. 여기서는 타이머 인터럽트가 발생하는 시점부터 쓰레드가 컨텍스트 스위칭을 하는 시점까지를 설명할 것이다. ===== 타이머 인터럽트의 발생 ===== 인터럽트가 발생하면, irq 모드로 전환된다. 또한, 지정되어진 주소(0x18) 로 점프한다. 또한 스택 포인터(sp)가 0x30900000 로 설정된다. (0x18) b vh_fiq // 0xD8 ... ... ... (0xD8) vh_fiq: ldr pc, vh_remapped_fiq // ldr pc, 0x16C ... ... ... (0x30000018) b vh_irq // 0x30000108 ... ... ... (0x30000108) vh_irq: vh_entering_interrupt: sub lr, lr, #4 // r14(lr) 에서 0x4 를 빼서 저장 str sp, =vk_save_irq_mode_stack_ptr // 0x30000228 주소에 현재의 sp 값(0x30900000)을 집어 넣는다 stmfd sp!, {r0-r14}^ // 현재 sp 의 값에서 r0 ~ r14 을 0x308FFFFC ~ 0x308FFFC4 에 저장한다(실제로는 r0 ~ r12) mrs r0, spsr_all // spsr 을 r0 에 저장 stmfd sp!, {r0, lr} // r0 와 r14 의 값을 각각 sp 영역(0x308FFFBC, 0x308FFFC0)에 저장한다 str sp, =vk_save_irq_current_tcb_bottom // 0x3000022C 에 현재의 sp 값(0x308FFFBC)을 집어 넣는다 bl vh_hwi_classifier // 0x3000025E8 ; vh_hwi_classifier 로 점프 ===== vh_hwi_classifier ===== 이제 어셈레벨에서 C 레벨로 옮겨왔다. 아직 irq 모드이다. void vh_hwi_classifier(void) { vk_dd_table[vk_idt_table[vh_rINTOFFSET]].fop_list->vk_interrupt(); // vh_rINTOFFSET = 0x4a000014 로서 s3c2410 user manual 을 보면(p358), 인터럽트의 소스(source)를 찾을 수 있는 레지스터라고 나와있다 } 현재 VPOS 에서는 vk_dd_table 에 총 3개의 디바이스를 등록시킨다. timer, rtc, lcd 가 그것이다. 위의 함수는 vk_dd_table 에 등록된, 6 가지의 오퍼레이션 중에서 인터럽트 핸들러 함수를 실행하고 있다. 여기서는 타이머 인터럽트이므로 vh_timer_interrupt_handler 가 호출된다. int vh_timer_interrupt_handler(void) { vh_rSRCPND = vh_BIT_TIMER; vh_rINTPND = vh_BIT_TIMER; ++(vk_current_thread->cpu_tick); // 초기값은 vk_current_thread->cpu_tick = 0 if(vk_sched_lock==0) vk_scheduler(); // 스케줄러 호출 return 0; } 마지막에 스케줄러를 호출하고, 결국 vk_scheduler() 를 호출한다. 원래의 스케줄러의 루틴을 수행하고, vk_thread_switch_ctx 함수를 호출한다. ===== vk_thread_switch_ctx ===== 함수 이름대로 컨텍스트 스위칭을 하는 역할을 한다. 아직까지도 여전히 irq 모드이다. void vk_thread_switch_ctx(vk_thread_t* from_thread, vk_thread_t* to_thread) { // from_thread : 지금까지 수행한 쓰레드, to_thread : 이제 수행할 쓰레드 unsigned bottom; unsigned dispatcher_arg; if(from_thread->state == RUNNING) from_thread->state = READY; // from_thread->state = 1 로 설정 bottom = (unsigned)(from_thread->tcb_bottom); // 저장된 쓰레드의 tcb_bottom 값(0x30127FB4) 값 저장 vh_save_thread_ctx(bottom); // 함수 호출 to_thread->state = RUNNING; // 현재 수행중인 쓰레드의 상태를 0 을 설정 dispatcher_arg = (unsigned)(to_thread->tcb_bottom); // 현재 수행중인 쓰레드의 tcb_bottom 값(0x30127FB4) 값 저장 vk_scheduler_unlock(); // 스케줄러를 풀어준다 vh_restore_thread_ctx(dispatcher_arg); // 현재 쓰레드를 저장한다 } ===== vh_save_thread_ctx ===== 다음은 어셈블리어로 구현되어 있는 vh_save_thread_ctx 함수이다. 저장된 쓰레드의 tcb_bottom 값을 인자로 받았다. 참고로 여전히 irq 모드다. vh_save_thread_ctx: mrs r1, cpsr_all // cpsr 의 값을 r1 에 저장 bic r1, r1, #0xfffffff0 // r1 에 2 저장 mov r2, #0x2 // r2 에 2 저장 cmps r1, r2 // r1 과 r2 를 비교, 결과가 참이면, ldreq 구문이 실행되고, 거짓이면, ldrne 구문이 실행된다(참고로 지금은 참) ldreq r2, =vk_save_irq_current_tcb_bottom // 0x3000022C 주소의 데이터 값(0x308FFFBC)이 r2(0x2) 와 같으면, 저장한다 ldrne r2, =vk_save_swi_current_tcb_bottom // 0x30000224 주소의 데이터 값(0x308FFFBC)이 다르면, r2 에 저장한다 .rept 17 // 17 번을 반복해서 루프를 돈다, 여기부터 ldr r1, [r2], #4 // r2 가 가리키는 0x308FFFBC 에 저장되어 있는 값 0x10 을 r1 에 저장하고, r2 는 0x308FFFBC 에 4 를 더해, r2 에는 0x308FFFC0 가 들어간다 str r1, [r0], #4 // r0 가 가리키는 0x30127FB4 주소에 r1 의 값(0x10) 을 저장하고, 4 를 더해, r0 에 0x30127FB8 이 들어간다 .endr // 여기까지 mov pc, lr // lr = 0x30000A0B8 위의 루프는 총 17번 동안, 0x308FFFFC 부터의 메모리 영역과 0x30127FF4 부터의 메모리 영역간에 데이터 복사를 한다. ===== vh_restore_thread_ctx ===== 앞에서 호출한 vh_restore_thread_ctx() 는 HAL_arch_startup.S 파일에 구현되어 있다. vh_restore_thread_ctx: mrs r1, cpsr_all // cpsr 의 값을 r1 에 저장 bic r1, r1, #0xfffffff0 // r1 에 3 저장 mov r2, #0x2 // r2 에 2 저장 cmps r1, r2 // r1, r2 비교 mov sp, r0 // sp 에 r0 의 값을 저장, sp 의 주소가 0x30127FB4 로 변경 ldmfd sp!, {r0, lr} // r0 와 r14 레지스터에 sp, 즉 0x30127FB4 와 0x30127FB8 에 저장되어 있는 값을 저장한다 msr spsr_all, r0 // spsr 에 r0 의 값을 저장, spsr = 10 ldmfd sp!, {r0-r14}^ // sp 의 주소(0x30127FBC)에서 부터 저장된 값을 r0 ~ r14 까지 저장(실제적으로 r12 까지) ldreq sp, =vk_save_irq_mode_stack_ptr // r13, 0x30000228, sp 가 0x30000228 와 같으면 (0x30000228 에는 0x3000020C 저장) ldrne sp, =vk_save_swi_mode_stack_ptr // r13, 0x30000220, sp 가 0x30000220 가 아니면 (0x30000220 에는 0x30800000 저장) movs pc, lr // lr 에는 앞에서 저장한 vector->func 값(0x300236B4)이 저장되어 있다, 함수 실행을 위해 점프, 또한 이때 svc -> user 모드로 전환된다 마지막에서 lr 이 가리키는 값은 쓰레드의 함수 주소이므로, 함수를 호출했을 때와 마찬가지로 pc 값이 옮겨진다. 수행중에 타이머 인터럽트가 발생하면, vk_scheduler() 가 호출되어, 앞에서 설명한 절차를 다시 실행하게 된다.