# The LED Sensing Matrix

The LED Sensing Matrix is an interactive display that responds to light and shadow.

A standard LED is designed simply to emit light. But with some clever circuitry, we can configure it to detect light as well.

How does it work? Read on.

### Multiplexing: Controlling Multiple LEDs

Controlling a single LED is simple.

1. Connect the cathode to ground.
2. Connect the anode to a positive voltage. In this case, we connect it to an output pin on our microcontroller (the Arduino Uno).

When the output is high (positive voltage), the light is on; and when the output is low (zero voltage), the light is off.

But what if we want to control many LEDs?

We could try connecting each anode to a separate output pin on the Uno. However, with a large number of LEDs, we would quickly run out of pins — not to mention electrical power. There is a better solution.

Multiplexing is a technique used to light many LEDs at the same time. The LEDs are arranged in a grid, as shown below. Each column shares a common anode, and each row shares a common cathode. To light an individual LED, say, LED 8, we would connect column 3 to power and row 2 to ground. In this example, we can control 25 individual LEDs using a mere 10 pins.

Problem solved? Not quite. Say we want to light only LED 2 and LED 19. To turn on LED 2, we would connect column 2 to power and row 1 to ground. To turn on LED 19, we would connect column 4 to power and row 4 to ground. We would like for only LEDs 2 and 19 to be illuminated. But what we end with is a display like this:

LEDs 2 and 19 are illuminated — but so are the LEDs 4 and 17. The problem is that we can’t have a column connected to power in one row, and at the same time, have the column not connected to power in another. To resolve this issue, we connect each row to ground, one at a time, and light the appropriate columns. See the sequence of images below. If we do this fast enough, the human eye can’t notice the flicker of LEDs turning on and off, and it looks like we are lighting all the rows at the same time.

### Lighting the Matrix

The Sensing Matrix display is made up of two 8 x 8 dot matrices (Sparkfun COM-00682), which are set up for multiplexing as shown in the diagram below. Each dot contains both a red and a green LED. By turning on only one LED, we can emit either red light or green light. By turning on both of them, we can display yellow light from the dot.

Let us first consider only one of the 8 x 8 dot matrices. The 24 pins on the matrix outnumber the available outputs on the Uno, so we need some external device to control the display. For this purpose, we use three 8-bit shift registers (74HC595). Each register has eight digital outputs, similar to the ones on the Uno. To set the outputs high or low, we write an 8-bit word from the Uno to the register. Each bit in the word corresponds to an output on the register, and a 0 or 1 sets the output low or high, respectively. For more information on shift registers, look here.

The matrix datasheet tells us that each LED can handle a peak forward current of 30 mA. Go higher than this number and we risk damaging the circuit — go significantly lower and LEDs will appear dim. Given a 5 V power supply, we can restrict the current through a single LED to a safe 28 mA by putting a 180-ohm resistor at each anode. Since we are lighting the matrix one row at a time, each cathode must be capable of sinking enough current to light all 16 LEDs at once — a total of 448 mA.

It’s at this point that we run into a problem: the shift register can’t handle that much current. Each register output can only accommodate 20 mA — short of the 28 mA allotted for the each column and well below the 448 mA needed for each of the rows. Even if the shift register didn’t have this limitation, the Uno itself is only capable of sinking 400 mA. How do we get around this issue?

use PNP transistors for sourcing current

Instead of using the register outputs to power the LEDs directly, we use them to control series of transistors. The cathode side is connected to ground through an NPN Darlington array (ULN2803), which is capable of sinking up to 500 mA per pin. On the anode side, each column is connected to a 5V power supply through a PNP transistor (2N3906), which can source up to 200 mA. (See image above.) Generally speaking, NPN transistors are better for sinking current and PNP transistors are better for sourcing current — which is why we have them at the cathode and the anode respectively. See the circuit diagram below. (Note: not pictured in this diagram are the 200-ohm resistors between the base of the PNP transistors and the shift register outputs.)

### An LED as a Light Sensor: Two Methods of Detection

What makes this project exciting is that we can use the LED matrix to detect light, not just emit it. The catch is that the LEDs can’t be illuminated during the sensing process. This means that sensing has to be done very quickly, so the human eye doesn’t notice that the LEDs have been switched off.

There are two methods of light detection with an LED, described below.

#### Cathode Sensing

This technique works by reverse biasing the LED. When the diode is in this state, it acts like a parallel plate capacitor. The diode’s depletion region serves as the insulator, and the anode and cathode act as the charged plates.

When light hits the depletion region, it dislodges an electron, creating an electron-hole pair. The electric field then sweeps the hole to the p-side and the electron to n-side — creating a small reverse current from the cathode to anode. If more light hits the depletion region, a larger current is generated.

To measure this current, we switch the cathode connection from a 5V output to a digital input. We then measure the amount of time it takes for the input pin to go low. If there is a lot of light, then a large current will be generated and the input pin will fall low very rapidly. If there is only a little light, then a small current will be generated and the input pin will take a longer time to reach low.

The advantage of this method is that it doesn’t require any additional hardware — with just two I/O pins on your microcontroller, you can both emit and detect light using an LED. The downside is that detecting light involves waiting for an input to drop low, which can take up to a few seconds in situations with little light.

Unfortunately, this means that cathode sensing and multiplexing are incompatible. For LED multiplexing to be effective, the LED can only be off for a few milliseconds, so the human eye doesn’t notice the flicker. If the LED needs to be off for multiple seconds to detect light, then there will be noticeable flickering.

#### Anode Sensing

Anode sensing requires more hardware (and is therefore more expensive) than cathode sensing, but measurements can be taken in microseconds rather than milliseconds or seconds.

The anode is connected to an analog input and the cathode is connected to ground. As in cathode sensing, when light hits the depletion region, it dislodges an electron to create an electron-hole pair. The electron is swept to the n-side and the hole is swept to the p-side. Charge quickly builds, and the voltage difference can be measured by the analog input at the anode. The more light that the LED detects, the larger the voltage will be.

With this method, we are able to measure a row of LEDs in mere microseconds — a large improvement from the previous method. Because of the speed of this technique, we will use anode sensing to detect light in the matrix.

The Uno only has six analog inputs, so for this application we use an external analog-to-digital converter (MCP3208). The MCP3208 is a 12-bit, 8-channel ADC that communicates with the Uno using the Serial Peripheral Interface. We connect each channel to an anode in the matrix, and read the values one at a time through pin 12 on the Uno.

### Diagrams and Code

Final circuit diagrams and commented microcontroller code are shown below.

In the final section of the posted video, a dot on the LED display is shown to “bounce” off of physical objects that are placed on the screen. The matrix senses that certain spots on the display are covered, and moves the dot accordingly. The commented microcontroller code is below.

``````#include
#include

#define DATA_ANODE 2
#define CLOCK_ANODE 3
#define LATCH_ANODE 4

#define DATA_CATHODE 6
#define CLOCK_CATHODE 7
#define LATCH_CATHODE 8

#define CS_0 9
#define CS_1 10

int NUM_CALIB = 100;    // number of samples to be taken during calibration
int calibrate;          // number of samples taken so far during calibration

byte sensor[2][8];          // array to hold sensor readings (2 screens, 8 rows)
int ref[2][8][8];           // array to hold reference values
unsigned long avg[2][8][8]; // array to hold reference values, for calibration (2 screens, 8 rows, 8 cols)

byte screen_red[2][8];      // array representing all red anodes (8 anodes per screen, 2 screens)
byte screen_green[2][8];    // array representing all green anodes (8 anodes per screen, 2 screens)

int ball_x = 4;
int ball_y = 5;

int ball_dx = 1;
int ball_dy = -1;

unsigned long time;

void setup() {

// begin serial communications at 57600 bps
Serial.begin(57600);

calibrate = 0;        // no calibration samples have been taken yet

// Shift register pins to control anodes (columns)
pinMode(LATCH_ANODE, OUTPUT);
pinMode(CLOCK_ANODE, OUTPUT);
pinMode(DATA_ANODE, OUTPUT);

// Shift register pins to control cathodes (rows)
pinMode(LATCH_CATHODE, OUTPUT);
pinMode(CLOCK_CATHODE, OUTPUT);
pinMode(DATA_CATHODE, OUTPUT);

}

void loop() {

// if done calibrating...
if (calibrate > NUM_CALIB*8*2) {

measureRows();                    // take measurements at anode

if (millis() - time > 100) {
time = millis();
}

for (int i = 0; i < 8; i++) {
lightRow(i);                    // turn on appropriate LEDs
}

resetAnodes();

// if still calibrating...
} else {

clearScreen();                    // turn LEDs off
measureRows();                    // take measurements at anode

for (int i = 0; i < 8; i++) {
lightRow(i);                    // turn on appropriate LEDs
}

}

}

void measureRows() {

// setting PNP transistor bases to HIGH disconnects the anodes from the power supply
digitalWrite(LATCH_ANODE, LOW);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, B11111111);    // set PNP bases HIGH
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, B11111111);    // set PNP bases HIGH
digitalWrite(LATCH_ANODE, HIGH);

for (int i = 0; i < 8; i++) {

byte cat = B00000001 << i;   // corresponding bit in cathode register

// set NPN transistor base HIGH to connect selected cathode to ground; leave
// all other NPN bases LOW to disconnect remaining cathodes from GND
digitalWrite(LATCH_CATHODE, LOW);
shiftOut(DATA_CATHODE, CLOCK_CATHODE, MSBFIRST, cat);    // set one NPN base HIGH, others LOW
shiftOut(DATA_CATHODE, CLOCK_CATHODE, MSBFIRST, cat);    // set one NPN base HIGH, others LOW
digitalWrite(LATCH_CATHODE, HIGH);

// current state:
// - all anodes disconnected
// - cathodes for selected row connected to GND, all other cathodes disconnected

measureRow(i, 0);    // measure selected row on screen 0
measureRow(i, 1);    // measure selected row on screen 1
}

}

