Anant Desai (ahd76)

Jeremy Storey (jks284)

 

Project - VR Sword Defense Video Game

 https://lh3.googleusercontent.com/TlYTRtldHXN00B6FvcoeB4ToeCOwHcTcS5YKIDewFcsfFBVuNZtcUDGhis3DPkyd7weDzfW5s_Ln1WCju1oQiGW5Rho7N98Vn8leLnyWKHzs_6L_3PQkX1PTEvLwR-JK2KFnxGJx

https://www.youtube.com/watch?v=TVv_XRQA7QU&t=

Introduction

 

In this project, we constructed a virtual reality (VR) video game. The video game uses several peripherals to give the user an immersive gaming experience. This includes providing visual, auditory, and tactile feedback from the game. The game features a headpiece with a TFT display. The display shows incoming projectiles that the player must deflect/eliminate with a sword. Upon impact, the player receives an unpleasant sound through the earpiece and tactile feedback via the vibration motors in the headpiece. A magnetometer at the top of the headpiece causes the screen to change based on the direction the player is facing. The objective of the game is to survive as long as possible. The goal of the project is to provide an enjoyable yet safe immersive gaming experience.

 

High Level Design

 

In the game, the user wears a headpiece with a TFT display on it. This TFT displays projectiles (balls) that are moving towards the player. There can be a projectile incoming on the left or the right of the screen. Additionally, the TFT displays the player’s score and health. The score is how long in seconds that the user has survived the game. The health begins at 5 and decrements every time the user gets “hit” by one of the balls. When the health reaches 0, the game terminates. There is a game reset button which allows the player to continue with another round of the game.

 

The headpiece features a magnetometer, which determines which way the headpiece is pointing. Based on this, the screen that the TFT is displaying will change. This gives the impression to the player that the game’s screen changes based on the direction that the player is facing. This is a pivotal part of the immersive experience. In total, there are 4 possible screens. The figure below illustrates what the virtual environment looks like for one ball. The “stages” are meant to illustrate the ball increasing in size as it gets closer to the user. Screens 1-3 illustrate different screens the user is not currently looking at.

 

https://lh4.googleusercontent.com/Gd3DpDB_GX12APS20YtBGyq34XJVzDFniZNbM9sKZc7cGjBpSaiRSdb9NQgXa8UzkL1yg6F8Dew-HqBB9XDgIkfCsi1m2ymi-fvDR3UFBviDa9FPTz1-k92wIggC3RWta59J8KFp

 

To combat the incoming projectiles, the player is armed with a sword. The sword contains accelerometers at its tip to detect when the sword has been swung and when the player is swinging the sword left/right. When the ball is close enough, the player must swing the sword in the correct direction to deflect/eliminate the incoming projectile.

 

If the player gets hit by a ball, there is auditory and tactile feedback. The auditory feedback consists of a sharp, slightly distasteful, sound to indicate to the user that he or she has been hit. To allow for tactile feedback, the headpiece has vibration motors placed on the light, right and back side. When the user gets hit from the left, right, or back, then the appropriate vibration motor will vibrate. If the user gets hit from the front, all three vibration motors will vibrate.

 

There were hardware software tradeoffs in the form of adding hardware complexity tended to add software complexity. In general, changing hardware created more software work, whereas software changes didn’t change much of the hardware.

 

This project was the result of brainstorming between the group members with the goal of creating a game that involved swinging a physical sword to control a video game.

 

Program/Hardware Design

 

https://lh5.googleusercontent.com/vfEBWCSb72Nn-kgsUCeX1-9dPqLhatceeHbWS7pvCeI9pC3tnTsSrTM3W0BcIb3yTq5hArqY_FQW-fOk3e7vpWiWFdA5J5q_IcG_H37wBk8fx05dBbpM_JnGMXW90BsqAy3m35nR

Hardware Description:

 

A full hardware schematic is available in Appendix B. For the following, refer to the schematic to find the specific “port pins” being mentioned.

 

The first piece of hardware to be constructed was the sword. Two 1-dimensional accelerometers were soldered together at a 90 degree angle to essentially create a 2-dimensional accelerometer, capable of noting the general direction of a sword swing. These accelerometers were placed in the tip of a recycled, light up, plastic sword with wires from the accelerometers coming out of the handle of the sword. The accelerometers provided values between 0 and 3.3V, corresponding to the force being applied to the accelerometer. As such they were connected to the ADC inputs of the PIC32 for interpretation via 330Ω resistors.

 

The PIC32 port pins could not supply the required 70mA of current to power the three vibration motors. A simple current driver was constructed using three 2N3904 transistors with the collector connected to power via a vibration motor. The base was connected to one of the port pins of the PIC32 via a 1kΩ resistor. This pin was internally pulled down, and pulled high when the program told the motor to vibrate.

 

The vibration motors were placed in the helmet to provide the user tactile feedback when being hit by a ball. There were three motors in total: left, back, and right. Being hit from the front triggered a special condition. The screen and circuitry for the rest of the device were attached to the visor of the helmet. The screen was quite close to the users face when wearing the helmet, so a Fresnel lens was attached to the front of a pair of safety glasses to help the user focus on the screen.

 

The simplest part of the schematic was the restart button. This was a push button connected to ground on one side and a PIC32 port pin via a 330Ω resistor. This pin was internally pulled high. The restart button is meant to signal the software to restart the game when pushed after the game has ended.

 

The audio output circuit takes the audio signal from the DAC, passes it through a low-pass filter, puts it into an audio jack, and outputs the sound to an external speaker. The audio jack’s left and right audio pins are tied together since there was no differentiation between left and right audio. Since the design only required a short sound upon being hit, an audio filter to increase the quality of sound was not constructed.

 

Software Description:

 

The software design consists of 5 protothreads, 1 interrupt service routine, 2 helper functions for I2C, and 1 main function. First, we will describe the main function and all the peripherals it sets up along with the variables it initializes. Then we will discuss the purpose of the interrupt service routine and the need for it. Lastly, we will go through the 5 protothreads, describe each one’s functions, and how they intertwine. When describing the communication over I2C with the magnetometer in one of the protothreads, we will describe the read and write I2C functions in detail.

 

