1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * leds-lp3944.c - driver for National Semiconductor LP3944 Funlight Chip |
4 | * |
5 | * Copyright (C) 2009 Antonio Ospite <ospite@studenti.unina.it> |
6 | */ |
7 | |
8 | /* |
9 | * I2C driver for National Semiconductor LP3944 Funlight Chip |
10 | * http://www.national.com/pf/LP/LP3944.html |
11 | * |
12 | * This helper chip can drive up to 8 leds, with two programmable DIM modes; |
13 | * it could even be used as a gpio expander but this driver assumes it is used |
14 | * as a led controller. |
15 | * |
16 | * The DIM modes are used to set _blink_ patterns for leds, the pattern is |
17 | * specified supplying two parameters: |
18 | * - period: from 0s to 1.6s |
19 | * - duty cycle: percentage of the period the led is on, from 0 to 100 |
20 | * |
21 | * LP3944 can be found on Motorola A910 smartphone, where it drives the rgb |
22 | * leds, the camera flash light and the displays backlights. |
23 | */ |
24 | |
25 | #include <linux/module.h> |
26 | #include <linux/i2c.h> |
27 | #include <linux/slab.h> |
28 | #include <linux/leds.h> |
29 | #include <linux/mutex.h> |
30 | #include <linux/leds-lp3944.h> |
31 | |
32 | /* Read Only Registers */ |
33 | #define LP3944_REG_INPUT1 0x00 /* LEDs 0-7 InputRegister (Read Only) */ |
34 | #define LP3944_REG_REGISTER1 0x01 /* None (Read Only) */ |
35 | |
36 | #define LP3944_REG_PSC0 0x02 /* Frequency Prescaler 0 (R/W) */ |
37 | #define LP3944_REG_PWM0 0x03 /* PWM Register 0 (R/W) */ |
38 | #define LP3944_REG_PSC1 0x04 /* Frequency Prescaler 1 (R/W) */ |
39 | #define LP3944_REG_PWM1 0x05 /* PWM Register 1 (R/W) */ |
40 | #define LP3944_REG_LS0 0x06 /* LEDs 0-3 Selector (R/W) */ |
41 | #define LP3944_REG_LS1 0x07 /* LEDs 4-7 Selector (R/W) */ |
42 | |
43 | /* These registers are not used to control leds in LP3944, they can store |
44 | * arbitrary values which the chip will ignore. |
45 | */ |
46 | #define LP3944_REG_REGISTER8 0x08 |
47 | #define LP3944_REG_REGISTER9 0x09 |
48 | |
49 | #define LP3944_DIM0 0 |
50 | #define LP3944_DIM1 1 |
51 | |
52 | /* period in ms */ |
53 | #define LP3944_PERIOD_MIN 0 |
54 | #define LP3944_PERIOD_MAX 1600 |
55 | |
56 | /* duty cycle is a percentage */ |
57 | #define LP3944_DUTY_CYCLE_MIN 0 |
58 | #define LP3944_DUTY_CYCLE_MAX 100 |
59 | |
60 | #define ldev_to_led(c) container_of(c, struct lp3944_led_data, ldev) |
61 | |
62 | /* Saved data */ |
63 | struct lp3944_led_data { |
64 | u8 id; |
65 | enum lp3944_type type; |
66 | struct led_classdev ldev; |
67 | struct i2c_client *client; |
68 | }; |
69 | |
70 | struct lp3944_data { |
71 | struct mutex lock; |
72 | struct i2c_client *client; |
73 | struct lp3944_led_data leds[LP3944_LEDS_MAX]; |
74 | }; |
75 | |
76 | static int lp3944_reg_read(struct i2c_client *client, u8 reg, u8 *value) |
77 | { |
78 | int tmp; |
79 | |
80 | tmp = i2c_smbus_read_byte_data(client, command: reg); |
81 | if (tmp < 0) |
82 | return tmp; |
83 | |
84 | *value = tmp; |
85 | |
86 | return 0; |
87 | } |
88 | |
89 | static int lp3944_reg_write(struct i2c_client *client, u8 reg, u8 value) |
90 | { |
91 | return i2c_smbus_write_byte_data(client, command: reg, value); |
92 | } |
93 | |
94 | /** |
95 | * lp3944_dim_set_period() - Set the period for DIM status |
96 | * |
97 | * @client: the i2c client |
98 | * @dim: either LP3944_DIM0 or LP3944_DIM1 |
99 | * @period: period of a blink, that is a on/off cycle, expressed in ms. |
100 | */ |
101 | static int lp3944_dim_set_period(struct i2c_client *client, u8 dim, u16 period) |
102 | { |
103 | u8 psc_reg; |
104 | u8 psc_value; |
105 | int err; |
106 | |
107 | if (dim == LP3944_DIM0) |
108 | psc_reg = LP3944_REG_PSC0; |
109 | else if (dim == LP3944_DIM1) |
110 | psc_reg = LP3944_REG_PSC1; |
111 | else |
112 | return -EINVAL; |
113 | |
114 | /* Convert period to Prescaler value */ |
115 | if (period > LP3944_PERIOD_MAX) |
116 | return -EINVAL; |
117 | |
118 | psc_value = (period * 255) / LP3944_PERIOD_MAX; |
119 | |
120 | err = lp3944_reg_write(client, reg: psc_reg, value: psc_value); |
121 | |
122 | return err; |
123 | } |
124 | |
125 | /** |
126 | * lp3944_dim_set_dutycycle - Set the duty cycle for DIM status |
127 | * |
128 | * @client: the i2c client |
129 | * @dim: either LP3944_DIM0 or LP3944_DIM1 |
130 | * @duty_cycle: percentage of a period during which a led is ON |
131 | */ |
132 | static int lp3944_dim_set_dutycycle(struct i2c_client *client, u8 dim, |
133 | u8 duty_cycle) |
134 | { |
135 | u8 pwm_reg; |
136 | u8 pwm_value; |
137 | int err; |
138 | |
139 | if (dim == LP3944_DIM0) |
140 | pwm_reg = LP3944_REG_PWM0; |
141 | else if (dim == LP3944_DIM1) |
142 | pwm_reg = LP3944_REG_PWM1; |
143 | else |
144 | return -EINVAL; |
145 | |
146 | /* Convert duty cycle to PWM value */ |
147 | if (duty_cycle > LP3944_DUTY_CYCLE_MAX) |
148 | return -EINVAL; |
149 | |
150 | pwm_value = (duty_cycle * 255) / LP3944_DUTY_CYCLE_MAX; |
151 | |
152 | err = lp3944_reg_write(client, reg: pwm_reg, value: pwm_value); |
153 | |
154 | return err; |
155 | } |
156 | |
157 | /** |
158 | * lp3944_led_set() - Set the led status |
159 | * |
160 | * @led: a lp3944_led_data structure |
161 | * @status: one of LP3944_LED_STATUS_OFF |
162 | * LP3944_LED_STATUS_ON |
163 | * LP3944_LED_STATUS_DIM0 |
164 | * LP3944_LED_STATUS_DIM1 |
165 | */ |
166 | static int lp3944_led_set(struct lp3944_led_data *led, u8 status) |
167 | { |
168 | struct lp3944_data *data = i2c_get_clientdata(client: led->client); |
169 | u8 id = led->id; |
170 | u8 reg; |
171 | u8 val = 0; |
172 | int err; |
173 | |
174 | dev_dbg(&led->client->dev, "%s: %s, status before normalization:%d\n" , |
175 | __func__, led->ldev.name, status); |
176 | |
177 | switch (id) { |
178 | case LP3944_LED0: |
179 | case LP3944_LED1: |
180 | case LP3944_LED2: |
181 | case LP3944_LED3: |
182 | reg = LP3944_REG_LS0; |
183 | break; |
184 | case LP3944_LED4: |
185 | case LP3944_LED5: |
186 | case LP3944_LED6: |
187 | case LP3944_LED7: |
188 | id -= LP3944_LED4; |
189 | reg = LP3944_REG_LS1; |
190 | break; |
191 | default: |
192 | return -EINVAL; |
193 | } |
194 | |
195 | if (status > LP3944_LED_STATUS_DIM1) |
196 | return -EINVAL; |
197 | |
198 | /* |
199 | * Invert status only when it's < 2 (i.e. 0 or 1) which means it's |
200 | * controlling the on/off state directly. |
201 | * When, instead, status is >= 2 don't invert it because it would mean |
202 | * to mess with the hardware blinking mode. |
203 | */ |
204 | if (led->type == LP3944_LED_TYPE_LED_INVERTED && status < 2) |
205 | status = 1 - status; |
206 | |
207 | mutex_lock(&data->lock); |
208 | lp3944_reg_read(client: led->client, reg, value: &val); |
209 | |
210 | val &= ~(LP3944_LED_STATUS_MASK << (id << 1)); |
211 | val |= (status << (id << 1)); |
212 | |
213 | dev_dbg(&led->client->dev, "%s: %s, reg:%d id:%d status:%d val:%#x\n" , |
214 | __func__, led->ldev.name, reg, id, status, val); |
215 | |
216 | /* set led status */ |
217 | err = lp3944_reg_write(client: led->client, reg, value: val); |
218 | mutex_unlock(lock: &data->lock); |
219 | |
220 | return err; |
221 | } |
222 | |
223 | static int lp3944_led_set_blink(struct led_classdev *led_cdev, |
224 | unsigned long *delay_on, |
225 | unsigned long *delay_off) |
226 | { |
227 | struct lp3944_led_data *led = ldev_to_led(led_cdev); |
228 | u16 period; |
229 | u8 duty_cycle; |
230 | int err; |
231 | |
232 | /* units are in ms */ |
233 | if (*delay_on + *delay_off > LP3944_PERIOD_MAX) |
234 | return -EINVAL; |
235 | |
236 | if (*delay_on == 0 && *delay_off == 0) { |
237 | /* Special case: the leds subsystem requires a default user |
238 | * friendly blink pattern for the LED. Let's blink the led |
239 | * slowly (1Hz). |
240 | */ |
241 | *delay_on = 500; |
242 | *delay_off = 500; |
243 | } |
244 | |
245 | period = (*delay_on) + (*delay_off); |
246 | |
247 | /* duty_cycle is the percentage of period during which the led is ON */ |
248 | duty_cycle = 100 * (*delay_on) / period; |
249 | |
250 | /* invert duty cycle for inverted leds, this has the same effect of |
251 | * swapping delay_on and delay_off |
252 | */ |
253 | if (led->type == LP3944_LED_TYPE_LED_INVERTED) |
254 | duty_cycle = 100 - duty_cycle; |
255 | |
256 | /* NOTE: using always the first DIM mode, this means that all leds |
257 | * will have the same blinking pattern. |
258 | * |
259 | * We could find a way later to have two leds blinking in hardware |
260 | * with different patterns at the same time, falling back to software |
261 | * control for the other ones. |
262 | */ |
263 | err = lp3944_dim_set_period(client: led->client, LP3944_DIM0, period); |
264 | if (err) |
265 | return err; |
266 | |
267 | err = lp3944_dim_set_dutycycle(client: led->client, LP3944_DIM0, duty_cycle); |
268 | if (err) |
269 | return err; |
270 | |
271 | dev_dbg(&led->client->dev, "%s: OK hardware accelerated blink!\n" , |
272 | __func__); |
273 | |
274 | lp3944_led_set(led, status: LP3944_LED_STATUS_DIM0); |
275 | |
276 | return 0; |
277 | } |
278 | |
279 | static int lp3944_led_set_brightness(struct led_classdev *led_cdev, |
280 | enum led_brightness brightness) |
281 | { |
282 | struct lp3944_led_data *led = ldev_to_led(led_cdev); |
283 | |
284 | dev_dbg(&led->client->dev, "%s: %s, %d\n" , |
285 | __func__, led_cdev->name, brightness); |
286 | |
287 | return lp3944_led_set(led, status: !!brightness); |
288 | } |
289 | |
290 | static int lp3944_configure(struct i2c_client *client, |
291 | struct lp3944_data *data, |
292 | struct lp3944_platform_data *pdata) |
293 | { |
294 | int i, err = 0; |
295 | |
296 | for (i = 0; i < pdata->leds_size; i++) { |
297 | struct lp3944_led *pled = &pdata->leds[i]; |
298 | struct lp3944_led_data *led = &data->leds[i]; |
299 | led->client = client; |
300 | led->id = i; |
301 | |
302 | switch (pled->type) { |
303 | |
304 | case LP3944_LED_TYPE_LED: |
305 | case LP3944_LED_TYPE_LED_INVERTED: |
306 | led->type = pled->type; |
307 | led->ldev.name = pled->name; |
308 | led->ldev.max_brightness = 1; |
309 | led->ldev.brightness_set_blocking = |
310 | lp3944_led_set_brightness; |
311 | led->ldev.blink_set = lp3944_led_set_blink; |
312 | led->ldev.flags = LED_CORE_SUSPENDRESUME; |
313 | |
314 | err = led_classdev_register(parent: &client->dev, led_cdev: &led->ldev); |
315 | if (err < 0) { |
316 | dev_err(&client->dev, |
317 | "couldn't register LED %s\n" , |
318 | led->ldev.name); |
319 | goto exit; |
320 | } |
321 | |
322 | /* to expose the default value to userspace */ |
323 | led->ldev.brightness = |
324 | (enum led_brightness) pled->status; |
325 | |
326 | /* Set the default led status */ |
327 | err = lp3944_led_set(led, status: pled->status); |
328 | if (err < 0) { |
329 | dev_err(&client->dev, |
330 | "%s couldn't set STATUS %d\n" , |
331 | led->ldev.name, pled->status); |
332 | goto exit; |
333 | } |
334 | break; |
335 | |
336 | case LP3944_LED_TYPE_NONE: |
337 | default: |
338 | break; |
339 | |
340 | } |
341 | } |
342 | return 0; |
343 | |
344 | exit: |
345 | if (i > 0) |
346 | for (i = i - 1; i >= 0; i--) |
347 | switch (pdata->leds[i].type) { |
348 | |
349 | case LP3944_LED_TYPE_LED: |
350 | case LP3944_LED_TYPE_LED_INVERTED: |
351 | led_classdev_unregister(led_cdev: &data->leds[i].ldev); |
352 | break; |
353 | |
354 | case LP3944_LED_TYPE_NONE: |
355 | default: |
356 | break; |
357 | } |
358 | |
359 | return err; |
360 | } |
361 | |
362 | static int lp3944_probe(struct i2c_client *client) |
363 | { |
364 | struct lp3944_platform_data *lp3944_pdata = |
365 | dev_get_platdata(dev: &client->dev); |
366 | struct lp3944_data *data; |
367 | int err; |
368 | |
369 | if (lp3944_pdata == NULL) { |
370 | dev_err(&client->dev, "no platform data\n" ); |
371 | return -EINVAL; |
372 | } |
373 | |
374 | /* Let's see whether this adapter can support what we need. */ |
375 | if (!i2c_check_functionality(adap: client->adapter, |
376 | I2C_FUNC_SMBUS_BYTE_DATA)) { |
377 | dev_err(&client->dev, "insufficient functionality!\n" ); |
378 | return -ENODEV; |
379 | } |
380 | |
381 | data = devm_kzalloc(dev: &client->dev, size: sizeof(struct lp3944_data), |
382 | GFP_KERNEL); |
383 | if (!data) |
384 | return -ENOMEM; |
385 | |
386 | data->client = client; |
387 | i2c_set_clientdata(client, data); |
388 | |
389 | mutex_init(&data->lock); |
390 | |
391 | err = lp3944_configure(client, data, pdata: lp3944_pdata); |
392 | if (err < 0) |
393 | return err; |
394 | |
395 | dev_info(&client->dev, "lp3944 enabled\n" ); |
396 | return 0; |
397 | } |
398 | |
399 | static void lp3944_remove(struct i2c_client *client) |
400 | { |
401 | struct lp3944_platform_data *pdata = dev_get_platdata(dev: &client->dev); |
402 | struct lp3944_data *data = i2c_get_clientdata(client); |
403 | int i; |
404 | |
405 | for (i = 0; i < pdata->leds_size; i++) |
406 | switch (data->leds[i].type) { |
407 | case LP3944_LED_TYPE_LED: |
408 | case LP3944_LED_TYPE_LED_INVERTED: |
409 | led_classdev_unregister(led_cdev: &data->leds[i].ldev); |
410 | break; |
411 | |
412 | case LP3944_LED_TYPE_NONE: |
413 | default: |
414 | break; |
415 | } |
416 | } |
417 | |
418 | /* lp3944 i2c driver struct */ |
419 | static const struct i2c_device_id lp3944_id[] = { |
420 | {"lp3944" , 0}, |
421 | {} |
422 | }; |
423 | |
424 | MODULE_DEVICE_TABLE(i2c, lp3944_id); |
425 | |
426 | static struct i2c_driver lp3944_driver = { |
427 | .driver = { |
428 | .name = "lp3944" , |
429 | }, |
430 | .probe = lp3944_probe, |
431 | .remove = lp3944_remove, |
432 | .id_table = lp3944_id, |
433 | }; |
434 | |
435 | module_i2c_driver(lp3944_driver); |
436 | |
437 | MODULE_AUTHOR("Antonio Ospite <ospite@studenti.unina.it>" ); |
438 | MODULE_DESCRIPTION("LP3944 Fun Light Chip" ); |
439 | MODULE_LICENSE("GPL" ); |
440 | |