Help , Arduino 328p has problem when reading PWM duty cycle

Hello~

Here is my sketch, that can read 5x PWM duty cycle and FAN clock.
The PWM is 25KHz.
I currently have a problem for pwm duty cycle.

The sketch works fine if PWM duty cycle setting from16% to 100%.
However, if the duty cycle is less than 16% (1% -> 16%), the read value will be incorrect, maybe show 6%, 8%, 10% .......
Does anyone know why?



#include <Wire.h>
// 可將 Arduino\hardware\arduino\avr\libraries\Wire\utility\twi.h 改成400000L (400 KHz) 加快顯示速度

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 20, 4);

// FAN pin 4 PWM input
#define PWM1 6
#define PWM2 5
#define PWM3 4
#define PWM4 3
#define PWM5 2


// FAN pin 3 clock input
#define CLK1 12
#define CLK2 11
#define CLK3 10
#define CLK4 9
#define CLK5 8


static double duty;
static double freq;
static double freq1;



static double FANspeed;
static long highTime = 0;
static long lowTime = 0;
static long tempPulse;
static long lastSeen;



float val;
float va0;
int sensorValue = 0;
#define READ_DELAY 3

void setup()
{
  //  TCCR2B = TCCR2B & B11111000 | B00000011; // // set timer 2 divisor to    32 for PWM frequency of   980.39 Hz
  lcd.init();                      // initialize the lcd


  lcd.backlight();
  delay(300);
  lcd.noBacklight();
  delay(300);
  lcd.backlight();
  delay(300);
  lcd.noBacklight();
  delay(300);
  lcd.backlight();
  delay(300);
  lcd.noBacklight();
  delay(300);
  lcd.backlight();
  delay(200);


  // Print a message to the LCD.
  lcd.setCursor(0, 0);
  lcd.print(" MAJESTIC TEST JIG ");
  lcd.setCursor(0, 2);
  lcd.print(" FAN ROTATION TEST ");
  lcd.setCursor(0, 3);
  lcd.print("            Ver.:1.0");
  delay(1000);
  lcd.clear();

  // PIN I/O DEFINE
  pinMode(PWM1, INPUT_PULLUP);
  pinMode(PWM2, INPUT_PULLUP);
  pinMode(PWM3, INPUT_PULLUP);
  pinMode(PWM4, INPUT_PULLUP);
  pinMode(PWM5, INPUT_PULLUP);
  digitalWrite(PWM1, HIGH);
  digitalWrite(PWM2, HIGH);
  digitalWrite(PWM3, HIGH);
  digitalWrite(PWM4, HIGH);
  digitalWrite(PWM5, HIGH);

  /*
    pinMode(CLK1, INPUT_PULLUP);
    pinMode(CLK2, INPUT_PULLUP);
    pinMode(CLK3, INPUT_PULLUP);
    pinMode(CLK4, INPUT_PULLUP);
    pinMode(CLK5, INPUT_PULLUP);
  */

  pinMode(CLK1, INPUT);
  pinMode(CLK2, INPUT);
  pinMode(CLK3, INPUT);
  pinMode(CLK4, INPUT);
  pinMode(CLK5, INPUT);
  digitalWrite(CLK1, HIGH);
  digitalWrite(CLK2, HIGH);
  digitalWrite(CLK3, HIGH);
  digitalWrite(CLK4, HIGH);
  digitalWrite(CLK5, HIGH);

  Serial.begin(9600);

  lcd.setCursor(0, 0);
  lcd.print("DT_");
  lcd.setCursor(0, 1);
  lcd.print("FQ1_");
  lcd.setCursor(10, 1);
  lcd.print("FQ2_");
  lcd.setCursor(0, 2);
  lcd.print("FQ3_");
  lcd.setCursor(10, 2);
  lcd.print("FQ4_");
  lcd.setCursor(0, 3);
  lcd.print("FQ5_");
  lcd.setCursor(10, 3);
  lcd.print("PWM");
  lcd.setCursor(17, 3);
  lcd.print("KHz");


}



void loop()
{


  //read FAN colck (ratation)
  readPWM(PWM1);
  //  lcd.setCursor(0, 0);
  //  lcd.print("DT_");

  lcd.setCursor(3, 0);
  lcd.print(duty, 0);
  lcd.setCursor(4, 0);
  duty_detect();
  lcd.setCursor(5, 0);
  lcd.print(" ");

  readPWM(PWM2);
  lcd.setCursor(6, 0);
  lcd.print(duty, 0);
  lcd.setCursor(7, 0);
  duty_detect();
  lcd.setCursor(8, 0);
  lcd.print(" ");

  /////////////////////////////////////
  lcd.setCursor(14, 3);

  if (freq < 1)
  {
    lcd.print("0  ");
  }

  else
  {
    lcd.print(freq /1000, 0);
  }
////////////////////////////////////////////


  readPWM(PWM3);
  lcd.setCursor(9, 0);
  lcd.print(duty, 0);
  lcd.setCursor(10, 0);
  duty_detect();
  lcd.setCursor(11, 0);
  lcd.print(" ");


  readPWM(PWM4);
  lcd.setCursor(12, 0);
  lcd.print(duty, 0);
  lcd.setCursor(13, 0);
  duty_detect();
  lcd.setCursor(14, 0);
  lcd.print(" ");


  readPWM(PWM5);
  lcd.setCursor(15, 0);
  lcd.print(duty, 0);
  lcd.setCursor(16, 0);
  duty_detect();
  lcd.setCursor(17, 0);
  lcd.print(" ");




  readPWM(CLK1);
  //  lcd.setCursor(0, 1);
  //  lcd.print("FQ1_");
  lcd.setCursor(4, 1);
  //lcd.print(freq*30, 0);
  zero_freq_detect();  //(讀取頻率及輸出數值)
  lcd.setCursor(8, 1);
  blank_bit();   //(刪除最後兩個雜訊讀到的最後兩位數) , 最多僅能顯示 9999

  // 如果轉速是千位數變成百位數,則清除最後邊殘留未清除的數字)
  lcd.setCursor(7, 1);
  if ((freq * 30) < 999)
  {
    lcd.print(" ");
  }




  readPWM(CLK2);
  //  lcd.setCursor(10, 1);
  //  lcd.print("FQ2_");
  lcd.setCursor(14, 1);
  //  lcd.print(freq*30, 0);
  zero_freq_detect();
  lcd.setCursor(18, 1);
  blank_bit();

  // 如果轉速是千位數變成百位數,則清除最後邊殘留未清除的數字)
  lcd.setCursor(17, 1);
  if ((freq * 30) < 999)
  {
    lcd.print(" ");
  }





  readPWM(CLK3);
  //  lcd.setCursor(0, 2);
  //  lcd.print("FQ3_");
  lcd.setCursor(4, 2);
  //  lcd.print(freq*30, 0);
  zero_freq_detect();
  lcd.setCursor(8, 2);
  blank_bit();

  // 如果轉速是千位數變成百位數,則清除最後邊殘留未清除的數字)
  lcd.setCursor(7, 2);
  if ((freq * 30) < 999)
  {
    lcd.print(" ");
  }




  readPWM(CLK4);
  //  lcd.setCursor(10, 2);
  //  lcd.print("FQ4_");
  lcd.setCursor(14, 2);
  //  lcd.print(freq*30, 0);
  zero_freq_detect();
  lcd.setCursor(18, 2 );
  blank_bit();

  // 如果轉速是千位數變成百位數,則清除最後邊殘留未清除的數字)
  lcd.setCursor(17, 2);
  if ((freq * 30) < 999)
  {
    lcd.print(" ");
  }





  readPWM(CLK5);
  //  lcd.setCursor(0, 3);
  //  lcd.print("FQ5_");
  lcd.setCursor(4, 3);
  //  lcd.print(freq*30, 0);
  zero_freq_detect();
  lcd.setCursor(8, 3);
  blank_bit();

  // 如果轉速是千位數變成百位數,則清除最後邊殘留未清除的數字)
  lcd.setCursor(7, 3);
  if ((freq * 30) < 999)
  {
    lcd.print(" ");
  }





  /*
  //偵測PWM的頻率
  //  lcd.setCursor(17, 3);
  //  lcd.print("KHz");
   readPWM(PWM2);
    lcd.setCursor(14, 3);

  if (freq <1)
  {
     lcd.print("0  ");
  }

  else
  {
      lcd.print(freq/1000, 0);
  }
  */




}





// 偵測到 duty cycle <1 的話,就顯示 0)
void duty_detect()
{
  if (duty < 1)
  {
    lcd.print("0");
  }
}





// 將9999 , 後的數字強制清除,避免顯示亂碼)
void blank_bit()
{
  lcd.print("  ");
}







//讀取風扇頻率(CLOCK), 如果 <1 的話,就顯示 0 (6個位元都清0) ,
void zero_freq_detect()
{
  delay (10);
  if (freq < 1)
  {
    lcd.print("0     ");
  }

  else
  {
    lcd.print(freq * 30, 0);
  }

}

// ---------------------------------------------
//Takes in reading pins and outputs pwm frequency and duty cycle.

void readPWM(int readPin)
{
  
  highTime = 0;
  lowTime = 0;
  lastSeen = millis();
  while ((millis() - lastSeen) < READ_DELAY)
  {
    // tempPulse = pulseIn(readPin,HIGH,50000);
    tempPulse = pulseIn(readPin, HIGH, 50000);
    if (tempPulse > highTime) {
      highTime = tempPulse;
    }
  }
  lastSeen = millis();




  while ((millis() - lastSeen) < READ_DELAY)
  {
    // tempPulse = pulseIn(readPin,LOW,50000);
    tempPulse = pulseIn(readPin, LOW, 50000);
    if (tempPulse > lowTime) {
      lowTime = tempPulse;
    }
  }
  freq1 = ((double) 1000000) / (double (lowTime + highTime));

  if (freq1 == INFINITY) {
    freq = 0;
    duty = 0;
  }
  else {
    freq = freq1;
    duty = (100 * (highTime / (double (lowTime + highTime))));
  }

  //freq = ((double) 1000000)/(double (lowTime+highTime));
  //duty = (100*(highTime/(double (lowTime+highTime))));
}




/*
void readPWM(int readPin)
{
  


   
    highTime = pulseIn(readPin, HIGH, 50000);
   lowTime = pulseIn(readPin, LOW, 50000);

  freq1 = 1000000 / ( highTime + lowTime);
 
  if (freq1 == INFINITY) {
    freq = 0;
    duty = 0;
  }
  else {
    freq = freq1;
    duty = (100 * (highTime / (double (lowTime + highTime))));
  }

  //freq = ((double) 1000000)/(double (lowTime+highTime));
  //duty = (100*(highTime/(double (lowTime+highTime))));
}
*/

At 25KHz, the period is 40us. With a duty cycle of 16%, the HIGH pulse will be 6~7us.

On types of Arduino with atmega328, 16MHz, pulseIn() cannot measure pulses of less than about 10us, which would be about 25% duty cycle at 25KHz:

The timing of this function has been determined empirically and will probably show errors in longer pulses. Works on pulses from 10 microseconds to 3 minutes in length.

1 Like

@GolamMostafa please read post #1 more carefully. Your suggestion is not relevant.

1 Like

@knee266 I guess your next question will be "if I can't use pulseIn(), how can I do it?"

The short answer is that I do not know. But I suspect the atmega328's hardware timers could make it possible in some way. Much research will be required!

How accurately do you hope to measure the duty cycle? 1%? 0.1%?

Take a look at this topic:

This is advanced code which is way beyond your level of experience at the moment. Maybe @dlloyd can help you with your problem.

Have you considered using an RC filter to get an analog value that you can then read with an ADC?

1 Like

Using the timer1 input capture function would be the way to do it, but because it is a single input you would need to add an external multiplexer to select from one of the 5 PWM inputs.

1 Like

The newer 0- and 1-series Atmel chips have built-in functionality in the timers that can read PWM duty cycles, and can be linked to pins at will. I recall there's an Arduino board out there that has one of those on board. This may be useful for you.
I honestly forgot the details, it's been a few years since I've been playing with it, that was using an ATtiny412. I was trying to read PWM to convert this to phase cutting an AC mains supply. At least the PWM reading part worked well.

For an ATmega328p I'd probably be using the timers and pin chance interrupts. Then for added accuracy read not one but something like 128 pulses (that in turn is a nice round number in binary, so a simple bit shift rather than complicated floating point calculation for the division).

Hello PaulRB,

The target is 1% dutycycle, but I'm not a professional programmer, so it looks like I'll have to try something else, maybe use divider (ex. 7490) on schematic.

Thanks for your help.

Hello Johnerrington,

ADC is not suitable because the duty cycle has to be checked.

Thank you.

If you feed your PWM through a simple RC filter it gives you a voltage that is directtly proportional to the PWM duty cycle.
SO EG a 5V PWM signal with a 50% duty cycle will read 2.5V and
a 5V PWM signal with a 5% duty cycle will read 0.25V and
a 5V PWM signal with a 0.5% duty cycle will read 0.025V
etc.
The resolution of the ADC is 1/1024 so you should have little difficulty determining the duty cycle to an accuracy of 0.1%

Since the PWM is at 25kHz there should be little difficulty in filtering it.
A 2 component solution to your problem?

For more info

I think it is a quite impossible to measure 5 PWM signals directly on the Uno even for more experiencing programmer. You have to use PWM-t0-ADC conversion, as @johnerrington suggested, or use a more advanced MCU/ several MCU to measure a PWM signals.
And I think you have to use a hardware timers for it rather than a slow arduino functions.

Shouldn't be that hard. There are two "external" interrupts available, that covers two of the signals. Then there are three pin change interrupts, on PORTB, PORTC and PORTD respectively. That covers three more. All in parallel.
The high 25 kHz frequency will become an issue in this case, as that's only 640 clock ticks. Just the few clock ticks to get into the ISR can throw off measurements if the pulse edges are very close in time.
If it is not needed to time them in parallel, but check one, then another, it becomes quite straightforward to handle like a dozen PWM signals. Set the pin change interrupt to the signal you want to measure, take the measurement, and move on to the next. At 25 kHz you can take 100 samples of a signal, and you can still sample each signal 40 times a second if you have 10 signals to measure.

Here is some code from Nick Gammon for measuring pulses using timer1 input capture method

Uno pin 8 must be used, so to measure pulses from multiple inputs, you would need to use a multiplexer, as has already been suggested, such as 74hc151.

Hello PaulRB,

When I use 74LS90, the frequency drops to 2.5 KHz, but the duty cycle is fixed at 20%, so it doesn't work.
I thought I'd start working on the 74HC151 sketch.

Thanks.

74LS90 is a counter. It will reduce the frequency but it will not preserve the duty cycle. I would expect the duty cycle of the output to be 50%, so maybe you measured it incorrectly, but that is not important.

74HC151 is a multiplexer which would allow the Arduino to select which is the 5 (up to 8 if needed) signals are passed to the single pin which can measure the pulse width using timer 1 input capture function.

But before you make the circuit more complex, you could explore the suggestion from @wvmarle using pin change interrupts. If it can work, it would require no extra hardware.

If that cannot work, the suggestion from @johnerrington would require only a few capacitors and resistors, no extra chips, so would also be worthwhile exploring.

1 Like

@wvmarle I agree this idea would be worthwhile for @knee266 to explore because it would require no extra hardware. But because an accuracy of 1% is desired, micros() will not be sufficiently precise. A counter clocked at 25KHz x 100 = 2.5MHz or higher will be required. Perhaps timer1 can be configured with a clock divider of 4 to count at 4MHz. The pin change interrupt routine could capture the count.

Since the PWM signals to be measured are intended to control the RPM speed of fans, I expect the duty cycles of the signals will not change rapidly. So it will be sufficient to sample each signal in turn at a modest rate. @knee266 please confirm this and explain to the forum what how the duty cycle measurements will be used.

I have finished the first version of the test device, but the minimum detection duty cycle is 10%.
I haven't tried other methods yet because I'm not a professional software designer and it takes a lot of time.
If I use STM32 or Aduino 2560 will it improve?

Thanks.

The 2560 will have no improvement with the same code, as it also runs at 16 MHz.
The STM32 runs at higher clock speed, on that controller it may or may not improve depending on your code.

I expect the shortest pulses an ATmega328p or ATmega2560 can time the length of to be less than 10 clock ticks, which is 0.625 µs (16 MHz clock). A 25 kHz signal has a period of 40 µs, giving a 1.5% duty cycle minimum (and a maximum of 98.5% on the other end of the scale). Outside those limits that it can still count frequency but won't be able to reliable measure the duty cycle.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.