Skip to content

A native Go library for accessing GPIO lines on Linux platforms using the GPIO character device

License

Notifications You must be signed in to change notification settings

warthog618/go-gpiocdev

Repository files navigation

gpiocdev

Build Status PkgGoDev Go Report Card License: MIT

A native Go library for Linux GPIO.

gpiocdev is a library for accessing GPIO pins/lines on Linux platforms using the GPIO character device.

The goal of this library is to provide the Go equivalent of the C libgpiod library. The intent is not to mirror the libgpiod API but to provide the equivalent functionality.

Features

Supports the following functionality per line and for collections of lines:

  • direction (input/output)1
  • write (active/inactive)
  • read (active/inactive)
  • active high/low (defaults to high)
  • output mode (push-pull/open-drain/open-source)
  • pull up/down2
  • watches and edge detection (rising/falling/both)
  • chip and line labels
  • debouncing input lines3
  • different configurations for lines within a collection3

1 Dynamically changing line direction without releasing the line requires Linux 5.5 or later.

2 Requires Linux 5.5 or later.

3 Requires Linux 5.10 or later.

All library functions are safe to call from different goroutines.

Quick Start

A simple piece of wire example that reads the value of an input line (pin 2) and writes its value to an output line (pin 3):

import "github.com/warthog618/go-gpiocdev"

...

in, _ := gpiocdev.RequestLine("gpiochip0", 2, gpiocdev.AsInput)
val, _ := in.Value()
out, _ := gpiocdev.RequestLine("gpiochip0", 3, gpiocdev.AsOutput(val))

...

Error handling and releasing of resources omitted for brevity.

Usage

import "github.com/warthog618/go-gpiocdev"

Error handling is omitted from the following examples for brevity.

Line Requests

To read or alter the value of a line it must first be requested using gpiocdev.RequestLine:

l, _ := gpiocdev.RequestLine("gpiochip0", 4)   // in its existing state

or from the Chip object using Chip.RequestLine:

l, _ := c.RequestLine(4)                    // from a Chip object

The offset parameter identifies the line on the chip, and is specific to the GPIO chip. To improve readability, convenience mappings can be provided for specific devices, such as the Raspberry Pi:

l, _ := c.RequestLine(rpi.J8p7)             // using Raspberry Pi J8 mapping

The initial configuration of the line can be set by providing line configuration options, as shown in this AsOutput example:

l, _ := gpiocdev.RequestLine("gpiochip0", 4, gpiocdev.AsOutput(1))  // as an output line

Multiple lines from the same chip may be requested as a collection of lines using gpiocdev.RequestLines

ll, _ := gpiocdev.RequestLines("gpiochip0", []int{0, 1, 2, 3}, gpiocdev.AsOutput(0, 0, 1, 1))

or from a Chip object using Chip.RequestLines:

ll, _ := c.RequestLines([]int{0, 1, 2, 3}, gpiocdev.AsOutput(0, 0, 1, 1))

When no longer required, the line(s) should be closed to release resources:

l.Close()
ll.Close()

Line Values

Lines must be requsted using RequestLine before their values can be accessed.

Read Input

The current line value can be read with the Value method:

r, _ := l.Value()  // Read state from line (active / inactive)

For collections of lines, the level of all lines is read simultaneously using the Values method:

rr := []int{0, 0, 0, 0} // buffer to read into...
ll.Values(rr)           // Read the state of a collection of lines

Write Output

For lines requested as output, the current line value can be set with the SetValue method:

l.SetValue(1)     // Set line active
l.SetValue(0)     // Set line inactive

Also refer to the toggle_line_value example.

For collections of lines, all lines are set simultaneously using the SetValues method:

ll.SetValues([]int{0, 1, 0, 1}) // Set a collection of lines

Edge Watches

The value of an input line can be watched and trigger calls to handler functions.

The watch can be on rising or falling edges, or both.

The events are passed to a handler function provided using the WithEventHandler(eh) option. The handler function is passed a LineEvent, which contains details of the edge event including the offset of the triggering line, the time the edge was detected and the type of edge detected:

func handler(evt gpiocdev.LineEvent) {
  // handle edge event
}

l, _ = c.RequestLine(rpi.J8p7, gpiocdev.WithEventHandler(handler), gpiocdev.WithBothEdges)

To maintain event ordering, the event handler is called serially from a goroutine that reads the events from the kernel. The event handler is expected to be short lived, and so should hand off any potentially blocking operations to a separate goroutine.

An edge watch can be removed by closing the line:

l.Close()

or by reconfiguring the requested lines to disable edge detection:

l.Reconfigure(gpiocdev.WithoutEdges)

Note that the Close waits for the event handler to return and so must not be called from the event handler context - it should be called from a separate goroutine.

Also see the watch_line_value example.

Line Configuration

Line configuration is set via options to Chip.RequestLine(s) and Line.Reconfigure. These override any default which may be set in NewChip.

Note that configuration options applied to a collection of lines apply to all lines in the collection, unless they are applied to a subset of the requested lines using the WithLines option.

Reconfiguration

Requested lines may be reconfigured using the Reconfigure method:

l.Reconfigure(gpiocdev.AsInput)            // set direction to Input
ll.Reconfigure(gpiocdev.AsOutput(1, 0))    // set direction to Output (and values to active and inactive)

The Line.Reconfigure method accepts differential changes to the configuration for the lines, so option categories not specified or overridden by the specified changes will remain unchanged.

The Line.Reconfigure method requires Linux 5.5 or later.

Complex Configurations

It is sometimes necessary for the configuration of lines within a request to have slightly different configurations. Line options may be applied to a subset of requested lines using the WithLines(offsets, options) option.

The following example requests a set of output lines and sets some of the lines in the request to active low:

ll, _ = c.RequestLines([]int{0, 1, 2, 3}, gpiocdev.AsOutput(0, 0, 1, 1),
    gpiocdev.WithLines([]int{0, 3}, gpiocdev.AsActiveLow),
    gpiocdev.AsOpenDrain)

The configuration of the subset of lines inherits the configuration of the request at the point the WithLines is invoked. Subsequent changes to the request configuration do not alter the configuration of the subset - in the example above, lines 0 and 3 will not be configured as open-drain.

Once a line's configuration has branched from the request configuration it can only be altered with WithLines options:

ll.Reconfigure(gpiocdev.WithLines([]int{0}, gpiocdev.AsActiveHigh))

or reset to the request configuration using the Defaulted option:

ll.Reconfigure(gpiocdev.WithLines([]int{3}, gpiocdev.Defaulted))

Complex configurations require Linux 5.10 or later.

Chip Initialization

The Chip object is used to discover details about avaialble lines and can be used to request lines from a GPIO chip.

A Chip object is constructed using the NewChip function.

c, _ := gpiocdev.NewChip("gpiochip0")

The parameter is the chip name, which corresponds to the name of the device in the /dev directory, so in this example /dev/gpiochip0.

The list of currently available GPIO chips is returned by the Chips function:

cc := gpiocdev.Chips()

Default attributes for Lines requested from the Chip can be set via configuration options to NewChip.

c, _ := gpiocdev.NewChip("gpiochip0", gpiocdev.WithConsumer("myapp"))

In this example the consumer label is defaulted to "myapp".

When no longer required, the chip should be closed to release resources:

c.Close()

Closing a chip does not close or otherwise alter the state of any lines requested from the chip.

Line Info

Info about a line can be read at any time from the chip using the LineInfo method:

inf, _ := c.LineInfo(4)
inf, _ := c.LineInfo(rpi.J8p7) // Using Raspberry Pi J8 mapping

Note that the line info does not include the value. The line must be requested from the chip to access the value.

Once requested, the line info can also be read from the line:

inf, _ := l.Info()
infs, _ := ll.Info()

Info Watches

Changes to the line info can be monitored by adding an info watch for the line:

func infoChangeHandler( evt gpiocdev.LineInfoChangeEvent) {
    // handle change in line info
}

inf, _ := c.WatchLineInfo(4, infoChangeHandler)

Note that the info watch does not monitor the line value (active or inactive) only its configuration. Refer to Edge Watches for monitoring line value.

An info watch can be cancelled by unwatching:

c.UnwatchLineInfo(4)

or by closing the chip.

Categories

