[Elixir Nerves] Potentiometer with SPI-based Analog to Digital Converter

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 and the IoT platform Nerves project to achieve the above.

spi-potentiometer

spi-potentiometer-readings

Resources

I found the following resources helpful:

Hardware

Software

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

Wiring and connections

MCP3002Raspberry PiPotentiometer
VDD/VREF3.3VVcc (either side)
CLKSPI0 SCLK (Serial Clock)-
DoutSPI0 CIPO (Controllor In Peripheral Out)-
DinSPI0 COPI (Controllor Out Peripheral In)-
CS/SHDNSPI0 CS (Chip Select)-
VssGNDGND (either side)
CH0-wiper pin (middle)

MCP3002-pins

SPI Pinout

How to communicate with MCP3002 A/D Converter

It is explained in Figure 6-1 of the MCP3002 datasheet.

MCP3002 SPI communication

Connecting to the peripheral

# 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.

MCP3002-config-bits

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

StartSGL/DIFFODD/SIGNMSBF
x1101xxxxxxxxxxx
bitvaluedescription
Start1always 1
SGL/DIFF1single-ended-mode Ch0
ODD/SIGN0single-ended-mode Ch0
MSBF1most-significant bit first

Transmitted data

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

# 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).

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

Received data

Since the resolution of MCP3002 is 10-bit (0..1023), we only read low 10 bits and ignore the rest.

# 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.

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 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 as a playground.

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

The servo demo

We could apply this technique to many other things like LEDs and servo motors. I used

potentiometer-servo

Here is my source code.

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.