Bonus Assignment 1 - Mouse Driver

Note: this bonus assignment is a bit more work than an average homework. However, it is optional and will leave you with a real, working mouse driver for the xv6 operating system that accomplishes a userful task (copy & paste).

Any operating system worth the name these days will have support for using a mouse or trackpad to interact with it. In this assignment, you will get to see what it takes to implement a driver for mouse support in an OS. By the end, we'll have a mouse driver that will let you move a mouse cursor around the screen, left click to copy a line of text, and right click to paste it.

Because the mouse protocol is somewhat complex, I've simplified things a bit by writing some library functions that handle some of the lower level details of mouse communication. These are available by working off of the bonus_hw branch in git:

$ git clone https://github.com/moyix/xv6-public.git
Cloning into 'xv6-public'...
remote: Counting objects: 4484, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 4484 (delta 11), reused 0 (delta 0), pack-reused 4448
Receiving objects: 100% (4484/4484), 11.69 MiB | 3.79 MiB/s, done.
Resolving deltas: 100% (1779/1779), done.
Checking connectivity... done.
$ cd xv6-public
$ git checkout bonus_hw
Branch bonus_hw set up to track remote branch bonus_hw from origin.
Switched to a new branch 'bonus_hw'

Note that you must use git checkout bonus_hw for the extra library to be available. The new functions you will use are defined in mouse.c:

// Wait until the mouse controller is ready for us to send a packet
void mousewait_send(void);

// Wait until the mouse controller has data for us to receive
void mousewait_recv(void);

// Send a one-byte command to the mouse controller, and wait for it
// to be properly acknowledged
void mousecmd(uchar cmd);

A new constant IRQ_MOUSE has also been added to traps.h, which defines the interrupt number used by the mouse. Its value is 12.

Because interacting with the mouse controller is fairly complicated, I have created a skeleton of the code you should implement in mouse.c. The file defines some useful constants (PS2CTRL, PS2DATA, etc.) at the top of the file, has the implementations of the three functions listed above, and has mouseinit and mouseintr functions that you will fill out.

Part 1 - Mouse Initialization

When the system boots, the mouse is disabled. To enable it, we need to talk to the PS/2 controller and tell it that we would like to activate the mouse. We will also tell it that we want to receive an interrupt whenever the mouse state changes, and set up the interrupt controller to send enable the appropriate interrupt number.

In the following specification, we will be using two I/O ports on the PS/2 controller: the control port, which is port 0x64, and the data port, which is port 0x60.

The initialization procedure to enable the mouse on the PS/2 controller is as follows. Note that each of these steps must be carried out in this order so that the mouse will be initialized correctly. You should put the code to carry out these steps in the mouse.c file, in the function named mouseinit:

  1. Wait until the controller can receive a control packet.
  2. Send 0xA8 to the control port. 0xA8 is the command that tells the PS/2 controller that it should enable the "secondary PS/2 device" (i.e., the mouse).
  3. Get and modify the "Compaq Status Byte" of the PS/2 controller to tell the controller to send interrupts. This takes several steps:

    1. Wait until the controller is ready for us to send.
    2. Send 0x20 to the control port, which selects the Compaq Status byte as the data we want to retrieve.
    3. Wait until the controller has data for us to receive.
    4. Read the status byte from the data port.
    5. Change the status byte, setting the 2nd bit (which specifies that interrupts should be enabled) to 1. This can be done by ORing it with 0x02.
    6. Wait until the controller is ready for us to send.
    7. Send 0x60 to the control port, which tells the controller we are about to send it the modified status byte.
    8. Wait until the controller is ready for us to send.
    9. Send the modified status byte to the data port.
  4. Use the mousecmd function to send 0xF6 to the controller, which selects "default settings" for the mouse.
  5. Use the mousecmd function to send 0xF4 to the controller, which tells the mouse to activate and start sending us interrupts.
  6. Tell the interrupt controller that we want to receive the mouse interrupt (IRQ 12) on CPU 0. This is done using the picenable and ioapicenable. You can see how they are used by looking at how the keyboard interrupt is enabled in console.c.

Once you've put all of that into mouseinit, you will need to make sure your mouseinit function is actually called! The best time to do this is when the operating system first boots. If you look at main.c, you'll see that the main function initializes lots of hardware. So add a call to mouseinit to main. The exact placement of the call doesn't matter too much, but it should happen after the interrupt controller is initialized (i.e., after picinit and ioapicinit).

Now, when you run make qemu, you should get the following output:

$ make qemu
qemu-system-i386 -serial mon:stdio -hdb fs.img xv6.img -smp 2 -m 512 
WARNING: Image format was not specified for 'fs.img' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
WARNING: Image format was not specified for 'xv6.img' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
xv6...
cpu1: starting
cpu0: starting
unexpected trap 44 from cpu 0 eip 80104646 (cr2=0x0)
cpu0: panic: trap
 80106ba9 80106853 80104d30 801039aa 8010394a 0 0 0 0 0