The main function first sets up timers 2 and 3. Timer 2 is set with a period of 400000 so that it completes 100 cycles every second. Timer 3 is set with a period of 909 so that it runs at a rate of 44 KHz. Timer 2 is then set up to be used to trigger an interrupt, while Timer 3 is used to initiate DMA transfer for audio. Then, the main function sets up the SPI channel so that the PIC32 can send data to the DAC in order to produce sound. Next, the I2C channel is set up and I2C1 is enabled. Along with this, we write certain control bits in the magnetometer to bring the magnetometer to active mode from standby mode so that its registers can be read. Next, the ADC is initialized with auto alternate mode, which allows the two specified analog input channels to be read in an alternate fashion via subsequent calls of the ReadADC10 function. Then, certain pins are designated as digital outputs for controlling the vibration motor current drivers. The DMA channel is initialized with Timer 3. The protothreads and TFT are then initialized and system wide interrupts are enabled. The variables for the game (health, score, restart) and the valid bits for the all the screen are initialized to appropriate values before the game commences. Then the 5 protothreads are scheduled round robin.

 

The interrupt service routine, which executes at 100 Hz, is used to read and process analogy inputs from the accelerometers on the sword. First the ISR checks if the previous sword swing has been processed. If not, then the ISR just ends. Otherwise, we are accepting new sword input. In this case, we read from the ADC to obtain the value for the vertical and horizontal accelerometers. If the vertical accelerometer’s value indicates that the sword is swinging downward, then the horizontal accelerometer value is checked and we determine if the sword is swinging left or right. Then, the accept_sword_input variable is set to false so that the next time the ISR occurs we will not accept further sword input until the current swing is processed. We need to use an ISR to read sword input rather than simply reading it during a thread because the time scale of the accelerometer is too fast to be read by a msec timed thread.

 

Next we will describe the five protothreads: change screen, display screen, update screen, sword, and sec. The first protothread, change screen, reads from the magnetometer using i2c read. The i2c read function is modeled off of the i2c read function from the Blimp-F-O project (http://people.ece.cornell.edu/land/courses/ece4760/FinalProjects/f2015/bjr96_jl2628/bjr96_jl2628/bjr96_jl2628/index.html). The i2c read function starts an I2C transaction by transmitting the start bit. Then the i2c read function write the slave address of the magnetometer for writing followed by the register to read from. Then the i2c read function restarts the i2c transaction and then it writes the slave address of the magnetometer for reading. Then the i2c read function reads the register and does not acknowledge the result to end the transaction. Based on the values read from the magnetometer, the change screen thread determines if the current screen should change. If it should, then the current thread is cleared. Lastly, the change screen thread displays the player’s stats, such as health and score.

 

The next protothread is the display screen thread. The display screen thread simply checks if the left and right sides of the current screen are valid (i.e. if they have a projectile that should be displayed). If is is valid, then this thread draws the projectile onto the screen.

 

The next protothread is the update screen thread. The update screen thread iterates through all of the screens and checks if any of the projectiles have hit the player. If one of the projectiles has hit the player, then that projectile is eliminated off of its respective screen. The health counter is then decremented. Furthermore, the DMA transfer for the hit sound is initiated. Lastly, the appropriate vibration motor is turned on. For example, if the ball that hit the player is on the screen that is behind where the player is currently looking, then the vibration motor on the rear of the helmet will vibrate. If the user was hit from the left, the left vibration motor will vibrate. If the user is hit from the front, all three motors vibrate. After a quarter of a second, all vibration motors are turned off.

 

The next protothread is the sword thread. The sword thread uses the variables set by the ISR to handle the sword event. Specifically, if there was a left sword swing detected and there is a valid projectile on the left of the current screen, then that projectile is marked as invalid and disappears off the screen. The same is true for the right sword swing and a projectile on the right side of the screen. Once the sword input is processed, the accept_sword_input variable is marked as true so that the ISR can process further accelerometer input.

 

The last protothread is the sec thread. This thread keeps a running count of the number of seconds that the game has been active. These number of seconds also correspond to the score of the player. If the player’s health hits 0, then this thread clears the screen, displays “GAME OVER” with the score, and then waits in a while loop for the restart button to be pressed. If the restart button is pressed, then the game variables are reinitialized. This means that all screens are marked as invalid (i.e. there are no valid projectiles on any screen), the health is reinstated at 5, and the score begins again at 0. The screen is then cleared for a new game.

                         

Results

 

https://lh3.googleusercontent.com/bc_eXtfhmKHscxgK-B1Z1x_zf4fnbMZNqW4jX8Hq0tV8dkVRZS5rNWJdzjUGOtNkPw4M50PWHJW9vMAoUn18DrBJ3hvwDEskh-p-8Ea_FkjbMRUWw92spgCZregq_-GnsRT9GD7P

 

Because this was a video game, a lot of the testing dealt with user experience. This meant that if the game looked like it was functioning correctly, then it was functioning correctly. The major aspects of this testing dealt with making sure we were reading from the accelerometers fast enough to capture the sword motions and to make sure we were reading from the magnetometer fast enough to enable smooth transitions of the screen. Lastly, we needed to ensure the important protothreads were getting enough CPU time to render the game and make the visuals look believable. We describe these three aspects of testing further.

 

First, we worked with the accelerometers. Because of the small time scale of the sensor output when the accelerometer was perturbed about an inertial state, we needed to embed the ADC reading in an interrupt service routine. After slight experimenting with the frequency of the ISR, we determined that 100 Hz was sufficient to capture the behavior of the accelerometer. Having the ISR frequency be lower than this would have resulted in missing information, but having it much higher would cause unnecessary CPU time wasted when the ISR is triggered unnecessarily often. For the accelerometer, when the sensor accelerates in one direction, we see a spike in the output voltage, but when the sensor slows down (decelerates), there is an equivalent negative spike in voltage. It was important to sample fast enough to ensure we were able to capture the first spike so that we could accurately determine the direction of the sword motion.

 

Next, we worked with the magnetometer. The magnetometer didn’t need to be read as frequently as the accelerometer. This is because we only needed to read the magnetometer as often as we wanted to allow the screen to change. This meant that we could put the code that read from the magnetometer via I2C in the protothread that was in charge of changing which screen the user was viewing. This allowed us to couple the magnetometer reading and the rate of screen change, since these two values should match up. Specifically, it does not do us any good if we read from the magnetometer more often than we are updating which screen the user is looking at. Additionally, we seemed to be getting a significant amount of magnetic interference from the room as the magnetometer was somewhat unreliable at higher degrees of resolution.

 

Lastly, we needed to ensure the protothreads weren’t yielding for too long and that the game rendering had minimal flicker and glitches. The first place to check was the protothread that actually printed the projectiles on the screen. This thread had to execute often enough that the motion didn’t appear extremely rigid. Next came the protothread that handled the screen updates. This protothread only had to occur as frequently as the protothread that read magnetometer data/changed screens and the protothread that handled sword swings (since this protothread marked certain screens as valid/invalid). The final product was animated smoothly and was responsive in both that the screens changed properly when rotating the helmet and the sword destroyed the balls when the proper swing was performed. There was a noticeable amount of flicker whenever the balls progressed, but this did not impede the gameplay (the most important aspect). The screen was somewhat hard to focus on without the use of the focusing glasses. This would be solved in future iterations.

https://lh4.googleusercontent.com/1FVxm4P8xSzQVfwdGLgmExp9efoC57nQeEjHKqgU5cQcEOho9ZXafm3wlpJb3E_m0C6-ekcNFrZxA-X94etrPfxWs5wy4Ow4LHREyjduDbV8W7VAtW4_x5p_H_8Y2wWsTrs8BU7F

 

The board was powered by a relatively short 5V power chord. As such the range of motion was limited for the user. Additionally, the game involves swinging a sword at virtual objects, therefore some caution should be taken to ensure the sword won’t connect with anything in the physical realm. The game is designed to allow for rotational movement, but since the screen impedes the users vision, lateral movement should not be made while the helmet is worn. Excessive rotational movement in a single direction will likely cause the user to become tangled in the power cord, so it is advised to try to make balanced rotational movements. These safety considerations were taken into account when designing, constructing, and testing the device.

https://lh3.googleusercontent.com/PVM3WSJeVl3XwyYXhczz0yrgytMCZiuPdfWHBhQUrfmyzIVf2O0rGR1A2Qajsn1cn-XO2-uFOScJHiDRS0LPRQIMgrzqL5eHgNENxSV84PwzwSgX3CJHkvai1og0qOR9jm6waYQS

 

To use the system, plug the power cord into the board and place the helmet on the user’s head. Place the sword in the users hands with the yellow arrow facing the user and pointed at the ceiling. Wear the focusing lenses for better visual interaction with the game. Insert the earbuds and flip the power switch. The game will start automatically. The screen displays the user’s score (game time), health, and current screen. The user should rotate their head to look for balls on the screen. Note: try to keep the helmet level for best results. Upon finding a ball, swing downwards and left/right (corresponding to which side of the screen the ball is on). If the swing was successful, the ball will be destroyed. When a ball hits, a sound will be played and the user’s health will be decremented. Upon the health reaching zero, the game will end and the user’s score will be displayed. Press the restart button located above the visor to start a new game. Flip the power switch again to turn the system off.

https://lh4.googleusercontent.com/krhcfog_b2Ms18s-IUkvIHKXZ-2RvxzBYR5nN1DOkaRc8dWIZd5AZyGeTf7LR3NZwYbaNBxFsAgY1w2TxAcTYKVfhwHmHRrZFa0xpU7hmjrF_SJAEG2zYGv5oUTtkenxOKFSuLxQ

 

Conclusions

 

General Reflections:

             

The project was a great opportunity to not only gain experience with implementing the hardware and software of an embedded system, but also to design the whole project. It is this design portion that is crucial takeaway from this project. While the previous lab projects had most of the design complete, this term project was completely ours. Going through the design gave us an appreciation for the parts of the previous lab projects that were already provided. For example, in the lab projects, many hardware details had already been thought out and plenty of suggestions were provided along with all the components we would need. In this project, we had to design the hardware and then select hardware components that matched our needs. On the software side, the previous lab projects often had a suggested software implementation, right down to what threads should do what. In this project, we had the opportunity to design how the software would function and interact with the hardware from scratch. It was a very valuable experience.

 

In addition to the benefits gained from the design aspect, another less tangible but still important skill gained was time management. Because the scale of the project was 5 weeks, it was important to set goal and make sure we were achieving those goals at the specified times. Without this time management, it would have been impossible to complete the project while attempting to shove everything in the last two weeks.

 

The design lived up to most of our expectations. We had initially planned for six possible screens and a corresponding six vibration motors, but due to the unreliability of the magnetometer at that degree of resolution in addition to a lack of viable port pins for the motors, we dropped the number of possible screens to four. This provided a better user experience than if we had used six possible directions. We also had originally planned for there to only be one ball with a sword that would simply have to detect motion. During the project, we increased the complexity of this section by adding an additional accelerometer and corresponding ball. With this, we were able to make the user’s experience more interactive.

 

Safety Concerns:

           

As noted previously, there is the potential to accidentally hit real world objects with the sword. It’s recommended to clear a play area first and only make rotational movements while playing. Additionally, there is a chance the power cord may tangle the user if they make multiple rotations in the same direction. This can be mitigated by making balance rotational movements.

 

Issues Faced:

           

There were 4 slight hiccups faced throughout the project.

 

The first dealt with the sword and accelerometer. The original plan for the sword consisted of a single vertical swing, for which the single accelerometer would have sufficed. After implementing this, we decided to augment the design and allow for multiple balls on the screen. We wanted to add a left, center, and right ball on each screen. The accelerometer we had would only give us information in one dimension, so our solution was two pair two accelerometers, offset by 90 degrees, such that we could get information over a 2D plane. Even with this plan, it was difficult to get the resolution to support a left, center, and right ball, since the boundary for the center ball was difficult to calibrate. In the end, we decided to only include the left and right balls, which was already an improvement over the original design. If we had planned to incorporate multiple balls per screen from the beginning, then we would have most likely purchased a gyroscopic sensor in place of the accelerometer.

 

The second issue we faced was in finding the current to drive the vibration motors. The current they drew was too much to be sourced from the pins of the PIC32. The solution was to use simple current drivers. The current drivers consisted of transistors, whose gate was driven by the output of the pin. The source and drain of the transistor were connected to an external power supply in series with the vibration motors. This setup worked very well.

 

The third was that we ran out of viable port pins in the final stages of construction. We were faced with a decision to either include sound in the project or have four motors instead of three. We decided to take out a motor in order to include sound.

 

The last issue we faced was in interfacing with the magnetometer via I2C. There was a delivery delay causing the magnetometer to be delivered very late in the term. The ultimate solution to setting up the magnetometer was to write the appropriate control bits during the setup phase. To accomplish there, there was some simple trial and error and re-reading of the data sheet. After adding this setup, the magnetometer worked very well.

 

Further Improvements:

           

There are several potential improvements to the system. The sword could be made wireless if some from of wireless protocol were used to transmit data between the accelerometer sensor on the sword and the PIC32. The sword could also be programmed to have different movements do different things. For example if an “X” was coming at the user instead of a ball, an “X” motion could be made with the sword to destroy it. Additionally, if a more sophisticated magnetometer were used then more distinct screens would be possible and this would allow for smoother screen changes as the direction the player is facing changes. With a very sophisticated magnetometer, it may be possible to perform gradual, smooth screen updates more similar to the real world than the choppy method of updating the players entire field of view immediately upon reaching a threshold. The display and graphics of the game could be improved to make the projectiles look more realistic. Finally, the helmet interface could be improved so the user did not have to strain as much to focus on the screen. These would be the first improvements to the system given more time.

 

Appendix - A

 

The group approves this report for inclusion on the course website.

The group approves the video for inclusion on the course youtube channel.

 

Appendix B - Schematic

 

https://lh5.googleusercontent.com/4yutRGksdjG46ggPTxjin444auwbKozIspX8Ogf37Zxs7p-Qv1-ditF1F-mNDrB_g3lFCtztBUNd_4oeZSyIUblmOEJ1VtR6ijGbZJkf21MeQltrrD_fO3uBG-9veHTU5WMZld06

 

Appendix C - Parts list/vendors and costs

 

Part

Quantity

Vendor

Price

Big Board

1

ECE4760

$10.00

Bread Board

1

ECE4760

$6.00

PIC32MX250F128B

1

ECE4760

$5.00

TFT LCD

1

ECE4760

$10.00

Vibration Motor Product ID: 1201

3

Adafruit

$6.00

Headphones

1

Walmart

$10.00

Plastic Sword

1

Ithaca Reuse Center

$0.75

MAG3110

1

Sparkfun

$14.95

841-MMA1250KEG

2

ECE4760

$10.00

Helmet

1

Walmart

$14.50

Push Button

1

ECE4760

$0.50

Jumper Cables

17

ECE4760

$8.50

Transistor 2N3904

3

ECE4760

$1.26

Safety Glasses

1

ECE4760

$10.00

Fresnel Lens

1

ECE4760

$4.00

Total:

$111.46

 

Appendix D - Individual Contributions

 

Anant: Wrote the video game software skeleton code; Interfaced video game with accelerometers; designed logic to use two 1D accelerometers to determine a sword swing over 2D plane; interfaced video game with magnetometer via I2C; interfaced video game with vibration motors; set up DMA for sound effects; interfaced video game with DAC for sound effects; interfaced video game with additional user input button for reset; designed visual representation of video game on TFT screen.

 

Jeremy: Designed circuitry; obtained hardware and materials; constructed and tested sword with accelerometers installed; implemented multiple randomly generated balls on screen; implemented ball destruction upon proper sword swing; constructed and tested current drivers for vibration motors; constructed and tested helmet with vibration motors installed; constructed focusing glasses; added reset button.

 

Appendix E - References

 

The board used in this project was designed by Sean Carroll.

The base code was written by Syed Tahmid Mahbub.

The current driver design was provided by Bruce Land.

 

PIC32 Datasheet:

http://people.ece.cornell.edu/land/courses/ece4760/PIC32/Microchip_stuff/2xx_datasheet.pdf

 

PIC32 reference manual:

http://hades.mech.northwestern.edu/images/2/21/61132B_PIC32ReferenceManual.pdf

 

Appendix F - Code

 

/*

 * File:        project_ahd76_jks284.c

 * Author:      Anant Desai, Jeremy Storey

 *

 * For use with Sean Carroll's Big Board

 * Adapted from:

 *              main.c by

 * Author:      Syed Tahmid Mahbub

 * Target PIC:  PIC32MX250F128B

 */

 

////////////////////////////////////

// clock AND protoThreads configure!

// You MUST check this file!

#include "config.h"

// threading library

#include "pt_cornell_1_2_1.h"

#include "sounds.h" /* predefined sound arrays */

 

////////////////////////////////////

// graphics libraries

#include "tft_master.h"

#include "tft_gfx.h"

// need for rand function

#include <stdlib.h>

////////////////////////////////////

 

////////////////////////////////////

// pullup/down macros for keypad

// PORT B

#define EnablePullDownB(bits) CNPUBCLR=bits; CNPDBSET=bits;

#define DisablePullDownB(bits) CNPDBCLR=bits;

#define EnablePullUpB(bits) CNPDBCLR=bits; CNPUBSET=bits;

#define DisablePullUpB(bits) CNPUBCLR=bits;

//PORT A

#define EnablePullDownA(bits) CNPUACLR=bits; CNPDASET=bits;

#define DisablePullDownA(bits) CNPDACLR=bits;

#define EnablePullUpA(bits) CNPDACLR=bits; CNPUASET=bits;

#define DisablePullUpA(bits) CNPUACLR=bits;

////////////////////////////////////

 

////////////////////////////////////

// some precise, fixed, short delays

// to use for extending pulse durations on the keypad

// if behavior is erratic

#define NOP asm("nop");

// 1/2 microsec

#define wait20 NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;

// one microsec

#define wait40 wait20;wait20;

 

int dmaChn = 0; // the DMA channel to use

 

////////////////////////////////////

 

int raw_sword_input_1; // analog sword input from vertical accelerometer

int raw_sword_input_2; // analog sword input from horizontal accelerometer

int accept_sword_input = 1; // boolean whether to accept sword input or not

 

// sword threshold constants

#define DOWN_THRESHOLD 250

#define UP_THRESHOLD 530

#define LEFT_RIGHT_THRESHOLD 475

 

int button_1 = 0;

int button_2 = 0;

 

// screen number

int curr_screen = 0;

 

// player's health

int health = 5;

 

// index counters to be used with for loops

int i = 0;

int j = 0;

 

// game restart variable

int restart = 0;

 

// score, how long the player has survived

int game_time = 0;

 

// enum variable for left swing or right swing

typedef enum swing_type

{

    none, left, right

} swing_type;

 

// swing variable

swing_type swing = none;

 

// constant for number of screens

#define NUM_SCREENS 4

 

typedef enum state

{

    ready, pressed, released

} state;

 

state button_press = ready;

state button_press_sword = ready;

 

// struct for the information contained in each swing

typedef struct screen

{

    int valid_l; // left ball valid

    int x_l; // x coordinate for left ball

    int y_l; // y coordinate for left ball

    int radius_l; // radius for left ball

    int sec_until_advance_l;

 

    int valid_r; // right ball valid

    int x_r; // x coordinate for right ball

    int y_r; // y coordinate for right ball

    int radius_r; // radius for right ball

    int sec_until_advance_r;

} screen;

 

// array of screen struct variables

screen screens[NUM_SCREENS];




// string buffer

char buffer[60];

 

// DAC ISR

// A-channel, 1x, active

#define DAC_config_chan_A 0b0011000000000000

//== Timer 2 interrupt handler ===========================================

volatile unsigned int DAC_data ;// output value

volatile SpiChannel spiChn = SPI_CHANNEL2 ;       // the SPI channel to use

volatile int spiClkDiv = 2 ; // 20 MHz max speed for this DAC

 

// interrupt service routine which periodically reads analog input to check for

// sword motion

void __ISR(_TIMER_2_VECTOR, ipl2) Timer2Handler(void)

{

    mT2ClearIntFlag();

 

    // if we haven't read from sword too recently

    if (accept_sword_input)

    {

        // read from vertical and horizontal accelerometers

        raw_sword_input_1 = ReadADC10(0);

        raw_sword_input_2 = ReadADC10(1);

 

        // if vertical accelerometer indicates that sword is swinging downward

        if (raw_sword_input_1 < DOWN_THRESHOLD || raw_sword_input_1 > UP_THRESHOLD)

        {

            // if horizontal accelerometer indicates that sword is swinging left

            if (raw_sword_input_2 > LEFT_RIGHT_THRESHOLD)

            {

                // left swing

                swing = left;

            }

            else // if horizontal accelerometer indicates that sword is swinging right

            {

                // right swing

                swing = right;

            }

            accept_sword_input = 0; // do not accept further sword input until

                                    // current sword input has been processed

                                    // by sword thread

        }

    }

 

}



// Wait by executing nops

// to be used by i2c read and write functions

void i2c_wait(unsigned int cnt)

{

            while(--cnt)

            {

                        asm( "nop" );

                        asm( "nop" );

            }

}

 

// hardware defined address of the manetometer

unsigned char SlaveAddress = 0x0E;

 

// Read a char from the register specified by address

char i2c_read(char address)

{

    static char i2c_header[2];

    i2c_header[0] = ( (SlaveAddress << 1) | 0 );          //device address & WR

            i2c_header[1] = address;                //register address

 

    StartI2C1(); //Send the Start Bit

            IdleI2C1();                   //Wait to complete

 

            for(i = 0; i < 2; i++)

            {

        MasterWriteI2C1( i2c_header[i] );

                        IdleI2C1();                   //Wait to complete

 

        while(I2C1STATbits.ACKSTAT){};

            }

 

    //now send a start sequence again

            RestartI2C1(); //Send the Restart condition

            i2c_wait(10);

            //wait for this bit to go back to zero

            IdleI2C1();       //Wait to complete

 

            MasterWriteI2C1( (SlaveAddress << 1) | 1 ); //transmit read command

            IdleI2C1();                   //Wait to complete

    while(I2C1STATbits.ACKSTAT){};

 

            // read some bytes back

    char data = MasterReadI2C1();

            IdleI2C1();       //Wait to complete

 

    // need a NAK here

    NotAckI2C1();

 

    StopI2C1();  //Send the Stop condition

            IdleI2C1();       //Wait to complete

 

    return data;

}

 

void i2c_write(char address, char data)

{

    static char i2c_header[2];

    i2c_header[0] = ( (SlaveAddress << 1) | 0 );          //device address & WR

            i2c_header[1] = address;                //register address

 

    StartI2C1(); //Send the Start Bit

            IdleI2C1();                   //Wait to complete

 

            for(i = 0; i < 2; i++)

            {

        MasterWriteI2C1( i2c_header[i] );

                        IdleI2C1();                   //Wait to complete

 

        while(I2C1STATbits.ACKSTAT){};

            }

 

    // write actual data to device

    MasterWriteI2C1(data);

    IdleI2C1();

    while(I2C1STATbits.ACKSTAT){};

 

    StopI2C1();  //Send the Stop condition

            IdleI2C1();       //Wait to complete

 

    return;

}

 

// variables to store the data read from magnetometer

int magnetometer_data_x;

int magnetometer_data_y;

int magnetometer_data_z;

 

unsigned char device_id;

 

// === thread structures ============================================

// thread control structs

// note that UART input and output are threads

static struct pt pt_change_screen, pt_disp_screen, pt_update_screens, pt_sword, pt_sec;

 

// system 1 second interval tick

 

// === Timer Thread =================================================

// This thread reads from the magnetometer and updates the curr_screen variable

// which determines which screen the user is looking at. This new screen is not

// drawn onto the TFT in this protothread

static PT_THREAD (protothread_change_screen(struct pt *pt))

{

    PT_BEGIN(pt);

 

      while(1) {

        // yield time 100 msec

        PT_YIELD_TIME_msec(100) ;



        magnetometer_data_x = i2c_read(0x01); // read upper byte in X direction

 

        magnetometer_data_y = i2c_read(0x03); // read upper byte in Y direction

 

        device_id = i2c_read(0x07); // read device ID

                                    // (just to make sure we're communicating

                                    // with device correctly)

 

        // clear screen

        if (screens[curr_screen].valid_l)

        {

            tft_fillCircle(screens[curr_screen].x_l, screens[curr_screen].y_l, screens[curr_screen].radius_l, ILI9340_BLACK);

        }

        if (screens[curr_screen].valid_r)

        {

            tft_fillCircle(screens[curr_screen].x_r, screens[curr_screen].y_r, screens[curr_screen].radius_r, ILI9340_BLACK);

        }

 

        // update curr_screen based on reading from magnetometer

        if (magnetometer_data_x <= -4 && magnetometer_data_y <= 5)

        {

            curr_screen = 0;

        }

        else if (magnetometer_data_x <= -4 && magnetometer_data_y >= 6)

        {

            curr_screen = 1;

        }

        else if (magnetometer_data_x >= -3 && magnetometer_data_y >= 6)

        {

            curr_screen = 2;

        }

        else if (magnetometer_data_x >= -3 && magnetometer_data_y <= 5)

        {

            curr_screen = 3;

        }

 

        // display score and health onto the TFT screen

        tft_fillRoundRect(0,10, 320, 28, 0, ILI9340_BLACK);// x,y,w,h,radius,color

        tft_setCursor(0, 10);

        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);

//        sprintf(buffer,"X: %d, Y: %d\n ID: %uc", magnetometer_data_x, magnetometer_data_y, device_id);

        sprintf(buffer,"Score: %d, Health: %d, %d", game_time, health, curr_screen);

        tft_writeString(buffer);

        // NEVER exit while

      } // END WHILE(1)

  PT_END(pt);

} // timer thread



int radius = 4;

 

// This thread actually displays the relevant content onto the TFT screen. So,

// if the curr_screen variable was modified in the change screen protothread,

// then this protothraed displays the contents of the new screen

static PT_THREAD (protothread_disp_screen(struct pt *pt))

{

    PT_BEGIN(pt);

      while(1) {

        PT_YIELD_TIME_msec(80);

 

        if (screens[curr_screen].valid_l)

        {

            tft_fillCircle(screens[curr_screen].x_l, screens[curr_screen].y_l, screens[curr_screen].radius_l, ILI9340_GREEN);

        }

        if (screens[curr_screen].valid_r)

        {

            tft_fillCircle(screens[curr_screen].x_r, screens[curr_screen].y_r, screens[curr_screen].radius_r, ILI9340_GREEN);

        }

        // NEVER exit while

      } // END WHILE(1)

  PT_END(pt);

} // display screen thread



// This protothread updates the balls on the screens to move them closer.

// Additionally, this screen checks if the ball has "hit" the player, in which

// case the ball should be removed. If the player is hit, then this thread

// initiates the DMA for sound and activates the corresponding vibraiton motor.

static PT_THREAD (protothread_update_screens(struct pt *pt))

{

    PT_BEGIN(pt);

      while(1) {

        // yield time 1 second

        PT_YIELD_TIME_msec(750);

 

        // update all screens, not just the one the player is looking at

        for (i = 0; i < NUM_SCREENS; ++i)

        {

            if (screens[i].valid_l) // if the left ball is valid

            {

                screens[i].radius_l += 3; // increase the size of the ball

                if (screens[i].radius_l > 55)

                {

                    // if the ball too close, then the player has been "hit"

                    tft_fillCircle(screens[i].x_l, screens[i].y_l, screens[i].radius_l, ILI9340_BLACK); //x, y, radius, color

                    screens[i].valid_l = 0;

                    -- health;

                    // if ball hits you

                    // play death sound

                    DmaChnSetTxfer(dmaChn, death_table, (void*) &SPI2BUF, 2 * death_table_size, 2, 2);

                    DmaChnEnable(dmaChn);

 

                    // choose correct vibration motor

                    if (i == 0)

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else // curr_screen == 3

                        {

                            mPORTBSetBits(BIT_7);

                        }

                    }

                    else if (i == 1) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_3);

                        }

                    }

                    else if (i == 2) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_4);

                        }

                    }

                    else if (i == 3) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                    }

 

                }

            }

            else // randomly introduce a ball

            {

                if (rand() % 20 == 0) // 5% of the time

                {

                    screens[i].valid_l = 1;

                    screens[i].x_l = 80;

                    screens[i].y_l = 120;

                    screens[i].radius_l = 4;

                }

            }

 

            if (screens[i].valid_r) // if the right ball is valid

            {

                screens[i].radius_r += 3; // increase the size of the ball

                if (screens[i].radius_r > 55)

                {

                    // if the ball too close, then the player has been "hit"

                    tft_fillCircle(screens[i].x_r, screens[i].y_r, screens[i].radius_r, ILI9340_BLACK); //x, y, radius, color

                    screens[i].valid_r = 0;

                    -- health;

                    // if ball hits you

                    // play death sound

                    DmaChnSetTxfer(dmaChn, death_table, (void*) &SPI2BUF, 2 * death_table_size, 2, 2);

                    DmaChnEnable(dmaChn);

 

                    // choose correct vibration motor

                    if (i == 0)

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else // curr_screen == 3

                        {

                            mPORTBSetBits(BIT_7);

                        }

                    }

                    else if (i == 1) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_3);

                        }

                    }

                    else if (i == 2) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_4);

                        }

                    }

                    else if (i == 3) // choose correct vibration motor

                    {

                        if (curr_screen == 0)

                        {

                            mPORTASetBits(BIT_4);

                        }

                        else if (curr_screen == 1)

                        {

                            mPORTASetBits(BIT_3);

                        }

                        else if (curr_screen == 2)

                        {

                            mPORTBSetBits(BIT_7);

                        }

                        else // curr_screen == 3

                        {

                            mPORTASetBits(BIT_4);

                            mPORTASetBits(BIT_3);

                            mPORTBSetBits(BIT_7);

                        }

                    }

 

                }

            }

            else // randomly introduce a ball

            {

                if (rand() % 20 == 0) // 5% of the time

                {

                    screens[i].valid_r = 1;

                    screens[i].x_r = 240;

                    screens[i].y_r = 120;

                    screens[i].radius_r = 4;

                }

            }

        }

 

        // allow vibration motors to be on for a quarter of a second

        PT_YIELD_TIME_msec(250);

 

        // turn off vibration motors

        mPORTAClearBits(BIT_3 | BIT_4);

        mPORTBClearBits(BIT_7);

 

      } // END WHILE(1)

  PT_END(pt);

} // update screens thread

 

