Skip to main content

sn3218_hal/
lib.rs

1//! SN3218 [`embedded-hal`] driver for 18-channel LED driver SN3218
2//!
3//! The SN3218 is an 18-channel LED driver with PWM control, commonly arranged as 6 RGB channels.
4//! This LED driver is used on the Raspberry Pi GFX HAT by Pimoroni and other SN3218-based boards.
5//!
6//! # Features
7//!
8//! - 18-channel PWM LED control (typically 6 RGB LEDs)
9//! - Automatic gamma correction for natural brightness curves
10//! - Simple enable/disable control
11//! - Mask-based channel selection
12//! - Built on `embedded-hal` for platform independence
13//!
14//! # Examples
15//!
16//! ```no_run
17//! use sn3218::SN3218;
18//! # use embedded_hal_mock::i2c::Mock as I2c;
19//! # let expectations = [];
20//! # let i2c = I2c::new(&expectations);
21//!
22//! let mut led_driver = SN3218::new(i2c);
23//!
24//! // Enable output and all LEDs
25//! led_driver.enable().unwrap();
26//! led_driver.enable_leds(0x3FFFF).unwrap(); // All 18 channels
27//!
28//! // Set RGB values for 6 LEDs (18 channels total)
29//! let values = [
30//!     255, 0, 0,     // LED 0: Red
31//!     0, 255, 0,     // LED 1: Green
32//!     0, 0, 255,     // LED 2: Blue
33//!     255, 255, 0,   // LED 3: Yellow
34//!     255, 0, 255,   // LED 4: Magenta
35//!     0, 255, 255,   // LED 5: Cyan
36//! ];
37//! led_driver.output(&values).unwrap();
38//! ```
39//!
40//! # Channel Layout
41//!
42//! The 18 channels are organized as 6 RGB LEDs:
43//! - LED 0: channels 0 (R), 1 (G), 2 (B)
44//! - LED 1: channels 3 (R), 4 (G), 5 (B)
45//! - LED 2: channels 6 (R), 7 (G), 8 (B)
46//! - LED 3: channels 9 (R), 10 (G), 11 (B)
47//! - LED 4: channels 12 (R), 13 (G), 14 (B)
48//! - LED 5: channels 15 (R), 16 (G), 17 (B)
49//!
50//! Based on the Python implementation: <https://github.com/pimoroni/sn3218>
51//!
52
53use embedded_hal::i2c::I2c;
54
55/// SN3218 LED driver instance
56///
57/// Controls an 18-channel SN3218 LED driver over I2C. The driver includes
58/// automatic gamma correction for natural brightness perception.
59///
60/// # Type Parameters
61///
62/// * `T` - An I2C implementation that satisfies the `embedded_hal::i2c::I2c` trait
63pub struct SN3218<T: I2c> {
64    i2c: T,
65    gamma_table: [u8; 256],
66}
67
68const I2C_ADDRESS: u8 = 0x54;
69const CMD_ENABLE_OUTPUT: u8 = 0x00;
70const CMD_SET_PWM_VALUES: u8 = 0x01;
71const CMD_ENABLE_LEDS: u8 = 0x13;
72const CMD_UPDATE: u8 = 0x16;
73const CMD_RESET: u8 = 0x17;
74
75const BUF_CMD_ENABLE_ENABLE: [u8; 1] = [0x01];
76const BUF_CMD_ENABLE_DISABLE: [u8; 1] = [0x00];
77const BUF_CMD_255: [u8; 1] = [0xFF];
78
79impl<T: I2c> SN3218<T> {
80    /// Creates a new SN3218 driver instance
81    ///
82    /// Initializes the driver with automatic gamma correction. The gamma table
83    /// is pre-calculated using the formula: `255^(x/255)` for natural brightness curves.
84    ///
85    /// # Parameters
86    ///
87    /// * `i2c` - An I2C interface implementation
88    ///
89    /// # Examples
90    ///
91    /// ```no_run
92    /// use sn3218::SN3218;
93    /// # use embedded_hal_mock::i2c::Mock as I2c;
94    /// # let expectations = [];
95    /// # let i2c = I2c::new(&expectations);
96    ///
97    /// let led_driver = SN3218::new(i2c);
98    /// ```
99    pub fn new(i2c: T) -> Self {
100        let mut gamma_table: [u8; 256] = [0; 256];
101        for i in 0..256 {
102            gamma_table[i] = (255f64.powf(i as f64 / 255f64)) as u8;
103        }
104        Self { i2c, gamma_table }
105    }
106
107    /// Enables the LED driver output
108    ///
109    /// This must be called before the LEDs will produce any light.
110    /// Use [`disable`](Self::disable) to turn off all outputs.
111    ///
112    /// # Errors
113    ///
114    /// Returns the I2C implementation's error type if communication fails.
115    ///
116    /// # Examples
117    ///
118    /// ```no_run
119    /// # use sn3218::SN3218;
120    /// # use embedded_hal_mock::i2c::Mock as I2c;
121    /// # let expectations = [];
122    /// # let i2c = I2c::new(&expectations);
123    /// let mut driver = SN3218::new(i2c);
124    /// driver.enable().unwrap();
125    /// ```
126    pub fn enable(&mut self) -> Result<(), T::Error> {
127        self.i2c
128            .cmd_write(CMD_ENABLE_OUTPUT, &BUF_CMD_ENABLE_ENABLE)
129    }
130
131    /// Disables the LED driver output
132    ///
133    /// Turns off all LED outputs immediately. The PWM values and enabled
134    /// channels are preserved and will resume when [`enable`](Self::enable) is called.
135    ///
136    /// # Errors
137    ///
138    /// Returns the I2C implementation's error type if communication fails.
139    pub fn disable(&mut self) -> Result<(), T::Error> {
140        self.i2c
141            .cmd_write(CMD_ENABLE_OUTPUT, &BUF_CMD_ENABLE_DISABLE)
142    }
143
144    /// Resets the LED driver to its default state
145    ///
146    /// Clears all PWM values and LED enable states. After reset, you'll need to
147    /// call [`enable`](Self::enable) and [`enable_leds`](Self::enable_leds) again.
148    ///
149    /// # Errors
150    ///
151    /// Returns the I2C implementation's error type if communication fails.
152    pub fn reset(&mut self) -> Result<(), T::Error> {
153        self.i2c.cmd_write(CMD_RESET, &BUF_CMD_255)
154    }
155
156    /// Enables specific LED channels using a bitmask
157    ///
158    /// Each bit in the mask corresponds to one of the 18 LED channels.
159    /// Only enabled channels will produce light when PWM values are set.
160    ///
161    /// # Parameters
162    ///
163    /// * `mask` - 18-bit mask where each bit enables the corresponding channel
164    ///
165    /// # Examples
166    ///
167    /// ```no_run
168    /// # use sn3218::SN3218;
169    /// # use embedded_hal_mock::i2c::Mock as I2c;
170    /// # let expectations = [];
171    /// # let i2c = I2c::new(&expectations);
172    /// let mut driver = SN3218::new(i2c);
173    ///
174    /// // Enable all 18 channels
175    /// driver.enable_leds(0x3FFFF).unwrap();
176    ///
177    /// // Enable only the first RGB LED (channels 0, 1, 2)
178    /// driver.enable_leds(0x07).unwrap();
179    ///
180    /// // Enable only red channels (0, 3, 6, 9, 12, 15)
181    /// driver.enable_leds(0b001001001001001001).unwrap();
182    /// ```
183    ///
184    /// # Errors
185    ///
186    /// Returns the I2C implementation's error type if communication fails.
187    pub fn enable_leds(&mut self, mask: u32) -> Result<(), T::Error> {
188        let buf = [
189            (mask & 0x3F) as u8,
190            ((mask >> 6) & 0x3F) as u8,
191            ((mask >> 12) & 0x3F) as u8,
192        ];
193        self.i2c.cmd_write(CMD_ENABLE_LEDS, &buf)?;
194        self.i2c.cmd_write(CMD_UPDATE, &BUF_CMD_255)
195    }
196
197    /// Sets PWM values for all 18 LED channels
198    ///
199    /// Values are automatically gamma-corrected for natural brightness perception.
200    /// The array must contain exactly 18 elements, typically arranged as 6 RGB triplets.
201    ///
202    /// # Parameters
203    ///
204    /// * `values` - Array of 18 PWM values (0-255) for each channel
205    ///
206    /// # Panics
207    ///
208    /// Panics if the values array is not exactly 18 elements long.
209    ///
210    /// # Examples
211    ///
212    /// ```no_run
213    /// # use sn3218::SN3218;
214    /// # use embedded_hal_mock::i2c::Mock as I2c;
215    /// # let expectations = [];
216    /// # let i2c = I2c::new(&expectations);
217    /// let mut driver = SN3218::new(i2c);
218    ///
219    /// // Set RGB values for 6 LEDs
220    /// let values = [
221    ///     255, 0, 0,     // LED 0: Full red
222    ///     0, 255, 0,     // LED 1: Full green
223    ///     0, 0, 255,     // LED 2: Full blue
224    ///     128, 128, 128, // LED 3: Half brightness white
225    ///     255, 128, 64,  // LED 4: Orange
226    ///     0, 0, 0,       // LED 5: Off
227    /// ];
228    /// driver.output(&values).unwrap();
229    /// ```
230    ///
231    /// # Errors
232    ///
233    /// Returns the I2C implementation's error type if communication fails.
234    pub fn output(&mut self, values: &[u8]) -> Result<(), T::Error> {
235        if values.len() != 18 {
236            panic!(
237                "values array must be exactly 18 elements long (got {})",
238                values.len()
239            );
240        }
241        let mut buf = [0u8; 18];
242        for i in 0..18 {
243            buf[i] = self.gamma_table[values[i] as usize];
244        }
245        self.i2c.cmd_write(CMD_SET_PWM_VALUES, &buf)?;
246        self.i2c.cmd_write(CMD_UPDATE, &BUF_CMD_255)
247    }
248}
249
250/// Internal trait for sending commands to the SN3218 device
251///
252/// This trait extends the I2C interface with a helper method for sending
253/// commands with data to the SN3218 chip.
254trait SN3218CmdWrite<T: I2c> {
255    /// Writes a command followed by data to the SN3218
256    fn cmd_write(&mut self, command: u8, buf: &[u8]) -> Result<(), T::Error>;
257}
258
259impl<T: I2c> SN3218CmdWrite<T> for T {
260    /// Sends a command byte followed by data bytes to the SN3218
261    ///
262    /// This method constructs a message with the command byte first,
263    /// followed by the provided data buffer, and sends it to the SN3218's
264    /// I2C address (0x54).
265    fn cmd_write(&mut self, command: u8, buffer: &[u8]) -> Result<(), T::Error> {
266        let to_send: Vec<u8> = [command].iter().chain(buffer).copied().collect();
267        self.write(I2C_ADDRESS, &to_send)
268    }
269}