Why do we panic? Well, although the mouse controller is now sending us interrupts, we aren't doing anything to handle them. So the switch statement in trap.c doesn't have an appropriate case to handle the mouse interrupt, and we go to the "panic" case. We'll fix this in the next part.

Part 2 - Handling Mouse Interrupts

Panicking as soon as we receive a mouse interrupt is probably not the best course of action. Instead we'd like to actually handle the mouse interrupts in an interrupt handler.

Start by adding a new case to the switch statement in trap.c to handle our mouse interrupt (IRQ_MOUSE). Have it call mouseintr() and then acknowledge the interrupt with lapiceoi(). If you add a print statement (using cprintf) to mouseintr, you can now see when the interrupt handler gets called:

Now that we're handling the interrupt, the system won't crash, but it's still not working properly. To start with, when we get an interrupt, it means that the PS/2 controller has data for us, but our interrupt controller isn't reading it. This means that it continues to sit in the PS/2 controller's buffer, which eventually gets full. At minimum, our interrupt handler should read the data available. To do so, you should first read one byte from the control port, and check if the "data in buffer" bit (PS2DIB) is set. If so, then you can read one byte from the data port.

If you do that, you will see that you now get a steady stream of interrupts whenever the mouse is being moved. We are successfully receiving data from the mouse!

This is all well and good, but we would like to actually interpret the data being sent by the mouse. The mouse, it turns out, only sends one byte with each interrupt. However, each mouse movement is described using three bytes of data:

  1. A status byte, which has individual bits set for things like the state of the mouse buttons and whether X-axis and Y-axis movement was positive or negative.
  2. A byte giving the amount of movement on the X-axis.
  3. A byte giving the amount of movement on the Y-axis.

The status byte has the following format:

Bit 7 6 5 4 3 2 1 0
Description X overflow Y overflow X sign Y sign Always 1 Middle Right Left

So in your interrupt handler, you will need to keep track of how many bytes you have received so far. The interrupt handler should read just one byte and store it in a buffer. Once you have read 3 bytes, you have a full set of data describing the mouse movement, and you can parse all three bytes together. You can then check the bits in the status byte, the amount of X and Y movement, and perhaps print out a message giving information about the mouse movement. That might look something like the following:

Part 3 - Creating a Cursor

Now that you have a mouse driver that can tell when the mouse moved, by how much, and whether any buttons are pressed, we want to integrate this with the rest of the system.

Create a function in console.c called movecursor(int x, int y) that takes in the amount of X and Y axis movement, and draws a cursor on the screen. You should examine the cgaputc() function to see how to draw something on the screen. The screen is basically a big array of 16-bit integers, accessible through the crt array. The lower 8 bits of this value are the ASCII character at that position, and the upper 8 bits contain the foreground and background color. Since the screen is 80 characters wide and 25 high, we access a character on screen at row r and column c by getting crt[80*r + c].

Because we don't want our cursor to obscure text on the screen, we will instead just change the foreground and background color of the character under the mouse cursor. For example, to set the character at row 10 and column 14 to a white background with black text, we could do:

uchar c = crt[10 * 80 + 14] & 0xFF;
crt[10 * 80 + 14] = c | (0x70 << 8);

The result will look something like this:

Hints:

Part 4 - Copy and Paste

So far we haven't actually done anything useful with our mouse. This final piece will change that, by adding handling for left and right mouse clicks. A left click will copy the text currently on screen starting from the mouse cursor position until the end of the line. A right click will paste the copied text onto the command line, as though it had been typed in at the keyboard.

To do this, you will first have to add code to mouse.c to track the state of the mouse buttons. When the left mouse button goes from being pressed to released, you should call a function mouse_leftclick in console.c. Do the same thing for the right mouse button, calling mouse_rightclick.

Inside console.c, implement code for mouse_leftclick that copies characters starting from the current cursor position until the end of the row. You can read a character by accessing it from the screen buffer (the crt array). You should store the characters in some global copy/paste buffer (a character array) so that they can be pasted later.

Implement mouse_rightclick by taking the characters from the copy/paste buffer and placing them into the input buffer (the struct named input).

The final result would look like this demo, where we copy the line starting with nblocks and paste it:

Hints:

Submitting

Create a patch file and submit it on NYU Classes, as in previous homework:

$ git commit -a -m "Implement mouse driver with copy and paste"
[bonus_hw_solution de1c4d7] Implement mouse driver with copy and paste
 5 files changed, 175 insertions(+), 2 deletions(-)
$ git format-patch bonus_hw.unmodified
0001-Implement-mouse-driver-with-copy-and-paste.patch

Submit 0001-Implement-mouse-driver-with-copy-and-paste.patch.