Blinky prop for Comic Con

Thread Starter

nsaspook

Joined Aug 27, 2009
13,262
I'm making a little blink led prop controller for my kids Comic Con costume. I've got my best Dr. Strange T-shirt for the event and Stan 'The Man' Lee is there for an Autograph on one item of your own. Yes!
http://rosecitycomiccon.com/celebrity-guests/

It's going inside a arm controller band so I wanted touch control instead of buttons to make it look cool so I used the PIC18F25K22 with its CTMU module to read finger movements and presses.

The CTMU is configured to read changes in capacitance with a timed constant current source to two hand sensors. As the finger get nears the sensor (a wire) the capacitance increases causing the timed current source voltage across the capacitor to drop in a linear fashion.

One of the first design options is the value of charging current and charge times that vary with sensor types.
Slower with lower current or faster with higher currents?

Usually higher currents give better noise immunity but lower sensitivity to touches.

I then wrote the software and built a small proto board for the controller.
There's not really much to it. Socket for the pic, a reset button, connector for the sensors, a socket for the LEDs wiring header, 5vdc switching regulator for the 9vdc input and a fiber optic transmitter for rs-232 debugging data.


The C18 software does a simple timer based round-robin CTMU/ADC sample routine in the ISR to gather sensor data into an array for later processing. This routine also sends the realtime data out the SPI port and into the timer3 data buffer for possible processing.

C:
void InterruptHandlerHigh(void)
{
    static union Timers timer;
    static uint8_t i_adc = 0;

    if (INTCONbits.TMR0IF) { // check timer0 irq
        if (!CTMUCONHbits.IDISSEN) { // charge cycle timer0 int, because not shorting the CTMU voltage.
            DLED1 = HIGH;
            CTMUCONLbits.EDG1STAT = 0; // Stop charging touch circuit
            TIME_CHARGE = FALSE; // clear charging flag
            CTMU_WORKING = TRUE; // set working flag, doing touch ADC conversion
            // configure ADC for next reading
            ADCON0bits.CHS = ctmu_button; // Select ADC channel
            ADCON0bits.ADON = 1; // Turn on ADC
            ADCON0bits.GO = 1; // and begin A/D conv, will set adc int flag when done.
        } else { // discharge cycle timer0 int, because CTMU voltage is shorted
            DLED1 = LOW;
            ADCON0bits.CHS = ctmu_button; // Select ADC channel for charging
            CTMUCONHbits.IDISSEN = 0; // end drain of touch circuit
            TIME_CHARGE = TRUE; // set charging flag
            CTMU_WORKING = TRUE; // set working flag, doing
            timer.lt = charge_time[ctmu_button]; // set timer to charge rate time
            TMR0H = timer.bt[HIGH]; // Write high byte to Timer0
            TMR0L = timer.bt[LOW]; // Write low byte to Timer0
            CTMUCONLbits.EDG1STAT = 1; // Begin charging the touch circuit
        }
        // clr  TMR0 int flag
        INTCONbits.TMR0IF = 0; //clear interrupt flag
    }

    if (PIR1bits.ADIF) { // check ADC irq
        PIR1bits.ADIF = 0; // clear ADC int flag
        spi_stat.adc_count++;
        PIR1bits.SSPIF = LOW; // clear SPI flags
        PIE1bits.SSP1IE = HIGH; // enable to send second byte
        SSP1BUF = ADRESH | ((ctmu_button << 4)&0xf3);
        adc_buffer[ctmu_button][i_adc] = ADRES;
        timer.lt = TIMERDISCHARGE; // set timer to discharge rate
        if (++i_adc >= ADC_READS) {
            TMR3H = ADRESH | ((ctmu_button << 4)&0xf3); // copy high byte/channel data [4..7] bits
            TMR3L = ADRESL; // copy low byte and write to timer counter
            i_adc = 0; // reset adc buffer position
            CTMU_ADC_UPDATED = TRUE; // New data is in timer3 counter, set to FALSE in main program flow
            timer.lt = TIMERPROCESS; // set timer to data processing rate
        }
        CTMU_WORKING = FALSE; // clear working flag, ok to read timer3 counter.
        // config CTMU for next reading
        CTMUCONHbits.CTMUEN = 1; // Enable the CTMU
        CTMUCONLbits.EDG1STAT = 0; // Set Edge status bits to zero
        CTMUCONLbits.EDG2STAT = 0;
        CTMUCONHbits.IDISSEN = 1; // drain charge on the circuit
        TMR0H = timer.bt[HIGH]; // Write high byte to Timer0
        TMR0L = timer.bt[LOW]; // Write low byte to Timer0
    }

    if (PIE1bits.SSP1IE && PIR1bits.SSPIF) { // SPI port #1 receiver
        PIR1bits.SSPIF = LOW;
        spi_stat.int_count++;
        spi_stat.data_in = SSP1BUF;
        PIE1bits.SSP1IE = LOW; // disable so we don't send again
        SSP1BUF = ADRESL;
    }

    if (PIR1bits.TMR1IF) {
        PIR1bits.TMR1IF = 0; // clear TMR2 int flag
        timer.lt = PDELAY;
        TMR1H = timer.bt[HIGH]; // Write high byte to Timer1
        TMR1L = timer.bt[LOW]; // Write low byte to Timer1
        spi_stat.time_tick++;
    }
}

