analogWrite creating misleading results

Hello,
I am using an Arduino UNO R3 with the 1.6.13 IDE
My sketch is attempting to measure the AC mains frequency and drive a voltmeter. Here is the code:

/*void loop() __attribute__((optimize("-O0"))); // Compiler directive - do not optimise for loops */

const int pulse_pin = 2;    // UNO Sampling pin
const int meter_pin = 11;    // Do not use 5 or 6 as these interact with millis()

int pulse_cnt = 0;
int sample_size = 120;      // Sample size, should be at least 2 times expected frequency
// Don't make lower otherwise the processor "freezes"!

long unsigned start_time, end_time;
int meter_value = 0;
int prev_meter_value = 0;
int value_change = 0;
int delay_factor = 0;
int old_analog =  -999;     // Needle value last displayed
float elapsed = 0;
float frequency = 0;

void setup()
{
  Serial.begin(9600);

  pinMode(pulse_pin, INPUT);
  pinMode(meter_pin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(pulse_pin), interrupt_count, RISING); // code for counting the increasing values

  Serial.println("Starting...");

  start_time = millis();                 // Sampling start time
  end_time = 0;                          // Sampling end time
}

void loop()
{

  if (end_time != 0) {

    noInterrupts();                     // Disable interrupts to ensure accuracy of readings
    elapsed = end_time - start_time;
    frequency = (sample_size / elapsed) * 1000;

    frequency = frequency - 0.03;        // Adjust frequency to be in line with web site
    if (frequency >= 49.50 and frequency <= 50.50) { // Only display if valid data

      // meter_value = mapFloat(frequency,49.50,50.50,0,255);
      // meter_value = map(int_freq,4950,5050,0,255);
      meter_value = (frequency - 49.5) * 255;  // Avoid map function and do it yourself

      Serial.print("Frequency: ");
      Serial.print(frequency);
      Serial.print(" Elapsed: ");
      Serial.print(elapsed);
      Serial.print(" Meter value=");
      Serial.println(meter_value);

      // analogWrite(meter_pin, meter_value);

      //move_needle(meter_value,10);
    }

    //Reset variables
    pulse_cnt = 0;
    start_time = millis(); // Was start_time=millis();
    end_time = 0;

    interrupts();  //Enable interrupts again
  }
}

void interrupt_count()
{
  pulse_cnt++;
  if (pulse_cnt == sample_size) {  //signal when set number of samples have been taken
    end_time = millis();
  }
}

void move_needle(int meter_value, byte ms_delay) {

  // Move the needle util new value reached
  while (!(meter_value == old_analog)) {
    if (old_analog < meter_value) old_analog++;
    else old_analog--;

    if (ms_delay == 0) old_analog = meter_value; // Update immediately id delay is 0

    analogWrite(meter_pin, old_analog);

    // Slow needle down slightly as it approaches new position
    if (abs(old_analog - meter_value) < 10) ms_delay += ms_delay / 5;

    // Wait before next update
    delay(ms_delay);
  }
}

If I execute the code with the analogWrite instruction commented out it works perfectly (n.b. I have a reputable source to compare my results with) as shown by the Serial monitor output here:

Starting...
Frequency: 50.37 Elapsed: 2381.00 Meter value=221
Frequency: 49.95 Elapsed: 2401.00 Meter value=114
Frequency: 49.95 Elapsed: 2401.00 Meter value=114
Frequency: 49.97 Elapsed: 2400.00 Meter value=119
Frequency: 49.97 Elapsed: 2400.00 Meter value=119
Frequency: 49.93 Elapsed: 2402.00 Meter value=109
Frequency: 49.95 Elapsed: 2401.00 Meter value=114
Frequency: 49.93 Elapsed: 2402.00 Meter value=109
Frequency: 49.93 Elapsed: 2402.00 Meter value=109
Frequency: 49.95 Elapsed: 2401.00 Meter value=114

If I then uncomment the analogWrite instruction, things start to go awry:

Starting...
Frequency: 50.10 Elapsed: 2394.00 Meter value=151
Frequency: 50.43 Elapsed: 2378.00 Meter value=237 <<< Wrong value
Frequency: 50.41 Elapsed: 2379.00 Meter value=232
Frequency: 49.99 Elapsed: 2399.00 Meter value=125
Frequency: 50.39 Elapsed: 2380.00 Meter value=226 <<< Wrong value
Frequency: 49.99 Elapsed: 2399.00 Meter value=125
Frequency: 49.97 Elapsed: 2400.00 Meter value=119
Frequency: 49.99 Elapsed: 2399.00 Meter value=125
Frequency: 50.39 Elapsed: 2380.00 Meter value=226 <<< Wrong value
Frequency: 49.99 Elapsed: 2399.00 Meter value=125
Frequency: 50.41 Elapsed: 2379.00 Meter value=232 <<< Wrong value

Things I have tried so far:

  1. Used every PWM pin in turn,
  2. Replaced the UNO with a Nano,
  3. Tried the map function then tried the mapFloat function before resorting to creating my own formula to evaluate meter_value. (I read somewhere that the map function sometimes caused problems with timing),
  4. Inserted a compiler directive which I thought might help.

Please can anyone help?

This may not be your issue but it is definitely an issue: end_time and pulse_cnt need to be defined with the volatile keyword.

1 Like

You're using analogWrite for PWM on pin 11 where the duty cycle range is 0-255 (byte), but you're working with variables assigned type int.

Hi Toddl1962
Thank you for your reply. I made the changes as suggested but it didn't solve the problem.

Hi dlloyd,
Thank you for your reply. I changed the meter_value to byte but it hasn't made any difference. Can you suggest anything further please?

I'd tighten up the code between the noInterrupts() and interrupts() to just deal with the variables potentially modified by the interrupts. The floating point math and a stream of serial text at 9600 baud could take a while.

Maybe:

    noInterrupts();                     // Disable interrupts to ensure accuracy of readings
    elapsed = end_time - start_time;
    //Reset variables
    pulse_cnt = 0;
    end_time = 0;
    interrupts();  //Enable interrupts again
    start_time = millis(); // Was start_time=millis();
    frequency = (sample_size / elapsed) * 1000;
    ...
1 Like

Hi DaveX,
I implemented your suggested code but it didn't improve matters.
I also tried changing the Serial baud rate from 9600 to 115200 and also
commented out completely the Serial.print statements as I can see the resulting meter value on the meter. Both changes did not work either which seems to confirm my suspicion of the analogWrite statement itself.

What kind of meter do you have? DMMs can be thrown off by PWM duty cycles.

Hi DaveX
It's just a cheap Chinese panel meter. I've got a digital multi-meter and an old fashioned moving coil multi-meter - I'll try those and reply soon.

What's wrong with the values?

120/2380*1000 - 0.03 = 50.39017
(50.39017 -49.5)*255 = 226.9933
(int) 226.9933 = 226

Did you want 227 here? Then you need to round the float to the nearest int, rather than let it be truncated:

meter_value = (frequency - 49.5) * 255+0.5;

Hi DaveX,

In reply to "What's wrong with the values?" they are completely out of range from the expected values - by that I mean I have a comparable system ( it uses LEDs instead of a meter and the circuitry is identical for both projects) so I can see what the correct figure should be by the appropriate LED illuminating.
I take your point regarding rounding but I'm not overly fussed about an error of +/- 1 in the meter value. As I said earlier I'm about to test with two alternate meters. As a slight aside to this topic I also have a project which uses three 5V panel meters with modified scales to display the time in hours , minutes and seconds which again uses analogWrite without issue. However these meters are a different model to the one I'm using for the frequency project. Back soon with the meter results.

Hi DaveX,

OK. I tried both my digital multi-meter and also my ALTAI moving coil multi-meter - still getting erroneous results unfortunately. So I guess we can rule out the meter.

I also tried to remove the floating point maths like this:

if (end_time != 0) {

    noInterrupts();                     // Disable interrupts to ensure accuracy of readings
    elapsed = end_time - start_time;
    pulse_cnt = 0;
    end_time = 0;
    start_time = millis(); 
    interrupts();  //Enable interrupts again
    //frequency = (sample_size / elapsed) * 1000;
    int_freq=((sample_size/elapsed)*1000)*100; // int_freq declared as int

    //frequency = frequency - 0.03;        // Adjust frequency to be in line with web site
    int_freq = int_freq - 3;
    if (int_freq >= 4950 and int_freq <= 5050) { // Only display if valid data
    //  if (frequency >= 49.50 and frequency <= 50.50) { // Only display if valid data

      // meter_value = mapFloat(frequency,49.50,50.50,0,255);
      // meter_value = map(int_freq,4950,5050,0,255);
      //meter_value = (frequency - 49.5) * 255;  // Avoid map function and do it yourself
      meter_value = ((int_freq - 4950) * 255) / 100;  // Avoid map function and do it yourself
      
      
      Serial.print("Frequency: ");
      Serial.print(int_freq);
      Serial.print(" Elapsed: ");
      Serial.print(elapsed);
      Serial.print(" Meter value=");
      Serial.println(meter_value);
      
      analogWrite(meter_pin, meter_value);

      //move_needle(meter_value,10);
    }

but that didn't work either.

On the int frequency, I'd try the multiplies before the divides, so, for example, 120/2379 doesn't truncate to zero.

Without the hardware setup, or a clear error, it's hard to tell what the problem is. From our view of your project, the numbers seem reasonable, but it is hard to tell a defect from not a defect. For instance, why is the 232 line not a wrong value?

How does what was delivered differ from what was expected?

Hi DaveX

Ok I'll try and re-arrange the coding for int_frequency however this exact code is used for the LED version of the sketch which works fine (zero truncation notwthstanding!).

Regarding your point "For instance, why is the 232 line not a wrong value?"
that's an error on my part, I should have denoted that line as in error.

In terms of hardware setup do you mean a circuit diagram or photograph of the components - I'm not sure what you mean, please elaborate for me.

Hi DaveX,
I forgot to answer your last question - "How does what was delivered differ from what was expected?"

As I said in post 11, the wrong values deviate from the expected which I can monitor via my LED based application. If the values jumped from say 49.99 to 50.39 in the real world that would represent a sudden (i.e. in the region of a couple of seconds) increase in the power output to the grid. I think that's highly unlikely especially when the number of occurrences are as high as the data I've presented to you demonstrates.

... and even more so when the Serial monitor output does not show any wrong values when the analogWrite statement is removed.

So the issue isn't in the calculations, but that either there are spurious extra counts, or that the counts are correct but the timing is wrong.

In this part of the world, the grid power phase is well controlled so different systems can be linked together efficiently.

120 cycles at 50Hz should be 2.4sec, or 2400 msec. The 2.4 sec reporting code shouldn't have a big effect. The 50Hz interrupt should have 1/50s = 20ms = 20000us = 320000 clock cycles to work with.

If it's not the circuitry, the problem may be the timing. Have you considered using micros() instead of millis()?

1 Like

Try initializing the timer inside the interrupt routine. Remove or comment out line 63:

    start_time = millis(); // Was start_time=millis();

and change the ISR to

void interrupt_count()
{
	if (!pulse_cnt) { // first RISING? start timing
		start_time = millis();
	}
  pulse_cnt++;
  if (pulse_cnt == sample_size) {  //signal when set number of samples have been taken
    end_time = millis();
  }

and declare all variables involved as volatile:

volatile int pulse_cnt = 0;
volatile int sample_size = 120;      
volatile long unsigned start_time, end_time;

Then let us know!

1 Like

You may have to change

    frequency = (sample_size / elapsed) * 1000;

to

    frequency = ((sample_size+1) / elapsed) * 1000;

Maybe there's some clock jitter where the unusually large values are immediately balanced by unusually small values that your code may not report.

1 Like