-
Notifications
You must be signed in to change notification settings - Fork 65
/
adafruit_pca9685.py
executable file
·197 lines (157 loc) · 6.74 KB
/
adafruit_pca9685.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# SPDX-FileCopyrightText: 2016 Radomir Dopieralski for Adafruit Industries
# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_pca9685`
====================================================
Driver for the PCA9685 PWM control IC. Its commonly used to control servos, leds and motors.
.. seealso:: The `Adafruit CircuitPython Motor library
<https://github.com/adafruit/Adafruit_CircuitPython_Motor>`_ can be used to control the PWM
outputs for specific uses instead of generic duty_cycle adjustments.
* Author(s): Scott Shawcroft
Implementation Notes
--------------------
**Hardware:**
* Adafruit `16-Channel 12-bit PWM/Servo Driver - I2C interface - PCA9685
<https://www.adafruit.com/product/815>`_ (Product ID: 815)
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards:
https://github.com/adafruit/circuitpython/releases
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
"""
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PCA9685.git"
import time
from adafruit_register.i2c_struct import UnaryStruct
from adafruit_register.i2c_struct_array import StructArray
from adafruit_bus_device import i2c_device
try:
from typing import Optional, Type
from types import TracebackType
from busio import I2C
except ImportError:
pass
class PWMChannel:
"""A single PCA9685 channel that matches the :py:class:`~pwmio.PWMOut` API.
:param PCA9685 pca: The PCA9685 object
:param int index: The index of the channel
"""
def __init__(self, pca: "PCA9685", index: int):
self._pca = pca
self._index = index
@property
def frequency(self) -> float:
"""The overall PWM frequency in Hertz (read-only).
A PWMChannel's frequency cannot be set individually.
All channels share a common frequency, set by PCA9685.frequency."""
return self._pca.frequency
@frequency.setter
def frequency(self, _):
raise NotImplementedError("frequency cannot be set on individual channels")
@property
def duty_cycle(self) -> int:
"""16 bit value that dictates how much of one cycle is high (1) versus low (0). 0xffff will
always be high, 0 will always be low and 0x7fff will be half high and then half low.
"""
pwm = self._pca.pwm_regs[self._index]
if pwm[0] == 0x1000:
return 0xFFFF
if pwm[1] == 0x1000:
return 0x0000
return pwm[1] << 4
@duty_cycle.setter
def duty_cycle(self, value: int) -> None:
if not 0 <= value <= 0xFFFF:
raise ValueError(f"Out of range: value {value} not 0 <= value <= 65,535")
if value == 0xFFFF:
# Special case for "fully on":
self._pca.pwm_regs[self._index] = (0x1000, 0)
elif value < 0x0010:
# Special case for "fully off":
self._pca.pwm_regs[self._index] = (0, 0x1000)
else:
# Shift our value by four because the PCA9685 is only 12 bits but our value is 16
value = value >> 4
# value should never be zero here because of the test for the "fully off" case
# (the LEDn_ON and LEDn_OFF registers should never be set with the same values)
self._pca.pwm_regs[self._index] = (0, value)
class PCAChannels: # pylint: disable=too-few-public-methods
"""Lazily creates and caches channel objects as needed. Treat it like a sequence.
:param PCA9685 pca: The PCA9685 object
"""
def __init__(self, pca: "PCA9685") -> None:
self._pca = pca
self._channels = [None] * len(self)
def __len__(self) -> int:
return 16
def __getitem__(self, index: int) -> PWMChannel:
if not self._channels[index]:
self._channels[index] = PWMChannel(self._pca, index)
return self._channels[index]
class PCA9685:
"""
Initialise the PCA9685 chip at ``address`` on ``i2c_bus``.
The internal reference clock is 25mhz but may vary slightly with environmental conditions and
manufacturing variances. Providing a more precise ``reference_clock_speed`` can improve the
accuracy of the frequency and duty_cycle computations. See the ``calibration.py`` example for
how to derive this value by measuring the resulting pulse widths.
:param ~busio.I2C i2c_bus: The I2C bus which the PCA9685 is connected to.
:param int address: The I2C address of the PCA9685.
:param int reference_clock_speed: The frequency of the internal reference clock in Hertz.
"""
# Registers:
mode1_reg = UnaryStruct(0x00, "<B")
mode2_reg = UnaryStruct(0x01, "<B")
prescale_reg = UnaryStruct(0xFE, "<B")
pwm_regs = StructArray(0x06, "<HH", 16)
def __init__(
self,
i2c_bus: I2C,
*,
address: int = 0x40,
reference_clock_speed: int = 25000000,
) -> None:
self.i2c_device = i2c_device.I2CDevice(i2c_bus, address)
self.channels = PCAChannels(self)
"""Sequence of 16 `PWMChannel` objects. One for each channel."""
self.reference_clock_speed = reference_clock_speed
"""The reference clock speed in Hz."""
self.reset()
def reset(self) -> None:
"""Reset the chip."""
self.mode1_reg = 0x00 # Mode1
@property
def frequency(self) -> float:
"""The overall PWM frequency in Hertz."""
prescale_result = self.prescale_reg
if prescale_result < 3:
raise ValueError(
"The device pre_scale register (0xFE) was not read or returned a value < 3"
)
return self.reference_clock_speed / 4096 / (prescale_result + 1)
@frequency.setter
def frequency(self, freq: float) -> None:
prescale = int(self.reference_clock_speed / 4096.0 / freq + 0.5) - 1
if prescale < 3:
raise ValueError("PCA9685 cannot output at the given frequency")
old_mode = self.mode1_reg # Mode 1
self.mode1_reg = (old_mode & 0x7F) | 0x10 # Mode 1, sleep
self.prescale_reg = prescale # Prescale
self.mode1_reg = old_mode # Mode 1
time.sleep(0.005)
# Mode 1, autoincrement on, fix to stop pca9685 from accepting commands at all addresses
self.mode1_reg = old_mode | 0xA0
def __enter__(self) -> "PCA9685":
return self
def __exit__(
self,
exception_type: Optional[Type[type]],
exception_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.deinit()
def deinit(self) -> None:
"""Stop using the pca9685."""
self.reset()