Eight samples per channel are collected as a background process, averaged and examined for noise and stability for a zero baseline and signal trip points. If all signal criteria are go for a good sensor detection a call is made to the flasher function that's just a simple 32 bit circular shift resister with a bit pattern in this test version of software.

Rs-232 debug output via light link @38400bps. Channel, zero set-point and data raw adc trigger values.


C:
int16_t ctmu_touch(uint8_t channel, uint8_t diff_val)
{
    int16_t ctmu_change;
    uint8_t i;

    if (CTMU_ADC_UPDATED) {
        finger[channel].moving_val = finger[channel].avg_val;
        finger[channel].avg_val = 0;
        for (i = 0; i < 8; i++) {
            finger[channel].avg_val += adc_buffer[channel][i]&0x03ff;
        }
        finger[channel].avg_val = finger[channel].avg_val >> (uint16_t) 3;
        finger[channel].moving_avg = (finger[channel].moving_val + finger[channel].avg_val) >> (uint16_t) 1;

        if (!diff_val) {
            return finger[channel].avg_val;
        }
        ctmu_change = finger[channel].zero_ref - finger[channel].moving_avg; // read diff
        return ctmu_change;
    } else {
        return 0;
    }
}

/*
* compute the gesture zero
*/
uint16_t touch_base_calc(uint8_t channel)
{
    uint8_t i;

    touch_channel(channel);
    CTMU_ADC_UPDATED = FALSE;
    while (!CTMU_ADC_UPDATED) ClrWdt(); // wait for touch update cycle
    finger[channel].avg_val = 0;
    finger[channel].zero_noise = 0;
    finger[channel].zero_max = adc_buffer[channel][0]&0x03ff;
    finger[channel].zero_min = adc_buffer[channel][0]&0x03ff;
    for (i = 0; i < 8; i++) {
        finger[channel].avg_val += adc_buffer[channel][i]&0x03ff;
        if (adc_buffer[channel][i]&0x03ff > finger[channel].zero_max) // look at the noise spreads
            finger[channel].zero_max = adc_buffer[channel][i]&0x03ff;
        if (adc_buffer[channel][i]&0x03ff < finger[channel].zero_min)
            finger[channel].zero_min = adc_buffer[channel][i]&0x03ff;
    }
    finger[channel].avg_val = finger[channel].avg_val >> (uint16_t) 3;
    finger[channel].zero_ref = finger[channel].avg_val;
    if ((finger[channel].zero_max - finger[channel].zero_min) > ZERO_NOISE)
        finger[channel].zero_noise = 1;
    return finger[channel].zero_ref;
}

void touch_channel(uint8_t channel)
{
    CTMU_ADC_UPDATED = FALSE;
    while (!CTMU_ADC_UPDATED); // wait for touch update cycle
    ctmu_button = channel;
    CTMU_ADC_UPDATED = FALSE;
    while (!CTMU_ADC_UPDATED); // wait for touch update cycle
}

