CS452 - Real-Time Programming - Spring 2017
Lecture 12 - AwaitEvent, Clock Server
Pubilc Service Annoucements
-
Due date for kernel 3: 5 June, 2017.
Context Switches for Interrupts
Difference from Software Interrupts
It is impossible to predict where they occur
-
You may have made some assumptions about when they can occur
Assymmetry between User Task and Kernel
Scratch registers of the user task must be saved.
Scratch registers of the kernel need not be saved.
-
assuming that you do not let the optimizer loose on your
context switch.
Four link registers must be put in the correct places.
- One to return from interrupt
-
In the IRQ mode link register when the interrupt handling
code starts,
-
It must be moved to svc mode because that's where it will
be needed for a future context switch. (Don't just assume
that your kernel will crash before the future context
switch. That's an example of a self-fulfilling prediction.)
-
One allowing the interrupting task to return from the function
that was executing when it was interrupted.
-
When the interrupt occurs it is in the link register of
usr mode.
-
It must be saved in a way that gets it back on the usr
mode link register when the interrupted task is next
scheduled.
-
One allowing IRQ mode code to return from whatever function
it is executing when the interrupt occurs.
-
One allowing code executing in svc mode to return from whatever
function called it.
Helpful Features of the ICU
Because the ICU was designed by programmers it
has a few features that are helpful to programmers
-
Several places where you can read state
-
Several places where you can block interrupt flow
-
Trigger hardware interrupt from software
-
What makes interrupts hard is that you are doing two
semi-hard things at once
-
Making the hardware produce the interrupt
-
Responding to the interrupt
-
This allows you to separate them in developing/debugging
The Hardware in the Trains Lab
32-bit Timer
Base address: 0x80810080
The four registers of the timer.
Offset |
Function |
R/W |
Bits |
Comments |
0x0 |
Timer3Load |
R/W |
32: <Load/Reload Value> |
|
0x4 |
Timer3Value |
R |
32:<Current value> |
Set when Load is written,
even when counting |
0x8 |
Timer3Control |
R/W |
32:the lowest order bits are <CLKSEL>xx<MODE><ENABLE> |
<CLKSEL>: 0, 2KHz clock; 1, 508KHz
<MODE>: 1, count continuously; 0, count once
<ENABLE>: Clock turned on
No bit for turning interrupts off and on
|
0xc |
Timer3Clear |
W |
32: |
Writing anything clears the interrupt |
Interrupt Control Unit (ICU)
The actual device is the ARM PL190, and there are two of them
in the SoC. In the Cirrus documentation they are called VIC1
and VIC2.
-
They are wired together (daisy-chained) so that all 64
interrupts assert a single IRQ output which is connected to
the IRQ input of the CPU.
Base addresses
-
VIC1:
0x800B0000
-
VIC2:
0x800C0000
Basic Operation
VIC powers up with
- all vectored interrupts disabled.
- all interrupts giving IRQ
- all interrupts masked
Most likely you will change only the last of them.
Procedure
Initialization
-
leave protection off, unless you are in doubt about your
partner's ethics or competence.
-
enable in VICxIntEnable only when you are ready to handle the
interrupt
On an interrupt, the kernel
- reads the VICxIRQStatus register,
- chooses which interrupt to handle and
- clears the interrupt source in the device.
For debugging
- Use VICxSoftInt and VICxSoftIntClear to turn interrupt sources off and
on in software
Hardware Definitions
Registers for Basic Operation
Register Name |
Offset |
R/W |
Description |
VICxIRQStatus |
0x00 |
RO |
One bit for each interrupt source
1 if interrupt is asserted and enabled
|
VICxFIQStatus |
0x04 |
RO |
As above for FIQ |
VICxRawIntr |
0x08 |
RO |
As above but not masked |
VICxIntSelect |
0x0c |
R/W |
0: IRQ, 1: FIQ |
VICxIntEnable |
0x10 |
R/W |
0: Masked, 1: Enabled |
VICxIntEnClear |
0x14 |
WO |
Clears bits in VICxIntEnable |
VICxSoftInt |
0x18 |
R/W |
Asserts interrupt from software |
VICxSoftIntClear |
0x1c |
WO |
Clears interrupt from software |
VICxProtection |
0x20 |
R/W |
Bit 0 enables protection from user mode access |
VICxVectAddr |
0x30 |
R/W |
Enables priority hardware
See documentation.
|
Non-vectored Operation
Initialization
-
Create user tasks with interrupts enabled.
-
Enable interrupt in device
- Enable interrupt in ICU
-
Enable interrupt in CPU, by MOVS whenever a task is activated.
Normal operation
When a hardware interrupt occurs you find yourself in IRQ
mode. Next, you
-
Remember that you started in IRQ mode.
-
Switch to svc mode.
-
Do the context switch.
-
Look at the active interrupt sources; decide which one you
will service; turn it off.
-
Find which task is blocked waiting for the interrupt.
-
Set up its return value, and make it ready.
Tasks waiting on interrupts are normally high priority, and
usually run right away.
If a second interrupt occurs immediately
-
The CPU checks AND( IRQ, NOT( IRQ disabled bit set in CPSR ) )
before each instruction fetch.
-
If it is asserted an IRQ exception is taken in place of next
instruction fetch.
-
Possibly, zero instructions of the active task are executed.
-
Make sure that this case works
- Context switch into kernel
- Turn off interrupt in device
-
Do you need to turn off the interrupt in the ICU?
AwaitEvent
The final Kernel Primitive
... except for Destroy
int AwaitEvent( int eventType )
-
eventType
identifies the event on which a user
program wishes to wait.
-
In selecting the argument for AwaitEvent, the application
programmer is required to know more about the hardware
than is optimal. For us that's not a problem because we
combine application and system programmer. When they are
split apart, however, the application programmer must
know aspects of the hardware that operating systems
normally hide.
-
Might something like autoconf, establishing an extra level
of indirection, be a solution?
-
The return value contains any volatile data picked up by the
server, and/or error codes.
More About AwaitEvent
Argument
-
Somewhere there is a list of event types
- Application programmer knows the list
- Kernel can respond to each event type on the list
-
This is not very portable
- The list would normally be the union of all types occurring on all
hardware
- This is the Windows problem
Processing in the kernel
- Initialization
- Kernel creates first user task
- Kernel always has
IRQ masked
- Kernel initializes
ICU
- For each device
- Kernel initializes hardware
- Kernel turns on interrupt(s) in the device
- Procedure
- Kernel activates first user task
active = schedule( ) // first user task
nextReq = activate( active );
-
The first user bootstraps the user program into existence
using
Create
;
-
Then in response to an interrupt the kernel
- identifies interrupt source
-
identifies the task waiting on the interrupt, which we
call a Notifier
- acquires volatile data
- re-enables interrupt in the device
- re-enables interrupt in the CPU during task activation (eg,
movs
)
- puts volatile data into
AwaitEvent
's return value
- Makes Notifier ready
- Notifier
- collects and packages data
Send
s to server
- Eventually Server
Reply
s to Notifier
- acts on the data
- Advantage
- Clean consistent user code
- Disadvantage
- Kernel has to know a lot about the hardware.
Clock Server, Task Structure
What does the clock server do?
-
Increments the time every time the timer counts down.
-
Gives out the time.
-
Accepts delay requests; queues delayed tasks; readies delayed tasks
when their delay period is complete.
How is AwaitEvent Used?
AwaitEvent is used to update the time.
-
The event is the timer counting down to zero.
-
When AwaitEvent returns the time is increased by one tick.
Generalizing
-
There should (almost) always be a task blocked on AwaitEvent
for every interrupt type, at least when interrupts are enabled.
Why?
-
A server cannot call AwaitEvent. Why?
-
We call the task that calls AwaitEvent a Notifier. Why?
-
Code for a typical Notifier
struct delivery {
int type;
int data;
}
void notifier( ) {
struct delivery request;
// Initialization, probably including device
// Synchronization
request.type = NOTIFIER;
FOREVER {
request.data = AwaitEvent( evtType );
Send( server, &request, ... );
}
}
-
Code for a typical server
void server( ) {
struct delivery request;
// create notifier
// other initialization
// synchronization
FOREVER {
Receive( &requester, &request, ... );
switch ( request.type ) {
case NOTIFIER:
Reply( notifier );
data = request.data;
// use data
case CLIENT:
...
}
}
}
HALT versus an Idle Task
What do you do when there are no tasks ready to run? Some tasks
are probably blocked on AwaitEvent, and will run as soon as an
interrupt occurs. What do you do?
- Idle task
- lowest priority
- diagnose system: at least % of time idle should be known.
- search for ETI
- HALT
- turns off CPU clock
- save power (battery)
- provided two ways
- through System Controller Co-processor
- through the TS-7200 clock controller
- IRQ path is asynchronous, so it works when the clock is off
Clock Server
Primitives
int Time( )
- Clock server starts at zero when it initializes
- Unit of time is tick
int Delay( int ticks )
- Note error returns
- You might want to add an error for negative arguments
- ticks is usually calculated, and a negative value is an early
warning of falling behind.
int DelayUntil( int ticks )
- Can be constructed from the above two primitives.
Pseudo-implementation
void clock( ) {
// Create Notifier and send any initialization data
// Initialize self
FOREVER {
Receive( &requester, &request, ... );
switch ( request.type ) {
case NOTIFIER:
Reply( notifier, ... )
// update time and check for terminated delays
case TIME_REQUEST:
Reply( requester, time,... )
case DELAY_REQUEST:
// Add requester to list of suspended tasks
}
// Reply to suspended tasks that have timed out
}
}
Comments:
- You need a common request type, or possibly a union.
- You should notice a typical server pattern.
- Notifier updates data
- Client who can be serviced now is serviced
- Client who needs service in the future is suspended
- List of suspended tasks is checked regularly
It's normal to sort the list of suspended tasks. Why?
Return to: