Skip to content

Commit

Permalink
Update PCA9685 driver to latest Adafruit CircuitPython driver
Browse files Browse the repository at this point in the history
- change pi and nano install to install the new driver.
- update pins.py to use new driver to provide output and pwm
  pins on a PCA9685 board.
- Change the PCA9685 actuator to be a subclass of PulseController,
  so it inherits the underlying pin provider and pulse controller
  api.  This elimnates the duplicate code in pins.py and actuator.py.
  • Loading branch information
Ezward committed May 13, 2024
1 parent 45dc516 commit d9d3660
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 85 deletions.
53 changes: 11 additions & 42 deletions donkeycar/parts/actuator.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,60 +125,29 @@ def run(self, pulse:int) -> None:


@deprecated("Deprecated in favor or PulseController. This will be removed in a future release")
class PCA9685:
class PCA9685(PulseController):
'''
PWM motor controler using PCA9685 boards.
This is used for most RC Cars
'''
def __init__(self, channel, address=0x40, frequency=60, busnum=None, init_delay=0.1):

self.default_freq = 60
self.pwm_scale = frequency / self.default_freq

import Adafruit_PCA9685
# Initialise the PCA9685 using the default address (0x40).
if busnum is not None:
from Adafruit_GPIO import I2C
# replace the get_bus function with our own
def get_bus():
return busnum
I2C.get_default_bus = get_bus
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
self.pwm.set_pwm_freq(frequency)
self.channel = channel
def __init__(self, channel, address=0x40, frequency=60, busnum=1, init_delay=0.1):
from donkeycar.parts.pins import pca9685pin
pca_pin = pca9685pin(channel, busnum, address, frequency)
super().__init__(self, pca_pin, frequency / 60, False)
self.pca_pin = pca_pin
time.sleep(init_delay) # "Tamiya TBLE-02" makes a little leap otherwise

def set_high(self):
self.pwm.set_pwm(self.channel, 4096, 0)
self.pca_pin.set_high()

def set_low(self):
self.pwm.set_pwm(self.channel, 0, 4096)
self.pca_pin.set_low()

def set_duty_cycle(self, duty_cycle):
def set_duty_cycle(self, duty_cycle: float):
if duty_cycle < 0 or duty_cycle > 1:
logging.error("duty_cycle must be in range 0 to 1")
logging.error("duty_cycle must be in range 0.0 to 1.0")
duty_cycle = clamp(duty_cycle, 0, 1)

if duty_cycle == 1:
self.set_high()
elif duty_cycle == 0:
self.set_low()
else:
# duty cycle is fraction of the 12 bits
pulse = int(4096 * duty_cycle)
try:
self.pwm.set_pwm(self.channel, 0, pulse)
except:
self.pwm.set_pwm(self.channel, 0, pulse)

def set_pulse(self, pulse):
try:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
except:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))

def run(self, pulse):
self.set_pulse(pulse)
self.pca_pin.set_duty_cycle(duty_cycle)


class VESC:
Expand Down
130 changes: 89 additions & 41 deletions donkeycar/parts/pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,61 +560,84 @@ def duty_cycle(self, duty: float) -> None:
#
# ----- PCA9685 implementations -----
#
class PCA9685:
class PCA9685board:
'''
Pin controller using PCA9685 boards.
This is used for most RC Cars. This
driver can output ttl HIGH or LOW or
produce a duty cycle at the given frequency.
Adapter over PCA9685 driver.
It initializes the PCA9685 board at the given busnum:address
to produce pulses at the given frequency in hertz.
>> NOTE: the I2C bus and address is auto detected using
>> Adafruit Blinka board interface, which is
>> implemented for RaspberryPi, but may not be
>> implemented for other SBCs.
>> See [boards.py](https://github.com/adafruit/Adafruit_Blinka/blob/main/src/board.py)
>> and [busio.py](https://github.com/adafruit/Adafruit_Blinka/blob/b3beba7399bc4d5faa203ef705691ef79fc4e87f/src/busio.py#L29)
'''
def __init__(self, busnum: int, address: int, frequency: int):
def __init__(self, busnum: int = 1, address: int = 0x40, frequency: int = 60):
import board
import busio
import adafruit_pca9685
i2c = busio.I2C(board.SCL, board.SDA)
self.pca9685driver = adafruit_pca9685.PCA9685(i2c, address=address)
self.pca9685driver.frequency(float(frequency))
self.busnum = busnum
self.address = address
self.frequency = frequency

def get_frequency(self) -> int:
return self._frequency

import Adafruit_PCA9685
if busnum is not None:
from Adafruit_GPIO import I2C
class PCA9685Pin:
'''
Adapter over PCA9685 pin driver.
This can output ttl HIGH or LOW or
produce a duty cycle at the given frequency.
'''
def __init__(self, channel: int, busnum: int = 1, address: int = 0x40, frequency: int = 60):

# monkey-patch I2C driver to use our bus number
def get_bus():
return busnum
import adafruit_pca9685
self.pca = pca9685(busnum, address, frequency)
self.pca_pin = adafruit_pca9685.PWMChannel(self.pca.pca9685driver, channel)
self.channel = channel

I2C.get_default_bus = get_bus
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
self.pwm.set_pwm_freq(frequency)
self._frequency = frequency
def get_frequency(self) -> int:
return self.pca.get_frequency()

def get_frequency(self):
return self._frequency
def set_high(self):
# adafruit blinka uses 16bit values
# where 0xFFFF is a flag that means fully high
self.pca_pin.duty_cycle(0xFFFF)

