Using a Serial Device

In this chapter, we will build a root task that interacts with a serial device. Start by navigating to and running this chapter's example, which, so far, doesn't do anything interesting.

cd workspaces/root-task/serial-device
make simulate

The module at device.rs implements a higher-level interface over the serial device's MMIO registers, whose physical base address is:

Our first goal will be to map the serial device's MMIO registers into the root task's address space.

After that, we will set up the root task's access to the serial device's interrupt, whose value is:

Finally, we will implement a simple loop that echoes serial input to serial output.

Step 5.A       

First, add some familiar snippets that we will use for allocating CSlots and kernel objects:

Step 5.B (exercise)       

largest_kernel_ut will be useful for allocating kernel objects whose backing physical addresses don't matter to us, but we must allocate the frame which contains the serial device's MMIO registers at a particular physicall address (SERIAL_DEVICE_MMIO_PADDR). Furthermore, the seL4 API distinguishes between general purpose untyped and device untyped. General purpose untyped is backed by normal memory, and can be used to create any type of object. Device untyped is not backed by normal memory, and can only be used to create frames. See the last two paragraphs of seL4 Reference Manual § 2.4 (Kernel Memory Allocation) for more information. So, we must allocate the serial device MMIO frame from the particular initial device untyped that contains SERIAL_DEVICE_MMIO_PADDR.

Exercice: Identify this initial untyped in the bootinfo. We will need a corresponding sel4::cap::Untyped along with the untyped's physical address (or sel4::UntypedDesc, which contains the physical address) for the next step.

Step 5.C       

The untyped we've identified contains the frame we are targeting, but that frame may be somewhere in the middle of the region of device memory the untyped covers. To allocate the frame at SERIAL_DEVICE_MMIO_PADDR, we must allocate dummy objects from this untyped until its watermark is at SERIAL_DEVICE_MMIO_PADDR.

This trim_untyped function takes the untyped capability, its physical address, the desired physical address, and two empty slots for temporarily holding dummy objects. We need two slots because the kernel resets an untyped's watermark if it has no live children. So, we must always keep one child around so that our progress on advancing the watermark is never lost.

Step 5.D       

device_ut_cap is now primed; the physical address of the next allocation will be SERIAL_DEVICE_MMIO_PADDR.

Exercise: Allocate a small frame object (sel4::cap_type::Granule) from device_ut_cap.

If your sel4::cap::Granule is called serial_device_frame_cap, then the following assertion should succeed:

Step 5.E (exercise)       

Exercise: Using code from Step 4.B, Step 4.C, and Step 4.D, map serial_device_frame_cap into the root task's virtual address space.

You should now be able interact with the serial device's MMIO registers. Try printing "Hello, World!" to the serial console with something like:

where serial_device_mmio_page_addr: *mut _ is a pointer to where the MMIO registers are mapped in the root task's virtual address space.

Step 5.F (exercise)       

Interrupts are delivered to userspace via notifications. A IRQHandler capability represents the authority to manage a particular interrupt. Specifically, an IRQHandler capability (sel4::cap::IrqHandler) has the following methods:

The Rust bindings for these methods are:

The root task spawns with a special IRQControl capability (sel4::cap::IrqControl) which can be used to create IRQHandler capabilities with seL4_IRQControl_Get() (sel4::cap::IrqControl::irq_control_get()).

The intent behind this API is that a highly-privileged component will hold an IRQControl capability, which it will use to distribute more finely-grained IRQHandler capabilities to less privileged components for the interrupts they will manage.

The root task can access its IRQControl capability with sel4::init_thread::slot::IRQ_CONTROL.cap()

Exercise: Use sel4::init_thread::slot::IRQ_CONTROL.cap() to create a sel4::cap::IrqHandler for SERIAL_DEVICE_IRQ.

Step 5.G (exercise)       

Exercise: Create a notification object from largest_kernel_ut and associate it with the IRQHandler you just created using sel4::cap::IrqHandler::irq_handler_set_notification().

Step 5.H (exercise)       

Exercise: Use serial_device: Device, your IRQHandler, and the notification you associated with the interrupt to write a loop that echoes serial input to serial output.

Use serial_device.clear_all_interrupts() and irq_handler_cap.irq_handler_ack() in sequence to clear the interrupt at the device and interrupt controller levels respectively. Note that you should do this at the beginning of the loop in case your loop enters with an interrupt already pending.

Use irq_notification_cap.wait() to wait for hte interrupt.

Use serial_device.get_char() and serial_device.put_char() to read and write data.