Arduino DUE acceleration ramp with PWM

Hi!

Just want to share my findings regarding ramp generation with PWM. In case somebody wants to make a library out of the following stuff please feel free to proceed - but as there is knowledge of other people involved I would like to ask for publication in this thread.

As mentioned in this thread I started a while ago with basic PWM ramps, driven by this PWM library for the DUE. Here an example showing 2 stepper motors, travelling synchronised but with different acceleration:

#include <pwm01.h>

float stepper1_min = 50;       // Stepper1 start speed in Hz
float stepper1_max = 400;      // Stepper1 desired speed in Hz
float stepper1_actual;         // Stepper1 actual speed

float stepper2_min = 25;       // Stepper2 start speed in Hz
float stepper2_max = 200;      // Stepper1 desired speed in Hz
float stepper2_actual;         // Stepper2 actual speed

float ramp_delay = 500;        // ramp stair-step duration in micros
float ramp_relation;           // speed relation Stepper1 / Stepper2
float ramp_val = 0;            // ramp stair-step

void setup() {
  pwm_set_resolution(16);
  pwm_setup(8, 0, 1);          // setup PWM clockA at pin 8
  pwm_write_duty(8, 32767);    // setup 50% duty cycle
  pwm_setup(9, 0, 2);          // setup PWM clockB at pin 9
  pwm_write_duty(9, 32767);    // setup 50% duty cycle

  ramp_relation = (stepper1_max - stepper1_min) / (stepper2_max - stepper2_min);
  delay(100);
}

void loop() {
  accel();
  deccel();
  while (1);
}

void accel() {
  while (stepper1_actual < stepper1_max - 1) {
    stepper1_actual = stepper1_min + ramp_val;
    stepper2_actual = stepper2_min + ramp_val / ramp_relation;
    pwm_set_clockAB_freqs(stepper1_actual, stepper2_actual);
    delayMicroseconds(ramp_delay);
    ramp_val++;
  }
  pwm_set_clockAB_freqs(stepper1_max, stepper2_max);
}

void deccel() {
  while (stepper1_actual > stepper1_min) {
    stepper1_actual = stepper1_min + ramp_val - 1;
    stepper2_actual = stepper2_min + (ramp_val - 1) / ramp_relation;
    pwm_set_clockAB_freqs(stepper1_actual, stepper2_actual);
    if (stepper1_actual <= stepper1_min) {
      pwm_set_clockAB_freqs(0, 0);
      ramp_val = 0.0;
      stepper1_actual = 0.0;
      stepper2_actual = 0.0;
    }
    else {
      delayMicroseconds(ramp_delay);
      ramp_val--;
    }
  }
}

I modified the library a little bit, see attachment. I was able to accelerate the steppers up to ~3000 RPM. To verify the PWM frequencies I put together the following sketch:

// PWM Frequency verifyer for Arduino DUE

// Uses PWM library pwm01 by randomvibe and Collin Kidder
// Uses Frequency Counter Dave Curran based on http://forum.arduino.cc/index.php?topic=64219.msg1433217#msg1433217 
// frequency calculation inspired by Arduino libsam FindClockConfiguration()

#include <math.h>
#include <pwm01.h>

// PWM Pin: 8                       // PWM output at Pin8
int input_pin = 12;                 // frequency meter input at Pin12

uint32_t mck = VARIANT_MCK;
uint32_t pwm_duty = 32767;          // = 50% duty cycle
uint32_t pwm_frequency = 400;

void setup() {
  Serial.begin(115200);
  pwm_set_resolution(16);
  pwm_setup(8, pwm_frequency, 1);
  pwm_write_duty(8, pwm_duty);
  delay(100);
  Serial.println(" ");
  Serial.println(" ");
  Serial.println(" ");
}

// volatile variables used during isr calls
volatile unsigned long startTime;
volatile unsigned long endTime;
volatile unsigned long count;

// trigger isr, sometimes there is an initial false trigger
void trigger()
{
  attachInterrupt(input_pin, start, RISING);
}

// initial isr, record the start time and enable the pulse isr
void start()
{
  startTime = micros();
  attachInterrupt(input_pin, pulse, RISING);
}

// subsequent isr, record the time and increment the count
void pulse()
{
  endTime = micros();
  count++;
}

// calculate the frequency as the number of pulses received during the sample interval
float getFrequency(unsigned int sampleTime)
{
  // start counter
  count = 0;
  attachInterrupt(input_pin, trigger, RISING);

  // delay, isr calls count pulses during this time
  delay(sampleTime);

  // no more isr calls
  detachInterrupt(input_pin);

  // calculate the result, note not depended on sampleTime, using more accurate start and end times of pulses received
  if (count == 0)
  {
    return 0;
  }
  else
  {
    return float(1000000 * count) / float(endTime - startTime);
  }
}

// storage for the last frequency reading
float lastFreq = 0;

void loop()
{
  pwm_set_clockA_freq(pwm_frequency);

  float freq = 0;
  float period = 0;

  // check for low frequencies
  if (lastFreq > 5000 || lastFreq == 0)
  {
    // 1Hz sample rate
    freq = 1000 * getFrequency(1000); // mHz @ 1Hz
  }
  else
  {
    // drop sample rate to 0.3Hz for lower frequencies < 5Hz, can now read down to 1Hz
    freq = 1000 * getFrequency(3333); // mHz @ 0.3Hz
  }
  lastFreq = freq;

  // don't divide by zero
  if (freq > 0)
  {
    period = 1000000000 / freq; // uS
  }

  // split it up into 1000 chunks
  unsigned int MHz = freq / 1000000000;
  unsigned int KHz = (freq / 1000000) - (MHz * 1000);
  unsigned int Hz = (freq / 1000) - (KHz * 1000) - (MHz * 1000000);
  unsigned int mHz = freq - (Hz * 1000) - (KHz * 1000000) - (MHz * 1000000000);

  unsigned int mS = (period / 1000);
  unsigned int uS = period  - (mS * 1000);
  unsigned int nS = (period * 1000) - (uS * 1000) - (mS * 1000000);
  unsigned int pS = (period * 1000000) - (nS * 1000) - (uS * 1000000) - (mS * 1000000000);

  Serial.println(" ");
  Serial.print("Input Frequency: ");
  Serial.print(pwm_frequency);
  Serial.print("          Calculated Frequency: ");
  Serial.print(calc_freq(pwm_frequency), 3);
  Serial.print("          Measured Frequency:");
  if (MHz > 0)
  {
    print3digit(MHz, ' ');
    Serial.print(',');
    print3digit(KHz, '0');
    Serial.print(',');
    print3digit(Hz, '0');
  }
  else if (KHz > 0)
  {
    print3digit(KHz, ' ');
    Serial.print(',');
    print3digit(Hz, '0');
    Serial.print('.');
    print3digit(mHz, '0');
  }
  else
  {
    Serial.print("    ");
    print3digit(Hz, ' ');
    Serial.print('.');
    print3digit(mHz, '0');
  }
  Serial.print(" Hz");

  Serial.print("          ");
  Serial.print("Measured Period:");
  if (mS > 100)
  {
    Serial.print("    ");
    print3digit(mS, ' ');
    Serial.print('.');
    print3digit(uS, '0');
    Serial.print(" mS");
  }
  else if (mS > 0)
  {
    print3digit(mS, ' ');
    Serial.print(',');
    print3digit(uS, '0');
    Serial.print('.');
    print3digit(nS, '0');
    Serial.print(" uS");
  }
  else if (uS > 0)
  {
    Serial.print("    ");
    print3digit(uS, ' ');
    Serial.print('.');
    print3digit(nS, '0');
    Serial.print(" uS");
  }
  else
  {
    Serial.print("    ");
    print3digit(nS, ' ');
    Serial.print('.');
    print3digit(pS, '0');
    Serial.print(" nS");
  }
  pwm_frequency++;
}

// display a three digit number with padding if necessary
void print3digit(int n, char leadingchar)
{
  if (n < 100)
  {
    Serial.print(leadingchar);
  }
  if (n < 10)
  {
    Serial.print(leadingchar);
  }
  Serial.print(n);
}

float calc_freq(uint32_t des_freq) {
  des_freq *= 255;        // period = 255 as in pwm01 library
  uint32_t divisors[11] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024};
  uint8_t divisor = 0;
  uint32_t prescaler;
  prescaler = (mck / divisors[divisor]) / des_freq;
  while ((prescaler > 255) && (divisor < 11)) {
    divisor++;
    prescaler = (mck / divisors[divisor]) / des_freq;
  }
  if (divisor < 11) {
    return mck / pow(2, divisor) / prescaler / 255;
  }
  else {
    return 0;
  }
}

EDIT: minor fixes to first sketch.

pwm01.zip (6.99 KB)

With kind help of Mr. Walter Bislin the attached sketch was created including a highly optimized version of cal_freq(). I was able to reproduce <0.02mm accurate positioning on my lead screw driven linear rail. [EDIT: updated version see reply #3]

He also made a diagram explaining the sketch:

The sketch automatically compensates for the inaccuracies of the generated frequencies. But it's also possible to use it without the compensations (see comments in the code), if the DUE drives DDS chips such as the AD9850 which would result in something similar to the UKCNC Pulse Train Hat (BTW it would be a great idea to make a library to control the Pulse Train Hat). For the AD9850 there exist several libraries, here is one for the DUE which uses hardware SPI (if even faster SPI is needed it could be done with DMA, check out this library). There is a nice online calculator for the exact output frequencies of the AD9850 and other DDS chips (just in case the link dies some day I made an XLS out of the provided information which calculates the exact output frequency of the AD9850). And here you can find how to direct access the PWM registers of the DUE.

EDIT1: added links to AD9850 and DMA-SPI libraries
EDIT2: see this library I made to use DDS instead of PWM.

AD9850_output_frequency.zip (1.73 KB)

dds_module.jpg

Here is a different approach for calculating / generating PWM frequencies (I attach these calculations also as an XLS). I now keep the prescaler and calculate the period. This method outputs much closer matches to the input frequencies:

const byte enable_pin = 36;
const byte pin8 = 8;                            // PWM output at Pin 8
const byte chan8 = g_APinDescription[pin8].ulPWMChannel;
const byte pin9 = 9;                            // PWM output at Pin 9
const byte chan9 = g_APinDescription[pin9].ulPWMChannel;

const byte duty_percent = 50;                   // 50% duty cycle

#define clockA 1                                // PWM clock source A
#define clockB 2                                // PWM clock source B
uint8_t prescaler = 1;  // don't change!        // best accuracy with smallest divider(s)
uint8_t divisor;                                // smallest possible divisor will be calced
uint32_t period;                                // required value for period will be calced
uint32_t period1;                               // required value for period will be calced
uint32_t period2;                               // required value for period will be calced
uint32_t mck = VARIANT_MCK;                     // cpu clock
uint32_t mck_div;                               // divided clock
uint32_t sourceA;                               // clock source A
uint32_t sourceB;                               // clock source B
uint16_t duty;                                  // calculated duty
uint16_t duty1;                                 // calculated duty
uint16_t duty2;                                 // calculated duty

uint32_t log2_tab[64] = {
  0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
  4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
  5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
  5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
};

uint32_t lmb12(int x) {
  // find index of left most bit (lmb) position equal 1 in x
  // last bit has index 0
  // require x >= 0 and x < 4096 (max 12 bits)
  // x = 0 -> 0
  if (x >= 64) return log2_tab[x >> 6] + 6;
  return log2_tab[x];
}

void setup() {
  pwm_assign(pin8, clockA);                // Pin8 bound to PWM clock source A
  pwm_assign(pin9, clockB);                // Pin9 bound to PWM clock source B
  pwm_frequency(chan9, 1000, clockB);      // set channel8 at PWM clock source A to 1000Hz
}

void loop() {
}

void pwm_assign(const byte pwm_pin, const byte clk) {
  const byte chan = g_APinDescription[pwm_pin].ulPWMChannel;
  if (pwm_pin >= 6 && pwm_pin <= 9)
  {
    pmc_enable_periph_clk(PWM_INTERFACE_ID);
    PWMC_ConfigureClocks(0, 0, VARIANT_MCK);
    PIO_Configure(g_APinDescription[pwm_pin].pPort, g_APinDescription[pwm_pin].ulPinType, g_APinDescription[pwm_pin].ulPin, g_APinDescription[pwm_pin].ulPinConfiguration);
    if (clk == 1) PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKA, 0, 0);
    if (clk == 2) PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKB, 0, 0);
    PWMC_SetPeriod(PWM_INTERFACE, chan, 255);
    PWMC_EnableChannel(PWM_INTERFACE, chan);
  }
}

uint32_t calc_period(double freq) {
  divisor = 0;
  uint32_t ticks = mck / (freq * 256);
  if (ticks >= 262144) return 0;
  else {
    if (ticks >= 256) divisor = lmb12(ticks >> 7);
    mck_div = mck >> divisor;
    return (mck_div / freq) + 0.5;                                // return (mck_div / (prescaler * freq)) + 0.5;
  }
}

double calc_freq(double freq) {
  period = calc_period(freq);
  return (double) mck_div / period;                               // return (double) mck_div / (prescaler * period);
}

void pwm_frequency(const byte chan, double freq, const byte clk) {
  uint32_t config = 0;
  period = calc_period(freq);
  duty = period * duty_percent * 0.01;
  if (clk == 1) {
    sourceA = 0;
    if (freq != 0) sourceA = prescaler | (divisor << 8);
  }
  if (clk == 2) {
    sourceB = 0;
    if (freq != 0) sourceB = prescaler | (divisor << 8);
  }
  config |= sourceA;
  config |= (sourceB << 16);
  REG_PWM_CLK = config;
  PWM->PWM_CH_NUM[chan].PWM_CPRDUPD = period;
  PWM->PWM_CH_NUM[chan].PWM_CDTYUPD = duty;
}

void pwm_frequencies(const byte chan1, double freq1, const byte chan2, double freq2) {
  uint32_t config = 0;
  period1 = calc_period(freq1);
  duty1 = period1 * duty_percent * 0.01;
  sourceA = 0;
  if (freq1 != 0) sourceA = prescaler | (divisor << 8);
  config |= sourceA;
  period2 = calc_period(freq2);
  duty2 = period2 * duty_percent * 0.001;
  sourceB = 0;
  if (freq2 != 0) sourceB = prescaler | (divisor << 8);
  config |= (sourceB << 16);
  REG_PWM_CLK = config;
  PWM->PWM_CH_NUM[chan1].PWM_CPRDUPD = period1;
  PWM->PWM_CH_NUM[chan1].PWM_CDTYUPD = duty1;
  PWM->PWM_CH_NUM[chan2].PWM_CPRDUPD = period2;
  PWM->PWM_CH_NUM[chan2].PWM_CDTYUPD = duty2;
}

EDIT_1: with this method I was able to accelerate a Nema17 stepper up to 15.000 RPM, see this video!
EDIT_2: code now can handle 2 independent frequencies

DUE_exact_frequency.zip (3.78 KB)

I updated the sketch with the above improvements [EDIT: see #6 for latest version].

todo list::

  • build library from the code

  • ...with the possibility to control any number of external DDS boards in addition to or instead of the DUEs internal 2 PWM clock sources (see reply #1 for information about external DDS boards)

  • ...with user selectable possibility to talk to the DDS boards via hardware SPI or via DMA'ed SPI (see reply #1)

EDIT 2017/07/20: bugfixes

I think this is very interesting, however one thing puzzles me. If using PWM to generate the step profile, how does the code accurately track the number of steps moved?

bobcousins:
I think this is very interesting, however one thing puzzles me. If using PWM to generate the step profile, how does the code accurately track the number of steps moved?

It's easy to adapt the code to run the desired number of steps rather than travel in cm. Example: 200 steps/R with 16x microstepping and input travel of 8mm of a lead screw driven linear rail with a screw pitch of 8mm/R results in 3200 steps.

According to the calculations in my code one should end up with the desired number of steps. Unfortunately the time related functions such as micros() are not accurate enough to allow single step accuracy. To verify you can connect the out-pin to a pulse counter. Another way of doing it could be to poll some registers for the counter overflow of the period counter. Or you could add a formula to the while-loop in simul_weg() which sums up frequency*duration for each ramp stage and compare the result with the result of a pulse counter. In any case it should be possible to incorporate a feedback loop to compensate for the inaccuracies. Or replace micros() etc. with more accurate code.

I managed to make a pulse accurate version of the sketch, see here.

[EDIT: latest version for DUE see #8]

I am not sure to understand what you are willing to do, but if it is a smooth modification of the duty cycle to simulate a ramp (acceleration then deceleration) IMO the best option is to use the PWM synchro mode with automatic update via the PDC DMA.

With this method, you fill a phase accumulator for a given slope (acceleration then deceleration or whatever you want), you set the duty cycle update period and the DMA updates DTYUPD in the background.

ard_newbie:
I am not sure to understand what you are willing to do

Please check the diagram in #1: it's about a frequency ramp. I can take the period counter of the fastest frequency for a duty of 50% and use that for the entire ramp, or I can calculate the duty for every frequency.

I did more tests with the DUE to get more accurate number of pulses, see attached sketch. But the teensy 3.6 (see above link) can calculate faster and does the micros() related stuff more accurate.

[EDIT: attached final version of sketch]

ramp.zip (2.96 KB)

Just to let you know: there is a new and very good stepper library!

@jpk

From which site of the web, we can down load the Stepper/PWM Library in the form of Folder and not as discrete files as GitHub contains. If it is a Folder, we can just place it in the installation path of the PC.

...just click on "clone or download":

The method works.

Thanks!