Sunday, June 17, 2012

Braking distance and time on level road and on an incline

Off topic entry. Nothing to do with electronics. But I've been obsessed with (or should I say possessed by) this topic for the past week or so. Just want to have all the equations and derivations in one place so I can refresh my memory if I need to in the future.  

Let
vi = initial velocity, m/s
vf = final velocity, m/s
v = average velocity, m/s
a = deceleration rate, m/s2
t = time to stop the vehicle, s
d = braking distance, m
μ = coefficient of friction, unitless
Ff = force of friction, kg·m/s2
Fn = normal force (force perpendicular to surface), kg·m/s2
Fg = force due gravity pulling the vehicle down a slope, kg·m/s2
m = mass, kg
g = gravitational acceleration, 9.8 m/s2
θ = angle of inclination (positive if vehicle going uphill, negative if going downhill)

Final velocity is given by the equation:



If vf  = 0 then



Distance is given by:



Average velocity is:



Given  vf  = 0 and substituting we have:



Alternatively we can derive the distance formula using kinetic energy and work equations:







Equating work and energy we have:



Solving for d we obtain:



The force of friction is given by the following formula:



If the vehicle is on a level surface then normal force is just its weight. Therefore,



Since



then



Therefore,



Substituting the above into the distance and time equations we get:





Interesting to note that we can (at least in principle) determine the coefficient of friction by noting the velocity when the brakes are fully applied and time to bring the vehicle to a dead stop.


Now let's tackle the situation when the vehicle is braking on a slope.

When the vehicle is on an incline then a portion of the gravitational force acts to pull the vehicle down the slope. This force we denote as Fg and is derived via basic vector analysis. Refer to the free-body diagram. On the left of the diagram is an object on an incline with an angle of θ. Its weight is labeled as mg. Normal force Fn is the component of mg that is perpendicular to the incline. Fg is the component of mg that is parallel to the incline. To the right I've drawn a vector addition of the forces. Since this is a right triangle we can use the pertinent trigonometric functions: Sin θ = opposite side / hypotenuse. Cos θ = adjacent side / hypotenuse. Since the hypotenuse = mg we obtain the following:





An aside: Since the above are the legs of the right triangle it follows from the Pythagorean theorem that




When on a slope with the brakes engaged so that the vehicle is at a standstill Ff is the force that keeps the vehicle from slipping and sliding down and acts in the opposite direction as Fg which is pulling it down the incline. The net force holding the vehicle in place is therefore Ff - Fg. It stands to reason that when Fg becomes >= Ff then the vehicle--even with the brake pads fully gripping the brake discs and drums--will begin to slip and slide. We can calculate the angle of inclination at which this will occur:











Bear in mind that in this case we are using the static coefficient of friction rather than kinetic (or sliding) coefficient of friction since the vehicle is at rest with respect to the road.

When a vehicle is going downhill its braking distance will increase because a fraction of the gravitational force is acting to pull it down even when the brakes are applied. θ will be negative if going downhill, and so Fg will be negative. When going uphill part of gravitational force adds to the force of friction thereby decreasing braking distance. 

The sum of forces acting on the vehicle parallel to its direction of motion is given by:



Since



Therefore,





Since







Substituting into our distance and time equations we obtain:





The above are exact equations in contrast to the approximate formula using road grade. The gradient is equal to the rise over run, or tangent of θ. The arctangent of the gradient = angle θ.

Reminder: when using the above equations θ should be negative when the vehicle is going downhill. Given -90° < θ < 0, sin θ is negative while cos θ is positive. Hence Fg is effectively subtracted from Ff. When going uphill and θ is positive then Ff is effectively increased by amount Fg. As anyone who's driven knows, it's harder to bring a vehicle to a stop when going downhill. And that it's easier to do so when going uphill than when on a level road.

Beware that the above distances and times do not take into account the driver's perception and reaction time--the time to perceive the need to stop and the time to actually to apply the brakes. This takes a minimum of around half a second for a driver who's alert and concentrating on the road, but may be much greater if the driver is drowsy, tired, intoxicated, on his cell phone, has poor eyesight, etc. or if visibility is poor among other possible factors beyond the driver's control.

The distance traveled by the vehicle during this time is simply




