Friday, May 18, 2012

BevSense v4 -- Getting it right this time

Version 3 was supposed to be definitive. But it turned out to be a bit of a facepalm. Although it achieved the goal of power reduction and using the slowest clock available, the drawback--and it's a major one--is that the output of the piezo transducer was severely compromised. It's volume is more than just noticeably softer. And I found this out only after uploading v3 to the pcb (not the breadboard version I was using for firmware testing). You see, connecting the PICkit2 causes RA1 (ICSPCLK) pin to drop well below VDD causing the circuit to think that the low probe has detected liquid and so turns on the appropriate audible alert. The sound level I heard was with firmware version 2. Once I had uploaded v3 and before I could remove the PICkit, the circuit again sounded the same alert. But now the transducer output volume was markedly lower. In fact it was too low even as the PWM duty cycle is the same for both v2 and v3. At that moment my heart sank.

The reason, I believe, is the PWM frequency. With v2 it was ~2.1kHz. In v3 it's ~1.9kHz. As I had discussed before, the highest dB output of the transducer was determined to be around 2.15kHz. But because the PWM resolution is extremely coarse when using 31kHz, the closest frequency that could be obtained was 1.9kHz. (Decreasing PR2 by just one shifts the frequency to 2.58kHz).

Increasing the duty cycle would of course increase the volume, but that method is just out of the question since it eye-poppingly and unacceptably raises the current draw. So I decided it would be best to switch to the 500kHz clock whenever the transducer is on, and use the 31kHz clock at all other times. And so v4 was born.

Incidentally, I bench-tested another transducer--the same model--and to my ear its highest sound level output is at 2kHz. So I've taken the average of 2.15 and 2kHz and set the PWM to 2.083kHz in the firmware below.

As you can see in the program listing below OSCCON is configured to use the 500kHz MFINTOSC in the function BuzzOn() and to use the 31kHz LFINTOSC in BuzzOff(). In all STATEs other than _standby, timer 0 is used to set the cycle time--the time interval between probes readings. But remember the clock frequency is continually being switched back and forth (when liquid has been detected or when a probe error has occurred). In order to have a more or less equal cycle time when using either clock frequencies, I've pegged the value that TMR0 is initialized to as a constant and varied the timer0 prescale value. Since 500kHz / 31kHz = approx 16, the prescale value when in 500kHz mode must be 16x that of the value when using the low frequency clock. I've set TMR0 = 256 - 66 giving a theoretical cycle time of ~17ms. Number of stored probe readings has been reduced from 8 to 6. The 17ms cycle was chosen so that 17ms x 6 readings = ~100ms. Using the Saleae Logic, measured cycle time is ~18.6ms @31kHz and ~17.0ms @500kHz (see the screenshots below)

For timer1 I took the opposite tact. I kept the prescale value constant (for each STATE) and varied the initial value of TMR1H since TMR1H initial value is different for transducer on-time and off-time anyway. Up to v3 transducer on-time and off-time during liquid detection was equal. In v4 I've decreased the on-time slightly to reduce current draw just a tad.

The transducer on-times during probe error have been limited to a couple of 100ms pulses every 8.5 seconds.

/*

Liquid Level Indicator version 4
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  int8                unsigned char

#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  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   // weak pull for low probe
#define  wpu_phi             WPUA.f4   // weak pull for high probe

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

#define  t0_ini              256-66    // TMR0 initial value (clock = 31kHz / 500kHz)

#define  t1h_buzzon          256-196   // TMR1H initial value for audible indicator when low probe immersed or low and high probe immersed -- length of time buzzer on   (clock = 500kHz)
#define  t1h_buzzoff         256-15    // TMR1H initial value for audible indicator when low probe immersed or low and high probe immersed -- length of time buzzer off  (clock = 31kHz)

#define  t1h_beep1           256-49    // TMR1H initial value for first beep during probe error (clock = 500kHz)
#define  t1h_pause1          256-9     // TMR1H initial value for first pause during probe error (clock = 31kHz)
#define  t1h_beep2           256-49    // TMR1H initial value for second beep during probe error (clock = 500kHz)
#define  t1h_pause2          256-242   // TMR1H initial value for second pause during probe error (clock = 31kHz)

#define  osc500khz           0b111000  // 500kHz MFINTOSC, for use with OSCCON
#define  osc31khz            0b0       // 31kHz LFINTOSC, for use with OSCCON
#define  wdt512ms            0b10011   // WDT time out = 512ms, for use with WDTCON

int8 PLOval;                           // stores the last six low probe readings
int8 PHIval;                           // stores the last six high probe readings

enum {_bouncing, _standby, _plo_immersed, _plophi_immersed, _phi_immersed}
     STATE = _bouncing,                // current state
     PREVSTATE = _bouncing;            // 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 = 0b0;          // global pull ups enabled

  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-

  // initialize probe readings to zero (no liquid detection)
  PLOval = 0;
  PHIval = 0;

  // 500kHz MFINTOSC is used only when piezoelectric transducer is turned on
  // piezoelectric transducer empirically determined to be loudest at ~2 to ~2.15kHz
  // 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 = 59, timer2 prescale = 1, CCPR1L:CCP1CON<5:4> = 60, Fosc = 500kHz,
  // PWM period = 480us (freq = 2.083kHz) and duty cycle = 25%
  PR2 = 59;
  CCPR1L = 0b1111;           // with CCPR1L = 0b1111 and CCP1CON<5:4> = 0b00, CCPR1L:CCP1CON<5:4> = 0b111100 = 60 decimal
  CCP1CON = 0b1100;          // PWM mode with P1A active high, P1B disabled, CCP1CON<5:4> = 0b00
  
  PIE1.TMR1IE = 1;           // timer1 interrupt enabled
  INTCON.PEIE = 1;           // peripheral interrupt enabled. Global interrupt is by default disabled upon any reset. GIE is enabled only when timer1 is enabled (i.e., when buzzer needs to be sounded)
  WDTCON = wdt512ms;         // WDT time out = 512ms. This is the sleep time between probe reads when in Standby mode
                             // longer sleep times would be better from a power consumption perspective
                             // but would be too long when the user starts using the unit for its intended purpose -- to detect liquid and as soon as possible
} // void IniReg()


// turn on buzzer
void BuzzOn()
{
  OSCCON = osc500khz;        // 500kHz clock used only when piezo transducer is actually sounding
  TMR2 = 0;
  PIR1.TMR2IF = 0;
  T2CON.TMR2ON = 1;
  OPTION_REG = 0b100;        // global pull ups enabled, timer0 prescale = 1:32
}

// turn off buzzer
void BuzzOff()
{
  buzz = off;
  OSCCON = osc31khz;        // 31kHz clock used whenever piezo transducer not sounding
  T2CON.TMR2ON = 0;
  OPTION_REG = 0b0;         // global pull ups enabled, timer0 prescale = 1:2
}

// audible indicator is armed, with the particular sequence of sounds emitted depending on
// whether liquid has reached low probe, low and high probe, or just the high probe
void EnableAudible()
{
  TMR1L = 0;
  PIR1.TMR1IF = 0;
  INTCON.GIE = 1;
  BuzzOn();
}

// both low and high probes not immersed
// PLOval = PHIval = 0
void Standby()
{
  STATE = _standby;
  wpu_plo = 1;
  wpu_phi = 1;
  
  // =========================
  // disable audible indicator
  BuzzOff();
  T1CON.TMR1ON = 0;
  INTCON.GIE = 0;
  // =========================
}

// low probe immersed -- cup nearly full
void PLOimmersed()
{
  if (PREVSTATE != _plo_immersed)
  {
    STATE = _plo_immersed;
    TMR1H = t1h_buzzon;
    T1CON = 0b1100001;       // Timer1 clock source is system clock (FOSC), prescale = 1:4, timer1 oscillator off, timer1 on
                             // given 500kHz clock and TMR1H ini value = 256-196, timer1 tick is ~400ms
                             // given 31kHz clock and TMR1H ini value = 256-15, timer1 tick is ~500ms
    EnableAudible();
  }
}

// low and high probes both immersed -- cup is full
void PLOPHIimmersed()
{
  if (PREVSTATE != _plophi_immersed)
  {
    STATE = _plophi_immersed;
    TMR1H = t1h_buzzon;
    T1CON = 0b1000001;       // Timer1 clock source is system clock (FOSC), prescale = 1:1, timer1 oscillator off, timer1 on
                             // given 500kHz clock and TMR1H ini value = 256-196, timer1 tick is ~100ms
                             // given 31kHz clock and TMR1H ini value = 256-15, timer1 tick is ~125ms
    EnableAudible();
  }
}

// probe error -- only high probe is immersed
void PHIimmersed()
{
  if (PREVSTATE != _phi_immersed)
  {
    STATE = _phi_immersed;
    TMR1H = t1h_beep1;
    T1CON = 0b1100001;       // Timer1 clock source is system clock (FOSC), prescale = 1:4, timer1 oscillator off, timer1 on
    STATEPERROR = _beep1;
    EnableAudible();
  }
}


void DetermineState()
{
  asm clrwdt

  // ===============================
  // Read Probes -- for modes other than standby
  // Individual weak pull ups are enabled prior to reading and disabled after reading to minimize current draw since the probes are immersed and weak pull ups will draw current
  // DAC, weak pull ups, and comparator are turned on before reading and then turned off afterwards to minimize power consumption
  // current reading stored in PXXval.f0
  // ===============================
  CM1CON0.C1ON = 1;          // turn on comparator (current draw ~4.5uA)
  DACCON0.DACEN = 1;         // turn on DAC (current draw ~19uA)

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

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

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

  // ===============================
  // Determine state/mode
  // ===============================
  if (!PHIval)
  {
    if (!PLOval)
      Standby();
    else if (PLOval == 0b111111)
      PLOimmersed();
  }
  else if (PHIval == 0b111111)
  {
    if (PLOval == 0b111111)
      PLOPHIimmersed();
    else if (!PLOval)
      PHIimmersed();
  }

  // all bits (readings) of PXXval can now be shifted left
  // clear two MSb since we are retaining only 6 readings
  PLOval <<= 1;
  PHIval <<= 1;
  PLOval.f6 = 0;
  PHIval.f6 = 0;

  PREVSTATE = STATE;
} // void DetermineState()


void interrupt()
{
  if (PIR1.TMR1IF)
  {
    if (STATE == _phi_immersed)
    {
      if (++STATEPERROR > _pause2)
        STATEPERROR = _beep1;

      if (!STATEPERROR)
      {
        BuzzOn();
        TMR1H = t1h_beep1;             // given 500kHz, system clock as clock source, prescale = 1:4, TMR1H ini value = 49, timer1 tick is ~100ms
      }
      else if (STATEPERROR == _pause1)
      {
        BuzzOff();
        TMR1H = t1h_pause1;            // given 31kHz, system clock as clock source, prescale = 1:4, TMR1H ini value = 9, timer1 tick is ~300ms
      }
      else if (STATEPERROR == _beep2)
      {
        BuzzOn();
        TMR1H = t1h_beep2;             // given 500kHz, system clock as clock source, prescale = 1:4, TMR1H ini value = 49, timer1 tick is ~100ms
      }
      else
      {
        BuzzOff();
        TMR1H = t1h_pause2;            // given 31kHz, system clock as clock source, prescale = 1:4, TMR1H ini value = 242, timer1 tick is ~8000ms
      }
    } // if (STATE == _phi_immersed)
    else // if (STATE == _plo_immersed || STATE == _plophi_immersed)
    {
      if (T2CON.TMR2ON)
      {
        BuzzOff();
        TMR1H = t1h_buzzoff;
      }
      else
      {
        BuzzOn();
        TMR1H = t1h_buzzon;
      }
    }
    PIR1.TMR1IF = 0;
  } // if (PIR1.TMR1IF)
} // void interrupt()


void main()
{
  IniReg();

  while(1)
  {
    if (STATE != _standby)
    {
      while (!INTCON.TMR0IF) ;
      TMR0 = t0_ini;
      INTCON.TMR0IF = 0;
      DetermineState();      // just one function call to reduce execution time
    }
    else
    {
      asm sleep              // WDT is automatically cleared right before sleep and after waking up
                             // MCU wakes up upon WDT timeout

      // Reduction of MCU awake time during Standby mode reduces current draw
      // Ways by which instructions executed when MCU is awake are reduced:
      //     1. Probe reading is stored in PXXval only if reading is high
      //     2. Weak pull ups are kept on since they don't consume current unless probes are immersed.
      //     3. Standby mode is when PLOval = PHIval = PLOlevel = PHIlevel = 0 therefore we need only monitor for instantaneous readings that are high
      //     4. Reduction/elimination of function calls and using inline code instead
      // DAC and voltage comparator are turned on before reading and then turned off afterwards to minimize power consumption
      DACCON0.DACEN = 1;     // turn on DAC
      CM1CON0.C1ON = 1;      // turn on comparator

      CM1CON1.C1NCH = ch_phi;
      if (CMOUT)
      {
        STATE = _bouncing;
        PLOval.f0 = 1;
      }

      CM1CON1.C1NCH = ch_plo;
      if (CMOUT)
      {
        STATE = _bouncing;
        PHIval.f0 = 1;
      }

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


To measure various time intervals and widths using the logic analyzer I temporarily modified the firmware as follows. Added are the LATA.f5 and LATA.f0 statements.

The following permits us to see measure cycle time and idle time.

    if (STATE != _standby)
    {
        LATA.f5 = 1;
        while (!INTCON.TMR0IF) ;
        LATA.f5 = 0;

        TMR0 = t0_ini;
        INTCON.TMR0IF = 0;
      DetermineState();      // just one function call to reduce execution time
    }

The following allows measurement of transducer on/off times.

void interrupt()
{
  LATA.f0 = 1;
  if (PIR1.TMR1IF)
  {

  ... other statements 

  } // if (PIR1.TMR1IF)
  LATA.f0 = 0;
} // void interrupt()  

In the following screenshots firmware was in STATE = _plophi_immersed (both probes immersed in liquid). Bear in mind in this mode/state, transducer is designed to be on ~100ms and off ~125ms. Channel 7 is hooked up to RA5 pin; Ch6 to RA0.


Length of time that transducer is on is ~103ms (see value for "Period" in the screenshot)


Time that transducer is off is ~124ms.



With clock = 500kHz (transducer is sounding), out of the ~17.0ms cycle time the firmware is spending 16.35ms doing nothing but waiting for timer0 interrupt flag to get set. 


With clock = 31kHz (transducer off), cycle time ~18.5ms and "dead time" ~8ms. That's still a fair amount of idle time even with the LFINTOSC.


Monday, May 14, 2012

Panasonic IR remote control protocol

I used an Osram SFH5110-38 IR receiver and Saleae Logic to capture the output of a Panasonic air conditioner remote control. I googled but couldn't find any site providing decoding info for the 32-bit packet. The closest I've found shows the protocol for a Panasonic 22-bit packet. Though not exactly what I'm looking for, the page does provide crucial information about pulse widths and what constitutes a logic one and zero. 

The Panasonic remote control uses pulse distance encoding where pulse widths for both logic 1 and 0 are the same: 2T, but the periods are not: 4T for logic zero and 8T for logic 1. Thus a logic 0 is high for 2T and low for 2T while a logic 1 is high for 2T and low for 6T. The start pulse is 8T high and 8T low. From the logic analyzer measurements T for the particular unit I have is anywhere from 435 to 440µs. (Bear in mind that at the receiver end everything is inverted--high is low, and low is high, with the output of the IR receiver normally high when no transmission is being received.)


The remote control sends out the least significant byte (LSB) is first. And for every byte the least significant bits (LSb) are sent out first. A start pulse and four bytes constitute one packet. The LSB is the command code. This code is sent again (without inversion) as the second byte. The third byte is the address code. This is transmitted again and is the fourth and most significant byte.

With the exception of three, keeping the buttons depressed does not continually transmit a signal. Instead exactly three packets are sent without pause (i.e., a start pulse immediately follows after the last bit of the packet has been transmitted). The series of three packets is then capped with an end pulse which is equivalent to a start pulse and 2T high.

Holding down the up arrow, down arrow, or fan speed buttons will continually send the packet (with no end pulse). Panasonic must've made a mistake with the fan speed button since it need not send the packet continuously. Testing shows that even if the fan speed button on the remote control is kept depressed the speed of the blower is toggled only once per button push (it has to be released and pressed again to change fan speed).



From the looks of it the numbers at the back of the control are stamped after molding. Another Panasonic air conditioner remote has the same "model number" A75C2454 but has "batch number" 9225 instead of the 91X4 on this unit.
 

I tested all the buttons on the remote control and collected the following command and address codes. 

Panasonic air conditioner remote control codes
Key/Button MSB LSB
Address Command
Operation 0x33 0x33 0x91 0x91
Mode 0x33 0x33 0x92 0x92
Fan Speed 0x33 0x33 0x93 0x93
Up arrow 0x33 0x33 0x94 0x94
Down arrow 0x33 0x33 0x95 0x95
Timer 0x33 0x33 0x96 x096
Set/Cancel 0x33 0x33 0x97 0x97
Air Swing 0x30 0x30 0x80 0x80
Powerful 0x35 0x35 0x86 0x86

Sunday, May 13, 2012

BevSense v3

Thought I'd give it a more succinct and at the same time livelier name than the absolutely eggheady and dull "liquid level indicator."

Tweaked the firmware and made a number of major changes to reduce current in all modes as well as number of instructions executed when not in standby mode--i.e., when liquid has been detected or if a probe error has occurred. Among the changes are:
  • Only the 31kHz LFINTOSC is used for all modes. With the PWM module taking care of energizing the piezo transducer it's no longer necessary to use the 500kHz clock and toggle it on and off in the ISR. The 500kHz clock is the default frequency upon reset and is used only during initialization of various SFRs.
  • WDT timeout is permanently set to 512 ms.
  • 8 probe readings instead of 5 are stored in PXXval. Because number of instructions has been reduced, even with just a 31kHz clock, time interval between probe reads is usually less than 10ms. So debouncing the probes takes less than 100ms.
  • States _begin, _insta_plo, and _insta_phi, and functions InstaPLO(), InstaPHI(), and DisableAudible have been discarded.
  • Audible indicator is now only disabled when entering standby mode. It isn't necessary to disable it in any other mode since only standby mode needs it shut down.
  • Variables PLOlevel, PHIlevel, plo, phi, buzzing have been discarded. These have been found to be redundant and to increase execution time.
  • Timer1 interrupt is always enabled; only global interrupt is enabled/disabled when audible indicator is enabled/disabled 
  • Timer0 is no longer used. There is now no fixed time between reads when not in standby mode, although the time between probe reads has been determined to be 9.5 to 9.8ms depending on the mode, excluding interrupt service. Measured ISR execution time when low probe is immersed or when both low and high probes are immersed is ~3.1ms or ~3.2ms depending on whether the buzzer is being turned on or off.
  • Probe reading and determination of status/mode have been highly optimized. And for standby mode no function calls outside main() are made, in order to reduce execution time.
  • Although the use of switch / case is more elegant, given the way mikroC implements it, execution time takes a tad longer than using a series of if statements. So in the ISR the probe error routine now uses if / else statements. ISR times using switch / case are ~4.8ms, ~5.3ms, ~5.8ms, ~6.4ms for the first beep, first pause, second beep, second pause, respectively. With if / else times are ~4.3ms, ~4.9ms, ~5.6ms and ~ 5.4ms, respectively.
  • Because of the 31kHz clock, the buzzer frequency of 2.15kHz can no longer be attained with the PWM module. The best that can obtained is 1.94kHz.
  • Left shifting of bits in PXXval is now done after determination of status/mode
With the various improvements average standby mode current is now down to around 0.63 to 0.64µA. When liquid is detected maximum current draw is ~2.0mA and average is ~1.0mA.


/*

Liquid Level Indicator version 3
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)
Internal oscillator starts at 500kHz after power up then switched to 31kHz LFINTOSC after initialization of SFRs and remains at that setting throughout

*/


#define  int8                unsigned char

#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  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-15    // TMR1H initial value for audible indicator when low probe immersed or low and high probe immersed
#define  t1h_beep1           256-30    // 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-30    // TMR1H initial value for second beep during probe error
#define  t1h_pause2          256-122   // TMR1H initial value for second pause during probe error

#define  osc31khz            0b0       // 31kHz LFINTOSC, for use with OSCCON
#define  wdt512ms            0b10011   // WDT time out = 512ms, for use with WDTCON

int8 PLOval;                           // stores the last eight low probe readings
int8 PHIval;                           // stores the last eight high probe readings

enum {_bouncing, _standby, _plo_immersed, _plophi_immersed, _phi_immersed}
     STATE = _bouncing,                // current state
     PREVSTATE = _bouncing;            // 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 = 0;            // global pull ups enabled
  
  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-

  // piezoelectric transducer empirically determined to be loudest at 2.15kHz
  // 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 = 3, timer2 prescale = 1, CCPR1L:CCP1CON<5:4> = 4, Fosc = 31kHz,
  // PWM period = 516us (freq = 1.937kHz) and duty cycle = 25%
  T2CON = 0;                 // prescale = 1:1, postscale = 1:1, timer2 off
  PR2 = 3;
  CCPR1L = 0b1;              // with CCPR1L = 0b1 and CCP1CON<5:4> = 0b00, CCPR1L:CCP1CON<5:4> = 0b100 = 4 decimal
  CCP1CON = 0b1100;          // PWM mode with P1A active high, P1B disabled, CCP1CON<5:4> = 0b00

  // initialize probe readings to zero and levels to zero
  PLOval = 0;
  PHIval = 0;

  PIE1.TMR1IE = 1;           // timer1 interrupt enabled
  INTCON.PEIE = 1;           // peripheral interrupt enabled. Global interrupt is by default disabled upon any reset. GIE is enabled only when timer1 is enabled (i.e., when buzzer needs to be sounded)
  OSCCON = osc31khz;         // default INTOSC frequency = 500khz. Change to 31kHz LFINTOSC after initialization.
  WDTCON = wdt512ms;         // WDT time out = 512ms. This is the sleep time between probe reads when in Standby mode
                             // longer sleep times would be better from a power consumption perspective
                             // but would be too long when the user starts using the unit for its intended purpose -- to detect liquid and as soon as possible

} // void IniReg()


// turn on buzzer
void BuzzOn()
{
  TMR2 = 0;
  PIR1.TMR2IF = 0;
  T2CON.TMR2ON = 1;
}

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

// audible indicator is armed, with the particular sequence of sounds emitted depending on
// whether liquid has reached low probe, low and high probe, or just the high probe
void EnableAudible()
{
  TMR1L = 0;              
  PIR1.TMR1IF = 0;
  INTCON.GIE = 1;
  BuzzOn();
}

// both low and high probes not immersed
// PLOval = PHIval = 0
void Standby()
{
  STATE = _standby;
  BuzzOff();                // following three statements disable audible indicator
  T1CON.TMR1ON = 0;         //  
  INTCON.GIE = 0;           // 
  wpu_plo = 1;              // enable weak pullup for low probe
  wpu_phi = 1;              // enable weak pullup for high probe
}

// low probe immersed -- cup nearly full
void PLOimmersed()
{
  if (PREVSTATE != _plo_immersed)
  {
    STATE = _plo_immersed;
    TMR1H = t1h_fill;
    T1CON = 0b1100001;       // Timer1 clock source is system clock (FOSC), prescale = 1:4, timer1 oscillator off, timer1 on
                             // given 31kHz clock and TMR1H ini value = 15, timer1 tick is ~500ms
    EnableAudible();
  }
}

// low and high probes both immersed -- cup is full
void PLOPHIimmersed()
{
  if (PREVSTATE != _plophi_immersed)
  {
    STATE = _plophi_immersed;
    TMR1H = t1h_fill;
    T1CON = 0b1000001;       // Timer1 clock source is system clock (FOSC), prescale = 1:1, timer1 oscillator off, timer1 on
                             // given 31kHz clock and TMR1H ini value = 15, timer1 tick is ~125ms
    EnableAudible();
  }
}

// probe error -- only high probe is immersed
void PHIimmersed()
{
  if (PREVSTATE != _phi_immersed)
  {
    STATE = _phi_immersed;
    TMR1H = t1h_beep1;
    T1CON = 0b1000001;       // Timer1 clock source is system clock (FOSC), prescale = 1:1, timer1 oscillator off, timer1 on
    EnableAudible();
    STATEPERROR = _beep1;
  }
}


void DetermineState()
{
  asm clrwdt

  // ===============================
  // Read Probes -- for modes other than standby
  // Individual weak pull ups are enabled prior to reading and disabled after reading to minimize current draw since the probes are immersed and weak pull ups will draw current
  // DAC, weak pull ups, and comparator are turned on before reading and then turned off afterwards to minimize power consumption
  // current reading stored in PXXval.f0
  // ===============================
  CM1CON0.C1ON = 1;          // turn on comparator (current draw ~4.5uA)
  DACCON0.DACEN = 1;         // turn on DAC (current draw ~19uA)

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

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

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

  // ===============================
  // Determine state/mode
  // ===============================
  if (!PHIval)
  {
    if (!PLOval)
      Standby();
    else if (PLOval == 0xFF)
      PLOimmersed();
  }
  else if (PHIval == 0xFF)
  {
    if (PLOval == 0xFF)
      PLOPHIimmersed();
    else if (!PLOval)
      PHIimmersed();
  }

  // all bits (readings) of PXXval can now be shifted left
  // discard PXXval.f7 and make PXXval.f0 ready for the new reading
  PLOval <<= 1;
  PHIval <<= 1;

  PREVSTATE = STATE;
} // void DetermineState()


void interrupt()
{
  if (PIR1.TMR1IF)
  {
    if (STATE == _phi_immersed)
    {
      if (++STATEPERROR > _pause2)
        STATEPERROR = _beep1;

      if (!STATEPERROR)
      {
        BuzzOn();
        TMR1H = t1h_beep1;           // given 31kHz, system clock as clock source, prescale = 1:1, TMR1H ini value = 30, timer1 tick is ~250ms
      }
      else if (STATEPERROR == _pause1)
      {
        BuzzOff();
        TMR1H = t1h_pause1;          // given 31kHz, system clock as clock source, prescale = 1:1, TMR1H ini value = 19, timer1 tick is ~160ms
      }
      else if (STATEPERROR == _beep2)
      {
        BuzzOn();
        TMR1H = t1h_beep2;           // given 31kHz, system clock as clock source, prescale = 1:1, TMR1H ini value = 30, timer1 tick is ~250ms
      }
      else
      {
        BuzzOff();
        TMR1H = t1h_pause2;          // given 31kHz, system clock as clock source, prescale = 1:1, TMR1H ini value = 122, timer1 tick is ~1000ms
      }
    } // if (STATE == _phi_immersed)
    else // if (STATE == _plo_immersed || STATE == _plophi_immersed)
    {
      TMR1H = t1h_fill;
      if (T2CON.TMR2ON)
        BuzzOff();
      else
        BuzzOn();
    }
    PIR1.TMR1IF = 0;
  } // if (PIR1.TMR1IF)
} // void interrupt()


