Tuesday, April 5, 2011

Good Times, Part 2.

[ Part 1   Part 2    Part 3 ]

One of the most fun parts of making the timer was the capacitive touch buttons. So in this post, I'm going to touch on how I did it. I might even post a little code :) ( I'm still getting the hang of this blogger thing so bare with me as I play with formatting, etc). 

The inspiration.

I've worked with mechanical buttons on past projects, and I was always less than happy with how the buttons looked, or cost, or how much effort you had to put in to "getting it right". I've spent hours on mouser or digikey looking at buttons, only to discover the datasheets didn't really give you a good grip on the aesthetics or "feel" of the mechanics. Also, the physical chassis construction work, (my least favorite part of a project), seemed to consume a disproportionate amount of effort. Touch buttons, I thought, would be easy, cheap, and I had a lot of options for how to make them look nice. I was aware of the Atmel QTouch chips, but every time I went to tack some on to one of my hobby parts purchases, they were always out of stock. Then I came across this in an Arduino forum (I don't actually own an Arduindo, but a lot of people do some clever things with them):


That got me thinking, the low AVR microcontrollers are cheaper than most of the touch sensor controllers, and come in DIP form factor. It's such a simple idea, it's brilliant!. So with with a breadboard, my trusty AVR Dragon, and some aluminum foil, I experimented.

The implementation:

I didn't have this blog in mind when doing the project, so I neglected to get a good shot of the early experiments, but this one shows my test touch panel, as well as other parts of the project at various levels of completion.

My test touch panel. (also check out the finished display board, and my homemade  breadboard/6 pin isp adapter)


At first, I thought a clever use of a multiplexor would allow me to isolate a bunch of buttons and minimize cpu pins.

The multiplexor idea came from this guy's post (Viacheslav Slavinsky),Who uses a 12 button touch matrix, but has bit bit more complicated circuit driving his sensors.
I played with his code as a starting point.



It worked, but turned out to be an unnecessary complication. I needed at least 5 cpu pins for the multiplexor, for up to 8 inputs, a max savings of 3 pins. I also decided that the inputs would need a dedicated cpu, since it's very timing sensitive, and I did not want interrupts or processing form the main logic to interfere with the timings.  This meant that I had pins to spare, so I would see if I could make it work without the mux.

The end result, for multiple touch inputs was essentially a circuit like this:

Dirt simple capacitive touch circuit

Where the pins are going to my 2313. The whole trick to this is that I alternate the output pin high and low and measure the time it takes for each pin to "see" the output. Each input pin is a basic RC circuit, and the Mega Ohm resistors empirically proved to be able to isolate each input from interfering with one another.  When a human comes into contact with a touch button, they add capacitance to the circuit, enough to alter the timing of the rise and fall. I measure the rise and fall for each button, and compare it to a "baseline" timing to register a touch.

The initial setup and code worked, but it had a few problems. First, the timings needed to detect a key press were experimental, and hard coded. I also worried that when put together, or needed to use a different clock speed, it would not work. So I take a "baseline" timing measurement, and compare subsequent timings for percentage change. It turned out a 25% change in the time it took to see the input could safely be considered a "hit". When I detect a hit, I send a "d" character out on the UART, followed by the key number. When the timing is back within 25% of the original baseline, I send a "u" along with the key code. I would also listen for a command on the UART that would trigger a "recalibration" - re measure baseline timings.

I hooked the UART up to my Bus Pirate to verify that it was working however:

The second major problem was that I often got bursts of key presses registered. I needed to "debounce" or dampen the states of the buttons. The way that I did this was in code: any detected state transition of a button was tested several times for that new state. For example, if the button's previous was "off" and an "on" state was detected, I retested immediately several times and if all re-tests agree, I consider that button "on". Otherwise, I reject as a false detection and move on.

Those two changes resulted in really solid key press detection. I was so impressed with how well it worked, I decided that I would have to implement some easter eggs that took advantage of responsive, accurate key presses.

Here's some snippets of the code used to do the detection:

Note that a lot of utility macros and functions are left out, you can guess what they do though.

Pulse: this is the core of the detection, just turn on/off the output, and wait for an input, and report the amount of time it took.


static int Pulse(int *runup, int* rundown, uint8_t index)
{
    int up = 0;
    int down = 0;
    //disable intertupts
    cli();
    ON(OUTPUT_PORT, OUTPUT_PIN);
    while ( PinVal(index) == 0 && up != __INT_MAX__)
    {        
        ++up;
    }
    _delay_ms(1);
    OFF(OUTPUT_PORT, OUTPUT_PIN);
    while ( PinVal(index) == 1 && down != __INT_MAX__)
    {        
        ++down;
    }
    //re-enable interrupts
    sei();
    *runup = up;
    *rundown = down;
    _delay_ms(1);
    return 0;
}

Detect: This examines the timing, and decides if the timings merit a detection. Note that I flip the sign if the timings are negative. This was an artifact of the experiments early on, and I just left:

static int Detect(uint8_t index)
{
    int runup;
    int release;

    Pulse(&runup, &release, index);
    int runupDiff = runup - g_baselineRunup[index];
    if (runupDiff < 0)
        runupDiff *= -1;
    int releaseDiff = release - g_baselineRundown[index];
    if (releaseDiff < 0)
        releaseDiff *= -1;
    if (/**/(runupDiff > g_baselineRunup[index] / 4) && (releaseDiff > g_baselineRundown[index] / 4))
    {
        return 1; //we had a detection
    }
    else
    {
        return 0;       
    }    
}

Update: this calls detection routines and does the debouncing, and notifies the UART code about key presses:

void Update()
{
    for (int8_t i = 0; i < NUMBER_OF_BUTTONS; i++)
    {
        int8_t lastResult = GetLastState(i);
        int8_t result = Detect(i);

        //"debounce"
        if (lastResult != result)
        {
            int8_t retries = DETECTION_RETRIES;
            while ((lastResult != result) && (retries > 0))
            {
                result = Detect(i);
                --retries;
            }

            if (lastResult != result)
            {
                //button state change!
                if (result)
                {
                    CommandManager_SendCommand(ButtonCommand_KeyDown, i + IOKEY_OFFSET);
                }
                else
                {
                    CommandManager_SendCommand(ButtonCommand_KeyUp, i + IOKEY_OFFSET);
                }
                //check for commands that need to be sent out
                SetLastState(i, result);
            }
        }
    }
}

Notice the use of 8 bit data types.. I did that where ever I could, as I mentioned in the last post, I frequently ran into problems with my stack pointer running into global memory. The data for timings was one area where I needed 16 bit integers. That was a fun problem to track down. (o.0) Even though I wrote the code in C, it wouldn't fit on the chip without maximum optimization turned on, so I mostly debugged it at the assembly level, good times, ah.

-P

2 comments:

  1. thats a really cool idea!
    ever thought of making a arduino library out
    of it?

    ReplyDelete
  2. That's not a bad idea, jan. I don't know about Arduino, but I could release it as a general AVR package. I intended it to run by itself on a dedicated chip, I'm not sure how well it would work when it had to share cpu time with other code. It may take a little while, I'm pretty swamped, but I'll clean up/vet the code and find somewhere to release it, maybe it would be helpful to others.

    ReplyDelete

I welcome you're thoughts. Keep it classy, think of the children.