Here are a couple of online braking distance calculators which don't, however, have provisions for vehicles on an incline:
http://www.csgnetwork.com/stopdistcalc.html
http://forensicdynamics.com/stopping-distance-calculator

Tables of coefficients of friction:
http://www.engineeringtoolbox.com/friction-coefficients-d_778.html
http://www.roymech.co.uk/Useful_Tables/Tribology/co_of_frict.htm

Summary of the relevant data from the above tables:

tire material road type and condition kinetic coefficient of friction
rubber dry concrete0.6 - 0.85
rubber wet concrete 0.45 - 0.75
rubber dry asphalt 0.5 - 0.8
rubber wet asphalt 0.25 - 0.75

A point worth mentioning: While the wheels are turning static coefficient of friction is operative because at the point of contact the tire is not sliding along the road. This is desirable because the static coefficient is higher than the kinetic coefficient of friction thus leading to decreased braking distance. Anti-lock brakes (ABS) prevent the wheels from locking even when the driver slams the brake pedal, therefore preventing the tires from skidding and sliding. However, the speed of the tire is lower than the vehicle's and so some slip occurs between tire and road. The static coefficient of friction cannot therefore be applied to vehicles even if they're equipped with ABS. For safety reasons it's best to be conservative and use the kinetic coefficient.


The take home lessons from the above formulas:

Braking distance varies with the square of velocity. Thus, doubling one's speed quadruples the distance. Increasing one's speed from 40kph to 100kph increases the braking distance by (100/40)2 = 6.25 times. Braking time, on the other hand, increases linearly. And thus, since 100kph is 2.5 times 40kph, braking time at 100kph is 2.5x that at 40kph.

The worst braking scenario is when the coefficient of friction is low--when the roads are wet, muddy, or iced up--and one is going downhill, with braking time and distance becoming greater as the inclination increases.

Hence, from the standpoint of safety the best thing a driver can do is to keep speeds low and/or to keep one's distance from other vehicles (and pedestrians) as large as practically possible. One can also apply the two-second rule. Although as weather and road conditions worsen this should be increased to three or four seconds.

Wednesday, June 13, 2012

Detecting liquified petroleum gas (LPG)

Figaro manufactures sensors that can sniff out various kinds of gases (e.g. hydrogen, methane, carbon monoxide, etc.). I've designed a simple circuit built around the Figaro LPM2610 module which will sound an alarm if and when the level of liquified petroleum gas (LPG) in the kitchen rises above a certain predetermined level.

I knew the dimensions of the module from the datasheet but when they arrived I was still quite surprised they're tiny. Nothing against it of course. I don't want a huge daughter board sitting on my main board. The surface mount chips are all resistors. No electrostatic discharge sensitive components here at all. The thermistor is the unmarked black chip at the upper left corner. 



As per datasheet the dark blue bands on the trimpot should be lined up to ensure it's in the calibrated position.





The Figaro module contains the TGS2610 sensor and various resistors to create two voltage dividers--one for the sensor output and the other for the reference voltage. Whenever sensor voltage VOUT is equal to the temperature-compensated reference voltage this indicates the LPG concentration in the air is (theoretically) 10% of the lower explosive limit (LEL) of isobutane. As per the datasheet, butane LEL is 18,000 ppm which is equivalent to 1.8% by volume (although one website puts it at 16,000 ppm or 1.6%).

In Application Note for LP Gas Detectors using TGS2610 Figaro provides a wealth of information to get an application up and running. One of the more important tips therein is that the TGS2610 needs a warm up time to get the sensor up to its working temperature. Figaro recommends waiting 2.5 minutes. During this preheating stage the sensor's output is unreliable. In the same AN Figaro suggests circuits to detect heater as well as sensor failures.