int16_t ctmu_setup(uint8_t current, uint8_t channel)
{
    //CTMUCONH/1 - CTMU Control registers
    CTMUCONH = 0x00; //make sure CTMU is disabled
    CTMUCONL = 0x90;
    //CTMU continues to run when emulator is stopped,CTMU continues
    //to run in idle mode,Time Generation mode disabled, Edges are blocked
    //No edge sequence order, Analog current source not grounded, trigger
    //output disabled, Edge2 polarity = positive level, Edge2 source =
    //source 0, Edge1 polarity = positive level, Edge1 source = source 0,
    //CTMUICON - CTMU Current Control Register
    CTMUICON = 0x01; //.55uA, Nominal - No Adjustment default

    switch (current) {
    case 2:
        CTMUICON = 0x02; //5.5uA, Nominal - No Adjustment
        charge_time[channel] = TIMERCHARGE_BASE_X10; // faster
        break;
    case 11:
        charge_time[channel] = TIMERCHARGE_BASE_1;
        break;
    case 12:
        charge_time[channel] = TIMERCHARGE_BASE_2;
        break;
    case 13:
        charge_time[channel] = TIMERCHARGE_BASE_3;
        break;
    case 14:
        charge_time[channel] = TIMERCHARGE_BASE_4;
        break;
    default:
        charge_time[channel] = TIMERCHARGE_BASE_3; // slower
        break;
    }

    // timer3 register used for atomic data transfer
    T3CONbits.TMR3ON = 0; // Timer is off
    T3CONbits.T3RD16 = 1; // enable 16 bit reads/writes
    TMR3H = 0;
    TMR3L = 0;
    return 0;
}

void ctmu_zero_set(void)
{
    uint8_t i, max_count;

    for (i = 0; i < 4; i++) {
        max_count = 0;
        do {
            touch_base_calc(i);
            if (finger[i].zero_noise)
                if (++max_count > 64)
                    break;
        } while (finger[i].zero_noise);
    }
}

int16_t finger_diff(int16_t finger1, int16_t finger2)
{
    return abs(finger1 - finger2);
}

/* bit rotations for 32 bit led motion control */
uint32_t rotl32(uint32_t value, unsigned int count)
{
    const unsigned int mask = (CHAR_BIT * sizeof(value) - 1);
    count &= mask;
    return(value << count) | (value >> ((-count) & mask)); // unary minus warning cheated with -nw=2059
}

uint32_t rotr32(uint32_t value, unsigned int count)
{
    const unsigned int mask = (CHAR_BIT * sizeof(value) - 1);
    count &= mask;
    return(value >> count) | (value << ((-count) & mask));
}

void led_motion(uint8_t mode)
{
    FLED0 = mode;
}

int16_t finger_trigger(uint8_t channel_count)
{
    static uint32_t roller = ROLL_PATTERN0;
    /* check finger trigger conditions */
    if (((finger[0].moving_diff > TRIP) && (finger[1].moving_diff > TRIP)) && (finger_diff(finger[0].moving_diff, finger[1].moving_diff) < TRIP_DIFF)) {
        led_motion(roller & 0x1);
        sprintf(mesg, " %u:%d:%d:%d:%d %d:%d diff %d: rotr %lu\r\n", channel_count, finger[channel_count].zero_ref, (int16_t) finger[channel_count].avg_val,
            finger[channel_count].moving_avg, finger[channel_count].moving_val,
            finger[0].moving_diff, finger[1].moving_diff, finger_diff(finger[0].moving_diff, finger[1].moving_diff), roller);
        puts1USART(mesg);
        roller = rotr32(roller, 1);
    } else {
        led_motion(1);
    }
    return 0;
}
The result is a device that senses when the finger is between two sensor wires/plates not just touching one or the other. This gives better touch control positioning.

C:
    while (1) { // just loop

        /* update error stats */
        if (SSP2CON1bits.WCOL || SSP2CON1bits.SSPOV) { // check for overruns/collisions
            SSP2CON1bits.WCOL = SSP2CON1bits.SSPOV = 0;
            spi_stat.adc_error_count = spi_stat.adc_count - spi_stat.adc_error_count;
            spi_stat.last_int_count = spi_stat.int_count;
        }

        _asm clrwdt _endasm // reset the WDT timer

        /* update detector data */
        CTMU_ADC_UPDATED = FALSE;
        while (!CTMU_ADC_UPDATED); // wait for complete channel touch update cycle

        if (ctmu_button == SCAN_MAX_CHAN) DLED0 = !DLED0;
        /* clean up some detector noise */
        if (finger[channel_count].avg_val > finger[channel_count].zero_ref) {
            finger[channel_count].zero_ref = finger[channel_count].avg_val;
        }

        /* check for finger trips */
        finger[0].moving_diff = ctmu_touch(0, 1);
        finger[1].moving_diff = ctmu_touch(1, 1);

        /* check finger trigger conditions */
        finger_trigger(channel_count);

        /* reset the finger zeros on a schedule */
        if (++channel_count > SCAN_MAX_CHAN) {
            channel_count = 0;
            if (!recal_count++)
                ctmu_zero_set();
        }
        ctmu_button = channel_count;
    }
The example software is in the zip file. updated
 

Attachments

Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,262
The circuit is in the box with one hot spot. I just need to add a few more remote lights and have the artist kid decorate the box heavy Metal style.

 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,262
It works perfectly but there was change in costume to Miss Pauling instead so it won't be used by her so I rigged some lights for my T-shirt with the controller in my pocket for effects. The little kids at the show seemed to like it.



The Stan Lee signing line was pretty long but it was fun.:D
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,262
There is one little timing trick (that takes much longer to explain than to write) for the SPI ISR driven output that needs an MSO scope to see clearly with profile/debug traces.

Data Lines:
1 timer0 ctmu charge/discharge cycle
2 spi clock
3 mosi data out (0x02 0x48)
4 ISR start to stop timer pulse
5 ADC routine in ISR start to stop pulse.

Analog line:
2 CTMU charging voltage.

The ADC returns 16 bit data (with a 10 bit conversion result) so you need to send two spi 8 bit words of data for a remote host to receive the complete result. If you try to send the second word before the first one completes this will result in a buffer overrun error on the unbuffered PIC. The little trick is to use the spi received word interrupt for timing as we always receive something when we send. The first byte (0x02) will help to time and send the second byte(0x48). This works better if we never expect to receive remote data from a SPI slave.

In the image above you can see the first #4 pulse of the ISR as the timer starts the ADC conversion as the charge time completes.
When the conversion completes it sends an ADC interrupt so we have the #4 ISR then the #5 ADC routine pulses with bytes 1&2 sent via the SPI module.

1659837985816.png

On this image we see the SPI timing detail (4.08us) between the start of byte 1 and the start of byte 2. We send the second byte before the ISR ends but it still works either way.
C:
if (PIR1bits.ADIF) { // check ADC irq
DLED3 = HIGH; // line#5 on the image
PIR1bits.ADIF = 0; // clear ADC int flag
spi_stat.adc_count++;
PIR1bits.SSPIF = LOW; // clear SPI flags
PIE1bits.SSP1IE = HIGH; // enable to send second byte
SSP1BUF = ADRESH | ((ctmu_button << 4)&0xf3);
... COMPLETE CTMU PROCESSING
}

if (PIE1bits.SSP1IE && PIR1bits.SSPIF) { // SPI port #1 receiver
PIR1bits.SSPIF = LOW;
spi_stat.int_count++;
spi_stat.data_in = SSP1BUF;
PIE1bits.SSP1IE = LOW; // disable so we don't send again
SSP1BUF = ADRESL;
}
In the adc routine we first clear the receive 'IF' flag, enable the 'IE' spi receive interrupt and finally send the high byte by loading the SSP1BUF. In the ISR there is enough of a time delay while processing the CTMU codes for the byte clock out, a received (dummy) byte to clock in and set the 'IF' flag before the program counter starts to execute the "// SPI port #1 receiver" check enable/flag code. Inside the SPI receive ISR routine we read the dummy data from SSPIBUF, disable the receive interrupt so we don't process the next received byte flag and then load the low byte of the ADC register to SSP1BUF. If the timing of the first byte is too short (~ <3.0us at the current spi clock speed) for the first byte to finish before executing the receiver enable/flag code the ISR will exit and then be quickly called again with the SSP1IF flag set to execute the receiver routine for the second byte transmit code that was missed during the first pass.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,262
It works perfectly but there was change in costume to Miss Pauling instead so it won't be used by her so I rigged some lights for my T-shirt with the controller in my pocket for effects. The little kids at the show seemed to like it.


The Stan Lee signing line was pretty long but it was fun.:D
https://www.hollywoodreporter.com/news/stan-lee-marvel-comics-legend-721450
Stan Lee, the legendary writer, editor and publisher of Marvel Comics whose fantabulous but flawed creations made him a real-life superhero to comic book lovers everywhere, has died. He was 95.
R.I.P.
 
Top