xref: /linux/drivers/platform/x86/amd/amd_halo_led.c (revision 5b05bb3f6c5716fab6911e12d60dd1f43ad9806a)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * AMD Halo Box RGB LED Driver
4  *
5  * Copyright (C) 2026 Advanced Micro Devices, Inc.
6  *
7  * This driver provides RGB LED control for AMD Halo Box devices through
8  * the LED multicolor subsystem. The Halo Box light bar can be controlled
9  * via sysfs to display any RGB color combination.
10  */
11 
12 #include <linux/array_size.h>
13 #include <linux/cleanup.h>
14 #include <linux/compiler_attributes.h>
15 #include <linux/container_of.h>
16 #include <linux/dev_printk.h>
17 #include <linux/device.h>
18 #include <linux/device/devres.h>
19 #include <linux/led-class-multicolor.h>
20 #include <linux/leds.h>
21 #include <linux/module.h>
22 #include <linux/mutex.h>
23 #include <linux/slab.h>
24 #include <linux/types.h>
25 #include <linux/wmi.h>
26 
27 #include <linux/byteorder/generic.h>
28 
29 #define AMD_HALO_GUID "081E747B-E028-4232-AF24-EAAEAB2B1E86"
30 
31 /* WMI method IDs from MOF */
32 enum {
33 	AMD_HALO_WMI_TURN_OFF	= 0x04,
34 	AMD_HALO_WMI_RGB	= 0x07,
35 };
36 
37 /* Arg0 of the AMD_HALO_WMI_RGB Method */
38 enum {
39 	AMD_HALO_RGB_CMD_GET	= 0x0,
40 	AMD_HALO_RGB_CMD_SET	= 0x1,
41 };
42 
43 /* Status codes from spec */
44 #define AMD_HALO_STATUS_SUCCESS		0x0000
45 #define AMD_HALO_STATUS_INVALID_PARAM	0xFFFD
46 
47 /* Brightness uses 0-100 range */
48 #define AMD_HALO_MAX_HW_BRIGHTNESS	100
49 
50 /**
51  * struct amd_halo_led_data - Driver private data
52  * @wdev: WMI device pointer
53  * @led_mc: LED multicolor class device
54  * @subled_info: RGB channel information
55  * @lock: Mutex to protect WMI calls
56  */
57 struct amd_halo_led_data {
58 	struct wmi_device *wdev;
59 	struct led_classdev_mc led_mc;
60 	struct mc_subled subled_info[3];
61 	struct mutex lock;
62 };
63 
64 struct amd_halo_wmi_args {
65 	__le32 arg0;
66 	__le32 arg1;
67 };
68 
69 struct amd_halo_wmi_args_rgb {
70 	__le32 cmd;
71 	__le32 red;
72 	__le32 green;
73 	__le32 blue;
74 };
75 
76 struct amd_halo_wmi_output_rgb {
77 	__le16 status;
78 	u8 red;
79 	u8 green;
80 	u8 blue;
81 } __packed;
82 
wmi_status_to_err(u16 status)83 static inline int wmi_status_to_err(u16 status)
84 {
85 	switch (status) {
86 	case AMD_HALO_STATUS_SUCCESS:
87 		return 0;
88 	case AMD_HALO_STATUS_INVALID_PARAM:
89 		return -EINVAL;
90 	default:
91 		return -EIO;
92 	}
93 }
94 
__amd_halo_wmi_call(struct wmi_device * wdev,u32 method_id,void * data,size_t length)95 static int __amd_halo_wmi_call(struct wmi_device *wdev,
96 			       u32 method_id, void *data, size_t length)
97 {
98 	struct wmi_buffer input = {
99 		.length = length,
100 		.data = data,
101 	};
102 	struct wmi_buffer output = { };
103 	int ret;
104 
105 	/* Return buffer per spec: Bytes[0:1] = Status (little-endian) */
106 	ret = wmidev_invoke_method(wdev, 0, method_id,
107 				   &input, &output, sizeof(__le16));
108 	if (ret)
109 		return ret;
110 
111 	__le16 *result_status __free(kfree) = output.data;
112 
113 	return wmi_status_to_err(le16_to_cpu(*result_status));
114 }
115 
116 /**
117  * amd_halo_wmi_turn_off - Turn off all LED channels
118  * @wdev: WMI device pointer
119  *
120  * Return: 0 on success, negative error code on failure
121  */
amd_halo_wmi_turn_off(struct wmi_device * wdev)122 static int amd_halo_wmi_turn_off(struct wmi_device *wdev)
123 {
124 	struct amd_halo_wmi_args args = { };
125 
126 	return __amd_halo_wmi_call(wdev, AMD_HALO_WMI_TURN_OFF, &args, sizeof(args));
127 }
128 
129 /**
130  * amd_halo_wmi_set_rgb - Set all RGB channels atomically
131  * @wdev: WMI device pointer
132  * @r: brightness for red channel (0 - 100)
133  * @g: brightness for green channel (0 - 100)
134  * @b: brightness for blue channel (0 - 100)
135  *
136  * Return: 0 on success, negative error code on failure
137  */
amd_halo_wmi_set_rgb(struct wmi_device * wdev,u32 r,u32 g,u32 b)138 static int amd_halo_wmi_set_rgb(struct wmi_device *wdev, u32 r, u32 g, u32 b)
139 {
140 	struct amd_halo_wmi_args_rgb args = {
141 		.cmd = cpu_to_le32(AMD_HALO_RGB_CMD_SET),
142 		.red = cpu_to_le32(r),
143 		.green = cpu_to_le32(g),
144 		.blue = cpu_to_le32(b),
145 	};
146 
147 	if (r > AMD_HALO_MAX_HW_BRIGHTNESS ||
148 	    g > AMD_HALO_MAX_HW_BRIGHTNESS ||
149 	    b > AMD_HALO_MAX_HW_BRIGHTNESS) {
150 		return -EINVAL;
151 	}
152 
153 	return __amd_halo_wmi_call(wdev, AMD_HALO_WMI_RGB, &args, sizeof(args));
154 }
155 
156 /**
157  * amd_halo_wmi_get_rgb - Get RGB values
158  * @wdev: WMI device pointer
159  * @r: output buffer for red value
160  * @g: output buffer for green value
161  * @b: output buffer for blue value
162  *
163  * Return: 0 on success, negative error code on failure
164  */
amd_halo_wmi_get_rgb(struct wmi_device * wdev,u8 * r,u8 * g,u8 * b)165 static int amd_halo_wmi_get_rgb(struct wmi_device *wdev, u8 *r, u8 *g, u8 *b)
166 {
167 	struct amd_halo_wmi_args_rgb args = {
168 		.cmd = cpu_to_le32(AMD_HALO_RGB_CMD_GET),
169 	};
170 	struct wmi_buffer input = {
171 		.length = sizeof(args),
172 		.data = &args,
173 	};
174 	struct wmi_buffer output = { };
175 	int ret;
176 
177 	ret = wmidev_invoke_method(wdev, 0, AMD_HALO_WMI_RGB,
178 				   &input, &output, sizeof(struct amd_halo_wmi_output_rgb));
179 	if (ret)
180 		return ret;
181 
182 	struct amd_halo_wmi_output_rgb *data __free(kfree) = output.data;
183 
184 	ret = wmi_status_to_err(le16_to_cpu(data->status));
185 	if (ret)
186 		return ret;
187 
188 	if (data->red > AMD_HALO_MAX_HW_BRIGHTNESS ||
189 	    data->green > AMD_HALO_MAX_HW_BRIGHTNESS ||
190 	    data->blue > AMD_HALO_MAX_HW_BRIGHTNESS) {
191 		return -EPROTO;
192 	}
193 
194 	*r = data->red;
195 	*g = data->green;
196 	*b = data->blue;
197 
198 	return 0;
199 }
200 
201 /**
202  * amd_halo_brightness_set - Set LED brightness
203  * @cdev: LED class device
204  * @brightness: Brightness value
205  *
206  * Return: 0 on success, negative error code on failure
207  */
amd_halo_brightness_set(struct led_classdev * cdev,enum led_brightness brightness)208 static int amd_halo_brightness_set(struct led_classdev *cdev,
209 				   enum led_brightness brightness)
210 {
211 	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
212 	struct amd_halo_led_data *data = container_of(mc_cdev,
213 						      struct amd_halo_led_data,
214 						      led_mc);
215 	u32 red_hw, green_hw, blue_hw;
216 	int ret;
217 
218 	guard(mutex)(&data->lock);
219 
220 	led_mc_calc_color_components(mc_cdev, brightness);
221 
222 	if (brightness == 0)
223 		return amd_halo_wmi_turn_off(data->wdev);
224 
225 	red_hw = mc_cdev->subled_info[0].brightness;
226 	green_hw = mc_cdev->subled_info[1].brightness;
227 	blue_hw = mc_cdev->subled_info[2].brightness;
228 
229 	ret = amd_halo_wmi_set_rgb(data->wdev, red_hw, green_hw, blue_hw);
230 	if (ret)
231 		goto out;
232 
233 	return 0;
234 
235 out:
236 	/*
237 	 * Consider the light bar non-functional if AMD_HALO_WMI_RGB failed.
238 	 * Attempt to turn the LED off completely as clean-up.
239 	 */
240 	if (amd_halo_wmi_turn_off(data->wdev))
241 		dev_warn_ratelimited(&data->wdev->dev, "Failed to turn LED off on cleanup\n");
242 
243 	return ret;
244 }
245 
amd_halo_probe(struct wmi_device * wdev,const void * context)246 static int amd_halo_probe(struct wmi_device *wdev, const void *context)
247 {
248 	struct led_init_data led_init_data = {
249 		.devicename = "amd_halo",
250 		.default_label = "multicolor:" LED_FUNCTION_STATUS,
251 		.devname_mandatory = true,
252 	};
253 	struct amd_halo_led_data *data;
254 	u8 r, g, b;
255 	int ret;
256 
257 	data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL);
258 	if (!data)
259 		return -ENOMEM;
260 
261 	ret = devm_mutex_init(&wdev->dev, &data->lock);
262 	if (ret)
263 		return ret;
264 
265 	data->wdev = wdev;
266 	dev_set_drvdata(&wdev->dev, data);
267 
268 	data->subled_info[0].color_index = LED_COLOR_ID_RED;
269 	data->subled_info[1].color_index = LED_COLOR_ID_GREEN;
270 	data->subled_info[2].color_index = LED_COLOR_ID_BLUE;
271 
272 	data->led_mc.led_cdev.brightness = AMD_HALO_MAX_HW_BRIGHTNESS;
273 	data->led_mc.led_cdev.max_brightness = AMD_HALO_MAX_HW_BRIGHTNESS;
274 	data->led_mc.led_cdev.brightness_set_blocking = amd_halo_brightness_set;
275 	data->led_mc.led_cdev.flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
276 	data->led_mc.num_colors = ARRAY_SIZE(data->subled_info);
277 	data->led_mc.subled_info = data->subled_info;
278 
279 	ret = amd_halo_wmi_get_rgb(wdev, &r, &g, &b);
280 	if (ret)
281 		return ret;
282 
283 	data->subled_info[0].intensity = r;
284 	data->subled_info[1].intensity = g;
285 	data->subled_info[2].intensity = b;
286 
287 	ret = devm_led_classdev_multicolor_register_ext(&wdev->dev, &data->led_mc,
288 							&led_init_data);
289 	if (ret)
290 		return dev_err_probe(&wdev->dev, ret,
291 				     "Failed to register multicolor LED\n");
292 	return 0;
293 }
294 
295 static const struct wmi_device_id amd_halo_id_table[] = {
296 	{ .guid_string = AMD_HALO_GUID },
297 	{ }
298 };
299 MODULE_DEVICE_TABLE(wmi, amd_halo_id_table);
300 
301 static struct wmi_driver amd_halo_driver = {
302 	.driver = {
303 		.name = "amd_halo_led",
304 	},
305 	.id_table = amd_halo_id_table,
306 	.probe = amd_halo_probe,
307 	.no_singleton = true,
308 };
309 
310 module_wmi_driver(amd_halo_driver);
311 
312 MODULE_AUTHOR("Mario Limonciello (AMD) <superm1@kernel.org>");
313 MODULE_AUTHOR("Yo-Jung Leo Lin (AMD) <Leo.Lin@amd.com>");
314 MODULE_DESCRIPTION("AMD Halo Box RGB LED Control Driver");
315 MODULE_LICENSE("GPL");
316