Skip to main content

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}