Current section

Files

Jump to
circuits_cdev lib chip.ex
Raw

lib/chip.ex

defmodule Circuits.GPIO.Chip do
@moduledoc """
Control GPIOs using the GPIO chip interface
With the character device driver for GPIOs there three concepts to learn.
First, the API is made up of chips and lines that are grouped together for
that chip. A chip is more of a grouping identifier than anything physical
property about the board.
Secondly, the API requires us to request lines from a GPIO chip. The reason
for this is the kernel can provide control over who "owns" that line and
prevent multiple programs from trying to control the same GPIO pin.
Lastly, you can listen for events on a line. These events report if the line
is high or low.
Generally speaking the character device driver allows more fine grain control
and more reliability than the `sysfs` API.
"""
alias Circuits.GPIO.Chip.{Events, LineHandle, LineInfo, Nif}
@type t() :: %__MODULE__{
name: String.t(),
label: String.t(),
number_of_lines: non_neg_integer(),
reference: reference()
}
@typedoc """
The offset of the pin
An offset is the pin number provided. Normally these are labeled `GPIO N` or
`GPIO_N` where `N` is the pin number. For example, if you wanted to use to
use `GPIO 17` on a Raspberry PI the offset value would be `17`.
More resources:
Raspberry PI: https://pinout.xyz/
Beaglebone: https://beagleboard.org/Support/bone101
"""
@type offset() :: non_neg_integer()
@typedoc """
The value of the offset
This is either 0 for low or off, or 1 for high or on.
"""
@type offset_value() :: 0 | 1
@typedoc """
The direction of the line
With the character device you drive a line with configured offsets. These
offsets all share a direction, either `:output` or `:input`, which is called
the line direction.
The `:output` direction means you control the GPIOs by setting the value of
the GPIOs to 1 or 0. See `Circuits.GPIO.Chip.set_value/2` for more
information.
The `:input` direction means you can only read the current value of the GPIOs
on the line. See `Circuits.GPIO.Chip.read_value/1` for more information.
"""
@type line_direction() :: :input | :output
defstruct name: nil, label: nil, number_of_lines: 0, reference: nil
@doc """
Getting information about a line
"""
@spec get_line_info(t(), offset()) :: {:ok, LineInfo.t()} | {:error, atom()}
def get_line_info(%__MODULE__{} = chip, offset) do
case Nif.get_line_info_nif(chip.reference, offset) do
{:ok, name, consumer, direction, active_low} ->
{:ok,
%LineInfo{
offset: offset,
name: to_string(name),
consumer: to_string(consumer),
direction: direction_to_atom(direction),
active_low: active_low_int_to_bool(active_low)
}}
error ->
error
end
end
@doc """
Listen to line events on the line offset
```elixir
Circuits.GPIO.Chip.listen_event(mygpio_chip, 24)
# cause the offset to change value
flush
{:circuits_cdev, 24, timestamp, new_value}
```
The timestamp will be in nanoseconds so as you do time calculations and
conversions be sure to take that into account.
The `new_value` will be the value the offset value changed to either `1` or
`0`.
"""
@spec listen_event(t() | String.t(), offset()) :: :ok
def listen_event(%__MODULE__{} = chip, offset) do
Events.listen_event(chip, offset)
end
def listen_event(chip_name, offset) when is_binary(chip_name) do
case open(chip_name) do
{:ok, chip} -> listen_event(chip, offset)
end
end
@doc """
Open a GPIO Chip
```elixir
{:ok, chip} = Circuits.GPIO.Chip.open(gpiochip_device)
```
"""
@spec open(String.t()) :: {:ok, t()}
def open(chip_name) do
chip_name = Path.join("/dev", chip_name)
{:ok, ref} = Nif.chip_open_nif(to_charlist(chip_name))
{:ok, name, label, number_of_lines} = Nif.get_chip_info_nif(ref)
{:ok,
%__MODULE__{
name: to_string(name),
label: to_string(label),
number_of_lines: number_of_lines,
reference: ref
}}
end
@doc """
Read value from a line handle
This is useful when you have a line handle that contains only one GPIO
offset.
If you want to read multiple GPIOs at once see
`Circuits.GPIO.Chip.read_values/1`.
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_line("gpiochip0", 17)
{:ok, 0} = Circuits.GPIO.Chip.read_value(line_handle)
```
"""
@spec read_value(LineHandle.t()) :: {:ok, offset_value()} | {:error, atom()}
def read_value(line_handle) do
case read_values(line_handle) do
{:ok, [value]} ->
{:ok, value}
error ->
error
end
end
@doc """
Read values for a line handle
This is useful when you a line handle that contains multiple GPIO offsets.
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_lines("gpiochip0", [17, 22, 23, 24])
{:ok, [0, 0, 0, 0]} = Circuits.GPIO.Chip.read_values(line_handle)
```
Note that the values in the list match the index order of how the offsets were
requested.
Note that the order of the values returned return the order that the offsets
were requested.
"""
@spec read_values(LineHandle.t()) :: {:ok, [offset_value()]} | {:error, atom()}
def read_values(line_handle) do
%LineHandle{handle: handle} = line_handle
Nif.read_values_nif(handle)
end
@doc """
Request a line handle for a single GPIO offset
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_line(my_gpio_chip, 17, :output)
```
See `Circuits.GPIO.Chip.request_lines/3` and `Circuits.GPIO.LineHandle` for
more details about line handles.
"""
@spec request_line(t() | String.t(), offset(), line_direction()) :: {:ok, LineHandle.t()}
def request_line(%__MODULE__{} = chip, offset, direction) do
request_lines(chip, [offset], direction)
end
def request_line(chip_name, offset, direction) when is_binary(chip_name) do
case open(chip_name) do
{:ok, chip} ->
request_lines(chip, [offset], direction)
end
end
@doc """
Request a line handle for multiple GPIO offsets
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_lines(my_gpio_chip, [17, 24], :output)
```
For the GPIO character device driver you drive GPIOs by requesting for a line
handle what contains one or more GPIO offsets. The line handle is mechanism
by which you can read and set the values of the GPIO(s). The line handle is
attached to the calling process and kernel will not allow others to control
the GPIO(s) that are part of that the line handle. Moreover, one the process
that requested the line handle goes away the kernel will be able to
automatically free the system resources that were tied to that line handle.
"""
@spec request_lines(t() | String.t(), [offset()], line_direction()) :: {:ok, LineHandle.t()}
def request_lines(%__MODULE__{} = chip, offsets, direction) do
{:ok, handle} = Nif.request_lines_nif(chip.reference, offsets, direction_from_atom(direction))
{:ok, %LineHandle{chip: chip, handle: handle}}
end
def request_lines(chip_name, offsets, direction) when is_binary(chip_name) do
case open(chip_name) do
{:ok, chip} ->
request_lines(chip, offsets, direction)
end
end
@doc """
Set the value of the GPIO
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_lines(my_gpio_chip, 17)
{:ok, 0} = Circuits.GPIO.Chip.read_value(line_handle)
:ok = Circuits.GPIO.Chip.set_value(line_handle, 1)
{:ok, 1} = Circuits.GPIO.Chip.read_value(line_handle)
```
"""
@spec set_value(LineHandle.t(), offset_value()) :: :ok | {:error, atom()}
def set_value(handle, value) do
set_values(handle, [value])
end
@doc """
Set values of the GPIOs
```elixir
{:ok, line_handle} = Circuits.GPIO.Chip.request_lines(my_gpio_chip, [17, 24, 22])
{:ok, [0, 0, 0]} = Circuits.GPIO.Chip.read_value(line_handle)
:ok = Circuits.GPIO.Chip.set_value(line_handle, [1, 0, 1])
{:ok, [1, 0, 1]} = Circuits.GPIO.Chip.read_value(line_handle)
```
Note that the order of the values that were sent matches the order by which
the GPIO offsets where requested. In the example above offset 17 was set to
1, offset 24 was stayed at 0, offset 22 was set to 1.
"""
@spec set_values(LineHandle.t(), [offset_value()]) :: :ok | {:error, atom()}
def set_values(line_handle, values) do
%LineHandle{handle: handle} = line_handle
Nif.set_values_nif(handle, values)
end
defp direction_from_atom(:input), do: 0
defp direction_from_atom(:output), do: 1
defp direction_to_atom(0), do: :input
defp direction_to_atom(1), do: :output
defp active_low_int_to_bool(0), do: false
defp active_low_int_to_bool(1), do: true
end