From 36bdd0caf4a39caa628152522e420b70e5d93f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Grill?= Date: Tue, 12 May 2020 22:08:23 +0200 Subject: [PATCH] pca9685: Added Full-on/off support, fixed scaling to 12 bits (#443) * added sanity check to channel ID arguments * Extracted init sequence in tests * Fixed max duty constant --- experimental/devices/pca9685/pca9685.go | 44 ++++++ experimental/devices/pca9685/pca9685_test.go | 144 ++++++++++++------- experimental/devices/pca9685/pins.go | 38 ++++- 3 files changed, 168 insertions(+), 58 deletions(-) diff --git a/experimental/devices/pca9685/pca9685.go b/experimental/devices/pca9685/pca9685.go index b4bc2d5..a95f323 100644 --- a/experimental/devices/pca9685/pca9685.go +++ b/experimental/devices/pca9685/pca9685.go @@ -5,6 +5,7 @@ package pca9685 import ( + "fmt" "time" "periph.io/x/periph/conn/gpio" @@ -143,5 +144,48 @@ func (d *Dev) SetAllPwm(on, off gpio.Duty) error { // SetPwm set a PWM value for a given PCA9685 channel. func (d *Dev) SetPwm(channel int, on, off gpio.Duty) error { + err := verifyChannel(channel) + if err != nil { + return err + } return d.setPWM(led0OnL+byte(4*channel), on, off) } + +// SetFullOff sets PWM duty to 0%. +// +// This function uses the dedicated bit to reduce bus traffic. +func (d *Dev) SetFullOff(channel int) error { + err := verifyChannel(channel) + if err != nil { + return err + } + _, err = d.dev.Write([]byte{ + led0OnL + byte(4*channel) + 3, // LEDX_OFF_H + 0x10, // bit 4 is full-off + }) + return err +} + +// SetFullOn sets PWM duty to 100%. +// +// This function uses the dedicated FULL_ON bit. +func (d *Dev) SetFullOn(channel int) error { + err := verifyChannel(channel) + if err != nil { + return err + } + _, err = d.dev.Write([]byte{ + led0OnL + byte(4*channel) + 1, // LEDX_ON_H + 0x10, // bit 4 is full-on + 0, + 0, // LEDX_OFF_H is cleared because full-off has a priority over full-on + }) + return err +} + +func verifyChannel(channel int) error { + if channel < 0 || channel > 15 { + return fmt.Errorf("PCA9685: invalid channel: %d", channel) + } + return nil +} diff --git a/experimental/devices/pca9685/pca9685_test.go b/experimental/devices/pca9685/pca9685_test.go index e75ccfd..aecf8fd 100644 --- a/experimental/devices/pca9685/pca9685_test.go +++ b/experimental/devices/pca9685/pca9685_test.go @@ -13,34 +13,38 @@ import ( "periph.io/x/periph/conn/physic" ) +func initializationSequence() []i2ctest.IO { + return []i2ctest.IO{ + // All leds cleared by init + {Addr: I2CAddr, W: []byte{allLedOnL, 0, 0, 0, 0}, R: nil}, + // mode2 is set + {Addr: I2CAddr, W: []byte{mode2, outDrv}, R: nil}, + // mode1 is set + {Addr: I2CAddr, W: []byte{mode1, allCall}, R: nil}, + // mode1 is read and sleep bit is cleared + {Addr: I2CAddr, W: []byte{mode1}, R: []byte{allCall | sleep}}, + {Addr: I2CAddr, W: []byte{mode1, allCall | ai}, R: nil}, + + // SetPwmFreq 50 Hz + // Read mode + {Addr: I2CAddr, W: []byte{0x00}, R: []byte{allCall | ai}}, + // Set sleep + {Addr: I2CAddr, W: []byte{0x00, allCall | ai | sleep}, R: nil}, + // Set prescale + {Addr: I2CAddr, W: []byte{prescale, 122}, R: nil}, + // Clear sleep + {Addr: I2CAddr, W: []byte{0x00, allCall | ai}, R: nil}, + // Set Restart + {Addr: I2CAddr, W: []byte{0x00, allCall | ai | restart}, R: nil}, + } +} + func TestPCA9685_pin(t *testing.T) { scenario := &i2ctest.Playback{ - Ops: []i2ctest.IO{ - // All leds cleared by init - {Addr: I2CAddr, W: []byte{allLedOnL, 0, 0, 0, 0}, R: nil}, - // mode2 is set - {Addr: I2CAddr, W: []byte{mode2, outDrv}, R: nil}, - // mode1 is set - {Addr: I2CAddr, W: []byte{mode1, allCall}, R: nil}, - // mode1 is read and sleep bit is cleared - {Addr: I2CAddr, W: []byte{mode1}, R: []byte{allCall | sleep}}, - {Addr: I2CAddr, W: []byte{mode1, allCall | ai}, R: nil}, - - // SetPwmFreq 50 Hz - // Read mode - {Addr: I2CAddr, W: []byte{0x00}, R: []byte{allCall | ai}}, - // Set sleep - {Addr: I2CAddr, W: []byte{0x00, allCall | ai | sleep}, R: nil}, - // Set prescale - {Addr: I2CAddr, W: []byte{prescale, 122}, R: nil}, - // Clear sleep - {Addr: I2CAddr, W: []byte{0x00, allCall | ai}, R: nil}, - // Set Restart - {Addr: I2CAddr, W: []byte{0x00, allCall | ai | restart}, R: nil}, - + Ops: append(initializationSequence(), // Set PWM value of pin 0 to 50% - {Addr: I2CAddr, W: []byte{led0OnL, 0, 0, 0, 128}, R: nil}, - }, + i2ctest.IO{Addr: I2CAddr, W: []byte{led0OnL, 0, 0, 0, 0x08}, R: nil}, + ), } dev, err := NewI2C(scenario, I2CAddr) @@ -51,39 +55,62 @@ func TestPCA9685_pin(t *testing.T) { if err = dev.RegisterPins(); err != nil { t.Fatal(err) } + defer dev.UnregisterPins() pin := gpioreg.ByName("PCA9685_40_0") pin.PWM(gpio.DutyHalf, 50*physic.Hertz) } -func TestPCA9685(t *testing.T) { +func TestPCA9685_pin_fullOff(t *testing.T) { + scenario := &i2ctest.Playback{ + Ops: append(initializationSequence(), + // Set PWM value of pin 0 to 0% + i2ctest.IO{Addr: I2CAddr, W: []byte{led0OnL + 3, 0x10}, R: nil}, + ), + } + + dev, err := NewI2C(scenario, I2CAddr) + if err != nil { + t.Fatal(err) + } + + if err = dev.RegisterPins(); err != nil { + t.Fatal(err) + } + defer dev.UnregisterPins() + + pin := gpioreg.ByName("PCA9685_40_0") + pin.PWM(0, 50*physic.Hertz) +} + +func TestPCA9685_pin_fullOn(t *testing.T) { scenario := &i2ctest.Playback{ - Ops: []i2ctest.IO{ - // All leds cleared by init - {Addr: I2CAddr, W: []byte{allLedOnL, 0, 0, 0, 0}, R: nil}, - // mode2 is set - {Addr: I2CAddr, W: []byte{mode2, outDrv}, R: nil}, - // mode1 is set - {Addr: I2CAddr, W: []byte{mode1, allCall}, R: nil}, - // mode1 is read and sleep bit is cleared - {Addr: I2CAddr, W: []byte{mode1}, R: []byte{allCall | sleep}}, - {Addr: I2CAddr, W: []byte{mode1, allCall | ai}, R: nil}, - - // SetPwmFreq 50 Hz - // Read mode - {Addr: I2CAddr, W: []byte{0x00}, R: []byte{allCall | ai}}, - // Set sleep - {Addr: I2CAddr, W: []byte{0x00, allCall | ai | sleep}, R: nil}, - // Set prescale - {Addr: I2CAddr, W: []byte{prescale, 122}, R: nil}, - // Clear sleep - {Addr: I2CAddr, W: []byte{0x00, allCall | ai}, R: nil}, - // Set Restart - {Addr: I2CAddr, W: []byte{0x00, allCall | ai | restart}, R: nil}, + Ops: append(initializationSequence(), + // Set PWM value of pin 0 to 100% + i2ctest.IO{Addr: I2CAddr, W: []byte{led0OnL + 1, 0x10, 0, 0}, R: nil}, + ), + } + dev, err := NewI2C(scenario, I2CAddr) + if err != nil { + t.Fatal(err) + } + + if err = dev.RegisterPins(); err != nil { + t.Fatal(err) + } + defer dev.UnregisterPins() + + pin := gpioreg.ByName("PCA9685_40_0") + pin.PWM(gpio.DutyMax, 50*physic.Hertz) +} + +func TestPCA9685(t *testing.T) { + scenario := &i2ctest.Playback{ + Ops: append(initializationSequence(), // Set PWM value of pin 0 to 50% - {Addr: I2CAddr, W: []byte{led0OnL, 0, 0, 0, 128}, R: nil}, - }, + i2ctest.IO{Addr: I2CAddr, W: []byte{led0OnL, 0, 0, 0, 0x08}, R: nil}, + ), } dev, err := NewI2C(scenario, I2CAddr) @@ -91,7 +118,22 @@ func TestPCA9685(t *testing.T) { t.Fatal(err) } - if err = dev.SetPwm(0, 0, 0x8000); err != nil { + if err = dev.SetPwm(0, 0, 0x800); err != nil { t.Fatal(err) } } + +func TestPCA9685_invalidCh(t *testing.T) { + scenario := &i2ctest.Playback{ + Ops: append(initializationSequence()), + } + + dev, err := NewI2C(scenario, I2CAddr) + if err != nil { + t.Fatal(err) + } + + if err = dev.SetPwm(16, 0, 0x800); err == nil { + t.Fatal("Error expected") + } +} diff --git a/experimental/devices/pca9685/pins.go b/experimental/devices/pca9685/pins.go index a74eb29..6b80612 100644 --- a/experimental/devices/pca9685/pins.go +++ b/experimental/devices/pca9685/pins.go @@ -7,7 +7,6 @@ package pca9685 import ( "errors" "fmt" - "math" "time" "periph.io/x/periph/conn/gpio" @@ -17,7 +16,7 @@ import ( ) const ( - dutyMax gpio.Duty = math.MaxUint16 + dutyMax gpio.Duty = 1<<12 - 1 ) type pin struct { @@ -27,8 +26,9 @@ type pin struct { // CreatePin creates a gpio handle for the given channel. func (d *Dev) CreatePin(channel int) (gpio.PinIO, error) { - if channel < 0 || channel >= 16 { - return nil, errors.New("PCA9685: Valid channel range is 0..15") + err := verifyChannel(channel) + if err != nil { + return nil, err } return &pin{ dev: d, @@ -52,6 +52,17 @@ func (d *Dev) RegisterPins() error { return nil } +// UnregisterPins remove all previously created pin handles of this device from the GPIO registry +func (d *Dev) UnregisterPins() error { + for i := 0; i < 16; i++ { + err := gpioreg.Unregister(d.pinName(i)) + if err != nil { + return err + } + } + return nil +} + func (p *pin) String() string { return p.Name() } @@ -60,8 +71,12 @@ func (p *pin) Halt() error { return p.Out(gpio.Low) } +func (d *Dev) pinName(channel int) string { + return fmt.Sprintf("PCA9685_%x_%d", d.dev.Addr, channel) +} + func (p *pin) Name() string { - return fmt.Sprintf("PCA9685_%x_%d", p.dev.dev.Addr, p.channel) + return p.dev.pinName(p.channel) } func (p *pin) Number() int { @@ -100,8 +115,17 @@ func (p *pin) PWM(duty gpio.Duty, freq physic.Frequency) error { if err := p.dev.SetPwmFreq(freq); err != nil { return err } - // PWM duty scaled down from 24 to 16 bits - scaled := duty >> 8 + + if duty == gpio.DutyMax { + return p.dev.SetFullOn(p.channel) + } + + if duty == 0 { + return p.dev.SetFullOff(p.channel) + } + + // PWM duty scaled down from 24 to 12 bits + scaled := duty >> 12 if scaled > dutyMax { scaled = dutyMax }