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

Using PWM to step a stepper driver #16

Closed dinther closed 1 year ago

dinther commented 1 year ago

Thank you for making your RP2040_PWM library available. It was super easy to use. Yesterday I tried using it to drive a single stepper motor with it using a STEP/DIR style stepper driver. A TMC2209 in my case. It worked perfectly at first try.

I discussed the details with the author of arduinocontinuousstepper to hear his take on it and he thinks it has merit.

However, looking ahead there might be an issue when multiple motors are used. From what I understand from your RP2040_PWM project is that we can have 8 independent PWM frequencies but will they all run in sync.

So my question is: Are all these frequencies derived from a common clock or is there a way to achieve synchronization between PWM channels?

[edit]

Since I posted this I came across a video explaining the pico pwm in laymans terms. From it I understand that each PWM channel is basically a hardware based counter counting pulses from a common125MHz clock using their own setpoint and wrappoint. That confirms to me that yes there is a common clock and my motors will be perfectly in sync.

But please correct me if I am wrong :-)

khoih-prog commented 1 year ago

Hi @dinther

I'm glad that you find out a new way to drive the stepper-motor driver using PWM, instead of using conventional Timers, etc. The fully-hardware PIO PWM of cheap-yet-powerful RP2040 is perfect for this purpose to offload the MPU for other purposes.

You can read The RP2040's PWM excellence to appreciate RP2040 PWM if you'd like to use for more complex purpose (phase-shift PWM, etc.)

there is a common clock and my motors will be perfectly in sync.

I believe you're correct.

Best Regards,

khoih-prog commented 1 year ago

Hi @dinther

Just have an idea from you novel implementation using PWM for Stepper-Motor control, which is better solution than using Timers.

As I have many PWM-related libraries, can you make PRs to add the similar examples controlling Stepper to those libraries, as I don't have the motor as well as driver (such as TMC2209) to test.

Please inform if you're OK

Best,

dinther commented 1 year ago

I have not checked your source yet but I have been assuming that the RP2040 uses PIO for PWM because PIO can also offer up to 8 independent channels. Same limit as PWM other limitations match PIO limitations as well. You are absolutely correct that the PIO is just ideal for this from what I read and watched in PIO for beginners videos. I am way out of my depth on all this as I don't have the expertise you have.

I will have to dig deeper but a stepper library making use of PIO is the right way to go about it. I definitely avoid timers.

This code below using your library worked although after a while I noticed irregularities in the running of the motor. I have not examined that yet. I'' put it on the oscilloscope sometime this week.

// Use with Stepper-Motor driver, such as TMC2209

#define _PWM_LOGLEVEL_        1

#if ( defined(ARDUINO_NANO_RP2040_CONNECT) || defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || \
      defined(ARDUINO_GENERIC_RP2040) ) && defined(ARDUINO_ARCH_MBED)

  #if(_PWM_LOGLEVEL_>3)
    #warning USING_MBED_RP2040_PWM
  #endif

#elif ( defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || \
        defined(ARDUINO_GENERIC_RP2040) ) && !defined(ARDUINO_ARCH_MBED)

  #if(_PWM_LOGLEVEL_>3)
    #warning USING_RP2040_PWM
  #endif
#else
  #error This code is intended to run on the RP2040 mbed_nano, mbed_rp2040 or arduino-pico platform! Please check your Tools->Board setting.
#endif

#include <RP2040_PWM.h>

RP2040_PWM* stepper;

#define STEP_PIN      8
#define DIR_PIN       9

void setSpeed(int speed)
{
  if (speed == 0)
  {
    // Use DC = 0 to stop stepper
    stepper->setPWM(STEP_PIN, 500, 0);
  }
  else
  {
    //  Set the frequency of the PWM output and a duty cycle of 50%
    digitalWrite(DIR_PIN, (speed < 0));
    stepper->setPWM(STEP_PIN, abs(speed), 50);
  }
}

void setup() 
{
  pinMode(DIR_PIN, OUTPUT);

  Serial.begin(115200);

  while (!Serial && millis() < 5000);

  delay(100);

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

  // Create PWM object and passed just a random frequency of 500
  // The duty cycle is how you turn the motor on and off
  stepper = new RP2040_PWM(STEP_PIN, 500, 0);
}

void loop() 
{
  setSpeed(1000);
  delay(3000);

  // Stop before reversing
  setSpeed(0);
  delay(3000);

  // Reversing
  setSpeed(-500);
  delay(3000);

  // Stop before reversing
  setSpeed(0);
  delay(3000);
}
khoih-prog commented 1 year ago

Hi @dinther

Just add the example PWM_StepperControl, with your credit notes.

https://github.com/khoih-prog/RP2040_PWM/blob/b3d063aafcb526336290e5578c6149a90c67f2f9/examples/PWM_StepperControl/PWM_StepperControl.ino#L13

Please check, test and report / fix errors

Best,

khoih-prog commented 1 year ago

Please try this if OK, with stepper stops before reversing

// Use with Stepper-Motor driver, such as TMC2209

#define _PWM_LOGLEVEL_        1

#if ( defined(ARDUINO_NANO_RP2040_CONNECT) || defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || \
      defined(ARDUINO_GENERIC_RP2040) ) && defined(ARDUINO_ARCH_MBED)

  #if(_PWM_LOGLEVEL_>3)
    #warning USING_MBED_RP2040_PWM
  #endif

#elif ( defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_RASPBERRY_PI_PICO) || defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || \
        defined(ARDUINO_GENERIC_RP2040) ) && !defined(ARDUINO_ARCH_MBED)

  #if(_PWM_LOGLEVEL_>3)
    #warning USING_RP2040_PWM
  #endif
#else
  #error This code is intended to run on the RP2040 mbed_nano, mbed_rp2040 or arduino-pico platform! Please check your Tools->Board setting.
#endif

#include <RP2040_PWM.h>

RP2040_PWM* stepper;
float frequency;
float dutyCycle;
#define STEP_PIN      8
#define DIR_PIN       9

void setSpeed(int speed)
{
  if (speed == 0)
  {
    // Use DC = 0 to stop stepper
    stepper->setPWM(STEP_PIN, 500, 0);
  }
  else
  {
    //  Set the frequency of the PWM output and a duty cycle of 50%
    digitalWrite(DIR_PIN, (speed < 0));
    stepper->setPWM(STEP_PIN, abs(speed), 50);
  }
}

void setup() 
{
  pinMode(DIR_PIN, OUTPUT);

  Serial.begin(115200);

  while (!Serial && millis() < 5000);

  delay(100);

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

  //  Create PWM object and passed just a random frequency of 500 in it
  //  The duty cycle is how you turn the motor on and off
  stepper = new RP2040_PWM(STEP_PIN, 500, 0);
}

void loop() 
{
  setSpeed(1000);
  delay(3000);
  setSpeed(0);
  delay(3000);
  setSpeed(-500);
  delay(3000);
}
khoih-prog commented 1 year ago

Hi @dinther

The new RP2040_PWM releases v1.4.1 has just been published. Your contribution is noted in Contributions and Thanks

Best Regards,


Releases v1.4.1

  1. Add example PWM_StepperControl to demo how to control Stepper Motor using PWM. Check Using PWM to step a stepper driver #16
  2. Use allman astyle and add utils
dinther commented 1 year ago

Oh wow, I get to it as soon as I have a clear head. It's been quite the weekend ;-) I just might have to send you some hardware to play with.

dinther commented 1 year ago

I left it running for 30 minutes and as you can see that code worked perfectly fine.

https://user-images.githubusercontent.com/1192916/213936159-41df680a-f3ec-4c94-9715-28591648fbfe.mp4

In the reality stepper motors never suddenly get engaged to a speed. Inertia and poor torque causes the motor to loose track with the rotating field and it could just sit there and rattle instead.

Normally an acceleration mechanism is built in.

I did a crude one where I increased the speed in a for loop. Increasing the speed one unit at the time didn't work well. The acceleration was not smooth and the wheel just decelerated in two steps. But the below code in steps of 10 did work.

