seL4 Level-1 Tutorials - Exercise #8: I/O Keyboard
Objectives
- Initialize keyboard driver.
- Pass I/O port capabilities of the driver to a child process.
- Customize the I/O operations in the child process.
- Capture input in the child process and send it to the main process.
Background
The seL4 platform support libraries already define a keyboard device driver – capturing input from the keyboard is similar to how we initialized the timer in an earlier exercise. Once I/O operations are set up, it is rather straightforward. All of this is true when operating within the rootserver, which contains bootinfo and sets up allocators with ease.
In order to talk to I/O devices, I/O ports must be established as capabilities. The default behavior for the keyboard driver is to instantiate a capability when a write/read command is requested to/from an I/O port. This requires an initialized allocation environment. Unfortunately, the child process does not have the same information as the rootserver, and setting up allocation in the child process is a lengthy and complicated procedure.
Fortunately, we do not have to strictly follow the driver’s method of allocating capabilities for every read/write request. We can actually override that behavior with a customized one. For a keyboard driver, we already know there are only two I/O ports we need to talk to. Therefore, we can instantiate two capabilities in the rootserver, and pass them to the child process instead. Then, we can customize a cookie and override the read/write requests in the child process in such a way that they do not need any allocation, and just directly use the capabilities received from the rootserver.
Therefore, the alternative implementation allows us to completely initialize the keyboard driver using the rootserver process, and then pass the capabilities needed to communicate with the driver to the child process. The child process is completely blind to any allocation mechanisms, and it only knows how to communicate with the keyboard and capture its input.
Steps
1. Initialize build environment:
$ cd /home/$USER/sel4-files
$ container # if using our environment setup
$ cd level-1-tutorials
$ ./init-build exercise-8 x86
$ cd application/build/exercose-8-x86
$ ninja
# RUN THE FOLLOWING IN A SEPERATE TERMINAL OUTSIDE OF 'container'
$ ./simulate -g '-vga std' # use Ctrl + A ⟶ X to quit qemu
# use Ctrl + Alt + G to release mouse from qemu |
The output should be:
2. Manual initialization of I/O port capabilities (TODO #1 and TODO #2 in main.c). We determined that the control port is 0x64 and the data port is 0x60 for the keyboard device. Accordingly, we will manually allocate those capabilities for TODO #1:
simple_get_IOPort_cap(simple_t *simple,
int start_port, int end_port,
seL4_CPtr cnode_root, seL4_CPtr index,
seL4_Word depth)
- simple: pointer to initialized simple
- start_port: the start or IO port range
- end_port: the end of IO port range
- cnode_root: cspace to place the capability (hint ctrl_path.cptr)
- index: where to place capability within root_cnode (ctrl_path.capPtr)
- depth: depth of each capability bits (seL4_WordBits)
- RETURNS: error code (0 on success) |
Recompile and Simulate. The output should be:
3. Set the control and data port capabilities in the child process (TODO #3 in process.c). The capabilities are already passed to the child process when it is spawned. And the child process already gets a reference to the first passed capability it received. From Exercise-5, we know the first capability is the endpoint capability used for communication. Accordingly, the control and data capabilities come after it. To access them, we can just increment the counter by one and it should work just fine:
((seL4_CPtr)CAPS) + 1; // second capability minted by Main process to this process
((seL4_CPtr)CAPS) + 2; // third capability minted by Main process to this process |
Recompile and Simulate. The output should be:
4. Customize the cookie (TODO #4 in process.c). We have created a custom cookie that only contains two capabilities: control and data port capabilities (port_control and port_data, respectively). The task is to set those capabilities properly in the keyboard_get_io_port_ops function (in process.c).
cookie->port_control
cookie->port_data |
Recompile and Simulate. The output should be:
5. Customize the I/O operations (TODO #5 in process.c). The last part is to let the driver framework understand how to communicate with the keyboard. And it needs to know this information before attempting to communicate with the keyboard. Accordingly, we need to set the in and out functions to our custom functions (defined in process.c), and also set the cookie to our custom cookie. The functions we defined already use the custom cookie format, so they will understand the cookie without issues.
ops->io_port_in_fn
ops->io_port_out_fn
ops->cookie |
Recompile and Simulate. The output should be:
6. Once everything is set up, the keyboard is only set to capture input in the child process (must select Qemu window to capture your input) and pass it to the main process. However, it does not send them character-by-character. Rather, it waits for either a newline character or for the total number of typed characters to exceed 30 before it sends message to the main thread. The main thread will then display the message it received.
Note: read the function init_cdev in process.c: this function uses the I/O operations structure to initialize communication with the keyboard. If the I/O operations structure is invalid, the whole procedure fails. This is why we customized it with our cookie, and with the new execution logic, it has no problem verifying that everything works as intended.
Learned APIs & Structures
- simple_get_IOPort_cap
- ps_io_ops_t
- ps_io_port_ops_t
- ps_chardevice_t