Skip to content

Commit 1bff13f

Browse files
committed
Update PCA9685 driver to latest Adafruit CircuitPython driver
- 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.
1 parent 45dc516 commit 1bff13f

File tree

3 files changed

+103
-85
lines changed

3 files changed

+103
-85
lines changed

donkeycar/parts/actuator.py

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -125,60 +125,29 @@ def run(self, pulse:int) -> None:
125125

126126

127127
@deprecated("Deprecated in favor or PulseController. This will be removed in a future release")
128-
class PCA9685:
128+
class PCA9685(PulseController):
129129
'''
130130
PWM motor controler using PCA9685 boards.
131131
This is used for most RC Cars
132132
'''
133-
def __init__(self, channel, address=0x40, frequency=60, busnum=None, init_delay=0.1):
134-
135-
self.default_freq = 60
136-
self.pwm_scale = frequency / self.default_freq
137-
138-
import Adafruit_PCA9685
139-
# Initialise the PCA9685 using the default address (0x40).
140-
if busnum is not None:
141-
from Adafruit_GPIO import I2C
142-
# replace the get_bus function with our own
143-
def get_bus():
144-
return busnum
145-
I2C.get_default_bus = get_bus
146-
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
147-
self.pwm.set_pwm_freq(frequency)
148-
self.channel = channel
133+
def __init__(self, channel, address=0x40, frequency=60, busnum=1, init_delay=0.1):
134+
from donkeycar.parts.pins import pca9685pin
135+
pca_pin = pca9685pin(channel, busnum, address, frequency)
136+
super().__init__(self, pca_pin, frequency / 60, False)
137+
self.pca_pin = pca_pin
149138
time.sleep(init_delay) # "Tamiya TBLE-02" makes a little leap otherwise
150139

151140
def set_high(self):
152-
self.pwm.set_pwm(self.channel, 4096, 0)
141+
self.pca_pin.set_high()
153142

154143
def set_low(self):
155-
self.pwm.set_pwm(self.channel, 0, 4096)
144+
self.pca_pin.set_low()
156145

157-
def set_duty_cycle(self, duty_cycle):
146+
def set_duty_cycle(self, duty_cycle: float):
158147
if duty_cycle < 0 or duty_cycle > 1:
159-
logging.error("duty_cycle must be in range 0 to 1")
148+
logging.error("duty_cycle must be in range 0.0 to 1.0")
160149
duty_cycle = clamp(duty_cycle, 0, 1)
161-
162-
if duty_cycle == 1:
163-
self.set_high()
164-
elif duty_cycle == 0:
165-
self.set_low()
166-
else:
167-
# duty cycle is fraction of the 12 bits
168-
pulse = int(4096 * duty_cycle)
169-
try:
170-
self.pwm.set_pwm(self.channel, 0, pulse)
171-
except:
172-
self.pwm.set_pwm(self.channel, 0, pulse)
173-
174-
def set_pulse(self, pulse):
175-
try:
176-
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
177-
except:
178-
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
179-
180-
def run(self, pulse):
181-
self.set_pulse(pulse)
150+
self.pca_pin.set_duty_cycle(duty_cycle)
182151

183152

184153
class VESC:

