The Elixir Telescope
Part 8 : Controlling motors from Elixir
Entries up to this point :
- Part 7 : Elixir architecture questions
- Part 6 : Three.js + websockets = a 3D moving telescope
- Part 5 : Porting the depth of field simulation to Elixir + Rust
- Part 4 : Simulating image capture and focusing
- Part 3 : Raytracing a parabolic mirror from scratch
- Part 2 : Primary mirror design & calculation
- Part 1 : Simulating movement & state with Elixir
Controlling motors from Elixir
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.
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.
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.