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.
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
:
Get and modify the "Compaq Status Byte" of the PS/2 controller to tell the controller to send interrupts. This takes several steps:
mousecmd
function to send 0xF6 to the controller, which selects "default settings" for the mouse.mousecmd
function to send 0xF4 to the controller, which tells the mouse to activate and start sending us interrupts.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.
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:
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:
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:
movecursor()
function in mouse.c
, you should add its prototype to defs.h
.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:
mouse_leftclick
, you may want to trim the end of the string, replacing any trailing spaces with NULL bytes, so that you don't copy and paste a bunch of spaces at the end of the line.consoleintr
to copy each character into the edit buffer and print it out to the screen. You can avoid doing this with a clever trick: create a function int copy_paste_getc(void)
that returns one character at a time from the copy/paste buffer, and then just call consoleintr(copy_paste_getc)
.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.