Shop OBEX P1 Docs P2 Docs Learn Events
Question about cnt — Parallax Forums

Question about cnt

I have a need to accurately measure a period of time around 60 second. I know at 80MHz cnt rolls over just past 53 seconds, so I thought I'd try running at 40MHz in hopes that I would have up to 107 seconds, which would be more than enough. But it isn't working. If the period of the input signal is more that 53 seconds, I get incorrect time measurements. Interestingly, at 40MHz I tried a simple waitcnt(cnt) and indeed the cog stopped for 107 seconds. But in my test code below where I wait for 60 seconds, the time calculation is not correct. After the first iteration, I always get 47372ms. If I change the last line to wait for less than 53 seconds, it gives the correct result. Can someone explain what's happening?
CON
  _clkfreq = 40_000_000        ' clock frequency MHz
  _clkmode = xtal1 + pll8x     ' standard clock mode 

  mSec = _clkfreq/1_000        ' mSec 

VAR
  long now,past

OBJ
  fds : "FullDuplexSerial"
  
PUB main

  fds.Start(31,30,%0000,115200)

  waitcnt(clkfreq*2 + cnt)    ' two second pause

  fds.tx(16)     ' Clear screen

  repeat
    past := now
    now := cnt

    fds.dec(||(now-past)/mSec)
    fds.tx(13)
  
    waitcnt(clkfreq*60 + cnt)   ' 60 second pause

