frequency counter (using timer and CCP)

General discussion on mikroBasic.
Post Reply
Author
Message
munchy
Posts: 20
Joined: 19 Mar 2009 19:49

frequency counter (using timer and CCP)

#1 Post by munchy » 27 Mar 2009 19:07

Hi everybody,

I have some code that I am trying to get working and would like some help...

It's basically a frequency counter. I want to be able to read low frequencies (0 - ~50Hz) input on CCP1 pin, <C.2>. I have a CCP interrupt counting pulses on C.2 and I am using timer 0 (8 bit) counting up and overflowing, incrementing a tally. When the tally gets to a number corresponding to 1 second, HERTZ is updated with the number of pulses (REV) counted. Everything is zeroed and the 1 second cycle starts again. Is it output to a GLCD and refreshed every 50ms.

I am using a PIC16F877, 8Mhz external clock and an EasyPic4 development board.

All I'm getting is 0 as the HERTZ output, the timer 0 doesn;t seem to be counting and hence tally doesn't get updated, hence HERTZ never gets to see REV. At the moment I am just pressing the button on the development board to simulate a typical frequency of 2-5 hertz.

Any suggestions?

Code: Select all

Program frequency_counter

'=============================================================

DIM
       REV     as Byte        ' total pulse counts
       HERTZ   as Byte
       RPMStr  as String[5]
       Tally   as integer

'=============================================================
Sub Procedure INTERRUPT()

       If PIR1.CCP1IF = 1 Then     ' input pulse counter, ignore CCPR1
          REV = REV + 1                 ' accumulate pulses
          PIR1.CCP1IF = 0          ' prepare for next CCP interrupt
       End If

       If INTCON.T0IF = 1 Then     'test overflow, 256 bits
          Tally = Tally + 1               'increment tally
          INTCON.T0IF = 0           ' set T0IE, clear T0IF
          TMR0 = 0


             if tally = 31250 Then     'test for 1 second, (1 / (1/8Mhz * 256))
                HERTZ = REV
                REV   = 0
                tally = 0
             end if

       end if

End sub

'=============================================================
Sub Procedure CCP_SETUP()

       TRISC.2      = 1                     ' CCP1 pin is input
       PIE1.CCP1IE  = 1                     ' enable CCP1 Interrupt
       PIR1.CCP1IF  = 0                     ' Clear CCP1 INTERRUPT Flag
       INTCON = 11100000                    ' enable TMRO interrupt

       OPTION_REG.T0CS = 1                  ' bit 5 TMR0 Clock Source Select bit:0=Internal Clock (CLKO) / 1=Transition on T0CKI pin
       OPTION_REG.T0SE = 0                  ' bit 4 TMR0 Source Edge Select bit: 0=low/high / 1=high/low
       OPTION_REG.PSA  = 1                  ' bit 3 Prescaler Assignment bit: 0=Prescaler is assigned to the Timer0
       OPTION_REG.PS2  = 0                  ' bits 2-0  PS2:PS0: Prescaler Rate Select bits
       OPTION_REG.PS1  = 0
       OPTION_REG.PS0  = 0
       TMR0 = 0                             ' preset for timer register


end sub

'=============================================================

Sub Procedure GENERAL_INIT()
       ADCON1 = 0                           ' disable analogs
       TRISA  = 0
       TRISB  = 0
       TRISC  = 1
       TRISD  = 0
end sub

'=============================================================

Main:

       General_Init()
       CCP_Setup()

       Glcd_Init(PORTB, 0, 1, 2, 3, 5, 4, PORTD)
       Glcd_Fill(0xAA)
       Glcd_Fill(0x00)
       Glcd_Set_Font(@SYSTEM3x6, 3, 6, 32)

       REV = 0
       HERTZ = 0

       while TRUE
          Delay_ms(50)                             ' update LCD every 50ms
          WordToStr(HERTZ, RPMStr)                 ' convert for LCD
          glcd_write_text("RPM",20,0,1)
          glcd_write_text(RPMStr,65,1,1)
       wend