void main()
{
  IniReg();

  while(1)
  {
    if (STATE != _standby)
      DetermineState();      // just one function call to reduce execution time
    else
    {
      asm sleep              // WDT is automatically cleared right before sleep and after waking up
                             // MCU wakes up upon WDT timeout

      // Reduction of MCU awake time during Standby mode reduces current draw
      // Ways by which instructions executed when MCU is awake are reduced:
      //     1. Probe reading is stored in PXXval only if reading is high
      //     2. Weak pull ups are kept on since they don't consume current unless probes are immersed.
      //     3. Standby mode is when PLOval = PHIval = PLOlevel = PHIlevel = 0 therefore we need only monitor for instantaneous readings that are high
      //     4. Reduction/elimination of function calls and using inline code instead
      // DAC and voltage comparator are turned on before reading and then turned off afterwards to minimize power consumption
      DACCON0.DACEN = 1;     // turn on DAC
      CM1CON0.C1ON = 1;      // turn on comparator

      CM1CON1.C1NCH = ch_phi;
      if (CMOUT)
      {
        STATE = _bouncing;
        PLOval.f0 = 1;
      }

      CM1CON1.C1NCH = ch_plo;
      if (CMOUT)
      {
        STATE = _bouncing;
        PHIval.f0 = 1;
      }

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

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.