From b33c84ff7823a7a3aa1e5f5bdcc99596c210200b Mon Sep 17 00:00:00 2001 From: soypat Date: Sun, 21 Nov 2021 00:50:56 -0300 Subject: [PATCH] add PCA9685 driver --- examples/pca9685/main.go | 68 ++++++++++++ pca9685/errors.go | 8 ++ pca9685/io.go | 9 ++ pca9685/pca9685.go | 216 +++++++++++++++++++++++++++++++++++++++ pca9685/registers.go | 69 +++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 examples/pca9685/main.go create mode 100644 pca9685/errors.go create mode 100644 pca9685/io.go create mode 100644 pca9685/pca9685.go create mode 100644 pca9685/registers.go diff --git a/examples/pca9685/main.go b/examples/pca9685/main.go new file mode 100644 index 0000000..c47c038 --- /dev/null +++ b/examples/pca9685/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/pca9685" +) + +func main() { + const ( + // Default address on most breakout boards. + pcaAddr = 0x40 + ) + err := machine.I2C0.Configure(machine.I2CConfig{}) + if err != nil { + panic(err.Error()) + } + d := pca9685.New(machine.I2C0, 0x40) + err = d.IsConnected() + if err != nil { + panic(err.Error()) + } + err = d.Configure(pca9685.PWMConfig{Period: 1e9 / 200}) // 200Hz PWM + if err != nil { + panic(err.Error()) + } + + var value uint32 + step := d.Top() / 5 + for { + for value = 0; value <= d.Top(); value += step { + d.SetAll(value) + dc := 100 * value / d.Top() + println("set dc @", dc, "%") + time.Sleep(800 * time.Millisecond) + } + } +} + +// ScanI2CDev finds I2C devices on the bus and rreturns them inside +// a slice. If slice is nil then no devices were found. +func ScanI2CDev(bus machine.I2C) (addrs []uint8) { + var addr, count uint8 + var err error + w := []byte{1} + // Count devices in first scan + for addr = 1; addr < 127; addr++ { + err = bus.Tx(uint16(addr), w, nil) + if err == nil { + count++ + } + } + if count == 0 { + return nil + } + // Allocate slice and populate slice with addresses + addrs = make([]uint8, count) + count = 0 + for addr = 1; addr < 127; addr++ { + err = bus.Tx(uint16(addr), w, nil) + if err == nil && count < uint8(len(addrs)) { + addrs[count] = addr + count++ + } + } + return addrs +} diff --git a/pca9685/errors.go b/pca9685/errors.go new file mode 100644 index 0000000..3c546c7 --- /dev/null +++ b/pca9685/errors.go @@ -0,0 +1,8 @@ +package pca9685 + +import "errors" + +var ( + ErrInvalidMode1 = errors.New("pca9685: data read from MODE1 not valid") + ErrBadPeriod = errors.New("pca9685: period must be in range 1..25ms") +) diff --git a/pca9685/io.go b/pca9685/io.go new file mode 100644 index 0000000..798ae58 --- /dev/null +++ b/pca9685/io.go @@ -0,0 +1,9 @@ +package pca9685 + +func (d *Dev) readReg(reg uint8, data []byte) error { + return d.bus.ReadRegister(d.addr, reg, data) +} + +func (d *Dev) writeReg(reg uint8, data []byte) error { + return d.bus.WriteRegister(d.addr, reg, data) +} diff --git a/pca9685/pca9685.go b/pca9685/pca9685.go new file mode 100644 index 0000000..68b4e61 --- /dev/null +++ b/pca9685/pca9685.go @@ -0,0 +1,216 @@ +package pca9685 + +import ( + "encoding/binary" + "time" + + "tinygo.org/x/drivers" +) + +const ( + // Internal oscillator frequency. + oscclock = 25000000 + // Max value PWM value (on or off) can take. + maxtop = 1<<12 - 1 + milliseconds = 1_000_000 // units in nanoseconds +) + +// Dev is a handle to the PCA9685 device given an address +// (usually 0x47) and an i2c bus. +type Dev struct { + addr uint8 + bus drivers.I2C + buf [4]byte +} + +type PWMConfig struct { + Period uint64 +} + +// New creates a new instance of a PCA9685 device. It performs +// no IO on the i2c bus. +func New(bus drivers.I2C, addr uint8) Dev { + return Dev{ + bus: bus, + addr: addr, + } +} + +// Configure enables autoincrement, sets all PWM signals to logic low (Ground) +// and finally sets the Period. +func (d Dev) Configure(cfg PWMConfig) error { + err := d.SetAI(true) + if err != nil { + return err + } + d.SetAll(0) + d.SetDrive(true) + return d.SetPeriod(cfg.Period) +} + +// SetPeriod updates the period of this PWM integrated circuit in nanoseconds. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// In the equation above frequency is in Hertz. +// +// If you use a period of 0, a period that works well for LEDs will be picked. +// +// PCA9685 accepts frequencies inbetween [40..1000 Hz], +// or expressed as a period [1..25ms]. +func (d Dev) SetPeriod(period uint64) error { + const div = maxtop + 1 + if period == 0 { + period = 1 * milliseconds + } + if period > 25*milliseconds || period < 1*milliseconds { + return ErrBadPeriod + } + // Correct for overshoot in provided frequency: https://github.com/adafruit/Adafruit-PWM-Servo-Driver-Library/issues/11 + // Note: 0.96 was empirically determined to be closer. Should follow up to understand what is happening here. + freq := 96 * 1e9 / (100 * period) + prescale := byte(oscclock/(div*freq) - 1) + err := d.Sleep(true) // Enable sleep to write to PRESCALE register + if err != nil { + return err + } + d.buf[0] = prescale + err = d.writeReg(PRESCALE, d.buf[:1]) + if err != nil { + return err + } + return d.Sleep(false) +} + +// Top returns max value PWM can take. +func (d Dev) Top() uint32 { + return maxtop +} + +// Set sets the `on` value of a PWM channel in the range [0..15]. +// Example: +// d.Set(1, d.Top()/4) +// sets the dutycycle of second (LED1) channel to 25%. +func (d Dev) Set(channel uint8, on uint32) { + switch { + case on > maxtop: + panic("pca9685: value must be in range 0..4096") + case on == 0: + d.SetPhased(channel, 0, maxtop) + return + } + + d.SetPhased(channel, on, 0) +} + +// SetAll sets all PWM signals to a ON value. Equivalent of calling +// Dev.Set(pca9685.ALLLED, value) +func (d Dev) SetAll(on uint32) { + d.Set(ALLLED, on) +} + +// IsConnected returns error if read fails or if +// driver suspects device is not connected. +func (d Dev) IsConnected() error { + // Set data to the NOT of default MODE1 contents. + // If read is succesful then data will be modified + const notdefaultMODE1 = ^defaultMODE1Value + d.buf[0] = notdefaultMODE1 + + err := d.readReg(MODE1, d.buf[:1]) + if err != nil { + return err + } else if d.buf[0] == notdefaultMODE1 { + return ErrInvalidMode1 + } + return nil +} + +// SetAI enables or disables autoincrement feature on device. Useful for +// writing to many consecutive registers in one shot. +func (d Dev) SetAI(ai bool) error { + err := d.readReg(MODE1, d.buf[:1]) + if err != nil { + return err + } + if ai { + d.buf[0] |= AI + } else { + d.buf[0] &^= AI + } + err = d.writeReg(MODE1, d.buf[:1]) + return err +} + +// SetDrive configures PWM output connection in MODE2 register. +// false: The 16 LEDn outputs are configured with an open-drain structure. +// true: The 16 LEDn outputs are configured with a totem pole structure. +func (d Dev) SetDrive(outdrv bool) error { + err := d.readReg(MODE2, d.buf[:1]) + if err != nil { + return err + } + if outdrv { + d.buf[0] |= OUTDRV + } else { + d.buf[0] &^= OUTDRV + } + return d.writeReg(MODE2, d.buf[:1]) +} + +// Sleep sets/unsets SLEEP bit in MODE1. +// if sleepEnabled +// Stops PWM. Allows writing to PRE_SCALE register. +// else +// wakes PCA9685. Resumes PWM. +func (d Dev) Sleep(sleepEnabled bool) error { + err := d.readReg(MODE1, d.buf[:1]) + if err != nil { + return err + } + if sleepEnabled { + d.buf[0] |= SLEEP + return d.writeReg(MODE1, d.buf[:1]) + } + d.buf[0] &^= SLEEP + err = d.writeReg(MODE1, d.buf[:1]) + // It takes 500μs max for the oscillator to be up and running once SLEEP bit + // has been set to logic 0. Timings on LEDn outputs are not guaranteed if PWM + // control registers are accessed within the 500μs window. + // There is no start-up delay required when using the EXTCLK pin as the PWM clock. + time.Sleep(1000 * time.Microsecond) // Requested by datasheet. + return err +} + +// SetInverting inverts ALL PCA9685 PWMs. The channel argument merely implements PWM interface. +// +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (d Dev) SetInverting(_ uint8, inverting bool) error { + err := d.readReg(MODE2, d.buf[:1]) + if err != nil { + return err + } + if inverting { + d.buf[0] |= INVRT + } else { + d.buf[0] &^= INVRT + } + return d.writeReg(MODE2, d.buf[:1]) +} + +// SetPhased sets PWM on and off mark. +// The ON time, which is programmable, will be the time the LED output +// will be asserted and the OFF time, which is also programmable, will be +// the time when the LED output will be negated. +// In this way, the phase shift becomes completely programmable. +// The resolution for the phase shift is 1⁄4096 of the target frequency. +func (d Dev) SetPhased(channel uint8, on, off uint32) { + binary.LittleEndian.PutUint16(d.buf[:2], uint16(on)&maxtop) + binary.LittleEndian.PutUint16(d.buf[2:4], uint16(off)&maxtop) + onLReg, _, _, _ := LED(channel) + d.writeReg(onLReg, d.buf[:4]) +} diff --git a/pca9685/registers.go b/pca9685/registers.go new file mode 100644 index 0000000..2ec8e09 --- /dev/null +++ b/pca9685/registers.go @@ -0,0 +1,69 @@ +package pca9685 + +// Software reset addresses for generic i2c implementation +// and PCA specific SWRST. See section 7.6 +const ( + I2CSWRSTADR = 0x0 + PCA9685SWRSTBYTE = 0b0000_0110 // 0x06 +) + +// Registries with nomenclature as seen in manual +const ( + SUBADR1 uint8 = 0x2 + SUBADR2 uint8 = 0x3 + SUBADR3 uint8 = 0x4 + + MODE1 uint8 = 0x0 + MODE2 uint8 = 0x1 + ALLCALLADR uint8 = 0x05 + + SWRESET uint8 = 0b0000_0011 + + // Start of LED registries. corresponds to LED0_ON_L register. + // Use LED function to get registries of a specific PWM channel register. + LEDSTART = 0x06 +) + +// MODE1 +const ( + RESET byte = 0b1000_0000 + EXTCLK byte = 0b0100_0000 + AI byte = 0b0010_0000 + SLEEP byte = 0b0001_0000 + defaultMODE1Value byte = 0b0001_0001 +) + +// MODE2 +const ( + OUTDRV = 1 << 2 + INVRT = 1 << 4 +) + +// LED channels from 0-15. Returns 4 registries associated with +// the channel PWM signal. Channel 250 (0xFA) gives ALL_LED registers. +// The L suffix represents the 8 LSB of 12 bit timing, the H suffix represents +// the 4 MSB of the timing. This way you have 0-4095 timing options. ON or OFF +// will be a number between these two or will be simply ON or OFF (fifth bit of H registry). +// +// The SetPWM implementation in this library does not do phase-shifting so OFF will always +// happen at time stamp 0. ON will solely decide duty cycle. +func LED(ch uint8) (ONL, ONH, OFFL, OFFH uint8) { + switch { + case ch == ALLLED: + return ALLLED, ALLLED + 1, ALLLED + 2, ALLLED + 3 + + case ch > 15: + panic("PWM channel out of range [0-15]") + default: + } + // 4 registries per channel. Starts at 6 + onLReg := LEDSTART + 4*ch + return onLReg, onLReg + 1, onLReg + 2, onLReg + 3 +} + +const ( + // ALLLED is channel that selects registries to control all leds. Use with function LED + ALLLED = 0xfa + // PRESCALE Prescaling byte + PRESCALE byte = 0xFE +)