Stepper-motor speed profile generation
My 4xiDraw project has been a source of inspiration for other projects. A while ago I mentioned how to add wireless connectivity to a serial-based device, but for a subject I teach I wanted to get a bit deeper on the details about stepper motor timing generation for trapezoidal (or any other) speed profile.
While this functionality is implemented in every CNC or 3D printer controller software, most of them are based on GRBL development, which is efficient but not easy to grasp on a first look. There are many different but related algorithms working together there.
Just by chance I bought a Wemos D1 board that replaces the Arduino UNO Atmega 328 by an ESP8266 but keeps the UNO form factor. It was a weird proposal but I bought anyway as we all know that anything that stamps wifi on it makes it a better product.
I have used the ESP8266 in the past, through the Arduino IDE, but I have never needed to achieve any realtime operation. But once I checked that CNCshield board could be used (and it will work ok) together with Wemos D1, I set my mind to replace the Arduino UNO of my 4xiDraw by the wifi-enabled Wemos sibling.
The good news was that the ESP8266 32 bit processor and generous flash memory space will allow me to get decent performance without much effort on my part while coding. However, I was not so sure about getting good real-time performance on the stepper's step signal.
While this functionality is implemented in every CNC or 3D printer controller software, most of them are based on GRBL development, which is efficient but not easy to grasp on a first look. There are many different but related algorithms working together there.
Just by chance I bought a Wemos D1 board that replaces the Arduino UNO Atmega 328 by an ESP8266 but keeps the UNO form factor. It was a weird proposal but I bought anyway as we all know that anything that stamps wifi on it makes it a better product.
I have used the ESP8266 in the past, through the Arduino IDE, but I have never needed to achieve any realtime operation. But once I checked that CNCshield board could be used (and it will work ok) together with Wemos D1, I set my mind to replace the Arduino UNO of my 4xiDraw by the wifi-enabled Wemos sibling.
The good news was that the ESP8266 32 bit processor and generous flash memory space will allow me to get decent performance without much effort on my part while coding. However, I was not so sure about getting good real-time performance on the stepper's step signal.
Timing is everything
Steppers are picky motors. They do not rotate unless the controller keeps sending step signals at the proper pace. Using a fixed rate is the simple alternative, but physics get in the way and this approach is somehow limited. So instead of using a fixed speed, a more common approach is to use a variable speed, starting from a low speed and ramping up to a cruise speed to later decelerate back to a stop.
Given that the speed of a stepper is directly proportional to the rate of the step signal, we need to create a signal whose rate will increase [linearly] and decrease. But for coding purposes, we need to establish the time period in-between the different steps.
Unfortunately, given the reverse nature of frequency and period, a linear increase in frequency (speed) does not translate into a linear decrease of the period. Failing to make this point, programmers are in for a big disappointment.
There is a very interesting application note by ATMEL that goes into a lot of detail on how you can calculate such timer intervals without much computing cost. This is what GRBL and Marlin and Smoothieware do.
But for a 3D printing or CNC machine we are interested on accurately controlling the position of each move too. That means that for each basic movement, a straight line is drawn from an initial point to a destination point on a multidimensional space. A certain distance to be covered over an axis comes immediately to a given number of steps. It is that total distance what will be traversed using a so called trapezoidal speed pattern that will smooth acceleration and deceleration phases so hopefully motion will happen without missing any steps. This helps the motors to reach much higher speeds than what would be possible when using only a fixed speed, which will contribute to lower 3D printing or machining times too.
For a stepper motor, the zero speed corresponds to an infinity period in-between steps. We would consider a non-zero initial speed, than will give us a value T0. This value will be decreasing at each step while accelerating and it will remain the same while motor cruises to start increasing again to slow down the stepper till it stops when no more pulses are provided to the step signal of the motor driver.
The time to cover the distance of a step can be formulated as T0 = sqrt ( 2 / accel ) and the period difference between one step n and step n+1 can be expressed as Tn+1 = Tn * ( sqrt( n + 1 ) - sqrt( n ) ). So we could use that for iteratively calculate the new time interval till the next step. Unfortunately, a couple of square roots take time to calculate, even more if using an 8-bit processor.
Luckily, a series expansion of the expression above allows us to obtain a simpler relationship that can be easily calculated so now Tn = Tn-1 - 2 * Tn-1 / ( 4 * n + 1 )
Each step there is a slight speed increase, so when the desired maximum speed is reached, no more increases happen. So that last Tn value is kept for the period of all the steps while cruising until the deceleration phase starts, that can use the same sequence of numbers (1..n) but now these will be negative numbers going from -n to -1.
What is left is to determine the amount of pulses for the acceleration and deceleration phases. For simplicity I considered the same acceleration for both of them, so they will need the same amount of steps. The remaining steps, if any, will be traveled at the maximum speed. Please note that for short movements it may not be possible to reach the desired maximum speed (feedrate) so half of the time will be used accelerating and half of the time decelerating to/from a speed lower than the maximum one.
Timer0 on ESP
One thing I have not used before was a timer on the ESP. I assumed the Servo library would use one but I did not dig into the details. However, now I wanted to make sure the timing I was carefully calculating for each step would not be disturbed by other tasks the processor might get into when communicating wirelessly.
My plan was to use a timer so each new step will be scheduled with the help of the timer, that will cause an interrupt at the right time of the next step. Being interrupt-driven should help getting the timing right.
Once configured a new call to timer0_write(ESP.getCycleCount() + 80 * microseconds) will schedule a new interrupt after that number of microseconds from now. The code of the interrupt will calculated and schedule the time on the new interrupt, plus it will perform the motion on any of the steppers, determining the end of acceleration, the begin of the deceleration or the end of the move.
However, I have found I cannot stop the timer0 from running without getting a watchdog interrupt afterwards, so I depend on whether or not there are more steps to be processed to run the motion related code or a dummy new interrupt is scheduled every 10 milliseconds, just to keep the ball rolling and avoid the watchdog from complaining. Maybe there is a better way but I settled with what worked first.
The servo
I only needed to get a servo working, so the obvious choice was to use the servo library, but for reasons unknown, it won't work. Not even when defining SERVO_EXCLUDE_TIMER0 to prevent it to use timer0. But that is not really a big deal as I can use some time in the main loop to create a pulse of the desired width (1.5 .. 2 msec) and to refreshed it every 20 msec or so. And so I did and it seems to be working nicely as shown in the sample video below:
The code that was driving the motion was being created in my desktop computer by this line of code: (while true; do X=$((RANDOM % 100)); Y=$((RANDOM %100)); Z=$((RANDOM % 1000 + 1000)); echo "M3S$Z"; echo "G1 X$X Y$Y"; sleep 2; done ) |nc -u 192.168.4.1 9999
You can get the project code and some extra details, like a logic-analyzer trace and source code from here. Almost forgot: I have based my project on Dan's code from MarginalClever.com
Comments