Packages
circuits_gpio
2.3.0
2.3.0
2.2.0
2.1.3
2.1.2
retired
2.1.1
retired
2.1.0
retired
2.0.2
retired
2.0.1
retired
2.0.0
retired
2.0.0-pre.6
retired
2.0.0-pre.5
retired
2.0.0-pre.4
retired
2.0.0-pre.3
retired
2.0.0-pre.2
retired
2.0.0-pre.1
retired
2.0.0-pre.0
retired
1.2.2
1.2.1
1.2.0
1.1.0
1.0.1
1.0.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.0
Use GPIOs in Elixir
Current section
Files
Jump to
Current section
Files
lib/gpio.ex
# SPDX-FileCopyrightText: 2014 Frank Hunleth
# SPDX-FileCopyrightText: 2016 Justin Schneck
# SPDX-FileCopyrightText: 2017 Giovanni Visciano
# SPDX-FileCopyrightText: 2017 Tim Mecklem
# SPDX-FileCopyrightText: 2018 Jon Carstens
# SPDX-FileCopyrightText: 2018 Mark Sebald
# SPDX-FileCopyrightText: 2018 Matt Ludwigs
# SPDX-FileCopyrightText: 2019 Michael Roach
# SPDX-FileCopyrightText: 2023 Connor Rigby
#
# SPDX-License-Identifier: Apache-2.0
defmodule Circuits.GPIO do
@moduledoc """
Control GPIOs from Elixir
See the [Readme](README.md) for a tutorial and the [porting guide](PORTING.md)
if updating from Circuits.GPIO v1.x.
Simple example:
```elixir
# GPIO 2 is connected to GPIO 3
iex> {:ok, my_output_gpio} = Circuits.GPIO.open({"gpiochip0", 2}, :output)
iex> {:ok, my_input_gpio} = Circuits.GPIO.open({"gpiochip0", 3}, :input)
iex> Circuits.GPIO.write(my_output_gpio, 1)
:ok
iex> Circuits.GPIO.read(my_input_gpio)
1
iex> Circuits.GPIO.close(my_output_gpio)
iex> Circuits.GPIO.close(my_input_gpio)
```
"""
alias Circuits.GPIO.Handle
require Logger
@typedoc """
Backends specify an implementation of a Circuits.GPIO.Backend behaviour
The second parameter of the Backend 2-tuple is a list of options. These are
passed to the behaviour function call implementations.
"""
@type backend() :: {module(), keyword()}
@typedoc """
GPIO controller
GPIO controllers manage one or more GPIO lines. They're referred to by
strings. For example, controllers are named `"gpiochip0"`, etc. for the
Linux cdev backend. Other backends may have similar conventions or use
the empty string if there's only one controller.
"""
@type controller() :: String.t()
@typedoc """
GPIO line offset on a controller
GPIOs are numbered based on how they're connected to a controller. The
details are controller specific, but usually the first one is `0`, then `1`,
etc.
"""
@type line_offset() :: non_neg_integer()
@typedoc """
A GPIO controller label or GPIO label
Labels provide aliases for GPIO lines and controllers. They're
system-specific. On Linux, labels are provided in device tree files.
"""
@type label() :: String.t()
@typedoc """
An identifier for a GPIO
Call `Circuits.GPIO.enumerate/0` to see what GPIOs are available on your
device. Several ways exist to refer to GPIOs due to variations in devices and
programmer preference. Most Raspberry Pi models have labels like `"GPIO26"`.
The Raspberry Pi 5 has labels based on physical location (e.g., `"PIN37"` for
GPIO 26.)
Options:
1. `index` - Many examples exist where GPIOs are referred to by a GPIO
number. There are issues with this strategy since GPIO indices can change.
It is so common that it's still supported. Prefer other ways when you're
able to change code.
2. `{controller_name, line_offset}` - Specify a line on a specific GPIO
controller. E.g., `{"gpiochip0", 10}`
3. `label` - Specify a GPIO label. The first controller that has a
matching GPIO is used. This lets you move the mapping of GPIOs to
peripheral connections to a device tree file or other central place. E.g.,
`"LED_ENABLE"`
4. `{controller_name, label}` - Specify both GPIO controller and GPIO labels.
E.g., `{"gpiochip4", "PIO4"}`
"""
@type gpio_spec() ::
non_neg_integer() | {controller(), line_offset()} | label() | {controller(), label()}
@typedoc "The GPIO direction (input or output)"
@type direction() :: :input | :output
@typedoc """
Value of one or more GPIOs
In the common single-GPIO case, the value is either 0 (low) or 1 (high). When
opening groups of GPIOs, the constituent GPIOs are ordered starting from least
significant bit (bit 0).
"""
@type value() :: non_neg_integer()
@typedoc "Trigger edge for pin change notifications"
@type trigger() :: :rising | :falling | :both | :none
@typedoc "Pull mode for platforms that support controllable pullups and pulldowns"
@type pull_mode() :: :not_set | :none | :pullup | :pulldown
@typedoc "Drive mode for platforms that support push-pull, open-drain, and open-source"
@type drive_mode() :: :push_pull | :open_drain | :open_source
@typedoc """
Ways of referring to a GPIO
It's possible to refer to a GPIOs in many ways and this map contains
information for doing that. See `enumerate/1` and `identifiers/1` for
querying `Circuits.GPIO` for these maps.
The information in this map is backend specific. At a minimum, all backends
provide the `:location` field which is an unambiguous `t:gpio_spec/0` for use
with `open/3`.
When provided, the `:label` field is a string name for the GPIO that should
be unique to the system but this isn't guaranteed. A common convention is to
label GPIOs by their pin names in documentation or net names in schematics.
The Linux cdev backend uses labels from the device tree file.
Fields:
* `:location` - this is the canonical gpio_spec for a GPIO.
* `:label` - an optional label for the GPIO that may indicate what the GPIO is connected to
or be more helpful that the `:location`. It may be passed to `GPIO.open/3`.
* `:controller` - the name or an alias for the GPIO controller. Empty string if unused
"""
@type identifiers() :: %{
location: {controller(), non_neg_integer()},
controller: controller(),
label: label()
}
@typedoc """
Dynamic GPIO configuration and status
Fields:
* `:consumer` - if this GPIO is in use, this optional string gives a hint as to who is
using it.
* `:direction` - whether this GPIO is an input or output
* `:pull_mode` - if this GPIO is an input, then this is the pull mode
* `:drive_mode` - how the GPIO is driven (push-pull, open-drain, or open-source)
"""
@type status() :: %{
consumer: String.t(),
direction: direction(),
pull_mode: pull_mode(),
drive_mode: drive_mode()
}
@typedoc """
Options for `open/3`
* `:initial_value` - the initial value of an output GPIO
* `:pull_mode` - the initial pull mode for an input GPIO
* `:drive_mode` - the drive mode of an output GPIO
* `:on_busy` - behavior when an open fails because the GPIO is already open;
`:error` returns the error (default), `:take_over` closes other references and retries.
* `:force_enumeration` - Linux cdev-specific option to force a scan of
available GPIOs rather than using the cache. This is only for test purposes
since the GPIO cache should refresh as needed.
"""
@type open_options() :: [
initial_value: value(),
pull_mode: pull_mode(),
drive_mode: drive_mode(),
on_busy: :take_over | :error,
force_enumeration: boolean()
]
@typedoc """
Options for `set_interrupt/2`
"""
@type interrupt_options() :: [suppress_glitches: boolean(), receiver: pid() | atom()]
@typedoc """
Options for `subscribe/2`
* `:receiver` - process that should receive notifications. Defaults to the
calling process (`self()`).
* `:tag` - a term echoed in the `:ref` field of every notification instead of
the auto-generated reference. Use this to route messages with a
domain-specific label.
* `:trigger` - send notifications on the `:rising` edges, `:falling` edges, or
`:both`. Defaults to `:both`.
"""
@type subscribe_options() :: [trigger: trigger(), receiver: pid() | atom(), tag: term()]
@doc """
Guard version of `gpio_spec?/1`
Add `require Circuits.GPIO` to your source file to use this guard.
"""
defguard is_gpio_spec(x)
when (is_tuple(x) and is_binary(elem(x, 0)) and
(is_binary(elem(x, 1)) or is_integer(elem(x, 1)))) or is_binary(x) or
is_integer(x)
@doc """
Return if a term looks like a `gpio_spec`
This function only verifies that the term has the right shape to be a
`t:gpio_spec/0`. Whether or not it refers to a usable GPIO is checked by
`Circuits.GPIO.open/3`.
"""
@spec gpio_spec?(any) :: boolean()
def gpio_spec?(x), do: is_gpio_spec(x)
@doc """
Return identifying information about a GPIO
See `t:gpio_spec/0` for the ways of referring to GPIOs. If the GPIO is found,
this function returns information about the GPIO.
"""
@spec identifiers(gpio_spec()) :: {:ok, identifiers()} | {:error, atom()}
def identifiers(gpio_spec) do
{backend, backend_defaults} = default_backend()
backend.identifiers(gpio_spec, backend_defaults)
end
@doc """
Return dynamic configuration and status information about a GPIO or handle
Pass a `t:gpio_spec/0` to query a GPIO without opening it, or a `t:Handle.t/0`
to query an open GPIO. If the GPIO is found, this function returns information
about the GPIO. Status for GPIO groups is not supported.
"""
@spec status(gpio_spec() | Handle.t()) :: {:ok, status()} | {:error, atom()}
def status(handle) when is_struct(handle), do: Handle.status(handle)
def status(gpio_spec) do
{backend, backend_defaults} = default_backend()
backend.status(gpio_spec, backend_defaults)
end
@doc """
Open one or more GPIOs
See `t:gpio_spec/0` for the ways of referring to GPIOs. Set `direction` to
either `:input` or `:output`. If opening as an output, then be sure to set
the `:initial_value` option to minimize the time the GPIO is in the default
state.
## Groups
Passing a list of GPIO specs opens them together as a group. `read/1` then
returns a single integer and `write/2` takes one. The first GPIO in the list
is the least significant bit (bit 0), the second is bit 1, and so on:
```elixir
iex> {:ok, bus} = Circuits.GPIO.open([{"gpiochip0", 2}, {"gpiochip0", 4}], :output)
iex> Circuits.GPIO.write(bus, 0b10) # GPIO 2 -> 0, GPIO 4 -> 1
:ok
iex> Circuits.GPIO.close(bus)
:ok
```
All GPIOs in a group must be on the same controller. `:initial_value` is
interpreted as an integer with the same one-bit-per-line layout, and
`:pull_mode`/`:drive_mode` apply to every line in the group.
## Exclusivity
Each GPIO may only have one open handle at a time. This is enforced by the
backend, so while one backend may support it, it's not guaranteed. Library
authors should assume exclusivity on GPIOs to be cross platform. In
practice, this isn't a big deal except when debugging.
Use the `:on_busy` option to `:take_over` a GPIO from another open handle
when it's busy. This is useful for `GenServer`s when they restart to avoid
waiting for the BEAMs GC to clean up the old handle. It's also handy when
experimenting at the IEx prompt when you lose a reference. The `:on_busy`
option only works for handles known to the running BEAM instance.
## Troubleshooting
The most common issue is figuring out the names or labels on GPIOs. See
`enumerate/0` for available GPIOs. On Linux, labels are defined in the device
tree.
If you suspect a hardware or driver issue, see `Circuits.GPIO.Diagnostics`.
If you're getting `{:error, :already_open}`, try passing `on_busy: :take_over`
to try taking over the GPIO from a lost or outdated reference.
Options:
* `:initial_value` - Set to `0` or `1` (or an integer with one bit per line for
a group). Only used for outputs. Defaults to `0`.
* `:pull_mode` - Set to `:not_set`, `:pullup`, `:pulldown`, or `:none` for an
input pin. `:not_set` is the default.
* `:drive_mode` - Set to `:push_pull`, `:open_drain`, or `:open_source`.
`:push_pull` is the default. Only used for outputs.
* `:on_busy` - Set to `:take_over` or `:error`. `:take_over` will try
closing other GPIO users to allow this call to succeed. Defaults to
`:error`.
Returns `{:ok, handle}` on success.
"""
@spec open(
gpio_spec() | identifiers() | [gpio_spec() | identifiers()],
direction(),
open_options()
) ::
{:ok, Handle.t()} | {:error, atom()}
def open(gpio_spec_or_line_info, direction, options \\ [])
def open(%{location: gpio_spec}, direction, options) do
open(gpio_spec, direction, options)
end
def open(gpio_specs, direction, options) when is_list(gpio_specs) do
specs = Enum.map(gpio_specs, &normalize_spec/1)
check_gpio_specs!(specs)
check_direction!(direction)
check_options!(options)
do_open(specs, direction, options)
end
def open(gpio_spec, direction, options) do
check_gpio_spec!(gpio_spec)
check_direction!(direction)
check_options!(options)
do_open(gpio_spec, direction, options)
end
defp normalize_spec(%{location: gpio_spec}), do: gpio_spec
defp normalize_spec(gpio_spec), do: gpio_spec
defp do_open(gpio_spec, direction, options) do
{backend, backend_defaults} = default_backend()
all_options =
backend_defaults
|> Keyword.merge(options)
|> Keyword.put_new(:initial_value, 0)
|> Keyword.put_new(:pull_mode, :not_set)
|> Keyword.put_new(:drive_mode, :push_pull)
|> Keyword.put_new(:on_busy, :error)
case backend.open(gpio_spec, direction, all_options) do
{:error, :already_open} ->
take_over_and_retry(backend, gpio_spec, direction, all_options)
result ->
result
end
end
defp take_over_and_retry(backend, gpio_spec, direction, options) do
if Keyword.get(options, :on_busy) == :take_over do
with :ok <- force_close_specs(backend, List.wrap(gpio_spec), options) do
backend.open(gpio_spec, direction, options)
end
else
{:error, :already_open}
end
end
defp check_gpio_spec!(gpio_spec) do
if not gpio_spec?(gpio_spec) do
raise ArgumentError, "Invalid GPIO spec: #{inspect(gpio_spec)}"
end
end
defp check_gpio_specs!(specs) do
cond do
specs == [] ->
raise ArgumentError, "Expected a non-empty list of GPIO specs"
length(specs) > 64 ->
raise ArgumentError, "A GPIO group is limited to 64 lines"
true ->
Enum.each(specs, &check_gpio_spec!/1)
end
end
defp check_direction!(direction) do
if direction not in [:input, :output] do
raise ArgumentError,
"Invalid direction: #{inspect(direction)}. Options are :input or :output"
end
end
defp check_options!([]), do: :ok
defp check_options!([{:initial_value, value} | rest]) do
case value do
:not_set ->
Logger.warning("Circuits.GPIO no longer supports :not_set for :initial_value")
v when is_integer(v) and v >= 0 ->
:ok
_ ->
raise ArgumentError,
":initial_value should be 0 or 1 (or an integer with one bit per line for a group)"
end
check_options!(rest)
end
defp check_options!([{:pull_mode, value} | rest]) do
if value not in [:not_set, :pullup, :pulldown, :none],
do: raise(ArgumentError, ":pull_mode should be :not_set, :pullup, :pulldown, or :none")
check_options!(rest)
end
defp check_options!([{:drive_mode, value} | rest]) do
if value not in [:push_pull, :open_drain, :open_source],
do: raise(ArgumentError, ":drive_mode should be :push_pull, :open_drain, or :open_source")
check_options!(rest)
end
defp check_options!([{:on_busy, value} | rest]) do
if value not in [:take_over, :error],
do: raise(ArgumentError, ":on_busy should be :take_over or :error")
check_options!(rest)
end
defp check_options!([_unknown_option | rest]) do
# Ignore unknown options - the backend might use them
check_options!(rest)
end
@doc """
Release the resources associated with a GPIO
This is optional. The garbage collector will free GPIO resources that aren't
in use, but this will free them sooner.
"""
@spec close(Handle.t()) :: :ok
defdelegate close(handle), to: Handle
@doc """
Close GPIO handles by gpio spec or identifier
This is useful when a process has lost its handles or when GPIO hardware is
being reconfigured. After this call, any use of the old file handles will
raise an exception.
See the `:on_busy` option in the `t:open_options/0` docs for automatically
taking over GPIOs when calling `open/3`.
"""
@spec force_close(gpio_spec() | identifiers() | [gpio_spec() | identifiers()]) ::
:ok | {:error, atom()}
def force_close(gpio_specs) do
specs = gpio_specs |> List.wrap() |> Enum.map(&normalize_spec/1)
Enum.each(specs, &check_gpio_spec!/1)
{backend, backend_options} = default_backend()
force_close_specs(backend, specs, backend_options)
end
defp force_close_specs(backend, specs, options) do
Enum.reduce_while(specs, :ok, fn spec, :ok ->
case backend.force_close(spec, options) do
:ok -> {:cont, :ok}
error -> {:halt, error}
end
end)
end
@doc """
Read a GPIO's value
For a group opened with a list of GPIO specs, this returns an integer with the
first GPIO as bit 0, the second as bit 1, and so on. See `t:value/0`.
The value returned for GPIO's that are configured as outputs is undefined.
Backends may choose not to support this.
"""
@spec read(Handle.t()) :: value()
defdelegate read(handle), to: Handle
@doc """
One line GPIO read
This is a convenience function that opens, reads, and closes a GPIO. It's
intended to simplify one-off reads in code and for IEx prompt use.
Prefer using handles in other situations.
"""
@spec read_one(gpio_spec(), open_options()) :: value() | {:error, atom()}
def read_one(gpio_spec, options \\ []) do
with {:ok, handle} <- open(gpio_spec, :input, options),
value <- read(handle) do
:ok = close(handle)
value
end
end
@doc """
Set the value of a GPIO
The GPIO must be configured as an output.
For a group opened with a list of GPIO specs, pass an integer with the first
GPIO as bit 0, the second as bit 1, and so on. See `t:value/0`.
"""
@spec write(Handle.t(), value()) :: :ok
defdelegate write(handle, value), to: Handle
@doc """
One line GPIO write
This is a convenience function that opens, writes, and closes a GPIO. It's
intended to simplify one-off writes in code and for IEx prompt use.
Prefer using handles in other situations.
"""
@spec write_one(gpio_spec(), value(), open_options()) :: :ok | {:error, atom()}
def write_one(gpio_spec, value, options \\ []) do
with {:ok, handle} <- open(gpio_spec, :output, options),
:ok <- write(handle, value) do
:ok = close(handle)
end
end
@doc """
Enable or disable GPIO value change notifications
New code should prefer `subscribe/2`, which delivers a map (with the value and
previous value), works with GPIO groups, and returns a reference for matching
messages. `set_interrupts/3` remains for backwards compatibility and sends the
tuple described below. It cannot be used with a group handle.
Notifications are sent based on the trigger:
* `:none` - No notifications are sent
* `:rising` - Send a notification when the pin changes from 0 to 1
* `:falling` - Send a notification when the pin changes from 1 to 0
* `:both` - Send a notification on all changes
Available Options:
* `:suppress_glitches` - Not supported in Circuits.GPIO v2
* `:receiver` - Process which should receive the notifications.
Defaults to the calling process (`self()`)
Notification messages look like:
```
{:circuits_gpio, gpio_spec, timestamp, value}
```
Where `gpio_spec` is the `t:gpio_spec/0` passed to `open/3`, `timestamp` is
an OS monotonic timestamp in nanoseconds, and `value` is the new value.
Timestamps are not necessarily the same as from `System.monotonic_time/0`.
For example, with the cdev backend, they come the Linux kernel. It's also
possible for them to come from a monotonic hardware timer. Both may be
different from Erlang's monotonic time. The takeaway is that these timestamps
can be compared with each other, but be careful when comparing them to
anything else.
NOTE: You will need to store the `Circuits.GPIO` reference somewhere (like
your `GenServer`'s state) so that it doesn't get garbage collected. Event
messages stop when it gets collected. If you only get one message and you are
expecting more, this is likely the case.
"""
@spec set_interrupts(Handle.t(), trigger(), interrupt_options()) :: :ok | {:error, atom()}
defdelegate set_interrupts(handle, trigger, options \\ []), to: Handle
@doc """
Subscribe to GPIO value change notifications
This is the preferred way to receive change notifications and the only way to
receive them for a group (a handle opened with a list of GPIO specs). It
returns `{:ok, ref}` where `ref` is a reference echoed in every notification
so you can match messages to this subscription.
Available options:
* `:receiver` - process that should receive notifications. Defaults to the
calling process (`self()`).
* `:tag` - a term echoed in the `:ref` field instead of the auto-generated
reference.
* `:trigger` - send notifications on the `:rising`, `:falling`, or `:both`
edges. Defaults to `:both`.
Notification messages are maps:
```
{:circuits_gpio, %{ref: ref, timestamp: timestamp, value: value, previous_value: previous_value}}
```
Where `ref` is the reference returned by this function (or your `:tag`),
`timestamp` is an OS monotonic timestamp in nanoseconds, `value` is the new
value (an integer with one bit per line for a group), and `previous_value` is
the previously reported value. `Bitwise.bxor(value, previous_value)` has
the changed bits. It's possible to receive reports with no changes due to
transients.
Timestamps are not necessarily the same as from `System.monotonic_time/0`.
For example, with the cdev backend, they're applied by the Linux kernel or
can be come from a hardware timer. Erlang's monotonic time is adjusted so
it's not the same as OS monotonic time. The result is that these timestamps
can be compared with each other, but not with anything else.
NOTE: You will need to keep the handle from `open/3` (for example, in your
`GenServer`'s state) so that it isn't garbage collected. Notifications stop
when it is collected.
"""
@spec subscribe(Handle.t(), subscribe_options()) :: {:ok, term()} | {:error, atom()}
defdelegate subscribe(handle, options \\ []), to: Handle
@doc """
Stop GPIO value change notifications started with `subscribe/2`
"""
@spec unsubscribe(Handle.t()) :: :ok | {:error, atom()}
defdelegate unsubscribe(handle), to: Handle
@doc """
Change the direction of the pin
"""
@spec set_direction(Handle.t(), direction()) :: :ok | {:error, atom()}
defdelegate set_direction(handle, direction), to: Handle
@doc """
Enable or disable an internal pull-up or pull-down resistor
"""
@spec set_pull_mode(Handle.t(), pull_mode()) :: :ok | {:error, atom()}
defdelegate set_pull_mode(gpio, pull_mode), to: Handle
@doc """
Set the drive mode (push-pull, open-drain, or open-source)
"""
@spec set_drive_mode(Handle.t(), drive_mode()) :: :ok | {:error, atom()}
defdelegate set_drive_mode(gpio, drive_mode), to: Handle
@doc """
Return info about the low level GPIO interface
This may be helpful when debugging issues.
"""
@spec backend_info(backend() | nil) :: map()
def backend_info(backend \\ nil)
def backend_info(nil), do: backend_info(default_backend())
def backend_info({backend, _options}), do: backend.backend_info()
@doc """
Return a list of accessible GPIOs
Each GPIO is described in a `t:identifiers/0` map. Some fields in the map like
`:location` and `:label` may be passed to `open/3` to use the GPIO. The map
itself can also be passed to `open/3` and the function will figure out how to
access the GPIO.
"""
@spec enumerate(backend() | nil) :: [identifiers()]
def enumerate(backend \\ nil)
def enumerate(nil), do: enumerate(default_backend())
def enumerate({backend, options}), do: backend.enumerate(options)
defp default_backend() do
case Application.get_env(:circuits_gpio, :default_backend) do
nil -> {Circuits.GPIO.NilBackend, []}
m when is_atom(m) -> {m, []}
{m, o} = value when is_atom(m) and is_list(o) -> value
end
end
end