Comments

  • From the Propeller Education Kit manual, 64: You can click Help in the Propeller Tool and find the pdf.
    ''File: TimekeepingGood.spin
    CON
    _xinfreq = 5_000_000
    _clkmode = xtal1 + pll1x
    VAR
    long seconds, dT, T
    PUB GoodTimeCount
    dira[9..4]~~
    dT := clkfreq
    T := cnt
    repeat
    T += dT
    waitcnt(T)
    seconds ++
    outa[9..4] := seconds
    
  • JonnyMacJonnyMac Posts: 9,105
    edited 2015-12-17 01:55
    The problem is your target period, when measured at the resolution of cnt, is larger than posx. What I suggest is using a cog to operate a timer at a more appropriate resolution. Here's an easy example using a 1ms timer.
    con  
    
      _clkmode = xtal1 + pll16x
      _xinfreq = 5_000_000                                           ' use 5MHz crystal
    
      CLK_FREQ = (_clkmode >> 6) * _xinfreq                          ' system freq as a constant
      MS_001   = CLK_FREQ / 1_000                                    ' ticks in 1ms
      US_001   = CLK_FREQ / 1_000_000                                ' ticks in 1us
    
    
    obj
    
      fds : "fullduplexserial"
    
    
    var
    
      long  stack[16]
      long  millis
    
      long  elapsed
    
    
    pub main | t
    
      fds.start(31, 30, %0000, 115_200)
      cognew(cntr1ms, @stack)
    
      waitcnt(cnt + (CLK_FREQ * 3))                                  ' 3s delay to open PST
    
      fds.str(string("Started", 13))
    
      elapsed := -millis                                             ' start timing
    
      t := cnt
      repeat 60_000
        waitcnt(t += MS_001)
    
      elapsed += millis                                              ' stop timing
    
      fds.str(string("Elapsed time: "))
      fds.dec(elapsed)
      fds.str(string(" ms"))
    
      repeat
        waitcnt(0)
      
    
    pri cntr1ms | t                                                  ' launch with cognew()
    
      t := cnt
      repeat
        waitcnt(t += MS_001)
        ++millis
    

    BTW, you can't do an inline delay of more than ~53 seconds at 80MHz -- you run into the same 32-bit resolution of the cnt register.

    Note: By using the differential between two points you can measure multiple events simultaneously using the strategy illustrated above (you'll need a variable for each event). If you only have one event you can start the timer by setting teh global variable millis to zero, then reading it at the end of your event.

  • Jon,

    Thanks for the code sample. I think posx is what I overlooked. At 20MHz, I can measure a 60 second period.
  • I had a project that required time in milliseconds up to 1second. For this I used the Ping sensor object written by Chris Savage and modified it. I count the period between high and low pin times.

    Pub Ticks(Pin) : Microseconds | cnt1, cnt2

    Dira [Pin]~
    waitpne(0, |< Pin,0)
    Cnt1 :=cnt
    waitpne (0, |< Pin,0)
    Cnt2 := cnt
    Microseconds :=(||(cnt1-cnt2) / (clkfreq / 1_000_000))>>1

    Pub Time(Pin) : Dis

    Dis := Milt(Pin)/10

    The above is my ping2 Object

    Then my in my main object

    Var
    Long range,time


    Obj

    LCD : "debug_lcd"
    Ping : "ping2"


    Pub

    Repeat

    Range :=ping.time(ping_pin)
    Range:=range-555
    LCD.decf(range/10,3)
    LCD.putc(".")
    LCD.decx(range//10,1)
    waitcnt(clkfreq/10+cnt)


    I left out the details about the LCD , con clkmode +pll16x xinfreq 5_000_000

  • FWIW, the P1, a used OCXO on ebay (~$20), and the 1pps output of the average cheap GPS (~$20), provides the basis of an unbelievably precise reference for almost anything related to duration or frequency. The OCXO can be used to clock the Prop (typically via a FF prescaler). The 1pps input is measured by the OCXO, and accumulated error is the error of the OCXO. This can be addressed by trimming the VFC pin or by averaging and compensating digitally.

    So in your case (in a nutshell...), you can measure the offset (in 80MHz tics) between the 1pps and the start of your period, and again against the 1pps and the stop of your period. Even periods of hours or days can be measure this way and with great accuracy.

    Even better accuracy can be obtained with a bit more software sophistication and across more time. Pretty soon you've created your own Trimble Thunderbolt. :)
  • There's a simpler way:

    Wait one second, sixty times
    MyCounter := cnt
    Repeat c from 1 to 60
      MyCounter += clkfreq
      Waitcnt( MyCounter )
    

  • But there is a slight error per second then due to the execution overhead.
    Why not take the delay time in seconds, divide by 32 (shift right if an integer),
    and wait units of 32 seconds, then wait for the remainder. The error is only
    per 32 seconds.

    You can also correct for the error by trial and error (say wait repeatedly for 1ms,
    toggling a pin and measure the pin frequency error (should be 500Hz), while changing
    a fiddle factor that is subtracted from the counter target value.
  • Duane DegnDuane Degn Posts: 10,588
    edited 2015-12-17 16:00
    Sapphire wrote: »
    I have a need to accurately measure a period of time around 60 second. I know at 80MHz cnt rolls over just past 53 seconds,

    Actually you run into trouble at half this time. Since the Propeller treats the 32-bit value as a signed long, the now - before calculation will give you bad results once half the roll over time has elapsed.

    You could use the full roll over time if you treated the numbers as unsigned longs. I believe Phil has an object which allows unsigned math.

    You could also watch for the negative value of the now - before calculation and use a different method to calculate the time from the two values.

    Of course it's easy to count seconds or milliseconds as others have suggested.
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2015-12-17 17:10
    markt wrote:
    But there is a slight error per second then due to the execution overhead.
    If that comment was directed at Jason's code, it's not correct. His code does not have a per-second overhead problem, thanks to the += operation.

    -Phil
  • Phil is right - I store the CNT value to a variable before the loop starts, then in the loop, I add the delay time to the variable and wait for the counter to hit it, repeatedly.
  • JonnyMacJonnyMac Posts: 9,105
    edited 2015-12-17 18:30
    Simplifying further, you don't even need a variable for the repeat loop:
      mycounter := cnt
      repeat 60
        waitcnt(mycounter += clkfreq)
    

    I did the same thing for my example, but in milliseconds so I could validate the resolution of the timer (it worked).
  • AribaAriba Posts: 2,690
    edited 2015-12-17 18:38
    But he wants to measure a time between two events and not wait for 60 seconds.
    I don't think this needs a resolution of 25ns, so you can shift the CNT value so that it fit into the positive range of a 32bit variable.
    This should work (still 50ns resolution!):
    CON
      _clkfreq = 40_000_000        ' clock frequency MHz
      _clkmode = xtal1 + pll8x     ' standard clock mode 
    
      mSec = _clkfreq/1_000        ' mSec 
    
    VAR
      long now,past
    
    OBJ
      fds : "FullDuplexSerial"
      
    PUB main
    
      fds.Start(31,30,%0000,115200)
    
      waitcnt(clkfreq*2 + cnt)    ' two second pause
    
      fds.tx(16)     ' Clear screen
    
      repeat
        past := now
        now := cnt >> 1
    
        fds.dec(((now-past) & POSX) / (mSec>>1))
        fds.tx(13)
      
        waitcnt(clkfreq*60 + cnt)   ' 60 second pause
    

    Andy
  • Thanks for all the feedback. With Jon's comment on posx, I figured out what Duane said, you can only go half way using subtraction. And Andy's right I don't want to wait for the 60 seconds, so shifting cnt down one bit is nice trick since ms is the target resolution.
  • You don't have to wait -- most of us inserted a delay just to demonstrate the process working (as you did in your initial attempt).

    If you're not using the counters and you have a spare pin you can create a free-running timer without using a cog (or funky cnt register math).
    pub start_timer(pin)
    
    '' Starts free-running oscillator/counter
    '' -- pin will output 1kHz
    '' -- phsa holds ms count
    
      ctra := (%01010 << 26) | pin                                   ' ctra is pos-edge counter on pin
      frqa := 1
      phsa := 0  
    
      ctrb := (%00100 << 26) | pin                                   ' ctrb is 1kHz oscillator on pin
      frqb := ($8000_0000 / (clkfreq / 1000)) << 1                       
      dira[pin] := 1
    

    The current time (ms) will be in phsa. If you're just timing one event, you can write 0 to phsa to reset the timer. If you want to have multimple running timers, do this:
      elapsed := -phsa  { start timer }
      elapsed += phsa   { stop timer }
    
  • Jon,

    Since the events are successive, and the end of one is the beginning of the next, I can use:
    elapsed := phsa~
    

    to read it and reset it at the same instant (even though I know you don't like ~). It works in your first example, which I have running now.

    I have to read up on how you got the oscillator frequency. Why $8000_0000 and later << 1? The clkfreq/1000 part I get.
  • I don't like the post tilde operators for two reasons:
    1) They are not obvious to coders new to Spin
    2) They are slower than direct assignments -- which are obvious to anyone

    Yes, I have run a test to prove point 2.

    phsa~ takes 416 ticks
    phsa := 0 takes 368 ticks (faster and clearer to other coders)

    Here's the code (the - 544 is to remove the Spin overhead for the assignment operators)
      t1 := -cnt
      phsa~
      t1 += cnt - 544
      term.dec(t1)
      term.tx(13)
    
      t2 := -cnt
      phsa := 0
      t2 += cnt - 544
      term.dec(t2)
      term.tx(13)
    

    Why $8000_0000 and later << 1?

    To do the calculation for frqx you need 2^32 which Spin sees as 0. By expressing 2^32 as $8000_0000 << 1 and rearranging the order of things, the problem is averted.

    For the record, I didn't come up with this solution (I think it was Beau Schwabe), but jumped right on it when it was presented.
  • But Jon,

    elapsed := phsa
    phsa :=0

    does take longer and leaves a bigger gap between reading and restarting than

    elapsed := phsa~

    Do you have a trick for that?
  • Nope.

    There will be exceptional cases, and this is one. In general, however, I think it's wisest to use code that is obvious. But then, I'm just an actor who writes code to keep a roof over his head until stardom calls. :lol:
  • Jon,

    I find the post ~ and ~~ confusing too, but thought in this case reading and clearing was the way to minimize lost time. I try to avoid them and it's interesting to know they take longer. It would seem that they should compile into a simpler instruction, but I guess not!
  • Sapphire wrote: »
    Jon,

    I find the post ~ and ~~ confusing too, but thought in this case reading and clearing was the way to minimize lost time. I try to avoid them and it's interesting to know they take longer. It would seem that they should compile into a simpler instruction, but I guess not!

    Post ~ and ~~ certainly do compile into simpler instructions - in fact, PNut has special versions of its assignment operator just for them. I don't believe that they're slower. Tracy Allen says they're faster.

    (Sorry if I misunderstood your post.)
  • Electrodude,

    I was commenting on Jon's code that showed phsa := 0 is faster than phsa~. You're saying phsa~ should be faster?

    In my application I want to read and clear as fast as possible, so using
    elapsed := phsa~
    
    is necessary.

  • shouldn't

    elapsed := phsa := 0

    do the same?

    Mike
  • No. Like C, that sets everything to zero.
  • Unlike other operators, := is right-associative. IOW, multiple := operations proceed from right to left.

    -Phil
Sign In or Register to comment.