According to the datasheet the typical current through the heater @5.0V is 56mA. Although Figaro suggests using a 3.57-ohm current sense resistor to detect heater failure, at the moment what I have are a couple of 2 ohms. 56mA x 2 ohms = 112mV. Therefore, my VDD has to be around 5.1V to obtain a voltage drop of ~5.0V across the heater. So I configured a power supply using an LM317 voltage regulator to output this value. VOUT and VREF were unity-gain buffered while the current sense voltage was gained by 20. Output of the op amps were hooked up to a Dataq DI-145 to record the values. I then performed a cold start (i.e, the module had not been powered for at least an hour to make sure it's at room temperature).

In the screenshot below you can see that while the heater is ramping up to its working temperature VOUT gradually settles down (approx 0.3 to 0.4V). This takes >100sec.

Interestingly, voltage across heater current sense resistor spikes upon power up and then exponentially settles. This inrush current suggests that the heater has a cold resistance lower than its hot resistance--just like the filament of an incandescent lamp.

Channel 1: VOUT
Ch2: VREF
Ch3: voltage across 2-ohm current sense resistor amplified 20x
Ch4: unused

Vertical: 0.5 volts/div
Horizontal: 5 sec/div
Data acquisition rate: 16 samples/sec


To get a higher resolution and better picture of what's happening during power up I increased the sample rate to the Dataq's maximum of 240/sec.

What's interesting in the image below is how VOUT stays at nearly zero volt (~20 to 30mV according to Windaq) for around 1 second before climbing. Since this is the voltage across the load resistor (RL), apparently the semiconductor sensor (RS) which is in series with RL has an extremely high resistance at room temperature which drops drops dramatically as it begins to heat up, thus behaving like a negative temperature coefficient (NTC) thermistor. However, its resistance begins to gradually increase as it reaches a certain temperature. Figaro has this short intro to the construction and operating principles of its sensors

Channel 1: VOUT
Ch2: VREF
Ch3: voltage across 2ohm current sense resistor and then amplified by a nominal gain of 20
Ch4: VDD

Vertical: 0.491 volts/div for Channels 1 to 3, 0.231 V/div for Ch4
Horizontal: 0.33 sec/div
Data acquisition rate: 240 samples/sec


Complete Working Circuit

Given that Figaro AN gives a suggestion on how to detect sensor failure, it must be that this is a distinct possibility. Since I'm using a quad op amp (MCP609) I decided to make use of what would be an unused device and connected VOUT to it and then configured it to have the same gain as the amplifier for the heater current sense resistor. Normally the output of this noninverting amplifier would be close to VDD. However, when the sensor does fail op amp output will be much closer to ground.

Schematic of a fully working LPG detector: 



Instead of using comparators as the Figaro AN suggests an MCU with ADC and/or a comparator simplifies the design and affords far more flexibility. The schematic shows the connections given a Microchip PIC12F615 8-pin microcontroller. The 1K resistors in series with the output of two op amps are only necessary because the MCU pins to which they are connected are also used for in-circuit serial programming. The resistors isolate the op amps during programming. Without them the PICkit2/3 will not be able to properly upload the firmware. A buzzer and an LED provide the alarms.

I've routed VOUT to one of the MCU's comparator inverting input pin (CIN0- / AN1) and VREF to the comparator noninverting input pin (CIN+ / AN0). This allows the firmware to use the comparator as well as the ADC to compare the two voltages. Thus, in the firmware below, I have preprocessor directives that allow the user to choose--before compilation--either ADC or voltage comparator.

To make sure the firmware works I initially used simple voltage dividers to emulate the outputs of the Figaro module:
Heater failure was simulated by simply disconnecting the 91-ohm resistor from VDD . Potentiometer permits testing the full range of VOUT from 0 to VDD.

After I was satisfied the firmware was bug-free, I removed all the resistors and plugged the LPM2610 into the breadboard and tested the circuit in the kitchen with the stove disconnected from the mains power so that the stove's spark plug like igniters wouldn't function as I turned the knobs to let gas out. During the test what got me immediately concerned is that it took what seemed a rather high concentration of gas to trigger the sensor. I had to place the breadboard really up close to the stove burner where the gas comes out to get the circuit to trigger the alarm. When the breadboard was a couple of feet away, I had to keep the stove knob depressed for longer than I felt safe (and the smell of gas was getting to me). I had a DMM hooked up to VOUT so I could monitor it. And from that feedback I felt that 10% LEL was still too high a threshold. Why not lower it to 5% or perhaps even less than that? And while at it why not include a digital display showing how much gas the sensor is detecting? Since that involves a host of mathematical acrobatics I will tackle that project in another blog entry.

One other revelation of note. LPG is usually a mix of butane and propane. At room temperature butane has a density of ~2.5 kg/m3, propane ~1.9, and air ~1.2. Since the two gases have higher densities than air (butane being twice as heavy as air) they ought to sink and crawl along the floor. And so that's where I plan to install the circuit when I finish building it. But during the test I got practically the same reading at floor level as before I started letting gas leak out. Instead, and oddly, the sensor was giving a higher reading at half a meter above the stove. It's possible that air currents at the time of the test could have carried the gas upwards. Exit velocity of the gas and its trajectory could be factors too. And there may be other parameters as well of course. Will need to reassess and determine where best to place the sensor circuit.

Firmware

Upon power up there will be a 150-second preheating period indicated by a an audible alert (a short beep once every second). During normal operation the outputs of the LPM2610 is checked four times a second--gas travels and diffuses through the air at a very low speed so there's no need to rapidly poll the sensor. In fact 4 samples/sec may even be considered too fast. During each reading heater and sensor are checked. If voltage across current sense resistor is below a set threshold then the LSb of the16-bit variable VHEAT is set (made = 1). The latest reading is in the LSb while the oldest reading is in the MSb. If VHEAT = 0xFFFF and its current state is that heater is ok, then state is now that heater is faulty. A "heater just became faulty" edge trigger is issued. On the other hand, if VHEAT = 0x0000 and and its current state is that heater is faulty then state is now that heater is ok. A "heater has just returned to normal operation" edge trigger is issued. Any VHEAT value other than 0x0000 and 0xFFFF is analogous to a bouncing switch contact and no action is taken. All bits in VHEAT are then shifted left with zero getting shifted into the LSb.

To check the integrity of the sensor the value of VOUT (gained by 20) is checked. If it dips below a set threshold then the LSb of the16-bit variable VSENSE is set (made = 1). The latest reading is in the LSb while the oldest reading is in the MSb. If VSENSE = 0xFFFF and its current state is that sensor is ok, then state is now that sensor is faulty. A "sensor just became faulty" edge trigger is issued. On the other hand, if VSENSE = 0x0000 and and its current state is that sensor is faulty then state is now that sensor is ok. A "sensor has just returned to normal operation" edge trigger is issued. Any VSENSE value other than 0x0000 and 0xFFFF is analogous to a bouncing switch contact and no action is taken. All bits in VSENSE are then shifted left with zero getting shifted into the LSb. 

If heater and sensor are both ok (but not if either is faulty), then VOUT is compared to VREF. If VOUT is > VREF then the LSb of the16-bit variable VLPG is set (made = 1). The latest reading is in the LSb while the oldest reading is in the MSb. If VLPG = 0xFFFF and its current state is that LPG is absent, then state is now that LPG is present. An "LPG present" edge trigger is issued. On the other hand, if VLPG = 0x0000 and and its current state is that LPG is present then state is now that LPG is absent. An "LPG absent" edge trigger is issued. Any VLPG value other than 0x0000 and 0xFFFF is analogous to a bouncing switch contact and no action is taken. All bits in VLPG are then shifted left with zero getting shifted into the LSb.

If either heater or sensor is faulty or if LPG is detected then the appropriate alarm is sounded. The alarm patterns for each of the conditions are as follows:

Heater fault: 100ms beep -> 500ms silence -> 100ms beep -> 4000ms silence -> repeat
Sensor fault: 100ms beep -> 500ms silence -> 100ms beep -> 500ms silence -> 100ms beep -> 4000ms silence -> repeat
LPG detection: 500ms beep -> 500ms silence -> 500ms beep -> 500ms silence ->  2000ms silence -> repeat

Should heater be faulty and for some strange reason reverts to normal, then firmware will enter an infinite loop to let the watchdog timer time out and reset the MCU. This is to force the heater to go through the preheating stage.


/*

Liquified petroleum gas (LPG) detector

When LPG level exceeds a set threshhold audible and visual alarms are activated
When Figaro LPM2610 heater or sensor fault is detected, alarms are activated

Configuration: all enabled except for code protect
               Internal oscllator @4MHz, I/0 on clock pins
               
CONFIG   :$2007 : 0x036C


ALARMS/ALERTS:

During power up delay: 25ms beeps every second for around 2.5mins
Heater fault: 100ms beep -> 500ms silence -> 100ms beep -> 4000ms silence -> repeat
Sensor fault: 100ms beep -> 500ms silence -> 100ms beep -> 500ms silence -> 100ms beep -> 4000ms silence -> repeat
LPG detection: 500ms beep -> 500ms silence -> 500ms beep -> 500ms silence ->  2000ms silence -> repeat


LEL = lower explosion limit
LEL of butane = 18,000ppm = 1.8% by volume

For the LPM2610 when Vout = Vref then LPG level is ~10% LEL

*/



// ===========================================================================================
//       Values for the following defines can be changed as needed
// ===========================================================================================

//***********************************************
// comment out the following define when done debugging
#define  debug
//***********************************************

#ifdef debug
  #define  powerupdelaysec     10
#else
  #define  powerupdelaysec     150     // number of seconds to allow heater to reach working temperature during power up reset
                                       // according to datasheet recommended preheating time is 2.5min
#endif

//***********************************************
// comment out the following define if vlpg and vref will be measured by the ADC instead of volt comaparator module
// either ADC or comparator can be used
#define  use_comparator
//***********************************************


#define  heat_resist_volt    75        // actual millivolts across current sense resistor below which is considered practically zero and therefore indication of heater failure
#define  sense_volt          50        // actual millivolts of LM2610 sensor output voltage below which is considered practically zero and therefore indication of sensor failure
#define  gain                20        // op amp gain

#define  shortbeep_ms        100       // duration of a short beep, in actual milliseconds. maximum of 524ms
#define  shortpause_ms       500       // duration of a short pause, in actual milliseconds. maximum of 524ms
#define  longpause_ms        4000      // duration of a long pause, in actual milliseconds. *minimum* of 500ms

#define  lpg_beep_ms         500       // LPG detected alarm: duration of first beep, in actual milliseconds. maximum of 524ms
#define  lpg_pause1_ms       500       // LPG detected alarm: duration of first pause, in actual milliseconds. maximum of 524ms
#define  lpg_pause2_ms       2000      // LPG detected alarm: duration of second pause, in actual milliseconds. *mininum* of 500ms

#define  reset_beep_ms       25        // power up delay alert: duration of beep, in actual milliseconds. maximum of 524ms
#define  reset_pause_ms      1000      // power up delay alert: duration of pause between beeps, in actual milliseconds. *mininum* of 500ms


// ===========================================================================================
//       !! WARNING WILL ROBINSON !! DO NOT ALTER THE DEFINES BELOW !!
// ===========================================================================================

#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  Vdd                 5100                                   // Vdd voltage
#define  heater_threshold    heat_resist_volt*gain*1024/Vdd         // convert to 10-bit ADC value given ADC ref voltage = Vdd
#define  sense_threshold     sense_volt*gain*1024/Vdd               // convert to 10-bit ADC value given ADC ref voltage = Vdd

// TMR1H initial values for various alarms (Fosc = 4MHz, timer1 prescale = 1:8, TMR1L starts at zero)
#define  t1h_shortbeep       256-(shortbeep_ms*1000/(256*8) + 0.5)
#define  t1h_shortpause      256-(shortpause_ms*1000/(256*8) + 0.5)
#define  t1h_longpause       256-(500*1000/(256*8) + 0.5)
#define  longpause_cycles    longpause_ms / 500

// TMR1H initial values for LPG detection alarm (Fosc = 4MHz, timer1 prescale = 1:8, TMR1L starts at zero)
#define  t1h_lpgbeep         256-(lpg_beep_ms*1000/(256*8) + 0.5)
#define  t1h_lpgpause1       256-(lpg_pause1_ms*1000/(256*8) + 0.5)
#define  t1h_lpgpause2       256-(500*1000/(256*8) + 0.5)
#define  lpg_pause2_cycles   lpg_pause2_ms / 500

// TMR1H initial values for power-up delay alert (Fosc = 4MHz, timer1 prescale = 1:8, TMR1L starts at zero)
#define  t1h_resetbeep       256-(reset_beep_ms*1000/(256*8) + 0.5)
#define  t1h_resetpause      256-(500*1000/(256*8) + 0.5)
#define  reset_pause_cycles  reset_pause_ms / 500

#define  buz                 GPIO.f5   // buzzer
#define  tris_buz            TRISIO.f5

#define  tris_vref           TRISIO.f0 // LPM2610 Vref pin
#define  tris_vlpg           TRISIO.f1 // LPM2610 Vout pin
#define  tris_vsense         TRISIO.f2 // LPM2610 Vout pin with gain = 20
#define  tris_vheat          TRISIO.f4 // voltage across heater current sense resistor with gain = 20
                                       // GP4 is AN3

#define  an_vref             ANSEL.f0
#define  an_vlpg             ANSEL.f1
#define  an_vsense           ANSEL.f2
#define  an_vheat            ANSEL.f3

#define  ch_vref             0         // for use with ADCON0 when selecting analog channel
#define  ch_vlpg             1         // for use with ADCON0 when selecting analog channel
#define  ch_vsense           2         // for use with ADCON0 when selecting analog channel
#define  ch_vheat            3         // for use with ADCON0 when selecting analog channel

#define  cmch_vlpg           0         // for use with CMCON0 when selecting comparator channel

#define  _lpg_absent         0         // for use with variable statelpg
#define  _lpg_present        1         // for use with variable statelpg

#define  _heater_ok          0         // for use with variable stateheater
#define  _heater_fault       1         // for use with variable stateheater

#define  _sensor_ok          0         // for use with variable statesense
#define  _sensor_fault       1         // for use with variable statesense

#define  none                0         // for use with edge detect

bit stateheater;                       // status of LPM2610 heater, whether ok or faulty
bit statesense;                        // status of LPM2610 sensor, whether ok or faulty
bit statelpg;                          // status of lpg detection, whether detected or not
bit powerreset;                        // 1 = power up delay in progress; ISR needs this to determine which alarm pattern to sound

enum {heatfaultbegin = 1, heatfaultend} HEATEDGE;          // edge detect:  0 = no edge detected, 1 = heater fault has just been detected, 2 = heater fault has just been resolved
enum {sensefaultbegin = 1, sensefaultend} SENSEEDGE;       // edge detect:  0 = no edge detected, 1 = sensor fault has just been detected, 2 = sensor fault has just been resolved
enum {lpgdetectbegin = 1, lpgdetectend} LPGEDGE;           // edge detect:  0 = no edge detected, 1 = level of LPG in the air has just gone above detection threshold, 2 = level of LPG has just dropped below threshold

enum {_heatbeep1, _heatpause1, _heatbeep2, _heatpause2} STATEHEATALARM;
enum {_sensebeep1, _sensepause1, _sensebeep2, _sensepause2, _sensebeep3, _sensepause3} STATESENSEALARM;
enum {_lpgbeep1, _lpgpause1, _lpgbeep2, _lpgpause2} STATELPGALARM;
enum {_resetbeep1, _resetpause1} STATERESETALARM;

int16  VLPG = 0;             // 0x0000 = no lpg detected, 0xFFFF = lpg detected, any other value implies bouncing
int16  VHEAT = 0;            // 0x0000 = LPM2610 heater functioning, 0xFFFF = heater fault detected, any other value implies bouncing
int16  VSENSE = 0;           // 0x0000 = LPM2610 sensor functioning, 0xFFFF = sensor fault detected, any other value implies bouncing


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

void BuzzOn()
{
  buz = on;
}

void BuzzOff()
{
  buz = off;
}

// turn off buz and led and disable timer
void DisarmAlarm()
{
  T1CON.TMR1ON = 0;
  INTCON.GIE = 0;
  BuzzOff();
}

void ArmAlarm()
{
  INTCON.GIE = 1;
  TMR1L = 0;
  T1CON = 0b110001;      // prescale = 1:8, timer1 on
  PIR1.TMR1IF = 1;
}

// Figaro LPM2610 needs preheating time during which output signal is unreliable.
void PowerUpDelay()
{
  int8 i;
  
  OPTION_REG = 0b10001111;   // global weak pull ups disabled, prescaler assigned to WDT, prescale = 1:128
  STATERESETALARM = 0;
  powerreset = 1;
  ArmAlarm();
  for (i = 0; i < powerupdelaysec; i++)
  {
    asm clrwdt               // nominal WDT timeout = 18ms without prescale, given WDT prescale = 1:128 WDT = ~2sec
    Delay_ms(1000);
  }
  powerreset = 0;
  DisarmAlarm();
  OPTION_REG = 0b10000111;   // global weak pull ups disabled, prescaler assigned to timer0, prescale = 1:256
                             // WDT timeout = 18ms
}

void IniReg()
{
  GPIO = 0;
  ANSEL = 0b1011111;         // A/D Conversion Clock = Fosc/16, all four analog channels are analog
  tris_buz = output;

  WPU = 0;                   // disable individual pullups
  OPTION_REG.NOT_GPPU = 1;   // disable global pull up

  INTCON.PEIE = 1;
  PIE1.TMR1IE = 1;

  #ifdef use_comparator
  CMCON0 = 0b10000;          // comparator off, COUT internal only, COUT logic inverted, CMVin+ connected to CIN+
  CMCON0.CMCH = cmch_vlpg;   // only one comparator channel is used -- the one connected to Vout of LPM2610
  #endif
  
  stateheater = _heater_ok;
  statesense = _sensor_ok;
  statelpg = _lpg_absent;
}


int16 ADC(int8 channel)
{
  ADCON0 = channel << 2;     // shift in channel
  ADCON0.ADFM = 1;           // right justified format
  ADCON0.VCFG = 0;           // VDD as ref voltage
  ADCON0.ADON = 1;           // ADC on
  Delay_us(20);              // datasheet says ADC acquisition time typically takes 12us
  ADCON0.GO = 1;             // start ADC conversion
  while(ADCON0.GO) ;         // wait
  ADCON0.ADON = 0;           // ADC off
  return ADRESH*256 + ADRESL;
}


void ReadFigaro()
{
  HEATEDGE = none;
  SENSEEDGE = none;
  LPGEDGE = none;

  if (ADC(ch_vheat) < heater_threshold)
    ++VHEAT;

  if (VHEAT == 0xFFFF && stateheater == _heater_ok)
  {
    HEATEDGE = heatfaultbegin;
    stateheater = _heater_fault;
  }
  else if (!VHEAT && stateheater == _heater_fault)
  {
    HEATEDGE = heatfaultend;
    stateheater = _heater_ok;
  }
  VHEAT <<= 1;

  if (ADC(ch_vsense) < sense_threshold)
      ++VSENSE;

  if (VSENSE == 0xFFFF && statesense == _sensor_ok)
  {
    SENSEEDGE = sensefaultbegin;
    statesense = _sensor_fault;
  }
  else if (!VSENSE && statesense == _sensor_fault)
  {
    SENSEEDGE = sensefaultend;
    statesense = _sensor_ok;
  }
  VSENSE <<= 1;

  if (stateheater == _heater_ok && statesense == _sensor_ok);      // do not read figaro outputs if heater or sensor is faulty
  {
    // ===========================================================================================
    // ADC or comparator can be used to measure vlpg and vref
    #ifdef use_comparator
    CMCON0.CMON = 1;
    Delay_us(10);            // datasheet does not specify a settling time but it has a "mode change to output valid time" of 10us. So to be on the safe side allow some delay between comparator turn-on and reading of output
    if (CMCON0.COUT)
      ++VLPG;
    CMCON0.CMON = 0;         // comparator module consumes >60uA @5V so turn off after reading channel
    #else
    if (ADC(ch_vlpg) > ADC(ch_vref))
      ++VLPG;
    #endif
    // ===========================================================================================

    if (VLPG == 0xFFFF && statelpg == _lpg_absent)
    {
      LPGEDGE = lpgdetectbegin;
      statelpg = _lpg_present;
    }
    else if (!VLPG && statelpg == _lpg_present)
    {
      LPGEDGE = lpgdetectend;
      statelpg = _lpg_absent;
    }
    VLPG <<= 1;
  }
}


void Status()
{
  if (LPGEDGE == lpgdetectbegin)
  {
    STATELPGALARM = 0;
    ArmAlarm();
  }
  else if (LPGEDGE == lpgdetectend)
    DisarmAlarm();

  if (SENSEEDGE == sensefaultbegin)
  {
    STATESENSEALARM = 0;
    ArmAlarm();
  }
  else if (SENSEEDGE == sensefaultend)
    DisarmAlarm();

  if (HEATEDGE == heatfaultbegin)
  {
    STATEHEATALARM = 0;
    ArmAlarm();
  }
  else if (HEATEDGE == heatfaultend)
    while(1) ;     // if heater was faulty but somehow fault condition got resolved then restart the MCU
                   // let WDT timeout reset the MCU so that heater will have preheat time
} // void Status()


void interrupt()
{
  int8 COUNT;
  
  if (PIR1.TMR1IF)
  {
    PIR1.TMR1IF = 0;
    if (stateheater == _heater_fault)
    {
      switch (STATEHEATALARM)
      {
        default:
        case _heatbeep1:
          BuzzOn();
          TMR1H = t1h_shortbeep;
          STATEHEATALARM = _heatpause1;
          break;

        case _heatpause1:
          BuzzOff();
          TMR1H = t1h_shortpause;
          STATEHEATALARM = _heatbeep2;
          break;

        case _heatbeep2:
          BuzzOn();
          TMR1H = t1h_shortbeep;
          STATEHEATALARM = _heatpause2;
          COUNT = 0;
          break;

        case _heatpause2:
          BuzzOff();
          TMR1H = t1h_longpause;
          if (++COUNT >= longpause_cycles)
            STATEHEATALARM = _heatbeep1;
          break;
      } // switch (STATEHEATALARM)
    } // if (stateheater == _heater_fault)

    else if (statesense == _sensor_fault)
    {
      switch (STATESENSEALARM)
      {
        default:
        case _sensebeep1:
          BuzzOn();
          TMR1H = t1h_shortbeep;
          STATESENSEALARM = _sensepause1;
          break;

        case _sensepause1:
          BuzzOff();
          TMR1H = t1h_shortpause;
          STATESENSEALARM = _sensebeep2;
          break;

        case _sensebeep2:
          BuzzOn();
          TMR1H = t1h_shortbeep;
          STATESENSEALARM = _sensepause2;
          break;

        case _sensepause2:
          BuzzOff();
          TMR1H = t1h_shortpause;
          STATESENSEALARM = _sensebeep3;
          break;

        case _sensebeep3:
          BuzzOn();
          TMR1H = t1h_shortbeep;
          STATESENSEALARM = _sensepause3;
          COUNT = 0;
          break;

        case _sensepause3:
          BuzzOff();
          TMR1H = t1h_longpause;
          if (++COUNT >= longpause_cycles)
            STATESENSEALARM = _sensebeep1;
          break;
      } // switch (STATESENSEALARM)
    } // if (statesense == _sensor_fault)
    
    else if (statelpg == _lpg_present)
    {
      switch (STATELPGALARM)
      {
        default:
        case _lpgbeep1:
          BuzzOn();
          TMR1H = t1h_lpgbeep;
          STATELPGALARM = _lpgpause1;
          break;

        case _lpgpause1:
          BuzzOff();
          TMR1H = t1h_lpgpause1;
          STATELPGALARM = _lpgbeep2;
          break;

        case _lpgbeep2:
          BuzzOn();
          TMR1H = t1h_lpgbeep;
          STATELPGALARM = _lpgpause2;
          COUNT = 0;
          break;

        case _lpgpause2:
          BuzzOff();
          TMR1H = t1h_lpgpause2;
          if (++COUNT >= lpg_pause2_cycles)
            STATELPGALARM = _lpgbeep1;
          break;
      } // switch (STATELPGALARM)
    } // if (statelpg == _lpg_present)
    
    else if (powerreset)
    {
      switch (STATERESETALARM)
      {
        default:
        case _resetbeep1:
          BuzzOn();
          TMR1H = t1h_resetbeep;
          STATERESETALARM = _resetpause1;
          COUNT = 0;
          break;

        case _resetpause1:
          BuzzOff();
          TMR1H = t1h_resetpause;
          if (++COUNT >= reset_pause_cycles)
            STATERESETALARM = _resetbeep1;
          break;
      } // switch (STATERESETALARM)
    } // if (powerreset)
  } // if (PIR1.TMR1IF)
} // void interrupt()

void main() 
{
  int8 T0COUNT = 0;
  
  IniReg();
  PowerUpDelay();
  while(1)
  {
    asm clrwdt;
    if (INTCON.T0IF)
    {
      INTCON.T0IF = 0;
      if (++T0COUNT >= 4)    // read sensor approx every quarter second
      {
        T0COUNT = 0;
        ReadFigaro();
        Status();
      } // if (++T0COUNT >= 4)
    } // if (INTCON.T0IF)
  } // while(1)
} // void main()

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