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