Sunday, July 1, 2012

Melody door chime with "dead time"

Here's a very similar circuit and firmware to the door open/closed detector. However, instead of a magnetic contact (magnetic proximity switch) triggering the MCU to play the melody, it's an ordinary bell push for doorbells.

The "dead time" in the title refers to the time period after the melody is played when button presses are ignored. This discourages (but unfortunately does not prevent) pranksters (usually kids) from playing with the door chime. Also prevents impatient visitors/mailmen/couriers/extraterrestrials from making the chime go off continuously to put you into panic mode. Moreover, the circuit cannot be fooled by taping down the button in the hopes of forcing the circuit to keep playing the tune since the microcontroller only responds to edge triggering.

VR1 = L7812 +12V voltage regulator 1000mA
VR2 = 78L05ACZ +5V voltage regulator 100mA
MCU = Microchip PIC10F202 microcontroller
PB = bell push
OC1 = Avago HCPL-817 optoisolator
A1 = S9013 NPN transistor
BR = W01M 100V 1.5A bridge rectifier
XF = 220VAC to 6VAC transformer
SPKR = 8-ohm, 250mW, 3-inch speaker

The speaker is powered with 12VDC to significantly increase the output volume. As with the door status detector, I used an old computer speaker. PB is located several meters away from the circuit board. The internal weak pull-ups of the MCU are used so no external resistors are necessary on the collector of the optotransistor. I installed this circuit over three years ago and thus far I've not had any problems. Still working like a charm.

See the door open/closed detector for the principle of operation of the melody playing routines. Debounce routine works by storing the last eight readings in one 8-bit variable. Pushbutton is deemed not bouncing if the the variable is equal to 0x00 or 0xFF. Any other value means the switch is bouncing. Upon detection of a falling edge MCU plays the melody just once. To implement the dead time a simple one-second delay routine is called several times (depending on how many seconds is specified in the constant secdelay) after playing the tune and before handing back control to the pushbutton polling routine. When the melody is being played and during the dead time PB is not polled therefore button pushes are ignored. Because there's only one timer in the PC10F202, timer 0 is used for both playing the notes and for polling of the switches. The prescale values are different for the two applications and so OPTION register is edited accordingly when switching between the two.

et.inc contains various defines and macro. 


; ********************************************************************************
;
; DOOR CHIME   
; February 2009
;
; When bell push is momentarily pressed a melody is played.
; After the melody has finished there is a delay of 15 seconds when PB presses are ignored. 
; Because PB is not polled when melody is playing and during the delay,
; repeatedly pressing PB during this time does not result in any action.   
; Since the circuit responds only to edge triggering keeping the bell push depressed 
; will not result in any further action.
;
; MCU has debounce firmware that monitors bell push switch readings. Upon detection of a falling edge 
; MCU plays a sequence of musical notes. Firmware does not react to a rising edge. 
;
; MCU weak pull-ups are enabled in firmware. 
; Watchdog timer is activated. WDT nominal time-out according to datasheet is 18ms (without prescaler) 
;
; ********************************************************************************

     
    processor      10F202
    #include       p10F202.inc
    #include       et.inc

    __config       _MCLRE_OFF & _CP_OFF & _WDT_ON
     
    cblock         0x08
CYCLES             ; determines how long the note sounds; value of 4 makes the note sound for approx 0.3sec
LENGTH:2           ; time the note will play
NOTE               ; note's T/2 in terms of TMR0 initial value
PB_VAL             ; each bit is a push button switch reading; bit0 has the latest reading; bits are shifted left during each TMR0 rollover
FEN                ; assortment of flag and enable bits for various purposes
COUNTER:3          ; loop counters
COUNTER2:2         ; loop counters
    endc


; ********************************************************************
    ; comment out when not debugging

    ; #define      debug
; ********************************************************************

    ifdef     debug
      #define     secdelay     .1
    else
      #define     secdelay     .15     ; number of seconds after melody has ended before bell push can be pressed again for the chime to play again
    endif

    #define   spkr      GPIO,2    ; output speaker via NPN transistor
    #define   pb        GPIO,0    ; input from bell push


; macro for playing the note
    ; _note is the note's T/2 (half period)
    ; _length is the number of times the note is played 
key macro     _note, _length
    movlf     _note, NOTE
    movlf     _length, LENGTH
    call      PlayNote
    endm

; see wikipedia article "scientific pitch notation" for table of frequency values of notes
; B4 = ti = 493.88 Hz, period = 2.0248 ms, T/2 = 1.0124 us
; C5 = do = 523.25 Hz, period = 1.9111 ms, T/2 = 955.57 us
; D5 = re = 587.33 Hz, period = 1.7026 ms, T/2 = 851.31 us
; E5 = mi = 659.26 Hz, period = 1.5168 ms, T/2 = 758.43 us
; F5 = fa = 698.46 Hz, period = 1.4317 ms, T/2 = 715.86 us
; G5 = sol = 783.99 Hz, period = 1.2755 ms, T/2 = 637.76 us
; A5 = la = 880.00 Hz, period = 1.1364 ms, T/2 = 568.18 us
; B5 = ti = 987.77 Hz, period = 1.0124 ms, T/2 = 506.19 us
; C6 = do = 1046.5 Hz, period = 955.57 us, T/2 = 477.78 us
; D6 = re = 1174.7 Hz, period = 851.28 us, T/2 = 425.64 us
; E6 = mi = 1318.5 Hz, period = 758.44 us, T/2 = 379.22 us

; given internal clock = 4Mhz and timer0 prescaler = 1:4
; timer zero rolls over to zero every 1microsecond x 4 x TMR0 steps
; TMR0 steps = (T/2) / (1 us x 4)
; TMR0 initial value = 256 - TMR0 steps
; the following values should be loaded into TMR0 in order to produce the above notes, respectively
    #define   B4   .256 - .253
    #define   C5   .256 - .239
    #define   D5   .256 - .213
    #define   E5   .256 - .190
    #define   F5   .256 - .179
    #define   G5   .256 - .159
    #define   A5   .256 - .142
    #define   B5   .256 - .127
    #define   C6   .256 - .119
    #define   D6   .256 - .106
    #define   E6   .256 - .95

