Skip to content

Instantly share code, notes, and snippets.

@kieraneglin
Last active March 30, 2021 15:06
Show Gist options
  • Save kieraneglin/ff8707c5db1aeeb5d28049d6ed29d61b to your computer and use it in GitHub Desktop.
Save kieraneglin/ff8707c5db1aeeb5d28049d6ed29d61b to your computer and use it in GitHub Desktop.
Elixir module for interfacing with DRV8825 via Nerves
_My_ portion of this code is licensed under the very permissive DWYW License (https://github.com/thornjad/DWYW).
If you are unable to recognize DWYW as a valid license, _my_ portion of this code is then licensed under MIT.
TL;DR: I don't care what you do with this, just don't get mad at me if a stepper breaks itself/something else when using this module. Liability = no. When I say "my code", I mean everything that isn't Circuits.GPIO.
The Circuits.GPIO library is licenced as such: https://github.com/elixir-circuits/circuits_gpio/blob/main/LICENSE
defmodule Motor.StepperDriver do
@moduledoc """
This module interfaces directly with the DRV8825, including things
like GPIO initialization, raw stepping, mode changes, etc. This module
does not concern real-world implementation details such as gear ratios
"""
alias __MODULE__
alias Circuits.GPIO
@enforce_keys [:dir, :step, :enable, :mode]
defstruct [:dir, :step, :enable, :mode]
@type t() :: %StepperDriver{}
# Order is important since the 0 or 1 written relates to the label's index in this list
# eg: `:reverse` = index 1 = write 1 (high) to GPIO
@directions [:forward, :reverse]
# This is more for code clarity than anything - I don't expect to be swapping these any time soon
@high 1
@low 0
@doc """
Creates a new `StepperDriver` struct containing the GPIO references of main motor functions
"""
@spec new(reference(), reference(), reference(), [reference()]) :: StepperDriver.t()
def new(dir_gpio, step_gpio, enable_gpio, mode_gpios) do
%StepperDriver{
dir: dir_gpio,
step: step_gpio,
enable: enable_gpio,
mode: mode_gpios
}
end
@doc """
Given a map of pins, opens and sets all specified pins to a default.
Returns a struct containing the GPIO references to each pin.
Expects the input to be in the form of:
%{
dir: integer(),
step: integer(),
enable: integer(),
mode: [integer()]
}
Where each integer is the number of the GPIO pin.
Sets the `enable` pin high by default to turn stepper "off".
With my specific motor hat (Waveshare) this was inverted so low was "off". Something to be aware of.
"""
@spec initialize(map()) :: StepperDriver.t()
def initialize(pins) do
{:ok, dir_gpio} = GPIO.open(pins.dir, :output, initial_value: @low)
{:ok, step_gpio} = GPIO.open(pins.step, :output, initial_value: @low)
{:ok, enable_gpio} = GPIO.open(pins.enable, :output, initial_value: @high)
mode_gpios =
Enum.map(pins.mode, fn mode_pin ->
{:ok, mode_gpio} = GPIO.open(mode_pin, :output, initial_value: @low)
mode_gpio
end)
StepperDriver.new(dir_gpio, step_gpio, enable_gpio, mode_gpios)
end
@doc """
Rotates a given driver (ie: stepper) a given direction and number of steps.
The `step_delay` option is a pause between writing the step pin high and low (measured in ms) and is basically
how fast the motor rotates. 5 is a good baseline, but even 1 appears to be reasonable in my testing.
If steps are < 1 (eg: accidental negative instead of direction change), the motor is disabled
for paranoia's sake but is otherwise effectively a noop.
"""
@spec step(StepperDriver.t(), :forward | :reverse, integer(), keyword()) :: StepperDriver.t()
def step(driver, _direction, steps, opts \\ [])
def step(driver, _direction, steps, _opts) when steps < 1 do
stop(driver)
end
def step(driver, direction, steps, opts) when direction in @directions do
# Cheeky way to set pin value from index - see definition of @directions above
dir_pin_value = Enum.find_index(@directions, &(&1 == direction))
delay = Keyword.get(opts, :step_delay, 5)
:ok = GPIO.write(driver.enable, @low)
:ok = GPIO.write(driver.dir, dir_pin_value)
Enum.each(1..steps, fn _ -> do_step(driver.step, delay) end)
# Stepping complete, shut it down
stop(driver)
end
@doc """
Sets the enable pin low for a given driver, stopping it
"""
@spec stop(StepperDriver.t()) :: StepperDriver.t()
def stop(driver = %StepperDriver{enable: enable}) do
:ok = GPIO.write(enable, @high)
driver
end
@doc """
Sets mode pins to specified values. Doesn't directly handle specific modes,
but according to documentation the microstepping modes are as follows:
Full step: 0, 0, 0
Half step: 1, 0, 0
1/4 step: 0, 1, 0
1/8 step: 1, 1, 0
1/16 step: 0, 0, 1
1/32 step: 1, 0, 1 OR 0, 1, 1 OR 1, 1, 1
"""
@spec set_mode(StepperDriver.t(), [integer()]) :: StepperDriver.t()
def set_mode(driver = %StepperDriver{mode: mode_pins}, mode_values)
when length(mode_pins) == length(mode_values) do
mode_pins
|> Enum.zip(mode_values)
|> Enum.each(fn {mode_pin, mode_value} ->
GPIO.write(mode_pin, mode_value)
end)
driver
end
defp do_step(step_gpio, delay) do
# Notice that the process sleeps after the write to low.
# Not a big deal, just something to be aware of.
:ok = GPIO.write(step_gpio, @high)
Process.sleep(delay)
:ok = GPIO.write(step_gpio, @low)
Process.sleep(delay)
end
end
alias Motor.StepperDriver
motor_pins = %{
dir: 13,
step: 19,
enable: 12,
mode: [16, 17, 20]
}
motor_pins
|> StepperDriver.initialize()
|> StepperDriver.set_mode([1, 0, 0])
|> StepperDriver.step(:forward, 200)
|> StepperDriver.step(:reverse, 400, step_delay: 3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment