Tetris on a PIC Microcontroller

The PIC Tetris game (PICtris) spawned from a recent question by RetroBrad in the forum. He wanted to know how to manipulate LEDs and draw 2D graphics. I'm more of a practical kind of guy and in the past, graphics were never an area of concern. I was intrigued to delve into the world of 2D to find out how things tick.

The 64 LEDs and four buttons are controlled by a single PIC (18LF4520) and two AA batteries. The source code operates very efficiently, and the program is extremely responsive. I was worried about the latter as I was determined to use algorithm based graphic rotations as compared to predefined constants. From here I could use a similar approach to create other interactive games!{nomultithumb}

NOTE: Tetris was developed for Swordfish Basic; the source code will compile under the free Swordfish Special Edition version.

Before I press on, here is a video of the completed Tetris game

As always, new features are a must! Since the above video, I have added the following:

• A splash screen that displays "TETRIS" when the game starts.
• High scores - your efforts are recorded in the EEPROM.
• High power darlington driver to brighten up the display.

All of the above features are incorporated in the source code (found below).

Also, here is a recently completed version of the game - with audio! (Big thanks to DomS from Digital DIY for this project video)

Enrico Castellani also made a version of the PICTris board, and has been kind enough to share the Eagle PCB layout files. You can find the files and a video demonstration in the comments below.

Creating 2D Graphics

In the Tetris game, graphics are treated as 8x16 pixel objects. To ease the overhead of graphic design, I made an Excel spreadsheet that does all of the hard work for you.

There's an 8x16 field where you can place an "x" for each block of the object. Any block with a "x" will be formatted to look as it does below (I found that approach easier to interpret compared to scattered x's). The spreadsheet will calculate and display the Swordfish code required for the graphic! Simple. Here's a screen shot:

Create & Display Text

The spreadsheet can be used to create any object, including text. I recently wanted to add a small feature - a splash-screen. Using the graphic designer made it extremely easy to do.

I put an "x" in all the spots that would make up the word TETRIS on the 8x16 display. I had to get a little creative and split the word into two columns as shown on the right.

If you'd like to look at how I used the object as a text - browse through the source code (listed below), there will be a routine there called "SplashScreen".

Graphics on a PIC

There are several avenues to take when working with 2D graphics. I explored the mathematics behind two approaches: trigonometry (rotating Cartesian coordinates) and 2D affine matrix transformation. Both of them sound like you need a university degree to understand, though they are really quite simple in application. I originally used trigonometry to rotate the objects, though affine matrix transformation was much simpler and more importantly, faster.

Affine Matrix Transformation

Rotating a 2D object +/-90 degrees can be easily done like so:

Graphic Rotation
```Public Sub NewRotation(ByRef pSource() As Word, ByRef pTarget() As Word, ByVal pDirection As Byte)
Dim X2,Y2,Q,PX,PY As Integer
Dim X1,Y1 As Byte

// define object origin
PX = originx
PY = originY
// define object width
Q = 0

// clear the target array
For X1 = 0 To 7
pTarget(X1) = 0
Next

// analyse each pixel
For X1 = 0 To 7
For Y1 = 0 To 15
If pSource(X1).Bits(Y1) = 1 Then
If pDirection = CW Then
X2 = (Y1 + PX - PY)
Y2 = (PX + PY - X1 - Q)
Else
X2 = (PX + PY - Y1 - Q)
Y2 = (X1 + PY - PX)
EndIf
if X2 >= 0 And X2 <= 7 Then
If Y2 >= 0 and Y2 <= 15 Then
pTarget(X2).Bits(Y2) = 1
endif
endif
End If
Next
Next
End Sub```

What's going on? The array pSource contains graphic information for the object. Each byte represents a column of pixels, which can be referred to in X,Y format like so:

`pObject(X).Bits(Y)`

pObject is of type Word. This means that each index has 16 bits (two bytes). This allows two 8x8 displays to be joined and form a single 8x16 display. Consider any object in the display:

Keeping in mind that the top left hand corner is pixel 0,0: There are only four pixels that are enabled, they are:

• X = 3, Y = 2
• X = 4, Y = 2
• X = 5, Y = 2
• X = 4, Y = 3

The above information can be placed into pObject quite easily:

• pObject(3).Bits(2)
• pObject(4).Bits(2)
• pObject(5).Bits(2)
• pObject(4).Bits(3)

Now we have an array of pixel information that can be used anyway we want to.

Rotating Pixels

In the Tetris game, I was originally rotating objects CCW, though it has now changed to CW. As this example was written before that change, it will focus on turning an object CCW. If you want to spin an object the other way, then check the source code - both formulas are included. CCW Rotation Formula

```X2 = (Y1 + PX - PY)
Y2 = (PX + PY - X1 - Q)```

Step 1

Define the origin (PX,PY)

The origin is the pixel which the object is to rotate around. For the above object, it would be wise to choose pixel (4,2). Why? because that coordinate will ensure the object remains at the same location as it spins around (consider what would happen if you spun a circle around anywhere else but the centre..)

Step 2

Define the object length/width (Q)

We are working in a single pixel by pixel environment. This means that the length and width (Q) of each pixel is always 0. I have included this step for use with other conditions.

Step 3

Define the pixel to be moved (X1,Y2)

Initially, I am going to rotate pixel (3,2).

Run the math

If

```PX = 4
PY = 2

X1 = 3
Y1 = 2

Q = 0```

And

```X2 = (Y1 + PX - PY)
Y2 = (PX + PY - X1 - Q)```

Therefore

```X2 = 4
Y2 = 3```

A 90 degree rotation of pixel (3,2) would land the pixel at (4,3).

Rinse and Repeat

The last step is to repeat the process for each and every pixel

Source Code

PICtris (Tetris on a PIC Microcontroller)
```' v1.0
' release of the source code. Tetris without any bells + whistles.

' v1.1
' Added scorekeeping & highscores. The game will display the last high score at the start.
' If your score beats the highscore, it will be saved to the EEPROM as the new high score.

Device = 18F4520
Clock = 32
Config MCLRE = Off

Include "InternalOscillator.bas"
Include "RandGen.bas"
Include "Utils.bas"
Include "EEPROM.bas"

// PROGRAM DEFINES
Dim ObjectData(8) As Word
Dim tmpObjectData(8) As Word
Dim BackgroundData(8) As Word

Dim OriginX As Byte
Dim OriginY As Byte
Dim LimitedRotation As Byte
Dim CurrentRotation As Byte
Dim NumberOfLines As Word

Dim Left As PORTB.0
Dim Right As PORTB.1
Dim Rotate As PORTB.2
Dim Down As PORTB.3

Dim Left_Delay As Byte
Dim Right_Delay As Byte
Dim Rotate_Delay As Byte
Dim Down_Delay As Byte
Dim ButtonFlags As Byte
Dim Left_Debounce As ButtonFlags.0
Dim Right_Debounce As ButtonFlags.1
Dim Rotate_Debounce As ButtonFlags.2
Dim Down_Debounce As ButtonFlags.3

Dim Flags As Byte
Dim InterruptComplete As Flags.1
Dim DropObject As Flags.2
Dim EndOfGame As Flags.3
Dim CheckForNewLines As Flags.4

// used for random graphic selection
Const NumberOfShapes = 7
Const RndWindow = 255/NumberOfShapes

// terminology (I could have added more..)
Const CW = 1
Const CCW = 0

// TMR2 variables
Dim TMR2IE As PIE1.1,                // TMR2 interrupt enable
TMR2IF As PIR1.1,         // TMR2 overflow flag
TMR2ON As T2CON.2,        // Enables TMR2 to begin incrementing
mS As Word,               // mS register
CurrentX As Byte,
sWDT As Byte              // Software Watch Dog Timer for entering SLEEP mode

Const mS_Inc = 2
Const DebounceDelay = 10

// Sleep mode registers
Dim IDLEN As OSCCON.7,
SCS1 As OSCCON.1,
SCS0 As OSCCON.0

// SHAPES!
// Be sure to update "NumberOfShapes" if more are added (or some are removed)
// ###
//  #
Const Graphic0(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000001,%0000000000000011,%0000000000000001,%0000000000000000,%0000000000000000)
Const Graphic0_X = 4
Const Graphic0_Y = 0
Const LimitedRotation0 = 0

// ####
Const Graphic1(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000001,%0000000000000001,%0000000000000001,%0000000000000001,%0000000000000000,%0000000000000000)
Const Graphic1_X = 4
Const Graphic1_Y = 0
Const Graphic_W = 1
Const Graphic_H = 1
Const LimitedRotation1 = 1

// ###
// #
Const Graphic2(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000011,%0000000000000001,%0000000000000001,%0000000000000000,%0000000000000000)
Const Graphic2_X = 4
Const Graphic2_Y = 0
Const LimitedRotation2 = 0

// ###
//   #
Const Graphic3(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000001,%0000000000000001,%0000000000000011,%0000000000000000,%0000000000000000)
Const Graphic3_X = 4
Const Graphic3_Y = 0
Const LimitedRotation3 = 0

// ##
// ##
Const Graphic4(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000011,%0000000000000011,%0000000000000000,%0000000000000000,%0000000000000000)
Const Graphic4_X = 3
Const Graphic4_Y = 1
Const LimitedRotation4 = 2

//  ###
// ###
Const Graphic5(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000010,%0000000000000011,%0000000000000001,%0000000000000000,%0000000000000000)
Const Graphic5_X = 4
Const Graphic5_Y = 1
Const LimitedRotation5 = 1

// ###
//  ###
Const Graphic6(8) As Word = (%0000000000000000,%0000000000000000,%0000000000000000,%0000000000000001,%0000000000000011,%0000000000000010,%0000000000000000,%0000000000000000)
Const Graphic6_X = 4
Const Graphic6_Y = 1
Const LimitedRotation6 = 1

// TEXT
Const TETRIS(8) As Word = (%1000100001000001,%1111101111011111,%1000100001000001,%0000000000000000,%1001101111011111,%1010100101010101,%1100101010010001,%0000000000000000)

// NUMBERS
Const Number_0(8) As Word = (%0000000000011110,%0000000000010010,%0000000000010010,%0000000000010010,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_1(8) As Word = (%0000000000000100,%0000000000001100,%0000000000000100,%0000000000000100,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_2(8) As Word = (%0000000000011110,%0000000000000010,%0000000000011110,%0000000000010000,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_3(8) As Word = (%0000000000011110,%0000000000000010,%0000000000001110,%0000000000000010,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_4(8) As Word = (%0000000000010010,%0000000000010010,%0000000000011110,%0000000000000010,%0000000000000010,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_5(8) As Word = (%0000000000011110,%0000000000010000,%0000000000011110,%0000000000000010,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_6(8) As Word = (%0000000000011110,%0000000000010000,%0000000000011110,%0000000000010010,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_7(8) As Word = (%0000000000011110,%0000000000000010,%0000000000000100,%0000000000001000,%0000000000010000,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_8(8) As Word = (%0000000000011110,%0000000000010010,%0000000000011110,%0000000000010010,%0000000000011110,%0000000000000000,%0000000000000000,%0000000000000000)
Const Number_9(8) As Word = (%0000000000011110,%0000000000010010,%0000000000011110,%0000000000000010,%0000000000000010,%0000000000000000,%0000000000000000,%0000000000000000)

Inline Sub Sleep()                // inline sub for SLEEP command
Asm
Sleep
End Asm
End Sub

Interrupt TMR2_Interrupt(2)
Save(0)                   // Back up system variables

If TMR2IF = 1 Then        // Check if the interrupt was from TMR2
TMR2IF = 0            // Clear the TMR2 interrupt flag
// increment the mS variable
mS = mS + mS_Inc
If mS = 1000 Then
mS = 0
sWDT = sWDT + 1
If sWDT = 30 Then
IDLEN = 1     // Put the PIC in RC_IDLE mode on SLEEP
SCS1 = 1      //
SCS0 = 0      //
TRISA = \$FF   // Make all I/Os high impedance inputs
TRISB = \$FF
TRISC = \$FF
TRISD = \$FF
TMR2ON = 0    // Disable TMR2
// Configure oscillator for 31Khz
OSCCON = OSCCON And %10001111
Sleep
EndIf
EndIf

// Button Debounces
// Ensures the button has been activated before performing a debounce.
// Interrupt driven, the user has X mS between button presses.
// Prevents double presses and and minimize the chance of switch chatter.
// The sub routine 'CheckButtons' controls the state of various flags here.
// check if left button requires debounce
If Left_Debounce = 1 Then
If Left_Delay = 0 Then
If Left = 1 Then
Left_Debounce = 0
EndIf
Else
Left_Delay = Left_Delay - mS_Inc
EndIf
EndIf
// check if the right button requires debounce
If Right_Debounce = 1 Then
If Right_Delay = 0 Then
If Right = 1 Then
Right_Debounce = 0
EndIf
Else
Right_Delay = Right_Delay - mS_Inc
EndIf
EndIf
// check if the rotate button requires debounce
If Rotate_Debounce = 1 Then
If Rotate_Delay = 0 Then
If Rotate = 1 Then
Rotate_Debounce = 0
EndIf
Else
Rotate_Delay = Rotate_Delay - mS_Inc
EndIf
EndIf
// check if the down button requires debounce
If Down_Debounce = 1 Then
If Down_Delay = 0 Then
If Down = 1 Then
Down_Debounce = 0
EndIf
Else
Down_Delay = Down_Delay - mS_Inc
EndIf
EndIf

// Event Handler - Object Drop
// Once every 800mS, a flag is set to initiate the next object drop.
// This is one of few classic features of tetris
If mS = 800 Then
DropObject = 1
End If

// Displays content on 64 LEDs via multiplexing
// Ensure multiplexing is enabled.
// This is vital to protect shared variables such as ObjectData and BackgroundData.
// Whenever a write to either is done outside of the interrupt, multiplexing should be disabled.
// disable ALL x axis
PORTA = PORTA Or \$0F
PORTB = PORTB Or \$F0

// fill y axis
PORTC = ObjectData(CurrentX).Byte0 Or BackgroundData(CurrentX).Byte0
PORTD = ObjectData(CurrentX).Byte1 Or BackgroundData(CurrentX).Byte1

// enable ONE x axis (the correct one for current Y axis)
If CurrentX < 4 Then
PORTA.bits(CurrentX) = 0
Else
PORTB.bits(CurrentX) = 0
EndIf

// increment x axis, ready for the next multiplex
Inc(CurrentX)
If CurrentX = 8 Then
CurrentX = 0
EndIf

// a general purpose flag to inidcate the completion of an interrupt
InterruptComplete = 1
EndIf
EndIf
Restore
End Interrupt

// Loop until an interrupt occurs.
// Useful for semaphores - allows shared resources to be used with little (no) impact on timing
Sub WaitForInterrupt()
InterruptComplete = 0
Repeat
Until InterruptComplete = 1
End Sub

// This is the primary semaphore routine. If the program uses shared resources, this routine is called.
Sub PauseMultiplexing()
WaitForInterrupt
'    TMR2IE = 0
End Sub

// Enables multiplexing to resume
Sub ResumeMultiplexing()
'    TMR2IE = 1
End Sub

// clears the passed array of type Word
Sub ClearArray(ByRef pArray() As Word)
Dim i As Byte

For i = 0 To Bound(pArray)
pArray(i) = \$0000
Next
End Sub

// select a random object based on the number of objects defined
Sub SelectNextObject(ByRef pTarget() As Word)
Dim RndSelection As Byte
Dim i As Byte
Dim Counter, Selection As Byte

// create a random number from 0-255
RndSelection = RandGen.Rand

// make a selection based on the random number created
Counter = 0
Selection = 0
Repeat
Counter = Counter + RndWindow
Selection = Selection + 1
Until Counter >= RndSelection

// initalise object variables
CurrentRotation = 0

Select Selection
Case 1
For i = 0 To 7
pTarget(i) = Graphic0(i)
Next
OriginX = Graphic0_X
OriginY = Graphic0_Y
LimitedRotation = LimitedRotation0
Case 2
For i = 0 To 7
pTarget(i) = Graphic1(i)
Next
OriginX = Graphic1_X
OriginY = Graphic1_Y
LimitedRotation = LimitedRotation1
Case 3
For i = 0 To 7
pTarget(i) = Graphic2(i)
Next
OriginX = Graphic2_X
OriginY = Graphic2_Y
LimitedRotation = LimitedRotation2
Case 4
For i = 0 To 7
pTarget(i) = Graphic3(i)
Next
OriginX = Graphic3_X
OriginY = Graphic3_Y
LimitedRotation = LimitedRotation3
Case 5
For i = 0 To 7
pTarget(i) = Graphic4(i)
Next
OriginX = Graphic4_X
OriginY = Graphic4_Y
LimitedRotation = LimitedRotation4
Case 6
For i = 0 To 7
pTarget(i) = Graphic5(i)
Next
OriginX = Graphic5_X
OriginY = Graphic5_Y
LimitedRotation = LimitedRotation5
Case 7
For i = 0 To 7
pTarget(i) = Graphic6(i)
Next
OriginX = Graphic6_X
OriginY = Graphic6_Y
LimitedRotation = LimitedRotation6
// else statement, just in case (prefer not to, but hey)
Else
For i = 0 To 7
pTarget(i) = Graphic0(i)
Next
OriginX = Graphic0_X
OriginY = Graphic0_Y
LimitedRotation = LimitedRotation0
End Select
End Sub

// Merge two arrays with the selected mode
// 0 - Overwrite
// 1 - Or
Sub MergeObjects(ByRef pSource() As Word, ByRef pTarget() As Word, ByVal pMode As Byte)
Dim i As Byte
Dim tmpWord As Word

If pMode = 0 Then
For i = 0 To Bound(pSource)
pTarget(i) = pSource(i)
Next
ElseIf pMode = 1 Then
For i = 0 To Bound(pSource)
tmpWord = pTarget(i) Or pSource(i)
pTarget(i) = tmpWord
Next
EndIf
End Sub

// Move an objects y axis in pDirection
// 0 - move object down
// 1 - move object up
Sub MoveObjectY(ByRef pObject() As Word, pDirection As Byte, ByVal pCycles As Byte)
Dim i,c As Byte
Select pDirection
Case 0
For c = 1 To pCycles
For i = 0 To Bound(pObject)
pObject(i) = pObject(i) << 1
Next
OriginY = OriginY + 1
Next
Case 1
For c = 1 To pCycles
For i = 0 To Bound(pObject)
pObject(i) = pObject(i) >> 1
Next
OriginY = OriginY - 1
Next
End Select
End Sub

// Move an objects x axis in pDirection
// 0 - move object right
// 1 - move object left
Sub MoveObjectX(ByRef pObject() As Word, pDirection As Byte)
Dim i As Byte

Select pDirection
Case 0
For i = 7 To 1 Step -1
pObject(i) = pObject(i-1)
Next
pObject(0) = 0
OriginX = OriginX + 1
Case 1
For i = 0 To 6
pObject(i) = pObject(i+1)
Next
pObject(7) = 0
OriginX = OriginX - 1
End Select
End Sub

// check the passed object to see if it has reached the bottom
// return true if so
Function CheckForBottom(ByRef pObject() As Word) As Byte
Dim i As Byte

Result = 0
For i = 0 To Bound(pObject)
If pObject(i).Bits(15) = 1 Then
Result = 1
EndIf
Next
End Function

// check the passed object to see if it is against the left wall already
// return true if so
Function CheckForLeftWall(ByRef pObject() As Word) As Byte
Result = 1
If pObject(0) = 0 Then
Result = 0
EndIf
End Function

// check the passed object to see if it is against the right wall already
// return true if so
Function CheckForRightWall(ByRef pObject() As Word) As Byte
Result = 1
If pObject(7) = 0 Then
Result = 0
EndIf
End Function

// compare the passed source and target for a collision
// return true if so
Function CollisionDetect(ByRef pSource() As Word, ByRef pTarget() As Word) As Byte
Dim i As Byte
Dim tmpWord As Word

Result = 0
For i = 0 To Bound(pSource)
tmpWord = pSource(i) And pTarget(i)
If tmpword > 0 Then
Result = 1
Break
EndIf
Next
End Function

// An extremely quick way to rotate objects +90/-90 degrees.
//
// Notes:
// Work out the centre of the block (to be used as a pivot point), i.e. the centre of the block shape. Call that (px, py).
// Each brick that makes up the block shape will rotate around that point. For each brick, you can apply the following calculation.
// Where each brick's width and height is q, the brick's current location (of the upper left corner) is (x1, y1) and the new brick location is (x2, y2):
// x2 = (y1 + px - py)
// y2 = (px + py - x1 - q)
// To rotate the opposite direction:
// x2 = (px + py - y1 - q)
// y2 = (x1 + py - px)
//
// Based on a 2D affine matrix transformation.
Sub NewRotation(ByRef pSource() As Word, ByRef pTarget() As Word, ByVal pDirection As Byte)
Dim X2,Y2,Q,PX,PY As Integer
Dim X1,Y1 As Byte

// check to see if rotations are disabled for current object
If LimitedRotation = 2 Then
For X1 = 0 To 7
pTarget(X1) = pSource(X1)
Next
Else
// define object origin
PX = OriginX
PY = OriginY
// define Q
Q = 0

// clear the target array
For X1 = 0 To 7
pTarget(X1) = 0
Next

// if the object is limited by rotation, then reverse
// rotations 1 & 3. This will make the object turn like so
// CW, CCW, CW, CCW
If LimitedRotation = 1 Then
If CurrentRotation = 1 Then
pDirection = CCW
EndIf
EndIf
// analyse each pixel (working with 5x5 graphic)
For X1 = 0 To 7
For Y1 = 0 To 15
If pSource(X1).Bits(Y1) = 1 Then
If pDirection = CW Then
X2 = (PX + PY - Y1 - Q)
Y2 = (X1 + PY - PX)
Else
X2 = (Y1 + PX - PY)
Y2 = (PX + PY - X1 - Q)
EndIf
If X2 >= 0 And X2 <= 7 Then
If Y2 >= 0 And Y2 <= 15 Then
pTarget(X2).Bits(Y2) = 1     //pDestinationObject(GetIndexY(Int(Y2)), GetIndexX(Int(X2))) = 1
EndIf
EndIf
End If
Next
Next
EndIf
End Sub

// Count the number of pixels that equal "1". Standard bit-wise operators did not suit
// this routine as pixels could very well end up ANYWHERE after a rotation.
Function PixelCount(ByRef pSource() As Word) As Byte
Dim X,Y As Byte

Result = 0
For X = 0 To 7
For Y = 0 To 15
If pSource(X).Bits(Y) = 1 Then
Result = Result + 1
EndIf
Next
Next
End Function

// Drop the object by one Y increment. Return 0 if failed.
Function MoveObjectDown(ByRef pObject() As Word) As Byte
Result = 1
// Semaphore, to protect shared variables
PauseMultiplexing
// check for a collision with the bottom of the screen
If CheckForBottom(pObject) = 1 Then
// ... yes it has - time to save it there
Result = 0
// OR the object into the background
MergeObjects(pObject,BackgroundData,1)
// get a new object up-and-running
SelectNextObject(pObject)
// check for new lines
CheckForNewLines = 1
Else
// Move the object down
MoveObjectY(pObject,0,1)
// check for a collision with the background
If CollisionDetect(pObject,BackgroundData) = 1 Then
// the object HIT something in the background. The object needs to be
// moved BACK UP and saved to the background.
Result = 0
// move the object up one
MoveObjectY(pObject,1,1)
// move the object to the background (OR it over)
MergeObjects(pObject,BackgroundData,1)
// get a new object up-and-running
SelectNextObject(ObjectData)
// check if the object has collided with the background already. If yes, then
// the game is over.
If CollisionDetect(pObject,BackgroundData) = 1 Then
Result = 0
EndOfGame = 1
EndIf
// it's always possible that new lines may have been made already
CheckForNewLines = 1
EndIf
// resume multiplexing, shared resources are done with
EndIf
ResumeMultiplexing
End Function

// Check the state of each button. Because this routine is called often, the chances of missing a button press
// are very unlikely.
Sub CheckButtons()
// check if left button pressed
If Left = 0 And Left_Debounce = 0 Then
// semaphore - shared variables are about to be accessed
PauseMultiplexing
// ensure the object is not ALREADY against the left wall
If CheckForLeftWall(ObjectData) = 0 Then
// move the object to a temporary buffer
MergeObjects(ObjectData,tmpObjectData,0)
// Decrement the x axis
MoveObjectX(tmpObjectData,1)
// ensure no collisions with background
If CollisionDetect(tmpObjectData,BackgroundData) = 0 Then
// all went well, merge the array to the working buffer
MergeObjects(tmpObjectData,ObjectData,0)
// enable the debounce timer (prevents double presses etc)
Left_Delay = DebounceDelay
Left_Debounce = 1
// always a chance that a new line was just made
CheckForNewLines = 1
EndIf
EndIf
ResumeMultiplexing
EndIf
// check if the right button is pressed
// NOTE: THESE COMMENTS ARE MUCH THE SAME AS ABOVE, excluded for that reason
If Right = 0 And Right_Debounce = 0 Then
// semaphore - shared variables are about to be accessed
PauseMultiplexing
// ensure not already on the right wall
If CheckForRightWall(ObjectData) = 0 Then
MergeObjects(ObjectData,tmpObjectData,0)
MoveObjectX(tmpObjectData,0)
// ensure no collisions with objects
If CollisionDetect(tmpObjectData,BackgroundData) = 0 Then
MergeObjects(tmpObjectData,ObjectData,0)
Right_Delay = DebounceDelay
Right_Debounce = 1
CheckForNewLines = 1
EndIf
EndIf
ResumeMultiplexing
EndIf
// check if rotate button is pressed
If Rotate = 0 And Rotate_Debounce = 0 Then
// semaphore - shared variables are about to be accessed
PauseMultiplexing
// new rotation method, named accordingly, more info there
NewRotation(ObjectData,tmpObjectData,CW)
ResumeMultiplexing
// ensure nothing has fallen off due to near-wall rotations
If PixelCount(ObjectData) = PixelCount(tmpObjectData) Then
// check for object collision due to rotation
If CollisionDetect(tmpObjectData,BackgroundData) = 0 Then
// FINALLY, if it gets here then ALL WENT WELL.
// Temp buffer merged with working buffer
PauseMultiplexing
MergeObjects(tmpObjectData,ObjectData,0)
ResumeMultiplexing
// update the current angle (0=0, 1=90, 2=180, 3=270)
CurrentRotation = CurrentRotation + 1
If CurrentRotation = 2 Then
CurrentRotation = 0
EndIf
Rotate_Delay = DebounceDelay
Rotate_Debounce = 1
EndIf
EndIf
EndIf
// check if down is pressed
If Down = 0 And Down_Debounce = 0 Then
Down_Delay = DebounceDelay
Down_Debounce = 1
// move the current object straight to the bottom
Repeat
Until MoveObjectDown(ObjectData) = 0
EndIf
End Sub

// remove the line at location pY
Sub RemoveLine(ByRef pObject() As Word, ByRef pY As Byte)
Dim X,Y,CurrentLine As Byte

// shift each line down
For Y = pY-1 To 0 Step - 1
CurrentLine = Y+1
For X = 0 To 7
pObject(X).Bits(CurrentLine) = pObject(X).Bits(Y)
Next
Next
// clear the top line
For X = 0 To 7
pObject(X).Bits(0) = 0
Next
End Sub

// remove COMPLETED lines & return the number of lines found
Sub CheckForLines(ByRef pObject() As Word)
Dim X,Y,Pixels As Byte

For Y = 0 To 15
Pixels = 0
For X = 0 To 7
If pObject(X).Bits(Y) = 1 Then
Pixels = Pixels + 1
EndIf
Next
If Pixels = 8 Then
RemoveLine(pObject, Y)
NumberOfLines = NumberOfLines + 1
EndIf
Pixels = 0
Next
End Sub

// initialise TMR2
Private Sub Initialise_TMR2()
TMR2ON = 0                // Disable TMR2
TMR2IE = 0                // Turn off TMR2 interrupts
mS = 0                    // Initialise mS
PR2 = 199                 // TMR2 Period register PR2
T2CON = %00100011         // T2CON 0:1 = Prescale
//        00 = Prescaler is 1:1
//        01 = Prescaler is 1:4
//        1x = Prescaler is 1:16
//      3:6 = Postscale
//     0000 = 1:1 postscale
//     0001 = 1:2 postscale
//     0010 = 1:3 postscale...
//     1111 = 1:16 postscale

TMR2 = 0                  // Reset TMR2 Value
TMR2IE = 1                // Enable TMR2 interrupts
TMR2ON = 1                // Enable TMR2 to increment
Enable(TMR2_Interrupt)
End Sub

// initialise the program
Sub InitialiseProgram()
// make all inputs digital
Utils.SetAllDigital

// configure I/O
TRISB = \$0F
PORTB = \$00
TRISC = \$00
PORTC = \$00
TRISD = \$00
PORTD = \$00
TRISA = \$00
PORTA = \$11
// enable PORTB weakpullups
INTCON2.7 = 0
// intialise variables
ClearArray(BackgroundData)
ClearArray(ObjectData)
OriginX = 0
OriginY = 0
CurrentX = 0
NumberOfLines = 0
mS = 0
// initialise events
DropObject = 0
EndOfGame = 0
CheckForNewLines = 0
sWDT = 0

// initialise buttons
Left_Debounce = 0
Left_Delay = 0
Right_Debounce = 0
Right_Delay = 0
Rotate_Debounce = 0
Rotate_Delay = 0
Down_Debounce = 0
Down_Delay = 0
End Sub

Sub SplashScreen()
Dim i As Byte

// semaphore - shared variables are about to be accessed
PauseMultiplexing

For i = 0 To 7
ObjectData(i) = TETRIS(i)
Next

ResumeMultiplexing

DelayMS(3500)
End Sub

Sub GetNumber(ByRef pDigit As Byte, ByRef pTarget() As Word)
Dim i As Byte
Select pDigit
Case 0
For i = 0 To 7
pTarget(i) = Number_0(i)
Next
Case 1
For i = 0 To 7
pTarget(i) = Number_1(i)
Next
Case 2
For i = 0 To 7
pTarget(i) = Number_2(i)
Next
Case 3
For i = 0 To 7
pTarget(i) = Number_3(i)
Next
Case 4
For i = 0 To 7
pTarget(i) = Number_4(i)
Next
Case 5
For i = 0 To 7
pTarget(i) = Number_5(i)
Next
Case 6
For i = 0 To 7
pTarget(i) = Number_6(i)
Next
Case 7
For i = 0 To 7
pTarget(i) = Number_7(i)
Next
Case 8
For i = 0 To 7
pTarget(i) = Number_8(i)
Next
Case 9
For i = 0 To 7
pTarget(i) = Number_9(i)
Next
End Select
End Sub

Sub ShowScore(ByRef pScore As Word)
Dim i As Byte
Dim CurrentNumber As Byte

// semaphore - shared variables are about to be accessed
PauseMultiplexing

// clear all graphic buffers
ClearArray(tmpObjectData)
ClearArray(ObjectData)
ClearArray(BackgroundData)

For i = 1 To 3
CurrentNumber = Utils.Digit(pScore,i)
GetNumber(CurrentNumber, tmpObjectData)
// shift the result down, depending on index
Select i
Case 2
MoveObjectY(tmpObjectData,0,5)
Case 3
MoveObjectY(tmpObjectData,0,10)
End Select
// or result with graphic object
MergeObjects(tmpObjectData,ObjectData,1)
Next

ResumeMultiplexing

// wait for "down" button to be pressed

// perform button debounce
Repeat
DelayMS(10)
Until Down = 1

// wait for button to be pressed
Repeat
Until Down = 0

// perform button debounce
Repeat
DelayMS(10)
Until Down = 1
End Sub

Dim tmpByte As Byte
Dim LastHighScore As Word

If tmpByte = \$77 Then
ShowScore(LastHighScore)
Else
LastHighScore = 0
EE.Write(0,\$77,LastHighScore)
ShowScore(LastHighScore)
EndIf
End Sub

Sub WriteHighScore()
Dim LastHighScore As Word

If NumberOfLines > LastHighScore Then
EE.Write(1,NumberOfLines)
EndIf
End Sub

// this is the main game loop for Tetris
Sub MainGameLoop()
// initialise screen and variables
InitialiseProgram

// enable screen multiplexing & other timer functions
Initialise_TMR2

// display the flash screen
SplashScreen

// Show last high score from EEPROM

// re-initialise screen and variables (splash screen and highscores may have altered stuff..)
PauseMultiplexing
InitialiseProgram
Initialise_TMR2
ResumeMultiplexing

// select an object and place it in ObjectData
SelectNextObject(ObjectData)

Repeat
// check if drop object flag has been set (invoked occurs by the interrupt "TimerObjectDrop")
If DropObject = 1 Then
DropObject = 0
MoveObjectDown(ObjectData)
EndIf
// check for completed lines. any routine that moves an object should enable this flag.
If CheckForNewLines = 1 Then
CheckForLines(BackgroundData)
EndIf
// scan buttons and action as necessary
CheckButtons()
// reset software watch dog timer
sWDT = 0
// loop forever, or until the EndOfGame event is set true
Until EndOfGame = 1

// write the high score to EEPROM
WriteHighScore()
// display the score!
ShowScore(NumberOfLines)
End Sub

// main program start

// seed randgen module
RandGen.Initialize(\$77)

While True
// enter main game program, will stay there until an END event occurs
MainGameLoop
Wend```

