Circuits.SPI

Hex version API docs CircleCI REUSE status

Circuits.SPI lets you communicate with hardware devices using the SPI protocol.

This is Circuits.SPI v2. Circuits.SPI v1.x is still maintained in the maint-v1.x branch.

Circuits.SPI v2.0 is an almost backward-compatible update to Circuits.SPI v1.x. Here's what's new:

If you've used Circuits.SPI v1.x, nearly all of your code will be the same. If you're a library author, we'd appreciate it if you could try this out and update your :circuits_spi dependency to allow v2.0. Details can be found in our porting guide.

Getting started on Nerves and Linux

If you're using Nerves or compiling on a Raspberry Pi or other device with SPI support, then add circuits_spi like any other Elixir library:

def deps do
  [{:circuits_spi, "~> 2.0"}]
end

Circuits.SPI doesn't load device drivers, so you'll need to load any necessary ones beforehand. On the Raspberry Pi, the Adafruit Raspberry Pi SPI instructions may be helpful. (SPI is already enabled for you if you are using Nerves.)

A Serial Peripheral Interface (SPI) bus is a common multi-wire bus used to connect components on a circuit board. A clock line drives the timing of sending bits between components. Bits on the Controller Out Peripheral In, COPI, line go from the controller (usually the processor running this library) to the peripheral, and bits on the Controller In Peripheral Out, CIPO, line go the other direction. If you see references to MOSI or MISO, those are the former terms for COPI and CIPO.

Bits transfer on the SPI bus in both directions simultaneously on each transaction. However, most of the time, programs and devices only care about bits traveling in one direction at a time. For example, when the program makes a request, the bits sent to the device are important, but the ones received get ignored. Then, when the device sends the response, the program only pays attention to the received bits. Anything it sends will be ignored by the device, and programs frequently just send zeros. This will become clearer in the example below.

The final important concept with SPI is the Chip Select, CS, line. This is a common but optional wire to the device that's used by the program to tell the device that it's talking to it. This is useful when multiple SPI devices are connected to the same wires. Each device has a CS wire going to it and the controller asserts the line for the device it wants to communicate with. Chip select is typically active-low: the controller drives it low to select the device and high to deselect it. Check the device's data sheet for its requirements. While the CS wire can be any GPIO, most processors can automatically toggle it when making SPI transactions.

When using Circuits.SPI on Linux and Nerves, Circuits.SPI.open/2 uses the Linux SPI device naming convention, which includes the SPI bus number and chip select. For example, the name "spidev1.0" refers to SPI bus 1 and CS0. Transactions automatically assert CS0 and deassert it when finished. Refer to your board or processor for SPI bus and CS numbering.

The Linux backend automatically splits messages larger than Circuits.SPI.max_transfer_size(spi) into multiple transfers. Chip select is deasserted between these transfers, and there may be a short pause. If your device requires chip select to stay asserted throughout a command, keep that command within the single-transfer limit.

The following shows an example analog-to-digital converter (ADC) that reads from either a temperature sensor on CH0 (channel 0) or a potentiometer on CH1 (channel 1). It converts the analog measurements to digital values and sends the digital measurements to SPI pins on the main processor running Linux (e.g. Raspberry Pi). Many processors, like the one on the Raspberry Pi, can't read analog signals directly, so they need an ADC to convert the signal.

SPI schematic

The protocol for talking to the ADC in the example below is described in the MCP3002 data sheet. The protocol is very similar to an application programming interface (API) for software. It will tell you the position and function of the bits you will send to the ADC, along with how the data (in the form of bits) will be returned.

See Figure 6-1 in the data sheet for the communication protocol. Sending a 0x68 first reads the temperature, and sending a 0x78 reads the potentiometer. Since the data sheet shows bits, 0x68 corresponds to 01101000b. The leftmost bit is a leading zero, which the ADC ignores. The second bit is the "Start" bit, the third bit is SGL/DIFF, the fourth bit is ODD/SIGN, and the fifth bit is MSBF. From Table 5-1, if SGL/DIFF==1, ODD/SIGN==0, and MSBF==1, then that specifies channel 0, which is connected to the temperature sensor.

