Introduction to the Problem
This tutorial will show how to drive a Pololu style stepper (A4988) driver using a timer interrupt. This method is non blocking, efficient, and as far as I know is pretty much what most 3D printer firmwares use.The idea is this. A pololu style stepper driver (the kind that plugs into the RAMPS board) only requires two inputs from the Arduino. One is a direction pin. The other is a pulse train. One rising edge equals one step (or micro step, depending on how the set pins are wired). Most people when they first get going with steppers probably do one of two things. 1) They use some library that does all this for them (I don't know if one exists, but maybe it does) or 2) They just throw a digitalWrite in the loop() and pulse it that way. The problem with that is that it is dependent on the speed with which the loop runs. Enter Timer Interrupts
Interrupts - Conceptually
The timer interrupt is a low level feature of the ATmega family. It is not something that is provided by Arduino, and in fact functions such as millis() and delay() are based on them. I have always been a bit surprised that Arduino does not break timer interrupts out a little. They are really pretty easy to use but are very powerful. I am not going to go into great detail on the specifics of timer interrupts because there are other sources out there. The best of which is the ATmega datasheet.The idea is this - the ATmega CPU is sitting there executing your code, pulling commands off of the stack. It does this in the same order each time. On another part of the chip there is this thing called a timer. It is counting up from 0 to some value over and over again incrementing at a set frequency. When it reaches the target value it sets a flag and goes back to 0. When that flag is set, the ATmega chip sees it and says, "it is time to execute a special piece of code. Drop everything and do it." What ever it was doing before goes back on the stack and what you put in the "interrupt service routine (ISR)" gets executed. Then it goes back to its normal business. We want to put our "pulse stepper driver" code in the ISR.
There are a couple of dangers with this, but I will just leave you with this. Keep the ISR short. Don't do any serial prints or heavy computations (floating point math) in there. Calculate those ahead of time and pull them in as compile time constants ideally.
Solving the Problem
Now I actually came up with 3 ways of solving this problem- Using a fixed rate Timer Interrupt and only pulsing on some of the ISRs
- Using CTC mode and pulsing inside the ISR
- Using a special PWM mode
This tutorial covers method 2. It uses Timer5 in Clear Timer on Compare (CTC) Mode. This allows you to call an interrupt at whatever frequency you want. If you're familiar with timer interrupts the picture below might help. Again, I will not take the time to go into that much detail on that in this post. For now, I will point you to the ATMega datasheet which covers all of this stuff and THIS post by maxembedded.
Another important point is that I use direct port manipulation in the interrupt. I will not cover that here, but there are numerous examples online of how that works in addition to the ATMega datasheet. HERE is one example. I use direct port manipulation because it is much faster. As stated above, the ISR should execute as quickly as possible.
CTC Mode - From ATMega Datasheet |
Another important point is that I use direct port manipulation in the interrupt. I will not cover that here, but there are numerous examples online of how that works in addition to the ATMega datasheet. HERE is one example. I use direct port manipulation because it is much faster. As stated above, the ISR should execute as quickly as possible.
The Practical Stuff
Copy the code below. Wire it according to pins set in the code. Change the pulses per second calculation based on your setup (change it in the ISR Location calculation too). Set targSpeed in mm/s. Then set the Z_DIR_PIN and DirFlag based on the direction you want to drive. Test your code.
I hope this is helpful to someone. If it is, please let me know in the comments. If anyone that reads this has any insight into libraries available or other methods, comment those too. Good luck!
/* * Drives stepper using a pololu stepper driver and timer interrupts * * This example uses pinouts associated with RAMPS 1.4 z-axis * * Last edited by Matthew 11/14/2016 Arduino 1.6.7 * projectsfromtech.blogspot.com * TRCCR1A/B * COM1A = 0b00 - disconnect OCR * WGM1 = 0b0100 - Fast PWM with the top value at compare match * CS1 = 0b001 - no prescaling * ICNC1 = ICES = 0b0 - doesn't apply * * */ const byte Z_STEP_PIN = 46; const byte Z_DIR_PIN = 48; const byte Z_ENABLE_PIN = 62; //62 //Interrupt Variables volatile uint16_t PulseOnISRNum = 0; volatile uint16_t isrSincePulse = 0; //============================================================================ void setup() { Serial.begin(115200); pinMode(Z_STEP_PIN, OUTPUT); pinMode(Z_ENABLE_PIN,OUTPUT); pinMode(Z_DIR_PIN, OUTPUT); //setup Timer1 TCCR5A = 0b00000000; TCCR5B = 0b00001001; TIMSK5 |= 0b00000010; //set for output compare interrupt sei(); //enables interrups. Use cli() to turn them off } float targSpeed = 2.5; // mm/s float PPS = 0; // Pulses Per Second int8_t DirFlag = 1; // Direction flag. Set this to keep track of location int32_t Location = 0; // nanometers (m*10^-9) scaled by 10^-6 to avoid floating point math in interrupt long clk = micros(); //============================================================================ void loop() { //Set Direction digitalWrite(Z_DIR_PIN, LOW); // Low is forward (based on setup) DirFlag = 1; // digitalWrite(Z_DIR_PIN,HIGH); // High is backward (based on setup) // DirFlag = -1; digitalWrite(Z_ENABLE_PIN , LOW); // Active Low // Set Speed - these calculation are based on your harware setup // - Mine are for 1/16 microstepping and an m5 threaded rod driving the stage //------------------------ for(float ind = 0 ; ind <3.0 ; ind = ind+0.0005) { targSpeed = ind; // mm/s PPS = targSpeed * 4000; //Pulses/s OCR5A = 16000000/PPS - 1; //equation from pg 146 in datasheet- removed factor of 2 b/c I am manually pulsing in an interrupt every time Serial.print("Speed (mm/s): "); Serial.print(targSpeed); Serial.print(" Loop Time (ms): "); Serial.print(micros()-clk); Serial.print(" Location (mm): "); Serial.println(Location/1000000.); clk=micros(); // Input other code here! Stepper driver will run even if this code is blocking! }} //================================================================================ ISR(TIMER5_COMPA_vect) { // digitalWrite(46, HIGH); // Driver only looks for rising edge // digitalWrite(46, LOW); // DigitalWrite executes in 16 us //Generate Rising Edge PORTL = PORTL |= 0b00001000; //Direct Port manipulation executes in 450 ns => 16x faster! PORTL = PORTL &= 0b11110111; Location = Location + 250 * DirFlag ; //Updates Location (based on 4000 Pulses/mm) }