| // 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"); |