donkeycar/parts/pins.py

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def pwm_pin(
382382
if pin_provider == PinProvider.RPI_GPIO:
383383
return PwmPinGpio(pin_number, pin_scheme, frequency_hz)
384384
if pin_provider == PinProvider.PCA9685:
385-
return PwmPinPCA9685(pin_number, pca9685(i2c_bus, i2c_address, frequency_hz))
385+
return PwmPinPCA9685(pin_number, i2c_bus, i2c_address, frequency_hz)
386386
if pin_provider == PinProvider.PIGPIO:
387387
if pin_scheme != PinScheme.BCM:
388388
raise ValueError("Pin scheme must be PinScheme.BCM for PIGPIO")
@@ -560,61 +560,85 @@ def duty_cycle(self, duty: float) -> None:
560560
#
561561
# ----- PCA9685 implementations -----
562562
#
563-
class PCA9685:
563+
class PCA9685board:
564564
'''
565-
Pin controller using PCA9685 boards.
566-
This is used for most RC Cars. This
567-
driver can output ttl HIGH or LOW or
568-
produce a duty cycle at the given frequency.
565+
Adapter over PCA9685 driver.
566+
It initializes the PCA9685 board at the given busnum:address
567+
to produce pulses at the given frequency in hertz.
568+
>> NOTE: The busnum argument is now ignored.
569+
>> The I2C bus device is auto detected using
570+
>> Adafruit Blinka board interface, which is
571+
>> implemented for RaspberryPi, but may not be
572+
>> implemented for other SBCs.
573+
>> See [boards.py](https://github.com/adafruit/Adafruit_Blinka/blob/main/src/board.py)
574+
>> and [busio.py](https://github.com/adafruit/Adafruit_Blinka/blob/b3beba7399bc4d5faa203ef705691ef79fc4e87f/src/busio.py#L29)
569575
'''
570-
def __init__(self, busnum: int, address: int, frequency: int):
576+
def __init__(self, busnum: int = 1, address: int = 0x40, frequency: int = 60):
577+
import board
578+
import busio
579+
import adafruit_pca9685
580+
i2c = busio.I2C(board.SCL, board.SDA)
581+
self.pca9685driver = adafruit_pca9685.PCA9685(i2c, address=address)
582+
self.pca9685driver.frequency = frequency
583+
self.busnum = busnum
584+
self.address = address
585+
self._frequency = frequency
571586

572-
import Adafruit_PCA9685
573-
if busnum is not None:
574-
from Adafruit_GPIO import I2C
587+
def get_frequency(self) -> int:
588+
return self._frequency
575589

576-
# monkey-patch I2C driver to use our bus number
577-
def get_bus():
578-
return busnum
590+
class PCA9685Pin:
591+
'''
592+
Adapter over PCA9685 pin driver.
593+
This can output ttl HIGH or LOW or
594+
produce a duty cycle at the given frequency.
595+
'''
596+
def __init__(self, channel: int, busnum: int = 1, address: int = 0x40, frequency: int = 60):
579597

580-
I2C.get_default_bus = get_bus
581-
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
582-
self.pwm.set_pwm_freq(frequency)
583-
self._frequency = frequency
598+
import adafruit_pca9685
599+
self.pca = pca9685(busnum, address, frequency)
600+
self.pca_pin = adafruit_pca9685.PWMChannel(self.pca.pca9685driver, channel)
601+
self.channel = channel
584602

585-
def get_frequency(self):
586-
return self._frequency
603+
def get_frequency(self) -> int:
604+
return self.pca.get_frequency()
587605

588-
def set_high(self, channel: int):
589-
self.pwm.set_pwm(channel, 4096, 0)
606+
def set_high(self):
607+
# adafruit blinka uses 16bit values
608+
# where 0xFFFF is a flag that means fully high
609+
self.pca_pin.duty_cycle = 0xFFFF
590610

591-
def set_low(self, channel: int):
592-
self.pwm.set_pwm(channel, 0, 4096)
611+
def set_low(self):
612+
# adafruit blinka uses 16bit values
613+
# where 0x0000 is a flag that means fully low
614+
self.pca_pin.duty_cycle = 0x0000
593615

594-
def set_duty_cycle(self, channel: int, duty_cycle: float):
616+
def set_duty_cycle(self, duty_cycle: float):
595617
if duty_cycle < 0 or duty_cycle > 1:
596618
raise ValueError("duty_cycle must be in range 0 to 1")
597619
if duty_cycle == 1:
598-
self.set_high(channel)
620+
self.set_high()
599621
elif duty_cycle == 0:
600-
self.set_low(channel)
622+
self.set_low()
601623
else:
602-
# duty cycle is fraction of the 12 bits
603-
pulse = int(4096 * duty_cycle)
624+
# adafruit blinka uses 16 bit values
625+
# for resolution of duty cycle.
626+
pulse = int(0x10000 * duty_cycle)
604627
try:
605-
self.pwm.set_pwm(channel, 0, pulse)
628+
self.pca_pin.duty_cycle = pulse
606629
except Exception as e:
607-
logger.error(f'Error on PCA9685 channel {channel}: {str(e)}')
630+
logger.error(f'Error on PCA9685 channel {self.channel}: {str(e)}')
608631

609632

610633
#
611634
# lookup map for PCA9685 singletons
612635
# key is "busnum:address"
613636
#
614637
_pca9685 = {}
638+
_pca9685pin = {}
615639

616640

617-
def pca9685(busnum: int, address: int, frequency: int = 60):
641+
def pca9685(busnum: int, address: int, frequency: int = 60) -> PCA9685board:
618642
"""
619643
pca9685 factory allocates driver for pca9685
620644
at given bus number and i2c address.
@@ -632,21 +656,46 @@ def pca9685(busnum: int, address: int, frequency: int = 60):
632656
key = str(busnum) + ":" + hex(address)
633657
pca = _pca9685.get(key)
634658
if pca is None:
635-
pca = PCA9685(busnum, address, frequency)
659+
pca = PCA9685board(busnum, address, frequency)
660+
_pca9685[key] = pca
636661
if pca.get_frequency() != frequency:
637662
raise ValueError(
638663
f"Frequency {frequency} conflicts with pca9685 at {key} "
639-
f"with frequency {pca.pwm.get_pwm_freq()}")
664+
f"with frequency {pca.get_frequency()}")
640665
return pca
641666

642667

643-
class OutputPinPCA9685(ABC):
668+
def pca9685pin(channel: int, busnum: int, address: int, frequency: int = 60) -> PCA9685Pin:
669+
"""
670+
pca9685 pin factory allocates driver for pin on channel on pca9685
671+
at given bus number and i2c address.
672+
If we have already created one for that bus/addr/channel
673+
pair then use that singleton. If frequency is
674+
not the same, then error.
675+
:param channel: PCA9685 channel 0..15 to control
676+
:param busnum: I2C bus number of PCA9685
677+
:param address: address of PCA9685 on I2C bus
678+
:param frequency: frequency in hertz of duty cycle
679+
:except: PCA9685 has a single frequency for all channels,
680+
so attempts to allocate a controller at a
681+
given bus number and address with different
682+
frequencies will raise a ValueError
683+
"""
684+
key = str(busnum) + ":" + hex(address) + ":" + str(channel)
685+
pca_pin = _pca9685pin.get(key)
686+
if pca_pin is None:
687+
pca_pin = PCA9685Pin(channel, busnum, address, frequency)
688+
_pca9685pin[key] = pca_pin
689+
return pca_pin
690+
691+
692+
class OutputPinPCA9685(OutputPin):
644693
"""
645694
Output pin ttl HIGH/LOW using PCA9685
646695
"""
647-
def __init__(self, pin_number: int, pca9685: PCA9685) -> None:
696+
def __init__(self, pin_number: int, busnum: int, address: int, frequency: int = 60) -> None:
648697
self.pin_number = pin_number
649-
self.pca9685 = pca9685
698+
self.pca_pin = pca9685pin(pin_number, busnum, address, frequency)
650699
self._state = PinState.NOT_STARTED
651700

652701
def start(self, state: int = PinState.LOW) -> None:
@@ -688,19 +737,19 @@ def output(self, state: int) -> None:
688737
if self.state() == PinState.NOT_STARTED:
689738
raise RuntimeError(f"Attempt to use pin ({self.pin_number}) that is not started")
690739
if state == PinState.HIGH:
691-
self.pca9685.set_high(self.pin_number)
740+
self.pca_pin.set_high()
692741
else:
693-
self.pca9685.set_low(self.pin_number)
742+
self.pca_pin.set_low()
694743
self._state = state
695744

696745

697746
class PwmPinPCA9685(PwmPin):
698747
"""
699748
PWM output pin using PCA9685
700749
"""
701-
def __init__(self, pin_number: int, pca9685: PCA9685) -> None:
750+
def __init__(self, pin_number: int, busnum: int, address: int, frequency: int = 60) -> None:
702751
self.pin_number = pin_number
703-
self.pca9685 = pca9685
752+
self.pca_pin = pca9685pin(pin_number, busnum, address, frequency)
704753
self._state = PinState.NOT_STARTED
705754

706755
def start(self, duty: float = 0) -> None:
@@ -739,7 +788,7 @@ def duty_cycle(self, duty: float) -> None:
739788
raise RuntimeError(f"Attempt to use pin ({self.pin_number}) that is not started")
740789
if duty < 0 or duty > 1:
741790
raise ValueError("duty_cycle must be in range 0 to 1")
742-
self.pca9685.set_duty_cycle(self.pin_number, duty)
791+
self.pca_pin.set_duty_cycle(duty)
743792
self._state = duty
744793

745794

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ install_requires =
4545
[options.extras_require]
4646
pi =
4747
picamera2
48-
Adafruit_PCA9685
48+
adafruit-circuitpython-pca9685
4949
adafruit-circuitpython-ssd1306
5050
adafruit-circuitpython-rplidar
5151
RPi.GPIO
@@ -60,7 +60,7 @@ pi =
6060
albumentations
6161

6262
nano =
63-
Adafruit_PCA9685
63+
adafruit-circuitpython-pca9685
6464
adafruit-circuitpython-ssd1306
6565
adafruit-circuitpython-rplidar
6666
Jetson.GPIO

0 commit comments

Comments
 (0)