Help with tone() output "hiccup"

I’m hoping I’ve chosen the correct forum category for this…

I need to drive a tachometer via a square wave operating from ~0-200 Hz.

Currently, I’m using tone() via the 16 bit timer to output this Hz range.

This works great except there is a point where the wave hiccups or “interrupts” itself and outputs no tone for a brief moment before it picks right back up and resumes. You can see this behavior in the needle of the tachometer as it momentarily sweeps back towards 0 RPM before it jumps back to a higher reading as the hiccup ends.

You can also observe this via an oscilloscope where the wave clearly flat lines for a moment before resuming.

Interestingly, if I output a constant fixed Hz value, the hiccup or interruption does not occur and the needle will indefinitely point at X RPM. The hiccup seems to only manifest itself when I’m actually trying to drive it via constantly fluctuating values, as would occur when driven by an engine’s ignition system or test code that has the needle constantly sweeping.

I’m hoping someone here can shed some light on what I’m either doing wrong or what I can do to address the issue.

Here’s a very simple sketch that sweeps the tachometer from 0-6000 RPM and back from 6000-0 RPM on a repeating loop. I’d assume if you had a speaker, you’d hear the hiccup audibly or if you have an oscilloscope you can see it happening.

Thanks in advance for your thoughts.

#include <Tone.h>

Tone freq1;
Tone freq2;

int rpmHz = 25;
int state = 0;

void setup()
{
  freq1.begin(11);
  freq2.begin(9);
}

void loop()
{
  if (state == 0)
  {
    rpmHz++;
    freq2.play(rpmHz);
    if (rpmHz == 200)
      {
        state = 1;
      }
  }
  else if (state == 1)
    {
      rpmHz--;
      freq2.play(rpmHz);
      if (rpmHz == 0)
        {
          state = 0;
        }
    }
    delay (50);  
}

A tachometer is, in my world, a device sending out pulses telling about some kind of rotation. You want to generate pulses using a controller.

Can You draw a block diagram showing how the parts in project interact with each other?

So what do you suppose happens if you "freq2.play()" a value of zero?

@Railroader The overall scope of this project is to read a CAN-Bus-based engine RPM message, convert it into a squarewave signal that then drives a squarewave-to-analog signal converter, which drives an analog tachometer as fitted to a factory instrument cluster. I have all of this completed and working in a vehicle, save for this odd hiccup in the output of the squarewave generated via tone(). To simplify things here, I’m just providing super basic code that illustrates the issue should one test it out themselves. No tachometer is needed to see the hiccup.

@Paul__B I think I know what you’re implying, forgive me if I’m wrong – that the code I’ve got is outputting a value of zero at some stage and therefore, the tach is going to read zero. That isn’t related to the issue I’m experiencing. As the tach needle sweeps either up or down, at some point well past 0 RPM (and it isn’t at the same point on every sweep pass, either) it just drops as though someone lifted the throttle to make a gear change, and then it jumps back to about where it was and continues smoothly on its way up or down the range. In fact, I can hear the oscillation cut out in the tach’s electro-mechanical mechanism. It makes the faintest of buzzing, which increases/decreases in intensity as the needle sweeps up/down its range. You can just hear it cut out and then resume. Like I said, hook up a speaker or oscilloscope and see it happen in real time.

What Arduino platform are you using?

der_mechaniker: As the tach needle sweeps either up or down, at some point well past 0 RPM (and it isn't at the same point on every sweep pass, either) it just drops as though someone lifted the throttle to make a gear change, and then it jumps back to about where it was and continues smoothly on its way up or down the range.

Several years ago this question came up. I didn't track down the post/thread but, the answer IIRC, was - as the tone frequency changes, at certain points the prescaler setting down in the hardware needs to be adjusted. Each prescaler setting gives a range of frequencies - that's the smooth progression part. As the frequency changes every so often the prescaler value needs to change and it takes some time to make this happen. Plus it's not necessarily possible to go from 1200Hz to 1201Hz, for example. The jump may be from 1200Hz to 1223Hz. That's the hiccup part.

@Blackfin I'm currently using either an Arduino Uno or a SparkFun Redboard programmed w/Arduino. The behavior is the same with either.

@dougp Now that is starting to sound like something that explains what I'm experiencing. Unfortunately, that aspect of microcontroller behavior is outside my current scope of knowledge. I thank you, though, for giving me a starting point to research, both here and elsewhere. That sounds like a promising direction. Hopefully, I can come to a conclusion that will resolve this for me.

There's a chance for a "bug" in the tone library I think. The library uses CTC mode for the timer in question which means it's using output compares to generate the waveform.

Every 50mS your code calls .play(...); this results in a new OCR1A (timer 1 output compare) value being calculated and stored. The potential for a problem is if the new value of OCR1A is lower than the value of TCNT at the moment of writing; if this is the case, the output compare will miss and the timer will have to count all the way up to 0xffff before circling back to 0 and up to the new OCR1A value.

The datasheet for the '328 warns of this:

"... However, changing the TOP to a value close to BOTTOM when the counter is running with none or a low prescaler value must be done with care since the CTC mode does not have the double buffering feature. If the new value written to OCR1A or ICR1 is lower than the current value of TCNT1, the counter will miss the compare match. The counter will then have to count to its maximum value (0xFFFF) and wrap around starting at 0x0000 before the compare match can occur..."

I futzed around in the Tone.cpp library source. In method void Tone::play(uint16_t frequency, uint32_t duration) I added a small hack to see if the hiccup could be "fixed" or at least made less of a problem for this particular case (timer 1):

.
.
.
    case 1:
        OCR1A = ocr;
        //added starting here
        //if current TCNT is very close to or bigger than OCR value...
        if( TCNT1 >= (ocr - 10) )
        {
            //zero TCNT1 and force an output compare interrupt to maintain continuity
            TCNT1 = 0;
            bitWrite(TCCR1C, FOC1A, 1);

        }//if
        //end of added code
        timer1_toggle_count = toggle_count;
        bitWrite(TIMSK1, OCIE1A, 1);
    break;
.
.
.

As a quick attempt it seemed to help but it may not be a real fix.

You could probably use direct timer manipulation to achieve what you want without having to resort to a "flawed" library.

@Blackfin Thank you. Again, this is so beyond my knowledge base that I'm going to have to take the steps required to study and understand these fundamentals before I can take any steps to implimenting a "fix". On the one hand I'm intimidated, on the other I'm excited to learn something new.

One thing I'm pondering here is that in my actual code that is driven via CAN-bus, I don't have a delay() implemented. The delay(50) used in the sample code I posted here is just to slow down the sweep of the tach needle to watch it move in a more fluid motion. In the actual automotive application, the RPM can fluctuate rapidly and any delay would cause the tach's reading to be incorrect.

Interestingly, thinking about the comment made by @Paul__B about the Hz values perhaps not allowing for ++ values and, maybe, having to jump in larger increments, I wanted to mention an alternate sketch that behaves quite differently, but with the same hiccup. In the alternate sketch, I apply the fixed Hz values for 0, 1000, 2000, 3000, 4000, 5000 & 6000 RPM and then reverse them back down to 0. In this code there is not a regular small incremental increase/decrease of the Hz value, but rather a large jump to the next value. I see the hiccup occur in that code, as well. Of course, that code still uses delay(1500) between each jump to hold the needle at each value prior to the next jump.

der_mechaniker: One thing I'm pondering here is that in my actual code that is driven via CAN-bus, I don't have a delay() implemented. The delay(50) used in the sample code I posted here is just to slow down the sweep of the tach needle to watch it move in a more fluid motion. In the actual automotive application, the RPM can fluctuate rapidly and any delay would cause the tach's reading to be incorrect.

Interestingly, thinking about the comment made by @Paul__B about the Hz values perhaps not allowing for ++ values and, maybe, having to jump in larger increments, I wanted to mention an alternate sketch that behaves quite differently, but with the same hiccup. In the alternate sketch, I apply the fixed Hz values for 0, 1000, 2000, 3000, 4000, 5000 & 6000 RPM and then reverse them back down to 0. In this code there is not a regular small incremental increase/decrease of the Hz value, but rather a large jump to the next value. I see the hiccup occur in that code, as well. Of course, that code still uses delay(1500) between each jump to hold the needle at each value prior to the next jump.

The problem can still exist in the big-step code: If the compare value you write is less than the current timer count you'll get no activity on the PWM until the timer rolls over at 0xffff and counts back up to the new compare value. When you call the play() method there's actually a fair amount of math going on because it's trying to find the best prescaler value by an iterative approach; it then sets the prescaler and writes the new OCR value based on those iterations and it's doing so on a running, counting timer. There's definitely a chance for things to go pear-shaped, especially at smaller prescale values when the timer clock is running more quickly.

re the delay(); you definitely don't want to use delay(...) in any "serious" code unless there's just no way around it or the delay is of the delayMicroseconds() type where the blocking is not detrimental to other stuff that may be running (like, say, receiving CAN data...) I've seen mention that J1939 CAN data for engine RPM comes in at 100Hz; if so, you should be able to use that data basically directly and should get decently smooth tach operation.

You can always add some filtering and/or interframe interpolation to the RPM values to smooth your tach out too. Part of the problem is that your tach runs at just 200Hz so there's a lot of granularity between even 100Hz frames.

Might be a dumb question...

Could you avoid the bug by not changing the tone if TCNT1 >= (OCR1A - 100) or TCNT1 >= (OCR1A - 1000)?

I realize that is essentially what was suggested inside the library.

It's also not that difficult to manually toggle an output pin at 200Hz.