// This thread processes the sword data read in from the ISR

static PT_THREAD (protothread_sword(struct pt *pt))

{

    PT_BEGIN(pt);

      while(1) {

        // yield time 1 second

        PT_YIELD_TIME_msec(750);

 

        // if the player has swung the sword downward

        if (raw_sword_input_1 < DOWN_THRESHOLD)

        {

            tft_setCursor(0, 220);

            tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);

 

            // if ball is close enough, i.e. radius is big enough

            if (screens[curr_screen].valid_l && swing == left)

            {

                // remove ball, make left ball invalid

                tft_fillCircle(screens[curr_screen].x_l, screens[curr_screen].y_l, screens[curr_screen].radius_l, ILI9340_BLACK);

                screens[curr_screen].valid_l = 0;

 

                // if we hit the ball

                // play munch sound

                DmaChnSetTxfer(dmaChn, munch_table, (void*) &SPI2BUF, 2 * munch_table_size, 2, 2);

                DmaChnEnable(dmaChn);

            }

 

            if (screens[curr_screen].valid_r && swing == right)

            {

                // remove ball, make right ball invalid

                tft_fillCircle(screens[curr_screen].x_r, screens[curr_screen].y_r, screens[curr_screen].radius_r, ILI9340_BLACK);

                screens[curr_screen].valid_r = 0;

                // if we hit the ball

                // play munch sound

                DmaChnSetTxfer(dmaChn, munch_table, (void*) &SPI2BUF, 2 * munch_table_size, 2, 2);

                DmaChnEnable(dmaChn);

            }

        }

        accept_sword_input = 1; // now that the sword swing has been processed

                                // we can accept another sword swing from ISR

        swing = none;

 

      } // END WHILE(1)

  PT_END(pt);

} // update screens thread



