pca9685_rppal/lib.rs
1//! A `rppal`-based wrapper for the Adafruit PCA9685 Servo/PWM Controller.
2//!
3//! This crate provides a convenient interface to the PCA9685 PWM driver via I²C on the Raspberry Pi,
4//! using the [`rppal`](https://docs.rs/rppal) library.
5//!
6//! The PCA9685 is often used to control servos or LEDs, offering up to 16 independent PWM channels.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use pca9685_rppal::*;
12//!
13//! let mut pwm = Pca9685::new().expect("Create Pca9685");
14//! pwm.init().expect("Initialize Pca9685");
15//! pwm.set_pwm_freq(50.0).expect("Set Frequency to 50hz (common for servos)");
16//! pwm.set_pwm(0, 0, 1500).expect("Set PWM on channel 0");
17//!
18//! ```
19
20use std::time::Duration;
21
22use rppal::i2c::I2c;
23
24// Register definitions.
25
26/// The default I²C address of the PCA9685 device.
27pub const PCA9685_ADDRESS: u16 = 0x40;
28/// Mode 1 register address, used for configuring basic operation modes of the PCA9685.
29pub const MODE1: u8 = 0x00;
30/// Mode 2 register address, used for configuring output behavior of the PCA9685.
31pub const MODE2: u8 = 0x01;
32/// Subaddress 1 register, used to set the first alternative I²C subaddress.
33pub const SUBADR1: u8 = 0x02;
34/// Subaddress 2 register, used to set the second alternative I²C subaddress.
35pub const SUBADR2: u8 = 0x03;
36/// Subaddress 3 register, used to set the third alternative I²C subaddress.
37pub const SUBADR3: u8 = 0x04;
38/// Prescale register, used to configure the PWM frequency of the PCA9685.
39pub const PRESCALE: u8 = 0xFE;
40/// LED0_ON_L register address, low byte of the LED0 ON time.
41pub const LED0_ON_L: u8 = 0x06;
42/// LED0_ON_H register address, high byte of the LED0 ON time.
43pub const LED0_ON_H: u8 = 0x07;
44/// LED0_OFF_L register address, low byte of the LED0 OFF time.
45pub const LED0_OFF_L: u8 = 0x08;
46/// LED0_OFF_H register address, high byte of the LED0 OFF time.
47pub const LED0_OFF_H: u8 = 0x09;
48/// ALL_LED_ON_L register address, low byte for turning all LEDs ON.
49pub const ALL_LED_ON_L: u8 = 0xFA;
50/// ALL_LED_ON_H register address, high byte for turning all LEDs ON.
51pub const ALL_LED_ON_H: u8 = 0xFB;
52/// ALL_LED_OFF_L register address, low byte for turning all LEDs OFF.
53pub const ALL_LED_OFF_L: u8 = 0xFC;
54/// ALL_LED_OFF_H register address, high byte for turning all LEDs OFF.
55pub const ALL_LED_OFF_H: u8 = 0xFD;
56
57// Bit definitions.
58
59/// Bit mask for restarting the PCA9685 oscillator and resetting its state.
60pub const RESTART: u8 = 0x80;
61/// Bit mask for enabling low-power sleep mode.
62pub const SLEEP: u8 = 0x10;
63/// Bit mask for enabling the ALLCALL address, allowing all devices to respond to a general call.
64pub const ALLCALL: u8 = 0x01;
65/// Bit mask for inverting the output logic of the PCA9685.
66pub const INVRT: u8 = 0x10;
67/// Bit mask for setting the output driver mode to totem-pole (instead of open-drain).
68pub const OUTDRV: u8 = 0x04;
69
70// Other constants.
71
72/// Software reset command for the PCA9685, used to reset all devices on the I²C bus.
73pub const SWRST: u8 = 0x06;
74
75/// Represents a PCA9685 device connected via I²C.
76///
77/// This struct wraps an `rppal::i2c::I2c` instance pointed at the
78/// appropriate slave address, providing helper methods to easily
79/// configure the PCA9685 and set PWM values on its channels.
80pub struct Pca9685 {
81 /// Underlying I²c Device
82 i2c: I2c,
83}
84
85impl Pca9685 {
86 /// Constructs a new `Pca9685` device at the default address (0x40) on the default I²C bus.
87 ///
88 /// # Errors
89 ///
90 /// Returns an error of type [`rppal::i2c::Error`] if an I²C device
91 /// cannot be created or the default bus cannot be accessed.
92 pub fn new() -> rppal::i2c::Result<Self> {
93 let mut i2c = I2c::new()?;
94 i2c.set_slave_address(PCA9685_ADDRESS)?;
95
96 Ok(Self { i2c })
97 }
98
99 /// Initializes the PCA9685 for standard operation.
100 ///
101 /// 1. Sets all PWM outputs to "off".
102 /// 2. Configures the device to use the totem-pole driver and enable the ALLCALL address.
103 /// 3. Takes the PCA9685 out of sleep mode.
104 ///
105 /// # Errors
106 ///
107 /// Returns an error if any I²C write or read operations fail.
108 ///
109 /// # Example
110 ///
111 /// ```ignore
112 /// let mut pca = Pca9685::new()?;
113 /// pca.init()?;
114 /// ```
115 pub fn init(&mut self) -> rppal::i2c::Result<()> {
116 self.set_all_pwm(0, 0)?;
117
118 self.i2c.smbus_write_byte(MODE2, OUTDRV)?;
119 self.i2c.smbus_write_byte(MODE1, ALLCALL)?;
120 std::thread::sleep(Duration::from_millis(5));
121
122 let mode1 = self.i2c.smbus_read_byte(MODE1)?;
123 let mode1 = mode1 & !SLEEP;
124
125 self.i2c.smbus_write_byte(MODE1, mode1)?;
126 std::thread::sleep(Duration::from_millis(5));
127
128 Ok(())
129 }
130
131 /// Sets the PWM frequency (in Hertz).
132 ///
133 /// The PCA9685's internal clock is assumed to run at 25 MHz. The prescaler is computed to
134 /// achieve the target frequency, then the device is put to sleep momentarily while the new
135 /// prescaler is written.
136 ///
137 /// # Arguments
138 ///
139 /// * `freq_hz` - Desired frequency in Hertz (e.g., `50.0` for servos).
140 ///
141 /// # Errors
142 ///
143 /// Returns an error if any I²C write or read operations fail.
144 ///
145 /// # Example
146 ///
147 /// ```ignore
148 /// let mut pca = Pca9685::new()?;
149 /// pca.init()?;
150 /// pca.set_pwm_freq(60.0)?; // typical for some servos
151 /// ```
152 pub fn set_pwm_freq(&mut self, freq_hz: f32) -> rppal::i2c::Result<()> {
153 let prescaleval = 25e6 / 4096.0 / freq_hz - 1.0;
154 let prescale = (prescaleval + 0.5).floor() as u8;
155
156 let old_mode = self.i2c.smbus_read_byte(MODE1)?;
157 let new_mode = old_mode & 0x7F | SLEEP;
158
159 self.i2c.smbus_write_byte(MODE1, new_mode)?;
160 self.i2c.smbus_write_byte(PRESCALE, prescale)?;
161 self.i2c.smbus_write_byte(MODE1, old_mode)?;
162 std::thread::sleep(Duration::from_millis(5));
163
164 self.i2c.smbus_write_byte(MODE1, old_mode | 0xA1)
165 }
166
167 /// Sets the PWM ON and OFF counts for a single channel.
168 ///
169 /// # Arguments
170 ///
171 /// * `channel` - The channel index (0–15) on the PCA9685.
172 /// * `on` - The timer tick at which the output is switched ON.
173 /// * `off` - The timer tick at which the output is switched OFF.
174 ///
175 /// Each channel output is controlled by two 12-bit registers: ON and OFF. The timer counts from 0 to 4095.
176 ///
177 /// # Errors
178 ///
179 /// Returns an error if any I²C write operation fails.
180 ///
181 /// # Example
182 ///
183 /// ```ignore
184 /// let mut pca = Pca9685::new()?;
185 /// pca.init()?;
186 /// // Turn channel 0 on at 0, off at 1500
187 /// pca.set_pwm(0, 0, 1500)?;
188 /// ```
189 pub fn set_pwm(&mut self, channel: u8, on: u16, off: u16) -> rppal::i2c::Result<()> {
190 self.i2c
191 .smbus_write_byte(LED0_ON_L + 4 * channel, (on & 0xFF) as u8)?;
192
193 self.i2c
194 .smbus_write_byte(LED0_ON_H + 4 * channel, (on >> 8) as u8)?;
195
196 self.i2c
197 .smbus_write_byte(LED0_OFF_L + 4 * channel, (off & 0xFF) as u8)?;
198
199 self.i2c
200 .smbus_write_byte(LED0_OFF_H + 4 * channel, (off >> 8) as u8)
201 }
202
203 /// Sets the PWM ON and OFF counts for *all* channels simultaneously.
204 ///
205 /// # Arguments
206 ///
207 /// * `on` - The timer tick at which all outputs are switched ON.
208 /// * `off` - The timer tick at which all outputs are switched OFF.
209 ///
210 /// # Errors
211 ///
212 /// Returns an error if any I²C write operation fails.
213 ///
214 /// # Example
215 ///
216 /// ```ignore
217 /// let mut pca = Pca9685::new()?;
218 /// pca.init()?;
219 /// // Turn all channels fully OFF
220 /// pca.set_all_pwm(0, 0)?;
221 /// ```
222 pub fn set_all_pwm(&mut self, on: u16, off: u16) -> rppal::i2c::Result<()> {
223 self.i2c.smbus_write_byte(ALL_LED_ON_L, (on & 0xFF) as u8)?;
224
225 self.i2c.smbus_write_byte(ALL_LED_ON_H, (on >> 8) as u8)?;
226
227 self.i2c
228 .smbus_write_byte(ALL_LED_OFF_L, (off & 0xFF) as u8)?;
229
230 self.i2c.smbus_write_byte(ALL_LED_OFF_H, (off >> 8) as u8)
231 }
232}