end.
Thanks, Andrew

nots
Posts: 11
Joined: 09 May 2007 21:24

This is what im trying to do also!

#2 Post by nots » 01 Apr 2009 12:35

But i have only started to learn about the timer and interrupt but if you got this working it would help me greatly in understanding these functions.

I hope you get an answer to this.

Br Nots

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#3 Post by munchy » 01 Apr 2009 14:35

Hi nots,

Thanks for the reply - I haven't got any further with this. I can get the flashing LED program to work from the Mikro CD (it uses TMR0 and interrupts) but still haven't been able to adapt it for my project.

I'm going to be working on this over the next couple of days, hopefully I will figure it out or someone will help...

Thanks! Andrew

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#4 Post by munchy » 01 Apr 2009 16:13

OK, so I'm confused... I have taken the example from the Mikro CD that uses TMR0 to flash the PORTB LED's once a second. It works fine on my PIC16F877. Since I want to develop this code and mess around with other interrupts, I wanted to understand exactly what was going on.

I have changed the one line INTCON = $xx and OPTION_REG = $xx from HEX to their individual set bits, but it seems I have broken the interrupt somehow. I have gone through the data sheet but can't see what I am doing wrong... This should be simple!!!

Code from Mikro CD (WORKS, with hex values)

Code: Select all

program Timer0

dim counter as word

sub procedure interrupt
   inc(counter)               ' Increment value of counter on every interrupt
   TMR0   = 96
   INTCON = $20              ' Set T0IE, claer T0IF
   'INTCON.T0IE = 1            ' don't mask TMR0 interrupt, (set T0IE), bit 5
   'INTCON.T0IF = 0            ' Clear T0IF the TMR0 interrupt, ready for next interrupt, bit 2

end sub


main:
  OPTION_REG = $84           ' Assign prescaler to TMR0
  'OPTION_REG.PS2 = 1          ' Assign prescaler to 1:32, bit 2
  'OPTION_REG.NOT_RBPU = 1     ' assign PortB pullup bit, bit 7

  TRISB = 0                   ' PORTB is output
  PORTB = $FF                 ' Initialize PORTB
  counter = 0                 ' Initialize counter
  TMR0 = 96
  INTCON = $A0               ' Enable TMRO interrupt
  'INTCON.GIE  = 1             ' enable global interrupts, GIE = 1, bit7
  'INTCON.T0IE = 1             ' don't mask TMR0 interrupt, (set T0IE), bit 5
  'INTCON.T0IF = 0             ' clear TOIF the TMR0 interrupt, ready for the next interrupt

  while TRUE
    if counter = 400 then     ' if counter is 400, then toggle PORTB leds and reset counter
      PORTB  = not(PORTB)
      counter = 0
    end if
  wend

