## Sunday, May 6, 2012

### 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 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
{
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

// 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
{
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 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
{
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
}