CS452 - Real-Time Programming - Winter 2018
Lecture 12 - AwaitEvent, Clock Server
Pubilc Service Annoucements
-
Due date for kernel 3: 5 February, 2018.
-
Data from kernel 2.
The Hardware in the Trains Lab
32-bit Timer
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
The first three lines of the table below are the part of the ICU
that are used when your kernel is debugged. The remainder, except
the last line, are useful during debugging.
The fast interrupt is not used in our kernels. It's used when
there is so little to do in response to an interrupt that you
don't need to perform a complete context switch.
Hardware priority is something that is widely used when it's worth
saving a few instructions. If you select "vectored interrupts"
then you give the interrupt a priority and an address. The address
is the entry point for that particular interrupt. There is a
register in the ICU containing the address of the highest priority
asserted interrupt, and the kernel uses it directly.
Registers for Basic Operation
Register Name |
Offset |
R/W |
Description |
VICxIntEnable |
0x10 |
R/W |
0: Masked, 1: Enabled |
VICxIntEnClear |
0x14 |
WO |
Clears bits in VICxIntEnable |
VICxIRQStatus |
0x00 |
RO |
One bit for each interrupt source
1 if interrupt is asserted and enabled
|
VICxRawIntr |
0x08 |
RO |
As above but not masked |
VICxIntSelect |
0x0c |
R/W |
0: IRQ, 1: FIQ
Set this to 0x0.
|
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, probably by storing
something in a location only IRG mode can access.
-
Switch to svc mode.
-
Save user state (complete save!).
-
Switch to svc mode.
-
Install kernel state. Only at this point can you access kernel
variables.
-
Switch to IRQ state. You still have the kernel frame pointer.
-
Put "something" into a kernel variable.
-
Switch to svc mode.
-
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.
Steps 1-8 are given in detail so that you can see the main pitfalls.
They are not the only way to accompish what you want, and surely
not the most efficient.
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?
Last gasp of the hardware context switch
I have alluded several times to the IP bug, which occurs when you
fail to save IP. Very infrequently a hardware interrupt occurs
between two instructions
mov ip,sp
stm sp!,{fp,ip,lr}
in every function preamble. The result is an activated task with
another task's stack. In a running train program this bug occurs
every several minutes.
There is a dual bug, the LR bug, in which one task's data gets
another task's instructions. Here is a sequence that produces it.
-
A hardware interrupt occurs.
-
The registers of the interrupted task are pushed onto its
stack.
-
The lr_IRQ overwrites the lr on the stack.
-
When the interrupted task is next scheduled, registers r0-r14
are restored in system mode.
-
Then,
movs pc, lr
restarts execution of the
interrupted task in scv mode.
This almost always works, but very infrequently it crashes. Why?
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
kernel, 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 existing/possible/conceivable 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.
- Hardware knowledge split between Notifier and kernel
-
In the future the kernel may need to do too much.
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.
-
When the time is increased by a tick?
Generalizing
-
There should (almost) always be a task blocked on AwaitEvent for every
interrupt type. 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:
data = request.data;
Reply( notifier );
// 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
An interesting question. How do you get the clock back on
after calling HALT?
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, including setting time to zero.
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 any timed-out tasks
}
}
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: