Our project is a tilt maze game controlled by a glove worn by the user, which contains an accelerometer as well as sensors for each finger. The tilt of the maze is continuously updated in accordance with the position of the user’s hand. The maze itself contains several holes with servo-controlled trapdoors obstructing the path, which can be opened and closed based on the input from the finger sensors. At the start of each round, a display on the TFT screen will illustrate the destination hole and indicate which trap door each finger can control. The user will be given time to practice controlling the doors before starting the game and gaining control of the maze tilting. If the ball falls through one of the other holes, this results in a failed round. Otherwise, once the ball is navigated to the correct location, the screen will indicate a success. At the end of the round, the same button can be pressed again to renavigate back to the start screen, where a new destination will be indicated on the screen.
We wanted to create a game that was fun to play and that incorporated various different features, such as the IR sensors, servos and finger sensors. We were also excited to build a physical product that could be interacted with.
The Entire Project
- TFT displays a start screen illustrating the destination hole as well as the mapping between the finger sensors and remaining trap doors
- Practice controlling the trap doors, then press a button to start the game
- Move the ball through the maze by tilting the glove; the ball falling through the wrong hole results in an immediate fail
- Once the ball reaches its destination, the TFT screen displays either success or failure
- Press the button to go to the start screen and begin a new game
The basic idea of this game has already been used several times, so we wanted to build on it to increase the complexity and make it our own design. This is why we added the trap door aspect to the game; in doing so we also needed to include detection for which hole the ball fell through. One challenge we faced with this project was the shortage of pins since we wanted to control seven servos independently from one another, as well as have five IR detection circuits and four finger sensor inputs. As a result, we had to make use of the port expander in order to include all of our desired sensors, as well as a demux to be able to control all the servos. The port expander communicates with the PIC over SPI, which is rather slow, so setting and clearing bits of the port expander introduced more overhead. However, this tradeoff enabled us to include these various hardware components, and since we did not have strict timing deadlines in our code, this was a worthwhile adjustment.
Regarding copyrights, since the main concept for this game has already been reused by several different companies and apps, we believe that there are no issues regarding copyright. Additionally, this article specifically states that a similar game (Labyrinth) does not have a copyright in the United States. Our gameplay is also significantly different than that of the designs already created.
The Control Glove
The MPU-6050 accelerometer we used communicates with the PIC over i2c. We fortunately discovered a previous ECE 4760 project (3DOF Stewart Platform) that used the same accelerometer we had, so we were able to adopt part of their code to read the accelerometer data. Their code contained several helper functions such as i2c_read, which enabled us to read various registers containing the data we wanted. The accelerometer values were updated in a thread that ran every 1ms. This was also where we calculated the values being sent to the motors. Initially, we were having problems with jitter, since the accelerometer readings were sensitive and fluctuated quite significantly. To address this, we created filter_x and filter_y variables, which low-passed the readings. We then used the filtered readings to create a direct mapping between the accelerometer values and pwm signal.
For the finger sensors, we cut up a perfboard into 5 pieces (one for each finger) to be used as contact plates. The plate on the thumb is connected to 3.3V and the plates on the other fingers are conencted to an input pin. These sensors essentially behaved as buttons, where connecting the plate on a finger to the plate on the thumb closed a switch and pulled the voltage on the pin high. The four inputs were connected to pins 0-3 on Port Y. In order to address problems regarding debouncing, we implemented a thread to poll the state of each pin every 30ms (determined by human reaction speed) and utilized a button FSM for each input. bit_val is the value of one of four pins on the port expander corresponding to the specific finger we want to check.
The physical structure of our maze was made out of both 3D printed (from PLA plastic) and laser cut (from 1/8 inch birch wood) pieces, which we designed ourselves in Autodesk Fusion 360. We 3D printed the intricate parts that would be difficult to laser cut or assemble from laser cut pieces, such as the cones, servo doors, and maze itself. We laser cut the large and more basic parts that would take a 3D printer too long to print or would exceed the bed size of the 3D printer, such as the base for our maze. The CAD files (in STL and DXF formats) can be found in the Appendix page of this website.
The servos are controlled using PWM, which we implemented using output-compare peripherals on the PIC. We configured the output-compares to PWM mode, then connected them to timer2 and mapped them to different pins on the PIC. Then, in the ISR for timer2, which generated an interrupt every 1 millisecond (44,000 clock cycles), we wrote the updated pwm values, which were calculated in the accelerometer thread. We used a timer interrupt instead of a regular thread because we wanted to update the PWM duty cycles at precise intervals.
For our game, we needed to control a total of seven servos; two for the tilt of the maze and five for the trap doors. However, the PIC only has five output-compare peripherals and each unique pwm signal requires its own. To get around this issue, we noted that the five servos for the trap doors will never need to move at the same time (due to the way in which we designed the game). This meant that we could output one pwm signal from the PIC, connect it to the COM OUT/IN pin of a demultiplexer, and use three additional pins for the demux select signal (labeled A, B and C in the diagram). Based on the select signal, the demux would send this pwm signal to one of the five servos.
To implement ball detection, we placed an IR LED on one side of each hole, and an IR receiver diode in series with a resistor that goes to ground. A signal pin connected to the PIC is placed between the diode and resistor. The resistance of the IR receiver diode changes depending on whether it detects infrared light (high when no IR is detected and low when it is detected). When the ball falls through it momentarily prevents the light from reaching the sensor, causing the diode resistance to greatly increase and making voltage drop across the resistor change from 3.08V to about 0.2V. We found that a higher resistance value for the resistor in series with the IR receiver allowed for a higher change in voltage across it, which is why we replaced our original design using 330 Ohms with 1 kOhms.
The IR signal inputs are connected to pins 0-4 on Port Z of the port expander, where we configured interrupts for each of these five pins. In order to trigger an interrupt on the PIC, we attached an external interrupt to RPA2, the PIC pin that communicates with Port Z. However, this means that the ISR will be entered upon a change in value on any of the port expander pins with interrupts enabled, without a direct way of determining which specific pin caused the interrupt. To solve this issue, we added a function that reads the Port Z interrupt capture register (which stores the state of the port when the interrupt was triggered) so that we could compare the new port values to the previous ones and determine which one changed. Then, based on the value of the destination hole, we set a win_round flag which is used in the after_round game state to display the appropriate message.
To display the images on the TFT, we first converted our images to bitmaps using this website, then rewrote the tft_drawBitmap function in the tft graphics source file to draw rectangles for each pixel in the image. The size of this rectangle was passed in as a parameter so we could decide how much we wanted to scale the image by. Though it is rather slow to loop through each element in a bitmap and draw a rectangle for each pixel, we decided that this was an acceptable sacrifice due to the fact that we only need to print everything once. This means that it will not interfere with the timing of the game, since after the thread runs at the beginning, it is not scheduled again until the start of the next round.
To control the game flow, we created a game state enum consisting of before_round, playing_round and after_round. We included a button in our circuit that, when pressed, would change the game state depending on the current game state, as illustrated in the figure below. To generate a random number for the destination, we utilized the ADC to generate a random seed. This involved taking two readings from a floating pin and summing them together, with the first reading left shifted by one and the second left shifted by 5. We then were able to call the rand() function and receive different random numbers upon resetting the PIC. To get a number between 0 and 4, we computed the randomized number mod 5. Using this number, we were able to create a direct mapping between each finger and the remaining doors.
Servo Calibration and Timing Considerations
To calibrate the accelerometer, we measured the reading when the board was positioned vertically for all four positions (forward, backward, left, right); see images below. We then used these values to map the instantaneous accelerometer readings to a pwm value. The servos we used have a period of 20ms, and according to the data sheet, a 1ms on time corresponds to 0 degrees and a 2ms on time to 180 degrees. However, when working with the servos, we realized that the servo range was actually from about .8ms to 2.3ms.