From 421dc1560d72f409e6a6b76418e4031ac9bd3851 Mon Sep 17 00:00:00 2001 From: Eric Naim Date: Mon, 12 Jan 2026 14:20:23 +0700 Subject: [PATCH] handheld Signed-off-by: Eric Naim --- .../ABI/testing/sysfs-driver-hid-lenovo-go | 724 +++ .../ABI/testing/sysfs-driver-hid-lenovo-go-s | 304 + .../wmi/devices/msi-wmi-platform.rst | 26 + MAINTAINERS | 9 + drivers/bus/mhi/host/boot.c | 15 +- drivers/bus/mhi/host/init.c | 18 +- drivers/bus/mhi/host/internal.h | 2 + drivers/bus/mhi/host/pm.c | 1 + drivers/extcon/Kconfig | 7 + drivers/extcon/Makefile | 1 + drivers/extcon/extcon-steamdeck.c | 180 + drivers/gpu/drm/amd/amdgpu/amdgpu_mode.h | 2 - .../gpu/drm/amd/amdgpu/atombios_encoders.c | 10 +- .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c | 5 +- drivers/gpu/drm/amd/display/dc/dc.h | 2 +- .../amd/display/dc/hwss/dce110/dce110_hwseq.c | 9 + .../drm/amd/display/dc/link/link_validation.c | 11 + .../dc/resource/dcn301/dcn301_resource.c | 6 +- .../gpu/drm/drm_panel_orientation_quirks.c | 54 + drivers/hid/Kconfig | 39 + drivers/hid/Makefile | 7 + drivers/hid/hid-asus-ally.c | 2215 +++++++ drivers/hid/hid-asus-ally.h | 398 ++ drivers/hid/hid-asus.c | 29 +- drivers/hid/hid-asus.h | 13 + drivers/hid/hid-core.c | 5 + drivers/hid/hid-ids.h | 13 + drivers/hid/hid-input.c | 2 + drivers/hid/hid-lenovo-go-s.c | 1577 +++++ drivers/hid/hid-lenovo-go.c | 2399 ++++++++ drivers/hid/hid-msi-claw.c | 842 +++ drivers/hid/zotac-zone-hid/Kconfig | 8 + drivers/hid/zotac-zone-hid/Makefile | 6 + .../zotac-zone-hid/zotac-zone-hid-config.c | 2231 +++++++ .../hid/zotac-zone-hid/zotac-zone-hid-core.c | 597 ++ .../hid/zotac-zone-hid/zotac-zone-hid-input.c | 522 ++ .../hid/zotac-zone-hid/zotac-zone-hid-rgb.c | 717 +++ drivers/hid/zotac-zone-hid/zotac-zone.h | 214 + drivers/hwmon/Kconfig | 11 + drivers/hwmon/Makefile | 1 + drivers/hwmon/steamdeck-hwmon.c | 294 + drivers/input/joystick/xpad.c | 4 + drivers/leds/Kconfig | 7 + drivers/leds/Makefile | 1 + drivers/leds/leds-steamdeck.c | 123 + drivers/mfd/Kconfig | 11 + drivers/mfd/Makefile | 2 + drivers/mfd/steamdeck.c | 147 + drivers/net/wireless/ath/ath11k/core.c | 2 +- drivers/net/wireless/ath/ath11k/mhi.c | 8 +- drivers/net/wireless/ath/ath12k/mhi.c | 14 +- drivers/platform/x86/Kconfig | 13 + drivers/platform/x86/Makefile | 2 + drivers/platform/x86/asus-wmi.c | 6 +- drivers/platform/x86/msi-wmi-platform.c | 1176 +++- drivers/platform/x86/zotac-zone-platform.c | 1122 ++++ drivers/usb/usbip/vhci_hcd.c | 33 +- drivers/video/backlight/backlight.c | 2 + include/linux/device.h | 46 + include/linux/hid.h | 1 + include/linux/mhi.h | 2 + sound/hda/codecs/realtek/alc269.c | 59 + sound/soc/amd/acp/acp-mach.h | 2 +- sound/soc/codecs/Kconfig | 2 + sound/soc/codecs/Makefile | 1 + sound/soc/codecs/aw87xxx/Kconfig | 5 + sound/soc/codecs/aw87xxx/Makefile | 4 + sound/soc/codecs/aw87xxx/aw87xxx.c | 1601 +++++ sound/soc/codecs/aw87xxx/aw87xxx.h | 130 + sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.c | 1558 +++++ sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.h | 191 + sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.c | 515 ++ sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.h | 73 + sound/soc/codecs/aw87xxx/aw87xxx_device.c | 977 +++ sound/soc/codecs/aw87xxx/aw87xxx_device.h | 149 + sound/soc/codecs/aw87xxx/aw87xxx_dsp.c | 355 ++ sound/soc/codecs/aw87xxx/aw87xxx_dsp.h | 65 + sound/soc/codecs/aw87xxx/aw87xxx_log.h | 33 + sound/soc/codecs/aw87xxx/aw87xxx_monitor.c | 1208 ++++ sound/soc/codecs/aw87xxx/aw87xxx_monitor.h | 96 + sound/soc/codecs/aw87xxx/aw87xxx_pid_18_reg.h | 2315 ++++++++ sound/soc/codecs/aw87xxx/aw87xxx_pid_39_reg.h | 67 + .../codecs/aw87xxx/aw87xxx_pid_59_3x9_reg.h | 93 + .../codecs/aw87xxx/aw87xxx_pid_59_5x9_reg.h | 94 + sound/soc/codecs/aw87xxx/aw87xxx_pid_5a_reg.h | 4124 +++++++++++++ sound/soc/codecs/aw87xxx/aw87xxx_pid_60_reg.h | 5246 +++++++++++++++++ sound/soc/codecs/aw87xxx/aw87xxx_pid_76_reg.h | 1205 ++++ sound/soc/codecs/aw87xxx/aw87xxx_pid_9b_reg.h | 81 + sound/soc/codecs/max98388.c | 24 +- 89 files changed, 36395 insertions(+), 126 deletions(-) create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s create mode 100644 drivers/extcon/extcon-steamdeck.c create mode 100644 drivers/hid/hid-asus-ally.c create mode 100644 drivers/hid/hid-asus-ally.h create mode 100644 drivers/hid/hid-asus.h create mode 100644 drivers/hid/hid-lenovo-go-s.c create mode 100644 drivers/hid/hid-lenovo-go.c create mode 100644 drivers/hid/hid-msi-claw.c create mode 100644 drivers/hid/zotac-zone-hid/Kconfig create mode 100644 drivers/hid/zotac-zone-hid/Makefile create mode 100644 drivers/hid/zotac-zone-hid/zotac-zone-hid-config.c create mode 100644 drivers/hid/zotac-zone-hid/zotac-zone-hid-core.c create mode 100644 drivers/hid/zotac-zone-hid/zotac-zone-hid-input.c create mode 100644 drivers/hid/zotac-zone-hid/zotac-zone-hid-rgb.c create mode 100644 drivers/hid/zotac-zone-hid/zotac-zone.h create mode 100644 drivers/hwmon/steamdeck-hwmon.c create mode 100644 drivers/leds/leds-steamdeck.c create mode 100644 drivers/mfd/steamdeck.c create mode 100644 drivers/platform/x86/zotac-zone-platform.c create mode 100644 sound/soc/codecs/aw87xxx/Kconfig create mode 100644 sound/soc/codecs/aw87xxx/Makefile create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_device.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_device.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_dsp.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_dsp.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_log.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_monitor.c create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_monitor.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_18_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_39_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_59_3x9_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_59_5x9_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_5a_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_60_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_76_reg.h create mode 100644 sound/soc/codecs/aw87xxx/aw87xxx_pid_9b_reg.h diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go new file mode 100644 index 000000000000..4e298567ac40 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go @@ -0,0 +1,724 @@ ++What: /sys/class/leds/go:rgb:joystick_rings/effect ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the display effect of the RGB interface. ++ ++ Values are monocolor, breathe, chroma, or rainbow. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/effect_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the effect attribute. ++ ++ Values are monocolor, breathe, chroma, or rainbow. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the RGB interface. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the enabled attribute. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the operating mode of the RGB interface. ++ ++ Values are dynamic or custom. Custom allows setting the RGB effect and color. ++ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently ++ supported under Linux. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the mode attribute. ++ ++ Values are dynamic or custom. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/profile ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls selecting the configured RGB profile. ++ ++ Values are 1-3. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/profile_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the profile attribute. ++ ++ Values are 1-3. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/speed ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the change rate for the breathe, chroma, and rainbow effects. ++ ++ Values are 0-100. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/class/leds/go:rgb:joystick_rings/speed_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the speed attribute. ++ ++ Values are 0-100. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./firmware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the firmware version of the internal MCU. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./fps_mode_dpi ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the DPI of the right handle when the FPS mode switch is on. ++ ++ Values are 500, 800, 1200, and 1800. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./fps_mode_dpi_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the fps_mode_dpi attribute. ++ ++ Values are 500, 800, 1200, and 1800. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./hardware_generation ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware generation of the internal MCU. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./hardware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware version of the internal MCU. ++ ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/auto_sleep_time ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the sleep timer due to inactivity for the left removable controller. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/auto_sleep_time_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/auto_sleep_time attribute. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_gyro ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the left removable controller's IMU. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_gyro_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/calibrate_gyro attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_gyro_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the left removable controller's IMU. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_joystick ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the left removable controller's joystick. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_joystick_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/calibrate_jotstick attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_joystick_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the left removable controller's joystick. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_tirgger ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the left removable controller's trigger. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_gyro_trigger ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/calibrate_trigger attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/calibrate_trigger_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the left removable controller's trigger. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/firmware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the left removable controller's firmware version. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/hardware_generation ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware generation of the left removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/hardware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware version of the left removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/imu_bypass_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU bypass function of the left removable controller. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/imu_bypass_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/imu_bypass_enabled attribute. ++ ++ Values are true or false. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/imu_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU of the left removable controller. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/imu_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/imu_enabled attribute. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/product_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the product version of the left removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/protocol_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the protocol version of the left removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/reset ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: Resets the left removable controller to factory defaults. ++ ++ Writing 1 to this path initiates. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/rumble_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls setting the response behavior for rumble events for the left removable controller. ++ ++ Values are fps, racing, standarg, spg, rpg. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/rumble_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/rumble_mode attribute. ++ ++ Values are fps, racing, standarg, spg, rpg. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/rumble_notification ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling haptic rumble events for the left removable controller. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/rumble_notification_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the left_handle/rumble_notification attribute. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the operating mode of the built-in controller. ++ ++ Values are xinput or dinput. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./left_handle/mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the mode attribute. ++ ++ Values are xinput or dinput. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./os_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the behavior of built in chord combinations. ++ ++ Values are windows or linux. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./os_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the os_mode attribute. ++ ++ Values are windows or linux. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./product_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the product version of the internal MCU. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/protocol_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the protocol version of the internal MCU. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./reset_mcu ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: Resets the internal MCU to factory defaults. ++ ++ Writing 1 to this path initiates. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/auto_sleep_time ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the sleep timer due to inactivity for the right removable controller. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/auto_sleep_time_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/auto_sleep_time attribute. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_gyro ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the right removable controller's IMU. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_gyro_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/calibrate_gyro attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_gyro_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the right removable controller's IMU. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_joystick ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the right removable controller's joystick. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_joystick_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/calibrate_jotstick attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_joystick_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the right removable controller's joystick. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_tirgger ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This initiates or halts calibration of the right removable controller's trigger. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_gyro_trigger ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/calibrate_trigger attribute. ++ ++ Values are start, stop. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/calibrate_trigger_status ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the result of the last attempted calibration of the right removable controller's trigger. ++ ++ Values are unknown, success, failure. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/firmware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the right removable controller's firmware version. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/hardware_generation ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware generation of the right removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/hardware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware version of the right removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/imu_bypass_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU bypass function of the right removable controller. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/imu_bypass_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/imu_bypass_enabled attribute. ++ ++ Values are true or false. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/imu_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU of the right removable controller. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/imu_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/imu_enabled attribute. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/product_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the product version of the right removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/protocol_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the protocol version of the right removable controller. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/reset ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: Resets the right removable controller to factory defaults. ++ ++ Writing 1 to this path initiates. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/rumble_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls setting the response behavior for rumble events for the right removable controller. ++ ++ Values are fps, racing, standarg, spg, rpg. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/rumble_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/rumble_mode attribute. ++ ++ Values are fps, racing, standarg, spg, rpg. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/rumble_notification ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling haptic rumble events for the right removable controller. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./right_handle/rumble_notification_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the right_handle/rumble_notification attribute. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./rumble_intensity ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls setting the rumble intensity for both removable controllers. ++ ++ Values are off, low, medium, high. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./rumble_intensity_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the rumble_intensity attribute. ++ ++ Values are off, low, medium, high. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the touchpad. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/enabled attribute. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/vibration_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling haptic rumble events for the touchpad. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/vibration_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/vibration_enabled attribute. ++ ++ Values are true, false. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/vibration_intensity ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls setting the intensity of the touchpad haptics. ++ ++ Values are off, low, medium, high. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/vibration_intensity_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/vibration_intensity attribute. ++ ++ Values are off, low, medium, high. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./tx_dongle/firmware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the firmware version of the internal wireless transmission dongle. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./tx_dongle/hardware_generation ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware generation of the internal wireless transmission dongle. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./tx_dongle/hardware_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the hardware version of the internal wireless transmission dongle. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./tx_dongle/product_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the product version of the internal wireless transmission dongle. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./tx_dongle/protocol_version ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the protocol version of the internal wireless transmission dongle. ++ ++ Applies to Lenovo Legion Go and Go 2 line of handheld devices. ++ diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s new file mode 100644 index 000000000000..c3c7b0918986 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s @@ -0,0 +1,304 @@ ++What: /sys/class/leds/go_s:rgb:joystick_rings/effect ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the display effect of the RGB interface. ++ ++ Values are monocolor, breathe, chroma, or rainbow. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/effect_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the effect attribute. ++ ++ Values are monocolor, breathe, chroma, or rainbow. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the RGB interface. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the enabled attribute. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the operating mode of the RGB interface. ++ ++ Values are dynamic or custom. Custom allows setting the RGB effect and color. ++ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently ++ supported under Linux. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the mode attribute. ++ ++ Values are dynamic or custom. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/profile ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls selecting the configured RGB profile. ++ ++ Values are 1-3. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/profile_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the profile attribute. ++ ++ Values are 1-3. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/speed ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the change rate for the breathe, chroma, and rainbow effects. ++ ++ Values are 0-100. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/class/leds/go_s:rgb:joystick_rings/speed_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the speed attribute. ++ ++ Values are 0-100. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/auto_sleep_time ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the sleep timer due to inactivity for the built-in controller. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/auto_sleep_time_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the gamepad/auto_sleep_time attribute. ++ ++ Values are 0-255. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/dpad_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the operating mode of the built-in controllers D-pad. ++ ++ Values are 4-way or 8-way. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/dpad_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the gamepad/dpad_mode attribute. ++ ++ Values are 4-way or 8-way. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the operating mode of the built-in controller. ++ ++ Values are xinput or dinput. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the gamepad/mode attribute. ++ ++ Values are xinput or dinput. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/poll_rate ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls the poll rate in Hz of the built-in controller. ++ ++ Values are 125, 250, 500, or 1000. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./gamepad/poll_rate_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the gamepad/poll_rate attribute. ++ ++ Values are 125, 250, 500, or 1000. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./imu/bypass_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU bypass function. When enabled the IMU data is directly reported to the OS through ++an HIDRAW interface. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./imu/bypass_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the imu/bypass_enabled attribute. ++ ++ Values are true or false. ++ ++What: /sys/bus/usb/devices/-:./::./imu/manufacturer ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the manufacturer of the intertial measurment unit. ++ ++ Values are Bosch or ST. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./imu/sensor_enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the IMU. ++ ++ Values are true, false, or wake-2s. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./imu/sensor_enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the imu/sensor_enabled attribute. ++ ++ Values are true, false, or wake-2s. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./mcu_id ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the MCU Identification Number ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./mouse/step ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls which value is used for the mouse sensitivity. ++ ++ Values are 1-127. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./mouse/step_range ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the mouse/step attribute. ++ ++ Values are 1-127. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./os_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls which value is used for the touchpads operating mode. ++ ++ Values are windows or linux. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./os_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the os_mode attribute. ++ ++ Values are windows or linux. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/enabled ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls enabling or disabling the built-in touchpad. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/enabled_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/enabled attribute. ++ ++ Values are true or false. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/linux_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls behavior of the touchpad events when os_mode is set to linux. ++ ++ Values are absolute or relative. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/linux_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/linux_mode attribute. ++ ++ Values are absolute or relative. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/windows_mode ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This controls behavior of the touchpad events when os_mode is set to windows. ++ ++ Values are absolute or relative. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. ++ ++What: /sys/bus/usb/devices/-:./::./touchpad/windows_mode_index ++Date: April 2026 ++Contact: linux-input@vger.kernel.org ++Description: This displays the available options for the touchpad/windows_mode attribute. ++ ++ Values are absolute or relative. ++ ++ Applies to Lenovo Legion Go S line of handheld devices. diff --git a/Documentation/wmi/devices/msi-wmi-platform.rst b/Documentation/wmi/devices/msi-wmi-platform.rst index 73197b31926a..704bfdac5203 100644 --- a/Documentation/wmi/devices/msi-wmi-platform.rst +++ b/Documentation/wmi/devices/msi-wmi-platform.rst @@ -169,6 +169,32 @@ The fan RPM readings can be calculated with the following formula: If the fan speed reading is zero, then the fan RPM is zero too. +The subfeature ``0x01`` is used to retrieve the fan speed table for the CPU fan. The output +data contains the fan speed table and two bytes with unknown data. The fan speed table +consists of six 8-bit entries, each containing a fan speed value in percent. + +The subfeature ``0x02`` is used tho retrieve the same data for the GPU fan. + +WMI method Set_Fan() +-------------------- + +The fan speed tables can be accessed using subfeature ``0x01`` (CPU fan) and subfeature ``0x02`` +(GPU fan). The input data has the same format as the output data of the ``Get_Fan`` WMI method. + +WMI method Get_AP() +------------------- + +The current fan mode can be accessed using subfeature ``0x01``. The output data contains a flag +byte and two bytes of unknown data. If the 7th bit inside the flag byte is cleared then all fans +are operating in automatic mode, otherwise the fans operate based on the fan speed tables +accessible thru the ``Get_Fan``/``Set_Fan`` WMI methods. + +WMI method Set_AP() +------------------- + +The current fan mode can be changed using subfeature ``0x01``. The input data has the same format +as the output data of the ``Get_AP`` WMI method. + WMI method Get_WMI() -------------------- diff --git a/MAINTAINERS b/MAINTAINERS index fcd0e94b1c14..4b300d8702e5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14328,6 +14328,15 @@ L: platform-driver-x86@vger.kernel.org S: Maintained F: drivers/platform/x86/lenovo/wmi-hotkey-utilities.c +LENOVO HID drivers +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go +F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s +F: drivers/hid/hid-lenovo-go-s.c +F: drivers/hid/hid-lenovo-go.c + LETSKETCH HID TABLET DRIVER M: Hans de Goede L: linux-input@vger.kernel.org diff --git a/drivers/bus/mhi/host/boot.c b/drivers/bus/mhi/host/boot.c index 205d83ac069f..b3a85aa3c476 100644 --- a/drivers/bus/mhi/host/boot.c +++ b/drivers/bus/mhi/host/boot.c @@ -584,10 +584,17 @@ void mhi_fw_load_handler(struct mhi_controller *mhi_cntrl) * device transitioning into MHI READY state */ if (fw_load_type == MHI_FW_LOAD_FBC) { - ret = mhi_alloc_bhie_table(mhi_cntrl, &mhi_cntrl->fbc_image, fw_sz); - if (ret) { - release_firmware(firmware); - goto error_fw_load; + if (mhi_cntrl->fbc_image && fw_sz != mhi_cntrl->prev_fw_sz) { + mhi_free_bhie_table(mhi_cntrl, mhi_cntrl->fbc_image); + mhi_cntrl->fbc_image = NULL; + } + if (!mhi_cntrl->fbc_image) { + ret = mhi_alloc_bhie_table(mhi_cntrl, &mhi_cntrl->fbc_image, fw_sz); + if (ret) { + release_firmware(firmware); + goto error_fw_load; + } + mhi_cntrl->prev_fw_sz = fw_sz; } /* Load the firmware into BHIE vec table */ diff --git a/drivers/bus/mhi/host/init.c b/drivers/bus/mhi/host/init.c index 099be8dd1900..5a21c8f9b105 100644 --- a/drivers/bus/mhi/host/init.c +++ b/drivers/bus/mhi/host/init.c @@ -1172,8 +1172,9 @@ int mhi_prepare_for_power_up(struct mhi_controller *mhi_cntrl) /* * Allocate RDDM table for debugging purpose if specified */ - mhi_alloc_bhie_table(mhi_cntrl, &mhi_cntrl->rddm_image, - mhi_cntrl->rddm_size); + if (!mhi_cntrl->rddm_image) + mhi_alloc_bhie_table(mhi_cntrl, &mhi_cntrl->rddm_image, + mhi_cntrl->rddm_size); if (mhi_cntrl->rddm_image) { ret = mhi_rddm_prepare(mhi_cntrl, mhi_cntrl->rddm_image); @@ -1199,6 +1200,14 @@ int mhi_prepare_for_power_up(struct mhi_controller *mhi_cntrl) } EXPORT_SYMBOL_GPL(mhi_prepare_for_power_up); +void __mhi_unprepare_keep_dev(struct mhi_controller *mhi_cntrl) +{ + mhi_cntrl->bhi = NULL; + mhi_cntrl->bhie = NULL; + + mhi_deinit_dev_ctxt(mhi_cntrl); +} + void mhi_unprepare_after_power_down(struct mhi_controller *mhi_cntrl) { if (mhi_cntrl->fbc_image) { @@ -1211,10 +1220,7 @@ void mhi_unprepare_after_power_down(struct mhi_controller *mhi_cntrl) mhi_cntrl->rddm_image = NULL; } - mhi_cntrl->bhi = NULL; - mhi_cntrl->bhie = NULL; - - mhi_deinit_dev_ctxt(mhi_cntrl); + __mhi_unprepare_keep_dev(mhi_cntrl); } EXPORT_SYMBOL_GPL(mhi_unprepare_after_power_down); diff --git a/drivers/bus/mhi/host/internal.h b/drivers/bus/mhi/host/internal.h index 7937bb1f742c..22251fc3da37 100644 --- a/drivers/bus/mhi/host/internal.h +++ b/drivers/bus/mhi/host/internal.h @@ -423,4 +423,6 @@ void mhi_unmap_single_no_bb(struct mhi_controller *mhi_cntrl, void mhi_unmap_single_use_bb(struct mhi_controller *mhi_cntrl, struct mhi_buf_info *buf_info); +void __mhi_unprepare_keep_dev(struct mhi_controller *mhi_cntrl); + #endif /* _MHI_INT_H */ diff --git a/drivers/bus/mhi/host/pm.c b/drivers/bus/mhi/host/pm.c index b4ef115189b5..ae17a7299479 100644 --- a/drivers/bus/mhi/host/pm.c +++ b/drivers/bus/mhi/host/pm.c @@ -1271,6 +1271,7 @@ void mhi_power_down_keep_dev(struct mhi_controller *mhi_cntrl, bool graceful) { __mhi_power_down(mhi_cntrl, graceful, false); + __mhi_unprepare_keep_dev(mhi_cntrl); } EXPORT_SYMBOL_GPL(mhi_power_down_keep_dev); diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig index aec46bf03302..2b26545df5eb 100644 --- a/drivers/extcon/Kconfig +++ b/drivers/extcon/Kconfig @@ -227,4 +227,11 @@ config EXTCON_RTK_TYPE_C The DHC (Digital Home Hub) RTD series SoC contains a type c module. This driver will detect the status of the type-c port. +config EXTCON_STEAMDECK + tristate "Steam Deck extcon support" + depends on MFD_STEAMDECK + help + Say Y here to enable support of USB Type C cable detection extcon + support on Steam Deck devices + endif diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile index 6482f2bfd661..770dceb94c1d 100644 --- a/drivers/extcon/Makefile +++ b/drivers/extcon/Makefile @@ -28,3 +28,4 @@ obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o obj-$(CONFIG_EXTCON_USBC_TUSB320) += extcon-usbc-tusb320.o obj-$(CONFIG_EXTCON_RTK_TYPE_C) += extcon-rtk-type-c.o +obj-$(CONFIG_EXTCON_STEAMDECK) += extcon-steamdeck.o diff --git a/drivers/extcon/extcon-steamdeck.c b/drivers/extcon/extcon-steamdeck.c new file mode 100644 index 000000000000..49cb5d30bf6b --- /dev/null +++ b/drivers/extcon/extcon-steamdeck.c @@ -0,0 +1,180 @@ + +#include +#include +#include + +#define ACPI_STEAMDECK_NOTIFY_STATUS 0x80 + +/* 0 - port connected, 1 -port disconnected */ +#define ACPI_STEAMDECK_PORT_CONNECT BIT(0) +/* 0 - Upstream Facing Port, 1 - Downdstream Facing Port */ +#define ACPI_STEAMDECK_CUR_DATA_ROLE BIT(3) +/* + * Debouncing delay to allow negotiation process to settle. 2s value + * was arrived at via trial and error. + */ +#define STEAMDECK_ROLE_SWITCH_DELAY (msecs_to_jiffies(2000)) + +struct steamdeck_extcon { + struct acpi_device *adev; + struct delayed_work role_work; + struct extcon_dev *edev; + struct device *dev; +}; + +static int steamdeck_read_pdcs(struct steamdeck_extcon *sd, unsigned long long *pdcs) +{ + acpi_status status; + + status = acpi_evaluate_integer(sd->adev->handle, "PDCS", NULL, pdcs); + if (ACPI_FAILURE(status)) { + dev_err(sd->dev, "PDCS evaluation failed: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + return 0; +} + +static void steamdeck_usb_role_work(struct work_struct *work) +{ + struct steamdeck_extcon *sd = + container_of(work, struct steamdeck_extcon, role_work.work); + unsigned long long pdcs; + bool usb_host; + + if (steamdeck_read_pdcs(sd, &pdcs)) + return; + + /* + * We only care about these two + */ + pdcs &= ACPI_STEAMDECK_PORT_CONNECT | ACPI_STEAMDECK_CUR_DATA_ROLE; + + /* + * For "connect" events our role is determined by a bit in + * PDCS, for "disconnect" we switch to being a gadget + * unconditionally. The thinking for the latter is we don't + * want to start acting as a USB host until we get + * confirmation from the firmware that we are a USB host + */ + usb_host = (pdcs & ACPI_STEAMDECK_PORT_CONNECT) ? + pdcs & ACPI_STEAMDECK_CUR_DATA_ROLE : false; + + dev_dbg(sd->dev, "USB role is %s\n", usb_host ? "host" : "device"); + WARN_ON(extcon_set_state_sync(sd->edev, EXTCON_USB_HOST, + usb_host)); + +} + +static void steamdeck_notify(acpi_handle handle, u32 event, void *context) +{ + struct device *dev = context; + struct steamdeck_extcon *sd = dev_get_drvdata(dev); + unsigned long long pdcs; + unsigned long delay; + + switch (event) { + case ACPI_STEAMDECK_NOTIFY_STATUS: + if (steamdeck_read_pdcs(sd, &pdcs)) + return; + /* + * We process "disconnect" events immediately and + * "connect" events with a delay to give the HW time + * to settle. For example attaching USB hub (at least + * for HW used for testing) will generate intermediary + * event with "host" bit not set, followed by the one + * that does have it set. + */ + delay = (pdcs & ACPI_STEAMDECK_PORT_CONNECT) ? + STEAMDECK_ROLE_SWITCH_DELAY : 0; + + queue_delayed_work(system_long_wq, &sd->role_work, delay); + break; + default: + dev_warn(dev, "Unsupported event [0x%x]\n", event); + } +} + +static void steamdeck_remove_notify_handler(void *data) +{ + struct steamdeck_extcon *sd = data; + + acpi_remove_notify_handler(sd->adev->handle, ACPI_DEVICE_NOTIFY, + steamdeck_notify); + cancel_delayed_work_sync(&sd->role_work); +} + +static const unsigned int steamdeck_extcon_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_CHG_USB_SDP, + EXTCON_CHG_USB_CDP, + EXTCON_CHG_USB_DCP, + EXTCON_CHG_USB_ACA, + EXTCON_NONE, +}; + +static int steamdeck_extcon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct steamdeck_extcon *sd; + acpi_status status; + int ret; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + + INIT_DELAYED_WORK(&sd->role_work, steamdeck_usb_role_work); + platform_set_drvdata(pdev, sd); + sd->adev = ACPI_COMPANION(dev->parent); + sd->dev = dev; + sd->edev = devm_extcon_dev_allocate(dev, steamdeck_extcon_cable); + if (IS_ERR(sd->edev)) + return PTR_ERR(sd->edev); + + ret = devm_extcon_dev_register(dev, sd->edev); + if (ret < 0) { + dev_err(dev, "Failed to register extcon device: %d\n", ret); + return ret; + } + + /* + * Set initial role value + */ + queue_delayed_work(system_long_wq, &sd->role_work, 0); + flush_delayed_work(&sd->role_work); + + status = acpi_install_notify_handler(sd->adev->handle, + ACPI_DEVICE_NOTIFY, + steamdeck_notify, + dev); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Error installing ACPI notify handler\n"); + return -EIO; + } + + ret = devm_add_action_or_reset(dev, steamdeck_remove_notify_handler, + sd); + return ret; +} + +static const struct platform_device_id steamdeck_extcon_id_table[] = { + { .name = "steamdeck-extcon" }, + {} +}; +MODULE_DEVICE_TABLE(platform, steamdeck_extcon_id_table); + +static struct platform_driver steamdeck_extcon_driver = { + .probe = steamdeck_extcon_probe, + .driver = { + .name = "steamdeck-extcon", + }, + .id_table = steamdeck_extcon_id_table, +}; +module_platform_driver(steamdeck_extcon_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck extcon driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_mode.h b/drivers/gpu/drm/amd/amdgpu/amdgpu_mode.h index dc8d2f52c7d6..28d5ff2c109b 100644 --- a/drivers/gpu/drm/amd/amdgpu/amdgpu_mode.h +++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_mode.h @@ -437,8 +437,6 @@ struct amdgpu_mode_info { struct drm_property *regamma_tf_property; }; -#define AMDGPU_MAX_BL_LEVEL 0xFF - struct amdgpu_backlight_privdata { struct amdgpu_encoder *encoder; uint8_t negative; diff --git a/drivers/gpu/drm/amd/amdgpu/atombios_encoders.c b/drivers/gpu/drm/amd/amdgpu/atombios_encoders.c index a51f3414b65d..bc0f9759c5c5 100644 --- a/drivers/gpu/drm/amd/amdgpu/atombios_encoders.c +++ b/drivers/gpu/drm/amd/amdgpu/atombios_encoders.c @@ -39,6 +39,10 @@ #include #include "bif/bif_4_1_d.h" + +/* Maximum backlight level. */ +#define AMDGPU_ATOM_MAX_BL_LEVEL 0xFF + u8 amdgpu_atombios_encoder_get_backlight_level_from_reg(struct amdgpu_device *adev) { @@ -127,8 +131,8 @@ static u8 amdgpu_atombios_encoder_backlight_level(struct backlight_device *bd) /* Convert brightness to hardware level */ if (bd->props.brightness < 0) level = 0; - else if (bd->props.brightness > AMDGPU_MAX_BL_LEVEL) - level = AMDGPU_MAX_BL_LEVEL; + else if (bd->props.brightness > AMDGPU_ATOM_MAX_BL_LEVEL) + level = AMDGPU_ATOM_MAX_BL_LEVEL; else level = bd->props.brightness; @@ -198,7 +202,7 @@ void amdgpu_atombios_encoder_init_backlight(struct amdgpu_encoder *amdgpu_encode } memset(&props, 0, sizeof(props)); - props.max_brightness = AMDGPU_MAX_BL_LEVEL; + props.max_brightness = AMDGPU_ATOM_MAX_BL_LEVEL; props.type = BACKLIGHT_RAW; snprintf(bl_name, sizeof(bl_name), "amdgpu_bl%d", dev->primary->index); diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c index ed8860aa7074..4bd2a143c2e3 100644 --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c @@ -158,6 +158,9 @@ MODULE_FIRMWARE(FIRMWARE_DCN_401_DMUB); /* Number of bytes in PSP footer for firmware. */ #define PSP_FOOTER_BYTES 0x100 +/* Maximum backlight level. */ +#define AMDGPU_MAX_BL_LEVEL 0xFFFF + /** * DOC: overview * @@ -9876,7 +9879,7 @@ static void amdgpu_dm_commit_planes(struct drm_atomic_state *state, int planes_count = 0, vpos, hpos; unsigned long flags; u32 target_vblank, last_flip_vblank; - bool vrr_active = amdgpu_dm_crtc_vrr_active(acrtc_state); + bool vrr_active = true;//amdgpu_dm_crtc_vrr_active(acrtc_state); bool cursor_update = false; bool pflip_present = false; bool dirty_rects_changed = false; diff --git a/drivers/gpu/drm/amd/display/dc/dc.h b/drivers/gpu/drm/amd/display/dc/dc.h index 29edfa51ea2c..94147fcf16c2 100644 --- a/drivers/gpu/drm/amd/display/dc/dc.h +++ b/drivers/gpu/drm/amd/display/dc/dc.h @@ -68,7 +68,7 @@ struct dcn_dccg_reg_state; /** * MAX_SURFACES - representative of the upper bound of surfaces that can be piped to a single CRTC */ -#define MAX_SURFACES 4 +#define MAX_SURFACES 6 /** * MAX_PLANES - representative of the upper bound of planes that are supported by the HW */ diff --git a/drivers/gpu/drm/amd/display/dc/hwss/dce110/dce110_hwseq.c b/drivers/gpu/drm/amd/display/dc/hwss/dce110/dce110_hwseq.c index ebd74b43e935..a1f3a45f4325 100644 --- a/drivers/gpu/drm/amd/display/dc/hwss/dce110/dce110_hwseq.c +++ b/drivers/gpu/drm/amd/display/dc/hwss/dce110/dce110_hwseq.c @@ -96,6 +96,8 @@ #define FN(reg_name, field_name) \ hws->shifts->field_name, hws->masks->field_name +static const uint8_t DP_SINK_BRANCH_DEV_NAME_KT50X0[] = "KT50X0!"; + struct dce110_hw_seq_reg_offsets { uint32_t crtc; }; @@ -3327,6 +3329,13 @@ void dce110_enable_dp_link_output( link->dc->res_pool->dp_clock_source; const struct link_hwss *link_hwss = get_link_hwss(link, link_res); unsigned int i; + if (link->ctx->dce_version == DCN_VERSION_3_01 && + link->dpcd_caps.sink_dev_id == DP_BRANCH_DEVICE_ID_0060AD && + memcmp(&link->dpcd_caps.branch_dev_name, + DP_SINK_BRANCH_DEV_NAME_KT50X0, + sizeof(link->dpcd_caps.branch_dev_name)) == 0) { + msleep(2000); + } /* * Add the logic to extract BOTH power up and power down sequences diff --git a/drivers/gpu/drm/amd/display/dc/link/link_validation.c b/drivers/gpu/drm/amd/display/dc/link/link_validation.c index acdc162de535..53b7ea0daba4 100644 --- a/drivers/gpu/drm/amd/display/dc/link/link_validation.c +++ b/drivers/gpu/drm/amd/display/dc/link/link_validation.c @@ -35,6 +35,8 @@ #define DC_LOGGER_INIT(logger) +static const uint8_t DP_SINK_BRANCH_DEV_NAME_KT50X0[] = "KT50X0!"; + static uint32_t get_tmds_output_pixel_clock_100hz(const struct dc_crtc_timing *timing) { @@ -292,6 +294,15 @@ static bool dp_validate_mode_timing( timing->v_addressable == (uint32_t) 480) return true; + if (link->ctx->dce_version == DCN_VERSION_3_01 && + link->dpcd_caps.sink_dev_id == DP_BRANCH_DEVICE_ID_0060AD && + memcmp(&link->dpcd_caps.branch_dev_name, + DP_SINK_BRANCH_DEV_NAME_KT50X0, + sizeof(link->dpcd_caps.branch_dev_name)) == 0) { + if (timing->pix_clk_100hz / 10 >= (uint32_t) 1200000) + return false; /* KT50X0 does not support Pxl clock >= 1200MHz */ + } + link_setting = dp_get_verified_link_cap(link); /* TODO: DYNAMIC_VALIDATION needs to be implemented */ diff --git a/drivers/gpu/drm/amd/display/dc/resource/dcn301/dcn301_resource.c b/drivers/gpu/drm/amd/display/dc/resource/dcn301/dcn301_resource.c index 3ad6a3d4858e..18d2a81070b7 100644 --- a/drivers/gpu/drm/amd/display/dc/resource/dcn301/dcn301_resource.c +++ b/drivers/gpu/drm/amd/display/dc/resource/dcn301/dcn301_resource.c @@ -1440,9 +1440,9 @@ static bool dcn301_resource_construct( dc->caps.max_cursor_size = 256; dc->caps.min_horizontal_blanking_period = 80; dc->caps.dmdata_alloc_size = 2048; - dc->caps.max_slave_planes = 2; - dc->caps.max_slave_yuv_planes = 2; - dc->caps.max_slave_rgb_planes = 2; + dc->caps.max_slave_planes = 3; + dc->caps.max_slave_yuv_planes = 3; + dc->caps.max_slave_rgb_planes = 3; dc->caps.is_apu = true; dc->caps.post_blend_color_processing = true; dc->caps.force_dp_tps4_for_cp2520 = true; diff --git a/drivers/gpu/drm/drm_panel_orientation_quirks.c b/drivers/gpu/drm/drm_panel_orientation_quirks.c index 3a218fb592ce..178adc19e670 100644 --- a/drivers/gpu/drm/drm_panel_orientation_quirks.c +++ b/drivers/gpu/drm/drm_panel_orientation_quirks.c @@ -203,6 +203,24 @@ static const struct dmi_system_id orientation_data[] = { DMI_MATCH(DMI_PRODUCT_NAME, "AYANEO 2"), }, .driver_data = (void *)&lcd1200x1920_rightside_up, + }, { /* AYA NEO AYANEO 2S */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "AYANEO 2S"), + }, + .driver_data = (void *)&lcd1200x1920_rightside_up, + }, { /* AYANEO 3 */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "AYANEO 3"), + }, + .driver_data = (void *)&lcd1080x1920_rightside_up, + }, { /* AYA NEO FLIP DS */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "FLIP DS"), + }, + .driver_data = (void *)&lcd1080x1920_leftside_up, }, { /* AYA NEO 2021 */ .matches = { DMI_EXACT_MATCH(DMI_SYS_VENDOR, "AYADEVICE"), @@ -486,6 +504,42 @@ static const struct dmi_system_id orientation_data[] = { DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONE XPLAYER"), }, .driver_data = (void *)&lcd1200x1920_leftside_up, + }, { /* OneXPlayer X1 AMD */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER X1 A"), + }, + .driver_data = (void *)&lcd1600x2560_leftside_up, + }, { /* OneXPlayer X1 AMD Strix Point */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER X1Pro"), + }, + .driver_data = (void *)&lcd1600x2560_leftside_up, + }, { /* OneXPlayer X1 mini (AMD) */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER X1 mini"), + }, + .driver_data = (void *)&lcd1600x2560_leftside_up, + }, { /* OneXPlayer OneXFly F1 Pro (OLED) */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER F1Pro"), + }, + .driver_data = (void *)&lcd1080x1920_leftside_up, + }, { /* OneXPlayer OneXFly F1 Pro (OLED) LE Red variant */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER F1 EVA-02"), + }, + .driver_data = (void *)&lcd1080x1920_leftside_up, + }, { /* Zotac Gaming Zone (OLED) */ + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ZOTAC"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "ZOTAC GAMING ZONE"), + }, + .driver_data = (void *)&lcd1080x1920_leftside_up, }, { /* OrangePi Neo */ .matches = { DMI_EXACT_MATCH(DMI_SYS_VENDOR, "OrangePi"), diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index ecb3059c68fb..c721270c29ba 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -202,6 +202,15 @@ config HID_ASUS - GL553V series - GL753V series +config HID_ASUS_ALLY + tristate "Asus Ally gamepad configuration support" + depends on USB_HID + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR + select POWER_SUPPLY + help + Support for configuring the Asus ROG Ally gamepad using attributes. + config HID_AUREAL tristate "Aureal" help @@ -622,6 +631,30 @@ config HID_LENOVO - ThinkPad Compact Bluetooth Keyboard with TrackPoint (supports Fn keys) - ThinkPad Compact USB Keyboard with TrackPoint (supports Fn keys) +config HID_LENOVO_GO + tristate "HID Driver for Lenovo Legion Go Series Controllers" + depends on USB_HID + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + Support for Lenovo Legion Go devices with detachable controllers. + + Say Y here to include configuration interface support for the Lenovo Legion Go + and Legion Go 2 Handheld Console Controllers. Say M here to compile this + driver as a module. The module will be called hid-lenovo-go. + +config HID_LENOVO_GO_S + tristate "HID Driver for Lenovo Legion Go S Controller" + depends on USB_HID + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + Support for Lenovo Legion Go S Handheld Console Controller. + + Say Y here to include configuration interface support for the Lenovo Legion Go + S. Say M here to compile this driver as a module. The module will be called + hid-lenovo-go-s. + config HID_LETSKETCH tristate "Letsketch WP9620N tablets" depends on USB_HID @@ -1430,6 +1463,10 @@ config HID_KUNIT_TEST If in doubt, say "N". +config HID_MSI_CLAW + tristate "MSI Claw series gamepad support (work-in-progress)" + depends on USB_HID + endmenu source "drivers/hid/bpf/Kconfig" @@ -1454,4 +1491,6 @@ source "drivers/hid/spi-hid/Kconfig" source "drivers/hid/dockchannel-hid/Kconfig" +source "drivers/hid/zotac-zone-hid/Kconfig" + endif # HID_SUPPORT diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 397b6cf5b6c3..4fb469b3b7e9 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -34,6 +34,7 @@ obj-$(CONFIG_HID_APPLETB_BL) += hid-appletb-bl.o obj-$(CONFIG_HID_APPLETB_KBD) += hid-appletb-kbd.o obj-$(CONFIG_HID_CREATIVE_SB0540) += hid-creative-sb0540.o obj-$(CONFIG_HID_ASUS) += hid-asus.o +obj-$(CONFIG_HID_ASUS_ALLY) += hid-asus-ally.o obj-$(CONFIG_HID_AUREAL) += hid-aureal.o obj-$(CONFIG_HID_BELKIN) += hid-belkin.o obj-$(CONFIG_HID_BETOP_FF) += hid-betopff.o @@ -76,6 +77,8 @@ obj-$(CONFIG_HID_KYE) += hid-kye.o obj-$(CONFIG_HID_KYSONA) += hid-kysona.o obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o +obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o +obj-$(CONFIG_HID_LENOVO_GO_S) += hid-lenovo-go-s.o obj-$(CONFIG_HID_LETSKETCH) += hid-letsketch.o obj-$(CONFIG_HID_LOGITECH) += hid-logitech.o obj-$(CONFIG_HID_LOGITECH) += hid-lg-g15.o @@ -177,6 +180,10 @@ obj-$(CONFIG_SPI_HID_APPLE_CORE) += spi-hid/ obj-$(CONFIG_HID_DOCKCHANNEL) += dockchannel-hid/ +obj-$(CONFIG_ZOTAC_ZONE_HID) += zotac-zone-hid/ + obj-$(CONFIG_SURFACE_HID_CORE) += surface-hid/ obj-$(CONFIG_INTEL_THC_HID) += intel-thc-hid/ + +obj-$(CONFIG_HID_MSI_CLAW) += hid-msi-claw.o diff --git a/drivers/hid/hid-asus-ally.c b/drivers/hid/hid-asus-ally.c new file mode 100644 index 000000000000..8b51560fdbcf --- /dev/null +++ b/drivers/hid/hid-asus-ally.c @@ -0,0 +1,2215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for Asus ROG laptops and Ally + * + * Copyright (c) 2023 Luke Jones + */ + +#include "linux/compiler_attributes.h" +#include "linux/device.h" +#include +#include +#include "linux/pm.h" +#include "linux/printk.h" +#include "linux/slab.h" +#include +#include +#include +#include +#include + +#include "hid-ids.h" +#include "hid-asus.h" +#include "hid-asus-ally.h" + +#define DEBUG + +#define READY_MAX_TRIES 3 +#define FEATURE_REPORT_ID 0x0d +#define FEATURE_ROG_ALLY_REPORT_ID 0x5a +#define FEATURE_ROG_ALLY_CODE_PAGE 0xD1 +#define FEATURE_ROG_ALLY_REPORT_SIZE 64 +#define ALLY_X_INPUT_REPORT_USB 0x0B +#define ALLY_X_INPUT_REPORT_USB_SIZE 16 + +#define ROG_ALLY_REPORT_SIZE 64 +#define ROG_ALLY_X_MIN_MCU 313 +#define ROG_ALLY_MIN_MCU 319 + +#define FEATURE_KBD_LED_REPORT_ID1 0x5d +#define FEATURE_KBD_LED_REPORT_ID2 0x5e + +#define BTN_DATA_LEN 11; +#define BTN_CODE_BYTES_LEN 8 + +static const u8 EC_INIT_STRING[] = { 0x5A, 'A', 'S', 'U', 'S', ' ', 'T', 'e','c', 'h', '.', 'I', 'n', 'c', '.', '\0' }; +static const u8 EC_MODE_LED_APPLY[] = { 0x5A, 0xB4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static const u8 EC_MODE_LED_SET[] = { 0x5A, 0xB5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static const u8 FORCE_FEEDBACK_OFF[] = { 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; + +static const struct hid_device_id rog_ally_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY) }, + { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X) }, + {} +}; + +struct btn_code_map { + u64 code; + const char *name; +}; + +static const struct btn_code_map ally_btn_codes[] = { + { 0, "NONE" }, + /* Gamepad button codes */ + { BTN_PAD_A, "PAD_A" }, + { BTN_PAD_B, "PAD_B" }, + { BTN_PAD_X, "PAD_X" }, + { BTN_PAD_Y, "PAD_Y" }, + { BTN_PAD_LB, "PAD_LB" }, + { BTN_PAD_RB, "PAD_RB" }, + { BTN_PAD_LS, "PAD_LS" }, + { BTN_PAD_RS, "PAD_RS" }, + { BTN_PAD_DPAD_UP, "PAD_DPAD_UP" }, + { BTN_PAD_DPAD_DOWN, "PAD_DPAD_DOWN" }, + { BTN_PAD_DPAD_LEFT, "PAD_DPAD_LEFT" }, + { BTN_PAD_DPAD_RIGHT, "PAD_DPAD_RIGHT" }, + { BTN_PAD_VIEW, "PAD_VIEW" }, + { BTN_PAD_MENU, "PAD_MENU" }, + { BTN_PAD_XBOX, "PAD_XBOX" }, + + /* Triggers mapped to keyboard codes */ + { BTN_KB_M2, "KB_M2" }, + { BTN_KB_M1, "KB_M1" }, + { BTN_KB_ESC, "KB_ESC" }, + { BTN_KB_F1, "KB_F1" }, + { BTN_KB_F2, "KB_F2" }, + { BTN_KB_F3, "KB_F3" }, + { BTN_KB_F4, "KB_F4" }, + { BTN_KB_F5, "KB_F5" }, + { BTN_KB_F6, "KB_F6" }, + { BTN_KB_F7, "KB_F7" }, + { BTN_KB_F8, "KB_F8" }, + { BTN_KB_F9, "KB_F9" }, + { BTN_KB_F10, "KB_F10" }, + { BTN_KB_F11, "KB_F11" }, + { BTN_KB_F12, "KB_F12" }, + { BTN_KB_F14, "KB_F14" }, + { BTN_KB_F15, "KB_F15" }, + { BTN_KB_BACKTICK, "KB_BACKTICK" }, + { BTN_KB_1, "KB_1" }, + { BTN_KB_2, "KB_2" }, + { BTN_KB_3, "KB_3" }, + { BTN_KB_4, "KB_4" }, + { BTN_KB_5, "KB_5" }, + { BTN_KB_6, "KB_6" }, + { BTN_KB_7, "KB_7" }, + { BTN_KB_8, "KB_8" }, + { BTN_KB_9, "KB_9" }, + { BTN_KB_0, "KB_0" }, + { BTN_KB_HYPHEN, "KB_HYPHEN" }, + { BTN_KB_EQUALS, "KB_EQUALS" }, + { BTN_KB_BACKSPACE, "KB_BACKSPACE" }, + { BTN_KB_TAB, "KB_TAB" }, + { BTN_KB_Q, "KB_Q" }, + { BTN_KB_W, "KB_W" }, + { BTN_KB_E, "KB_E" }, + { BTN_KB_R, "KB_R" }, + { BTN_KB_T, "KB_T" }, + { BTN_KB_Y, "KB_Y" }, + { BTN_KB_U, "KB_U" }, + { BTN_KB_O, "KB_O" }, + { BTN_KB_P, "KB_P" }, + { BTN_KB_LBRACKET, "KB_LBRACKET" }, + { BTN_KB_RBRACKET, "KB_RBRACKET" }, + { BTN_KB_BACKSLASH, "KB_BACKSLASH" }, + { BTN_KB_CAPS, "KB_CAPS" }, + { BTN_KB_A, "KB_A" }, + { BTN_KB_S, "KB_S" }, + { BTN_KB_D, "KB_D" }, + { BTN_KB_F, "KB_F" }, + { BTN_KB_G, "KB_G" }, + { BTN_KB_H, "KB_H" }, + { BTN_KB_J, "KB_J" }, + { BTN_KB_K, "KB_K" }, + { BTN_KB_L, "KB_L" }, + { BTN_KB_SEMI, "KB_SEMI" }, + { BTN_KB_QUOTE, "KB_QUOTE" }, + { BTN_KB_RET, "KB_RET" }, + { BTN_KB_LSHIFT, "KB_LSHIFT" }, + { BTN_KB_Z, "KB_Z" }, + { BTN_KB_X, "KB_X" }, + { BTN_KB_C, "KB_C" }, + { BTN_KB_V, "KB_V" }, + { BTN_KB_B, "KB_B" }, + { BTN_KB_N, "KB_N" }, + { BTN_KB_M, "KB_M" }, + { BTN_KB_COMMA, "KB_COMMA" }, + { BTN_KB_PERIOD, "KB_PERIOD" }, + { BTN_KB_RSHIFT, "KB_RSHIFT" }, + { BTN_KB_LCTL, "KB_LCTL" }, + { BTN_KB_META, "KB_META" }, + { BTN_KB_LALT, "KB_LALT" }, + { BTN_KB_SPACE, "KB_SPACE" }, + { BTN_KB_RALT, "KB_RALT" }, + { BTN_KB_MENU, "KB_MENU" }, + { BTN_KB_RCTL, "KB_RCTL" }, + { BTN_KB_PRNTSCN, "KB_PRNTSCN" }, + { BTN_KB_SCRLCK, "KB_SCRLCK" }, + { BTN_KB_PAUSE, "KB_PAUSE" }, + { BTN_KB_INS, "KB_INS" }, + { BTN_KB_HOME, "KB_HOME" }, + { BTN_KB_PGUP, "KB_PGUP" }, + { BTN_KB_DEL, "KB_DEL" }, + { BTN_KB_END, "KB_END" }, + { BTN_KB_PGDWN, "KB_PGDWN" }, + { BTN_KB_UP_ARROW, "KB_UP_ARROW" }, + { BTN_KB_DOWN_ARROW, "KB_DOWN_ARROW" }, + { BTN_KB_LEFT_ARROW, "KB_LEFT_ARROW" }, + { BTN_KB_RIGHT_ARROW, "KB_RIGHT_ARROW" }, + + /* Numpad mappings */ + { BTN_NUMPAD_LOCK, "NUMPAD_LOCK" }, + { BTN_NUMPAD_FWDSLASH, "NUMPAD_FWDSLASH" }, + { BTN_NUMPAD_ASTERISK, "NUMPAD_ASTERISK" }, + { BTN_NUMPAD_HYPHEN, "NUMPAD_HYPHEN" }, + { BTN_NUMPAD_0, "NUMPAD_0" }, + { BTN_NUMPAD_1, "NUMPAD_1" }, + { BTN_NUMPAD_2, "NUMPAD_2" }, + { BTN_NUMPAD_3, "NUMPAD_3" }, + { BTN_NUMPAD_4, "NUMPAD_4" }, + { BTN_NUMPAD_5, "NUMPAD_5" }, + { BTN_NUMPAD_6, "NUMPAD_6" }, + { BTN_NUMPAD_7, "NUMPAD_7" }, + { BTN_NUMPAD_8, "NUMPAD_8" }, + { BTN_NUMPAD_9, "NUMPAD_9" }, + { BTN_NUMPAD_PLUS, "NUMPAD_PLUS" }, + { BTN_NUMPAD_ENTER, "NUMPAD_ENTER" }, + { BTN_NUMPAD_PERIOD, "NUMPAD_PERIOD" }, + + /* Mouse mappings */ + { BTN_MOUSE_LCLICK, "MOUSE_LCLICK" }, + { BTN_MOUSE_RCLICK, "MOUSE_RCLICK" }, + { BTN_MOUSE_MCLICK, "MOUSE_MCLICK" }, + { BTN_MOUSE_WHEEL_UP, "MOUSE_WHEEL_UP" }, + { BTN_MOUSE_WHEEL_DOWN, "MOUSE_WHEEL_DOWN" }, + + /* Media mappings */ + { BTN_MEDIA_SCREENSHOT, "MEDIA_SCREENSHOT" }, + { BTN_MEDIA_SHOW_KEYBOARD, "MEDIA_SHOW_KEYBOARD" }, + { BTN_MEDIA_SHOW_DESKTOP, "MEDIA_SHOW_DESKTOP" }, + { BTN_MEDIA_START_RECORDING, "MEDIA_START_RECORDING" }, + { BTN_MEDIA_MIC_OFF, "MEDIA_MIC_OFF" }, + { BTN_MEDIA_VOL_DOWN, "MEDIA_VOL_DOWN" }, + { BTN_MEDIA_VOL_UP, "MEDIA_VOL_UP" }, +}; +static const size_t keymap_len = ARRAY_SIZE(ally_btn_codes); + +/* byte_array must be >= 8 in length */ +static void btn_code_to_byte_array(u64 keycode, u8 *byte_array) +{ + /* Convert the u64 to bytes[8] */ + for (int i = 0; i < 8; ++i) { + byte_array[i] = (keycode >> (56 - 8 * i)) & 0xFF; + } +} + +static u64 name_to_btn(const char *name) +{ + int len = strcspn(name, "\n"); + for (size_t i = 0; i < keymap_len; ++i) { + if (strncmp(ally_btn_codes[i].name, name, len) == 0) { + return ally_btn_codes[i].code; + } + } + return -EINVAL; +} + +static const char* btn_to_name(u64 key) +{ + for (size_t i = 0; i < keymap_len; ++i) { + if (ally_btn_codes[i].code == key) { + return ally_btn_codes[i].name; + } + } + return NULL; +} + +struct btn_data { + u64 button; + u64 macro; + bool turbo; +}; + +struct btn_mapping { + struct btn_data btn_a; + struct btn_data btn_b; + struct btn_data btn_x; + struct btn_data btn_y; + struct btn_data btn_lb; + struct btn_data btn_rb; + struct btn_data btn_ls; + struct btn_data btn_rs; + struct btn_data btn_lt; + struct btn_data btn_rt; + struct btn_data dpad_up; + struct btn_data dpad_down; + struct btn_data dpad_left; + struct btn_data dpad_right; + struct btn_data btn_view; + struct btn_data btn_menu; + struct btn_data btn_m1; + struct btn_data btn_m2; +}; + +struct deadzone { + u8 inner; + u8 outer; +}; + +struct response_curve { + uint8_t move_pct_1; + uint8_t response_pct_1; + uint8_t move_pct_2; + uint8_t response_pct_2; + uint8_t move_pct_3; + uint8_t response_pct_3; + uint8_t move_pct_4; + uint8_t response_pct_4; +} __packed; + +struct js_axis_calibrations { + uint16_t left_y_stable; + uint16_t left_y_min; + uint16_t left_y_max; + uint16_t left_x_stable; + uint16_t left_x_min; + uint16_t left_x_max; + uint16_t right_y_stable; + uint16_t right_y_min; + uint16_t right_y_max; + uint16_t right_x_stable; + uint16_t right_x_min; + uint16_t right_x_max; +} __packed; + +struct tr_axis_calibrations { + uint16_t left_stable; + uint16_t left_max; + uint16_t right_stable; + uint16_t right_max; +} __packed; + +/* ROG Ally has many settings related to the gamepad, all using the same n-key endpoint */ +struct ally_gamepad_cfg { + struct hid_device *hdev; + struct input_dev *input; + + enum xpad_mode mode; + /* + * index: [mode] + */ + struct btn_mapping key_mapping[xpad_mode_mouse]; + /* + * index: left, right + * max: 64 + */ + u8 vibration_intensity[2]; + + /* deadzones */ + struct deadzone ls_dz; // left stick + struct deadzone rs_dz; // right stick + struct deadzone lt_dz; // left trigger + struct deadzone rt_dz; // right trigger + /* anti-deadzones */ + u8 ls_adz; // left stick + u8 rs_adz; // right stick + /* joystick response curves */ + struct response_curve ls_rc; + struct response_curve rs_rc; + + struct js_axis_calibrations js_cal; + struct tr_axis_calibrations tr_cal; +}; + +/* The hatswitch outputs integers, we use them to index this X|Y pair */ +static const int hat_values[][2] = { + { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, + { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, +}; + +/* rumble packet structure */ +struct ff_data { + u8 enable; + u8 magnitude_left; + u8 magnitude_right; + u8 magnitude_strong; + u8 magnitude_weak; + u8 pulse_sustain_10ms; + u8 pulse_release_10ms; + u8 loop_count; +} __packed; + +struct ff_report { + u8 report_id; + struct ff_data ff; +} __packed; + +struct ally_x_input_report { + uint16_t x, y; + uint16_t rx, ry; + uint16_t z, rz; + uint8_t buttons[4]; +} __packed; + +struct ally_x_device { + struct input_dev *input; + struct hid_device *hdev; + spinlock_t lock; + + struct ff_report *ff_packet; + struct work_struct output_worker; + bool output_worker_initialized; + /* Prevent multiple queued event due to the enforced delay in worker */ + bool update_qam_btn; + /* Set if the QAM and AC buttons emit Xbox and Xbox+A */ + bool qam_btns_steam_mode; + bool update_ff; +}; + +struct ally_rgb_dev { + struct hid_device *hdev; + struct led_classdev_mc led_rgb_dev; + struct work_struct work; + bool output_worker_initialized; + spinlock_t lock; + + bool removed; + bool update_rgb; + uint8_t red[4]; + uint8_t green[4]; + uint8_t blue[4]; +}; + +struct ally_rgb_data { + uint8_t brightness; + uint8_t red[4]; + uint8_t green[4]; + uint8_t blue[4]; + bool initialized; +}; + +static struct ally_drvdata { + struct hid_device *hdev; + struct ally_x_device *ally_x; + struct ally_gamepad_cfg *gamepad_cfg; + struct ally_rgb_dev *led_rgb_dev; + struct ally_rgb_data led_rgb_data; + uint mcu_version; +} drvdata; + +static void reverse_bytes_in_pairs(u8 *buf, size_t size) { + uint16_t *word_ptr; + size_t i; + + for (i = 0; i < size; i += 2) { + if (i + 1 < size) { + word_ptr = (uint16_t *)&buf[i]; + *word_ptr = cpu_to_be16(*word_ptr); + } + } +} + +/** + * asus_dev_set_report - send set report request to device. + * + * @hdev: hid device + * @buf: in/out data to transfer + * @len: length of buf + * + * Return: count of data transferred, negative if error + * + * Same behavior as hid_hw_raw_request. Note that the input buffer is duplicated. + */ +static int asus_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len) +{ + unsigned char *dmabuf; + int ret; + + dmabuf = kmemdup(buf, len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + ret = hid_hw_raw_request(hdev, buf[0], dmabuf, len, HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); + kfree(dmabuf); + + return ret; +} + +/** + * asus_dev_get_report - send get report request to device. + * + * @hdev: hid device + * @out: buffer to write output data in to + * @len: length the output buffer provided + * + * Return: count of data transferred, negative if error + * + * Same behavior as hid_hw_raw_request. + */ +static int asus_dev_get_report(struct hid_device *hdev, u8 *out, size_t len) +{ + return hid_hw_raw_request(hdev, FEATURE_REPORT_ID, out, len, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); +} + +static u8 get_endpoint_address(struct hid_device *hdev) +{ + struct usb_interface *intf; + struct usb_host_endpoint *ep; + + intf = to_usb_interface(hdev->dev.parent); + + if (intf) { + ep = intf->cur_altsetting->endpoint; + if (ep) { + return ep->desc.bEndpointAddress; + } + } + + return -ENODEV; +} + +/**************************************************************************************************/ +/* ROG Ally gamepad configuration */ +/**************************************************************************************************/ + +/* This should be called before any attempts to set device functions */ +static int ally_gamepad_check_ready(struct hid_device *hdev) +{ + int ret, count; + u8 *hidbuf; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + ret = 0; + for (count = 0; count < READY_MAX_TRIES; count++) { + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_check_ready; + hidbuf[3] = 01; + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + hid_dbg(hdev, "ROG Ally check failed set report: %d\n", ret); + + hidbuf[0] = hidbuf[1] = hidbuf[2] = hidbuf[3] = 0; + ret = asus_dev_get_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + hid_dbg(hdev, "ROG Ally check failed get report: %d\n", ret); + + ret = hidbuf[2] == xpad_cmd_check_ready; + if (ret) + break; + usleep_range( + 1000, + 2000); /* don't spam the entire loop in less than USB response time */ + } + + if (count == READY_MAX_TRIES) + hid_warn(hdev, "ROG Ally never responded with a ready\n"); + + kfree(hidbuf); + return ret; +} + +/* VIBRATION INTENSITY ****************************************************************************/ +static ssize_t gamepad_vibration_intensity_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "left right\n"); +} + +ALLY_DEVICE_ATTR_RO(gamepad_vibration_intensity_index, vibration_intensity_index); + +static ssize_t _gamepad_apply_intensity(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_vibe_intensity; + hidbuf[3] = xpad_cmd_len_vibe_intensity; + hidbuf[4] = ally_cfg->vibration_intensity[0]; + hidbuf[5] = ally_cfg->vibration_intensity[1]; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static ssize_t gamepad_vibration_intensity_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + return sysfs_emit( + buf, "%d %d\n", + ally_cfg->vibration_intensity[0], + ally_cfg->vibration_intensity[1]); +} + +static ssize_t gamepad_vibration_intensity_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + u32 left, right; + int ret; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + if (sscanf(buf, "%d %d", &left, &right) != 2) + return -EINVAL; + + if (left > 64 || right > 64) + return -EINVAL; + + ally_cfg->vibration_intensity[0] = left; + ally_cfg->vibration_intensity[1] = right; + + ret = _gamepad_apply_intensity(hdev, ally_cfg); + if (ret < 0) + return ret; + + return count; +} + +ALLY_DEVICE_ATTR_RW(gamepad_vibration_intensity, vibration_intensity); + +/* ANALOGUE DEADZONES *****************************************************************************/ +static ssize_t _gamepad_apply_deadzones(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + return ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_js_dz; + hidbuf[3] = xpad_cmd_len_deadzone; + hidbuf[4] = ally_cfg->ls_dz.inner; + hidbuf[5] = ally_cfg->ls_dz.outer; + hidbuf[6] = ally_cfg->rs_dz.inner; + hidbuf[7] = ally_cfg->rs_dz.outer; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto end; + + hidbuf[2] = xpad_cmd_set_tr_dz; + hidbuf[4] = ally_cfg->lt_dz.inner; + hidbuf[5] = ally_cfg->lt_dz.outer; + hidbuf[6] = ally_cfg->rt_dz.inner; + hidbuf[7] = ally_cfg->rt_dz.outer; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto end; + +end: + kfree(hidbuf); + return ret; +} + +static void _gamepad_set_deadzones_default(struct ally_gamepad_cfg *ally_cfg) +{ + ally_cfg->ls_dz.inner = 0x00; + ally_cfg->ls_dz.outer = 0x64; + ally_cfg->rs_dz.inner = 0x00; + ally_cfg->rs_dz.outer = 0x64; + ally_cfg->lt_dz.inner = 0x00; + ally_cfg->lt_dz.outer = 0x64; + ally_cfg->rt_dz.inner = 0x00; + ally_cfg->rt_dz.outer = 0x64; +} + +static ssize_t axis_xyz_deadzone_index_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "inner outer\n"); +} + +ALLY_DEVICE_ATTR_RO(axis_xyz_deadzone_index, deadzone_index); + +ALLY_DEADZONES(axis_xy_left, ls_dz); +ALLY_DEADZONES(axis_xy_right, rs_dz); +ALLY_DEADZONES(axis_z_left, lt_dz); +ALLY_DEADZONES(axis_z_right, rt_dz); + +/* ANTI-DEADZONES *********************************************************************************/ +static ssize_t _gamepad_apply_js_ADZ(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_adz; + hidbuf[3] = xpad_cmd_len_adz; + hidbuf[4] = ally_cfg->ls_adz; + hidbuf[5] = ally_cfg->rs_adz; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static void _gamepad_set_anti_deadzones_default(struct ally_gamepad_cfg *ally_cfg) +{ + ally_cfg->ls_adz = 0x00; + ally_cfg->rs_adz = 0x00; +} + +static ssize_t _gamepad_js_ADZ_store(struct device *dev, const char *buf, u8 *adz) +{ + int ret, val; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (val < 0 || val > 32) + return -EINVAL; + + *adz = val; + + return ret; +} + +static ssize_t axis_xy_left_anti_deadzone_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + return sysfs_emit(buf, "%d\n", ally_cfg->ls_adz); +} + +static ssize_t axis_xy_left_anti_deadzone_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret; + + ret = _gamepad_js_ADZ_store(dev, buf, &ally_cfg->ls_adz); + if (ret) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_RW(axis_xy_left_anti_deadzone, anti_deadzone); + +static ssize_t axis_xy_right_anti_deadzone_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + return sysfs_emit(buf, "%d\n", ally_cfg->rs_adz); +} + +static ssize_t axis_xy_right_anti_deadzone_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret; + + ret = _gamepad_js_ADZ_store(dev, buf, &ally_cfg->rs_adz); + if (ret) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_RW(axis_xy_right_anti_deadzone, anti_deadzone); + +/* JS RESPONSE CURVES *****************************************************************************/ +static void _gamepad_set_js_response_curves_default(struct ally_gamepad_cfg *ally_cfg) +{ + struct response_curve *js1_rc = &ally_cfg->ls_rc; + struct response_curve *js2_rc = &ally_cfg->rs_rc; + js1_rc->move_pct_1 = js2_rc->move_pct_1 = 0x16; // 25% + js1_rc->move_pct_2 = js2_rc->move_pct_2 = 0x32; // 50% + js1_rc->move_pct_3 = js2_rc->move_pct_3 = 0x48; // 75% + js1_rc->move_pct_4 = js2_rc->move_pct_4 = 0x64; // 100% + js1_rc->response_pct_1 = js2_rc->response_pct_1 = 0x16; + js1_rc->response_pct_2 = js2_rc->response_pct_2 = 0x32; + js1_rc->response_pct_3 = js2_rc->response_pct_3 = 0x48; + js1_rc->response_pct_4 = js2_rc->response_pct_4 = 0x64; +} + +static ssize_t _gamepad_apply_response_curves(struct hid_device *hdev, + struct ally_gamepad_cfg *ally_cfg) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + memcpy(&hidbuf[2], &ally_cfg->ls_rc, sizeof(ally_cfg->ls_rc)); + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + hidbuf[4] = 0x02; + memcpy(&hidbuf[5], &ally_cfg->rs_rc, sizeof(ally_cfg->rs_rc)); + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +ALLY_JS_RC_POINT(axis_xy_left, move, 1); +ALLY_JS_RC_POINT(axis_xy_left, move, 2); +ALLY_JS_RC_POINT(axis_xy_left, move, 3); +ALLY_JS_RC_POINT(axis_xy_left, move, 4); +ALLY_JS_RC_POINT(axis_xy_left, response, 1); +ALLY_JS_RC_POINT(axis_xy_left, response, 2); +ALLY_JS_RC_POINT(axis_xy_left, response, 3); +ALLY_JS_RC_POINT(axis_xy_left, response, 4); + +ALLY_JS_RC_POINT(axis_xy_right, move, 1); +ALLY_JS_RC_POINT(axis_xy_right, move, 2); +ALLY_JS_RC_POINT(axis_xy_right, move, 3); +ALLY_JS_RC_POINT(axis_xy_right, move, 4); +ALLY_JS_RC_POINT(axis_xy_right, response, 1); +ALLY_JS_RC_POINT(axis_xy_right, response, 2); +ALLY_JS_RC_POINT(axis_xy_right, response, 3); +ALLY_JS_RC_POINT(axis_xy_right, response, 4); + +/* CALIBRATIONS ***********************************************************************************/ +static int gamepad_get_calibration(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + u8 *hidbuf; + int ret, i; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + for (i = 0; i < 2; i++) { + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = 0xD0; + hidbuf[2] = 0x03; + hidbuf[3] = i + 1; // 0x01 JS, 0x02 TR + hidbuf[4] = 0x20; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_warn(hdev, "ROG Ally check failed set report: %d\n", ret); + goto cleanup; + } + + memset(hidbuf, 0, FEATURE_ROG_ALLY_REPORT_SIZE); + ret = asus_dev_get_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0 || hidbuf[5] != 1) { + hid_warn(hdev, "ROG Ally check failed get report: %d\n", ret); + goto cleanup; + } + + if (i == 0) { + /* Joystick calibration */ + reverse_bytes_in_pairs(&hidbuf[6], sizeof(struct js_axis_calibrations)); + ally_cfg->js_cal = *(struct js_axis_calibrations *)&hidbuf[6]; + print_hex_dump(KERN_INFO, "HID Buffer JS: ", DUMP_PREFIX_OFFSET, 16, 1, hidbuf, 32, true); + struct js_axis_calibrations *cal = &drvdata.gamepad_cfg->js_cal; + pr_err("LS_CAL: X: %d, Min: %d, Max: %d", cal->left_x_stable, cal->left_x_min, cal->left_x_max); + pr_err("LS_CAL: Y: %d, Min: %d, Max: %d", cal->left_y_stable, cal->left_y_min, cal->left_y_max); + pr_err("RS_CAL: X: %d, Min: %d, Max: %d", cal->right_x_stable, cal->right_x_min, cal->right_x_max); + pr_err("RS_CAL: Y: %d, Min: %d, Max: %d", cal->right_y_stable, cal->right_y_min, cal->right_y_max); + } else { + /* Trigger calibration */ + reverse_bytes_in_pairs(&hidbuf[6], sizeof(struct tr_axis_calibrations)); + ally_cfg->tr_cal = *(struct tr_axis_calibrations *)&hidbuf[6]; + print_hex_dump(KERN_INFO, "HID Buffer TR: ", DUMP_PREFIX_OFFSET, 16, 1, hidbuf, 32, true); + } + } + +cleanup: + kfree(hidbuf); + return ret; +} + +static struct attribute *axis_xy_left_attrs[] = { + &dev_attr_axis_xy_left_anti_deadzone.attr, + &dev_attr_axis_xy_left_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + &dev_attr_axis_xy_left_move_1.attr, + &dev_attr_axis_xy_left_move_2.attr, + &dev_attr_axis_xy_left_move_3.attr, + &dev_attr_axis_xy_left_move_4.attr, + &dev_attr_axis_xy_left_response_1.attr, + &dev_attr_axis_xy_left_response_2.attr, + &dev_attr_axis_xy_left_response_3.attr, + &dev_attr_axis_xy_left_response_4.attr, + NULL +}; +static const struct attribute_group axis_xy_left_attr_group = { + .name = "axis_xy_left", + .attrs = axis_xy_left_attrs, +}; + +static struct attribute *axis_xy_right_attrs[] = { + &dev_attr_axis_xy_right_anti_deadzone.attr, + &dev_attr_axis_xy_right_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + &dev_attr_axis_xy_right_move_1.attr, + &dev_attr_axis_xy_right_move_2.attr, + &dev_attr_axis_xy_right_move_3.attr, + &dev_attr_axis_xy_right_move_4.attr, + &dev_attr_axis_xy_right_response_1.attr, + &dev_attr_axis_xy_right_response_2.attr, + &dev_attr_axis_xy_right_response_3.attr, + &dev_attr_axis_xy_right_response_4.attr, + NULL +}; +static const struct attribute_group axis_xy_right_attr_group = { + .name = "axis_xy_right", + .attrs = axis_xy_right_attrs, +}; + +static struct attribute *axis_z_left_attrs[] = { + &dev_attr_axis_z_left_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL, +}; +static const struct attribute_group axis_z_left_attr_group = { + .name = "axis_z_left", + .attrs = axis_z_left_attrs, +}; + +static struct attribute *axis_z_right_attrs[] = { + &dev_attr_axis_z_right_deadzone.attr, + &dev_attr_axis_xyz_deadzone_index.attr, + NULL, +}; +static const struct attribute_group axis_z_right_attr_group = { + .name = "axis_z_right", + .attrs = axis_z_right_attrs, +}; + +/* A HID packet conatins mappings for two buttons: btn1, btn1_macro, btn2, btn2_macro */ +static void _btn_pair_to_hid_pkt(struct ally_gamepad_cfg *ally_cfg, + enum btn_pair_index pair, + struct btn_data *btn1, struct btn_data *btn2, + u8 *out, int out_len) +{ + int start = 5; + + out[0] = FEATURE_ROG_ALLY_REPORT_ID; + out[1] = FEATURE_ROG_ALLY_CODE_PAGE; + out[2] = xpad_cmd_set_mapping; + out[3] = pair; + out[4] = xpad_cmd_len_mapping; + + btn_code_to_byte_array(btn1->button, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn1->macro, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn2->button, &out[start]); + start += BTN_DATA_LEN; + btn_code_to_byte_array(btn2->macro, &out[start]); + //print_hex_dump(KERN_DEBUG, "byte_array: ", DUMP_PREFIX_OFFSET, 64, 1, out, 64, false); +} + +/* Apply the mapping pair to the device */ +static int _gamepad_apply_btn_pair(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg, + enum btn_pair_index btn_pair) +{ + u8 mode = ally_cfg->mode - 1; + struct btn_data *btn1, *btn2; + u8 *hidbuf; + int ret; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + return ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + switch (btn_pair) { + case btn_pair_dpad_u_d: + btn1 = &ally_cfg->key_mapping[mode].dpad_up; + btn2 = &ally_cfg->key_mapping[mode].dpad_down; + break; + case btn_pair_dpad_l_r: + btn1 = &ally_cfg->key_mapping[mode].dpad_left; + btn2 = &ally_cfg->key_mapping[mode].dpad_right; + break; + case btn_pair_ls_rs: + btn1 = &ally_cfg->key_mapping[mode].btn_ls; + btn2 = &ally_cfg->key_mapping[mode].btn_rs; + break; + case btn_pair_lb_rb: + btn1 = &ally_cfg->key_mapping[mode].btn_lb; + btn2 = &ally_cfg->key_mapping[mode].btn_rb; + break; + case btn_pair_lt_rt: + btn1 = &ally_cfg->key_mapping[mode].btn_lt; + btn2 = &ally_cfg->key_mapping[mode].btn_rt; + break; + case btn_pair_a_b: + btn1 = &ally_cfg->key_mapping[mode].btn_a; + btn2 = &ally_cfg->key_mapping[mode].btn_b; + break; + case btn_pair_x_y: + btn1 = &ally_cfg->key_mapping[mode].btn_x; + btn2 = &ally_cfg->key_mapping[mode].btn_y; + break; + case btn_pair_view_menu: + btn1 = &ally_cfg->key_mapping[mode].btn_view; + btn2 = &ally_cfg->key_mapping[mode].btn_menu; + break; + case btn_pair_m1_m2: + btn1 = &ally_cfg->key_mapping[mode].btn_m1; + btn2 = &ally_cfg->key_mapping[mode].btn_m2; + break; + default: + break; + } + + _btn_pair_to_hid_pkt(ally_cfg, btn_pair, btn1, btn2, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + + kfree(hidbuf); + + return ret; +} + +static int _gamepad_apply_turbo(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg) +{ + struct btn_mapping *map = &ally_cfg->key_mapping[ally_cfg->mode - 1]; + u8 *hidbuf; + int ret; + + /* set turbo */ + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_turbo; + hidbuf[3] = xpad_cmd_len_turbo; + + hidbuf[4] = map->dpad_up.turbo; + hidbuf[6] = map->dpad_down.turbo; + hidbuf[8] = map->dpad_left.turbo; + hidbuf[10] = map->dpad_right.turbo; + + hidbuf[12] = map->btn_ls.turbo; + hidbuf[14] = map->btn_rs.turbo; + hidbuf[16] = map->btn_lb.turbo; + hidbuf[18] = map->btn_rb.turbo; + + hidbuf[20] = map->btn_a.turbo; + hidbuf[22] = map->btn_b.turbo; + hidbuf[24] = map->btn_x.turbo; + hidbuf[26] = map->btn_y.turbo; + + hidbuf[28] = map->btn_lt.turbo; + hidbuf[30] = map->btn_rt.turbo; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + + kfree(hidbuf); + + return ret; +} + +static ssize_t _gamepad_apply_all(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg) +{ + int ret; + + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_dpad_u_d); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_dpad_l_r); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_ls_rs); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_lb_rb); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_a_b); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_x_y); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_view_menu); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + if (ret < 0) + return ret; + ret = _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_lt_rt); + if (ret < 0) + return ret; + ret = _gamepad_apply_turbo(hdev, ally_cfg); + if (ret < 0) + return ret; + ret = _gamepad_apply_deadzones(hdev, ally_cfg); + if (ret < 0) + return ret; + ret = _gamepad_apply_js_ADZ(hdev, ally_cfg); + if (ret < 0) + return ret; + ret =_gamepad_apply_response_curves(hdev, ally_cfg); + if (ret < 0) + return ret; + + return 0; +} + +static ssize_t gamepad_apply_all_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + struct hid_device *hdev = to_hid_device(dev); + int ret; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + ret = _gamepad_apply_all(hdev, ally_cfg); + if (ret < 0) + return ret; + + return count; +} +ALLY_DEVICE_ATTR_WO(gamepad_apply_all, apply_all); + +/* button map attributes, regular and macro*/ +ALLY_BTN_MAPPING(m1, btn_m1); +ALLY_BTN_MAPPING(m2, btn_m2); +ALLY_BTN_MAPPING(view, btn_view); +ALLY_BTN_MAPPING(menu, btn_menu); +ALLY_TURBO_BTN_MAPPING(a, btn_a); +ALLY_TURBO_BTN_MAPPING(b, btn_b); +ALLY_TURBO_BTN_MAPPING(x, btn_x); +ALLY_TURBO_BTN_MAPPING(y, btn_y); +ALLY_TURBO_BTN_MAPPING(lb, btn_lb); +ALLY_TURBO_BTN_MAPPING(rb, btn_rb); +ALLY_TURBO_BTN_MAPPING(ls, btn_ls); +ALLY_TURBO_BTN_MAPPING(rs, btn_rs); +ALLY_TURBO_BTN_MAPPING(lt, btn_lt); +ALLY_TURBO_BTN_MAPPING(rt, btn_rt); +ALLY_TURBO_BTN_MAPPING(dpad_u, dpad_up); +ALLY_TURBO_BTN_MAPPING(dpad_d, dpad_down); +ALLY_TURBO_BTN_MAPPING(dpad_l, dpad_left); +ALLY_TURBO_BTN_MAPPING(dpad_r, dpad_right); + +static void _gamepad_set_xpad_default(struct ally_gamepad_cfg *ally_cfg) +{ + struct btn_mapping *map = &ally_cfg->key_mapping[ally_cfg->mode - 1]; + map->btn_m1.button = BTN_KB_M1; + map->btn_m2.button = BTN_KB_M2; + map->btn_a.button = BTN_PAD_A; + map->btn_b.button = BTN_PAD_B; + map->btn_x.button = BTN_PAD_X; + map->btn_y.button = BTN_PAD_Y; + map->btn_lb.button = BTN_PAD_LB; + map->btn_rb.button = BTN_PAD_RB; + map->btn_lt.button = BTN_PAD_LT; + map->btn_rt.button = BTN_PAD_RT; + map->btn_ls.button = BTN_PAD_LS; + map->btn_rs.button = BTN_PAD_RS; + map->dpad_up.button = BTN_PAD_DPAD_UP; + map->dpad_down.button = BTN_PAD_DPAD_DOWN; + map->dpad_left.button = BTN_PAD_DPAD_LEFT; + map->dpad_right.button = BTN_PAD_DPAD_RIGHT; + map->btn_view.button = BTN_PAD_VIEW; + map->btn_menu.button = BTN_PAD_MENU; +} + +static ssize_t btn_mapping_reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + switch (ally_cfg->mode) { + case xpad_mode_game: + _gamepad_set_xpad_default(ally_cfg); + break; + default: + _gamepad_set_xpad_default(ally_cfg); + break; + } + + return count; +} +ALLY_DEVICE_ATTR_WO(btn_mapping_reset, reset_btn_mapping); + +/* GAMEPAD MODE */ +static ssize_t _gamepad_set_mode(struct hid_device *hdev, struct ally_gamepad_cfg *ally_cfg, + int val) +{ + u8 *hidbuf; + int ret; + + hidbuf = kzalloc(FEATURE_ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + if (!hidbuf) + return -ENOMEM; + + hidbuf[0] = FEATURE_ROG_ALLY_REPORT_ID; + hidbuf[1] = FEATURE_ROG_ALLY_CODE_PAGE; + hidbuf[2] = xpad_cmd_set_mode; + hidbuf[3] = xpad_cmd_len_mode; + hidbuf[4] = val; + + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) + goto report_fail; + + ret = asus_dev_set_report(hdev, hidbuf, FEATURE_ROG_ALLY_REPORT_SIZE); + if (ret < 0) + goto report_fail; + + ret = _gamepad_apply_all(hdev, ally_cfg); + if (ret < 0) + goto report_fail; + +report_fail: + kfree(hidbuf); + return ret; +} + +static ssize_t gamepad_mode_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + return sysfs_emit(buf, "%d\n", ally_cfg->mode); +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int ret, val; + + if (!drvdata.gamepad_cfg) + return -ENODEV; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (val < xpad_mode_game || val > xpad_mode_mouse) + return -EINVAL; + + ally_cfg->mode = val; + + ret = _gamepad_set_mode(hdev, ally_cfg, val); + if (ret < 0) + return ret; + + return count; +} + +DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t mcu_version_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%d\n", drvdata.mcu_version); +} + +DEVICE_ATTR_RO(mcu_version); + +/* ROOT LEVEL ATTRS *******************************************************************************/ +static struct attribute *gamepad_device_attrs[] = { + &dev_attr_btn_mapping_reset.attr, + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_apply_all.attr, + &dev_attr_gamepad_vibration_intensity.attr, + &dev_attr_gamepad_vibration_intensity_index.attr, + &dev_attr_mcu_version.attr, + NULL +}; + +static const struct attribute_group ally_controller_attr_group = { + .attrs = gamepad_device_attrs, +}; + +static const struct attribute_group *gamepad_device_attr_groups[] = { + &ally_controller_attr_group, + &axis_xy_left_attr_group, + &axis_xy_right_attr_group, + &axis_z_left_attr_group, + &axis_z_right_attr_group, + &btn_mapping_m1_attr_group, + &btn_mapping_m2_attr_group, + &btn_mapping_a_attr_group, + &btn_mapping_b_attr_group, + &btn_mapping_x_attr_group, + &btn_mapping_y_attr_group, + &btn_mapping_lb_attr_group, + &btn_mapping_rb_attr_group, + &btn_mapping_ls_attr_group, + &btn_mapping_rs_attr_group, + &btn_mapping_lt_attr_group, + &btn_mapping_rt_attr_group, + &btn_mapping_dpad_u_attr_group, + &btn_mapping_dpad_d_attr_group, + &btn_mapping_dpad_l_attr_group, + &btn_mapping_dpad_r_attr_group, + &btn_mapping_view_attr_group, + &btn_mapping_menu_attr_group, + NULL, +}; + +static struct ally_gamepad_cfg *ally_gamepad_cfg_create(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg; + struct input_dev *input_dev; + int err; + + ally_cfg = devm_kzalloc(&hdev->dev, sizeof(*ally_cfg), GFP_KERNEL); + if (!ally_cfg) + return ERR_PTR(-ENOMEM); + ally_cfg->hdev = hdev; + // Allocate memory for each mode's `btn_mapping` + ally_cfg->mode = xpad_mode_game; + + input_dev = devm_input_allocate_device(&hdev->dev); + if (!input_dev) { + err = -ENOMEM; + goto free_ally_cfg; + } + + input_dev->id.bustype = hdev->bus; + input_dev->id.vendor = hdev->vendor; + input_dev->id.product = hdev->product; + input_dev->id.version = hdev->version; + input_dev->uniq = hdev->uniq; + input_dev->name = "ASUS ROG Ally Config"; + input_set_capability(input_dev, EV_KEY, KEY_PROG1); + input_set_capability(input_dev, EV_KEY, KEY_F16); + input_set_capability(input_dev, EV_KEY, KEY_F17); + input_set_capability(input_dev, EV_KEY, KEY_F18); + input_set_drvdata(input_dev, hdev); + + err = input_register_device(input_dev); + if (err) + goto free_input_dev; + ally_cfg->input = input_dev; + + /* ignore all errors for this as they are related to USB HID I/O */ + _gamepad_set_xpad_default(ally_cfg); + ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m1.button = BTN_KB_M1; + ally_cfg->key_mapping[ally_cfg->mode - 1].btn_m2.button = BTN_KB_M2; + _gamepad_apply_btn_pair(hdev, ally_cfg, btn_pair_m1_m2); + gamepad_get_calibration(hdev); + + ally_cfg->vibration_intensity[0] = 0x64; + ally_cfg->vibration_intensity[1] = 0x64; + _gamepad_set_deadzones_default(ally_cfg); + _gamepad_set_anti_deadzones_default(ally_cfg); + _gamepad_set_js_response_curves_default(ally_cfg); + + drvdata.gamepad_cfg = ally_cfg; // Must asign before attr group setup + if (sysfs_create_groups(&hdev->dev.kobj, gamepad_device_attr_groups)) { + err = -ENODEV; + goto unregister_input_dev; + } + + return ally_cfg; + +unregister_input_dev: + input_unregister_device(input_dev); + ally_cfg->input = NULL; // Prevent double free when kfree(ally_cfg) happens + +free_input_dev: + devm_kfree(&hdev->dev, input_dev); + +free_ally_cfg: + devm_kfree(&hdev->dev, ally_cfg); + return ERR_PTR(err); +} + +static void ally_cfg_remove(struct hid_device *hdev) +{ + // __gamepad_set_mode(hdev, drvdata.gamepad_cfg, xpad_mode_mouse); + sysfs_remove_groups(&hdev->dev.kobj, gamepad_device_attr_groups); +} + +/**************************************************************************************************/ +/* ROG Ally gamepad i/o and force-feedback */ +/**************************************************************************************************/ +static int ally_x_raw_event(struct ally_x_device *ally_x, struct hid_report *report, u8 *data, + int size) +{ + struct ally_x_input_report *in_report; + unsigned long flags; + u8 byte; + + if (data[0] == 0x0B) { + in_report = (struct ally_x_input_report *)&data[1]; + + input_report_abs(ally_x->input, ABS_X, in_report->x); + input_report_abs(ally_x->input, ABS_Y, in_report->y); + input_report_abs(ally_x->input, ABS_RX, in_report->rx); + input_report_abs(ally_x->input, ABS_RY, in_report->ry); + input_report_abs(ally_x->input, ABS_Z, in_report->z); + input_report_abs(ally_x->input, ABS_RZ, in_report->rz); + + byte = in_report->buttons[0]; + input_report_key(ally_x->input, BTN_A, byte & BIT(0)); + input_report_key(ally_x->input, BTN_B, byte & BIT(1)); + input_report_key(ally_x->input, BTN_X, byte & BIT(2)); + input_report_key(ally_x->input, BTN_Y, byte & BIT(3)); + input_report_key(ally_x->input, BTN_TL, byte & BIT(4)); + input_report_key(ally_x->input, BTN_TR, byte & BIT(5)); + input_report_key(ally_x->input, BTN_SELECT, byte & BIT(6)); + input_report_key(ally_x->input, BTN_START, byte & BIT(7)); + + byte = in_report->buttons[1]; + input_report_key(ally_x->input, BTN_THUMBL, byte & BIT(0)); + input_report_key(ally_x->input, BTN_THUMBR, byte & BIT(1)); + input_report_key(ally_x->input, BTN_MODE, byte & BIT(2)); + + byte = in_report->buttons[2]; + input_report_abs(ally_x->input, ABS_HAT0X, hat_values[byte][0]); + input_report_abs(ally_x->input, ABS_HAT0Y, hat_values[byte][1]); + } + /* + * The MCU used on Ally provides many devices: gamepad, keyboord, mouse, other. + * The AC and QAM buttons route through another interface making it difficult to + * use the events unless we grab those and use them here. Only works for Ally X. + */ + else if (data[0] == 0x5A) { + if (ally_x->qam_btns_steam_mode) { + spin_lock_irqsave(&ally_x->lock, flags); + if ((data[1] == 0x38 || data[1] == 0x93) && !ally_x->update_qam_btn) { + ally_x->update_qam_btn = true; + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + } + spin_unlock_irqrestore(&ally_x->lock, flags); + /* Left/XBox button. Long press does ctrl+alt+del which we can't catch */ + input_report_key(ally_x->input, BTN_MODE, data[1] == 0xA6); + } else { + input_report_key(ally_x->input, KEY_F16, data[1] == 0xA6); + input_report_key(ally_x->input, KEY_PROG1, data[1] == 0x38 || data[1] == 0x93); + } + /* QAM long press */ + input_report_key(ally_x->input, KEY_F17, data[1] == 0xA7); + /* QAM long press released */ + input_report_key(ally_x->input, KEY_F18, data[1] == 0xA8); + } + + input_sync(ally_x->input); + + return 0; +} + +static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev, + const char *name_suffix) +{ + struct input_dev *input_dev; + + input_dev = devm_input_allocate_device(&hdev->dev); + if (!input_dev) + return ERR_PTR(-ENOMEM); + + input_dev->id.bustype = hdev->bus; + input_dev->id.vendor = hdev->vendor; + input_dev->id.product = hdev->product; + input_dev->id.version = hdev->version; + input_dev->uniq = hdev->uniq; + input_dev->name = "ASUS ROG Ally X Gamepad"; + + input_set_drvdata(input_dev, hdev); + + return input_dev; +} + +static int ally_x_play_effect(struct input_dev *idev, void *data, struct ff_effect *effect) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + unsigned long flags; + + if (effect->type != FF_RUMBLE) + return 0; + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->ff_packet->ff.magnitude_strong = effect->u.rumble.strong_magnitude / 512; + ally_x->ff_packet->ff.magnitude_weak = effect->u.rumble.weak_magnitude / 512; + ally_x->update_ff = true; + spin_unlock_irqrestore(&ally_x->lock, flags); + + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + + return 0; +} + +static void ally_x_work(struct work_struct *work) +{ + struct ally_x_device *ally_x = container_of(work, struct ally_x_device, output_worker); + struct ff_report *ff_report = NULL; + bool update_qam = false; + bool update_ff = false; + unsigned long flags; + + spin_lock_irqsave(&ally_x->lock, flags); + update_ff = ally_x->update_ff; + if (ally_x->update_ff) { + ff_report = kmemdup(ally_x->ff_packet, sizeof(*ally_x->ff_packet), GFP_KERNEL); + ally_x->update_ff = false; + } + update_qam = ally_x->update_qam_btn; + spin_unlock_irqrestore(&ally_x->lock, flags); + + if (update_ff && ff_report) { + ff_report->ff.magnitude_left = ff_report->ff.magnitude_strong; + ff_report->ff.magnitude_right = ff_report->ff.magnitude_weak; + asus_dev_set_report(ally_x->hdev, (u8 *)ff_report, sizeof(*ff_report)); + } + kfree(ff_report); + + if (update_qam) { + /* + * The sleeps here are required to allow steam to register the button combo. + */ + usleep_range(1000, 2000); + input_report_key(ally_x->input, BTN_MODE, 1); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_A, 1); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_A, 0); + input_sync(ally_x->input); + + msleep(80); + input_report_key(ally_x->input, BTN_MODE, 0); + input_sync(ally_x->input); + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->update_qam_btn = false; + spin_unlock_irqrestore(&ally_x->lock, flags); + } +} + +static struct input_dev *ally_x_setup_input(struct hid_device *hdev) +{ + int ret, abs_min = 0, js_abs_max = 65535, tr_abs_max = 1023; + struct input_dev *input; + + input = ally_x_alloc_input_dev(hdev, NULL); + if (IS_ERR(input)) + return ERR_CAST(input); + + input_set_abs_params(input, ABS_X, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_Y, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_RX, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_RY, abs_min, js_abs_max, 0, 0); + input_set_abs_params(input, ABS_Z, abs_min, tr_abs_max, 0, 0); + input_set_abs_params(input, ABS_RZ, abs_min, tr_abs_max, 0, 0); + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0); + input_set_capability(input, EV_KEY, BTN_A); + input_set_capability(input, EV_KEY, BTN_B); + input_set_capability(input, EV_KEY, BTN_X); + input_set_capability(input, EV_KEY, BTN_Y); + input_set_capability(input, EV_KEY, BTN_TL); + input_set_capability(input, EV_KEY, BTN_TR); + input_set_capability(input, EV_KEY, BTN_SELECT); + input_set_capability(input, EV_KEY, BTN_START); + input_set_capability(input, EV_KEY, BTN_MODE); + input_set_capability(input, EV_KEY, BTN_THUMBL); + input_set_capability(input, EV_KEY, BTN_THUMBR); + + input_set_capability(input, EV_KEY, KEY_PROG1); + input_set_capability(input, EV_KEY, KEY_F16); + input_set_capability(input, EV_KEY, KEY_F17); + input_set_capability(input, EV_KEY, KEY_F18); + + input_set_capability(input, EV_FF, FF_RUMBLE); + input_ff_create_memless(input, NULL, ally_x_play_effect); + + ret = input_register_device(input); + if (ret) + return ERR_PTR(ret); + + return input; +} + +static ssize_t ally_x_qam_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + + return sysfs_emit(buf, "%d\n", ally_x->qam_btns_steam_mode); +} + +static ssize_t ally_x_qam_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret < 0) + return ret; + + ally_x->qam_btns_steam_mode = val; + + return count; +} +ALLY_DEVICE_ATTR_RW(ally_x_qam_mode, qam_mode); + +static struct ally_x_device *ally_x_create(struct hid_device *hdev) +{ + uint8_t max_output_report_size; + struct ally_x_device *ally_x; + struct ff_report *report; + int ret; + + ally_x = devm_kzalloc(&hdev->dev, sizeof(*ally_x), GFP_KERNEL); + if (!ally_x) + return ERR_PTR(-ENOMEM); + + ally_x->hdev = hdev; + INIT_WORK(&ally_x->output_worker, ally_x_work); + spin_lock_init(&ally_x->lock); + ally_x->output_worker_initialized = true; + ally_x->qam_btns_steam_mode = + true; /* Always default to steam mode, it can be changed by userspace attr */ + + max_output_report_size = sizeof(struct ally_x_input_report); + report = devm_kzalloc(&hdev->dev, sizeof(*report), GFP_KERNEL); + if (!report) { + ret = -ENOMEM; + goto free_ally_x; + } + + /* None of these bytes will change for the FF command for now */ + report->report_id = 0x0D; + report->ff.enable = 0x0F; /* Enable all by default */ + report->ff.pulse_sustain_10ms = 0xFF; /* Duration */ + report->ff.pulse_release_10ms = 0x00; /* Start Delay */ + report->ff.loop_count = 0xEB; /* Loop Count */ + ally_x->ff_packet = report; + + ally_x->input = ally_x_setup_input(hdev); + if (IS_ERR(ally_x->input)) { + ret = PTR_ERR(ally_x->input); + goto free_ff_packet; + } + + if (sysfs_create_file(&hdev->dev.kobj, &dev_attr_ally_x_qam_mode.attr)) { + ret = -ENODEV; + goto unregister_input; + } + + ally_x->update_ff = true; + if (ally_x->output_worker_initialized) + schedule_work(&ally_x->output_worker); + + hid_info(hdev, "Registered Ally X controller using %s\n", + dev_name(&ally_x->input->dev)); + return ally_x; + +unregister_input: + input_unregister_device(ally_x->input); +free_ff_packet: + kfree(ally_x->ff_packet); +free_ally_x: + kfree(ally_x); + return ERR_PTR(ret); +} + +static void ally_x_remove(struct hid_device *hdev) +{ + struct ally_x_device *ally_x = drvdata.ally_x; + unsigned long flags; + + spin_lock_irqsave(&ally_x->lock, flags); + ally_x->output_worker_initialized = false; + spin_unlock_irqrestore(&ally_x->lock, flags); + cancel_work_sync(&ally_x->output_worker); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_ally_x_qam_mode.attr); +} + +/**************************************************************************************************/ +/* ROG Ally LED control */ +/**************************************************************************************************/ +static void ally_rgb_schedule_work(struct ally_rgb_dev *led) +{ + unsigned long flags; + + spin_lock_irqsave(&led->lock, flags); + if (!led->removed) + schedule_work(&led->work); + spin_unlock_irqrestore(&led->lock, flags); +} + +/* + * The RGB still has the basic 0-3 level brightness. Since the multicolour + * brightness is being used in place, set this to max + */ +static int ally_rgb_set_bright_base_max(struct hid_device *hdev) +{ + u8 buf[] = { FEATURE_KBD_LED_REPORT_ID1, 0xba, 0xc5, 0xc4, 0x02 }; + + return asus_dev_set_report(hdev, buf, sizeof(buf)); +} + +static void ally_rgb_do_work(struct work_struct *work) +{ + struct ally_rgb_dev *led = container_of(work, struct ally_rgb_dev, work); + int ret; + unsigned long flags; + + u8 buf[16] = { [0] = FEATURE_ROG_ALLY_REPORT_ID, + [1] = FEATURE_ROG_ALLY_CODE_PAGE, + [2] = xpad_cmd_set_leds, + [3] = xpad_cmd_len_leds }; + + spin_lock_irqsave(&led->lock, flags); + if (!led->update_rgb) { + spin_unlock_irqrestore(&led->lock, flags); + return; + } + + for (int i = 0; i < 4; i++) { + buf[5 + i * 3] = drvdata.led_rgb_dev->green[i]; + buf[6 + i * 3] = drvdata.led_rgb_dev->blue[i]; + buf[4 + i * 3] = drvdata.led_rgb_dev->red[i]; + } + led->update_rgb = false; + + spin_unlock_irqrestore(&led->lock, flags); + + ret = asus_dev_set_report(led->hdev, buf, sizeof(buf)); + if (ret < 0) + hid_err(led->hdev, "Ally failed to set gamepad backlight: %d\n", ret); +} + +static void ally_rgb_set(struct led_classdev *cdev, enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); + struct ally_rgb_dev *led = container_of(mc_cdev, struct ally_rgb_dev, led_rgb_dev); + int intensity, bright; + unsigned long flags; + + led_mc_calc_color_components(mc_cdev, brightness); + spin_lock_irqsave(&led->lock, flags); + led->update_rgb = true; + bright = mc_cdev->led_cdev.brightness; + for (int i = 0; i < 4; i++) { + intensity = mc_cdev->subled_info[i].intensity; + drvdata.led_rgb_dev->red[i] = (((intensity >> 16) & 0xFF) * bright) / 255; + drvdata.led_rgb_dev->green[i] = (((intensity >> 8) & 0xFF) * bright) / 255; + drvdata.led_rgb_dev->blue[i] = ((intensity & 0xFF) * bright) / 255; + } + spin_unlock_irqrestore(&led->lock, flags); + drvdata.led_rgb_data.initialized = true; + + ally_rgb_schedule_work(led); +} + +static int ally_rgb_set_static_from_multi(struct hid_device *hdev) +{ + u8 buf[17] = {FEATURE_KBD_LED_REPORT_ID1, 0xb3}; + int ret; + + /* + * Set single zone single colour based on the first LED of EC software mode. + * buf[2] = zone, buf[3] = mode + */ + buf[4] = drvdata.led_rgb_data.red[0]; + buf[5] = drvdata.led_rgb_data.green[0]; + buf[6] = drvdata.led_rgb_data.blue[0]; + + ret = asus_dev_set_report(hdev, buf, sizeof(buf)); + if (ret < 0) + return ret; + + ret = asus_dev_set_report(hdev, EC_MODE_LED_APPLY, sizeof(EC_MODE_LED_APPLY)); + if (ret < 0) + return ret; + + return asus_dev_set_report(hdev, EC_MODE_LED_SET, sizeof(EC_MODE_LED_SET)); +} + +/* + * Store the RGB values for restoring on resume, and set the static mode to the first LED colour +*/ +static void ally_rgb_store_settings(void) +{ + int arr_size = sizeof(drvdata.led_rgb_data.red); + + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + + drvdata.led_rgb_data.brightness = led_rgb->led_rgb_dev.led_cdev.brightness; + + memcpy(drvdata.led_rgb_data.red, led_rgb->red, arr_size); + memcpy(drvdata.led_rgb_data.green, led_rgb->green, arr_size); + memcpy(drvdata.led_rgb_data.blue, led_rgb->blue, arr_size); + + ally_rgb_set_static_from_multi(led_rgb->hdev); +} + +static void ally_rgb_restore_settings(struct ally_rgb_dev *led_rgb, struct led_classdev *led_cdev, + struct mc_subled *mc_led_info) +{ + int arr_size = sizeof(drvdata.led_rgb_data.red); + + memcpy(led_rgb->red, drvdata.led_rgb_data.red, arr_size); + memcpy(led_rgb->green, drvdata.led_rgb_data.green, arr_size); + memcpy(led_rgb->blue, drvdata.led_rgb_data.blue, arr_size); + for (int i = 0; i < 4; i++) { + mc_led_info[i].intensity = (drvdata.led_rgb_data.red[i] << 16) | + (drvdata.led_rgb_data.green[i] << 8) | + drvdata.led_rgb_data.blue[i]; + } + led_cdev->brightness = drvdata.led_rgb_data.brightness; +} + +/* Set LEDs. Call after any setup. */ +static void ally_rgb_resume(void) +{ + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + struct led_classdev *led_cdev; + struct mc_subled *mc_led_info; + + if (!led_rgb) + return; + + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + mc_led_info = led_rgb->led_rgb_dev.subled_info; + + if (drvdata.led_rgb_data.initialized) { + ally_rgb_restore_settings(led_rgb, led_cdev, mc_led_info); + led_rgb->update_rgb = true; + ally_rgb_schedule_work(led_rgb); + ally_rgb_set_bright_base_max(led_rgb->hdev); + } +} + +static int ally_rgb_register(struct hid_device *hdev, struct ally_rgb_dev *led_rgb) +{ + struct mc_subled *mc_led_info; + struct led_classdev *led_cdev; + + mc_led_info = + devm_kmalloc_array(&hdev->dev, 12, sizeof(*mc_led_info), GFP_KERNEL | __GFP_ZERO); + if (!mc_led_info) + return -ENOMEM; + + mc_led_info[0].color_index = LED_COLOR_ID_RGB; + mc_led_info[1].color_index = LED_COLOR_ID_RGB; + mc_led_info[2].color_index = LED_COLOR_ID_RGB; + mc_led_info[3].color_index = LED_COLOR_ID_RGB; + + led_rgb->led_rgb_dev.subled_info = mc_led_info; + led_rgb->led_rgb_dev.num_colors = 4; + + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + led_cdev->brightness = 128; + led_cdev->name = "ally:rgb:joystick_rings"; + led_cdev->max_brightness = 255; + led_cdev->brightness_set = ally_rgb_set; + + if (drvdata.led_rgb_data.initialized) { + ally_rgb_restore_settings(led_rgb, led_cdev, mc_led_info); + } + + return devm_led_classdev_multicolor_register(&hdev->dev, &led_rgb->led_rgb_dev); +} + +static struct ally_rgb_dev *ally_rgb_create(struct hid_device *hdev) +{ + struct ally_rgb_dev *led_rgb; + int ret; + + led_rgb = devm_kzalloc(&hdev->dev, sizeof(struct ally_rgb_dev), GFP_KERNEL); + if (!led_rgb) + return ERR_PTR(-ENOMEM); + + ret = ally_rgb_register(hdev, led_rgb); + if (ret < 0) { + cancel_work_sync(&led_rgb->work); + devm_kfree(&hdev->dev, led_rgb); + return ERR_PTR(ret); + } + + led_rgb->hdev = hdev; + led_rgb->removed = false; + + INIT_WORK(&led_rgb->work, ally_rgb_do_work); + led_rgb->output_worker_initialized = true; + spin_lock_init(&led_rgb->lock); + + ally_rgb_set_bright_base_max(hdev); + + /* Not marked as initialized unless ally_rgb_set() is called */ + if (drvdata.led_rgb_data.initialized) { + msleep(1500); + led_rgb->update_rgb = true; + ally_rgb_schedule_work(led_rgb); + } + + return led_rgb; +} + +static void ally_rgb_remove(struct hid_device *hdev) +{ + struct ally_rgb_dev *led_rgb = drvdata.led_rgb_dev; + unsigned long flags; + int ep; + + ep = get_endpoint_address(hdev); + if (ep != ROG_ALLY_CFG_INTF_IN) + return; + + if (!drvdata.led_rgb_dev || led_rgb->removed) + return; + + spin_lock_irqsave(&led_rgb->lock, flags); + led_rgb->removed = true; + led_rgb->output_worker_initialized = false; + spin_unlock_irqrestore(&led_rgb->lock, flags); + cancel_work_sync(&led_rgb->work); + devm_led_classdev_multicolor_unregister(&hdev->dev, &led_rgb->led_rgb_dev); + + hid_info(hdev, "Removed Ally RGB interface"); +} + +/**************************************************************************************************/ +/* ROG Ally driver init */ +/**************************************************************************************************/ + +static int ally_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, + int size) +{ + struct ally_gamepad_cfg *cfg = drvdata.gamepad_cfg; + struct ally_x_device *ally_x = drvdata.ally_x; + + if (ally_x) { + if ((hdev->bus == BUS_USB && report->id == ALLY_X_INPUT_REPORT_USB && + size == ALLY_X_INPUT_REPORT_USB_SIZE) || + (data[0] == 0x5A)) { + ally_x_raw_event(ally_x, report, data, size); + } else { + return -1; + } + } + + if (cfg && !ally_x) { + input_report_key(cfg->input, KEY_PROG1, data[1] == 0x38); + input_report_key(cfg->input, KEY_F16, data[1] == 0xA6); + input_report_key(cfg->input, KEY_F17, data[1] == 0xA7); + input_report_key(cfg->input, KEY_F18, data[1] == 0xA8); + input_sync(cfg->input); + } + + return 0; +} + +static int ally_hid_init(struct hid_device *hdev) +{ + int ret; + + ret = asus_dev_set_report(hdev, EC_INIT_STRING, sizeof(EC_INIT_STRING)); + if (ret < 0) { + hid_err(hdev, "Ally failed to send init command: %d\n", ret); + return ret; + } + + ret = asus_dev_set_report(hdev, FORCE_FEEDBACK_OFF, sizeof(FORCE_FEEDBACK_OFF)); + if (ret < 0) + hid_err(hdev, "Ally failed to send init command: %d\n", ret); + + return ret; +} + +static void ally_disable_nkey_wakeup(struct hid_device *hdev) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev; + + if (!intf) + return; + + udev = interface_to_usbdev(intf); + if (!udev) + return; + + /* HACK: Mark Ally N-Key as incapable of wakeup */ + device_set_wakeup_enable(&udev->dev, false); + hid_info(hdev, "Disabled wakeup capability on %s\n", dev_name(&udev->dev)); +} + +static int ally_hid_probe(struct hid_device *hdev, const struct hid_device_id *_id) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); + u16 idProduct = le16_to_cpu(udev->descriptor.idProduct); + int ret, ep; + + ep = get_endpoint_address(hdev); + if (ep < 0) + return ep; + + if (ep != ROG_ALLY_CFG_INTF_IN && + ep != ROG_ALLY_X_INTF_IN) + return -ENODEV; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "Parse failed\n"); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "Failed to start HID device\n"); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "Failed to open HID device\n"); + goto err_stop; + } + + /* Initialize MCU even before alloc */ + ret = ally_hid_init(hdev); + if (ret < 0) + return ret; + + drvdata.hdev = hdev; + hid_set_drvdata(hdev, &drvdata); + + /* This should almost always exist */ + if (ep == ROG_ALLY_CFG_INTF_IN) { + validate_mcu_fw_version(hdev, idProduct); + ally_disable_nkey_wakeup(hdev); + + drvdata.led_rgb_dev = ally_rgb_create(hdev); + if (IS_ERR(drvdata.led_rgb_dev)) + hid_err(hdev, "Failed to create Ally gamepad LEDs.\n"); + else + hid_info(hdev, "Created Ally RGB LED controls.\n"); + + drvdata.gamepad_cfg = ally_gamepad_cfg_create(hdev); + if (IS_ERR(drvdata.gamepad_cfg)) + hid_err(hdev, "Failed to create Ally gamepad attributes.\n"); + else + hid_info(hdev, "Created Ally gamepad attributes.\n"); + + if (IS_ERR(drvdata.led_rgb_dev) && IS_ERR(drvdata.gamepad_cfg)) + goto err_close; + } + + /* May or may not exist */ + if (ep == ROG_ALLY_X_INTF_IN) { + drvdata.ally_x = ally_x_create(hdev); + if (IS_ERR(drvdata.ally_x)) { + hid_err(hdev, "Failed to create Ally X gamepad.\n"); + drvdata.ally_x = NULL; + goto err_close; + } + hid_info(hdev, "Created Ally X controller.\n"); + + // Not required since we send this inputs ep through the gamepad input dev + if (drvdata.gamepad_cfg && drvdata.gamepad_cfg->input) { + input_unregister_device(drvdata.gamepad_cfg->input); + hid_info(hdev, "Ally X removed unrequired input dev.\n"); + } + } + + return 0; + +err_close: + hid_hw_close(hdev); +err_stop: + hid_hw_stop(hdev); + return ret; +} + +static void ally_hid_remove(struct hid_device *hdev) +{ + if (drvdata.led_rgb_dev) + ally_rgb_remove(hdev); + + if (drvdata.ally_x) + ally_x_remove(hdev); + + if (drvdata.gamepad_cfg) + ally_cfg_remove(hdev); + + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static int ally_hid_resume(struct hid_device *hdev) +{ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; + int err; + + if (!ally_cfg) + return 0; + + err = _gamepad_apply_all(hdev, ally_cfg); + if (err) + return err; + + return 0; +} + +static int ally_hid_reset_resume(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + if (ep != ROG_ALLY_CFG_INTF_IN) + return 0; + + ally_hid_init(hdev); + ally_rgb_resume(); + + return ally_hid_resume(hdev); +} + +static int ally_pm_thaw(struct device *dev) +{ + struct hid_device *hdev = to_hid_device(dev); + + return ally_hid_reset_resume(hdev); +} + +static int ally_pm_suspend(struct device *dev) +{ + if (drvdata.led_rgb_dev) { + ally_rgb_store_settings(); + } + + return 0; +} + +static const struct dev_pm_ops ally_pm_ops = { + .thaw = ally_pm_thaw, + .suspend = ally_pm_suspend, + .poweroff = ally_pm_suspend, +}; + +MODULE_DEVICE_TABLE(hid, rog_ally_devices); + +static struct hid_driver rog_ally_cfg = { .name = "asus_rog_ally", + .id_table = rog_ally_devices, + .probe = ally_hid_probe, + .remove = ally_hid_remove, + .raw_event = ally_raw_event, + /* HID is the better place for resume functions, not pm_ops */ + .resume = ally_hid_resume, + /* ALLy 1 requires this to reset device state correctly */ + .reset_resume = ally_hid_reset_resume, + .driver = { + .pm = &ally_pm_ops, + } +}; + +static int __init rog_ally_init(void) +{ + return hid_register_driver(&rog_ally_cfg); +} + +static void __exit rog_ally_exit(void) +{ + hid_unregister_driver(&rog_ally_cfg); +} + +module_init(rog_ally_init); +module_exit(rog_ally_exit); + +MODULE_IMPORT_NS("ASUS_WMI"); +MODULE_IMPORT_NS("HID_ASUS"); +MODULE_AUTHOR("Luke D. Jones"); +MODULE_DESCRIPTION("HID Driver for ASUS ROG Ally gamepad configuration."); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-asus-ally.h b/drivers/hid/hid-asus-ally.h new file mode 100644 index 000000000000..c83817589082 --- /dev/null +++ b/drivers/hid/hid-asus-ally.h @@ -0,0 +1,398 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * + * HID driver for Asus ROG laptops and Ally + * + * Copyright (c) 2023 Luke Jones + */ + +#include +#include + +/* + * the xpad_mode is used inside the mode setting packet and is used + * for indexing (xpad_mode - 1) + */ +enum xpad_mode { + xpad_mode_game = 0x01, + xpad_mode_wasd = 0x02, + xpad_mode_mouse = 0x03, +}; + +/* the xpad_cmd determines which feature is set or queried */ +enum xpad_cmd { + xpad_cmd_set_mode = 0x01, + xpad_cmd_set_mapping = 0x02, + xpad_cmd_set_js_dz = 0x04, /* deadzones */ + xpad_cmd_set_tr_dz = 0x05, /* deadzones */ + xpad_cmd_set_vibe_intensity = 0x06, + xpad_cmd_set_leds = 0x08, + xpad_cmd_check_ready = 0x0A, + xpad_cmd_set_turbo = 0x0F, + xpad_cmd_set_response_curve = 0x13, + xpad_cmd_set_adz = 0x18, +}; + +/* the xpad_cmd determines which feature is set or queried */ +enum xpad_cmd_len { + xpad_cmd_len_mode = 0x01, + xpad_cmd_len_mapping = 0x2c, + xpad_cmd_len_deadzone = 0x04, + xpad_cmd_len_vibe_intensity = 0x02, + xpad_cmd_len_leds = 0x0C, + xpad_cmd_len_turbo = 0x20, + xpad_cmd_len_response_curve = 0x09, + xpad_cmd_len_adz = 0x02, +}; + +/* Values correspond to the actual HID byte value required */ +enum btn_pair_index { + btn_pair_dpad_u_d = 0x01, + btn_pair_dpad_l_r = 0x02, + btn_pair_ls_rs = 0x03, + btn_pair_lb_rb = 0x04, + btn_pair_a_b = 0x05, + btn_pair_x_y = 0x06, + btn_pair_view_menu = 0x07, + btn_pair_m1_m2 = 0x08, + btn_pair_lt_rt = 0x09, +}; + +#define BTN_PAD_A 0x0101000000000000 +#define BTN_PAD_B 0x0102000000000000 +#define BTN_PAD_X 0x0103000000000000 +#define BTN_PAD_Y 0x0104000000000000 +#define BTN_PAD_LB 0x0105000000000000 +#define BTN_PAD_RB 0x0106000000000000 +#define BTN_PAD_LS 0x0107000000000000 +#define BTN_PAD_RS 0x0108000000000000 +#define BTN_PAD_DPAD_UP 0x0109000000000000 +#define BTN_PAD_DPAD_DOWN 0x010A000000000000 +#define BTN_PAD_DPAD_LEFT 0x010B000000000000 +#define BTN_PAD_DPAD_RIGHT 0x010C000000000000 +#define BTN_PAD_LT 0x010D000000000000 +#define BTN_PAD_RT 0x010E000000000000 +#define BTN_PAD_VIEW 0x0111000000000000 +#define BTN_PAD_MENU 0x0112000000000000 +#define BTN_PAD_XBOX 0x0113000000000000 + +#define BTN_KB_M2 0x02008E0000000000 +#define BTN_KB_M1 0x02008F0000000000 +#define BTN_KB_ESC 0x0200760000000000 +#define BTN_KB_F1 0x0200500000000000 +#define BTN_KB_F2 0x0200600000000000 +#define BTN_KB_F3 0x0200400000000000 +#define BTN_KB_F4 0x02000C0000000000 +#define BTN_KB_F5 0x0200030000000000 +#define BTN_KB_F6 0x02000B0000000000 +#define BTN_KB_F7 0x0200800000000000 +#define BTN_KB_F8 0x02000A0000000000 +#define BTN_KB_F9 0x0200010000000000 +#define BTN_KB_F10 0x0200090000000000 +#define BTN_KB_F11 0x0200780000000000 +#define BTN_KB_F12 0x0200070000000000 +#define BTN_KB_F14 0x0200180000000000 +#define BTN_KB_F15 0x0200100000000000 +#define BTN_KB_BACKTICK 0x02000E0000000000 +#define BTN_KB_1 0x0200160000000000 +#define BTN_KB_2 0x02001E0000000000 +#define BTN_KB_3 0x0200260000000000 +#define BTN_KB_4 0x0200250000000000 +#define BTN_KB_5 0x02002E0000000000 +#define BTN_KB_6 0x0200360000000000 +#define BTN_KB_7 0x02003D0000000000 +#define BTN_KB_8 0x02003E0000000000 +#define BTN_KB_9 0x0200460000000000 +#define BTN_KB_0 0x0200450000000000 +#define BTN_KB_HYPHEN 0x02004E0000000000 +#define BTN_KB_EQUALS 0x0200550000000000 +#define BTN_KB_BACKSPACE 0x0200660000000000 +#define BTN_KB_TAB 0x02000D0000000000 +#define BTN_KB_Q 0x0200150000000000 +#define BTN_KB_W 0x02001D0000000000 +#define BTN_KB_E 0x0200240000000000 +#define BTN_KB_R 0x02002D0000000000 +#define BTN_KB_T 0x02002C0000000000 +#define BTN_KB_Y 0x0200350000000000 +#define BTN_KB_U 0x02003C0000000000 +#define BTN_KB_O 0x0200440000000000 +#define BTN_KB_P 0x02004D0000000000 +#define BTN_KB_LBRACKET 0x0200540000000000 +#define BTN_KB_RBRACKET 0x02005B0000000000 +#define BTN_KB_BACKSLASH 0x02005D0000000000 +#define BTN_KB_CAPS 0x0200580000000000 +#define BTN_KB_A 0x02001C0000000000 +#define BTN_KB_S 0x02001B0000000000 +#define BTN_KB_D 0x0200230000000000 +#define BTN_KB_F 0x02002B0000000000 +#define BTN_KB_G 0x0200340000000000 +#define BTN_KB_H 0x0200330000000000 +#define BTN_KB_J 0x02003B0000000000 +#define BTN_KB_K 0x0200420000000000 +#define BTN_KB_L 0x02004B0000000000 +#define BTN_KB_SEMI 0x02004C0000000000 +#define BTN_KB_QUOTE 0x0200520000000000 +#define BTN_KB_RET 0x02005A0000000000 +#define BTN_KB_LSHIFT 0x0200880000000000 +#define BTN_KB_Z 0x02001A0000000000 +#define BTN_KB_X 0x0200220000000000 +#define BTN_KB_C 0x0200210000000000 +#define BTN_KB_V 0x02002A0000000000 +#define BTN_KB_B 0x0200320000000000 +#define BTN_KB_N 0x0200310000000000 +#define BTN_KB_M 0x02003A0000000000 +#define BTN_KB_COMMA 0x0200410000000000 +#define BTN_KB_PERIOD 0x0200490000000000 +#define BTN_KB_RSHIFT 0x0200890000000000 +#define BTN_KB_LCTL 0x02008C0000000000 +#define BTN_KB_META 0x0200820000000000 +#define BTN_KB_LALT 0x02008A0000000000 +#define BTN_KB_SPACE 0x0200290000000000 +#define BTN_KB_RALT 0x02008B0000000000 +#define BTN_KB_MENU 0x0200840000000000 +#define BTN_KB_RCTL 0x02008D0000000000 +#define BTN_KB_PRNTSCN 0x0200C30000000000 +#define BTN_KB_SCRLCK 0x02007E0000000000 +#define BTN_KB_PAUSE 0x0200910000000000 +#define BTN_KB_INS 0x0200C20000000000 +#define BTN_KB_HOME 0x0200940000000000 +#define BTN_KB_PGUP 0x0200960000000000 +#define BTN_KB_DEL 0x0200C00000000000 +#define BTN_KB_END 0x0200950000000000 +#define BTN_KB_PGDWN 0x0200970000000000 +#define BTN_KB_UP_ARROW 0x0200980000000000 +#define BTN_KB_DOWN_ARROW 0x0200990000000000 +#define BTN_KB_LEFT_ARROW 0x0200910000000000 +#define BTN_KB_RIGHT_ARROW 0x02009B0000000000 + +#define BTN_NUMPAD_LOCK 0x0200770000000000 +#define BTN_NUMPAD_FWDSLASH 0x0200900000000000 +#define BTN_NUMPAD_ASTERISK 0x02007C0000000000 +#define BTN_NUMPAD_HYPHEN 0x02007B0000000000 +#define BTN_NUMPAD_0 0x0200700000000000 +#define BTN_NUMPAD_1 0x0200690000000000 +#define BTN_NUMPAD_2 0x0200720000000000 +#define BTN_NUMPAD_3 0x02007A0000000000 +#define BTN_NUMPAD_4 0x02006B0000000000 +#define BTN_NUMPAD_5 0x0200730000000000 +#define BTN_NUMPAD_6 0x0200740000000000 +#define BTN_NUMPAD_7 0x02006C0000000000 +#define BTN_NUMPAD_8 0x0200750000000000 +#define BTN_NUMPAD_9 0x02007D0000000000 +#define BTN_NUMPAD_PLUS 0x0200790000000000 +#define BTN_NUMPAD_ENTER 0x0200810000000000 +#define BTN_NUMPAD_PERIOD 0x0200710000000000 + +#define BTN_MOUSE_LCLICK 0x0300000001000000 +#define BTN_MOUSE_RCLICK 0x0300000002000000 +#define BTN_MOUSE_MCLICK 0x0300000003000000 +#define BTN_MOUSE_WHEEL_UP 0x0300000004000000 +#define BTN_MOUSE_WHEEL_DOWN 0x0300000005000000 + +#define BTN_MEDIA_SCREENSHOT 0x0500001600000000 +#define BTN_MEDIA_SHOW_KEYBOARD 0x0500001900000000 +#define BTN_MEDIA_SHOW_DESKTOP 0x0500001C00000000 +#define BTN_MEDIA_START_RECORDING 0x0500001E00000000 +#define BTN_MEDIA_MIC_OFF 0x0500000100000000 +#define BTN_MEDIA_VOL_DOWN 0x0500000200000000 +#define BTN_MEDIA_VOL_UP 0x0500000300000000 + +#define ALLY_DEVICE_ATTR_WO(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0200, NULL, _name##_store) + +/* required so we can have nested attributes with same name but different functions */ +#define ALLY_DEVICE_ATTR_RW(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0644, _name##_show, _name##_store) + +#define ALLY_DEVICE_ATTR_RO(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name = \ + __ATTR(_sysfs_name, 0444, _name##_show, NULL) + +/* button specific macros */ +#define ALLY_BTN_SHOW(_fname, _btn_name, _secondary) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + const char* name; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + name = btn_to_name(_secondary ? btn->macro : btn->button); \ + return sysfs_emit(buf, "%s\n", name); \ + } + +#define ALLY_BTN_STORE(_fname, _btn_name, _secondary) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + u64 code; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + code = name_to_btn(buf); \ + if (_secondary) \ + btn->macro = code; \ + else \ + btn->button = code; \ + return count; \ + } + +#define ALLY_TURBO_SHOW(_fname, _btn_name) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + return sysfs_emit(buf, "%d\n", btn->turbo); \ + } + +#define ALLY_TURBO_STORE(_fname, _btn_name) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct btn_data *btn; \ + bool turbo; \ + int ret; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + btn = &ally_cfg->key_mapping[ally_cfg->mode - 1]._btn_name; \ + ret = kstrtobool(buf, &turbo); \ + if (ret) \ + return ret; \ + btn->turbo = turbo; \ + return count; \ + } + +#define ALLY_DEADZONE_SHOW(_fname, _axis_name) \ + static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct deadzone *dz; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + dz = &ally_cfg->_axis_name; \ + return sysfs_emit(buf, "%d %d\n", dz->inner, dz->outer); \ + } + +#define ALLY_DEADZONE_STORE(_fname, _axis_name) \ + static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + struct hid_device *hdev = to_hid_device(dev); \ + u32 inner, outer; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + if (sscanf(buf, "%d %d", &inner, &outer) != 2) \ + return -EINVAL; \ + if (inner > 64 || outer > 64 || inner > outer) \ + return -EINVAL; \ + ally_cfg->_axis_name.inner = inner; \ + ally_cfg->_axis_name.outer = outer; \ + _gamepad_apply_deadzones(hdev, ally_cfg); \ + return count; \ + } + +#define ALLY_DEADZONES(_fname, _mname) \ + ALLY_DEADZONE_SHOW(_fname##_deadzone, _mname); \ + ALLY_DEADZONE_STORE(_fname##_deadzone, _mname); \ + ALLY_DEVICE_ATTR_RW(_fname##_deadzone, deadzone) + +/* response curve macros */ +#define ALLY_RESP_CURVE_SHOW(_fname, _mname) \ +static ssize_t _fname##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + return sysfs_emit(buf, "%d\n", ally_cfg->ls_rc._mname); \ + } + +#define ALLY_RESP_CURVE_STORE(_fname, _mname) \ +static ssize_t _fname##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct ally_gamepad_cfg *ally_cfg = drvdata.gamepad_cfg; \ + int ret, val; \ + if (!drvdata.gamepad_cfg) \ + return -ENODEV; \ + ret = kstrtoint(buf, 0, &val); \ + if (ret) \ + return ret; \ + if (val < 0 || val > 100) \ + return -EINVAL; \ + ally_cfg->ls_rc._mname = val; \ + return count; \ + } + +/* _point_n must start at 1 */ +#define ALLY_JS_RC_POINT(_fname, _mname, _num) \ + ALLY_RESP_CURVE_SHOW(_fname##_##_mname##_##_num, _mname##_pct_##_num); \ + ALLY_RESP_CURVE_STORE(_fname##_##_mname##_##_num, _mname##_pct_##_num); \ + ALLY_DEVICE_ATTR_RW(_fname##_##_mname##_##_num, curve_##_mname##_pct_##_num) + +#define ALLY_BTN_ATTRS_GROUP(_name, _fname) \ + static struct attribute *_fname##_attrs[] = { \ + &dev_attr_##_fname.attr, \ + &dev_attr_##_fname##_macro.attr, \ + }; \ + static const struct attribute_group _fname##_attr_group = { \ + .name = __stringify(_name), \ + .attrs = _fname##_attrs, \ + } + +#define _ALLY_BTN_REMAP(_fname, _btn_name) \ + ALLY_BTN_SHOW(btn_mapping_##_fname##_remap, _btn_name, false); \ + ALLY_BTN_STORE(btn_mapping_##_fname##_remap, _btn_name, false); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_remap, remap); + +#define _ALLY_BTN_MACRO(_fname, _btn_name) \ + ALLY_BTN_SHOW(btn_mapping_##_fname##_macro, _btn_name, true); \ + ALLY_BTN_STORE(btn_mapping_##_fname##_macro, _btn_name, true); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_macro, macro_remap); + +#define ALLY_BTN_MAPPING(_fname, _btn_name) \ + _ALLY_BTN_REMAP(_fname, _btn_name) \ + _ALLY_BTN_MACRO(_fname, _btn_name) \ + static struct attribute *_fname##_attrs[] = { \ + &dev_attr_btn_mapping_##_fname##_remap.attr, \ + &dev_attr_btn_mapping_##_fname##_macro.attr, \ + NULL, \ + }; \ + static const struct attribute_group btn_mapping_##_fname##_attr_group = { \ + .name = __stringify(btn_##_fname), \ + .attrs = _fname##_attrs, \ + } + +#define ALLY_TURBO_BTN_MAPPING(_fname, _btn_name) \ + _ALLY_BTN_REMAP(_fname, _btn_name) \ + _ALLY_BTN_MACRO(_fname, _btn_name) \ + ALLY_TURBO_SHOW(btn_mapping_##_fname##_turbo, _btn_name); \ + ALLY_TURBO_STORE(btn_mapping_##_fname##_turbo, _btn_name); \ + ALLY_DEVICE_ATTR_RW(btn_mapping_##_fname##_turbo, turbo); \ + static struct attribute *_fname##_turbo_attrs[] = { \ + &dev_attr_btn_mapping_##_fname##_remap.attr, \ + &dev_attr_btn_mapping_##_fname##_macro.attr, \ + &dev_attr_btn_mapping_##_fname##_turbo.attr, \ + NULL, \ + }; \ + static const struct attribute_group btn_mapping_##_fname##_attr_group = { \ + .name = __stringify(btn_##_fname), \ + .attrs = _fname##_turbo_attrs, \ + } diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 472bca54642b..96bbd8161f49 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -23,6 +23,7 @@ /* */ +#include "linux/export.h" #include #include #include @@ -34,6 +35,7 @@ #include #include "hid-ids.h" +#include "hid-asus.h" MODULE_AUTHOR("Yusuke Fujimaki "); MODULE_AUTHOR("Brendan McGrath "); @@ -602,7 +604,7 @@ static int mcu_request_version(struct hid_device *hdev) return ret; } -static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) +void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) { int min_version, version; @@ -630,12 +632,11 @@ static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) set_ally_mcu_powersave(true); } } +EXPORT_SYMBOL_NS(validate_mcu_fw_version, "HID_ASUS"); static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); - struct usb_interface *intf; - struct usb_device *udev; unsigned char kbd_func; int ret; @@ -660,12 +661,14 @@ static int asus_kbd_register_leds(struct hid_device *hdev) return ret; } + #if !IS_REACHABLE(CONFIG_HID_ASUS_ALLY) if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { - intf = to_usb_interface(hdev->dev.parent); - udev = interface_to_usbdev(intf); + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = interface_to_usbdev(intf); validate_mcu_fw_version(hdev, le16_to_cpu(udev->descriptor.idProduct)); } + #endif /* !IS_REACHABLE(CONFIG_HID_ASUS_ALLY) */ } else { /* Initialize keyboard */ @@ -1126,8 +1129,10 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev) static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) { - int ret; struct asus_drvdata *drvdata; + struct usb_host_endpoint *ep; + struct usb_interface *intf; + int ret; drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); if (drvdata == NULL) { @@ -1139,6 +1144,18 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) drvdata->quirks = id->driver_data; + /* Ignore these endpoints as they are used by hid-asus-ally */ + #if IS_REACHABLE(CONFIG_HID_ASUS_ALLY) + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + intf = to_usb_interface(hdev->dev.parent); + ep = intf->cur_altsetting->endpoint; + if (ep->desc.bEndpointAddress == ROG_ALLY_X_INTF_IN || + ep->desc.bEndpointAddress == ROG_ALLY_CFG_INTF_IN || + ep->desc.bEndpointAddress == ROG_ALLY_CFG_INTF_OUT) + return -ENODEV; + } + #endif /* IS_REACHABLE(CONFIG_HID_ASUS_ALLY) */ + /* * T90CHI's keyboard dock returns same ID values as T100CHI's dock. * Thus, identify T90CHI dock with product name string. diff --git a/drivers/hid/hid-asus.h b/drivers/hid/hid-asus.h new file mode 100644 index 000000000000..f67dd5a3a1bc --- /dev/null +++ b/drivers/hid/hid-asus.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __HID_ASUS_H +#define __HID_ASUS_H + +#include + +#define ROG_ALLY_CFG_INTF_IN 0x83 +#define ROG_ALLY_CFG_INTF_OUT 0x04 +#define ROG_ALLY_X_INTF_IN 0x87 + +void validate_mcu_fw_version(struct hid_device *hdev, int idProduct); + +#endif /* __HID_ASUS_H */ diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c index b353fd819989..7734cc87494c 100644 --- a/drivers/hid/hid-core.c +++ b/drivers/hid/hid-core.c @@ -2896,6 +2896,11 @@ static int hid_uevent(const struct device *dev, struct kobj_uevent_env *env) if (add_uevent_var(env, "MODALIAS=hid:b%04Xg%04Xv%08Xp%08X", hdev->bus, hdev->group, hdev->vendor, hdev->product)) return -ENOMEM; + if (hdev->firmware_version) { + if (add_uevent_var(env, "HID_FIRMWARE_VERSION=0x%04llX", + hdev->firmware_version)) + return -ENOMEM; + } return 0; } diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index adad71cc8bf0..4329c41274b5 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -235,6 +235,7 @@ #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2 0x19b6 #define USB_DEVICE_ID_ASUSTEK_ROG_Z13_FOLIO 0x1a30 #define USB_DEVICE_ID_ASUSTEK_ROG_Z13_LIGHTBAR 0x18c6 +#define USB_DEVICE_ID_ASUSTEK_ROG_RAIKIRI_PAD 0x1abb #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY 0x1abe #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X 0x1b4c #define USB_DEVICE_ID_ASUSTEK_ROG_CLAYMORE_II_KEYBOARD 0x196b @@ -739,6 +740,10 @@ #define USB_DEVICE_ID_ITE8595 0x8595 #define USB_DEVICE_ID_ITE_MEDION_E1239T 0xce50 +#define USB_VENDOR_ID_QHE 0x1a86 +#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT 0xe310 +#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT 0xe311 + #define USB_VENDOR_ID_JABRA 0x0b0e #define USB_DEVICE_ID_JABRA_SPEAK_410 0x0412 #define USB_DEVICE_ID_JABRA_SPEAK_510 0x0420 @@ -857,7 +862,10 @@ #define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_602E 0x602e #define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_6093 0x6093 #define USB_DEVICE_ID_LENOVO_LEGION_GO_DUAL_DINPUT 0x6184 +#define USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT 0x61eb +#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT 0x61ec #define USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT 0x61ed +#define USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS 0x61ee #define USB_VENDOR_ID_LETSKETCH 0x6161 #define USB_DEVICE_ID_WP9620N 0x4d15 @@ -1075,6 +1083,7 @@ #define USB_VENDOR_ID_NOVATEK 0x0603 #define USB_DEVICE_ID_NOVATEK_PCT 0x0600 #define USB_DEVICE_ID_NOVATEK_MOUSE 0x1602 +#define I2C_DEVICE_ID_ONEXPLAYER_X1 0xF001 #define USB_VENDOR_ID_NTI 0x0757 #define USB_DEVICE_ID_USB_SUN 0x0a00 @@ -1533,6 +1542,10 @@ #define USB_VENDOR_ID_ZYTRONIC 0x14c8 #define USB_DEVICE_ID_ZYTRONIC_ZXY100 0x0005 +#define USB_VENDOR_ID_ZOTAC 0x1EE9 +#define USB_VENDOR_ID_ZOTAC_ALT 0x1E19 +#define USB_DEVICE_ID_ZOTAC_ZONE_GAME_CONTROLLER 0x1590 + #define USB_VENDOR_ID_PRIMAX 0x0461 #define USB_DEVICE_ID_PRIMAX_MOUSE_4D22 0x4d22 #define USB_DEVICE_ID_PRIMAX_MOUSE_4E2A 0x4e2a diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c index 2633fcd8f910..639e2e22bda6 100644 --- a/drivers/hid/hid-input.c +++ b/drivers/hid/hid-input.c @@ -403,6 +403,8 @@ static const struct hid_device_id hid_battery_quirks[] = { * set HID_BATTERY_QUIRK_IGNORE for all Elan I2C and USB HID devices. */ { HID_I2C_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_IGNORE }, + { HID_I2C_DEVICE(USB_VENDOR_ID_NOVATEK, I2C_DEVICE_ID_ONEXPLAYER_X1), + HID_BATTERY_QUIRK_IGNORE }, { HID_USB_DEVICE(USB_VENDOR_ID_ELAN, HID_ANY_ID), HID_BATTERY_QUIRK_IGNORE }, {} }; diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c new file mode 100644 index 000000000000..11000cf54e56 --- /dev/null +++ b/drivers/hid/hid-lenovo-go-s.c @@ -0,0 +1,1577 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for Lenovo Legion Go S devices. + * + * Copyright (c) 2025 Derek J. Clark + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define GO_S_CFG_INTF_IN 0x84 +#define GO_S_PACKET_SIZE 64 + +struct hid_gos_cfg { + unsigned char *buf; + struct delayed_work gos_cfg_setup; + struct completion send_cmd_complete; + struct led_classdev *led_cdev; + struct hid_device *hdev; + struct mutex cfg_mutex; /*ensure single synchronous output report*/ + u8 gp_auto_sleep_time; + u8 gp_dpad_mode; + u8 gp_mode; + u8 gp_poll_rate; + u8 imu_bypass_en; + u8 imu_manufacturer; + u8 imu_sensor_en; + u8 mcu_id[12]; + u8 mouse_step; + u8 os_mode; + u8 rgb_effect; + u8 rgb_en; + u8 rgb_mode; + u8 rgb_profile; + u8 rgb_speed; + u8 tp_en; + u8 tp_linux_mode; + u8 tp_manufacturer; + u8 tp_version; + u8 tp_windows_mode; +} drvdata; + +struct gos_cfg_attr { + u8 index; +}; + +struct command_report { + u8 cmd; + u8 sub_cmd; + u8 data[63]; +} __packed; + +struct version_report { + u8 cmd; + u32 version; + u8 reserved[59]; +} __packed; + +enum mcu_command_index { + GET_VERSION = 0x01, + GET_MCU_ID, + GET_GAMEPAD_CFG, + SET_GAMEPAD_CFG, + GET_TP_PARAM, + SET_TP_PARAM, + GET_RGB_CFG = 0x0f, + SET_RGB_CFG, + GET_PL_TEST = 0xdf, +}; + +enum feature_enabled_index { + FEATURE_DISABLED, + FEATURE_ENABLED, +}; + +static const char *const feature_enabled_text[] = { + [FEATURE_DISABLED] = "false", + [FEATURE_ENABLED] = "true", +}; + +enum feature_status_index { + FEATURE_NONE = 0x00, + FEATURE_GAMEPAD_MODE = 0x01, + FEATURE_AUTO_SLEEP_TIME = 0x04, + FEATURE_IMU_BYPASS, + FEATURE_RGB_ENABLE, + FEATURE_IMU_ENABLE, + FEATURE_TOUCHPAD_ENABLE, + FEATURE_OS_MODE = 0x0A, + FEATURE_POLL_RATE = 0x10, + FEATURE_DPAD_MODE, + FEATURE_MOUSE_WHEEL_STEP, +}; + +enum gamepad_mode_index { + XINPUT, + DINPUT, +}; + +static const char *const gamepad_mode_text[] = { + [XINPUT] = "xinput", + [DINPUT] = "dinput", +}; + +enum os_type_index { + WINDOWS, + LINUX, +}; + +static const char *const os_type_text[] = { + [WINDOWS] = "windows", + [LINUX] = "linux", +}; + +enum poll_rate_index { + HZ125, + HZ250, + HZ500, + HZ1000, +}; + +static const char *const poll_rate_text[] = { + [HZ125] = "125", + [HZ250] = "250", + [HZ500] = "500", + [HZ1000] = "1000", +}; + +enum dpad_mode_index { + DIR8, + DIR4, +}; + +static const char *const dpad_mode_text[] = { + [DIR8] = "8-way", + [DIR4] = "4-way", +}; + +enum touchpad_mode_index { + TP_REL, + TP_ABS, +}; + +static const char *const touchpad_mode_text[] = { + [TP_REL] = "relative", + [TP_ABS] = "absolute", +}; + +enum touchpad_config_index { + CFG_WINDOWS_MODE = 0x03, + CFG_LINUX_MODE, + +}; + +enum rgb_mode_index { + RGB_MODE_DYNAMIC, + RGB_MODE_CUSTOM, +}; + +static const char *const rgb_mode_text[] = { + [RGB_MODE_DYNAMIC] = "dynamic", + [RGB_MODE_CUSTOM] = "custom", +}; + +enum rgb_effect_index { + RGB_EFFECT_MONO, + RGB_EFFECT_BREATHE, + RGB_EFFECT_CHROMA, + RGB_EFFECT_RAINBOW, +}; + +static const char *const rgb_effect_text[] = { + [RGB_EFFECT_MONO] = "monocolor", + [RGB_EFFECT_BREATHE] = "breathe", + [RGB_EFFECT_CHROMA] = "chroma", + [RGB_EFFECT_RAINBOW] = "rainbow", +}; + +enum rgb_config_index { + LIGHT_MODE_SEL = 0x01, + LIGHT_PROFILE_SEL, + USR_LIGHT_PROFILE_1, + USR_LIGHT_PROFILE_2, + USR_LIGHT_PROFILE_3, +}; + +enum test_command_index { + TEST_TP_MFR = 0x02, + TEST_IMU_MFR, + TEST_TP_VER, +}; + +enum tp_mfr_index { + TP_NONE, + TP_BETTERLIFE, + TP_SIPO, +}; + +static const char *const touchpad_manufacturer_text[] = { + [TP_NONE] = "none", + [TP_BETTERLIFE] = "BetterLife", + [TP_SIPO] = "SIPO", +}; + +enum imu_mfr_index { + IMU_NONE, + IMU_BOSCH, + IMU_ST, +}; + +static const char *const imu_manufacturer_text[] = { + [IMU_NONE] = "none", + [IMU_BOSCH] = "Bosch", + [IMU_ST] = "ST", +}; + +static int hid_gos_version_event(u8 *data) +{ + struct version_report *ver_rep = (struct version_report *)data; + + drvdata.hdev->firmware_version = get_unaligned_le32(&ver_rep->version); + return 0; +} + +static int hid_gos_mcu_id_event(struct command_report *cmd_rep) +{ + drvdata.mcu_id[0] = cmd_rep->sub_cmd; + memcpy(&drvdata.mcu_id[1], cmd_rep->data, 11); + + return 0; +} + +static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep) +{ + int ret = 0; + + switch (cmd_rep->sub_cmd) { + case FEATURE_GAMEPAD_MODE: + drvdata.gp_mode = cmd_rep->data[0]; + break; + case FEATURE_AUTO_SLEEP_TIME: + drvdata.gp_auto_sleep_time = cmd_rep->data[0]; + break; + case FEATURE_IMU_BYPASS: + drvdata.imu_bypass_en = cmd_rep->data[0]; + break; + case FEATURE_RGB_ENABLE: + drvdata.rgb_en = cmd_rep->data[0]; + break; + case FEATURE_IMU_ENABLE: + drvdata.imu_sensor_en = cmd_rep->data[0]; + break; + case FEATURE_TOUCHPAD_ENABLE: + drvdata.tp_en = cmd_rep->data[0]; + break; + case FEATURE_OS_MODE: + drvdata.os_mode = cmd_rep->data[0]; + break; + case FEATURE_POLL_RATE: + drvdata.gp_poll_rate = cmd_rep->data[0]; + break; + case FEATURE_DPAD_MODE: + drvdata.gp_dpad_mode = cmd_rep->data[0]; + break; + case FEATURE_MOUSE_WHEEL_STEP: + drvdata.mouse_step = cmd_rep->data[0]; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int hid_gos_touchpad_event(struct command_report *cmd_rep) +{ + int ret = 0; + + switch (cmd_rep->sub_cmd) { + case CFG_LINUX_MODE: + drvdata.tp_linux_mode = cmd_rep->data[0]; + break; + case CFG_WINDOWS_MODE: + drvdata.tp_windows_mode = cmd_rep->data[0]; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int hid_gos_pl_test_event(struct command_report *cmd_rep) +{ + int ret = 0; + + switch (cmd_rep->sub_cmd) { + case TEST_TP_MFR: + drvdata.tp_manufacturer = cmd_rep->data[0]; + ret = 0; + break; + case TEST_IMU_MFR: + drvdata.imu_manufacturer = cmd_rep->data[0]; + ret = 0; + break; + case TEST_TP_VER: + drvdata.tp_version = cmd_rep->data[0]; + ret = 0; + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static int hid_gos_light_event(struct command_report *cmd_rep) +{ + struct led_classdev_mc *mc_cdev; + int ret = 0; + + switch (cmd_rep->sub_cmd) { + case LIGHT_MODE_SEL: + drvdata.rgb_mode = cmd_rep->data[0]; + ret = 0; + break; + case LIGHT_PROFILE_SEL: + drvdata.rgb_profile = cmd_rep->data[0]; + ret = 0; + break; + case USR_LIGHT_PROFILE_1: + case USR_LIGHT_PROFILE_2: + case USR_LIGHT_PROFILE_3: + mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + drvdata.rgb_effect = cmd_rep->data[0]; + mc_cdev->subled_info[0].intensity = cmd_rep->data[1]; + mc_cdev->subled_info[1].intensity = cmd_rep->data[2]; + mc_cdev->subled_info[2].intensity = cmd_rep->data[3]; + drvdata.led_cdev->brightness = cmd_rep->data[4]; + drvdata.rgb_speed = cmd_rep->data[5]; + ret = 0; + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static int hid_gos_set_event_return(struct command_report *cmd_rep) +{ + if (cmd_rep->data[0] != 0) + return -EIO; + + return 0; +} + +static u8 get_endpoint_address(struct hid_device *hdev) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_host_endpoint *ep; + + if (intf) { + ep = intf->cur_altsetting->endpoint; + if (ep) + return ep->desc.bEndpointAddress; + } + + return -ENODEV; +} + +static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct command_report *cmd_rep; + int ep, ret; + + if (size != GO_S_PACKET_SIZE) + goto passthrough; + + ep = get_endpoint_address(hdev); + if (ep != GO_S_CFG_INTF_IN) + goto passthrough; + + cmd_rep = (struct command_report *)data; + + switch (cmd_rep->cmd) { + case GET_VERSION: + ret = hid_gos_version_event(data); + break; + case GET_MCU_ID: + ret = hid_gos_mcu_id_event(cmd_rep); + break; + case GET_GAMEPAD_CFG: + ret = hid_gos_gamepad_cfg_event(cmd_rep); + break; + case GET_TP_PARAM: + ret = hid_gos_touchpad_event(cmd_rep); + break; + case GET_PL_TEST: + ret = hid_gos_pl_test_event(cmd_rep); + break; + case GET_RGB_CFG: + ret = hid_gos_light_event(cmd_rep); + break; + case SET_GAMEPAD_CFG: + case SET_RGB_CFG: + case SET_TP_PARAM: + ret = hid_gos_set_event_return(cmd_rep); + break; + default: + ret = -EINVAL; + break; + } + dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n", + GO_S_PACKET_SIZE, data); + + complete(&drvdata.send_cmd_complete); + return ret; + +passthrough: + /* Forward other HID reports so they generate events */ + hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1); + return 0; +} + +static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index, + u8 *data, size_t len) +{ + u8 header[] = { command, index }; + size_t header_size = ARRAY_SIZE(header); + size_t total_size = header_size + len; + int timeout = 5; + int ret; + + /* PL_TEST commands can take longer because they go out to another device */ + if (command == GET_PL_TEST) + timeout = 200; + + guard(mutex)(&drvdata.cfg_mutex); + memcpy(drvdata.buf, header, header_size); + memcpy(drvdata.buf + header_size, data, len); + memset(drvdata.buf + total_size, 0, GO_S_PACKET_SIZE - total_size); + + dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", + GO_S_PACKET_SIZE, drvdata.buf); + + ret = hid_hw_output_report(hdev, drvdata.buf, GO_S_PACKET_SIZE); + if (ret < 0) + return ret; + + ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL; + if (ret) + return ret; + + ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete, + msecs_to_jiffies(timeout)); + + if (ret == 0) /* timeout occurred */ + ret = -EBUSY; + if (ret > 0) /* timeout/interrupt didn't occur */ + ret = 0; + + reinit_completion(&drvdata.send_cmd_complete); + return ret; +} + +static ssize_t gamepad_property_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + enum feature_status_index index) +{ + size_t size = 1; + u8 val = 0; + int ret; + + switch (index) { + case FEATURE_GAMEPAD_MODE: + ret = sysfs_match_string(gamepad_mode_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_AUTO_SLEEP_TIME: + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val < 0 || val > 255) + return -EINVAL; + break; + case FEATURE_IMU_ENABLE: + ret = sysfs_match_string(feature_enabled_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_IMU_BYPASS: + ret = sysfs_match_string(feature_enabled_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_RGB_ENABLE: + ret = sysfs_match_string(feature_enabled_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_TOUCHPAD_ENABLE: + ret = sysfs_match_string(feature_enabled_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_OS_MODE: + ret = sysfs_match_string(os_type_text, buf); + if (ret < 0) + return ret; + val = ret; + drvdata.os_mode = val; + break; + case FEATURE_POLL_RATE: + ret = sysfs_match_string(poll_rate_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_DPAD_MODE: + ret = sysfs_match_string(dpad_mode_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case FEATURE_MOUSE_WHEEL_STEP: + ret = kstrtou8(buf, 10, &val); + if (ret) + return ret; + if (val < 1 || val > 127) + return -EINVAL; + break; + default: + return -EINVAL; + } + + if (!val) + size = 0; + + ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, index, &val, + size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t gamepad_property_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum feature_status_index index) +{ + size_t count = 0; + u8 i; + + count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, 0, 0); + if (count < 0) + return count; + + switch (index) { + case FEATURE_GAMEPAD_MODE: + i = drvdata.gp_mode; + if (i >= ARRAY_SIZE(gamepad_mode_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]); + break; + case FEATURE_AUTO_SLEEP_TIME: + count = sysfs_emit(buf, "%u\n", drvdata.gp_auto_sleep_time); + break; + case FEATURE_IMU_ENABLE: + i = drvdata.imu_sensor_en; + if (i >= ARRAY_SIZE(feature_enabled_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); + break; + case FEATURE_IMU_BYPASS: + i = drvdata.imu_bypass_en; + if (i >= ARRAY_SIZE(feature_enabled_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); + break; + case FEATURE_RGB_ENABLE: + i = drvdata.rgb_en; + if (i >= ARRAY_SIZE(feature_enabled_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); + break; + case FEATURE_TOUCHPAD_ENABLE: + i = drvdata.tp_en; + if (i >= ARRAY_SIZE(feature_enabled_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); + break; + case FEATURE_OS_MODE: + i = drvdata.os_mode; + if (i >= ARRAY_SIZE(os_type_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", os_type_text[i]); + break; + case FEATURE_POLL_RATE: + i = drvdata.gp_poll_rate; + if (i >= ARRAY_SIZE(poll_rate_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", poll_rate_text[i]); + break; + case FEATURE_DPAD_MODE: + i = drvdata.gp_dpad_mode; + if (i >= ARRAY_SIZE(dpad_mode_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", dpad_mode_text[i]); + break; + case FEATURE_MOUSE_WHEEL_STEP: + i = drvdata.mouse_step; + if (i < 1 || i > 127) + return -EINVAL; + count = sysfs_emit(buf, "%u\n", i); + break; + default: + return -EINVAL; + } + + return count; +} + +static ssize_t gamepad_property_options(struct device *dev, + struct device_attribute *attr, + char *buf, + enum feature_status_index index) +{ + size_t count = 0; + unsigned int i; + + switch (index) { + case FEATURE_GAMEPAD_MODE: + for (i = 0; i < ARRAY_SIZE(gamepad_mode_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + gamepad_mode_text[i]); + } + break; + case FEATURE_AUTO_SLEEP_TIME: + return sysfs_emit(buf, "0-255\n"); + case FEATURE_IMU_ENABLE: + for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + feature_enabled_text[i]); + } + break; + case FEATURE_IMU_BYPASS: + case FEATURE_RGB_ENABLE: + case FEATURE_TOUCHPAD_ENABLE: + for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + feature_enabled_text[i]); + } + break; + case FEATURE_OS_MODE: + for (i = 0; i < ARRAY_SIZE(os_type_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + os_type_text[i]); + } + break; + case FEATURE_POLL_RATE: + for (i = 0; i < ARRAY_SIZE(poll_rate_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + poll_rate_text[i]); + } + break; + case FEATURE_DPAD_MODE: + for (i = 0; i < ARRAY_SIZE(dpad_mode_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + dpad_mode_text[i]); + } + break; + case FEATURE_MOUSE_WHEEL_STEP: + return sysfs_emit(buf, "1-127\n"); + default: + return count; + } + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t touchpad_property_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + enum touchpad_config_index index) +{ + size_t size = 1; + u8 val = 0; + int ret; + + switch (index) { + case CFG_WINDOWS_MODE: + ret = sysfs_match_string(touchpad_mode_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + case CFG_LINUX_MODE: + ret = sysfs_match_string(touchpad_mode_text, buf); + if (ret < 0) + return ret; + val = ret; + break; + default: + return -EINVAL; + } + if (!val) + size = 0; + + ret = mcu_property_out(drvdata.hdev, SET_TP_PARAM, index, &val, size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t touchpad_property_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum touchpad_config_index index) +{ + int ret = 0; + u8 i; + + ret = mcu_property_out(drvdata.hdev, GET_TP_PARAM, index, 0, 0); + if (ret < 0) + return ret; + + switch (index) { + case CFG_WINDOWS_MODE: + i = drvdata.tp_windows_mode; + break; + case CFG_LINUX_MODE: + i = drvdata.tp_linux_mode; + break; + default: + return -EINVAL; + } + + if (i >= ARRAY_SIZE(touchpad_mode_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", touchpad_mode_text[i]); +} + +static ssize_t touchpad_property_options(struct device *dev, + struct device_attribute *attr, + char *buf, + enum touchpad_config_index index) +{ + size_t count = 0; + unsigned int i; + + switch (index) { + case CFG_WINDOWS_MODE: + case CFG_LINUX_MODE: + for (i = 0; i < ARRAY_SIZE(touchpad_mode_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + touchpad_mode_text[i]); + } + break; + default: + return count; + } + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t test_property_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum test_command_index index) +{ + size_t count = 0; + int ret; + u8 i; + + ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, index, 0, 0); + if (ret) + return ret; + + switch (index) { + case TEST_TP_MFR: + i = drvdata.tp_manufacturer; + if (i >= ARRAY_SIZE(touchpad_manufacturer_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", touchpad_manufacturer_text[i]); + break; + case TEST_IMU_MFR: + i = drvdata.imu_manufacturer; + if (i >= ARRAY_SIZE(imu_manufacturer_text)) + return -EINVAL; + count = sysfs_emit(buf, "%s\n", imu_manufacturer_text[i]); + break; + case TEST_TP_VER: + count = sysfs_emit(buf, "%u\n", drvdata.tp_version); + break; + default: + count = -EINVAL; + break; + } + + return count; +} + +static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id); +} + +static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd, + enum rgb_config_index index, u8 *val, size_t size) +{ + if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG) + return -EINVAL; + + if (index < LIGHT_MODE_SEL || index > USR_LIGHT_PROFILE_3) + return -EINVAL; + + return mcu_property_out(hdev, cmd, index, val, size); +} + +static int rgb_attr_show(void) +{ + enum rgb_config_index index; + + index = drvdata.rgb_profile + 2; + + return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0); +}; + +static ssize_t rgb_effect_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + u8 effect; + int ret; + + ret = sysfs_match_string(rgb_effect_text, buf); + if (ret < 0) + return ret; + + effect = ret; + index = drvdata.rgb_profile + 2; + u8 rgb_profile[6] = { effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + drvdata.led_cdev->brightness, + drvdata.rgb_speed }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + if (ret) + return ret; + + drvdata.rgb_effect = effect; + return count; +}; + +static ssize_t rgb_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = rgb_attr_show(); + if (ret) + return ret; + + if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]); +} + +static ssize_t rgb_effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t rgb_speed_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + int val = 0; + int ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val < 0 || val > 100) + return -EINVAL; + + index = drvdata.rgb_profile + 2; + u8 rgb_profile[6] = { drvdata.rgb_effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + drvdata.led_cdev->brightness, + val }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + if (ret) + return ret; + + drvdata.rgb_speed = val; + + return count; +}; + +static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = rgb_attr_show(); + if (ret) + return ret; + + if (drvdata.rgb_speed > 100) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed); +} + +static ssize_t rgb_speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-100\n"); +} + +static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(rgb_mode_text, buf); + if (ret <= 0) + return ret; + + val = ret; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, + 1); + if (ret) + return ret; + + drvdata.rgb_mode = val; + + return count; +}; + +static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0); + if (ret) + return ret; + + if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]); +}; + +static ssize_t rgb_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++) + count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t rgb_profile_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + size_t size = 1; + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret < 0) + return ret; + + if (val < 1 || val > 3) + return -EINVAL; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val, + size); + if (ret) + return ret; + + drvdata.rgb_profile = val; + + return count; +}; + +static ssize_t rgb_profile_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0, + 0); + if (ret) + return ret; + + if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile); +}; + +static ssize_t rgb_profile_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "1-3\n"); +} + +static void hid_gos_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + int ret; + + if (brightness > led_cdev->max_brightness) { + dev_err(led_cdev->dev, "Invalid argument\n"); + return; + } + + index = drvdata.rgb_profile + 2; + u8 rgb_profile[6] = { drvdata.rgb_effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + brightness, + drvdata.rgb_speed }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + switch (ret) { + case 0: + led_cdev->brightness = brightness; + break; + case -ENODEV: /* during switch to IAP -ENODEV is expected */ + case -ENOSYS: /* during rmmod -ENOSYS is expected */ + dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", + ret); + break; + default: + dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", + ret); + }; +} + +#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return _group##_property_store(dev, attr, buf, count, \ + _name.index); \ + } \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_property_show(dev, attr, buf, _name.index); \ + } \ + static ssize_t _name##_##_rtype##_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return _group##_property_options(dev, attr, buf, _name.index); \ + } \ + static DEVICE_ATTR_RW_NAMED(_name, _attrname) + +#define LEGOS_DEVICE_ATTR_RO(_name, _attrname, _group) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_property_show(dev, attr, buf, _name.index); \ + } \ + static DEVICE_ATTR_RO_NAMED(_name, _attrname) + +/* Gamepad */ +struct gos_cfg_attr auto_sleep_time = { FEATURE_AUTO_SLEEP_TIME }; +LEGOS_DEVICE_ATTR_RW(auto_sleep_time, "auto_sleep_time", range, gamepad); +static DEVICE_ATTR_RO(auto_sleep_time_range); + +struct gos_cfg_attr dpad_mode = { FEATURE_DPAD_MODE }; +LEGOS_DEVICE_ATTR_RW(dpad_mode, "dpad_mode", index, gamepad); +static DEVICE_ATTR_RO(dpad_mode_index); + +struct gos_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE }; +LEGOS_DEVICE_ATTR_RW(gamepad_mode, "mode", index, gamepad); +static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index"); + +struct gos_cfg_attr gamepad_poll_rate = { FEATURE_POLL_RATE }; +LEGOS_DEVICE_ATTR_RW(gamepad_poll_rate, "poll_rate", index, gamepad); +static DEVICE_ATTR_RO_NAMED(gamepad_poll_rate_index, "poll_rate_index"); + +static struct attribute *legos_gamepad_attrs[] = { + &dev_attr_auto_sleep_time.attr, + &dev_attr_auto_sleep_time_range.attr, + &dev_attr_dpad_mode.attr, + &dev_attr_dpad_mode_index.attr, + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + &dev_attr_gamepad_poll_rate.attr, + &dev_attr_gamepad_poll_rate_index.attr, + NULL, +}; + +static const struct attribute_group gamepad_attr_group = { + .name = "gamepad", + .attrs = legos_gamepad_attrs, +}; + +/* IMU */ +struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS }; +LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad); +static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index"); + +struct gos_cfg_attr imu_manufacturer = { TEST_IMU_MFR }; +LEGOS_DEVICE_ATTR_RO(imu_manufacturer, "manufacturer", test); + +struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE }; +LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad); +static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index"); + +static struct attribute *legos_imu_attrs[] = { + &dev_attr_imu_bypass_enabled.attr, + &dev_attr_imu_bypass_enabled_index.attr, + &dev_attr_imu_manufacturer.attr, + &dev_attr_imu_sensor_enabled.attr, + &dev_attr_imu_sensor_enabled_index.attr, + NULL, +}; + +static const struct attribute_group imu_attr_group = { + .name = "imu", + .attrs = legos_imu_attrs, +}; + +/* MCU */ +static DEVICE_ATTR_RO(mcu_id); + +struct gos_cfg_attr os_mode = { FEATURE_OS_MODE }; +LEGOS_DEVICE_ATTR_RW(os_mode, "os_mode", index, gamepad); +static DEVICE_ATTR_RO(os_mode_index); + +static struct attribute *legos_mcu_attrs[] = { + &dev_attr_mcu_id.attr, + &dev_attr_os_mode.attr, + &dev_attr_os_mode_index.attr, + NULL, +}; + +static const struct attribute_group mcu_attr_group = { + .attrs = legos_mcu_attrs, +}; + +/* Mouse */ +struct gos_cfg_attr mouse_wheel_step = { FEATURE_MOUSE_WHEEL_STEP }; +LEGOS_DEVICE_ATTR_RW(mouse_wheel_step, "step", range, gamepad); +static DEVICE_ATTR_RO_NAMED(mouse_wheel_step_range, "step_range"); + +static struct attribute *legos_mouse_attrs[] = { + &dev_attr_mouse_wheel_step.attr, + &dev_attr_mouse_wheel_step_range.attr, + NULL, +}; + +static const struct attribute_group mouse_attr_group = { + .name = "mouse", + .attrs = legos_mouse_attrs, +}; + +/* Touchpad */ +struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE }; +LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad); +static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index"); + +struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE }; +LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad); +static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index"); + +struct gos_cfg_attr touchpad_manufacturer = { TEST_TP_MFR }; +LEGOS_DEVICE_ATTR_RO(touchpad_manufacturer, "manufacturer", touchpad); + +struct gos_cfg_attr touchpad_version = { TEST_TP_VER }; +LEGOS_DEVICE_ATTR_RO(touchpad_version, "version", touchpad); + +struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE }; +LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad); +static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index"); + +static struct attribute *legos_touchpad_attrs[] = { + &dev_attr_touchpad_enabled.attr, + &dev_attr_touchpad_enabled_index.attr, + &dev_attr_touchpad_linux_mode.attr, + &dev_attr_touchpad_linux_mode_index.attr, + &dev_attr_touchpad_manufacturer.attr, + &dev_attr_touchpad_version.attr, + &dev_attr_touchpad_windows_mode.attr, + &dev_attr_touchpad_windows_mode_index.attr, + NULL, +}; + +static const struct attribute_group touchpad_attr_group = { + .name = "touchpad", + .attrs = legos_touchpad_attrs, +}; + +static const struct attribute_group *top_level_attr_groups[] = { + &gamepad_attr_group, + &imu_attr_group, + &mcu_attr_group, + &mouse_attr_group, + &touchpad_attr_group, + NULL, +}; + +/* RGB */ +struct gos_cfg_attr rgb_enabled = { FEATURE_RGB_ENABLE }; +LEGOS_DEVICE_ATTR_RW(rgb_enabled, "enabled", index, gamepad); +static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index"); + +static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); +static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index"); +static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode"); +static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index"); +static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile"); +static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range"); +static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed"); +static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range"); + +static struct attribute *gos_rgb_attrs[] = { + &dev_attr_rgb_enabled.attr, + &dev_attr_rgb_enabled_index.attr, + &dev_attr_rgb_effect.attr, + &dev_attr_rgb_effect_index.attr, + &dev_attr_rgb_mode.attr, + &dev_attr_rgb_mode_index.attr, + &dev_attr_rgb_profile.attr, + &dev_attr_rgb_profile_range.attr, + &dev_attr_rgb_speed.attr, + &dev_attr_rgb_speed_range.attr, + NULL, +}; + +static struct attribute_group rgb_attr_group = { + .attrs = gos_rgb_attrs, +}; + +struct mc_subled gos_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 0x50, + .intensity = 0x24, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 0x50, + .intensity = 0x22, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 0x50, + .intensity = 0x99, + .channel = 0x3, + }, +}; + +struct led_classdev_mc gos_cdev_rgb = { + .led_cdev = { + .name = "go_s:rgb:joystick_rings", + .brightness = 0x50, + .max_brightness = 0x64, + .brightness_set = hid_gos_brightness_set, + }, + .num_colors = ARRAY_SIZE(gos_rgb_subled_info), + .subled_info = gos_rgb_subled_info, +}; + +static void cfg_setup(struct work_struct *work) +{ + int ret; + + /* MCU */ + ret = mcu_property_out(drvdata.hdev, GET_MCU_ID, FEATURE_NONE, 0, 0); + if (ret) { + dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU ID: %i\n", + ret); + return; + } + + ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, 0, 0); + if (ret) { + dev_err(&drvdata.hdev->dev, + "Failed to retrieve MCU Version: %i\n", ret); + return; + } + + /* RGB */ + ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_RGB_ENABLE, + 0, 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB enabled: %i\n", ret); + return; + } + + ret = mcu_property_out(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, + 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Mode: %i\n", ret); + return; + } + + ret = mcu_property_out(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, + 0, 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Profile: %i\n", ret); + return; + } + + ret = rgb_attr_show(); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Profile Data: %i\n", ret); + return; + } +} + +static int hid_gos_cfg_probe(struct hid_device *hdev, + const struct hid_device_id *_id) +{ + unsigned char *buf; + int ret; + + buf = devm_kzalloc(&hdev->dev, GO_S_PACKET_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + hid_set_drvdata(hdev, &drvdata); + drvdata.buf = buf; + drvdata.hdev = hdev; + mutex_init(&drvdata.cfg_mutex); + + ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups); + if (ret) { + dev_err_probe(&hdev->dev, ret, + "Failed to create gamepad configuration attributes\n"); + return ret; + } + + ret = devm_led_classdev_multicolor_register(&hdev->dev, &gos_cdev_rgb); + if (ret) { + dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n"); + return ret; + } + + ret = devm_device_add_group(gos_cdev_rgb.led_cdev.dev, &rgb_attr_group); + if (ret) { + dev_err_probe(&hdev->dev, ret, + "Failed to create RGB configuratiion attributes\n"); + return ret; + } + + drvdata.led_cdev = &gos_cdev_rgb.led_cdev; + + init_completion(&drvdata.send_cmd_complete); + + /* Executing calls prior to returning from probe will lock the MCU. Schedule + * initial data call after probe has completed and MCU can accept calls. + */ + INIT_DELAYED_WORK(&drvdata.gos_cfg_setup, &cfg_setup); + ret = schedule_delayed_work(&drvdata.gos_cfg_setup, + msecs_to_jiffies(2)); + if (!ret) { + dev_err(&hdev->dev, + "Failed to schedule startup delayed work\n"); + return -ENODEV; + } + return 0; +} + +static void hid_gos_cfg_remove(struct hid_device *hdev) +{ + guard(mutex)(&drvdata.cfg_mutex); + cancel_delayed_work_sync(&drvdata.gos_cfg_setup); + sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups); + hid_hw_close(hdev); + hid_hw_stop(hdev); + hid_set_drvdata(hdev, NULL); +} + +static int hid_gos_cfg_reset_resume(struct hid_device *hdev) +{ + u8 os_mode = drvdata.os_mode; + int ret; + + ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, FEATURE_OS_MODE, + &os_mode, 1); + if (ret < 0) + return ret; + + ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_OS_MODE, 0, + 0); + if (ret < 0) + return ret; + + if (drvdata.os_mode != os_mode) + return -ENODEV; + + return 0; +} + +static int hid_gos_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + int ret, ep; + + hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "Parse failed\n"); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + hid_err(hdev, "Failed to start HID device\n"); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "Failed to open HID device\n"); + hid_hw_stop(hdev); + return ret; + } + + ep = get_endpoint_address(hdev); + if (ep != GO_S_CFG_INTF_IN) { + dev_dbg(&hdev->dev, + "Started interface %x as generic HID device.\n", ep); + return 0; + } + + ret = hid_gos_cfg_probe(hdev, id); + if (ret) + dev_err_probe(&hdev->dev, ret, + "Failed to start configuration interface"); + + dev_dbg(&hdev->dev, "Started Legion Go S HID Device: %x\n", ep); + return ret; +} + +static void hid_gos_remove(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + + switch (ep) { + case GO_S_CFG_INTF_IN: + hid_gos_cfg_remove(hdev); + break; + default: + hid_hw_close(hdev); + hid_hw_stop(hdev); + + break; + } +} + +static int hid_gos_reset_resume(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + + switch (ep) { + case GO_S_CFG_INTF_IN: + return hid_gos_cfg_reset_resume(hdev); + default: + break; + } + + return 0; +} + +static const struct hid_device_id hid_gos_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_QHE, + USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_QHE, + USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT) }, + {} +}; + +MODULE_DEVICE_TABLE(hid, hid_gos_devices); +static struct hid_driver hid_lenovo_go_s = { + .name = "hid-lenovo-go-s", + .id_table = hid_gos_devices, + .probe = hid_gos_probe, + .remove = hid_gos_remove, + .reset_resume = hid_gos_reset_resume, + .raw_event = hid_gos_raw_event, +}; +module_hid_driver(hid_lenovo_go_s); + +MODULE_AUTHOR("Derek J. Clark"); +MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go S Series gamepad."); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c new file mode 100644 index 000000000000..f3623871edbf --- /dev/null +++ b/drivers/hid/hid-lenovo-go.c @@ -0,0 +1,2399 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for Lenovo Legion Go series gamepads. + * + * Copyright (c) 2025 Valve Corporation + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define GO_GP_INTF_IN 0x83 +#define GO_OUTPUT_REPORT_ID 0x05 +#define GO_GP_RESET_SUCCESS 0x01 +#define GO_PACKET_SIZE 64 + +struct hid_go_cfg { + unsigned char *buf; + struct delayed_work go_cfg_setup; + struct completion send_cmd_complete; + struct led_classdev *led_cdev; + struct hid_device *hdev; + struct mutex cfg_mutex; /*ensure single synchronous output report*/ + u8 fps_mode; + u8 gp_left_auto_sleep_time; + u8 gp_left_gyro_cal_status; + u8 gp_left_joy_cal_status; + u8 gp_left_notify_en; + u8 gp_left_rumble_mode; + u8 gp_left_trigg_cal_status; + u32 gp_left_version_firmware; + u8 gp_left_version_gen; + u32 gp_left_version_hardware; + u32 gp_left_version_product; + u32 gp_left_version_protocol; + u8 gp_mode; + u8 gp_right_auto_sleep_time; + u8 gp_right_gyro_cal_status; + u8 gp_right_joy_cal_status; + u8 gp_right_notify_en; + u8 gp_right_rumble_mode; + u8 gp_right_trigg_cal_status; + u32 gp_right_version_firmware; + u8 gp_right_version_gen; + u32 gp_right_version_hardware; + u32 gp_right_version_product; + u32 gp_right_version_protocol; + u8 gp_rumble_intensity; + u8 imu_left_bypass_en; + u8 imu_left_sensor_en; + u8 imu_right_bypass_en; + u8 imu_right_sensor_en; + u32 mcu_version_firmware; + u8 mcu_version_gen; + u32 mcu_version_hardware; + u32 mcu_version_product; + u32 mcu_version_protocol; + u32 mouse_dpi; + u8 os_mode; + u8 rgb_effect; + u8 rgb_en; + u8 rgb_mode; + u8 rgb_profile; + u8 rgb_speed; + u8 tp_en; + u8 tp_vibration_en; + u8 tp_vibration_intensity; + u32 tx_dongle_version_firmware; + u8 tx_dongle_version_gen; + u32 tx_dongle_version_hardware; + u32 tx_dongle_version_product; + u32 tx_dongle_version_protocol; +} drvdata; + +struct go_cfg_attr { + u8 index; +}; + +struct command_report { + u8 report_id; + u8 id; + u8 cmd; + u8 sub_cmd; + u8 device_type; + u8 data[59]; +} __packed; + +enum command_id { + MCU_CONFIG_DATA = 0x00, + OS_MODE_DATA = 0x06, + GAMEPAD_DATA = 0x3c, +}; + +enum mcu_command_index { + GET_VERSION_DATA = 0x02, + GET_FEATURE_STATUS, + SET_FEATURE_STATUS, + GET_MOTOR_CFG, + SET_MOTOR_CFG, + GET_DPI_CFG, + SET_DPI_CFG, + SET_TRIGGER_CFG = 0x0a, + SET_JOYSTICK_CFG = 0x0c, + SET_GYRO_CFG = 0x0e, + GET_RGB_CFG, + SET_RGB_CFG, + GET_DEVICE_STATUS = 0xa0, + +}; + +enum dev_type { + UNSPECIFIED, + USB_MCU, + TX_DONGLE, + LEFT_CONTROLLER, + RIGHT_CONTROLLER, +}; + +enum enabled_status_index { + FEATURE_UNKNOWN, + FEATURE_ENABLED, + FEATURE_DISABLED, +}; + +static const char *const enabled_status_text[] = { + [FEATURE_UNKNOWN] = "unknown", + [FEATURE_ENABLED] = "true", + [FEATURE_DISABLED] = "false", +}; + +enum version_data_index { + PRODUCT_VERSION = 0x02, + PROTOCOL_VERSION, + FIRMWARE_VERSION, + HARDWARE_VERSION, + HARDWARE_GENERATION, +}; + +enum feature_status_index { + FEATURE_RESET_GAMEPAD = 0x02, + FEATURE_IMU_BYPASS, + FEATURE_IMU_ENABLE = 0x05, + FEATURE_TOUCHPAD_ENABLE = 0x07, + FEATURE_LIGHT_ENABLE, + FEATURE_AUTO_SLEEP_TIME, + FEATURE_FPS_SWITCH_STATUS = 0x0b, + FEATURE_GAMEPAD_MODE = 0x0e, +}; + +#define FEATURE_OS_MODE 0x69 + +enum fps_switch_status_index { + FPS_STATUS_UNKNOWN, + GAMEPAD, + FPS, +}; + +static const char *const fps_switch_text[] = { + [FPS_STATUS_UNKNOWN] = "unknown", + [GAMEPAD] = "gamepad", + [FPS] = "fps", +}; + +enum gamepad_mode_index { + GAMEPAD_MODE_UNKNOWN, + XINPUT, + DINPUT, +}; + +static const char *const gamepad_mode_text[] = { + [GAMEPAD_MODE_UNKNOWN] = "unknown", + [XINPUT] = "xinput", + [DINPUT] = "dinput", +}; + +enum motor_cfg_index { + MOTOR_CFG_ALL = 0x01, + MOTOR_INTENSITY, + VIBRATION_NOTIFY_ENABLE, + RUMBLE_MODE, + TP_VIBRATION_ENABLE, + TP_VIBRATION_INTENSITY, +}; + +enum intensity_index { + INTENSITY_UNKNOWN, + INTENSITY_OFF, + INTENSITY_LOW, + INTENSITY_MEDIUM, + INTENSITY_HIGH, +}; + +static const char *const intensity_text[] = { + [INTENSITY_UNKNOWN] = "unknown", + [INTENSITY_OFF] = "off", + [INTENSITY_LOW] = "low", + [INTENSITY_MEDIUM] = "medium", + [INTENSITY_HIGH] = "high", +}; + +enum rumble_mode_index { + RUMBLE_MODE_UNKNOWN, + RUMBLE_MODE_FPS, + RUMBLE_MODE_RACE, + RUMBLE_MODE_AVERAGE, + RUMBLE_MODE_SPG, + RUMBLE_MODE_RPG, +}; + +static const char *const rumble_mode_text[] = { + [RUMBLE_MODE_UNKNOWN] = "unknown", + [RUMBLE_MODE_FPS] = "fps", + [RUMBLE_MODE_RACE] = "racing", + [RUMBLE_MODE_AVERAGE] = "standard", + [RUMBLE_MODE_SPG] = "spg", + [RUMBLE_MODE_RPG] = "rpg", +}; + +#define FPS_MODE_DPI 0x02 +#define TRIGGER_CALIBRATE 0x04 +#define JOYSTICK_CALIBRATE 0x04 +#define GYRO_CALIBRATE 0x06 + +enum cal_device_type { + CALDEV_GYROSCOPE = 0x01, + CALDEV_JOYSTICK, + CALDEV_TRIGGER, + CALDEV_JOY_TRIGGER, +}; + +enum cal_enable { + CAL_UNKNOWN, + CAL_START, + CAL_STOP, +}; + +static const char *const cal_enabled_text[] = { + [CAL_UNKNOWN] = "unknown", + [CAL_START] = "start", + [CAL_STOP] = "stop", +}; + +enum cal_status_index { + CAL_STAT_UNKNOWN, + CAL_STAT_SUCCESS, + CAL_STAT_FAILURE, +}; + +static const char *const cal_status_text[] = { + [CAL_STAT_UNKNOWN] = "unknown", + [CAL_STAT_SUCCESS] = "success", + [CAL_STAT_FAILURE] = "failure", +}; + +enum rgb_config_index { + LIGHT_CFG_ALL = 0x01, + LIGHT_MODE_SEL, + LIGHT_PROFILE_SEL, + USR_LIGHT_PROFILE_1, + USR_LIGHT_PROFILE_2, + USR_LIGHT_PROFILE_3, +}; + +enum rgb_mode_index { + RGB_MODE_UNKNOWN, + RGB_MODE_DYNAMIC, + RGB_MODE_CUSTOM, +}; + +static const char *const rgb_mode_text[] = { + [RGB_MODE_UNKNOWN] = "unknown", + [RGB_MODE_DYNAMIC] = "dynamic", + [RGB_MODE_CUSTOM] = "custom", +}; + +enum rgb_effect_index { + RGB_EFFECT_MONO, + RGB_EFFECT_BREATHE, + RGB_EFFECT_CHROMA, + RGB_EFFECT_RAINBOW, +}; + +static const char *const rgb_effect_text[] = { + [RGB_EFFECT_MONO] = "monocolor", + [RGB_EFFECT_BREATHE] = "breathe", + [RGB_EFFECT_CHROMA] = "chroma", + [RGB_EFFECT_RAINBOW] = "rainbow", +}; + +enum device_status_index { + GET_CAL_STATUS = 0x02, + GET_UPGRADE_STATUS, + GET_MACRO_REC_STATUS, + GET_HOTKEY_TRIGG_STATUS, +}; + +enum os_mode_cfg_index { + SET_OS_MODE = 0x09, + GET_OS_MODE, +}; + +enum os_mode_index { + OS_UNKNOWN, + WINDOWS, + LINUX, +}; + +static const char *const os_mode_text[] = { + [OS_UNKNOWN] = "unknown", + [WINDOWS] = "windows", + [LINUX] = "linux", +}; + +static int hid_go_version_event(struct command_report *cmd_rep) +{ + switch (cmd_rep->sub_cmd) { + case PRODUCT_VERSION: + switch (cmd_rep->device_type) { + case USB_MCU: + drvdata.mcu_version_product = + get_unaligned_le32(cmd_rep->data); + return 0; + case TX_DONGLE: + drvdata.tx_dongle_version_product = + get_unaligned_le32(cmd_rep->data); + return 0; + case LEFT_CONTROLLER: + drvdata.gp_left_version_product = + get_unaligned_le32(cmd_rep->data); + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_version_product = + get_unaligned_le32(cmd_rep->data); + return 0; + default: + return -EINVAL; + } + case PROTOCOL_VERSION: + switch (cmd_rep->device_type) { + case USB_MCU: + drvdata.mcu_version_protocol = + get_unaligned_le32(cmd_rep->data); + return 0; + case TX_DONGLE: + drvdata.tx_dongle_version_protocol = + get_unaligned_le32(cmd_rep->data); + return 0; + case LEFT_CONTROLLER: + drvdata.gp_left_version_protocol = + get_unaligned_le32(cmd_rep->data); + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_version_protocol = + get_unaligned_le32(cmd_rep->data); + return 0; + default: + return -EINVAL; + } + case FIRMWARE_VERSION: + switch (cmd_rep->device_type) { + case USB_MCU: + drvdata.mcu_version_firmware = + get_unaligned_le32(cmd_rep->data); + return 0; + case TX_DONGLE: + drvdata.tx_dongle_version_firmware = + get_unaligned_le32(cmd_rep->data); + return 0; + case LEFT_CONTROLLER: + drvdata.gp_left_version_firmware = + get_unaligned_le32(cmd_rep->data); + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_version_firmware = + get_unaligned_le32(cmd_rep->data); + return 0; + default: + return -EINVAL; + } + case HARDWARE_VERSION: + switch (cmd_rep->device_type) { + case USB_MCU: + drvdata.mcu_version_hardware = + get_unaligned_le32(cmd_rep->data); + return 0; + case TX_DONGLE: + drvdata.tx_dongle_version_hardware = + get_unaligned_le32(cmd_rep->data); + return 0; + case LEFT_CONTROLLER: + drvdata.gp_left_version_hardware = + get_unaligned_le32(cmd_rep->data); + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_version_hardware = + get_unaligned_le32(cmd_rep->data); + return 0; + default: + return -EINVAL; + } + case HARDWARE_GENERATION: + switch (cmd_rep->device_type) { + case USB_MCU: + drvdata.mcu_version_gen = cmd_rep->data[0]; + return 0; + case TX_DONGLE: + drvdata.tx_dongle_version_gen = cmd_rep->data[0]; + return 0; + case LEFT_CONTROLLER: + drvdata.gp_left_version_gen = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_version_gen = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + } + default: + return -EINVAL; + } +} + +static int hid_go_feature_status_event(struct command_report *cmd_rep) +{ + switch (cmd_rep->sub_cmd) { + case FEATURE_RESET_GAMEPAD: + return 0; + case FEATURE_IMU_ENABLE: + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + drvdata.imu_left_sensor_en = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.imu_right_sensor_en = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; + case FEATURE_IMU_BYPASS: + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + drvdata.imu_left_bypass_en = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.imu_right_bypass_en = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; + break; + case FEATURE_LIGHT_ENABLE: + drvdata.rgb_en = cmd_rep->data[0]; + return 0; + case FEATURE_AUTO_SLEEP_TIME: + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + drvdata.gp_left_auto_sleep_time = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_auto_sleep_time = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; + break; + case FEATURE_TOUCHPAD_ENABLE: + drvdata.tp_en = cmd_rep->data[0]; + return 0; + case FEATURE_GAMEPAD_MODE: + drvdata.gp_mode = cmd_rep->data[0]; + return 0; + case FEATURE_FPS_SWITCH_STATUS: + drvdata.fps_mode = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + } +} + +static int hid_go_motor_event(struct command_report *cmd_rep) +{ + switch (cmd_rep->sub_cmd) { + case MOTOR_CFG_ALL: + return -EINVAL; + case MOTOR_INTENSITY: + drvdata.gp_rumble_intensity = cmd_rep->data[0]; + return 0; + case VIBRATION_NOTIFY_ENABLE: + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + drvdata.gp_left_notify_en = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_notify_en = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; + break; + case RUMBLE_MODE: + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + drvdata.gp_left_rumble_mode = cmd_rep->data[0]; + return 0; + case RIGHT_CONTROLLER: + drvdata.gp_right_rumble_mode = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; + case TP_VIBRATION_ENABLE: + drvdata.tp_vibration_en = cmd_rep->data[0]; + return 0; + case TP_VIBRATION_INTENSITY: + drvdata.tp_vibration_intensity = cmd_rep->data[0]; + return 0; + } + return -EINVAL; +} + +static int hid_go_fps_dpi_event(struct command_report *cmd_rep) +{ + if (cmd_rep->sub_cmd != FPS_MODE_DPI) + return -EINVAL; + + drvdata.mouse_dpi = get_unaligned_le32(cmd_rep->data); + + return 0; +} + +static int hid_go_light_event(struct command_report *cmd_rep) +{ + struct led_classdev_mc *mc_cdev; + + switch (cmd_rep->sub_cmd) { + case LIGHT_MODE_SEL: + drvdata.rgb_mode = cmd_rep->data[0]; + return 0; + case LIGHT_PROFILE_SEL: + drvdata.rgb_profile = cmd_rep->data[0]; + return 0; + case USR_LIGHT_PROFILE_1: + case USR_LIGHT_PROFILE_2: + case USR_LIGHT_PROFILE_3: + mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + drvdata.rgb_effect = cmd_rep->data[0]; + mc_cdev->subled_info[0].intensity = cmd_rep->data[1]; + mc_cdev->subled_info[1].intensity = cmd_rep->data[2]; + mc_cdev->subled_info[2].intensity = cmd_rep->data[3]; + drvdata.led_cdev->brightness = cmd_rep->data[4]; + drvdata.rgb_speed = cmd_rep->data[5]; + return 0; + default: + return -EINVAL; + } +} + +static int hid_go_device_status_event(struct command_report *cmd_rep) +{ + switch (cmd_rep->device_type) { + case LEFT_CONTROLLER: + switch (cmd_rep->data[0]) { + case CALDEV_GYROSCOPE: + drvdata.gp_left_gyro_cal_status = cmd_rep->data[1]; + return 0; + case CALDEV_JOYSTICK: + drvdata.gp_left_joy_cal_status = cmd_rep->data[1]; + return 0; + case CALDEV_TRIGGER: + drvdata.gp_left_trigg_cal_status = cmd_rep->data[1]; + return 0; + default: + return -EINVAL; + } + break; + case RIGHT_CONTROLLER: + switch (cmd_rep->data[0]) { + case CALDEV_GYROSCOPE: + drvdata.gp_right_gyro_cal_status = cmd_rep->data[1]; + return 0; + case CALDEV_JOYSTICK: + drvdata.gp_right_joy_cal_status = cmd_rep->data[1]; + return 0; + case CALDEV_TRIGGER: + drvdata.gp_right_trigg_cal_status = cmd_rep->data[1]; + return 0; + default: + return -EINVAL; + } + break; + default: + return -EINVAL; + } +} + +static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep) +{ + switch (cmd_rep->sub_cmd) { + case SET_OS_MODE: + if (cmd_rep->data[0] != 1) + return -EIO; + return 0; + case GET_OS_MODE: + drvdata.os_mode = cmd_rep->data[0]; + return 0; + default: + return -EINVAL; + }; +} + +static int hid_go_set_event_return(struct command_report *cmd_rep) +{ + if (cmd_rep->data[0] != 0) + return -EIO; + + return 0; +} + +static int get_endpoint_address(struct hid_device *hdev) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_host_endpoint *ep; + + if (!intf) + return -ENODEV; + + ep = intf->cur_altsetting->endpoint; + if (!ep) + return -ENODEV; + + return ep->desc.bEndpointAddress; +} + +static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct command_report *cmd_rep; + int ep, ret; + + if (size != GO_PACKET_SIZE) + goto passthrough; + + ep = get_endpoint_address(hdev); + if (ep != GO_GP_INTF_IN) + goto passthrough; + + cmd_rep = (struct command_report *)data; + + switch (cmd_rep->id) { + case MCU_CONFIG_DATA: + switch (cmd_rep->cmd) { + case GET_VERSION_DATA: + ret = hid_go_version_event(cmd_rep); + break; + case GET_FEATURE_STATUS: + ret = hid_go_feature_status_event(cmd_rep); + break; + case GET_MOTOR_CFG: + ret = hid_go_motor_event(cmd_rep); + break; + case GET_DPI_CFG: + ret = hid_go_fps_dpi_event(cmd_rep); + break; + case GET_RGB_CFG: + ret = hid_go_light_event(cmd_rep); + break; + case GET_DEVICE_STATUS: + ret = hid_go_device_status_event(cmd_rep); + break; + case SET_FEATURE_STATUS: + case SET_MOTOR_CFG: + case SET_DPI_CFG: + case SET_RGB_CFG: + case SET_TRIGGER_CFG: + case SET_JOYSTICK_CFG: + case SET_GYRO_CFG: + ret = hid_go_set_event_return(cmd_rep); + break; + default: + ret = -EINVAL; + break; + }; + break; + case OS_MODE_DATA: + ret = hid_go_os_mode_cfg_event(cmd_rep); + break; + default: + goto passthrough; + }; + dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n", + GO_PACKET_SIZE, data); + + complete(&drvdata.send_cmd_complete); + return ret; + +passthrough: + /* Forward other HID reports so they generate events */ + hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1); + return 0; +} + +static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command, + u8 index, enum dev_type device, u8 *data, size_t len) +{ + u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device }; + size_t header_size = ARRAY_SIZE(header); + size_t total_size = header_size + len; + int timeout = 50; + int ret; + + guard(mutex)(&drvdata.cfg_mutex); + memcpy(drvdata.buf, header, header_size); + memcpy(drvdata.buf + header_size, data, len); + memset(drvdata.buf + total_size, 0, GO_PACKET_SIZE - total_size); + + dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", + GO_PACKET_SIZE, drvdata.buf); + + ret = hid_hw_output_report(hdev, drvdata.buf, GO_PACKET_SIZE); + if (ret < 0) + return ret; + + ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL; + if (ret) + return ret; + + ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete, + msecs_to_jiffies(timeout)); + + if (ret == 0) /* timeout occurred */ + ret = -EBUSY; + if (ret > 0) /* timeout/interrupt didn't occur */ + ret = 0; + + reinit_completion(&drvdata.send_cmd_complete); + return ret; +} + +static ssize_t version_show(struct device *dev, struct device_attribute *attr, + char *buf, enum version_data_index index, + enum dev_type device_type) +{ + ssize_t count = 0; + int ret; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA, + index, device_type, 0, 0); + if (ret) + return ret; + + switch (index) { + case PRODUCT_VERSION: + switch (device_type) { + case USB_MCU: + count = sysfs_emit(buf, "%u\n", + drvdata.mcu_version_product); + break; + case TX_DONGLE: + count = sysfs_emit(buf, "%u\n", + drvdata.tx_dongle_version_product); + break; + case LEFT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_left_version_product); + break; + case RIGHT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_right_version_product); + break; + default: + return -EINVAL; + } + break; + case PROTOCOL_VERSION: + switch (device_type) { + case USB_MCU: + count = sysfs_emit(buf, "%u\n", + drvdata.mcu_version_protocol); + break; + case TX_DONGLE: + count = sysfs_emit(buf, "%u\n", + drvdata.tx_dongle_version_protocol); + break; + case LEFT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_left_version_protocol); + break; + case RIGHT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_right_version_protocol); + break; + default: + return -EINVAL; + } + break; + case FIRMWARE_VERSION: + switch (device_type) { + case USB_MCU: + count = sysfs_emit(buf, "%u\n", + drvdata.mcu_version_firmware); + break; + case TX_DONGLE: + count = sysfs_emit(buf, "%u\n", + drvdata.tx_dongle_version_firmware); + break; + case LEFT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_left_version_firmware); + break; + case RIGHT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_right_version_firmware); + break; + default: + return -EINVAL; + } + break; + case HARDWARE_VERSION: + switch (device_type) { + case USB_MCU: + count = sysfs_emit(buf, "%u\n", + drvdata.mcu_version_hardware); + break; + case TX_DONGLE: + count = sysfs_emit(buf, "%u\n", + drvdata.tx_dongle_version_hardware); + break; + case LEFT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_left_version_hardware); + break; + case RIGHT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_right_version_hardware); + break; + default: + return -EINVAL; + } + break; + case HARDWARE_GENERATION: + switch (device_type) { + case USB_MCU: + count = sysfs_emit(buf, "%u\n", + drvdata.mcu_version_gen); + break; + case TX_DONGLE: + count = sysfs_emit(buf, "%u\n", + drvdata.tx_dongle_version_gen); + break; + case LEFT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_left_version_gen); + break; + case RIGHT_CONTROLLER: + count = sysfs_emit(buf, "%u\n", + drvdata.gp_right_version_gen); + break; + default: + return -EINVAL; + } + break; + } + + return count; +} + +static ssize_t feature_status_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + enum feature_status_index index, + enum dev_type device_type) +{ + size_t size = 1; + u8 val = 0; + int ret; + + switch (index) { + case FEATURE_IMU_ENABLE: + case FEATURE_IMU_BYPASS: + case FEATURE_LIGHT_ENABLE: + case FEATURE_TOUCHPAD_ENABLE: + ret = sysfs_match_string(enabled_status_text, buf); + val = ret; + break; + case FEATURE_AUTO_SLEEP_TIME: + ret = kstrtou8(buf, 10, &val); + break; + case FEATURE_RESET_GAMEPAD: + ret = kstrtou8(buf, 10, &val); + if (val != GO_GP_RESET_SUCCESS) + return -EINVAL; + break; + case FEATURE_FPS_SWITCH_STATUS: + ret = sysfs_match_string(fps_switch_text, buf); + val = ret; + break; + case FEATURE_GAMEPAD_MODE: + ret = sysfs_match_string(gamepad_mode_text, buf); + val = ret; + break; + default: + return -EINVAL; + }; + + if (ret < 0) + return ret; + + if (!val) + size = 0; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, + SET_FEATURE_STATUS, index, device_type, &val, + size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t feature_status_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum feature_status_index index, + enum dev_type device_type) +{ + ssize_t count = 0; + int ret; + u8 i; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, + GET_FEATURE_STATUS, index, device_type, 0, 0); + if (ret) + return ret; + + switch (index) { + case FEATURE_IMU_ENABLE: + switch (device_type) { + case LEFT_CONTROLLER: + i = drvdata.imu_left_sensor_en; + break; + case RIGHT_CONTROLLER: + i = drvdata.imu_right_sensor_en; + break; + default: + return -EINVAL; + } + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case FEATURE_IMU_BYPASS: + switch (device_type) { + case LEFT_CONTROLLER: + i = drvdata.imu_left_bypass_en; + break; + case RIGHT_CONTROLLER: + i = drvdata.imu_right_bypass_en; + break; + default: + return -EINVAL; + } + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case FEATURE_LIGHT_ENABLE: + i = drvdata.rgb_en; + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case FEATURE_TOUCHPAD_ENABLE: + i = drvdata.tp_en; + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case FEATURE_AUTO_SLEEP_TIME: + switch (device_type) { + case LEFT_CONTROLLER: + i = drvdata.gp_left_auto_sleep_time; + break; + case RIGHT_CONTROLLER: + i = drvdata.gp_right_auto_sleep_time; + break; + default: + return -EINVAL; + }; + count = sysfs_emit(buf, "%u\n", i); + break; + case FEATURE_FPS_SWITCH_STATUS: + i = drvdata.fps_mode; + if (i >= ARRAY_SIZE(fps_switch_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", fps_switch_text[i]); + break; + case FEATURE_GAMEPAD_MODE: + i = drvdata.gp_mode; + if (i >= ARRAY_SIZE(gamepad_mode_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]); + break; + default: + return -EINVAL; + }; + + return count; +} + +static ssize_t feature_status_options(struct device *dev, + struct device_attribute *attr, char *buf, + enum feature_status_index index) +{ + ssize_t count = 0; + unsigned int i; + + switch (index) { + case FEATURE_IMU_ENABLE: + case FEATURE_IMU_BYPASS: + case FEATURE_LIGHT_ENABLE: + case FEATURE_TOUCHPAD_ENABLE: + for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + enabled_status_text[i]); + } + break; + case FEATURE_AUTO_SLEEP_TIME: + return sysfs_emit(buf, "0-255\n"); + case FEATURE_FPS_SWITCH_STATUS: + for (i = 1; i < ARRAY_SIZE(fps_switch_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + fps_switch_text[i]); + } + break; + case FEATURE_GAMEPAD_MODE: + for (i = 1; i < ARRAY_SIZE(gamepad_mode_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + gamepad_mode_text[i]); + } + break; + default: + return -EINVAL; + }; + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t motor_config_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + enum motor_cfg_index index, + enum dev_type device_type) +{ + size_t size = 1; + u8 val = 0; + int ret; + + switch (index) { + case MOTOR_CFG_ALL: + return -EINVAL; + case MOTOR_INTENSITY: + ret = sysfs_match_string(intensity_text, buf); + val = ret; + break; + case VIBRATION_NOTIFY_ENABLE: + ret = sysfs_match_string(enabled_status_text, buf); + val = ret; + break; + case RUMBLE_MODE: + ret = sysfs_match_string(rumble_mode_text, buf); + val = ret; + break; + case TP_VIBRATION_ENABLE: + ret = sysfs_match_string(enabled_status_text, buf); + val = ret; + break; + case TP_VIBRATION_INTENSITY: + ret = sysfs_match_string(intensity_text, buf); + val = ret; + break; + }; + + if (ret < 0) + return ret; + + if (!val) + size = 0; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_MOTOR_CFG, + index, device_type, &val, size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t motor_config_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum motor_cfg_index index, + enum dev_type device_type) +{ + ssize_t count = 0; + int ret; + u8 i; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_MOTOR_CFG, + index, device_type, 0, 0); + if (ret) + return ret; + + switch (index) { + case MOTOR_CFG_ALL: + return -EINVAL; + case MOTOR_INTENSITY: + i = drvdata.gp_rumble_intensity; + if (i >= ARRAY_SIZE(intensity_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", intensity_text[i]); + break; + case VIBRATION_NOTIFY_ENABLE: + switch (device_type) { + case LEFT_CONTROLLER: + i = drvdata.gp_left_notify_en; + break; + case RIGHT_CONTROLLER: + i = drvdata.gp_right_notify_en; + break; + default: + return -EINVAL; + }; + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case RUMBLE_MODE: + switch (device_type) { + case LEFT_CONTROLLER: + i = drvdata.gp_left_rumble_mode; + break; + case RIGHT_CONTROLLER: + i = drvdata.gp_right_rumble_mode; + break; + default: + return -EINVAL; + }; + if (i >= ARRAY_SIZE(rumble_mode_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", rumble_mode_text[i]); + break; + case TP_VIBRATION_ENABLE: + i = drvdata.tp_vibration_en; + if (i >= ARRAY_SIZE(enabled_status_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", enabled_status_text[i]); + break; + case TP_VIBRATION_INTENSITY: + i = drvdata.tp_vibration_intensity; + if (i >= ARRAY_SIZE(intensity_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", intensity_text[i]); + break; + }; + + return count; +} + +static ssize_t motor_config_options(struct device *dev, + struct device_attribute *attr, char *buf, + enum motor_cfg_index index) +{ + ssize_t count = 0; + unsigned int i; + + switch (index) { + case MOTOR_CFG_ALL: + break; + case RUMBLE_MODE: + for (i = 1; i < ARRAY_SIZE(rumble_mode_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + rumble_mode_text[i]); + } + break; + case MOTOR_INTENSITY: + case TP_VIBRATION_INTENSITY: + for (i = 1; i < ARRAY_SIZE(intensity_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + intensity_text[i]); + } + break; + case VIBRATION_NOTIFY_ENABLE: + case TP_VIBRATION_ENABLE: + for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) { + count += sysfs_emit_at(buf, count, "%s ", + enabled_status_text[i]); + } + break; + }; + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t fps_mode_dpi_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) + +{ + size_t size = 4; + u32 value; + u8 val[4]; + int ret; + + ret = kstrtou32(buf, 10, &value); + if (ret) + return ret; + + if (value != 500 && value != 800 && value != 1200 && value != 1800) + return -EINVAL; + + put_unaligned_le32(value, val); + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_DPI_CFG, + FPS_MODE_DPI, UNSPECIFIED, val, size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t fps_mode_dpi_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_DPI_CFG, + FPS_MODE_DPI, UNSPECIFIED, 0, 0); + if (ret < 0) + return ret; + + return sysfs_emit(buf, "%u\n", drvdata.mouse_dpi); +} + +static ssize_t fps_mode_dpi_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "500 800 1200 1800\n"); +} + +static ssize_t device_status_show(struct device *dev, + struct device_attribute *attr, char *buf, + enum device_status_index index, + enum dev_type device_type, + enum cal_device_type cal_type) +{ + u8 i; + + switch (index) { + case GET_CAL_STATUS: + switch (device_type) { + case LEFT_CONTROLLER: + switch (cal_type) { + case CALDEV_GYROSCOPE: + i = drvdata.gp_left_gyro_cal_status; + break; + case CALDEV_JOYSTICK: + i = drvdata.gp_left_joy_cal_status; + break; + case CALDEV_TRIGGER: + i = drvdata.gp_left_trigg_cal_status; + break; + default: + return -EINVAL; + } + break; + case RIGHT_CONTROLLER: + switch (cal_type) { + case CALDEV_GYROSCOPE: + i = drvdata.gp_right_gyro_cal_status; + break; + case CALDEV_JOYSTICK: + i = drvdata.gp_right_joy_cal_status; + break; + case CALDEV_TRIGGER: + i = drvdata.gp_right_trigg_cal_status; + break; + default: + return -EINVAL; + } + break; + default: + return -EINVAL; + } + break; + default: + return -EINVAL; + }; + + if (i >= ARRAY_SIZE(cal_status_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", cal_status_text[i]); +} + +static ssize_t calibrate_config_store(struct device *dev, + struct device_attribute *attr, + const char *buf, u8 cmd, u8 sub_cmd, + size_t count, enum dev_type device_type) +{ + size_t size = 1; + u8 val = 0; + int ret; + + ret = sysfs_match_string(cal_enabled_text, buf); + if (ret < 0) + return ret; + + val = ret; + if (!val) + size = 0; + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd, + device_type, &val, size); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t calibrate_config_options(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(cal_enabled_text); i++) + count += sysfs_emit_at(buf, count, "%s ", cal_enabled_text[i]); + + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t os_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + size_t size = 1; + int ret; + u8 val; + + ret = sysfs_match_string(os_mode_text, buf); + if (ret <= 0) + return ret; + + val = ret; + ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE, + SET_OS_MODE, USB_MCU, &val, size); + if (ret < 0) + return ret; + + drvdata.os_mode = val; + + return count; +} + +static ssize_t os_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + ssize_t count = 0; + int ret; + u8 i; + + ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE, + GET_OS_MODE, USB_MCU, 0, 0); + if (ret) + return ret; + + i = drvdata.os_mode; + if (i >= ARRAY_SIZE(os_mode_text)) + return -EINVAL; + + count = sysfs_emit(buf, "%s\n", os_mode_text[i]); + + return count; +} + +static ssize_t os_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(os_mode_text); i++) + count += sysfs_emit_at(buf, count, "%s ", os_mode_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd, + enum rgb_config_index index, u8 *val, size_t size) +{ + if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG) + return -EINVAL; + + if (index < LIGHT_CFG_ALL || index > USR_LIGHT_PROFILE_3) + return -EINVAL; + + return mcu_property_out(hdev, MCU_CONFIG_DATA, cmd, index, UNSPECIFIED, + val, size); +} + +static int rgb_attr_show(void) +{ + enum rgb_config_index index; + + index = drvdata.rgb_profile + 3; + + return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0); +}; + +static ssize_t rgb_effect_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + u8 effect; + int ret; + + ret = sysfs_match_string(rgb_effect_text, buf); + if (ret < 0) + return ret; + + effect = ret; + index = drvdata.rgb_profile + 3; + u8 rgb_profile[6] = { effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + drvdata.led_cdev->brightness, + drvdata.rgb_speed }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + if (ret) + return ret; + + drvdata.rgb_effect = effect; + return count; +}; + +static ssize_t rgb_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = rgb_attr_show(); + if (ret) + return ret; + + if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]); +} + +static ssize_t rgb_effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++) + count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t rgb_speed_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + int val = 0; + int ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val < 0 || val > 100) + return -EINVAL; + + index = drvdata.rgb_profile + 3; + u8 rgb_profile[6] = { drvdata.rgb_effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + drvdata.led_cdev->brightness, + val }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + if (ret) + return ret; + + drvdata.rgb_speed = val; + + return count; +}; + +static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = rgb_attr_show(); + if (ret) + return ret; + + if (drvdata.rgb_speed > 100) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed); +} + +static ssize_t rgb_speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-100\n"); +} + +static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + u8 val; + + ret = sysfs_match_string(rgb_mode_text, buf); + if (ret <= 0) + return ret; + + val = ret; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, 1); + if (ret) + return ret; + + drvdata.rgb_mode = val; + + return count; +}; + +static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int ret; + + ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0); + if (ret) + return ret; + + if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]); +}; + +static ssize_t rgb_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count = 0; + unsigned int i; + + for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++) + count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]); + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t rgb_profile_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + size_t size = 1; + int ret; + u8 val; + + ret = kstrtou8(buf, 10, &val); + if (ret < 0) + return ret; + + if (val < 1 || val > 3) + return -EINVAL; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val, + size); + if (ret) + return ret; + + drvdata.rgb_profile = val; + + return count; +}; + +static ssize_t rgb_profile_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0, + 0); + if (ret) + return ret; + + if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3) + return -EINVAL; + + return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile); +}; + +static ssize_t rgb_profile_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "1-3\n"); +} + +static void hid_go_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); + enum rgb_config_index index; + int ret; + + if (brightness > led_cdev->max_brightness) { + dev_err(led_cdev->dev, "Invalid argument\n"); + return; + } + + index = drvdata.rgb_profile + 3; + u8 rgb_profile[6] = { drvdata.rgb_effect, + mc_cdev->subled_info[0].intensity, + mc_cdev->subled_info[1].intensity, + mc_cdev->subled_info[2].intensity, + brightness, + drvdata.rgb_speed }; + + ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); + switch (ret) { + case 0: + led_cdev->brightness = brightness; + break; + case -ENODEV: /* during switch to IAP -ENODEV is expected */ + case -ENOSYS: /* during rmmod -ENOSYS is expected */ + dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", ret); + break; + default: + dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret); + }; +} + +#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return _group##_store(dev, attr, buf, count, _name.index, \ + _dtype); \ + } \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_show(dev, attr, buf, _name.index, _dtype); \ + } \ + static ssize_t _name##_##_rtype##_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return _group##_options(dev, attr, buf, _name.index); \ + } \ + static DEVICE_ATTR_RW_NAMED(_name, _attrname) + +#define LEGO_DEVICE_ATTR_WO(_name, _attrname, _dtype, _group) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return _group##_store(dev, attr, buf, count, _name.index, \ + _dtype); \ + } \ + static DEVICE_ATTR_WO_NAMED(_name, _attrname) + +#define LEGO_DEVICE_ATTR_RO(_name, _attrname, _dtype, _group) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return _group##_show(dev, attr, buf, _name.index, _dtype); \ + } \ + static DEVICE_ATTR_RO_NAMED(_name, _attrname) + +#define LEGO_CAL_DEVICE_ATTR(_name, _attrname, _scmd, _dtype, _rtype) \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return calibrate_config_store(dev, attr, buf, _name.index, \ + _scmd, count, _dtype); \ + } \ + static ssize_t _name##_##_rtype##_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return calibrate_config_options(dev, attr, buf); \ + } \ + static DEVICE_ATTR_WO_NAMED(_name, _attrname) + +#define LEGO_DEVICE_STATUS_ATTR(_name, _attrname, _scmd, _dtype) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, char *buf) \ + { \ + return device_status_show(dev, attr, buf, _name.index, _scmd, \ + _dtype); \ + } \ + static DEVICE_ATTR_RO_NAMED(_name, _attrname) + +/* Gamepad - MCU */ +struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION }; +LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version); + +struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION }; +LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version); + +struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version); + +struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version); + +struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION }; +LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version); + +struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS }; +LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED, + feature_status); + +struct go_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE }; +LEGO_DEVICE_ATTR_RW(gamepad_mode, "mode", UNSPECIFIED, index, feature_status); +static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index"); + +struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD }; +LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status); + +struct go_cfg_attr gamepad_rumble_intensity = { MOTOR_INTENSITY }; +LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED, + index, motor_config); +static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index, + "rumble_intensity_index"); + +static DEVICE_ATTR_RW(fps_mode_dpi); +static DEVICE_ATTR_RO(fps_mode_dpi_index); + +static DEVICE_ATTR_RW(os_mode); +static DEVICE_ATTR_RO(os_mode_index); + +static struct attribute *mcu_attrs[] = { + &dev_attr_fps_mode_dpi.attr, + &dev_attr_fps_mode_dpi_index.attr, + &dev_attr_fps_switch_status.attr, + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + &dev_attr_gamepad_rumble_intensity.attr, + &dev_attr_gamepad_rumble_intensity_index.attr, + &dev_attr_os_mode.attr, + &dev_attr_os_mode_index.attr, + &dev_attr_reset_mcu.attr, + &dev_attr_version_firmware_mcu.attr, + &dev_attr_version_gen_mcu.attr, + &dev_attr_version_hardware_mcu.attr, + &dev_attr_version_product_mcu.attr, + &dev_attr_version_protocol_mcu.attr, + NULL, +}; + +static const struct attribute_group mcu_attr_group = { + .attrs = mcu_attrs, +}; + +/* Gamepad - TX Dongle */ +struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION }; +LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version); + +struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION }; +LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version); + +struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version); + +struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version); + +struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION }; +LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version); + +struct go_cfg_attr reset_tx_dongle = { FEATURE_RESET_GAMEPAD }; +LEGO_DEVICE_ATTR_RO(reset_tx_dongle, "reset", TX_DONGLE, feature_status); + +static struct attribute *tx_dongle_attrs[] = { + &dev_attr_reset_tx_dongle.attr, + &dev_attr_version_hardware_tx_dongle.attr, + &dev_attr_version_firmware_tx_dongle.attr, + &dev_attr_version_gen_tx_dongle.attr, + &dev_attr_version_product_tx_dongle.attr, + &dev_attr_version_protocol_tx_dongle.attr, + NULL, +}; + +static const struct attribute_group tx_dongle_attr_group = { + .name = "tx_dongle", + .attrs = tx_dongle_attrs, +}; + +/* Gamepad - Left */ +struct go_cfg_attr version_product_left = { PRODUCT_VERSION }; +LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version); + +struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION }; +LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version); + +struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version); + +struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version); + +struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION }; +LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version); + +struct go_cfg_attr auto_sleep_time_left = { FEATURE_AUTO_SLEEP_TIME }; +LEGO_DEVICE_ATTR_RW(auto_sleep_time_left, "auto_sleep_time", LEFT_CONTROLLER, + range, feature_status); +static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range, + "auto_sleep_time_range"); + +struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS }; +LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enable", LEFT_CONTROLLER, + index, feature_status); +static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enable_index"); + +struct go_cfg_attr imu_enable_left = { FEATURE_IMU_ENABLE }; +LEGO_DEVICE_ATTR_RW(imu_enable_left, "imu_enable", LEFT_CONTROLLER, index, + feature_status); +static DEVICE_ATTR_RO_NAMED(imu_enable_left_index, "imu_enable_index"); + +struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD }; +LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status); + +struct go_cfg_attr rumble_mode_left = { RUMBLE_MODE }; +LEGO_DEVICE_ATTR_RW(rumble_mode_left, "rumble_mode", LEFT_CONTROLLER, index, + motor_config); +static DEVICE_ATTR_RO_NAMED(rumble_mode_left_index, "rumble_mode_index"); + +struct go_cfg_attr rumble_notification_left = { VIBRATION_NOTIFY_ENABLE }; +LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification", + LEFT_CONTROLLER, index, motor_config); +static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index, + "rumble_notification_index"); + +struct go_cfg_attr cal_trigg_left = { TRIGGER_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", SET_TRIGGER_CFG, + LEFT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index"); + +struct go_cfg_attr cal_joy_left = { JOYSTICK_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", SET_JOYSTICK_CFG, + LEFT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index"); + +struct go_cfg_attr cal_gyro_left = { GYRO_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", SET_GYRO_CFG, + LEFT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index"); + +struct go_cfg_attr cal_trigg_left_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_trigg_left_status, "calibrate_trigger_status", + LEFT_CONTROLLER, CALDEV_TRIGGER); + +struct go_cfg_attr cal_joy_left_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_joy_left_status, "calibrate_joystick_status", + LEFT_CONTROLLER, CALDEV_JOYSTICK); + +struct go_cfg_attr cal_gyro_left_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_gyro_left_status, "calibrate_gyro_status", + LEFT_CONTROLLER, CALDEV_GYROSCOPE); + +static struct attribute *left_gamepad_attrs[] = { + &dev_attr_auto_sleep_time_left.attr, + &dev_attr_auto_sleep_time_left_range.attr, + &dev_attr_cal_gyro_left.attr, + &dev_attr_cal_gyro_left_index.attr, + &dev_attr_cal_gyro_left_status.attr, + &dev_attr_cal_joy_left.attr, + &dev_attr_cal_joy_left_index.attr, + &dev_attr_cal_joy_left_status.attr, + &dev_attr_cal_trigg_left.attr, + &dev_attr_cal_trigg_left_index.attr, + &dev_attr_cal_trigg_left_status.attr, + &dev_attr_imu_bypass_left.attr, + &dev_attr_imu_bypass_left_index.attr, + &dev_attr_imu_enable_left.attr, + &dev_attr_imu_enable_left_index.attr, + &dev_attr_reset_left.attr, + &dev_attr_rumble_mode_left.attr, + &dev_attr_rumble_mode_left_index.attr, + &dev_attr_rumble_notification_left.attr, + &dev_attr_rumble_notification_left_index.attr, + &dev_attr_version_hardware_left.attr, + &dev_attr_version_firmware_left.attr, + &dev_attr_version_gen_left.attr, + &dev_attr_version_product_left.attr, + &dev_attr_version_protocol_left.attr, + NULL, +}; + +static const struct attribute_group left_gamepad_attr_group = { + .name = "left_handle", + .attrs = left_gamepad_attrs, +}; + +/* Gamepad - Right */ +struct go_cfg_attr version_product_right = { PRODUCT_VERSION }; +LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version); + +struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION }; +LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version); + +struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version); + +struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION }; +LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version); + +struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION }; +LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version); + +struct go_cfg_attr auto_sleep_time_right = { FEATURE_AUTO_SLEEP_TIME }; +LEGO_DEVICE_ATTR_RW(auto_sleep_time_right, "auto_sleep_time", RIGHT_CONTROLLER, + range, feature_status); +static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range, + "auto_sleep_time_range"); + +struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS }; +LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enable", RIGHT_CONTROLLER, + index, feature_status); +static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enable_index"); + +struct go_cfg_attr imu_enable_right = { FEATURE_IMU_BYPASS }; +LEGO_DEVICE_ATTR_RW(imu_enable_right, "imu_enable", RIGHT_CONTROLLER, index, + feature_status); +static DEVICE_ATTR_RO_NAMED(imu_enable_right_index, "imu_enable_index"); + +struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD }; +LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status); + +struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE }; +LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index, + motor_config); +static DEVICE_ATTR_RO_NAMED(rumble_mode_right_index, "rumble_mode_index"); + +struct go_cfg_attr rumble_notification_right = { VIBRATION_NOTIFY_ENABLE }; +LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification", + RIGHT_CONTROLLER, index, motor_config); +static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index, + "rumble_notification_index"); + +struct go_cfg_attr cal_trigg_right = { TRIGGER_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", SET_TRIGGER_CFG, + RIGHT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index"); + +struct go_cfg_attr cal_joy_right = { JOYSTICK_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", SET_JOYSTICK_CFG, + RIGHT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index"); + +struct go_cfg_attr cal_gyro_right = { GYRO_CALIBRATE }; +LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", SET_GYRO_CFG, + RIGHT_CONTROLLER, index); +static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index"); + +struct go_cfg_attr cal_trigg_right_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_trigg_right_status, "calibrate_trigger_status", + RIGHT_CONTROLLER, CALDEV_TRIGGER); + +struct go_cfg_attr cal_joy_right_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_joy_right_status, "calibrate_joystick_status", + RIGHT_CONTROLLER, CALDEV_JOYSTICK); + +struct go_cfg_attr cal_gyro_right_status = { GET_CAL_STATUS }; +LEGO_DEVICE_STATUS_ATTR(cal_gyro_right_status, "calibrate_gyro_status", + RIGHT_CONTROLLER, CALDEV_GYROSCOPE); + +static struct attribute *right_gamepad_attrs[] = { + &dev_attr_auto_sleep_time_right.attr, + &dev_attr_auto_sleep_time_right_range.attr, + &dev_attr_cal_gyro_right.attr, + &dev_attr_cal_gyro_right_index.attr, + &dev_attr_cal_gyro_right_status.attr, + &dev_attr_cal_joy_right.attr, + &dev_attr_cal_joy_right_index.attr, + &dev_attr_cal_joy_right_status.attr, + &dev_attr_cal_trigg_right.attr, + &dev_attr_cal_trigg_right_index.attr, + &dev_attr_cal_trigg_right_status.attr, + &dev_attr_imu_bypass_right.attr, + &dev_attr_imu_bypass_right_index.attr, + &dev_attr_imu_enable_right.attr, + &dev_attr_imu_enable_right_index.attr, + &dev_attr_reset_right.attr, + &dev_attr_rumble_mode_right.attr, + &dev_attr_rumble_mode_right_index.attr, + &dev_attr_rumble_notification_right.attr, + &dev_attr_rumble_notification_right_index.attr, + &dev_attr_version_hardware_right.attr, + &dev_attr_version_firmware_right.attr, + &dev_attr_version_gen_right.attr, + &dev_attr_version_product_right.attr, + &dev_attr_version_protocol_right.attr, + NULL, +}; + +static const struct attribute_group right_gamepad_attr_group = { + .name = "right_handle", + .attrs = right_gamepad_attrs, +}; + +/* Touchpad */ +struct go_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE }; +LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index, + feature_status); +static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index"); + +struct go_cfg_attr touchpad_vibration_enabled = { TP_VIBRATION_ENABLE }; +LEGO_DEVICE_ATTR_RW(touchpad_vibration_enabled, "vibration_enabled", UNSPECIFIED, + index, motor_config); +static DEVICE_ATTR_RO_NAMED(touchpad_vibration_enabled_index, + "vibration_enabled_index"); + +struct go_cfg_attr touchpad_vibration_intensity = { TP_VIBRATION_INTENSITY }; +LEGO_DEVICE_ATTR_RW(touchpad_vibration_intensity, "vibration_intensity", + UNSPECIFIED, index, motor_config); +static DEVICE_ATTR_RO_NAMED(touchpad_vibration_intensity_index, + "vibration_intensity_index"); + +static struct attribute *touchpad_attrs[] = { + &dev_attr_touchpad_enabled.attr, + &dev_attr_touchpad_enabled_index.attr, + &dev_attr_touchpad_vibration_enabled.attr, + &dev_attr_touchpad_vibration_enabled_index.attr, + &dev_attr_touchpad_vibration_intensity.attr, + &dev_attr_touchpad_vibration_intensity_index.attr, + NULL, +}; + +static const struct attribute_group touchpad_attr_group = { + .name = "touchpad", + .attrs = touchpad_attrs, +}; + +static const struct attribute_group *top_level_attr_groups[] = { + &mcu_attr_group, &tx_dongle_attr_group, + &left_gamepad_attr_group, &right_gamepad_attr_group, + &touchpad_attr_group, NULL, +}; + +/* RGB */ +struct go_cfg_attr rgb_enabled = { FEATURE_LIGHT_ENABLE }; + +LEGO_DEVICE_ATTR_RW(rgb_enabled, "enabled", UNSPECIFIED, index, feature_status); +static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index"); +static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index"); +static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index"); +static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range"); +static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range"); +static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); +static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode"); +static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile"); +static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed"); + +static struct attribute *go_rgb_attrs[] = { + &dev_attr_rgb_effect.attr, + &dev_attr_rgb_effect_index.attr, + &dev_attr_rgb_enabled.attr, + &dev_attr_rgb_enabled_index.attr, + &dev_attr_rgb_mode.attr, + &dev_attr_rgb_mode_index.attr, + &dev_attr_rgb_profile.attr, + &dev_attr_rgb_profile_range.attr, + &dev_attr_rgb_speed.attr, + &dev_attr_rgb_speed_range.attr, + NULL, +}; + +static struct attribute_group rgb_attr_group = { + .attrs = go_rgb_attrs, +}; + +struct mc_subled go_rgb_subled_info[] = { + { + .color_index = LED_COLOR_ID_RED, + .brightness = 0x50, + .intensity = 0x24, + .channel = 0x1, + }, + { + .color_index = LED_COLOR_ID_GREEN, + .brightness = 0x50, + .intensity = 0x22, + .channel = 0x2, + }, + { + .color_index = LED_COLOR_ID_BLUE, + .brightness = 0x50, + .intensity = 0x99, + .channel = 0x3, + }, +}; + +struct led_classdev_mc go_cdev_rgb = { + .led_cdev = { + .name = "go:rgb:joystick_rings", + .color = LED_COLOR_ID_RGB, + .brightness = 0x50, + .max_brightness = 0x64, + .brightness_set = hid_go_brightness_set, + }, + .num_colors = ARRAY_SIZE(go_rgb_subled_info), + .subled_info = go_rgb_subled_info, +}; + +static void cfg_setup(struct work_struct *work) +{ + int ret; + + /* RGB */ + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, + GET_FEATURE_STATUS, FEATURE_LIGHT_ENABLE, + UNSPECIFIED, 0, 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB enabled: %i\n", ret); + return; + } + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG, + LIGHT_MODE_SEL, UNSPECIFIED, 0, 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Mode: %i\n", ret); + return; + } + + ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_RGB_CFG, + LIGHT_PROFILE_SEL, UNSPECIFIED, 0, 0); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Profile: %i\n", ret); + return; + } + + ret = rgb_attr_show(); + if (ret < 0) { + dev_err(drvdata.led_cdev->dev, + "Failed to retrieve RGB Profile Data: %i\n", ret); + return; + } +} + +static int hid_go_cfg_probe(struct hid_device *hdev, + const struct hid_device_id *_id) +{ + unsigned char *buf; + int ret; + + buf = devm_kzalloc(&hdev->dev, GO_PACKET_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + hid_set_drvdata(hdev, &drvdata); + drvdata.buf = buf; + drvdata.hdev = hdev; + mutex_init(&drvdata.cfg_mutex); + + ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups); + if (ret) { + dev_err_probe(&hdev->dev, ret, + "Failed to create gamepad configuration attributes\n"); + return ret; + } + + ret = devm_led_classdev_multicolor_register(&hdev->dev, &go_cdev_rgb); + if (ret) { + dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n"); + return ret; + } + + ret = devm_device_add_group(go_cdev_rgb.led_cdev.dev, &rgb_attr_group); + if (ret) { + dev_err_probe(&hdev->dev, ret, + "Failed to create RGB configuration attributes\n"); + return ret; + } + + drvdata.led_cdev = &go_cdev_rgb.led_cdev; + + init_completion(&drvdata.send_cmd_complete); + + /* Executing calls prior to returning from probe will lock the MCU. Schedule + * initial data call after probe has completed and MCU can accept calls. + */ + INIT_DELAYED_WORK(&drvdata.go_cfg_setup, &cfg_setup); + ret = schedule_delayed_work(&drvdata.go_cfg_setup, msecs_to_jiffies(2)); + if (!ret) { + dev_err(&hdev->dev, + "Failed to schedule startup delayed work\n"); + return -ENODEV; + } + return 0; +} + +static void hid_go_cfg_remove(struct hid_device *hdev) +{ + guard(mutex)(&drvdata.cfg_mutex); + cancel_delayed_work_sync(&drvdata.go_cfg_setup); + sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups); + hid_hw_close(hdev); + hid_hw_stop(hdev); + hid_set_drvdata(hdev, NULL); +} + +static int hid_go_cfg_reset_resume(struct hid_device *hdev) +{ + u8 os_mode = drvdata.os_mode; + int ret; + + ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE, + SET_OS_MODE, USB_MCU, &os_mode, 1); + if (ret < 0) + return ret; + + ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE, + GET_OS_MODE, USB_MCU, 0, 0); + if (ret < 0) + return ret; + + if (drvdata.os_mode != os_mode) + return -ENODEV; + + return 0; +} + +static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int ret, ep; + + hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "Parse failed\n"); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + hid_err(hdev, "Failed to start HID device\n"); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "Failed to open HID device\n"); + hid_hw_stop(hdev); + return ret; + } + + ep = get_endpoint_address(hdev); + if (ep != GO_GP_INTF_IN) { + dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep); + return 0; + } + + ret = hid_go_cfg_probe(hdev, id); + if (ret) + dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface\n"); + + dev_dbg(&hdev->dev, "Started Legion Go HID Device: %x\n", ep); + + return ret; +} + +static void hid_go_remove(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + + if (ep <= 0) + return; + + switch (ep) { + case GO_GP_INTF_IN: + hid_go_cfg_remove(hdev); + break; + default: + hid_hw_close(hdev); + hid_hw_stop(hdev); + break; + } +} + +static int hid_go_reset_resume(struct hid_device *hdev) +{ + int ep = get_endpoint_address(hdev); + + switch (ep) { + case GO_GP_INTF_IN: + return hid_go_cfg_reset_resume(hdev); + default: + break; + } + + return 0; +} + +static const struct hid_device_id hid_go_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, + USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, + USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, + USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, + USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS) }, + {} +}; +MODULE_DEVICE_TABLE(hid, hid_go_devices); + +static struct hid_driver hid_lenovo_go = { + .name = "hid-lenovo-go", + .id_table = hid_go_devices, + .probe = hid_go_probe, + .remove = hid_go_remove, + .raw_event = hid_go_raw_event, + .reset_resume = hid_go_reset_resume, +}; +module_hid_driver(hid_lenovo_go); + +MODULE_AUTHOR("Derek J. Clark"); +MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go Series Gamepads."); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/hid-msi-claw.c b/drivers/hid/hid-msi-claw.c new file mode 100644 index 000000000000..3d95e31c1ae3 --- /dev/null +++ b/drivers/hid/hid-msi-claw.c @@ -0,0 +1,842 @@ +#include +#include +#include +#include +#include + +//#include "hid-ids.h" + +#define MSI_CLAW_FEATURE_GAMEPAD_REPORT_ID 0x0f + +#define MSI_CLAW_READ_SIZE 64 +#define MSI_CLAW_WRITE_SIZE 64 + +#define MSI_CLAW_GAME_CONTROL_DESC 0x05 +#define MSI_CLAW_DEVICE_CONTROL_DESC 0x06 + +enum msi_claw_gamepad_mode { + MSI_CLAW_GAMEPAD_MODE_OFFLINE = 0x00, + MSI_CLAW_GAMEPAD_MODE_XINPUT = 0x01, + MSI_CLAW_GAMEPAD_MODE_DINPUT = 0x02, + MSI_CLAW_GAMEPAD_MODE_MSI = 0x03, + MSI_CLAW_GAMEPAD_MODE_DESKTOP = 0x04, + MSI_CLAW_GAMEPAD_MODE_BIOS = 0x05, + MSI_CLAW_GAMEPAD_MODE_TESTING = 0x06, + + MSI_CLAW_GAMEPAD_MODE_MAX, +}; + +enum msi_claw_mkeys_function { + MSI_CLAW_MKEY_FUNCTION_MACRO = 0x00, + MSI_CLAW_MKEY_FUNCTION_COMBINATION = 0x01, + MSI_CLAW_MKEY_FUNCTION_DISABLED = 0x02, + + MSI_CLAW_MKEY_FUNCTION_MAX, +}; + +static const bool gamepad_mode_debug = false; + +static const struct { + const char* name; + const bool available; +} gamepad_mode_map[] = { + {"offline", gamepad_mode_debug}, + {"xinput", true}, + {"dinput", gamepad_mode_debug}, + {"msi", gamepad_mode_debug}, + {"desktop", true}, + {"bios", gamepad_mode_debug}, + {"testing", gamepad_mode_debug}, +}; + +static const char* mkeys_function_map[] = +{ + "macro", + "combination", +}; + +enum msi_claw_command_type { + MSI_CLAW_COMMAND_TYPE_ENTER_PROFILE_CONFIG = 0x01, + MSI_CLAW_COMMAND_TYPE_EXIT_PROFILE_CONFIG = 0x02, + MSI_CLAW_COMMAND_TYPE_WRITE_PROFILE = 0x03, + MSI_CLAW_COMMAND_TYPE_READ_PROFILE = 0x04, + MSI_CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05, + // ACK is read after a WRITE_RGB_STATUS + MSI_CLAW_COMMAND_TYPE_ACK = 0x06, + MSI_CLAW_COMMAND_TYPE_SWITCH_PROFILE = 0x07, + MSI_CLAW_COMMAND_TYPE_WRITE_PROFILE_TO_EEPROM = 0x08, + MSI_CLAW_COMMAND_TYPE_SYNC_RGB = 0x09, + MSI_CLAW_COMMAND_TYPE_READ_RGB_STATUS_ACK = 0x0a, + MSI_CLAW_COMMAND_TYPE_READ_CURRENT_PROFILE = 0x0b, + MSI_CLAW_COMMAND_TYPE_READ_CURRENT_PROFILE_ACK = 0x0c, + MSI_CLAW_COMMAND_TYPE_READ_RGB_STATUS = 0x0d, + // TODO: 0f00003c210100b137ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000ff00000000 + MSI_CLAW_COMMAND_TYPE_WRITE_RGB_STATUS = 0x21, + MSI_CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22, + MSI_CLAW_COMMAND_TYPE_RESTORE_FROM_ROM = 0x23, + MSI_CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24, + MSI_CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26, + MSI_CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27, + MSI_CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28, + MSI_CLAW_COMMAND_TYPE_RGB_CONTROL = 0xe0, + MSI_CLAW_COMMAND_TYPE_CALIBRATION_CONTROL = 0xfd, + MSI_CLAW_COMMAND_TYPE_CALIBRATION_ACK = 0xfe, +}; + +struct msi_claw_control_status { + enum msi_claw_gamepad_mode gamepad_mode; + enum msi_claw_mkeys_function mkeys_function; +}; + +struct msi_claw_read_data { + const uint8_t *data; + int size; + + struct msi_claw_read_data *next; +}; + +struct msi_claw_drvdata { + struct hid_device *hdev; + + //struct input_dev *input; + + struct msi_claw_control_status *control; + + struct mutex read_data_mutex; + struct msi_claw_read_data *read_data; +}; + +static int msi_claw_write_cmd(struct hid_device *hdev, enum msi_claw_command_type cmdtype, + const uint8_t *const buffer, size_t buffer_len) +{ + int ret; + uint8_t *dmabuf = NULL; + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + const uint8_t buf[MSI_CLAW_WRITE_SIZE] = { + MSI_CLAW_FEATURE_GAMEPAD_REPORT_ID, 0, 0, 0x3c, cmdtype }; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto msi_claw_write_cmd_err; + } + + if (buffer != NULL) + memcpy((void *)&buf[5], buffer, buffer_len); + else + buffer_len = 0; + + memset((void *)&buf[5 + buffer_len], 0, MSI_CLAW_WRITE_SIZE - (5 + buffer_len)); + dmabuf = kmemdup(buf, MSI_CLAW_WRITE_SIZE, GFP_KERNEL); + if (!dmabuf) { + ret = -ENOMEM; + hid_err(hdev, "hid-msi-claw failed to alloc dma buf: %d\n", ret); + goto msi_claw_write_cmd_err; + } + + ret = hid_hw_output_report(hdev, dmabuf, MSI_CLAW_WRITE_SIZE); + if (ret != MSI_CLAW_WRITE_SIZE) { + hid_err(hdev, "hid-msi-claw failed to switch controller mode: %d\n", ret); + goto msi_claw_write_cmd_err; + } + + hid_notice(hdev, "hid-msi-claw sent %d bytes, cmd: 0x%02x\n", ret, dmabuf[4]); + +msi_claw_write_cmd_err: + kfree(dmabuf); + + return ret; +} + +static int msi_claw_read(struct hid_device *hdev, uint8_t *const buffer, int size, uint32_t timeout) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + struct msi_claw_read_data *event = NULL; + int ret = 0; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto msi_claw_read_err; + } + + for (uint32_t i = 0; (event == NULL) && (i <= timeout); i++) { + if (timeout - i) + msleep(20); + + scoped_guard(mutex, &drvdata->read_data_mutex) { + event = drvdata->read_data; + + if (event != NULL) + drvdata->read_data = event->next; + }; + } + + if (event == NULL) { + ret = -EIO; + hid_err(hdev, "hid-msi-claw no answer from device\n"); + goto msi_claw_read_err; + } + + if (size < event->size) { + ret = -EINVAL; + hid_err(hdev, "hid-msi-claw invalid buffer size: too short\n"); + goto msi_claw_read_err; + } + + memcpy((void *)buffer, (const void *)event->data, event->size); + ret = event->size; + +msi_claw_read_err: + if (event != NULL) { + kfree(event->data); + kfree(event); + } + + return ret; +} + +static int msi_claw_raw_event_control(struct hid_device *hdev, struct msi_claw_drvdata *drvdata, + struct hid_report *report, uint8_t *data, int size) +{ + struct msi_claw_read_data **list = NULL; + struct msi_claw_read_data *node = NULL; + uint8_t *buffer; + int ret, i; + + if (size != MSI_CLAW_READ_SIZE) { + //hid_err(hdev, "hid-msi-claw got unknown %d bytes\n", size); + goto msi_claw_raw_event_control_err; + } else if (data[0] != 0x10) { + hid_err(hdev, "hid-msi-claw unrecognised byte at offset 0: expected 0x10, got 0x%02x\n", data[0]); + goto msi_claw_raw_event_control_err; + } else if (data[1] != 0x00) { + hid_err(hdev, "hid-msi-claw unrecognised byte at offset 1: expected 0x00, got 0x%02x\n", data[1]); + goto msi_claw_raw_event_control_err; + } else if (data[2] != 0x00) { + hid_err(hdev, "hid-msi-claw unrecognised byte at offset 2: expected 0x00, got 0x%02x\n", data[2]); + goto msi_claw_raw_event_control_err; + } else if (data[3] != 0x3c) { + hid_err(hdev, "hid-msi-claw unrecognised byte at offset 3: expected 0x3c, got 0x%02x\n", data[3]); + goto msi_claw_raw_event_control_err; + } + + buffer = kmemdup(data, size, GFP_KERNEL); + if (buffer == NULL) { + ret = -ENOMEM; + hid_err(hdev, "hid-msi-claw failed to alloc %d bytes for read buffer: %d\n", size, ret); + goto msi_claw_raw_event_control_err; + } + + struct msi_claw_read_data evt = { + .data = buffer, + .size = size, + .next = NULL, + }; + + node = kmemdup(&evt, sizeof(evt), GFP_KERNEL); + if (!node) { + ret = -ENOMEM; + kfree(buffer); + hid_err(hdev, "hid-msi-claw failed to alloc event node: %d\n", ret); + goto msi_claw_raw_event_control_err; + } + + scoped_guard(mutex, &drvdata->read_data_mutex) { + list = &drvdata->read_data; + for (i = 0; (i < 32) && (*list != NULL); i++) + list = &(*list)->next; + + if (*list != NULL) { + ret = -EIO; + hid_err(hdev, "too many unparsed events: ignoring\n"); + goto msi_claw_raw_event_control_err; + } + + *list = node; + } + + hid_notice(hdev, "hid-msi-claw received %d bytes, cmd: 0x%02x\n", size, buffer[4]); + + return 0; + +msi_claw_raw_event_control_err: + if (buffer != NULL) + kfree(buffer); + + if (node != NULL) + kfree(node); + + return ret; +} + +static int msi_claw_raw_event(struct hid_device *hdev, struct hid_report *report, uint8_t *data, int size) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (!drvdata->control) { + hid_notice(hdev, "hid-msi-claw event not from control interface: ignoring\n"); + return 0; + } + + return msi_claw_raw_event_control(hdev, drvdata, report, data, size); +} + +static int msi_claw_await_ack(struct hid_device *hdev) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + uint8_t buffer[MSI_CLAW_READ_SIZE]; + int ret; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto msi_claw_await_ack_err; + } + + ret = msi_claw_read(hdev, buffer, MSI_CLAW_READ_SIZE, 1000); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw failed to read ack: %d\n", ret); + goto msi_claw_await_ack_err; + } else if (ret != MSI_CLAW_READ_SIZE) { + hid_err(hdev, "hid-msi-claw invalid read: expected %d bytes, got %d\n", MSI_CLAW_READ_SIZE, ret); + ret = -EINVAL; + goto msi_claw_await_ack_err; + } + + if (buffer[4] != (uint8_t)MSI_CLAW_COMMAND_TYPE_ACK) { + hid_err(hdev, "hid-msi-claw received invalid response: expected ack 0x06, got 0x%02x\n", buffer[4]); + ret = -EINVAL; + goto msi_claw_await_ack_err; + } + + ret = 0; + +msi_claw_await_ack_err: + return ret; +} + +static int sync_to_rom(struct hid_device *hdev) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto sync_to_rom_err; + } + + ret = msi_claw_write_cmd(hdev, MSI_CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw failed to send write request for switch controller mode: %d\n", ret); + goto sync_to_rom_err; + } else if (ret != MSI_CLAW_WRITE_SIZE) { + hid_err(hdev, "hid-msi-claw failed to send the sync to rom command: %d\n", ret); + ret = -EIO; + goto sync_to_rom_err; + } + + ret = msi_claw_await_ack(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to await first ack: %d\n", ret); + goto sync_to_rom_err; + } + + // the sync to rom also triggers two ack + ret = msi_claw_await_ack(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to await second ack: %d\n", ret); + goto sync_to_rom_err; + } + + ret = 0; + +sync_to_rom_err: + return ret; +} + +static int msi_claw_reset_device(struct hid_device *hdev) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto msi_claw_reset_device_err; + } + + ret = msi_claw_write_cmd(hdev, MSI_CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw failed to send reset: %d\n", ret); + goto msi_claw_reset_device_err; + } else if (ret != MSI_CLAW_WRITE_SIZE) { + hid_err(hdev, "hid-msi-claw couldn't send reset request: %d\n", ret); + ret = -EIO; + goto msi_claw_reset_device_err; + } + + ret = msi_claw_await_ack(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to await ack: %d\n", ret); + goto msi_claw_reset_device_err; + } + +msi_claw_reset_device_err: + return ret; +} + +static int msi_claw_read_gamepad_mode(struct hid_device *hdev, + struct msi_claw_control_status *status) +{ + uint8_t buffer[MSI_CLAW_READ_SIZE] = {}; + int ret; + + ret = msi_claw_write_cmd(hdev, MSI_CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw failed to send read request for controller mode: %d\n", ret); + goto msi_claw_read_gamepad_mode_err; + } else if (ret != MSI_CLAW_WRITE_SIZE) { + hid_err(hdev, "hid-msi-claw couldn't send request: %d\n", ret); + ret = -EIO; + goto msi_claw_read_gamepad_mode_err; + } + + ret = msi_claw_read(hdev, buffer, MSI_CLAW_READ_SIZE, 50); + if (ret != MSI_CLAW_READ_SIZE) { + hid_err(hdev, "hid-msi-claw failed to read: %d\n", ret); + ret = -EINVAL; + goto msi_claw_read_gamepad_mode_err; + } + + if (buffer[4] != (uint8_t)MSI_CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK) { + hid_err(hdev, "hid-msi-claw received invalid response: expected 0x27, got 0x%02x\n", buffer[4]); + ret = -EINVAL; + goto msi_claw_read_gamepad_mode_err; + } else if (buffer[5] >= MSI_CLAW_GAMEPAD_MODE_MAX) { + hid_err(hdev, "hid-msi-claw unknown gamepad mode: 0x%02x\n", buffer[5]); + ret = -EINVAL; + goto msi_claw_read_gamepad_mode_err; + } else if (buffer[6] >= MSI_CLAW_MKEY_FUNCTION_MAX) { + hid_err(hdev, "hid-msi-claw unknown gamepad mode: 0x%02x\n", buffer[6]); + ret = -EINVAL; + goto msi_claw_read_gamepad_mode_err; + } + + status->gamepad_mode = (enum msi_claw_gamepad_mode)buffer[5]; + status->mkeys_function = (enum msi_claw_mkeys_function)buffer[6]; + + ret = 0; + +msi_claw_read_gamepad_mode_err: + return ret; +} + +static int msi_claw_switch_gamepad_mode(struct hid_device *hdev, + const struct msi_claw_control_status *status) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + struct msi_claw_control_status check_status; + const uint8_t cmd_buffer[2] = {(uint8_t)status->gamepad_mode, (uint8_t)status->mkeys_function}; + int ret; + + if (!drvdata->control) { + hid_err(hdev, "hid-msi-claw couldn't find control interface\n"); + ret = -ENODEV; + goto msi_claw_switch_gamepad_mode_err; + } + + ret = msi_claw_write_cmd(hdev, MSI_CLAW_COMMAND_TYPE_SWITCH_MODE, cmd_buffer, sizeof(cmd_buffer)); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw failed to send write request to switch controller mode: %d\n", ret); + goto msi_claw_switch_gamepad_mode_err; + } else if (ret != MSI_CLAW_WRITE_SIZE) { + hid_err(hdev, "hid-msi-claw failed to write: %d bytes got written\n", ret); + ret = -EIO; + goto msi_claw_switch_gamepad_mode_err; + } + + ret = msi_claw_await_ack(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to await first ack: %d\n", ret); + goto msi_claw_switch_gamepad_mode_err; + } + + // the gamepad mode switch mode triggers two ack + ret = msi_claw_await_ack(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to await second ack: %d\n", ret); + goto msi_claw_switch_gamepad_mode_err; + } + + // check the new mode as official application does + ret = msi_claw_read_gamepad_mode(hdev, &check_status); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to read status: %d\n", ret); + goto msi_claw_switch_gamepad_mode_err; + } + + if (memcmp((const void *)&check_status, (const void *)status, sizeof(*status))) { + hid_err(hdev, "hid-msi-claw current status and target one are different\n"); + ret = -EIO; + goto msi_claw_switch_gamepad_mode_err; + } + + // the device now sends back 03 00 00 00 00 00 00 00 00 + + // this command is always issued by the windows counterpart after a mode switch + ret = sync_to_rom(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed the sync to rom command: %d\n", ret); + return ret; + } + +msi_claw_switch_gamepad_mode_err: + return ret; +} + +static ssize_t reset_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + struct hid_device *hdev = to_hid_device(dev); + int ret; + + ret = msi_claw_reset_device(hdev); + if (ret < 0) { + hid_err(hdev, "hid-msi-claw error resetting device: %d\n", ret); + goto reset_store_err; + } + + return count; + +reset_store_err: + return ret; +} +static DEVICE_ATTR_WO(reset); + +static ssize_t gamepad_mode_available_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + int i, ret = 0; + int len = ARRAY_SIZE(gamepad_mode_map); + + for (i = 0; i < len; i++) { + if (!gamepad_mode_map[i].available) + continue; + + ret += sysfs_emit_at(buf, ret, "%s", gamepad_mode_map[i].name); + + if (i < len-1) + ret += sysfs_emit_at(buf, ret, " "); + } + ret += sysfs_emit_at(buf, ret, "\n"); + + return ret; +} +static DEVICE_ATTR_RO(gamepad_mode_available); + +static ssize_t gamepad_mode_current_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct msi_claw_control_status status; + int ret; + + ret = msi_claw_read_gamepad_mode(hdev, &status); + if (ret) { + hid_err(hdev, "hid-msi-claw error reaging the gamepad mode: %d\n", ret); + return ret; + } + + return sysfs_emit(buf, "%s\n", gamepad_mode_map[(int)status.gamepad_mode].name); +} + +static ssize_t gamepad_mode_current_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) +{ + ssize_t ret; + uint8_t *input; + struct hid_device *hdev = to_hid_device(dev); + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + enum msi_claw_gamepad_mode new_gamepad_mode = ARRAY_SIZE(gamepad_mode_map); + struct msi_claw_control_status status = { + .gamepad_mode = drvdata->control->gamepad_mode, + .mkeys_function = drvdata->control->mkeys_function, + }; + + if (!count) { + ret = -EINVAL; + goto gamepad_mode_current_store_err; + } + + input = kmemdup(buf, count+1, GFP_KERNEL); + if (!input) { + ret = -ENOMEM; + goto gamepad_mode_current_store_err; + } + + input[count] = '\0'; + if (input[count-1] == '\n') + input[count-1] = '\0'; + + for (size_t i = 0; i < (size_t)new_gamepad_mode; i++) + if ((!strcmp(input, gamepad_mode_map[i].name)) && (gamepad_mode_map[i].available)) + new_gamepad_mode = (enum msi_claw_gamepad_mode)i; + + kfree(input); + + if (new_gamepad_mode == ARRAY_SIZE(gamepad_mode_map)) { + hid_err(hdev, "Invalid gamepad mode selected\n"); + ret = -EINVAL; + goto gamepad_mode_current_store_err; + } + + status.gamepad_mode = new_gamepad_mode; + ret = msi_claw_switch_gamepad_mode(hdev, &status); + if (ret) { + hid_err(hdev, "Error changing gamepad mode: %d\n", (int)ret); + goto gamepad_mode_current_store_err; + } + + ret = count; + +gamepad_mode_current_store_err: + return ret; +} +static DEVICE_ATTR_RW(gamepad_mode_current); + +static ssize_t mkeys_function_available_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + int i, ret = 0; + int len = ARRAY_SIZE(mkeys_function_map); + + for (i = 0; i < len; i++) { + ret += sysfs_emit_at(buf, ret, "%s", mkeys_function_map[i]); + + if (i < len-1) + ret += sysfs_emit_at(buf, ret, " "); + } + ret += sysfs_emit_at(buf, ret, "\n"); + + return ret; +} +static DEVICE_ATTR_RO(mkeys_function_available); + +static ssize_t mkeys_function_current_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev = to_hid_device(dev); + struct msi_claw_control_status status; + int ret = msi_claw_read_gamepad_mode(hdev, &status); + + if (ret) { + hid_err(hdev, "hid-msi-claw error reaging the gamepad mode: %d\n", ret); + return ret; + } + + return sysfs_emit(buf, "%s\n", mkeys_function_map[(int)status.mkeys_function]); +} + +static ssize_t mkeys_function_current_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + uint8_t *input; + ssize_t err; + struct hid_device *hdev = to_hid_device(dev); + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + enum msi_claw_mkeys_function new_mkeys_function = ARRAY_SIZE(mkeys_function_map); + struct msi_claw_control_status status = { + .gamepad_mode = drvdata->control->gamepad_mode, + .mkeys_function = drvdata->control->mkeys_function, + }; + + if (!count) + return -EINVAL; + + input = kmemdup(buf, count+1, GFP_KERNEL); + if (!input) + return -ENOMEM; + + input[count] = '\0'; + if (input[count-1] == '\n') + input[count-1] = '\0'; + + for (size_t i = 0; i < (size_t)new_mkeys_function; i++) + if (!strcmp(input, mkeys_function_map[i])) + new_mkeys_function = i; + + kfree(input); + + if (new_mkeys_function == ARRAY_SIZE(mkeys_function_map)) { + hid_err(hdev, "Invalid mkeys function selected\n"); + return -EINVAL; + } + + status.mkeys_function = new_mkeys_function; + err = msi_claw_switch_gamepad_mode(hdev, &status); + if (err) { + hid_err(hdev, "Error changing mkeys function: %d\n", (int)err); + return err; + } + + return count; +} +static DEVICE_ATTR_RW(mkeys_function_current); + +static int __maybe_unused msi_claw_resume(struct hid_device *hdev) +{ + int ret; + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + struct msi_claw_control_status status = { + .gamepad_mode = drvdata->control->gamepad_mode, + .mkeys_function = drvdata->control->mkeys_function, + }; + + // TODO: clear out events list here (or in suspend?) + + // wait for device to be ready + msleep(500); + + ret = msi_claw_switch_gamepad_mode(hdev, &status); + if (ret) { + hid_err(hdev, "Error changing gamepad mode: %d\n", (int)ret); + goto msi_claw_resume_err; + } + + // TODO: retry until this works? + + return 0; + +msi_claw_resume_err: + return ret; +} + +static int msi_claw_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int ret; + struct msi_claw_drvdata *drvdata; + + if (!hid_is_usb(hdev)) { + hid_err(hdev, "hid-msi-claw hid not usb\n"); + return -ENODEV; + } + + drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (drvdata == NULL) { + hid_err(hdev, "hid-msi-claw can't alloc descriptor\n"); + return -ENOMEM; + } + + mutex_init(&drvdata->read_data_mutex); + drvdata->read_data = NULL; + drvdata->control = NULL; + + hid_set_drvdata(hdev, drvdata); + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw hid parse failed: %d\n", ret); + return ret; + } + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + hid_err(hdev, "hid-msi-claw hw start failed: %d\n", ret); + return ret; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to open HID device: %d\n", ret); + goto err_stop_hw; + } + + if (hdev->rdesc[0] == MSI_CLAW_DEVICE_CONTROL_DESC) { + drvdata->control = devm_kzalloc(&hdev->dev, sizeof(*(drvdata->control)), GFP_KERNEL); + if (drvdata->control == NULL) { + hid_err(hdev, "hid-msi-claw can't alloc control interface data\n"); + ret = -ENOMEM; + goto err_close; + } + + drvdata->control->gamepad_mode = MSI_CLAW_GAMEPAD_MODE_XINPUT; + drvdata->control->mkeys_function = MSI_CLAW_MKEY_FUNCTION_MACRO; + + ret = sysfs_create_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_available.attr); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to sysfs_create_file dev_attr_gamepad_mode_available: %d\n", ret); + goto err_close; + } + + ret = sysfs_create_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_current.attr); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to sysfs_create_file dev_attr_gamepad_mode_current: %d\n", ret); + goto err_dev_attr_gamepad_mode_current; + } + + ret = sysfs_create_file(&hdev->dev.kobj, &dev_attr_mkeys_function_available.attr); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to sysfs_create_file dev_attr_mkeys_function_available: %d\n", ret); + goto err_dev_attr_mkeys_function_available; + } + + ret = sysfs_create_file(&hdev->dev.kobj, &dev_attr_mkeys_function_current.attr); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to sysfs_create_file dev_attr_mkeys_function_current: %d\n", ret); + goto err_dev_attr_mkeys_function_current; + } + + ret = sysfs_create_file(&hdev->dev.kobj, &dev_attr_reset.attr); + if (ret) { + hid_err(hdev, "hid-msi-claw failed to sysfs_create_file dev_attr_reset: %d\n", ret); + goto err_dev_attr_reset; + } + } + + return 0; + +err_dev_attr_gamepad_mode_current: + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_available.attr); +err_dev_attr_mkeys_function_available: + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_current.attr); +err_dev_attr_mkeys_function_current: + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_mkeys_function_available.attr); +err_dev_attr_reset: + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_mkeys_function_current.attr); +err_close: + hid_hw_close(hdev); +err_stop_hw: + hid_hw_stop(hdev); + return ret; +} + +static void msi_claw_remove(struct hid_device *hdev) +{ + struct msi_claw_drvdata *drvdata = hid_get_drvdata(hdev); + + if (drvdata->control) { + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_available.attr); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_gamepad_mode_current.attr); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_mkeys_function_available.attr); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_mkeys_function_current.attr); + sysfs_remove_file(&hdev->dev.kobj, &dev_attr_reset.attr); + } + + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static const struct hid_device_id msi_claw_devices[] = { + { HID_USB_DEVICE(0x0DB0, 0x1901) }, + { } +}; +MODULE_DEVICE_TABLE(hid, msi_claw_devices); + +static struct hid_driver msi_claw_driver = { + .name = "hid-msi-claw", + .id_table = msi_claw_devices, + .raw_event = msi_claw_raw_event, + .probe = msi_claw_probe, + .remove = msi_claw_remove, +}; +module_hid_driver(msi_claw_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Denis Benato "); +MODULE_DESCRIPTION("Manage MSI Claw gamepad device"); diff --git a/drivers/hid/zotac-zone-hid/Kconfig b/drivers/hid/zotac-zone-hid/Kconfig new file mode 100644 index 000000000000..89fb2ac080bb --- /dev/null +++ b/drivers/hid/zotac-zone-hid/Kconfig @@ -0,0 +1,8 @@ +config ZOTAC_ZONE_HID + tristate "Zotac Zone handheld support" + depends on USB_HID + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR + select POWER_SUPPLY + help + Support for the Zotac Zone handheld gaming console. diff --git a/drivers/hid/zotac-zone-hid/Makefile b/drivers/hid/zotac-zone-hid/Makefile new file mode 100644 index 000000000000..2f7053b981ac --- /dev/null +++ b/drivers/hid/zotac-zone-hid/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Makefile - Zotac Zone handheld device driver +# +zotac-zone-hid-y := zotac-zone-hid-core.o zotac-zone-hid-rgb.o zotac-zone-hid-input.o zotac-zone-hid-config.o +obj-$(CONFIG_ZOTAC_ZONE_HID) := zotac-zone-hid.o diff --git a/drivers/hid/zotac-zone-hid/zotac-zone-hid-config.c b/drivers/hid/zotac-zone-hid/zotac-zone-hid-config.c new file mode 100644 index 000000000000..049615194392 --- /dev/null +++ b/drivers/hid/zotac-zone-hid/zotac-zone-hid-config.c @@ -0,0 +1,2231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* +* HID driver for ZOTAC Gaming Zone Controller - RGB LED control +* +* Copyright (c) 2025 Luke D. Jones +*/ + +#include "asm-generic/errno-base.h" +#include "linux/kstrtox.h" +#include +#include +#include +#include +#include + +#include "zotac-zone.h" + +#define REPORT_SIZE 64 + +#define HEADER_TAG_POS 0 +#define RESERVED_POS 1 +#define SEQUENCE_POS 2 +#define PAYLOADSIZE_POS 3 +#define COMMAND_POS 4 +#define SETTING_POS 5 +#define VALUE_POS 6 +#define CRC_H_POS 0x3E +#define CRC_L_POS 0x3F + +#define HEADER_TAG 0xE1 +#define PAYLOAD_SIZE 0x3C + +/* + * Button mapping structure indices (relative to data buffer) + * The data buffer is copied to the correct location in the HID report. + */ +#define HEADER_LEN 5 +#define BTN_MAP_SOURCE_IDX (0x05 - HEADER_LEN) /* Source button ID */ +#define BTN_MAP_GAMEPAD_START_IDX (0x06 - HEADER_LEN) +#define BTN_MAP_GAMEPAD_SIZE 4 +#define BTN_MAP_MODIFIER_IDX (0x0A - HEADER_LEN) +#define BTN_MAP_KEYBOARD_START_IDX (0x0C - HEADER_LEN) +#define BTN_MAP_KEYBOARD_SIZE 6 +#define BTN_MAP_MOUSE_IDX (0x12 - HEADER_LEN) +#define BTN_MAP_RESPONSE_MIN_SIZE 14 + +#define GAMEPAD_DPAD_STICK_IDX 0 /* DPad, Stick buttons */ +#define GAMEPAD_FACE_BUMPER_IDX 1 /* Face buttons, Bumpers */ +#define GAMEPAD_TRIGGER_IDX 2 /* Triggers */ +#define GAMEPAD_RESERVED_IDX 3 /* Reserved/unused */ +/* + * Note: The above indices are relative to our data buffer, not the full protocol packet. + * In the full protocol packet (as described in documentation), these fields would be + * at different positions due to the packet header. + * + * For example, BTN_MAP_MOUSE_IDX (13 in our buffer) corresponds to offset 0x12-0x13 + * in the full protocol packet. + */ + +/* Mouse speed constants */ +#define CMD_SET_MOUSE_SPEED 0xA3 +#define CMD_GET_MOUSE_SPEED 0xA4 +#define MOUSE_SPEED_MIN 0x01 /* Slow */ +#define MOUSE_SPEED_MAX 0x0A /* Fast */ + +#define STICK_SENSITIVITY_NUM_IDX 0 +#define STICK_SENSITIVITY_DATA_IDX 1 +#define STICK_SENSITIVITY_SIZE 8 + +#define DZ_LEFT_INNER_IDX 0 +#define DZ_LEFT_OUTER_IDX 1 +#define DZ_RIGHT_INNER_IDX 2 +#define DZ_RIGHT_OUTER_IDX 3 +#define DZ_RESPONSE_SIZE 4 + +#define VIB_LEFT_TRIGGER_IDX 0 +#define VIB_RIGHT_TRIGGER_IDX 1 +#define VIB_LEFT_RUMBLE_IDX 2 +#define VIB_RIGHT_RUMBLE_IDX 3 +#define VIB_RESPONSE_SIZE 4 + +#define CMD_SET_PROFILE 0xB1 +#define CMD_GET_PROFILE 0xB2 +#define CMD_GET_PROFILE_NUM 0xB3 +#define CMD_RESTORE_PROFILE 0xF1 +#define CMD_SAVE_CONFIG 0xFB + +#define CMD_SET_VIBRATION_STRENGTH 0xA9 +#define CMD_GET_VIBRATION_STRENGTH 0xAA +#define CMD_SET_STICK_DEADZONES 0xA5 +#define CMD_GET_STICK_DEADZONES 0xA6 +#define CMD_SET_TRIGGER_DEADZONES 0xB4 +#define CMD_GET_TRIGGER_DEADZONES 0xB5 +#define CMD_SET_STICK_SENSITIVITY 0xBA +#define CMD_GET_STICK_SENSITIVITY 0xBB +#define CMD_SET_BUTTON_TURBO 0xB8 +#define CMD_GET_BUTTON_TURBO 0xB9 +#define CMD_GET_DEVICE_INFO 0xFA +#define CMD_MOTOR_TEST 0xBD + +#define MOTOR_TEST_SIZE 4 + +#define STICK_LEFT 0 +#define STICK_RIGHT 1 + +#define PROFILE_DEFAULT 0x00 +#define PROFILE_SECONDARY 0x01 + +/* Button bit positions within the turbo byte */ +#define A_BTN_POS 0 +#define B_BTN_POS 1 +#define X_BTN_POS 2 +#define Y_BTN_POS 3 +#define LB_BTN_POS 4 +#define RB_BTN_POS 5 +#define LT_BTN_POS 6 +#define RT_BTN_POS 7 + +/* Button ID definitions */ +#define BUTTON_NONE 0x00 +#define BUTTON_M1 0x01 +#define BUTTON_M2 0x02 +#define BUTTON_L_TOUCH_UP 0x03 +#define BUTTON_L_TOUCH_DOWN 0x04 +#define BUTTON_L_TOUCH_LEFT 0x05 +#define BUTTON_L_TOUCH_RIGHT 0x06 +#define BUTTON_R_TOUCH_UP 0x07 +#define BUTTON_R_TOUCH_DOWN 0x08 +#define BUTTON_R_TOUCH_LEFT 0x09 +#define BUTTON_R_TOUCH_RIGHT 0x0A +#define BUTTON_LB 0x0B +#define BUTTON_RB 0x0C +#define BUTTON_LT 0x0D +#define BUTTON_RT 0x0E +#define BUTTON_A 0x0F +#define BUTTON_B 0x10 +#define BUTTON_X 0x11 +#define BUTTON_Y 0x12 +#define BUTTON_DPAD_UP 0x13 +#define BUTTON_DPAD_DOWN 0x14 +#define BUTTON_DPAD_LEFT 0x15 +#define BUTTON_DPAD_RIGHT 0x16 +#define BUTTON_LS 0x17 +#define BUTTON_RS 0x18 + +/* Modifier key definitions */ +#define MOD_NONE 0x00 +#define MOD_LEFT_CTRL 0x01 +#define MOD_LEFT_SHIFT 0x02 +#define MOD_LEFT_ALT 0x04 +#define MOD_LEFT_WIN 0x08 +#define MOD_RIGHT_CTRL 0x10 +#define MOD_RIGHT_SHIFT 0x20 +#define MOD_RIGHT_ALT 0x40 +#define MOD_RIGHT_WIN 0x80 + +int zotac_cfg_refresh(struct zotac_device *zotac); + +struct button_directory { + const char *name; /* Directory name (e.g., "btn_a") */ + u8 button_id; /* Associated button ID */ + bool has_turbo; /* Whether this button supports turbo */ + struct kobject * + kobj; /* kobject for this directory (filled during registration) */ + struct attribute_group *main_group; /* Main button attributes */ + struct attribute_group *remap_group; /* Remap subdirectory attributes */ +}; + +/* Structure to map button names to their byte and bit positions */ +struct button_mapping_entry { + const char *name; + u8 byte_index; + u8 bit_mask; +}; + +/* Define all supported buttons with their byte index and bit mask */ +static const struct button_mapping_entry button_map[] = { + { "dpad_up", 0, 0x01 }, + { "dpad_down", 0, 0x02 }, + { "dpad_left", 0, 0x04 }, + { "dpad_right", 0, 0x08 }, + { "ls", 0, 0x40 }, + { "rs", 0, 0x80 }, + { "lb", 1, 0x01 }, + { "rb", 1, 0x02 }, + { "a", 1, 0x10 }, + { "b", 1, 0x20 }, + { "x", 1, 0x40 }, + { "y", 1, 0x80 }, + { "lt", 2, 0x01 }, + { "rt", 2, 0x02 }, + { NULL, 0, 0 } /* Terminator */ +}; + +static const struct { + const char *name; + u8 id; +} gamepad_button_names[] = { + { "none", BUTTON_NONE }, + { "a", BUTTON_A }, + { "b", BUTTON_B }, + { "x", BUTTON_X }, + { "y", BUTTON_Y }, + { "lb", BUTTON_LB }, + { "rb", BUTTON_RB }, + { "lt", BUTTON_LT }, + { "rt", BUTTON_RT }, + { "ls", BUTTON_LS }, + { "rs", BUTTON_RS }, + { "dpad_up", BUTTON_DPAD_UP }, + { "dpad_down", BUTTON_DPAD_DOWN }, + { "dpad_left", BUTTON_DPAD_LEFT }, + { "dpad_right", BUTTON_DPAD_RIGHT }, +}; + +static const struct { + const char *name; + u8 value; +} modifier_names[] = { + { "none", MOD_NONE }, + { "left_ctrl", MOD_LEFT_CTRL }, + { "left_shift", MOD_LEFT_SHIFT }, + { "left_alt", MOD_LEFT_ALT }, + { "left_win", MOD_LEFT_WIN }, + { "right_ctrl", MOD_RIGHT_CTRL }, + { "right_shift", MOD_RIGHT_SHIFT }, + { "right_alt", MOD_RIGHT_ALT }, + { "right_win", MOD_RIGHT_WIN }, +}; + +/* Keyboard key definitions */ +struct key_name_mapping { + const char *name; + u8 keycode; +}; + +static const struct key_name_mapping keyboard_keys[] = { + { "none", 0x00 }, { "a", 0x04 }, { "b", 0x05 }, + { "c", 0x06 }, { "d", 0x07 }, { "e", 0x08 }, + { "f", 0x09 }, { "g", 0x0a }, { "h", 0x0b }, + { "i", 0x0c }, { "j", 0x0d }, { "k", 0x0e }, + { "l", 0x0f }, { "m", 0x10 }, { "n", 0x11 }, + { "o", 0x12 }, { "p", 0x13 }, { "q", 0x14 }, + { "r", 0x15 }, { "s", 0x16 }, { "t", 0x17 }, + { "u", 0x18 }, { "v", 0x19 }, { "w", 0x1a }, + { "x", 0x1b }, { "y", 0x1c }, { "z", 0x1d }, + { "1", 0x1e }, { "2", 0x1f }, { "3", 0x20 }, + { "4", 0x21 }, { "5", 0x22 }, { "6", 0x23 }, + { "7", 0x24 }, { "8", 0x25 }, { "9", 0x26 }, + { "0", 0x27 }, { "enter", 0x28 }, { "esc", 0x29 }, + { "backspace", 0x2a }, { "tab", 0x2b }, { "space", 0x2c }, + { "minus", 0x2d }, { "equals", 0x2e }, { "leftbrace", 0x2f }, + { "rightbrace", 0x30 }, { "backslash", 0x31 }, { "semicolon", 0x33 }, + { "apostrophe", 0x34 }, { "grave", 0x35 }, { "comma", 0x36 }, + { "dot", 0x37 }, { "slash", 0x38 }, { "capslock", 0x39 }, + { "f1", 0x3a }, { "f2", 0x3b }, { "f3", 0x3c }, + { "f4", 0x3d }, { "f5", 0x3e }, { "f6", 0x3f }, + { "f7", 0x40 }, { "f8", 0x41 }, { "f9", 0x42 }, + { "f10", 0x43 }, { "f11", 0x44 }, { "f12", 0x45 }, + { "printscreen", 0x46 }, { "scrolllock", 0x47 }, { "pause", 0x48 }, + { "insert", 0x49 }, { "home", 0x4a }, { "pageup", 0x4b }, + { "delete", 0x4c }, { "end", 0x4d }, { "pagedown", 0x4e }, + { "right", 0x4f }, { "left", 0x50 }, { "down", 0x51 }, + { "up", 0x52 }, { "numlock", 0x53 }, { "kpslash", 0x54 }, + { "kpasterisk", 0x55 }, { "kpminus", 0x56 }, { "kpplus", 0x57 }, + { "kpenter", 0x58 }, { "kp1", 0x59 }, { "kp2", 0x5a }, + { "kp3", 0x5b }, { "kp4", 0x5c }, { "kp5", 0x5d }, + { "kp6", 0x5e }, { "kp7", 0x5f }, { "kp8", 0x60 }, + { "kp9", 0x61 }, { "kp0", 0x62 }, { "kpdot", 0x63 }, + { "application", 0x65 } +}; + +/* Mouse button definitions */ +#define MOUSE_LEFT 0x01 +#define MOUSE_RIGHT 0x02 +#define MOUSE_MIDDLE 0x04 + +static const struct { + const char *name; + u8 value; +} mouse_button_names[] = { + { "left", MOUSE_LEFT }, + { "right", MOUSE_RIGHT }, + { "middle", MOUSE_MIDDLE }, +}; + +struct zotac_device_info { + u64 device_id; + u16 vid; + u16 pid; + u8 num_led_zones; + struct { + u8 major; + u8 mid; + u8 minor; + u16 revision; + } fw_version; + struct { + u8 major; + u8 mid; + u8 minor; + } hw_version; +}; + +static u16 zotac_calc_crc(u8 *data) +{ + const int payload_end = 0x3D; + const int payload_start = 4; + const u16 crc_seed = 0; + u16 crc = crc_seed; + u32 h1, h2, h3, h4; + int i; + + for (i = payload_start; i <= payload_end; i++) { + h1 = (u32)((crc ^ data[i]) & 0xFF); + h2 = h1 & 0x0F; + h3 = (h2 << 4) ^ h1; + h4 = h3 >> 4; + + crc = (u16)((((((h3 << 1) ^ h4) << 4) ^ h2) << 3) ^ h4 ^ + (crc >> 8)); + } + + return crc; +} + +static int zotac_send_command_and_get_response_usb(struct hid_device *hdev, + u8 *send_buf, int send_len, + u8 *recv_buf, int recv_len) +{ + struct usb_device *udev = + interface_to_usbdev(to_usb_interface(hdev->dev.parent)); + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + const int polling_interval_ms = 50; + int actual_length = 0; + + unsigned int pipe_in, pipe_out; + int ret; + + if (!intf || !udev) { + hid_err(hdev, "Failed to get USB interface or device\n"); + return -ENODEV; + } + + pipe_out = usb_sndintpipe(udev, 0x05); + pipe_in = usb_rcvintpipe(udev, 0x84); + + ret = usb_interrupt_msg(udev, pipe_out, send_buf, send_len, + &actual_length, 1000); + if (ret < 0) { + hid_err(hdev, "Failed to send USB command: %d\n", ret); + return ret; + } + + memset(recv_buf, 0, recv_len); + ret = usb_interrupt_msg(udev, pipe_in, recv_buf, recv_len, + &actual_length, polling_interval_ms); + if (ret == 0 && actual_length > 0) { + return actual_length; + } + + hid_err(hdev, "Timeout waiting for USB response\n"); + return -ETIMEDOUT; +} + +static int zotac_send_command_raw(struct zotac_device *zotac, u8 cmd_code, + u8 setting, const u8 *data, size_t data_len, + u8 *response_buffer, size_t *response_len) +{ + bool is_cmd = (cmd_code == CMD_SET_RGB || cmd_code == CMD_GET_RGB); + int reply_len, result = -EIO; + + struct zotac_cfg_data *cfg; + size_t copy_len; + u8 *buffer; + u16 crc; + + if (!zotac->cfg_data || !zotac->hdev) + return -ENODEV; + + cfg = zotac->cfg_data; + + buffer = kzalloc(REPORT_SIZE, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + if (mutex_lock_interruptible(&cfg->command_mutex)) { + kfree(buffer); + return -EINTR; + } + + memset(buffer, 0, REPORT_SIZE); + buffer[HEADER_TAG_POS] = HEADER_TAG; + buffer[RESERVED_POS] = 0x00; + buffer[SEQUENCE_POS] = cfg->sequence_num; + buffer[PAYLOADSIZE_POS] = PAYLOAD_SIZE; + buffer[COMMAND_POS] = cmd_code; + + if (is_cmd) { + buffer[SETTING_POS] = setting; + + if (data && data_len > 0) { + copy_len = min_t(size_t, data_len, PAYLOAD_SIZE - 2); + memcpy(&buffer[VALUE_POS], data, copy_len); + } + } else { + if (data && data_len > 0) { + copy_len = min_t(size_t, data_len, PAYLOAD_SIZE - 1); + memcpy(&buffer[SETTING_POS], data, copy_len); + } else { + buffer[SETTING_POS] = setting; + } + } + + crc = zotac_calc_crc(buffer); + buffer[CRC_H_POS] = (crc >> 8) & 0xFF; + buffer[CRC_L_POS] = crc & 0xFF; + + reply_len = zotac_send_command_and_get_response_usb( + zotac->hdev, buffer, REPORT_SIZE, response_buffer, REPORT_SIZE); + + if (reply_len > 0) { + if (response_buffer[COMMAND_POS] == cmd_code) { + *response_len = reply_len; + cfg->sequence_num = (cfg->sequence_num + 1) & 0xFF; + result = 0; + } else { + hid_err(zotac->hdev, + "Command mismatch in response: expected 0x%02x, got 0x%02x\n", + cmd_code, response_buffer[COMMAND_POS]); + result = -EIO; + } + } else { + hid_err(zotac->hdev, + "No response received for command 0x%02x: %d\n", + cmd_code, reply_len); + result = reply_len < 0 ? reply_len : -EIO; + } + + mutex_unlock(&cfg->command_mutex); + kfree(buffer); + + return result; +} + +int zotac_send_get_command(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *req_data, size_t req_data_len, + u8 *output_data, size_t *output_len) +{ + bool is_status_offset = + (cmd_code == CMD_SET_RGB || cmd_code == CMD_SET_BUTTON_MAPPING); + size_t response_size = REPORT_SIZE, available, to_copy; + int data_offset, ret; + u8 *response_buffer; + + response_buffer = kzalloc(REPORT_SIZE, GFP_KERNEL); + if (!response_buffer) + return -ENOMEM; + + ret = zotac_send_command_raw(zotac, cmd_code, setting, req_data, + req_data_len, response_buffer, + &response_size); + if (ret < 0) { + kfree(response_buffer); + return ret; + } + + if (response_size <= COMMAND_POS || + response_buffer[COMMAND_POS] != cmd_code) { + kfree(response_buffer); + return -EIO; + } + + data_offset = is_status_offset ? 7 : 5; + + if (output_data && output_len && *output_len > 0 && + response_size > data_offset) { + available = response_size - data_offset; + to_copy = min_t(size_t, available, *output_len); + + memcpy(output_data, &response_buffer[data_offset], to_copy); + *output_len = to_copy; + } + + kfree(response_buffer); + return 0; +} + +int zotac_send_set_command(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *data, size_t data_len) +{ + bool is_status_offset = + (cmd_code == CMD_SET_RGB || cmd_code == CMD_SET_BUTTON_MAPPING); + size_t response_size = REPORT_SIZE; + + int ret, status_offset; + u8 *response_buffer; + + response_buffer = kzalloc(REPORT_SIZE, GFP_KERNEL); + if (!response_buffer) { + hid_err(zotac->hdev, + "SET_COMMAND: Failed to allocate response buffer"); + return -ENOMEM; + } + + ret = zotac_send_command_raw(zotac, cmd_code, setting, data, data_len, + response_buffer, &response_size); + + if (ret < 0) { + hid_err(zotac->hdev, + "SET_COMMAND: Command failed with error %d", ret); + kfree(response_buffer); + return ret; + } + + if (response_size <= COMMAND_POS || + response_buffer[COMMAND_POS] != cmd_code) { + hid_err(zotac->hdev, + "SET_COMMAND: Invalid response - size=%zu, cmd=0x%02x", + response_size, response_buffer[COMMAND_POS]); + kfree(response_buffer); + return -EIO; + } + + status_offset = is_status_offset ? 6 : 5; + + if (response_size > status_offset) { + if (response_buffer[status_offset] != 0) { + hid_err(zotac->hdev, + "SET_COMMAND: Command rejected by device, status=0x%02x", + response_buffer[status_offset]); + kfree(response_buffer); + return -EIO; + } + } + + kfree(response_buffer); + return 0; +} + +int zotac_send_get_byte(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *req_data, size_t req_data_len) +{ + size_t output_len = 1; + u8 output_data = 0; + int ret; + + ret = zotac_send_get_command(zotac, cmd_code, setting, req_data, + req_data_len, &output_data, &output_len); + if (ret < 0) + return ret; + + if (output_len < 1) + return -EIO; + + return output_data; +} + +static int zotac_get_device_info(struct zotac_device *zotac, + struct zotac_device_info *info) +{ + u8 data[21]; + size_t data_len = sizeof(data); + int ret; + + if (!zotac || !info) + return -EINVAL; + + ret = zotac_send_get_command(zotac, CMD_GET_DEVICE_INFO, 0, NULL, 0, + data, &data_len); + if (ret < 0) + return ret; + + if (data_len < 20) { + dev_err(&zotac->hdev->dev, + "Incomplete device info received: %zu bytes\n", + data_len); + return -EIO; + } + + info->device_id = ((u64)data[0] | ((u64)data[1] << 8) | + ((u64)data[2] << 16) | ((u64)data[3] << 24) | + ((u64)data[4] << 32) | ((u64)data[5] << 40) | + ((u64)data[6] << 48) | ((u64)data[7] << 56)); + + info->vid = data[8] | (data[9] << 8); + info->pid = data[10] | (data[11] << 8); + + info->num_led_zones = data[12]; + + info->fw_version.major = data[13]; + info->fw_version.mid = data[14]; + info->fw_version.minor = data[15]; + info->fw_version.revision = data[19] | (data[20] << 8); + + info->hw_version.major = data[16]; + info->hw_version.mid = data[17]; + info->hw_version.minor = data[18]; + + return 0; +} + +static void zotac_log_device_info(struct zotac_device *zotac) +{ + struct zotac_device_info info; + int ret; + + ret = zotac_get_device_info(zotac, &info); + if (ret < 0) { + dev_err(&zotac->hdev->dev, "Failed to get device info: %d\n", + ret); + return; + } + + dev_info(&zotac->hdev->dev, + "Device Info:\n" + " Device ID: %016llx\n" + " VID/PID: %04x:%04x\n" + " LED Zones: %u\n" + " Firmware: %u.%u.%u (revision %u)\n" + " Hardware: %u.%u.%u\n", + info.device_id, info.vid, info.pid, info.num_led_zones, + info.fw_version.major, info.fw_version.mid, + info.fw_version.minor, info.fw_version.revision, + info.hw_version.major, info.hw_version.mid, + info.hw_version.minor); +} + +static const struct button_mapping_entry *find_button_by_name(const char *name) +{ + int i; + for (i = 0; button_map[i].name != NULL; i++) { + if (strcmp(button_map[i].name, name) == 0) + return &button_map[i]; + } + return NULL; +} + +static bool is_button_in_mapping(u8 *mapping_bytes, + const struct button_mapping_entry *button) +{ + return (mapping_bytes[button->byte_index] & button->bit_mask) != 0; +} + +static void add_button_to_mapping(u8 *mapping_bytes, + const struct button_mapping_entry *button) +{ + mapping_bytes[button->byte_index] |= button->bit_mask; +} + +static int zotac_get_button_mapping(struct zotac_device *zotac, u8 button_id) +{ + if (!zotac || !zotac->cfg_data || button_id > BUTTON_MAX || + button_id == 0) + return -EINVAL; + return 0; +} + +static int zotac_set_button_mapping(struct zotac_device *zotac, u8 button_id) +{ + struct button_mapping *mapping = + &zotac->cfg_data->button_mappings[button_id]; + u8 data[BTN_MAP_RESPONSE_MIN_SIZE] = { 0 }; + u8 *gamepad_bytes = (u8 *)&mapping->target_gamepad_buttons; + + /* Source button goes at offset 0 */ + data[BTN_MAP_SOURCE_IDX] = button_id; + + /* Controller button mappings */ + data[BTN_MAP_GAMEPAD_START_IDX + GAMEPAD_DPAD_STICK_IDX] = + gamepad_bytes[GAMEPAD_DPAD_STICK_IDX]; + data[BTN_MAP_GAMEPAD_START_IDX + GAMEPAD_FACE_BUMPER_IDX] = + gamepad_bytes[GAMEPAD_FACE_BUMPER_IDX]; + data[BTN_MAP_GAMEPAD_START_IDX + GAMEPAD_TRIGGER_IDX] = + gamepad_bytes[GAMEPAD_TRIGGER_IDX]; + data[BTN_MAP_GAMEPAD_START_IDX + GAMEPAD_RESERVED_IDX] = + gamepad_bytes[GAMEPAD_RESERVED_IDX]; + + data[BTN_MAP_MODIFIER_IDX] = mapping->target_modifier_keys; + + memcpy(&data[BTN_MAP_KEYBOARD_START_IDX], mapping->target_keyboard_keys, + BTN_MAP_KEYBOARD_SIZE); + + data[BTN_MAP_MOUSE_IDX] = mapping->target_mouse_buttons; + + return zotac_send_set_command(zotac, CMD_SET_BUTTON_MAPPING, 0, data, + BTN_MAP_RESPONSE_MIN_SIZE); +} + +static void zotac_modifier_value_to_names(u8 modifiers, char *buf, + size_t buf_size) +{ + bool first = true; + int i, pos = 0; + + if (modifiers == 0) { + strscpy(buf, "none", buf_size); + return; + } + + for (i = 0; i < ARRAY_SIZE(modifier_names); i++) { + if (modifier_names[i].value != MOD_NONE && + (modifiers & modifier_names[i].value)) { + if (!first) + pos += scnprintf(buf + pos, buf_size - pos, + " "); + pos += scnprintf(buf + pos, buf_size - pos, "%s", + modifier_names[i].name); + first = false; + + if (pos >= buf_size - 1) + break; + } + } +} + +static ssize_t gamepad_show(struct device *dev, struct device_attribute *attr, + char *buf, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + bool found = false; + char *p = buf; + + u8 *mapping_bytes; + int i; + + if (zotac_get_button_mapping(&zotac, button_id) < 0) + return -EIO; + + mapping_bytes = (u8 *)&mapping->target_gamepad_buttons; + + for (i = 0; button_map[i].name != NULL; i++) { + if (is_button_in_mapping(mapping_bytes, &button_map[i])) { + p += sprintf(p, "%s ", button_map[i].name); + found = true; + } + } + + if (found) { + *(p - 1) = '\n'; + } else { + p += sprintf(p, "none\n"); + } + + return p - buf; +} + +static ssize_t gamepad_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + u8 *mapping_bytes = (u8 *)&mapping->target_gamepad_buttons; + char *buffer, *token, *cursor; + bool any_valid = false; + + /* Make a copy of the input buffer for tokenization */ + buffer = kstrndup(buf, count, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + memset(mapping_bytes, 0, 4); + + cursor = buffer; + while ((token = strsep(&cursor, " \t\n")) != NULL) { + if (*token == '\0') + continue; + + if (strcmp(token, "none") == 0) { + any_valid = true; + break; + } + + const struct button_mapping_entry *button = + find_button_by_name(token); + if (button) { + add_button_to_mapping(mapping_bytes, button); + any_valid = true; + } + } + + kfree(buffer); + + if (!any_valid) + return -EINVAL; + + return zotac_set_button_mapping(&zotac, button_id) ? -EIO : count; +} + +static ssize_t modifier_show(struct device *dev, struct device_attribute *attr, + char *buf, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + u8 modifiers; + + modifiers = mapping->target_modifier_keys; + /* Leave room for newline and null */ + zotac_modifier_value_to_names(modifiers, buf, PAGE_SIZE - 2); + strcat(buf, "\n"); + return strlen(buf); +} + +static ssize_t modifier_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + char *buffer, *token, *cursor; + u8 new_modifiers = 0; + bool any_valid = false; + + buffer = kstrndup(buf, count, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + cursor = buffer; + while ((token = strsep(&cursor, " \t\n")) != NULL) { + if (*token == '\0') + continue; + + if (strcmp(token, "none") == 0) { + any_valid = true; + new_modifiers = 0; + break; + } + + int i; + for (i = 0; i < ARRAY_SIZE(modifier_names); i++) { + if (strcmp(token, modifier_names[i].name) == 0) { + new_modifiers |= modifier_names[i].value; + any_valid = true; + break; + } + } + } + + kfree(buffer); + + if (!any_valid) + return -EINVAL; + + mapping->target_modifier_keys = new_modifiers; + + return zotac_set_button_mapping(&zotac, button_id) ? -EIO : count; +} + +static u8 find_key_code_by_name(const char *name) +{ + int i; + for (i = 0; i < ARRAY_SIZE(keyboard_keys); i++) { + if (strcmp(keyboard_keys[i].name, name) == 0) + return keyboard_keys[i].keycode; + } + return 0; /* Return "none" if not found */ +} + +static const char *find_key_name_by_code(u8 code) +{ + int i; + for (i = 0; i < ARRAY_SIZE(keyboard_keys); i++) { + if (keyboard_keys[i].keycode == code) + return keyboard_keys[i].name; + } + return "none"; +} + +static ssize_t keyboard_keys_show(struct device *dev, + struct device_attribute *attr, char *buf, + u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + bool any_key = false; + char *p = buf; + int i; + + if (zotac_get_button_mapping(&zotac, button_id) < 0) + return -EIO; + + /* Check each key code in the mapping */ + for (i = 0; i < BTN_MAP_KEYBOARD_SIZE; i++) { + u8 keycode = mapping->target_keyboard_keys[i]; + if (keycode != 0) { + p += sprintf(p, "%s ", find_key_name_by_code(keycode)); + any_key = true; + } + } + + if (any_key) { + *(p - 1) = '\n'; + } else { + p += sprintf(p, "none\n"); + } + + return p - buf; +} + +static ssize_t keyboard_keys_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + u8 new_keys[BTN_MAP_KEYBOARD_SIZE] = { 0 }; + bool any_valid = false; + int key_count = 0; + + char *buffer, *token, *cursor; + + buffer = kstrndup(buf, count, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + cursor = buffer; + while ((token = strsep(&cursor, " \t\n")) != NULL) { + if (*token == '\0') + continue; + + if (strcmp(token, "none") == 0) { + any_valid = true; + key_count = 0; /* Clear all keys */ + break; + } + + if (key_count < BTN_MAP_KEYBOARD_SIZE) { + u8 keycode = find_key_code_by_name(token); + if (keycode != 0) { + new_keys[key_count++] = keycode; + any_valid = true; + } + } + } + + kfree(buffer); + + if (!any_valid) + return -EINVAL; + + memcpy(mapping->target_keyboard_keys, new_keys, BTN_MAP_KEYBOARD_SIZE); + + return zotac_set_button_mapping(&zotac, button_id) ? -EIO : count; +} + +static ssize_t keyboard_list_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *p = buf; + int i; + + for (i = 0; i < ARRAY_SIZE(keyboard_keys); i++) { + if (keyboard_keys[i].keycode != 0x00) { + int len = sprintf(p, "%s ", keyboard_keys[i].name); + p += len; + + if (p - buf > PAGE_SIZE - 32) { + /* Add an indication that the list was truncated */ + p += sprintf(p, "..."); + break; + } + } + } + + if (p > buf) + *(p - 1) = '\n'; + else + *p++ = '\n'; + + return p - buf; +} +static DEVICE_ATTR_RO_NAMED(keyboard_list, "keyboard_list"); + +static ssize_t mouse_buttons_show(struct device *dev, + struct device_attribute *attr, char *buf, + u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + u8 mouse_buttons = mapping->target_mouse_buttons; + bool found = false; + char *p = buf; + int i; + + if (zotac_get_button_mapping(&zotac, button_id) < 0) + return -EIO; + + for (i = 0; i < ARRAY_SIZE(mouse_button_names); i++) { + if (mouse_buttons & mouse_button_names[i].value) { + p += sprintf(p, "%s ", mouse_button_names[i].name); + found = true; + } + } + + if (found) { + *(p - 1) = '\n'; + } else { + p += sprintf(p, "none\n"); + } + + return p - buf; +} + +static ssize_t mouse_buttons_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, u8 button_id) +{ + struct button_mapping *mapping = + &zotac.cfg_data->button_mappings[button_id]; + char *buffer, *token, *cursor; + u8 new_mouse_buttons = 0; + bool any_valid = false; + + buffer = kstrndup(buf, count, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + cursor = buffer; + while ((token = strsep(&cursor, " \t\n")) != NULL) { + if (*token == '\0') + continue; + + if (strcmp(token, "none") == 0) { + any_valid = true; + new_mouse_buttons = 0; + break; + } + + /* Find the button value */ + int i; + for (i = 0; i < ARRAY_SIZE(mouse_button_names); i++) { + if (strcmp(token, mouse_button_names[i].name) == 0) { + new_mouse_buttons |= + mouse_button_names[i].value; + any_valid = true; + break; + } + } + } + + kfree(buffer); + + if (!any_valid) + return -EINVAL; + + mapping->target_mouse_buttons = new_mouse_buttons; + + return zotac_set_button_mapping(&zotac, button_id) ? -EIO : count; +} + +static ssize_t mouse_list_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *p = buf; + int i; + + for (i = 0; i < ARRAY_SIZE(mouse_button_names); i++) { + p += sprintf(p, "%s ", mouse_button_names[i].name); + } + + if (p > buf) + *(p - 1) = '\n'; + else + *p++ = '\n'; + + return p - buf; +} +static DEVICE_ATTR_RO_NAMED(mouse_list, "mouse_list"); + +static ssize_t modifier_list_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *p = buf; + int i; + + for (i = 0; i < ARRAY_SIZE(modifier_names); i++) { + p += sprintf(p, "%s ", modifier_names[i].name); + } + + if (p > buf) + *(p - 1) = '\n'; + else + *p++ = '\n'; + + return p - buf; +} +static DEVICE_ATTR_RO_NAMED(modifier_list, "modifier_list"); + +/* List attributes for showing available options */ +static ssize_t gamepad_list_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + char *p = buf; + int i; + + for (i = 0; i < ARRAY_SIZE(gamepad_button_names); i++) { + if (gamepad_button_names[i].id != BUTTON_NONE) + p += sprintf(p, "%s ", gamepad_button_names[i].name); + } + + if (p > buf) + *(p - 1) = '\n'; + else + *p++ = '\n'; + + return p - buf; +} + +static ssize_t gamepad_max_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", MAX_GAMEPAD_BUTTONS); +} + +static ssize_t keyboard_max_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", MAX_KEYBOARD_KEYS); +} + +static ssize_t mouse_max_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "%d\n", MAX_MOUSE_BUTTONS); +} + +static DEVICE_ATTR_RO_NAMED(gamepad_list, "gamepad_list"); +static DEVICE_ATTR_RO_NAMED(gamepad_max, "gamepad_max"); +static DEVICE_ATTR_RO_NAMED(keyboard_max, "keyboard_max"); +static DEVICE_ATTR_RO_NAMED(mouse_max, "mouse_max"); + +#define DEFINE_BUTTON_REMAP_ATTRS(btn_name, btn_id) \ + static ssize_t btn_name##_gamepad_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return gamepad_show(dev, attr, buf, btn_id); \ + } \ + \ + static ssize_t btn_name##_gamepad_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return gamepad_store(dev, attr, buf, count, btn_id); \ + } \ + \ + static ssize_t btn_name##_modifier_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return modifier_show(dev, attr, buf, btn_id); \ + } \ + \ + static ssize_t btn_name##_modifier_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return modifier_store(dev, attr, buf, count, btn_id); \ + } \ + \ + static ssize_t btn_name##_keyboard_keys_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return keyboard_keys_show(dev, attr, buf, btn_id); \ + } \ + \ + static ssize_t btn_name##_keyboard_keys_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return keyboard_keys_store(dev, attr, buf, count, btn_id); \ + } \ + \ + static ssize_t btn_name##_mouse_buttons_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return mouse_buttons_show(dev, attr, buf, btn_id); \ + } \ + \ + static ssize_t btn_name##_mouse_buttons_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return mouse_buttons_store(dev, attr, buf, count, btn_id); \ + } \ + \ + static DEVICE_ATTR_RW_NAMED(btn_name##_gamepad, "gamepad"); \ + static DEVICE_ATTR_RW_NAMED(btn_name##_modifier, "modifier"); \ + static DEVICE_ATTR_RW_NAMED(btn_name##_keyboard_keys, "keyboard"); \ + static DEVICE_ATTR_RW_NAMED(btn_name##_mouse_buttons, "mouse"); + +/* Create all button attribute groups */ +DEFINE_BUTTON_REMAP_ATTRS(btn_a, BUTTON_A); +DEFINE_BUTTON_REMAP_ATTRS(btn_b, BUTTON_B); +DEFINE_BUTTON_REMAP_ATTRS(btn_x, BUTTON_X); +DEFINE_BUTTON_REMAP_ATTRS(btn_y, BUTTON_Y); +DEFINE_BUTTON_REMAP_ATTRS(btn_lb, BUTTON_LB); +DEFINE_BUTTON_REMAP_ATTRS(btn_rb, BUTTON_RB); +DEFINE_BUTTON_REMAP_ATTRS(btn_lt, BUTTON_LT); +DEFINE_BUTTON_REMAP_ATTRS(btn_rt, BUTTON_RT); +DEFINE_BUTTON_REMAP_ATTRS(btn_ls, BUTTON_LS); +DEFINE_BUTTON_REMAP_ATTRS(btn_rs, BUTTON_RS); +DEFINE_BUTTON_REMAP_ATTRS(dpad_up, BUTTON_DPAD_UP); +DEFINE_BUTTON_REMAP_ATTRS(dpad_down, BUTTON_DPAD_DOWN); +DEFINE_BUTTON_REMAP_ATTRS(dpad_left, BUTTON_DPAD_LEFT); +DEFINE_BUTTON_REMAP_ATTRS(dpad_right, BUTTON_DPAD_RIGHT); +DEFINE_BUTTON_REMAP_ATTRS(btn_m1, BUTTON_M1); +DEFINE_BUTTON_REMAP_ATTRS(btn_m2, BUTTON_M2); + +static int zotac_get_button_turbo(struct zotac_device *zotac) +{ + size_t data_len = 1; + u8 turbo_byte; + int ret; + + ret = zotac_send_get_command(zotac, CMD_GET_BUTTON_TURBO, 0, NULL, 0, + &turbo_byte, &data_len); + if (ret < 0) + return ret; + + if (data_len < 1) + return -EIO; + + zotac->cfg_data->button_turbo = turbo_byte; + return 0; +} + +static int zotac_set_button_turbo(struct zotac_device *zotac, u8 turbo_byte) +{ + int ret; + + ret = zotac_send_set_command(zotac, CMD_SET_BUTTON_TURBO, 0, + &turbo_byte, 1); + if (ret < 0) + return ret; + + zotac->cfg_data->button_turbo = turbo_byte; + return 0; +} + +static ssize_t button_turbo_show(struct device *dev, + struct device_attribute *attr, char *buf, + int btn_pos) +{ + u8 turbo_val; + int ret; + + ret = zotac_get_button_turbo(&zotac); + if (ret < 0) + return ret; + + turbo_val = (zotac.cfg_data->button_turbo >> btn_pos) & 0x01; + return sprintf(buf, "%d\n", turbo_val); +} + +static ssize_t button_turbo_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, int btn_pos) +{ + u8 turbo_byte; + int val, ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val != 0 && val != 1) + return -EINVAL; + + ret = zotac_get_button_turbo(&zotac); + if (ret < 0) + return ret; + + turbo_byte = zotac.cfg_data->button_turbo; + + if (val) + turbo_byte |= (1 << btn_pos); + else + turbo_byte &= ~(1 << btn_pos); + + ret = zotac_set_button_turbo(&zotac, turbo_byte); + if (ret < 0) + return ret; + + return count; +} + +#define DEFINE_BUTTON_TURBO_ATTRS(btn_name, btn_pos) \ + static ssize_t btn_##btn_name##_turbo_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return button_turbo_show(dev, attr, buf, btn_pos); \ + } \ + \ + static ssize_t btn_##btn_name##_turbo_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return button_turbo_store(dev, attr, buf, count, btn_pos); \ + } \ + static DEVICE_ATTR_RW_NAMED(btn_##btn_name##_turbo, "turbo"); + +DEFINE_BUTTON_TURBO_ATTRS(a, A_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(b, B_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(x, X_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(y, Y_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(lb, LB_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(rb, RB_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(lt, LT_BTN_POS); +DEFINE_BUTTON_TURBO_ATTRS(rt, RT_BTN_POS); + +/* Create attribute groups for buttons with/without turbo */ +#define DEFINE_BUTTON_TURBO_GROUP(btn_name, btn_id) \ + static struct attribute *btn_name##_main_attrs[] = { \ + &dev_attr_##btn_name##_turbo.attr, NULL \ + }; \ + static const struct attribute_group btn_name##_main_group = { \ + .attrs = btn_name##_main_attrs, \ + }; + +#define DEFINE_BUTTON_NO_TURBO_GROUP(btn_name, btn_id) \ + static struct attribute *btn_name##_main_attrs[] = { NULL }; \ + static const struct attribute_group btn_name##_main_group = { \ + .attrs = btn_name##_main_attrs, \ + }; + +/* Define remap subgroups */ +#define DEFINE_BUTTON_REMAP_GROUP(btn_name, btn_id) \ + static struct attribute *btn_name##_remap_attrs[] = { \ + &dev_attr_##btn_name##_gamepad.attr, \ + &dev_attr_##btn_name##_modifier.attr, \ + &dev_attr_##btn_name##_keyboard_keys.attr, \ + &dev_attr_##btn_name##_mouse_buttons.attr, \ + &dev_attr_gamepad_list.attr, \ + &dev_attr_gamepad_max.attr, \ + &dev_attr_modifier_list.attr, \ + &dev_attr_keyboard_list.attr, \ + &dev_attr_keyboard_max.attr, \ + &dev_attr_mouse_list.attr, \ + &dev_attr_mouse_max.attr, \ + NULL \ + }; \ + static const struct attribute_group btn_name##_remap_group = { \ + .name = "remap", \ + .attrs = btn_name##_remap_attrs, \ + }; + +/* Define button directory */ +#define DEFINE_BUTTON_GROUP_WITH_TURBO(btn_name, btn_id) \ + DEFINE_BUTTON_TURBO_GROUP(btn_name, btn_id) \ + DEFINE_BUTTON_REMAP_GROUP(btn_name, btn_id) + +#define DEFINE_BUTTON_DIR_WITH_TURBO(btn_name, btn_id) \ + { \ + .name = #btn_name, \ + .button_id = btn_id, \ + .has_turbo = true, \ + .kobj = NULL, \ + .main_group = \ + (struct attribute_group *)&btn_name##_main_group, \ + .remap_group = \ + (struct attribute_group *)&btn_name##_remap_group, \ + } + +#define DEFINE_BUTTON_GROUP_NO_TURBO(btn_name, btn_id) \ + DEFINE_BUTTON_NO_TURBO_GROUP(btn_name, btn_id) \ + DEFINE_BUTTON_REMAP_GROUP(btn_name, btn_id) + +#define DEFINE_BUTTON_DIR_NO_TURBO(btn_name, btn_id) \ + { \ + .name = #btn_name, \ + .button_id = btn_id, \ + .has_turbo = false, \ + .kobj = NULL, \ + .main_group = \ + (struct attribute_group *)&btn_name##_main_group, \ + .remap_group = \ + (struct attribute_group *)&btn_name##_remap_group, \ + } + +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_a, BUTTON_A); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_b, BUTTON_B); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_x, BUTTON_X); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_y, BUTTON_Y); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_lb, BUTTON_LB); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_rb, BUTTON_RB); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_lt, BUTTON_LT); +DEFINE_BUTTON_GROUP_WITH_TURBO(btn_rt, BUTTON_RT); +DEFINE_BUTTON_GROUP_NO_TURBO(btn_ls, BUTTON_LS); +DEFINE_BUTTON_GROUP_NO_TURBO(btn_rs, BUTTON_RS); +DEFINE_BUTTON_GROUP_NO_TURBO(dpad_up, BUTTON_DPAD_UP); +DEFINE_BUTTON_GROUP_NO_TURBO(dpad_down, BUTTON_DPAD_DOWN); +DEFINE_BUTTON_GROUP_NO_TURBO(dpad_left, BUTTON_DPAD_LEFT); +DEFINE_BUTTON_GROUP_NO_TURBO(dpad_right, BUTTON_DPAD_RIGHT); +DEFINE_BUTTON_GROUP_NO_TURBO(btn_m1, BUTTON_M1); +DEFINE_BUTTON_GROUP_NO_TURBO(btn_m2, BUTTON_M2); + +/* Define all button directories */ +static struct button_directory button_dirs[] = { + DEFINE_BUTTON_DIR_WITH_TURBO(btn_a, BUTTON_A), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_b, BUTTON_B), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_x, BUTTON_X), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_y, BUTTON_Y), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_lb, BUTTON_LB), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_rb, BUTTON_RB), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_lt, BUTTON_LT), + DEFINE_BUTTON_DIR_WITH_TURBO(btn_rt, BUTTON_RT), + DEFINE_BUTTON_DIR_NO_TURBO(btn_ls, BUTTON_LS), + DEFINE_BUTTON_DIR_NO_TURBO(btn_rs, BUTTON_RS), + DEFINE_BUTTON_DIR_NO_TURBO(dpad_up, BUTTON_DPAD_UP), + DEFINE_BUTTON_DIR_NO_TURBO(dpad_down, BUTTON_DPAD_DOWN), + DEFINE_BUTTON_DIR_NO_TURBO(dpad_left, BUTTON_DPAD_LEFT), + DEFINE_BUTTON_DIR_NO_TURBO(dpad_right, BUTTON_DPAD_RIGHT), + DEFINE_BUTTON_DIR_NO_TURBO(btn_m1, BUTTON_M1), + DEFINE_BUTTON_DIR_NO_TURBO(btn_m2, BUTTON_M2), + { NULL, 0, false, NULL, NULL, NULL } /* Terminator */ +}; + +static int zotac_get_stick_sensitivity(struct zotac_device *zotac, + int stick_num, u8 *output_values) +{ + u8 request_data = stick_num; + u8 temp_values[STICK_SENSITIVITY_SIZE]; + size_t output_len = STICK_SENSITIVITY_SIZE; + int i, ret; + + ret = zotac_send_get_command(zotac, CMD_GET_STICK_SENSITIVITY, 0, + &request_data, 1, temp_values, + &output_len); + + if (ret == 0 && output_len == STICK_SENSITIVITY_SIZE) { + /* Scale from percentage (0-100) to device values (0-255) */ + for (i = 0; i < STICK_SENSITIVITY_SIZE; i++) { + output_values[i] = temp_values[i] * 255 / 100; + } + } + + return ret; +} + +static struct stick_sensitivity * +get_sensitivity_for_stick(struct zotac_device *zotac, int stick_num) +{ + if (stick_num == STICK_LEFT) + return &zotac->cfg_data->left_stick_sensitivity; + else + return &zotac->cfg_data->right_stick_sensitivity; +} + +static ssize_t curve_response_show(struct device *dev, + struct device_attribute *attr, char *buf, + int stick_num, int point_index) +{ + struct stick_sensitivity *sensitivity = + get_sensitivity_for_stick(&zotac, stick_num); + int base_idx = point_index * 2; + + // Dev input is 0-255, but it outputs 0-100. So we store as 0-255 + int x_pct = sensitivity->values[base_idx] * 100 / 255; + int y_pct = sensitivity->values[base_idx + 1] * 100 / 255; + + return sprintf(buf, "%d %d\n", x_pct, y_pct); +} + +static ssize_t curve_response_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + int stick_num, int point_index) +{ + struct stick_sensitivity *sensitivity = + get_sensitivity_for_stick(&zotac, stick_num); + u8 data[STICK_SENSITIVITY_SIZE + 1]; // +1 for stick ID + int base_idx = point_index * 2; + + int x_pct, y_pct; + int ret; + + ret = sscanf(buf, "%d %d", &x_pct, &y_pct); + if (ret != 2) + return -EINVAL; + + x_pct = clamp_val(x_pct, 0, 100); + y_pct = clamp_val(y_pct, 0, 100); + + sensitivity->values[base_idx] = x_pct * 255 / 100; + sensitivity->values[base_idx + 1] = y_pct * 255 / 100; + + data[STICK_SENSITIVITY_NUM_IDX] = stick_num; + + memcpy(&data[STICK_SENSITIVITY_DATA_IDX], sensitivity->values, + STICK_SENSITIVITY_SIZE); + + ret = zotac_send_set_command(&zotac, CMD_SET_STICK_SENSITIVITY, 0, data, + sizeof(data)); + if (ret < 0) + return ret; + + return count; +} + +#define DEFINE_CURVE_RESPONSE_ATTRS(stick_name, stick_num, point_num) \ + static ssize_t stick_##stick_name##_curve_response_##point_num##_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return curve_response_show(dev, attr, buf, stick_num, \ + point_num - 1); \ + } \ + \ + static ssize_t stick_##stick_name##_curve_response_##point_num##_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return curve_response_store(dev, attr, buf, count, stick_num, \ + point_num - 1); \ + } \ + static DEVICE_ATTR_RW_NAMED( \ + stick_##stick_name##_curve_response_##point_num, \ + "curve_response_pct_" #point_num); + +DEFINE_CURVE_RESPONSE_ATTRS(xy_left, STICK_LEFT, 1) +DEFINE_CURVE_RESPONSE_ATTRS(xy_left, STICK_LEFT, 2) +DEFINE_CURVE_RESPONSE_ATTRS(xy_left, STICK_LEFT, 3) +DEFINE_CURVE_RESPONSE_ATTRS(xy_left, STICK_LEFT, 4) + +DEFINE_CURVE_RESPONSE_ATTRS(xy_right, STICK_RIGHT, 1) +DEFINE_CURVE_RESPONSE_ATTRS(xy_right, STICK_RIGHT, 2) +DEFINE_CURVE_RESPONSE_ATTRS(xy_right, STICK_RIGHT, 3) +DEFINE_CURVE_RESPONSE_ATTRS(xy_right, STICK_RIGHT, 4) + +static ssize_t axis_xyz_deadzone_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, "inner outer\n"); +} +static DEVICE_ATTR_RO_NAMED(axis_xyz_deadzone_index, "deadzone_index"); + +static ssize_t axis_xyz_deadzone_show(struct device *dev, + struct device_attribute *attr, char *buf, + struct deadzone *dz) +{ + return sprintf(buf, "%d %d\n", dz->inner, dz->outer); +} + +static int zotac_apply_deadzones(struct zotac_device *zotac, + struct deadzone *left_dz, + struct deadzone *right_dz, u8 cmd_code) +{ + u8 data[DZ_RESPONSE_SIZE]; + int ret; + + data[DZ_LEFT_INNER_IDX] = left_dz->inner; + data[DZ_LEFT_OUTER_IDX] = left_dz->outer; + data[DZ_RIGHT_INNER_IDX] = right_dz->inner; + data[DZ_RIGHT_OUTER_IDX] = right_dz->outer; + + ret = zotac_send_set_command(zotac, cmd_code, 0, data, sizeof(data)); + + return ret; +} + +static ssize_t axis_xyz_deadzone_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count, + struct deadzone *dz) +{ + struct zotac_cfg_data *cfg = zotac.cfg_data; + int inner, outer; + u8 cmd_code; + int ret; + + ret = sscanf(buf, "%d %d", &inner, &outer); + if (ret != 2) + return -EINVAL; + + if (inner < 0 || inner > 100 || outer < 0 || outer > 100) + return -EINVAL; + + dz->inner = inner; + dz->outer = outer; + + /* Determine which command to use based on which deadzone is being modified */ + if (dz == &cfg->ls_dz || dz == &cfg->rs_dz) { + cmd_code = CMD_SET_STICK_DEADZONES; + ret = zotac_apply_deadzones(&zotac, &cfg->ls_dz, &cfg->rs_dz, + cmd_code); + } else { + cmd_code = CMD_SET_TRIGGER_DEADZONES; + ret = zotac_apply_deadzones(&zotac, &cfg->lt_dz, &cfg->rt_dz, + cmd_code); + } + + if (ret < 0) + return ret; + + return count; +} + +#define DEFINE_DEADZONE_HANDLERS(axis_name, dz_field) \ + static ssize_t axis_##axis_name##_deadzone_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return axis_xyz_deadzone_show(dev, attr, buf, \ + &zotac.cfg_data->dz_field); \ + } \ + \ + static ssize_t axis_##axis_name##_deadzone_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return axis_xyz_deadzone_store(dev, attr, buf, count, \ + &zotac.cfg_data->dz_field); \ + } \ + static DEVICE_ATTR_RW_NAMED(axis_##axis_name##_deadzone, "deadzone"); + +#define DEFINE_XY_AXIS_ATTR_GROUP(axis_name) \ + static struct attribute *axis_##axis_name##_attrs[] = { \ + &dev_attr_axis_##axis_name##_deadzone.attr, \ + &dev_attr_stick_##axis_name##_curve_response_1.attr, \ + &dev_attr_stick_##axis_name##_curve_response_2.attr, \ + &dev_attr_stick_##axis_name##_curve_response_3.attr, \ + &dev_attr_stick_##axis_name##_curve_response_4.attr, \ + &dev_attr_axis_xyz_deadzone_index.attr, \ + NULL \ + }; \ + \ + static const struct attribute_group axis_##axis_name##_attr_group = { \ + .name = "axis_" #axis_name, \ + .attrs = axis_##axis_name##_attrs, \ + }; + +#define DEFINE_Z_AXIS_ATTR_GROUP(axis_name) \ + static struct attribute *axis_##axis_name##_attrs[] = { \ + &dev_attr_axis_##axis_name##_deadzone.attr, \ + &dev_attr_axis_xyz_deadzone_index.attr, NULL \ + }; \ + \ + static const struct attribute_group axis_##axis_name##_attr_group = { \ + .name = "axis_" #axis_name, \ + .attrs = axis_##axis_name##_attrs, \ + }; + +DEFINE_DEADZONE_HANDLERS(xy_left, ls_dz); +DEFINE_DEADZONE_HANDLERS(xy_right, rs_dz); +DEFINE_DEADZONE_HANDLERS(z_left, lt_dz); +DEFINE_DEADZONE_HANDLERS(z_right, rt_dz); + +DEFINE_XY_AXIS_ATTR_GROUP(xy_left); +DEFINE_XY_AXIS_ATTR_GROUP(xy_right); +DEFINE_Z_AXIS_ATTR_GROUP(z_left); +DEFINE_Z_AXIS_ATTR_GROUP(z_right); + +static ssize_t vibration_intensity_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sprintf(buf, + "trigger_left trigger_right rumble_left rumble_right\n"); +} +static DEVICE_ATTR_RO_NAMED(vibration_intensity_index, + "vibration_intensity_index"); + +static ssize_t vibration_intensity_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[VIB_RESPONSE_SIZE]; + size_t data_len = sizeof(data); + int ret; + + ret = zotac_send_get_command(&zotac, CMD_GET_VIBRATION_STRENGTH, 0, NULL, + 0, data, &data_len); + if (ret < 0) + return ret; + + if (data_len < VIB_RESPONSE_SIZE) { + dev_err(&zotac.hdev->dev, + "Incomplete vibration data received: %zu bytes\n", + data_len); + return -EIO; + } + + return sprintf(buf, "%d %d %d %d\n", data[VIB_LEFT_TRIGGER_IDX], + data[VIB_RIGHT_TRIGGER_IDX], data[VIB_LEFT_RUMBLE_IDX], + data[VIB_RIGHT_RUMBLE_IDX]); +} + +static ssize_t vibration_intensity_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 data[VIB_RESPONSE_SIZE]; + int lt, rt, lr, rr; + int ret; + + ret = sscanf(buf, "%d %d %d %d", <, &rt, &lr, &rr); + if (ret != 4) + return -EINVAL; + + data[VIB_LEFT_TRIGGER_IDX] = clamp_val(lt, 0, 100); + data[VIB_RIGHT_TRIGGER_IDX] = clamp_val(rt, 0, 100); + data[VIB_LEFT_RUMBLE_IDX] = clamp_val(lr, 0, 100); + data[VIB_RIGHT_RUMBLE_IDX] = clamp_val(rr, 0, 100); + + ret = zotac_send_set_command(&zotac, CMD_SET_VIBRATION_STRENGTH, 0, data, + sizeof(data)); + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_RW(vibration_intensity); + +static ssize_t mouse_speed_max_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", MOUSE_SPEED_MAX); +} +static DEVICE_ATTR_RO_NAMED(mouse_speed_max, "mouse_speed_max"); + +static ssize_t mouse_speed_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int speed; + + speed = zotac_send_get_byte(&zotac, CMD_GET_MOUSE_SPEED, 0, NULL, 0); + if (speed < 0) + return speed; + + return sprintf(buf, "%d\n", speed); +} + +static ssize_t mouse_speed_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int speed_val; + u8 speed; + int ret; + + ret = kstrtoint(buf, 10, &speed_val); + if (ret) + return ret; + + if (speed_val < MOUSE_SPEED_MIN || speed_val > MOUSE_SPEED_MAX) + return -EINVAL; + + speed = (u8)speed_val; + ret = zotac_send_set_command(&zotac, CMD_SET_MOUSE_SPEED, 0, &speed, 1); + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_RW(mouse_speed); + +static ssize_t motor_test_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + u8 data[MOTOR_TEST_SIZE] = { 0 }; + + int left_trigger, right_trigger, left_rumble, right_rumble; + int ret; + + ret = sscanf(buf, "%d %d %d %d", &left_trigger, &right_trigger, + &left_rumble, &right_rumble); + if (ret != 4) + return -EINVAL; + + left_trigger = clamp_val(left_trigger, 0, 100); + right_trigger = clamp_val(right_trigger, 0, 100); + left_rumble = clamp_val(left_rumble, 0, 100); + right_rumble = clamp_val(right_rumble, 0, 100); + + data[0] = (u8)left_trigger; /* Left Trigger Motor */ + data[1] = (u8)right_trigger; /* Right Trigger Motor */ + data[2] = (u8)left_rumble; /* Left Rumble Motor */ + data[3] = (u8)right_rumble; /* Right Rumble Motor */ + + ret = zotac_send_set_command(&zotac, CMD_MOTOR_TEST, 0, data, + MOTOR_TEST_SIZE); + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_WO(motor_test); + +static ssize_t motor_test_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, + "left_trigger right_trigger left_rumble right_rumble\n"); +} +static DEVICE_ATTR_RO_NAMED(motor_test_index, "motor_test_index"); + +static ssize_t profile_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int profile; + + profile = zotac_send_get_byte(&zotac, CMD_GET_PROFILE, 0, NULL, 0); + if (profile < 0) + return profile; + + return sprintf(buf, "%d\n", profile); +} + +static ssize_t profile_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int profile_id, ret; + u8 profile; + + ret = kstrtoint(buf, 10, &profile_id); + if (ret) + return ret; + + if (profile_id > PROFILE_SECONDARY) + return -EINVAL; + + profile = (u8)profile_id; + ret = zotac_send_set_command(&zotac, CMD_SET_PROFILE, 0, &profile, 1); + if (ret) + return ret; + + ret = zotac_cfg_refresh(&zotac); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_RW_NAMED(profile, "current"); + +static ssize_t profile_count_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + return sprintf(buf, "%d\n", + zotac_send_get_byte(&zotac, CMD_GET_PROFILE_NUM, 0, NULL, + 0)); +} +static DEVICE_ATTR_RO_NAMED(profile_count, "count"); + +/* The device resets and reconnects */ +static ssize_t restore_profile_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int val, ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val != 1) + return -EINVAL; + + dev_warn(dev, "Restoring profile, the device will reset and reconnect"); + ret = zotac_send_set_command(&zotac, CMD_RESTORE_PROFILE, 0, NULL, 0); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO_NAMED(restore_profile, "restore"); + +static struct attribute *zotac_profile_attrs[] = { + &dev_attr_profile.attr, &dev_attr_profile_count.attr, + &dev_attr_restore_profile.attr, NULL +}; + +static const struct attribute_group zotac_profile_attr_group = { + .name = "profile", + .attrs = zotac_profile_attrs, +}; + +static ssize_t save_config_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int val, ret; + + ret = kstrtoint(buf, 10, &val); + if (ret) + return ret; + + if (val != 1) + return -EINVAL; + + ret = zotac_send_set_command(&zotac, CMD_SAVE_CONFIG, 0, NULL, 0); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(save_config); + +static ssize_t qam_mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!zotac.gamepad) + return -ENODEV; + + return sprintf(buf, "%i\n", zotac.gamepad->qam_mode); +} + +static ssize_t qam_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + u8 value; + int ret; + + if (!zotac.gamepad) + return -ENODEV; + + ret = kstrtou8(buf, 10, &value); + if (ret) + return ret; + + if (value >= QAM_MODE_LENGTH) + return -EINVAL; + + zotac.gamepad->qam_mode = value; + + return count; +} +DEVICE_ATTR_RW(qam_mode); + +static struct attribute *zotac_root_attrs[] = { + &dev_attr_save_config.attr, + &dev_attr_qam_mode.attr, + &dev_attr_vibration_intensity.attr, + &dev_attr_vibration_intensity_index.attr, + &dev_attr_mouse_speed.attr, + &dev_attr_mouse_speed_max.attr, + &dev_attr_motor_test.attr, + &dev_attr_motor_test_index.attr, + NULL +}; + +static const struct attribute_group zotac_root_attr_group = { + .attrs = zotac_root_attrs, +}; + +static const struct attribute_group *zotac_top_level_attr_groups[] = { + &zotac_profile_attr_group, + &zotac_root_attr_group, + &axis_xy_left_attr_group, + &axis_xy_right_attr_group, + &axis_z_left_attr_group, + &axis_z_right_attr_group, + NULL +}; + +int zotac_register_sysfs(struct zotac_device *zotac) +{ + struct device *dev; + int ret, i; + + if (!zotac || !zotac->hdev) + return -ENODEV; + + dev = &zotac->hdev->dev; + + ret = sysfs_create_groups(&dev->kobj, zotac_top_level_attr_groups); + if (ret) { + dev_err(dev, "Failed to create top-level sysfs groups: %d\n", + ret); + return ret; + } + + for (i = 0; button_dirs[i].name != NULL; i++) { + struct button_directory *btn_dir = &button_dirs[i]; + + /* Create the button directory kobject */ + btn_dir->kobj = + kobject_create_and_add(btn_dir->name, &dev->kobj); + if (!btn_dir->kobj) { + dev_err(dev, "Failed to create kobject for %s\n", + btn_dir->name); + ret = -ENOMEM; + goto cleanup; + } + + /* Add the main attributes to the button directory */ + ret = sysfs_create_group(btn_dir->kobj, btn_dir->main_group); + if (ret) { + dev_err(dev, "Failed to create main group for %s: %d\n", + btn_dir->name, ret); + goto cleanup; + } + + /* Add the remap subgroup to the button directory */ + ret = sysfs_create_group(btn_dir->kobj, btn_dir->remap_group); + if (ret) { + dev_err(dev, + "Failed to create remap group for %s: %d\n", + btn_dir->name, ret); + goto cleanup; + } + } + + return 0; + +cleanup: + /* Clean up on error */ + for (i = 0; button_dirs[i].name != NULL; i++) { + if (button_dirs[i].kobj) { + kobject_put(button_dirs[i].kobj); + button_dirs[i].kobj = NULL; + } + } + sysfs_remove_groups(&dev->kobj, zotac_top_level_attr_groups); + return ret; +} + +void zotac_unregister_sysfs(struct zotac_device *zotac) +{ + int i; + struct device *dev; + + if (!zotac || !zotac->hdev) { + pr_err("Invalid zotac device in unregister_sysfs\n"); + return; + } + + dev = &zotac->hdev->dev; + + /* Remove button directories and their attributes */ + for (i = 0; button_dirs[i].name != NULL; i++) { + if (button_dirs[i].kobj) { + kobject_put(button_dirs[i].kobj); + button_dirs[i].kobj = NULL; + } + } + + sysfs_remove_groups(&dev->kobj, zotac_top_level_attr_groups); +} + +/** + * zotac_cfg_refresh - Refresh all configuration data from the device + * @zotac: The zotac device to refresh + * + * This function queries the device for all current configuration and + * updates the driver's cached values. It should be called during + * initialization and after profile changes or restores. + */ +int zotac_cfg_refresh(struct zotac_device *zotac) +{ + struct zotac_cfg_data *cfg; + u8 data[DZ_RESPONSE_SIZE]; + size_t data_len = sizeof(data); + int ret, i; + + if (!zotac->cfg_data) + return -EINVAL; + + cfg = zotac->cfg_data; + + ret = zotac_send_get_command(zotac, CMD_GET_STICK_DEADZONES, 0, NULL, 0, + data, &data_len); + if (ret == 0 && data_len >= DZ_RESPONSE_SIZE) { + cfg->ls_dz.inner = data[DZ_LEFT_INNER_IDX]; + cfg->ls_dz.outer = data[DZ_LEFT_OUTER_IDX]; + cfg->rs_dz.inner = data[DZ_RIGHT_INNER_IDX]; + cfg->rs_dz.outer = data[DZ_RIGHT_OUTER_IDX]; + } else { + dev_info( + &zotac->hdev->dev, + "Could not retrieve stick deadzone settings, using defaults\n"); + cfg->ls_dz.inner = 0; + cfg->ls_dz.outer = 100; + cfg->rs_dz.inner = 0; + cfg->rs_dz.outer = 100; + } + + ret = zotac_send_get_command(zotac, CMD_GET_TRIGGER_DEADZONES, 0, NULL, + 0, data, &data_len); + if (ret == 0 && data_len >= DZ_RESPONSE_SIZE) { + cfg->lt_dz.inner = data[DZ_LEFT_INNER_IDX]; + cfg->lt_dz.outer = data[DZ_LEFT_OUTER_IDX]; + cfg->rt_dz.inner = data[DZ_RIGHT_INNER_IDX]; + cfg->rt_dz.outer = data[DZ_RIGHT_OUTER_IDX]; + } else { + dev_info( + &zotac->hdev->dev, + "Could not retrieve trigger deadzone settings, using defaults\n"); + cfg->lt_dz.inner = 0; + cfg->lt_dz.outer = 100; + cfg->rt_dz.inner = 0; + cfg->rt_dz.outer = 100; + } + + ret = zotac_get_stick_sensitivity(zotac, STICK_LEFT, + cfg->left_stick_sensitivity.values); + if (ret < 0) { + dev_info( + &zotac->hdev->dev, + "Could not retrieve left stick sensitivity, using defaults\n"); + /* Initialize with linear response: 25%, 50%, 75%, 100% */ + cfg->left_stick_sensitivity.values[0] = 64; /* X1 = 25% */ + cfg->left_stick_sensitivity.values[1] = 64; /* Y1 = 25% */ + cfg->left_stick_sensitivity.values[2] = 128; /* X2 = 50% */ + cfg->left_stick_sensitivity.values[3] = 128; /* Y2 = 50% */ + cfg->left_stick_sensitivity.values[4] = 192; /* X3 = 75% */ + cfg->left_stick_sensitivity.values[5] = 192; /* Y3 = 75% */ + cfg->left_stick_sensitivity.values[6] = 255; /* X4 = 100% */ + cfg->left_stick_sensitivity.values[7] = 255; /* Y4 = 100% */ + } + + ret = zotac_get_stick_sensitivity(zotac, STICK_RIGHT, + cfg->right_stick_sensitivity.values); + if (ret < 0) { + dev_info( + &zotac->hdev->dev, + "Could not retrieve right stick sensitivity, using defaults\n"); + /* Copy the left stick values which may be initialized or linear defaults */ + memcpy(cfg->right_stick_sensitivity.values, + cfg->left_stick_sensitivity.values, + sizeof(cfg->right_stick_sensitivity.values)); + } + + ret = zotac_get_button_turbo(zotac); + if (ret < 0) { + dev_info( + &zotac->hdev->dev, + "Could not retrieve button turbo settings, using defaults\n"); + cfg->button_turbo = 0; /* Default: no turbo buttons */ + } + + for (i = 1; i <= BUTTON_MAX; i++) { + u8 request_data = i; + u8 response_data[BTN_MAP_RESPONSE_MIN_SIZE]; + size_t response_len = sizeof(response_data); + + /* Set default values first */ + cfg->button_mappings[i].target_gamepad_buttons = 0; + cfg->button_mappings[i].target_modifier_keys = 0; + memset(cfg->button_mappings[i].target_keyboard_keys, 0, + MAX_KEYBOARD_KEYS); + cfg->button_mappings[i].target_mouse_buttons = 0; + + ret = zotac_send_get_command(zotac, CMD_GET_BUTTON_MAPPING, 0, + &request_data, 1, response_data, + &response_len); + if (ret == 0 && response_len >= BTN_MAP_RESPONSE_MIN_SIZE) { + memcpy(&cfg->button_mappings[i].target_gamepad_buttons, + &response_data[BTN_MAP_GAMEPAD_START_IDX], + BTN_MAP_GAMEPAD_SIZE); + + cfg->button_mappings[i].target_modifier_keys = + response_data[BTN_MAP_MODIFIER_IDX]; + + memcpy(cfg->button_mappings[i].target_keyboard_keys, + &response_data[BTN_MAP_KEYBOARD_START_IDX], + BTN_MAP_KEYBOARD_SIZE); + + cfg->button_mappings[i].target_mouse_buttons = + response_data[BTN_MAP_MOUSE_IDX]; + } else { + dev_info( + &zotac->hdev->dev, + "Could not retrieve button %d mapping, using defaults\n", + i); + } + } + + return 0; +} + +/** + * zotac_cfg_setup - Allocate and initialize config data structure + * @zotac: The zotac device to set up + * + * This function allocates the config data structure and initializes + * the mutex and sequence number. It should be called once during + * driver initialization. + */ +static int zotac_cfg_setup(struct zotac_device *zotac) +{ + struct zotac_cfg_data *cfg; + + if (!zotac) + return -EINVAL; + + cfg = kzalloc(sizeof(*cfg), GFP_KERNEL); + if (!cfg) + return -ENOMEM; + + mutex_init(&cfg->command_mutex); + cfg->sequence_num = 0; + zotac->cfg_data = cfg; + + zotac_log_device_info(zotac); + + return 0; +} + +/** + * zotac_cfg_init - Initialize the device configuration system + * @zotac: The zotac device to initialize + * + * This function sets up the configuration system and loads the initial + * configuration from the device. + */ +int zotac_cfg_init(struct zotac_device *zotac) +{ + int ret; + + ret = zotac_cfg_setup(zotac); + if (ret < 0) + return ret; + + ret = zotac_cfg_refresh(zotac); + if (ret < 0) { + /* If refresh fails, still keep the structure but log an error */ + dev_err(&zotac->hdev->dev, + "Failed to load initial configuration: %d\n", ret); + } + + return 0; +} + +void zotac_cfg_cleanup(struct zotac_device *zotac) +{ + if (!zotac || !zotac->cfg_data) + return; + + kfree(zotac->cfg_data); + zotac->cfg_data = NULL; +} diff --git a/drivers/hid/zotac-zone-hid/zotac-zone-hid-core.c b/drivers/hid/zotac-zone-hid/zotac-zone-hid-core.c new file mode 100644 index 000000000000..6f80ea2f9aad --- /dev/null +++ b/drivers/hid/zotac-zone-hid/zotac-zone-hid-core.c @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for ZOTAC Gaming Zone Controller - RGB LED control + * + * Copyright (c) 2025 Luke D. Jones + */ + +#include +#include +#include +#include +#include + +#include "zotac-zone.h" + +#define ZOTAC_VENDOR_ID 0x1ee9 +#define ZOTAC_ALT_VENDOR_ID 0x1e19 +#define ZOTAC_PRODUCT_ID 0x1590 + +#define ZOTAC_DIAL_INTERFACE 1 +#define ZOTAC_REPORT_INTERFACE 2 +#define ZOTAC_COMMAND_INTERFACE 3 + +#define ZOTAC_DIAL_REPORT_ID 0x03 +#define ZOTAC_KBD_REPORT_ID 0x02 +#define ZOTAC_MOUSE_REPORT_ID 0x04 +#define ZOTAC_STATUS_REPORT_ID 0x07 +#define ZOTAC_STATUS2_REPORT_ID 0x08 +#define ZOTAC_STATUS3_REPORT_ID 0x09 + +#define ZOTAC_RIGHT_DIAL_CW_BIT 0 +#define ZOTAC_RIGHT_DIAL_CCW_BIT 1 +#define ZOTAC_LEFT_DIAL_CW_BIT 3 +#define ZOTAC_LEFT_DIAL_CCW_BIT 4 + +#define HID_USAGE_F16 0x6B +#define HID_USAGE_F17 0x6C +#define HID_USAGE_F18 0x6D +#define HID_USAGE_F19 0x6E +#define HID_USAGE_F20 0x6F + +struct zotac_device zotac; + +typedef void (*zotac_report_handler)(struct zotac_device *zotac, u8 *data, + int size); + +struct zotac_report_handler { + u8 report_id; + zotac_report_handler handler; + const char *name; +}; + +/** + * zotac_get_usb_interface - Get the USB interface from a HID device + * @hdev: The HID device + * + * Returns the USB interface if the device is a USB device, NULL otherwise + */ +struct usb_interface *zotac_get_usb_interface(struct hid_device *hdev) +{ + struct usb_interface *intf = NULL; + + if (hid_is_usb(hdev)) + intf = to_usb_interface(hdev->dev.parent); + + return intf; +} + +static int get_interface_num(struct hid_device *hdev) +{ + struct usb_interface *intf = zotac_get_usb_interface(hdev); + + if (intf && intf->cur_altsetting) + return intf->cur_altsetting->desc.bInterfaceNumber; + + return -1; +} + +static void process_dial_wheel_report(struct zotac_device *zotac, u8 *data, + int size) +{ + u8 value; + + if (size < 4 || !zotac->wheel_input) + return; + + value = data[3]; + if (value == 0) + return; + + if (value & BIT(ZOTAC_RIGHT_DIAL_CW_BIT)) + input_report_rel(zotac->wheel_input, REL_WHEEL, 1); + if (value & BIT(ZOTAC_RIGHT_DIAL_CCW_BIT)) + input_report_rel(zotac->wheel_input, REL_WHEEL, -1); + + if (value & BIT(ZOTAC_LEFT_DIAL_CW_BIT)) + input_report_rel(zotac->wheel_input, REL_HWHEEL, 1); + if (value & BIT(ZOTAC_LEFT_DIAL_CCW_BIT)) + input_report_rel(zotac->wheel_input, REL_HWHEEL, -1); + + input_sync(zotac->wheel_input); +} + +static int process_keyboard_report(struct zotac_device *zotac, u8 *data, + int size) +{ + u32 pattern; + enum qam_mode qam_mode; + + if (zotac->gamepad) + qam_mode = zotac->gamepad->qam_mode; + + if (size < 5) + return 0; + + pattern = (data[1] << 16) | (data[2] << 8) | data[3]; + + if (pattern == 0x09006c || pattern == 0x09006d || pattern == 0x080007 || pattern == 0x050063) { + switch (data[3]) { + case 0x63: + switch (qam_mode) { + case QAM_MODE_KEYBOARD: + data[3] = HID_USAGE_F19; + break; + case QAM_MODE_CUSTOM: + zotac_gamepad_send_button(zotac, (int[]){ BTN_TRIGGER_HAPPY6 }, 1); + break; + default: + zotac_gamepad_send_button(zotac, (int[]){ BTN_MODE, BTN_X }, 2); + break; + } + break; + case 0x6C: + switch (qam_mode) { + case QAM_MODE_KEYBOARD: + data[3] = HID_USAGE_F16; + break; + default: + zotac_gamepad_send_button(zotac, (int[]){ BTN_MODE }, 1); + break; + } + break; + case 0x6D: + switch (qam_mode) { + case QAM_MODE_KEYBOARD: + data[3] = HID_USAGE_F17; + break; + default: + zotac_gamepad_send_button(zotac, (int[]){ BTN_MODE, BTN_A }, 2); + break; + } + break; + case 0x07: + switch (qam_mode) { + case QAM_MODE_KEYBOARD: + data[3] = HID_USAGE_F18; + break; + case QAM_MODE_CUSTOM: + zotac_gamepad_send_button(zotac, (int[]){ BTN_TRIGGER_HAPPY5 }, 1); + break; + default: + zotac_gamepad_send_button(zotac, (int[]){ BTN_MODE, BTN_B }, 2); + break; + } + break; + } + if (qam_mode) { + memset(data, 0, size); + return 1; + } else { + data[1] = 0; + return 0; + } + } + return 0; +} + +static void process_mouse_report(struct zotac_device *zotac, u8 *data, int size) +{ + s8 x_movement = 0, y_movement = 0, wheel_movement = 0; + int bit, i; + + if (size < 5 || !zotac->mouse_input) + return; + + x_movement = (s8)data[2]; + y_movement = (s8)data[3]; + + if (size >= 5) + wheel_movement = (s8)data[4]; + + for (i = 0; i < 8; i++) { + bit = 1 << i; + input_report_key(zotac->mouse_input, BTN_LEFT + i, + (data[1] & bit) ? 1 : 0); + } + + input_report_rel(zotac->mouse_input, REL_X, x_movement); + input_report_rel(zotac->mouse_input, REL_Y, y_movement); + + if (wheel_movement) + input_report_rel(zotac->mouse_input, REL_WHEEL, wheel_movement); + + input_sync(zotac->mouse_input); +} + +static const struct zotac_report_handler dial_interface_handlers[] = { + { .report_id = ZOTAC_DIAL_REPORT_ID, + .handler = process_dial_wheel_report, + .name = "dial wheel" }, + { .report_id = ZOTAC_MOUSE_REPORT_ID, + .handler = process_mouse_report, + .name = "mouse" }, + { 0 } +}; + +static int zotac_process_report(struct zotac_device *zotac, + const struct zotac_report_handler *handlers, + u8 *data, int size) +{ + const struct zotac_report_handler *handler; + u8 report_id; + + if (size < 1) + return 0; + + report_id = data[0]; + + for (handler = handlers; handler->handler; handler++) { + if (handler->report_id == report_id) { + handler->handler(zotac, data, size); + return 1; + } + } + + return 0; +} + +static int zotac_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + int intf_num = get_interface_num(hdev); + int handled = 0; + + if (size < 2) + return 0; + + switch (intf_num) { + case ZOTAC_GAMEPAD_INTERFACE: + if (zotac.gamepad) { + zotac_process_gamepad_report(&zotac, data, size); + handled = 1; + } + break; + case ZOTAC_DIAL_INTERFACE: + if (data[0] == ZOTAC_KBD_REPORT_ID) + return process_keyboard_report(&zotac, data, size); + + handled = zotac_process_report(&zotac, dial_interface_handlers, + data, size); + break; + case ZOTAC_REPORT_INTERFACE: + if (data[0] >= ZOTAC_STATUS_REPORT_ID && + data[0] <= ZOTAC_STATUS3_REPORT_ID) + handled = 1; + break; + } + + return handled; +} + +static int zotac_input_mapping(struct hid_device *hdev, struct hid_input *hi, + struct hid_field *field, struct hid_usage *usage, + unsigned long **bit, int *max) +{ + int intf_num = get_interface_num(hdev); + + if (intf_num == ZOTAC_REPORT_INTERFACE || + intf_num == ZOTAC_COMMAND_INTERFACE) + return -1; + + if (intf_num == ZOTAC_DIAL_INTERFACE) { + if (field->report && + (field->report->id == ZOTAC_MOUSE_REPORT_ID || + field->report->id == ZOTAC_DIAL_REPORT_ID)) + return -1; + + if (field->report && field->report->id == ZOTAC_KBD_REPORT_ID) + return 0; + } + + return 0; +} + +/** + * zotac_init_input_device - Initialize common input device properties + * @input_dev: The input device to initialize + * @hdev: The HID device associated with this input device + * @name: The name to assign to the input device + * + * Sets up common properties for an input device based on the HID device + */ +void zotac_init_input_device(struct input_dev *input_dev, + struct hid_device *hdev, const char *name) +{ + input_dev->name = name; + input_dev->phys = hdev->phys; + input_dev->uniq = hdev->uniq; + input_dev->id.bustype = hdev->bus; + input_dev->id.vendor = hdev->vendor; + input_dev->id.product = hdev->product; + input_dev->id.version = hdev->version; + input_dev->dev.parent = &hdev->dev; +} + +static int setup_wheel_input_device(struct zotac_device *zotac) +{ + int ret; + + zotac->wheel_input = devm_input_allocate_device(&zotac->hdev->dev); + if (!zotac->wheel_input) { + hid_err(zotac->hdev, "Failed to allocate wheel input device\n"); + return -ENOMEM; + } + + zotac_init_input_device(zotac->wheel_input, zotac->hdev, + "ZOTAC Gaming Zone Dials"); + + __set_bit(EV_REL, zotac->wheel_input->evbit); + __set_bit(REL_WHEEL, zotac->wheel_input->relbit); + __set_bit(REL_HWHEEL, zotac->wheel_input->relbit); + + ret = input_register_device(zotac->wheel_input); + if (ret) { + hid_err(zotac->hdev, "Failed to register wheel input device\n"); + return ret; + } + + return 0; +} + +static int setup_mouse_input_device(struct zotac_device *zotac) +{ + int ret, i; + + zotac->mouse_input = devm_input_allocate_device(&zotac->hdev->dev); + if (!zotac->mouse_input) { + hid_err(zotac->hdev, "Failed to allocate mouse input device\n"); + return -ENOMEM; + } + + zotac_init_input_device(zotac->mouse_input, zotac->hdev, + "ZOTAC Gaming Zone Mouse"); + + __set_bit(EV_KEY, zotac->mouse_input->evbit); + __set_bit(EV_REL, zotac->mouse_input->evbit); + + for (i = 0; i < 8; i++) + __set_bit(BTN_LEFT + i, zotac->mouse_input->keybit); + + __set_bit(REL_X, zotac->mouse_input->relbit); + __set_bit(REL_Y, zotac->mouse_input->relbit); + __set_bit(REL_WHEEL, zotac->mouse_input->relbit); + + ret = input_register_device(zotac->mouse_input); + if (ret) { + hid_err(zotac->hdev, "Failed to register mouse input device\n"); + return ret; + } + + return 0; +} + +static int zotac_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int intf_num, ret; + bool gamepad_initialized = false; + bool cfg_initialized = false; + + intf_num = get_interface_num(hdev); + + zotac.hdev = hdev; + hid_set_drvdata(hdev, &zotac); + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "Parse failed\n"); + return ret; + } + + switch (intf_num) { + case ZOTAC_DIAL_INTERFACE: + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + break; + case ZOTAC_REPORT_INTERFACE: + case ZOTAC_COMMAND_INTERFACE: + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + break; + default: + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + break; + } + + if (ret) { + hid_err(hdev, "HID hw start failed\n"); + goto err; + } + + if (intf_num == ZOTAC_DIAL_INTERFACE) { + ret = setup_wheel_input_device(&zotac); + if (ret) { + hid_err(hdev, "Wheel input setup failed\n"); + goto err_stop_hw; + } + + ret = setup_mouse_input_device(&zotac); + if (ret) { + hid_err(hdev, "Mouse input setup failed\n"); + /* Unregister wheel input before jumping to err_stop_hw */ + input_unregister_device(zotac.wheel_input); + zotac.wheel_input = NULL; + goto err_stop_hw; + } + + ret = zotac_init_gamepad(&zotac, zotac_get_usb_interface(hdev)); + if (ret) { + hid_warn(hdev, "Gamepad initialization failed: %d\n", + ret); + } else { + gamepad_initialized = true; + } + } + + if (intf_num == ZOTAC_COMMAND_INTERFACE) { + ret = zotac_cfg_init(&zotac); + if (ret) { + hid_warn(hdev, "Cfg data initialization failed: %d\n", + ret); + goto err_stop_hw; + } else { + cfg_initialized = true; + zotac_register_sysfs(&zotac); + } + ret = zotac_rgb_init(&zotac); + if (ret) + hid_warn(hdev, "RGB initialization failed: %d\n", ret); + } + + hid_info(hdev, "Loaded version %s\n", ZOTAC_VERSION); + + return 0; + +err_stop_hw: + if (gamepad_initialized) + zotac_cleanup_gamepad(&zotac); + if (cfg_initialized) + zotac_cfg_cleanup(&zotac); + + hid_hw_stop(hdev); +err: + return ret; +} + +static int zotac_input_configured(struct hid_device *hdev, struct hid_input *hi) +{ + int intf_num = get_interface_num(hdev); + + if (intf_num == ZOTAC_DIAL_INTERFACE) + hi->input->name = "ZOTAC Gaming Zone Keyboard"; + + return 0; +} + +static int zotac_resubmit_urbs(struct hid_device *hdev) +{ + int intf_num = get_interface_num(hdev); + int ret = 0; + + if (zotac.gamepad && zotac.gamepad->urbs[0] && + (intf_num == ZOTAC_GAMEPAD_INTERFACE || + intf_num == ZOTAC_DIAL_INTERFACE)) { + ret = usb_submit_urb(zotac.gamepad->urbs[0], GFP_NOIO); + if (ret) { + hid_err(hdev, "Failed to resubmit gamepad URB: %d\n", + ret); + return ret; + } + hid_dbg(hdev, "Gamepad URB resubmitted successfully\n"); + } + + return 0; +} + +static int zotac_resume(struct hid_device *hdev) +{ + hid_dbg(hdev, "resume called for interface %d\n", + get_interface_num(hdev)); + return zotac_resubmit_urbs(hdev); +} + +static int zotac_reset_resume(struct hid_device *hdev) +{ + int intf_num = get_interface_num(hdev); + + hid_info(hdev, "reset_resume called for interface %d\n", intf_num); + + if (zotac.led_rgb_dev && intf_num == ZOTAC_COMMAND_INTERFACE) + zotac_rgb_resume(&zotac); + + return zotac_resubmit_urbs(hdev); +} + +static int zotac_suspend(struct hid_device *hdev, pm_message_t message) +{ + int intf_num = get_interface_num(hdev); + int i; + + hid_dbg(hdev, "suspend called for interface %d\n", intf_num); + + if (zotac.gamepad && (intf_num == ZOTAC_GAMEPAD_INTERFACE || + intf_num == ZOTAC_DIAL_INTERFACE)) { + /* Kill all input URBs */ + for (i = 0; i < ZOTAC_NUM_URBS; i++) { + if (zotac.gamepad->urbs[i]) { + usb_kill_urb(zotac.gamepad->urbs[i]); + hid_dbg(hdev, + "Gamepad URB %d killed for suspend\n", + i); + } + } + + /* Kill all force feedback URBs */ + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (zotac.gamepad->ff_urbs[i]) { + usb_kill_urb(zotac.gamepad->ff_urbs[i]); + hid_dbg(hdev, + "Force feedback URB %d killed for suspend\n", + i); + } + } + } + + if (zotac.led_rgb_dev && intf_num == ZOTAC_COMMAND_INTERFACE) + zotac_rgb_suspend(&zotac); + + return 0; +} + +static void zotac_remove(struct hid_device *hdev) +{ + int intf_num = get_interface_num(hdev); + + dev_info(&hdev->dev, "Removing driver for interface %d", intf_num); + + if (intf_num == ZOTAC_COMMAND_INTERFACE) { + dev_info(&hdev->dev, "Unregistering sysfs entries"); + zotac_unregister_sysfs(&zotac); + } + + if (zotac.gamepad) + zotac_cleanup_gamepad(&zotac); + + if (zotac.cfg_data) + zotac_cfg_cleanup(&zotac); + + if (zotac.led_rgb_dev) + zotac_rgb_cleanup(&zotac); + + hid_hw_stop(hdev); +} + +static const struct hid_device_id zotac_devices[] = { + { HID_USB_DEVICE(ZOTAC_VENDOR_ID, ZOTAC_PRODUCT_ID) }, + { HID_USB_DEVICE(ZOTAC_ALT_VENDOR_ID, ZOTAC_PRODUCT_ID) }, + {} +}; + +MODULE_DEVICE_TABLE(hid, zotac_devices); + +static struct hid_driver zotac_driver = { + .name = "zotac_zone_hid", + .id_table = zotac_devices, + .probe = zotac_probe, + .remove = zotac_remove, + .raw_event = zotac_raw_event, + .input_mapping = zotac_input_mapping, + .input_configured = zotac_input_configured, + .reset_resume = zotac_reset_resume, + .suspend = zotac_suspend, + .resume = zotac_resume, +}; + +module_hid_driver(zotac_driver); + +MODULE_AUTHOR("Luke D. Jones"); +MODULE_DESCRIPTION("HID driver for ZOTAC Gaming Zone Controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/hid/zotac-zone-hid/zotac-zone-hid-input.c b/drivers/hid/zotac-zone-hid/zotac-zone-hid-input.c new file mode 100644 index 000000000000..571eec2df446 --- /dev/null +++ b/drivers/hid/zotac-zone-hid/zotac-zone-hid-input.c @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for ZOTAC Gaming Zone Controller - RGB LED control + * + * Copyright (c) 2025 Luke D. Jones + */ + +#include "linux/input-event-codes.h" +#include +#include +#include +#include +#include +#include +#include + +#include "zotac-zone.h" + +#define ZOTAC_GAMEPAD_REPORT_SIZE 64 +#define ZOTAC_GAMEPAD_URB_INTERVAL 1 + +static void zotac_gamepad_urb_irq(struct urb *urb) +{ + struct zotac_device *zotac = urb->context; + struct zotac_gamepad *gamepad; + unsigned char *data = urb->transfer_buffer; + int retval, status = urb->status; + + if (!zotac || !zotac->gamepad) + return; + + gamepad = zotac->gamepad; + + /* Use memory barrier before reading disconnect flag to ensure latest value */ + smp_rmb(); + if (READ_ONCE(gamepad->disconnect)) { + return; + } + + switch (status) { + case 0: + break; + case -ECONNRESET: + case -ENOENT: + case -ESHUTDOWN: + return; + default: + goto exit; + } + + zotac_process_gamepad_report(zotac, data, urb->actual_length); + +exit: + /* Use memory barrier before reading disconnect flag to ensure latest value */ + smp_rmb(); + if (!READ_ONCE(gamepad->disconnect)) { + retval = usb_submit_urb(urb, GFP_ATOMIC); + if (retval) + dev_err(&urb->dev->dev, + "usb_submit_urb failed with result %d\n", + retval); + } +} + +static void zotac_gamepad_ff_urb_complete(struct urb *urb) +{ + struct zotac_device *zotac = urb->context; + struct zotac_gamepad *gamepad; + int i; + + if (!zotac || !zotac->gamepad) + return; + + gamepad = zotac->gamepad; + + if (urb->status) + dev_dbg(&urb->dev->dev, "FF urb status %d\n", urb->status); + + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (gamepad->ff_urbs[i] == urb) { + gamepad->ff_urbs[i]->transfer_flags &= + ~URB_NO_TRANSFER_DMA_MAP; + atomic_set(&gamepad->ff_active[i], 0); + break; + } + } +} + +static int zotac_gamepad_play_effect(struct input_dev *dev, void *data, + struct ff_effect *effect) +{ + struct zotac_device *zotac = input_get_drvdata(dev); + struct zotac_gamepad *gamepad = zotac->gamepad; + int retval = -EBUSY, i; + u16 strong, weak; + + if (!gamepad || READ_ONCE(gamepad->disconnect)) + return -ENODEV; + if (effect->type != FF_RUMBLE) + return 0; + + strong = effect->u.rumble.strong_magnitude; + weak = effect->u.rumble.weak_magnitude; + + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (atomic_read(&gamepad->ff_active[i]) == 0) { + gamepad->ff_data[i][0] = ZOTAC_FF_REPORT_ID; + gamepad->ff_data[i][1] = 0x08; + gamepad->ff_data[i][2] = 0x00; + gamepad->ff_data[i][3] = strong / 256; + gamepad->ff_data[i][4] = weak / 256; + gamepad->ff_data[i][5] = 0x00; + gamepad->ff_data[i][6] = 0x00; + gamepad->ff_data[i][7] = 0x00; + + /* Use atomic compare-and-swap to claim this URB */ + if (atomic_cmpxchg(&gamepad->ff_active[i], 0, 1) == 0) { + retval = usb_submit_urb(gamepad->ff_urbs[i], + GFP_ATOMIC); + if (retval) { + dev_err(&zotac->hdev->dev, + "usb_submit_urb(ff) failed: %d\n", + retval); + atomic_set(&gamepad->ff_active[i], 0); + } + break; + } + } + } + return retval; +} + +void zotac_process_gamepad_report(struct zotac_device *zotac, u8 *data, + int size) +{ + struct zotac_gamepad *gamepad = zotac->gamepad; + struct input_dev *input_dev; + + if (!gamepad || size < 14 || !(input_dev = gamepad->dev) || + data[0] != 0x00) + return; + + input_report_abs(input_dev, ABS_HAT0X, + !!(data[2] & 0x08) - !!(data[2] & 0x04)); + input_report_abs(input_dev, ABS_HAT0Y, + !!(data[2] & 0x02) - !!(data[2] & 0x01)); + input_report_key(input_dev, BTN_START, data[2] & BIT(4)); + input_report_key(input_dev, BTN_SELECT, data[2] & BIT(5)); + input_report_key(input_dev, BTN_THUMBL, data[2] & BIT(6)); + input_report_key(input_dev, BTN_THUMBR, data[2] & BIT(7)); + input_report_key(input_dev, BTN_A, data[3] & BIT(4)); + input_report_key(input_dev, BTN_B, data[3] & BIT(5)); + input_report_key(input_dev, BTN_X, data[3] & BIT(6)); + input_report_key(input_dev, BTN_Y, data[3] & BIT(7)); + input_report_key(input_dev, BTN_TL, data[3] & BIT(0)); + input_report_key(input_dev, BTN_TR, data[3] & BIT(1)); + input_report_key(input_dev, BTN_MODE, data[3] & BIT(2)); + input_report_abs(input_dev, ABS_X, + (__s16)le16_to_cpup((__le16 *)(data + 6))); + input_report_abs(input_dev, ABS_Y, + ~(__s16)le16_to_cpup((__le16 *)(data + 8))); + input_report_abs(input_dev, ABS_RX, + (__s16)le16_to_cpup((__le16 *)(data + 10))); + input_report_abs(input_dev, ABS_RY, + ~(__s16)le16_to_cpup((__le16 *)(data + 12))); + input_report_abs(input_dev, ABS_Z, data[4]); + input_report_abs(input_dev, ABS_RZ, data[5]); + input_sync(input_dev); +} + +static void zotac_button_work_func(struct work_struct *work) +{ + struct zotac_gamepad *gamepad = container_of( + to_delayed_work(work), struct zotac_gamepad, button_work); + unsigned int button2, button; + bool qam_update; + + if (READ_ONCE(gamepad->disconnect) || !gamepad->dev) + return; + + /* Access these values atomically without a spinlock */ + /* We copy them locally to avoid races */ + button = READ_ONCE(gamepad->button_to_press); + button2 = READ_ONCE(gamepad->button_to_press2); + qam_update = READ_ONCE(gamepad->update_qam); + + /* Update the state variables */ + WRITE_ONCE(gamepad->update_qam, false); + WRITE_ONCE(gamepad->button_to_press, 0); + WRITE_ONCE(gamepad->button_to_press2, 0); + + /* Memory barrier to ensure these writes complete before proceeding */ + smp_wmb(); + + if (qam_update) { + input_report_key(gamepad->dev, button, 1); + input_sync(gamepad->dev); + msleep(150); + input_report_key(gamepad->dev, button2, 1); + input_sync(gamepad->dev); + input_report_key(gamepad->dev, button2, 0); + input_sync(gamepad->dev); + input_report_key(gamepad->dev, button, 0); + input_sync(gamepad->dev); + } else if (button) { + input_report_key(gamepad->dev, button, 1); + input_sync(gamepad->dev); + input_report_key(gamepad->dev, button, 0); + input_sync(gamepad->dev); + } + + /* Release the button press lock so others can schedule button presses */ + atomic_set(&gamepad->button_press_in_progress, 0); +} + +void zotac_gamepad_send_button(struct zotac_device *zotac, int buttons[], + int num_buttons) +{ + struct zotac_gamepad *gamepad; + + if (!zotac || !zotac->gamepad || !zotac->gamepad->dev || + READ_ONCE(zotac->gamepad->disconnect)) + return; + + gamepad = zotac->gamepad; + + /* Try to atomically take the button press lock */ + if (atomic_cmpxchg(&gamepad->button_press_in_progress, 0, 1) == 0) { + /* We got the lock, update button values */ + + /* Reset button state first */ + WRITE_ONCE(gamepad->button_to_press, 0); + WRITE_ONCE(gamepad->button_to_press2, 0); + WRITE_ONCE(gamepad->update_qam, false); + + /* Set new button values */ + if (num_buttons == 1) { + WRITE_ONCE(gamepad->button_to_press, buttons[0]); + } else if (num_buttons == 2) { + WRITE_ONCE(gamepad->update_qam, true); + WRITE_ONCE(gamepad->button_to_press, buttons[0]); + WRITE_ONCE(gamepad->button_to_press2, buttons[1]); + } else { + /* No buttons to press, release the lock */ + atomic_set(&gamepad->button_press_in_progress, 0); + return; + } + + /* Memory barrier to ensure writes complete before scheduling work */ + smp_wmb(); + + /* Schedule the work */ + schedule_delayed_work(&gamepad->button_work, + msecs_to_jiffies(5)); + } +} + +static void zotac_find_endpoints(struct usb_interface *intf, + struct usb_endpoint_descriptor **ep_in, + struct usb_endpoint_descriptor **ep_out) +{ + struct usb_host_interface *host_interface = intf->cur_altsetting; + int i; + + *ep_in = *ep_out = NULL; + for (i = 0; i < host_interface->desc.bNumEndpoints; i++) { + struct usb_endpoint_descriptor *ep = + &host_interface->endpoint[i].desc; + if (usb_endpoint_is_int_in(ep)) + *ep_in = ep; + else if (usb_endpoint_is_int_out(ep)) + *ep_out = ep; + } +} + +int zotac_init_gamepad(struct zotac_device *zotac, struct usb_interface *intf) +{ + struct usb_endpoint_descriptor *ep_in = NULL, *ep_out = NULL; + struct usb_device *udev = interface_to_usbdev(intf); + struct hid_device *hdev = zotac->hdev; + int pipe, maxp, interval, ret = 0, i; + + struct usb_interface *gamepad_intf; + struct zotac_gamepad *gamepad; + struct input_dev *input_dev; + + if (!(gamepad = kzalloc(sizeof(*gamepad), GFP_KERNEL))) + return -ENOMEM; + + WRITE_ONCE(gamepad->disconnect, false); + gamepad->zotac = zotac; + zotac->gamepad = gamepad; + zotac->udev = udev; + + if (!(gamepad_intf = usb_ifnum_to_if(udev, ZOTAC_GAMEPAD_INTERFACE))) { + ret = -ENODEV; + goto err_free_gamepad; + } + + zotac_find_endpoints(gamepad_intf, &ep_in, &ep_out); + if (!ep_in) { + ret = -ENODEV; + goto err_free_gamepad; + } + + gamepad->qam_mode = QAM_MODE_STEAM; + gamepad->ep_in = ep_in; + gamepad->ep_out = ep_out; + + if (!(input_dev = input_allocate_device())) { + ret = -ENOMEM; + goto err_free_gamepad; + } + + gamepad->dev = input_dev; + zotac_init_input_device(input_dev, hdev, "ZOTAC Gaming Zone Gamepad"); + input_set_drvdata(input_dev, zotac); + + input_set_abs_params(input_dev, ABS_X, -32768, 32767, 16, 128); + input_set_abs_params(input_dev, ABS_Y, -32768, 32767, 16, 128); + input_set_abs_params(input_dev, ABS_RX, -32768, 32767, 16, 128); + input_set_abs_params(input_dev, ABS_RY, -32768, 32767, 16, 128); + input_set_abs_params(input_dev, ABS_Z, 0, 255, 0, 0); + input_set_abs_params(input_dev, ABS_RZ, 0, 255, 0, 0); + input_set_abs_params(input_dev, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input_dev, ABS_HAT0Y, -1, 1, 0, 0); + + input_set_capability(input_dev, EV_KEY, BTN_A); + input_set_capability(input_dev, EV_KEY, BTN_B); + input_set_capability(input_dev, EV_KEY, BTN_X); + input_set_capability(input_dev, EV_KEY, BTN_Y); + input_set_capability(input_dev, EV_KEY, BTN_TL); + input_set_capability(input_dev, EV_KEY, BTN_TR); + input_set_capability(input_dev, EV_KEY, BTN_MODE); + input_set_capability(input_dev, EV_KEY, BTN_START); + input_set_capability(input_dev, EV_KEY, BTN_SELECT); + input_set_capability(input_dev, EV_KEY, BTN_THUMBL); + input_set_capability(input_dev, EV_KEY, BTN_THUMBR); + + /* Allow the gamepad to emit these events for screenface buttons */ + input_set_capability(input_dev, EV_KEY, KEY_F14); + input_set_capability(input_dev, EV_KEY, KEY_F15); + input_set_capability(input_dev, EV_KEY, KEY_F16); + input_set_capability(input_dev, EV_KEY, KEY_F17); + input_set_capability(input_dev, EV_KEY, KEY_F18); + input_set_capability(input_dev, EV_KEY, KEY_F19); + + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY1); + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY2); + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY3); + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY4); + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY5); + input_set_capability(input_dev, EV_KEY, BTN_TRIGGER_HAPPY6); + + pipe = usb_rcvintpipe(udev, gamepad->ep_in->bEndpointAddress); + if (!(maxp = usb_maxpacket(udev, pipe))) { + ret = -EINVAL; + goto err_free_input; + } + + interval = gamepad->ep_in->bInterval ? gamepad->ep_in->bInterval : + ZOTAC_GAMEPAD_URB_INTERVAL; + + for (i = 0; i < ZOTAC_NUM_URBS; i++) { + if (!(gamepad->urb_buf[i] = + kzalloc(ZOTAC_GAMEPAD_REPORT_SIZE, GFP_KERNEL))) { + ret = -ENOMEM; + goto err_free_urbs; + } + if (!(gamepad->urbs[i] = usb_alloc_urb(0, GFP_KERNEL))) { + ret = -ENOMEM; + goto err_free_urbs; + } + usb_fill_int_urb(gamepad->urbs[i], udev, pipe, + gamepad->urb_buf[i], ZOTAC_GAMEPAD_REPORT_SIZE, + zotac_gamepad_urb_irq, zotac, interval); + } + + INIT_DELAYED_WORK(&gamepad->button_work, zotac_button_work_func); + + /* Initialize atomic variables */ + atomic_set(&gamepad->button_press_in_progress, 0); + + if (gamepad->ep_out) { + input_set_capability(input_dev, EV_FF, FF_RUMBLE); + + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + /* Initialize atomic ff_active */ + atomic_set(&gamepad->ff_active[i], 0); + + if (!(gamepad->ff_data[i] = usb_alloc_coherent( + udev, ZOTAC_FF_REPORT_LEN, GFP_KERNEL, + &gamepad->ff_dma[i]))) { + ret = -ENOMEM; + goto err_free_ff_data; + } + if (!(gamepad->ff_urbs[i] = + usb_alloc_urb(0, GFP_KERNEL))) { + ret = -ENOMEM; + goto err_free_ff_urbs; + } + usb_fill_int_urb( + gamepad->ff_urbs[i], udev, + usb_sndintpipe( + udev, + gamepad->ep_out->bEndpointAddress), + gamepad->ff_data[i], ZOTAC_FF_REPORT_LEN, + zotac_gamepad_ff_urb_complete, zotac, + gamepad->ep_out->bInterval); + gamepad->ff_urbs[i]->transfer_dma = gamepad->ff_dma[i]; + gamepad->ff_urbs[i]->transfer_flags |= + URB_NO_TRANSFER_DMA_MAP; + } + + if ((ret = input_ff_create_memless( + input_dev, NULL, zotac_gamepad_play_effect))) { + dev_err(&zotac->hdev->dev, + "Failed to create FF device: %d\n", ret); + goto err_free_ff_urbs; + } + } + + if ((ret = input_register_device(input_dev))) { + dev_err(&zotac->hdev->dev, + "Failed to register input device: %d\n", ret); + goto err_free_ff_urbs; + } + + for (i = 0; i < ZOTAC_NUM_URBS; i++) { + if ((ret = usb_submit_urb(gamepad->urbs[i], GFP_KERNEL))) { + dev_err(&zotac->hdev->dev, + "Failed to submit URB %d: %d\n", i, ret); + while (--i >= 0) + usb_kill_urb(gamepad->urbs[i]); + input_unregister_device(input_dev); + gamepad->dev = NULL; + goto err_free_ff_urbs; + } + } + return 0; + +err_free_ff_urbs: + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (gamepad->ff_urbs[i]) { + usb_free_urb(gamepad->ff_urbs[i]); + gamepad->ff_urbs[i] = NULL; + } + } + +err_free_ff_data: + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (gamepad->ff_data[i]) { + usb_free_coherent(udev, ZOTAC_FF_REPORT_LEN, + gamepad->ff_data[i], + gamepad->ff_dma[i]); + gamepad->ff_data[i] = NULL; + } + } + +err_free_urbs: + for (i = 0; i < ZOTAC_NUM_URBS; i++) { + if (gamepad->urbs[i]) { + usb_free_urb(gamepad->urbs[i]); + gamepad->urbs[i] = NULL; + } + if (gamepad->urb_buf[i]) { + kfree(gamepad->urb_buf[i]); + gamepad->urb_buf[i] = NULL; + } + } + +err_free_input: + if (gamepad->dev) { + input_free_device(gamepad->dev); + gamepad->dev = NULL; + } + +err_free_gamepad: + zotac->gamepad = NULL; + kfree(gamepad); + return ret; +} + +void zotac_cleanup_gamepad(struct zotac_device *zotac) +{ + struct zotac_gamepad *gamepad; + int i; + + if (!zotac || !zotac->gamepad) + return; + + gamepad = zotac->gamepad; + + /* Set disconnect first, use WRITE_ONCE and memory barrier to ensure visibility */ + WRITE_ONCE(gamepad->disconnect, true); + smp_wmb(); + + cancel_delayed_work_sync(&gamepad->button_work); + + for (i = 0; i < ZOTAC_NUM_URBS; i++) { + if (gamepad->urbs[i]) { + usb_kill_urb(gamepad->urbs[i]); + usb_free_urb(gamepad->urbs[i]); + gamepad->urbs[i] = NULL; + } + } + + for (i = 0; i < ZOTAC_NUM_FF_URBS; i++) { + if (gamepad->ff_urbs[i]) { + usb_kill_urb(gamepad->ff_urbs[i]); + usb_free_urb(gamepad->ff_urbs[i]); + gamepad->ff_urbs[i] = NULL; + } + } + + if (gamepad->dev) { + input_unregister_device(gamepad->dev); + gamepad->dev = NULL; + } + + zotac->gamepad = NULL; +} diff --git a/drivers/hid/zotac-zone-hid/zotac-zone-hid-rgb.c b/drivers/hid/zotac-zone-hid/zotac-zone-hid-rgb.c new file mode 100644 index 000000000000..0305de2bd549 --- /dev/null +++ b/drivers/hid/zotac-zone-hid/zotac-zone-hid-rgb.c @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for ZOTAC Gaming Zone Controller - RGB LED control + * + * Copyright (c) 2025 Luke D. Jones + */ + +#include +#include +#include +#include +#include +#include + +#include "zotac-zone.h" + +#define SETTING_COLOR 0x00 +#define SETTING_SPEED 0x01 +#define SETTING_EFFECT 0x02 +#define SETTING_BRIGHTNESS 0x03 +#define SETTING_REAL_TIME 0x04 + +#define EFFECT_RAINBOW 0x00 +#define EFFECT_BREATHE 0x01 +#define EFFECT_STARS 0x02 +#define EFFECT_FADE 0x03 +#define EFFECT_DANCE 0x04 +#define EFFECT_OFF 0xF0 + +#define SPEED_SLOW 0x00 +#define SPEED_NORMAL 0x01 +#define SPEED_FAST 0x02 + +#define BRIGHTNESS_OFF 0x00 /* 0% */ +#define BRIGHTNESS_LOW 0x19 /* 25% */ +#define BRIGHTNESS_MED 0x32 /* 50% */ +#define BRIGHTNESS_HIGH 0x4B /* 75% */ +#define BRIGHTNESS_MAX 0x64 /* 100% */ + +static void zotac_rgb_set_default_colors(struct zotac_device *zotac, + struct zotac_rgb_dev *led_rgb, + int zone_idx) +{ + int j, led_index; + + for (j = 0; j < ZOTAC_RGB_LEDS_PER_ZONE; j++) { + led_index = zone_idx * ZOTAC_RGB_LEDS_PER_ZONE + j; + + led_rgb->red[led_index] = 128; + led_rgb->green[led_index] = 128; + led_rgb->blue[led_index] = 128; + + zotac->led_rgb_data.zone[zone_idx].red[j] = 128; + zotac->led_rgb_data.zone[zone_idx].green[j] = 128; + zotac->led_rgb_data.zone[zone_idx].blue[j] = 128; + } +} + +static int zotac_rgb_read_zone_colors(struct zotac_device *zotac, + struct zotac_rgb_dev *led_rgb, + u8 zone_idx) +{ + size_t expected_len = 1 + ZOTAC_RGB_LEDS_PER_ZONE * 3; + u8 zone_rgb_data[1 + ZOTAC_RGB_LEDS_PER_ZONE * 3]; + size_t data_len = sizeof(zone_rgb_data); + int ret, j, led_index, red_idx, green_idx, blue_idx; + u8 red, green, blue; + + zone_rgb_data[0] = zone_idx; + + ret = zotac_send_get_command(zotac, CMD_GET_RGB, SETTING_COLOR, + &zone_idx, 1, zone_rgb_data, &data_len); + + if (ret < 0) { + hid_err(zotac->hdev, "Failed to read RGB data for zone %d\n", + zone_idx); + zotac_rgb_set_default_colors(zotac, led_rgb, zone_idx); + return ret; + } + + if (data_len < expected_len) { + hid_warn(zotac->hdev, + "Incomplete RGB data for zone %d: %zu bytes\n", + zone_idx, data_len); + zotac_rgb_set_default_colors(zotac, led_rgb, zone_idx); + return 0; + } + + for (j = 0; j < ZOTAC_RGB_LEDS_PER_ZONE; j++) { + led_index = zone_idx * ZOTAC_RGB_LEDS_PER_ZONE + j; + red_idx = 1 + (j * 3); + green_idx = red_idx + 1; + blue_idx = red_idx + 2; + + if (red_idx >= data_len || green_idx >= data_len || + blue_idx >= data_len) { + hid_warn(zotac->hdev, + "Index out of bounds for zone %d, LED %d\n", + zone_idx, j); + break; + } + + red = zone_rgb_data[red_idx]; + green = zone_rgb_data[green_idx]; + blue = zone_rgb_data[blue_idx]; + + if (led_index < + ZOTAC_RGB_ZONE_COUNT * ZOTAC_RGB_LEDS_PER_ZONE) { + led_rgb->red[led_index] = red; + led_rgb->green[led_index] = green; + led_rgb->blue[led_index] = blue; + + zotac->led_rgb_data.zone[zone_idx].red[j] = red; + zotac->led_rgb_data.zone[zone_idx].green[j] = green; + zotac->led_rgb_data.zone[zone_idx].blue[j] = blue; + } else { + hid_warn( + zotac->hdev, + "Output index out of bounds for zone %d, LED %d\n", + zone_idx, j); + break; + } + } + + return 0; +} + +static int zotac_rgb_set_globals(struct zotac_device *zotac) +{ + int effect, speed, brightness; + + effect = zotac_send_get_byte(zotac, CMD_GET_RGB, SETTING_EFFECT, NULL, + 0); + if (effect < 0) { + hid_warn( + zotac->hdev, + "Could not read effect from device, using default: %d\n", + EFFECT_RAINBOW); + effect = EFFECT_RAINBOW; + } + + speed = zotac_send_get_byte(zotac, CMD_GET_RGB, SETTING_SPEED, NULL, 0); + if (speed < 0) { + hid_warn( + zotac->hdev, + "Could not read speed from device, using default: %d\n", + SPEED_NORMAL); + speed = SPEED_NORMAL; + } + + /* This brightness is firmware level, not LED class level */ + brightness = zotac_send_get_byte(zotac, CMD_GET_RGB, SETTING_BRIGHTNESS, + NULL, 0); + if (brightness < 0) { + hid_warn( + zotac->hdev, + "Could not read brightness from device, using default: %d\n", + BRIGHTNESS_MED); + brightness = BRIGHTNESS_MED; + } + + zotac->led_rgb_data.effect = effect; + zotac->led_rgb_data.speed = speed; + zotac->led_rgb_data.brightness = brightness; + + return 0; +} + +static void zotac_rgb_schedule_work(struct zotac_rgb_dev *led) +{ + unsigned long flags; + + spin_lock_irqsave(&led->lock, flags); + if (!led->removed) + schedule_work(&led->work); + spin_unlock_irqrestore(&led->lock, flags); +} + +static void zotac_rgb_do_work(struct work_struct *work) +{ + struct zotac_rgb_dev *led = + container_of(work, struct zotac_rgb_dev, work); + struct zotac_device *zotac = led->zotac; + u8 zone_idx = led - zotac->led_rgb_dev; + u8 zone_data[3 + ZOTAC_RGB_LEDS_PER_ZONE * 3]; + unsigned long flags; + int j, led_index; + + spin_lock_irqsave(&led->lock, flags); + if (!led->update_rgb) { + spin_unlock_irqrestore(&led->lock, flags); + return; + } + led->update_rgb = false; + + zone_data[0] = zone_idx; + + // [0] = zone number, [1..2] = blank, [3..] = data + for (j = 0; j < ZOTAC_RGB_LEDS_PER_ZONE; j++) { + led_index = zone_idx * ZOTAC_RGB_LEDS_PER_ZONE + j; + zone_data[3 + (j * 3)] = led->red[led_index]; + zone_data[4 + (j * 3)] = led->green[led_index]; + zone_data[5 + (j * 3)] = led->blue[led_index]; + } + spin_unlock_irqrestore(&led->lock, flags); + + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_COLOR, zone_data, + sizeof(zone_data)); + + if (zone_idx == 0) { + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_EFFECT, + &zotac->led_rgb_data.effect, 1); + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_SPEED, + &zotac->led_rgb_data.speed, 1); + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_BRIGHTNESS, + &led->brightness, 1); + } + + zotac_send_set_command(zotac, CMD_SAVE_CONFIG, 0, NULL, 0); +} + +static void zotac_rgb_set_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); + struct zotac_rgb_dev *led = + container_of(mc_cdev, struct zotac_rgb_dev, led_rgb_dev); + struct zotac_device *zotac = led->zotac; + u8 zone_idx = led - zotac->led_rgb_dev; + unsigned long flags; + int i, led_index, intensity, bright; + + led_mc_calc_color_components(mc_cdev, brightness); + + spin_lock_irqsave(&led->lock, flags); + led->update_rgb = true; + bright = mc_cdev->led_cdev.brightness; + + for (i = 0; i < ZOTAC_RGB_LEDS_PER_ZONE; i++) { + led_index = zone_idx * ZOTAC_RGB_LEDS_PER_ZONE + i; + intensity = mc_cdev->subled_info[i].intensity; + led->red[led_index] = (((intensity >> 16) & 0xFF) * bright) / 255; + led->green[led_index] = (((intensity >> 8) & 0xFF) * bright) / 255; + led->blue[led_index] = ((intensity & 0xFF) * bright) / 255; + + zotac->led_rgb_data.zone[zone_idx].red[i] = led->red[led_index]; + zotac->led_rgb_data.zone[zone_idx].green[i] = led->green[led_index]; + zotac->led_rgb_data.zone[zone_idx].blue[i] = led->blue[led_index]; + } + + zotac->led_rgb_data.zone[zone_idx].brightness = bright; + zotac->led_rgb_data.initialized = true; + spin_unlock_irqrestore(&led->lock, flags); + + zotac_rgb_schedule_work(led); +} + +static void zotac_rgb_store_settings(struct zotac_device *zotac) +{ + struct zotac_rgb_dev *led_rgb; + int i, arr_size = ZOTAC_RGB_LEDS_PER_ZONE; + + for (i = 0; i < ZOTAC_RGB_ZONE_COUNT; i++) { + led_rgb = &zotac->led_rgb_dev[i]; + + zotac->led_rgb_data.zone[i].brightness = + led_rgb->led_rgb_dev.led_cdev.brightness; + + memcpy(zotac->led_rgb_data.zone[i].red, + led_rgb->red + (i * ZOTAC_RGB_LEDS_PER_ZONE), arr_size); + memcpy(zotac->led_rgb_data.zone[i].green, + led_rgb->green + (i * ZOTAC_RGB_LEDS_PER_ZONE), + arr_size); + memcpy(zotac->led_rgb_data.zone[i].blue, + led_rgb->blue + (i * ZOTAC_RGB_LEDS_PER_ZONE), arr_size); + } +} + +static void zotac_rgb_restore_settings(struct zotac_rgb_dev *led_rgb, + struct led_classdev *led_cdev, + struct mc_subled *mc_led_info) +{ + struct zotac_device *zotac = led_rgb->zotac; + u8 zone_idx = led_rgb - zotac->led_rgb_dev; + int i, offset = zone_idx * ZOTAC_RGB_LEDS_PER_ZONE; + int arr_size = ZOTAC_RGB_LEDS_PER_ZONE; + + memcpy(led_rgb->red + offset, zotac->led_rgb_data.zone[zone_idx].red, + arr_size); + memcpy(led_rgb->green + offset, + zotac->led_rgb_data.zone[zone_idx].green, arr_size); + memcpy(led_rgb->blue + offset, zotac->led_rgb_data.zone[zone_idx].blue, + arr_size); + + for (i = 0; i < ZOTAC_RGB_LEDS_PER_ZONE; i++) { + mc_led_info[i].intensity = + (zotac->led_rgb_data.zone[zone_idx].red[i] << 16) | + (zotac->led_rgb_data.zone[zone_idx].green[i] << 8) | + zotac->led_rgb_data.zone[zone_idx].blue[i]; + } + + led_cdev->brightness = zotac->led_rgb_data.zone[zone_idx].brightness; +} + +static ssize_t rgb_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int effect; + + if (!zotac.cfg_data) + return -ENODEV; + effect = zotac_send_get_byte(&zotac, CMD_GET_RGB, SETTING_EFFECT, NULL, + 0); + if (effect < 0) + return effect; + + return sysfs_emit(buf, "%d\n", effect); +} + +static ssize_t rgb_effect_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int effect, ret; + u8 effect_val; + + if (!zotac.cfg_data) + return -ENODEV; + + ret = kstrtoint(buf, 10, &effect); + if (ret) + return ret; + + switch (effect) { + case EFFECT_RAINBOW: + case EFFECT_BREATHE: + case EFFECT_STARS: + case EFFECT_FADE: + case EFFECT_DANCE: + case EFFECT_OFF: + break; + default: + return -EINVAL; + } + + effect_val = (u8)effect; + ret = zotac_send_set_command(&zotac, CMD_SET_RGB, SETTING_EFFECT, + &effect_val, 1); + if (ret < 0) + return ret; + + zotac.led_rgb_data.effect = effect_val; + + return count; +} +static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); + +static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int speed; + + if (!zotac.cfg_data) + return -ENODEV; + + speed = zotac_send_get_byte(&zotac, CMD_GET_RGB, SETTING_SPEED, NULL, 0); + if (speed < 0) + return speed; + + return sysfs_emit(buf, "%d\n", speed); +} + +static ssize_t rgb_speed_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + int speed, ret; + u8 speed_val; + + if (!zotac.cfg_data) + return -ENODEV; + + ret = kstrtoint(buf, 10, &speed); + if (ret) { + dev_err(dev, "Invalid speed value format\n"); + return ret; + } + + switch (speed) { + case SPEED_SLOW: + case SPEED_NORMAL: + case SPEED_FAST: + break; + default: + dev_err(dev, "Invalid speed value: %d (valid: 0-2)\n", speed); + return -EINVAL; + } + + speed_val = (u8)speed; + ret = zotac_send_set_command(&zotac, CMD_SET_RGB, SETTING_SPEED, + &speed_val, 1); + if (ret < 0) { + dev_err(dev, "Failed to set RGB speed: %d\n", ret); + return ret; + } + + zotac.led_rgb_data.speed = speed_val; + + return count; +} +static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed"); + +static u8 brightness_level_to_value(unsigned int level) +{ + switch (level) { + case 0: + return BRIGHTNESS_OFF; + case 1: + return BRIGHTNESS_LOW; + case 2: + return BRIGHTNESS_MED; + case 3: + return BRIGHTNESS_HIGH; + case 4: + return BRIGHTNESS_MAX; + default: + return BRIGHTNESS_MED; + } +} + +static unsigned int brightness_value_to_level(u8 value) +{ + if (value <= (BRIGHTNESS_OFF + BRIGHTNESS_LOW) / 2) + return 0; + else if (value <= (BRIGHTNESS_LOW + BRIGHTNESS_MED) / 2) + return 1; + else if (value <= (BRIGHTNESS_MED + BRIGHTNESS_HIGH) / 2) + return 2; + else if (value <= (BRIGHTNESS_HIGH + BRIGHTNESS_MAX) / 2) + return 3; + else + return 4; +} + +static ssize_t rgb_brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int brightness = 0; + unsigned int level; + + if (!zotac.cfg_data) + return -ENODEV; + + brightness = zotac_send_get_byte(&zotac, CMD_GET_RGB, SETTING_BRIGHTNESS, + NULL, 0); + if (brightness < 0) + return brightness; + + level = brightness_value_to_level(brightness); + + return sysfs_emit(buf, "%u\n", level); +} + +static ssize_t rgb_brightness_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int level, ret; + u8 brightness; + + if (!zotac.cfg_data) + return -ENODEV; + + ret = kstrtoint(buf, 10, &level); + if (ret) + return ret; + + if (level > 4) + return -EINVAL; + + brightness = brightness_level_to_value(level); + + ret = zotac_send_set_command(&zotac, CMD_SET_RGB, SETTING_BRIGHTNESS, + &brightness, 1); + if (ret < 0) + return ret; + + ret = zotac_send_set_command(&zotac, CMD_SAVE_CONFIG, 0, NULL, 0); + if (ret < 0) + return ret; + + zotac.led_rgb_data.brightness = brightness; + + return count; +} +static DEVICE_ATTR_RW_NAMED(rgb_brightness, "brightness"); + +static struct attribute *zotac_rgb_attrs[] = { &dev_attr_rgb_effect.attr, + &dev_attr_rgb_speed.attr, + &dev_attr_rgb_brightness.attr, + NULL }; + +static const struct attribute_group zotac_rgb_attr_group = { + .name = "rgb", + .attrs = zotac_rgb_attrs, +}; + +/** +* zotac_rgb_resume - Restore RGB LED settings after system resume +* @zotac: Pointer to the zotac device structure +* +* Restores previously saved RGB settings after the system resumes from +* suspend. This includes effect, speed, and color settings for all zones. +*/ +void zotac_rgb_resume(struct zotac_device *zotac) +{ + struct zotac_rgb_dev *led_rgb; + struct led_classdev *led_cdev; + struct mc_subled *mc_led_info; + int i; + + if (!zotac->led_rgb_dev) + return; + + if (!zotac->led_rgb_data.initialized) { + hid_warn( + zotac->hdev, + "RGB data not initialized, skipping resume restoration\n"); + return; + } + + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_EFFECT, + &zotac->led_rgb_data.effect, 1); + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_SPEED, + &zotac->led_rgb_data.speed, 1); + zotac_send_set_command(zotac, CMD_SET_RGB, SETTING_BRIGHTNESS, + &zotac->led_rgb_data.brightness, 1); + + for (i = 0; i < ZOTAC_RGB_ZONE_COUNT; i++) { + led_rgb = &zotac->led_rgb_dev[i]; + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + mc_led_info = led_rgb->led_rgb_dev.subled_info; + + zotac_rgb_restore_settings(led_rgb, led_cdev, mc_led_info); + led_rgb->update_rgb = true; + zotac_rgb_schedule_work(led_rgb); + } +} + +/** +* zotac_rgb_suspend - Save RGB LED settings before system suspend +* @zotac: Pointer to the zotac device structure +* +* Stores current RGB settings before the system suspends so they +* can be restored when the system resumes. +*/ +void zotac_rgb_suspend(struct zotac_device *zotac) +{ + if (!zotac->led_rgb_dev) + return; + + zotac_rgb_store_settings(zotac); +} + +static int zotac_rgb_register_zone(struct hid_device *hdev, + struct zotac_rgb_dev *led_rgb, + int zone_index) +{ + struct mc_subled *mc_led_info; + struct led_classdev *led_cdev; + char name[32]; + int i, err; + + snprintf(name, sizeof(name), "zotac:rgb:spectra_zone_%d", zone_index); + + mc_led_info = devm_kmalloc_array(&hdev->dev, ZOTAC_RGB_LEDS_PER_ZONE, + sizeof(*mc_led_info), + GFP_KERNEL | __GFP_ZERO); + if (!mc_led_info) + return -ENOMEM; + + for (i = 0; i < ZOTAC_RGB_LEDS_PER_ZONE; i++) { + mc_led_info[i].color_index = LED_COLOR_ID_RGB; + } + + led_rgb->led_rgb_dev.subled_info = mc_led_info; + led_rgb->led_rgb_dev.num_colors = ZOTAC_RGB_LEDS_PER_ZONE; + + led_cdev = &led_rgb->led_rgb_dev.led_cdev; + led_cdev->brightness = 128; + led_cdev->name = kstrdup(name, GFP_KERNEL); + if (!led_cdev->name){ + devm_kfree(&hdev->dev, mc_led_info); + return -ENOMEM; + } + + led_cdev->max_brightness = 255; + led_cdev->brightness_set = zotac_rgb_set_brightness; + + err = devm_led_classdev_multicolor_register(&hdev->dev, + &led_rgb->led_rgb_dev); + if (err) { + kfree(led_cdev->name); + return err; + } + + err = sysfs_create_group(&led_cdev->dev->kobj, &zotac_rgb_attr_group); + if (err) { + return err; + } + + return 0; +} + +static int zotac_rgb_init_zone(struct zotac_device *zotac, int zone_idx) +{ + struct zotac_rgb_dev *led_rgb = &zotac->led_rgb_dev[zone_idx]; + int ret; + + led_rgb->hdev = zotac->hdev; + led_rgb->zotac = zotac; + led_rgb->removed = false; + led_rgb->brightness = 128; + + zotac->led_rgb_data.zone[zone_idx].brightness = + zotac->led_rgb_data.brightness; + + zotac_rgb_read_zone_colors(zotac, led_rgb, zone_idx); + + INIT_WORK(&led_rgb->work, zotac_rgb_do_work); + led_rgb->output_worker_initialized = true; + spin_lock_init(&led_rgb->lock); + + ret = zotac_rgb_register_zone(zotac->hdev, led_rgb, zone_idx); + if (ret < 0) + return ret; + + return 0; +} + +/** + * zotac_rgb_cleanup - Clean up resources used by RGB LED subsystem + * @zotac: Pointer to the zotac device structure + * + * Releases resources allocated for RGB LED control, including sysfs + * attributes, scheduled work, and allocated memory. + */ +void zotac_rgb_cleanup(struct zotac_device *zotac) +{ + struct zotac_rgb_dev *led_rgb; + unsigned long flags; + int i; + + if (!zotac->led_rgb_dev) + return; + + sysfs_remove_group(&zotac->led_rgb_dev->led_rgb_dev.led_cdev.dev->kobj, &zotac_rgb_attr_group); + + for (i = 0; i < ZOTAC_RGB_ZONE_COUNT; i++) { + led_rgb = &zotac->led_rgb_dev[i]; + + if (led_rgb->removed) + continue; + + spin_lock_irqsave(&led_rgb->lock, flags); + led_rgb->removed = true; + led_rgb->output_worker_initialized = false; + spin_unlock_irqrestore(&led_rgb->lock, flags); + + cancel_work_sync(&led_rgb->work); + } + + zotac->led_rgb_dev = NULL; +} + +/** +* zotac_rgb_init - Initialize RGB LED subsystem +* @zotac: Pointer to the zotac device structure +* +* Initializes the RGB LED subsystem by allocating memory for LEDs, +* fetching current settings from the device, registering LED zones, +* and creating sysfs attributes for RGB control. +* +* Return: 0 on success, negative error code on failure +*/ +int zotac_rgb_init(struct zotac_device *zotac) +{ + struct zotac_rgb_dev *led_rgb; + int ret, i; + + led_rgb = devm_kcalloc(&zotac->hdev->dev, ZOTAC_RGB_ZONE_COUNT, + sizeof(*led_rgb), GFP_KERNEL); + if (!led_rgb) + return -ENOMEM; + + zotac->led_rgb_dev = led_rgb; + + zotac_rgb_set_globals(zotac); + + for (i = 0; i < ZOTAC_RGB_ZONE_COUNT; i++) { + ret = zotac_rgb_init_zone(zotac, i); + if (ret < 0) { + zotac_rgb_cleanup(zotac); + return ret; + } + } + + zotac->led_rgb_data.initialized = true; + + for (i = 0; i < ZOTAC_RGB_ZONE_COUNT; i++) { + led_rgb = &zotac->led_rgb_dev[i]; + led_rgb->update_rgb = true; + zotac_rgb_schedule_work(led_rgb); + } + + return 0; +} diff --git a/drivers/hid/zotac-zone-hid/zotac-zone.h b/drivers/hid/zotac-zone-hid/zotac-zone.h new file mode 100644 index 000000000000..31188cee80f8 --- /dev/null +++ b/drivers/hid/zotac-zone-hid/zotac-zone.h @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for ZOTAC Gaming Zone Controller - RGB LED control + * + * Copyright (c) 2025 Luke D. Jones + */ + +#ifndef __HID_ZOTAC_ZONE_H +#define __HID_ZOTAC_ZONE_H + +#include +#include +#include +#include +#include + +#define ZOTAC_VERSION "0.1.4" + +#define ZOTAC_VENDOR_ID 0x1ee9 +#define ZOTAC_ALT_VENDOR_ID 0x1e19 +#define ZOTAC_PRODUCT_ID 0x1590 + +#define ZOTAC_GAMEPAD_INTERFACE 0 +#define ZOTAC_DIAL_INTERFACE 1 +#define ZOTAC_REPORT_INTERFACE 2 +#define ZOTAC_COMMAND_INTERFACE 3 + +#define ZOTAC_FF_REPORT_ID 0x00 +#define ZOTAC_FF_REPORT_LEN 8 +#define ZOTAC_NUM_URBS 3 +#define ZOTAC_NUM_FF_URBS 2 + +#define CMD_SAVE_CONFIG 0xFB +#define CMD_SET_RGB 0xAD +#define CMD_GET_RGB 0xAE + +#define ZOTAC_RGB_ZONE_COUNT 2 /* Number of physical zones (0 and 1) */ +#define ZOTAC_RGB_LEDS_PER_ZONE 10 /* Number of LEDs in each zone */ + +#define SENSITIVITY_POINT_COUNT 4 + +/* Command codes for button mapping */ +#define CMD_SET_BUTTON_MAPPING 0xA1 +#define CMD_GET_BUTTON_MAPPING 0xA2 + +#define BUTTON_MAX 0x18 +#define MAX_GAMEPAD_BUTTONS 14 +#define MAX_KEYBOARD_KEYS 6 +#define MAX_MOUSE_BUTTONS 3 + +#define DEVICE_ATTR_RO_NAMED(_name, _attr_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attr_name, .mode = 0444 }, \ + .show = _name##_show, \ + } + +#define DEVICE_ATTR_WO_NAMED(_name, _attr_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attr_name, .mode = 0200 }, \ + .store = _name##_store, \ + } + +#define DEVICE_ATTR_RW_NAMED(_name, _attr_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attr_name, .mode = 0644 }, \ + .show = _name##_show, \ + .store = _name##_store, \ + } + +enum qam_mode { + QAM_MODE_KEYBOARD = 0, + QAM_MODE_STEAM, + QAM_MODE_CUSTOM, + QAM_MODE_LENGTH, +}; + +struct zotac_gamepad { + struct input_dev *dev; /* input device interface */ + struct zotac_device *zotac; /* back-pointer to parent zotac device */ + bool disconnect; /* set when device disconnected */ + + struct usb_endpoint_descriptor *ep_in; + struct usb_endpoint_descriptor *ep_out; + + struct urb *urbs[ZOTAC_NUM_URBS]; + unsigned char *urb_buf[ZOTAC_NUM_URBS]; + + struct urb *ff_urbs[ZOTAC_NUM_FF_URBS]; + unsigned char *ff_data[ZOTAC_NUM_FF_URBS]; + dma_addr_t ff_dma[ZOTAC_NUM_FF_URBS]; + atomic_t ff_active[ZOTAC_NUM_FF_URBS]; + + atomic_t button_press_in_progress; + + struct delayed_work button_work; + unsigned int button_to_press; + unsigned int button_to_press2; + bool update_qam; + enum qam_mode qam_mode; +}; + +struct zotac_rgb_dev { + struct zotac_device *zotac; + struct hid_device *hdev; + struct led_classdev_mc led_rgb_dev; + struct work_struct work; + bool output_worker_initialized; + spinlock_t lock; + + bool removed; + bool update_rgb; + uint8_t red[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t green[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t blue[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t brightness; +}; + +struct zotac_rgb_data { + struct { + uint8_t red[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t green[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t blue[ZOTAC_RGB_LEDS_PER_ZONE]; + uint8_t brightness; // MC LED class level brightness + } zone[ZOTAC_RGB_ZONE_COUNT]; + uint8_t effect; // Global effect + uint8_t speed; // Global speed + uint8_t brightness; // Global brightness + bool initialized; +}; + +struct stick_sensitivity { + /* X1, Y1, X2, Y2, X3, Y3, X4, Y4 */ + u8 values[SENSITIVITY_POINT_COUNT * 2]; +}; + +struct deadzone { + u8 inner; + u8 outer; +}; + +struct button_mapping { + u32 target_gamepad_buttons; /* Bit field for controller buttons */ + u8 target_modifier_keys; /* Bit field for modifier keys */ + u8 target_keyboard_keys[MAX_KEYBOARD_KEYS]; /* Array of keyboard key codes */ + u8 target_mouse_buttons; /* Bit field for mouse buttons */ +}; + +struct zotac_cfg_data { + struct mutex command_mutex; + u8 sequence_num; + /* deadzones */ + struct deadzone ls_dz; // left stick + struct deadzone rs_dz; // right stick + struct deadzone lt_dz; // left trigger + struct deadzone rt_dz; // right trigger + struct stick_sensitivity left_stick_sensitivity; + struct stick_sensitivity right_stick_sensitivity; + u8 button_turbo; + /* Indexed by the button number */ + struct button_mapping button_mappings[BUTTON_MAX+1]; +}; + +struct zotac_device { + struct hid_device *hdev; + struct input_dev *wheel_input; + struct input_dev *mouse_input; + struct zotac_gamepad *gamepad; + struct usb_device *udev; + struct zotac_cfg_data *cfg_data; + struct zotac_rgb_dev *led_rgb_dev; + struct zotac_rgb_data led_rgb_data; +}; +extern struct zotac_device zotac; + +void zotac_init_input_device(struct input_dev *input_dev, + struct hid_device *hdev, const char *name); + +struct usb_interface *zotac_get_usb_interface(struct hid_device *hdev); + +int zotac_init_gamepad(struct zotac_device *zotac, struct usb_interface *intf); + +void zotac_process_gamepad_report(struct zotac_device *zotac, u8 *data, + int size); + +void zotac_cleanup_gamepad(struct zotac_device *zotac); + +void zotac_gamepad_send_button(struct zotac_device *zotac, int buttons[], + int num_buttons); + +int zotac_cfg_init(struct zotac_device *zotac); + +void zotac_cfg_cleanup(struct zotac_device *zotac); + +int zotac_register_sysfs(struct zotac_device *zotac); + +void zotac_unregister_sysfs(struct zotac_device *zotac); + +int zotac_send_get_command(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *req_data, size_t req_data_len, + u8 *output_data, size_t *output_len); + +int zotac_send_set_command(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *data, size_t data_len); + +int zotac_send_get_byte(struct zotac_device *zotac, u8 cmd_code, u8 setting, + const u8 *req_data, size_t req_data_len); + +/* RGB LED functions */ +int zotac_rgb_init(struct zotac_device *zotac); +void zotac_rgb_cleanup(struct zotac_device *zotac); +void zotac_rgb_resume(struct zotac_device *zotac); +void zotac_rgb_suspend(struct zotac_device *zotac); + +#endif /* __HID_ZOTAC_ZONE_H */ diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 157678b821fc..abe0ff79c6a6 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -2170,6 +2170,17 @@ config SENSORS_SCH5636 This driver can also be built as a module. If so, the module will be called sch5636. +config SENSORS_STEAMDECK + tristate "Steam Deck EC sensors" + depends on MFD_STEAMDECK + help + If you say yes here you get support for the hardware + monitoring features exposed by EC firmware on Steam Deck + devices + + This driver can also be built as a module. If so, the module + will be called steamdeck-hwmon. + config SENSORS_STTS751 tristate "ST Microelectronics STTS751" depends on I2C diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index eade8e3b1bde..e63ff2f45903 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -219,6 +219,7 @@ obj-$(CONFIG_SENSORS_SMSC47B397)+= smsc47b397.o obj-$(CONFIG_SENSORS_SMSC47M1) += smsc47m1.o obj-$(CONFIG_SENSORS_SMSC47M192)+= smsc47m192.o obj-$(CONFIG_SENSORS_SPARX5) += sparx5-temp.o +obj-$(CONFIG_SENSORS_STEAMDECK) += steamdeck-hwmon.o obj-$(CONFIG_SENSORS_SPD5118) += spd5118.o obj-$(CONFIG_SENSORS_STTS751) += stts751.o obj-$(CONFIG_SENSORS_SURFACE_FAN)+= surface_fan.o diff --git a/drivers/hwmon/steamdeck-hwmon.c b/drivers/hwmon/steamdeck-hwmon.c new file mode 100644 index 000000000000..9d0a5471b181 --- /dev/null +++ b/drivers/hwmon/steamdeck-hwmon.c @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Steam Deck EC sensors driver + * + * Copyright (C) 2021-2022 Valve Corporation + */ + +#include +#include +#include + +#define STEAMDECK_HWMON_NAME "steamdeck-hwmon" + +struct steamdeck_hwmon { + struct acpi_device *adev; +}; + +static long +steamdeck_hwmon_get(struct steamdeck_hwmon *sd, const char *method) +{ + unsigned long long val; + if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle, + (char *)method, NULL, &val))) + return -EIO; + + return val; +} + +static int +steamdeck_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *out) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + + switch (type) { + case hwmon_curr: + if (attr != hwmon_curr_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "PDAM"); + if (*out < 0) + return *out; + break; + case hwmon_in: + if (attr != hwmon_in_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "PDVL"); + if (*out < 0) + return *out; + break; + case hwmon_temp: + if (attr != hwmon_temp_input) + return -EOPNOTSUPP; + + *out = steamdeck_hwmon_get(sd, "BATT"); + if (*out < 0) + return *out; + /* + * Assuming BATT returns deg C we need to mutiply it + * by 1000 to convert to mC + */ + *out *= 1000; + break; + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + *out = steamdeck_hwmon_get(sd, "FANR"); + if (*out < 0) + return *out; + break; + case hwmon_fan_target: + *out = steamdeck_hwmon_get(sd, "FSSR"); + if (*out < 0) + return *out; + break; + case hwmon_fan_fault: + *out = steamdeck_hwmon_get(sd, "FANC"); + if (*out < 0) + return *out; + /* + * FANC (Fan check): + * 0: Abnormal + * 1: Normal + */ + *out = !*out; + break; + default: + return -EOPNOTSUPP; + } + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +steamdeck_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + switch (type) { + /* + * These two aren't, strictly speaking, measured. EC + * firmware just reports what PD negotiation resulted + * in. + */ + case hwmon_curr: + *str = "PD Contract Current"; + break; + case hwmon_in: + *str = "PD Contract Voltage"; + break; + case hwmon_temp: + *str = "Battery Temp"; + break; + case hwmon_fan: + *str = "System Fan"; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +steamdeck_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + + if (type != hwmon_fan || + attr != hwmon_fan_target) + return -EOPNOTSUPP; + + val = clamp_val(val, 0, 7300); + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "FANS", val))) + return -EIO; + + return 0; +} + +static umode_t +steamdeck_hwmon_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + if (type == hwmon_fan && + attr == hwmon_fan_target) + return 0644; + + return 0444; +} + +static const struct hwmon_channel_info *steamdeck_hwmon_info[] = { + HWMON_CHANNEL_INFO(in, + HWMON_I_INPUT | HWMON_I_LABEL), + HWMON_CHANNEL_INFO(curr, + HWMON_C_INPUT | HWMON_C_LABEL), + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL | + HWMON_F_TARGET | HWMON_F_FAULT), + NULL +}; + +static const struct hwmon_ops steamdeck_hwmon_ops = { + .is_visible = steamdeck_hwmon_is_visible, + .read = steamdeck_hwmon_read, + .read_string = steamdeck_hwmon_read_string, + .write = steamdeck_hwmon_write, +}; + +static const struct hwmon_chip_info steamdeck_hwmon_chip_info = { + .ops = &steamdeck_hwmon_ops, + .info = steamdeck_hwmon_info, +}; + + +static ssize_t +steamdeck_hwmon_simple_store(struct device *dev, const char *buf, size_t count, + const char *method, + unsigned long upper_limit) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + unsigned long value; + + if (kstrtoul(buf, 10, &value) || value >= upper_limit) + return -EINVAL; + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + (char *)method, value))) + return -EIO; + + return count; +} + +static ssize_t +steamdeck_hwmon_simple_show(struct device *dev, char *buf, + const char *method) +{ + struct steamdeck_hwmon *sd = dev_get_drvdata(dev); + unsigned long value; + + value = steamdeck_hwmon_get(sd, method); + if (value < 0) + return value; + + return sprintf(buf, "%ld\n", value); +} + +#define STEAMDECK_HWMON_ATTR_RW(_name, _set_method, _get_method, \ + _upper_limit) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + return steamdeck_hwmon_simple_show(dev, buf, \ + _get_method); \ + } \ + static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + return steamdeck_hwmon_simple_store(dev, buf, count, \ + _set_method, \ + _upper_limit); \ + } \ + static DEVICE_ATTR_RW(_name) + +STEAMDECK_HWMON_ATTR_RW(max_battery_charge_level, "FCBL", "SFBL", 101); +STEAMDECK_HWMON_ATTR_RW(max_battery_charge_rate, "CHGR", "GCHR", 101); + +static struct attribute *steamdeck_hwmon_attributes[] = { + &dev_attr_max_battery_charge_level.attr, + &dev_attr_max_battery_charge_rate.attr, + NULL +}; + +static const struct attribute_group steamdeck_hwmon_group = { + .attrs = steamdeck_hwmon_attributes, +}; + +static const struct attribute_group *steamdeck_hwmon_groups[] = { + &steamdeck_hwmon_group, + NULL +}; + +static int steamdeck_hwmon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct steamdeck_hwmon *sd; + struct device *hwmon; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + + sd->adev = ACPI_COMPANION(dev->parent); + hwmon = devm_hwmon_device_register_with_info(dev, + "steamdeck_hwmon", + sd, + &steamdeck_hwmon_chip_info, + steamdeck_hwmon_groups); + if (IS_ERR(hwmon)) { + dev_err(dev, "Failed to register HWMON device"); + return PTR_ERR(hwmon); + } + + return 0; +} + +static const struct platform_device_id steamdeck_hwmon_id_table[] = { + { .name = STEAMDECK_HWMON_NAME }, + {} +}; +MODULE_DEVICE_TABLE(platform, steamdeck_hwmon_id_table); + +static struct platform_driver steamdeck_hwmon_driver = { + .probe = steamdeck_hwmon_probe, + .driver = { + .name = STEAMDECK_HWMON_NAME, + }, + .id_table = steamdeck_hwmon_id_table, +}; +module_platform_driver(steamdeck_hwmon_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck EC sensors driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/input/joystick/xpad.c b/drivers/input/joystick/xpad.c index 363d50949386..0944f058b1e6 100644 --- a/drivers/input/joystick/xpad.c +++ b/drivers/input/joystick/xpad.c @@ -359,7 +359,9 @@ static const struct xpad_device { { 0x1bad, 0xfa01, "MadCatz GamePad", 0, XTYPE_XBOX360 }, { 0x1bad, 0xfd00, "Razer Onza TE", 0, XTYPE_XBOX360 }, { 0x1bad, 0xfd01, "Razer Onza", 0, XTYPE_XBOX360 }, + #if !IS_REACHABLE(CONFIG_ZOTAC_ZONE_HID) { 0x1ee9, 0x1590, "ZOTAC Gaming Zone", 0, XTYPE_XBOX360 }, + #endif /* !IS_REACHABLE(CONFIG_ZOTAC_ZONE_HID) */ { 0x20d6, 0x2001, "BDA Xbox Series X Wired Controller", 0, XTYPE_XBOXONE }, { 0x20d6, 0x2009, "PowerA Enhanced Wired Controller for Xbox Series X|S", 0, XTYPE_XBOXONE }, { 0x20d6, 0x2064, "PowerA Wired Controller for Xbox", MAP_SHARE_BUTTON, XTYPE_XBOXONE }, @@ -561,7 +563,9 @@ static const struct usb_device_id xpad_table[] = { XPAD_XBOX360_VENDOR(0x1949), /* Amazon controllers */ XPAD_XBOX360_VENDOR(0x1a86), /* Nanjing Qinheng Microelectronics (WCH) */ XPAD_XBOX360_VENDOR(0x1bad), /* Harmonix Rock Band guitar and drums */ + #if !IS_REACHABLE(CONFIG_ZOTAC_ZONE_HID) XPAD_XBOX360_VENDOR(0x1ee9), /* ZOTAC Technology Limited */ + #endif /* !IS_REACHABLE(CONFIG_ZOTAC_ZONE_HID) */ XPAD_XBOX360_VENDOR(0x20d6), /* PowerA controllers */ XPAD_XBOXONE_VENDOR(0x20d6), /* PowerA controllers */ XPAD_XBOX360_VENDOR(0x2345), /* Machenike Controllers */ diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 11e7282dc297..65395dfab3f2 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -1016,6 +1016,13 @@ config LEDS_ACER_A500 This option enables support for the Power Button LED of Acer Iconia Tab A500. +config LEDS_STEAMDECK + tristate "LED support for Steam Deck" + depends on LEDS_CLASS && MFD_STEAMDECK + help + This option enabled support for the status LED (next to the + power button) on Steam Deck + source "drivers/leds/blink/Kconfig" comment "Flash and Torch LED drivers" diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 9a0333ec1a86..e93fa6d6d2b9 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -86,6 +86,7 @@ obj-$(CONFIG_LEDS_QNAP_MCU) += leds-qnap-mcu.o obj-$(CONFIG_LEDS_REGULATOR) += leds-regulator.o obj-$(CONFIG_LEDS_SC27XX_BLTC) += leds-sc27xx-bltc.o obj-$(CONFIG_LEDS_ST1202) += leds-st1202.o +obj-$(CONFIG_LEDS_STEAMDECK) += leds-steamdeck.o obj-$(CONFIG_LEDS_SUN50I_A100) += leds-sun50i-a100.o obj-$(CONFIG_LEDS_SUNFIRE) += leds-sunfire.o obj-$(CONFIG_LEDS_SYSCON) += leds-syscon.o diff --git a/drivers/leds/leds-steamdeck.c b/drivers/leds/leds-steamdeck.c new file mode 100644 index 000000000000..3d56ec4403cd --- /dev/null +++ b/drivers/leds/leds-steamdeck.c @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * Steam Deck EC MFD LED cell driver + * + * Copyright (C) 2021-2022 Valve Corporation + * + */ + +#include +#include +#include + +struct steamdeck_led { + struct acpi_device *adev; + struct led_classdev cdev; +}; + +static ssize_t led_brightness_multiplier_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct steamdeck_led *sd = container_of(cdev, struct steamdeck_led, + cdev); + unsigned long long led_brightness_multiplier; + + if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle, + "GLDM", + NULL, + &led_brightness_multiplier))) + return -EIO; + + + return sprintf(buf, "%llu", led_brightness_multiplier); +} + +static ssize_t led_brightness_multiplier_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct steamdeck_led *sd = container_of(cdev, struct steamdeck_led, + cdev); + unsigned long value; + + if (kstrtoul(buf, 10, &value) || value > 100) + return -EINVAL; + + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "SLDM", value))) + return -EIO; + + + return count; +} + +static DEVICE_ATTR_RW(led_brightness_multiplier); + +static struct attribute *steamdeck_led_attrs[] = { + &dev_attr_led_brightness_multiplier.attr, + NULL +}; +ATTRIBUTE_GROUPS(steamdeck_led); + +static int steamdeck_leds_brightness_set(struct led_classdev *cdev, + enum led_brightness value) +{ + struct steamdeck_led *sd = container_of(cdev, struct steamdeck_led, + cdev); + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "CHBV", value))) + return -EIO; + + return 0; +} + +static int steamdeck_leds_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct steamdeck_led *sd; + int ret; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + + sd->adev = ACPI_COMPANION(dev->parent); + + sd->cdev.name = "status:white"; + sd->cdev.brightness_set_blocking = steamdeck_leds_brightness_set; + sd->cdev.max_brightness = 100; + sd->cdev.groups = steamdeck_led_groups; + + ret = devm_led_classdev_register(dev, &sd->cdev); + if (ret) { + dev_err(dev, "Failed to register LEDs device: %d\n", ret); + return ret; + } + + return 0; +} + +static const struct platform_device_id steamdeck_leds_id_table[] = { + { .name = "steamdeck-leds" }, + {} +}; +MODULE_DEVICE_TABLE(platform, steamdeck_leds_id_table); + +static struct platform_driver steamdeck_leds_driver = { + .probe = steamdeck_leds_probe, + .driver = { + .name = "steamdeck-leds", + }, + .id_table = steamdeck_leds_id_table, +}; +module_platform_driver(steamdeck_leds_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck LEDs driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index aace5766b38a..6e2ada9bdab4 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2566,5 +2566,16 @@ config MFD_MAX7360 additional drivers must be enabled in order to use the functionality of the device. +config MFD_STEAMDECK + tristate "Valve Steam Deck" + select MFD_CORE + depends on ACPI + depends on X86_64 || COMPILE_TEST + help + This driver registers various MFD cells that expose aspects + of Steam Deck specific ACPI functionality. + + Say N here, unless you are running on Steam Deck hardware. + endmenu endif diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index e75e8045c28a..5fc48aa2bab1 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -301,6 +301,8 @@ obj-$(CONFIG_MFD_QNAP_MCU) += qnap-mcu.o obj-$(CONFIG_MFD_RSMU_I2C) += rsmu_i2c.o rsmu_core.o obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o +obj-$(CONFIG_MFD_STEAMDECK) += steamdeck.o + obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o obj-$(CONFIG_MFD_LOONGSON_SE) += loongson-se.o diff --git a/drivers/mfd/steamdeck.c b/drivers/mfd/steamdeck.c new file mode 100644 index 000000000000..a60fa7db9141 --- /dev/null +++ b/drivers/mfd/steamdeck.c @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* + * Steam Deck EC MFD core driver + * + * Copyright (C) 2021-2022 Valve Corporation + * + */ + +#include +#include +#include + +#define STEAMDECK_STA_OK \ + (ACPI_STA_DEVICE_ENABLED | \ + ACPI_STA_DEVICE_PRESENT | \ + ACPI_STA_DEVICE_FUNCTIONING) + +struct steamdeck { + struct acpi_device *adev; + struct device *dev; +}; + +#define STEAMDECK_ATTR_RO(_name, _method) \ + static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ + { \ + struct steamdeck *sd = dev_get_drvdata(dev); \ + unsigned long long val; \ + \ + if (ACPI_FAILURE(acpi_evaluate_integer( \ + sd->adev->handle, \ + _method, NULL, &val))) \ + return -EIO; \ + \ + return sysfs_emit(buf, "%llu\n", val); \ + } \ + static DEVICE_ATTR_RO(_name) + +STEAMDECK_ATTR_RO(firmware_version, "PDFW"); +STEAMDECK_ATTR_RO(board_id, "BOID"); + +static ssize_t controller_board_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct steamdeck *sd = dev_get_drvdata(dev); + bool enabled; + ssize_t ret = kstrtobool(buf, &enabled); + + if (ret) + return ret; + + if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle, + "SCBP", enabled))) + return -EIO; + + return count; +} +static DEVICE_ATTR_WO(controller_board_power); + +static struct attribute *steamdeck_attrs[] = { + &dev_attr_firmware_version.attr, + &dev_attr_board_id.attr, + &dev_attr_controller_board_power.attr, + NULL +}; + +ATTRIBUTE_GROUPS(steamdeck); + +static const struct mfd_cell steamdeck_cells[] = { + { .name = "steamdeck-hwmon" }, + { .name = "steamdeck-leds" }, + { .name = "steamdeck-extcon" }, +}; + +static void steamdeck_remove_sysfs_groups(void *data) +{ + struct steamdeck *sd = data; + + sysfs_remove_groups(&sd->dev->kobj, steamdeck_groups); +} + +static int steamdeck_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + unsigned long long sta; + struct steamdeck *sd; + acpi_status status; + int ret; + + sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL); + if (!sd) + return -ENOMEM; + sd->adev = ACPI_COMPANION(dev); + sd->dev = dev; + platform_set_drvdata(pdev, sd); + + status = acpi_evaluate_integer(sd->adev->handle, "_STA", + NULL, &sta); + if (ACPI_FAILURE(status)) { + dev_err(dev, "Status check failed (0x%x)\n", status); + return -EINVAL; + } + + if ((sta & STEAMDECK_STA_OK) != STEAMDECK_STA_OK) { + dev_err(dev, "Device is not ready\n"); + return -EINVAL; + } + + ret = sysfs_create_groups(&dev->kobj, steamdeck_groups); + if (ret) { + dev_err(dev, "Failed to create sysfs group\n"); + return ret; + } + + ret = devm_add_action_or_reset(dev, steamdeck_remove_sysfs_groups, + sd); + if (ret) { + dev_err(dev, "Failed to register devres action\n"); + return ret; + } + + return devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, + steamdeck_cells, ARRAY_SIZE(steamdeck_cells), + NULL, 0, NULL); +} + +static const struct acpi_device_id steamdeck_device_ids[] = { + { "VLV0100", 0 }, + { "", 0 }, +}; +MODULE_DEVICE_TABLE(acpi, steamdeck_device_ids); + +static struct platform_driver steamdeck_driver = { + .probe = steamdeck_probe, + .driver = { + .name = "steamdeck", + .acpi_match_table = steamdeck_device_ids, + }, +}; +module_platform_driver(steamdeck_driver); + +MODULE_AUTHOR("Andrey Smirnov "); +MODULE_DESCRIPTION("Steam Deck EC MFD core driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/net/wireless/ath/ath11k/core.c b/drivers/net/wireless/ath/ath11k/core.c index 812686173ac8..8963cd8477db 100644 --- a/drivers/net/wireless/ath/ath11k/core.c +++ b/drivers/net/wireless/ath/ath11k/core.c @@ -734,7 +734,7 @@ static const struct ath11k_hw_params ath11k_hw_params[] = { .name = "qca2066 hw2.1", .hw_rev = ATH11K_HW_QCA2066_HW21, .fw = { - .dir = "QCA2066/hw2.1", + .dir = "QCA206X/hw2.1", .board_size = 256 * 1024, .cal_offset = 128 * 1024, }, diff --git a/drivers/net/wireless/ath/ath11k/mhi.c b/drivers/net/wireless/ath/ath11k/mhi.c index acd76e9392d3..c5dc776b2364 100644 --- a/drivers/net/wireless/ath/ath11k/mhi.c +++ b/drivers/net/wireless/ath/ath11k/mhi.c @@ -460,12 +460,12 @@ void ath11k_mhi_stop(struct ath11k_pci *ab_pci, bool is_suspend) * workaround, otherwise ath11k_core_resume() will timeout * during resume. */ - if (is_suspend) + if (is_suspend) { mhi_power_down_keep_dev(ab_pci->mhi_ctrl, true); - else + } else { mhi_power_down(ab_pci->mhi_ctrl, true); - - mhi_unprepare_after_power_down(ab_pci->mhi_ctrl); + mhi_unprepare_after_power_down(ab_pci->mhi_ctrl); + } } int ath11k_mhi_suspend(struct ath11k_pci *ab_pci) diff --git a/drivers/net/wireless/ath/ath12k/mhi.c b/drivers/net/wireless/ath/ath12k/mhi.c index 08f44baf182a..3af524ccf4a5 100644 --- a/drivers/net/wireless/ath/ath12k/mhi.c +++ b/drivers/net/wireless/ath/ath12k/mhi.c @@ -601,6 +601,12 @@ static int ath12k_mhi_set_state(struct ath12k_pci *ab_pci, ath12k_mhi_set_state_bit(ab_pci, mhi_state); + /* mhi_power_down_keep_dev() has been updated to DEINIT without + * freeing bhie tables + */ + if (mhi_state == ATH12K_MHI_POWER_OFF_KEEP_DEV) + ath12k_mhi_set_state_bit(ab_pci, ATH12K_MHI_DEINIT); + return 0; out: @@ -635,12 +641,12 @@ void ath12k_mhi_stop(struct ath12k_pci *ab_pci, bool is_suspend) * workaround, otherwise ath12k_core_resume() will timeout * during resume. */ - if (is_suspend) + if (is_suspend) { ath12k_mhi_set_state(ab_pci, ATH12K_MHI_POWER_OFF_KEEP_DEV); - else + } else { ath12k_mhi_set_state(ab_pci, ATH12K_MHI_POWER_OFF); - - ath12k_mhi_set_state(ab_pci, ATH12K_MHI_DEINIT); + ath12k_mhi_set_state(ab_pci, ATH12K_MHI_DEINIT); + } } void ath12k_mhi_suspend(struct ath12k_pci *ab_pci) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 4cb7d97a9fcc..e8c24c08c2dd 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -552,9 +552,12 @@ config MSI_WMI config MSI_WMI_PLATFORM tristate "MSI WMI Platform features" + depends on ACPI_BATTERY depends on ACPI_WMI depends on DMI depends on HWMON + select ACPI_PLATFORM_PROFILE + select FW_ATTR_CLASS help Say Y here if you want to have support for WMI-based platform features like fan sensor access on MSI machines. @@ -1043,6 +1046,16 @@ config OXP_EC source "drivers/platform/x86/tuxedo/Kconfig" +config ZOTAC_ZONE_PLATFORM + tristate "Zotac Zone platform driver" + select FW_ATTR_CLASS + select ACPI_PLATFORM_PROFILE + help + Support for fans and PPT on Zotac Zone devices. + + To compile this driver as a module, choose M here: the module + will be called zotac-zone. + endif # X86_PLATFORM_DEVICES config P2SB diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile index d25762f7114f..1e41b8a1dd28 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile @@ -167,5 +167,7 @@ obj-$(CONFIG_WINMATE_FM07_KEYS) += winmate-fm07-keys.o # SEL obj-$(CONFIG_SEL3350_PLATFORM) += sel3350-platform.o +obj-$(CONFIG_ZOTAC_ZONE_PLATFORM) += zotac-zone-platform.o + # OneXPlayer obj-$(CONFIG_OXP_EC) += oxpec.o diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c index 4aec7ec69250..2bb6459d9b6e 100644 --- a/drivers/platform/x86/asus-wmi.c +++ b/drivers/platform/x86/asus-wmi.c @@ -3953,7 +3953,7 @@ static int asus_wmi_platform_profile_get(struct device *dev, *profile = PLATFORM_PROFILE_PERFORMANCE; break; case ASUS_THROTTLE_THERMAL_POLICY_SILENT: - *profile = PLATFORM_PROFILE_QUIET; + *profile = PLATFORM_PROFILE_LOW_POWER; break; default: return -EINVAL; @@ -3977,7 +3977,7 @@ static int asus_wmi_platform_profile_set(struct device *dev, case PLATFORM_PROFILE_BALANCED: tp = ASUS_THROTTLE_THERMAL_POLICY_DEFAULT; break; - case PLATFORM_PROFILE_QUIET: + case PLATFORM_PROFILE_LOW_POWER: tp = ASUS_THROTTLE_THERMAL_POLICY_SILENT; break; default: @@ -3990,7 +3990,7 @@ static int asus_wmi_platform_profile_set(struct device *dev, static int asus_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) { - set_bit(PLATFORM_PROFILE_QUIET, choices); + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); set_bit(PLATFORM_PROFILE_BALANCED, choices); set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c index e912fcc12d12..b40e15ac71cf 100644 --- a/drivers/platform/x86/msi-wmi-platform.c +++ b/drivers/platform/x86/msi-wmi-platform.c @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Linux driver for WMI platform features on MSI notebooks. + * Linux driver for WMI platform features on MSI notebooks and handhelds. * - * Copyright (C) 2024 Armin Wolf + * Copyright (C) 2024-2025 Armin Wolf + * Copyright (C) 2025 Antheas Kapenekakis */ #define pr_format(fmt) KBUILD_MODNAME ": " fmt @@ -16,26 +17,38 @@ #include #include #include +#include +#include #include +#include #include +#include +#include #include #include #include #include +#include +#include #include #include +#include #include +#include "firmware_attributes_class.h" + #define DRIVER_NAME "msi-wmi-platform" #define MSI_PLATFORM_GUID "ABBC0F6E-8EA1-11D1-00A0-C90629100000" #define MSI_WMI_PLATFORM_INTERFACE_VERSION 2 +/* Get_WMI() WMI method */ #define MSI_PLATFORM_WMI_MAJOR_OFFSET 1 #define MSI_PLATFORM_WMI_MINOR_OFFSET 2 +/* Get_EC() and Set_EC() WMI methods */ #define MSI_PLATFORM_EC_FLAGS_OFFSET 1 #define MSI_PLATFORM_EC_MINOR_MASK GENMASK(3, 0) #define MSI_PLATFORM_EC_MAJOR_MASK GENMASK(5, 4) @@ -43,6 +56,33 @@ #define MSI_PLATFORM_EC_IS_TIGERLAKE BIT(7) #define MSI_PLATFORM_EC_VERSION_OFFSET 2 +/* Get_Fan() and Set_Fan() WMI methods */ +#define MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED 0x0 +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE 0x1 +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE 0x2 +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE 0x1 +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE 0x2 + +/* Get_AP() and Set_AP() WMI methods */ +#define MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE 0x1 +#define MSI_PLATFORM_AP_FAN_FLAGS_OFFSET 1 +#define MSI_PLATFORM_AP_ENABLE_FAN_TABLES BIT(7) + +/* Get_Data() and Set_Data() Shift Mode Register */ +#define MSI_PLATFORM_SHIFT_ADDR 0xd2 +#define MSI_PLATFORM_SHIFT_DISABLE BIT(7) +#define MSI_PLATFORM_SHIFT_ENABLE (BIT(7) | BIT(6)) +#define MSI_PLATFORM_SHIFT_SPORT (MSI_PLATFORM_SHIFT_ENABLE + 4) +#define MSI_PLATFORM_SHIFT_COMFORT (MSI_PLATFORM_SHIFT_ENABLE + 0) +#define MSI_PLATFORM_SHIFT_GREEN (MSI_PLATFORM_SHIFT_ENABLE + 1) +#define MSI_PLATFORM_SHIFT_ECO (MSI_PLATFORM_SHIFT_ENABLE + 2) +#define MSI_PLATFORM_SHIFT_USER (MSI_PLATFORM_SHIFT_ENABLE + 3) + +/* Get_Data() and Set_Data() Params */ +#define MSI_PLATFORM_PL1_ADDR 0x50 +#define MSI_PLATFORM_PL2_ADDR 0x51 +#define MSI_PLATFORM_BAT_ADDR 0xd7 + static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); @@ -79,9 +119,68 @@ enum msi_wmi_platform_method { MSI_PLATFORM_GET_WMI = 0x1d, }; +struct msi_wmi_platform_quirk { + bool shift_mode; /* Shift mode is supported */ + bool charge_threshold; /* Charge threshold is supported */ + bool dual_fans; /* For devices with two hwmon fans */ + bool restore_curves; /* Restore factory curves on unload */ + int pl_min; /* Minimum PLx value */ + int pl1_max; /* Maximum PL1 value */ + int pl2_max; /* Maximum PL2 value */ +}; + +struct msi_wmi_platform_factory_curves { + u8 cpu_fan_table[32]; + u8 gpu_fan_table[32]; + u8 cpu_temp_table[32]; + u8 gpu_temp_table[32]; +}; + struct msi_wmi_platform_data { struct wmi_device *wdev; + struct msi_wmi_platform_quirk *quirks; struct mutex wmi_lock; /* Necessary when calling WMI methods */ + struct device *ppdev; + struct msi_wmi_platform_factory_curves factory_curves; + struct acpi_battery_hook battery_hook; + struct device *fw_attrs_dev; + struct kset *fw_attrs_kset; +}; + +enum msi_fw_attr_id { + MSI_ATTR_PPT_PL1_SPL, + MSI_ATTR_PPT_PL2_SPPT, +}; + +static const char *const msi_fw_attr_name[] = { + [MSI_ATTR_PPT_PL1_SPL] = "ppt_pl1_spl", + [MSI_ATTR_PPT_PL2_SPPT] = "ppt_pl2_sppt", +}; + +static const char *const msi_fw_attr_desc[] = { + [MSI_ATTR_PPT_PL1_SPL] = "CPU Steady package limit (PL1/SPL)", + [MSI_ATTR_PPT_PL2_SPPT] = "CPU Boost slow package limit (PL2/SPPT)", +}; + +#define MSI_ATTR_LANGUAGE_CODE "en_US.UTF-8" + +struct msi_fw_attr { + struct msi_wmi_platform_data *data; + enum msi_fw_attr_id fw_attr_id; + struct attribute_group attr_group; + struct kobj_attribute display_name; + struct kobj_attribute current_value; + struct kobj_attribute min_value; + struct kobj_attribute max_value; + + u32 min; + u32 max; + + int (*get_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf); + ssize_t (*set_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, const char *buf, + size_t count); }; struct msi_wmi_platform_debugfs_data { @@ -124,6 +223,53 @@ static const char * const msi_wmi_platform_debugfs_names[] = { "get_wmi" }; +static struct msi_wmi_platform_quirk quirk_default = {}; +static struct msi_wmi_platform_quirk quirk_gen1 = { + .shift_mode = true, + .charge_threshold = true, + .dual_fans = true, + .restore_curves = true, + .pl_min = 8, + .pl1_max = 43, + .pl2_max = 45 +}; +static struct msi_wmi_platform_quirk quirk_gen2 = { + .shift_mode = true, + .charge_threshold = true, + .dual_fans = true, + .restore_curves = true, + .pl_min = 8, + .pl1_max = 30, + .pl2_max = 37 +}; + +static const struct dmi_system_id msi_quirks[] = { + { + .ident = "MSI Claw (gen 1)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T41"), + }, + .driver_data = &quirk_gen1, + }, + { + .ident = "MSI Claw AI+ 7", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T42"), + }, + .driver_data = &quirk_gen2, + }, + { + .ident = "MSI Claw AI+ 8", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."), + DMI_MATCH(DMI_BOARD_NAME, "MS-1T52"), + }, + .driver_data = &quirk_gen2, + }, +}; + static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, size_t length) { if (obj->type != ACPI_TYPE_BUFFER) @@ -140,45 +286,332 @@ static int msi_wmi_platform_parse_buffer(union acpi_object *obj, u8 *output, siz return 0; } -static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, - enum msi_wmi_platform_method method, u8 *input, - size_t input_length, u8 *output, size_t output_length) +static int msi_wmi_platform_query_unlocked(struct msi_wmi_platform_data *data, + enum msi_wmi_platform_method method, u8 *buffer, + size_t length) { struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; struct acpi_buffer in = { - .length = input_length, - .pointer = input + .length = length, + .pointer = buffer }; union acpi_object *obj; acpi_status status; int ret; - if (!input_length || !output_length) + if (!length) return -EINVAL; + status = wmidev_evaluate_method(data->wdev, 0x0, method, &in, &out); + if (ACPI_FAILURE(status)) + return -EIO; + + obj = out.pointer; + if (!obj) + return -ENODATA; + + ret = msi_wmi_platform_parse_buffer(obj, buffer, length); + kfree(obj); + + return ret; +} + +static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, + enum msi_wmi_platform_method method, u8 *buffer, + size_t length) +{ /* * The ACPI control method responsible for handling the WMI method calls * is not thread-safe. Because of this we have to do the locking ourself. */ scoped_guard(mutex, &data->wmi_lock) { - status = wmidev_evaluate_method(data->wdev, 0x0, method, &in, &out); - if (ACPI_FAILURE(status)) - return -EIO; + return msi_wmi_platform_query_unlocked(data, method, buffer, length); } +} - obj = out.pointer; - if (!obj) - return -ENODATA; +static ssize_t msi_wmi_platform_fan_table_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + u8 fan_percent; + int ret; - ret = msi_wmi_platform_parse_buffer(obj, output, output_length); - kfree(obj); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, sizeof(buffer)); + if (ret < 0) + return ret; - return ret; + fan_percent = buffer[sattr->index + 1]; + if (fan_percent > 100) + return -EIO; + + return sysfs_emit(buf, "%d\n", fixp_linear_interpolate(0, 0, 100, 255, fan_percent)); +} + +static ssize_t msi_wmi_platform_fan_table_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + long speed; + int ret; + + ret = kstrtol(buf, 10, &speed); + if (ret < 0) + return ret; + + speed = clamp_val(speed, 0, 255); + + guard(mutex)(&data->wmi_lock); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = sattr->nr; + buffer[sattr->index + 1] = fixp_linear_interpolate(0, 0, 255, 100, speed); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t msi_wmi_platform_temp_table_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + u8 temp_c; + int ret; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + temp_c = buffer[sattr->index + 1]; + + return sysfs_emit(buf, "%d\n", temp_c); +} + +static ssize_t msi_wmi_platform_temp_table_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { sattr->nr }; + long temp_c; + int ret; + + ret = kstrtol(buf, 10, &temp_c); + if (ret < 0) + return ret; + + temp_c = clamp_val(temp_c, 0, 255); + + guard(mutex)(&data->wmi_lock); + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = sattr->nr; + buffer[sattr->index + 1] = temp_c; + + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_TEMPERATURE, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return count; +} + +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x0); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x6); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x1); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x2); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x6); + +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x0); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x6); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp, msi_wmi_platform_temp_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x7); + +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x1); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x2); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x3); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x4); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x5); +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_pwm, msi_wmi_platform_fan_table, + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x6); + +static struct attribute *msi_wmi_platform_hwmon_attrs[] = { + &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point6_temp.dev_attr.attr, + + &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm1_auto_point6_pwm.dev_attr.attr, + + &sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point3_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point4_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point5_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point6_temp.dev_attr.attr, + + &sensor_dev_attr_pwm2_auto_point1_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point2_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point3_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point4_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point5_pwm.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point6_pwm.dev_attr.attr, + NULL +}; +ATTRIBUTE_GROUPS(msi_wmi_platform_hwmon); + +static int msi_wmi_platform_curves_save(struct msi_wmi_platform_data *data) +{ + int ret; + + data->factory_curves.cpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_FAN, + data->factory_curves.cpu_fan_table, + sizeof(data->factory_curves.cpu_fan_table)); + if (ret < 0) + return ret; + data->factory_curves.cpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE; + + data->factory_curves.gpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_FAN, + data->factory_curves.gpu_fan_table, + sizeof(data->factory_curves.gpu_fan_table)); + if (ret < 0) + return ret; + data->factory_curves.gpu_fan_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE; + + data->factory_curves.cpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_TEMPERATURE, + data->factory_curves.cpu_temp_table, + sizeof(data->factory_curves.cpu_temp_table)); + if (ret < 0) + return ret; + data->factory_curves.cpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE; + + data->factory_curves.gpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_TEMPERATURE, + data->factory_curves.gpu_temp_table, + sizeof(data->factory_curves.gpu_temp_table)); + if (ret < 0) + return ret; + data->factory_curves.gpu_temp_table[0] = + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE; + + return 0; } +static int msi_wmi_platform_curves_load(struct msi_wmi_platform_data *data) +{ + u8 buffer[32] = { }; + int ret; + + memcpy(buffer, data->factory_curves.cpu_fan_table, + sizeof(data->factory_curves.cpu_fan_table)); + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.gpu_fan_table, + sizeof(data->factory_curves.gpu_fan_table)); + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, + buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.cpu_temp_table, + sizeof(data->factory_curves.cpu_temp_table)); + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_TEMPERATURE, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + memcpy(buffer, data->factory_curves.gpu_temp_table, + sizeof(data->factory_curves.gpu_temp_table)); + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_TEMPERATURE, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + return 0; +} + + static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { + if (type == hwmon_pwm && attr == hwmon_pwm_enable) + return 0644; + return 0444; } @@ -186,28 +619,114 @@ static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types typ int channel, long *val) { struct msi_wmi_platform_data *data = dev_get_drvdata(dev); - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; u16 value; + u8 flags; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, input, sizeof(input), output, - sizeof(output)); - if (ret < 0) - return ret; + switch (type) { + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + buffer[0] = MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED; + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + value = get_unaligned_be16(&buffer[channel * 2 + 1]); + if (!value) + *val = 0; + else + *val = 480000 / value; + + return 0; + default: + return -EOPNOTSUPP; + } + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_AP, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + flags = buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET]; + if (flags & MSI_PLATFORM_AP_ENABLE_FAN_TABLES) + *val = 1; + else + *val = 2; + + return 0; + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } +} - value = get_unaligned_be16(&output[channel * 2 + 1]); - if (!value) - *val = 0; - else - *val = 480000 / value; +static int msi_wmi_platform_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long val) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { }; + int ret; - return 0; + guard(mutex)(&data->wmi_lock); + + switch (type) { + case hwmon_pwm: + switch (attr) { + case hwmon_pwm_enable: + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_GET_AP, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; + switch (val) { + case 1: + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] |= + MSI_PLATFORM_AP_ENABLE_FAN_TABLES; + break; + case 2: + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] &= + ~MSI_PLATFORM_AP_ENABLE_FAN_TABLES; + break; + default: + return -EINVAL; + } + + ret = msi_wmi_platform_query_unlocked( + data, MSI_PLATFORM_SET_AP, buffer, + sizeof(buffer)); + if (ret < 0) + return ret; + + if (val == 2 && data->quirks->restore_curves) { + ret = msi_wmi_platform_curves_load(data); + if (ret < 0) + return ret; + } + + return 0; + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } } static const struct hwmon_ops msi_wmi_platform_ops = { .is_visible = msi_wmi_platform_is_visible, .read = msi_wmi_platform_read, + .write = msi_wmi_platform_write, }; static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { @@ -217,6 +736,10 @@ static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { HWMON_F_INPUT, HWMON_F_INPUT ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_ENABLE, + HWMON_PWM_ENABLE + ), NULL }; @@ -225,8 +748,497 @@ static const struct hwmon_chip_info msi_wmi_platform_chip_info = { .info = msi_wmi_platform_info, }; -static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, size_t length, - loff_t *offset) +static const struct hwmon_channel_info * const msi_wmi_platform_info_dual[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT, + HWMON_F_INPUT + ), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_ENABLE, + HWMON_PWM_ENABLE + ), + NULL +}; + +static const struct hwmon_chip_info msi_wmi_platform_chip_info_dual = { + .ops = &msi_wmi_platform_ops, + .info = msi_wmi_platform_info_dual, +}; + +static int msi_wmi_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + return 0; +} + +static int msi_wmi_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + int ret; + + u8 buffer[32] = { }; + + buffer[0] = MSI_PLATFORM_SHIFT_ADDR; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_DATA, buffer, sizeof(buffer)); + if (ret < 0) + return ret; + + if (buffer[0] != 1) + return -EINVAL; + + switch (buffer[1]) { + case MSI_PLATFORM_SHIFT_SPORT: + *profile = PLATFORM_PROFILE_PERFORMANCE; + return 0; + case MSI_PLATFORM_SHIFT_COMFORT: + *profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE; + return 0; + case MSI_PLATFORM_SHIFT_GREEN: + *profile = PLATFORM_PROFILE_BALANCED; + return 0; + case MSI_PLATFORM_SHIFT_ECO: + *profile = PLATFORM_PROFILE_LOW_POWER; + return 0; + case MSI_PLATFORM_SHIFT_USER: + *profile = PLATFORM_PROFILE_CUSTOM; + return 0; + default: + return -EINVAL; + } +} + +static int msi_wmi_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); + u8 buffer[32] = { }; + + buffer[0] = MSI_PLATFORM_SHIFT_ADDR; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + buffer[1] = MSI_PLATFORM_SHIFT_SPORT; + break; + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + buffer[1] = MSI_PLATFORM_SHIFT_COMFORT; + break; + case PLATFORM_PROFILE_BALANCED: + buffer[1] = MSI_PLATFORM_SHIFT_GREEN; + break; + case PLATFORM_PROFILE_LOW_POWER: + buffer[1] = MSI_PLATFORM_SHIFT_ECO; + break; + case PLATFORM_PROFILE_CUSTOM: + buffer[1] = MSI_PLATFORM_SHIFT_USER; + break; + default: + return -EINVAL; + } + + return msi_wmi_platform_query(data, MSI_PLATFORM_SET_DATA, buffer, sizeof(buffer)); +} + +static const struct platform_profile_ops msi_wmi_platform_profile_ops = { + .probe = msi_wmi_platform_profile_probe, + .profile_get = msi_wmi_platform_profile_get, + .profile_set = msi_wmi_platform_profile_set, +}; + +/* Firmware Attributes setup */ +static int data_get_addr(struct msi_wmi_platform_data *data, + const enum msi_fw_attr_id id) +{ + switch (id) { + case MSI_ATTR_PPT_PL1_SPL: + return MSI_PLATFORM_PL1_ADDR; + case MSI_ATTR_PPT_PL2_SPPT: + return MSI_PLATFORM_PL2_ADDR; + default: + pr_warn("Invalid attribute id %d\n", id); + return -EINVAL; + } +} + +static ssize_t data_set_value(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, const char *buf, + size_t count) +{ + u8 buffer[32] = { 0 }; + int ret, fwid; + u32 value; + + fwid = data_get_addr(data, fw_attr->fw_attr_id); + if (fwid < 0) + return fwid; + + ret = kstrtou32(buf, 10, &value); + if (ret) + return ret; + + if (fw_attr->min >= 0 && value < fw_attr->min) + return -EINVAL; + if (fw_attr->max >= 0 && value > fw_attr->max) + return -EINVAL; + + buffer[0] = fwid; + put_unaligned_le32(value, &buffer[1]); + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_SET_DATA, buffer, sizeof(buffer)); + if (ret) { + pr_warn("Failed to set_data with id %d: %d\n", + fw_attr->fw_attr_id, ret); + return ret; + } + + return count; +} + +static int data_get_value(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf) +{ + u8 buffer[32] = { 0 }; + u32 value; + int ret, addr; + + addr = data_get_addr(data, fw_attr->fw_attr_id); + if (addr < 0) + return addr; + + buffer[0] = addr; + + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_DATA, buffer, sizeof(buffer)); + if (ret) { + pr_warn("Failed to show set_data for id %d: %d\n", + fw_attr->fw_attr_id, ret); + return ret; + } + + value = get_unaligned_le32(&buffer[1]); + + return sysfs_emit(buf, "%d\n", value); +} + +static ssize_t display_name_language_code_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", MSI_ATTR_LANGUAGE_CODE); +} + +static struct kobj_attribute fw_attr_display_name_language_code = + __ATTR_RO(display_name_language_code); + +static ssize_t scalar_increment_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "1\n"); +} + +static struct kobj_attribute fw_attr_scalar_increment = + __ATTR_RO(scalar_increment); + +static ssize_t pending_reboot_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static struct kobj_attribute fw_attr_pending_reboot = __ATTR_RO(pending_reboot); + +static ssize_t display_name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, display_name); + + return sysfs_emit(buf, "%s\n", msi_fw_attr_desc[fw_attr->fw_attr_id]); +} + +static ssize_t current_value_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, current_value); + + return fw_attr->get_value(fw_attr->data, fw_attr, buf); +} + +static ssize_t current_value_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, current_value); + + return fw_attr->set_value(fw_attr->data, fw_attr, buf, count); +} + +static ssize_t type_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +static struct kobj_attribute fw_attr_type_int = { + .attr = { .name = "type", .mode = 0444 }, + .show = type_show, +}; + +static ssize_t min_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, min_value); + + return sysfs_emit(buf, "%d\n", fw_attr->min); +} + +static ssize_t max_value_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct msi_fw_attr *fw_attr = + container_of(attr, struct msi_fw_attr, max_value); + + return sysfs_emit(buf, "%d\n", fw_attr->max); +} + +#define FW_ATTR_ENUM_MAX_ATTRS 7 + +static int +msi_fw_attr_init(struct msi_wmi_platform_data *data, + const enum msi_fw_attr_id fw_attr_id, + struct kobj_attribute *fw_attr_type, const s32 min, + const s32 max, + int (*get_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, char *buf), + ssize_t (*set_value)(struct msi_wmi_platform_data *data, + struct msi_fw_attr *fw_attr, + const char *buf, size_t count)) +{ + struct msi_fw_attr *fw_attr; + struct attribute **attrs; + int idx = 0; + + fw_attr = devm_kzalloc(&data->wdev->dev, sizeof(*fw_attr), GFP_KERNEL); + if (!fw_attr) + return -ENOMEM; + + attrs = devm_kcalloc(&data->wdev->dev, FW_ATTR_ENUM_MAX_ATTRS + 1, + sizeof(*attrs), GFP_KERNEL); + if (!attrs) + return -ENOMEM; + + fw_attr->data = data; + fw_attr->fw_attr_id = fw_attr_id; + fw_attr->attr_group.name = msi_fw_attr_name[fw_attr_id]; + fw_attr->attr_group.attrs = attrs; + fw_attr->get_value = get_value; + fw_attr->set_value = set_value; + + attrs[idx++] = &fw_attr_type->attr; + if (fw_attr_type == &fw_attr_type_int) + attrs[idx++] = &fw_attr_scalar_increment.attr; + attrs[idx++] = &fw_attr_display_name_language_code.attr; + + sysfs_attr_init(&fw_attr->display_name.attr); + fw_attr->display_name.attr.name = "display_name"; + fw_attr->display_name.attr.mode = 0444; + fw_attr->display_name.show = display_name_show; + attrs[idx++] = &fw_attr->display_name.attr; + + sysfs_attr_init(&fw_attr->current_value.attr); + fw_attr->current_value.attr.name = "current_value"; + fw_attr->current_value.attr.mode = 0644; + fw_attr->current_value.show = current_value_show; + fw_attr->current_value.store = current_value_store; + attrs[idx++] = &fw_attr->current_value.attr; + + if (min >= 0) { + fw_attr->min = min; + sysfs_attr_init(&fw_attr->min_value.attr); + fw_attr->min_value.attr.name = "min_value"; + fw_attr->min_value.attr.mode = 0444; + fw_attr->min_value.show = min_value_show; + attrs[idx++] = &fw_attr->min_value.attr; + } else { + fw_attr->min = -1; + } + + if (max >= 0) { + fw_attr->max = max; + sysfs_attr_init(&fw_attr->max_value.attr); + fw_attr->max_value.attr.name = "max_value"; + fw_attr->max_value.attr.mode = 0444; + fw_attr->max_value.show = max_value_show; + attrs[idx++] = &fw_attr->max_value.attr; + } else { + fw_attr->max = -1; + } + + attrs[idx] = NULL; + return sysfs_create_group(&data->fw_attrs_kset->kobj, &fw_attr->attr_group); +} + +static void msi_kset_unregister(void *data) +{ + struct kset *kset = data; + + sysfs_remove_file(&kset->kobj, &fw_attr_pending_reboot.attr); + kset_unregister(kset); +} + +static void msi_fw_attrs_dev_unregister(void *data) +{ + struct device *fw_attrs_dev = data; + + device_unregister(fw_attrs_dev); +} + +static int msi_wmi_fw_attrs_init(struct msi_wmi_platform_data *data) +{ + int err; + + data->fw_attrs_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(data->fw_attrs_dev)) + return PTR_ERR(data->fw_attrs_dev); + + err = devm_add_action_or_reset(&data->wdev->dev, + msi_fw_attrs_dev_unregister, + data->fw_attrs_dev); + if (err) + return err; + + data->fw_attrs_kset = kset_create_and_add("attributes", NULL, + &data->fw_attrs_dev->kobj); + if (!data->fw_attrs_kset) + return -ENOMEM; + + err = sysfs_create_file(&data->fw_attrs_kset->kobj, + &fw_attr_pending_reboot.attr); + if (err) { + kset_unregister(data->fw_attrs_kset); + return err; + } + + err = devm_add_action_or_reset(&data->wdev->dev, msi_kset_unregister, + data->fw_attrs_kset); + if (err) + return err; + + if (data->quirks->pl1_max) { + err = msi_fw_attr_init(data, MSI_ATTR_PPT_PL1_SPL, + &fw_attr_type_int, data->quirks->pl_min, + data->quirks->pl1_max, &data_get_value, + &data_set_value); + if (err) + return err; + } + + if (data->quirks->pl2_max) { + err = msi_fw_attr_init(data, MSI_ATTR_PPT_PL2_SPPT, + &fw_attr_type_int, data->quirks->pl_min, + data->quirks->pl2_max, &data_get_value, + &data_set_value); + if (err) + return err; + } + + return 0; +} + +static int msi_platform_psy_ext_get_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct msi_wmi_platform_data *msi = data; + u8 buffer[32] = { 0 }; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + buffer[0] = MSI_PLATFORM_BAT_ADDR; + ret = msi_wmi_platform_query(msi, MSI_PLATFORM_GET_DATA, + buffer, sizeof(buffer)); + if (ret) + return ret; + + val->intval = buffer[1] & ~BIT(7); + if (val->intval > 100) + return -EINVAL; + + return 0; + default: + return -EINVAL; + } +} + +static int msi_platform_psy_ext_set_prop(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct msi_wmi_platform_data *msi = data; + u8 buffer[32] = { 0 }; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval > 100) + return -EINVAL; + buffer[0] = MSI_PLATFORM_BAT_ADDR; + buffer[1] = val->intval | BIT(7); + return msi_wmi_platform_query(msi, MSI_PLATFORM_SET_DATA, + buffer, sizeof(buffer)); + default: + return -EINVAL; + } +} + +static int +msi_platform_psy_prop_is_writeable(struct power_supply *psy, + const struct power_supply_ext *ext, + void *data, enum power_supply_property psp) +{ + return true; +} + +static const enum power_supply_property oxp_psy_ext_props[] = { + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static const struct power_supply_ext msi_platform_psy_ext = { + .name = "msi-platform-charge-control", + .properties = oxp_psy_ext_props, + .num_properties = ARRAY_SIZE(oxp_psy_ext_props), + .get_property = msi_platform_psy_ext_get_prop, + .set_property = msi_platform_psy_ext_set_prop, + .property_is_writeable = msi_platform_psy_prop_is_writeable, +}; + +static int msi_wmi_platform_battery_add(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + struct msi_wmi_platform_data *data = + container_of(hook, struct msi_wmi_platform_data, battery_hook); + + return power_supply_register_extension(battery, &msi_platform_psy_ext, + &data->wdev->dev, data); +} + +static int msi_wmi_platform_battery_remove(struct power_supply *battery, + struct acpi_battery_hook *hook) +{ + power_supply_unregister_extension(battery, &msi_platform_psy_ext); + return 0; +} + +static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, + size_t length, loff_t *offset) { struct seq_file *seq = fp->private_data; struct msi_wmi_platform_debugfs_data *data = seq->private; @@ -246,17 +1258,21 @@ static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, return ret; down_write(&data->buffer_lock); - ret = msi_wmi_platform_query(data->data, data->method, payload, data->length, data->buffer, + ret = msi_wmi_platform_query(data->data, data->method, data->buffer, data->length); up_write(&data->buffer_lock); if (ret < 0) return ret; + down_write(&data->buffer_lock); + memcpy(data->buffer, payload, data->length); + up_write(&data->buffer_lock); + return length; } -static int msi_wmi_platform_show(struct seq_file *seq, void *p) +static int msi_wmi_platform_debugfs_show(struct seq_file *seq, void *p) { struct msi_wmi_platform_debugfs_data *data = seq->private; int ret; @@ -268,19 +1284,19 @@ static int msi_wmi_platform_show(struct seq_file *seq, void *p) return ret; } -static int msi_wmi_platform_open(struct inode *inode, struct file *fp) +static int msi_wmi_platform_debugfs_open(struct inode *inode, struct file *fp) { struct msi_wmi_platform_debugfs_data *data = inode->i_private; /* The seq_file uses the last byte of the buffer for detecting buffer overflows */ - return single_open_size(fp, msi_wmi_platform_show, data, data->length + 1); + return single_open_size(fp, msi_wmi_platform_debugfs_show, data, data->length + 1); } static const struct file_operations msi_wmi_platform_debugfs_fops = { .owner = THIS_MODULE, - .open = msi_wmi_platform_open, + .open = msi_wmi_platform_debugfs_open, .read = seq_read, - .write = msi_wmi_platform_write, + .write = msi_wmi_platform_debugfs_write, .llseek = seq_lseek, .release = single_release, }; @@ -341,31 +1357,32 @@ static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data) { struct device *hdev; - hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data, - &msi_wmi_platform_chip_info, NULL); + hdev = devm_hwmon_device_register_with_info( + &data->wdev->dev, "msi_wmi_platform", data, + data->quirks->dual_fans ? &msi_wmi_platform_chip_info_dual : + &msi_wmi_platform_chip_info, + msi_wmi_platform_hwmon_groups); return PTR_ERR_OR_ZERO(hdev); } static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data) { - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; u8 flags; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, input, sizeof(input), output, - sizeof(output)); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_EC, buffer, sizeof(buffer)); if (ret < 0) return ret; - flags = output[MSI_PLATFORM_EC_FLAGS_OFFSET]; + flags = buffer[MSI_PLATFORM_EC_FLAGS_OFFSET]; dev_dbg(&data->wdev->dev, "EC RAM version %lu.%lu\n", FIELD_GET(MSI_PLATFORM_EC_MAJOR_MASK, flags), FIELD_GET(MSI_PLATFORM_EC_MINOR_MASK, flags)); dev_dbg(&data->wdev->dev, "EC firmware version %.28s\n", - &output[MSI_PLATFORM_EC_VERSION_OFFSET]); + &buffer[MSI_PLATFORM_EC_VERSION_OFFSET]); if (!(flags & MSI_PLATFORM_EC_IS_TIGERLAKE)) { if (!force) @@ -379,35 +1396,46 @@ static int msi_wmi_platform_ec_init(struct msi_wmi_platform_data *data) static int msi_wmi_platform_init(struct msi_wmi_platform_data *data) { - u8 input[32] = { 0 }; - u8 output[32]; + u8 buffer[32] = { 0 }; int ret; - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, input, sizeof(input), output, - sizeof(output)); + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_WMI, buffer, sizeof(buffer)); if (ret < 0) return ret; dev_dbg(&data->wdev->dev, "WMI interface version %u.%u\n", - output[MSI_PLATFORM_WMI_MAJOR_OFFSET], - output[MSI_PLATFORM_WMI_MINOR_OFFSET]); + buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET], + buffer[MSI_PLATFORM_WMI_MINOR_OFFSET]); - if (output[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) { + if (buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET] != MSI_WMI_PLATFORM_INTERFACE_VERSION) { if (!force) return -ENODEV; dev_warn(&data->wdev->dev, "Loading despite unsupported WMI interface version (%u.%u)\n", - output[MSI_PLATFORM_WMI_MAJOR_OFFSET], - output[MSI_PLATFORM_WMI_MINOR_OFFSET]); + buffer[MSI_PLATFORM_WMI_MAJOR_OFFSET], + buffer[MSI_PLATFORM_WMI_MINOR_OFFSET]); } return 0; } +static int msi_wmi_platform_profile_setup(struct msi_wmi_platform_data *data) +{ + if (!data->quirks->shift_mode) + return 0; + + data->ppdev = devm_platform_profile_register( + &data->wdev->dev, "msi-wmi-platform", data, + &msi_wmi_platform_profile_ops); + + return PTR_ERR_OR_ZERO(data->ppdev); +} + static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) { struct msi_wmi_platform_data *data; + const struct dmi_system_id *dmi_id; int ret; data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); @@ -417,6 +1445,12 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) data->wdev = wdev; dev_set_drvdata(&wdev->dev, data); + dmi_id = dmi_first_match(msi_quirks); + if (dmi_id) + data->quirks = dmi_id->driver_data; + else + data->quirks = &quirk_default; + ret = devm_mutex_init(&wdev->dev, &data->wmi_lock); if (ret < 0) return ret; @@ -429,11 +1463,44 @@ static int msi_wmi_platform_probe(struct wmi_device *wdev, const void *context) if (ret < 0) return ret; + ret = msi_wmi_fw_attrs_init(data); + if (ret < 0) + return ret; + + if (data->quirks->charge_threshold) { + data->battery_hook.name = "MSI Battery"; + data->battery_hook.add_battery = msi_wmi_platform_battery_add; + data->battery_hook.remove_battery = msi_wmi_platform_battery_remove; + battery_hook_register(&data->battery_hook); + } + msi_wmi_platform_debugfs_init(data); + msi_wmi_platform_profile_setup(data); + + if (data->quirks->restore_curves) { + guard(mutex)(&data->wmi_lock); + ret = msi_wmi_platform_curves_save(data); + if (ret < 0) + return ret; + } + return msi_wmi_platform_hwmon_init(data); } +static void msi_wmi_platform_remove(struct wmi_device *wdev) +{ + struct msi_wmi_platform_data *data = dev_get_drvdata(&wdev->dev); + + if (data->quirks->charge_threshold) + battery_hook_unregister(&data->battery_hook); + + if (data->quirks->restore_curves) { + guard(mutex)(&data->wmi_lock); + msi_wmi_platform_curves_load(data); + } +} + static const struct wmi_device_id msi_wmi_platform_id_table[] = { { MSI_PLATFORM_GUID, NULL }, { } @@ -447,6 +1514,7 @@ static struct wmi_driver msi_wmi_platform_driver = { }, .id_table = msi_wmi_platform_id_table, .probe = msi_wmi_platform_probe, + .remove = msi_wmi_platform_remove, .no_singleton = true, }; diff --git a/drivers/platform/x86/zotac-zone-platform.c b/drivers/platform/x86/zotac-zone-platform.c new file mode 100644 index 000000000000..d097f7b9c5a0 --- /dev/null +++ b/drivers/platform/x86/zotac-zone-platform.c @@ -0,0 +1,1122 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Zotac Handheld Platform Driver + * + * Copyright (C) 2025 Luke D. Jones + */ + +#include "linux/mod_devicetable.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "firmware_attributes_class.h" + +#define DRIVER_NAME "zotac_zone_platform" + +#define EC_COMMAND_PORT 0x4E +#define EC_DATA_PORT 0x4F + +#define EC_FAN_CTRL_ADDR 0x44A +#define EC_FAN_DUTY_ADDR 0x44B +#define EC_FAN_SPEED_UPPER_ADDR 0x476 +#define EC_FAN_SPEED_LOWER_ADDR 0x477 +#define EC_CPU_TEMP_ADDR 0x462 + +/* Internal values */ +#define EC_FAN_MODE_AUTO 0 +#define EC_FAN_MODE_MANUAL 1 +/* Follow standard convention for userspace */ +#define PWM_ENABLE_OFF 0 +#define PWM_ENABLE_MANUAL 1 +#define PWM_ENABLE_AUTO 2 /* Automatic control (EC control) */ +#define PWM_ENABLE_CURVE 3 /* Custom curve control */ + +#define PWM_MIN 0 +#define PWM_MAX 255 + +#define FAN_CURVE_POINTS 9 /* 9 points for 10-90°C like in the Zotac C# code */ + +/* AMD APU WMI DPTC constants */ +#define AMD_APU_WMI_METHODS_GUID "1f72b0f1-bfea-4472-9877-6e62937ab616" +#define AMD_APU_WMI_DATA_GUID "05901221-d566-11d1-b2f0-00a0c9062910" + +/* DPTC command IDs */ +#define DPTC_STAPM_TIME_CONSTANT 1 +#define DPTC_SUSTAINED_POWER 5 +#define DPTC_FAST_POWER_1 6 +#define DPTC_FAST_POWER_2 7 +#define DPTC_SLOW_PPT_CONSTANT 8 +#define DPTC_P3T_LIMIT 0x32 + +/* DPTC power limits in milliwatts */ +#define DPTC_MIN_POWER 5000 +#define DPTC_MAX_POWER 28000 + +#define PPT_PL1_SPL_MIN 8 +#define PPT_PL1_SPL_MAX 28 +#define PPT_PL2_SPPT_MIN 8 +#define PPT_PL2_SPPT_MAX 28 +#define PPT_PL3_FPPT_MIN 8 +#define PPT_PL3_FPPT_MAX 28 + +struct power_limits { + u8 ppt_pl1_spl; + u8 ppt_pl2_sppt; + u8 ppt_pl3_fppt; +}; + +static const struct power_limits ppt_quiet_profile = { + .ppt_pl1_spl = 5, + .ppt_pl2_sppt = 10, + .ppt_pl3_fppt = 15, +}; + +static const struct power_limits ppt_balanced_profile = { + .ppt_pl1_spl = 12, + .ppt_pl2_sppt = 19, + .ppt_pl3_fppt = 26, +}; + +static const struct power_limits ppt_performance_profile = { + .ppt_pl1_spl = 28, + .ppt_pl2_sppt = 35, + .ppt_pl3_fppt = 45, +}; + +static struct timer_list fan_curve_timer; + +struct zotac_platform_data { + struct device *hwmon_dev; + struct mutex update_lock; + unsigned int fan_rpm; + unsigned int pwm; + unsigned int pwm_enable; + unsigned int temp; + unsigned long last_updated; + bool valid; + bool curve_enabled; + + /* Fan curve points */ + unsigned int curve_temp[FAN_CURVE_POINTS]; /* Temperature points */ + unsigned int curve_pwm[FAN_CURVE_POINTS]; /* PWM/duty points */ + + /* DPTC values */ + struct device *ppdev; + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + + bool wmi_dptc_supported; + struct power_limits current_power_limits; + enum platform_profile_option current_profile; + /* TODO: hacking - must be removed later */ + unsigned int ppt_pl1_stapm_time_const; + unsigned int ppt_pl2_sppt_time_const; + unsigned int ppt_platform_sppt; +}; + +static struct platform_device *zotac_platform_device; +static DEFINE_MUTEX(ec_mutex); +static struct resource ec_io_ports[] = { + { + .start = EC_COMMAND_PORT, + .end = EC_COMMAND_PORT, + .name = "ec-command", + .flags = IORESOURCE_IO, + }, + { + .start = EC_DATA_PORT, + .end = EC_DATA_PORT, + .name = "ec-data", + .flags = IORESOURCE_IO, + }, +}; + +static u8 ec_read_byte(u16 addr) +{ + u8 addr_upper = (addr >> 8) & 0xFF; + u8 addr_lower = addr & 0xFF; + u8 value; + + mutex_lock(&ec_mutex); + + /* Select upper byte address */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x11, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + outb(addr_upper, EC_DATA_PORT); + + /* Select lower byte address */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x10, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + outb(addr_lower, EC_DATA_PORT); + + /* Read data */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x12, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + value = inb(EC_DATA_PORT); + + mutex_unlock(&ec_mutex); + + return value; +} + +static int ec_write_byte(u16 addr, u8 value) +{ + u8 addr_upper = (addr >> 8) & 0xFF; + u8 addr_lower = addr & 0xFF; + + mutex_lock(&ec_mutex); + + /* Select upper byte address */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x11, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + outb(addr_upper, EC_DATA_PORT); + + /* Select lower byte address */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x10, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + outb(addr_lower, EC_DATA_PORT); + + /* Write data */ + outb(0x2E, EC_COMMAND_PORT); + outb(0x12, EC_DATA_PORT); + outb(0x2F, EC_COMMAND_PORT); + outb(value, EC_DATA_PORT); + + mutex_unlock(&ec_mutex); + + return 0; +} + +static int send_dptc_cmd(u8 cmd_id, u32 value) +{ + struct acpi_buffer input = { 0, NULL }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + u8 *buffer; + acpi_status status; + int ret; + + buffer = kzalloc(8, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + buffer[0] = cmd_id; + *(u32 *)(buffer + 4) = value; + + input.length = 8; + input.pointer = buffer; + + status = wmi_evaluate_method(AMD_APU_WMI_METHODS_GUID, 0, 9, + &input, &output); + + ret = ACPI_SUCCESS(status) ? 0 : -EIO; + + kfree(buffer); + if (output.pointer) + kfree(output.pointer); + + return ret; +} + +static struct zotac_platform_data *zotac_platform_update_device(struct device *dev) +{ + struct zotac_platform_data *data = dev_get_drvdata(dev); + unsigned long current_time = jiffies; + + if (time_after(current_time, data->last_updated + HZ) || !data->valid) { + mutex_lock(&data->update_lock); + + data->pwm_enable = ec_read_byte(EC_FAN_CTRL_ADDR); + data->pwm = ec_read_byte(EC_FAN_DUTY_ADDR); + + u32 upper = ec_read_byte(EC_FAN_SPEED_UPPER_ADDR); + u32 lower = ec_read_byte(EC_FAN_SPEED_LOWER_ADDR); + data->fan_rpm = (upper << 8) | lower; + + data->temp = ec_read_byte(EC_CPU_TEMP_ADDR); + + data->last_updated = current_time; + data->valid = true; + + mutex_unlock(&data->update_lock); + } + + return data; +} + +/* Internal version doesn't acquire the lock */ +static int set_fan_duty_internal(unsigned int duty_percent) +{ + u8 duty_val; + + if (duty_percent > 100) + return -EINVAL; + + duty_val = + (duty_percent * (PWM_MAX - PWM_MIN)) / 100 + + PWM_MIN; + return ec_write_byte(EC_FAN_DUTY_ADDR, duty_val); +} + +static void fan_curve_function(struct timer_list *t) +{ + struct zotac_platform_data *data = platform_get_drvdata(zotac_platform_device); + unsigned int current_temp; + unsigned int pwm = 0; + int i; + + if (!data || !data->curve_enabled) { + if (data && data->curve_enabled) + mod_timer(&fan_curve_timer, jiffies + HZ); + return; + } + + mutex_lock(&data->update_lock); + + current_temp = ec_read_byte(EC_CPU_TEMP_ADDR); + data->temp = current_temp; + + pwm = data->curve_pwm[0]; + + if (current_temp >= data->curve_temp[FAN_CURVE_POINTS - 1]) { + /* Above highest temperature point - use maximum PWM */ + pwm = data->curve_pwm[FAN_CURVE_POINTS - 1]; + } else { + /* Find the temperature range and interpolate */ + for (i = 0; i < FAN_CURVE_POINTS - 1; i++) { + if (current_temp >= data->curve_temp[i] && + current_temp < data->curve_temp[i + 1]) { + /* Linear interpolation between points */ + int temp_range = data->curve_temp[i + 1] - + data->curve_temp[i]; + int pwm_range = data->curve_pwm[i + 1] - + data->curve_pwm[i]; + int temp_offset = + current_temp - data->curve_temp[i]; + + if (temp_range > 0) { + pwm = data->curve_pwm[i] + + (pwm_range * temp_offset) / + temp_range; + } else { + pwm = data->curve_pwm[i]; + } + + break; + } + } + } + + set_fan_duty_internal(pwm); + mutex_unlock(&data->update_lock); + + mod_timer(&fan_curve_timer, jiffies + HZ); +} + +/* Fan speed RPM */ +static ssize_t fan1_input_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct zotac_platform_data *data = zotac_platform_update_device(dev); + return sprintf(buf, "%u\n", data->fan_rpm); +} +static DEVICE_ATTR_RO(fan1_input); + +/* Fan mode */ + +static int set_pwm_enable(struct device *dev, u8 mode) +{ + struct zotac_platform_data *data = dev_get_drvdata(dev); + int err = 0; + u8 ec_mode; + + /* Convert from standard modes to EC-specific modes */ + switch (mode) { + case PWM_ENABLE_OFF: + /* If supported by EC, turn fan off */ + return -EOPNOTSUPP; /* If EC doesn't support OFF mode */ + case PWM_ENABLE_MANUAL: + ec_mode = + EC_FAN_MODE_MANUAL; /* Assuming this is your actual EC value */ + data->curve_enabled = false; + if (data->curve_enabled) { + data->curve_enabled = false; + timer_delete(&fan_curve_timer); + } + break; + case PWM_ENABLE_AUTO: + ec_mode = + EC_FAN_MODE_AUTO; /* Assuming this is your actual EC value */ + data->curve_enabled = false; + if (data->curve_enabled) { + data->curve_enabled = false; + timer_delete(&fan_curve_timer); + } + break; + case PWM_ENABLE_CURVE: + ec_mode = + EC_FAN_MODE_MANUAL; /* We'll control manually but via the curve */ + data->curve_enabled = true; + break; + default: + return -EINVAL; + } + + /* Set mode to EC if needed */ + if (!data->curve_enabled || mode != PWM_ENABLE_CURVE) { + err = ec_write_byte(EC_FAN_CTRL_ADDR, ec_mode); + } + + if (err == 0) { + data->pwm_enable = mode; + if (mode == PWM_ENABLE_CURVE) + mod_timer(&fan_curve_timer, jiffies + HZ); + } + + return err; +} + +/* Replace fan1_duty with pwm1 and scale to 0-255 */ +static int set_pwm(struct device *dev, u8 pwm_value) +{ + struct zotac_platform_data *data = dev_get_drvdata(dev); + int err; + + if (pwm_value > PWM_MAX) + return -EINVAL; + + mutex_lock(&data->update_lock); + err = ec_write_byte(EC_FAN_DUTY_ADDR, pwm_value); + if (err == 0) + data->pwm = pwm_value; + mutex_unlock(&data->update_lock); + + return err; +} + +static ssize_t pwm1_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct zotac_platform_data *data = zotac_platform_update_device(dev); + return sprintf(buf, "%u\n", data->pwm_enable); +} + +static ssize_t pwm1_enable_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + unsigned long mode; + int err; + + err = kstrtoul(buf, 10, &mode); + if (err) + return err; + + if (mode > PWM_ENABLE_CURVE) + return -EINVAL; + + err = set_pwm_enable(dev, mode); + if (err) + return err; + + return count; +} +static DEVICE_ATTR_RW(pwm1_enable); + +/* Fan duty cycle (percent) */ +static ssize_t pwm1_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct zotac_platform_data *data = zotac_platform_update_device(dev); + return sprintf(buf, "%u\n", data->pwm); +} + +static ssize_t pwm1_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned long pwm_value; + int err; + + err = kstrtoul(buf, 10, &pwm_value); + if (err) + return err; + + if (pwm_value > PWM_MAX) + return -EINVAL; + + err = set_pwm(dev, pwm_value); + if (err) + return err; + + return count; +} +static DEVICE_ATTR_RW(pwm1); + +/* Macro to generate temperature point attributes */ +#define CURVE_TEMP_ATTR(index) \ + static ssize_t pwm1_auto_point##index##_temp_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + return sprintf(buf, "%u\n", data->curve_temp[index - 1]); \ + } \ + \ + static ssize_t pwm1_auto_point##index##_temp_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + unsigned long temp; \ + int err; \ + \ + err = kstrtoul(buf, 10, &temp); \ + if (err) \ + return err; \ + \ + mutex_lock(&data->update_lock); \ + data->curve_temp[index - 1] = temp; \ + mutex_unlock(&data->update_lock); \ + \ + return count; \ + } \ + static DEVICE_ATTR_RW(pwm1_auto_point##index##_temp) + +/* Macro to generate PWM point attributes */ +#define CURVE_PWM_ATTR(index) \ + static ssize_t pwm1_auto_point##index##_pwm_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + return sprintf(buf, "%u\n", data->curve_pwm[index - 1]); \ + } \ + \ + static ssize_t pwm1_auto_point##index##_pwm_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + unsigned long pwm; \ + int err; \ + \ + err = kstrtoul(buf, 10, &pwm); \ + if (err) \ + return err; \ + \ + if (pwm > 100) \ + return -EINVAL; \ + \ + mutex_lock(&data->update_lock); \ + data->curve_pwm[index - 1] = pwm; \ + mutex_unlock(&data->update_lock); \ + \ + return count; \ + } \ + static DEVICE_ATTR_RW(pwm1_auto_point##index##_pwm) + +/* Generate attributes for each point */ +CURVE_TEMP_ATTR(1); +CURVE_PWM_ATTR(1); +CURVE_TEMP_ATTR(2); +CURVE_PWM_ATTR(2); +CURVE_TEMP_ATTR(3); +CURVE_PWM_ATTR(3); +CURVE_TEMP_ATTR(4); +CURVE_PWM_ATTR(4); +CURVE_TEMP_ATTR(5); +CURVE_PWM_ATTR(5); +CURVE_TEMP_ATTR(6); +CURVE_PWM_ATTR(6); +CURVE_TEMP_ATTR(7); +CURVE_PWM_ATTR(7); +CURVE_TEMP_ATTR(8); +CURVE_PWM_ATTR(8); +CURVE_TEMP_ATTR(9); +CURVE_PWM_ATTR(9); + +/* Temperature reading */ +static ssize_t temp1_input_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct zotac_platform_data *data = zotac_platform_update_device(dev); + return sprintf(buf, "%u\n", + data->temp * 1000); /* Convert to milli-degrees */ +} +static DEVICE_ATTR_RO(temp1_input); + +static struct attribute *zotac_platform_hwmon_attrs[] = { + &dev_attr_fan1_input.attr, + &dev_attr_pwm1_enable.attr, + &dev_attr_pwm1.attr, + &dev_attr_temp1_input.attr, + &dev_attr_pwm1_auto_point1_temp.attr, + &dev_attr_pwm1_auto_point1_pwm.attr, + &dev_attr_pwm1_auto_point2_temp.attr, + &dev_attr_pwm1_auto_point2_pwm.attr, + &dev_attr_pwm1_auto_point3_temp.attr, + &dev_attr_pwm1_auto_point3_pwm.attr, + &dev_attr_pwm1_auto_point4_temp.attr, + &dev_attr_pwm1_auto_point4_pwm.attr, + &dev_attr_pwm1_auto_point5_temp.attr, + &dev_attr_pwm1_auto_point5_pwm.attr, + &dev_attr_pwm1_auto_point6_temp.attr, + &dev_attr_pwm1_auto_point6_pwm.attr, + &dev_attr_pwm1_auto_point7_temp.attr, + &dev_attr_pwm1_auto_point7_pwm.attr, + &dev_attr_pwm1_auto_point8_temp.attr, + &dev_attr_pwm1_auto_point8_pwm.attr, + &dev_attr_pwm1_auto_point9_temp.attr, + &dev_attr_pwm1_auto_point9_pwm.attr, + NULL +}; + +/* DPTC attributes */ +#define DPTC_ATTR(display_name, cmd_id, min_val, max_val) \ + static ssize_t display_name##_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + return sprintf(buf, "%u\n", data->display_name); \ + } \ + \ + static ssize_t display_name##_store(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct zotac_platform_data *data = dev_get_drvdata(dev); \ + unsigned long val; \ + int err; \ + \ + if (!data->wmi_dptc_supported) \ + return -ENODEV; \ + \ + err = kstrtoul(buf, 10, &val); \ + if (err) \ + return err; \ + \ + if (val < min_val || val > max_val) \ + return -EINVAL; \ + \ + mutex_lock(&data->update_lock); \ + err = send_dptc_cmd(cmd_id, val); \ + if (err == 0) \ + data->display_name = val; \ + mutex_unlock(&data->update_lock); \ + \ + return err ? err : count; \ + } \ + static DEVICE_ATTR_RW(display_name) + +/* Generate DPTC attributes with AMD-specific naming */ +DPTC_ATTR(ppt_pl1_stapm_time_const, DPTC_STAPM_TIME_CONSTANT, 1, 10000); +DPTC_ATTR(ppt_pl2_sppt_time_const, DPTC_SLOW_PPT_CONSTANT, 1, 0xFF); +DPTC_ATTR(ppt_platform_sppt, DPTC_P3T_LIMIT, DPTC_MIN_POWER, DPTC_MAX_POWER); + +static struct attribute *zotac_platform_dptc_attrs[] = { + &dev_attr_ppt_pl1_stapm_time_const.attr, + &dev_attr_ppt_pl2_sppt_time_const.attr, + &dev_attr_ppt_platform_sppt.attr, + NULL +}; + +static const struct attribute_group zotac_platform_hwmon_group = { + .attrs = zotac_platform_hwmon_attrs, +}; + +static const struct attribute_group zotac_platform_dptc_group = { + .name = "dptc", + .attrs = zotac_platform_dptc_attrs, +}; + +static const struct attribute_group *zotac_platform_hwmon_groups[] = { + &zotac_platform_hwmon_group, + NULL +}; + +/* Helper function to show a simple integer attribute */ +static ssize_t show_int_attr(struct device *dev, + struct device_attribute *attr, + char *buf, int value) +{ + return sprintf(buf, "%d\n", value); +} + +/* Helper function to show a simple string attribute */ +static ssize_t show_string_attr(struct device *dev, + struct device_attribute *attr, + char *buf, const char *value) +{ + return sprintf(buf, "%s\n", value); +} + +#define PPT_ATTR_RO(_name, _attr_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attr_name, .mode = 0444 }, \ + .show = _name##_show, \ + } + +#define PPT_ATTR_RW(_name, _attr_name) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attr_name, .mode = 0644 }, \ + .show = _name##_show, \ + .store = _name##_store, \ + } + +/* Macro that creates a complete set of attributes for a power limit */ +#define DEFINE_POWER_LIMIT_ATTRS(attr_name, cmd_id, min, max, desc) \ + static ssize_t attr_name##_current_value_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + struct zotac_platform_data *data = \ + platform_get_drvdata(zotac_platform_device); \ + return show_int_attr(dev, attr, buf, \ + data->current_power_limits.attr_name); \ + } \ + \ + static ssize_t attr_name##_current_value_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct zotac_platform_data *data = \ + platform_get_drvdata(zotac_platform_device); \ + unsigned long val; \ + int err; \ + \ + if (!data->wmi_dptc_supported) \ + return -ENODEV; \ + \ + err = kstrtoul(buf, 10, &val); \ + if (err) \ + return err; \ + \ + if (val < min || val > max) \ + return -EINVAL; \ + \ + mutex_lock(&data->update_lock); \ + data->current_power_limits.attr_name = val; \ + data->current_profile = PLATFORM_PROFILE_CUSTOM; \ + \ + err = send_dptc_cmd(cmd_id, val * 1000); \ + mutex_unlock(&data->update_lock); \ + \ + return err ? err : count; \ + } \ + \ + static ssize_t attr_name##_min_value_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return show_int_attr(dev, attr, buf, min); \ + } \ + \ + static ssize_t attr_name##_max_value_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return show_int_attr(dev, attr, buf, max); \ + } \ + \ + static ssize_t attr_name##_default_value_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + struct zotac_platform_data *data = \ + platform_get_drvdata(zotac_platform_device); \ + int default_value; \ + \ + if (data->current_profile == PLATFORM_PROFILE_CUSTOM) { \ + default_value = ppt_balanced_profile.attr_name; \ + } else { \ + const struct power_limits *profile; \ + \ + switch (data->current_profile) { \ + case PLATFORM_PROFILE_LOW_POWER: \ + profile = &ppt_quiet_profile; \ + break; \ + case PLATFORM_PROFILE_BALANCED: \ + profile = &ppt_balanced_profile; \ + break; \ + case PLATFORM_PROFILE_PERFORMANCE: \ + profile = &ppt_performance_profile; \ + break; \ + default: \ + profile = &ppt_balanced_profile; \ + break; \ + } \ + \ + default_value = profile->attr_name; \ + } \ + \ + return show_int_attr(dev, attr, buf, default_value); \ + } \ + \ + static ssize_t attr_name##_scalar_increment_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return show_int_attr(dev, attr, buf, 1); \ + } \ + \ + static ssize_t attr_name##_display_name_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return show_string_attr(dev, attr, buf, desc); \ + } \ + \ + static ssize_t attr_name##_type_show( \ + struct device *dev, struct device_attribute *attr, char *buf) \ + { \ + return show_string_attr(dev, attr, buf, "int"); \ + } \ + \ + static PPT_ATTR_RW(attr_name##_current_value, "current_value"); \ + static PPT_ATTR_RO(attr_name##_min_value, "min_value"); \ + static PPT_ATTR_RO(attr_name##_max_value, "max_value"); \ + static PPT_ATTR_RO(attr_name##_default_value, "default_value"); \ + static PPT_ATTR_RO(attr_name##_scalar_increment, "scalar_increment"); \ + static PPT_ATTR_RO(attr_name##_display_name, "display_name"); \ + static PPT_ATTR_RO(attr_name##_type, "type"); \ + \ + static struct attribute *attr_name##_attrs[] = { \ + &dev_attr_##attr_name##_current_value.attr, \ + &dev_attr_##attr_name##_min_value.attr, \ + &dev_attr_##attr_name##_max_value.attr, \ + &dev_attr_##attr_name##_default_value.attr, \ + &dev_attr_##attr_name##_scalar_increment.attr, \ + &dev_attr_##attr_name##_display_name.attr, \ + &dev_attr_##attr_name##_type.attr, \ + NULL \ + }; \ + \ + static const struct attribute_group attr_name##_attr_group = { \ + .name = #attr_name, .attrs = attr_name##_attrs \ + } + +/* Define the power limit attribute groups */ +DEFINE_POWER_LIMIT_ATTRS(ppt_pl1_spl, DPTC_SUSTAINED_POWER, PPT_PL1_SPL_MIN, + PPT_PL1_SPL_MAX, + "CPU Sustained Power Limit (PL1/SPL)"); + +DEFINE_POWER_LIMIT_ATTRS(ppt_pl2_sppt, DPTC_FAST_POWER_1, PPT_PL2_SPPT_MIN, + PPT_PL2_SPPT_MAX, + "CPU Short Term Power Limit (PL2/SPPT)"); + +DEFINE_POWER_LIMIT_ATTRS(ppt_pl3_fppt, DPTC_FAST_POWER_2, PPT_PL3_FPPT_MIN, + PPT_PL3_FPPT_MAX, "CPU Fast Power Limit (PL3/FPPT)"); + +/* Combine all power attribute groups */ +static const struct attribute_group *zotac_platform_power_groups[] = { + &ppt_pl1_spl_attr_group, &ppt_pl2_sppt_attr_group, + &ppt_pl3_fppt_attr_group, NULL +}; + +/* Helper function to create all power attribute groups */ +static int create_power_attributes(struct platform_device *pdev, struct zotac_platform_data *data) +{ + int i, ret = 0; + + if (!data->wmi_dptc_supported) + return 0; + + data->fw_attr_dev = device_create(&firmware_attributes_class, NULL, MKDEV(0, 0), + NULL, "%s", DRIVER_NAME); + if (IS_ERR(data->fw_attr_dev)) { + ret = PTR_ERR(data->fw_attr_dev); + goto fail_class_get; + } + + data->fw_attr_kset = kset_create_and_add("attributes", NULL, + &data->fw_attr_dev->kobj); + if (!data->fw_attr_kset) { + ret = -ENOMEM; + goto err_destroy_kset; + } + + for (i = 0; zotac_platform_power_groups[i]; i++) { + ret = sysfs_create_group(&data->fw_attr_kset->kobj, + zotac_platform_power_groups[i]); + if (ret) { + dev_warn(&pdev->dev, + "Failed to create power limit group %d\n", i); + goto error; + } + } + + return 0; + +error: + while (--i >= 0) + sysfs_remove_group(&data->fw_attr_kset->kobj, + zotac_platform_power_groups[i]); +err_destroy_kset: + kset_unregister(data->fw_attr_kset); +fail_class_get: + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); + return ret; +} + +/* Helper function to remove all power attribute groups */ +static void remove_power_attributes(struct platform_device *pdev) +{ + struct zotac_platform_data *data = platform_get_drvdata(pdev); + int i; + + for (i = 0; zotac_platform_power_groups[i]; i++) + sysfs_remove_group(&data->fw_attr_kset->kobj, + zotac_platform_power_groups[i]); + + kset_unregister(data->fw_attr_kset); + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); +} + +static int zotac_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct zotac_platform_data *data = dev_get_drvdata(dev); + *profile = data->current_profile; + return 0; +} + +static int zotac_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct zotac_platform_data *data = dev_get_drvdata(dev); + const struct power_limits *limits; + int ret = 0; + + if (!data->wmi_dptc_supported) + return -ENODEV; + + switch (profile) { + case PLATFORM_PROFILE_PERFORMANCE: + limits = &ppt_performance_profile; + break; + case PLATFORM_PROFILE_BALANCED: + limits = &ppt_balanced_profile; + break; + case PLATFORM_PROFILE_LOW_POWER: + limits = &ppt_quiet_profile; + break; + case PLATFORM_PROFILE_CUSTOM: + limits = &data->current_power_limits; + break; + default: + return -EOPNOTSUPP; + } + + mutex_lock(&data->update_lock); + ret = send_dptc_cmd(DPTC_SUSTAINED_POWER, limits->ppt_pl1_spl * 1000); + if (ret == 0) + ret = send_dptc_cmd(DPTC_FAST_POWER_1, limits->ppt_pl2_sppt * 1000); + if (ret == 0) + ret = send_dptc_cmd(DPTC_FAST_POWER_2, limits->ppt_pl3_fppt * 1000); + if (ret == 0) { + data->current_profile = profile; + if (profile != PLATFORM_PROFILE_CUSTOM) + memcpy(&data->current_power_limits, limits, sizeof(struct power_limits)); + } + mutex_unlock(&data->update_lock); + + return ret; +} + +static int zotac_platform_profile_probe(void *drvdata, unsigned long *choices) +{ + set_bit(PLATFORM_PROFILE_LOW_POWER, choices); + set_bit(PLATFORM_PROFILE_BALANCED, choices); + set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); + set_bit(PLATFORM_PROFILE_CUSTOM, choices); + return 0; +} + +static const struct platform_profile_ops zotac_platform_profile_ops = { + .probe = zotac_platform_profile_probe, + .profile_get = zotac_platform_profile_get, + .profile_set = zotac_platform_profile_set, +}; + +static int platform_profile_setup(struct zotac_platform_data *data) +{ + struct device *dev = &zotac_platform_device->dev; + + if (!data->wmi_dptc_supported) + return 0; + + data->ppdev = devm_platform_profile_register(dev, DRIVER_NAME, data, + &zotac_platform_profile_ops); + if (IS_ERR(data->ppdev)) { + dev_err(dev, "Failed to register platform_profile device\n"); + return PTR_ERR(data->ppdev); + } + + /* Set default profile */ + zotac_platform_profile_set(dev, PLATFORM_PROFILE_BALANCED); + + return 0; +} + +static int zotac_platform_probe(struct platform_device *pdev) +{ + struct zotac_platform_data *data; + struct device *hwmon_dev; + int i, ret; + + data = devm_kzalloc(&pdev->dev, sizeof(struct zotac_platform_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->wmi_dptc_supported = wmi_has_guid(AMD_APU_WMI_METHODS_GUID); + data->valid = false; + data->curve_enabled = false; + mutex_init(&data->update_lock); + + for (i = 0; i < FAN_CURVE_POINTS; i++) { + /* Set default temperature points from 10°C to 90°C */ + data->curve_temp[i] = 10 + (i * 10); + /* Set default PWM values - simple linear curve from 20% to 100% */ + data->curve_pwm[i] = 20 + (i * 10); + if (data->curve_pwm[i] > 100) + data->curve_pwm[i] = 100; + } + + data->current_profile = PLATFORM_PROFILE_BALANCED; + memcpy(&data->current_power_limits, &ppt_balanced_profile, sizeof(struct power_limits)); + data->ppt_pl1_stapm_time_const = 300; + data->ppt_pl2_sppt_time_const = 0x11; + data->ppt_platform_sppt = 13000; + + hwmon_dev = devm_hwmon_device_register_with_groups( + &pdev->dev, "zotac_platform", data, zotac_platform_hwmon_groups); + if (IS_ERR(hwmon_dev)) + return PTR_ERR(hwmon_dev); + + data->hwmon_dev = hwmon_dev; + + if (data->wmi_dptc_supported) { + ret = sysfs_create_group(&pdev->dev.kobj, &zotac_platform_dptc_group); + if (ret) + dev_warn(&pdev->dev, "Failed to create DPTC sysfs group\n"); + } + + platform_set_drvdata(pdev, data); + + timer_setup(&fan_curve_timer, fan_curve_function, 0); + + zotac_platform_update_device(&pdev->dev); + + ret = platform_profile_setup(data); + if (ret) + dev_warn(&pdev->dev, "Failed to setup platform profile\n"); + + ret = create_power_attributes(pdev, data); + if (ret) + dev_warn(&pdev->dev, "Failed to setup firmware attributes\n"); + + return 0; +} + +static void zotac_platform_remove(struct platform_device *pdev) +{ + struct zotac_platform_data *data = platform_get_drvdata(pdev); + + if (data && data->wmi_dptc_supported) { + sysfs_remove_group(&pdev->dev.kobj, &zotac_platform_dptc_group); + remove_power_attributes(pdev); + } +} + +static struct platform_driver zotac_platform_driver = { + .driver = { + .name = DRIVER_NAME, + }, + .probe = zotac_platform_probe, + .remove = zotac_platform_remove, +}; + +static const struct dmi_system_id zotac_platform_dmi_table[] __initconst = { + { + .ident = "Zotac Gaming Handheld", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ZOTAC"), + DMI_MATCH(DMI_BOARD_NAME, "G0A1W"), + }, + }, + { + .ident = "Zotac ZONE", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "ZOTAC"), + DMI_MATCH(DMI_PRODUCT_NAME, "ZOTAC GAMING ZONE"), + }, + }, + {} /* Terminate list */ +}; +MODULE_DEVICE_TABLE(dmi, zotac_platform_dmi_table); + +static int __init zotac_platform_init(void) +{ + int err; + + if (!dmi_check_system(zotac_platform_dmi_table)) { + pr_info("No compatible Zotac hardware found\n"); + return -ENODEV; + } + + /* Request I/O regions */ + if (!request_region(EC_COMMAND_PORT, 1, "zotac_platform_ec") || + !request_region(EC_DATA_PORT, 1, "zotac_platform_ec")) { + pr_err("Failed to request EC I/O ports\n"); + err = -EBUSY; + goto err_release; + } + + zotac_platform_device = platform_device_register_simple( + DRIVER_NAME, -1, ec_io_ports, ARRAY_SIZE(ec_io_ports)); + if (IS_ERR(zotac_platform_device)) { + err = PTR_ERR(zotac_platform_device); + goto err_release; + } + + err = platform_driver_register(&zotac_platform_driver); + if (err) + goto err_device_unregister; + + return 0; + +err_device_unregister: + platform_device_unregister(zotac_platform_device); +err_release: + release_region(EC_COMMAND_PORT, 1); + release_region(EC_DATA_PORT, 1); + return err; +} + +static void __exit zotac_platform_exit(void) +{ + timer_delete_sync(&fan_curve_timer); + + platform_driver_unregister(&zotac_platform_driver); + platform_device_unregister(zotac_platform_device); + release_region(EC_COMMAND_PORT, 1); + release_region(EC_DATA_PORT, 1); +} + +module_init(zotac_platform_init); +module_exit(zotac_platform_exit); + +MODULE_AUTHOR("Luke D. Jones"); +MODULE_DESCRIPTION("Zotac Handheld Platform Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/usbip/vhci_hcd.c b/drivers/usb/usbip/vhci_hcd.c index e55690da19e5..55e8089409c3 100644 --- a/drivers/usb/usbip/vhci_hcd.c +++ b/drivers/usb/usbip/vhci_hcd.c @@ -1443,11 +1443,6 @@ static void vhci_hcd_remove(struct platform_device *pdev) static int vhci_hcd_suspend(struct platform_device *pdev, pm_message_t state) { struct usb_hcd *hcd; - struct vhci *vhci; - int rhport; - int connected = 0; - int ret = 0; - unsigned long flags; dev_dbg(&pdev->dev, "%s\n", __func__); @@ -1455,33 +1450,9 @@ static int vhci_hcd_suspend(struct platform_device *pdev, pm_message_t state) if (!hcd) return 0; - vhci = *((void **)dev_get_platdata(hcd->self.controller)); - - spin_lock_irqsave(&vhci->lock, flags); - - for (rhport = 0; rhport < VHCI_HC_PORTS; rhport++) { - if (vhci->vhci_hcd_hs->port_status[rhport] & - USB_PORT_STAT_CONNECTION) - connected += 1; - - if (vhci->vhci_hcd_ss->port_status[rhport] & - USB_PORT_STAT_CONNECTION) - connected += 1; - } + clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags); - spin_unlock_irqrestore(&vhci->lock, flags); - - if (connected > 0) { - dev_info(&pdev->dev, - "We have %d active connection%s. Do not suspend.\n", - connected, str_plural(connected)); - ret = -EBUSY; - } else { - dev_info(&pdev->dev, "suspend vhci_hcd"); - clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags); - } - - return ret; + return 0; } static int vhci_hcd_resume(struct platform_device *pdev) diff --git a/drivers/video/backlight/backlight.c b/drivers/video/backlight/backlight.c index 1e9b7e85d99a..b9af334e306b 100644 --- a/drivers/video/backlight/backlight.c +++ b/drivers/video/backlight/backlight.c @@ -116,6 +116,7 @@ EXPORT_SYMBOL(backlight_notify_blank_all); static void backlight_generate_event(struct backlight_device *bd, enum backlight_update_reason reason) { +#if 0 // We don't want to generate udev events for brightness changes on Steam Deck, as some games like Celeste will re-enumerate controller devices in response to this event. char *envp[2]; switch (reason) { @@ -131,6 +132,7 @@ static void backlight_generate_event(struct backlight_device *bd, } envp[1] = NULL; kobject_uevent_env(&bd->dev.kobj, KOBJ_CHANGE, envp); +#endif // 0 sysfs_notify(&bd->dev.kobj, NULL, "actual_brightness"); } diff --git a/include/linux/device.h b/include/linux/device.h index 0be95294b6e6..381463baed6d 100644 --- a/include/linux/device.h +++ b/include/linux/device.h @@ -189,6 +189,22 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr, #define DEVICE_ATTR_ADMIN_RW(_name) \ struct device_attribute dev_attr_##_name = __ATTR_RW_MODE(_name, 0600) +/** + * DEVICE_ATTR_RW_NAMED - Define a read-write device attribute with a sysfs name + * that differs from the function name. + * @_name: Attribute function preface + * @_attrname: Attribute name as it wil be exposed in the sysfs. + * + * Like DEVICE_ATTR_RW(), but allows for reusing names under separate paths in + * the same driver. + */ +#define DEVICE_ATTR_RW_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0644 }, \ + .show = _name##_show, \ + .store = _name##_store, \ + } + /** * DEVICE_ATTR_RO - Define a readable device attribute. * @_name: Attribute name. @@ -207,6 +223,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr, #define DEVICE_ATTR_ADMIN_RO(_name) \ struct device_attribute dev_attr_##_name = __ATTR_RO_MODE(_name, 0400) +/** + * DEVICE_ATTR_RO_NAMED - Define a read-only device attribute with a sysfs name + * that differs from the function name. + * @_name: Attribute function preface + * @_attrname: Attribute name as it wil be exposed in the sysfs. + * + * Like DEVICE_ATTR_RO(), but allows for reusing names under separate paths in + * the same driver. + */ +#define DEVICE_ATTR_RO_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0444 }, \ + .show = _name##_show, \ + } + /** * DEVICE_ATTR_WO - Define an admin-only writable device attribute. * @_name: Attribute name. @@ -216,6 +247,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr, #define DEVICE_ATTR_WO(_name) \ struct device_attribute dev_attr_##_name = __ATTR_WO(_name) +/** + * DEVICE_ATTR_WO_NAMED - Define a read-only device attribute with a sysfs name + * that differs from the function name. + * @_name: Attribute function preface + * @_attrname: Attribute name as it wil be exposed in the sysfs. + * + * Like DEVICE_ATTR_WO(), but allows for reusing names under separate paths in + * the same driver. + */ +#define DEVICE_ATTR_WO_NAMED(_name, _attrname) \ + struct device_attribute dev_attr_##_name = { \ + .attr = { .name = _attrname, .mode = 0200 }, \ + .store = _name##_store, \ + } + /** * DEVICE_ULONG_ATTR - Define a device attribute backed by an unsigned long. * @_name: Attribute name. diff --git a/include/linux/hid.h b/include/linux/hid.h index 5952d95a53e8..13fa09f5ade4 100644 --- a/include/linux/hid.h +++ b/include/linux/hid.h @@ -700,6 +700,7 @@ struct hid_device { char name[128]; /* Device name */ char phys[64]; /* Device physical location */ char uniq[64]; /* Device unique identifier (serial #) */ + u64 firmware_version; /* Firmware version */ void *driver_data; diff --git a/include/linux/mhi.h b/include/linux/mhi.h index dd372b0123a6..6fd218a87785 100644 --- a/include/linux/mhi.h +++ b/include/linux/mhi.h @@ -306,6 +306,7 @@ struct mhi_controller_config { * if fw_image is NULL and fbc_download is true (optional) * @fw_sz: Firmware image data size for normal booting, used only if fw_image * is NULL and fbc_download is true (optional) + * @prev_fw_sz: Previous firmware image data size, when fbc_download is true * @edl_image: Firmware image name for emergency download mode (optional) * @rddm_size: RAM dump size that host should allocate for debugging purpose * @sbl_size: SBL image size downloaded through BHIe (optional) @@ -382,6 +383,7 @@ struct mhi_controller { const char *fw_image; const u8 *fw_data; size_t fw_sz; + size_t prev_fw_sz; const char *edl_image; size_t rddm_size; size_t sbl_size; diff --git a/sound/hda/codecs/realtek/alc269.c b/sound/hda/codecs/realtek/alc269.c index 61c7372e6307..3e590df37cfd 100644 --- a/sound/hda/codecs/realtek/alc269.c +++ b/sound/hda/codecs/realtek/alc269.c @@ -2555,6 +2555,20 @@ static void alc294_gx502_toggle_output(struct hda_codec *codec, alc_write_coef_idx(codec, 0x10, 0x0a20); } +static void alc269_fixup_headphone_volume(struct hda_codec *codec, + const struct hda_fixup *fix, int action) +{ + /* Pin 0x21: Some devices share 0x14 for headphones and speakers. + * This will fix ensure these devices have volume controls. */ + if (!is_jack_detectable(codec, 0x21)) + return; + + if (action == HDA_FIXUP_ACT_PRE_PROBE) { + static const hda_nid_t conn1[] = { 0x02 }; + snd_hda_override_conn_list(codec, 0x14, ARRAY_SIZE(conn1), conn1); + } +} + static void alc294_fixup_gx502_hp(struct hda_codec *codec, const struct hda_fixup *fix, int action) { @@ -3526,6 +3540,8 @@ enum { ALC269_FIXUP_DELL4_MIC_NO_PRESENCE, ALC269_FIXUP_DELL4_MIC_NO_PRESENCE_QUIET, ALC269_FIXUP_HEADSET_MODE, + ALC269_FIXUP_DMI_MATCH, + ALC269_FIXUP_AYA_HEADSET_VOLUME, ALC269_FIXUP_HEADSET_MODE_NO_HP_MIC, ALC269_FIXUP_ASPIRE_HEADSET_MIC, ALC269_FIXUP_ASUS_X101_FUNC, @@ -3539,6 +3555,7 @@ enum { ALC269VB_FIXUP_ASUS_ZENBOOK, ALC269VB_FIXUP_ASUS_ZENBOOK_UX31A, ALC269VB_FIXUP_ASUS_MIC_NO_PRESENCE, + ALC269VB_FIXUP_AYANEO_SPKR_PIN_FIX, ALC269_FIXUP_LIMIT_INT_MIC_BOOST_MUTE_LED, ALC269VB_FIXUP_ORDISSIMO_EVE2, ALC283_FIXUP_CHROME_BOOK, @@ -3806,6 +3823,30 @@ enum { ALC236_FIXUP_HP_MUTE_LED_MICMUTE_GPIO, }; +/* A special fixup for AYN and AYANEO handhelds as both +* have the same PCI SSID as well as the same codec, but +* require different quirks, falling back to DMI matching. +*/ +static void alc269_fixup_match_via_dmi(struct hda_codec *codec, + const struct hda_fixup *fix, int action) +{ + int alc269_fix_id; + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + + if (dmi_name_in_vendors("AYANEO") || dmi_name_in_vendors("AYADEVICE") || dmi_name_in_vendors("AYA DEVICE")) { + if (board_name && (strcmp(board_name, "AYANEO 2") || strcmp(board_name, "AYANEO 2S") || strcmp(board_name, "GEEK") || strcmp(board_name, "GEEK 1S"))) { + alc269_fix_id = ALC269_FIXUP_AYA_HEADSET_VOLUME; + } else { + return; + } + } else if (dmi_name_in_vendors("ayn") && strcmp(board_name, "Loki MiniPro")) { + alc269_fix_id = ALC269VB_FIXUP_AYANEO_SPKR_PIN_FIX; + } else { + return; + } + __snd_hda_apply_fixup(codec, alc269_fix_id, action, 0); +} + /* A special fixup for Lenovo C940 and Yoga Duet 7; * both have the very same PCI SSID, and we need to apply different fixups * depending on the codec ID @@ -4273,6 +4314,13 @@ static const struct hda_fixup alc269_fixups[] = { .chained = true, .chain_id = ALC269_FIXUP_HEADSET_MIC }, + [ALC269VB_FIXUP_AYANEO_SPKR_PIN_FIX] = { + .type = HDA_FIXUP_PINS, + .v.pins = (const struct hda_pintbl[]) { + { 0x1a, 0x90170110 }, + { } + }, + }, [ALC269_FIXUP_LIMIT_INT_MIC_BOOST_MUTE_LED] = { .type = HDA_FIXUP_FUNC, .v.func = alc269_fixup_limit_int_mic_boost, @@ -5078,6 +5126,14 @@ static const struct hda_fixup alc269_fixups[] = { { 0x1b, 0x90170152 } /* use as internal speaker (back) */ } }, + [ALC269_FIXUP_DMI_MATCH] = { + .type = HDA_FIXUP_FUNC, + .v.func = alc269_fixup_match_via_dmi, + }, + [ALC269_FIXUP_AYA_HEADSET_VOLUME] = { + .type = HDA_FIXUP_FUNC, + .v.func = alc269_fixup_headphone_volume, + }, [ALC299_FIXUP_PREDATOR_SPK] = { .type = HDA_FIXUP_PINS, .v.pins = (const struct hda_pintbl[]) { @@ -7329,6 +7385,8 @@ static const struct hda_quirk alc269_fixup_tbl[] = { SND_PCI_QUIRK(0x2782, 0x1701, "Infinix Y4 Max", ALC269VC_FIXUP_INFINIX_Y4_MAX), SND_PCI_QUIRK(0x2782, 0x1705, "MEDION E15433", ALC269VC_FIXUP_INFINIX_Y4_MAX), SND_PCI_QUIRK(0x2782, 0x1707, "Vaio VJFE-ADL", ALC298_FIXUP_SPK_VOLUME), + SND_PCI_QUIRK(0x1f66, 0x0101, "AYANEO 2/GEEK/Ayn MiniPro", ALC269_FIXUP_DMI_MATCH), + SND_PCI_QUIRK(0x1f66, 0x0103, "AYANEO AIR 1S", ALC269VB_FIXUP_AYANEO_SPKR_PIN_FIX), SND_PCI_QUIRK(0x2782, 0x4900, "MEDION E15443", ALC233_FIXUP_MEDION_MTL_SPK), SND_PCI_QUIRK(0x8086, 0x2074, "Intel NUC 8", ALC233_FIXUP_INTEL_NUC8_DMIC), SND_PCI_QUIRK(0x8086, 0x2080, "Intel NUC 8 Rugged", ALC256_FIXUP_INTEL_NUC8_RUGGED), @@ -7455,6 +7513,7 @@ static const struct hda_model_fixup alc269_fixup_models[] = { {.id = ALC269VB_FIXUP_ASUS_ZENBOOK, .name = "asus-zenbook"}, {.id = ALC269VB_FIXUP_ASUS_ZENBOOK_UX31A, .name = "asus-zenbook-ux31a"}, {.id = ALC269VB_FIXUP_ORDISSIMO_EVE2, .name = "ordissimo"}, + {.id = ALC269VB_FIXUP_AYANEO_SPKR_PIN_FIX, .name = "ayaneo-speaker-pin-fix"}, {.id = ALC282_FIXUP_ASUS_TX300, .name = "asus-tx300"}, {.id = ALC283_FIXUP_INT_MIC, .name = "alc283-int-mic"}, {.id = ALC290_FIXUP_MONO_SPEAKERS_HSJACK, .name = "mono-speakers"}, diff --git a/sound/soc/amd/acp/acp-mach.h b/sound/soc/amd/acp/acp-mach.h index f94c30c20f20..c3b3f047969c 100644 --- a/sound/soc/amd/acp/acp-mach.h +++ b/sound/soc/amd/acp/acp-mach.h @@ -29,8 +29,8 @@ enum be_id { HEADSET_BE_ID = 0, AMP_BE_ID, - DMIC_BE_ID, BT_BE_ID, + DMIC_BE_ID, }; enum cpu_endpoints { diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 061791e61907..dde28c7331bc 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -2832,4 +2832,6 @@ config SND_SOC_LPASS_TX_MACRO select SND_SOC_LPASS_MACRO_COMMON tristate "Qualcomm TX Macro in LPASS(Low Power Audio SubSystem)" +source "sound/soc/codecs/aw87xxx/Kconfig" + endmenu diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index d687d4f74363..f32095de202c 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -864,6 +864,7 @@ obj-$(CONFIG_SND_SOC_WSA884X) += snd-soc-wsa884x.o obj-$(CONFIG_SND_SOC_ZL38060) += snd-soc-zl38060.o # Amp +obj-$(CONFIG_SND_SOC_AW87XXX) += aw87xxx/ obj-$(CONFIG_SND_SOC_MAX9877) += snd-soc-max9877.o obj-$(CONFIG_SND_SOC_MAX98504) += snd-soc-max98504.o obj-$(CONFIG_SND_SOC_SIMPLE_AMPLIFIER) += snd-soc-simple-amplifier.o diff --git a/sound/soc/codecs/aw87xxx/Kconfig b/sound/soc/codecs/aw87xxx/Kconfig new file mode 100644 index 000000000000..bd0f208e2cfe --- /dev/null +++ b/sound/soc/codecs/aw87xxx/Kconfig @@ -0,0 +1,5 @@ +config SND_SOC_AW87XXX + tristate "SoC Audio for awinic AW87XXX Smart K PA" + depends on I2C + help + This option enables support for AW87XXX Smart K PA. diff --git a/sound/soc/codecs/aw87xxx/Makefile b/sound/soc/codecs/aw87xxx/Makefile new file mode 100644 index 000000000000..d32f319a5b01 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/Makefile @@ -0,0 +1,4 @@ +#for AWINIC AW87XXX Smart K PA +snd-soc-aw87xxx-objs := aw87xxx.o aw87xxx_device.o aw87xxx_monitor.o aw87xxx_bin_parse.o aw87xxx_dsp.o aw87xxx_acf_bin.o +obj-$(CONFIG_SND_SOC_AW87XXX) += snd-soc-aw87xxx.o + diff --git a/sound/soc/codecs/aw87xxx/aw87xxx.c b/sound/soc/codecs/aw87xxx/aw87xxx.c new file mode 100644 index 000000000000..3d732400e449 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx.c @@ -0,0 +1,1601 @@ +/* + * aw87xxx.c aw87xxx pa module + * + * Copyright (c) 2021 AWINIC Technology CO., LTD + * + * Author: Barry + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx.h" +#include "aw87xxx_device.h" +#include "aw87xxx_log.h" +#include "aw87xxx_monitor.h" +#include "aw87xxx_acf_bin.h" +#include "aw87xxx_bin_parse.h" +#include "aw87xxx_dsp.h" + +/***************************************************************** +* aw87xxx marco +******************************************************************/ +#define AW87XXX_I2C_NAME "aw87xxx_pa" +#define AW87XXX_DRIVER_VERSION "v2.7.0" +#define AW87XXX_FW_BIN_NAME "aw87xxx_acf.bin" +#define AW87XXX_PROF_MUSIC "Music" +/************************************************************************* + * aw87xxx variable + ************************************************************************/ +static LIST_HEAD(g_aw87xxx_list); +static DEFINE_MUTEX(g_aw87xxx_mutex_lock); +unsigned int g_aw87xxx_dev_cnt = 0; + +static const char *const aw87xxx_monitor_switch[] = {"Disable", "Enable"}; +static const char *const aw87xxx_spin_switch[] = {"spin_0", "spin_90", + "spin_180", "spin_270"}; +#if defined(AW_KERNEL_VER_OVER_6_16_1) +static void new_snd_soc_unregister_component(struct device *dev) +{ + return snd_soc_unregister_component_by_driver(dev, NULL); +} +static struct aw_componet_codec_ops aw_componet_codec_ops = { + .add_codec_controls = snd_soc_add_component_controls, + .unregister_codec = new_snd_soc_unregister_component, +}; +#elif defined(AW_KERNEL_VER_OVER_4_19_1) +static struct aw_componet_codec_ops aw_componet_codec_ops = { + .add_codec_controls = snd_soc_add_component_controls, + .unregister_codec = snd_soc_unregister_component, +}; +#else +static struct aw_componet_codec_ops aw_componet_codec_ops = { + .add_codec_controls = snd_soc_add_codec_controls, + .unregister_codec = snd_soc_unregister_codec, +}; +#endif + +enum smi_bus_type { + SMI_I2C, + SMI_SPI, + SMI_AUTO_DETECT, +}; + +struct smi_instance { + const char *type; + unsigned int flags; + int irq_idx; +}; + +struct smi_node { + enum smi_bus_type bus_type; + struct smi_instance instances[]; +}; + +/************************************************************************ + * + * aw87xxx device update profile + * + ************************************************************************/ +static int aw87xxx_power_down(struct aw87xxx *aw87xxx, char *profile) +{ + int ret = 0; + struct aw_prof_desc *prof_desc = NULL; + struct aw_prof_info *prof_info = &aw87xxx->acf_info.prof_info; + struct aw_data_container *data_container = NULL; + struct aw_device *aw_dev = &aw87xxx->aw_dev; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + + if (!prof_info->status) { + AW_DEV_LOGE(aw87xxx->dev, "profile_cfg not load"); + return -EINVAL; + } + + prof_desc = aw87xxx_acf_get_prof_desc_form_name(aw87xxx->dev, &aw87xxx->acf_info, profile); + if (prof_desc == NULL) + goto no_bin_pwr_off; + + if (!prof_desc->prof_st) + goto no_bin_pwr_off; + + + data_container = &prof_desc->data_container; + AW_DEV_LOGD(aw87xxx->dev, "get profile[%s] data len [%d]", + profile, data_container->len); + + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGI(aw87xxx->dev, "profile[%s] has already load ", profile); + } else { + if (aw_dev->ops.pwr_off_func) { + ret = aw_dev->ops.pwr_off_func(aw_dev, data_container); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "load profile[%s] failed ", profile); + goto pwr_off_failed; + } + } else { + ret = aw87xxx_dev_default_pwr_off(aw_dev, data_container); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "load profile[%s] failed ", profile); + goto pwr_off_failed; + } + } + } + + aw87xxx->current_profile = prof_desc->prof_name; + return 0; + +pwr_off_failed: +no_bin_pwr_off: + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, false); + aw87xxx->current_profile = aw87xxx->prof_off_name; + return ret; +} + +static int aw87xxx_power_on(struct aw87xxx *aw87xxx, char *profile) +{ + int ret = -EINVAL; + struct aw_prof_desc *prof_desc = NULL; + struct aw_prof_info *prof_info = &aw87xxx->acf_info.prof_info; + struct aw_data_container *data_container = NULL; + struct aw_device *aw_dev = &aw87xxx->aw_dev; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + + if (!prof_info->status) { + AW_DEV_LOGE(aw87xxx->dev, "profile_cfg not load"); + return -EINVAL; + } + + if (0 == strncmp(profile, aw87xxx->prof_off_name, AW_PROFILE_STR_MAX)) + return aw87xxx_power_down(aw87xxx, profile); + + prof_desc = aw87xxx_acf_get_prof_desc_form_name(aw87xxx->dev, &aw87xxx->acf_info, profile); + if (prof_desc == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "not found [%s] parameter", profile); + return -EINVAL; + } + + if (!prof_desc->prof_st) { + AW_DEV_LOGE(aw87xxx->dev, "not found data container"); + return -EINVAL; + } + + data_container = &prof_desc->data_container; + AW_DEV_LOGD(aw87xxx->dev, "get profile[%s] data len [%d]", + profile, data_container->len); + + if (aw_dev->ops.pwr_on_func) { + ret = aw_dev->ops.pwr_on_func(aw_dev, data_container); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "load profile[%s] failed ", + profile); + return aw87xxx_power_down(aw87xxx, aw87xxx->prof_off_name); + } + } else { + ret = aw87xxx_dev_default_pwr_on(aw_dev, data_container); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "load profile[%s] failed ", + profile); + return aw87xxx_power_down(aw87xxx, aw87xxx->prof_off_name); + } + } + + aw87xxx->current_profile = prof_desc->prof_name; + AW_DEV_LOGD(aw87xxx->dev, "load profile[%s] succeed", profile); + + return 0; +} + + + +int aw87xxx_update_profile(struct aw87xxx *aw87xxx, char *profile) +{ + int ret = -1; + + AW_DEV_LOGD(aw87xxx->dev, "load profile[%s] enter", profile); + mutex_lock(&aw87xxx->reg_lock); + aw87xxx_monitor_stop(&aw87xxx->monitor); + if (0 == strncmp(profile, aw87xxx->prof_off_name, AW_PROFILE_STR_MAX)) { + ret = aw87xxx_power_down(aw87xxx, profile); + } else { + ret = aw87xxx_power_on(aw87xxx, profile); + if (!ret) + aw87xxx_monitor_start(&aw87xxx->monitor); + } + mutex_unlock(&aw87xxx->reg_lock); + + return ret; +} + +int aw87xxx_update_profile_esd(struct aw87xxx *aw87xxx, char *profile) +{ + int ret = -1; + + if (0 == strncmp(profile, aw87xxx->prof_off_name, AW_PROFILE_STR_MAX)) + ret = aw87xxx_power_down(aw87xxx, profile); + else + ret = aw87xxx_power_on(aw87xxx, profile); + + return ret; +} + +char *aw87xxx_show_current_profile(int dev_index) +{ + struct list_head *pos = NULL; + struct aw87xxx *aw87xxx = NULL; + + list_for_each(pos, &g_aw87xxx_list) { + aw87xxx = list_entry(pos, struct aw87xxx, list); + if (aw87xxx->dev_index == dev_index) { + AW_DEV_LOGI(aw87xxx->dev, "current profile is [%s]", + aw87xxx->current_profile); + return aw87xxx->current_profile; + } + } + + AW_LOGE("not found struct aw87xxx, dev_index = [%d]", dev_index); + return NULL; +} +EXPORT_SYMBOL(aw87xxx_show_current_profile); + +int aw87xxx_set_profile(int dev_index, char *profile) +{ + struct list_head *pos = NULL; + struct aw87xxx *aw87xxx = NULL; + + list_for_each(pos, &g_aw87xxx_list) { + aw87xxx = list_entry(pos, struct aw87xxx, list); + if (profile && aw87xxx->dev_index == dev_index) { + AW_DEV_LOGD(aw87xxx->dev, "set dev_index = %d, profile = %s", + dev_index, profile); + return aw87xxx_update_profile(aw87xxx, profile); + } + } + + AW_LOGE("not found struct aw87xxx, dev_index = [%d]", dev_index); + return -EINVAL; +} +EXPORT_SYMBOL(aw87xxx_set_profile); + +int aw87xxx_set_profile_by_id(int dev_index, int profile_id) +{ + char *profile = NULL; + + profile = aw87xxx_ctos_get_prof_name(profile_id); + if (profile == NULL) { + AW_LOGE("aw87xxx, dev_index[%d] profile[%d] not support!", + dev_index, profile_id); + return -EINVAL; + } + + AW_LOGI("aw87xxx, dev_index[%d] set profile[%s] by id[%d]", + dev_index, profile, profile_id); + return aw87xxx_set_profile(dev_index, profile); +} +EXPORT_SYMBOL(aw87xxx_set_profile_by_id); + +/**************************************************************************** + * + * aw87xxx Kcontrols + * + ****************************************************************************/ +static int aw87xxx_profile_switch_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int count = 0; + char *name = NULL; + char *profile_name = NULL; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + + if (aw87xxx == NULL) { + AW_LOGE("get struct aw87xxx failed"); + return -EINVAL; + } + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + + /*make sure have prof */ + count = aw87xxx_acf_get_profile_count(aw87xxx->dev, &aw87xxx->acf_info); + if (count <= 0) { + uinfo->value.enumerated.items = 0; + AW_DEV_LOGE(aw87xxx->dev, "get count[%d] failed", count); + return 0; + } + + uinfo->value.enumerated.items = count; + if (uinfo->value.enumerated.item >= count) + uinfo->value.enumerated.item = count - 1; + + name = uinfo->value.enumerated.name; + count = uinfo->value.enumerated.item; + profile_name = aw87xxx_acf_get_prof_name_form_index(aw87xxx->dev, + &aw87xxx->acf_info, count); + if (profile_name == NULL) { + strscpy(uinfo->value.enumerated.name, "NULL", + strlen("NULL") + 1); + return 0; + } + + strscpy(name, profile_name, sizeof(uinfo->value.enumerated.name)); + + return 0; +} + +static int aw87xxx_profile_switch_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + int ret = -1; + char *profile_name = NULL; + int index = ucontrol->value.integer.value[0]; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + struct acf_bin_info *acf_info = NULL; + + if (aw87xxx == NULL) { + AW_LOGE("get struct aw87xxx failed"); + return -EINVAL; + } + + acf_info = &aw87xxx->acf_info; + + profile_name = aw87xxx_acf_get_prof_name_form_index(aw87xxx->dev, acf_info, index); + if (!profile_name) { + AW_DEV_LOGE(aw87xxx->dev, "not found profile name,index=[%d]", + index); + return -EINVAL; + } + + AW_DEV_LOGI(aw87xxx->dev, "set profile [%s]", profile_name); + + ret = aw87xxx_update_profile(aw87xxx, profile_name); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "set dev_index[%d] profile failed, profile = %s", + aw87xxx->dev_index, profile_name); + return ret; + } + + return 0; +} + +static int aw87xxx_profile_switch_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + int index = 0; + char *profile; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + + if (aw87xxx == NULL) { + AW_LOGE("get struct aw87xxx failed"); + return -EINVAL; + } + + if (!aw87xxx->current_profile) { + AW_DEV_LOGE(aw87xxx->dev, "profile not init"); + return -EINVAL; + } + + profile = aw87xxx->current_profile; + AW_DEV_LOGI(aw87xxx->dev, "current profile:[%s]", + aw87xxx->current_profile); + + + index = aw87xxx_acf_get_prof_index_form_name(aw87xxx->dev, + &aw87xxx->acf_info, aw87xxx->current_profile); + if (index < 0) { + AW_DEV_LOGE(aw87xxx->dev, "get profile index failed"); + return index; + } + + ucontrol->value.integer.value[0] = index; + + return 0; +} + +static int aw87xxx_vmax_get_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = INT_MIN; + uinfo->value.integer.max = AW_VMAX_MAX; + + return 0; +} + +static int aw87xxx_vmax_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + int ret = -1; + int vmax_val = 0; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + + if (aw87xxx == NULL) { + AW_LOGE("get struct aw87xxx failed"); + return -EINVAL; + } + + ret = aw87xxx_monitor_no_dsp_get_vmax(&aw87xxx->monitor, &vmax_val); + if (ret < 0) + return ret; + + ucontrol->value.integer.value[0] = vmax_val; + AW_DEV_LOGI(aw87xxx->dev, "get vmax = [0x%x]", vmax_val); + + return 0; +} + +static int aw87xxx_monitor_switch_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int count; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + count = ARRAY_SIZE(aw87xxx_monitor_switch); + + uinfo->value.enumerated.items = count; + + if (uinfo->value.enumerated.item >= count) + uinfo->value.enumerated.item = count - 1; + + strscpy(uinfo->value.enumerated.name, + aw87xxx_monitor_switch[uinfo->value.enumerated.item], + strlen(aw87xxx_monitor_switch[uinfo->value.enumerated.item]) + 1); + + return 0; +} + +static int aw87xxx_monitor_switch_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + uint32_t ctrl_value = ucontrol->value.integer.value[0]; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + struct aw_monitor *aw_monitor = &aw87xxx->monitor; + int ret = -1; + + ret = aw87xxx_dev_monitor_switch_set(aw_monitor, ctrl_value); + if (ret) + return ret; + + return 0; +} + +static int aw87xxx_monitor_switch_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + struct aw_monitor *aw_monitor = &aw87xxx->monitor; + + ucontrol->value.integer.value[0] = aw_monitor->monitor_hdr.monitor_switch; + + AW_DEV_LOGI(aw87xxx->dev, "monitor switch is %ld", ucontrol->value.integer.value[0]); + return 0; +} + +static int aw87xxx_spin_switch_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int count; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + count = ARRAY_SIZE(aw87xxx_spin_switch); + + uinfo->value.enumerated.items = count; + + if (uinfo->value.enumerated.item >= count) + uinfo->value.enumerated.item = count - 1; + + strscpy(uinfo->value.enumerated.name, + aw87xxx_spin_switch[uinfo->value.enumerated.item], + strlen(aw87xxx_spin_switch[uinfo->value.enumerated.item]) + 1); + + return 0; +} + +static int aw87xxx_spin_switch_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + uint32_t ctrl_value = 0; + int ret = 0; + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + ctrl_value = ucontrol->value.integer.value[0]; + + ret = aw87xxx_dsp_set_spin(ctrl_value); + if (ret) { + AW_DEV_LOGE(aw87xxx->dev, "write spin failed"); + return ret; + } + AW_DEV_LOGD(aw87xxx->dev, "write spin done ctrl_value=%d", ctrl_value); + return 0; +} + +static int aw87xxx_spin_switch_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct aw87xxx *aw87xxx = (struct aw87xxx *)kcontrol->private_value; + + ucontrol->value.integer.value[0] = aw87xxx_dsp_get_spin(); + AW_DEV_LOGD(aw87xxx->dev, "current spin is %ld", ucontrol->value.integer.value[0]); + + return 0; +} + + +static int aw87xxx_kcontrol_dynamic_create(struct aw87xxx *aw87xxx, + void *codec) +{ + struct snd_kcontrol_new *aw87xxx_kcontrol = NULL; + aw_snd_soc_codec_t *soc_codec = (aw_snd_soc_codec_t *)codec; + char *kctl_name[AW87XXX_PRIVATE_KCONTROL_NUM]; + int kcontrol_num = AW87XXX_PRIVATE_KCONTROL_NUM; + int ret = -1; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + aw87xxx->codec = soc_codec; + + aw87xxx_kcontrol = devm_kzalloc(aw87xxx->dev, + sizeof(struct snd_kcontrol_new) * kcontrol_num, + GFP_KERNEL); + if (aw87xxx_kcontrol == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "aw87xxx_kcontrol devm_kzalloc failed"); + return -ENOMEM; + } + + kctl_name[0] = devm_kzalloc(aw87xxx->dev, AW_NAME_BUF_MAX, + GFP_KERNEL); + if (kctl_name[0] == NULL) + return -ENOMEM; + + snprintf(kctl_name[0], AW_NAME_BUF_MAX, "aw87xxx_profile_switch_%d", + aw87xxx->dev_index); + + aw87xxx_kcontrol[0].name = kctl_name[0]; + aw87xxx_kcontrol[0].iface = SNDRV_CTL_ELEM_IFACE_MIXER; + aw87xxx_kcontrol[0].info = aw87xxx_profile_switch_info; + aw87xxx_kcontrol[0].get = aw87xxx_profile_switch_get; + aw87xxx_kcontrol[0].put = aw87xxx_profile_switch_put; + aw87xxx_kcontrol[0].private_value = (unsigned long)aw87xxx; + + kctl_name[1] = devm_kzalloc(aw87xxx->codec->dev, AW_NAME_BUF_MAX, + GFP_KERNEL); + if (kctl_name[1] == NULL) + return -ENOMEM; + + snprintf(kctl_name[1], AW_NAME_BUF_MAX, "aw87xxx_vmax_get_%d", + aw87xxx->dev_index); + + aw87xxx_kcontrol[1].name = kctl_name[1]; + aw87xxx_kcontrol[1].iface = SNDRV_CTL_ELEM_IFACE_MIXER; + aw87xxx_kcontrol[1].access = SNDRV_CTL_ELEM_ACCESS_READ; + aw87xxx_kcontrol[1].info = aw87xxx_vmax_get_info; + aw87xxx_kcontrol[1].get = aw87xxx_vmax_get; + aw87xxx_kcontrol[1].private_value = (unsigned long)aw87xxx; + + kctl_name[2] = devm_kzalloc(aw87xxx->codec->dev, AW_NAME_BUF_MAX, + GFP_KERNEL); + if (kctl_name[2] == NULL) + return -ENOMEM; + + snprintf(kctl_name[2], AW_NAME_BUF_MAX, "aw87xxx_monitor_switch_%d", + aw87xxx->dev_index); + + aw87xxx_kcontrol[2].name = kctl_name[2]; + aw87xxx_kcontrol[2].iface = SNDRV_CTL_ELEM_IFACE_MIXER; + aw87xxx_kcontrol[2].info = aw87xxx_monitor_switch_info; + aw87xxx_kcontrol[2].get = aw87xxx_monitor_switch_get; + aw87xxx_kcontrol[2].put = aw87xxx_monitor_switch_put; + aw87xxx_kcontrol[2].private_value = (unsigned long)aw87xxx; + + ret = aw_componet_codec_ops.add_codec_controls(aw87xxx->codec, + aw87xxx_kcontrol, kcontrol_num); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "add codec controls failed, ret = %d", + ret); + return ret; + } + + AW_DEV_LOGI(aw87xxx->dev, "add codec controls[%s,%s,%s]", + aw87xxx_kcontrol[0].name, + aw87xxx_kcontrol[1].name, + aw87xxx_kcontrol[2].name); + + return 0; +} + +static int aw87xxx_public_kcontrol_create(struct aw87xxx *aw87xxx, + void *codec) +{ + struct snd_kcontrol_new *aw87xxx_kcontrol = NULL; + aw_snd_soc_codec_t *soc_codec = (aw_snd_soc_codec_t *)codec; + char *kctl_name[AW87XXX_PUBLIC_KCONTROL_NUM]; + int kcontrol_num = AW87XXX_PUBLIC_KCONTROL_NUM; + int ret = -1; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + aw87xxx->codec = soc_codec; + + aw87xxx_kcontrol = devm_kzalloc(aw87xxx->dev, + sizeof(struct snd_kcontrol_new) * kcontrol_num, + GFP_KERNEL); + if (aw87xxx_kcontrol == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "aw87xxx_kcontrol devm_kzalloc failed"); + return -ENOMEM; + } + + kctl_name[0] = devm_kzalloc(aw87xxx->dev, AW_NAME_BUF_MAX, + GFP_KERNEL); + if (kctl_name[0] == NULL) + return -ENOMEM; + + snprintf(kctl_name[0], AW_NAME_BUF_MAX, "aw87xxx_spin_switch"); + + aw87xxx_kcontrol[0].name = kctl_name[0]; + aw87xxx_kcontrol[0].iface = SNDRV_CTL_ELEM_IFACE_MIXER; + aw87xxx_kcontrol[0].info = aw87xxx_spin_switch_info; + aw87xxx_kcontrol[0].get = aw87xxx_spin_switch_get; + aw87xxx_kcontrol[0].put = aw87xxx_spin_switch_put; + aw87xxx_kcontrol[0].private_value = (unsigned long)aw87xxx; + + ret = aw_componet_codec_ops.add_codec_controls(aw87xxx->codec, + aw87xxx_kcontrol, kcontrol_num); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "add codec controls failed, ret = %d", + ret); + return ret; + } + + AW_DEV_LOGI(aw87xxx->dev, "add public codec controls[%s]", + aw87xxx_kcontrol[0].name); + + return 0; +} + +/**************************************************************************** + * + *aw87xxx kcontrol create + * + ****************************************************************************/ +int aw87xxx_add_codec_controls(void *codec) +{ + struct list_head *pos = NULL; + struct aw87xxx *aw87xxx = NULL; + int ret = -1; + + list_for_each(pos, &g_aw87xxx_list) { + aw87xxx = list_entry(pos, struct aw87xxx, list); + ret = aw87xxx_kcontrol_dynamic_create(aw87xxx, codec); + if (ret < 0) + return ret; + + if (aw87xxx->dev_index == 0) { + ret = aw87xxx_public_kcontrol_create(aw87xxx, codec); + if (ret < 0) + return ret; + } + } + + return 0; +} +EXPORT_SYMBOL(aw87xxx_add_codec_controls); + + +/**************************************************************************** + * + * aw87xxx firmware cfg load + * + ***************************************************************************/ +static void aw87xxx_fw_cfg_free(struct aw87xxx *aw87xxx) +{ + AW_DEV_LOGD(aw87xxx->dev, "enter"); + aw87xxx_acf_profile_free(aw87xxx->dev, &aw87xxx->acf_info); + aw87xxx_monitor_cfg_free(&aw87xxx->monitor); +} + +static int aw87xxx_init_default_prof(struct aw87xxx *aw87xxx) +{ + char *profile = NULL; + + profile = aw87xxx_acf_get_prof_off_name(aw87xxx->dev, &aw87xxx->acf_info); + if (profile == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "get profile off name failed"); + return -EINVAL; + } + + snprintf(aw87xxx->prof_off_name, AW_PROFILE_STR_MAX, "%s", profile); + aw87xxx->current_profile = profile; + AW_DEV_LOGI(aw87xxx->dev, "init profile name [%s]", + aw87xxx->current_profile); + + return 0; +} + +static void aw87xxx_fw_load_retry(struct aw87xxx *aw87xxx) +{ + struct acf_bin_info *acf_info = &aw87xxx->acf_info; + int ram_timer_val = 2000; + + AW_DEV_LOGD(aw87xxx->dev, "failed to read [%s]", + aw87xxx->fw_name); + + if (acf_info->load_count < AW_LOAD_FW_RETRIES) { + AW_DEV_LOGD(aw87xxx->dev, + "restart hrtimer to load firmware"); + schedule_delayed_work(&aw87xxx->fw_load_work, + msecs_to_jiffies(ram_timer_val)); + } else { + acf_info->load_count = 0; + AW_DEV_LOGE(aw87xxx->dev, + "can not load firmware,please check name or file exists"); + return; + } + acf_info->load_count++; +} + +static void aw87xxx_fw_load(const struct firmware *fw, void *context) +{ + int ret = -1; + struct aw87xxx *aw87xxx = context; + struct acf_bin_info *acf_info = &aw87xxx->acf_info; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + + if (!fw) { + aw87xxx_fw_load_retry(aw87xxx); + return; + } + + AW_DEV_LOGD(aw87xxx->dev, "loaded %s - size: %ld", + aw87xxx->fw_name, (u_long)(fw ? fw->size : 0)); + + mutex_lock(&aw87xxx->reg_lock); + acf_info->fw_data = vmalloc(fw->size); + if (!acf_info->fw_data) { + AW_DEV_LOGE(aw87xxx->dev, "fw_data kzalloc memory failed"); + goto exit_vmalloc_failed; + } + memset(acf_info->fw_data, 0, fw->size); + memcpy(acf_info->fw_data, fw->data, fw->size); + acf_info->fw_size = fw->size; + + ret = aw87xxx_acf_parse(aw87xxx->dev, &aw87xxx->acf_info); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "fw_data parse failed"); + goto exit_acf_parse_failed; + } + + ret = aw87xxx_init_default_prof(aw87xxx); + if (ret < 0) { + aw87xxx_fw_cfg_free(aw87xxx); + goto exit_acf_parse_failed; + } + + AW_DEV_LOGI(aw87xxx->dev, "acf parse succeed"); + mutex_unlock(&aw87xxx->reg_lock); + release_firmware(fw); + // Updating profile to "Music" because the firmware is set to "off" during init + aw87xxx_update_profile(aw87xxx, AW87XXX_PROF_MUSIC); + + return; + +exit_acf_parse_failed: +exit_vmalloc_failed: + release_firmware(fw); + mutex_unlock(&aw87xxx->reg_lock); +} + +static void aw87xxx_fw_load_work_routine(struct work_struct *work) +{ + struct aw87xxx *aw87xxx = container_of(work, + struct aw87xxx, fw_load_work.work); + struct aw_prof_info *prof_info = &aw87xxx->acf_info.prof_info; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + + if (prof_info->status == AW_ACF_WAIT) { + request_firmware_nowait(THIS_MODULE, +// FW_ACTION_HOTPLUG, + FW_ACTION_UEVENT, + aw87xxx->fw_name, + aw87xxx->dev, + GFP_KERNEL, aw87xxx, + aw87xxx_fw_load); + } +} + +static const struct dmi_system_id firmware_names[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), + }, + .driver_data = (void *)"aw87xxx_acf_kun.bin", + }, + { + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "Loki MiniPro"), + }, + .driver_data = (void *)"aw87xxx_acf_minipro.bin", + }, + { + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "AIR 1S"), + }, + .driver_data = (void *)"aw87xxx_acf_air1s.bin", + }, + { + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "AIR Plus"), + }, + .driver_data = (void *)"aw87xxx_acf_airplus.bin", + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "AYANEO"), + DMI_MATCH(DMI_PRODUCT_NAME, "FLIP"), + }, + .driver_data = (void *)"aw87xxx_acf_flip.bin", + }, + { + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "NEO-01"), + }, + .driver_data = (void *)"aw87xxx_acf_orangepi.bin", + }, + {} +}; + +static void aw87xxx_fw_load_init(struct aw87xxx *aw87xxx) +{ +#ifdef AW_CFG_UPDATE_DELAY + int cfg_timer_val = AW_CFG_UPDATE_DELAY_TIMER; +#else + int cfg_timer_val = 0; +#endif + const struct dmi_system_id *fwname_sysid = dmi_first_match(firmware_names); + const char *fwname = fwname_sysid ? + fwname_sysid->driver_data : AW87XXX_FW_BIN_NAME; + + AW_DEV_LOGI(aw87xxx->dev, "loading firmware name: [%s]", fwname); + snprintf(aw87xxx->fw_name, AW87XXX_FW_NAME_MAX, "%s", fwname); + aw87xxx_acf_init(&aw87xxx->aw_dev, &aw87xxx->acf_info, aw87xxx->dev_index); + + INIT_DELAYED_WORK(&aw87xxx->fw_load_work, aw87xxx_fw_load_work_routine); + schedule_delayed_work(&aw87xxx->fw_load_work, + msecs_to_jiffies(cfg_timer_val)); +} + +/**************************************************************************** + * + *aw87xxx attribute node + * + ****************************************************************************/ +static ssize_t aw87xxx_attr_get_reg(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + int ret = 0; + unsigned int i = 0; + unsigned char reg_val = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_device *aw_dev = &aw87xxx->aw_dev; + + mutex_lock(&aw87xxx->reg_lock); + for (i = 0; i < aw_dev->reg_max_addr; i++) { + if (!(aw_dev->reg_access[i] & AW_DEV_REG_RD_ACCESS)) + continue; + ret = aw87xxx_dev_i2c_read_byte(&aw87xxx->aw_dev, i, ®_val); + if (ret < 0) { + len += snprintf(buf + len, PAGE_SIZE - len, + "read reg [0x%x] failed\n", i); + AW_DEV_LOGE(aw87xxx->dev, "read reg [0x%x] failed", i); + } else { + len += snprintf(buf + len, PAGE_SIZE - len, + "reg:0x%02X=0x%02X\n", i, reg_val); + AW_DEV_LOGD(aw87xxx->dev, "reg:0x%02X=0x%02X", + i, reg_val); + } + } + mutex_unlock(&aw87xxx->reg_lock); + + return len; +} + +static ssize_t aw87xxx_attr_set_reg(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t len) +{ + unsigned int databuf[2] = { 0 }; + int ret = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + + mutex_lock(&aw87xxx->reg_lock); + if (sscanf(buf, "0x%x 0x%x", &databuf[0], &databuf[1]) == 2) { + if (databuf[0] >= aw87xxx->aw_dev.reg_max_addr) { + AW_DEV_LOGE(aw87xxx->dev, "set reg[0x%x] error,is out of reg_addr_max[0x%x]", + databuf[0], aw87xxx->aw_dev.reg_max_addr); + mutex_unlock(&aw87xxx->reg_lock); + return -EINVAL; + } + + ret = aw87xxx_dev_i2c_write_byte(&aw87xxx->aw_dev, + databuf[0], databuf[1]); + if (ret < 0) + AW_DEV_LOGE(aw87xxx->dev, "set [0x%x]=0x%x failed", + databuf[0], databuf[1]); + else + AW_DEV_LOGD(aw87xxx->dev, "set [0x%x]=0x%x succeed", + databuf[0], databuf[1]); + } else { + AW_DEV_LOGE(aw87xxx->dev, "i2c write cmd input error"); + } + mutex_unlock(&aw87xxx->reg_lock); + + return len; +} + +static ssize_t aw87xxx_attr_get_profile(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + unsigned int i = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_prof_info *prof_info = &aw87xxx->acf_info.prof_info; + + if (!prof_info->status) { + len += snprintf(buf + len, PAGE_SIZE - len, + "profile_cfg not load\n"); + return len; + } + + AW_DEV_LOGI(aw87xxx->dev, "current profile:[%s]", aw87xxx->current_profile); + + for (i = 0; i < prof_info->count; i++) { + if (!strncmp(aw87xxx->current_profile, prof_info->prof_name_list[i], + AW_PROFILE_STR_MAX)) + len += snprintf(buf + len, PAGE_SIZE - len, + ">%s\n", prof_info->prof_name_list[i]); + else + len += snprintf(buf + len, PAGE_SIZE - len, + " %s\n", prof_info->prof_name_list[i]); + } + + return len; +} + +static ssize_t aw87xxx_attr_set_profile(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t len) +{ + char profile[AW_PROFILE_STR_MAX] = {0}; + int ret = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + + if (strlen(buf) > AW_PROFILE_STR_MAX) { + AW_DEV_LOGE(aw87xxx->dev, "input profile_str_len is out of max[%d]", + AW_PROFILE_STR_MAX); + return -EINVAL; + } + + if (sscanf(buf, "%s", profile) == 1) { + AW_DEV_LOGD(aw87xxx->dev, "set profile [%s]", profile); + ret = aw87xxx_update_profile(aw87xxx, profile); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "set profile[%s] failed", + profile); + return ret; + } + } + + return len; +} + +static ssize_t aw87xxx_attr_get_hwen(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + int hwen = aw87xxx->aw_dev.hwen_status; + + if (hwen >= AW_DEV_HWEN_INVALID) + len += snprintf(buf + len, PAGE_SIZE - len, "hwen_status: invalid\n"); + else if (hwen == AW_DEV_HWEN_ON) + len += snprintf(buf + len, PAGE_SIZE - len, "hwen_status: on\n"); + else if (hwen == AW_DEV_HWEN_OFF) + len += snprintf(buf + len, PAGE_SIZE - len, "hwen_status: off\n"); + + return len; +} + +static ssize_t aw87xxx_attr_set_hwen(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t len) +{ + int ret = -1; + unsigned int state; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + + ret = kstrtouint(buf, 0, &state); + if (ret) { + AW_DEV_LOGE(aw87xxx->dev, "fail to channelge str to int"); + return ret; + } + + mutex_lock(&aw87xxx->reg_lock); + if (state == AW_DEV_HWEN_OFF) + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, false); /*OFF*/ + else if (state == AW_DEV_HWEN_ON) + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, true); /*ON*/ + else + AW_DEV_LOGE(aw87xxx->dev, "input [%d] error, hwen_on=[%d],hwen_off=[%d]", + state, AW_DEV_HWEN_ON, AW_DEV_HWEN_OFF); + mutex_unlock(&aw87xxx->reg_lock); + return len; +} + +int aw87xxx_awrw_write(struct aw87xxx *aw87xxx, + const char *buf, size_t count) +{ + int i = 0, ret = -1; + char *data_buf = NULL; + int buf_len = 0; + int temp_data = 0; + int data_str_size = 0; + char *reg_data; + struct aw_i2c_packet *packet = &aw87xxx->i2c_packet; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + /* one addr or one data string Composition of Contains two bytes of symbol(0X)*/ + /* and two byte of hexadecimal data*/ + data_str_size = 2 + 2 * AWRW_DATA_BYTES; + + /* The buf includes the first address of the register to be written and all data */ + buf_len = AWRW_ADDR_BYTES + packet->reg_num * AWRW_DATA_BYTES; + AW_DEV_LOGI(aw87xxx->dev, "buf_len = %d,reg_num = %d", buf_len, packet->reg_num); + data_buf = vmalloc(buf_len); + if (data_buf == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "alloc memory failed"); + return -ENOMEM; + } + memset(data_buf, 0, buf_len); + + data_buf[0] = packet->reg_addr; + reg_data = data_buf + 1; + + AW_DEV_LOGD(aw87xxx->dev, "reg_addr: 0x%02x", data_buf[0]); + + /*ag:0x00 0x01 0x01 0x01 0x01 0x00\x0a*/ + for (i = 0; i < packet->reg_num; i++) { + ret = sscanf(buf + AWRW_HDR_LEN + 1 + i * (data_str_size + 1), + "0x%x", &temp_data); + if (ret != 1) { + AW_DEV_LOGE(aw87xxx->dev, "sscanf failed,ret=%d", ret); + vfree(data_buf); + data_buf = NULL; + return ret; + } + reg_data[i] = temp_data; + AW_DEV_LOGD(aw87xxx->dev, "[%d] : 0x%02x", i, reg_data[i]); + } + + mutex_lock(&aw87xxx->reg_lock); + ret = i2c_master_send(aw87xxx->aw_dev.i2c, data_buf, buf_len); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "write failed"); + vfree(data_buf); + data_buf = NULL; + return -EFAULT; + } + mutex_unlock(&aw87xxx->reg_lock); + + vfree(data_buf); + data_buf = NULL; + + AW_DEV_LOGD(aw87xxx->dev, "down"); + return 0; +} + +static int aw87xxx_awrw_data_check(struct aw87xxx *aw87xxx, + int *data, size_t count) +{ + struct aw_i2c_packet *packet = &aw87xxx->i2c_packet; + int req_data_len = 0; + int act_data_len = 0; + int data_str_size = 0; + + if ((data[AWRW_HDR_ADDR_BYTES] != AWRW_ADDR_BYTES) || + (data[AWRW_HDR_DATA_BYTES] != AWRW_DATA_BYTES)) { + AW_DEV_LOGE(aw87xxx->dev, "addr_bytes [%d] or data_bytes [%d] unsupport", + data[AWRW_HDR_ADDR_BYTES], data[AWRW_HDR_DATA_BYTES]); + return -EINVAL; + } + + /* one data string Composition of Contains two bytes of symbol(0x)*/ + /* and two byte of hexadecimal data*/ + data_str_size = 2 + 2 * AWRW_DATA_BYTES; + act_data_len = count - AWRW_HDR_LEN - 1; + + /* There is a comma(,) or space between each piece of data */ + if (data[AWRW_HDR_WR_FLAG] == AWRW_FLAG_WRITE) { + /*ag:0x00 0x01 0x01 0x01 0x01 0x00\x0a*/ + req_data_len = (data_str_size + 1) * packet->reg_num; + if (req_data_len > act_data_len) { + AW_DEV_LOGE(aw87xxx->dev, "data_len checkfailed,requeset data_len [%d],actaul data_len [%d]", + req_data_len, act_data_len); + return -EINVAL; + } + } + + return 0; +} + +/* flag addr_bytes data_bytes reg_num reg_addr*/ +static int aw87xxx_awrw_parse_buf(struct aw87xxx *aw87xxx, + const char *buf, size_t count, int *wr_status) +{ + int data[AWRW_HDR_MAX] = {0}; + struct aw_i2c_packet *packet = &aw87xxx->i2c_packet; + int ret = -1; + + if (sscanf(buf, "0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", + &data[AWRW_HDR_WR_FLAG], &data[AWRW_HDR_ADDR_BYTES], + &data[AWRW_HDR_DATA_BYTES], &data[AWRW_HDR_REG_NUM], + &data[AWRW_HDR_REG_ADDR]) == 5) { + + packet->reg_addr = data[AWRW_HDR_REG_ADDR]; + packet->reg_num = data[AWRW_HDR_REG_NUM]; + *wr_status = data[AWRW_HDR_WR_FLAG]; + ret = aw87xxx_awrw_data_check(aw87xxx, data, count); + if (ret < 0) + return ret; + + return 0; + } + + return -EINVAL; +} + +static ssize_t aw87xxx_attr_awrw_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_i2c_packet *packet = &aw87xxx->i2c_packet; + int wr_status = 0; + int ret = -1; + + if (count < AWRW_HDR_LEN) { + AW_DEV_LOGE(aw87xxx->dev, "data count too smaller, please check write format"); + AW_DEV_LOGE(aw87xxx->dev, "string %s,count=%ld", + buf, (u_long)count); + return -EINVAL; + } + + AW_DEV_LOGI(aw87xxx->dev, "string:[%s],count=%ld", buf, (u_long)count); + ret = aw87xxx_awrw_parse_buf(aw87xxx, buf, count, &wr_status); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "can not parse string"); + return ret; + } + + if (wr_status == AWRW_FLAG_WRITE) { + ret = aw87xxx_awrw_write(aw87xxx, buf, count); + if (ret < 0) + return ret; + } else if (wr_status == AWRW_FLAG_READ) { + packet->status = AWRW_I2C_ST_READ; + AW_DEV_LOGI(aw87xxx->dev, "read_cmd:reg_addr[0x%02x], reg_num[%d]", + packet->reg_addr, packet->reg_num); + } else { + AW_DEV_LOGE(aw87xxx->dev, "please check str format, unsupport read_write_status: %d", + wr_status); + return -EINVAL; + } + + return count; +} + +static ssize_t aw87xxx_attr_awrw_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_i2c_packet *packet = &aw87xxx->i2c_packet; + int data_len = 0; + size_t len = 0; + int ret = -1, i = 0; + char *reg_data = NULL; + + if (packet->status != AWRW_I2C_ST_READ) { + AW_DEV_LOGE(aw87xxx->dev, "please write read cmd first"); + return -EINVAL; + } + + data_len = AWRW_DATA_BYTES * packet->reg_num; + reg_data = (char *)vmalloc(data_len); + if (reg_data == NULL) { + AW_DEV_LOGE(aw87xxx->dev, "memory alloc failed"); + ret = -EINVAL; + goto exit; + } + + mutex_lock(&aw87xxx->reg_lock); + ret = aw87xxx_dev_i2c_read_msg(&aw87xxx->aw_dev, packet->reg_addr, + (char *)reg_data, data_len); + if (ret < 0) { + ret = -EFAULT; + mutex_unlock(&aw87xxx->reg_lock); + goto exit; + } + mutex_unlock(&aw87xxx->reg_lock); + + AW_DEV_LOGI(aw87xxx->dev, "reg_addr 0x%02x, reg_num %d", + packet->reg_addr, packet->reg_num); + + for (i = 0; i < data_len; i++) { + len += snprintf(buf + len, PAGE_SIZE - len, + "0x%02x,", reg_data[i]); + AW_DEV_LOGI(aw87xxx->dev, "0x%02x", reg_data[i]); + } + + ret = len; + +exit: + if (reg_data) { + vfree(reg_data); + reg_data = NULL; + } + packet->status = AWRW_I2C_ST_NONE; + return ret; +} + +static ssize_t aw87xxx_drv_ver_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + + len += snprintf(buf + len, PAGE_SIZE - len, + "driver_ver: %s \n", AW87XXX_DRIVER_VERSION); + + return len; +} + +static DEVICE_ATTR(reg, S_IWUSR | S_IRUGO, + aw87xxx_attr_get_reg, aw87xxx_attr_set_reg); +static DEVICE_ATTR(profile, S_IWUSR | S_IRUGO, + aw87xxx_attr_get_profile, aw87xxx_attr_set_profile); +static DEVICE_ATTR(hwen, S_IWUSR | S_IRUGO, + aw87xxx_attr_get_hwen, aw87xxx_attr_set_hwen); +static DEVICE_ATTR(awrw, S_IWUSR | S_IRUGO, + aw87xxx_attr_awrw_show, aw87xxx_attr_awrw_store); +static DEVICE_ATTR(drv_ver, S_IRUGO, aw87xxx_drv_ver_show, NULL); + +static struct attribute *aw87xxx_attributes[] = { + &dev_attr_reg.attr, + &dev_attr_profile.attr, + &dev_attr_hwen.attr, + &dev_attr_awrw.attr, + &dev_attr_drv_ver.attr, + NULL +}; + +static struct attribute_group aw87xxx_attribute_group = { + .attrs = aw87xxx_attributes +}; + +/**************************************************************************** + * + *aw87xxx device probe + * + ****************************************************************************/ +static const struct acpi_gpio_params reset_gpio = { 0, 0, false }; +static const struct acpi_gpio_mapping reset_acpi_gpios[] = { + { "reset-gpios", &reset_gpio, 1 }, + { } +}; + +static struct aw87xxx *aw87xxx_malloc_init(struct i2c_client *client) +{ + struct aw87xxx *aw87xxx = NULL; + + aw87xxx = devm_kzalloc(&client->dev, sizeof(struct aw87xxx), + GFP_KERNEL); + if (aw87xxx == NULL) { + AW_DEV_LOGE(&client->dev, "failed to devm_kzalloc aw87xxx"); + return NULL; + } + memset(aw87xxx, 0, sizeof(struct aw87xxx)); + + aw87xxx->dev = &client->dev; + aw87xxx->aw_dev.dev = &client->dev; + aw87xxx->aw_dev.i2c_bus = client->adapter->nr; + aw87xxx->aw_dev.i2c_addr = client->addr; + aw87xxx->aw_dev.i2c = client; + aw87xxx->aw_dev.hwen_status = false; + aw87xxx->aw_dev.reg_access = NULL; + aw87xxx->aw_dev.hwen_status = AW_DEV_HWEN_INVALID; + aw87xxx->off_bin_status = AW87XXX_NO_OFF_BIN; + aw87xxx->codec = NULL; + aw87xxx->current_profile = aw87xxx->prof_off_name; + + mutex_init(&aw87xxx->reg_lock); + + AW_DEV_LOGI(&client->dev, "Driver struct alloc and mutex init done, devinfo: i2c_bus=%u, i2c_addr=%x", client->adapter->nr, client->addr); + return aw87xxx; +} + +static int aw87xxx_i2c_probe(struct i2c_client *client) +{ + struct device_node *dev_node = client->dev.of_node; + const struct smi_node *node; + struct acpi_device *adev = ACPI_COMPANION(&client->dev); + struct aw87xxx *aw87xxx = NULL; + struct gpio_desc *gpiod = NULL; + struct i2c_board_info board_info = {}; + char i2c_name[32]; + int ret = -1; + int acpi_dev_count = 0; + + /* aw87xxx Get APCI I2C device count */ + if(g_aw87xxx_dev_cnt == 0){ + acpi_dev_count = i2c_acpi_client_count(adev); + AW_DEV_LOGI(&client->dev, "I2C_ACPI_CLIENT_COUNT returned [%d]", acpi_dev_count); + } + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + AW_DEV_LOGE(&client->dev, "check_functionality failed"); + ret = -ENODEV; + goto exit_check_functionality_failed; + } + + /* aw87xxx i2c_dev struct init */ + aw87xxx = aw87xxx_malloc_init(client); + if (aw87xxx == NULL) + goto exit_malloc_init_failed; + + i2c_set_clientdata(client, aw87xxx); + + aw87xxx_device_parse_port_id_dt(&aw87xxx->aw_dev); + aw87xxx_device_parse_topo_id_dt(&aw87xxx->aw_dev); + + /* aw87xxx Get ACPI GPIO */ + + if (g_aw87xxx_dev_cnt == 0){ + ret = devm_acpi_dev_add_driver_gpios(aw87xxx->dev, reset_acpi_gpios); + if(ret){ + AW_DEV_LOGE(aw87xxx->dev, "Unable to add GPIO mapping table"); + goto exit_device_init_failed; + } + + gpiod = devm_gpiod_get(aw87xxx->dev, "reset", GPIOD_OUT_LOW); + if (gpiod == NULL){ + AW_DEV_LOGE(aw87xxx->dev, "Gpiod returned NULL failing gracefully."); + goto exit_device_init_failed; + } + + if (IS_ERR(gpiod)){ + AW_DEV_LOGE(aw87xxx->dev, "Get gpiod failed."); + goto exit_device_init_failed; + } + + aw87xxx->aw_dev.rst_gpio = desc_to_gpio(gpiod); + aw87xxx->aw_dev.hwen_status = AW_DEV_HWEN_OFF; + AW_DEV_LOGI(aw87xxx->dev, "reset gpio[%x] parse succeed", aw87xxx->aw_dev.rst_gpio); + + if (gpio_is_valid(aw87xxx->aw_dev.rst_gpio)) { + ret = devm_gpio_request_one(aw87xxx->dev, aw87xxx->aw_dev.rst_gpio, GPIOF_OUT_INIT_LOW, "aw87xxx_reset"); + if ((ret < 0) && (ret != -EBUSY)) { + AW_DEV_LOGE(aw87xxx->dev, "reset request failed, returned [%d]", ret); + goto exit_device_init_failed; + } + }else{ + /*Disabling RESET GPIO*/ + AW_DEV_LOGI(aw87xxx->dev, "no reset gpio provided, hardware reset unavailable"); + aw87xxx->aw_dev.rst_gpio = AW_NO_RESET_GPIO; + aw87xxx->aw_dev.hwen_status = AW_DEV_HWEN_INVALID; + } + + } + + /*hw power on PA*/ + if(g_aw87xxx_dev_cnt == 0) { + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, true); + } + + /* aw87xxx devices private attributes init */ + ret = aw87xxx_dev_init(&aw87xxx->aw_dev); + if (ret < 0) + goto exit_device_init_failed; + + /*product register reset */ + aw87xxx_dev_soft_reset(&aw87xxx->aw_dev); + + /*hw power off */ + if(g_aw87xxx_dev_cnt == 0) { + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, false); + } + + /* create debug attrbute nodes */ + ret = sysfs_create_group(&aw87xxx->dev->kobj, &aw87xxx_attribute_group); + if (ret < 0) + AW_DEV_LOGE(aw87xxx->dev, "failed to create sysfs nodes, will not allowed to use"); + + /* cfg_load init */ + aw87xxx_fw_load_init(aw87xxx); + + /*monitor init*/ + aw87xxx_monitor_init(aw87xxx->dev, &aw87xxx->monitor, dev_node); + + /*add device to total list */ + mutex_lock(&g_aw87xxx_mutex_lock); + g_aw87xxx_dev_cnt++; + list_add(&aw87xxx->list, &g_aw87xxx_list); + aw87xxx->dev_index = g_aw87xxx_dev_cnt; + + mutex_unlock(&g_aw87xxx_mutex_lock); + AW_DEV_LOGI(aw87xxx->dev, "succeed, dev_index=[%d], g_aw87xxx_dev_cnt= [%d]", + aw87xxx->dev_index, g_aw87xxx_dev_cnt); + + AW_DEV_LOGI(aw87xxx->dev, "acpi_c=[%d] dev_c=[%d]", acpi_dev_count, g_aw87xxx_dev_cnt); + + /* Attempt to add other I2C AMPs */ + if ((acpi_dev_count > 1) && (g_aw87xxx_dev_cnt == 1)){ + /* power on the chip */ + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, true); + + node = device_get_match_data(aw87xxx->dev); + memset(&board_info, 0, sizeof(board_info)); + strscpy(board_info.type, client->name, I2C_NAME_SIZE); + snprintf(i2c_name, sizeof(i2c_name), "%s.%d", client->name, 1); + board_info.dev_name = i2c_name; + + aw87xxx_i2c_probe(i2c_acpi_new_device_by_fwnode(acpi_fwnode_handle(adev), 1, &board_info)); + } + + return 0; + +exit_device_init_failed: + AW_DEV_LOGE(aw87xxx->dev, "pa init failed"); + + devm_kfree(&client->dev, aw87xxx); + aw87xxx = NULL; +exit_malloc_init_failed: +exit_check_functionality_failed: + return ret; +} + +static void aw87xxx_i2c_remove(struct i2c_client *client) +{ + struct aw87xxx *aw87xxx = i2c_get_clientdata(client); + + aw87xxx_monitor_exit(&aw87xxx->monitor); + + /*rm attr node*/ + sysfs_remove_group(&aw87xxx->dev->kobj, &aw87xxx_attribute_group); + + aw87xxx_fw_cfg_free(aw87xxx); + + mutex_lock(&g_aw87xxx_mutex_lock); + g_aw87xxx_dev_cnt--; + list_del(&aw87xxx->list); + mutex_unlock(&g_aw87xxx_mutex_lock); + + devm_kfree(&client->dev, aw87xxx); + aw87xxx = NULL; + +// return 0; +} + +static void aw87xxx_i2c_shutdown(struct i2c_client *client) +{ + struct aw87xxx *aw87xxx = i2c_get_clientdata(client); + + AW_DEV_LOGI(&client->dev, "enter"); + + /*soft and hw power off*/ + aw87xxx_update_profile(aw87xxx, aw87xxx->prof_off_name); +} + +static int aw87xxx_runtime_suspend(struct device *dev) +{ + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + + AW_DEV_LOGI(aw87xxx->dev, "Suspending..."); + + // soft and hw power off + aw87xxx_update_profile(aw87xxx, aw87xxx->prof_off_name); + + return 0; +} + +static int aw87xxx_runtime_resume(struct device *dev) +{ + struct list_head *pos = NULL; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + + // Power on PA + if (aw87xxx->dev_index == 1) + aw87xxx_dev_hw_pwr_ctrl(&aw87xxx->aw_dev, true); + + // Set profile to Music + list_for_each_prev(pos, &g_aw87xxx_list) { + aw87xxx = list_entry(pos, struct aw87xxx, list); + AW_DEV_LOGI(aw87xxx->dev, "Resuming..."); + + mutex_lock(&aw87xxx->reg_lock); + aw87xxx_power_on(aw87xxx, AW87XXX_PROF_MUSIC); + mutex_unlock(&aw87xxx->reg_lock); + } + + return 0; +} + +static SIMPLE_DEV_PM_OPS(aw87xxx_pm_ops, aw87xxx_runtime_suspend, aw87xxx_runtime_resume); + +static const struct acpi_device_id aw87xxx_acpi_match[] = { + { "AWDZ8830", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, aw87xxx_acpi_match); + +// This is not necessary if the acpi match probes correctly. This is needed for userspace `new_device() functionality +static const struct i2c_device_id aw87xxx_i2c_id[] = { + {AW87XXX_I2C_NAME, 0}, + {}, +}; + +static struct i2c_driver aw87xxx_i2c_driver = { + .driver = { + .owner = THIS_MODULE, + .name = AW87XXX_I2C_NAME, + .acpi_match_table = aw87xxx_acpi_match, + .pm = &aw87xxx_pm_ops, + }, + .probe = aw87xxx_i2c_probe, + .remove = aw87xxx_i2c_remove, + .shutdown = aw87xxx_i2c_shutdown, + .id_table = aw87xxx_i2c_id, +}; + +module_i2c_driver(aw87xxx_i2c_driver) + +MODULE_AUTHOR(""); +MODULE_DESCRIPTION("awinic aw87xxx pa driver"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/codecs/aw87xxx/aw87xxx.h b/sound/soc/codecs/aw87xxx/aw87xxx.h new file mode 100644 index 000000000000..9638819cdc7e --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx.h @@ -0,0 +1,130 @@ +#ifndef __AW87XXX_H__ +#define __AW87XXX_H__ +#include +#include +#include +#include + +#include "aw87xxx_device.h" +#include "aw87xxx_monitor.h" +#include "aw87xxx_acf_bin.h" + +#define AW_CFG_UPDATE_DELAY +#define AW_CFG_UPDATE_DELAY_TIMER (3000) + +#define AW87XXX_NO_OFF_BIN (0) +#define AW87XXX_OFF_BIN_OK (1) + +#define AW87XXX_PRIVATE_KCONTROL_NUM (3) +#define AW87XXX_PUBLIC_KCONTROL_NUM (1) + +#define AW_I2C_RETRIES (5) +#define AW_I2C_RETRY_DELAY (2) +#define AW_I2C_READ_MSG_NUM (2) + +#define AW87XXX_FW_NAME_MAX (64) +#define AW_NAME_BUF_MAX (64) +#define AW_LOAD_FW_RETRIES (3) + +#define AW_DEV_REG_RD_ACCESS (1 << 0) +#define AW_DEV_REG_WR_ACCESS (1 << 1) + +#define AWRW_ADDR_BYTES (1) +#define AWRW_DATA_BYTES (1) +#define AWRW_HDR_LEN (24) + +/*********************************************************** + * + * aw87xxx codec control compatible with kernel 4.19 + * + ***********************************************************/ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 16, 1) +#define AW_KERNEL_VER_OVER_6_16_1 +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 19, 1) +#define AW_KERNEL_VER_OVER_4_19_1 +#endif + +#ifdef AW_KERNEL_VER_OVER_4_19_1 +typedef struct snd_soc_component aw_snd_soc_codec_t; +#else +typedef struct snd_soc_codec aw_snd_soc_codec_t; +#endif + +struct aw_componet_codec_ops { + int (*add_codec_controls)(aw_snd_soc_codec_t *codec, + const struct snd_kcontrol_new *controls, unsigned int num_controls); + void (*unregister_codec)(struct device *dev); +}; + + +/******************************************** + * + * aw87xxx devices attributes + * + *******************************************/ +enum { + AWRW_FLAG_WRITE = 0, + AWRW_FLAG_READ, +}; + +enum { + AWRW_I2C_ST_NONE = 0, + AWRW_I2C_ST_READ, + AWRW_I2C_ST_WRITE, +}; + +enum { + AWRW_HDR_WR_FLAG = 0, + AWRW_HDR_ADDR_BYTES, + AWRW_HDR_DATA_BYTES, + AWRW_HDR_REG_NUM, + AWRW_HDR_REG_ADDR, + AWRW_HDR_MAX, +}; + +struct aw_i2c_packet { + char status; + unsigned int reg_num; + unsigned int reg_addr; + char *reg_data; +}; + + +/******************************************** + * + * aw87xxx device struct + * + *******************************************/ +struct aw87xxx { + char fw_name[AW87XXX_FW_NAME_MAX]; + int32_t dev_index; + char *current_profile; + char prof_off_name[AW_PROFILE_STR_MAX]; + uint32_t off_bin_status; + struct device *dev; + + struct mutex reg_lock; + struct aw_device aw_dev; + struct aw_i2c_packet i2c_packet; + + struct delayed_work fw_load_work; + struct acf_bin_info acf_info; + + aw_snd_soc_codec_t *codec; + + struct list_head list; + + struct aw_monitor monitor; +}; + +int aw87xxx_update_profile(struct aw87xxx *aw87xxx, char *profile); +int aw87xxx_update_profile_esd(struct aw87xxx *aw87xxx, char *profile); + +char *aw87xxx_show_current_profile(int dev_index); +int aw87xxx_set_profile(int dev_index, char *profile); +int aw87xxx_set_profile_by_id(int dev_index, int profile_id); +int aw87xxx_add_codec_controls(void *codec); +int aw87xxx_awrw_write(struct aw87xxx *aw87xxx, const char *buf, size_t count); +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.c b/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.c new file mode 100644 index 000000000000..00c7aedb7c11 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.c @@ -0,0 +1,1558 @@ +/* + * aw87xxx_acf_bin.c + * + * Copyright (c) 2021 AWINIC Technology CO., LTD + * + * Author: Barry + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx.h" +#include "aw87xxx_acf_bin.h" +#include "aw87xxx_monitor.h" +#include "aw87xxx_log.h" +#include "aw87xxx_bin_parse.h" + +/************************************************************************* + * + *Table corresponding to customized profile ids to profile names + * + *************************************************************************/ +enum aw_customers_profile_id { + AW_CTOS_PROFILE_OFF = 0, + AW_CTOS_PROFILE_MUSIC, + AW_CTOS_PROFILE_VOICE, + AW_CTOS_PROFILE_VOIP, + AW_CTOS_PROFILE_RINGTONE, + AW_CTOS_PROFILE_RINGTONE_HS, + AW_CTOS_PROFILE_LOWPOWER, + AW_CTOS_PROFILE_BYPASS, + AW_CTOS_PROFILE_MMI, + AW_CTOS_PROFILE_FM, + AW_CTOS_PROFILE_NOTIFICATION, + AW_CTOS_PROFILE_RECEIVER, + AW_CTOS_PROFILE_MAX, +}; + +static char *g_ctos_profile_name[AW_PROFILE_MAX] = { + [AW_CTOS_PROFILE_OFF] = "Off", + [AW_CTOS_PROFILE_MUSIC] = "Music", + [AW_CTOS_PROFILE_VOICE] = "Voice", + [AW_CTOS_PROFILE_VOIP] = "Voip", + [AW_CTOS_PROFILE_RINGTONE] = "Ringtone", + [AW_CTOS_PROFILE_RINGTONE_HS] = "Ringtone_hs", + [AW_CTOS_PROFILE_LOWPOWER] = "Lowpower", + [AW_CTOS_PROFILE_BYPASS] = "Bypass", + [AW_CTOS_PROFILE_MMI] = "Mmi", + [AW_CTOS_PROFILE_FM] = "Fm", + [AW_CTOS_PROFILE_NOTIFICATION] = "Notification", + [AW_CTOS_PROFILE_RECEIVER] = "Receiver", +}; + + +char *aw87xxx_ctos_get_prof_name(int profile_id) +{ + if (profile_id < 0 || profile_id >= AW_CTOS_PROFILE_MAX) + return NULL; + else + return g_ctos_profile_name[profile_id]; +} + + +static char *g_profile_name[] = {"Music", "Voice", "Voip", + "Ringtone", "Ringtone_hs", "Lowpower", "Bypass", "Mmi", + "Fm", "Notification", "Receiver", "Off"}; + +static char *g_power_off_name[] = {"Off", "OFF", "off", "oFF", "power_down"}; + +static char *aw_get_prof_name(int profile) +{ + if (profile < 0 || profile >= AW_PROFILE_MAX) + return "NULL"; + else + return g_profile_name[profile]; +} + +/************************************************************************* + * + *acf check + * + *************************************************************************/ +static int aw_crc8_check(const unsigned char *data, unsigned int data_size) + +{ + unsigned char crc_value = 0x00; + unsigned char *pdata; + int i; + unsigned char pdatabuf = 0; + + pdata = (unsigned char *)data; + + while (data_size--) { + pdatabuf = *pdata++; + for (i = 0; i < 8; i++) { + if ((crc_value ^ (pdatabuf)) & 0x01) { + crc_value ^= 0x18; + crc_value >>= 1; + crc_value |= 0x80; + } else { + crc_value >>= 1; + } + pdatabuf >>= 1; + } + } + + return (int)crc_value; +} + +static int aw_check_file_id(struct device *dev, + char *fw_data, int32_t file_id) +{ + int32_t *acf_file_id = NULL; + + acf_file_id = (int32_t *)fw_data; + if (*acf_file_id != file_id) { + AW_DEV_LOGE(dev, "file id [%x] check failed", *acf_file_id); + return -ENFILE; + } + + return 0; +} + +static int aw_check_header_size(struct device *dev, + char *fw_data, size_t fw_size) +{ + if (fw_size < sizeof(struct aw_acf_hdr)) { + AW_DEV_LOGE(dev, "acf size check failed,size less-than aw_acf_hdr"); + return -ENOEXEC; + } + + return 0; +} + +/*************************************************************************** + * V0.0.0.1 version acf check + **************************************************************************/ +static int aw_check_ddt_size_v_0_0_0_1(struct device *dev, char *fw_data) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)fw_data; + struct aw_acf_dde *acf_dde = NULL; + + acf_dde = (struct aw_acf_dde *)(fw_data + acf_hdr->ddt_offset); + + /* check ddt_size in acf_header is aqual to ddt_num multiply by dde_size */ + if (acf_hdr->ddt_size != acf_hdr->dde_num * sizeof(struct aw_acf_dde)) { + AW_DEV_LOGE(dev, "acf ddt size check failed"); + return -EINVAL; + } + + return 0; +} + +static int aw_check_data_size_v_0_0_0_1(struct device *dev, + char *fw_data, size_t fw_size) +{ + int i = 0; + size_t data_size = 0; + struct aw_acf_hdr *acf_hdr = NULL; + struct aw_acf_dde *acf_dde = NULL; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + acf_dde = (struct aw_acf_dde *)(fw_data + acf_hdr->ddt_offset); + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (acf_dde[i].data_size % 2) { + AW_DEV_LOGE(dev, "acf dde[%d].data_size[%d],dev_name[%s],data_type[%d], data_size check failed", + i, acf_dde[i].data_size, acf_dde[i].dev_name, + acf_dde[i].data_type); + return -EINVAL; + } + data_size += acf_dde[i].data_size; + } + + /* Verify that the file size is equal to the header size plus */ + /* the table size and data size */ + if (fw_size != data_size + sizeof(struct aw_acf_hdr) + acf_hdr->ddt_size) { + AW_DEV_LOGE(dev, "acf size check failed"); + AW_DEV_LOGE(dev, "fw_size=%ld,hdr_size and ddt size and data size =%ld", + (u_long)fw_size, (u_long)(data_size + sizeof(struct aw_acf_hdr) + + acf_hdr->ddt_size)); + return -EINVAL; + } + + return 0; +} + +static int aw_check_data_crc_v_0_0_0_1(struct device *dev, char *fw_data) +{ + int i = 0; + size_t crc_val = 0; + char *data = NULL; + struct aw_acf_hdr *acf_hdr = NULL; + struct aw_acf_dde *acf_dde = NULL; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + acf_dde = (struct aw_acf_dde *)(fw_data + acf_hdr->ddt_offset); + + for (i = 0; i < acf_hdr->dde_num; ++i) { + data = fw_data + acf_dde[i].data_offset; + crc_val = aw_crc8_check(data, acf_dde[i].data_size); + if (crc_val != acf_dde[i].data_crc) { + AW_DEV_LOGE(dev, "acf dde_crc check failed"); + return -EINVAL; + } + } + + return 0; +} + +static int aw_check_profile_id_v_0_0_0_1(struct device *dev, char *fw_data) +{ + int i = 0; + struct aw_acf_hdr *acf_hdr = NULL; + struct aw_acf_dde *acf_dde = NULL; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + acf_dde = (struct aw_acf_dde *)(fw_data + acf_hdr->ddt_offset); + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (acf_dde[i].data_type == AW_MONITOR) + continue; + if (acf_dde[i].dev_profile > AW_PROFILE_MAX) { + AW_DEV_LOGE(dev, "parse profile_id[%d] failed", acf_dde[i].dev_profile); + return -EINVAL; + } + } + + return 0; +} +static int aw_check_data_v_0_0_0_1(struct device *dev, + char *fw_data, size_t size) +{ + int ret = -1; + + /* check file type id is awinic acf file */ + ret = aw_check_file_id(dev, fw_data, AW_ACF_FILE_ID); + if (ret < 0) + return ret; + + /* check ddt_size in header is equal to all ddt aize */ + ret = aw_check_ddt_size_v_0_0_0_1(dev, fw_data); + if (ret < 0) + return ret; + + /* Verify that the file size is equal to the header size plus */ + /* the table size and data size */ + ret = aw_check_data_size_v_0_0_0_1(dev, fw_data, size); + if (ret < 0) + return ret; + + /* check crc in is equal to dde data crc */ + ret = aw_check_data_crc_v_0_0_0_1(dev, fw_data); + if (ret < 0) + return ret; + + /* check profile id is in profile_id_max */ + ret = aw_check_profile_id_v_0_0_0_1(dev, fw_data); + if (ret < 0) + return ret; + + AW_DEV_LOGI(dev, "acf fimware check succeed"); + + return 0; +} + +/*************************************************************************** + * V1.0.0.0 version acf chack + **************************************************************************/ +static int aw_check_ddt_size_v_1_0_0_0(struct device *dev, char *fw_data) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)fw_data; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = NULL; + + acf_dde = (struct aw_acf_dde_v_1_0_0_0 *)(fw_data + acf_hdr->ddt_offset); + + /* check ddt_size in acf_header is aqual to ddt_num multiply by dde_size */ + if (acf_hdr->ddt_size != acf_hdr->dde_num * sizeof(struct aw_acf_dde_v_1_0_0_0)) { + AW_DEV_LOGE(dev, "acf ddt size check failed"); + return -EINVAL; + } + + return 0; +} + +static int aw_check_data_size_v_1_0_0_0(struct device *dev, + char *fw_data, size_t fw_size) +{ + int i = 0; + size_t data_size = 0; + struct aw_acf_hdr *acf_hdr = NULL; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = NULL; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + acf_dde = (struct aw_acf_dde_v_1_0_0_0 *)(fw_data + acf_hdr->ddt_offset); + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (acf_dde[i].data_size % 2) { + AW_DEV_LOGE(dev, "acf dde[%d].data_size[%d],dev_name[%s],data_type[%d], data_size check failed", + i, acf_dde[i].data_size, acf_dde[i].dev_name, + acf_dde[i].data_type); + return -EINVAL; + } + data_size += acf_dde[i].data_size; + } + + /* Verify that the file size is equal to the header size plus */ + /* the table size and data size */ + if (fw_size != data_size + sizeof(struct aw_acf_hdr) + acf_hdr->ddt_size) { + AW_DEV_LOGE(dev, "acf size check failed"); + AW_DEV_LOGE(dev, "fw_size=%ld,hdr_size and ddt size and data size =%ld", + (u_long)fw_size, (u_long)(data_size + sizeof(struct aw_acf_hdr) + + acf_hdr->ddt_size)); + return -EINVAL; + } + + return 0; +} + +static int aw_check_data_crc_v_1_0_0_0(struct device *dev, char *fw_data) +{ + int i = 0; + size_t crc_val = 0; + char *data = NULL; + struct aw_acf_hdr *acf_hdr = NULL; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = NULL; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + acf_dde = (struct aw_acf_dde_v_1_0_0_0 *)(fw_data + acf_hdr->ddt_offset); + + for (i = 0; i < acf_hdr->dde_num; ++i) { + data = fw_data + acf_dde[i].data_offset; + crc_val = aw_crc8_check(data, acf_dde[i].data_size); + if (crc_val != acf_dde[i].data_crc) { + AW_DEV_LOGE(dev, "acf dde_crc check failed"); + return -EINVAL; + } + } + + return 0; +} + +static int aw_check_data_v_1_0_0_0(struct device *dev, + char *fw_data, size_t size) +{ + int ret = -1; + + /* check file type id is awinic acf file */ + ret = aw_check_file_id(dev, fw_data, AW_ACF_FILE_ID); + if (ret < 0) + return ret; + + /* check ddt_size in header is equal to all ddt aize */ + ret = aw_check_ddt_size_v_1_0_0_0(dev, fw_data); + if (ret < 0) + return ret; + + /* Verify that the file size is equal to the header size plus */ + /* the table size and data size */ + ret = aw_check_data_size_v_1_0_0_0(dev, fw_data, size); + if (ret < 0) + return ret; + + /* check crc in is equal to dde data crc */ + ret = aw_check_data_crc_v_1_0_0_0(dev, fw_data); + if (ret < 0) + return ret; + + AW_DEV_LOGI(dev, "acf fimware check succeed"); + + return 0; +} + +/*************************************************************************** + * acf chack API + **************************************************************************/ +static int aw_check_acf_firmware(struct device *dev, + char *fw_data, size_t size) +{ + int ret = -1; + struct aw_acf_hdr *acf_hdr = NULL; + + if (fw_data == NULL) { + AW_DEV_LOGE(dev, "fw_data is NULL,fw_data check failed"); + return -ENODATA; + } + + /* check file size is less-than header size */ + ret = aw_check_header_size(dev, fw_data, size); + if (ret < 0) + return ret; + + acf_hdr = (struct aw_acf_hdr *)fw_data; + AW_DEV_LOGI(dev, "project name: [%s]", acf_hdr->project); + AW_DEV_LOGI(dev, "custom name: [%s]", acf_hdr->custom); + AW_DEV_LOGI(dev, "version name: [%s]", acf_hdr->version); + AW_DEV_LOGI(dev, "author_id: [%d]", acf_hdr->author_id); + + switch (acf_hdr->hdr_version) { + case AW_ACF_HDR_VER_0_0_0_1: + return aw_check_data_v_0_0_0_1(dev, fw_data, size); + case AW_ACF_HDR_VER_1_0_0_0: + return aw_check_data_v_1_0_0_0(dev, fw_data, size); + default: + AW_DEV_LOGE(dev, "unsupported hdr_version [0x%x]", + acf_hdr->hdr_version); + return -EINVAL; + } + + return ret; +} + + + +/************************************************************************* + * + *acf parse + * + *************************************************************************/ +static int aw_parse_raw_reg(struct device *dev, uint8_t *data, + uint32_t data_len, struct aw_prof_desc *prof_desc) +{ + AW_DEV_LOGD(dev, "data_size:%d enter", data_len); + + prof_desc->data_container.data = data; + prof_desc->data_container.len = data_len; + + prof_desc->prof_st = AW_PROFILE_OK; + + return 0; +} + +static int aw_parse_reg_with_hdr(struct device *dev, uint8_t *data, + uint32_t data_len, struct aw_prof_desc *prof_desc) +{ + struct aw_bin *aw_bin = NULL; + int ret = -1; + + AW_DEV_LOGD(dev, "data_size:%d enter", data_len); + + aw_bin = kzalloc(data_len + sizeof(struct aw_bin), GFP_KERNEL); + if (aw_bin == NULL) { + AW_DEV_LOGE(dev, "devm_kzalloc aw_bin failed"); + return -ENOMEM; + } + + aw_bin->info.len = data_len; + memcpy(aw_bin->info.data, data, data_len); + + ret = aw87xxx_parsing_bin_file(aw_bin); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse bin failed"); + goto parse_bin_failed; + } + + if ((aw_bin->all_bin_parse_num != 1) || + (aw_bin->header_info[0].bin_data_type != DATA_TYPE_REGISTER)) { + AW_DEV_LOGE(dev, "bin num or type error"); + goto parse_bin_failed; + } + + prof_desc->data_container.data = + data + aw_bin->header_info[0].valid_data_addr; + prof_desc->data_container.len = aw_bin->header_info[0].valid_data_len; + prof_desc->prof_st = AW_PROFILE_OK; + + kfree(aw_bin); + aw_bin = NULL; + + return 0; + +parse_bin_failed: + kfree(aw_bin); + aw_bin = NULL; + return ret; +} + +static int aw_parse_monitor_config(struct device *dev, + char *monitor_data, uint32_t data_len) +{ + int ret = -1; + + if (monitor_data == NULL || data_len == 0) { + AW_DEV_LOGE(dev, "no data to parse"); + return -EBFONT; + } + + ret = aw87xxx_monitor_bin_parse(dev, monitor_data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "monitor_config parse failed"); + return ret; + } + + AW_DEV_LOGI(dev, "monitor_bin parse succeed"); + + return 0; +} + +static int aw_check_prof_str_is_off(char *profile_name) +{ + int i = 0; + + for (i = 0; i < AW_POWER_OFF_NAME_SUPPORT_COUNT; i++) { + if (strnstr(profile_name, g_power_off_name[i], + strlen(profile_name) + 1)) + return 0; + } + + return -EINVAL; +} + +/*************************************************************************** + * V0.0.0.1 version acf paese + **************************************************************************/ +static int aw_check_product_name_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_acf_dde *prof_hdr) +{ + int i = 0; + + for (i = 0; i < acf_info->product_cnt; i++) { + if (0 == strcmp(acf_info->product_tab[i], prof_hdr->dev_name)) { + AW_DEV_LOGD(dev, "bin_dev_name:%s", + prof_hdr->dev_name); + return 0; + } + } + + return -ENXIO; +} + +static int aw_check_data_type_is_monitor_v_0_0_0_1(struct device *dev, + struct aw_acf_dde *prof_hdr) +{ + if (prof_hdr->data_type == AW_MONITOR) { + AW_DEV_LOGD(dev, "bin data is monitor"); + return 0; + } + + return -ENXIO; +} + +static int aw_parse_data_by_sec_type_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_acf_dde *prof_hdr, + struct aw_prof_desc *profile_prof_desc) +{ + int ret = -1; + char *cfg_data = acf_info->fw_data + prof_hdr->data_offset; + + switch (prof_hdr->data_type) { + case AW_BIN_TYPE_REG: + snprintf(profile_prof_desc->dev_name, sizeof(prof_hdr->dev_name), + "%s", prof_hdr->dev_name); + profile_prof_desc->prof_name = aw_get_prof_name(prof_hdr->dev_profile); + AW_DEV_LOGD(dev, "parse reg type data enter,profile=%s", + aw_get_prof_name(prof_hdr->dev_profile)); + ret = aw_parse_raw_reg(dev, cfg_data, prof_hdr->data_size, + profile_prof_desc); + break; + case AW_BIN_TYPE_HDR_REG: + snprintf(profile_prof_desc->dev_name, sizeof(prof_hdr->dev_name), + "%s", prof_hdr->dev_name); + profile_prof_desc->prof_name = aw_get_prof_name(prof_hdr->dev_profile); + AW_DEV_LOGD(dev, "parse hdr_reg type data enter,profile=%s", + aw_get_prof_name(prof_hdr->dev_profile)); + ret = aw_parse_reg_with_hdr(dev, cfg_data, + prof_hdr->data_size, + profile_prof_desc); + break; + } + + return ret; +} + +static int aw_parse_dev_type_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, struct aw_all_prof_info *all_prof_info) +{ + int i = 0; + int ret = -1; + int sec_num = 0; + char *cfg_data = NULL; + struct aw_prof_desc *prof_desc = NULL; + struct aw_acf_dde *acf_dde = + (struct aw_acf_dde *)(acf_info->fw_data + acf_info->acf_hdr.ddt_offset); + + AW_DEV_LOGD(dev, "enter"); + + for (i = 0; i < acf_info->acf_hdr.dde_num; i++) { + if ((acf_info->aw_dev->i2c_bus == acf_dde[i].dev_bus) && + (acf_info->aw_dev->i2c_addr == acf_dde[i].dev_addr) && + (acf_dde[i].type == AW_DDE_DEV_TYPE_ID)) { + + ret = aw_check_product_name_v_0_0_0_1(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + ret = aw_check_data_type_is_monitor_v_0_0_0_1(dev, &acf_dde[i]); + if (ret == 0) { + cfg_data = acf_info->fw_data + acf_dde[i].data_offset; + ret = aw_parse_monitor_config(dev, cfg_data, acf_dde[i].data_size); + if (ret < 0) + return ret; + continue; + } + + prof_desc = &all_prof_info->prof_desc[acf_dde[i].dev_profile]; + ret = aw_parse_data_by_sec_type_v_0_0_0_1(dev, acf_info, &acf_dde[i], + prof_desc); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse dev type data failed"); + return ret; + } + sec_num++; + } + } + + if (sec_num == 0) { + AW_DEV_LOGD(dev, "get dev type num is %d, please use default", + sec_num); + return AW_DEV_TYPE_NONE; + } + + return AW_DEV_TYPE_OK; +} + +static int aw_parse_default_type_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, struct aw_all_prof_info *all_prof_info) +{ + int i = 0; + int ret = -1; + int sec_num = 0; + char *cfg_data = NULL; + struct aw_prof_desc *prof_desc = NULL; + struct aw_acf_dde *acf_dde = + (struct aw_acf_dde *)(acf_info->fw_data + acf_info->acf_hdr.ddt_offset); + + AW_DEV_LOGD(dev, "enter"); + + for (i = 0; i < acf_info->acf_hdr.dde_num; i++) { + if ((acf_info->dev_index == acf_dde[i].dev_index) && + (acf_dde[i].type == AW_DDE_DEV_DEFAULT_TYPE_ID)) { + + ret = aw_check_product_name_v_0_0_0_1(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + ret = aw_check_data_type_is_monitor_v_0_0_0_1(dev, &acf_dde[i]); + if (ret == 0) { + cfg_data = acf_info->fw_data + acf_dde[i].data_offset; + ret = aw_parse_monitor_config(dev, cfg_data, acf_dde[i].data_size); + if (ret < 0) + return ret; + continue; + } + + prof_desc = &all_prof_info->prof_desc[acf_dde[i].dev_profile]; + ret = aw_parse_data_by_sec_type_v_0_0_0_1(dev, acf_info, &acf_dde[i], + prof_desc); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse default type data failed"); + return ret; + } + sec_num++; + } + } + + if (sec_num == 0) { + AW_DEV_LOGE(dev, "get dev default type failed, get num[%d]", + sec_num); + return -EINVAL; + } + + return 0; +} + +static int aw_get_prof_count_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_all_prof_info *all_prof_info) +{ + int i = 0; + int prof_count = 0; + struct aw_prof_desc *prof_desc = all_prof_info->prof_desc; + + for (i = 0; i < AW_PROFILE_MAX; i++) { + if (prof_desc[i].prof_st == AW_PROFILE_OK) { + prof_count++; + } else if (i == AW_PROFILE_OFF) { + prof_count++; + AW_DEV_LOGI(dev, "not found profile [Off], set default"); + } + } + + AW_DEV_LOGI(dev, "get profile count=[%d]", prof_count); + return prof_count; +} + +static int aw_set_prof_off_info_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_all_prof_info *all_prof_info, + int index) +{ + struct aw_prof_desc *prof_desc = all_prof_info->prof_desc; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + if (index >= prof_info->count) { + AW_DEV_LOGE(dev, "index[%d] is out of table,profile count[%d]", + index, prof_info->count); + return -EINVAL; + } + + if (prof_desc[AW_PROFILE_OFF].prof_st == AW_PROFILE_OK) { + prof_info->prof_desc[index] = prof_desc[AW_PROFILE_OFF]; + AW_DEV_LOGI(dev, "product=[%s]----profile=[%s]", + prof_info->prof_desc[index].dev_name, + aw_get_prof_name(AW_PROFILE_OFF)); + } else { + memset(&prof_info->prof_desc[index].data_container, 0, + sizeof(struct aw_data_container)); + prof_info->prof_desc[index].prof_st = AW_PROFILE_WAIT; + prof_info->prof_desc[index].prof_name = aw_get_prof_name(AW_PROFILE_OFF); + AW_DEV_LOGI(dev, "set default power_off with no data to profile"); + } + + return 0; +} + + +static int aw_get_vaild_prof_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_all_prof_info *all_prof_info) +{ + int i = 0; + int ret = 0; + int index = 0; + struct aw_prof_desc *prof_desc = all_prof_info->prof_desc; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + prof_info->count = 0; + ret = aw_get_prof_count_v_0_0_0_1(dev, acf_info, all_prof_info); + if (ret < 0) + return ret; + prof_info->count = ret; + prof_info->prof_desc = devm_kzalloc(dev, + prof_info->count * sizeof(struct aw_prof_desc), + GFP_KERNEL); + if (prof_info->prof_desc == NULL) { + AW_DEV_LOGE(dev, "prof_desc kzalloc failed"); + return -ENOMEM; + } + + for (i = 0; i < AW_PROFILE_MAX; i++) { + if (i != AW_PROFILE_OFF && prof_desc[i].prof_st == AW_PROFILE_OK) { + if (index >= prof_info->count) { + AW_DEV_LOGE(dev, "get profile index[%d] overflow count[%d]", + index, prof_info->count); + return -ENOMEM; + } + prof_info->prof_desc[index] = prof_desc[i]; + AW_DEV_LOGI(dev, "product=[%s]----profile=[%s]", + prof_info->prof_desc[index].dev_name, + aw_get_prof_name(i)); + index++; + } + } + + ret = aw_set_prof_off_info_v_0_0_0_1(dev, acf_info, all_prof_info, index); + if (ret < 0) + return ret; + + AW_DEV_LOGD(dev, "get vaild profile succeed"); + return 0; +} + +static int aw_set_prof_name_list_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info) +{ + int i = 0; + int count = acf_info->prof_info.count; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + prof_info->prof_name_list = (char (*)[AW_PROFILE_STR_MAX])devm_kzalloc(dev, + count * (AW_PROFILE_STR_MAX), GFP_KERNEL); + if (prof_info->prof_name_list == NULL) { + AW_DEV_LOGE(dev, "prof_name_list devm_kzalloc failed"); + return -ENOMEM; + } + + for (i = 0; i < count; ++i) { + snprintf(prof_info->prof_name_list[i], AW_PROFILE_STR_MAX, "%s", + prof_info->prof_desc[i].prof_name); + AW_DEV_LOGI(dev, "index=[%d], profile_name=[%s]", + i, prof_info->prof_name_list[i]); + } + + return 0; +} + +static int aw_parse_acf_v_0_0_0_1(struct device *dev, + struct acf_bin_info *acf_info) + +{ + int ret = 0; + struct aw_all_prof_info all_prof_info; + + AW_DEV_LOGD(dev, "enter"); + acf_info->prof_info.status = AW_ACF_WAIT; + + memset(&all_prof_info, 0, sizeof(struct aw_all_prof_info)); + + ret = aw_parse_dev_type_v_0_0_0_1(dev, acf_info, &all_prof_info); + if (ret < 0) { + return ret; + } else if (ret == AW_DEV_TYPE_NONE) { + AW_DEV_LOGD(dev, "get dev type num is 0, parse default dev type"); + ret = aw_parse_default_type_v_0_0_0_1(dev, acf_info, &all_prof_info); + if (ret < 0) + return ret; + } + + ret = aw_get_vaild_prof_v_0_0_0_1(dev, acf_info, &all_prof_info); + if (ret < 0) { + aw87xxx_acf_profile_free(dev, acf_info); + AW_DEV_LOGE(dev, "hdr_cersion[0x%x] parse failed", + acf_info->acf_hdr.hdr_version); + return ret; + } + + ret = aw_set_prof_name_list_v_0_0_0_1(dev, acf_info); + if (ret < 0) { + aw87xxx_acf_profile_free(dev, acf_info); + AW_DEV_LOGE(dev, "creat prof_id_and_name_list failed"); + return ret; + } + + acf_info->prof_info.status = AW_ACF_UPDATE; + AW_DEV_LOGI(dev, "acf parse success"); + return 0; +} + +/*************************************************************************** + * V1.0.0.0 version acf paese + **************************************************************************/ +static int aw_check_product_name_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_acf_dde_v_1_0_0_0 *prof_hdr) +{ + int i = 0; + + for (i = 0; i < acf_info->product_cnt; i++) { + if (0 == strcmp(acf_info->product_tab[i], prof_hdr->dev_name)) { + AW_DEV_LOGI(dev, "bin_dev_name:%s", prof_hdr->dev_name); + return 0; + } + } + + return -ENXIO; +} + +static int aw_get_dde_type_info_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int i; + int dev_num = 0; + int default_num = 0; + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)acf_info->fw_data; + struct aw_prof_info *prof_info = &acf_info->prof_info; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_hdr->ddt_offset); + + prof_info->prof_type = AW_DEV_NONE_TYPE_ID; + for (i = 0; i < acf_hdr->dde_num; i++) { + if (acf_dde[i].type == AW_DDE_DEV_TYPE_ID) + dev_num++; + if (acf_dde[i].type == AW_DDE_DEV_DEFAULT_TYPE_ID) + default_num++; + } + + if (!(dev_num || default_num)) { + AW_DEV_LOGE(dev, "can't find scene"); + return -EINVAL; + } + + if (dev_num != 0) + prof_info->prof_type = AW_DDE_DEV_TYPE_ID; + else if (default_num != 0) + prof_info->prof_type = AW_DDE_DEV_DEFAULT_TYPE_ID; + + return 0; +} + + +static int aw_parse_get_dev_type_prof_count_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)acf_info->fw_data; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_hdr->ddt_offset); + int i = 0; + int ret = 0; + int found_off_prof_flag = 0; + int count = acf_info->prof_info.count; + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (((acf_dde[i].data_type == AW_BIN_TYPE_REG) || + (acf_dde[i].data_type == AW_BIN_TYPE_HDR_REG)) && + ((acf_info->aw_dev->i2c_bus == acf_dde[i].dev_bus) && + (acf_info->aw_dev->i2c_addr == acf_dde[i].dev_addr)) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + ret = aw_check_prof_str_is_off(acf_dde[i].dev_profile_str); + if (ret == 0) { + found_off_prof_flag = AW_PROFILE_OK; + } + count++; + } + } + + if (count == 0) { + AW_DEV_LOGE(dev, "can't find profile"); + return -EINVAL; + } + + if (!found_off_prof_flag) { + count++; + AW_DEV_LOGD(dev, "set no config power off profile in count"); + } + + acf_info->prof_info.count = count; + AW_DEV_LOGI(dev, "profile dev_type profile count is %d", acf_info->prof_info.count); + return 0; +} + +static int aw_parse_get_default_type_prof_count_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)acf_info->fw_data; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_hdr->ddt_offset); + int i = 0; + int ret = 0; + int found_off_prof_flag = 0; + int count = acf_info->prof_info.count; + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (((acf_dde[i].data_type == AW_BIN_TYPE_REG) || + (acf_dde[i].data_type == AW_BIN_TYPE_HDR_REG)) && + (acf_info->dev_index == acf_dde[i].dev_index) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + ret = aw_check_prof_str_is_off(acf_dde[i].dev_profile_str); + if (ret == 0) { + found_off_prof_flag = AW_PROFILE_OK; + } + count++; + } + } + + if (count == 0) { + AW_DEV_LOGE(dev, "can't find profile"); + return -EINVAL; + } + + if (!found_off_prof_flag) { + count++; + AW_DEV_LOGD(dev, "set no config power off profile in count"); + } + + acf_info->prof_info.count = count; + AW_DEV_LOGI(dev, "profile default_type profile count is %d", acf_info->prof_info.count); + return 0; +} + +static int aw_parse_get_profile_count_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int ret = 0; + + ret = aw_get_dde_type_info_v_1_0_0_0(dev, acf_info); + if (ret < 0) + return ret; + + if (acf_info->prof_info.prof_type == AW_DDE_DEV_TYPE_ID) { + ret = aw_parse_get_dev_type_prof_count_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse dev_type profile count failed"); + return ret; + } + } else if (acf_info->prof_info.prof_type == AW_DDE_DEV_DEFAULT_TYPE_ID) { + ret = aw_parse_get_default_type_prof_count_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse default_type profile count failed"); + return ret; + } + } else { + AW_DEV_LOGE(dev, "unsupport prof_type[0x%x]", + acf_info->prof_info.prof_type); + return -EINVAL; + } + + AW_DEV_LOGI(dev, "profile count is %d", acf_info->prof_info.count); + return 0; +} + +static int aw_parse_dev_type_prof_name_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)acf_info->fw_data; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_hdr->ddt_offset); + struct aw_prof_info *prof_info = &acf_info->prof_info; + int i, ret, list_index = 0; + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (((acf_dde[i].data_type == AW_BIN_TYPE_REG) || + (acf_dde[i].data_type == AW_BIN_TYPE_HDR_REG)) && + (acf_info->aw_dev->i2c_bus == acf_dde[i].dev_bus) && + (acf_info->aw_dev->i2c_addr == acf_dde[i].dev_addr) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + if (list_index > prof_info->count) { + AW_DEV_LOGE(dev, "%s:Alrealdy set list_index [%d], redundant profile [%s]exist\n", + __func__, list_index, + acf_dde[i].dev_profile_str); + return -EINVAL; + } + + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + snprintf(prof_info->prof_name_list[list_index], AW_PROFILE_STR_MAX, "%s", + acf_dde[i].dev_profile_str); + AW_DEV_LOGI(dev, "profile_name=[%s]", + prof_info->prof_name_list[list_index]); + list_index++; + } + } + + return 0; +} + +static int aw_parse_default_type_prof_name_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_acf_hdr *acf_hdr = (struct aw_acf_hdr *)acf_info->fw_data; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_hdr->ddt_offset); + struct aw_prof_info *prof_info = &acf_info->prof_info; + int i, ret, list_index = 0; + + for (i = 0; i < acf_hdr->dde_num; ++i) { + if (((acf_dde[i].data_type == AW_BIN_TYPE_REG) || + (acf_dde[i].data_type == AW_BIN_TYPE_HDR_REG)) && + (acf_info->dev_index == acf_dde[i].dev_index) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + if (list_index > prof_info->count) { + AW_DEV_LOGE(dev, "%s:Alrealdy set list_index [%d], redundant profile [%s]exist\n", + __func__, list_index, + acf_dde[i].dev_profile_str); + return -EINVAL; + } + + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + snprintf(prof_info->prof_name_list[list_index], AW_PROFILE_STR_MAX, "%s", + acf_dde[i].dev_profile_str); + AW_DEV_LOGI(dev, "profile_name=[%s]", + prof_info->prof_name_list[list_index]); + list_index++; + } + } + + return 0; +} + +static int aw_parse_prof_name_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int ret = 0; + int count = acf_info->prof_info.count; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + prof_info->prof_name_list = (char (*)[AW_PROFILE_STR_MAX])devm_kzalloc(dev, + count * (AW_PROFILE_STR_MAX), GFP_KERNEL); + if (prof_info->prof_name_list == NULL) { + AW_DEV_LOGE(dev, "prof_name_list devm_kzalloc failed"); + return -ENOMEM; + } + + if (acf_info->prof_info.prof_type == AW_DDE_DEV_TYPE_ID) { + ret = aw_parse_dev_type_prof_name_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse dev_type profile count failed"); + return ret; + } + } else if (acf_info->prof_info.prof_type == AW_DDE_DEV_DEFAULT_TYPE_ID) { + ret = aw_parse_default_type_prof_name_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse default_type profile count failed"); + return ret; + } + } else { + AW_DEV_LOGE(dev, "unsupport prof_type[0x%x]", + acf_info->prof_info.prof_type); + return -EINVAL; + } + + AW_DEV_LOGI(dev, "profile name parse succeed"); + return 0; +} + + +static int aw_search_prof_index_from_list_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_prof_desc **prof_desc, + struct aw_acf_dde_v_1_0_0_0 *prof_hdr) +{ + int i = 0; + int count = acf_info->prof_info.count; + char (*prof_name_list)[AW_PROFILE_STR_MAX] = acf_info->prof_info.prof_name_list; + + for (i = 0; i < count; i++) { + if (!strncmp(prof_name_list[i], prof_hdr->dev_profile_str, AW_PROFILE_STR_MAX)) { + *prof_desc = &(acf_info->prof_info.prof_desc[i]); + return 0; + } + } + + if (i == count) + AW_DEV_LOGE(dev, "not find prof_id and prof_name in list"); + + return -EINVAL; +} + +static int aw_parse_data_by_sec_type_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info, + struct aw_acf_dde_v_1_0_0_0 *prof_hdr) +{ + int ret = -1; + char *cfg_data = acf_info->fw_data + prof_hdr->data_offset; + struct aw_prof_desc *prof_desc = NULL; + + ret = aw_search_prof_index_from_list_v_1_0_0_0(dev, acf_info, &prof_desc, prof_hdr); + if (ret < 0) + return ret; + + switch (prof_hdr->data_type) { + case AW_BIN_TYPE_REG: + snprintf(prof_desc->dev_name, sizeof(prof_hdr->dev_name), + "%s", prof_hdr->dev_name); + AW_DEV_LOGI(dev, "parse reg type data enter,product=[%s],prof_id=[%d],prof_name=[%s]", + prof_hdr->dev_name, prof_hdr->dev_profile, + prof_hdr->dev_profile_str); + prof_desc->prof_name = prof_hdr->dev_profile_str; + ret = aw_parse_raw_reg(dev, cfg_data, prof_hdr->data_size, + prof_desc); + break; + case AW_BIN_TYPE_HDR_REG: + snprintf(prof_desc->dev_name, sizeof(prof_hdr->dev_name), + "%s", prof_hdr->dev_name); + AW_DEV_LOGI(dev, "parse hdr_reg type data enter,product=[%s],prof_id=[%d],prof_name=[%s]", + prof_hdr->dev_name, prof_hdr->dev_profile, + prof_hdr->dev_profile_str); + prof_desc->prof_name = prof_hdr->dev_profile_str; + ret = aw_parse_reg_with_hdr(dev, cfg_data, + prof_hdr->data_size, prof_desc); + break; + } + + return ret; +} + +static int aw_parse_dev_type_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int i = 0; + int ret; + int parse_prof_count = 0; + char *cfg_data = NULL; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_info->acf_hdr.ddt_offset); + + AW_DEV_LOGD(dev, "enter"); + + for (i = 0; i < acf_info->acf_hdr.dde_num; i++) { + if ((acf_dde[i].type == AW_DDE_DEV_TYPE_ID) && + (acf_info->aw_dev->i2c_bus == acf_dde[i].dev_bus) && + (acf_info->aw_dev->i2c_addr == acf_dde[i].dev_addr) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + if (acf_dde[i].data_type == AW_MONITOR) { + cfg_data = acf_info->fw_data + acf_dde[i].data_offset; + AW_DEV_LOGD(dev, "parse monitor type data enter"); + ret = aw_parse_monitor_config(dev, cfg_data, + acf_dde[i].data_size); + } else { + ret = aw_parse_data_by_sec_type_v_1_0_0_0(dev, acf_info, + &acf_dde[i]); + if (ret < 0) + AW_DEV_LOGE(dev, "parse dev type data failed"); + else + parse_prof_count++; + } + } + } + + if (parse_prof_count == 0) { + AW_DEV_LOGE(dev, "get dev type num is %d, parse failed", parse_prof_count); + return -EINVAL; + } + + return AW_DEV_TYPE_OK; +} + +static int aw_parse_default_type_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int i = 0; + int ret; + int parse_prof_count = 0; + char *cfg_data = NULL; + struct aw_acf_dde_v_1_0_0_0 *acf_dde = + (struct aw_acf_dde_v_1_0_0_0 *)(acf_info->fw_data + acf_info->acf_hdr.ddt_offset); + + AW_DEV_LOGD(dev, "enter"); + + for (i = 0; i < acf_info->acf_hdr.dde_num; i++) { + if ((acf_dde[i].type == AW_DDE_DEV_DEFAULT_TYPE_ID) && + (acf_info->dev_index == acf_dde[i].dev_index) && + (acf_info->aw_dev->chipid == acf_dde[i].chip_id)) { + ret = aw_check_product_name_v_1_0_0_0(dev, acf_info, &acf_dde[i]); + if (ret < 0) + continue; + + if (acf_dde[i].data_type == AW_MONITOR) { + cfg_data = acf_info->fw_data + acf_dde[i].data_offset; + AW_DEV_LOGD(dev, "parse monitor type data enter"); + ret = aw_parse_monitor_config(dev, cfg_data, + acf_dde[i].data_size); + } else { + ret = aw_parse_data_by_sec_type_v_1_0_0_0(dev, acf_info, + &acf_dde[i]); + if (ret < 0) + AW_DEV_LOGE(dev, "parse default type data failed"); + else + parse_prof_count++; + } + } + } + + if (parse_prof_count == 0) { + AW_DEV_LOGE(dev, "get default type num is %d,parse failed", parse_prof_count); + return -EINVAL; + } + + return AW_DEV_TYPE_OK; +} + +static int aw_parse_by_hdr_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + int ret; + + if (acf_info->prof_info.prof_type == AW_DDE_DEV_TYPE_ID) { + ret = aw_parse_dev_type_v_1_0_0_0(dev, acf_info); + if (ret < 0) + return ret; + } else if (acf_info->prof_info.prof_type == AW_DDE_DEV_DEFAULT_TYPE_ID) { + ret = aw_parse_default_type_v_1_0_0_0(dev, acf_info); + if (ret < 0) + return ret; + } + + return 0; +} + +static int aw_set_prof_off_info_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_prof_info *prof_info = &acf_info->prof_info; + int i = 0; + int ret = 0; + + for (i = 0; i < prof_info->count; ++i) { + if (!(prof_info->prof_desc[i].prof_st)) { + snprintf(prof_info->prof_name_list[i], AW_PROFILE_STR_MAX, "%s", + g_power_off_name[0]); + prof_info->prof_desc[i].prof_name = prof_info->prof_name_list[i]; + prof_info->prof_desc[i].prof_st = AW_PROFILE_WAIT; + memset(&prof_info->prof_desc[i].data_container, 0, + sizeof(struct aw_data_container)); + return 0; + } + + ret = aw_check_prof_str_is_off(prof_info->prof_name_list[i]); + if (ret == 0) { + AW_DEV_LOGD(dev, "found profile off,data_len=[%d]", + prof_info->prof_desc[i].data_container.len); + return 0; + } + } + + AW_DEV_LOGE(dev, "index[%d] is out of table,profile count[%d]", + i, prof_info->count); + return -EINVAL; +} + +static int aw_parse_acf_v_1_0_0_0(struct device *dev, + struct acf_bin_info *acf_info) + +{ + struct aw_prof_info *prof_info = &acf_info->prof_info; + int ret; + + ret = aw_parse_get_profile_count_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "get profile count failed"); + return ret; + } + + ret = aw_parse_prof_name_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "get profile count failed"); + return ret; + } + + acf_info->prof_info.prof_desc = devm_kzalloc(dev, + prof_info->count * sizeof(struct aw_prof_desc), GFP_KERNEL); + if (acf_info->prof_info.prof_desc == NULL) { + AW_DEV_LOGE(dev, "prof_desc devm_kzalloc failed"); + return -ENOMEM; + } + + ret = aw_parse_by_hdr_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "parse data failed"); + return ret; + } + + ret = aw_set_prof_off_info_v_1_0_0_0(dev, acf_info); + if (ret < 0) { + AW_DEV_LOGE(dev, "set profile off info failed"); + return ret; + } + + prof_info->status = AW_ACF_UPDATE; + AW_DEV_LOGI(dev, "acf paese succeed"); + return 0; +} + + +/************************************************************************* + * + *acf parse API + * + *************************************************************************/ +void aw87xxx_acf_profile_free(struct device *dev, struct acf_bin_info *acf_info) +{ + struct aw_prof_info *prof_info = &acf_info->prof_info; + + prof_info->count = 0; + prof_info->status = AW_ACF_WAIT; + memset(&acf_info->acf_hdr, 0, sizeof(struct aw_acf_hdr)); + + if (prof_info->prof_desc) { + devm_kfree(dev, prof_info->prof_desc); + prof_info->prof_desc = NULL; + } + + if (prof_info->prof_name_list) { + devm_kfree(dev, prof_info->prof_name_list); + prof_info->prof_name_list = NULL; + } + + if (acf_info->fw_data) { + vfree(acf_info->fw_data); + acf_info->fw_data = NULL; + } +} + +int aw87xxx_acf_parse(struct device *dev, struct acf_bin_info *acf_info) +{ + int ret = 0; + + AW_DEV_LOGD(dev, "enter"); + acf_info->prof_info.status = AW_ACF_WAIT; + ret = aw_check_acf_firmware(dev, acf_info->fw_data, + acf_info->fw_size); + if (ret < 0) { + AW_DEV_LOGE(dev, "load firmware check failed"); + return -EINVAL; + } + + memcpy(&acf_info->acf_hdr, acf_info->fw_data, + sizeof(struct aw_acf_hdr)); + + switch (acf_info->acf_hdr.hdr_version) { + case AW_ACF_HDR_VER_0_0_0_1: + return aw_parse_acf_v_0_0_0_1(dev, acf_info); + case AW_ACF_HDR_VER_1_0_0_0: + return aw_parse_acf_v_1_0_0_0(dev, acf_info); + default: + AW_DEV_LOGE(dev, "unsupported hdr_version [0x%x]", + acf_info->acf_hdr.hdr_version); + return -EINVAL; + } + + return ret; +} + +struct aw_prof_desc *aw87xxx_acf_get_prof_desc_form_name(struct device *dev, + struct acf_bin_info *acf_info, char *profile_name) +{ + int i = 0; + struct aw_prof_desc *prof_desc = NULL; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + AW_DEV_LOGD(dev, "enter"); + + if (!acf_info->prof_info.status) { + AW_DEV_LOGE(dev, "profile_cfg not load"); + return NULL; + } + + for (i = 0; i < prof_info->count; i++) { + if (!strncmp(profile_name, prof_info->prof_desc[i].prof_name, + AW_PROFILE_STR_MAX)) { + prof_desc = &prof_info->prof_desc[i]; + break; + } + } + + if (i == prof_info->count) { + AW_DEV_LOGE(dev, "profile not found"); + return NULL; + } + + AW_DEV_LOGI(dev, "get prof desc down"); + return prof_desc; +} + +int aw87xxx_acf_get_prof_index_form_name(struct device *dev, + struct acf_bin_info *acf_info, char *profile_name) +{ + int i = 0; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + if (!acf_info->prof_info.status) { + AW_DEV_LOGE(dev, "profile_cfg not load"); + return -EINVAL; + } + + for (i = 0; i < prof_info->count; i++) { + if (!strncmp(profile_name, prof_info->prof_name_list[i], + AW_PROFILE_STR_MAX)) { + return i; + } + } + + AW_DEV_LOGE(dev, "profile_index not found"); + return -EINVAL; +} + +char *aw87xxx_acf_get_prof_name_form_index(struct device *dev, + struct acf_bin_info *acf_info, int index) +{ + struct aw_prof_info *prof_info = &acf_info->prof_info; + + if (!acf_info->prof_info.status) { + AW_DEV_LOGE(dev, "profile_cfg not load"); + return NULL; + } + + if (index >= prof_info->count || index < 0) { + AW_DEV_LOGE(dev, "profile_index out of table"); + return NULL; + } + + return prof_info->prof_desc[index].prof_name; +} + + +int aw87xxx_acf_get_profile_count(struct device *dev, + struct acf_bin_info *acf_info) +{ + struct aw_prof_info *prof_info = &acf_info->prof_info; + + if (!acf_info->prof_info.status) { + AW_DEV_LOGE(dev, "profile_cfg not load"); + return -EINVAL; + } + + if (prof_info->count > 0) { + return prof_info->count; + } + + return -EINVAL; +} + +char *aw87xxx_acf_get_prof_off_name(struct device *dev, + struct acf_bin_info *acf_info) +{ + int i = 0; + int ret = 0; + struct aw_prof_info *prof_info = &acf_info->prof_info; + + if (!acf_info->prof_info.status) { + AW_DEV_LOGE(dev, "profile_cfg not load"); + return NULL; + } + + for (i = 0; i < prof_info->count; i++) { + ret = aw_check_prof_str_is_off(prof_info->prof_name_list[i]); + if (ret == 0) + return prof_info->prof_name_list[i]; + } + + return NULL; +} + +void aw87xxx_acf_init(struct aw_device *aw_dev, struct acf_bin_info *acf_info, int index) +{ + + acf_info->load_count = 0; + acf_info->prof_info.status = AW_ACF_WAIT; + acf_info->dev_index = index; + acf_info->aw_dev = aw_dev; + acf_info->product_cnt = aw_dev->product_cnt; + acf_info->product_tab = aw_dev->product_tab; + acf_info->prof_info.prof_desc = NULL; + acf_info->fw_data = NULL; + acf_info->fw_size = 0; +} + diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.h b/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.h new file mode 100644 index 000000000000..ebe0c77f5674 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_acf_bin.h @@ -0,0 +1,191 @@ +#ifndef __AW87XXX_ACF_BIN_H__ +#define __AW87XXX_ACF_BIN_H__ + +#include "aw87xxx_device.h" + +#define AW_PROJECT_NAME_MAX (24) +#define AW_CUSTOMER_NAME_MAX (16) +#define AW_CFG_VERSION_MAX (4) +#define AW_TBL_VERSION_MAX (4) +#define AW_DDE_DEVICE_TYPE (0) +#define AW_DDE_SKT_TYPE (1) +#define AW_DDE_DEFAULT_TYPE (2) + +#define AW_REG_ADDR_BYTE (1) +#define AW_REG_DATA_BYTE (1) + +#define AW_ACF_FILE_ID (0xa15f908) +#define AW_PROFILE_STR_MAX (32) +#define AW_POWER_OFF_NAME_SUPPORT_COUNT (5) + +enum aw_cfg_hdr_version { + AW_ACF_HDR_VER_0_0_0_1 = 0x00000001, + AW_ACF_HDR_VER_1_0_0_0 = 0x01000000, +}; + +enum aw_acf_dde_type_id { + AW_DEV_NONE_TYPE_ID = 0xFFFFFFFF, + AW_DDE_DEV_TYPE_ID = 0x00000000, + AW_DDE_SKT_TYPE_ID = 0x00000001, + AW_DDE_DEV_DEFAULT_TYPE_ID = 0x00000002, + AW_DDE_TYPE_MAX, +}; + +enum aw_raw_data_type_id { + AW_BIN_TYPE_REG = 0x00000000, + AW_BIN_TYPE_DSP, + AW_BIN_TYPE_DSP_CFG, + AW_BIN_TYPE_DSP_FW, + AW_BIN_TYPE_HDR_REG, + AW_BIN_TYPE_HDR_DSP_CFG, + AW_BIN_TYPE_HDR_DSP_FW, + AW_BIN_TYPE_MUTLBIN, + AW_SKT_UI_PROJECT, + AW_DSP_CFG, + AW_MONITOR, + AW_BIN_TYPE_MAX, +}; + +enum { + AW_DEV_TYPE_OK = 0, + AW_DEV_TYPE_NONE = 1, +}; + +enum aw_profile_status { + AW_PROFILE_WAIT = 0, + AW_PROFILE_OK, +}; + +enum aw_acf_load_status { + AW_ACF_WAIT = 0, + AW_ACF_UPDATE, +}; + +enum aw_bin_dev_profile_id { + AW_PROFILE_MUSIC = 0x0000, + AW_PROFILE_VOICE, + AW_PROFILE_VOIP, + AW_PROFILE_RINGTONE, + AW_PROFILE_RINGTONE_HS, + AW_PROFILE_LOWPOWER, + AW_PROFILE_BYPASS, + AW_PROFILE_MMI, + AW_PROFILE_FM, + AW_PROFILE_NOTIFICATION, + AW_PROFILE_RECEIVER, + AW_PROFILE_OFF, + AW_PROFILE_MAX, +}; + +struct aw_acf_hdr { + int32_t a_id; /* acf file ID 0xa15f908 */ + char project[AW_PROJECT_NAME_MAX]; /* project name */ + char custom[AW_CUSTOMER_NAME_MAX]; /* custom name :huawei xiaomi vivo oppo */ + uint8_t version[AW_CFG_VERSION_MAX]; /* author update version */ + int32_t author_id; /* author id */ + int32_t ddt_size; /* sub section table entry size */ + int32_t dde_num; /* sub section table entry num */ + int32_t ddt_offset; /* sub section table offset in file */ + int32_t hdr_version; /* sub section table version */ + int32_t reserve[3]; /* Reserved Bits */ +}; + +struct aw_acf_dde { + int32_t type; /* dde type id */ + char dev_name[AW_CUSTOMER_NAME_MAX]; /* customer dev name */ + int16_t dev_index; /* dev id */ + int16_t dev_bus; /* dev bus id */ + int16_t dev_addr; /* dev addr id */ + int16_t dev_profile; /* dev profile id */ + int32_t data_type; /* data type id */ + int32_t data_size; /* dde data size in block */ + int32_t data_offset; /* dde data offset in block */ + int32_t data_crc; /* dde data crc checkout */ + int32_t reserve[5]; /* Reserved Bits */ +}; + +struct aw_acf_dde_v_1_0_0_0 { + uint32_t type; /* DDE type id */ + char dev_name[AW_CUSTOMER_NAME_MAX]; /* customer dev name */ + uint16_t dev_index; /* dev id */ + uint16_t dev_bus; /* dev bus id */ + uint16_t dev_addr; /* dev addr id */ + uint16_t dev_profile; /* dev profile id*/ + uint32_t data_type; /* data type id */ + uint32_t data_size; /* dde data size in block */ + uint32_t data_offset; /* dde data offset in block */ + uint32_t data_crc; /* dde data crc checkout */ + char dev_profile_str[AW_PROFILE_STR_MAX]; /* dde custom profile name */ + uint32_t chip_id; /* dde custom product chip id */ + uint32_t reserve[4]; +}; + +struct aw_data_with_header { + uint32_t check_sum; + uint32_t header_ver; + uint32_t bin_data_type; + uint32_t bin_data_ver; + uint32_t bin_data_size; + uint32_t ui_ver; + char product[8]; + uint32_t addr_byte_len; + uint32_t data_byte_len; + uint32_t device_addr; + uint32_t reserve[4]; +}; + +struct aw_data_container { + uint32_t len; + uint8_t *data; +}; + +struct aw_prof_desc { + uint32_t prof_st; + char *prof_name; + char dev_name[AW_CUSTOMER_NAME_MAX]; + struct aw_data_container data_container; +}; + +struct aw_all_prof_info { + struct aw_prof_desc prof_desc[AW_PROFILE_MAX]; +}; + +struct aw_prof_info { + int count; + int status; + int prof_type; + char (*prof_name_list)[AW_PROFILE_STR_MAX]; + struct aw_prof_desc *prof_desc; +}; + +struct acf_bin_info { + int load_count; + int fw_size; + int16_t dev_index; + char *fw_data; + int product_cnt; + const char **product_tab; + struct aw_device *aw_dev; + + struct aw_acf_hdr acf_hdr; + struct aw_prof_info prof_info; +}; + +char *aw87xxx_ctos_get_prof_name(int profile_id); +void aw87xxx_acf_profile_free(struct device *dev, + struct acf_bin_info *acf_info); +int aw87xxx_acf_parse(struct device *dev, struct acf_bin_info *acf_info); +struct aw_prof_desc *aw87xxx_acf_get_prof_desc_form_name(struct device *dev, + struct acf_bin_info *acf_info, char *profile_name); +int aw87xxx_acf_get_prof_index_form_name(struct device *dev, + struct acf_bin_info *acf_info, char *profile_name); +char *aw87xxx_acf_get_prof_name_form_index(struct device *dev, + struct acf_bin_info *acf_info, int index); +int aw87xxx_acf_get_profile_count(struct device *dev, + struct acf_bin_info *acf_info); +char *aw87xxx_acf_get_prof_off_name(struct device *dev, + struct acf_bin_info *acf_info); +void aw87xxx_acf_init(struct aw_device *aw_dev, struct acf_bin_info *acf_info, int index); + + +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.c b/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.c new file mode 100644 index 000000000000..7eab9efde147 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.c @@ -0,0 +1,515 @@ +/* +* aw87xxx_bin_parse.c +* +* Copyright (c) 2020 AWINIC Technology CO., LTD +* +* This program is free software; you can redistribute it and/or modify it +* under the terms of the GNU General Public License as published by the +* Free Software Foundation; either version 2 of the License, or (at your +* option) any later version. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx_bin_parse.h" + +#define AWINIC_CODE_VERSION "V0.0.7-V1.0.4" /* "code version"-"excel version" */ + +#define DEBUG_LOG_LEVEL +#ifdef DEBUG_LOG_LEVEL +#define DBG(fmt, arg...) do {\ +printk("AWINIC_BIN %s,line= %d,"fmt, __func__, __LINE__, ##arg);\ +} while (0) +#define DBG_ERR(fmt, arg...) do {\ +printk("AWINIC_BIN_ERR %s,line= %d,"fmt, __func__, __LINE__, ##arg);\ +} while (0) +#else +#define DBG(fmt, arg...) do {} while (0) +#define DBG_ERR(fmt, arg...) do {} while (0) +#endif + +#define printing_data_code + +typedef unsigned short int aw_uint16; +typedef unsigned long int aw_uint32; + +#define BigLittleSwap16(A) ((((aw_uint16)(A) & 0xff00) >> 8) | \ + (((aw_uint16)(A) & 0x00ff) << 8)) + +#define BigLittleSwap32(A) ((((aw_uint32)(A) & 0xff000000) >> 24) | \ + (((aw_uint32)(A) & 0x00ff0000) >> 8) | \ + (((aw_uint32)(A) & 0x0000ff00) << 8) | \ + (((aw_uint32)(A) & 0x000000ff) << 24)) + + +static int aw_parse_bin_header_1_0_0(struct aw_bin *bin); + +/** +* +* Interface function +* +* return value: +* value = 0 :success; +* value = -1 :check bin header version +* value = -2 :check bin data type +* value = -3 :check sum or check bin data len error +* value = -4 :check data version +* value = -5 :check register num +* value = -6 :check dsp reg num +* value = -7 :check soc app num +* value = -8 :bin is NULL point +* +**/ + +/******************************************************** +* +* check sum data +* +********************************************************/ +static int aw_check_sum(struct aw_bin *bin, int bin_num) +{ + unsigned int i = 0; + unsigned int sum_data = 0; + unsigned int check_sum = 0; + unsigned char *p_check_sum = NULL; + + DBG("enter\n"); + + p_check_sum = + &(bin->info.data[(bin->header_info[bin_num].valid_data_addr - + bin->header_info[bin_num].header_len)]); + DBG("aw_bin_parse p_check_sum = %p\n", p_check_sum); + check_sum = GET_32_DATA(*(p_check_sum + 3), + *(p_check_sum + 2), + *(p_check_sum + 1), *(p_check_sum)); + + for (i = 4; + i < + bin->header_info[bin_num].bin_data_len + + bin->header_info[bin_num].header_len; i++) { + sum_data += *(p_check_sum + i); + } + DBG("aw_bin_parse bin_num=%d, check_sum = 0x%x, sum_data = 0x%x\n", + bin_num, check_sum, sum_data); + if (sum_data != check_sum) { + p_check_sum = NULL; + DBG_ERR("aw_bin_parse check sum or check bin data len error\n"); + DBG_ERR("aw_bin_parse bin_num=%d, check_sum = 0x%x, sum_data = 0x%x\n", bin_num, check_sum, sum_data); + return -3; + } + p_check_sum = NULL; + + return 0; +} + +static int aw_check_data_version(struct aw_bin *bin, int bin_num) +{ + int i = 0; + DBG("enter\n"); + + for (i = DATA_VERSION_V1; i < DATA_VERSION_MAX; i++) { + if (bin->header_info[bin_num].bin_data_ver == i) { + return 0; + } + } + DBG_ERR("aw_bin_parse Unrecognized this bin data version\n"); + return -4; +} + +static int aw_check_register_num_v1(struct aw_bin *bin, int bin_num) +{ + unsigned int check_register_num = 0; + unsigned int parse_register_num = 0; + unsigned char *p_check_sum = NULL; + + DBG("enter\n"); + + p_check_sum = + &(bin->info.data[(bin->header_info[bin_num].valid_data_addr)]); + DBG("aw_bin_parse p_check_sum = %p\n", p_check_sum); + parse_register_num = GET_32_DATA(*(p_check_sum + 3), + *(p_check_sum + 2), + *(p_check_sum + 1), *(p_check_sum)); + check_register_num = (bin->header_info[bin_num].bin_data_len - 4) / + (bin->header_info[bin_num].reg_byte_len + + bin->header_info[bin_num].data_byte_len); + DBG + ("aw_bin_parse bin_num=%d, parse_register_num = 0x%x, check_register_num = 0x%x\n", + bin_num, parse_register_num, check_register_num); + if (parse_register_num != check_register_num) { + p_check_sum = NULL; + DBG_ERR("aw_bin_parse register num is error\n"); + DBG_ERR("aw_bin_parse bin_num=%d, parse_register_num = 0x%x, check_register_num = 0x%x\n", bin_num, parse_register_num, check_register_num); + return -5; + } + bin->header_info[bin_num].reg_num = parse_register_num; + bin->header_info[bin_num].valid_data_len = + bin->header_info[bin_num].bin_data_len - 4; + p_check_sum = NULL; + bin->header_info[bin_num].valid_data_addr = + bin->header_info[bin_num].valid_data_addr + 4; + return 0; +} + +static int aw_check_dsp_reg_num_v1(struct aw_bin *bin, int bin_num) +{ + unsigned int check_dsp_reg_num = 0; + unsigned int parse_dsp_reg_num = 0; + unsigned char *p_check_sum = NULL; + + DBG("enter\n"); + + p_check_sum = + &(bin->info.data[(bin->header_info[bin_num].valid_data_addr)]); + DBG("aw_bin_parse p_check_sum = %p\n", p_check_sum); + parse_dsp_reg_num = GET_32_DATA(*(p_check_sum + 7), + *(p_check_sum + 6), + *(p_check_sum + 5), *(p_check_sum + 4)); + bin->header_info[bin_num].reg_data_byte_len = + GET_32_DATA(*(p_check_sum + 11), *(p_check_sum + 10), + *(p_check_sum + 9), *(p_check_sum + 8)); + check_dsp_reg_num = + (bin->header_info[bin_num].bin_data_len - + 12) / bin->header_info[bin_num].reg_data_byte_len; + DBG + ("aw_bin_parse bin_num=%d, parse_dsp_reg_num = 0x%x, check_dsp_reg_num = 0x%x\n", + bin_num, parse_dsp_reg_num, check_dsp_reg_num); + if (parse_dsp_reg_num != check_dsp_reg_num) { + p_check_sum = NULL; + DBG_ERR("aw_bin_parse dsp reg num is error\n"); + DBG_ERR("aw_bin_parse bin_num=%d, parse_dsp_reg_num = 0x%x, check_dsp_reg_num = 0x%x\n", bin_num, parse_dsp_reg_num, check_dsp_reg_num); + return -6; + } + bin->header_info[bin_num].download_addr = + GET_32_DATA(*(p_check_sum + 3), *(p_check_sum + 2), + *(p_check_sum + 1), *(p_check_sum)); + bin->header_info[bin_num].reg_num = parse_dsp_reg_num; + bin->header_info[bin_num].valid_data_len = + bin->header_info[bin_num].bin_data_len - 12; + p_check_sum = NULL; + bin->header_info[bin_num].valid_data_addr = + bin->header_info[bin_num].valid_data_addr + 12; + return 0; +} + +static int aw_check_soc_app_num_v1(struct aw_bin *bin, int bin_num) +{ + unsigned int check_soc_app_num = 0; + unsigned int parse_soc_app_num = 0; + unsigned char *p_check_sum = NULL; + + DBG("enter\n"); + + p_check_sum = + &(bin->info.data[(bin->header_info[bin_num].valid_data_addr)]); + DBG("aw_bin_parse p_check_sum = %p\n", p_check_sum); + bin->header_info[bin_num].app_version = GET_32_DATA(*(p_check_sum + 3), + *(p_check_sum + 2), + *(p_check_sum + 1), + *(p_check_sum)); + parse_soc_app_num = GET_32_DATA(*(p_check_sum + 11), + *(p_check_sum + 10), + *(p_check_sum + 9), *(p_check_sum + 8)); + check_soc_app_num = bin->header_info[bin_num].bin_data_len - 12; + DBG + ("aw_bin_parse bin_num=%d, parse_soc_app_num = 0x%x, check_soc_app_num = 0x%x\n", + bin_num, parse_soc_app_num, check_soc_app_num); + if (parse_soc_app_num != check_soc_app_num) { + p_check_sum = NULL; + DBG_ERR("aw_bin_parse soc app num is error\n"); + DBG_ERR("aw_bin_parse bin_num=%d, parse_soc_app_num = 0x%x, check_soc_app_num = 0x%x\n", bin_num, parse_soc_app_num, check_soc_app_num); + return -7; + } + bin->header_info[bin_num].reg_num = parse_soc_app_num; + bin->header_info[bin_num].download_addr = + GET_32_DATA(*(p_check_sum + 7), *(p_check_sum + 6), + *(p_check_sum + 5), *(p_check_sum + 4)); + bin->header_info[bin_num].valid_data_len = + bin->header_info[bin_num].bin_data_len - 12; + p_check_sum = NULL; + bin->header_info[bin_num].valid_data_addr = + bin->header_info[bin_num].valid_data_addr + 12; + return 0; +} + +/************************ +*** +***bin header 1_0_0 +*** +************************/ +static void aw_get_single_bin_header_1_0_0(struct aw_bin *bin) +{ + int i; + DBG("enter %s\n", __func__); + bin->header_info[bin->all_bin_parse_num].header_len = 60; + bin->header_info[bin->all_bin_parse_num].check_sum = + GET_32_DATA(*(bin->p_addr + 3), *(bin->p_addr + 2), + *(bin->p_addr + 1), *(bin->p_addr)); + bin->header_info[bin->all_bin_parse_num].header_ver = + GET_32_DATA(*(bin->p_addr + 7), *(bin->p_addr + 6), + *(bin->p_addr + 5), *(bin->p_addr + 4)); + bin->header_info[bin->all_bin_parse_num].bin_data_type = + GET_32_DATA(*(bin->p_addr + 11), *(bin->p_addr + 10), + *(bin->p_addr + 9), *(bin->p_addr + 8)); + bin->header_info[bin->all_bin_parse_num].bin_data_ver = + GET_32_DATA(*(bin->p_addr + 15), *(bin->p_addr + 14), + *(bin->p_addr + 13), *(bin->p_addr + 12)); + bin->header_info[bin->all_bin_parse_num].bin_data_len = + GET_32_DATA(*(bin->p_addr + 19), *(bin->p_addr + 18), + *(bin->p_addr + 17), *(bin->p_addr + 16)); + bin->header_info[bin->all_bin_parse_num].ui_ver = + GET_32_DATA(*(bin->p_addr + 23), *(bin->p_addr + 22), + *(bin->p_addr + 21), *(bin->p_addr + 20)); + bin->header_info[bin->all_bin_parse_num].reg_byte_len = + GET_32_DATA(*(bin->p_addr + 35), *(bin->p_addr + 34), + *(bin->p_addr + 33), *(bin->p_addr + 32)); + bin->header_info[bin->all_bin_parse_num].data_byte_len = + GET_32_DATA(*(bin->p_addr + 39), *(bin->p_addr + 38), + *(bin->p_addr + 37), *(bin->p_addr + 36)); + bin->header_info[bin->all_bin_parse_num].device_addr = + GET_32_DATA(*(bin->p_addr + 43), *(bin->p_addr + 42), + *(bin->p_addr + 41), *(bin->p_addr + 40)); + for (i = 0; i < 8; i++) { + bin->header_info[bin->all_bin_parse_num].chip_type[i] = + *(bin->p_addr + 24 + i); + } + bin->header_info[bin->all_bin_parse_num].reg_num = 0x00000000; + bin->header_info[bin->all_bin_parse_num].reg_data_byte_len = 0x00000000; + bin->header_info[bin->all_bin_parse_num].download_addr = 0x00000000; + bin->header_info[bin->all_bin_parse_num].app_version = 0x00000000; + bin->header_info[bin->all_bin_parse_num].valid_data_len = 0x00000000; + bin->all_bin_parse_num += 1; +} + +static int aw_parse_each_of_multi_bins_1_0_0(unsigned int bin_num, int bin_serial_num, + struct aw_bin *bin) +{ + int ret = 0; + unsigned int bin_start_addr = 0; + unsigned int valid_data_len = 0; + DBG("aw_bin_parse enter multi bin branch -- %s\n", __func__); + if (!bin_serial_num) { + bin_start_addr = GET_32_DATA(*(bin->p_addr + 67), + *(bin->p_addr + 66), + *(bin->p_addr + 65), + *(bin->p_addr + 64)); + bin->p_addr += (60 + bin_start_addr); + bin->header_info[bin->all_bin_parse_num].valid_data_addr = + bin->header_info[bin->all_bin_parse_num - + 1].valid_data_addr + 4 + 8 * bin_num + 60; + } else { + valid_data_len = + bin->header_info[bin->all_bin_parse_num - 1].bin_data_len; + bin->p_addr += (60 + valid_data_len); + bin->header_info[bin->all_bin_parse_num].valid_data_addr = + bin->header_info[bin->all_bin_parse_num - + 1].valid_data_addr + + bin->header_info[bin->all_bin_parse_num - 1].bin_data_len + + 60; + } + + ret = aw_parse_bin_header_1_0_0(bin); + return ret; +} + +/* Get the number of bins in multi bins, and set a for loop, loop processing each bin data */ +static int aw_get_multi_bin_header_1_0_0(struct aw_bin *bin) +{ + int i = 0; + int ret = 0; + unsigned int bin_num = 0; + DBG("aw_bin_parse enter multi bin branch -- %s\n", __func__); + bin_num = GET_32_DATA(*(bin->p_addr + 63), + *(bin->p_addr + 62), + *(bin->p_addr + 61), *(bin->p_addr + 60)); + if (bin->multi_bin_parse_num == 1) { + bin->header_info[bin->all_bin_parse_num].valid_data_addr = 60; + } + aw_get_single_bin_header_1_0_0(bin); + + for (i = 0; i < bin_num; i++) { + DBG("aw_bin_parse enter multi bin for is %d\n", i); + ret = aw_parse_each_of_multi_bins_1_0_0(bin_num, i, bin); + if (ret < 0) { + return ret; + } + } + return 0; +} + +/******************************************************** +* +* If the bin framework header version is 1.0.0, + determine the data type of bin, and then perform different processing + according to the data type + If it is a single bin data type, write the data directly into the structure array + If it is a multi-bin data type, first obtain the number of bins, + and then recursively call the bin frame header processing function + according to the bin number to process the frame header information of each bin separately +* +********************************************************/ +static int aw_parse_bin_header_1_0_0(struct aw_bin *bin) +{ + int ret = 0; + unsigned int bin_data_type; + DBG("enter %s\n", __func__); + bin_data_type = GET_32_DATA(*(bin->p_addr + 11), + *(bin->p_addr + 10), + *(bin->p_addr + 9), *(bin->p_addr + 8)); + DBG("aw_bin_parse bin_data_type 0x%x\n", bin_data_type); + switch (bin_data_type) { + case DATA_TYPE_REGISTER: + case DATA_TYPE_DSP_REG: + case DATA_TYPE_SOC_APP: + /* Divided into two processing methods, + one is single bin processing, + and the other is single bin processing in multi bin */ + DBG("aw_bin_parse enter single bin branch\n"); + bin->single_bin_parse_num += 1; + DBG("%s bin->single_bin_parse_num is %d\n", __func__, + bin->single_bin_parse_num); + if (!bin->multi_bin_parse_num) { + bin->header_info[bin-> + all_bin_parse_num].valid_data_addr = + 60; + } + aw_get_single_bin_header_1_0_0(bin); + break; + case DATA_TYPE_MULTI_BINS: + /* Get the number of times to enter multi bins */ + DBG("aw_bin_parse enter multi bin branch\n"); + bin->multi_bin_parse_num += 1; + DBG("%s bin->multi_bin_parse_num is %d\n", __func__, + bin->multi_bin_parse_num); + ret = aw_get_multi_bin_header_1_0_0(bin); + if (ret < 0) { + return ret; + } + break; + default: + DBG_ERR("aw_bin_parse Unrecognized this bin data type\n"); + return -2; + } + return 0; +} + +/* get the bin's header version */ +static int aw_check_bin_header_version(struct aw_bin *bin) +{ + int ret = 0; + unsigned int header_version = 0; + + header_version = GET_32_DATA(*(bin->p_addr + 7), + *(bin->p_addr + 6), + *(bin->p_addr + 5), *(bin->p_addr + 4)); + + DBG("aw_bin_parse header_version 0x%x\n", header_version); + + /* Write data to the corresponding structure array + according to different formats of the bin frame header version */ + switch (header_version) { + case HEADER_VERSION_1_0_0: + ret = aw_parse_bin_header_1_0_0(bin); + return ret; + default: + DBG_ERR("aw_bin_parse Unrecognized this bin header version \n"); + return -1; + } +} + +int aw87xxx_parsing_bin_file(struct aw_bin *bin) +{ + int i = 0; + int ret = 0; + + DBG("aw_bin_parse code version:%s\n", AWINIC_CODE_VERSION); + if (!bin) { + DBG_ERR("aw_bin_parse bin is NULL\n"); + return -8; + } + bin->p_addr = bin->info.data; + bin->all_bin_parse_num = 0; + bin->multi_bin_parse_num = 0; + bin->single_bin_parse_num = 0; + + /* filling bins header info */ + ret = aw_check_bin_header_version(bin); + if (ret < 0) { + DBG_ERR("aw_bin_parse check bin header version error\n"); + return ret; + } + bin->p_addr = NULL; + + /* check bin header info */ + for (i = 0; i < bin->all_bin_parse_num; i++) { + /* check sum */ + ret = aw_check_sum(bin, i); + if (ret < 0) { + DBG_ERR("aw_bin_parse check sum data error\n"); + return ret; + } + /* check bin data version */ + ret = aw_check_data_version(bin, i); + if (ret < 0) { + DBG_ERR("aw_bin_parse check data version error\n"); + return ret; + } + /* check valid data */ + if (bin->header_info[i].bin_data_ver == DATA_VERSION_V1) { + /* check register num */ + if (bin->header_info[i].bin_data_type == + DATA_TYPE_REGISTER) { + ret = aw_check_register_num_v1(bin, i); + if (ret < 0) { + DBG_ERR + ("aw_bin_parse check register num error\n"); + return ret; + } + /* check dsp reg num */ + } else if (bin->header_info[i].bin_data_type == + DATA_TYPE_DSP_REG) { + ret = aw_check_dsp_reg_num_v1(bin, i); + if (ret < 0) { + DBG_ERR + ("aw_bin_parse check dsp reg num error\n"); + return ret; + } + /* check soc app num */ + } else if (bin->header_info[i].bin_data_type == + DATA_TYPE_SOC_APP) { + ret = aw_check_soc_app_num_v1(bin, i); + if (ret < 0) { + DBG_ERR + ("aw_bin_parse check soc app num error\n"); + return ret; + } + } else { + bin->header_info[i].valid_data_len = + bin->header_info[i].bin_data_len; + } + } + } + DBG("aw_bin_parse parsing success\n"); + + return 0; +} diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.h b/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.h new file mode 100644 index 000000000000..a99c2409e613 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_bin_parse.h @@ -0,0 +1,73 @@ +#ifndef __AW87XXX_BIN_PARSE_H__ +#define __AW87XXX_BIN_PARSE_H__ + +#define NULL ((void *)0) +#define GET_32_DATA(w, x, y, z) ((unsigned int)(((w) << 24) | ((x) << 16) | ((y) << 8) | (z))) +#define BIN_NUM_MAX 100 +#define HEADER_LEN 60 +/********************************************************* + * + * header information + * + ********************************************************/ +enum bin_header_version_enum { + HEADER_VERSION_1_0_0 = 0x01000000, +}; + +enum data_type_enum { + DATA_TYPE_REGISTER = 0x00000000, + DATA_TYPE_DSP_REG = 0x00000010, + DATA_TYPE_DSP_CFG = 0x00000011, + DATA_TYPE_SOC_REG = 0x00000020, + DATA_TYPE_SOC_APP = 0x00000021, + DATA_TYPE_MULTI_BINS = 0x00002000, + DATA_TYPE_MONITOR_ANALOG = 0x00020000, +}; + +enum data_version_enum { + DATA_VERSION_V1 = 0X00000001, /*default little edian */ + DATA_VERSION_MAX, +}; + +struct bin_header_info { + unsigned int header_len; /* Frame header length */ + unsigned int check_sum; /* Frame header information-Checksum */ + unsigned int header_ver; /* Frame header information-Frame header version */ + unsigned int bin_data_type; /* Frame header information-Data type */ + unsigned int bin_data_ver; /* Frame header information-Data version */ + unsigned int bin_data_len; /* Frame header information-Data length */ + unsigned int ui_ver; /* Frame header information-ui version */ + unsigned char chip_type[8]; /* Frame header information-chip type */ + unsigned int reg_byte_len; /* Frame header information-reg byte len */ + unsigned int data_byte_len; /* Frame header information-data byte len */ + unsigned int device_addr; /* Frame header information-device addr */ + unsigned int valid_data_len; /* Length of valid data obtained after parsing */ + unsigned int valid_data_addr; /* The offset address of the valid data obtained after parsing relative to info */ + + unsigned int reg_num; /* The number of registers obtained after parsing */ + unsigned int reg_data_byte_len; /* The byte length of the register obtained after parsing */ + unsigned int download_addr; /* The starting address or download address obtained after parsing */ + unsigned int app_version; /* The software version number obtained after parsing */ +}; + +/************************************************************ +* +* function define +* +************************************************************/ +struct bin_container { + unsigned int len; /* The size of the bin file obtained from the firmware */ + unsigned char data[]; /* Store the bin file obtained from the firmware */ +}; + +struct aw_bin { + unsigned char *p_addr; /* Offset pointer (backward offset pointer to obtain frame header information and important information) */ + unsigned int all_bin_parse_num; /* The number of all bin files */ + unsigned int multi_bin_parse_num; /* The number of single bin files */ + unsigned int single_bin_parse_num; /* The number of multiple bin files */ + struct bin_header_info header_info[BIN_NUM_MAX]; /* Frame header information and other important data obtained after parsing */ + struct bin_container info; /* Obtained bin file data that needs to be parsed */ +}; + +extern int aw87xxx_parsing_bin_file(struct aw_bin *bin); +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_device.c b/sound/soc/codecs/aw87xxx/aw87xxx_device.c new file mode 100644 index 000000000000..a4c9ad7d96dc --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_device.c @@ -0,0 +1,977 @@ +/* + * aw87xxx_device.c aw87xxx pa module + * + * Copyright (c) 2021 AWINIC Technology CO., LTD + * + * Author: Barry + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx.h" +#include "aw87xxx_device.h" +#include "aw87xxx_log.h" +#include "aw87xxx_pid_9b_reg.h" +#include "aw87xxx_pid_18_reg.h" +#include "aw87xxx_pid_39_reg.h" +#include "aw87xxx_pid_59_3x9_reg.h" +#include "aw87xxx_pid_59_5x9_reg.h" +#include "aw87xxx_pid_5a_reg.h" +#include "aw87xxx_pid_76_reg.h" +#include "aw87xxx_pid_60_reg.h" + +/************************************************************************* + * aw87xxx variable + ************************************************************************/ +const char *g_aw_pid_9b_product[] = { + "aw87319", +}; +const char *g_aw_pid_18_product[] = { + "aw87418", +}; + +const char *g_aw_pid_39_product[] = { + "aw87329", + "aw87339", + "aw87349", +}; + +const char *g_aw_pid_59_3x9_product[] = { + "aw87359", + "aw87389", +}; + +const char *g_aw_pid_59_5x9_product[] = { + "aw87509", + "aw87519", + "aw87529", + "aw87539", +}; + +const char *g_aw_pid_5a_product[] = { + "aw87549", + "aw87559", + "aw87569", + "aw87579", + "aw81509", +}; + +const char *g_aw_pid_76_product[] = { + "aw87390", + "aw87320", + "aw87401", + "aw87360", +}; + +const char *g_aw_pid_60_product[] = { + "aw87560", + "aw87561", + "aw87562", + "aw87501", + "aw87550", +}; + +static int aw87xxx_dev_get_chipid(struct aw_device *aw_dev); + +/*************************************************************************** + * + * reading and writing of I2C bus + * + ***************************************************************************/ +int aw87xxx_dev_i2c_write_byte(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t reg_data) +{ + int ret = -1; + unsigned char cnt = 0; + + while (cnt < AW_I2C_RETRIES) { + ret = i2c_smbus_write_byte_data(aw_dev->i2c, reg_addr, reg_data); + if (ret < 0) + AW_DEV_LOGE(aw_dev->dev, "i2c_write cnt=%d error=%d i2c_bus=%u i2c_addr=%X chipid=%X", + cnt, ret, aw_dev->i2c_bus, aw_dev->i2c_addr, aw_dev->chipid); + else + break; + + cnt++; + msleep(AW_I2C_RETRY_DELAY); + } + + return ret; +} + +int aw87xxx_dev_i2c_read_byte(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t *reg_data) +{ + int ret = -1; + unsigned char cnt = 0; + + while (cnt < AW_I2C_RETRIES) { + ret = i2c_smbus_read_byte_data(aw_dev->i2c, reg_addr); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "i2c_read cnt=%d error=%d i2c_bus=%u i2c_addr=%X chipid=%X", + cnt, ret, aw_dev->i2c_bus, aw_dev->i2c_addr, aw_dev->chipid); + } else { + *reg_data = ret; + break; + } + cnt++; + msleep(AW_I2C_RETRY_DELAY); + } + + return ret; +} + +int aw87xxx_dev_i2c_read_msg(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t *data_buf, uint32_t data_len) +{ + int ret = -1; + + struct i2c_msg msg[] = { + [0] = { + .addr = aw_dev->i2c_addr, + .flags = 0, + .len = sizeof(uint8_t), + .buf = ®_addr, + }, + [1] = { + .addr = aw_dev->i2c_addr, + .flags = I2C_M_RD, + .len = data_len, + .buf = data_buf, + }, + }; + + ret = i2c_transfer(aw_dev->i2c->adapter, msg, ARRAY_SIZE(msg)); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "transfer failed"); + return ret; + } else if (ret != AW_I2C_READ_MSG_NUM) { + AW_DEV_LOGE(aw_dev->dev, "transfer failed(size error)"); + return -ENXIO; + } + + return 0; +} + +int aw87xxx_dev_i2c_write_bits(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t mask, uint8_t reg_data) +{ + int ret = -1; + unsigned char reg_val = 0; + + ret = aw87xxx_dev_i2c_read_byte(aw_dev, reg_addr, ®_val); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "i2c read error, ret=%d", ret); + return ret; + } + reg_val &= mask; + reg_val |= (reg_data & (~mask)); + ret = aw87xxx_dev_i2c_write_byte(aw_dev, reg_addr, reg_val); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "i2c write error, ret=%d", ret); + return ret; + } + + return 0; +} + +/************************************************************************ + * + * aw87xxx device update profile data to registers + * + ************************************************************************/ +static int aw87xxx_dev_reg_update(struct aw_device *aw_dev, + struct aw_data_container *profile_data) +{ + int i = 0; + int ret = -1; + + if (profile_data == NULL) + return -EINVAL; + + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGE(aw_dev->dev, "dev is pwr_off,can not update reg"); + return -EINVAL; + } + + for (i = 0; i < profile_data->len; i = i + 2) { + AW_DEV_LOGI(aw_dev->dev, "reg=0x%02x, val = 0x%02x", + profile_data->data[i], profile_data->data[i + 1]); + + ret = aw87xxx_dev_i2c_write_byte(aw_dev, profile_data->data[i], + profile_data->data[i + 1]); + if (ret < 0) + return ret; + } + + return 0; +} + +static void aw87xxx_dev_reg_mute_bits_set(struct aw_device *aw_dev, + uint8_t *reg_val, bool enable) +{ + if (enable) { + *reg_val &= aw_dev->mute_desc.mask; + *reg_val |= aw_dev->mute_desc.enable; + } else { + *reg_val &= aw_dev->mute_desc.mask; + *reg_val |= aw_dev->mute_desc.disable; + } +} + +static int aw87xxx_dev_reg_update_mute(struct aw_device *aw_dev, + struct aw_data_container *profile_data) +{ + int i = 0; + int ret = -1; + uint8_t reg_val = 0; + + if (profile_data == NULL) + return -EINVAL; + + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGE(aw_dev->dev, "hwen is off,can not update reg"); + return -EINVAL; + } + + if (aw_dev->mute_desc.mask == AW_DEV_REG_INVALID_MASK) { + AW_DEV_LOGE(aw_dev->dev, "mute ctrl mask invalid"); + return -EINVAL; + } + + for (i = 0; i < profile_data->len; i = i + 2) { + AW_DEV_LOGI(aw_dev->dev, "reg=0x%02x, val = 0x%02x", + profile_data->data[i], profile_data->data[i + 1]); + + reg_val = profile_data->data[i + 1]; + if (profile_data->data[i] == aw_dev->mute_desc.addr) { + aw87xxx_dev_reg_mute_bits_set(aw_dev, ®_val, true); + AW_DEV_LOGD(aw_dev->dev, "change mute_mask, val = 0x%02x", + reg_val); + } + + ret = aw87xxx_dev_i2c_write_byte(aw_dev, profile_data->data[i], reg_val); + if (ret < 0) + return ret; + } + + return 0; +} + +/************************************************************************ + * + * aw87xxx device hadware and soft contols + * + ************************************************************************/ +static bool aw87xxx_dev_gpio_is_valid(struct aw_device *aw_dev) +{ + if (gpio_is_valid(aw_dev->rst_gpio)) + return true; + else + return false; +} + +void aw87xxx_dev_hw_pwr_ctrl(struct aw_device *aw_dev, bool enable) +{ + if (aw_dev->hwen_status == AW_DEV_HWEN_INVALID) { + AW_DEV_LOGD(aw_dev->dev, "product not have reset-pin,hardware pwd control invalid"); + return; + } + if (enable) { + if (aw87xxx_dev_gpio_is_valid(aw_dev)) { + gpio_set_value_cansleep(aw_dev->rst_gpio, AW_GPIO_LOW_LEVEL); + mdelay(2); + gpio_set_value_cansleep(aw_dev->rst_gpio, AW_GPIO_HIGHT_LEVEL); + mdelay(2); + aw_dev->hwen_status = AW_DEV_HWEN_ON; + AW_DEV_LOGI(aw_dev->dev, "hw power on"); + } else { + AW_DEV_LOGI(aw_dev->dev, "hw already power on"); + } + } else { + if (aw87xxx_dev_gpio_is_valid(aw_dev)) { + gpio_set_value_cansleep(aw_dev->rst_gpio, AW_GPIO_LOW_LEVEL); + mdelay(2); + aw_dev->hwen_status = AW_DEV_HWEN_OFF; + AW_DEV_LOGI(aw_dev->dev, "hw power off"); + } else { + AW_DEV_LOGI(aw_dev->dev, "hw already power off"); + } + } +} + +static int aw87xxx_dev_mute_ctrl(struct aw_device *aw_dev, bool enable) +{ + int ret = 0; + + if (enable) { + ret = aw87xxx_dev_i2c_write_bits(aw_dev, aw_dev->mute_desc.addr, + aw_dev->mute_desc.mask, aw_dev->mute_desc.enable); + if (ret < 0) + return ret; + AW_DEV_LOGI(aw_dev->dev, "set mute down"); + } else { + ret = aw87xxx_dev_i2c_write_bits(aw_dev, aw_dev->mute_desc.addr, + aw_dev->mute_desc.mask, aw_dev->mute_desc.disable); + if (ret < 0) + return ret; + AW_DEV_LOGI(aw_dev->dev, "close mute down"); + } + + return 0; +} + +void aw87xxx_dev_soft_reset(struct aw_device *aw_dev) +{ + int i = 0; + int ret = -1; + struct aw_soft_rst_desc *soft_rst = &aw_dev->soft_rst_desc; + + AW_DEV_LOGD(aw_dev->dev, "enter"); + + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGE(aw_dev->dev, "hw is off,can not softrst"); + return; + } + + if (aw_dev->soft_rst_enable == AW_DEV_SOFT_RST_DISENABLE) { + AW_DEV_LOGD(aw_dev->dev, "softrst is disenable"); + return; + } + + if (soft_rst->access == NULL || soft_rst->len == 0) { + AW_DEV_LOGE(aw_dev->dev, "softrst_info not init"); + return; + } + + if (soft_rst->len % 2) { + AW_DEV_LOGE(aw_dev->dev, "softrst data_len[%d] is odd number,data not available", + aw_dev->soft_rst_desc.len); + return; + } + + for (i = 0; i < soft_rst->len; i += 2) { + AW_DEV_LOGD(aw_dev->dev, "softrst_reg=0x%02x, val = 0x%02x", + soft_rst->access[i], soft_rst->access[i + 1]); + + ret = aw87xxx_dev_i2c_write_byte(aw_dev, soft_rst->access[i], + soft_rst->access[i + 1]); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "write failed,ret = %d,cnt=%d", + ret, i); + return; + } + } + AW_DEV_LOGD(aw_dev->dev, "down"); +} + + +int aw87xxx_dev_default_pwr_off(struct aw_device *aw_dev, + struct aw_data_container *profile_data) +{ + int ret = 0; + + AW_DEV_LOGD(aw_dev->dev, "enter"); + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGE(aw_dev->dev, "hwen is already off"); + return 0; + } + + if (aw_dev->soft_off_enable && profile_data) { + ret = aw87xxx_dev_reg_update(aw_dev, profile_data); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "update profile[Off] fw config failed"); + goto reg_off_update_failed; + } + } + + aw87xxx_dev_hw_pwr_ctrl(aw_dev, false); + AW_DEV_LOGD(aw_dev->dev, "down"); + return 0; + +reg_off_update_failed: + aw87xxx_dev_hw_pwr_ctrl(aw_dev, false); + return ret; +} + + +/************************************************************************ + * + * aw87xxx device power on process function + * + ************************************************************************/ + +int aw87xxx_dev_default_pwr_on(struct aw_device *aw_dev, + struct aw_data_container *profile_data) +{ + int ret = 0; + + /*hw power on*/ + aw87xxx_dev_hw_pwr_ctrl(aw_dev, true); + + ret = aw87xxx_dev_reg_update(aw_dev, profile_data); + if (ret < 0) + return ret; + + return 0; +} + +/**************************************************************************** + * + * aw87xxx chip esd status check + * + ****************************************************************************/ +int aw87xxx_dev_esd_reg_status_check(struct aw_device *aw_dev) +{ + int ret; + unsigned char reg_val = 0; + struct aw_esd_check_desc *esd_desc = &aw_dev->esd_desc; + + AW_DEV_LOGD(aw_dev->dev, "enter"); + + if (!esd_desc->first_update_reg_addr) { + AW_DEV_LOGE(aw_dev->dev, "esd check info if not init,please check"); + return -EINVAL; + } + + ret = aw87xxx_dev_i2c_read_byte(aw_dev, esd_desc->first_update_reg_addr, + ®_val); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "read reg 0x%02x failed", + esd_desc->first_update_reg_addr); + return ret; + } + + AW_DEV_LOGD(aw_dev->dev, "0x%02x:default val=0x%02x real val=0x%02x", + esd_desc->first_update_reg_addr, + esd_desc->first_update_reg_val, reg_val); + + if (reg_val == esd_desc->first_update_reg_val) { + AW_DEV_LOGE(aw_dev->dev, "reg status check failed"); + return -EINVAL; + } + return 0; +} + +int aw87xxx_dev_check_reg_is_rec_mode(struct aw_device *aw_dev) +{ + int ret; + unsigned char reg_val = 0; + struct aw_rec_mode_desc *rec_desc = &aw_dev->rec_desc; + + if (!rec_desc->addr) { + AW_DEV_LOGE(aw_dev->dev, "rec check info if not init,please check"); + return -EINVAL; + } + + ret = aw87xxx_dev_i2c_read_byte(aw_dev, rec_desc->addr, ®_val); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "read reg 0x%02x failed", + rec_desc->addr); + return ret; + } + + if (rec_desc->enable) { + if (reg_val & ~(rec_desc->mask)) { + AW_DEV_LOGI(aw_dev->dev, "reg status is receiver mode"); + aw_dev->is_rec_mode = AW_IS_REC_MODE; + } else { + aw_dev->is_rec_mode = AW_NOT_REC_MODE; + } + } else { + if (!(reg_val & ~(rec_desc->mask))) { + AW_DEV_LOGI(aw_dev->dev, "reg status is receiver mode"); + aw_dev->is_rec_mode = AW_IS_REC_MODE; + } else { + aw_dev->is_rec_mode = AW_NOT_REC_MODE; + } + } + return 0; +} + + +/**************************************************************************** + * + * aw87xxx product attributes init info + * + ****************************************************************************/ + +/********************** aw87xxx_pid_9A attributes ***************************/ + +static int aw_dev_pid_9b_reg_update(struct aw_device *aw_dev, + struct aw_data_container *profile_data) +{ + int i = 0; + int ret = -1; + uint8_t reg_val = 0; + + if (profile_data == NULL) + return -EINVAL; + + if (aw_dev->hwen_status == AW_DEV_HWEN_OFF) { + AW_DEV_LOGE(aw_dev->dev, "dev is pwr_off,can not update reg"); + return -EINVAL; + } + + if (profile_data->len != AW_PID_9B_BIN_REG_CFG_COUNT) { + AW_DEV_LOGE(aw_dev->dev, "reg_config count of bin is error,can not update reg"); + return -EINVAL; + } + ret = aw87xxx_dev_i2c_write_byte(aw_dev, AW87XXX_PID_9B_ENCRYPTION_REG, + AW87XXX_PID_9B_ENCRYPTION_BOOST_OUTPUT_SET); + if (ret < 0) + return ret; + + for (i = 1; i < AW_PID_9B_BIN_REG_CFG_COUNT; i++) { + AW_DEV_LOGI(aw_dev->dev, "reg=0x%02x, val = 0x%02x", + i, profile_data->data[i]); + reg_val = profile_data->data[i]; + if (i == AW87XXX_PID_9B_SYSCTRL_REG) { + aw87xxx_dev_reg_mute_bits_set(aw_dev, ®_val, true); + AW_DEV_LOGD(aw_dev->dev, "change mute_mask, val = 0x%02x", + reg_val); + } + + ret = aw87xxx_dev_i2c_write_byte(aw_dev, i, reg_val); + if (ret < 0) + return ret; + } + + return 0; +} + +static int aw_dev_pid_9b_pwr_on(struct aw_device *aw_dev, struct aw_data_container *data) +{ + int ret = 0; + + /*hw power on*/ + aw87xxx_dev_hw_pwr_ctrl(aw_dev, true); + + /* open the mute */ + ret = aw87xxx_dev_mute_ctrl(aw_dev, true); + if (ret < 0) + return ret; + + /* Update scene parameters in mute mode */ + ret = aw_dev_pid_9b_reg_update(aw_dev, data); + if (ret < 0) + return ret; + + /* close the mute */ + ret = aw87xxx_dev_mute_ctrl(aw_dev, false); + if (ret < 0) + return ret; + + return 0; +} + +static void aw_dev_pid_9b_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_9B_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_9b_reg_access; + + aw_dev->mute_desc.addr = AW87XXX_PID_9B_SYSCTRL_REG; + aw_dev->mute_desc.mask = AW87XXX_PID_9B_REG_EN_SW_MASK; + aw_dev->mute_desc.enable = AW87XXX_PID_9B_REG_EN_SW_DISABLE_VALUE; + aw_dev->mute_desc.disable = AW87XXX_PID_9B_REG_EN_SW_ENABLE_VALUE; + aw_dev->ops.pwr_on_func = aw_dev_pid_9b_pwr_on; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_9b_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_9b_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_DISENABLE; + + aw_dev->product_tab = g_aw_pid_9b_product; + aw_dev->product_cnt = AW87XXX_PID_9B_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_9B_SYSCTRL_REG; + aw_dev->rec_desc.disable = AW87XXX_PID_9B_SPK_MODE_ENABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_9B_SPK_MODE_DISABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_9B_SPK_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_9B_SYSCTRL_REG; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_9B_SYSCTRL_DEFAULT; +} + +static int aw_dev_pid_9a_init(struct aw_device *aw_dev) +{ + int ret = 0; + + ret = aw87xxx_dev_i2c_write_byte(aw_dev, AW87XXX_PID_9B_ENCRYPTION_REG, + AW87XXX_PID_9B_ENCRYPTION_BOOST_OUTPUT_SET); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "write 0x64=0x2C error"); + return -EINVAL; + } + + ret = aw87xxx_dev_get_chipid(aw_dev); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "read chipid is failed,ret=%d", ret); + return ret; + } + + if (aw_dev->chipid == AW_DEV_CHIPID_9B) { + AW_DEV_LOGI(aw_dev->dev, "product is pid_9B class"); + aw_dev_pid_9b_init(aw_dev); + } else { + AW_DEV_LOGE(aw_dev->dev, "product is not pid_9B class,not support"); + return -EINVAL; + } + + return 0; +} + +/********************** aw87xxx_pid_9b attributes end ***********************/ + +/********************** aw87xxx_pid_18 attributes ***************************/ +static int aw_dev_pid_18_pwr_on(struct aw_device *aw_dev, struct aw_data_container *data) +{ + int ret = 0; + + /*hw power on*/ + aw87xxx_dev_hw_pwr_ctrl(aw_dev, true); + + /* open the mute */ + ret = aw87xxx_dev_mute_ctrl(aw_dev, true); + if (ret < 0) + return ret; + + /* Update scene parameters in mute mode */ + ret = aw87xxx_dev_reg_update_mute(aw_dev, data); + if (ret < 0) + return ret; + + /* close the mute */ + ret = aw87xxx_dev_mute_ctrl(aw_dev, false); + if (ret < 0) + return ret; + + return 0; +} + +static void aw_dev_chipid_18_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_18_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_18_reg_access; + + aw_dev->mute_desc.addr = AW87XXX_PID_18_SYSCTRL_REG; + aw_dev->mute_desc.mask = AW87XXX_PID_18_REG_EN_SW_MASK; + aw_dev->mute_desc.enable = AW87XXX_PID_18_REG_EN_SW_DISABLE_VALUE; + aw_dev->mute_desc.disable = AW87XXX_PID_18_REG_EN_SW_ENABLE_VALUE; + aw_dev->ops.pwr_on_func = aw_dev_pid_18_pwr_on; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_18_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_18_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_18_product; + aw_dev->product_cnt = AW87XXX_PID_18_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_18_SYSCTRL_REG; + aw_dev->rec_desc.disable = AW87XXX_PID_18_REG_REC_MODE_DISABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_18_REG_REC_MODE_ENABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_18_REG_REC_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_18_CLASSD_REG; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_18_CLASSD_DEFAULT; +} +/********************** aw87xxx_pid_18 attributes end ***********************/ + +/********************** aw87xxx_pid_39 attributes ***************************/ +static void aw_dev_chipid_39_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_39_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_39_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_39_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_39_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_39_product; + aw_dev->product_cnt = AW87XXX_PID_39_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_39_REG_MODECTRL; + aw_dev->rec_desc.disable = AW87XXX_PID_39_REC_MODE_DISABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_39_REC_MODE_ENABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_39_REC_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_39_REG_MODECTRL; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_39_MODECTRL_DEFAULT; +} +/********************* aw87xxx_pid_39 attributes end *************************/ + + +/********************* aw87xxx_pid_59_5x9 attributes *************************/ +static void aw_dev_chipid_59_5x9_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_59_5X9_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_59_5x9_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_59_5x9_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_59_5x9_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_59_5x9_product; + aw_dev->product_cnt = AW87XXX_PID_59_5X9_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_59_5X9_REG_SYSCTRL; + aw_dev->rec_desc.disable = AW87XXX_PID_59_5X9_REC_MODE_DISABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_59_5X9_REC_MODE_ENABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_59_5X9_REC_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_59_5X9_REG_ENCR; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_59_5X9_ENCRY_DEFAULT; +} +/******************* aw87xxx_pid_59_5x9 attributes end ***********************/ + +/********************* aw87xxx_pid_59_3x9 attributes *************************/ +static void aw_dev_chipid_59_3x9_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_59_3X9_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_59_3x9_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_59_3x9_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_59_3x9_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_59_3x9_product; + aw_dev->product_cnt = AW87XXX_PID_59_3X9_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_59_3X9_REG_MDCRTL; + aw_dev->rec_desc.disable = AW87XXX_PID_59_3X9_SPK_MODE_ENABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_59_3X9_SPK_MODE_DISABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_59_3X9_SPK_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_59_3X9_REG_ENCR; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_59_3X9_ENCR_DEFAULT; +} +/******************* aw87xxx_pid_59_3x9 attributes end ***********************/ + +/********************** aw87xxx_pid_5a attributes ****************************/ +static void aw_dev_chipid_5a_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_5A_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_5a_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_5a_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_5a_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* Whether to allow register operation to power off */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_5a_product; + aw_dev->product_cnt = AW87XXX_PID_5A_PRODUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_5A_REG_SYSCTRL_REG; + aw_dev->rec_desc.disable = AW87XXX_PID_5A_REG_RCV_MODE_DISABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_5A_REG_RCV_MODE_ENABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_5A_REG_RCV_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_5A_REG_DFT3R_REG; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_5A_DFT3R_DEFAULT; +} +/********************** aw87xxx_pid_5a attributes end ************************/ + +/********************** aw87xxx_pid_76 attributes ****************************/ +static void aw_dev_chipid_76_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_76_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_76_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_76_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_76_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* software power off control info */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_76_product; + aw_dev->product_cnt = AW87XXX_PID_76_PROFUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_76_MDCTRL_REG; + aw_dev->rec_desc.disable = AW87XXX_PID_76_EN_SPK_ENABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_76_EN_SPK_DISABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_76_EN_SPK_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_76_DFT_ADP1_REG; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_76_DFT_ADP1_CHECK; +} +/********************** aw87xxx_pid_76 attributes end ************************/ + +/********************** aw87xxx_pid_60 attributes ****************************/ +static void aw_dev_chipid_60_init(struct aw_device *aw_dev) +{ + /* Product register permission info */ + aw_dev->reg_max_addr = AW87XXX_PID_60_REG_MAX; + aw_dev->reg_access = aw87xxx_pid_60_reg_access; + + /* software reset control info */ + aw_dev->soft_rst_desc.len = sizeof(aw87xxx_pid_60_softrst_access); + aw_dev->soft_rst_desc.access = aw87xxx_pid_60_softrst_access; + aw_dev->soft_rst_enable = AW_DEV_SOFT_RST_ENABLE; + + /* software power off control info */ + aw_dev->soft_off_enable = AW_DEV_SOFT_OFF_ENABLE; + + aw_dev->product_tab = g_aw_pid_60_product; + aw_dev->product_cnt = AW87XXX_PID_60_PROFUCT_MAX; + + aw_dev->rec_desc.addr = AW87XXX_PID_60_SYSCTRL_REG; + aw_dev->rec_desc.disable = AW87XXX_PID_60_RCV_MODE_DISABLE; + aw_dev->rec_desc.enable = AW87XXX_PID_60_RCV_MODE_ENABLE; + aw_dev->rec_desc.mask = AW87XXX_PID_60_RCV_MODE_MASK; + + /* esd reg info */ + aw_dev->esd_desc.first_update_reg_addr = AW87XXX_PID_60_NG3_REG; + aw_dev->esd_desc.first_update_reg_val = AW87XXX_PID_60_ESD_REG_VAL; +} +/********************** aw87xxx_pid_60 attributes end ************************/ + +static int aw_dev_chip_init(struct aw_device *aw_dev) +{ + int ret = 0; + + /*get info by chipid*/ + switch (aw_dev->chipid) { + case AW_DEV_CHIPID_9A: + ret = aw_dev_pid_9a_init(aw_dev); + if (ret < 0) + AW_DEV_LOGE(aw_dev->dev, "product is pid_9B init failed"); + break; + case AW_DEV_CHIPID_9B: + aw_dev_pid_9b_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_9B class"); + break; + case AW_DEV_CHIPID_18: + aw_dev_chipid_18_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_18 class"); + break; + case AW_DEV_CHIPID_39: + aw_dev_chipid_39_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_39 class"); + break; + case AW_DEV_CHIPID_59: + if (aw87xxx_dev_gpio_is_valid(aw_dev)) { + aw_dev_chipid_59_5x9_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_59_5x9 class"); + } else { + aw_dev_chipid_59_3x9_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_59_3x9 class"); + } + break; + case AW_DEV_CHIPID_5A: + aw_dev_chipid_5a_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_5A class"); + break; + case AW_DEV_CHIPID_76: + aw_dev_chipid_76_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_76 class"); + break; + case AW_DEV_CHIPID_60: + aw_dev_chipid_60_init(aw_dev); + AW_DEV_LOGI(aw_dev->dev, "product is pid_60 class"); + break; + default: + AW_DEV_LOGE(aw_dev->dev, "unsupported device revision [0x%x]", + aw_dev->chipid); + return -EINVAL; + } + + return 0; +} + +static int aw87xxx_dev_get_chipid(struct aw_device *aw_dev) +{ + int ret = -1; + unsigned int cnt = 0; + unsigned char reg_val = 0; + + for (cnt = 0; cnt < AW_READ_CHIPID_RETRIES; cnt++) { + ret = aw87xxx_dev_i2c_read_byte(aw_dev, AW_DEV_REG_CHIPID, ®_val); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "[%d] read chip is failed, ret=%d", + cnt, ret); + continue; + } + break; + } + + + if (cnt == AW_READ_CHIPID_RETRIES) { + AW_DEV_LOGE(aw_dev->dev, "read chip is failed,cnt=%d", cnt); + return -EINVAL; + } + + AW_DEV_LOGI(aw_dev->dev, "read chipid[0x%x] succeed", reg_val); + aw_dev->chipid = reg_val; + + return 0; +} + +int aw87xxx_dev_init(struct aw_device *aw_dev) +{ + int ret = -1; + + ret = aw87xxx_dev_get_chipid(aw_dev); + if (ret < 0) { + AW_DEV_LOGE(aw_dev->dev, "read chipid is failed,ret=%d", ret); + return ret; + } + + ret = aw_dev_chip_init(aw_dev); + + return ret; +} + + diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_device.h b/sound/soc/codecs/aw87xxx/aw87xxx_device.h new file mode 100644 index 000000000000..7c85f80a958e --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_device.h @@ -0,0 +1,149 @@ +#ifndef __AW87XXX_DEVICE_H__ +#define __AW87XXX_DEVICE_H__ +#include +#include +#include +#include +#include "aw87xxx_acf_bin.h" + +#define AW87XXX_PID_9B_PRODUCT_MAX (1) +#define AW87XXX_PID_18_PRODUCT_MAX (1) +#define AW87XXX_PID_39_PRODUCT_MAX (3) +#define AW87XXX_PID_59_3X9_PRODUCT_MAX (2) +#define AW87XXX_PID_59_5X9_PRODUCT_MAX (4) +#define AW87XXX_PID_5A_PRODUCT_MAX (5) +#define AW87XXX_PID_76_PROFUCT_MAX (4) +#define AW87XXX_PID_60_PROFUCT_MAX (5) +#define AW_PRODUCT_NAME_LEN (8) + +#define AW_GPIO_HIGHT_LEVEL (1) +#define AW_GPIO_LOW_LEVEL (0) + +#define AW_I2C_RETRIES (5) +#define AW_I2C_RETRY_DELAY (2) +#define AW_I2C_READ_MSG_NUM (2) + +#define AW_READ_CHIPID_RETRIES (5) +#define AW_READ_CHIPID_RETRY_DELAY (2) +#define AW_DEV_REG_CHIPID (0x00) + +#define AW_DEV_REG_INVALID_MASK (0xff) + +#define AW_NO_RESET_GPIO (-1) + +#define AW_PID_9B_BIN_REG_CFG_COUNT (10) + +/******************************************** + * + * aw87xxx devices attributes + * + *******************************************/ +struct aw_device; + +struct aw_device_ops { + int (*pwr_on_func)(struct aw_device *aw_dev, struct aw_data_container *data); + int (*pwr_off_func)(struct aw_device *aw_dev, struct aw_data_container *data); +}; + +enum aw_dev_chipid { + AW_DEV_CHIPID_18 = 0x18, + AW_DEV_CHIPID_39 = 0x39, + AW_DEV_CHIPID_59 = 0x59, + AW_DEV_CHIPID_69 = 0x69, + AW_DEV_CHIPID_5A = 0x5A, + AW_DEV_CHIPID_9A = 0x9A, + AW_DEV_CHIPID_9B = 0x9B, + AW_DEV_CHIPID_76 = 0x76, + AW_DEV_CHIPID_60 = 0x60, +}; + +enum aw_dev_hw_status { + AW_DEV_HWEN_OFF = 0, + AW_DEV_HWEN_ON, + AW_DEV_HWEN_INVALID, + AW_DEV_HWEN_STATUS_MAX, +}; + +enum aw_dev_soft_off_enable { + AW_DEV_SOFT_OFF_DISENABLE = 0, + AW_DEV_SOFT_OFF_ENABLE = 1, +}; + +enum aw_dev_soft_rst_enable { + AW_DEV_SOFT_RST_DISENABLE = 0, + AW_DEV_SOFT_RST_ENABLE = 1, +}; + +enum aw_reg_receiver_mode { + AW_NOT_REC_MODE = 0, + AW_IS_REC_MODE = 1, +}; + +struct aw_mute_desc { + uint8_t addr; + uint8_t enable; + uint8_t disable; + uint16_t mask; +}; + +struct aw_soft_rst_desc { + int len; + unsigned char *access; +}; + +struct aw_esd_check_desc { + uint8_t first_update_reg_addr; + uint8_t first_update_reg_val; +}; + +struct aw_rec_mode_desc { + uint8_t addr; + uint8_t enable; + uint8_t disable; + uint8_t mask; +}; + +struct aw_device { + uint8_t i2c_addr; + uint8_t chipid; + uint8_t soft_rst_enable; + uint8_t soft_off_enable; + uint8_t is_rec_mode; + int hwen_status; + int i2c_bus; + int rst_gpio; + int reg_max_addr; + int product_cnt; + const char **product_tab; + const unsigned char *reg_access; + + struct device *dev; + struct i2c_client *i2c; + struct aw_mute_desc mute_desc; + struct aw_soft_rst_desc soft_rst_desc; + struct aw_esd_check_desc esd_desc; + struct aw_rec_mode_desc rec_desc; + + struct aw_device_ops ops; +}; + + +int aw87xxx_dev_i2c_write_byte(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t reg_data); +int aw87xxx_dev_i2c_read_byte(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t *reg_data); +int aw87xxx_dev_i2c_read_msg(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t *data_buf, uint32_t data_len); +int aw87xxx_dev_i2c_write_bits(struct aw_device *aw_dev, + uint8_t reg_addr, uint8_t mask, uint8_t reg_data); +void aw87xxx_dev_soft_reset(struct aw_device *aw_dev); +void aw87xxx_dev_hw_pwr_ctrl(struct aw_device *aw_dev, bool enable); +int aw87xxx_dev_default_pwr_on(struct aw_device *aw_dev, + struct aw_data_container *profile_data); +int aw87xxx_dev_default_pwr_off(struct aw_device *aw_dev, + struct aw_data_container *profile_data); +int aw87xxx_dev_esd_reg_status_check(struct aw_device *aw_dev); +int aw87xxx_dev_check_reg_is_rec_mode(struct aw_device *aw_dev); +int aw87xxx_dev_init(struct aw_device *aw_dev); + +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_dsp.c b/sound/soc/codecs/aw87xxx/aw87xxx_dsp.c new file mode 100644 index 000000000000..93b02e30122d --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_dsp.c @@ -0,0 +1,355 @@ +/* + * aw87xxx_dsp.c + * + * Copyright (c) 2021 AWINIC Technology CO., LTD + * + * Author: Barry + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx_log.h" +#include "aw87xxx_dsp.h" + +static DEFINE_MUTEX(g_dsp_lock); +static unsigned int g_spin_value = 0; + +static int g_rx_topo_id = AW_RX_DEFAULT_TOPO_ID; +static int g_rx_port_id = AW_RX_DEFAULT_PORT_ID; + +#ifdef AW_MTK_OPEN_DSP_PLATFORM +extern int mtk_spk_send_ipi_buf_to_dsp(void *data_buffer, + uint32_t data_size); +extern int mtk_spk_recv_ipi_buf_from_dsp(int8_t *buffer, + int16_t size, uint32_t *buf_len); +/* +static int mtk_spk_send_ipi_buf_to_dsp(void *data_buffer, + uint32_t data_size) +{ + AW_LOGI("enter"); + return 0; +} + +static int mtk_spk_recv_ipi_buf_from_dsp(int8_t *buffer, + int16_t size, uint32_t *buf_len) +{ + AW_LOGI("enter"); + return 0; +} +*/ +#elif defined AW_QCOM_OPEN_DSP_PLATFORM +extern int afe_get_topology(int port_id); +extern int aw_send_afe_cal_apr(uint32_t param_id, + void *buf, int cmd_size, bool write); +/* +static int afe_get_topology(int port_id) +{ + return -EPERM; +} + +static int aw_send_afe_cal_apr(uint32_t param_id, + void *buf, int cmd_size, bool write) +{ + AW_LOGI("enter, no define AWINIC_ADSP_ENABLE", __func__); + return 0; +} +*/ +#endif + +#ifdef AW_QCOM_OPEN_DSP_PLATFORM +extern void aw_set_port_id(int rx_port_id); +#else +static void aw_set_port_id(int rx_port_id) +{ + return; +} +#endif + +uint8_t aw87xxx_dsp_isEnable(void) +{ +#if (defined AW_QCOM_OPEN_DSP_PLATFORM) || (defined AW_MTK_OPEN_DSP_PLATFORM) + return true; +#else + return false; +#endif +} + +/*****************mtk dsp communication function start**********************/ +#ifdef AW_MTK_OPEN_DSP_PLATFORM +static int aw_mtk_write_data_to_dsp(int32_t param_id, + void *data, int size) +{ + int32_t *dsp_data = NULL; + mtk_dsp_hdr_t *hdr = NULL; + int ret; + + dsp_data = kzalloc(sizeof(mtk_dsp_hdr_t) + size, GFP_KERNEL); + if (!dsp_data) { + AW_LOGE("kzalloc dsp_msg error"); + return -ENOMEM; + } + + hdr = (mtk_dsp_hdr_t *)dsp_data; + hdr->type = DSP_MSG_TYPE_DATA; + hdr->opcode_id = param_id; + hdr->version = AW_DSP_MSG_HDR_VER; + + memcpy(((char *)dsp_data) + sizeof(mtk_dsp_hdr_t), + data, size); + + ret = mtk_spk_send_ipi_buf_to_dsp(dsp_data, + sizeof(mtk_dsp_hdr_t) + size); + if (ret < 0) { + AW_LOGE("write data failed"); + kfree(dsp_data); + dsp_data = NULL; + return ret; + } + + kfree(dsp_data); + dsp_data = NULL; + return 0; +} + +static int aw_mtk_read_data_from_dsp(int32_t param_id, void *data, + int data_size) +{ + int ret; + mtk_dsp_hdr_t hdr; + + mutex_lock(&g_dsp_lock); + hdr.type = DSP_MSG_TYPE_CMD; + hdr.opcode_id = param_id; + hdr.version = AW_DSP_MSG_HDR_VER; + + ret = mtk_spk_send_ipi_buf_to_dsp(&hdr, sizeof(mtk_dsp_hdr_t)); + if (ret < 0) + goto failed; + + ret = mtk_spk_recv_ipi_buf_from_dsp(data, data_size, &data_size); + if (ret < 0) + goto failed; + + mutex_unlock(&g_dsp_lock); + return 0; + +failed: + mutex_unlock(&g_dsp_lock); + return ret; +} + +#endif +/********************mtk dsp communication function end***********************/ + +/******************qcom dsp communication function start**********************/ +#ifdef AW_QCOM_OPEN_DSP_PLATFORM +static void aw_check_dsp_ready(void) +{ + int ret; + + ret = afe_get_topology(g_rx_port_id); + AW_LOGD("topo_id 0x%x", ret); + + if (ret != g_rx_topo_id) + AW_LOGE("topo id 0x%x", ret); + +} + +static int aw_qcom_write_data_to_dsp(int32_t param_id, + void *data, int data_size) +{ + int ret = 0; + + AW_LOGI("enter"); + mutex_lock(&g_dsp_lock); + aw_check_dsp_ready(); + ret = aw_send_afe_cal_apr(param_id, data, + data_size, true); + mutex_unlock(&g_dsp_lock); + return ret; +} + +static int aw_qcom_read_data_from_dsp(int32_t param_id, + void *data, int data_size) +{ + int ret = 0; + + AW_LOGI("enter"); + mutex_lock(&g_dsp_lock); + aw_check_dsp_ready(); + ret = aw_send_afe_cal_apr(param_id, data, + data_size, false); + mutex_unlock(&g_dsp_lock); + return ret; +} + +#endif +/*****************qcom dsp communication function end*********************/ + +/*****************read/write msg communication function*********************/ +static int aw_write_data_to_dsp(int32_t param_id, void *data, int data_size) +{ +#if defined AW_QCOM_OPEN_DSP_PLATFORM + return aw_qcom_write_data_to_dsp(param_id, data, data_size); +#elif defined AW_MTK_OPEN_DSP_PLATFORM + return aw_mtk_write_data_to_dsp(param_id, data, data_size); +#else + return -EINVAL; +#endif +} + +static int aw_read_data_from_dsp(int32_t param_id, void *data, int data_size) +{ +#if defined AW_QCOM_OPEN_DSP_PLATFORM + return aw_qcom_read_data_from_dsp(param_id, data, data_size); +#elif defined AW_MTK_OPEN_DSP_PLATFORM + return aw_mtk_read_data_from_dsp(param_id, data, data_size); +#else + return -EINVAL; +#endif +} + +/***************read/write msg communication function end*******************/ + +int aw87xxx_dsp_get_rx_module_enable(int *enable) +{ + if (!enable) { + AW_LOGE("enable is NULL"); + return -EINVAL; + } + + return aw_read_data_from_dsp(AWDSP_RX_SET_ENABLE, + (void *)enable, sizeof(uint32_t)); +} + +int aw87xxx_dsp_set_rx_module_enable(int enable) +{ + switch (enable) { + case AW_RX_MODULE_DISENABLE: + AW_LOGD("set enable=%d", enable); + break; + case AW_RX_MODULE_ENABLE: + AW_LOGD("set enable=%d", enable); + break; + default: + AW_LOGE("unsupport enable=%d", enable); + return -EINVAL; + } + + return aw_write_data_to_dsp(AWDSP_RX_SET_ENABLE, + &enable, sizeof(uint32_t)); +} + + +int aw87xxx_dsp_get_vmax(uint32_t *vmax, int dev_index) +{ + int32_t param_id = 0; + + switch (dev_index % AW_DSP_CHANNEL_MAX) { + case AW_DSP_CHANNEL_0: + param_id = AWDSP_RX_VMAX_0; + break; + case AW_DSP_CHANNEL_1: + param_id = AWDSP_RX_VMAX_1; + break; + default: + AW_LOGE("algo only support double PA channel:%d unsupport", + dev_index); + return -EINVAL; + } + + return aw_read_data_from_dsp(param_id, + (void *)vmax, sizeof(uint32_t)); +} + +int aw87xxx_dsp_set_vmax(uint32_t vmax, int dev_index) +{ + int32_t param_id = 0; + + switch (dev_index % AW_DSP_CHANNEL_MAX) { + case AW_DSP_CHANNEL_0: + param_id = AWDSP_RX_VMAX_0; + break; + case AW_DSP_CHANNEL_1: + param_id = AWDSP_RX_VMAX_1; + break; + default: + AW_LOGE("algo only support double PA channel:%d unsupport", + dev_index); + return -EINVAL; + } + + return aw_write_data_to_dsp(param_id, &vmax, sizeof(uint32_t)); +} + +int aw87xxx_dsp_set_spin(uint32_t ctrl_value) +{ + int ret = 0; + + if (ctrl_value >= AW_SPIN_MAX) { + AW_LOGE("spin [%d] unsupported ", ctrl_value); + return -EINVAL; + } + ret = aw_write_data_to_dsp(AW_MSG_ID_SPIN, &ctrl_value, + sizeof(uint32_t)); + if (ret) { + AW_LOGE("spin [%d] set failed ", ctrl_value); + return ret; + } + + g_spin_value = ctrl_value; + return 0; +} + +int aw87xxx_dsp_get_spin(void) +{ + return g_spin_value; +} + +int aw87xxx_spin_set_record_val(void) +{ + AW_LOGD("record write spin enter"); + + return aw87xxx_dsp_set_spin(g_spin_value); +} +EXPORT_SYMBOL(aw87xxx_spin_set_record_val); + +void aw87xxx_device_parse_topo_id_dt(struct aw_device *aw_dev) +{ + int ret; + + ret = of_property_read_u32(aw_dev->dev->of_node, "aw-rx-topo-id", &g_rx_topo_id); + if (ret < 0) { + g_rx_topo_id = AW_RX_DEFAULT_TOPO_ID; + AW_DEV_LOGI(aw_dev->dev, "read aw-rx-topo-id failed,use default"); + } + + AW_DEV_LOGI(aw_dev->dev, "rx-topo-id: 0x%x", g_rx_topo_id); +} + +void aw87xxx_device_parse_port_id_dt(struct aw_device *aw_dev) +{ + int ret; + + ret = of_property_read_u32(aw_dev->dev->of_node, "aw-rx-port-id", &g_rx_port_id); + if (ret < 0) { + g_rx_port_id = AW_RX_DEFAULT_PORT_ID; + AW_DEV_LOGI(aw_dev->dev, "read aw-rx-port-id failed,use default"); + } + + aw_set_port_id(g_rx_port_id); + AW_DEV_LOGI(aw_dev->dev, "rx-port-id: 0x%x", g_rx_port_id); +} + diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_dsp.h b/sound/soc/codecs/aw87xxx/aw87xxx_dsp.h new file mode 100644 index 000000000000..7acc4dc0dfd9 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_dsp.h @@ -0,0 +1,65 @@ +#ifndef __AW87XXX_DSP_H__ +#define __AW87XXX_DSP_H__ + +#include "aw87xxx_device.h" + +/*#define AW_MTK_OPEN_DSP_PLATFORM*/ +/*#define AW_QCOM_OPEN_DSP_PLATFORM*/ + +/*Note: The pord_ID is configured according to different platforms*/ +#define AW_DSP_SLEEP_TIME (10) + +#define AW_DSP_MSG_HDR_VER (1) + +#define AW_RX_DEFAULT_TOPO_ID (0x1000FF01) +#define AW_RX_DEFAULT_PORT_ID (0x4000) + +#define AWDSP_RX_SET_ENABLE (0x10013D11) +#define AWDSP_RX_PARAMS (0x10013D12) +#define AWDSP_RX_VMAX_0 (0X10013D17) +#define AWDSP_RX_VMAX_1 (0X10013D18) +#define AW_MSG_ID_SPIN (0x10013D2E) + +enum { + AW_SPIN_0 = 0, + AW_SPIN_90, + AW_SPIN_180, + AW_SPIN_270, + AW_SPIN_MAX, +}; + +typedef struct mtk_dsp_msg_header { + int32_t type; + int32_t opcode_id; + int32_t version; + int32_t reserver[3]; +} mtk_dsp_hdr_t; + +enum aw_rx_module_enable { + AW_RX_MODULE_DISENABLE = 0, + AW_RX_MODULE_ENABLE, +}; + +enum aw_dsp_msg_type { + DSP_MSG_TYPE_DATA = 0, + DSP_MSG_TYPE_CMD = 1, +}; + +enum aw_dsp_channel { + AW_DSP_CHANNEL_0 = 0, + AW_DSP_CHANNEL_1, + AW_DSP_CHANNEL_MAX, +}; + +uint8_t aw87xxx_dsp_isEnable(void); +int aw87xxx_dsp_get_rx_module_enable(int *enable); +int aw87xxx_dsp_set_rx_module_enable(int enable); +int aw87xxx_dsp_get_vmax(uint32_t *vmax, int channel); +int aw87xxx_dsp_set_vmax(uint32_t vmax, int channel); +int aw87xxx_dsp_set_spin(uint32_t ctrl_value); +int aw87xxx_dsp_get_spin(void); +int aw87xxx_spin_set_record_val(void); +void aw87xxx_device_parse_port_id_dt(struct aw_device *aw_dev); +void aw87xxx_device_parse_topo_id_dt(struct aw_device *aw_dev); + +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_log.h b/sound/soc/codecs/aw87xxx/aw87xxx_log.h new file mode 100644 index 000000000000..b3bde38a23c6 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_log.h @@ -0,0 +1,33 @@ +#ifndef __AW87XXX_LOG_H__ +#define __AW87XXX_LOG_H__ + +#include + + +/******************************************** + * + * print information control + * + *******************************************/ +#define AW_LOGI(fmt, ...)\ + pr_info("[Awinic] %s:" fmt "\n", __func__, ##__VA_ARGS__) + +#define AW_LOGD(fmt, ...)\ + pr_debug("[Awinic] %s:" fmt "\n", __func__, ##__VA_ARGS__) + +#define AW_LOGE(fmt, ...)\ + pr_err("[Awinic] %s:" fmt "\n", __func__, ##__VA_ARGS__) + + +#define AW_DEV_LOGI(dev, fmt, ...)\ + pr_info("[Awinic] [%s]%s: " fmt "\n", dev_name(dev), __func__, ##__VA_ARGS__) + +#define AW_DEV_LOGD(dev, fmt, ...)\ + pr_debug("[Awinic] [%s]%s: " fmt "\n", dev_name(dev), __func__, ##__VA_ARGS__) + +#define AW_DEV_LOGE(dev, fmt, ...)\ + pr_err("[Awinic] [%s]%s: " fmt "\n", dev_name(dev), __func__, ##__VA_ARGS__) + + + +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_monitor.c b/sound/soc/codecs/aw87xxx/aw87xxx_monitor.c new file mode 100644 index 000000000000..f580506b2786 --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_monitor.c @@ -0,0 +1,1208 @@ +/* + * aw87xxx_monitor.c + * + * Copyright (c) 2021 AWINIC Technology CO., LTD + * + * Author: Barry + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "aw87xxx.h" +#include "aw87xxx_log.h" +#include "aw87xxx_monitor.h" +#include "aw87xxx_dsp.h" +#include "aw87xxx_bin_parse.h" +#include "aw87xxx_device.h" + +#define AW_MONITOT_BIN_PARSE_VERSION "V0.1.0" + +#define AW_GET_32_DATA(w, x, y, z) \ + ((uint32_t)((((uint8_t)w) << 24) | (((uint8_t)x) << 16) | \ + (((uint8_t)y) << 8) | ((uint8_t)z))) + +/**************************************************************************** + * + * aw87xxx monitor bin check + * + ****************************************************************************/ +static int aw_monitor_check_header_v_1_0_0(struct device *dev, + char *data, uint32_t data_len) +{ + int i = 0; + struct aw_bin_header *header = (struct aw_bin_header *)data; + + if (header->bin_data_type != DATA_TYPE_MONITOR_ANALOG) { + AW_DEV_LOGE(dev, "monitor data_type check error!"); + return -EINVAL; + } + + if (header->bin_data_size != AW_MONITOR_HDR_DATA_SIZE) { + AW_DEV_LOGE(dev, "monitor data_size error!"); + return -EINVAL; + } + + if (header->data_byte_len != AW_MONITOR_HDR_DATA_BYTE_LEN) { + AW_DEV_LOGE(dev, "monitor data_byte_len error!"); + return -EINVAL; + } + + for (i = 0; i < AW_MONITOR_DATA_VER_MAX; i++) { + if (header->bin_data_ver == i) { + AW_LOGD("monitor bin_data_ver[0x%x]", i); + break; + } + } + if (i == AW_MONITOR_DATA_VER_MAX) + return -EINVAL; + + return 0; +} + +static int aw_monitor_check_data_v1_size(struct device *dev, + char *data, int32_t data_len) +{ + int32_t bin_header_len = sizeof(struct aw_bin_header); + int32_t monitor_header_len = sizeof(struct aw_monitor_header); + int32_t monitor_data_len = sizeof(struct vmax_step_config); + int32_t len = 0; + struct aw_monitor_header *monitor_header = NULL; + + AW_DEV_LOGD(dev, "enter"); + + if (data_len < bin_header_len + monitor_header_len) { + AW_DEV_LOGE(dev, "bin len is less than aw_bin_header and monitoor_header,check failed"); + return -EINVAL; + } + + monitor_header = (struct aw_monitor_header *)(data + bin_header_len); + len = data_len - bin_header_len - monitor_header_len; + if (len < monitor_header->step_count * monitor_data_len) { + AW_DEV_LOGE(dev, "bin data len is not enough,check failed"); + return -EINVAL; + } + + AW_DEV_LOGD(dev, "succeed"); + + return 0; +} + +static int aw_monitor_check_data_size(struct device *dev, + char *data, int32_t data_len) +{ + int ret = -1; + struct aw_bin_header *header = (struct aw_bin_header *)data; + + switch (header->bin_data_ver) { + case AW_MONITOR_DATA_VER: + ret = aw_monitor_check_data_v1_size(dev, data, data_len); + if (ret < 0) + return ret; + break; + default: + AW_DEV_LOGE(dev, "bin data_ver[0x%x] non support", + header->bin_data_ver); + return -EINVAL; + } + + return 0; +} + + +static int aw_monitor_check_bin_header(struct device *dev, + char *data, int32_t data_len) +{ + int ret = -1; + struct aw_bin_header *header = NULL; + + if (data_len < sizeof(struct aw_bin_header)) { + AW_DEV_LOGE(dev, "bin len is less than aw_bin_header,check failed"); + return -EINVAL; + } + header = (struct aw_bin_header *)data; + + switch (header->header_ver) { + case HEADER_VERSION_1_0_0: + ret = aw_monitor_check_header_v_1_0_0(dev, data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "monitor bin haeder info check error!"); + return ret; + } + break; + default: + AW_DEV_LOGE(dev, "bin version[0x%x] non support", + header->header_ver); + return -EINVAL; + } + + return 0; +} + +static int aw_monitor_bin_check_sum(struct device *dev, + char *data, int32_t data_len) +{ + int i, data_sum = 0; + uint32_t *check_sum = (uint32_t *)data; + + for (i = 4; i < data_len; i++) + data_sum += data[i]; + + if (*check_sum != data_sum) { + AW_DEV_LOGE(dev, "check_sum[%d] is not equal to data_sum[%d]", + *check_sum, data_sum); + return -ENOMEM; + } + + AW_DEV_LOGD(dev, "succeed"); + + return 0; +} + +static int aw_monitor_bin_check(struct device *dev, + char *monitor_data, uint32_t data_len) +{ + int ret = -1; + + if (monitor_data == NULL || data_len == 0) { + AW_DEV_LOGE(dev, "none data to parse"); + return -EINVAL; + } + + ret = aw_monitor_bin_check_sum(dev, monitor_data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "bin data check sum failed"); + return ret; + } + + ret = aw_monitor_check_bin_header(dev, monitor_data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "bin data len check failed"); + return ret; + } + + ret = aw_monitor_check_data_size(dev, monitor_data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "bin header info check failed"); + return ret; + } + + return 0; +} + +/***************************************************************************** + * + * aw87xxx monitor header bin parse + * + *****************************************************************************/ +static void aw_monitor_write_to_table_v1(struct device *dev, + struct vmax_step_config *vmax_step, + char *vmax_data, uint32_t step_count) +{ + int i = 0; + int index = 0; + int vmax_step_size = (int)sizeof(struct vmax_step_config); + + for (i = 0; i < step_count; i++) { + index = vmax_step_size * i; + vmax_step[i].vbat_min = + AW_GET_32_DATA(vmax_data[index + 3], + vmax_data[index + 2], + vmax_data[index + 1], + vmax_data[index + 0]); + vmax_step[i].vbat_max = + AW_GET_32_DATA(vmax_data[index + 7], + vmax_data[index + 6], + vmax_data[index + 5], + vmax_data[index + 4]); + vmax_step[i].vmax_vol = + AW_GET_32_DATA(vmax_data[index + 11], + vmax_data[index + 10], + vmax_data[index + 9], + vmax_data[index + 8]); + } + + for (i = 0; i < step_count; i++) + AW_DEV_LOGI(dev, "vbat_min:%d, vbat_max%d, vmax_vol:0x%x", + vmax_step[i].vbat_min, + vmax_step[i].vbat_max, + vmax_step[i].vmax_vol); +} + +static int aw_monitor_parse_vol_data_v1(struct device *dev, + struct aw_monitor *monitor, char *monitor_data) +{ + uint32_t step_count = 0; + char *vmax_data = NULL; + struct vmax_step_config *vmax_step = NULL; + + AW_DEV_LOGD(dev, "enter"); + + step_count = monitor->monitor_hdr.step_count; + if (step_count) { + vmax_step = devm_kzalloc(dev, sizeof(struct vmax_step_config) * step_count, + GFP_KERNEL); + if (vmax_step == NULL) { + AW_DEV_LOGE(dev, "vmax_cfg vmalloc failed"); + return -ENOMEM; + } + memset(vmax_step, 0, + sizeof(struct vmax_step_config) * step_count); + } + + vmax_data = monitor_data + sizeof(struct aw_bin_header) + + sizeof(struct aw_monitor_header); + aw_monitor_write_to_table_v1(dev, vmax_step, vmax_data, step_count); + monitor->vmax_cfg = vmax_step; + + AW_DEV_LOGI(dev, "vmax_data parse succeed"); + + return 0; +} + +static int aw_monitor_parse_data_v1(struct device *dev, + struct aw_monitor *monitor, char *monitor_data) +{ + int ret = -1; + int header_len = 0; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + header_len = sizeof(struct aw_bin_header); + memcpy(monitor_hdr, monitor_data + header_len, + sizeof(struct aw_monitor_header)); + + AW_DEV_LOGI(dev, "monitor_switch:%d, monitor_time:%d (ms), monitor_count:%d, step_count:%d", + monitor_hdr->monitor_switch, monitor_hdr->monitor_time, + monitor_hdr->monitor_count, monitor_hdr->step_count); + + ret = aw_monitor_parse_vol_data_v1(dev, monitor, monitor_data); + if (ret < 0) { + AW_DEV_LOGE(dev, "vmax_data parse failed"); + return ret; + } + + monitor->bin_status = AW_MONITOR_CFG_OK; + + return 0; +} + + +static int aw_monitor_parse_v_1_0_0(struct device *dev, + struct aw_monitor *monitor, char *monitor_data) +{ + int ret = -1; + struct aw_bin_header *header = (struct aw_bin_header *)monitor_data; + + switch (header->bin_data_ver) { + case AW_MONITOR_DATA_VER: + ret = aw_monitor_parse_data_v1(dev, monitor, monitor_data); + if (ret < 0) + return ret; + break; + default: + return -EINVAL; + } + + return 0; +} + +void aw87xxx_monitor_cfg_free(struct aw_monitor *monitor) +{ + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + + monitor->bin_status = AW_MONITOR_CFG_WAIT; + memset(&monitor->monitor_hdr, 0, + sizeof(struct aw_monitor_header)); + if (monitor->vmax_cfg) { + devm_kfree(aw87xxx->dev, monitor->vmax_cfg); + monitor->vmax_cfg = NULL; + } +} + +int aw87xxx_monitor_bin_parse(struct device *dev, + char *monitor_data, uint32_t data_len) +{ + int ret = -1; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = NULL; + struct aw_bin_header *bin_header = NULL; + + if (aw87xxx == NULL) { + AW_DEV_LOGE(dev, "get struct aw87xxx failed"); + return -EINVAL; + } + + monitor = &aw87xxx->monitor; + monitor->bin_status = AW_MONITOR_CFG_WAIT; + + AW_DEV_LOGI(dev, "monitor bin parse version: %s", + AW_MONITOT_BIN_PARSE_VERSION); + + ret = aw_monitor_bin_check(dev, monitor_data, data_len); + if (ret < 0) { + AW_DEV_LOGE(dev, "monitor bin check failed"); + return ret; + } + + bin_header = (struct aw_bin_header *)monitor_data; + switch (bin_header->bin_data_ver) { + case DATA_VERSION_V1: + ret = aw_monitor_parse_v_1_0_0(dev, monitor, + monitor_data); + if (ret < 0) { + aw87xxx_monitor_cfg_free(monitor); + return ret; + } + break; + default: + AW_DEV_LOGE(dev, "Unrecognized this bin data version[0x%x]", + bin_header->bin_data_ver); + } + + return 0; +} + +/*************************************************************************** + * + * aw87xxx monitor get adjustment vmax of power + * + ***************************************************************************/ +static int aw_monitor_get_battery_capacity(struct device *dev, + struct aw_monitor *monitor, int *vbat_capacity) +{ + char name[] = "battery"; + int ret = -1; + union power_supply_propval prop = { 0 }; + struct power_supply *psy = NULL; + + psy = power_supply_get_by_name(name); + if (psy == NULL) { + AW_DEV_LOGE(dev, "no struct power supply name:%s", name); + return -EINVAL; + } + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_CAPACITY, &prop); + if (ret < 0) { + AW_DEV_LOGE(dev, "get vbat capacity failed"); + return -EINVAL; + } + *vbat_capacity = prop.intval; + AW_DEV_LOGI(dev, "The percentage is %d", + *vbat_capacity); + + return 0; +} + +static int aw_search_vmax_from_table(struct device *dev, + struct aw_monitor *monitor, + const int vbat_vol, int *vmax_vol) +{ + int i = 0; + int vmax_set = 0; + uint32_t vmax_flag = 0; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + struct vmax_step_config *vmax_cfg = monitor->vmax_cfg; + + if (monitor->bin_status == AW_MONITOR_CFG_WAIT) { + AW_DEV_LOGE(dev, "vmax_cfg not loaded or parse failed"); + return -ENODATA; + } + + for (i = 0; i < monitor_hdr->step_count; i++) { + if (vbat_vol == AW_VBAT_MAX) { + vmax_set = AW_VMAX_MAX; + vmax_flag = 1; + AW_DEV_LOGD(dev, "vbat=%d, setting vmax=0x%x", + vbat_vol, vmax_set); + break; + } + + if (vbat_vol >= vmax_cfg[i].vbat_min && + vbat_vol < vmax_cfg[i].vbat_max) { + vmax_set = vmax_cfg[i].vmax_vol; + vmax_flag = 1; + AW_DEV_LOGD(dev, "read setting vmax=0x%x, step[%d]: vbat_min=%d,vbat_max=%d", + vmax_set, i, + vmax_cfg[i].vbat_min, + vmax_cfg[i].vbat_max); + break; + } + } + + if (!vmax_flag) { + AW_DEV_LOGE(dev, "vmax_cfg not found"); + return -ENODATA; + } + + *vmax_vol = vmax_set; + return 0; +} + + +/*************************************************************************** + * + *monitor_esd_func + * + ***************************************************************************/ +static int aw_chip_status_recover(struct aw87xxx *aw87xxx) +{ + int ret = -1; + struct aw_monitor *monitor = &aw87xxx->monitor; + char *profile = aw87xxx->current_profile; + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + + ret = aw87xxx_update_profile_esd(aw87xxx, profile); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "load profile[%s] failed ", + profile); + return ret; + } + + AW_DEV_LOGI(aw87xxx->dev, "current prof[%s], dev_index[%d] ", + profile, aw87xxx->dev_index); + + monitor->pre_vmax = AW_VMAX_INIT_VAL; + monitor->first_entry = AW_FIRST_ENTRY; + monitor->timer_cnt = 0; + monitor->vbat_sum = 0; + + return 0; +} + +static int aw_monitor_chip_esd_check_work(struct aw87xxx *aw87xxx) +{ + int ret = 0; + int i = 0; + + for (i = 0; i < REG_STATUS_CHECK_MAX; i++) { + AW_DEV_LOGD(aw87xxx->dev, "reg_status_check[%d]", i); + + ret = aw87xxx_dev_esd_reg_status_check(&aw87xxx->aw_dev); + if (ret < 0) { + aw_chip_status_recover(aw87xxx); + } else { + AW_DEV_LOGD(aw87xxx->dev, "chip status check succeed"); + break; + } + msleep(AW_ESD_CHECK_DELAY); + } + + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "chip status recover failed,chip off"); + aw87xxx_update_profile_esd(aw87xxx, aw87xxx->prof_off_name); + return ret; + } + + return 0; +} + + +/*************************************************************************** + * + * aw87xxx monitor work with dsp + * + ***************************************************************************/ +static int aw_monitor_update_vmax_to_dsp(struct device *dev, + struct aw_monitor *monitor, int vmax_set) +{ + int ret = -1; + uint32_t enable = 0; + + if (monitor->pre_vmax != vmax_set) { + ret = aw87xxx_dsp_get_rx_module_enable(&enable); + if (!enable || ret < 0) { + AW_DEV_LOGE(dev, "get rx failed or rx disable, ret=%d, enable=%d", + ret, enable); + return -EPERM; + } + + ret = aw87xxx_dsp_set_vmax(vmax_set, monitor->dev_index); + if (ret) { + AW_DEV_LOGE(dev, "set dsp msg fail, ret=%d", ret); + return ret; + } + + AW_DEV_LOGI(dev, "set dsp vmax=0x%x sucess", vmax_set); + monitor->pre_vmax = vmax_set; + } else { + AW_DEV_LOGI(dev, "vmax=0x%x no change", vmax_set); + } + + return 0; +} + +static void aw_monitor_with_dsp_vmax_work(struct device *dev, + struct aw_monitor *monitor) +{ + int ret = -1; + int vmax_set = 0; + int vbat_capacity = 0; + int ave_capacity = 0; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + AW_DEV_LOGD(dev, "enter with dsp monitor"); + + ret = aw_monitor_get_battery_capacity(dev, monitor, &vbat_capacity); + if (ret < 0) + return; + + if (monitor->timer_cnt < monitor_hdr->monitor_count) { + monitor->timer_cnt++; + monitor->vbat_sum += vbat_capacity; + AW_DEV_LOGI(dev, "timer_cnt = %d", + monitor->timer_cnt); + } + if ((monitor->timer_cnt >= monitor_hdr->monitor_count) || + (monitor->first_entry == AW_FIRST_ENTRY)) { + if (monitor->first_entry == AW_FIRST_ENTRY) + monitor->first_entry = AW_NOT_FIRST_ENTRY; + ave_capacity = monitor->vbat_sum / monitor->timer_cnt; + + if (monitor->custom_capacity) + ave_capacity = monitor->custom_capacity; + + AW_DEV_LOGI(dev, "get average capacity = %d", ave_capacity); + + ret = aw_search_vmax_from_table(dev, monitor, + ave_capacity, &vmax_set); + if (ret < 0) + AW_DEV_LOGE(dev, "not find vmax_vol"); + else + aw_monitor_update_vmax_to_dsp(dev, monitor, vmax_set); + + monitor->timer_cnt = 0; + monitor->vbat_sum = 0; + } +} + +static void aw_monitor_work_func(struct work_struct *work) +{ + int ret = 0; + struct aw87xxx *aw87xxx = container_of(work, + struct aw87xxx, monitor.with_dsp_work.work); + struct device *dev = aw87xxx->dev; + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + AW_DEV_LOGD(dev, "enter"); + + if (monitor->esd_enable) { + ret = aw_monitor_chip_esd_check_work(aw87xxx); + if (ret < 0) + return; + } + + if (monitor_hdr->monitor_switch && !(aw87xxx->aw_dev.is_rec_mode) && + monitor->open_dsp_en && monitor->bin_status == AW_ACF_UPDATE) { + AW_DEV_LOGD(dev, "start low power protection"); + aw_monitor_with_dsp_vmax_work(dev, monitor); + } + + if (monitor->esd_enable || (monitor_hdr->monitor_switch && + !(aw87xxx->aw_dev.is_rec_mode) && monitor->open_dsp_en && + monitor->bin_status == AW_ACF_UPDATE)) { + schedule_delayed_work(&monitor->with_dsp_work, + msecs_to_jiffies(monitor_hdr->monitor_time)); + } +} + +void aw87xxx_monitor_stop(struct aw_monitor *monitor) +{ + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + cancel_delayed_work_sync(&monitor->with_dsp_work); +} + +void aw87xxx_monitor_start(struct aw_monitor *monitor) +{ + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + int ret = 0; + + ret = aw87xxx_dev_check_reg_is_rec_mode(&aw87xxx->aw_dev); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "get reg current mode failed"); + return; + } + + if (monitor->esd_enable || (monitor->monitor_hdr.monitor_switch && + !(aw87xxx->aw_dev.is_rec_mode) && monitor->open_dsp_en + && monitor->bin_status == AW_ACF_UPDATE)) { + + AW_DEV_LOGD(aw87xxx->dev, "enter"); + monitor->pre_vmax = AW_VMAX_INIT_VAL; + monitor->first_entry = AW_FIRST_ENTRY; + monitor->timer_cnt = 0; + monitor->vbat_sum = 0; + + schedule_delayed_work(&monitor->with_dsp_work, + msecs_to_jiffies(monitor->monitor_hdr.monitor_time)); + } +} +/*************************************************************************** + * + * aw87xxx no dsp monitor func + * + ***************************************************************************/ +int aw87xxx_monitor_no_dsp_get_vmax(struct aw_monitor *monitor, int32_t *vmax) +{ + int vbat_capacity = 0; + int ret = -1; + int vmax_vol = 0; + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + struct device *dev = aw87xxx->dev; + + ret = aw_monitor_get_battery_capacity(dev, monitor, &vbat_capacity); + if (ret < 0) + return ret; + + if (monitor->custom_capacity) + vbat_capacity = monitor->custom_capacity; + AW_DEV_LOGI(dev, "get_battery_capacity is[%d]", vbat_capacity); + + ret = aw_search_vmax_from_table(dev, monitor, + vbat_capacity, &vmax_vol); + if (ret < 0) { + AW_DEV_LOGE(dev, "not find vmax_vol"); + return ret; + } + + *vmax = vmax_vol; + return 0; +} + + +/*************************************************************************** + * + * aw87xxx monitor sysfs nodes + * + ***************************************************************************/ +static ssize_t aw_attr_get_esd_enable(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + if (monitor->esd_enable) { + AW_DEV_LOGI(aw87xxx->dev, "esd-enable=true"); + len += snprintf(buf + len, PAGE_SIZE - len, + "esd-enable=true\n"); + } else { + AW_DEV_LOGI(aw87xxx->dev, "esd-enable=false"); + len += snprintf(buf + len, PAGE_SIZE - len, + "esd-enable=false\n"); + } + + return len; +} + +static ssize_t aw_attr_set_esd_enable(struct device *dev, + struct device_attribute *attr, const char *buf, size_t len) +{ + char esd_enable[AW_ESD_ENABLE_STRLEN] = {0}; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + if (strlen(buf) > AW_ESD_ENABLE_STRLEN) { + AW_DEV_LOGE(aw87xxx->dev, "input esd_enable_str_len is out of max[%d]", + AW_ESD_ENABLE_STRLEN); + return -EINVAL; + } + + if (sscanf(buf, "%s", esd_enable) == 1) { + AW_DEV_LOGD(aw87xxx->dev, "input esd-enable=[%s]", esd_enable); + if (!strcmp(esd_enable, "true")) + monitor->esd_enable = AW_ESD_ENABLE; + else + monitor->esd_enable = AW_ESD_DISABLE; + AW_DEV_LOGI(dev, "set esd-enable=[%s]", + monitor->esd_enable ? "true" : "false"); + } else { + AW_DEV_LOGE(aw87xxx->dev, "input esd-enable error"); + return -EINVAL; + } + + return len; +} + +static ssize_t aw_attr_get_vbat(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + int ret = -1; + int vbat_capacity = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + if (monitor->custom_capacity == 0) { + ret = aw_monitor_get_battery_capacity(dev, monitor, + &vbat_capacity); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "get battery_capacity failed"); + return ret; + } + len += snprintf(buf + len, PAGE_SIZE - len, + "vbat capacity=%d\n", vbat_capacity); + } else { + len += snprintf(buf + len, PAGE_SIZE - len, + "vbat capacity=%d\n", + monitor->custom_capacity); + } + + return len; +} + +static ssize_t aw_attr_set_vbat(struct device *dev, + struct device_attribute *attr, const char *buf, size_t len) +{ + int ret = -1; + int capacity = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + ret = kstrtouint(buf, 0, &capacity); + if (ret < 0) + return ret; + AW_DEV_LOGI(aw87xxx->dev, "set capacity = %d", capacity); + if (capacity >= AW_VBAT_CAPACITY_MIN && + capacity <= AW_VBAT_CAPACITY_MAX){ + monitor->custom_capacity = capacity; + } else { + AW_DEV_LOGE(aw87xxx->dev, "vbat_set=invalid,please input value [%d-%d]", + AW_VBAT_CAPACITY_MIN, AW_VBAT_CAPACITY_MAX); + return -EINVAL; + } + + return len; +} + +static ssize_t aw_attr_get_vmax(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + int ret = -1; + int vbat_capacity = 0; + int vmax_get = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + if (monitor->open_dsp_en) { + ret = aw87xxx_dsp_get_vmax(&vmax_get, aw87xxx->dev_index); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, + "get dsp vmax fail, ret=%d", ret); + return ret; + } + len += snprintf(buf + len, PAGE_SIZE - len, + "get_vmax=%d\n", vmax_get); + } else { + ret = aw_monitor_get_battery_capacity(dev, monitor, + &vbat_capacity); + if (ret < 0) + return ret; + AW_DEV_LOGI(aw87xxx->dev, "get_battery_capacity is [%d]", + vbat_capacity); + + if (monitor->custom_capacity) { + vbat_capacity = monitor->custom_capacity; + AW_DEV_LOGI(aw87xxx->dev, "get custom_capacity is [%d]", + vbat_capacity); + } + + ret = aw_search_vmax_from_table(aw87xxx->dev, monitor, + vbat_capacity, &vmax_get); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "not find vmax_vol"); + len += snprintf(buf + len, PAGE_SIZE - len, + "not_find_vmax_vol\n"); + return len; + } + len += snprintf(buf + len, PAGE_SIZE - len, + "0x%x\n", vmax_get); + AW_DEV_LOGI(aw87xxx->dev, "0x%x", vmax_get); + } + + return len; +} + +static ssize_t aw_attr_set_vmax(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + uint32_t vmax_set = 0; + int ret = -1; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + ret = kstrtouint(buf, 0, &vmax_set); + if (ret < 0) + return ret; + + AW_DEV_LOGI(aw87xxx->dev, "vmax_set=0x%x", vmax_set); + + if (monitor->open_dsp_en) { + ret = aw87xxx_dsp_set_vmax(vmax_set, aw87xxx->dev_index); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "send dsp_msg error, ret = %d", + ret); + return ret; + } + msleep(2); + } else { + AW_DEV_LOGE(aw87xxx->dev, "no_dsp system,vmax_set invalid"); + return -EINVAL; + } + + return count; +} + +static ssize_t aw_attr_get_monitor_switch(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + len += snprintf(buf + len, PAGE_SIZE - len, + "aw87xxx monitor switch: %u\n", + monitor_hdr->monitor_switch); + return len; +} + + +int aw87xxx_dev_monitor_switch_set(struct aw_monitor *monitor, uint32_t enable) +{ + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + AW_DEV_LOGI(aw87xxx->dev, "monitor switch set =%d", enable); + + if (!monitor->bin_status) { + AW_DEV_LOGE(aw87xxx->dev, "bin parse faile or not loaded,set invalid"); + return -EINVAL; + } + + if (monitor_hdr->monitor_switch == enable) + return 0; + + if (enable > 0) { + monitor_hdr->monitor_switch = 1; + if (monitor->open_dsp_en) { + monitor->pre_vmax = AW_VMAX_INIT_VAL; + monitor->first_entry = AW_FIRST_ENTRY; + monitor->timer_cnt = 0; + monitor->vbat_sum = 0; + } + } else { + monitor_hdr->monitor_switch = 0; + } + + return 0; +} + +static ssize_t aw_attr_set_monitor_switch(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + uint32_t enable = 0; + int ret = -1; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + + ret = kstrtouint(buf, 0, &enable); + if (ret < 0) + return ret; + + ret = aw87xxx_dev_monitor_switch_set(monitor, enable); + if (ret) + return ret; + + return count; +} + +static ssize_t aw_attr_get_monitor_time(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + len += snprintf(buf + len, PAGE_SIZE - len, + "aw_monitor_timer = %u(ms)\n", + monitor_hdr->monitor_time); + return len; +} + +static ssize_t aw_attr_set_monitor_time(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + unsigned int timer_val = 0; + int ret = -1; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + ret = kstrtouint(buf, 0, &timer_val); + if (ret < 0) + return ret; + + AW_DEV_LOGI(aw87xxx->dev, "input monitor timer=%d(ms)", timer_val); + + if (!monitor->bin_status) { + AW_DEV_LOGE(aw87xxx->dev, "bin parse faile or not loaded,set invalid"); + return -EINVAL; + } + + if (timer_val != monitor_hdr->monitor_time) + monitor_hdr->monitor_time = timer_val; + else + AW_DEV_LOGI(aw87xxx->dev, "no_change monitor_time"); + + return count; +} + +static ssize_t aw_attr_get_monitor_count(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t len = 0; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + len += snprintf(buf + len, PAGE_SIZE - len, + "aw_monitor_count = %u\n", + monitor_hdr->monitor_count); + return len; +} + +static ssize_t aw_attr_set_monitor_count(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + unsigned int monitor_count = 0; + int ret = -1; + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + struct aw_monitor_header *monitor_hdr = &monitor->monitor_hdr; + + ret = kstrtouint(buf, 0, &monitor_count); + if (ret < 0) + return ret; + AW_DEV_LOGI(aw87xxx->dev, "input monitor count=%d", monitor_count); + + if (!monitor->bin_status) { + AW_DEV_LOGE(aw87xxx->dev, "bin parse faile or not loaded,set invalid"); + return -EINVAL; + } + + if (monitor_count != monitor_hdr->monitor_count) + monitor_hdr->monitor_count = monitor_count; + else + AW_DEV_LOGI(aw87xxx->dev, "no_change monitor_count"); + + return count; +} + + +static ssize_t aw_attr_get_rx(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + ssize_t len = 0; + int ret = -1; + uint32_t enable = 0; + + if (monitor->open_dsp_en) { + ret = aw87xxx_dsp_get_rx_module_enable(&enable); + if (ret) { + AW_DEV_LOGE(aw87xxx->dev, "dsp_msg error, ret=%d", ret); + return ret; + } + len += snprintf(buf + len, PAGE_SIZE - len, + "aw87xxx rx: %u\n", enable); + } else { + len += snprintf(buf + len, PAGE_SIZE - len, + "command is invalid\n"); + } + + return len; +} + +static ssize_t aw_attr_set_rx(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct aw87xxx *aw87xxx = dev_get_drvdata(dev); + struct aw_monitor *monitor = &aw87xxx->monitor; + int ret = -1; + uint32_t enable; + + ret = kstrtouint(buf, 0, &enable); + if (ret < 0) + return ret; + + if (monitor->open_dsp_en) { + AW_DEV_LOGI(aw87xxx->dev, "set rx enable=%d", enable); + + ret = aw87xxx_dsp_set_rx_module_enable(enable); + if (ret < 0) { + AW_DEV_LOGE(aw87xxx->dev, "dsp_msg error, ret=%d", + ret); + return ret; + } + } else { + AW_DEV_LOGE(aw87xxx->dev, "command is invalid"); + return -EINVAL; + } + + return count; +} + + +static DEVICE_ATTR(esd_enable, S_IWUSR | S_IRUGO, + aw_attr_get_esd_enable, aw_attr_set_esd_enable); +static DEVICE_ATTR(vbat, S_IWUSR | S_IRUGO, + aw_attr_get_vbat, aw_attr_set_vbat); +static DEVICE_ATTR(vmax, S_IWUSR | S_IRUGO, + aw_attr_get_vmax, aw_attr_set_vmax); + +static DEVICE_ATTR(monitor_switch, S_IWUSR | S_IRUGO, + aw_attr_get_monitor_switch, aw_attr_set_monitor_switch); +static DEVICE_ATTR(monitor_time, S_IWUSR | S_IRUGO, + aw_attr_get_monitor_time, aw_attr_set_monitor_time); +static DEVICE_ATTR(monitor_count, S_IWUSR | S_IRUGO, + aw_attr_get_monitor_count, aw_attr_set_monitor_count); +static DEVICE_ATTR(rx, S_IWUSR | S_IRUGO, + aw_attr_get_rx, aw_attr_set_rx); + +static struct attribute *aw_monitor_vol_adjust[] = { + &dev_attr_esd_enable.attr, + &dev_attr_vbat.attr, + &dev_attr_vmax.attr, + NULL +}; + +static struct attribute_group aw_monitor_vol_adjust_group = { + .attrs = aw_monitor_vol_adjust, +}; + +static struct attribute *aw_monitor_control[] = { + &dev_attr_monitor_switch.attr, + &dev_attr_monitor_time.attr, + &dev_attr_monitor_count.attr, + &dev_attr_rx.attr, + NULL +}; + +static struct attribute_group aw_monitor_control_group = { + .attrs = aw_monitor_control, +}; + +/*************************************************************************** + * + * aw87xxx monitor init + * + ***************************************************************************/ +static void aw_monitor_dtsi_parse(struct device *dev, + struct aw_monitor *monitor, + struct device_node *dev_node) +{ + int ret = -1; + const char *esd_enable; + + ret = of_property_read_string(dev_node, "esd-enable", &esd_enable); + if (ret < 0) { + AW_DEV_LOGI(dev, "esd_enable parse failed, user default[disable]"); + monitor->esd_enable = AW_ESD_DISABLE; + } else { + if (!strcmp(esd_enable, "true")) + monitor->esd_enable = AW_ESD_ENABLE; + else + monitor->esd_enable = AW_ESD_DISABLE; + + AW_DEV_LOGI(dev, "parse esd-enable=[%s]", + monitor->esd_enable ? "true" : "false"); + } +} + +void aw87xxx_monitor_init(struct device *dev, struct aw_monitor *monitor, + struct device_node *dev_node) +{ + int ret = -1; + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + + monitor->dev_index = aw87xxx->dev_index; + monitor->monitor_hdr.monitor_time = AW_DEFAULT_MONITOR_TIME; + + aw_monitor_dtsi_parse(dev, monitor, dev_node); + + /* get platform open dsp type */ + monitor->open_dsp_en = aw87xxx_dsp_isEnable(); + + ret = sysfs_create_group(&dev->kobj, &aw_monitor_vol_adjust_group); + if (ret < 0) + AW_DEV_LOGE(dev, "failed to create monitor vol_adjust sysfs nodes"); + + INIT_DELAYED_WORK(&monitor->with_dsp_work, aw_monitor_work_func); + + if (monitor->open_dsp_en) { + ret = sysfs_create_group(&dev->kobj, &aw_monitor_control_group); + if (ret < 0) + AW_DEV_LOGE(dev, "failed to create monitor dsp control sysfs nodes"); + } + + if (!ret) + AW_DEV_LOGI(dev, "monitor init succeed"); +} + +void aw87xxx_monitor_exit(struct aw_monitor *monitor) +{ + struct aw87xxx *aw87xxx = + container_of(monitor, struct aw87xxx, monitor); + /*rm attr node*/ + sysfs_remove_group(&aw87xxx->dev->kobj, + &aw_monitor_vol_adjust_group); + + aw87xxx_monitor_stop(monitor); + + if (monitor->open_dsp_en) { + sysfs_remove_group(&aw87xxx->dev->kobj, + &aw_monitor_control_group); + } +} + diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_monitor.h b/sound/soc/codecs/aw87xxx/aw87xxx_monitor.h new file mode 100644 index 000000000000..daf9f2bfa09f --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_monitor.h @@ -0,0 +1,96 @@ +#ifndef __AW87XXX_MONITOR_H__ +#define __AW87XXX_MONITOR_H__ + +#define AW_WAIT_DSP_OPEN_TIME (3000) +#define AW_VBAT_CAPACITY_MIN (0) +#define AW_VBAT_CAPACITY_MAX (100) +#define AW_VMAX_INIT_VAL (0xFFFFFFFF) +#define AW_VBAT_MAX (100) +#define AW_VMAX_MAX (0) +#define AW_DEFAULT_MONITOR_TIME (3000) +#define AW_WAIT_TIME (3000) +#define REG_STATUS_CHECK_MAX (10) +#define AW_ESD_CHECK_DELAY (1) + +#define AW_ESD_ENABLE (true) +#define AW_ESD_DISABLE (false) +#define AW_ESD_ENABLE_STRLEN (16) + +enum aw_monitor_init { + AW_MONITOR_CFG_WAIT = 0, + AW_MONITOR_CFG_OK = 1, +}; + +enum aw_monitor_hdr_info { + AW_MONITOR_HDR_DATA_SIZE = 0x00000004, + AW_MONITOR_HDR_DATA_BYTE_LEN = 0x00000004, +}; + +enum aw_monitor_data_ver { + AW_MONITOR_DATA_VER = 0x00000001, + AW_MONITOR_DATA_VER_MAX, +}; + +enum aw_monitor_first_enter { + AW_FIRST_ENTRY = 0, + AW_NOT_FIRST_ENTRY = 1, +}; + +struct aw_bin_header { + uint32_t check_sum; + uint32_t header_ver; + uint32_t bin_data_type; + uint32_t bin_data_ver; + uint32_t bin_data_size; + uint32_t ui_ver; + char product[8]; + uint32_t addr_byte_len; + uint32_t data_byte_len; + uint32_t device_addr; + uint32_t reserve[4]; +}; + +struct aw_monitor_header { + uint32_t monitor_switch; + uint32_t monitor_time; + uint32_t monitor_count; + uint32_t step_count; + uint32_t reserve[4]; +}; + +struct vmax_step_config { + uint32_t vbat_min; + uint32_t vbat_max; + int vmax_vol; +}; + +struct aw_monitor { + bool open_dsp_en; + bool esd_enable; + int32_t dev_index; + uint8_t first_entry; + uint8_t timer_cnt; + uint32_t vbat_sum; + int32_t custom_capacity; + uint32_t pre_vmax; + + int bin_status; + struct aw_monitor_header monitor_hdr; + struct vmax_step_config *vmax_cfg; + + struct delayed_work with_dsp_work; +}; + +void aw87xxx_monitor_cfg_free(struct aw_monitor *monitor); +int aw87xxx_monitor_bin_parse(struct device *dev, + char *monitor_data, uint32_t data_len); +void aw87xxx_monitor_stop(struct aw_monitor *monitor); +void aw87xxx_monitor_start(struct aw_monitor *monitor); +int aw87xxx_monitor_no_dsp_get_vmax(struct aw_monitor *monitor, + int32_t *vmax); +void aw87xxx_monitor_init(struct device *dev, struct aw_monitor *monitor, + struct device_node *dev_node); +void aw87xxx_monitor_exit(struct aw_monitor *monitor); +int aw87xxx_dev_monitor_switch_set(struct aw_monitor *monitor, uint32_t enable); + +#endif diff --git a/sound/soc/codecs/aw87xxx/aw87xxx_pid_18_reg.h b/sound/soc/codecs/aw87xxx/aw87xxx_pid_18_reg.h new file mode 100644 index 000000000000..74d6548db91e --- /dev/null +++ b/sound/soc/codecs/aw87xxx/aw87xxx_pid_18_reg.h @@ -0,0 +1,2315 @@ +#ifndef __AW87XXX_PID_18_REG_H__ +#define __AW87XXX_PID_18_REG_H__ + +/* registers list */ +#define AW87XXX_PID_18_CHIPID_REG (0x00) +#define AW87XXX_PID_18_SYSST_REG (0x01) +#define AW87XXX_PID_18_SYSINT_REG (0x02) +#define AW87XXX_PID_18_SYSCTRL_REG (0x03) +#define AW87XXX_PID_18_CPOC_REG (0x04) +#define AW87XXX_PID_18_CLASSD_REG (0x05) +#define AW87XXX_PID_18_MADPVTH_REG (0x06) +#define AW87XXX_PID_18_A3PARAM_REG (0x07) +#define AW87XXX_PID_18_A3A2PO_REG (0x08) +#define AW87XXX_PID_18_A2PARAM_REG (0x09) +#define AW87XXX_PID_18_A1PARAM_REG (0x0A) +#define AW87XXX_PID_18_POPCLK_REG (0x0B) +#define AW87XXX_PID_18_GTDRCPSS_REG (0x0C) +#define AW87XXX_PID_18_MULTI_REG (0x0D) +#define AW87XXX_PID_18_DFT1_REG (0x61) +#define AW87XXX_PID_18_DFT2_REG (0x62) +#define AW87XXX_PID_18_DFT3_REG (0x63) +#define AW87XXX_PID_18_DFT4_REG (0x64) +#define AW87XXX_PID_18_DFT5_REG (0x65) +#define AW87XXX_PID_18_DFT6_REG (0x66) + +#define AW87XXX_PID_18_CLASSD_DEFAULT (0x10) + +/******************************************** + * soft control info + * If you need to update this file, add this information manually + *******************************************/ +unsigned char aw87xxx_pid_18_softrst_access[2] = {0x00, 0xaa}; + +/******************************************** + * Register Access + *******************************************/ +#define AW87XXX_PID_18_REG_MAX (0x67) + +#define REG_NONE_ACCESS (0) +#define REG_RD_ACCESS (1 << 0) +#define REG_WR_ACCESS (1 << 1) + +const unsigned char aw87xxx_pid_18_reg_access[AW87XXX_PID_18_REG_MAX] = { + [AW87XXX_PID_18_CHIPID_REG] = (REG_RD_ACCESS), + [AW87XXX_PID_18_SYSST_REG] = (REG_RD_ACCESS), + [AW87XXX_PID_18_SYSINT_REG] = (REG_RD_ACCESS), + [AW87XXX_PID_18_SYSCTRL_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_CPOC_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_CLASSD_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_MADPVTH_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_A3PARAM_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_A3A2PO_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_A2PARAM_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_A1PARAM_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_POPCLK_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_GTDRCPSS_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_MULTI_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT1_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT2_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT3_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT4_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT5_REG] = (REG_RD_ACCESS | REG_WR_ACCESS), + [AW87XXX_PID_18_DFT6_REG] = (REG_RD_ACCESS), +}; + +/* detail information of registers begin */ +/* CHIPID (0x00) detail */ +/* IDCODE bit 7:0 (CHIPID 0x00) */ +#define AW87XXX_PID_18_IDCODE_START_BIT (0) +#define AW87XXX_PID_18_IDCODE_BITS_LEN (8) +#define AW87XXX_PID_18_IDCODE_MASK \ + (~(((1<regmap, MAX98388_R2000_SW_RESET, MAX98388_SOFT_RESET, MAX98388_SOFT_RESET); - if (ret) + + if (ret) { dev_err(dev, "Reset command failed. (ret:%d)\n", ret); + goto exit; + } + count = 0; while (count < 3) { usleep_range(10000, 11000); + /* Software Reset Verification */ ret = regmap_read(max98388->regmap, MAX98388_R22FF_REV_ID, ®); + if (!ret) { dev_info(dev, "Reset completed (retry:%d)\n", count); - return; + goto exit; } count++; } + dev_err(dev, "Reset failed. (ret:%d)\n", ret); + + +exit: + regcache_cache_only(max98388->regmap, true); + ret = regmap_update_bits(max98388->regmap, + MAX98388_R2000_SW_RESET, + MAX98388_SOFT_RESET, 0); + regcache_cache_only(max98388->regmap, false); } static int max98388_probe(struct snd_soc_component *component) @@ -419,6 +435,7 @@ static int max98388_probe(struct snd_soc_component *component) /* Software Reset */ max98388_reset(max98388, component->dev); + usleep_range(400, 1000); /* General channel source configuration */ regmap_write(max98388->regmap, @@ -812,6 +829,7 @@ static bool max98388_readable_register(struct device *dev, case MAX98388_R210E_AUTO_RESTART: case MAX98388_R210F_GLOBAL_EN: case MAX98388_R22FF_REV_ID: + case MAX98388_R2000_SW_RESET: return true; default: return false; @@ -824,6 +842,7 @@ static bool max98388_volatile_reg(struct device *dev, unsigned int reg) case MAX98388_R2001_INT_RAW1 ... MAX98388_R2005_INT_STATE2: case MAX98388_R210F_GLOBAL_EN: case MAX98388_R22FF_REV_ID: + case MAX98388_R2000_SW_RESET: return true; default: return false; @@ -867,6 +886,7 @@ static int max98388_resume(struct device *dev) regcache_cache_only(max98388->regmap, false); max98388_reset(max98388, dev); + usleep_range(400, 1000); regcache_sync(max98388->regmap); return 0; -- 2.52.0