Software Description
The control software for the LED cube has a very simple structure.
First we initialized two flash arrays with pre-rendered columns for all possible digits and letters so that we could look these up easily for string display on the cube. In our case we could have just hard coded the string since we have only one intro string, but we decided to do this and use an initialization routine so that this could perhaps be utilized in future projects that attempt an LED cube and want to display text. It also saved some memory space.
We made an initialization routine that we call at the very beginning of the main function to set up the directionality of the ports that we used to control the cube. We basically just needed the bottom two bits of PORTA for our analog to digital conversion to be inputs for the two accelerometer lines, all of the port pins of PORTC and the bottom 3 port pins of PORTB to be output to control the lighting of the cube. The actual layout of pins are described in detail in the comments of the code in the initialization routine. We set up timer 0 to reset on compare-match with a prescaler of 64, set the OCR register to 249, and enabled the compare-match interrupt. This gave us an interrupt that triggered once per millisecond. With this millisecond time base, we could schedule a task precisely. We also initialize a counter for scheduling our task. We initialize the analog to digital converter to run at 125KHz, to first convert the 0 channel when a conversion is started, and to interrupt on conversion done so that we can do two conversions in a row easily. We then call our initialize sensors routine to initialize the bias values of our accelerometers and then our initialize intro routine to initialize an array of column values for use in displaying our introduction string. Finally, we initialize our state variables and start a conversion of the accelerometer values.
The calibrate sensors routine does just what its name suggests. Assuming the cube starts standing on its base (up and down orientation), we figure out the zero bias of both of the sensors. We simply do three conversions of each of the two accelerometer values and divide by three to get the average value. We save these values as our x and y accelerometer conversion biases (to be subtracted from the reading when we do future conversions).
The analog to digital conversion done interrupt service routine is used to do two consecutive conversions (of the x and y accelerometer values) quickly without slowing down the rest of the program or doing some other more complicated coding trick. We wanted the two conversions values to be done at very similar times and available together. We used a state variable to keep track of which channel we were converting, and checked it in this routine. If it is the first of the two, we grab the conversion value, change the analog to digital multiplexer to multiplex the next channel, change the state variable to reflect that we are converting the next value, and initialize the next conversion. When this next conversion gets back to the ISR, we save the conversion value and set a global flag which signals that the conversion data is ready so that the rest of the program can use the conversion values.
We have only one task in the software which is our display task. It is scheduled once every 16 mS using a counter that counts down from 16 in our millisecond interrupt tick (and a simple if statement in the main loop which checks for when the counter becomes zero). This delay gives rise to a calling rate of 62.5Hz which is well above the refresh rate of a display that a human eye can perceive. Thus, we were not able to detect any flickering in the LED's. Despite its name, it actually does a couple of other things as well. At the beginning of the display task, we check our animation/demo state and update a frame buffer with the current frame of the animation. The frame buffer consists of an array of 25 bytes. Each byte in the frame buffer corresponds to a column in the LED display. The index number in the array corresponds to the number of the column. The columns of the cube are laid out as follows assuming we are looking at the cube from above with the front of the cube at the bottom of the screen:
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
The bottom five bits of each byte in the frame buffer represent which of the five LED's in that column are currently lit. The least significant bit of the byte is the bottom LED in the column and the 5th least significant bit is the top LED. A 1 represents on whereas a 0 represents off.
Next, the display task checks whether there is an acceleration conversion ready, converts it to a floating point value and subtracts the bias that we calculate when we initialize the system, and finally starts a new conversion (putting the conversion in the first state of converting y acceleration). This gives us a scaled value of the accelerations in the x and y axes. We actually ended up using only the x acceleration, but left the code for how to do two conversions in a row as an example for how future students could perhaps accomplish this. The additional data could even be used in an extension of this project along with a state machine to give more complex state information for how the cube is oriented. With a little bit of debugging using the USART of the Mega32, we found that 1g of gravity was about +-74 ADC units of the conversion of the x axis accelerometer (after bias). We use this to detect whether the cube is tipped on its side. We basically say it is tipped on the positive x side if the value of the acceleration in the x axis is greater than 65 and it is tipped on the negative x side if the acceleration in the x axis is less than -65. This gave us pretty good performance when we tested it. In the case that the board is tipped, we had to copy the current frame buffer to a temporary buffer and then "flip the buffer" 90 degrees to the correct side. This turned out to be a non-trivial calculation and can be seen in this section of the display task code in the appendix. The basic idea is as follows:
Looking at the front of the cube, we can think of the problem in the context of a 2D display since all of the slices of the cube had to be rotated the same way and we could just construct a loop that iterated through the 5 slices back to front doing the same thing with just a simple offset. This is illustrated in Figure 5.
With a little bit fiddling, it is trivial (although slightly tedious) to flip the frame 90 degrees in the appropriate direction.
After all of this, the only thing left to do in the display function, is actually turn on the LED's which are indicated as on in the frame buffer. Basically, we use a for loop to walk through the columns one at a time, turning on the appropriate LED's for 200uS and then off again. This strobing of LED's is so fast that it is not perceptible to the human eye (one sequence of strobing takes about 10mS = 25*2*200uS). For each column, we actually turn on the bottom 3 LED's that are indicated as lit and then the top 2. This limits the current draw from our LED power gating transistors (which aren't rated for a current high enough to turn on 5 LED's in series at the current we were putting through each) and also extended our battery life without causing any negative consequence besides perhaps a small loss in brightness (if any). So, given the column that we want to turn on, we first initialize a temporary character with the bottom three bits equal to the current column's bottom three bits. We then set PORTB.0 to the topmost of the three bits (which grounds layer 3 if high) and set the top 2 most significant bits of PORTC with the next two bits. These ports ground layer 2 and 1 respectively. We then set the lower 6 bits of PORTC with the appropriate select value for which column we are currently lighting. If the column is 0 to 23, we can just set the 6th bit of PORTC to 0 and the lower five bits of PORTC to the number of the column since we set up our encoder select hardware and microcontroller lines to address the columns correctly given the number of the column. If, however, the column is the very last one (number 24 or the 25th column) we need to disable the decoders and enable our sixth select line which simply enables the 25th column. We disable the decoders by setting the bottom five bits of PORTC to 11000. This sends a 11 to the master encoder which selects a non-existent decoder, making all the decoders that gate power to the other columns disabled. Finally, we now have the appropriate bottom 3 LED's lit and we delay 200uS to allow the LED to light up and light to be perceived by viewers eyes, and then we disable the layers and columns by setting all of the grounding lines low, disabling the power line for the 25th and setting the decoder lines to 11000 which again disables all of the lines enabled by the decoders. We then do the top 2 LED's in the column in the exact same fashion except that we enable those layers instead using their appropriate lines on PORTC.