-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
10 changed files
with
1,497 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# GPIO IOCTL | ||
|
||
This directory contains an implementation for Linux GPIO manipulation | ||
using the ioctl v2 interface. | ||
|
||
Basic test is provided, but a much more complete smoke test is provided | ||
in periph.io/x/cmd/periph-smoketest/gpiosmoketest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright 2024 The Periph Authors. All rights reserved. | ||
// Use of this source code is governed under the Apache License, Version 2.0 | ||
// that can be found in the LICENSE file. | ||
|
||
// Basic tests. More complete test is contained in the | ||
// periph.io/x/cmd/periph-smoketest/gpiosmoketest | ||
// folder. | ||
|
||
//go:build linux | ||
|
||
package gpioioctl | ||
|
||
import ( | ||
"log" | ||
"testing" | ||
|
||
"periph.io/x/conn/v3/gpio" | ||
"periph.io/x/conn/v3/gpio/gpioreg" | ||
) | ||
|
||
var testLine *GPIOLine | ||
|
||
func init() { | ||
var err error | ||
|
||
if len(Chips) == 0 { | ||
/* | ||
During pipeline builds, GPIOChips may not be available, or | ||
it may build on another OS. In that case, mock in enough | ||
for a test to pass. | ||
*/ | ||
line := GPIOLine{ | ||
number: 0, | ||
name: "DummyGPIOLine", | ||
consumer: "", | ||
edge: gpio.NoEdge, | ||
pull: gpio.PullNoChange, | ||
direction: LineDirNotSet, | ||
} | ||
|
||
chip := GPIOChip{name: "DummyGPIOChip", | ||
path: "/dev/gpiochipdummy", | ||
label: "Dummy GPIOChip for Testing Purposes", | ||
lineCount: 1, | ||
lines: []*GPIOLine{&line}, | ||
} | ||
Chips = append(Chips, &chip) | ||
if err = gpioreg.Register(&line); err != nil { | ||
nameStr := chip.Name() | ||
lineStr := line.String() | ||
log.Println("chip", nameStr, " gpioreg.Register(line) ", lineStr, " returned ", err) | ||
} | ||
} | ||
} | ||
|
||
func TestChips(t *testing.T) { | ||
chip := Chips[0] | ||
t.Log(chip.String()) | ||
if len(chip.Name()) == 0 { | ||
t.Error("chip.Name() is 0 length") | ||
} | ||
if len(chip.Path()) == 0 { | ||
t.Error("chip path is 0 length") | ||
} | ||
if len(chip.Label()) == 0 { | ||
t.Error("chip label is 0 length!") | ||
} | ||
if len(chip.Lines()) != chip.LineCount() { | ||
t.Errorf("Incorrect line count. Found: %d for LineCount, Returned Lines length=%d", chip.LineCount(), len(chip.Lines())) | ||
} | ||
for _, line := range chip.Lines() { | ||
if len(line.Consumer()) == 0 && len(line.Name()) > 0 { | ||
testLine = line | ||
break | ||
} | ||
} | ||
if testLine == nil { | ||
t.Error("Error finding unused line for testing!") | ||
} | ||
for _, c := range Chips { | ||
s := c.String() | ||
if len(s) == 0 { | ||
t.Error("Error calling chip.String(). No output returned!") | ||
} else { | ||
t.Log(s) | ||
} | ||
|
||
} | ||
|
||
} | ||
|
||
func TestGPIORegistryByName(t *testing.T) { | ||
if testLine == nil { | ||
return | ||
} | ||
outLine := gpioreg.ByName(testLine.Name()) | ||
if outLine == nil { | ||
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name()) | ||
} | ||
if outLine.Name() != testLine.Name() { | ||
t.Errorf("Error checking name. Expected %s, received %s", testLine.Name(), outLine.Name()) | ||
} | ||
|
||
if outLine.Number() < 0 || outLine.Number() >= len(Chips[0].Lines()) { | ||
t.Errorf("Invalid chip number %d received for %s", outLine.Number(), testLine.Name()) | ||
} | ||
} | ||
|
||
func TestNumber(t *testing.T) { | ||
chip := Chips[0] | ||
if testLine == nil { | ||
return | ||
} | ||
l := chip.ByName(testLine.Name()) | ||
if l == nil { | ||
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name()) | ||
} | ||
if l.Number() < 0 || l.Number() >= chip.LineCount() { | ||
t.Errorf("line.Number() returned value (%d) out of range", l.Number()) | ||
} | ||
l2 := chip.ByNumber(l.Number()) | ||
if l2 == nil { | ||
t.Errorf("retrieve Line from chip by number %d failed.", l.Number()) | ||
} | ||
|
||
} | ||
|
||
func TestString(t *testing.T) { | ||
if testLine == nil { | ||
return | ||
} | ||
line := gpioreg.ByName(testLine.Name()) | ||
if line == nil { | ||
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name()) | ||
} | ||
s := line.String() | ||
if len(s) == 0 { | ||
t.Errorf("GPIOLine.String() failed.") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright 2024 The Periph Authors. All rights reserved. | ||
// Use of this source code is governed under the Apache License, Version 2.0 | ||
// that can be found in the LICENSE file. | ||
// | ||
// Package gpioioctl provides access to Linux GPIO lines using the ioctl interface. | ||
// | ||
// https://docs.kernel.org/userspace-api/gpio/index.html | ||
// | ||
// GPIO Pins can be accessed via periph.io/x/conn/v3/gpio/gpioreg, | ||
// or using the Chips collection to access the specific GPIO chip | ||
// and using it's ByName()/ByNumber methods. | ||
// | ||
// GPIOChip provides a LineSet feature that allows you to atomically | ||
// read/write to multiple GPIO pins as a single operation. | ||
package gpioioctl |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
//go:build linux | ||
|
||
package gpioioctl_test | ||
|
||
// Copyright 2024 The Periph Authors. All rights reserved. | ||
// Use of this source code is governed under the Apache License, Version 2.0 | ||
// that can be found in the LICENSE file. | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"time" | ||
|
||
"periph.io/x/conn/v3/driver/driverreg" | ||
"periph.io/x/conn/v3/gpio" | ||
"periph.io/x/conn/v3/gpio/gpioreg" | ||
"periph.io/x/host/v3" | ||
"periph.io/x/host/v3/gpioioctl" | ||
) | ||
|
||
func Example() { | ||
_, _ = host.Init() | ||
_, _ = driverreg.Init() | ||
|
||
fmt.Println("GPIO Test Program") | ||
chip := gpioioctl.Chips[0] | ||
defer chip.Close() | ||
fmt.Println(chip.String()) | ||
// Test by flashing an LED. | ||
led := gpioreg.ByName("GPIO5") | ||
fmt.Println("Flashing LED ", led.Name()) | ||
for i := range 20 { | ||
_ = led.Out((i % 2) == 0) | ||
time.Sleep(500 * time.Millisecond) | ||
} | ||
_ = led.Out(true) | ||
|
||
testRotary(chip, "GPIO20", "GPIO21", "GPIO19") | ||
} | ||
|
||
// Test the LineSet functionality by using it to read a Rotary Encoder w/ Button. | ||
func testRotary(chip *gpioioctl.GPIOChip, stateLine, dataLine, buttonLine string) { | ||
config := gpioioctl.LineSetConfig{DefaultDirection: gpioioctl.LineInput, DefaultEdge: gpio.RisingEdge, DefaultPull: gpio.PullUp} | ||
config.Lines = []string{stateLine, dataLine, buttonLine} | ||
// The Data Pin of the Rotary Encoder should NOT have an edge. | ||
_ = config.AddOverrides(gpioioctl.LineInput, gpio.NoEdge, gpio.PullUp, dataLine) | ||
ls, err := chip.LineSetFromConfig(&config) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer ls.Close() | ||
statePinNumber := uint32(ls.ByOffset(0).Number()) | ||
buttonPinNumber := uint32(ls.ByOffset(2).Number()) | ||
|
||
var tLast = time.Now().Add(-1 * time.Second) | ||
var halting bool | ||
go func() { | ||
time.Sleep(60 * time.Second) | ||
halting = true | ||
fmt.Println("Sending halt!") | ||
_ = ls.Halt() | ||
}() | ||
fmt.Println("Test Rotary Switch - Turn dial to test rotary encoder, press button to test it.") | ||
for { | ||
lineNumber, _, err := ls.WaitForEdge(0) | ||
if err == nil { | ||
tNow := time.Now() | ||
if (tNow.UnixMilli() - tLast.UnixMilli()) < 100 { | ||
continue | ||
} | ||
tLast = tNow | ||
if lineNumber == statePinNumber { | ||
var bits uint64 | ||
tDeadline := tNow.UnixNano() + 20_000_000 | ||
var consecutive uint64 | ||
for time.Now().UnixNano() < tDeadline { | ||
// Spin on reading the pins until we get some number | ||
// of consecutive readings that are the same. | ||
bits, _ = ls.Read(0x03) | ||
if bits&0x01 == 0x00 { | ||
// We're bouncing. | ||
consecutive = 0 | ||
} else { | ||
consecutive += 1 | ||
if consecutive > 25 { | ||
if bits == 0x01 { | ||
fmt.Printf("Clockwise bits=%d\n", bits) | ||
} else if bits == 0x03 { | ||
fmt.Printf("CounterClockwise bits=%d\n", bits) | ||
} | ||
break | ||
} | ||
} | ||
} | ||
} else if lineNumber == buttonPinNumber { | ||
fmt.Println("Button Pressed!") | ||
} | ||
} else { | ||
fmt.Println("Timeout detected") | ||
if halting { | ||
break | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.