Using a laser pointer and a matrix LED as a two-dimensional input device

Laser Command is a game which I build using a 8x8 matrix LED and an Arduino Mini. This game was developed as a "sample" class project in S10-05833 Gadgets, Sensors and Activity Recognition in HCI. The class is taught by Scott Hudson at Carnegie Mellon University, and I'm doing TA for the class. The name "Laser Command" comes from an old game called Missile Command. In Missile Command, you are asked to shoot enemy's missiles using missiles. In Laser Command, you shoot using laser, i.e., a laser pointer.

The most interesting part in this game is that the game uses a laser pointer as a two-dimensional input device in conjunction with a matrix LED. Here are a video, explanations of how it works, circuit diagrams and source code. I hope that these are enough for you to replicate and/or build on the technique :)

How it works

Essentially, I make a 8x8 matrix LED as a 8x8 light sensor array using two techniques.
In the followings, I will explain how the two techniques work using a simplified example shown below. The example consists of two digital ports (D0 and D1), two analog ports (A0 and A1) and four LEDs, i.e., a 2x2 matrix LED.

Reverse Bias

The first technique is well-known technique for using LEDs as light sensors. In this technique, we charge LEDs by applying reverse bias, and, then, measure how quickly the charged current leaks after stopping the reverse bias. In the example here, the technique works as follows:
  1. Reverse bias the LEDs by making D0 and D1 HIGH, and A0 and A1 LOW.
  2. Make D0 and D1 INPUT. Initially, both of them are HIGH because of current charged in the LEDs.
  3. Then, as the current leaks, D0 and D1 becomes LOW after a certain period.
The time required for the leakage to discharge the LEDs is inversely proportional to the brightness. So, if D0 becomes LOW quickly, we can know that one of (or both) the LEDs at the top row is pointed by a laser pointer. Likewise, if D1 becomes LOW quickly, the LEDs at the bottom row are pointed by a laser pointer.

This technique is sufficient if we just want to use a single LED as a light sensor. But, it is not sufficient if we want to use a matrix LED as a light sensor array. As described above, we can detect which row is pointed by a laser pointer, but we cannot distinguish which column is pointed because LEDs at the same row share a cathode.
So, we need one more technique to detect which column is pointed.

Direct Measurement

The second technique is not commonly used because it requires analog inputs. When we project strong light on LEDs, the LEDs are charged. In this technique, we directly measure the voltage generated by the charged LEDs as follows:
  1. Discharge all current stored in the LED by making all ports LOW.
  2. Then, measure voltage at A0 and A1 using analogRead(). The voltage is proportional to the brightness.
In my circuit, the voltage rises from 0.5V to 1.5V (this value depends on LEDs) when the LED is pointed by a laser pointer. This is not enough for digital input to become HIGH. But, the difference can be detected by analogRead(). Using this technique, we can detect which column is pointed by a laser pointer. Therefore, by using these two technique by turns, we can detect which LED is pointed.

Circuit

Here is a circuit diagram which you can use with sensor sample source code. The circuit is a 8x8 matrix LED version of the example (2x2 version) discussed above. In the circuit, A6 and A7 are connected to D11 and D12 receptively. This is because these two ports do not work as digital outputs. This circuit requires eight analog inputs, so that you need an Arduino Mini, Nano or Mega (or whatever which supports more than eight analog ports). If you use an Arduino Duemilanove, you still can make a 8x5 matrix LED as a light sensor array with slight modifications.
In Laser Command, I also connected D10 to a piezo in addition to this circuit diagram.

Source Code

Here is a sample code which make a 8x8 LED matrix a light sensor array.
Download a sample code
Also, here is a source code of Laser Command.
Download Laser Command
001//
002// Using a Laser Pointer and a 8x8 Matrix LED
003// as Two-dimensional Input device
004//   Developed Eiji Hayashi 
005//   2010/03/26 Version 1.0
006//
007// *** What's this? ***
008// This is a sample code which shows how we can use
009// a 8x8 matrix LED as a light sensor array, and
010// how we can detect position of a LED pointed by
011// a laser pointer.
012//
013// *** Wiring ***
014// This code assumes:
015//   D2 to D9 are connected to cathodes
016//   A0 to A7 are connected to anodes
017//   D11 and D12 are connected to A6 and A7 respectively
018// Please refer to circuit diagrams for more details.
019//
020// *** How it works ***
021// When you turn on a circuit, all LEDs turn on one-by-one.
022// After that LEDs pointed by a laser pointer turn on.
023//
024 
025#include <avr/delay.h>
026  
027// A technique to make analogRead faster
029#ifndef cbi
030#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
031#endif
032#ifndef sbi
033#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
034#endif
035 
036int screen[8] = {0,0,0,0,0,0,0,0};  // bits displayed on the LED matrix
037int thresh[8] = {0,0,0,0,0,0,0,0};  // threshold values used in CheckColumn()
038                                    // the thresholds are adjusted in
039                                    // adjustThreshCol()
040// Initial setup
041void setup(){
042  int rows[8] = { 2, 3, 4, 5, 6, 7, 8, 9};
043  int cols[8] = { 14, 15, 16, 17, 18, 19, 11, 12 };
044   
045  //Serial.begin(9600);
046  DDRC = B11111111;
047  DDRD = B11111111;
048  PORTD = B11111111;
049 
050  // Adjust prescaler to make analogRead faster
051  sbi(ADCSRA,ADPS2) ;
052  cbi(ADCSRA,ADPS1) ;
053  cbi(ADCSRA,ADPS0) ;
054 
055  // Adjust threshold for column detection
056  adjustThreshCol();
057   
058  // Turn on LEDs one-by-one for sanity check
059  for( int i=0; i<8; i++ ){
060    pinMode( rows[i], INPUT );
061    pinMode( cols[i], OUTPUT );
062  }
063   
064  for( int i=0; i<8; i++ ){
065    pinMode(rows[i], OUTPUT );
066    digitalWrite( rows[i], LOW );
067    for( int j=0; j<8; j++ ){
068      digitalWrite( cols[j], HIGH );
069      delay(50);
070      digitalWrite( cols[j], LOW );
071    }
072    pinMode( rows[i], INPUT );
073  }
074}
075 
076// Main loop
077// Detect a laser pointer and turn a pointed LED on
078void loop()
079{
080  // detect a pointer
081  // if no pointer is detected, 8 will be returned.
082  int row = checkRow(); 
083  int col = checkColumn();
084 
085  // put 1 at the pointed location
086  if( row != 8 && col != 8 )
087     screen[row] |= 1 << col;
088   
089   /*
090  Serial.print(row);
091  Serial.print(",");
092  Serial.println(col);
093  */
094 
095  // Show the content of screen
096  DDRC |= B00111111;  // make column 0 to 5 port output
097  DDRB |= B00011000;   // make column 6 and 7 output
098  DDRD &= B00000011;  // make row 0 to 5 input
099  DDRB &= B11111100;  // make row 6 and 7 input
100   
101  for( int i=0; i<5; i++ ){
102    for( int row=0; row<8; row++ ){
103       
104      if( row<6 ){
105        DDRD |= 1 << (row+2);
106      }
107      else{
108        DDRB |= 1 << (row-6);
109      }
110    
111      PORTC = screen[row] & B00111111;
112      PORTB = ( screen[row] & B11000000 ) >> 3;
113      _delay_us( 300 );
114   
115      DDRD &= B00000011;
116      DDRB &= B11111100;
117    }
118  }
119}
120 
121// Check which column is pointed
122int checkColumn(){
123  int val[8];  // brightness
124 
125  // *** clear charges ***
126  // make all anodes output and low
127  DDRB = B00011000;
128  PORTB = B00000000;
129  DDRC = B11111111;
130  PORTC = B00000000;
131 
132  // make all cathodes output and low;
133  DDRD |= B11111100;
134  DDRB |= B00000011;
135  PORTD &= B00000011;
136  PORTB &= B11111100;
137 
138  _delay_us(10);  // wait for a while to clear charges
139 
140  // make all anodes input to measure charges cause by light
141  DDRB &= B11100111;
142  DDRC = B00000000;
143   
144  _delay_us(200);  // wait for a while
145   
146  // read analog values at all anodes
147  for( int col=0; col<8; col++ ){
148    val[col] = analogRead( col );
149  }
150   
151  // calculate difference between current values and thresholds
152  for( int col=0; col<8; col++ ){
153    val[col] = val[col] - thresh[col];
154  }
155  
156  
157  // if the differences are bigger than 10,
158  // the column is pointed by a laser pointer
159  int signal = 8;
160  for( int col=0; col<8; col++ ){
161    if( val[col] > 10 ){
162      signal = col;
163      break;
164    }
165  }
166   
167  // uncomment the following to see sensor values via serial communication
168  /* for( int col=0; col<7; col ++ ){
169    Serial.print(val[col]);
170    Serial.print(",");
171  }
172  Serial.println(val[7]);
173  */
174   
175  return( signal );
176   
177}
178 
179// Measure analog values at anodes to calculate threshold values.
180// Using the threshold values mitigates effects of ambient light conditions
181void adjustThreshCol()
182{
183  for( int cnt=0; cnt<100; cnt ++ ){  // measure the values 100 times and take average
184    int val[8];
185 
186    // *** clear charges ***
187    // make all anodes output and low
188    DDRB = B00011000;
189    PORTB = B00000000;
190    DDRC = B11111111;
191    PORTC = B00000000;
192   
193    // make all cathodes output and low;
194    DDRD |= B11111100;
195    DDRB |= B00000011;
196    PORTD &= B00000011;
197    PORTB &= B11111100;
198   
199    _delay_us(10);  // wait for a while to clear charges
200   
201    // make all anodes input to measure charges cause by light
202    DDRB &= B11100111;
203    DDRC = B00000000;
204 
205    _delay_us(200);
206    int input = 0;
207    for( int i=0; i<8; i++ ){
208      val[i] = analogRead( i );
209      thresh[i] += val[i];
210    }
211  
212   
213  // take average
214  for( int i=0; i<8; i++ ){
215    thresh[i] = thresh[i] / 100;
216  
217}
218 
219// Check which row is pointed
220int checkRow()
221{
222  int input = 0;
223   
224  // *** Apply reverse voltage, charge up the pin and led capacitance
225  // make all cathodes high
226  DDRD |= B11111100;
227  DDRB |= B00000011;
228  PORTD |= B11111100;
229  PORTB |= B00000011;
230 
231   
232  // set all anodes low
233  DDRC = B00111111;
234  PORTC = B11000000;
235  DDRB |= B00011000;
236  PORTB &= B11100111;
237 
238  _delay_us(100);  // wait for a while to charge
239   
240  // Isolate the pin connected to cathods
241  DDRD &= B00000011;  // make N0-N5 INPUT
242  DDRB &= B11111100;  // make N6 and N7 INPUT
243   
244  // turn off internal pull-up resistor
245  PORTD &= B00000011; // make N0-N5 LOW
246  PORTB &= B11111100; // make N6 and N7 LOW 
247 
248  // measure how long it takes for cathodes to become low
249  int val[8] = {100,100,100,100,100,100,100,100};
250  for( int cnt=0; cnt<50; cnt++ ){  //you may need to adjust this threshold
251    for( int r=0; r<8; r++ ){
252      if( digitalRead( 2+r ) == LOW && val[r] == 100 )
253        val[r] = cnt;
254    }
255  }
256   
257  // uncomment the following if you want to check values
258  /*
259  for( int r=0; r<7; r++ ){
260    Serial.print( val[r] );
261    Serial.print(",");
262  }
263  Serial.println( val[7] );
264*/
265  // if a pin becomes low quicker than 50, the pin is pointed
266  int signal = 8;
267  for( int i=0; i<8; i++ ){
268    if( val[i] < 49 ){
269      signal = i;
270      break;
271    }
272  }
273 
274  return( signal );
275}