Most line configuration options belong to one of the following categories:

  • Active Level
  • Direction
  • Bias
  • Drive
  • Debounce
  • Edge Detection
  • Event Clock

Only one option from each category may be applied. If multiple options from a category are applied then all but the last are ignored.

Active Level

The values used throughout the API for line values are the logical value, which is 0 for inactive and 1 for active. The physical value considered active can be controlled using the AsActiveHigh and AsActiveLow options:

l, _ := c.RequestLine(4, gpiocdev.AsActiveLow) // during request
l.Reconfigure(gpiocdev.AsActiveHigh)           // once requested

Lines are typically active high by default.

Direction

The line direction can be controlled using the AsInput and AsOutput options:

l, _ := c.RequestLine(4, gpiocdev.AsInput) // during request
l.Reconfigure(gpiocdev.AsInput)            // set direction to Input
l.Reconfigure(gpiocdev.AsOutput(0))        // set direction to Output (and value to inactive)
Bias

The bias options control the pull up/down state of the line:

l, _ = c.RequestLine(4, gpiocdev.WithPullUp) // during request
l.Reconfigure(gpiocdev.WithBiasDisabled)     // once requested

The bias options require Linux 5.5 or later.

Drive

The drive options control how an output line is driven when active and inactive:

l,_ := c.RequestLine(4, gpiocdev.AsOpenDrain) // during request
l.Reconfigure(gpiocdev.AsOpenSource)          // once requested

The default drive for output lines is push-pull, which actively drives the line in both directions.

Debounce

Input lines may be debounced using the WithDebounce option. The debouncing will be performed by the underlying hardware, if supported, else by the Linux kernel.

period := 10 * time.Millisecond
l, _ = c.RequestLine(4, gpiocdev.WithDebounce(period))// during request
l.Reconfigure(gpiocdev.WithDebounce(period))         // once requested

The WithDebounce option requires Linux 5.10 or later.

Edge Detection

The edge options control which edges on input lines will generate edge events. Edge events are passed to the event handler specified in the WithEventHandler(eh) option.

By default edge detection is not enabled on requested lines.

Refer to Edge Watches for examples of the edge detection options.

Event Clock

The event clock options control the source clock used to timestamp edge events. This is only useful for Linux kernels 5.11 and later - prior to that the clock source is fixed.

The event clock source used by the kernel has changed over time as follows:

Kernel Version Clock source
pre-5.7 CLOCK_REALTIME
5.7 - 5.10 CLOCK_MONOTONIC
5.11 and later configurable (defaults to CLOCK_MONOTONIC)

Determining which clock the edge event timestamps contain is currently left as an exercise for the user.

Configuration Options

The available configuration options are:

Option Category Description
WithConsumer1 Info Set the consumer label for the lines
AsActiveLow Level Treat a low physical line value as active
AsActiveHigh Level Treat a high physical line value as active (default)
AsInput Direction Request lines as input
AsIs2 Direction Request lines in their current input/output state (default)
AsOutput(<values>...)3 Direction Request lines as output with the provided values
AsPushPull Drive Request output lines drive both high and low (default)
AsOpenDrain Drive Request lines as open drain outputs
AsOpenSource Drive Request lines as open source outputs
WithEventHandler(eh)1 Send edge events detected on requested lines to the provided handler
WithEventBufferSize(num)1,5 Suggest the minimum number of events that can be stored in the kernel event buffer for the requested lines
WithFallingEdge Edge Detection3 Request lines with falling edge detection
WithRisingEdge Edge Detection3 Request lines with rising edge detection
WithBothEdges Edge Detection3 Request lines with rising and falling edge detection
WithoutEdges5 Edge Detection3 Request lines with edge detection disabled (default)
WithBiasAsIs Bias4 Request the lines have their bias setting left unaltered (default)
WithBiasDisabled Bias4 Request the lines have internal bias disabled
WithPullDown Bias4 Request the lines have internal pull-down enabled
WithPullUp Bias4 Request the lines have internal pull-up enabled
WithDebounce(period)5 Debounce Request the lines be debounced with the provided period
WithMonotonicEventClock Event Clock Request the timestamp in edge events use the monotonic clock (default)
WithRealtimeEventClock6 Event Clock Request the timestamp in edge events use the realtime clock
WithLines(offsets, options...)3,5 Specify configuration options for a subset of lines in a request
Defaulted5 Reset the configuration for a request to the default configuration, or the configuration of a particular line in a request to the default for that request

