diff --git a/gpioioctl/README.md b/gpioioctl/README.md new file mode 100644 index 0000000..089dbc2 --- /dev/null +++ b/gpioioctl/README.md @@ -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 diff --git a/gpioioctl/basic_test.go b/gpioioctl/basic_test.go new file mode 100644 index 0000000..9e6bf2e --- /dev/null +++ b/gpioioctl/basic_test.go @@ -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.") + } +} diff --git a/gpioioctl/doc.go b/gpioioctl/doc.go new file mode 100644 index 0000000..095c198 --- /dev/null +++ b/gpioioctl/doc.go @@ -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 diff --git a/gpioioctl/example_test.go b/gpioioctl/example_test.go new file mode 100644 index 0000000..a5af380 --- /dev/null +++ b/gpioioctl/example_test.go @@ -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 + } + } + } +} diff --git a/gpioioctl/gpio.go b/gpioioctl/gpio.go new file mode 100644 index 0000000..2005c3a --- /dev/null +++ b/gpioioctl/gpio.go @@ -0,0 +1,610 @@ +//go:build linux + +package gpioioctl + +// 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 ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "periph.io/x/conn/v3/driver/driverreg" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/gpio/gpioreg" + "periph.io/x/conn/v3/physic" +) + +// LineDir is the configured direction of a GPIOLine. +type LineDir uint32 + +const ( + LineDirNotSet LineDir = 0 + LineInput LineDir = 1 + LineOutput LineDir = 2 +) + +// The consumer name to use for line requests. Initialized in init() +var consumer []byte + +// The set of GPIO Chips found on the running device. +var Chips []*GPIOChip + +type Label string + +var DirectionLabels = []Label{"NotSet", "Input", "Output"} +var PullLabels = []Label{"PullNoChange", "Float", "PullDown", "PullUp"} +var EdgeLabels = []Label{"NoEdge", "RisingEdge", "FallingEdge", "BothEdges"} + +// A GPIOLine represents a specific line of a GPIO Chip. GPIOLine implements +// periph.io/conn/v3/gpio.PinIn, PinIO, and PinOut. A line is obtained by +// calling gpioreg.ByName(), or using the GPIOChip.ByName() or ByNumber() +// methods. +type GPIOLine struct { + // The Offset of this line on the chip. Note that this has NO RELATIONSHIP + // to the pin numbering scheme that may be in use on a board. + number uint32 + // The name supplied by the OS Driver + name string + // If the line is in use, this may be populated with the + // consuming application's information. + consumer string + edge gpio.Edge + pull gpio.Pull + direction LineDir + mu sync.Mutex + chip_fd uintptr + fd int32 + fEdge *os.File +} + +func newGPIOLine(lineNum uint32, name string, consumer string, fd uintptr) *GPIOLine { + line := GPIOLine{ + number: lineNum, + name: strings.Trim(name, "\x00"), + consumer: strings.Trim(consumer, "\x00"), + mu: sync.Mutex{}, + chip_fd: fd, + } + return &line +} + +// Close the line, and any associated files/file descriptors that were created. +func (line *GPIOLine) Close() { + line.mu.Lock() + defer line.mu.Unlock() + if line.fEdge != nil { + _ = line.fEdge.Close() + } else if line.fd != 0 { + _ = syscall.Close(int(line.fd)) + } + line.fd = 0 + line.consumer = "" + line.edge = gpio.NoEdge + line.direction = LineDirNotSet + line.pull = gpio.PullNoChange + line.fEdge = nil +} + +// Consumer returns the name of the consumer specified for a line when +// a line request was performed. The format used by this library is +// program_name@pid. +func (line *GPIOLine) Consumer() string { + return line.consumer +} + +// DefaultPull - return gpio.PullNoChange. Reviewing the GPIO v2 Kernel IOCTL docs, this isn't possible. +func (line *GPIOLine) DefaultPull() gpio.Pull { + return gpio.PullNoChange +} + +// Deprecated: Use PinFunc.Func. Will be removed in v4. +func (line *GPIOLine) Function() string { + return "deprecated" +} + +// Halt interrupts a pending WaitForEdge() command. +func (line *GPIOLine) Halt() error { + if line.fEdge != nil { + return line.fEdge.SetReadDeadline(time.UnixMilli(0)) + } + return nil +} + +// Configure the GPIOLine for input. Implements gpio.PinIn. +func (line *GPIOLine) In(pull gpio.Pull, edge gpio.Edge) error { + line.mu.Lock() + defer line.mu.Unlock() + flags := getFlags(LineInput, edge, pull) + line.edge = edge + line.direction = LineInput + line.pull = pull + + return line.setLine(flags) +} + +// Implements gpio.Pin +func (line *GPIOLine) Name() string { + return line.name +} + +// Number returns the line offset/number within the GPIOChip. Implements gpio.Pin +func (line *GPIOLine) Number() int { + return int(line.number) +} + +// Write the specified level to the line. Implements gpio.PinOut +func (line *GPIOLine) Out(l gpio.Level) error { + line.mu.Lock() + defer line.mu.Unlock() + if line.direction != LineOutput { + err := line.setOut() + if err != nil { + return fmt.Errorf("GPIOLine.Out(): %w", err) + } + } + var data gpio_v2_line_values + data.mask = 0x01 + if l { + data.bits = 0x01 + } + return ioctl_set_gpio_v2_line_values(uintptr(line.fd), &data) +} + +// Pull returns the configured Line Bias. +func (line *GPIOLine) Pull() gpio.Pull { + return line.pull +} + +// Not implemented because the kernel PWM is not in the ioctl library +// but a different one. +func (line *GPIOLine) PWM(gpio.Duty, physic.Frequency) error { + return errors.New("PWM() not implemented") +} + +// Read the value of this line. Implements gpio.PinIn +func (line *GPIOLine) Read() gpio.Level { + if line.direction != LineInput { + err := line.In(gpio.PullUp, gpio.NoEdge) + if err != nil { + log.Println("GPIOLine.Read(): ", err) + return false + } + } + line.mu.Lock() + defer line.mu.Unlock() + var data gpio_v2_line_values + data.mask = 0x01 + err := ioctl_get_gpio_v2_line_values(uintptr(line.fd), &data) + if err != nil { + log.Println(err) + return false + } + return data.bits&0x01 == 0x01 +} + +func (line *GPIOLine) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Line int `json:"Line"` + Name string `json:"Name"` + Consumer string `json:"Consumer"` + Direction Label `json:"Direction"` + Pull Label `json:"Pull"` + Edges Label `json:"Edges"` + }{ + Line: line.Number(), + Name: line.Name(), + Consumer: line.Consumer(), + Direction: DirectionLabels[line.direction], + Pull: PullLabels[line.pull], + Edges: EdgeLabels[line.edge]}) +} + +// String returns information about the line in valid JSON format. +func (line *GPIOLine) String() string { + json, _ := json.MarshalIndent(line, "", " ") + return string(json) +} + +// Wait for this line to trigger and edge event. You must call In() with +// a valid edge for this to work. To interrupt a waiting line, call Halt(). +// Implements gpio.PinIn. +// +// Note that this does not return which edge was detected for the +// gpio.EdgeBoth configuration. If you really need the edge, +// LineSet.WaitForEdge() does return the edge that triggered. +// +// timeout for the edge change to occur. If 0, waits forever. +func (line *GPIOLine) WaitForEdge(timeout time.Duration) bool { + if line.edge == gpio.NoEdge || line.direction == LineDirNotSet { + log.Println("call to WaitForEdge() when line hasn't been configured for edge detection.") + return false + } + var err error + if line.fEdge == nil { + err = syscall.SetNonblock(int(line.fd), true) + if err != nil { + log.Println("WaitForEdge() SetNonblock(): ", err) + return false + } + line.fEdge = os.NewFile(uintptr(line.fd), fmt.Sprintf("gpio-%d", line.number)) + } + + if timeout == 0 { + err = line.fEdge.SetReadDeadline(time.Time{}) + } else { + err = line.fEdge.SetReadDeadline(time.Now().Add(timeout)) + } + if err != nil { + log.Println("GPIOLine.WaitForEdge() setReadDeadline() returned:", err) + return false + } + var event gpio_v2_line_event + // If the read times out, or is interrupted via Halt(), it will + // return "i/o timeout" + err = binary.Read(line.fEdge, binary.LittleEndian, &event) + + return err == nil +} + +// Return the file descriptor associated with this line. If it +// hasn't been previously requested, then open the file descriptor +// for it. +func (line *GPIOLine) getLine() (int32, error) { + if line.fd != 0 { + return line.fd, nil + } + var req gpio_v2_line_request + req.offsets[0] = uint32(line.number) + req.num_lines = 1 + for ix, charval := range []byte(consumer) { + req.consumer[ix] = charval + } + + err := ioctl_gpio_v2_line_request(uintptr(line.chip_fd), &req) + if err == nil { + line.fd = req.fd + line.consumer = string(consumer) + } else { + err = fmt.Errorf("line_request ioctl: %w", err) + } + return line.fd, err +} + +func (line *GPIOLine) setOut() error { + line.direction = LineOutput + line.edge = gpio.NoEdge + line.pull = gpio.PullNoChange + return line.setLine(getFlags(LineOutput, line.edge, line.pull)) +} + +func (line *GPIOLine) setLine(flags uint64) error { + req_fd, err := line.getLine() + if err != nil { + return err + } + + var req gpio_v2_line_config + req.flags = flags + return ioctl_gpio_v2_line_config(uintptr(req_fd), &req) +} + +// A representation of a Linux GPIO Chip. A computer may have +// more than one GPIOChip. +type GPIOChip struct { + // The name of the device as reported by the kernel. + name string + // Path represents the path to the /dev/gpiochip* character + // device used for ioctl() calls. + path string + label string + // The number of lines this device supports. + lineCount int + // The set of Lines associated with this device. + lines []*GPIOLine + // The LineSets opened on this device. + lineSets []*LineSet + // The file descriptor to the Path device. + fd uintptr + // File associated with the file descriptor. + file *os.File +} + +func (chip *GPIOChip) Name() string { + return chip.name +} + +func (chip *GPIOChip) Path() string { + return chip.path +} + +func (chip *GPIOChip) Label() string { + return chip.label +} + +func (chip *GPIOChip) LineCount() int { + return chip.lineCount +} + +func (chip *GPIOChip) Lines() []*GPIOLine { + return chip.lines +} + +func (chip *GPIOChip) LineSets() []*LineSet { + return chip.lineSets +} + +// Construct a new GPIOChip by opening the /dev/gpiochip* +// path specified and using Kernel ioctl() calls to +// read information about the chip and it's associated lines. +func newGPIOChip(path string) (*GPIOChip, error) { + chip := GPIOChip{path: path} + f, err := os.OpenFile(path, os.O_RDONLY, 0400) + if err != nil { + err = fmt.Errorf("opening gpio chip %s failed. error: %w", path, err) + log.Println(err) + return nil, err + } + chip.file = f + chip.fd = chip.file.Fd() + os.NewFile(uintptr(chip.fd), "GPIO Chip - "+path) + var info gpiochip_info + err = ioctl_gpiochip_info(chip.fd, &info) + if err != nil { + log.Printf("newGPIOChip: %s\n", err) + return nil, fmt.Errorf("newgpiochip %s: %w", path, err) + } + + chip.name = strings.Trim(string(info.name[:]), "\x00") + chip.label = strings.Trim(string(info.label[:]), "\x00") + chip.lineCount = int(info.lines) + var line_info gpio_v2_line_info + for line := 0; line < int(info.lines); line++ { + line_info.offset = uint32(line) + err := ioctl_gpio_v2_line_info(chip.fd, &line_info) + if err != nil { + log.Println("newGPIOChip get line info", err) + return nil, fmt.Errorf("reading line info: %w", err) + } + line := newGPIOLine(uint32(line), string(line_info.name[:]), string(line_info.consumer[:]), chip.fd) + chip.lines = append(chip.lines, line) + } + return &chip, nil +} + +// Close closes the file descriptor associated with the chipset, +// along with any configured Lines and LineSets. +func (chip *GPIOChip) Close() { + _ = chip.file.Close() + + for _, line := range chip.lines { + if line.fd != 0 { + line.Close() + } + } + for _, lineset := range chip.lineSets { + _ = lineset.Close() + } + _ = syscall.Close(int(chip.fd)) +} + +// ByName returns a GPIOLine for a specific name. If not +// found, returns nil. +func (chip *GPIOChip) ByName(name string) *GPIOLine { + for _, line := range chip.lines { + if line.name == name { + return line + } + } + return nil +} + +// ByNumber returns a line by it's specific GPIO Chip line +// number. Note this has NO RELATIONSHIP to a pin # on +// a board. +func (chip *GPIOChip) ByNumber(number int) *GPIOLine { + if number < 0 || number >= len(chip.lines) { + log.Printf("GPIOChip.ByNumber(%d) with out of range value.", number) + return nil + } + return chip.lines[number] +} + +// getFlags accepts a set of GPIO configuration values and returns an +// appropriate uint64 ioctl gpio flag. +func getFlags(dir LineDir, edge gpio.Edge, pull gpio.Pull) uint64 { + var flags uint64 + if dir == LineInput { + flags |= _GPIO_V2_LINE_FLAG_INPUT + } else if dir == LineOutput { + flags |= _GPIO_V2_LINE_FLAG_OUTPUT + } + if pull == gpio.PullUp { + flags |= _GPIO_V2_LINE_FLAG_BIAS_PULL_UP + } else if pull == gpio.PullDown { + flags |= _GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN + } + if edge == gpio.RisingEdge { + flags |= _GPIO_V2_LINE_FLAG_EDGE_RISING + } else if edge == gpio.FallingEdge { + flags |= _GPIO_V2_LINE_FLAG_EDGE_FALLING + } else if edge == gpio.BothEdges { + flags |= _GPIO_V2_LINE_FLAG_EDGE_RISING | _GPIO_V2_LINE_FLAG_EDGE_FALLING + } + return flags +} + +// Create a LineSet using the configuration specified by config. +func (chip *GPIOChip) LineSetFromConfig(config *LineSetConfig) (*LineSet, error) { + lines := make([]uint32, len(config.Lines)) + for ix, name := range config.Lines { + gpioLine := chip.ByName(name) + if gpioLine == nil { + return nil, fmt.Errorf("line %s not found in chip %s", name, chip.Name()) + } + lines[ix] = uint32(gpioLine.Number()) + } + req := config.getLineSetRequestStruct(lines) + + err := ioctl_gpio_v2_line_request(chip.fd, req) + if err != nil { + return nil, fmt.Errorf("LineSetFromConfig: %w", err) + } + ls := LineSet{fd: req.fd} + + for offset, lineName := range config.Lines { + lsl := chip.newLineSetLine(int(chip.ByName(lineName).Number()), offset, config) + lsl.parent = &ls + ls.lines = append(ls.lines, lsl) + } + + return &ls, nil +} + +// Create a representation of a specific line in the set. +func (chip *GPIOChip) newLineSetLine(line_number, offset int, config *LineSetConfig) *LineSetLine { + line := chip.ByNumber(line_number) + lsl := &LineSetLine{ + number: uint32(line_number), + offset: uint32(offset), + name: line.Name(), + direction: config.DefaultDirection, + pull: config.DefaultPull, + edge: config.DefaultEdge} + + for _, override := range config.Overrides { + for _, overrideLine := range override.Lines { + if overrideLine == line.Name() { + lsl.direction = override.Direction + lsl.edge = override.Edge + lsl.pull = override.Pull + + } + } + } + return lsl +} + +func (chip *GPIOChip) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Name string `json:"Name"` + Path string `json:"Path"` + Label string `json:"Label"` + LineCount int `json:"LineCount"` + Lines []*GPIOLine `json:"Lines"` + LineSets []*LineSet `json:"LineSets"` + }{ + Name: chip.Name(), + Path: chip.Path(), + Label: chip.Label(), + LineCount: chip.LineCount(), + Lines: chip.lines, + LineSets: chip.lineSets}) +} + +// String returns the chip information, and line information in JSON format. +func (chip *GPIOChip) String() string { + json, _ := json.MarshalIndent(chip, "", " ") + return string(json) +} + +// LineSet requests a set of io pins and configures them according to the +// parameters. Using a LineSet, you can perform IO operations on multiple +// lines in a single operation. For more control, see LineSetFromConfig. +func (chip *GPIOChip) LineSet(defaultDirection LineDir, defaultEdge gpio.Edge, defaultPull gpio.Pull, lines ...string) (*LineSet, error) { + cfg := &LineSetConfig{DefaultDirection: defaultDirection, DefaultEdge: defaultEdge, DefaultPull: defaultPull} + for _, lineName := range lines { + p := chip.ByName(lineName) + if p == nil { + return nil, fmt.Errorf("line %s not found", lineName) + } + cfg.Lines = append(cfg.Lines, p.Name()) + } + return chip.LineSetFromConfig(cfg) +} + +// driverGPIO implements periph.Driver. +type driverGPIO struct { + _ string +} + +func (d *driverGPIO) String() string { + return "ioctl-gpio" +} + +func (d *driverGPIO) Prerequisites() []string { + return nil +} + +func (d *driverGPIO) After() []string { + return nil +} + +// Init initializes GPIO ioctl handling code. +// +// # Uses Linux gpio ioctl as described at +// +// https://docs.kernel.org/userspace-api/gpio/chardev.html +func (d *driverGPIO) Init() (bool, error) { + items, err := filepath.Glob("/dev/gpiochip*") + if err != nil { + return true, err + } + if len(items) == 0 { + return false, errors.New("no GPIO chips found") + } + Chips = make([]*GPIOChip, 0) + for _, item := range items { + chip, err := newGPIOChip(item) + if err != nil { + log.Println("gpioioctl.driverGPIO.Init() Error", err) + return false, err + } + Chips = append(Chips, chip) + for _, line := range chip.lines { + if len(line.name) > 0 && line.name != "_" && line.name != "-" { + if err = gpioreg.Register(line); err != nil { + log.Println("chip", chip.Name(), " gpioreg.Register(line) ", line, " returned ", err) + } + } + } + } + return true, nil +} + +var drvGPIO driverGPIO + +func init() { + if runtime.GOOS == "linux" { + + // Init our consumer name. It's used when a line is requested, and + // allows utility programs like gpioinfo to find out who has a line + // open. + fname := path.Base(os.Args[0]) + s := fmt.Sprintf("%s@%d", fname, os.Getpid()) + charBytes := []byte(s) + if len(charBytes) >= _GPIO_MAX_NAME_SIZE { + charBytes = charBytes[:_GPIO_MAX_NAME_SIZE-1] + } + consumer = charBytes + + driverreg.MustRegister(&drvGPIO) + } +} + +// Ensure that Interfaces for these types are implemented fully. +var _ gpio.PinIO = &GPIOLine{} +var _ gpio.PinIn = &GPIOLine{} +var _ gpio.PinOut = &GPIOLine{} diff --git a/gpioioctl/ioctl.go b/gpioioctl/ioctl.go new file mode 100644 index 0000000..c45cdbb --- /dev/null +++ b/gpioioctl/ioctl.go @@ -0,0 +1,197 @@ +//go:build linux + +package gpioioctl + +// 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. + +// This file contains definitions and methods for using the GPIO IOCTL calls. +// +// Documentation for the ioctl() API is at: +// +// https://docs.kernel.org/userspace-api/gpio/index.html + +import ( + "errors" + "syscall" + "unsafe" +) + +// From the linux /usr/include/asm-generic/ioctl.h file. +const ( + _IOC_NONE = 0 + _IOC_WRITE = 1 + _IOC_READ = 2 + + _IOC_NRBITS = 8 + _IOC_TYPEBITS = 8 + _IOC_SIZEBITS = 14 + + _IOC_NRSHIFT = 0 + _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS + _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS + _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS +) + +func _IOC(dir, typ, nr, size uintptr) uintptr { + return dir<<_IOC_DIRSHIFT | + typ<<_IOC_TYPESHIFT | + nr<<_IOC_NRSHIFT | + size<<_IOC_SIZESHIFT +} + +func _IOR(typ, nr, size uintptr) uintptr { + return _IOC(_IOC_READ, typ, nr, size) +} + +func _IOWR(typ, nr, size uintptr) uintptr { + return _IOC(_IOC_READ|_IOC_WRITE, typ, nr, size) +} + +// From the /usr/include/linux/gpio.h header file. +const ( + _GPIO_MAX_NAME_SIZE = 32 + _GPIO_V2_LINE_NUM_ATTRS_MAX = 10 + _GPIO_V2_LINES_MAX = 64 + + _GPIO_V2_LINE_FLAG_USED uint64 = 1 << 0 + _GPIO_V2_LINE_FLAG_ACTIVE_LOW uint64 = 1 << 1 + _GPIO_V2_LINE_FLAG_INPUT uint64 = 1 << 2 + _GPIO_V2_LINE_FLAG_OUTPUT uint64 = 1 << 3 + _GPIO_V2_LINE_FLAG_EDGE_RISING uint64 = 1 << 4 + _GPIO_V2_LINE_FLAG_EDGE_FALLING uint64 = 1 << 5 + _GPIO_V2_LINE_FLAG_OPEN_DRAIN uint64 = 1 << 6 + _GPIO_V2_LINE_FLAG_OPEN_SOURCE uint64 = 1 << 7 + _GPIO_V2_LINE_FLAG_BIAS_PULL_UP uint64 = 1 << 8 + _GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN uint64 = 1 << 9 + _GPIO_V2_LINE_FLAG_BIAS_DISABLED uint64 = 1 << 10 + _GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME uint64 = 1 << 11 + _GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE uint64 = 1 << 12 + + _GPIO_V2_LINE_EVENT_RISING_EDGE uint32 = 1 + _GPIO_V2_LINE_EVENT_FALLING_EDGE uint32 = 2 + + _GPIO_V2_LINE_ATTR_ID_FLAGS uint32 = 1 + _GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES uint32 = 2 + _GPIO_V2_LINE_ATTR_ID_DEBOUNCE uint32 = 3 +) + +type gpiochip_info struct { + name [_GPIO_MAX_NAME_SIZE]byte + label [_GPIO_MAX_NAME_SIZE]byte + lines uint32 +} + +type gpio_v2_line_attribute struct { + id uint32 + padding uint32 + // value is actually a union who's interpretation is dependent upon + // the value of id. + value uint64 +} + +type gpio_v2_line_config_attribute struct { + attr gpio_v2_line_attribute + + mask uint64 +} + +type gpio_v2_line_config struct { + flags uint64 + num_attrs uint32 + padding [5]uint32 + attrs [_GPIO_V2_LINE_NUM_ATTRS_MAX]gpio_v2_line_config_attribute +} + +type gpio_v2_line_request struct { + offsets [_GPIO_V2_LINES_MAX]uint32 + consumer [_GPIO_MAX_NAME_SIZE]byte + config gpio_v2_line_config + num_lines uint32 + event_buffer_size uint32 + padding [5]uint32 + fd int32 +} + +// setLineNumber works around the false positive in gosec for using copy +func (lr *gpio_v2_line_request) setLineNumber(element int, number uint32) { + lr.offsets[element] = number +} + +type gpio_v2_line_values struct { + bits uint64 + mask uint64 +} + +type gpio_v2_line_info struct { + name [_GPIO_MAX_NAME_SIZE]byte + consumer [_GPIO_MAX_NAME_SIZE]byte + offset uint32 + num_attrs uint32 + flags uint64 + attrs [_GPIO_V2_LINE_NUM_ATTRS_MAX]gpio_v2_line_attribute + padding [4]uint32 +} + +type gpio_v2_line_event struct { + Timestamp_ns uint64 + Id uint32 + Offset uint32 + Seqno uint32 + LineSeqno uint32 + Padding [6]uint32 +} + +func ioctl_get_gpio_v2_line_values(fd uintptr, data *gpio_v2_line_values) error { + arg := _IOWR(0xb4, 0x0e, unsafe.Sizeof(gpio_v2_line_values{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} + +func ioctl_set_gpio_v2_line_values(fd uintptr, data *gpio_v2_line_values) error { + arg := _IOWR(0xb4, 0x0f, unsafe.Sizeof(gpio_v2_line_values{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} +func ioctl_gpiochip_info(fd uintptr, data *gpiochip_info) error { + arg := _IOR(0xb4, 0x01, unsafe.Sizeof(gpiochip_info{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} + +func ioctl_gpio_v2_line_info(fd uintptr, data *gpio_v2_line_info) error { + arg := _IOWR(0xb4, 0x05, unsafe.Sizeof(gpio_v2_line_info{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} + +func ioctl_gpio_v2_line_config(fd uintptr, data *gpio_v2_line_config) error { + arg := _IOWR(0xb4, 0x0d, unsafe.Sizeof(gpio_v2_line_config{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} + +func ioctl_gpio_v2_line_request(fd uintptr, data *gpio_v2_line_request) error { + arg := _IOWR(0xb4, 0x07, unsafe.Sizeof(gpio_v2_line_request{})) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, arg, uintptr(unsafe.Pointer(data))) + if ep != 0 { + return errors.New(ep.Error()) + } + return nil +} diff --git a/gpioioctl/lineset.go b/gpioioctl/lineset.go new file mode 100644 index 0000000..8eeb361 --- /dev/null +++ b/gpioioctl/lineset.go @@ -0,0 +1,419 @@ +//go:build linux + +package gpioioctl + +// 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 ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "sync" + "syscall" + "time" + + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/physic" +) + +// LineConfigOverride is an override for a LineSet configuration. +// For example, using this, you could configure a LineSet with +// multiple output lines, and a single input line with edge +// detection. +type LineConfigOverride struct { + Lines []string + Direction LineDir + Edge gpio.Edge + Pull gpio.Pull +} + +// LineSetConfig is used to create a structure for a LineSet request. +// It allows you to specify the default configuration for lines, as well +// as provide overrides for specific lines within the set. +type LineSetConfig struct { + Lines []string + DefaultDirection LineDir + DefaultEdge gpio.Edge + DefaultPull gpio.Pull + Overrides []*LineConfigOverride +} + +// AddOverrides adds a set of override values for specified lines. If a line +// specified is not already part of the configuration line set, it's dynamically +// added. +func (cfg *LineSetConfig) AddOverrides(direction LineDir, edge gpio.Edge, pull gpio.Pull, lines ...string) error { + if len(cfg.Overrides) == _GPIO_V2_LINE_NUM_ATTRS_MAX { + return fmt.Errorf("a maximum of %d override entries can be configured", _GPIO_V2_LINE_NUM_ATTRS_MAX) + } + for _, l := range lines { + if cfg.getLineOffset(l) < 0 { + cfg.Lines = append(cfg.Lines, l) + } + } + cfg.Overrides = append(cfg.Overrides, &LineConfigOverride{Lines: lines, Direction: direction, Edge: edge, Pull: pull}) + return nil +} + +func (cfg *LineSetConfig) getLineOffset(lineName string) int { + for ix, name := range cfg.Lines { + if name == lineName { + return ix + } + } + return -1 +} + +// Return a gpio_v2_line_request that represents this LineSetConfig. +// the returned value can then be used to request the lines. +func (cfg *LineSetConfig) getLineSetRequestStruct(lineNumbers []uint32) *gpio_v2_line_request { + + var lr gpio_v2_line_request + for ix, char := range []byte(consumer) { + lr.consumer[ix] = char + } + for ix, lineNumber := range lineNumbers { + lr.setLineNumber(ix, lineNumber) + } + lr.num_lines = uint32(len(cfg.Lines)) + lr.config.flags = getFlags(cfg.DefaultDirection, cfg.DefaultEdge, cfg.DefaultPull) + for _, lco := range cfg.Overrides { + var mask uint64 + attr := gpio_v2_line_attribute{id: _GPIO_V2_LINE_ATTR_ID_FLAGS, value: getFlags(lco.Direction, lco.Edge, lco.Pull)} + for _, line := range lco.Lines { + offset := cfg.getLineOffset(line) + mask |= uint64(1 << offset) + + } + lr.config.attrs[lr.config.num_attrs] = gpio_v2_line_config_attribute{attr: attr, mask: mask} + lr.config.num_attrs += 1 + } + + return &lr +} + +// LineSet is a set of GPIO lines that can be manipulated as one device. +// A LineSet is created by calling GPIOChip.LineSet(). Using a LineSet, +// you can write to multiple pins, or read from multiple +// pins as one operation. Additionally, you can configure multiple lines +// for edge detection, and have a single WaitForEdge() call that will +// trigger on a change to any of the lines in the set. According +// to the Linux kernel docs: +// +// "A number of lines may be requested in the one line request, and request +// operations are performed on the requested lines by the kernel as +// atomically as possible. e.g. GPIO_V2_LINE_GET_VALUES_IOCTL will read all +// the requested lines at once." +// +// https://docs.kernel.org/userspace-api/gpio/gpio-v2-get-line-ioctl.html +type LineSet struct { + lines []*LineSetLine + mu sync.Mutex + // The anonymous file descriptor for this set of lines. + fd int32 + // The file required for edge detection. + fEdge *os.File +} + +// Close the anonymous file descriptor allocated for this LineSet and release +// the pins. +func (ls *LineSet) Close() error { + ls.mu.Lock() + defer ls.mu.Unlock() + if ls.fd == 0 { + return nil + } + var err error + if ls.fEdge != nil { + err = ls.fEdge.Close() + } else if ls.fd != 0 { + err = syscall.Close(int(ls.fd)) + } + ls.fd = 0 + ls.fEdge = nil + // TODO: This really needs erased from GPIOChip.LineSets + return err +} + +// LineCount returns the number of lines in this LineSet. +func (ls *LineSet) LineCount() int { + return len(ls.lines) +} + +// Lines returns the set of LineSetLine that are in +// this set. +func (ls *LineSet) Lines() []*LineSetLine { + return ls.lines +} + +// Interrupt any calls to WaitForEdge(). +func (ls *LineSet) Halt() error { + if ls.fEdge != nil { + return ls.fEdge.SetReadDeadline(time.UnixMilli(0)) + } + return nil + +} + +// Out writes the set of bits to the LineSet's lines. If mask is 0, then the +// default mask of all bits is used. Note that by using the mask value, +// you can write to a subset of the lines if desired. +// +// bits is the values for each line in the bit set. +// +// mask is a bitmask indicating which bits should be applied. +func (ls *LineSet) Out(bits, mask uint64) error { + ls.mu.Lock() + defer ls.mu.Unlock() + var data gpio_v2_line_values + data.bits = bits + if mask == 0 { + mask = (1 << ls.LineCount()) - 1 + } + data.mask = mask + return ioctl_set_gpio_v2_line_values(uintptr(ls.fd), &data) +} + +// Read the pins in this LineSet. This is done as one syscall to the +// operating system and will be very fast. mask is a bitmask of set pins +// to read. If 0, then all pins are read. +func (ls *LineSet) Read(mask uint64) (uint64, error) { + ls.mu.Lock() + defer ls.mu.Unlock() + if mask == 0 { + mask = (1 << ls.LineCount()) - 1 + } + var lvalues gpio_v2_line_values + lvalues.mask = mask + if err := ioctl_get_gpio_v2_line_values(uintptr(ls.fd), &lvalues); err != nil { + return 0, err + } + return lvalues.bits, nil +} + +func (ls *LineSet) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Lines []*LineSetLine `json:"Lines"` + }{ + Lines: ls.lines}) +} + +// String returns the LineSet information in JSON, along with the details for +// all of the lines. +func (ls *LineSet) String() string { + json, _ := json.MarshalIndent(ls, "", " ") + return string(json) +} + +// WaitForEdge waits for an edge to be triggered on the LineSet. +// +// Returns: +// +// number - the number of the line that was triggered. +// +// edge - The edge value. gpio.Edge. If a timeout or halt occurred, +// then the edge returned will be gpio.NoEdge +// +// err - Error value if any. +func (ls *LineSet) WaitForEdge(timeout time.Duration) (number uint32, edge gpio.Edge, err error) { + number = 0 + edge = gpio.NoEdge + if ls.fEdge == nil { + err = syscall.SetNonblock(int(ls.fd), true) + if err != nil { + err = fmt.Errorf("WaitForEdge() - SetNonblock: %w", err) + return + } + ls.fEdge = os.NewFile(uintptr(ls.fd), "gpio-lineset") + } + + if timeout == 0 { + err = ls.fEdge.SetReadDeadline(time.Time{}) + } else { + err = ls.fEdge.SetReadDeadline(time.Now().Add(timeout)) + } + if err != nil { + err = fmt.Errorf("WaitForEdge() - SetReadDeadline(): %w", err) + return + } + + var event gpio_v2_line_event + err = binary.Read(ls.fEdge, binary.LittleEndian, &event) + if err != nil { + return + } + if event.Id == _GPIO_V2_LINE_EVENT_RISING_EDGE { + edge = gpio.RisingEdge + } else if event.Id == _GPIO_V2_LINE_EVENT_FALLING_EDGE { + edge = gpio.FallingEdge + } + number = uint32(event.Offset) + return +} + +// ByOffset returns a line by it's offset in the LineSet. +func (ls *LineSet) ByOffset(offset int) *LineSetLine { + if offset < 0 || offset >= len(ls.lines) { + return nil + } + return ls.lines[offset] +} + +// ByName returns a Line by name from the LineSet. +func (ls *LineSet) ByName(name string) *LineSetLine { + + for _, line := range ls.lines { + if line.Name() == name { + return line + } + } + return nil +} + +// LineNumber Return a line from the LineSet via it's GPIO line +// number. +func (ls *LineSet) ByNumber(number int) *LineSetLine { + for _, line := range ls.lines { + if line.Number() == number { + return line + } + } + return nil +} + +// LineSetLine is a specific line in a lineset. Using a LineSetLine, +// you can read/write to a single pin in the set using the PinIO +// interface. +type LineSetLine struct { + // The GPIO Line Number + number uint32 + // The offset for this LineSet struct + offset uint32 + name string + parent *LineSet + direction LineDir + pull gpio.Pull + edge gpio.Edge +} + +/* + gpio.Pin +*/ + +// Number returns the Line's GPIO Line Number. Implements gpio.Pin +func (lsl *LineSetLine) Number() int { + return int(lsl.number) +} + +// Name returns the line's name. Implements gpio.Pin +func (lsl *LineSetLine) Name() string { + return lsl.name +} + +func (lsl *LineSetLine) Function() string { + return "not implemented" +} + +func (lsl *LineSetLine) Direction() LineDir { + return lsl.direction +} + +func (lsl *LineSetLine) Edge() gpio.Edge { + return lsl.edge +} + +// Out writes to this specific GPIO line. +func (lsl *LineSetLine) Out(l gpio.Level) error { + var mask, bits uint64 + mask = 1 << lsl.offset + if l { + bits |= mask + } + return lsl.parent.Out(bits, mask) +} + +// PWM is not implemented because of kernel design. +func (lsl *LineSetLine) PWM(gpio.Duty, physic.Frequency) error { + return errors.New("not implemented") +} + +// Halt interrupts a pending WaitForEdge. You can't halt a read +// for a single line in a LineSet, so this returns an error. Use +// LineSet.Halt() +func (lsl *LineSetLine) Halt() error { + return errors.New("you can't halt an individual line in a LineSet. you must halt the LineSet") +} + +// In configures the line for input. Since individual lines in a +// LineSet cannot be re-configured this always returns an error. +func (lsl *LineSetLine) In(pull gpio.Pull, edge gpio.Edge) error { + return errors.New("a LineSet line cannot be re-configured") +} + +// Read returns the value of this specific line. +func (lsl *LineSetLine) Read() gpio.Level { + var mask uint64 = 1 << lsl.offset + bits, err := lsl.parent.Read(mask) + if err != nil { + log.Printf("LineSetLine.Read() Error reading line %d. Error: %s\n", lsl.number, err) + return false + } + + return (bits & mask) == mask +} + +func (lsl *LineSetLine) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Name string `json:"Name"` + Offset uint32 `json:"Offset"` + Number int `json:"Number"` + Direction Label `json:"Direction"` + Pull Label `json:"Pull"` + Edges Label `json:"Edges"` + }{ + Name: lsl.Name(), + Offset: lsl.Offset(), + Number: lsl.Number(), + Direction: DirectionLabels[lsl.direction], + Pull: PullLabels[lsl.pull], + Edges: EdgeLabels[lsl.edge]}) +} + +// String returns information about the line in JSON format. +func (lsl *LineSetLine) String() string { + json, _ := json.MarshalIndent(lsl, "", " ") + return string(json) +} + +// WaitForEdge will always return false for a LineSetLine. You MUST +// use LineSet.WaitForEdge() +func (lsl *LineSetLine) WaitForEdge(timeout time.Duration) bool { + return false +} + +// Pull returns the configured PullUp/PullDown value for this line. +func (lsl *LineSetLine) Pull() gpio.Pull { + return lsl.pull +} + +// DefaultPull return gpio.PullNoChange. +// +// The GPIO v2 ioctls do not support this. +func (lsl *LineSetLine) DefaultPull() gpio.Pull { + return gpio.PullNoChange +} + +// Offset returns the offset if this LineSetLine within the LineSet. +// 0..LineSet.LineCount +func (lsl *LineSetLine) Offset() uint32 { + return lsl.offset +} + +// Ensure that Interfaces for these types are implemented fully. +var _ gpio.PinIO = &LineSetLine{} +var _ gpio.PinIn = &LineSetLine{} +var _ gpio.PinOut = &LineSetLine{} diff --git a/host_linux.go b/host_linux.go index 9a569c1..5a729b9 100644 --- a/host_linux.go +++ b/host_linux.go @@ -5,6 +5,7 @@ package host import ( - // Make sure sysfs drivers are registered. + // Make sure required drivers are registered. + _ "periph.io/x/host/v3/gpioioctl" _ "periph.io/x/host/v3/sysfs" ) diff --git a/sysfs/gpio.go b/sysfs/gpio.go index 7e947ad..a776ca8 100644 --- a/sysfs/gpio.go +++ b/sysfs/gpio.go @@ -431,7 +431,7 @@ func (d *driverGPIO) String() string { } func (d *driverGPIO) Prerequisites() []string { - return nil + return []string{"ioctl-gpio"} } func (d *driverGPIO) After() []string { diff --git a/sysfs/gpio_test.go b/sysfs/gpio_test.go index e8204a0..32e1538 100644 --- a/sysfs/gpio_test.go +++ b/sysfs/gpio_test.go @@ -204,7 +204,7 @@ func TestPin_readInt(t *testing.T) { } func TestGPIODriver(t *testing.T) { - if len((&driverGPIO{}).Prerequisites()) != 0 { + if len((&driverGPIO{}).Prerequisites()) != 1 { t.Fatal("unexpected GPIO prerequisites") } }