def set_high(self, channel: int):
self.pwm.set_pwm(channel, 4096, 0)
def set_low(self):
# adafruit blinka uses 16bit values
# where 0x0000 is a flag that means fully low
self.pca_pin.duty_cycle(0x0000)

def set_low(self, channel: int):
self.pwm.set_pwm(channel, 0, 4096)

def set_duty_cycle(self, channel: int, duty_cycle: float):
def set_duty_cycle(self, duty_cycle: float):
if duty_cycle < 0 or duty_cycle > 1:
raise ValueError("duty_cycle must be in range 0 to 1")
if duty_cycle == 1:
self.set_high(channel)
self.set_high(self.channel)
elif duty_cycle == 0:
self.set_low(channel)
self.set_low(self.channel)
else:
# duty cycle is fraction of the 12 bits
pulse = int(4096 * duty_cycle)
# adafruit blinka uses 16 bit values
# for resolution of duty cycle.
pulse = int(0x10000 * duty_cycle)
try:
self.pwm.set_pwm(channel, 0, pulse)
self.pca_pin.duty_cycle(pulse)
except Exception as e:
logger.error(f'Error on PCA9685 channel {channel}: {str(e)}')
logger.error(f'Error on PCA9685 channel {self.channel}: {str(e)}')


#
# lookup map for PCA9685 singletons
# key is "busnum:address"
#
_pca9685 = {}
_pca9685pin = {}


def pca9685(busnum: int, address: int, frequency: int = 60):
def pca9685(busnum: int, address: int, frequency: int = 60) -> PCA9685board:
"""
pca9685 factory allocates driver for pca9685
at given bus number and i2c address.
Expand All @@ -632,21 +655,46 @@ def pca9685(busnum: int, address: int, frequency: int = 60):
key = str(busnum) + ":" + hex(address)
pca = _pca9685.get(key)
if pca is None:
pca = PCA9685(busnum, address, frequency)
pca = PCA9685board(busnum, address, frequency)
_pca9685[key] = pca
if pca.get_frequency() != frequency:
raise ValueError(
f"Frequency {frequency} conflicts with pca9685 at {key} "
f"with frequency {pca.pwm.get_pwm_freq()}")
f"with frequency {pca.get_frequency()}")
return pca


class OutputPinPCA9685(ABC):
def pca9685pin(channel: int, busnum: int, address: int, frequency: int = 60) -> PCA9685Pin:
"""
pca9685 pin factory allocates driver for pin on channel on pca9685
at given bus number and i2c address.
If we have already created one for that bus/addr/channel
pair then use that singleton. If frequency is
not the same, then error.
:param channel: PCA9685 channel 0..15 to control
:param busnum: I2C bus number of PCA9685
:param address: address of PCA9685 on I2C bus
:param frequency: frequency in hertz of duty cycle
:except: PCA9685 has a single frequency for all channels,
so attempts to allocate a controller at a
given bus number and address with different
frequencies will raise a ValueError
"""
key = str(busnum) + ":" + hex(address) + ":" + str(channel)
pca_pin = _pca9685pin.get(key)
if pca_pin is None:
pca_pin = PCA9685Pin(channel, busnum, address, frequency)
_pca9685pin[key] = pca_pin
return pca_pin


class OutputPinPCA9685(OutputPin):
"""
Output pin ttl HIGH/LOW using PCA9685
"""
def __init__(self, pin_number: int, pca9685: PCA9685) -> None:
def __init__(self, pin_number: int, busnum: int, address: int, frequency: int = 60) -> None:
self.pin_number = pin_number
self.pca9685 = pca9685
self.pca_pin = pca9685pin(pin_number, busnum, address, frequency)
self._state = PinState.NOT_STARTED

def start(self, state: int = PinState.LOW) -> None:
Expand Down Expand Up @@ -688,19 +736,19 @@ def output(self, state: int) -> None:
if self.state() == PinState.NOT_STARTED:
raise RuntimeError(f"Attempt to use pin ({self.pin_number}) that is not started")
if state == PinState.HIGH:
self.pca9685.set_high(self.pin_number)
self.pca_pin.set_high()
else:
self.pca9685.set_low(self.pin_number)
self.pca_pin.set_low()
self._state = state


class PwmPinPCA9685(PwmPin):
"""
PWM output pin using PCA9685
"""
def __init__(self, pin_number: int, pca9685: PCA9685) -> None:
def __init__(self, pin_number: int, busnum: int, address: int, frequency: int = 60) -> None:
self.pin_number = pin_number
self.pca9685 = pca9685
self.pca_pin = pca9685pin(pin_number, busnum, address, frequency)
self._state = PinState.NOT_STARTED

def start(self, duty: float = 0) -> None:
Expand Down Expand Up @@ -739,7 +787,7 @@ def duty_cycle(self, duty: float) -> None:
raise RuntimeError(f"Attempt to use pin ({self.pin_number}) that is not started")
if duty < 0 or duty > 1:
raise ValueError("duty_cycle must be in range 0 to 1")
self.pca9685.set_duty_cycle(self.pin_number, duty)
self.pca_pin.set_duty_cycle(duty)
self._state = duty


Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ install_requires =
[options.extras_require]
pi =
picamera2
Adafruit_PCA9685
adafruit-circuitpython-pca9685
adafruit-circuitpython-ssd1306
adafruit-circuitpython-rplidar
RPi.GPIO
Expand All @@ -60,7 +60,7 @@ pi =
albumentations

nano =
Adafruit_PCA9685
adafruit-circuitpython-pca9685
adafruit-circuitpython-ssd1306
adafruit-circuitpython-rplidar
Jetson.GPIO
Expand Down

0 comments on commit d9d3660

Please sign in to comment.