[Elixir/Nerves] Potentiometer with SPI-based Analog to Digital Converter
Tags: elixir, nerves, iot, spi
Published: 2021-01-08 04:42:30

I will talk about how to read values from a potentiometer using an SPI-based Analog to Digital Converter (ADC).
This is my study note. I am hoping it will help somebody.

## Goals

- Read values from a potentiometer using an SPI-based Analog to Digital Converter (ADC).
- Print the readings to the log.
- Use the programming language [Elixir](https://elixir-lang.org) and the IoT platform [Nerves project](https://www.nerves-project.org) to achieve the above.

![spi-potentiometer](https://user-images.githubusercontent.com/7563926/104024920-dd4f4680-5191-11eb-8e36-d991aacbe1fb.jpg)

![spi-potentiometer-readings](https://user-images.githubusercontent.com/7563926/104030802-ef34e780-5199-11eb-99d0-4f9afcb8b0d7.gif)

## Resources

I found the following resources helpful:

- [Potentiometer sensor | Microsoft](https://docs.microsoft.com/en-us/samples/microsoft/windows-iotcore-samples/potentiometer-sensor/)
  - [MCP3002 wiring & connections](https://docs.microsoft.com/en-us/samples/microsoft/windows-iotcore-samples/potentiometer-sensor/#mcp3002)
- [Circuits I2C library documentation](https://github.com/elixir-circuits/circuits_spi#getting-started)
  - tips on reading the MCP3002 datasheet
  - how to use SPI from Elixir code

## Hardware

- [Potentiometer](https://www.google.com/search?q=potentiometer+electronics&tbm=isch)
- [MCP3002: 10-bit Analog-to-Digital Converter (ADC)](https://www.microchip.com/wwwproducts/en/MCP3002)
- [Raspberry Pi Zero WH](https://www.raspberrypi.org/blog/zero-wh/)

## Software

- [Nerves project](https://hexdocs.pm/nerves/getting-started.html) - IoT platform and infrastructure
- [elixir-circuits/circuits_spi](https://hexdocs.pm/circuits_spi/Circuits.SPI.html) - Communicate over SPI from Elixir

## Analog to Digital Converter (ADC)

There are different types of ADC out there. The higher the resolution, more accurate (10-bit vs 12-bit).
The more channels, the more different inputs you can read (2-channel vs 8-channel).

I chose MCP3002 (10-bit resolution 2-channel) because I need neither high resolution or many channels.

**Some options**

- [MCP3002: 10-bit 2-channel](https://www.microchip.com/wwwproducts/en/MCP3002)
- [MCP3008: 10-bit 8-channel](https://www.microchip.com/wwwproducts/en/MCP3008)
- [MCP3208: 12-bit 8-channel](https://www.microchip.com/wwwproducts/en/MCP3208)

## Wiring and connections

| MCP3002  | Raspberry Pi                             | Potentiometer      |
| -------- | ---------------------------------------- | ------------------ |
| VDD/VREF | 3.3V                                     | Vcc (either side)  |
| CLK      | SPI0 SCLK (Serial Clock)                 | -                  |
| Dout     | SPI0 CIPO (Controllor In Peripheral Out) | -                  |
| Din      | SPI0 COPI (Controllor Out Peripheral In) | -                  |
| CS/SHDN  | SPI0 CS (Chip Select)                    | -                  |
| Vss      | GND                                      | GND (either side)  |
| CH0      | -                                        | wiper pin (middle) |

[![MCP3002-pins](https://user-images.githubusercontent.com/7563926/103961375-5ca53180-5122-11eb-9a6e-a55b6dee2213.png)](https://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf)

[![SPI Pinout](https://user-images.githubusercontent.com/7563926/103969349-b95d1800-5133-11eb-939b-2f1daa61f8e8.png)](https://pinout.xyz/pinout/spi)

[![](https://docs.microsoft.com/en-us/samples/microsoft/windows-iotcore-samples/potentiometer-sensor/media/overallcon-3002.png)](https://docs.microsoft.com/en-us/samples/microsoft/windows-iotcore-samples/potentiometer-sensor/#mcp3002)

## How to communicate with [MCP3002 A/D Converter](https://www.microchip.com/wwwproducts/en/MCP3002)

It is explained in Figure 6-1 of the [MCP3002 datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf).

[![MCP3002 SPI communication](https://user-images.githubusercontent.com/7563926/103962390-da6a3c80-5124-11eb-8b37-45dd36ca6c82.png)](https://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf)

### Connecting to the peripheral

```elixir
# Raspberry Pis have two SPI buses.
iex> Circuits.SPI.bus_names
["spidev0.0", "spidev0.1"]

# Open the connection to one of them.
iex> {:ok, ref} = Circuits.SPI.open("spidev0.0")
{:ok, #Reference<0.3977234826.537788429.135085>}
```

### Config bits

We specify which channel we want to read data from, configuring the config bits in Table 5-1 of the [MCP3002 datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf).

[![MCP3002-config-bits](https://user-images.githubusercontent.com/7563926/103961378-5e6ef500-5122-11eb-940f-3afd4710d624.png)](https://ww1.microchip.com/downloads/en/DeviceDoc/21294E.pdf)

For using Ch0, the data we need to transmit is two bytes like the following, where `x` is ignored.

|     | Start | SGL/DIFF | ODD/SIGN | MSBF |     |     |     |     |     |     |     |     |     |     |     |
| --- | ----- | -------- | -------- | ---- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| x   | 1     | 1        | 0        | 1    | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   | x   |

| bit      | value | description                |
| -------- | ----- | -------------------------- |
| Start    | 1     | always 1                   |
| SGL/DIFF | 1     | single-ended-mode Ch0      |
| ODD/SIGN | 0     | single-ended-mode Ch0      |
| MSBF     | 1     | most-significant bit first |


### Transmitted data

In Elixir, the above two bytes can be expressed as follows:

```elixir
# First byte as an integer
iex> 0b01101000
104

iex> 0x68
104

# Second byte as an integer
iex> 0b00000000
0

iex> 0x00
0

# Together as a binary
iex> ch0 = <<0x68, 0x00>>
<<104, 0>>
```

Then we send it to the peripheral (MCP3002).

```elixir
iex> {:ok, <<_::size(6), value::size(10)>>} = Circuits.SPI.transfer(ref, ch0)
{:ok, <<1, 197>>}
```

### Received data

Since the resolution of [MCP3002](https://www.microchip.com/wwwproducts/en/MCP3002) is 10-bit (`0..1023`), we only read low 10 bits and ignore the rest.

```elixir
# Min value of the potentiometer when the potetiometer is at one limit.
iex> with {:ok, <<_::6, value::10>>} <- Circuits.SPI.transfer(ref, ch0), do: value
1

# Max value of the potentiometer when the potetiometer is at the other limit.
iex> with {:ok, <<_::6, value::10>>} <- Circuits.SPI.transfer(ref, ch0), do: value
1023
```

## Mapping value to a different range

Once we are able to read values from the potentiometer, we will most likely need to map the value to a different range, such as percentage.

```elixir
defmodule MyModule do
  @doc """
  ## Examples
      iex> MyModule.map_range(65, {0, 1023}, {0, 100})
      6.35386119257087
  """
  def map_range(x, {in_min, in_max}, {out_min, out_max}) do
    (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
  end
end
```

## Demos

### The logging demo

Once started, this demo will keep on logging the potentiometer value every second.

Only dependency is the [elixir-circuits/circuits_spi](https://github.com/elixir-circuits/circuits_spi) library. As long as it is installed, you can just SSH into your target device and copy and paste the following snippet to play with the demo.

Or you could use [my example Elixir/Nerves app](https://github.com/mnishiguchi/nerves_hello_potentiometer) as a playground.

```elixir
defmodule SpiPotentiometer do
  @moduledoc """
  ## Examples

      RingLogger.attach
      (
        "spidev0.0"
        |> SpiPotentiometer.open_potentiometer()
        |> SpiPotentiometer.read_potentiometner_forever(1000)
      )
  """

  require Logger

  def open_potentiometer(spi_device) do
    {:ok, ref} = Circuits.SPI.open(spi_device)
    ref
  end

  def read_potentiometner_forever(spi_ref, interval \\ 1000) do
    {:ok, <<_::size(6), ten_bit_value::size(10)>>} = Circuits.SPI.transfer(spi_ref, <<0x68, 0x00>>)
    Logger.info("#{ten_bit_value} (#{map_range(ten_bit_value, {0, 1023}, {0, 100})}%)")
    Process.sleep(interval)
    read_potentiometner_forever(spi_ref, interval)
  end

  defp map_range(x, {in_min, in_max}, {out_min, out_max}) do
    (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
  end
end
```

![spi-potentiometer-readings](https://user-images.githubusercontent.com/7563926/104030802-ef34e780-5199-11eb-99d0-4f9afcb8b0d7.gif)

### The servo demo

We could apply this technique to many other things like LEDs and servo motors. I used
- [SG90 Servo](https://www.google.com/search?q=sg90+servo&tbm=isch)
- [Adafruit 16-Channel PWM / Servo HAT for Raspberry Pi](https://www.adafruit.com/product/2327)


![potentiometer-servo](https://user-images.githubusercontent.com/7563926/104080569-d7d81780-51f6-11eb-9c93-43361156c167.gif)

Here is my [source code](https://github.com/mnishiguchi/nerves_hello_potentiometer/blob/f370ee401a14ea2f6c36cf5f4c2eb81400ccfe81/lib/nerves_hello_potentiometer.ex).

## Finally

This might be nothing special for electrical engineers but to me an Elixir/Nerves hobbist it was quite challenging. This post is actually for myself so that I can do the same thing quickly next time. That would be great if this post helps someone.