Skip to content

Commit

Permalink
raspi(pwm): add support for sysfs and fix pi-blaster (hybridgroup#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas authored Dec 11, 2023
1 parent a2690d2 commit 915d0c8
Show file tree
Hide file tree
Showing 25 changed files with 1,054 additions and 805 deletions.
2 changes: 1 addition & 1 deletion examples/raspi_led_brightness.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

func main() {
r := raspi.NewAdaptor()
led := gpio.NewLedDriver(r, "11")
led := gpio.NewLedDriver(r, "pwm0")

work := func() {
brightness := uint8(0)
Expand Down
92 changes: 92 additions & 0 deletions examples/raspi_servo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//go:build example
// +build example

// Do not build by default.

package main

import (
"fmt"
"log"
"time"

"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/drivers/gpio"
"gobot.io/x/gobot/v2/platforms/adaptors"
"gobot.io/x/gobot/v2/platforms/raspi"
)

// Wiring
// PWM Raspi: header pin 12 (GPIO18-PWM0), please refer to the README.md, located in the folder of raspi platform, on
// how to activate the pwm support.
// Servo: orange (PWM), black (GND), red (VCC) 4-6V (please read the manual of your device)
func main() {
const (
pwmPin = "pwm0"
wait = 3 * time.Second

fiftyHzNanos = 20 * 1000 * 1000 // 50Hz = 0.02 sec = 20 ms
)
// usually a frequency of 50Hz is used for servos, most servos have 0.5 ms..2.5 ms for 0-180°, however the mapping
// can be changed with options...
//
// for usage of pi-blaster driver just add the option "adaptors.WithPWMUsePiBlaster()" and use your pin number
// instead of "pwm0"
adaptor := raspi.NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pwmPin, fiftyHzNanos))
servo := gpio.NewServoDriver(adaptor, pwmPin)

work := func() {
fmt.Printf("first move to minimal position for %s...\n", wait)
if err := servo.ToMin(); err != nil {
log.Println(err)
}

time.Sleep(wait)

fmt.Printf("second move to center position for %s...\n", wait)
if err := servo.ToCenter(); err != nil {
log.Println(err)
}

time.Sleep(wait)

fmt.Printf("third move to maximal position for %s...\n", wait)
if err := servo.ToMax(); err != nil {
log.Println(err)
}

time.Sleep(wait)

fmt.Println("finally move 0-180° (or what your servo do for the new mapping) and back forever...")
angle := 0
fadeAmount := 45

gobot.Every(time.Second, func() {
if err := servo.Move(byte(angle)); err != nil {
log.Println(err)
}
angle = angle + fadeAmount
if angle < 0 || angle > 180 {
if angle < 0 {
angle = 0
}
if angle > 180 {
angle = 180
}
// change direction and recalculate
fadeAmount = -fadeAmount
angle = angle + fadeAmount
}
})
}

robot := gobot.NewRobot("motorBot",
[]gobot.Connection{adaptor},
[]gobot.Device{servo},
work,
)

if err := robot.Start(); err != nil {
panic(err)
}
}
2 changes: 1 addition & 1 deletion examples/tinkerboard_servo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
)

// Wiring
// PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND)
// PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND)
// PWM Tinkerboard: header pin 33 (PWM2) or pin 32 (PWM3)
func main() {
const (
Expand Down
118 changes: 118 additions & 0 deletions platforms/adaptors/piblasterpwm_pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package adaptors

import (
"fmt"
"os"
"strconv"

"gobot.io/x/gobot/v2/system"
)

const (
piBlasterPath = "/dev/pi-blaster"
piBlasterMinDutyNano = 10000 // 10 us
)

// piBlasterPWMPin is the Raspberry Pi implementation of the PWMPinner interface.
// It uses Pi Blaster.
type piBlasterPWMPin struct {
sys *system.Accesser
pin string
dc uint32
period uint32
}

// newPiBlasterPWMPin returns a new PWM pin for pi-blaster access.
func newPiBlasterPWMPin(sys *system.Accesser, pinNo int) *piBlasterPWMPin {
return &piBlasterPWMPin{
sys: sys,
pin: strconv.Itoa(pinNo),
}
}

// Export exports the pin for use by the Raspberry Pi
func (p *piBlasterPWMPin) Export() error {
return nil
}

// Unexport releases the pin from the operating system
func (p *piBlasterPWMPin) Unexport() error {
return p.writeValue(fmt.Sprintf("release %v\n", p.pin))
}

// Enabled returns always true for "enabled"
func (p *piBlasterPWMPin) Enabled() (bool, error) {
return true, nil
}

// SetEnabled do nothing for PiBlaster
func (p *piBlasterPWMPin) SetEnabled(e bool) error {
return nil
}

// Polarity returns always true for "normal"
func (p *piBlasterPWMPin) Polarity() (bool, error) {
return true, nil
}

// SetPolarity does not do anything when using PiBlaster
func (p *piBlasterPWMPin) SetPolarity(bool) error {
return nil
}

// Period returns the cached PWM period for pin
func (p *piBlasterPWMPin) Period() (uint32, error) {
return p.period, nil
}

// SetPeriod uses PiBlaster setting and cannot be changed. We allow setting once here to define a base period for
// ServoWrite(). see https://github.com/sarfata/pi-blaster#how-to-adjust-the-frequency-and-the-resolution-of-the-pwm
func (p *piBlasterPWMPin) SetPeriod(period uint32) error {
if p.period != 0 {
return fmt.Errorf("the period of PWM pins needs to be set to '%d' in pi-blaster source code", period)
}
p.period = period
return nil
}

// DutyCycle returns the duty cycle for the pin
func (p *piBlasterPWMPin) DutyCycle() (uint32, error) {
return p.dc, nil
}

// SetDutyCycle writes the duty cycle to the pin
func (p *piBlasterPWMPin) SetDutyCycle(dutyNanos uint32) error {
if p.period == 0 {
return fmt.Errorf("pi-blaster PWM pin period not set while try to set duty cycle to '%d'", dutyNanos)
}

if dutyNanos > p.period {
return fmt.Errorf("the duty cycle (%d) exceeds period (%d) for pi-blaster", dutyNanos, p.period)
}

// never go below minimum allowed duty for pi blaster unless the duty equals to 0
if dutyNanos < piBlasterMinDutyNano && dutyNanos != 0 {
dutyNanos = piBlasterMinDutyNano
fmt.Printf("duty cycle value limited to '%d' ns for pi-blaster", dutyNanos)
}

duty := float64(dutyNanos) / float64(p.period)
if err := p.writeValue(fmt.Sprintf("%v=%v\n", p.pin, duty)); err != nil {
return err
}

p.dc = dutyNanos
return nil
}

func (p *piBlasterPWMPin) writeValue(data string) error {
fi, err := p.sys.OpenFile(piBlasterPath, os.O_WRONLY|os.O_APPEND, 0o644)
defer fi.Close() //nolint:staticcheck // for historical reasons

if err != nil {
return err
}

_, err = fi.WriteString(data)
return err
}
79 changes: 79 additions & 0 deletions platforms/adaptors/piblasterpwm_pin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package adaptors

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/system"
)

var _ gobot.PWMPinner = (*piBlasterPWMPin)(nil)

func TestPiBlasterPWMPin(t *testing.T) {
// arrange
const path = "/dev/pi-blaster"
a := system.NewAccesser()
a.UseMockFilesystem([]string{path})
pin := newPiBlasterPWMPin(a, 1)
// act & assert: activate pin for usage
require.NoError(t, pin.Export())
require.NoError(t, pin.SetEnabled(true))
// act & assert: get and set polarity
val, err := pin.Polarity()
require.NoError(t, err)
assert.True(t, val)
require.NoError(t, pin.SetPolarity(false))
polarity, err := pin.Polarity()
assert.True(t, polarity)
require.NoError(t, err)
// act & assert: get and set period
period, err := pin.Period()
require.NoError(t, err)
assert.Equal(t, uint32(0), period)
require.NoError(t, pin.SetPeriod(20000000))
period, err = pin.Period()
require.NoError(t, err)
assert.Equal(t, uint32(20000000), period)
err = pin.SetPeriod(10000000)
require.EqualError(t, err, "the period of PWM pins needs to be set to '10000000' in pi-blaster source code")
// act & assert: cleanup
require.NoError(t, pin.Unexport())
}

func TestPiBlasterPWMPin_DutyCycle(t *testing.T) {
// arrange
const path = "/dev/pi-blaster"
a := system.NewAccesser()
a.UseMockFilesystem([]string{path})
pin := newPiBlasterPWMPin(a, 1)
// act & assert: activate pin for usage
require.NoError(t, pin.Export())
require.NoError(t, pin.SetEnabled(true))
// act & assert zero
dc, err := pin.DutyCycle()
require.NoError(t, err)
assert.Equal(t, uint32(0), dc)
// act & assert error without period set, the value remains zero
err = pin.SetDutyCycle(10000)
require.EqualError(t, err, "pi-blaster PWM pin period not set while try to set duty cycle to '10000'")
dc, err = pin.DutyCycle()
require.NoError(t, err)
assert.Equal(t, uint32(0), dc)
// arrange, act & assert a value
pin.period = 20000000
require.NoError(t, pin.SetDutyCycle(10000))
dc, err = pin.DutyCycle()
require.NoError(t, err)
assert.Equal(t, uint32(10000), dc)
// act & assert error on over limit, the value remains
err = pin.SetDutyCycle(20000001)
require.EqualError(t, err, "the duty cycle (20000001) exceeds period (20000000) for pi-blaster")
dc, err = pin.DutyCycle()
require.NoError(t, err)
assert.Equal(t, uint32(10000), dc)
// act & assert: cleanup
require.NoError(t, pin.Unexport())
}
Loading

0 comments on commit 915d0c8

Please sign in to comment.