Shop OBEX P1 Docs P2 Docs Learn Events
Created a quick and dirty piezo buzzer driver - It works but the output frequency is a tad low? — Parallax Forums

Created a quick and dirty piezo buzzer driver - It works but the output frequency is a tad low?

Hey guys,
Created a quick and dirty piece of code to drive a piezo buzzer, code is below.
It sounds pretty good, but I can tell its "out of tune" - the notes I play from the piezo are a bit lower frequency than if I play them on a keyboard or computer. For a test I downloaded a guitar tuner app on my phone, and I had the piezo play a G7 (3134 Hz)... the tuner said I was playing a D7 (2348).

Is this just the nature of piezo buzzers or is something incorrect with my code? I am guessing it has to do with me not accounting for the actual delay of the instructions themselves, which is adding a tiny bit of extra time between the waitcnt functions... thus lowering my desired frequency. Can this be it? Is there a good way to account for this, or am I going to have to manually tune this thing on every single note? Thanks and any help is greatly appreciated!

CON
  '////////////////////////////////////////////////////////////////////////////
  '///    Notes
  '////////////////////////////////////////////////////////////////////////////
  NOTE_B5 = 987
  NOTE_E6 = 1318
  NOTE_G6 = 1567
  NOTE_E7 = 2636
  NOTE_C7 = 2092
  NOTE_D7 = 2348
  NOTE_G7 = 3134

PUB PlayFrequency(pin, tone, duration)| counter
  repeat counter from 0 to (tone * duration / 1000)
    outa[pin] := 1
    waitcnt(clkfreq / (tone + tone) + cnt)
    outa[pin] := 0
    waitcnt(clkfreq / (tone + tone) + cnt)
  

PUB PlaySound(pin,sound)
  case sound
    SOUND_COIN:
      PlayFrequency(pin,NOTE_B5,100)
      PlayFrequency(pin,NOTE_E6,850)

    SOUND_1UP:
      PlayFrequency(pin,NOTE_E6,125)
      PlayFrequency(pin,NOTE_G6,125)
      PlayFrequency(pin,NOTE_E7,125)
      PlayFrequency(pin,NOTE_C7,125)
      PlayFrequency(pin,NOTE_D7,125)
      PlayFrequency(pin,NOTE_G7,125)
        
    'SOUND_SUCCESS:
    'SOUND_FAIL:

Comments

  • Cluso99Cluso99 Posts: 18,069
    edited 2016-02-16 22:00
    Welcome to the forums Mahonroy.

    Your notes are referencing the clock frequency (the CNT counter). With a typical 5.00MHz xtal and PLL16x you get a clock of 80MHz which is 12.5ns per clock.

    To get a 1ms time you would use
    waitcnt(cnt + clkfreq/1000)
    where clkfreq/1000 = 80,000,000/1000 = 80,000
    so this would wait 80,000 clocks.

    If my maths are correct (need a coffee ;) ) you need this...
    waitcnt(cnt + (clkfreq/1_000_000)*NOTE_G7))
    Note the parenthesis use denoting the divide before multiply to ensure no overflow!

    Another way to use the output to the pin is
    repeat .....
      !out[pin] 'invert the output
      waitcnt(....)
    
  • Cluso99 wrote: »
    Welcome to the forums Mahonroy.

    Your notes are referencing the clock frequency (the CNT counter). With a typical 5.00MHz xtal and PLL16x you get a clock of 80MHz which is 12.5ns per clock.

    To get a 1ms time you would use
    waitcnt(cnt + clkfreq/1000)
    where clkfreq/1000 = 80,000,000/1000 = 80,000
    so this would wait 80,000 clocks.

    If my maths are correct (need a coffee ;) ) you need this...
    waitcnt(cnt + (clkfreq/1_000_000)*NOTE_G7))
    Note the parenthesis use denoting the divide before multiply to ensure no overflow!

    Another way to use the output to the pin is
    repeat .....
      !out[pin] 'invert the output
      waitcnt(....)
    

    Thanks for the response!

    I tried your piece of code "waitcnt(cnt + (clkfreq/1_000_000)*tone)" but it didn't work... as the frequencies increase, the tone decreases with this (because its creating a larger delay the larger the frequency).

  • ElectrodudeElectrodude Posts: 1,657
    edited 2016-02-16 22:21
    Using something like: (completely untested)
    PUB PlayFrequency(pin, tone, duration) | time, delta
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      time := cnt
      repeat (duration / 1000 * tone) << 1 ' (milliseconds) * (1/1000 seconds/millisecond) * (cycles per second) * (2 transitions/cycle) = transitions
        !out[pin]
        waitcnt(time += delta)
    
    would make it even more precise by eliminating drift originating in the fact that the !out[pin] takes a non-zero amount of time.

    My function's tone parameter is in Hz, since your note constants are in Hz. I guess Cluso thought you wanted tone to be a period in milliseconds and not a frequency in Hz.

    This is theoretically slightly dangerous, in that if tone is too big, it will result in a delta that is too small, which will make the waitcnt wrap around. However, I'm pretty sure this won't happen for any audible frequencies.
  • Cluso99Cluso99 Posts: 18,069
    Mahonroy wrote: »
    Cluso99 wrote: »
    Welcome to the forums Mahonroy.

    Your notes are referencing the clock frequency (the CNT counter). With a typical 5.00MHz xtal and PLL16x you get a clock of 80MHz which is 12.5ns per clock.

    To get a 1ms time you would use
    waitcnt(cnt + clkfreq/1000)
    where clkfreq/1000 = 80,000,000/1000 = 80,000
    so this would wait 80,000 clocks.

    If my maths are correct (need a coffee ;) ) you need this...
    waitcnt(cnt + (clkfreq/1_000_000)*NOTE_G7))
    Note the parenthesis use denoting the divide before multiply to ensure no overflow!

    Another way to use the output to the pin is
    repeat .....
      !out[pin] 'invert the output
      waitcnt(....)
    

    Thanks for the response!

    I tried your piece of code "waitcnt(cnt + (clkfreq/1_000_000)*tone)" but it didn't work... as the frequencies increase, the tone decreases with this (because its creating a larger delay the larger the frequency).

    I was just trying to give you an example.
    Because you are outputting a 1 then a 0 for each waitcnt, you would need to divide by 2 because it is the time for half the note.
    ie waitcnt(cnt + (clkfreq/1_000_000)*tone/2)
  • Cluso99Cluso99 Posts: 18,069
    BTW there are other ways using the internal cog counters to generate the tone automatically. Then you can use the waitcnt for the length of time the note will be played.
    When you get to this point, take a look at the counter app notes.
  • Electrodude is right on here - the way you're using waitcnt is referencing the present time plus a delta, but ignoring the time taken by your code. Your waitcnt() calls are doing two adds and a divide, so they'll take even longer than the outa[] bits.

    In addition, the repeat call is possibly computing the count every time it goes through the loop, so I'd pre-compute that value, like this:
    PUB PlayFrequency(pin, tone, duration) | time, delta, loops
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      time := cnt
      loops := (duration / 1000 * tone) << 1  'cache the loop count so it's not calculated per iteration
    
      repeat loops
        !out[pin]
        waitcnt(time += delta)
    
  • JasonDorie wrote: »
    Electrodude is right on here - the way you're using waitcnt is referencing the present time plus a delta, but ignoring the time taken by your code. Your waitcnt() calls are doing two adds and a divide, so they'll take even longer than the outa[] bits.

    In addition, the repeat call is possibly computing the count every time it goes through the loop, so I'd pre-compute that value, like this:
    PUB PlayFrequency(pin, tone, duration) | time, delta, loops
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      time := cnt
      loops := (duration / 1000 * tone) << 1  'cache the loop count so it's not calculated per iteration
    
      repeat loops
        !out[pin]
        waitcnt(time += delta)
    

    The argument to the repeat is only calculated once. There's not really any reason to cache it like that.

    If you look at the resulting PNUT bytecode, you'll see that it computes the argument once at the beginning and then does a djnz loop on it, i.e. it loops while (internal_counter--) == 0
  • There's one variant that does recompute every iteration, so it's probably the repeat from X to Y version. Good to know!
  • JasonDorie wrote: »
    There's one variant that does recompute every iteration, so it's probably the repeat from X to Y version. Good to know!

    Yes, the "repeat i from x to y step z" version recomputes x, y, and z every iteration, according to BST's compiler listing. It even recomputes x every time, so that it can figure out if it's iterating forwards or backwards.
  • JasonDorie wrote: »
    Electrodude is right on here - the way you're using waitcnt is referencing the present time plus a delta, but ignoring the time taken by your code. Your waitcnt() calls are doing two adds and a divide, so they'll take even longer than the outa[] bits.

    In addition, the repeat call is possibly computing the count every time it goes through the loop, so I'd pre-compute that value, like this:
    PUB PlayFrequency(pin, tone, duration) | time, delta, loops
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      time := cnt
      loops := (duration / 1000 * tone) << 1  'cache the loop count so it's not calculated per iteration
    
      repeat loops
        !out[pin]
        waitcnt(time += delta)
    

    Thanks again for the help!
    This piece of code does not play any sound either. I'm working on it to see where the bug is.

  • It might be worth changing it to this:
    PUB PlayFrequency(pin, tone, duration) | time, delta, loops
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      loops := (duration / 1000 * tone) << 1  'cache the loop count so it's not calculated per iteration
      time := cnt    '**moved this below the loop compute line**
    
      repeat loops
        !out[pin]
        time += delta   'moved up here to make sure the += part happens before the waitcnt
        waitcnt(time)
    

    The "loops := (duration....)" line will take a decent amount of time to compute as it contains a divide and a multiply. A single Spin instruction has about 400 cycles of overhead, and your "waitcnt(time += delta)" line is likely that or a little better because it includes an assignment. Your highest frequency tone (3134) gives you a time delta of 12763 clocks, so you *should* have plenty of time left over, but if you go much higher in frequency you'll start cutting it close.
  • JasonDorie wrote: »
    It might be worth changing it to this:
    PUB PlayFrequency(pin, tone, duration) | time, delta, loops
      delta := clkfreq / (tone << 1)
      dira[pin]~~ ' make pin an output
      loops := (duration / 1000 * tone) << 1  'cache the loop count so it's not calculated per iteration
      time := cnt    '**moved this below the loop compute line**
    
      repeat loops
        !out[pin]
        time += delta   'moved up here to make sure the += part happens before the waitcnt
        waitcnt(time)
    

    The "loops := (duration....)" line will take a decent amount of time to compute as it contains a divide and a multiply. A single Spin instruction has about 400 cycles of overhead, and your "waitcnt(time += delta)" line is likely that or a little better because it includes an assignment. Your highest frequency tone (3134) gives you a time delta of 12763 clocks, so you *should* have plenty of time left over, but if you go much higher in frequency you'll start cutting it close.

    Thanks Jason, the problem was that it couldn't play a note that was under 1 second in duration. So I changed it to this and it now works great:
    loops := (tone * duration / 1000) << 1
    

    The notes are also in tune with how they are supposed to be.
  • Ahhh... Good catch. I totally missed that. You had it right in your original, and Electrodude reversed it.
Sign In or Register to comment.