Schematic

Some considerations with this schematic:

• I have used two 8x8 common cathode LED matrix. I haven't included any pinout into for them as the pinout varies so much between manufacturers. You will need to check the datasheet for your specific matrix.
• The Y connections are the positive side of the LEDs.
• The X connections are the negative side of the LEDs.
• Resistors could be any value under 200ohms (lower until you have the desired brightness - I used 80ohms).
• Please try a "blinky" program on the PIC should your circuit not fire up and work the first time. I can guarantee you that this layout works; I used the netlist from this very schyematic to develop my PCB.
• The symbol of a triangle with a line through it signifies Vdd (5V). Such ULN2803 Pin 10.

Other tips:

• For common anode LED type displays; you will need to modify the source code. You will have to change the ULN2803 for a 8-Channel Source Driver such as UDN2803A.

Click to enarlge the image. Note: I have used the low voltage variant of the above PIC Micro, its part number is 18LF4520.

The PCB

Due to popular request, I'm also sharing the Gerber files for the PCB shown below:

Posted: 8 years 9 months ago
Graham made it to Hack-a-Day! Way to go Graham!

Posted: 8 years 9 months ago
Nice. The PIC will run with just 3.0V directly from the batteries? Is it a 3.3V device?
Posted: 8 years 9 months ago
O nice Jon, thanks for the update That's my first time on Hack-a-Day!
Nice. The PIC will run with just 3.0V directly from the batteries? Is it a 3.3V device?
That's correct Steve - When buying from Microchip, simply get the "L" version of the PIC you want. In my case it was the 18LF4520.

