The Prototype – Disco Bike Light Part 5

The disco bike light on its custom circuit board

Last week the printed circuit board for the Disco Bike Light project arrived.  This allowed me to move from my breadboard prototype circuit to the actual “form-factor” design.  If you’re not sure what the Disco Bike Light project is you can look through some of my older blog posts (here’s my last post). The short story is that I wanted to design a flashy, fun, multicolor bike light using an existing bike light’s enclosure.

Even after 19 years of electronic engineering, I still get a little excited when I receive a circuit board.  It’s like nerd Christmas.  In our line of work there is almost always something wrong with the first PCB we design for a project.  Something doesn’t fit or there’s an error in a circuit, or we modeled an electronic component wrong.  So, of course I had an error in this circuit, but it wasn’t a deal killer.  I placed a capacitor on the wrong side of a resistor, which prevented me from using the debugger/programmer.  This error was no big deal, since the fix was simply taking the capacitor off the circuit board.  Here’s the circuit board before I soldered on the parts.

dbl_proto

Most electronic products are designed fro machine assembly.  The parts are getting pretty tiny these days.  This design would be best suited for machine assembly as well, but in quantities of 1 the only economical option is to assemble it by hand.  I used a microscope and some other techniques to get the tiny parts soldered on.  The hardest to solder are the little square QFM packages, which is how the accelerometer and microcontroller came packaged.  These parts had a ground pad underneath which make them even more problematic since you can’t get a soldering iron under the part.  I usually place a dab of solder on the pad, heat it with a heat gun, and when the solder flows I use a pair of tweezers to place the package on the circuit boards.   After it cools I can solder the rest of the pads.

Once assembled I was able to connect a USB mini cable between my PC and the Disco Bike Light to test my Lithium battery charging circuit.  I also added a debugger connection so I could connect a Microchip PICkit3 for programming and debugging.  I used one of our CS005 adapter kits to keep the footprint of the debugger connection tiny. In the image below you can see those connections.

dbl_debugger
Finally, after all of the assembly I had to make sure the PCB and mounted parts would actually fit into the Schwinn bike light enclosure I was re-purposing.  It all fit together nicely.
dbl_enclosure

At this point the design was somewhere between 50%-80% complete.  Here’s what I had completed…

1.  The PCB form factor and basic circuit connections were correct.
2.  I had implemented a USB to serial communication protocol, including firmware and software for adjusting the light’s settings
3.  The basic concepts of using an accelerometer to detect taps as an on/off switch was implemented in firmware and tested
4.  I also used the accelerometer to determine one of two operating modes, white light or disco mode, and wrote firmware to do this

So what have I not completed?

1.   Power reduction- My firmware was written for a microcontroller running at 32MHz.  It could be much slower, reducing the current draw for that IC.  I could also put the microcontroller to sleep when not reading the accelerometer’s in analog outputs.  The main goal would be to reduce the current the design uses when the light is turned off and the charger is not plugged in.  If this design were headed for the consumer marketplace this would be a priority.
2.  Digital filters– I used integer math, timers, and averaging routines among other gimmicks to convert accelerometer analog values to on/off and mode select indicators.  If I wanted to bullet-proof this design I would implement digital filters to process the analog signals.  This would entail floating point math and data acquisition of the analog signals to determine coefficients for the equations.  Since this isn’t a project I’m getting paid to complete I didn’t want to take the additional time to go this route.

Overview of how the firmware works:
The over-arching functionality of the microcontroller firmware is pretty straight-forward.  If the USB port is plugged in then the battery charger is powered and active and the serial interface is enabled.

The microcontroller polls the ADXL335 accelerometer analog outputs once every millisecond whether or not the USB port is powered.  Every 50mS it checks to see if the accelerometer has detected 3 taps (see last week’s post) or if the accelerometer has been rotated, indicating a mode change. Every 50mS * the flash rate the LED drive signals are updated.  The flash rate is programmable via the USB/serial interface using software created for this project.

dbl_main_code_block

The Measure_Analogs() function does a few things.  The Z axis of the accelerometer is measured every 1mS and averaged every 34mS.  I track the min and max over 34mS and sum the measurements.  After 34 measurements I subtract the min and the max and divide the result by 32.  The division is done with a right-rotate (>>5 in C) to minimize the time the routine takes to do its thing.  This provides me with a Z axis measurement that is less likely to be impacted by vibration/spurious noise.  The Z axis is used to determine the operating mode (either white LED or flashy disco LED).

