Wednesday, 12 October 2011

Musical ping-pong tables and 2d multilateration

I'm currently trying to help out a local artist who is building an Interactive Ping-Pong Table, where each bounce of the ball generates a musical sound which depends on the position of the bounce.

I thought it should be possible to do this without drastically changing the table (i.e. without chopping the surface up) by using 3 piezo disks and an Arduino or PIC to time the arrival of the pulse at each disk and work out the position of the ball. It all sounded pretty easy, and an interesting project. Its certainly been interesting, but I'll think twice in future before deciding something is easy before I've properly thought  it through :o)

I found pretty quickly that some kind of amplification is needed.. the piezo disks are pretty sensitive to a sounds close by but not so great for something the other end of a table. First of all I tried to boost the level using 4069 inverter chips (I got that idea from Nicholas Collins' book - Handmade electronic music) since I've never really understood op amps and didn't want to get into all that dual supply rubbish. In my initial circuit I used an NPN Darlington transistor on the output of the amplifier stage to generate the logic pulse.

It kind of worked, but I was finding that the MCU would hang when an interrupt-on-change interrupt was being fired by multiple sensors. I also had a problem with the output getting stuck on (I think this might have been due to supply noise, noise picked up on a long wire to the piezo, and an overly sensitive amp stage). I  think the hang thing might have been due to noisy outputs triggering a rapid train of interrupts than the poor PIC could not handle. I have an IKA Logic analyser and using this I could see a mad train of pulses coming from sound waveform, echoes, supply noise whatever... I don't really know, but the PIC didn't like it.

Searching about for ideas online I read about running op-amps like LM358 from a single supply, which seemed to be a better way to do things than using logic chips as amps. I also saw how a 555 monostable circuit can be used to clean up a dirty pulse by keeping an output high for a timed period as soon as the first edge of the input pulse comes in, so the train of pulses from reverberations and so on get masked by a nice clean extended output pulse... nice and friendly for MCU interrupt pins.

The resulting circuit seems to work pretty well, even though it still seems a bit complicated. Maybe it is a case of over-engineering, but I learned a lot and it does at least work pretty well. Using SMDs I can also get it on a board about the same size as the piezo disk so it can sit on top.



For some reason I thought the maths behind working out a point from timing would be easy..and it is in one dimension with 2 sensors...



However working in 2 dimensions with 3 sensors seems to be a completely different kettle of fish... the technique is called Multilateration and there have been entire research papers written about it :o) The problem is that all the timing readings you're working with are relative... its more complicated than I  thought to get back to an actual position. Maybe I can simplify things, since my sensors will be arranged in the corners of a rectangular area and I can always calibrate them at the start by tapping the corners of the table. Or maybe some dirty trial and error approach will be good enough... Anway thats the next step... wish me luck..!

Here is the source code used in this clip

// SOURCEBOOST C
// PIC16F688
#include <system.h>
#pragma DATA _CONFIG, _MCLRE_OFF&_WDT_OFF&_INTRC_OSC_NOCLKOUT
#pragma CLOCK_FREQ 8000000

#define SENSEA            0b00010000
#define SENSEB            0b00100000
#define SENSE_MASK        (SENSEA|SENSEB)

typedef unsigned char byte;

// INITIALISE SERIAL PORT FOR MIDI
void init_usart()
{
    pir1.1 = 1;    //TXIF transmit enable
    pie1.1 = 0;    //TXIE no interrupts
    
    baudctl.4 = 0;        // synchronous bit polarity 
    baudctl.3 = 1;        // enable 16 bit brg
    baudctl.1 = 0;        // wake up enable off
    baudctl.0 = 0;        // disable auto baud detect
        
    txsta.6 = 0;    // 8 bit transmission
    txsta.5 = 1;    // transmit enable
    txsta.4 = 0;    // async mode
    txsta.2 = 0;    // high baudrate BRGH

    rcsta.7 = 1;    // serial port enable
    rcsta.6 = 0;    // 8 bit operation
    rcsta.4 = 0;    // enable receiver
        
    spbrgh = 0;        // brg high byte
    spbrg = 15;        // brg low byte (31250)    
}

