Skip to content

Commit

Permalink
Add Livebook example (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnishiguchi authored Jan 18, 2024
1 parent 209f586 commit cf0285d
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ The following examples were tested on a Raspberry Pi that was connected to an
[Erlang Embedded Demo Board](http://solderpad.com/omerk/erlhwdemo/). There's
nothing special about either the demo board or the Raspberry Pi.

## Nerves + Livebook

[Nerves Livebook project](https://github.com/nerves-livebook/nerves_livebook)
allows you to try out the Nerves project on real hardware without needing
to build a project from scratch.

Within minutes, you'll have a Raspberry Pi or Beaglebone running Nerves. You'll
be able to run code in [Livebook](https://livebook.dev/) and work through
Nerves tutorials from the comfort of your browser.

[![Run in Nerves Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Felixir-circuits%2Fcircuits_gpio%2Fblob%2Fmain%2Fnotebooks%2Fbasics.livemd)

## GPIO

A [General Purpose
Expand Down
285 changes: 285 additions & 0 deletions notebooks/basics.livemd
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# Hello Circuits GPIO v2

```elixir
Mix.install([
{:circuits_gpio, "~> 2.0.1"},
{:kino, "~> 0.12.2"}
])
```

## Introduction

`Circuits.GPIO` lets you use GPIOs in Elixir. In this exercise, we will
familiarize ourselves with `Circuits.GPIO` v2.0. For details, see
[documentation](https://hexdocs.pm/circuits_gpio).

## Supported systems

While `Circuits.GPIO` can support non-Nerves and non-Linux systems, the examples
below were made using Nerves. Operation on other devices mostly differs on how
to refer to GPIOs.

This Livebook only works on Raspberry Pi (1, 2, 3, 4, 400 and Zero families)
without modification.

The following examples were tested on a Raspberry Pi that was connected to an
[Erlang Embedded Demo Board](http://solderpad.com/omerk/erlhwdemo/). There's
nothing special about either the demo board or the Raspberry Pi.

## GPIO

A [General Purpose
Input/Output](https://en.wikipedia.org/wiki/General-purpose_input/output) (GPIO)
is just a wire that you can use as an input or an output. It can only be one of
two values, 0 or 1. A 1 corresponds to a logic high voltage like 3.3 V and a 0
corresponds to 0 V. The actual voltage depends on the hardware.

## GPIO Specs

`Circuits.GPIO` v2.0 supports a new form of specifying how to open a GPIO called
a `t:gpio_spec/0`. These specs are very flexible and allow for GPIOs to be
opened by number, a string label, or a tuple that includes both the GPIO
controller hardware name and a line offset.

The contents of a `gpio_spec` depend on the backend. When running on Nerves or
a Linux machine, `Circuits.GPIO` uses the Linux gpio-cdev backend. This backend
prefers the use of GPIO controller/line offset tuples and labels. For backwards
compatibility, it somewhat supports use of the _older_ pin numbering scheme.

See [Enumeration](#enumeration) for listing out all available `gpio_specs` for
your device.

## Enumeration

`Circuits.GPIO` v2.0 supports a new function, `enumerate/0`, which lists every
known GPIO pin.

For Nerves and Linux users, the `gpio-cdev` subsystem maintains the official
list. See the [Official DeviceTree documentation for
GPIOs](https://elixir.bootlin.com/linux/v6.6.6/source/Documentation/devicetree/bindings/gpio/gpio.txt)
for more information on how to configure the fields of this struct for your own
system.

Here's an example:

```elixir
Circuits.GPIO.enumerate() |> Kino.DataTable.new()
```

<!-- livebook:{"output":true} -->

```text
[%{label: "ID_SDA", location: {"gpiochip0", 0}, controller: "pinctrl-bcm2835"}, {label: "ID_SCL", location: {"gpiochip0", 1}, controller: "pinctrl-bcm2835"}, {label: "SDA1", location: {"gpiochip0", 2}, controller: "pinctrl-bcm2835"}, {label: "SCL1", location: {"gpiochip0", 3}, controller: "pinctrl-bcm2835"}, { label: "GPIO_GCLK", location: {"gpiochip0", 4}, controller: "pinctrl-bcm2835" }, {label: "GPIO5", location: {"gpiochip0", 5}, controller: "pinctrl-bcm2835"}, {label: "GPIO6", location: {"gpiochip0", 6}, controller: "pinctrl-bcm2835"}, { label: "SPI_CE1_N", location: {"gpiochip0", 7}, controller: "pinctrl-bcm2835" }, { label: "SPI_CE0_N", location: {"gpiochip0", 8}, controller: "pinctrl-bcm2835" }, { label: "SPI_MISO", location: {"gpiochip0", 9}, controller: "pinctrl-bcm2835" }, { label: "SPI_MOSI", location: {"gpiochip0", 10}, controller: "pinctrl-bcm2835" }, { label: "SPI_SCLK", location: {"gpiochip0", 11}, controller: "pinctrl-bcm2835" }, {label: "GPIO12", location: {"gpiochip0", 12}, controller: "pinctrl-bcm2835"}, {label: "GPIO13", location: {"gpiochip0", 13}, controller: "pinctrl-bcm2835"}, {label: "TXD1", location: {"gpiochip0", 14}, controller: "pinctrl-bcm2835"}, {label: "RXD1", location: {"gpiochip0", 15}, controller: "pinctrl-bcm2835"}, {label: "GPIO16", location: {"gpiochip0", 16}, controller: "pinctrl-bcm2835"}, {label: "GPIO17", location: {"gpiochip0", 17}, controller: "pinctrl-bcm2835"}, {label: "GPIO18", location: {"gpiochip0", 18}, controller: "pinctrl-bcm2835"}, {label: "GPIO19", location: {"gpiochip0", 19}, controller: "pinctrl-bcm2835"}, {label: "GPIO20", location: {"gpiochip0", 20}, controller: "pinctrl-bcm2835"}, {label: "GPIO21", location: {"gpiochip0", 21}, controller: "pinctrl-bcm2835"}, {label: "GPIO22", location: {"gpiochip0", 22}, controller: "pinctrl-bcm2835"}, {label: "GPIO23", location: {"gpiochip0", 23}, controller: "pinctrl-bcm2835"}, {label: "GPIO24", location: {"gpiochip0", 24}, controller: "pinctrl-bcm2835"}, {label: "GPIO25", location: {"gpiochip0", 25}, controller: "pinctrl-bcm2835"}, {label: "GPIO26", location: {"gpiochip0", 26}, controller: "pinctrl-bcm2835"}, {label: "GPIO27", location: {"gpiochip0", 27}, controller: "pinctrl-bcm2835"}, {label: "SDA0", location: {"gpiochip0", 28}, controller: "pinctrl-bcm2835"}, {label: "SCL0", location: {"gpiochip0", 29}, controller: "pinctrl-bcm2835"}, {label: "CTS0", location: {"gpiochip0", 30}, controller: "pinctrl-bcm2835"}, {label: "RTS0", location: {"gpiochip0", 31}, controller: "pinctrl-bcm2835"}, {label: "TXD0", location: {"gpiochip0", 32}, controller: "pinctrl-bcm2835"}, {label: "RXD0", location: {"gpiochip0", 33}, controller: "pinctrl-bcm2835"}, { label: "SD1_CLK", location: {"gpiochip0", 34}, controller: "pinctrl-bcm2835" }, { label: "SD1_CMD", location: {"gpiochip0", 35}, controller: "pinctrl-bcm2835" }, { label: "SD1_DATA0", location: {"gpiochip0", 36}, controller: "pinctrl-bcm2835" }, { label: "SD1_DATA1", location: {"gpiochip0", 37}, controller: "pinctrl-bcm2835" }, { label: "SD1_DATA2", location: {"gpiochip0", 38}, controller: "pinctrl-bcm2835" }, { label: "SD1_DATA3", location: {"gpiochip0", 39}, controller: "pinctrl-bcm2835" }, { label: "CAM_GPIO1", location: {"gpiochip0", 40}, controller: "pinctrl-bcm2835" }, {label: "WL_ON", location: {"gpiochip0", 41}, controller: "pinctrl-bcm2835"}, {label: "NC", location: {"gpiochip0", 42}, controller: "pinctrl-bcm2835"}, { label: "WIFI_CLK", location: {"gpiochip0", 43}, controller: "pinctrl-bcm2835" }, { label: "CAM_GPIO0", location: {"gpiochip0", 44}, controller: "pinctrl-bcm2835" }, {label: "BT_ON", location: {"gpiochip0", 45}, controller: "pinctrl-bcm2835"}, { label: "HDMI_HPD_N", location: {"gpiochip0", 46}, controller: "pinctrl-bcm2835" }, { label: "STATUS_LED_N", location: {"gpiochip0", 47}, controller: "pinctrl-bcm2835" }, { label: "SD_CLK_R", location: {"gpiochip0", 48}, controller: "pinctrl-bcm2835" }, { label: "SD_CMD_R", location: {"gpiochip0", 49}, controller: "pinctrl-bcm2835" }, { label: "SD_DATA0_R", location: {"gpiochip0", 50}, controller: "pinctrl-bcm2835" }, { label: "SD_DATA1_R", location: {"gpiochip0", 51}, controller: "pinctrl-bcm2835" }, { label: "SD_DATA2_R", location: {"gpiochip0", 52}, controller: "pinctrl-bcm2835" }, { label: "SD_DATA3_R", location: {"gpiochip0", 53}, controller: "pinctrl-bcm2835"}]
```

The `:location` can always be passed as the first parameter to
`Circuits.GPIO.open/3`. You may find the `:label` field more descriptive to use,
though.

The GPIO controller part of the `:location` tuple is usually some variation on
`"gpiochip0"` that depends on what controllers are available under `/dev`. The
line offset is a the line offset of the GPIO on that controller.

```elixir
Path.wildcard("/dev/gpiochip*")
```

<!-- livebook:{"output":true} -->

```text
["/dev/gpiochip0"]
```

If you're deploying to multiple types of devices and you can set labels in the
device tree, labels make it really easy for code using `Circuits.GPIO` to just
use the right GPIO.

Labels are not guaranteed to be unique, so if your device defines one twice,
`Circuits.GPIO` will use the first GPIO it finds that has the specified label.

```elixir
# label only
{:ok, gpio} = Circuits.GPIO.open("GPIO18", :input)
Circuits.GPIO.close(gpio)

# location (controller_name and line_offset)
{:ok, gpio} = Circuits.GPIO.open({"gpiochip0", 18}, :input)
Circuits.GPIO.close(gpio)

# controller_name and label
{:ok, gpio} = Circuits.GPIO.open({"gpiochip0", "GPIO18"}, :input)
Circuits.GPIO.close(gpio)

# index only (the older pin numbering scheme)
{:ok, gpio} = Circuits.GPIO.open(18, :input)
Circuits.GPIO.close(gpio)
```

<!-- livebook:{"output":true} -->

```
:ok
```

When the Linux device tree is configured with GPIO labels, you can use those instead:

```elixir
# {:ok, gpio} = Circuits.GPIO.open("special-name-for-pin-1")
```

<!-- livebook:{"output":true} -->

```
nil
```

## Turning an LED on or off

Here's an example of turning an LED on or off:

![GPIO LED schematic](https://github.com/elixir-circuits/circuits_gpio/raw/v2.0.1/assets/images/schematic-gpio-led.png)

To turn on the LED that's connected to the net (or wire) labeled `GPIO18`, you
need to open it first. The first parameter to `Circuits.GPIO.open/2` is called a
GPIO spec and identifies the GPIO. The Raspberry Pis are nice and provide string
names for GPIOs. Other boards are not as nice so you always have to check. The
string name for this GPIO is `"GPIO18"` (use `"PIN12"` on a Raspberry Pi 5).

```elixir
{:ok, gpio} = Circuits.GPIO.open("GPIO12", :output)

Circuits.GPIO.write(gpio, 1)

Circuits.GPIO.close(gpio)
```

<!-- livebook:{"output":true} -->

```
:ok
```

The call to `Circuits.GPIO.close/1` is not necessary, since the garbage
collector will free up unreferenced GPIOs. It's a good practice, though,
since backends can enforce exclusivity and prevent future opens from
working until the GC occurs.

Input works similarly. Here's an example of a button with a pull down resistor
connected.

![GPIO Button schematic](https://github.com/elixir-circuits/circuits_gpio/raw/v2.0.1/assets/images/schematic-gpio-button.png)

If you're not familiar with pull up or pull down resistors, they're resistors
whose purpose is to drive a wire high or low when the button isn't pressed. In
this case, it drives the wire low. Many processors have ways of configuring
internal resistors to accomplish the same effect without needing to add an
external resistor. If you're using a Raspberry Pi, you can use [the built-in
pull-up/pull-down resistors](#internal-pull-uppull-down).

The code looks like this in `Circuits.GPIO`:

```elixir
{:ok, gpio} = Circuits.GPIO.open("GPIO17", :input)

Circuits.GPIO.read(gpio)
```

<!-- livebook:{"output":true} -->

```
0
```
Push the button down.

```elixir
Circuits.GPIO.read(gpio)
```

<!-- livebook:{"output":true} -->

```
1
```

If you'd like to get a message when the button is pressed or released, call the
`set_interrupts` function. You can trigger on the `:rising` edge, `:falling`
edge or `:both`.

```elixir
Circuits.GPIO.set_interrupts(gpio, :both)

IEx.Helpers.flush()
```

<!-- livebook:{"output":true} -->

```
:ok
```

Note that after calling `set_interrupts`, the calling process will receive an
initial message with the state of the pin. This prevents the race condition
between getting the initial state of the pin and turning on interrupts. Without
it, you could get the state of the pin, it could change states, and then you
could start waiting on it for interrupts. If that happened, you would be out of
sync.

### Internal pull-up/pull-down

To connect or disconnect an internal [pull-up or pull-down resistor](https://github.com/raspberrypilearning/physical-computing-guide/blob/master/pull_up_down.md) to a GPIO
pin, call the `set_pull_mode` function.

```elixir
Circuits.GPIO.set_pull_mode(gpio, :pullup)
```

<!-- livebook:{"output":true} -->

```
:ok
```

Valid `pull_mode` values are `:none` `:pullup`, or `:pulldown`

Note that `set_pull_mode` is platform dependent, and currently only works for
Raspberry Pi hardware. Calls to `set_pull_mode` on other platforms will have no
effect. The internal pull-up resistor value is between 50K and 65K, and the
pull-down is between 50K and 60K. It is not possible to read back the current
Pull-up/down settings, and GPIO pull-up pull-down resistor connections are
maintained, even when the CPU is powered down.

## Convenience functions

Having to `open` _then_ `read` and `write` can be a cumbersome for one-off GPIO
access in code and when working at the IEx prompt. Circuits v2.0 has a pair of
new functions to help:


```elixir
Circuits.GPIO.write_one("special-name-for-pin-1", 1)
```

<!-- livebook:{"output":true} -->

```
:ok
```

```elixir
Circuits.GPIO.read_one("special-name-for-pin-2")
```

<!-- livebook:{"output":true} -->

```
1
```

These functions get passed a `t:gpio_spec/0` just like `open/3` and internally
open the GPIO and read or write it. Importantly, they `close` the GPIO when done
to avoid reserving the GPIO any longer than necessary.

Please note that this is not a performant way of reading or writing the same
GPIO more than once. Opening a GPIO takes much longer than reading or writing an
already opened one, so if these are used in tight loops, the open overhead will
dominate (>99% of the time taken in a trivial benchmark.)

0 comments on commit cf0285d

Please sign in to comment.