CS452 - Real-Time Programming - Fall 2011

Lecture 6 - ARM Architecture

Pubilc Service Annoucements


Context Switch

Kernel Structure

The kernel is just a function called main( ), which runs forever. It looks like this

main( ) {
  initialize( );  // includes starting the first user task
  FOREVER {
    active = schedule( )
    request = activate( active );
    handle( request );
  }
}

Where is the OS?

All the interesting stuff inside done by the kernel is hidden inside activate( active ).

schedule( ) returns the ready task of the highest priority or nil.

All the interesting stuff inside done by the kernel is hidden inside activate( active )?

  1. transfer of control to the active task
  2. execution to completion of the active task
  3. transfer of control back to the kernel
  4. getting the request

The hard part to get right is `transfer of control'

Context Switch

The context switch is an examnple of the NOP design pattern, which is extremely widespread in actual code. A common, and relevant, example occurs when we call a function

Function Call (gcc calling conventions)

; In calling code
                      ; store values of r0-r3
                      ; load arguments into r0-r3
   bl  <entry point>  ; this treats the pc and lr specially
                      ; lr <- pc, pc <- <entry point>
                      ; r0 has the return value
                      ; r1-r3 have useless junk

; In called code
entry point:
   mov     ip, sp
   stmdb   sp!, {..., fp, ip, lr}
                         ; fp stands in for all registers except scratch, 
                         ; determined by the registers the function uses
   ...
   ldmia   sp, {..., fp, sp, pc}
                         ; exact inverse of bl, mov and stmdb

Note the role of the index pointer (ip), link register (lr) and stack pointer (sp).

Software Interrupt

The above code sets up function execution within the context of the calling task. When we want to run code that is in a different context we use the software interrupt instruction

What happens when it's executed?

  1. r14_svc <- address of the following instruction. This is where the kernel will return to.
  2. SPSR_svc <- CPSR. This saves the mode, condition codes, etc.
  3. CPSR[0:4] <- 0b10011. Supervisor mode.
  4. CPSR[5] <- 0. ARM (not Thumb) state.
  5. CPSR[7] <- 1. Normal interrupts disabled.
  6. PC <- 0x08

The CPU ignores the 24-bit immediate value, which can be used by the programmer as an argument identifying the system call, for example.

; In calling code
   ; Store r0-r3
   ; Put arguments into r0-r3
   ; the kernel entry point must be in 0x028
   swi  n                 ; n identifies which system call you are calling
   ; retrieve return value from r0
   ; r1-r3 have even more useless junk

; In kernel
kernel entry:
; Change to system mode
; Save user state on user stack
; Return to supervisor mode
   ldr    r4, [lr, #-4]    ; gets the request type
                           ; at this point you can get the arguments
                           ; Where are they?
; Retrieve kernel state from kernel stack
; Do kernel work

Questions:

  1. What is above kernel entry?
  2. If you put swi in a wrapper or stub what happens before and after it?
  3. If the request had arguments, how would you get them into the kernel?

    Hint. How does gcc pass arguments into a function?

  4. It might be important that there are two link registers. Which two link registers? Why?
  5. In practice it isn't important. Why not?
  6. This is not the only way to do a context switch. But remember that every correct implementation of a content switch contains either a software interrupt (swi ) or a hardware interrupt.

Suggestions:

  1. Try this first on paper drawing the stack, registers, etc after each instruction
  2. Try coding in baby steps, which is usually a good idea in assembly language.

Try reading this, which uses the NOP design pattern repeatedly to create a context switch step-by-step, with every step testable.


After the Software Interrupt

In the kernel

The order matters

kernel entry:
  1. State on entry
  2. Change to system state
  3. Save the user state
  4. Return to supervisor mode
  5. Get the request into a scratch register
    ldr r3, [lr, #-4]
  6. Retrieve the kernel state, which should not include the scratch registers
  7. Put what you need to in the active task's TD
  8. Some where above you must have picked up the arguments
  9. Return from getNextRequest( active ) and get to work

There is more than one way to do almost everything in this list, and I have chosen this way of describing what is to be done because it's simplest to describe, not because it's best!.


Before the Software Interrupt

After a while it's time to leave the kernel

  1. Schedule the next task to run
  2. Call GetNextRequest( active )

Inside GetNextRequest

  1. Save kernel state on kernel stack
  2. From TD
  3. Set return value by overwriting r0 on user stack
  4. Switch to system mode
  5. Load registers from user stack
  6. Return to supervisor mode
  7. Let it go
    movs   pc, lr

The instruction after this one is normally the kernel entry.


Making the Stub that Wraps swi

For each kernel primitive there must be a function available in usr code: the kernel's API.

What gcc does for you

Before calling Create

  1. gcc saves the scratch registers to memory.
  2. gcc puts the arguments into the scratch registers, and possibly on the stack.

While calling Create

  1. bl to the entry point of Create

While executing Create

  1. gcc saves the registers that it thinks will be altered during execution of the function.
  2. your code gets executed
  3. gcc restores the registers it saved, and only those registers.

Exiting from Create

  1. mov pc, lr, or equivalent, is executed, returning the execution to the instruction following bl

After calling Create

  1. gcc stores register r0, the return value, in the variable, to which the result of Create is assigned.

What the code you write does

  1. Moves the arguments from gcc's locations to whatever convention you choose for your kernel
  2. Does swi n, where n is the code for Create.
  3. Moves the return value from your kernel's conventional location to r0.

Creating a Task

In creating a task you have to do two things

  1. Get and initialize resources needed by the task
  2. Make the task look as if it had just entered the kernel

Things you need to do

Get an unused TD and memory for its stack

Mostly filling in fields in the TD.

  1. task id
  2. stack pointer
  3. SPSR
  4. link register
  5. parent tid
  6. return value
  7. state
  8. install in the ready queues

Must also initialize the stack


The Create Function

You also need a int Create( int priority, void (*code) ( ) ) function to call from user tasks.

Although it's no more than a wrapper there are a few problems to solve.

  1. Passing arguments
  2. Jumping into the kernel
  3. Getting the return value from the kernel and returning it.

What follows just seems to say the same thing again. But we might as well leave it here because some student might find it useful.

How do we implement the API for user code?

  1. This requires a bit of assembly language.
  2. In assembly language all arguments & return values are words.
  3. What happens when Create is called?

What happens when Create returns?

When the caller is next activated,

user code has to get it and put it in the compiler's special place, r0 for gcc.


Return to: