Easy Build: How to Implement a Linear Stage With Raspberry Pi Project

Contributed By Digi-Key's North American Editors

Single board computers (SBCs) like the Raspberry Pi make it easy to build complex machines in just a few hours instead of starting from scratch with a bare microcontroller. To show just how easy it can be to use a modern SBC, this “Easy Build” session will take you through the steps required to build and code a linear stage, a platform that moves back and forth in a linear direction and is used by system engineers or researchers to do repetitive tasks.

Along with taking you through the steps, we will also include the code used to demonstrate the linear stage in an accompanying video, as well as a bill-of-material for the project to help get you off the ground.

Why a linear stage?

We started this project with the idea of demonstrating how easy it is to get a Raspberry Pi microcomputer set up to do a mildly complex task. While we wanted to do more than blink lights, we also didn't want to try and calculate orbital mechanics. We settled on controlling a stepper-motor-actuated linear stage. Spinning a stepper motor is, after all, right between blinking LEDs and calculating the n-body problem for three celestial objects.

We divided our project into three sections: the electronics hardware, software, and the mechanics.

Electronics hardware

Image of full system layout showing all the hardware elements (click for full-size)

Figure 1: The full system layout showing all the hardware elements, from the Raspberry Pi Model B to the limit switches (upper left). (Diagram drawn using Digi-Key Scheme-it)

This bit was by far the easiest and most straightforward. It is immediately obvious that along with a Raspberry Pi 2 (Rpi), we were going to need a stepper motor, a stepper motor driver, and a power supply for those last two devices. Then we figured out what inputs we wanted. Ultimately, we decided on the following:

- Move left button

- Move right button

- Hi/Low speed switch

- Left limit switch

- Right limit switch 

We laid out the circuitry on a B&K Precision GS-830 breadboard. As the name suggests, it has 830 tie points, as well a bus strip.

Image of carriage pressing against the limit switch

Figure 2: The carriage is shown pressing against the limit switch, seen here as the blue box with a plunger located under the lead screw. (Source: Digi-Key Electronics) 

For the inputs, left and right were completely arbitrary. We just needed to give names to the two opposite directions. Notice that the limit switches are wired up as “normally high” with a pull-up resistor. We did this to provide a bit of noise immunity to those inputs. The Raspberry Pi can be surprisingly sensitive and we have found that external sources of noise like CFL lighting and inductors like a relay coil can sometimes trigger inputs on a Pi.

For the stepper motor we opted for the 23KM-K2 from NMB Technologies Corporation (Figure 3). The stepper driver we used was a G210X from Geckodrive. Geckodrives are a bit pricier than many of the inexpensive no-name drivers out there, but they provide smooth, trouble free, stepper control. Also, we already had one. Most inexpensive stepper motor drivers like the G210x are controlled via a DIRECTION pin and a STEP pin. This is great for simple projects: we just wire those pins directly to free GPIO pins on our Rpi.

Image of NMB Technologies 23KM-K2 standard hybrid motor

Figure 3: For the step motor, we opted for NMB Technologies Corp.’s 23KM-K2 standard hybrid motor. (Source: NMB Technologies Corp.)

We also wired the ENABLE pin from the Geckodrive to the Rpi. We did this so that the driver was only “on” and powering the coils of the motor when a program was running. If we didn't do this, the driver would just dump power into the motor as soon as we powered up the whole circuit, even though the driver wasn't receiving any instructions to do anything.

Dumping power into the motor coils does a couple things. First, it causes the motor to have a braking effect. The motor would actively resist any attempt to rotate its shaft. This is a desirable effect in some situations; like when you move the carriage to a certain point and you want it to lock in place so it can't be bumped out of position.

The second effect, and what we wanted to avoid, was that the motor would just sit there, heating up. The circuit was active for all the hours we were editing and testing our code. If we didn’t deactivate the driver, it would just keep dumping power into our poor motor. Stepper motors typically heat up, but we figured it best not to let it cook in its own heat, while it was waiting for instructions.

Image of XP Power SHP350 series 36 V switching power supply

Figure 4: A 36 VDC XP Power SHP350 series 36 V switching power supply was used to power the system, replacing a 24 V supply in order to increase motor speed. (Source: XP Power)

Originally, we wired up a 24 VDC power supply for the motor. Then we decided that we wanted to go a little faster, so we found a 36 VDC power supply from XP Power’s SHP350 line of single-output, 350 W AC-DC supplies. This did give us a noticeable speed boost over the 24 V supply.

The gecko driver limits the maximum current through the motor coils. You can set the current limit with either a resistor between terminals 11 and 12, or you can use the DIP switches on the drive. Incidentally, that same bank of DIP switches control the microstepping mode of the drive. We will discuss this in a bit more detail under the software section.

Software

The software routines are shown at the bottom of this page. Right away we decided that we wanted two programs: one for manually controlling the stage, and another program that would just send the stage carriage bouncing back and forth between limit switches. All code was written in Python using IDLE 3.2.1. We used Python because it is the default language of the Rpi. IDLE is a bare bones development environment for Python that comes with Raspbian.

Why Python? Don't let the idea of a new language scare you. Python is so similar to C that even if you can't write Python code, you can almost certainly read and understand what the program is meant to do if you are familiar with C.

What our manual control program does is outlined pretty clearly, but it does not cover any of the “whys”. Specifically, the issue of motor acceleration. A stepper motor must be ramped up to speed. I know that many readers will say that they have seen or controlled stepper motors before and that the motors just moved as fast as they could feed pulses to them. That may be so, but we are betting that in these instances, the motors were very small and/or, were under no load whatsoever.

The motor in our linear stage is most definitely under load. A loaded stepper must be brought up to speed or else the motor will stall. It will just stutter in place as pulses are fed into its driver. This is why the acceleration routine was put into the software. The acceleration is quick enough to not be very noticeable by an observer going from a standstill to maximum speed, but slow enough not to stall.

The acceleration routine starts turning the motor very slowly (steppers have the highest torque at low speeds) to exert maximum torque into the system, thereby overcoming inertia and “stiction”. Then the routine rapidly decreases the time between step pulses until the maximum speed is reached. Torque drops as the rotational speed increases, but by then, if adjusted properly, mechanical resistance in the system has been overcome.

A quick word on the meaning of the word “stiction”. A true explanation of this term is well beyond the purpose of this project breakdown. However, we offer an analogy to help to further understand. Imagine grasping the knob or handwheel of a machine and attempting to rotate it. Very often, there is an initial resistance to your effort that must be overcome. Once overcome, the knob or handwheel becomes easier to rotate, now requiring far less torque. That resistance at the beginning, for our purposes today, is stiction. This phenomenon is especially noticeable on machines that utilized sliding tables in their operation, like a lathe or a milling machine.

We did notice one significant limitation with Python. There was a limit on how quickly we could toggle the step pin going out to the motor driver. After a point, it did not matter how small of a delay we put between our 5 µs step pulses because an inherent delay was present between command executions in the Python code. We don’t know exactly what is causing this or how to overcome it. The best we could do was work around it and that is why we set the motor driver to Full_Step mode.

Had we used any sort of microstepping, we would have been dividing out maximum pulse rate (as dictated by the particulars of Python) by the number of microsteps we switched the driver to.

So, just half-stepping the motor would have divided our max speed by two. With 1/10 microstepping, we would have divided the max speed by 10! Microstepping is great when you are going for smoother, less jerky motion, but for this project's purposes it just wasn't needed.

A word on microstepping

Bi-polar stepper motors of the type we used here are almost always constructed to have 200 steps per revolution. That is, 200 “full” steps will spin the motor shaft once around. Now, let's say we set our motor driver to ¼ microstepping. What we just did, is divide each full step of the motor into 4 smaller steps. Each STEP pulse received by our motor driver rotates the motor shaft, ¼ of a full step. That means that now, we must send 800 pulses to the driver to rotate the shaft through one complete revolution.

What about 1/10 microstepping? Well now, you need to send 2000 pulses for a complete revolution. At a glance this looks great. You are increasing the positional resolution of the motor. Yes you are, up to a point: there are physical limitations to how small a step you can take. Also, the more you microstep, the less available torque you will have.

Manual control program breakdown

First we import the libraries for time and GPIO. Next we setup the pins for each variable and the inputs/outputs of those pins.

The direction buttons and speed switch are set up to use the internal pull-down resistors. The limit switches are setup to use the internal pull-up resistors.

The speed and acceleration variables are set in the beginning and can be globally changed to adjust speed and acceleration of the motor in the running program.

The program just runs a “while” loop as the main loop, scanning the “left” and “right” buttons.

Once one of these buttons is pushed, the rampUp() subroutine executes the motor to start accelerating to top speed in the direction of the pushed button.

As long as the button is held down, the program stays in the rampUp() routine. When the button is released, the rampUp() routine jumps into the rampDown() routine to decelerate the motor to a stop. The rampDown() decelerates until the deceleration number of steps runs out. Then the program returns to the main loop to check the direction buttons. The speed switch has two settings, Hi and Low. The switch changes the speed variable to the corresponding speed variable.

While the left or right direction button is being held down, along with spitting out step pulses, the stepping routine is also checking to see if either of the limit switches have been activated. When the moving stage hits the limit switch, the motor stops, changes direction, and executes a 50-step (decelPulseCount) move opposite the original direction of travel. This moves the carriage far enough off the limit switch so it isn't being pressed anymore.

Bounce back and forth program

Once the program is run from the Raspberry Pi, the linear stage will move at a fixed speed in one direction until it hits one of the limits. Then changes direction, ramps up to speed, and continues on in the other direction until it hits the limit on the other side of the stage’s travel and repeats the same process. The speed can be toggled between a preset high or low rate with the same switch we used in the manual control program.

Mechanical build

Image of final build of the linear stage

Figure 5: The final build of the linear stage, put together using-off-the-shelf parts, including 1.5-inch aluminum extrusion and an 8-thread-per-inch leadscrew with 8 starts. (Source: Digi-Key Electronics)

The linear stage was constructed almost completely from parts we already had on the shelf: 1.5-inch aluminum extrusion for the frame, clamps, shafts, motor mounts, and a 0.5-inch-diameter, 8-thread-per-inch leadscrew with 8 starts. Eight starts means that the screw has 8 parallel threads, similar to orange juice container caps. This resulted in a mechanical system that would move 1 inch per complete revolution of the leadscrew. We did this so that the videos of the stage in action would be a little more exciting. We did have a 10-thread-per-inch, single-start screw, but that would have resulted in excruciatingly slow movement. A limitation of stepper motors is that their maximum rotational speed is actually pretty slow.

The carriage rides on a pair of 20 mm linear shaft and mating bearings. We grabbed a generic NEMA 23 mounting bracket for the NEMA 23 bipolar stepper motor we used. You can see in the pictures and video that we transmitted mechanical power from the motor to the end of the shaft with cogs and a timing belt so that we could adjust the gear ratio in case we had problems with vibration or mechanical stiction. A bit of experimentation revealed that a 1:1 ratio between motor and shaft worked just fine.

The one part that we did have to do a bit of design work and fabrication on was the pillow block located at the driven end of the leadscrew (Figure 6). A pillow block is a mechanical element that supports a shaft and, in this case, transmits only rotational motion and blocks linear motion (at least theoretically). Could we have gotten away without a pillow block? Perhaps. We could have coupled the leadscrew directly to the stepper motor output shaft, but if we did go that route, we would have had a system that was living on borrowed time. Therefore, we designed and built one that attached right to the 20 mm linear shafts on the linear stage.

Image of pillow block used to support the shaft and transmit only rotational motion

Figure 6: For a longer-life design, a pillow block was used to support the shaft and transmit only rotational motion, while blocking linear motion (at least theoretically). (Source: Digi-Key Electronics)

The bearings inside the stepper motor are usually only setup to handle a lateral force and not an axial force. Over time, as the carriage went back and forth, those motor bearings would be subject to stresses that they were not designed to take. This would eventually destroy the motor. How quickly this might have happened we didn't know. However, we didn't want the motor to wear out in the middle of the project, so we just went ahead and fabricated a pillow block out of some thrust bearings, a needle bearing, and a 1/4" shaft we had on our parts shelf. The needle bearing (red) retains the 1/4" shaft right in the middle of the block and the thrust bearings (blue) handle any axial force transmitted from the leadscrew.

Finally, we positioned the limit switches so that the carriage tapped them before crashing into their mechanical limits of travel. Crashing the carriage wouldn't have been catastrophic in this case, but it would certainly start to wear out the timing belt and cogs, so we tried to avoid it.

Here’s the complete project in action:

Conclusion

SBCs like the Raspberry Pi Model B have made the design and implementation of practical, useful systems a lot easier for engineers and researchers. This step-by-step Easy Build guide provided a good guide through the process of designing a linear stage, as a practical example, while providing insight into component selection and design decisions along the way. Along with a bill of materials and related code, it is an excellent starting point for your next idea.

Bill of materials:

  1. Seeed Raspberry Pi 2 Model B
  2. XP Power: 36 V power supply
  3. NMB Technologies Corporation: Stepper motor (Part going non-stock.)
  4. B&K Precision: Breadboard
  5. Project wires
    1. Bud Industries
    2. MikroElektronika
  6. Honeywell: Limit switches
    1. DTE6-2RQ9
    2. NGCMB10AX01R
  7. Judco Manufacturing: Surface-mount switches
  8. C&K Components: JS Series slide switch

Additional Parts:

Gecko 210X stepper motor controller

½” 8 start leadscrew

Bass leadscrew nut

Bearings for pillow block

Shaft coupler

Pulleys

Timing belt for the motor drive

Aluminum extrusion

Linear bearings

Precision rods

Two custom mounts, carriage and pillow block

CODE:

Code, Push Button Control

 

Send clockwise (CW) and counterclockwise (CCW) pulses out to a stepper driver; react to limit switches; spin motor opposite to its initial direction of travel to "back-off" a limit switch.

 

"""

"""

Import Modules and/or module sections

"""

import RPi.GPIO as GPIO

import time

 

"""

Set I/O pin masks

"""

 

step = 18       # step signal to driver

directionPin = 23  # direction signal to driver

enable = 24     # enable signal to driver

button1 = 13    # direction pin

button2 = 5

output1 = 19

output2 = 12

output3 = 21

speedHiLo = 6

limitLeft = 12

limitRight = 16

 

"""

Set up general-purpose IO

"""

GPIO.setmode(GPIO.BCM)          #configure pin layout

GPIO.setwarnings(False)

 

GPIO.setup(step, GPIO.OUT)     

GPIO.output(step, GPIO.LOW)

 

GPIO.setup(directionPin, GPIO.OUT)

GPIO.output(directionPin, GPIO.LOW)

 

GPIO.setup(enable, GPIO.OUT)

GPIO.output(enable, GPIO.HIGH)

 

 

GPIO.setup(button1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(button2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(speedHiLo, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(limitLeft, GPIO.IN, pull_up_down=GPIO.PUD_UP)

GPIO.setup(limitRight, GPIO.IN, pull_up_down=GPIO.PUD_UP)

 

 

"""

General config & declarations

"""

 

global lospeed, hispeed, startTime, endTime, limitFlag, timeVar, accelTime, decelTime,decelPulseCoun, flag1, highPulse, direction, limitPulseCount

 

flag1 = 0               # variable for loop control

timeVar = 0             # variable for loop control

direction = 0           # variable for storing direction pin value

endTime = 0             # variable for storing wait time between pulses whilst slowing to a stop

limitFlag = 0           # signals that limit has been reached and flow must return to main loop

 

highPulse = 0.0001    # time for pulses to go high to trigger driver

 

 

startTime = 0.001       # time between pulses at start of movement

hispeed = 0.0001       # time between pulses at full speed

lospeed = 0.001         # time between pulses

 

 

accelTime = 0.000003   # amount of time to decrement between acceleration pulses

decelTime = 0.00005    # amount of time to increment between deceleration pulses

 

 

decelPulseCount = 50   # number of pulses sent during deceleration, 1/4 rev for current setup

limitPulseCount = 200  # number of pulses sent to the driver when a limit is tripped, 1/2 rev for current setup

 

 

"""

Function Definitions

"""

 

def rampUp():

        GPIO.output(enable, 1)  # enable driver for movement

        timeVar = startTime #initialize time variable with starting time between pulses

        global flag1

        global limitFlag

        global direction

        flag1 = 1  # set flag HI

 

        if(GPIO.input(button1)):

                direction = 1

                GPIO.output(directionPin, direction)

                         # light an LED

        if(GPIO.input(button2)):

                direction = 0

                GPIO.output(directionPin, direction)

                        # light an LED

   

        while((GPIO.input(button1) or GPIO.input(button2) == 1)):

                if(GPIO.input(limitLeft)== 0): # if a limit input goes LOW, call the limit function

                        limit()

                if(GPIO.input(limitRight)== 0): #if a limit input goes LOW, call the limit function

                        limit()

                if(limitFlag == 1):

                        limitFlag = 0

                        break

               

                GPIO.output(step, 1)

                time.sleep(highPulse)

                GPIO.output(step, 0)

                time.sleep(abs(timeVar))                               

                if(timeVar > endTime):

                        timeVar = timeVar - accelTime #decrease time between pulses until they reach endTime

                           

 

def rampDown():

        global flag1

        flag1 = 0

        global timeVar

        timeVar = endTime #initialize time variable with ending time bewteen pulses

       

        x = decelPulseCount

        while(x > 0):

                x = x - 1

                GPIO.output(step, 1)

                time.sleep(highPulse)

                GPIO.output(step, 0)      

                time.sleep(abs(timeVar))

                       

                if(timeVar < startTime):

                        timeVar = timeVar + decelTime

                     

 

def limit():  # this routine is like the ramp down routine

        print("Limit Hit")

        time.sleep(0.015) #wait for a bit

        if(GPIO.input(limitLeft) and GPIO.input(limitRight) == 1): # debounce

                return 

       

        GPIO.output(enable, 1) # enable driver for movement

        global direction

        global timeVar

        global limitFlag

        global flag1

       

        flag1 = 0 # disable the flag so u dont call rampDown upon exiting limit()

        timeVar = endTime  #initialize time variable with ending time bewteen pulses

       

        direction = not direction

        GPIO.output(directionPin, direction)

        limitFlag = 1 # set this flag so that the rampUp routine 'breaks' and jumps back to Main()

        x = limitPulseCount

        while(x > 0):               

                x = x - 1

                GPIO.output(step, 1)

                time.sleep(highPulse)

                GPIO.output(step, 0)      

                time.sleep(abs(timeVar))

                       

                if(timeVar < startTime):

                        timeVar = timeVar + decelTime

                           

"""

MAIN Loop

"""

 

while(1): #loop forever, check for button presses, speed changes and limit trips

         #disable driver to keep from overheatings

                       

        if(GPIO.input(button1) or GPIO.input(button2) == 1): # button pressed, call rampUp function

                rampUp()

 

        if(flag1 == 1):

                rampDown()   

 

        # movement over, deactivate direction LEDs

       

 

 

        if(GPIO.input(speedHiLo) == 0):

                endTime = lospeed       #if lo-speed selected, initialize endTIme with lo-speed wait time

               

        else:

                endTime = hispeed       #if hi-speed selected, initialize endTIme with hi-speed wait time

                 # light up red LED to indicate hi-speed mode

 

 

 

        if(GPIO.input(limitLeft)== 0): # if a limit input goes LOW, call the limit function

                limit()

 

        if(GPIO.input(limitRight)== 0): #if a limit input goes LOW, call the limit function

                limit()

 

 

Code, Bounce Back and Forth

 

import RPi.GPIO as GPIO

import time

 

"""

Set I/O pin masks

"""

 

step = 18       # step signal to driver

directionPin = 23  # direction signal to driver

enable = 24     # enable signal to driver

button1 = 13    # direction pin

button2 = 5

output1 = 19

output2 = 12

output3 = 21

speedHiLo = 6

limitLeft = 12

limitRight = 16

 

"""

Set up general purpose IO

"""

GPIO.setmode(GPIO.BCM)          #configure pin layout

GPIO.setwarnings(False)

 

GPIO.setup(step, GPIO.OUT)     

GPIO.output(step, GPIO.LOW)

 

GPIO.setup(directionPin, GPIO.OUT)

GPIO.output(directionPin, GPIO.LOW)

 

GPIO.setup(enable, GPIO.OUT)

GPIO.output(enable, GPIO.HIGH)

 

 

GPIO.setup(button1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(button2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(speedHiLo, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(limitLeft, GPIO.IN, pull_up_down=GPIO.PUD_UP)

GPIO.setup(limitRight, GPIO.IN, pull_up_down=GPIO.PUD_UP)

 

 

 

"""

General config & declarations

"""

 

global lospeed, hispeed, startTime, endTime, limitFlag, timeVar, accelTime, decelTime,decelPulseCoun, flag1, highPulse, direction, limitPulseCount

 

flag1 = 0               # variable for loop control

timeVar = 0             # variable for loop control

direction = 0           # variable for storing direction pin value

endTime = 0             # variable for storing wait time between pulses whilst slowing to a stop

limitFlag = 0           # signals that limit has been reached and flow must retuen to main loop

 

highPulse = 0.000005    # time for pulses to go high to trigger driver

 

 

startTime = 0.001       # time between pulses at start of movement

hispeed = 0.0005      # time bewtween pulses at full speed

lospeed = 0.0009         # time between pulses

 

 

accelTime = 0.000003    # amount of time to decrement between accleration pulses

decelTime = 0.00005    # amount of time to increment between deceleration pulses

 

 

decelPulseCount = 50   # number of pulses sent during deceleration, 1/4 rev for current setup

limitPulseCount = 5  # number of pulses sent to the driver when a limit is tripped, 1/2 rev for current setup

timeVar = startTime

 

def limit():  # this routine is like the ramp down routine

        GPIO.output(enable, 0)

        print("Limit Hit")

        time.sleep(0.015) #wait for a bit

        if(GPIO.input(limitLeft) and GPIO.input(limitRight) == 1): # debounce

                return 

       

        GPIO.output(enable, 1) # enable driver for movement

        global direction

        global timeVar

        global limitFlag

        global flag1

       

        flag1 = 0 # disable the flag so u dont call rampDown upon exiting limit()

        timeVar = endTime  #initialize time variable with ending time bewteen pulses

       

        direction = not direction

        GPIO.output(directionPin, direction)

        limitFlag = 1 # set this flag so that the rampUp routine 'breaks' and jumps back to Main()

        x = limitPulseCount

        while(x > 0):               

                x = x - 1

                GPIO.output(step, 1)

                time.sleep(highPulse)

                GPIO.output(step, 0)      

                time.sleep(abs(timeVar))

                       

                if(timeVar < startTime):

                        timeVar = timeVar + decelTime

 

"""                       

        global direction

        print("Limit Hit")

        time.sleep(0.015)

        if(GPIO.input(limitLeft) and GPIO.input(limitRight) == 1): # debounce

                return

        timeVar = startTime

        flag1 = 0

        direction = not direction

"""              

 

 

"""

MAIN Loop

"""

 

while(1):

        GPIO.output(enable, 1)

        if(GPIO.input(limitLeft)== 0): # if a limit input goes LOW, call the limit function

                        GPIO.output(enable, 0)

                        limit()

        if(GPIO.input(limitRight)== 0): #if a limit input goes LOW, call the limit function

                        GPIO.output(enable, 0)

                        limit()

        if(limitFlag == 1):

                        limitFlag = 0

                        

               

        GPIO.output(directionPin, direction)

        GPIO.output(step, 1)

        time.sleep(highPulse)

        GPIO.output(step, 0)

        time.sleep(abs(timeVar))                               

        if(timeVar > endTime):

                timeVar = timeVar - accelTime

 

       

        if(GPIO.input(speedHiLo) == 0):

                endTime = lospeed       #if lo-speed selected, initialize endTIme with lo-speed wait time

               

        else:

                endTime = hispeed

Disclaimer: The opinions, beliefs, and viewpoints expressed by the various authors and/or forum participants on this website do not necessarily reflect the opinions, beliefs, and viewpoints of Digi-Key Electronics or official policies of Digi-Key Electronics.

About this publisher

Digi-Key's North American Editors