Eye Snake - Low-Level Design

Hardware Design

Our EOG circuit can be successfully run on two 9V batteries for each of the horizontal sensing and vertical sensing circuit boards. The power circuit, taken from a previous BioNB440 RF-linked EMG final project, is designed to split the rails in +4.5 and –4.5 rails. Note: The reference electrode placed behind your ear or forehead is hooked to ground.

To amplify the signal, we use an INA121 instrumentation amplifier with a gain of 150-220 and an inverting amplifier with a gain of 10 to amplify the gain by 1500-2200 to achieve a 30-50 mV signal per degree of rotation from the eyes. Refer to Background Math for more calculations. The following circuit is shown below. The potentiometer at the pin 5 of the INA121 should be connected to a 10 kΩ potentiometer between Vcc and –Vcc to generate an offset. This offset and potentiometer is the key to calibrating the electoro-oculogram successfully.
We also use a LM358 op-amp wired as an active low pass filter so that the 3dB cutoff frequency occurs at 31.2 Hz. Refer to Background Math for additional calculations. The low pass filter circuitry with pinouts is shown below.
Then we need to isolate the above circuits from the MCU with an optoisolator. The optoisolator 4N35 is very sensitive to loads, especially when it was tested with sine wave signals of a few mV. Therefore, a voltage follower was added prior to the optoisolator to serve as a buffer between the amplifying and filtering stages and the isolation stage. Without the offset in the instrumentation amplifier and voltage follower, it did not seem likely that an electrode signal out of the optoisolator would show up on the oscilloscope. Oftentimes, the optoisolator would not pass a sine or square wave through to the collector with correctly biasing the circuit. Originally, we thought it was due to the instrumentation amplifier's inability to drive the optoisolator. So we tried hooking a LMC7111 opamp with its feedback loop passing through pins 1 and 2 of 4N35 optoisolator. Eventually it was the potentiometer that sets the offset of the instrumentation amplifier that solved the problem. The optoisolator circuit was tweaked optimally for our electrode signals. The circuit is shown below.
The optoisolator circuit served as the bottleneck to our progress on getting the electrode signals into an ADC. Because we are technically not allowed to hook test subjects up to the scope at the output of the instrumentation amplifier, it was indefinitely necessary to get the optoisolator circuitry working optimally. However, even now, the optoisolator circuit still requires frequent calibration and recalibration so that the horizontal and vertical electrode signals come out of the collector of BJT and into the ADC of the MCU. In fact, calibration requires a great deal of patience. The electrodes work better when stuck closer to the eye. However, for the test subject, this can be deemed uncomfortable. Unfortunately, a recalibration is nearly needed each time the test subject moves his/her head or pushes on the wires or if the electrodes lose their adhesive nature. Lastly, to wire the TV up to the Mega32, we used the same setup from our video game lab as shown below. RCA connectors are stripped such that alligator clips can directly connect to the lines.

Software Design

(view the source code here)

The program started off with the video framework from the course, except with no sound.

The first thing added was a basic one-pixel-per-length snake to move around the screen. This part was fairly simple and had no complications. After that, screen boundary conditions and collision detection with a randomly generated fruit were added, both of which again were fairly simple as the random generation is simply done by two calls to the Math.rand() function and the collision detection consists of just checking the position of the snake's head with the other positions (fruit, rest of snake).

The next step consisted of updating the graphics. Now, indead of single-pixel snake and "fruits", we created 6x6 bitmap images of five different types of fruits and the snake's head and tail.
Initially, they were 8x8 bitmaps with the snake's width being 6 pixels, but it seemed that the logic to determine the snake's "cornering" (image transition from horizontal to vertical or vice versa, since snake does not fill entire bitmap) seemed to be taking too long and causing sync issues, so they were reduced to 6x6. Additional logic had to be added to draw the snake's head and tail pointing in the correct direction, as well as constraining the randomly generated fruit locations to align to the screen grid of 6x6 cells.

After that, we added up/down buttons to the initial left/right to control the snake. Now, instead of left/right being 90deg turns, "up" corresponds to screen up, "down" to screen down, "left" to screen left, and "right" to screen right. Thus, if the snake is traveling horizontally, you can only turn up or down, and if it's traveling vertically on the screen, you can only turn left or right.

Finally, we started adding external input from the electrodes to control the screen. Initially, we started with the left/right only control scheme, since the left/right signal was much cleaner and easier to calibrate. Even then, however, the signal had a tendency to go flat and drop below threshholds for left/right, causing the "center" signal to evaluate to left and the "right" signal to evaluate to an indeterminate value. However, if the signal levels were distinct enough and the threshhold values in the code were properly set, reading and decoding the signal from the ADC was straight-forward.

Calibrating the up/down signal was even more troublesome, but when it was set up correctly, there were still initially problems with the directions being decoded. As it seemed that the values were being read in correctly, we thought that it might be a problem with debouncing. Since reading up/down and left/right requires two A/D conversions, we split it up between frames - one frame left/right is read, then mux is set to the other pin and the next frame up/down is read.
However, this causes a debounce for up/down or left/right to take 8 frames to go from not-pushed to pushed back to not-pushed. So since left/right and up/down commands need to be alternated for there to be any effect (eg. turning "up" while already going up or down has no effect), we decided that removing debouncing would have no effect and was worth a try; and indeed it did work.

After getting those working (mostly), we moved on to adding multiple levels and lives. For each new level, the starting speed increases, the starting length of the snake is incremented, and a new obstacle is added. The obstacles are randomly generated in the same way as the fruit, and when you die, your lives are decremented, and the speed and length are reset to the level's starting values (length and speed are incremented each time you eat a fruit).

After that, we added the starting menu. This consisted of the starting level, number of extra lives, play mode, and sound toggle options. For this, we used left/right (taken from either the eyes or the buttons based on the play mode) to scroll through the menu and a separate push-button to cycle through each selections options. We considered using up/down to scroll and left/right to cycle through options, but opted not to use that because of the relative touchiness of the up/down eye signal as well as debouncing issues. And even though we had the sound option in the menu, sound was the last thing we added.

All in all, the biggest software problem we encountered was trying to draw/erase too many pixels at a time to the screen in one frame. To resolve the issue, we added several state machines to divide the drawing/erasing over several frames. And anything that had to be erased and redrawn in the same frame was always put together to occur in the same frame so no flickering occured.

Theo Chao (tc99), Diane Chang (dcc34) © 2005