# Make sure that you've enabled or loaded the SPI driver or this will
# fail.
iex> {:ok, spi} = Circuits.SPI.open("spidev0.0")
{:ok, %Circuits.SPI.SPIDev{ref: #Reference<...>}}

# Read the potentiometer

# Use binary pattern matching to pull out the ADC counts (low 10 bits)
iex> {:ok, <<_::size(6), counts::size(10)>>} = Circuits.SPI.transfer(spi, <<0x78, 0x00>>)
{:ok, <<1, 197>>}

iex> counts
453

# Convert counts to volts (1023 = 3.3 V)
iex> volts = counts / 1023 * 3.3
1.461290322580645

iex> Circuits.SPI.close(spi)
:ok

As shown above, you'll find out that Elixir's binary pattern matching is extremely convenient when working with hardware. More information can be found in the Kernel.SpecialForms documentation and by running h <<>> at the IEx prompt.

FAQ

How do I only receive data?

SPI always sends a bit for every bit it receives. That means that to receive a byte, you have to send a byte. Luckily, devices are designed with this in mind and discard or ignore bytes in these situations. For example, if you have a sensor and need to read 9 bytes of data, send 9 zeros to read it. The zeros will be ignored and you'll get the data.

How do I debug?

The most common issue is communicating with an SPI device for the first time. First, check that an SPI bus is available:

iex> Circuits.SPI.bus_names()
["spidev0.0", "spidev0.1"]

If the list is empty, then an SPI bus is either not available, not enabled, or not configured in the kernel. If you're using Raspbian, run raspi-config and check that SPI is enabled in the advanced options. If you're on a BeagleBone, try running config-pin and see the Universal I/O project to enable the SPI pins. On other ARM boards, double-check that SPI is enabled in the kernel and that the device tree configures it.

How do I set the speed of the SPI bus?

SPI bus options like frequency (:speed_hz) and bits per word (:bits_per_word) are set as optional parameters to Circuits.SPI.open/2.

For example, the following configures the SPI bus to run at 122,000 Hz:

{:ok, my_spi} = Circuits.SPI.open("spidev0.0", speed_hz: 122000)

The ability to set the bus speed is device-specific. Please verify with a logic analyzer that the speed is actually being set and consult the documentation for limitations.

Where can I get help?

Many issues are unrelated to Circuits.SPI. If you expand your searches to include Python and C forums, it's possible that someone else has run into your problem too. SPI libraries in other languages should be similar to Circuits.SPI, so hopefully you'll find the answer.

If that fails, try posting a question to the Elixir Forum. Tag the question with Nerves and it will have a good chance of getting to the right people. Feel free to do this even if you're not using Nerves.

Can I develop code that uses Circuits.SPI on my laptop?

Yes. You can use simulated devices with CircuitsSim or create a custom backend to mock interactions with the Circuits.SPI API.

How do I configure a backend?

Set :default_backend in your project's config/config.exs. For example, to explicitly select the Linux backend:

import Config

config :circuits_spi, default_backend: Circuits.SPI.SPIDev

You can also supply a {backend_module, default_options} tuple. When you call bus_names/0, these defaults are passed to the backend's bus_names/1 callback. They are also merged with the options passed to open/2, with options passed to open/2 taking precedence:

config :circuits_spi,
  default_backend: {Circuits.SPI.SPIDev, speed_hz: 500_000}

Configure the backend before compiling dependencies. Selecting an alternative backend this way disables compilation of the Linux NIF. To implement your own backend, implement the Circuits.SPI.Backend behaviour and the Circuits.SPI.Bus protocol for the bus value returned by open/2.

How do I use CircuitsSim?

Add CircuitsSim to your project's dependencies in mix.exs:

def deps do
  [
    {:circuits_spi, "~> 2.0"},
    {:circuits_sim, "~> 0.1"}
  ]
end

Then select its SPI backend and configure a simulated device in config/config.exs:

import Config

config :circuits_spi, default_backend: CircuitsSim.SPI.Backend

config :circuits_sim,
  config: [
    {CircuitsSim.Device.TM1620, bus_name: "spidev0.0", render: :binary_clock}
  ]

Run mix deps.get and start your project with iex -S mix. The simulated TM1620 LED driver is now available through the usual SPI API:

iex> Circuits.SPI.bus_names()
["spidev0.0"]
iex> {:ok, spi} = Circuits.SPI.open("spidev0.0", lsb_first: true)
iex> Circuits.SPI.transfer(spi, <<0x02>>)
{:ok, <<0>>}
iex> Circuits.SPI.close(spi)
:ok

The TM1620 is write-only, so its simulated response contains zeros. It does not simulate the ADC in the earlier example. See the CircuitsSim documentation for supported devices and their configuration options.

We hope to have support for USB adapters that have SPI interfaces in the future.

License

This project follows the REUSE recommendations.

All original source code in this project is licensed under Apache-2.0. Exceptions to Apache-2.0 licensing are: