Charlieplexing

Note:  If you are seeing this note, then you are looking at an incomplete draft.  Keep checking back as I complete these series of blog posts over the next week or two.

You might be familiar with Charlieplexing with LEDS, but the same technique can be adapted for switches.   Let’s start by replacing LEDs and resistors with diodes and switches in the familiar Charlieplexing schematic layout.

For greater clarity, we are assuming the microprocessor has weak pullups enabled and can omit the external pullup resisors on the lines. This still looks insane and it gets even worse with more switches.   Let’s find a more systematic way to represent the connections.

All we’ve done here is swap the position of some diodes and switches, but the overall circuit is electrically identical.  Now we can begin to make the relation to a conventional scanning matrix more clear.

In this case, the common connection at the cathode for a group of switches would have been a column in a typical matrix configuration.  Let’s make the similarity even more clear by rearranging like this:

 

Basically, the columns are tied into the row lines.  Since we’re using a given row to select a “column”, we can’t use it to read that row at the same time. Thus, there is one less switch per row.  So with n lines, you can read n(n-1) switches.

When a “column” is selected, the other lines are tri-stated with pullups enabled to serve as inputs to read the rows.  For instance, when column 1 is selected, lines 2 & 3 function as input rows.  When column 2 is selected, lines 1 and 3 become input rows.

#define NUM_COLS (n)
#define NUM_ROWS (NUM_COLS - 1)

uint8_t keyMatrix[NUM_ROWS][NUM_COLS];

uint8_t buttonPressed(uint8_t row, uint8_t col)
{
    return keyMatrix[row][col];
}

void deselectCols(void)
{
    TRIS_GPIO0 = 1;
    TRIS_GPIO1 = 1;
    ...
    TRIS_GPIO(n-1) = 1;
}

void selectCol(uint8_t col)
{
    deselectCols();
    
    // Col LAT should already be set to LOW
    // so just enable the driver out of
    // tri-state mode so it can pull the
    // column low.
    switch (col)
    {
        case 0:
            TRIS_GPIO0 = 0;
            break;
            
        case 1:
            TRIS_GPIO1 = 0;
            break;
        ...    
        case (n):
            TRIS_GPIO(n-1) = 0;
            break;
    }
}

void readRows(uint8_t col)
{
    // Read Rows.  Switches will be active low,
    // so invert the reading.
    
    // Here is concrete example, using 3 GPIO
    // lines in a 3 x 2 matrix.  The key to
    // remember is there's n-1 rows and you
    // cannot read a row from selected col.     
    switch(col)
    {
        case 0:
            keyMatrix[0][col].input = !GPIO1;
            keyMatrix[1][col].input = !GPIO2;
            break;
        case 1:
            keyMatrix[0][col].input = !GPIO0;
            keyMatrix[1][col].input = !GPIO2;
            break;

        case 2:
            keyMatrix[0][col].input = !GPIO0;
            keyMatrix[1][col].input = !GPIO1;
            break;
    }
}

//call every millisecond
void scanMatrix(void)
{
    for (uint8_t i =0; i< NUM_COLS; i++)
    {
        selectCol(i);
        // Give time for voltage levels to stabilize
        __delay_us(50); 
        readRows(i);
        deselectCols();
    
    }
}

void initMatrixIO(void)
{
    //configure GPIO pins
    
    // All digital mode
    ANSEL_GPIO(0..n-1) = 0; 

    // Set all latches low. The latches needs to
    // remain low for the rest of the code
    //  to work. This way they can pull columns
    // low when no longer in tri-state.
    LAT_GPIO1(0..n-1) = 0;

    deselectCols();

    // Enable all pullups.  This is necessary
    // when pin is serving as input.
    // Should do no harm when it's an output.
    WPU_GPIO(0..n-1) = 1;

}

void initKeyMatrix(void)
{
    initMatrixIO();
    
    for (uint8_t i = 0; i < NUM_ROWS; i++)
        for (uint8_t j = 0; j < NUM_COLS; j++)
            keyMatrix[i][j] = 0;
}

The main drawback to Charlieplexing is that it’s no longer possible to resolve the ghosting problem when multiple keys are pressed at once.  The other is that you can’t use a keypad wired in a standard matrix.

I know you’ll stop being my friend if I leave you without a working example, so I hacked together a 5 x 4 Charlieplexed key array from five 1 x 4 key matrix strips.

<USB HID Keyboard example firmware using 5 x 4 matrix>

Ghosting and Masking

Note:  If you are seeing this note, then you are looking at an incomplete draft.  Keep checking back as I complete these series of blog posts over the next week or two.

A straightforward implementation of a scanning matrix works well enough when keys are pressed one at a time.  Pressing multiple keys at once can lead to ghosting and masking.

Ghosting makes it appear a switch has been pressed when in reality it hasn’t.  How does this happen?  Let’s examine the following scenario where the keys at (1,1), (1,3) and (3,3) are pressed simultaneously.

The controller starts by bringing column 1 low and since the switch at (1,1) is pressed, it brings row 1 low as expected.  Great so far!

Uh oh.  Looks like the switch at (1,3) is providing an alternate pathway to column 3, also bringing it low.  If you play a lot of Khet, all that follows should be painfully obvious.

Since the switch at (3,3) is also pressed, it brings row 3 low ahead of schedule; it shouldn’t be low until column 3 is actually selected.  The controller reads row 1 and row 3 as active and thus thinks a switch at (1,1) AND (3,1) have been pressed.

The switch at (3,1) has not been pressed, it’s just a ghost!  Hopefully that wasn’t the button for launching the nukes.

Masking happens in a symmetric fashion.  Imagine if you actually pressed the switch at (3,1) while still holding down the others.  If you were to release the switch at (1,3) it would still register as being pressed because pressing the one at (3,1) created a new ghost right at that spot.  This is the sort of thing that causes so many preventable deaths in the PC gaming world.

Fortunately there is an easy fix by incorporating diodes into the matrix so that current goes where it is intended.

Now let’s revisit the same scenario, only with the diodes inserted as previously shown. The diode at (1,3) becomes reverse biased, eliminating the alternate path along with any masking or ghost effects.

So now you know where babies come from… And why you see so many diodes in those joysticks you have been taking apart.

If diodes can fix this issue, why is the problem still present on keyboards?  Mostly because omitting the diodes saves on cost and the matrix is organized such that the problem doesn’t surface under typical usage.