// This thread keeps track fo the player's score and restarts the game when the

// player presses the restart button

static PT_THREAD(protothread_sec(struct pt *pt)){

    PT_BEGIN(pt);

 

    while(1){

        PT_YIELD_TIME_msec(1000);

 

        ++ game_time; // player's score

 

        // game is over

        if (health <= 0)

        {

            // clear screen

            tft_fillRoundRect(0,0, 320, 240, 0, ILI9340_BLACK);// x,y,w,h,radius,color

            tft_setCursor(20, 120);

            tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(3);

 

            // play ending sound

            DmaChnSetTxfer(dmaChn, intro_table, (void*) &SPI2BUF, 2 * intro_table_size, 2, 2);

            DmaChnEnable(dmaChn);

 

            // print score

            sprintf(buffer, "GAME OVER!\n Score: %d", game_time);

            tft_writeString(buffer);



            // wait for restart button to be pressed

            while (!restart)

            {

                restart = mPORTAReadBits(BIT_2);

                mPORTAClearBits(BIT_3 | BIT_4);

                mPORTBClearBits(BIT_7);

            }

 

            // if we are here, then the game is restarting.

            // so, need to reinitialize all game parameters

 

            // screens initialization

            for (i = 0; i < NUM_SCREENS; ++i)

            {

                screens[i].valid_l = 0;

                screens[i].valid_r = 0;

            }

 

            health = 5; // reset health

            restart = 0; // reset restart variable

            game_time = 0; // reset score

 

            tft_fillRoundRect(0,0, 320, 240, 0, ILI9340_BLACK);// x,y,w,h,radius,color

 

        }

 

    }

    PT_END(pt);

}

 

// === Main  ======================================================

void main(void) {

 //SYSTEMConfigPerformance(PBCLK);

 

  ANSELA = 0; ANSELB = 0;

 

  // set up DAC on big board

  // timer interrupt //////////////////////////

    // Set up timer2 on,  interrupts, internal clock, prescalar 1, toggle rate

    // at 30 MHz PB clock 60 counts is two microsec

    // 400 is 100 ksamples/sec

    // 2000 is 20 ksamp/sec

    OpenTimer2(T2_ON | T2_SOURCE_INT | T2_PS_1_1, 400000);

 

    OpenTimer3(T3_ON | T3_SOURCE_INT | T3_PS_1_1, 909);

 

    // set up the timer interrupt with a priority of 2

    ConfigIntTimer2(T2_INT_ON | T2_INT_PRIOR_2);

    mT2ClearIntFlag(); // and clear the interrupt flag

 

    // SCK2 is pin 26

    // SDO2 (MOSI) is in PPS output group 2, could be connected to RB5 which is pin 14

    PPSOutput(2, RPB5, SDO2);

 

    // control CS for DAC

    mPORTBSetPinsDigitalOut(BIT_4);

    mPORTBSetBits(BIT_4);

 

    // divide Fpb by 2, configure the I/O ports. Not using SS in this example

    // 16 bit transfer CKP=1 CKE=1

    // possibles SPI_OPEN_CKP_HIGH;   SPI_OPEN_SMP_END;  SPI_OPEN_CKE_REV

    // For any given peripherial, you will need to match these

    // clk divider set to 2 for 20 MHz

    SpiChnOpen(SPI_CHANNEL2,

            SPI_OPEN_ON | SPI_OPEN_MODE16 | SPI_OPEN_MSTEN | SPI_OPEN_CKE_REV | SPICON_FRMEN | SPICON_FRMPOL,

            2);

    // SS1 to RPB0 for FRAMED SPI

    PPSOutput(4, RPB10, SS2);

    // end DAC setup

 

    ///////////////////////////////////////////////////////////

    // set up I2C

 

    OpenI2C1(I2C_ON, 0x0C2); // enable I2C1

 

    i2c_write(0x11, 0x80); // write to control bits to put magnetometer in active mode

 

    i2c_write(0x10, 0x01); // other magnetometer configuration



    // the ADC ///////////////////////////////////////

    // configure and enable the ADC

    CloseADC10(); // ensure the ADC is off before setting the configuration

 

    // define setup parameters for OpenADC10

    // Turn module on | ouput in integer | trigger mode auto | enable autosample

    // ADC_CLK_AUTO -- Internal counter ends sampling and starts conversion (Auto convert)

    // ADC_AUTO_SAMPLING_ON -- Sampling begins immediately after last conversion completes; SAMP bit is automatically set

    // ADC_AUTO_SAMPLING_OFF -- Sampling begins with AcquireADC10();

    #define PARAM1  ADC_FORMAT_INTG16 | ADC_CLK_AUTO | ADC_AUTO_SAMPLING_ON //

 

    // define setup parameters for OpenADC10

    // ADC ref external  | disable offset test | disable scan mode | do 2 sample | use single buf | alternate mode on

    #define PARAM2  ADC_VREF_AVDD_AVSS | ADC_OFFSET_CAL_DISABLE | ADC_SCAN_OFF | ADC_SAMPLES_PER_INT_2 | ADC_ALT_BUF_OFF | ADC_ALT_INPUT_ON

            //

    // Define setup parameters for OpenADC10

    // use peripherial bus clock | set sample time | set ADC clock divider

    // ADC_CONV_CLK_Tcy2 means divide CLK_PB by 2 (max speed)

    // ADC_SAMPLE_TIME_5 seems to work with a source resistance < 1kohm

    // SLOW it down a little

    #define PARAM3 ADC_CONV_CLK_PB | ADC_SAMPLE_TIME_15 | ADC_CONV_CLK_Tcy //ADC_SAMPLE_TIME_15| ADC_CONV_CLK_Tcy2

 

    // define setup parameters for OpenADC10

    // set AN11 and  as analog inputs

    #define PARAM4 ENABLE_AN11_ANA | ENABLE_AN5_ANA //

 

    // define setup parameters for OpenADC10

    // do not assign channels to scan

    #define PARAM5 SKIP_SCAN_ALL //|SKIP_SCAN_AN5 //SKIP_SCAN_AN1 |SKIP_SCAN_AN5  //SKIP_SCAN_ALL

 

    // // configure to sample AN5 and AN1 on MUX A and B

    SetChanADC10( ADC_CH0_NEG_SAMPLEA_NVREF | ADC_CH0_POS_SAMPLEA_AN11 | ADC_CH0_NEG_SAMPLEB_NVREF | ADC_CH0_POS_SAMPLEB_AN5 );

 

    OpenADC10( PARAM1, PARAM2, PARAM3, PARAM4, PARAM5 ); // configure ADC using the parameters defined above

 

    EnableADC10(); // Enable the ADC

 

    ////////////////////////////////////////////////////////////

 

    // set up digital outputs for vibration motors

    mPORTBSetPinsDigitalOut(BIT_7);

    mPORTBClearBits(BIT_7);

 

    mPORTASetPinsDigitalOut(BIT_3 | BIT_4);

    mPORTAClearBits(BIT_3 | BIT_4);

 

    //////////////////// DMA setup ///////////////////////

    // Open the desired DMA channel.

    // We enable the AUTO option, we'll keep repeating the sam transfer over and over.

    DmaChnOpen(dmaChn, 0, DMA_OPEN_DEFAULT);

 

    // set the transfer event control: what event is to start the DMA transfer

    // In this case, timer2

    DmaChnSetEventControl(dmaChn, DMA_EV_START_IRQ(_TIMER_3_IRQ));

 

  // === config threads ==========

  // turns OFF UART support and debugger pin, unless defines are set

  PT_setup();

 

  mPORTASetPinsDigitalIn(BIT_2);

  EnablePullDownA(BIT_2);

 

  // === setup system wide interrupts  ========

  INTEnableSystemMultiVectoredInt();

 

  // init the threads

  PT_INIT(&pt_change_screen);

  PT_INIT(&pt_disp_screen);

  PT_INIT(&pt_update_screens);

  PT_INIT(&pt_sword);

  PT_INIT(&pt_sec);

 

  // init the display

  tft_init_hw();

  tft_begin();

  tft_fillScreen(ILI9340_BLACK);

  //240x320 vertical display

  tft_setRotation(3); // Use tft_setRotation(1) for 320x240

 

  // screens initialization

  for (i = 0; i < NUM_SCREENS; ++i)

  {

      screens[i].valid_l = 0;

      screens[i].valid_r = 0;

  }

 

  // initialize other game variables

  health = 5;

  restart = 0;

  game_time = 0;

 

  // seed random color

  srand(1);

 

  // round-robin scheduler for threads

  while (1){

      PT_SCHEDULE(protothread_change_screen(&pt_change_screen));

      PT_SCHEDULE(protothread_disp_screen(&pt_disp_screen));

      PT_SCHEDULE(protothread_update_screens(&pt_update_screens));

      PT_SCHEDULE(protothread_sword(&pt_sword));

      PT_SCHEDULE(protothread_sec(&pt_sec));

      }

  } // main

 

// === end  ======================================================