1 # Python control of stepper motors.
3 # Copyright (C) 2008-2011 W. Trevor King
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 import logging as _logging
19 import logging.handlers as _logging_handlers
20 from time import sleep as _sleep
26 LOG = _logging.getLogger('stepper')
29 LOG.setLevel(_logging.WARN)
30 _formatter = _logging.Formatter(
31 '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
33 _stream_handler = _logging.StreamHandler()
34 _stream_handler.setLevel(_logging.DEBUG)
35 _stream_handler.setFormatter(_formatter)
36 LOG.addHandler(_stream_handler)
39 def _binary(i, width=4):
40 """Convert `i` to a binary string of width `width`.
48 return '0'*(width-len(str)) + str
51 class Stepper (object):
52 """Stepper motor control
56 write (fn(value)) write the 4-bit integer `value` to
57 appropriate digital output channels.
58 full_step (boolean) select full or half stepping
59 logic (boolean) select active high (True) or active low (False)
60 delay (float) time delay between steps in seconds, in case the
61 motor response is slower than the digital output
63 step_size (float) approximate step size in meters
64 backlash (int) generous estimate of backlash in half-steps
66 >>> from pycomedi.device import Device
67 >>> from pycomedi.channel import DigitalChannel
68 >>> from pycomedi.constant import SUBDEVICE_TYPE, IO_DIRECTION
70 >>> device = Device('/dev/comedi0')
73 >>> subdevice = device.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
74 >>> channels = [subdevice.channel(i, factory=DigitalChannel)
75 ... for i in (0, 1, 2, 3)]
76 >>> for chan in channels:
77 ... chan.dio_config(IO_DIRECTION.output)
80 ... subdevice.dio_bitfield(bits=value, write_mask=2**4-1)
82 >>> s = Stepper(write=write)
100 >>> s.single_step(-1)
103 >>> s.full_step = False
104 >>> s.single_step(-1)
107 >>> s.single_step(-1)
110 >>> s.single_step(-1)
128 >>> s.step_relative(1000)
134 def __init__(self, write, full_step=True, logic=True, delay=1e-2,
135 step_size=170e-9, backlash=100):
137 self.full_step = full_step
140 self.step_size = step_size
141 self.backlash = backlash
142 self.port_values = [1, # binary ---1 setup for logic == True
150 self._set_position(0)
152 def _get_output(self, position):
153 """Get the port value that places the stepper in `position`.
155 >>> s = Stepper(write=lambda value: value, logic=True)
156 >>> _binary(s._get_output(0))
158 >>> _binary(s._get_output(1))
160 >>> _binary(s._get_output(2))
162 >>> _binary(s._get_output(-79))
164 >>> _binary(s._get_output(81))
167 >>> _binary(s._get_output(0))
169 >>> _binary(s._get_output(1))
171 >>> _binary(s._get_output(2))
174 value = self.port_values[position % len(self.port_values)]
176 value = 2**4 - 1 - value
179 def _set_position(self, position):
180 self.position = position # current stepper index in half steps
181 output = self._get_output(position)
182 LOG.debug('set postition to %d (%s)' % (position, _binary(output)))
185 def single_step(self, direction):
186 LOG.debug('single step')
187 if self.full_step and self.position % 2 == 1:
188 self.position -= 1 # round down to a full step
194 raise ValueError(direction) # no step
197 self._set_position(self.position + step)
201 def step_to(self, target_position):
202 if target_position != int(target_position):
204 'target_position %s must be an int' % target_position)
205 if self.full_step and target_position % 2 == 1:
206 target_position -= 1 # round down to a full step
207 if target_position > self.position:
211 while self.position != target_position:
212 LOG.debug('stepping %s -> %s (%s)' % (target_position, self.position, direction))
213 self.single_step(direction)
215 def step_relative(self, relative_target_position, backlash_safe=False):
216 """Step relative to the current position.
218 If `backlash_safe` is `True` and `relative_target_position` is
219 negative, step back an additional `.backlash` half-steps and
220 then come back to the target position. This takes the slack
221 out of the drive chain and ensures that you actually do move
222 back to the target location. Note that as the drive chain
223 loosens up after the motion completes, the stepper position
224 will creep forward again.
226 target = self.position + relative_target_position
227 if backlash_safe and relative_target_position < 0:
228 self.step_to(target - self.backlash)