The options described as default are generally not required, except to override other options earlier in a chain of configuration options.

1 Can be applied to either NewChip or Chip.RequestLine, but cannot be used with Line.Reconfigure.

2 Can be applied to Chip.RequestLine, but cannot be used with NewChip or Line.Reconfigure.

3 Can be applied to either Chip.RequestLine or Line.Reconfigure, but cannot be used with NewChip.

4 Requires Linux 5.5 or later.

5 Requires Linux 5.10 or later.

6 Requires Linux 5.11 or later.

Installation

On Linux:

go get github.com/warthog618/go-gpiocdev

For other platforms, where you intend to cross-compile for Linux, don't attempt to compile the package when it is installed:

go get -d github.com/warthog618/go-gpiocdev

Tools

A companion package, gpiocdev-cli provides a command line tool that allows manual or scripted manipulation of GPIO lines. This utility combines the Go equivalent of all the libgpiod command line tools into a single tool.

Tests

The library is fully tested, other than some error cases and sanity checks that are difficult to trigger.

The tests require a kernel release 5.19 or later to run, built with CONFIG_GPIO_SIM set or as a module.

The tests must be run as root, to allow contruction of gpio-sims. They can still be built as an unprivileged user, e.g.

$ go test -c

but must be run as root.

The tests can also be cross-compiled for other platforms. e.g. build tests for a Raspberry Pi using:

$ GOOS=linux GOARCH=arm GOARM=6 go test -c

Later Pis can also use ARM7 (GOARM=7).

Benchmarks

The tests include benchmarks on reads, writes, bulk reads and writes, and interrupt latency.

These are the results from a Raspberry Pi Zero W running Linux 6.4 and built with go1.20.6:

$ ./go-gpiocdev.test -test.bench=.*
goos: linux
goarch: arm
pkg: github.com/warthog618/go-gpiocdev
BenchmarkChipNewClose     	     248	   4381075 ns/op
BenchmarkLineInfo         	   24651	     47278 ns/op
BenchmarkLineReconfigure  	   20312	     55273 ns/op
BenchmarkLineValue        	   71774	     14933 ns/op
BenchmarkLinesValues      	   54920	     24659 ns/op
BenchmarkLineSetValue     	   73359	     16501 ns/op
BenchmarkLinesSetValues   	   53557	     21056 ns/op
BenchmarkInterruptLatency 	     105	  10407929 ns/op
PASS

The latency benchmark is no longer representative as the measurement now depends on how quickly gpio-sim can toggle lines, and that is considerably slower than how quickly gpiocdev responds. For comparison, the same test using looped Raspberry Pi lines produced a result of ~640μsec on the same platform.

And on a Raspberry Pi 4 running Linux 6.4 (32bit kernel) and built with go1.20.6:

$ ./go-gpiocdev.test -test.bench=.*
goos: linux
goarch: arm
pkg: github.com/warthog618/go-gpiocdev
BenchmarkChipNewClose-4       	    9727	    118291 ns/op
BenchmarkLineInfo-4           	  185316	      6104 ns/op
BenchmarkLineReconfigure-4    	  364795	      3205 ns/op
BenchmarkLineValue-4          	 1072785	      1061 ns/op
BenchmarkLinesValues-4        	  816200	      1428 ns/op
BenchmarkLineSetValue-4       	 1015972	      1150 ns/op
BenchmarkLinesSetValues-4     	  715154	      1717 ns/op
BenchmarkInterruptLatency-4   	   18439	     61145 ns/op
PASS

Prerequisites

The library targets Linux with support for the GPIO character device API. That generally means that /dev/gpiochip0 exists.

The caller must have access to the character device - typically /dev/gpiochip0. That is generally root unless you have changed the permissions of that device.

The Bias line options and the Line.Reconfigure method both require Linux 5.5 or later.

Debounce and other uAPI v2 features require Linux 5.10 or later.

The requirements for each configuration option are noted in that section.

About

A native Go library for accessing GPIO lines on Linux platforms using the GPIO character device

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages