Finally! Let's get to writing Snake. In the next two assignments, you will build a working game from the ground up. This first assignment consists of creating the SnakeOS, the operating system that will handle all of the I/O routines for Snake. The next assignment consists of writing the game logic.
The operations that a program can ask the operating system to perform are: (1) drawing a rectangle on the screen, (2) checking the color of a pixel, (3) generating a random number, and (4) checking for keyboard and timer input. These operations are the fundamental pieces that allow us to build up more complex functionalities in the next assignment. User-level (non-privileged) programs invoke these four operating systems operations using the TRAP instruction. As such, the code you write in this assignment will be in the form of trap handling routines.
Important files:All of the files you'll need for the assignment are available in a hw6code.zip Zip archive. You can also view the snakeos.asm template file.
Video console interface: The video console is directly linked to a region of addressable memory, namely xC000 to xFDFF. Every row is 128 dots (also called pixels) across, and there are 124 rows in total, which gives us a 128x124 screen to work with. Each pixel is linked to one memory address, so xC000 is the pixel on the top left corner, xC001 is the pixel to the right of it, and so on. Since 128 is x0080 in hex, xC000 + x0080 = xC080 is the location of the leftmost pixel on the second row from the top, and xC000 + 2*x0080 = xC100 would be the next pixel down. In general, you can use the following formula to calculate which address of a pixel:
address = xC000 + (y_coordinate * 128) + x_coordinate
Color encoding: Each location holds a 16-bit value which determines the color of that pixel. The high-order bit is ignored, and the remaining 15 bits specify the color, using a 5-bit values for each of red [14:10], green [9:5], and blue [4:0].
Coordinate systems: This code uses just the video console coordinates. This system is zero based (that is, the top-most row is row 0 and the left-most column is column 0). Thus, if you start your rows and columns at 1, you'll be off by quite a bit by the time you're done.
New instructions:We've added some new instructions to the LC-3 to make writing LC-3 code easier.
MUL R0, R1, R2 ; R0 = R1 * R2 MUL R0, R1, #4 ; R0 = R1 * 4
The range of allowable constants for MUL is the same as that for ADD. With a 5 bit immediate specification, you are allowed to specify a constant between -16 and 15.
Subtract (SUB) instruction: We've also added a SUB instruction. This instruction is identical to the ADD register instruction except instead it subtracts the registers. There is no SUB immediate instruction (since this can be accomplished as ADD R1, R1, -5 for example).
SUB R2, R3, R4 ; R2 = R3 - R4
Register Saving/Restoring: You will notice, in the stubs provided in snakeos.asm that each trap begins with a register saving section and ends with a register restoring section. This saving and restoring is done because the caller expects register values to be preserved when the trap returns. Because no traps call each other recursively, we won't have to worry about the values saved being overwritten.
Memory-mapped Timer:We've extended the LC-3 architecture with a timer device, so we can actually know when to move the snakes! This timer, as with the other I/O devices like the keyboard or display, is memory-mapped. Initially, the Timer Status Register (TSR) is set to 0. Every time the timer goes off, the TSR is set to 1. When any instruction reads from the TSR, it resets itself back to 0 until the next time the timer expires.
There is also a second register associated with the timer that can be useful. The Timer Interval Register (TIR) tells the simulator how short or long the timer interval is. In fact, it specifies the amount of time in milliseconds between each timer clock tick. However, there is a special case; if the register contains the value 0, then the timer will never expire and hence is disabled. Disabling the timer is useful when debugging.
Operating System Template: We're giving you a basic operating system template to which you can add your code. The template is called snakeos.asm. Based on this template, your implementation of the SnakeOS operating system will have three sections. Of these three sections you'll only need to modify the third and final section.
Each of the trap routines starts with a prolog (which saves the current value of all the registers) and ends with an epilogue (which restores the original register values and invokes the Return from Trap (RTT) instruction).
Input Range Checking: While it's generally necessary to error check strenuously when programming, for the purposes of this assignment you are not required to do any out of bounds checking on the input arguments.
Official Test Scripts: To use each official test script, first assemble the asm test file associated with the part, then open up the simulator and run "script p#-test#.lcs", replacing the two # with the part number and the test number. You can run all the tests by running "script all-test.lcs". Important: The test scripts we're giving you are NOT exhaustive tests; they are only to get you started. For grading, we'll make additional test cases that were not covered in the test cases we gave you.
Turnin: The only file you need to turn in is your version of the snakeos.asm file. Similar as before, do the following:
turnin -c cse240 -p hw6 snakeos.asm
For this assignment you will implement the four of the trap routines that constitude the SnakeOS. Each trap handler is described, below.
vid_x = 4*x; vid_y = 4*y; for (r=0; r<=3; r=r+1) vid_mem_addr = xC000 + ((vid_y + r) * 128) + vid_x; set memory at vid_mem_addr to color; set memory at vid_mem_addr+1 to color; set memory at vid_mem_addr+2 to color; set memory at vid_mem_addr+3 to color;Write the LC-3 code for the above pseudocode assuming that x is in R0, y is in R1, and the color is in R2. The following LC-3 assembly declarations have been included in the snakeos.asm skeleton code; you may find them helpful:
; Video Memory Constants VIDEO_MEM_BEGIN: .FILL xC000 VIDEO_ROW_SIZE: .FILL #128
To test whether this code is working, first run the scripts we've provided. Then, if you would like to test further, try setting a breakpoint to the beginning of the trap. Once the program execution breaks there, set R0 and R1 to various values and R2 to a bright enough color to be seen (x7FFF, white, is recommended). By altering values of R0 and R1, you should be able to see white blocks showing up on the video memory. Unfortunately, to compute precise locations to check, you'll have to do the math to compute the address corresponding to where each block is supposed to go.
The code for querying the color of a block is conceptually similar to the coded in the previous part. Once again, you'll need to map game coordinates to video memory coordinates. However, this time you'll only need to access the top left corner of the block, because we can assume the entire block is a single color.
Assume that row and col are given in game field coordinates.
vid_x = 4*x; vid_y = 4*y; vid_mem_addr = xC000 + (vid_y * 128) + vid_x; put content at vid_mem_addr into R5;Write the LC-3 code for the above pseudocode assuming that x is in R0 and y is in R1. The code must place into R5 the 16-bit value that encodes the current color of the specified block.
Testing this involves doing the reverse of Part 1: put a color in some block on the screen, then run the trap with that location as the input.
R_POS := R_POS + 1 if R_POS >= R_SIZE R_POS := 0 R5 := Memory[R_START+R_POS]Some local variables and the list of numbers is included in the skeleton.
R_POS: .FILL 0 ; initially zero R_SIZE: .FILL x19 R_START: .FILL x35FE ; Array of "random" numbers .FILL x342D .FILL x26D3 .FILL xDE45 .FILL xF56E .FILL xBFD4 .FILL xDE31 .FILL x25DE .FILL xFF45 .FILL x34ED .FILL x12E9 .FILL x82AB .FILL x08E4 .FILL x5400 .FILL xED25 .FILL x346E .FILL x2300 .FILL x3644 .FILL x1AE4 .FILL x85D4 .FILL xED48 .FILL x347D .FILL x1234 .FILL x4842 .FILL xADEF
Our game of snake uses two types of inputs to trigger these movements: (1) a hardware timer and (2) the keyboard. The timer is designed to tick on a regular interval. When it does, a "move forward" command will be issued. The keyboard, on the other hand, is harder to deal with. When a key is pressed, you must check through each key that signifies an event, for example the "a" key to issue a command to move the snake left. If the key pressed doesn't match any event keys, then ignore it and keep polling for input. Basically, the game will just sit in a loop waiting for a command or the timer tick before doing anything.
The trap handler you will write will return a value (in register R5) indicating what event has taken place. Note that your handler will not return until some event happens. If the timer goes off (i.e., TSR takes on a non-zero value), we return 0 (in R5) indicating that the snakes should move. If the user presses the 'a' key, we return 1 (in R5) indicating that the user wants to move the snake to the left. If the user presses the 'd' key, we return 2 (in R5) indicating that the user wants to move the snake to the right, etc. If the user presses the '*' key, we return -1 (in R5) indicating that the user wants to quit the game.
To handle the timer, when TSR becomes non-zero, we know the timer has expired and we return the appropriate value. Reading TSR automatically resets it back to zero. The keyboard is a little trickier. We must first determine when a key is pressed (via KBSR). When a key is pressed we must fetch it (via KBDR) and check it against all the keys we are interested in (see below). When we find a match, we return the appropriate value. Otherwise, we continue polling the TSR and KBSR. Important Note: Please note that the LC-3 terminal output window (the lower-left of the simulator window) must be the active window for the keystrokes to be sent to the keyboard device in the simulator. That is, click on the terminal window to make it active before typing characters intended for the simulated keyboard device. The window will be highlighted yellow when keyboard input is active.
The events each correspond to an integer value, so that value will be what you report (i.e., return) in R5:
loop: x := OS_TSR if x != 0 R5 := 0 ; Timer goto done ; break out of the loop x := OS_KBSR if x == 0 goto loop ; continue with the next iteration x := OS_KBDR if x == LEFT_KEY R5 := 1 ; Left goto done ; break out of the loop if x == UP_KEY R5 := 2 ; Up goto done ; break out of the loop if x == RIGHT_KEY R5 := 3 ; Right goto done ; break out of the loop if x == DOWN_KEY R5 := 4 ; Down goto done ; break out of the loop if x == QUIT_KEY R5 := -1 ; Quit goto done ; break out of the loop goto loop ; continue with the next iteration done:The exact keys and input locations should be specified in memory using the following LC-3 assembly code:
; Input key ASCII codes LEFT_KEY .FILL x0061 ; Left - 'a' UP_KEY .FILL x0077 ; Up - 'w' RIGHT_KEY .FILL x0064 ; Right - 'd' DOWN_KEY .FILL x0073 ; Down - 's' QUIT_KEY .FILL x002A ; Quit - '*' ; Addresses of various registers OS_KBSR .FILL xFE00 ; keyboard status register OS_KBDR .FILL xFE02 ; keyboard data register OS_TSR .FILL xFE08 ; timer status register OS_TIR .FILL xFE0A ; timer interval register
This part is the hardest to write a script for, but the easiest to test by hand. When testing manually, all you have to do is set a good time interval for the timer, run the trap, and see if it returns correctly when the timer goes off. To test the keyboard stuff, set the TIR to 0 to disable the timer and check if the keys correspond correctly to the return values. Make sure that unrecognized keys don't cause the loop to break!
Each script for this part involves two files: p5-test#.lcs and p5-test#.in. The former will take care of setting the registers and checking values, while the latter is the actual sequence of simulated keystrokes. If you want to create your own test scripts for this part, you will need to do something similar.