Friday, November 16, 2007

The story Interrupt handling in linux 2.6.11 on ARM.

This post explains how the interrupts are handled in linux kernel, what homework kernel has to do before first interrupt is recieved. We took 2.6.11 kernel as reference, and for architecture specific details we used a ARM11 based devlopment board.

Following conventions are used in this writeup:

  • All 'C' function names are in italics.
  • All 'Assembly' function names and lables are metioned in bold italics.
  • No file names will be mentioned here (with exception of assembly files), use ctags.
  • All details are for uniprocessor system, nothing related to SMP is covered here.
  • Any other convention - ???? :) NO.

We'll cover the whole interrupt stuff in two sections:

  • Interrupt setup - Explanation of Generic and architecture specific setup that kernel does.

  • Interrupt handling - Explanation of what happens after processor recieves an interrupt.

1. Interrupt setup

start_kernel( ) is the first 'C' function that opens its eyes when kernel is booting up. It intializes various subsystems of the kernel, including IRQ system. Intialization of IRQ requires that you have valid vector table in place and you have first level interrupt hadlers in place, both of these things are architecture specifc. Lets setup the vector table first.

start_kernel( ) calls a function called trap_init( ) which does following:

  • Call __trap_init() to setup exception vector table at location 0xffff0000.
  • Flush the icache in range 0xffff0000 to 0xffff0000 + PAGE_SIZE. This is required because __trap_init( ) moves the vector table and vector stubs to 0xffff0000.

Vector table and vector stub code for ARM resides in arch/arm/kernel/entry-armv.S file. In this file you'll find implementaion of __trap_init( ) funtion. Following is a code snippet from entry-armv.S, we'll go in details of this code:

----------------------------------------------------

  1. .equ __real_stubs_start, .LCvectors + 0x200
  2. .LCvectors:
  3. swi SYS_ERROR0
  4. b __real_stubs_start + (vector_und - __stubs_start)
  5. ldr pc, __real_stubs_start + (.LCvswi - __stubs_start)
  6. b __real_stubs_start + (vector_pabt - __stubs_start)
  7. b __real_stubs_start + (vector_dabt - __stubs_start)
  8. b __real_stubs_start + (vector_addrexcptn - __stubs_start)
  9. b __real_stubs_start + (vector_irq - __stubs_start)
  10. b __real_stubs_start + (vector_fiq - __stubs_start)
  11. ENTRY(__trap_init)
  12. stmfd sp!, {r4 - r6, lr}
  13. mov r0, #0xff000000
  14. orr r0, r0, #0x00ff0000 @ high vectors position
  15. adr r1, .LCvectors @ set up the vectors
  16. ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
  17. stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr}
  18. add r2, r0, #0x200
  19. adr r0, __stubs_start @ copy stubs to 0x200
  20. adr r1, __stubs_end
  21. 1: ldr r3, [r0], #4
  22. str r3, [r2], #4
  23. cmp r0, r1
  24. blt 1b

LOADREGS(fd, sp!, {r4 - r6, pc})

-------------------------------------------------------------
In the code snippet given above vector table is between line 4- 10. As we can see this vector contains branch instruction for branching to exception handler code which also resides in same file (arm-entryV.S). Vector table contains the branch instructions for all the exceptions defined in ARM (Undefined instruction, SWI, data abort, prefecth abort, IRQ, and FIQ). The most important thing to note about this vector table is that branch instructions are used for all exceptions except SWI. Using branch instruction instead of loading PC directly with the exceptions handler address makes this code position independent. Since branch instruction take offset (+ive or -ve) from current PC, this code will run fine as long as the offset between vector table instructions and the exception handlers is maintained as desired by this code. And It is assumed here that exception handlers will be at +0x200 offset from starting address of vector table.

__trap_init( ) function copies the vector table at location 0xffff0000 (line 13-17) and copies the exception handlers at 0xffff0200 (line 18-24). Remember that addresses we are talking about here are virtual addresses.

So we are done with setting up vector tables and the exception handlers. If you want then you can hook your exception handler directly to the vector table, so that you bypass all linux interrupt handling code, which is pretty heavy. But if you do so then you'll have to get your hands dirty with all the architecture details which kernel handles beautifully and cleanly.

After setting up vector tables start_kernel ( ) calls init_IRQ() to set up kernel IRQ handling infrastructure, on ARM we have 32 hard interrupts for which kernel kernel sets up the a default desctiptor called bad_irq_desc, which has do_bad_IRQ( ) as IRQ handler. Then init_IRQ( ) calls init_arch_irq( ), here the architecture specfic code has to setup the IRQ handlers for 32 IRQs, if not set then do_bad_IRQ( ) will handle the IRQs. On our reference architecture we setup do_level_IRQ( ) as IRQ handlers of all the IRQs except the system timer IRQ. This is the second place where you can bypass kernel IRQ handlers and hook your IRQ handler directly. If you do'nt have requirement of hooking your IRQ handler here then just let do_level_IRQ( ) handle the IRQs then you can register your IRQ handlers with request_irq() in traditional way, and kernel will call your IRQ handler whenever interrupt occurs hiding all the dirty arch details :)