void measureRow(int row, int screen) {

int cs;    // chip select

// choose screen to read from
if (screen == 0)
cs = CS_0;
else if (screen == 1)
cs = CS_1;

delayMicroseconds(50);    // pause to let charge build

int rowValues[8];

// if we are still calibrating the device...
if (calibrate < NUM_CALIB*8*2) {
calibrate++;      // add to calibration sample count

// add raw values into array, values to be averaged later
for (int j = 0; j < 8; j++) {
avg[screen][row][j] += rowValues[j];
}
}

// if we have just finished calibrating the device...
if (calibrate == NUM_CALIB*8*2) {

calibrate++;    // add to calibration count so we don't repeat this code later
for (int k = 0; k < 2; k++) {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
avg[k][i][j] = avg[k][i][j] / NUM_CALIB;     // average values
ref[k][i][j] = (int) (avg[k][i][j] * 0.75);  // compute reference values
}
sensor[k][i] = B11111111;                          // set up array to hold sensor data
}
}

}

// if the device has already been calibrated...
if (calibrate > NUM_CALIB*8*2) {

// compare measured value to reference value, and record in sensor array
for (int j = 0; j < 8; j++) {

if (rowValues[j] < ref[screen][row][j])
sensor[screen][row] &= ~(B10000000 >> j);  // measured value is lower, light not detected
else
sensor[screen][row] |= B10000000 >> j;     // measured value is higher, light detected

}
}

}

byte ball_byte = B10000000 >> (ball_x % 8);
byte ball_screen = (ball_x - (ball_x % 8)) / 8;

// erase ball from screen
screen_red[ball_screen][ball_y] |= ball_byte;
screen_green[ball_screen][ball_y] |= ball_byte;

int ball_screen_dx;
int ball_byte_dx;

if (ball_x + ball_dx == 8 && ball_x == 7) {
ball_screen_dx = ball_screen + 1;
ball_byte_dx = B10000000;
} else if (ball_x + ball_dx == 7 && ball_x == 8) {
ball_screen_dx = ball_screen - 1;
ball_byte_dx = B00000001;
} else {
ball_screen_dx = ball_screen;
if (ball_dx > 0)
ball_byte_dx = ball_byte >> 1;
else
ball_byte_dx = ball_byte << 1;
}

// x collision
if (((~sensor[ball_screen_dx][ball_y] & ball_byte_dx) != 0) || (ball_x + ball_dx > 15 || ball_x + ball_dx < 0))
ball_dx *= -1;

// y collision
if (((~sensor[ball_screen][ball_y + ball_dy] & ball_byte) != 0) || (ball_y + ball_dy > 7 || ball_y + ball_dy < 0))
ball_dy *= -1;

// update ball location
ball_x += ball_dx;
ball_y += ball_dy;

ball_screen = (ball_x - (ball_x % 8)) / 8;

ball_byte = B10000000 >> (ball_x % 8);
screen_red[ball_screen][ball_y] &= ~ball_byte;
screen_green[ball_screen][ball_y] &= ~ball_byte;

}

// Turn on appropriate LEDs for the given row
void lightRow(int row) {
byte cat = B00000001 << row;   // corresponding bit in cathode register

// set NPN transistor base HIGH to connect selected cathode to ground; leave
// all other NPN bases LOW to disconnect remaining cathodes from GND
digitalWrite(LATCH_CATHODE, LOW);
shiftOut(DATA_CATHODE, CLOCK_CATHODE, MSBFIRST, cat);    // set one NPN base HIGH, others LOW
shiftOut(DATA_CATHODE, CLOCK_CATHODE, MSBFIRST, cat);    // set one NPN base HIGH, others LOW

// current state:
// - all anodes disconnected
// - cathodes for selected row connected to GND, all other cathodes disconnected

// setting PNP transistor bases to LOW connects the anodes to the power supply
// this code sets appropriate bases LOW according to screen_red and screen_green arrays
digitalWrite(LATCH_ANODE, LOW);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, screen_red[1][row]);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, screen_red[0][row]);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, screen_green[1][row]);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, screen_green[0][row]);

digitalWrite(LATCH_CATHODE, HIGH);
digitalWrite(LATCH_ANODE, HIGH);

// current state:
// - some anodes connected to power, the rest are disconnected
// - cathodes for selected row connected to GND, all other cathodes disconnected

delayMicroseconds(500);

}

void resetAnodes() {
// setting PNP transistor bases to HIGH disconnects the anodes from the power supply
digitalWrite(LATCH_ANODE, LOW);
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, B11111111);    // set PNP bases HIGH
shiftOut(DATA_ANODE, CLOCK_ANODE, LSBFIRST, B11111111);    // set PNP bases HIGH
digitalWrite(LATCH_ANODE, HIGH);
}

// wipe screen (set LEDs off)
void clearScreen() {
for (int k = 0; k < 2; k++) {
for (int i = 0; i < 8; i ++) {
screen_red[k][i] = B11111111;
screen_green[k][i] = B11111111;
}
}
}	``````

Questions? Comments? Feel free to contact me here.