Thursday, May 10, 2012

A simple mod to assist the visually impaired

Appliance remote controls usually have more than half a dozen keys. And generally they follow the 80/20 rule: 80% of the keys are used 20% of the time. For the blind, it's a nightmare scanning the keys with their fingers and inadvertently pressing the wrong ones, particularly buttons they're unfamiliar with because they don't need to use them.

For a particular remote control that my dad uses, only 2 keys out of the total of 9 are relevant to him. To prevent possible errors in operation, I opened up the plastic case and inserted a sheet of paper between the rubber keypad and the printed circuit board. The paper presents a barrier that's electrically nonconductive and prevents contact between the electrically conductive pads of the keys and their corresponding contacts on the pcb. I then cut out the two areas for the buttons that he will be using.

Thus, when either of the two keys are pressed, no paper blocks the buttons from making contact with the circuit board, allowing normal operation. On the other hand, pressing any of the other keys, however hard you may do so, will not cause the remote control to send any infrared signal to the appliance because the paper prevents electrical contact.

If in the future other keys will need to be "unblocked" it's a simple matter of cutting out the relevant portions of the paper insert.

Wednesday, May 9, 2012

Princeton Tec EOS headlamp repair

I took two of the three AAA batteries from the EOS yesterday to use in an appliance remote control I had just brought back from the dead. I bought replacement batteries today and when I was about to install them I was utterly horrified to see the lone AAA inside shrouded in white greenish ectoplasm. I quickly removed and threw it away. Cleaned the battery case with tissue and used isopropanol soaked Q-tips to remove the gunk from the nooks. Installed fresh batteries. The headlamp immediately lit up as I put the last battery in. That shouldn't happen and was the first omen that not everything is well. Then when I switched off the lamp it magically switched back on again at full brightness a couple of seconds later. Oh no! It's possessed! And it kept turning back on whenever I switched it. And so exorcism was called for.

To my my initial consternation I discovered that the battery case was held down not by what I thought were torx screws but by heat stakes. I actually tried to carve the heads of the stakes off using the torx screwdriver. No dice. Had to go to the drill press and drill them out, which turned out to be painless--for me that is.

I could see that the chemical spill had reached the circuit board, particularly the ground (negative power supply) solder joint. I proceeded to clean the board with cotton and alcohol. After I was satisfied I installed the batteries. Uh oh. The lamp turned on automatically again. And after switching it off, it switched itself back on.

I suspected that the battery goo must have shorted out some of the vias and/or solder joints on the other side of the board. And I now had no choice--other than resigning myself to removing the batteries every time I'm done with the lamp--but to uncouple the board from the battery case. That meant desoldering the two battery terminals and gouging the four heat stakes locking the board to the case.

I actually thought there were only two heat stakes--yes, my eyesight is really that poor. And that's the reason I had a hard time prying the board off the case. Good thing I didn't inadvertently snap the board or break any tracks.

Turning the board over I immediately noticed that part of the solder mask on the ground trace had been eaten away. Alkaline battery chemical is really nasty stuff! I lost no time and quickly cleaned the board with alcohol.

To test the board I used the 3.3V output of my ATX power supply. Homemade cables with banana plugs on one end and alligator clips on the other conveniently routed power to the board without having to solder any wires. Moment of truth. I switched on the power supply. The lamp didn't turn on! Good sign. I then turned the lamp on and then off. 2 seconds ... 3 sec .. 4 ... 5 ... The headlamp's no longer possessed! You should've heard me singing and doing a jig.

With anxiety level and blood pressure back to normal it was then that I started taking pictures. So all the photos below are after the clean up. I now kind of regret I don't have shots of the ectoplasm-covered battery while it was still in the battery case. Would've been a great pièce de résistance for this horror story.



That's an Opulent Rebel Star 1-watt white LED on its own star pcb. Green board contains all the drive circuitry. Round black cylinder on the left is an inductor. Black square glob on the right is the microcontroller.










White rectangular thing on the top right is the momentary contact switch.


Solder mask partially dissolved by the horrible stuff that leaked out of the alkaline AAA battery. The gold flashing surely prevented the copper from being attacked. The offending battery chemical spill bridged the cluster of three vias and the ground throughhole solder pad.


Another angle of the same. All that glitters is gold.


Who's Azoteq?

The LED lens


The holes snapped into four studs on the battery case


The heat stakes are on the four crosses. The LED lens sits on the four studs close to the center.









Sunday, May 6, 2012

Current draw of the pcb versions of the beverage sensor

Just finished assembling a second unit of the liquid level indicator. I should give dad half a dozen--he's too concerned about breaking and overusing the gadget. Found a balloon whisk made of stainless steel wires. Used that for the probes. Now corrosion won't ever be a problem (hopefully).

All along I've been measuring the currents of a breadboarded circuit. It was only prudent to check to see how the pcb versions are doing. Both units have been loaded with the latest firmware version. Here are the values I got for the two units. 

Standby mode Current (µA) Battery voltage (V)
Max Min Ave
1st unit (April 28) 2.05 0.48 0.74 3.099
2nd unit (May 6) 2.29 0.56 0.80 3.207

The earlier unit didn't use a new battery. Its initial voltage was around 3.14 when I installed it in the circuit. The latest unit does use a new one. With a higher voltage than the 3.0V of the breadboarded version the higher currents are surprising. The latest unit, however, seems to be drawing a lot more current. But it seems to be within the specs of the MCU. Then again there could be some leakage current along the board.

I failed to mention in the last post that I also measured the DAC and voltage comparator currents in the breadboarded version. I modified the firmware so that the comparator was kept enabled throughout. I then measured the standby current. Thereafter, I changed the firmware back to its original form and kept the DAC enabled throughout and measured the standby current. I obtained a DAC current of approximately 19µA and a voltage comparator current of around 4.5µA. Enabling these modules only during probe reads does indeed help minimize power consumption.

Further improvements to the beverage sensor

I can't help myself. I'm driven to endlessly tweak and minimize the power consumption of the liquid level indicator. My biggest headache at the moment is the piezo transducer which is sucking as much as 4mA. Not good for the battery at all. As per the Maxell CR2032 datasheet the plots in the discharge capacity vs discharge current graph don't go beyond 3.5mA which probably implies the battery isn't designed to output that much current. Moreover, at room temperature and above, the discharge capacity takes a nosedive after current exceeds 2mA.

To address these issues and remain within the limits of the battery, I've decreased the pulse duration to the transducer by reducing the duty cycle from 50% to 24%. As it was, achieving the 50% duty cycle the firmware used timer2 interrupt to toggle pin RA2 output. To implement a DC other than 50% I've opted to enable the PWM module. RA2 is also the CCP1 pin (one of two alternate pins) so this isn't a problem.

I tested various duty cycle values and recorded the associated current. Bear in mind that for this test I modified the firmware slightly so that the buzzer sounds continuously--there are no pauses wherein the buzzer is turned off--when probes are immersed. Here are the values I obtained.

Duty Cycle (%) Average Current (mA)
50 3.99
24 2.154
12 1.162
5 0.545

I then posted the numbers on a spreadsheet and had it perform a simple linear regression analysis. Turns out the correlation is >99%. Coefficient a = 7.60 and constant b = 0.23. So the equation--valid for DC = 5% to 50% and series capacitance = 1uF--is

I = aDC + b = 7.6DC + 0.23

where I = current, DC = duty cycle. That b is nonzero befuddled me for a moment since when DC = 0, current should be zero. Then I realized b is the overheard current--this is current flowing through the various microcontroller modules including the MFINTOSC, comparator, DAC, weak pull-ups, and WDT.

Unfortunately I don't have a sound level meter to measure the change in audio volume. Volume definitely is reduced as DC is lowered but I don't know the exact relationship. Nevertheless, halving DC to 24% does not greatly diminish the sound level and is still very much audible. In the firmware below I use 24% as the duty cycle. I chose this value because the maximum current that the transducer draws at this DC is around 2mA which in the CR2032 datasheet translates to a discharge capacity of around 200mAh. We can reduce DC further of course but transducer volume suffers. So the DC value is a compromise between battery life and buzzer volume.

On a different note (ha! pun unintended or is it?), I've been able to further reduce the current during standby mode by taking down 4 statements which correspond to 4 assembly instructions. According to my tests the internal weak pull-ups don't consume any current unless the input pins have a path to ground--meaning, in the case of our circuit, liquid is detected or the pin is shorted to ground. So what I did was to keep the global and individual weak pull-up register bits enabled during standby mode. But once liquid is detected the individual weak pull-ups are enabled only during probe reads. As you'll see in the firmware, there are now two functions for probe reads: ReadProbesV1() and ReadProbesV2(). There's loads of unused Flash memory (only around 10% is used) so having two nearly identical functions albeit with a net result of lower power consumption makes a lot of sense. Measurements show that maximum current has dropped from ~2.54 to ~2.05µA (this is the current draw when the MCU is awake) and average current from ~0.75µA to ~0.69µA. The latter is an 8% reduction. That isn't too bad I think.

Just to make sure setting unused pins as digital output is really better than configuring them as inputs, I compared the currents for the various configurations.

Configuration Current (µA)
Max Min Ave
All unused pins are digital inputs 12.57 8.81 9.33
All unused pins are analog inputs 8.01 4.95 5.31
All unused pins are digital outputs (with RAx pulled to ground) 2.54 0.36 0.74

Without a shadow of a doubt, digital output pins win hands down.


Below is the latest version of the firmware. Besides the changes mentioned above, there are other minor ones which don't significantly affect power consumption.

/*

Liquid Level Indicator version 2
May 2012

processor = PIC12LF1840
compiler = mikroC v5.6.0

configuration word:
  INTOSC with I/O on clk pin
  enabled: power up timer, WDT via SWDTEN, MCLR, stack over/underflow
  all else disabled

CONFIG1   :$8007 : 0x09CC
CONFIG2   :$8008 : 0x1613

To minimize power consumption BOR should be disabled and MCLR enabled (so that RA3 is not left floating)

*/

#define  int1                bit
#define  int8                unsigned char
#define  int16               unsigned int
#define  int32               unsigned long

#define  on                  1
#define  off                 0

#define  input               1         // for TRISx
#define  output              0         // for TRISx

#define  analog              1         // for ANSELx
#define  digital             0         // for ANSELx

#define  hi                  1         // switch level high
#define  lo                  0         // switch level low

#define  buzz                LATA.f2

#define  an_plo              ANSELA.f1
#define  an_phi              ANSELA.f4
#define  tris_plo            TRISA.f1
#define  tris_phi            TRISA.f4
#define  wpu_plo             WPUA.f1
#define  wpu_phi             WPUA.f4

#define  ch_plo              0         // comparator input channel for low probe
#define  ch_phi              1         // comparator input channel for high probe

#define  t1h_fill            256-30    // TMR1H initial value, for audible indicator when low probe immersed or low and high probe immersed
#define  t1h_beep1           256-31    // TMR1H initial value, for first beep during probe error
#define  t1h_pause1          256-19    // TMR1H initial value, for first pause during probe error
#define  t1h_beep2           256-31    // TMR1H initial value, for second beep during probe error
#define  t1h_pause2          256-122   // TMR1H initial value, for second pause during probe error

#define  osc500khz           0b111000  // 500kHz MFINTOSC, for use with OSCCON
#define  osc31khz            0b0       // 31kHz LFINTOSC, for use with OSCCON

#define  wdt8ms              0b111     // value for WDTCON
#define  wdt16ms             0b1001    // value for WDTCON
#define  wdt32ms             0b1011    // value for WDTCON
#define  wdt128ms            0b1111    // value for WDTCON
#define  wdt256ms            0b10001   // value for WDTCON
#define  wdt512ms            0b10011   // value for WDTCON

int8 PLOval;                           // last five low probe readings
int8 PHIval;                           // last five high probe readings
bit PLOlevel;                          // voltage level of low probe when not bouncing (hi = 1, lo = 0)
bit PHIlevel;                          // voltage level of high probe when not bouncing (hi = 1, lo = 0)

bit plo;                               // comparator reading of low probe. 1 = liquid detected, 0 = no detection
bit phi;                               // comparator reading of high probe. 1 = liquid detected, 0 = no detection
bit buzzing;                           // buzzer status flag; 1 = buzzer is on, 0 = buzzer off

enum {_begin, _standby, _insta_plo, _immersed_plo, _insta_phi, _immersed_phi, _probe_error}
     STATE = _begin,                   // current state
     PREVSTATE = _begin;               // previous state

enum {_beep1, _pause1, _beep2, _pause2} STATEPERROR;    // for use with buzzer sound pattern when probe error detected


// ===========================================================================================
//       Functions
// ===========================================================================================

void IniReg()
{
  ANSELA = digital;
  TRISA = output;
  PORTA = 0;

  an_plo = analog;
  an_phi = analog;
  tris_plo = input;
  tris_phi = input;
  
  WPUA = 0;                  // disable individual pull ups
  OPTION_REG = 0b00000010;   // global pull ups enabled, timer0 prescale = 1:8
                             // given clock = 500khz, TMR0 increments from zero to 255, prescale = 1:8, timer0 tick = 256*8 / (500kHz/4) = 16.384ms

  wpu_plo = 1;
  wpu_phi = 1;
  
  DACCON0 = 0;               // DAC off, DAC is not output on DACOUT pin, Vdd as positive source
  DACCON1 = 0b11000;         // 0x18, Vref = 24/32 = 75% of Vdd

  CM1CON0 = 0b10;            // comparator off, comp output polarity not inverted, comp output internal only,
                             // comparator in low power low speed mode, hysteresis enabled
  CM1CON1 = 0b10000;         // comparator interrupts disabled, C1VP connected to DAC, C1VN connected to C1N0-

  // piezoelectric transducer empirically determined to be loudest at 2.15kHz
  // transducer is energized at a fixed frequency of 2.155kHz and fixed duty cycle of 24%
  // Current draw of the transducer has been determined to be directly proportional to duty cycle.
  // Audio volume of the transducer is proportional to duty cycle, but whether it is linear, logarithmic, or otherwise, is unknown
  // PWM Period = (PR2 + 1) x 4 x Tosc x (TMR2 Prescale Value), where Tosc = 1/Fosc
  // Duty Cycle = (CCPR1L:CCP1CON<5:4>) / [4 x (PR2 + 1)]
  // Given PR2 = 57, timer2 prescale = 1, CCPR1L:CCP1CON<5:4> = 56, Fosc = 500kHz,
  // PWM period = 464us (freq = 2.155kHz) and duty cycle = 24%
  PR2 = 57;
  T2CON = 0;                 // prescale = 1:1, postscale = 1:1, timer2 off
  CCPR1L = 0b1110;           // with CCPR1L = 0b1110 and CCP1CON<5:4> = 0b00, CCPR1L:CCP1CON<5:4> = 0b111000 = 56 decimal

  INTCON.GIE = 0;
  INTCON.PEIE = 1;

  // initialize probe readings to zero and level to zero
  PLOval = 0;
  PHIval = 0;
  PLOlevel = lo;
  PHIlevel = lo;
  
  buzzing = 0;

  OSCCON = osc31khz;         // default INTOSC frequency = 500khz. Change to 31kHz LFINTOSC after initialization.
                             // in all probability when coin battery is inserted and circuit powered up, the probes are not immersed in liquid and so the MCU will be put to sleep
} // void IniReg()


// turn on buzzer
void BuzzOn()
{
  TMR2 = 0;
  PIR1.TMR2IF = 0;
  CCP1CON = 0b1100;          // PWM mode with P1A active high, P1B disabled, CCP1CON<5:4> = 0b00
  T2CON.TMR2ON = 1;
  buzzing = 1;
}

// turn off buzzer
void BuzzOff()
{
  buzz = off;
  T2CON.TMR2ON = 0;
  CCP1CON = 0;               // CCP off
  buzzing = 0;
}

// audible indicator is armed and the specific sequence of sounds emitted depends on
// whether liquid has reached low probe or high probe or whether a probe error has occurred.
void EnableAudible()
{
  TMR1L = 0;
  PIR1.TMR1IF = 0;
  PIE1.TMR1IE = 1;
  INTCON.GIE = 1;
  BuzzOn();
}

void DisableAudible()
{
  BuzzOff();
  T1CON.TMR1ON = 0;
  PIE1.TMR1IE = 0;
  INTCON.GIE = 0;
}

// ReadProbes Version 1 -- for Standby mode only -- weak pull ups are always on since weak pull ups don't consume current unless probes are immersed.
// reducing the number of instructions (enabling and disabling weak pull ups) reduces time when MCU is awake and therefore reduces current draw
// voltage comparator reads the probes
// Weak pull ups are assumed to be already enabled. DAC and comparator are turned on before reading and then turned off afterwards to minimize power consumption
void ReadProbesV1()
{
  DACCON0.DACEN = 1;         // turn on DAC
  CM1CON0.C1ON = 1;          // turn on comparator

  CM1CON1.C1NCH = ch_plo;
  if (CMOUT)
    plo = 1;
  else
    plo = 0;

  CM1CON1.C1NCH = ch_phi;
  if (CMOUT)
    phi = 1;
  else
    phi = 0;

  CM1CON0.C1ON = 0;          // turn off comparator
  DACCON0.DACEN = 0;         // turn off DAC
} // void ReadProbesV1()


// ReadProbes Version 2 -- for all other modes except Standby. 
// Individual weak pull ups are enabled and disabled to minimize current draw since the probes are immersed and weak pull ups will draw current
// voltage comparator reads the probes
// DAC, weak pull ups, and comparator are turned on before reading and then turned off afterwards to minimize power consumption
void ReadProbesV2()
{
  DACCON0.DACEN = 1;         // turn on DAC
  CM1CON0.C1ON = 1;          // turn on comparator

  wpu_plo = 1;               // enable low probe weak pull up
  CM1CON1.C1NCH = ch_plo;
  if (CMOUT)
    plo = 1;
  else
    plo = 0;
  wpu_plo = 0;               // disable low probe weak pull up

  wpu_phi = 1;               // enable high probe weak pull up
  CM1CON1.C1NCH = ch_phi;
  if (CMOUT)
    phi = 1;
  else
    phi = 0;
  wpu_phi = 0;               // disable high probe weak pull up

  CM1CON0.C1ON = 0;          // turn off comparator
  DACCON0.DACEN = 0;         // turn off DAC
} // void ReadProbesV2()


void Debounce()
{
  // shift all bits to the left
  // if probe reading is high then PXXval bit 0 = 1
  PLOval <<= 1;
  if (plo)
    ++PLOval;
  PLOval.f5 = 0;             // only bits 0 to 4 (5 least significant bits) are used so bits 5 to 7 must be cleared
                             // need only clear bit 5 because this zero will eventually be left shifted into bits 6 and 7

  PHIval <<= 1;
  if (phi)
    ++PHIval;
  PHIval.f5 = 0;             // only bits 0 to 4 (5 least significant bits) are used so bits 5 to 7 must be cleared
                             // need only clear bit 5 because this zero will eventually be left shifted into bits 6 and 7

  if (!PLOlevel)
  {
    if (PLOval == 0b11111)
      PLOlevel = hi;
  }
  else if (!PLOval)
    PLOlevel = lo;

  if (!PHIlevel)
  {
    if (PHIval == 0b11111)
     PHIlevel = hi;
  }
  else if (!PHIval)
    PHIlevel = lo;
} // void DebounceSwitch()


void Standby()
{
  if (PREVSTATE != _standby)
  {
    STATE = _standby;
    OSCCON = osc31khz;     // 31kHz LFINTOSC
    DisableAudible();
    WDTCON = wdt512ms;
    wpu_plo = 1;
    wpu_phi = 1;
  }
  asm sleep
}


void InstaPLO()
{
  if (PREVSTATE != _insta_plo)
  {
    STATE = _insta_plo;
    OSCCON = osc31khz;   // 31kHz LFINTOSC
    DisableAudible();
  }
  asm clrwdt             // this clrwdt is absolutely necessary or there will be a wdt timeout every time plo is high
  WDTCON = wdt8ms;       // since plo is high (liquid has been detected during this pass) shorten sleep and check plo more often than during standby mode
  asm sleep
  WDTCON = wdt128ms;     // return wdt period to at least 32ms-- one loop through void main() at clock = 31khz LFINTOSC takes around 10 to 14ms
}


void InstaPHI()
{
  if (PREVSTATE != _insta_phi)
  {
    STATE = _insta_phi;
    OSCCON = osc31khz;   // 31kHz LFINTOSC
    DisableAudible();
  }
  asm clrwdt             // this clrwdt is absolutely necessary or there will be a wdt timeout every time phi is high
  WDTCON = wdt8ms;       // since phi is high (liquid has been detected during this pass) shorten sleep and check phi more often than during standby mode
  asm sleep
  WDTCON = wdt128ms;     // return wdt period to at least 32ms-- instructions in void main() during 31khz LFINTOSC take around 10 to 14ms
}


void ImmersedPLO()
{
  if (PREVSTATE != _immersed_plo)
  {
    OSCCON = osc500khz;     // 500kHz MFINTOSC
    STATE = _immersed_plo;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_fill;
    T1CON = 0b110001;       // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:8, timer1 oscillator off, timer1 on
    EnableAudible();
  }
}


void ImmersedPHI()
{
  if (PREVSTATE != _immersed_phi)
  {
    OSCCON = osc500khz;    // 500kHz MFINTOSC
    STATE = _immersed_phi;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_fill;
    T1CON = 0b10001;       // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:2, timer1 oscillator off, timer1 on
    EnableAudible();
  }
}


void ProbeError()
{
  if (PREVSTATE != _probe_error)
  {
    OSCCON = osc500khz;  // 500kHz MFINTOSC
    STATE = _probe_error;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_beep1;
    T1CON = 0b100001;    // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:4, timer1 oscillator off, timer1 on
    EnableAudible();
    STATEPERROR = _beep1;
  }
}


void Status()
{
  if (!PHIlevel)
  {
    if (!PLOlevel)
    {
      if (plo)               // current pass through comparator has detected liquid at low probe but there is no confirmation of submersion yet (PLOlevel is low), and hi probe not submerged
        InstaPLO();
      else if (phi)
        InstaPHI();
      else
        Standby();
    }
    else
      ImmersedPLO();
  }
  else // if (PHIlevel)
  {
    if (PLOlevel)
      ImmersedPHI();
    else
      ProbeError();
  }
  PREVSTATE = STATE;
}


void interrupt()
{
  if (PIE1.TMR1IE && PIR1.TMR1IF)
  {
    if (STATE == _probe_error)
    {
      if (++STATEPERROR > _pause2)
        STATEPERROR = _beep1;

      switch (STATEPERROR)
      {
        default:

        case _beep1:
          BuzzOn();
          TMR1H = t1h_beep1;
          break;

        case _pause1:
          BuzzOff();
          TMR1H = t1h_pause1;
          break;

        case _beep2:
          BuzzOn();
          TMR1H = t1h_beep2;
          break;

        case _pause2:
          BuzzOff();
          TMR1H = t1h_pause2;
          break;
      } // switch
    } // if (STATE == _probe_error)
    else // if (STATE == _immersed_plo || STATE == _immersed_phi)
    {
      TMR1H = t1h_fill;
      if (buzzing)
        BuzzOff();
      else
        BuzzOn();
    }
    PIR1.TMR1IF = 0;
  } // if (PIE1.TMR1IE && PIR1.TMR1IF)
} // void interrupt()


void main()
{
  IniReg();

  while(1)
  {
    if (STATE == _standby)             // during standby there is no need to run the debounce routine. Just record the instantaneous value of the probes
    {
      ReadProbesV1();
      PLOval.f0 = plo;
      PHIval.f0 = phi;
      Status();
    }
    else
    {
      if (OSCCON == osc500khz)         // this conditional is equivalent to "if (STATE == _immersed_plo || STATE == _immersed_phi || STATE == _probe_error)" since these states run at 500kHz
      {
        while (!INTCON.TMR0IF) ;       // do nothing until one timer0 tick has elapsed
        INTCON.TMR0IF = 0;
        asm clrwdt
      }
      ReadProbesV2();
      Debounce();
      Status();
    }
  }
}

Thursday, May 3, 2012

Measuring the current draw of the liquid level indicator

I breadboarded the liquid level indicator circuit to measure its current draw when operating in its different modes. I powered it using the PICkit2 set to 3.0V. I ran out of 1µF capacitors so C1 = 1.5µF. To simulate immersion in a water-based liquid I used 1K resistors.

VDD was measured using a Fluke 8842A. All current measurements were taken using a Fluke 87V set to high resolution, with MIN-MAX enabled, and with selector knob set to µA when measuring standby mode current and to mA range for all others. Reading was nulled (REL button pressed) before measurements were taken. The 87V probes were connected on the high side in series with the PICkit 2 VDD. The last significant digits of the average values when the transducer was buzzing kept jumping around--that's the reason for the approximation sign.  

Current draw @VDD = 3.01V, ambient temp. = 31°C
Mode/Condition Min Max Ave units
Standby mode 12.65 15.55 13.08 µA
only low probe immersed 0.137 3.827 ~2.01 mA
low and high probes immersed 0.328 3.654 ~2.00 mA
only high probe immersed 0.139 3.855 ~1.28 mA


Since WDT is always on and since it runs off the 31kHz LFINTOSC, both of these modules are always enabled in all modes even during sleep. Therefore the minimum current consumption of the circuit is the sum of these two. According to the datasheet typical current for the LFINTOSC is 4.0µA with a maximum of 22µA. WDT typical current is around 0.5µA. [See very important current measurements for these two and discussion below].

As I pored over the datasheet I noticed the brownout reset consumes a hefty amount of current--6.9µA typical. It also occurred to me that since I had disabled the MCLR (in the configuration word) this means pin RA3 is configured as a digital input, but is floating--it has no pull-up or pull-down resistor--which can only mean that it's drawing more current than it should. So I checked how the current consumption would change by first enabling MCLR and then disabling BOR. The following are the results. Values in the first row is just a control since the measurements below were made several hours after I had performed the ones above.

Current draw @VDD = 3.01V, ambient temp. = 30°C
Mode/Condition Min Max Ave units
Standby mode, MCLR disabled, BOR enabled 11.73 14.27 12.12 µA
Standby mode, MCLR enabled, BOR enabled 7.47 9.66 7.93 µA
Standby mode, MCLR enabled, BOR disabled 0.23 2.57 0.75 µA

Clearly, the reduction in power consumption is just incredible. From the averages we can infer that a floating RA3 pin wastes around 4µA, while BOR guzzles some 7µA--just as the datasheet says. With MCLR enabled and BOR disabled, I measured the currents for when low and high probes are immersed and they're practically the same as in the first table above.

With all these data we can now compute battery life. The Maxell CR2032 coin cell I'm using has a nominal capacity of 220mAh. Given the average current of  0.75µA when in standby mode the battery should be able to last for 220mAh / 0.75µA = 33.5 years! But of course I built the circuit to be used not displayed on the shelf. So let's say it's used thrice daily and its probes are immersed in liquid for 30 seconds each time. That's 1.5 minutes every 24 hours. In those 90 seconds the circuit is drawing an average current of 2.0mA. Therefore, everyday, the average current is:

(1.5 min / 60 min x 2000µA + 24 hrs x 0.75µA) / 24.025 hrs = 2.8uA

If you look at the graphs in the Maxell datasheet you'll see that the discharge capacity declines as the discharge current increases. Let's be conservative and take the 2.0mA as our discharge current. According to the graph discharge capacity will now only be around 200mAh. Thus the discharge duration of the liquid level indicator given our hypothetical conditions is:

200mAh / 2.8µA = 8 years.

Not bad. Even derating that by 50% still gives a very good battery lifespan.

----

With MCLR enabled and BOR disabled I proceeded to comment out void main() and used the following instead:

void main()
{
  IniReg();
  WDTCON = wdt512ms;
  while(1)
  {
    asm sleep
  }
}

With the voltage comparator, DAC, and weak pull-ups out of the picture average current dropped to 0.48µA.

Using the same main() I then disabled WDT in the configuration word. Average current fell to a mere 0.02µA (pretty much just as Microchip boasts). 20nA is practically the threshhold of the Fluke 87V's measurement capability!


Still with MCLR enabled and BOR disabled I tried the following to determine the amount of current the LFINTOSC uses while the MCU is continually awake.

void main()
{
  IniReg();
  WDTCON = wdt512ms;
  while(1) 
    asm clrwdt;
}

Average current was 4.11µA with WDT enabled. This agrees well with the datasheet spec for LFINTOSC current. Disabling WDT brought the average current marginally down to 4.03µA.

From the above measurements I gather that the LFINTOSC current of 4µA in the datasheet refers to when the MCU is awake. However, during sleep and if WDT is running this 4µA no longer applies. As we saw above it was a mere 0.48µA--the WDT current as per datasheet spec.

Sunday, April 29, 2012

Liquid level indicator for the visually impaired

My dad has had glaucoma for about a decade now and in the last 7 or so years has been nearly completely blind. He's been able to cope to a large extent, including making coffee (instant) on his own. Lately, however, he's been having difficulty judging whether his cup is nearly full. On several occasions he's overfilled his cup with piping hot water. Fortunately he hasn't scalded himself (yet). At over 80 years old and given his disability, we can expect things to get worse.

So to at least aid him in making his drinks I decided to build him a liquid level indicator (LLI) similar to the following.



I could've bought him any one of the various commercially available units out there, but hey this is such a simple circuit and I couldn't resist the challenge of designing a circuit for which a single coin battery would last for months. But not being the creative sort I've patterned the look of my gizmo after those on the market.

The principle behind detecting water or water-based liquids using metal probes is pretty straightforward. Since ordinary water is partially conductive we can set up a voltage divider using the liquid and a pull-up resistor of fixed value. (We can reverse the set up using a pull down resistor instead but as we'll see in a moment the former has an advantage given the microcontroller I'm using). A voltage comparator can then be used to detect the change in voltage when the liquid bridges the fixed resistor to ground.


Low power specification

Since this is a battery-operated unit power consumption is a big deal. I'm using a Microchip PIC and in order to have the least consumption possible I had to use an LF version. I opted for an 8-pin PIC12LF1840 (the 12LF1822 would do as well and is slightly cheaper but I haven't used the 1840 and wanted to try it out and up until recently didn't have any known silicon issues; it does now--yeah, drats!). According to the datasheet its typical sleep current @3.0V is a mere 20nA with all peripherals shut down and 500nA with the watchdog timer on (the LLI circuit needs WDT to be on at all times). Compare that with the F version of the 1840 which draws 5600nA and 5900nA, respectively, at 3.0V.

Another way to minimize consumption is by judicious manipulation of oscillator frequency. It's a truism that the higher the clock frequency the higher the power consumption. So whenever possible this circuit uses the the low power 31kHz LFINTOSC (low frequency internal oscillator). The only reason I use the medium frequency MFINTOSC is because the piezoelectric transducer needs a constant 2kHz square wave to make it beep. The typical current consumption at 500kHz MFINTOSC is 124µA versus a mere 4.0µA using the LFINTOSC.

According to the datasheet the on-board voltage comparator in low power mode draws around 4.9µA. During standby--when no liquid is detected--the circuit could go to sleep with the comparator enabled. The MCU would then wake up when the comparator interrupt flag gets set--liquid has been detected. But given that the watchdog timer uses only about 0.5µA, it's more economical to disable the comparator, put the MCU to sleep, let a WDT timeout (>100ms) wake the MCU, power up the comparator, check the probes, and then go back to sleep if no liquid is detected.


Schematic

I initially used an external pull-up resistor for each for the high and low probes. It then occurred to me I might be able to use the weak pull-ups of the microcontroller. Tried it and it worked and so I was able to dispense with two discretes. Note that the datasheet says the weak pull-ups typically guzzle a massive 100µA. I'm not sure if this is true whether or not there is an external path to ground. Just to be sure, in the firmware, the pull-ups are enabled only when the probes are being checked.

The buzzer is not a Sonalert alarm. It's a simple piezoelectric transducer in a Sonalert-like black plastic housing. The transducer will not sound if it is simply connected to a power source. It needs a constant square wave to produce sound. I tested the transducer using a function generator and to my ear it was loudest at a frequency of around 2.15kHz. So this is the frequency of the square wave I feeding it in the LLI.
Connecting the transducer directly to the MCU pin does work but the sound level is very low. Increasing the value of C3 more than 2µF does not make the sound any much louder. 1µF seemed sufficient.

Firmware

I initially used the following structure to store the values of the instantaneous comparator readings, level, and edge detect.

struct
{
  val: 5;
  edge: 2;
  level: 1;
} PLO, PHI;

But the amount of compiled assembly instructions was scandalously high compared to having separate 8-bit and one-bit variables. Since every instruction adds to processing time and therefore time when MCU is awake instead of asleep using the least current, it simply was unthinkable to employ structures, however more elegant they may be from a programming standpoint. (It can be argued, rather successfully, that given my goal of maximum power efficiency, I should've written the firmware in pure assembly rather than C. But my skill in assembly is rusty and right now I'm not ready for a grueling punishment.)

The MCU regularly checks the status of the probes (time between checks depends on whether no liquid has been detected--standby mode--or if liquid has already been sensed) with the instantaneous one-bit readings stored in a 5-bit variable. The bits in this variable are left shifted each time the probes are read. Thus it stores the five most recent readings. A debounce routine then simply checks if the variable contains all ones or all zeroes. If it's all ones then level = 1, and if it's all zeroes level = 0. A rising edge is defined as the level transition from 0 to 1, and a falling edge the level transition from 1 to 0. Rising and falling edges are not used so the edge variables and statements have been commented out to reduce the size of the assembly and therefore reduce time when the MCU is awake when in standby mode. Again, slashing power consumption is good.

A probe error is defined as the state where the high probe detects liquid but the low probe doesn't. It may be that the probes have shorted out in his pocket for instance. When this happens the a characteristic audible pattern is sounded to alert the user. Shorting of the high and low probes does not require any alarms to go off.

The reference voltage for the comparator is derived from the on-board digital-to-analog converter (DAC). I initially set the DAC output voltage to 50% of VDD, but increased to 75% in the hopes that even a liquid with an unexpectedly high resistance will be detected. Thus, if the voltage detected by the comparator is less than 0.75VDD  this is taken to mean that liquid has been detected.

Global weak pull-up (found in the OPTION register) is always enabled. However, individual weak pull-ups for the two probes are enabled only just after the DAC and comparator are turned on. The pull-ups are turned off right after the particular probe has been read. The comparator and DAC are disabled right after reading both probes.

Timer 0 is used to set the interval between probe reads when the 500kHz MFINTOSC is being used. When the LFINTOSC is active timer0 is ignored.

Timer 1 is used to create the three different audible alert patterns. When liquid has been detected at the low probe the pattern is a continuous loop of 500ms beep --> 500ms silence. When liquid has also been detected at the high probe the pattern becomes a continuous loop of 120ms beep --> 120ms silence. When a probe error is sensed the alarm pattern is a continuous loop of 250ms beep --> 150ms silence --> 250ms beep --> 1000ms silence.

Timer 2 is dedicated to producing the 2.15kHz square wave for the piezoelectric transducer.

When in standby mode the watchdog timeout is 512ms, meaning the MCU wakes up every half second and checks the status of the probes then goes back to sleep for another half a second. Assuming the current mode is standby (ie., variables PXXlevel for both probes = 0) then once the instantaneous reading of either the low or high probe is high (meaning liquid has been sensed), the WDT timeout is changed to 8ms with the MCU going back to sleep and waking up 8ms later. If the instantaneous reading now becomes low (zero) then WDT timeout reverts back to 512ms, else WDT remains at 8ms. If five consecutive instantaneous readings are all high then PXXlevel goes high, the MCU remains awake, begins using the 500kHz clock, and the appropriate audible alert is sounded.

Once PLOlevel and PHIlevel both go low and instantaneous readings are zero, the MCU returns to standby mode and reverts to the 31kHz clock.

/*

Liquid Level Indicator
April 2012

processor = PIC12LF1840
compiler = mikroC v5.6.0

configuration word: 
  INTOSC with I/O on clk pin
  enabled: power up timer, brownout reset low trip point, WDT via SWDTEN, stack over/underflow
  all else disabled

CONFIG1   :$8007 : 0x0F8C
CONFIG2   :$8008 : 0x1613
*/

#define  int1                bit
#define  int8                unsigned char
#define  int16               unsigned int
#define  int32               unsigned long

#define  on                  1
#define  off                 0

#define  input               1         // for TRISx
#define  output              0         // for TRISx

#define  analog              1         // for ANSELx
#define  digital             0         // for ANSELx

#define  hi                  1         // switch level high
#define  lo                  0         // switch level low

#define  buzz                LATA.f2

#define  an_plo              ANSELA.f1
#define  an_phi              ANSELA.f4
#define  tris_plo            TRISA.f1
#define  tris_phi            TRISA.f4
#define  wpu_plo             WPUA.f1
#define  wpu_phi             WPUA.f4

#define  ch_plo              0         // comparator input channel
#define  ch_phi              1         // comparator input channel

#define  t1h_fill            256-30    // TMR1H initial value, for audible indicator when low probe submerged or low and high probe submerged
#define  t1h_beep1           256-31    // TMR1H initial value, for audible indicator when probe error
#define  t1h_pause1          256-19    // TMR1H initial value, for audible indicator when probe error
#define  t1h_beep2           256-31    // TMR1H initial value, for audible indicator when probe error
#define  t1h_pause2          256-122   // TMR1H initial value, for audible indicator when probe error

#define  osc500khz           0b111000  // 500kHz MFINTOSC, for use with OSCCON
#define  osc31khz            0b0       // 31kHz LFINTOSC, for use with OSCCON

#define  wdt8ms              0b111     // value for WDTCON
#define  wdt16ms             0b1001    // value for WDTCON
#define  wdt32ms             0b1011    // value for WDTCON
#define  wdt128ms            0b1111    // value for WDTCON
#define  wdt256ms            0b10001   // value for WDTCON
#define  wdt512ms            0b10011   // value for WDTCON

#define  rising              1         // rising edge detected.
#define  falling             2         // falling edge detected.
#define  none                0         // no edge.

int8 PLOval;                           // values of last five low probe readings
int8 PHIval;                           // values of last five high probe readings
//int8 PLOedge;                          // edge detected?, 0 = no edge detect, 1 = rising edge, 2 = falling edge; other values = Not Used / Undefined for now
//int8 PHIedge;
bit PLOlevel;                          // voltage level of probe when not bouncing (hi = 1, lo = 0)
bit PHIlevel;

bit plo;                               // comparator reading of low probe. 1 = liquid detected, 0 = no detection
bit phi;                               // comparator reading of high probe. 1 = liquid detected, 0 = no detection
bit buzzing;                           // buzzer status flag; 1 = buzzer is on, 0 = buzzer off

enum {_begin, _standby, _insta_plo, _immersed_plo, _insta_phi, _immersed_phi, _probe_error}
     STATE = _begin,         // current state
     PREVSTATE = _begin;     // previous state

enum {_beep1, _pause1, _beep2, _pause2} STATEPERROR;


// ===========================================================================================
//       Functions
// ===========================================================================================

void IniReg()
{
  ANSELA = digital;
  TRISA = output;
  PORTA = 0;

  an_plo = analog;
  an_phi = analog;
  tris_plo = input;
  tris_phi = input;

  PLOval = 0;
  PHIval = 0;
  PLOlevel = lo;
  PHIlevel = lo;

  DACCON0 = 0;               // DAC off, DAC is not output on DACOUT pin, Vdd as positive source
  //DACCON1 = 0b10000;         // 0x10, Vref = 16/32 = 50% of Vdd
  DACCON1 = 0b11000;         // 0x18, Vref = 24/32 = 75% of Vdd

  CM1CON0 = 0b10;            // comparator off, comp output polarity not inverted, comp output internal only,
                             // comparator in low power low speed mode, hysteresis enabled
  CM1CON1 = 0b10000;         // comparator interrupts disabled, C1VP connected to DAC, C1VN connected to C1N0-

  WPUA = 0;                  // disable individual pull ups
  OPTION_REG = 0b00000010;   // global pull ups enabled, timer0 prescale = 1:8
                             // given clock = 500khz, TMR0 increments from zero to 255, prescale = 1:8, timer0 tick = 256*8 / (500kHz/4) = 16.384ms

  // timer2 initialize
  // piezoelectric buzzer toggled on and off at 2.15kHz -- loudest sound is at this frequency (empirically determined)
  // with timer2 prescale = 1:1, postscale = 1:1, PR2 = 29, Fosc = 500kHz, timer2 tick = 29 /(500khz/4) = 232us
  // buzzer is on 232us and off 232us. Period therefore = 464us. Freq = 2.155khz
  PR2 = 29;
  T2CON = 0;                 // prescale = 1:1, postscale = 1:1, timer2 off
  
  buzzing = 0;

  INTCON.GIE = 0;
  INTCON.PEIE = 1;

  OSCCON = osc31khz;         // default INTOSC frequency = 500khz. Change to 31kHz LFINTOSC after initialization.
                             // in all probability when coin battery is inserted and circuit powered up, the probes are not immersed in liquid and so the MCU will be put to sleep
} // void IniReg()


// turn on buzzer
// piezoelectric buzzer needs constant square wave to produce sound
// timer2 provides this square wave with a frequency of 2.15kHz
void BuzzOn()
{
  buzz = on;
  buzzing = 1;
  TMR2 = 0;
  PIR1.TMR2IF = 0;
  PIE1.TMR2IE = 1;
  T2CON.TMR2ON = 1;
}

// turn off buzzer
void BuzzOff()
{
  buzz = off;
  buzzing = 0;
  T2CON.TMR2ON = 0;
  PIE1.TMR2IE = 0;
}

// audible indicator is armed and the specific sequence of sounds emitted depends on
// whether liquid has reached low probe or high probe or whether a probe error has occurred.
void EnableAudible()
{
  TMR1L = 0;
  PIR1.TMR1IF = 0;
  PIE1.TMR1IE = 1;
  INTCON.GIE = 1;
  BuzzOn();
}

void DisableAudible()
{
  BuzzOff();
  T1CON.TMR1ON = 0;
  PIE1.TMR1IE = 0;
  INTCON.GIE = 0;
}

// voltage comparator reads the probes
// DAC, weak pull ups, and comparator are turned on before reading and then turned off afterwards to minimize power consumption
void ReadProbes()
{
  // OPTION_REG.NOT_WPUEN = 0;
  DACCON0.DACEN = 1;         // turn on DAC
  CM1CON0.C1ON = 1;          // turn on comparator

  wpu_plo = 1;               // enable low probe weak pull up
  CM1CON1.C1NCH = ch_plo; // PIC12F1840
  if (CMOUT)
    plo = 1;
  else
    plo = 0;
  wpu_plo = 0;               // disable low probe weak pull up

  wpu_phi = 1;               // enable high probe weak pull up
  CM1CON1.C1NCH = ch_phi;  // PIC12F1840
  if (CMOUT)
    phi = 1;
  else
    phi = 0;
  wpu_phi = 0;               // disable high probe weak pull up

  CM1CON0.C1ON = 0;          // turn off comparator
  DACCON0.DACEN = 0;         // turn off DAC
}


void Debounce()
{
  // shift all bits to the left
  // if probe reading is high then PXXval bit 0 = 1
  PLOval <<= 1;
  if (plo)
    ++PLOval;
  PLOval.f5 = 0;             // only bits 0 to 4 (5 least significant bits) are used so bits 5 to 7 must be cleared
                             // need only clear bit 5 because this zero will eventually be left shifted into bits 6 and 7
  PHIval <<= 1;
  if (phi)
    ++PHIval;
  PHIval.f5 = 0;             // only bits 0 to 4 (5 least significant bits) are used so bits 5 to 7 must be cleared

  //PLOedge = none;
  //PHIedge = none;

  // if level is low and all bits of PXXval are now high then
  // a rising edge has been detected
  // switch is considered just released when rising edge is detected
  // switch level is now high
 if ((!PLOlevel) && (PLOval == 0b11111))
  {
    PLOlevel = hi;
    //PLOedge = rising;
  }

 if ((!PHIlevel) && (PHIval == 0b11111))
  {
    PHIlevel = hi;
    //PHIedge = rising;
  }

  // if level is high and all bits of PXXval are now low then
  // a falling edge has been detected
  // switch is considered just pressed when falling edge is detected
  // switch level is now low
  if ((PLOlevel) && (!PLOval))
  {
    PLOlevel = lo;
    //PLOedge = falling;
  }

  if ((PHIlevel) && (!PHIval))
  {
    PHIlevel = lo;
    //PHIedge = falling;
  }
} // void DebounceSwitch()


void Standby()
{
  if (PREVSTATE != _standby)
  {
    STATE = _standby;
    OSCCON = osc31khz;     // 31kHz LFINTOSC
    DisableAudible();
    WDTCON = wdt512ms;
  }
  asm sleep
}


void InstaPLO()
{
  if (PREVSTATE != _insta_plo)
  {
    STATE = _insta_plo;
    OSCCON = osc31khz;   // 31kHz LFINTOSC
    DisableAudible();
  }
  asm clrwdt             // this clrwdt is absolutely necessary or there will be a wdt timeout every time plo is high
  WDTCON = wdt8ms;       // since plo is high (liquid has been detected during this pass) shorten sleep and check plo more often than during standby mode
  asm sleep
  WDTCON = wdt128ms;     // return wdt period to at least 32ms-- one loop through void main() at clock = 31khz LFINTOSC takes around 10 to 14ms
}


void InstaPHI()
{
  if (PREVSTATE != _insta_phi)
  {
    STATE = _insta_phi;
    OSCCON = osc31khz;   // 31kHz LFINTOSC
    DisableAudible();
  }
  asm clrwdt             // this clrwdt is absolutely necessary or there will be a wdt timeout every time phi is high
  WDTCON = wdt8ms;       // since phi is high (liquid has been detected during this pass) shorten sleep and check phi more often than during standby mode
  asm sleep
  WDTCON = wdt128ms;     // return wdt period to at least 32ms-- instructions in void main() during 31khz LFINTOSC take around 10 to 14ms
}


void ImmersedPLO()
{
  if (PREVSTATE != _immersed_plo)
  {
    OSCCON = osc500khz;     // 500kHz MFINTOSC
    STATE = _immersed_plo;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_fill;
    T1CON = 0b110001;       // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:8, timer1 oscillator off, timer1 on
    EnableAudible();
  }
}


void ImmersedPHI()
{
  if (PREVSTATE != _immersed_phi)
  {
    OSCCON = osc500khz;    // 500kHz MFINTOSC
    STATE = _immersed_phi;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_fill;
    T1CON = 0b10001;       // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:2, timer1 oscillator off, timer1 on
    EnableAudible();
  }
}


void ProbeError()
{
  if (PREVSTATE != _probe_error)
  {
    OSCCON = osc500khz;  // 500kHz MFINTOSC
    STATE = _probe_error;
    INTCON.TMR0IF = 0;
    TMR0 = 0;
    WDTCON = wdt128ms;
    TMR1H = t1h_beep1;
    T1CON = 0b100001;    // Timer1 clock source is instruction clock (FOSC/4), prescale = 1:4, timer1 oscillator off, timer1 on
    EnableAudible();
    STATEPERROR = _beep1;
  }
}


void Status()
{
  if (!PHIlevel)
  {
    if (!PLOlevel)
    {
      if (plo)               // current pass through comparator has detected liquid at low probe but there is no confirmation of submersion yet (PLOlevel is low), and hi probe not submerged
        InstaPLO();
      else if (phi)
        InstaPHI();
      else
        Standby();
    }
    else
      ImmersedPLO();
  }
  else // if (PHIlevel)
  {
    if (PLOlevel)
      ImmersedPHI();
    else
      ProbeError();
  }
  PREVSTATE = STATE;
}


void interrupt()
{
  if (PIE1.TMR2IE && PIR1.TMR2IF)
  {
    if (buzz)
      buzz = off;
    else
      buzz = on;
    PIR1.TMR2IF = 0;
  } // if (PIE1.TMR2IE && PIR1.TMR2IF)

  else if (PIE1.TMR1IE && PIR1.TMR1IF)
  {
    if (STATE == _probe_error)
    {
      if (++STATEPERROR > _pause2)
        STATEPERROR = _beep1;

      switch (STATEPERROR)
      {
        default:

        case _beep1:
          BuzzOn();
          TMR1H = t1h_beep1;
          break;

        case _pause1:
          BuzzOff();
          TMR1H = t1h_pause1;
          break;

        case _beep2:
          BuzzOn();
          TMR1H = t1h_beep2;
          break;

        case _pause2:
          BuzzOff();
          TMR1H = t1h_pause2;
          break;
      } // switch
    } // if (STATE == _probe_error)
    else // if (STATE == _immersed_plo || STATE == _immersed_phi)
    {
      TMR1H = t1h_fill;
      if (buzzing)
        BuzzOff();
      else
        BuzzOn();
    }
    PIR1.TMR1IF = 0;
  } // if (PIE1.TMR1IE && PIR1.TMR1IF)
} // void interrupt()


void main()
{
  IniReg();

  while(1)
  {
    if (OSCCON != osc31khz)            // this conditional is equivalent to "if (STATE == _immersed_plo || STATE == _immersed_phi || STATE == _probe_error)" since these states run at 500kHz
    {
      while (!INTCON.TMR0IF) ;         // do nothing until one timer0 tick has elapsed
      INTCON.TMR0IF = 0;
    }
    asm clrwdt
    ReadProbes();
    if (STATE == _standby)             // during standby there is no need to run the debounce routine. Just record the instantaneous value of the probes
    {
      PLOval.f0 = plo;
      PHIval.f0 = phi;
    }
    else
      Debounce();
    Status();
  }
}

The build

Wanted to make the pcb as small as possible and so I used an SOIC package for the MCU. Unfortunately I don't keep surface mount caps and resistors (cost is prohibitive for me at the moment) and so I had to use throughholes. It seemed like a grievous sin not to place the MCU in the large unused space on the copper side of the board beneath the battery holder and so that's just what I did even if I had already drawn a previous layout that situates the MCU partially underneath the terminal block.

I scoured shops for a plastic box that would house the circuit board and found this near perfect cream-colored plastic box that's in fact a portable USB power supply that uses 2 x AA  batteries. I simply removed the small DC-to-DC converter pcb and the metal spring contacts for the batteries. Having found a suitable container I proceeded to finalize the PCB size and layout. The biggest component is the CR2032 battery holder, taking up half the real estate! I intentionally did not include a polarity reversal protection Schottky diode since that immediately slashes 0.3V off the power 3-volt supply. User will just have to be extra careful when changing batteries. Yeah, relying on user competence is a recipe for disaster. Hey, I'm talking about myself too you know.


Below is the freshly etched panel which I scored and snapped into two. You will notice the track on the right side of the board is right at the border. The track is a ground trace from the negative terminal of the battery holder to one of the pins of the terminal block as well as to one of the header pins. Had little choice since I didn't have enough board width. Of course I made sure I didn't commit the same booboo that caused a short between the power supply rails. I added the triangles just to keep more copper on the board and to complement the diagonal circuit tracks. It kinda looks good I think.


Even while laying out the board I knew the headers wouldn't have enough leeway on either side. The battery holder on one side and the terminal on the other would prevent the PICkit2/3 programmer from plugging in properly. So I bent the header pin by almost 30 degrees. Simple fix. I wanted to use a right angle header but the pins would end up extending beyond the board perimeter and the board wouldn't fit the plastic case. 


I had to sand down the top of the (green) terminal block because it was sticking out about a millimeter preventing the plastic cover from sliding in and locking. 



For the SOIC pads and those alone I used a Multicore "VOC-free No-clean" flux pen. The usual flux paste I use leaves a messy unsightly residue which is hard to clean off an SMD and could cause a high resistance bridging between the closely spaced pins--something that could contribute to unnecessary battery drain. Nevertheless, after all the soldering was completed, I used isopropanol and acetone to clean the solder joints of all the components including the MCU.  




The probes (metal wires) are just nickel-plated paper clips. For now these will have to do since I haven't found any stainless steel wires yet. I don't have a photo of it, but after screwing down the probes onto the terminal block, I encased the three in hot melt glue to give the set of probes some rigidity.


The liquid level indicator at work. Just hang it over the lip of the cup, mug or tumbler. No switches to flip, no buttons to push.





Even during the breadboard stage of design the liquid level indicator was showing signs of having some difficulty detecting water. Reason? I thought it might be the nickel plated probes. But ohmmeter readings show their resistance to be in the milliohm range. And more importantly because the LLI detected coffee, chocolate and juice the instant these liquids reached the tips of the probes, those metal wires can't be the problem.

Now I'm inclined to believe it may be due to the fact that Microchip explicitly recommends an input signal impedance of less than 10 kilohms when using the analog peripherals such as the voltage comparator. Coffee et al. of course have a relatively low resistance because of the solutes, and that translates to, I reckon, a liquid resistance of less than 3K. Plain potable water has a higher resistance which could be anywhere along the spectrum depending on how pure it is or how much minerals are dissolved. To make the battery last as long as possible and because of printed circuit board real estate constraints, I intentionally decided not to design in a unity gain buffer to provide the comparator a low impedance signal which I definitely would in a circuit that had little power consumption concerns. Will have to test a breadboarded version of the LLI and see how the inclusion of the buffer will affect water-detection performance.

If the voltage follower licks the problem then a future version 2.0 of the gizmo will include a dual op amp which gets powered up only when probe readings are being taken. Power consumption is still my major concern given that the circuit is using a single CR2032 3-volt lithium cell which I want to last for months and hopefully--FSM willing--years.


Dad has already used the gizmo and he seems to like it. Told him to inform me immediately if the unit starts acting up.

Monday, April 2, 2012

Equation for determining belt size of a three-pulley system

April 3 Erratum: Thanks to rickets007 I spotted several egregious errors! I had used pulley diameter in some parts when clearly I was intending to use radius. Utter carelessness on my part. I have already corrected the blunders. The equations below are now (hopefully!) free of mistakes.


This is off-topic, but I've been googling and thus far haven't found any page that has the equation for computing the belt circumference given pulley diameters and distance between three pulleys. So I'm posting mine. I've tested my equation against an online 3-pulley calculator and while they don't give exactly the same numbers for some input values, they're close enough (there might be rounding errors in the webpage or my spreadsheet). You will note that the calculator necessitates the user inputting three angles. My equation below has no such requirements.

When I'm looking for formulas online I frequently just want a plug and play equation without any of the mathematical esoterica and history. So for those who couldn't give a rat's bleep about the derivation here's the formula up front.




For my sake (so I don't have to do it all over again!) and for skeptics who want to make sure I didn't make any booboos here's the derivation.

In Fig.1 we have three circles representing three pulleys. Pulley A has its center marked as point A. Pulley B has its center marked as point B. Pulley C has its center marked as point C.


We are given the diameters of each of the three pulleys and the distance between their centers. We need to find the belt length.

Let
DA = diameter of pulley A
DB = diameter of pulley B
DC = diameter of pulley C

Let
a = distance between centers of pulleys B and C = segment BC
b = distance between centers of pulleys C and A = segment CA
c = distance between centers of pulleys A and B = segment AB

L = belt length = segments HJ + DE + FG + arc lengths FE + GH + DJ

Let
a' = segment HJ
b' = segment DE
c' = segment FG

Let
α = ∠CAB
β = ∠ABC
γ = ∠BCA

Let
α' = ∠FAE
β' = ∠GBH
γ' = ∠DCJ

Let
lα' = arc length of the belt looping around pulley A = arc FE
lβ' = arc length of the belt looping around pulley B = arc GH
lγ' = arc length of the belt looping around pulley C = arc DJ

Note: All angles and arcs are less than π radians (180 degrees).

Rewriting the equation for belt length we have:

L = a' + b' +c' + lα' + lβ' + lγ'


In order to find the arc lengths we need to first determine the angles α', β',  and γ'. To achieve this we shall, for each pulley, find the values of all the other angles in the pulley and then subtract them from 2π radians (360 degrees).

For triangle ABC we are given the values of all the sides (a, b, c). Therefore, we can determine all three angles of the triangle using cosine law:

α = arccos[(b2+c2-a2)/(2bc)]
β = arccos[(c2+a2-b2)/(2ca)]
γ = arccos[(a2+b2-c2)/(2ab)]


As we know if a line is tangent to a circle then a radius of the circle drawn to the point of contact of the tangent line with the circle will be perpendicular to the tangent line. Thus, ∠AFG, ∠BGF, ∠BHJ, ∠CJH, ∠CDE, ∠AED are all right angles.


In Fig.2 we have segment BK drawn parallel to FG. Since ∠AFG is a right angle, therefore, ∠AKB is also a right angle. It follows that triangle AKB is a right triangle. Because BK is parallel to FG and FK is parallel to GB, segment AK = difference in the radii of pulleys A and B = DA/2 - DB/2.

Likewise, we draw segment CL parallel to DE and obtain right triangle ALC. LE = CD and so AL = DA/2 - DC/2.

Our objective is to find the value of ∠KAB and ∠LAC. Because we know the values of the hypotenuse and the adjacent side of both right triangles we can use the cosine function for right triangles:

∠KAB = arccos[(DA/2-DB/2)/c]
∠LAC = arccos[(DA/2-DC/2)/b]

We now have all the angles to compute for α'.

α' = 2π - arccos[(DA/2-DB/2)/c] - arccos[(DA/2-DC/2)/b] - α

We use the same method above for pulleys B and C to obtain:

β' = 2π - arccos[(DB/2-DC/2)/a] - arccos[(DB/2-DA/2)/c] - β
γ' = 2π - arccos[(DC/2-DA/2)/b] - arccos[(DC/2-DB/2)/a] - γ

Note that the fact that (DC/2 - DA/2) is negative is not an error. In fact it provides the correct answer--an obtuse angle which is the ∠ACD in this case. We don't need to know which pulleys are bigger and which are smaller. The equations will always give us the correct values.

We now turn to finding the arc lengths. The arc length of the belt looping around a pulley is the angle subtended by the belt divided by the angle measure of an entire circle (2π) multiplied by the circumference of the pulley:

lα' = (α'/2π)(πDA) = α'DA/2
lβ' = (β'/2π)(πDB) = β'DB/2
lγ' = (γ'/2π)(πDC) = γ'DC/2

Let's return to triangle AKB. Notice that segment FG and KB are not only parallel but also equal in length (because quadrilateral BKFG is a rectangle). Above we let c' = segment FG. It follows that c' is also = segment KB. We can get the value of KB using the sine or tangent function, but to minimize trigonometric functions we will apply the Pythagorean theorem, with c (segment AB) as the hypotenuse:

c' = √[c2-(DA/2-DB/2)2]

The same applies to the other two segments of the belt.

a' = √[a2-(DB/2-DC/2)2]
b' = √[b2-(DC/2-DA/2)2]

And at last we have all the values needed to compute for belt length.