So now we have our IRQ infrastructure in place, and various modules can register thier IRQ handlers through request_irq(). When you call request_irq( ) kernel appends your IRQ handler to list of IRQ handlers registered for that particular IRQ line, it does not change the exception vector table.

Now lets see what happens after interrupt is recieved.

2. Interrupt Handling

When a IRQ is raised, ARM stops what it is processing ( Asuming it is not processing a FIQ!), disables further IRQs (not FIQs), puts CPSR in SPSR, puts current PC to LR and swithes to IRQ mode, refers to the vector table and jumps to the exception handler. In our case it jumps to the exception handler of IRQ. following is the snippet of code for exception handler code for IRQ (again from entry-armV.S file):

------------------------------------------------------------------------

  1. vector_irq:
  2. ldr r13, .LCsirq
  3. .if \correction
  4. sub lr, lr, #\correction
  5. .endif
  6. str lr, [r13] @ save lr_IRQ
  7. mrs lr, spsr
  8. str lr, [r13, #4] @ save spsr_IRQ
  9. mrs r13, cpsr
  10. bic r13, r13, #MODE_MASK
  11. orr r13, r13, #MODE_SVC
  12. msr spsr_cxsf, r13 @ switch to SVC_32 mode
  13. and lr, lr, #15
  14. ldr lr, [pc, lr, lsl #2]
  15. movs pc, lr @ Changes mode and branches
  16. .long __irq_usr @ 0 (USR_26 / USR_32)
  17. .long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
  18. .long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
  19. .long __irq_svc @ 3 (SVC_26 / SVC_32)

--------------------------------------------------------------------------

In the above snipped a macro called vector_stub has been intensionally expanded to improve readability. So when ARM refers to the vector table it follows the branch and lands up at line 2. At this moment ARM is in IRQ mode, IRQs are disabled, LR contains PC of when interrupt occured and SPSR contains CPSR of when interrupt occured. Since we are in IRQ mode so r13 (SP) is banked, so we load SP with address of a small stack frame that we have created at __temp_irq (.LCsirq contains address of __temp_irq). This stack is only used when we are are in IRQ mode.

In lines 6-8 we save LR and SPSR on the temporary IRQ stack (__temp_irq). And we switch to SVC mode (line 10-12). After this basic setup is done depending on the mode in which ARM was there when interrupt occured we switch to specific handler. We'll assume that ARM was executing in SVC mode, so we'll look ino the details of __irq_svc .

__irq_svc saves r0-12 on SVC mode stack (i.e kernel stack of process which was interrupted), reads LR and SPSR from temporary IRQ stack (__temp_irq) and saves them on SVC mode stack, increments the preemt count and then calls get_irqnr_and_base to find out the IRQ line number. Each architecutre has to provide implementation of get_irqnr_and_base, in which it has to query the Interrupt Controller in ARCH specific way to find out which IRQ line raised this interrupt.

After doing all this __irq_svc calls asm_do_IRQ(), Finally we are out of assembly code, now life will be simpler :). And after asm_do_IRQ returns __irq_svc will restore the state of process which was interrupted.

asm_do_IRQ( ) just calls the IRQ handler that was registered by architecure code (refer to section 1), in our case we had set do_level_irq( ) as interrupt handler for all IRQs except timer IRQ. so for all IRQs except timer do_level_irq( ) will handle our interrupts.

do_level_irq() first ACKs the interrupt, by calling architecure specific ACK function. On our reference architecture we just mask this interrupt line, which means that no IRQs of same IRQ line will be allowed until all the IRQ handlers have completed their job. After this do_level_irq( ) checks whether we have any action registered for this IRQ (the interrupt handler that you register through request_irq ( ) are called actions). If there is an action registered then __do_irq( ) is called which in trun enables the interrupts (remember ARM had disabled it) except current IRQ line, and executes the actions.

After all actions have completed excecuting, IRQ line for which interrupt was raised is unmasked and do_level_irq() returns. After this interrupt handling is complete __irq_svc restores the state of interrupted process and that process lives happily until another interrupt bugs it again :)

That was the overview of interrupt handling in linux on ARM, we hope it was useful :)

- Pankaj And Sripurna.




4 comments:

Unknown said...

Thx pankaj & Sri for posting this valuable info. in this blog apart from your "Biker specific blogs" :) . It is very much useful.

--Ash

gokul said...

Hey Pankaj,

Thank you for posting this. I didnt follow the part in which 1MB + 4MB is being mapped to physical address space. What exactly is happening here and why are we doing this and how is this mapping being used. My understanding was TEXT_ADDR is the physical KERNEL_START_ADDR. In a nutshell can you throw some more light on what is being done in _creat_page_tables? Appreciate you help!

Thanks
Gok

mokkai said...

Hello,
Please give me your contact id.
I want to talk to you.

Thanks
G Hariprasad

DreamKote said...

Thats great explanation...Thanks Pankaj