Advanced Operating Systems (LV 7680) -- Milestone description
M1 - A timer driver
- Due: Week 6
- Marks: 4 (-1 per week late, discontinue course if more than one week late)
- Write a simple device driver for the timers available on the platform.
Your first milestone is to write a basic timer driver.
Version Control
We expect that you are using Git to maintain your source code, and that a repository be setup in your group account with the correct permissions and sticky bits set so that you can access it. Comprehensive information on git can be found in this free ebook.
You should consider using a merge tool such as the meld (or equivalent) as the default merge program to avoid painful merges.
Goals
The aim of this milestone is to design and implement device driver to accurately provide time stamps and trigger activities. You should add a file for the timer implementation and modify the main system call loop to handle timer interrupts.
- Learn and understand the fundamentals of writing a device driver.
- Play with real hardware.
- Learn about interrupt handling in seL4.
- Learn about memory mapped device access and control.
- Time system behaviour on seL4.
Motivation
For many purposes, such as benchmarking, a high clock resolution is desired. The i.MX6 Sabre Lite platform features a clock controller module which contains several high-frequency and low-frequency counters which are used as timers. The BeagleBone Black also has suitable timer hardware which is supported in libplatsupport. A high clock resolution timer is desirable for benchmarking, i.e. accurately measuring elapsed time for completion of some unit of work. Using the timer you implement in this milestone, you will be measuring the performance of the file system that you implement in milestone 6. Thus your timer needs to be capable of providing high resolution time stamps.
The Driver Interface
Your driver needs to export the interface specified in libs/libclock/include/clock/clock.h
.
There are the following functions:
int start_timer(seL4_CPtr interrupt_ep)
Initialises the driver. Should set up any interrupts needed by the timer to be delivered to the given endpoint
uint32_t register_timer(uint64_t delay, void (*callback)(uint32_t id, void *data), void *data)
Registers a callback function be called after the specified interval (in microseconds, though actual wakeup resolution will depend on the timer resolution). Several registrations may be pending at any time. The return value is zero on failure, otherwise a unique identifier for this timeout. This identifier can be used to remove a timeout. After a time out has occured, or the timeout has been removed, the identifier may be reused.
int remove_timer(uint32_t id)
Remove a previously registered timer callback, using the unique identifier
returned by register_timer
.
int timer_interrupt(void)
Function that will be called whenever a message is received on the
interrupt_ep
given to start_timer
timestamp_t time_stamp(void)
Returns the current real-time clock value (microsecond accurate).
int stop_timer(void)
Stops operation of the driver. This will remove any outstanding time requests.
The above interface is just an internal function call interface. You do not need to export this interface to the users. User programs will indirectly access the clock driver through the time and sleep syscalls that are implemented in a later milestone.
Note: After registering an interrupt, you must call seL4_IRQHandler_Ack
to ensure the kernel in a sane initial state.
Reference information for thje i.MX6 (SabreLite and Wandboard) can be found here.
Similarly, for the BeagleBone Black, a hardware reference manual is found here.
Although the i.MX6 as well as the am335x is a complex platform with lots
of functionality, we will only be implementing a driver for the Enhanced
Periodic Interrupt Timer (EPIT) and/or General Purpose Timer (GPT) modules.
If you are interested in device drivers for the other components in the
chip, particularly the networking stacks then explore the directory
libs/libethdrivers
.
General Purpose Timer Modules
Your main job is to learn how to program the Board's EPIT and/or GPT timers to generate timer interrupts and how to write an seL4 driver to handle these interrupts.
The i.MX6 as well as the am335x SoC has many peripherals for various purposes, such as GPU, UART, video, watchdog and so on. For our purposes, we are interested in using the timers EPIT1, EPIT2 and GPT, to set them up as our sources of timer interrupts. The EPIT and GPT timer modules have very similar interfaces.
Implementing a device driver really just a matter of learning about its registers, what values to read and write to those registers, and when to do it. You may choose to start with implementing either the EPIT or the GPT timer. This milestone can be implemented with a single or multiple timers.
The minimal subset of a timer module's functionality that you must understand and use is listed below. You may find the registers for GPT are very similar to EPIT. Refer to the SoC manuals for more complete descriptions.
The following is a deliberately short breakdown related to the i.MX6.
Yo are encouraged to explore your specific SoC and other registers for
a deeper understanding. Also, for the BeagleBone, you may find some
interesting hints and sample code in the libclock
library.
- Timer Control Register (EPITxCR, GPTCR) : The actual timer/counters.
You need to use at least one of them (take your pick). You will need
to understand one of these to properly configure and enable the
corresponding timers. As part of the configuration process, you will
need to select an input clock to drive these timers. We recommend the
Peripheral Clock (
ipg_clk
) which you may assume to be configured to 66MHz (see Chapter 18 of the i.MX6 reference manual). - Timer Counter Register (
EPITx_CNR
,GPT_CNT
) : Use this register as the lower 32-bits of your timestamp value. - Timer Load Register (
EPITx_LR
): Contains the value that is to be loaded on EPIT overflow. - Timer Compare Register (
EPITx_CMPR
,GPT_OCRn
): Contains the comparison value which will trigger a timer interrupt. - Timer Status (
EPITx_SR
,GPT_SR
) : When an interrupt occurs, this register provides more information about the source of the interrupt. When you have finished processing the interrupt, you you must clear the appropriate flag in this register and then callseL4_IRQHandler_Ack
to clear the interrupt event in the kernel. Note that writing a '1' to any bit in this register will clear the corresponding flag while writing a '0' will have no effect.
Note: This section is deliberately kept short (e.g., we do not dictate which timer to use or in what mode to use it in). The idea is for you to develop your own design and implementation. There are only two conditions that must be satisfied:
- You must use interrupt(s) generated from the EPIT or GPT timers.
- You must implement the driver interface described above.
Supplied Code
For this project you have been supplied with skeleton code to help you along the way. This code is intended as an implementation guide, not as a 'black-box' library.
It is important that you fully understand all provided code that you use. For the purposes of assessment, we treat any supplied code that you call as your code and as such you may be asked to describe how it works.
Now might be a good time to get familiar with the the framework documentation.
seL4/ARM Interrupts
The seL4/ARM kernel exports specific interrupts to a user level interrupt handler via asynchronous notification.
You will need to perform a series of steps to register to receive interrupts and manage interrupts for the timer device.
You need to create an interrupt handling cap using our cspace library
irq_handler = cspace_irq_control_get_cap(cur_cspace, seL4_CapIRQControl, irq)
(See libsel4cspace documentation and Chapter 3 of the i.MX6 reference manual).We have already created and bound an async endpoint within the framework we have given you. Its cptr is
_sos_interrupt_ep_cap
. You should create a new badged copy of this cap and pass it tostart_timer
, who should then register your new interrupt handler with the kernel and direct it to forward the interrupt to the provided async endpoint. An example of badging is done for network interrupts, see the call tobadge_irq_ep
inmain.c
.
Before attempting this, you should read Chapter 5 of the sel4 documentation to gain an understanding of TCBs, and Section 7.1 to understand how interrupts are delivered.
Device Mappings
In seL4/ARM, device registers are memory mapped. That is, hardware registers
can be accessed via normal load/store operations to special addresses. To
access device registers, you must first map the device into the driver's
virtual address space with the appropriate attributes. A function to
achieve this has been provided for you in apps/sos/src/mapping.c
.
Issues
You may need to resolve some or all of these issues:
- At what address do the timer device registers (e.g. EPIT or GPT for i.MX6) need to be mapped and accessed through?
- What value must be programmed to the timer to get a frequency of x milliseconds?
- How are the interrupts acknowledged?
- Periodic timer ticks or variable length time-outs (a so called tickless kernel)?
- Single or multi-threaded driver?
- Which data structures should I use?
- What race conditions might exist? (imagine the timer hardware as parallel thread of execution). More specifically, how do you derive a 64-bit timer from a 32-bit one.
- What is an acceptable granularity/resolution for timeouts/timestamps? Hint: 100ms is too long, 1ms ticks are too frequent for timer ticks, though your timestamp should be more accurate than 1ms.
- In principle, good device drivers attempt to minimise the length of interrupt handling code.
Assessment
Background
For the remainder of the semester progress through the milestones is contingent on passing the demonstration requirements below, and not having any show stoppers.
In general, we don't mark down for milestones that don't meet minimal requirements. Instead, we'll point out what is required, which groups can then fix, and then submit the following week for a small late penalty. Thus less than perfect project marks usually come about via late penalties, not lower marks for poor solutions.
It's in your best interest to fix problems, rather than letting them snowball into something more problematic as the semester progresses.
Better solutions outlines (only) some potential differentiators of solution quality that are expected to have more favourable marks at the end of the semester, however they are not required.
In general, do not jeopardise the progression of your project by chasing better solutions at the expense of correctness of your project.
Demonstration
You should be able to show some test code that uses all the functions specified in the driver interface. Specifically set up and demonstrate:
- A regular 100ms timer tick. Register a timeout to fire a callback
every 100ms. Then, print the value returned by
time_stamp
every time this callback is received. Note: Your timestamps must be at least accurate to the nearest 10ms. - Register another timeout at a different interval in addition to the 100ms running concurrently (i.e. demo more than one timeout registered at a time).
- Before entering the main system call loop, set up a few calls to
register_timer
. Make sure the delay used is long enough such that the system call loop is entered before these wake up. These callbacks should just print out the current timestamp as each delay expires. You will later useregister_timer
to implement asleep
system call for your user-level processes.
Show Stoppers
Note this is not a complete list. The following designs are considered unsatisfactory:
- Only supporting a single timeout registered at a time.
- Delivering callbacks in the wrong order
- O(n) searches in the interrupt path of the timer interrupt handler.
- Interrupt frequencies greater than 100Hz, if your timer ticks regularly.
- Race conditions when managing the hardware timer.
Better Solutions
The following approaches are considered better than minimum, and favourably contribute to the final project mark.
- Timestamp accuracy to sub-millisecond without increasing the tick rate of the timer.
- A tickless timer where interrupts are only generated when threads need waking (or due to the finite size of the timer).