The above PIC will run at 8 million instructions per second on battery power.
Posted: 8 years 9 months ago
Fantastic project Graham!

Thanks for sharing this with us all. I'm looking forward to making this with my son as his first micro project.

As we have heaps of them, we will be using LED's instead of LED matrix's, I'm trying to work out the polarity of the ones used in your schematic, do the Y's represent the Anodes or Cathodes of the LED's please?

thanks!
Posted: 8 years 9 months ago
do the Y's represent the Anodes or Cathodes of the LED's please?
The devices I was using were common cathode types. 16-bits of row (Y) data are shifted in and then the appropriate column (X) is enabled. With that said, it is extremely easy to use either common anode/cathode devices - just a couple of minor edits.
Posted: 8 years 9 months ago
Thanks for clearing that up Graham, by the looks you aren't using the dual color mode? So i'll make a pcb with our mini-cnc to use 128 x 5mm LED's instead. This way the code doesn't need to be changed as i'll make the 128 LED's appear the same electrically as 2 x 8x8 LED Matrixes.
Posted: 8 years 9 months ago
Sounds like you guys have quite the hobby setup! I'm really interested to see the progress of your project - perhaps you could throw the odd post in the forum? digital-diy.com/forum/
Posted: 8 years 9 months ago
Be more than happy too! To cut down on wear and breakages on the bits (which cost us a bundle), we use isolation routing, so it's quite a big job to create th NC files from the vector graphic layout. I've just finished the LED portion of the board, after sorting out how to get the Cathode columns lined up vertically while connecting the Anodes horizontally.

When we're done, assuming it actually works, I'll post a vid of the cnc in action making the board (computer controlled router = COOL!), and put up the NC files and my drawings to make it easier for those wanting to replicate it.

I see you have a debouncedelay constant defined, so i'm guessing we can safely put in a reset switch from the Pos power input?

cheers,
Ian
Posted: 8 years 9 months ago
Hi Graham, i managed to make an Eagle layout for your project, and tweaked it a bit, adding a power supply section using a LM7805 (to use a 9v battery).
The led matrix i used is made by LITE-ON, the model is "LTP-14088A-03"
I produced a master ready to be printed and used to etch a pcb.

You can find all the stuff here at my website:

www.eddieslab.com/?p=1544

Along with a small video showing the final product (I'm sorry it is in italian, i can make one in english if needed).

Hope you like it and if you think that offended in some ways your intellectual property, just let me know, i will take it off.

Cheers,

Enrico Castellani
Posted: 8 years 9 months ago
Hi Enrico,

Very impressive result - and thank you for the attribution. I've added your comment to the article as a source for the PCB Layout

Forum Activity

• No posts to display.