summaryrefslogtreecommitdiff
path: root/drivers/platform/x86/amd/amd_halo_led.c
blob: b21d956c7d96cfcf833261f7effc73b67b172ad2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// SPDX-License-Identifier: GPL-2.0
/*
 * AMD Halo Box RGB LED Driver
 *
 * Copyright (C) 2026 Advanced Micro Devices, Inc.
 *
 * This driver provides RGB LED control for AMD Halo Box devices through
 * the LED multicolor subsystem. The Halo Box light bar can be controlled
 * via sysfs to display any RGB color combination.
 */

#include <linux/array_size.h>
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/dev_printk.h>
#include <linux/device.h>
#include <linux/device/devres.h>
#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/wmi.h>

#include <linux/byteorder/generic.h>

#define AMD_HALO_GUID "081E747B-E028-4232-AF24-EAAEAB2B1E86"

/* WMI method IDs from MOF */
enum {
	AMD_HALO_WMI_TURN_OFF	= 0x04,
	AMD_HALO_WMI_RGB	= 0x07,
};

/* Arg0 of the AMD_HALO_WMI_RGB Method */
enum {
	AMD_HALO_RGB_CMD_GET	= 0x0,
	AMD_HALO_RGB_CMD_SET	= 0x1,
};

/* Status codes from spec */
#define AMD_HALO_STATUS_SUCCESS		0x0000
#define AMD_HALO_STATUS_INVALID_PARAM	0xFFFD

/* Brightness uses 0-100 range */
#define AMD_HALO_MAX_HW_BRIGHTNESS	100

/**
 * struct amd_halo_led_data - Driver private data
 * @wdev: WMI device pointer
 * @led_mc: LED multicolor class device
 * @subled_info: RGB channel information
 * @lock: Mutex to protect WMI calls
 */
struct amd_halo_led_data {
	struct wmi_device *wdev;
	struct led_classdev_mc led_mc;
	struct mc_subled subled_info[3];
	struct mutex lock;
};

struct amd_halo_wmi_args {
	__le32 arg0;
	__le32 arg1;
};

struct amd_halo_wmi_args_rgb {
	__le32 cmd;
	__le32 red;
	__le32 green;
	__le32 blue;
};

struct amd_halo_wmi_output_rgb {
	__le16 status;
	u8 red;
	u8 green;
	u8 blue;
} __packed;

static inline int wmi_status_to_err(u16 status)
{
	switch (status) {
	case AMD_HALO_STATUS_SUCCESS:
		return 0;
	case AMD_HALO_STATUS_INVALID_PARAM:
		return -EINVAL;
	default:
		return -EIO;
	}
}

static int __amd_halo_wmi_call(struct wmi_device *wdev,
			       u32 method_id, void *data, size_t length)
{
	struct wmi_buffer input = {
		.length = length,
		.data = data,
	};
	struct wmi_buffer output = { };
	int ret;

	/* Return buffer per spec: Bytes[0:1] = Status (little-endian) */
	ret = wmidev_invoke_method(wdev, 0, method_id,
				   &input, &output, sizeof(__le16));
	if (ret)
		return ret;

	__le16 *result_status __free(kfree) = output.data;

	return wmi_status_to_err(le16_to_cpu(*result_status));
}

/**
 * amd_halo_wmi_turn_off - Turn off all LED channels
 * @wdev: WMI device pointer
 *
 * Return: 0 on success, negative error code on failure
 */
static int amd_halo_wmi_turn_off(struct wmi_device *wdev)
{
	struct amd_halo_wmi_args args = { };

	return __amd_halo_wmi_call(wdev, AMD_HALO_WMI_TURN_OFF, &args, sizeof(args));
}

/**
 * amd_halo_wmi_set_rgb - Set all RGB channels atomically
 * @wdev: WMI device pointer
 * @r: brightness for red channel (0 - 100)
 * @g: brightness for green channel (0 - 100)
 * @b: brightness for blue channel (0 - 100)
 *
 * Return: 0 on success, negative error code on failure
 */
static int amd_halo_wmi_set_rgb(struct wmi_device *wdev, u32 r, u32 g, u32 b)
{
	struct amd_halo_wmi_args_rgb args = {
		.cmd = cpu_to_le32(AMD_HALO_RGB_CMD_SET),
		.red = cpu_to_le32(r),
		.green = cpu_to_le32(g),
		.blue = cpu_to_le32(b),
	};

	if (r > AMD_HALO_MAX_HW_BRIGHTNESS ||
	    g > AMD_HALO_MAX_HW_BRIGHTNESS ||
	    b > AMD_HALO_MAX_HW_BRIGHTNESS) {
		return -EINVAL;
	}

	return __amd_halo_wmi_call(wdev, AMD_HALO_WMI_RGB, &args, sizeof(args));
}

/**
 * amd_halo_wmi_get_rgb - Get RGB values
 * @wdev: WMI device pointer
 * @r: output buffer for red value
 * @g: output buffer for green value
 * @b: output buffer for blue value
 *
 * Return: 0 on success, negative error code on failure
 */
static int amd_halo_wmi_get_rgb(struct wmi_device *wdev, u8 *r, u8 *g, u8 *b)
{
	struct amd_halo_wmi_args_rgb args = {
		.cmd = cpu_to_le32(AMD_HALO_RGB_CMD_GET),
	};
	struct wmi_buffer input = {
		.length = sizeof(args),
		.data = &args,
	};
	struct wmi_buffer output = { };
	int ret;

	ret = wmidev_invoke_method(wdev, 0, AMD_HALO_WMI_RGB,
				   &input, &output, sizeof(struct amd_halo_wmi_output_rgb));
	if (ret)
		return ret;

	struct amd_halo_wmi_output_rgb *data __free(kfree) = output.data;

	ret = wmi_status_to_err(le16_to_cpu(data->status));
	if (ret)
		return ret;

	if (data->red > AMD_HALO_MAX_HW_BRIGHTNESS ||
	    data->green > AMD_HALO_MAX_HW_BRIGHTNESS ||
	    data->blue > AMD_HALO_MAX_HW_BRIGHTNESS) {
		return -EPROTO;
	}

	*r = data->red;
	*g = data->green;
	*b = data->blue;

	return 0;
}

/**
 * amd_halo_brightness_set - Set LED brightness
 * @cdev: LED class device
 * @brightness: Brightness value
 *
 * Return: 0 on success, negative error code on failure
 */
static int amd_halo_brightness_set(struct led_classdev *cdev,
				   enum led_brightness brightness)
{
	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
	struct amd_halo_led_data *data = container_of(mc_cdev,
						      struct amd_halo_led_data,
						      led_mc);
	u32 red_hw, green_hw, blue_hw;
	int ret;

	guard(mutex)(&data->lock);

	led_mc_calc_color_components(mc_cdev, brightness);

	if (brightness == 0)
		return amd_halo_wmi_turn_off(data->wdev);

	red_hw = mc_cdev->subled_info[0].brightness;
	green_hw = mc_cdev->subled_info[1].brightness;
	blue_hw = mc_cdev->subled_info[2].brightness;

	ret = amd_halo_wmi_set_rgb(data->wdev, red_hw, green_hw, blue_hw);
	if (ret)
		goto out;

	return 0;

out:
	/*
	 * Consider the light bar non-functional if AMD_HALO_WMI_RGB failed.
	 * Attempt to turn the LED off completely as clean-up.
	 */
	if (amd_halo_wmi_turn_off(data->wdev))
		dev_warn_ratelimited(&data->wdev->dev, "Failed to turn LED off on cleanup\n");

	return ret;
}

static int amd_halo_probe(struct wmi_device *wdev, const void *context)
{
	struct led_init_data led_init_data = {
		.devicename = "amd_halo",
		.default_label = "multicolor:" LED_FUNCTION_STATUS,
		.devname_mandatory = true,
	};
	struct amd_halo_led_data *data;
	u8 r, g, b;
	int ret;

	data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL);
	if (!data)
		return -ENOMEM;

	ret = devm_mutex_init(&wdev->dev, &data->lock);
	if (ret)
		return ret;

	data->wdev = wdev;
	dev_set_drvdata(&wdev->dev, data);

	data->subled_info[0].color_index = LED_COLOR_ID_RED;
	data->subled_info[1].color_index = LED_COLOR_ID_GREEN;
	data->subled_info[2].color_index = LED_COLOR_ID_BLUE;

	data->led_mc.led_cdev.brightness = AMD_HALO_MAX_HW_BRIGHTNESS;
	data->led_mc.led_cdev.max_brightness = AMD_HALO_MAX_HW_BRIGHTNESS;
	data->led_mc.led_cdev.brightness_set_blocking = amd_halo_brightness_set;
	data->led_mc.led_cdev.flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
	data->led_mc.num_colors = ARRAY_SIZE(data->subled_info);
	data->led_mc.subled_info = data->subled_info;

	ret = amd_halo_wmi_get_rgb(wdev, &r, &g, &b);
	if (ret)
		return ret;

	data->subled_info[0].intensity = r;
	data->subled_info[1].intensity = g;
	data->subled_info[2].intensity = b;

	ret = devm_led_classdev_multicolor_register_ext(&wdev->dev, &data->led_mc,
							&led_init_data);
	if (ret)
		return dev_err_probe(&wdev->dev, ret,
				     "Failed to register multicolor LED\n");
	return 0;
}

static const struct wmi_device_id amd_halo_id_table[] = {
	{ .guid_string = AMD_HALO_GUID },
	{ }
};
MODULE_DEVICE_TABLE(wmi, amd_halo_id_table);

static struct wmi_driver amd_halo_driver = {
	.driver = {
		.name = "amd_halo_led",
	},
	.id_table = amd_halo_id_table,
	.probe = amd_halo_probe,
	.no_singleton = true,
};

module_wmi_driver(amd_halo_driver);

MODULE_AUTHOR("Mario Limonciello (AMD) <superm1@kernel.org>");
MODULE_AUTHOR("Yo-Jung Leo Lin (AMD) <Leo.Lin@amd.com>");
MODULE_DESCRIPTION("AMD Halo Box RGB LED Control Driver");
MODULE_LICENSE("GPL");