Blinky prop for Comic Con

Discussion in 'Embedded Systems and Microcontrollers' started by nsaspook, Sep 8, 2016.

  1. nsaspook

    Thread Starter AAC Fanatic!

    Aug 27, 2009
    2,906
    2,159
    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?
    [​IMG] [​IMG]
    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.
    [​IMG]

    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.

    Code (C):
    1.  
    2. void InterruptHandlerHigh(void)
    3. {
    4.     static union Timers timer;
    5.     static uint8_t i_adc = 0;
    6.  
    7.     if (INTCONbits.TMR0IF) { // check timer0 irq
    8.         if (!CTMUCONHbits.IDISSEN) { // charge cycle timer0 int, because not shorting the CTMU voltage.
    9.             DLED1 = HIGH;
    10.             CTMUCONLbits.EDG1STAT = 0; // Stop charging touch circuit
    11.             TIME_CHARGE = FALSE; // clear charging flag
    12.             CTMU_WORKING = TRUE; // set working flag, doing touch ADC conversion
    13.             // configure ADC for next reading
    14.             ADCON0bits.CHS = ctmu_button; // Select ADC channel
    15.             ADCON0bits.ADON = 1; // Turn on ADC
    16.             ADCON0bits.GO = 1; // and begin A/D conv, will set adc int flag when done.
    17.         } else { // discharge cycle timer0 int, because CTMU voltage is shorted
    18.             DLED1 = LOW;
    19.             ADCON0bits.CHS = ctmu_button; // Select ADC channel for charging
    20.             CTMUCONHbits.IDISSEN = 0; // end drain of touch circuit
    21.             TIME_CHARGE = TRUE; // set charging flag
    22.             CTMU_WORKING = TRUE; // set working flag, doing
    23.             timer.lt = charge_time[ctmu_button]; // set timer to charge rate time
    24.             TMR0H = timer.bt[HIGH]; // Write high byte to Timer0
    25.             TMR0L = timer.bt[LOW]; // Write low byte to Timer0
    26.             CTMUCONLbits.EDG1STAT = 1; // Begin charging the touch circuit
    27.         }
    28.         // clr  TMR0 int flag
    29.         INTCONbits.TMR0IF = 0; //clear interrupt flag
    30.     }
    31.  
    32.     if (PIR1bits.ADIF) { // check ADC irq
    33.         PIR1bits.ADIF = 0; // clear ADC int flag
    34.         spi_stat.adc_count++;
    35.         PIR1bits.SSPIF = LOW; // clear SPI flags
    36.         PIE1bits.SSP1IE = HIGH; // enable to send second byte
    37.         SSP1BUF = ADRESH | ((ctmu_button << 4)&0xf3);
    38.         adc_buffer[ctmu_button][i_adc] = ADRES;
    39.         timer.lt = TIMERDISCHARGE; // set timer to discharge rate
    40.         if (++i_adc >= ADC_READS) {
    41.             TMR3H = ADRESH | ((ctmu_button << 4)&0xf3); // copy high byte/channel data [4..7] bits
    42.             TMR3L = ADRESL; // copy low byte and write to timer counter
    43.             i_adc = 0; // reset adc buffer position
    44.             CTMU_ADC_UPDATED = TRUE; // New data is in timer3 counter, set to FALSE in main program flow
    45.             timer.lt = TIMERPROCESS; // set timer to data processing rate
    46.         }
    47.         CTMU_WORKING = FALSE; // clear working flag, ok to read timer3 counter.
    48.         // config CTMU for next reading
    49.         CTMUCONHbits.CTMUEN = 1; // Enable the CTMU
    50.         CTMUCONLbits.EDG1STAT = 0; // Set Edge status bits to zero
    51.         CTMUCONLbits.EDG2STAT = 0;
    52.         CTMUCONHbits.IDISSEN = 1; // drain charge on the circuit
    53.         TMR0H = timer.bt[HIGH]; // Write high byte to Timer0
    54.         TMR0L = timer.bt[LOW]; // Write low byte to Timer0
    55.     }
    56.  
    57.     if (PIE1bits.SSP1IE && PIR1bits.SSPIF) { // SPI port #1 receiver
    58.         PIR1bits.SSPIF = LOW;
    59.         spi_stat.int_count++;
    60.         spi_stat.data_in = SSP1BUF;
    61.         PIE1bits.SSP1IE = LOW; // disable so we don't send again
    62.         SSP1BUF = ADRESL;
    63.     }
    64.  
    65.     if (PIR1bits.TMR1IF) {
    66.         PIR1bits.TMR1IF = 0; // clear TMR2 int flag
    67.         timer.lt = PDELAY;
    68.         TMR1H = timer.bt[HIGH]; // Write high byte to Timer1
    69.         TMR1L = timer.bt[LOW]; // Write low byte to Timer1
    70.         spi_stat.time_tick++;
    71.     }
    72. }
    73.  
    [​IMG]
    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.
    [​IMG]

    Code (C):
    1.  
    2. int16_t ctmu_touch(uint8_t channel, uint8_t diff_val)
    3. {
    4.     int16_t ctmu_change;
    5.     uint8_t i;
    6.  
    7.     if (CTMU_ADC_UPDATED) {
    8.         finger[channel].moving_val = finger[channel].avg_val;
    9.         finger[channel].avg_val = 0;
    10.         for (i = 0; i < 8; i++) {
    11.             finger[channel].avg_val += adc_buffer[channel][i]&0x03ff;
    12.         }
    13.         finger[channel].avg_val = finger[channel].avg_val >> (uint16_t) 3;
    14.         finger[channel].moving_avg = (finger[channel].moving_val + finger[channel].avg_val) >> (uint16_t) 1;
    15.  
    16.         if (!diff_val) {
    17.             return finger[channel].avg_val;
    18.         }
    19.         ctmu_change = finger[channel].zero_ref - finger[channel].moving_avg; // read diff
    20.         return ctmu_change;
    21.     } else {
    22.         return 0;
    23.     }
    24. }
    25.  
    26. /*
    27. * compute the gesture zero
    28. */
    29. uint16_t touch_base_calc(uint8_t channel)
    30. {
    31.     uint8_t i;
    32.  
    33.     touch_channel(channel);
    34.     CTMU_ADC_UPDATED = FALSE;
    35.     while (!CTMU_ADC_UPDATED) ClrWdt(); // wait for touch update cycle
    36.     finger[channel].avg_val = 0;
    37.     finger[channel].zero_noise = 0;
    38.     finger[channel].zero_max = adc_buffer[channel][0]&0x03ff;
    39.     finger[channel].zero_min = adc_buffer[channel][0]&0x03ff;
    40.     for (i = 0; i < 8; i++) {
    41.         finger[channel].avg_val += adc_buffer[channel][i]&0x03ff;
    42.         if (adc_buffer[channel][i]&0x03ff > finger[channel].zero_max) // look at the noise spreads
    43.             finger[channel].zero_max = adc_buffer[channel][i]&0x03ff;
    44.         if (adc_buffer[channel][i]&0x03ff < finger[channel].zero_min)
    45.             finger[channel].zero_min = adc_buffer[channel][i]&0x03ff;
    46.     }
    47.     finger[channel].avg_val = finger[channel].avg_val >> (uint16_t) 3;
    48.     finger[channel].zero_ref = finger[channel].avg_val;
    49.     if ((finger[channel].zero_max - finger[channel].zero_min) > ZERO_NOISE)
    50.         finger[channel].zero_noise = 1;
    51.     return finger[channel].zero_ref;
    52. }
    53.  
    54. void touch_channel(uint8_t channel)
    55. {
    56.     CTMU_ADC_UPDATED = FALSE;
    57.     while (!CTMU_ADC_UPDATED); // wait for touch update cycle
    58.     ctmu_button = channel;
    59.     CTMU_ADC_UPDATED = FALSE;
    60.     while (!CTMU_ADC_UPDATED); // wait for touch update cycle
    61. }
    62.  
    63. int16_t ctmu_setup(uint8_t current, uint8_t channel)
    64. {
    65.     //CTMUCONH/1 - CTMU Control registers
    66.     CTMUCONH = 0x00; //make sure CTMU is disabled
    67.     CTMUCONL = 0x90;
    68.     //CTMU continues to run when emulator is stopped,CTMU continues
    69.     //to run in idle mode,Time Generation mode disabled, Edges are blocked
    70.     //No edge sequence order, Analog current source not grounded, trigger
    71.     //output disabled, Edge2 polarity = positive level, Edge2 source =
    72.     //source 0, Edge1 polarity = positive level, Edge1 source = source 0,
    73.     //CTMUICON - CTMU Current Control Register
    74.     CTMUICON = 0x01; //.55uA, Nominal - No Adjustment default
    75.  
    76.     switch (current) {
    77.     case 2:
    78.         CTMUICON = 0x02; //5.5uA, Nominal - No Adjustment
    79.         charge_time[channel] = TIMERCHARGE_BASE_X10; // faster
    80.         break;
    81.     case 11:
    82.         charge_time[channel] = TIMERCHARGE_BASE_1;
    83.         break;
    84.     case 12:
    85.         charge_time[channel] = TIMERCHARGE_BASE_2;
    86.         break;
    87.     case 13:
    88.         charge_time[channel] = TIMERCHARGE_BASE_3;
    89.         break;
    90.     case 14:
    91.         charge_time[channel] = TIMERCHARGE_BASE_4;
    92.         break;
    93.     default:
    94.         charge_time[channel] = TIMERCHARGE_BASE_3; // slower
    95.         break;
    96.     }
    97.  
    98.     // timer3 register used for atomic data transfer
    99.     T3CONbits.TMR3ON = 0; // Timer is off
    100.     T3CONbits.T3RD16 = 1; // enable 16 bit reads/writes
    101.     TMR3H = 0;
    102.     TMR3L = 0;
    103.     return 0;
    104. }
    105.  
    106. void ctmu_zero_set(void)
    107. {
    108.     uint8_t i, max_count;
    109.  
    110.     for (i = 0; i < 4; i++) {
    111.         max_count = 0;
    112.         do {
    113.             touch_base_calc(i);
    114.             if (finger[i].zero_noise)
    115.                 if (++max_count > 64)
    116.                     break;
    117.         } while (finger[i].zero_noise);
    118.     }
    119. }
    120.  
    121. int16_t finger_diff(int16_t finger1, int16_t finger2)
    122. {
    123.     return abs(finger1 - finger2);
    124. }
    125.  
    126. /* bit rotations for 32 bit led motion control */
    127. uint32_t rotl32(uint32_t value, unsigned int count)
    128. {
    129.     const unsigned int mask = (CHAR_BIT * sizeof(value) - 1);
    130.     count &= mask;
    131.     return(value << count) | (value >> ((-count) & mask)); // unary minus warning cheated with -nw=2059
    132. }
    133.  
    134. uint32_t rotr32(uint32_t value, unsigned int count)
    135. {
    136.     const unsigned int mask = (CHAR_BIT * sizeof(value) - 1);
    137.     count &= mask;
    138.     return(value >> count) | (value << ((-count) & mask));
    139. }
    140.  
    141. void led_motion(uint8_t mode)
    142. {
    143.     FLED0 = mode;
    144. }
    145.  
    146. int16_t finger_trigger(uint8_t channel_count)
    147. {
    148.     static uint32_t roller = ROLL_PATTERN0;
    149.     /* check finger trigger conditions */
    150.     if (((finger[0].moving_diff > TRIP) && (finger[1].moving_diff > TRIP)) && (finger_diff(finger[0].moving_diff, finger[1].moving_diff) < TRIP_DIFF)) {
    151.         led_motion(roller & 0x1);
    152.         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,
    153.             finger[channel_count].moving_avg, finger[channel_count].moving_val,
    154.             finger[0].moving_diff, finger[1].moving_diff, finger_diff(finger[0].moving_diff, finger[1].moving_diff), roller);
    155.         puts1USART(mesg);
    156.         roller = rotr32(roller, 1);
    157.     } else {
    158.         led_motion(1);
    159.     }
    160.     return 0;
    161. }
    162.  
    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.


    Code (C):
    1.  
    2.     while (1) { // just loop
    3.  
    4.         /* update error stats */
    5.         if (SSP2CON1bits.WCOL || SSP2CON1bits.SSPOV) { // check for overruns/collisions
    6.             SSP2CON1bits.WCOL = SSP2CON1bits.SSPOV = 0;
    7.             spi_stat.adc_error_count = spi_stat.adc_count - spi_stat.adc_error_count;
    8.             spi_stat.last_int_count = spi_stat.int_count;
    9.         }
    10.  
    11.         _asm clrwdt _endasm // reset the WDT timer
    12.  
    13.         /* update detector data */
    14.         CTMU_ADC_UPDATED = FALSE;
    15.         while (!CTMU_ADC_UPDATED); // wait for complete channel touch update cycle
    16.  
    17.         if (ctmu_button == SCAN_MAX_CHAN) DLED0 = !DLED0;
    18.         /* clean up some detector noise */
    19.         if (finger[channel_count].avg_val > finger[channel_count].zero_ref) {
    20.             finger[channel_count].zero_ref = finger[channel_count].avg_val;
    21.         }
    22.  
    23.         /* check for finger trips */
    24.         finger[0].moving_diff = ctmu_touch(0, 1);
    25.         finger[1].moving_diff = ctmu_touch(1, 1);
    26.  
    27.         /* check finger trigger conditions */
    28.         finger_trigger(channel_count);
    29.  
    30.         /* reset the finger zeros on a schedule */
    31.         if (++channel_count > SCAN_MAX_CHAN) {
    32.             channel_count = 0;
    33.             if (!recal_count++)
    34.                 ctmu_zero_set();
    35.         }
    36.         ctmu_button = channel_count;
    37.     }
    38.  
    The example software is in the zip file. updated
     
    Last edited: Sep 9, 2016
    MrSoftware and NorthGuy like this.
  2. nsaspook

    Thread Starter AAC Fanatic!

    Aug 27, 2009
    2,906
    2,159
  3. nsaspook

    Thread Starter AAC Fanatic!

    Aug 27, 2009
    2,906
    2,159
    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.
    [​IMG]
     
    Last edited: Sep 9, 2016
  4. nsaspook

    Thread Starter AAC Fanatic!

    Aug 27, 2009
    2,906
    2,159
    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.

    [​IMG]

    The Stan Lee signing line was pretty long but it was fun.:D
     
    JohnInTX likes this.
  5. nsaspook

    Thread Starter AAC Fanatic!

    Aug 27, 2009
    2,906
    2,159
    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.
    [​IMG]
    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.

    [​IMG]

    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.
    Code (C):
    1.  
    2. if (PIR1bits.ADIF) { // check ADC irq
    3. DLED3 = HIGH; // line#5 on the image
    4. PIR1bits.ADIF = 0; // clear ADC int flag
    5. spi_stat.adc_count++;
    6. PIR1bits.SSPIF = LOW; // clear SPI flags
    7. PIE1bits.SSP1IE = HIGH; // enable to send second byte
    8. SSP1BUF = ADRESH | ((ctmu_button << 4)&0xf3);
    9. ... COMPLETE CTMU PROCESSING
    10. }
    11.  
    12. if (PIE1bits.SSP1IE && PIR1bits.SSPIF) { // SPI port #1 receiver
    13. PIR1bits.SSPIF = LOW;
    14. spi_stat.int_count++;
    15. spi_stat.data_in = SSP1BUF;
    16. PIE1bits.SSP1IE = LOW; // disable so we don't send again
    17. SSP1BUF = ADRESL;
    18. }
    19.  
    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: Sep 11, 2016
Loading...