Although I collect the Y axis analog value, I don’t use it in this code.

The X axis data is collected and the max, min, and maximum difference between the two is calculated.  These measurements are reset every 50mS in the tap detect function.

Anytime you see a variable with the prefix “NV.Bytes.x” or NV.RegArray[x]” these bytes may be stored in EEPROM and written/read with the software interface.

/******************************************************************************/
/* Measure_Analogs:
   Measures accelerometer voltages               */
/******************************************************************************/
void Measure_Analogs()
{
    char i = 0;
/* Get Z axis value.  The Z axis is used to determine the mode of
operation.  The analog value is averaged with 34 measurements taken.  The
min/max values are removed, and the remaining 32 measurements averaged.
 */
    ADCON0bits.CHS = 0x04;        // measures AN4 at pin RC0
    for(i=0;i<64;i++);            // wait some uS
    ADCON0bits.GO = 0x01;         // start measurement
    while(ADCON0bits.GO);

    NV.Bytes.Z_InLo = ADRESL;
    NV.Bytes.Z_InHi = ADRESH & 0x03;

    Zmeas = NV.Bytes.Z_InHi <<8;    // Copy Z axis to a variable
    Zmeas += NV.Bytes.Z_InLo;
    if (Zmeas < Zmin)               // keep track of min
       Zmin = Zmeas;
    if (Zmeas > Zmax)
       Zmax = Zmeas;                // keep track of max
    Zaccumulator += Zmeas;           // sum measurements

    Zcount++;
    if (Zcount >= 34)
    {
        Zaccumulator -= Zmax;       // remove max meas
        Zaccumulator -= Zmin;       // remove min meas
        Zaccumulator = Zaccumulator>>5; // divide by 32
        ZmeasAvg = (int)Zaccumulator;  // copy result to vari
        Zaccumulator = 0;
        Zcount = 0;
        Zmax = 0;
        Zmin = 1023;
    }

    // Get Y axis value
    ADCON0bits.CHS = 0x05;        // measures AN5 at pin RC1
    for(i=0;i<64;i++);            // wait some uS
    ADCON0bits.GO = 0x01;         // start measurement
    while(ADCON0bits.GO);         // everything else pretty much same as Z channel

    NV.Bytes.Y_InLo = ADRESL;
    NV.Bytes.Y_InHi = ADRESH & 0x03;

    // Get Xaxis value
     ADCON0bits.CHS = 0x06;        // measures AN6 at pin RC2
    for(i=0;i<64;i++);            // wait some uS
    ADCON0bits.GO = 0x01;         // start measurement
    while(ADCON0bits.GO);         

    Xmeas = ADRESH<<8;
    Xmeas = (Xmeas & 0xff00) + ADRESL;

    if (Xmeas < Xmin)            // keep track of min/max. calc. diff   
       Xmin = Xmeas;
    if (Xmeas > Xmax)
       Xmax = Xmeas;
    if ((Xmax - Xmin)> Xdiff)
        Xdiff = Xmax - Xmin;

    if (MODE.Bits.LOADDIFF == 1)  // sometimes we want to display
    {                             // the difference measurement in                      
        NV.Bytes.X_InHi = Xdiff>>8; //  the software.  The LOADDIFF
        NV.Bytes.X_InLo = Xdiff & 0xff; // bit does that
    }
    else
    {
        NV.Bytes.X_InLo = ADRESL;
        NV.Bytes.X_InHi = ADRESH & 0x03;
    }
}

TapDetect() was covered last week.  However, I made some changes to allow me to put the microcontroller to “sleep” (low power mode) if I get to that point in the design.  Namely, I moved the 50mS timer code to the main code loop.

This code will check for a tap (when Xdiff > TapDiff).  Every 1.5S the tap counter is reset and the number of taps recorded is set to 0.  This is to clear spurious taps that are detected.  If a tap is detected the code also sets a flag to skip the detection attempt 50mS down the road.  This limits the detection of a single tap as multiple taps, since the accelerometer output may be disturbed for tens of milliseconds when a tap occurs.  Finally, if 3 taps are detected they must occur between 200mS-400mS of each other.

/******************************************************************************/
/* TapDetect:
   Detects taps on the X axis channel and resets some of the analog registers  */
/******************************************************************************/
void TapDetect()
{
    // load tap threshold detection level from comm array into variable
    int TapDiff = 0;
    TapDiff = NV.Bytes.TapDiffHi<<8;
    TapDiff +=  NV.Bytes.TapDiffLo;

    TapCounter++;
    if (TapCounter > 30)    // if 30 counts (1.5S reset it all)
    {
        TapNumber = 0;
        TapCounter = 0;
    }
    if (INDICATOR.Bits.TAPDETECTDELAY == 0) // check to see if we already saw this tap
    {
        if (Xdiff > TapDiff)    // check for tap
        {
            INDICATOR.Bits.TAPDETECTDELAY = 1;  // set flag to make sure we don't look again for 50mS
            if (TapNumber == 0)                 // if first tap reset counters
                TapCounter = 0;
            TapNumber++;                        // increment tap counter
            TapTime[TapNumber] = TapCounter;    // store 50mS increments between taps
            if (TapNumber == 3)                 // do we have 3 taps?
                {
                    // make sure taps were between 200mS and 400mS apart
                    if (((TapTime[2] - TapTime[1]) >= 4) 
                        && ((TapTime[2] - TapTime[1]) <= 8) && 
                        ((TapTime[3] - TapTime[2]) >= 4) && 
                        ((TapTime[3] - TapTime[2]) <= 8))
                    // if taps met requirements toggle power bit

                        INDICATOR.Bits.POWERON = ~INDICATOR.Bits.POWERON;  
                    TapNumber = 0;                  // reset counters
                    TapCounter = 0;
                }
        }
    }
    else
       INDICATOR.Bits.TAPDETECTDELAY = 0;  // clear flag used to skip tests within 50mS of tap detected

    NV.Bytes.TapNum = TapNumber;  // load tap count into comm array

    // reset analog storage registers every 50mS
    Xmax = 0;
    Xmin = 1023;
    Xdiff = 0;
}

The disco bike light has two operating modes.  One is white LED mode, and the other is disco mode (flashing).  The module also needs to know if it is plugged into the USB charger and if it is fully charged.

To detect the mode of operation I check for a change in the Z axis reading over a 1.5S period.  I place the average Z axis measurement (Zmeas variable) into an array that is updated every 50mS.  The array is 32 elements deep.  Every 50mS I compare the last value in the array with the most recent.  If there has been a change in the average Z axis measurement that is greater than the ModeDifference variable then I toggle the operating mode.  The amount of difference required to change modes is programmable by writing values to the NV.Bytes.ModeDiffHi and ModeDiffLo registers via the software interface.

This routine also checks the charge state.  If RB6 is low the USB charger is plugged in, and if RB4 is low the Lithium battery is charged.  These pins are attached to the _PG (RB6)  and STAT2 (RB4) pins of the MCP73833, a Microchip part.

/******************************************************************************/
/* LEDDetermineMode:
   Determines LED drive mode and charger LED state   */
/******************************************************************************/
void LEDDetermineMode()
{
    char i = 0;
    for (i=0;i<LEDModeSize-1;i++)
        LEDMode[i+1]=LEDMode[i];
    LEDMode[0] = ZmeasAvg;

    int ModeDifference = 0;  // Get the change in Z axis that constitures mode switch
    ModeDifference = NV.Bytes.ModeDiffHi<<8;
    ModeDifference += NV.Bytes.ModeDiffLo;

    if ((LEDMode[LEDModeSize-1]-LEDMode[0] > ModeDifference)
        ||(LEDMode[0]-LEDMode[LEDModeSize-1] > ModeDifference))       // see if timer has elapsed
    {
        NV.Bytes.LEDMode += 1;        // increment mode reg
        NV.Bytes.LEDMode = NV.Bytes.LEDMode & 0x01; // clear upper 7 bits
        for(i=0;i<LEDModeSize;i++) // copy all current values to array to stop future bit toggles.
            LEDMode[i] = ZmeasAvg;
    }

    // check to see if USB is plugged in and charge not complete
    if ((PORTBbits.RB6==0)&&(PORTBbits.RB4==1))
        {
        INDICATOR.Bits.PLUGGED = 1;
        INDICATOR.Bits.CHARGED = 0;
        LED = 1;
        }
    // check to see if USB is plugged in and charge complete
    else if ((PORTBbits.RB6==0)&&(PORTBbits.RB4==0))
        {
        INDICATOR.Bits.PLUGGED = 1;
        INDICATOR.Bits.CHARGED = 1;
        LED = ~LED;
        }
    else
        {
        INDICATOR.Bits.PLUGGED = 0;
        INDICATOR.Bits.CHARGED = 0;
        LED = 0;
    }
}

