Controlling servo in Elixir/Nerves
Tags: elixir, nerves, servo, raspberrypi
Published: 2021-03-21 02:15:59

I really wanted to control a servo motor using my favorite programming language Elixir. Here is what I learned when I did experiments controlling a [SG90](http://www.towerpro.com.tw/product/sg90-7/)-type digital servo motor from my [Nerves](https://www.nerves-project.org/)-powered [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/) using [PCA9685 PWM/Servo controller](https://www.adafruit.com/product/815).

[にほんご](https://qiita.com/mnishiguchi/items/5b3aadb26850a36d58f2)

## Servo motors

I am not gonna talk about what a servo motor is since I am not an expert and also there are tons of good resources on the Internet, such as Sparkfun's [Hobby Servo Tutorial](https://learn.sparkfun.com/tutorials/hobby-servo-tutorial/all) and [Basic Servo Control for Beginners](https://learn.sparkfun.com/tutorials/basic-servo-control-for-beginners/servo-motor-basics).

Long story short, there are two types of servo motors available for hobby use:

### Standard servo

e.g. SG90

![standard_servo](https://user-images.githubusercontent.com/7563926/111398863-89ce0d00-869a-11eb-9073-678ff1dba90f.gif)

### Continuous-rotation servo

e.g. FS90R

![continuous_servo](https://user-images.githubusercontent.com/7563926/111398873-8c306700-869a-11eb-8c73-e4c9e60639e5.gif)

## Pulse width modulation (PWM)

Pulse width modulation (PWM) is commonly used for controlling a digital servo. We control a servo by changing the ratio of on-time to off-time of the waveform.

[![](https://upload.wikimedia.org/wikipedia/commons/b/b8/Duty_Cycle_Examples.png)](https://en.wikipedia.org/wiki/Pulse-width_modulation)

PWM can be used for [smoothly dimming LEDs](https://dev.to/mnishiguchi/elixir-nerves-smoothly-dimming-leds-using-a-servo-driver-5gid) like this.

![servo-driver-pwm-leds](https://user-images.githubusercontent.com/7563926/105923166-f36e5b00-6009-11eb-9a0a-18e4e7c0c839.gif)

## PWM for servo motors

As far as I know, most hobby servos accept the 50Hz signal whose period is 20ms. What confused me was after that.
Many sources say:

- 1.0ms pulse width for full clockwise
- 1.5ms pulse width for neutral position
- 2.0ms pulse width for full counterclockwise

but my SG90 moves far less than 180-degree range with that setting. It is kind of mysterious how many degrees the servo rotates for what pulse width. It seems like this guy had the same question: [What is the proper calculation of duty cycle range for the sg90 servo? | https://raspberrypi.stackexchange.com](https://raspberrypi.stackexchange.com/questions/106858/what-is-the-proper-calculation-of-duty-cycle-range-for-the-sg90-servo)

I am not sure whether those tutorials are wrong or different servos have different specifications. Anyway, I cannot find any solid information on detailed specifications of SG90-type servo. Please let me know if you have any information!

Here I put together my researches and observations.

| pulse width | duty cycle | memo                                             |
| ----------- | ---------- | ------------------------------------------------ |
| 400µs       | 2%         | my SG90's lower limit                            |
| 500µs       | 2.5%       | looks close to 0 degree to me                    |
| 1500µs      | 7.5%       | typically a neutral position (90 degrees)        |
| 2500µs      | 12.5%      | looks close to 180 degrees to me; maybe like 170 |
| 2800µs      | 14%        | my SG90's upper limit                            |

Some documents say SG90 rated pulse ranges is 500..2400µs, but my SG90 can rotate between 400..2800µs.

## Servo tester

Probably experts would use some machines to see the actual waveforms, but I do not want to dig that deep. I tried this inexpensive (~10 USD) servo tester and it was super useful in my understanding the characteristics of my SG90. It generates a signal changing the pulse width between 800..2200µs.

## PWM controller

As a hardware noob, I asked Elixir/Nerves community questions about how to control a servo from Nerves-powered Pi. A few people including [Frank Hunleth](https://twitter.com/fhunleth) co-author of [Nerves project](https://www.nerves-project.org/) kindly gave me valuable advice. They recommend using an "I2C->PWM" board rather than using the Raspberry Pi's builtin PWM. Frank added that Linux PWM support is poor based on his past experience.

So I decided to use [PCA9685: 16-channel 12-bit PWM controller](https://www.adafruit.com/product/815), which I control from my Raspberry Pi Zero W that is powered by [Nerves IoT framework](https://www.nerves-project.org/) via [I2C](https://en.wikipedia.org/wiki/I%C2%B2C). Having 16 channels means we can control 16 different servo motors simultaneously, which is cool despite being overkill for my experiments.

## Libraries

I could not found any Elixir libraries for controlling a servo other than [jimsynz/pca9685.ex](https://github.com/jimsynz/pca9685.ex), which was unfortunately outdated and unmaintained.

So I decided to [write one on my own](https://hexdocs.pm/servo_kit/0.2.0/readme.html), reading [PCA9685 data sheet](https://cdn-shop.adafruit.com/datasheets/PCA9685.pdf) and adopting ideas from existing libraries written in various programming languages. My servo kit is working pretty well so far.

For communicating between Raspberry Pi and PCA9685, I use [elixir-circuits/circuits_i2c](https://github.com/elixir-circuits/circuits_i2c) library, which is a reliable and established I2C library in Elixir community.

## Demo

**hardware**

- [SG90](http://www.towerpro.com.tw/product/sg90-7/)
- [Raspberry Pi Zero W](https://www.adafruit.com/product/3708)
- [Adafruit 16-Channel PWM / Servo Bonnet for Raspberry Pi](https://www.adafruit.com/product/3416)
- [AC adapter](https://www.adafruit.com/product/276)

**firmware**

- [Nerves project](https://www.nerves-project.org/)

**software**

- [ServoKit](https://hexdocs.pm/servo_kit/0.2.0/readme.html) - Use PCA9685 PWM/Servo Controller in Elixir

![servo-demo](https://user-images.githubusercontent.com/7563926/111891281-33c5d600-89c8-11eb-8f1f-cc33cae9f442.gif)

Here is a throw-away Elixir script I wrote, which I ran from the Interactive Elixir Shell on my Raspberry Pi.

```elixir
# Start a pwm control process
ServoKit.start_link()

# Define a function that changes duty cycle and delays a little
set_pwm_duty_cycle = fn x, ch ->
  ServoKit.set_pwm_duty_cycle(x, ch: ch)
  Process.sleep(50)
end

# Iterate changing duty cycle with 0.5 step betweem 2.5 and 12.5 for channel 15
list1 = 2.5  |> Stream.iterate(&(&1 + 0.5)) |> Enum.take(21)
list2 = 12.5 |> Stream.iterate(&(&1 - 0.5)) |> Enum.take(21)
0..99 |> Enum.each(fn _ ->
  list1 |> Enum.each(&set_pwm_duty_cycle.(&1, 15))
  list2 |> Enum.each(&set_pwm_duty_cycle.(&1, 15))
end)
```

That's it!