; in the PlayNote subroutine LENGTH determines how many times the note will play.
; since T/2 for every note is different LENGTH must be different in order for each note to play for the same length of time 
; LENGTH is computed as follows: L1 x T1 = L2 x T2 
; where L1 = length note 1, T1 = period of note 1, L2 = length of note 2, T2 = period of note2
; arbitrarily set LENGTH for E6 = 200 (leave room for a few more higher notes)
    #define   LB4  .75
    #define   LC5  .79
    #define   LD5  .89
    #define   LE5  .100
    #define   LF5  .106
    #define   LG5  .119
    #define   LA5  .133
    #define   LB5  .150
    #define   LC6  .159
    #define   LD6  .178
    #define   LE6  .200

    #define   pb_level  FEN,0     ; voltage level: 1 = hi, 0 = low 

    #define   option_poll_sw b'10000011'    ; value to copy to OPTION register when polling switches
                                            ; pullups enabled, prescaler to timer0, prescaler = 1:16  

    #define   option_chime   b'10000001'    ; value to copy to OPTION register in chime routine 
                                            ; pullups enabled, prescaler to timer0, prescaler = 1:4 

    #define   option_no_wpu  b'11000011'    ; value to copy to OPTION register when polling switches but with weak pull-ups off
 
    #define   tmr_init_val   .11            ; initial value for timer0, given 4Mhz clock and prescaler 1:16, TMR0 will rollover every 3.92ms


    org       0x0
     
    movwf     OSCCAL    ; calibrate the chip 
    goto      initialize
     
; =======================================================================================
;     subroutines
;
;     10F202 can only call subroutines if they are located in first 256 memory locations
; =======================================================================================

; ---------------------------------------------------------------------------------
; Notes are played by switching the speaker on and off at the frequency of the note. 
; the following routine also plays the note for a specified time as determined by LENGTH and CYCLES
PlayNote:
    movff     NOTE, TMR0
    movff     LENGTH, LENGTH+1
    movff     CYCLES, COUNTER
    bsf       spkr
loop_note     
    dnxnotzero     TMR0     
    goto      loop_note
    movff     NOTE, TMR0
    toggle    spkr
    clrwdt
    decfsz    LENGTH+1,f
    goto      loop_note
    movff     LENGTH, LENGTH+1
    decfsz    COUNTER,f
    goto      loop_note
    bcf       spkr
    return     
; ---------------------------------------------------------------------------------
     

; ---------------------------------------------------------------------------------
OptionChime:
    movlw     option_chime     
    option
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
OptionPollSw:
    movlw     option_poll_sw
    option
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
; approximately one second delay
Delay_onesec:
    clrf      COUNTER
    clrf      COUNTER+1
    movlf     .5, COUNTER+2
delay1sec_loop     
    decfsz    COUNTER,f
    goto      delay1sec_loop
    clrwdt
    decfsz    COUNTER+1,f
    goto      delay1sec_loop
    decfsz    COUNTER+2,f
    goto      delay1sec_loop
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
; the following melody is played when bell push is pressed (falling edge detected)
SoundChime:
    call      OptionChime
    movlf     .4, CYCLES

    ; melody from the movie Close Encounters of the Third Kind
    ; --------------------------------------------------------
    key       D6, LD6
    key       E6, LE6
    key       C6, LC6
    key       E5, LE5
    key       G5, .255

    ; time interval when chime has already finished playing  
    ; and button presses are ignored
    movlf     secdelay, COUNTER2
    call      Delay_onesec
    decfsz    COUNTER2,f
    goto      $-2

    call      OptionPollSw
    return
; ---------------------------------------------------------------------------------
     


; =======================================================================================
;     main program
; =======================================================================================

; ---------------------------------------------------------------------------------
initialize:
    movlw     b'1'
    tris      GPIO
    clrwdt
    call      OptionPollSw

    clrf      GPIO                ; speaker off
    movlf     0xFF, PB_VAL        ; all bits hi, pb contacts considered open
    bsf       pb_level            ; level = hi, pb contacts considered open
    movlf     tmr_init_val, TMR0      

; ---------------------------------------------------------------------------------
main:


poll_timer0:
    dnxnotzero     TMR0      ; poll TMR0 until it rolls over
    goto      poll_timer0
    movlf     tmr_init_val, TMR0
    clrwdt
poll_timer0_end:


; poll and check bell push; store value in 8-bit record
; if PB_VAL = 0xFF AND pb_level = 0 then let pb_level = 1, this is a rising edge detect, do nothing
; if PB_VAL = 0x0 AND pb_level = 1 then let pb_level = 0, this is a falling edge detect and chime is sounded
; if PB_VAL > 0 AND PB_VAL < 0xFF then switch is bouncing, so do nothing 
; if PB_VAL = 0xFF AND pb_level = 1 then there is no level change, so do nothing
; if PB_VAL = 0 AND pb_level = 0 then there is no level change, so do nothing 
check_pb:
    rlf       PB_VAL,f       ; discard oldest switch reading
    bcf       PB_VAL,0       ; carry bit will be moved into bit zero so clear bit0 
    dnxbit1   pb
    bsf       PB_VAL,0

    dnxeqlit  PB_VAL, 0xFF
    goto      all_bits_hi
    dnxzero   PB_VAL
    goto      all_bits_lo
    goto      check_pb_end
all_bits_hi:
    dnxbit1   pb_level
    goto      check_pb_end
    bsf       pb_level       ; rising edge detected
    goto      check_pb_end
all_bits_lo:     
    dnxbit0   pb_level
    goto      check_pb_end
    bcf       pb_level       ; falling edge detected
    call      SoundChime
check_pb_end:

    goto      main
; ---------------------------------------------------------------------------------


    end


Door opened, closed, and left open too long detector with corresponding melodies for each condition

The musical candle brought to mind a couple of circuits I designed and built back in 2008 that play a melody when the room door is opened, closed, and left open for too long. They were installed in my sister's home and dad's office. The two circuits vary slightly in that one of them is tamper-proof--cutting the wires coming out of the magnetic contact (switch) will activate the music.

To sense whether the door is open or closed the circuits rely on a common type of magnetic proximity switch called magnetic contact. It's widely used in home security systems. They come in pairs with one block containing a magnet and the other housing a reed switch. The switch may be normally open or normally closed or may be a single-pole double-throw type. In the circuits below I use an NO and an NC type. Check out Honeywell's magnetic contacts catalog to see a wide variety of models. The ones I used had no brand and may have been made in Taiwan.

The circuits that were installed some four years ago:

VR = L7805 +5V voltage regulator 1000mA
MCU = Microchip PIC10F202 microcontroller
OPTO1, OPTO2 = Avago HCPL-817 optoisolator
RS = reed switch (magnetic contact)
Q1 = S9013 NPN transistor
SPKR = 8-ohm. 2-watt, 3-inch speaker

RS and magnet are fastened to the door jamb and door, respectively. When door is closed, magnet actuates  RS and its contacts close. This shunts current to ground. When door is opened, RS contacts open, and current passes through LED of OPTO1 causing optotransistor to turn on. MCU then senses a low on its input pin. With this setup cutting either of the wires to RS will cause OPTO1 to be energized leading to the MCU playing the melody that indicates the door has just been opened. The drawback of this configuration is that it's always dissipating power. To minimize consumption R1 should be as large as possible but should still be able to supply sufficient current to OPTO1's  LED to turn the optotransistor on.

MCU weak pull-ups are enabled in firmware so no external pull-up resistors are necessary on the optotransistors. GP3 is also the MCLR pin and is very sensitive to transients. So pcb track length to the optoisolator should be kept as short as possible.

R3 potentiometer allows adjustment of speaker volume to desired level.

When the door has been left open for a certain amount of time, MCU will start playing a different melody. The tune will be played over and over with a predetermined interval between plays until the door is closed, or until PB is momentarily pressed. PB prevents/stops the melody from playing (closing the door resets the variables and the "door has been left open" melody will play again if the door is left open once more). Firmware is such that RS switching and PB presses are detected and processed only when no melody is playing. 
VR = L7805 +5V voltage regulator 1000mA
MCU = Microchip PIC10F202 microcontroller
OPTO1 = Avago HCPL-817 optoisolator
RS = reed switch (magnetic contact)
Q1 = 2N7000 MOSFET transistor
SPKR = 8-ohm. 250-mW, 2-inch speaker

In the above circuit (labeled "office circuit"), RS is open when magnet is in close proximity (door closed). This cuts off current to the optoisolator and therefore optotransistor is off. RS contacts close when the door is opened. 

As for PB you may be wondering why I chose to use a normally closed momentary contact pushbutton. I actually can't recall exactly. It now strikes me as an odd (and bad) choice. It could've been due to nonavailability at that time of a normally open button. As you will see in the firmware I ran into problems with this set-up. The 5.1K pull-down resistor was too high and the internal pull-ups have to be disabled while GP3 is being read. Rather than having a pull-down resistor I should've just used the internal pull-up and connected the normally-closed PB to ground (instead of VDD). This would of course have necessitated changes in firmware--PB would now be deemed pressed if a high was sensed.

Playing melodies

To make music we need notes and to produce notes we need different frequencies. To create frequencies using a microcontroller and a speaker we can feed the speaker a square wave with a 50% duty cycle and a frequency corresponding to the note we wish to play. The following table summarizes the notes that the firmware below uses. Other frequencies from C0 to B10 can of course be utilized.  

Note Pitch Frequency f (Hz) Period T = 1/f (microseconds) T/2
ti B4 493.88 2024.8 1012.4
do C5 523.25 1911.1 955.57
re D5 587.33 1702.6 851.31
mi E5 659.26 1516.8 758.43
fa F5 698.46 1431.7 715.86
sol G5 783.99 1275.5 637.76
la A5 880.00 1136.4 568.18
ti B5 987.77 1012.4 506.19
do C6 1046.5 955.57 477.78
re D6 1174.7 851.28 425.64
mi E6 1318.5 758.44 379.22

In the firmware implementation the sole timer that exists in the PIC10F202 is used to determine when to switch the speaker on and off to produce the desired note. The 10F202 has a fixed internal clock frequency of 4MHz. With timer 0 prescaler set to 1:4 TMR0 rolls over to zero every 1 µs x 4 x TMR0 counts. Shifting the variables around we have: TMR0 counts = (T/2) / (1 µs x 4). Computed values are given in the following table.

Pitch number of timer 0 counts given clock = 4MHz and prescale = 1:4
B4 253
C5 239
D5 213
E5 190
F5 179
G5 159
A5 142
B5 127
C6 119
D6 106
E6 95

In order to obtain the above counts TMR0 is loaded with an initial value = 256 - TMR0 counts.

By energizing the speaker for T/2 and de-energizing it for T/2 the note of the desired frequency can be played. In creating a melody, however, we also need to control how long each note is played. Thus in the firmware this time duration of play is specified with each note. Since T/2 varies for every note the duration of play must vary accordingly as well in order for each note to play for the same length of time. This is computed as follows:

L1 x T1 = L2 x T2

where
L1 = play duration of note 1
T1 = period of note 1
L2 = play duration of note 2
T2 = period of note2

In the firmware E6 is arbitrarily set to 200. This leaves some room for a few more higher notes that may be necessary for other tunes. Table below lists the computed play durations.

Pitch Play duration of each note (E6 used as standard and arbitrarily set to 200)
B4 75
C5 79
D5 89
E5 100
F5 106
G5 119
A5 133
B5 150
C6 159
D6 178
E6 200

In the PlayNote subroutine variable LENGTH determines how many times a note plays.

Firmware

Since the firmware for the two circuits above are almost the same except for the melodies they play, I just had one program and had preprocessor defines to determine which part of the code would be assembled. By commenting out either #define home or #define office the desired firmware can be assembled.

The time duration that a note is played is implemented through the variable LENGTH, or in lieu of that by using a literal, and through the variable CYCLES. Pauses (silence) between notes when necessary are implemented using a 100ms delay routine.

Because there's only one timer, timer 0 is used for both playing the notes and for polling of the switches. The prescale values are different for the two applications and so OPTION register is edited accordingly when switching between the two. For the office circuit OPTION is also updated when weak pull-ups have to be temporarily disabled when the pushbutton is being polled.

Firmware debounces all switch readings. The last eight readings are stored in one 8-bit variable and the switch is deemed not bouncing if the the variable is equal to 0x00 or 0xFF. Any other value means the switch is bouncing. Upon detection of a falling edge MCU plays one sequence of musical notes, and on detection of rising edge a different melody, to distinguish between opening and closing of the door.  

I coded the firmware in PIC assembly using Microchip's MPLAB IDE. You'll probably be perplexed by the code since it uses a lot of homebrew defines and macros. On my computer they're in the include file "et.inc" and thus the reason for the #include    et.inc.



; ********************************************************************************
;
; DOOR OPENED, CLOSED, AND LEFT OPEN TOO LONG DETECTOR 
; WITH CORRESPONDING MELODIES FOR EACH CONDITION   
; October 2008
;
; MPLAB IDE
;  
; ********************************************************************************

    processor 10F202
    #include p10F202.inc
    #include et.inc

    __config _MCLRE_OFF & _CP_OFF & _WDT_OFF
 
    cblock 0x08
COUNTER:3     ; loop counters
CYCLES        ; determines how long the note sounds; value of 4 makes the note sound for approx 0.3sec
LENGTH:2      ; number of times a frequency (note) will play so that all frequencies will play with the same time duration.  
NOTE          ; T/2 in terms of TMR0 initial value
SW_VAL        ; each bit is a reed switch reading; bit0 contains the latest reading,
              ; bit7 the oldest; bits are shifted left during each TMR0 rollover
PB_VAL        ; each bit is a push button switch reading; bit0 has the latest reading;
              ; bits are shifted left during each TMR0 rollover
FEN           ; assortment of flag and enable bits for various purposes
DOOR_OPEN_COUNT:2  ; counts number of elapsed 4.08ms, when count has exceeded preset level, 
                   ; chime sounds alerting door open condition 
    endc


 
    ; **********************************************
    ; !! SET THE CORRECT DEFINES !! 
    ; is firmware for office or for home?

    #define   office

    ; #define   home

    ; **********************************************

    ; **********************************************
    ; change the following constants if need be
 
    ; number of seconds door is open before door-open-too-long chime plays
    #define   open_time .30 
 
    ; time interval in seconds between door-open-too-long chime 
    ; make sure this value is less than open_time since it will be deducted from it 
    #define   alert_interval .20 
 
    ; **********************************************
 

    #define   spkr      GPIO,2    ; output speaker
    #define   reedsw    GPIO,0    ; input from reed switch / optoisolator
    #define   pb        GPIO,3    ; momentary N.O. push button; 
                                  ; pressing pb prevents/stops door-open-too-long melody from playing 


    ; macro for playing the note
    ; _note is the note's T/2 (half period)
    ; _length is the number of times the note is played 
key macro     _note, _length
    movlf     _note, NOTE
    movlf     _length, LENGTH
    call      PlayNote
    endm


; see wikipedia article "scientific pitch notation" for table of frequency values of notes
; B4 = ti = 493.88 Hz, period = 2.0248 ms, T/2 = 1.0124 us
; C5 = do = 523.25 Hz, period = 1.9111 ms, T/2 = 955.57 us
; D5 = re = 587.33 Hz, period = 1.7026 ms, T/2 = 851.31 us
; E5 = mi = 659.26 Hz, period = 1.5168 ms, T/2 = 758.43 us
; F5 = fa = 698.46 Hz, period = 1.4317 ms, T/2 = 715.86 us
; G5 = sol = 783.99 Hz, period = 1.2755 ms, T/2 = 637.76 us
; A5 = la = 880.00 Hz, period = 1.1364 ms, T/2 = 568.18 us
; B5 = ti = 987.77 Hz, period = 1.0124 ms, T/2 = 506.19 us
; C6 = do = 1046.5 Hz, period = 955.57 us, T/2 = 477.78 us
; D6 = re = 1174.7 Hz, period = 851.28 us, T/2 = 425.64 us
; E6 = mi = 1318.5 Hz, period = 758.44 us, T/2 = 379.22 us

; given internal clock = 4Mhz and timer0 prescaler = 1:4
; timer zero rolls over to zero every 1microsecond x 4 x TMR0 steps
; TMR0 steps = (T/2) / (1 us x 4)
; TMR0 initial value = 256 - TMR0 steps
; the following values should be loaded into TMR0 in order to produce the above notes, respectively
    #define   B4   .256 - .253
    #define   C5   .256 - .239
    #define   D5   .256 - .213
    #define   E5   .256 - .190
    #define   F5   .256 - .179
    #define   G5   .256 - .159
    #define   A5   .256 - .142
    #define   B5   .256 - .127
    #define   C6   .256 - .119
    #define   D6   .256 - .106
    #define   E6   .256 - .95

; in the PlayNote subroutine LENGTH determines how many times the note will play.
; since T/2 for every note is different LENGTH must be different in order for each note to play for the same length of time 
; LENGTH is computed as follows: L1 x T1 = L2 x T2 
; where L1 = length note 1, T1 = period of note 1, L2 = length of note 2, T2 = period of note2
; arbitrarily set LENGTH for E6 = 200 (leave room for a few more higher notes)
    #define   LB4  .75
    #define   LC5  .79
    #define   LD5  .89
    #define   LE5  .100
    #define   LF5  .106
    #define   LG5  .119
    #define   LA5  .133
    #define   LB5  .150
    #define   LC6  .159
    #define   LD6  .178
    #define   LE6  .200


    #define   sw_level  FEN,0     ; voltage level: 1 = hi, 0 = low 
    #define   override  FEN,1     ; DoorOpenTooLongChime override flag; 1 = override active, 0 = no override  

    #define   option_poll_sw b'10000011'    ; value to copy to OPTION register when polling switches
                                            ; pullups enabled, prescaler to timer0, prescaler = 1:16  

    #define   option_chime   b'10000001'    ; value to copy to OPTION register in chime routine 
                                            ; pullups enabled, prescaler to timer0, prescaler = 1:4 

    #define   option_no_wpu  b'11000011'    ; value to copy to OPTION register when polling switches but with weak pull-ups off
 
    #define   tmr_init_val   .11            ; initial value for timer0, given 4Mhz clock and prescaler 1:16, TMR0 will rollover every 3.92ms
                                            ; 3.92ms was chosen so that in the check_level routine open_time and alert_interval are both 
                                            ; equivalent to seconds; every time DOOR_OPEN_COUNT rolls over, time elapsed = 3.92ms x 256 = 1.004sec 



; =======================================================================================
; =======================================================================================

    org  0x0
 
    movwf     OSCCAL    ; calibrate the chip 
    goto      initialize
 
; =======================================================================================
; subroutines
;
; 10F202 can only call subroutines if they are located in first 256 memory locations
; =======================================================================================

; ---------------------------------------------------------------------------------
; Notes are played by switching the speaker on and off at the frequency of the note. 
; the note is played for a specified time as determined by LENGTH and CYCLES
PlayNote:
    movff     NOTE, TMR0
    movff     LENGTH, LENGTH+1
    movff     CYCLES, COUNTER
    bsf       spkr
loop_note 
    dnxnotzero     TMR0 
    goto      loop_note
    movff     NOTE, TMR0
    toggle    spkr
    decfsz    LENGTH+1,f
    goto      loop_note
    movff     LENGTH, LENGTH+1
    decfsz    COUNTER,f
    goto      loop_note
    bcf       spkr
    return 
; ---------------------------------------------------------------------------------
 

; ---------------------------------------------------------------------------------
OptionChime:
    movlw     option_chime 
    option
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
OptionPollSw:
    movlw     option_poll_sw
    option
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
OptionNoWPU:
    movlw     option_no_wpu
    option
    return
; ---------------------------------------------------------------------------------
 
 
    ; ************************************************************
    ; following subroutines are for office circuit melodies
    #ifdef    office

; ---------------------------------------------------------------------------------
; the following melody is played when door is opened (falling edge detected)
DoorJustOpenedChime:
    call      OptionChime
    movlf     .4, CYCLES

    key       C5, LC5
    key       E5, LE5
    key       B5, .255

    call      OptionPollSw
    return
; ---------------------------------------------------------------------------------
 

; ---------------------------------------------------------------------------------
; the following melody is played when door is closed (rising edge detected)
DoorClosed:
    call      OptionChime
    movlf     .2, CYCLES

    key       A5, LA5
    key       F5, LF5
    movlf     .5, CYCLES
    key       D5, LD5
    key       C5, LC5

    call      OptionPollSw
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
; the following melody is played when door has been left open for too long
DoorOpenTooLongChime:
    dnxbit1   override       ; don't play chime if override button had been pressed
    return  

    call      OptionChime
    movlf     .2, CYCLES
 
    movlf     .3, COUNTER+2
door_open_loop1
    key       C5, LC5
    key       E5, LE5
    key       B5, LB5
    decfsz    COUNTER+2,f
    goto      door_open_loop1

    movlf     .3, COUNTER+2
door_open_loop2
    key       C5, LC5
    key       E5, LE5   
    key       A5, LA5
    decfsz    COUNTER+2,f
    goto      door_open_loop2
   
    key       A5, LA5

    call      OptionPollSw
 
    return
; ---------------------------------------------------------------------------------

    #endif  ; ifdef office
    ; ************************************************************



    ; ************************************************************
    ; following subroutines are for home circuit melodies 
    #ifdef    home

; ---------------------------------------------------------------------------------
; approximately 100 millisecond delay
D100ms
    clrf      COUNTER
    movlf     .130, COUNTER+1
d100ms_loop 
    decfsz    COUNTER,f
    goto      d100ms_loop
    decfsz    COUNTER+1,f
    goto      d100ms_loop
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
; the following melody is played when door is opened (falling edge detected)
; "mary had a little lamb (partial)" melody
DoorJustOpenedChime:
    call      OptionChime
    movlf     .3, CYCLES

    key       B5, .255
    key       A5, LA5
    key       G5, LG5
    key       A5, LA5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, .255

    call      OptionPollSw
    return
; ---------------------------------------------------------------------------------
 

; ---------------------------------------------------------------------------------
; the following melody is played when door is closed (rising edge detected)
DoorClosed:
    ; none to be played
    return
; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------
; the following melody is played when door has been left open for too long
; "mary had a little lamb (complete)" melody
DoorOpenTooLongChime:
    dnxbit1   override       ; don't play chime if override button had been pressed
    return  

    call      OptionChime
    movlf     .3, CYCLES

    key       B5, .255
    key       A5, LA5
    key       G5, LG5
    key       A5, LA5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, .255

    key       A5, LA5
    call      D100ms
    key       A5, LA5
    call      D100ms
    key       A5, .255 
 
    key       B5, LB5
    key       D6, LD6
    call      D100ms
    key       D6, .255

    key       B5, .255
    key       A5, LA5
    key       G5, LG5
    key       A5, LA5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, LB5
    call      D100ms
    key       B5, LB5
    call      D100ms
    call      D100ms

    key       B5, LB5
    key       A5, LA5
    call      D100ms
    key       A5, LA5
    key       B5, LB5
    key       A5, LA5
    key       G5, .255

    call      OptionPollSw
 
    return
; ---------------------------------------------------------------------------------

    #endif    ; ifdef home
    ; ************************************************************



; =======================================================================================
; main program
; =======================================================================================

; ---------------------------------------------------------------------------------
initialize:
    movlw     b'1001'
    tris      GPIO
    call      OptionPollSw

    clrf      GPIO           ; speaker off
    clrf      DOOR_OPEN_COUNT 
    clrf      DOOR_OPEN_COUNT+1
    movlf     0xFF, SW_VAL   ; all bits hi, door considered closed
    bsf       sw_level       ; level = hi, door considered closed
    movlf     0xFF, PB_VAL   ; all bits hi, push button considered unpressed
    bcf       override       ; manual override flag initially off
    movlf     tmr_init_val, TMR0  

; ---------------------------------------------------------------------------------
main:

poll_timer0:
    dnxnotzero     TMR0      ; poll TMR0 until it rolls over
    goto      poll_timer0
    movlf     tmr_init_val, TMR0
poll_timer0_end:

 
; check switch level if zero -- if it is this means that door is open
; if door has been left open for amount of time = open_time then sound the door-open-too-long chime
; and keep sounding the melody every alert_interval amount of time
check_level:
    dnxbit1   sw_level
    goto      check_level_end
    incfsz    DOOR_OPEN_COUNT,f
    goto      check_level_end
    incf      DOOR_OPEN_COUNT+1,f
    dnxltlit  DOOR_OPEN_COUNT+1, open_time
    goto      check_level_end
    call      DoorOpenTooLongChime
    movlf     open_time - alert_interval, DOOR_OPEN_COUNT+1 
check_level_end:


; poll and check reed switch; store value in 8-bit record
; if SW_VAL = 0xFF AND sw_level = 0 then let sw_level = 1, this is a rising edge detect and chime for door-just-closed is played
; if SW_VAL = 0x0 AND sw_level = 1 then let sw_level = 0, this is a falling edge detect and chime for door-just-opened is played
; if SW_VAL > 0 AND SW_VAL < 0xFF then switch is bouncing, so do nothing 
; if SW_VAL = 0xFF AND sw_level = 1 then there is no level change, so do nothing
; if SW_VAL = 0 AND sw_level = 0 then there is no level change, so do nothing 
check_reedsw:
    rlf       SW_VAL,f       ; discard oldest switch reading
    bcf       SW_VAL,0       ; carry bit will be moved into bit zero so clear bit0 
    dnxbit1   reedsw
    bsf       SW_VAL,0

    dnxeqlit  SW_VAL, 0xFF
    goto      all_bits_hi
    dnxzero   SW_VAL
    goto      all_bits_lo
    goto      check_reedsw_end
all_bits_hi:
    dnxbit1   sw_level
    goto      check_reedsw_end
    bsf       sw_level       ; rising edge detected - door has just been closed
    clrf      DOOR_OPEN_COUNT
    clrf      DOOR_OPEN_COUNT+1
    bcf       override
    call      DoorClosed
    goto      check_reedsw_end
all_bits_lo: 
    dnxbit0   sw_level
    goto      check_reedsw_end
    bcf       sw_level       ; falling edge detected - door has just been opened
    call      DoorJustOpenedChime
check_reedsw_end:


; check the override button if it's been pressed
; if door is open (i.e., sw_level = 0) AND pb is pressed (i.e., PB_VAL = 0) 
; then set override flag -- this will prevent DoorOpenTooLongChime from playing 
check_pb: 
    rlf       PB_VAL,f       ; discard oldest switch reading
    bcf       PB_VAL,0       ; carry bit will be moved into bit zero so clear bit0 
 
    ; ************************************************************
    ; the following compensates for a flaw in the circuit:
    ; the 5.1K pulldown resistor for PB is too high, given the 16k to 20Kohm weak pullup of the 10F202;
    ; When N.C. PB is pressed, 5.1K resistor cannot pull close enough to ground for GP3 to detect a low
    ; According to datasheet input low = 0.2Vdd max
    ; Firmware fix of hardware problem:
    ; Turn off the weak pull-up just before PB is read and restore right after the read
    ; solves the hardware problem without affecting anything else. 
    #ifdef     office
    call       OptionNoWPU   ; turn off weak pull ups
    #endif
    ; ************************************************************

    dnxbit1   pb
    bsf       PB_VAL,0

    ; ************************************************************
    #ifdef    office
    call      OptionPollSw   ; restore weak pull ups
    #endif
    ; ************************************************************

    dnxnotzero     PB_VAL
    goto      check_pb_end
    dnxbit0   sw_level
    bsf       override
check_pb_end:

    goto      main
; ---------------------------------------------------------------------------------


 end






; ==============================================================================
; alternative melodies for chime
; ==============================================================================

    ; melody from the movie Close Encounters of the Third Kind
    ; --------------------------------------------------------
;   key       D6, LD6
;   key       E6, LE6
;   key       C6, LC6
;   key       E5, LE5
;   key       G5, .255
;
    ; melody akin to music box dancer
    ; -------------------------------
;   key      C5, LC5
;   key      E5, LE5
;   key      G5, LG5
;   key      C6, LC6
;   key      B5, LB5
;   key      A5, LA5
;   key      G5, .255
 

    ; edwardson-composed melody (part of the melodies used in the door opened/closed routines)
    ; -------------------------------
;   movlf     .2, CYCLES
;   movlf     .3, COUNTER+2
;door_open_loop1
;   key       A5, LA5
;   key       F5, LF5
;   key       D5, LD5
;   decfsz    COUNTER+2,f
;   goto      door_open_loop1
;
;   movlf     .3, COUNTER+2
;door_open_loop2
;   key       A5, LA5
;   key       E5, LE5
;   key       C5, LC5
;   decfsz    COUNTER+2,f
;   goto      door_open_loop2
;
;   movlf     .4, CYCLES
;   key       A5, LA5