Lastly, I need to drive the LEDs.  I’m using a high power RGB LED.  The circuit that I use can draw around 500mA when going full throttle.  While testing I found this was enough power to re-flow the solder holding the LED on the board.  I found this out because when I tapped the module the LED slid off the PCB.   Not good.  I’ve later determined that limiting the PWM signal that turns on each LED to about 50% keeps things cool enough for normal operation.

In this function we don’t turn on any LEDs if the POWERON flag is clear, or if for some reason the NV.Bytes.LEDMode variable is not a 0 or 1.  If power is on and the NV.Bytes.LEDMode register is 0 then we used the contents of the NV.Bytes.QxDrive varibles to drive the LED.  If the LEDMode is a 1 we cycle through 1 of 10 possible values stored in NV.Bytes.DBLArray0 through NV.Bytes.DBLArray9.  This cycling of values is what causes the different colors to show up on the flashlight.

/******************************************************************************/
/* LEDDrive:
   Drives LEDs              */
/******************************************************************************/
void DriveLEDs()
{
    char LEDPointer1 = 0;
    char LEDPointer2 = 0;
    char LEDPointer3 = 0;

    DiscoModeCounter++;  // increment counter
    if (DiscoModeCounter >= 10) // has counter been matched?
        DiscoModeCounter = 0;  // reset counter

    if (INDICATOR.Bits.POWERON == 1)  // see if power is on
    {
        if (NV.Bytes.LEDMode == 0)  // are we in mode 1, white light?
        {
            CCPR4L = NV.Bytes.Q1Drive;
            CCPR2L = NV.Bytes.Q2Drive;
            CCPR1L = NV.Bytes.Q3Drive;
        }
        else if (NV.Bytes.LEDMode == 1)  // are we in disco mode?
        {
            // Here I convert a counter to an indirect pointer to
            // the comm registers that store the PWM values for the
            // LED updates (NV.RegArray[23] through NV.RegArray[32]
            LEDPointer1 = DiscoModeCounter + 23;
            if (LEDPointer1 > 32)
                LEDPointer1 -=10;
            LEDPointer2 = DiscoModeCounter + 24;
            if (LEDPointer1 > 32)
                LEDPointer1 -=10;
            LEDPointer3 = DiscoModeCounter + 25;
            if (LEDPointer1 > 32)
                LEDPointer1 -=10;

            CCPR4L = NV.RegArray[LEDPointer1];
            CCPR2L = NV.RegArray[LEDPointer2];
            CCPR1L = NV.RegArray[LEDPointer3];
        }
        else // if neither mode then turn off LEDs
        {
            CCPR4L = 0;
            CCPR2L = 0;
            CCPR1L = 0;
        }
    }
    else  // if power off then shut off LEDs.
    {
                CCPR4L = 0;
                CCPR2L = 0;
                CCPR1L = 0;
    }

}

Most of the functionality in the disco bike light can be adjusted using the software interface I wrote for the project.  This includes changing settings like the tap detection threshold, adjusting the LED color output for both operating modes, and modifying the flash rate.  Having a test software interface is really useful for testing project functionality.  For consulting projects it allows a customer to experience/modify the project without having to understand the more advanced tools we use (like debuggers).  Below is a screen capture of the software I wrote for this design.

image

I plan on posting the design files, including source code for the software and firmware, for this project.  I’d like to use it for a week or two and see what adjustments need to be made before I do that.  Testing is one of the most important facets of an engineering project.  And even though this is just a design for fun, I still want to take it through the whole engineering process before I call it good..

Trackbacks

  1. […] 3-axis output signal  (I’m using the Analog Devices ADXL335, which I also used in the Disco Bike Light project).   This requires that the motor controller and accelerometer be located with the solar […]

Speak Your Mind

*