enum {
    READY,
    LISTENING,
    TIMING,
    TIMEOUT
};

byte remaining;
long timeA;
long timeB;
byte state;

void interrupt( void )
{
    // check for interrupt on change
    if(intcon.0) // IOCA fired
    {
        // are any of the signals we're waiting
        // for now ready for us?
        byte savePortA = porta;
        byte whichSensor = savePortA & remaining;
        unsigned long thisTime;
        if(whichSensor)
        {
            if(state == LISTENING)
            {
                // start the timer
                t1con.0 = 1;
                thisTime = 0;
                state = TIMING;
            }
            else
            {
                // grab the current time
                thisTime = tmr1h << 8 | tmr1l;
            }
        
            // Grab times from sensors
            if(!!(whichSensor & SENSEA))
                timeA = thisTime;
            if(!!(whichSensor & SENSEB))
                timeB = thisTime;
                
            // clear bits for the sensors we 
            // already have
            remaining &= ~savePortA;
            if(!remaining)
            {
                intcon.3 = 0;            // ioca off
                state = READY;
            }
        }        
        
        // clear interrupt fired flag
        intcon.0 = 0;
    }    
}
        
////////////////////////////////////////////////////////////
// SEND A MIDI BYTE
void send(unsigned char c)
{
    txreg = c;
    while(!txsta.1);
}

////////////////////////////////////////////////////////////
// NOTE MESSAGE
void sendNote(byte channel, byte note, byte value)
{
    send(0x90 | channel);
    send(note&0x7f);
    send(value&0x7f);
}
void main()
{ 
    // osc control / 8MHz / internal
    osccon         = 0b01110001;
    
    
    // comparator off
    cmcon0         = 7;                      
    
    // configure io
    trisa         = SENSE_MASK;                  
    trisc         = 0b00000000;              
    ansel         = 0b00000000;
    porta        = 0b00000000;
    portc        = 0b00000000;

    // initialise MIDI comms
    init_usart();

    // 
    t1con = 0b00000000;
    
    // interrupt on change porta.4
    ioca = SENSE_MASK;
    intcon.7 = 1;
    intcon.3 = 0;
    intcon.0 = 0;

    byte note = 0;
    for(;;)
    {
        // Prepare to listen
        timeA=0xffff;
        timeB=0xffff;
        remaining = SENSE_MASK;
        state = LISTENING;
        t1con.0 = 0;            // reset the timer
        tmr1h=0;
        tmr1l=0;
        intcon.3 = 1;            // ioca on
        
        // wait to start timing
        while(LISTENING == state);
        
        // wait to complete timing
        while(TIMING == state)
        {
            unsigned long timeNow = tmr1h << 8 | tmr1l;
            if(timeNow > 0x8000)
                state = TIMEOUT;
        }
        
        if(TIMEOUT == state)
        {
            // ignore the interrupt if it does 
            // not register on all the sensors
        }
        else
        {
            long x=0;
            // i know i'm getting reading of up to 5000 'cos I printed 
            // them to serial port... you might get something different
            if(timeA > 5000) 
                timeA = 5000;
            if(timeB > 5000) 
                timeB = 5000;
            if(timeA) 
                x = 5000 + timeA;
            else if(timeB) 
                x = 5000 - timeB;
            if(x)
            {
                note = x/100; // is is in range 0-10000 so move this to MIDI range 0-100
                sendNote(0, note, 127);
            }
        }
        
        // delay (I think delay_ms function needs timer1)
        int i=1000;
        while(++i);        
        if(note)
        {
            sendNote(0, note, 0);
            note=0;
        }
    }
}

3 comments:

  1. Take a look at this for some help with a simple approach to the math: http://asuwlink.uwyo.edu/~hamann/TrilatShow.pdf

    ReplyDelete
  2. @JesseL: Thanks for the link.. I had seen that article before, but it seemed to me that it needed the distance/signal travel time from each sensor to the source to be known absolutely, rather the relative to the time the first signal arrives at any sensor (which is what I will have). I will read through it again and see if I got it wrong

    ReplyDelete
  3. It looks like a very complicated project to build.

    ReplyDelete