1
0
mirror of https://github.com/hybridgroup/gobot.git synced 2025-04-24 13:48:49 +08:00

raspi(pwm): add support for sysfs and fix pi-blaster

This commit is contained in:
Thomas Kohler 2023-12-09 15:29:08 +01:00
parent a2690d2b15
commit ab4a76ba8f
25 changed files with 1059 additions and 810 deletions

View File

@ -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)

92
examples/raspi_servo.go Normal file
View File

@ -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)
}
}

View File

@ -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 (

View File

@ -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
}

View File

@ -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())
}

View File

@ -34,7 +34,10 @@ type pwmPinServoScale struct {
// pwmPinConfiguration contains all changeable attributes of the adaptor.
type pwmPinsConfiguration struct {
initialize pwmPinInitializer
usePiBlasterPin bool
periodDefault uint32
periodMinimum uint32
dutyRateMinimum float64 // is the minimal relation of duty/period (except 0.0)
polarityNormalIdentifier string
polarityInvertedIdentifier string
adjustDutyOnSetPeriod bool
@ -73,7 +76,7 @@ func NewPWMPinsAdaptor(sys *system.Accesser, t pwmPinTranslator, opts ...PwmPins
pinsDefaultPeriod: make(map[string]uint32),
pinsServoScale: make(map[string]pwmPinServoScale),
polarityNormalIdentifier: "normal",
polarityInvertedIdentifier: "inverted",
polarityInvertedIdentifier: "inversed",
adjustDutyOnSetPeriod: true,
},
}
@ -91,12 +94,28 @@ func WithPWMPinInitializer(pc pwmPinInitializer) pwmPinsInitializeOption {
return pwmPinsInitializeOption(pc)
}
// WithPWMUsePiBlaster substitute the default sysfs-implementation for PWM-pins by the implementation for pi-blaster.
func WithPWMUsePiBlaster() pwmPinsUsePiBlasterPinOption {
return pwmPinsUsePiBlasterPinOption(true)
}
// WithPWMDefaultPeriod substitute the default period of 10 ms (100 Hz) for all created pins.
func WithPWMDefaultPeriod(periodNanoSec uint32) pwmPinsPeriodDefaultOption {
return pwmPinsPeriodDefaultOption(periodNanoSec)
}
// WithPWMPolarityInvertedIdentifier use the given identifier, which will replace the default "inverted".
// WithPWMMinimumPeriod substitute the default minimum period limit of 0 nanoseconds.
func WithPWMMinimumPeriod(periodNanoSec uint32) pwmPinsPeriodMinimumOption {
return pwmPinsPeriodMinimumOption(periodNanoSec)
}
// WithPWMMinimumDutyRate substitute the default minimum duty rate of 1/period. The given limit only come into effect,
// if the rate is > 0, because a rate of 0.0 is always allowed.
func WithPWMMinimumDutyRate(dutyRate float64) pwmPinsDutyRateMinimumOption {
return pwmPinsDutyRateMinimumOption(dutyRate)
}
// WithPWMPolarityInvertedIdentifier use the given identifier, which will replace the default "inversed".
func WithPWMPolarityInvertedIdentifier(identifier string) pwmPinsPolarityInvertedIdentifierOption {
return pwmPinsPolarityInvertedIdentifierOption(identifier)
}
@ -132,6 +151,11 @@ func (a *PWMPinsAdaptor) Connect() error {
defer a.mutex.Unlock()
a.pins = make(map[string]gobot.PWMPinner)
if a.pwmPinsCfg.dutyRateMinimum == 0 && a.pwmPinsCfg.periodDefault > 0 {
a.pwmPinsCfg.dutyRateMinimum = 1 / float64(a.pwmPinsCfg.periodDefault)
}
return nil
}
@ -164,12 +188,18 @@ func (a *PWMPinsAdaptor) PwmWrite(id string, val byte) error {
if err != nil {
return err
}
period, err := pin.Period()
periodNanos, err := pin.Period()
if err != nil {
return err
}
duty := gobot.FromScale(float64(val), 0, 255.0)
return pin.SetDutyCycle(uint32(float64(period) * duty))
dutyNanos := float64(periodNanos) * gobot.FromScale(float64(val), 0, 255.0)
if err := a.validateDutyCycle(id, dutyNanos, float64(periodNanos)); err != nil {
return err
}
return pin.SetDutyCycle(uint32(dutyNanos))
}
// ServoWrite writes a servo signal to the specified pin. The given angle is between 0 and 180°.
@ -181,13 +211,14 @@ func (a *PWMPinsAdaptor) ServoWrite(id string, angle byte) error {
if err != nil {
return err
}
period, err := pin.Period() // nanoseconds
periodNanos, err := pin.Period()
if err != nil {
return err
}
if period != fiftyHzNanos {
log.Printf("WARNING: the PWM acts with a period of %d, but should use %d (50Hz) for servos\n", period, fiftyHzNanos)
if periodNanos != fiftyHzNanos {
log.Printf("WARNING: the PWM acts with a period of %d, but should use %d (50Hz) for servos\n",
periodNanos, fiftyHzNanos)
}
scale, ok := a.pwmPinsCfg.pinsServoScale[id]
@ -195,10 +226,15 @@ func (a *PWMPinsAdaptor) ServoWrite(id string, angle byte) error {
return fmt.Errorf("no scaler found for servo pin '%s'", id)
}
duty := gobot.ToScale(gobot.FromScale(float64(angle),
dutyNanos := gobot.ToScale(gobot.FromScale(float64(angle),
scale.minDegree, scale.maxDegree),
float64(scale.minDuty), float64(scale.maxDuty))
return pin.SetDutyCycle(uint32(duty))
if err := a.validateDutyCycle(id, dutyNanos, float64(periodNanos)); err != nil {
return err
}
return pin.SetDutyCycle(uint32(dutyNanos))
}
// SetPeriod adjusts the period of the specified PWM pin immediately.
@ -286,7 +322,13 @@ func (a *PWMPinsAdaptor) pwmPin(id string) (gobot.PWMPinner, error) {
if err != nil {
return nil, err
}
pin = a.sys.NewPWMPin(path, channel, a.pwmPinsCfg.polarityNormalIdentifier, a.pwmPinsCfg.polarityInvertedIdentifier)
if a.pwmPinsCfg.usePiBlasterPin {
pin = newPiBlasterPWMPin(a.sys, channel)
} else {
pin = a.sys.NewPWMPin(path, channel, a.pwmPinsCfg.polarityNormalIdentifier,
a.pwmPinsCfg.polarityInvertedIdentifier)
}
if err := a.pwmPinsCfg.initialize(id, pin); err != nil {
return nil, err
}
@ -296,6 +338,28 @@ func (a *PWMPinsAdaptor) pwmPin(id string) (gobot.PWMPinner, error) {
return pin, nil
}
func (a *PWMPinsAdaptor) validateDutyCycle(id string, dutyNanos, periodNanos float64) error {
if periodNanos == 0 {
return nil
}
if dutyNanos > periodNanos {
return fmt.Errorf("duty cycle (%d) exceeds period (%d) for PWM pin id '%s'",
uint32(dutyNanos), uint32(periodNanos), id)
}
if dutyNanos == 0 {
return nil
}
rate := dutyNanos / periodNanos
if rate < a.pwmPinsCfg.dutyRateMinimum {
return fmt.Errorf("duty rate (%.8f) is lower than allowed (%.8f) for PWM pin id '%s'",
rate, a.pwmPinsCfg.dutyRateMinimum, id)
}
return nil
}
// setPeriod adjusts the PWM period of the given pin. If duty cycle is already set and this feature is not suppressed,
// also this value will be adjusted in the same ratio. The order of writing the values must be observed, otherwise an
// error occur "write error: Invalid argument".

View File

@ -17,23 +17,32 @@ import (
)
const (
pwmDir = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/" //nolint:gosec // false positive
pwmPwm0Dir = pwmDir + "pwm44/"
pwmExportPath = pwmDir + "export"
pwmUnexportPath = pwmDir + "unexport"
pwmEnablePath = pwmPwm0Dir + "enable"
pwmPeriodPath = pwmPwm0Dir + "period"
pwmDutyCyclePath = pwmPwm0Dir + "duty_cycle"
pwmPolarityPath = pwmPwm0Dir + "polarity"
pwmDir = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/" //nolint:gosec // false positive
pwmPwm44Dir = pwmDir + "pwm44/"
pwmPwm47Dir = pwmDir + "pwm47/"
pwmExportPath = pwmDir + "export"
pwmUnexportPath = pwmDir + "unexport"
pwm44EnablePath = pwmPwm44Dir + "enable"
pwm44PeriodPath = pwmPwm44Dir + "period"
pwm44DutyCyclePath = pwmPwm44Dir + "duty_cycle"
pwm44PolarityPath = pwmPwm44Dir + "polarity"
pwm47EnablePath = pwmPwm47Dir + "enable"
pwm47PeriodPath = pwmPwm47Dir + "period"
pwm47DutyCyclePath = pwmPwm47Dir + "duty_cycle"
pwm47PolarityPath = pwmPwm47Dir + "polarity"
)
var pwmMockPaths = []string{
pwmExportPath,
pwmUnexportPath,
pwmEnablePath,
pwmPeriodPath,
pwmDutyCyclePath,
pwmPolarityPath,
pwm44EnablePath,
pwm44PeriodPath,
pwm44DutyCyclePath,
pwm44PolarityPath,
pwm47EnablePath,
pwm47PeriodPath,
pwm47DutyCyclePath,
pwm47PolarityPath,
}
// make sure that this PWMPinsAdaptor fulfills all the required interfaces
@ -47,10 +56,10 @@ func initTestPWMPinsAdaptorWithMockedFilesystem(mockPaths []string) (*PWMPinsAda
sys := system.NewAccesser()
fs := sys.UseMockFilesystem(mockPaths)
a := NewPWMPinsAdaptor(sys, testPWMPinTranslator)
fs.Files[pwmEnablePath].Contents = "0"
fs.Files[pwmPeriodPath].Contents = "0"
fs.Files[pwmDutyCyclePath].Contents = "0"
fs.Files[pwmPolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier
fs.Files[pwm44EnablePath].Contents = "0"
fs.Files[pwm44PeriodPath].Contents = "0"
fs.Files[pwm44DutyCyclePath].Contents = "0"
fs.Files[pwm44PolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier
if err := a.Connect(); err != nil {
panic(err)
}
@ -62,7 +71,7 @@ func testPWMPinTranslator(id string) (string, int, error) {
if err != nil {
return "", -1, fmt.Errorf("'%s' is not a valid id of a PWM pin", id)
}
channel = channel + 11 // just for tests
channel = channel + 11 // just for tests, 33=>pwm0, 36=>pwm3
return pwmDir, channel, err
}
@ -74,7 +83,7 @@ func TestNewPWMPinsAdaptor(t *testing.T) {
// assert
assert.Equal(t, uint32(pwmPeriodDefault), a.pwmPinsCfg.periodDefault)
assert.Equal(t, "normal", a.pwmPinsCfg.polarityNormalIdentifier)
assert.Equal(t, "inverted", a.pwmPinsCfg.polarityInvertedIdentifier)
assert.Equal(t, "inversed", a.pwmPinsCfg.polarityInvertedIdentifier)
assert.True(t, a.pwmPinsCfg.adjustDutyOnSetPeriod)
}
@ -97,8 +106,8 @@ func TestPWMPinsFinalize(t *testing.T) {
sys := system.NewAccesser()
fs := sys.UseMockFilesystem(pwmMockPaths)
a := NewPWMPinsAdaptor(sys, testPWMPinTranslator)
fs.Files[pwmPeriodPath].Contents = "0"
fs.Files[pwmDutyCyclePath].Contents = "0"
fs.Files[pwm44PeriodPath].Contents = "0"
fs.Files[pwm44DutyCyclePath].Contents = "0"
// assert that finalize before connect is working
require.NoError(t, a.Finalize())
// arrange
@ -140,30 +149,136 @@ func TestPWMPinsReConnect(t *testing.T) {
assert.Empty(t, a.pins)
}
func TestPwmWrite(t *testing.T) {
a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
err := a.PwmWrite("33", 100)
func TestPWMPinsCache(t *testing.T) {
// arrange
a, _ := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
// act
firstSysPin, err := a.PWMPin("33")
require.NoError(t, err)
secondSysPin, err := a.PWMPin("33")
require.NoError(t, err)
otherSysPin, err := a.PWMPin("36")
require.NoError(t, err)
// assert
assert.Equal(t, secondSysPin, firstSysPin)
assert.NotEqual(t, otherSysPin, firstSysPin)
}
assert.Equal(t, "44", fs.Files[pwmExportPath].Contents)
assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents)
//nolint:perfsprint // ok here
assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents)
assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents)
err = a.PwmWrite("notexist", 42)
require.ErrorContains(t, err, "'notexist' is not a valid id of a PWM pin")
fs.WithWriteError = true
err = a.PwmWrite("33", 100)
require.ErrorContains(t, err, "write error")
fs.WithWriteError = false
fs.WithReadError = true
err = a.PwmWrite("33", 100)
require.ErrorContains(t, err, "read error")
func TestPwmWrite(t *testing.T) {
tests := map[string]struct {
pin string
value byte
minimumRate float64
simulateWriteErr bool
simulateReadErr bool
wantExport string
wantEnable string
wantPeriod string
wantDutyCycle string
wantErr string
}{
"write_max": {
pin: "33",
value: 255,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "10000000",
},
"write_nearmax": {
pin: "33",
value: 254,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "9960784",
},
"write_mid": {
pin: "33",
value: 100,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "3921568",
},
"write_near min": {
pin: "33",
value: 1,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "39215",
},
"write_min": {
pin: "33",
value: 0,
minimumRate: 0.05,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "0",
},
"error_min_rate": {
pin: "33",
value: 1,
minimumRate: 0.05,
wantExport: "44",
wantEnable: "1",
wantPeriod: "10000000",
wantDutyCycle: "0",
wantErr: "is lower than allowed (0.05",
},
"error_non_existent_pin": {
pin: "notexist",
wantEnable: "0",
wantPeriod: "0",
wantDutyCycle: "0",
wantErr: "'notexist' is not a valid id of a PWM pin",
},
"error_write_error": {
pin: "33",
value: 10,
simulateWriteErr: true,
wantEnable: "0",
wantPeriod: "0",
wantDutyCycle: "0",
wantErr: "write error",
},
"error_read_error": {
pin: "33",
value: 11,
simulateReadErr: true,
wantExport: "44",
wantEnable: "0",
wantPeriod: "0",
wantDutyCycle: "0",
wantErr: "read error",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
if tc.minimumRate > 0 {
a.pwmPinsCfg.dutyRateMinimum = tc.minimumRate
}
fs.WithWriteError = tc.simulateWriteErr
fs.WithReadError = tc.simulateReadErr
// act
err := a.PwmWrite(tc.pin, tc.value)
// assert
assert.Equal(t, tc.wantExport, fs.Files[pwmExportPath].Contents)
assert.Equal(t, tc.wantEnable, fs.Files[pwm44EnablePath].Contents)
assert.Equal(t, tc.wantPeriod, fs.Files[pwm44PeriodPath].Contents)
assert.Equal(t, tc.wantDutyCycle, fs.Files[pwm44DutyCyclePath].Contents)
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
}
})
}
}
func TestServoWrite(t *testing.T) {
@ -172,15 +287,15 @@ func TestServoWrite(t *testing.T) {
err := a.ServoWrite("33", 0)
require.NoError(t, err)
assert.Equal(t, "44", fs.Files[pwmExportPath].Contents)
assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents)
assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents)
//nolint:perfsprint // ok here
assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents)
assert.Equal(t, "250000", fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents)
assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwm44PeriodPath].Contents)
assert.Equal(t, "250000", fs.Files[pwm44DutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
err = a.ServoWrite("33", 180)
require.NoError(t, err)
assert.Equal(t, "1250000", fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, "1250000", fs.Files[pwm44DutyCyclePath].Contents)
err = a.ServoWrite("notexist", 42)
require.ErrorContains(t, err, "'notexist' is not a valid id of a PWM pin")
@ -209,15 +324,15 @@ func TestSetPeriod(t *testing.T) {
// assert
require.NoError(t, err)
assert.Equal(t, "44", fs.Files[pwmExportPath].Contents)
assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents)
assert.Equal(t, fmt.Sprintf("%d", newPeriod), fs.Files[pwmPeriodPath].Contents) //nolint:perfsprint // ok here
assert.Equal(t, "0", fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents)
assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents)
assert.Equal(t, fmt.Sprintf("%d", newPeriod), fs.Files[pwm44PeriodPath].Contents) //nolint:perfsprint // ok here
assert.Equal(t, "0", fs.Files[pwm44DutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
// arrange test for automatic adjustment of duty cycle to lower value
err = a.PwmWrite("33", 127) // 127 is a little bit smaller than 50% of period
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(1270000), fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, strconv.Itoa(1270000), fs.Files[pwm44DutyCyclePath].Contents)
newPeriod = newPeriod / 10
// act
@ -225,7 +340,7 @@ func TestSetPeriod(t *testing.T) {
// assert
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(127000), fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, strconv.Itoa(127000), fs.Files[pwm44DutyCyclePath].Contents)
// arrange test for automatic adjustment of duty cycle to higher value
newPeriod = newPeriod * 20
@ -235,7 +350,7 @@ func TestSetPeriod(t *testing.T) {
// assert
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(2540000), fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, strconv.Itoa(2540000), fs.Files[pwm44DutyCyclePath].Contents)
// act
err = a.SetPeriod("not_exist", newPeriod)
@ -255,7 +370,7 @@ func Test_PWMPin(t *testing.T) {
wantErr string
}{
"pin_ok": {
mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath, pwmPolarityPath},
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath},
period: "0",
dutyCycle: "0",
translate: translator,
@ -269,7 +384,7 @@ func Test_PWMPin(t *testing.T) {
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/export: no such file",
},
"init_setenabled_error": {
mockPaths: []string{pwmExportPath, pwmPeriodPath},
mockPaths: []string{pwmExportPath, pwm44PeriodPath},
period: "1000",
translate: translator,
pin: "33",
@ -277,14 +392,14 @@ func Test_PWMPin(t *testing.T) {
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/enable: no such file",
},
"init_setperiod_dutycycle_no_error": {
mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath, pwmPolarityPath},
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath},
period: "0",
dutyCycle: "0",
translate: translator,
pin: "33",
},
"init_setperiod_error": {
mockPaths: []string{pwmExportPath, pwmEnablePath, pwmDutyCyclePath},
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44DutyCyclePath},
dutyCycle: "0",
translate: translator,
pin: "33",
@ -292,7 +407,7 @@ func Test_PWMPin(t *testing.T) {
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/period: no such file",
},
"init_setpolarity_error": {
mockPaths: []string{pwmExportPath, pwmEnablePath, pwmPeriodPath, pwmDutyCyclePath},
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath},
period: "0",
dutyCycle: "0",
translate: translator,
@ -311,10 +426,10 @@ func Test_PWMPin(t *testing.T) {
sys := system.NewAccesser()
fs := sys.UseMockFilesystem(tc.mockPaths)
if tc.period != "" {
fs.Files[pwmPeriodPath].Contents = tc.period
fs.Files[pwm44PeriodPath].Contents = tc.period
}
if tc.dutyCycle != "" {
fs.Files[pwmDutyCyclePath].Contents = tc.dutyCycle
fs.Files[pwm44DutyCyclePath].Contents = tc.dutyCycle
}
a := NewPWMPinsAdaptor(sys, tc.translate)
if err := a.Connect(); err != nil {

View File

@ -10,12 +10,22 @@ type PwmPinsOptionApplier interface {
// pwmPinInitializeOption is the type for applying another than the default initializer.
type pwmPinsInitializeOption pwmPinInitializer
// pwmPinsUsePiBlasterPinOption is the type for applying the usage of the pi-blaster PWM pin implementation, which will
// replace the default sysfs-implementation for PWM-pins.
type pwmPinsUsePiBlasterPinOption bool
// pwmPinPeriodDefaultOption is the type for applying another than the default period of 10 ms (100 Hz) for all
// created pins.
type pwmPinsPeriodDefaultOption uint32
// pwmPinsPeriodMinimumOption is the type for applying another than the default minimum period of "0".
type pwmPinsPeriodMinimumOption uint32
// pwmPinsDutyRateMinimumOption is the type for applying another than the default minimum rate of 1/period.
type pwmPinsDutyRateMinimumOption float64
// pwmPinPolarityInvertedIdentifierOption is the type for applying another identifier, which will replace the default
// "inverted".
// "inversed".
type pwmPinsPolarityInvertedIdentifierOption string
// pwmPinsAdjustDutyOnSetPeriodOption is the type for applying the automatic adjustment of duty cycle on setting
@ -49,12 +59,24 @@ func (o pwmPinsInitializeOption) String() string {
return "pin initializer option for PWM's"
}
func (o pwmPinsUsePiBlasterPinOption) String() string {
return "pi-blaster pin implementation option for PWM's"
}
func (o pwmPinsPeriodDefaultOption) String() string {
return "default period option for PWM's"
}
func (o pwmPinsPeriodMinimumOption) String() string {
return "minimum period option for PWM's"
}
func (o pwmPinsDutyRateMinimumOption) String() string {
return "minimum duty rate option for PWM's"
}
func (o pwmPinsPolarityInvertedIdentifierOption) String() string {
return "inverted identifier option for PWM's"
return "identifier for 'inversed' option for PWM's"
}
func (o pwmPinsAdjustDutyOnSetPeriodOption) String() string {
@ -77,10 +99,22 @@ func (o pwmPinsInitializeOption) apply(cfg *pwmPinsConfiguration) {
cfg.initialize = pwmPinInitializer(o)
}
func (o pwmPinsUsePiBlasterPinOption) apply(cfg *pwmPinsConfiguration) {
cfg.usePiBlasterPin = bool(o)
}
func (o pwmPinsPeriodDefaultOption) apply(cfg *pwmPinsConfiguration) {
cfg.periodDefault = uint32(o)
}
func (o pwmPinsPeriodMinimumOption) apply(cfg *pwmPinsConfiguration) {
cfg.periodMinimum = uint32(o)
}
func (o pwmPinsDutyRateMinimumOption) apply(cfg *pwmPinsConfiguration) {
cfg.dutyRateMinimum = float64(o)
}
func (o pwmPinsPolarityInvertedIdentifierOption) apply(cfg *pwmPinsConfiguration) {
cfg.polarityInvertedIdentifier = string(o)
}

View File

@ -25,6 +25,15 @@ func TestWithPWMPinInitializer(t *testing.T) {
assert.Equal(t, wantErr, err)
}
func TestWithPWMUsePiBlaster(t *testing.T) {
// arrange
cfg := &pwmPinsConfiguration{usePiBlasterPin: false}
// act
WithPWMUsePiBlaster().apply(cfg)
// assert
assert.True(t, cfg.usePiBlasterPin)
}
func TestWithPWMDefaultPeriod(t *testing.T) {
// arrange
const newPeriod = uint32(10)
@ -35,6 +44,26 @@ func TestWithPWMDefaultPeriod(t *testing.T) {
assert.Equal(t, newPeriod, cfg.periodDefault)
}
func TestWithPWMMinimumPeriod(t *testing.T) {
// arrange
const newMinPeriod = uint32(10)
cfg := &pwmPinsConfiguration{periodMinimum: 23}
// act
WithPWMMinimumPeriod(newMinPeriod).apply(cfg)
// assert
assert.Equal(t, newMinPeriod, cfg.periodMinimum)
}
func TestWithPWMMinimumDutyRate(t *testing.T) {
// arrange
const newRate = 11.0
cfg := &pwmPinsConfiguration{dutyRateMinimum: 33}
// act
WithPWMMinimumDutyRate(newRate).apply(cfg)
// assert
assert.InDelta(t, newRate, cfg.dutyRateMinimum, 0.0)
}
func TestWithPWMPolarityInvertedIdentifier(t *testing.T) {
// arrange
const newPolarityIdent = "pwm_invers"
@ -146,3 +175,16 @@ func TestWithPWMServoAngleRangeForPin(t *testing.T) {
})
}
}
func TestStringer(t *testing.T) {
assert.NotEmpty(t, pwmPinsInitializeOption(nil).String())
assert.NotEmpty(t, pwmPinsUsePiBlasterPinOption(true).String())
assert.NotEmpty(t, pwmPinsPeriodDefaultOption(1).String())
assert.NotEmpty(t, pwmPinsPeriodMinimumOption(1).String())
assert.NotEmpty(t, pwmPinsDutyRateMinimumOption(1).String())
assert.NotEmpty(t, pwmPinsPolarityInvertedIdentifierOption("1").String())
assert.NotEmpty(t, pwmPinsAdjustDutyOnSetPeriodOption(true).String())
assert.NotEmpty(t, pwmPinsDefaultPeriodForPinOption{}.String())
assert.NotEmpty(t, pwmPinsServoDutyScaleForPinOption{}.String())
assert.NotEmpty(t, pwmPinsServoAngleScaleForPinOption{}.String())
}

View File

@ -1,7 +1,6 @@
package jetson
import (
"errors"
"fmt"
"sync"
@ -13,7 +12,9 @@ import (
)
const (
pwmPeriodDefault = 3000000 // 3 ms = 333 Hz
pwmPeriodDefault = 3000000 // 3 ms = 333 Hz
pwmPeriodMinimum = 5334
pwmDutyRateMinimum = 0.0005 // minimum duty of 1500 for default period, ~3 for minimum period
defaultI2cBusNumber = 1
@ -26,11 +27,11 @@ const (
// Adaptor is the Gobot adaptor for the Jetson Nano
type Adaptor struct {
name string
sys *system.Accesser
mutex sync.Mutex
pwmPins map[string]gobot.PWMPinner
name string
sys *system.Accesser
mutex *sync.Mutex
*adaptors.DigitalPinsAdaptor
*adaptors.PWMPinsAdaptor
*adaptors.I2cBusAdaptor
*adaptors.SpiBusAdaptor
}
@ -41,143 +42,99 @@ type Adaptor struct {
//
// adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs
// adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.#
func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor {
//
// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor]
func NewAdaptor(opts ...interface{}) *Adaptor {
sys := system.NewAccesser()
c := &Adaptor{
name: gobot.DefaultName("JetsonNano"),
sys: sys,
a := &Adaptor{
name: gobot.DefaultName("JetsonNano"),
sys: sys,
mutex: &sync.Mutex{},
}
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...)
c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber)
c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber,
var digitalPinsOpts []func(adaptors.DigitalPinsOptioner)
pwmPinsOpts := []adaptors.PwmPinsOptionApplier{
adaptors.WithPWMDefaultPeriod(pwmPeriodDefault),
adaptors.WithPWMMinimumPeriod(pwmPeriodMinimum),
adaptors.WithPWMMinimumDutyRate(pwmDutyRateMinimum),
}
for _, opt := range opts {
switch o := opt.(type) {
case func(adaptors.DigitalPinsOptioner):
digitalPinsOpts = append(digitalPinsOpts, o)
case adaptors.PwmPinsOptionApplier:
pwmPinsOpts = append(pwmPinsOpts, o)
default:
panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name))
}
}
a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...)
a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...)
a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber)
a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber,
defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed)
return c
return a
}
// Name returns the Adaptor's name
func (c *Adaptor) Name() string {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Name() string {
a.mutex.Lock()
defer a.mutex.Unlock()
return c.name
return a.name
}
// SetName sets the Adaptor's name
func (c *Adaptor) SetName(n string) {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) SetName(n string) {
a.mutex.Lock()
defer a.mutex.Unlock()
c.name = n
a.name = n
}
// Connect create new connection to board and pins.
func (c *Adaptor) Connect() error {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Connect() error {
a.mutex.Lock()
defer a.mutex.Unlock()
if err := c.SpiBusAdaptor.Connect(); err != nil {
if err := a.SpiBusAdaptor.Connect(); err != nil {
return err
}
if err := c.I2cBusAdaptor.Connect(); err != nil {
if err := a.I2cBusAdaptor.Connect(); err != nil {
return err
}
c.pwmPins = make(map[string]gobot.PWMPinner)
return c.DigitalPinsAdaptor.Connect()
if err := a.PWMPinsAdaptor.Connect(); err != nil {
return err
}
return a.DigitalPinsAdaptor.Connect()
}
// Finalize closes connection to board and pins
func (c *Adaptor) Finalize() error {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Finalize() error {
a.mutex.Lock()
defer a.mutex.Unlock()
err := c.DigitalPinsAdaptor.Finalize()
err := a.DigitalPinsAdaptor.Finalize()
for _, pin := range c.pwmPins {
if pin != nil {
if perr := pin.Unexport(); err != nil {
err = multierror.Append(err, perr)
}
}
}
c.pwmPins = nil
if e := c.I2cBusAdaptor.Finalize(); e != nil {
if e := a.PWMPinsAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := c.SpiBusAdaptor.Finalize(); e != nil {
if e := a.I2cBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := a.SpiBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
return err
}
// PWMPin returns a Jetson Nano. PWMPin which provides the gobot.PWMPinner interface
func (c *Adaptor) PWMPin(pin string) (gobot.PWMPinner, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.pwmPin(pin)
}
// PwmWrite writes a PWM signal to the specified pin
func (c *Adaptor) PwmWrite(pin string, val byte) error {
c.mutex.Lock()
defer c.mutex.Unlock()
sysPin, err := c.pwmPin(pin)
if err != nil {
return err
}
duty := uint32(gobot.FromScale(float64(val), 0, 255) * float64(pwmPeriodDefault))
return sysPin.SetDutyCycle(duty)
}
// ServoWrite writes a servo signal to the specified pin
func (c *Adaptor) ServoWrite(pin string, angle byte) error {
c.mutex.Lock()
defer c.mutex.Unlock()
sysPin, err := c.pwmPin(pin)
if err != nil {
return err
}
duty := uint32(gobot.FromScale(float64(angle), 0, 180) * float64(pwmPeriodDefault))
return sysPin.SetDutyCycle(duty)
}
func (c *Adaptor) pwmPin(pin string) (gobot.PWMPinner, error) {
if c.pwmPins == nil {
return nil, fmt.Errorf("not connected")
}
if c.pwmPins[pin] != nil {
return c.pwmPins[pin], nil
}
fn, err := c.translatePwmPin(pin)
if err != nil {
return nil, err
}
c.pwmPins[pin] = NewPWMPin(c.sys, "/sys/class/pwm/pwmchip0", fn)
if err := c.pwmPins[pin].Export(); err != nil {
return nil, err
}
if err := c.pwmPins[pin].SetPeriod(pwmPeriodDefault); err != nil {
return nil, err
}
if err := c.pwmPins[pin].SetEnabled(true); err != nil {
return nil, err
}
return c.pwmPins[pin], nil
}
func (c *Adaptor) validateSpiBusNumber(busNr int) error {
func (a *Adaptor) validateSpiBusNumber(busNr int) error {
// Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x.
// x is the chip number <255
if (busNr < 0) || (busNr > 1) {
@ -186,7 +143,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error {
return nil
}
func (c *Adaptor) validateI2cBusNumber(busNr int) error {
func (a *Adaptor) validateI2cBusNumber(busNr int) error {
// Valid bus number is [0..1] which corresponds to /dev/i2c-0 through /dev/i2c-1.
if (busNr < 0) || (busNr > 1) {
return fmt.Errorf("Bus number %d out of range", busNr)
@ -194,16 +151,16 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error {
return nil
}
func (c *Adaptor) translateDigitalPin(id string) (string, int, error) {
func (a *Adaptor) translateDigitalPin(id string) (string, int, error) {
if line, ok := gpioPins[id]; ok {
return "", line, nil
}
return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id)
}
func (c *Adaptor) translatePwmPin(pin string) (string, error) {
if fn, ok := pwmPins[pin]; ok {
return fn, nil
func (a *Adaptor) translatePWMPin(id string) (string, int, error) {
if channel, ok := pwmPins[id]; ok {
return "/sys/class/pwm/pwmchip0", channel, nil
}
return "", errors.New("Not a valid pin")
return "", 0, fmt.Errorf("'%s' is not a valid pin id for PWM on '%s'", id, a.name)
}

View File

@ -66,15 +66,12 @@ func TestFinalize(t *testing.T) {
func TestPWMPinsConnect(t *testing.T) {
a := NewAdaptor()
assert.Equal(t, (map[string]gobot.PWMPinner)(nil), a.pwmPins)
err := a.PwmWrite("33", 1)
require.ErrorContains(t, err, "not connected")
err = a.Connect()
require.NoError(t, err)
assert.NotEqual(t, (map[string]gobot.PWMPinner)(nil), a.pwmPins)
assert.Empty(t, a.pwmPins)
}
func TestPWMPinsReConnect(t *testing.T) {
@ -84,18 +81,16 @@ func TestPWMPinsReConnect(t *testing.T) {
"/sys/class/pwm/pwmchip0/unexport",
"/sys/class/pwm/pwmchip0/pwm2/duty_cycle",
"/sys/class/pwm/pwmchip0/pwm2/period",
"/sys/class/pwm/pwmchip0/pwm2/polarity",
"/sys/class/pwm/pwmchip0/pwm2/enable",
}
a, _ := initTestAdaptorWithMockedFilesystem(mockPaths)
assert.Empty(t, a.pwmPins)
require.NoError(t, a.PwmWrite("33", 1))
assert.Len(t, a.pwmPins, 1)
require.NoError(t, a.Finalize())
// act
err := a.Connect()
// assert
require.NoError(t, err)
assert.Empty(t, a.pwmPins)
}
func TestDigitalIO(t *testing.T) {
@ -240,3 +235,35 @@ func Test_validateI2cBusNumber(t *testing.T) {
})
}
}
func Test_translatePWMPin(t *testing.T) {
tests := map[string]struct {
pin string
wantDir string
wantChannel int
wantErr error
}{
"32_pwm0": {
pin: "32",
wantDir: "/sys/class/pwm/pwmchip0",
wantChannel: 0,
},
"33_pwm2": {
pin: "33",
wantDir: "/sys/class/pwm/pwmchip0",
wantChannel: 2,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor()
// act
dir, channel, err := a.translatePWMPin(tc.pin)
// assert
assert.Equal(t, tc.wantErr, err)
assert.Equal(t, tc.wantDir, dir)
assert.Equal(t, tc.wantChannel, channel)
})
}
}

View File

@ -25,7 +25,8 @@ var gpioPins = map[string]int{
"40": 78,
}
var pwmPins = map[string]string{
"32": "0",
"33": "2",
// pin to pwm channel (pwm0, pwm2)
var pwmPins = map[string]int{
"32": 0,
"33": 2,
}

View File

@ -1,148 +0,0 @@
package jetson
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/system"
)
const (
minimumPeriod = 5334
minimumRate = 0.05
)
// PWMPin is the Jetson Nano implementation of the PWMPinner interface.
// It uses gpio pwm.
type PWMPin struct {
sys *system.Accesser
path string
fn string
dc uint32
period uint32
enabled bool
}
// NewPWMPin returns a new PWMPin
// pin32 pwm0, pin33 pwm2
func NewPWMPin(sys *system.Accesser, path string, fn string) *PWMPin {
p := &PWMPin{
sys: sys,
path: path,
fn: fn,
}
return p
}
// Export exports the pin for use by the Jetson Nano
func (p *PWMPin) Export() error {
return p.writeFile("export", p.fn)
}
// Unexport releases the pin from the operating system
func (p *PWMPin) Unexport() error {
return p.writeFile("unexport", p.fn)
}
// Enabled returns the cached enabled state of the PWM pin
func (p *PWMPin) Enabled() (bool, error) {
return p.enabled, nil
}
// SetEnabled enables/disables the PWM pin
func (p *PWMPin) SetEnabled(e bool) error {
if err := p.writeFile(fmt.Sprintf("pwm%s/enable", p.fn), strconv.Itoa(bool2int(e))); err != nil {
return err
}
p.enabled = e
return nil
}
// Polarity returns always the polarity "true" for normal
func (p *PWMPin) Polarity() (bool, error) {
return true, nil
}
// SetPolarity does not do anything when using Jetson Nano
func (p *PWMPin) SetPolarity(bool) error {
return nil
}
// Period returns the cached PWM period for pin
func (p *PWMPin) Period() (uint32, error) {
if p.period == 0 {
return p.period, errors.New("Jetson PWM pin period not set")
}
return p.period, nil
}
// SetPeriod uses Jetson Nano setting and cannot be changed once set
func (p *PWMPin) SetPeriod(period uint32) error {
if p.period != 0 {
return errors.New("Cannot set the period of individual PWM pins on Jetson")
}
// JetsonNano Minimum period
if period < minimumPeriod {
return errors.New("Cannot set the period more then minimum")
}
//nolint:perfsprint // ok here
if err := p.writeFile(fmt.Sprintf("pwm%s/period", p.fn), fmt.Sprintf("%v", period)); err != nil {
return err
}
p.period = period
return nil
}
// DutyCycle returns the cached duty cycle for the pin
func (p *PWMPin) DutyCycle() (uint32, error) {
return p.dc, nil
}
// SetDutyCycle writes the duty cycle to the pin
func (p *PWMPin) SetDutyCycle(duty uint32) error {
if p.period == 0 {
return errors.New("Jetson PWM pin period not set")
}
if duty > p.period {
return errors.New("Duty cycle exceeds period")
}
rate := gobot.FromScale(float64(duty), 0, float64(p.period))
// never go below minimum allowed duty because very short duty
if rate < minimumRate {
duty = uint32(minimumRate * float64(p.period) / 100)
}
//nolint:perfsprint // ok here
if err := p.writeFile(fmt.Sprintf("pwm%s/duty_cycle", p.fn), fmt.Sprintf("%v", duty)); err != nil {
return err
}
p.dc = duty
return nil
}
func (p *PWMPin) writeFile(subpath string, value string) error {
sysfspath := path.Join(p.path, subpath)
fi, err := p.sys.OpenFile(sysfspath, os.O_WRONLY|os.O_APPEND, 0o644)
defer fi.Close() //nolint:staticcheck // for historical reasons
if err != nil {
return err
}
_, err = fi.WriteString(value)
return err
}
func bool2int(b bool) int {
if b {
return 1
}
return 0
}

View File

@ -1,79 +0,0 @@
package jetson
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 = (*PWMPin)(nil)
func TestPwmPin(t *testing.T) {
a := system.NewAccesser()
const (
exportPath = "/sys/class/pwm/pwmchip0/export"
unexportPath = "/sys/class/pwm/pwmchip0/unexport"
enablePath = "/sys/class/pwm/pwmchip0/pwm3/enable"
periodPath = "/sys/class/pwm/pwmchip0/pwm3/period"
dutyCyclePath = "/sys/class/pwm/pwmchip0/pwm3/duty_cycle"
)
mockPaths := []string{
exportPath,
unexportPath,
enablePath,
periodPath,
dutyCyclePath,
}
fs := a.UseMockFilesystem(mockPaths)
pin := NewPWMPin(a, "/sys/class/pwm/pwmchip0", "3")
require.Equal(t, "", fs.Files[exportPath].Contents)
require.Equal(t, "", fs.Files[unexportPath].Contents)
require.Equal(t, "", fs.Files[enablePath].Contents)
require.Equal(t, "", fs.Files[periodPath].Contents)
require.Equal(t, "", fs.Files[dutyCyclePath].Contents)
require.NoError(t, pin.Export())
assert.Equal(t, "3", fs.Files[exportPath].Contents)
require.NoError(t, pin.SetEnabled(true))
assert.Equal(t, "1", fs.Files[enablePath].Contents)
val, _ := pin.Polarity()
assert.True(t, val)
require.NoError(t, pin.SetPolarity(false))
val, _ = pin.Polarity()
assert.True(t, val)
_, err := pin.Period()
require.ErrorContains(t, err, "Jetson PWM pin period not set")
require.ErrorContains(t, pin.SetDutyCycle(10000), "Jetson PWM pin period not set")
assert.Equal(t, "", fs.Files[dutyCyclePath].Contents)
require.NoError(t, pin.SetPeriod(20000000))
assert.Equal(t, "20000000", fs.Files[periodPath].Contents)
period, _ := pin.Period()
assert.Equal(t, uint32(20000000), period)
require.ErrorContains(t, pin.SetPeriod(10000000), "Cannot set the period of individual PWM pins on Jetson")
assert.Equal(t, "20000000", fs.Files[periodPath].Contents)
dc, _ := pin.DutyCycle()
assert.Equal(t, uint32(0), dc)
require.NoError(t, pin.SetDutyCycle(10000))
assert.Equal(t, "10000", fs.Files[dutyCyclePath].Contents)
dc, _ = pin.DutyCycle()
assert.Equal(t, uint32(10000), dc)
require.ErrorContains(t, pin.SetDutyCycle(999999999), "Duty cycle exceeds period")
dc, _ = pin.DutyCycle()
assert.Equal(t, "10000", fs.Files[dutyCyclePath].Contents)
assert.Equal(t, uint32(10000), dc)
require.NoError(t, pin.Unexport())
assert.Equal(t, "3", fs.Files[unexportPath].Contents)
}

View File

@ -12,8 +12,6 @@ import (
)
const (
pwmInvertedIdentifier = "inversed"
defaultI2cBusNumber = 0
defaultSpiBusNumber = 0
@ -83,7 +81,7 @@ func NewNeoAdaptor(opts ...interface{}) *Adaptor {
}
var digitalPinsOpts []func(adaptors.DigitalPinsOptioner)
pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)}
var pwmPinsOpts []adaptors.PwmPinsOptionApplier
for _, opt := range opts {
switch o := opt.(type) {
case func(adaptors.DigitalPinsOptioner):

View File

@ -31,6 +31,8 @@ const (
pwmPeriodPath = pwmPwmDir + "period"
pwmDutyCyclePath = pwmPwmDir + "duty_cycle"
pwmPolarityPath = pwmPwmDir + "polarity"
pwmInvertedIdentifier = "inversed"
)
var pwmMockPaths = []string{

View File

@ -51,9 +51,7 @@ func main() {
}
```
## How to Connect
### Compiling
## Compiling
Compile your Gobot program on your workstation like this:
@ -74,9 +72,48 @@ scp raspi_blink pi@192.168.1.xxx:/home/pi/
ssh -t pi@192.168.1.xxx "./raspi_blink"
```
### Enabling PWM output on GPIO pins
## Enabling PWM output on GPIO pins
For extended PWM support on the Raspberry Pi, you will need to use a program called pi-blaster. You can follow the
instructions for pi-blaster install in the pi-blaster repo here:
### Using Linux Kernel sysfs implementation
[https://github.com/sarfata/pi-blaster](https://github.com/sarfata/pi-blaster)
The PWM needs to be enabled in the device tree. Please read `/boot/overlays/README` of your device. Usually "pwm0" can
be activated for all raspi variants with a line `dtoverlay=pwm,pin=18,func=2` added to `/boot/config.txt`. The number
relates to "GPIO18", not the header number, which is "12" in this case.
Now the pin can be used with gobot by the pwm channel name, e.g. for our example above:
```go
...
// create the adaptor with a 50Hz default frequency for usage with servos
a := NewAdaptor(adaptors.WithPWMDefaultPeriod(20000000))
// move servo connected with header pin 12 to 90°
a.ServoWrite("pwm0", 90)
...
```
> If the activation fails or something strange happen, maybe the audio driver conflicts with the PWM. Please deactivate
> the audio device tree overlay in `/boot/config.txt` to avoid conflicts.
### Using pi-blaster
For support PWM on all pins, you may use a program called pi-blaster. You can follow the instructions for install in
the pi-blaster repo here: <https://github.com/sarfata/pi-blaster>
For using a PWM for servo, the default 100Hz period needs to be adjusted to 50Hz in the source code of the driver.
Please refer to <https://github.com/sarfata/pi-blaster#how-to-adjust-the-frequency-and-the-resolution-of-the-pwm>.
It is not possible to change the period from gobot side.
Now the pin can be used with gobot by the header number, e.g.:
```go
...
// create the adaptor with usage of pi-blaster instead of default sysfs, 50Hz default is given for calculate
// duty cycle for servos but will not change everything for the pi-blaster driver, see description above
a := NewAdaptor(adaptors.WithPWMUsePiBlaster(), adaptors.WithPWMDefaultPeriod(20000000))
// move servo to 90°
a.ServoWrite("11", 90)
// this will not work like expected, see description
a.SetPeriod("11", 20000000)
...
```

View File

@ -1,119 +0,0 @@
package raspi
import (
"errors"
"fmt"
"os"
"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/system"
)
// PWMPin is the Raspberry Pi implementation of the PWMPinner interface.
// It uses Pi Blaster.
type PWMPin struct {
sys *system.Accesser
path string
pin string
dc uint32
period uint32
}
// NewPWMPin returns a new PWMPin
func NewPWMPin(sys *system.Accesser, path string, pin string) *PWMPin {
return &PWMPin{
sys: sys,
path: path,
pin: pin,
}
}
// Export exports the pin for use by the Raspberry Pi
func (p *PWMPin) Export() error {
return nil
}
// Unexport releases the pin from the operating system
func (p *PWMPin) Unexport() error {
return p.writeValue(fmt.Sprintf("release %v\n", p.pin))
}
// Enabled returns always true for "enabled"
func (p *PWMPin) Enabled() (bool, error) {
return true, nil
}
// SetEnabled do nothing for PiBlaster
func (p *PWMPin) SetEnabled(e bool) error {
return nil
}
// Polarity returns always true for "normal"
func (p *PWMPin) Polarity() (bool, error) {
return true, nil
}
// SetPolarity does not do anything when using PiBlaster
func (p *PWMPin) SetPolarity(bool) error {
return nil
}
// Period returns the cached PWM period for pin
func (p *PWMPin) Period() (uint32, error) {
if p.period == 0 {
return p.period, errors.New("Raspi PWM pin period not set")
}
return p.period, nil
}
// SetPeriod uses PiBlaster setting and cannot be changed once set
func (p *PWMPin) SetPeriod(period uint32) error {
if p.period != 0 {
return errors.New("Cannot set the period of individual PWM pins on Raspi")
}
p.period = period
return nil
}
// DutyCycle returns the duty cycle for the pin
func (p *PWMPin) DutyCycle() (uint32, error) {
return p.dc, nil
}
// SetDutyCycle writes the duty cycle to the pin
func (p *PWMPin) SetDutyCycle(duty uint32) error {
if p.period == 0 {
return errors.New("Raspi PWM pin period not set")
}
if duty > p.period {
return errors.New("Duty cycle exceeds period")
}
val := gobot.FromScale(float64(duty), 0, float64(p.period))
// never go below minimum allowed duty for pi blaster
// unless the duty equals to 0
if val < 0.05 && val != 0 {
val = 0.05
}
if err := p.writeValue(fmt.Sprintf("%v=%v\n", p.pin, val)); err != nil {
return err
}
p.dc = duty
return nil
}
func (p *PWMPin) writeValue(data string) error {
fi, err := p.sys.OpenFile(p.path, 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
}

View File

@ -1,55 +0,0 @@
package raspi
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 = (*PWMPin)(nil)
func TestPwmPin(t *testing.T) {
const path = "/dev/pi-blaster"
a := system.NewAccesser()
a.UseMockFilesystem([]string{path})
pin := NewPWMPin(a, path, "1")
require.NoError(t, pin.Export())
require.NoError(t, pin.SetEnabled(true))
val, _ := pin.Polarity()
assert.True(t, val)
require.NoError(t, pin.SetPolarity(false))
val, _ = pin.Polarity()
assert.True(t, val)
_, err := pin.Period()
require.ErrorContains(t, err, "Raspi PWM pin period not set")
require.ErrorContains(t, pin.SetDutyCycle(10000), "Raspi PWM pin period not set")
require.NoError(t, pin.SetPeriod(20000000))
period, _ := pin.Period()
assert.Equal(t, uint32(20000000), period)
require.ErrorContains(t, pin.SetPeriod(10000000), "Cannot set the period of individual PWM pins on Raspi")
dc, _ := pin.DutyCycle()
assert.Equal(t, uint32(0), dc)
require.NoError(t, pin.SetDutyCycle(10000))
dc, _ = pin.DutyCycle()
assert.Equal(t, uint32(10000), dc)
require.ErrorContains(t, pin.SetDutyCycle(999999999), "Duty cycle exceeds period")
dc, _ = pin.DutyCycle()
assert.Equal(t, uint32(10000), dc)
require.NoError(t, pin.Unexport())
}

View File

@ -1,7 +1,6 @@
package raspi
import (
"errors"
"fmt"
"strconv"
"strings"
@ -37,12 +36,11 @@ type Adaptor struct {
mutex sync.Mutex
sys *system.Accesser
revision string
pwmPins map[string]gobot.PWMPinner
*adaptors.AnalogPinsAdaptor
*adaptors.DigitalPinsAdaptor
*adaptors.PWMPinsAdaptor
*adaptors.I2cBusAdaptor
*adaptors.SpiBusAdaptor
PiBlasterPeriod uint32
}
// NewAdaptor creates a Raspi Adaptor
@ -56,83 +54,95 @@ type Adaptor struct {
// adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior
// adaptors.WithGpioDebounce(pin, period): sets the input debouncer
// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection
func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor {
func NewAdaptor(opts ...interface{}) *Adaptor {
sys := system.NewAccesser(system.WithDigitalPinGpiodAccess())
c := &Adaptor{
name: gobot.DefaultName("RaspberryPi"),
sys: sys,
PiBlasterPeriod: 10000000,
a := &Adaptor{
name: gobot.DefaultName("RaspberryPi"),
sys: sys,
}
c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin)
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.getPinTranslatorFunction(), opts...)
c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, 1)
c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber,
var digitalPinsOpts []func(adaptors.DigitalPinsOptioner)
var pwmPinsOpts []adaptors.PwmPinsOptionApplier
for _, opt := range opts {
switch o := opt.(type) {
case func(adaptors.DigitalPinsOptioner):
digitalPinsOpts = append(digitalPinsOpts, o)
case adaptors.PwmPinsOptionApplier:
pwmPinsOpts = append(pwmPinsOpts, o)
default:
panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name))
}
}
a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin)
a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.getPinTranslatorFunction(), digitalPinsOpts...)
a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.getPinTranslatorFunction(), pwmPinsOpts...)
a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, 1)
a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber,
defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed)
return c
return a
}
// Name returns the Adaptor's name
func (c *Adaptor) Name() string {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Name() string {
a.mutex.Lock()
defer a.mutex.Unlock()
return c.name
return a.name
}
// SetName sets the Adaptor's name
func (c *Adaptor) SetName(n string) {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) SetName(n string) {
a.mutex.Lock()
defer a.mutex.Unlock()
c.name = n
a.name = n
}
// Connect create new connection to board and pins.
func (c *Adaptor) Connect() error {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Connect() error {
a.mutex.Lock()
defer a.mutex.Unlock()
if err := c.SpiBusAdaptor.Connect(); err != nil {
if err := a.SpiBusAdaptor.Connect(); err != nil {
return err
}
if err := c.I2cBusAdaptor.Connect(); err != nil {
if err := a.I2cBusAdaptor.Connect(); err != nil {
return err
}
if err := c.AnalogPinsAdaptor.Connect(); err != nil {
if err := a.AnalogPinsAdaptor.Connect(); err != nil {
return err
}
c.pwmPins = make(map[string]gobot.PWMPinner)
return c.DigitalPinsAdaptor.Connect()
if err := a.PWMPinsAdaptor.Connect(); err != nil {
return err
}
return a.DigitalPinsAdaptor.Connect()
}
// Finalize closes connection to board and pins
func (c *Adaptor) Finalize() error {
c.mutex.Lock()
defer c.mutex.Unlock()
func (a *Adaptor) Finalize() error {
a.mutex.Lock()
defer a.mutex.Unlock()
err := c.DigitalPinsAdaptor.Finalize()
err := a.DigitalPinsAdaptor.Finalize()
for _, pin := range c.pwmPins {
if pin != nil {
if perr := pin.Unexport(); err != nil {
err = multierror.Append(err, perr)
}
}
}
c.pwmPins = nil
if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
if e := a.PWMPinsAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := c.I2cBusAdaptor.Finalize(); e != nil {
if e := a.AnalogPinsAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := c.SpiBusAdaptor.Finalize(); e != nil {
if e := a.I2cBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := a.SpiBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
return err
@ -140,51 +150,15 @@ func (c *Adaptor) Finalize() error {
// DefaultI2cBus returns the default i2c bus for this platform.
// This overrides the base function due to the revision dependency.
func (c *Adaptor) DefaultI2cBus() int {
rev := c.readRevision()
func (a *Adaptor) DefaultI2cBus() int {
rev := a.readRevision()
if rev == "2" || rev == "3" {
return 1
}
return 0
}
// PWMPin returns a raspi.PWMPin which provides the gobot.PWMPinner interface
func (c *Adaptor) PWMPin(id string) (gobot.PWMPinner, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.pwmPin(id)
}
// PwmWrite writes a PWM signal to the specified pin
func (c *Adaptor) PwmWrite(pin string, val byte) error {
c.mutex.Lock()
defer c.mutex.Unlock()
sysPin, err := c.pwmPin(pin)
if err != nil {
return err
}
duty := uint32(gobot.FromScale(float64(val), 0, 255) * float64(c.PiBlasterPeriod))
return sysPin.SetDutyCycle(duty)
}
// ServoWrite writes a servo signal to the specified pin
func (c *Adaptor) ServoWrite(pin string, angle byte) error {
c.mutex.Lock()
defer c.mutex.Unlock()
sysPin, err := c.pwmPin(pin)
if err != nil {
return err
}
duty := uint32(gobot.FromScale(float64(angle), 0, 180) * float64(c.PiBlasterPeriod))
return sysPin.SetDutyCycle(duty)
}
func (c *Adaptor) validateSpiBusNumber(busNr int) error {
func (a *Adaptor) validateSpiBusNumber(busNr int) error {
// Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x.
// x is the chip number <255
if (busNr < 0) || (busNr > 1) {
@ -193,7 +167,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error {
return nil
}
func (c *Adaptor) validateI2cBusNumber(busNr int) error {
func (a *Adaptor) validateI2cBusNumber(busNr int) error {
// Valid bus number is [0..1] which corresponds to /dev/i2c-0 through /dev/i2c-1.
if (busNr < 0) || (busNr > 1) {
return fmt.Errorf("Bus number %d out of range", busNr)
@ -201,14 +175,14 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error {
return nil
}
func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) {
func (a *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) {
pinInfo, ok := analogPinDefinitions[id]
if !ok {
return "", false, false, 0, fmt.Errorf("'%s' is not a valid id for a analog pin", id)
}
path := pinInfo.path
info, err := c.sys.Stat(path)
info, err := a.sys.Stat(path)
if err != nil {
return "", false, false, 0, fmt.Errorf("Error (%v) on access '%s'", err, path)
}
@ -219,28 +193,37 @@ func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, err
return path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil
}
func (c *Adaptor) getPinTranslatorFunction() func(string) (string, int, error) {
// getPinTranslatorFunction returns a function to be able to translate GPIO and PWM pins.
// This means for pi-blaster usage, each pin can be used and therefore the pin is given as number, like a GPIO pin.
// For sysfs-PWM usage, the pin will be given as "pwm0" or "pwm1", because the real pin number depends on the user
// configuration in "/boot/config.txt". For further details, see "/boot/overlays/README".
func (a *Adaptor) getPinTranslatorFunction() func(string) (string, int, error) {
return func(pin string) (string, int, error) {
var line int
if val, ok := pins[pin][c.readRevision()]; ok {
if val, ok := pins[pin][a.readRevision()]; ok {
line = val
} else if val, ok := pins[pin]["*"]; ok {
line = val
} else {
return "", 0, errors.New("Not a valid pin")
return "", 0, fmt.Errorf("'%s' is not a valid pin id for raspi revision %s", pin, a.revision)
}
// TODO: Pi1 model B has only this single "gpiochip0", a change of the translator is needed,
// to support different chips with different revisions
return "gpiochip0", line, nil
// We always use "gpiochip0", because currently all pins are available with this approach. A change of the
// translator would be needed to support different chips (e.g. gpiochip1) with different revisions.
path := "gpiochip0"
if strings.HasPrefix(pin, "pwm") {
path = "/sys/class/pwm/pwmchip0"
}
return path, line, nil
}
}
func (c *Adaptor) readRevision() string {
if c.revision == "" {
c.revision = "0"
content, err := c.sys.ReadFile(infoFile)
func (a *Adaptor) readRevision() string {
if a.revision == "" {
a.revision = "0"
content, err := a.sys.ReadFile(infoFile)
if err != nil {
return c.revision
return a.revision
}
for _, v := range strings.Split(string(content), "\n") {
if strings.Contains(v, "Revision") {
@ -248,34 +231,15 @@ func (c *Adaptor) readRevision() string {
version, _ := strconv.ParseInt("0x"+s[len(s)-1], 0, 64)
switch {
case version <= 3:
c.revision = "1"
a.revision = "1"
case version <= 15:
c.revision = "2"
a.revision = "2"
default:
c.revision = "3"
a.revision = "3"
}
}
}
}
return c.revision
}
func (c *Adaptor) pwmPin(id string) (gobot.PWMPinner, error) {
pin := c.pwmPins[id]
if pin == nil {
tf := c.getPinTranslatorFunction()
_, i, err := tf(id)
if err != nil {
return nil, err
}
pin = NewPWMPin(c.sys, "/dev/pi-blaster", strconv.Itoa(i))
if err := pin.SetPeriod(c.PiBlasterPeriod); err != nil {
return nil, err
}
c.pwmPins[id] = pin
}
return pin, nil
return a.revision
}

View File

@ -16,9 +16,32 @@ import (
"gobot.io/x/gobot/v2/drivers/gpio"
"gobot.io/x/gobot/v2/drivers/i2c"
"gobot.io/x/gobot/v2/drivers/spi"
"gobot.io/x/gobot/v2/platforms/adaptors"
"gobot.io/x/gobot/v2/system"
)
const (
pwmDir = "/sys/class/pwm/pwmchip0/" //nolint:gosec // false positive
pwmPwmDir = pwmDir + "pwm0/"
pwmExportPath = pwmDir + "export"
pwmUnexportPath = pwmDir + "unexport"
pwmEnablePath = pwmPwmDir + "enable"
pwmPeriodPath = pwmPwmDir + "period"
pwmDutyCyclePath = pwmPwmDir + "duty_cycle"
pwmPolarityPath = pwmPwmDir + "polarity"
pwmInvertedIdentifier = "inversed"
)
var pwmMockPaths = []string{
pwmExportPath,
pwmUnexportPath,
pwmEnablePath,
pwmPeriodPath,
pwmDutyCyclePath,
pwmPolarityPath,
}
// make sure that this Adaptor fulfills all the required interfaces
var (
_ gobot.Adaptor = (*Adaptor)(nil)
@ -33,10 +56,19 @@ var (
_ spi.Connector = (*Adaptor)(nil)
)
func preparePwmFs(fs *system.MockFilesystem) {
fs.Files[pwmEnablePath].Contents = "0"
fs.Files[pwmPeriodPath].Contents = "0"
fs.Files[pwmDutyCyclePath].Contents = "0"
fs.Files[pwmPolarityPath].Contents = pwmInvertedIdentifier
}
func initTestAdaptorWithMockedFilesystem(mockPaths []string) (*Adaptor, *system.MockFilesystem) {
a := NewAdaptor()
fs := a.sys.UseMockFilesystem(mockPaths)
_ = a.Connect()
if err := a.Connect(); err != nil {
panic(err)
}
return a, fs
}
@ -133,35 +165,96 @@ func TestAnalog(t *testing.T) {
require.NoError(t, a.Finalize())
}
func TestDigitalPWM(t *testing.T) {
func TestPwmWrite(t *testing.T) {
// arrange
a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths)
preparePwmFs(fs)
// act
err := a.PwmWrite("pwm0", 100)
// assert
require.NoError(t, err)
assert.Equal(t, "0", fs.Files[pwmExportPath].Contents)
assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents)
assert.Equal(t, "10000000", fs.Files[pwmPeriodPath].Contents)
assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents)
assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents)
// act & assert invalid pin
err = a.PwmWrite("pwm1", 42)
require.ErrorContains(t, err, "'pwm1' is not a valid pin id for raspi revision 0")
require.NoError(t, a.Finalize())
}
func TestServoWrite(t *testing.T) {
// arrange: prepare 50Hz for servos
const (
pin = "pwm0"
fiftyHzNano = 20000000
)
a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano))
fs := a.sys.UseMockFilesystem(pwmMockPaths)
preparePwmFs(fs)
require.NoError(t, a.Connect())
// act & assert for 0° (min default value)
err := a.ServoWrite(pin, 0)
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents)
assert.Equal(t, "500000", fs.Files[pwmDutyCyclePath].Contents)
// act & assert for 180° (max default value)
err = a.ServoWrite(pin, 180)
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents)
assert.Equal(t, "2500000", fs.Files[pwmDutyCyclePath].Contents)
// act & assert invalid pins
err = a.ServoWrite("3", 120)
require.ErrorContains(t, err, "'3' is not a valid pin id for raspi revision 0")
require.NoError(t, a.Finalize())
}
func TestPWMWrite_piPlaster(t *testing.T) {
// arrange
const hundredHzNano = 10000000
mockedPaths := []string{"/dev/pi-blaster"}
a, fs := initTestAdaptorWithMockedFilesystem(mockedPaths)
a.PiBlasterPeriod = 20000000
require.NoError(t, a.PwmWrite("7", 4))
pin, _ := a.PWMPin("7")
period, _ := pin.Period()
assert.Equal(t, uint32(20000000), period)
a := NewAdaptor(adaptors.WithPWMUsePiBlaster())
fs := a.sys.UseMockFilesystem(mockedPaths)
require.NoError(t, a.Connect())
// act & assert: Write & Pin & Period
require.NoError(t, a.PwmWrite("7", 255))
assert.Equal(t, "4=1", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
require.NoError(t, a.ServoWrite("11", 90))
assert.Equal(t, "17=0.5", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
require.ErrorContains(t, a.PwmWrite("notexist", 1), "Not a valid pin")
require.ErrorContains(t, a.ServoWrite("notexist", 1), "Not a valid pin")
pin, _ := a.PWMPin("7")
period, err := pin.Period()
require.NoError(t, err)
assert.Equal(t, uint32(hundredHzNano), period)
// act & assert: nonexistent pin
require.ErrorContains(t, a.PwmWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 0")
// act & assert: SetDutyCycle
pin, _ = a.PWMPin("12")
period, _ = pin.Period()
assert.Equal(t, uint32(20000000), period)
require.NoError(t, pin.SetDutyCycle(1.5*1000*1000))
assert.Equal(t, "18=0.15", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
}
assert.Equal(t, "18=0.075", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
func TestPWM_piPlaster(t *testing.T) {
// arrange
const fiftyHzNano = 20000000 // 20 ms
mockedPaths := []string{"/dev/pi-blaster"}
a := NewAdaptor(adaptors.WithPWMUsePiBlaster(), adaptors.WithPWMDefaultPeriod(fiftyHzNano))
fs := a.sys.UseMockFilesystem(mockedPaths)
require.NoError(t, a.Connect())
// act & assert: Pin & Period
pin, _ := a.PWMPin("7")
period, err := pin.Period()
require.NoError(t, err)
assert.Equal(t, uint32(fiftyHzNano), period)
// act & assert for 180° (max default value), 2.5 ms => 12.5%
require.NoError(t, a.ServoWrite("11", 180))
assert.Equal(t, "17=0.125", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
// act & assert for 90° (center value), 1.5 ms => 7.5% duty
require.NoError(t, a.ServoWrite("11", 90))
assert.Equal(t, "17=0.075", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
// act & assert for 0° (min default value), 0.5 ms => 2.5% duty
require.NoError(t, a.ServoWrite("11", 0))
assert.Equal(t, "17=0.025", strings.Split(fs.Files["/dev/pi-blaster"].Contents, "\n")[0])
// act & assert: nonexistent pin
require.ErrorContains(t, a.ServoWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 0")
}
func TestDigitalIO(t *testing.T) {
@ -187,7 +280,7 @@ func TestDigitalIO(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 1, i)
require.ErrorContains(t, a.DigitalWrite("notexist", 1), "Not a valid pin")
require.ErrorContains(t, a.DigitalWrite("notexist", 1), "'notexist' is not a valid pin id for raspi revision 2")
require.NoError(t, a.Finalize())
}
@ -214,55 +307,6 @@ func TestDigitalPinConcurrency(t *testing.T) {
}
}
func TestPWMPin(t *testing.T) {
a := NewAdaptor()
if err := a.Connect(); err != nil {
panic(err)
}
assert.Empty(t, a.pwmPins)
a.revision = "3"
firstSysPin, err := a.PWMPin("35")
require.NoError(t, err)
assert.Len(t, a.pwmPins, 1)
secondSysPin, err := a.PWMPin("35")
require.NoError(t, err)
assert.Len(t, a.pwmPins, 1)
assert.Equal(t, secondSysPin, firstSysPin)
otherSysPin, err := a.PWMPin("36")
require.NoError(t, err)
assert.Len(t, a.pwmPins, 2)
assert.NotEqual(t, otherSysPin, firstSysPin)
}
func TestPWMPinsReConnect(t *testing.T) {
// arrange
a := NewAdaptor()
a.revision = "3"
if err := a.Connect(); err != nil {
panic(err)
}
_, err := a.PWMPin("35")
require.NoError(t, err)
assert.Len(t, a.pwmPins, 1)
require.NoError(t, a.Finalize())
// act
err = a.Connect()
// assert
require.NoError(t, err)
assert.Empty(t, a.pwmPins)
_, _ = a.PWMPin("35")
_, err = a.PWMPin("36")
require.NoError(t, err)
assert.Len(t, a.pwmPins, 2)
}
func TestSpiDefaultValues(t *testing.T) {
a := NewAdaptor()
@ -407,3 +451,73 @@ func Test_translateAnalogPin(t *testing.T) {
})
}
}
func Test_getPinTranslatorFunction(t *testing.T) {
tests := map[string]struct {
id string
revision string
wantPath string
wantLine int
wantErr string
}{
"translate_12_rev0": {
id: "12",
wantPath: "gpiochip0",
wantLine: 18,
},
"translate_13_rev0": {
id: "13",
wantErr: "'13' is not a valid pin id for raspi revision 0",
},
"translate_13_rev1": {
id: "13",
revision: "1",
wantPath: "gpiochip0",
wantLine: 21,
},
"translate_29_rev1": {
id: "29",
revision: "1",
wantErr: "'29' is not a valid pin id for raspi revision 1",
},
"translate_29_rev3": {
id: "29",
revision: "3",
wantPath: "gpiochip0",
wantLine: 5,
},
"translate_pwm0_rev0": {
id: "pwm0",
wantPath: "/sys/class/pwm/pwmchip0",
wantLine: 0,
},
"translate_pwm1_rev0": {
id: "pwm1",
wantErr: "'pwm1' is not a valid pin id for raspi revision 0",
},
"translate_pwm1_rev3": {
id: "pwm1",
revision: "3",
wantPath: "/sys/class/pwm/pwmchip0",
wantLine: 1,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor()
a.revision = tc.revision
// act
f := a.getPinTranslatorFunction()
path, line, err := f(tc.id)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantPath, path)
assert.Equal(t, tc.wantLine, line)
})
}
}

View File

@ -85,6 +85,12 @@ var pins = map[string]map[string]int{
"40": {
"3": 21,
},
"pwm0": { // pin 12 (GPIO18) and pin 32 (GPIO12) can be configured for "pwm0"
"*": 0,
},
"pwm1": { // pin 33 (GPIO13) and pin 35 (GPIO19) can be configured for "pwm1"
"3": 1,
},
}
var analogPinDefinitions = map[string]analogPinDefinition{

View File

@ -12,8 +12,6 @@ import (
)
const (
pwmInvertedIdentifier = "inversed"
defaultI2cBusNumber = 1
defaultSpiBusNumber = 0
@ -80,7 +78,7 @@ func NewAdaptor(opts ...interface{}) *Adaptor {
}
var digitalPinsOpts []func(adaptors.DigitalPinsOptioner)
pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)}
var pwmPinsOpts []adaptors.PwmPinsOptionApplier
for _, opt := range opts {
switch o := opt.(type) {
case func(adaptors.DigitalPinsOptioner):

View File

@ -31,6 +31,8 @@ const (
pwmPeriodPath = pwmPwmDir + "period"
pwmDutyCyclePath = pwmPwmDir + "duty_cycle"
pwmPolarityPath = pwmPwmDir + "polarity"
pwmInvertedIdentifier = "inversed"
)
var pwmMockPaths = []string{

View File

@ -14,7 +14,7 @@ var _ gobot.PWMPinner = (*pwmPinSysFs)(nil)
const (
normal = "normal"
inverted = "inverted"
inverted = "inversed"
)
func initTestPWMPinSysFsWithMockedFilesystem(mockPaths []string) (*pwmPinSysFs, *MockFilesystem) {