For our final project, we created a 3D marble maze whose tilt and movement would be controlled by the FRDM-KL25Z board. Players can try moving the marble throughout different types of mazes. As avid gamers ourselves, we decided creating a marble maze game would be a task that would not only utilize the board and content from this class but also align with our interests. We continuously poll from data collected from our board's accelerometer and smooth the data before writing it to serial. The game is implemented using Godot Engine, a game engine, and our C# code reads the accelerometer data and utilize it to move the marble maze based on the movements performed with the board. Throughout this project we learned how to read accelerometer data, perform some type of signal processing, and create our game with Godot Engine. In previous assignments in this course we were only exposed to using the LED from the board for our labs, but our final project gave us a great opportunity to learn other capabilities and features of the board.
We first initialized the relevant accelerometer code provided from the issdk examples and set up the UART code provided by Professor Napp’s
serial_example.c in our file
aMaze.c. We had two functions that would deal with updating and sending the accelerometer data. The function
updateData would read and convert the raw sensor data, which includes the x, y, and z positions of the FRDM-KL25Z board. Then, we would add the collected data and store them into a large 2D array for smoothing. The function
sendCurrentData would calculate the moving average of the raw sensor data stored in our 2D array to help smooth out our data and eventually write to serial.
The frequency in which we would write to serial was every 1/60th of a second (60 Hz). This logic was performed by our
PIT_IRQHandler, which is the associated interrupt handler for the PIT timer. In this function, we would disable the timer, call
sendCurrentData, and then enable the timer as well as clear the flag. The primary motivation behind using the PIT Timer, rather than using periodic real-time scheduling, was due to its simplistic and effective nature.
We used Godot, a free and open-source game engine, to create the GUI for our 3D marble maze. We used C# to implement the code that reads the data sent from the serial port and converts it to an integer array representing the x, y, and z orientations of the board. Additionally, in the function
_PhysicsProcess we set the point of origin and orientation of the marble maze with respect to the accelerometer data from the board.
There were two main components of the testing process for our project. The first component consisted of testing how the marble maze would respond when we tilted the board in various directions and speeds. Even before we connected the board to the game engine, we simulated it by using mouse inputs to tilt the board to make sure board angling worked. With the board, we made sure to test for all possible cases by tilting the board in all cardinal directions at different speeds (slow, moderate, fast). We adjusted and changed the default orientation of the marble maze to align with how the board would be simply facing upwards. Our testing for the timing values was extremely rigorous and methodical; we made sure that our timing values were correct such that the accelerometer values were being sent in exactly 1 second so when we divided it by 60 the rate would be accurate. Additionally, we developed a fail-safe mechanism on the front end that would prevent the balls in our maze from phasing out of our maze indefinitely in a particular direction when we tilted the board too quickly.
The second component of the testing process was smoothing (noise reduction) the raw accelerometer data being written to serial. Initially, the raw accelerometer data that was being read made the maze move in an extremely rugged manner. Thus, we decided to use a moving average filter to reduce the noise of our raw data. We tested various sample sizes for our moving average (starting from a small number and progressively increasing it) until we were satisfied with the degree to which our accelerometer data stabilized. One interesting bug we encountered was integer overflow with smoothing, so we had to change the field width of our integer types. While adjusting the sample size for our moving average we also tested our GUI to see how our marble maze would respond to these changes in our accelerometer data.
Project Video Demo
Detailed Technical Description
Technical Description of our C code (collecting and writing accelerometer data):
Our entire implementation for collecting and writing accelerometer data was in the file
aMaze.c. Firstly, we incorporated the code related to collecting and writing accelerometer to UART from the file
mma845x_fifo.c in MCUXpresso IDE's issdk examples. The function
init_accelerometer effectively initializes the accelerometer and returns 0 if it does so successfully and -1 otherwise. Additionally, we used the code the serial example
serial_example.c posted on Canvas to help initialize UART. The function
init_uart sets up UART by initializing the PLL clock source for UART0 and enabling both the UART and PORTA clock. Additionally, we modify the BDH and BDL registers to waits until space is available in the FIFO and then set an 8 bit char to the D register from UART0 that will be sent. The last function used to configure UART is
uart_puts which sends each character of a string to UART.
We defined a C macro called
SAMPLE_SIZE that holds the value representing the sample size we use for the moving average filter. Also, we initialized a 2D array called
samples with dimensions of
3 (for each x, y, and z position) to store accelerometer data. In our function
updateData, we first wait for data ready from the MMA845x, then read the raw sensor data from the MMA865x, and lastly store the raw accelerometer data in the array
samples. In our function
sendCurrentData we calculate the moving average of the raw sensor data by taking the sum of each of the x, y, and z positions in within each index of
samples and then dividing each sum by
SAMPLE_SIZE. Additionally, we call
uart_puts which sends our string of smoothed data to UART.
Our interrupt handler for the PIT timer
PIT_IRQHandler, was called every 1/60th of a second (60 Hz) so we could write to serial under that frequency. In this function, we would disable the timer, call
sendCurrentData, and then enable the timer as well as clear the flag.
main function we first set up UART by calling
init_uart and then set up the accelerometer by calling the following functions:
BOARD_InitDebugConsole. After setting up UART and accelerometer, we set up PIT interrupts, enabled the clock, set the load value of the 0th PIT to be 1/60th of a second, and enabled interrupts. Lastly, we would call the function
updateData within an infinite loop to continuously poll data and send it when necessary.
Technical Description of our C# code (GUI and reading accelerometer data):
Our implementation for the frontend of our game and reading of accelerometer data sent through serial uses Godot and C# System.IO.Ports. The motion of the marble maze is controlled through
Level.cs, which is attached to every level in the game. The function
_Ready initializes the port by setting the port name, the baud rate to 115200, the read timeout to be 1000 ms, and opens a new serial port connection by calling
_port.Open(). The function
_Process, which runs 60 times per second, reads up to the newline value in the input buffer which contains the smoothed accelerometer data and forms an array consisting of the x, y, and z positions. The function
_PhysicsProcess sets the orientation of the marble maze with respect to the accelerometer data from the board by using the function
When the game is initialized,
Game.cs loads the game levels and instantiates the first one. We made heavy use of callback functions and Godot signals to do things like trigger ball spawning and level changing. The first level sets up the port as described above, and also requests for the game to spawn the correct number of balls for how many goals the level has. Each
Goal.cs goal contains an Area that detects when it collides with a physics body. When this happens, the ball and the goal are removed from the scene and queued for freeing, and the goal emits a signal
GoalHit that the goal has been hit. In the level, the signal handler counts down how many goals remain. When all goals have been reached (0 goals left), the level emits
LevelComplete to trigger the switch to the next level. In
Game.cs the current level is queued to be freed and the next one is instantiated and added to the scene. Finally,
Ball.cs handles when a ball goes out-of-bounds by setting its position to the origin and its velocity to 0.
Additional Resources Used
We used the relevant accelerometer code that was from the issdk examples in MCUXpresso IDE as the starting foundation for our accelerometer code. We used the serial example
serial_example.c posted on Canvas to help initialize UART. Additionally, we used Godot Engine and it's documentation Godot Docs to create the GUI and read the data sent from the serial port.
Throughout this project, we worked together by calling each other on Discord and pair-programmed using VSCode Live Share. The majority of the project was done together during call, with exception to Changyuan figuring out how to create the maze using Godot and setting up the initial codebase with the accelerometer code and serial example by himself. We tested our system together by screen sharing the GUI while we moved the maze with the board and by seeing how our accelerometer data changed during the smoothing process. Additionally, we created the project video and the webpage together.