khoih-prog / RP2040_PWM

This library enables you to use Hardware-based PWM channels on RP2040-based boards, such as Nano_RP2040_Connect, RASPBERRY_PI_PICO, with either Arduino-mbed (mbed_nano or mbed_rp2040) or arduino-pico core to create and output PWM any GPIO pin. The most important feature is they're purely hardware-based PWM channels, supporting very high PWM frequencies. Therefore, their executions are not blocked by bad-behaving functions or tasks. This important feature is absolutely necessary for mission-critical tasks. These hardware-based PWMs, still work even if other software functions are blocking. Moreover, they are much more precise (certainly depending on clock frequency accuracy) than other software-based PWM using ISR, millis() or micros(). That's necessary if you need to control devices requiring high precision. New efficient setPWM_manual function to facilitate waveform creation using PWM
MIT License
73 stars 19 forks source link

Changing Duty Cycle Dynamically Creates Runt PWM pulse #10

Closed RockingYProductions closed 2 years ago

RockingYProductions commented 2 years ago

Arduino IDE version 1.8.13 RP2040 Core Version arduino-pico core v2.5.2 RP2040 Board type RASPBERRY_PI_PICO Using OSX 12.6

Contextual information

There was observed to be a runt or glitch generated when switching the duty cycle, that lasted only the cycle the update occurred at. This runt/glitch is detectable by our receiver, and although it only lasts one cycle, it causes jitter issues.

Is there a way to switch Duty Cycle precisely between the last complete cycle of the current PWM cycle and the next complete cycle of the new PWM duty cycle value.

Simplest possible steps to reproduce (see attached scope shot). You can pick different duty cycles and get the same effect.

const float PWMFREQ           = 100;      // PWM frequency on GPIO pin
const byte GPIO_PIN           = 19;

void setup()
{
  PWM_Instance = new RP2040_PWM(GPIO_PIN, PWMFREQ, 0);
  PWM_Instance->setPWM(GPIO_PIN, PWMFREQ, 0, true);
  ..
}
void loop()
{   
  PWM_Instance->setPWM(GPIO_PIN, 100, 50, true);
  ...
  PWM_Instance->setPWM(GPIO_PIN, 100, 20, true);
  ...
}

Attached Scope Screen Shot where the duty cycle changed.

SDS00007

khoih-prog commented 2 years ago

Hi @RockingYProductions

Thanks for the issue report. I'll certainly spend some time soon to investigate and try to fix, depending on the necessity and popularity.

This can be much harder to solve as the library is using fully hardware-based PWM, designed for very high Frequencies.

These purely hardware-based PWM channels can generate from very low (lowest is 7.5Hz) to very high PWM frequencies (in the MHz range, up to 62.5MHz).

Can you post more info about your use-case, such as

so that we can all learn from the experience.

BTW, if your use-case requires only low frequency ( < 1000Hz ) as you're using 100Hz now, try the ISR-based RP2040_Slow_PWM library, which already fixed the same issue


From RP2040_Slow_PWM v1.2.0

3. DutyCycle to be optionally updated at the end current PWM period instead of immediately

// Default is true, uncomment to false
//#define CHANGING_PWM_END_OF_CYCLE     false
khoih-prog commented 2 years ago

Hi @RockingYProductions

The new RP2040_PWM v1.4.0 has just been published. Your contribution, by requesting good enhancement, is noted in Contributions and Thanks

Please test and report if there is any more issue.

Be sure to modify your code as follows by not using phase_correct == true

#define _PWM_LOGLEVEL_        1
#include "RP2040_PWM.h"

//creates pwm instance
RP2040_PWM* PWM_Instance;

const float PWMFREQ           = 100;            // PWM frequency on GPIO pin
const byte GPIO_PIN           = 19;

float dutyCycle;

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  delay(100);

  Serial.print(F("\nStarting PWM_Glitch on ")); Serial.println(BOARD_NAME);
  Serial.println(RP2040_PWM_VERSION);

  PWM_Instance = new RP2040_PWM(GPIO_PIN, PWMFREQ, 0);
  PWM_Instance->setPWM(GPIO_PIN, PWMFREQ, 0);
}

void loop()
{   
  dutyCycle = 50;

  PWM_Instance->setPWM(GPIO_PIN, PWMFREQ, dutyCycle);

  delay(100);

  dutyCycle = 20;

  PWM_Instance->setPWM(GPIO_PIN, PWMFREQ, dutyCycle);

  delay(100);
}

Releases v1.4.0

  1. Fix glitch when dynamically changing dutycycle. Check Changing Duty Cycle Dynamically Creates Runt PWM pulse #10
  2. Adjust MIN_PWM_FREQUENCY and MAX_PWM_FREQUENCY dynamically according to actual F_CPU
  3. Update examples
RockingYProductions commented 2 years ago

Confirmed that Release v1.4.0 removes the observed glitch when changing duty cycles and verified by scope. Excellent!

In our use case, a system was wired such that it had one GPIO pin between the RPI PICO (buffered to 5v) and an Arduino UNO input pin. This generated a simple logic signal that triggered the Arduino to start a mechanical assembly. Later, it was required to have four (possibly more) trigger signals. We cannot add/move additional signals in the design.

By using the PWM output of the PICO, 10 PWM frequencies (duty cycle 0%, 10%, etc,) where chosen to represent 10 trigger outputs. The Arduino UNO used pin-change IRQ's to measure the pulse width (or lack of at 0% and 100%). 100Hz was chosen to minimize the IRQ service overhead on the UNO. The PICO can now transmit 10 (or more) 'trigger' outputs and it was acceptable that only one trigger output at a time was required.

We observed that when the PWM runt/glitch was received, the UNO would measure the pulse and create an incorrect trigger output, then create the correct trigger after the next cycle. A work around was to 'debounce' the received PWM for two consecutive PWM values, but this was adding IRQ overhead. Now we can successfully transmit may triggers without any PCB changes or IRQ overhead. Thank you very much!

khoih-prog commented 2 years ago

Great to know it's working for you now. We can all learn from your use-case and experience.

The library is better and better thanks to contributing users like you.

Best Regards,