1 | // SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause) |
2 | // |
3 | // Copyright (c) 2018 Mellanox Technologies. All rights reserved. |
4 | // Copyright (c) 2018 Vadim Pasternak <vadimp@mellanox.com> |
5 | |
6 | #include <linux/bitops.h> |
7 | #include <linux/device.h> |
8 | #include <linux/io.h> |
9 | #include <linux/leds.h> |
10 | #include <linux/module.h> |
11 | #include <linux/platform_data/mlxreg.h> |
12 | #include <linux/platform_device.h> |
13 | #include <linux/regmap.h> |
14 | |
15 | /* Codes for LEDs. */ |
16 | #define MLXREG_LED_OFFSET_BLINK_3HZ 0x01 /* Offset from solid: 3Hz blink */ |
17 | #define MLXREG_LED_OFFSET_BLINK_6HZ 0x02 /* Offset from solid: 6Hz blink */ |
18 | #define MLXREG_LED_IS_OFF 0x00 /* Off */ |
19 | #define MLXREG_LED_RED_SOLID 0x05 /* Solid red */ |
20 | #define MLXREG_LED_GREEN_SOLID 0x0D /* Solid green */ |
21 | #define MLXREG_LED_AMBER_SOLID 0x09 /* Solid amber */ |
22 | #define MLXREG_LED_BLINK_3HZ 167 /* ~167 msec off/on - HW support */ |
23 | #define MLXREG_LED_BLINK_6HZ 83 /* ~83 msec off/on - HW support */ |
24 | #define MLXREG_LED_CAPABILITY_CLEAR GENMASK(31, 8) /* Clear mask */ |
25 | |
26 | /** |
27 | * struct mlxreg_led_data - led control data: |
28 | * |
29 | * @data: led configuration data; |
30 | * @led_cdev: led class data; |
31 | * @base_color: base led color (other colors have constant offset from base); |
32 | * @data_parent: pointer to private device control data of parent; |
33 | * @led_cdev_name: class device name |
34 | */ |
35 | struct mlxreg_led_data { |
36 | struct mlxreg_core_data *data; |
37 | struct led_classdev led_cdev; |
38 | u8 base_color; |
39 | void *data_parent; |
40 | char led_cdev_name[MLXREG_CORE_LABEL_MAX_SIZE]; |
41 | }; |
42 | |
43 | #define cdev_to_priv(c) container_of(c, struct mlxreg_led_data, led_cdev) |
44 | |
45 | /** |
46 | * struct mlxreg_led_priv_data - platform private data: |
47 | * |
48 | * @pdev: platform device; |
49 | * @pdata: platform data; |
50 | * @access_lock: mutex for attribute IO access; |
51 | */ |
52 | struct mlxreg_led_priv_data { |
53 | struct platform_device *pdev; |
54 | struct mlxreg_core_platform_data *pdata; |
55 | struct mutex access_lock; /* protect IO operations */ |
56 | }; |
57 | |
58 | static int |
59 | mlxreg_led_store_hw(struct mlxreg_led_data *led_data, u8 vset) |
60 | { |
61 | struct mlxreg_led_priv_data *priv = led_data->data_parent; |
62 | struct mlxreg_core_platform_data *led_pdata = priv->pdata; |
63 | struct mlxreg_core_data *data = led_data->data; |
64 | u32 regval; |
65 | u32 nib; |
66 | int ret; |
67 | |
68 | /* |
69 | * Each LED is controlled through low or high nibble of the relevant |
70 | * register byte. Register offset is specified by off parameter. |
71 | * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, |
72 | * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink |
73 | * green. |
74 | * Parameter mask specifies which nibble is used for specific LED: mask |
75 | * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - |
76 | * higher nibble (bits from 4 to 7). |
77 | */ |
78 | mutex_lock(&priv->access_lock); |
79 | |
80 | ret = regmap_read(map: led_pdata->regmap, reg: data->reg, val: ®val); |
81 | if (ret) |
82 | goto access_error; |
83 | |
84 | nib = (ror32(word: data->mask, shift: data->bit) == 0xf0) ? rol32(word: vset, shift: data->bit) : |
85 | rol32(word: vset, shift: data->bit + 4); |
86 | regval = (regval & data->mask) | nib; |
87 | |
88 | ret = regmap_write(map: led_pdata->regmap, reg: data->reg, val: regval); |
89 | |
90 | access_error: |
91 | mutex_unlock(lock: &priv->access_lock); |
92 | |
93 | return ret; |
94 | } |
95 | |
96 | static enum led_brightness |
97 | mlxreg_led_get_hw(struct mlxreg_led_data *led_data) |
98 | { |
99 | struct mlxreg_led_priv_data *priv = led_data->data_parent; |
100 | struct mlxreg_core_platform_data *led_pdata = priv->pdata; |
101 | struct mlxreg_core_data *data = led_data->data; |
102 | u32 regval; |
103 | int err; |
104 | |
105 | /* |
106 | * Each LED is controlled through low or high nibble of the relevant |
107 | * register byte. Register offset is specified by off parameter. |
108 | * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, |
109 | * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink |
110 | * green. |
111 | * Parameter mask specifies which nibble is used for specific LED: mask |
112 | * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - |
113 | * higher nibble (bits from 4 to 7). |
114 | */ |
115 | err = regmap_read(map: led_pdata->regmap, reg: data->reg, val: ®val); |
116 | if (err < 0) { |
117 | dev_warn(led_data->led_cdev.dev, "Failed to get current brightness, error: %d\n" , |
118 | err); |
119 | /* Assume the LED is OFF */ |
120 | return LED_OFF; |
121 | } |
122 | |
123 | regval = regval & ~data->mask; |
124 | regval = (ror32(word: data->mask, shift: data->bit) == 0xf0) ? ror32(word: regval, |
125 | shift: data->bit) : ror32(word: regval, shift: data->bit + 4); |
126 | if (regval >= led_data->base_color && |
127 | regval <= (led_data->base_color + MLXREG_LED_OFFSET_BLINK_6HZ)) |
128 | return LED_FULL; |
129 | |
130 | return LED_OFF; |
131 | } |
132 | |
133 | static int |
134 | mlxreg_led_brightness_set(struct led_classdev *cled, enum led_brightness value) |
135 | { |
136 | struct mlxreg_led_data *led_data = cdev_to_priv(cled); |
137 | |
138 | if (value) |
139 | return mlxreg_led_store_hw(led_data, vset: led_data->base_color); |
140 | else |
141 | return mlxreg_led_store_hw(led_data, MLXREG_LED_IS_OFF); |
142 | } |
143 | |
144 | static enum led_brightness |
145 | mlxreg_led_brightness_get(struct led_classdev *cled) |
146 | { |
147 | struct mlxreg_led_data *led_data = cdev_to_priv(cled); |
148 | |
149 | return mlxreg_led_get_hw(led_data); |
150 | } |
151 | |
152 | static int |
153 | mlxreg_led_blink_set(struct led_classdev *cled, unsigned long *delay_on, |
154 | unsigned long *delay_off) |
155 | { |
156 | struct mlxreg_led_data *led_data = cdev_to_priv(cled); |
157 | int err; |
158 | |
159 | /* |
160 | * HW supports two types of blinking: full (6Hz) and half (3Hz). |
161 | * For delay on/off zero LED is setting to solid color. For others |
162 | * combination blinking is to be controlled by the software timer. |
163 | */ |
164 | if (!(*delay_on == 0 && *delay_off == 0) && |
165 | !(*delay_on == MLXREG_LED_BLINK_3HZ && |
166 | *delay_off == MLXREG_LED_BLINK_3HZ) && |
167 | !(*delay_on == MLXREG_LED_BLINK_6HZ && |
168 | *delay_off == MLXREG_LED_BLINK_6HZ)) |
169 | return -EINVAL; |
170 | |
171 | if (*delay_on == MLXREG_LED_BLINK_6HZ) |
172 | err = mlxreg_led_store_hw(led_data, vset: led_data->base_color + |
173 | MLXREG_LED_OFFSET_BLINK_6HZ); |
174 | else if (*delay_on == MLXREG_LED_BLINK_3HZ) |
175 | err = mlxreg_led_store_hw(led_data, vset: led_data->base_color + |
176 | MLXREG_LED_OFFSET_BLINK_3HZ); |
177 | else |
178 | err = mlxreg_led_store_hw(led_data, vset: led_data->base_color); |
179 | |
180 | return err; |
181 | } |
182 | |
183 | static int mlxreg_led_config(struct mlxreg_led_priv_data *priv) |
184 | { |
185 | struct mlxreg_core_platform_data *led_pdata = priv->pdata; |
186 | struct mlxreg_core_data *data = led_pdata->data; |
187 | struct mlxreg_led_data *led_data; |
188 | struct led_classdev *led_cdev; |
189 | enum led_brightness brightness; |
190 | u32 regval; |
191 | int i; |
192 | int err; |
193 | |
194 | for (i = 0; i < led_pdata->counter; i++, data++) { |
195 | led_data = devm_kzalloc(dev: &priv->pdev->dev, size: sizeof(*led_data), |
196 | GFP_KERNEL); |
197 | if (!led_data) |
198 | return -ENOMEM; |
199 | |
200 | if (data->capability) { |
201 | err = regmap_read(map: led_pdata->regmap, reg: data->capability, |
202 | val: ®val); |
203 | if (err) { |
204 | dev_err(&priv->pdev->dev, "Failed to query capability register\n" ); |
205 | return err; |
206 | } |
207 | if (!(regval & data->bit)) |
208 | continue; |
209 | /* |
210 | * Field "bit" can contain one capability bit in 0 byte |
211 | * and offset bit in 1-3 bytes. Clear capability bit and |
212 | * keep only offset bit. |
213 | */ |
214 | data->bit &= MLXREG_LED_CAPABILITY_CLEAR; |
215 | } |
216 | |
217 | led_cdev = &led_data->led_cdev; |
218 | led_data->data_parent = priv; |
219 | if (strstr(data->label, "red" ) || |
220 | strstr(data->label, "orange" )) { |
221 | brightness = LED_OFF; |
222 | led_data->base_color = MLXREG_LED_RED_SOLID; |
223 | } else if (strstr(data->label, "amber" )) { |
224 | brightness = LED_OFF; |
225 | led_data->base_color = MLXREG_LED_AMBER_SOLID; |
226 | } else { |
227 | brightness = LED_OFF; |
228 | led_data->base_color = MLXREG_LED_GREEN_SOLID; |
229 | } |
230 | snprintf(buf: led_data->led_cdev_name, size: sizeof(led_data->led_cdev_name), |
231 | fmt: "mlxreg:%s" , data->label); |
232 | led_cdev->name = led_data->led_cdev_name; |
233 | led_cdev->brightness = brightness; |
234 | led_cdev->max_brightness = LED_ON; |
235 | led_cdev->brightness_set_blocking = |
236 | mlxreg_led_brightness_set; |
237 | led_cdev->brightness_get = mlxreg_led_brightness_get; |
238 | led_cdev->blink_set = mlxreg_led_blink_set; |
239 | led_cdev->flags = LED_CORE_SUSPENDRESUME; |
240 | led_data->data = data; |
241 | err = devm_led_classdev_register(parent: &priv->pdev->dev, led_cdev); |
242 | if (err) |
243 | return err; |
244 | |
245 | if (led_cdev->brightness) |
246 | mlxreg_led_brightness_set(cled: led_cdev, |
247 | value: led_cdev->brightness); |
248 | dev_info(led_cdev->dev, "label: %s, mask: 0x%02x, offset:0x%02x\n" , |
249 | data->label, data->mask, data->reg); |
250 | } |
251 | |
252 | return 0; |
253 | } |
254 | |
255 | static int mlxreg_led_probe(struct platform_device *pdev) |
256 | { |
257 | struct mlxreg_core_platform_data *led_pdata; |
258 | struct mlxreg_led_priv_data *priv; |
259 | |
260 | led_pdata = dev_get_platdata(dev: &pdev->dev); |
261 | if (!led_pdata) { |
262 | dev_err(&pdev->dev, "Failed to get platform data.\n" ); |
263 | return -EINVAL; |
264 | } |
265 | |
266 | priv = devm_kzalloc(dev: &pdev->dev, size: sizeof(*priv), GFP_KERNEL); |
267 | if (!priv) |
268 | return -ENOMEM; |
269 | |
270 | mutex_init(&priv->access_lock); |
271 | priv->pdev = pdev; |
272 | priv->pdata = led_pdata; |
273 | |
274 | return mlxreg_led_config(priv); |
275 | } |
276 | |
277 | static void mlxreg_led_remove(struct platform_device *pdev) |
278 | { |
279 | struct mlxreg_led_priv_data *priv = dev_get_drvdata(dev: &pdev->dev); |
280 | |
281 | mutex_destroy(lock: &priv->access_lock); |
282 | } |
283 | |
284 | static struct platform_driver mlxreg_led_driver = { |
285 | .driver = { |
286 | .name = "leds-mlxreg" , |
287 | }, |
288 | .probe = mlxreg_led_probe, |
289 | .remove_new = mlxreg_led_remove, |
290 | }; |
291 | |
292 | module_platform_driver(mlxreg_led_driver); |
293 | |
294 | MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>" ); |
295 | MODULE_DESCRIPTION("Mellanox LED regmap driver" ); |
296 | MODULE_LICENSE("Dual BSD/GPL" ); |
297 | MODULE_ALIAS("platform:leds-mlxreg" ); |
298 | |