void loop() 
{
  int i;
  for (i = 1; i < 100; ++i)
  {
    setSpeed(i*10);
    delay(30);
  }
  for (i = 100; i > 0; --i)
  {
    setSpeed(i*10);
    delay(30);
  }

  for (i = 0; i > -100; --i)
  {
    setSpeed(i*10);
    delay(30);
  }

  for (i = -100; i < 0; ++i)
  {
    setSpeed(i*10);
    delay(30);
  }
}

That is of course incredibly inefficient. First of all I have no idea how many calls are made as a result of calling stepper->setPWM(STEP_PIN, 500, 0); and the use of delay is bad. But it worked OK.

https://user-images.githubusercontent.com/1192916/213938249-947a239a-39ef-486f-8186-35272c7c7f2b.mp4

The TCM2209 is an incredibly advanced driver chip rarely used to it's potential. Currently used in it's most basic form: using STEP and DIR pins. I already have working code to set the finer features of the chip via UART. The most notable features of the TMC2209 are the ability to micro step and allows a user specified max current to the motor coils by using what they call the Chopper. But from what I understand this is all just PWM based

Realizing the speed and capability of the Pico PIO I think it should be possible to move the most important features of the TMC2209 into the pico POI and replace the TCM2209 driver with basic power electronics in the form of a few mosfets. The resulting footprint of the pico and a few mosfets would be tiny.

khoih-prog commented 1 year ago

Hi @dinther

Nice tests and videos.

As you know, the examples are just simple demo for basic way to use the libraries, and everybody has to test and adapt to the real-world use-case.

Your demo videos as well as comments here are so useful for interested users to start experimenting with Stepper-Motors using PWM.

I don't see any issue in changing the speed in smaller steps, as you demonstrate here, to smooth our the movements, and very simple to implement in the code.

Realizing the speed and capability of the Pico PIO I think it should be possible to move the most important features of the TMC2209 into the pico POI and replace the TCM2209 driver with basic power electronics in the form of a few mosfets. The resulting footprint of the pico and a few mosfets would be tiny.

It'd be good if you can improve, or even design a new simpler and cheaper chip (compared to TCM2209). But design new circuit is not so simple for many users. It'll be beneficial if you can post the final working simpler circuit (RP2040 + MOSFETs) to replace TCM2209.

Cheers

khoih-prog commented 1 year ago

Another note is that your idea of using PWM for Stepper-Motor has been spreading fast to the libraries of mines

  1. RP2040_PWM
    1. AVR_PWM
    2. megaAVR_PWM
    3. ESP32_FastPWM
    4. SAMD_PWM
    5. SAMDUE_PWM
    6. nRF52_PWM
    7. Teensy_PWM
    8. ATtiny_PWM
  2. Dx_PWM
  3. Portenta_H7_PWM
  4. MBED_RP2040_PWM
  5. nRF52_MBED_PWM
  6. STM32_PWM
khoih-prog commented 1 year ago

If you think it's beneficial, I can write and add some library's functions to smooth-out the Stepper movement, so that you can specify the acceleration rate, interval, acceleration-steps, etc.

dinther commented 1 year ago

In my application I don't need acceleration because I manage that in my own code. Currently I am currently using this library https://github.com/bblanchon/ArduinoContinuousStepper It's the continuous rotation aspect that drew me to this library. PWM is of course perfect for continuous rotation. Most Stepper motor users will be more interested in position operation or in other words: Rotate the stepper motor x steps cockwise with a certain acceleration deceleration profile.

I am pleased you ran with the PWM idea and embraced some out of the box thinking. I need to experiment and educate myself with PIO and get a better handle on what might be possible. Replacing the stepper driver with mosfets is actually not that simple.

dinther commented 1 year ago

Most users would be using much cheaper stepper drivers such as the DRV8825 or A4988 They work the same way with a step and dir pin but with microstep capability of 16 steps for the A4988 and 32 steps for the DRV8825

However the TMC2209 can do 256 microsteps and for me more importantly it has a build in variable pulse generator. This made it possible to simply set the VACTUAL register in the chip to a value via the UART and that was it. The chip did all the work.

The reason I stopped using it is that the clock speeds between TMC2209 chips vary constantly due temperature. I need absolute synchronization. The chips do have an external clock option but I can not access that So I do it the old Step/Dir way.

khoih-prog commented 1 year ago

Hi @dinther

Also check the new ContinuousStepper_Generic library for your Contributions


Initial Releases v1.0.0

  1. Initial coding to use PWM to control continuous Stepper Motor. Check Using PWM to step a stepper driver #16
  2. Use allman astyle and add utils
dinther commented 1 year ago

Oh wow super cool. I will try it in my pico project tomorrow. Thanks for the credit.

khoih-prog commented 1 year ago

Hi @dinther

Try this for 8.0Hz. You can go to 7.5+ Hz if CPU_F set to 125MHz, and 7.98Hz for 133MHz

Note: Must use float, such as 8.0f or 7.51f. The auto conversion is wrong if use only 8

#define _PWM_LOGLEVEL_        4
#include "RP2040_PWM.h"

//creates pwm instance
RP2040_PWM* PWM_Instance;

float frequency;
float dutyCycle;

#define pinToUse      19    //25

void printPWMInfo(RP2040_PWM* PWM_Instance)
{
  uint32_t div = PWM_Instance->get_DIV();
  uint32_t top = PWM_Instance->get_TOP();

  Serial.print("Actual PWM Frequency = ");
  Serial.println(PWM_Instance->getActualFreq());

  PWM_LOGDEBUG5("TOP =", top, ", DIV =", div, ", CPU_freq =", PWM_Instance->get_freq_CPU());
}

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

  while (!Serial && millis() < 5000);

  delay(100);

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

  frequency = 200;

  //assigns pin 25 (built in LED), with frequency of 20 KHz and a duty cycle of 0%
  PWM_Instance = new RP2040_PWM(pinToUse, frequency, 0);

  frequency = 8.0f;
  dutyCycle = 50.0f;

  PWM_Instance->setPWM(pinToUse, frequency, dutyCycle);

  printPWMInfo(PWM_Instance);
}

void loop()
{
}
khoih-prog commented 1 year ago

Debug Terminal

freq = 8.0f @ 125MHz CPU_F
Starting PWM_Basic on RASPBERRY_PI_PICO
RP2040_PWM v1.6.0
[PWM] _PWM_config.top = 62499 , _actualFrequency = 200.00
[PWM] _PWM_config.top = 61273 , _actualFrequency = 8.00
[PWM] Changing PWM frequency to 8.00 and dutyCycle = 50.00
[PWM] pin =  19 , PWM_CHAN = 1
[PWM] PWM enabled, slice =  1 , _frequency =  8.00
Actual PWM Frequency = 8.00
[PWM] TOP = 61273 , DIV = 255 , CPU_freq = 125000000
freq = 7.55f @ 125MHz CPU_F
Starting PWM_Basic on RASPBERRY_PI_PICO
RP2040_PWM v1.6.0
[PWM] _PWM_config.top = 62499 , _actualFrequency = 200.00
[PWM] _PWM_config.top = 64925 , _actualFrequency = 7.00
[PWM] Changing PWM frequency to 7.55 and dutyCycle = 50.00
[PWM] pin =  19 , PWM_CHAN = 1
[PWM] PWM enabled, slice =  1 , _frequency =  7.55
Actual PWM Frequency = 7.00
[PWM] TOP = 64925 , DIV = 255 , CPU_freq = 125000000
freq = 8.0f @ 133MHz CPU_F
Starting PWM_Basic on RASPBERRY_PI_PICO
RP2040_PWM v1.6.0
[PWM] _PWM_config.top = 66499 , _actualFrequency = 200.00
[PWM] _PWM_config.top = 65195 , _actualFrequency = 8.00
[PWM] Changing PWM frequency to 8.00 and dutyCycle = 50.00
[PWM] pin =  19 , PWM_CHAN = 1
[PWM] PWM enabled, slice =  1 , _frequency =  8.00
Actual PWM Frequency = 8.00
[PWM] TOP = 65195 , DIV = 255 , CPU_freq = 133000000