Wednesday, June 27, 2012

Musical candle teardown

While in an office supplies store I saw a display rack full of "Singing Candles" and thought it would be interesting to take one apart to see how it works.

I've already taken off the white end cap when I took the picture.


There's a pre-made hole in the candle through which the thin silver strip threads and out the top of the candle. 


The MCU is in the black glob of course. The red ceramic capacitor is 10nF. The negative terminal of the button battery is soldered to the pcb. 




An S9014 NPN transistor drives the speaker. Base current limiting resistor is 4.7K 1/4-watt.




This is the speaker.

Other end of the speaker. It doesn't show in the pics but there's a coil of enamel wire if you peer into the holes.


Tinned pcb tracks after the retaining clip, rectangular metal bridge and metalized strip have been removed


From left to right: retaining clip, rectangular metal bridge, metalized strip, pcb



The narrow silver strip consists of two electrically conductive metalized outer layers and a nonconducting layer sandwiched in between. Measured resistance at DMM probe-to-probe distance of 1cm is 9 ohms, and ~60 ohms at 7 cm.

The following is a stylized diagram showing how the strip is positioned on the pcb tracks. Thickness of the strip and tracks are exaggerated of course just for illustration purposes.
The rectangular nickel-plated bridge sits on top of the strip and one of the tinned pcb tracks, providing an electrical pathway from the top layer of the strip to the other track. However, because of the insulating layer, the track on which the strip rests is isolated from the other track. A retaining clip secures the bridge and strip to the pcb.

As you might have seen in the first couple of images above the blister pack has a "press here" sticker that allows the would-be buyer to test the candle before purchasing. After opening the package I took a look at what they had under the sticker and found there's a 1-cm diameter aluminum foil glued beneath the plastic film and another on the cardboard. Ohmmeter check confirms they're zero ohm. There's one or two millimeters of airspace between the top and bottom foil. When the user pushes down on the plastic film the two foils make contact. At the same time they pinch the exposed silver strip. This causes electrical contact between the two outer layers of the strip and immediately wakes up the MCU thus causing the music to start playing. Only a momentary contact is necessary and the MCU will play the song three times.

Here's the musical candle in action.



Here's what I think happens: When the candle is lit and the flame begins to burn the strip, carbon forms and provides a relatively low resistance between the metalized layers, thus causing a switching action which wakes the MCU up (via an interrupt-on-change of sorts). As long as the candle is lit the MCU plays the song over and over.

Interestingly when the flame is snuffed out the MCU stops playing (not immediately but only after playing the song three in a row after the last check of the pin monitoring the "strip switch"). This implies that the resistance of the carbon and other substances in the burnt strip markedly increases when it cools down. Reigniting the wick quickly brings the temperature back up and causes the resistance to decrease once again thus triggering the MCU.

Pics of strip after candle had been lit




Monday, June 25, 2012

Measuring liquified petroleum gas (LPG) levels

Before anything else, I forgot to mention in in the LPG detection test that I took temperature measurements of the TGS2610 sensor of the LPM2610 module using a Fluke 87V and its thermocouple. After the module had been powered up for many minutes I held the thermocouple junction to the case for a couple of minutes and obtained a maximum reading of 52.8°C. Ambient temperature was 32.7°C. I am not confident with this case temperature measurement. Contact area between thermocouple and case is small and manually holding the thermocouple did not of course properly secure it to the sensor. Taping or even gluing it in place may be a more reliable way of taking a measurement.

As I mentioned in Detecting LPG, it occurred to me that we can use the Figaro LPM2610 module to not just detect the presence of LPG when it's >= 10% of its lower explosive limit (LEL), we may also be able to use its analog outputs to measure the amount of gas in the air.

Before we jump in, a couple of caveats relevant to measuring gas levels. The TGS2610 datasheet says the sensor's typical detection range is between 500 to 10,000 ppm. Given butane LEL = 18,000 ppm, the sensor's detection range translates to 2.8% to 55.5% LEL. I want to be able to measure all the way down to 1% but it looks like this may be asking too much from this sensor. Then again we can take our chances.

Secondly, the sensor is hardly accurate as can be seen in this Figaro diagram of the module's expected performance:


The note below the chart says 
When using LPM2610, typical alarm tolerances for 10% LEL of LP gas such as those shown in the figure above can be expected. However, in actual usage, alarm thresholds may vary since the threshold is also affected by such factors as the tolerances of test conditions and heat generation inside the gas detection enclosure. 
The tolerances are pretty wide: 5 to 20% LEL. Not at all encouraging.

Given these characteristics, using the LPM2610 to measure gas levels seems almost foolhardy. But in quixotic fashion we shall nevertheless proceed--if only as a mathematical exercise.


To start off here's a list of definitions of terms and variables we'll be using:
  • Standard values are sensor resistance and output voltage at an ambient temperature of 20°C. To standardize a voltage is to apply a corrective factor provided by VREF so that the voltage value becomes that which would be expected if ambient = 20°C.
  • Actual values are LPM2610 resistance and output voltage values without consideration to ambient temperature.
  • VREF is the output of the LPM2610 voltage divider consisting of a thermistor and various fixed-value resistors. VREF is theoretically = VDD/2 when ambient temperature = 20°C.
  • VOUT(act) = actual output of the LPM2610 voltage divider consisting of (1) the TGS2610 sensor whose resistance varies with amount of LPG present and (2) a fixed value load resistor RL.
  • RS(act) = actual sensor resistance in the LPM2610
  • RS(std) = standard sensor resistance  
  • VOUT(std) = standard voltage output given RS(std) 
  • RL = load resistance in series with RS 

Preliminary equations

Take a look at the LPM2610 circuit diagram above. Given the voltage divider RS and RL, output voltage VOUT is given by the equation below. VOUT in this case is the actual VOUT and RS is the actual RS.



Given our definitions of standard sensor resistance and output voltage it follows that:



The LPM2610 module is calibrated at an ambient temperature of 20°C where VREF = VDD/2. As ambient temperature deviates from 20°C, VREF is adjusted by the module's thermistor. In our equations we will need to determine VOUT(std). And so we need to adjust VOUT(act) by a certain factor. This correction factor is as follows:



Derivation of RS(std) Equation

We need to find an equation that describes sensor resistance in terms of % LEL. Since Figaro provides no equation we need to derive it from sensor data. Fortunately, Application Note for LP Gas Detectors using TGS2610 lists RL = RS for various %LEL and TGS2610 ID numbers (ID# corresponds to specific sensor resistances). For example the Figaro LPM2610 modules that I got have sensors with ID#15. From the AN here are its characteristics:


Specification of TGS2610 with ID#15
% LEL RS(std) in ohms
5 3830
10 2740
15 2260
20 2000

To transform that table of data into a formula we perform regression analysis. If we plot the above we end up with a nonlinear curve, one which shows the characteristics of a power law curve where y = axb. Unfortunately, the spreadsheet I'm using can only do linear regression. Fortunately, we can work around this limitation. If we plot the values in the above table on a log-log paper (where x and y axes are graduated in a logarithmic scale), we get a nearly straight line and not a curve. Therefore, we can take the log(x) and log(y) and plug those in the linear regression equations. Solving for the log of both x and y, i.e., of %LEL and RS(std), we obtain the following:

log(%LEL) log(RS(std))
0.698970004336019 3.58319877396862
1 3.43775056282039
1.17609125905568 3.3541084391474
1.30102999566398 3.30102999566398

We let the spreadsheet perform a linear regression using the above logarithmic data and it magically spews out the following:

b = x coefficient = -0.471269891979091
c = constant = 3.91103846203761
r squared = 0.999483957661255

r2 is the square of the correlation coefficient. Since it's practically = 1 it implies that the correlation between data and the equation is extremely high. Keeping in mind that we had used the logarithms of x and y, the regression equation is:

log(y) = b*log(x) + c

Here's a plot of both the datasheet values (red points) and computed line using an online linear regression calculator:



Taking the antilog of both sides of the equation above gives us:

y =  10[b*log(x) + c]

Table below shows the computed RS(std) and the %error from values given in the datasheet.

RS(std) using regression equation
% LEL RS(std) in ohms error in %
5 3816 -0.36
10 2753 0.47
15 2274 0.62
20 1986 -0.72


Recall that I performed a linear regression because my spreadsheet does not have the capability of doing a power regression. However, there are various online resources that we can use, obviating the need to get the logarithms of x and y. Plugging the data from the very first table above into a power regression calculator gives us the following results:



a = 8147.7643914991
b = -0.471269892
correlation coefficient = 0.9998059883

So the power equation is

y =  8147.7643914991x-0.471269892

Now, since both the linear and power equations yield the same y we can equate the two:

axb = 10[b*log(x) + c]

Taking the log of both sides we obtain:

log(a) + b*log(x) = b*log(x) + c

log(a) =  c

Indeed, if we take the log of  8147.76 we get 3.911

Bear in mind that the above equations apply only to TGS2610 ID#15. Nevertheless, upon subjecting ID#10 and #20 to power regression and computing for VOUT(std) as a percentage of VDD I obtained results which confirmed the guess that they'd all show practically the same characteristic voltage outputs given their respective voltage divider resistor values. Click the "Vout as %Vdd" tab at the bottom of the following spreadsheet to see the computed values. Take a look at the columns with blue headers. The percentages for each of the three ID# are practically the same for every %LEL.

Determining % LEL


There's at least two ways--when using a microcontroller--to determine the % LEL: direct computation and use of a look-up table.

I. Direct Computation

The direct way is to derive a formula which the microcontroller can use to calculate the amount of gas detected (as % LEL).

Given the voltage divider in the TGS2610 sensor and from our definition of standard values we infer that:



From the above power regression we have:



Therefore:


Given VOUT(act) we can obtain VOUT(std) by applying the corrective factor:




Rearranging the terms, substituting and solving for x:















RL is a constant and will be contingent upon the particular TGS2610 unit being used (its ID# will determine what RLwill be).


II. Look-up Table

The other method--which is less CPU intensive--would be to compare the standardized VOUT as a fraction of VDD with the values in a look-up table to determine % LEL (eg., to the nearest %). The look-up table is populated with values computed using the following formula, where x = %LEL and a and b derived from power regression:



As we already obtained in the direct computation method:



We then divide VOUT(std) by VDD to obtain a unitless fraction:



So all the MCU has to do is evaluate the relatively simply equation on the right and then look for the closest value in the table to determine %LEL.

Monday, June 18, 2012

June 6, 2012 Venus transit pics

Another off topic entry. Should've uploaded these images on the day I took them. Transit began about an hour after sunrise and ended noon. It was a depressingly cloudy day and I had to keep monitoring the skies and took pics whenever there was a break. Never actually had absolutely clear skies--haze and high altitude cirrus clouds were practically a fixture. The first five images below were obviously taken even with clouds in the way (you hear Joni Mitchell singing Both Sides Now?). Well, at least I got something. It is a once-in-a-lifetime event, you know.

Camera used was an el cheapo Canon Powershot A480 point and shoot. I used one green-tinted welding goggle glass pane during the first couple of hours but doubled it by around 9am. I just held the glass in front of the lens. Camera was for most part mounted on a tripod. ISO rating was initially set to 80 but in most of the shots it was in auto. Maximum optical + digital zoom using spot metering with the sun centered in the metering zone. Bracketing was performed throughout the shoot--I incremented/decremented exposure compensation by one notch (1/3 stop). Shot some 250 frames, 80% of which are either severely overexposed or underexposed, blurry from camera shake (two welding glass plates is so effective in limiting sunlight that shutter speed went as low as 1/60 sec), lack of focus or hazy due to cloud cover. Venus of course crossed the sun in a straight line but because I moved the camera the planet seems to be all over the place.