From f2576ad053bd88422b1e0aa2d707c881ca43564d Mon Sep 17 00:00:00 2001 From: George Sexton Date: Tue, 27 Aug 2024 20:30:44 -0600 Subject: [PATCH] Implement ioctl access to Linux GPIO chips/lines. --- gpioioctl/Testing.md | 73 +++++ gpioioctl/example_test.go | 103 +++++++ gpioioctl/gpio.go | 578 ++++++++++++++++++++++++++++++++++++++ gpioioctl/gpio_test.go | 239 ++++++++++++++++ gpioioctl/ioctl.go | 254 +++++++++++++++++ gpioioctl/lineset.go | 400 ++++++++++++++++++++++++++ gpioioctl/lineset_test.go | 317 +++++++++++++++++++++ sysfs/gpio.go | 520 ---------------------------------- sysfs/gpio_test.go | 248 ---------------- 9 files changed, 1964 insertions(+), 768 deletions(-) create mode 100644 gpioioctl/Testing.md create mode 100644 gpioioctl/example_test.go create mode 100644 gpioioctl/gpio.go create mode 100644 gpioioctl/gpio_test.go create mode 100644 gpioioctl/ioctl.go create mode 100644 gpioioctl/lineset.go create mode 100644 gpioioctl/lineset_test.go delete mode 100644 sysfs/gpio.go delete mode 100644 sysfs/gpio_test.go diff --git a/gpioioctl/Testing.md b/gpioioctl/Testing.md new file mode 100644 index 00000000..bd905b3c --- /dev/null +++ b/gpioioctl/Testing.md @@ -0,0 +1,73 @@ +# Testing + +The tests implemented perform *functional* tests of the library. This means that +the tests performed interact with a GPIO chipset, and perform actual read/write +operations. Using this test set, it's possible to quickly and accurately check +if the library is working as expected on a specific hardware/kernel combination. + +## Requirements + +Although the library is not Raspberry Pi specific, the GPIO pin names used for +tests are. + +As written, the tests must be executed on a Raspberry Pi SBC running Linux. Tested +models are: + +* Raspberry Pi 3B +* Raspberry Pi Zero W +* Raspberry Pi 4 +* Raspberry Pi 5 + +You must also have the golang SDK installed. + +## Setting Up + +In order to execute the functional tests, you must jumper the sets of pins shown +below together. + +For example, the single line tests require GPIO5 and GPIO13 to be connected to +each other, so a jumper is required between pins 29 and 33. For the multi-line +tests to work, you must connect the following GPIO pins together with jumpers. + +| GPIO Output | Output Pin # | GPIO Input | Input Pin # | +| ----------- | ------------ | ---------- | ----------- | +| GPIO2 | 3 | GPIO10 | 19 | +| GPIO3 | 5 | GPIO11 | 23 | +| GPIO4 | 7 | GPIO12 | 32 | +| GPIO5 | 29 | GPIO13 | 33 | +| GPIO6 | 31 | GPIO14 | 8 | +| GPIO7 | 26 | GPIO15 | 10 | +| GPIO8 | 24 | GPIO16 | 36 | +| GPIO9 | 21 | GPIO17 | 11 | + +## Cross-Compiling +If you don't have a working go installation on the target machine, you can cross +compile from one machine and then copy the test binary to the target machine. + +To cross compile for Raspberry Pi, execute the command: + +```bash +$periph.io/x/host/gpioctl> GOOS=linux GOARCH=arm64 go test -c +$periph.io/x/host/gpioctl> scp gpioioctl.test user@test.machine:~ +$periph.io/x/host/gpioctl> ssh user@test.machine +$user> ./gpioioctl.test -test.v +``` +for Pi Zero W, use: + +```bash +$periph.io/x/host/gpioctl> GOOS=linux GOARCH=arm GOARM=6 go test -c +$periph.io/x/host/gpioctl> scp gpioioctl.test user@test.machine:~ +$periph.io/x/host/gpioctl> ssh user@test.machine +$user> ./gpioioctl.test -test.v + +``` + +## Executing the Tests + +After connecting the jumper wires as shown above, and you have golang installed +and the go/bin directory in the path, change to this directory and execute the +command: + +```bash +$> go test -v -cover +``` diff --git a/gpioioctl/example_test.go b/gpioioctl/example_test.go new file mode 100644 index 00000000..2c018dff --- /dev/null +++ b/gpioioctl/example_test.go @@ -0,0 +1,103 @@ +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 ExampleChips() { + _, _ = 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 00000000..9328a845 --- /dev/null +++ b/gpioioctl/gpio.go @@ -0,0 +1,578 @@ +// 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. +// +// The gpioioctl package 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 + +import ( + "encoding/binary" + "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 + +var directionLabels = []string{"NotSet", "Input", "Output"} +var pullLabels = []string{"PullNoChange", "Float", "PullDown", "PullUp"} +var edgeLabels = []string{"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 +} + +// String returns information about the line in JSON format. +func (line *GPIOLine) String() string { + + return fmt.Sprintf("{\"Line\": %d, \"Name\": \"%s\", \"Consumer\": \"%s\", \"Direction\": \"%s\", \"Pull\": \"%s\", \"Edges\": \"%s\"}", + line.number, + line.name, + line.consumer, + directionLabels[line.direction], + pullLabels[line.pull], + edgeLabels[line.edge]) +} + +// 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. +// +// # Parameters +// +// 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 +} + +// 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, 0444) + 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 +} + +// String returns the chip information, and line information in JSON format. +func (chip *GPIOChip) String() string { + s := fmt.Sprintf("{\"Name\": \"%s\", \"Path\": \"%s\", \"Label\": \"%s\", \"LineCount\": %d, \"Lines\": [ \n", + chip.Name, chip.Path, chip.Label, chip.LineCount) + for _, line := range chip.Lines { + s += line.String() + ",\n" + } + s = s[:len(s)-2] + "]," + s+="\n\"LineSets\": [ \n" + for _, ls:= range chip.LineSets { + if ls.fd > 0 { + s+=ls.String()+",\n" + } + } + s = s[:len(s)-2] + "]}" + return s +} + +// 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) + } + } + } + fmt.Println(chip) + } + 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/gpio_test.go b/gpioioctl/gpio_test.go new file mode 100644 index 00000000..4b50b487 --- /dev/null +++ b/gpioioctl/gpio_test.go @@ -0,0 +1,239 @@ +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. +// +// The In/Out tests depend upon having a jumper wire connecting _OUT_LINE and +// _IN_LINE + +import ( + "testing" + "time" + + "periph.io/x/conn/v3/driver/driverreg" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/gpio/gpioreg" +) + +const ( + _OUT_LINE = "GPIO5" + _IN_LINE = "GPIO13" +) + +func init() { + _, _ = driverreg.Init() +} + +func TestChips(t *testing.T) { + if len(Chips) <= 0 { + t.Fatalf("Chips contains no entries.") + } + chip := Chips[0] + if len(chip.Name) == 0 { + t.Errorf("chip.Name() 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)) + } + s := chip.String() + if len(s) == 0 { + t.Error("Error calling chip.String(). No output returned!") + } +} + +func TestGPIORegistryByName(t *testing.T) { + outLine := gpioreg.ByName(_OUT_LINE) + if outLine == nil { + t.Fatalf("Error retrieving GPIO Line %s", _OUT_LINE) + } + if outLine.Name() != _OUT_LINE { + t.Errorf("Error checking name. Expected %s, received %s", _OUT_LINE, outLine.Name()) + } + + if outLine.Number() < 0 || outLine.Number() >= len(Chips[0].Lines) { + t.Errorf("Invalid chip number %d received for %s", outLine.Number(), _OUT_LINE) + } +} + +func TestConsumer(t *testing.T) { + chip := Chips[0] + l := chip.ByName(_OUT_LINE) + if l == nil { + t.Fatalf("Error retrieving GPIO Line %s", _OUT_LINE) + } + defer l.Close() + // Consumer isn't written until the line is configured. + err := l.Out(true) + if err != nil { + t.Errorf("l.Out() %s", err) + } + if l.Consumer() != string(consumer) { + t.Errorf("Incorrect consumer name. Expected consumer name %s on line. received empty %s", string(consumer), l.Consumer()) + } +} + +func TestNumber(t *testing.T) { + chip := Chips[0] + l := chip.ByName(_OUT_LINE) + if l == nil { + t.Fatalf("Error retrieving GPIO Line %s", _OUT_LINE) + } + 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) { + line := gpioreg.ByName(_OUT_LINE) + if line == nil { + t.Fatalf("Error retrieving GPIO Line %s", _OUT_LINE) + } + s := line.String() + if len(s) == 0 { + t.Errorf("GPIOLine.String() failed.") + } +} + +func TestWriteReadSinglePin(t *testing.T) { + var err error + chip := Chips[0] + inLine := chip.ByName(_IN_LINE) + outLine := chip.ByName(_OUT_LINE) + defer inLine.Close() + defer outLine.Close() + err = outLine.Out(true) + if err != nil { + t.Errorf("outLine.Out() %s", err) + } + if val := inLine.Read(); !val { + t.Error("Error reading/writing GPIO Pin. Expected true, received false!") + } + if inLine.Pull()!=gpio.PullUp { + t.Errorf("Pull() returned %s expected %s",pullLabels[inLine.Pull()],pullLabels[gpio.PullUp]) + } + err = outLine.Out(false) + if err != nil { + t.Errorf("outLine.Out() %s", err) + } + if val := inLine.Read(); val { + t.Error("Error reading/writing GPIO Pin. Expected false, received true!") + } + /* + By Design, lines should auto change directions if Read()/Out() are called + and they don't match. + */ + err = inLine.Out(false) + if err != nil { + t.Errorf("inLine.Out() %s", err) + } + time.Sleep(500 * time.Millisecond) + err = inLine.Out(true) + if err != nil { + t.Errorf("inLine.Out() %s", err) + } + if val := outLine.Read(); !val { + t.Error("Error read/writing with auto-reverse of line functions.") + } + err = inLine.Out(false) + if err != nil { + t.Errorf("TestWriteReadSinglePin() %s", err) + } + if val := outLine.Read(); val { + t.Error("Error read/writing with auto-reverse of line functions.") + } + +} + +func clearEdges(line gpio.PinIn) bool { + result := false + for line.WaitForEdge(10 * time.Millisecond) { + result = true + } + return result +} + +func TestWaitForEdgeTimeout(t *testing.T) { + line := Chips[0].ByName(_IN_LINE) + defer line.Close() + err := line.In(gpio.PullUp, gpio.BothEdges) + if err != nil { + t.Error(err) + } + clearEdges(line) + tStart := time.Now().UnixMilli() + line.WaitForEdge(5 * time.Second) + tEnd := time.Now().UnixMilli() + tDiff := tEnd - tStart + if tDiff < 4500 || tDiff > 5500 { + t.Errorf("timeout duration failure. Expected duration: 5000, Actual duration: %d", tDiff) + } +} + +// Test detection of rising, falling, and both. +func TestWaitForEdgeSinglePin(t *testing.T) { + tests := []struct { + startVal gpio.Level + edge gpio.Edge + writeVal gpio.Level + }{ + {startVal: false, edge: gpio.RisingEdge, writeVal: true}, + {startVal: true, edge: gpio.FallingEdge, writeVal: false}, + {startVal: false, edge: gpio.BothEdges, writeVal: true}, + {startVal: true, edge: gpio.BothEdges, writeVal: false}, + } + var err error + line := Chips[0].ByName(_IN_LINE) + outLine := Chips[0].ByName(_OUT_LINE) + defer line.Close() + defer outLine.Close() + + for _, test := range tests { + err = outLine.Out(test.startVal) + if err != nil { + t.Errorf("set initial value. %s", err) + } + err = line.In(gpio.PullUp, test.edge) + if err != nil { + t.Errorf("line.In() %s", err) + } + clearEdges(line) + err = outLine.Out(test.writeVal) + if err != nil { + t.Errorf("outLine.Out() %s", err) + } + if edgeReceived := line.WaitForEdge(time.Second); !edgeReceived { + t.Errorf("Expected Edge %s was not received on transition from %t to %t", edgeLabels[test.edge], test.startVal, test.writeVal) + } + } +} + +func TestHalt(t *testing.T) { + line := Chips[0].ByName(_IN_LINE) + defer line.Close() + err := line.In(gpio.PullUp, gpio.BothEdges) + if err != nil { + t.Fatalf("TestHalt() %s", err) + } + clearEdges(line) + // So what we'll do here is setup a goroutine to wait three seconds and then send a halt. + go func() { + time.Sleep(time.Second * 3) + err = line.Halt() + if err != nil { + t.Error(err) + } + }() + tStart := time.Now().UnixMilli() + line.WaitForEdge(time.Second * 30) + tEnd := time.Now().UnixMilli() + tDiff := tEnd - tStart + if tDiff > 3500 { + t.Errorf("error calling halt to interrupt WaitForEdge() Duration %d exceeded expected value.",tDiff) + } +} diff --git a/gpioioctl/ioctl.go b/gpioioctl/ioctl.go new file mode 100644 index 00000000..dc34f768 --- /dev/null +++ b/gpioioctl/ioctl.go @@ -0,0 +1,254 @@ +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" + "fmt" + "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 +} + +func (ci *gpiochip_info) String() string { + return fmt.Sprintf("{\"Name\": \"%s\", \"Label\": \"%s\", \"Lines\": %d}", string(ci.name[:]), string(ci.label[:]), ci.lines) +} + +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 +} + +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 +} + +func (li *gpio_v2_line_info) String() string { + FLAG_LIST := [...]uint64{ + _GPIO_V2_LINE_FLAG_USED, + _GPIO_V2_LINE_FLAG_ACTIVE_LOW, + _GPIO_V2_LINE_FLAG_INPUT, + _GPIO_V2_LINE_FLAG_OUTPUT, + _GPIO_V2_LINE_FLAG_EDGE_RISING, + _GPIO_V2_LINE_FLAG_EDGE_FALLING, + _GPIO_V2_LINE_FLAG_OPEN_DRAIN, + _GPIO_V2_LINE_FLAG_OPEN_SOURCE, + _GPIO_V2_LINE_FLAG_BIAS_PULL_UP, + _GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN, + _GPIO_V2_LINE_FLAG_BIAS_DISABLED, + _GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME, + _GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE, + } + FLAG_NAMES := [...][2]string{ + {"USED", "UNUSED"}, + {"ACTIVE_LOW", ""}, + {"INPUT", ""}, + {"OUTPUT", ""}, + {"EDGE_RISING", ""}, + {"EDGE_FALLING", ""}, + {"OPEN_DRAIN", ""}, + {"OPEN_SOURCE", ""}, + {"PULL_UP", ""}, + {"PULL_DOWN", ""}, + {"BIAS DISABLED", ""}, + {"EVENT_CLOCK_REALTIME", ""}, + {"EVENT_CLOCK_HTE", ""}, + } + + name := string(li.name[:]) + consumer := string(li.consumer[:]) + attr_string := "[" + for attr := 0; attr < int(li.num_attrs); attr++ { + attr_string = attr_string + fmt.Sprintf("{id: %x, value: %x},", li.attrs[attr].id, li.attrs[attr].value) + } + attr_string = attr_string + "]" + flag_str := "" + for i := range FLAG_LIST { + + if li.flags&FLAG_LIST[i] == FLAG_LIST[i] { + flag_str += FLAG_NAMES[i][0] + " " + } else { + if len(FLAG_NAMES[i][1]) > 0 { + flag_str += FLAG_NAMES[i][1] + " " + } + } + } + return fmt.Sprintf("{\"Name\": \"%s\", \"Consumer\": \"%s\", \"Offset\": %d, \"# Attrs\": %d, \"Flags\": \"%s\" 0x%x, \"Attributes\": \"%s\"}", + name, consumer, + li.offset, + li.num_attrs, + flag_str, + li.flags, + attr_string) +} + +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 00000000..f31b2b27 --- /dev/null +++ b/gpioioctl/lineset.go @@ -0,0 +1,400 @@ +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" + "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.offsets[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 { + if ls.fd==0 { + return nil + } + ls.mu.Lock() + defer ls.mu.Unlock() + 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 +} + +// String returns the LineSet information in JSON, along with the details for +// all of the lines. +func (ls *LineSet) String() string { + s := "{\"lines\": [\n" + for _, line := range ls.lines { + s += fmt.Stringer(line).String() + ",\n" + } + s += "]}" + return s +} + +// 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" +} + +/* +gpio.PinOut +*/ +// 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") +} + +/* +gpio.PinIn +*/ +// 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 +} + +// Return the line information in JSON format. +func (lsl *LineSetLine) String() string { + return fmt.Sprintf("{\"Name\": \"%s\", \"Offset\": %d, \"Number\": %d, \"Direction\": \"%s\", \"Pull\": \"%s\", \"Edge\": \"%s\"}", + lsl.name, + lsl.offset, + lsl.number, + directionLabels[lsl.direction], + pullLabels[lsl.pull], + edgeLabels[lsl.edge]) +} + +// 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. Reviewing the GPIO v2 Kernel +// IOCTL docs, this isn't possible. Returns gpio.PullNoChange +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/gpioioctl/lineset_test.go b/gpioioctl/lineset_test.go new file mode 100644 index 00000000..08a6dd94 --- /dev/null +++ b/gpioioctl/lineset_test.go @@ -0,0 +1,317 @@ +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 is the set of tests for the LineSet functionality. + +import ( + "testing" + "time" + + "periph.io/x/conn/v3/gpio" +) + +var outputLines = []string{"GPIO2", "GPIO3", "GPIO4", "GPIO5", "GPIO6", "GPIO7", "GPIO8", "GPIO9"} +var inputLines = []string{"GPIO10", "GPIO11", "GPIO12", "GPIO13", "GPIO14", "GPIO15", "GPIO16", "GPIO17"} + +func verifyLineSet(t *testing.T, + chip *GPIOChip, + lsTest *LineSet, + direction LineDir, + lines []string) { + lsTestLines := lsTest.Lines() + if lsTest.LineCount() != len(lines) || len(lsTestLines) != lsTest.LineCount() { + t.Errorf("lineset does not match length of lines %#v", lines) + } + s := lsTest.String() + if len(s) == 0 { + t.Error("error - empty string") + } + + // Verify each individual line is as expected. + for ix, lineName := range lines { + lsl := lsTestLines[ix] + line := chip.ByName(lineName) + if lsl.Offset() != uint32(ix) { + t.Errorf("Unexpected offset in LineSetLine. Expected: %d Found %d", ix, lsl.Offset()) + } + if lsl.Name() != line.Name() { + t.Errorf("Expected LineSetLine.Name()=%s, found %s", lineName, lsl.Name()) + } + if lsl.Number() != line.Number() { + t.Errorf("Expected LineSetLine.Number()=%d, found %d", line.Number(), lsl.Number()) + } + if lsl.Offset() != uint32(ix) { + t.Errorf("Line # %d, expected offset %d, got %d", lsl.Number(), ix, lsl.Offset()) + } + if lsl.direction != direction { + t.Errorf("Expected LineSetLine.direction=%s, found %s", directionLabels[direction], directionLabels[lsl.direction]) + } + s := lsl.String() + if len(s) == 0 { + t.Error("LineSetLine.String() returned empty string.") + } + e := lsl.Halt() + if e == nil { + t.Error("LineSetLine.Halt() should return an error!") + } + } +} + +func createLineSets(t *testing.T, chip *GPIOChip, edge gpio.Edge) (lsOutput *LineSet, lsInput *LineSet) { + // Create the Output Lineset + lsOutput, err := chip.LineSet(LineOutput, gpio.NoEdge, gpio.PullNoChange, outputLines...) + if err != nil { + t.Fatalf("Error creating output LineSet %s", err.Error()) + } + + verifyLineSet(t, chip, lsOutput, LineOutput, outputLines) + + // Create the Input LineSet + lsInput, err = chip.LineSet(LineInput, edge, gpio.PullUp, inputLines...) + if err != nil { + t.Fatalf("Error creating input LineSet %s", err.Error()) + } + + verifyLineSet(t, chip, lsInput, LineInput, inputLines) + return +} + +// Test Creating the line set and verify the pin setups. +func TestLineSetCreation(t *testing.T) { + chip := Chips[0] + lsOutput, lsInput := createLineSets(t, chip, gpio.NoEdge) + if lsOutput != nil { + errClose := lsOutput.Close() + if errClose != nil { + t.Errorf("Closing Output LineSet %v", errClose) + } + } + if lsInput != nil { + errClose := lsInput.Close() + if errClose != nil { + t.Errorf("Closing Output LineSet %v", errClose) + } + + } +} + +// Test writing to the output set and reading from the input set. +func TestLineSetReadWrite(t *testing.T) { + chip := Chips[0] + lsOutput, lsInput := createLineSets(t, chip, gpio.NoEdge) + if lsOutput == nil || lsInput == nil { + return + } + defer lsOutput.Close() + defer lsInput.Close() + limit := (1 << len(outputLines)) - 1 + mask := uint64(limit) + for i := range limit { + // Test Read of all pins in the set at once. + // + // Generally, if this is failing double-check your + // jumper wires between pins. + // + err := lsOutput.Out(uint64(i), mask) + if err != nil { + t.Errorf("Error writing to output set. Error=%s", err.Error()) + break + } + val, err := lsInput.Read(0) + if err != nil { + t.Error(err) + } + if val != uint64(i) { + t.Errorf("Error on input. Expected %d, Received: %d", i, val) + } + // Now, test the value obtained by reading each pin in the set + // individually. + var sum uint64 + for ix, line := range lsInput.Lines() { + if lineVal := line.Read(); lineVal { + sum += uint64(1 << ix) + } + } + if sum != uint64(i) { + t.Errorf("Error reading pins individually and summing them. Expected value: %d, Summed Value: %d", i, sum) + } + } +} + +func clearLineSetEdges(ls *LineSet) bool { + result := false + for { + _, _, err := ls.WaitForEdge(10 * time.Millisecond) + if err == nil { + result = true + } else { + // It timed out, so it's empty. + break + } + } + return result +} + +// Test the timeout function of the LineSet WaitForEdge +func TestLineSetWaitForEdgeTimeout(t *testing.T) { + lsOutput, lsInput := createLineSets(t, Chips[0], gpio.RisingEdge) + lsOutput.Close() + defer lsInput.Close() + clearLineSetEdges(lsInput) + tStart := time.Now().UnixMilli() + _, _, _ = lsInput.WaitForEdge(5 * time.Second) + tEnd := time.Now().UnixMilli() + tDiff := tEnd - tStart + if tDiff < 4500 || tDiff > 5500 { + t.Errorf("timeout duration failure. Expected duration: 5000, Actual duration: %d", tDiff) + } +} + +// Test the halt function successfully interupts a WaitForEdge() +func TestLineSetHalt(t *testing.T) { + chip := Chips[0] + lsOutput, lsInput := createLineSets(t, chip, gpio.BothEdges) + if lsOutput == nil || lsInput == nil { + return + } + lsOutput.Close() // Don't need it. + defer lsInput.Close() + + clearLineSetEdges(lsInput) + // So what we'll do here is setup a goroutine to wait three seconds and then send a halt. + go func() { + time.Sleep(time.Second * 3) + err := lsInput.Halt() + if err != nil { + t.Error(err) + } + }() + tStart := time.Now().UnixMilli() + _, _, _ = lsInput.WaitForEdge(time.Second * 30) + tEnd := time.Now().UnixMilli() + tDiff := tEnd - tStart + if tDiff > 3500 { + t.Errorf("error calling halt to interrupt LineSet.WaitForEdge() Duration not as expected. Actual Duration: %d",tDiff) + } +} + +// Execute WaitForEdge tests. The implementation ensures that the +// LineSetLine.Out() and LineSetLine.Read() functions work as +// expected too. +func TestLineSetWaitForEdge(t *testing.T) { + // Step 1 - Get the LineSets + chip := Chips[0] + lsOutput, err := chip.LineSet(LineOutput, gpio.NoEdge, gpio.PullNoChange, outputLines...) + if lsOutput == nil { + t.Errorf("Error creating output lineset. %s", err) + } + defer lsOutput.Close() + tests := []struct { + initValue gpio.Level + edgeSet gpio.Edge + expectedEdge gpio.Edge + writeValue gpio.Level + }{ + {initValue: false, edgeSet: gpio.RisingEdge, expectedEdge: gpio.RisingEdge, writeValue: true}, + {initValue: true, edgeSet: gpio.FallingEdge, expectedEdge: gpio.FallingEdge, writeValue: false}, + {initValue: false, edgeSet: gpio.BothEdges, expectedEdge: gpio.RisingEdge, writeValue: true}, + {initValue: true, edgeSet: gpio.BothEdges, expectedEdge: gpio.FallingEdge, writeValue: false}, + } + for _, test := range tests { + lsInput, err := chip.LineSet(LineInput, test.edgeSet, gpio.PullUp, inputLines...) + if err != nil { + t.Error(err) + return + } + for ix, line := range lsOutput.Lines() { + inLine := lsInput.Lines()[ix] + // Write the initial value. + err = line.Out(test.initValue) + if err != nil { + t.Error(err) + continue + } + // Clear any queued events. + clearLineSetEdges(lsInput) + go func() { + // Write that line to high + err := line.Out(test.writeValue) + if err != nil { + t.Error(err) + } + }() + // lineTriggered is the line number. + lineTriggered, edge, err := lsInput.WaitForEdge(time.Second) + if err == nil { + if lineTriggered != uint32(inLine.Number()) { + t.Errorf("Test: %#v expected line: %d triggered line: %d", test, lineTriggered, line.Number()) + } + if edge != test.expectedEdge { + t.Errorf("Test: %#v expected edge: %s received edge: %s", test, edgeLabels[test.expectedEdge], edgeLabels[edge]) + } + + if inLine.Read() != test.writeValue { + t.Errorf("Test: %#v received %t expected %t", test, inLine.Read(), test.writeValue) + } + } else { + t.Errorf("Test: %#v Line Offset: %d Error: %s", test, ix, err) + } + + } + lsInput.Close() + } +} + +// Test LineSetFromConfig with an Override on one line. +func TestLineSetConfigWithOverride(t *testing.T) { + chip := Chips[0] + line0 := chip.ByName(outputLines[0]) + line1 := chip.ByName(outputLines[1]) + cfg := LineSetConfig{ + Lines: []string{line0.Name(), line1.Name()}, + DefaultDirection: LineOutput, + DefaultEdge: gpio.NoEdge, + DefaultPull: gpio.PullNoChange, + } + err := cfg.AddOverrides(LineInput, gpio.RisingEdge, gpio.PullUp, []string{line1.Name()}...) + if err != nil { + t.Errorf("AddOverrides() %s", err) + } + ls, err := chip.LineSetFromConfig(&cfg) + if err != nil { + t.Errorf("Error creating lineset with override. %s", err) + return + } + if ls == nil { + t.Error("Error creating lineset. Returned value=nil") + return + } + defer ls.Close() + lsl := ls.ByNumber(line0.Number()) + if lsl.Number() != line0.Number() { + t.Errorf("LineSetLine pin 0 not as expected. Number=%d Expected: %d", lsl.Number(), line0.Number()) + } + if lsl.direction != LineOutput { + t.Error("LineSetLine override direction!=LineOutput") + } + if lsl.edge != gpio.NoEdge { + t.Error("LineSetLine override, edge!=gpio.NoEdge") + } + if lsl.Pull() != gpio.PullNoChange { + t.Error("LineSetLine override pull!=gpio.PullUp") + } + + lsl = ls.ByNumber(line1.Number()) + if lsl.direction != LineInput { + t.Errorf("LineSetLine override direction!=LineInput ls=%s", lsl) + } + if lsl.edge != gpio.RisingEdge { + t.Error("LineSetLine override, edge!=gpio.RisingEdge") + } + if lsl.Pull() != gpio.PullUp { + t.Error("LineSetLine override pull!=gpio.PullUp") + } +} diff --git a/sysfs/gpio.go b/sysfs/gpio.go deleted file mode 100644 index 7e947ad2..00000000 --- a/sysfs/gpio.go +++ /dev/null @@ -1,520 +0,0 @@ -// Copyright 2016 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 sysfs - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strconv" - "sync" - "time" - - "periph.io/x/conn/v3" - "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" - "periph.io/x/conn/v3/pin" - "periph.io/x/host/v3/fs" -) - -// Pins is all the pins exported by GPIO sysfs. -// -// Some CPU architectures have the pin numbers start at 0 and use consecutive -// pin numbers but this is not the case for all CPU architectures, some -// have gaps in the pin numbering. -// -// This global variable is initialized once at driver initialization and isn't -// mutated afterward. Do not modify it. -var Pins map[int]*Pin - -// Pin represents one GPIO pin as found by sysfs. -type Pin struct { - number int - name string - root string // Something like /sys/class/gpio/gpio%d/ - - mu sync.Mutex - err error // If open() failed - direction direction // Cache of the last known direction - edge gpio.Edge // Cache of the last edge used. - fDirection fileIO // handle to /sys/class/gpio/gpio*/direction; never closed - fEdge fileIO // handle to /sys/class/gpio/gpio*/edge; never closed - fValue fileIO // handle to /sys/class/gpio/gpio*/value; never closed - event fs.Event // Initialized once - buf [4]byte // scratch buffer for Func(), Read() and Out() -} - -// String implements conn.Resource. -func (p *Pin) String() string { - return p.name -} - -// Halt implements conn.Resource. -// -// It stops edge detection if enabled. -func (p *Pin) Halt() error { - p.mu.Lock() - defer p.mu.Unlock() - return p.haltEdge() -} - -// Name implements pin.Pin. -func (p *Pin) Name() string { - return p.name -} - -// Number implements pin.Pin. -func (p *Pin) Number() int { - return p.number -} - -// Function implements pin.Pin. -func (p *Pin) Function() string { - return string(p.Func()) -} - -// Func implements pin.PinFunc. -func (p *Pin) Func() pin.Func { - p.mu.Lock() - defer p.mu.Unlock() - // TODO(maruel): There's an internal bug which causes p.direction to be - // invalid (!?) Need to figure it out ASAP. - if err := p.open(); err != nil { - return pin.FuncNone - } - if _, err := seekRead(p.fDirection, p.buf[:]); err != nil { - return pin.FuncNone - } - if p.buf[0] == 'i' && p.buf[1] == 'n' { - p.direction = dIn - } else if p.buf[0] == 'o' && p.buf[1] == 'u' && p.buf[2] == 't' { - p.direction = dOut - } - if p.direction == dIn { - if p.Read() { - return gpio.IN_HIGH - } - return gpio.IN_LOW - } else if p.direction == dOut { - if p.Read() { - return gpio.OUT_HIGH - } - return gpio.OUT_LOW - } - return pin.FuncNone -} - -// SupportedFuncs implements pin.PinFunc. -func (p *Pin) SupportedFuncs() []pin.Func { - return []pin.Func{gpio.IN, gpio.OUT} -} - -// SetFunc implements pin.PinFunc. -func (p *Pin) SetFunc(f pin.Func) error { - switch f { - case gpio.IN: - return p.In(gpio.PullNoChange, gpio.NoEdge) - case gpio.OUT_HIGH: - return p.Out(gpio.High) - case gpio.OUT, gpio.OUT_LOW: - return p.Out(gpio.Low) - default: - return p.wrap(errors.New("unsupported function")) - } -} - -// In implements gpio.PinIn. -func (p *Pin) In(pull gpio.Pull, edge gpio.Edge) error { - if pull != gpio.PullNoChange && pull != gpio.Float { - return p.wrap(errors.New("doesn't support pull-up/pull-down")) - } - p.mu.Lock() - defer p.mu.Unlock() - if p.direction != dIn { - if err := p.open(); err != nil { - return p.wrap(err) - } - if err := seekWrite(p.fDirection, bIn); err != nil { - return p.wrap(err) - } - p.direction = dIn - } - // Always push none to help accumulated flush edges. This is not fool proof - // but it seems to help. - if p.fEdge != nil { - if err := seekWrite(p.fEdge, bNone); err != nil { - return p.wrap(err) - } - } - // Assume that when the pin was switched, the driver doesn't recall if edge - // triggering was enabled. - if edge != gpio.NoEdge { - if p.fEdge == nil { - var err error - p.fEdge, err = fileIOOpen(p.root+"edge", os.O_RDWR) - if err != nil { - return p.wrap(err) - } - if err = p.event.MakeEvent(p.fValue.Fd()); err != nil { - _ = p.fEdge.Close() - p.fEdge = nil - return p.wrap(err) - } - } - // Always reset the edge detection mode to none after starting the epoll - // otherwise edges are not always delivered, as observed on an Allwinner A20 - // running kernel 4.14.14. - if err := seekWrite(p.fEdge, bNone); err != nil { - return p.wrap(err) - } - var b []byte - switch edge { - case gpio.RisingEdge: - b = bRising - case gpio.FallingEdge: - b = bFalling - case gpio.BothEdges: - b = bBoth - } - if err := seekWrite(p.fEdge, b); err != nil { - return p.wrap(err) - } - } - p.edge = edge - // This helps to remove accumulated edges but this is not 100% sufficient. - // Most of the time the interrupts are handled promptly enough that this loop - // flushes the accumulated interrupt. - // Sometimes the kernel may have accumulated interrupts that haven't been - // processed for a long time, it can easily be >300µs even on a quite idle - // CPU. In this case, the loop below is not sufficient, since the interrupt - // will happen afterward "out of the blue". - if edge != gpio.NoEdge { - p.WaitForEdge(0) - } - return nil -} - -// Read implements gpio.PinIn. -func (p *Pin) Read() gpio.Level { - // There's no lock here. - if p.fValue == nil { - return gpio.Low - } - if _, err := seekRead(p.fValue, p.buf[:]); err != nil { - // Error. - return gpio.Low - } - if p.buf[0] == '0' { - return gpio.Low - } - if p.buf[0] == '1' { - return gpio.High - } - // Error. - return gpio.Low -} - -// WaitForEdge implements gpio.PinIn. -func (p *Pin) WaitForEdge(timeout time.Duration) bool { - // Run lockless, as the normal use is to call in a busy loop. - var ms int - if timeout == -1 { - ms = -1 - } else { - ms = int(timeout / time.Millisecond) - } - start := time.Now() - for { - if nr, err := p.event.Wait(ms); err != nil { - return false - } else if nr == 1 { - // TODO(maruel): According to pigpio, the correct way to consume the - // interrupt is to call Seek(). - return true - } - // A signal occurred. - if timeout != -1 { - ms = int((timeout - time.Since(start)) / time.Millisecond) - } - if ms <= 0 { - return false - } - } -} - -// Pull implements gpio.PinIn. -// -// It returns gpio.PullNoChange since gpio sysfs has no support for input pull -// resistor. -func (p *Pin) Pull() gpio.Pull { - return gpio.PullNoChange -} - -// DefaultPull implements gpio.PinIn. -// -// It returns gpio.PullNoChange since gpio sysfs has no support for input pull -// resistor. -func (p *Pin) DefaultPull() gpio.Pull { - return gpio.PullNoChange -} - -// Out implements gpio.PinOut. -func (p *Pin) Out(l gpio.Level) error { - p.mu.Lock() - defer p.mu.Unlock() - if p.direction != dOut { - if err := p.open(); err != nil { - return p.wrap(err) - } - if err := p.haltEdge(); err != nil { - return err - } - // "To ensure glitch free operation, values "low" and "high" may be written - // to configure the GPIO as an output with that initial value." - var d []byte - if l == gpio.Low { - d = bLow - } else { - d = bHigh - } - if err := seekWrite(p.fDirection, d); err != nil { - return p.wrap(err) - } - p.direction = dOut - return nil - } - if l == gpio.Low { - p.buf[0] = '0' - } else { - p.buf[0] = '1' - } - if err := seekWrite(p.fValue, p.buf[:1]); err != nil { - return p.wrap(err) - } - return nil -} - -// PWM implements gpio.PinOut. -// -// This is not supported on sysfs. -func (p *Pin) PWM(gpio.Duty, physic.Frequency) error { - return p.wrap(errors.New("pwm is not supported via sysfs")) -} - -// - -// open opens the gpio sysfs handle to /value and /direction. -// -// lock must be held. -func (p *Pin) open() error { - if p.fDirection != nil || p.err != nil { - return p.err - } - - if drvGPIO.exportHandle == nil { - return errors.New("sysfs gpio is not initialized") - } - - // Try to open the pin if it was there. It's possible it had been exported - // already. - if p.fValue, p.err = fileIOOpen(p.root+"value", os.O_RDWR); p.err == nil { - // Fast track. - goto direction - } else if !os.IsNotExist(p.err) { - // It exists but not accessible, not worth doing the remainder. - p.err = fmt.Errorf("need more access, try as root or setup udev rules: %v", p.err) - return p.err - } - - if _, p.err = drvGPIO.exportHandle.Write([]byte(strconv.Itoa(p.number))); p.err != nil && !isErrBusy(p.err) { - if os.IsPermission(p.err) { - p.err = fmt.Errorf("need more access, try as root or setup udev rules: %v", p.err) - } - return p.err - } - - // There's a race condition where the file may be created but udev is still - // running the Raspbian udev rule to make it readable to the current user. - // It's simpler to just loop a little as if /export is accessible, it doesn't - // make sense that gpioN/value doesn't become accessible eventually. - for start := time.Now(); time.Since(start) < 5*time.Second; { - // The virtual file creation is synchronous when writing to /export; albeit - // udev rule execution is asynchronous, so file mode change via udev rules - // takes some time to propagate. - if p.fValue, p.err = fileIOOpen(p.root+"value", os.O_RDWR); p.err == nil || !os.IsPermission(p.err) { - // Either success or a failure that is not a permission error. - break - } - } - if p.err != nil { - return p.err - } - -direction: - if p.fDirection, p.err = fileIOOpen(p.root+"direction", os.O_RDWR); p.err != nil { - _ = p.fValue.Close() - p.fValue = nil - } - return p.err -} - -// haltEdge stops any on-going edge detection. -func (p *Pin) haltEdge() error { - if p.edge != gpio.NoEdge { - if err := seekWrite(p.fEdge, bNone); err != nil { - return p.wrap(err) - } - p.edge = gpio.NoEdge - // This is still important to remove an accumulated edge. - p.WaitForEdge(0) - } - return nil -} - -func (p *Pin) wrap(err error) error { - return fmt.Errorf("sysfs-gpio (%s): %v", p, err) -} - -// - -type direction int - -const ( - dUnknown direction = 0 - dIn direction = 1 - dOut direction = 2 -) - -var ( - bIn = []byte("in") - bLow = []byte("low") - bHigh = []byte("high") - bNone = []byte("none") - bRising = []byte("rising") - bFalling = []byte("falling") - bBoth = []byte("both") -) - -// readInt reads a pseudo-file (sysfs) that is known to contain an integer and -// returns the parsed number. -func readInt(path string) (int, error) { - f, err := fileIOOpen(path, os.O_RDONLY) - if err != nil { - return 0, err - } - defer f.Close() - var b [24]byte - n, err := f.Read(b[:]) - if err != nil { - return 0, err - } - raw := b[:n] - if len(raw) == 0 || raw[len(raw)-1] != '\n' { - return 0, errors.New("invalid value") - } - return strconv.Atoi(string(raw[:len(raw)-1])) -} - -// driverGPIO implements periph.Driver. -type driverGPIO struct { - exportHandle io.Writer // handle to /sys/class/gpio/export -} - -func (d *driverGPIO) String() string { - return "sysfs-gpio" -} - -func (d *driverGPIO) Prerequisites() []string { - return nil -} - -func (d *driverGPIO) After() []string { - return nil -} - -// Init initializes GPIO sysfs handling code. -// -// Uses gpio sysfs as described at -// https://www.kernel.org/doc/Documentation/gpio/sysfs.txt -// -// GPIO sysfs is often the only way to do edge triggered interrupts. Doing this -// requires cooperation from a driver in the kernel. -// -// The main drawback of GPIO sysfs is that it doesn't expose internal pull -// resistor and it is much slower than using memory mapped hardware registers. -func (d *driverGPIO) Init() (bool, error) { - items, err := filepath.Glob("/sys/class/gpio/gpiochip*") - if err != nil { - return true, err - } - if len(items) == 0 { - return false, errors.New("no GPIO pin found") - } - - // There are hosts that use non-continuous pin numbering so use a map instead - // of an array. - Pins = map[int]*Pin{} - for _, item := range items { - if err = d.parseGPIOChip(item + "/"); err != nil { - return true, err - } - } - drvGPIO.exportHandle, err = fileIOOpen("/sys/class/gpio/export", os.O_WRONLY) - if os.IsPermission(err) { - return true, fmt.Errorf("need more access, try as root or setup udev rules: %v", err) - } - return true, err -} - -func (d *driverGPIO) parseGPIOChip(path string) error { - base, err := readInt(path + "base") - if err != nil { - return err - } - number, err := readInt(path + "ngpio") - if err != nil { - return err - } - // TODO(maruel): The chip driver may lie and lists GPIO pins that cannot be - // exported. The only way to know about it is to export it before opening. - for i := base; i < base+number; i++ { - if _, ok := Pins[i]; ok { - return fmt.Errorf("found two pins with number %d", i) - } - p := &Pin{ - number: i, - name: fmt.Sprintf("GPIO%d", i), - root: fmt.Sprintf("/sys/class/gpio/gpio%d/", i), - } - Pins[i] = p - if err := gpioreg.Register(p); err != nil { - return err - } - // If there is a CPU memory mapped gpio pin with the same number, the - // driver has to unregister this pin and map its own after. - if err := gpioreg.RegisterAlias(strconv.Itoa(i), p.name); err != nil { - return err - } - } - return nil -} - -func init() { - if isLinux { - driverreg.MustRegister(&drvGPIO) - } -} - -var drvGPIO driverGPIO - -var _ conn.Resource = &Pin{} -var _ gpio.PinIn = &Pin{} -var _ gpio.PinOut = &Pin{} -var _ gpio.PinIO = &Pin{} -var _ pin.PinFunc = &Pin{} diff --git a/sysfs/gpio_test.go b/sysfs/gpio_test.go deleted file mode 100644 index e8204a05..00000000 --- a/sysfs/gpio_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2017 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 sysfs - -import ( - "errors" - "testing" - - "periph.io/x/conn/v3/gpio" - "periph.io/x/conn/v3/physic" - "periph.io/x/conn/v3/pin" -) - -func TestPin_String(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if s := p.String(); s != "foo" { - t.Fatal(s) - } -} - -func TestPin_Name(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if s := p.Name(); s != "foo" { - t.Fatal(s) - } -} - -func TestPin_Number(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if n := p.Number(); n != 42 { - t.Fatal(n) - } -} - -func TestPin_Func(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - // Fails because open is not mocked. - if s := p.Func(); s != pin.FuncNone { - t.Fatal(s) - } - p = Pin{ - number: 42, - name: "foo", - root: "/tmp/gpio/priv/", - fDirection: &fakeGPIOFile{}, - } - if s := p.Func(); s != pin.FuncNone { - t.Fatal(s) - } - p.fDirection = &fakeGPIOFile{data: []byte("foo")} - if s := p.Func(); s != pin.FuncNone { - t.Fatal(s) - } - p.fDirection = &fakeGPIOFile{data: []byte("in")} - if s := p.Func(); s != gpio.IN_LOW { - t.Fatal(s) - } - p.fDirection = &fakeGPIOFile{data: []byte("out")} - if s := p.Func(); s != gpio.OUT_LOW { - t.Fatal(s) - } -} - -func TestPin_In(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if p.In(gpio.PullNoChange, gpio.NoEdge) == nil { - t.Fatal("can't open") - } - p = Pin{ - number: 42, - name: "foo", - root: "/tmp/gpio/priv/", - fDirection: &fakeGPIOFile{}, - } - if p.In(gpio.PullNoChange, gpio.NoEdge) == nil { - t.Fatal("can't read direction") - } - - p.fDirection = &fakeGPIOFile{data: []byte("out")} - if err := p.In(gpio.PullNoChange, gpio.NoEdge); err != nil { - t.Fatal(err) - } - if p.In(gpio.PullDown, gpio.NoEdge) == nil { - t.Fatal("pull not supported on sysfs-gpio") - } - if p.In(gpio.PullNoChange, gpio.BothEdges) == nil { - t.Fatal("can't open edge") - } - - p.fEdge = &fakeGPIOFile{} - if p.In(gpio.PullNoChange, gpio.NoEdge) == nil { - t.Fatal("edge I/O failed") - } - - p.fEdge = &fakeGPIOFile{data: []byte("none")} - if err := p.In(gpio.PullNoChange, gpio.NoEdge); err != nil { - t.Fatal(err) - } - if err := p.In(gpio.PullNoChange, gpio.RisingEdge); err != nil { - t.Fatal(err) - } - if err := p.In(gpio.PullNoChange, gpio.FallingEdge); err != nil { - t.Fatal(err) - } - if err := p.In(gpio.PullNoChange, gpio.BothEdges); err != nil { - t.Fatal(err) - } -} - -func TestPin_Read(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if l := p.Read(); l != gpio.Low { - t.Fatal("broken pin is always low") - } - p.fValue = &fakeGPIOFile{} - if l := p.Read(); l != gpio.Low { - t.Fatal("broken pin is always low") - } - p.fValue = &fakeGPIOFile{data: []byte("0")} - if l := p.Read(); l != gpio.Low { - t.Fatal("pin is low") - } - p.fValue = &fakeGPIOFile{data: []byte("1")} - if l := p.Read(); l != gpio.High { - t.Fatal("pin is high") - } - p.fValue = &fakeGPIOFile{data: []byte("2")} - if l := p.Read(); l != gpio.Low { - t.Fatal("pin is unknown") - } -} - -func TestPin_WaitForEdges(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if p.WaitForEdge(-1) { - t.Fatal("broken pin doesn't have edge triggered") - } -} - -func TestPin_Pull(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if pull := p.Pull(); pull != gpio.PullNoChange { - t.Fatal(pull) - } -} - -func TestPin_Out(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/", direction: dIn, edge: gpio.NoEdge} - if p.Out(gpio.High) == nil { - t.Fatal("can't open fake root") - } - p.fDirection = &fakeGPIOFile{} - if p.Out(gpio.High) == nil { - t.Fatal("failed to write to direction") - } - p.fDirection = &fakeGPIOFile{data: []byte("dummy")} - if err := p.Out(gpio.High); err != nil { - t.Fatal(err) - } - p.direction = dIn - if err := p.Out(gpio.Low); err != nil { - t.Fatal(err) - } - p.direction = dIn - p.edge = gpio.RisingEdge - p.fEdge = &fakeGPIOFile{} - if p.Out(gpio.High) == nil { - t.Fatal("failed to write to edge") - } - p.edge = gpio.RisingEdge - p.fEdge = &fakeGPIOFile{data: []byte("dummy")} - if err := p.Out(gpio.Low); err != nil { - t.Fatal(err) - } - - p.direction = dOut - p.edge = gpio.NoEdge - p.fValue = &fakeGPIOFile{} - if p.Out(gpio.Low) == nil { - t.Fatal("write to value failed") - } - p.fValue = &fakeGPIOFile{data: []byte("dummy")} - if err := p.Out(gpio.Low); err != nil { - t.Fatal(err) - } - if err := p.Out(gpio.High); err != nil { - t.Fatal(err) - } -} - -func TestPin_PWM(t *testing.T) { - p := Pin{number: 42, name: "foo", root: "/tmp/gpio/priv/"} - if p.PWM(gpio.DutyHalf, physic.KiloHertz) == nil { - t.Fatal("sysfs-gpio doesn't support PWM") - } -} - -func TestPin_readInt(t *testing.T) { - if _, err := readInt("/tmp/gpio/priv/invalid_file"); err == nil { - t.Fatal("file is not expected to exist") - } -} - -func TestGPIODriver(t *testing.T) { - if len((&driverGPIO{}).Prerequisites()) != 0 { - t.Fatal("unexpected GPIO prerequisites") - } -} - -// - -type fakeGPIOFile struct { - data []byte -} - -func (f *fakeGPIOFile) Close() error { - return nil -} - -func (f *fakeGPIOFile) Fd() uintptr { - return 0 -} - -func (f *fakeGPIOFile) Ioctl(op uint, data uintptr) error { - return errors.New("injected") -} - -func (f *fakeGPIOFile) Read(b []byte) (int, error) { - if f.data == nil { - return 0, errors.New("injected") - } - copy(b, f.data) - return len(f.data), nil -} - -func (f *fakeGPIOFile) Write(b []byte) (int, error) { - if f.data == nil { - return 0, errors.New("injected") - } - copy(f.data, b) - return len(f.data), nil -} - -func (f *fakeGPIOFile) Seek(offset int64, whence int) (int64, error) { - return 0, nil -}