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}