end.
My modified code (DOESN'T WORK, with individual bits set)

Code: Select all

program Timer0

dim counter as word

sub procedure interrupt
   inc(counter)               ' Increment value of counter on every interrupt
   TMR0   = 96
   'INTCON = $20              ' Set T0IE, claer T0IF
   INTCON.T0IE = 1            ' don't mask TMR0 interrupt, (set T0IE), bit 5
   INTCON.T0IF = 0            ' Clear T0IF the TMR0 interrupt, ready for next interrupt, bit 2

end sub


main:
  'OPTION_REG = $84           ' Assign prescaler to TMR0
  OPTION_REG.PS2 = 1          ' Assign prescaler to 1:32, bit 2
  OPTION_REG.NOT_RBPU = 1     ' assign PortB pullup bit, bit 7

  TRISB = 0                   ' PORTB is output
  PORTB = $FF                 ' Initialize PORTB
  counter = 0                 ' Initialize counter
  TMR0 = 96
  'INTCON = $A0               ' Enable TMRO interrupt
  INTCON.GIE  = 1             ' enable global interrupts, GIE = 1, bit7
  INTCON.T0IE = 1             ' don't mask TMR0 interrupt, (set T0IE), bit 5
  INTCON.T0IF = 0             ' clear TOIF the TMR0 interrupt, ready for the next interrupt

  while TRUE
    if counter = 400 then     ' if counter is 400, then toggle PORTB leds and reset counter
      PORTB  = not(PORTB)
      counter = 0
    end if
  wend

end.
Also, does anyone know why my D0 LED is flashing with the first program as well as PORTB??

The only thing I can think of is that some bits need to be set to 0, but I don't see from the data sheet which ones. Shouldn't all data bits be set to zero anyway on reset?

Thanks in advance, Andrew

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#5 Post by munchy » 01 Apr 2009 22:07

It's working! sort of...

I found that I needed to add the line

Code: Select all

CCP1CON = %00000101 
to enable capture mode on every rising edge.

My code is now:

Code: Select all

'==============================================================
'| frequency counter that measures pulses input on C2 and     |
'| displays number of HERTZ on graphical LCD display.         |
'| PIC16F877 (40 pin)                                         |
'| Easypic4 development board                                 |
'| 8Mhz external oscillator                                   |
'|                                                            |
'| prescaler set to 32 so internal clock/32 and TMR0          |
'| increments every 31us. If TMR0 init at 96, overflow        |
'| occurs in (256-96)*31us = 5ms                              |
'| when tally = 200, total time elapsed = 200*5ms = 1 sec     |                           |
'==============================================================

Program frequency_counter

'=============================================================

DIM
       REV      as word         'total pulse counts
       HERTZ    as word         'number of HERTZ (pulses per second)
       HertzStr as String[5]    'Hertz to output to LCD
       Tally    as word         'used for number of overflows of TMR0

'=============================================================

Sub Procedure INTERRUPT()

       If PIR1.CCP1IF = 1 Then     'test if INTERRUPT was pulse on pin C2
          PIR1.CCP1IF = 0          'reset CCP INTERRUPT and prepare for next INTERRUPT
          inc(REV)                 'accumulate pulses
       End If

       If INTCON.T0IF = 1 Then     'test if INTERRUPT was TMR0 overflowing, 256 bits
          inc(Tally)               'increment tally
          TMR0 = 96                'reset TMR0 t0 96 (see calc in header)
          INTCON.T0IF = 0          'reset TMR0 INTERRUPT flag and prepare for next INTERRUPT
          INTCON.T0IE = 1          'set T0IE, enables TMR0 INTERRUPT (needed?)

             if tally = 200 Then   'test for 1 second
                HERTZ = REV
                REV   = 0          'reset number of pulses ready for next second
                tally = 0          'reset Tally to count up for next second
             end if

       end if

End sub

'=============================================================

Sub Procedure CCP_TMR0_SETUP()
       intcon  = %11100000        'enable global <7>, Peripheral <6> and TMR0 <5> interrupts
       TRISC.2 = 1                'CCP1 pin is input
       PIE1    = %00000100        'enable CCP1 Interrupt
       PIR1    = %00000000        'Clear CCP1 INTERRUPT Flag

       Tally = 0                  'set Tally to 0
       TMR0 = 96                  'preset for timer register (see calc in header)

       OPTION_REG = %00000100     'set prescaler for 1:32 (see calc in header)
       CCP1CON    = %00000101     'CCP capture mode, every rising edge

end sub

'=============================================================

Sub Procedure GENERAL_INIT()
       TRISA  = 0
       PORTA  = 0
       TRISB  = 0
       PORTB  = 0
       TRISC  = 1
       PORTC  = 0
       TRISD  = 0
       PORTD  = 0
end sub

'=============================================================

Main:

       General_Init()
       CCP_TMR0_Setup()

       Glcd_Init(PORTB, 0, 1, 2, 3, 5, 4, PORTD)
       Glcd_Fill(0xAA)
       Glcd_Fill(0x00)
       Glcd_Set_Font(@SYSTEM3x6, 3, 6, 32)

       REV = 0
       HERTZ = 0

       while TRUE
          Delay_ms(50)                               'update LCD every 50ms
          wordToStr(HERTZ, HertzStr)                 'convert HERTZ for LCD
          glcd_write_text("HERTZ",20,0,1)            'write text to LCD
          glcd_write_text(HertzStr,65,1,1)           'write data to LCD
       wend

end.
I have tested this with a function generator and it behaves very well except that it reads half of what the function generator says (I have set function generator to 1khz pulsed waveform, LCD reads ~516 etc.)

I guess I could change the tally line to read

Code: Select all

if tally = 100 then

but I would like to understand why my timings are wrong. Any suggestions?

Thanks, Andrew

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#6 Post by munchy » 01 Apr 2009 22:17

Apologies, that should be

Code: Select all

if tally = 400 then
I've tested it up to 30khz, i get 30800 Hertz on the display. Maybe with a bit of tweaking I could get this a little more accurate. Above that and the interrupts get too much for the PIC...

Andrew

xor
Posts: 5465
Joined: 18 May 2005 00:59
Location: NYC
Contact:

#7 Post by xor » 01 Apr 2009 23:17

Low frequencies require a longer pulse sampling period to assure accuracy.

Create a One Second Period using TIMER0. Use SEARCH and you can see an example method I demonstrated for making an RTC with a PIC with TImer0. This is the only interrupt required.
http://www.mikroe.com/forum/viewtopic.php?t=10057

Count external pulses using the TIMER1 TCKI input pin during the 1 second period. Read Timer1 at the end of 1sec and voila! that value is your HERTZ. No special calcs necessary, and no interrupt conflicts.

Good for a range of 0 Hz. to 65536 Hz. Actually...you can easily read up to double that speed by checking the Timer1 rollover flag at the end of 1sec. 0 Hz to 131KHz. TMR1IF set means that the first 65535 counts completed.

And your PIC will behave nicely while working on other program tasks.
[color=darkred][b]xor[/b][/color]
[url=http://circuit-ed.com]CircuitED -[/url]

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#8 Post by munchy » 02 Apr 2009 13:36

xor,

Thanks for the reply. I will try and get the timer1 TCKI working as it would save an interrupt. To be honest, this is for a kind of wind turbine so frequency is very low indeed, but it's always good to see how the technology reacts in extremes.

For those that are interested, I have found tweaking the value that TMR0 resets to (and hence the sampling period) makes this very accurate indeed. I found that changing from 96 to 101 (both lines) gives me exactly what the signal generator is putting out, from 0 - 30khz.

The problem I have on these forums is that the programs seem to be very specific - people use hex values instead of setting specific bits - which means that I have no idea which bits are supposed to be 0 and which bits don't really matter...

Do you have any idea why this works:

Code: Select all

OPTION_REG = $84
but this doesn't?

Code: Select all

OPTION_REG.PS2 = 1          
OPTION_REG.NOT_RBPU = 1
Andrew

xor
Posts: 5465
Joined: 18 May 2005 00:59
Location: NYC
Contact:

#9 Post by xor » 02 Apr 2009 17:32

Keep in mind that timers increment to their max count and then rollover to 0... and then continue incrementing from lower to higher numbers. Essentially, you are working with negative values. If you want TIMER0 to rollover after 100 ticks you load it with 256-100, or 156.

When using a timer with interrupts you need to remember that the timer was still running and incrementing the whole time for the jump to interrupt along with ISR context saving cycles that the program was performing. For accuracy of timings you must compensate for these used up cycles in your calculations.

If HEX is confusing convert it to a binary value and you will see the bit values more easily. If the compiler doesn't have the bit id's defined, you can use SYMBOL to define them yourself:

Code: Select all

symbol NOT_RBPU = 7
symbol PS0   = 0
symbol PS1   = 1
symbol PS2   = 2

OPTION_REG = $84
OPTION_REG = %10000100     ' portb pullups disabled ; prescale=32

OPTION_REG.NOT_RBPU = 1
OPTION_REG.PS2           = 1

munchy wrote:xor,

Thanks for the reply. I will try and get the timer1 TCKI working as it would save an interrupt. To be honest, this is for a kind of wind turbine so frequency is very low indeed, but it's always good to see how the technology reacts in extremes.

For those that are interested, I have found tweaking the value that TMR0 resets to (and hence the sampling period) makes this very accurate indeed. I found that changing from 96 to 101 (both lines) gives me exactly what the signal generator is putting out, from 0 - 30khz.

The problem I have on these forums is that the programs seem to be very specific - people use hex values instead of setting specific bits - which means that I have no idea which bits are supposed to be 0 and which bits don't really matter...

Do you have any idea why this works:

Code: Select all

OPTION_REG = $84
but this doesn't?

Code: Select all

OPTION_REG.PS2 = 1          
OPTION_REG.NOT_RBPU = 1
Andrew
[color=darkred][b]xor[/b][/color]
[url=http://circuit-ed.com]CircuitED -[/url]

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#10 Post by munchy » 02 Apr 2009 18:15

xor wrote: If HEX is confusing convert it to a binary value and you will see the bit values more easily.
:) You miss my point. I can easily convert Hex to binary (MikroBasic even has the handy tool on the 'converter' tab).

My point is when people use hex they are setting the full byte to a set of '1's and '0's. It would be exactly the same as using the binary representation. Without proper comments, I don't know if some of those are meant to be '0's or it doesn't matter. I was trying to set individual bits to understand better what was going on but it doesn't seem to work.

Why would setting the full byte at one time work and setting certain individual bits of that byte one after the other not?

BTW, I don't get any errors when I compile so I am pretty sure the bit id's are defined. I had to look up OPTION_REG.NOT_RBPU to get the right bit name for instance...

<edit>

Argh! I have tried to add in a temperature measurement to this program but have now realised that the DS1820 can't cope with interrupts! I get a temperature display but it 'flickers' every now and again as TMR0 and/or CCP are interrupting... I can obviously set incon.gie = 0 at the beginning of the temperature read - and then set it to 1 at the end but then I miss pulses...

This PIC stuff is just one step forward and three back...

xor
Posts: 5465
Joined: 18 May 2005 00:59
Location: NYC
Contact:

#11 Post by xor » 02 Apr 2009 21:43

munchy wrote:My point is when people use hex they are setting the full byte to a set of '1's and '0's. It would be exactly the same as using the binary representation. Without proper comments, I don't know if some of those are meant to be '0's or it doesn't matter. I was trying to set individual bits to understand better what was going on but it doesn't seem to work.

Why would setting the full byte at one time work and setting certain individual bits of that byte one after the other not?
You know what you did, but you don't know what you didn't do.

The smart programmer wrote to the whole register. In fact, that programmer can tell you the exact state of each control bit in the register when he finished. And further, you can't. You only know the state of 2 bits of the 8 and assume something (wrongly) about the rest.

Look in your datasheet at TABLE 2-1 and find OPTION_REG. Move to the column that shows the value of the register at POR (power-on reset). It would appear that your settings for 2 bits only was an act of futility. You changed nothing at all.... :?
[color=darkred][b]xor[/b][/color]
[url=http://circuit-ed.com]CircuitED -[/url]

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#12 Post by munchy » 02 Apr 2009 23:20

:oops:

That is exactly what I needed to know... I hadn't seen that column of the data sheet and (stupidly) assumed that the registers would be set to 0 on startup/reset.

I really appreciate you taking the time to answer these questions xor. Thank you!!

Andrew

p.s. I have got round the interrupt problem by essentially doing one thing at a time. I read the frequency one second, then read temp (ignoring all intterupts) and reset rev, tally and TMR0 at the end of the temp read procedure. As long as there is >1 sec between calling the temp read procedure everything is happy.

xor
Posts: 5465
Joined: 18 May 2005 00:59
Location: NYC
Contact:

#13 Post by xor » 03 Apr 2009 02:53

munchy wrote:p.s. I have got round the interrupt problem by essentially doing one thing at a time. I read the frequency one second, then read temp (ignoring all intterupts) and reset rev, tally and TMR0 at the end of the temp read procedure. As long as there is >1 sec between calling the temp read procedure everything is happy.
This code has not been tested but it compiles. Interrupts only for Timer0 and every 33ms. That's a lot of breathing room for your program.

Code: Select all

program FREQ_COUNTER

' *****  0Hz to 65.5KHz  Frequency Counter  *****
' *****  PIC16F877A @ 8MHz  Default Configs *****
' *****  By Warren Schroeder Apr 02, 2009   *****

' *****  Counts pulses on T1CKI pin for 1 Second Sampling Period *****


CONST FILLER as Byte = 256-132    ' = 132 * 128us = 16896us
Dim TIMER1 as Word  Absolute 14   '  TMR1L:TMR1H pair
Dim FREQ as Word
Dim TALLY as Byte


Sub Procedure Interrupt()
    dec(TALLY)
    If TALLY = 0 Then          ' 30 int's x 32768us = 983040us
       TMR0 = TMR0 + FILLER    ' add 16896us = 999,936us  ~1sec.
       TALLY = 31
    End If
    If TALLY = 30 Then         ' finished 1 second
        T1CON.TMR1ON = 0       ' stop  timer1
        FREQ = TIMER1          ' save  timer1
        TIMER1 = 0             ' clear timer1
        T1CON.TMR1ON  = 1      ' start timer1
        TMR0   = 0             ' clear timer0 ; fresh start for next 1sec.
     End If
    INTCON.TMR0IF = 0     ' clear int flag
End Sub

Sub Procedure SetupT1asCounter()
    T1CON = %00000110      ' ext clock, no sync, no prescale
    TMR1H = 0              ' clear timer1
    TMR1L = 0
    T1CON.TMR1ON = 1       ' start timer1
    TALLY = 30             ' 30 interrupts, each 256*128us= 32768us
End Sub

Sub Procedure SetupT0asTimer()
    OPTION_REG = %00000111 ' prescale=256  = 128us each tick
    TMR0 = 0               ' timer cleared and rolling
    INTCON = %10100000     'GIE enabled,TMR0IE enabled,TMR0IF cleared
End Sub
    
 main:
 
     ADCON1 = 6             ' disable analog inputs
     CMCON = 7
     FREQ  = 0
     
     SetupT1asCounter()
     SetupT0asTimer()
     
     While 1=1              ' do something
     Wend
     
end.
[color=darkred][b]xor[/b][/color]
[url=http://circuit-ed.com]CircuitED -[/url]

munchy
Posts: 20
Joined: 19 Mar 2009 19:49

#14 Post by munchy » 03 Apr 2009 14:22

xor,

Thanks! I had to change
INTCON.TMR0IF = 0 to INTCON.T0IF = 0 and remove CMCON = 7. I have a plain old 877, not an 877A, I think this is why. Other than that it is working perfectly.

I don't really see how this helps me though, other than giving me less interrupts... I am trying to interface to a DS1820 during those 33ms, which is fine most of the time. But if there is an interrupt while getting the temperature reading, will it not still corrupt? I have added the temperature read procedure and the display looks stable - am I just getting 'lucky' most of the time?

Andrew

xor
Posts: 5465
Joined: 18 May 2005 00:59
Location: NYC
Contact:

#15 Post by xor » 03 Apr 2009 17:34

If the One-Wire protocol is synchronous then interrupts should not affect data transmission. Interrupts will certainly mess up software based timed signals, such as Soft UART communication.
[color=darkred][b]xor[/b][/color]
[url=http://circuit-ed.com]CircuitED -[/url]

Post Reply

Return to “mikroBasic General”