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.


No comments:

Post a Comment