![]() | ![]() | |||
| TAMS / Java / Hades / applets: contents | previous | next | ||||
| Hades Applets contents visual index introduction std_logic_1164 gatelevel circuits delay models flipflops adders and arithm... counters LFSR and selftest memories programmable logic state-machine editor misc. demos I/O and displays DCF-77 clock relays (switch-le... CMOS circuits (sw... RTLIB logic RTLIB registers Prima processor D*CORE MicroJava Pic16 cosimulation Mips R3000 cosimu... Intel MCS4 (i4004) image processing ... [Sch04] Codeumsetzer [Sch04] Addierer [Sch04] Flipflops [Sch04] Schaltwerke [Sch04] RALU, Min... [Fer05] State-Mac... [Fer05] PIC16F84/... PIC LCD cont... PIC LCD chro... PIC calculator PIC mastermi... PIC16F84 ele... PIC RX/TX PIC CCP PIC CCP PIC Tetris [Fer05] Miscellan... [Fer05] Femtojava FreeTTS | PIC16F628 Tetris for Terminals game
Circuit Description
This applet demonstrates a version of the famous
Tetris game
running on the PIC16F628 microcontroller.
The circuit consists of the microcontroller
connected to a standard serial terminal.
The program running on the microcontroller (see below) first sets up the serial communication parameters (currently 9600 baud 8N2), initializes the game board, and enables timer interrupts. The main program then enters an endless loop which calls the check_handle_command() function over and over. This function first checks whether the 'game over' bit is set in the global 'state' variable, in which case only the 'restart game' command is accepted. Next, the 'timer overflow' bit in the 'state' variable is checked. If set, the current block falls down one position and the bit is cleared. The 'timer overflow' bit is set again in the interrupt handler function on the next timer0 register overflow (T0IF). Otherwise, the function checks whether a user input command has been received from the terminal, and tries to execute the command. With the simulation running, use a mouse click (left button) to transfer the keyboard focus to the terminal window. Afterwards, type the following keys into the terminal window to control the game:
Depending on your computer and Java virtual machine, the game might run slowly; on my Athlon-2600 (2 GHz) with JDK 1.5.0 the speed is a bit slower than the very first (easy) levels of the real Tetris game. This is limited by the simulation of the microcontroller with all its peripheral functions and the frequent repainting of the terminal window. When built as a real hardware device and connected to a terminal, the game will be pretty fast, and you might want to change the timer initialization values (or prescaler) used in vt100_initialize() and the isr() functions. Due to severe code-size limitations - the picclite compiler only supports the PIC16F627 with its program memory of 1024 instructions - only a slightly simplified version of the original game is implemented. Most importantly, the static game board only shows whether a position is occupied, but not the type of block dropped to that position. However, the game board (24 rows of 10 columns) has the standard size, and all seven types of 4-characters block from the original game are supported. In case you want build the circuit yourself, you can download the following files:
The program running on the microcontroller is based on the following C source code:
/* PIC16F628 Tetris for Terminals (2nd version)
*
* This program implements a (simplified) version of the famous 'Tetris'
* game with a PIC16F628 connected to a serial terminal (VT52 or higher).
* Just connect the terminal to the PIC RX/TX ports, select the required
* communication settings (9600 baud 8N1) on the terminal, and you are
* ready to go.
*
* The program assumes an input clock of 1.075 MHz (in order to generate
* the correct baud-clock for 9600 baud communication), but it might also
* work with 1.00 MHz clock if your terminal tolerates deviations from the
* exact baud-clock. The low clock rate was chosen to make the program
* (almost) useable in a simulator; you might want to use 4 MHz or 16 MHz
* and 19200 or 38400 kbaud for real hardware. This requires changing the
* RX/TX settings in vt100_initialize().
*
* Use the following keys to control the game:
*
* 'j' - move current block left
* 'k' - rotate current block
* 'l' - move current block right
* 'space' - drop current block
* 'r' - redraw the screen
* 'q' - quit (give up current game)
* 'f' - faster
* 'd' - slower
*
* Tip: if you run the demo via the Hades simulator, please type at most
* one command key during each repaint iteration. Otherwise, the 16F628
* receive buffer will overrun, and the 16F628 simulation model seems to
* misbehave afterwards - it reacts only to each second keypress afterwards,
* which makes playing much more difficult :-)
*
* Originally intended for the PIC16F84 with only 68 bytes of RAM,
* the program uses a very compact representation of the gaming board,
* with one bit per position (1=occupied 0=empty). All accesses to the
* gaming board should be via the setPixel() and occupied() functions.
* A standard gaming board of 24 rows of 10 columns each is used.
* Note that the decision for a 1-bit representation means that we lose
* the option to display the original type (color) of the different blocks.
* Unfortunately, I found no way to tweak the program into the 16F84
* via the picclite compiler, so I switched to the pincompatible 16F628.
* The original 1-bit datastructures were kept, though the 16F628 should
* have enough RAM to support storing the block-type in the gaming board.
*
* Similarly, the current block is represented via a 4x4 bitmap stored
* in the 'current_block<0..3>' variables, where the lower nibble of each
* byte specify the 4 bits of one row of the current block. New blocks
* are created via the create_block() and create_random_block() functions,
* and rotate_block() uses some bit-fiddling to rotate the block inplace
* by 90 degrees. The current position (row, column) of the current block
* is maintained in the current_row and current_col variables, and the
* test_if_block_fits() functions checks whether the block fits within the
* gaming board bounds and the already placed blocks.
*
* The program enables interrupts for both the timer0 interrupts and
* the serial-port receiver interrupts (user input via the terminal).
* The interrupt handler just copies the received input character to the
* 'command' variable, and sets a bit in the 'state' variable on a timer
* overflow.
* On each iteration of the check_handle_command() function, the 'state'
* variable is checked for the timer overflow bit. If set, the program
* attempts to let the current block fall down one position and handles
* the resulting situation (collapsing completed rows, or setting the
* game-over bit if the block is stuck at the topmost row).
* Otherwise, the check_handle_command() bit copies the contents of the
* command variable to a tmp variable, resets the command variable, and
* then continues to check for and execute the given user command.
*
* Despite using an 'incremental repainting' strategy of first erasing and
* then overdrawing the current block during the game-play, the performance
* of the program is still limited by the slow serial connection.
* A single block movement still needs four VT52 goto commands and writes
* four characters to erase a block, and another four gotos and writes to
* redraw the block. This alones takes 40*(1+8+1)*(1/baudrate) seconds -
* about 40 msecs at 9600 baud.
* While the program uses the 16F628 transmitter register to send characters,
* it also uses a wait loop in function vt100_putc() to avoid overrunning
* the transmitter. This is far simpler than trying to maintain a send buffer,
* but it also means that we are likely to drop user input requests, because
* the sending is triggered from within the interrupt handler. Also, note
* that the wait-loop limits in vt100_putc() need to be adapted when changing
* the terminal baud-rate or the 16F628 input clock.
*
* TODO:
* The program was written for the free Picclite compiler (9.50PL1), which
* supports the 16F627 but not the 16F628. This means that the program must
* fit into the first 1K words of program memory, which severely limits the
* things we can do. At the moment, there are just seven unused words left
* free in the program memory...
*
*
* If you have access to the full version of the compiler or another compiler
* for the 16F628, you could try to re-instate the uncommented functions
* and to add some functions that were omitted due to program-memory size
* limitiations. For example:
* - show the 'level' and 'score' info after each new block
* - distinguish the different block-types via different characters
* - distinguish the 'score' between block-types
* - add highlighting of 'collapsing' rows before redrawing the game-board
* - implement the 'pause' command
* - make timer0 delays smaller with increasing game level
* - add high-score list management, could use the data EEPROM
* - program should run with watchdog timer enabled (needs extra clrwdt)
* - etc.
*
* Note: see http://www.rickard.gunee.com/projects for a really amazing
* assembly-code version of Tetris for the PIC16F84 - with direct connection
* to a PAL/NTSC black&white television set, and even including audio!
* I only learned about that version after writing this C program, however.
* Fortunately, data-structures and timing of both programs are quite different.
*
* Note: make sure to set the UART initialization code to match the
* selected clock rate. You might also have to play with the TMR0
* reload value in the interrupt handler to optimize the game-play
* (not too slow, but neither too fast...)
*
*
* (C) 2005, 2006 fnh hendrich@informatik.uni-hamburg.de
*
* 2006.03.23 - change vt100_putc to support 4MHz and 250 kHz clock
* 2006.03.20 - ifdef stuff to change between 4 MHz and 250 kHz clock
* 2006.03.06 - change 16F628 to use XT osc. mode at 4.000 MHz
* 2006.01.04 - reorganize main-loop <-> isr interaction
* 2005.12.29 - implemented check_remove_complete_rows()
* 2005.12.28 - first working version (with interrupts)
* 2005.12.27 - first code (game board drawing etc)
*/
#include "init.h" // included by C-Wiz
#include "htc.h"
#include "stdlib.h" // we need rand()
#include "tetris.h"
// NOTE: YOU MUST SELECT AND UNCOMMENT ONE OF THOSE MACROS.
// Depending on the selected clock-rate of course - a 4 MHz clock
// is a good choice for the actual real hardware, while 250 kHz
// means that the Hades simulation will run near real-time.
// For other input clock rates, change the RX/TX baudrate stuff
// and the TMR0 and "divider"/"divider_limit" values to get an
// acceptable game speed.
//
#define CLK4MHZ 4000000
//#define CLK250KHZ 250000
#define ROWS ((unsigned char) 24)
#define COLS ((unsigned char) 10)
#define PAINT_FIXED ((unsigned char) 2)
#define PAINT_ACTIVE ((unsigned char) 1)
#define ERASE ((unsigned char) 0)
#define XOFFSET ((unsigned char) 3)
#define XLIMIT ((unsigned char) (XOFFSET+COLS))
static unsigned char board[30]; // the main game-board
static unsigned char current_block0; // bit-pattern of the current block,
static unsigned char current_block1; // with one four-bit bitmap stored
static unsigned char current_block2; // in the lower nibble of each of these
static unsigned char current_block3; // variables
static signed char current_row; // row of the current block
static signed char current_col; // col of the current block
static unsigned char level;
static unsigned char score;
// the possible user commands encoded as one-byte constants
//
/*
#define CMD_LEFT ((unsigned char) 0x01)
#define CMD_RIGHT ((unsigned char) 0x02)
#define CMD_ROTATE ((unsigned char) 0x04)
#define CMD_DROP ((unsigned char) 0x08)
#define CMD_REDRAW ((unsigned char) 0x10)
#define GAME_OVER ((unsigned char) 0x40)
#define NEEDS_INIT ((unsigned char) 0x80)
*/
#define STATE_IDLE ((unsigned char) 0x1)
#define TIMEOUT ((unsigned char) 0x2)
#define NEEDS_INIT ((unsigned char) 0x4)
#define GAME_OVER ((unsigned char) 0x8)
#define CMD_NONE ((unsigned char) 0)
#define CMD_LEFT ((unsigned char) 'j')
#define CMD_RIGHT ((unsigned char) 'l')
#define CMD_ROTATE ((unsigned char) 'k')
#define CMD_DROP ((unsigned char) ' ')
#define CMD_REDRAW ((unsigned char) 'r')
#define CMD_START ((unsigned char) 's')
#define CMD_FASTER ((unsigned char) 'f')
#define CMD_SLOWER ((unsigned char) 'd')
// high-scores: 1 point per new block, 20 points per completed row
#define SCORE_PER_BLOCK ((unsigned char) 1)
#define SCORE_PER_ROW ((unsigned char) 20)
static unsigned char state;
static unsigned char command;
static unsigned char divider;
static unsigned char divider_limit;
static unsigned char heartbeat;
/**
* utility function to calculate (1 << nbits)
*/
unsigned char power_of_two( unsigned char nbits ) {
unsigned char mask;
mask = (1 << nbits);
return mask;
}
/**
* utility function that returns the bit-patterns of the seven
* predefined types (shapes) of Tetris blocks via ROM lookups.
* This method returns the block in the standard orientation only.
* We use a 4x4 matrix packed into two unsigned chars as
* (first-row << 4 | second-row), (third-row << 4 | fourth-row).
*/
unsigned char getRawBlockPattern( unsigned char index ) {
switch( index ) {
// brown square 2x2
case 0: return 0b00000110;
case 1: return 0b01100000;
// brown square 2x2
case 2: return 0b00000110;
case 3: return 0b01100000;
// yellow cursor-type block
case 4: return 0b00001110;
case 5: return 0b01000000;
// red 1x4 block
case 6: return 0b01000100;
case 7: return 0b01000100;
// blue 2+2 shifted block
case 8: return 0b00001100;
case 9: return 0b01100000;
// green=2+2 shifted block (inverse to blue)
case 10: return 0b00000110;
case 11: return 0b11000000;
// cyan=3+1 L-shaped block
case 12: return 0b00001110;
case 13: return 0b00100000;
// lilac 1+3 L-shaped block
case 14: return 0b00001110;
case 15: return 0b10000000;
// we will not arrive here, but the compiler wants this
default: return 0b11111111;
}
}
/**
* fill the current_block0..3 variables with the bit pattern of the
* selected block type (0,1..7).
*
* Implementation note: the first version of the code used an array
* current_block[4], which made for clean C sources but very clumsy
* assembly code after compiling. Switching to four separate variables
* with two utility functions reduced the code size and made the program
* fit into the 16F627...
*/
void create_block( unsigned char index ) {
unsigned char tmp;
tmp = getRawBlockPattern(index+index);
current_block0 = (tmp & 0xf0) >> 4;
current_block1 = (tmp & 0x0f);
tmp = getRawBlockPattern(index+index+1);
current_block2 = (tmp & 0xf0) >> 4;
current_block3 = (tmp & 0x0f);
}
/**
* create a new randomly-chosen block.
* Note that rand() doesn't seem to return zero at all,
* so the seven types of blocks are indexed via values 1..7
* instead of 0..6.
*/
void create_random_block( void ) {
unsigned char x;
x = rand() & 0x7;
create_block( x );
}
/**
* helper function to access one nibble (row) of the current block.
*/
unsigned char getBlockNibble( unsigned char i ) {
unsigned char tmp;
switch( i ) {
case 0: tmp = current_block0; break;
case 1: tmp = current_block1; break;
case 2: tmp = current_block2; break;
case 3: tmp = current_block3; break;
}
return tmp;
}
/**
* helper function for rotate_block()
*/
void updateBlockNibble( unsigned char i, unsigned char mask ) {
switch( i ) {
case 0: current_block0 |= mask; break;
case 1: current_block1 |= mask; break;
case 2: current_block2 |= mask; break;
case 3: current_block3 |= mask; break;
}
}
/**
* rotate the current_block data by 90 degrees (counterclockwise).
* We use an inplace rotation that uses the upper nibbles of the
* current_block0..3 variables as intermediate storage.
*
*
| |||
| Print version | Run this demo in the Hades editor (via Java WebStart) | ||||
| Usage | FAQ | About | License | Feedback | Tutorial (PDF) | Referenzkarte (PDF, in German) | ||||
| Impressum | http://tams.informatik.uni-hamburg.de/applets/hades/webdemos/95-dpi/pic16f628-tetris/tetris.html |