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 STATE
s 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.