Skip to content

Commit b69b28c

Browse files
authored
gpioioctl:Implement ioctl access to Linux GPIO chips/lines. (#59)
Includes new unit test that are conditionally compiled on linux. Mock in case of no chip available.
1 parent 1e513a0 commit b69b28c

File tree

10 files changed

+1497
-3
lines changed

10 files changed

+1497
-3
lines changed

gpioioctl/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# GPIO IOCTL
2+
3+
This directory contains an implementation for Linux GPIO manipulation
4+
using the ioctl v2 interface.
5+
6+
Basic test is provided, but a much more complete smoke test is provided
7+
in periph.io/x/cmd/periph-smoketest/gpiosmoketest

gpioioctl/basic_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2024 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
// Basic tests. More complete test is contained in the
6+
// periph.io/x/cmd/periph-smoketest/gpiosmoketest
7+
// folder.
8+
9+
//go:build linux
10+
11+
package gpioioctl
12+
13+
import (
14+
"log"
15+
"testing"
16+
17+
"periph.io/x/conn/v3/gpio"
18+
"periph.io/x/conn/v3/gpio/gpioreg"
19+
)
20+
21+
var testLine *GPIOLine
22+
23+
func init() {
24+
var err error
25+
26+
if len(Chips) == 0 {
27+
/*
28+
During pipeline builds, GPIOChips may not be available, or
29+
it may build on another OS. In that case, mock in enough
30+
for a test to pass.
31+
*/
32+
line := GPIOLine{
33+
number: 0,
34+
name: "DummyGPIOLine",
35+
consumer: "",
36+
edge: gpio.NoEdge,
37+
pull: gpio.PullNoChange,
38+
direction: LineDirNotSet,
39+
}
40+
41+
chip := GPIOChip{name: "DummyGPIOChip",
42+
path: "/dev/gpiochipdummy",
43+
label: "Dummy GPIOChip for Testing Purposes",
44+
lineCount: 1,
45+
lines: []*GPIOLine{&line},
46+
}
47+
Chips = append(Chips, &chip)
48+
if err = gpioreg.Register(&line); err != nil {
49+
nameStr := chip.Name()
50+
lineStr := line.String()
51+
log.Println("chip", nameStr, " gpioreg.Register(line) ", lineStr, " returned ", err)
52+
}
53+
}
54+
}
55+
56+
func TestChips(t *testing.T) {
57+
chip := Chips[0]
58+
t.Log(chip.String())
59+
if len(chip.Name()) == 0 {
60+
t.Error("chip.Name() is 0 length")
61+
}
62+
if len(chip.Path()) == 0 {
63+
t.Error("chip path is 0 length")
64+
}
65+
if len(chip.Label()) == 0 {
66+
t.Error("chip label is 0 length!")
67+
}
68+
if len(chip.Lines()) != chip.LineCount() {
69+
t.Errorf("Incorrect line count. Found: %d for LineCount, Returned Lines length=%d", chip.LineCount(), len(chip.Lines()))
70+
}
71+
for _, line := range chip.Lines() {
72+
if len(line.Consumer()) == 0 && len(line.Name()) > 0 {
73+
testLine = line
74+
break
75+
}
76+
}
77+
if testLine == nil {
78+
t.Error("Error finding unused line for testing!")
79+
}
80+
for _, c := range Chips {
81+
s := c.String()
82+
if len(s) == 0 {
83+
t.Error("Error calling chip.String(). No output returned!")
84+
} else {
85+
t.Log(s)
86+
}
87+
88+
}
89+
90+
}
91+
92+
func TestGPIORegistryByName(t *testing.T) {
93+
if testLine == nil {
94+
return
95+
}
96+
outLine := gpioreg.ByName(testLine.Name())
97+
if outLine == nil {
98+
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
99+
}
100+
if outLine.Name() != testLine.Name() {
101+
t.Errorf("Error checking name. Expected %s, received %s", testLine.Name(), outLine.Name())
102+
}
103+
104+
if outLine.Number() < 0 || outLine.Number() >= len(Chips[0].Lines()) {
105+
t.Errorf("Invalid chip number %d received for %s", outLine.Number(), testLine.Name())
106+
}
107+
}
108+
109+
func TestNumber(t *testing.T) {
110+
chip := Chips[0]
111+
if testLine == nil {
112+
return
113+
}
114+
l := chip.ByName(testLine.Name())
115+
if l == nil {
116+
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
117+
}
118+
if l.Number() < 0 || l.Number() >= chip.LineCount() {
119+
t.Errorf("line.Number() returned value (%d) out of range", l.Number())
120+
}
121+
l2 := chip.ByNumber(l.Number())
122+
if l2 == nil {
123+
t.Errorf("retrieve Line from chip by number %d failed.", l.Number())
124+
}
125+
126+
}
127+
128+
func TestString(t *testing.T) {
129+
if testLine == nil {
130+
return
131+
}
132+
line := gpioreg.ByName(testLine.Name())
133+
if line == nil {
134+
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
135+
}
136+
s := line.String()
137+
if len(s) == 0 {
138+
t.Errorf("GPIOLine.String() failed.")
139+
}
140+
}

gpioioctl/doc.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2024 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
//
5+
// Package gpioioctl provides access to Linux GPIO lines using the ioctl interface.
6+
//
7+
// https://docs.kernel.org/userspace-api/gpio/index.html
8+
//
9+
// GPIO Pins can be accessed via periph.io/x/conn/v3/gpio/gpioreg,
10+
// or using the Chips collection to access the specific GPIO chip
11+
// and using it's ByName()/ByNumber methods.
12+
//
13+
// GPIOChip provides a LineSet feature that allows you to atomically
14+
// read/write to multiple GPIO pins as a single operation.
15+
package gpioioctl

gpioioctl/example_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//go:build linux
2+
3+
package gpioioctl_test
4+
5+
// Copyright 2024 The Periph Authors. All rights reserved.
6+
// Use of this source code is governed under the Apache License, Version 2.0
7+
// that can be found in the LICENSE file.
8+
9+
import (
10+
"fmt"
11+
"log"
12+
"time"
13+
14+
"periph.io/x/conn/v3/driver/driverreg"
15+
"periph.io/x/conn/v3/gpio"
16+
"periph.io/x/conn/v3/gpio/gpioreg"
17+
"periph.io/x/host/v3"
18+
"periph.io/x/host/v3/gpioioctl"
19+
)
20+
21+
func Example() {
22+
_, _ = host.Init()
23+
_, _ = driverreg.Init()
24+
25+
fmt.Println("GPIO Test Program")
26+
chip := gpioioctl.Chips[0]
27+
defer chip.Close()
28+
fmt.Println(chip.String())
29+
// Test by flashing an LED.
30+
led := gpioreg.ByName("GPIO5")
31+
fmt.Println("Flashing LED ", led.Name())
32+
for i := range 20 {
33+
_ = led.Out((i % 2) == 0)
34+
time.Sleep(500 * time.Millisecond)
35+
}
36+
_ = led.Out(true)
37+
38+
testRotary(chip, "GPIO20", "GPIO21", "GPIO19")
39+
}
40+
41+
// Test the LineSet functionality by using it to read a Rotary Encoder w/ Button.
42+
func testRotary(chip *gpioioctl.GPIOChip, stateLine, dataLine, buttonLine string) {
43+
config := gpioioctl.LineSetConfig{DefaultDirection: gpioioctl.LineInput, DefaultEdge: gpio.RisingEdge, DefaultPull: gpio.PullUp}
44+
config.Lines = []string{stateLine, dataLine, buttonLine}
45+
// The Data Pin of the Rotary Encoder should NOT have an edge.
46+
_ = config.AddOverrides(gpioioctl.LineInput, gpio.NoEdge, gpio.PullUp, dataLine)
47+
ls, err := chip.LineSetFromConfig(&config)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
defer ls.Close()
52+
statePinNumber := uint32(ls.ByOffset(0).Number())
53+
buttonPinNumber := uint32(ls.ByOffset(2).Number())
54+
55+
var tLast = time.Now().Add(-1 * time.Second)
56+
var halting bool
57+
go func() {
58+
time.Sleep(60 * time.Second)
59+
halting = true
60+
fmt.Println("Sending halt!")
61+
_ = ls.Halt()
62+
}()
63+
fmt.Println("Test Rotary Switch - Turn dial to test rotary encoder, press button to test it.")
64+
for {
65+
lineNumber, _, err := ls.WaitForEdge(0)
66+
if err == nil {
67+
tNow := time.Now()
68+
if (tNow.UnixMilli() - tLast.UnixMilli()) < 100 {
69+
continue
70+
}
71+
tLast = tNow
72+
if lineNumber == statePinNumber {
73+
var bits uint64
74+
tDeadline := tNow.UnixNano() + 20_000_000
75+
var consecutive uint64
76+
for time.Now().UnixNano() < tDeadline {
77+
// Spin on reading the pins until we get some number
78+
// of consecutive readings that are the same.
79+
bits, _ = ls.Read(0x03)
80+
if bits&0x01 == 0x00 {
81+
// We're bouncing.
82+
consecutive = 0
83+
} else {
84+
consecutive += 1
85+
if consecutive > 25 {
86+
if bits == 0x01 {
87+
fmt.Printf("Clockwise bits=%d\n", bits)
88+
} else if bits == 0x03 {
89+
fmt.Printf("CounterClockwise bits=%d\n", bits)
90+
}
91+
break
92+
}
93+
}
94+
}
95+
} else if lineNumber == buttonPinNumber {
96+
fmt.Println("Button Pressed!")
97+
}
98+
} else {
99+
fmt.Println("Timeout detected")
100+
if halting {
101+
break
102+
}
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)