Skip to content

Instantly share code, notes, and snippets.

@wsmoak
Last active April 24, 2019 07:11
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wsmoak/1e9d110b400118ec8642 to your computer and use it in GitHub Desktop.
Save wsmoak/1e9d110b400118ec8642 to your computer and use it in GitHub Desktop.
Turning a stepper motor with Elixir

Turny is an Elixir port of the Adafruit Python Library for DC + Stepper Motor HAT, which is a Python library for interfacing with the Adafruit Motor HAT for Raspberry Pi to control DC motors with speed control and Stepper motors with single, double, interleave and microstepping, designed specifically to work with the Adafruit Motor Hat.

To reconstruct the Elixir project, execute mix new turny and then copy mix.exs and lib/turny.ex into place.

Run it with:

$ mix run -e Turny.start
(CTRL-c twice to stop it)
$ mix run -e Turny.swrst

Note: Be sure to do the software reset or you will leave one or more of the coils energized and the motor will get very hot.

In program code, use something like the stop function to set all the channels you're using to zero, or examine the Python code for "Release"-ing a motor to see if it does something different. I haven't gotten there yet.

The current code is doing double coil stepping where two coils are energized at once. This is much stronger than single coil stepping, but uses more power. Other options are interleaved, which alternates between single and double, and micro-stepping.

Turny is an Elixir port of the Adafruit Python Library for DC + Stepper Motor HAT, which is a Python library for interfacing with the Adafruit Motor HAT for Raspberry Pi to control DC motors with speed control and Stepper motors with single, double, interleave and microstepping, designed specifically to work with the Adafruit Motor Hat

----> https://www.adafruit.com/product/2348

Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit!

Written by Limor Fried for Adafruit Industries. MIT license, all text above must be included in any redistribution

The MIT License (MIT) Copyright © 2016 Wendy Smoak wsmoak@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

defmodule Turny.Mixfile do
use Mix.Project
def project do
[app: :turny,
version: "0.0.1",
elixir: "~> 1.2-rc",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[
{:elixir_ale, "~> 0.4.1"},
]
end
end
defmodule Turny do
require Logger
use Bitwise
@mode1 0x00 # bit 4 is SLEEP 000X0000
@mode2 0x01
@prescale 0xFE
@all_on_l 0xFA
@all_on_h 0xFB
@all_off_l 0xFC
@all_off_h 0xFD
@led0_on_l 0x06
@led0_on_h 0x07
@led0_off_l 0x08
@led0_off_h 0x09
@allcall 0x01
@outdrv 0x04
@swrst 0x06
# These seem to be some of the 16 channels on the PCA9685
# I think the diagrams in the tutorial show how the PCA9685 is connected to the TB6612's on the board
# https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi?view=all
@pwma 8
@ain2 9
@ain1 10
@pwmb 13
@bin2 12
@bin1 11
def start do
Logger.debug "Starting..."
{:ok,pid}= I2c.start_link("i2c-1", 0x60)
init(pid)
prescale(pid, 1600) # set pwm to 1600 Hz
set_pwm_ab(pid)
turn(pid)
end
# PCA9685 Software Reset (Section 7.6 on pg 28)
def swrst do
Logger.debug "PCA9685 Software Reset..."
{:ok,pid}=I2c.start_link("i2c-1", 0x00)
:timer.sleep 10
I2c.write(pid,<<@swrst>>)
end
def init(pid) do
Logger.debug "Initializing..."
set_all_pwm(pid,0,0)
I2c.write(pid, <<@mode2, @outdrv>>) # external driver, see docs
I2c.write(pid, <<@mode1, @allcall>>) # program all PCA9685's at once
:timer.sleep 5
end
def prescale(pid,freq) do
Logger.debug "Setting prescale to #{freq} Hz"
# pg 14 and solve for prescale or example on pg 25
prescaleval = trunc(Float.round( 25000000.0 / 4096.0 / freq ) - 1 )
Logger.debug "prescale value is #{prescaleval}"
oldmode = I2c.write_read(pid, <<@mode1>>, 1)
:timer.sleep 5
I2c.write(pid, <<@mode1, 0x11>>) # set bit 4 (sleep) to allow setting prescale
I2c.write(pid, <<@prescale, prescaleval>> )
I2c.write(pid, <<@mode1, 0x01>> ) #un-set sleep bit
:timer.sleep 5 # pg 14 it takes 500 us for the oscillator to be ready
I2c.write(pid, <<@mode1>> <> oldmode ) # put back old mode
end
def step_one(pid) do
set_pin(pid,@ain2,1)
set_pin(pid,@bin1,1)
set_pin(pid,@ain1,0)
set_pin(pid,@bin2,0)
end
def step_two(pid) do
set_pin(pid,@ain2,0)
set_pin(pid,@bin1,1)
set_pin(pid,@ain1,1)
set_pin(pid,@bin2,0)
end
def step_three(pid) do
set_pin(pid,@ain2,0)
set_pin(pid,@bin1,0)
set_pin(pid,@ain1,1)
set_pin(pid,@bin2,1)
end
def step_four(pid) do
set_pin(pid,@ain2,1)
set_pin(pid,@bin1,0)
set_pin(pid,@ain1,0)
set_pin(pid,@bin2,1)
end
def stop(pid) do
set_pin(pid,@ain2,0)
set_pin(pid,@bin1,0)
set_pin(pid,@ain1,0)
set_pin(pid,@bin2,0)
end
def turn(pid) do
Logger.debug "turning..."
step_one(pid)
:timer.sleep 10
step_two(pid)
:timer.sleep 10
step_three(pid)
:timer.sleep 10
step_four(pid)
:timer.sleep 10
turn(pid)
end
def set_pin(pid,channel,0) do
set_pwm(pid,channel,0,0x1000)
end
def set_pin(pid,channel,1) do
set_pwm(pid,channel,0x1000,0)
end
# These don't need to change unless you are micro-stepping
def set_pwm_ab(pid) do
set_pwm(pid, @pwma, 0, 0x0FF0)
set_pwm(pid, @pwmb, 0, 0x0FF0)
end
# The registers for each of the 16 channels are sequential
# so the address can be calculated as an offset from the first one
def set_pwm(pid, channel, on, off) do
I2c.write(pid, <<@led0_on_l+4*channel, on &&& 0xFF>>)
I2c.write(pid, <<@led0_on_h+4*channel, on >>> 8>>)
I2c.write(pid, <<@led0_off_l+4*channel, off &&& 0xFF>>)
I2c.write(pid, <<@led0_off_h+4*channel, off >>> 8>>)
end
# The PCA9685 has special registers for setting ALL channels
# (or 1/3 of them) to the same value.
def set_all_pwm(pid, on, off) do
I2c.write(pid, <<@all_on_l, on &&& 0xFF>>)
I2c.write(pid, <<@all_on_h, on >>> 8>>)
I2c.write(pid, <<@all_off_l, off &&& 0xFF>>)
I2c.write(pid, <<@all_off_h, off >>> 8>>)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment