Lucas Sifoni

The Elixir Telescope
Part 8 : Controlling motors from Elixir

elixirhardware


Entries up to this point :

Controlling motors from Elixir

Picture of the MangoPI connected to a 5V stepper motor

I wished to control the three motors of the final build from Elixir. That would allow me to avoid having an additional microcontroller, but I knew it wouldn’t be really suited to drive steppers at a consistent rate. Since I’m working on a terrestrial telescope that just has to move in response to user input, that shouldn’t be a problem. An astronomical telescope that needs precise tracking wouldn’t allow to even try to drive the motors from this soft-realtime environment.

So, how unsuitable is that ? I started by designing a module allowing me to get a process for each motor, and writing to the relevant GPIO pins in order to make it turn. To make a single step, the motor must have its phases activated and deactivated like the below bit pattern : you can graphically see the cyclic nature of the pattern, with activated phases shifting to the right.

defmodule Scope.Motor do
  require Logger

  @moduledoc """
  Module to use the ULN2003 Stepper drivers.
  See https://42bots.com/tutorials/28byj-48-stepper-motor-with-uln2003-driver-and-arduino-uno/.

  Usage :
  iex> {:ok, my_motor} = Scope.Motor.make({1,2,3,4})
  iex> my_motor |> Scope.Motor.turn_cw()
  iex> my_motor |> Scope.Motor.turn_ccw()
  iex> my_motor |> Scope.Motor.stop()
  iex> my_motor |> Scope.Motor.change_speed(23)
  iex> my_motor |> Scope.Motor.change_speed_accel_linear(30, 150_000, 10)
  """
  @cycle_cw [
    {'1', '0', '0', '0'},
    {'1', '1', '0', '0'},
    {'0', '1', '0', '0'},
    {'0', '1', '1', '0'},
    {'0', '0', '1', '0'},
    {'0', '0', '1', '1'},
    {'0', '0', '0', '1'},
    {'1', '0', '0', '1'}
  ]
end

I then needed to calculate the delay for each 8th of a step from a given speed, and given that those motors need (approximately) 4096 steps/turn.

  @doc """
  Converts a value expressed in RPM to the amount of time needed to
  do 1/8th of a step, in µs.
  """
  def rpm_to_ustep_μs(n) do
    rps = n / 60
    sps = rps * @steps_per_turn
    usps = sps * 8
    trunc(1000 / usps * 1000)
  end

Stepping is then cycling through the bit patterns above and writing them to the relevant GPIO file representation in /sysfs.

  @doc """
  Makes a step, clockwise
  """
  def step_cw(f1, f2, f3, f4, delay) do
    ustep(@cycle_cw, f1, f2, f3, f4, delay)
  end

    @doc """
  Recurses through the list of bit masks to apply to the 4-pin
  motor driver input.
  """
  def ustep([{v1, v2, v3, v4} | t], f1, f2, f3, f4, delay) do
    IO.binwrite(f1, v1)
    IO.binwrite(f2, v2)
    IO.binwrite(f3, v3)
    IO.binwrite(f4, v4)
    MicroTimer.usleep(delay)
    ustep(t, f1, f2, f3, f4, delay)
  end
  def ustep([], _, _, _, _, _), do: nil

We recurse through the 8-step pattern for each step. Every 4096 steps, we made a turn. Every 8 8th-of-step, we made a step. As long as the direction doesn’t become :ccw or :stop, we continue.

  def handle_cast(:turn_cw, {f1, f2, f3, f4, {_dir, speed, stepμs}, s}) do
    Logger.info("Starting turning clockwise at #{speed} rpm")
    Process.send_after(self(), :continue, 1)
    {:noreply, {f1, f2, f3, f4, {:cw, speed, stepμs}, s}}
  end

  def handle_info(:continue, {f1, f2, f3, f4, {:cw, speed, stepμs}, s}) do
    ns =
      if s == 0 do
        Logger.info("#{DateTime.utc_now()} Made a whole turn")
        @steps_per_turn
      else
        s - 1
      end

    step_cw(f1, f2, f3, f4, stepμs)
    MicroTimer.send_after(stepμs, :continue, self())
    {:noreply, {f1, f2, f3, f4, {:cw, speed, stepμs}, ns}}
  end

To be able to run this while developing, I have this small utility allowing to open RAM IO devices instead of GPIOs :

  @doc """
  Returns a GPIO pin file descriptor on the MangoPI,
  or a RAM IO device on Mac OS
  """
  def open_pin(pin_nb) when is_integer(pin_nb) do
    case get_platform() do
      :nezha -> File.open!("/sys/class/gpio/gpio#{pin_nb}/value", [:write])
      _ -> File.open!([], [:ram, :write])
    end
  end

On my main computer, all of this code (of which some utilities were not shown there, but the whole code can be seen there on github ) runs perfectly smoothly. I can create motors in IEX, drive them, change their speed, accelerate or deccelerate them, smoothly.

screenshot of a virtual motor being driven in IEX

On the MangoPI, well.. it’s another story. The speed seems to hit a ceiling at around 6rpm, after which the code doesn’t run fast enough to increase the speed as asked by the user. This was quite previsible, but I’m happy to have driven this specific stepper from pure Elixir. That said, I knew it in advance thanks to previous experiments, and the docs of Circuits.GPIO and GPIO Twiddler : benchmark of GPIO switching methods from Elixir, both by fhunleth, so there’s no disappointment.

The motor running at around 6rpm, driven by Elixir on the MangoPI

I will then add an arduino nano, and talk to it over serial, to drive the three motors. Maybe switching the motor drivers to 1-wire stepping drivers would allow reaching high enough speeds to be useful, but I’d like to run the motor at at least 60rpm and mechanically reduce it with a custom gearbox to reach my desired speed of 4rpm for the real physical movements.

This was a very satisfying kind of failure. The process of being able to, once again, simulate a physical component in my terminal, then run it on the hardware, in a single language, is really enjoyable.


Previous post : The Elixir Telescope -- Part 7 : Elixir architecture questions
Next post : [ATM-Buddy] Addition of spray silvering calculator